---
name: microi-form-engine
description: Microi 表单引擎设计与控件配置指南。用于创建或修改 diy_table、diy_field、表单组件、字段属性、选项/SQL/数据源引擎数据源、子表、关联表单、定制组件、表单布局和字段事件。
---

> **Codex 非阻塞自动更新：** 当前宿主为 Codex 时，吾码 CLI、Codex 插件与工作区 AI/MCP 由后台自动更新；需要诊断时读取 `../microi-codex-installer/SKILL.md`。更新失败、等待空闲或尚未重载均不得阻断当前、正在进行或新建任务。非 Codex 宿主跳过此项。

# Microi 表单引擎设计

表单引擎同时驱动数据模型、表单、列表、模块、接口配置和工作流配置。处理“新增字段”
不能只做物理 `ALTER TABLE`：必须让 `diy_table`、`diy_field`、物理列、组件
Config/Data、菜单查询列与缓存保持一致。

平台创建 DIY 表时会自动加入 `DiyCommon.FixedDiyField` 定义的 Id、创建/更新时间、
创建人、租户等固定字段。业务 Manifest 不重复声明这些字段；读取实时 Schema 或离线快照时也不能
因为 `_Fields` 只列出可配置字段，就误判物理表缺少固定字段。

## 必读参考

- 控件完整目录、推荐物理类型和选择规则：`references/component-catalog.md`
- 数据源、字段属性、事件与定制组件：`references/data-source-events.md`
- 表单分组与宽度：`../microi-form-layout/SKILL.md`
- 后端表单事件：`../v8-table-event/SKILL.md`
- 前端字段事件：`../v8-frontend-events/SKILL.md`

## 标准工作流

### 物理字段允许为空（强制）

除作为主键的 `Id` 外，通过 MCP、表单设计器、FormEngine、V8 或应用包新增、修改的普通字段，数据库列一律允许 `NULL`。`NotEmpty` 等业务必填要求只作用于表单/服务端校验，不能生成 `NOT NULL`。默认值和是否允许为空是两项独立属性；兼容旧列时只放宽可空约束，保留类型、有效默认值、字符集、排序规则、注释、索引和历史数据，不以补零/空串替代结构修复。

MySQL 的 `ALTER COLUMN DROP DEFAULT` 会让可空列也在省略字段时报 1364；移除普通标量列的旧默认值应使用 `SET DEFAULT NULL`。`IS_NULLABLE=YES` 和 `COLUMN_DEFAULT=NULL` 不足以证明可省略字段，需要通过 `SELECT DEFAULT(列) FROM 表 LIMIT 0` 验证。TEXT/BLOB 的缺失默认标志应保留完整列定义执行 `MODIFY COLUMN` 修复，不能对它们使用 `ALTER COLUMN SET DEFAULT NULL`。

1. 先通过 `microi_get_db_schema` 读取目标租户的真实表、字段和菜单。
2. 从当前源码
   `Microi.Client/src/views/form-engine/diy-field-component/diy-component-list.json`
   核对控件名；官网页面可能含历史控件。
3. 新表用 `microi_create_table`；字段用 `microi_add_field`，不得直接写
   `diy_field` 或执行临时 DDL。
4. 选项控件同时设置 `data/config`；关联控件明确保存字段和显示字段。
5. 字段多时设置 `diy_table.Tabs` 与字段 `Tab`；只有整行控件设置
   `FormWidth=24`，普通字段省略。`CollapseGroup` 属于整行控件，必须显式保存
   `FormWidth=24`，且 `Config.CollapseGroup.ShowFieldCount` 默认补为 `true`。
6. 绑定菜单后补齐/允许平台推断列表列、搜索列、隐藏列、排序列、移动端列和默认排序。
7. 回读 `diy_field`、刷新 schema 缓存，再在真实新增/编辑/查看表单中验收。

## 原生查询主库策略

- 对不能接受副本延迟的权限关系等表，显式配置 `diy_table.ReadPrimary=1`；普通表保持
  `NULL/0`。只能从可信表元数据选择连接，禁止让查询请求的 `ReadPrimary`、`TableModel`
  或 `DataBaseId` 控制主库/副本。此配置不扩大菜单、表、记录或字段权限。
- 原生 Get、List、Count/CountBatch、SUM 批量与回退、Tree 子计数和 Export 必须共用
  目标表所属数据库的 writer 选择；扩展库不得误回租户基础库。主库不可用或配置非法必须
  失败，不能以副本或0计数伪装成功。直接调用 `V8.DbRead` 的语义不变。
