diff --git a/src/main/java/quant/rich/emoney/client/EmoneyClient.java b/src/main/java/quant/rich/emoney/client/EmoneyClient.java index d39ae6a..37a4edb 100644 --- a/src/main/java/quant/rich/emoney/client/EmoneyClient.java +++ b/src/main/java/quant/rich/emoney/client/EmoneyClient.java @@ -29,7 +29,8 @@ import quant.rich.emoney.util.SpringContextHolder; import okhttp3.OkHttpClient; /** - * 益盟操盘手基本请求客户端,提供基本功能 + * 益盟操盘手基本请求客户端,提供基本功能,如登录和请求。 + *
本类一般只提供静态方法,故不能也不该实例化本类。具体请求内容需要自己封装。整个系统共用一套鉴权,所以不要复制本例 *
请求头顺序
**
益盟操盘手对于不同的 protocolId 有不同的 URL 负责。在进行某类请求之前,请先通过调试 APP 进行确认,否则可能无法获取到相应内容 + * * @param protocolId - * @return + * @return 对应的 URL,当所给 protocolId 为 null 或非 String/Integer 类型时,返回为 null,使用前需要检查 */ private static String getUrlByProtocolId(Serializable protocolId) { + String strProtocolId; if (protocolId instanceof Integer intProtocolId) { - switch (intProtocolId) { - case 9400: return STRATEGY_URL; - default: return MBS_URL; - } + strProtocolId = String.valueOf(intProtocolId); } - else if (protocolId instanceof String strProtocolId) { - switch (strProtocolId) { + else if (protocolId instanceof String s) { + strProtocolId = s; + } + else { + return null; + } + switch (strProtocolId) { + case STRATEGY_X_PROTOCOL_ID: return STRATEGY_URL; case LOGIN_X_PROTOCOL_ID: return LOGIN_URL; case RELOGIN_X_PROTOCOL_ID: return RELOGIN_URL; - default: return null; - } + default: return MBS_URL; } - return null; } /** @@ -126,6 +131,9 @@ public class EmoneyClient implements Cloneable { return requestResponseInspectService; } + /** + * 不允许外部实例化对象 + */ private EmoneyClient() {} /** @@ -142,7 +150,6 @@ public class EmoneyClient implements Cloneable { } } - /** * 使用系统管理的用户名密码登录 *
建议仅在调试时使用,其他情况请用 {@code loginWithManaged()}
@@ -182,7 +189,7 @@ public class EmoneyClient implements Cloneable { * 触发重登陆验证 * @return */ - public static Boolean relogin() { + public static Boolean reloginCheck() { RequestInfo requestInfo = getDefaultRequestInfo(); ObjectNode reloginObject = requestInfo.getReloginObject(); if (reloginObject == null) { @@ -250,11 +257,9 @@ public class EmoneyClient implements Cloneable { */ private static Boolean login(ObjectNode formObject) { try { - //OkHttpClient okHttpClient = new OkHttpClient(); RequestInfo requestInfo = getDefaultRequestInfo(); OkHttpClient okHttpClient = OkHttpClientProvider.getInstance(); MediaType type = MediaType.parse("application/json"); - //type.charset(StandardCharsets.UTF_8); byte[] content = formObject.toString().getBytes("utf-8"); RequestBody body = RequestBody.create( content, type); @@ -273,7 +278,7 @@ public class EmoneyClient implements Cloneable { .header("Authorization", "") .header("X-Android-Agent", requestInfo.getXAndroidAgent()) .header("Emapp-ViewMode", requestInfo.getEmappViewMode()); - //.header("User-Agent", requestInfo.getOkHttpUserAgent()) + //此处 User-Agent 由 ByteBuddy 拦截修改 Request request = requestBuilder.build(); @@ -312,8 +317,10 @@ public class EmoneyClient implements Cloneable { /** * 获取基本返回 Base_Response * - * @param
+ * 此处提供的 OkHttpClient 方便使用平台配置的代理,方便是否启用 HTTPS 证书认证等
* @see quant.rich.emoney.entity.config.ProxyConfig
* @see okhttp3.internal.http.BridgeInterceptor
*/
diff --git a/src/main/java/quant/rich/emoney/component/RequireAuthAndProxyAspect.java b/src/main/java/quant/rich/emoney/component/RequireAuthAndProxyAspect.java
index ffad611..77b48c7 100644
--- a/src/main/java/quant/rich/emoney/component/RequireAuthAndProxyAspect.java
+++ b/src/main/java/quant/rich/emoney/component/RequireAuthAndProxyAspect.java
@@ -57,7 +57,7 @@ public class RequireAuthAndProxyAspect {
throw new RuntimeException("鉴权登录失败");
}
}
- else if (!EmoneyClient.relogin()) {
+ else if (!EmoneyClient.reloginCheck()) {
throw new RuntimeException("检查重鉴权失败");
}
diff --git a/src/main/java/quant/rich/emoney/config/SecurityConfig.java b/src/main/java/quant/rich/emoney/config/SecurityConfig.java
index c7633ed..200f339 100644
--- a/src/main/java/quant/rich/emoney/config/SecurityConfig.java
+++ b/src/main/java/quant/rich/emoney/config/SecurityConfig.java
@@ -26,7 +26,10 @@ public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
- .headers(headers -> headers.cacheControl(cache -> cache.disable()))
+ .headers(headers -> headers
+ .cacheControl(cache -> cache.disable())
+ .frameOptions(f -> f.sameOrigin())
+ )
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/favicon.ico").permitAll()
diff --git a/src/main/java/quant/rich/emoney/controller/common/ErrorPageController.java b/src/main/java/quant/rich/emoney/controller/common/ErrorPageController.java
index 9783347..59ea1fb 100644
--- a/src/main/java/quant/rich/emoney/controller/common/ErrorPageController.java
+++ b/src/main/java/quant/rich/emoney/controller/common/ErrorPageController.java
@@ -29,21 +29,8 @@ public class ErrorPageController implements ErrorController {
@GetMapping(value = ERROR_PATH)
@PostMapping(value = ERROR_PATH)
public String errorHtml(HttpServletRequest request) {
- HttpStatus status = getStatus(request);
String prefix = "error/";
-
return prefix + "error_400";
-
- // switch (status) {
- // case BAD_REQUEST:
- // return prefix + "error_400";
- // case NOT_FOUND:
- // return prefix + "error_404";
- // case METHOD_NOT_ALLOWED:
- // return prefix + "error_405";
- // default:
- // return prefix + "error_5xx";
- // }
}
@GetMapping(value = ERROR_PATH, produces = "application/json")
diff --git a/src/main/java/quant/rich/emoney/controller/common/KaptchaController.java b/src/main/java/quant/rich/emoney/controller/common/KaptchaController.java
index e805cec..4bddcf3 100644
--- a/src/main/java/quant/rich/emoney/controller/common/KaptchaController.java
+++ b/src/main/java/quant/rich/emoney/controller/common/KaptchaController.java
@@ -13,6 +13,8 @@ import org.springframework.web.bind.annotation.RestController;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import quant.rich.emoney.service.AuthService;
+import quant.rich.emoney.util.ArithmeticCaptchaGen;
+import quant.rich.emoney.util.ArithmeticCaptchaGen.Captcha;
@RestController
@RequestMapping("/captcha")
@@ -20,13 +22,13 @@ public class KaptchaController extends BaseController {
@Autowired
DefaultKaptcha kaptcha;
-
@GetMapping(value = "/get", produces = MediaType.IMAGE_JPEG_VALUE)
public byte[] getCaptcha() throws Exception {
- String createText = kaptcha.createText();
+
+ Captcha captcha = ArithmeticCaptchaGen.generate();
ByteArrayOutputStream os = new ByteArrayOutputStream();
- session.setAttribute(AuthService.CAPTCHA, createText);
- ImageIO.write(kaptcha.createImage(createText), "jpg", os);
+ session.setAttribute(AuthService.CAPTCHA, captcha.answer());
+ ImageIO.write(kaptcha.createImage(captcha.expr()), "jpg", os);
byte[] result = os.toByteArray();
os.close();
return result;
diff --git a/src/main/java/quant/rich/emoney/controller/manage/PlanControllerV1.java b/src/main/java/quant/rich/emoney/controller/manage/PlanControllerV1.java
index 179cd70..5e9dddf 100644
--- a/src/main/java/quant/rich/emoney/controller/manage/PlanControllerV1.java
+++ b/src/main/java/quant/rich/emoney/controller/manage/PlanControllerV1.java
@@ -2,20 +2,29 @@ package quant.rich.emoney.controller.manage;
import java.util.Arrays;
import java.util.List;
+import java.util.Locale;
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.PathVariable;
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 org.springframework.web.servlet.mvc.support.RedirectAttributes;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
import lombok.extern.slf4j.Slf4j;
import quant.rich.emoney.controller.common.UpdateBoolServiceController;
import quant.rich.emoney.entity.sqlite.Plan;
+import quant.rich.emoney.enums.PlanType;
+import quant.rich.emoney.exception.PageNotFoundException;
import quant.rich.emoney.exception.RException;
import quant.rich.emoney.interfaces.IQueryableEnum;
import quant.rich.emoney.pojo.dto.LayPageReq;
@@ -31,6 +40,9 @@ public class PlanControllerV1 extends UpdateBoolServiceController, Boolean> func;
+
+ private IndexInfo[] indexInfos;
+
+ @Setter(AccessLevel.PRIVATE)
+ private String indexNames;
+
+ public EmoneyIndexCallable setIndexInfos(IndexInfo[] indexInfos) {
+ this.indexInfos = indexInfos;
+ indexNames = Arrays.stream(indexInfos)
+ .map(IndexInfo::getIndexName)
+ .collect(Collectors.joining(", "));
+ return this;
+ }
+ /**
+ * 初始化一个 Callable
+ * @param tsCode
+ * @param beginPosition 起始位置,一般为当日或当时,如 20230921L
+ * @param stockSpan 数据粒度
+ * @param totalNeeds 经过计算需要的总数据数
+ * @param func 消费 {@code List
, Boolean> func,
+ IndexInfo...indexInfos) {
+
+ this.tsCode = tsCode;
+ this.beginPosition = beginPosition;
+ this.stockSpan = stockSpan;
+ setIndexInfos(indexInfos);
+ this.totalNeeds = totalNeeds;
+ this.func = func;
+
+ }
+
+ /**
+ * 从 EmoneyIndexCallable 恢复 Callable
+ * @param request
+ */
+ public EmoneyIndexCallable(EmoneyIndexCallable other) {
+ this.tsCode = other.tsCode;
+ this.beginPosition = other.beginPosition;
+ this.stockSpan = other.stockSpan;
+ this.indexInfos = other.indexInfos;
+ this.indexNames = other.indexNames;
+ this.totalNeeds = other.totalNeeds;
+ this.func = other.func;
+ }
+
+ @Override
+ public EmoneyCrawlerResult
+ * 服务器上配置了 fail2ban 对默认 User-Agent
+ * 拦截(Java/*),所以这里自定义 User-Agent
*/
-@FeignClient(name="tushare-data-service-client", url="http://localhost:9999")
+@Headers("User-Agent: At17DataService/1.0")
+@FeignClient(name="tushare-data-service-client", url="https://tushare.database.at17.link")
public interface TushareDataServiceClient {
@GetMapping("/api/v1/common/stockInfo/list")
diff --git a/src/main/java/quant/rich/emoney/util/ArithmeticCaptchaGen.java b/src/main/java/quant/rich/emoney/util/ArithmeticCaptchaGen.java
new file mode 100644
index 0000000..1c135e6
--- /dev/null
+++ b/src/main/java/quant/rich/emoney/util/ArithmeticCaptchaGen.java
@@ -0,0 +1,164 @@
+package quant.rich.emoney.util;
+import java.security.SecureRandom;
+
+public class ArithmeticCaptchaGen {
+
+ private static final SecureRandom RND = new SecureRandom();
+ private static final char[] OPS = new char[]{'+', '-', '×', '÷'};
+
+ public static class Captcha {
+ public final int a;
+ public final int b;
+ public final char op;
+
+ public Captcha(int a, int b, char op) {
+ this.a = a;
+ this.b = b;
+ this.op = op;
+ }
+
+ public String expr() {
+ return a + " " + op + " " + b;
+ }
+
+ public int answer() {
+ return switch (op) {
+ case '+' -> a + b;
+ case '-' -> a - b;
+ case '×' -> a * b;
+ case '÷' -> a / b; // 保证整除
+ default -> throw new IllegalStateException("Unexpected op: " + op);
+ };
+ }
+
+ @Override
+ public String toString() {
+ return expr() + " = ?";
+ }
+ }
+
+ /** 对外:随机生成一题 */
+ public static Captcha generate() {
+ char op = OPS[RND.nextInt(OPS.length)];
+ return generate(op);
+ }
+
+ /** 对外:指定运算符生成 */
+ public static Captcha generate(char op) {
+ return switch (op) {
+ case '+' -> genAddCarryAllowedButIfCarryUnitsZero();
+ case '-' -> genSubNoBorrowNonNegative();
+ case '×' -> genMulNoCarryOneDigit();
+ case '÷' -> genDivExact();
+ default -> throw new IllegalArgumentException("Unsupported op: " + op);
+ };
+ }
+
+ // ---------------- +:允许进位;若发生进位,则结果个位必须为 0;且和 <= 100 ----------------
+ private static Captcha genAddCarryAllowedButIfCarryUnitsZero() {
+ // 仍保留 100 的特殊题(不是必须,但能保证覆盖 100)
+ if (RND.nextInt(20) == 0) { // 5% 概率出 100
+ if (RND.nextBoolean()) return new Captcha(100, 0, '+');
+ return new Captcha(0, 100, '+');
+ }
+
+ while (true) {
+ int a = RND.nextInt(101); // 0..100
+ int b = RND.nextInt(101); // 0..100
+ int sum = a + b;
+ if (sum > 100) continue;
+
+ if (carryHappened(a, b)) {
+ // 发生进位:个位必须为 0
+ if (sum % 10 == 0) return new Captcha(a, b, '+');
+ } else {
+ // 未发生进位:不限制个位
+ return new Captcha(a, b, '+');
+ }
+ }
+ }
+
+ /** 判断十进制逐位相加是否发生过进位(任意一位) */
+ private static boolean carryHappened(int a, int b) {
+ int carry = 0;
+ while (a > 0 || b > 0) {
+ int da = a % 10;
+ int db = b % 10;
+ int s = da + db + carry;
+ if (s >= 10) return true;
+ carry = 0; // 因为 s<10 时 carry 必为 0
+ a /= 10;
+ b /= 10;
+ }
+ return false;
+ }
+
+ // ---------------- -:无退位,差 >= 0 ----------------
+ private static Captcha genSubNoBorrowNonNegative() {
+ while (true) {
+ int a = RND.nextInt(100); // 0..99
+ int b = RND.nextInt(100); // 0..99
+
+ int aT = a / 10, aU = a % 10;
+ int bT = b / 10, bU = b % 10;
+
+ // 无退位:每一位都要 a>=b;且整体差>=0
+ if (aU >= bU && aT >= bT) {
+ return new Captcha(a, b, '-'); // 此时 a>=b 自然成立
+ }
+ }
+ }
+
+ // ---------------- *:无进位;一元为 0..9;另一元每位*d < 10 ----------------
+ private static Captcha genMulNoCarryOneDigit() {
+ while (true) {
+ int d = RND.nextInt(10); // 0..9 其中一个因子
+ int x = RND.nextInt(100); // 另一元 0..99(你可自行放大范围)
+
+ // 如果 d=0,任何 x 都满足
+ if (d == 0) return orderMulOperands(d, x);
+
+ // 对 x 的每一位要求:digit*d < 10 才不会在该位产生进位
+ if (allDigitsMulLessThan10(x, d)) {
+ return orderMulOperands(d, x);
+ }
+ }
+ }
+
+ private static Captcha orderMulOperands(int d, int x) {
+ // 随机决定把 0..9 放左边还是右边
+ if (RND.nextBoolean()) return new Captcha(d, x, '×');
+ return new Captcha(x, d, '×');
+ }
+
+ private static boolean allDigitsMulLessThan10(int x, int d) {
+ // x 是非负
+ if (x == 0) return true;
+ int t = x;
+ while (t > 0) {
+ int digit = t % 10;
+ if (digit * d >= 10) return false;
+ t /= 10;
+ }
+ return true;
+ }
+
+ // ---------------- /:必须整除 ----------------
+ private static Captcha genDivExact() {
+ // 控制题目大小: dividend <= 100
+ while (true) {
+ int b = 1 + RND.nextInt(10); // 除数 1..10
+ int q = RND.nextInt(21); // 商 0..20
+ int a = b * q; // 被除数
+ if (a <= 100) return new Captcha(a, b, '÷');
+ }
+ }
+
+ // demo
+ public static void main(String[] args) {
+ for (int i = 0; i < 100; i++) {
+ Captcha c = generate();
+ System.out.println(c.expr() + " = " + c.answer());
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/quant/rich/emoney/util/ChunkRandomIter.java b/src/main/java/quant/rich/emoney/util/ChunkRandomIter.java
new file mode 100644
index 0000000..4a90a96
--- /dev/null
+++ b/src/main/java/quant/rich/emoney/util/ChunkRandomIter.java
@@ -0,0 +1,28 @@
+package quant.rich.emoney.util;
+import java.util.*;
+
+public class ChunkRandomIter {
+
+ final static Random RANDOM = new Random();
+
+ public static
+ *