map对齐
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 7m7s
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 7m7s
This commit is contained in:
parent
a0ae7b1c8d
commit
1e14e2dbd9
@ -6,10 +6,12 @@ import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/hibiken/asynq"
|
||||
commonLogic "github.com/perfect-panel/server/internal/logic/common"
|
||||
@ -35,9 +37,10 @@ type AttachTransactionLogic struct {
|
||||
}
|
||||
|
||||
const (
|
||||
orderTypeSubscribe uint8 = 1
|
||||
orderStatusPending uint8 = 1
|
||||
orderStatusPaid uint8 = 2
|
||||
orderTypeSubscribe uint8 = 1
|
||||
orderStatusPending uint8 = 1
|
||||
orderStatusPaid uint8 = 2
|
||||
orderStatusFinished uint8 = 5
|
||||
)
|
||||
|
||||
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")
|
||||
}
|
||||
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
|
||||
var existTx *iapmodel.Transaction
|
||||
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)
|
||||
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()))
|
||||
var orderLinkedSub *user.Subscribe
|
||||
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 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 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("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()))
|
||||
return &types.AttachAppleTransactionResponse{ExpiresAt: exp.Unix(), Tier: tier}, nil
|
||||
}
|
||||
|
||||
existSub, err := l.findIAPSubscribeByOriginalTransactionID(txPayload.OriginalTransactionId)
|
||||
switch {
|
||||
case err == nil && existSub != nil && existSub.Id > 0:
|
||||
newExpire, updateErr := l.extendSubscribeForIAP(existSub, exp, subscribeId)
|
||||
case existSubErr == nil && existSub != nil && existSub.Id > 0:
|
||||
newExpire, updateErr := l.extendSubscribeForIAP(existSub, exp, accumulateDuration, subscribeId)
|
||||
if updateErr != nil {
|
||||
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())
|
||||
}
|
||||
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 {
|
||||
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())
|
||||
}
|
||||
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()))
|
||||
return &types.AttachAppleTransactionResponse{
|
||||
ExpiresAt: newExpire.Unix(),
|
||||
Tier: tier,
|
||||
}, 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 {
|
||||
newExpire, updateErr := l.extendSubscribeForIAP(orderLinkedSub, exp, subscribeId)
|
||||
newExpire, updateErr := l.extendSubscribeForIAP(orderLinkedSub, exp, accumulateDuration, subscribeId)
|
||||
if updateErr != nil {
|
||||
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())
|
||||
}
|
||||
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 {
|
||||
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())
|
||||
}
|
||||
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()))
|
||||
return &types.AttachAppleTransactionResponse{ExpiresAt: newExpire.Unix(), Tier: tier}, nil
|
||||
}
|
||||
if singleModeAnchorSub != nil {
|
||||
newExpire, updateErr := l.extendSubscribeForIAP(singleModeAnchorSub, exp, subscribeId)
|
||||
newExpire, updateErr := l.extendSubscribeForIAP(singleModeAnchorSub, exp, accumulateDuration, subscribeId)
|
||||
if updateErr != nil {
|
||||
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())
|
||||
}
|
||||
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 {
|
||||
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())
|
||||
}
|
||||
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()))
|
||||
return &types.AttachAppleTransactionResponse{ExpiresAt: newExpire.Unix(), Tier: tier}, nil
|
||||
}
|
||||
@ -349,6 +414,10 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest
|
||||
} else {
|
||||
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 {
|
||||
l.Errorw("同步订单状态失败", logger.Field("orderNo", req.OrderNo), logger.Field("error", e.Error()))
|
||||
return e
|
||||
@ -359,6 +428,7 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest
|
||||
l.Errorw("绑定事务提交失败", logger.Field("error", 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()))
|
||||
return &types.AttachAppleTransactionResponse{
|
||||
ExpiresAt: exp.Unix(),
|
||||
@ -407,14 +477,11 @@ func (l *AttachTransactionLogic) findIAPSubscribeByOriginalTransactionID(origina
|
||||
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 {
|
||||
return time.Time{}, errors.New("user subscribe is nil")
|
||||
}
|
||||
newExpire := userSub.ExpireTime
|
||||
if exp.After(newExpire) {
|
||||
newExpire = exp
|
||||
}
|
||||
newExpire := l.calcIAPRenewalExpire(userSub.ExpireTime, exp, durationDays)
|
||||
userSub.ExpireTime = newExpire
|
||||
if subscribeId > 0 && userSub.SubscribeId != subscribeId {
|
||||
userSub.SubscribeId = subscribeId
|
||||
@ -426,3 +493,176 @@ func (l *AttachTransactionLogic) extendSubscribeForIAP(userSub *user.Subscribe,
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@ -1610,8 +1610,8 @@ type Order struct {
|
||||
TradeNo string `json:"trade_no"`
|
||||
Status uint8 `json:"status"`
|
||||
SubscribeId int64 `json:"subscribe_id"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
CreatedAt int64 `json:"created_at"` // Unix milliseconds
|
||||
UpdatedAt int64 `json:"updated_at"` // Unix milliseconds
|
||||
}
|
||||
|
||||
type OrderDetail struct {
|
||||
@ -1634,8 +1634,8 @@ type OrderDetail struct {
|
||||
Status uint8 `json:"status"`
|
||||
SubscribeId int64 `json:"subscribe_id"`
|
||||
Subscribe Subscribe `json:"subscribe"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
CreatedAt int64 `json:"created_at"` // Unix milliseconds
|
||||
UpdatedAt int64 `json:"updated_at"` // Unix milliseconds
|
||||
}
|
||||
|
||||
type OrdersStatistics struct {
|
||||
|
||||
@ -56,6 +56,9 @@ func ParseTransactionJWS(jws string) (*TransactionPayload, error) {
|
||||
if v, ok := raw["productId"].(string); ok {
|
||||
resp.ProductId = v
|
||||
}
|
||||
if v, ok := raw["type"].(string); ok {
|
||||
resp.Type = v
|
||||
}
|
||||
if v, ok := raw["transactionId"].(string); ok {
|
||||
resp.TransactionId = v
|
||||
}
|
||||
|
||||
@ -3,12 +3,12 @@ package apple
|
||||
import "time"
|
||||
|
||||
type TransactionPayload struct {
|
||||
BundleId string `json:"bundleId"`
|
||||
ProductId string `json:"productId"`
|
||||
TransactionId string `json:"transactionId"`
|
||||
OriginalTransactionId string `json:"originalTransactionId"`
|
||||
PurchaseDate time.Time `json:"purchaseDate"`
|
||||
RevocationDate *time.Time`json:"revocationDate"`
|
||||
AppAccountToken string `json:"appAccountToken"`
|
||||
BundleId string `json:"bundleId"`
|
||||
ProductId string `json:"productId"`
|
||||
Type string `json:"type"`
|
||||
TransactionId string `json:"transactionId"`
|
||||
OriginalTransactionId string `json:"originalTransactionId"`
|
||||
PurchaseDate time.Time `json:"purchaseDate"`
|
||||
RevocationDate *time.Time `json:"revocationDate"`
|
||||
AppAccountToken string `json:"appAccountToken"`
|
||||
}
|
||||
|
||||
|
||||
@ -733,9 +733,8 @@ func (l *ActivateOrderLogic) Renewal(ctx context.Context, orderInfo *order.Order
|
||||
}
|
||||
|
||||
if iapExpireAt > 0 {
|
||||
// Apple IAP 续费:attachTransactionLogic 已通过 payload 传入 Apple 端计算的到期时间,
|
||||
// 直接使用该时间,避免在现有 expire_time 基础上再叠加天数导致双重计算。
|
||||
if err = l.updateSubscriptionWithIAPExpire(ctx, userSub, sub, iapExpireAt); err != nil {
|
||||
// Apple IAP 续费:按“累计加时”语义处理,避免连续购买时仅覆盖到期时间。
|
||||
if err = l.updateSubscriptionWithIAPExpire(ctx, userSub, sub, orderInfo, iapExpireAt); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
@ -776,11 +775,20 @@ func (l *ActivateOrderLogic) getUserSubscription(ctx context.Context, token stri
|
||||
return userSub, nil
|
||||
}
|
||||
|
||||
// updateSubscriptionWithIAPExpire 用于 Apple IAP 续费:直接将 Apple 服务端计算的
|
||||
// 到期时间写入订阅,同时处理流量重置和 FinishedAt 清零,不再叠加天数。
|
||||
func (l *ActivateOrderLogic) updateSubscriptionWithIAPExpire(ctx context.Context, userSub *user.Subscribe, sub *subscribe.Subscribe, iapExpireAt int64) error {
|
||||
// updateSubscriptionWithIAPExpire 用于 Apple IAP 续费:按累计加时语义更新到期时间。
|
||||
func (l *ActivateOrderLogic) updateSubscriptionWithIAPExpire(ctx context.Context, userSub *user.Subscribe, sub *subscribe.Subscribe, orderInfo *order.Order, iapExpireAt int64) error {
|
||||
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()
|
||||
resetDay := newExpire.Day()
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user