package recharge import ( "context" "fmt" "math/rand" "time" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/internal/model/dto" "github.com/break/junhong_cmp_fiber/internal/store/postgres" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" "go.uber.org/zap" "gorm.io/gorm" ) // ForceRechargeRequirement 强充要求信息 type ForceRechargeRequirement struct { NeedForceRecharge bool `json:"need_force_recharge"` // 是否需要强充 ForceRechargeAmount int64 `json:"force_recharge_amount"` // 强充金额(分) TriggerType string `json:"trigger_type"` // 触发类型: single_recharge/accumulated_recharge MinAmount int64 `json:"min_amount"` // 最小充值金额 MaxAmount int64 `json:"max_amount"` // 最大充值金额 CurrentAccumulated int64 `json:"current_accumulated"` // 当前累计充值 Threshold int64 `json:"threshold"` // 佣金触发阈值 Message string `json:"message"` // 提示信息 FirstCommissionPaid bool `json:"first_commission_paid"` // 一次性佣金是否已发放 } // Service 充值服务 // 负责充值订单的创建、预检、支付回调处理等业务逻辑 type Service struct { db *gorm.DB rechargeStore *postgres.RechargeStore walletStore *postgres.WalletStore walletTransactionStore *postgres.WalletTransactionStore iotCardStore *postgres.IotCardStore deviceStore *postgres.DeviceStore shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore commissionRecordStore *postgres.CommissionRecordStore logger *zap.Logger } // New 创建充值服务实例 func New( db *gorm.DB, rechargeStore *postgres.RechargeStore, walletStore *postgres.WalletStore, walletTransactionStore *postgres.WalletTransactionStore, iotCardStore *postgres.IotCardStore, deviceStore *postgres.DeviceStore, shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore, commissionRecordStore *postgres.CommissionRecordStore, logger *zap.Logger, ) *Service { return &Service{ db: db, rechargeStore: rechargeStore, walletStore: walletStore, walletTransactionStore: walletTransactionStore, iotCardStore: iotCardStore, deviceStore: deviceStore, shopSeriesAllocationStore: shopSeriesAllocationStore, commissionRecordStore: commissionRecordStore, logger: logger, } } // Create 创建充值订单 // 验证资源、金额范围、强充要求,生成订单号 func (s *Service) Create(ctx context.Context, req *dto.CreateRechargeRequest, userID uint) (*dto.RechargeResponse, error) { // 1. 验证金额范围 if req.Amount < constants.RechargeMinAmount { return nil, errors.New(errors.CodeRechargeAmountInvalid, "充值金额不能低于1元") } if req.Amount > constants.RechargeMaxAmount { return nil, errors.New(errors.CodeRechargeAmountInvalid, "充值金额不能超过100000元") } // 2. 获取资源(卡或设备) var wallet *model.Wallet var err error if req.ResourceType == "iot_card" { wallet, err = s.walletStore.GetByResourceTypeAndID(ctx, "iot_card", req.ResourceID, "main") } else if req.ResourceType == "device" { wallet, err = s.walletStore.GetByResourceTypeAndID(ctx, "device", req.ResourceID, "main") } else { return nil, errors.New(errors.CodeInvalidParam, "无效的资源类型") } if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeWalletNotFound, "钱包不存在") } return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询钱包失败") } // 3. 验证强充要求 forceReq, err := s.checkForceRechargeRequirement(ctx, req.ResourceType, req.ResourceID) if err != nil { return nil, err } if forceReq.NeedForceRecharge && req.Amount != forceReq.ForceRechargeAmount { return nil, errors.New(errors.CodeForceRechargeAmountMismatch, fmt.Sprintf("必须充值%d分才能满足强充要求", forceReq.ForceRechargeAmount)) } // 4. 生成充值订单号 rechargeNo := s.generateRechargeNo() // 5. 创建充值订单 recharge := &model.RechargeRecord{ BaseModel: model.BaseModel{ Creator: userID, Updater: userID, }, UserID: userID, WalletID: wallet.ID, RechargeNo: rechargeNo, Amount: req.Amount, PaymentMethod: req.PaymentMethod, Status: constants.RechargeStatusPending, } if err := s.rechargeStore.Create(ctx, recharge); err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "创建充值订单失败") } s.logger.Info("创建充值订单成功", zap.Uint("recharge_id", recharge.ID), zap.String("recharge_no", rechargeNo), zap.Int64("amount", req.Amount), zap.Uint("user_id", userID), ) return s.buildRechargeResponse(recharge), nil } // GetRechargeCheck 充值预检 // 返回强充要求、金额限制等信息 func (s *Service) GetRechargeCheck(ctx context.Context, resourceType string, resourceID uint) (*ForceRechargeRequirement, error) { // 验证资源类型 if resourceType != "iot_card" && resourceType != "device" { return nil, errors.New(errors.CodeInvalidParam, "无效的资源类型") } // 验证资源是否存在 if resourceType == "iot_card" { if _, err := s.iotCardStore.GetByID(ctx, resourceID); err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeIotCardNotFound, "IoT卡不存在") } return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败") } } else { if _, err := s.deviceStore.GetByID(ctx, resourceID); err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeNotFound, "设备不存在") } return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询设备失败") } } return s.checkForceRechargeRequirement(ctx, resourceType, resourceID) } // GetByID 根据ID查询充值订单详情 // 支持数据权限过滤 func (s *Service) GetByID(ctx context.Context, id uint, userID uint) (*dto.RechargeResponse, error) { recharge, err := s.rechargeStore.GetByID(ctx, id) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeRechargeNotFound, "充值订单不存在") } return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询充值订单失败") } // 数据权限检查:只能查看自己的充值订单 if recharge.UserID != userID { return nil, errors.New(errors.CodeForbidden, "无权查看此充值订单") } return s.buildRechargeResponse(recharge), nil } // List 查询充值订单列表 // 支持分页、筛选、数据权限 func (s *Service) List(ctx context.Context, req *dto.RechargeListRequest, userID uint) (*dto.RechargeListResponse, error) { page := req.Page pageSize := req.PageSize if page == 0 { page = 1 } if pageSize == 0 { pageSize = constants.DefaultPageSize } params := &postgres.ListRechargeParams{ Page: page, PageSize: pageSize, UserID: &userID, // 数据权限:只能查看自己的 } if req.Status != nil { params.Status = req.Status } if req.WalletID != nil { params.WalletID = req.WalletID } if req.StartTime != nil { params.StartTime = req.StartTime } if req.EndTime != nil { params.EndTime = req.EndTime } recharges, total, err := s.rechargeStore.List(ctx, params) if err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询充值订单列表失败") } var list []*dto.RechargeResponse for _, r := range recharges { list = append(list, s.buildRechargeResponse(r)) } totalPages := int(total) / pageSize if int(total)%pageSize > 0 { totalPages++ } return &dto.RechargeListResponse{ List: list, Total: total, Page: page, PageSize: pageSize, TotalPages: totalPages, }, nil } // HandlePaymentCallback 支付回调处理 // 支持幂等性检查、事务处理、更新余额、触发佣金 func (s *Service) HandlePaymentCallback(ctx context.Context, rechargeNo string, paymentMethod string, paymentTransactionID string) error { // 1. 查询充值订单 recharge, err := s.rechargeStore.GetByRechargeNo(ctx, rechargeNo) if err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "查询充值订单失败") } if recharge == nil { return errors.New(errors.CodeRechargeNotFound, "充值订单不存在") } // 2. 幂等性检查:已支付则直接返回成功 if recharge.Status == constants.RechargeStatusPaid || recharge.Status == constants.RechargeStatusCompleted { s.logger.Info("充值订单已支付,跳过处理", zap.String("recharge_no", rechargeNo), zap.Int("status", recharge.Status), ) return nil } // 3. 检查订单状态是否允许支付 if recharge.Status != constants.RechargeStatusPending { return errors.New(errors.CodeInvalidStatus, "订单状态不允许支付") } // 4. 获取钱包信息 wallet, err := s.walletStore.GetByID(ctx, recharge.WalletID) if err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "查询钱包失败") } // 5. 获取钱包对应的资源类型和ID resourceType := wallet.ResourceType resourceID := wallet.ResourceID // 6. 事务处理:更新订单状态、增加余额、更新累计充值、触发佣金 now := time.Now() err = s.db.Transaction(func(tx *gorm.DB) error { // 6.1 更新充值订单状态(带状态检查,实现乐观锁) oldStatus := constants.RechargeStatusPending if err := s.rechargeStore.UpdateStatus(ctx, recharge.ID, &oldStatus, constants.RechargeStatusPaid, &now, nil); err != nil { if err == gorm.ErrRecordNotFound { // 状态已变更,幂等处理 return nil } return errors.Wrap(errors.CodeDatabaseError, err, "更新充值订单状态失败") } // 6.2 更新支付信息 if err := s.rechargeStore.UpdatePaymentInfo(ctx, recharge.ID, &paymentMethod, &paymentTransactionID); err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "更新支付信息失败") } // 6.3 增加钱包余额(使用乐观锁) balanceBefore := wallet.Balance result := tx.Model(&model.Wallet{}). Where("id = ? AND version = ?", wallet.ID, wallet.Version). Updates(map[string]any{ "balance": gorm.Expr("balance + ?", recharge.Amount), "version": gorm.Expr("version + 1"), }) if result.Error != nil { return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新钱包余额失败") } if result.RowsAffected == 0 { return errors.New(errors.CodeInternalError, "钱包版本冲突,请重试") } // 6.4 创建钱包交易记录 remark := "钱包充值" refType := "recharge" transaction := &model.WalletTransaction{ WalletID: wallet.ID, UserID: recharge.UserID, TransactionType: "recharge", Amount: recharge.Amount, BalanceBefore: balanceBefore, BalanceAfter: balanceBefore + recharge.Amount, Status: 1, ReferenceType: &refType, ReferenceID: &recharge.ID, Remark: &remark, Creator: recharge.UserID, } if err := tx.Create(transaction).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "创建钱包交易记录失败") } // 6.5 更新累计充值 if err := s.updateAccumulatedRechargeInTx(ctx, tx, resourceType, resourceID, recharge.Amount); err != nil { return err } // 6.6 触发一次性佣金判断 if err := s.triggerOneTimeCommissionIfNeededInTx(ctx, tx, resourceType, resourceID, recharge.Amount, recharge.UserID); err != nil { return err } // 6.7 更新充值订单状态为已完成 if err := tx.Model(&model.RechargeRecord{}). Where("id = ?", recharge.ID). Update("status", constants.RechargeStatusCompleted).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "更新充值订单完成状态失败") } return nil }) if err != nil { return err } s.logger.Info("充值支付回调处理成功", zap.String("recharge_no", rechargeNo), zap.Int64("amount", recharge.Amount), zap.String("resource_type", resourceType), zap.Uint("resource_id", resourceID), ) return nil } // checkForceRechargeRequirement 检查强充要求 // 根据资源类型和ID检查是否需要强制充值指定金额 func (s *Service) checkForceRechargeRequirement(ctx context.Context, resourceType string, resourceID uint) (*ForceRechargeRequirement, error) { result := &ForceRechargeRequirement{ NeedForceRecharge: false, MinAmount: constants.RechargeMinAmount, MaxAmount: constants.RechargeMaxAmount, Message: "无强充要求,可自由充值", } var seriesID *uint var shopID *uint var accumulatedRecharge int64 var firstCommissionPaid bool // 1. 查询资源信息 if resourceType == "iot_card" { card, err := s.iotCardStore.GetByID(ctx, resourceID) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeIotCardNotFound, "IoT卡不存在") } return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败") } seriesID = card.SeriesID shopID = card.ShopID accumulatedRecharge = card.AccumulatedRecharge firstCommissionPaid = card.FirstCommissionPaid } else if resourceType == "device" { device, err := s.deviceStore.GetByID(ctx, resourceID) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeNotFound, "设备不存在") } return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询设备失败") } seriesID = device.SeriesID shopID = device.ShopID accumulatedRecharge = device.AccumulatedRecharge firstCommissionPaid = device.FirstCommissionPaid } result.CurrentAccumulated = accumulatedRecharge result.FirstCommissionPaid = firstCommissionPaid // 2. 如果没有系列ID或店铺ID,无强充要求 if seriesID == nil || shopID == nil { return result, nil } // 3. 查询系列分配配置 allocation, err := s.shopSeriesAllocationStore.GetByShopAndSeries(ctx, *shopID, *seriesID) if err != nil { if err == gorm.ErrRecordNotFound { return result, nil } return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询系列分配失败") } // 4. 如果未启用一次性佣金,无强充要求 if !allocation.EnableOneTimeCommission { return result, nil } result.Threshold = allocation.OneTimeCommissionThreshold result.TriggerType = allocation.OneTimeCommissionTrigger // 5. 如果一次性佣金已发放,无强充要求 if firstCommissionPaid { result.Message = "一次性佣金已发放,无强充要求" return result, nil } // 6. 根据触发类型判断强充要求 if allocation.OneTimeCommissionTrigger == model.OneTimeCommissionTriggerSingleRecharge { // 首次充值触发:必须充值阈值金额 result.NeedForceRecharge = true result.ForceRechargeAmount = allocation.OneTimeCommissionThreshold result.Message = fmt.Sprintf("首次充值必须充值%d分", allocation.OneTimeCommissionThreshold) } else if allocation.OneTimeCommissionTrigger == model.OneTimeCommissionTriggerAccumulatedRecharge { // 累计充值触发:检查是否启用强充 if allocation.EnableForceRecharge { result.NeedForceRecharge = true // 强充金额优先使用配置值,否则使用阈值 if allocation.ForceRechargeAmount > 0 { result.ForceRechargeAmount = allocation.ForceRechargeAmount } else { result.ForceRechargeAmount = allocation.OneTimeCommissionThreshold } result.Message = fmt.Sprintf("每次充值必须充值%d分", result.ForceRechargeAmount) } else { result.Message = "累计充值模式,可自由充值" } } return result, nil } // updateAccumulatedRechargeInTx 更新累计充值(事务内使用) // 原子操作更新卡或设备的累计充值金额 func (s *Service) updateAccumulatedRechargeInTx(ctx context.Context, tx *gorm.DB, resourceType string, resourceID uint, amount int64) error { if resourceType == "iot_card" { result := tx.Model(&model.IotCard{}). Where("id = ?", resourceID). Update("accumulated_recharge", gorm.Expr("accumulated_recharge + ?", amount)) if result.Error != nil { return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新卡累计充值失败") } } else if resourceType == "device" { result := tx.Model(&model.Device{}). Where("id = ?", resourceID). Update("accumulated_recharge", gorm.Expr("accumulated_recharge + ?", amount)) if result.Error != nil { return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新设备累计充值失败") } } return nil } // triggerOneTimeCommissionIfNeededInTx 触发一次性佣金(事务内使用) // 检查是否满足一次性佣金触发条件,满足则创建佣金记录并入账 func (s *Service) triggerOneTimeCommissionIfNeededInTx(ctx context.Context, tx *gorm.DB, resourceType string, resourceID uint, rechargeAmount int64, userID uint) error { var seriesID *uint var accumulatedRecharge int64 var firstCommissionPaid bool var shopID *uint // 1. 查询资源当前状态(需要从数据库重新查询以获取更新后的累计充值) if resourceType == "iot_card" { var card model.IotCard if err := tx.First(&card, resourceID).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败") } seriesID = card.SeriesID accumulatedRecharge = card.AccumulatedRecharge firstCommissionPaid = card.FirstCommissionPaid shopID = card.ShopID } else if resourceType == "device" { var device model.Device if err := tx.First(&device, resourceID).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "查询设备失败") } seriesID = device.SeriesID accumulatedRecharge = device.AccumulatedRecharge firstCommissionPaid = device.FirstCommissionPaid shopID = device.ShopID } // 2. 如果没有系列ID或已发放佣金,跳过 if seriesID == nil || firstCommissionPaid { return nil } // 3. 如果没有归属店铺,无法发放佣金 if shopID == nil { s.logger.Warn("资源未归属店铺,无法发放一次性佣金", zap.String("resource_type", resourceType), zap.Uint("resource_id", resourceID), ) return nil } // 4. 查询系列分配配置 allocation, err := s.shopSeriesAllocationStore.GetByShopAndSeries(ctx, *shopID, *seriesID) if err != nil { if err == gorm.ErrRecordNotFound { return nil } return errors.Wrap(errors.CodeDatabaseError, err, "查询系列分配失败") } // 5. 如果未启用一次性佣金,跳过 if !allocation.EnableOneTimeCommission { return nil } // 6. 根据触发类型判断是否满足条件 var rechargeAmountToCheck int64 switch allocation.OneTimeCommissionTrigger { case model.OneTimeCommissionTriggerSingleRecharge: rechargeAmountToCheck = rechargeAmount case model.OneTimeCommissionTriggerAccumulatedRecharge: rechargeAmountToCheck = accumulatedRecharge default: return nil } // 7. 检查是否达到阈值 if rechargeAmountToCheck < allocation.OneTimeCommissionThreshold { return nil } // 8. 计算佣金金额 commissionAmount := s.calculateOneTimeCommission(allocation, rechargeAmount) if commissionAmount <= 0 { return nil } // 9. 查询店铺的佣金钱包 var commissionWallet model.Wallet if err := tx.Where("resource_type = ? AND resource_id = ? AND wallet_type = ?", "shop", *shopID, "commission"). First(&commissionWallet).Error; err != nil { if err == gorm.ErrRecordNotFound { s.logger.Warn("店铺佣金钱包不存在,跳过佣金发放", zap.Uint("shop_id", *shopID), ) return nil } return errors.Wrap(errors.CodeDatabaseError, err, "查询店铺佣金钱包失败") } // 10. 创建佣金记录 var iotCardID, deviceID *uint if resourceType == "iot_card" { iotCardID = &resourceID } else { deviceID = &resourceID } commissionRecord := &model.CommissionRecord{ BaseModel: model.BaseModel{ Creator: userID, Updater: userID, }, ShopID: *shopID, IotCardID: iotCardID, DeviceID: deviceID, CommissionSource: model.CommissionSourceOneTime, Amount: commissionAmount, Status: model.CommissionStatusReleased, Remark: "钱包充值触发一次性佣金", } if err := tx.Create(commissionRecord).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "创建佣金记录失败") } // 11. 佣金入账到店铺佣金钱包 balanceBefore := commissionWallet.Balance result := tx.Model(&model.Wallet{}). Where("id = ? AND version = ?", commissionWallet.ID, commissionWallet.Version). Updates(map[string]any{ "balance": gorm.Expr("balance + ?", commissionAmount), "version": gorm.Expr("version + 1"), }) if result.Error != nil { return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新佣金钱包余额失败") } if result.RowsAffected == 0 { return errors.New(errors.CodeInternalError, "佣金钱包版本冲突,请重试") } // 12. 更新佣金记录的入账后余额 now := time.Now() if err := tx.Model(commissionRecord).Updates(map[string]any{ "balance_after": balanceBefore + commissionAmount, "released_at": now, }).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "更新佣金记录失败") } // 13. 创建佣金钱包交易记录 remark := "一次性佣金入账(充值触发)" refType := "commission" commissionTransaction := &model.WalletTransaction{ WalletID: commissionWallet.ID, UserID: userID, TransactionType: "commission", Amount: commissionAmount, BalanceBefore: balanceBefore, BalanceAfter: balanceBefore + commissionAmount, Status: 1, ReferenceType: &refType, ReferenceID: &commissionRecord.ID, Remark: &remark, Creator: userID, } if err := tx.Create(commissionTransaction).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "创建佣金钱包交易记录失败") } // 14. 标记一次性佣金已发放 if resourceType == "iot_card" { if err := tx.Model(&model.IotCard{}).Where("id = ?", resourceID). Update("first_commission_paid", true).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "更新卡佣金发放状态失败") } } else { if err := tx.Model(&model.Device{}).Where("id = ?", resourceID). Update("first_commission_paid", true).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "更新设备佣金发放状态失败") } } s.logger.Info("一次性佣金发放成功", zap.String("resource_type", resourceType), zap.Uint("resource_id", resourceID), zap.Uint("shop_id", *shopID), zap.Int64("commission_amount", commissionAmount), ) return nil } // calculateOneTimeCommission 计算一次性佣金金额 func (s *Service) calculateOneTimeCommission(allocation *model.ShopSeriesAllocation, orderAmount int64) int64 { if allocation.OneTimeCommissionType == model.OneTimeCommissionTypeFixed { // 固定佣金 if allocation.OneTimeCommissionMode == model.CommissionModeFixed { return allocation.OneTimeCommissionValue } else if allocation.OneTimeCommissionMode == model.CommissionModePercent { // 百分比佣金(千分比) return orderAmount * allocation.OneTimeCommissionValue / 1000 } } // 梯度佣金在此不处理,由 commission_calculation 服务处理 return 0 } // generateRechargeNo 生成充值订单号 // 格式: RCH + 14位时间戳 + 6位随机数 func (s *Service) generateRechargeNo() string { now := time.Now() timestamp := now.Format("20060102150405") randomNum := rand.Intn(1000000) return fmt.Sprintf("RCH%s%06d", timestamp, randomNum) } // buildRechargeResponse 构建充值订单响应 func (s *Service) buildRechargeResponse(recharge *model.RechargeRecord) *dto.RechargeResponse { statusText := "" switch recharge.Status { case constants.RechargeStatusPending: statusText = "待支付" case constants.RechargeStatusPaid: statusText = "已支付" case constants.RechargeStatusCompleted: statusText = "已完成" case constants.RechargeStatusClosed: statusText = "已关闭" case constants.RechargeStatusRefunded: statusText = "已退款" } return &dto.RechargeResponse{ ID: recharge.ID, RechargeNo: recharge.RechargeNo, UserID: recharge.UserID, WalletID: recharge.WalletID, Amount: recharge.Amount, PaymentMethod: recharge.PaymentMethod, PaymentChannel: recharge.PaymentChannel, PaymentTransactionID: recharge.PaymentTransactionID, Status: recharge.Status, StatusText: statusText, PaidAt: recharge.PaidAt, CompletedAt: recharge.CompletedAt, CreatedAt: recharge.CreatedAt, UpdatedAt: recharge.UpdatedAt, } }