状态机必须显式定义合法流转路径,否则会导致状态乱跳、显示错乱、审计失效;需用action区分上下文,数据库应预留last_status_action和status_changed_at字段。

任务状态机不是加个状态字段就能用的,必须显式定义合法流转路径,否则数据库里状态乱跳、前端显示错乱、审计日志失效都是大概率事件。
为什么直接改数据库 status 字段会出问题
很多团队初期图快,直接在代码里写 $task->status = 'completed'; $task->save();,看似简单,实则埋下三类隐患:
- 没人校验当前状态是否允许跳转到
completed(比如从draft直接到completed就该被拦住) - 缺少统一入口,不同 Controller、Command、API 都可能绕过业务逻辑直接更新状态
- 无法自动记录变更人、时间、触发动作(是用户点击“完成”?还是超时自动关闭?还是定时脚本批量归档?)
用 OrderStateMachine 类封装状态流转规则
参考你知识库中已有的 OrderStateMachine 结构,它轻量、无框架依赖,适合中小项目快速落地:
关键点在于 $transitions 数组必须明确声明每种合法跳转:
$transitions = [
'draft' => ['pending', 'cancelled'],
'pending' => ['processing', 'cancelled'],
'processing' => ['completed', 'failed', 'paused'],
'paused' => ['processing', 'cancelled'],
'completed' => ['archived'],
];
调用时统一走 canTransition('processing', 'completed') 判断,再执行实际更新。别把判断和更新拆开写——中间可能被并发请求插队。
状态变更必须绑定动作(action),不能只看 from/to
同一个 from → to 路径,在不同上下文里合法性可能不同。例如:
- 用户手动点击“完成”,
processing → completed是允许的 - 但系统定时脚本发现任务超时 72 小时未更新,想自动标为
expired,就不能复用同一套transitions数组
解决方案:在状态机中引入 action 参数,比如 complete_by_user、expire_by_cron,并在 canTransition() 内部做细粒度校验:
public function canTransition($from, $to, $action = null) {
if (!$this->isValidState($from) || !$this->isValidState($to)) {
return false;
}
// 先查基础路径
if (!isset($this->transitions[$from]) || !in_array($to, $this->transitions[$from])) {
return false;
}
// 再按 action 做权限/条件拦截
if ($action === 'expire_by_cron' && $to !== 'expired') {
return false;
}
return true;
}
数据库字段设计要预留扩展空间
别只建一个 status 字符串字段。至少补两个字段:
-
last_status_action:记录最后一次变更的动作名(如mark_as_completed),方便排查谁、为什么改了状态 -
status_changed_at:带时区的时间戳,不是updated_at(后者会被其他字段更新污染)
如果后续要支持多级审批或并行子任务,还得加 parent_status_id 或 status_version 字段——一开始不加,后面加索引、改迁移、补历史数据,成本远高于提前留半行字段。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











