Uses per Customer依赖customer_id统计,游客没有customer_id,单靠这个字段不能实现“同一邮箱一次”。如果业务真的要限制游客,需要在下单前用归一化邮箱与已下单记录做额外校验。
排查记录
我保留了一个失败样本,并用同样输入在无缓存或单节点条件下再跑一遍。这样可以把前端假象、缓存和真正的数据问题分开。
我先看了这几处
- 确认规则同时设置Uses per Coupon全局上限,避免无限使用。
- 检查历史订单是否允许取消后恢复次数,先定义业务口径。
mysql -e "SELECT coupon_id,customer_id,times_used FROM salesrule_customer WHERE rule_id=12;" mysql -e "SELECT customer_email,COUNT(*) c FROM sales_order WHERE coupon_code='WELCOME10' GROUP BY LOWER(customer_email) HAVING c>1;" grep -R "sales_quote_collect_totals_before|placeOrder" app/code/Vendor -n
日志取样要带上请求时间、Store View、实体ID和部署版本。只截一行异常往往不够,至少保留错误前后几十行,才能判断是入口参数还是后续状态被改写。
处理办法
在提交订单的服务层对trim+小写后的邮箱做查询,并把coupon_code、email_hash、order_id写入带唯一索引的自定义使用表;在同一业务事务中抢占记录,冲突则返回明确提示。敏感邮箱只存带密钥HMAC,不用裸SHA。取消订单是否释放由状态观察者按规则处理。
修改范围
- 先在单个SKU、单个订单或单个Store验证。
- 保留可回退的旧配置,不覆盖原始证据。
- 需要批量处理时分批提交并输出成功/失败清单。
回归时别只刷新页面
- 同邮箱并发两次只有一次成功
- 大小写和首尾空格不能绕过
- 注册用户原有次数统计仍正常
- 取消与退款按既定口径处理
验收不只看后台提示“成功”。我还会从前台/API再走一次真实链路,并在下一轮cron、索引或缓存命中后重复检查,确认不是短暂正常。
补记
只在前端JS检查可以被直接调用API绕过,必须服务端兜底。

