Commit Graph

5 Commits

Author SHA1 Message Date
wangjia a62a2b1797 fix(server/pay): promo 限购 settle 侧幂等复查 + 000027 部分唯一索引(TOCTOU)
ci-pangolin / Lint — shellcheck (pull_request) Successful in 11s
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (pull_request) Successful in 25s
ci-pangolin / Cleartext Scan — Android 禁明文 (pull_request) Successful in 19s
ci-pangolin / OpenAPI Sync Check (pull_request) Successful in 41s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (pull_request) Successful in 20s
ci-pangolin / Codegen Drift — token 生成物未漂移 (pull_request) Successful in 4s
ci-pangolin / DS-flow — 原型/跨端同源/代码色单源闸 (pull_request) Successful in 5s
ci-pangolin / Go — build + test (pull_request) Failing after 14s
ci-pangolin / E2E Smoke — L4 进程级端到端 (pull_request) Failing after 11s
ci-pangolin / Go — integration (mysql/redis testcontainers) (pull_request) Failing after 4m44s
ci-pangolin / Golden — 视觉回归 (全量:components/auth/desktop/tablet) (pull_request) Failing after 20s
ci-pangolin / Flutter — analyze + test (pull_request) Failing after 11m55s
CreateOrder 下单时的 HasPaidPurchase 只是裸 SELECT 无锁,并发/多挂起单可绕过
promo SKU「每账号限购一次」。两层修:
① webhook.go settle 在锁行、开通前对 item.Promo 的 SKU 复查一次(排除本单),
   命中说明另一笔同 user+SKU 订单已抢先 settle,跳过发放(不二次 +N 天)、
   仍 ack(否则 pay 无限重投)。新增 store.HasPaidPurchaseExcludingTx /
   MarkDuplicatePromoTx——重复单标记 canceled 而非 paid,避免自撞下面的
   唯一索引、也避免整笔 500 触发死循环重投。
② migration 000027(sqlite):部分唯一索引 ux_pay_promo_paid ON
   pay_purchases(user_id, sku) WHERE status='paid' AND sku='pro_month_promo',
   兜底防止任何路径把同一用户的 promo 单二次写成 paid。mysql 8 不支持部分
   索引,000027 mysql 侧是 no-op 占位(仅对齐编号),该场景 mysql 只靠①的
   应用层复查兜底——两库防线强度不同,已在迁移文件与代码注释中记录。

副作用:新增迁移把 sqlite 迁移顶点从 26 推到 27,同步更新
internal/store/sqlite_migrate_test.go 的版本断言,以及
internal/store/codes_lib_migrate_test.go 手动 Steps(-1) 序列(补一步跳过
000027,才能精确落在 000022 边界,这条测试是硬编码步数的)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 15:53:56 +08:00
wangjia e1e198e169 feat(server): 到账通知四接缝(邀请注册/首充/TG/购买开通)——与发放同事务零孤儿 2026-07-13 13:49:09 +08:00
wangjia de1117b0e6 feat(server/pay): 首充钩子 — settle 内接邀请首充段发奖(first_paid_at 幂等) 2026-07-13 07:41:15 +08:00
wangjia bc9aa38392 fix(server): pay webhook 兜底补行不再漏停用用户(修静默丢钱)+ 未知事件日志 + PAY_BIZ_SECRET 启动守卫
- webhook 兜底路径(台账缺行按 biz_ref 定位用户)去掉 status='active' 过滤,
  与正常路径(row.UserID 直接开通不看状态)对齐:钱已实收,不因用户被停用
  (banned)而拒绝补行开通,避免 500→pay 12 次重投后死信→静默丢钱。
- 未知 event_type 分支加 slog.Warn,便于将来 pay 侧误注册无 handler 的事件
  类型时能被观测到。
- PAY_BASE_URL 已设但 PAY_BIZ_SECRET 为空时 log.Fatal 拒绝启动,避免出站
  签名失败+入站验签全 401 的静默瘫痪。
- InsertFromWebhookTx 不再把 channel 冒充 method 写入台账(payload 无 method
  字段可复原,留空并加注释,消除台账观感误导)。

新增 TestWebhook_MissingLedgerFallsBackEvenWhenUserSuspended 覆盖 #1:
台账缺行 + 目标用户 banned 时,兜底补行仍成功开通并回 SUCCESS。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
2026-07-11 00:48:57 +08:00
wangjia 16b8823f1f feat(server): pay webhook 接收器(验签/时间窗/nonce + out_trade_no 幂等开通,回 SUCCESS)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
2026-07-10 22:38:01 +08:00