DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,403 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AsyncStoreOptions 配置异步写后队列、重试和死信存储。
|
||||
type AsyncStoreOptions[T any, F comparable] struct {
|
||||
FlushInterval time.Duration
|
||||
MaxPending int
|
||||
RetryBackoff time.Duration
|
||||
MaxRetries int
|
||||
DeadLetterLimit int
|
||||
DeadLetterStore DeadLetterStore[T, F]
|
||||
|
||||
RecordKey KeyFunc[T]
|
||||
NormalizeKey func(string) string
|
||||
Clone CloneFunc[T]
|
||||
AllFields func() []F
|
||||
NormalizeFields func([]F) []F
|
||||
SaveFields SaveFieldsFunc[T, F]
|
||||
SaveBatch SaveFieldBatchFunc[T, F]
|
||||
AutoExpire bool
|
||||
AutoExpireTTL time.Duration
|
||||
Expire ExpireFunc[T]
|
||||
ClosedError error
|
||||
}
|
||||
|
||||
// AsyncStoreStats 描述异步存储当前运行状态。
|
||||
type AsyncStoreStats struct {
|
||||
Pending int `json:"pending"`
|
||||
PendingDue int `json:"pending_due"`
|
||||
DeadLetters int `json:"dead_letters"`
|
||||
Closed bool `json:"closed"`
|
||||
FlushInterval string `json:"flush_interval"`
|
||||
RetryBackoff string `json:"retry_backoff"`
|
||||
MaxPending int `json:"max_pending"`
|
||||
MaxRetries int `json:"max_retries"`
|
||||
AutoExpireTTL string `json:"auto_expire_ttl,omitempty"`
|
||||
DeadLetterLimit int `json:"dead_letter_limit"`
|
||||
DeadLetterStore string `json:"dead_letter_store,omitempty"`
|
||||
DeadLetterStoreError string `json:"dead_letter_store_error,omitempty"`
|
||||
}
|
||||
|
||||
const defAsyncFlush = 5 * time.Second
|
||||
|
||||
// AsyncStore 用内存队列包装同步 Store,实现异步刷盘和死信兜底。
|
||||
type AsyncStore[T any, F comparable] struct {
|
||||
base Store[T]
|
||||
flushInterval time.Duration
|
||||
maxPending int
|
||||
allFields func() []F
|
||||
closedError error
|
||||
|
||||
mu sync.Mutex
|
||||
closed bool
|
||||
|
||||
queue *asyncPendingQueue[T, F]
|
||||
retry asyncRetryPolicy[T, F]
|
||||
deadLetters *asyncDeadLetterSink[T, F]
|
||||
flusher *asyncFlusher[T, F]
|
||||
|
||||
done chan struct{}
|
||||
flushNow chan struct{}
|
||||
stopped chan struct{}
|
||||
stopOnce sync.Once
|
||||
}
|
||||
|
||||
// NewAsyncStore 用写后队列包装同步 Store。
|
||||
// Load 会先读 pending 和 dead-letter,再读 base store;这样即使后台刷盘还没落到持久层,
|
||||
// 调用方也能看到自己已被接受的最新 mutation。
|
||||
func NewAsyncStore[T any, F comparable](base Store[T], options AsyncStoreOptions[T, F]) (*AsyncStore[T, F], error) {
|
||||
if base == nil {
|
||||
return nil, fmt.Errorf("db.AsyncStore requires a base store")
|
||||
}
|
||||
if options.AutoExpire && options.AutoExpireTTL <= 0 {
|
||||
return nil, fmt.Errorf("db.AsyncStore auto expire ttl must be positive")
|
||||
}
|
||||
return newAsyncStore(base, options), nil
|
||||
}
|
||||
|
||||
// MustNewAsyncStore 创建异步存储,失败时 panic,适合启动期固定配置。
|
||||
func MustNewAsyncStore[T any, F comparable](base Store[T], options AsyncStoreOptions[T, F]) *AsyncStore[T, F] {
|
||||
store, err := NewAsyncStore(base, options)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return store
|
||||
}
|
||||
|
||||
func newAsyncStore[T any, F comparable](base Store[T], options AsyncStoreOptions[T, F]) *AsyncStore[T, F] {
|
||||
if options.RetryBackoff < 0 {
|
||||
options.RetryBackoff = time.Second
|
||||
}
|
||||
if options.DeadLetterLimit <= 0 {
|
||||
options.DeadLetterLimit = 128
|
||||
}
|
||||
if options.Clone == nil {
|
||||
options.Clone = IdentityClone[T]
|
||||
}
|
||||
if options.SaveFields == nil {
|
||||
options.SaveFields = func(ctx context.Context, store Store[T], record T, fields ...F) error {
|
||||
return SaveFields(ctx, store, record, options.NormalizeFields, fields...)
|
||||
}
|
||||
}
|
||||
if options.SaveBatch == nil {
|
||||
if _, ok := base.(BatchFieldStore[T, F]); ok {
|
||||
options.SaveBatch = func(ctx context.Context, store Store[T], saves []FieldSave[T, F]) error {
|
||||
return SaveFieldBatch(ctx, store, options.NormalizeFields, saves)
|
||||
}
|
||||
}
|
||||
}
|
||||
if options.AutoExpire && options.Expire == nil {
|
||||
options.Expire = func(ctx context.Context, store Store[T], key string, ttl time.Duration) error {
|
||||
return Expire(ctx, store, key, ttl)
|
||||
}
|
||||
}
|
||||
if options.ClosedError == nil {
|
||||
options.ClosedError = ErrAsyncStoreClosed
|
||||
}
|
||||
|
||||
queue := newAsyncPendingQueue(options.RecordKey, options.NormalizeKey, options.Clone, options.NormalizeFields)
|
||||
retry := asyncRetryPolicy[T, F]{backoff: options.RetryBackoff, maxRetries: options.MaxRetries}
|
||||
deadLetters := newAsyncDeadSink(asyncDeadSinkOpts[T, F]{
|
||||
Limit: options.DeadLetterLimit,
|
||||
Store: options.DeadLetterStore,
|
||||
RecordKey: options.RecordKey,
|
||||
NormalizeKey: options.NormalizeKey,
|
||||
Clone: options.Clone,
|
||||
NormalizeFields: options.NormalizeFields,
|
||||
})
|
||||
flusher := newAsyncFlusher(asyncFlusherOptions[T, F]{
|
||||
Base: base,
|
||||
Queue: queue,
|
||||
Retry: retry,
|
||||
DeadLetters: deadLetters,
|
||||
Clone: options.Clone,
|
||||
NormalizeFields: options.NormalizeFields,
|
||||
SaveFields: options.SaveFields,
|
||||
SaveBatch: options.SaveBatch,
|
||||
AutoExpireTTL: options.AutoExpireTTL,
|
||||
Expire: options.Expire,
|
||||
})
|
||||
|
||||
store := &AsyncStore[T, F]{
|
||||
base: base,
|
||||
flushInterval: options.FlushInterval,
|
||||
maxPending: options.MaxPending,
|
||||
allFields: options.AllFields,
|
||||
closedError: options.ClosedError,
|
||||
queue: queue,
|
||||
retry: retry,
|
||||
deadLetters: deadLetters,
|
||||
flusher: flusher,
|
||||
done: make(chan struct{}),
|
||||
flushNow: make(chan struct{}, 1),
|
||||
stopped: make(chan struct{}),
|
||||
}
|
||||
store.deadLetters.Load()
|
||||
go store.run()
|
||||
return store
|
||||
}
|
||||
|
||||
// Load 优先读取 pending 和 dead-letter,再读取底层存储。
|
||||
func (s *AsyncStore[T, F]) Load(ctx context.Context, accountID string) (T, bool, error) {
|
||||
ctx = contextOrBackground(ctx)
|
||||
accountID = s.queue.NormalizeRecordKey(accountID)
|
||||
if pending, ok := s.queue.Get(accountID); ok {
|
||||
return s.queue.Clone(pending.profile), true, nil
|
||||
}
|
||||
if dead, ok := s.deadLetters.Get(accountID); ok {
|
||||
return s.queue.Clone(dead.Profile), true, nil
|
||||
}
|
||||
return s.base.Load(ctx, accountID)
|
||||
}
|
||||
|
||||
// Check 检查异步存储、底层存储和死信存储状态。
|
||||
func (s *AsyncStore[T, F]) Check(ctx context.Context) error {
|
||||
ctx = contextOrBackground(ctx)
|
||||
s.mu.Lock()
|
||||
closed := s.closed
|
||||
base := s.base
|
||||
closedError := s.closedError
|
||||
s.mu.Unlock()
|
||||
|
||||
if closed {
|
||||
return closedError
|
||||
}
|
||||
if deadLetterStoreError := s.deadLetters.LastError(); deadLetterStoreError != "" {
|
||||
return fmt.Errorf("dead letter store: %s", deadLetterStoreError)
|
||||
}
|
||||
if err := Check(ctx, base); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.deadLetters.CheckStore(ctx)
|
||||
}
|
||||
|
||||
// Save 保存完整记录字段。
|
||||
func (s *AsyncStore[T, F]) Save(ctx context.Context, profile T) error {
|
||||
fields := s.allFieldsForSave()
|
||||
if len(fields) == 0 {
|
||||
return ErrAllFieldsRequired
|
||||
}
|
||||
return s.SaveFields(ctx, profile, fields...)
|
||||
}
|
||||
|
||||
// SaveFields 把局部字段写入 pending 队列。
|
||||
func (s *AsyncStore[T, F]) SaveFields(ctx context.Context, profile T, fields ...F) error {
|
||||
ctx = contextOrBackground(ctx)
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
accountID, err := s.queue.RecordKey(profile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fields = s.queue.NormalizeFields(fields)
|
||||
if len(fields) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
save := pendingSave[T, F]{
|
||||
profile: s.queue.Clone(profile),
|
||||
fields: fields,
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
if s.closed {
|
||||
s.mu.Unlock()
|
||||
return s.closedError
|
||||
}
|
||||
pending := s.queue.Add(accountID, save)
|
||||
s.mu.Unlock()
|
||||
|
||||
if s.maxPending > 0 && pending >= s.maxPending {
|
||||
s.requestFlush()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Flush 强制刷出当前可写 pending 队列。
|
||||
func (s *AsyncStore[T, F]) Flush(ctx context.Context) error {
|
||||
ctx = contextOrBackground(ctx)
|
||||
return s.flusher.Flush(ctx, true)
|
||||
}
|
||||
|
||||
// Stats 返回异步队列、死信和重试状态。
|
||||
func (s *AsyncStore[T, F]) Stats() AsyncStoreStats {
|
||||
now := time.Now().UTC()
|
||||
pending, pendingDue := s.queue.Stats(now)
|
||||
|
||||
s.mu.Lock()
|
||||
closed := s.closed
|
||||
s.mu.Unlock()
|
||||
|
||||
return AsyncStoreStats{
|
||||
Pending: pending,
|
||||
PendingDue: pendingDue,
|
||||
DeadLetters: s.deadLetters.Count(),
|
||||
Closed: closed,
|
||||
FlushInterval: s.flushInterval.String(),
|
||||
RetryBackoff: s.retry.backoff.String(),
|
||||
MaxPending: s.maxPending,
|
||||
MaxRetries: s.retry.maxRetries,
|
||||
AutoExpireTTL: asyncDurationString(s.flusher.AutoExpireTTL()),
|
||||
DeadLetterLimit: s.deadLetters.Limit(),
|
||||
DeadLetterStore: s.deadLetters.StoreName(),
|
||||
DeadLetterStoreError: s.deadLetters.LastError(),
|
||||
}
|
||||
}
|
||||
|
||||
// DeadLetters 返回当前死信快照。
|
||||
func (s *AsyncStore[T, F]) DeadLetters() []AsyncStoreDeadLetter[T, F] {
|
||||
return s.deadLetters.List()
|
||||
}
|
||||
|
||||
// RequeueDeadLetter 把指定死信重新放回 pending 队列。
|
||||
func (s *AsyncStore[T, F]) RequeueDeadLetter(accountID string) bool {
|
||||
accountID = s.queue.NormalizeRecordKey(accountID)
|
||||
s.mu.Lock()
|
||||
if s.closed {
|
||||
s.mu.Unlock()
|
||||
return false
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
dead, ok := s.deadLetters.Take(accountID)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
fields := s.queue.NormalizeFields(dead.Fields)
|
||||
if len(fields) == 0 {
|
||||
fields = s.allFieldsForSave()
|
||||
}
|
||||
save := pendingSave[T, F]{
|
||||
profile: s.queue.Clone(dead.Profile),
|
||||
fields: fields,
|
||||
attempts: dead.Attempts,
|
||||
lastError: dead.Error,
|
||||
firstFailedAt: dead.FirstFailedAt,
|
||||
lastFailedAt: dead.LastFailedAt,
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.closed {
|
||||
if err := s.deadLetters.Add(save); err != nil {
|
||||
s.deadLetters.restoreMemory(dead)
|
||||
}
|
||||
return false
|
||||
}
|
||||
s.queue.Requeue(accountID, save)
|
||||
return true
|
||||
}
|
||||
|
||||
// Close 停止后台刷盘并做最后一次 Flush。
|
||||
func (s *AsyncStore[T, F]) Close(ctx context.Context) error {
|
||||
ctx, cancel := asyncCloseCtx(ctx)
|
||||
defer cancel()
|
||||
s.mu.Lock()
|
||||
s.closed = true
|
||||
s.mu.Unlock()
|
||||
|
||||
s.stopOnce.Do(func() {
|
||||
close(s.done)
|
||||
})
|
||||
select {
|
||||
case <-s.stopped:
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
return errors.Join(s.Flush(ctx), CloseOrFlush(ctx, s.base))
|
||||
}
|
||||
|
||||
func asyncCloseCtx(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if _, ok := ctx.Deadline(); ok {
|
||||
return ctx, func() {}
|
||||
}
|
||||
return context.WithTimeout(ctx, defAsyncFlush)
|
||||
}
|
||||
|
||||
func (s *AsyncStore[T, F]) run() {
|
||||
defer close(s.stopped)
|
||||
if s.flushInterval <= 0 {
|
||||
for {
|
||||
select {
|
||||
case <-s.flushNow:
|
||||
s.flushBackground()
|
||||
case <-s.done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(s.flushInterval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
s.flushBackground()
|
||||
case <-s.flushNow:
|
||||
s.flushBackground()
|
||||
case <-s.done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AsyncStore[T, F]) requestFlush() {
|
||||
select {
|
||||
case s.flushNow <- struct{}{}:
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AsyncStore[T, F]) flushBackground() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defAsyncFlush)
|
||||
_ = s.flusher.Flush(ctx, false)
|
||||
cancel()
|
||||
}
|
||||
|
||||
func (s *AsyncStore[T, F]) allFieldsForSave() []F {
|
||||
if s.allFields == nil {
|
||||
return nil
|
||||
}
|
||||
return s.queue.NormalizeFields(s.allFields())
|
||||
}
|
||||
|
||||
func asyncDurationString(duration time.Duration) string {
|
||||
if duration <= 0 {
|
||||
return ""
|
||||
}
|
||||
return duration.String()
|
||||
}
|
||||
Reference in new issue
Block a user