All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m26s
152 lines
6.2 KiB
Go
152 lines
6.2 KiB
Go
// 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)
|
|
}
|