七月迭代短暂完结,还有很多后端的关键东西没有弄,这是一版赶时间做的东西
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m26s

This commit is contained in:
2026-07-25 17:06:58 +08:00
parent ad9f613dd6
commit 73f5125d3d
249 changed files with 17137 additions and 877 deletions

View File

@@ -0,0 +1,145 @@
package refund
import (
"context"
"strings"
"gorm.io/gorm"
"gorm.io/gorm/clause"
approvalapp "github.com/break/junhong_cmp_fiber/internal/application/approval"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
)
// Handle 幂等处理退款的渠道无关审批终态。
func (s *Service) Handle(ctx context.Context, event approvalapp.TerminalDecisionEvent) error {
if s == nil || s.db == nil || event.BusinessType != constants.ApprovalBusinessTypeRefund ||
event.BusinessID == 0 || event.InstanceID == 0 {
return errors.New(errors.CodeInvalidParam, "退款审批终态参数无效")
}
switch event.Decision {
case constants.ApprovalDecisionApproved:
return s.applyApprovedDecision(ctx, event)
case constants.ApprovalDecisionRejected, constants.ApprovalDecisionCancelled, constants.ApprovalDecisionDeleted:
return s.applyClosedDecision(ctx, event)
case constants.ApprovalDecisionRevokedAfterApproved:
return nil
default:
return errors.New(errors.CodeInvalidParam, "不支持的退款审批终态")
}
}
func (s *Service) applyApprovedDecision(ctx context.Context, event approvalapp.TerminalDecisionEvent) error {
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var refund model.RefundRequest
if err := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", event.BusinessID).First(&refund).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
}
return errors.Wrap(errors.CodeDatabaseError, err, "锁定退款申请失败")
}
if refund.ApprovalInstanceID == nil || *refund.ApprovalInstanceID != event.InstanceID {
return errors.New(errors.CodeConflict, "退款申请关联的审批实例不一致")
}
if refund.Status != model.RefundStatusPending && refund.Status != model.RefundStatusApproved {
return errors.New(errors.CodeInvalidStatus, "退款申请状态不允许审批通过")
}
var order model.Order
if err := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", refund.OrderID).First(&order).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "锁定退款关联订单失败")
}
approvedAmount := refund.RequestedRefundAmount
if err := validateApprovedRefundAmount(approvedAmount, refund.RequestedRefundAmount, &order); err != nil {
return err
}
if refund.Status == model.RefundStatusPending {
result := tx.WithContext(ctx).Model(&model.RefundRequest{}).
Where("id = ? AND status = ?", refund.ID, model.RefundStatusPending).
Updates(map[string]any{
"status": model.RefundStatusApproved, "processed_at": event.OccurredAt,
"approved_refund_amount": approvedAmount, "remark": "企业微信审批通过",
"updated_at": event.OccurredAt,
})
if result.Error != nil {
return errors.Wrap(errors.CodeDatabaseError, result.Error, "完成退款审批申请失败")
}
if result.RowsAffected != 1 {
return errors.New(errors.CodeConflict, "退款申请状态已变化")
}
}
switch order.PaymentStatus {
case model.PaymentStatusPaid:
result := tx.WithContext(ctx).Model(&model.Order{}).
Where("id = ? AND payment_status = ?", order.ID, model.PaymentStatusPaid).
Updates(map[string]any{"payment_status": model.PaymentStatusRefunded, "updated_at": event.OccurredAt})
if result.Error != nil {
return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新订单退款状态失败")
}
if result.RowsAffected != 1 {
return errors.New(errors.CodeConflict, "订单退款状态已变化")
}
order.PaymentStatus = model.PaymentStatusRefunded
case model.PaymentStatusRefunded:
default:
return errors.New(errors.CodeInvalidStatus, "订单状态不允许完成退款")
}
return s.refundWalletPayment(ctx, tx, &refund, &order, approvedAmount, event.SubmitterAccountID)
})
if err != nil {
return err
}
return s.ensureApprovedPostProcessing(ctx, event.BusinessID)
}
func (s *Service) applyClosedDecision(ctx context.Context, event approvalapp.TerminalDecisionEvent) error {
reason := map[string]string{
constants.ApprovalDecisionRejected: "企业微信审批已拒绝",
constants.ApprovalDecisionCancelled: "企业微信审批已撤销",
constants.ApprovalDecisionDeleted: "企业微信审批已删除",
}[event.Decision]
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var refund model.RefundRequest
if err := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", event.BusinessID).First(&refund).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "锁定退款申请失败")
}
if refund.ApprovalInstanceID == nil || *refund.ApprovalInstanceID != event.InstanceID {
return errors.New(errors.CodeConflict, "退款申请关联的审批实例不一致")
}
if refund.Status == model.RefundStatusRejected {
return nil
}
if refund.Status != model.RefundStatusPending {
return errors.New(errors.CodeInvalidStatus, "退款申请状态不允许结束审批")
}
result := tx.WithContext(ctx).Model(&model.RefundRequest{}).
Where("id = ? AND status = ?", refund.ID, model.RefundStatusPending).
Updates(map[string]any{
"status": model.RefundStatusRejected, "processed_at": event.OccurredAt,
"reject_reason": strings.TrimSpace(reason), "updated_at": event.OccurredAt,
})
if result.Error != nil {
return errors.Wrap(errors.CodeDatabaseError, result.Error, "结束退款审批申请失败")
}
if result.RowsAffected != 1 {
return errors.New(errors.CodeConflict, "退款申请状态已变化")
}
return nil
})
}
func (s *Service) ensureApprovedPostProcessing(ctx context.Context, refundID uint) error {
s.deductAllCommission(ctx, refundID)
s.handleRefundAssetProcessing(ctx, refundID)
var refund model.RefundRequest
if err := s.db.WithContext(ctx).Select("commission_deducted", "asset_reset").Where("id = ?", refundID).First(&refund).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "复核退款后处理状态失败")
}
if !refund.CommissionDeducted || !refund.AssetReset {
return errors.New(errors.CodeServiceUnavailable, "退款后处理尚未全部完成,将自动重试")
}
return nil
}
var _ approvalapp.BusinessDecisionHandler = (*Service)(nil)

