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,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]
}