MySQL 存储过程编码规范

MySQL 存储过程编码规范

适用范围:ERP / SaaS / 单据型业务,基于 MySQL 8+ ,面向写事务型存储过程(保存、审核、反审核、作废、批处理等)。

本规范统一采用:

  • LEAVE 模式
  • 独立调用的业务存储过程:直接 SELECT code、msg 返回
  • 子存储过程:统一 OUT out_code / OUT out_msg
  • 业务主存储过程可按业务返回额外字段
  • 复杂单据支持 JSON 入参
  • 适配主表 + 明细表 + 状态流转 + 库存联动场景

1. 适用范围

本规范适用于以下类型的 MySQL 存储过程:

  1. 单据保存类

    • 销售单保存
    • 采购单保存
    • 入库单 / 出库单保存
    • 调拨单保存
    • 消息发送
  2. 状态流转类

    • 审核
    • 反审核
    • 作废
    • 关闭
    • 完成
  3. 批处理类

    • 批量生成消息
    • 批量结转积分
    • 批量更新状态
    • 日 / 月汇总
  4. 简单查询类

    • 原则上不推荐用存储过程做普通列表查询
    • 查询优先走 SQL / Mapper / Repository
    • 存储过程主要承担写事务与批处理

2. 设计原则

2.1 一个存储过程只负责一个明确业务动作

例如:

  • sp_sale_order_save:保存销售单
  • sp_sale_order_audit:审核销售单
  • sp_sale_order_cancel:作废销售单
  • sp_msg_send:发送消息
  • sp_member_point_expire:积分失效处理

不要把多个业务动作塞进一个过程,例如:

  • 保存 + 审核 + 发货 + 关闭 全放一个过程

2.2 存储过程负责"数据库事务逻辑",不负责页面逻辑

适合放在存储过程中的内容

  • 参数校验
  • 数据存在性校验
  • 单据状态校验
  • 主表保存
  • 明细新增 / 修改 / 删除
  • 金额重算
  • 库存调整
  • 状态流转
  • 批量写库
  • 事务控制
  • 错误回滚

不适合放在存储过程中的内容

  • 页面展示逻辑
  • UI 交互逻辑
  • 权限菜单逻辑
  • 调外部 HTTP 接口
  • 调消息中间件
  • 调其他微服务
  • 跨系统编排

2.3 过程内部优先使用集合操作,不要写游标式逐行逻辑

优先使用:

  • INSERT ... SELECT
  • UPDATE ... JOIN
  • DELETE ... JOIN
  • JSON_TABLE
  • 临时表
  • 中间差异表

尽量避免:

  • 游标逐行处理
  • WHILE 一行行循环更新
  • 用字符串拆分模拟数组

2.4 写操作过程必须可回滚、可返回明确状态

所有写操作过程都必须:

  • 成功时返回 code = 200
  • 失败时返回 code = 500
  • 出错时回滚
  • 成功时明确提交

3. 命名规范

3.1 存储过程命名

统一前缀

存储过程统一使用前缀:

sql 复制代码
sp_

命名结构

建议格式:

sql 复制代码
sp_业务对象_动作

例如:

  • sp_sale_order_save
  • sp_sale_order_audit
  • sp_sale_order_cancel
  • sp_purchase_order_save
  • sp_stock_in_save
  • sp_stock_out_audit
  • sp_msg_send

3.2 参数命名

输入参数统一前缀:in_

sql 复制代码
IN in_id BIGINT,
IN in_user_id BIGINT,
IN in_shop_id BIGINT,
IN in_data_json JSON

子过程输出参数统一前缀:out_

sql 复制代码
OUT out_code INT,
OUT out_msg VARCHAR(200)

注意:这里只针对子存储过程

独立调用的业务存储过程不使用 OUT out_code/out_msg 返回状态。


3.3 局部变量命名

局部变量统一前缀:

sql 复制代码
v_

例如:

sql 复制代码
DECLARE v_count INT DEFAULT 0;
DECLARE v_bill_id BIGINT DEFAULT 0;
DECLARE v_bill_no VARCHAR(50) DEFAULT '';
DECLARE v_total_num DECIMAL(18,2) DEFAULT 0;
DECLARE v_total_amount DECIMAL(18,2) DEFAULT 0;
DECLARE v_now DATETIME DEFAULT NOW();

3.4 临时表命名

临时表统一使用:

sql 复制代码
tmp_

例如:

  • tmp_order_item
  • tmp_order_item_diff
  • tmp_msg_user
  • tmp_stock_adjust

3.5 标签命名

统一使用:

sql 复制代码
proc_label:

例如:

sql 复制代码
proc_label:BEGIN
    ...
    LEAVE proc_label;
END

4. 返回规范

4.1 返回分层原则