- V8 的 JavaScript Number 经 .NET 互操作可能成为浮点类型；配置解析接受数值精确等于
  `0/1` 的表示，仍拒绝小数、NaN、Infinity、布尔值及其它非法值，禁止通过截断或舍入放宽值域。
- 显式 `DbTrans` 保留，不为主库策略跳出事务；父事务旧 RR 快照仍可能看见旧授权关系，
  不能声称仅开启本字段就实现与撤权原子串行。需要实时授权时采用独立受控主库身份读取。
- 标准 `microi_create_table`、`microi_update_table` 和 Manifest `tables[].readPrimary`
  支持 `null/0/1`，省略保持已有配置，显式 `null` 清回缺省。先升级后端并安装正式表单
  引擎字段，再配置；不通过定制 `.NET` 启动迁移或原始 SQL 补字段。
- 元数据冷回源使用主库，写后清表缓存并回读。验收分别证明字段定义、可空物理列、配置值、
  普通身份真实 Rows/Count/SUM/Tree/Export 与撤权边界；离线会话选择测试不能冒充复制延迟
  或七种数据库实机测试。所有新普通表字段保持可空，不把已有表批量设为主库。

## 上传字段配置（AI 生成时强制）

- 需要逐附件角色权限时，在 `Config.FileUpload` 设置 `EnableRolePermission:true` 和 `Limit:true`；
  `HideUnauthorizedFiles`（隐藏无权行）、`ShowUnauthorizedFileName`（显示无权名称）、
  `DisableRoleInheritance`（关闭角色继承）均为 boolean，默认 false。未启用的旧字段保持兼容。
- 每个文件的 `VisibleRoleIds` 是真实角色 Id 数组；空数组跟随表单权限。标签名称及 `_FileAccess`
  由后端投影，不能作为客户端授权事实。默认真实持有角色的 Level 严格更高即可继承，
  同级不同角色不继承；多选任一角色命中即可，仍须有菜单/表单/记录访问与编辑权限。
- `Config.FileUpload.ConfigurableRoleIds` 是“可配置角色列表”，使用真实角色 Id 数组，默认 `[]` 表示全部。
  非空时，角色目录和后端新增附件授权都只允许该范围；缩小范围保留旧附件已有角色，可移除但不可再添加。
  此范围只限制可选授权角色，不改变 Level 继承规则。先用 `microi_list_roles` 读取当前租户角色，禁止复制其它租户的 Id。
- 无权限附件默认只显示锁定占位、不显示名称；开启隐藏只改变展示，保存时服务端保留原附件，
  篡改无权元数据、复制他人路径及删除含无权附件的整条记录均应失败。条件批量写附件/删除应改为逐条。
- 新附件必须通过当前记录/字段的私有上传取得短期 `_UploadProof`，保存后由服务端移除。
  不能手工拼接他人路径；历史公有文件设置角色前重新私有上传。先部署前后端，再启用字段配置。
- MCP 可复用 `microi_get_field_list`、`microi_update_field`、`microi_refresh_schema_cache`、
  `microi_list_roles` 和 `microi_save_role`，先完整回读并合并 Config，禁止覆盖原上传大小、数量和 V8。
  新版 `microi_build_field_config` 支持 `sourceType:"FileUpload"`，自动强制私有存储并校验布尔值；
  旧 MCP 直接传同等 Config JSON 即可，无需另建业务接口。回归必须使用高/中/低角色和直接 HTTP 篡改，
  同时覆盖名称开关、隐藏开关、继承开关、单文件、列表、私有/版本预览和保存后重开。

- 图片使用 `Config.ImgUpload`，至少明确 `Limit`、`Multiple`、`MaxCount`、`Preview`、
  `MaxSize` 和 `Crop`；文件使用 `Config.FileUpload`，至少明确 `Limit`、`Multiple`、
  `MaxCount`、`MaxSize`。完整键和值域读取 `references/component-catalog.md`。
- `ImgUpload.Preview` 未配置时默认开启压缩；普通业务图片优先保持开启。裁剪前的原图无论
  展示图配置为公有或私有、是否压缩，都必须保存在 HDFS 私有桶，业务字段不得保存原图路径。
- `ImgUpload.Crop.Enabled` 只控制表单打开时是否默认选中裁剪。运行时的裁剪开关、存储范围、
  单/多图数量、压缩状态和最大体积统一显示在紧凑上传面板中；不得把 `Enabled=false` 误解为
  禁止用户裁剪。`Mode` 只允许 `free/fixed/select`。
