- 新增资产状态、订单来源、操作人类型、实名链接类型常量 - 8个模型新增字段(asset_status/generation/source/retail_price等) - 数据库迁移000082:7张表15+字段,含存量retail_price回填 - BUG-1修复:代理零售价渠道隔离,cost_price分配锁定 - BUG-2修复:一次性佣金仅客户端订单触发 - BUG-4修复:充值回调Store操作纳入事务 - 新增资产手动停用接口(PATCH /iot-cards/:id/deactivate、/devices/:id/deactivate) - Carrier管理新增实名链接配置 - 后台订单generation写时快照 - BatchUpdatePricing支持retail_price调价目标 - 清理全部H5旧接口和个人客户旧登录方法
785 lines
27 KiB
Go
785 lines
27 KiB
Go
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"` // 一次性佣金是否已发放
|
||
}
|
||
|
||
// WechatConfigServiceInterface 支付配置服务接口
|
||
type WechatConfigServiceInterface interface {
|
||
GetActiveConfig(ctx context.Context) (*model.WechatConfig, error)
|
||
}
|
||
|
||
// Service 充值服务
|
||
// 负责资产钱包(IoT卡/设备)的充值订单创建、预检、支付回调处理等业务逻辑
|
||
type Service struct {
|
||
db *gorm.DB
|
||
assetRechargeStore *postgres.AssetRechargeStore
|
||
assetWalletStore *postgres.AssetWalletStore
|
||
assetWalletTransactionStore *postgres.AssetWalletTransactionStore
|
||
iotCardStore *postgres.IotCardStore
|
||
deviceStore *postgres.DeviceStore
|
||
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore
|
||
packageSeriesStore *postgres.PackageSeriesStore
|
||
commissionRecordStore *postgres.CommissionRecordStore
|
||
wechatConfigService WechatConfigServiceInterface
|
||
logger *zap.Logger
|
||
}
|
||
|
||
// New 创建充值服务实例
|
||
func New(
|
||
db *gorm.DB,
|
||
assetRechargeStore *postgres.AssetRechargeStore,
|
||
assetWalletStore *postgres.AssetWalletStore,
|
||
assetWalletTransactionStore *postgres.AssetWalletTransactionStore,
|
||
iotCardStore *postgres.IotCardStore,
|
||
deviceStore *postgres.DeviceStore,
|
||
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore,
|
||
packageSeriesStore *postgres.PackageSeriesStore,
|
||
commissionRecordStore *postgres.CommissionRecordStore,
|
||
wechatConfigService WechatConfigServiceInterface,
|
||
logger *zap.Logger,
|
||
) *Service {
|
||
return &Service{
|
||
db: db,
|
||
assetRechargeStore: assetRechargeStore,
|
||
assetWalletStore: assetWalletStore,
|
||
assetWalletTransactionStore: assetWalletTransactionStore,
|
||
iotCardStore: iotCardStore,
|
||
deviceStore: deviceStore,
|
||
shopSeriesAllocationStore: shopSeriesAllocationStore,
|
||
packageSeriesStore: packageSeriesStore,
|
||
commissionRecordStore: commissionRecordStore,
|
||
wechatConfigService: wechatConfigService,
|
||
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.AssetWallet
|
||
var err error
|
||
|
||
if req.ResourceType == "iot_card" {
|
||
wallet, err = s.assetWalletStore.GetByResourceTypeAndID(ctx, "iot_card", req.ResourceID)
|
||
} else if req.ResourceType == "device" {
|
||
wallet, err = s.assetWalletStore.GetByResourceTypeAndID(ctx, "device", req.ResourceID)
|
||
} 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. 查询当前生效的支付配置
|
||
var paymentConfigID *uint
|
||
if req.PaymentMethod == "wechat" || req.PaymentMethod == "alipay" {
|
||
activeConfig, err := s.wechatConfigService.GetActiveConfig(ctx)
|
||
if err != nil {
|
||
s.logger.Warn("查询生效支付配置失败", zap.Error(err))
|
||
}
|
||
if activeConfig != nil {
|
||
paymentConfigID = &activeConfig.ID
|
||
}
|
||
}
|
||
|
||
// 6. 创建充值订单
|
||
recharge := &model.AssetRechargeRecord{
|
||
UserID: userID,
|
||
AssetWalletID: wallet.ID,
|
||
ResourceType: req.ResourceType,
|
||
ResourceID: req.ResourceID,
|
||
RechargeNo: rechargeNo,
|
||
Amount: req.Amount,
|
||
PaymentMethod: req.PaymentMethod,
|
||
PaymentConfigID: paymentConfigID,
|
||
Status: constants.RechargeStatusPending,
|
||
ShopIDTag: wallet.ShopIDTag,
|
||
EnterpriseIDTag: wallet.EnterpriseIDTag,
|
||
}
|
||
|
||
if err := s.assetRechargeStore.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.assetRechargeStore.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.ListAssetRechargeParams{
|
||
Page: page,
|
||
PageSize: pageSize,
|
||
UserID: &userID,
|
||
}
|
||
|
||
if req.Status != nil {
|
||
params.Status = req.Status
|
||
}
|
||
if req.WalletID != nil {
|
||
walletID := *req.WalletID
|
||
params.AssetWalletID = &walletID
|
||
}
|
||
if req.StartTime != nil {
|
||
params.StartTime = req.StartTime
|
||
}
|
||
if req.EndTime != nil {
|
||
params.EndTime = req.EndTime
|
||
}
|
||
|
||
recharges, total, err := s.assetRechargeStore.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 支付回调处理
|
||
// 支持幂等性检查、事务处理、更新余额、触发佣金
|
||
// TODO: 按 payment_config_id 加载配置验签(当前留桩,验签由外层处理)
|
||
func (s *Service) HandlePaymentCallback(ctx context.Context, rechargeNo string, paymentMethod string, paymentTransactionID string) error {
|
||
// 1. 查询充值订单
|
||
recharge, err := s.assetRechargeStore.GetByRechargeNo(ctx, rechargeNo)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return errors.New(errors.CodeRechargeNotFound, "充值订单不存在")
|
||
}
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "查询充值订单失败")
|
||
}
|
||
|
||
// 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.assetWalletStore.GetByID(ctx, recharge.AssetWalletID)
|
||
if err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "查询钱包失败")
|
||
}
|
||
|
||
// 5. 获取钱包对应的资源类型和ID(从充值记录中直接获取)
|
||
resourceType := recharge.ResourceType
|
||
resourceID := recharge.ResourceID
|
||
|
||
// 6. 事务处理:更新订单状态、增加余额、更新累计充值、触发佣金
|
||
now := time.Now()
|
||
err = s.db.Transaction(func(tx *gorm.DB) error {
|
||
// 6.1 更新充值订单状态(带状态检查,使用事务内 tx 确保原子性)
|
||
oldStatus := constants.RechargeStatusPending
|
||
if err := s.assetRechargeStore.UpdateStatusWithOptimisticLockDB(ctx, tx, recharge.ID, &oldStatus, constants.RechargeStatusPaid, &now, nil); err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return nil
|
||
}
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "更新充值订单状态失败")
|
||
}
|
||
|
||
// 6.2 更新支付信息(使用事务内 tx)
|
||
if err := s.assetRechargeStore.UpdatePaymentInfoWithDB(ctx, tx, recharge.ID, &paymentMethod, &paymentTransactionID); err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "更新支付信息失败")
|
||
}
|
||
|
||
// 6.3 增加钱包余额(使用乐观锁)
|
||
balanceBefore := wallet.Balance
|
||
result := tx.Model(&model.AssetWallet{}).
|
||
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 创建钱包交易记录(reference_no 存储充值单号,便于前端跳转)
|
||
remark := "钱包充值"
|
||
refType := "recharge"
|
||
transaction := &model.AssetWalletTransaction{
|
||
AssetWalletID: wallet.ID,
|
||
ResourceType: resourceType,
|
||
ResourceID: resourceID,
|
||
UserID: recharge.UserID,
|
||
TransactionType: "recharge",
|
||
Amount: recharge.Amount,
|
||
BalanceBefore: balanceBefore,
|
||
BalanceAfter: balanceBefore + recharge.Amount,
|
||
Status: 1,
|
||
ReferenceType: &refType,
|
||
ReferenceNo: &recharge.RechargeNo,
|
||
Remark: &remark,
|
||
ShopIDTag: wallet.ShopIDTag,
|
||
EnterpriseIDTag: wallet.EnterpriseIDTag,
|
||
}
|
||
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.AssetRechargeRecord{}).
|
||
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
|
||
|
||
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
|
||
if seriesID != nil {
|
||
accumulatedRecharge = card.GetAccumulatedRechargeBySeries(*seriesID)
|
||
firstCommissionPaid = card.IsFirstRechargeTriggeredBySeries(*seriesID)
|
||
}
|
||
} 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
|
||
if seriesID != nil {
|
||
accumulatedRecharge = device.GetAccumulatedRechargeBySeries(*seriesID)
|
||
firstCommissionPaid = device.IsFirstRechargeTriggeredBySeries(*seriesID)
|
||
}
|
||
}
|
||
|
||
result.CurrentAccumulated = accumulatedRecharge
|
||
result.FirstCommissionPaid = firstCommissionPaid
|
||
|
||
if seriesID == nil || shopID == nil {
|
||
return result, nil
|
||
}
|
||
|
||
series, err := s.packageSeriesStore.GetByID(ctx, *seriesID)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return result, nil
|
||
}
|
||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询套餐系列失败")
|
||
}
|
||
|
||
config, err := series.GetOneTimeCommissionConfig()
|
||
if err != nil || config == nil || !config.Enable {
|
||
return result, nil
|
||
}
|
||
|
||
result.Threshold = config.Threshold
|
||
result.TriggerType = config.TriggerType
|
||
|
||
if firstCommissionPaid {
|
||
result.Message = "一次性佣金已发放,无强充要求"
|
||
return result, nil
|
||
}
|
||
|
||
if config.TriggerType == model.OneTimeCommissionTriggerFirstRecharge {
|
||
result.NeedForceRecharge = true
|
||
result.ForceRechargeAmount = config.Threshold
|
||
result.Message = fmt.Sprintf("首次充值必须充值%d分", config.Threshold)
|
||
} else if config.EnableForceRecharge {
|
||
result.NeedForceRecharge = true
|
||
if config.ForceAmount > 0 {
|
||
result.ForceRechargeAmount = config.ForceAmount
|
||
} else {
|
||
result.ForceRechargeAmount = config.Threshold
|
||
}
|
||
result.Message = fmt.Sprintf("每次充值必须充值%d分", result.ForceRechargeAmount)
|
||
} else {
|
||
result.Message = "累计充值模式,可自由充值"
|
||
}
|
||
|
||
return result, nil
|
||
}
|
||
|
||
// updateAccumulatedRechargeInTx 更新累计充值(事务内使用)
|
||
// 同时更新旧的 accumulated_recharge 字段和新的 accumulated_recharge_by_series JSON 字段
|
||
func (s *Service) updateAccumulatedRechargeInTx(ctx context.Context, tx *gorm.DB, resourceType string, resourceID uint, amount int64) error {
|
||
if resourceType == "iot_card" {
|
||
var card model.IotCard
|
||
if err := tx.First(&card, resourceID).Error; err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败")
|
||
}
|
||
|
||
if card.SeriesID != nil {
|
||
if err := card.AddAccumulatedRechargeBySeries(*card.SeriesID, amount); err != nil {
|
||
return errors.Wrap(errors.CodeInternalError, err, "更新卡按系列累计充值失败")
|
||
}
|
||
}
|
||
|
||
result := tx.Model(&model.IotCard{}).
|
||
Where("id = ?", resourceID).
|
||
Updates(map[string]any{
|
||
"accumulated_recharge": gorm.Expr("accumulated_recharge + ?", amount),
|
||
"accumulated_recharge_by_series": card.AccumulatedRechargeBySeriesJSON,
|
||
})
|
||
if result.Error != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新卡累计充值失败")
|
||
}
|
||
} else if resourceType == "device" {
|
||
var device model.Device
|
||
if err := tx.First(&device, resourceID).Error; err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "查询设备失败")
|
||
}
|
||
|
||
if device.SeriesID != nil {
|
||
if err := device.AddAccumulatedRechargeBySeries(*device.SeriesID, amount); err != nil {
|
||
return errors.Wrap(errors.CodeInternalError, err, "更新设备按系列累计充值失败")
|
||
}
|
||
}
|
||
|
||
result := tx.Model(&model.Device{}).
|
||
Where("id = ?", resourceID).
|
||
Updates(map[string]any{
|
||
"accumulated_recharge": gorm.Expr("accumulated_recharge + ?", amount),
|
||
"accumulated_recharge_by_series": device.AccumulatedRechargeBySeriesJSON,
|
||
})
|
||
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
|
||
|
||
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
|
||
shopID = card.ShopID
|
||
if seriesID != nil {
|
||
accumulatedRecharge = card.GetAccumulatedRechargeBySeries(*seriesID)
|
||
firstCommissionPaid = card.IsFirstRechargeTriggeredBySeries(*seriesID)
|
||
}
|
||
} 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
|
||
shopID = device.ShopID
|
||
if seriesID != nil {
|
||
accumulatedRecharge = device.GetAccumulatedRechargeBySeries(*seriesID)
|
||
firstCommissionPaid = device.IsFirstRechargeTriggeredBySeries(*seriesID)
|
||
}
|
||
}
|
||
|
||
if seriesID == nil || firstCommissionPaid {
|
||
return nil
|
||
}
|
||
|
||
if shopID == nil {
|
||
s.logger.Warn("资源未归属店铺,无法发放一次性佣金",
|
||
zap.String("resource_type", resourceType),
|
||
zap.Uint("resource_id", resourceID),
|
||
)
|
||
return nil
|
||
}
|
||
|
||
series, err := s.packageSeriesStore.GetByID(ctx, *seriesID)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return nil
|
||
}
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "查询套餐系列失败")
|
||
}
|
||
|
||
config, cfgErr := series.GetOneTimeCommissionConfig()
|
||
if cfgErr != nil || config == nil || !config.Enable {
|
||
return nil
|
||
}
|
||
|
||
allocation, err := s.shopSeriesAllocationStore.GetByShopAndSeries(ctx, *shopID, *seriesID)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return nil
|
||
}
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "查询系列分配失败")
|
||
}
|
||
|
||
var rechargeAmountToCheck int64
|
||
switch config.TriggerType {
|
||
case model.OneTimeCommissionTriggerFirstRecharge:
|
||
rechargeAmountToCheck = rechargeAmount
|
||
default:
|
||
rechargeAmountToCheck = accumulatedRecharge
|
||
}
|
||
|
||
if rechargeAmountToCheck < config.Threshold {
|
||
return nil
|
||
}
|
||
|
||
commissionAmount := allocation.OneTimeCommissionAmount
|
||
if commissionAmount <= 0 {
|
||
return nil
|
||
}
|
||
|
||
var commissionWallet model.AgentWallet
|
||
if err := tx.Where("shop_id = ? AND wallet_type = ?", *shopID, constants.AgentWalletTypeCommission).
|
||
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, "查询店铺佣金钱包失败")
|
||
}
|
||
|
||
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.AgentWallet{}).
|
||
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.AgentWalletTransaction{
|
||
AgentWalletID: commissionWallet.ID,
|
||
ShopID: *shopID,
|
||
UserID: userID,
|
||
TransactionType: "commission",
|
||
Amount: commissionAmount,
|
||
BalanceBefore: balanceBefore,
|
||
BalanceAfter: balanceBefore + commissionAmount,
|
||
Status: 1,
|
||
ReferenceType: &refType,
|
||
ReferenceID: &commissionRecord.ID,
|
||
Remark: &remark,
|
||
ShopIDTag: *shopID,
|
||
}
|
||
if err := tx.Create(commissionTransaction).Error; err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "创建佣金钱包交易记录失败")
|
||
}
|
||
|
||
// 14. 标记一次性佣金已发放
|
||
if resourceType == "iot_card" {
|
||
var card model.IotCard
|
||
if err := tx.First(&card, resourceID).Error; err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败")
|
||
}
|
||
if err := card.SetFirstRechargeTriggeredBySeries(*seriesID, true); err != nil {
|
||
return errors.Wrap(errors.CodeInternalError, err, "设置卡佣金发放状态失败")
|
||
}
|
||
if err := tx.Model(&model.IotCard{}).Where("id = ?", resourceID).
|
||
Updates(map[string]any{
|
||
"first_commission_paid": true,
|
||
"first_recharge_triggered_by_series": card.FirstRechargeTriggeredBySeriesJSON,
|
||
}).Error; err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "更新卡佣金发放状态失败")
|
||
}
|
||
} else {
|
||
var device model.Device
|
||
if err := tx.First(&device, resourceID).Error; err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "查询设备失败")
|
||
}
|
||
if err := device.SetFirstRechargeTriggeredBySeries(*seriesID, true); err != nil {
|
||
return errors.Wrap(errors.CodeInternalError, err, "设置设备佣金发放状态失败")
|
||
}
|
||
if err := tx.Model(&model.Device{}).Where("id = ?", resourceID).
|
||
Updates(map[string]any{
|
||
"first_commission_paid": true,
|
||
"first_recharge_triggered_by_series": device.FirstRechargeTriggeredBySeriesJSON,
|
||
}).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
|
||
}
|
||
|
||
// 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.AssetRechargeRecord) *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.AssetWalletID,
|
||
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,
|
||
}
|
||
}
|