From 664cae654867b528ea339f187ebe290cb04d6fcf Mon Sep 17 00:00:00 2001
From: xyc <jc_xiong@hotmail.com>
Date: 星期四, 27 八月 2026 18:41:21 +0800
Subject: [PATCH] docs: 关机前交接——运行状态、待验证项、外置配置要点
---
traffic-audit-server/src/main/java/com/trafficaudit/security/captcha/CaptchaService.java | 444 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 444 insertions(+), 0 deletions(-)
diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/security/captcha/CaptchaService.java b/traffic-audit-server/src/main/java/com/trafficaudit/security/captcha/CaptchaService.java
new file mode 100644
index 0000000..ebb972c
--- /dev/null
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/security/captcha/CaptchaService.java
@@ -0,0 +1,444 @@
+package com.trafficaudit.security.captcha;
+
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 琛屼负楠岃瘉鐮侊細鎸夋彁绀洪『搴忕偣鍑荤珛浣撳浘褰€��
+ * 鏈嶅姟绔洿鎺ョ敓鎴愭瘡涓浘褰㈢殑绛夎酱娴嬪嚑浣曢潰锛堝杈瑰舰/妞渾/璺緞锛夛紝鍓嶇鍙礋璐f覆鏌擄紝
+ * 鍝嶅簲涓笉鍖呭惈浠讳綍鍥惧舰绫诲瀷鍚嶇О锛岀瓟妗堝彧淇濆瓨鍦ㄦ湇鍔$銆�
+ */
+@Service
+public class CaptchaService {
+
+ public static final int CANVAS_W = 340;
+ public static final int CANVAS_H = 148;
+
+ private static final int SHAPE_COUNT = 6;
+ private static final int ANSWER_COUNT = 4;
+ private static final long TTL_MILLIS = 5 * 60 * 1000L;
+ private static final int MAX_FAIL = 3;
+ private static final char[] ID_CHARS = "ABCDEFGHJKMNPQRSTUVWXYZ23456789".toCharArray();
+
+ private static final Map<String, String> KIND_CN = new LinkedHashMap<>();
+
+ static {
+ KIND_CN.put("CUBE", "姝f柟浣�");
+ KIND_CN.put("BOX", "闀挎柟浣�");
+ KIND_CN.put("CYLINDER", "鍦嗘煴");
+ KIND_CN.put("CONE", "鍦嗛敟");
+ KIND_CN.put("SPHERE", "鐞�");
+ KIND_CN.put("TRI_PRISM", "涓夋1鏌�");
+ KIND_CN.put("PYRAMID", "閲戝瓧濉�");
+ KIND_CN.put("HEX_PRISM", "鍏1鏌�");
+ }
+
+ private static final String[] KINDS = KIND_CN.keySet().toArray(new String[0]);
+ private static final String[] PATTERNS = {"dots", "grid", "lines", "rings", "cross", "tri"};
+
+ private final Map<String, Challenge> store = new ConcurrentHashMap<>();
+ private final Random random = new Random();
+
+ public enum VerifyResult { OK, WRONG, NOT_FOUND, RETRY_LIMIT }
+
+ public enum ConsumeResult { OK, WRONG, NOT_FOUND, NOT_VERIFIED }
+
+ private static class Challenge {
+ String id;
+ List<String> answerKinds;
+ Map<String, String> idToKind;
+ boolean verified;
+ long expireAt;
+ int failCount;
+ }
+
+ /** 鐢熸垚涓�缁勬寫鎴橈紝杩斿洖鍙洿鎺ヤ笅鍙戝墠绔殑 JSON 缁撴瀯銆� */
+ public Map<String, Object> generate() {
+ boolean darkBg = random.nextBoolean();
+ int bgHue = random.nextInt(360);
+ int bgSat = 30 + random.nextInt(40);
+ int bgLight = darkBg ? 14 + random.nextInt(16) : 74 + random.nextInt(16);
+ String pattern = PATTERNS[random.nextInt(PATTERNS.length)];
+ int patHue = (bgHue + 120 + random.nextInt(200)) % 360;
+
+ int shapeSat = 55 + random.nextInt(30);
+ int shapeLight = darkBg ? 60 + random.nextInt(18) : 28 + random.nextInt(16);
+ int shapeHue = (bgHue + 30 + random.nextInt(300)) % 360;
+
+ // 绛旀搴忓垪锛氶殢鏈哄彇 ANSWER_COUNT 涓笉閲嶅绫诲瀷锛涘叾浣欎綅缃敤鍓╀綑绫诲瀷濉厖
+ List<String> pool = new ArrayList<>(Arrays.asList(KINDS));
+ Collections.shuffle(pool, random);
+ List<String> answerKinds = new ArrayList<>(pool.subList(0, ANSWER_COUNT));
+ List<String> allKinds = new ArrayList<>();
+ for (int i = 0; i < SHAPE_COUNT; i++) {
+ if (i < ANSWER_COUNT) {
+ allKinds.add(answerKinds.get(i));
+ } else {
+ allKinds.add(pool.get(ANSWER_COUNT + random.nextInt(pool.size() - ANSWER_COUNT)));
+ }
+ }
+ Collections.shuffle(allKinds, random);
+
+ int cols = 3, rows = 2;
+ List<Map<String, Object>> shapes = new ArrayList<>();
+ Map<String, String> idToKind = new HashMap<>();
+ for (int i = 0; i < SHAPE_COUNT; i++) {
+ int col = i % cols, row = i / cols;
+ double x = CANVAS_W * (col + 0.5) / cols + (random.nextDouble() * 24 - 12);
+ double y = CANVAS_H * (row + 0.5) / rows + (random.nextDouble() * 20 - 10);
+ double size = 0.6 + random.nextDouble() * 0.35;
+ double rotate = (random.nextInt(5) - 2) * 6 + (random.nextInt(5) - 2);
+ String kind = allKinds.get(i);
+ String id = randomId();
+ idToKind.put(id, kind);
+ shapes.add(buildShape(kind, id, x, y, size, rotate,
+ (shapeHue + random.nextInt(40) - 20 + 360) % 360, shapeSat, shapeLight));
+ }
+
+ Challenge ch = new Challenge();
+ ch.id = randomId();
+ ch.answerKinds = answerKinds;
+ ch.idToKind = idToKind;
+ ch.expireAt = System.currentTimeMillis() + TTL_MILLIS;
+ store.put(ch.id, ch);
+
+ Map<String, Object> data = new LinkedHashMap<>();
+ data.put("captchaId", ch.id);
+ List<String> prompt = new ArrayList<>();
+ for (String k : answerKinds) {
+ prompt.add(KIND_CN.get(k));
+ }
+ data.put("prompt", prompt);
+
+ Map<String, Object> bg = new LinkedHashMap<>();
+ bg.put("hue", bgHue);
+ bg.put("sat", bgSat);
+ bg.put("light", bgLight);
+ bg.put("pattern", pattern);
+ bg.put("patternHue", patHue);
+ bg.put("patternSat", 50);
+ bg.put("patternLight", 60);
+ bg.put("patternOpacity", 0.22);
+ bg.put("patternSize", 9 + random.nextInt(8));
+ data.put("background", bg);
+ data.put("shapes", shapes);
+ return data;
+ }
+
+ /** 鏍¢獙鐐瑰嚮椤哄簭锛涢�氳繃鍚庢爣璁板凡璁よ瘉锛岀櫥褰曟椂娑堣垂銆� */
+ public VerifyResult verify(String captchaId, List<String> clickIds) {
+ Challenge ch = getLive(captchaId);
+ if (ch == null) {
+ return VerifyResult.NOT_FOUND;
+ }
+ if (match(ch, clickIds)) {
+ ch.verified = true;
+ return VerifyResult.OK;
+ }
+ ch.failCount++;
+ if (ch.failCount >= MAX_FAIL) {
+ store.remove(ch.id);
+ return VerifyResult.RETRY_LIMIT;
+ }
+ return VerifyResult.WRONG;
+ }
+
+ /** 鐧诲綍鏃舵秷璐归獙璇佺爜锛氫竴娆℃�э紝鏍¢獙绛旀骞跺垹闄ゆ寫鎴樸�� */
+ public ConsumeResult consume(String captchaId, List<String> clickIds) {
+ Challenge ch = getLive(captchaId);
+ if (ch == null) {
+ return ConsumeResult.NOT_FOUND;
+ }
+ store.remove(ch.id);
+ if (!ch.verified) {
+ return ConsumeResult.NOT_VERIFIED;
+ }
+ if (!match(ch, clickIds)) {
+ return ConsumeResult.WRONG;
+ }
+ return ConsumeResult.OK;
+ }
+
+ private Challenge getLive(String captchaId) {
+ if (captchaId == null || captchaId.isEmpty()) {
+ return null;
+ }
+ Challenge ch = store.get(captchaId);
+ if (ch == null) {
+ return null;
+ }
+ if (ch.expireAt < System.currentTimeMillis()) {
+ store.remove(ch.id);
+ return null;
+ }
+ return ch;
+ }
+
+ private boolean match(Challenge ch, List<String> clickIds) {
+ if (clickIds == null || clickIds.isEmpty() || clickIds.size() != ch.answerKinds.size()) {
+ return false;
+ }
+ List<String> got = new ArrayList<>();
+ for (String id : clickIds) {
+ String kind = ch.idToKind.get(id);
+ if (kind == null) {
+ return false;
+ }
+ got.add(kind);
+ }
+ return ch.answerKinds.equals(got);
+ }
+
+ private String randomId() {
+ StringBuilder sb = new StringBuilder(6);
+ for (int i = 0; i < 6; i++) {
+ sb.append(ID_CHARS[random.nextInt(ID_CHARS.length)]);
+ }
+ return sb.toString();
+ }
+
+ // ==================== 绛夎酱娴嬪嚑浣曠敓鎴� ====================
+
+ private Map<String, Object> buildShape(String kind, String id, double x, double y, double size,
+ double rotate, int hue, int sat, int light) {
+ ShapeArt art;
+ switch (kind) {
+ case "CUBE": art = cube(hue, sat, light); break;
+ case "BOX": art = box(hue, sat, light); break;
+ case "CYLINDER": art = cylinder(hue, sat, light); break;
+ case "CONE": art = cone(hue, sat, light); break;
+ case "SPHERE": art = sphere(hue, sat, light); break;
+ case "TRI_PRISM": art = triPrism(hue, sat, light); break;
+ case "PYRAMID": art = pyramid(hue, sat, light); break;
+ case "HEX_PRISM": art = hexPrism(hue, sat, light); break;
+ default: art = cube(hue, sat, light);
+ }
+ Map<String, Object> m = new LinkedHashMap<>();
+ m.put("id", id);
+ m.put("x", x);
+ m.put("y", y);
+ m.put("size", size);
+ m.put("rotate", rotate);
+ m.put("hitW", art.bbox[0]);
+ m.put("hitH", art.bbox[1]);
+ m.put("faces", art.faces);
+ return m;
+ }
+
+ private static class ShapeArt {
+ List<Map<String, Object>> faces;
+ double[] bbox; // {w, h} 鏈棆杞湭缂╂斁鏃剁殑鍖呭洿鐩�
+
+ ShapeArt(List<Map<String, Object>> faces, double w, double h) {
+ this.faces = faces;
+ this.bbox = new double[]{w, h};
+ }
+ }
+
+ private static Map<String, Object> poly(String pts, String fill, String stroke) {
+ Map<String, Object> f = new LinkedHashMap<>();
+ f.put("type", "poly");
+ f.put("pts", pts);
+ f.put("fill", fill);
+ f.put("stroke", stroke);
+ f.put("sw", 1.2);
+ return f;
+ }
+
+ private static Map<String, Object> ellipse(double cx, double cy, double rx, double ry,
+ String fill, String stroke, double opacity) {
+ Map<String, Object> f = new LinkedHashMap<>();
+ f.put("type", "ellipse");
+ f.put("cx", cx);
+ f.put("cy", cy);
+ f.put("rx", rx);
+ f.put("ry", ry);
+ f.put("fill", fill);
+ f.put("stroke", stroke);
+ f.put("sw", opacity > 0 ? 0 : 1.2);
+ f.put("opacity", opacity);
+ return f;
+ }
+
+ private static Map<String, Object> path(String d, String fill, String stroke) {
+ Map<String, Object> f = new LinkedHashMap<>();
+ f.put("type", "path");
+ f.put("d", d);
+ f.put("fill", fill);
+ f.put("stroke", stroke);
+ f.put("sw", 1.2);
+ return f;
+ }
+
+ private static String hsl(int h, int s, int l) {
+ return "hsl(" + h + "," + s + "%," + l + "%)";
+ }
+
+ private static Map<String, Object> stop(double offset, String color) {
+ Map<String, Object> m = new LinkedHashMap<>();
+ m.put("o", offset);
+ m.put("c", color);
+ return m;
+ }
+
+ private static String shade(int h, int s, int l, int delta) {
+ int v = Math.max(6, Math.min(94, l + delta));
+ return hsl(h, s, v);
+ }
+
+ private static String fmt(double d) {
+ return String.format(Locale.ROOT, "%.1f", d);
+ }
+
+ private static String pts(double... p) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < p.length; i += 2) {
+ if (sb.length() > 0) {
+ sb.append(' ');
+ }
+ sb.append(fmt(p[i])).append(',').append(fmt(p[i + 1]));
+ }
+ return sb.toString();
+ }
+
+ private static ShapeArt cube(int h, int s, int l) {
+ double side = 26, d = 13, cy = d / 2;
+ String fillTop = shade(h, s, l, 18);
+ String fillFront = hsl(h, s, l);
+ String fillRight = shade(h, s, l, -14);
+ String stroke = shade(h, s, l, -30);
+ List<Map<String, Object>> faces = new ArrayList<>();
+ faces.add(poly(pts(-side / 2, -cy, side / 2, -cy, side / 2 + d, -cy - d, -side / 2 + d, -cy - d), fillTop, stroke));
+ faces.add(poly(pts(side / 2, -cy, side / 2, -cy + side, side / 2 + d, -cy + side - d, side / 2 + d, -cy - d), fillRight, stroke));
+ faces.add(poly(pts(-side / 2, -cy, side / 2, -cy, side / 2, -cy + side, -side / 2, -cy + side), fillFront, stroke));
+ return new ShapeArt(faces, side + d, side + d);
+ }
+
+ private static ShapeArt box(int h, int s, int l) {
+ double w = 34, height = 22, d = 14, cy = d / 2;
+ String fillTop = shade(h, s, l, 18);
+ String fillFront = hsl(h, s, l);
+ String fillRight = shade(h, s, l, -14);
+ String stroke = shade(h, s, l, -30);
+ List<Map<String, Object>> faces = new ArrayList<>();
+ faces.add(poly(pts(-w / 2, -cy, w / 2, -cy, w / 2 + d, -cy - d, -w / 2 + d, -cy - d), fillTop, stroke));
+ faces.add(poly(pts(w / 2, -cy, w / 2, -cy + height, w / 2 + d, -cy + height - d, w / 2 + d, -cy - d), fillRight, stroke));
+ faces.add(poly(pts(-w / 2, -cy, w / 2, -cy, w / 2, -cy + height, -w / 2, -cy + height), fillFront, stroke));
+ return new ShapeArt(faces, w + d, height + d);
+ }
+
+ private static ShapeArt cylinder(int h, int s, int l) {
+ double r = 18, height = 34;
+ String fillSide = hsl(h, s, l);
+ String fillTop = shade(h, s, l, 18);
+ String stroke = shade(h, s, l, -30);
+ List<Map<String, Object>> faces = new ArrayList<>();
+ String side = "M" + fmt(-r) + "," + fmt(-height / 2)
+ + " L" + fmt(-r) + "," + fmt(height / 2)
+ + " A" + fmt(r) + "," + fmt(r * 0.45) + ",0,0,0," + fmt(r) + "," + fmt(height / 2)
+ + " L" + fmt(r) + "," + fmt(-height / 2)
+ + " A" + fmt(r) + "," + fmt(r * 0.45) + ",0,0,0," + fmt(-r) + "," + fmt(-height / 2) + " Z";
+ faces.add(path(side, fillSide, stroke));
+ faces.add(ellipse(0, -height / 2, r, r * 0.45, fillTop, stroke, 1));
+ return new ShapeArt(faces, r * 2, height + r * 0.9);
+ }
+
+ private static ShapeArt cone(int h, int s, int l) {
+ double r = 20, height = 38;
+ double apexY = -height;
+ double cy = (-height + r * 0.45) / 2;
+ String fillBase = shade(h, s, l, -14);
+ String fillSide = hsl(h, s, l);
+ String stroke = shade(h, s, l, -30);
+ List<Map<String, Object>> faces = new ArrayList<>();
+ faces.add(ellipse(0, -cy, r, r * 0.45, fillBase, stroke, 1));
+ String side = "M" + fmt(0) + "," + fmt(apexY - cy)
+ + " L" + fmt(-r) + "," + fmt(-cy)
+ + " A" + fmt(r) + "," + fmt(r * 0.45) + ",0,0,1," + fmt(r) + "," + fmt(-cy) + " Z";
+ faces.add(path(side, fillSide, stroke));
+ return new ShapeArt(faces, r * 2, height + r * 0.45);
+ }
+
+ private static ShapeArt sphere(int h, int s, int l) {
+ double r = 21;
+ String stroke = shade(h, s, l, -30);
+ List<Map<String, Object>> faces = new ArrayList<>();
+ Map<String, Object> grad = new LinkedHashMap<>();
+ grad.put("id", "g");
+ grad.put("cx", 0.35);
+ grad.put("cy", 0.32);
+ grad.put("r", 0.85);
+ List<Map<String, Object>> stops = new ArrayList<>();
+ stops.add(stop(0, shade(h, s, l, 26)));
+ stops.add(stop(0.5, hsl(h, s, l)));
+ stops.add(stop(1, shade(h, s, l, -20)));
+ grad.put("stops", stops);
+ Map<String, Object> f = new LinkedHashMap<>();
+ f.put("type", "radial");
+ f.put("cx", 0.0);
+ f.put("cy", 0.0);
+ f.put("r", r);
+ f.put("grad", grad);
+ f.put("stroke", stroke);
+ f.put("sw", 1.2);
+ faces.add(f);
+ faces.add(ellipse(-r * 0.38, -r * 0.45, r * 0.3, r * 0.2, "#ffffff", "none", 0.65));
+ faces.add(ellipse(r * 0.25, r * 0.45, r * 0.55, r * 0.2, shade(h, s, l, -32), "none", 0.45));
+ return new ShapeArt(faces, r * 2, r * 2);
+ }
+
+ private static ShapeArt triPrism(int h, int s, int l) {
+ double w = 34, height = 30, depth = 12;
+ double cy = (depth * 0.5 + height) / 2;
+ String fillTop = shade(h, s, l, 18);
+ String fillRight = hsl(h, s, l);
+ String fillLeft = shade(h, s, l, -14);
+ String stroke = shade(h, s, l, -30);
+ List<Map<String, Object>> faces = new ArrayList<>();
+ faces.add(poly(pts(0, -cy + depth * 0.5, w / 2, -cy, -w / 2, -cy), fillTop, stroke));
+ faces.add(poly(pts(0, -cy + depth * 0.5, w / 2, -cy, w / 2, -cy + height, 0, -cy + depth * 0.5 + height), fillRight, stroke));
+ faces.add(poly(pts(0, -cy + depth * 0.5, -w / 2, -cy, -w / 2, -cy + height, 0, -cy + depth * 0.5 + height), fillLeft, stroke));
+ return new ShapeArt(faces, w, depth * 0.5 + height);
+ }
+
+ private static ShapeArt pyramid(int h, int s, int l) {
+ double half = 26, height = 34;
+ double cy = (-height + half) / 2;
+ String fillBase = shade(h, s, l, -20);
+ String fillRight = hsl(h, s, l);
+ String fillLeft = shade(h, s, l, -10);
+ String stroke = shade(h, s, l, -30);
+ List<Map<String, Object>> faces = new ArrayList<>();
+ faces.add(poly(pts(0, -cy, half, -cy + half * 0.5, 0, -cy + half, -half, -cy + half * 0.5), fillBase, stroke));
+ faces.add(poly(pts(0, -cy - height, 0, -cy, -half, -cy + half * 0.5), fillLeft, stroke));
+ faces.add(poly(pts(0, -cy - height, 0, -cy, half, -cy + half * 0.5), fillRight, stroke));
+ return new ShapeArt(faces, half * 2, height + half);
+ }
+
+ private static ShapeArt hexPrism(int h, int s, int l) {
+ double r = 22, height = 28, ry = r * 0.433;
+ double cy = ry;
+ String fillTop = shade(h, s, l, 18);
+ String fillR = hsl(h, s, l);
+ String fillF = shade(h, s, l, -12);
+ String fillL = shade(h, s, l, -22);
+ String stroke = shade(h, s, l, -30);
+ List<Map<String, Object>> faces = new ArrayList<>();
+ faces.add(poly(pts(r, -cy, r * 0.5, -cy + ry, -r * 0.5, -cy + ry, -r, -cy,
+ -r * 0.5, -cy - ry, r * 0.5, -cy - ry), fillTop, stroke));
+ faces.add(poly(pts(r, -cy, r * 0.5, -cy + ry, r * 0.5, -cy + ry + height, r, -cy + height), fillR, stroke));
+ faces.add(poly(pts(r * 0.5, -cy + ry, -r * 0.5, -cy + ry, -r * 0.5, -cy + ry + height, r * 0.5, -cy + ry + height), fillF, stroke));
+ faces.add(poly(pts(-r * 0.5, -cy + ry, -r, -cy, -r, -cy + height, -r * 0.5, -cy + ry + height), fillL, stroke));
+ return new ShapeArt(faces, r * 2, ry * 2 + height);
+ }
+}
\ No newline at end of file
--
Gitblit v1.9.1