- 图片和文件的拖放区都使用同一紧凑配置面板。`Multiple=true` 时必须设置业务合理的
  `MaxCount`；`MaxSize` 单位为 MB，字段限制只能收紧租户/平台上限，不能放大。
- 富文本使用 `Config.RichText`：必须明确 `Limit`，并按需配置
  `Image.Enabled/MaxSize/MaxCount/Preview/CompressMaxSize/CompressMaxWidth`、
  `Video.Enabled/MaxSize/MaxCount`、`File.Enabled/MaxSize/MaxCount/Accept`。公开公告、商品详情
  等匿名正文显式使用 `Limit=false`；内部正文使用 `true`。旧字段缺失配置时安全默认为私有桶，
  图片默认压缩到约 500 KB、最长边 1920 px。
- 私有富文本只能持久化稳定对象标识，禁止把 HDFS 签名 URL、审计代理 Ticket 或 DiyToken 写入
  HTML。每次查看/编辑按当前菜单、表、记录和字段权限换取新短效地址；外部匿名页面没有此权限
  上下文，因此不能把私有 RichText 当作公开正文。普通用户通过当前表单新增/编辑授权后，后端
  必须回读 `diy_field.Config.RichText.Limit` 决定公私桶；只改请求 `Limit=false` 不能绕过字段配置。
- 图片、文件和富文本上传必须携带 `FormEngineKey + FieldId + SysMenuId`，编辑记录再带
  `FormDataId`，TableChild 再带父子授权上下文。后端不得按用户等级统一覆盖字段的“禁止匿名访问”；
  无法回查字段与动作权限的普通上传才安全降级为私有桶。

新建表/模块时，除非用户显式指定或表单达到极重阈值（约 36+ 业务字段、2+ 子表或同等
重型控件密度），默认保存 `diy_table.FormOpenType=Dialog` 与 `FormOpenWidth=80%`。
Drawer 只服务超长复杂表单，不能作为所有 CRUD 模块的模板默认值。
   若设计器显示而运行态不显示，先检查 `InFormV8`/字段 V8 是否调用
   `V8.FieldSet(..., 'Visible', false)`、`hideField` 或传入 `HideFields`，再判断前端源码。

## 表单 Banner（所有新业务表必做）

标准表单 Banner 默认显示，以当前主题色约 50% 混合强度叠加深蓝灰渐变，并适配浅色、
深色与移动端。视觉应有层次但保持清爽，标题始终维持安全对比度；统计卡片使用半透明背景
和柔和阴影分层，避免堆叠边框。它属于表单语义，配置
必须写入 `diy_table` 的 `FormBannerEnabled`、`FormBannerTitleField`、
`FormBannerSubtitleField`、`FormBannerImageField`、`FormBannerIcon`、
`FormBannerBackgroundField`、`FormBannerTagFields`、`FormBannerMetrics`，禁止写进
`sys_menu`、`DiyConfig` 或项目定制组件。

- 标题优先业务自动编号/单号/编码，再选名称或标题；副标题优先客户、项目、公司、分类、
  日期等可读字段。
- 左侧图片使用 `ImgUpload`。单图、多图取首图，继续遵循吾码公有/私有文件路径与授权
  规则；图片为空时必须有语义合适的 Font Awesome 图标回退。
- 右侧标签优先 `Select/Radio/Switch/Checkbox/SelectTree/Department` 等选项字段，最多
  选择 3 个有业务意义的状态、类型或等级。显式 `[]` 表示不要自动标签。
- 自动统计最多 3 项，只选择真实金额、合计、数量、成本、余额、评分、比率、进度等具有
  明确业务口径的数值字段；必须排除 Id、排序、启用、状态、版本、分页和本页加载量。
  存在 `TableChild` 时，默认统计必须携带完整父表/父字段/父记录授权上下文，在服务端对全部
  关联子表数据计算行数或业务数值合计，不能只统计当前页。跨表自定义口径使用 `ApiEngineKey +
  ValuePath + ParamMap + RefreshSeconds`，相同接口批量返回，禁止 N+1、随机数和固定演示数；
  没有可靠指标时隐藏统计区。显式 `[]` 表示不要自动统计。
- 兼容旧模块 Hero 时仅迁移视觉、`Source=Field` 或显式记录作用域指标；列表总数、分类数量和
  未引用当前 `Form/RecordId` 的全局接口统计不得进入单记录 Banner，缺省时回到当前记录和
  授权 `TableChild` 的语义统计。
