Commit Graph

41 Commits

Author SHA1 Message Date
wangjia 6427e8c848 feat(v2): reconcile Runner 骨架 + 订单级过期清理(超 TTL pending 单/零尝试孤儿单自动关闭) 2026-07-10 17:19:38 +08:00
wangjia aa14204640 fix(v2): 超退守卫事务化 CreateRefundGuarded(锁单行+求和+插入单事务)+ sqlite DSN 补 _txlock=immediate
Task 3 计划的 RefundSum→比较→CreateRefund 两段式裸读写在并发下会超退(两个请求都读到
reserved=0、都通过、都插入)。仿 store/order.go::MarkAttemptPaid 的模式,把锁订单行
(clause.Locking FOR UPDATE,MySQL 真锁/SQLite 由 glebarez 静默丢弃)+ 求和 + 校验 + 插入
收进单个 s.db.Transaction。SQLite 侧真正的串行化靠 DSN _txlock=immediate(BEGIN IMMEDIATE
在事务开始就抢写锁)+ busy_timeout,补进 testdb.go 与 main.go 的 sqlite DSN 构造。
2026-07-10 17:08:25 +08:00
wangjia 338e28d386 feat(v2): RefundStore(建单/求和/条件翻转/人工待办)+ Order 退款态机(PaidAttempt/ApplyRefundToOrder) 2026-07-10 16:49:15 +08:00
wangjia 00e032f577 fix(v2): uq_delivery 索引显式升级迁移(AutoMigrate 不改同名索引)+ refund_id NOT NULL DEFAULT ''
AutoMigrate 只按名字判断索引是否存在,P2 时代库升级后同名 uq_delivery 仍停在
2 列,store.EnqueueDelivery 的 3 列 ON CONFLICT 每次调用(含普通 payment
webhook)都报 "ON CONFLICT clause does not match any PRIMARY KEY or UNIQUE
constraint"(reviewer 在 glebarez/sqlite 上实测复现)。新增
model.UpgradeWebhookDeliveryIndex,AutoMigrate 后显式检测/加宽索引 +
backfill refund_id,main.go 与 OpenTestDB 两处调用点接入;RefundID 补
not null;default:'' 补上 NULL/"" 语义缺口。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
2026-07-10 16:42:29 +08:00
wangjia ed5ec5c894 feat(v2): outbox 退款感知(refund_id 进唯一键)+ 投递门禁放行退款态 + Enqueue 贯通 refundID 2026-07-10 16:26:32 +08:00
wangjia c4fc962db2 chore(v2): P3 终审收尾——GetReserved 移 export_test / BuildRegistry 表测试 / 坏签名负路径 / 注释更正 2026-07-10 15:32:10 +08:00
wangjia c933657350 feat(v2): 配置驱动注册三渠道 adapter + crypto 真 adapter 全链 e2e(下单→查单→入账→webhook) 2026-07-10 15:17:43 +08:00
wangjia 79b9111a99 feat(v2): stripe adapter — Checkout Session 跳转 + webhook 验签(stripe-go/v79 pin) 2026-07-10 15:07:10 +08:00
wangjia d3050d25d8 feat(v2): alipay adapter — 迁移 v1 验签/查单,render_type=redirect(page/wap) 2026-07-10 14:53:03 +08:00
wangjia 527de1c93e fix(v2): crypto 预留键改按地址(对齐链上匹配维度)——防共享地址跨账户同额碰撞
- allocateAmount 中解析地址后改用 address+'/'+amount 作为预留键,而非 accountID
- 同一地址的不同账户不能同时占用同金额,链上唯一性保证对齐 Query 的 to==addr 维度
- 新增 GetReserved() 测试方法暴露预留表副本,验证键格式
- 新增 TestReservationKeyByAddress 测试:两个账户共享地址,验证预留键基于地址

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
2026-07-10 14:43:21 +08:00
wangjia fbd3bd6f36 feat(v2): crypto adapter — 移植 pangolin-pay 单地址+唯一金额模型(only_confirmed 扫链,VerifyCallback 不适用) 2026-07-10 14:33:21 +08:00
wangjia 98b95dde19 fix(v2): 币种错误映射 409/503(不再误导 500 重试)+ DailyLimit 量纲注释
- handler: ErrCurrencyMismatch → 409, ErrNoSettleCurrency → 503
- test: TestV2RetryCurrencyMismatch409 验证重试不同币种渠道返回 409
- config: DailyLimit 添加量纲注释(minor units)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
2026-07-10 14:27:39 +08:00
wangjia 585133532c feat(v2): 多币种 product(ProductPrice 分币价目 + v1 元回退)+ 结算币种由渠道能力驱动
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
2026-07-10 14:20:12 +08:00
wangjia 7a27a6223a feat(v2): order 补 biz_code + webhook payload 带 product_biz_code(收口 P2 延后项) 2026-07-10 14:12:44 +08:00
wangjia c06f6d3a3c feat(v2): gateway 经 Picker 路由选账户 + retry 换号(ExcludeAccounts);替换首个 enabled 2026-07-10 14:02:33 +08:00
wangjia 381288e707 docs(v2): Router.Pick 注明 weighted 桶序绑定 config 账户声明顺序(reorder=repin) 2026-07-10 13:56:03 +08:00
wangjia bd50a0d561 feat(v2): limit_aware 策略(UsageSource 留位)+ Router 按配置派发 + config.Routing 2026-07-10 13:55:16 +08:00
wangjia 875093dff2 fix(v2): weighted 零权重测试改确定性断言(去抽样偏置)——套件回绿
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
2026-07-10 13:50:16 +08:00
wangjia 689de61e94 feat(v2): weighted 路由策略(按 Weight,hash(OutTradeNo) 确定性) 2026-07-10 13:46:05 +08:00
wangjia d78945b09a feat(v2): 账户路由 Picker/Strategy 接口 + round_robin 策略(替换首个 enabled) 2026-07-10 13:36:31 +08:00
wangjia 764ed558a1 chore(v2): 终审收尾——settle-sync 非常态结果落日志 + Retry 补 body 上限
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
2026-07-10 11:51:33 +08:00
wangjia 6cbaa59db7 fix(v2): 回调按 SettleResult 分终态/可重试——amount_mismatch 回 200 停投,不再无限重投 2026-07-10 11:44:14 +08:00
wangjia a633b2ac0a feat(v2): /api/v2 网关路由 + handler(下单/查/重试/取消/回调)+ main 装配 gateway/notifier 2026-07-10 11:36:47 +08:00
wangjia 63af44bfe1 fix(v2): webhook 截断 UTF-8 安全 + 投递门禁查库失败落日志(与未付区分)
- MarkFailed: truncateUTF8 避免截断中点 UTF-8 rune, last_error 安全 <=255B
- notifier truncate: 同上, 投递失败消息截断 UTF-8 安全
- deliverOne: 拆分 orderPaid 错误分支 — DB 错误落日志 [webhook] 投递门禁查单失败,与"订单未付"(静默)区分
- test: MarkFailed with long Chinese string, 验证 stored last_error 为有效 UTF-8 且 <=255B

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
2026-07-10 11:31:16 +08:00
wangjia 3f5e44581f feat(v2): webhook v2 outbox + Notifier(event_type + HMAC 双向 + 重试兜底) 2026-07-10 11:25:14 +08:00
wangjia 0877b01874 fix(v2): settle 定位尝试的瞬时读库错误归 SettleFailed(可重试),不再误标 not_found
AttemptByProviderRef 返回非哨兵错误(如 DB 抖动)时原先与"查无此单"终态
一并标 SettleNotFound;调用方按结果值决定 ack,会把已付订单永久丢弃。
非哨兵分支改判 SettleFailed(可重试,渠道会重投),errors.Is(ErrAttemptNotFound)
分支维持 SettleNotFound 不变。新增 TestSettleTransientReadErrorIsFailed 通过
关闭底层连接强制产出瞬时读库错误,验证结果与语义。
2026-07-10 11:20:53 +08:00
wangjia 68d162d6c4 feat(v2): 入账管线 Settle/HandleCallback/SyncPending(归一→定位→幂等→核对→标付→入队) 2026-07-10 11:15:10 +08:00
wangjia 0ab0713161 feat(v2): 一次性收款管线 CreateOrder/GetOrder/Retry/Cancel + DBProductResolver 2026-07-10 11:08:03 +08:00
wangjia 31e354e31c feat(v2): OrderStore 扩展 GetOrder/AttemptByProviderRef/ListByStatus/ExpirePending
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
2026-07-10 11:01:42 +08:00
wangjia 20ec7b4942 feat(v2): fake provider(确定性,验证收款管线;真渠道 P3) 2026-07-10 10:57:53 +08:00
wangjia 7d05c2f655 feat(v2): Provider 渠道抽象接口 + 6 render_type + PaidEvent + 注册表 2026-07-10 10:54:22 +08:00
wangjia af15f4736e fix(v2): MarkAttemptPaid 按单scope+校验attempt命中 + OpenTestDB 唯一库名
- attempt UPDATE 加 out_trade_no 过滤并检查 RowsAffected,防止 order 已翻
  paid 但 providerRef 错配导致没有对应 attempt 被标记的账本背离(未命中
  时返回 error 触发事务回滚,order 保持 pending)
