Files
xiaotutongxue/scripts/dev-seed-warn-state.py
T
lovife fe7615d12b feat(h5): 学生端材料预览 + 删掉部署里的 MinIO
一、部署配置清理
  docker-compose.yml 里的 MinIO 删了(连 minio-data 卷)。它当初是
  "M0 阶段不写入但先起好 —— 本地/生产走同一套 S3 接口",但代码最终走
  七牛官方 SDK,那个容器**从来没被连过** —— 留着只会让人以为"本地还有个
  对象存储要用"。.env.example / .env 的 MINIO_* 换成 QINIU_* + XT_STORAGE_*。

  ⚠️ 删的时候我自己犯了个错:用 volumes:\n 当插入锚点,结果匹配到了
     **pg 服务内部的 `  volumes:`**,把注释插进 pg 定义里还把它顶到第 0 列,
     **YAML 结构就这么坏了**。好在写完立刻用 yaml.safe_load 复查(services: [pg, redis])
     才发现。教训:改 YAML 之后一定要解析一遍,别靠肉眼看缩进。

二、学生端材料预览(新功能)
  已传材料每行加「预览」,点开全屏浮层(深底 —— 材料照片多是白底证件件,
  浅底会看不清边界),点图片以外关闭。

  **两种存储两种取法,不能只写一种**:
    · 七牛:接口返回带签名的绝对 URL,直接给 <image src> 用
    · 本地磁盘:那个接口要带 Authorization,而 <image src> 发不了请求头 ——
      先用 uni.request 取字节,再转 blob URL
  只写七牛那一种,本地开发时预览会一片空白,而**原因完全看不出来**。
  签名链接有有效期(30 分钟),所以每次预览都现取,不缓存 URL。

  顺手修两个后端问题:
    · LocalStorageService.url() 返回的路径指向一个**不存在的接口**
      (我建的是 /{id}/content,不是 ?path=)→ 改由 MaterialFileService 按
      provider 决定:七牛给签名链接、本地给 /{id}/content(按 id 取能复用归属校验)
    · /content 原来固定 application/octet-stream —— 表现是**点开身份证照片
      变成下载一个文件**。现在按文件名推 Content-Type + inline

三、浏览器实测
  点预览 → 浮层打开、图片真的解码(naturalWidth=900、complete=true);
  直接 fetch 那个签名 URL → 200 / 17728 字节。
  中间发现一个视觉问题:**浮层标题与「关闭」被 uni-app 导航栏盖住了**
  (page-head 的 z-index 是 998,我给的 95)—— 看起来像"标题没渲染出来"。
  改成 9999 + padding-top 让出导航高度;问题清单浮层一并提到 9998。复验通过。

四、顺带
  dev-seed-warn-state.py 改为没有可用草稿时自己建一份 —— 原来写死 submission id,
  被 E2E 清理后就报"填报单不存在或不属于你",而那看起来像权限问题。

验收:e2e-m3-file 32/32、m3-submit 31/31、m1-storage 16/16
2026-09-29 15:16:36 +08:00

164 lines
6.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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
WANT_SID = int(sys.argv[1]) if len(sys.argv) > 1 else None
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 ensure_draft(token: str, task_id: int, want: int | None) -> int:
"""
拿一份可用的草稿 id。
<p>不写死某个 id:草稿会被 E2E 清理掉,写死的脚本下次跑就报
"填报单不存在或不属于你" —— 而那看起来像权限问题,不像"数据被清了"。
没给 id 或给的那个不可用,就现建一份(服务端是幂等的)。
"""
if want:
r = call("GET", f"/api/v1/mp/files/submissions/{want}", token)
if r.get("code") == 0:
return want
print(f" 给定的草稿 {want} 不可用({r.get('msg')}),改为现建一份")
r = call("POST", "/api/v1/mp/submissions", token, {"taskId": task_id})
if r.get("code") != 0:
raise SystemExit(f" 建草稿失败:{r}")
sid = r["data"]["id"]
print(f" 已建草稿 submission={sid}")
return sid
def main() -> int:
cfg = read_cfg()
tok = mint(cfg["jwt"], cfg["issuer"], UID, 1)
SID = ensure_draft(tok, TASK, WANT_SID)
# ① 填掉全部必填(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())