diff --git a/.gitignore b/.gitignore index b2bcfd1..c6c3b21 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,6 @@ __pycache__/ .DS_Store Thumbs.db desktop.ini + +# 运行时上传的文件(材料原图/规范件)—— 是数据不是代码,绝不入库 +backend/data/ diff --git a/docs/交接摘要.md b/docs/交接摘要.md index 354c7b6..15d4ade 100644 --- a/docs/交接摘要.md +++ b/docs/交接摘要.md @@ -908,6 +908,38 @@ P4 写 M5 校验引擎时,我凭记忆用了一套 VAL 编号,**与 PRD 权 没有覆盖「动态表单的必填字段」(因为表单字段是配置出来的)。 按系列顺延补的 —— **这是新增,不是 PRD 原有条目**,需要登记。 +### ✅ 浏览器验证已完成(同日补做) + +`fill.vue` 的问题清单浮层在**真浏览器**里跑通了两条分支(穿透慢,单次要 30 秒左右,耐心等): + +| 分支 | 触发条件 | 实测表现 | +|---|---|---| +| **红色**(阻断) | 缺身份证材料 + 必填未填 | 标题「**还不能提交**」、`[必须处理 3]`、三张红卡(`--s3`,左边框 `#E34D59`/底 `#FDF3F4`):`VAL-16` / `VAL-22` / `VAL-05`;按钮「**知道了,去修改**」→ 点击后浮层关闭 | +| **黄色**(可强提) | 只差一个 `VAL-09` 重复文件 | 标题「**提交前请确认**」、`[提醒 1]`、一张橙卡(`--s2`,左边框 `#ED7B2F`);按钮「**仍然提交**」→ 点后弹出「提交成功 / 已提交,等待班主任初审」 | + +**提交成功后的状态切换也验证了**: +- 上传按钮与「删除」**全部消失**(`canEdit=false`,已提交不再可改) +- 提交按钮被替换为只读文案「已提交,等待班主任初审」 +- 材料列表显示规范名 + 压缩后体积:`张同学-361029XXXXXX0015-身份证.jpg 17KB · 900×1200` +- 库:`status=1 red=0 yellow=1 submit_time` 已写;日志 `提交未通过 ×2 → 提交成功` + +**顺带验证**:`loadFiles()` 正常(重进页面能看到上次传的材料与体积、并能删)。 + +### 演示态的现状(下一个会话请注意) +- **`collect_submission.id = 21` 现在是「已提交」状态**(上面这次验证留下的)。 + 它是个很好的演示态,但**要重跑填报流程必须先删掉**: + ```sql + DELETE FROM collect_file WHERE submission_id = 21; + DELETE FROM collect_submission WHERE id = 21; + ``` +- 名册(task 17,id=38)绑定 `sys_user.id=151`(学生微信账号)——**保持不动**。 + +### 新增的开发工具 +`scripts/dev-seed-warn-state.py`:把某份草稿快速摆到"只剩黄色警告"的状态 +(填必填 + 传两张同内容的图 → `VAL-09`)。 +**用途**:验证浮层/审核端的"只有警告"分支时,不必在页面上手填 7 个字段再等穿透。 +它**不**替代 E2E —— 逻辑仍由 `e2e-m3-submit.py` 断言,这个脚本只负责摆状态。 + ### 下一步 1. **提交页在浏览器里的肉眼验证还没做**(问题清单浮层的渲染与交互)。 代码与产物都对,但**未经浏览器验证** —— 不要当成已验证。 diff --git a/scripts/dev-seed-warn-state.py b/scripts/dev-seed-warn-state.py new file mode 100644 index 0000000..7660b8c --- /dev/null +++ b/scripts/dev-seed-warn-state.py @@ -0,0 +1,141 @@ +# -*- coding: utf-8 -*- +""" +把演示草稿(submission 21)摆到"只剩黄色警告"的状态,用来验证浮层的第二个分支。 + +为什么要用脚本摆而不是在页面上手填: + · 页面上填 7 个字段 + 传 2 张图 + 等穿透,一次要两三分钟; + · 而这一步要验的是**浮层在"只有警告"时的表现**,不是填表本身。 +摆状态用接口最快,浮层那一段仍然在真浏览器里点。 + +跑法:python scripts/_seed_warn_state.py [submissionId] +""" +import base64 +import hashlib +import hmac +import io +import json +import os +import re +import sys +import time +import urllib.error +import urllib.request +import uuid + +sys.stdout.reconfigure(encoding="utf-8") + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +SRC = os.path.join(ROOT, "backend", "xt-server", "src", "main", "resources") +UID = 151 +TASK = 17 +SID = int(sys.argv[1]) if len(sys.argv) > 1 else 21 + + +def read_cfg() -> dict: + text = "\n".join(open(os.path.join(SRC, f), encoding="utf-8").read() + for f in ("application-dev.yml", "application.yml") + if os.path.exists(os.path.join(SRC, f))) + out = {} + for k, p in [("jwt", r"^\s*secret:\s*(\S+)\s*$"), ("issuer", r"^\s*issuer:\s*(\S+)\s*$")]: + m = re.search(p, text, re.M) + if not m: + raise SystemExit(f"找不到 {k}") + out[k] = m.group(1) + return out + + +def b64u(b: bytes) -> str: + return base64.urlsafe_b64encode(b).rstrip(b"=").decode() + + +def mint(secret: str, issuer: str, uid: int, tenant: int) -> str: + key = base64.b64decode(secret) + now = int(time.time()) + signing = (b64u(json.dumps({"alg": "HS512"}, separators=(",", ":")).encode()) + "." + + b64u(json.dumps({"iss": issuer, "sub": str(uid), "tenantId": tenant, + "orgIds": [], "roles": ["STUDENT"], "userType": 4, + "iat": now, "exp": now + 3600}, separators=(",", ":")).encode())) + return signing + "." + b64u(hmac.new(key, signing.encode(), hashlib.sha512).digest()) + + +def call(method, path, token, body=None): + h = {"Content-Type": "application/json; charset=utf-8"} + if token: + h["Authorization"] = f"Bearer {token}" + data = json.dumps(body, ensure_ascii=False).encode() if body is not None else None + req = urllib.request.Request("http://localhost:8080" + path, data=data, headers=h, method=method) + try: + with urllib.request.urlopen(req, timeout=90) as x: + return json.loads(x.read().decode("utf-8")) + except urllib.error.HTTPError as e: + try: + return json.loads(e.read().decode("utf-8")) + except Exception: + return {"_http": e.code} + + +def upload(token, sid, code, filename, data): + boundary = "----seed" + uuid.uuid4().hex[:12] + parts = [] + for k, v in [("submissionId", str(sid)), ("materialCode", code)]: + parts.append(f'--{boundary}\r\nContent-Disposition: form-data; name="{k}"\r\n\r\n{v}\r\n'.encode()) + parts.append(f'--{boundary}\r\nContent-Disposition: form-data; name="file"; filename="{filename}"\r\n' + f"Content-Type: image/jpeg\r\n\r\n".encode()) + parts.append(data) + parts.append(f"\r\n--{boundary}--\r\n".encode()) + req = urllib.request.Request( + "http://localhost:8080/api/v1/mp/files/upload", data=b"".join(parts), method="POST", + headers={"Content-Type": f"multipart/form-data; boundary={boundary}", + "Authorization": f"Bearer {token}"}) + try: + with urllib.request.urlopen(req, timeout=90) as x: + return json.loads(x.read().decode("utf-8")) + except urllib.error.HTTPError as e: + try: + return json.loads(e.read().decode("utf-8")) + except Exception: + return {"_http": e.code} + + +def main() -> int: + cfg = read_cfg() + tok = mint(cfg["jwt"], cfg["issuer"], UID, 1) + + # ① 填掉全部必填(key 是 snake_case;is_retake=false 让补考三科不可见 → 不必填) + form = {"work_unit": "无", "is_retake": False, "is_student": True, + "school": "示范职业学校", "major": "数控技术", "enroll_date": "2023-09-01"} + r = call("PUT", f"/api/v1/mp/submissions/{SID}", tok, {"formData": form}) + print(f" 填必填字段 code={r.get('code')} msg={r.get('msg')}") + if r.get("code") != 0: + return 1 + + # ② 传两张**同内容**的图 → VAL-09 重复文件(黄色警告) + from PIL import Image + buf = io.BytesIO() + Image.new("RGB", (900, 1200), (205, 212, 220)).save(buf, "JPEG", quality=90) + img = buf.getvalue() + for name in ("身份证正面.jpg", "身份证反面.jpg"): + up = upload(tok, SID, "IDCARD", name, img) + if up.get("code") != 0: + print(f" 上传 {name} 失败:{up}") + return 1 + pr = call("POST", f"/api/v1/mp/files/{up['data']['id']}/process", tok) + d = pr.get("data") or {} + print(f" {name} → {d.get('standardName')} {d.get('rawSize')}→{d.get('finalSize')} 字节") + + # ③ 试提交一次,看看还剩什么 + r = call("POST", f"/api/v1/mp/submissions/{SID}/submit", tok, {"confirmWarnings": False}) + d = r.get("data") or {} + print(f"\n 试提交:code={r.get('code')} submitted={d.get('submitted')} " + f"红={d.get('redCount')} 黄={d.get('yellowCount')} 蓝={d.get('infoCount')}") + for i in d.get("issues") or []: + print(f" [sev{i.get('severity')}] {i.get('ruleCode')} {i.get('title')}") + if (d.get("redCount") or 0) > 0: + print("\n ✗ 还有红色项 —— 浏览器里只会看到「还不能提交」那一支,验不到黄色分支") + return 3 + print("\n ok 只剩黄色/蓝色 —— 可以在浏览器里验「提交前请确认」那一支了") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())