七月迭代短暂完结,还有很多后端的关键东西没有弄,这是一版赶时间做的东西
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m26s

This commit is contained in:
2026-07-25 17:06:58 +08:00
parent ad9f613dd6
commit 73f5125d3d
249 changed files with 17137 additions and 877 deletions

View File

@@ -0,0 +1,151 @@
// 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)
}