票务系统订单业务需确保状态可控、库存不超卖、流程可追溯;通过yii的activerecord、事务、事件和behavior构建订单链路,订单创建须防重校验、原子写入与行锁,支付状态流转用状态机管理,电子票生成与核销解耦,日志结构化且事务安全。

票务系统订单业务的核心在于状态可控、库存不超卖、流程可追溯。Yii 框架本身不提供现成的票务订单模块,但利用其 ActiveRecord、事务机制、事件系统和行为(Behavior)能力,能快速构建稳定、可扩展的订单处理链路。
订单创建阶段:防重、校验、原子写入
用户选座下单不是简单插入一条记录,必须同步锁定座位、生成唯一订单号、并确保整个过程要么全成功,要么全回滚。
- 订单号建议用业务规则生成,如
TKT+ 年月日 + 6位随机码(避免暴露自增 ID,也方便排查) - 创建订单前,先用 Redis 或数据库行锁(
SELECT ... FOR UPDATE)锁定所选座位,防止并发超卖 - 所有写操作(订单主表、订单明细、座位状态更新、库存扣减)必须包裹在同一个数据库事务中
- 不要在 Controller 层直接调用
Order::save(),应封装进OrderService::create($seats, $userId)统一处理
订单支付与状态流转:用状态机而非 if-else
票务订单常见状态包括:待支付、已支付、已出票、已核销、已退票、已过期。硬编码判断或散落在模型方法里的状态跳转极易失控。
- 定义独立的
TicketOrderStateMachine类,作为唯一状态变更入口,例如:$stateMachine->apply($order, 'paid', $context) - 状态值使用 PHP 枚举(
enum TicketOrderStatus: string),保证类型安全和 IDE 提示 - 状态变更后触发事件(如
TicketOrder::EVENT_PAID),由监听器负责发短信、生成电子票、更新座位状态等副作用 - 所有状态变更逻辑不得出现在
beforeSave或模型方法中,否则无法区分是用户支付还是后台补单
电子票与核销:解耦生成与交付
“出票”不是单纯改个状态字段,而是生成可验证的电子凭证,并与真实座位绑定。
- 电子票数据(含唯一票号、座位号、场次 ID、防伪签名)单独存入
tickets表,与订单通过order_id关联 - 出票动作应在事务提交后执行(用
Transaction::onCommit()),避免订单回滚但票已生成 - 核销时需校验:票是否未使用、是否在有效期内、是否属于该场次、是否为本人购票(可结合用户 token 或实名信息)
- 核销成功后,同步更新座位状态为“已使用”,并触发
TicketOrder::EVENT_CHECKED_IN用于统计和通知
日志与审计:结构化、带上下文、事务安全
票务订单一旦出错,必须能快速还原“谁、何时、在哪、做了什么、参数是什么”。
- 日志统一写入
ticket_order_log表,字段至少包含:order_id、action(如'created'、'paid'、'checked_in')、user_id(操作人,非购票人)、ip、source(web/api/admin)、data(JSON 存原始参数,过滤敏感字段) - 日志写入必须放在事务提交后,推荐用
afterCommit回调注册,而不是afterSave - 避免只记 “状态变为2”,而应记录语义化变更:
{"from":"unpaid","to":"paid","payment_method":"wxpay"} - 关键操作(如退票、手工出票)建议增加操作人二次确认或审批流,日志中记录审批节点











