Fix: 보상 이중 지급 방지, 에러 응답 개선, Rate Limit 조정
- reward_worker에 txCheck 기반 이중 지급 방지 추가 (LastTxID 저장 후 재시도 전 확인) - RewardFailure 모델에 LastTxID 필드 추가 - grantWithRetry가 txID를 반환하도록 변경 - 10회 재시도 초과 시 CRITICAL 로그에 상세 정보 포함 - 경험치 실패도 hasRewardFailure에 반영하여 reward_failed 상태 전이 - 에러 응답에 requestId 필드 포함 (관측성 개선) - /api/auth/refresh를 authLimiter에서 분리 (NAT 환경 한도 초과 방지) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,8 +11,9 @@ import (
|
||||
// RewardWorker periodically retries failed reward grants.
|
||||
type RewardWorker struct {
|
||||
repo *Repository
|
||||
rewardGrant func(username string, tokenAmount uint64, assets []core.MintAssetPayload) error
|
||||
rewardGrant func(username string, tokenAmount uint64, assets []core.MintAssetPayload) (txID string, err error)
|
||||
expGrant func(username string, exp int) error
|
||||
txCheck func(txID string) (confirmed bool, err error) // 이중 지급 방지: tx 상태 확인
|
||||
interval time.Duration
|
||||
stopCh chan struct{}
|
||||
}
|
||||
@@ -20,13 +21,15 @@ type RewardWorker struct {
|
||||
// NewRewardWorker creates a new RewardWorker. Default interval is 1 minute.
|
||||
func NewRewardWorker(
|
||||
repo *Repository,
|
||||
rewardGrant func(username string, tokenAmount uint64, assets []core.MintAssetPayload) error,
|
||||
rewardGrant func(username string, tokenAmount uint64, assets []core.MintAssetPayload) (txID string, err error),
|
||||
expGrant func(username string, exp int) error,
|
||||
txCheck func(txID string) (confirmed bool, err error),
|
||||
) *RewardWorker {
|
||||
return &RewardWorker{
|
||||
repo: repo,
|
||||
rewardGrant: rewardGrant,
|
||||
expGrant: expGrant,
|
||||
txCheck: txCheck,
|
||||
interval: 1 * time.Minute,
|
||||
stopCh: make(chan struct{}),
|
||||
}
|
||||
@@ -71,6 +74,21 @@ func (w *RewardWorker) retryOne(rf RewardFailure) {
|
||||
|
||||
// 블록체인 보상 재시도 (토큰 또는 에셋이 있는 경우)
|
||||
if (rf.TokenAmount > 0 || rf.Assets != "[]") && w.rewardGrant != nil {
|
||||
// 이중 지급 방지: 마지막 tx가 이미 성공했는지 확인
|
||||
if rf.LastTxID != "" && w.txCheck != nil {
|
||||
confirmed, checkErr := w.txCheck(rf.LastTxID)
|
||||
if checkErr != nil {
|
||||
log.Printf("보상 재시도 tx 상태 확인 실패: ID=%d, txID=%s: %v", rf.ID, rf.LastTxID, checkErr)
|
||||
// 상태 확인 실패 시 안전하게 재시도 건너뜀 (다음 주기에 다시 확인)
|
||||
return
|
||||
}
|
||||
if confirmed {
|
||||
log.Printf("보상 재시도 건너뜀 (이전 tx 이미 성공): ID=%d, txID=%s, %s", rf.ID, rf.LastTxID, rf.Username)
|
||||
// 블록체인 보상은 이미 지급됨 → 경험치만 확인
|
||||
goto expRetry
|
||||
}
|
||||
}
|
||||
|
||||
var assets []core.MintAssetPayload
|
||||
if rf.Assets != "" && rf.Assets != "[]" {
|
||||
if err := json.Unmarshal([]byte(rf.Assets), &assets); err != nil {
|
||||
@@ -82,9 +100,18 @@ func (w *RewardWorker) retryOne(rf RewardFailure) {
|
||||
return
|
||||
}
|
||||
}
|
||||
retryErr = w.rewardGrant(rf.Username, rf.TokenAmount, assets)
|
||||
txID, grantErr := w.rewardGrant(rf.Username, rf.TokenAmount, assets)
|
||||
retryErr = grantErr
|
||||
|
||||
// 시도한 txID 저장 (다음 재시도 시 이중 지급 방지용)
|
||||
if txID != "" {
|
||||
if err := w.repo.UpdateLastTxID(rf.ID, txID); err != nil {
|
||||
log.Printf("보상 재시도 txID 저장 실패: ID=%d: %v", rf.ID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expRetry:
|
||||
// 경험치 재시도 (블록체인 보상이 없거나 성공한 경우)
|
||||
if retryErr == nil && rf.Experience > 0 && w.expGrant != nil {
|
||||
retryErr = w.expGrant(rf.Username, rf.Experience)
|
||||
@@ -105,7 +132,8 @@ func (w *RewardWorker) retryOne(rf RewardFailure) {
|
||||
}
|
||||
newCount := rf.RetryCount + 1
|
||||
if newCount >= 10 {
|
||||
log.Printf("보상 재시도 포기 (최대 횟수 초과): ID=%d, %s", rf.ID, rf.Username)
|
||||
log.Printf("CRITICAL: 보상 재시도 포기 (최대 횟수 초과) — 수동 복구 필요: ID=%d, session=%s, user=%s, token=%d, exp=%d",
|
||||
rf.ID, rf.SessionName, rf.Username, rf.TokenAmount, rf.Experience)
|
||||
} else {
|
||||
log.Printf("보상 재시도 실패 (%d/10): ID=%d, %s: %v", newCount, rf.ID, rf.Username, retryErr)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user