feat: 为订单表添加 app_account_token 字段并增强 Apple IAP 对账逻辑,支持通过交易历史记录查找。
Build docker and publish / build (20.15.1) (push) Successful in 7m25s
Build docker and publish / build (20.15.1) (push) Successful in 7m25s
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/perfect-panel/server/internal/model/order"
|
||||
"github.com/perfect-panel/server/internal/model/payment"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
iapapple "github.com/perfect-panel/server/pkg/iap/apple"
|
||||
@@ -52,44 +53,112 @@ func (l *ReconcileLogic) reconcile(ctx context.Context, minAge, maxAge time.Dura
|
||||
logger.Infof("[IAPReconcile] found %d pending IAP orders (requireTradeNo=%v)", len(orders), requireTradeNo)
|
||||
|
||||
for _, ord := range orders {
|
||||
if ord.TradeNo == "" {
|
||||
continue
|
||||
}
|
||||
// 用 originalTransactionId(即 trade_no)向 Apple Server API 查询交易详情
|
||||
jws, e := iapapple.GetTransactionInfo(*apiCfg, ord.TradeNo)
|
||||
if e != nil {
|
||||
logger.Errorf("[IAPReconcile] GetTransactionInfo error: orderNo=%s tradeNo=%s err=%v",
|
||||
ord.OrderNo, ord.TradeNo, e)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
// 解析 JWS(不校验证书链,只读取 payload)
|
||||
txPayload, e := iapapple.ParseTransactionJWS(jws)
|
||||
if e != nil {
|
||||
logger.Errorf("[IAPReconcile] ParseTransactionJWS error: orderNo=%s err=%v", ord.OrderNo, e)
|
||||
continue
|
||||
}
|
||||
if txPayload.RevocationDate != nil {
|
||||
// 苹果已撤销交易 → 关闭订单
|
||||
logger.Infof("[IAPReconcile] transaction revoked, closing order: %s", ord.OrderNo)
|
||||
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 内部有幂等保护)
|
||||
payload, _ := json.Marshal(queueTypes.ForthwithActivateOrderPayload{OrderNo: ord.OrderNo})
|
||||
task := asynq.NewTask(queueTypes.ForthwithActivateOrder, payload)
|
||||
if _, e = l.svc.Queue.EnqueueContext(ctx, task); e != nil {
|
||||
logger.Errorf("[IAPReconcile] enqueue activate error: orderNo=%s err=%v", ord.OrderNo, e)
|
||||
} else {
|
||||
logger.Infof("[IAPReconcile] enqueued activate: %s", ord.OrderNo)
|
||||
if ord.TradeNo != "" {
|
||||
// 有 trade_no:直接用 transactionId 查询 Apple Server API
|
||||
l.reconcileByTradeNo(ctx, apiCfg, ord)
|
||||
} else if ord.AppAccountToken != "" {
|
||||
// trade_no 为空但有 app_account_token:兜底查询
|
||||
l.reconcileByAppAccountToken(ctx, apiCfg, ord)
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond) // 避免 Apple API 限频
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// reconcileByTradeNo 通过 trade_no (originalTransactionId) 直接查询 Apple 交易状态
|
||||
func (l *ReconcileLogic) reconcileByTradeNo(ctx context.Context, apiCfg *iapapple.ServerAPIConfig, ord *order.Order) {
|
||||
jws, e := iapapple.GetTransactionInfo(*apiCfg, ord.TradeNo)
|
||||
if e != nil {
|
||||
logger.Errorf("[IAPReconcile] GetTransactionInfo error: orderNo=%s tradeNo=%s err=%v",
|
||||
ord.OrderNo, ord.TradeNo, e)
|
||||
return
|
||||
}
|
||||
txPayload, e := iapapple.ParseTransactionJWS(jws)
|
||||
if e != nil {
|
||||
logger.Errorf("[IAPReconcile] ParseTransactionJWS error: orderNo=%s err=%v", ord.OrderNo, e)
|
||||
return
|
||||
}
|
||||
if txPayload.RevocationDate != nil {
|
||||
logger.Infof("[IAPReconcile] transaction revoked, closing order: %s", ord.OrderNo)
|
||||
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)
|
||||
}
|
||||
return
|
||||
}
|
||||
// 正常已付款交易 → enqueue 激活
|
||||
payload, _ := json.Marshal(queueTypes.ForthwithActivateOrderPayload{OrderNo: ord.OrderNo})
|
||||
task := asynq.NewTask(queueTypes.ForthwithActivateOrder, payload)
|
||||
if _, e = l.svc.Queue.EnqueueContext(ctx, task); e != nil {
|
||||
logger.Errorf("[IAPReconcile] enqueue activate error: orderNo=%s err=%v", ord.OrderNo, e)
|
||||
} else {
|
||||
logger.Infof("[IAPReconcile] enqueued activate: %s", ord.OrderNo)
|
||||
}
|
||||
}
|
||||
|
||||
// reconcileByAppAccountToken 兜底:trade_no 为空但有 app_account_token
|
||||
// 通过用户历史交易 ID 查询 Apple 交易历史,匹配 appAccountToken 找到对应交易
|
||||
func (l *ReconcileLogic) reconcileByAppAccountToken(ctx context.Context, apiCfg *iapapple.ServerAPIConfig, ord *order.Order) {
|
||||
// 1. 获取该用户最近的已知交易 ID
|
||||
lastTx, err := l.svc.IAPAppleTransactionModel.FindLatestByUserId(ctx, ord.UserId)
|
||||
if err != nil || lastTx == nil || lastTx.OriginalTransactionId == "" {
|
||||
logger.Infof("[IAPReconcile-Fallback] no historical transaction found for user %d, orderNo=%s, skip",
|
||||
ord.UserId, ord.OrderNo)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Infof("[IAPReconcile-Fallback] searching Apple history for orderNo=%s appAccountToken=%s using transactionId=%s",
|
||||
ord.OrderNo, ord.AppAccountToken, lastTx.OriginalTransactionId)
|
||||
|
||||
// 2. 查询 Apple 交易历史
|
||||
jwsList, err := iapapple.GetTransactionHistory(*apiCfg, lastTx.OriginalTransactionId)
|
||||
if err != nil {
|
||||
logger.Errorf("[IAPReconcile-Fallback] GetTransactionHistory error: orderNo=%s err=%v", ord.OrderNo, err)
|
||||
return
|
||||
}
|
||||
|
||||
// 3. 在交易历史中查找匹配 appAccountToken 的交易
|
||||
for _, jws := range jwsList {
|
||||
txPayload, e := iapapple.ParseTransactionJWS(jws)
|
||||
if e != nil {
|
||||
continue
|
||||
}
|
||||
if txPayload.AppAccountToken != ord.AppAccountToken {
|
||||
continue
|
||||
}
|
||||
// 匹配成功!
|
||||
logger.Infof("[IAPReconcile-Fallback] MATCHED! orderNo=%s appAccountToken=%s transactionId=%s",
|
||||
ord.OrderNo, ord.AppAccountToken, txPayload.TransactionId)
|
||||
|
||||
if txPayload.RevocationDate != nil {
|
||||
logger.Infof("[IAPReconcile-Fallback] transaction revoked, closing order: %s", ord.OrderNo)
|
||||
if closeErr := l.svc.OrderModel.UpdateOrderStatus(ctx, ord.OrderNo, 3); closeErr != nil {
|
||||
logger.Errorf("[IAPReconcile-Fallback] close revoked order failed: orderNo=%s err=%v", ord.OrderNo, closeErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 更新 trade_no 为找到的 originalTransactionId
|
||||
if updateErr := l.svc.DB.Model(&order.Order{}).
|
||||
Where("order_no = ?", ord.OrderNo).
|
||||
Update("trade_no", txPayload.OriginalTransactionId).Error; updateErr != nil {
|
||||
logger.Errorf("[IAPReconcile-Fallback] update trade_no error: orderNo=%s err=%v", ord.OrderNo, updateErr)
|
||||
}
|
||||
|
||||
// enqueue 激活
|
||||
payload, _ := json.Marshal(queueTypes.ForthwithActivateOrderPayload{OrderNo: ord.OrderNo})
|
||||
task := asynq.NewTask(queueTypes.ForthwithActivateOrder, payload)
|
||||
if _, e := l.svc.Queue.EnqueueContext(ctx, task); e != nil {
|
||||
logger.Errorf("[IAPReconcile-Fallback] enqueue activate error: orderNo=%s err=%v", ord.OrderNo, e)
|
||||
} else {
|
||||
logger.Infof("[IAPReconcile-Fallback] enqueued activate: %s", ord.OrderNo)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
logger.Infof("[IAPReconcile-Fallback] no matching transaction found for orderNo=%s appAccountToken=%s (checked %d transactions)",
|
||||
ord.OrderNo, ord.AppAccountToken, len(jwsList))
|
||||
}
|
||||
|
||||
// loadAppleAPIConfig 从 payment 表读取第一个启用的 Apple IAP 支付方式配置
|
||||
func (l *ReconcileLogic) loadAppleAPIConfig(ctx context.Context) (*iapapple.ServerAPIConfig, error) {
|
||||
pays, err := l.svc.PaymentModel.FindListByPlatform(ctx, pkgpayment.AppleIAP.String())
|
||||
|
||||
Reference in New Issue
Block a user