build / build-push (push) Successful in 1m26s
含10-22精修六类修复: 续租renewRunning/结果INCOMPLETE_RESULT门/终态TASK_TERMINAL/liveGrant复用ACL/hosted幂等重放绑定/错误明细整条重序列化。 合规审计与证据: 工作区docs/tasks/REFINEMENT_AUDIT.md + evidence/refinement-20261022/(patch+指纹)。SQL未执行、测试未运行(仅-skipTests编译)。
146 lines
13 KiB
Markdown
146 lines
13 KiB
Markdown
# 公共 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支持。
|