From 148583cdaafc94445cb098881fd4f47bf940c200 Mon Sep 17 00:00:00 2001
From: Doghole 如:
+ *
+ *
+ * @param dataSource
+ */
+ public void initSQLiteLocation(DataSource dataSource) {
+ // 指定 sqlite 路径
+ if (dataSource instanceof HikariDataSource hikariDataSource) {
+
+ String filePath = hikariDataSource.getJdbcUrl();
+ if (filePath == null || !filePath.startsWith("jdbc:sqlite:")) {
+ log.warn("无法在 SQLite HikariDataSource 中找到合法 SQLite JDBC url, 数据库可能会加载失败。获取到的 jdbc-url: {}", filePath);
+ return;
+ }
+ filePath = filePath.substring("jdbc:sqlite:".length()).trim();
+
+ ClassPathResource original = new ClassPathResource(RESOURCE_PATH);
+ if (!original.exists()) {
+ log.warn("未找到 SQLite 资源: {}", RESOURCE_PATH);
+ return;
+ }
+ String protocol = EmoneyAutoApplication.class.getProtectionDomain().getCodeSource().getLocation().getProtocol();
+ boolean isJar = "jar".equals(protocol);
+
+ if (isJar) {
+ // 复制到外部 yml 指定路径,已存在则不复制
+ File dest = new File(filePath), parentDir = dest.getParentFile();
+ String destAbsolutePath = dest.getAbsolutePath();
+ if (!parentDir.exists() && !parentDir.mkdirs()) {
+ log.warn("无法创建放置 SQLite 文件的目录: {}", parentDir.getAbsolutePath());
+ return;
+ }
+
+ if (dest.exists()) {
+ // 已存在
+ log.warn("目标资源 {} 已存在,忽略", destAbsolutePath);
+ return;
+ }
+
+ try (InputStream in = getClass().getClassLoader().getResourceAsStream(RESOURCE_PATH)) {
+ if (in == null) {
+ log.warn("无法读取 SQLite 资源: {}", RESOURCE_PATH);
+ return;
+ }
+ Files.copy(in, dest.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING);
+ log.info("SQLite 数据库文件已复制至:{}", destAbsolutePath);
+ } catch (Exception e) {
+ log.warn("复制 SQLite 数据库文件失败", e);
+ }
+ }
+ else {
+ // 使用当前绝对路径
+ Path path = Path.of("src/main/resources", RESOURCE_PATH);
+ hikariDataSource.setJdbcUrl("jdbc:sqlite:" + path.toAbsolutePath().toString());
+ }
+ }
+ }
@Bean("sqliteSqlSessionFactory")
public SqlSessionFactory sqliteSqlSessionFactory(
@Qualifier("sqliteDataSource") DataSource dataSource) throws Exception {
+
+ initSQLiteLocation(dataSource);
+
MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
factory.setDataSource(dataSource);
diff --git a/src/main/java/quant/rich/emoney/controller/IndexControllerV1.java b/src/main/java/quant/rich/emoney/controller/IndexControllerV1.java
index f672ff3..c856ae7 100644
--- a/src/main/java/quant/rich/emoney/controller/IndexControllerV1.java
+++ b/src/main/java/quant/rich/emoney/controller/IndexControllerV1.java
@@ -54,7 +54,7 @@ public class IndexControllerV1 extends BaseController {
String newPassword,
String email) {
- if (passwordIsNotEmpty(newPassword)) {
+ if (EncryptUtils.passwordIsNotEmpty(newPassword)) {
if (!platformConfig.getPassword().equals(password)) {
throw RException.badRequest("密码错误");
}
@@ -75,10 +75,4 @@ public class IndexControllerV1 extends BaseController {
return false;
});
}
-
- static final String EMPTY_PASSWORD = EncryptUtils.sha3("", 224);
-
- static boolean passwordIsNotEmpty(String password) {
- return StringUtils.isNotEmpty(password) && !password.equalsIgnoreCase(EMPTY_PASSWORD);
- }
}
diff --git a/src/main/java/quant/rich/emoney/controller/LoginControllerV1.java b/src/main/java/quant/rich/emoney/controller/LoginControllerV1.java
index 722d650..84e48eb 100644
--- a/src/main/java/quant/rich/emoney/controller/LoginControllerV1.java
+++ b/src/main/java/quant/rich/emoney/controller/LoginControllerV1.java
@@ -61,7 +61,7 @@ public class LoginControllerV1 extends BaseController {
if (Objects.isNull(sessionCaptcha) || !captcha.equalsIgnoreCase(sessionCaptcha.toString())) {
throw new LoginException("验证码错误");
}
- if (StringUtils.isAnyBlank(username) || !passwordIsNotEmpty(password)) {
+ if (StringUtils.isAnyBlank(username) || !EncryptUtils.passwordIsNotEmpty(password)) {
throw new LoginException("用户名和密码不能为空");
}
if (!username.equals(platformConfig.getUsername())
@@ -81,7 +81,7 @@ public class LoginControllerV1 extends BaseController {
}
// 初始化流程
- if (StringUtils.isAnyBlank(username) || !passwordIsNotEmpty(password)) {
+ if (StringUtils.isAnyBlank(username) || !EncryptUtils.passwordIsNotEmpty(password)) {
throw new LoginException("用户名和密码不能为空");
}
platformConfig.setUsername(username).setPassword(password).setIsInited(true);
@@ -99,10 +99,4 @@ public class LoginControllerV1 extends BaseController {
return "redirect:/admin/v1/login";
}
- static final String EMPTY_PASSWORD = EncryptUtils.sha3("", 224);
-
- static boolean passwordIsNotEmpty(String password) {
- return StringUtils.isNotEmpty(password) && !password.equalsIgnoreCase(EMPTY_PASSWORD);
- }
-
}
diff --git a/src/main/java/quant/rich/emoney/controller/api/CommonAbilityControllerV1.java b/src/main/java/quant/rich/emoney/controller/api/CommonAbilityControllerV1.java
new file mode 100644
index 0000000..4fa0913
--- /dev/null
+++ b/src/main/java/quant/rich/emoney/controller/api/CommonAbilityControllerV1.java
@@ -0,0 +1,58 @@
+package quant.rich.emoney.controller.api;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.lang.NonNull;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.protobuf.nano.MessageNano;
+
+import org.apache.commons.lang3.StringUtils;
+import org.reflections.Reflections;
+
+import lombok.extern.slf4j.Slf4j;
+import nano.BaseResponse.Base_Response;
+import quant.rich.emoney.entity.sqlite.ProtocolMatch;
+import quant.rich.emoney.exception.RException;
+import quant.rich.emoney.interfaces.IQueryableEnum;
+import quant.rich.emoney.pojo.dto.EmoneyConvertResult;
+import quant.rich.emoney.pojo.dto.EmoneyProtobufBody;
+import quant.rich.emoney.service.sqlite.ProtocolMatchService;
+
+@RestController
+@RequestMapping("/api/v1/common")
+@Slf4j
+public class CommonAbilityControllerV1 {
+
+ @Autowired
+ Reflections reflections;
+
+ @GetMapping("/getIQueryableEnum")
+ public Map
guid = MD5(androidId)exIdentify.AndroidID = androidIdUser-AgentUser-AgentDeviceInfoConfig
+ * @see DeviceInfoConfig
+ */
+ @Setter(AccessLevel.PRIVATE)
+ @TableField(exist=false)
+ private String androidVersion;
+
+ /**
+ * 用于:X-Android-Agent = EMAPP/{emoneyVersion}(Android;{androidSdkLevel})osVersion = androidSdkLevelDeviceInfoConfig, 经由 AndroidSdkLevelConfig 转换,由本例代管
+ * @see DeviceInfoConfig
+ * @see AndroidSdkLevelConfig
+ */
+ @Setter(AccessLevel.PRIVATE)
+ @TableField(exist=false)
+ private String androidSdkLevel;
+
+ /**
+ * 用于:softwareType = softwareTypeDeviceInfoConfig,由本例代管
+ * @see DeviceInfoConfig
+ */
+ private String softwareType;
+
+ /**
+ * 用于:User-Agent = okHttpUserAgentUser-AgentUser-AgentDeviceInfoConfig, 由本例代为管理
+ * @see DeviceInfoConfig
+ */
+ private String deviceName;
+
+ /**
+ * 对应 build.prop 中 Build.FINGERPRINT, 用于:
+ * - 益盟登录接口
hardware = MD5(fingerprint)
+ * - 益盟登录接口
exIdentify.OSFingerPrint = fingerprint
+ *
+ * 注意最终生成的 exIdentify 是 jsonString, 且有对斜杠的转义
+ * 来源:DeviceInfoConfig, 由本例代为管理
+ * @see DeviceInfoConfig
+ *
+ */
+ private String fingerprint;
+
+
+ /**
+ * 对应 build.prop 中 Build.ID, 用于:
+ * - WebView
User-Agent
+ * - 非 WebView 图片
User-Agent
+ *
+ * 来源:DeviceInfoConfig, 由本例代为管理
+ * @see DeviceInfoConfig
+ *
+ */
+ @TableField(exist=false)
+ private String buildId;
+
+ /**
+ * 用于:
+ * - WebView
User-Agent
+ *
+ * 来源:ChromeVersionsConfig, 由本例代为管理
+ * @see ChromeVersionsConfig
+ */
+ private String chromeVersion = chromeVersionsConfig.getRandomChromeVersion();
+
+ /**
+ * 用于:
+ * - 益盟通讯接口请求头
X-Android-Agent =
+ * EMAPP/{emoneyVersion}(Android;{androidSdkLevel})
+ *
+ * 由程序版本决定
+ * 来源:本例管理
+ * @see EmoneyRequestConfig.androidSdkLevel
+ */
+ private String emoneyVersion = "5.8.1";
+
+ /**
+ * 用于:
+ * - 益盟通讯接口请求头
Emapp-ViewMode = emappViewMode
+ *
+ * 由程序决定, 一般默认为 "1"
+ * 来源:本例管理
+ */
+ private String emappViewMode = "1";
+
+ /**
+ * 从 deviceInfo 设置相关字段
+ * @param deviceInfo
+ * @return
+ */
+ public RequestInfo setRelativeFieldsFromDeviceInfo(DeviceInfo deviceInfo) {
+ if (deviceInfo == null) {
+ throw new NullPointerException("deviceInfo is null");
+ }
+ deviceName = deviceInfo.getModel();
+ androidVersion = deviceInfo.getVersionRelease();
+ androidSdkLevel = String.valueOf(androidSdkLevelConfig.getSdkLevel(androidVersion));
+ softwareType = deviceInfo.getDeviceType();
+ fingerprint = deviceInfo.getFingerprint();
+ buildId = deviceInfo.getBuildId();
+ return this;
+ }
+
+ /**
+ * 设置密码:
+ * - null or empty,保存空字符串
+ * - 尝试解密成功,说明是密文,直接保存
+ * - 尝试解密失败,说明是明文,加密保存
+ *
+ * @param password
+ * @return
+ */
+ public RequestInfo setPassword(String password) {
+ if (StringUtils.isEmpty(password)) {
+ this.password = "";
+ return this;
+ }
+ String tryDecryptPassword = EncryptUtils.decryptAesForEmoneyPassword(password);
+ if (tryDecryptPassword != null) {
+ this.password = password;
+ }
+ else {
+ this.password = EncryptUtils.encryptAesForEmoneyPassword(password);
+ }
+ return this;
+ }
+
+ /**
+ * 设置 fingerprint,该设置可能会影响相关字段
+ * @param fingerprint
+ * @return
+ * @see RequestInfo#setRelativeFieldsFromDeviceInfo
+ */
+ public RequestInfo setFingerprint(String fingerprint) {
+ if (ObjectUtils.allNotNull(deviceName, softwareType, fingerprint, androidSdkLevelConfig)) {
+ DeviceInfo deviceInfo = DeviceInfo.from(deviceName, fingerprint, softwareType);
+ setRelativeFieldsFromDeviceInfo(deviceInfo);
+ }
+ else {
+ this.fingerprint = fingerprint;
+ }
+ return this;
+ }
+
+
+ /**
+ * 根据当前配置获取 guid,用于益盟登录接口
+ * @return
+ */
+ @JsonIgnore
+ public String getGuid() {
+ return EncryptUtils.toMD5String(androidId);
+ }
+
+ /**
+ * 一般 Protobuf 请求 X-Android-Agent 头,由 emoneyVersion 和 androidSdkLevel 组成
+ * @return
+ */
+ @JsonIgnore
+ public String getXAndroidAgent() {
+ // EMAPP/{emoneyVersion}(Android;{androidSdkLevel})
+ return
+ new StringBuilder()
+ .append("EMAPP/")
+ .append(getEmoneyVersion())
+ .append("(Android;")
+ .append(getAndroidSdkLevel())
+ .append(")").toString();
+ }
+
+ /**
+ * 用于 App 内用到 Webview 的地方
+ * @return
+ */
+ @JsonIgnore
+ public String getWebviewUserAgent() {
+ return new StringBuilder()
+ .append("Mozilla/5.0 (Linux; Android ")
+ .append(getAndroidVersion())
+ .append("; ")
+ .append(getDeviceName())
+ .append(" Build/")
+ .append(getBuildId())
+ .append("; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/")
+ .append(getChromeVersion())
+ .append(" Mobile Safari/537.36")
+ .toString();
+ }
+
+ /**
+ * 用于 App 内少量未用到 Webview 的地方,如首页获取图片等
+ * @return
+ */
+ @JsonIgnore
+ public String getNonWebviewResourceUserAgent() {
+ // Dalvik/2.1.0 (Linux; U; Android {安卓版本};{Build.DEVICE} Build/{Build.ID})
+ return new StringBuilder()
+ .append("Dalvik/2.1.0 (Linux; U; Android ")
+ .append(getAndroidVersion())
+ .append(";")
+ .append(getDeviceName())
+ .append(" Build/")
+ .append(getBuildId())
+ .append(")")
+ .toString();
+ }
+ /**
+ * 根据当前配置获取 hardware,用于益盟登录接口
+ * @return
+ */
+ @JsonIgnore
+ public String getHardware() {
+ return EncryptUtils.toMD5String(getFingerprint());
+ }
+
+ /**
+ * 根据本例信息(包括保存的用户名和密码)生成一个用于登录的 ObjectNode
+ * @return
+ */
+ @JsonIgnore
+ public ObjectNode getUsernamePasswordLoginObject() {
+ return getUsernamePasswordLoginObject(username, password);
+ }
+
+ /**
+ * 根据指定用户名、密码和本例信息生成一个用于登录的 ObjectNode
+ * @param username 用户名
+ * @param password 密码(可以是加密过的,也可以是明文)
+ * @return
+ */
+ public ObjectNode getUsernamePasswordLoginObject(String username, String password) {
+
+ if (StringUtils.isAnyBlank(username, password)) {
+ throw new RuntimeException("Try to generate a emoney login object but username and/or password is blank");
+ }
+
+ ObjectNode node = getAnonymousLoginObject();
+ node.put("accId", username);
+ node.put("accType", 1);
+
+ // 尝试解密 password 看是否成功,如果成功说明原本就已经是加密了的
+ String tryDecryptPassword = EncryptUtils.decryptAesForEmoneyPassword(password);
+
+ node.put("pwd",
+ tryDecryptPassword != null ? password :
+ EncryptUtils.encryptAesForEmoneyPassword(password)
+ );
+
+ return node;
+ }
+
+ /**
+ * 根据本例信息生成一个用于匿名登录的 ObjectNode
+ * @return
+ */
+ @JsonIgnore
+ public ObjectNode getAnonymousLoginObject() {
+ ObjectMapper mapper = new ObjectMapper();
+ ObjectNode node = mapper.createObjectNode();
+ ObjectNode exIdentify = mapper.createObjectNode();
+ exIdentify.put("IMEI", "");
+ exIdentify.put("AndroidID", getAndroidId());
+ exIdentify.put("MAC", "");
+ exIdentify.put("OSFingerPrint", getFingerprint());
+ String exIdentifyString = exIdentify.toString().replace("/", "\\/");
+
+ // 这玩意最好按照顺序来,当前这个顺序是 5.8.1 的顺序
+ String guid = getGuid();
+ node.put("appVersion", getEmoneyVersion());
+ node.put("productId", 4);
+ node.put("softwareType", getSoftwareType());
+ node.put("deviceName", getDeviceName());
+ node.put("ssid", "0");
+ node.put("platform", "android");
+ node.put("exIdentify", exIdentifyString);
+ node.put("osVersion", getAndroidSdkLevel());
+ node.put("accId", guid);
+ node.put("guid", guid);
+ node.put("accType", 4);
+ node.put("pwd", "");
+ node.put("channelId", "1711");
+ node.put("hardware", getHardware());
+
+ return node;
+ }
+
+ /**
+ * 根据本例信息获取 Relogin ObjectNode
+ * @return 如果 authorization 和 uid 任意 null 则本例返回 null
+ */
+ @JsonIgnore
+ public ObjectNode getReloginObject() {
+
+ if (ObjectUtils.anyNull(getAuthorization(), getUid())) {
+ return null;
+ }
+
+ ObjectMapper mapper = new ObjectMapper();
+ ObjectNode node = mapper.createObjectNode();
+ ObjectNode exIdentify = mapper.createObjectNode();
+ exIdentify.put("IMEI", "");
+ exIdentify.put("AndroidID", getAndroidId());
+ exIdentify.put("MAC", "");
+ exIdentify.put("OSFingerPrint", getFingerprint());
+ String exIdentifyString = exIdentify.toString().replace("/", "\\/");
+
+ // 这玩意最好按照顺序来,当前这个顺序是 5.8.1 的顺序
+ String guid = getGuid();
+ node.put("appVersion", getEmoneyVersion());
+ node.put("productId", 4);
+ node.put("softwareType", getSoftwareType());
+ node.put("deviceName", getDeviceName());
+ node.put("ssid", "0");
+ node.put("platform", "android");
+ node.put("token", getAuthorization()); // 和登录不同的地方: token
+ node.put("exIdentify", exIdentifyString);
+ node.put("uid", getUid()); // 和登录不同的地方: uid
+ node.put("osVersion", getAndroidSdkLevel());
+ node.put("guid", guid);
+ node.put("channelId", "1711");
+ node.put("hardware", getHardware());
+
+ return node;
+ }
+
+}
diff --git a/src/main/java/quant/rich/emoney/interceptor/EnumOptionsInterceptor.java b/src/main/java/quant/rich/emoney/interceptor/EnumOptionsInterceptor.java
index 2c94c88..5369b6f 100644
--- a/src/main/java/quant/rich/emoney/interceptor/EnumOptionsInterceptor.java
+++ b/src/main/java/quant/rich/emoney/interceptor/EnumOptionsInterceptor.java
@@ -97,7 +97,15 @@ public class EnumOptionsInterceptor implements HandlerInterceptor {
return true;
}
-
+ /**
+ * 将其注解到类型为 enum 的字段上,可以将 enum 注入到 Model 中,供 thymeleaf 使用
+ * 例:某 entity 的某字段为:
+ * private Proxy.Type proxyType;
+ * 则在其上注解 @EnumOptions("ProxyTypeEnum"), 即可在 thymeleaf 中直接使用:
+ * ProxyTypeEnum.DIRECT
+ * 如果只是注解 @EnumOptions,未指定 value,则在 thymeleaf 为字段名 + Options,上例中便为:
+ * ProxyTypeOptions.DIRECT
+ */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
diff --git a/src/main/java/quant/rich/emoney/interfaces/ConfigInfo.java b/src/main/java/quant/rich/emoney/interfaces/ConfigInfo.java
index 3534303..0f9694d 100644
--- a/src/main/java/quant/rich/emoney/interfaces/ConfigInfo.java
+++ b/src/main/java/quant/rich/emoney/interfaces/ConfigInfo.java
@@ -33,7 +33,7 @@ public @interface ConfigInfo {
/**
*
* 为 true 时,最终会调用其无参构造方法,若需要填充默认值的,请在无参构造器中填充。
- * 当为 true 且无法载入配置文件而触发初始化时,若存在 ./conf/system/{field}.fallback.json 文件时,从 fallback
+ * 当为 true 且无法载入配置文件而触发初始化时,若存在 /conf/system/{field}.fallback.json 文件时,从 fallback
* 文件中初始化。fallback 仅参与初始化,不参与持久化
*
*
diff --git a/src/main/java/quant/rich/emoney/interfaces/IQueryableEnum.java b/src/main/java/quant/rich/emoney/interfaces/IQueryableEnum.java
new file mode 100644
index 0000000..8cf5024
--- /dev/null
+++ b/src/main/java/quant/rich/emoney/interfaces/IQueryableEnum.java
@@ -0,0 +1,11 @@
+package quant.rich.emoney.interfaces;
+
+public interface IQueryableEnum {
+
+ public default String getName() {
+ return name();
+ }
+ String name();
+ public String getNote();
+
+}
diff --git a/src/main/java/quant/rich/emoney/mapper/sqlite/ProxySettingMapper.java b/src/main/java/quant/rich/emoney/mapper/sqlite/ProxySettingMapper.java
new file mode 100644
index 0000000..ac131b3
--- /dev/null
+++ b/src/main/java/quant/rich/emoney/mapper/sqlite/ProxySettingMapper.java
@@ -0,0 +1,16 @@
+package quant.rich.emoney.mapper.sqlite;
+
+import org.apache.ibatis.annotations.Mapper;
+import org.springframework.stereotype.Component;
+
+import com.baomidou.dynamic.datasource.annotation.DS;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+
+import quant.rich.emoney.entity.sqlite.ProxySetting;
+
+@Component
+@Mapper
+@DS("sqlite")
+public interface ProxySettingMapper extends BaseMapper {
+
+}
diff --git a/src/main/java/quant/rich/emoney/mapper/sqlite/RequestInfoMapper.java b/src/main/java/quant/rich/emoney/mapper/sqlite/RequestInfoMapper.java
new file mode 100644
index 0000000..00faf21
--- /dev/null
+++ b/src/main/java/quant/rich/emoney/mapper/sqlite/RequestInfoMapper.java
@@ -0,0 +1,16 @@
+package quant.rich.emoney.mapper.sqlite;
+
+import org.apache.ibatis.annotations.Mapper;
+import org.springframework.stereotype.Component;
+
+import com.baomidou.dynamic.datasource.annotation.DS;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+
+import quant.rich.emoney.entity.sqlite.RequestInfo;
+
+@Component
+@Mapper
+@DS("sqlite")
+public interface RequestInfoMapper extends BaseMapper {
+
+}
diff --git a/src/main/java/quant/rich/emoney/pojo/dto/IndexDetail.java b/src/main/java/quant/rich/emoney/pojo/dto/IndexDetail.java
index 85c2800..b6e9de4 100644
--- a/src/main/java/quant/rich/emoney/pojo/dto/IndexDetail.java
+++ b/src/main/java/quant/rich/emoney/pojo/dto/IndexDetail.java
@@ -7,6 +7,18 @@ import lombok.experimental.Accessors;
public interface IndexDetail {
+ /**
+ * 设置原始文本内容
+ * @param original
+ */
+ public void setOriginal(String original);
+
+ /**
+ * 获取原始 json 文本内容
+ * @return
+ */
+ public String getOriginal();
+
/**
* 获取 indexName
* @return
diff --git a/src/main/java/quant/rich/emoney/pojo/dto/NonParamsIndexDetail.java b/src/main/java/quant/rich/emoney/pojo/dto/NonParamsIndexDetail.java
index 910c975..6f59e41 100644
--- a/src/main/java/quant/rich/emoney/pojo/dto/NonParamsIndexDetail.java
+++ b/src/main/java/quant/rich/emoney/pojo/dto/NonParamsIndexDetail.java
@@ -13,7 +13,6 @@ import lombok.experimental.Accessors;
import quant.rich.emoney.util.HtmlSanitizer;
@Data
-@Accessors(chain=true)
public class NonParamsIndexDetail implements IndexDetail {
@JsonView(IndexDetail.class)
@@ -24,6 +23,8 @@ public class NonParamsIndexDetail implements IndexDetail {
private String nameCode;
@JsonView(IndexDetail.class)
private List data = new ArrayList<>();
+ @JsonView(IndexDetail.class)
+ private String original;
@Data
@Accessors(chain=true)
diff --git a/src/main/java/quant/rich/emoney/pojo/dto/ParamsIndexDetail.java b/src/main/java/quant/rich/emoney/pojo/dto/ParamsIndexDetail.java
index e04e356..2f8c653 100644
--- a/src/main/java/quant/rich/emoney/pojo/dto/ParamsIndexDetail.java
+++ b/src/main/java/quant/rich/emoney/pojo/dto/ParamsIndexDetail.java
@@ -6,11 +6,9 @@ import java.util.List;
import com.fasterxml.jackson.annotation.JsonView;
import lombok.Data;
-import lombok.experimental.Accessors;
import quant.rich.emoney.util.HtmlSanitizer;
@Data
-@Accessors(chain=true)
public class ParamsIndexDetail implements IndexDetail {
@JsonView(IndexDetail.class)
@@ -21,15 +19,19 @@ public class ParamsIndexDetail implements IndexDetail {
private String code;
@JsonView(IndexDetail.class)
private List descriptions = new ArrayList<>();
+ @JsonView(IndexDetail.class)
+ private String original;
@Override
public String getIndexName() {
return name;
}
+
@Override
public String getIndexCode() {
return code;
}
+
@Override
public List getDetails() {
List list = new ArrayList<>();
@@ -38,6 +40,7 @@ public class ParamsIndexDetail implements IndexDetail {
});
return list;
}
+
@Override
public void sanitize() {
List descriptions = new ArrayList<>();
diff --git a/src/main/java/quant/rich/emoney/service/ConfigService.java b/src/main/java/quant/rich/emoney/service/ConfigService.java
index 65f15cb..de85878 100644
--- a/src/main/java/quant/rich/emoney/service/ConfigService.java
+++ b/src/main/java/quant/rich/emoney/service/ConfigService.java
@@ -1,9 +1,11 @@
package quant.rich.emoney.service;
import lombok.extern.slf4j.Slf4j;
+import quant.rich.emoney.EmoneyAutoApplication;
import quant.rich.emoney.entity.config.SmartViewWriter;
import quant.rich.emoney.interfaces.ConfigInfo;
import quant.rich.emoney.interfaces.IConfig;
+import quant.rich.emoney.util.SmartResourceResolver;
import quant.rich.emoney.util.SpringContextHolder;
import com.fasterxml.jackson.databind.DeserializationFeature;
@@ -11,10 +13,13 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
+import io.micrometer.core.instrument.util.IOUtils;
import jakarta.annotation.PostConstruct;
import java.io.IOException;
+import java.io.UncheckedIOException;
import java.lang.reflect.InvocationTargetException;
+import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -30,6 +35,7 @@ import org.reflections.scanners.Scanners;
import org.reflections.util.ConfigurationBuilder;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.io.ClassPathResource;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.stereotype.Service;
@@ -47,6 +53,7 @@ public class ConfigService implements InitializingBean {
@Autowired
Reflections reflections;
+ static final boolean isJar = "jar".equals(EmoneyAutoApplication.class.getProtectionDomain().getCodeSource().getLocation().getProtocol());
static final ObjectMapper mapper = new ObjectMapper();
static {
@@ -242,11 +249,12 @@ public class ConfigService implements InitializingBean {
if (info.save()) {
try {
String filePath = getConfigFilePath(field, false);
- Path dirPath = Paths.get(filePath).getParent();
- if (Files.notExists(dirPath)) {
- Files.createDirectories(dirPath);
- }
- Files.writeString(Path.of(filePath), configJoString);
+ SmartResourceResolver.saveText(filePath, configJoString);
+ //Path dirPath = Paths.get(filePath).getParent();
+ //if (Files.notExists(dirPath)) {
+ // Files.createDirectories(dirPath);
+ //}
+ //Files.writeString(Path.of(filePath), configJoString);
} catch (IOException e) {
log.error("Cannot write config to local file {}.json, error: {}", field, e.getMessage());
return false;
@@ -258,12 +266,21 @@ public class ConfigService implements InitializingBean {
return true;
}
+ /**
+ * 从指定路径获取配置文件并转换为实例对象
+ * @param
+ * @param path
+ * @param configClass
+ * @return
+ */
private > Config getFromFile(String path, Class configClass) {
String configString;
Config config = null;
- try {
- configString = Files.readString(Path.of(path), Charset.defaultCharset());
- } catch (IOException e) {
+
+ try {
+ // 此处只是读取文件,并不关心该文件是否可写
+ configString = IOUtils.toString(SmartResourceResolver.loadResource(path), Charset.defaultCharset());
+ } catch (UncheckedIOException e) {
String field = fieldClassCache.inverse().get(configClass);
log.warn("Cannot read config {}.json: {}", field, e.getMessage());
return config;
diff --git a/src/main/java/quant/rich/emoney/service/IndexDetailService.java b/src/main/java/quant/rich/emoney/service/IndexDetailService.java
index 11ea5d0..0b5cf8f 100644
--- a/src/main/java/quant/rich/emoney/service/IndexDetailService.java
+++ b/src/main/java/quant/rich/emoney/service/IndexDetailService.java
@@ -1,9 +1,11 @@
package quant.rich.emoney.service;
import java.io.IOException;
+import java.io.InputStream;
import java.io.Serializable;
import java.net.URI;
import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.MessageFormat;
@@ -30,6 +32,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
+import io.micrometer.core.instrument.util.IOUtils;
import lombok.extern.slf4j.Slf4j;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
@@ -44,7 +47,7 @@ import quant.rich.emoney.pojo.dto.IndexDetail;
import quant.rich.emoney.pojo.dto.NonParamsIndexDetail;
import quant.rich.emoney.pojo.dto.NonParamsIndexDetail.NonParamsIndexDetailData;
import quant.rich.emoney.util.EncryptUtils;
-import quant.rich.emoney.util.SpringContextHolder;
+import quant.rich.emoney.util.SmartResourceResolver;
import quant.rich.emoney.pojo.dto.ParamsIndexDetail;
/**
@@ -76,13 +79,17 @@ public class IndexDetailService {
*/
@CacheEvict(cacheNames="@indexDetailService.getIndexDetail(Serializable)", key="#indexCode.toString()")
public IndexDetail forceRefreshAndGetIndexDetail(Serializable indexCode) {
- Path path = getIndexDetailPath(indexCode);
- try {
- Files.deleteIfExists(path);
- } catch (IOException e) {
- String msg = MessageFormat.format("本地 IndexDetail 文件删除失败,path: {0}, msg: {1}", path.toString(), e.getLocalizedMessage());
- throw new RuntimeException(msg, e);
+
+ // 刷新的本质就是从网络获取,因为此处已经清理了缓存,所以直接从网络获取后再
+ // 走一次 getIndexDetail,获取到的就是从网络保存到了本地的,此时缓存也更新了
+
+ if (!hasParams(indexCode)) {
+ getNonParamsIndexDetailOnline(indexCode);
}
+ else {
+ getParamsIndexDetailOnline(indexCode);
+ }
+
return getIndexDetail(indexCode);
}
@@ -105,11 +112,11 @@ public class IndexDetailService {
private ParamsIndexDetail getParamsIndexDetail(Serializable indexCode) {
// 先判断本地有没有
- Path localFilePath = getIndexDetailPath(indexCode);
- if (Files.exists(localFilePath)) {
+ InputStream stream = getIndexDetailStream(indexCode);
+ if (stream != null) {
ParamsIndexDetail detail = null;
try {
- String str = Files.readString(localFilePath);
+ String str = IOUtils.toString(stream, StandardCharsets.UTF_8);
detail = mapper.readValue(str, ParamsIndexDetail.class);
} catch (IOException e) {
log.warn("无法获取本地无参数指标说明,将尝试重新从网络获取 indexCode: {}", indexCode, e);
@@ -122,6 +129,11 @@ public class IndexDetailService {
return getParamsIndexDetailOnline(indexCode);
}
+ /**
+ * 从网络获取有参指标详情
+ * @param indexCode
+ * @return
+ */
private ParamsIndexDetail getParamsIndexDetailOnline(Serializable indexCode) {
try {
@@ -147,13 +159,16 @@ public class IndexDetailService {
if (code == 0) {
ParamsIndexDetail detail = mapper.treeToValue(result.get("detail"), ParamsIndexDetail.class);
if (detail == null) {
- // 网络访问成功但为 null, 新建一空 detail
+ /** 网络访问成功但为 null, 新建一空 detail **/
detail = new ParamsIndexDetail();
detail.setCode(indexCode.toString());
detail.getDescriptions().add("该指标说明接口返回为空");
}
- // 清洗内容:凡是文本类型的内容的,都要清洗一遍,判断是否有脚本、
- // 视频和图片等,有的要清除并记录,以免有泄露网址、客户端 IP 风险
+ else {
+ detail.setOriginal(result.get("detail").toString());
+ }
+ /** 清洗内容:凡是文本类型的内容的,都要清洗一遍,判断是否有脚本、
+ 视频和图片等,有的要清除并记录,以免有泄露网址、客户端 IP 风险*/
detail.sanitize();
saveIndexDetail(detail);
return detail;
@@ -180,11 +195,11 @@ public class IndexDetailService {
*/
private NonParamsIndexDetail getNonParamsIndexDetail(Serializable indexCode) {
// 先判断本地有没有
- Path localFilePath = getIndexDetailPath(indexCode);
- if (Files.exists(localFilePath)) {
+ InputStream stream = getIndexDetailStream(indexCode);
+ if (stream != null) {
NonParamsIndexDetail detail = null;
try {
- String str = Files.readString(localFilePath);
+ String str = IOUtils.toString(stream, StandardCharsets.UTF_8);
detail = mapper.readValue(str, NonParamsIndexDetail.class);
}
catch (IOException e) {
@@ -270,6 +285,9 @@ public class IndexDetailService {
.header("Referer", url)
.header("Accept-Language", "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7");
List valids = new ArrayList<>();
+
+ // 循环获取脚本,一旦获取的脚本内正则匹配到包含无参
+ // 指标的文本,立即转换为 json、转换指标并结束循环
scriptLoop:
for (String scriptUrl : scripts) {
Request scriptRequest = scriptBuilder.url(scriptUrl).build();
@@ -298,6 +316,7 @@ public class IndexDetailService {
obj.has("nameCode") && obj.get("nameCode").isTextual() &&
obj.has("data") && obj.get("data").isArray()) {
NonParamsIndexDetail detail = mapper.treeToValue(obj, NonParamsIndexDetail.class);
+ detail.setOriginal(obj.toString());
valids.add(detail);
foundAny = true;
}
@@ -330,7 +349,7 @@ public class IndexDetailService {
if (!numericPattern.matcher(detail.getNameCode()).matches()) {
continue;
}
- Path path = getIndexDetailPath(detail);
+ String path = getIndexDetailPath(detail);
// 判断是否是需求的 detail
if (indexCode.toString().equals(detail.getIndexCode())) {
loadImages(detail);
@@ -339,11 +358,25 @@ public class IndexDetailService {
// 清洗内容:凡是文本类型的内容的,都要清洗一遍,判断是否有脚本、
// 视频和图片等,有的要清除并记录,以免有泄露网址、客户端 IP 风险
detail.sanitize();
-
- if (!Files.exists(path)) {
+
+ InputStream inputStream = SmartResourceResolver.loadResource(path);
+ if (inputStream == null) {
// 不存在则保存
saveIndexDetail(detail);
}
+ else {
+ // 判断 original 是否一致,不一致则更新
+ NonParamsIndexDetail existed;
+ try {
+ existed = mapper.readValue(inputStream, NonParamsIndexDetail.class);
+ if (!existed.getOriginal().equals(detail.getOriginal())) {
+ saveIndexDetail(detail);
+ }
+ }
+ catch (IOException e) {
+ log.debug("读取本地存在的 NonParamsIndexDetail 文件成功,但转换失败。格式错误?路径:{}", path, e);
+ }
+ }
}
if (targetDetail == null) {
@@ -354,9 +387,9 @@ public class IndexDetailService {
List items = List.of("该指标说明接口返回为空");
data.setItems(items);
targetDetail.getData().add(data);
- Path path = getIndexDetailPath(targetDetail);
+ String path = getIndexDetailPath(targetDetail);
- if (!Files.exists(path)) {
+ if (SmartResourceResolver.loadResource(path) == null) {
// 不存在则保存
saveIndexDetail(targetDetail);
}
@@ -431,16 +464,16 @@ public class IndexDetailService {
}
/**
- * 保存指标详情到本地文件
+ * 保存指标详情到本地文件,无论其原本是否存在
* @param
* @param des
*/
private void saveIndexDetail(Description des) {
SmartViewWriter writer = new SmartViewWriter();
String joString = writer.writeWithSmartView(des, IndexDetail.class);
- Path path = getIndexDetailPath(des);
+ String path = getIndexDetailPath(des);
try {
- Files.writeString(path, joString);
+ SmartResourceResolver.saveText(path, joString);
}
catch (IOException e) {
log.error("写入指标详情到 {} 失败", path.toString(), e);
@@ -453,12 +486,12 @@ public class IndexDetailService {
* @param description
* @return
*/
- private Path getIndexDetailPath(Description description) {
+ private String getIndexDetailPath(Description description) {
Path path = Path.of(new StringBuilder(filePath)
.append((description instanceof NonParamsIndexDetail) ? "nonParams/": "params/")
.append(description.getIndexCode())
.append(".json").toString());
- return path;
+ return path.normalize().toString();
}
/**
@@ -466,13 +499,14 @@ public class IndexDetailService {
* @param indexCode
* @return
*/
- private Path getIndexDetailPath(Serializable indexCode) {
+ private InputStream getIndexDetailStream(Serializable indexCode) {
boolean hasParams = hasParams(indexCode);
- Path path = Path.of(new StringBuilder(filePath)
+ String path = new StringBuilder(filePath)
.append(!hasParams ? "nonParams/": "params/")
.append(indexCode)
- .append(".json").toString());
- return path;
+ .append(".json").toString();
+ InputStream inputStream = SmartResourceResolver.loadResource(path);
+ return inputStream;
}
/**
diff --git a/src/main/java/quant/rich/emoney/service/sqlite/ProxySettingService.java b/src/main/java/quant/rich/emoney/service/sqlite/ProxySettingService.java
new file mode 100644
index 0000000..8672b2a
--- /dev/null
+++ b/src/main/java/quant/rich/emoney/service/sqlite/ProxySettingService.java
@@ -0,0 +1,12 @@
+package quant.rich.emoney.service.sqlite;
+
+import org.springframework.stereotype.Service;
+import com.baomidou.dynamic.datasource.annotation.DS;
+import quant.rich.emoney.entity.sqlite.ProxySetting;
+import quant.rich.emoney.mapper.sqlite.ProxySettingMapper;
+
+@DS("sqlite")
+@Service
+public class ProxySettingService extends SqliteServiceImpl {
+
+}
diff --git a/src/main/java/quant/rich/emoney/service/sqlite/RequestInfoService.java b/src/main/java/quant/rich/emoney/service/sqlite/RequestInfoService.java
new file mode 100644
index 0000000..8fcb10d
--- /dev/null
+++ b/src/main/java/quant/rich/emoney/service/sqlite/RequestInfoService.java
@@ -0,0 +1,14 @@
+package quant.rich.emoney.service.sqlite;
+
+import org.springframework.stereotype.Service;
+import com.baomidou.dynamic.datasource.annotation.DS;
+
+import quant.rich.emoney.entity.config.DeviceInfoConfig.DeviceInfo;
+import quant.rich.emoney.entity.sqlite.RequestInfo;
+import quant.rich.emoney.mapper.sqlite.RequestInfoMapper;
+
+@DS("sqlite")
+@Service
+public class RequestInfoService extends SqliteServiceImpl {
+
+}
diff --git a/src/main/java/quant/rich/emoney/util/CallerLockUtil.java b/src/main/java/quant/rich/emoney/util/CallerLockUtil.java
index 20ff425..72b8838 100644
--- a/src/main/java/quant/rich/emoney/util/CallerLockUtil.java
+++ b/src/main/java/quant/rich/emoney/util/CallerLockUtil.java
@@ -5,6 +5,7 @@ import java.lang.StackWalker;
import java.lang.ref.WeakReference;
import java.util.Arrays;
import java.util.Objects;
+import java.util.Optional;
import java.util.concurrent.*;
import java.util.concurrent.locks.ReentrantLock;
@@ -37,6 +38,44 @@ public class CallerLockUtil {
return new WeakReference<>(l);
}).get();
}
+
+ /**
+ * ✅ 方式三:尝试获取锁并运行,支持超时,失败后不阻塞
+ * @return true 表示成功执行,false 表示未获得锁
+ */
+ public static boolean tryRunWithCallerLock(Runnable task, long timeoutMs, Object... extraKeys) {
+ ReentrantLock lock = acquireLock(extraKeys);
+ boolean locked = false;
+ try {
+ locked = lock.tryLock(timeoutMs, TimeUnit.MILLISECONDS);
+ if (locked) {
+ task.run();
+ }
+ return locked;
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return false;
+ } finally {
+ if (locked) lock.unlock();
+ }
+ }
+
+ /**
+ * ✅ 非阻塞获取锁,超时失败返回 null 或抛异常
+ */
+ public static Optional tryCallWithCallerLock(Callable task, long timeoutMs, Object... extraKeys) {
+ ReentrantLock lock = acquireLock(extraKeys);
+ boolean locked = false;
+ try {
+ locked = lock.tryLock(timeoutMs, TimeUnit.MILLISECONDS);
+ if (!locked) return Optional.empty();
+ return Optional.of(task.call());
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ } finally {
+ if (locked) lock.unlock();
+ }
+ }
/**
* 构造调用者方法 + 附加参数为 key
diff --git a/src/main/java/quant/rich/emoney/util/EncryptUtils.java b/src/main/java/quant/rich/emoney/util/EncryptUtils.java
index aa4bd21..7c595ad 100644
--- a/src/main/java/quant/rich/emoney/util/EncryptUtils.java
+++ b/src/main/java/quant/rich/emoney/util/EncryptUtils.java
@@ -38,6 +38,16 @@ public class EncryptUtils {
private static final String EM_SIGN_MESS_2 = "994fec3c512f2f7756fd5e4403147f01";
private static final String SLASH = "/";
private static final String COLON = ":";
+ private static final String EMPTY_SHA3_224 = sha3("", 224);
+
+ /**
+ * 判断密码是否空字符串,或经过 SHA_224 加密过的空字符串
+ * @param password
+ * @return
+ */
+ public static boolean passwordIsNotEmpty(String password) {
+ return StringUtils.isNotEmpty(password) && !password.equalsIgnoreCase(EMPTY_SHA3_224);
+ }
/**
* 加密用于 Emoney 登录的密码
diff --git a/src/main/java/quant/rich/emoney/util/GeoIPUtil.java b/src/main/java/quant/rich/emoney/util/GeoIPUtil.java
index 2471924..725af2f 100644
--- a/src/main/java/quant/rich/emoney/util/GeoIPUtil.java
+++ b/src/main/java/quant/rich/emoney/util/GeoIPUtil.java
@@ -13,12 +13,12 @@ import quant.rich.emoney.client.OkHttpClientProvider;
import quant.rich.emoney.entity.config.ProxyConfig;
import quant.rich.emoney.pojo.dto.IpInfo;
-import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Proxy;
import java.util.concurrent.TimeUnit;
-import java.util.concurrent.locks.ReentrantLock;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.scheduling.annotation.Async;
@Slf4j
public class GeoIPUtil {
@@ -28,16 +28,16 @@ public class GeoIPUtil {
static {
try {
- cityReader = new DatabaseReader.Builder(new File("./conf/extra/GeoLite2-City.mmdb")).build();
+ ClassPathResource geoLite2CityResource = new ClassPathResource("/conf/extra/GeoLite2-City.mmdb");
+ cityReader = new DatabaseReader.Builder(geoLite2CityResource.getInputStream()).build();
} catch (IOException e) {
throw new RuntimeException("IP 地址库初始化失败", e);
}
}
+ @Async
public static IpInfo getIpInfoThroughProxy(ProxyConfig proxyConfig) {
- ReentrantLock lock = CallerLockUtil.acquireLock();
- lock.lock();
- try {
+ return CallerLockUtil.tryCallWithCallerLock(() -> {
Proxy proxy = proxyConfig.getProxy();
boolean ignoreHttpsVerification = proxyConfig.getIgnoreHttpsVerification();
// OkHttp 客户端配置
@@ -82,10 +82,7 @@ public class GeoIPUtil {
log.warn("Proxy ipv6 error {}", e.getMessage());
}
return queryIpInfoGeoLite(ipInfo);
- }
- finally {
- lock.unlock();
- }
+ }, 100, proxyConfig).orElse(IpInfo.EMPTY);
}
/**
diff --git a/src/main/java/quant/rich/emoney/util/SmartResourceResolver.java b/src/main/java/quant/rich/emoney/util/SmartResourceResolver.java
new file mode 100644
index 0000000..71668f8
--- /dev/null
+++ b/src/main/java/quant/rich/emoney/util/SmartResourceResolver.java
@@ -0,0 +1,111 @@
+package quant.rich.emoney.util;
+
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.*;
+
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+public class SmartResourceResolver {
+
+ private static RunningFrom runningFrom;
+
+ static {
+ if (isRunningFromJar()) {
+ runningFrom = RunningFrom.JAR;
+ }
+ else if (isRunningFromWar()) {
+ runningFrom = RunningFrom.WAR;
+ }
+ else {
+ runningFrom = RunningFrom.IDE;
+ }
+ }
+
+ /**
+ 获取资源
+
+
+ - JAR
+
+ - 优先以 jar 文件所在目录为基准,寻找相对路径外部文件
+ - 当外部文件不存在时,读取 classpath,即 jar 内部资源文件
+
+
+ - WAR 只获取 classpath 文件,即 /WEB-INF/classes/ 下文件
+ - IDE 只获取源文件,即 src/main/resources/ 下文件
+
+ * @param relativePath 相对路径
+ * @param writable 是否一定可写
+ * @return
+ */
+ public static InputStream loadResource(String relativePath) {
+ try {
+ Path externalPath = resolveExternalPath(relativePath);
+
+ if (externalPath != null && Files.exists(externalPath)) {
+ log.debug("从外部文件系统加载资源: {}", externalPath);
+ return Files.newInputStream(externalPath);
+ }
+
+ // 否则回退到 classpath(JAR、WAR、IDE)
+ InputStream in = SmartResourceResolver.class.getClassLoader().getResourceAsStream(relativePath);
+ if (in != null) {
+ log.debug("从 classpath 内部加载资源: {}", relativePath);
+ return in;
+ }
+
+ throw new FileNotFoundException("无法找到资源: " + relativePath);
+ } catch (Exception e) {
+ throw new RuntimeException("读取资源失败: " + relativePath, e);
+ }
+ }
+
+ public static void saveText(String relativePath, String content) throws IOException {
+ Path outputPath = resolveExternalPath(relativePath);
+ Files.createDirectories(outputPath.getParent()); // 确保目录存在
+ Files.writeString(outputPath, content, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
+ log.debug("写入外部资源文件成功: {}", outputPath);
+ }
+
+ private static Path resolveExternalPath(String relativePath) {
+ try {
+ Path basePath;
+ if (runningFrom == RunningFrom.JAR) {
+ basePath = Paths.get(SmartResourceResolver.class.getProtectionDomain()
+ .getCodeSource().getLocation().toURI()).getParent();
+ return basePath.resolve(relativePath).normalize();
+ } else if (runningFrom == RunningFrom.WAR) {
+ basePath = Paths.get(SmartResourceResolver.class.getProtectionDomain()
+ .getCodeSource().getLocation().toURI()); // e.g., WEB-INF/classes/
+ return basePath.resolve(relativePath).normalize();
+ } else {
+ // IDE 环境:返回 src/main/resources 下真实文件
+ return Paths.get("src/main/resources", relativePath).normalize();
+ }
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ private static boolean isRunningFromJar() {
+ String path = SmartResourceResolver.class.getResource(
+ SmartResourceResolver.class.getSimpleName() + ".class").toString();
+ return path.startsWith("jar:");
+ }
+
+ private static boolean isRunningFromWar() {
+ String path = SmartResourceResolver.class.getResource(
+ SmartResourceResolver.class.getSimpleName() + ".class").toString();
+ return path.contains("/WEB-INF/classes/");
+ }
+
+ private static enum RunningFrom {
+ JAR,
+ WAR,
+ IDE
+ }
+}
diff --git a/src/main/java/quant/rich/emoney/validator/ProxySettingValid.java b/src/main/java/quant/rich/emoney/validator/ProxySettingValid.java
new file mode 100644
index 0000000..ad46734
--- /dev/null
+++ b/src/main/java/quant/rich/emoney/validator/ProxySettingValid.java
@@ -0,0 +1,20 @@
+package quant.rich.emoney.validator;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Target;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import jakarta.validation.Constraint;
+import jakarta.validation.Payload;
+
+@Documented
+@Constraint(validatedBy = ProxySettingValidator.class)
+@Target({ ElementType.TYPE })
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ProxySettingValid {
+ String message() default "非法的 ProxySetting";
+ Class>[] groups() default {};
+ Class extends Payload>[] payload() default {};
+}
diff --git a/src/main/java/quant/rich/emoney/validator/ProxySettingValidator.java b/src/main/java/quant/rich/emoney/validator/ProxySettingValidator.java
new file mode 100644
index 0000000..798d35f
--- /dev/null
+++ b/src/main/java/quant/rich/emoney/validator/ProxySettingValidator.java
@@ -0,0 +1,33 @@
+package quant.rich.emoney.validator;
+
+import java.net.Proxy;
+import java.util.Objects;
+import org.apache.commons.lang3.StringUtils;
+
+import jakarta.validation.ConstraintValidator;
+import jakarta.validation.ConstraintValidatorContext;
+import quant.rich.emoney.entity.sqlite.ProxySetting;
+
+public class ProxySettingValidator implements IValidator, ConstraintValidator {
+
+ @Override
+ public boolean isValid(ProxySetting value, ConstraintValidatorContext context) {
+
+ if (value == null) return true;
+ if (!(value instanceof ProxySetting proxySetting)) return true;
+
+ if (proxySetting.getProxyType() != null && proxySetting.getProxyType() != Proxy.Type.DIRECT) {
+ if (StringUtils.isBlank(proxySetting.getProxyHost())) {
+ return invalid(context, "设置代理为 HTTP 或 SOCKS 时,代理地址不允许为空");
+ }
+ if (Objects.isNull(proxySetting.getProxyPort()) || proxySetting.getProxyPort() <= 0 || proxySetting.getProxyPort() > 65535) {
+ return invalid(context, "端口不合法");
+ }
+ // 不做连通性校验:有的代理可能先添加后生效
+ }
+
+ return true;
+
+ }
+
+}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 11b8b39..34a2fd6 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -19,7 +19,10 @@ spring:
devtools:
restart:
enabled: true
- additional-exclude: '**/*.html'
+ additional-exclude:
+ - '**/*.html'
+ - '**/*.js'
+ - '**/*.css'
additional-paths: lib/
jackson:
date-format: yyyy-MM-dd HH:mm:ss
diff --git a/src/main/resources/database.db b/src/main/resources/database.db
index 7ba0eee906a0249515c96e3dbf7d6f989603df3e..c469ec359ac7425eed7c801befbebb71cfcd92ed 100644
GIT binary patch
delta 1618
zcmbtU&2Jk;6yLR-de?M4F;!%_q3U9YLQ1;c@mJzh7I7O>8o9O!n<_mmYkTah;{E9E
z+QA^?8hWTGf&|mQ011Hvg2+Pj03!EZxgjKkNXrSNo_eSh#$E^8ktHgj(cAfW^WMDQ
zZ+^2k_Z>I)9Xr#$#{@xmusN`~u+8nxN6tPbxcVK2`k9!RI3&D}PJT>r6Nk^-a@HuW
z+xA+miPc;4KDU2nhP*v(Nrfsh(2W{0Em2m>+VI$QX)T=+(yWlWm`St2VONlSiSe*(
zuGM4
zrlQ7ROLPn%q)56h)@8IUDq86{B*rt>=BUqI%$JUD_gY^$uD(Y->-NvilJDnwg3t}^
zt%hhK%fi);OiqkSV#GJ13EMNEp?|Tx!~>FtS2s1Y&*|{0t{Hf>f2p=ZuBce1z3VW$
zEXZZK*jL5T<*!ibS5xa5f#o|No;*XkzzTU~uazd-kG$*T54)|Orkwb_(AX$!?!M6-
zAO8pNGTEBFiC@A%jsL2Ab8S;E!Hqi3V`N4zl?)>@9T^Np37IL|WqTeVqDxr<+b+o2r
z*REBP$wQck7Z6PFu|+(J7m8Q4f?P$~R#%Xr2nWMlIDc7ykk94wxsW{!^vOA6tTv
z&>MM};=+($gv~I=!+1#GN_;sCp;$~5qDx&xhYVCjl8Mf5p@s<~QI6;0iNqp@gJK-+
zAO^Bb1n5)DxWb>hz~+BeIIh6fMNHZZ_528FUw?LR@7}{tzj$!_d!#hr0v|~%EY=Fw
zB)*L_4PSeB-fivyh+!TZ_+7sV%y`J$gG<}XeYOmO;dFNeQV+RB{z~cP{Ob19e
r;h#E7(^yb2@|qyzt6EX2b{`ks@HgnN{BQPWcjxs_-gI?bJ5Rtrk7LR?
literal 40960
zcmeI*e{2&~00;2bwX8q3ySE?AZUo`h1;*GydfmDO4S~VR(on|SI*|}_IeN#qth*cS
zZ44JkHZdj=BP>w?|1bm-VvNC<_>caf2K`42iBbPj{xAkH{-g09WBkW=Ev)PM7#m2!
z@Vlnj^?UDL-~06Lb=Tg5!DkYNtxJck%&2Nh?WBshTx5qN5kjWfJ;3gFej3<<=gtH6
z+cj_bZi~}o%fxAc_7J~s3laYm-mQGC;$q;6|1)~Y_Z^Gy0|5v?00Izzz#}g(-6VK}
zEiJAYm#rR3>th+qwo+DFVShWMjubx%>>7x5Cu35wduJjhg^E{(q)kFVnwJ_{NHR=Y
zAJ#Kc-@c^OHZl%)lCkHKxwL;EzPEeefV3xeK-yHOo7K`H1X`wz
zN^dY0bIlCpTB#I2w_>F=B@&KA!yR(hV(a|HJy@*hgZ3wMNN0b6=}K157;4&hVHtf*
z2fV>}+%+?JufBFFkwv<|)_RxRUsjmgV_ToJm-6yGKzA*FzPktR-CcRn?#kuM=&sD|
zZoDrU>x~UO&ND%;H&|EaI#rz?VA85N#_p?%
zTkk}3tz^rJGkh$3HKh52ME6i~U%ZdivNzV3A3{Q@jAbhN_57r)Sgo|c<78+Io6ZPo
zXH?BFhZTpk6f(>p`^(CMt*x&7;gmg^W~aYw+&wz3n<;(4Lw6D2f|Z+`_oOzqWea=T
zRvzM8_rZ$tpcQwK+zxoV5$d>Y<#)(M2f2u{_#hbZ3F1GbLAXeSi}VV8i@r>=RH1ul
z7j2@H{wCfMzZE|f-xJ>yUlc9zpx7(6i|fS-;VJpbgX}KCdC-ZSKFDEPKWUDxthm*NESy^euj*8|b92u)C
za+_8Qe*fHVFm*y;Tb9uqgx;bz=od?<21OtM0SG_<0uX=z1Rwwb2tWV=5O|~n{JwH`
zoy)l_WKD3DTdpp>03;H1&i~m(|M-Cb1Rwwb2tWV=5P$##AOHafK;SVJ5a^fe`~Pi(
zZlkXVKQ8C?7^9$T5P$##AOHafKmY;|fB*zmm;mp>fA-I|eZTOccb0w5$=>H==Q-I+
zoa`tkd!CcUIa!pGHF7ecG$Vg=vYVXj8^NPNB*`wU)eo2XH9?FnIlqYM{}-aLUkcw6&AH@D*vqb{(prX)MzdQAOHaf
zKmY;|fB*y_009U<;64O!{(m26*aQL)fB*y_009U<00Izz00bbg!US^v!5`-Tudr7Q
z&4mC2AOHafKmY;|fB*y_009UrPr&*9zk+Tg^mqC(eTN>V8|f_licZr`IzWF|UR@{+
z0SG_<0uX=z1Rwwb2tWV=5Lnp)!W)K%tQX>@rcdrt)2W854Ia`Z42@;GO|8c|W@ZO%
z)i$!Wk;+zI(>$bE7_>90tq&g`w5`-g{p^H?M1@_dsipNnR%&FwVIRp=CteuvkVcZs
z=%$tn=()8X(nNNunPJs1dsJJko%MN0YraUrF!k7^%?b-=>sgz~vub)=cS?!p+4HiB
zj8XOssy&{qzJ8K5gzPoUaa-@t=o5y1Ogz)!6dJTLwyq^ugPrsLO1h5FKj=^NYx*I*
z#LoX`Xojw%`)LoASN7|M)UM9&8XxPm
zEG=7g{UnQuPO7V}X)G!};lJA8CSBam2ZRd)Zqisd-xtoats4uc?c%w$ZqhXG^uO{A
u!%f!DefAKXop6&V@1um8S)ZG<=4vi{Oi+2Yo^3t%DS>dNgFRc}{QqBz2+.layui-layer-content>*:not(:last-child) {
margin-bottom: 1em;
@@ -394,7 +395,7 @@ blockquote.layui-elem-quote {
display: block
}
.layui-layer-adminRight>.layui-layer-content {
- overflow: visible !important;
+ /* overflow: visible !important; */
}
.layui-anim-rl {
-webkit-animation-name: layui-rl;
diff --git a/src/main/resources/static/admin/v1/static/js/dog.js b/src/main/resources/static/admin/v1/static/js/dog.js
index 6070294..0fe6281 100644
--- a/src/main/resources/static/admin/v1/static/js/dog.js
+++ b/src/main/resources/static/admin/v1/static/js/dog.js
@@ -1,6 +1,12 @@
function InitDog() {
const dog = {};
- dog.error = ({msg = '服务器错误', time = 2000, onClose}) => {
+ dog.error = ({msg = '服务器错误', defaultMsg = '服务器错误', time = 2000, onClose}) => {
+ if (typeof msg === 'object') {
+ if (msg.responseJSON) {
+ const r = msg.responseJSON;
+ msg = r && r.data || defaultMsg
+ }
+ }
return layui.layer.msg(
'' + msg, {
offset: '15px',
@@ -10,7 +16,7 @@ function InitDog() {
end: onClose
})
};
- dog.success = ({msg = '操作成功', time = 2000, onClose}) => {
+ dog.success = ({msg = '操作成功', defaultMsg = '操作成功', time = 2000, onClose}) => {
return layui.layer.msg(
'' + msg, {
offset: '15px',
@@ -19,8 +25,17 @@ function InitDog() {
skin: 'dog success',
end: onClose
})
+ };
+ dog.reloadTable = (tableFilter) => {
+ if (!tableFilter) {
+ tableFilter = document.querySelector('table[lay-filter]').getAttribute('lay-filter');
+ }
+ layui.table.reload(tableFilter, {
+ page: {
+ curr: $('.layui-laypage-em').next().html()
+ }
+ })
}
-
return dog;
}
const Dog = window.Dog = InitDog();
\ No newline at end of file
diff --git a/src/main/resources/static/admin/v1/static/js/helper.js b/src/main/resources/static/admin/v1/static/js/helper.js
index 2fccc51..b68453c 100644
--- a/src/main/resources/static/admin/v1/static/js/helper.js
+++ b/src/main/resources/static/admin/v1/static/js/helper.js
@@ -1,14 +1,6 @@
if (!window.Helper) { window.Helper = {} }
window.Helper = {
- emoneyPeriodToName: function(x) {
- if (x < 10000) return `${x} 分钟`;
- if (x == 10000) return '日线';
- if (x == 20000) return '周线';
- if (x == 30000) return '月线';
- if (x == 40000) return '季线';
- if (x == 50000) return '半年线';
- if (x == 60000) return '年线';
- },
+ emoneyPeriodToName: x => x < 10000 && `${x} 分钟线` || [null, '日线', '周线', '月线', '季线', '半年线', '年线'][x/10000],
allEmoneyPeriods: [1, 5, 15, 30, 60, 120, 10000, 20000, 30000, 40000, 50000, 60000],
showIndexDetailLayer: async function(obj, forceRefresh) {
// obj: {indexCode: _, indexName: _}
@@ -50,14 +42,12 @@ window.Helper = {
skin: 'layui-layer-indexDetail',
area: ['520px', '320px'],
btn: ['刷新', '确定'],
- btn1: function(index, layero, that) {
+ btn1: function(index, _, _) {
layer.close(index);
Helper.showIndexDetailLayer(obj, !0);
},
- success: function(layero, index) {
- var btns = layero.find('.layui-layer-btn>*');
- btns[0].setAttribute('class', 'layui-layer-btn1');
- btns[1].setAttribute('class', 'layui-layer-btn0');
+ success: function(layero, _) {
+ Helper.setLayerMainBtn(layero, -1);
}
})
}
@@ -70,6 +60,211 @@ window.Helper = {
const escaped = chars.split('').map(c => c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('');
const pattern = new RegExp(`^[${escaped}]+|[${escaped}]+$`, 'g');
return str.replace(pattern, '');
- }
+ },
+ setLayerMainBtn: function (layero, index) {
+ var btns = layero.find('.layui-layer-btn>*'), j = 1;
+ if (index < 0) index = btns.length + index;
+ for (let i = 0; i < btns.length; i++) {
+ const btn = btns[i];
+ const clazz = btn.getAttribute('class');
+ const classes = clazz.split(' ');
+ let filtered = classes.filter(str => !/^layui\-layer\-btn/gi.test(str));
+ filtered.push('layui-layer-btn' + (index == i ? '0' : j++));
+ btn.setAttribute('class', filtered.join(' '));
+ }
+ },
+ openR: function (option) {
+ const defaultOption = {
+ type: 1, area: '500px',
+ skin: 'layui-anim layui-anim-rl layui-layer-adminRight',
+ anim: -1, shadeClose: !0, closeBtn: !0, move: !1, offset: 'r'
+ };
+ option = $.extend(defaultOption, option);
+ return layui.layer.open(option)
+ },
+ /**
+ * 按照通用配置来渲染表格
+ * option: 和 table.render 选项基本一致, 但需要额外提供:
+ * idName: 该表格行对象 id 的名称
+ * baseUrl: 当前基本 URL, 将在此基础上拼接 list/updateBool/batchOp 等内容
+ * batchOpEnum: 批量操作枚举名,若不提供则不渲染批量操作控件和回调逻辑,若为 true 则仅渲染批量删除逻辑
+ * 除此以外,cols 内列如果需要开关选项的, 在相应 col 内添加 switchTemplet: true
+ */
+ renderTable: (option) => {
+ const defaultOption = {
+ page: !0, skin: 'line'
+ };
+ option = $.extend(defaultOption, option);
+ if (!option.idName) throw new Error('idName 不允许为空');
+ if (!option.baseUrl) throw new Error('baseUrl 不允许为空');
+ if (!option.baseUrl.endsWith('/')) option.baseUrl += '/';
+ option.url = option.baseUrl + 'list';
+ let tableSwitchTemplet = function () {
+ // 以 elem 选择器 + '.' + switchFilter 作为 filter
+ const filter = `${option.elem}.switchFilter`;
+ layui.form.on(`switch(${filter})`, function (obj) {
+ console.log(obj, obj.elem.checked);
+ const data = {
+ field: obj.elem.dataset.field,
+ value: obj.elem.checked,
+ id: obj.elem.dataset.id
+ };
+ $.ajax({
+ url: option.baseUrl + 'updateBool', method: 'POST',
+ data:data,
+ success: () => Dog.success({time: 1000}),
+ error: function (res) {
+ Dog.error({msg: res})
+ // 恢复 enabled 状态
+ obj.elem.checked = !obj.elem.checked;
+ layui.form.render('checkbox')
+ return
+ }
+ })
+ });
+ return d => {
+ var fieldName = d.LAY_COL.field;
+ return ``;
+ }
+ }
+ let cols = option.cols[0];
+ cols.forEach(col => {
+ if (col.switchTemplet) {
+ col.templet = tableSwitchTemplet()
+ }
+ })
+ layui.table.render(option)
+ },
+ onSubmitForm: (submitButtonFilter, func) => {
+ layui.form.on(`submit(${submitButtonFilter})`, _ => {
+ const buttonEl = $(`[lay-filter="${submitButtonFilter}"]`);
+ if (!buttonEl.length) {
+ Dog.error({msg: `找不到对应 filter 为 ${submitButtonFilter} 的 button`});
+ return
+ }
+ const form = $(buttonEl.parents('.layui-form')[0] || buttonEl.parents('form')[0]);
+ if (!form.length) {
+ Dog.error({msg: `找不到对应 filter 为 ${submitButtonFilter} 的 button 对应的表单`});
+ return
+ }
+ // 获取 form 内所有表单
+ const els = form.find('input[name], select[name], textarea[name], button[name]');
+ let obj = {form: form[0], field: {}};
+ $.each(els, (i, el) => {
+ const name = el.name;
+ if (!name) return true
+ switch (el.type) {
+ case 'checkbox':
+ if (el.getAttribute('lay-skin')) {
+ // 带 skin 当做二值简单表单
+ obj.field[name] = el.value;
+ }
+ else {
+ if (!obj.field.hasOwnProperty(name)) {
+ obj.field[name] = [];
+ }
+ if (el.checked) {
+ obj.field[name].push(el.value);
+ }
+ }
+ break;
+ case 'radio':
+ if (el.checked) {
+ obj.field[name] = el.value;
+ }
+ break;
+
+ case 'select-multiple':
+ obj.field[name] = Array.from(el.selectedOptions).map(opt => opt.value);
+ break;
+
+ default:
+ obj.field[name] = el.value;
+ }
+ });
+ if (func && typeof func === 'function') return func(obj);
+ else if (func && typeof func === 'string') {
+ // 按照默认来 post
+ $.ajax({
+ url: func,
+ method: 'POST',
+ contentType: 'application/json',
+ data: JSON.stringify(obj.field),
+ success: function (r) {
+ Dog.success({onClose: () => {
+ if (window.editLayer) layui.layer.close(window.editLayer);
+ Dog.reloadTable()}})
+ },
+ error: res => Dog.error({msg: res}),
+ });
+ }
+ })
+ },
+ fillEditForm: (r, layero, layerIndex, extraSwitchFuncs) => {
+ const el = $(layero);
+ let switchFuncs = [];
+ for (let key in r.data) {
+ let val = r.data[key];
+ const fieldEl = el[0].querySelector(`[name="${key}"]`);
+ if (!fieldEl) continue;
+ const type = fieldEl.type;
+ switch (type) {
+ case 'checkbox':
+ const checked = fieldEl.value = fieldEl.chceked = val == 'true' || val == true;
+ const laySkin = fieldEl.getAttribute('lay-skin');
+ if (laySkin) {
+ switchFuncs[key] = function (obj) {
+ obj.elem.value = obj.elem.checked;
+ layui.form.render();
+ }
+ layui.form.on(`switch(${key})`, function (obj) {
+ switchFuncs[obj.elem.name](obj);
+ })
+ layui.event.call(this, 'form', `switch(${key})`, {
+ elem: fieldEl,
+ value: checked
+ });
+ }
+ break;
+ case 'radio':
+ fieldEl.value = fieldEl.chceked = val == 'true' || val == true;
+ break;
+ case 'select-one':
+ const options = fieldEl.querySelectorAll('option');
+ options.forEach(option => {
+ if (option.value == val) {
+ option.selected = true;
+ return false;
+ }
+ })
+ break;
+ default:
+ fieldEl.value = val
+ }
+ if (type === 'text' || type === 'password' || type === 'hidden') {
+ fieldEl.value = val;
+ }
+ }
+ if (extraSwitchFuncs) {
+ switchFuncs = $.extend(switchFuncs, extraSwitchFuncs)
+ }
+ },
+ tableSwitchTemplet: idName => {
+ layui.form.on('switch(switchFilter)', function (obj) {
+ console.log(obj, obj.elem.checked);
+ $.ajax({
+
+ })
+ })
+ return d => {
+ var fieldName = d.LAY_COL.field;
+ return ``;
+ }
+ },
+ randomHexString: n => [...Array(n)].map(() => 'abcdef0123456789'[~~(Math.random() * 16)]).join('')
}
\ No newline at end of file
diff --git a/src/main/resources/static/img/emograb_logo.webp b/src/main/resources/static/img/emograb_logo.webp
new file mode 100644
index 0000000000000000000000000000000000000000..2d5bfc306e36165d44a94050f0397996e6c96b13
GIT binary patch
literal 62806
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