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,307 @@
|
||||
package idempotency
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
mysql "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
func mysqlReqStatusSQL() string {
|
||||
return "SELECT " + mysqlQuoteIdentifier("status") + " FROM " + mysqlQuoteIdentifier(mysqlRequestsTable) + " WHERE " + mysqlQuoteIdentifier("guard_key") + " = ? AND " + mysqlQuoteIdentifier("expires_at") + " > ? LIMIT 1 FOR UPDATE"
|
||||
}
|
||||
|
||||
func mysqlReqMetaSQL() string {
|
||||
return "SELECT " + mysqlQuoteIdentifier("request_key") + ", " + mysqlQuoteIdentifier("expires_at") + " FROM " + mysqlQuoteIdentifier(mysqlRequestsTable) + " WHERE " + mysqlQuoteIdentifier("guard_key") + " = ? AND " + mysqlQuoteIdentifier("expires_at") + " > ? LIMIT 1"
|
||||
}
|
||||
|
||||
func mysqlStatusDecision(status Status, key string, sequence uint64, fingerprint, ownerToken string, expiresAt time.Time) Decision {
|
||||
switch status {
|
||||
case statusPending:
|
||||
return Decision{Status: StatusInFlight, Key: key, Sequence: sequence, fingerprint: fingerprint, ownerToken: ownerToken, expiresAt: expiresAt}
|
||||
case StatusReplay:
|
||||
return Decision{Status: StatusReplay, Key: key, Sequence: sequence, fingerprint: fingerprint, ownerToken: ownerToken, expiresAt: expiresAt}
|
||||
default:
|
||||
return Decision{Status: StatusDuplicate, Key: key, Sequence: sequence, fingerprint: fingerprint, ownerToken: ownerToken, expiresAt: expiresAt}
|
||||
}
|
||||
}
|
||||
|
||||
func mysqlHighestSeq(ctx context.Context, tx *sql.Tx, seqKey string, now time.Time) (uint64, error) {
|
||||
if _, err := tx.ExecContext(ctx, //nolint:gosec // G202:SQL 只拼接框架固定表名/列名,过期时间通过参数绑定。
|
||||
"DELETE FROM "+mysqlQuoteIdentifier(mysqlResvTable)+" WHERE "+mysqlQuoteIdentifier("expires_at")+" <= ?", //nolint:gosec // G202:SQL 只拼接框架固定表名/列名,过期时间通过参数绑定。
|
||||
now,
|
||||
); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var highest uint64
|
||||
err := tx.QueryRowContext(ctx,
|
||||
"SELECT COALESCE(MAX("+mysqlQuoteIdentifier("sequence_num")+"), 0) FROM "+mysqlQuoteIdentifier(mysqlResvTable)+" WHERE "+mysqlQuoteIdentifier("sequence_key")+" = ? AND "+mysqlQuoteIdentifier("expires_at")+" > ?",
|
||||
seqKey,
|
||||
now,
|
||||
).Scan(&highest)
|
||||
return highest, err
|
||||
}
|
||||
|
||||
func mysqlHighestSeqDB(ctx context.Context, db *sql.DB, seqKey string, now time.Time) (uint64, error) {
|
||||
if db == nil {
|
||||
return 0, fmt.Errorf("mysql idempotency store is nil")
|
||||
}
|
||||
var highest uint64
|
||||
err := db.QueryRowContext(ctx,
|
||||
"SELECT COALESCE(MAX("+mysqlQuoteIdentifier("sequence_num")+"), 0) FROM "+mysqlQuoteIdentifier(mysqlResvTable)+" WHERE "+mysqlQuoteIdentifier("sequence_key")+" = ? AND "+mysqlQuoteIdentifier("expires_at")+" > ?",
|
||||
seqKey,
|
||||
now,
|
||||
).Scan(&highest)
|
||||
return highest, err
|
||||
}
|
||||
|
||||
func insertMySQLRequest(ctx context.Context, tx *sql.Tx, guardKey string, item Request, requestKey, fingerprint, ownerToken string, status Status, now, expiresAt time.Time) error {
|
||||
_, err := tx.ExecContext(ctx, //nolint:gosec // G202:SQL 只拼接框架固定表名/列名,请求字段通过参数绑定。
|
||||
"INSERT INTO "+mysqlQuoteIdentifier(mysqlRequestsTable)+" ("+ //nolint:gosec // G202:SQL 只拼接框架固定表名/列名,请求字段通过参数绑定。
|
||||
strings.Join([]string{
|
||||
mysqlQuoteIdentifier("guard_key"),
|
||||
mysqlQuoteIdentifier("scope"),
|
||||
mysqlQuoteIdentifier("subject"),
|
||||
mysqlQuoteIdentifier("session_id"),
|
||||
mysqlQuoteIdentifier("request_key"),
|
||||
mysqlQuoteIdentifier("sequence_num"),
|
||||
mysqlQuoteIdentifier("status"),
|
||||
mysqlQuoteIdentifier("created_at"),
|
||||
mysqlQuoteIdentifier("expires_at"),
|
||||
}, ", ")+") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
guardKey,
|
||||
limitStorageText(item.Scope, 128),
|
||||
limitStorageText(item.Subject, 128),
|
||||
limitStorageText(item.Session, 128),
|
||||
mysqlMetadataStorage(requestKey, fingerprint, ownerToken),
|
||||
item.Sequence,
|
||||
string(status),
|
||||
now,
|
||||
expiresAt,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func upsertMySQLStatus(ctx context.Context, tx *sql.Tx, guardKey string, item Request, requestKey, fingerprint, ownerToken string, status Status, now, expiresAt time.Time) error {
|
||||
result, err := tx.ExecContext(ctx, //nolint:gosec // G202:SQL 只拼接框架固定表名/列名,状态字段通过参数绑定。
|
||||
"UPDATE "+mysqlQuoteIdentifier(mysqlRequestsTable)+" SET "+mysqlQuoteIdentifier("status")+" = ?, "+mysqlQuoteIdentifier("expires_at")+" = ? WHERE "+mysqlQuoteIdentifier("guard_key")+" = ?", //nolint:gosec // G202:SQL 只拼接框架固定表名/列名,状态字段通过参数绑定。
|
||||
string(status),
|
||||
expiresAt,
|
||||
guardKey,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
affected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if affected > 0 {
|
||||
return nil
|
||||
}
|
||||
insertErr := insertMySQLRequest(ctx, tx, guardKey, item, requestKey, fingerprint, ownerToken, status, now, expiresAt)
|
||||
if insertErr != nil && !isMySQLDuplicate(insertErr) {
|
||||
return insertErr
|
||||
}
|
||||
if isMySQLDuplicate(insertErr) {
|
||||
_, err = tx.ExecContext(ctx, //nolint:gosec // G202:SQL 只拼接框架固定表名/列名,状态字段通过参数绑定。
|
||||
"UPDATE "+mysqlQuoteIdentifier(mysqlRequestsTable)+" SET "+mysqlQuoteIdentifier("status")+" = ?, "+mysqlQuoteIdentifier("expires_at")+" = ? WHERE "+mysqlQuoteIdentifier("guard_key")+" = ?", //nolint:gosec // G202:SQL 只拼接框架固定表名/列名,状态字段通过参数绑定。
|
||||
string(status),
|
||||
expiresAt,
|
||||
guardKey,
|
||||
)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func mysqlMetadataStorage(requestKey, fingerprint, ownerToken string) string {
|
||||
fingerprint = strings.TrimSpace(fingerprint)
|
||||
ownerToken = strings.TrimSpace(ownerToken)
|
||||
if fingerprint == "" && ownerToken == "" {
|
||||
// 直接调用旧 Store 的路径保留 request_key 原义;新 Guard 总会携带 owner token。
|
||||
return limitStorageText(requestKey, 512)
|
||||
}
|
||||
hash := ""
|
||||
if fingerprint != "" {
|
||||
sum := sha256.Sum256([]byte(fingerprint))
|
||||
hash = hex.EncodeToString(sum[:])
|
||||
}
|
||||
return mysqlMetadataTag + hash + ":" + ownerToken
|
||||
}
|
||||
|
||||
func mysqlMetaFromDB(value, requestKey string) mysqlRequestMeta {
|
||||
if value == limitStorageText(requestKey, 512) {
|
||||
return mysqlRequestMeta{legacy: true}
|
||||
}
|
||||
if strings.HasPrefix(value, mysqlMetadataTag) {
|
||||
hash, ownerToken, ok := strings.Cut(strings.TrimPrefix(value, mysqlMetadataTag), ":")
|
||||
if ok && (hash == "" || len(hash) == sha256.Size*2) {
|
||||
return mysqlRequestMeta{fingerprintHash: hash, ownerToken: ownerToken}
|
||||
}
|
||||
return mysqlRequestMeta{legacy: true}
|
||||
}
|
||||
if strings.HasPrefix(value, mysqlFingerprintTag) {
|
||||
return mysqlRequestMeta{fingerprint: strings.TrimPrefix(value, mysqlFingerprintTag)}
|
||||
}
|
||||
return mysqlRequestMeta{legacy: true}
|
||||
}
|
||||
|
||||
func (m mysqlRequestMeta) conflicts(incoming string) bool {
|
||||
incoming = strings.TrimSpace(incoming)
|
||||
if incoming == "" {
|
||||
return false
|
||||
}
|
||||
if m.fingerprint != "" {
|
||||
return m.fingerprint != incoming
|
||||
}
|
||||
if m.fingerprintHash != "" {
|
||||
sum := sha256.Sum256([]byte(incoming))
|
||||
return m.fingerprintHash != hex.EncodeToString(sum[:])
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m mysqlRequestMeta) decisionFingerprint(incoming string) string {
|
||||
if m.legacy {
|
||||
return ""
|
||||
}
|
||||
if m.fingerprint != "" {
|
||||
return m.fingerprint
|
||||
}
|
||||
if m.fingerprintHash != "" && !m.conflicts(incoming) {
|
||||
return strings.TrimSpace(incoming)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func upsertMySQLResult(ctx context.Context, tx *sql.Tx, guardKey string, payload []byte, now, expiresAt time.Time) error {
|
||||
_, err := tx.ExecContext(ctx, //nolint:gosec // G202:SQL 只拼接框架固定表名/列名,结果内容通过参数绑定。
|
||||
"INSERT INTO "+mysqlQuoteIdentifier(mysqlResultsTable)+" ("+ //nolint:gosec // G202:SQL 只拼接框架固定表名/列名,结果内容通过参数绑定。
|
||||
strings.Join([]string{
|
||||
mysqlQuoteIdentifier("guard_key"),
|
||||
mysqlQuoteIdentifier("result_data"),
|
||||
mysqlQuoteIdentifier("created_at"),
|
||||
mysqlQuoteIdentifier("expires_at"),
|
||||
}, ", ")+") VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE "+
|
||||
mysqlQuoteIdentifier("result_data")+" = VALUES("+mysqlQuoteIdentifier("result_data")+"), "+
|
||||
mysqlQuoteIdentifier("expires_at")+" = VALUES("+mysqlQuoteIdentifier("expires_at")+")",
|
||||
guardKey,
|
||||
payload,
|
||||
now,
|
||||
expiresAt,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func insertMySQLReserve(ctx context.Context, tx *sql.Tx, seqKey, guardKey string, sequence uint64, expiresAt time.Time) error {
|
||||
_, err := tx.ExecContext(ctx, //nolint:gosec // G202:SQL 只拼接框架固定表名/列名,reservation 字段通过参数绑定。
|
||||
"INSERT INTO "+mysqlQuoteIdentifier(mysqlResvTable)+" ("+ //nolint:gosec // G202:SQL 只拼接框架固定表名/列名,reservation 字段通过参数绑定。
|
||||
strings.Join([]string{
|
||||
mysqlQuoteIdentifier("sequence_key"),
|
||||
mysqlQuoteIdentifier("sequence_num"),
|
||||
mysqlQuoteIdentifier("guard_key"),
|
||||
mysqlQuoteIdentifier("expires_at"),
|
||||
}, ", ")+") VALUES (?, ?, ?, ?)",
|
||||
seqKey,
|
||||
sequence,
|
||||
guardKey,
|
||||
expiresAt,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func mysqlReqTableDDL() string {
|
||||
return "CREATE TABLE IF NOT EXISTS " + mysqlQuoteIdentifier(mysqlRequestsTable) + " (\n" +
|
||||
" " + mysqlQuoteIdentifier("guard_key") + " CHAR(64) NOT NULL,\n" +
|
||||
" " + mysqlQuoteIdentifier("scope") + " VARCHAR(128) NOT NULL,\n" +
|
||||
" " + mysqlQuoteIdentifier("subject") + " VARCHAR(128) NOT NULL,\n" +
|
||||
" " + mysqlQuoteIdentifier("session_id") + " VARCHAR(128) NOT NULL DEFAULT '',\n" +
|
||||
" " + mysqlQuoteIdentifier("request_key") + " VARCHAR(512) NOT NULL,\n" +
|
||||
" " + mysqlQuoteIdentifier("sequence_num") + " BIGINT UNSIGNED NOT NULL DEFAULT 0,\n" +
|
||||
" " + mysqlQuoteIdentifier("status") + " VARCHAR(32) NOT NULL,\n" +
|
||||
" " + mysqlQuoteIdentifier("created_at") + " DATETIME(6) NOT NULL,\n" +
|
||||
" " + mysqlQuoteIdentifier("expires_at") + " DATETIME(6) NOT NULL,\n" +
|
||||
" PRIMARY KEY (" + mysqlQuoteIdentifier("guard_key") + "),\n" +
|
||||
" KEY " + mysqlQuoteIdentifier("idx_idempotency_requests_expires_at") + " (" + mysqlQuoteIdentifier("expires_at") + "),\n" +
|
||||
" KEY " + mysqlQuoteIdentifier("idx_idempotency_requests_subject") + " (" + mysqlQuoteIdentifier("scope") + ", " + mysqlQuoteIdentifier("subject") + ")\n" +
|
||||
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
|
||||
}
|
||||
|
||||
func mysqlSeqTableDDL() string {
|
||||
return "CREATE TABLE IF NOT EXISTS " + mysqlQuoteIdentifier(mysqlSequencesTable) + " (\n" +
|
||||
" " + mysqlQuoteIdentifier("sequence_key") + " CHAR(64) NOT NULL,\n" +
|
||||
" " + mysqlQuoteIdentifier("scope") + " VARCHAR(128) NOT NULL,\n" +
|
||||
" " + mysqlQuoteIdentifier("subject") + " VARCHAR(128) NOT NULL,\n" +
|
||||
" " + mysqlQuoteIdentifier("session_id") + " VARCHAR(128) NOT NULL DEFAULT '',\n" +
|
||||
" " + mysqlQuoteIdentifier("highest_sequence") + " BIGINT UNSIGNED NOT NULL DEFAULT 0,\n" +
|
||||
" " + mysqlQuoteIdentifier("updated_at") + " DATETIME(6) NOT NULL,\n" +
|
||||
" PRIMARY KEY (" + mysqlQuoteIdentifier("sequence_key") + "),\n" +
|
||||
" KEY " + mysqlQuoteIdentifier("idx_idempotency_sequences_subject") + " (" + mysqlQuoteIdentifier("scope") + ", " + mysqlQuoteIdentifier("subject") + "),\n" +
|
||||
" KEY " + mysqlQuoteIdentifier("idx_idempotency_sequences_updated_at") + " (" + mysqlQuoteIdentifier("updated_at") + ")\n" +
|
||||
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
|
||||
}
|
||||
|
||||
func mysqlResvTableDDL() string {
|
||||
return "CREATE TABLE IF NOT EXISTS " + mysqlQuoteIdentifier(mysqlResvTable) + " (\n" +
|
||||
" " + mysqlQuoteIdentifier("sequence_key") + " CHAR(64) NOT NULL,\n" +
|
||||
" " + mysqlQuoteIdentifier("sequence_num") + " BIGINT UNSIGNED NOT NULL,\n" +
|
||||
" " + mysqlQuoteIdentifier("guard_key") + " CHAR(64) NOT NULL,\n" +
|
||||
" " + mysqlQuoteIdentifier("expires_at") + " DATETIME(6) NOT NULL,\n" +
|
||||
" PRIMARY KEY (" + mysqlQuoteIdentifier("sequence_key") + ", " + mysqlQuoteIdentifier("sequence_num") + "),\n" +
|
||||
" UNIQUE KEY " + mysqlQuoteIdentifier("idx_idempotency_reservations_guard_key") + " (" + mysqlQuoteIdentifier("guard_key") + "),\n" +
|
||||
" KEY " + mysqlQuoteIdentifier("idx_idempotency_reservations_expires_at") + " (" + mysqlQuoteIdentifier("expires_at") + ")\n" +
|
||||
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
|
||||
}
|
||||
|
||||
func mysqlResultTableDDL() string {
|
||||
return "CREATE TABLE IF NOT EXISTS " + mysqlQuoteIdentifier(mysqlResultsTable) + " (\n" +
|
||||
" " + mysqlQuoteIdentifier("guard_key") + " CHAR(64) NOT NULL,\n" +
|
||||
" " + mysqlQuoteIdentifier("result_data") + " LONGBLOB NOT NULL,\n" +
|
||||
" " + mysqlQuoteIdentifier("created_at") + " DATETIME(6) NOT NULL,\n" +
|
||||
" " + mysqlQuoteIdentifier("expires_at") + " DATETIME(6) NOT NULL,\n" +
|
||||
" PRIMARY KEY (" + mysqlQuoteIdentifier("guard_key") + "),\n" +
|
||||
" KEY " + mysqlQuoteIdentifier("idx_idempotency_results_expires_at") + " (" + mysqlQuoteIdentifier("expires_at") + ")\n" +
|
||||
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
|
||||
}
|
||||
|
||||
func mysqlQuoteIdentifier(identifier string) string {
|
||||
return "`" + strings.ReplaceAll(identifier, "`", "``") + "`"
|
||||
}
|
||||
|
||||
func mysqlCleanupSQL(table, column string, limit int) string {
|
||||
if limit <= 0 {
|
||||
limit = 500
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
"DELETE FROM %s WHERE %s <= ? ORDER BY %s LIMIT %d",
|
||||
mysqlQuoteIdentifier(table),
|
||||
mysqlQuoteIdentifier(column),
|
||||
mysqlQuoteIdentifier(column),
|
||||
limit,
|
||||
)
|
||||
}
|
||||
|
||||
func isMySQLDuplicate(err error) bool {
|
||||
var mysqlErr *mysql.MySQLError
|
||||
return errors.As(err, &mysqlErr) && mysqlErr.Number == 1062
|
||||
}
|
||||
|
||||
func limitStorageText(value string, limit int) string {
|
||||
if limit <= 0 || len(value) <= limit {
|
||||
return value
|
||||
}
|
||||
last := 0
|
||||
for idx := range value {
|
||||
if idx > limit {
|
||||
return value[:last]
|
||||
}
|
||||
last = idx
|
||||
}
|
||||
return value[:last]
|
||||
}
|
||||
Reference in new issue
Block a user