Files
form-center-service/docs/T2-report.md
T
chenxing 90f6b0a767
build / build-push (push) Successful in 1m46s
docs(T2): 回填 §4 推送与 CI 证据(service run 1468 completed/success,front 零改动)
同时补记 CI 取证方式(本地凭据调 Actions API,凭据不回显不落盘)与
local-debug 目录不在两仓工作树内的事实。
2026-10-14 15:27:20 +08:00

20 KiB
Raw Blame History

T2 执行说明 · 后端·定义域

任务书:《表单中心任务书-Qoder-20261014.md》v1.1 §T2|执行日期:2026-10-14|执行方:Qoder 输入权威:方案 v1.4(§4.2/§5.1/§5.3/§5.4/§7/§9)+ 契约 v1.0(§1/§2/§2.1/§10)+ 执行手册 §5 裁决 P5/P2/P10 前置:T1 骨架已验收通过。本轮未改动任何已验收产出(pom.xml / application.yml / BaseEntity / FormBaseController / CORS / Dockerfile / CI 全部零改动,无新增依赖)。


1. 做了什么

1.1 落包清单(19 个主类 + 6 个测试类 + 1 份 DDL 脚本)

包 新增 承担
entity Form / FormVersion / FormDict / FormDictItem 契约 §10 定义域四表;全部 extends BaseEntity(P5:审计四件,实际表=草案+create_by/update_by);无物理外键;uuid/sch_id/app_code/code/version/value 等身份列 updatable=false
enums FormEnums FormStatus 0/1/2、Audience 1/2、FillLimit 0/1、LimitScope(SUBJECT/SUBMITTER 推导结果,不是配置项)
repository FormRepository / FormVersionRepository / FormDictRepository / FormDictItemRepository 纯 JPQL/派生查询,零 native SQL(P10:没有 CTE/WITH/函数索引可写错的地方);FormVersionRepository 不提供任何"改已发布行"的方法
common FormSchemaValidator / FormStatusDefs / FormRuntimeRules 19 项白名单+14/11 控件+options 契约+searchable+跨版本字段身份;V12 状态定义;截止 23:59:59 与 limitScope/quotaKey(T3 提交与 T7 只读态共用同一份口径)
model FormModels 出入参 DTO;响应体不含库内 id(契约 §1,V5 断言点)
service FormDefService / FormDictService / FormAdminGuard 定义域全部业务写路径
controller/api FormAdminController(20 端点)/ FormResolveController(1 端点) 见 §1.5
sql/ t2_definition_domain.sql 定义域四表的 MySQL 5.7 兼容 DDL(评审权威 + 生产 ddl-auto=none 时的升级脚本底稿)
local-debug t2-def-smoke.sh 手册 §4.2"能脚本化的断言入 local-debug"

1.2 版本模型(V7 的核心取舍)

  • 头表 + 版本行:草稿行与发布行同在 form_version,区分依据是 publish_time(NULL=草稿)与头表 current_version 指针(方案 §5.1)。
  • 两种合法写通道,都不触碰已发布内容:①未发布草稿行的 schema_json/change_log 直改;②发布动作补 publish_time。已发布行没有任何写路径(仓库不提供、服务不暴露),updateDraft 还有一道 publishTime != null 即拒的显式守卫。
  • 升版期间旧版仍可填:已发布表单保存新 schema → 只开 version=max+1 的草稿行,头表 status/current_version 不动,发布才推指针。于是契约 §5"按填报页加载时解析到的版本校验并钉版"与方案 §3.2 的两层缓存声明天然成立 → 问题单 P20。
  • key 稳定性:form_version 是快照、form 是头表,name 只在头表(改名不升版);code 仅"从未发布"可改(契约 §1),改时 bulk update 同步草稿行冗余 code 列(uk_form_ver 含 code,不同步则 resolve 失配)。
  • 表单级配置不进版本快照(方案 §5.1/§5.4):audience/fill_limit/deadline/answer_no_enabled/status_defs/group_tag 都在头表原地改,测试断言"改配置后版本行数仍是 1、v1 schema 逐字节不变"。

1.3 schema 校验器(与 svc-hall FormValidator 的三处硬修订)