存储过程返回规范分为两层:

  1. 独立调用的业务存储过程

    • 供前端 / Java / 程序层直接调用
    • 不使用 OUT 返回状态
    • 在过程末尾直接 SELECT code、msg 返回
    • 如有业务结果,可一并 SELECT 返回
  2. 子存储过程

    • 供主存储过程内部调用
    • 必须统一返回 out_codeout_msg

4.2 独立调用的业务存储过程返回规范

4.2.1 适用范围

独立调用的业务存储过程是指:

  • 前端直接调用
  • Java 服务直接调用
  • 作为一个完整业务动作入口调用

例如:

  • sp_msg_read
  • sp_msg_send
  • sp_sale_order_save
  • sp_sale_order_audit
  • sp_sale_order_cancel

4.2.2 返回方式

独立调用的业务存储过程,统一通过 结果集 返回状态:

sql 复制代码
SELECT 200 AS code, '成功' AS msg;

失败时:

sql 复制代码
SELECT 500 AS code, '失败原因' AS msg;

4.2.3 可扩展返回字段

如果过程还需要返回业务结果,应直接和 code/msg 一起返回,例如:

sql 复制代码
SELECT
    200 AS code,
    '成功' AS msg,
    v_id AS id,
    v_bill_no AS billNo,
    v_status AS status;

常见业务返回字段

  • id
  • billNo
  • status
  • totalAmount
  • totalNum

4.2.4 统一要求

成功返回

sql 复制代码
SELECT 200 AS code, '成功' AS msg;

失败返回

sql 复制代码
SELECT 500 AS code, '失败原因' AS msg;

禁止写法

独立调用的业务存储过程中,不要使用

sql 复制代码
SET out_code = 200;
SET out_msg = '成功';

不要定义

sql 复制代码
OUT out_code INT,
OUT out_msg VARCHAR(200)

4.3 子存储过程返回规范

4.3.1 适用范围

子存储过程是供其他存储过程内部调用的过程,例如:

  • 明细同步过程
  • 汇总重算过程
  • 库存处理过程
  • 状态校验过程
  • 日志写入过程
  • 编号生成过程
  • 数据校验过程

例如:

  • sp_sale_order_item_sync
  • sp_sale_order_recalc
  • sp_sale_order_stock_apply
  • sp_stock_validate
  • sp_bill_no_generate

4.3.2 必须返回字段

所有子存储过程统一要求:

sql 复制代码
OUT out_code INT,
OUT out_msg VARCHAR(200)

4.3.3 返回规则

成功

sql 复制代码
SET out_code = 200;
SET out_msg = '成功';

失败

sql 复制代码
SET out_code = 500;
SET out_msg = '具体失败原因';

例如:

  • '库存不足'
  • '单据明细不合法'
  • '商品不存在'
  • '当前状态不允许操作'

4.4 状态码约定

默认基础约定:

状态码 含义
200 成功
500 失败

如果业务需要细分,建议扩展为:

状态码 含义
200 成功
400 参数错误
401 数据不存在
402 状态不允许
403 权限 / 归属不允许
409 数据冲突 / 重复
500 系统异常

如果没有特殊要求,默认先用 200 / 500


5. 过程结构规范

5.1 独立调用业务过程标准骨架

sql 复制代码
CREATE PROCEDURE sp_xxx(
    IN in_xxx ...
)
proc_label:BEGIN
    DECLARE v_now DATETIME DEFAULT NOW();
    DECLARE v_error_text TEXT;

    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        GET DIAGNOSTICS CONDITION 1 v_error_text = MESSAGE_TEXT;
        ROLLBACK;
        SELECT 500 AS code, CONCAT('系统异常:', v_error_text) AS msg;
    END;

    START TRANSACTION;

    -- 1. 参数校验

    -- 2. 数据校验

    -- 3. 业务处理

    COMMIT;
    SELECT 200 AS code, '成功' AS msg;
END;

5.2 子存储过程标准骨架

sql 复制代码
CREATE PROCEDURE sp_xxx_sub(
    IN in_xxx ...,
    OUT out_code INT,
    OUT out_msg VARCHAR(200)
)
proc_label:BEGIN
    DECLARE v_error_text TEXT;

    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        GET DIAGNOSTICS CONDITION 1 v_error_text = MESSAGE_TEXT;
        SET out_code = 500;
        SET out_msg = CONCAT('系统异常:', v_error_text);
    END;

    SET out_code = 200;
    SET out_msg = '成功';

    -- 子过程业务逻辑
END;

5.3 统一使用 LEAVE 模式提前退出

禁止在过程里到处嵌套多层 IF ... ELSE ...。校验失败时,直接:

独立业务过程

sql 复制代码
ROLLBACK;
SELECT 500 AS code, 'xxx' AS msg;
LEAVE proc_label;

子过程

sql 复制代码
SET out_code = 500;
SET out_msg = 'xxx';
LEAVE proc_label;

