# 业务表单 JSON 格式与数据规范分析

分析对象：`samples/forms/食品经营许可证-20260508-1057.txt`  
表单名称：食品经营许可证  
表单 ID：`601`  
模板 ID：`601~1778209020987`  
表单版本：`3.6.0522`

## 1. 总体结构

表单 JSON 是一份完整的动态表单定义，不只是字段列表。顶层结构包含：

- 表单元数据：`formId`、`tempId`、`formName`、`creator`、`createTime`、`version`
- 表单展示配置：`labelPosition`、`fontSizeControl`、`labelWidth`、`lineGap`
- 表单组件树：`formItems`
- 流程与备注：`process`、`remark`
- 自定义校验脚本：`js`
- 导出信息：`extra`

后端解析时，核心输入是 `formItems`，但必须保留顶层元数据和 `js`，因为它们影响版本、校验和业务追溯。

## 2. 组件树规则

`formItems` 是递归树。节点分为容器节点和业务字段节点。

容器节点：

- `GroupLayout`
  - 分组容器，可展示为卡片或标签分组
  - 子节点在 `props.items`
- `SpanLayout`
  - 分栏容器，常用于两列布局
  - 子节点在 `props.items`
- `TableList`
  - 明细表容器
  - 表格列在 `props.columns`

业务字段节点：

- `TextInput`
- `DateTime`
- `SelectInput`
- `MultipleSelect`
- `CascadeSelect`
- `NumberInput`

辅助节点：

- `Description`
  - 说明文本，不应作为填表字段

重要过滤规则：

- 只解析 `formItems` 和 `TableList.props.columns` 下的真实字段
- 不解析组件 `super` 内的默认模板字段
- `Description` 不进入业务字段表
- `invisible=true` 的字段仍要入库，因为它可能由触发规则显示，也可能需要自动填值

本样例中，真实业务字段统计为：

| 类型 | 数量 |
| --- | ---: |
| TextInput | 9 |
| DateTime | 1 |
| SelectInput | 14 |
| CascadeSelect | 1 |
| NumberInput | 1 |
| MultipleSelect | 1 |
| TableList | 1 |

其中 `TableList` 包含 10 个明细列。真实可填字段加明细表结构共 28 项。如果把 `super` 里的原型控件也算进去会得到 31 项，但这 3 项不应作为业务字段导入。

## 3. 字段对象规范

普通字段对象常见结构：

```json
{
  "cnName": "单行文本输入",
  "title": "经营者名称",
  "name": "TextInput",
  "inputName": "spjy_foodopername",
  "value": "",
  "defValue": "",
  "valueType": "String",
  "props": {
    "required": true,
    "readOnly": false,
    "invisible": false,
    "regexp": "",
    "options": [],
    "triggers": []
  },
  "id": "field35675"
}
```

必须抽取并入库的字段元数据：

- `id`：表单控件 ID，如 `field35675`
- `title`：显示标题，如 `经营者名称`
- `name`：组件类型，如 `TextInput`
- `cnName`：组件中文类型
- `inputName`：业务字段表达式，必须保留原文
- `canonicalKey`：从 `inputName` 解析出的主字段名
- `valueType`：值类型，如 `String`、`Date`、`Array`、`Number`
- `defValue`：默认值
- `props.required`：是否必填
- `props.readOnly`：是否只读
- `props.invisible`：是否初始隐藏
- `props.options`：选项/字典候选
- `props.triggers`：联动规则
- `props.regexp` / `regexpFormat`：校验规则
- `props.placeholder` / `suffix` / `precision`：输入显示与数值规则
- 分组路径：根据父级 `GroupLayout` / `SpanLayout` 生成

## 4. inputName 表达式规范

`inputName` 不只是字段名，常包含业务指令：

```text
jjxz[复制到food_operation_licence_widget:jjxzbm&字典值编码spjy_jjxz&复制到food_operation_licence_widget:jjxz&清理]
```

解析规则：

- `canonicalKey`：第一个 `[` 之前的内容
  - 示例：`jjxz`
- 指令区：`[` 和 `]` 之间的内容
- 多个指令用 `&` 分隔
- 指令必须按原文保留，同时解析为结构化数组

常见指令：

| 指令形式 | 含义 |
| --- | --- |
| `复制到xxx` | 识别值还要写入目标业务路径 |
| `字典值编码spjy_jjxz` | 当前字段使用指定字典编码 |
| `字典列表key:label编码spjy_common` | 字典列表以 label 作为 key 进行匹配 |
| `转类型string` | 输出时需要转为字符串 |
| `清理` | 填表前需要清理中间值或临时状态 |

目标业务路径可能是多级路径：

```text
food_operation_licence_widget:ztyt:yjztytbm
```

