refactor: 更新项目引用路径从perfect-panel/ppanel-server到perfect-panel/server
Build docker and publish / build (20.15.1) (push) Failing after 6m27s

feat: 添加版本和构建时间变量
fix: 修正短信队列类型注释错误
style: 清理未使用的代码和测试文件
docs: 更新安装文档中的下载链接
chore: 迁移数据库脚本添加日志和订阅配置
This commit is contained in:
2025-10-13 01:33:03 -07:00
parent 393b42f35a
commit c582087c0f
974 changed files with 23609 additions and 23398 deletions
+21 -11
View File
@@ -2,15 +2,16 @@ package handler
import (
"github.com/hibiken/asynq"
"github.com/perfect-panel/ppanel-server/internal/svc"
countrylogic "github.com/perfect-panel/ppanel-server/queue/logic/country"
orderLogic "github.com/perfect-panel/ppanel-server/queue/logic/order"
smslogic "github.com/perfect-panel/ppanel-server/queue/logic/sms"
"github.com/perfect-panel/ppanel-server/queue/logic/subscription"
"github.com/perfect-panel/ppanel-server/queue/logic/traffic"
"github.com/perfect-panel/ppanel-server/queue/types"
"github.com/perfect-panel/server/internal/svc"
countrylogic "github.com/perfect-panel/server/queue/logic/country"
orderLogic "github.com/perfect-panel/server/queue/logic/order"
smslogic "github.com/perfect-panel/server/queue/logic/sms"
"github.com/perfect-panel/server/queue/logic/subscription"
"github.com/perfect-panel/server/queue/logic/task"
"github.com/perfect-panel/server/queue/logic/traffic"
"github.com/perfect-panel/server/queue/types"
emailLogic "github.com/perfect-panel/ppanel-server/queue/logic/email"
emailLogic "github.com/perfect-panel/server/queue/logic/email"
)
func RegisterHandlers(mux *asynq.ServeMux, serverCtx *svc.ServiceContext) {
@@ -20,7 +21,6 @@ func RegisterHandlers(mux *asynq.ServeMux, serverCtx *svc.ServiceContext) {
mux.Handle(types.ForthwithSendEmail, emailLogic.NewSendEmailLogic(serverCtx))
// Send sms task
mux.Handle(types.ForthwithSendSms, smslogic.NewSendSmsLogic(serverCtx))
// Defer close order task
mux.Handle(types.DeferCloseOrder, orderLogic.NewDeferCloseOrderLogic(serverCtx))
// Forthwith activate order task
@@ -34,6 +34,16 @@ func RegisterHandlers(mux *asynq.ServeMux, serverCtx *svc.ServiceContext) {
// Schedule total server data
mux.Handle(types.SchedulerTotalServerData, traffic.NewServerDataLogic(serverCtx))
//定时查单
mux.Handle(types.SchedulerCheckOrder, orderLogic.NewCheckOrderLogic(serverCtx))
// Schedule reset traffic
mux.Handle(types.SchedulerResetTraffic, traffic.NewResetTrafficLogic(serverCtx))
// ScheduledBatchSendEmail
mux.Handle(types.ScheduledBatchSendEmail, emailLogic.NewBatchEmailLogic(serverCtx))
// ScheduledTrafficStat
mux.Handle(types.SchedulerTrafficStat, traffic.NewStatLogic(serverCtx))
// ForthwithQuotaTask
mux.Handle(types.ForthwithQuotaTask, task.NewQuotaTaskLogic(serverCtx))
}
+1 -39
View File
@@ -2,14 +2,9 @@ package countrylogic
import (
"context"
"encoding/json"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"github.com/hibiken/asynq"
"github.com/perfect-panel/ppanel-server/internal/svc"
"github.com/perfect-panel/ppanel-server/pkg/ip"
"github.com/perfect-panel/ppanel-server/queue/types"
"github.com/perfect-panel/server/internal/svc"
)
type GetNodeCountryLogic struct {
@@ -22,39 +17,6 @@ func NewGetNodeCountryLogic(svcCtx *svc.ServiceContext) *GetNodeCountryLogic {
}
}
func (l *GetNodeCountryLogic) ProcessTask(ctx context.Context, task *asynq.Task) error {
var payload types.GetNodeCountry
if err := json.Unmarshal(task.Payload(), &payload); err != nil {
logger.WithContext(ctx).Error("[GetNodeCountryLogic] Unmarshal payload failed",
logger.Field("error", err.Error()),
logger.Field("payload", task.Payload()),
)
return nil
}
serverAddr := payload.ServerAddr
resp, err := ip.GetRegionByIp(serverAddr)
if err != nil {
logger.WithContext(ctx).Error("[GetNodeCountryLogic] ", logger.Field("error", err.Error()), logger.Field("serverAddr", serverAddr))
return nil
}
servers, err := l.svcCtx.ServerModel.FindNodeByServerAddrAndProtocol(ctx, payload.ServerAddr, payload.Protocol)
if err != nil {
logger.WithContext(ctx).Error("[GetNodeCountryLogic] FindNodeByServerAddrAnd", logger.Field("error", err.Error()), logger.Field("serverAddr", serverAddr))
return err
}
if len(servers) == 0 {
return nil
}
for _, ser := range servers {
ser.Country = resp.Country
ser.City = resp.City
ser.Latitude = resp.Latitude
ser.Longitude = resp.Longitude
err := l.svcCtx.ServerModel.Update(ctx, ser)
if err != nil {
logger.WithContext(ctx).Error("[GetNodeCountryLogic] ", logger.Field("error", err.Error()), logger.Field("id", ser.Id))
}
}
logger.WithContext(ctx).Info("[GetNodeCountryLogic] ", logger.Field("country", resp.Country), logger.Field("city", resp.Country))
return nil
}
+78
View File
@@ -0,0 +1,78 @@
package emailLogic
import (
"context"
"strconv"
"github.com/hibiken/asynq"
taskModel "github.com/perfect-panel/server/internal/model/task"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/pkg/email"
"github.com/perfect-panel/server/pkg/logger"
)
type BatchEmailLogic struct {
svcCtx *svc.ServiceContext
}
type ErrorInfo struct {
Error string `json:"error"`
Email string `json:"email"`
Time int64 `json:"time"`
}
func NewBatchEmailLogic(svcCtx *svc.ServiceContext) *BatchEmailLogic {
return &BatchEmailLogic{
svcCtx: svcCtx,
}
}
func (l *BatchEmailLogic) ProcessTask(ctx context.Context, task *asynq.Task) error {
// 解析任务负载
payload := task.Payload()
if len(payload) == 0 {
logger.Error("[BatchEmailLogic] ProcessTask failed: empty payload")
return asynq.SkipRetry
}
// 转换获取任务id
taskID, err := strconv.ParseInt(string(payload), 10, 64)
if err != nil {
logger.WithContext(ctx).Error("[BatchEmailLogic] ProcessTask failed: invalid task ID",
logger.Field("error", err.Error()),
logger.Field("payload", string(payload)),
)
return asynq.SkipRetry
}
tx := l.svcCtx.DB.WithContext(ctx)
var taskInfo taskModel.Task
if err = tx.Model(&taskModel.Task{}).Where("id = ?", taskID).First(&taskInfo).Error; err != nil {
logger.WithContext(ctx).Error("[BatchEmailLogic] ProcessTask failed",
logger.Field("error", err.Error()),
logger.Field("taskID", taskID),
)
return asynq.SkipRetry
}
if taskInfo.Status != 0 {
logger.WithContext(ctx).Info("[BatchEmailLogic] ProcessTask skipped: task already processed",
logger.Field("taskID", taskID),
logger.Field("status", taskInfo.Status),
)
return nil
}
sender, err := email.NewSender(l.svcCtx.Config.Email.Platform, l.svcCtx.Config.Email.PlatformConfig, l.svcCtx.Config.Site.SiteName)
if err != nil {
logger.WithContext(ctx).Error("[BatchEmailLogic] NewSender failed", logger.Field("error", err.Error()))
return nil
}
manager := email.NewWorkerManager(l.svcCtx.DB, sender)
if manager == nil {
logger.WithContext(ctx).Error("[BatchEmailLogic] ProcessTask failed: worker manager is nil")
return asynq.SkipRetry
}
// 添加或获取 Worker 实例
manager.AddWorker(taskID)
return nil
}
+106 -11
View File
@@ -1,16 +1,19 @@
package emailLogic
import (
"bytes"
"context"
"encoding/json"
"text/template"
"time"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"github.com/perfect-panel/server/pkg/logger"
"github.com/hibiken/asynq"
"github.com/perfect-panel/ppanel-server/internal/model/log"
"github.com/perfect-panel/ppanel-server/internal/svc"
"github.com/perfect-panel/ppanel-server/pkg/email"
"github.com/perfect-panel/ppanel-server/queue/types"
"github.com/perfect-panel/server/internal/model/log"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/pkg/email"
"github.com/perfect-panel/server/queue/types"
)
type SendEmailLogic struct {
@@ -31,8 +34,7 @@ func (l *SendEmailLogic) ProcessTask(ctx context.Context, task *asynq.Task) erro
)
return nil
}
messageLog := log.MessageLog{
Type: log.Email.String(),
messageLog := log.Message{
Platform: l.svcCtx.Config.Email.Platform,
To: payload.Email,
Subject: payload.Subject,
@@ -43,18 +45,111 @@ func (l *SendEmailLogic) ProcessTask(ctx context.Context, task *asynq.Task) erro
logger.WithContext(ctx).Error("[SendEmailLogic] NewSender failed", logger.Field("error", err.Error()))
return nil
}
err = sender.Send([]string{payload.Email}, payload.Subject, payload.Content)
var content string
switch payload.Type {
case types.EmailTypeVerify:
tpl, _ := template.New("verify").Parse(l.svcCtx.Config.Email.VerifyEmailTemplate)
var result bytes.Buffer
payload.Content["Type"] = uint8(payload.Content["Type"].(float64))
err = tpl.Execute(&result, payload.Content)
if err != nil {
logger.WithContext(ctx).Error("[SendEmailLogic] Execute template failed",
logger.Field("error", err.Error()),
logger.Field("data", payload.Content),
)
return nil
}
content = result.String()
case types.EmailTypeMaintenance:
tpl, _ := template.New("maintenance").Parse(l.svcCtx.Config.Email.MaintenanceEmailTemplate)
var result bytes.Buffer
err = tpl.Execute(&result, payload.Content)
if err != nil {
logger.WithContext(ctx).Error("[SendEmailLogic] Execute template failed",
logger.Field("error", err.Error()),
logger.Field("template", l.svcCtx.Config.Email.MaintenanceEmailTemplate),
logger.Field("data", payload.Content),
)
return nil
}
content = result.String()
case types.EmailTypeExpiration:
tpl, _ := template.New("expiration").Parse(l.svcCtx.Config.Email.ExpirationEmailTemplate)
var result bytes.Buffer
err = tpl.Execute(&result, payload.Content)
if err != nil {
logger.WithContext(ctx).Error("[SendEmailLogic] Execute template failed",
logger.Field("error", err.Error()),
logger.Field("template", l.svcCtx.Config.Email.ExpirationEmailTemplate),
logger.Field("data", payload.Content),
)
return nil
}
content = result.String()
case types.EmailTypeTrafficExceed:
tpl, _ := template.New("traffic_exceed").Parse(l.svcCtx.Config.Email.TrafficExceedEmailTemplate)
var result bytes.Buffer
err = tpl.Execute(&result, payload.Content)
if err != nil {
logger.WithContext(ctx).Error("[SendEmailLogic] Execute template failed",
logger.Field("error", err.Error()),
logger.Field("template", l.svcCtx.Config.Email.TrafficExceedEmailTemplate),
logger.Field("data", payload.Content),
)
return nil
}
content = result.String()
case types.EmailTypeCustom:
if payload.Content == nil {
logger.WithContext(ctx).Error("[SendEmailLogic] Custom email content is empty",
logger.Field("payload", payload),
)
return nil
}
if tpl, ok := payload.Content["content"].(string); !ok {
logger.WithContext(ctx).Error("[SendEmailLogic] Custom email content is not a string",
logger.Field("payload", payload),
)
return nil
} else {
content = tpl
}
default:
logger.WithContext(ctx).Error("[SendEmailLogic] Unsupported email type",
logger.Field("type", payload.Type),
logger.Field("payload", payload),
)
return nil
}
err = sender.Send([]string{payload.Email}, payload.Subject, content)
if err != nil {
logger.WithContext(ctx).Error("[SendEmailLogic] Send email failed", logger.Field("error", err.Error()))
return nil
}
messageLog.Status = 1
if err = l.svcCtx.LogModel.InsertMessageLog(ctx, &messageLog); err != nil {
logger.WithContext(ctx).Error("[SendEmailLogic] InsertMessageLog failed",
emailLog, err := messageLog.Marshal()
if err != nil {
logger.WithContext(ctx).Error("[SendEmailLogic] Marshal message log failed",
logger.Field("error", err.Error()),
logger.Field("messageLog", messageLog),
)
return nil
}
if err = l.svcCtx.LogModel.Insert(ctx, &log.SystemLog{
Type: log.TypeEmailMessage.Uint8(),
Date: time.Now().Format("2006-01-02"),
ObjectID: 0,
Content: string(emailLog),
}); err != nil {
logger.WithContext(ctx).Error("[SendEmailLogic] Insert email log failed",
logger.Field("error", err.Error()),
logger.Field("emailLog", string(emailLog)),
)
return nil
}
logger.WithContext(ctx).Info("[SendEmailLogic] Send email", logger.Field("email", payload.Email), logger.Field("content", payload.Content))
return nil
}
File diff suppressed because it is too large Load Diff
+675
View File
@@ -0,0 +1,675 @@
package orderLogic
import (
"context"
"encoding/json"
"fmt"
"strconv"
"time"
"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/config"
"github.com/perfect-panel/server/internal/logic/telegram"
"github.com/perfect-panel/server/internal/model/order"
"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"
)
const (
Subscribe = 1
Renewal = 2
ResetTraffic = 3
Recharge = 4
)
type ActivateOrderLogic struct {
svc *svc.ServiceContext
}
func NewActivateOrderLogic(svc *svc.ServiceContext) *ActivateOrderLogic {
return &ActivateOrderLogic{
svc: svc,
}
}
func (l *ActivateOrderLogic) ProcessTask(ctx context.Context, task *asynq.Task) error {
payload := types.ForthwithActivateOrderPayload{}
if err := json.Unmarshal(task.Payload(), &payload); err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Unmarshal payload failed",
logger.Field("error", err.Error()),
logger.Field("payload", string(task.Payload())),
)
return nil
}
// Find order by order no
orderInfo, err := l.svc.OrderModel.FindOneByOrderNo(ctx, payload.OrderNo)
if err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Find order failed",
logger.Field("error", err.Error()),
logger.Field("order_no", payload.OrderNo),
)
return nil
}
// 1: Pending, 2: Paid, 3:Close, 4: Failed, 5:Finished
if orderInfo.Status != 2 {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Order status error",
logger.Field("order_no", orderInfo.OrderNo),
logger.Field("status", orderInfo.Status),
)
return nil
}
switch orderInfo.Type {
case Subscribe:
err = l.NewPurchase(ctx, orderInfo)
case Renewal:
err = l.Renewal(ctx, orderInfo)
case ResetTraffic:
err = l.ResetTraffic(ctx, orderInfo)
case Recharge:
err = l.Recharge(ctx, orderInfo)
default:
logger.WithContext(ctx).Error("[ActivateOrderLogic] Order type is invalid", logger.Field("type", orderInfo.Type))
return nil
}
if err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Process task failed", logger.Field("error", err.Error()))
return nil
}
// if coupon is not empty
if orderInfo.Coupon != "" {
// update coupon status
err = l.svc.CouponModel.UpdateCount(ctx, orderInfo.Coupon)
if err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Update coupon status failed",
logger.Field("error", err.Error()),
logger.Field("coupon", orderInfo.Coupon),
)
}
}
// update order status
orderInfo.Status = 5
err = l.svc.OrderModel.Update(ctx, orderInfo)
if err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Update order status failed",
logger.Field("error", err.Error()),
logger.Field("order_no", orderInfo.OrderNo),
)
}
return nil
}
// NewPurchase New purchase
func (l *ActivateOrderLogic) NewPurchase(ctx context.Context, orderInfo *order.Order) error {
var userInfo *user.User
var err error
if orderInfo.UserId != 0 {
// find user by user id
userInfo, err = l.svc.UserModel.FindOne(ctx, orderInfo.UserId)
if err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Find user failed",
logger.Field("error", err.Error()),
logger.Field("user_id", orderInfo.UserId),
)
return err
}
} else {
// If User ID is 0, it means that the order is a guest order, need to create a new user
// query info with redis
cacheKey := fmt.Sprintf(constant.TempOrderCacheKey, orderInfo.OrderNo)
data, err := l.svc.Redis.Get(ctx, cacheKey).Result()
if err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Get temp order cache failed",
logger.Field("error", err.Error()),
logger.Field("cache_key", cacheKey),
)
return err
}
var tempOrder constant.TemporaryOrderInfo
if err = json.Unmarshal([]byte(data), &tempOrder); err != nil {
logger.WithContext(ctx).Errorw("[ActivateOrderLogic] Unmarshal temp order failed",
logger.Field("error", err.Error()),
)
return err
}
// create user
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 {
// Save user information
if err := tx.Save(userInfo).Error; err != nil {
return err
}
// Generate ReferCode
userInfo.ReferCode = uuidx.UserInviteCode(userInfo.Id)
// Update ReferCode
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("[ActivateOrderLogic] Create user failed",
logger.Field("error", err.Error()),
)
return err
}
if tempOrder.InviteCode != "" {
// find referer by refer code
referer, err := l.svc.UserModel.FindOneByReferCode(ctx, tempOrder.InviteCode)
if err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Find referer failed",
logger.Field("error", err.Error()),
logger.Field("refer_code", tempOrder.InviteCode),
)
} else {
userInfo.RefererId = referer.Id
err = l.svc.UserModel.Update(ctx, userInfo)
if err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Update user referer failed",
logger.Field("error", err.Error()),
logger.Field("user_id", userInfo.Id),
)
}
}
}
logger.WithContext(ctx).Info("[ActivateOrderLogic] Create guest user success", logger.Field("user_id", userInfo.Id), logger.Field("Identifier", tempOrder.Identifier), logger.Field("AuthType", tempOrder.AuthType))
}
// find subscribe by id
sub, err := l.svc.SubscribeModel.FindOne(ctx, orderInfo.SubscribeId)
if err != nil {
logger.WithContext(ctx).Errorw("[ActivateOrderLogic] Find subscribe failed",
logger.Field("error", err.Error()),
logger.Field("subscribe_id", orderInfo.SubscribeId),
)
return err
}
// create user subscribe
now := time.Now()
userSub := user.Subscribe{
Id: 0,
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,
}
err = l.svc.UserModel.InsertSubscribe(ctx, &userSub)
if err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Insert user subscribe failed",
logger.Field("error", err.Error()),
)
return err
}
// handler commission
if userInfo.RefererId != 0 &&
l.svc.Config.Invite.ReferralPercentage != 0 &&
(!l.svc.Config.Invite.OnlyFirstPurchase || orderInfo.IsNew) {
referer, err := l.svc.UserModel.FindOne(ctx, userInfo.RefererId)
if err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Find referer failed",
logger.Field("error", err.Error()),
logger.Field("referer_id", userInfo.RefererId),
)
goto updateCache
}
// calculate commission
amount := float64(orderInfo.Price) * (float64(l.svc.Config.Invite.ReferralPercentage) / 100)
referer.Commission += int64(amount)
err = l.svc.UserModel.Update(ctx, referer)
if err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Update referer commission failed",
logger.Field("error", err.Error()),
)
goto updateCache
}
// create commission log
commissionLog := user.CommissionLog{
UserId: referer.Id,
OrderNo: orderInfo.OrderNo,
Amount: int64(amount),
}
err = l.svc.UserModel.InsertCommissionLog(ctx, &commissionLog)
if err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Insert commission log failed",
logger.Field("error", err.Error()),
)
}
err = l.svc.UserModel.UpdateUserCache(ctx, referer)
if err != nil {
logger.WithContext(ctx).Errorw("[ActivateOrderLogic] Update referer cache", logger.Field("error", err.Error()), logger.Field("user_id", referer.Id))
}
}
updateCache:
for _, id := range tool.StringToInt64Slice(sub.Server) {
cacheKey := fmt.Sprintf("%s%d", config.ServerUserListCacheKey, id)
err = l.svc.Redis.Del(ctx, cacheKey).Err()
if err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Del server user list cache failed",
logger.Field("error", err.Error()),
logger.Field("cache_key", cacheKey),
)
}
}
data, err := l.svc.ServerModel.FindServerListByGroupIds(ctx, tool.StringToInt64Slice(sub.ServerGroup))
if err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Find server list failed", logger.Field("error", err.Error()))
return err
}
for _, item := range data {
cacheKey := fmt.Sprintf("%s%d", config.ServerUserListCacheKey, item.Id)
err = l.svc.Redis.Del(ctx, cacheKey).Err()
if err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Del server user list cache failed",
logger.Field("error", err.Error()),
logger.Field("cache_key", cacheKey),
)
}
}
userTelegramChatId, ok := findTelegram(userInfo)
// sendMessage To Telegram
if ok {
text, err := tool.RenderTemplateToString(telegram.PurchaseNotify, map[string]string{
"OrderNo": orderInfo.OrderNo,
"SubscribeName": sub.Name,
"OrderAmount": fmt.Sprintf("%.2f", float64(orderInfo.Price)/100),
"ExpireTime": userSub.ExpireTime.Format("2006-01-02 15:04:05"),
})
if err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Render template failed",
logger.Field("error", err.Error()),
)
}
l.sendUserNotifyWithTelegram(userTelegramChatId, text)
}
// send message to admin
text, err := tool.RenderTemplateToString(telegram.AdminOrderNotify, map[string]string{
"OrderNo": orderInfo.OrderNo,
"TradeNo": orderInfo.TradeNo,
"SubscribeName": sub.Name,
//"UserEmail": userInfo.Email,
"OrderAmount": fmt.Sprintf("%.2f", float64(orderInfo.Price)/100),
"OrderStatus": "已支付",
"OrderTime": orderInfo.CreatedAt.Format("2006-01-02 15:04:05"),
"PaymentMethod": orderInfo.Method,
})
if err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Render AdminOrderNotify template failed",
logger.Field("error", err.Error()),
)
}
l.sendAdminNotifyWithTelegram(ctx, text)
logger.WithContext(ctx).Info("[ActivateOrderLogic] Insert user subscribe success")
return nil
}
// Renewal Renewal
func (l *ActivateOrderLogic) Renewal(ctx context.Context, orderInfo *order.Order) error {
// find user by user id
userInfo, err := l.svc.UserModel.FindOne(ctx, orderInfo.UserId)
if err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Find user failed",
logger.Field("error", err.Error()),
logger.Field("user_id", orderInfo.UserId),
)
return err
}
// find user subscribe by subscribe token
userSub, err := l.svc.UserModel.FindOneSubscribeByToken(ctx, orderInfo.SubscribeToken)
if err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Find user subscribe failed",
logger.Field("error", err.Error()),
logger.Field("order_id", orderInfo.Id),
)
return err
}
// find subscribe by id
sub, err := l.svc.SubscribeModel.FindOne(ctx, orderInfo.SubscribeId)
if err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Find subscribe failed",
logger.Field("error", err.Error()),
logger.Field("subscribe_id", orderInfo.SubscribeId),
logger.Field("order_id", orderInfo.Id),
)
return err
}
now := time.Now()
if userSub.ExpireTime.Before(now) {
userSub.ExpireTime = now
}
// Check whether traffic reset on renewal is enabled
if sub.RenewalReset != nil && *sub.RenewalReset {
userSub.Download = 0
userSub.Upload = 0
}
if userSub.FinishedAt != nil {
userSub.FinishedAt = nil
}
userSub.ExpireTime = tool.AddTime(sub.UnitTime, orderInfo.Quantity, userSub.ExpireTime)
userSub.Status = 1
// update user subscribe
err = l.svc.UserModel.UpdateSubscribe(ctx, userSub)
if err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Update user subscribe failed",
logger.Field("error", err.Error()),
)
return err
}
// handler commission
if userInfo.RefererId != 0 &&
l.svc.Config.Invite.ReferralPercentage != 0 &&
!l.svc.Config.Invite.OnlyFirstPurchase {
referer, err := l.svc.UserModel.FindOne(ctx, userInfo.RefererId)
if err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Find referer failed",
logger.Field("error", err.Error()),
logger.Field("referer_id", userInfo.RefererId),
)
goto sendMessage
}
// calculate commission
amount := float64(orderInfo.Price) * (float64(l.svc.Config.Invite.ReferralPercentage) / 100)
referer.Commission += int64(amount)
err = l.svc.UserModel.Update(ctx, referer)
if err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Update referer commission failed",
logger.Field("error", err.Error()),
)
goto sendMessage
}
// create commission log
commissionLog := user.CommissionLog{
UserId: referer.Id,
OrderNo: orderInfo.OrderNo,
Amount: int64(amount),
}
err = l.svc.UserModel.InsertCommissionLog(ctx, &commissionLog)
if err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Insert commission log failed",
logger.Field("error", err.Error()),
)
}
err = l.svc.UserModel.UpdateUserCache(ctx, referer)
if err != nil {
logger.WithContext(ctx).Errorw("[ActivateOrderLogic] Update referer cache", logger.Field("error", err.Error()), logger.Field("user_id", referer.Id))
}
}
sendMessage:
userTelegramChatId, ok := findTelegram(userInfo)
// SendMessage To Telegram
if ok {
text, err := tool.RenderTemplateToString(telegram.RenewalNotify, map[string]string{
"OrderNo": orderInfo.OrderNo,
"SubscribeName": sub.Name,
"OrderAmount": fmt.Sprintf("%.2f", float64(orderInfo.Price)/100),
"ExpireTime": userSub.ExpireTime.Format("2006-01-02 15:04:05"),
})
if err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Render template failed",
logger.Field("error", err.Error()),
)
}
l.sendUserNotifyWithTelegram(userTelegramChatId, text)
}
// send message to admin
text, err := tool.RenderTemplateToString(telegram.AdminOrderNotify, map[string]string{
"OrderNo": orderInfo.OrderNo,
"TradeNo": orderInfo.TradeNo,
"SubscribeName": sub.Name,
//"UserEmail": userInfo.Email,
"OrderAmount": fmt.Sprintf("%.2f", float64(orderInfo.Price)/100),
"OrderStatus": "已支付",
"OrderTime": orderInfo.CreatedAt.Format("2006-01-02 15:04:05"),
"PaymentMethod": orderInfo.Method,
})
if err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Render AdminOrderNotify template failed",
logger.Field("error", err.Error()),
)
}
l.sendAdminNotifyWithTelegram(ctx, text)
return nil
}
// ResetTraffic Reset traffic
func (l *ActivateOrderLogic) ResetTraffic(ctx context.Context, orderInfo *order.Order) error {
// find user by user id
userInfo, err := l.svc.UserModel.FindOne(ctx, orderInfo.UserId)
if err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Find user failed",
logger.Field("error", err.Error()),
logger.Field("user_id", orderInfo.UserId),
)
return err
}
// Generate a Subscribe Token through orderNo
// find user subscribe by subscribe token
userSub, err := l.svc.UserModel.FindOneSubscribeByToken(ctx, orderInfo.SubscribeToken)
if err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Find user subscribe failed",
logger.Field("error", err.Error()),
logger.Field("order_id", orderInfo.Id),
)
return err
}
userSub.Download = 0
userSub.Upload = 0
userSub.Status = 1
// update user subscribe
err = l.svc.UserModel.UpdateSubscribe(ctx, userSub)
if err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Update user subscribe failed",
logger.Field("error", err.Error()),
)
return err
}
sub, err := l.svc.SubscribeModel.FindOne(ctx, userSub.SubscribeId)
if err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Find subscribe failed",
logger.Field("error", err.Error()),
logger.Field("subscribe_id", userSub.SubscribeId),
)
return err
}
userTelegramChatId, ok := findTelegram(userInfo)
// SendMessage To Telegram
if ok {
text, err := tool.RenderTemplateToString(telegram.ResetTrafficNotify, map[string]string{
"OrderNo": orderInfo.OrderNo,
"SubscribeName": sub.Name,
"ResetTime": time.Now().Format("2006-01-02 15:04:05"),
"ExpireTime": userSub.ExpireTime.Format("2006-01-02 15:04:05"),
})
if err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Render template failed",
logger.Field("error", err.Error()),
)
}
l.sendUserNotifyWithTelegram(userTelegramChatId, text)
}
// send message to admin
text, err := tool.RenderTemplateToString(telegram.AdminOrderNotify, map[string]string{
"OrderNo": orderInfo.OrderNo,
"TradeNo": orderInfo.TradeNo,
"SubscribeName": "流量重置",
"OrderAmount": fmt.Sprintf("%.2f", float64(orderInfo.Price)/100),
"OrderStatus": "已支付",
"OrderTime": orderInfo.CreatedAt.Format("2006-01-02 15:04:05"),
"PaymentMethod": orderInfo.Method,
})
if err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Render AdminOrderNotify template failed",
logger.Field("error", err.Error()),
)
}
l.sendAdminNotifyWithTelegram(ctx, text)
return nil
}
// Recharge Recharge to user
func (l *ActivateOrderLogic) Recharge(ctx context.Context, orderInfo *order.Order) error {
// find user by user id
userInfo, err := l.svc.UserModel.FindOne(ctx, orderInfo.UserId)
if err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Find user failed",
logger.Field("error", err.Error()),
logger.Field("user_id", orderInfo.UserId),
)
return err
}
userInfo.Balance += orderInfo.Price
// update user
err = l.svc.DB.Transaction(func(tx *gorm.DB) error {
err = l.svc.UserModel.Update(ctx, userInfo, tx)
if err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Update user failed",
logger.Field("error", err.Error()),
)
return err
}
// Create Balance Log
balanceLog := user.BalanceLog{
UserId: orderInfo.UserId,
Amount: orderInfo.Price,
Type: 1,
OrderId: orderInfo.Id,
Balance: userInfo.Balance,
}
err = l.svc.UserModel.InsertBalanceLog(ctx, &balanceLog, tx)
if err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Insert balance log failed",
logger.Field("error", err.Error()),
)
return err
}
return nil
})
if err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Database transaction failed",
logger.Field("error", err.Error()),
)
return err
}
userTelegramChatId, ok := findTelegram(userInfo)
// SendMessage To Telegram
if ok {
text, err := tool.RenderTemplateToString(telegram.RechargeNotify, 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 err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Render template failed",
logger.Field("error", err.Error()),
)
}
l.sendUserNotifyWithTelegram(userTelegramChatId, text)
}
// send message to admin
text, err := tool.RenderTemplateToString(telegram.AdminOrderNotify, 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 err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Render AdminOrderNotify template failed",
logger.Field("error", err.Error()),
)
}
l.sendAdminNotifyWithTelegram(ctx, text)
return nil
}
// sendUserNotifyWithTelegram send message to user
func (l *ActivateOrderLogic) sendUserNotifyWithTelegram(chatId int64, text string) {
msg := tgbotapi.NewMessage(chatId, text)
msg.ParseMode = "markdown"
_, err := l.svc.TelegramBot.Send(msg)
if err != nil {
logger.Error("[ActivateOrderLogic] Send telegram user message failed",
logger.Field("error", err.Error()),
)
}
}
// sendAdminNotifyWithTelegram send message to admin
func (l *ActivateOrderLogic) sendAdminNotifyWithTelegram(ctx context.Context, text string) {
admins, err := l.svc.UserModel.QueryAdminUsers(ctx)
if err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Query admin users failed",
logger.Field("error", err.Error()),
)
return
}
for _, admin := range admins {
telegramId, ok := findTelegram(admin)
if !ok {
continue
}
msg := tgbotapi.NewMessage(telegramId, text)
msg.ParseMode = "markdown"
_, err := l.svc.TelegramBot.Send(msg)
if err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Send telegram admin message failed",
logger.Field("error", err.Error()),
)
}
}
}
// findTelegram find user telegram id
func findTelegram(u *user.User) (int64, bool) {
for _, item := range u.AuthMethods {
if item.AuthType == "telegram" {
// string to int64
parseInt, err := strconv.ParseInt(item.AuthIdentifier, 10, 64)
if err != nil {
return 0, false
}
return parseInt, true
}
}
return 0, false
}
-161
View File
@@ -1,161 +0,0 @@
package orderLogic
import (
"context"
"encoding/json"
"github.com/hibiken/asynq"
order2 "github.com/perfect-panel/ppanel-server/internal/logic/public/order"
"github.com/perfect-panel/ppanel-server/internal/model/payment"
"github.com/perfect-panel/ppanel-server/internal/svc"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"github.com/perfect-panel/ppanel-server/pkg/payment/alipay"
"github.com/perfect-panel/ppanel-server/pkg/payment/payssion"
"github.com/perfect-panel/ppanel-server/pkg/payment/stripe"
"github.com/perfect-panel/ppanel-server/queue/types"
"go.uber.org/zap"
)
type CheckOrderLogic struct {
svc *svc.ServiceContext
}
func NewCheckOrderLogic(svc *svc.ServiceContext) *CheckOrderLogic {
return &CheckOrderLogic{
svc: svc,
}
}
func (l *CheckOrderLogic) ProcessTask(ctx context.Context, task *asynq.Task) error {
orderList, err := l.svc.OrderModel.QueryPendingOrders(ctx)
if err != nil {
logger.Errorf("query pending orders error: %v", zap.Error(err))
return err
}
logger.Infof("查到订单数据: %v", orderList)
for _, order := range orderList {
paymentConfig, err := l.svc.PaymentModel.FindOne(ctx, order.PaymentId)
if err != nil {
logger.Errorw("[CheckOrder] Find payment config failed", logger.Field("error", err.Error()), logger.Field("paymentMark", order.Method))
continue
}
logger.Infof("查到配置数据[%s]: %v", order.Method, orderList)
var flag bool
switch order.Method {
case order2.AlipayF2f:
if l.queryAlipay(paymentConfig, order.TradeNo) {
flag = true
}
break
case order2.Payssion:
logger.Infof("匹配配置类型: %v", order2.Payssion)
if l.queryPayssion(paymentConfig, order.OrderNo) {
flag = true
}
break
case order2.StripeWeChatPay:
if l.queryStripe(paymentConfig, order.TradeNo) {
flag = true
}
break
default:
logger.Infow("[CheckOrder] Unsupported payment method", logger.Field("paymentMethod", order.Method))
continue
}
logger.Infof("[CheckOrder] Unsupported payment method[%v]", flag)
if flag {
err := l.svc.OrderModel.UpdateOrderStatus(ctx, order.OrderNo, 2)
if err != nil {
logger.Errorf("[CheckOrder] query order status error: %v", zap.Error(err))
}
logger.Info("[CheckOrder] Notify status success", logger.Field("orderNo", order.TradeNo))
payload := types.ForthwithActivateOrderPayload{
OrderNo: order.OrderNo,
}
bytes, err := json.Marshal(&payload)
if err != nil {
logger.Error("[CheckOrder] Marshal payload failed", logger.Field("error", err.Error()))
return err
}
task := asynq.NewTask(types.ForthwithActivateOrder, bytes)
taskInfo, err := l.svc.Queue.EnqueueContext(ctx, task)
if err != nil {
logger.Error("[CheckOrder] Enqueue task failed", logger.Field("error", err.Error()))
return err
}
logger.Info("[CheckOrder] Enqueue task success", logger.Field("taskInfo", taskInfo))
}
}
return nil
}
// queryAlipay Query Alipay payment status
//
//nolint:unused
func (l *CheckOrderLogic) queryAlipay(paymentConfig *payment.Payment, TradeNo string) bool {
config := payment.AlipayF2FConfig{}
if err := json.Unmarshal([]byte(paymentConfig.Config), &config); err != nil {
zap.S().Errorw("[CheckOrder] Unmarshal payment config failed", logger.Field("error", err.Error()), logger.Field("config", paymentConfig.Config))
return false
}
client := alipay.NewClient(alipay.Config{
AppId: config.AppId,
PrivateKey: config.PrivateKey,
PublicKey: config.PublicKey,
InvoiceName: config.InvoiceName,
})
status, err := client.QueryTrade(context.Background(), TradeNo)
if err != nil {
zap.S().Errorw("[CheckOrder] Query trade failed", logger.Field("error", err.Error()), logger.Field("TradeNo", TradeNo))
return false
}
if status == alipay.Success || status == alipay.Finished {
return true
}
return false
}
// queryStripe Query Stripe payment status
//
//nolint:unused
func (l *CheckOrderLogic) queryStripe(paymentConfig *payment.Payment, TradeNo string) bool {
config := payment.StripeConfig{}
if err := json.Unmarshal([]byte(paymentConfig.Config), &config); err != nil {
zap.S().Errorw("[CheckOrder] Unmarshal payment config failed", logger.Field("error", err.Error()), logger.Field("config", paymentConfig.Config))
return false
}
client := stripe.NewClient(stripe.Config{
PublicKey: config.PublicKey,
SecretKey: config.SecretKey,
WebhookSecret: config.WebhookSecret,
})
status, err := client.QueryOrderStatus(TradeNo)
if err != nil {
zap.S().Errorw("[CheckOrder] Query order status failed", logger.Field("error", err.Error()), logger.Field("TradeNo", TradeNo))
return false
}
return status
}
// queryPayssion Query Stripe payment status
//
//nolint:unused
func (l *CheckOrderLogic) queryPayssion(paymentConfig *payment.Payment, TradeNo string) bool {
zap.S().Infof("[CheckOrder]1 Query Payssion called")
payssionConfig := payment.PayssionConfig{}
if err := json.Unmarshal([]byte(paymentConfig.Config), &payssionConfig); err != nil {
zap.S().Errorw("[CheckOrder] Unmarshal error", logger.Field("error", err.Error()))
return false
}
zap.S().Infof("[CheckOrder]2 Query Payssion called")
client := payssion.NewClient(payssionConfig.ApiKey, payssionConfig.SecretKey, payssionConfig.PmId, payssionConfig.Currency, payssionConfig.QueryUrl, payssionConfig.CreateUrl)
// create payment
result, err := client.QueryOrder(TradeNo)
if err != nil {
zap.S().Errorw("[CheckOrder] Query order status failed", logger.Field("error", err.Error()), logger.Field("TradeNo", TradeNo))
return false
}
zap.S().Infof("[CheckOrder]3 Query Payssion called")
return result.Transaction.State == "completed"
}
+5 -5
View File
@@ -4,13 +4,13 @@ import (
"context"
"encoding/json"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"github.com/perfect-panel/server/pkg/logger"
"github.com/hibiken/asynq"
"github.com/perfect-panel/ppanel-server/internal/logic/public/order"
"github.com/perfect-panel/ppanel-server/internal/svc"
internal "github.com/perfect-panel/ppanel-server/internal/types"
"github.com/perfect-panel/ppanel-server/queue/types"
"github.com/perfect-panel/server/internal/logic/public/order"
"github.com/perfect-panel/server/internal/svc"
internal "github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/queue/types"
)
type DeferCloseOrderLogic struct {
+19 -12
View File
@@ -4,15 +4,16 @@ import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"github.com/perfect-panel/server/pkg/logger"
"github.com/hibiken/asynq"
"github.com/perfect-panel/ppanel-server/internal/model/log"
"github.com/perfect-panel/ppanel-server/internal/svc"
"github.com/perfect-panel/ppanel-server/pkg/constant"
"github.com/perfect-panel/ppanel-server/pkg/sms"
"github.com/perfect-panel/ppanel-server/queue/types"
"github.com/perfect-panel/server/internal/model/log"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/pkg/constant"
"github.com/perfect-panel/server/pkg/sms"
"github.com/perfect-panel/server/queue/types"
)
type SmsSendCount struct {
@@ -43,17 +44,16 @@ func (l *SendSmsLogic) ProcessTask(ctx context.Context, task *asynq.Task) error
logger.WithContext(ctx).Error("[SendSmsLogic] New send sms client failed", logger.Field("error", err.Error()), logger.Field("payload", payload))
return err
}
createSms := &log.MessageLog{
Type: log.Mobile.String(),
createSms := &log.Message{
Platform: l.svcCtx.Config.Mobile.Platform,
To: fmt.Sprintf("+%s%s", payload.TelephoneArea, payload.Telephone),
Subject: constant.ParseVerifyType(payload.Type).String(),
Content: "",
Content: map[string]interface{}{
"content": client.GetSendCodeContent(payload.Content),
},
}
err = client.SendCode(payload.TelephoneArea, payload.Telephone, payload.Content)
createSms.Content = client.GetSendCodeContent(payload.Content)
if err != nil {
logger.WithContext(ctx).Error("[SendSmsLogic] Send sms failed", logger.Field("error", err.Error()), logger.Field("payload", payload))
if l.svcCtx.Config.Model != constant.DevMode {
@@ -64,7 +64,14 @@ func (l *SendSmsLogic) ProcessTask(ctx context.Context, task *asynq.Task) error
}
createSms.Status = 1
logger.WithContext(ctx).Info("[SendSmsLogic] Send sms", logger.Field("telephone", payload.Telephone), logger.Field("content", createSms.Content))
err = l.svcCtx.LogModel.InsertMessageLog(ctx, createSms)
content, _ := createSms.Marshal()
err = l.svcCtx.LogModel.Insert(ctx, &log.SystemLog{
Type: log.TypeMobileMessage.Uint8(),
Date: time.Now().Format("2006-01-02"),
ObjectID: 0,
Content: string(content),
})
if err != nil {
logger.WithContext(ctx).Error("[SendSmsLogic] Send sms failed", logger.Field("error", err.Error()), logger.Field("payload", payload))
return nil
@@ -1,19 +1,17 @@
package subscription
import (
"bytes"
"context"
"encoding/json"
"text/template"
"time"
queue "github.com/perfect-panel/ppanel-server/queue/types"
queue "github.com/perfect-panel/server/queue/types"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"github.com/perfect-panel/server/pkg/logger"
"github.com/hibiken/asynq"
"github.com/perfect-panel/ppanel-server/internal/model/user"
"github.com/perfect-panel/ppanel-server/internal/svc"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
"gorm.io/gorm"
)
@@ -32,7 +30,7 @@ func (l *CheckSubscriptionLogic) ProcessTask(ctx context.Context, _ *asynq.Task)
// Check subscription traffic
err := l.svc.UserModel.Transaction(ctx, func(db *gorm.DB) error {
var list []*user.Subscribe
err := db.Model(&user.Subscribe{}).Where("upload + download >= traffic AND status = 1 AND traffic > 0 ").Find(&list).Error
err := db.Model(&user.Subscribe{}).Where("upload + download >= traffic AND status IN (0, 1) AND traffic > 0 ").Find(&list).Error
if err != nil {
logger.Errorw("[Check Subscription Traffic] Query subscribe failed", logger.Field("error", err.Error()))
return err
@@ -62,7 +60,7 @@ func (l *CheckSubscriptionLogic) ProcessTask(ctx context.Context, _ *asynq.Task)
return err
}
}
l.clearServerCache(ctx, list...)
logger.Infow("[Check Subscription Traffic] Update subscribe status", logger.Field("user_ids", ids), logger.Field("count", int64(len(ids))))
} else {
@@ -77,7 +75,7 @@ func (l *CheckSubscriptionLogic) ProcessTask(ctx context.Context, _ *asynq.Task)
// Check subscription expire
err = l.svc.UserModel.Transaction(ctx, func(db *gorm.DB) error {
var list []*user.Subscribe
err = db.Model(&user.Subscribe{}).Where("`status` = 1 AND `expire_time` < ? AND `expire_time` != ? and `finished_at` IS NULL", time.Now(), time.UnixMilli(0)).Find(&list).Error
err = db.Model(&user.Subscribe{}).Where("`status` IN (0, 1) AND `expire_time` < ? AND `expire_time` != ? and `finished_at` IS NULL", time.Now(), time.UnixMilli(0)).Find(&list).Error
if err != nil {
logger.Error("[Check Subscription] Find subscribe failed", logger.Field("error", err.Error()))
return err
@@ -87,7 +85,10 @@ func (l *CheckSubscriptionLogic) ProcessTask(ctx context.Context, _ *asynq.Task)
ids = append(ids, item.Id)
}
if len(ids) > 0 {
err = db.Model(&user.Subscribe{}).Where("id IN ?", ids).Update("status", 3).Error
err = db.Model(&user.Subscribe{}).Where("id IN ?", ids).Updates(map[string]interface{}{
"status": 3,
"finished_at": time.Now(),
}).Error
if err != nil {
logger.Error("[Check Subscription Expire] Update subscribe status failed", logger.Field("error", err.Error()))
return err
@@ -97,17 +98,17 @@ func (l *CheckSubscriptionLogic) ProcessTask(ctx context.Context, _ *asynq.Task)
logger.Error("[Check Subscription Expire] Send email failed", logger.Field("error", err.Error()))
return nil
}
if len(list) > 0 {
if err = l.svc.UserModel.ClearSubscribeCache(ctx, list...); err != nil {
logger.Errorw("[Check Subscription Traffic] Clear subscribe cache failed", logger.Field("error", err.Error()))
return err
}
if err = l.svc.UserModel.ClearSubscribeCache(ctx, list...); err != nil {
logger.Errorw("[Check Subscription Traffic] Clear subscribe cache failed", logger.Field("error", err.Error()))
return err
}
l.clearServerCache(ctx, list...)
logger.Info("[Check Subscription Expire] Update subscribe status", logger.Field("user_ids", ids), logger.Field("count", int64(len(ids))))
} else {
logger.Info("[Check Subscription Expire] No subscribe need to update")
}
return l.svc.UserModel.ClearSubscribeCache(ctx, list...)
return nil
})
if err != nil {
logger.Info("[CheckSubscription] Transaction failed", logger.Field("error", err.Error()))
@@ -128,24 +129,14 @@ func (l *CheckSubscriptionLogic) sendExpiredNotify(ctx context.Context, subs []i
continue
}
var taskPayload queue.SendEmailPayload
taskPayload.Type = queue.EmailTypeExpiration
taskPayload.Email = method.AuthIdentifier
taskPayload.Subject = "Subscription Expired"
tpl, err := template.New("Expired").Parse(l.svc.Config.Email.ExpirationEmailTemplate)
if err != nil {
logger.Errorw("[CheckSubscription] Parse template failed", logger.Field("error", err.Error()))
continue
}
var result bytes.Buffer
err = tpl.Execute(&result, map[string]interface{}{
taskPayload.Content = map[string]interface{}{
"SiteLogo": l.svc.Config.Site.SiteLogo,
"SiteName": l.svc.Config.Site.SiteName,
"ExpireDate": sub.ExpireTime.Format("2006-01-02 15:04:05"),
})
if err != nil {
logger.Errorw("[CheckSubscription] Execute template failed", logger.Field("error", err.Error()))
continue
}
taskPayload.Content = result.String()
payloadBuy, err := json.Marshal(taskPayload)
if err != nil {
logger.Errorw("[CheckSubscription] Marshal payload failed", logger.Field("error", err.Error()))
@@ -178,23 +169,13 @@ func (l *CheckSubscriptionLogic) sendTrafficNotify(ctx context.Context, subs []i
continue
}
var taskPayload queue.SendEmailPayload
taskPayload.Type = queue.EmailTypeTrafficExceed
taskPayload.Email = method.AuthIdentifier
taskPayload.Subject = "Subscription Traffic Exceed"
tpl, err := template.New("Traffic").Parse(l.svc.Config.Email.TrafficExceedEmailTemplate)
if err != nil {
logger.Errorw("[CheckSubscription] Parse template failed", logger.Field("error", err.Error()))
continue
}
var result bytes.Buffer
err = tpl.Execute(&result, map[string]interface{}{
taskPayload.Content = map[string]interface{}{
"SiteLogo": l.svc.Config.Site.SiteLogo,
"SiteName": l.svc.Config.Site.SiteName,
})
if err != nil {
logger.Errorw("[CheckSubscription] Execute template failed", logger.Field("error", err.Error()))
continue
}
taskPayload.Content = result.String()
payloadBuy, err := json.Marshal(taskPayload)
if err != nil {
logger.Errorw("[CheckSubscription] Marshal payload failed", logger.Field("error", err.Error()))
@@ -213,3 +194,18 @@ func (l *CheckSubscriptionLogic) sendTrafficNotify(ctx context.Context, subs []i
}
return nil
}
func (l *CheckSubscriptionLogic) clearServerCache(ctx context.Context, userSubs ...*user.Subscribe) {
subs := make(map[int64]bool)
for _, sub := range userSubs {
if _, ok := subs[sub.SubscribeId]; !ok {
subs[sub.SubscribeId] = true
}
}
for sub, _ := range subs {
if err := l.svc.SubscribeModel.ClearCache(ctx, sub); err != nil {
logger.Errorw("[CheckSubscription] ClearCache failed", logger.Field("error", err.Error()), logger.Field("subscribe_id", sub))
}
}
}
+407
View File
@@ -0,0 +1,407 @@
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/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:
// 获取订阅对应的套餐信息
subscribeInfo, err := l.svcCtx.SubscribeModel.FindOne(context.Background(), sub.SubscribeId)
if err != nil {
*errors = append(*errors, ErrorInfo{
UserSubscribeId: sub.Id,
Error: "find subscribe error: " + err.Error(),
})
return nil
}
if subscribeInfo.UnitPrice > 0 {
giftAmount = int64(float64(subscribeInfo.UnitPrice) * (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) 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
}
+625
View File
@@ -0,0 +1,625 @@
package traffic
import (
"context"
"encoding/json"
"errors"
"strconv"
"strings"
"time"
"github.com/perfect-panel/server/internal/model/log"
"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/logger"
"github.com/perfect-panel/server/queue/types"
"github.com/hibiken/asynq"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
// ResetTrafficLogic handles traffic reset logic for different subscription cycles
// Supports three reset modes:
// - reset_cycle = 1: Reset on 1st of every month
// - reset_cycle = 2: Reset monthly based on subscription start date
// - reset_cycle = 3: Reset yearly based on subscription start date
type ResetTrafficLogic struct {
svc *svc.ServiceContext
}
// Cache and retry configuration constants
const (
maxRetryAttempts = 3
retryDelay = 30 * time.Minute
lockTimeout = 5 * time.Minute
)
// Cache keys
var (
cacheKey = "reset_traffic_cache"
retryCountKey = "reset_traffic_retry_count"
lockKey = "reset_traffic_lock"
)
// resetTrafficCache stores the last reset time to prevent duplicate processing
type resetTrafficCache struct {
LastResetTime time.Time
}
func NewResetTrafficLogic(svc *svc.ServiceContext) *ResetTrafficLogic {
return &ResetTrafficLogic{
svc: svc,
}
}
// ProcessTask executes the traffic reset task for all subscription types with enhanced retry mechanism
func (l *ResetTrafficLogic) ProcessTask(ctx context.Context, _ *asynq.Task) error {
var err error
startTime := time.Now()
// Get current retry count
retryCount := l.getRetryCount(ctx)
logger.Infow("[ResetTraffic] Starting task execution",
logger.Field("retryCount", retryCount),
logger.Field("startTime", startTime))
// Acquire distributed lock to prevent duplicate execution
lockAcquired := l.acquireLock(ctx)
if !lockAcquired {
logger.Infow("[ResetTraffic] Another task is already running, skipping execution")
return nil
}
defer l.releaseLock(ctx)
defer func() {
if err != nil {
// Check if error is retryable and within retry limit
if l.isRetryableError(err) && retryCount < maxRetryAttempts {
// Increment retry count
l.setRetryCount(ctx, retryCount+1)
// Schedule retry with delay
task := asynq.NewTask(types.SchedulerResetTraffic, nil)
_, retryErr := l.svc.Queue.Enqueue(task, asynq.ProcessIn(retryDelay))
if retryErr != nil {
logger.Errorw("[ResetTraffic] Failed to enqueue retry task",
logger.Field("error", retryErr.Error()),
logger.Field("retryCount", retryCount))
} else {
logger.Infow("[ResetTraffic] Task failed, retrying in 30 minutes",
logger.Field("error", err.Error()),
logger.Field("retryCount", retryCount+1),
logger.Field("maxRetryAttempts", maxRetryAttempts))
}
} else {
// Max retries reached or non-retryable error
if retryCount >= maxRetryAttempts {
logger.Errorw("[ResetTraffic] Max retry attempts reached, giving up",
logger.Field("retryCount", retryCount),
logger.Field("maxRetryAttempts", maxRetryAttempts),
logger.Field("error", err.Error()))
} else {
logger.Errorw("[ResetTraffic] Non-retryable error, not retrying",
logger.Field("error", err.Error()),
logger.Field("retryCount", retryCount))
}
// Reset retry count for next scheduled task
l.clearRetryCount(ctx)
}
} else {
// Task completed successfully, reset retry count
l.clearRetryCount(ctx)
logger.Infow("[ResetTraffic] Task completed successfully",
logger.Field("processingTime", time.Since(startTime)),
logger.Field("retryCount", retryCount))
}
}()
// Load last reset time from cache
var cache resetTrafficCache
cacheData, err := l.svc.Redis.Get(ctx, cacheKey).Result()
if err != nil {
if !errors.Is(err, redis.Nil) {
logger.Errorw("[ResetTraffic] Failed to get cache", logger.Field("error", err.Error()))
}
// Set default value if cache not found
cache = resetTrafficCache{
LastResetTime: time.Now().Add(-10 * time.Minute),
}
logger.Infow("[ResetTraffic] Using default cache value", logger.Field("lastResetTime", cache.LastResetTime))
} else {
// Parse JSON data
if err := json.Unmarshal([]byte(cacheData), &cache); err != nil {
logger.Errorw("[ResetTraffic] Failed to unmarshal cache", logger.Field("error", err.Error()))
cache = resetTrafficCache{
LastResetTime: time.Now().Add(-10 * time.Minute),
}
} else {
logger.Infow("[ResetTraffic] Cache loaded successfully", logger.Field("lastResetTime", cache.LastResetTime))
}
}
// Execute reset operations in order: yearly -> monthly (1st) -> monthly (cycle)
err = l.resetYear(ctx)
if err != nil {
logger.Errorw("[ResetTraffic] Yearly reset failed", logger.Field("error", err.Error()))
return err
}
err = l.reset1st(ctx, cache)
if err != nil {
logger.Errorw("[ResetTraffic] Monthly 1st reset failed", logger.Field("error", err.Error()))
return err
}
err = l.resetMonth(ctx)
if err != nil {
logger.Errorw("[ResetTraffic] Monthly cycle reset failed", logger.Field("error", err.Error()))
return err
}
// Update cache with current time after successful processing
updatedCache := resetTrafficCache{
LastResetTime: startTime,
}
cacheDataBytes, marshalErr := json.Marshal(updatedCache)
if marshalErr != nil {
logger.Errorw("[ResetTraffic] Failed to marshal cache", logger.Field("error", marshalErr.Error()))
} else {
cacheErr := l.svc.Redis.Set(ctx, cacheKey, cacheDataBytes, 0).Err()
if cacheErr != nil {
logger.Errorw("[ResetTraffic] Failed to update cache", logger.Field("error", cacheErr.Error()))
// Don't return error here as the main task completed successfully
} else {
logger.Infow("[ResetTraffic] Cache updated successfully", logger.Field("newLastResetTime", startTime))
}
}
return nil
}
// resetMonth handles monthly cycle reset based on subscription start date
// reset_cycle = 2: Reset monthly based on subscription start date
func (l *ResetTrafficLogic) resetMonth(ctx context.Context) error {
now := time.Now()
err := l.svc.UserModel.Transaction(ctx, func(db *gorm.DB) error {
// Get all subscriptions that reset monthly based on start date
var resetMonthSubIds []int64
err := db.Model(&subscribe.Subscribe{}).Select("`id`").Where("`reset_cycle` = ?", 2).Find(&resetMonthSubIds).Error
if err != nil {
logger.Errorw("[ResetTraffic] Failed to query monthly subscriptions", logger.Field("error", err.Error()))
return err
}
if len(resetMonthSubIds) == 0 {
logger.Infow("[ResetTraffic] No monthly cycle subscriptions found")
return nil
}
// Query users for monthly reset based on subscription start date cycle
var monthlyResetUsers []int64
// Check if today is the last day of current month
isLastDayOfMonth := now.AddDate(0, 0, 1).Month() != now.Month()
query := db.Model(&user.Subscribe{}).Select("`id`").
Where("`subscribe_id` IN ?", resetMonthSubIds).
Where("`status` IN ?", []int64{1, 2}). // Only active subscriptions
Where("TIMESTAMPDIFF(MONTH, CURDATE(),DATE(expire_time)) >= 1") // At least 1 month passed
if isLastDayOfMonth {
// Last day of month: handle subscription start dates >= today
query = query.Where("DAY(`expire_time`) >= ?", now.Day())
} else {
// Normal case: exact day match
query = query.Where("DAY(`expire_time`) = ?", now.Day())
}
err = query.Find(&monthlyResetUsers).Error
if err != nil {
logger.Errorw("[ResetTraffic] Failed to query monthly reset users", logger.Field("error", err.Error()))
return err
}
if len(monthlyResetUsers) > 0 {
logger.Infow("[ResetTraffic] Found users for monthly reset",
logger.Field("count", len(monthlyResetUsers)),
logger.Field("userIds", monthlyResetUsers))
err = db.Model(&user.Subscribe{}).Where("`id` IN ?", monthlyResetUsers).
Updates(map[string]interface{}{
"upload": 0,
"download": 0,
"status": 1, // Ensure status is active
"finished_at": nil,
}).Error
if err != nil {
logger.Errorw("[ResetTraffic] Failed to update monthly reset users", logger.Field("error", err.Error()))
return err
}
// Find user subscriptions for these users
var userSubs []*user.Subscribe
err = db.Model(&user.Subscribe{}).Where("`id` IN ?", monthlyResetUsers).Find(&userSubs).Error
if err != nil {
logger.Errorw("[ResetTraffic] Failed to find user subscriptions for 1st reset", logger.Field("error", err.Error()))
return err
}
// Clear cache for these subscriptions
l.clearCache(ctx, userSubs)
logger.Infow("[ResetTraffic] Monthly reset completed", logger.Field("count", len(monthlyResetUsers)))
} else {
logger.Infow("[ResetTraffic] No users found for monthly reset")
}
return l.svc.SubscribeModel.ClearCache(ctx, resetMonthSubIds...)
})
if err != nil {
logger.Errorw("[ResetTraffic] Monthly reset transaction failed", logger.Field("error", err.Error()))
return err
}
logger.Infow("[ResetTraffic] Monthly reset process completed")
return nil
}
// reset1st handles reset on 1st of every month
// reset_cycle = 1: Reset on 1st of every month
func (l *ResetTrafficLogic) reset1st(ctx context.Context, cache resetTrafficCache) error {
now := time.Now()
// Check if we already reset this month using cache
if cache.LastResetTime.Year() == now.Year() && cache.LastResetTime.Month() == now.Month() {
logger.Infow("[ResetTraffic] Already reset this month, skipping 1st reset",
logger.Field("lastResetTime", cache.LastResetTime),
logger.Field("currentTime", now))
return nil
}
// Only reset if it's the 1st day of the month
if now.Day() != 1 {
logger.Infow("[ResetTraffic] Not 1st day of month, skipping 1st reset", logger.Field("currentDay", now.Day()))
return nil
}
err := l.svc.UserModel.Transaction(ctx, func(db *gorm.DB) error {
// Get all subscriptions that reset on 1st of month
var reset1stSubIds []int64
err := db.Model(&subscribe.Subscribe{}).Select("`id`").Where("`reset_cycle` = ?", 1).Find(&reset1stSubIds).Error
if err != nil {
logger.Errorw("[ResetTraffic] Failed to query 1st reset subscriptions", logger.Field("error", err.Error()))
return err
}
if len(reset1stSubIds) == 0 {
logger.Infow("[ResetTraffic] No 1st reset subscriptions found")
return nil
}
// Get all active users with these subscriptions
var users1stReset []int64
err = db.Model(&user.Subscribe{}).Select("`id`").
Where("`subscribe_id` IN ?", reset1stSubIds).
Where("`status` IN ?", []int64{1, 2}). // Only active subscriptions
Find(&users1stReset).Error
if err != nil {
logger.Errorw("[ResetTraffic] Failed to query 1st reset users", logger.Field("error", err.Error()))
return err
}
if len(users1stReset) > 0 {
logger.Infow("[ResetTraffic] Found users for 1st reset",
logger.Field("count", len(users1stReset)),
logger.Field("userIds", users1stReset))
// Reset upload and download traffic to zero
err = db.Model(&user.Subscribe{}).Where("`id` IN ?", users1stReset).
Updates(map[string]interface{}{
"upload": 0,
"download": 0,
"status": 1, // Ensure status is active
"finished_at": nil,
}).Error
if err != nil {
logger.Errorw("[ResetTraffic] Failed to update 1st reset users", logger.Field("error", err.Error()))
return err
}
var userSubs []*user.Subscribe
err = db.Model(&user.Subscribe{}).Where("`id` IN ?", users1stReset).Find(&userSubs).Error
if err != nil {
logger.Errorw("[ResetTraffic] Failed to find user subscriptions for 1st reset", logger.Field("error", err.Error()))
return err
}
// Clear cache for these subscriptions
l.clearCache(ctx, userSubs)
logger.Infow("[ResetTraffic] 1st reset completed", logger.Field("count", len(users1stReset)))
} else {
logger.Infow("[ResetTraffic] No users found for 1st reset")
}
return l.svc.SubscribeModel.ClearCache(ctx, reset1stSubIds...)
})
if err != nil {
logger.Errorw("[ResetTraffic] 1st reset transaction failed", logger.Field("error", err.Error()))
return err
}
logger.Infow("[ResetTraffic] 1st reset process completed")
return nil
}
// resetYear handles yearly reset based on subscription start date anniversary
// reset_cycle = 3: Reset yearly based on subscription start date
func (l *ResetTrafficLogic) resetYear(ctx context.Context) error {
now := time.Now()
err := l.svc.UserModel.Transaction(ctx, func(db *gorm.DB) error {
// Get all subscriptions that reset yearly
var resetYearSubIds []int64
err := db.Model(&subscribe.Subscribe{}).Select("`id`").Where("`reset_cycle` = ?", 3).Find(&resetYearSubIds).Error
if err != nil {
logger.Errorw("[ResetTraffic] Failed to query yearly subscriptions", logger.Field("error", err.Error()))
return err
}
if len(resetYearSubIds) == 0 {
logger.Infow("[ResetTraffic] No yearly reset subscriptions found")
return nil
}
// Query users for yearly reset based on subscription start date anniversary
var usersYearReset []int64
// Check if today is February 28th (handle leap year case)
isLeapYearCase := now.Month() == 2 && now.Day() == 28
query := db.Model(&user.Subscribe{}).Select("`id`").
Where("`subscribe_id` IN ?", resetYearSubIds).
Where("MONTH(expire_time) = ?", now.Month()). // Same month
Where("`status` IN ?", []int64{1, 2}). // Only active subscriptions
Where("TIMESTAMPDIFF(YEAR, CURDATE(),DATE(expire_time)) >= 1") // At least 1 year passed
if isLeapYearCase {
// February 28th: handle both Feb 28 and Feb 29 subscriptions
query = query.Where("DAY(expire_time) IN (28, 29)")
} else {
// Normal case: exact day match
query = query.Where("DAY(expire_time) = ?", now.Day())
}
err = query.Find(&usersYearReset).Error
if err != nil {
logger.Errorw("[ResetTraffic] Query yearly reset users failed", logger.Field("error", err.Error()))
return err
}
if len(usersYearReset) > 0 {
logger.Infow("[ResetTraffic] Found users for yearly reset",
logger.Field("count", len(usersYearReset)),
logger.Field("userIds", usersYearReset))
// Reset upload and download traffic to zero
err = db.Model(&user.Subscribe{}).Where("`id` IN ?", usersYearReset).
Updates(map[string]interface{}{
"upload": 0,
"download": 0,
"status": 1, // Ensure status is active
"finished_at": nil,
}).Error
if err != nil {
logger.Errorw("[ResetTraffic] Failed to update yearly reset users", logger.Field("error", err.Error()))
return err
}
// Find user subscriptions for these users
var userSubs []*user.Subscribe
err = db.Model(&user.Subscribe{}).Where("`id` IN ?", usersYearReset).Find(&userSubs).Error
if err != nil {
logger.Errorw("[ResetTraffic] Failed to find user subscriptions for 1st reset", logger.Field("error", err.Error()))
return err
}
// Clear cache for these subscriptions
l.clearCache(ctx, userSubs)
logger.Infow("[ResetTraffic] Yearly reset completed", logger.Field("count", len(usersYearReset)))
} else {
logger.Infow("[ResetTraffic] No users found for yearly reset")
}
err = l.svc.SubscribeModel.ClearCache(ctx, resetYearSubIds...)
if err != nil {
logger.Errorw("[ResetTraffic] Failed to clear yearly reset subscription cache", logger.Field("error", err.Error()))
}
return nil
})
if err != nil {
logger.Errorw("[ResetTraffic] Yearly reset transaction failed", logger.Field("error", err.Error()))
return err
}
logger.Infow("[ResetTraffic] Yearly reset process completed")
return nil
}
// getRetryCount retrieves the current retry count from Redis
func (l *ResetTrafficLogic) getRetryCount(ctx context.Context) int {
countStr, err := l.svc.Redis.Get(ctx, retryCountKey).Result()
if err != nil {
if errors.Is(err, redis.Nil) {
return 0 // No retry count found, start with 0
}
logger.Errorw("[ResetTraffic] Failed to get retry count", logger.Field("error", err.Error()))
return 0
}
count, err := strconv.Atoi(countStr)
if err != nil {
logger.Errorw("[ResetTraffic] Invalid retry count format", logger.Field("value", countStr))
return 0
}
return count
}
// setRetryCount sets the retry count in Redis
func (l *ResetTrafficLogic) setRetryCount(ctx context.Context, count int) {
err := l.svc.Redis.Set(ctx, retryCountKey, count, 24*time.Hour).Err()
if err != nil {
logger.Errorw("[ResetTraffic] Failed to set retry count",
logger.Field("count", count),
logger.Field("error", err.Error()))
}
}
// clearRetryCount removes the retry count from Redis
func (l *ResetTrafficLogic) clearRetryCount(ctx context.Context) {
err := l.svc.Redis.Del(ctx, retryCountKey).Err()
if err != nil {
logger.Errorw("[ResetTraffic] Failed to clear retry count", logger.Field("error", err.Error()))
}
}
// acquireLock attempts to acquire a distributed lock
func (l *ResetTrafficLogic) acquireLock(ctx context.Context) bool {
result := l.svc.Redis.SetNX(ctx, lockKey, "locked", lockTimeout)
acquired, err := result.Result()
if err != nil {
logger.Errorw("[ResetTraffic] Failed to acquire lock", logger.Field("error", err.Error()))
return false
}
if acquired {
logger.Infow("[ResetTraffic] Lock acquired successfully")
} else {
logger.Infow("[ResetTraffic] Lock already exists, another task is running")
}
return acquired
}
// releaseLock releases the distributed lock
func (l *ResetTrafficLogic) releaseLock(ctx context.Context) {
err := l.svc.Redis.Del(ctx, lockKey).Err()
if err != nil {
logger.Errorw("[ResetTraffic] Failed to release lock", logger.Field("error", err.Error()))
} else {
logger.Infow("[ResetTraffic] Lock released successfully")
}
}
// isRetryableError determines if an error is retryable
func (l *ResetTrafficLogic) isRetryableError(err error) bool {
if err == nil {
return false
}
errorMessage := strings.ToLower(err.Error())
// Network and connection errors (retryable)
retryableErrors := []string{
"connection refused",
"connection reset",
"connection timeout",
"network",
"timeout",
"dial",
"context deadline exceeded",
"temporary failure",
"server error",
"service unavailable",
"internal server error",
"database is locked",
"too many connections",
"deadlock",
"lock wait timeout",
}
// Database constraint errors (non-retryable)
nonRetryableErrors := []string{
"foreign key constraint",
"unique constraint",
"check constraint",
"not null constraint",
"invalid input syntax",
"column does not exist",
"table does not exist",
"permission denied",
"access denied",
"authentication failed",
"invalid credentials",
}
// Check for non-retryable errors first
for _, nonRetryable := range nonRetryableErrors {
if strings.Contains(errorMessage, nonRetryable) {
logger.Infow("[ResetTraffic] Non-retryable error detected",
logger.Field("error", err.Error()),
logger.Field("pattern", nonRetryable))
return false
}
}
// Check for retryable errors
for _, retryable := range retryableErrors {
if strings.Contains(errorMessage, retryable) {
logger.Infow("[ResetTraffic] Retryable error detected",
logger.Field("error", err.Error()),
logger.Field("pattern", retryable))
return true
}
}
// Default: treat unknown errors as retryable, but log for analysis
logger.Infow("[ResetTraffic] Unknown error type, treating as retryable",
logger.Field("error", err.Error()))
return true
}
// clearCache clears the reset traffic cache
func (l *ResetTrafficLogic) clearCache(ctx context.Context, list []*user.Subscribe) {
if len(list) != 0 {
subs := make(map[int64]bool)
for _, sub := range list {
if sub.SubscribeId > 0 {
err := l.svc.UserModel.ClearSubscribeCache(ctx, sub)
if err != nil {
logger.Errorw("[ResetTraffic] Failed to clear cache for subscription",
logger.Field("subscribeId", sub.SubscribeId),
logger.Field("error", err.Error()))
}
if _, ok := subs[sub.SubscribeId]; !ok {
subs[sub.SubscribeId] = true
}
}
// Insert traffic reset log
l.insertLog(ctx, sub.Id, sub.UserId)
}
for sub, _ := range subs {
if err := l.svc.SubscribeModel.ClearCache(ctx, sub); err != nil {
logger.Errorw("[ResetTraffic] Failed to clear subscription cache",
logger.Field("subscribeId", sub),
logger.Field("error", err.Error()),
)
}
}
}
}
// insertLog inserts a reset traffic log entry
func (l *ResetTrafficLogic) insertLog(ctx context.Context, subId, userId int64) {
trafficLog := log.ResetSubscribe{
Type: log.ResetSubscribeTypeAuto,
UserId: userId,
Timestamp: time.Now().UnixMilli(),
}
content, _ := trafficLog.Marshal()
if err := l.svc.DB.WithContext(ctx).Model(&log.SystemLog{}).Create(&log.SystemLog{
Type: log.TypeResetSubscribe.Uint8(),
ObjectID: subId,
Date: time.Now().Format(time.DateOnly),
Content: string(content),
}).Error; err != nil {
logger.Errorw("[ResetTraffic] Failed to create system log for subscription", logger.Field("error", err.Error()))
}
}
+6 -6
View File
@@ -5,12 +5,12 @@ import (
"encoding/json"
"time"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"github.com/perfect-panel/server/pkg/logger"
"github.com/hibiken/asynq"
"github.com/perfect-panel/ppanel-server/internal/config"
"github.com/perfect-panel/ppanel-server/internal/svc"
"github.com/perfect-panel/ppanel-server/internal/types"
"github.com/perfect-panel/server/internal/config"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
)
type ServerDataLogic struct {
@@ -73,7 +73,7 @@ func (l *ServerDataLogic) getRanking(ctx context.Context) (top10ServerToday, top
if s.ServerId == 0 {
continue
}
serverInfo, err := l.svc.ServerModel.FindOne(ctx, s.ServerId)
serverInfo, err := l.svc.NodeModel.FindOneServer(ctx, s.ServerId)
if err != nil {
logger.Error("[ServerDataLogic] Find server failed", logger.Field("error", err.Error()))
continue
@@ -92,7 +92,7 @@ func (l *ServerDataLogic) getRanking(ctx context.Context) (top10ServerToday, top
logger.Error("[ServerDataLogic] Get top servers traffic by day failed", logger.Field("error", err.Error()))
} else {
for _, s := range serverYesterday {
serverInfo, err := l.svc.ServerModel.FindOne(ctx, s.ServerId)
serverInfo, err := l.svc.NodeModel.FindOneServer(ctx, s.ServerId)
if err != nil {
logger.Error("[ServerDataLogic] Find server failed", logger.Field("error", err.Error()))
continue
+176
View File
@@ -0,0 +1,176 @@
package traffic
import (
"context"
"time"
"github.com/hibiken/asynq"
"github.com/perfect-panel/server/internal/model/log"
"github.com/perfect-panel/server/internal/model/traffic"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/pkg/logger"
)
type StatLogic struct {
svc *svc.ServiceContext
}
func NewStatLogic(svc *svc.ServiceContext) *StatLogic {
return &StatLogic{
svc: svc,
}
}
func (l *StatLogic) ProcessTask(ctx context.Context, _ *asynq.Task) error {
now := time.Now()
tx := l.svc.DB.Begin()
var err error
defer func(err error) {
if err != nil {
logger.Errorf("[Traffic Stat Queue] Process task failed: %v", err.Error())
tx.Rollback()
} else {
logger.Infof("[Traffic Stat Queue] Process task completed successfully, consuming: %s", time.Since(now).String())
// 提交事务
if err = tx.Commit().Error; err != nil {
logger.Errorf("[Traffic Stat Queue] Commit transaction failed: %v", err.Error())
}
}
}(err)
// 获取全部有效订阅
var userTraffic []log.UserTraffic
// 获取统计时间范围
start := time.Date(now.Year(), now.Month(), now.Day()-1, 0, 0, 0, 0, time.Local)
end := start.Add(24 * time.Hour).Add(-time.Nanosecond)
// 查询用户流量统计, 按用户和订阅分组
err = tx.WithContext(ctx).Model(&traffic.TrafficLog{}).
Select("user_id, subscribe_id, SUM(download + upload) AS total, SUM(download) AS download, SUM(upload) AS upload").
Where("timestamp BETWEEN ? AND ?", start, end).
Group("user_id, subscribe_id").
Order("total DESC").
Scan(&userTraffic).Error
if err != nil {
logger.Errorf("[Traffic Stat Queue] Query user traffic failed: %v", err.Error())
return err
}
date := start.Format(time.DateOnly)
userTop10 := log.UserTrafficRank{
Rank: make(map[uint8]log.UserTraffic),
}
// 更新用户流量统计
for i, trafficData := range userTraffic {
if i < 10 {
userTop10.Rank[uint8(i+1)] = trafficData
}
// 更新用户流量统计日志
content, _ := trafficData.Marshal()
err = tx.WithContext(ctx).Model(&log.SystemLog{}).Create(&log.SystemLog{
Type: log.TypeSubscribeTraffic.Uint8(),
Date: date,
ObjectID: trafficData.SubscribeId,
Content: string(content),
}).Error
if err != nil {
logger.Errorf("[Traffic Stat Queue] Create user traffic log failed: %v", err.Error())
return err
}
}
userTop10Content, _ := userTop10.Marshal()
// 更新用户排行榜
err = tx.WithContext(ctx).Model(&log.SystemLog{}).Create(&log.SystemLog{
Type: log.TypeUserTrafficRank.Uint8(),
Date: date,
ObjectID: 0, // 0表示全局用户排行榜
Content: string(userTop10Content),
}).Error
if err != nil {
logger.Errorf("[Traffic Stat Queue] Create user traffic rank log failed: %v", err.Error())
return err
}
// 统计服务器流量
var serverTraffic []log.ServerTraffic
err = tx.WithContext(ctx).Model(&traffic.TrafficLog{}).
Select("server_id, SUM(download + upload) AS total, SUM(download) AS download, SUM(upload) AS upload").
Where("timestamp BETWEEN ? AND ?", start, end).
Group("server_id").
Order("total DESC").
Scan(&serverTraffic).Error
if err != nil {
logger.Errorf("[Traffic Stat Queue] Query server traffic failed: %v", err.Error())
return err
}
serverTop10 := log.ServerTrafficRank{
Rank: make(map[uint8]log.ServerTraffic),
}
for i, trafficData := range serverTraffic {
if i < 10 {
serverTop10.Rank[uint8(i+1)] = trafficData
}
// 更新服务器流量统计日志
content, _ := trafficData.Marshal()
err = tx.WithContext(ctx).Model(&log.SystemLog{}).Create(&log.SystemLog{
Type: log.TypeServerTraffic.Uint8(),
Date: date,
ObjectID: trafficData.ServerId,
Content: string(content),
}).Error
if err != nil {
logger.Errorf("[Traffic Stat Queue] Create server traffic log failed: %v", err.Error())
return err
}
}
serverTop10Content, _ := serverTop10.Marshal()
// 更新服务器排行榜
err = tx.WithContext(ctx).Model(&log.SystemLog{}).Create(&log.SystemLog{
Type: log.TypeServerTrafficRank.Uint8(),
Date: date,
ObjectID: 0, // 0表示全局服务器排行榜
Content: string(serverTop10Content),
}).Error
if err != nil {
logger.Errorf("[Traffic Stat Queue] Create server traffic rank log failed: %v", err.Error())
return err
}
// traffic stat
var stat log.TrafficStat
err = tx.WithContext(ctx).Model(&traffic.TrafficLog{}).
Select("SUM(download + upload) AS total, SUM(download) AS download, SUM(upload) AS upload").
Where("timestamp BETWEEN ? AND ?", start, end).
Scan(&stat).Error
if err != nil {
logger.Errorf("[Traffic Stat Queue] Query traffic stat failed: %v", err.Error())
return err
}
// 更新流量统计日志
content, _ := stat.Marshal()
err = tx.WithContext(ctx).Model(&log.SystemLog{}).Create(&log.SystemLog{
Type: log.TypeTrafficStat.Uint8(),
Date: date,
ObjectID: 0,
Content: string(content),
}).Error
if err != nil {
logger.Errorf("[Traffic Stat Queue] Create traffic stat log failed: %v", err.Error())
return err
}
// Delete old traffic logs
if l.svc.Config.Log.AutoClear {
err = tx.WithContext(ctx).Model(&traffic.TrafficLog{}).Where("created_at <= ?", end.AddDate(0, 0, int(-l.svc.Config.Log.ClearDays))).Delete(&traffic.TrafficLog{}).Error
if err != nil {
logger.Errorf("[Traffic Stat Queue] Delete server traffic log failed: %v", err.Error())
}
}
return nil
}
+54 -24
View File
@@ -3,14 +3,16 @@ package traffic
import (
"context"
"encoding/json"
"strings"
"time"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"github.com/perfect-panel/server/internal/model/node"
"github.com/perfect-panel/server/pkg/logger"
"github.com/hibiken/asynq"
"github.com/perfect-panel/ppanel-server/internal/model/traffic"
"github.com/perfect-panel/ppanel-server/internal/svc"
"github.com/perfect-panel/ppanel-server/queue/types"
"github.com/perfect-panel/server/internal/model/traffic"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/queue/types"
)
//goland:noinspection GoNameStartsWithPackageName
@@ -38,7 +40,7 @@ func (l *TrafficStatisticsLogic) ProcessTask(ctx context.Context, task *asynq.Ta
return nil
}
// query server info
serverInfo, err := l.svc.ServerModel.FindOne(ctx, payload.ServerId)
serverInfo, err := l.svc.NodeModel.FindOneServer(ctx, payload.ServerId)
if err != nil {
logger.WithContext(ctx).Error("[TrafficStatistics] Find server info failed",
logger.Field("serverId", payload.ServerId),
@@ -46,27 +48,38 @@ func (l *TrafficStatisticsLogic) ProcessTask(ctx context.Context, task *asynq.Ta
)
return nil
}
if serverInfo.TrafficRatio == 0 {
logger.WithContext(ctx).Error("[TrafficStatistics] Server log ratio is 0",
logger.Field("serverId", payload.ServerId),
)
// query protocol ratio
// default ratio is 1.0
protocols, err := serverInfo.UnmarshalProtocols()
if err != nil {
logger.Errorf("[TrafficStatistics] Unmarshal protocols failed: %s", err.Error())
return nil
}
var protocol *node.Protocol
var ratio float32 = 1.0
for _, p := range protocols {
if strings.ToLower(p.Type) == strings.ToLower(payload.Protocol) {
protocol = &p
break
}
}
if protocol == nil {
logger.WithContext(ctx).Error("[TrafficStatistics] Protocol not found: %s", payload.Protocol)
return nil
}
// use protocol ratio if it's greater than 0
if protocol.Ratio > 0 {
ratio = float32(protocol.Ratio)
}
now := time.Now()
realTimeMultiplier := l.svc.NodeMultiplierManager.GetMultiplier(now)
for _, log := range payload.Logs {
// update user subscribe with log
d := int64(float32(log.Download) * serverInfo.TrafficRatio * realTimeMultiplier)
u := int64(float32(log.Upload) * serverInfo.TrafficRatio * realTimeMultiplier)
if err := l.svc.UserModel.UpdateUserSubscribeWithTraffic(ctx, log.SID, d, u); err != nil {
logger.WithContext(ctx).Error("[TrafficStatistics] Update user subscribe with log failed",
logger.Field("sid", log.SID),
logger.Field("download", float32(log.Download)*serverInfo.TrafficRatio),
logger.Field("upload", float32(log.Upload)*serverInfo.TrafficRatio),
logger.Field("error", err.Error()),
)
continue
}
// query user Subscribe Info
sub, err := l.svc.UserModel.FindOneSubscribe(ctx, log.SID)
if err != nil {
@@ -77,8 +90,25 @@ func (l *TrafficStatisticsLogic) ProcessTask(ctx context.Context, task *asynq.Ta
continue
}
if log.Download+log.Upload <= l.svc.Config.Node.TrafficReportThreshold {
// no traffic, skip
continue
}
// update user subscribe with log
d := int64(float32(log.Download) * ratio * realTimeMultiplier)
u := int64(float32(log.Upload) * ratio * realTimeMultiplier)
if err := l.svc.UserModel.UpdateUserSubscribeWithTraffic(ctx, sub.Id, d, u); err != nil {
logger.WithContext(ctx).Error("[TrafficStatistics] Update user subscribe with log failed",
logger.Field("sid", log.SID),
logger.Field("download", float32(log.Download)*ratio),
logger.Field("upload", float32(log.Upload)*ratio),
logger.Field("error", err.Error()),
)
continue
}
// create log log
if err := l.svc.TrafficLogModel.Insert(ctx, &traffic.TrafficLog{
if err = l.svc.TrafficLogModel.Insert(ctx, &traffic.TrafficLog{
ServerId: payload.ServerId,
SubscribeId: log.SID,
UserId: sub.UserId,
@@ -88,8 +118,8 @@ func (l *TrafficStatisticsLogic) ProcessTask(ctx context.Context, task *asynq.Ta
}); err != nil {
logger.WithContext(ctx).Error("[TrafficStatistics] Create log log failed",
logger.Field("uid", log.SID),
logger.Field("download", float32(log.Download)*serverInfo.TrafficRatio),
logger.Field("upload", float32(log.Upload)*serverInfo.TrafficRatio),
logger.Field("download", float32(log.Download)*ratio),
logger.Field("upload", float32(log.Upload)*ratio),
logger.Field("error", err.Error()),
)
}
+3 -3
View File
@@ -2,9 +2,9 @@ package queue
import (
"github.com/hibiken/asynq"
"github.com/perfect-panel/ppanel-server/internal/svc"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"github.com/perfect-panel/ppanel-server/queue/handler"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/queue/handler"
)
type Service struct {
+12 -3
View File
@@ -5,10 +5,19 @@ const (
ForthwithSendEmail = "forthwith:email:send"
)
const (
EmailTypeVerify = "verify"
EmailTypeMaintenance = "maintenance"
EmailTypeExpiration = "expiration"
EmailTypeTrafficExceed = "traffic_exceed"
EmailTypeCustom = "custom"
)
type (
SendEmailPayload struct {
Email string `json:"to"`
Subject string `json:"subject"`
Content string `json:"content"`
Type string `json:"type"`
Email string `json:"to"`
Subject string `json:"subject"`
Content map[string]interface{} `json:"content"`
}
)
-3
View File
@@ -6,9 +6,6 @@ const (
)
type (
DeferCheckOrderLogic struct {
OrderNo string `json:"order_no"`
}
DeferCloseOrderPayload struct {
OrderNo string `json:"order_no"`
}
+2 -1
View File
@@ -3,5 +3,6 @@ package types
const (
SchedulerCheckSubscription = "scheduler:check:subscription"
SchedulerTotalServerData = "scheduler:total:server"
SchedulerCheckOrder = "scheduler:check:order"
SchedulerResetTraffic = "scheduler:reset:traffic"
SchedulerTrafficStat = "scheduler:traffic:stat"
)
+1
View File
@@ -10,6 +10,7 @@ type UserTraffic struct {
type TrafficStatistics struct {
ServerId int64 `json:"server_id"`
Protocol string `json:"protocol"`
Logs []UserTraffic `json:"logs"`
}
+1 -1
View File
@@ -1,7 +1,7 @@
package types
const (
// ForthwithSendEmail forthwith send email
// ForthwithSendSms forthwith send email
ForthwithSendSms = "forthwith:sms:send"
)
+9
View File
@@ -0,0 +1,9 @@
package types
const (
// ScheduledBatchSendEmail scheduled batch send email
ScheduledBatchSendEmail = "scheduled:email:batch"
// ForthwithQuotaTask create quota task immediately
ForthwithQuotaTask = "forthwith:quota:task"
)