钱包订单退款时应当正确退回,充值时允许最低充值1分
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m7s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m7s
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
@@ -210,8 +211,15 @@ func (s *Service) Approve(ctx context.Context, id uint, req *dto.ApproveRefundRe
|
||||
if req.ApprovedRefundAmount != nil {
|
||||
approvedAmount = *req.ApprovedRefundAmount
|
||||
}
|
||||
order, err := s.orderStore.GetByID(ctx, refund.OrderID)
|
||||
if err != nil {
|
||||
return errors.New(errors.CodeNotFound, "订单不存在")
|
||||
}
|
||||
if err := validateApprovedRefundAmount(approvedAmount, refund.RequestedRefundAmount, order); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 事务内同步更新退款状态和订单支付状态,确保审批成功后订单立即变为已退款
|
||||
// 事务内同步更新退款状态、订单支付状态和钱包回款,避免订单已退款但资金未退回。
|
||||
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
result := tx.Model(&model.RefundRequest{}).
|
||||
Where("id = ? AND status = ?", id, model.RefundStatusPending).
|
||||
@@ -244,6 +252,10 @@ func (s *Service) Approve(ctx context.Context, id uint, req *dto.ApproveRefundRe
|
||||
return errors.New(errors.CodeInvalidStatus, "订单状态已变更,无法执行退款审批")
|
||||
}
|
||||
|
||||
if err := s.refundWalletPayment(ctx, tx, refund, order, approvedAmount, userID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
@@ -263,6 +275,244 @@ func (s *Service) Approve(ctx context.Context, id uint, req *dto.ApproveRefundRe
|
||||
return nil
|
||||
}
|
||||
|
||||
// refundWalletPayment 处理钱包支付订单的退款回款。
|
||||
func (s *Service) refundWalletPayment(ctx context.Context, tx *gorm.DB, refund *model.RefundRequest, order *model.Order, amount int64, operatorID uint) error {
|
||||
if order.PaymentMethod != model.PaymentMethodWallet {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch order.BuyerType {
|
||||
case model.BuyerTypeAgent:
|
||||
return s.refundAgentWalletPayment(ctx, tx, refund, order, amount, operatorID)
|
||||
case model.BuyerTypePersonal:
|
||||
return s.refundAssetWalletPayment(ctx, tx, refund, order, amount, operatorID)
|
||||
default:
|
||||
return errors.New(errors.CodeInvalidParam, "不支持的钱包退款买家类型")
|
||||
}
|
||||
}
|
||||
|
||||
// refundAgentWalletPayment 将代理钱包支付的订单退款退回原扣款主钱包。
|
||||
func (s *Service) refundAgentWalletPayment(ctx context.Context, tx *gorm.DB, refund *model.RefundRequest, order *model.Order, amount int64, operatorID uint) error {
|
||||
wallet, relatedShopID, subtype, err := s.resolveAgentRefundWallet(tx, order)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
balanceBefore := wallet.Balance
|
||||
if err := s.agentWalletStore.AddBalanceWithTx(ctx, tx, wallet.ID, amount); err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "退回代理预充值钱包失败")
|
||||
}
|
||||
|
||||
refType := constants.ReferenceTypeRefund
|
||||
refID := refund.ID
|
||||
remark := fmt.Sprintf("订单%s退款退回预充值钱包", order.OrderNo)
|
||||
transaction := &model.AgentWalletTransaction{
|
||||
AgentWalletID: wallet.ID,
|
||||
ShopID: wallet.ShopID,
|
||||
UserID: operatorID,
|
||||
TransactionType: constants.AgentTransactionTypeRefund,
|
||||
TransactionSubtype: subtype,
|
||||
Amount: amount,
|
||||
BalanceBefore: balanceBefore,
|
||||
BalanceAfter: balanceBefore + amount,
|
||||
Status: constants.TransactionStatusSuccess,
|
||||
ReferenceType: &refType,
|
||||
ReferenceID: &refID,
|
||||
RelatedShopID: relatedShopID,
|
||||
Remark: &remark,
|
||||
Creator: operatorID,
|
||||
ShopIDTag: wallet.ShopIDTag,
|
||||
EnterpriseIDTag: wallet.EnterpriseIDTag,
|
||||
}
|
||||
if err := s.agentWalletTransactionStore.CreateWithTx(ctx, tx, transaction); err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建代理钱包退款流水失败")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveAgentRefundWallet 优先按原扣款流水定位回款钱包,兼容历史缺失扣款流水的订单。
|
||||
func (s *Service) resolveAgentRefundWallet(tx *gorm.DB, order *model.Order) (*model.AgentWallet, *uint, *string, error) {
|
||||
var deductTx model.AgentWalletTransaction
|
||||
err := tx.Where("reference_type = ? AND reference_id = ? AND transaction_type = ?",
|
||||
constants.ReferenceTypeOrder, order.ID, constants.AgentTransactionTypeDeduct).
|
||||
Order("id ASC").
|
||||
First(&deductTx).Error
|
||||
if err == nil {
|
||||
wallet, err := lockAgentMainWalletByID(tx, deductTx.AgentWalletID)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
return wallet, deductTx.RelatedShopID, deductTx.TransactionSubtype, nil
|
||||
}
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
return nil, nil, nil, errors.Wrap(errors.CodeDatabaseError, err, "查询原代理钱包扣款流水失败")
|
||||
}
|
||||
|
||||
payerShopID, relatedShopID, err := resolveAgentWalletRefundShopID(order)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
wallet, err := lockAgentMainWalletByShopID(tx, payerShopID)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
return wallet, relatedShopID, nil, nil
|
||||
}
|
||||
|
||||
// resolveAgentWalletRefundShopID 兼容旧订单:没有扣款流水时按订单角色推导原扣款店铺。
|
||||
func resolveAgentWalletRefundShopID(order *model.Order) (uint, *uint, error) {
|
||||
if order.PurchaseRole == model.PurchaseRolePurchaseForSubordinate {
|
||||
if order.OperatorID == nil || *order.OperatorID == 0 {
|
||||
return 0, nil, errors.New(errors.CodeInternalError, "代理代购订单缺少原扣款代理")
|
||||
}
|
||||
relatedShopID := order.BuyerID
|
||||
return *order.OperatorID, &relatedShopID, nil
|
||||
}
|
||||
|
||||
if order.OperatorID != nil && *order.OperatorID > 0 && order.OperatorType == model.OperatorAccountTypeAgent {
|
||||
return *order.OperatorID, nil, nil
|
||||
}
|
||||
if order.BuyerID == 0 {
|
||||
return 0, nil, errors.New(errors.CodeInternalError, "订单买家店铺为空")
|
||||
}
|
||||
return order.BuyerID, nil, nil
|
||||
}
|
||||
|
||||
// lockAgentMainWalletByID 锁定代理主钱包,保证余额快照与后续入账一致。
|
||||
func lockAgentMainWalletByID(tx *gorm.DB, walletID uint) (*model.AgentWallet, error) {
|
||||
var wallet model.AgentWallet
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("id = ? AND wallet_type = ?", walletID, constants.AgentWalletTypeMain).
|
||||
First(&wallet).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeWalletNotFound, "代理预充值钱包不存在")
|
||||
}
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "锁定代理预充值钱包失败")
|
||||
}
|
||||
return &wallet, nil
|
||||
}
|
||||
|
||||
// lockAgentMainWalletByShopID 按店铺锁定代理主钱包。
|
||||
func lockAgentMainWalletByShopID(tx *gorm.DB, shopID uint) (*model.AgentWallet, error) {
|
||||
var wallet model.AgentWallet
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("shop_id = ? AND wallet_type = ?", shopID, constants.AgentWalletTypeMain).
|
||||
First(&wallet).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeWalletNotFound, "代理预充值钱包不存在")
|
||||
}
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "锁定代理预充值钱包失败")
|
||||
}
|
||||
return &wallet, nil
|
||||
}
|
||||
|
||||
// refundAssetWalletPayment 将个人资产钱包支付的订单退款退回原资产钱包。
|
||||
func (s *Service) refundAssetWalletPayment(ctx context.Context, tx *gorm.DB, refund *model.RefundRequest, order *model.Order, amount int64, operatorID uint) error {
|
||||
wallet, err := s.resolveAssetRefundWallet(tx, order)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
balanceBefore := wallet.Balance
|
||||
if err := s.assetWalletStore.AddBalanceWithTx(ctx, tx, wallet.ID, amount); err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "退回资产钱包失败")
|
||||
}
|
||||
|
||||
refType := constants.ReferenceTypeRefund
|
||||
refNo := refund.RefundNo
|
||||
remark := fmt.Sprintf("订单%s退款退回资产钱包", order.OrderNo)
|
||||
transaction := &model.AssetWalletTransaction{
|
||||
AssetWalletID: wallet.ID,
|
||||
ResourceType: wallet.ResourceType,
|
||||
ResourceID: wallet.ResourceID,
|
||||
UserID: operatorID,
|
||||
TransactionType: constants.AssetTransactionTypeRefund,
|
||||
Amount: amount,
|
||||
BalanceBefore: balanceBefore,
|
||||
BalanceAfter: balanceBefore + amount,
|
||||
Status: constants.TransactionStatusSuccess,
|
||||
ReferenceType: &refType,
|
||||
ReferenceNo: &refNo,
|
||||
Remark: &remark,
|
||||
Creator: operatorID,
|
||||
ShopIDTag: wallet.ShopIDTag,
|
||||
EnterpriseIDTag: wallet.EnterpriseIDTag,
|
||||
}
|
||||
if err := tx.WithContext(ctx).Create(transaction).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建资产钱包退款流水失败")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveAssetRefundWallet 优先按原扣款流水定位回款资产钱包。
|
||||
func (s *Service) resolveAssetRefundWallet(tx *gorm.DB, order *model.Order) (*model.AssetWallet, error) {
|
||||
var deductTx model.AssetWalletTransaction
|
||||
err := tx.Where("reference_type = ? AND reference_no = ? AND transaction_type = ?",
|
||||
constants.ReferenceTypeOrder, order.OrderNo, constants.AssetTransactionTypeDeduct).
|
||||
Order("id ASC").
|
||||
First(&deductTx).Error
|
||||
if err == nil {
|
||||
return lockAssetWalletByID(tx, deductTx.AssetWalletID)
|
||||
}
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询原资产钱包扣款流水失败")
|
||||
}
|
||||
|
||||
resourceType, resourceID, err := resolveOrderAssetWalletResource(order)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return lockAssetWalletByResource(tx, resourceType, resourceID)
|
||||
}
|
||||
|
||||
// resolveOrderAssetWalletResource 从订单载体推导资产钱包归属。
|
||||
func resolveOrderAssetWalletResource(order *model.Order) (string, uint, error) {
|
||||
switch order.OrderType {
|
||||
case model.OrderTypeSingleCard:
|
||||
if order.IotCardID == nil || *order.IotCardID == 0 {
|
||||
return "", 0, errors.New(errors.CodeInternalError, "单卡订单缺少卡ID")
|
||||
}
|
||||
return constants.AssetWalletResourceTypeIotCard, *order.IotCardID, nil
|
||||
case model.OrderTypeDevice:
|
||||
if order.DeviceID == nil || *order.DeviceID == 0 {
|
||||
return "", 0, errors.New(errors.CodeInternalError, "设备订单缺少设备ID")
|
||||
}
|
||||
return constants.AssetWalletResourceTypeDevice, *order.DeviceID, nil
|
||||
default:
|
||||
return "", 0, errors.New(errors.CodeInvalidParam, "未知订单类型")
|
||||
}
|
||||
}
|
||||
|
||||
// lockAssetWalletByID 锁定资产钱包。
|
||||
func lockAssetWalletByID(tx *gorm.DB, walletID uint) (*model.AssetWallet, error) {
|
||||
var wallet model.AssetWallet
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("id = ?", walletID).
|
||||
First(&wallet).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeWalletNotFound, "资产钱包不存在")
|
||||
}
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "锁定资产钱包失败")
|
||||
}
|
||||
return &wallet, nil
|
||||
}
|
||||
|
||||
// lockAssetWalletByResource 按资产归属锁定资产钱包。
|
||||
func lockAssetWalletByResource(tx *gorm.DB, resourceType string, resourceID uint) (*model.AssetWallet, error) {
|
||||
var wallet model.AssetWallet
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("resource_type = ? AND resource_id = ?", resourceType, resourceID).
|
||||
First(&wallet).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeWalletNotFound, "资产钱包不存在")
|
||||
}
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "锁定资产钱包失败")
|
||||
}
|
||||
return &wallet, nil
|
||||
}
|
||||
|
||||
// Reject 审批拒绝退款申请
|
||||
// 条件更新 WHERE status=1
|
||||
func (s *Service) Reject(ctx context.Context, id uint, req *dto.RejectRefundRequest) error {
|
||||
@@ -631,6 +881,17 @@ func validateRequestedRefundAmountByOrder(requestedRefundAmount int64, order *mo
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateApprovedRefundAmount 校验审批退款金额不能超过申请金额和订单实收金额。
|
||||
func validateApprovedRefundAmount(approvedAmount int64, requestedRefundAmount int64, order *model.Order) error {
|
||||
if approvedAmount <= 0 {
|
||||
return errors.New(errors.CodeInvalidParam, "审批退款金额必须大于0")
|
||||
}
|
||||
if approvedAmount > requestedRefundAmount {
|
||||
return errors.New(errors.CodeInvalidParam, "审批退款金额不能大于申请退款金额")
|
||||
}
|
||||
return validateRequestedRefundAmountByOrder(approvedAmount, order)
|
||||
}
|
||||
|
||||
// buildRefundResponse 将退款 Model 转换为 DTO 响应
|
||||
func buildRefundResponse(r *model.RefundRequest) *dto.RefundResponse {
|
||||
assetType := ""
|
||||
|
||||
Reference in New Issue
Block a user