- 补测试覆盖 providerRef 错配场景:断言返回 error 且回滚后订单仍 pending
- OpenTestDB 改为每次调用生成唯一内存库名(atomic 计数器),避免同包多
  测试共享 file::memory:?cache=shared 导致数据串扰

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 08:35:22 +08:00
wangjia 5aecb18d7c feat(v2): 账户配置注册表(config.accounts + env 取凭证 + 渠道/区域筛选) 2026-07-10 08:25:01 +08:00
wangjia 6a53d7e008 feat(v2): OrderStore 建单/建尝试/幂等标付/取消/列表(条件UPDATE+RowsAffected) 2026-07-10 08:21:02 +08:00
wangjia 01887dd571 feat(v2): Order/Attempt/Account/Refund 模型 + 状态枚举 + :memory: 测试约定
v2 OrderStatus 三个值(Pending/Paid/Refunded)与 v1 model.OrderStatus 撞名,
统一加 V2 后缀(OrderPendingV2 等)规避包内重名;AttemptStatus/RefundStatus
无此冲突,原样保留。
2026-07-10 08:14:27 +08:00
wangjia 7da15c00a1 fix(v2): money 去 float 进制 + 溢出防护 + 补 Format/负数/溢出测试
- money.go: 用纯 int64 循环乘法替代 int64(math.Pow10(exp)) 计算进制因子
- Parse: whole*mult+frac 前加溢出检查(math.MaxInt64 仅作整型常量参与比较,无 float 参与)
- money_test.go: 补 Format 未知币种错误分支(errors.Is ErrUnknownCurrency)、
  Parse 溢出(整数部分过大)、负数往返测试
