fe7615d12b
一、部署配置清理
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
164 lines
6.7 KiB
Python
164 lines
6.7 KiB
Python
# -*- 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())
|