// Package refundapproval 收口退款申请与渠道无关审批的事务边界。 package refundapproval import ( "context" "fmt" "strings" "github.com/bytedance/sonic" "gorm.io/gorm" 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" ) // CreateCommand 描述已通过订单与金额校验的退款审批申请。 type CreateCommand struct { Refund *model.RefundRequest SubmitterAccountID uint } // CreateResult 返回原子保存后的退款申请和初始审批状态。 type CreateResult struct { Refund *model.RefundRequest SubmitterName string ApprovalStatus int } // CreationService 原子创建退款申请、通用审批实例、企微上下文和提交 Outbox。 type CreationService struct { db *gorm.DB approval approvalapp.Port } // NewCreationService 创建退款审批申请用例。 func NewCreationService(db *gorm.DB, approval approvalapp.Port) *CreationService { return &CreationService{db: db, approval: approval} } // Execute 在业务写入前校验审批渠道,并在同一事务冻结退款事实和审批事实。 func (s *CreationService) Execute(ctx context.Context, command CreateCommand) (*CreateResult, error) { if s == nil || s.db == nil || s.approval == nil { return nil, errors.New(errors.CodeServiceUnavailable, "退款审批能力未配置") } if command.Refund == nil || command.Refund.OrderID == 0 || command.SubmitterAccountID == 0 || command.Refund.Creator != command.SubmitterAccountID || strings.TrimSpace(command.Refund.RefundNo) == "" { return nil, errors.New(errors.CodeInvalidParam) } account, err := s.loadSubmitter(ctx, command.SubmitterAccountID) if err != nil { return nil, err } preparation, err := s.approval.Prepare(ctx, approvalapp.PrepareRequest{ BusinessType: constants.ApprovalBusinessTypeRefund, SubmitterAccountID: command.SubmitterAccountID, CorrelationID: command.Refund.RefundNo, }) if err != nil { return nil, err } submitterSnapshot, requestSnapshot, err := refundSnapshots(command.Refund, account) if err != nil { return nil, err } var approvalStatus int err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Exec("SELECT pg_advisory_xact_lock(?)", int64(command.Refund.OrderID)).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "锁定退款订单申请边界失败") } var activeCount int64 if err := tx.WithContext(ctx).Model(&model.RefundRequest{}). Where("order_id = ? AND status IN ?", command.Refund.OrderID, []int{model.RefundStatusPending, model.RefundStatusApproved}). Count(&activeCount).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "复核订单活跃退款申请失败") } if activeCount > 0 { return errors.New(errors.CodeConflict, "该订单已存在退款申请") } if err := tx.WithContext(ctx).Create(command.Refund).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "创建退款申请失败") } reference, err := s.approval.CreateInTx(ctx, tx, approvalapp.CreateRequest{ Preparation: preparation, BusinessType: constants.ApprovalBusinessTypeRefund, BusinessID: command.Refund.ID, SubmitterAccountID: command.SubmitterAccountID, SubmitterSnapshot: submitterSnapshot, RequestSnapshot: requestSnapshot, CorrelationID: command.Refund.RefundNo, }) if err != nil { return err } result := tx.WithContext(ctx).Model(&model.RefundRequest{}). Where("id = ? AND approval_instance_id IS NULL", command.Refund.ID). Update("approval_instance_id", reference.InstanceID) if result.Error != nil { return errors.Wrap(errors.CodeDatabaseError, result.Error, "关联退款审批实例失败") } if result.RowsAffected != 1 { return errors.New(errors.CodeConflict, "退款审批实例关联已变化") } command.Refund.ApprovalInstanceID = &reference.InstanceID approvalStatus = reference.Status return nil }) if err != nil { return nil, err } return &CreateResult{Refund: command.Refund, SubmitterName: account.Username, ApprovalStatus: approvalStatus}, nil } func (s *CreationService) loadSubmitter(ctx context.Context, accountID uint) (*model.Account, error) { var account model.Account if err := s.db.WithContext(ctx).Where("id = ? AND status = ?", accountID, constants.StatusEnabled).First(&account).Error; err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeForbidden, "退款提交人账号不可用") } return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询退款提交人失败") } return &account, nil } func refundSnapshots(refund *model.RefundRequest, account *model.Account) ([]byte, []byte, error) { submitterSnapshot, err := sonic.Marshal(map[string]any{ "account_id": account.ID, "account_name": account.Username, "user_type": account.UserType, }) if err != nil { return nil, nil, errors.Wrap(errors.CodeInternalError, err, "编码退款提交人快照失败") } requestSnapshot, err := sonic.Marshal(map[string]any{ constants.ApprovalFieldRefundNo: refund.RefundNo, constants.ApprovalFieldOrderID: refund.OrderID, constants.ApprovalFieldOrderNo: refund.OrderNo, constants.ApprovalFieldAssetIdentifier: refund.AssetIdentifier, constants.ApprovalFieldAssetType: refund.OrderType, constants.ApprovalFieldActualReceivedAmount: formatCentAmount(refund.ActualReceivedAmount), constants.ApprovalFieldRequestedRefundAmount: formatCentAmount(refund.RequestedRefundAmount), constants.ApprovalFieldRefundVoucherKey: []string(refund.RefundVoucherKey), constants.ApprovalFieldRefundReason: refund.RefundReason, constants.ApprovalFieldPackageUsageID: refund.PackageUsageID, constants.ApprovalFieldSubmitterID: account.ID, constants.ApprovalFieldSubmitterName: account.Username, }) if err != nil { return nil, nil, errors.Wrap(errors.CodeInternalError, err, "编码退款审批业务快照失败") } return submitterSnapshot, requestSnapshot, nil } func formatCentAmount(amount int64) string { return fmt.Sprintf("%d.%02d", amount/100, amount%100) }