# 通用存储过程分发 当元数据命名一个存储过程时(例如通过 `gdsmodule.sSaveProName`、`sSaveProNameBefore`、`sDeleteProName`、`sCalcProName`、`sProcName`,或 `gdsconfigformslave.sButtonParam`),框架会按名称分发该过程。`sSaveProName` 和 `sSaveProNameBefore` 是**钩子点**:它们作为保存后 / 保存前阶段叠加在始终会运行的基础新增 / 更新路径(`BusinessBaseServiceImpl.addBusinessData` / `updateBusinessData`)之上,由 `BusinessBaseServiceImpl.java:1824` 与 `:1778` 的 `checkUpdate(...,"sSaveProName")` 调用。其他列驱动按需调用:`sCalcProName` 用于按钮计算,`sProcName` 用于自定义取数流等。同一套通用分发机制处理这些情况。 处理器是 `xlyEntry/com/xly/web/businessweb/` 中的 `GenericProcedureCallController`。 ## 端点形状 前端向 `/procedureCall/doGenericProcedureCall` POST: - 存储过程名(通常从 `gdsmodule` 或 `gdsconfigformslave.sButtonParam` 解析)。 - 参数值(前端提供,框架注入租户身份)。 handler 会: 1. 从 `information_schema.PARAMETERS` 加载过程签名(参数名和类型)。 2. 拼出 `CALL (?, ?, ...)`,并按位置绑定参数。 3. 用 `RequestAddParamUtil` 增补 `sBrandsId`、`sSubsidiaryId`、`sLoginId`、`sLanguage`。 4. 通过 MyBatis 执行。 5. 返回过程结果集(过程内 SELECT)或 OUT 参数(保存 / 计算过程)。 ## 为什么动态过程分发重要 schema 中有 1,687 个存储过程和约 177 个函数,把每一个硬编码进 Java 不可承受。按名称分发让框架能调用元数据指向的任意过程,而无需代码变更。框架把过程当作黑盒:输入名称和参数,得到结果。 缺点是:运行时无法静态知道哪些过程存在或它们有什么效果。`gdsmodule.sSaveProName` 中的拼写错误会变成运行时“proc not found”,不是编译错误。 ## 过程遵循的约定 xly 过程共享一套调用约定,以便通用分发可行: - **固定顺序的公共参数:** 通常是 `(IN sLoginId, IN sCustomerId, IN sBrId, IN sSuId, ...)`,跨切面的身份参数总是在前。 - **租户感知:** 每个过程接受 `sBrId`(=`sBrandsId`)和 `sSuId`(=`sSubsidiaryId`),并在内部每个谓词中使用它们保证租户安全。 - **通过 OUT 参数输出:** 保存 / 计算过程通过 `OUT sCode INT, OUT sReturn LONGTEXT` 返回成功 / 错误;规范脚手架见 `xlyEntry/src/main/resources/templates/templesql/sSaveProName.sql`。 - **内部动态 SQL:** 许多过程用 `CONCAT` + `PREPARE` + `EXECUTE` 构造 SQL 字符串。这是 xly 对运行时形状变化的逃生口,也是依赖树中存在 `jSqlParser` 的原因(框架偶尔会检查这些字符串)。 ## 配套文件:`templesql/` `xlyEntry/src/main/resources/templates/templesql/` 保存标准过程类型的**脚手架**,当前树中有 8 个文件: - `sSaveProName.sql` - `sDeleteProName.sql` - `sProcName.sql` - `sSaveProNameBefore.sql` - `sButtonParam.sql` - `sProTempleByDb.sql` - `sp_btn_calc.sql` - `sSqlStr.sql` 这些是工程师编写新过程时填充的模板;运行时不会用它们动态生成过程。loader 和占位符语法见 [SQL 模板](sql-templates.md)。 ## 实时 schema 中的过程命名模具 实时 DB 的 1687 个存储过程围绕少数命名模具聚集,不只是裸 `Sp_*` 家族: | 模具 | 约数 | 角色 | |---|---:|---| | `Sp_*` | 大多数 | 主导家族,由 `sSaveProName` / `sCalcProName` / `sProcName` 等分发。 | | `Sp_*_BeforeSave` | ~62 | 保存前钩子,对应 `sSaveProNameBefore`。 | | `Sp_*_AfterSave` / `Sp_*_SaveReturn` | ~62 / ~54 | 保存后钩子;`_SaveReturn` 会写回父事务。 | | `Sp_*_Calc` | ~178 | 按钮计算过程,由按钮流程(`sCalcProName` / `sButtonParam`)调用。 | | `sp_btn_*` | ~65 | 按钮事件子家族,通常是 `sp_btn_calc*` / `sp_btn_validate*`(约定使用小写)。 | | `PRO_ERPMERGE*` | ~11 | 数据迁移 / 合并工具。**不由运行时分发**,只给工程师直接使用。 | | `PRO_*`(其他) | ~12 | 其他一次性工具。 | | `Get_*`、`del_*`、`Cal*`、`Tj_*` | 少量 | 历史 / 领域特定 helper,不属于通用分发契约。 | 被分发列里如果写错过程名,通常也会期望目标是 `Sp_*` 形状;其他模具不会通过 `sSaveProName` / `sCalcProName` 等解析。非 `Sp_*` 过程只能通过 mapper XML 或其他过程直接调用。 ## 函数层 schema 还包含 **177 个用户自定义函数**,命名模具与过程平行:`Fun_*`(约 150)、`Fn_*`(约 8)、`get_*`(约 10)。 这些函数**不由 Java 分发**。它们从其他存储过程、视图定义和 mapper XML 的 SELECT 语句中被调用。没有 `gdsmodule.sFunctionName` 之类的元数据列;函数由提到它们的 SQL 自行引入。维护人员排查慢报表时,应在过程和视图里 grep `Fun_*` / `Fn_*` / `get_*` 引用;框架 Java 侧看不到这些函数。 ## 需要关注的失效模式 1. **参数顺序不匹配。** 通用分发按位置绑定;IN 参数重排的过程会在运行时炸掉。 2. **缺少租户过滤。** 某过程在内部谓词中忘记 `sBrandsId` / `sSubsidiaryId`,就是多租户泄漏。过程不会被框架自动过滤,必须自己处理。 3. **长运行过程阻塞请求线程。** 长计算应放到独立后台线程或任务服务中(见 `xlyErpTask`,尽管它当前在 `settings.gradle` 中禁用)。