契约条目 实现
14 类一次冻结 / 一期 11 类 FROZEN_TYPES(14) / PHASE1_TYPES(11),address/user/users 明确报"二期开放,一期不可用"
白名单 19 项 16 项同源 + precision/unit/searchable;valueSchema 不在名单内(它是契约 §4 读接口生成物,设计器回传整份 schema 时必须剥掉,否则按契约拒绝)
options 只接受两种形态:{label,value} 对象数组(字符串数组被拒,V2)或字典引用对象 {"source":"dict:{code}"};条目 label ≤128、value 非空 ≤64 且同字段内唯一
searchable 只认真布尔值;searchableKeys() 是"筛选候选列"的唯一出处(V3),已开 GET /api/form/admin/filterFields 给 T4/T8 消费
key 规则 ^[A-Za-z0-9_]{1,64}$ + 表单内唯一;跨版本同 key 不得换 type(assertFieldTypesStable)——否则 answer_index 的 v_str/v_num/v_date 与按 type 收敛的检索算子错配,V8"删字段后历史仍可筛"当场失效
值形状(契约 §3) defaultValue 按其 type 的 §3 形状校验(number 给字符串、date 非 YYYY-MM-DD、multiselect 非数组都拒);precision ≤6 对齐 v_num DECIMAL(20,6)

