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
This commit is contained in:
2026-09-29 15:16:36 +08:00
parent d8463a051f
commit fe7615d12b
7 changed files with 263 additions and 40 deletions
+23 -1
View File
@@ -28,7 +28,7 @@ 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
WANT_SID = int(sys.argv[1]) if len(sys.argv) > 1 else None
def read_cfg() -> dict:
@@ -97,9 +97,31 @@ def upload(token, sid, code, filename, data):
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,