同步历史版本代码
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
@@ -48,10 +49,33 @@ func (l *SendEmailLogic) ProcessTask(ctx context.Context, task *asynq.Task) erro
|
||||
var content string
|
||||
switch payload.Type {
|
||||
case types.EmailTypeVerify:
|
||||
tpl, _ := template.New("verify").Parse(l.svcCtx.Config.Email.VerifyEmailTemplate)
|
||||
var result bytes.Buffer
|
||||
tplStr := l.svcCtx.Config.Email.VerifyEmailTemplate
|
||||
|
||||
payload.Content["Type"] = uint8(payload.Content["Type"].(float64))
|
||||
// Use int for better template compatibility
|
||||
if t, ok := payload.Content["Type"].(float64); ok {
|
||||
payload.Content["Type"] = int(t)
|
||||
} else if t, ok := payload.Content["Type"].(int); ok {
|
||||
payload.Content["Type"] = t
|
||||
}
|
||||
|
||||
typeVal, _ := payload.Content["Type"].(int)
|
||||
|
||||
// Smart Fallback: If template is empty OR (Type is 4 but template doesn't support it), use default
|
||||
// We check for "Type 4" or "Type eq 4" string in the template as a heuristic
|
||||
needDefault := tplStr == ""
|
||||
if !needDefault && typeVal == 4 &&
|
||||
!strings.Contains(tplStr, "Type 4") &&
|
||||
!strings.Contains(tplStr, "Type eq 4") {
|
||||
logger.WithContext(ctx).Infow("[SendEmailLogic] Configured template might not support DeleteAccount (Type 4), forcing default template")
|
||||
needDefault = true
|
||||
}
|
||||
|
||||
if needDefault {
|
||||
tplStr = email.DefaultEmailVerifyTemplate
|
||||
}
|
||||
|
||||
tpl, _ := template.New("verify").Parse(tplStr)
|
||||
var result bytes.Buffer
|
||||
|
||||
err = tpl.Execute(&result, payload.Content)
|
||||
if err != nil {
|
||||
@@ -63,7 +87,11 @@ func (l *SendEmailLogic) ProcessTask(ctx context.Context, task *asynq.Task) erro
|
||||
}
|
||||
content = result.String()
|
||||
case types.EmailTypeMaintenance:
|
||||
tpl, _ := template.New("maintenance").Parse(l.svcCtx.Config.Email.MaintenanceEmailTemplate)
|
||||
tplStr := l.svcCtx.Config.Email.MaintenanceEmailTemplate
|
||||
if tplStr == "" {
|
||||
tplStr = email.DefaultMaintenanceEmailTemplate
|
||||
}
|
||||
tpl, _ := template.New("maintenance").Parse(tplStr)
|
||||
var result bytes.Buffer
|
||||
err = tpl.Execute(&result, payload.Content)
|
||||
if err != nil {
|
||||
@@ -76,7 +104,11 @@ func (l *SendEmailLogic) ProcessTask(ctx context.Context, task *asynq.Task) erro
|
||||
}
|
||||
content = result.String()
|
||||
case types.EmailTypeExpiration:
|
||||
tpl, _ := template.New("expiration").Parse(l.svcCtx.Config.Email.ExpirationEmailTemplate)
|
||||
tplStr := l.svcCtx.Config.Email.ExpirationEmailTemplate
|
||||
if tplStr == "" {
|
||||
tplStr = email.DefaultExpirationEmailTemplate
|
||||
}
|
||||
tpl, _ := template.New("expiration").Parse(tplStr)
|
||||
var result bytes.Buffer
|
||||
err = tpl.Execute(&result, payload.Content)
|
||||
if err != nil {
|
||||
@@ -89,7 +121,11 @@ func (l *SendEmailLogic) ProcessTask(ctx context.Context, task *asynq.Task) erro
|
||||
}
|
||||
content = result.String()
|
||||
case types.EmailTypeTrafficExceed:
|
||||
tpl, _ := template.New("traffic_exceed").Parse(l.svcCtx.Config.Email.TrafficExceedEmailTemplate)
|
||||
tplStr := l.svcCtx.Config.Email.TrafficExceedEmailTemplate
|
||||
if tplStr == "" {
|
||||
tplStr = email.DefaultTrafficExceedEmailTemplate
|
||||
}
|
||||
tpl, _ := template.New("traffic_exceed").Parse(tplStr)
|
||||
var result bytes.Buffer
|
||||
err = tpl.Execute(&result, payload.Content)
|
||||
if err != nil {
|
||||
|
||||
@@ -5,6 +5,7 @@ package orderLogic
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -68,14 +69,32 @@ func NewActivateOrderLogic(svc *svc.ServiceContext) *ActivateOrderLogic {
|
||||
// It handles the complete workflow of activating a paid order including validation,
|
||||
// processing based on order type, and finalization.
|
||||
func (l *ActivateOrderLogic) ProcessTask(ctx context.Context, task *asynq.Task) error {
|
||||
logger.WithContext(ctx).Info("[ActivateOrderLogic] 开始处理订单激活任务",
|
||||
logger.Field("payload", string(task.Payload())))
|
||||
|
||||
payload, err := l.parsePayload(ctx, task.Payload())
|
||||
if err != nil {
|
||||
return err // Return error to trigger retry
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] 解析 payload 失败,跳过任务",
|
||||
logger.Field("error", err.Error()))
|
||||
return nil // payload 解析失败不重试,因为重试也会失败
|
||||
}
|
||||
|
||||
logger.WithContext(ctx).Info("[ActivateOrderLogic] 正在验证订单",
|
||||
logger.Field("order_no", payload.OrderNo))
|
||||
|
||||
orderInfo, err := l.validateAndGetOrder(ctx, payload.OrderNo)
|
||||
if err != nil {
|
||||
return err // Return error to trigger retry
|
||||
// 如果订单不存在或状态不对,不重试
|
||||
if errors.Is(err, ErrInvalidOrderStatus) {
|
||||
logger.WithContext(ctx).Info("[ActivateOrderLogic] 订单状态不是已支付,跳过",
|
||||
logger.Field("order_no", payload.OrderNo))
|
||||
return nil
|
||||
}
|
||||
// 数据库查询失败,应该重试
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] 查询订单失败,将重试",
|
||||
logger.Field("order_no", payload.OrderNo),
|
||||
logger.Field("error", err.Error()))
|
||||
return err
|
||||
}
|
||||
|
||||
// Idempotency: if order is already finished, skip processing
|
||||
@@ -83,12 +102,25 @@ func (l *ActivateOrderLogic) ProcessTask(ctx context.Context, task *asynq.Task)
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.WithContext(ctx).Info("[ActivateOrderLogic] 订单验证通过,开始处理",
|
||||
logger.Field("order_no", orderInfo.OrderNo),
|
||||
logger.Field("order_type", orderInfo.Type),
|
||||
logger.Field("user_id", orderInfo.UserId))
|
||||
|
||||
if err = l.processOrderByType(ctx, orderInfo); err != nil {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Process task failed", logger.Field("error", err.Error()))
|
||||
return err // Return error to trigger retry
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] 处理订单失败,将重试",
|
||||
logger.Field("order_no", orderInfo.OrderNo),
|
||||
logger.Field("order_type", orderInfo.Type),
|
||||
logger.Field("error", err.Error()))
|
||||
return err // 返回 err 允许 asynq 重试
|
||||
}
|
||||
|
||||
l.finalizeCouponAndOrder(ctx, orderInfo)
|
||||
|
||||
logger.WithContext(ctx).Info("[ActivateOrderLogic] 订单激活成功",
|
||||
logger.Field("order_no", orderInfo.OrderNo),
|
||||
logger.Field("order_type", orderInfo.Type),
|
||||
logger.Field("user_id", orderInfo.UserId))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -191,9 +223,31 @@ func (l *ActivateOrderLogic) NewPurchase(ctx context.Context, orderInfo *order.O
|
||||
return err
|
||||
}
|
||||
|
||||
userSub, err := l.createUserSubscription(ctx, orderInfo, sub)
|
||||
if err != nil {
|
||||
return err
|
||||
var userSub *user.Subscribe
|
||||
|
||||
// 单订阅模式下,检查用户是否已有赠送订阅(order_id=0)
|
||||
if l.svc.Config.Subscribe.SingleModel {
|
||||
giftSub, err := l.findGiftSubscription(ctx, orderInfo.UserId, orderInfo.SubscribeId)
|
||||
if err == nil && giftSub != nil {
|
||||
// 在赠送订阅上延长时间,保持 token 不变
|
||||
userSub, err = l.extendGiftSubscription(ctx, giftSub, orderInfo, sub)
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("Extend gift subscription failed",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("gift_subscribe_id", giftSub.Id),
|
||||
)
|
||||
// 合并失败时回退到创建新订阅
|
||||
userSub = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有合并赠送订阅,则正常创建新订阅
|
||||
if userSub == nil {
|
||||
userSub, err = l.createUserSubscription(ctx, orderInfo, sub)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Handle commission in separate goroutine to avoid blocking
|
||||
@@ -385,10 +439,61 @@ func (l *ActivateOrderLogic) createUserSubscription(ctx context.Context, orderIn
|
||||
return userSub, nil
|
||||
}
|
||||
|
||||
// findGiftSubscription 查找用户指定套餐的赠送订阅(order_id=0),包括已过期的
|
||||
// 返回找到的赠送订阅记录,如果没有则返回 nil
|
||||
func (l *ActivateOrderLogic) findGiftSubscription(ctx context.Context, userId int64, subscribeId int64) (*user.Subscribe, error) {
|
||||
// 直接查询数据库,查找 order_id=0(赠送)且同套餐的订阅,不限制过期状态
|
||||
var giftSub user.Subscribe
|
||||
err := l.svc.DB.Model(&user.Subscribe{}).
|
||||
Where("user_id = ? AND order_id = 0 AND subscribe_id = ?", userId, subscribeId).
|
||||
Order("created_at DESC").
|
||||
First(&giftSub).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &giftSub, nil
|
||||
}
|
||||
|
||||
// extendGiftSubscription 在现有赠送订阅上延长到期时间,保持 token 不变
|
||||
// 将购买的天数叠加到赠送订阅的到期时间上,并更新 order_id 为新订单 ID
|
||||
func (l *ActivateOrderLogic) extendGiftSubscription(ctx context.Context, giftSub *user.Subscribe, orderInfo *order.Order, sub *subscribe.Subscribe) (*user.Subscribe, error) {
|
||||
now := time.Now()
|
||||
// 计算基准时间:取赠送订阅到期时间和当前时间的较大值
|
||||
baseTime := giftSub.ExpireTime
|
||||
if baseTime.Before(now) {
|
||||
baseTime = now
|
||||
}
|
||||
// 在基准时间上增加购买的天数
|
||||
newExpireTime := tool.AddTime(sub.UnitTime, orderInfo.Quantity, baseTime)
|
||||
|
||||
// 更新赠送订阅的信息
|
||||
giftSub.OrderId = orderInfo.Id
|
||||
giftSub.ExpireTime = newExpireTime
|
||||
giftSub.Status = 1
|
||||
|
||||
if err := l.svc.UserModel.UpdateSubscribe(ctx, giftSub); err != nil {
|
||||
logger.WithContext(ctx).Error("Update gift subscription failed",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("subscribe_id", giftSub.Id),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logger.WithContext(ctx).Info("Extended gift subscription successfully",
|
||||
logger.Field("subscribe_id", giftSub.Id),
|
||||
logger.Field("old_expire_time", baseTime),
|
||||
logger.Field("new_expire_time", newExpireTime),
|
||||
logger.Field("order_id", orderInfo.Id),
|
||||
)
|
||||
|
||||
return giftSub, nil
|
||||
}
|
||||
|
||||
// handleCommission processes referral commission for the referrer if applicable.
|
||||
// This runs asynchronously to avoid blocking the main order processing flow.
|
||||
func (l *ActivateOrderLogic) handleCommission(ctx context.Context, userInfo *user.User, orderInfo *order.Order) {
|
||||
if !l.shouldProcessCommission(userInfo, orderInfo.IsNew) {
|
||||
l.grantGiftDaysToBothParties(ctx, userInfo, orderInfo.OrderNo)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -456,6 +561,58 @@ func (l *ActivateOrderLogic) handleCommission(ctx context.Context, userInfo *use
|
||||
}
|
||||
}
|
||||
|
||||
func (l *ActivateOrderLogic) grantGiftDaysToBothParties(ctx context.Context, referee *user.User, orderNo string) {
|
||||
giftDays := l.svc.Config.Invite.GiftDays
|
||||
if giftDays <= 0 || referee == nil || referee.Id == 0 {
|
||||
return
|
||||
}
|
||||
_ = l.grantGiftDays(ctx, referee, int(giftDays), orderNo, "邀请赠送")
|
||||
if referee.RefererId == 0 {
|
||||
return
|
||||
}
|
||||
referer, err := l.svc.UserModel.FindOne(ctx, referee.RefererId)
|
||||
if err != nil || referer == nil {
|
||||
return
|
||||
}
|
||||
_ = l.grantGiftDays(ctx, referer, int(giftDays), orderNo, "邀请赠送")
|
||||
}
|
||||
|
||||
func (l *ActivateOrderLogic) grantGiftDays(ctx context.Context, u *user.User, days int, orderNo string, remark string) error {
|
||||
if u == nil || days <= 0 {
|
||||
return nil
|
||||
}
|
||||
activeSubscribe, err := l.svc.UserModel.FindActiveSubscribe(ctx, u.Id)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
activeSubscribe.ExpireTime = activeSubscribe.ExpireTime.Add(time.Duration(days) * 24 * time.Hour)
|
||||
err = l.svc.UserModel.UpdateSubscribe(ctx, activeSubscribe)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Insert system log
|
||||
giftLog := &log.Gift{
|
||||
Type: log.GiftTypeIncrease,
|
||||
OrderNo: orderNo,
|
||||
SubscribeId: activeSubscribe.Id,
|
||||
Amount: int64(days),
|
||||
Balance: u.Balance,
|
||||
Remark: remark,
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
}
|
||||
content, _ := giftLog.Marshal()
|
||||
return l.svc.LogModel.Insert(ctx, &log.SystemLog{
|
||||
Type: log.TypeGift.Uint8(),
|
||||
Date: time.Now().Format("2006-01-02"),
|
||||
ObjectID: u.Id,
|
||||
Content: string(content),
|
||||
})
|
||||
}
|
||||
|
||||
// shouldProcessCommission determines if commission should be processed based on
|
||||
// referrer existence, commission settings, and order type
|
||||
func (l *ActivateOrderLogic) shouldProcessCommission(userInfo *user.User, isFirstPurchase bool) bool {
|
||||
|
||||
@@ -32,9 +32,17 @@ func (l *RateLogic) ProcessTask(ctx context.Context, _ *asynq.Task) error {
|
||||
CurrencyUnit string
|
||||
CurrencySymbol string
|
||||
AccessKey string
|
||||
FixedRate float64
|
||||
}{}
|
||||
tool.SystemConfigSliceReflectToStruct(currency, &configs)
|
||||
|
||||
// Check if fixed rate is enabled (greater than 0)
|
||||
if configs.FixedRate > 0 {
|
||||
l.svcCtx.ExchangeRate = configs.FixedRate
|
||||
logger.WithContext(ctx).Infof("[RateLogic] Use Fixed Exchange Rate: %f", configs.FixedRate)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Skip conversion if no exchange rate API key configured
|
||||
if configs.AccessKey == "" {
|
||||
logger.Debugf("[RateLogic] skip exchange rate, no access key configured")
|
||||
|
||||
Reference in New Issue
Block a user