83 lines
3.2 KiB
Go
83 lines
3.2 KiB
Go
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
|
||
})
|
||
}
|