服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
247 lines
5.5 KiB
Go
247 lines
5.5 KiB
Go
package leaderboard
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"longheng.io/server/internal/platform/eventlog"
|
|
)
|
|
|
|
var (
|
|
ErrNilEventLogHistoryStore = errors.New("leaderboard eventlog history store is required")
|
|
ErrNilEventLog = errors.New("leaderboard eventlog is required")
|
|
)
|
|
|
|
const (
|
|
LeaderboardEventStream = "leaderboard"
|
|
DefaultLeaderboardHistoryEventType = "history.appended"
|
|
histUndoTimeout = 3 * time.Second
|
|
)
|
|
|
|
type HistoryStoreOptions struct {
|
|
Stream string
|
|
Type string
|
|
Strict bool
|
|
}
|
|
|
|
type EventLogHistoryStore struct {
|
|
next HistoryStore
|
|
log *eventlog.Log
|
|
stream string
|
|
typ string
|
|
strict bool
|
|
errors uint64
|
|
}
|
|
|
|
type evErrorReporter interface {
|
|
EventLogErrors() uint64
|
|
}
|
|
|
|
type histErrorReporter interface {
|
|
HistoryErrors() uint64
|
|
}
|
|
|
|
type histUndo func(context.Context) error
|
|
|
|
type histUndoer interface {
|
|
appendWithRollback(context.Context, HistoryEntry) (histUndo, error)
|
|
}
|
|
|
|
func NewEventLogHistoryStore(next HistoryStore, log *eventlog.Log, options HistoryStoreOptions) (*EventLogHistoryStore, error) {
|
|
if next == nil {
|
|
return nil, ErrNilEventLogHistoryStore
|
|
}
|
|
if log == nil {
|
|
return nil, ErrNilEventLog
|
|
}
|
|
stream := strings.TrimSpace(options.Stream)
|
|
if stream == "" {
|
|
stream = LeaderboardEventStream
|
|
}
|
|
typ := strings.TrimSpace(options.Type)
|
|
if typ == "" {
|
|
typ = DefaultLeaderboardHistoryEventType
|
|
}
|
|
return &EventLogHistoryStore{
|
|
next: next,
|
|
log: log,
|
|
stream: stream,
|
|
typ: typ,
|
|
strict: options.Strict,
|
|
}, nil
|
|
}
|
|
|
|
type eventHistoryStore struct {
|
|
log *eventlog.Log
|
|
stream string
|
|
typ string
|
|
}
|
|
|
|
func newEventHistoryStore(log *eventlog.Log, stream string, typ string) *eventHistoryStore {
|
|
return &eventHistoryStore{
|
|
log: log,
|
|
stream: normalizeEventStream(stream),
|
|
typ: normHistoryEventType(typ),
|
|
}
|
|
}
|
|
|
|
func (s *EventLogHistoryStore) Append(ctx context.Context, entry HistoryEntry) error {
|
|
if err := ctxErr(ctx); err != nil {
|
|
return err
|
|
}
|
|
if s == nil || s.next == nil {
|
|
return ErrNilEventLogHistoryStore
|
|
}
|
|
if s.log == nil {
|
|
return ErrNilEventLog
|
|
}
|
|
entry = normHistoryEntry(entry)
|
|
payload, err := json.Marshal(entry)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rollback, err := appendHist(ctx, s.next, entry)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = s.log.Append(ctx, eventlog.Event{
|
|
Stream: s.stream,
|
|
Type: s.typ,
|
|
AggregateID: entry.LeaderboardID,
|
|
IdempotencyKey: historyIdemKey(entry, payload),
|
|
Payload: payload,
|
|
Headers: map[string]string{
|
|
"action": entry.Action,
|
|
"leaderboard_id": entry.LeaderboardID,
|
|
"owner_id": entry.OwnerID,
|
|
},
|
|
})
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
atomic.AddUint64(&s.errors, 1)
|
|
if s.strict {
|
|
if rollbackErr := undoHist(ctx, rollback); rollbackErr != nil {
|
|
return errors.Join(err, rollbackErr)
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *EventLogHistoryStore) List(ctx context.Context, leaderboardID string, limit int) ([]HistoryEntry, error) {
|
|
if s == nil || s.next == nil {
|
|
return nil, ErrNilEventLogHistoryStore
|
|
}
|
|
return s.next.List(ctx, leaderboardID, limit)
|
|
}
|
|
|
|
func (s *EventLogHistoryStore) EventLogErrors() uint64 {
|
|
if s == nil {
|
|
return 0
|
|
}
|
|
return atomic.LoadUint64(&s.errors)
|
|
}
|
|
|
|
func (s *eventHistoryStore) Append(ctx context.Context, entry HistoryEntry) error {
|
|
if err := ctxErr(ctx); err != nil {
|
|
return err
|
|
}
|
|
if s == nil || s.log == nil {
|
|
return ErrNilEventLog
|
|
}
|
|
entry = normHistoryEntry(entry)
|
|
payload, err := json.Marshal(entry)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = s.log.Append(ctx, eventlog.Event{
|
|
Stream: s.stream,
|
|
Type: s.typ,
|
|
AggregateID: entry.LeaderboardID,
|
|
IdempotencyKey: historyIdemKey(entry, payload),
|
|
Payload: payload,
|
|
Headers: map[string]string{
|
|
"action": entry.Action,
|
|
"leaderboard_id": entry.LeaderboardID,
|
|
"owner_id": entry.OwnerID,
|
|
},
|
|
})
|
|
return err
|
|
}
|
|
|
|
func (s *eventHistoryStore) List(context.Context, string, int) ([]HistoryEntry, error) {
|
|
return nil, ErrNilEventLogHistoryStore
|
|
}
|
|
|
|
func historyStoreErrors(store HistoryStore) uint64 {
|
|
reporter, ok := store.(evErrorReporter)
|
|
if !ok {
|
|
return 0
|
|
}
|
|
return reporter.EventLogErrors()
|
|
}
|
|
|
|
func historyErrors(store HistoryStore) uint64 {
|
|
reporter, ok := store.(histErrorReporter)
|
|
if !ok {
|
|
return 0
|
|
}
|
|
return reporter.HistoryErrors()
|
|
}
|
|
|
|
func appendHist(ctx context.Context, store HistoryStore, entry HistoryEntry) (histUndo, error) {
|
|
if rollbacker, ok := store.(histUndoer); ok {
|
|
return rollbacker.appendWithRollback(ctx, entry)
|
|
}
|
|
if err := store.Append(ctx, entry); err != nil {
|
|
return nil, err
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func undoHist(parent context.Context, rollback histUndo) error {
|
|
if rollback == nil {
|
|
return nil
|
|
}
|
|
base := context.Background()
|
|
if parent != nil {
|
|
base = context.WithoutCancel(parent)
|
|
}
|
|
ctx, cancel := context.WithTimeout(base, histUndoTimeout)
|
|
defer cancel()
|
|
return rollback(ctx)
|
|
}
|
|
|
|
func normHistoryEntry(entry HistoryEntry) HistoryEntry {
|
|
entry = cloneHistoryEntry(entry)
|
|
entry.Action = strings.TrimSpace(entry.Action)
|
|
entry.LeaderboardID = strings.TrimSpace(entry.LeaderboardID)
|
|
entry.OwnerID = strings.TrimSpace(entry.OwnerID)
|
|
if entry.At.IsZero() {
|
|
entry.At = time.Now().UTC()
|
|
} else {
|
|
entry.At = entry.At.UTC()
|
|
}
|
|
return entry
|
|
}
|
|
|
|
func historyIdemKey(entry HistoryEntry, payload []byte) string {
|
|
raw := fmt.Sprintf("%s|%s|%s|%d|%x",
|
|
entry.LeaderboardID,
|
|
entry.Action,
|
|
entry.OwnerID,
|
|
entry.At.UTC().UnixNano(),
|
|
payload,
|
|
)
|
|
hash := sha256.Sum256([]byte(raw))
|
|
return "leaderboard:history:" + hex.EncodeToString(hash[:16])
|
|
}
|