新增(移动)一些应该放在“管理”形成列表管理,而非放在“设置”形成单一配置的内容

This commit is contained in:
2025-07-03 15:58:27 +08:00
parent 04cf470ead
commit 148583cdaa
52 changed files with 2433 additions and 362 deletions

View File

@@ -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);
}
}

View File

@@ -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()

View File

@@ -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";
/**
* 配置数据库连接
* <ul>
* <li>如果非打包状态,则直接选取当前项目内数据库位置</li>
* <li>如果打包状态,以 JDBC 链接位置为主,如果位置不存在则覆盖</li>
* </ul>
* @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);

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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<String, String> getIQueryableEnum(String enumName) {
Set<Class<? extends IQueryableEnum>> enums = reflections.getSubTypesOf(IQueryableEnum.class);
Map<String, String> map = new HashMap<>();
for (Class<? extends IQueryableEnum> clazz : enums) {
if (clazz.getSimpleName().equals(enumName) && clazz.isEnum()) {
@SuppressWarnings({ "unchecked", "rawtypes" })
Class<? extends Enum> enumClass = (Class<? extends Enum<?>>) clazz;
for (Enum<?> e : enumClass.getEnumConstants()) {
if (e instanceof IQueryableEnum iqe) {
map.put(e.name(), iqe.getNote());
}
}
break;
}
}
return map;
}
}

View File

@@ -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();
}
}

View File

@@ -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<T, M extends BaseMapper<T>, S extends ServiceImpl<M, T>> extends BaseController {
ObjectMapper mapper = new ObjectMapper();
protected
R<?> updateBool(S service, Class<T> clazz, SFunction<T, ?> 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<TableFieldInfo> fieldInfo = tableInfo.getFieldList().stream()
.filter(f -> f.getProperty().equals(field))
.findFirst();
if (declaredField.getType().equals(Boolean.class)) {
return R.judge(service.update(
new UpdateWrapper<T>()
.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);
}

View File

@@ -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<Plan, PlanMapper, PlanService> {
@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<TableFieldInfo> fieldInfo = tableInfo.getFieldList().stream()
.filter(f -> f.getProperty().equals(field))
.findFirst();
if (declaredField.getType().equals(Boolean.class)) {
planService.update(
new UpdateWrapper<Plan>()
.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<String> 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<Plan> 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;
}
}
}

View File

@@ -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<ProxySetting> pageReq) {
Page<ProxySetting> 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<TableFieldInfo> fieldInfo = tableInfo.getFieldList().stream()
.filter(f -> f.getProperty().equals(field))
.findFirst();
if (declaredField.getType().equals(Boolean.class)) {
proxySettingService.update(
new UpdateWrapper<ProxySetting>()
.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<String> idArray = Arrays.asList(ids);
if (op == null) {
// op 为空是删除
throw RException.badRequest("操作类型不能为空");
}
else if (ProxySettingBatchOp.DELETE == op) {
return R.judge(
proxySettingService.removeBatchByIds(idArray));
}
LambdaUpdateWrapper<ProxySetting> 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
}
}

View File

@@ -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<RequestInfo, RequestInfoMapper, RequestInfoService> {
@Autowired
RequestInfoService requestInfoService;
@GetMapping({"", "/", "/index"})
public String index() {
return "/admin/v1/manage/requestInfo/index";
}
@GetMapping("/list")
@ResponseBody
public LayPageResp<?> list(LayPageReq<RequestInfo> pageReq) {
Page<RequestInfo> 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;
}
}

View File

@@ -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)
* <p>如:
* <li>socks5://127.0.0.1:8888</li>
* <li>http://10.0.0.1:7890</li>
* </p>
* @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();
}
}

View File

@@ -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;
/**
* <b>用于:</b><ul>
* <li>益盟登录接口 <code><i>guid</i> = MD5(<b>androidId</b>)</code></li>
* <li>益盟登录接口 <code><i>exIdentify.AndroidID</i> = <b>androidId</b></code></li>
* </ul>
* <b>来源:</b><br>本例随机生成并管理,需要符合 16 位
*
*/
private String androidId = TextUtils.randomString("abcdef0123456789", 16);
/**
* <b>用于:</b><ul>
* <li>Webview <code><i>User-Agent</i></li>
* <li>Non-Webview Image <code><i>User-Agent</i></code></li>
* </ul>
* <b>来源:</b><code><b>DeviceInfoConfig</b></code>
* @see DeviceInfoConfig
*/
@Setter(AccessLevel.PRIVATE)
@TableField(exist=false)
private String androidVersion;
/**
* <b>用于:</b><ul>
* <li>益盟通讯接口请求头 <code><i>X-Android-Agent</i> = EMAPP/{<b>emoneyVersion</b>}(Android;{<b>androidSdkLevel</b>})</code></li>
* <li>益盟登录接口 <code><i>osVersion</i> = <b>androidSdkLevel</b></code></li>
* </ul>
* <b>来源:</b><code><b>DeviceInfoConfig</b>, 经由 <code><b>AndroidSdkLevelConfig</b></code> 转换,由本例代管</code>
* @see DeviceInfoConfig
* @see AndroidSdkLevelConfig
*/
@Setter(AccessLevel.PRIVATE)
@TableField(exist=false)
private String androidSdkLevel;
/**
* <b>用于:</b><ul>
* <li>益盟登录接口 <code><i>softwareType</i> = <b>softwareType</b></code></li>
* </ul>
* <b>来源:</b><code><b>DeviceInfoConfig</b>,由本例代管</code>
* @see DeviceInfoConfig
*/
private String softwareType;
/**
* <b>用于:</b><ul>
* <li>益盟通讯接口请求头 <code><i>User-Agent</i> = <b>okHttpUserAgent</b></code></li>
* </ul>
* 一般由程序所使用的 OkHttp 版本决定<br>
* <b>来源:</b>本例管理
*/
private String okHttpUserAgent = "okhttp/3.12.2";
/**
* 对应 build.prop 中 Build.MODEL, <b>用于:</b><ul>
* <li>WebView <code><i>User-Agent</i></code></li>
* <li>非 WebView 图片<code><i>User-Agent</i></code></li>
* </ul>
* <b>来源:</b><code><b>DeviceInfoConfig</b>, 由本例代为管理
* @see DeviceInfoConfig
*/
private String deviceName;
/**
* 对应 build.prop 中 Build.FINGERPRINT, <b>用于:</b><ul>
* <li>益盟登录接口 <code><i>hardware</i> = MD5(<b>fingerprint</b>)</code></li>
* <li>益盟登录接口 <code><i>exIdentify.OSFingerPrint</i> = <b>fingerprint</b></code></li>
* </ul>
* <font color="red">注意最终生成的 exIdentify 是 jsonString, 且有对斜杠的转义</font><br>
* <b>来源:</b><code><b>DeviceInfoConfig</b>, 由本例代为管理
* @see DeviceInfoConfig
*
*/
private String fingerprint;
/**
* 对应 build.prop 中 Build.ID, <b>用于:</b><ul>
* <li>WebView <code><i>User-Agent</i></code></li>
* <li>非 WebView 图片<code><i>User-Agent</i></code></li>
* </ul>
* <b>来源:</b><code><b>DeviceInfoConfig</b>, 由本例代为管理
* @see DeviceInfoConfig
*
*/
@TableField(exist=false)
private String buildId;
/**
* <b>用于:</b><ul>
* <li>WebView <code><i>User-Agent</i></code></li>
* </ul>
* <b>来源:</b><code><b>ChromeVersionsConfig</b>, 由本例代为管理
* @see ChromeVersionsConfig
*/
private String chromeVersion = chromeVersionsConfig.getRandomChromeVersion();
/**
* <b>用于:</b><ul>
* <li>益盟通讯接口请求头 <code><i>X-Android-Agent</i> =
* EMAPP/{<b>emoneyVersion</b>}(Android;{androidSdkLevel})</code></li>
* </ul>
* 由程序版本决定<br>
* <b>来源:</b>本例管理
* @see EmoneyRequestConfig.androidSdkLevel
*/
private String emoneyVersion = "5.8.1";
/**
* <b>用于:</b><ul>
* <li>益盟通讯接口请求头 <code><i>Emapp-ViewMode</i> = <b>emappViewMode</b></code></li>
* </ul>
* 由程序决定, 一般默认为 "1"<br>
* <b>来源:</b>本例管理
*/
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;
}
/**
* 设置密码:<ul>
* <li>null or empty保存空字符串</li>
* <li>尝试解密成功,说明是密文,直接保存</li>
* <li>尝试解密失败,说明是明文,加密保存</li>
* </ul>
* @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;
}
}

View File

@@ -97,7 +97,15 @@ public class EnumOptionsInterceptor implements HandlerInterceptor {
return true;
}
/**
* 将其注解到类型为 enum 的字段上,可以将 enum 注入到 Model 中,供 thymeleaf 使用
* <p>例:某 entity 的某字段为:</p>
* <code><b>private</b> Proxy.Type proxyType;</code>
* <p>则在其上注解 @EnumOptions("ProxyTypeEnum"), 即可在 thymeleaf 中直接使用:</p>
* ProxyTypeEnum.DIRECT
* <p>如果只是注解 @EnumOptions未指定 value则在 thymeleaf 为字段名 + Options上例中便为</p>
* ProxyTypeOptions.DIRECT
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented

View File

@@ -33,7 +33,7 @@ public @interface ConfigInfo {
/**
* <p>
* 为 true 时,最终会调用其无参构造方法,若需要填充默认值的,请在无参构造器中填充。
* 当为 true 且无法载入配置文件而触发初始化时,若存在 ./conf/system/{field}.fallback.json 文件时,从 fallback
* 当为 true 且无法载入配置文件而触发初始化时,若存在 /conf/system/{field}.fallback.json 文件时,从 fallback
* 文件中初始化。fallback 仅参与初始化,不参与持久化
* </p>
*

View File

@@ -0,0 +1,11 @@
package quant.rich.emoney.interfaces;
public interface IQueryableEnum {
public default String getName() {
return name();
}
String name();
public String getNote();
}

View File

@@ -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<ProxySetting> {
}

View File

@@ -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<RequestInfo> {
}

View File

@@ -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

View File

@@ -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<NonParamsIndexDetailData> data = new ArrayList<>();
@JsonView(IndexDetail.class)
private String original;
@Data
@Accessors(chain=true)

View File

@@ -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<String> descriptions = new ArrayList<>();
@JsonView(IndexDetail.class)
private String original;
@Override
public String getIndexName() {
return name;
}
@Override
public String getIndexCode() {
return code;
}
@Override
public List<Detail> getDetails() {
List<Detail> list = new ArrayList<>();
@@ -38,6 +40,7 @@ public class ParamsIndexDetail implements IndexDetail {
});
return list;
}
@Override
public void sanitize() {
List<String> descriptions = new ArrayList<>();

View File

@@ -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 <Config>
* @param path
* @param configClass
* @return
*/
private <Config extends IConfig<Config>> Config getFromFile(String path, Class<Config> 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;

View File

@@ -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<NonParamsIndexDetail> 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<String> 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 <Description>
* @param des
*/
private <Description extends IndexDetail> 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 <Description extends IndexDetail> Path getIndexDetailPath(Description description) {
private <Description extends IndexDetail> 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;
}
/**

View File

@@ -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<ProxySettingMapper, ProxySetting> {
}

View File

@@ -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<RequestInfoMapper, RequestInfo> {
}

View File

@@ -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 <T> Optional<T> tryCallWithCallerLock(Callable<T> 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

View File

@@ -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 登录的密码

View File

@@ -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);
}
/**

View File

@@ -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;
}
}
/**
获取资源
<p>
<ul>
<li>JAR
<ul>
<li>优先以 jar 文件所在目录为基准,寻找相对路径外部文件</li>
<li>当外部文件不存在时,读取 classpath即 jar 内部资源文件</li>
</ul>
</li>
<li>WAR 只获取 classpath 文件,即 /WEB-INF/classes/ 下文件</li>
<li>IDE 只获取源文件,即 src/main/resources/ 下文件</li>
</ul></p>
* @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);
}
// 否则回退到 classpathJAR、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
}
}

View File

@@ -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 {};
}

View File

@@ -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<ProxySettingValid, ProxySetting> {
@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;
}
}

View File

@@ -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

Binary file not shown.

View File

@@ -382,6 +382,7 @@ blockquote.layui-elem-quote {
bottom: 0;
box-shadow: 1px 1px 10px rgba(0, 0, 0, .1);
border-radius: 0;
overflow: auto
}
.layui-layer-indexDetail>.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;

View File

@@ -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(
'<i class="fa-solid fa-circle-exclamation"></i>' + 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(
'<i class="fa-solid fa-circle-check"></i>' + 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();

View File

@@ -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 `<input type="checkbox" lay-skin="switch" lay-text="|"
data-field="${fieldName}" data-id="${d[option.idName]}"
${d[fieldName] ? 'checked' : ''} lay-filter="${filter}">`;
}
}
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 `<input type="checkbox" lay-skin="switch" lay-text="|"
data-field="${fieldName}" data-id="${d[idName]}"
${d[fieldName] ? 'checked' : ''} lay-filter="switchFilter">`;
}
},
randomHexString: n => [...Array(n)].map(() => 'abcdef0123456789'[~~(Math.random() * 16)]).join('')
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

View File

@@ -2,81 +2,132 @@
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head th:fragment="head">
<meta charset="utf-8" />
<title>
[[${(title!=null?title:'后台管理')}]]
</title>
<link rel="shortcut icon" th:href="@{/favicon.ico}" />
<meta name="renderer" content="webkit" />
<meta name="base" th:content="@{/}" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=0" />
<link rel="stylesheet" th:href="@{/admin/v1/static/layui/css/layui.css}" media="all" />
<link rel="stylesheet" th:href="@{/admin/v1/static/layuiadmin/style/login.css}" media="all" />
<link rel="stylesheet" th:href="@{/admin/v1/static/css/admin.css}" media="all" />
<link rel="stylesheet" th:href="@{/public/plugins/fa5/css/all.min.css}" media="all" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<style>
.nav-last>i.layui-icon {
top: 30px;
line-height: 0;
}
</style>
<meta charset="utf-8" />
<title>[[${(title!=null?title:'后台管理')}]]</title>
<link rel="shortcut icon" th:href="@{/favicon.ico}" />
<meta name="renderer" content="webkit" />
<meta name="base" th:content="@{/}" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=0" />
<link rel="stylesheet" th:href="@{/admin/v1/static/layui/css/layui.css}"
media="all" />
<link rel="stylesheet"
th:href="@{/admin/v1/static/layuiadmin/style/login.css}" media="all" />
<link rel="stylesheet" th:href="@{/admin/v1/static/css/admin.css}"
media="all" />
<link rel="stylesheet" th:href="@{/public/plugins/fa6/css/all.min.css}"
media="all" />
<style>
.nav-last>i.layui-icon {
top: 30px;
line-height: 0;
}
</style>
</head>
<body>
<div class="main-nav" th:fragment="nav">
<ul class="layui-nav" style="display:table;width:100%">
<li class="layui-nav-item">
<a href="#">控制台</a>
<ul class="layui-nav" style="display: table; width: 100%">
<li class="layui-nav-item"><a href="#"> <i
class="fa-fw fa-solid fa-chalkboard "></i> 控制台
</a>
<dl class="layui-nav-child">
<dd><a th:href="@{/admin/v1/index}">概要</a></dd>
<dd><a href="#">个人设置</a></dd>
<dd><a href="#">插件</a></dd>
<dd><a href="#">外观</a></dd>
<dd><a href="#">备份</a></dd>
</dl>
</li>
<li class="layui-nav-item">
<a href="javascript:;">管理</a>
<dd>
<a th:href="@{/admin/v1/index}">概要</a>
</dd>
<dd>
<a href="#">个人设置</a>
</dd>
<dd>
<a href="#">插件</a>
</dd>
<dd>
<a href="#">外观</a>
</dd>
<dd>
<a href="#">备份</a>
</dd>
</dl></li>
<li class="layui-nav-item"><a href="javascript:;"> <i
class="fa-fw fa-solid fa-screwdriver-wrench"></i> 管理
</a>
<dl class="layui-nav-child">
<dd><a th:href="@{/admin/v1/manage/plan}">计划任务</a></dd>
<dd><a th:href="@{/admin/v1/manage/indexInfo}">指标配置</a></dd>
<dd><a th:href="@{/admin/v1/manage/protocolMatch}">Protocol 配置</a></dd>
</dl>
</li>
<li class="layui-nav-item">
<a href="javascript:;">设置</a>
<dd>
<a th:href="@{/admin/v1/manage/plan}"> <i
class="fa-fw fa-regular fa-calendar"></i> 计划任务
</a>
</dd>
<dd>
<a th:href="@{/admin/v1/manage/requestInfo}"> <i
class="fas fa-fw fa-envelope-open-text"></i> 请求配置
</a>
</dd>
<dd>
<a th:href="@{/admin/v1/manage/indexInfo}"> <i
class="fa-fw fa-solid fa-chart-line "></i> 指标配置
</a>
</dd>
<dd>
<a th:href="@{/admin/v1/manage/proxySetting}"> <i
class="fa-fw fa-solid fa-network-wired"></i> 代理配置
</a>
</dd>
<dd>
<a th:href="@{/admin/v1/manage/protocolMatch}"> <i
class="fa-fw fa-regular fa-handshake "></i> Protocol 配置
</a>
</dd>
</dl></li>
<li class="layui-nav-item"><a href="javascript:;"> <i
class="fa-fw fa-solid fa-gears"></i> 设置
</a>
<dl class="layui-nav-child">
<dd><a th:href="@{/admin/v1/config/emoneyRequest}">请求头设置</a></dd>
<dd><a th:href="@{/admin/v1/config/proxy}">代理设置</a></dd>
</dl>
</li>
<li class="layui-nav-item" style="float:right;margin-right: 1px;" lay-unselect="">
<a href="javascript:;" class="nav-last nav-user-info" >
<img id="adminUserGravatar" th:src="@{/img/dog-avatar.webp}" loading="lazy" referrerpolicy="same-origin" class="layui-nav-img" />
<span id="adminUserNickname">[[${@platformConfig.username}]]</span>
</a>
<dd>
<a th:href="@{/admin/v1/config/emoneyRequest}"> <i
class="fa-fw fa-solid fa-heading"></i> 请求头设置
</a>
</dd>
<dd>
<a th:href="@{/admin/v1/config/proxy}"> <i
class="fa-fw fa-solid fa-network-wired"></i> 代理设置
</a>
</dd>
</dl></li>
<li class="layui-nav-item"
style="float: right; margin-right: 1px;" lay-unselect=""><a
href="javascript:;" class="nav-last nav-user-info"> <img
id="adminUserGravatar" th:src="@{/img/dog-avatar.webp}"
loading="lazy" referrerpolicy="same-origin"
class="layui-nav-img" /> <span id="adminUserNickname">[[${@platformConfig.username}]]</span>
</a>
<dl class="layui-nav-child">
<dd><a href="#" class="change-user-info">修改信息</a></dd>
<dd><a th:href="@{/admin/v1/logout}">退出登录</a></dd>
</dl>
</li>
<li class="layui-nav-item ipInfo" style="float:right;margin-right: 1px" lay-unselect="">
<a id="ipThroughProxy" href="javascript:manualRefreshIp()" title="立即刷新">
IP 属地:
<span th:if="${@proxyConfig.ipInfo == null}" class="layui-badge layui-bg-cyan">加载中...</span>
<span th:if="${@proxyConfig.ipInfo != null}" class="layui-badge layui-bg-cyan">[[${@proxyConfig.ipInfo.geoString}]]</span>
</a>
<th:block th:if="${@proxyConfig.ipInfo != null}">
<dd>
<a href="#" class="change-user-info">修改信息</a>
</dd>
<dd>
<a th:href="@{/admin/v1/logout}">退出登录</a>
</dd>
</dl></li>
<li class="layui-nav-item ipInfo"
style="float: right; margin-right: 1px" lay-unselect=""><a
id="ipThroughProxy" href="javascript:manualRefreshIp()"
title="立即刷新"> IP 属地: <span
th:if="${@proxyConfig.ipInfo == null}"
class="layui-badge layui-bg-cyan">加载中...</span> <span
th:if="${@proxyConfig.ipInfo != null}"
class="layui-badge layui-bg-cyan">[[${@proxyConfig.ipInfo.geoString}]]</span>
</a> <th:block th:if="${@proxyConfig.ipInfo != null}">
<dl class="layui-nav-child">
<dd class="ip"><a title="点击复制">[[${@proxyConfig.ipInfo.ip}]]</a></dd>
<dd class="ipv6" th:if="${@proxyConfig.ipInfo.ipv6 != null}"><a title="点击复制">[[${@proxyConfig.ipInfo.ipv6}]]</a></dd>
<dd class="ip">
<a title="点击复制">[[${@proxyConfig.ipInfo.ip}]]</a>
</dd>
<dd class="ipv6" th:if="${@proxyConfig.ipInfo.ipv6 != null}">
<a title="点击复制">[[${@proxyConfig.ipInfo.ipv6}]]</a>
</dd>
</dl>
</th:block>
</li>
</th:block></li>
</ul>
<script type="text/html" id="editUser">
<div class="layui-form" style="margin:10px 15px" id="editUserForm" lay-filter="editUserForm">
@@ -116,12 +167,14 @@
<th:block th:fragment="head-script">
<script type="text/javascript" src="/admin/v1/static/js/helper.js"></script>
<script th:src="@{/admin/v1/static/layui/layui.js}"></script>
<script th:src="@{/public/plugins/jquery/1.12.4/jquery-1.12.4.min.js}"></script>
<script
th:src="@{/public/plugins/jquery/1.12.4/jquery-1.12.4.min.js}"></script>
<script th:src="@{/public/plugins/js-sha3/sha3.min.js}"></script>
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
<script th:src="@{/public/plugins/alpinejs@3.14.9/dist/cdn.min.js}"
defer></script>
<script th:src="@{/admin/v1/static/giggity/toast.js}"></script>
<script th:src="@{/admin/v1/static/js/dog.js}"></script>
<script>
let refreshTimer = null;
@@ -139,7 +192,7 @@
try {
let geoEl =
document.querySelector('#ipThroughProxy>span');
geoEl.textContent = '加载中...';
//geoEl.textContent = '加载中...';
let res = await (await fetch('/admin/v1/config/proxy/refreshIpThroughProxy')).json();
if (res.ok) {
geoEl.textContent = res.data.geoString || '获取失败';
@@ -269,10 +322,9 @@
</script>
</th:block>
<div th:fragment="feet" class="layui-trans layadmin-user-login-footer">
Driven by Latte<br />
&copy;2025-[[${#dates.format(new java.util.Date().getTime(),'yyyy')}]]
<a href="#">Latte</a>
All Rights Reserved.
Driven by Latte<br /> &copy;2025-[[${#dates.format(new
java.util.Date().getTime(),'yyyy')}]] <a href="#">Latte</a> All
Rights Reserved.
</div>
</body>

View File

@@ -59,7 +59,7 @@
</footer>
<!-- Alpine.js -->
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
<script th:src="@{/public/plugins/alpinejs@3.14.9/dist/cdn.min.js}" defer></script>
<!-- SHA3 224 -->
<script th:src="@{/public/plugins/js-sha3/sha3.min.js}"></script>

View File

@@ -14,7 +14,6 @@
<style>
.logo {
margin-top: -50px!important;
max-width: 50%;
margin: auto;
}
</style>
@@ -23,7 +22,7 @@
<div class="flex items-center justify-center flex-1">
<div x-data="loginForm()">
<img class="logo" th:src="@{/img/emograb_logo.png}"/>
<img class="logo" th:src="@{/img/emograb_logo.webp}"/>
<h2 class="text-2xl font-bold text-center">EmoGrab</h2>
<p class="text-center text-gray-500 text-sm pb-4">
EA Data Crawling Platform
@@ -69,7 +68,7 @@
</footer>
<!-- Alpine.js -->
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
<script th:src="@{/public/plugins/alpinejs@3.14.9/dist/cdn.min.js}" defer></script>
<!-- SHA3 224 -->
<script th:src="@{/public/plugins/js-sha3/sha3.min.js}"></script>
<script th:src="@{/admin/v1/static/giggity/toast.js}"></script>

View File

@@ -270,9 +270,7 @@
openEditForm(json)
}
else {
window.toastModule.errorLayer({
msg: json.data || '服务器错误'
});
Dog.error()
}
}
layui.table.on('tool(plans)', async function (obj) {
@@ -282,24 +280,16 @@
else if (obj.event == 'del') {
layui.layer.confirm('确定删除该计划任务吗?', function (index) {
layui.layer.close(index);
const load = layui.layer.load(2);
$.ajax({
url: '/admin/v1/manage/plan/delete',
method: 'POST',
data: {planId: obj.data.planId},
success: function (data) {
layui.table.reload('plans', {
page: {
curr: $(".layui-laypage-em").next().html() //当前页码值
}
});
layer.msg('删除成功', {offset: '15px', icon: 1, time: 1000})
},
error: function (res) {
var r = res.responseJSON;
layer.msg(r && r.data || '服务器错误',
{offset: '15px', icon: 2, time: 2000});
return
}
success: () => Dog.success({
msg: '删除成功', onClose: () => Dog.reloadTable('plans')
}),
error: res => Dog.error({msg: res}),
complete: () => layui.layer.close(load)
})
})
}

View File

@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head th:insert="~{admin/v1/include::head}"
th:with="title=${'计划任务管理'}">
th:with="title=${'计划任务'}">
</head>
<body>
@@ -10,7 +10,8 @@
<div class="manage-body">
<div>
<h1 class="manage-title">
<b>计划任务列表</b><a href="javascript:openNewForm()" class="operate">新增</a>
<i class="fa-fw fa-regular fa-calendar"></i>
<b>计划任务</b><a href="javascript:openNewForm()" class="operate">新增</a>
</h1>
</div>
@@ -39,25 +40,19 @@
})
.use(['table', 'form', 'dropdown', 'layer', 'xmSelect', 'cron'], function(){
var dropdown = layui.dropdown, table = layui.table, form = layui.form;
function switchTemplet(d) {
var fieldName = d.LAY_COL.field;
return `<input type="checkbox" lay-skin="switch" lay-text="|"
data-field="${fieldName}" data-id="${d.planId}"
${d[fieldName] ? 'checked' : ''} lay-filter="switchFilter">`;
}
table.render({
Helper.renderTable({
elem: '#plans',
url:'/admin/v1/manage/plan/list',
page:true, skin:'line',
idName: 'planId',
baseUrl:'/admin/v1/manage/plan',
cols: [ [
{type:'checkbox'},
{field:'enabled', title: '启用', width: 95, templet: switchTemplet},
{field:'openDayCheck', title: '交易日校验', width: 95, templet: switchTemplet},
{field:'enabled', title: '启用', width: 95, switchTemplet: true},
{field:'openDayCheck', title: '交易日校验', width: 95, switchTemplet: true},
{field:'planId', hide: true, width: 60, title: 'ID'},
{field:'planName', title: '计划名称'},
{field:'cronExpression', title: '计划表达式'},
{field:'indexCode', title: '指标代码'},
{field:'params', title: '请求参数',templet: function(d) {
{field:'params', title: '请求参数', templet: function(d) {
if (typeof d.params === 'object' && d.params !== null) {
return Object.entries(d.params).map(([key, value]) => `${key}=${value}`).join(', ');
}
@@ -71,49 +66,15 @@
}},
{field:'operation', title: '操作', toolbar: '#operationTpl'}
]]
});
form.on('switch(switchFilter)', function(obj) {
console.log(obj);
console.log(obj.elem.checked);
$.ajax({
url: '/admin/v1/manage/plan/updateBool',
method: 'POST',
data: {
planId: obj.elem.dataset.id,
field: obj.elem.dataset.field,
value: obj.elem.checked
},
success: function () {
layer.msg('操作成功', {
offset: '15px',
icon: 1,
time: 1000
},
function() {}
)
},
error: function (res) {
var r = res.responseJSON;
layer.msg(r && r.data || '服务器错误', {
offset: '15px',
icon: 2,
time: 1000
});
// 恢复 enabled 状态
obj.elem.checked = !obj.elem.checked;
layui.form.render('checkbox')
return
}
})
});
})
dropdown.render({
elem: '.operdown',
data: [
{title: '删除'},
{title: '启用', op: 'enable'},
{title: '停用', op: 'disable'},
{title: '开启交易日校验', op: 'enableOpenDayCheck'},
{title: '关闭交易日校验', op: 'disableOpenDayCheck'}],
{title: '删除', op: 'DELETE'},
{title: '启用', op: 'ENABLE'},
{title: '停用', op: 'DISABLE'},
{title: '开启交易日校验', op: 'ENABLE_OPEN_DAY_CHECK'},
{title: '关闭交易日校验', op: 'DISABLE_OPEN_DAY_CHECK'}],
click: function (data, othis){
var checked = layui.table.checkStatus('plans'), planIds = [];
if (!checked.data.length) {
@@ -124,7 +85,7 @@
planIds.push(plan.planId);
});
data = $.extend(data, {ids: planIds});
var op = function() {
var op = async function() {
$.ajax({
url: '/admin/v1/manage/plan/batchOp',
method: 'POST',
@@ -155,7 +116,7 @@
}
})
}
data.op ? op() : layer.confirm('确认批量删除吗?该操作不可恢复', function(){
data.op != 'DELETE' ? op() : layer.confirm('确认批量删除吗?该操作不可恢复', function(){
op();
})
}

View File

@@ -72,10 +72,7 @@ function openEditForm(r) {
offset: 'r',
content: $('#addProtocolMatch').html(),
success: function(layero, layerIndex) {
var el = $(layero);
['protocolId', 'className'].forEach(x => {
el.find(`[name="${x}"]`).val(r.data[x])
});
Helper.fillEditForm(r, layero, layerIndex);
}
})
}

View File

@@ -0,0 +1,108 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<div th:fragment="proxyExtra">
<script id="addProxySetting" type="text/html">
<style>.layui-form-select dl{max-height: 160px}</style>
<div class="layui-form" style="margin:10px 15px" id="editPlanForm" lay-filter="editPlanForm">
<div class="layui-form-item">
<input type="hidden" name="id"/>
<label class="layui-form-label">代理名称<span>*</span></label>
<div class="layui-input-block">
<input type="text" lay-verify="required" name="proxyName" placeholder="" autocomplete="off" class="layui-input"/>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">代理类型<span>*</span></label>
<div class="layui-input-block">
<select name="proxyType" lay-filter="proxyTypeFilter">
<option value="">选择代理类型</option>
<option value="DIRECT">直连</option>
<option value="HTTP">Http(s)</option>
<option value="SOCKS">Socks</option>
</select>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">代理主机<span>*</span></label>
<div class="layui-input-block">
<input type="text" lay-verify="required" name="proxyHost" placeholder="" autocomplete="off" class="layui-input"/>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">代理端口<span>*</span></label>
<div class="layui-input-block">
<input type="text" lay-verify="required" name="proxyPort" placeholder="" autocomplete="off" class="layui-input"/>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">忽略 HTTPS 校验<span>*</span></label>
<div class="layui-input-block">
<input type="checkbox"
name="ignoreHttpsVerification" lay-skin="switch" lay-filter="ignoreHttpsVerification" lay-text="ON|OFF">
</div>
</div>
<div style="display:none" class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn" lay-submit="*" lay-filter="submitProxySetting">提交</button>
</div>
</div>
</div>
</script>
<script type="text/javascript">
Helper.onSubmitForm('submitProxySetting', '/admin/v1/manage/proxySetting/save');
function openEditForm(r) {
if (r && r.ok) {
window.editLayer = Helper.openR({
title: `${r.data.id ? '编辑' : '新增'}代理设置`,
btn: ['提交', '关闭'],
yes: function (index, layero) {
layero.find('[lay-filter="submitProxySetting"]').click()
},
content: $('#addProxySetting').html(),
success: async function (layero, layerIndex) {
Helper.fillEditForm(r, layero, layerIndex);
}
})
}
else Dog.error({
msg: r && r.data || '服务器错误'
});
}
async function openNewForm(id) {
const json = await (await fetch((() => {
const url = '/admin/v1/manage/proxySetting/getOne';
if (id) return url + '?id=' + id;
return url
})())).json();
if (json.ok) {
openEditForm(json)
}
else {
Dog.error()
}
}
layui.table.on('tool(proxySettings)', async function (obj) {
if (obj.event == 'edit') {
openNewForm(obj.data.id)
}
else if (obj.event == 'del') {
layui.layer.confirm('确定删除该代理配置吗?', function (index) {
layui.layer.close(index);
const load = layui.layer.load(2);
$.ajax({
url: '/admin/v1/manage/proxySetting/delete',
method: 'POST',
data: {id: obj.data.id},
success: () => Dog.success({
msg: '删除成功', onClose: () => Dog.reloadTable('proxySettings')
}),
error: res => Dog.error({msg: res}),
complete: () => layui.layer.close(load)
})
})
}
})
</script>
</div>
</html>

View File

@@ -0,0 +1,138 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head th:insert="~{admin/v1/include::head}"
th:with="title=${'代理设置'}">
</head>
<body>
<ul th:replace="~{admin/v1/include::nav}"></ul>
<div class="manage-body">
<div>
<h1 class="manage-title">
<i class="fa-fw fa-regular fa-calendar"></i>
<b>代理设置</b><a href="javascript:openNewForm()" class="operate">新增</a>
</h1>
</div>
<button class="layui-btn layui-btn-sm operdown">
<span>选中项<i
class="layui-icon layui-icon-sm layui-icon-triangle-d"></i></span>
</button>
<div>
<table class="layui-table" id="proxySettings" lay-filter="proxySettings">
</table>
</div>
</div>
<div th:replace="~{admin/v1/include::feet}"></div>
<script type="text/html" id="operationTpl">
<div class="layui-btn-group">
<a class="layui-btn layui-btn-xs" lay-event="edit">编辑</a>
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>
</div>
</script>
<th:block th:replace="~{admin/v1/include::head-script}"></th:block>
<script th:inline="javascript">
layui
.extend({
xmSelect: '/admin/v1/static/layuiadmin/lib/xm-select',
cron: '/admin/v1/static/layuiadmin/lib/cron'
})
.use(['table', 'form', 'dropdown', 'layer', 'xmSelect', 'cron'], function(){
var dropdown = layui.dropdown, table = layui.table, form = layui.form;
table.render({
elem: '#proxySettings',
url:'/admin/v1/manage/proxySetting/list',
page:true, skin:'line',
cols: [ [
{type:'checkbox'},
{field:'id', hide: true, width: 60, title: 'ID'},
{field:'proxyName', title: '名称'},
{field:'proxyType', title: '类型'},
{field:'proxyHost', title: '主机'},
{field:'proxyPort', title: '端口'},
{field:'ignoreHttpsVerification', title: '忽略 HTTPS 校验', width: 95, templet: Helper.tableSwitchTemplet('id')},
{field:'operation', title: '操作', toolbar: '#operationTpl'}
]]
});
form.on('switch(switchFilter)', function(obj) {
console.log(obj);
console.log(obj.elem.checked);
$.ajax({
url: '/admin/v1/manage/proxySetting/updateBool',
method: 'POST',
data: {
id: obj.elem.dataset.id,
field: obj.elem.dataset.field,
value: obj.elem.checked
},
success: () => Dog.success({time: 1000}),
error: function (res) {
Dog.error({msg: res})
// 恢复 enabled 状态
obj.elem.checked = !obj.elem.checked;
layui.form.render('checkbox')
return
}
})
});
dropdown.render({
elem: '.operdown',
data: [
{title: '删除', op: 'DELETE'},
{title: '启用', op: 'ENABLE'},
{title: '停用', op: 'DISABLE'},
{title: '开启交易日校验', op: 'ENABLE_OPEN_DAY_CHECK'},
{title: '关闭交易日校验', op: 'DISABLE_OPEN_DAY_CHECK'}],
click: function (data, othis){
var checked = layui.table.checkStatus('proxySettings'), planIds = [];
if (!checked.data.length) {
layui.layer.msg('未选中任何项', {time: 1000});
return;
}
$.each(checked.data, function (i, plan){
planIds.push(plan.planId);
});
data = $.extend(data, {ids: planIds});
var op = async function() {
$.ajax({
url: '/admin/v1/manage/plan/batchOp',
method: 'POST',
data: data,
success: function () {
layer.msg('批量操作成功', {
offset: '15px',
icon: 1,
time: 1000
},
function() {
layui.table.reload('proxySettings', {
page: {
curr: $(".layui-laypage-em").next().html() //当前页码值
}
});
}
)
},
error: function (res) {
var r = res.responseJSON;
layer.msg(r&&r.data||'服务器错误', {
offset: '15px',
icon: 2,
time: 1000
});
return
}
})
}
data.op != 'DELETE' ? op() : layer.confirm('确认批量删除吗?该操作不可恢复', function(){
op();
})
}
});
})
</script>
<th:block th:replace="~{admin/v1/manage/proxySetting/include::proxyExtra}"></th:block>
</body>
</html>

View File

@@ -0,0 +1,218 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<div th:fragment="requestInfoExtra">
<script id="addRequestInfo" type="text/html">
<style>.layui-form-select dl{max-height: 160px}</style>
<div class="layui-form" style="margin:10px 15px" id="editRequestInfoForm" lay-filter="editRequestInfoForm">
<div class="layui-form-item">
<input type="hidden" name="id"/>
<label class="layui-form-label">计划名称<span>*</span></label>
<div class="layui-input-block">
<input type="text" lay-verify="required" name="name" placeholder="" autocomplete="off" class="layui-input"/>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">匿名<span>*</span></label>
<div class="layui-input-block">
<input type="checkbox" name="isAnonymous" lay-skin="switch" lay-filter="isAnonymous" checked lay-text="ON|OFF">
</div>
</div>
<div class="layui-form-item non-anonymous">
<label class="layui-form-label">用户名</label>
<div class="layui-input-block">
<input type="text" name="username" placeholder="" class="layui-input"/>
</div>
</div>
<div class="layui-form-item non-anonymous">
<label class="layui-form-label">密码</label>
<div class="layui-input-block">
<input type="password" name="password" placeholder="" autocomplete="new-password" class="layui-input"/>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">鉴权信息</label>
<div class="layui-input-block">
<input type="text" readonly name="authorization" placeholder="" autocomplete="off" class="layui-input"/>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">UID</label>
<div class="layui-input-block">
<input type="text" readonly name="uid" placeholder="" autocomplete="off" class="layui-input"/>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">Android ID<span>*</span></label>
<div class="layui-input-block">
<input type="text" lay-verify="required" name="androidId" placeholder="" autocomplete="off" class="layui-input"/>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">设备名称<span>*</span></label>
<div class="layui-input-block">
<input type="text" lay-verify="required" name="deviceName" placeholder="" autocomplete="off" class="layui-input"/>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">指纹<span>*</span></label>
<div class="layui-input-block">
<input type="text" lay-verify="required" name="fingerprint" placeholder="" autocomplete="off" class="layui-input"/>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">Software Type<span>*</span></label>
<div class="layui-input-block">
<input type="text" lay-verify="required" name="softwareType" placeholder="" autocomplete="off" class="layui-input"/>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">Chrome 版本号<span>*</span></label>
<div class="layui-input-block">
<input type="text" lay-verify="required" name="chromeVersion" placeholder="" autocomplete="off" class="layui-input"/>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">益盟版本号<span>*</span></label>
<div class="layui-input-block">
<input type="text" lay-verify="required" name="emoneyVersion" placeholder="" autocomplete="off" class="layui-input"/>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">OkHttp UA<span>*</span></label>
<div class="layui-input-block">
<input type="text" lay-verify="required" name="okHttpUserAgent" placeholder="" autocomplete="off" class="layui-input"/>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">Emapp-ViewMode<span>*</span></label>
<div class="layui-input-block">
<input type="text" lay-verify="required" name="emappViewMode" placeholder="" autocomplete="off" class="layui-input"/>
</div>
</div>
<div style="display:none" class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn" lay-submit="*" lay-filter="submitRequestInfo">提交</button>
</div>
</div>
</div>
</script>
<script type="text/javascript">
Helper.onSubmitForm('submitRequestInfo', '/admin/v1/manage/requestInfo/save');
function refreshAndroidId() {
let androidIdEl = document.querySelector('[name="androidId"]');
androidIdEl.value = Helper.randomHexString(16);
}
async function refreshDeviceInfo() {
let deviceNameEl = document.querySelector('[name="deviceName"]');
let fingerprintEl = document.querySelector('[name="fingerprint"]');
let softwareTypeEl = document.querySelector('[name="softwareType"]');
let json = await (await fetch('/admin/v1/config/emoneyRequest/getRandomDeviceInfo')).json();
if (!json.ok) {
Dog.error({msg: json.message || '获取随机设备信息失败'});
return
}
deviceNameEl.value = json.data.model;
fingerprintEl.value = json.data.fingerprint;
softwareTypeEl.value = json.data.deviceType;
refreshAndroidId()
}
async function refreshChromeVersion() {
let chromeVersionEl = document.querySelector('[name="chromeVersion"]');
let json = await (await fetch('/admin/v1/config/emoneyRequest/getRandomChromeVersion')).json();
if (!json.ok) {
Dog.error({msg: json.message || '获取随机 Chrome Version 失败'});
return
}
chromeVersionEl.value = json.data;
}
function openEditForm(r, done) {
if (done && typeof done === 'function') done(r);
if (r && r.ok) {
window.editLayer = Helper.openR({
title: `${r.data.id ? '编辑' : '新增'}请求信息`,
btn: ['随机设备信息', '随机 Chrome 版本', r.data.id ? '编辑' : '新增'],
btn1: () => refreshDeviceInfo() && !1,
btn2: () => refreshChromeVersion() && !1,
btn3: (_, o) => o.find('[lay-filter="submitRequestInfo"]').click() && !1,
content: $('#addRequestInfo').html(),
success: async function (layero, layerIndex) {
Helper.setLayerMainBtn(layero, -1);
var el = $(layero), extraSwitchFuncs = [];
// 覆写 isAnonymous
extraSwitchFuncs.isAnonymous = function (obj) {
const checked = obj.elem.value = obj.elem.checked;
const nonAnonymouses = document.querySelectorAll('.non-anonymous');
nonAnonymouses.forEach(non => {
if (!checked) {
// 非匿名,那 username/password 等就需要选必选
if (!non.querySelector('label>span')) {
$(non).children('label').append('<span>*</span>');
}
const input = $(non).find('input:first');
input.attr('lay-verify', 'required');
}
else {
$(non).children('label').children('span').remove();
$(non).find('lay-verify').removeAttr('lay-verify');
}
});
};
Helper.fillEditForm(r, layero, layerIndex, extraSwitchFuncs);
layui.event.call(this, 'form', 'switch(isAnonymous)', {
elem: el[0].querySelector('[name="isAnonymous"]'),
value: r.data.isAnonymous == 'true' || r.data.isAnonymous == true
});
}
})
}
else Dog.error({
msg: r && r.data || '服务器错误'
});
}
async function openNewRequestInfoForm(id, done) {
const json = await (await fetch((() => {
const url = '/admin/v1/manage/requestInfo/getOne';
if (id) return url + '?id=' + id;
return url
})())).json();
if (json.ok) {
openEditForm(json, done)
}
else {
window.toastModule.errorLayer({
msg: json.data || '服务器错误'
});
}
}
layui.table.on('tool(requestInfos)', async function (obj) {
if (obj.event == 'edit') {
openNewRequestInfoForm(obj.data.id)
}
else if (obj.event == 'copy') {
openNewRequestInfoForm(obj.data.id, r => {
r.data.id = null
})
}
else if (obj.event == 'del') {
layui.layer.confirm('确定删除该请求配置吗?', function (index) {
layui.layer.close(index);
const load = layui.layer.load(2);
$.ajax({
url: '/admin/v1/manage/requestInfo/delete',
method: 'POST',
data: {id: obj.data.id},
success: () => Dog.success({
msg: '删除成功', onClose: () => Dog.reloadTable('requestInfos')
}),
error: res => Dog.error({msg: res}),
complete: () => layui.layer.close(load)
})
})
}
})
</script>
</div>
</html>

View File

@@ -0,0 +1,98 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head th:insert="~{admin/v1/include::head}" th:with="title=${'计划任务管理'}">
</head>
<style>
.layui-form-item:after {
clear: unset;
}
</style>
<body>
<ul th:replace="~{admin/v1/include::nav}"></ul>
<div class="manage-body">
<div>
<h1 class="manage-title">
<b>请求信息管理</b><a href="javascript:openNewRequestInfoForm()"
class="operate">新增</a>
</h1>
</div>
<button class="layui-btn layui-btn-sm operdown">
<span>选中项<i
class="layui-icon layui-icon-sm layui-icon-triangle-d"></i></span>
</button>
<div>
<table class="layui-table" id="requestInfos"
lay-filter="requestInfos">
</table>
</div>
</div>
<div th:replace="~{admin/v1/include::feet}"></div>
<script type="text/html" id="operationTpl">
<div class="layui-btn-group">
<a class="layui-btn layui-btn-xs" lay-event="copy">复制</a>
<a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="edit">编辑</a>
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>
</div>
</script>
<th:block th:replace="~{admin/v1/include::head-script}"></th:block>
<script th:inline="javascript">
layui
.use(['table', 'form', 'dropdown', 'layer'], function(){
var dropdown = layui.dropdown, table = layui.table, form = layui.form;
Helper.renderTable({
elem: '#requestInfos',
baseUrl: '/admin/v1/manage/requestInfo',
idName: 'id',
cols: [ [
{type:'checkbox'},
{field:'id', width: 60, title: 'ID'},
{field:'name', title: '名称'},
{field:'isAnonymous', title: '匿名', width: 95, switchTemplet: true},
{field:'username', title: '用户名', width: 95},
{field:'uid', title: 'UID'},
{field:'androidVersion', title: '安卓版本'},
{field:'emoneyVersion', title: '益盟版本'},
{field:'operation', title: '操作', toolbar: '#operationTpl'}
]]
})
dropdown.render({
elem: '.operdown',
data: [
{title: '删除', op: 'DELETE'},
{title: '启用', op: 'enable'},
{title: '停用', op: 'disable'},
{title: '开启交易日校验', op: 'enableOpenDayCheck'},
{title: '关闭交易日校验', op: 'disableOpenDayCheck'}],
click: function (data, othis){
var checked = layui.table.checkStatus('requestInfos'), requestInfoIds = [];
if (!checked.data.length) {
Dog.error({msg: '未选中任何项', time: 1000});
return;
}
$.each(checked.data, function (i, requestInfo){
requestInfoIds.push(requestInfo.id);
});
data = $.extend(data, {ids: requestInfoIds});
var op = function() {
$.ajax({
url: '/admin/v1/manage/requestInfo/batchOp',
method: 'POST',
data: data,
success: () => Dog.success({msg: '批量操作成功', onClose: () => Dog.reloadTable('plans')}),
error: res => Dog.error({msg: res, time: 1000})
})
}
data.op !== 'DELETE' ? op() : layer.confirm('确认批量删除吗?该操作不可恢复', function(){
op();
})
}
});
})
</script>
<th:block
th:replace="~{/admin/v1/manage/requestInfo/include::requestInfoExtra}"></th:block>
</body>
</html>

View File

@@ -1,69 +0,0 @@
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Base64;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import quant.rich.emoney.util.VersionComparator;
public class WeibaPlayground {
private static final short[] XCUtil_short = new short[]{2176, 1289, 2869, 2868, 2854, 2893, 2904, 2219, 2231, 2231, 2227, 2297, 2284, 2284, 2228, 2214, 2218, 2209, 2210, 2285, 2215, 2230, 2220, 2216, 2210, 2218, 2234, 2210, 2285, 2208, 2220, 2222, 2284, 2221, 2214, 2228, 2227, 2219, 2220, 2221, 2214, 2285, 2227, 2219, 2227, 2300, 2218, 2215, 2302, 1330, 1378, 1393, 1382, 1321, 1971, 2022, 2044, 2034, 2043, 1960};
public static void main(String[] args) throws IOException {
System.out.println(
((Object)"ۤۢۢ").hashCode()
);
System.out.println(decodeShort(XCUtil_short, 1, 1, 1397));
// (1745665457, MTc0NTY2NTQ1N3xkZjJkOThjYw==)
// FM%5C%29GMR%2BGMJ*G%2CqdScCdHMacRp%3D%3D
Path path = Path.of("E:\\eclipse-workspace\\emoney-auto\\conf\\system\\emoneyRequest.androidChromeVersions.json");
String str = Files.readString(path);
ObjectMapper mapper = new ObjectMapper();
ArrayNode node = (ArrayNode) mapper.readTree(str);
List<String> ver = mapper.convertValue(node, new TypeReference<List<String>>() {});
ver.sort(VersionComparator.INSTANCE.reversed());
Set<String> verSet = new LinkedHashSet<>(ver);
ver = List.copyOf(verSet);
ArrayNode ja = mapper.valueToTree(verSet);
Files.writeString(path, ja.toPrettyString());
//System.out.println(ccc("1745665457", "MTc0NTY2NTQ1N3xkZjJkOThjYw=="));
}
public static String decodeShort(short[] sArr, int i, int i2, int i3) {
char[] cArr = new char[i2];
for (int i4 = 0; i4 < i2; i4++) {
cArr[i4] = (char) (sArr[i + i4] ^ i3);
}
return new String(cArr);
}
public static String ccc(String var0, String var1) throws UnsupportedEncodingException {
String decoded = new String(Base64.getDecoder().decode(var1)).trim();
StringBuilder sb = new StringBuilder();
// 获取时间戳最后一位作为减法因子(如 1745660488 → 8
int offset = var0.charAt(9) - '0'; // var0 是时间戳
for (char ch : decoded.toCharArray()) {
if (ch == '=') continue;
sb.append((char)(ch - offset));
}
return URLEncoder.encode(sb.toString(), "UTF-8");
}
}

View File

@@ -233,7 +233,7 @@ public class EmoneyIndexScraper {
try {
Files.writeString(
Path.of("./conf/extra/nonParamsIndexDetail." + detail.getNameCode() + ".json"),
Path.of("./resources/conf/extra/nonParamsIndexDetail." + detail.getNameCode() + ".json"),
MAPPER.valueToTree(detail).toPrettyString());
} catch (IllegalArgumentException | IOException e) {
// TODO Auto-generated catch block

View File

@@ -4,8 +4,10 @@ import quant.rich.emoney.EmoneyAutoApplication;
import quant.rich.emoney.client.EmoneyClient;
import quant.rich.emoney.client.WebviewClient;
import quant.rich.emoney.client.WebviewClient.WebviewResponseWrapper;
import quant.rich.emoney.util.SmartResourceResolver;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map.Entry;
@@ -21,6 +23,8 @@ import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import io.micrometer.core.instrument.util.IOUtils;
@SpringBootTest
@ContextConfiguration(classes = EmoneyAutoApplication.class)
@RunWith(SpringJUnit4ClassRunner.class)
@@ -31,8 +35,8 @@ public class RelativeEmoneyScraper {
@Test
void test() throws Exception {
String js = Files.readString(Path.of("./conf/extra/indexJs.js"));
String js = IOUtils.toString(SmartResourceResolver.loadResource("./conf/extra/indexJs.js"), StandardCharsets.UTF_8);
String jsArrayText;
Matcher m = nonParamsIndexDetailPattern.matcher(js);

View File

@@ -0,0 +1,38 @@
package quant.rich;
import lombok.extern.slf4j.Slf4j;
import quant.rich.emoney.EmoneyAutoApplication;
import quant.rich.emoney.client.EmoneyClient;
import quant.rich.emoney.client.WebviewClient;
import quant.rich.emoney.client.WebviewClient.WebviewResponseWrapper;
import quant.rich.emoney.util.SmartResourceResolver;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@SpringBootTest
@ContextConfiguration(classes = EmoneyAutoApplication.class)
@RunWith(SpringJUnit4ClassRunner.class)
@Slf4j
public class SmartResourceLoaderTest {
@Test
void test() throws Exception {
SmartResourceResolver.loadResource("/conf/system/proxy.json");
}
}

View File

@@ -1,9 +1,8 @@
package quant.rich;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@@ -11,17 +10,32 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.graalvm.polyglot.*;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import io.micrometer.core.instrument.util.IOUtils;
import quant.rich.emoney.entity.config.SmartViewWriter;
import quant.rich.emoney.pojo.dto.IndexDetail;
import quant.rich.emoney.pojo.dto.NonParamsIndexDetail;
import quant.rich.emoney.util.SmartResourceResolver;
public class TestIndexJsMatch {
private static final ObjectMapper MAPPER = new ObjectMapper();
private static final SmartViewWriter writer = new SmartViewWriter();
static {
MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
public static void main(String[] args) throws IOException {
String script = Files.readString(Path.of("./conf/extra/indexJs.js"), StandardCharsets.UTF_8);
//String script = Files.readString(Path.of("./conf/extra/indexJs.js"), StandardCharsets.UTF_8);
String script = IOUtils.toString(SmartResourceResolver.loadResource("./conf/extra/indexJs.js"), StandardCharsets.UTF_8);
/** 一般来说如果要匹配,只会匹配出一个,但是为了保险起见还是做一个 List 来存储 */
@@ -30,6 +44,16 @@ public class TestIndexJsMatch {
System.out.println("✔ 匹配数组,共 " + array.size() + " 个对象");
for (JsonNode obj : array) {
System.out.println(obj.toPrettyString());
if (!obj.has("nameCode")) continue;
String nameCode = obj.get("nameCode").asText();
// 载入已有本地文件填充 original 字段
String path = "./conf/extra/indexDetail/nonParams/" + nameCode + ".json";
InputStream stream = SmartResourceResolver.loadResource(path);
NonParamsIndexDetail detail = MAPPER.readValue(stream, NonParamsIndexDetail.class);
detail.setOriginal(obj.toString());
String joString = MAPPER.writerWithView(IndexDetail.class).writeValueAsString(detail);
SmartResourceResolver.saveText(path, joString);
}
}
}