新增(移动)一些应该放在“管理”形成列表管理,而非放在“设置”形成单一配置的内容
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)) {
|
||||
uw.set(Plan::getEnabled, true);
|
||||
}
|
||||
else if ("disable".equals(op)) {
|
||||
switch (op) {
|
||||
case ENABLE:
|
||||
uw.set(Plan::getEnabled, true);
|
||||
break;
|
||||
case DISABLE:
|
||||
uw.set(Plan::getEnabled, false);
|
||||
}
|
||||
else if ("enableOpenDayCheck".equals(op)) {
|
||||
break;
|
||||
case ENABLE_OPEN_DAY_CHECK:
|
||||
uw.set(Plan::getOpenDayCheck, true);
|
||||
}
|
||||
else if ("disableOpenDayCheck".equals(op)) {
|
||||
break;
|
||||
case DISABLE_OPEN_DAY_CHECK:
|
||||
uw.set(Plan::getOpenDayCheck, false);
|
||||
break;
|
||||
default:
|
||||
throw RException.badRequest("未知操作");
|
||||
}
|
||||
else {
|
||||
throw RException.badRequest("未识别的操作");
|
||||
}
|
||||
|
||||
return R.judge(planService.update(uw));
|
||||
return R.judge(() -> planService.update(uw));
|
||||
}
|
||||
|
||||
private static enum PlanBatchOp implements IQueryableEnum {
|
||||
DELETE("删除"),
|
||||
ENABLE("启用"),
|
||||
DISABLE("停用"),
|
||||
ENABLE_OPEN_DAY_CHECK("开启交易日校验"),
|
||||
DISABLE_OPEN_DAY_CHECK("关闭交易日校验");
|
||||
|
||||
private String note;
|
||||
|
||||
private PlanBatchOp(String note) {
|
||||
this.note = note;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNote() {
|
||||
return note;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
try {
|
||||
// 此处只是读取文件,并不关心该文件是否可写
|
||||
configString = IOUtils.toString(SmartResourceResolver.loadResource(path), Charset.defaultCharset());
|
||||
} catch (UncheckedIOException e) {
|
||||
String field = fieldClassCache.inverse().get(configClass);
|
||||
log.warn("Cannot read config {}.json: {}", field, e.getMessage());
|
||||
return config;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package quant.rich.emoney.service;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.Serializable;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.text.MessageFormat;
|
||||
@@ -30,6 +32,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
import io.micrometer.core.instrument.util.IOUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
@@ -44,7 +47,7 @@ import quant.rich.emoney.pojo.dto.IndexDetail;
|
||||
import quant.rich.emoney.pojo.dto.NonParamsIndexDetail;
|
||||
import quant.rich.emoney.pojo.dto.NonParamsIndexDetail.NonParamsIndexDetailData;
|
||||
import quant.rich.emoney.util.EncryptUtils;
|
||||
import quant.rich.emoney.util.SpringContextHolder;
|
||||
import quant.rich.emoney.util.SmartResourceResolver;
|
||||
import quant.rich.emoney.pojo.dto.ParamsIndexDetail;
|
||||
|
||||
/**
|
||||
@@ -76,13 +79,17 @@ public class IndexDetailService {
|
||||
*/
|
||||
@CacheEvict(cacheNames="@indexDetailService.getIndexDetail(Serializable)", key="#indexCode.toString()")
|
||||
public IndexDetail forceRefreshAndGetIndexDetail(Serializable indexCode) {
|
||||
Path path = getIndexDetailPath(indexCode);
|
||||
try {
|
||||
Files.deleteIfExists(path);
|
||||
} catch (IOException e) {
|
||||
String msg = MessageFormat.format("本地 IndexDetail 文件删除失败,path: {0}, msg: {1}", path.toString(), e.getLocalizedMessage());
|
||||
throw new RuntimeException(msg, e);
|
||||
|
||||
// 刷新的本质就是从网络获取,因为此处已经清理了缓存,所以直接从网络获取后再
|
||||
// 走一次 getIndexDetail,获取到的就是从网络保存到了本地的,此时缓存也更新了
|
||||
|
||||
if (!hasParams(indexCode)) {
|
||||
getNonParamsIndexDetailOnline(indexCode);
|
||||
}
|
||||
else {
|
||||
getParamsIndexDetailOnline(indexCode);
|
||||
}
|
||||
|
||||
return getIndexDetail(indexCode);
|
||||
}
|
||||
|
||||
@@ -105,11 +112,11 @@ public class IndexDetailService {
|
||||
|
||||
private ParamsIndexDetail getParamsIndexDetail(Serializable indexCode) {
|
||||
// 先判断本地有没有
|
||||
Path localFilePath = getIndexDetailPath(indexCode);
|
||||
if (Files.exists(localFilePath)) {
|
||||
InputStream stream = getIndexDetailStream(indexCode);
|
||||
if (stream != null) {
|
||||
ParamsIndexDetail detail = null;
|
||||
try {
|
||||
String str = Files.readString(localFilePath);
|
||||
String str = IOUtils.toString(stream, StandardCharsets.UTF_8);
|
||||
detail = mapper.readValue(str, ParamsIndexDetail.class);
|
||||
} catch (IOException e) {
|
||||
log.warn("无法获取本地无参数指标说明,将尝试重新从网络获取 indexCode: {}", indexCode, e);
|
||||
@@ -122,6 +129,11 @@ public class IndexDetailService {
|
||||
return getParamsIndexDetailOnline(indexCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从网络获取有参指标详情
|
||||
* @param indexCode
|
||||
* @return
|
||||
*/
|
||||
private ParamsIndexDetail getParamsIndexDetailOnline(Serializable indexCode) {
|
||||
|
||||
try {
|
||||
@@ -147,13 +159,16 @@ public class IndexDetailService {
|
||||
if (code == 0) {
|
||||
ParamsIndexDetail detail = mapper.treeToValue(result.get("detail"), ParamsIndexDetail.class);
|
||||
if (detail == null) {
|
||||
// 网络访问成功但为 null, 新建一空 detail
|
||||
/** 网络访问成功但为 null, 新建一空 detail **/
|
||||
detail = new ParamsIndexDetail();
|
||||
detail.setCode(indexCode.toString());
|
||||
detail.getDescriptions().add("该指标说明接口返回为空");
|
||||
}
|
||||
// 清洗内容:凡是文本类型的内容的,都要清洗一遍,判断是否有脚本、
|
||||
// 视频和图片等,有的要清除并记录,以免有泄露网址、客户端 IP 风险
|
||||
else {
|
||||
detail.setOriginal(result.get("detail").toString());
|
||||
}
|
||||
/** 清洗内容:凡是文本类型的内容的,都要清洗一遍,判断是否有脚本、
|
||||
视频和图片等,有的要清除并记录,以免有泄露网址、客户端 IP 风险*/
|
||||
detail.sanitize();
|
||||
saveIndexDetail(detail);
|
||||
return detail;
|
||||
@@ -180,11 +195,11 @@ public class IndexDetailService {
|
||||
*/
|
||||
private NonParamsIndexDetail getNonParamsIndexDetail(Serializable indexCode) {
|
||||
// 先判断本地有没有
|
||||
Path localFilePath = getIndexDetailPath(indexCode);
|
||||
if (Files.exists(localFilePath)) {
|
||||
InputStream stream = getIndexDetailStream(indexCode);
|
||||
if (stream != null) {
|
||||
NonParamsIndexDetail detail = null;
|
||||
try {
|
||||
String str = Files.readString(localFilePath);
|
||||
String str = IOUtils.toString(stream, StandardCharsets.UTF_8);
|
||||
detail = mapper.readValue(str, NonParamsIndexDetail.class);
|
||||
}
|
||||
catch (IOException e) {
|
||||
@@ -270,6 +285,9 @@ public class IndexDetailService {
|
||||
.header("Referer", url)
|
||||
.header("Accept-Language", "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7");
|
||||
List<NonParamsIndexDetail> valids = new ArrayList<>();
|
||||
|
||||
// 循环获取脚本,一旦获取的脚本内正则匹配到包含无参
|
||||
// 指标的文本,立即转换为 json、转换指标并结束循环
|
||||
scriptLoop:
|
||||
for (String scriptUrl : scripts) {
|
||||
Request scriptRequest = scriptBuilder.url(scriptUrl).build();
|
||||
@@ -298,6 +316,7 @@ public class IndexDetailService {
|
||||
obj.has("nameCode") && obj.get("nameCode").isTextual() &&
|
||||
obj.has("data") && obj.get("data").isArray()) {
|
||||
NonParamsIndexDetail detail = mapper.treeToValue(obj, NonParamsIndexDetail.class);
|
||||
detail.setOriginal(obj.toString());
|
||||
valids.add(detail);
|
||||
foundAny = true;
|
||||
}
|
||||
@@ -330,7 +349,7 @@ public class IndexDetailService {
|
||||
if (!numericPattern.matcher(detail.getNameCode()).matches()) {
|
||||
continue;
|
||||
}
|
||||
Path path = getIndexDetailPath(detail);
|
||||
String path = getIndexDetailPath(detail);
|
||||
// 判断是否是需求的 detail
|
||||
if (indexCode.toString().equals(detail.getIndexCode())) {
|
||||
loadImages(detail);
|
||||
@@ -339,11 +358,25 @@ public class IndexDetailService {
|
||||
// 清洗内容:凡是文本类型的内容的,都要清洗一遍,判断是否有脚本、
|
||||
// 视频和图片等,有的要清除并记录,以免有泄露网址、客户端 IP 风险
|
||||
detail.sanitize();
|
||||
|
||||
if (!Files.exists(path)) {
|
||||
|
||||
InputStream inputStream = SmartResourceResolver.loadResource(path);
|
||||
if (inputStream == null) {
|
||||
// 不存在则保存
|
||||
saveIndexDetail(detail);
|
||||
}
|
||||
else {
|
||||
// 判断 original 是否一致,不一致则更新
|
||||
NonParamsIndexDetail existed;
|
||||
try {
|
||||
existed = mapper.readValue(inputStream, NonParamsIndexDetail.class);
|
||||
if (!existed.getOriginal().equals(detail.getOriginal())) {
|
||||
saveIndexDetail(detail);
|
||||
}
|
||||
}
|
||||
catch (IOException e) {
|
||||
log.debug("读取本地存在的 NonParamsIndexDetail 文件成功,但转换失败。格式错误?路径:{}", path, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (targetDetail == null) {
|
||||
@@ -354,9 +387,9 @@ public class IndexDetailService {
|
||||
List<String> items = List.of("该指标说明接口返回为空");
|
||||
data.setItems(items);
|
||||
targetDetail.getData().add(data);
|
||||
Path path = getIndexDetailPath(targetDetail);
|
||||
String path = getIndexDetailPath(targetDetail);
|
||||
|
||||
if (!Files.exists(path)) {
|
||||
if (SmartResourceResolver.loadResource(path) == null) {
|
||||
// 不存在则保存
|
||||
saveIndexDetail(targetDetail);
|
||||
}
|
||||
@@ -431,16 +464,16 @@ public class IndexDetailService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存指标详情到本地文件
|
||||
* 保存指标详情到本地文件,无论其原本是否存在
|
||||
* @param <Description>
|
||||
* @param des
|
||||
*/
|
||||
private <Description extends IndexDetail> void saveIndexDetail(Description des) {
|
||||
SmartViewWriter writer = new SmartViewWriter();
|
||||
String joString = writer.writeWithSmartView(des, IndexDetail.class);
|
||||
Path path = getIndexDetailPath(des);
|
||||
String path = getIndexDetailPath(des);
|
||||
try {
|
||||
Files.writeString(path, joString);
|
||||
SmartResourceResolver.saveText(path, joString);
|
||||
}
|
||||
catch (IOException e) {
|
||||
log.error("写入指标详情到 {} 失败", path.toString(), e);
|
||||
@@ -453,12 +486,12 @@ public class IndexDetailService {
|
||||
* @param description
|
||||
* @return
|
||||
*/
|
||||
private <Description extends IndexDetail> Path getIndexDetailPath(Description description) {
|
||||
private <Description extends IndexDetail> String getIndexDetailPath(Description description) {
|
||||
Path path = Path.of(new StringBuilder(filePath)
|
||||
.append((description instanceof NonParamsIndexDetail) ? "nonParams/": "params/")
|
||||
.append(description.getIndexCode())
|
||||
.append(".json").toString());
|
||||
return path;
|
||||
return path.normalize().toString();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -466,13 +499,14 @@ public class IndexDetailService {
|
||||
* @param indexCode
|
||||
* @return
|
||||
*/
|
||||
private Path getIndexDetailPath(Serializable indexCode) {
|
||||
private InputStream getIndexDetailStream(Serializable indexCode) {
|
||||
boolean hasParams = hasParams(indexCode);
|
||||
Path path = Path.of(new StringBuilder(filePath)
|
||||
String path = new StringBuilder(filePath)
|
||||
.append(!hasParams ? "nonParams/": "params/")
|
||||
.append(indexCode)
|
||||
.append(".json").toString());
|
||||
return path;
|
||||
.append(".json").toString();
|
||||
InputStream inputStream = SmartResourceResolver.loadResource(path);
|
||||
return inputStream;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -37,6 +38,44 @@ public class CallerLockUtil {
|
||||
return new WeakReference<>(l);
|
||||
}).get();
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ 方式三:尝试获取锁并运行,支持超时,失败后不阻塞
|
||||
* @return true 表示成功执行,false 表示未获得锁
|
||||
*/
|
||||
public static boolean tryRunWithCallerLock(Runnable task, long timeoutMs, Object... extraKeys) {
|
||||
ReentrantLock lock = acquireLock(extraKeys);
|
||||
boolean locked = false;
|
||||
try {
|
||||
locked = lock.tryLock(timeoutMs, TimeUnit.MILLISECONDS);
|
||||
if (locked) {
|
||||
task.run();
|
||||
}
|
||||
return locked;
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
} finally {
|
||||
if (locked) lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ 非阻塞获取锁,超时失败返回 null 或抛异常
|
||||
*/
|
||||
public static <T> Optional<T> tryCallWithCallerLock(Callable<T> task, long timeoutMs, Object... extraKeys) {
|
||||
ReentrantLock lock = acquireLock(extraKeys);
|
||||
boolean locked = false;
|
||||
try {
|
||||
locked = lock.tryLock(timeoutMs, TimeUnit.MILLISECONDS);
|
||||
if (!locked) return Optional.empty();
|
||||
return Optional.of(task.call());
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
if (locked) lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造调用者方法 + 附加参数为 key
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user