Files
form-center-service/docs/T4-report.md
T

29 KiB
Raw Blame History

T4 执行说明 · 后端·检索统计 + 数据范围 + 导出

任务书:《表单中心任务书-Qoder-20261014.md》v1.1 §T4|执行日期:2026-10-14|执行方:Qoder 输入权威:方案 v1.4(§5.2/§5.5/§6.1/§6.4/§9)+ 契约 v1.0(§2/§4/§8/§9/§10,含 10-14 T3 验收后回写)

  • 执行手册 §5 P23~P34 裁决(P25/P33/P34 前置三项、P21 守卫到期替换、P28/P30 归本轮的收紧点)
  • 《表单中心T3验收报告-20261014.md》§5 开工清单与 §4 三处勘误 前置:T1/T2/T3 已验收。本轮对已验收文件的改动全部是裁决驱动(§1.6 逐条列明),未重构已验收行为; T2 的 67 条 + T3 的 54 条测试原样全绿(AnswerAdminIT/AnswerSubmitIT 因签名切换做了等义改写,见 §1.6)。

1. 做了什么

1.1 开工前置三项(T3 验收裁决落地,先做)

# 落地点 实现
P25 Answer.formUuid(varchar(32) NOT NULL updatable=false)+ sql/t3_answer_domain.sql 同步加列;AnswerService.doSubmit 写入 resolvePublished 命中的表单 uuid;FormDefService.pinnedForData(formUuid, version) 改按 form_uuid → form(uk_form_uuid) → 版本行 精确取数 删除了 T3 的"本校缺版本行退平台级"回退启发式(连带删除已无调用方的 FormRepository.findDataDomainCandidates)。理由与裁决一致:那组 (校, app_code, code, version) 坐标不含"表单归属校",遮蔽歧义时同版本号会静默按错 schema 校验/重建索引,比查不到更糟。开发库答卷零行 → 无回填(ddl-auto=update 直接加列,实测见 §3)
P33 新接口 SubjectDirectory.existsInSchool(subjectId, schId) + 生产实现 PlatformSubjectDirectory(SDK OpenApiUserClient.getUserById → UserBase GET /oapi/user/byId?id=,签名由 starter 出站链负责) 提交(AnswerService.requireSubject)与编辑(AnswerAdminService.assertSubjectStillValid)两处,凡代填(subject_id 非空且 ≠ 提交人)判存在性 + 同校归属,不过则 PARAM_ERROR。本人自填不查(会话里的人必然存在)。归属判定取 schools(全部可选身份学校,本期 UserBase 起填)为主、school/currentRole.schId 为兜底(PlatformSubjectDirectoryTest 逐层断言)
P34 两处 assertAttachmentsOwned(提交 / 编辑) 查到 form_file 行后比对值里 key == store_key、fileName == file_name,不一致 PARAM_ERROR。配套把 FormValueValidator.attachmentFileIds 改为 attachments()(返回完整三件套,否则要比 key 就得把值文档再走一遍)

外部调用一律收在策略接口后面:业务服务不直连 UserBase 库、不私写签名;本地 IT 不启动 UserBase/CAS,用注入桩取证,真实链路留 T9 联调(§2 环境事实)。

1.2 P21 守卫到期替换:FormAdminGuard 从角色闸门切"授权模块集"

  • 新接口 ModuleTreeSource(+ 生产实现 PlatformModuleTreeSource 走 SDK YschBaseOpenApi.getUserModule,即 UserBase /oapi/user/modules),roleId∈{2,7} 硬编码整体替换、不保留回落;
  • 管理权 = 被授权对应模块行,四个 path 常量逐字对齐手册 §2.2 表:/admin/forms 定义管理、/admin/data 数据管理、/admin/stats 统计、/admin/dicts 字典管理(requireAdmin(user, targetSchId, modulePath) 25 处调用点全部按面归类);
  • 超管例外保留:type==1 可跨校且唯一可管 sch_id=0 —— 那是 UserBase 用户类型不是 roleId,不属于 P21 要移除的硬编码(方案 §6.5"超管不受单校开通约束");
  • canManage 同步切口径 = 超管 或 被授权 /admin/data 或 /admin/forms(代填分支用);
  • requireAdminAny:统计按手册"可并入数据管理,二选一"放行 /admin/stats 或 /admin/data;
  • 与数据范围正交:管理权看 path、范围看 customId,FormModuleKeys 把两者分开取(scopeAnchorIsNotAModuleRow 用例钉住"path 空锚点不给管理权")。

