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:
@@ -55,3 +55,6 @@ __pycache__/
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
desktop.ini
|
desktop.ini
|
||||||
|
|
||||||
|
# 运行时上传的文件(材料原图/规范件)—— 是数据不是代码,绝不入库
|
||||||
|
backend/data/
|
||||||
|
|||||||
@@ -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. **提交页在浏览器里的肉眼验证还没做**(问题清单浮层的渲染与交互)。
|
||||||
代码与产物都对,但**未经浏览器验证** —— 不要当成已验证。
|
代码与产物都对,但**未经浏览器验证** —— 不要当成已验证。
|
||||||
|
|||||||
@@ -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())
|
||||||
Reference in New Issue
Block a user