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 }) }