From 148583cdaafc94445cb098881fd4f47bf940c200 Mon Sep 17 00:00:00 2001 From: Doghole Date: Thu, 3 Jul 2025 15:58:27 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=EF=BC=88=E7=A7=BB=E5=8A=A8?= =?UTF-8?q?=EF=BC=89=E4=B8=80=E4=BA=9B=E5=BA=94=E8=AF=A5=E6=94=BE=E5=9C=A8?= =?UTF-8?q?=E2=80=9C=E7=AE=A1=E7=90=86=E2=80=9D=E5=BD=A2=E6=88=90=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E7=AE=A1=E7=90=86=EF=BC=8C=E8=80=8C=E9=9D=9E=E6=94=BE?= =?UTF-8?q?=E5=9C=A8=E2=80=9C=E8=AE=BE=E7=BD=AE=E2=80=9D=E5=BD=A2=E6=88=90?= =?UTF-8?q?=E5=8D=95=E4=B8=80=E9=85=8D=E7=BD=AE=E7=9A=84=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../rich/emoney/EmoneyAutoApplication.java | 3 +- .../rich/emoney/config/SecurityConfig.java | 1 + .../emoney/config/SqliteMybatisConfig.java | 80 +++- .../emoney/controller/IndexControllerV1.java | 8 +- .../emoney/controller/LoginControllerV1.java | 10 +- .../api/CommonAbilityControllerV1.java | 58 +++ .../controller/common/BaseController.java | 4 +- .../common/UpdateBoolController.java | 54 +++ .../controller/manage/PlanControllerV1.java | 86 ++-- .../manage/ProxySettingControllerV1.java | 154 +++++++ .../manage/RequestInfoControllerV1.java | 95 ++++ .../emoney/entity/sqlite/ProxySetting.java | 79 ++++ .../emoney/entity/sqlite/RequestInfo.java | 433 ++++++++++++++++++ .../interceptor/EnumOptionsInterceptor.java | 10 +- .../rich/emoney/interfaces/ConfigInfo.java | 2 +- .../emoney/interfaces/IQueryableEnum.java | 11 + .../mapper/sqlite/ProxySettingMapper.java | 16 + .../mapper/sqlite/RequestInfoMapper.java | 16 + .../rich/emoney/pojo/dto/IndexDetail.java | 12 + .../emoney/pojo/dto/NonParamsIndexDetail.java | 3 +- .../emoney/pojo/dto/ParamsIndexDetail.java | 7 +- .../rich/emoney/service/ConfigService.java | 33 +- .../emoney/service/IndexDetailService.java | 94 ++-- .../service/sqlite/ProxySettingService.java | 12 + .../service/sqlite/RequestInfoService.java | 14 + .../rich/emoney/util/CallerLockUtil.java | 39 ++ .../quant/rich/emoney/util/EncryptUtils.java | 10 + .../quant/rich/emoney/util/GeoIPUtil.java | 17 +- .../emoney/util/SmartResourceResolver.java | 111 +++++ .../emoney/validator/ProxySettingValid.java | 20 + .../validator/ProxySettingValidator.java | 33 ++ src/main/resources/application.yml | 5 +- src/main/resources/database.db | Bin 40960 -> 40960 bytes .../static/admin/v1/static/css/admin.css | 3 +- .../static/admin/v1/static/js/dog.js | 21 +- .../static/admin/v1/static/js/helper.js | 225 ++++++++- .../resources/static/img/emograb_logo.webp | Bin 0 -> 62806 bytes .../resources/webpage/admin/v1/include.html | 198 +++++--- src/main/resources/webpage/admin/v1/init.html | 2 +- .../resources/webpage/admin/v1/login.html | 5 +- .../webpage/admin/v1/manage/plan/include.html | 24 +- .../webpage/admin/v1/manage/plan/index.html | 73 +-- .../v1/manage/protocolMatch/include.html | 5 +- .../admin/v1/manage/proxySetting/include.html | 108 +++++ .../admin/v1/manage/proxySetting/index.html | 138 ++++++ .../admin/v1/manage/requestInfo/include.html | 218 +++++++++ .../admin/v1/manage/requestInfo/index.html | 98 ++++ src/test/java/WeibaPlayground.java | 69 --- .../java/quant/rich/EmoneyIndexScraper.java | 2 +- .../quant/rich/RelativeEmoneyScraper.java | 8 +- .../quant/rich/SmartResourceLoaderTest.java | 38 ++ .../java/quant/rich/TestIndexJsMatch.java | 30 +- 52 files changed, 2433 insertions(+), 362 deletions(-) create mode 100644 src/main/java/quant/rich/emoney/controller/api/CommonAbilityControllerV1.java create mode 100644 src/main/java/quant/rich/emoney/controller/common/UpdateBoolController.java create mode 100644 src/main/java/quant/rich/emoney/controller/manage/ProxySettingControllerV1.java create mode 100644 src/main/java/quant/rich/emoney/controller/manage/RequestInfoControllerV1.java create mode 100644 src/main/java/quant/rich/emoney/entity/sqlite/ProxySetting.java create mode 100644 src/main/java/quant/rich/emoney/entity/sqlite/RequestInfo.java create mode 100644 src/main/java/quant/rich/emoney/interfaces/IQueryableEnum.java create mode 100644 src/main/java/quant/rich/emoney/mapper/sqlite/ProxySettingMapper.java create mode 100644 src/main/java/quant/rich/emoney/mapper/sqlite/RequestInfoMapper.java create mode 100644 src/main/java/quant/rich/emoney/service/sqlite/ProxySettingService.java create mode 100644 src/main/java/quant/rich/emoney/service/sqlite/RequestInfoService.java create mode 100644 src/main/java/quant/rich/emoney/util/SmartResourceResolver.java create mode 100644 src/main/java/quant/rich/emoney/validator/ProxySettingValid.java create mode 100644 src/main/java/quant/rich/emoney/validator/ProxySettingValidator.java create mode 100644 src/main/resources/static/img/emograb_logo.webp create mode 100644 src/main/resources/webpage/admin/v1/manage/proxySetting/include.html create mode 100644 src/main/resources/webpage/admin/v1/manage/proxySetting/index.html create mode 100644 src/main/resources/webpage/admin/v1/manage/requestInfo/include.html create mode 100644 src/main/resources/webpage/admin/v1/manage/requestInfo/index.html delete mode 100644 src/test/java/WeibaPlayground.java create mode 100644 src/test/java/quant/rich/SmartResourceLoaderTest.java diff --git a/src/main/java/quant/rich/emoney/EmoneyAutoApplication.java b/src/main/java/quant/rich/emoney/EmoneyAutoApplication.java index 386d992..5789f3c 100644 --- a/src/main/java/quant/rich/emoney/EmoneyAutoApplication.java +++ b/src/main/java/quant/rich/emoney/EmoneyAutoApplication.java @@ -13,7 +13,8 @@ import org.springframework.scheduling.annotation.EnableScheduling; public class EmoneyAutoApplication { public static void main(String[] args) { - SpringApplication.run(EmoneyAutoApplication.class, args); + SpringApplication app = new SpringApplication(EmoneyAutoApplication.class); + app.run(args); } } diff --git a/src/main/java/quant/rich/emoney/config/SecurityConfig.java b/src/main/java/quant/rich/emoney/config/SecurityConfig.java index 694967e..c7633ed 100644 --- a/src/main/java/quant/rich/emoney/config/SecurityConfig.java +++ b/src/main/java/quant/rich/emoney/config/SecurityConfig.java @@ -29,6 +29,7 @@ public class SecurityConfig { .headers(headers -> headers.cacheControl(cache -> cache.disable())) .csrf(csrf -> csrf.disable()) .authorizeHttpRequests(auth -> auth + .requestMatchers("/favicon.ico").permitAll() .requestMatchers("/admin/*/login").permitAll() .requestMatchers("/admin/*/static/**").permitAll() .requestMatchers("/public/**").permitAll() diff --git a/src/main/java/quant/rich/emoney/config/SqliteMybatisConfig.java b/src/main/java/quant/rich/emoney/config/SqliteMybatisConfig.java index 9e5cf36..6192d25 100644 --- a/src/main/java/quant/rich/emoney/config/SqliteMybatisConfig.java +++ b/src/main/java/quant/rich/emoney/config/SqliteMybatisConfig.java @@ -1,5 +1,11 @@ package quant.rich.emoney.config; +import java.io.File; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + import javax.sql.DataSource; import org.apache.ibatis.session.SqlSessionFactory; @@ -8,6 +14,7 @@ import org.mybatis.spring.annotation.MapperScan; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import com.baomidou.mybatisplus.annotation.DbType; @@ -15,16 +22,87 @@ import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean; +import com.zaxxer.hikari.HikariDataSource; +import lombok.extern.slf4j.Slf4j; +import quant.rich.emoney.EmoneyAutoApplication; + +@Slf4j @Configuration @MapperScan(basePackages = "quant.rich.emoney.mapper.sqlite", sqlSessionTemplateRef = "sqliteSqlSessionTemplate") public class SqliteMybatisConfig { - + + private static final String RESOURCE_PATH = "database.db"; + public static final String SQLITE_TRANSACTION_MANAGER = "sqliteTransactionManager"; + + /** + * 配置数据库连接 + * + * @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 getIQueryableEnum(String enumName) { + Set> enums = reflections.getSubTypesOf(IQueryableEnum.class); + Map map = new HashMap<>(); + for (Class clazz : enums) { + if (clazz.getSimpleName().equals(enumName) && clazz.isEnum()) { + @SuppressWarnings({ "unchecked", "rawtypes" }) + Class enumClass = (Class>) clazz; + for (Enum e : enumClass.getEnumConstants()) { + if (e instanceof IQueryableEnum iqe) { + map.put(e.name(), iqe.getNote()); + } + } + break; + } + } + return map; + } + +} diff --git a/src/main/java/quant/rich/emoney/controller/common/BaseController.java b/src/main/java/quant/rich/emoney/controller/common/BaseController.java index e628c6e..affe3b4 100644 --- a/src/main/java/quant/rich/emoney/controller/common/BaseController.java +++ b/src/main/java/quant/rich/emoney/controller/common/BaseController.java @@ -3,9 +3,12 @@ package quant.rich.emoney.controller.common; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; + import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; +import quant.rich.emoney.pojo.dto.R; import quant.rich.emoney.service.AuthService; @Controller @@ -26,5 +29,4 @@ public abstract class BaseController { protected Boolean isLogin() { return authService.isLogin(); } - } diff --git a/src/main/java/quant/rich/emoney/controller/common/UpdateBoolController.java b/src/main/java/quant/rich/emoney/controller/common/UpdateBoolController.java new file mode 100644 index 0000000..52a084c --- /dev/null +++ b/src/main/java/quant/rich/emoney/controller/common/UpdateBoolController.java @@ -0,0 +1,54 @@ +package quant.rich.emoney.controller.common; + +import java.lang.reflect.Field; +import java.util.Optional; + +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.TableFieldInfo; +import com.baomidou.mybatisplus.core.metadata.TableInfo; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import com.baomidou.mybatisplus.core.toolkit.support.SFunction; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.fasterxml.jackson.databind.ObjectMapper; + +import lombok.extern.slf4j.Slf4j; +import quant.rich.emoney.exception.RException; +import quant.rich.emoney.pojo.dto.R; + +@Slf4j +public abstract class UpdateBoolController, S extends ServiceImpl> extends BaseController { + + ObjectMapper mapper = new ObjectMapper(); + + protected + R updateBool(S service, Class clazz, SFunction idName, Object idValue, String field, Boolean value) { + TableInfo tableInfo = TableInfoHelper.getTableInfo(clazz); + Object converted = mapper.convertValue(idValue, tableInfo.getKeyType()); + try { + Field declaredField = clazz.getDeclaredField(field); + Optional fieldInfo = tableInfo.getFieldList().stream() + .filter(f -> f.getProperty().equals(field)) + .findFirst(); + if (declaredField.getType().equals(Boolean.class)) { + return R.judge(service.update( + new UpdateWrapper() + .set(fieldInfo.get().getColumn(), value) + .lambda().eq(idName, converted) + ), "更新失败,请查看日志"); + } + } + catch (Exception e) { + log.error("update bool failed", e); + } + throw RException.badRequest().setLogRequest(true); + } + + @PostMapping("/updateBool") + @ResponseBody + abstract protected R updateBool(String id, String field, Boolean value); + +} diff --git a/src/main/java/quant/rich/emoney/controller/manage/PlanControllerV1.java b/src/main/java/quant/rich/emoney/controller/manage/PlanControllerV1.java index 0bd9dd1..c794f91 100644 --- a/src/main/java/quant/rich/emoney/controller/manage/PlanControllerV1.java +++ b/src/main/java/quant/rich/emoney/controller/manage/PlanControllerV1.java @@ -1,11 +1,9 @@ package quant.rich.emoney.controller.manage; -import java.lang.reflect.Field; +import java.io.Serializable; import java.util.Arrays; import java.util.List; import java.util.Objects; -import java.util.Optional; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; @@ -16,17 +14,15 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; -import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; -import com.baomidou.mybatisplus.core.metadata.TableFieldInfo; -import com.baomidou.mybatisplus.core.metadata.TableInfo; -import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; import com.baomidou.mybatisplus.core.toolkit.StringUtils; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import lombok.extern.slf4j.Slf4j; -import quant.rich.emoney.controller.common.BaseController; +import quant.rich.emoney.controller.common.UpdateBoolController; import quant.rich.emoney.entity.sqlite.Plan; import quant.rich.emoney.exception.RException; +import quant.rich.emoney.interfaces.IQueryableEnum; +import quant.rich.emoney.mapper.sqlite.PlanMapper; import quant.rich.emoney.pojo.dto.LayPageReq; import quant.rich.emoney.pojo.dto.LayPageResp; import quant.rich.emoney.pojo.dto.R; @@ -35,7 +31,7 @@ import quant.rich.emoney.service.sqlite.PlanService; @Slf4j @Controller @RequestMapping("/admin/v1/manage/plan") -public class PlanControllerV1 extends BaseController { +public class PlanControllerV1 extends UpdateBoolController { @Autowired PlanService planService; @@ -79,24 +75,8 @@ public class PlanControllerV1 extends BaseController { @PostMapping("/updateBool") @ResponseBody - public R updateBool(String planId, String field, Boolean value) { - TableInfo tableInfo = TableInfoHelper.getTableInfo(Plan.class); - try { - Field declaredField = Plan.class.getDeclaredField(field); - - Optional fieldInfo = tableInfo.getFieldList().stream() - .filter(f -> f.getProperty().equals(field)) - .findFirst(); - if (declaredField.getType().equals(Boolean.class)) { - planService.update( - new UpdateWrapper() - .eq("plan_id", planId) - .set(fieldInfo.get().getColumn(), value)); - return R.ok(); - } - } - catch (Exception e) {} - throw RException.badRequest(); + public R updateBool(String id, String field, Boolean value) { + return updateBool(planService, Plan.class, Plan::getPlanId, id, field, value); } @PostMapping("/save") @@ -121,14 +101,17 @@ public class PlanControllerV1 extends BaseController { @ResponseBody public R batchOp( @RequestParam(value="ids[]", required=true) - String[] ids, String op) { + String[] ids, PlanBatchOp op) { if (Objects.isNull(ids) || ids.length == 0) { throw RException.badRequest("提供的计划 ID 不能为空"); } List idArray = Arrays.asList(ids); - if (StringUtils.isBlank(op)) { + if (op == null) { // op 为空是删除 + throw RException.badRequest("操作类型不能为空"); + } + else if (PlanBatchOp.DELETE == op) { return R.judge( planService.removeBatchByIds(idArray)); } @@ -136,23 +119,42 @@ public class PlanControllerV1 extends BaseController { LambdaUpdateWrapper uw = new LambdaUpdateWrapper<>(); uw.in(Plan::getPlanId, idArray); - if ("enable".equals(op)) { - uw.set(Plan::getEnabled, true); - } - else if ("disable".equals(op)) { + switch (op) { + case ENABLE: + uw.set(Plan::getEnabled, true); + break; + case DISABLE: uw.set(Plan::getEnabled, false); - } - else if ("enableOpenDayCheck".equals(op)) { + break; + case ENABLE_OPEN_DAY_CHECK: uw.set(Plan::getOpenDayCheck, true); - } - else if ("disableOpenDayCheck".equals(op)) { + break; + case DISABLE_OPEN_DAY_CHECK: uw.set(Plan::getOpenDayCheck, false); + break; + default: + throw RException.badRequest("未知操作"); } - else { - throw RException.badRequest("未识别的操作"); - } - - return R.judge(planService.update(uw)); + return R.judge(() -> planService.update(uw)); } + + private static enum PlanBatchOp implements IQueryableEnum { + DELETE("删除"), + ENABLE("启用"), + DISABLE("停用"), + ENABLE_OPEN_DAY_CHECK("开启交易日校验"), + DISABLE_OPEN_DAY_CHECK("关闭交易日校验"); + + private String note; + + private PlanBatchOp(String note) { + this.note = note; + } + + @Override + public String getNote() { + return note; + } + } } diff --git a/src/main/java/quant/rich/emoney/controller/manage/ProxySettingControllerV1.java b/src/main/java/quant/rich/emoney/controller/manage/ProxySettingControllerV1.java new file mode 100644 index 0000000..f9eb54f --- /dev/null +++ b/src/main/java/quant/rich/emoney/controller/manage/ProxySettingControllerV1.java @@ -0,0 +1,154 @@ +package quant.rich.emoney.controller.manage; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +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.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; + +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.TableFieldInfo; +import com.baomidou.mybatisplus.core.metadata.TableInfo; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; + +import lombok.extern.slf4j.Slf4j; +import quant.rich.emoney.controller.common.BaseController; +import quant.rich.emoney.entity.sqlite.ProxySetting; +import quant.rich.emoney.exception.RException; +import quant.rich.emoney.pojo.dto.LayPageReq; +import quant.rich.emoney.pojo.dto.LayPageResp; +import quant.rich.emoney.pojo.dto.R; +import quant.rich.emoney.service.sqlite.ProxySettingService; + +@Slf4j +@Controller +@RequestMapping("/admin/v1/manage/proxySetting") +public class ProxySettingControllerV1 extends BaseController { + + @Autowired + ProxySettingService proxySettingService; + + @GetMapping({"", "/", "/index"}) + public String index() { + return "/admin/v1/manage/proxySetting/index"; + } + + @GetMapping("/list") + @ResponseBody + public LayPageResp list(LayPageReq pageReq) { + Page planPage = proxySettingService.page(pageReq); + return new LayPageResp<>(planPage); + } + + @GetMapping("/getOne") + @ResponseBody + public R getOne(String id) { + + // 如果 planId 是空,说明可能希望新建一个 ProxySetting,需要返回默认实例化对象 + if (id == null) { + return R.ok(new ProxySetting()); + } + + // 否则从数据库取 + ProxySetting proxy = proxySettingService.getById(id); + return R.judge(proxy != null, proxy, "无法找到对应 ID 的 ProxySetting"); + } + + @PostMapping("/updateBool") + @ResponseBody + public R updateBool(String id, String field, Boolean value) { + TableInfo tableInfo = TableInfoHelper.getTableInfo(ProxySetting.class); + try { + Field declaredField = ProxySetting.class.getDeclaredField(field); + + Optional fieldInfo = tableInfo.getFieldList().stream() + .filter(f -> f.getProperty().equals(field)) + .findFirst(); + if (declaredField.getType().equals(Boolean.class)) { + proxySettingService.update( + new UpdateWrapper() + .eq("id", id) + .set(fieldInfo.get().getColumn(), value)); + return R.ok(); + } + } + catch (Exception e) {} + throw RException.badRequest(); + } + + @PostMapping("/save") + @ResponseBody + public R save(@RequestBody ProxySetting proxySetting) { + if (!Objects.isNull(proxySetting.getId())) { + proxySettingService.updateById(proxySetting); + } + else { + proxySettingService.save(proxySetting.setId(null)); + } + return R.ok(); + } + + @PostMapping("/delete") + @ResponseBody + public R delete(String id) { + return R.judge(proxySettingService.removeById(id), "删除失败,是否已删除?"); + } + + @PostMapping("/batchOp") + @ResponseBody + public R batchOp( + @RequestParam(value="ids[]", required=true) + String[] ids, ProxySettingBatchOp op) { + if (Objects.isNull(ids) || ids.length == 0) { + throw RException.badRequest("提供的计划 ID 不能为空"); + } + List idArray = Arrays.asList(ids); + + if (op == null) { + // op 为空是删除 + throw RException.badRequest("操作类型不能为空"); + } + else if (ProxySettingBatchOp.DELETE == op) { + return R.judge( + proxySettingService.removeBatchByIds(idArray)); + } + + LambdaUpdateWrapper uw = new LambdaUpdateWrapper<>(); + uw.in(ProxySetting::getId, idArray); + + switch (op) { + case CHECK: + // TODO: 检查连通性 + break; + case DISABLE_HTTPS_VERIFY: + uw.set(ProxySetting::getIgnoreHttpsVerification, false); + break; + case ENABLE_HTTP_VERIFY: + uw.set(ProxySetting::getIgnoreHttpsVerification, true); + break; + default: + throw RException.badRequest("未知操作"); + } + return R.judge(() -> proxySettingService.update(uw)); + } + + private static enum ProxySettingBatchOp { + DELETE, + CHECK, + DISABLE_HTTPS_VERIFY, + ENABLE_HTTP_VERIFY + } + +} diff --git a/src/main/java/quant/rich/emoney/controller/manage/RequestInfoControllerV1.java b/src/main/java/quant/rich/emoney/controller/manage/RequestInfoControllerV1.java new file mode 100644 index 0000000..89a3ff3 --- /dev/null +++ b/src/main/java/quant/rich/emoney/controller/manage/RequestInfoControllerV1.java @@ -0,0 +1,95 @@ +package quant.rich.emoney.controller.manage; + +import java.io.Serializable; +import java.lang.reflect.Field; +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Controller; +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.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; + +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.TableFieldInfo; +import com.baomidou.mybatisplus.core.metadata.TableInfo; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; + +import lombok.extern.slf4j.Slf4j; +import quant.rich.emoney.controller.common.BaseController; +import quant.rich.emoney.controller.common.UpdateBoolController; +import quant.rich.emoney.entity.sqlite.RequestInfo; +import quant.rich.emoney.exception.RException; +import quant.rich.emoney.mapper.sqlite.RequestInfoMapper; +import quant.rich.emoney.pojo.dto.LayPageReq; +import quant.rich.emoney.pojo.dto.LayPageResp; +import quant.rich.emoney.pojo.dto.R; +import quant.rich.emoney.service.sqlite.RequestInfoService; + +@Slf4j +@Controller +@RequestMapping("/admin/v1/manage/requestInfo") +public class RequestInfoControllerV1 extends UpdateBoolController { + + @Autowired + RequestInfoService requestInfoService; + + @GetMapping({"", "/", "/index"}) + public String index() { + return "/admin/v1/manage/requestInfo/index"; + } + + @GetMapping("/list") + @ResponseBody + public LayPageResp list(LayPageReq pageReq) { + Page planPage = requestInfoService.page(pageReq); + return new LayPageResp<>(planPage); + } + + @GetMapping("/getOne") + @ResponseBody + public R getOne(Integer id) { + + // 如果 planId 是空,说明可能希望新建一个 Plan,需要返回默认实例化对象 + if (id == null) { + return R.ok(new RequestInfo()); + } + + // 否则从数据库取 + RequestInfo plan = requestInfoService.getById(id); + return R.judge(plan != null, plan, "无法找到对应 ID 的 Plan"); + } + + @PostMapping("/updateBool") + @ResponseBody + @Override + protected R updateBool(String id, String field, Boolean value) { + return updateBool(requestInfoService, RequestInfo.class, RequestInfo::getId, id, field, value); + } + + @PostMapping("/save") + @ResponseBody + public R save(@RequestBody @NonNull RequestInfo plan) { + return R.judge(() -> requestInfoService.saveOrUpdate(plan)); + } + + @PostMapping("/delete") + @ResponseBody + public R delete(String id) { + return R.judge(requestInfoService.removeById(id), "删除失败,是否已删除?"); + } + + @PostMapping("/batchOp") + @ResponseBody + public R batchOp( + @RequestParam(value="ids[]", required=true) + String[] ids, String op) { + return null; + } + +} diff --git a/src/main/java/quant/rich/emoney/entity/sqlite/ProxySetting.java b/src/main/java/quant/rich/emoney/entity/sqlite/ProxySetting.java new file mode 100644 index 0000000..292be50 --- /dev/null +++ b/src/main/java/quant/rich/emoney/entity/sqlite/ProxySetting.java @@ -0,0 +1,79 @@ +package quant.rich.emoney.entity.sqlite; + +import java.net.InetSocketAddress; +import java.net.Proxy; + +import org.apache.commons.lang3.ObjectUtils; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonIgnore; + +import jakarta.annotation.Nonnull; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import quant.rich.emoney.validator.ProxySettingValid; + +@Data +@Accessors(chain = true) +@NoArgsConstructor +@TableName(value = "proxy_setting", autoResultMap = true) +@ProxySettingValid +public class ProxySetting { + + @TableId(value="id", type=IdType.AUTO) + private Integer id; + + @Nonnull + private String proxyName; + + private Proxy.Type proxyType = Proxy.Type.DIRECT; + + private String proxyHost = ""; + + private Integer proxyPort = 1; + + private Boolean ignoreHttpsVerification = false; + + /** + * 根据配置获取 java.net.Proxy + * @return + */ + @JsonIgnore + public Proxy getProxy() { + if (getProxyType() != null && getProxyType() != Proxy.Type.DIRECT) { + return new Proxy(getProxyType(), + new InetSocketAddress(getProxyHost(), getProxyPort())); + } + return Proxy.NO_PROXY; + } + + /** + * 根据当前配置获取 ProxyUrl(String) + *

如: + *

  • socks5://127.0.0.1:8888
  • + *
  • http://10.0.0.1:7890
  • + *

    + * @return + */ + public String getProxyUrl() { + if (ObjectUtils.anyNull(getProxyType(), getProxyHost(), getProxyPort())) { + return null; + } + StringBuilder sb = new StringBuilder(); + if (getProxyType() == Proxy.Type.SOCKS) { + sb.append("socks5://"); + } + else if (getProxyType() == Proxy.Type.HTTP) { + sb.append("http://"); + } + else { + return null; + } + sb.append(getProxyHost()).append(':').append(getProxyPort()); + return sb.toString(); + } + +} diff --git a/src/main/java/quant/rich/emoney/entity/sqlite/RequestInfo.java b/src/main/java/quant/rich/emoney/entity/sqlite/RequestInfo.java new file mode 100644 index 0000000..0b0c7e4 --- /dev/null +++ b/src/main/java/quant/rich/emoney/entity/sqlite/RequestInfo.java @@ -0,0 +1,433 @@ +package quant.rich.emoney.entity.sqlite; + +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import lombok.AccessLevel; +import lombok.Data; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.extern.slf4j.Slf4j; +import quant.rich.emoney.entity.config.AndroidSdkLevelConfig; +import quant.rich.emoney.entity.config.ChromeVersionsConfig; +import quant.rich.emoney.entity.config.DeviceInfoConfig; +import quant.rich.emoney.entity.config.DeviceInfoConfig.DeviceInfo; +import quant.rich.emoney.entity.config.EmoneyRequestConfig; +import quant.rich.emoney.util.EncryptUtils; +import quant.rich.emoney.util.SpringContextHolder; +import quant.rich.emoney.util.TextUtils; + +@Data +@Accessors(chain = true) +@Slf4j +@TableName(value = "request_info") +public class RequestInfo { + + private static AndroidSdkLevelConfig androidSdkLevelConfig = SpringContextHolder.getBean(AndroidSdkLevelConfig.class); + private static DeviceInfoConfig deviceInfoConfig = SpringContextHolder.getBean(DeviceInfoConfig.class); + private static ChromeVersionsConfig chromeVersionsConfig = SpringContextHolder.getBean(ChromeVersionsConfig.class); + + public RequestInfo() { + DeviceInfo deviceInfo = deviceInfoConfig.getRandomDeviceInfo(); + setRelativeFieldsFromDeviceInfo(deviceInfo); + } + + @TableId(value = "id", type = IdType.AUTO) + private Integer id; + + /** + * 该请求信息配置的名称,助记用 + */ + private String name = ""; + + /** + * 是否匿名登录 + */ + private Boolean isAnonymous = true; + + /** + * 非匿名登录时的用户名 + */ + private String username = ""; + + /** + * 非匿名登录时的密码 + */ + private String password = ""; + + /** + * 鉴权信息 + */ + private String authorization = ""; + + /** + * UID + */ + private Integer uid = -1; + + /** + * 用于:
      + *
    • 益盟登录接口 guid = MD5(androidId)
    • + *
    • 益盟登录接口 exIdentify.AndroidID = androidId
    • + *
    + * 来源:
    本例随机生成并管理,需要符合 16 位 + * + */ + private String androidId = TextUtils.randomString("abcdef0123456789", 16); + + /** + * 用于:
      + *
    • Webview User-Agent
    • + *
    • Non-Webview Image User-Agent
    • + *
    + * 来源:DeviceInfoConfig + * @see DeviceInfoConfig + */ + @Setter(AccessLevel.PRIVATE) + @TableField(exist=false) + private String androidVersion; + + /** + * 用于:
      + *
    • 益盟通讯接口请求头 X-Android-Agent = EMAPP/{emoneyVersion}(Android;{androidSdkLevel})
    • + *
    • 益盟登录接口 osVersion = androidSdkLevel
    • + *
    + * 来源:DeviceInfoConfig, 经由 AndroidSdkLevelConfig 转换,由本例代管 + * @see DeviceInfoConfig + * @see AndroidSdkLevelConfig + */ + @Setter(AccessLevel.PRIVATE) + @TableField(exist=false) + private String androidSdkLevel; + + /** + * 用于:
      + *
    • 益盟登录接口 softwareType = softwareType
    • + *
    + * 来源:DeviceInfoConfig,由本例代管 + * @see DeviceInfoConfig + */ + private String softwareType; + + /** + * 用于:
      + *
    • 益盟通讯接口请求头 User-Agent = okHttpUserAgent
    • + *
    + * 一般由程序所使用的 OkHttp 版本决定
    + * 来源:本例管理 + */ + private String okHttpUserAgent = "okhttp/3.12.2"; + + /** + * 对应 build.prop 中 Build.MODEL, 用于:
      + *
    • WebView User-Agent
    • + *
    • 非 WebView 图片User-Agent
    • + *
    + * 来源:DeviceInfoConfig, 由本例代为管理 + * @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[] 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{!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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

    L2DX{VT?+j%*-%naA7(SFRR|43|O$ab4 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 - - - [[${(title!=null?title:'后台管理')}]] - - - - - - - - - - - - + +[[${(title!=null?title:'后台管理')}]] + + + + + + + + + +