普通会员策略 1.0 版本用了大量 if-else 判断会员状态,代码里到处都是 if ($member->status == 'active' && $member->expire_time > time()) 这样的判断,状态流转分散在各个业务方法里,加新状态要改很多地方。2022年8月做了 2.0 重构,用状态机管理会员生命周期,今天把设计思路分享出来。
一、1.0 的问题
- 状态分散:会员状态(待激活、正常、过期、冻结、注销)流转散落在各个方法里
- if-else 嵌套:判断会员是否可用要检查状态、过期时间、冻结原因等多个条件
- 不可追溯:状态变更没有记录,出问题查不到什么时候、因为什么变的状态
- 扩展困难:加新状态(如暂停)要改所有判断逻辑,容易漏改
- 并发问题:状态变更没有原子保护,并发请求可能导致状态错乱
二、状态机设计
2.1 状态定义
| |
2.2 状态流转图
2.3 流转规则配置
用配置定义允许的状态流转:
| |
关键点:
- 流转规则配置化:允许的状态流转在配置里定义,不允许的流转直接拒绝
- 乐观锁:更新时带
WHERE status = oldStatus,防止并发变更 - 变更日志:每次状态变更都记录,可追溯
- 后置动作:状态变更后触发通知、清缓存等动作
2.4 状态变更日志
| |
三、业务集成
3.1 会员可用判断
封装统一的可用判断方法,不再到处写 if-else:
| |
3.2 定时过期检查
定时任务检查过期会员,自动变更状态:
| |
四、踩坑经验
1. 状态流转循环:配置流转规则时要注意不要形成循环(如 active → expired → active 是正常的续费循环,但 active → frozen → active → frozen 无限循环要避免)。
2. 并发状态变更:两个请求同时变更同一个会员状态,乐观锁能防止数据错乱,但要处理好重试逻辑,给用户友好的提示。
3. 后置动作失败:状态变更成功但后置动作(如发通知)失败,不能回滚状态变更。用消息队列异步处理后置动作,失败重试,保证最终一致。
4. 历史数据兼容:1.0 版本的状态值和 2.0 不一致,要做数据迁移,把旧状态映射到新状态。
五、总结
基于状态机的会员生命周期管理核心经验:
- 状态显式化:所有状态明确定义,不允许隐式状态(如"过期但 status 还是 active")
- 流转规则化:允许的状态流转在配置里定义,不允许的直接拒绝
- 变更原子化:乐观锁保证并发安全,状态变更原子操作
- 日志可追溯:每次状态变更都记录,出问题能查到原因
- 动作后置化:状态变更后的通知、清缓存等动作用消息队列异步处理
- 判断统一化:会员可用判断封装成统一方法,不在业务代码里散写 if-else
状态机不是银弹,适合状态多、流转复杂的场景。如果只有两三个状态、流转简单,用简单的 if-else 反而更直接。技术选型要根据实际复杂度,不要为了用状态机而用状态机。