map对齐
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 7m7s

This commit is contained in:
shanshanzhong 2026-03-07 07:16:01 -08:00
parent a0ae7b1c8d
commit 1e14e2dbd9
5 changed files with 286 additions and 35 deletions

View File

@ -6,10 +6,12 @@ import (
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os"
"strconv" "strconv"
"strings" "strings"
"time" "time"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/hibiken/asynq" "github.com/hibiken/asynq"
commonLogic "github.com/perfect-panel/server/internal/logic/common" commonLogic "github.com/perfect-panel/server/internal/logic/common"
@ -35,9 +37,10 @@ type AttachTransactionLogic struct {
} }
const ( const (
orderTypeSubscribe uint8 = 1 orderTypeSubscribe uint8 = 1
orderStatusPending uint8 = 1 orderStatusPending uint8 = 1
orderStatusPaid uint8 = 2 orderStatusPaid uint8 = 2
orderStatusFinished uint8 = 5
) )
func NewAttachTransactionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AttachTransactionLogic { func NewAttachTransactionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AttachTransactionLogic {
@ -88,6 +91,17 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "invalid jws") return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "invalid jws")
} }
l.Infow("JWS 验签成功", logger.Field("productId", txPayload.ProductId), logger.Field("originalTransactionId", txPayload.OriginalTransactionId), logger.Field("purchaseAt", txPayload.PurchaseDate)) l.Infow("JWS 验签成功", logger.Field("productId", txPayload.ProductId), logger.Field("originalTransactionId", txPayload.OriginalTransactionId), logger.Field("purchaseAt", txPayload.PurchaseDate))
tradeNoCandidates := l.getAppleTradeNoCandidates(txPayload)
if err = l.validateOrderTradeNoBinding(orderInfo, tradeNoCandidates); err != nil {
l.Errorw("Apple 交易重复绑定,拒绝处理", logger.Field("orderNo", req.OrderNo), logger.Field("tradeNoCandidates", tradeNoCandidates), logger.Field("error", err.Error()))
l.sendIAPAttachTraceToTelegram("REJECT_DUPLICATE_TRANSACTION", orderInfo, u.Id, orderInfo.SubscribeId, "", orderInfo.Quantity, txPayload.PurchaseDate, txPayload.TransactionId, txPayload.OriginalTransactionId, err.Error())
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "apple transaction already used")
}
tradeNo := ""
if len(tradeNoCandidates) > 0 {
tradeNo = tradeNoCandidates[0]
}
orderAlreadyBound := containsString(tradeNoCandidates, orderInfo.TradeNo)
// idempotency: check existing transaction by original id // idempotency: check existing transaction by original id
var existTx *iapmodel.Transaction var existTx *iapmodel.Transaction
existTx, _ = iapmodel.NewModel(l.svcCtx.DB, l.svcCtx.Redis).FindByOriginalId(l.ctx, txPayload.OriginalTransactionId) existTx, _ = iapmodel.NewModel(l.svcCtx.DB, l.svcCtx.Redis).FindByOriginalId(l.ctx, txPayload.OriginalTransactionId)
@ -203,6 +217,11 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest
} }
} }
exp := iapapple.CalcExpire(txPayload.PurchaseDate, duration) exp := iapapple.CalcExpire(txPayload.PurchaseDate, duration)
shouldAccumulate := duration > 0 && (strings.EqualFold(parsedUnit, "Day") || strings.EqualFold(txPayload.Type, "Consumable"))
accumulateDuration := int64(0)
if shouldAccumulate {
accumulateDuration = duration
}
l.Infow("计算订阅到期时间", logger.Field("expireAt", exp), logger.Field("expireUnix", exp.Unix())) l.Infow("计算订阅到期时间", logger.Field("expireAt", exp), logger.Field("expireUnix", exp.Unix()))
var orderLinkedSub *user.Subscribe var orderLinkedSub *user.Subscribe
if !isNewPurchaseOrder && orderInfo != nil && orderInfo.SubscribeToken != "" { if !isNewPurchaseOrder && orderInfo != nil && orderInfo.SubscribeToken != "" {
@ -239,60 +258,106 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest
} }
} }
existSub, existSubErr := l.findIAPSubscribeByOriginalTransactionID(txPayload.OriginalTransactionId)
if existSubErr != nil && !errors.Is(existSubErr, gorm.ErrRecordNotFound) {
l.Errorw("查询 IAP 订阅失败", logger.Field("error", existSubErr.Error()), logger.Field("originalTransactionId", txPayload.OriginalTransactionId))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find iap subscribe error: %v", existSubErr.Error())
}
if existTx != nil && existTx.Id > 0 { if existTx != nil && existTx.Id > 0 {
if orderAlreadyBound {
expiresAt := exp.Unix()
if existSub != nil && existSub.Id > 0 {
expiresAt = existSub.ExpireTime.Unix()
}
if orderLinkedSub != nil && orderLinkedSub.Id > 0 {
expiresAt = orderLinkedSub.ExpireTime.Unix()
}
if singleModeAnchorSub != nil && singleModeAnchorSub.Id > 0 {
expiresAt = singleModeAnchorSub.ExpireTime.Unix()
}
if bindErr := l.bindOrderTradeNo(orderInfo, tradeNo); bindErr != nil {
l.Errorw("回填订单交易号失败", logger.Field("orderNo", req.OrderNo), logger.Field("tradeNo", tradeNo), logger.Field("error", bindErr.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "bind order trade_no failed: %v", bindErr.Error())
}
if syncErr := l.syncOrderStatusAndEnqueue(orderInfo, 0); syncErr != nil {
l.Errorw("同订单幂等同步失败", logger.Field("orderNo", req.OrderNo), logger.Field("error", syncErr.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "sync order status failed: %v", syncErr.Error())
}
l.sendIAPAttachTraceToTelegram("IDEMPOTENT_SAME_ORDER", orderInfo, u.Id, subscribeId, tier, duration, txPayload.PurchaseDate, txPayload.TransactionId, txPayload.OriginalTransactionId, "")
l.Infow("事务已处理,同订单幂等返回", logger.Field("orderNo", req.OrderNo), logger.Field("expiresAt", expiresAt))
return &types.AttachAppleTransactionResponse{ExpiresAt: expiresAt, Tier: tier}, nil
}
if isNewPurchaseOrder { if isNewPurchaseOrder {
if bindErr := l.bindOrderTradeNo(orderInfo, tradeNo); bindErr != nil {
l.Errorw("写入订单交易号失败", logger.Field("orderNo", req.OrderNo), logger.Field("tradeNo", tradeNo), logger.Field("error", bindErr.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "bind order trade_no failed: %v", bindErr.Error())
}
if syncErr := l.syncOrderStatusAndEnqueue(orderInfo, 0); syncErr != nil { if syncErr := l.syncOrderStatusAndEnqueue(orderInfo, 0); syncErr != nil {
l.Errorw("事务已处理但同步订单状态失败", logger.Field("orderNo", req.OrderNo), logger.Field("error", syncErr.Error())) l.Errorw("事务已处理但同步订单状态失败", logger.Field("orderNo", req.OrderNo), logger.Field("error", syncErr.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "sync order status failed: %v", syncErr.Error()) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "sync order status failed: %v", syncErr.Error())
} }
l.sendIAPAttachTraceToTelegram("SUCCESS_NEW_PURCHASE_QUEUE", orderInfo, u.Id, subscribeId, tier, duration, txPayload.PurchaseDate, txPayload.TransactionId, txPayload.OriginalTransactionId, "")
l.Infow("事务已处理,首购订单等待激活队列发放订阅", logger.Field("orderNo", req.OrderNo), logger.Field("expiresAt", exp.Unix())) l.Infow("事务已处理,首购订单等待激活队列发放订阅", logger.Field("orderNo", req.OrderNo), logger.Field("expiresAt", exp.Unix()))
return &types.AttachAppleTransactionResponse{ExpiresAt: exp.Unix(), Tier: tier}, nil return &types.AttachAppleTransactionResponse{ExpiresAt: exp.Unix(), Tier: tier}, nil
} }
existSub, err := l.findIAPSubscribeByOriginalTransactionID(txPayload.OriginalTransactionId)
switch { switch {
case err == nil && existSub != nil && existSub.Id > 0: case existSubErr == nil && existSub != nil && existSub.Id > 0:
newExpire, updateErr := l.extendSubscribeForIAP(existSub, exp, subscribeId) newExpire, updateErr := l.extendSubscribeForIAP(existSub, exp, accumulateDuration, subscribeId)
if updateErr != nil { if updateErr != nil {
l.Errorw("刷新 IAP 订阅失败", logger.Field("error", updateErr.Error()), logger.Field("subscribeId", existSub.Id)) l.Errorw("刷新 IAP 订阅失败", logger.Field("error", updateErr.Error()), logger.Field("subscribeId", existSub.Id))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update iap subscribe failed: %v", updateErr.Error()) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update iap subscribe failed: %v", updateErr.Error())
} }
if bindErr := l.bindOrderTradeNo(orderInfo, tradeNo); bindErr != nil {
l.Errorw("写入订单交易号失败", logger.Field("orderNo", req.OrderNo), logger.Field("tradeNo", tradeNo), logger.Field("error", bindErr.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "bind order trade_no failed: %v", bindErr.Error())
}
if syncErr := l.syncOrderStatusAndEnqueue(orderInfo, newExpire.Unix()); syncErr != nil { if syncErr := l.syncOrderStatusAndEnqueue(orderInfo, newExpire.Unix()); syncErr != nil {
l.Errorw("同步订单状态失败(existSub)", logger.Field("orderNo", req.OrderNo), logger.Field("error", syncErr.Error())) l.Errorw("同步订单状态失败(existSub)", logger.Field("orderNo", req.OrderNo), logger.Field("error", syncErr.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "sync order status failed: %v", syncErr.Error()) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "sync order status failed: %v", syncErr.Error())
} }
l.sendIAPAttachTraceToTelegram("SUCCESS_RENEW_EXIST_SUB", orderInfo, u.Id, subscribeId, tier, duration, txPayload.PurchaseDate, txPayload.TransactionId, txPayload.OriginalTransactionId, "")
l.Infow("事务已处理,刷新订阅到期时间", logger.Field("originalTransactionId", txPayload.OriginalTransactionId), logger.Field("tier", tier), logger.Field("expiresAt", newExpire.Unix())) l.Infow("事务已处理,刷新订阅到期时间", logger.Field("originalTransactionId", txPayload.OriginalTransactionId), logger.Field("tier", tier), logger.Field("expiresAt", newExpire.Unix()))
return &types.AttachAppleTransactionResponse{ return &types.AttachAppleTransactionResponse{
ExpiresAt: newExpire.Unix(), ExpiresAt: newExpire.Unix(),
Tier: tier, Tier: tier,
}, nil }, nil
case err != nil && !errors.Is(err, gorm.ErrRecordNotFound):
l.Errorw("查询 IAP 订阅失败", logger.Field("error", err.Error()), logger.Field("originalTransactionId", txPayload.OriginalTransactionId))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find iap subscribe error: %v", err.Error())
} }
if orderLinkedSub != nil { if orderLinkedSub != nil {
newExpire, updateErr := l.extendSubscribeForIAP(orderLinkedSub, exp, subscribeId) newExpire, updateErr := l.extendSubscribeForIAP(orderLinkedSub, exp, accumulateDuration, subscribeId)
if updateErr != nil { if updateErr != nil {
l.Errorw("刷新订单关联订阅失败", logger.Field("error", updateErr.Error()), logger.Field("subscribeId", orderLinkedSub.Id)) l.Errorw("刷新订单关联订阅失败", logger.Field("error", updateErr.Error()), logger.Field("subscribeId", orderLinkedSub.Id))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update order subscribe failed: %v", updateErr.Error()) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update order subscribe failed: %v", updateErr.Error())
} }
if bindErr := l.bindOrderTradeNo(orderInfo, tradeNo); bindErr != nil {
l.Errorw("写入订单交易号失败", logger.Field("orderNo", req.OrderNo), logger.Field("tradeNo", tradeNo), logger.Field("error", bindErr.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "bind order trade_no failed: %v", bindErr.Error())
}
if syncErr := l.syncOrderStatusAndEnqueue(orderInfo, newExpire.Unix()); syncErr != nil { if syncErr := l.syncOrderStatusAndEnqueue(orderInfo, newExpire.Unix()); syncErr != nil {
l.Errorw("同步订单状态失败(orderLinkedSub)", logger.Field("orderNo", req.OrderNo), logger.Field("error", syncErr.Error())) l.Errorw("同步订单状态失败(orderLinkedSub)", logger.Field("orderNo", req.OrderNo), logger.Field("error", syncErr.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "sync order status failed: %v", syncErr.Error()) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "sync order status failed: %v", syncErr.Error())
} }
l.sendIAPAttachTraceToTelegram("SUCCESS_RENEW_ORDER_LINKED_SUB", orderInfo, u.Id, subscribeId, tier, duration, txPayload.PurchaseDate, txPayload.TransactionId, txPayload.OriginalTransactionId, "")
l.Infow("事务已处理,刷新订单关联订阅到期时间", logger.Field("orderNo", req.OrderNo), logger.Field("userSubscribeId", orderLinkedSub.Id), logger.Field("expiresAt", newExpire.Unix())) l.Infow("事务已处理,刷新订单关联订阅到期时间", logger.Field("orderNo", req.OrderNo), logger.Field("userSubscribeId", orderLinkedSub.Id), logger.Field("expiresAt", newExpire.Unix()))
return &types.AttachAppleTransactionResponse{ExpiresAt: newExpire.Unix(), Tier: tier}, nil return &types.AttachAppleTransactionResponse{ExpiresAt: newExpire.Unix(), Tier: tier}, nil
} }
if singleModeAnchorSub != nil { if singleModeAnchorSub != nil {
newExpire, updateErr := l.extendSubscribeForIAP(singleModeAnchorSub, exp, subscribeId) newExpire, updateErr := l.extendSubscribeForIAP(singleModeAnchorSub, exp, accumulateDuration, subscribeId)
if updateErr != nil { if updateErr != nil {
l.Errorw("刷新单订阅锚点订阅失败", logger.Field("error", updateErr.Error()), logger.Field("subscribeId", singleModeAnchorSub.Id)) l.Errorw("刷新单订阅锚点订阅失败", logger.Field("error", updateErr.Error()), logger.Field("subscribeId", singleModeAnchorSub.Id))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update single mode anchor subscribe failed: %v", updateErr.Error()) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update single mode anchor subscribe failed: %v", updateErr.Error())
} }
if bindErr := l.bindOrderTradeNo(orderInfo, tradeNo); bindErr != nil {
l.Errorw("写入订单交易号失败", logger.Field("orderNo", req.OrderNo), logger.Field("tradeNo", tradeNo), logger.Field("error", bindErr.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "bind order trade_no failed: %v", bindErr.Error())
}
if syncErr := l.syncOrderStatusAndEnqueue(orderInfo, newExpire.Unix()); syncErr != nil { if syncErr := l.syncOrderStatusAndEnqueue(orderInfo, newExpire.Unix()); syncErr != nil {
l.Errorw("同步订单状态失败(singleModeAnchorSub)", logger.Field("orderNo", req.OrderNo), logger.Field("error", syncErr.Error())) l.Errorw("同步订单状态失败(singleModeAnchorSub)", logger.Field("orderNo", req.OrderNo), logger.Field("error", syncErr.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "sync order status failed: %v", syncErr.Error()) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "sync order status failed: %v", syncErr.Error())
} }
l.sendIAPAttachTraceToTelegram("SUCCESS_RENEW_SINGLE_MODE_ANCHOR", orderInfo, u.Id, subscribeId, tier, duration, txPayload.PurchaseDate, txPayload.TransactionId, txPayload.OriginalTransactionId, "")
l.Infow("事务已处理,刷新单订阅锚点到期时间", logger.Field("userSubscribeId", singleModeAnchorSub.Id), logger.Field("expiresAt", newExpire.Unix())) l.Infow("事务已处理,刷新单订阅锚点到期时间", logger.Field("userSubscribeId", singleModeAnchorSub.Id), logger.Field("expiresAt", newExpire.Unix()))
return &types.AttachAppleTransactionResponse{ExpiresAt: newExpire.Unix(), Tier: tier}, nil return &types.AttachAppleTransactionResponse{ExpiresAt: newExpire.Unix(), Tier: tier}, nil
} }
@ -349,6 +414,10 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest
} else { } else {
l.Infow("首购订单跳过 attach 阶段订阅写入", logger.Field("orderNo", orderInfo.OrderNo), logger.Field("orderType", orderInfo.Type)) l.Infow("首购订单跳过 attach 阶段订阅写入", logger.Field("orderNo", orderInfo.OrderNo), logger.Field("orderType", orderInfo.Type))
} }
if e := l.bindOrderTradeNo(orderInfo, tradeNo, tx); e != nil {
l.Errorw("写入订单交易号失败", logger.Field("orderNo", req.OrderNo), logger.Field("tradeNo", tradeNo), logger.Field("error", e.Error()))
return e
}
if e := l.syncOrderStatusAndEnqueue(orderInfo, exp.Unix(), tx); e != nil { if e := l.syncOrderStatusAndEnqueue(orderInfo, exp.Unix(), tx); e != nil {
l.Errorw("同步订单状态失败", logger.Field("orderNo", req.OrderNo), logger.Field("error", e.Error())) l.Errorw("同步订单状态失败", logger.Field("orderNo", req.OrderNo), logger.Field("error", e.Error()))
return e return e
@ -359,6 +428,7 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest
l.Errorw("绑定事务提交失败", logger.Field("error", err.Error())) l.Errorw("绑定事务提交失败", logger.Field("error", err.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert error: %v", err.Error()) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert error: %v", err.Error())
} }
l.sendIAPAttachTraceToTelegram("SUCCESS_COMMIT", orderInfo, u.Id, subscribeId, tier, duration, txPayload.PurchaseDate, txPayload.TransactionId, txPayload.OriginalTransactionId, "")
l.Infow("绑定完成", logger.Field("userId", u.Id), logger.Field("tier", tier), logger.Field("expiresAt", exp.Unix())) l.Infow("绑定完成", logger.Field("userId", u.Id), logger.Field("tier", tier), logger.Field("expiresAt", exp.Unix()))
return &types.AttachAppleTransactionResponse{ return &types.AttachAppleTransactionResponse{
ExpiresAt: exp.Unix(), ExpiresAt: exp.Unix(),
@ -407,14 +477,11 @@ func (l *AttachTransactionLogic) findIAPSubscribeByOriginalTransactionID(origina
return nil, gorm.ErrRecordNotFound return nil, gorm.ErrRecordNotFound
} }
func (l *AttachTransactionLogic) extendSubscribeForIAP(userSub *user.Subscribe, exp time.Time, subscribeId int64, tx ...*gorm.DB) (time.Time, error) { func (l *AttachTransactionLogic) extendSubscribeForIAP(userSub *user.Subscribe, exp time.Time, durationDays int64, subscribeId int64, tx ...*gorm.DB) (time.Time, error) {
if userSub == nil { if userSub == nil {
return time.Time{}, errors.New("user subscribe is nil") return time.Time{}, errors.New("user subscribe is nil")
} }
newExpire := userSub.ExpireTime newExpire := l.calcIAPRenewalExpire(userSub.ExpireTime, exp, durationDays)
if exp.After(newExpire) {
newExpire = exp
}
userSub.ExpireTime = newExpire userSub.ExpireTime = newExpire
if subscribeId > 0 && userSub.SubscribeId != subscribeId { if subscribeId > 0 && userSub.SubscribeId != subscribeId {
userSub.SubscribeId = subscribeId userSub.SubscribeId = subscribeId
@ -426,3 +493,176 @@ func (l *AttachTransactionLogic) extendSubscribeForIAP(userSub *user.Subscribe,
} }
return newExpire, nil return newExpire, nil
} }
func (l *AttachTransactionLogic) calcIAPRenewalExpire(currentExpire time.Time, fallbackExpire time.Time, durationDays int64) time.Time {
if durationDays > 0 {
base := currentExpire
now := time.Now()
if base.Before(now) {
base = now
}
return base.Add(time.Duration(durationDays) * 24 * time.Hour)
}
if fallbackExpire.After(currentExpire) {
return fallbackExpire
}
return currentExpire
}
func (l *AttachTransactionLogic) getAppleTradeNoCandidates(txPayload *iapapple.TransactionPayload) []string {
if txPayload == nil {
return nil
}
candidates := make([]string, 0, 2)
if txPayload.OriginalTransactionId != "" {
candidates = append(candidates, txPayload.OriginalTransactionId)
}
if txPayload.TransactionId != "" && txPayload.TransactionId != txPayload.OriginalTransactionId {
candidates = append(candidates, txPayload.TransactionId)
}
return candidates
}
func (l *AttachTransactionLogic) validateOrderTradeNoBinding(orderInfo *ordermodel.Order, tradeNoCandidates []string) error {
if orderInfo == nil || len(tradeNoCandidates) == 0 {
return nil
}
if orderInfo.TradeNo != "" && !containsString(tradeNoCandidates, orderInfo.TradeNo) {
return errors.New("order already bound to another apple transaction")
}
var boundOrder ordermodel.Order
err := l.svcCtx.DB.WithContext(l.ctx).
Model(&ordermodel.Order{}).
Where("trade_no IN ? AND order_no <> ? AND status IN ?", tradeNoCandidates, orderInfo.OrderNo, []uint8{orderStatusPaid, orderStatusFinished}).
Order("id DESC").
First(&boundOrder).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
if err != nil {
return err
}
return errors.Errorf("apple transaction already used by order %s", boundOrder.OrderNo)
}
func (l *AttachTransactionLogic) bindOrderTradeNo(orderInfo *ordermodel.Order, tradeNo string, tx ...*gorm.DB) error {
if orderInfo == nil || tradeNo == "" {
return nil
}
if orderInfo.TradeNo == tradeNo {
return nil
}
if orderInfo.TradeNo != "" && orderInfo.TradeNo != tradeNo {
return errors.New("order already bound to another apple transaction")
}
orderInfo.TradeNo = tradeNo
if err := l.svcCtx.OrderModel.Update(l.ctx, orderInfo, tx...); err != nil {
return err
}
return nil
}
func containsString(candidates []string, target string) bool {
if target == "" {
return false
}
for _, item := range candidates {
if item == target {
return true
}
}
return false
}
func (l *AttachTransactionLogic) sendIAPAttachTraceToTelegram(status string, orderInfo *ordermodel.Order, userID int64, subscribeID int64, subscribeName string, quantity int64, purchaseAt time.Time, transactionID string, originalTransactionID string, note string) {
if l.svcCtx == nil {
return
}
orderNo := ""
if orderInfo != nil {
orderNo = orderInfo.OrderNo
}
if subscribeName == "" {
subscribeName = "-"
}
message := fmt.Sprintf(
"IAP Attach Log [%s]\n订单号: %s\n购买时间: %s\n购买人ID: %d\n订阅信息: %s (subscribe_id=%d, quantity=%d)\ntransaction: %s\noriginal_transaction: %s",
status,
orderNo,
purchaseAt.Format("2006-01-02 15:04:05"),
userID,
subscribeName,
subscribeID,
quantity,
transactionID,
originalTransactionID,
)
if note != "" {
message += "\n备注: " + note
}
overrideBotToken := strings.TrimSpace(os.Getenv("TG_BOT_TOKEN"))
overrideChatID := strings.TrimSpace(os.Getenv("TG_CHAT_ID"))
if overrideBotToken != "" && overrideChatID != "" {
if chatID, err := strconv.ParseInt(overrideChatID, 10, 64); err == nil && chatID != 0 {
bot := l.svcCtx.TelegramBot
if bot == nil || strings.TrimSpace(l.svcCtx.Config.Telegram.BotToken) != overrideBotToken {
overrideBot, newErr := tgbotapi.NewBotAPI(overrideBotToken)
if newErr == nil {
bot = overrideBot
} else {
l.Errorw("初始化 TG 覆盖 Bot 失败", logger.Field("error", newErr.Error()))
}
}
if bot != nil {
msg := tgbotapi.NewMessage(chatID, message)
if _, sendErr := bot.Send(msg); sendErr != nil {
l.Errorw("发送 IAP TG 覆盖通道消息失败", logger.Field("error", sendErr.Error()))
}
return
}
}
}
if l.svcCtx.TelegramBot == nil || !l.svcCtx.Config.Telegram.EnableNotify {
return
}
if groupChatID, err := strconv.ParseInt(strings.TrimSpace(l.svcCtx.Config.Telegram.GroupChatID), 10, 64); err == nil && groupChatID != 0 {
msg := tgbotapi.NewMessage(groupChatID, message)
if _, sendErr := l.svcCtx.TelegramBot.Send(msg); sendErr != nil {
l.Errorw("发送 IAP TG 群消息失败", logger.Field("error", sendErr.Error()))
}
return
}
admins, err := l.svcCtx.UserModel.QueryAdminUsers(l.ctx)
if err != nil {
l.Errorw("查询管理员失败(IAP TG日志)", logger.Field("error", err.Error()))
return
}
for _, admin := range admins {
if telegramID, ok := findTelegramAuth(admin); ok {
msg := tgbotapi.NewMessage(telegramID, message)
if _, sendErr := l.svcCtx.TelegramBot.Send(msg); sendErr != nil {
l.Errorw("发送 IAP TG 管理员消息失败", logger.Field("error", sendErr.Error()))
}
}
}
}
func findTelegramAuth(u *user.User) (int64, bool) {
if u == nil {
return 0, false
}
for _, item := range u.AuthMethods {
if item.AuthType == "telegram" {
if telegramID, err := strconv.ParseInt(item.AuthIdentifier, 10, 64); err == nil {
return telegramID, true
}
}
}
return 0, false
}

View File

@ -1610,8 +1610,8 @@ type Order struct {
TradeNo string `json:"trade_no"` TradeNo string `json:"trade_no"`
Status uint8 `json:"status"` Status uint8 `json:"status"`
SubscribeId int64 `json:"subscribe_id"` SubscribeId int64 `json:"subscribe_id"`
CreatedAt int64 `json:"created_at"` CreatedAt int64 `json:"created_at"` // Unix milliseconds
UpdatedAt int64 `json:"updated_at"` UpdatedAt int64 `json:"updated_at"` // Unix milliseconds
} }
type OrderDetail struct { type OrderDetail struct {
@ -1634,8 +1634,8 @@ type OrderDetail struct {
Status uint8 `json:"status"` Status uint8 `json:"status"`
SubscribeId int64 `json:"subscribe_id"` SubscribeId int64 `json:"subscribe_id"`
Subscribe Subscribe `json:"subscribe"` Subscribe Subscribe `json:"subscribe"`
CreatedAt int64 `json:"created_at"` CreatedAt int64 `json:"created_at"` // Unix milliseconds
UpdatedAt int64 `json:"updated_at"` UpdatedAt int64 `json:"updated_at"` // Unix milliseconds
} }
type OrdersStatistics struct { type OrdersStatistics struct {

View File

@ -56,6 +56,9 @@ func ParseTransactionJWS(jws string) (*TransactionPayload, error) {
if v, ok := raw["productId"].(string); ok { if v, ok := raw["productId"].(string); ok {
resp.ProductId = v resp.ProductId = v
} }
if v, ok := raw["type"].(string); ok {
resp.Type = v
}
if v, ok := raw["transactionId"].(string); ok { if v, ok := raw["transactionId"].(string); ok {
resp.TransactionId = v resp.TransactionId = v
} }

View File

@ -3,12 +3,12 @@ package apple
import "time" import "time"
type TransactionPayload struct { type TransactionPayload struct {
BundleId string `json:"bundleId"` BundleId string `json:"bundleId"`
ProductId string `json:"productId"` ProductId string `json:"productId"`
TransactionId string `json:"transactionId"` Type string `json:"type"`
OriginalTransactionId string `json:"originalTransactionId"` TransactionId string `json:"transactionId"`
PurchaseDate time.Time `json:"purchaseDate"` OriginalTransactionId string `json:"originalTransactionId"`
RevocationDate *time.Time`json:"revocationDate"` PurchaseDate time.Time `json:"purchaseDate"`
AppAccountToken string `json:"appAccountToken"` RevocationDate *time.Time `json:"revocationDate"`
AppAccountToken string `json:"appAccountToken"`
} }

View File

@ -733,9 +733,8 @@ func (l *ActivateOrderLogic) Renewal(ctx context.Context, orderInfo *order.Order
} }
if iapExpireAt > 0 { if iapExpireAt > 0 {
// Apple IAP 续费attachTransactionLogic 已通过 payload 传入 Apple 端计算的到期时间, // Apple IAP 续费:按“累计加时”语义处理,避免连续购买时仅覆盖到期时间。
// 直接使用该时间,避免在现有 expire_time 基础上再叠加天数导致双重计算。 if err = l.updateSubscriptionWithIAPExpire(ctx, userSub, sub, orderInfo, iapExpireAt); err != nil {
if err = l.updateSubscriptionWithIAPExpire(ctx, userSub, sub, iapExpireAt); err != nil {
return err return err
} }
} else { } else {
@ -776,11 +775,20 @@ func (l *ActivateOrderLogic) getUserSubscription(ctx context.Context, token stri
return userSub, nil return userSub, nil
} }
// updateSubscriptionWithIAPExpire 用于 Apple IAP 续费:直接将 Apple 服务端计算的 // updateSubscriptionWithIAPExpire 用于 Apple IAP 续费:按累计加时语义更新到期时间。
// 到期时间写入订阅,同时处理流量重置和 FinishedAt 清零,不再叠加天数。 func (l *ActivateOrderLogic) updateSubscriptionWithIAPExpire(ctx context.Context, userSub *user.Subscribe, sub *subscribe.Subscribe, orderInfo *order.Order, iapExpireAt int64) error {
func (l *ActivateOrderLogic) updateSubscriptionWithIAPExpire(ctx context.Context, userSub *user.Subscribe, sub *subscribe.Subscribe, iapExpireAt int64) error {
now := time.Now() now := time.Now()
newExpire := time.Unix(iapExpireAt, 0) baseTime := userSub.ExpireTime
if baseTime.Before(now) {
baseTime = now
}
newExpire := tool.AddTime(sub.UnitTime, orderInfo.Quantity, baseTime)
if iapExpireAt > 0 {
appleExpire := time.Unix(iapExpireAt, 0)
if appleExpire.After(newExpire) {
newExpire = appleExpire
}
}
today := now.Day() today := now.Day()
resetDay := newExpire.Day() resetDay := newExpire.Day()