ario_server/queue/logic/order/activateOrderLogic.go
Leif Draven e895180388
Develop (#76)
* refactor: rename queryannouncementhandler.go to queryAnnouncementLogic.go for clarity

* feat(panDomain): update subscription logic to use V2 handler for improved functionality

* refactor(subscribe): replace V2 handler with a unified Handler method for subscription logic

* feat(subscribe): implement user agent limit feature with configurable list

* fix(subscribe): improve error handling and logging for subscription requests

* feat(subscribe): add user agent limit configuration to system settings

* refactor(api): remove deprecated application-related endpoints and types

* refactor(swagger): remove deprecated app.json generation from swagger configuration

* refactor(swagger): remove deprecated app.json check from swagger configuration

* fix(subscribe): update delete method to use Where clause for improved query accuracy

* fix(subscribe): update Id field tag to use primaryKey and improve save method query

* fix(subscribe): update Id field tag to use primaryKey and improve model queries

* fix(subscribe): rename variable for clarity and add special handling for Stash user agent

* fix(email): convert RegisterStartTime and RegisterEndTime to time.Time for accurate query filtering

* refactor(log): consolidate logging models and update related logic for improved clarity and functionality

* fix(types): change Content field type in MessageLog to interface{} for improved flexibility

* fix(log): change MessageLog list to use value type for improved performance and memory efficiency

* fix(email): set EmailTypeVerify in task payload and update content type conversion for verification email

* fix(log): remove unused Id field from SystemLog during login log insertion

* fix(login): remove debug logs and error logging during user login process

* fix(log): add traffic reset logging for subscription resets

* fix(log): insert reset traffic log during subscription activation

* feat(log): add endpoints for retrieving and resetting subscribe traffic logs

* refactor(log): remove Reset Subscribe Traffic Log endpoint and related types

* feat(traffic): add traffic statistics logging and scheduling

* fix(subscribe): ensure active status and reset timestamps during traffic resets

* feat(api): enhance server and node management with new request/response structures

* refactor(api): rename OnlineUser to ServerOnlineUser for clarity

* feat(api): define OnlineUser type with SID and IP fields

* feat(server): implement server management handlers and database schema

* feat(api): add traffic log details filtering and enhance traffic log structures

* feat(api): migrate server and node data handling, update related structures and logic

* feat(server): implement server deletion logic with error handling

* feat(api): update log filtering to use ResetSubscribe type for subscription logs

* feat(api): standardize timestamp field across log structures

* feat(api): refactor cache key handling for server and user lists

* feat(api): enhance server status handling with protocol support and refactor related logic

* fix(traffic): adjust start date for traffic statistics and improve log deletion comment

* feat(api): implement daily traffic ranking for users and servers with error handling

* feat(api): update server total data response to use 'OnlineUsers' and implement daily traffic statistics logging

* feat(api): add log settings management with auto-clear and clear days configuration

* fix(log): correct category in log settings update query

* feat(routes): add handler for scheduled traffic statistics

* feat(model): add user counts struct and update queries for new and renewal users

* feat(api): add referral percentage and only first purchase fields to user model and requests

* feat(database): update user table to add referral percentage and only first purchase fields

* feat(api): add reset sort endpoints for server and node

* feat(api): add sort field to server model

* feat(api): implement sorting functionality for nodes and servers

* fix(database): add sort column to nodes table

* fix(model): enhance user statistics queries with new order and renewal order counts

* fix(log): update timestamp handling in login and registration logs

* fix(log): update sorting logic for server and user subscribe traffic logs

* fix(server): add server status handling based on last reported time

* fix(model): correct filter condition to use 'date' instead of 'data'

* fix(migration): add index for traffic log on timestamp, user_id, and subscribe_id

* fix(log): optimize user traffic rank data handling by using append instead of index assignment

* fix(filter): refactor node list creation to use append and remove duplicates from tags

* fix(node): add ServerId and Enabled fields to node update logic

* feat(tags): add endpoint to query all node tags

* fix(preview): add Preload parameter to FilterNodeList for improved data retrieval

* fix(log): date is empty

* feat(subscribe): add Language field to subscription models and update query logic

* feat(subscription): add Language parameter to GetSubscription request and update query logic

* fix(server): encode ServerKey in base64 and update last reported time for nodes

* feat: delete common GetSubscription

* feat(subscription): implement FilterList method for subscription queries and update related logic

* fix(subscribe): remove duplicate user agents in SubscribeHandler

* fix(push): initialize onlineUsers as a map in pushOnlineUsersLogic

* fix(reset): initialize subs as a map in clearCache method

* refactor(query): simplify node and tag filtering using InSet function

* feat(userlist): enhance GetServerUserListLogic with improved node and tag handling

* fix(userlist): correct node ID assignment and update query logic for tag filtering

* fix(userlist): correct node ID assignment in getServerUserListLogic

* refactor(query): streamline query construction for tag filtering

* fix(statistics): optimize server ranking data handling in QueryServerTotalDataLogic

* refactor(statistics): simplify server ranking data construction in QueryServerTotalDataLogic

* fix(statistics): correct server traffic data assignment in QueryServerTotalDataLogic

* fix(statistics): optimize yesterday's top 10 server traffic data assignment in QueryServerTotalDataLogic

* fix(middleware): remove duplicate elements from user agent list in PanDomainMiddleware

* feat(middleware): enhance user agent handling by querying client list in PanDomainMiddleware

* feat(client): subscribe_template

* feat(oauth): add user agent and IP logging to registration and login processes

* fix(balance): add timestamp to balance logs for payment, refund, and recharge transactions

* fix(log): correct comment for CommissionTypeRefund to improve clarity

* fix(log): replace magic number with constant for gift type in purchase checkout logic

* fix(log): rename OrderId to OrderNo for consistency in balance logging

* feat(log): add logging for balance, gift amount, and commission adjustments

* fix(user): correct placement of DeepCopy for user info update logic

* feat(log): add UserSubscribeId to FilterSubscribeLogRequest for enhanced filtering

* fix(purchase): streamline error handling and improve JSON marshaling for temporary orders

* fix(order): simplify commission handling and improve payload parsing logic

* fix(order): update commission calculation to actual payment amount minus gateway handling fee

* feat(payment): add support for CryptoSaaS payment platform and enhance configuration handling

* fix(balance): update QueryUserBalanceLog response structure to include balance log list

* fix(email): update task progress handling to use specific task ID for updates

* feat(quota): add quota task creation and querying endpoints with updated data structures

* fix(email): update task handling to use generic task model and improve error logging

* fix(order): improve error logging for database transaction and user cache updates

* feat(quota): enhance quota task management with new request structures and processing logic

* fix(quota): remove redundant quota task status endpoint from admin marketing routes

* fix(worker): update task completion status handling in worker logic

* fix(quota): update taskInfo to include current subscription count in quota logic

* doc(log): rename function for clarity and add cache cleanup comment

* fix(quota): update time handling in quota logic and correct subscriber ID query

* fix(quota): update time handling to use UnixMilli for start time in quota logic

* feat(protocol): add server protocol configuration query and enhance protocol options

* fix(quota): correct time range queries for start and expire times in quota logic

* fix(types): update plugin options to include 'none' in the plugin field

---------

Co-authored-by: Chang lue Tsen <tension@ppanel.dev>
2025-09-14 09:50:22 -04:00

808 lines
26 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package orderLogic provides order processing logic for handling various types of orders
// including subscription purchases, renewals, traffic resets, and balance recharges.
package orderLogic
import (
"context"
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
"github.com/perfect-panel/server/internal/model/log"
"github.com/perfect-panel/server/internal/model/node"
"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"
"github.com/perfect-panel/server/internal/model/subscribe"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/pkg/tool"
"github.com/perfect-panel/server/pkg/uuidx"
"github.com/perfect-panel/server/queue/types"
"gorm.io/gorm"
)
// Order type constants define the different types of orders that can be processed
const (
OrderTypeSubscribe = 1 // New subscription purchase
OrderTypeRenewal = 2 // Subscription renewal
OrderTypeResetTraffic = 3 // Traffic quota reset
OrderTypeRecharge = 4 // Balance recharge
)
// Order status constants define the lifecycle states of an order
const (
OrderStatusPending = 1 // Order created but not paid
OrderStatusPaid = 2 // Order paid and ready for processing
OrderStatusClose = 3 // Order closed/cancelled
OrderStatusFailed = 4 // Order processing failed
OrderStatusFinished = 5 // Order successfully completed
)
// Predefined error variables for common error conditions
var (
ErrInvalidOrderStatus = fmt.Errorf("invalid order status")
ErrInvalidOrderType = fmt.Errorf("invalid order type")
)
// ActivateOrderLogic handles the activation and processing of paid orders
type ActivateOrderLogic struct {
svc *svc.ServiceContext // Service context containing dependencies
}
// NewActivateOrderLogic creates a new instance of ActivateOrderLogic
func NewActivateOrderLogic(svc *svc.ServiceContext) *ActivateOrderLogic {
return &ActivateOrderLogic{
svc: svc,
}
}
// ProcessTask is the main entry point for processing order activation tasks.
// It handles the complete workflow of activating a paid order including validation,
// processing based on order type, and finalization.
func (l *ActivateOrderLogic) ProcessTask(ctx context.Context, task *asynq.Task) error {
payload, err := l.parsePayload(ctx, task.Payload())
if err != nil {
return nil // Log and continue
}
orderInfo, err := l.validateAndGetOrder(ctx, payload.OrderNo)
if err != nil {
return nil // Log and continue
}
if err = l.processOrderByType(ctx, orderInfo); err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Process task failed", logger.Field("error", err.Error()))
return nil
}
l.finalizeCouponAndOrder(ctx, orderInfo)
return nil
}
// parsePayload unMarshals the task payload into a structured format
func (l *ActivateOrderLogic) parsePayload(ctx context.Context, payload []byte) (*types.ForthwithActivateOrderPayload, error) {
var p types.ForthwithActivateOrderPayload
if err := json.Unmarshal(payload, &p); err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Unmarshal payload failed",
logger.Field("error", err.Error()),
logger.Field("payload", string(payload)),
)
return nil, err
}
return &p, nil
}
// validateAndGetOrder retrieves an order by order number and validates its status
// Returns error if order is not found or not in paid status
func (l *ActivateOrderLogic) validateAndGetOrder(ctx context.Context, orderNo string) (*order.Order, error) {
orderInfo, err := l.svc.OrderModel.FindOneByOrderNo(ctx, orderNo)
if err != nil {
logger.WithContext(ctx).Error("Find order failed",
logger.Field("error", err.Error()),
logger.Field("order_no", orderNo),
)
return nil, err
}
if orderInfo.Status != OrderStatusPaid {
logger.WithContext(ctx).Error("Order status error",
logger.Field("order_no", orderInfo.OrderNo),
logger.Field("status", orderInfo.Status),
)
return nil, ErrInvalidOrderStatus
}
return orderInfo, nil
}
// processOrderByType routes order processing based on the order type
func (l *ActivateOrderLogic) processOrderByType(ctx context.Context, orderInfo *order.Order) error {
switch orderInfo.Type {
case OrderTypeSubscribe:
return l.NewPurchase(ctx, orderInfo)
case OrderTypeRenewal:
return l.Renewal(ctx, orderInfo)
case OrderTypeResetTraffic:
return l.ResetTraffic(ctx, orderInfo)
case OrderTypeRecharge:
return l.Recharge(ctx, orderInfo)
default:
logger.WithContext(ctx).Error("Order type is invalid", logger.Field("type", orderInfo.Type))
return ErrInvalidOrderType
}
}
// finalizeCouponAndOrder handles post-processing tasks including coupon updates
// and order status finalization
func (l *ActivateOrderLogic) finalizeCouponAndOrder(ctx context.Context, orderInfo *order.Order) {
// Update coupon if exists
if orderInfo.Coupon != "" {
if err := l.svc.CouponModel.UpdateCount(ctx, orderInfo.Coupon); err != nil {
logger.WithContext(ctx).Error("Update coupon status failed",
logger.Field("error", err.Error()),
logger.Field("coupon", orderInfo.Coupon),
)
}
}
// Update order status
orderInfo.Status = OrderStatusFinished
if err := l.svc.OrderModel.Update(ctx, orderInfo); err != nil {
logger.WithContext(ctx).Error("Update order status failed",
logger.Field("error", err.Error()),
logger.Field("order_no", orderInfo.OrderNo),
)
}
}
// NewPurchase handles new subscription purchase including user creation,
// subscription setup, commission processing, cache updates, and notifications
func (l *ActivateOrderLogic) NewPurchase(ctx context.Context, orderInfo *order.Order) error {
userInfo, err := l.getUserOrCreate(ctx, orderInfo)
if err != nil {
return err
}
sub, err := l.getSubscribeInfo(ctx, orderInfo.SubscribeId)
if err != nil {
return err
}
userSub, err := l.createUserSubscription(ctx, orderInfo, sub)
if err != nil {
return err
}
// Handle commission in separate goroutine to avoid blocking
go l.handleCommission(context.Background(), userInfo, orderInfo)
// 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
}
// getUserOrCreate retrieves an existing user or creates a new guest user based on order details
func (l *ActivateOrderLogic) getUserOrCreate(ctx context.Context, orderInfo *order.Order) (*user.User, error) {
if orderInfo.UserId != 0 {
return l.getExistingUser(ctx, orderInfo.UserId)
}
return l.createGuestUser(ctx, orderInfo)
}
// getExistingUser retrieves user information by user ID
func (l *ActivateOrderLogic) getExistingUser(ctx context.Context, userId int64) (*user.User, error) {
userInfo, err := l.svc.UserModel.FindOne(ctx, userId)
if err != nil {
logger.WithContext(ctx).Error("Find user failed",
logger.Field("error", err.Error()),
logger.Field("user_id", userId),
)
return nil, err
}
return userInfo, nil
}
// createGuestUser creates a new user account for guest orders using temporary order information
// stored in Redis cache
func (l *ActivateOrderLogic) createGuestUser(ctx context.Context, orderInfo *order.Order) (*user.User, error) {
tempOrder, err := l.getTempOrderInfo(ctx, orderInfo.OrderNo)
if err != nil {
return nil, err
}
userInfo := &user.User{
Password: tool.EncodePassWord(tempOrder.Password),
AuthMethods: []user.AuthMethods{
{
AuthType: tempOrder.AuthType,
AuthIdentifier: tempOrder.Identifier,
},
},
}
err = l.svc.UserModel.Transaction(ctx, func(tx *gorm.DB) error {
if err := tx.Save(userInfo).Error; err != nil {
return err
}
userInfo.ReferCode = uuidx.UserInviteCode(userInfo.Id)
if err := tx.Model(&user.User{}).Where("id = ?", userInfo.Id).Update("refer_code", userInfo.ReferCode).Error; err != nil {
return err
}
orderInfo.UserId = userInfo.Id
return tx.Model(&order.Order{}).Where("order_no = ?", orderInfo.OrderNo).Update("user_id", userInfo.Id).Error
})
if err != nil {
logger.WithContext(ctx).Error("Create user failed", logger.Field("error", err.Error()))
return nil, err
}
// Handle referrer relationship
l.handleReferrer(ctx, userInfo, tempOrder.InviteCode)
logger.WithContext(ctx).Info("Create guest user success",
logger.Field("user_id", userInfo.Id),
logger.Field("identifier", tempOrder.Identifier),
logger.Field("auth_type", tempOrder.AuthType),
)
return userInfo, nil
}
// getTempOrderInfo retrieves temporary order information from Redis cache
func (l *ActivateOrderLogic) getTempOrderInfo(ctx context.Context, orderNo string) (*constant.TemporaryOrderInfo, error) {
cacheKey := fmt.Sprintf(constant.TempOrderCacheKey, orderNo)
data, err := l.svc.Redis.Get(ctx, cacheKey).Result()
if err != nil {
logger.WithContext(ctx).Error("Get temp order cache failed",
logger.Field("error", err.Error()),
logger.Field("cache_key", cacheKey),
)
return nil, err
}
var tempOrder constant.TemporaryOrderInfo
if err = tempOrder.Unmarshal([]byte(data)); err != nil {
logger.WithContext(ctx).Error("Unmarshal temp order cache failed",
logger.Field("error", err.Error()),
logger.Field("cache_key", cacheKey),
logger.Field("data", data),
)
return nil, err
}
return &tempOrder, nil
}
// handleReferrer establishes referrer relationship if an invite code is provided
func (l *ActivateOrderLogic) handleReferrer(ctx context.Context, userInfo *user.User, inviteCode string) {
if inviteCode == "" {
return
}
referer, err := l.svc.UserModel.FindOneByReferCode(ctx, inviteCode)
if err != nil {
logger.WithContext(ctx).Error("Find referer failed",
logger.Field("error", err.Error()),
logger.Field("refer_code", inviteCode),
)
return
}
userInfo.RefererId = referer.Id
if err = l.svc.UserModel.Update(ctx, userInfo); err != nil {
logger.WithContext(ctx).Error("Update user referer failed",
logger.Field("error", err.Error()),
logger.Field("user_id", userInfo.Id),
)
}
}
// getSubscribeInfo retrieves subscription plan details by subscription ID
func (l *ActivateOrderLogic) getSubscribeInfo(ctx context.Context, subscribeId int64) (*subscribe.Subscribe, error) {
sub, err := l.svc.SubscribeModel.FindOne(ctx, subscribeId)
if err != nil {
logger.WithContext(ctx).Error("Find subscribe failed",
logger.Field("error", err.Error()),
logger.Field("subscribe_id", subscribeId),
)
return nil, err
}
return sub, nil
}
// createUserSubscription creates a new user subscription record based on order and subscription plan details
func (l *ActivateOrderLogic) createUserSubscription(ctx context.Context, orderInfo *order.Order, sub *subscribe.Subscribe) (*user.Subscribe, error) {
now := time.Now()
userSub := &user.Subscribe{
UserId: orderInfo.UserId,
OrderId: orderInfo.Id,
SubscribeId: orderInfo.SubscribeId,
StartTime: now,
ExpireTime: tool.AddTime(sub.UnitTime, orderInfo.Quantity, now),
Traffic: sub.Traffic,
Download: 0,
Upload: 0,
Token: uuidx.SubscribeToken(orderInfo.OrderNo),
UUID: uuid.New().String(),
Status: 1,
}
if err := l.svc.UserModel.InsertSubscribe(ctx, userSub); err != nil {
logger.WithContext(ctx).Error("Insert user subscribe failed", logger.Field("error", err.Error()))
return nil, err
}
return userSub, nil
}
// handleCommission processes referral commission for the referrer if applicable.
// This runs asynchronously to avoid blocking the main order processing flow.
func (l *ActivateOrderLogic) handleCommission(ctx context.Context, userInfo *user.User, orderInfo *order.Order) {
if !l.shouldProcessCommission(userInfo, orderInfo.IsNew) {
return
}
referer, err := l.svc.UserModel.FindOne(ctx, userInfo.RefererId)
if err != nil {
logger.WithContext(ctx).Error("Find referer failed",
logger.Field("error", err.Error()),
logger.Field("referer_id", userInfo.RefererId),
)
return
}
var referralPercentage uint8
if referer.ReferralPercentage != 0 {
referralPercentage = referer.ReferralPercentage
} else {
referralPercentage = uint8(l.svc.Config.Invite.ReferralPercentage)
}
// Order commission calculation (Order Amount - Order Fee) * Referral Percentage
amount := l.calculateCommission(orderInfo.Amount-orderInfo.FeeAmount, referralPercentage)
// Use transaction for commission updates
err = l.svc.DB.Transaction(func(tx *gorm.DB) error {
referer.Commission += amount
if err = l.svc.UserModel.Update(ctx, referer, tx); err != nil {
return err
}
var commissionType uint16
switch orderInfo.Type {
case OrderTypeSubscribe:
commissionType = log.CommissionTypePurchase
case OrderTypeRenewal:
commissionType = log.CommissionTypeRenewal
}
commissionLog := &log.Commission{
Type: commissionType,
Amount: amount,
OrderNo: orderInfo.OrderNo,
Timestamp: orderInfo.CreatedAt.UnixMilli(),
}
content, _ := commissionLog.Marshal()
return tx.Model(&log.SystemLog{}).Create(&log.SystemLog{
Type: log.TypeCommission.Uint8(),
Date: time.Now().Format("2006-01-02"),
ObjectID: referer.Id,
Content: string(content),
}).Error
})
if err != nil {
logger.WithContext(ctx).Error("Update referer commission failed", logger.Field("error", err.Error()))
return
}
// Update cache
if err = l.svc.UserModel.UpdateUserCache(ctx, referer); err != nil {
logger.WithContext(ctx).Error("Update referer cache failed",
logger.Field("error", err.Error()),
logger.Field("user_id", referer.Id),
)
}
}
// shouldProcessCommission determines if commission should be processed based on
// referrer existence, commission settings, and order type
func (l *ActivateOrderLogic) shouldProcessCommission(userInfo *user.User, isFirstPurchase bool) bool {
if userInfo == nil || userInfo.RefererId == 0 {
return false
}
referer, err := l.svc.UserModel.FindOne(context.Background(), userInfo.RefererId)
if err != nil {
logger.Errorw("Find referer failed",
logger.Field("error", err.Error()),
logger.Field("referer_id", userInfo.RefererId))
return false
}
if referer == nil {
return false
}
// use referer's custom settings if set
if referer.ReferralPercentage > 0 {
if referer.OnlyFirstPurchase != nil && *referer.OnlyFirstPurchase && !isFirstPurchase {
return false
}
return true
}
// use global settings
if l.svc.Config.Invite.ReferralPercentage == 0 {
return false
}
if l.svc.Config.Invite.OnlyFirstPurchase && !isFirstPurchase {
return false
}
return true
}
// calculateCommission computes the commission amount based on order price and referral percentage
func (l *ActivateOrderLogic) calculateCommission(price int64, percentage uint8) int64 {
return int64(float64(price) * (float64(percentage) / 100))
}
// clearServerCache clears user list cache for all servers associated with the subscription
func (l *ActivateOrderLogic) clearServerCache(ctx context.Context, sub *subscribe.Subscribe) {
nodeIds := tool.StringToInt64Slice(sub.Nodes)
tags := strings.Split(sub.NodeTags, ",")
err := l.svc.NodeModel.ClearNodeCache(ctx, &node.FilterNodeParams{
Page: 1,
Size: 1000,
ServerId: nodeIds,
Tag: tags,
})
if err != nil {
logger.WithContext(ctx).Error("[Order Queue] Clear node cache failed", logger.Field("error", err.Error()))
}
}
// Renewal handles subscription renewal including subscription extension,
// traffic reset (if configured), commission processing, and notifications
func (l *ActivateOrderLogic) Renewal(ctx context.Context, orderInfo *order.Order) error {
userInfo, err := l.getExistingUser(ctx, orderInfo.UserId)
if err != nil {
return err
}
userSub, err := l.getUserSubscription(ctx, orderInfo.SubscribeToken)
if err != nil {
return err
}
sub, err := l.getSubscribeInfo(ctx, orderInfo.SubscribeId)
if err != nil {
return err
}
if err = l.updateSubscriptionForRenewal(ctx, userSub, sub, orderInfo); err != nil {
return err
}
// Clear user subscription cache
err = l.svc.UserModel.ClearSubscribeCache(ctx, userSub)
if err != nil {
logger.WithContext(ctx).Error("Clear user subscribe cache failed",
logger.Field("error", err.Error()),
logger.Field("subscribe_id", userSub.Id),
logger.Field("user_id", userInfo.Id),
)
}
// Clear cache
l.clearServerCache(ctx, sub)
// Handle commission
go l.handleCommission(context.Background(), userInfo, orderInfo)
// Send notifications
l.sendNotifications(ctx, orderInfo, userInfo, sub, userSub, telegram.RenewalNotify)
return nil
}
// getUserSubscription retrieves user subscription by token
func (l *ActivateOrderLogic) getUserSubscription(ctx context.Context, token string) (*user.Subscribe, error) {
userSub, err := l.svc.UserModel.FindOneSubscribeByToken(ctx, token)
if err != nil {
logger.WithContext(ctx).Error("Find user subscribe failed", logger.Field("error", err.Error()))
return nil, err
}
return userSub, nil
}
// updateSubscriptionForRenewal updates subscription details for renewal including
// expiration time extension and traffic reset if configured
func (l *ActivateOrderLogic) updateSubscriptionForRenewal(ctx context.Context, userSub *user.Subscribe, sub *subscribe.Subscribe, orderInfo *order.Order) error {
now := time.Now()
if userSub.ExpireTime.Before(now) {
userSub.ExpireTime = now
}
today := time.Now().Day()
resetDay := userSub.ExpireTime.Day()
// Reset traffic if enabled
if (sub.RenewalReset != nil && *sub.RenewalReset) || today == resetDay {
userSub.Download = 0
userSub.Upload = 0
}
if userSub.FinishedAt != nil {
if userSub.FinishedAt.Before(now) && today > resetDay {
// reset user traffic if finished at is before now
userSub.Download = 0
userSub.Upload = 0
}
userSub.FinishedAt = nil
}
userSub.ExpireTime = tool.AddTime(sub.UnitTime, orderInfo.Quantity, userSub.ExpireTime)
userSub.Status = 1
if err := l.svc.UserModel.UpdateSubscribe(ctx, userSub); err != nil {
logger.WithContext(ctx).Error("Update user subscribe failed", logger.Field("error", err.Error()))
return err
}
return nil
}
// ResetTraffic handles traffic quota reset for existing subscriptions
func (l *ActivateOrderLogic) ResetTraffic(ctx context.Context, orderInfo *order.Order) error {
userInfo, err := l.getExistingUser(ctx, orderInfo.UserId)
if err != nil {
return err
}
userSub, err := l.getUserSubscription(ctx, orderInfo.SubscribeToken)
if err != nil {
return err
}
// Reset traffic
userSub.Download = 0
userSub.Upload = 0
userSub.Status = 1
if err := l.svc.UserModel.UpdateSubscribe(ctx, userSub); err != nil {
logger.WithContext(ctx).Error("Update user subscribe failed", logger.Field("error", err.Error()))
return err
}
sub, err := l.getSubscribeInfo(ctx, userSub.SubscribeId)
if err != nil {
return err
}
// Clear user subscription cache
err = l.svc.UserModel.ClearSubscribeCache(ctx, userSub)
if err != nil {
logger.WithContext(ctx).Error("Clear user subscribe cache failed",
logger.Field("error", err.Error()),
logger.Field("subscribe_id", userSub.Id),
logger.Field("user_id", userInfo.Id),
)
}
// Clear cache
l.clearServerCache(ctx, sub)
// insert reset traffic log
resetLog := &log.ResetSubscribe{
Type: log.ResetSubscribeTypePaid,
UserId: userInfo.Id,
OrderNo: orderInfo.OrderNo,
Timestamp: time.Now().UnixMilli(),
}
content, _ := resetLog.Marshal()
if err = l.svc.LogModel.Insert(ctx, &log.SystemLog{
Type: log.TypeResetSubscribe.Uint8(),
Date: time.Now().Format(time.DateOnly),
ObjectID: userSub.Id,
Content: string(content),
}); err != nil {
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
}
// Recharge handles balance recharge orders including balance updates,
// transaction logging, and notifications
func (l *ActivateOrderLogic) Recharge(ctx context.Context, orderInfo *order.Order) error {
userInfo, err := l.getExistingUser(ctx, orderInfo.UserId)
if err != nil {
return err
}
// Update balance in transaction
err = l.svc.DB.Transaction(func(tx *gorm.DB) error {
userInfo.Balance += orderInfo.Price
if err = l.svc.UserModel.Update(ctx, userInfo, tx); err != nil {
return err
}
balanceLog := &log.Balance{
Amount: orderInfo.Price,
Type: log.BalanceTypeRecharge,
OrderNo: orderInfo.OrderNo,
Balance: userInfo.Balance,
Timestamp: time.Now().UnixMilli(),
}
content, _ := balanceLog.Marshal()
return tx.Model(&log.Balance{}).Create(&log.SystemLog{
Type: log.TypeBalance.Uint8(),
Date: time.Now().Format("2006-01-02"),
ObjectID: userInfo.Id,
Content: string(content),
}).Error
})
if err != nil {
logger.WithContext(ctx).Error("[Recharge] Database transaction failed", logger.Field("error", err.Error()))
return err
}
// clear user cache
if err = l.svc.UserModel.UpdateUserCache(ctx, userInfo); err != nil {
logger.WithContext(ctx).Error("[Recharge] Update user cache failed", logger.Field("error", err.Error()))
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) {
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) {
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
}