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

146 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 公共 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`:
```java
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项:
```text
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`:
```java
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`:
```java
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;运算符、类型、深度、节点数、引用数需白名单和预算。下列形状只是评审样例,**现有字段白名单不会接受它**:
```json
{"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及整题显隐;不能靠每个单元格的动态隐藏解释同一复合值。
```json
{"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支持。