1.3 数据范围(契约 §8 / 方案 §6.4)

FormEnums.DataScope{ ALL, TEACHING_CLASS, SELF_SUBMITTED, ABOUT_ME }   // 闭集四枚举
AnswerScopeImplementor(策略接口,一值一实现器:×3 一期可用 + 任教班级 1.5 占位)
DataScopeService.resolve(会话用户, 表单) → 范围集合
AnswerScopeQueryBuilder ← 四条读出路径 + 管理动作行级把关的唯一谓词构造器
  • 矩阵 × audience:管理员(7)/校领导(2)=全部;教师(1)×面向教师=仅本人提交,×面向学生=任教班级一期实现器缺席 → 生效仅本人提交(方案表格明示要防"教师怎么只看到自己"被当 bug 报);学生(3)×面向学生=关于我;家长(4)/工作号(6)/局领导(5) 与未知角色=回落仅本人提交;全不命中=仅本人提交。
  • customId 扫描:授权模块树里 path 空、customId=fc:scope:all 的行 → 升级全部(递归扫描,锚点在子层也算命中)。
  • 多来源取最宽 = 并集:一个人有多个身份时逐格取默认再 OR(学生"关于我"与教师"仅本人提交"不是包含关系,取单值会丢一格);命中"全部"即收敛。
  • 任教班级:枚举里有、selectable()=false、解析永不产出、被误用时明确抛"一期未开放"(V28 的三半各有断言)。

1.4 四条铁律的落点(方案 §5.5)

铁律 实现 取证
① 两段式列表禁 SELECT values AnswerScopeQueryBuilder.pageIds 只 select a.id(Criteria 投影,类型上就取不到大列);当页文档走 AnswerRepository.findLiveByIds AnswerReadIT.listIsTwoPhaseAndLocatorNeverSelectsValuesColumn——把 Hibernate SQL 接到内存 appender,断言第一段 SQL 不含 values_json、第二段才含
② keyset 仅提交时间序 游标恒落 id(与 create_time 同序单调),asc/desc 只换方向;没有排序列参数、没有 offset keysetPagesWithoutOverlapAndCursorIsScopeBound:size=2 翻 3 页不重不漏;游标 uuid 不在可见集内 → 游标不可用 拒
③ 查询覆盖索引化 列表/计数/导出落 idx_answer_form_time(sch_id,app_code,code,is_deleted,id);筛选落 answer_index 三复合索引的子查询;按状态分组落 idx_answer_form DDL 与实体索引逐条对齐(§1.7 记 P35);筛选在 SQL 层完成,多条件 AND 叠加
④ 统计缓存仅升版失效 + 短 TTL AnswerStatsService:键=域 + form_uuid + 当前发布 version + 范围签名 + 用户 + 分组字段,TTL 60s,只缓存无筛选条件的全量统计;不按数据变更失效 statsCacheIsVersionScopedAndNotInvalidatedByNewData:新答卷落库后 TTL 内统计总数不变(预期行为)、cached=true,而列表实时可见新行;换身份不命中同一键

