# 通用存储过程分发 当 `gdsmodule.sSaveProName`(或 `sDeleteProName`、`sCalcProName`、`sProcName`、`sSaveProNameBefore`)**非空**时,框架会调用指定存储过程,而不是落入默认 Add/Update 路径。同一套机制也处理按钮计算和按需自定义逻辑。 处理器是 `xlyEntry/com/xly/web/businessweb/` 中的 `GenericProcedureCallController`。 ## 端点形状 前端向 `/business/genericProcedureCall*` 下的 URL 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)。 ## 需要关注的失效模式 1. **参数顺序不匹配。** 通用分发按位置绑定;IN 参数重排的过程会在运行时炸掉。 2. **缺少租户过滤。** 某过程在内部谓词中忘记 `sBrandsId` / `sSubsidiaryId`,就是多租户泄漏。过程不会被框架自动过滤,必须自己处理。 3. **长运行过程阻塞请求线程。** 长计算应放到独立 worker(见 `xlyErpTask`,尽管它当前在 `settings.gradle` 中禁用)。