会员积分变动须通过事件驱动、积分流水表和pointservice三位一体实现:所有变动写入member_point_log表并事务更新用户points字段,禁止直改;通过唯一source_type+source_id防重,异常时回滚并拒绝重试。

会员积分变动业务的核心是确保每次积分增减都可追溯、可回滚、事务安全,且与业务动作强关联(如下单、签到、退款)。Yii 框架下推荐采用“事件驱动 + 积分流水表 + 服务层封装”三位一体方案,避免在控制器或模型中直接操作积分字段。
积分变动必须走独立流水表
不要直接更新用户表的 points 字段。必须写入专用的积分流水表(如 member_point_log),包含关键字段:user_id、change_amount(正为增加,负为扣除)、balance_after(变动后余额,用于对账)、source_type(如 'order_pay'、'daily_sign')、source_id(关联订单ID或签到记录ID)、remark、created_at。
好处是:便于审计、支持按来源统计、方便做积分冻结/解冻、为后续积分有效期、过期清理留扩展空间。
用 Yii 事件解耦积分逻辑
在核心业务模型(如 Order、SignInRecord)中触发自定义事件,例如:
-
Order::EVENT_AFTER_PAY→ 触发“支付成功送积分” -
Order::EVENT_AFTER_REFUND→ 触发“退款扣回积分” -
SignInRecord::EVENT_AFTER_CREATE→ 触发“签到赠积分”
监听器统一放在 common/components/point/PointEventHandler.php 中,由 Service 层执行具体积分操作,控制器和模型不感知积分细节。
积分操作必须封装为原子服务方法
新建 common/services/PointService.php,提供明确语义的方法:
-
add($userId, $amount, $sourceType, $sourceId, $remark = ''):只增不减,自动校验参数 -
deduct($userId, $amount, $sourceType, $sourceId, $remark = ''):先查当前余额再扣,失败抛出InsufficientPointsException -
transfer($fromUserId, $toUserId, $amount, $sourceType, $sourceId):支持用户间转赠(需额外风控)
每个方法内部开启事务,先写流水,再更新用户总积分(通过 UPDATE ... SET points = points + ? WHERE id = ? 避免并发覆盖),最后刷新缓存(如 Redis 中的 user:{$id}:points)。
异常与幂等必须前置处理
积分变动不可重试失败,也不允许重复执行。建议:
- 所有积分操作前,用
$sourceType . '_' . $sourceId生成唯一业务键,插入前查member_point_log是否已存在该记录 - 退款场景下,需校验原订单是否已送积分、是否已扣回,避免二次扣减
- 日志表加联合唯一索引:
UNIQUE KEY `uk_source` (`source_type`, `source_id`) - 异步任务(如定时发放活动积分)需加分布式锁,防止重复投递
不复杂但容易忽略。











