查审批流当前状态不能只取最新记录,因多级审批存在驳回返工、条件跳过等非线性流转,需结合step_order、is_rejected及前序步骤完成状态逐层校验生效节点,否则易误判待办环节。

查审批流当前状态时,为什么不能只查最新一条记录
因为多级审批流的状态不是线性覆盖的,比如“财务驳回”后流程回到“部门负责人审批”,此时最新记录是驳回,但当前待办节点其实是上一级。直接 ORDER BY created_at DESC LIMIT 1 会误判状态。
必须结合状态机定义和审批层级顺序,从“未被否决且未跳过”的最近有效节点中找当前激活态。常见错误是把 status 字段当孤立值查,忽略了 step_order 和 is_rejected 的联合约束。
- 状态机里每个
step_code(如'dept_approve'、'finance_review')有预设顺序和允许转移条件 - 一条审批记录只有在
is_rejected = false且其前序所有步骤都已通过时,才算“生效” - 数据库里可能存着“财务已通过”但“法务还没发起”的中间态,此时当前状态应是法务待办,而非财务已通过
用相关子查询定位每个步骤是否“真正完成”
核心思路:对每个审批步骤,检查它是否满足“已提交 + 未被拒 + 前序步骤全部完成”。这需要逐层向上验证,不能靠单表聚合。
示例:查某 approval_id = 123 的当前状态
SELECT step_code, status, created_at
FROM approval_steps s1
WHERE approval_id = 123
AND status IN ('approved', 'rejected', 'pending')
AND NOT EXISTS (
-- 检查是否存在前序步骤未完成(即:有更小 step_order 且未 approved/rejected)
SELECT 1 FROM approval_steps s2
WHERE s2.approval_id = s1.approval_id
AND s2.step_order < s1.step_order
AND s2.status NOT IN ('approved', 'rejected')
)
AND NOT EXISTS (
-- 检查是否存在本步骤被驳回(驳回即终止后续,但本步骤自身仍算“已处理”)
SELECT 1 FROM approval_steps s3
WHERE s3.approval_id = s1.approval_id
AND s3.step_order < s1.step_order
AND s3.status = 'rejected'
)
ORDER BY step_order DESC
LIMIT 1;注意:NOT EXISTS 里两次子查询缺一不可——前者过滤“卡在中间”的步骤,后者防止“前序被拒后本步骤还误标为有效”。
用递归 CTE 展开完整审批路径再取顶点
当步骤间存在动态跳转(比如“金额>50万时跳过总监,直送 CFO”),硬编码 step_order 就不可靠,得依赖状态机规则表。这时要用递归 CTE 构建合法路径。
前提:有一张 approval_state_rules 表,定义 from_step → to_step + condition_sql(如 "amount > 500000")
- 递归起点是初始步骤(
step_code = 'init') - 每次 JOIN 都要动态计算
condition_sql是否成立(PostgreSQL 可用EXECUTE format(...),MySQL 则需提前展开为 CASE) - 最终结果集中,最后一个
to_step对应的实例记录,才是当前应处节点
实际执行中,condition_sql 字段不能直接执行,必须预编译成布尔表达式字段或用应用层拼接——否则 SQL 注入风险极高,且多数数据库不支持运行时动态条件求值。
性能陷阱:嵌套太深导致全表扫描
三层以上嵌套子查询在 MySQL 5.7 或早期 PostgreSQL 中极易触发临时表 + filesort,尤其当 approval_steps 表超 10 万行时,响应可能从毫秒级升到秒级。
- 务必给组合索引:
(approval_id, step_order, status, is_rejected) - 避免在子查询 WHERE 中用函数包裹字段,例如
DATE(created_at) = '2024-01-01'会让索引失效 - 如果业务允许,把“当前有效步骤”缓存到主表
approvals.current_step_code字段,用触发器或应用层维护,查状态就退化为单行查询
最易被忽略的是状态机规则变更后的数据一致性——比如新增一个步骤,旧审批流不会自动补记录,查询时可能因找不到对应行而返回空,而不是降级到上一步。这个边界必须由业务代码兜底,SQL 层做不到智能 fallback。


















