From 507ee16a30c0b0929c86189d6da61a30b6be3bbd Mon Sep 17 00:00:00 2001 From: shanshanzhong Date: Sat, 28 Mar 2026 09:06:24 -0700 Subject: [PATCH] x --- .../iap/apple/attachTransactionLogic.go | 103 +------------ internal/logic/public/order/purchaseLogic.go | 20 +++ internal/model/user/subscribe.go | 2 +- queue/logic/order/activateOrderLogic.go | 143 ------------------ 4 files changed, 22 insertions(+), 246 deletions(-) diff --git a/internal/logic/public/iap/apple/attachTransactionLogic.go b/internal/logic/public/iap/apple/attachTransactionLogic.go index bfecea6..797766a 100644 --- a/internal/logic/public/iap/apple/attachTransactionLogic.go +++ b/internal/logic/public/iap/apple/attachTransactionLogic.go @@ -6,12 +6,9 @@ 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" @@ -100,12 +97,10 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest existingOrderNo, validateErr := l.validateOrderTradeNoBinding(orderInfo, tradeNoCandidates) if validateErr != nil { l.Errorw("Apple 交易绑定校验失败", logger.Field("orderNo", req.OrderNo), logger.Field("tradeNoCandidates", tradeNoCandidates), logger.Field("error", validateErr.Error())) - l.sendIAPAttachTraceToTelegram("REJECT_BINDING_ERROR", orderInfo, u.Id, orderInfo.SubscribeId, "", orderInfo.Quantity, txPayload.PurchaseDate, txPayload.TransactionId, txPayload.OriginalTransactionId, validateErr.Error()) return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "apple transaction binding error") } if existingOrderNo != "" { l.Errorw("Apple 交易重复绑定,返回已绑定订单", logger.Field("orderNo", req.OrderNo), logger.Field("existingOrderNo", existingOrderNo), logger.Field("tradeNoCandidates", tradeNoCandidates)) - l.sendIAPAttachTraceToTelegram("REJECT_DUPLICATE_TRANSACTION", orderInfo, u.Id, orderInfo.SubscribeId, "", orderInfo.Quantity, txPayload.PurchaseDate, txPayload.TransactionId, txPayload.OriginalTransactionId, "already used by "+existingOrderNo) // 关闭当前 pending 订单,避免产生孤儿订单 if orderInfo.Status == orderStatusPending { if closeErr := l.svcCtx.DB.Model(&ordermodel.Order{}). @@ -282,7 +277,6 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest 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 } @@ -296,7 +290,6 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest 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 } @@ -316,7 +309,6 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest 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(), @@ -337,7 +329,6 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest 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 } @@ -355,7 +346,6 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest 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 } @@ -394,6 +384,7 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest userSub := user.Subscribe{ UserId: entitlement.EffectiveUserID, SubscribeId: subscribeId, + OrderId: orderInfo.Id, StartTime: time.Now(), ExpireTime: exp, Traffic: 0, @@ -426,7 +417,6 @@ 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, "") // 事务提交后立即清除订阅缓存,避免 App 查到旧数据(激活队列异步执行,存在竞态) if orderLinkedSub != nil { @@ -600,94 +590,3 @@ func containsString(candidates []string, target string) bool { } 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 -} diff --git a/internal/logic/public/order/purchaseLogic.go b/internal/logic/public/order/purchaseLogic.go index 0f21651..ca0075c 100644 --- a/internal/logic/public/order/purchaseLogic.go +++ b/internal/logic/public/order/purchaseLogic.go @@ -111,6 +111,26 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P } } + // 单订阅模式下,若已有同套餐 pending 订单,直接返回,防止重复创建 + if l.svcCtx.Config.Subscribe.SingleModel && orderType == 1 { + var existPending order.Order + if e := l.svcCtx.DB.WithContext(l.ctx). + Model(&order.Order{}). + Where("user_id = ? AND subscribe_id = ? AND status = 1", u.Id, targetSubscribeID). + Order("id DESC"). + First(&existPending).Error; e == nil && existPending.Id > 0 { + l.Infow("[Purchase] single mode pending order exists, returning existing", + logger.Field("user_id", u.Id), + logger.Field("order_no", existPending.OrderNo), + logger.Field("subscribe_id", targetSubscribeID), + ) + return &types.PurchaseOrderResponse{ + OrderNo: existPending.OrderNo, + AppAccountToken: existPending.AppAccountToken, + }, nil + } + } + // find subscribe plan sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, targetSubscribeID) diff --git a/internal/model/user/subscribe.go b/internal/model/user/subscribe.go index 7f798e4..854e9ea 100644 --- a/internal/model/user/subscribe.go +++ b/internal/model/user/subscribe.go @@ -67,7 +67,7 @@ func (m *defaultUserModel) FindSingleModeAnchorSubscribe(ctx context.Context, us var data Subscribe err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, _ interface{}) error { return conn.Model(&Subscribe{}). - Where("user_id = ? AND order_id > 0 AND token != '' AND `status` IN ?", userId, []int64{0, 1, 2, 3, 5}). + Where("user_id = ? AND token != '' AND (order_id > 0 OR token LIKE 'iap:%') AND `status` IN ?", userId, []int64{0, 1, 2, 3, 5}). Order("expire_time DESC"). Order("updated_at DESC"). Order("id DESC"). diff --git a/queue/logic/order/activateOrderLogic.go b/queue/logic/order/activateOrderLogic.go index ad4fe05..23032b2 100644 --- a/queue/logic/order/activateOrderLogic.go +++ b/queue/logic/order/activateOrderLogic.go @@ -7,7 +7,6 @@ import ( "encoding/json" "errors" "fmt" - "strconv" "time" "github.com/perfect-panel/server/internal/logic/admin/group" @@ -15,10 +14,8 @@ import ( "github.com/perfect-panel/server/pkg/constant" "github.com/perfect-panel/server/pkg/logger" - tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" "github.com/google/uuid" "github.com/hibiken/asynq" - "github.com/perfect-panel/server/internal/logic/telegram" "github.com/perfect-panel/server/internal/model/order" internaltypes "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/internal/model/redemption" @@ -333,9 +330,6 @@ func (l *ActivateOrderLogic) NewPurchase(ctx context.Context, orderInfo *order.O // Clear cache l.clearServerCache(ctx, sub) - // Send notifications - l.sendNotifications(ctx, orderInfo, userInfo, sub, userSub, telegram.PurchaseNotify) - logger.WithContext(ctx).Info("Insert user subscribe success") return nil } @@ -889,9 +883,6 @@ func (l *ActivateOrderLogic) Renewal(ctx context.Context, orderInfo *order.Order // Handle commission go l.handleCommission(context.Background(), userInfo, orderInfo) - // Send notifications - l.sendNotifications(ctx, orderInfo, userInfo, sub, userSub, telegram.RenewalNotify) - return nil } @@ -1041,9 +1032,6 @@ func (l *ActivateOrderLogic) ResetTraffic(ctx context.Context, orderInfo *order. logger.WithContext(ctx).Error("[Order Queue]Insert reset subscribe log failed", logger.Field("error", err.Error())) } - // Send notifications - l.sendNotifications(ctx, orderInfo, userInfo, sub, userSub, telegram.ResetTrafficNotify) - return nil } @@ -1108,140 +1096,9 @@ func (l *ActivateOrderLogic) Recharge(ctx context.Context, orderInfo *order.Orde return err } - // Send notifications - l.sendRechargeNotifications(ctx, orderInfo, userInfo) - return nil } -// sendNotifications sends both user and admin notifications for order completion -func (l *ActivateOrderLogic) sendNotifications(ctx context.Context, orderInfo *order.Order, userInfo *user.User, sub *subscribe.Subscribe, userSub *user.Subscribe, notifyType string) { - // Send user notification - if telegramId, ok := findTelegram(userInfo); ok { - templateData := l.buildUserNotificationData(orderInfo, sub, userSub) - if text, err := tool.RenderTemplateToString(notifyType, templateData); err == nil { - l.sendUserNotifyWithTelegram(telegramId, text) - } - } - - // Send admin notification - adminData := l.buildAdminNotificationData(orderInfo, sub) - if text, err := tool.RenderTemplateToString(telegram.AdminOrderNotify, adminData); err == nil { - l.sendAdminNotifyWithTelegram(ctx, text) - } -} - -// sendRechargeNotifications sends specific notifications for balance recharge orders -func (l *ActivateOrderLogic) sendRechargeNotifications(ctx context.Context, orderInfo *order.Order, userInfo *user.User) { - // Send user notification - if telegramId, ok := findTelegram(userInfo); ok { - templateData := map[string]string{ - "OrderAmount": fmt.Sprintf("%.2f", float64(orderInfo.Price)/100), - "PaymentMethod": orderInfo.Method, - "Time": orderInfo.CreatedAt.Format("2006-01-02 15:04:05"), - "Balance": fmt.Sprintf("%.2f", float64(userInfo.Balance)/100), - } - if text, err := tool.RenderTemplateToString(telegram.RechargeNotify, templateData); err == nil { - l.sendUserNotifyWithTelegram(telegramId, text) - } - } - - // Send admin notification - adminData := map[string]string{ - "OrderNo": orderInfo.OrderNo, - "TradeNo": orderInfo.TradeNo, - "OrderAmount": fmt.Sprintf("%.2f", float64(orderInfo.Price)/100), - "SubscribeName": "余额充值", - "OrderStatus": "已支付", - "OrderTime": orderInfo.CreatedAt.Format("2006-01-02 15:04:05"), - "PaymentMethod": orderInfo.Method, - } - if text, err := tool.RenderTemplateToString(telegram.AdminOrderNotify, adminData); err == nil { - l.sendAdminNotifyWithTelegram(ctx, text) - } -} - -// buildUserNotificationData creates template data for user notifications -func (l *ActivateOrderLogic) buildUserNotificationData(orderInfo *order.Order, sub *subscribe.Subscribe, userSub *user.Subscribe) map[string]string { - data := map[string]string{ - "OrderNo": orderInfo.OrderNo, - "SubscribeName": sub.Name, - "OrderAmount": fmt.Sprintf("%.2f", float64(orderInfo.Price)/100), - } - - if userSub != nil { - data["ExpireTime"] = userSub.ExpireTime.Format("2006-01-02 15:04:05") - data["ResetTime"] = time.Now().Format("2006-01-02 15:04:05") - } - - return data -} - -// buildAdminNotificationData creates template data for admin notifications -func (l *ActivateOrderLogic) buildAdminNotificationData(orderInfo *order.Order, sub *subscribe.Subscribe) map[string]string { - subscribeName := sub.Name - if orderInfo.Type == OrderTypeResetTraffic { - subscribeName = "流量重置" - } - - return map[string]string{ - "OrderNo": orderInfo.OrderNo, - "TradeNo": orderInfo.TradeNo, - "SubscribeName": subscribeName, - "OrderAmount": fmt.Sprintf("%.2f", float64(orderInfo.Price)/100), - "OrderStatus": "已支付", - "OrderTime": orderInfo.CreatedAt.Format("2006-01-02 15:04:05"), - "PaymentMethod": orderInfo.Method, - } -} - -// 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 { - logger.Error("Send telegram user message failed", logger.Field("error", err.Error())) - } -} - -// 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())) - return - } - - for _, admin := range admins { - if telegramId, ok := findTelegram(admin); ok { - msg := tgbotapi.NewMessage(telegramId, text) - msg.ParseMode = "markdown" - if _, err := l.svc.TelegramBot.Send(msg); err != nil { - logger.WithContext(ctx).Error("Send telegram admin message failed", logger.Field("error", err.Error())) - } - } - } -} - -// findTelegram extracts Telegram chat ID from user authentication methods. -// Returns the chat ID and a boolean indicating if Telegram auth was found. -func findTelegram(u *user.User) (int64, bool) { - 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 -} - // RedemptionActivate handles redemption code activation including subscription creation, // redemption record creation, used count update, cache clearing, and notifications func (l *ActivateOrderLogic) RedemptionActivate(ctx context.Context, orderInfo *order.Order) error {