收口审计治理与套餐任务进展

Constraint: 在线热修前必须保存当前迭代分支全部有效代码进展
Confidence: medium
Scope-risk: broad
Directive: 后续修改需保持审计事件与业务事务边界一致
Tested: git diff --cached --check
Not-tested: 未运行全量测试,提交用于切换分支前保存既有工作
This commit is contained in:
2026-08-05 14:30:54 +08:00
parent b3499adfca
commit 5e552d99bc
178 changed files with 16797 additions and 5674 deletions

View File

@@ -53,6 +53,7 @@ type Service struct {
operationPasswordService OperationPasswordServiceInterface
redis *redis.Client
logger *zap.Logger
rechargeAudit agentrechargeapp.RechargeAuditWriter
}
// New 创建代理预充值服务实例
@@ -90,6 +91,11 @@ func (s *Service) SetOfflineCreationService(service *agentrechargeapp.OfflineCre
s.offlineCreation = service
}
// SetRechargeAudit 注入代理充值统一审计 Writer。
func (s *Service) SetRechargeAudit(writer agentrechargeapp.RechargeAuditWriter) {
s.rechargeAudit = writer
}
// Create 创建代理充值订单
// POST /api/admin/agent-recharges
func (s *Service) Create(ctx context.Context, req *dto.CreateAgentRechargeRequest) (*dto.AgentRechargeResponse, error) {
@@ -215,33 +221,22 @@ func (s *Service) OfflinePay(ctx context.Context, id uint, req *dto.AgentOffline
if value := middleware.GetRequestIDFromContext(ctx); value != nil {
requestID = *value
}
if _, err := s.agentWalletPosting.PostInTx(ctx, tx, walletapp.PostingCommand{
_, err := s.agentWalletPosting.PostInTx(ctx, tx, walletapp.PostingCommand{
ShopID: record.ShopID, WalletID: record.AgentWalletID, Amount: record.Amount,
ReferenceType: constants.ReferenceTypeTopup, ReferenceID: record.ID,
TransactionType: constants.AgentTransactionTypeRecharge, UserID: userID, Creator: userID,
Remark: "线下充值确认", RequestID: requestID, CorrelationID: record.RechargeNo,
}); err != nil {
})
if err != nil {
return err
}
return nil
return s.appendCreditedAudit(ctx, tx, record, nil, constants.RechargeStatusCompleted, "线下充值确认已入账")
})
if err != nil {
return nil, err
}
// 异步记录审计日志
go s.auditService.LogOperation(ctx, &model.AccountOperationLog{
OperatorID: userID,
OperatorType: userType,
OperationType: "offline_recharge_confirm",
OperationDesc: fmt.Sprintf("确认线下充值,充值单号: %s金额: %d分", record.RechargeNo, record.Amount),
RequestID: middleware.GetRequestIDFromContext(ctx),
IPAddress: middleware.GetIPFromContext(ctx),
UserAgent: middleware.GetUserAgentFromContext(ctx),
})
shop, _ := s.shopStore.GetByID(ctx, record.ShopID)
shopName := ""
if shop != nil {
@@ -325,16 +320,16 @@ func (s *Service) HandlePaymentCallback(ctx context.Context, rechargeNo string,
if value := middleware.GetRequestIDFromContext(ctx); value != nil {
requestID = *value
}
if _, err := s.agentWalletPosting.PostInTx(ctx, tx, walletapp.PostingCommand{
_, err := s.agentWalletPosting.PostInTx(ctx, tx, walletapp.PostingCommand{
ShopID: record.ShopID, WalletID: record.AgentWalletID, Amount: record.Amount,
ReferenceType: constants.ReferenceTypeTopup, ReferenceID: record.ID,
TransactionType: constants.AgentTransactionTypeRecharge, UserID: record.UserID, Creator: record.UserID,
Remark: "在线支付充值", RequestID: requestID, CorrelationID: record.RechargeNo,
}); err != nil {
})
if err != nil {
return err
}
return nil
return s.appendCreditedAudit(ctx, tx, record, nil, constants.RechargeStatusCompleted, "代理充值支付回调已入账")
})
if err != nil {
@@ -391,11 +386,30 @@ func (s *Service) Reject(ctx context.Context, id uint, rejectionReason string) e
return errors.New(errors.CodeInvalidStatus, "该线下充值申请由企业微信审批决定,不能人工驳回")
}
if err := s.agentRechargeStore.UpdateStatusWithRejection(ctx, id, rejectionReason); err != nil {
if err == gorm.ErrRecordNotFound {
if s.rechargeAudit == nil {
return errors.New(errors.CodeInvalidStatus, "代理充值统一审计接缝未配置")
}
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
result := tx.Model(&model.AgentRechargeRecord{}).
Where("id = ? AND status = ?", record.ID, constants.RechargeStatusPending).
Updates(map[string]any{"status": constants.RechargeStatusRejected, "rejection_reason": strings.TrimSpace(rejectionReason)})
if result.Error != nil {
return errors.Wrap(errors.CodeDatabaseError, result.Error, "驳回充值订单失败")
}
if result.RowsAffected != 1 {
return errors.New(errors.CodeInvalidStatus, "仅待支付订单可驳回")
}
return errors.Wrap(errors.CodeDatabaseError, err, "驳回充值订单失败")
after := *record
after.Status = constants.RechargeStatusRejected
reason := strings.TrimSpace(rejectionReason)
after.RejectionReason = &reason
return s.rechargeAudit.WriteAgentRecharge(ctx, tx, agentrechargeapp.RechargeAudit{
ActionCode: constants.AuditActionAgentRechargeClosed, Summary: "驳回代理充值申请",
Record: &after, BeforeData: map[string]any{"status": record.Status},
AfterData: map[string]any{"status": after.Status, "rejection_reason": reason},
})
}); err != nil {
return err
}
s.logger.Info("代理充值订单驳回成功",
@@ -405,6 +419,29 @@ func (s *Service) Reject(ctx context.Context, id uint, rejectionReason string) e
return nil
}
func (s *Service) appendCreditedAudit(ctx context.Context, tx *gorm.DB, record *model.AgentRechargeRecord, payment *model.Payment, status int, summary string) error {
if s.rechargeAudit == nil {
return errors.New(errors.CodeInvalidStatus, "代理充值统一审计接缝未配置")
}
var wallet model.AgentWallet
if err := tx.WithContext(ctx).First(&wallet, record.AgentWalletID).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "查询代理充值钱包审计快照失败")
}
var transaction model.AgentWalletTransaction
if err := tx.WithContext(ctx).Where("reference_type = ? AND reference_id = ? AND transaction_type = ? AND status = ?",
constants.ReferenceTypeTopup, record.ID, constants.AgentTransactionTypeRecharge, constants.TransactionStatusSuccess).
First(&transaction).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "查询代理充值入账流水审计快照失败")
}
after := *record
after.Status = status
return s.rechargeAudit.WriteAgentRecharge(ctx, tx, agentrechargeapp.RechargeAudit{
ActionCode: constants.AuditActionAgentRechargeCredited, Summary: summary,
Record: &after, Payment: payment, Wallet: &wallet, Transaction: &transaction,
BeforeData: map[string]any{"status": record.Status}, AfterData: map[string]any{"status": status},
})
}
// GetByID 根据ID查询充值订单详情
// GET /api/admin/agent-recharges/:id
func (s *Service) GetByID(ctx context.Context, id uint) (*dto.AgentRechargeResponse, error) {