# 切片 5 — 扩展:每客户 SQL 覆盖 元数据驱动框架能表达的内容是有限的。 客户可能需要销售对账报表使用不同聚合规则,需要一个独特的自定义视图支撑成本看板,或需要一个行为从根本上偏离标准的存储过程。覆盖表([切片 4](04-custom-field.md))可以加字段和覆盖 SQL 片段,但不能替换存储过程的*逻辑*;过程是代码,不是数据。 到达这个边界时,xly 的逃生口是:**把手写 SQL 文件提交到 `script/客户//`,并直接应用到该客户 schema**。这完全绕过元数据层。它是生产中真实使用的通道,适用于若干客户;Wiki 必须诚实记录其范围、成本、适用时机和运维纪律。 ## 记录对象 | | | |---|---| | **客户** | 重庆展印 | | **覆盖文件** | `script/客户/重庆展印/Sp_SalSalesCheck.sql`(723 行) | | **替换内容** | 标准 `Sp_SalSalesCheck` 存储过程(销售对账列表) | | **配套文件** | `script/客户/重庆展印/viw_salsaleschecking_pro.sql`(该过程读取的自定义视图) | | **部署** | **手工**,由工程师 / DBA 对该客户 schema 应用 | 文件开头两行说明了这个通道的形状: ```sql DROP PROCEDURE IF EXISTS `Sp_SalSalesCheck`; delimiter ;; CREATE PROCEDURE `Sp_SalSalesCheck`(IN sLoginId varchar(100), ...) ``` 标准过程被 drop,自定义过程取而代之。`gdsmodule` 中的元数据仍按名称引用该过程(`sProcName = 'Sp_SalSalesCheck'`);运行时照常调用。差异完全存在于过程主体中,而主体现在是工程师编写的变体。 ## 当前代码库中有覆盖的客户 `script/客户/` 下有 18 个客户子目录: ```text 统兴 重庆展印 嘉诚 安徽金印 万昌 无锡中江 扬州浩宇 金九 亚明威 快马 金宣发 千彩 高旺 朝阳 上海亚峰 湛江新澳 福雅 远传 ``` 每个目录有一到十几个 `.sql` 文件。多数是自定义过程(`Sp_*`)和自定义视图(`viw_*`)。读取目录列表本身就是系统文档:这里的客户和功能,是框架无法表达、需要 bespoke 行为的地方。 ## 覆盖如何进入系统 没有自动通道。搜索 Java 代码中加载 `script/客户` 或遍历这些目录的部署服务,没有结果。机制是运维性的: 1. 工程师以标准过程为起点编写覆盖 `.sql` 并修改主体。 2. 文件提交到 `script/客户//`,用于可追溯性。 3. 使用 `mysql --defaults-file=... < the-file.sql` 或等价方式,**手工**对目标客户 MySQL schema 执行。 4. 从那以后,该客户 schema 中的过程主体就不同于其他客户;框架代码不变。 把文件放在代码库中很重要:工程团队可以一眼看出哪些客户偏离标准。但这不表示这些文件会作为任何 build 的一部分发布。 ## 为什么这不同于切片 4 切片 4 的 `gdsconfigformcustomslave` 和切片 1 的元数据 Add/Update 路径都留在框架内部。客户特定行为是叠加在共享代码库之上:每个租户运行时读取相同 Java、相同 MyBatis mapper、相同标准过程。 切片 5 的通道位于框架之下。客户 schema 中有一个同名但主体不同的存储过程。调用 `Sp_SalSalesCheck` 的 Java / MyBatis 代码不知道另一端是标准过程还是重庆展印变体。框架不知道,也无法分辨。 因此覆盖具有: - **强能力。** MySQL 存储过程 SQL 能写出的任何东西,都可替换标准行为。 - **运维脆弱。** 客户 schema 重建、恢复或迁移时,覆盖必须重新应用或保持存活。它不跟随代码库备份,只跟随数据库备份。 - **推理困难。** 维护人员读标准 `Sp_SalSalesCheck` 源码时,必须记得*某些客户*实时 DB 上同名过程是另一段代码。stack trace 和“这个过程做什么”取决于你连到哪个 schema。 经验规则:优先选择切片 4 的元数据定制。只有元数据模型确实无法表达客户需求时,才使用切片 5 SQL 覆盖。 ## 示例:重庆展印的 `Sp_SalSalesCheck` vs 标准过程 对实时 dev DB 做量化 diff 后: | 方面 | 标准 `Sp_SalSalesCheck`(DB 中) | 重庆展印覆盖(`script/客户/重庆展印/Sp_SalSalesCheck.sql`) | |---|---|---| | 主体长度 | 1714 行 | 723 行(约标准的 42%) | | 参数签名 | 14 个参数:`sLoginId, sCustomerId, sBrId, sSuId, bFilter, sUnTaskFormId, pageNum, pageSize, totalCount(OUT), countCloumn, countMapJson(OUT), sFilterOrderBy, sGroupby_select_sql, sGroupby_group_sql` | **完全相同**:14 个参数、相同顺序 | | `SysSystemSettings.CbxSrcNoCheck` 查询 | **未使用** | **使用**,驱动“未对账印件清单来源”,即哪些计费类型来源进入报表 | | `Fun_GetLookCustomer(sLoginId, sBrId, sSuId)` 权限作用域 | 使用 | 使用(相同调用) | | 临时表聚合流(`B1`、`B2` 等,多段 `DROP TEMPORARY TABLE` + `INSERT INTO`) | 很重,是 1714 行主体的大头 | 移除 / 简化 | 因此重庆展印的覆盖: - 保持框架调用点不变(参数签名完全相同,所以元数据驱动分发器仍能正确调用;见[通用存储过程分发](../reference/maintainer/proc-dispatch.md))。 - 增加了标准过程未暴露的 `CbxSrcNoCheck` 系统设置分支。schema 中另有 12 个 `Sp_*` 过程也使用 `CbxSrcNoCheck`(`Sp_Manufacture_MftWorkOrderAround`、`Sp_OverdueNoCheck`、`Sp_Receivables_*` 家族,以及兄弟过程 `Sp_SalSalesCheck1` / `_1227` / `_YanBao` / `_ded_copy1`);该覆盖把这个模式引入客户的主过程。 - 去掉标准过程中较重的临时表聚合流。这不是更复杂的查询,而是一条更简单的查询路径;该客户的对账语义显然不需要标准完整聚合。 如果维护人员需要精确业务规则差异,直接 diff 两个主体: ```bash mysql --defaults-file=$HOME/.my.cnf xlyweberp_saas_ai \ -BNe "SELECT ROUTINE_DEFINITION FROM information_schema.routines \ WHERE ROUTINE_NAME='Sp_SalSalesCheck'" > /tmp/std.sql diff /tmp/std.sql script/客户/重庆展印/Sp_SalSalesCheck.sql | head -200 ``` ## 示例 2:万昌构建多级审批工作流 {#示例-2万昌构建多级审批工作流} 上面的重庆展印例子替换的是**一个**过程主体。**`script/客户/万昌/`** 目录展示了更进一步的模式:客户扩展 schema,并构建标准框架没有随附的多级审批工作流。 定制树节选: ```text script/客户/万昌/ ├── 计件工资/ │ ├── 日报审核/ │ │ └── 领班驳回.sql ← 本切片锚点 │ ├── 报表/ │ │ ├── 包装补时.sql │ │ ├── 员工大废品.sql │ │ ├── 班组大废品率查询报表.sql │ │ ├── 手工质检组返工.sql │ │ └── Sp_Manual_quality_inspection_rework.sql │ └── 计件工资核算/ │ ├── 计件工资/ │ │ ├── sp_piece_rate_j.sql │ │ ├── sp_piece_rate_JZ.sql │ │ ├── sp_piece_rate_other.sql │ │ └── sp_piece_rate_w.sql │ ├── 员工工资汇总查询/员工工资汇总查询.sql │ ├── Sp_BtnEven_CalcJsHs.sql │ └── sp_btn_WorkOrderAssessmentPassRate.sql ├── Sp_getworkorder_calc_cb.sql └── … ``` 这些中文目录(`计件工资` / `日报审核`)把客户组织流程直接编码进文件系统。维护人员只看 `ls`,就能知道每个脚本属于哪条业务流程。 ### 驳回脚本实际做了什么 `领班驳回.sql` 共 185 行,定义 `Sp_mftproductionreportmaster_check1_0`。命名遵循 xly 的状态转换约定:`Sp__check_`,所以 `check1_0` 表示“从状态 1(已审核)回到状态 0(草稿)”,也就是驳回。 核心 UPDATE 逻辑裁剪如下: ```sql SET p_setSql = CONCAT('bManager = 0, bIPQC = 0, bDeputy = 0, bSubmit = 0, bWorkshopManager = 0, bCheck = 0, sRejectMemo = ''', p_sRejectMemo, ''', sMReserve1 = ', p_textareaValue); Set @sSqlStmt = CONCAT('Update mftproductionreportmaster Set ', p_setSql, ' Where sId = ''', p_sTmpId, ''' AND sBrandsId = ''', sBrId, ''' AND sSubsidiaryId = ''', sSuId, ''''); PREPARE sSqlStmt FROM @sSqlStmt; EXECUTE sSqlStmt; CALL sp_add_flow_log(p_sTmpId, p_sTmpId, '驳回', '驳回', '驳回', sMakePerson, p_sRejectMemo, @sReturn, @sCode); ``` 所以一次按钮点击会同时重置**六个**审批标志,追加每行的驳回原因历史,并写入客户自定义审计日志。 ### 哪些是客户侧内容,不在标准 schema 中 已对 dev DB 侦察目标(`xlyweberp_saas_ai`)验证: | 定制项 | 标准 schema 是否存在 | 万昌是否需要新增 | |---|---|---| | `mftproductionreportmaster` 上的多级审批列:`bManager`、`bIPQC`、`bDeputy`、`bSubmit`、`bWorkshopManager` | **不存在**;标准只有 `bCheck`、`sCheckPerson`、`tCheckDate` | 是,需要 `ALTER TABLE` 增加 5 个 boolean 列 | | `sRejectMemo` 驳回原因历史列 | **不存在** | 是,需要 `ALTER TABLE` 增加 longtext | | `sp_add_flow_log` 审计日志过程 | **不存在** | 是,完全由客户定义 | | `Sp_
_check_` 命名约定 | **标准 DB 中没有过程使用该模式** | 是,万昌自己的约定 | | 接入框架按钮机制 | 存在;`gdsconfigformslave.sButtonParam` 指向过程名 | 只需要配置 | 因此,万昌的“领班驳回”工作流是**建立在 xly 按钮原语之上的客户自建状态机**:schema 扩展 + 自定义过程 + 自定义审计日志。框架只提供按钮点击分发(经 `/business/genericProcedureCall*` 或 form-slave 上的 button-param 钩子)。其余内容,包括单据处于什么状态、哪些标志翻转、写什么审计文本,都在客户侧。 这与 Activiti 解决同一问题的方式完全不同(BPMN 图 + assignee model + Activiti 任务表)。xly 框架允许客户选择任一模型: - **Activiti 模式**:部署 BPMN,通过 `gdsmoduleflow` 关联,并把 `ConstantUtils.bCheckflowCheck` 改成 `true`;见 [activiti.md](../reference/maintainer/activiti.md#路径-3activiti-bpmn-工作流有闸门目前代码中禁用)。 - **万昌模式**:扩展 schema,编写状态转换过程,放到 `script/客户///` 下,手工应用。 代码库中生产相邻的定制展示的是万昌模式:Activiti 已接线,但 `script/客户/` 下没有任何客户目录部署 BPMN;而万昌式 schema-extending workflow 确实存在。这回答了“这个仓库里工作流如何定制”的实证问题:**通过每客户覆盖脚本交付 schema 扩展型存储过程**。 ### 客户定制模式一览 18 个客户覆盖目录中,大多数并不是定制“工作流”本身,而是定制**计算和报表**。各目录大致内容: - `万昌`(14 个文件):包含 `领班驳回.sql` 工作流扩展,也包含计件工资计算过程。 - `千彩`(50 个文件):定制最重的客户。主要是每租户计算覆盖(`Sp_Calc_*`、`Sp_Inventory_*`、`Sp_Manufacture_*`)和一个工作流列表视图(`viw_NoSalSalesChecking`)。 - `重庆展印`(2 个文件):如上文所述,替换一个销售对账过程和一个配套视图。 - `朝阳`(8)、`金宣发`(8)、`无锡中江`(8)、`亚明威`(6)、`福雅`(5)、`金九`(5)、`快马`(4)等:较小的计算 / 报表覆盖。 所以工作流定制模式(schema 扩展 + 状态转换过程 + 自定义审计)是**少见的**。只有客户流程确实不能被一步审批表达、标准框架的 `bCheck` toggle 不够时,才值得这样做。大部分客户分歧是计算逻辑,而不是工作流形状。 配套视图 `viw_salsaleschecking_pro.sql` 也出于同一原因存在:当覆盖需要标准没有的 join 形状时,工程师编写客户特定视图,并与过程一起应用到该客户 schema。 ## 本切片引入的概念 - *两条定制通道*(细化[现有概念页](../concepts/customization-channels.md)):通过**后台**编辑元数据(切片 1、2、4)vs. 直接应用到客户 schema 的原始 SQL 覆盖。 - *客户间 schema 分歧*:同一过程名在不同客户 DB 中可能表示不同过程,影响维护人员分析运行时行为。 ## 参考项 应新增维护人员页:*每客户 SQL 覆盖*,记录 `script/客户//` 约定、手工应用流程、运维影响和审计模式。 ## 待验证项 1. ~~**脚本应用是手工,还是存在 Quartz / DbToDb 机制?**~~ **已关闭:手工。** `xlyFlow/.../dbtodb/service/impl/DbToDbServiceImpl.java` 是**库间同步**(公开方法包括 `getData`、`getDataDetail`、`getDataCount`、`addSave`、`execute`、`select`、`testConnect`,都通过 `DruidDataSource` + `DruidProperties` + `JdbcUtils` 操作客户自己的远程 DB)。它**不是**脚本应用器:in-scope 代码库中没有任何遍历 `script/客户/` 目录的步骤(`grep -rn "script/客户" xly-src/.../*.java` 无命中)。每个 `script/客户//.sql` 都是为了可追溯性提交,并由工程师 / DBA 通过 `mysql --defaults-file=… < the-file.sql` 手工应用。 2. **审计。** 写小脚本连接客户 DB,把每个 `Sp_*` / `viw_*` 主体与标准版本做 diff。意外分歧是运维风险。*后续工作:针对单个租户 DB 的审计查询只是一条语句;真正工作量在于把它自动化到全部客户环境。* 3. **并排 `Sp_SalSalesCheck` diff。** 上面的结构化 diff 表(大小、参数、关键 SQL 特征、`CbxSrcNoCheck` 分支)覆盖了分歧的*形状*;如果要进一步解释精确业务规则差异,可以补过程主体级 diff。*后续工作:上面的命令可按需生成;把完整 diff 嵌入 wiki 页面的收益不值得增加页面重量。* 4. **生命周期。** 客户升级、恢复、重建 schema 时,每个覆盖如何重新应用?部署章节需要 runbook。