收口审计治理与套餐任务进展
Constraint: 在线热修前必须保存当前迭代分支全部有效代码进展 Confidence: medium Scope-risk: broad Directive: 后续修改需保持审计事件与业务事务边界一致 Tested: git diff --cached --check Not-tested: 未运行全量测试,提交用于切换分支前保存既有工作
This commit is contained in:
@@ -3,33 +3,55 @@ 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
|
||||
db *gorm.DB
|
||||
audit CreditChangeAuditWriter
|
||||
}
|
||||
|
||||
// NewChangeCreditService 创建实际信用额度调整服务。
|
||||
func NewChangeCreditService(db *gorm.DB) *ChangeCreditService {
|
||||
return &ChangeCreditService{db: db}
|
||||
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 {
|
||||
var stored model.AgentWallet
|
||||
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
|
||||
@@ -51,8 +73,62 @@ func (s *ChangeCreditService) Execute(ctx context.Context, shopID uint, enabled
|
||||
return errors.New(errors.CodeInsufficientQuota, "当前资金占用无法降低或关闭信用额度")
|
||||
}
|
||||
available, _ := aggregate.AvailableBalance()
|
||||
result = &dto.ShopCreditLimitResponse{ShopID: shopID, WalletID: stored.ID, Balance: stored.Balance, FrozenBalance: stored.FrozenBalance, CreditEnabled: enabled, CreditLimit: limit, AvailableBalance: available, Version: stored.Version + 1}
|
||||
return nil
|
||||
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
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ type CreditedEvent struct {
|
||||
ReferenceType string `json:"reference_type"`
|
||||
ReferenceID uint `json:"reference_id"`
|
||||
TransactionType string `json:"transaction_type"`
|
||||
Remark string `json:"remark,omitempty"`
|
||||
OccurredAt time.Time `json:"occurred_at"`
|
||||
RequestID string `json:"request_id,omitempty"`
|
||||
CorrelationID string `json:"correlation_id,omitempty"`
|
||||
@@ -145,6 +146,7 @@ func (s *PostingService) PostInTx(ctx context.Context, tx *gorm.DB, command Post
|
||||
WalletID: stored.ID, ShopID: stored.ShopID, Amount: command.Amount,
|
||||
BalanceBefore: stored.Balance, BalanceAfter: aggregate.Balance, Version: stored.Version + 1,
|
||||
ReferenceType: referenceType, ReferenceID: command.ReferenceID, TransactionType: command.TransactionType,
|
||||
Remark: remark,
|
||||
OccurredAt: now, RequestID: command.RequestID, CorrelationID: command.CorrelationID,
|
||||
}
|
||||
if err := s.eventWriter.Append(ctx, tx, event); err != nil {
|
||||
@@ -163,6 +165,9 @@ func validatePostingCommand(command PostingCommand) error {
|
||||
if !validRecharge && !validAdjustment {
|
||||
return errors.New(errors.CodeInvalidParam, "代理主钱包入账业务类型无效")
|
||||
}
|
||||
if validAdjustment && strings.TrimSpace(command.Remark) == "" {
|
||||
return errors.New(errors.CodeInvalidParam, "人工调整代理主钱包必须填写原因")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user