计划任务编辑详情前后端适配

This commit is contained in:
2026-02-05 13:45:39 +08:00
parent b0093ccccb
commit 8f6c9af00f
37 changed files with 2145 additions and 517 deletions

View File

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

View File

@@ -0,0 +1,28 @@
package quant.rich.emoney.util;
import java.util.*;
public class ChunkRandomIter {
final static Random RANDOM = new Random();
public static <T> List<T> splitShuffleAndFlatten(List<T> list, int n) {
int size = list.size();
if (n <= 0) throw new IllegalArgumentException("n must be > 0");
n = Math.min(n, size == 0 ? 1 : size);
List<T> parts = new ArrayList<>(n);
int base = size / n; // 每份至少 base 个
int extra = size % n; // 前 extra 份多一个
int idx = 0;
for (int i = 0; i < n; i++) {
int partSize = base + (i < extra ? 1 : 0);
List<T> part = new ArrayList<>(list.subList(idx, idx + partSize));
Collections.shuffle(part, RANDOM);
parts.addAll(part);
idx += partSize;
}
return parts;
}
}

View File

@@ -1,5 +1,6 @@
package quant.rich.emoney.util;
import java.time.DateTimeException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@@ -55,4 +56,40 @@ public final class DateUtils {
LocalDate date = LocalDate.parse(value, formatter);
return date;
}
/**
* 将数字类型日期转换成相应的 LocalDateTime
* <p>
* <li> 2309191130 → 2023-09-19 11:30:00
* <li> 230919 → 2023-09-19
*/
public static LocalDateTime longDatetimeToLocalDateTime(long dateTime) {
if (dateTime <= 0) {
throw new IllegalArgumentException("dateTime must be positive, got: " + dateTime);
}
try {
if (dateTime > 99_999_999L) {
// yyMMddHHmm 例如2309191130 -> 2023-09-19 11:30
int yy = (int) (dateTime / 100_000_000L); // 23
int year = 2000 + yy; // 2023
int month = (int) ((dateTime % 100_000_000L) / 1_000_000L); // 09
int day = (int) ((dateTime % 1_000_000L) / 10_000L); // 19
int hour = (int) ((dateTime % 10_000L) / 100L); // 11
int minute = (int) (dateTime % 100L); // 30
return LocalDateTime.of(year, month, day, hour, minute, 0);
} else {
// yyyyMMdd 例如20230919 -> 2023-09-19 00:00
int year = (int) (dateTime / 10_000L); // 2023
int month = (int) ((dateTime % 10_000L) / 100L); // 09
int day = (int) (dateTime % 100L); // 19
return LocalDateTime.of(year, month, day, 0, 0, 0);
}
} catch (DateTimeException ex) {
// 月/日/时/分越界会进这里(比如 month=13
throw new IllegalArgumentException("Invalid dateTime value: " + dateTime, ex);
}
}
}