- 未配置的存量表由运行时按字段类型智能推断，不能因为物理字段为空而隐藏或展示空壳。
  只有 `FormBannerEnabled=0` 才隐藏。
- 字段绑定须区分省略/`NULL` 与显式空字符串：`titleField/subtitleField/imageField/backgroundField`
  省略或 `NULL` 表示未选择，允许兼容推断；显式 `""` 表示不绑定该字段，不能被推断值或旧别名
  覆盖。取消标题/图片字段后仍保留表单名称/默认图标回退。完整验收须同时使用包含该语义的 MCP
  与平台前端，逐项回读空字符串并验证实际表单；不能只看到写入 `Code=1` 就宣称生效。
- 完整系统 Manifest 使用 `tables[].formBanner`；未提供时 `microi_generate_system` 仍须写入
  类型感知的默认值。逐步创建字段后调用 `microi_configure_form_banner` 并回读验证。
- `microi_generate_system` 的最终验收与 `microi_validate_system` 必须逐表回读 Banner 语义字段；
  写入返回成功、结构验收 `Code=1` 都不能代替已持久化配置，`Data.Passed=false` 仍视为失败。
  旧库缺少 Banner 物理列时先完成平台正式升级，再配置和重新验收，不直接执行 SQL 补列。
- 表单设计器验收必须覆盖有/无图片、有/无统计、子表完整聚合、接口失败回退、浅色、深色、
  PC 和窄屏，并检查文字对比度以及不存在技术字段伪统计。

## 物理类型底线

MCP 建模只使用：

- `varchar(N)`
- `mediumtext` / `longtext`
- `int` / `bigint`
- `decimal(18,N)`

日期时间用 `varchar(25)` 保存 `yyyy-MM-dd HH:mm:ss`，组件用 `DateTime`；
开关用 `int`。不得生成 `datetime/date/timestamp/float/double/boolean/bool/string/text/nvarchar`。
前端设计器 JSON 中的历史默认类型不能覆盖服务器建模规则。

## 选项字段

`Select`、`MultipleSelect`、`Radio`、`Checkbox` 没有数据源时会显示空选项：

```text
1|启用,0|禁用
```

推荐保存稳定 Key、显示可翻译 Label。修改 `Data/Config/KeyValue` 后必须
`microi_get_field_list` 回读，并执行 `microi_refresh_schema_cache`。

## `JoinForm` 与 `TableChild` 硬性判定

这两个控件都能在表单内显示另一张表，但数据关系和运行组件完全不同，生成表/字段前
必须先确定基数，不得因为名称里出现“关联”就默认使用 `JoinForm`。

| 判断项 | `JoinForm`（关联表单） | `TableChild`（子表） |
|---|---|---|
| 关系 | 当前记录关联**一个**独立目标记录，通常为 N:1 或 1:1 | 一条主表记录拥有 0..N 条明细，标准 1:N |
| 关系存储 | 主表字段保存目标记录 `Id` | **子表物理外键**保存主表 `Id`/指定主键值 |
| 界面 | 嵌入一张 `diy-form`，只展示/编辑一条目标记录 | 嵌入一张 `diy-table`，提供明细列表、分页及行级增删改 |
| 核心配置 | `Config.JoinForm.{TableId,TableName,JoinFieldName,FormMode,Id,_SearchEqual}` | `Config` 根节点的子表/菜单/外键 Id，加 `Config.TableChild` 运行选项 |
| 目标限制 | 目标表必须与当前表不同；相同则组件拒绝渲染 | 子表应是独立明细表，并通过外键限定到当前父记录 |

### 决策规则（强制）

- 需求出现“子表、明细、清单、条目、行项目、多个、若干条、记录列表”，且没有明确说明
  “只关联一条已有记录”时，默认建模为 `TableChild`。
- 只要一条父记录可能有 0..N 条目标记录，或需要在父表单内列表、分页、新增、编辑、删除
  多行，就必须用 `TableChild`。
- 只有主表保存一个目标记录 Id、并需要把该独立记录的完整表单嵌入当前表单时，才用
  `JoinForm`。选择一条记录但无需嵌入完整表单时，优先 `OpenTable`/`Select`。
- 语义仍不明确时必须在任何 MCP 写入前询问基数；禁止静默退化为 `JoinForm`。
- 禁止把“明细”设计为主表 `XxxId + JoinForm`；禁止让 `JoinForm.TableId/TableName`
  指向当前表；禁止把 1:N 外键放在主表。
