服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
241 lines
7.2 KiB
Go
241 lines
7.2 KiB
Go
// 本文件负责把框架区服路由转换为 DNF 仓储数据库计划。
|
|
package repository
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"longheng.io/server/internal/platform/servergroup"
|
|
)
|
|
|
|
const (
|
|
// FeatureRepository 是 servergroup 中用于解析 DNF 玩家数据仓储的功能名。
|
|
FeatureRepository = "dnf_repository"
|
|
|
|
metaDBPrefix = "dnf.repository."
|
|
metaReadDatabases = metaDBPrefix + "read_databases"
|
|
metaWriteDatabases = metaDBPrefix + "write_databases"
|
|
metaReadDatabase = metaDBPrefix + "read_database"
|
|
metaWriteDatabase = metaDBPrefix + "write_database"
|
|
MetaDatabaseCount = metaDBPrefix + "database_count"
|
|
MetaReadDBPrefix = metaDBPrefix + "read_database_prefix"
|
|
MetaWriteDBPrefix = metaDBPrefix + "write_database_prefix"
|
|
metaDatabasePrefix = metaDBPrefix + "database_prefix"
|
|
metaDatabaseDigits = metaDBPrefix + "database_digits"
|
|
metaDBStart = metaDBPrefix + "database_start_index"
|
|
)
|
|
|
|
var ErrDatabasePlanInvalid = errors.New("dnf database plan is invalid")
|
|
|
|
// DatabasePlan 描述当前区服 DNF 仓储的读库和写库。
|
|
type DatabasePlan struct {
|
|
Feature string
|
|
ShardID string
|
|
GroupID string
|
|
WriteDatabases []string
|
|
ReadDatabases []string
|
|
}
|
|
|
|
// ResolveDatabasePlan 通过 servergroup 解析当前区服的 DNF 读写库计划。
|
|
// 它只读取区服路由和 meta,不连接 MySQL,也不创建数据库。
|
|
func ResolveDatabasePlan(ctx context.Context, manager *servergroup.Manager, shardID string) (DatabasePlan, error) {
|
|
if manager == nil {
|
|
return DatabasePlan{}, fmt.Errorf("%w: servergroup manager is required", ErrDatabasePlanInvalid)
|
|
}
|
|
shardID = strings.TrimSpace(shardID)
|
|
if shardID == "" {
|
|
return DatabasePlan{}, fmt.Errorf("%w: shard id is required", ErrDatabasePlanInvalid)
|
|
}
|
|
target, ok, err := manager.ResolveWrite(ctx, FeatureRepository, shardID)
|
|
if err != nil {
|
|
return DatabasePlan{}, err
|
|
}
|
|
if !ok {
|
|
return DatabasePlan{}, fmt.Errorf("%w: route %s/%s not found", ErrDatabasePlanInvalid, FeatureRepository, shardID)
|
|
}
|
|
if target.RedirectShardID != "" {
|
|
target, ok, err = manager.Resolve(ctx, FeatureRepository, target.RedirectShardID)
|
|
if err != nil {
|
|
return DatabasePlan{}, err
|
|
}
|
|
if !ok {
|
|
return DatabasePlan{}, fmt.Errorf("%w: redirected route %s/%s not found", ErrDatabasePlanInvalid, FeatureRepository, target.RedirectShardID)
|
|
}
|
|
}
|
|
if !target.Available {
|
|
return DatabasePlan{}, fmt.Errorf("%w: route %s/%s unavailable: %s", ErrDatabasePlanInvalid, FeatureRepository, shardID, target.Reason)
|
|
}
|
|
return DatabasePlanFromTarget(target)
|
|
}
|
|
|
|
// DatabasePlanFromTarget 从 servergroup 目标 meta 中派生 DNF 仓储读写库名。
|
|
// 它支持显式库名,也支持 prefix + database_count 生成多个库名。
|
|
func DatabasePlanFromTarget(target servergroup.Target) (DatabasePlan, error) {
|
|
plan, err := DatabasePlanFromMeta(target.Meta)
|
|
if err != nil {
|
|
return DatabasePlan{}, err
|
|
}
|
|
plan.Feature = firstValue(target.Feature, FeatureRepository)
|
|
plan.ShardID = strings.TrimSpace(target.ShardID)
|
|
plan.GroupID = strings.TrimSpace(target.GroupID)
|
|
return plan, nil
|
|
}
|
|
|
|
// DatabasePlanFromMeta 解析区服配置 meta 中的 DNF 仓储数据库配置。
|
|
// 这里不使用硬编码库名,库名必须来自配置中的显式列表或 prefix/count 规则。
|
|
func DatabasePlanFromMeta(meta map[string]string) (DatabasePlan, error) {
|
|
writeDBs, err := metaDatabaseList(meta, metaWriteDatabases, metaWriteDatabase, MetaWriteDBPrefix)
|
|
if err != nil {
|
|
return DatabasePlan{}, err
|
|
}
|
|
readDBs, err := metaDatabaseList(meta, metaReadDatabases, metaReadDatabase, MetaReadDBPrefix)
|
|
if err != nil {
|
|
return DatabasePlan{}, err
|
|
}
|
|
if len(writeDBs) == 0 {
|
|
writeDBs, err = metaDatabaseList(meta, "", "", metaDatabasePrefix)
|
|
if err != nil {
|
|
return DatabasePlan{}, err
|
|
}
|
|
}
|
|
if len(readDBs) == 0 {
|
|
readDBs = append([]string(nil), writeDBs...)
|
|
}
|
|
if len(writeDBs) == 0 {
|
|
return DatabasePlan{}, fmt.Errorf("%w: %s or %s is required", ErrDatabasePlanInvalid, metaWriteDatabases, metaDatabasePrefix)
|
|
}
|
|
return DatabasePlan{
|
|
WriteDatabases: writeDBs,
|
|
ReadDatabases: uniqueDatabases(readDBs),
|
|
}, nil
|
|
}
|
|
|
|
// SchemaDatabases 返回需要执行建库建表的数据库集合。
|
|
// 首次启动只对写库建表;读库通常由复制或 DBA 计划提供,避免误写只读副本。
|
|
func (p DatabasePlan) SchemaDatabases() []string {
|
|
return uniqueDatabases(p.WriteDatabases)
|
|
}
|
|
|
|
func metaDatabaseList(meta map[string]string, listKey, oneKey, prefixKey string) ([]string, error) {
|
|
if listKey != "" {
|
|
if values := parseDatabaseList(meta[listKey]); len(values) > 0 {
|
|
return ValidateDatabases(values)
|
|
}
|
|
}
|
|
if oneKey != "" {
|
|
if values := parseDatabaseList(meta[oneKey]); len(values) > 0 {
|
|
return ValidateDatabases(values)
|
|
}
|
|
}
|
|
prefix := strings.TrimSpace(meta[prefixKey])
|
|
if prefix == "" {
|
|
return nil, nil
|
|
}
|
|
count, err := metaPositiveInt(meta, MetaDatabaseCount, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if count <= 0 {
|
|
return nil, fmt.Errorf("%w: %s is required when %s is set", ErrDatabasePlanInvalid, MetaDatabaseCount, prefixKey)
|
|
}
|
|
start, err := metaPositiveInt(meta, metaDBStart, 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
digits, err := metaPositiveInt(meta, metaDatabaseDigits, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
values := make([]string, 0, count)
|
|
for idx := 0; idx < count; idx++ {
|
|
n := start + idx
|
|
suffix := strconv.Itoa(n)
|
|
if digits > 0 {
|
|
suffix = fmt.Sprintf("%0*d", digits, n)
|
|
}
|
|
values = append(values, prefix+suffix)
|
|
}
|
|
return ValidateDatabases(values)
|
|
}
|
|
|
|
func parseDatabaseList(value string) []string {
|
|
value = strings.NewReplacer("\n", ",", "\t", ",", ";", ",").Replace(value)
|
|
parts := strings.Split(value, ",")
|
|
out := make([]string, 0, len(parts))
|
|
for _, part := range parts {
|
|
part = strings.TrimSpace(part)
|
|
if part != "" {
|
|
out = append(out, part)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func ValidateDatabases(values []string) ([]string, error) {
|
|
out := uniqueDatabases(values)
|
|
for _, value := range out {
|
|
if !IsSQLIdentifier(value) {
|
|
return nil, fmt.Errorf("%w: database name %q is invalid", ErrDatabasePlanInvalid, value)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func uniqueDatabases(values []string) []string {
|
|
out := make([]string, 0, len(values))
|
|
seen := make(map[string]struct{}, len(values))
|
|
for _, value := range values {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" {
|
|
continue
|
|
}
|
|
if _, ok := seen[value]; ok {
|
|
continue
|
|
}
|
|
seen[value] = struct{}{}
|
|
out = append(out, value)
|
|
}
|
|
slices.Sort(out)
|
|
return out
|
|
}
|
|
|
|
func metaPositiveInt(meta map[string]string, key string, fallback int) (int, error) {
|
|
value := strings.TrimSpace(meta[key])
|
|
if value == "" {
|
|
return fallback, nil
|
|
}
|
|
n, err := strconv.Atoi(value)
|
|
if err != nil || n < 0 {
|
|
return 0, fmt.Errorf("%w: %s must be a non-negative integer", ErrDatabasePlanInvalid, key)
|
|
}
|
|
return n, nil
|
|
}
|
|
|
|
func firstValue(values ...string) string {
|
|
for _, value := range values {
|
|
value = strings.TrimSpace(value)
|
|
if value != "" {
|
|
return value
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func IsSQLIdentifier(value string) bool {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" {
|
|
return false
|
|
}
|
|
for _, r := range value {
|
|
if r == '_' || (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
|
|
continue
|
|
}
|
|
return false
|
|
}
|
|
return true
|
|
}
|