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>
This commit is contained in:
@@ -47,10 +47,14 @@ func (s *OrderStore) MarkAttemptPaid(outTradeNo, channel, providerRef string, at
|
||||
if res.RowsAffected == 0 {
|
||||
return nil // 非 pending → 幂等 no-op
|
||||
}
|
||||
if err := tx.Model(&model.Attempt{}).
|
||||
Where("channel = ? AND provider_ref = ?", channel, providerRef).
|
||||
Updates(map[string]any{"status": model.AttemptPaid, "paid_at": at}).Error; err != nil {
|
||||
return err
|
||||
ares := tx.Model(&model.Attempt{}).
|
||||
Where("out_trade_no = ? AND channel = ? AND provider_ref = ?", outTradeNo, channel, providerRef).
|
||||
Updates(map[string]any{"status": model.AttemptPaid, "paid_at": at})
|
||||
if ares.Error != nil {
|
||||
return ares.Error
|
||||
}
|
||||
if ares.RowsAffected == 0 {
|
||||
return fmt.Errorf("store.MarkAttemptPaid: order %s flipped paid but no matching attempt (%s/%s)", outTradeNo, channel, providerRef)
|
||||
}
|
||||
flipped = true
|
||||
return nil
|
||||
|
||||
@@ -61,3 +61,25 @@ func TestMarkPaidIdempotentAndCancel(t *testing.T) {
|
||||
t.Fatalf("canceled 单应出现在列表")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkAttemptPaidProviderRefMismatchRollsBack(t *testing.T) {
|
||||
db := model.OpenTestDB(t)
|
||||
s := store.NewOrderStore(db)
|
||||
seedOrder(t, s, "PAY-X")
|
||||
|
||||
ok, err := s.MarkAttemptPaid("PAY-X", "crypto", "P-WRONG", time.Now())
|
||||
if err == nil {
|
||||
t.Fatalf("providerRef 错配应返回 error, got ok=%v err=%v", ok, err)
|
||||
}
|
||||
if ok {
|
||||
t.Fatalf("providerRef 错配不应返回 ok=true")
|
||||
}
|
||||
|
||||
var o model.OrderV2
|
||||
if err := db.Where("out_trade_no = ?", "PAY-X").First(&o).Error; err != nil {
|
||||
t.Fatalf("query order: %v", err)
|
||||
}
|
||||
if o.Status != model.OrderPendingV2 {
|
||||
t.Fatalf("事务应回滚, order status 应仍为 pending, got %v", o.Status)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user