修复一些问题,主要是生效套餐
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m16s

This commit is contained in:
2026-05-12 10:32:38 +08:00
parent b7369a9c71
commit 95fc0b0a1b
32 changed files with 5984 additions and 553 deletions

View File

@@ -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 {