Files
form-center-service/docs/schema-extension-draft.md
T
chenxing d7473112b0
build / build-push (push) Successful in 1m26s
t1~t3落地+10-22精修: 消费授权v3(owner/grant/订阅/目录)+答卷投递v2(可信提交上下文/快照/幂等关联)+答卷对账v1(清单/派发/租约/结果)
含10-22精修六类修复: 续租renewRunning/结果INCOMPLETE_RESULT门/终态TASK_TERMINAL/liveGrant复用ACL/hosted幂等重放绑定/错误明细整条重序列化。
合规审计与证据: 工作区docs/tasks/REFINEMENT_AUDIT.md + evidence/refinement-20261022/(patch+指纹)。SQL未执行、测试未运行(仅-skipTests编译)。
2026-10-22 10:02:57 +08:00

13 KiB
Raw Blame History

公共 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支持。