6. 主过程调用子过程规范

6.1 主过程调用子过程必须检查返回值

主过程调用子过程时,必须接收并判断子过程返回的 out_codeout_msg

禁止只调用不判断结果。

错误示例

sql 复制代码
CALL sp_sale_order_item_sync(...);
CALL sp_sale_order_recalc(...);
CALL sp_sale_order_stock_apply(...);

6.2 正确调用模板

sql 复制代码
CALL sp_sale_order_item_sync(v_order_id, in_data_json, v_sub_code, v_sub_msg);
IF v_sub_code <> 200 THEN
    ROLLBACK;
    SELECT v_sub_code AS code, v_sub_msg AS msg;
    LEAVE proc_label;
END IF;

CALL sp_sale_order_recalc(v_order_id, v_sub_code, v_sub_msg);
IF v_sub_code <> 200 THEN
    ROLLBACK;
    SELECT v_sub_code AS code, v_sub_msg AS msg;
    LEAVE proc_label;
END IF;

6.3 主过程定义子过程接收变量

建议统一定义:

sql 复制代码
DECLARE v_sub_code INT DEFAULT 200;
DECLARE v_sub_msg VARCHAR(200) DEFAULT '成功';

7. 参数规范

7.1 单值参数

简单过程直接传普通参数:

sql 复制代码
IN in_id BIGINT,
IN in_user_id BIGINT

例如:

  • 阅读消息
  • 审核单据
  • 删除草稿

7.2 复杂单据保存统一传 JSON

对于"主表 + 明细表"的保存类过程,建议统一传:

sql 复制代码
IN in_data_json JSON

而不是把每个字段都展开成几十个参数。


7.3 JSON 结构建议

统一采用:

json 复制代码
{
  "bill": {
    "id": 1,
    "billNo": "SO202606210001",
    "remark": "测试"
  },
  "items": [
    {
      "id": 11,
      "skuId": 101,
      "num": 2,
      "price": 100
    }
  ],
  "ext": {
    "source": "pc"
  }
}

建议分成三层:

  • bill:主表字段
  • items:明细列表
  • ext:扩展字段

8. 校验规范

8.1 参数校验放在最前面

先校验入参是否为空、格式是否合理,再做数据库操作。

例如独立过程:

sql 复制代码
IF in_user_id IS NULL OR in_user_id <= 0 THEN
    ROLLBACK;
    SELECT 500 AS code, '用户不能为空' AS msg;
    LEAVE proc_label;
END IF;

例如子过程:

sql 复制代码
IF in_user_id IS NULL OR in_user_id <= 0 THEN
    SET out_code = 500;
    SET out_msg = '用户不能为空';
    LEAVE proc_label;
END IF;

8.2 存在性校验要显式写

例如:

sql 复制代码
SELECT COUNT(1)
INTO v_count
FROM sale_order
WHERE id = in_id;

IF v_count = 0 THEN
    ROLLBACK;
    SELECT 500 AS code, '单据不存在' AS msg;
    LEAVE proc_label;
END IF;

8.3 状态校验必须明确

例如审核前要校验:

  • 是否已删除
  • 是否已审核
  • 是否允许当前状态流转

9. JSON 明细处理规范

9.1 明细统一落临时表

对于单据保存类过程,先把 JSON 明细转成临时表:

sql 复制代码
CREATE TEMPORARY TABLE tmp_order_item(
    id BIGINT,
    sku_id BIGINT,
    num DECIMAL(18,2),
    price DECIMAL(18,2)
);

然后:

sql 复制代码
INSERT INTO tmp_order_item(id, sku_id, num, price)
SELECT
    jt.id,
    jt.sku_id,
    jt.num,
    jt.price
FROM JSON_TABLE(
    JSON_EXTRACT(in_data_json, '$.items'),
    '$[*]' COLUMNS(
        id BIGINT PATH '$.id' DEFAULT NULL ON EMPTY,
        sku_id BIGINT PATH '$.skuId',
        num DECIMAL(18,2) PATH '$.num',
        price DECIMAL(18,2) PATH '$.price'
    )
) jt;

10. 明细同步规范(新增 / 修改 / 删除)

10.1 保存类单据必须支持明细差异同步

对于修改单据,不要先全删后全插,优先做:

  • 新增
  • 修改
  • 删除

10.2 建议做法:新旧集合比对

新明细

  • 来自 tmp_order_item

旧明细

  • 来自正式表 sale_order_item

然后分三类处理:

  • 新有旧无:新增
  • 新旧都有但字段变化:修改
  • 旧有新无:删除

11. 审核 / 反审核 / 作废规范

11.1 每个动作独立过程

例如:

  • sp_sale_order_audit
  • sp_sale_order_unaudit
  • sp_sale_order_cancel

不要把这些动作混在保存过程里通过 actionType 分支处理。


