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,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")
|
||||
}
|
||||
Reference in new issue
Block a user