七月迭代短暂完结,还有很多后端的关键东西没有弄,这是一版赶时间做的东西
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m26s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m26s
This commit is contained in:
145
internal/service/refund/approval_decision.go
Normal file
145
internal/service/refund/approval_decision.go
Normal 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)
|
||||
@@ -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(¤t).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 {
|
||||
|
||||
Reference in New Issue
Block a user