新增(移动)一些应该放在“管理”形成列表管理,而非放在“设置”形成单一配置的内容
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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)) {
|
||||
switch (op) {
|
||||
case ENABLE:
|
||||
uw.set(Plan::getEnabled, true);
|
||||
}
|
||||
else if ("disable".equals(op)) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
433
src/main/java/quant/rich/emoney/entity/sqlite/RequestInfo.java
Normal file
433
src/main/java/quant/rich/emoney/entity/sqlite/RequestInfo.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
*
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package quant.rich.emoney.interfaces;
|
||||
|
||||
public interface IQueryableEnum {
|
||||
|
||||
public default String getName() {
|
||||
return name();
|
||||
}
|
||||
String name();
|
||||
public String getNote();
|
||||
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<>();
|
||||
|
||||
@@ -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) {
|
||||
// 此处只是读取文件,并不关心该文件是否可写
|
||||
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;
|
||||
|
||||
@@ -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);
|
||||
@@ -340,10 +359,24 @@ 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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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> {
|
||||
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -38,6 +39,44 @@ public class CallerLockUtil {
|
||||
}).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
|
||||
*/
|
||||
|
||||
@@ -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 登录的密码
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
111
src/main/java/quant/rich/emoney/util/SmartResourceResolver.java
Normal file
111
src/main/java/quant/rich/emoney/util/SmartResourceResolver.java
Normal 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);
|
||||
}
|
||||
|
||||
// 否则回退到 classpath(JAR、WAR、IDE)
|
||||
InputStream in = SmartResourceResolver.class.getClassLoader().getResourceAsStream(relativePath);
|
||||
if (in != null) {
|
||||
log.debug("从 classpath 内部加载资源: {}", relativePath);
|
||||
return in;
|
||||
}
|
||||
|
||||
throw new FileNotFoundException("无法找到资源: " + relativePath);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("读取资源失败: " + relativePath, e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void saveText(String relativePath, String content) throws IOException {
|
||||
Path outputPath = resolveExternalPath(relativePath);
|
||||
Files.createDirectories(outputPath.getParent()); // 确保目录存在
|
||||
Files.writeString(outputPath, content, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
log.debug("写入外部资源文件成功: {}", outputPath);
|
||||
}
|
||||
|
||||
private static Path resolveExternalPath(String relativePath) {
|
||||
try {
|
||||
Path basePath;
|
||||
if (runningFrom == RunningFrom.JAR) {
|
||||
basePath = Paths.get(SmartResourceResolver.class.getProtectionDomain()
|
||||
.getCodeSource().getLocation().toURI()).getParent();
|
||||
return basePath.resolve(relativePath).normalize();
|
||||
} else if (runningFrom == RunningFrom.WAR) {
|
||||
basePath = Paths.get(SmartResourceResolver.class.getProtectionDomain()
|
||||
.getCodeSource().getLocation().toURI()); // e.g., WEB-INF/classes/
|
||||
return basePath.resolve(relativePath).normalize();
|
||||
} else {
|
||||
// IDE 环境:返回 src/main/resources 下真实文件
|
||||
return Paths.get("src/main/resources", relativePath).normalize();
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isRunningFromJar() {
|
||||
String path = SmartResourceResolver.class.getResource(
|
||||
SmartResourceResolver.class.getSimpleName() + ".class").toString();
|
||||
return path.startsWith("jar:");
|
||||
}
|
||||
|
||||
private static boolean isRunningFromWar() {
|
||||
String path = SmartResourceResolver.class.getResource(
|
||||
SmartResourceResolver.class.getSimpleName() + ".class").toString();
|
||||
return path.contains("/WEB-INF/classes/");
|
||||
}
|
||||
|
||||
private static enum RunningFrom {
|
||||
JAR,
|
||||
WAR,
|
||||
IDE
|
||||
}
|
||||
}
|
||||
@@ -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 {};
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
@@ -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('')
|
||||
}
|
||||
BIN
src/main/resources/static/img/emograb_logo.webp
Normal file
BIN
src/main/resources/static/img/emograb_logo.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
@@ -3,20 +3,21 @@
|
||||
|
||||
<head th:fragment="head">
|
||||
<meta charset="utf-8" />
|
||||
<title>
|
||||
[[${(title!=null?title:'后台管理')}]]
|
||||
</title>
|
||||
<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">
|
||||
<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;
|
||||
@@ -28,54 +29,104 @@
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<li class="layui-nav-item"><a href="#"> <i
|
||||
class="fa-fw fa-solid fa-chalkboard "></i> 控制台
|
||||
</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>
|
||||
<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>
|
||||
<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>
|
||||
<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}"> <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}">
|
||||
<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>
|
||||
</dl>
|
||||
</th:block>
|
||||
</li>
|
||||
</th:block></li>
|
||||
|
||||
</ul>
|
||||
<script type="text/html" id="editUser">
|
||||
@@ -116,9 +167,11 @@
|
||||
<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>
|
||||
|
||||
@@ -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 />
|
||||
©2025-[[${#dates.format(new java.util.Date().getTime(),'yyyy')}]]
|
||||
<a href="#">Latte</a>
|
||||
All Rights Reserved.
|
||||
Driven by Latte<br /> ©2025-[[${#dates.format(new
|
||||
java.util.Date().getTime(),'yyyy')}]] <a href="#">Latte</a> All
|
||||
Rights Reserved.
|
||||
</div>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,20 +40,14 @@
|
||||
})
|
||||
.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: '计划表达式'},
|
||||
@@ -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();
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -32,7 +36,7 @@ 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);
|
||||
|
||||
38
src/test/java/quant/rich/SmartResourceLoaderTest.java
Normal file
38
src/test/java/quant/rich/SmartResourceLoaderTest.java
Normal 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");
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user