Files
junhong_cmp_fiber/internal/application/refundapproval/recovery.go
break b063617153
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m35s
完善退款审批材料与恢复流程
2026-09-20 17:59:52 +08:00

145 lines
6.6 KiB
Go

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
}