feat: 新增多密码验证支持及架构文档
Build docker and publish / build (20.15.1) (push) Has been cancelled

refactor: 重构用户模型和密码验证逻辑
feat(epay): 添加支付类型支持
docs: 添加安装和配置指南文档
fix: 修复优惠券过期检查逻辑
perf: 优化设备解绑缓存清理流程
test: 添加密码验证测试用例
chore: 更新依赖版本
This commit is contained in:
2025-10-27 18:54:07 -07:00
parent fde3210a88
commit 00255a7118
65 changed files with 1523 additions and 504 deletions
-3
View File
@@ -3,7 +3,6 @@ package handler
import (
"github.com/hibiken/asynq"
"github.com/perfect-panel/server/internal/svc"
countrylogic "github.com/perfect-panel/server/queue/logic/country"
orderLogic "github.com/perfect-panel/server/queue/logic/order"
smslogic "github.com/perfect-panel/server/queue/logic/sms"
"github.com/perfect-panel/server/queue/logic/subscription"
@@ -15,8 +14,6 @@ import (
)
func RegisterHandlers(mux *asynq.ServeMux, serverCtx *svc.ServiceContext) {
// get country task
mux.Handle(types.ForthwithGetCountry, countrylogic.NewGetNodeCountryLogic(serverCtx))
// Send email task
mux.Handle(types.ForthwithSendEmail, emailLogic.NewSendEmailLogic(serverCtx))
// Send sms task
+19 -96
View File
@@ -179,8 +179,8 @@ func (l *ActivateOrderLogic) NewPurchase(ctx context.Context, orderInfo *order.O
return err
}
// Handle referral reward in separate goroutine to avoid blocking
go l.handleReferralReward(context.Background(), userInfo, orderInfo)
// Handle commission in separate goroutine to avoid blocking
go l.handleCommission(context.Background(), userInfo, orderInfo)
// Clear cache
l.clearServerCache(ctx, sub)
@@ -192,12 +192,12 @@ func (l *ActivateOrderLogic) NewPurchase(ctx context.Context, orderInfo *order.O
return nil
}
// getUserOrCreate retrieves an existing user or creates a new user based on order details
// getUserOrCreate retrieves an existing user or creates a new guest user based on order details
func (l *ActivateOrderLogic) getUserOrCreate(ctx context.Context, orderInfo *order.Order) (*user.User, error) {
if orderInfo.UserId != 0 {
return l.getExistingUser(ctx, orderInfo.UserId)
}
return l.createUserFromTempOrder(ctx, orderInfo)
return l.createGuestUser(ctx, orderInfo)
}
// getExistingUser retrieves user information by user ID
@@ -213,9 +213,9 @@ func (l *ActivateOrderLogic) getExistingUser(ctx context.Context, userId int64)
return userInfo, nil
}
// createUserFromTempOrder creates a new user account using temporary order information
// stored in Redis cache. All users created this way are formal users, not guests.
func (l *ActivateOrderLogic) createUserFromTempOrder(ctx context.Context, orderInfo *order.Order) (*user.User, error) {
// createGuestUser creates a new user account for guest orders using temporary order information
// stored in Redis cache
func (l *ActivateOrderLogic) createGuestUser(ctx context.Context, orderInfo *order.Order) (*user.User, error) {
tempOrder, err := l.getTempOrderInfo(ctx, orderInfo.OrderNo)
if err != nil {
return nil, err
@@ -223,6 +223,7 @@ func (l *ActivateOrderLogic) createUserFromTempOrder(ctx context.Context, orderI
userInfo := &user.User{
Password: tool.EncodePassWord(tempOrder.Password),
Algo: "default",
AuthMethods: []user.AuthMethods{
{
AuthType: tempOrder.AuthType,
@@ -253,7 +254,7 @@ func (l *ActivateOrderLogic) createUserFromTempOrder(ctx context.Context, orderI
// Handle referrer relationship
l.handleReferrer(ctx, userInfo, tempOrder.InviteCode)
logger.WithContext(ctx).Info("Create user success",
logger.WithContext(ctx).Info("Create guest user success",
logger.Field("user_id", userInfo.Id),
logger.Field("identifier", tempOrder.Identifier),
logger.Field("auth_type", tempOrder.AuthType),
@@ -349,12 +350,10 @@ func (l *ActivateOrderLogic) createUserSubscription(ctx context.Context, orderIn
return userSub, nil
}
// handleReferralReward processes referral rewards for the referrer if applicable.
// handleCommission processes referral commission for the referrer if applicable.
// This runs asynchronously to avoid blocking the main order processing flow.
// If referral percentage > 0: commission reward
// If referral percentage = 0: gift days to both parties
func (l *ActivateOrderLogic) handleReferralReward(ctx context.Context, userInfo *user.User, orderInfo *order.Order) {
if !l.shouldProcessReferralReward(userInfo, orderInfo.IsNew) {
func (l *ActivateOrderLogic) handleCommission(ctx context.Context, userInfo *user.User, orderInfo *order.Order) {
if !l.shouldProcessCommission(userInfo, orderInfo.IsNew) {
return
}
@@ -374,25 +373,13 @@ func (l *ActivateOrderLogic) handleReferralReward(ctx context.Context, userInfo
referralPercentage = uint8(l.svc.Config.Invite.ReferralPercentage)
}
// Check if this is commission reward or gift days reward
if referralPercentage > 0 {
// Commission reward mode
l.processCommissionReward(ctx, referer, orderInfo, referralPercentage)
} else {
// Gift days reward mode
l.processGiftDaysReward(ctx, referer, userInfo, orderInfo)
}
}
// processCommissionReward handles commission-based rewards
func (l *ActivateOrderLogic) processCommissionReward(ctx context.Context, referer *user.User, orderInfo *order.Order, percentage uint8) {
// Order commission calculation (Order Amount - Order Fee) * Referral Percentage
amount := l.calculateCommission(orderInfo.Amount-orderInfo.FeeAmount, percentage)
amount := l.calculateCommission(orderInfo.Amount-orderInfo.FeeAmount, referralPercentage)
// Use transaction for commission updates
err := l.svc.DB.Transaction(func(tx *gorm.DB) error {
err = l.svc.DB.Transaction(func(tx *gorm.DB) error {
referer.Commission += amount
if err := l.svc.UserModel.Update(ctx, referer, tx); err != nil {
if err = l.svc.UserModel.Update(ctx, referer, tx); err != nil {
return err
}
@@ -434,73 +421,9 @@ func (l *ActivateOrderLogic) processCommissionReward(ctx context.Context, refere
}
}
// processGiftDaysReward handles gift days rewards for both parties
func (l *ActivateOrderLogic) processGiftDaysReward(ctx context.Context, referer *user.User, referee *user.User, orderInfo *order.Order) {
giftDays := l.svc.Config.Invite.GiftDays
if giftDays <= 0 {
giftDays = 3 // Default to 3 days
}
// Get the subscription info to determine the unit time
sub, err := l.getSubscribeInfo(ctx, orderInfo.SubscribeId)
if err != nil {
logger.WithContext(ctx).Error("Get subscribe info failed for gift days",
logger.Field("error", err.Error()),
logger.Field("subscribe_id", orderInfo.SubscribeId),
)
return
}
// Grant gift days to both referer and referee
l.grantGiftDays(ctx, referer, giftDays, sub.UnitTime, "referer")
l.grantGiftDays(ctx, referee, giftDays, sub.UnitTime, "referee")
}
// grantGiftDays grants gift days to a user by extending their subscription
func (l *ActivateOrderLogic) grantGiftDays(ctx context.Context, user *user.User, days int64, unitTime string, role string) {
// Find user's active subscription
userSub, err := l.svc.UserModel.FindActiveSubscribe(ctx, user.Id)
if err != nil {
logger.WithContext(ctx).Error("Find user active subscription failed for gift days",
logger.Field("error", err.Error()),
logger.Field("user_id", user.Id),
logger.Field("role", role),
)
return
}
if userSub == nil {
logger.WithContext(ctx).Info("User has no active subscription for gift days",
logger.Field("user_id", user.Id),
logger.Field("role", role),
)
return
}
// Extend subscription by gift days
userSub.ExpireTime = tool.AddTime("day", days, userSub.ExpireTime)
err = l.svc.UserModel.UpdateSubscribe(ctx, userSub)
if err != nil {
logger.WithContext(ctx).Error("Update user subscription for gift days failed",
logger.Field("error", err.Error()),
logger.Field("user_id", user.Id),
logger.Field("role", role),
)
return
}
logger.WithContext(ctx).Info("Gift days granted successfully",
logger.Field("user_id", user.Id),
logger.Field("role", role),
logger.Field("days", days),
logger.Field("new_expire_time", userSub.ExpireTime),
)
}
// shouldProcessReferralReward determines if referral reward should be processed based on
// shouldProcessCommission determines if commission should be processed based on
// referrer existence, commission settings, and order type
func (l *ActivateOrderLogic) shouldProcessReferralReward(userInfo *user.User, isFirstPurchase bool) bool {
func (l *ActivateOrderLogic) shouldProcessCommission(userInfo *user.User, isFirstPurchase bool) bool {
if userInfo == nil || userInfo.RefererId == 0 {
return false
}
@@ -582,8 +505,8 @@ func (l *ActivateOrderLogic) Renewal(ctx context.Context, orderInfo *order.Order
// Clear cache
l.clearServerCache(ctx, sub)
// Handle referral reward
go l.handleReferralReward(context.Background(), userInfo, orderInfo)
// Handle commission
go l.handleCommission(context.Background(), userInfo, orderInfo)
// Send notifications
l.sendNotifications(ctx, orderInfo, userInfo, sub, userSub, telegram.RenewalNotify)
+52
View File
@@ -0,0 +1,52 @@
package task
import (
"context"
"github.com/hibiken/asynq"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/pkg/exchangeRate"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/tool"
)
type RateLogic struct {
svcCtx *svc.ServiceContext
}
func NewRateLogic(svcCtx *svc.ServiceContext) *RateLogic {
return &RateLogic{
svcCtx: svcCtx,
}
}
func (l *RateLogic) ProcessTask(ctx context.Context, _ *asynq.Task) error {
// Retrieve system currency configuration
currency, err := l.svcCtx.SystemModel.GetCurrencyConfig(ctx)
if err != nil {
logger.Errorw("[PurchaseCheckout] GetCurrencyConfig error", logger.Field("error", err.Error()))
return err
}
// Parse currency configuration
configs := struct {
CurrencyUnit string
CurrencySymbol string
AccessKey string
}{}
tool.SystemConfigSliceReflectToStruct(currency, &configs)
// Skip conversion if no exchange rate API key configured
if configs.AccessKey == "" {
logger.Debugf("[RateLogic] skip exchange rate, no access key configured")
return nil
}
// Update exchange rates
result, err := exchangeRate.GetExchangeRete(configs.CurrencyUnit, "CNY", configs.AccessKey, 1)
if err != nil {
logger.Errorw("[RateLogic] GetExchangeRete error", logger.Field("error", err.Error()))
return err
}
l.svcCtx.ExchangeRate = result
logger.WithContext(ctx).Infof("[RateLogic] GetExchangeRete success, result: %+v", result)
return nil
}
+1 -1
View File
@@ -167,7 +167,7 @@ func (l *StatLogic) ProcessTask(ctx context.Context, _ *asynq.Task) error {
// 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
err = tx.WithContext(ctx).Model(&traffic.TrafficLog{}).Where("timestamp <= ?", 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())
}
+3
View File
@@ -6,4 +6,7 @@ const (
// ForthwithQuotaTask create quota task immediately
ForthwithQuotaTask = "forthwith:quota:task"
// SchedulerExchangeRate fetch exchange rate task
SchedulerExchangeRate = "scheduler:exchange:rate"
)