package refundapproval import ( "context" 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" "github.com/break/junhong_cmp_fiber/pkg/middleware" "github.com/bytedance/sonic" "gorm.io/datatypes" "gorm.io/gorm" "gorm.io/gorm/clause" ) type RecoveryAuditWriter interface { WriteSnapshotRecovery(ctx context.Context, tx *gorm.DB, instance *model.ApprovalInstance, fields []string, actorID uint) error WriteRecoveryRequest(ctx context.Context, tx *gorm.DB, instance *model.ApprovalInstance, actorID uint, branch string) error } type RecoveryService struct { db *gorm.DB recovery approvalapp.RecoveryPort audit RecoveryAuditWriter } func NewRecoveryService(db *gorm.DB, recovery approvalapp.RecoveryPort, audit RecoveryAuditWriter) *RecoveryService { return &RecoveryService{db: db, recovery: recovery, audit: audit} } func (s *RecoveryService) Execute(ctx context.Context, refundID uint) error { actorID := middleware.GetUserIDFromContext(ctx) if actorID == 0 { return errors.New(errors.CodeUnauthorized, "未认证的恢复操作") } if s == nil || s.db == nil || s.recovery == nil || refundID == 0 { return errors.New(errors.CodeServiceUnavailable, "退款审批恢复能力未配置") } 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"}).First(&refund, refundID).Error; err != nil { return errors.New(errors.CodeNotFound, "退款申请不存在") } if refund.Status != model.RefundStatusPending || refund.LatestAttemptID == 0 || refund.LatestApprovalInstanceID == 0 || refund.ChannelRefundStatus == constants.RefundChannelStatusProcessing || refund.Status == model.RefundStatusChannelFailed { return errors.New(errors.CodeConflict, "当前退款状态不允许恢复审批") } var attempt model.RefundRequestAttempt if err := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ? AND refund_id = ?", refund.LatestAttemptID, refund.ID).First(&attempt).Error; err != nil { return errors.New(errors.CodeConflict, "退款审批尝试关联不一致") } if attempt.ApprovalInstanceID == nil || *attempt.ApprovalInstanceID != refund.LatestApprovalInstanceID { return errors.New(errors.CodeConflict, "退款审批实例关联不一致") } var instance model.ApprovalInstance if err := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ? AND business_type = ? AND business_id = ?", refund.LatestApprovalInstanceID, constants.ApprovalBusinessTypeRefund, attempt.ID).First(&instance).Error; err != nil { return errors.New(errors.CodeConflict, "退款审批实例业务关联不一致") } if instance.Status != constants.ApprovalStatusSubmitting && instance.Status != constants.ApprovalStatusSubmissionFailed && instance.Status != constants.ApprovalStatusSubmissionUnknown && instance.Status != constants.ApprovalStatusPending { return errors.New(errors.CodeConflict, "审批终态不允许恢复") } fields, err := fillMissingSnapshot(&instance, &attempt) if err != nil { return err } if len(fields) > 0 { if err := tx.Model(&model.ApprovalInstance{}).Where("id = ?", instance.ID).Update("request_snapshot", datatypes.JSON(instance.RequestSnapshot)).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "补齐退款审批快照失败") } if s.audit != nil { if err := s.audit.WriteSnapshotRecovery(ctx, tx, &instance, fields, actorID); err != nil { return err } } } var context model.WeComApprovalContext if err := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where("approval_instance_id = ? AND business_type = ?", instance.ID, constants.ApprovalBusinessTypeRefund).First(&context).Error; err != nil { return errors.New(errors.CodeConflict, "企业微信审批上下文不存在或业务类型不一致") } branch := "active" if context.SPNo != "" { branch = "sp_no" if s.audit != nil { if err := s.audit.WriteRecoveryRequest(ctx, tx, &instance, actorID, branch); err != nil { return err } } return s.recovery.EnqueueSubmittedSync(ctx, tx, instance.ID) } if context.SubmissionStatus == constants.WeComSubmissionStatusUnknown || instance.Status == constants.ApprovalStatusSubmissionUnknown { branch = "unknown" if s.audit != nil { if err := s.audit.WriteRecoveryRequest(ctx, tx, &instance, actorID, branch); err != nil { return err } } return s.recovery.EnqueueUnknownConfirm(ctx, tx, instance.ID) } if instance.Status == constants.ApprovalStatusSubmitting || instance.Status == constants.ApprovalStatusPending || context.SubmissionStatus == constants.WeComSubmissionStatusReady || context.SubmissionStatus == constants.WeComSubmissionStatusSending { if s.audit != nil { if err := s.audit.WriteRecoveryRequest(ctx, tx, &instance, actorID, branch); err != nil { return err } } return nil } if instance.Status != constants.ApprovalStatusSubmissionFailed || context.SubmissionStatus != constants.WeComSubmissionStatusFailed { return errors.New(errors.CodeConflict, "当前审批提交状态不允许恢复") } branch = "replay" if s.audit != nil { if err := s.audit.WriteRecoveryRequest(ctx, tx, &instance, actorID, branch); err != nil { return err } } _, err = s.recovery.RecoverSubmissionEvent(ctx, tx, instance.ID) return err }) } func fillMissingSnapshot(instance *model.ApprovalInstance, attempt *model.RefundRequestAttempt) ([]string, error) { var snapshot map[string]any if err := sonic.Unmarshal(instance.RequestSnapshot, &snapshot); err != nil { return nil, errors.Wrap(errors.CodeInvalidStatus, err, "退款审批快照无效") } if snapshot == nil { snapshot = map[string]any{} } values := map[string]any{constants.ApprovalFieldRefundMethodCode: attempt.Method, constants.ApprovalFieldRefundMethod: constants.RefundMethodName(attempt.Method), constants.ApprovalFieldCustomerAccountInfo: attempt.CustomerAccountInfo, constants.ApprovalFieldCustomerVoucherKey: []string(attempt.CustomerVoucherKeys)} fields := make([]string, 0, len(values)) for key, value := range values { if _, ok := snapshot[key]; ok { continue } snapshot[key] = value fields = append(fields, key) } if len(fields) > 0 { encoded, err := sonic.Marshal(snapshot) if err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "编码退款审批快照失败") } instance.RequestSnapshot = encoded } return fields, nil }