# 切片 4 — 扩展:按租户添加自定义字段 xly 卖给许多客户。每个客户都想记录框架原本不知道的一些东西:自定义编码、内部备注、分类标签。受支持的做法是向一个*定制*表插入行,并在运行时把该表内容合并到基础表单之上;这样可以**不 fork 代码库,也不修改共享元数据**。 本切片追踪该机制。这是首次覆盖 xly 的*每租户覆盖*模型,即单代码库 / 多客户 SaaS 得以运作的定制模型。 ## 三层定制 xly 在每个基础表单之上叠加**三张**定制表。每张表作用域不同,回答的问题也不同。 | 表 | 作用域 | 覆盖层 | dev 行数 | |---|---|---|---| | [`gdsconfigformpersonalize`](../auto-catalog/tables/gdsconfigformpersonalize.md) | 每租户(`sBrandsId` + `sSubsidiaryId`) | **form-master**,覆盖整个表单的 `sSqlStr` / `sWhere` / `sOrder` | 18 | | [`gdsconfigformcustomslave`](../auto-catalog/tables/gdsconfigformcustomslave.md) | 每租户 | **form-slave**,给表单新增、隐藏或替换*单个字段* | 0 | | [`gdsconfigformuserslave`](../auto-catalog/tables/gdsconfigformuserslave.md) | 每用户 | **form-slave**,每用户字段微调(列顺序、隐藏列) | 9 | 基础表单是 `gdsconfigformmaster` + `gdsconfigformslave`,即*系统默认*。上面三张表是运行时应用并合并的*覆盖层*。 本切片聚焦中间行:`gdsconfigformcustomslave`,也就是“给租户 X 添加自定义字段”的规范通道。 ## 示例会是什么样 假设租户“山东星海印务”想在客户列表表单上增加“客户内部编码”字段。客户或实施人员不修改 `gdsconfigformslave`,而是: 1. 打开**后台**中编辑 `gdsconfigformcustomslave` 行的模块(某个系统管理页面,可能是 `界面显示内容配置`;需在**后台**中点击验证)。 2. 新增一行: - `sParentId` = 表单 `sId`(与基础 slave 指向同一个表单)。 - `sName = 'sInternalCode'`(字段列名)。 - `sChinese = '客户内部编码'`。 - `sControlName = '文本框'`。 - `sBrandsId` + `sSubsidiaryId` = 该租户 ID。 3. 可选地给底层物理表添加一列(手工 schema 迁移;框架不会自动 `ALTER TABLE`)。否则该字段只存在于表单上,保存时没有可绑定目标。 下次该租户的任意用户加载表单时,会看到额外列。其他租户仍看到未修改的基础表单。 > **dev 中为空的重要说明。** `gdsconfigformcustomslave` 在 `xlyweberp_saas_ai` 中有 **0 行**。表已接入框架,但当前 dev DB 没有租户使用它。下面的追踪来自代码推导;实时观察仍待补。 ## 运行时如何合并 前端不会看到三张表,只会看到一个合并表单。框架通过两个数据库**视图**读取基础 slave 和覆盖 slave 与 form-master 的连接: - [`gdsconfigformslavemasterview`](../auto-catalog/views/gdsconfigformslavemasterview.md):连接 `gdsconfigformmaster` 与基础 `gdsconfigformslave`,总会加载。 - [`gdsconfigformcustomslavemasterview`](../auto-catalog/views/gdsconfigformcustomslavemasterview.md):连接 `gdsconfigformmaster` 与自定义 `gdsconfigformcustomslave`,仅对有覆盖行的租户加载。 服务层读取两者,按 `sName` 合并,并向 SPA 返回单一 slave 列表。如果租户的 `gdsconfigformcustomslave` 行与基础 slave 拥有相同 `sName`,则覆盖;新的 `sName` 会追加。隐藏 / 移除情况通过 `bVisible` 处理。 `gdsconfigformpersonalize` 的表单级覆盖以类似方式在 *form-master* 层合并:请求时运行时调用 `configformpersonalizeService.getConfigformpersonalize(sBrandsId, sSubsidiaryId, gdsconfigformmasterId)`;如果返回行,就用其 `sConfigSqlStr` / `sConfigWhere` / `sConfigOrder` 替换基础表单的 `sSqlStr` / `sWhere` / `sOrder`。 合并顺序如下: ```text gdsconfigformmaster (系统默认表单) ↓ 覆盖 gdsconfigformpersonalize (每租户整表单覆盖) ↓ 然后是基础从表 gdsconfigformslave (系统默认字段) ↓ 覆盖 / 扩展 gdsconfigformcustomslave (每租户字段) ↓ 可选继续微调 gdsconfigformuserslave (每用户视图偏好) ``` 这是“无 FK、语义 FK 现实”在框架自身配置上的应用:没有外键保证 `gdsconfigformcustomslave.sParentId` 真存在于 `gdsconfigformmaster.sId`。孤儿行可能存在,并在合并时被静默忽略。维护人员应增加审计脚本标记此类孤儿。 ## 为什么不改代码也能工作 最终客户不用向工程师申请新列。他们打开**后台**构建器,增加一行,字段只在自己的**前台**中出现,其他租户不受影响。这个单代码库属性来自 xly 的数据驱动论点;代价是每次请求合并元数据的运行时成本,以及三张多数表单不会使用的定制表带来的 schema 膨胀。 ## 本切片引入的概念 - *定制层级*:上面的三层覆盖模型、合并方式以及各层适用时间。 - *无 schema 扩展限制*:框架会渲染额外字段,但不会也不能 `ALTER TABLE`。基础表没有的自定义字段需要协调的手工 schema 迁移、宽表预留列,或 JSON 列模式。切片 5 覆盖需要结构变化时的每客户 SQL 覆盖通道。 ## 本切片使用的参考 - [配置人员:如何定义表单](../reference/builder/define-form.md):覆盖路径与基础配方相同,只是表不同。 - [维护人员:运行时](../reference/maintainer/runtime.md):三层合并发生在 `BusinessBaseServiceImpl` 和 `BusinessGdsconfigformsServiceImpl` 中。 ## 待验证项 1. 找到实际编辑 `gdsconfigformcustomslave` 的 **后台** 页面。最可能是侧边栏中的 `界面显示内容配置`。 2. **追踪合并代码。** 确认合并是在 MyBatis(两个视图)中完成,还是在 Java(`getFormSlaveData` + `getFormCustomSlaveData`)中完成。 3. **`bVisible = false` 语义。** 它是隐藏已有基础字段,还是只抑制覆盖行本身?很可能是前者,但需要确认。 4. **真实示例。** 在生产中找一个租户的实际 `gdsconfigformcustomslave` 行作为贯穿示例。