Commit Graph

2 Commits

Author SHA1 Message Date
lovife a4406f545a feat(task): 材料规则界面(M1-04 前端)—— 动态收集要求在界面上真的能配了
发布任务的 5 项前置检查里有一项是「至少配一项必传材料」(40018)。
在这之前,管理员配材料要求**只能直接改数据库** —— 而"能动态设置收集要求"
正是本产品区别于"发个 Excel 让学生填"的核心。界面上没有它,这个能力等于不存在。
(task/index.vue 里那句「请到『材料规则』里配置…」也一直指着一个不存在的界面。)

新增:
  · apis/xt/material.ts —— 材料项接口 + 材料目录 + 枚举镜像
  · views/task/TaskMaterialDrawer.vue —— 列表 + 新增/编辑/删除 + 按模板初始化
  · task/index.vue 操作列加「材料规则」(草稿/收集中/已暂停三态),操作列 200→240px

四处刻意的设计:
  1. 选材料类型**自动带出默认值**(规范名、别名、默认档位与份数)。
     让人手填这些,一是慢,二是必然填不一致 —— 而"档位选错"的后果是
     证件翻拍件被压糊、机构退回。实测:选「学信网备案表」自动带出
     别名(学信网/学历备案表/电子注册备案表)+ P4 + 1 份。
  2. **条件显示只给三选一**(所有学生/仅在校生/仅非在校生),不做通用规则构造器。
     实际业务只有一种需求:学籍表与工作证明按是否在校生互斥。给一个能拼任意条件的
     东西,用户拼出个永远为假的条件,表现是"学生那边根本不显示这一项",
     而原因藏在一条没人在意的规则里。
  3. **编辑时锁死材料码**:它是 collect_file 的关联键(两者之间没有外键),
     改了码已上传的文件就成了孤儿 —— 静默丢材料。
  4. **收集中改动显式警告**:服务端允许改,但学生会立刻看到新要求,
     已传的材料可能因此出现新的问题项。

浏览器真点过:抽屉打开、模板初始化生成标准 4 项且**条件标签正确**
(学籍表=仅在校生、工作证明=仅非在校生)、新增的自动带默认值、保存成功。
typecheck 通过,改动文件 lint 0 错 0 警。

⭐ 顺带验证了一条完整链路:e2e-m3-submit.py 原先**写死了 IDCARD**,
   配上标准 4 项之后全红 —— 那说明脚本不够健壮(材料要求本来就是可配置的)。
   改成从表单接口读"当前学生看得见、且必传"的材料后:
     当前必传且有条件成立的:['IDCARD', 'PHOTO1', 'SCHOOL_ROLL']
     跳过(选传或条件不成立):['WORK_PROOF']
   即:管理员配的「学籍表=仅在校生」→ 学生端正确隐藏工作证明 → 校验不误报缺失。
   **教训:E2E 不要写死业务配置。写死的那一刻,测试就从"验证行为"变成了
   "验证数据",配置一变全红 —— 看起来像代码坏了。**

又修一处文档缺陷:《03-接口设计》§2.7 的 taskStatus 把 2 与 4 写反了
   (文档:2已暂停/3已截止/4已归档;代码与 DDL:2已截止/3已归档/4暂停)。
   按文档实现会把「暂停」和「截止」做反,而这两者语义差别很大
   (暂停可恢复、已提交照常审;截止不可逆)。

40018 的提示改为「请**点本行的**「材料规则」…」—— 顶层菜单里也有一个「材料规则」
   (里程碑占位页),不点明"本行"会把人引到那个占位页去。

验收:e2e-m3-submit.py 31/31;
回归 材料目录 68 / 材料上传 29 / 草稿 27 / 门禁 29 全绿。
2026-09-29 13:33:09 +08:00
lovife 9911ff01f1 feat(submit): 提交 + 红黄绿校验(PRD §5.7 的 VAL 系列)
一次给全,不"改一条再拒一次":学生一次可能同时缺 3 项材料、2 个字段没填,
逐条拒绝会让他改 5 次提交 5 次。所以 validate 收全部问题后一次性返回
{ruleCode, severity, title, detail},ruleCode 用 PRD 的 VAL-xx 编号。

两段式提交:只有黄色警告时先返回 submitted=false 让学生确认,
再带 confirmWarnings=true 才真的提交。为什么不前端自己判断 ——
只有服务端的校验结果算数,两次请求之间学生可能又改坏了什么。

实现的规则:VAL-01 证件号 / VAL-05 材料完整性 / VAL-06 格式 /
VAL-07 体积 / VAL-08 清晰度 / VAL-09 重复文件 / VAL-13 报考三项 /
VAL-15 补考科目数按等级 / VAL-16 在校生三项 / VAL-17 工作年限 /
VAL-18 未规范化 / VAL-20 体积下限 / VAL-22 表单必填(**新增**:
PRD 的 VAL-01~21 是具体业务规则,没覆盖动态表单的必填字段,按系列顺延补的)。

═══ 本轮抓到的四个真问题(都记在代码注释里了)═══

① **校验器不能用列名当字段 key**。字段 key 是 snake_case
   (work_unit / job_level / enroll_date),不是列名;而且报考三项
   存在 ext_json 里、**不是独立列**(契约 §2.4)。写 camelCase 的表现是
   "明明填了却说没填"。更糟的是 VAL-16 读列、VAL-22 读表单装配值 ——
   **两条规则对同一份数据给出相反结论**,学生不知道该信哪一条。
   现在统一用 FillService.getForm 装配出来的那一份作为唯一取值口径。

② **VAL-22 没判可见性**。不判可见就会要求一个在校生填「补考理论」——
   而那个字段在他页面上根本不存在。求值口径已与前端 fill.vue 的 visible()
   逐字对齐(norm 归一 true/false↔1/0、未知 op 一律可见)。

③ **VAL-01 读错了列**:collect_roster.id_card_no 是 **AES 密文**
   (列注释写明的),明文只在导入与门禁时短暂存在。一开始拿它算校验位,
   于是每一份填报单都被判"身份证号未通过校验" —— 100% 阻断。
   这种"规则很对但读错列"的错,反而容易被当成"校验太严"而不是 bug。
   现在只确认名册里有证件号(用脱敏列),校验位交给导入与门禁。

④ **静态资源没有 Cache-Control/ETag**(新增 StaticCacheHeaderFilter)。
   没有它浏览器会走启发式缓存:旧 index.html → 旧 chunk →
   **学生发版后看到的是上个月的页面**,而开发机硬刷新一次就没了,
   极容易被当成"偶发"。现在 /assets/** 一年 immutable(文件名带内容哈希)、
   *.html no-cache(每次校验)、/api/** no-store。

排查过程里还确认了一件事:材料上传那轮的"浏览器加载旧 chunk"
不是构建问题 —— 从 8080 下载的 chunk 与磁盘文件 SHA-256 一致、里面确实有新代码,
是缓存头缺失导致的浏览器行为。

验收:e2e-m3-submit.py **30/30**(含"该拦的确实拦了"和"修好后确实放过")。
回归:草稿 27 / 门禁 29 / 材料上传 29 / 材料目录 68 全绿。
2026-09-29 12:39:16 +08:00