DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

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

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

No files matched your search

@@ -0,0 +1,453 @@
package storageobject
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"regexp"
"sort"
"strconv"
"strings"
"time"
)
var (
ErrSQLDBRequired = errors.New("storage object sql db is required")
ErrInvalidTable = errors.New("storage object sql table is invalid")
ErrInvalidSQLRows = errors.New("storage object sql rows are invalid")
)
const defaultSQLTable = "storage_objects"
var sqlTablePattern = regexp.MustCompile(`^[A-Za-z0-9_]+$`)
type SQLStatements struct {
Table string
EnsureSchema string
EnsureIndexColumn string
Insert string
Upsert string
UpdateWithVersion string
SelectOne string
Delete string
DeleteWithVersion string
List string
}
type SQLStore struct {
db *sql.DB
now func() time.Time
stmts SQLStatements
}
type SQLOptions struct {
DB *sql.DB
Table string
Now func() time.Time
}
func NewSQLStore(options SQLOptions) (*SQLStore, error) {
if options.DB == nil {
return nil, ErrSQLDBRequired
}
stmts, err := NewSQLStatements(options.Table)
if err != nil {
return nil, err
}
now := options.Now
if now == nil {
now = time.Now
}
return &SQLStore{db: options.DB, now: now, stmts: stmts}, nil
}
func NewSQLStatements(table string) (SQLStatements, error) {
table, err := normalizeSQLTable(table)
if err != nil {
return SQLStatements{}, err
}
quoted := "`" + table + "`"
return SQLStatements{
Table: table,
EnsureSchema: fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s (
collection VARCHAR(128) NOT NULL,
object_key VARCHAR(256) NOT NULL,
user_id VARCHAR(128) NOT NULL DEFAULT '',
value_json JSON NOT NULL,
index_json JSON NULL,
version VARCHAR(64) NOT NULL,
permission_read INT NOT NULL,
permission_write INT NOT NULL,
created_at TIMESTAMP(6) NOT NULL,
updated_at TIMESTAMP(6) NOT NULL,
PRIMARY KEY (collection, user_id, object_key),
KEY idx_storage_object_list (collection, user_id, object_key)
)`, quoted),
EnsureIndexColumn: fmt.Sprintf(`ALTER TABLE %s ADD COLUMN index_json JSON NULL AFTER value_json`, quoted),
Insert: fmt.Sprintf(`INSERT INTO %s (collection, object_key, user_id, value_json, index_json, version, permission_read, permission_write, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, quoted),
Upsert: fmt.Sprintf(`INSERT INTO %s (collection, object_key, user_id, value_json, index_json, version, permission_read, permission_write, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE value_json=VALUES(value_json), index_json=VALUES(index_json), version=VALUES(version), permission_read=VALUES(permission_read), permission_write=VALUES(permission_write), updated_at=VALUES(updated_at)`, quoted),
UpdateWithVersion: fmt.Sprintf(`UPDATE %s SET value_json=?, index_json=?, version=?, permission_read=?, permission_write=?, updated_at=? WHERE collection=? AND user_id=? AND object_key=? AND version=?`, quoted),
SelectOne: fmt.Sprintf(`SELECT collection, object_key, user_id, value_json, index_json, version, permission_read, permission_write, created_at, updated_at FROM %s WHERE collection=? AND user_id=? AND object_key=?`, quoted),
Delete: fmt.Sprintf(`DELETE FROM %s WHERE collection=? AND user_id=? AND object_key=?`, quoted),
DeleteWithVersion: fmt.Sprintf(`DELETE FROM %s WHERE collection=? AND user_id=? AND object_key=? AND version=?`, quoted),
List: fmt.Sprintf(`SELECT collection, object_key, user_id, value_json, index_json, version, permission_read, permission_write, created_at, updated_at FROM %s WHERE collection=? AND (?='' OR user_id=?) AND CONCAT(collection, '/', user_id, '/', object_key)>? ORDER BY collection, user_id, object_key LIMIT ?`, quoted),
}, nil
}
func (s *SQLStore) EnsureSchema(ctx context.Context) error {
if err := ctxErr(ctx); err != nil {
return err
}
stmts, err := s.ready()
if err != nil {
return err
}
if _, err := s.db.ExecContext(ctx, stmts.EnsureSchema); err != nil {
return err
}
if _, err := s.db.ExecContext(ctx, stmts.EnsureIndexColumn); err != nil && !isDupColumnErr(err) {
return err
}
return nil
}
func (s *SQLStore) Close() error {
if s == nil || s.db == nil {
return nil
}
return s.db.Close()
}
func (s *SQLStore) Write(ctx context.Context, request WriteRequest) (Object, error) {
if err := ctxErr(ctx); err != nil {
return Object{}, err
}
stmts, err := s.ready()
if err != nil {
return Object{}, err
}
now := s.nowUTC()
object, err := normalizeObject(request.Object, now)
if err != nil {
return Object{}, err
}
if request.IfMatchVersion != "" {
current, ok, err := s.Read(ctx, object.Key)
if err != nil {
return Object{}, err
}
if !ok || current.Version != strings.TrimSpace(request.IfMatchVersion) {
return Object{}, ErrVersionConflict
}
object.CreatedAt = current.CreatedAt
object.Version = versionFor(object)
result, err := s.db.ExecContext(ctx, stmts.UpdateWithVersion,
[]byte(object.Value), sqlIndexJSON(object), object.Version, object.PermissionRead, object.PermissionWrite, object.UpdatedAt,
object.Key.Collection, object.Key.UserID, object.Key.Key, strings.TrimSpace(request.IfMatchVersion),
)
if err != nil {
return Object{}, err
}
affected, err := result.RowsAffected()
if err != nil {
return Object{}, err
}
if affected == 0 {
return Object{}, ErrVersionConflict
}
return cloneObject(object), nil
}
if request.IfNoneMatch {
object.Version = versionFor(object)
_, err := s.db.ExecContext(ctx, stmts.Insert, sqlObjectArgs(object)...)
if err != nil {
if isDuplicateKeyError(err) {
return Object{}, ErrVersionConflict
}
return Object{}, err
}
return cloneObject(object), nil
}
if current, ok, err := s.Read(ctx, object.Key); err != nil {
return Object{}, err
} else if ok {
object.CreatedAt = current.CreatedAt
}
object.Version = versionFor(object)
_, err = s.db.ExecContext(ctx, stmts.Upsert, sqlObjectArgs(object)...)
if err != nil {
return Object{}, err
}
return cloneObject(object), nil
}
func (s *SQLStore) Read(ctx context.Context, key Key) (Object, bool, error) {
if err := ctxErr(ctx); err != nil {
return Object{}, false, err
}
stmts, err := s.ready()
if err != nil {
return Object{}, false, err
}
key, err = normalizeKey(key)
if err != nil {
return Object{}, false, err
}
row := s.db.QueryRowContext(ctx, stmts.SelectOne, key.Collection, key.UserID, key.Key)
object, err := scanObject(row)
if errors.Is(err, sql.ErrNoRows) {
return Object{}, false, nil
}
if err != nil {
return Object{}, false, err
}
return object, true, nil
}
func (s *SQLStore) Delete(ctx context.Context, key Key, ifMatchVersion string) error {
if err := ctxErr(ctx); err != nil {
return err
}
stmts, err := s.ready()
if err != nil {
return err
}
key, err = normalizeKey(key)
if err != nil {
return err
}
ifMatchVersion = strings.TrimSpace(ifMatchVersion)
stmt := stmts.Delete
args := []any{key.Collection, key.UserID, key.Key}
if ifMatchVersion != "" {
stmt = stmts.DeleteWithVersion
args = append(args, ifMatchVersion)
}
result, err := s.db.ExecContext(ctx, stmt, args...)
if err != nil {
return err
}
affected, err := result.RowsAffected()
if err != nil {
return err
}
if affected == 0 {
if ifMatchVersion != "" {
return ErrVersionConflict
}
return ErrObjectNotFound
}
return nil
}
func (s *SQLStore) List(ctx context.Context, request ListRequest) (result ListResult, err error) {
if err := ctxErr(ctx); err != nil {
return ListResult{}, err
}
stmts, err := s.ready()
if err != nil {
return ListResult{}, err
}
collection := strings.TrimSpace(request.Collection)
if collection == "" {
return ListResult{}, ErrInvalidKey
}
userID := strings.TrimSpace(request.UserID)
limit := normalizeListLimit(request.Limit)
cursor := strings.TrimSpace(request.Cursor)
indexFilters := normalizeIndex(request.Index)
query, indexArgs := listQuery(stmts, indexFilters)
args := []any{collection, userID, userID, cursor}
args = append(args, indexArgs...)
args = append(args, limit+1)
rows, err := s.db.QueryContext(ctx, query, args...)
if err != nil {
return ListResult{}, err
}
if rows == nil {
return ListResult{}, ErrInvalidSQLRows
}
defer closeSQLRowsErr(rows, &err)
var objects []Object
for rows.Next() {
object, err := scanObject(rows)
if err != nil {
return ListResult{}, err
}
objects = append(objects, object)
}
if err := rows.Err(); err != nil {
return ListResult{}, err
}
result = ListResult{Objects: objects}
if len(result.Objects) > limit {
result.Objects = result.Objects[:limit]
result.NextCursor = objectID(result.Objects[len(result.Objects)-1].Key)
}
return result, nil
}
func (s *SQLStore) ready() (SQLStatements, error) {
if s == nil || s.db == nil {
return SQLStatements{}, ErrSQLDBRequired
}
if sqlStatementsReady(s.stmts) {
return s.stmts, nil
}
table := strings.TrimSpace(s.stmts.Table)
if table == "" {
table = defaultSQLTable
}
return NewSQLStatements(table)
}
func (s *SQLStore) nowUTC() time.Time {
if s == nil || s.now == nil {
return time.Now().UTC()
}
return s.now().UTC()
}
func sqlStatementsReady(stmts SQLStatements) bool {
return strings.TrimSpace(stmts.Table) != "" &&
stmts.EnsureSchema != "" &&
stmts.EnsureIndexColumn != "" &&
stmts.Insert != "" &&
stmts.Upsert != "" &&
stmts.UpdateWithVersion != "" &&
stmts.SelectOne != "" &&
stmts.Delete != "" &&
stmts.DeleteWithVersion != "" &&
stmts.List != ""
}
func closeSQLRowsErr(rows interface{ Close() error }, err *error) {
if rows == nil || err == nil {
return
}
if closeErr := rows.Close(); closeErr != nil && *err == nil {
*err = closeErr
}
}
func listQuery(stmts SQLStatements, indexFilters map[string]string) (string, []any) {
indexFilters = normalizeIndex(indexFilters)
if len(indexFilters) == 0 {
return stmts.List, nil
}
keys := make([]string, 0, len(indexFilters))
for key := range indexFilters {
keys = append(keys, key)
}
sort.Strings(keys)
quoted := "`" + stmts.Table + "`"
var builder strings.Builder
builder.WriteString(`SELECT collection, object_key, user_id, value_json, index_json, version, permission_read, permission_write, created_at, updated_at FROM `)
builder.WriteString(quoted)
builder.WriteString(` WHERE collection=? AND (?='' OR user_id=?) AND CONCAT(collection, '/', user_id, '/', object_key)>?`)
args := make([]any, 0, len(keys)*2)
for _, key := range keys {
builder.WriteString(` AND JSON_UNQUOTE(JSON_EXTRACT(COALESCE(index_json, JSON_OBJECT()), ?)) = ?`)
args = append(args, jsonPathForIndexKey(key), indexFilters[key])
}
builder.WriteString(` ORDER BY collection, user_id, object_key LIMIT ?`)
return builder.String(), args
}
func jsonPathForIndexKey(key string) string {
return "$." + strconv.Quote(strings.TrimSpace(key))
}
type objectScanner interface {
Scan(dest ...any) error
}
func scanObject(scanner objectScanner) (Object, error) {
var object Object
var value []byte
var indexJSON []byte
if err := scanner.Scan(
&object.Key.Collection,
&object.Key.Key,
&object.Key.UserID,
&value,
&indexJSON,
&object.Version,
&object.PermissionRead,
&object.PermissionWrite,
&object.CreatedAt,
&object.UpdatedAt,
); err != nil {
return Object{}, err
}
object.Value = append([]byte(nil), value...)
object.Index = decodeIndexJSON(indexJSON)
object.CreatedAt = object.CreatedAt.UTC()
object.UpdatedAt = object.UpdatedAt.UTC()
return object, nil
}
func sqlObjectArgs(object Object) []any {
return []any{
object.Key.Collection,
object.Key.Key,
object.Key.UserID,
[]byte(object.Value),
sqlIndexJSON(object),
object.Version,
object.PermissionRead,
object.PermissionWrite,
object.CreatedAt,
object.UpdatedAt,
}
}
func sqlIndexJSON(object Object) []byte {
data, _ := json.Marshal(normalizeIndex(object.Index))
if string(data) == "null" {
return []byte(`{}`)
}
return data
}
func decodeIndexJSON(data []byte) map[string]string {
if len(data) == 0 || string(data) == "null" {
return nil
}
var out map[string]string
if err := json.Unmarshal(data, &out); err != nil {
return nil
}
return normalizeIndex(out)
}
func normalizeSQLTable(table string) (string, error) {
table = strings.TrimSpace(table)
if table == "" {
table = defaultSQLTable
}
if !sqlTablePattern.MatchString(table) {
return "", ErrInvalidTable
}
return table, nil
}
func isDuplicateKeyError(err error) bool {
if err == nil {
return false
}
text := strings.ToLower(err.Error())
return strings.Contains(text, "duplicate") || strings.Contains(text, "1062")
}
func isDupColumnErr(err error) bool {
if err == nil {
return false
}
text := strings.ToLower(err.Error())
return strings.Contains(text, "duplicate column") || strings.Contains(text, "1060")
}