feat(task): 规则模板 FORM 真正可套用 + 已归档任务可删(客户确认的两项)

一、规则模板的 FORM 部分(F-57/F-64)—— 此前只能「跳过并说明」,等于装饰
  · FormSchemaService.saveRawVersion:用已存在的 schemaJson 存为新版本。
    与手工保存的区别:内容不再校验/归一化(它当初就是按本服务规则存出来的,
    再走一遍解析→序列化只会丢未建模字段);状态检查与版本管理保留 —— 版本化
    逻辑只有一份
  · RuleTemplateService 的 FORM 分支接上它(不再 skipped)
  · 顺带补上另一半:saveAsTemplate 此前不存表单(存模板不存、套模板用不上,
    两头各缺一半)—— 现在把源任务的当前表单一并带走;没存过 = null =
    套用时按「用系统默认,不覆盖」跳过

二、已归档任务可删(F-14 / §17.2 D-9)—— 此前「已发布即删不掉」,
   演示/测试任务只能改名挪开,库只增不减
  · DELETE /tasks/{id} 支持 status=3:收集中/已截止仍拒(归档是最后的后悔药),
    拒绝的话说清「先归档」
  · 子表级联物理删 14 张表(TaskCascadeMapper 集中放置,表清单来自
    information_schema 的事实核对)—— 大头是名册(证件号密文)、材料、审核记录
  · ★ 任务行本身**软删**:它必须占住 task_code —— maxSeqByCodePrefix 靠
    「含软删行的最大序号」保证编码永不复用(编码同时是文件目录名,重号会把
    两批任务的文件混进同一目录)。第一版做成了物理删,e2e-m1-task 的
    「编码递增」断言当场抓到重号 —— 这条回归现在被 e2e-m1-task-delete
    的「删除后新建不重号」断言守住
  · 物理文件尽力删(先取路径再删行,顺序反了文件就永远留在磁盘上;
    删不掉只记日志不阻断);全程留痕(sys_operation_log 记 DELETE_TASK +
    行数/文件数)

验证:e2e-m7-taskconfig → 75/75(新增 FORM 完整链路 7 条断言)
      e2e-m1-task-delete(新增)→ 20/20(拒删/删净/占号软删/留痕/再删报错/草稿直删)
      e2e-m1-task → 71/71(旧口径断言更新为新口径)
This commit is contained in:
2026-09-30 12:38:47 +08:00
parent b991e3d68e
commit 8f80744d63
10 changed files with 550 additions and 10 deletions
+43 -2
View File
@@ -262,12 +262,52 @@ def main():
check("⭐ 模板的 useCount 被 +1(用户能看出哪个模板常用)",
use_after == use_before + 1, "前 %d → 后 %d" % (use_before, use_after))
# 只应用 FORM:应该 skipped 且说明原因
# 只应用 FORM(公共模板没带表单定义):应该明确 skipped 且说明原因
ap2 = body_of("POST", "/api/v1/rule-templates/%d/apply" % tpl_id, at,
{"targetTaskId": tgt, "parts": ["FORM"]})
check("⭐ 只应用 FORM 时明确 skipped + 原因(公共模板的 form_schema 为空)",
len((ap2.get("data") or {}).get("skipped") or []) >= 1,
json.dumps(ap2.get("data"), ensure_ascii=False)[:300])
# ── FORM 的完整链路(2026-09-30 接上,F-64:此前只能"跳过并说明",模板的 FORM 部分是装饰)──
print(" FORM 完整链路:存表单 → 存模板(带表单)→ 套用到另一个草稿")
fs = body_of("GET", "/api/v1/tasks/%d/form-schema" % tgt, at).get("data") or {}
save_fs = body_of("PUT", "/api/v1/tasks/%d/form-schema" % tgt, at, {
"baseVersion": fs.get("version"),
"fields": fs.get("fields") or [],
# ⚠️ 扩展字段的 key 必须以 extra_ 开头(服务端规则,写错会 40001)
"extras": [{"key": "extra_e2e", "label": "E2E扩展字段", "type": "text"}],
})
check("⭐ 给草稿任务存一份表单(v1,带一个扩展字段)",
save_fs.get("code") == 0 and (save_fs.get("data") or {}).get("version") == 1,
json.dumps(save_fs, ensure_ascii=False)[:200])
tpl2 = body_of("POST", "/api/v1/tasks/%d/save-as-template" % tgt, at,
{"templateName": "E2E表单模板" + suffix})
check("存为模板成功", tpl2.get("code") == 0, json.dumps(tpl2, ensure_ascii=False)[:160])
tpl2_id = ((tpl2.get("data") or {}).get("templateId"))
has_fs = sql("select (form_schema is not null)::text from collect_rule_template where id=%s;"
% tpl2_id).strip()
check("⭐ 模板把表单定义也存进去了(此前存模板不存表单,F-64 的前一半)",
has_fs == "true", "form_schema is not null = %s" % has_fs)
_, b3 = R.call("POST", BASE + "/api/v1/tasks", at, {
"taskName": "E2E套表单 " + suffix, "batchNo": "202609", "majorName": "数控加工",
"collectMode": 1, "certType": 1, "startTime": "2026-01-01", "endTime": "2026-12-31"
})
tgt2 = b3["data"]["id"]
CREATED_TASK_IDS.append(tgt2)
apf = body_of("POST", "/api/v1/rule-templates/%s/apply" % tpl2_id, at,
{"targetTaskId": tgt2, "parts": ["FORM"]})
check("⭐ FORM 部分真的应用了(不再「跳过并说明」)",
"FORM" in ((apf.get("data") or {}).get("applied") or []),
json.dumps(apf.get("data"), ensure_ascii=False)[:200])
rows2 = sql("select count(*) from collect_form_schema where task_id=%d;" % tgt2).strip()
check("⭐ 目标任务有了版本化的表单行(套用走 FormSchemaService 的版本化保存)",
rows2 >= "1", "collect_form_schema 行数=%s" % rows2)
# schema_json 是 jsonb:LIKE 之前要 ::text
got_extra = sql("select (schema_json::text like '%%extra_e2e%%')::text "
"from collect_form_schema where task_id=%d "
"order by version desc limit 1;" % tgt2).strip()
check("套过去的就是源任务那份定义(含 extra_e2e 扩展字段)", got_extra == "true", got_extra)
bad_parts = body_of("POST", "/api/v1/rule-templates/%d/apply" % tpl_id, at,
{"targetTaskId": tgt, "parts": ["NOPE"]})
check("parts 写错 → 明确报错并列出合法值",
@@ -568,9 +608,10 @@ def cleanup():
R.sql("delete from collect_roster where task_id=%d;" % tid)
R.sql("delete from collect_task_org where task_id=%d;" % tid)
R.sql("delete from collect_material_def where task_id=%d;" % tid)
R.sql("delete from collect_form_schema where task_id=%d;" % tid)
R.sql("delete from collect_task where id=%d;" % tid)
R.sql("delete from collect_rule_template where template_name like 'E2E自建模板%' "
"or template_name like 'E2E公共尝试%';")
"or template_name like 'E2E公共尝试%' or template_name like 'E2E表单模板%';")
R.sql("update collect_rule_template set use_count=0 where id in (1,2);")
R.sql("delete from collect_message where user_id in (995111, 995112);")
R.sql("delete from sys_user_role where user_id = 995101;")