View File

@@ -7,17 +7,20 @@ import (
"context"
"fmt"
"math/rand"
"strings"
"time"
"go.uber.org/zap"
"gorm.io/gorm"
"gorm.io/gorm/clause"
refundapprovalapp "github.com/break/junhong_cmp_fiber/internal/application/refundapproval"
walletapp "github.com/break/junhong_cmp_fiber/internal/application/wallet"
"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/config"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/middleware"
@@ -43,6 +46,7 @@ type Service struct {
deviceStore *postgres.DeviceStore
assetWalletStore *postgres.AssetWalletStore
agentWalletRefundService *walletapp.RefundService
refundApprovalCreation *refundapprovalapp.CreationService
logger *zap.Logger
}
@@ -84,6 +88,11 @@ func (s *Service) SetAgentWalletRefundService(service *walletapp.RefundService)
s.agentWalletRefundService = service
}
// SetRefundApprovalCreationService 注入退款企微审批申请用例。
func (s *Service) SetRefundApprovalCreationService(service *refundapprovalapp.CreationService) {
s.refundApprovalCreation = service
}
// Create 创建退款申请
// 校验订单存在且已支付,检查是否存在活跃退款申请,生成退款单号并创建记录
func (s *Service) Create(ctx context.Context, req *dto.CreateRefundRequest) (*dto.RefundResponse, error) {
@@ -111,12 +120,6 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateRefundRequest) (*dt
return nil, err
}
// 检查是否存在活跃退款申请(待审批、已通过、已退回状态)
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 {
@@ -144,11 +147,22 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateRefundRequest) (*dt
refund.Creator = userID
refund.Updater = userID
if err := s.refundStore.Create(ctx, refund); err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "创建退款申请失败")
if s.refundApprovalCreation == nil {
return nil, errors.New(errors.CodeServiceUnavailable, "退款审批能力未配置")
}
result, err := s.refundApprovalCreation.Execute(ctx, refundapprovalapp.CreateCommand{
Refund: refund, SubmitterAccountID: userID,
})
if err != nil {
return nil, err
}
return buildRefundResponse(refund), nil
resp := buildRefundResponse(result.Refund)
resp.SubmitterName = result.SubmitterName
resp.ApprovalProvider = constants.IntegrationProviderWeCom
resp.ApprovalStatus = &result.ApprovalStatus
resp.ApprovalStatusName = constants.GetApprovalStatusName(result.ApprovalStatus)
return resp, nil
}
// List 分页查询退款申请列表
@@ -176,10 +190,21 @@ func (s *Service) List(ctx context.Context, req *dto.RefundListRequest) (*dto.Re
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "查询退款申请列表失败")
}
submitterNames, err := s.loadSubmitterNames(ctx, refundSubmitterIDs(requests))
if err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询退款提交人失败")
}
approvalSummaries, err := s.loadApprovalSummaries(ctx, requests)
if err != nil {
return nil, err
}
items := make([]dto.RefundResponse, 0, len(requests))
for _, r := range requests {
items = append(items, *buildRefundResponse(r))
item := buildRefundResponse(r)
item.SubmitterName = submitterNames[r.Creator]
applyApprovalSummary(item, approvalSummaries, r.ApprovalInstanceID)
items = append(items, *item)
}
return &dto.RefundListResponse{
@@ -196,13 +221,23 @@ func (s *Service) GetByID(ctx context.Context, id uint) (*dto.RefundResponse, er
if err != nil {
return nil, errors.New(errors.CodeNotFound, "退款申请不存在")
}
return buildRefundResponse(refund), nil
resp := buildRefundResponse(refund)
resp.SubmitterName = s.loadSubmitterNameBestEffort(ctx, refund.Creator)
approvalSummaries, err := s.loadApprovalSummaries(ctx, []*model.RefundRequest{refund})
if err != nil {
return nil, err
}
applyApprovalSummary(resp, approvalSummaries, refund.ApprovalInstanceID)
return resp, nil
}
// Approve 审批通过退款申请
// 条件更新 WHERE status=1设置审批信息
// 事务提交成功后异步执行佣金回扣和退款后资产处理
func (s *Service) Approve(ctx context.Context, id uint, req *dto.ApproveRefundRequest) error {
if !legacyRefundManualEnabled() {
return errors.New(errors.CodeInvalidStatus, "退款人工审批入口已停用,请查看企业微信审批状态")
}
userID := middleware.GetUserIDFromContext(ctx)
if userID == 0 {
return errors.New(errors.CodeUnauthorized, "未授权访问")
@@ -218,6 +253,9 @@ func (s *Service) Approve(ctx context.Context, id uint, req *dto.ApproveRefundRe
if refund.Status != model.RefundStatusPending {
return errors.New(errors.CodeInvalidStatus, "仅待审批状态可审批通过")
}
if refund.ApprovalInstanceID != nil {
return errors.New(errors.CodeInvalidStatus, "该退款申请由企业微信审批决定,不能人工审批")
}
now := time.Now()
approvedAmount := refund.RequestedRefundAmount
@@ -356,6 +394,20 @@ func resolveAgentWalletRefundShopID(order *model.Order) (uint, *uint, error) {
// refundAssetWalletPayment 将个人资产钱包支付的订单退款退回原资产钱包。
func (s *Service) refundAssetWalletPayment(ctx context.Context, tx *gorm.DB, refund *model.RefundRequest, order *model.Order, amount int64, operatorID uint) error {
var existing model.AssetWalletTransaction
err := tx.WithContext(ctx).
Where("reference_type = ? AND reference_no = ? AND transaction_type = ? AND status = ?",
constants.ReferenceTypeRefund, refund.RefundNo, constants.AssetTransactionTypeRefund, constants.TransactionStatusSuccess).
First(&existing).Error
if err == nil {
if existing.Amount != amount {
return errors.New(errors.CodeConflict, "退款申请已存在不一致的资产钱包回款流水")
}
return nil
}
if err != gorm.ErrRecordNotFound {
return errors.Wrap(errors.CodeDatabaseError, err, "复核资产钱包退款流水失败")
}
wallet, err := s.resolveAssetRefundWallet(tx, order)
if err != nil {
return err
@@ -463,6 +515,9 @@ func lockAssetWalletByResource(tx *gorm.DB, resourceType string, resourceID uint
// Reject 审批拒绝退款申请
// 条件更新 WHERE status=1
func (s *Service) Reject(ctx context.Context, id uint, req *dto.RejectRefundRequest) error {
if !legacyRefundManualEnabled() {
return errors.New(errors.CodeInvalidStatus, "退款人工审批入口已停用,请查看企业微信审批状态")
}
userID := middleware.GetUserIDFromContext(ctx)
if userID == 0 {
return errors.New(errors.CodeUnauthorized, "未授权访问")
@@ -474,7 +529,7 @@ func (s *Service) Reject(ctx context.Context, id uint, req *dto.RejectRefundRequ
now := time.Now()
result := s.db.WithContext(ctx).
Model(&model.RefundRequest{}).
Where("id = ? AND status = ?", id, model.RefundStatusPending).
Where("id = ? AND status = ? AND approval_instance_id IS NULL", id, model.RefundStatusPending).
Updates(map[string]any{
"status": model.RefundStatusRejected,
"processor_id": userID,
@@ -492,6 +547,11 @@ func (s *Service) Reject(ctx context.Context, id uint, req *dto.RejectRefundRequ
return nil
}
func legacyRefundManualEnabled() bool {
cfg := config.Get()
return cfg == nil || cfg.Approval.LegacyRefundManualEnabled
}
// Return 退回退款申请
// 条件更新 WHERE status=1退回后可重新提交
func (s *Service) Return(ctx context.Context, id uint, req *dto.ReturnRefundRequest) error {
@@ -506,7 +566,7 @@ func (s *Service) Return(ctx context.Context, id uint, req *dto.ReturnRefundRequ
now := time.Now()
result := s.db.WithContext(ctx).
Model(&model.RefundRequest{}).
Where("id = ? AND status = ?", id, model.RefundStatusPending).
Where("id = ? AND status = ? AND approval_instance_id IS NULL", id, model.RefundStatusPending).
Updates(map[string]any{
"status": model.RefundStatusReturned,
"processor_id": userID,
@@ -602,9 +662,8 @@ func (s *Service) Resubmit(ctx context.Context, id uint, req *dto.ResubmitRefund
return nil
}
// deductAllCommission 异步回扣该订单所有已入账佣金
// 查询退款单关联订单的所有已入账佣金记录,逐条从代理佣金钱包扣减
// 失败仅记录日志,不影响审批结果
// deductAllCommission 幂等回扣该订单所有已入账佣金
// 每条佣金在独立事务中锁定并失效;全部完成后才设置退款单完成标记。
func (s *Service) deductAllCommission(ctx context.Context, refundID uint) {
logger := s.logger
@@ -614,6 +673,9 @@ func (s *Service) deductAllCommission(ctx context.Context, refundID uint) {
logger.Error("佣金回扣:查询退款单失败", zap.Uint("refund_id", refundID), zap.Error(err))
return
}
if refund.CommissionDeducted {
return
}
// 查询该订单所有已入账佣金记录
var commissions []model.CommissionRecord
@@ -629,9 +691,11 @@ func (s *Service) deductAllCommission(ctx context.Context, refundID uint) {
return
}
allSucceeded := true
// 对每条佣金记录执行扣减
for _, commission := range commissions {
if err := s.deductSingleCommission(ctx, &refund, &commission); err != nil {
allSucceeded = false
logger.Error("佣金回扣:单条佣金扣减失败",
zap.Uint("refund_id", refundID),
zap.Uint("commission_id", commission.ID),
@@ -642,6 +706,9 @@ func (s *Service) deductAllCommission(ctx context.Context, refundID uint) {
// 继续处理下一条,不中断
}
}
if !allSucceeded {
return
}
// 全部完成后标记退款单佣金已回扣
if err := s.db.Model(&model.RefundRequest{}).Where("id = ?", refundID).Update("commission_deducted", true).Error; err != nil {
@@ -652,54 +719,72 @@ func (s *Service) deductAllCommission(ctx context.Context, refundID uint) {
// 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
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var current model.CommissionRecord
if err := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", commission.ID).First(&current).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "锁定退款佣金记录失败")
}
if current.Status == constants.CommissionStatusInvalid {
return nil
}
if current.Status != constants.CommissionStatusReleased {
return errors.New(errors.CodeInvalidStatus, "退款佣金状态不允许回扣")
}
var existingCount int64
if err := tx.WithContext(ctx).Model(&model.AgentWalletTransaction{}).
Where("reference_type = ? AND reference_id = ? AND transaction_type = ? AND status = ?",
constants.ReferenceTypeCommission, current.ID, constants.AgentTransactionTypeCommissionDeduct, constants.TransactionStatusSuccess).
Count(&existingCount).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "复核退款佣金回扣流水失败")
}
if existingCount > 0 {
if err := tx.WithContext(ctx).Model(&model.CommissionRecord{}).Where("id = ?", current.ID).
Update("status", constants.CommissionStatusInvalid).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "复核后失效退款佣金记录失败")
}
return nil
}
var wallet model.AgentWallet
if err := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).
Where("shop_id = ? AND wallet_type = ?", current.ShopID, constants.AgentWalletTypeCommission).
First(&wallet).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "锁定退款佣金钱包失败")
}
result := tx.WithContext(ctx).Model(&model.AgentWallet{}).
Where("id = ? AND version = ?", wallet.ID, wallet.Version).
Updates(map[string]any{"balance": gorm.Expr("balance - ?", current.Amount), "version": gorm.Expr("version + 1")})
if result.Error != nil {
return errors.Wrap(errors.CodeDatabaseError, result.Error, "扣减退款佣金钱包失败")
}
if result.RowsAffected != 1 {
return errors.New(errors.CodeConflict, "退款佣金钱包版本已变化")
}
refType, refID := constants.ReferenceTypeCommission, current.ID
transaction := &model.AgentWalletTransaction{
AgentWalletID: wallet.ID, ShopID: current.ShopID, UserID: refund.Creator,
TransactionType: constants.AgentTransactionTypeCommissionDeduct, Amount: -current.Amount,
BalanceBefore: wallet.Balance, BalanceAfter: wallet.Balance - current.Amount,
Status: constants.TransactionStatusSuccess, ReferenceType: &refType, ReferenceID: &refID,
Creator: refund.Creator, ShopIDTag: current.ShopID,
}
if err := tx.WithContext(ctx).Create(transaction).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建退款佣金回扣流水失败")
}
updated := tx.WithContext(ctx).Model(&model.CommissionRecord{}).
Where("id = ? AND status = ?", current.ID, constants.CommissionStatusReleased).
Update("status", constants.CommissionStatusInvalid)
if updated.Error != nil {
return errors.Wrap(errors.CodeDatabaseError, updated.Error, "失效退款佣金记录失败")
}
if updated.RowsAffected != 1 {
return errors.New(errors.CodeConflict, "退款佣金状态已变化")
}
return nil
})
}
// handleRefundAssetProcessing 异步处理退款后的资产状态
// 包括退款套餐精准失效、尝试接续待生效主套餐必要时停机
// 失败仅记录日志,不影响审批结果
// handleRefundAssetProcessing 幂等处理退款后的资产状态
// 包括退款套餐精准失效、尝试接续待生效主套餐必要时停机;全部完成后才设置完成标记。
func (s *Service) handleRefundAssetProcessing(ctx context.Context, refundID uint) {
logger := s.logger
@@ -709,6 +794,9 @@ func (s *Service) handleRefundAssetProcessing(ctx context.Context, refundID uint
logger.Error("退款资产处理:查询退款单失败", zap.Uint("refund_id", refundID), zap.Error(err))
return
}
if refund.AssetReset {
return
}
// 查询关联订单
var order model.Order
@@ -784,7 +872,9 @@ func (s *Service) handleRefundAssetProcessing(ctx context.Context, refundID uint
}
if !hasActiveMain {
// 3. 无可用主套餐时才停机;退款不再重置世代或重建钱包。
s.stopAsset(ctx, assetType, assetID)
if !s.stopAsset(ctx, assetType, assetID) {
return
}
}
// 4. 标记退款后资产处理已完成
@@ -794,7 +884,7 @@ func (s *Service) handleRefundAssetProcessing(ctx context.Context, refundID uint
}
// stopAsset 根据资产类型执行停机操作
func (s *Service) stopAsset(ctx context.Context, assetType string, assetID uint) {
func (s *Service) stopAsset(ctx context.Context, assetType string, assetID uint) bool {
logger := s.logger
switch assetType {
@@ -803,21 +893,38 @@ func (s *Service) stopAsset(ctx context.Context, assetType string, assetID uint)
card, err := s.iotCardStore.GetByID(ctx, assetID)
if err != nil {
logger.Error("退款资产处理:查询卡信息失败", zap.Uint("card_id", assetID), zap.Error(err))
return
return false
}
if s.stopResumeService != nil {
if err := s.stopResumeService.ManualStopCard(ctx, card.ICCID); err != nil {
logger.Error("退款资产处理:单卡停机失败", zap.String("iccid", card.ICCID), zap.Error(err))
}
if s.stopResumeService == nil {
logger.Error("退款资产处理:单卡停机服务未注入", zap.Uint("card_id", assetID))
return false
}
if err := s.stopResumeService.ManualStopCard(ctx, card.ICCID); err != nil {
logger.Error("退款资产处理:单卡停机失败", zap.String("iccid", card.ICCID), zap.Error(err))
return false
}
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))
if s.stopResumeService == nil {
logger.Error("退款资产处理:设备停机服务未注入", zap.Uint("device_id", assetID))
return false
}
var cards []model.IotCard
if err := s.db.WithContext(ctx).Model(&model.IotCard{}).
Joins("JOIN tb_device_sim_binding binding ON binding.iot_card_id = tb_iot_card.id AND binding.deleted_at IS NULL").
Where("binding.device_id = ? AND binding.bind_status = ?", assetID, constants.BindStatusBound).
Find(&cards).Error; err != nil {
logger.Error("退款资产处理:查询设备绑定卡失败", zap.Uint("device_id", assetID), zap.Error(err))
return false
}
for _, card := range cards {
if err := s.stopResumeService.ManualStopCard(ctx, card.ICCID); err != nil {
logger.Error("退款资产处理:设备绑定卡停机失败",
zap.Uint("device_id", assetID), zap.Uint("card_id", card.ID), zap.Error(err))
return false
}
}
}
return true
}
// generateRefundNo 生成退款单号
@@ -841,7 +948,7 @@ func validateRequestedRefundAmountByOrder(requestedRefundAmount int64, order *mo
// validateApprovedRefundAmount 校验审批退款金额不能超过申请金额和订单实收金额。
func validateApprovedRefundAmount(approvedAmount int64, requestedRefundAmount int64, order *model.Order) error {
if approvedAmount < 0 {
if approvedAmount <= 0 {
return errors.New(errors.CodeInvalidParam, "审批退款金额必须大于0")
}
if approvedAmount > requestedRefundAmount {
@@ -857,7 +964,15 @@ func normalizeRefundVoucherKey(keys []string) (model.StringJSONBArray, error) {
if len(keys) > 5 {
return nil, errors.New(errors.CodeInvalidParam, "退款凭证最多上传5个")
}
return model.StringJSONBArray(keys), nil
normalized := make(model.StringJSONBArray, 0, len(keys))
for _, key := range keys {
key = strings.TrimSpace(key)
if key == "" {
return nil, errors.New(errors.CodeInvalidParam, "退款凭证对象存储 Key 不能为空")
}
normalized = append(normalized, key)
}
return normalized, nil
}
// buildRefundResponse 将退款 Model 转换为 DTO 响应
@@ -893,6 +1008,8 @@ func buildRefundResponse(r *model.RefundRequest) *dto.RefundResponse {
Remark: r.Remark,
CommissionDeducted: r.CommissionDeducted,
AssetReset: r.AssetReset,
SubmitterID: r.Creator,
ApprovalInstanceID: r.ApprovalInstanceID,
Creator: r.Creator,
Updater: r.Updater,
CreatedAt: r.CreatedAt.Format("2006-01-02 15:04:05"),
@@ -906,6 +1023,84 @@ func buildRefundResponse(r *model.RefundRequest) *dto.RefundResponse {
return resp
}
type approvalSummary struct {
Provider string
Status int
}
func (s *Service) loadApprovalSummaries(ctx context.Context, refunds []*model.RefundRequest) (map[uint]approvalSummary, error) {
ids := make([]uint, 0, len(refunds))
seen := make(map[uint]struct{}, len(refunds))
for _, refund := range refunds {
if refund == nil || refund.ApprovalInstanceID == nil || *refund.ApprovalInstanceID == 0 {
continue
}
id := *refund.ApprovalInstanceID
if _, exists := seen[id]; exists {
continue
}
seen[id] = struct{}{}
ids = append(ids, id)
}
summaries := make(map[uint]approvalSummary, len(ids))
if len(ids) == 0 {
return summaries, nil
}
var instances []model.ApprovalInstance
if err := s.db.WithContext(ctx).Select("id", "provider", "status").Where("id IN ?", ids).Find(&instances).Error; err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "批量查询退款审批状态失败")
}
for _, instance := range instances {
summaries[instance.ID] = approvalSummary{Provider: instance.Provider, Status: instance.Status}
}
return summaries, nil
}
func applyApprovalSummary(response *dto.RefundResponse, summaries map[uint]approvalSummary, instanceID *uint) {
if response == nil || instanceID == nil {
return
}
summary, exists := summaries[*instanceID]
if !exists {
return
}
status := summary.Status
response.ApprovalProvider = summary.Provider
response.ApprovalStatus = &status
response.ApprovalStatusName = constants.GetApprovalStatusName(status)
}
func refundSubmitterIDs(requests []*model.RefundRequest) []uint {
ids := make([]uint, 0, len(requests))
for _, request := range requests {
if request != nil && request.Creator > 0 {
ids = append(ids, request.Creator)
}
}
return ids
}
func (s *Service) loadSubmitterNames(ctx context.Context, ids []uint) (map[uint]string, error) {
accounts, err := postgres.NewAccountStore(s.db, nil).GetDisplayAccountsByIDs(ctx, ids)
if err != nil {
return nil, err
}
names := make(map[uint]string, len(accounts))
for _, account := range accounts {
names[account.ID] = account.Username
}
return names, nil
}
func (s *Service) loadSubmitterNameBestEffort(ctx context.Context, id uint) string {
names, err := s.loadSubmitterNames(ctx, []uint{id})
if err != nil {
s.logger.Warn("查询退款提交人失败", zap.Uint("submitter_id", id), zap.Error(err))
return ""
}
return names[id]
}
// buildRefundWalletTransactionAssetSnapshot 从订单中生成退款流水的资产快照。
func buildRefundWalletTransactionAssetSnapshot(order *model.Order) (string, uint, string) {
if order == nil {