# 切片 5 — 扩展：每客户 SQL 覆盖

元数据驱动框架能表达的内容是有限的。

客户可能需要销售对账报表使用不同聚合规则，需要一个独特的自定义视图支撑成本看板，或需要一个行为从根本上偏离标准的存储过程。覆盖表（[切片 4](04-custom-field.md)）可以加字段和覆盖 SQL 片段，但不能替换存储过程的*逻辑*；过程是代码，不是数据。

到达这个边界时，xly 的逃生口是：**把手写 SQL 文件提交到 `script/客户/<customer-name>/`，并直接应用到该客户 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/客户/<customer>/`，用于可追溯性。
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_<table>_check<currentState>_<nextState>`，所以 `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_<table>_check<n>_<m>` 命名约定 | **标准 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/客户/<name>/<flow-name>/` 下，手工应用。

代码库中生产相邻的定制展示的是万昌模式：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/客户/<customer>/` 约定、手工应用流程、运维影响和审计模式。

## 待验证项

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/客户/<customer>/<file>.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。