2026-07-10 08:10:36 +08:00
wangjia 8555368785 feat(v2): money 包 int64 最小单位 + 币种码(CNY/USD/USDT) 2026-07-10 08:05:14 +08:00
wangjia dbdadd1b10 feat(order): 下单支持显式 client_type 端型(契约 v1.1.0)
resolveIsMobile:mobile/pc 显式优先,空值回退 isMobileUA(请求 UA),非法 400;
修复业务方服务端下单被 Go client UA 误判为 PC、手机用户拿不到 wap 拉起页

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
2026-07-04 10:28:17 +08:00
wangjia e0c7baa7cb fix(sec): 安全评审加固——goroutine panic 恢复 + 下单 body 上限 + 错误脱敏
- 后台 goroutine (notifyBiz/SyncPending) 加 defer recover,防 panic 拖垮进程
- 下单请求体 64KB 上限 (MaxBytesReader),防超大 body 内存 DoS
- 下单错误不再回传原始 err.Error(),内部详情只进日志
- (另: pay nginx 加 per-IP 限流 limit_req,服务器侧配置)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019UQmqWmV67sXGLrb3U1XXn
2026-07-03 23:53:11 +08:00
wangjia fd5f56f7de feat: 微信V3 + 支付宝手机站/UA自适应 + 业务对接(签名下单/webhook) + 通用多业务
- 微信支付 V3 Native 渠道 (wechat.go):Native下单/回调AES-GCM解密验签/查单
- 支付宝:手机网站支付 wap.pay + 按 UA 自适应(PC page.pay扫码 / 手机拉App);qr_pay_mode=2 完整扫码收银台
- 业务对接:下单接口扩展(biz_system/biz_ref/return_url)+ HMAC 签名鉴权;支付成功 webhook 主动推送业务方 + 60s 重试 + BizNotifyLog
- 通用多业务:config.biz 改 map,加业务只改配置(BIZ_<SYS>_SECRET/_CALLBACK_URL)
- seedPlans:四档真实套餐 + promo_first_month(¥1) + test_liandiao(0.01),均挂 biz_code;/products 暴露 biz_code
- 删除沙箱 pay.html;对接设计文档入 docs

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019UQmqWmV67sXGLrb3U1XXn
2026-07-03 22:44:34 +08:00
wangjia 6da6964451 初始提交:岩美 pay 收款服务(支付宝当面付 + 多商户多渠道架构)
- Go/Gin/GORM + 纯 Go SQLite(无 cgo)
- Channel 多渠道接口:支付宝当面付(precreate)/电脑网站支付(page.pay) 已实现,微信占位
- 多商户 merchants 表,回调验签+金额核对+幂等+查单兜底
- 收款页/结果页/二维码端点;docs/ 设计文档与部署 Runbook
- 密钥走环境变量/Bitwarden,不入库

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 07:21:48 +08:00