状态机转移控制必须由BEFORE UPDATE触发器实现,显式枚举合法状态对、使用ENUM/外键约束status字段、应用层带条件更新并检查影响行数,严禁存储过程或模糊校验。

存储过程不能实现多级审批流程的状态机转移控制——它不是状态机引擎,也不该承担流程推进职责。
BEFORE UPDATE触发器才是唯一合法的校验入口
MySQL 和 PostgreSQL 的存储过程无法拦截或否决一条 UPDATE 语句;只有 BEFORE UPDATE 触发器能通过 SIGNAL SQLSTATE '45000' 中断非法状态跃迁。存储过程调用再深,也改不了当前 SQL 是否执行成功。
- 在触发器里只做一件事:检查
OLD.status→NEW.status是否在预设白名单中,例如('pending', 'approved_by_mgr')、('approved_by_mgr', 'approved_by_hr') - 所有允许转移对必须显式枚举,不能靠
IF NEW.status != OLD.status这类模糊判断——漏一对就等于开后门 - 别在触发器里查
users.role或调用存储过程,锁表+延迟高,一次更新可能从 2ms 拖到 150ms - 错误信息要带上下文:
SET MESSAGE_TEXT = CONCAT('Invalid transition: ', OLD.status, ' → ', NEW.status),方便快速定位漏覆盖路径
status 字段必须是 ENUM 或外键约束
如果 status 是 VARCHAR(50) 自由输入字段,触发器里的字符串匹配就会失效:'PENDING'、'pending'、'pendng' 全部逃逸校验。
- 建表时强制用
ENUM('draft','pending','approved_by_mgr','approved_by_hr','rejected'),MySQL 会自动拒绝非法值 - PostgreSQL 推荐用外键关联
status_types(code, name)表,便于审计和动态扩展 - 应用层传入的 status 值必须和数据库定义完全一致——大小写、下划线、空格都不能差
状态推进逻辑必须移出数据库
所谓“自动跳转”(比如审批人点通过后,状态从 pending 直接变成 approved_by_hr)不能靠触发器或存储过程完成。原因很实在:
- 两个审批人同时提交,都读到
OLD.status = 'approved_by_mgr',都试图 SETNEW.status = 'approved_by_hr',最终只有一条生效,另一条被覆盖 - MySQL 触发器内禁止
COMMIT或START TRANSACTION,强行写会报 ERROR 1422 - 审批规则常需动态加载(如临时加 CFO 审核),硬编码在存储过程中等于每次改规则都要 DBA 发版
- 真正该由存储过程干的,只有三件事:校验、补全字段(如
updated_at)、轻量聚合(如从子表算出当前最高通过级)
容易被忽略的并发陷阱
哪怕你把触发器写得滴水不漏,只要在应用层用 UPDATE ... SET status = 'xxx' WHERE id = ? 方式推进状态,就躲不开并发覆盖问题。
- 正确做法是带条件更新:
UPDATE audit_flow SET status = 'approved_by_hr' WHERE id = 123 AND status = 'approved_by_mgr',然后检查影响行数是否为 1 - 影响行数为 0?说明状态已被别人改过,此时应重试或返回冲突错误,而不是静默失败
- 审批记录表必须加唯一约束:
UNIQUE (flow_no, approver_role),防止同一角色重复审批导致状态误判 - 别依赖子查询取“最新一条记录”来判断当前状态——驳回返工、条件跳过会让这条逻辑直接崩掉


















