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

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

407 lines
12 KiB
Go

// 本文件把 C# SQLite inventory.db schema 转成 Go MySQL legacy 表。
// legacy 表只作为导入和协议证据镜像,不替代新的 DNF owner 权威表。
package mysql
import (
"embed"
"fmt"
"longheng.io/server/internal/modules/dnf/repository"
"strings"
)
//go:embed csharp_item_schema.sql
var csharpSchemaFS embed.FS
const csharpSchemaFile = "csharp_item_schema.sql"
// CSharpLegacyMySQLSchema 返回 C# inventory.db 对应的 MySQL legacy 表 DDL。
// 它不连接数据库、不执行迁移;调用方只有显式 AutoCreate 时才会写 MySQL。
func CSharpLegacyMySQLSchema(database, tablePrefix string) ([]string, error) {
tablePrefix, err := normTablePrefix(tablePrefix)
if err != nil {
return nil, err
}
if _, err := repository.ValidateDatabases([]string{database}); err != nil {
return nil, err
}
return csharpLegacyMySQLSchema(database, tablePrefix)
}
func csharpLegacyMySQLSchema(database, tablePrefix string) ([]string, error) {
raw, err := csharpSchemaFS.ReadFile(csharpSchemaFile)
if err != nil {
return nil, err
}
statements := splitSQL(stripSQLComments(string(raw)))
indexes, err := legacyIndexes(statements)
if err != nil {
return nil, err
}
out := make([]string, 0, len(statements))
for _, statement := range statements {
stmt := strings.TrimSpace(statement)
if stmt == "" {
continue
}
upper := strings.ToUpper(stmt)
if !strings.HasPrefix(upper, "CREATE TABLE IF NOT EXISTS ") {
if strings.HasPrefix(upper, "INSERT OR IGNORE INTO ") {
insert, err := legacyInsert(database, tablePrefix, stmt)
if err != nil {
return nil, err
}
out = append(out, insert)
}
continue
}
tableName := legacyTableName(stmt)
ddl, err := legacyTableDDL(database, tablePrefix, stmt, indexes[tableName])
if err != nil {
return nil, err
}
out = append(out, ddl)
if tableName == "character_items" {
out = append(out, legacyCharacterItemExtraDDL(database, tablePrefix))
}
if tableName == "item_audit_log" {
out = append(out, legacyItemAuditPayloadDDL(database, tablePrefix))
}
}
return out, nil
}
func legacyTableDDL(database, prefix, stmt string, indexes []string) (string, error) {
head := "CREATE TABLE IF NOT EXISTS "
rest := strings.TrimSpace(stmt[len(head):])
open := strings.Index(rest, "(")
close := strings.LastIndex(rest, ")")
if open <= 0 || close <= open {
return "", fmt.Errorf("%w: malformed legacy table statement", ErrSchemaConfigInvalid)
}
name := strings.TrimSpace(rest[:open])
name = strings.Trim(name, "`")
if !repository.IsSQLIdentifier(name) {
return "", fmt.Errorf("%w: legacy table %q is invalid", ErrSchemaConfigInvalid, name)
}
body := rest[open+1 : close]
parts := splitCols(body)
columns := make([]string, 0, len(parts))
for _, part := range parts {
if name == "character_items" && legacyColumnName(part) == "extra_json" {
continue
}
if name == "item_audit_log" && legacyColumnName(part) == "payload_json" {
continue
}
converted, ok, err := legacyColumn(part)
if err != nil {
return "", err
}
if ok {
columns = append(columns, converted)
}
}
columns = append(columns, indexes...)
if len(columns) == 0 {
return "", fmt.Errorf("%w: legacy table %q has no columns", ErrSchemaConfigInvalid, name)
}
table := mysqlTable(database, prefix, "legacy_"+name)
return fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (\n %s\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
table,
strings.Join(columns, ",\n "),
), nil
}
func legacyColumnName(part string) string {
part = strings.TrimSpace(part)
name, _, ok := strings.Cut(part, " ")
if !ok {
return ""
}
return strings.Trim(name, "`")
}
func legacyCharacterItemExtraDDL(database, prefix string) string {
table := mysqlTable(database, prefix, mysqlLegacyItemExtraTable)
return fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s (
item_uid BIGINT NOT NULL,
extra_key VARCHAR(128) NOT NULL,
extra_value LONGTEXT NOT NULL,
PRIMARY KEY (item_uid, extra_key)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`, table)
}
func legacyItemAuditPayloadDDL(database, prefix string) string {
table := mysqlTable(database, prefix, mysqlLegacyItemAuditPayloadTable)
return fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s (
audit_id BIGINT NOT NULL,
value_path VARCHAR(512) NOT NULL,
value_type VARCHAR(16) NOT NULL,
string_value LONGTEXT NULL,
number_value VARCHAR(128) NULL,
bool_value TINYINT UNSIGNED NULL,
PRIMARY KEY (audit_id, value_path)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`, table)
}
func legacyTableName(stmt string) string {
head := "CREATE TABLE IF NOT EXISTS "
if !strings.HasPrefix(strings.ToUpper(strings.TrimSpace(stmt)), head) {
return ""
}
rest := strings.TrimSpace(stmt[len(head):])
open := strings.Index(rest, "(")
if open <= 0 {
return ""
}
return strings.Trim(strings.TrimSpace(rest[:open]), "`")
}
func legacyIndexes(statements []string) (map[string][]string, error) {
out := make(map[string][]string)
for _, statement := range statements {
stmt := strings.TrimSpace(statement)
upper := strings.ToUpper(stmt)
if !strings.HasPrefix(upper, "CREATE INDEX IF NOT EXISTS ") && !strings.HasPrefix(upper, "CREATE UNIQUE INDEX IF NOT EXISTS ") {
continue
}
table, clause, err := legacyIndex(stmt)
if err != nil {
return nil, err
}
out[table] = append(out[table], clause)
}
return out, nil
}
func legacyIndex(stmt string) (string, string, error) {
unique := false
prefix := "CREATE INDEX IF NOT EXISTS "
if strings.HasPrefix(strings.ToUpper(stmt), "CREATE UNIQUE INDEX IF NOT EXISTS ") {
unique = true
prefix = "CREATE UNIQUE INDEX IF NOT EXISTS "
}
rest := strings.TrimSpace(stmt[len(prefix):])
onPos := strings.Index(strings.ToUpper(rest), " ON ")
if onPos <= 0 {
return "", "", fmt.Errorf("%w: malformed legacy index %q", ErrSchemaConfigInvalid, stmt)
}
indexName := strings.Trim(strings.TrimSpace(rest[:onPos]), "`")
if !repository.IsSQLIdentifier(indexName) {
return "", "", fmt.Errorf("%w: legacy index %q is invalid", ErrSchemaConfigInvalid, indexName)
}
target := strings.TrimSpace(rest[onPos+4:])
open := strings.Index(target, "(")
close := strings.LastIndex(target, ")")
if open <= 0 || close <= open {
return "", "", fmt.Errorf("%w: malformed legacy index target %q", ErrSchemaConfigInvalid, stmt)
}
table := strings.Trim(strings.TrimSpace(target[:open]), "`")
if !repository.IsSQLIdentifier(table) {
return "", "", fmt.Errorf("%w: legacy index table %q is invalid", ErrSchemaConfigInvalid, table)
}
columns := strings.TrimSpace(target[open : close+1])
kind := "KEY"
if unique {
kind = "UNIQUE KEY"
}
return table, fmt.Sprintf("%s %s %s", kind, quoteSQLIdentifier(indexName), columns), nil
}
func legacyColumn(part string) (string, bool, error) {
part = strings.TrimSpace(part)
if part == "" {
return "", false, nil
}
upper := strings.ToUpper(part)
if strings.HasPrefix(upper, "FOREIGN KEY") || strings.HasPrefix(upper, "CONSTRAINT") {
return "", false, nil
}
part = stripChecks(part)
upper = strings.ToUpper(part)
if strings.HasPrefix(upper, "PRIMARY KEY") || strings.HasPrefix(upper, "UNIQUE") {
return part, true, nil
}
name, rest, ok := strings.Cut(part, " ")
if !ok {
return "", false, fmt.Errorf("%w: malformed legacy column %q", ErrSchemaConfigInvalid, part)
}
name = strings.Trim(name, "`")
if !repository.IsSQLIdentifier(name) {
return "", false, fmt.Errorf("%w: legacy column %q is invalid", ErrSchemaConfigInvalid, name)
}
definition := legacyDef(strings.TrimSpace(rest))
return quoteSQLIdentifier(name) + " " + definition, true, nil
}
func legacyDef(definition string) string {
upper := strings.ToUpper(definition)
if strings.Contains(upper, "INTEGER PRIMARY KEY AUTOINCREMENT") {
return strings.TrimSpace(strings.Replace(upper, "INTEGER PRIMARY KEY AUTOINCREMENT", "BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY", 1))
}
if strings.Contains(upper, "INTEGER PRIMARY KEY") {
definition = replaceFirstFold(definition, "INTEGER PRIMARY KEY", "BIGINT NOT NULL PRIMARY KEY")
} else if strings.HasPrefix(upper, "INTEGER") {
definition = replaceFirstFold(definition, "INTEGER", "BIGINT")
} else if strings.HasPrefix(upper, "BLOB") {
definition = replaceFirstFold(definition, "BLOB", "LONGBLOB")
} else if strings.HasPrefix(upper, "TEXT") {
if strings.Contains(upper, "CURRENT_TIMESTAMP") {
definition = replaceFirstFold(definition, "TEXT", "DATETIME(6)")
definition = strings.ReplaceAll(definition, "CURRENT_TIMESTAMP", "CURRENT_TIMESTAMP(6)")
} else {
definition = replaceFirstFold(definition, "TEXT", "VARCHAR(255)")
}
}
definition = stripMySQLUnsupportedDefault(definition)
return strings.Join(strings.Fields(definition), " ")
}
func stripMySQLUnsupportedDefault(definition string) string {
fields := strings.Fields(definition)
if len(fields) == 0 {
return definition
}
switch strings.ToUpper(fields[0]) {
case "BLOB", "TINYBLOB", "MEDIUMBLOB", "LONGBLOB", "TEXT", "TINYTEXT", "MEDIUMTEXT", "LONGTEXT", "JSON":
default:
return definition
}
idx := indexSQLKeyword(definition, "DEFAULT")
if idx < 0 {
return definition
}
return strings.TrimSpace(definition[:idx])
}
func indexSQLKeyword(value, keyword string) int {
upperKeyword := strings.ToUpper(keyword)
inSingleQuote := false
for idx := 0; idx < len(value); idx++ {
if value[idx] == '\'' {
inSingleQuote = !inSingleQuote
continue
}
if inSingleQuote {
continue
}
if idx+len(keyword) > len(value) {
continue
}
if strings.ToUpper(value[idx:idx+len(keyword)]) != upperKeyword {
continue
}
beforeOK := idx == 0 || isSQLKeywordBoundary(value[idx-1])
after := idx + len(keyword)
afterOK := after == len(value) || isSQLKeywordBoundary(value[after])
if beforeOK && afterOK {
return idx
}
}
return -1
}
func isSQLKeywordBoundary(ch byte) bool {
return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n' || ch == '(' || ch == ')'
}
func legacyInsert(database, prefix, stmt string) (string, error) {
head := "INSERT OR IGNORE INTO "
rest := strings.TrimSpace(stmt[len(head):])
open := strings.Index(rest, "(")
if open <= 0 {
return "", fmt.Errorf("%w: malformed legacy insert", ErrSchemaConfigInvalid)
}
table := strings.Trim(strings.TrimSpace(rest[:open]), "`")
if !repository.IsSQLIdentifier(table) {
return "", fmt.Errorf("%w: legacy insert table %q is invalid", ErrSchemaConfigInvalid, table)
}
return "INSERT IGNORE INTO " + mysqlTable(database, prefix, "legacy_"+table) + " " + strings.TrimSpace(rest[open:]), nil
}
func stripSQLComments(value string) string {
lines := strings.Split(value, "\n")
for idx, line := range lines {
if pos := strings.Index(line, "--"); pos >= 0 {
line = line[:pos]
}
lines[idx] = line
}
return strings.Join(lines, "\n")
}
func splitSQL(value string) []string {
parts := strings.Split(value, ";")
out := make([]string, 0, len(parts))
for _, part := range parts {
if trimmed := strings.TrimSpace(part); trimmed != "" {
out = append(out, trimmed)
}
}
return out
}
func splitCols(body string) []string {
out := make([]string, 0)
start := 0
depth := 0
for idx, r := range body {
switch r {
case '(':
depth++
case ')':
if depth > 0 {
depth--
}
case ',':
if depth == 0 {
out = append(out, body[start:idx])
start = idx + 1
}
}
}
out = append(out, body[start:])
return out
}
func stripChecks(value string) string {
for {
idx := strings.Index(strings.ToUpper(value), "CHECK")
if idx < 0 {
return strings.TrimSpace(value)
}
open := strings.Index(value[idx:], "(")
if open < 0 {
return strings.TrimSpace(value[:idx])
}
open += idx
depth := 0
end := -1
scanGroup:
for pos := open; pos < len(value); pos++ {
switch value[pos] {
case '(':
depth++
case ')':
depth--
if depth == 0 {
end = pos + 1
break scanGroup
}
}
}
if end < 0 {
return strings.TrimSpace(value[:idx])
}
value = strings.TrimSpace(value[:idx] + value[end:])
}
}
func replaceFirstFold(value, old, new string) string {
idx := strings.Index(strings.ToUpper(value), strings.ToUpper(old))
if idx < 0 {
return value
}
return value[:idx] + new + value[idx+len(old):]
}