Files
90CNServer/server/internal/runtime/leaderboard/leaderboard.go
T
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

866 lines
23 KiB
Go

package leaderboard
import (
"context"
"fmt"
"sort"
"strings"
"sync"
"time"
)
type Manager struct {
name string
now func() time.Time
gen func(prefix string) string
hist HistoryStore
historyStrict bool
historyErrors int
mu sync.Mutex
next uint64
definitions map[string]Definition
records map[string]map[string]Record
repairs map[string]RepairReceipt
}
type managerStateSnapshot struct {
definitions map[string]Definition
records map[string]map[string]Record
repairs map[string]RepairReceipt
}
type boardRankData struct {
definition Definition
records map[string]Record
}
func New(options Options) *Manager {
name := strings.TrimSpace(options.Name)
if name == "" {
name = "leaderboard"
}
now := options.Now
if now == nil {
now = time.Now
}
historyStore := options.HistoryStore
if historyStore == nil && options.HistoryLimit >= 0 {
historyLimit := options.HistoryLimit
if historyLimit == 0 {
historyLimit = defaultHistoryLimit
}
historyStore = NewMemoryHistoryStore(historyLimit)
}
historyStore = wrapHistoryStore(historyStore, options.HistoryStrict)
return &Manager{
name: name,
now: now,
gen: options.IDGenerator,
hist: historyStore,
historyStrict: options.HistoryStrict,
definitions: make(map[string]Definition),
records: make(map[string]map[string]Record),
repairs: make(map[string]RepairReceipt),
}
}
func (m *Manager) Create(ctx context.Context, definition Definition) (Definition, error) {
if err := ctxErr(ctx); err != nil {
return Definition{}, err
}
if m == nil {
return Definition{}, ErrManagerRequired
}
m.mu.Lock()
defer m.mu.Unlock()
m.ensureReadyLocked()
normalized, err := m.normDefinitionLocked(definition)
if err != nil {
return Definition{}, err
}
if _, exists := m.definitions[normalized.ID]; exists {
return Definition{}, ErrLeaderboardExists
}
before := m.snapshotStateLocked()
m.definitions[normalized.ID] = normalized
m.records[normalized.ID] = make(map[string]Record)
if err := m.appendHistoryLocked(ctx, HistoryActionCreate, normalized.ID, nil, &normalized, normalized.CreatedAt); err != nil {
m.restoreStateLocked(before)
return Definition{}, err
}
return cloneDefinition(normalized), nil
}
func (m *Manager) Delete(ctx context.Context, leaderboardID string) (Definition, error) {
if err := ctxErr(ctx); err != nil {
return Definition{}, err
}
if m == nil {
return Definition{}, ErrManagerRequired
}
leaderboardID = strings.TrimSpace(leaderboardID)
if leaderboardID == "" {
return Definition{}, ErrLeaderboardNotFound
}
m.mu.Lock()
defer m.mu.Unlock()
m.ensureReadyLocked()
definition, ok := m.definitions[leaderboardID]
if !ok {
return Definition{}, ErrLeaderboardNotFound
}
before := m.snapshotStateLocked()
delete(m.definitions, leaderboardID)
delete(m.records, leaderboardID)
if err := m.appendHistoryLocked(ctx, HistoryDeleteBoard, definition.ID, nil, &definition, m.now().UTC()); err != nil {
m.restoreStateLocked(before)
return Definition{}, err
}
return cloneDefinition(definition), nil
}
func (m *Manager) Definition(leaderboardID string) (Definition, bool) {
leaderboardID = strings.TrimSpace(leaderboardID)
if m == nil || leaderboardID == "" {
return Definition{}, false
}
m.mu.Lock()
defer m.mu.Unlock()
m.ensureReadyLocked()
definition, ok := m.definitions[leaderboardID]
return cloneDefinition(definition), ok
}
func (m *Manager) Definitions() []Definition {
if m == nil {
return nil
}
m.mu.Lock()
defer m.mu.Unlock()
m.ensureReadyLocked()
out := make([]Definition, 0, len(m.definitions))
for _, definition := range m.definitions {
out = append(out, cloneDefinition(definition))
}
sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID })
return out
}
func (m *Manager) Submit(ctx context.Context, leaderboardID string, submission Submission) (Record, error) {
if err := ctxErr(ctx); err != nil {
return Record{}, err
}
if m == nil {
return Record{}, ErrManagerRequired
}
leaderboardID = strings.TrimSpace(leaderboardID)
if leaderboardID == "" {
return Record{}, ErrLeaderboardNotFound
}
m.mu.Lock()
defer m.mu.Unlock()
m.ensureReadyLocked()
definition, ok := m.definitions[leaderboardID]
if !ok {
return Record{}, ErrLeaderboardNotFound
}
normalized, err := normalizeSubmission(submission)
if err != nil {
return Record{}, err
}
now := m.now().UTC()
recordMap := m.records[leaderboardID]
existing, exists := recordMap[normalized.OwnerID]
record := Record{
LeaderboardID: leaderboardID,
OwnerID: normalized.OwnerID,
Score: normalized.Score,
Subscore: normalized.Subscore,
Metadata: cloneStringMap(normalized.Metadata),
CreatedAt: now,
UpdatedAt: now,
}
if exists {
record.CreatedAt = existing.CreatedAt
switch definition.Operator {
case OperatorBest:
candidate := record
if !betterThan(candidate, existing, definition.SortOrder) {
// Best 模式只接受更优成绩;较差提交返回当前排名,不写历史噪音。
ranked, ok := m.recordWithRankLocked(leaderboardID, normalized.OwnerID)
if !ok {
return Record{}, ErrRecordNotFound
}
return ranked, nil
}
case OperatorIncrement:
// Increment 模式用于累计榜,调用方要保证加分来源已经在关键写路径里做幂等。
record.Score, err = addScoreValue(existing.Score, normalized.Score)
if err != nil {
return Record{}, err
}
record.Subscore, err = addScoreValue(existing.Subscore, normalized.Subscore)
if err != nil {
return Record{}, err
}
case OperatorSet:
default:
return Record{}, fmt.Errorf("%w: unsupported operator %q", ErrInvalidDefinition, definition.Operator)
}
}
before := m.snapshotStateLocked()
recordMap[normalized.OwnerID] = record
var trimmed []Record
if definition.MaxSize > 0 {
// 内存实现按 MaxSize 裁剪只保证当前进程视角;跨服榜生产应使用 Redis/SQL 方案。
trimmed = m.trimLocked(definition)
}
if err := m.appendHistoryLocked(ctx, HistoryActionSubmit, definition.ID, &record, nil, record.UpdatedAt); err != nil {
m.restoreStateLocked(before)
return Record{}, err
}
for _, trimmedRecord := range trimmed {
if err := m.appendHistoryLocked(ctx, HistoryActionTrimRecord, definition.ID, &trimmedRecord, nil, m.now().UTC()); err != nil {
m.restoreStateLocked(before)
return Record{}, err
}
}
ranked, ok := m.recordWithRankLocked(leaderboardID, normalized.OwnerID)
if !ok {
return Record{}, ErrRecordTrimmed
}
return ranked, nil
}
func (m *Manager) DeleteRecord(ctx context.Context, leaderboardID string, ownerID string) (Record, error) {
if err := ctxErr(ctx); err != nil {
return Record{}, err
}
if m == nil {
return Record{}, ErrManagerRequired
}
leaderboardID = strings.TrimSpace(leaderboardID)
ownerID = strings.TrimSpace(ownerID)
if leaderboardID == "" {
return Record{}, ErrLeaderboardNotFound
}
if ownerID == "" {
return Record{}, ErrRecordNotFound
}
m.mu.Lock()
defer m.mu.Unlock()
m.ensureReadyLocked()
if _, ok := m.definitions[leaderboardID]; !ok {
return Record{}, ErrLeaderboardNotFound
}
record, ok := m.recordWithRankLocked(leaderboardID, ownerID)
if !ok {
return Record{}, ErrRecordNotFound
}
before := m.snapshotStateLocked()
delete(m.records[leaderboardID], ownerID)
if err := m.appendHistoryLocked(ctx, HistoryActionDeleteRecord, leaderboardID, &record, nil, m.now().UTC()); err != nil {
m.restoreStateLocked(before)
return Record{}, err
}
return record, nil
}
func (m *Manager) Repair(ctx context.Context, leaderboardID string, request RepairRequest) (RepairReceipt, error) {
if err := ctxErr(ctx); err != nil {
return RepairReceipt{}, err
}
if m == nil {
return RepairReceipt{}, ErrManagerRequired
}
leaderboardID = strings.TrimSpace(leaderboardID)
if leaderboardID == "" {
return RepairReceipt{}, ErrLeaderboardNotFound
}
normalized, err := normRepairReq(request)
if err != nil {
return RepairReceipt{}, err
}
m.mu.Lock()
defer m.mu.Unlock()
m.ensureReadyLocked()
definition, ok := m.definitions[leaderboardID]
if !ok {
return RepairReceipt{}, ErrLeaderboardNotFound
}
repairKey := repairIdempotencyKey(leaderboardID, normalized.RequestID)
if repairKey != "" {
// GM 修榜必须支持 request_id 重放,避免审批系统或网络重试重复修改成绩。
if previous, ok := m.repairs[repairKey]; ok {
return cloneRepairReceipt(previous), nil
}
}
now := m.now().UTC()
receipt := RepairReceipt{
LeaderboardID: leaderboardID,
OwnerID: normalized.OwnerID,
Action: HistoryActionRepairRecord,
Reason: normalized.Reason,
OperatorID: normalized.OperatorID,
RequestID: normalized.RequestID,
Metadata: cloneStringMap(normalized.Metadata),
At: now,
}
if before, found := m.recordWithRankLocked(leaderboardID, normalized.OwnerID); found {
before := before
receipt.Before = &before
}
stateBefore := m.snapshotStateLocked()
if normalized.Delete {
if receipt.Before == nil {
return RepairReceipt{}, ErrRecordNotFound
}
// 删除和改分都写 history,方便后续审计、回滚和跨节点重建。
delete(m.records[leaderboardID], normalized.OwnerID)
if err := m.appendHistoryEntry(ctx, repairHistoryEntry(receipt, nil)); err != nil {
m.restoreStateLocked(stateBefore)
return RepairReceipt{}, err
}
if repairKey != "" {
m.repairs[repairKey] = cloneRepairReceipt(receipt)
}
return cloneRepairReceipt(receipt), nil
}
createdAt := now
if receipt.Before != nil && !normalized.ResetCreatedAt {
createdAt = receipt.Before.CreatedAt
}
record := Record{
LeaderboardID: leaderboardID,
OwnerID: normalized.OwnerID,
Score: normalized.Score,
Subscore: normalized.Subscore,
Metadata: cloneStringMap(normalized.Metadata),
CreatedAt: createdAt,
UpdatedAt: now,
}
m.records[leaderboardID][record.OwnerID] = record
trimmed := m.trimLocked(definition)
if after, found := m.recordWithRankLocked(leaderboardID, record.OwnerID); found {
after := after
receipt.After = &after
}
if err := m.appendHistoryEntry(ctx, repairHistoryEntry(receipt, receipt.After)); err != nil {
m.restoreStateLocked(stateBefore)
return RepairReceipt{}, err
}
for _, trimmedRecord := range trimmed {
if err := m.appendHistoryLocked(ctx, HistoryActionTrimRecord, definition.ID, &trimmedRecord, nil, now); err != nil {
m.restoreStateLocked(stateBefore)
return RepairReceipt{}, err
}
}
if repairKey != "" {
m.repairs[repairKey] = cloneRepairReceipt(receipt)
}
return cloneRepairReceipt(receipt), nil
}
func (m *Manager) Record(leaderboardID string, ownerID string) (Record, bool) {
leaderboardID = strings.TrimSpace(leaderboardID)
ownerID = strings.TrimSpace(ownerID)
if m == nil || leaderboardID == "" || ownerID == "" {
return Record{}, false
}
m.mu.Lock()
m.ensureReadyLocked()
data, ok := m.rankDataLocked(leaderboardID)
m.mu.Unlock()
if !ok {
return Record{}, false
}
return recordWithRank(data.definition, data.records, ownerID)
}
func (m *Manager) List(ctx context.Context, leaderboardID string, options ListOptions) ([]Record, error) {
if err := ctxErr(ctx); err != nil {
return nil, err
}
if m == nil {
return nil, ErrManagerRequired
}
leaderboardID = strings.TrimSpace(leaderboardID)
if leaderboardID == "" {
return nil, ErrLeaderboardNotFound
}
m.mu.Lock()
m.ensureReadyLocked()
data, ok := m.rankDataLocked(leaderboardID)
m.mu.Unlock()
if !ok {
return nil, ErrLeaderboardNotFound
}
ranked := rankRecords(data.definition, data.records)
offset, limit := normalizeListOptions(options)
if offset >= len(ranked) {
return []Record{}, nil
}
end := offset + limit
if end > len(ranked) {
end = len(ranked)
}
return cloneRecords(ranked[offset:end]), nil
}
func (m *Manager) AroundOwner(ctx context.Context, leaderboardID string, ownerID string, limit int) ([]Record, error) {
if err := ctxErr(ctx); err != nil {
return nil, err
}
if m == nil {
return nil, ErrManagerRequired
}
leaderboardID = strings.TrimSpace(leaderboardID)
ownerID = strings.TrimSpace(ownerID)
if leaderboardID == "" {
return nil, ErrLeaderboardNotFound
}
if ownerID == "" {
return nil, ErrRecordNotFound
}
m.mu.Lock()
m.ensureReadyLocked()
data, ok := m.rankDataLocked(leaderboardID)
m.mu.Unlock()
if !ok {
return nil, ErrLeaderboardNotFound
}
ranked := rankRecords(data.definition, data.records)
idx := -1
for i, record := range ranked {
if record.OwnerID == ownerID {
idx = i
break
}
}
if idx < 0 {
return nil, ErrRecordNotFound
}
if limit <= 0 {
limit = defaultAroundLimit
}
if limit > maxListLimit {
limit = maxListLimit
}
if limit > len(ranked) {
limit = len(ranked)
}
start := idx - limit/2
if start < 0 {
start = 0
}
end := start + limit
if end > len(ranked) {
end = len(ranked)
start = end - limit
if start < 0 {
start = 0
}
}
return cloneRecords(ranked[start:end]), nil
}
func (m *Manager) Rank(ctx context.Context, leaderboardID string, ownerID string) (int, bool, error) {
if err := ctxErr(ctx); err != nil {
return 0, false, err
}
if m == nil {
return 0, false, ErrManagerRequired
}
leaderboardID = strings.TrimSpace(leaderboardID)
ownerID = strings.TrimSpace(ownerID)
if leaderboardID == "" {
return 0, false, ErrLeaderboardNotFound
}
if ownerID == "" {
return 0, false, nil
}
m.mu.Lock()
m.ensureReadyLocked()
data, ok := m.rankDataLocked(leaderboardID)
m.mu.Unlock()
if !ok {
return 0, false, ErrLeaderboardNotFound
}
record, ok := recordWithRank(data.definition, data.records, ownerID)
return record.Rank, ok, nil
}
func (m *Manager) Reset(ctx context.Context, leaderboardID string) (Definition, error) {
if err := ctxErr(ctx); err != nil {
return Definition{}, err
}
if m == nil {
return Definition{}, ErrManagerRequired
}
leaderboardID = strings.TrimSpace(leaderboardID)
if leaderboardID == "" {
return Definition{}, ErrLeaderboardNotFound
}
m.mu.Lock()
defer m.mu.Unlock()
m.ensureReadyLocked()
definition, ok := m.definitions[leaderboardID]
if !ok {
return Definition{}, ErrLeaderboardNotFound
}
before := m.snapshotStateLocked()
definition.UpdatedAt = m.now().UTC()
m.definitions[leaderboardID] = definition
m.records[leaderboardID] = make(map[string]Record)
if err := m.appendHistoryLocked(ctx, HistoryActionReset, definition.ID, nil, &definition, definition.UpdatedAt); err != nil {
m.restoreStateLocked(before)
return Definition{}, err
}
return cloneDefinition(definition), nil
}
func (m *Manager) Capture(ctx context.Context, leaderboardID string, options CaptureOptions) (Capture, error) {
if err := ctxErr(ctx); err != nil {
return Capture{}, err
}
if m == nil {
return Capture{}, ErrManagerRequired
}
leaderboardID = strings.TrimSpace(leaderboardID)
if leaderboardID == "" {
return Capture{}, ErrLeaderboardNotFound
}
normalized := normCaptureOpts(options)
m.mu.Lock()
defer m.mu.Unlock()
m.ensureReadyLocked()
if _, ok := m.definitions[leaderboardID]; !ok {
return Capture{}, ErrLeaderboardNotFound
}
ranked := m.rankedRecordsLocked(leaderboardID)
limit := normalized.Limit
if limit > len(ranked) {
limit = len(ranked)
}
capture := Capture{
LeaderboardID: leaderboardID,
Records: cloneRecords(ranked[:limit]),
RecordCount: len(ranked),
Reason: normalized.Reason,
OperatorID: normalized.OperatorID,
RequestID: normalized.RequestID,
Metadata: cloneStringMap(normalized.Metadata),
CapturedAt: m.now().UTC(),
}
if err := m.appendHistoryEntry(ctx, HistoryEntry{
Action: HistoryActionCapture,
LeaderboardID: leaderboardID,
Records: capture.Records,
Reason: capture.Reason,
OperatorID: capture.OperatorID,
RequestID: capture.RequestID,
Metadata: capture.Metadata,
At: capture.CapturedAt,
}); err != nil {
return Capture{}, err
}
return cloneCapture(capture), nil
}
func (m *Manager) History(ctx context.Context, leaderboardID string, limit int) ([]HistoryEntry, error) {
if err := ctxErr(ctx); err != nil {
return nil, err
}
if m == nil {
return nil, ErrManagerRequired
}
leaderboardID = strings.TrimSpace(leaderboardID)
if leaderboardID == "" {
return nil, ErrLeaderboardNotFound
}
if limit <= 0 {
limit = defaultListLimit
}
if limit > maxListLimit {
limit = maxListLimit
}
m.mu.Lock()
m.ensureReadyLocked()
store := m.hist
strict := m.historyStrict
m.mu.Unlock()
if store == nil {
return []HistoryEntry{}, nil
}
history, err := store.List(ctx, leaderboardID, limit)
if err != nil {
m.mu.Lock()
m.historyErrors++
m.mu.Unlock()
if strict {
return nil, err
}
return []HistoryEntry{}, nil
}
return history, nil
}
func (m *Manager) Snapshot() Snapshot {
if m == nil {
return Snapshot{}
}
m.mu.Lock()
defer m.mu.Unlock()
m.ensureReadyLocked()
snapshot := Snapshot{
Name: m.name,
LeaderboardCount: len(m.definitions),
RecordsByBoard: make(map[string]int, len(m.records)),
HistoryStoreErrors: m.historyErrors + intFromU64Sat(historyErrors(m.hist)),
}
for id, records := range m.records {
if len(records) == 0 {
continue
}
snapshot.RecordCount += len(records)
snapshot.RecordsByBoard[id] = len(records)
}
if len(snapshot.RecordsByBoard) == 0 {
snapshot.RecordsByBoard = nil
}
return snapshot
}
// Flush 刷新排行榜历史异步队列。
func (m *Manager) Flush(ctx context.Context) error {
if m == nil {
return nil
}
m.mu.Lock()
m.ensureReadyLocked()
store := m.hist
m.mu.Unlock()
return flushHistory(ctx, store)
}
// Close 关闭排行榜历史异步队列。
func (m *Manager) Close(ctx context.Context) error {
if m == nil {
return nil
}
m.mu.Lock()
m.ensureReadyLocked()
store := m.hist
m.mu.Unlock()
return closeHistory(ctx, store)
}
func (m *Manager) ensureReadyLocked() {
if m.name == "" {
m.name = "leaderboard"
}
if m.now == nil {
m.now = time.Now
}
if m.definitions == nil {
m.definitions = make(map[string]Definition)
}
if m.records == nil {
m.records = make(map[string]map[string]Record)
}
if m.repairs == nil {
m.repairs = make(map[string]RepairReceipt)
}
}
func (m *Manager) snapshotStateLocked() managerStateSnapshot {
return managerStateSnapshot{
definitions: cloneDefinitionMap(m.definitions),
records: cloneRecordMap(m.records),
repairs: cloneRepairMap(m.repairs),
}
}
func (m *Manager) restoreStateLocked(snapshot managerStateSnapshot) {
m.definitions = cloneDefinitionMap(snapshot.definitions)
m.records = cloneRecordMap(snapshot.records)
m.repairs = cloneRepairMap(snapshot.repairs)
}
func (m *Manager) normDefinitionLocked(definition Definition) (Definition, error) {
definition.ID = strings.TrimSpace(definition.ID)
if definition.ID == "" {
definition.ID = m.nextIDLocked("leaderboard")
}
definition.Title = strings.TrimSpace(definition.Title)
sortOrder, err := normalizeSortOrder(definition.SortOrder)
if err != nil {
return Definition{}, err
}
operator, err := normalizeOperator(definition.Operator)
if err != nil {
return Definition{}, err
}
definition.SortOrder = sortOrder
definition.Operator = operator
if definition.MaxSize < 0 {
return Definition{}, fmt.Errorf("%w: max_size must not be negative", ErrInvalidDefinition)
}
now := m.now().UTC()
if definition.CreatedAt.IsZero() {
definition.CreatedAt = now
} else {
definition.CreatedAt = definition.CreatedAt.UTC()
}
if definition.UpdatedAt.IsZero() {
definition.UpdatedAt = definition.CreatedAt
} else {
definition.UpdatedAt = definition.UpdatedAt.UTC()
}
definition.Metadata = cloneStringMap(definition.Metadata)
return definition, nil
}
func (m *Manager) rankedRecordsLocked(leaderboardID string) []Record {
data, ok := m.rankDataLocked(leaderboardID)
if !ok {
return nil
}
return rankRecords(data.definition, data.records)
}
func (m *Manager) rankDataLocked(leaderboardID string) (boardRankData, bool) {
definition, ok := m.definitions[leaderboardID]
if !ok {
return boardRankData{}, false
}
return boardRankData{
definition: cloneDefinition(definition),
records: cloneRecordSet(m.records[leaderboardID]),
}, true
}
func rankRecords(definition Definition, records map[string]Record) []Record {
out := make([]Record, 0, len(records))
for _, record := range records {
out = append(out, cloneRecord(record))
}
sort.SliceStable(out, func(i, j int) bool {
return compareRecords(out[i], out[j], definition.SortOrder) < 0
})
for i := range out {
out[i].Rank = i + 1
}
return out
}
func (m *Manager) recordWithRankLocked(leaderboardID string, ownerID string) (Record, bool) {
ranked := m.rankedRecordsLocked(leaderboardID)
return recordFromRanked(ranked, ownerID)
}
func recordWithRank(definition Definition, records map[string]Record, ownerID string) (Record, bool) {
return recordFromRanked(rankRecords(definition, records), ownerID)
}
func recordFromRanked(ranked []Record, ownerID string) (Record, bool) {
for _, record := range ranked {
if record.OwnerID == ownerID {
return cloneRecord(record), true
}
}
return Record{}, false
}
func (m *Manager) trimLocked(definition Definition) []Record {
if definition.MaxSize <= 0 {
return nil
}
ranked := m.rankedRecordsLocked(definition.ID)
if len(ranked) <= definition.MaxSize {
return nil
}
keep := make(map[string]struct{}, definition.MaxSize)
for _, record := range ranked[:definition.MaxSize] {
keep[record.OwnerID] = struct{}{}
}
trimmed := cloneRecords(ranked[definition.MaxSize:])
for ownerID := range m.records[definition.ID] {
if _, ok := keep[ownerID]; !ok {
delete(m.records[definition.ID], ownerID)
}
}
return trimmed
}
func (m *Manager) appendHistoryLocked(ctx context.Context, action string, leaderboardID string, record *Record, definition *Definition, at time.Time) error {
if at.IsZero() {
at = m.now().UTC()
} else {
at = at.UTC()
}
entry := HistoryEntry{
Action: action,
LeaderboardID: strings.TrimSpace(leaderboardID),
At: at,
}
if record != nil {
cloned := cloneRecord(*record)
entry.OwnerID = cloned.OwnerID
entry.Record = &cloned
}
if definition != nil {
cloned := cloneDefinition(*definition)
entry.Definition = &cloned
}
return m.appendHistoryEntry(ctx, entry)
}
func (m *Manager) appendHistoryEntry(ctx context.Context, entry HistoryEntry) error {
if m.hist == nil {
return nil
}
entry = cloneHistoryEntry(entry)
entry.LeaderboardID = strings.TrimSpace(entry.LeaderboardID)
entry.Action = strings.TrimSpace(entry.Action)
entry.OwnerID = strings.TrimSpace(entry.OwnerID)
entry.Reason = strings.TrimSpace(entry.Reason)
entry.OperatorID = strings.TrimSpace(entry.OperatorID)
entry.RequestID = strings.TrimSpace(entry.RequestID)
entry.Metadata = cloneStringMap(entry.Metadata)
if entry.At.IsZero() {
entry.At = m.now().UTC()
} else {
entry.At = entry.At.UTC()
}
if err := m.hist.Append(ctx, entry); err != nil {
m.historyErrors++
if m.historyStrict {
return err
}
}
return nil
}
func (m *Manager) nextIDLocked(prefix string) string {
if m.gen != nil {
if id := strings.TrimSpace(m.gen(prefix)); id != "" {
return id
}
}
m.next++
return fmt.Sprintf("%s-%d", prefix, m.next)
}