暂存一下,防止丢失

This commit is contained in:
2026-07-24 16:07:18 +08:00
parent 5d6e23f1a5
commit a18ed8bc8d
180 changed files with 13597 additions and 1986 deletions

View File

@@ -0,0 +1,297 @@
package wallet
import (
"context"
"strconv"
"time"
domainwallet "github.com/break/junhong_cmp_fiber/internal/domain/wallet"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// ReservationCommand 描述一次具有稳定业务引用的代理主钱包预占操作。
type ReservationCommand struct {
ShopID uint
Amount int64
ReferenceType string
ReferenceID uint
UserID uint
Creator uint
Subtype string
RelatedShopID *uint
AssetType string
AssetID uint
AssetIdentifier string
Remark string
RequestID string
CorrelationID string
}
// ReservationEvent 是代理主钱包预占状态变化事件。
type ReservationEvent struct {
EventID string `json:"event_id"`
ReservationID uint `json:"reservation_id"`
WalletID uint `json:"wallet_id"`
ShopID uint `json:"shop_id"`
Amount int64 `json:"amount"`
Status int `json:"status"`
ReferenceType string `json:"reference_type"`
ReferenceID uint `json:"reference_id"`
OccurredAt time.Time `json:"occurred_at"`
RequestID string `json:"request_id,omitempty"`
CorrelationID string `json:"correlation_id,omitempty"`
}
// ReservationEventWriter 在业务事务内追加预占状态事件。
type ReservationEventWriter interface {
Append(ctx context.Context, tx *gorm.DB, event ReservationEvent) error
}
// ReservationService 统一代理主钱包冻结、释放与完成扣除。
type ReservationService struct {
reservationEvents ReservationEventWriter
debitEvents DebitEventWriter
now func() time.Time
}
// NewReservationService 创建代理主钱包预占服务。
func NewReservationService(reservationEvents ReservationEventWriter, debitEvents DebitEventWriter, now func() time.Time) *ReservationService {
if now == nil {
now = time.Now
}
return &ReservationService{reservationEvents: reservationEvents, debitEvents: debitEvents, now: now}
}
// FreezeInTx 在调用方事务内冻结资金并创建唯一预占事实。
func (s *ReservationService) FreezeInTx(ctx context.Context, tx *gorm.DB, command ReservationCommand) error {
if err := s.validateFreeze(tx, command); err != nil {
return err
}
wallet, aggregate, err := lockMainWallet(ctx, tx, command.ShopID)
if err != nil {
return err
}
existing, err := findReservation(ctx, tx, command.ReferenceType, command.ReferenceID, false)
if err != nil {
return err
}
if existing != nil {
if existing.AgentWalletID == wallet.ID && existing.Amount == command.Amount && existing.Status == constants.AgentWalletReservationStatusFrozen {
return nil
}
return errors.New(errors.CodeConflict, "业务引用已存在不一致的钱包预占")
}
if err := aggregate.Freeze(command.Amount); err != nil {
return err
}
now := s.now().UTC()
if err := updateWalletFunds(ctx, tx, wallet, aggregate, now); err != nil {
return err
}
reservation := &model.AgentWalletReservation{
AgentWalletID: wallet.ID, ShopID: wallet.ShopID, Amount: command.Amount,
Status: constants.AgentWalletReservationStatusFrozen,
ReferenceType: command.ReferenceType, ReferenceID: command.ReferenceID, Creator: command.Creator,
ShopIDTag: wallet.ShopIDTag, EnterpriseIDTag: wallet.EnterpriseIDTag,
}
if err := tx.WithContext(ctx).Create(reservation).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建代理主钱包预占事实失败")
}
return s.appendReservationEvent(ctx, tx, reservation, now, command)
}
// ReleaseInTx 幂等释放处于冻结状态的资金预占。
func (s *ReservationService) ReleaseInTx(ctx context.Context, tx *gorm.DB, command ReservationCommand) error {
return s.finish(ctx, tx, command, constants.AgentWalletReservationStatusReleased)
}
// CompleteInTx 完成冻结资金扣除并创建真实扣款流水与扣款事件。
func (s *ReservationService) CompleteInTx(ctx context.Context, tx *gorm.DB, command ReservationCommand) error {
return s.finish(ctx, tx, command, constants.AgentWalletReservationStatusCompleted)
}
func (s *ReservationService) finish(ctx context.Context, tx *gorm.DB, command ReservationCommand, targetStatus int) error {
if err := s.validateFinish(tx, command); err != nil {
return err
}
reservation, err := findReservation(ctx, tx, command.ReferenceType, command.ReferenceID, true)
if err != nil {
return err
}
if reservation == nil {
return errors.New(errors.CodeNotFound, "代理主钱包预占事实不存在")
}
if (command.ShopID > 0 && reservation.ShopID != command.ShopID) || (command.Amount > 0 && reservation.Amount != command.Amount) {
return errors.New(errors.CodeConflict, "钱包预占事实与请求不一致")
}
command.ShopID = reservation.ShopID
command.Amount = reservation.Amount
if reservation.Status == targetStatus {
return nil
}
if reservation.Status != constants.AgentWalletReservationStatusFrozen {
return errors.New(errors.CodeInvalidStatus, "钱包预占已经进入其他终态")
}
wallet, aggregate, err := lockMainWallet(ctx, tx, command.ShopID)
if err != nil {
return err
}
if wallet.ID != reservation.AgentWalletID {
return errors.New(errors.CodeConflict, "钱包预占归属不一致")
}
if targetStatus == constants.AgentWalletReservationStatusReleased {
err = aggregate.Release(command.Amount)
} else {
err = aggregate.CompleteReserved(command.Amount)
}
if err != nil {
return err
}
now := s.now().UTC()
if err := updateWalletFunds(ctx, tx, wallet, aggregate, now); err != nil {
return err
}
result := tx.WithContext(ctx).Model(&model.AgentWalletReservation{}).
Where("id = ? AND status = ?", reservation.ID, constants.AgentWalletReservationStatusFrozen).
Updates(map[string]any{"status": targetStatus, "completed_at": now, "updated_at": now})
if result.Error != nil {
return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新代理主钱包预占状态失败")
}
if result.RowsAffected != 1 {
return errors.New(errors.CodeConflict, "钱包预占状态已变化")
}
reservation.Status = targetStatus
reservation.CompletedAt = &now
if targetStatus == constants.AgentWalletReservationStatusCompleted {
if err := s.createCompletedDebit(ctx, tx, wallet, aggregate, command, now); err != nil {
return err
}
}
return s.appendReservationEvent(ctx, tx, reservation, now, command)
}
func (s *ReservationService) validateConfigured(tx *gorm.DB) error {
if s == nil || s.reservationEvents == nil || s.debitEvents == nil || tx == nil {
return errors.New(errors.CodeInternalError, "代理主钱包预占能力未完整配置")
}
return nil
}
func (s *ReservationService) validateFreeze(tx *gorm.DB, command ReservationCommand) error {
if err := s.validateConfigured(tx); err != nil {
return err
}
if command.ShopID == 0 || command.ReferenceType != constants.ReferenceTypeOrder || command.ReferenceID == 0 || command.Amount <= 0 {
return errors.New(errors.CodeInvalidParam, "代理主钱包预占参数无效")
}
return nil
}
func (s *ReservationService) validateFinish(tx *gorm.DB, command ReservationCommand) error {
if err := s.validateConfigured(tx); err != nil {
return err
}
if command.ReferenceType != constants.ReferenceTypeOrder || command.ReferenceID == 0 || command.Amount < 0 {
return errors.New(errors.CodeInvalidParam, "代理主钱包预占参数无效")
}
return nil
}
func lockMainWallet(ctx context.Context, tx *gorm.DB, shopID uint) (model.AgentWallet, *domainwallet.AgentWallet, error) {
var wallet model.AgentWallet
if err := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).
Where("shop_id = ? AND wallet_type = ?", shopID, constants.AgentWalletTypeMain).First(&wallet).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return wallet, nil, errors.New(errors.CodeWalletNotFound, "代理主钱包不存在")
}
return wallet, nil, errors.Wrap(errors.CodeDatabaseError, err, "锁定代理主钱包失败")
}
aggregate := &domainwallet.AgentWallet{
ID: wallet.ID, ShopID: wallet.ShopID, WalletType: wallet.WalletType,
Balance: wallet.Balance, FrozenBalance: wallet.FrozenBalance,
CreditEnabled: wallet.CreditEnabled, CreditLimit: wallet.CreditLimit,
Status: wallet.Status, Version: wallet.Version,
}
return wallet, aggregate, nil
}
func findReservation(ctx context.Context, tx *gorm.DB, referenceType string, referenceID uint, lock bool) (*model.AgentWalletReservation, error) {
var reservation model.AgentWalletReservation
query := tx.WithContext(ctx)
if lock {
query = query.Clauses(clause.Locking{Strength: "UPDATE"})
}
err := query.Where("reference_type = ? AND reference_id = ?", referenceType, referenceID).First(&reservation).Error
if err == nil {
return &reservation, nil
}
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询代理主钱包预占事实失败")
}
func updateWalletFunds(ctx context.Context, tx *gorm.DB, stored model.AgentWallet, aggregate *domainwallet.AgentWallet, now time.Time) error {
result := tx.WithContext(ctx).Model(&model.AgentWallet{}).
Where("id = ? AND wallet_type = ? AND status = ? AND version = ?", stored.ID, constants.AgentWalletTypeMain, constants.AgentWalletStatusNormal, stored.Version).
Updates(map[string]any{
"balance": aggregate.Balance, "frozen_balance": aggregate.FrozenBalance,
"version": gorm.Expr("version + 1"), "updated_at": now,
})
if result.Error != nil {
return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新代理主钱包预占资金失败")
}
if result.RowsAffected != 1 {
return errors.New(errors.CodeConflict, "钱包版本已变化,请重试")
}
return nil
}
func (s *ReservationService) createCompletedDebit(ctx context.Context, tx *gorm.DB, stored model.AgentWallet, aggregate *domainwallet.AgentWallet, command ReservationCommand, now time.Time) error {
referenceType := command.ReferenceType
transaction := &model.AgentWalletTransaction{
AgentWalletID: stored.ID, ShopID: stored.ShopID, UserID: command.UserID,
TransactionType: constants.AgentTransactionTypeDeduct, Amount: -command.Amount,
BalanceBefore: stored.Balance, BalanceAfter: aggregate.Balance,
Status: constants.TransactionStatusSuccess, ReferenceType: &referenceType, ReferenceID: &command.ReferenceID,
RelatedShopID: command.RelatedShopID, AssetType: command.AssetType, AssetID: command.AssetID,
AssetIdentifier: command.AssetIdentifier, Remark: &command.Remark, Creator: command.Creator,
ShopIDTag: stored.ShopIDTag, EnterpriseIDTag: stored.EnterpriseIDTag,
}
if command.Subtype != "" {
transaction.TransactionSubtype = &command.Subtype
}
if err := tx.WithContext(ctx).Create(transaction).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建冻结资金扣款流水失败")
}
event := DebitedEvent{
EventID: "agent-wallet:order:" + strconv.FormatUint(uint64(command.ReferenceID), 10) + ":debited",
WalletID: stored.ID, ShopID: stored.ShopID, Amount: command.Amount,
BalanceBefore: stored.Balance, BalanceAfter: aggregate.Balance, Version: stored.Version + 1,
ReferenceType: command.ReferenceType, ReferenceID: command.ReferenceID,
TransactionType: constants.AgentTransactionTypeDeduct, OccurredAt: now,
RequestID: command.RequestID, CorrelationID: command.CorrelationID,
}
if err := s.debitEvents.Append(ctx, tx, event); err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "写入冻结资金扣款事件失败")
}
return nil
}
func (s *ReservationService) appendReservationEvent(ctx context.Context, tx *gorm.DB, reservation *model.AgentWalletReservation, now time.Time, command ReservationCommand) error {
event := ReservationEvent{
EventID: "agent-wallet-reservation:" + strconv.FormatUint(uint64(reservation.ID), 10) + ":" + strconv.Itoa(reservation.Status),
ReservationID: reservation.ID, WalletID: reservation.AgentWalletID, ShopID: reservation.ShopID,
Amount: reservation.Amount, Status: reservation.Status,
ReferenceType: reservation.ReferenceType, ReferenceID: reservation.ReferenceID,
OccurredAt: now, RequestID: command.RequestID, CorrelationID: command.CorrelationID,
}
if err := s.reservationEvents.Append(ctx, tx, event); err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "写入代理主钱包预占事件失败")
}
return nil
}