yii框架虽无内置优惠券系统,但凭借activerecord、事件机制等可在2–3天实现核心功能;需拆分ec_coupon_template与ec_user_coupon两张表,严格校验、事务控制、策略化计算及gii精简优化。

Yii 框架本身不内置优惠券系统,但它的 ActiveRecord、事件机制、服务层抽象和 Gii 代码生成能力,能让优惠券功能在 2–3 天内跑通核心链路。关键不是“有没有”,而是怎么组织模型、验证逻辑和状态流转。
优惠券模板与用户券必须分离建模
直接在 ec_coupon 表里存所有用户领取记录,很快会遇到查询慢、连表多、模板变更影响历史券的问题。正确做法是拆成两张表:
-
ec_coupon_template:存规则(min_amount、discount_type、valid_start_time、total_count等),只由运营后台操作 -
ec_user_coupon:存用户维度实例(user_id、template_id、status、used_at),领取时 copy 必要字段(如coupon_amount、coupon_name)避免后期模板改了导致已发券失效
Yii2 的 ActiveRecord::find()->with() 能轻松处理关联,但别在列表页查全部字段——用 select() 显式指定需要的冗余字段,比如 select('id, coupon_name, coupon_amount, status')。
核销前必须做四重校验(缺一不可)
常见错误是只查 status = 'unused' 就放行,结果出现超领、过期核销、跨店使用等线上事故。实际核销入口(比如订单提交时)应顺序执行:
- 查
ec_user_coupon存在且status === 'unused' - 查对应
ec_coupon_template的valid_end_time >= NOW()和total_used_count - 检查当前订单金额是否满足
min_amount(注意:不能只用前端传的,得从数据库重新取) - 用 Yii2 的事务 +
updateCounters(['total_used_count' => 1])原子更新模板使用计数,再改用户券状态为'used'
漏掉事务或顺序错乱,就可能让同一张券被并发核销两次。
购物车实时计算优惠时别硬编码折扣逻辑
很多团队把“满 100 减 10”写死在控制器里,后续加阶梯满减、N 选 1、叠加限制就全崩。正确方式是把计算抽成独立服务:
- 定义
CouponCalculator类,方法如calculateDiscount($cartItems, $userCoupons) - 每个优惠券模板配置一个
calculation_rule字段(值如'fixed_amount'或'percentage'),运行时 switch 分支调用不同策略 - 在购物车
computeCartInfo()中注入该服务,而非在视图里 if-else
这样新加“第二件半价”规则,只需新增一个策略类,不用动购物车主流程。
Gii 生成基础 CRUD 后,立刻删掉无用代码
Gii 生成的 CouponTemplateController 默认带 index/view/create/update/delete 全套动作,但运营后台往往只要 create 和 update,且 delete 必须是软删(is_deleted = 1)。别留着没用的路由和视图——它们会成为安全扫描器盯上的入口点。
同样,CouponTemplateSearch 模型里默认生成的 rules() 会包含所有字段验证,但 total_count 允许为空(表示不限量),valid_start_time 和 valid_end_time 必须成对存在——这些约束得手动补上,Gii 不会猜业务语义。
最易被忽略的是时间字段的时区处理:valid_start_time 存的是 UTC 还是本地时间?Yii2 默认用 PHP 配置的 date.timezone,但数据库连接层(PDO)可能另有设置。上线前务必在测试环境用 SELECT NOW() 和 Yii::$app->formatter->asDatetime(time()) 对齐,否则凌晨三点发的券,可能被当成昨天的过期券。











