切片 4 — 扩展:按租户添加自定义字段
xly 卖给许多客户。每个客户都想记录框架原本不知道的一些东西:自定义编码、内部备注、分类标签。受支持的做法是向一个定制表插入行,并在运行时把该表内容合并到基础表单之上;这样可以不 fork 代码库,也不修改共享元数据。
本切片追踪该机制。这是首次覆盖 xly 的每租户覆盖模型,即单代码库 / 多客户 SaaS 得以运作的定制模型。
三层定制
xly 在每个基础表单之上叠加三张定制表。每张表作用域不同,回答的问题也不同。
| 表 | 作用域 | 覆盖层 | dev 行数 |
|---|---|---|---|
gdsconfigformpersonalize |
每租户(sBrandsId + sSubsidiaryId) |
form-master,覆盖整个表单的 sSqlStr / sWhere / sOrder
|
18 |
gdsconfigformcustomslave |
每租户 | form-slave,给表单新增、隐藏或替换单个字段 | 0 |
gdsconfigformuserslave |
每用户 | form-slave,每用户字段微调(列顺序、隐藏列) | 9 |
基础表单是 gdsconfigformmaster + gdsconfigformslave,即系统默认。上面三张表是运行时应用并合并的覆盖层。
本切片聚焦中间行:gdsconfigformcustomslave,也就是“给租户 X 添加自定义字段”的规范通道。
示例会是什么样
假设租户“山东星海印务”想在客户列表表单上增加“客户内部编码”字段。客户或实施人员不修改 gdsconfigformslave,而是:
- 打开 BACK 中的
界面显示内容配置(gdsmodule.sId=11,/jmnrpz)。其第三个面板通过sId=19211681019715596285250620的 form-master 写入gdsconfigformcustomslave,已在线验证。各 panel 映射见下面“待验证项”第 1 项。 - 新增一行:
-
sParentId= 表单sId(与基础 slave 指向同一个表单)。 -
sName = 'sInternalCode'(字段列名)。 -
sChinese = '客户内部编码'。 -
sControlName = '文本框'。 -
sBrandsId+sSubsidiaryId= 该租户 ID。
-
- 可选地给底层物理表添加一列(手工 schema 迁移;框架不会自动
ALTER TABLE)。否则该字段只存在于表单上,保存时没有可绑定目标。
下次该租户的任意用户加载表单时,会看到额外列。其他租户仍看到未修改的基础表单。
已用 dev DB 确认。
gdsconfigformcustomslave在xlyweberp_saas_ai中当前为空(0 行)。表已接入框架,但当前 dev DB 没有租户注册字段级覆盖。下面的追踪来自代码推导;端到端观察需要一个真实填充该表的租户部署。
运行时如何合并
前端不会看到三张表,只会看到一个合并表单。框架通过两个数据库视图读取基础 slave 和覆盖 slave 与 form-master 的连接:
-
gdsconfigformslavemasterview:连接gdsconfigformmaster与基础gdsconfigformslave,总会加载。 -
gdsconfigformcustomslavemasterview:连接gdsconfigformmaster与自定义gdsconfigformcustomslave,仅对有覆盖行的租户加载。
服务层读取两者,按 sName 合并,并向 SPA 返回单一 slave 列表。如果租户的 gdsconfigformcustomslave 行与基础 slave 拥有相同 sName,则覆盖;新的 sName 会追加。隐藏 / 移除情况通过 bVisible 处理。
gdsconfigformpersonalize 的表单级覆盖以类似方式在 form-master 层合并:请求时运行时调用 configformpersonalizeService.getConfigformpersonalize(sBrandsId, sSubsidiaryId, gdsconfigformmasterId);如果返回行,就用其 sConfigSqlStr / sConfigWhere / sConfigOrder 替换基础表单的 sSqlStr / sWhere / sOrder。
合并顺序如下:
gdsconfigformmaster (系统默认表单)
↓ 覆盖
gdsconfigformpersonalize (每租户整表单覆盖)
↓ 然后是基础从表
gdsconfigformslave (系统默认字段)
↓ 覆盖 / 扩展
gdsconfigformcustomslave (每租户字段)
↓ 可选继续微调
gdsconfigformuserslave (每用户视图偏好)
这是“无 FK、语义 FK 现实”在框架自身配置上的应用:没有外键保证 gdsconfigformcustomslave.sParentId 真存在于 gdsconfigformmaster.sId。孤儿行可能存在,并在合并时被静默忽略。维护人员应增加审计脚本标记此类孤儿。
为什么不改代码也能工作
最终客户不用向工程师申请新列。他们打开后台构建器,增加一行,字段只在自己的前台中出现,其他租户不受影响。这个单代码库属性来自 xly 的数据驱动论点;代价是每次请求合并元数据的运行时成本,以及三张多数表单不会使用的定制表带来的 schema 膨胀。
本切片引入的概念
- 定制层级:上面的三层覆盖模型、合并方式以及各层适用时间。
-
无 schema 扩展限制:框架会渲染额外字段,但不会也不能
ALTER TABLE。基础表没有的自定义字段需要协调的手工 schema 迁移、宽表预留列,或 JSON 列模式。切片 5 覆盖需要结构变化时的每客户 SQL 覆盖通道。
本切片使用的参考
- 配置人员:如何定义表单:覆盖路径与基础配方相同,只是表不同。
-
维护人员:运行时:三层合并发生在
BusinessBaseServiceImpl和BusinessGdsconfigformsServiceImpl中。
待验证项
-
找到实际编辑已关闭:确认为gdsconfigformcustomslave的 BACK 页面。界面显示内容配置(gdsmodule.sId=11,URL/jmnrpz)。该页在一个 screen 中渲染三个 form-master panel,分别对应表单定义栈的三层:
| Panel | gdsconfigformmaster.sId | 写入的 sTbName |
|---|---|---|
| Form-master 编辑器 | 19211681019715574673782610 | gdsconfigformmaster |
| 基础 slave 编辑器 | 19211681019715596207594120 | gdsconfigformslave |
| 每租户覆盖 | 19211681019715596285250620 | gdsconfigformcustomslave |
第三个 panel 就是“给租户 X 添加自定义字段”的规范通道。在线验证:在 BACK(admin/123)点击 界面显示内容配置 会触发 POST /xlyEntry/business/getBusinessDataByFormcustomId/19211681019715596285250620?sModelsId=11 加载现有 customslave 行;后续新增 / 修改操作的 addUpdateDelBusinessData POST 也会落到同一个 form-master,运行时按标准通用保存路径解析为对 gdsconfigformcustomslave 的写入。
-
追踪合并代码。已关闭:合并发生在BusinessBaseServiceImpl.java:246-248的 Java 中,先调用businessGdsconfigformsService.getFormSlaveData(map),再调用getFormCustomSlaveData(map),并把两者addAll到同一个slaveList。两个视图(gdsconfigformslavemasterview、gdsconfigformcustomslavemasterview)提供 master-with-slave 的读取形状;合并在 Java,master-with-slave join 在 SQL。 -
已关闭:两者都会发生,但在不同层。bVisible = false语义:隐藏基础字段,还是只抑制覆盖行?BusinessGdsconfigformsServiceImpl.java:413-433中,当gdsconfigformcustomslave行按sControlName或sName匹配到基础gdsconfigformslave行时,customslave 行会完整替换基础行(sList.removeAll(_cstlist); sList.addAll(_cList);),所以 customslave 上的bVisible=false会按租户隐藏基础字段。用户级覆盖(gdsconfigformuserslave,446-468 行)随后叠加:只有用户行的bVisible为 true 且合并后行的bVisible也为 true 时,用户的iFitWidth/iOrder才生效;否则 464 行会显式cmap.put("bVisible", false),仅对该用户隐藏。因此bVisible=false在两层都能隐藏字段,只是作用域不同(每租户 vs 每用户)。Item 4 — 暂缓(需要填充过的租户部署)。 已对 dev DB 实证确认:
SELECT COUNT(*) FROM gdsconfigformcustomslave返回 0。该 DB 中没有租户注册每租户字段覆盖,因此无法从这里抽取真实 worked example。该项仍是真实缺口,等待有覆盖行的生产租户 DB。 真实示例。 在生产中找一个租户的实际
gdsconfigformcustomslave行作为贯穿示例。(dev DB 已确认为空,需要有覆盖行的租户部署。)