feat: 新增退款管理模块 — 完整的退款审批流程、佣金回扣和资产重置
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m8s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m8s
新增退款申请全生命周期管理:创建/列表/详情/审批通过/拒绝/退回/重新提交 审批通过后异步执行佣金全额回扣(扣减各代理佣金钱包)和资产重置(套餐失效+停机+世代重置) 新增 tb_refund_request 表(迁移 000093)、RefundRequest Model、8 个 DTO 新增 Store/Service/Handler/路由注册,仅平台用户可访问
This commit is contained in:
@@ -373,7 +373,7 @@ func mapSyncRespToDeviceGatewayInfo(r *gateway.SyncDeviceInfoResp) *dto.DeviceGa
|
||||
Rssi: strPtr(r.RSSI),
|
||||
Sinr: &r.Sinr,
|
||||
SSID: strPtr(r.SSID),
|
||||
WifiEnabled: &r.WifiEnabled,
|
||||
WifiEnabled: flexBoolPtr(r.WifiEnabled),
|
||||
WifiPassword: strPtr(r.WifiPassword),
|
||||
IPAddress: strPtr(r.IPAddress),
|
||||
WANIP: strPtr(r.WANIP),
|
||||
@@ -410,6 +410,11 @@ func strPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
func flexBoolPtr(fb gateway.FlexBool) *bool {
|
||||
b := bool(fb)
|
||||
return &b
|
||||
}
|
||||
|
||||
// Refresh 刷新资产数据(调网关同步)
|
||||
func (s *Service) Refresh(ctx context.Context, assetType string, id uint) (*dto.AssetRealtimeStatusResponse, error) {
|
||||
switch assetType {
|
||||
|
||||
@@ -369,3 +369,37 @@ func (s *ActivationService) syncCarrierStatusActivated(ctx context.Context, tx *
|
||||
zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// InvalidateAllPackagesByAsset 批量失效资产关联的所有有效套餐
|
||||
// 退款时调用:将该资产下状态为待生效(0)、生效中(1)、已用完(2)的套餐全部标记为已失效(4)
|
||||
func (s *ActivationService) InvalidateAllPackagesByAsset(ctx context.Context, assetType string, assetID uint) error {
|
||||
query := s.db.WithContext(ctx).
|
||||
Model(&model.PackageUsage{}).
|
||||
Where("status IN ?", []int{
|
||||
constants.PackageUsageStatusPending,
|
||||
constants.PackageUsageStatusActive,
|
||||
constants.PackageUsageStatusDepleted,
|
||||
})
|
||||
|
||||
switch assetType {
|
||||
case "iot_card":
|
||||
query = query.Where("iot_card_id = ?", assetID)
|
||||
case "device":
|
||||
query = query.Where("device_id = ?", assetID)
|
||||
default:
|
||||
return errors.New(errors.CodeInvalidParam, "无效的资产类型")
|
||||
}
|
||||
|
||||
result := query.Update("status", constants.PackageUsageStatusInvalidated)
|
||||
if result.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "批量失效套餐失败")
|
||||
}
|
||||
|
||||
s.logger.Info("批量失效套餐完成",
|
||||
zap.String("asset_type", assetType),
|
||||
zap.Uint("asset_id", assetID),
|
||||
zap.Int64("affected", result.RowsAffected),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
670
internal/service/refund/service.go
Normal file
670
internal/service/refund/service.go
Normal file
@@ -0,0 +1,670 @@
|
||||
// Package refund 提供退款申请的业务逻辑服务
|
||||
// 包含退款申请的创建、审批、拒绝、退回、重新提交等完整生命周期管理
|
||||
// 审批通过后异步执行佣金回扣和资产世代重置
|
||||
package refund
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"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"
|
||||
"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"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
|
||||
deviceSvc "github.com/break/junhong_cmp_fiber/internal/service/device"
|
||||
iotCardSvc "github.com/break/junhong_cmp_fiber/internal/service/iot_card"
|
||||
packageSvc "github.com/break/junhong_cmp_fiber/internal/service/package"
|
||||
)
|
||||
|
||||
// Service 退款业务服务
|
||||
// 负责退款申请的 CRUD、审批流程、佣金回扣和资产世代重置
|
||||
type Service struct {
|
||||
db *gorm.DB
|
||||
refundStore *postgres.RefundStore
|
||||
orderStore *postgres.OrderStore
|
||||
commissionRecordStore *postgres.CommissionRecordStore
|
||||
agentWalletStore *postgres.AgentWalletStore
|
||||
agentWalletTransactionStore *postgres.AgentWalletTransactionStore
|
||||
stopResumeService *iotCardSvc.StopResumeService
|
||||
deviceService *deviceSvc.Service
|
||||
packageActivationService *packageSvc.ActivationService
|
||||
iotCardStore *postgres.IotCardStore
|
||||
deviceStore *postgres.DeviceStore
|
||||
assetWalletStore *postgres.AssetWalletStore
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// New 创建退款业务服务实例
|
||||
func New(
|
||||
db *gorm.DB,
|
||||
refundStore *postgres.RefundStore,
|
||||
orderStore *postgres.OrderStore,
|
||||
commissionRecordStore *postgres.CommissionRecordStore,
|
||||
agentWalletStore *postgres.AgentWalletStore,
|
||||
agentWalletTransactionStore *postgres.AgentWalletTransactionStore,
|
||||
stopResumeService *iotCardSvc.StopResumeService,
|
||||
deviceService *deviceSvc.Service,
|
||||
packageActivationService *packageSvc.ActivationService,
|
||||
iotCardStore *postgres.IotCardStore,
|
||||
deviceStore *postgres.DeviceStore,
|
||||
assetWalletStore *postgres.AssetWalletStore,
|
||||
logger *zap.Logger,
|
||||
) *Service {
|
||||
return &Service{
|
||||
db: db,
|
||||
refundStore: refundStore,
|
||||
orderStore: orderStore,
|
||||
commissionRecordStore: commissionRecordStore,
|
||||
agentWalletStore: agentWalletStore,
|
||||
agentWalletTransactionStore: agentWalletTransactionStore,
|
||||
stopResumeService: stopResumeService,
|
||||
deviceService: deviceService,
|
||||
packageActivationService: packageActivationService,
|
||||
iotCardStore: iotCardStore,
|
||||
deviceStore: deviceStore,
|
||||
assetWalletStore: assetWalletStore,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Create 创建退款申请
|
||||
// 校验订单存在且已支付,检查是否存在活跃退款申请,生成退款单号并创建记录
|
||||
func (s *Service) Create(ctx context.Context, req *dto.CreateRefundRequest) (*dto.RefundResponse, error) {
|
||||
userID := middleware.GetUserIDFromContext(ctx)
|
||||
if userID == 0 {
|
||||
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
}
|
||||
|
||||
// 校验订单存在且已支付
|
||||
order, err := s.orderStore.GetByID(ctx, req.OrderID)
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.CodeNotFound, "订单不存在")
|
||||
}
|
||||
if order.PaymentStatus != model.PaymentStatusPaid {
|
||||
return nil, errors.New(errors.CodeInvalidStatus, "仅已支付订单可申请退款")
|
||||
}
|
||||
|
||||
// 检查是否存在活跃退款申请(待审批、已通过、已退回状态)
|
||||
existing, err := s.refundStore.FindActiveByOrderID(ctx, req.OrderID)
|
||||
if err == nil && existing != nil {
|
||||
return nil, errors.New(errors.CodeConflict, "该订单已存在退款申请")
|
||||
}
|
||||
|
||||
// 从订单获取 shop_id:优先使用 SellerShopID,代理商买家使用 BuyerID
|
||||
var shopID *uint
|
||||
if order.SellerShopID != nil {
|
||||
shopID = order.SellerShopID
|
||||
} else if order.BuyerType == model.BuyerTypeAgent {
|
||||
shopID = &order.BuyerID
|
||||
}
|
||||
|
||||
refund := &model.RefundRequest{
|
||||
RefundNo: generateRefundNo(),
|
||||
OrderID: req.OrderID,
|
||||
PackageUsageID: req.PackageUsageID,
|
||||
ShopID: shopID,
|
||||
ActualReceivedAmount: req.ActualReceivedAmount,
|
||||
RequestedRefundAmount: req.RequestedRefundAmount,
|
||||
RefundReason: req.RefundReason,
|
||||
Status: model.RefundStatusPending,
|
||||
}
|
||||
refund.Creator = userID
|
||||
refund.Updater = userID
|
||||
|
||||
if err := s.refundStore.Create(ctx, refund); err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "创建退款申请失败")
|
||||
}
|
||||
|
||||
return buildRefundResponse(refund), nil
|
||||
}
|
||||
|
||||
// List 分页查询退款申请列表
|
||||
func (s *Service) List(ctx context.Context, req *dto.RefundListRequest) (*dto.RefundListResponse, error) {
|
||||
opts := &store.QueryOptions{
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
OrderBy: "created_at DESC",
|
||||
}
|
||||
if opts.Page == 0 {
|
||||
opts.Page = 1
|
||||
}
|
||||
if opts.PageSize == 0 {
|
||||
opts.PageSize = constants.DefaultPageSize
|
||||
}
|
||||
|
||||
filters := &postgres.RefundListFilters{
|
||||
Status: req.Status,
|
||||
OrderID: req.OrderID,
|
||||
ShopID: req.ShopID,
|
||||
}
|
||||
|
||||
requests, total, err := s.refundStore.List(ctx, opts, filters)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询退款申请列表失败")
|
||||
}
|
||||
|
||||
items := make([]dto.RefundResponse, 0, len(requests))
|
||||
for _, r := range requests {
|
||||
items = append(items, *buildRefundResponse(r))
|
||||
}
|
||||
|
||||
return &dto.RefundListResponse{
|
||||
Items: items,
|
||||
Total: total,
|
||||
Page: opts.Page,
|
||||
Size: opts.PageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetByID 根据 ID 查询退款申请详情
|
||||
func (s *Service) GetByID(ctx context.Context, id uint) (*dto.RefundResponse, error) {
|
||||
refund, err := s.refundStore.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.CodeNotFound, "退款申请不存在")
|
||||
}
|
||||
return buildRefundResponse(refund), nil
|
||||
}
|
||||
|
||||
// Approve 审批通过退款申请
|
||||
// 条件更新 WHERE status=1,设置审批信息
|
||||
// 事务提交成功后异步执行佣金回扣和资产重置
|
||||
func (s *Service) Approve(ctx context.Context, id uint, req *dto.ApproveRefundRequest) error {
|
||||
userID := middleware.GetUserIDFromContext(ctx)
|
||||
if userID == 0 {
|
||||
return errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
}
|
||||
|
||||
refund, err := s.refundStore.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return errors.New(errors.CodeNotFound, "退款申请不存在")
|
||||
}
|
||||
if refund.Status != model.RefundStatusPending {
|
||||
return errors.New(errors.CodeInvalidStatus, "仅待审批状态可审批通过")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
approvedAmount := refund.RequestedRefundAmount
|
||||
if req.ApprovedRefundAmount != nil {
|
||||
approvedAmount = *req.ApprovedRefundAmount
|
||||
}
|
||||
|
||||
// 条件更新确保幂等性
|
||||
result := s.db.WithContext(ctx).
|
||||
Model(&model.RefundRequest{}).
|
||||
Where("id = ? AND status = ?", id, model.RefundStatusPending).
|
||||
Updates(map[string]any{
|
||||
"status": model.RefundStatusApproved,
|
||||
"processor_id": userID,
|
||||
"processed_at": now,
|
||||
"approved_refund_amount": approvedAmount,
|
||||
"remark": req.Remark,
|
||||
"updater": userID,
|
||||
"updated_at": now,
|
||||
})
|
||||
if result.Error != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, result.Error, "审批退款申请失败")
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New(errors.CodeInvalidStatus, "退款申请状态已变更,请刷新后重试")
|
||||
}
|
||||
|
||||
// 事务提交成功后,异步执行佣金回扣和资产重置(失败不影响审批结果)
|
||||
go func() {
|
||||
asyncCtx := context.Background()
|
||||
s.deductAllCommission(asyncCtx, id)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
asyncCtx := context.Background()
|
||||
s.handleAssetReset(asyncCtx, id)
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reject 审批拒绝退款申请
|
||||
// 条件更新 WHERE status=1
|
||||
func (s *Service) Reject(ctx context.Context, id uint, req *dto.RejectRefundRequest) error {
|
||||
userID := middleware.GetUserIDFromContext(ctx)
|
||||
if userID == 0 {
|
||||
return errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
result := s.db.WithContext(ctx).
|
||||
Model(&model.RefundRequest{}).
|
||||
Where("id = ? AND status = ?", id, model.RefundStatusPending).
|
||||
Updates(map[string]any{
|
||||
"status": model.RefundStatusRejected,
|
||||
"processor_id": userID,
|
||||
"processed_at": now,
|
||||
"reject_reason": req.RejectReason,
|
||||
"updater": userID,
|
||||
"updated_at": now,
|
||||
})
|
||||
if result.Error != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, result.Error, "拒绝退款申请失败")
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New(errors.CodeInvalidStatus, "退款申请状态已变更,请刷新后重试")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Return 退回退款申请
|
||||
// 条件更新 WHERE status=1,退回后可重新提交
|
||||
func (s *Service) Return(ctx context.Context, id uint, req *dto.ReturnRefundRequest) error {
|
||||
userID := middleware.GetUserIDFromContext(ctx)
|
||||
if userID == 0 {
|
||||
return errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
result := s.db.WithContext(ctx).
|
||||
Model(&model.RefundRequest{}).
|
||||
Where("id = ? AND status = ?", id, model.RefundStatusPending).
|
||||
Updates(map[string]any{
|
||||
"status": model.RefundStatusReturned,
|
||||
"processor_id": userID,
|
||||
"processed_at": now,
|
||||
"remark": req.Remark,
|
||||
"updater": userID,
|
||||
"updated_at": now,
|
||||
})
|
||||
if result.Error != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, result.Error, "退回退款申请失败")
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New(errors.CodeInvalidStatus, "退款申请状态已变更,请刷新后重试")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Resubmit 重新提交退款申请
|
||||
// 条件更新 WHERE status=4(已退回),修改部分字段后重新进入待审批状态
|
||||
func (s *Service) Resubmit(ctx context.Context, id uint, req *dto.ResubmitRefundRequest) error {
|
||||
userID := middleware.GetUserIDFromContext(ctx)
|
||||
if userID == 0 {
|
||||
return errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
updates := map[string]any{
|
||||
"status": model.RefundStatusPending,
|
||||
"updater": userID,
|
||||
"updated_at": now,
|
||||
}
|
||||
if req.ActualReceivedAmount != nil {
|
||||
updates["actual_received_amount"] = *req.ActualReceivedAmount
|
||||
}
|
||||
if req.RequestedRefundAmount != nil {
|
||||
updates["requested_refund_amount"] = *req.RequestedRefundAmount
|
||||
}
|
||||
if req.RefundReason != nil {
|
||||
updates["refund_reason"] = *req.RefundReason
|
||||
}
|
||||
|
||||
result := s.db.WithContext(ctx).
|
||||
Model(&model.RefundRequest{}).
|
||||
Where("id = ? AND status = ?", id, model.RefundStatusReturned).
|
||||
Updates(updates)
|
||||
if result.Error != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, result.Error, "重新提交退款申请失败")
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New(errors.CodeInvalidStatus, "仅已退回状态可重新提交")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// deductAllCommission 异步回扣该订单所有已入账佣金
|
||||
// 查询退款单关联订单的所有已入账佣金记录,逐条从代理佣金钱包扣减
|
||||
// 失败仅记录日志,不影响审批结果
|
||||
func (s *Service) deductAllCommission(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))
|
||||
return
|
||||
}
|
||||
|
||||
// 查询该订单所有已入账佣金记录
|
||||
var commissions []model.CommissionRecord
|
||||
if err := s.db.Where("order_id = ? AND status = ?", refund.OrderID, model.CommissionStatusReleased).Find(&commissions).Error; err != nil {
|
||||
logger.Error("佣金回扣:查询佣金记录失败", zap.Uint("refund_id", refundID), zap.Uint("order_id", refund.OrderID), zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
if len(commissions) == 0 {
|
||||
logger.Info("佣金回扣:无已入账佣金记录", zap.Uint("refund_id", refundID))
|
||||
// 无佣金需要回扣,直接标记完成
|
||||
s.db.Model(&model.RefundRequest{}).Where("id = ?", refundID).Update("commission_deducted", true)
|
||||
return
|
||||
}
|
||||
|
||||
// 对每条佣金记录执行扣减
|
||||
for _, commission := range commissions {
|
||||
if err := s.deductSingleCommission(ctx, &refund, &commission); err != nil {
|
||||
logger.Error("佣金回扣:单条佣金扣减失败",
|
||||
zap.Uint("refund_id", refundID),
|
||||
zap.Uint("commission_id", commission.ID),
|
||||
zap.Uint("shop_id", commission.ShopID),
|
||||
zap.Int64("amount", commission.Amount),
|
||||
zap.Error(err),
|
||||
)
|
||||
// 继续处理下一条,不中断
|
||||
}
|
||||
}
|
||||
|
||||
// 全部完成后标记退款单佣金已回扣
|
||||
if err := s.db.Model(&model.RefundRequest{}).Where("id = ?", refundID).Update("commission_deducted", true).Error; err != nil {
|
||||
logger.Error("佣金回扣:更新回扣标记失败", zap.Uint("refund_id", refundID), zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// deductSingleCommission 扣减单条佣金记录对应的代理钱包余额
|
||||
// 使用乐观锁扣减(允许余额为负数),并创建交易流水
|
||||
func (s *Service) deductSingleCommission(ctx context.Context, refund *model.RefundRequest, commission *model.CommissionRecord) error {
|
||||
// 获取对应代理的佣金钱包
|
||||
wallet, err := s.agentWalletStore.GetCommissionWallet(ctx, commission.ShopID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取佣金钱包失败: shop_id=%d, %w", commission.ShopID, err)
|
||||
}
|
||||
|
||||
// 乐观锁扣减余额(允许负数)
|
||||
result := s.db.Model(&model.AgentWallet{}).
|
||||
Where("id = ? AND version = ?", wallet.ID, wallet.Version).
|
||||
Updates(map[string]any{
|
||||
"balance": gorm.Expr("balance - ?", commission.Amount),
|
||||
"version": gorm.Expr("version + 1"),
|
||||
})
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("扣减钱包余额失败: %w", result.Error)
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return fmt.Errorf("乐观锁冲突: wallet_id=%d, version=%d", wallet.ID, wallet.Version)
|
||||
}
|
||||
|
||||
// 创建交易流水
|
||||
refType := constants.ReferenceTypeRefund
|
||||
refID := refund.ID
|
||||
transaction := &model.AgentWalletTransaction{
|
||||
AgentWalletID: wallet.ID,
|
||||
ShopID: commission.ShopID,
|
||||
UserID: refund.Creator,
|
||||
TransactionType: constants.AgentTransactionTypeCommissionDeduct,
|
||||
Amount: -commission.Amount,
|
||||
BalanceBefore: wallet.Balance,
|
||||
BalanceAfter: wallet.Balance - commission.Amount,
|
||||
Status: constants.TransactionStatusSuccess,
|
||||
ReferenceType: &refType,
|
||||
ReferenceID: &refID,
|
||||
Creator: refund.Creator,
|
||||
ShopIDTag: commission.ShopID,
|
||||
}
|
||||
|
||||
if err := s.agentWalletTransactionStore.CreateWithTx(ctx, s.db, transaction); err != nil {
|
||||
return fmt.Errorf("创建交易流水失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleAssetReset 异步处理资产重置
|
||||
// 包括:套餐失效、停机、世代重置(generation+1)、钱包重建、订单状态更新
|
||||
// 失败仅记录日志,不影响审批结果
|
||||
func (s *Service) handleAssetReset(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))
|
||||
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))
|
||||
return
|
||||
}
|
||||
|
||||
// 确定资产类型和 ID
|
||||
var assetType string
|
||||
var assetID uint
|
||||
switch order.OrderType {
|
||||
case model.OrderTypeSingleCard:
|
||||
if order.IotCardID == nil {
|
||||
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))
|
||||
return
|
||||
}
|
||||
assetType = "device"
|
||||
assetID = *order.DeviceID
|
||||
default:
|
||||
logger.Error("资产重置:未知订单类型", zap.String("order_type", order.OrderType))
|
||||
return
|
||||
}
|
||||
|
||||
// 1. 失效所有套餐
|
||||
if s.packageActivationService != nil {
|
||||
if err := s.packageActivationService.InvalidateAllPackagesByAsset(ctx, assetType, assetID); err != nil {
|
||||
logger.Error("资产重置:失效套餐失败", zap.String("asset_type", assetType), zap.Uint("asset_id", assetID), zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// 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))
|
||||
return
|
||||
}
|
||||
|
||||
// 4. 更新订单支付状态为已退款
|
||||
if err := s.db.Model(&model.Order{}).Where("id = ?", order.ID).Update("payment_status", model.PaymentStatusRefunded).Error; err != nil {
|
||||
logger.Error("资产重置:更新订单状态失败", zap.Uint("order_id", order.ID), zap.Error(err))
|
||||
}
|
||||
|
||||
// 5. 标记退款单资产已重置
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
// stopAsset 根据资产类型执行停机操作
|
||||
func (s *Service) stopAsset(ctx context.Context, assetType string, assetID uint) {
|
||||
logger := s.logger
|
||||
|
||||
switch assetType {
|
||||
case "iot_card":
|
||||
// 单卡停机:需要先查卡获取 iccid
|
||||
card, err := s.iotCardStore.GetByID(ctx, assetID)
|
||||
if err != nil {
|
||||
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))
|
||||
}
|
||||
}
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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": 0,
|
||||
"first_commission_paid": false,
|
||||
"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": 0,
|
||||
"first_commission_paid": false,
|
||||
"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 {
|
||||
now := time.Now()
|
||||
randomNum := rand.Intn(1000000)
|
||||
return fmt.Sprintf("RF%s%06d", now.Format("20060102150405"), randomNum)
|
||||
}
|
||||
|
||||
// buildRefundResponse 将退款 Model 转换为 DTO 响应
|
||||
func buildRefundResponse(r *model.RefundRequest) *dto.RefundResponse {
|
||||
resp := &dto.RefundResponse{
|
||||
ID: r.ID,
|
||||
RefundNo: r.RefundNo,
|
||||
OrderID: r.OrderID,
|
||||
PackageUsageID: r.PackageUsageID,
|
||||
ShopID: r.ShopID,
|
||||
ActualReceivedAmount: r.ActualReceivedAmount,
|
||||
RequestedRefundAmount: r.RequestedRefundAmount,
|
||||
ApprovedRefundAmount: r.ApprovedRefundAmount,
|
||||
RefundReason: r.RefundReason,
|
||||
Status: r.Status,
|
||||
ProcessorID: r.ProcessorID,
|
||||
RejectReason: r.RejectReason,
|
||||
Remark: r.Remark,
|
||||
CommissionDeducted: r.CommissionDeducted,
|
||||
AssetReset: r.AssetReset,
|
||||
Creator: r.Creator,
|
||||
Updater: r.Updater,
|
||||
CreatedAt: r.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
UpdatedAt: r.UpdatedAt.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
|
||||
if r.ProcessedAt != nil {
|
||||
resp.ProcessedAt = r.ProcessedAt.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
Reference in New Issue
Block a user