Files
lijiaoqiao/supply-api/internal/domain/compensation.go
Your Name 841d75191e refactor: 使用结构化日志替代标准 log 包
将所有 log.Printf/Println 调用替换为 logging.Logger 结构化日志输出:
- internal/middleware/auth.go: AUTH_WARNING 和 AUTH_ERROR 日志
- internal/middleware/middleware.go: Recovery 中间件 panic 日志
- internal/domain/account.go: emitAudit 审计错误日志
- internal/domain/settlement.go: emitAudit 审计错误日志
- internal/domain/package.go: emitAudit 审计错误日志
- internal/domain/compensation.go: 补偿处理器所有日志
- internal/compensation/compensation.go: 补偿执行器所有日志
- internal/outbox/outbox.go: OutboxProcessor 所有日志
- internal/iam/middleware/scope_auth.go: WILDCARD_SCOPE_ACCESS 审计日志

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 09:49:58 +08:00

393 lines
13 KiB
Go

package domain
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/jackc/pgx/v5/pgxpool"
"lijiaoqiao/supply-api/internal/pkg/logging"
)
// ==================== P0-07 批量补偿策略 ====================
// BatchCompensation 批量补偿记录
type BatchCompensation struct {
ID int64 `json:"id"`
BatchID string `json:"batch_id"`
OperationType string `json:"operation_type"`
ItemIndex int `json:"item_index"`
ItemPayload json.RawMessage `json:"item_payload"`
FailureReason string `json:"failure_reason,omitempty"`
Status string `json:"status"` // pending, retrying, resolved, manual_required, abandoned
RetryCount int `json:"retry_count"`
MaxRetries int `json:"max_retries"`
ResolvedAt *time.Time `json:"resolved_at,omitempty"`
ResolvedBy *int64 `json:"resolved_by,omitempty"`
ResolutionNotes string `json:"resolution_notes,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
CreatedBy *int64 `json:"created_by,omitempty"`
Version int64 `json:"version"`
}
// CompensationStatus 补偿状态
const (
CompensationStatusPending = "pending"
CompensationStatusRetrying = "retrying"
CompensationStatusResolved = "resolved"
CompensationStatusManualRequired = "manual_required"
CompensationStatusAbandoned = "abandoned"
)
// CompensationStore 补偿存储接口
type CompensationStore interface {
// Create 创建补偿记录
Create(ctx context.Context, comp *BatchCompensation) (int64, error)
// GetByBatchID 获取批次补偿列表
GetByBatchID(ctx context.Context, batchID string) ([]*BatchCompensation, error)
// GetPending 获取所有待处理的补偿记录
GetPending(ctx context.Context) ([]*BatchCompensation, error)
// UpdateStatus 更新状态
UpdateStatus(ctx context.Context, id int64, status string) error
// Resolve 解决补偿
Resolve(ctx context.Context, id int64, resolvedBy int64, notes string) error
// MarkManualRequired 标记需要人工介入
MarkManualRequired(ctx context.Context, id int64, reason string) error
}
// CompensationProcessor 补偿处理器
type CompensationProcessor struct {
store CompensationStore
operationExecutor OperationExecutor
stats CompensationStats
workerCancel context.CancelFunc // 保存worker context的cancel函数
}
// OperationExecutor 操作执行器接口
type OperationExecutor interface {
// Execute 执行单个操作
Execute(ctx context.Context, operationType string, payload json.RawMessage) error
}
// CompensationStats 补偿统计接口
type CompensationStats interface {
RecordCompensationRetry(operationType string)
RecordCompensationResolved(operationType string)
RecordCompensationManual(operationType string)
}
// DefaultCompensationConfig 默认补偿配置
func DefaultCompensationConfig() *CompensationConfig {
return &CompensationConfig{
MaxRetries: 3,
RetryInterval: 1 * time.Minute,
}
}
// NoOpCompensationStats No-op补偿统计实现
type NoOpCompensationStats struct{}
func (s *NoOpCompensationStats) RecordCompensationRetry(operationType string) {}
func (s *NoOpCompensationStats) RecordCompensationResolved(operationType string) {}
func (s *NoOpCompensationStats) RecordCompensationManual(operationType string) {}
// NewCompensationProcessor 创建补偿处理器
func NewCompensationProcessor(store CompensationStore, executor OperationExecutor, stats CompensationStats) *CompensationProcessor {
return &CompensationProcessor{
store: store,
operationExecutor: executor,
stats: stats,
}
}
// CompensationConfig 补偿配置
type CompensationConfig struct {
MaxRetries int
RetryInterval time.Duration
}
// ProcessBatchCompensations 处理批次补偿
func (p *CompensationProcessor) ProcessBatchCompensations(ctx context.Context, batchID string) (*CompensationResult, error) {
// 获取批次补偿列表
compensations, err := p.store.GetByBatchID(ctx, batchID)
if err != nil {
return nil, fmt.Errorf("failed to get compensations: %w", err)
}
result := &CompensationResult{
BatchID: batchID,
TotalItems: len(compensations),
SuccessCount: 0,
RetryCount: 0,
ManualCount: 0,
FailedCount: 0,
}
for _, comp := range compensations {
if comp.Status != CompensationStatusPending {
continue
}
// 重试执行
err := p.operationExecutor.Execute(ctx, comp.OperationType, comp.ItemPayload)
if err != nil {
comp.RetryCount++
comp.FailureReason = err.Error()
if comp.RetryCount >= comp.MaxRetries {
// 超过最大重试次数,标记需要人工介入
if err := p.store.MarkManualRequired(ctx, comp.ID, err.Error()); err != nil {
result.FailedCount++
continue
}
result.ManualCount++
p.stats.RecordCompensationManual(comp.OperationType)
} else {
// 继续重试
if err := p.store.UpdateStatus(ctx, comp.ID, CompensationStatusRetrying); err != nil {
result.FailedCount++
continue
}
result.RetryCount++
p.stats.RecordCompensationRetry(comp.OperationType)
}
} else {
// 执行成功,标记解决
if err := p.store.Resolve(ctx, comp.ID, 0, "auto_resolved"); err != nil {
result.FailedCount++
continue
}
result.SuccessCount++
p.stats.RecordCompensationResolved(comp.OperationType)
}
}
return result, nil
}
// CompensationResult 补偿处理结果
type CompensationResult struct {
BatchID string `json:"batch_id"`
TotalItems int `json:"total_items"`
SuccessCount int `json:"success_count"`
RetryCount int `json:"retry_count"`
ManualCount int `json:"manual_count"`
FailedCount int `json:"failed_count"`
}
// StartBackgroundWorker 启动后台补偿处理worker
func (p *CompensationProcessor) StartBackgroundWorker(ctx context.Context, interval time.Duration) context.Context {
workerCtx, cancel := context.WithCancel(ctx)
p.workerCancel = cancel // 保存cancel函数以便后续停止worker
go func() {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-workerCtx.Done():
logger := logging.NewLogger("supply-api", logging.LogLevelInfo)
logger.Info("compensation worker stopped", nil)
return
case <-ticker.C:
p.processPendingCompensations(workerCtx)
}
}
}()
return workerCtx
}
// StopBackgroundWorker 停止后台补偿处理worker
func (p *CompensationProcessor) StopBackgroundWorker() {
if p.workerCancel != nil {
p.workerCancel()
p.workerCancel = nil
}
}
// processPendingCompensations 处理所有待处理的补偿记录
func (p *CompensationProcessor) processPendingCompensations(ctx context.Context) {
// 获取所有pending和retrying状态的补偿记录
compensations, err := p.store.GetPending(ctx)
if err != nil {
logger := logging.NewLogger("supply-api", logging.LogLevelError)
logger.Error("compensation worker: failed to get pending compensations", map[string]interface{}{
"error": err.Error(),
})
return
}
if len(compensations) == 0 {
return
}
logger := logging.NewLogger("supply-api", logging.LogLevelInfo)
logger.Info("compensation worker: found pending compensations", map[string]interface{}{
"count": len(compensations),
})
for _, comp := range compensations {
// 重试执行
err := p.operationExecutor.Execute(ctx, comp.OperationType, comp.ItemPayload)
if err != nil {
comp.RetryCount++
comp.FailureReason = err.Error()
if comp.RetryCount >= comp.MaxRetries {
// 超过最大重试次数,标记需要人工介入
if markErr := p.store.MarkManualRequired(ctx, comp.ID, err.Error()); markErr != nil {
logger := logging.NewLogger("supply-api", logging.LogLevelError)
logger.Error("compensation worker: failed to mark manual required", map[string]interface{}{
"id": comp.ID,
"error": markErr.Error(),
})
}
p.stats.RecordCompensationManual(comp.OperationType)
} else {
// 继续重试
if updateErr := p.store.UpdateStatus(ctx, comp.ID, CompensationStatusRetrying); updateErr != nil {
logger := logging.NewLogger("supply-api", logging.LogLevelError)
logger.Error("compensation worker: failed to update status", map[string]interface{}{
"id": comp.ID,
"error": updateErr.Error(),
})
}
p.stats.RecordCompensationRetry(comp.OperationType)
}
} else {
// 执行成功,标记解决
if resolveErr := p.store.Resolve(ctx, comp.ID, 0, "worker_auto_resolved"); resolveErr != nil {
logger := logging.NewLogger("supply-api", logging.LogLevelError)
logger.Error("compensation worker: failed to resolve", map[string]interface{}{
"id": comp.ID,
"error": resolveErr.Error(),
})
}
p.stats.RecordCompensationResolved(comp.OperationType)
}
}
}
// SQLCompensationStore SQL实现的补偿存储
type SQLCompensationStore struct {
pool *pgxpool.Pool
}
// NewSQLCompensationStore 创建SQL补偿存储
func NewSQLCompensationStore(pool *pgxpool.Pool) *SQLCompensationStore {
return &SQLCompensationStore{pool: pool}
}
func (s *SQLCompensationStore) Create(ctx context.Context, comp *BatchCompensation) (int64, error) {
var id int64
err := s.pool.QueryRow(ctx, `
INSERT INTO supply_batch_compensation (
batch_id, operation_type, item_index, item_payload,
failure_reason, status, max_retries, created_by
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id
`, comp.BatchID, comp.OperationType, comp.ItemIndex, comp.ItemPayload,
comp.FailureReason, CompensationStatusPending, comp.MaxRetries, comp.CreatedBy).
Scan(&id)
return id, err
}
func (s *SQLCompensationStore) GetByBatchID(ctx context.Context, batchID string) ([]*BatchCompensation, error) {
rows, err := s.pool.Query(ctx, `
SELECT id, batch_id, operation_type, item_index, item_payload,
failure_reason, status, retry_count, max_retries,
resolved_at, resolved_by, resolution_notes,
created_at, updated_at, created_by, version
FROM supply_batch_compensation
WHERE batch_id = $1
ORDER BY item_index
`, batchID)
if err != nil {
return nil, err
}
defer rows.Close()
var compensations []*BatchCompensation
for rows.Next() {
comp := &BatchCompensation{}
err := rows.Scan(
&comp.ID, &comp.BatchID, &comp.OperationType, &comp.ItemIndex,
&comp.ItemPayload, &comp.FailureReason, &comp.Status,
&comp.RetryCount, &comp.MaxRetries, &comp.ResolvedAt,
&comp.ResolvedBy, &comp.ResolutionNotes, &comp.CreatedAt,
&comp.UpdatedAt, &comp.CreatedBy, &comp.Version,
)
if err != nil {
return nil, err
}
compensations = append(compensations, comp)
}
return compensations, rows.Err()
}
// GetPending 获取所有待处理的补偿记录
func (s *SQLCompensationStore) GetPending(ctx context.Context) ([]*BatchCompensation, error) {
rows, err := s.pool.Query(ctx, `
SELECT id, batch_id, operation_type, item_index, item_payload,
failure_reason, status, retry_count, max_retries,
resolved_at, resolved_by, resolution_notes,
created_at, updated_at, created_by, version
FROM supply_batch_compensation
WHERE status IN ($1, $2)
ORDER BY created_at ASC
`, CompensationStatusPending, CompensationStatusRetrying)
if err != nil {
return nil, err
}
defer rows.Close()
var compensations []*BatchCompensation
for rows.Next() {
comp := &BatchCompensation{}
err := rows.Scan(
&comp.ID, &comp.BatchID, &comp.OperationType, &comp.ItemIndex,
&comp.ItemPayload, &comp.FailureReason, &comp.Status,
&comp.RetryCount, &comp.MaxRetries, &comp.ResolvedAt,
&comp.ResolvedBy, &comp.ResolutionNotes, &comp.CreatedAt,
&comp.UpdatedAt, &comp.CreatedBy, &comp.Version,
)
if err != nil {
return nil, err
}
compensations = append(compensations, comp)
}
return compensations, rows.Err()
}
func (s *SQLCompensationStore) UpdateStatus(ctx context.Context, id int64, status string) error {
_, err := s.pool.Exec(ctx, `
UPDATE supply_batch_compensation
SET status = $1, updated_at = CURRENT_TIMESTAMP, version = version + 1
WHERE id = $2
`, status, id)
return err
}
func (s *SQLCompensationStore) Resolve(ctx context.Context, id int64, resolvedBy int64, notes string) error {
_, err := s.pool.Exec(ctx, `
UPDATE supply_batch_compensation
SET status = $1, resolved_at = CURRENT_TIMESTAMP,
resolved_by = $2, resolution_notes = $3,
updated_at = CURRENT_TIMESTAMP, version = version + 1
WHERE id = $4
`, CompensationStatusResolved, resolvedBy, notes, id)
return err
}
func (s *SQLCompensationStore) MarkManualRequired(ctx context.Context, id int64, reason string) error {
_, err := s.pool.Exec(ctx, `
UPDATE supply_batch_compensation
SET status = $1, failure_reason = COALESCE(failure_reason || '; ', '') || $2,
updated_at = CURRENT_TIMESTAMP, version = version + 1
WHERE id = $3
`, CompensationStatusManualRequired, reason, id)
return err
}