Files
junhong_cmp_fiber/internal/service/refund/approval_decision.go
break e134552ec5
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m35s
更新
2026-08-13 12:31:47 +08:00

195 lines
8.6 KiB
Go

package refund
import (
"context"
"strconv"
"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/infrastructure/commissiondelivery"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/messaging/outbox"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/auditcontext"
"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 {
ctx = auditcontext.With(ctx, auditcontext.Context{CorrelationID: event.CorrelationID, ParentEventID: event.EventID})
var refund model.RefundRequest
var order model.Order
changed := false
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
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, "退款申请状态不允许审批通过")
}
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, "锁定退款关联订单失败")
}
beforeRefund := refundAuditState(&refund)
beforeOrder := map[string]any{"payment_status": order.PaymentStatus}
approvedAmount := refund.RequestedRefundAmount
if err := validateApprovedRefundAmount(approvedAmount, refund.RequestedRefundAmount, &order); err != nil {
return err
}
if refund.Status == model.RefundStatusPending {
changed = true
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:
changed = true
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, "订单状态不允许完成退款")
}
if err := s.refundWalletPayment(ctx, tx, &refund, &order, approvedAmount, event.SubmitterAccountID); err != nil {
return err
}
if err := s.appendCompletedNotification(ctx, tx, &refund); err != nil {
return err
}
if err := commissiondelivery.AppendRefundCommissionDeduct(ctx, tx, outbox.NewRepository(), refund.ID, refund.OrderID); err != nil {
return err
}
if err := commissiondelivery.AppendRefundAssetProcess(ctx, tx, outbox.NewRepository(), refund.ID, refund.OrderID); err != nil {
return err
}
if !changed {
return nil
}
return s.appendRefundAudit(ctx, tx, refund.ID, constants.AuditActionRefundApproved, "通过退款审批",
"refund:"+strconv.FormatUint(uint64(refund.ID), 10)+":approved", beforeRefund, beforeOrder, "退款已通过")
})
if err != nil {
s.recordRefundFailure(ctx, constants.AuditActionRefundApproved, "通过退款审批失败", &refund, &order, err)
return err
}
return nil
}
func (s *Service) applyClosedDecision(ctx context.Context, event approvalapp.TerminalDecisionEvent) error {
ctx = auditcontext.With(ctx, auditcontext.Context{CorrelationID: event.CorrelationID, ParentEventID: event.EventID})
reason := map[string]string{
constants.ApprovalDecisionRejected: "企业微信审批已拒绝",
constants.ApprovalDecisionCancelled: "企业微信审批已撤销",
constants.ApprovalDecisionDeleted: "企业微信审批已删除",
}[event.Decision]
var refund model.RefundRequest
var order model.Order
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
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, "退款申请状态不允许结束审批")
}
if err := tx.WithContext(ctx).First(&order, refund.OrderID).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "查询退款关联订单失败")
}
beforeRefund := refundAuditState(&refund)
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 s.appendRefundAudit(ctx, tx, refund.ID, constants.AuditActionRefundRejected, "拒绝退款审批",
"refund:"+strconv.FormatUint(uint64(refund.ID), 10)+":rejected", beforeRefund, nil, "退款已拒绝")
})
if err != nil {
s.recordRefundFailure(ctx, constants.AuditActionRefundRejected, "拒绝退款审批失败", &refund, &order, err)
}
return err
}
func (s *Service) ProcessCommissionDeduction(ctx context.Context, refundID uint) error {
s.deductAllCommission(ctx, refundID)
var refund model.RefundRequest
if err := s.db.WithContext(ctx).Select("commission_deducted").First(&refund, refundID).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "复核退款佣金回扣状态失败")
}
if !refund.CommissionDeducted {
return errors.New(errors.CodeServiceUnavailable, "退款佣金回扣尚未完成")
}
return nil
}
func (s *Service) ProcessAssetPostProcessing(ctx context.Context, refundID uint) error {
s.handleRefundAssetProcessing(ctx, refundID)
var refund model.RefundRequest
if err := s.db.WithContext(ctx).Select("asset_reset").First(&refund, refundID).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "复核退款资产后处理状态失败")
}
if !refund.AssetReset {
return errors.New(errors.CodeServiceUnavailable, "退款资产后处理尚未完成")
}
return nil
}
var _ approvalapp.BusinessDecisionHandler = (*Service)(nil)