因此不能简单当作普通字符串字段处理。标准模型应同时保存：

```json
{
  "inputNameRaw": "yjztyt[复制到food_operation_licence_widget:ztyt:yjztytbm&字典值编码spjy_ztyt&复制到food_operation_licence_widget:ztyt:yjztyt&清理]",
  "canonicalKey": "yjztyt",
  "directives": [
    {"type": "copyTo", "target": "food_operation_licence_widget:ztyt:yjztytbm"},
    {"type": "dictCode", "code": "spjy_ztyt"},
    {"type": "copyTo", "target": "food_operation_licence_widget:ztyt:yjztyt"},
    {"type": "cleanup"}
  ]
}
```

## 5. 字段清单

| 分组 | 标题 | 组件 | fieldId | canonicalKey | 值类型 | 必填 | 初始隐藏 |
| --- | --- | --- | --- | --- | --- | --- | --- |
| 基本信息/基本信息1 | 经营者名称 | TextInput | field35675 | spjy_foodopername | String | 是 | 否 |
| 基本信息/基本信息1 | 申请日期 | DateTime | field55825 | spky_sqsj | Date | 是 | 否 |
| 基本信息/基本信息1 | 经济性质 | SelectInput | field23801 | jjxz | String | 是 | 否 |
| 基本信息/基本信息1 | 其他经济性质 | SelectInput | field52540 | qtjjxz | String | 是 | 是 |
| 基本信息 | 与申请人主体资格证明上标注的住所（经营场所）地址 | SelectInput | field32061 | jycsdzyzssfyz | String | 是 | 否 |
| 基本信息 | 经营场所地址 | TextInput | field28593 | jycsdz | String | 是 | 否 |
| 基本信息 | 仓库地址情况 | SelectInput | field12963 | sfsyck | String | 是 | 否 |
| 基本信息/主体业态 | 主体业态 | SelectInput | field53050 | yjztyt | String | 是 | 否 |
| 基本信息/主体业态 | 餐饮服务经营者 | SelectInput | field24525 | ejztyt | String | 是 | 否 |
| 基本信息/经营项目 | 经营项目 | CascadeSelect | field67572 | jyxm | Array | 是 | 否 |
| 基本信息 | 食品加工经营面积 | NumberInput | field83303 | spxsjcyfwcssymj | Number | 是 | 是 |
| 基本信息/基本信息3 | 网络经营 | SelectInput | field97812 | sfcswljy | String | 是 | 否 |
| 基本信息/基本信息3 | 是否具有实体门店 | SelectInput | field35002 | sfystmd | String | 是 | 否 |
| 基本信息/基本信息3/网络经营类型 | 网络经营类型 | MultipleSelect | field93184 | 网络经营类型 | Array | 是 | 否 |
| 基本信息/基本信息3/网络经营类型 | 自建网站具体网址 | TextInput | field14486 | website | String | 是 | 是 |
| 基本信息/基本信息3/网络经营类型 | 第三方平台 | TextInput | field03614 | ptmc | String | 是 | 是 |
| 基本信息/基本信息3/网络经营类型 | 其他 | TextInput | field81838 | qtwlpt | String | 是 | 是 |
| 食品安全管理人员信息 | 明细表 | TableList | field73056 | table_b8b541f4a70 | Array | 是 | 否 |

明细表 `table_b8b541f4a70` 列：

| 标题 | 组件 | fieldId | canonicalKey | 值类型 | 必填 | 校验 |
| --- | --- | --- | --- | --- | --- | --- |
| 姓名 | TextInput | field11050 | spjy_aqgly_xm | String | 是 | - |
| 性别 | SelectInput | field56541 | spjy_aqgly_xb | String | 是 | 字典 `spjy_common` |
| 证件类型 | SelectInput | field66493 | spjy_aqgly_zjlx | String | 是 | 字典 `spjy_grzjlx` |
| 证件号码 | TextInput | field56476 | spjy_aqgly_zjhm | String | 是 | 身份证号正则 |
| 联系电话 | TextInput | field78561 | spjy_aqgly_lxdh | String | 是 | 手机号正则 |
| 食品安全管理员证书号 | TextInput | field87429 | spjy_aqgly_zsh | String | 否 | - |
| 食品安全管理员证书证书类型 | SelectInput | field18407 | spjy_aqgly_zslx | String | 否 | 字典 `spjy_common` |
| 证书等级 | SelectInput | field07958 | spjy_aqgly_zsdj | String | 否 | 字典 `spjy_common` |
| 专职/兼职 | SelectInput | field94280 | spjy_aqgly_jzqk | String | 否 | 字典 `spjy_common` |
| 职务 | SelectInput | field98812 | spjy_aqgly_zw | String | 否 | 字典 `spjy_sayzw` |