11.2 审核动作必须做状态校验

例如:

  • 草稿才能审核
  • 审核后不能重复审核
  • 作废单不能审核

11.3 审核动作要明确副作用

审核可能伴随:

  • 扣减库存
  • 生成出库任务
  • 写财务流水
  • 写操作日志

这些都必须在同一事务内完成。


12. 日志规范

12.1 关键业务动作要写日志

例如:

  • 保存单据
  • 提交审核
  • 审核通过
  • 反审核
  • 作废

12.2 日志写入也放在事务里

这样能保证:

  • 单据保存成功,日志也成功
  • 单据回滚,日志也不留脏数据

13. SQL 编写规范

13.1 关键字统一大写

例如:

  • SELECT
  • UPDATE
  • DELETE
  • FROM
  • WHERE
  • JOIN
  • IF
  • BEGIN
  • END

13.2 不要写 SELECT *

过程里统一显式列出字段,便于维护和避免字段变更影响。


14. 事务规范

14.1 一个业务动作一个事务

例如:

  • 保存单据 = 一个事务
  • 审核单据 = 一个事务
  • 作废单据 = 一个事务

14.2 校验失败要显式回滚并 LEAVE

独立业务过程

sql 复制代码
ROLLBACK;
SELECT 500 AS code, 'xxx' AS msg;
LEAVE proc_label;

子过程

sql 复制代码
SET out_code = 500;
SET out_msg = 'xxx';
LEAVE proc_label;

14.3 成功只在末尾 COMMIT

避免在中间分支里随意 COMMIT


15. 独立调用业务过程模板

sql 复制代码
DELIMITER $$

DROP PROCEDURE IF EXISTS sp_xxx$$
CREATE PROCEDURE sp_xxx(
    IN in_id BIGINT,
    IN in_user_id BIGINT
)
proc_label:BEGIN
    DECLARE v_now DATETIME DEFAULT NOW();
    DECLARE v_count INT DEFAULT 0;
    DECLARE v_error_text TEXT;

    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        GET DIAGNOSTICS CONDITION 1 v_error_text = MESSAGE_TEXT;
        ROLLBACK;
        SELECT 500 AS code, CONCAT('系统异常:', v_error_text) AS msg;
    END;

    START TRANSACTION;

    IF in_user_id IS NULL OR in_user_id <= 0 THEN
        ROLLBACK;
        SELECT 500 AS code, '用户不能为空' AS msg;
        LEAVE proc_label;
    END IF;

    -- 业务逻辑

    COMMIT;
    SELECT 200 AS code, '成功' AS msg;
END$$

DELIMITER ;

16. 子存储过程模板

sql 复制代码
DELIMITER $$

DROP PROCEDURE IF EXISTS sp_xxx_sub$$
CREATE PROCEDURE sp_xxx_sub(
    IN in_xxx BIGINT,
    OUT out_code INT,
    OUT out_msg VARCHAR(200)
)
proc_label:BEGIN
    DECLARE v_error_text TEXT;

    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        GET DIAGNOSTICS CONDITION 1 v_error_text = MESSAGE_TEXT;
        SET out_code = 500;
        SET out_msg = CONCAT('系统异常:', v_error_text);
    END;

    SET out_code = 200;
    SET out_msg = '成功';

    -- 子过程业务逻辑

    IF in_xxx IS NULL OR in_xxx <= 0 THEN
        SET out_code = 500;
        SET out_msg = '参数不能为空';
        LEAVE proc_label;
    END IF;

END$$

DELIMITER ;

17. 项目中建议固定的统一规则

  1. MySQL 存储过程全部使用 LEAVE 模式
  2. 独立调用的业务存储过程统一 SELECT code,msg 返回
  3. 子存储过程统一返回 OUT out_code / OUT out_msg
  4. 保存类业务过程返回 code/msg + id/billNo
  5. 状态流转类过程可返回 code/msg + status
  6. 输入参数统一 in_ 前缀
  7. 局部变量统一 v_ 前缀
  8. 临时表统一 tmp_ 前缀
  9. 过程统一使用 proc_label:BEGIN ... LEAVE proc_label
  10. 复杂单据统一 in_data_json JSON 入参
  11. 前端不可信任汇总金额,数据库重算
  12. 修改单据时,明细按"新增 / 修改 / 删除"同步,不要简单全删全插
  13. 所有审核 / 反审核 / 作废动作单独一个过程
  14. 主过程调用子过程时必须检查子过程返回值
  15. 异常统一 EXIT HANDLER + ROLLBACK/SET out_code + out_msg

18. 返回规范一句话总结

独立调用业务过程

  • 直接 SELECT code,msg,... 返回给前端 / Java
  • 不使用 OUT 返回状态

子存储过程

  • 统一使用 OUT out_code / OUT out_msg
  • 供主过程内部调用和判断结果