- 完整系统 Manifest 中，`JoinForm` / `TableChild` 字段必须声明 `relation.cardinality`；
  `microi_plan_system` 与 `microi_generate_system` 会在任何写入前执行本节门禁。直接调用
  `microi_add_field` / `microi_update_field` 时，后端仍会校验目标表、主/子外键、隐藏菜单
  和子表索引，不能靠绕过 Manifest 写入未初始化配置。

示例：

- “订单包含多个商品明细” → `order_detail.OrderId` + `TableChild`。
- “访客单包含多件携带物品” → `fk_carry_item.VisitId` + `TableChild`，不能用
  `GuestId + JoinForm`，也不能把 `JoinForm` 指回 `fk_carry_item` 自己。
- “工单关联一个客户，并在工单内展开客户档案” → 主表 `CustomerId` + `JoinForm`。

### MCP 创建 `TableChild` 的两阶段流程

1. 创建主表和独立子表；在子表创建真实外键（如 `VisitId varchar(50)`）。
2. 在子表为回查创建与物理隔离方式一致的索引：表有 `OsClient` 时通常为
   `(OsClient, VisitId)`，独立租户表没有该列时为 `(VisitId)`。索引写入 Manifest
   `tables[].indexes`，并以 `microi_get_table_indexes` 回读；物理字段回读失败时停止配置。
3. 为子表创建绑定其 `diyTableId` 的隐藏 CRUD 菜单：`Display=0`、`AppDisplay=0`、
   `HasChild=0`。
4. 在完整系统 Manifest 的主表字段声明：

   ```json
   {
     "name": "Items",
     "label": "明细",
     "component": "TableChild",
     "formWidth": 24,
     "relation": {
       "cardinality": "1:N",
       "targetTable": "Biz_OrderItem",
       "childForeignKey": "OrderId",
       "childModule": "订单明细（隐藏）",
       "primaryTableFieldName": "Id"
     }
   }
   ```

   `microi_generate_system` 会先创建全部表与普通字段，再创建隐藏菜单，最后回读并写入
   当前租户真实的 `diy_table.Id` / `sys_menu.Id`。禁止在 Manifest 中编造这些 Id，禁止
   因依赖尚未创建而退化成 `JoinForm`。
5. `TableChild` 控件字段通常只是表单配置位，关系事实存放在子表外键。至少保存：

```json
{
  "TableChildTableId": "<子表 diy_table.Id>",
  "TableChildSysMenuId": "<子表 sys_menu.Id>",
  "TableChildSysMenuName": "携带物品明细",
  "TableChildFkFieldName": "VisitId",
  "TableChild": {
    "PrimaryTableFieldName": "Id",
    "Data": [],
    "SearchAppend": {},
    "ImportAutoFillFk": true,
    "FieldRelations": [],
    "LastTableId": "",
    "LastSysMenuId": "",
    "LastSysMenuName": "",
    "DisablePagination": false,
    "NoneDefaultHeight": false
  }
}
```

`FieldRelations` 使用紧凑格式 `[["父表字段","子表字段",true?], ...]`。全部关系用于新增回写和导入回填；第三位 `true` 仅标记参与导入反查父表的关系。后端兼容旧三项配置，新版前端会合并去重并在字段下次保存时清除旧键。

- `TableChild` 的父关联键为空、`null` 或仅空白时必须失败关闭：不得发起子表列表请求，服务端
  收到空关联键也必须返回空集合，不能把空筛选忽略后退化成全表查询。旧客户端无法在请求层保证时，
  应让控件在关联键有效前保持不挂载；`DisablePagination` 必须为 `false`，只作为第二道限流保护。
- `InFormV8`、字段进入事件和抽屉打开回调不得为预生成的父 `Id` 持久化默认子记录；用户取消
  新增不会提交父表，会留下无法回滚的孤儿数据。默认行应保留在前端状态，或在父表保存成功后通过
  幂等的后端事件创建；不需要真实明细时保持 0 行。

`OpenTable` 用于弹出列表选择数据，固定授权范围用 `V8.OpenTableSetWhere`；`JoinTable`
用于展示关联集合，不能用前端拼接代替数据权限。

### 子表验收与复盘

- 回读主表字段、子表字段、隐藏子菜单和索引，确认配置中的表 Id、菜单 Id、外键名均真实存在。
- 用父记录 A 新增/编辑/删除多条子记录；打开父记录 B，确认 A 的数据不可见且不可越权操作。
- 新增主表尚无真实 Id 时，不得产生孤儿子记录；保存后重新打开仍能正确回显。
- 若曾误选组件，复盘必须记录：触发用语、误判基数、正确关系、应增加的生成前断言；通用结论
  回写本节，不能只修一张业务表。

