切片 5 — 扩展:每客户 SQL 覆盖
元数据驱动框架能表达的内容是有限的。
客户可能需要销售对账报表使用不同聚合规则,需要一个独特的自定义视图支撑成本看板,或需要一个行为从根本上偏离标准的存储过程。覆盖表(切片 4)可以加字段和覆盖 SQL 片段,但不能替换存储过程的逻辑;过程是代码,不是数据。
到达这个边界时,xly 的逃生口是:把手写 SQL 文件提交到 script/客户/<customer-name>/,并直接应用到该客户 schema。这完全绕过元数据层。它是生产中真实使用的通道,适用于若干客户;Wiki 必须诚实记录其范围、成本、适用时机和运维纪律。
记录对象
| 客户 | 重庆展印 |
| 覆盖文件 |
script/客户/重庆展印/Sp_SalSalesCheck.sql(723 行) |
| 替换内容 | 标准 Sp_SalSalesCheck 存储过程(销售对账列表) |
| 配套文件 |
script/客户/重庆展印/viw_salsaleschecking_pro.sql(该过程读取的自定义视图) |
| 部署 | 手工,由工程师 / DBA 对该客户 schema 应用 |
文件开头两行说明了这个通道的形状:
DROP PROCEDURE IF EXISTS `Sp_SalSalesCheck`;
delimiter ;;
CREATE PROCEDURE `Sp_SalSalesCheck`(IN sLoginId varchar(100), ...)
标准过程被 drop,自定义过程取而代之。gdsmodule 中的元数据仍按名称引用该过程(sProcName = 'Sp_SalSalesCheck');运行时照常调用。差异完全存在于过程主体中,而主体现在是工程师编写的变体。
当前代码库中有覆盖的客户
script/客户/ 下有 18 个客户子目录:
统兴 重庆展印 嘉诚 安徽金印 万昌
无锡中江 扬州浩宇 金九 亚明威 快马
金宣发 千彩 高旺 朝阳 上海亚峰
湛江新澳 福雅 远传
每个目录有一到十几个 .sql 文件。多数是自定义过程(Sp_*)和自定义视图(viw_*)。读取目录列表本身就是系统文档:这里的客户和功能,是框架无法表达、需要 bespoke 行为的地方。
覆盖如何进入系统
没有自动通道。搜索 Java 代码中加载 script/客户 或遍历这些目录的部署服务,没有结果。机制是运维性的:
- 工程师以标准过程为起点编写覆盖
.sql并修改主体。 - 文件提交到
script/客户/<customer>/,用于可追溯性。 - 使用
mysql --defaults-file=... < the-file.sql或等价方式,手工对目标客户 MySQL schema 执行。 - 从那以后,该客户 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 行主体的大头 | 移除 / 简化 |
因此重庆展印的覆盖:
- 保持框架调用点不变(参数签名完全相同,所以元数据驱动分发器仍能正确调用;见通用存储过程分发)。
- 增加了标准过程未暴露的
CbxSrcNoCheck系统设置分支。schema 中另有 12 个Sp_*过程也使用CbxSrcNoCheck(Sp_Manufacture_MftWorkOrderAround、Sp_OverdueNoCheck、Sp_Receivables_*家族,以及兄弟过程Sp_SalSalesCheck1/_1227/_YanBao/_ded_copy1);该覆盖把这个模式引入客户的主过程。 - 去掉标准过程中较重的临时表聚合流。这不是更复杂的查询,而是一条更简单的查询路径;该客户的对账语义显然不需要标准完整聚合。
如果维护人员需要精确业务规则差异,直接 diff 两个主体:
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,并构建标准框架没有随附的多级审批工作流。
定制树节选:
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 逻辑裁剪如下:
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。 -
万昌模式:扩展 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。
本切片引入的概念
- 两条定制通道(细化现有概念页):通过后台编辑元数据(切片 1、2、4)vs. 直接应用到客户 schema 的原始 SQL 覆盖。
- 客户间 schema 分歧:同一过程名在不同客户 DB 中可能表示不同过程,影响维护人员分析运行时行为。
参考项
应新增维护人员页:每客户 SQL 覆盖,记录 script/客户/<customer>/ 约定、手工应用流程、运维影响和审计模式。
待验证项
-
脚本应用是手工,还是存在 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手工应用。 -
审计。 写小脚本连接客户 DB,把每个
Sp_*/viw_*主体与标准版本做 diff。意外分歧是运维风险。后续工作:针对单个租户 DB 的审计查询只是一条语句;真正工作量在于把它自动化到全部客户环境。 -
并排
Sp_SalSalesCheckdiff。 上面的结构化 diff 表(大小、参数、关键 SQL 特征、CbxSrcNoCheck分支)覆盖了分歧的形状;如果要进一步解释精确业务规则差异,可以补过程主体级 diff。后续工作:上面的命令可按需生成;把完整 diff 嵌入 wiki 页面的收益不值得增加页面重量。 - 生命周期。 客户升级、恢复、重建 schema 时,每个覆盖如何重新应用?部署章节需要 runbook。