发布路径三道校验:静态契约 → 跨版本字段身份 → 字典可解析(FormDictService#assertRefsResolvable)。

1.4 字典(契约 §2.1 三条不变式各就各位)

  • 两级 + 遮蔽:管理面按同层精确匹配(校级碰不到平台级行,方案 §6.5"可见可用不可改");解析面按域 sch_id ∈ {会话校, 0} 且本校遮蔽平台级且不回落(与表单 resolve 同一套校域规则)。
  • value 不可变:value 是条目的对外定位键(接口签名里根本没有"改 value"的通道)+ 列 updatable=false + uk(dict_id,value) 三重;label/sort 可改,测试断言改 label 后 allowedValues() 集合逐元素不变(V2 后半句的字典侧依据)。
  • 软删条目两口径:inlineOptions()(未删,设计器候选)与 allowedValues()(含软删,T3 提交放行/V38)——两个方法名把口径钉死,防止后来者混用。同 value 重新编辑即回到可选集。
  • 活引用不进快照:schema_json 存 {"source":"dict:grade"},expandSchema() 只在读路径展开;测试证明"字典改名后版本行一个字节没变,而 resolve 返回的 label 已经变了"。字典缺失时展开为 [] + warn(降级但不静默丢字段)。

1.5 定义域接口(21 端点,全部在 CAS 墙后)

/api/form/admin/**(FormAdminGuard 把关):list get versions version filterFields create save publish disable enable softDelete copy config statusDefs dict/list dict/selectable dict/save dict/delete dict/item/save dict/item/delete; /api/form/resolve?code=&appCode=(填报入口寻址,租户域只取会话校、不接受外部传 schId;options 已内联展开)。

寻址纪律:表单一律按 uuid(管理端)或 code(入口)寻址,字典按 (域, code)、条目按 (dictCode, value)——id 不出现在接口参数与响应外层。

1.6 环境动作(本机副作用,供知悉)

  • 复用 T1 拉起并已确认在跑的本地 MySQL 8.0.45(3306)+ 库 bsch_formcenter;ddl-auto=update 起了定义域四表(见 §2 取证)。
  • 为验证 sql/t2_definition_domain.sql 可执行,临时建过库 bsch_formcenter_ddlcheck_t2,验证完已删除,SHOW DATABASES LIKE 'bsch_formcenter%' 现只剩 bsch_formcenter。
  • form-center/local-debug/ 不在 service/front 任一仓工作树内(目录树里 form-center 根无 .git),故 t2-def-smoke.sh 与 T1 的 t1-accept-smoke.sh 一样是本地件、不随本轮推送;若要让脚本进版本库,需先在 form-center 根定一个归属仓(建议 service 仓 src/test/resources/local-debug/ 或单开 repo)→ 记本节而非问题单,属编排事项。
  • IT 全部 @Transactional 回滚,开发库现无残留业务行(form/form_dict_item COUNT=0 已核);仅 AUTO_INCREMENT 计数前移(回滚不掉自增游标,无害)。
  • 未启动 CAS/UserBase/Redis,未触碰 160 共享环境。

2. 自验证据(逐 V)

门禁命令:mvn -B -o clean package(离线,settings.xml 已指本地仓库,未传 -Dmaven.repo.local) → BUILD SUCCESS,Tests run: 67, Failures: 0, Errors: 0, Skipped: 0,产物 target/form-center.jar。

V 断言(方案 §9 原文口径) 证据
V2 options 非 {label,value} 结构被拒;改 label 不改 value 后历史筛选/分组口径不变 FormSchemaValidatorTest.stringArrayOptionsRejected(四类选项控件逐个断言"字符串数组被拒")、optionValueMustBePresentAndUnique、optionExtraPropRejected;字典侧 FormDictServiceIT.labelChangeKeepsValueSetStable(改 label 后 allowedValues 集合 equals 改前、inline 序列仍 g2,g3);FormDefServiceIT.saveRejectsInvalidSchemaWithoutTouchingDraft(非法 options 在保存阶段被拦,草稿行原样)
V3 白名单外属性被拒;未声明 searchable 的字段不进筛选候选 FormSchemaValidatorTest.propsOutsideWhitelistRejected(orderNum/dataValidator/valueSchema/required2/options2 五种都拒)、whitelistIsTheFrozenNineteen(19 项清点)、searchableMustBeBoolean、searchableKeysOnlyDeclaredOnes(containsExactly("a","c"),未声明的 b 与显式 false 的 d 都不进);接口侧 GET /api/form/admin/filterFields 取当前发布版本 schema 复用同一函数
V6 同 sch_id + 同 code、不同 app_code 共存不冲突 FormDefServiceIT.sameCodeUnderDifferentAppCodesCoexist(两条都建成功、uuid 不同、按 appCode 各查到 1 条、同 app_code 重复被拒);resolveIsScopedByAppCode(同 code 不同 app_code 各回各家);twoSchoolsAreIndependentNamespaces。物理侧 SHOW CREATE TABLE form → UNIQUE KEY uk_form (sch_id, app_code, code)
V7 已发布版本无法就地修改;改版产生 version+1 新行;老版本 schema_json 逐字节不变 FormDefServiceIT.publishedVersionIsImmutableAndUpgradeAddsNewRow:发布后 version(1).schemaJson == SCHEMA_A(常量逐字节比对)→ 保存 SCHEMA_B 得 draftVersion=2 且 currentVersion 仍 1 → 再断言 v1 仍 == SCHEMA_A、v2 == SCHEMA_B → 发布后 v1 依旧逐字节不变、历史 [2,1] 降序;republishWithoutDraftIsRejected(无草稿时再发布被拒,不给就地改留入口);fieldIdentityIsStableAcrossVersions(同 key 换 type 拒)
V12 单表单可自定义状态集合,默认含"待处理/已处理" FormStatusDefsTest.defaultSetIsPendingAndProcessed(列值 null/空白 → 待处理/已处理)、customSetReplacesDefault(自定义后默认不再存在、顺序即展示序、存储形状仍是 [{code,label,is_deleted}])、deletedDefStillResolvable(V13 的存储前提)、reuseOfDeletedCodeRejected、allDeletedRejected;IT 侧 FormDefServiceIT.formLevelConfigDoesNotCreateVersion 断言状态定义/配置改动不产生新版本行且 v1 schema 不变
V41(编排补充) 副本=新 code/uuid/version 1、schema 深拷贝、字典引用随行;原表单与其历史数据零改动 FormCopyIT.copyCreatesFreshIdentityAndKeepsSourceUntouched:新 uuid ≠ 源 uuid、code=honor_apply_2027、draftVersion=1、currentVersion=null、status=DRAFT、副本 v1 未被发布;副本 schema 与源 v1 等值且仍含 dict:grade(活引用随行);源表单逐字段回比 + v1 schema 逐字节比 + 版本历史不变;末尾断言"字典改 label 后源与副本读接口同时生效"(共享活引用,不是各自快照)。另有 copyInheritsConfigButNotDeadline、copyCanTargetAnotherAppCodeAndRejectsDuplicateCode、copyInputGuards 三条边界

其它取证

项 结果
契约 §1/§10 校域与标识 FormDefServiceIT.uuidGeneratedAtDraftAndIdsNotExposed(建草稿即生成 32 位 [0-9a-f]、升版跨版本不变)、resolveAppliesSchoolShadowing(草稿期不穿透到平台级、遮蔽不回落、停用给"已停用"业务错)、codeEditableOnlyBeforePublish、softDeletedFormKeepsItsCode、responseDtosDoNotExposeRowIds(objectMapper.writeValueAsString(FormView/VersionView) 不含 "id")
契约 §5.4 生命周期 disableAndEnableRestoreCurrentVersion(停用只断入口、版本历史仍可查;恢复回当前版本且不新增版本行)
字典语义 FormDictServiceIT 8 条:平台级可见可用不可改 / 校级遮蔽平台级 / value 稳定 / 软删条目放行 / 内联展开与降级空数组 / 发布前可解析性(未知字典、空字典都拦) / 活引用不进快照 / 字典 code 规则与软删恢复
MySQL 5.7 复核(P10) ①T2 零 native SQL(全 JPQL/派生查询,无 CTE、无函数索引);②sql/t2_definition_domain.sql 在本地实跑 mysql_exit=0 建出 4 表(scratch 库已删);③SHOW CREATE TABLE 逐列比对契约 §10 + P5 两列,utf8mb4/InnoDB/无物理 FK/TEXT 无 DEFAULT 全部成立;④复合唯一键 520~524 字节 < 767,无需前缀长度
启动冒烟 bash local-debug/t2-def-smoke.sh → 5/5 PASS:health=200;/api/form/admin/list、/api/form/admin/dict/list、/api/form/resolve?code=… 无票均 302→CAS(证明新映射已装载、且在鉴权墙后);/oapi/* 仍 401
登录态冒烟(P1 遗留项) 本轮未取得会话身份(本地 CAS/UserBase 未启),接口以"墙后 302 + 服务层 27 条直连真实 MySQL 的 IT"替代取证;缺口不变,按 P1 归 T5 联调轮补跑

3. 问题单(本轮登记,供执行手册 §5 裁决;编号接续 P13)

# 问题 本轮取值(最保守解读) 影响面
P14 契约 §6 错误码表要 PARAM/VALIDATE/AUTH/NOT_FOUND/业务规则 五段,但 javap 实测平台 ErrorCode 只有 8 值:SUCCESS 0 / ERROR 1 / PARAM_ERROR 2 / FORBIDDEN 3 / UNAUTHORIZED 4 / CUSTOM 5 / INTERNAL_SERVER_ERROR 6 / HTTP_METHOD_NOT_ALLOWED 405——没有 VALIDATE 与 NOT_FOUND 段 按现有枚举映射并全线沿用:参数·白名单·值形状=PARAM_ERROR(2)、未登录=UNAUTHORIZED(4)、越权/无管理权=FORBIDDEN(3)、业务规则(未发布/已停用/无草稿可发布/已发布行不可改,T3 的限次与截止同段)=CUSTOM(5)、"code/uuid 未找到"=PARAM_ERROR(2)(flow-center 先例)。契约 §6 自称"建议稿,对齐后定稿",请裁决:改契约口径为四段,或平台侧加枚举(跨项目) 全后端信封语义;T3/T4 沿用,前端 T5~T8 按 msg 展示
P15 契约 §10 写 create_time DATETIME NOT NULL,而 BaseEntity(T1 已验收、P5 裁决保留)两列未标 nullable=false → ddl-auto=update 生成 DEFAULT NULL 保留基类现状(改基类影响全平台所有应用,不在 T2 范围);业务上恒非空(FormCenterAuditListener 每次 persist/update 都写 now())。sql/t2_definition_domain.sql 按建表实态写 DEFAULT NULL 并注明本条 仅 DDL 约束文案;不改 §10 文本(P5 口径:草案非权威 schema)
P16 code 的字符集契约只写"人可读串 ≤64",而它要进 /form-center/f/{code} 与配置引用 取 URL-safe 保守子集 ^[A-Za-z0-9_-]{1,64}$(含连字符,不含中文/空格/点/百分号) T6 设计器需给出同规则的前端校验;T8 快应用模块行 path 需对齐
P17 分组结构:契约 §2 示例是 {"groups":[{"key","label","fields"}]},但"key 唯一/发布后不可改"只写了字段级 要求 group 必带 key(同 KEY_PATTERN)+ label 必填,且 group.key 与 field.key 共用一个唯一命名空间(读路径按 key 寻址时避免歧义);group 属性白名单锁 key/label/fields 与 svc-hall 旧形态(title、可无 key)不同,T6 需按此产出
P18 options[].value 与字典条目 value 的形态契约未写(只说"生成一次即不可变") 只定三条硬边界:非空、≤64(对齐 form_dict_item.value varchar(64),避免 inline 与字典两种长度语义)、同字段/同字典内唯一;不额外限制字符集。字典 code 因要在 dict:{code} 里解析,取 KEY_PATTERN 若超管要往平台级字典塞中文 value,本规则允许(中文 ≤64 字符即可)
P19 契约 §2"options value 不可变"在服务端不可校验:升版时"改了某个 value"与"删一项加一项"无法区分,而删/加都是合法升版(V8/V9 路径) 实现可校验的两条替代:①字典条目 value=物理不可变(updatable=false + 接口无改写通道 + uk);②同 key 跨版本不得换 type。另一半(inline options 的 value 不被改动)由 T6 设计器纪律保证:输入 label 自动生成 value、改 label 不动 value V2 的"改 label 历史不变"在服务端只能覆盖字典侧与 type 稳定性
P20 已发布表单"升版过程中"是否仍可填报,方案只写"改定义走升版""重新发布可恢复当前版本",未写窗口期语义 取旧版仍可填:保存只建草稿行,头表 status/current_version 不动,发布才推指针(与契约 §5 钉版、§3.2 两层缓存声明自洽)。副作用:设计者可能以为"保存了就生效"——T6 需在 UI 上区分"已发布版本 / 待发布草稿" T6 设计器文案;T7 resolve 语义无影响
P21 定义管理权的载体:方案 §6.1 第②层是"模块行授权(表单设计/字典管理)",但那是 T4 的 GetUserMod 授权树扫描链路;本轮接口必须先有闸门 沿用 fork 模板 flow-center 的角色口径:超管(type==1,读 UserBase UserTypeEnum 源码确认:0 普通/1 SUPER_ADMIN/2 校级管理员——注意 BoaUser.type 的 javadoc 写成"1=教师 2=家长"已过时)可跨校且唯一可管 sch_id=0 平台级行;校级管理员 roleId∈{2,5,7} 限本校。FormAdminGuard javadoc 已写明该缺口归属 T4/T8。附带请裁决:局领导(5) 按模板口径拿到定义编辑权是否收紧 权限把关链路;T4 实现 scope 扫描时应把本守卫切到"授权模块集"口径
P22 软删表单仍占用 uk_form(sch_id, app_code, code)(契约 §10 唯一键不含 is_deleted)→ 删掉一张表单后无法用同 code 重建 不自动改名、不物理删("有答卷的表单禁物理删除"是硬约束),给点名提示"同 code 的表单已存在(软删行仍占用该 code),请另起 code"。若要"删后可复用 code",需契约侧把 is_deleted 纳入唯一键——这会改 V6 的键语义,请裁决 运营体验;快应用模块行按 code 寻址,复用已删 code 本身有风险(本轮倾向保持现状)

4. 推送与 CI

仓 提交 推送 CI run 结论
form-center-service 03f6a97(26 文件,全为新增,未触碰 T1 已验收文件) 5c5abe1..03f6a97 main -> main run 1468(build.yml@refs/heads/main,head_sha 03f6a97…,run_number 3) status=completed / conclusion=success(Gitea Actions API 轮询实测:poll1~3 in_progress → poll4 completed+success)
form-center-front 无改动 未推送 — T2 是纯后端任务,front 仓零变更(T5/T6 范围)
  • 推送方式:常规 git push origin main(T1 采用 clone 建仓,注入提交已在历史,无需 --allow-unrelated-histories); 未使用 git reset;暂存按显式路径逐个 git add(26 条路径,见提交前 git status 核对:无 add -A、无敏感文件)。
  • CI 状态取证方式:git credential fill 取本地已存凭据后调 GET /api/v1/repos/form-center/form-center-service/actions/runs[/{id}],凭据不回显、不落盘、不入库。 注意 CI 只跑 mvn -B -DskipTests clean package(不跑单测,方案 §7.2),所以 CI 绿只证明产物链; 定义域的行为证据在本文 §2 的本地 mvn test(67 条)里。
  • 本轮工作树收尾:git status --short 干净,与 origin/main 推平。
  • 本节的回填本身是一次 docs-only 提交(只改本文,不动任何构建输入),其后同流水线再跑一次。

—— 全文完 ——