1.5 检索 / 统计 / 导出 / oapi

  • 列表(AnswerReadService.list):两段式 + keyset,返回行含 answerId(uuid)/version/status(+label)/submitterId/subjectId/answerNo/submitTime/values;scopes 一并回传便于前端与验收对照。值文档原样返回,三态空值与已删字段降级由前端做。
  • 详情(detail):uuid 寻址,返回钉住版本(form_uuid+version)内联展开后的 schema + 值文档;可见性用同一构造器判(不是 Java 里比字段)→ 越权 FORBIDDEN。
  • 筛选候选(filterFields + FormSchemaValidator.filterableFields):searchable 且有算子的字段;upload/image 排除(P28:不发明算子、不写索引行、也不进候选),按它筛选被点名拒绝。既有定义面 /api/form/admin/filterFields 同批改为排除附件类。
  • 算子收敛(common/FormFilterOperators):等值(选项类)、前缀模糊(文本类,LIKE 转义 \ % _)、闭区间(date→v_date、number→v_num);类型由 schema 定,形态不对一律 PARAM_ERROR(日期严格 yyyy-MM-dd,V4 不走字典序)。
  • 统计(AnswerStatsService):总数 / 按状态 / 按 searchable 字段分组(走 answer_index + count(distinct answer_id),多值字段一答卷不因多值重复计数);选项桶 label 回查展开后的 options。
  • 导出(AnswerExportService):easyexcel 3.3.2 动态表头 + ExcelWriter 分批 write,批大小 500,游标逐批前移;同步响应写 HttpServletResponse 输出流(不套 JSON 信封,void 返回);范围=单表单+可选筛选,无行数上限、无异步;列=六列身份/时间 + 当前发布版本字段序,选项列显 label、多选按 options 定义序、附件列显 fileName。
  • 软删排除:is_deleted=0 写在构造器里,四条路径与管理动作一律带上(V30 读侧半边)。
  • oapi(OapiFormController + OapiFormService):GET /oapi/form/schema?schId=&code=[&appCode=](内联 options + 每字段 valueSchema,audience 下发枚举名)与 GET /oapi/form/answer?schId=&answerId=(钉住版本 schema + 值文档);沿用 /oapi/* 验签墙与 code=0 信封;resolvePublished 与填报入口同一条遮蔽实现。

1.6 对本轮之前已验收文件的改动(全部裁决驱动,非重构)

文件 改了什么 依据
entity/Answer + form_uuid 列 + idx_answer_form_time 索引 P25;方案 §5.5 铁律③(索引记 P35)
repository/FormRepository + findByUuid;删除 findDataDomainCandidates(改按 form_uuid 取数后无调用方) P25
repository/AnswerRepository + findLiveByIds(两段式第二段)、findLiveByFormUuidAfterId(升版补建分批扫描);类注释补"四条读路径在构造器不在仓库" 铁律①/§6.4 红线;V9(记 P36)
repository/AnswerIndexRepository + findFieldKeysByAnswerId(补建去重) V9
service/FormDefService pinnedForData 换成 (formUuid, version) 形态并删平台级回退;新增 readDomainForm;publish 末尾调用索引补建;MAX_CODE_LEN_FOR_ANSWER_NO 行内注释按 49 口径更正(T3 验收 §4.2 勘误) P25、P32、V9、勘误②
service/AnswerService 提交写 form_uuid;requireSubject 增存在性+同校;assertAttachmentsOwned 增 key/fileName 比对 P25/P33/P34
service/AnswerAdminService 方法签名 (..., Integer operatorId, ...) → (..., BoaUser operator, ...);每条动作前过行级范围(P30 收紧点);pinnedForData 新坐标;P33/P34 两处校验;批量逐条判范围 P25/P30/P33/P34、V39 范围半边
service/AnswerIndexService write 抽出 rowsFor;新增 backfillAfterPublish(只增不删、幂等、按 id 游标分批) V9(记 P36)
service/FormAdminGuard 角色闸门 → 授权模块集;+requireAdminAny/authorizedPaths P21 到期替换
common/FormValueValidator attachmentFileIds → attachments(返回三件套节点) P34
common/FormSchemaValidator + filterableFields(searchable 且非附件类) P28
enums/FormEnums + DataScope 四值;Audience.nameOf(oapi 下发枚举名) 契约 §8/§4
controller/api/FormAdminController 20 个端点按 /admin/forms(定义 14)与 /admin/dicts(字典 6)归类;filterFields 排除附件类 P21、P28
controller/api/FormAnswerAdminController 5 个端点改 /admin/data 口径并把 user 传进服务 P21、P30
src/test/.../AnswerTestSupport + 两个网关桩(InMemoryModuleTreeSource/InMemorySubjectDirectory,@TestConfiguration+@Primary)、manager() 助手、roleId=7 默认授权中心模块行 P21/P33 落地后的测试形态
src/test/.../{AnswerSubmitIT,AnswerAdminIT,AnswerConcurrencyIT,FormFileServiceIT} 调用点随签名改写(断言语义未变);FormAdminGuardTest 按新口径重写(5→9 条) 同上
docs/T3-report.md §1.1 "无物理删除"措辞更正 + §2 V32 "两个不同教师"注明实为两个 roleId=7 操作者 T3 验收 §4 勘误①③
sql/t3_answer_domain.sql answer 加 form_uuid 列与 idx_answer_form_time,脚本头记 T4 变更说明 P25/铁律③
application.yml 一字未改(P8:/oapi 真实消费方出现再补 trusted-apps;教训注释原样保留) P8

1.7 环境动作(本机副作用)

  • 复用本地 MySQL 8.0.45(3306)+ 库 bsch_formcenter:ddl-auto=update 给 answer 追加 form_uuid varchar(32) NOT NULL 与 idx_answer_form_time,SHOW COLUMNS/SHOW INDEX 实测已生效(§3);当时表零行 → 无回填、无锁表风险。
  • 零残留:全量 177 条测试跑完后十张表实测 count(*)=0(IT 一律 @Transactional,并发 IT 靠 @AfterEach 定点清理)。
  • 未启动 CAS/UserBase/Redis;未触碰 160 共享环境;local-debug/t4-read-smoke.sh 启动冒烟用 target/form-center.jar 直跑(无 env 注入,走 yml 本地兜底键)。

2. 自验证据(逐 V)

门禁:mvn -B -o clean package → BUILD SUCCESS,Tests run: 177, Failures: 0, Errors: 0, Skipped: 0,产物 target/form-center.jar。 本轮新增 56 条(DataScopeServiceTest 15 + AnswerReadIT 14 + FormFilterOperatorsTest 7 + FormValueSchemaBuilderTest 5 + AnswerExportIT 4 + OapiFormServiceIT 4 + PlatformSubjectDirectoryTest 2 + AnswerSubmitIT +1 + FormAdminGuardTest 5→9),其中 23 条打真实 MySQL;T2 的 67 条与 T3 的 54 条一条未改语义仍全绿。 测试形态按任务书:纯逻辑走 *Test(不启 Spring),列表/统计/导出走 *IT(真库 + 注入桩顶掉 UserBase 两件事源)。

V 断言(方案 §9 原文要点) 证据
V9 新增 searchable 字段全程无 DDL,旧数据按新字段筛选正确 AnswerReadIT.searchableFieldAddedInV2FiltersOldAnswers:v1 里 advisor 存在但不 searchable → 不在候选;发布 v2 声明 searchable(零 DDL)→ 候选出现该字段,且三条 v1 期写的答卷按 advisor 前缀筛出 {张三, 代填张三}、按"张老师"筛出两条教师行。机制=发布时 backfillAfterPublish 补索引行(只增不删,与 V8 同向)
V11 统计返回总数/按状态/按字段分组,前端不做聚合 AnswerReadIT.statsReturnsTotalStatusAndFieldGroups:total=5、byStatus=[pending/待处理:5]、byField(grade)=[g2/高二:3, g3/高三:2](label 由 options 回查);带状态过滤时总数与桶一起窄化
V23 关闭 open-in-view 后详情/列表/导出无 LazyInitializationException 三条路径全在 @SpringBootTest(open-in-view=false 生效的那份配置,detailReturnsPinnedSchemaAndValues 显式断言该属性为 false)下跑通;实体无关联映射,谓词与取数均为显式批量
V27 范围贯穿:任一范围身份调统计/导出,结果集 ⊆ 其列表可见集(四路径同一构造器) AnswerReadIT.statsAndExportAreSubsetsOfListForEveryIdentity:对 6 个身份(管理员/教师1/教师2/学生1/学生2/局领导)逐个把列表可见集当基准,断言 `stats.total ==
V28 任教班级出现在契约枚举中但一期不可选 DataScopeServiceTest.teachingClassIsInEnumButNotSelectable(枚举四值齐全、selectableScopes() 只三个)、noCombinationResolvesToTeachingClass(7 角色 × 2 人群全遍历解析不出)、TeachingClassScopeImplementor 被误用时抛"一期未开放";FormAdminGuardTest 另钉住"锚点不给管理权"
V37 导出范围=单表单+筛选、行数=范围总数(非当页)、选项列显 label、附件列显 fileName、受限身份 ⊆ 可见集 AnswerExportIT 四条:25 行数据(页大小 20)导出 25 行且表头逐列断言;select 出"高二年级"而非 g2、多选 ["phy","math"] 出定义序"数学、物理"、image 列出"奖状.jpg"(值里无 URL)、第一列是 32 位 uuid;筛选 grade=g3 只出高三 3 行、非法筛选 key 被拒不静默导全量;受限身份逐身份子集断言
V42 角色矩阵验默认、customId 授权升级、全不命中回落、任一范围身份统计/导出 ⊆ 列表 单元半边 DataScopeServiceTest(矩阵逐格 + 并集 + 回落 + 超管 + 锚点递归);接口半边 AnswerReadIT.matrixResolvesPerContractSectionEight、scopeAllAnchorUpgradesTeacher(教师授权锚点后 scopes=[ALL]、列表 5 行)、statsAndExportAreSubsetsOfListForEveryIdentity
V30 读侧半边(编排补充③) 软删答卷在列表/详情/统计/导出均不可见 AnswerReadIT.softDeletedAnswerIsInvisibleOnAllFourPaths:列表不含、详情 答卷不存在或已删除、统计 total=5(未删数)、导出 5 行且 uuid 集合不含(T3 已验索引删行与管理侧,本轮补四条读路径)
V39 范围半边(编排补充①) 批量软删/改状态同受数据范围约束(与 V27 同构造器) AnswerReadIT.detailIsScopeBound:教师1(已授权 /admin/data 模块行,能进接口)批量改状态 [自己的, 别人的] → 第一条成功、第二条逐条报"不在你的数据范围内";留痕形态与单条一致由 T3 的 AnswerAdminIT.batchIsPerItemValidationAndLogging 承担
V3 筛选候选 / V4 日期 / P28 未声明 searchable 不进候选;日期按日期比;附件无算子 FormFilterOperatorsTest(算子逐类、2023-1-9 被拒、upload 明确"无检索算子"、候选集排除 cert 而索引口径不变)、AnswerReadIT.operatorsAreTypeBound(等值/前缀/区间/日期区间在真库上命中正确集合;LIKE 转义后 % 作为输入不命中全部)、attachmentFieldsAreNotFilterable
V1/V4 契约形状 / §4 下发 schema 端点下发内联 options + valueSchema FormValueSchemaBuilderTest 5 条(11 类逐类形状、不发明 multipleOf/uniqueItems、附件 additionalProperties:false、入参不被改写);OapiFormServiceIT.schemaIsSelfContainedWithInlineOptions(字典已内联、dict:grades 不出现在出参、audience="STUDENT")
V5/V8 相邻面 + P25 uuid 寻址、id 不外露、历史答卷按钉住版本读改 AnswerReadIT.detailReturnsPinnedSchemaAndValues(升版后详情仍给 v1 形状)、platformFormShadowedBySchoolFormStillValidatesItsOwnAnswers(P25 用例:平台表单被本校同 code 遮蔽后,历史答卷编辑按原 schema 放行、夹带本校新字段被拒"未知字段")、AnswerSubmitIT.adminCanFillOnBehalf 断言 form_uuid=resolvePublished 命中行
V17/V18 前置项加固 代填 subject 真实且同校 AnswerSubmitIT.proxySubjectMustExistInSessionSchool(查无此人 / 跨校 → PARAM_ERROR;同校放行;本人自填不触发查询)、PlatformSubjectDirectoryTest(三层归属字段 + fail-closed)
V26 相邻面 无授权模块行者后端拒 FormAdminGuardTest(未授权拒、四模块行互不通用、/admin/stats 或 /admin/data 二选一、跨校与平台级拒绝、无会话=UNAUTHORIZED)
P34 附件 key 与 store_key 一致 FormFileServiceIT.submittedAttachmentIdsMustBeOwned 增两例:id 真、key 指向别的相对路径 → "附件 key 与上传记录不一致";fileName 改名 → "附件文件名与上传记录不一致";正向原样转存不受影响

其它取证

项 结果
启动冒烟 bash local-debug/t4-read-smoke.sh → 9/9 PASS:health=200、Started FormCenterApplication;/api/form/admin/data/{list,detail,filterFields,stats,export} 无票均 302→CAS(POST 型用 -X POST -d '{}' 打,证明映射存在而非 404);/oapi/form/{schema,answer} 401 验签墙
端点清点 37(按方法级 @GetMapping/@PostMapping 逐条计):FormAdminController 20 + FormBaseController 2 + FormResolveController 1 + FormAnswerController 2 + FormAnswerAdminController 5 = 30(T1~T3 既有)+ FormAnswerReadController 5 + OapiFormController 2 = T4 新增 7。同口径复核 T3 提交(git grep -c on bff0fb1)是 30 个,而 T3 报告写"28=21+7"——那是按 Mapping( 行数计的另一种口径(含类级 @RequestMapping、且 21+7≠28),本轮起以 30/37 这组数为准(验收侧若沿用了 28 需同步)。全仓 零 nativeQuery(MySQL 5.7 兼容口径不变,P10)
红线自检 四条读出路径 + 管理动作的谓词全部出自 AnswerScopeQueryBuilder(AnswerReadService/AnswerStatsService/AnswerExportService/AnswerAdminService 四处调用点,均先经 scopeQuery 取规格);构造器不接受"跳过范围"开关,空范围直接 FORBIDDEN
错误码(P14 定稿四段) 游标不可见/筛选形态错/分组字段不可用/附件 key 不符/被代填人无效/表单·答卷未找到=PARAM_ERROR(2);未登录=UNAUTHORIZED(4);无模块权·越范围=FORBIDDEN(3);任教班级未开放·范围签名缺失·未发布=CUSTOM(5)。无新增枚举
容量口径 列表页上限 100(超出截断);导出批 500;索引补建批 500;统计缓存条目上限 256(超出整体清空)——都是安全阈值,不是契约条款

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

# 问题 本轮取值(最保守解读) 影响面
P35 契约 §10 的 idx_answer_form(sch_id,code,status,id) 对"单表单 + 未删 + 按 id keyset"的列表/导出不是覆盖索引(不含 app_code,且 status 夹在中间无法跨状态给 id 序) 新增 idx_answer_form_time(sch_id,app_code,code,is_deleted,id)(纯加项,不改任何既有列/uk/索引语义)。要否并入契约 §10 请裁决;V10 压测时 EXPLAIN 复验选择性 铁律③;生产 ddl-auto=none 需按脚本追加 CREATE INDEX
P36 V9"旧数据按新 searchable 字段筛选"要求历史答卷有索引行,而提交时只按当时钉住版本写行——方案 §5.2/§5.5 未指定补建机制 发布时同步补建(AnswerIndexService.backfillAfterPublish:按 form_uuid 未删答卷 id 游标分批、只插缺失 field_key、只增不删、幂等)。代价记账=大表单升版发布多一次全量值文档扫描(2 万级秒级);若裁决改异步任务,改点在 FormDefService.publish 一处。不做半成品双路径 V9;升版耗时;R12 引用影响面
P37 方案 §5.5 点名"大表单全量分组统计加 Redis 缓存",本轮实现为进程内 ConcurrentHashMap + TTL 进程内缓存:键含 form_uuid+version+范围签名、仅升版换键失效、TTL 60s、条目上限 256、只缓存无筛选的全量统计。不改失效语义(铁律④完全一致),改的是载体:本地无 Redis 即可跑通门禁,且少一个跨请求共享状态;多实例各自缓存对"容忍 N 分钟延迟"无实质影响 若要求一期就上 Redis,替换 AnswerStatsService 的 cache 字段即可(键与失效策略照用)
P38 契约 §8"多来源取最宽"在 仅本人提交 与 关于我 两个不可比较值之间取哪个,文档未写 取并集(谓词 OR),多身份用户两格都可见;命中 全部 即收敛。理由:单值取舍会丢掉一整格(学生+教师双身份者看不到"关于我"),而并集是"最宽"的字面实现且不引入包含关系误判 双身份用户的可见集;V42 断言形态
P39 铁律②游标是库内 answer_id,但 V5 要求 id 不出现在任何响应外层 对外游标=当页末行 uuid,服务端按 uk_answer_uuid 点查换成 id,且游标本身也过范围(不可见即拒)。每页多一次索引点查 URL 形态(T8 翻页参数用 cursor=uuid)
P40 契约 §9 的 schema 端点只写 ?schId=&code=,快应用表单(app_code≠form-center)无法按该形状取 加可选 appCode(不传=form-center,缺省行为与契约文本一致)。是否回写契约 §9 请裁决 L2 消费方接快应用表单时
P41 契约 §9 说 answer 返回"钉住版本的 schema 引用",但 §9 的 schema 端点只下发当前发布版——只给引用则消费方拿不到当时那张表 data 里同时带 schemaRef{formUuid,appCode,code,version} 与钉住版本内联展开后的 groups(自包含,同 §4 原则)。要"只给引用"就删 groups 字段,但消费方需另开一个按版本取 schema 的端点 对外载荷体积;快照语义
P42 P33 校验依赖 UserBase,不可达时按"查不到"还是"跳过校验"未写 fail-closed:不可达与查不到同样 PARAM_ERROR(日志分档便于排查,同 flow-center requireUserName 口径)。代填是低频动作,宁可让管理员重试 UserBase 故障时管理员代填不可用;真实链路 T9 验
P43 导出的列集合与"提交人/被代填人"显示形态,方案 §5.5/§6.4 未写 列=当前发布版本 schema 字段序(历史版本独有字段不出列,但该行仍在、身份列完整);两个身份列导出 id 而非姓名(姓名需按行批量查 UserBase,一期不引 N+1 远程调用)。要否"最新 schema ∪ 历史 searchable"请裁决 V37 列形态;T9 压测的导出耗时
P44 管理权与数据范围都来自 UserBase 模块树,一个读请求最多两次 byId/modules 调用 一期不做缓存(每请求 1~2 次签名调用),与 FormBaseController 现行形态一致。压测暴露再谈短 TTL 缓存或控制器内一次取树两处复用 列表/统计 p95(V10 阈值 1.5s/3s)——T9 关注项
P45 手册 §2.2 说统计模块行"可并入数据管理,二选一",接口按哪个判 新增 requireAdminAny,/admin/stats 或 /admin/data 任一命中即放行统计接口(其余接口仍单行精确判) 学校侧模块行配法自由度
P46 canManage(代填权)在"授权模块集"口径下该覆盖哪几行,P21 只说"整体替换" 超管 或 /admin/data 或 /admin/forms(与 P21 期 roleId∈{2,7} 等宽,避免替换口径顺手收窄已验收行为)。只授权 /admin/dicts 者不能代填(已测) V17/V32 的代填分支

trusted-apps 本轮未动(P8:真实消费方出现再补)。yml 与 T3 验收时逐字一致。


4. 推送与 CI

仓 提交 推送 CI run 结论
form-center-service 49b095e(57 条路径:34 新增 + 23 修改,逐条 git add,清单见 §1.6/§1.7) b2778e5..49b095e main -> main run 1493(head_sha 49b095eb812a…,display_title 即本次提交标题) status=completed / conclusion=success(Gitea Actions API 实测,HTTP 200)
form-center-service 2224523(docs-only,只改本文) 49b095e..2224523 main -> main run 1494 completed / success
form-center-front 无改动 未推送 — T4 是纯后端任务

回填到此为止:本次 docs 提交(2224523)自身触发的 run 1494 已是终态,其后再不追加自指回填提交。

  • 暂存方式:按显式路径逐条 git add(57 条),提交前 git status 核对无残留未跟踪项、无 add -A、无敏感文件; target/(含 jar 与冒烟临时 json)与 local-debug/ 都不在本仓工作树——local-debug/ 在 form-center/ 根,而仓库根是 form-center/service,git check-ignore 直接报 "outside repository"。 因此本轮新增的冒烟脚本 local-debug/t4-read-smoke.sh 不入库(沿 T1~T3 同一事实,执行手册 §4.2 要求的"脚本入 local-debug"落在该目录本身,入库需另建前端/脚本仓或调整仓库根,不在本轮擅自动)。
  • 取证方式:git credential fill 会拉起 GCM 图形框(本会话非交互直接超时),故改从 ~/.git-credentials 取 192.168.10.160 这条已存凭据调 GET /api/v1/repos/.../actions/runs[/{id}], 凭据不回显、不落盘、不入库(脚本产物 target/runs.json、target/run1493.json 已被 .gitignore 排除)。
  • CI 只跑 mvn -B -DskipTests clean package(不跑单测,方案 §7.2),所以 CI 绿只证明产物链可构建; 读路径/范围/导出的行为证据在本文 §2 的本地 mvn test(177 条,其中 23 条打真实 MySQL)。
  • 本节的回填本身是一次 docs-only 提交(只改本文,不动任何构建输入),其后同流水线再跑一次。

—— 全文完 ——