fix(backend): 特惠限次 webhook 兜底——第二笔已支付特惠单只标 paid 不叠加时长

按 pay-contract 看板要求:续期入账前事务内再查本店是否已有另一笔 paid 的
promo_first_month(防绕过前端/并发双买);命中则跳过 entitle、记 ALERT 告警,
回调仍回 SUCCESS(钱已收,退款人工处理)。补对应测试(双买不叠加/正常套餐不受影响)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
This commit is contained in:
wangjia
2026-07-03 22:28:21 +08:00
parent b3c9d98eae
commit a996593b1c
2 changed files with 61 additions and 2 deletions
+20 -2
View File
@@ -279,8 +279,26 @@ func (s *PayService) settle(outTradeNo, bizCode, amount, tradeNo, channel string
if !ok {
return ErrUnknownPlan
}
if err := entitle(tx, p.ShopID, plan); err != nil {
return err
// 特惠限次 webhook 兜底(防绕过前端/并发双买):入账前事务内再查一次,
// 本店已有另一笔已支付特惠单 → 本单只标 paid 不叠加时长,记告警(契约 INTEGRATION-BOARD 要求)
entitleOK := true
if p.ProductBizCode == PromoBizCode {
var dup int64
if err := tx.Model(&model.LicensePurchase{}).
Where("shop_id = ? AND product_biz_code = ? AND status = ? AND id <> ?",
p.ShopID, PromoBizCode, "paid", p.ID).
Count(&dup).Error; err != nil {
return err
}
if dup > 0 {
entitleOK = false
log.Printf("[pay] ALERT promo double-claim out_trade_no=%s shop=%d:本店已享受过首月特惠,本单不叠加时长", outTradeNo, p.ShopID)
}
}
if entitleOK {
if err := entitle(tx, p.ShopID, plan); err != nil {
return err
}
}
shopID = p.ShopID
return tx.Model(&model.LicensePurchase{}).Where("id = ?", p.ID).Updates(map[string]any{