verify(h5): 提交页问题清单浮层真浏览器验证通过

两条分支都跑通了(穿透单次要 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()(重进页面能看到上次传的材料与体积并能删)

⚠️ git add -A 差点把 backend/data/uploads/ 下的**运行时上传文件**提交进来。
   那是数据不是代码 —— 已加进 .gitignore(backend/data/)并从暂存区撤回。

新增 scripts/dev-seed-warn-state.py:把某份草稿快速摆到"只剩黄色警告"的状态,
用来验浮层/审核端的"只有警告"分支,省掉"手填 7 个字段 + 等穿透"。
它**不**替代 E2E —— 逻辑仍由 e2e-m3-submit.py 断言,这个脚本只负责摆状态。

演示态:collect_submission.id=21 现在是「已提交」(本次验证留下)。
它是很好的演示态,但要重跑填报流程必须先删掉它(见交接摘要 §一·十三)。
名册(task 17, id=38)绑定 sys_user.id=151,保持不动。
This commit is contained in:
2026-09-29 13:21:07 +08:00
parent 6620b024cf
commit edb47e8557
3 changed files with 176 additions and 0 deletions
+3
View File
@@ -55,3 +55,6 @@ __pycache__/
.DS_Store .DS_Store
Thumbs.db Thumbs.db
desktop.ini desktop.ini
# 运行时上传的文件(材料原图/规范件)—— 是数据不是代码,绝不入库
backend/data/
+32
View File
@@ -908,6 +908,38 @@ P4 写 M5 校验引擎时,我凭记忆用了一套 VAL 编号,**与 PRD 权
没有覆盖「动态表单的必填字段」(因为表单字段是配置出来的)。 没有覆盖「动态表单的必填字段」(因为表单字段是配置出来的)。
按系列顺延补的 —— **这是新增,不是 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. **提交页在浏览器里的肉眼验证还没做**(问题清单浮层的渲染与交互)。 1. **提交页在浏览器里的肉眼验证还没做**(问题清单浮层的渲染与交互)。
代码与产物都对,但**未经浏览器验证** —— 不要当成已验证。 代码与产物都对,但**未经浏览器验证** —— 不要当成已验证。
+141
View File
@@ -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())