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)