05-customer-sql-override.md 14.1 KB

切片 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/客户 或遍历这些目录的部署服务,没有结果。机制是运维性的:

  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) 权限作用域 使用 使用(相同调用)
临时表聚合流(B1B2 等,多段 DROP TEMPORARY TABLE + INSERT INTO 很重,是 1714 行主体的大头 移除 / 简化

因此重庆展印的覆盖:

  • 保持框架调用点不变(参数签名完全相同,所以元数据驱动分发器仍能正确调用;见通用存储过程分发)。
  • 增加了标准过程未暴露的 CbxSrcNoCheck 系统设置分支。schema 中另有 12 个 Sp_* 过程也使用 CbxSrcNoCheckSp_Manufacture_MftWorkOrderAroundSp_OverdueNoCheckSp_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 上的多级审批列:bManagerbIPQCbDeputybSubmitbWorkshopManager 不存在;标准只有 bChecksCheckPersontCheckDate 是,需要 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>/ 约定、手工应用流程、运维影响和审计模式。

待验证项

  1. 脚本应用是手工,还是存在 Quartz / DbToDb 机制? 已关闭:手工。 xlyFlow/.../dbtodb/service/impl/DbToDbServiceImpl.java库间同步(公开方法包括 getDatagetDataDetailgetDataCountaddSaveexecuteselecttestConnect,都通过 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。