x
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 8m20s

This commit is contained in:
shanshanzhong 2026-03-28 09:06:24 -07:00
parent 2db2bc0860
commit 507ee16a30
4 changed files with 22 additions and 246 deletions

View File

@ -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
}

View File

@ -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)

View File

@ -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").

View File

@ -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 {