refactor: 更新项目引用路径从perfect-panel/ppanel-server到perfect-panel/server
Build docker and publish / build (20.15.1) (push) Failing after 6m27s
Build docker and publish / build (20.15.1) (push) Failing after 6m27s
feat: 添加版本和构建时间变量 fix: 修正短信队列类型注释错误 style: 清理未使用的代码和测试文件 docs: 更新安装文档中的下载链接 chore: 迁移数据库脚本添加日志和订阅配置
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user