fix: retry critical writes and allow super login

This commit is contained in:
2026-01-08 17:37:42 +08:00
parent 8ac82aaeb0
commit 3c159a2e0f
4 changed files with 233 additions and 156 deletions

View File

@@ -247,106 +247,110 @@ func (s *order) payWithBalance(ctx context.Context, o *models.Order) (*transacti
func (s *order) settleOrder(ctx context.Context, o *models.Order, method, externalID string) error {
var tenantOwnerID int64
err := models.Q.Transaction(func(tx *models.Query) error {
// 1. Handle Balance Updates
if o.Type == consts.OrderTypeRecharge {
// Income: Recharge (Credit User Balance)
_, err := tx.User.WithContext(ctx).
Where(tx.User.ID.Eq(o.UserID)).
Update(tx.User.Balance, gorm.Expr("balance + ?", o.AmountPaid))
if err != nil {
return err
// 关键结算事务:遇到数据库冲突/死锁时短暂退避重试,避免支付状态卡死。
err := retryCriticalWrite(ctx, func() error {
tenantOwnerID = 0
return models.Q.Transaction(func(tx *models.Query) error {
// 1. Handle Balance Updates
if o.Type == consts.OrderTypeRecharge {
// Income: Recharge (Credit User Balance)
_, err := tx.User.WithContext(ctx).
Where(tx.User.ID.Eq(o.UserID)).
Update(tx.User.Balance, gorm.Expr("balance + ?", o.AmountPaid))
if err != nil {
return err
}
} else if method == "balance" {
// Expense: Purchase with Balance (Deduct User Balance)
info, err := tx.User.WithContext(ctx).
Where(tx.User.ID.Eq(o.UserID), tx.User.Balance.Gte(o.AmountPaid)).
Update(tx.User.Balance, gorm.Expr("balance - ?", o.AmountPaid))
if err != nil {
return err
}
if info.RowsAffected == 0 {
return errorx.ErrQuotaExceeded.WithMsg("余额不足")
}
}
} else if method == "balance" {
// Expense: Purchase with Balance (Deduct User Balance)
info, err := tx.User.WithContext(ctx).
Where(tx.User.ID.Eq(o.UserID), tx.User.Balance.Gte(o.AmountPaid)).
Update(tx.User.Balance, gorm.Expr("balance - ?", o.AmountPaid))
if err != nil {
return err
}
if info.RowsAffected == 0 {
return errorx.ErrQuotaExceeded.WithMsg("余额不足")
}
}
// 2. Update Order Status
now := time.Now()
// snapshot := o.Snapshot // Preserve existing snapshot or update it with external ID
// TODO: Update snapshot with payment info
_, err := tx.Order.WithContext(ctx).Where(tx.Order.ID.Eq(o.ID)).Updates(&models.Order{
Status: consts.OrderStatusPaid,
PaidAt: now,
UpdatedAt: now,
// 2. Update Order Status
now := time.Now()
// snapshot := o.Snapshot // Preserve existing snapshot or update it with external ID
// TODO: Update snapshot with payment info
_, err := tx.Order.WithContext(ctx).Where(tx.Order.ID.Eq(o.ID)).Updates(&models.Order{
Status: consts.OrderStatusPaid,
PaidAt: now,
UpdatedAt: now,
})
if err != nil {
return err
}
// 3. Grant Content Access
items, _ := tx.OrderItem.WithContext(ctx).Where(tx.OrderItem.OrderID.Eq(o.ID)).Find()
for _, item := range items {
// Check if access already exists (idempotency)
exists, _ := tx.ContentAccess.WithContext(ctx).
Where(tx.ContentAccess.UserID.Eq(o.UserID), tx.ContentAccess.ContentID.Eq(item.ContentID)).
Exists()
if exists {
continue
}
access := &models.ContentAccess{
TenantID: item.TenantID,
UserID: o.UserID,
ContentID: item.ContentID,
OrderID: o.ID,
Status: consts.ContentAccessStatusActive,
}
if err := tx.ContentAccess.WithContext(ctx).Save(access); err != nil {
return err
}
}
// 4. Create Tenant Ledger (Revenue) - Only for Content Purchase
if o.Type == consts.OrderTypeContentPurchase {
t, err := tx.Tenant.WithContext(ctx).Where(tx.Tenant.ID.Eq(o.TenantID)).First()
if err != nil {
return err
}
tenantOwnerID = t.UserID
// Calculate Commission
amount := o.AmountPaid
fee := int64(float64(amount) * 0.10)
creatorIncome := amount - fee
// Credit Tenant Owner Balance (Net Income)
_, err = tx.User.WithContext(ctx).
Where(tx.User.ID.Eq(tenantOwnerID)).
Update(tx.User.Balance, gorm.Expr("balance + ?", creatorIncome))
if err != nil {
return err
}
ledger := &models.TenantLedger{
TenantID: o.TenantID,
UserID: t.UserID, // Owner
OrderID: o.ID,
Type: consts.TenantLedgerTypeDebitPurchase, // Income from purchase
Amount: creatorIncome,
BalanceBefore: 0, // TODO
BalanceAfter: 0, // TODO
FrozenBefore: 0,
FrozenAfter: 0,
IdempotencyKey: uuid.NewString(),
Remark: "内容销售收入 (扣除平台费)",
OperatorUserID: o.UserID,
}
if err := tx.TenantLedger.WithContext(ctx).Create(ledger); err != nil {
return err
}
}
return nil
})
if err != nil {
return err
}
// 3. Grant Content Access
items, _ := tx.OrderItem.WithContext(ctx).Where(tx.OrderItem.OrderID.Eq(o.ID)).Find()
for _, item := range items {
// Check if access already exists (idempotency)
exists, _ := tx.ContentAccess.WithContext(ctx).
Where(tx.ContentAccess.UserID.Eq(o.UserID), tx.ContentAccess.ContentID.Eq(item.ContentID)).
Exists()
if exists {
continue
}
access := &models.ContentAccess{
TenantID: item.TenantID,
UserID: o.UserID,
ContentID: item.ContentID,
OrderID: o.ID,
Status: consts.ContentAccessStatusActive,
}
if err := tx.ContentAccess.WithContext(ctx).Save(access); err != nil {
return err
}
}
// 4. Create Tenant Ledger (Revenue) - Only for Content Purchase
if o.Type == consts.OrderTypeContentPurchase {
t, err := tx.Tenant.WithContext(ctx).Where(tx.Tenant.ID.Eq(o.TenantID)).First()
if err != nil {
return err
}
tenantOwnerID = t.UserID
// Calculate Commission
amount := o.AmountPaid
fee := int64(float64(amount) * 0.10)
creatorIncome := amount - fee
// Credit Tenant Owner Balance (Net Income)
_, err = tx.User.WithContext(ctx).
Where(tx.User.ID.Eq(tenantOwnerID)).
Update(tx.User.Balance, gorm.Expr("balance + ?", creatorIncome))
if err != nil {
return err
}
ledger := &models.TenantLedger{
TenantID: o.TenantID,
UserID: t.UserID, // Owner
OrderID: o.ID,
Type: consts.TenantLedgerTypeDebitPurchase, // Income from purchase
Amount: creatorIncome,
BalanceBefore: 0, // TODO
BalanceAfter: 0, // TODO
FrozenBefore: 0,
FrozenAfter: 0,
IdempotencyKey: uuid.NewString(),
Remark: "内容销售收入 (扣除平台费)",
OperatorUserID: o.UserID,
}
if err := tx.TenantLedger.WithContext(ctx).Create(ledger); err != nil {
return err
}
}
return nil
})
if err != nil {
return err