yumi-golang/internal/service/gameking/backfill_state.go
2026-07-18 18:56:15 +08:00

83 lines
3.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package gameking
import (
"context"
"net/http"
"time"
"chatapp3-golang/internal/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// loadOrCreateBackfillState 先建立活动级水位行,后续并发补数才能通过行锁检测游标竞争,
// 避免较慢请求把较快请求已经推进的复合游标覆盖回旧位置。
func (s *Service) loadOrCreateBackfillState(ctx context.Context, activity model.YumiGameKingActivity) (model.YumiGameKingBackfillState, error) {
now := time.Now()
start := activity.StartTime
initial := model.YumiGameKingBackfillState{
ActivityID: activity.ID, LastOccurredAt: &start, CreateTime: now, UpdateTime: now,
}
if err := s.db.WithContext(ctx).Clauses(clause.OnConflict{DoNothing: true}).Create(&initial).Error; err != nil {
return initial, err
}
var row model.YumiGameKingBackfillState
if err := s.db.WithContext(ctx).Where("activity_id = ?", activity.ID).First(&row).Error; err != nil {
return row, err
}
return row, nil
}
// adoptBackfillCursor 只兼容 migration 060 上线前调用方已经保存的合法 lastId水位一旦
// 非零,调用方就不能跳转或回退游标。
func (s *Service) adoptBackfillCursor(ctx context.Context, activityID int64, cursor model.TaskCenterEventArchiveOutbox) (model.YumiGameKingBackfillState, error) {
var state model.YumiGameKingBackfillState
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("activity_id = ?", activityID).First(&state).Error; err != nil {
return err
}
if state.LastOutboxID != 0 {
return NewAppError(http.StatusConflict, "backfill_cursor_conflict", "persisted backfill cursor changed; reload and retry")
}
now := time.Now()
if err := tx.Model(&model.YumiGameKingBackfillState{}).Where("activity_id = ?", activityID).Updates(map[string]any{
"last_outbox_id": cursor.ID, "last_occurred_at": cursor.OccurredAt,
"completed": false, "update_time": now,
}).Error; err != nil {
return err
}
state.LastOutboxID = cursor.ID
state.LastOccurredAt = &cursor.OccurredAt
state.UpdateTime = now
return nil
})
return state, err
}
func (s *Service) persistBackfillState(
ctx context.Context,
activityID, expectedLastID int64,
nextLastID int64,
nextOccurredAt time.Time,
scanned, accepted int,
completed bool,
) error {
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var locked model.YumiGameKingBackfillState
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("activity_id = ?", activityID).First(&locked).Error; err != nil {
return err
}
if locked.LastOutboxID != expectedLastID {
// 本页可能已经通过幂等 ledger 入账;不覆盖赢家水位,调用方重读后重放即可安全收敛。
return NewAppError(http.StatusConflict, "backfill_cursor_conflict", "another backfill request advanced the persisted cursor; reload and retry")
}
return tx.Model(&model.YumiGameKingBackfillState{}).Where("activity_id = ?", activityID).Updates(map[string]any{
"last_outbox_id": nextLastID, "last_occurred_at": nextOccurredAt,
"scanned_rows": gorm.Expr("scanned_rows + ?", scanned),
"accepted_rows": gorm.Expr("accepted_rows + ?", accepted),
"completed": completed, "update_time": time.Now(),
}).Error
})
}