## 6. 选项与字典规范

`SelectInput`、`MultipleSelect`、`CascadeSelect` 的选项通常在 `props.options` 中：

```json
{
  "value": "1",
  "label": "企业"
}
```

归一化时必须同时保存：

- 原始识别文本：如 `企业`
- 选项 label：如 `企业`
- 选项 value：如 `1`
- 字典编码：如 `spjy_jjxz`
- 匹配置信度

字典匹配规则：

1. 优先精确匹配 label
2. 再匹配 value
3. 再做同义词/别名匹配
4. 对多选和级联字段返回数组
5. 对级联字段保留完整路径和叶子值

示例：

```json
{
  "canonicalKey": "jjxz",
  "rawValue": "企业",
  "label": "企业",
  "value": "1",
  "dictCode": "spjy_jjxz"
}
```

## 7. 触发规则规范

字段联动规则在 `props.triggers` 中。常见结构：

```json
{
  "condition": "==",
  "compareType": "string",
  "compareToStr": "是",
  "actions": [
    {
      "event": "show",
      "effectOn": ["field18438"]
    }
  ]
}
```

本表单包含的动作类型：

- `show`：显示目标字段
- `hide`：隐藏目标字段
- `setProp`：动态修改目标字段属性，尤其是选项列表
- `setValue`：设置或清空目标字段值

第一版处理策略：

- 后端必须保存触发规则
- 自动填表输出所有识别字段值和规则提示
- 复杂显隐和动态选项优先由业务表单自身执行
- 后端字段映射模块可基于已识别值进行简单显隐推断，但不能覆盖表单前端运行时规则

## 8. 校验规范

字段级校验来源：

- `props.required`
- `props.regexp`
- `props.regexpFormat`
- `valueType`
- `props.min` / `max` / `precision`

本表单已有明确校验：

- 证件号码：身份证号正则
- 联系电话：手机号正则
- 申请日期：`yyyy-MM-dd`
- 食品加工经营面积：数字，精度 2，输出时有 `转类型string` 指令

表单级自定义校验在顶层 `js` 中，主要包括：

- `field67572` 经营项目互斥规则
- 单位证件类型必须为统一社会信用代码
- 单位证件号码必须为 18 位

注意：顶层 `js` 中引用了 `window.parent.TsFillDataFunc()`，这说明有些校验依赖父级业务系统数据，不完全在当前表单 JSON 字段树内。后端应保留这些脚本作为审计和提示，不在第一版尝试完整执行浏览器脚本。

## 9. OCR 自动填表输出规范

自动填表输出至少需要三种索引：

- `fieldValuesByFieldId`
- `fieldValuesByInputName`
- `fieldValuesByCanonicalKey`

推荐结构：

```json
{
  "formId": 601,
  "tempId": "601~1778209020987",
  "formName": "食品经营许可证",
  "taskId": "T202605080001",
  "fieldValuesByFieldId": {
    "field35675": "某某餐饮店"
  },
  "fieldValuesByCanonicalKey": {
    "spjy_foodopername": "某某餐饮店"
  },
  "fieldValuesByInputName": {
    "spjy_foodopername": "某某餐饮店"
  },
  "copyTargets": {
    "food_operation_licence_widget:jycsdz": "某地址",
    "spjy_dom": "某地址"
  },
  "tableValues": {
    "table_b8b541f4a70": [
      {
        "spjy_aqgly_xm": "张三",
        "spjy_aqgly_xb": "男",
        "spjy_aqgly_lxdh": "13800138000"
      }
    ]
  },
  "warnings": []
}
```

## 10. 数据库落表建议

表单解析结果需要落到以下逻辑表：

- `ocr_form_template`
  - 保存顶层表单元数据和原始 JSON
- `ocr_form_field`
  - 保存普通字段、明细表、明细列
- `ocr_field_option`
  - 保存普通选项、级联选项、动态选项快照
- `ocr_field_directive`
  - 保存 `inputName` 中解析出的复制、字典、转类型、清理等指令
- `ocr_field_trigger`
  - 保存显隐、动态属性、赋值规则
- `ocr_form_validation`
  - 保存正则和顶层 JS 校验摘要

## 11. 后续表单兼容规则

以后 `samples/forms` 或配置目录增加新表单时，导入器应按相同规则处理：

1. 自动扫描 JSON/TXT 文件
2. 读取顶层 `formId`、`tempId`、`formName`
3. 递归解析 `formItems`
4. 忽略 `super` 原型字段
5. 提取普通字段、明细表和明细列
6. 解析 `inputName` 指令
7. 保存原始 JSON 和结构化字段
8. 生成字段树和字段清单供人工确认
