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,433 @@
|
||||
package leaderboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNilHistoryStoreDB = errors.New("leaderboard history sql db is required")
|
||||
ErrInvalidTableName = errors.New("leaderboard history table name is invalid")
|
||||
ErrHistoryAppendRollbackMissed = errors.New("leaderboard history append rollback missed")
|
||||
ErrSQLHistoryRows = errors.New("leaderboard sql history rows are required")
|
||||
)
|
||||
|
||||
const defaultHistoryTable = "leaderboard_history"
|
||||
|
||||
type MemoryHistoryStore struct {
|
||||
mu sync.Mutex
|
||||
limit int
|
||||
entries map[string][]HistoryEntry
|
||||
}
|
||||
|
||||
func NewMemoryHistoryStore(limit int) *MemoryHistoryStore {
|
||||
return &MemoryHistoryStore{
|
||||
limit: limit,
|
||||
entries: make(map[string][]HistoryEntry),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MemoryHistoryStore) Append(ctx context.Context, entry HistoryEntry) error {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if s == nil {
|
||||
return errors.New("leaderboard memory history store is nil")
|
||||
}
|
||||
entry, err := normHistStore(entry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.mu.Lock()
|
||||
s.appendLocked(entry)
|
||||
s.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MemoryHistoryStore) appendWithRollback(ctx context.Context, entry HistoryEntry) (histUndo, error) {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if s == nil {
|
||||
return nil, errors.New("leaderboard memory history store is nil")
|
||||
}
|
||||
entry, err := normHistStore(entry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
leaderboardID := entry.LeaderboardID
|
||||
s.mu.Lock()
|
||||
before := cloneHistoryEntries(s.entries[leaderboardID])
|
||||
s.appendLocked(entry)
|
||||
s.mu.Unlock()
|
||||
return func(ctx context.Context) error {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
s.mu.Lock()
|
||||
if len(before) == 0 {
|
||||
delete(s.entries, leaderboardID)
|
||||
} else {
|
||||
s.entries[leaderboardID] = cloneHistoryEntries(before)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
return nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *MemoryHistoryStore) appendLocked(entry HistoryEntry) {
|
||||
if s.entries == nil {
|
||||
s.entries = make(map[string][]HistoryEntry)
|
||||
}
|
||||
s.entries[entry.LeaderboardID] = append(s.entries[entry.LeaderboardID], cloneHistoryEntry(entry))
|
||||
if s.limit > 0 && len(s.entries[entry.LeaderboardID]) > s.limit {
|
||||
s.entries[entry.LeaderboardID] = append([]HistoryEntry(nil), s.entries[entry.LeaderboardID][len(s.entries[entry.LeaderboardID])-s.limit:]...)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MemoryHistoryStore) List(ctx context.Context, leaderboardID string, limit int) ([]HistoryEntry, error) {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if s == nil {
|
||||
return nil, errors.New("leaderboard memory history store is nil")
|
||||
}
|
||||
leaderboardID = strings.TrimSpace(leaderboardID)
|
||||
if leaderboardID == "" {
|
||||
return nil, ErrLeaderboardNotFound
|
||||
}
|
||||
if limit <= 0 {
|
||||
limit = defaultListLimit
|
||||
}
|
||||
s.mu.Lock()
|
||||
entries := s.entries[leaderboardID]
|
||||
start := 0
|
||||
if limit > 0 && len(entries) > limit {
|
||||
start = len(entries) - limit
|
||||
}
|
||||
out := cloneHistoryEntries(entries[start:])
|
||||
s.mu.Unlock()
|
||||
return out, nil
|
||||
}
|
||||
|
||||
type SQLHistoryRows interface {
|
||||
Next() bool
|
||||
Scan(...any) error
|
||||
Close() error
|
||||
Err() error
|
||||
}
|
||||
|
||||
type SQLHistoryDB interface {
|
||||
ExecContext(context.Context, string, ...any) (sql.Result, error)
|
||||
QueryHistoryContext(context.Context, string, ...any) (SQLHistoryRows, error)
|
||||
}
|
||||
|
||||
type SQLHistoryDBAdapter struct {
|
||||
DB *sql.DB
|
||||
}
|
||||
|
||||
func (a SQLHistoryDBAdapter) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) {
|
||||
if a.DB == nil {
|
||||
return nil, ErrNilHistoryStoreDB
|
||||
}
|
||||
return a.DB.ExecContext(ctx, query, args...)
|
||||
}
|
||||
|
||||
func (a SQLHistoryDBAdapter) QueryHistoryContext(ctx context.Context, query string, args ...any) (SQLHistoryRows, error) { //nolint:rowserrcheck // 适配层只返回 rows,调用方负责遍历结束后的 Err 检查。
|
||||
if a.DB == nil {
|
||||
return nil, ErrNilHistoryStoreDB
|
||||
}
|
||||
return a.DB.QueryContext(ctx, query, args...) //nolint:rowserrcheck // rows 返回后由上层统一关闭并检查 Err。
|
||||
}
|
||||
|
||||
type SQLHistoryStoreOptions struct {
|
||||
Table string
|
||||
}
|
||||
|
||||
type SQLHistoryStore struct {
|
||||
db SQLHistoryDB
|
||||
table string
|
||||
}
|
||||
|
||||
func NewSQLHistoryStore(db SQLHistoryDB, options SQLHistoryStoreOptions) (*SQLHistoryStore, error) {
|
||||
if db == nil {
|
||||
return nil, ErrNilHistoryStoreDB
|
||||
}
|
||||
table, err := normHistoryTable(options.Table)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &SQLHistoryStore{db: db, table: table}, nil
|
||||
}
|
||||
|
||||
func NewSQLHistoryStoreFromDB(db *sql.DB, options SQLHistoryStoreOptions) (*SQLHistoryStore, error) {
|
||||
if db == nil {
|
||||
return nil, ErrNilHistoryStoreDB
|
||||
}
|
||||
return NewSQLHistoryStore(SQLHistoryDBAdapter{DB: db}, options)
|
||||
}
|
||||
|
||||
func (s *SQLHistoryStore) EnsureSchema(ctx context.Context) error {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if s == nil || s.db == nil {
|
||||
return ErrNilHistoryStoreDB
|
||||
}
|
||||
_, err := s.db.ExecContext(ctx, s.Schema())
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *SQLHistoryStore) Schema() string {
|
||||
table := quoteHistoryID(s.table)
|
||||
return fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
leaderboard_id VARCHAR(128) NOT NULL,
|
||||
owner_id VARCHAR(128) NOT NULL DEFAULT '',
|
||||
action VARCHAR(64) NOT NULL,
|
||||
record_json JSON NULL,
|
||||
definition_json JSON NULL,
|
||||
entry_json JSON NOT NULL,
|
||||
created_at DATETIME(6) NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_leaderboard_history_board_id (leaderboard_id, id),
|
||||
KEY idx_leaderboard_history_owner_id (owner_id, id),
|
||||
KEY idx_leaderboard_history_action_id (action, id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`, table)
|
||||
}
|
||||
|
||||
func (s *SQLHistoryStore) Append(ctx context.Context, entry HistoryEntry) error {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if s == nil || s.db == nil {
|
||||
return ErrNilHistoryStoreDB
|
||||
}
|
||||
_, err := s.appendWithRollback(ctx, entry)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *SQLHistoryStore) appendWithRollback(ctx context.Context, entry HistoryEntry) (histUndo, error) {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if s == nil || s.db == nil {
|
||||
return nil, ErrNilHistoryStoreDB
|
||||
}
|
||||
entry, err := normHistStore(entry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entryJSON, err := json.Marshal(entry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
recordJSON, err := nullableJSON(entry.Record)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
definitionJSON, err := nullableJSON(entry.Definition)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result, err := s.db.ExecContext(ctx,
|
||||
"INSERT INTO "+quoteHistoryID(s.table)+" (leaderboard_id, owner_id, action, record_json, definition_json, entry_json, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
entry.LeaderboardID,
|
||||
entry.OwnerID,
|
||||
entry.Action,
|
||||
recordJSON,
|
||||
definitionJSON,
|
||||
string(entryJSON),
|
||||
entry.At,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return func(ctx context.Context) error {
|
||||
return s.rollbackAppend(ctx, entry, result)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *SQLHistoryStore) rollbackAppend(ctx context.Context, entry HistoryEntry, result sql.Result) error {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if s == nil || s.db == nil {
|
||||
return ErrNilHistoryStoreDB
|
||||
}
|
||||
table := quoteHistoryID(s.table)
|
||||
if result != nil {
|
||||
if id, err := result.LastInsertId(); err == nil && id > 0 {
|
||||
deleted, err := s.db.ExecContext(ctx,
|
||||
"DELETE FROM "+table+" WHERE id = ? AND leaderboard_id = ? AND owner_id = ? AND action = ?",
|
||||
id,
|
||||
entry.LeaderboardID,
|
||||
entry.OwnerID,
|
||||
entry.Action,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return checkHistUndo(deleted)
|
||||
}
|
||||
}
|
||||
deleted, err := s.db.ExecContext(ctx,
|
||||
"DELETE FROM "+table+" WHERE id = (SELECT id FROM (SELECT id FROM "+table+" WHERE leaderboard_id = ? AND owner_id = ? AND action = ? AND created_at = ? ORDER BY id DESC LIMIT 1) AS rollback_candidate)",
|
||||
entry.LeaderboardID,
|
||||
entry.OwnerID,
|
||||
entry.Action,
|
||||
entry.At,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return checkHistUndo(deleted)
|
||||
}
|
||||
|
||||
func checkHistUndo(result sql.Result) error {
|
||||
if result == nil {
|
||||
return nil
|
||||
}
|
||||
affected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if affected == 0 {
|
||||
return ErrHistoryAppendRollbackMissed
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normHistStore(entry HistoryEntry) (HistoryEntry, error) {
|
||||
entry = cloneHistoryEntry(entry)
|
||||
entry.LeaderboardID = strings.TrimSpace(entry.LeaderboardID)
|
||||
if entry.LeaderboardID == "" {
|
||||
return HistoryEntry{}, ErrLeaderboardNotFound
|
||||
}
|
||||
if entry.At.IsZero() {
|
||||
entry.At = time.Now().UTC()
|
||||
} else {
|
||||
entry.At = entry.At.UTC()
|
||||
}
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
func (s *SQLHistoryStore) List(ctx context.Context, leaderboardID string, limit int) (out []HistoryEntry, err error) {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if s == nil || s.db == nil {
|
||||
return nil, ErrNilHistoryStoreDB
|
||||
}
|
||||
leaderboardID = strings.TrimSpace(leaderboardID)
|
||||
if leaderboardID == "" {
|
||||
return nil, ErrLeaderboardNotFound
|
||||
}
|
||||
if limit <= 0 {
|
||||
limit = defaultListLimit
|
||||
}
|
||||
if limit > maxListLimit {
|
||||
limit = maxListLimit
|
||||
}
|
||||
rows, err := s.db.QueryHistoryContext(ctx,
|
||||
"SELECT entry_json FROM "+quoteHistoryID(s.table)+" WHERE leaderboard_id = ? ORDER BY id DESC LIMIT ?",
|
||||
leaderboardID,
|
||||
limit,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if rows == nil {
|
||||
return nil, ErrSQLHistoryRows
|
||||
}
|
||||
defer closeHistRowsErr(rows, &err)
|
||||
out = make([]HistoryEntry, 0, limit)
|
||||
for rows.Next() {
|
||||
var data []byte
|
||||
if err := rows.Scan(&data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var entry HistoryEntry
|
||||
if err := json.Unmarshal(data, &entry); err != nil {
|
||||
return nil, fmt.Errorf("decode leaderboard sql history: %w", err)
|
||||
}
|
||||
out = append(out, cloneHistoryEntry(entry))
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reverseHistory(out)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func closeHistRowsErr(rows SQLHistoryRows, err *error) {
|
||||
if rows == nil || err == nil {
|
||||
return
|
||||
}
|
||||
if closeErr := rows.Close(); closeErr != nil && *err == nil {
|
||||
*err = closeErr
|
||||
}
|
||||
}
|
||||
|
||||
func cloneHistoryEntry(entry HistoryEntry) HistoryEntry {
|
||||
if entry.Record != nil {
|
||||
record := cloneRecord(*entry.Record)
|
||||
entry.Record = &record
|
||||
}
|
||||
entry.Records = cloneRecords(entry.Records)
|
||||
if entry.Definition != nil {
|
||||
definition := cloneDefinition(*entry.Definition)
|
||||
entry.Definition = &definition
|
||||
}
|
||||
entry.Metadata = cloneStringMap(entry.Metadata)
|
||||
return entry
|
||||
}
|
||||
|
||||
func cloneHistoryEntries(entries []HistoryEntry) []HistoryEntry {
|
||||
out := make([]HistoryEntry, len(entries))
|
||||
for i, entry := range entries {
|
||||
out[i] = cloneHistoryEntry(entry)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func reverseHistory(entries []HistoryEntry) {
|
||||
for i, j := 0, len(entries)-1; i < j; i, j = i+1, j-1 {
|
||||
entries[i], entries[j] = entries[j], entries[i]
|
||||
}
|
||||
}
|
||||
|
||||
func nullableJSON(value any) (any, error) {
|
||||
if value == nil {
|
||||
return nil, nil
|
||||
}
|
||||
data, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func normHistoryTable(table string) (string, error) {
|
||||
table = strings.TrimSpace(table)
|
||||
if table == "" {
|
||||
table = defaultHistoryTable
|
||||
}
|
||||
if !histTablePattern.MatchString(table) {
|
||||
return "", ErrInvalidTableName
|
||||
}
|
||||
return table, nil
|
||||
}
|
||||
|
||||
func quoteHistoryID(identifier string) string {
|
||||
return "`" + identifier + "`"
|
||||
}
|
||||
|
||||
var histTablePattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
|
||||
Reference in new issue
Block a user