含10-22精修六类修复: 续租renewRunning/结果INCOMPLETE_RESULT门/终态TASK_TERMINAL/liveGrant复用ACL/hosted幂等重放绑定/错误明细整条重序列化。 合规审计与证据: 工作区docs/tasks/REFINEMENT_AUDIT.md + evidence/refinement-20261022/(patch+指纹)。SQL未执行、测试未运行(仅-skipTests编译)。
13 KiB
公共 schema 扩展契约草案(TASK_4)
日期:2026-10-22。状态:文档检查完成,候选功能未实施。本文不批准SUR-T19/SUR-T20完整功能,不取消OPEN-T19b代提交;不修改校验器白名单、题型注册、统计或流程引擎。普通表单、匿名、自填与原有代填能力继续按现有代码解释。托管SH首批仅本人,是独立的既有边界。
1. 证据基线与当前实现
本次重新读取各子仓库HEAD及当前工作区,未创建提交:
| 项目 | HEAD |
|---|---|
| FC后端 | b2437a20742bfbfe38a564134904f3e0d7d931a3 |
| FC前端 | 2e406a88b1d9e09b2ca54a8cf68a3726174f49c0 |
| FL后端 | 2f54f37ab34ca71410ec854f1adcc1da7c4dbe5f |
HEAD不包含Task1~3及本轮精修的未提交内容。下列公共校验常量/函数锚点从当前源码重新摘录;FL快照字段搬运也与git show HEAD:...FormCenterSchemaClient.java对照。改动过的管理/提交路径引用当前工作区,并在根目录docs/tasks/evidence/refinement-20261022/留源文件指纹,不能把工作区源码宣称为HEAD中已实现。
FC / src/main/java/com/yunhai/formcenter/common/FormSchemaValidator.java / FormSchemaValidator#validateSchema、#validateField:
if (!root.isObject() || !root.path("groups").isArray()) {
throw fail(null, "表单结构必须是 {\"groups\":[…]}");
}
rejectUnknownProps(group, ALLOWED_GROUP_PROPS, "分组属性");
rejectUnknownProps(field, ALLOWED_PROPS, "字段属性(" + key + ")");
这三句分属根对象、group循环、field方法,不是连续源码。根对象当前只检查对象及groups形状,没有统一拒绝全部根级未知属性;这不意味着未知规则会执行。group仅允许key/label/fields;field允许以下19项:
key,label,type,required,pattern,min,max,maxLength,tip,placeholder,options,
rows,defaultValue,accept,maxSize,maxFiles,precision,unit,searchable
FC / 同文件 / FormSchemaValidator#validateOptions:inline选项对象仅label/value;字典引用仅source且匹配dict:<code>。字段valueSchema由读接口生成,不属于设计写入白名单。visible_when、matrix及任意脚本属性都不是已实现契约。
FC / 同文件 / FormSchemaValidator#PHASE1_TYPES:
public static final Set<String> PHASE1_TYPES = new LinkedHashSet<>(Arrays.asList(
"text", "textarea", "number", "date", "select", "radio", "upload",
"checkbox", "multiselect", "phone", "image"));
address/user/users虽在14类冻结命名中,但未在一期放行。FC前端 / src/types/formSchema.ts / PHASE1_TYPES、ALLOWED_FIELD_PROPS及group/option/ref常量与之对应;src/components/fields/index.ts / fieldWidget、widgetTypes注册11类,app端组件仍有槽位限制,不能仅按目录存在就声明移动端完整可用。
FC / src/main/java/com/yunhai/formcenter/common/FormValueValidator.java / FormValueValidator#validate:
rejectUnknownKeys(data, fields);
if (isEmptyValue(value)) {
if (isRequired(field)) {
throw fail(field, key, "为必填项,未填写");
}
continue;
}
validateValue(field, key, value, dictCandidates);
当前没有先执行visible_when的步骤。FC / common/FormValueSchemaBuilder.java / withValueSchemas在深拷贝上增加机读值形状,不改变版本schema原文;number输出number与边界,未生成precision对应multipleOf;多选输出array,不虚构uniqueItems;附件items闭集为id/fileName/key。机读片段不是完整的执行引擎替代物。
2. 扩展分类(未来候选,不是今天的兼容承诺)
| 类别 | 候选承载方式 | 未来识别策略 | 当前事实 |
|---|---|---|---|
| 不可执行display metadata | 独立版本化命名空间,如extensions.display,只放纯文本/枚举/布局提示 |
明确允许的读写链路原样保留;不参与资格、值校验、路由、hash派生规则;所有展示转义 | 当前field/group未知属性仍拒绝,设计器会按白名单序列化;没有通用保留保证 |
| 可见性、必填联动、复合值约束等安全规则 | 明确schema契约版本及required capabilities | 写入/发布/提交/编辑/导入遇到不支持的必需规则必须拒绝,不能静默忽略 | 没有扩展能力协商或visible_when执行 |
| 生成的机读描述 | valueSchema及未来投影说明 | 从已核验schema派生,不能作为客户端绕过后端校验的权威 | valueSchema当前仅在读取时生成 |
不使用eval、Function、JS脚本、任意表达式字符串或可调用URL。候选规则采用受限JSON AST,字段引用只能指向本表schema中的稳定key;运算符、类型、深度、节点数、引用数需白名单和预算。下列形状只是评审样例,现有字段白名单不会接受它:
{"key":"detail","type":"text","required":true,
"visible_when":{"op":"eq","field":"kind","value":"other"}}
将显示元数据和安全规则混放、遇到未知规则当作true、根据浏览器传来的visible结果跳过后端校验,均不属于候选契约。
3. 可见性、必填与隐藏值
未来统一顺序:按答卷formUuid/version取schema → 校验规则版本/类型/依赖 → 依拓扑顺序重算可见性 → 处理隐藏值策略 → 仅对可见字段执行required及值约束 → 附件归属/索引/存储。循环引用、未知key、类型错误、预算超限必须失败,不能转成“隐藏”。前端计算只用于体验,后端重算才是依据。
| 可见性结果 | required | 输入 | 候选处理 |
|---|---|---|---|
| visible | true | 缺失/null/类型约定的空值 | 拒绝必填空值;0与false不能当空值 |
| visible | false | 空 | 允许约定空值;是否缺键规范化为null须写入版本协议 |
| visible | 任意 | 非空 | 执行该题类型及值域校验 |
| hidden | 任意 | 无值 | 跳过required,不误报漏填 |
| hidden | 任意 | 有值 | 必须先定稿下表A/B策略,当前不选择、不实施 |
| 规则无效/结果未知 | 任意 | 任意 | 拒绝,返回稳定规则错误,不当作hidden |
| 隐藏值候选 | A:拒绝携带 | B:服务端剥离 |
|---|---|---|
| 用户改上游答案使下游隐藏 | 需要客户端清理后重交;错误可诊断 | 服务端规范化后接收,须告知哪些字段被移除 |
| 审计 | 记录拒绝字段名及规则版本,不把敏感values写日志 | 记录剥离字段名及规则版本,不能伪装成用户主动清空 |
| 幂等/hash | 需明确同一请求的完整输入一致性 | 必须先规范化再生成存储hash,并区分原始请求指纹 |
| 本人/管理员编辑 | 不允许通过隐藏字段偷偷补值 | 不允许以管理员权限跳过规则而无单独协议 |
| 历史数据 | 不在读详情时破坏性重写旧答卷 | 同样不在读详情时自动剥离历史值;需要显式编辑/迁移审计 |
A/B是实施前的业务决策,不能在开发中默认选一种。条件依赖于另一个隐藏字段时,该字段是缺失、历史保留还是不适用,也须一并定稿;禁止靠真假值隐式强转决定。
4. 普通题型与版本来源基线
| 项目/类#方法 | 当前值/版本事实 | 对扩展的边界 |
|---|---|---|
FC common/FormValueValidator#validateNumber |
JSON number,支持min/max、precision;number空值用null,不能用空字符串代替 | scale/矩阵不能借number类型塞数组或对象 |
同类 #validateSingleOption |
select/radio为候选value字符串,存value而非label | 条件比较需比较稳定value,label变化不改历史语义 |
同类 #validateMultiOption |
checkbox/multiselect为value字符串数组;逐项查候选,当前无显式去重拒绝 | 数组不得被FL按标量比较;不宣称JSONSchema有uniqueItems |
同类 #validateAttachments,AnswerService/AnswerAdminService附件归属检查 |
upload/image为[{id,fileName,key}],服务端另核归属、key和大小/数量;值中不存URL | 隐藏附件的剥离、引用释放、历史下载权限须成对设计 |
FC service/AnswerService#doSubmit |
登录普通提交按code/appCode解析发布版本;匿名走resolvePublishedForAnon,实际版本由服务端解析,不接受浏览器指定版本 | 未来普通提交须明确页面解析到写入之间的升版策略;不把hosted的钉版保证推到全部通道 |
同方法hosted分支,前端views/Fill.vue#loadForm/#handleSubmit |
contextToken确定formUuid+formVersion,CAS确定本人及学校;后端按上下文读取确切schema | 扩展也必须按token版本,不能按中心最新schema计算可见性 |
FC service/AnswerImportService#publishedFields/#importAnswers |
字段取当前发布版,并逐行走AnswerService.submit;并非已冻结整次导入的schema快照;upload/image列属于UNSUPPORTED_TYPES,不支持导入附件 | 未来需把预检与实际导入钉在同一版本,并与自填/编辑共用规则;不得把当前导入声称覆盖附件 |
FC service/AnswerReadService#myDetail/#editBySubmitter |
按answer.formUuid/version读schema、编辑校验 | 不因当前表单升级而重解释旧答卷可见性 |
FC service/AnswerAdminService#edit |
按答卷原版本校验并重建索引 | 管理员编辑不能另写显隐判据 |
FC service/AnswerExportService#fieldsForVersion/#fieldsByVersion |
列头按当前版字段,逐答卷版本取字段解释值,缓存按version;标签/字典经expandSchema解析 | 并非已按历史版本并集导出全部已删题;schema原文钉版不等于外部字典label永远冻结 |
FC service/AnswerStatsService#stats/#groupField/#currentSchema |
总数来自权限范围查询;字段元数据来自当前发布schema,按索引分组;没有题目可见性分母 | 不可把现有总数当作“看见该题人数”;跨版本同key统计须显式分组/映射 |
表内路径均相对于form-center/service,前端另已标明。该表为源码基线,本轮未执行上述题型的功能测试。
5. 统计、导出和历史语义候选
未来分母至少分别表示:范围内答卷总数N、该题在其钉版schema存在的答卷E、规则计算可见的答卷V、有效作答A。缺失题/隐藏/可见未答/有效答案不能合并为一个null桶。V与A必须按每份答卷自己的schema版本和规则版本计算;不能把最新规则用于全部历史版本。
- 创建/匿名创建/本人编辑/管理员编辑/导入:同一规则服务与相同的空值、隐藏值策略;写后索引、outbox和hash基于实际存储值,不能只在前端隐藏。
- 统计:题目存在比例用E/N,作答率用A/V(V=0时明确无定义而非除零);多选按答卷计可选次数,不能把选项次数当人数。矩阵每行分母、整题分母分别标注,不跨版本暗合并。
- 导出:按答卷版本给出字段/行列标识及不适用标记;隐藏值是“不适用”还是历史保留受A/B策略与权限共同决定。不得在导出过程中顺手改写答卷。
- 编辑:按原版本重算可见性;如果获批升级答卷schema,必须独立迁移任务、有原/新版本及审计,不混入普通编辑。
- 状态修改/软删:已有revision/outbox流程继续使用;规则版本与可见性派生数据是否改变快照/hash需后续协议显式升级,不能原地变更fc.canon.v1。
6. 矩阵/量表候选
未冻结matrix/scale题型,也未实现渲染、存储、校验或统计。若获批,先定义稳定rowKey/columnValue及整题显隐;不能靠每个单元格的动态隐藏解释同一复合值。
{"satisfaction":{"row_service":"good","row_speed":"average"}}
上例仅为object值的候选;也可评审显式array结构,但须择一版本化,不能同时接受多种模糊编码。unknown row/column、重复row、必答行缺失、额外键、空行与不适用行的行为均须有真值表。复合值到FL必须拒绝作为标量路由,或经获批、显式、版本化的投影变成单一键/数值/枚举。
7. 下一批实施与资源边界
下批应成对分配FC校验器/前端白名单/渲染/提交编辑导入/统计导出与FL快照兼容,文件与任务拆分见FL伴随文档。候选预算可先评审:规则深度8、节点64、引用32,矩阵行列各50、单题2500单元格;这些是待定资源上限,不是当前代码保证。超限必须静态拒绝,不执行递归无界计算。
实施前待定:A/B隐藏值策略、规则JSON版本、依赖隐藏字段语义、字典历史label、矩阵结构、投影命名/空值/值域及统计口径。未决项不影响本次只交付草案,但禁止据草案直接编码候选功能。
验收状态:文档边界/样例/真值表交叉检查完成;FormSchemaValidatorTest等题型测试未运行(遵循本轮用户仅编译/打包要求)。本次相关后端打包只证明编译及构建,不能证明visible_when/matrix支持。