ario_server/queue/logic/task/quotaLogic.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

451 lines
13 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 task
import (
"context"
"encoding/json"
"fmt"
"strconv"
"time"
"github.com/hibiken/asynq"
"github.com/perfect-panel/server/internal/model/log"
"github.com/perfect-panel/server/internal/model/order"
"github.com/perfect-panel/server/internal/model/subscribe"
"github.com/perfect-panel/server/internal/model/task"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/tool"
"gorm.io/gorm"
)
const (
UnitTimeNoLimit = "NoLimit" // Unlimited time subscription
UnitTimeYear = "Year" // Annual subscription
UnitTimeMonth = "Month" // Monthly subscription
UnitTimeDay = "Day" // Daily subscription
UnitTimeHour = "Hour" // Hourly subscription
UnitTimeMinute = "Minute" // Per-minute subscription
)
type QuotaTaskLogic struct {
svcCtx *svc.ServiceContext
}
type ErrorInfo struct {
UserSubscribeId int64 `json:"user_subscribe_id"`
Error string `json:"error"`
}
func NewQuotaTaskLogic(svcCtx *svc.ServiceContext) *QuotaTaskLogic {
return &QuotaTaskLogic{
svcCtx: svcCtx,
}
}
func (l *QuotaTaskLogic) ProcessTask(ctx context.Context, t *asynq.Task) error {
taskID, err := l.parseTaskID(ctx, t.Payload())
if err != nil {
return err
}
taskInfo, err := l.getTaskInfo(ctx, taskID)
if err != nil {
return err
}
if taskInfo.Status != 0 {
logger.WithContext(ctx).Info("[QuotaTaskLogic.ProcessTask] task already processed",
logger.Field("taskID", taskID),
logger.Field("status", taskInfo.Status),
)
return nil
}
scope, content, err := l.parseTaskData(ctx, taskInfo)
if err != nil {
return err
}
subscribes, err := l.getSubscribes(ctx, scope.Objects)
if err != nil {
return err
}
if err = l.processSubscribes(ctx, subscribes, content, taskInfo); err != nil {
return err
}
// 清理用户缓存(仅在有赠送金时清理)
if content.GiftValue != 0 {
var userIds []int64
for _, sub := range subscribes {
userIds = append(userIds, sub.UserId)
}
userIds = tool.RemoveDuplicateElements(userIds...)
var users []*user.User
if err = l.svcCtx.DB.WithContext(ctx).Model(&user.User{}).Where("id IN ?", userIds).Find(&users).Error; err != nil {
logger.WithContext(ctx).Error("[QuotaTaskLogic.ProcessTask] find users error",
logger.Field("error", err.Error()),
logger.Field("userIDs", userIds))
}
err = l.svcCtx.UserModel.ClearUserCache(ctx, users...)
if err != nil {
logger.WithContext(ctx).Error("[QuotaTaskLogic.ProcessTask] clear user cache error",
logger.Field("error", err.Error()),
logger.Field("userIDs", userIds))
}
}
// 清理用户订阅缓存
err = l.svcCtx.UserModel.ClearSubscribeCache(ctx, subscribes...)
if err != nil {
logger.WithContext(ctx).Error("[QuotaTaskLogic.ProcessTask] clear subscribe cache error",
logger.Field("error", err.Error()))
}
return nil
}
func (l *QuotaTaskLogic) parseTaskID(ctx context.Context, payload []byte) (int64, error) {
if len(payload) == 0 {
logger.WithContext(ctx).Error("[QuotaTaskLogic.parseTaskID] empty payload")
return 0, asynq.SkipRetry
}
taskID, err := strconv.ParseInt(string(payload), 10, 64)
if err != nil {
logger.WithContext(ctx).Error("[QuotaTaskLogic.parseTaskID] invalid task ID",
logger.Field("error", err.Error()),
logger.Field("payload", string(payload)),
)
return 0, asynq.SkipRetry
}
return taskID, nil
}
func (l *QuotaTaskLogic) getTaskInfo(ctx context.Context, taskID int64) (*task.Task, error) {
var taskInfo *task.Task
if err := l.svcCtx.DB.WithContext(ctx).Model(&task.Task{}).Where("id = ?", taskID).First(&taskInfo).Error; err != nil {
logger.WithContext(ctx).Error("[QuotaTaskLogic.getTaskInfo] find task error",
logger.Field("error", err.Error()),
logger.Field("taskID", taskID),
)
return nil, asynq.SkipRetry
}
return taskInfo, nil
}
func (l *QuotaTaskLogic) parseTaskData(ctx context.Context, taskInfo *task.Task) (task.QuotaScope, task.QuotaContent, error) {
var scope task.QuotaScope
if err := scope.Unmarshal([]byte(taskInfo.Scope)); err != nil {
logger.WithContext(ctx).Error("[QuotaTaskLogic.parseTaskData] unmarshal scope error",
logger.Field("error", err.Error()),
)
return scope, task.QuotaContent{}, asynq.SkipRetry
}
var content task.QuotaContent
if err := content.Unmarshal([]byte(taskInfo.Content)); err != nil {
logger.WithContext(ctx).Error("[QuotaTaskLogic.parseTaskData] unmarshal content error",
logger.Field("error", err.Error()),
)
return scope, content, asynq.SkipRetry
}
return scope, content, nil
}
func (l *QuotaTaskLogic) getSubscribes(ctx context.Context, subscriberIDs []int64) ([]*user.Subscribe, error) {
var subscribes []*user.Subscribe
if err := l.svcCtx.DB.WithContext(ctx).Model(&user.Subscribe{}).Where("id IN ?", subscriberIDs).Find(&subscribes).Error; err != nil {
logger.WithContext(ctx).Error("[QuotaTaskLogic.getSubscribes] find subscribes error",
logger.Field("error", err.Error()),
logger.Field("subscribers", subscriberIDs),
)
return nil, asynq.SkipRetry
}
return subscribes, nil
}
func (l *QuotaTaskLogic) processSubscribes(ctx context.Context, subscribes []*user.Subscribe, content task.QuotaContent, taskInfo *task.Task) error {
tx := l.svcCtx.DB.WithContext(ctx).Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
logger.WithContext(ctx).Error("[QuotaTaskLogic.processSubscribes] transaction panic",
logger.Field("panic", r),
)
}
}()
var errors []ErrorInfo
now := time.Now()
for _, sub := range subscribes {
if err := l.processSubscription(tx, sub, content, now, &errors); err != nil {
tx.Rollback()
return err
}
}
// 根据错误情况决定任务状态
status := int8(2) // Completed
if len(errors) > 0 {
logger.WithContext(ctx).Error("[QuotaTaskLogic.processSubscribes] some subscriptions failed",
logger.Field("total", len(subscribes)),
logger.Field("failed", len(errors)),
)
// 如果所有订阅都失败,标记为失败状态
if len(errors) == len(subscribes) {
status = 3 // Failed
}
errs, err := json.Marshal(errors)
if err != nil {
logger.WithContext(ctx).Error("[QuotaTaskLogic.processSubscribes] marshal errors failed",
logger.Field("error", err.Error()),
)
tx.Rollback()
return err
}
taskInfo.Errors = string(errs)
}
taskInfo.Current = uint64(len(subscribes))
taskInfo.Status = status
err := tx.Where("id = ?", taskInfo.Id).Save(taskInfo).Error
if err != nil {
logger.WithContext(ctx).Error("[QuotaTaskLogic.processSubscribes] update task status error",
logger.Field("error", err.Error()),
logger.Field("taskID", taskInfo.Id),
)
tx.Rollback()
return err
}
if err = tx.Commit().Error; err != nil {
logger.WithContext(ctx).Error("[QuotaTaskLogic.processSubscribes] commit transaction error",
logger.Field("error", err.Error()),
)
return err
}
return nil
}
func (l *QuotaTaskLogic) processSubscription(tx *gorm.DB, sub *user.Subscribe, content task.QuotaContent, now time.Time, errors *[]ErrorInfo) error {
// 验证订阅数据
if sub == nil {
*errors = append(*errors, ErrorInfo{
UserSubscribeId: 0,
Error: "subscription is nil",
})
return nil
}
updated := false
// 处理时间延长 - 修复逻辑只要Days不为0就处理不管ExpireTime是否为0
if content.Days != 0 {
if sub.ExpireTime.Unix() == 0 || sub.ExpireTime.Before(now) {
// 如果没有过期时间或已过期,从现在开始计算
sub.ExpireTime = now.AddDate(0, 0, int(content.Days))
} else {
// 在原有过期时间基础上延长
sub.ExpireTime = sub.ExpireTime.AddDate(0, 0, int(content.Days))
}
// 如果订阅延长到未来时间,设置为激活状态
if sub.ExpireTime.After(now) && sub.Status != 1 {
sub.Status = 1 // Active
}
updated = true
}
// 处理流量重置
if content.ResetTraffic {
sub.Download = 0
sub.Upload = 0
updated = true
if err := l.createResetTrafficLog(tx, sub.Id, sub.UserId, now); err != nil {
// 记录错误但不阻断整个任务,日志失败不影响主流程
*errors = append(*errors, ErrorInfo{
UserSubscribeId: sub.Id,
Error: "create reset traffic log error: " + err.Error(),
})
}
}
// 处理赠送金
if content.GiftValue != 0 {
if err := l.processGift(tx, sub, content, now, errors); err != nil {
return err
}
}
// 只有在有更新时才保存订阅信息
if updated {
if err := tx.Where("id = ?", sub.Id).Save(sub).Error; err != nil {
*errors = append(*errors, ErrorInfo{
UserSubscribeId: sub.Id,
Error: "update subscription error: " + err.Error(),
})
return nil
}
}
return nil
}
func (l *QuotaTaskLogic) processGift(tx *gorm.DB, sub *user.Subscribe, content task.QuotaContent, now time.Time, errors *[]ErrorInfo) error {
// 验证赠送类型
if content.GiftType != 1 && content.GiftType != 2 {
*errors = append(*errors, ErrorInfo{
UserSubscribeId: sub.Id,
Error: fmt.Sprintf("invalid gift type: %d", content.GiftType),
})
return nil
}
var userInfo user.User
if err := tx.Model(&user.User{}).Where("id = ?", sub.UserId).First(&userInfo).Error; err != nil {
*errors = append(*errors, ErrorInfo{
UserSubscribeId: sub.Id,
Error: "find user error: " + err.Error(),
})
return nil
}
var giftAmount int64
switch content.GiftType {
case 1:
giftAmount = int64(content.GiftValue)
case 2:
orderAmount, err := l.calculateOrderAmount(tx, sub, now)
if err != nil {
*errors = append(*errors, ErrorInfo{
UserSubscribeId: sub.Id,
Error: err.Error(),
})
return nil
}
if orderAmount > 0 {
giftAmount = int64(float64(orderAmount) * (float64(content.GiftValue) / 100))
}
}
if giftAmount > 0 {
userInfo.GiftAmount += giftAmount
// 使用Update而不是Save更精确地更新单个字段
if err := tx.Model(&user.User{}).Where("id = ?", sub.UserId).Update("gift_amount", userInfo.GiftAmount).Error; err != nil {
*errors = append(*errors, ErrorInfo{
UserSubscribeId: sub.Id,
Error: "update user gift amount error: " + err.Error(),
})
return nil
}
if err := l.createGiftLog(tx, sub.Id, userInfo.Id, giftAmount, userInfo.GiftAmount, now); err != nil {
*errors = append(*errors, ErrorInfo{
UserSubscribeId: sub.Id,
Error: "create gift log error: " + err.Error(),
})
// 回滚用户金额更新
userInfo.GiftAmount -= giftAmount
tx.Model(&user.User{}).Where("id = ?", sub.UserId).Update("gift_amount", userInfo.GiftAmount)
return nil
}
}
return nil
}
func (l *QuotaTaskLogic) getStartTime(sub *user.Subscribe, now time.Time) time.Time {
if sub.StartTime.Unix() == 0 {
return now
}
return sub.StartTime
}
func (l *QuotaTaskLogic) calculateOrderAmount(tx *gorm.DB, sub *user.Subscribe, now time.Time) (int64, error) {
if sub.OrderId != 0 {
var orderInfo *order.Order
if err := tx.Model(&order.Order{}).Where("id = ?", sub.OrderId).First(&orderInfo).Error; err != nil {
return 0, fmt.Errorf("find order error: %v", err)
}
return orderInfo.Amount + orderInfo.GiftAmount, nil
}
var subInfo *subscribe.Subscribe
if err := tx.Model(&subscribe.Subscribe{}).Where("id = ?", sub.SubscribeId).First(&subInfo).Error; err != nil {
return 0, fmt.Errorf("find subscribe error: %v", err)
}
startTime := l.getStartTime(sub, now)
if sub.ExpireTime.Before(startTime) {
return subInfo.UnitPrice, nil
}
switch subInfo.UnitTime {
case UnitTimeNoLimit:
return subInfo.UnitPrice, nil
case UnitTimeYear:
days := tool.DayDiff(startTime, sub.ExpireTime)
return subInfo.UnitPrice / 365 * days, nil
case UnitTimeMonth:
days := tool.DayDiff(startTime, sub.ExpireTime)
return subInfo.UnitPrice / 30 * days, nil
case UnitTimeDay:
days := tool.DayDiff(startTime, sub.ExpireTime)
return subInfo.UnitPrice * days, nil
case UnitTimeHour:
hours := int(tool.HourDiff(startTime, sub.ExpireTime))
return subInfo.UnitPrice * int64(hours), nil
case UnitTimeMinute:
minutes := tool.HourDiff(startTime, sub.ExpireTime) * 60
return subInfo.UnitPrice * minutes, nil
default:
return subInfo.UnitPrice, nil
}
}
func (l *QuotaTaskLogic) createGiftLog(tx *gorm.DB, subscribeId, userId, amount, balance int64, now time.Time) error {
giftLog := &log.Gift{
Type: log.GiftTypeIncrease,
OrderNo: "",
SubscribeId: subscribeId,
Amount: amount,
Balance: balance,
Remark: "Quota task gift",
Timestamp: now.UnixMilli(),
}
logString, err := giftLog.Marshal()
if err != nil {
return fmt.Errorf("marshal gift log error: %v", err)
}
return tx.Model(&log.SystemLog{}).Create(&log.SystemLog{
Type: log.TypeGift.Uint8(),
Content: string(logString),
ObjectID: userId,
Date: now.Format(time.DateOnly),
}).Error
}
func (l *QuotaTaskLogic) createResetTrafficLog(tx *gorm.DB, subscribeId, userId int64, now time.Time) error {
trafficLog := &log.ResetSubscribe{
Type: log.ResetSubscribeTypeQuota,
UserId: userId,
OrderNo: "",
Timestamp: now.UnixMilli(),
}
logString, err := trafficLog.Marshal()
if err != nil {
return fmt.Errorf("marshal traffic log error: %v", err)
}
return tx.Model(&log.SystemLog{}).Create(&log.SystemLog{
Type: log.TypeResetSubscribe.Uint8(),
Content: string(logString),
ObjectID: subscribeId,
Date: now.Format(time.DateOnly),
}).Error
}