## 自定义组件边界

优先使用现有 44 类标准控件。只有标准控件无法表达交互、且该交互会长期复用时，
才使用 `DevComponent`：

- 多租户共用且与主框架强耦合的 Vue 组件，路径必须稳定并纳入 `Microi.Client` 源码/构建。
- 支持 Add/Edit/View、只读、必填、清空、校验、移动端和暗色主题。
- 不在组件内绕过 FormEngine 权限直接访问任意表。
- 复杂但租户独有、需要固定嵌入表单的区域，优先使用 `DevComponent` + MicroService 路由；临时打开的复杂页面使用 `V8.OpenAppDialog`，都避免把客户逻辑打进主前端。
- MicroService 表单嵌入使用 `microi.routes.json` 页面级 `LegacyComponentPaths` 作为稳定别名，字段 `Config.DevComponentPath` 与其匹配。主前端存在同路径 Vue 文件时本地优先；不存在时平台自动加载对应 `sys_microiservice_page.RoutePath`。新别名不得与 `/src/views` 真实文件冲突。
- 组件宿主下发 `componentMode=true`、可序列化 `componentData` 与 `permissionContext`；子应用用 `dev-component:resize` 同步高度，用 `dev-component:event` 回传 `update:modelValue`、`CallbackFormValueChange`、`FormSet` 或 `ParentFormSet`。不传 Vue 实例、函数、循环引用或 `ParentV8`，不直接操作父页面 DOM。
- 表单嵌入验收必须覆盖 Add/Edit/View/只读、初始值与回写、自动高度、窄屏、暗色主题，以及当前菜单 `ModuleEngineKey` 下的有权/无权账号。
- `DevComponent` 配置了非空字段标题时必须正常渲染 Label；只有标题本身为空时才允许隐藏，
  不能按组件类型全局吞掉业务标题。`el-form--label-top` 下的字段级 `Button` 仍保留与其它
  控件等高的不可见 Label 占位，使按钮对齐控件区而不是对齐标题行。
- 字段显式配置 `FormLabelPosition=left/right/top` 时优先于子表、代码编辑器等特殊组件的默认
  `top` 布局；移动端仍可统一回落到 `top`。验收时必须在真实设计器保存后回读该字段配置。
- 同一标签行同时显示 `Label` 与 `Description` 时，业务 Label 不允许收缩或省略；说明文字使用
  剩余宽度并以省略号截断，完整说明通过 Element Plus tooltip 提供。自定义组件内部不得再次
  输出与宿主字段相同的标题；需要补充的是说明或安全提示。
- 权限树勾选子菜单时必须补齐全部祖先菜单的 `Read` 权限，祖先不得被动继承子菜单的增删改查；
  这样既保证路由可见，又避免扩大业务操作权限。

## 固定审计字段

- 核心协议迁移也必须遵守普通物理列允许 NULL。流式发布的 SaaS 开关、协议版本、栅栏、门禁代次和审计字段不能在旧迁移补跑时重新设为 NOT NULL；默认值、状态机、身份校验和完整审计由可信程序保证，只有主键 Id 保持数据库非空约束。

- `Id`、`CreateTime`、`UpdateTime`、`UserId`、`UserName`、`IsDeleted` 是 DIY 表的正常固定字段。物理列存在时必须有对应 `diy_field` 元数据，不能长期出现在“异常字段修复”列表；`diy_table.DisplayDefaultField` 只控制设计器默认是否显示这些字段，不等于删除元数据。
- 统一通过平台修复接口或 MCP `microi_repair_audit_fields` 补齐/恢复元数据。修复必须按 `OsClient` 使用共享租约锁，可重复执行，只处理已存在的固定物理列，不借机执行 DDL，并在成功后清理字段缓存。
- 表格里的创建人、创建时间、修改时间等审计列应与普通字段共用列头高级搜索、权限和格式化逻辑。

## 验收

- 物理列与 `diy_field` 一致，字段缓存已刷新。
- 新增、编辑、查看、列表、搜索、导入/导出至少覆盖适用场景。
- 选项显示 Label、保存 Key，回显和筛选一致。
- 子表新增/编辑/删除与父表外键正确，不能跨父记录串数据。
- PC 与移动端字段顺序、Tabs、整行控件无截断。
- 前端校验只改善体验；绕过前端直接 HTTP 提交时后端事件仍能阻止非法数据。
