fix: IAP 支付流程优化与关键 bug 修复
Build docker and publish / build (20.15.1) (push) Successful in 7m42s

- getStatusLogic 类型断言修复(*user.User)
- restoreLogic 事务拆分为单条处理 + appAccountToken 解析
- attachTransactionLogic 提取 ParseProductIdDuration 共享函数
- 新增 config_helper.go 统一 Apple API 配置加载
- reconcileLogic 补充 BundleID 配置读取
- activateOrderLogic 邀请赠送天数逻辑完善

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-03-09 00:27:16 -07:00
parent dcfcd036de
commit 130fb702ab
9 changed files with 358 additions and 249 deletions
+3 -1
View File
@@ -72,7 +72,9 @@ func (l *ReconcileLogic) reconcile(ctx context.Context, minAge, maxAge time.Dura
if txPayload.RevocationDate != nil {
// 苹果已撤销交易 → 关闭订单
logger.Infof("[IAPReconcile] transaction revoked, closing order: %s", ord.OrderNo)
_ = l.svc.OrderModel.UpdateOrderStatus(ctx, ord.OrderNo, 3)
if closeErr := l.svc.OrderModel.UpdateOrderStatus(ctx, ord.OrderNo, 3); closeErr != nil {
logger.Errorf("[IAPReconcile] close revoked order failed: orderNo=%s err=%v", ord.OrderNo, closeErr)
}
continue
}
// 正常已付款交易 → enqueue 激活(activateOrderLogic 内部有幂等保护)
+71 -10
View File
@@ -191,7 +191,7 @@ func (l *ActivateOrderLogic) processOrderByType(ctx context.Context, orderInfo *
// finalizeCouponAndOrder handles post-processing tasks including coupon updates
// and order status finalization
func (l *ActivateOrderLogic) finalizeCouponAndOrder(ctx context.Context, orderInfo *order.Order) {
// Update coupon if exists
// Update coupon if exists (non-critical, logged but not blocking)
if orderInfo.Coupon != "" {
if err := l.svc.CouponModel.UpdateCount(ctx, orderInfo.Coupon); err != nil {
logger.WithContext(ctx).Error("Update coupon status failed",
@@ -201,14 +201,14 @@ func (l *ActivateOrderLogic) finalizeCouponAndOrder(ctx context.Context, orderIn
}
}
// Update order status
orderInfo.Status = OrderStatusFinished
if err := l.svc.OrderModel.Update(ctx, orderInfo); err != nil {
// Update order status using state-guarded UpdateOrderStatus to prevent double finalization
if err := l.svc.OrderModel.UpdateOrderStatus(ctx, orderInfo.OrderNo, OrderStatusFinished); err != nil {
logger.WithContext(ctx).Error("Update order status failed",
logger.Field("error", err.Error()),
logger.Field("order_no", orderInfo.OrderNo),
)
}
orderInfo.Status = OrderStatusFinished
}
// NewPurchase handles new subscription purchase including user creation,
@@ -594,10 +594,28 @@ func (l *ActivateOrderLogic) handleCommission(ctx context.Context, userInfo *use
// Use transaction for commission updates
err = l.svc.DB.Transaction(func(tx *gorm.DB) error {
referer.Commission += amount
if err = l.svc.UserModel.Update(ctx, referer, tx); err != nil {
return err
// Idempotency: check if commission log already exists for this order
var existingLogCount int64
if e := tx.Model(&log.SystemLog{}).
Where("type = ? AND object_id = ? AND content LIKE ?",
log.TypeCommission.Uint8(), referer.Id, fmt.Sprintf("%%\"%s\"%%", orderInfo.OrderNo)).
Count(&existingLogCount).Error; e != nil {
return e
}
if existingLogCount > 0 {
logger.WithContext(ctx).Info("Commission already processed, skip",
logger.Field("order_no", orderInfo.OrderNo),
logger.Field("referer_id", referer.Id),
)
return nil
}
// Atomic increment to prevent lost updates under concurrency
if e := tx.Model(&user.User{}).Where("id = ?", referer.Id).
UpdateColumn("commission", gorm.Expr("commission + ?", amount)).Error; e != nil {
return e
}
referer.Commission += amount
var commissionType uint16
switch orderInfo.Type {
@@ -605,6 +623,8 @@ func (l *ActivateOrderLogic) handleCommission(ctx context.Context, userInfo *use
commissionType = log.CommissionTypePurchase
case OrderTypeRenewal:
commissionType = log.CommissionTypeRenewal
default:
commissionType = log.CommissionTypePurchase
}
commissionLog := &log.Commission{
@@ -657,6 +677,23 @@ func (l *ActivateOrderLogic) grantGiftDays(ctx context.Context, u *user.User, da
if u == nil || days <= 0 {
return nil
}
// Idempotency: check if gift log already exists for this order + user
var existingLogCount int64
if e := l.svc.DB.Model(&log.SystemLog{}).
Where("type = ? AND object_id = ? AND content LIKE ?",
log.TypeGift.Uint8(), u.Id, fmt.Sprintf("%%\"%s\"%%", orderNo)).
Count(&existingLogCount).Error; e != nil {
return e
}
if existingLogCount > 0 {
logger.WithContext(ctx).Info("Gift days already granted, skip",
logger.Field("order_no", orderNo),
logger.Field("user_id", u.Id),
)
return nil
}
activeSubscribe, err := l.svc.UserModel.FindActiveSubscribe(ctx, u.Id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -946,10 +983,28 @@ func (l *ActivateOrderLogic) Recharge(ctx context.Context, orderInfo *order.Orde
// Update balance in transaction
err = l.svc.DB.Transaction(func(tx *gorm.DB) error {
userInfo.Balance += orderInfo.Price
if err = l.svc.UserModel.Update(ctx, userInfo, tx); err != nil {
return err
// Idempotency: check if balance log already exists for this order
var existingLogCount int64
if e := tx.Model(&log.SystemLog{}).
Where("type = ? AND object_id = ? AND content LIKE ?",
log.TypeBalance.Uint8(), userInfo.Id, fmt.Sprintf("%%\"%s\"%%", orderInfo.OrderNo)).
Count(&existingLogCount).Error; e != nil {
return e
}
if existingLogCount > 0 {
logger.WithContext(ctx).Info("Recharge already processed, skip",
logger.Field("order_no", orderInfo.OrderNo),
logger.Field("user_id", userInfo.Id),
)
return nil
}
// Atomic increment to prevent lost updates
if e := tx.Model(&user.User{}).Where("id = ?", userInfo.Id).
UpdateColumn("balance", gorm.Expr("balance + ?", orderInfo.Price)).Error; e != nil {
return e
}
userInfo.Balance += orderInfo.Price
balanceLog := &log.Balance{
Amount: orderInfo.Price,
@@ -1068,6 +1123,9 @@ func (l *ActivateOrderLogic) buildAdminNotificationData(orderInfo *order.Order,
// sendUserNotifyWithTelegram sends a notification message to a user via Telegram
func (l *ActivateOrderLogic) sendUserNotifyWithTelegram(chatId int64, text string) {
if l.svc.TelegramBot == nil {
return
}
msg := tgbotapi.NewMessage(chatId, text)
msg.ParseMode = "markdown"
if _, err := l.svc.TelegramBot.Send(msg); err != nil {
@@ -1077,6 +1135,9 @@ func (l *ActivateOrderLogic) sendUserNotifyWithTelegram(chatId int64, text strin
// sendAdminNotifyWithTelegram sends a notification message to all admin users via Telegram
func (l *ActivateOrderLogic) sendAdminNotifyWithTelegram(ctx context.Context, text string) {
if l.svc.TelegramBot == nil {
return
}
admins, err := l.svc.UserModel.QueryAdminUsers(ctx)
if err != nil {
logger.WithContext(ctx).Error("Query admin users failed", logger.Field("error", err.Error()))