修复一些问题,主要是生效套餐
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m16s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m16s
This commit is contained in:
@@ -75,7 +75,7 @@ type RefundResponse struct {
|
||||
RejectReason string `json:"reject_reason,omitempty" description:"拒绝原因"`
|
||||
Remark string `json:"remark,omitempty" description:"审批备注"`
|
||||
CommissionDeducted bool `json:"commission_deducted" description:"佣金是否已回扣"`
|
||||
AssetReset bool `json:"asset_reset" description:"资产是否已重置"`
|
||||
AssetReset bool `json:"asset_reset" description:"退款后资产处理是否完成"`
|
||||
Creator uint `json:"creator" description:"创建人ID"`
|
||||
Updater uint `json:"updater" description:"更新人ID"`
|
||||
CreatedAt string `json:"created_at" description:"创建时间"`
|
||||
|
||||
@@ -42,7 +42,7 @@ type RefundRequest struct {
|
||||
|
||||
// 后处理标记
|
||||
CommissionDeducted bool `gorm:"column:commission_deducted;not null;default:false;comment:佣金是否已回扣" json:"commission_deducted"`
|
||||
AssetReset bool `gorm:"column:asset_reset;not null;default:false;comment:资产是否已重置(套餐停用+停机+世代重置)" json:"asset_reset"`
|
||||
AssetReset bool `gorm:"column:asset_reset;not null;default:false;comment:退款后资产处理是否完成" json:"asset_reset"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
|
||||
@@ -72,28 +72,26 @@ func (h *PackageActivationHandler) HandlePackageActivationCheck(ctx context.Cont
|
||||
return err
|
||||
}
|
||||
|
||||
if len(expiredPackages) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(expiredPackages) > 0 {
|
||||
h.logger.Info("发现过期主套餐",
|
||||
zap.Int("count", len(expiredPackages)),
|
||||
zap.Duration("check_duration", time.Since(startTime)))
|
||||
|
||||
h.logger.Info("发现过期主套餐",
|
||||
zap.Int("count", len(expiredPackages)),
|
||||
zap.Duration("check_duration", time.Since(startTime)))
|
||||
|
||||
// 处理每个过期的主套餐
|
||||
for _, pkg := range expiredPackages {
|
||||
if err := h.processExpiredPackage(ctx, pkg); err != nil {
|
||||
h.logger.Error("处理过期套餐失败",
|
||||
zap.Uint("package_usage_id", pkg.ID),
|
||||
zap.Error(err))
|
||||
// 继续处理下一个,不中断
|
||||
continue
|
||||
// 处理每个过期的主套餐
|
||||
for _, pkg := range expiredPackages {
|
||||
if err := h.processExpiredPackage(ctx, pkg); err != nil {
|
||||
h.logger.Error("处理过期套餐失败",
|
||||
zap.Uint("package_usage_id", pkg.ID),
|
||||
zap.Error(err))
|
||||
// 继续处理下一个,不中断
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h.logger.Info("套餐激活检查完成",
|
||||
zap.Int("processed", len(expiredPackages)),
|
||||
zap.Duration("total_duration", time.Since(startTime)))
|
||||
h.logger.Info("套餐激活检查完成",
|
||||
zap.Int("processed", len(expiredPackages)),
|
||||
zap.Duration("total_duration", time.Since(startTime)))
|
||||
}
|
||||
|
||||
// 任务 6: 孤儿套餐扫描
|
||||
orphanCount, err := h.findAndActivateOrphanPackages(ctx)
|
||||
@@ -109,15 +107,15 @@ func (h *PackageActivationHandler) HandlePackageActivationCheck(ctx context.Cont
|
||||
}
|
||||
|
||||
// findAndActivateOrphanPackages 任务 6.1-6.2: 查找孤儿载体并触发激活
|
||||
// 孤儿定义:存在 status=0 的主套餐(pending_realname_activation=false),但不存在 status=1 的主套餐
|
||||
// 孤儿定义:存在 status=0 的主套餐,但不存在 status=1 的主套餐。
|
||||
func (h *PackageActivationHandler) findAndActivateOrphanPackages(ctx context.Context) (int, error) {
|
||||
// 查询孤儿待生效主套餐(无生效主套餐但有待生效主套餐)
|
||||
var orphanUsages []*model.PackageUsage
|
||||
err := h.db.WithContext(ctx).
|
||||
Where("status = ?", constants.PackageUsageStatusPending).
|
||||
Where("master_usage_id IS NULL").
|
||||
Where("pending_realname_activation = ?", false).
|
||||
Where("deleted_at IS NULL").
|
||||
Order("priority ASC, created_at ASC").
|
||||
Limit(100).
|
||||
Find(&orphanUsages).Error
|
||||
if err != nil {
|
||||
@@ -178,9 +176,9 @@ func (h *PackageActivationHandler) findAndActivateOrphanPackages(ctx context.Con
|
||||
continue
|
||||
}
|
||||
|
||||
// 保留 priority 最低的套餐
|
||||
// 保留购买顺序最靠前的套餐,队首未满足实名条件时不跳过。
|
||||
if existing, ok := carrierMap[key]; ok {
|
||||
if usage.Priority < existing.Priority {
|
||||
if usage.Priority < existing.Priority || (usage.Priority == existing.Priority && usage.CreatedAt.Before(existing.CreatedAt)) {
|
||||
carrierMap[key] = usage
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -290,38 +290,34 @@ func (s *ActivationService) ActivateSpecificPackage(ctx context.Context, package
|
||||
return nil
|
||||
}
|
||||
|
||||
// 按 ExpiryBase 选择激活时间基准
|
||||
now := time.Now()
|
||||
var activatedAt time.Time
|
||||
if pkg.ExpiryBase == "from_purchase" {
|
||||
activatedAt = usage.CreatedAt
|
||||
} else {
|
||||
activatedAt = now
|
||||
}
|
||||
expiresAt := CalculateExpiryTime(pkg.CalendarType, activatedAt, pkg.DurationMonths, pkg.DurationDays)
|
||||
nextResetAt := CalculateNextResetTime(pkg.DataResetCycle, pkg.CalendarType, now, activatedAt)
|
||||
if currentUsage.MasterUsageID == nil {
|
||||
hasActive, err := s.hasActiveMainPackage(ctx, tx, carrierType, carrierID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if hasActive {
|
||||
s.logger.Info("已有生效主套餐,跳过指定套餐激活",
|
||||
zap.Uint("usage_id", currentUsage.ID),
|
||||
zap.String("carrier_type", carrierType),
|
||||
zap.Uint("carrier_id", carrierID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// 更新套餐状态
|
||||
updates := map[string]interface{}{
|
||||
"status": constants.PackageUsageStatusActive,
|
||||
"activated_at": activatedAt,
|
||||
"expires_at": expiresAt,
|
||||
}
|
||||
if nextResetAt != nil {
|
||||
updates["next_reset_at"] = *nextResetAt
|
||||
}
|
||||
if err := tx.Model(&usage).Updates(updates).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "激活套餐失败")
|
||||
canActivate, err := s.canActivatePendingUsage(ctx, tx, ¤tUsage, carrierType, carrierID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !canActivate {
|
||||
s.logger.Info("指定套餐暂不满足激活条件",
|
||||
zap.Uint("usage_id", currentUsage.ID),
|
||||
zap.Bool("pending_realname_activation", currentUsage.PendingRealnameActivation))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 同步载体状态
|
||||
s.syncCarrierStatusActivated(ctx, tx, &usage, carrierType, carrierID)
|
||||
|
||||
s.logger.Info("指定套餐已激活",
|
||||
zap.Uint("usage_id", usage.ID),
|
||||
zap.Uint("package_id", usage.PackageID),
|
||||
zap.String("carrier_type", carrierType),
|
||||
zap.Uint("carrier_id", carrierID))
|
||||
if err := s.activatePendingUsage(ctx, tx, ¤tUsage, &pkg, carrierType, carrierID, time.Now(), "指定套餐已激活"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 异步复机
|
||||
if s.resumeCallback != nil {
|
||||
@@ -339,6 +335,84 @@ func (s *ActivationService) ActivateSpecificPackage(ctx context.Context, package
|
||||
})
|
||||
}
|
||||
|
||||
// ActivateNextPendingMainPackage 按购买顺序激活载体的下一个待生效主套餐。
|
||||
// 返回 true 表示本次成功激活了一个主套餐;队首套餐未满足实名条件时返回 false。
|
||||
func (s *ActivationService) ActivateNextPendingMainPackage(ctx context.Context, carrierType string, carrierID uint) (bool, error) {
|
||||
if carrierType != constants.AssetTypeIotCard && carrierType != constants.AssetTypeDevice {
|
||||
return false, errors.New(errors.CodeInvalidParam, "无效的载体类型")
|
||||
}
|
||||
if carrierID == 0 {
|
||||
return false, errors.New(errors.CodeInvalidParam, "无效的载体ID")
|
||||
}
|
||||
|
||||
lockKey := constants.RedisPackageActivationLockKey(carrierType, carrierID)
|
||||
lockValue := time.Now().String()
|
||||
locked, err := s.redis.SetNX(ctx, lockKey, lockValue, 30*time.Second).Result()
|
||||
if err != nil {
|
||||
return false, errors.Wrap(errors.CodeRedisError, err, "获取分布式锁失败")
|
||||
}
|
||||
if !locked {
|
||||
s.logger.Warn("套餐激活正在进行中,跳过",
|
||||
zap.String("carrier_type", carrierType),
|
||||
zap.Uint("carrier_id", carrierID))
|
||||
return false, errors.New(errors.CodePackageActivationConflict)
|
||||
}
|
||||
defer s.redis.Del(ctx, lockKey)
|
||||
|
||||
activated := false
|
||||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
hasActive, err := s.hasActiveMainPackage(ctx, tx, carrierType, carrierID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if hasActive {
|
||||
return nil
|
||||
}
|
||||
|
||||
nextMain, err := s.getNextPendingMainPackage(ctx, tx, carrierType, carrierID)
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
canActivate, err := s.canActivatePendingUsage(ctx, tx, nextMain, carrierType, carrierID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !canActivate {
|
||||
s.logger.Info("队首待生效套餐暂不满足激活条件",
|
||||
zap.Uint("usage_id", nextMain.ID),
|
||||
zap.String("carrier_type", carrierType),
|
||||
zap.Uint("carrier_id", carrierID),
|
||||
zap.Bool("pending_realname_activation", nextMain.PendingRealnameActivation))
|
||||
return nil
|
||||
}
|
||||
|
||||
var pkg model.Package
|
||||
if err := tx.First(&pkg, nextMain.PackageID).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询套餐信息失败")
|
||||
}
|
||||
|
||||
if err := s.activatePendingUsage(ctx, tx, nextMain, &pkg, carrierType, carrierID, time.Now(), "队首待生效套餐已激活"); err != nil {
|
||||
return err
|
||||
}
|
||||
activated = true
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return activated, nil
|
||||
}
|
||||
|
||||
// HasActiveMainPackage 查询载体当前是否存在生效中的主套餐。
|
||||
func (s *ActivationService) HasActiveMainPackage(ctx context.Context, carrierType string, carrierID uint) (bool, error) {
|
||||
return s.hasActiveMainPackage(ctx, s.db.WithContext(ctx), carrierType, carrierID)
|
||||
}
|
||||
|
||||
// invalidateAddons 任务 9.7: 加油包级联失效
|
||||
func (s *ActivationService) invalidateAddons(ctx context.Context, tx *gorm.DB, masterUsageID uint) error {
|
||||
var addons []*model.PackageUsage
|
||||
@@ -374,13 +448,7 @@ func (s *ActivationService) invalidateAddons(ctx context.Context, tx *gorm.DB, m
|
||||
// activateNextMainPackage 任务 9.6: 激活下一个待生效主套餐
|
||||
func (s *ActivationService) activateNextMainPackage(ctx context.Context, tx *gorm.DB, carrierType string, carrierID uint, now time.Time) error {
|
||||
// 查询下一个待生效主套餐
|
||||
var nextMain model.PackageUsage
|
||||
err := tx.Where("status = ?", constants.PackageUsageStatusPending).
|
||||
Where("master_usage_id IS NULL").
|
||||
Where(carrierType+"_id = ?", carrierID).
|
||||
Order("priority ASC").
|
||||
First(&nextMain).Error
|
||||
|
||||
nextMain, err := s.getNextPendingMainPackage(ctx, tx, carrierType, carrierID)
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
s.logger.Info("没有待生效的主套餐",
|
||||
zap.String("carrier_type", carrierType),
|
||||
@@ -388,7 +456,20 @@ func (s *ActivationService) activateNextMainPackage(ctx context.Context, tx *gor
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询下一个待生效主套餐失败")
|
||||
return err
|
||||
}
|
||||
|
||||
canActivate, err := s.canActivatePendingUsage(ctx, tx, nextMain, carrierType, carrierID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !canActivate {
|
||||
s.logger.Info("下一个待生效主套餐暂不满足激活条件",
|
||||
zap.Uint("usage_id", nextMain.ID),
|
||||
zap.String("carrier_type", carrierType),
|
||||
zap.Uint("carrier_id", carrierID),
|
||||
zap.Bool("pending_realname_activation", nextMain.PendingRealnameActivation))
|
||||
return nil
|
||||
}
|
||||
|
||||
// 查询套餐信息
|
||||
@@ -397,11 +478,81 @@ func (s *ActivationService) activateNextMainPackage(ctx context.Context, tx *gor
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询套餐信息失败")
|
||||
}
|
||||
|
||||
// 激活套餐
|
||||
// 按 ExpiryBase 选择激活计时基准:from_purchase-购买时起算用 CreatedAt,否则用当前时刻
|
||||
return s.activatePendingUsage(ctx, tx, nextMain, &pkg, carrierType, carrierID, now, "排队主套餐已激活")
|
||||
}
|
||||
|
||||
func (s *ActivationService) getNextPendingMainPackage(ctx context.Context, tx *gorm.DB, carrierType string, carrierID uint) (*model.PackageUsage, error) {
|
||||
var nextMain model.PackageUsage
|
||||
err := tx.WithContext(ctx).
|
||||
Where("status = ?", constants.PackageUsageStatusPending).
|
||||
Where("master_usage_id IS NULL").
|
||||
Where(carrierType+"_id = ?", carrierID).
|
||||
Order("priority ASC, created_at ASC").
|
||||
First(&nextMain).Error
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, err
|
||||
}
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询下一个待生效主套餐失败")
|
||||
}
|
||||
return &nextMain, nil
|
||||
}
|
||||
|
||||
func (s *ActivationService) hasActiveMainPackage(ctx context.Context, tx *gorm.DB, carrierType string, carrierID uint) (bool, error) {
|
||||
var count int64
|
||||
if err := tx.WithContext(ctx).
|
||||
Model(&model.PackageUsage{}).
|
||||
Where("status = ?", constants.PackageUsageStatusActive).
|
||||
Where("master_usage_id IS NULL").
|
||||
Where(carrierType+"_id = ?", carrierID).
|
||||
Count(&count).Error; err != nil {
|
||||
return false, errors.Wrap(errors.CodeDatabaseError, err, "检查生效中主套餐失败")
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (s *ActivationService) canActivatePendingUsage(ctx context.Context, tx *gorm.DB, usage *model.PackageUsage, carrierType string, carrierID uint) (bool, error) {
|
||||
if usage == nil {
|
||||
return false, nil
|
||||
}
|
||||
if !usage.PendingRealnameActivation {
|
||||
return true, nil
|
||||
}
|
||||
return s.isCarrierRealnamed(ctx, tx, carrierType, carrierID)
|
||||
}
|
||||
|
||||
func (s *ActivationService) isCarrierRealnamed(ctx context.Context, tx *gorm.DB, carrierType string, carrierID uint) (bool, error) {
|
||||
switch carrierType {
|
||||
case constants.AssetTypeIotCard:
|
||||
var card model.IotCard
|
||||
if err := tx.WithContext(ctx).
|
||||
Select("real_name_status").
|
||||
First(&card, carrierID).Error; err != nil {
|
||||
return false, errors.Wrap(errors.CodeDatabaseError, err, "查询卡实名状态失败")
|
||||
}
|
||||
return card.RealNameStatus == constants.RealNameStatusVerified, nil
|
||||
case constants.AssetTypeDevice:
|
||||
var count int64
|
||||
subQuery := tx.WithContext(ctx).
|
||||
Model(&model.DeviceSimBinding{}).
|
||||
Select("iot_card_id").
|
||||
Where("device_id = ? AND bind_status = ?", carrierID, constants.BindStatusBound)
|
||||
if err := tx.WithContext(ctx).
|
||||
Model(&model.IotCard{}).
|
||||
Where("id IN (?) AND real_name_status = ?", subQuery, constants.RealNameStatusVerified).
|
||||
Count(&count).Error; err != nil {
|
||||
return false, errors.Wrap(errors.CodeDatabaseError, err, "查询设备实名状态失败")
|
||||
}
|
||||
return count > 0, nil
|
||||
default:
|
||||
return false, errors.New(errors.CodeInvalidParam, "无效的载体类型")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ActivationService) activatePendingUsage(ctx context.Context, tx *gorm.DB, usage *model.PackageUsage, pkg *model.Package, carrierType string, carrierID uint, now time.Time, logMessage string) error {
|
||||
var activatedAt time.Time
|
||||
if pkg.ExpiryBase == "from_purchase" {
|
||||
activatedAt = nextMain.CreatedAt
|
||||
activatedAt = usage.CreatedAt
|
||||
} else {
|
||||
activatedAt = now
|
||||
}
|
||||
@@ -412,23 +563,24 @@ func (s *ActivationService) activateNextMainPackage(ctx context.Context, tx *gor
|
||||
|
||||
// 更新套餐使用记录
|
||||
updates := map[string]interface{}{
|
||||
"status": constants.PackageUsageStatusActive,
|
||||
"activated_at": activatedAt,
|
||||
"expires_at": expiresAt,
|
||||
"status": constants.PackageUsageStatusActive,
|
||||
"pending_realname_activation": false,
|
||||
"activated_at": activatedAt,
|
||||
"expires_at": expiresAt,
|
||||
}
|
||||
if nextResetAt != nil {
|
||||
updates["next_reset_at"] = *nextResetAt
|
||||
}
|
||||
|
||||
if err := tx.Model(&nextMain).Updates(updates).Error; err != nil {
|
||||
if err := tx.Model(usage).Updates(updates).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "激活排队主套餐失败")
|
||||
}
|
||||
|
||||
s.syncCarrierStatusActivated(ctx, tx, &nextMain, carrierType, carrierID)
|
||||
s.syncCarrierStatusActivated(ctx, tx, usage, carrierType, carrierID)
|
||||
|
||||
s.logger.Info("排队主套餐已激活",
|
||||
zap.Uint("usage_id", nextMain.ID),
|
||||
zap.Uint("package_id", nextMain.PackageID),
|
||||
s.logger.Info(logMessage,
|
||||
zap.Uint("usage_id", usage.ID),
|
||||
zap.Uint("package_id", usage.PackageID),
|
||||
zap.Time("activated_at", activatedAt),
|
||||
zap.Time("expires_at", expiresAt))
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Package refund 提供退款申请的业务逻辑服务
|
||||
// 包含退款申请的创建、审批、拒绝、退回、重新提交等完整生命周期管理
|
||||
// 审批通过后异步执行佣金回扣和资产世代重置
|
||||
// 审批通过后异步执行佣金回扣和退款后资产处理
|
||||
package refund
|
||||
|
||||
import (
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
)
|
||||
|
||||
// Service 退款业务服务
|
||||
// 负责退款申请的 CRUD、审批流程、佣金回扣和资产世代重置
|
||||
// 负责退款申请的 CRUD、审批流程、佣金回扣和退款后资产处理
|
||||
type Service struct {
|
||||
db *gorm.DB
|
||||
refundStore *postgres.RefundStore
|
||||
@@ -187,7 +187,7 @@ func (s *Service) GetByID(ctx context.Context, id uint) (*dto.RefundResponse, er
|
||||
|
||||
// Approve 审批通过退款申请
|
||||
// 条件更新 WHERE status=1,设置审批信息
|
||||
// 事务提交成功后异步执行佣金回扣和资产重置
|
||||
// 事务提交成功后异步执行佣金回扣和退款后资产处理
|
||||
func (s *Service) Approve(ctx context.Context, id uint, req *dto.ApproveRefundRequest) error {
|
||||
userID := middleware.GetUserIDFromContext(ctx)
|
||||
if userID == 0 {
|
||||
@@ -249,7 +249,7 @@ func (s *Service) Approve(ctx context.Context, id uint, req *dto.ApproveRefundRe
|
||||
return err
|
||||
}
|
||||
|
||||
// 事务提交成功后,异步执行佣金回扣和资产重置(失败不影响审批结果)
|
||||
// 事务提交成功后,异步执行佣金回扣和退款后资产处理(失败不影响审批结果)
|
||||
go func() {
|
||||
asyncCtx := context.Background()
|
||||
s.deductAllCommission(asyncCtx, id)
|
||||
@@ -257,7 +257,7 @@ func (s *Service) Approve(ctx context.Context, id uint, req *dto.ApproveRefundRe
|
||||
|
||||
go func() {
|
||||
asyncCtx := context.Background()
|
||||
s.handleAssetReset(asyncCtx, id)
|
||||
s.handleRefundAssetProcessing(asyncCtx, id)
|
||||
}()
|
||||
|
||||
return nil
|
||||
@@ -489,23 +489,23 @@ func (s *Service) deductSingleCommission(ctx context.Context, refund *model.Refu
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleAssetReset 异步处理资产重置
|
||||
// 包括:退款套餐精准失效、停机、世代重置(generation+1)、钱包重建
|
||||
// handleRefundAssetProcessing 异步处理退款后的资产状态
|
||||
// 包括:退款套餐精准失效、尝试接续待生效主套餐、必要时停机
|
||||
// 失败仅记录日志,不影响审批结果
|
||||
func (s *Service) handleAssetReset(ctx context.Context, refundID uint) {
|
||||
func (s *Service) handleRefundAssetProcessing(ctx context.Context, refundID uint) {
|
||||
logger := s.logger
|
||||
|
||||
// 查询退款单
|
||||
var refund model.RefundRequest
|
||||
if err := s.db.Where("id = ?", refundID).First(&refund).Error; err != nil {
|
||||
logger.Error("资产重置:查询退款单失败", zap.Uint("refund_id", refundID), zap.Error(err))
|
||||
logger.Error("退款资产处理:查询退款单失败", zap.Uint("refund_id", refundID), zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
// 查询关联订单
|
||||
var order model.Order
|
||||
if err := s.db.Where("id = ?", refund.OrderID).First(&order).Error; err != nil {
|
||||
logger.Error("资产重置:查询订单失败", zap.Uint("refund_id", refundID), zap.Uint("order_id", refund.OrderID), zap.Error(err))
|
||||
logger.Error("退款资产处理:查询订单失败", zap.Uint("refund_id", refundID), zap.Uint("order_id", refund.OrderID), zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -515,53 +515,73 @@ func (s *Service) handleAssetReset(ctx context.Context, refundID uint) {
|
||||
switch order.OrderType {
|
||||
case model.OrderTypeSingleCard:
|
||||
if order.IotCardID == nil {
|
||||
logger.Error("资产重置:单卡订单缺少 iot_card_id", zap.Uint("order_id", order.ID))
|
||||
logger.Error("退款资产处理:单卡订单缺少 iot_card_id", zap.Uint("order_id", order.ID))
|
||||
return
|
||||
}
|
||||
assetType = "iot_card"
|
||||
assetID = *order.IotCardID
|
||||
case model.OrderTypeDevice:
|
||||
if order.DeviceID == nil {
|
||||
logger.Error("资产重置:设备订单缺少 device_id", zap.Uint("order_id", order.ID))
|
||||
logger.Error("退款资产处理:设备订单缺少 device_id", zap.Uint("order_id", order.ID))
|
||||
return
|
||||
}
|
||||
assetType = "device"
|
||||
assetID = *order.DeviceID
|
||||
default:
|
||||
logger.Error("资产重置:未知订单类型", zap.String("order_type", order.OrderType))
|
||||
logger.Error("退款资产处理:未知订单类型", zap.String("order_type", order.OrderType))
|
||||
return
|
||||
}
|
||||
|
||||
// 1. 按退款单精准失效套餐(仅处理本次退款订单关联套餐)
|
||||
if s.packageActivationService != nil {
|
||||
if err := s.packageActivationService.InvalidatePackagesForRefund(ctx, assetType, assetID, order.ID, refund.ID, refund.RefundNo, refund.PackageUsageID); err != nil {
|
||||
fields := []zap.Field{
|
||||
zap.String("asset_type", assetType),
|
||||
zap.Uint("asset_id", assetID),
|
||||
zap.Uint("order_id", order.ID),
|
||||
zap.Uint("refund_id", refund.ID),
|
||||
zap.String("refund_no", refund.RefundNo),
|
||||
zap.Error(err),
|
||||
}
|
||||
if refund.PackageUsageID != nil {
|
||||
fields = append(fields, zap.Uint("package_usage_id", *refund.PackageUsageID))
|
||||
}
|
||||
logger.Error("资产重置:退款套餐精准失效失败", fields...)
|
||||
}
|
||||
if s.packageActivationService == nil {
|
||||
logger.Error("退款资产处理:套餐激活服务未注入",
|
||||
zap.Uint("refund_id", refund.ID),
|
||||
zap.Uint("order_id", order.ID))
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 停机
|
||||
s.stopAsset(ctx, assetType, assetID)
|
||||
|
||||
// 3. 世代重置(参照 exchange/service.go)
|
||||
if err := s.resetAssetGeneration(ctx, assetType, assetID); err != nil {
|
||||
logger.Error("资产重置:世代重置失败", zap.String("asset_type", assetType), zap.Uint("asset_id", assetID), zap.Error(err))
|
||||
if err := s.packageActivationService.InvalidatePackagesForRefund(ctx, assetType, assetID, order.ID, refund.ID, refund.RefundNo, refund.PackageUsageID); err != nil {
|
||||
fields := []zap.Field{
|
||||
zap.String("asset_type", assetType),
|
||||
zap.Uint("asset_id", assetID),
|
||||
zap.Uint("order_id", order.ID),
|
||||
zap.Uint("refund_id", refund.ID),
|
||||
zap.String("refund_no", refund.RefundNo),
|
||||
zap.Error(err),
|
||||
}
|
||||
if refund.PackageUsageID != nil {
|
||||
fields = append(fields, zap.Uint("package_usage_id", *refund.PackageUsageID))
|
||||
}
|
||||
logger.Error("退款资产处理:退款套餐精准失效失败", fields...)
|
||||
return
|
||||
}
|
||||
|
||||
// 4. 标记退款单资产已重置
|
||||
// 2. 尝试按购买顺序接续待生效主套餐
|
||||
if _, err := s.packageActivationService.ActivateNextPendingMainPackage(ctx, assetType, assetID); err != nil {
|
||||
logger.Error("退款资产处理:接续激活待生效套餐失败",
|
||||
zap.String("asset_type", assetType),
|
||||
zap.Uint("asset_id", assetID),
|
||||
zap.Uint("refund_id", refund.ID),
|
||||
zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
hasActiveMain, err := s.packageActivationService.HasActiveMainPackage(ctx, assetType, assetID)
|
||||
if err != nil {
|
||||
logger.Error("退款资产处理:查询生效主套餐失败",
|
||||
zap.String("asset_type", assetType),
|
||||
zap.Uint("asset_id", assetID),
|
||||
zap.Uint("refund_id", refund.ID),
|
||||
zap.Error(err))
|
||||
return
|
||||
}
|
||||
if !hasActiveMain {
|
||||
// 3. 无可用主套餐时才停机;退款不再重置世代或重建钱包。
|
||||
s.stopAsset(ctx, assetType, assetID)
|
||||
}
|
||||
|
||||
// 4. 标记退款后资产处理已完成
|
||||
if err := s.db.Model(&model.RefundRequest{}).Where("id = ?", refundID).Update("asset_reset", true).Error; err != nil {
|
||||
logger.Error("资产重置:更新重置标记失败", zap.Uint("refund_id", refundID), zap.Error(err))
|
||||
logger.Error("退款资产处理:更新处理标记失败", zap.Uint("refund_id", refundID), zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -574,132 +594,24 @@ func (s *Service) stopAsset(ctx context.Context, assetType string, assetID uint)
|
||||
// 单卡停机:需要先查卡获取 iccid
|
||||
card, err := s.iotCardStore.GetByID(ctx, assetID)
|
||||
if err != nil {
|
||||
logger.Error("资产重置:查询卡信息失败", zap.Uint("card_id", assetID), zap.Error(err))
|
||||
logger.Error("退款资产处理:查询卡信息失败", zap.Uint("card_id", assetID), zap.Error(err))
|
||||
return
|
||||
}
|
||||
if s.stopResumeService != nil {
|
||||
if err := s.stopResumeService.ManualStopCard(ctx, card.ICCID); err != nil {
|
||||
logger.Error("资产重置:单卡停机失败", zap.String("iccid", card.ICCID), zap.Error(err))
|
||||
logger.Error("退款资产处理:单卡停机失败", zap.String("iccid", card.ICCID), zap.Error(err))
|
||||
}
|
||||
}
|
||||
case "device":
|
||||
// 设备停机
|
||||
if s.deviceService != nil {
|
||||
if _, err := s.deviceService.StopDevice(ctx, assetID); err != nil {
|
||||
logger.Error("资产重置:设备停机失败", zap.Uint("device_id", assetID), zap.Error(err))
|
||||
logger.Error("退款资产处理:设备停机失败", zap.Uint("device_id", assetID), zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// resetAssetGeneration 执行资产世代重置
|
||||
// 参照 exchange/service.go 实现:
|
||||
// 1. generation+1, asset_status=1, 清零累计充值和佣金标记
|
||||
// 2. 清理个人客户绑定
|
||||
// 3. 删除旧钱包、创建新空钱包
|
||||
func (s *Service) resetAssetGeneration(ctx context.Context, assetType string, assetID uint) error {
|
||||
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
now := time.Now()
|
||||
|
||||
if assetType == "iot_card" {
|
||||
var card model.IotCard
|
||||
if err := tx.Where("id = ?", assetID).First(&card).Error; err != nil {
|
||||
return fmt.Errorf("查询卡信息失败: %w", err)
|
||||
}
|
||||
|
||||
// 世代+1, 重置状态和累计数据
|
||||
if err := tx.Model(&model.IotCard{}).Where("id = ?", card.ID).Updates(map[string]any{
|
||||
"generation": card.Generation + 1,
|
||||
"asset_status": 1,
|
||||
"accumulated_recharge_by_series": "{}",
|
||||
"first_recharge_triggered_by_series": "{}",
|
||||
"updated_at": now,
|
||||
}).Error; err != nil {
|
||||
return fmt.Errorf("重置卡世代失败: %w", err)
|
||||
}
|
||||
|
||||
// 清理个人客户绑定(按 virtual_no)
|
||||
if card.VirtualNo != "" {
|
||||
if err := tx.Where("virtual_no = ?", card.VirtualNo).Delete(&model.PersonalCustomerDevice{}).Error; err != nil {
|
||||
return fmt.Errorf("清理个人客户绑定失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除旧钱包、创建新空钱包
|
||||
if err := tx.Where("resource_type = ? AND resource_id = ?", constants.ExchangeAssetTypeIotCard, card.ID).Delete(&model.AssetWallet{}).Error; err != nil {
|
||||
return fmt.Errorf("清理旧钱包失败: %w", err)
|
||||
}
|
||||
|
||||
shopTag := uint(0)
|
||||
if card.ShopID != nil {
|
||||
shopTag = *card.ShopID
|
||||
}
|
||||
if err := tx.Create(&model.AssetWallet{
|
||||
ResourceType: constants.ExchangeAssetTypeIotCard,
|
||||
ResourceID: card.ID,
|
||||
Balance: 0,
|
||||
FrozenBalance: 0,
|
||||
Currency: "CNY",
|
||||
Status: 1,
|
||||
Version: 0,
|
||||
ShopIDTag: shopTag,
|
||||
}).Error; err != nil {
|
||||
return fmt.Errorf("创建新钱包失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 设备类型
|
||||
var device model.Device
|
||||
if err := tx.Where("id = ?", assetID).First(&device).Error; err != nil {
|
||||
return fmt.Errorf("查询设备失败: %w", err)
|
||||
}
|
||||
|
||||
// 世代+1, 重置状态和累计数据
|
||||
if err := tx.Model(&model.Device{}).Where("id = ?", device.ID).Updates(map[string]any{
|
||||
"generation": device.Generation + 1,
|
||||
"asset_status": 1,
|
||||
"accumulated_recharge_by_series": "{}",
|
||||
"first_recharge_triggered_by_series": "{}",
|
||||
"updated_at": now,
|
||||
}).Error; err != nil {
|
||||
return fmt.Errorf("重置设备世代失败: %w", err)
|
||||
}
|
||||
|
||||
// 清理个人客户绑定(按 virtual_no)
|
||||
if device.VirtualNo != "" {
|
||||
if err := tx.Where("virtual_no = ?", device.VirtualNo).Delete(&model.PersonalCustomerDevice{}).Error; err != nil {
|
||||
return fmt.Errorf("清理个人客户绑定失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除旧钱包、创建新空钱包
|
||||
if err := tx.Where("resource_type = ? AND resource_id = ?", constants.ExchangeAssetTypeDevice, device.ID).Delete(&model.AssetWallet{}).Error; err != nil {
|
||||
return fmt.Errorf("清理旧钱包失败: %w", err)
|
||||
}
|
||||
|
||||
shopTag := uint(0)
|
||||
if device.ShopID != nil {
|
||||
shopTag = *device.ShopID
|
||||
}
|
||||
if err := tx.Create(&model.AssetWallet{
|
||||
ResourceType: constants.ExchangeAssetTypeDevice,
|
||||
ResourceID: device.ID,
|
||||
Balance: 0,
|
||||
FrozenBalance: 0,
|
||||
Currency: "CNY",
|
||||
Status: 1,
|
||||
Version: 0,
|
||||
ShopIDTag: shopTag,
|
||||
}).Error; err != nil {
|
||||
return fmt.Errorf("创建新钱包失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// generateRefundNo 生成退款单号
|
||||
// 格式:RF + 日期时间 + 6位随机数
|
||||
func generateRefundNo() string {
|
||||
|
||||
Reference in New Issue
Block a user