// Package wallet 提供代理主钱包复杂写用例。 package wallet import ( "context" stderrors "errors" "strconv" domainwallet "github.com/break/junhong_cmp_fiber/internal/domain/wallet" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/internal/model/dto" "github.com/break/junhong_cmp_fiber/pkg/auditcontext" "github.com/break/junhong_cmp_fiber/pkg/auditfailure" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" "gorm.io/gorm" ) // CreditChangeAudit 描述一次代理主钱包实际信用额度变化。 type CreditChangeAudit struct { Wallet *model.AgentWallet BeforeData map[string]any AfterData map[string]any Result string ErrorCode string ErrorSummary string } // CreditChangeAuditWriter 在信用额度业务事务内追加统一 Audit Event。 type CreditChangeAuditWriter interface { WriteAgentWalletCreditChange(context.Context, *gorm.DB, CreditChangeAudit) error } // ChangeCreditService 调整既有店铺主钱包实际信用额度。 type ChangeCreditService struct { db *gorm.DB audit CreditChangeAuditWriter } // NewChangeCreditService 创建实际信用额度调整服务。 func NewChangeCreditService(db *gorm.DB, audit CreditChangeAuditWriter) *ChangeCreditService { return &ChangeCreditService{db: db, audit: audit} } // Execute 使用服务端读取的版本条件更新,不修改余额、冻结金额或钱包流水。 func (s *ChangeCreditService) Execute(ctx context.Context, shopID uint, enabled bool, limit int64) (*dto.ShopCreditLimitResponse, error) { var result *dto.ShopCreditLimitResponse var stored model.AgentWallet var beforeData map[string]any err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Where("shop_id = ? AND wallet_type = ?", shopID, constants.AgentWalletTypeMain).First(&stored).Error; err != nil { return errors.New(errors.CodeWalletNotFound, "店铺主钱包不存在") } beforeData = creditAuditData(&stored) aggregate := domainwallet.AgentWallet{ID: stored.ID, ShopID: stored.ShopID, WalletType: stored.WalletType, Balance: stored.Balance, FrozenBalance: stored.FrozenBalance, CreditEnabled: stored.CreditEnabled, CreditLimit: stored.CreditLimit, Status: stored.Status, Version: stored.Version} if err := aggregate.ChangeCredit(enabled, limit); err != nil { return err } update := tx.Model(&model.AgentWallet{}). Where("id = ? AND wallet_type = ? AND version = ? AND balance::numeric - frozen_balance::numeric + ?::numeric >= 0", stored.ID, constants.AgentWalletTypeMain, stored.Version, aggregate.EffectiveCredit()). Updates(map[string]any{"credit_enabled": enabled, "credit_limit": limit, "version": gorm.Expr("version + 1")}) if update.Error != nil { return errors.Wrap(errors.CodeInternalError, update.Error, "更新店铺信用额度失败") } if update.RowsAffected != 1 { var current model.AgentWallet if err := tx.Where("id = ? AND wallet_type = ?", stored.ID, constants.AgentWalletTypeMain).First(¤t).Error; err != nil { return errors.New(errors.CodeWalletNotFound, "店铺主钱包不存在") } if current.Version != stored.Version { return errors.New(errors.CodeConflict, "钱包版本已变化,请重试") } return errors.New(errors.CodeInsufficientQuota, "当前资金占用无法降低或关闭信用额度") } available, _ := aggregate.AvailableBalance() after := stored after.CreditEnabled = enabled after.CreditLimit = limit after.Version++ result = &dto.ShopCreditLimitResponse{ShopID: shopID, WalletID: stored.ID, Balance: stored.Balance, FrozenBalance: stored.FrozenBalance, CreditEnabled: enabled, CreditLimit: limit, AvailableBalance: available, Version: after.Version} if s.audit == nil { return errors.New(errors.CodeInternalError, "代理主钱包信用额度审计接缝未配置") } return s.audit.WriteAgentWalletCreditChange(ctx, tx, CreditChangeAudit{ Wallet: &after, BeforeData: beforeData, AfterData: creditAuditData(&after), Result: constants.AuditResultSuccess, }) }) if err != nil && stored.ID != 0 { s.recordCreditChangeFailure(ctx, &stored, beforeData, err) } return result, err } func creditAuditData(wallet *model.AgentWallet) map[string]any { return map[string]any{ "balance": wallet.Balance, "frozen_balance": wallet.FrozenBalance, "credit_enabled": wallet.CreditEnabled, "credit_limit": wallet.CreditLimit, "version": wallet.Version, } } func (s *ChangeCreditService) recordCreditChangeFailure(ctx context.Context, wallet *model.AgentWallet, beforeData map[string]any, originalErr error) { result, code, summary := creditChangeError(originalErr) if s.audit == nil || s.db == nil { recordCreditChangeSecondaryFailure(ctx, wallet.ID, code, errors.New(errors.CodeInvalidStatus, "代理主钱包信用额度审计接缝未配置")) return } if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return s.audit.WriteAgentWalletCreditChange(ctx, tx, CreditChangeAudit{ Wallet: wallet, BeforeData: beforeData, Result: result, ErrorCode: code, ErrorSummary: summary, }) }); err != nil { recordCreditChangeSecondaryFailure(ctx, wallet.ID, code, err) } } func recordCreditChangeSecondaryFailure(ctx context.Context, walletID uint, errorCode string, err error) { linkage := auditcontext.From(ctx) auditfailure.RecordSecondaryWriteFailure( constants.AuditActionAgentWalletCreditChanged, strconv.FormatUint(uint64(walletID), 10), linkage.RequestID, linkage.CorrelationID, errorCode, err, ) } func creditChangeError(err error) (string, string, string) { var appErr *errors.AppError if !stderrors.As(err, &appErr) { return constants.AuditResultFailed, strconv.Itoa(errors.CodeInternalError), "更新代理主钱包信用额度失败" } result := constants.AuditResultDenied if appErr.Code == errors.CodeDatabaseError || appErr.Code == errors.CodeInternalError { result = constants.AuditResultFailed } return result, strconv.Itoa(appErr.Code), appErr.Message }