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,196 @@
|
||||
// Package dnf 提供 DNF 项目接入 logic 服务的装配入口。
|
||||
package dnf
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
|
||||
appkit "longheng.io/server/internal/platform/app"
|
||||
)
|
||||
|
||||
const (
|
||||
// EnvConfigPath 是 DNF logic manifest 使用的外部配置路径环境变量。
|
||||
EnvConfigPath = "LONGHENG_DNF_CONFIG"
|
||||
// DefaultConfigPath 是本地 DNF logic manifest 配置路径。
|
||||
DefaultConfigPath = "configs/dnf/logic.toml"
|
||||
)
|
||||
|
||||
var ErrConfigInvalid = errors.New("dnf logic config is invalid")
|
||||
|
||||
type Config struct {
|
||||
Repository RepositoryConfig `toml:"repository"`
|
||||
}
|
||||
|
||||
type RepositoryConfig struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
Driver string `toml:"driver"`
|
||||
SQLitePath string `toml:"sqlite_path"`
|
||||
MySQLDSN string `toml:"mysql_dsn"`
|
||||
MySQLMaxOpenConns int `toml:"mysql_max_open_conns"`
|
||||
MySQLMaxIdleConns int `toml:"mysql_max_idle_conns"`
|
||||
MySQLConnMaxLifetime int `toml:"mysql_conn_max_lifetime_seconds"`
|
||||
RedisEnabled bool `toml:"redis_enabled"`
|
||||
RedisAddress string `toml:"redis_address"`
|
||||
RedisPassword string `toml:"redis_password"`
|
||||
RedisDB int `toml:"redis_db"`
|
||||
RedisPoolSize int `toml:"redis_pool_size"`
|
||||
RedisTimeoutSeconds int `toml:"redis_timeout_seconds"`
|
||||
RedisKeyPrefix string `toml:"redis_key_prefix"`
|
||||
RedisTTLSeconds int `toml:"redis_ttl_seconds"`
|
||||
ShardID string `toml:"shard_id"`
|
||||
TablePrefix string `toml:"table_prefix"`
|
||||
ServerGroupPlanFile string `toml:"server_group_plan_file"`
|
||||
AutoCreateSchema bool `toml:"auto_create_schema"`
|
||||
CSharpLegacySchema bool `toml:"csharp_legacy_schema"`
|
||||
CreateDatabases bool `toml:"create_databases"`
|
||||
}
|
||||
|
||||
// LoadConfig 读取 DNF logic manifest 外部配置。
|
||||
// 该配置只属于 DNF 项目装配层,不进入平台通用 ServiceConfig;环境相关默认值由 LoadConfigForEnv 补齐。
|
||||
func LoadConfig(path string) (Config, error) {
|
||||
path = strings.TrimSpace(path)
|
||||
if path == "" {
|
||||
path = DefaultConfigPath
|
||||
}
|
||||
var cfg Config
|
||||
meta, err := toml.DecodeFile(path, &cfg)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
if keys := meta.Undecoded(); len(keys) > 0 {
|
||||
names := make([]string, 0, len(keys))
|
||||
for _, key := range keys {
|
||||
names = append(names, key.String())
|
||||
}
|
||||
return Config{}, fmt.Errorf("%w: unknown keys: %s", ErrConfigInvalid, strings.Join(names, ", "))
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// LoadConfigForEnv 读取并校验 DNF logic manifest 配置。
|
||||
// 它会从框架 Env 补齐当前区服 shard_id,再执行配置校验。
|
||||
func LoadConfigForEnv(path string, env *appkit.Env) (Config, error) {
|
||||
cfg, err := LoadConfig(path)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
cfg.Normalize(env)
|
||||
return cfg, cfg.Validate()
|
||||
}
|
||||
|
||||
// Normalize 补齐 DNF 仓储装配默认值。
|
||||
// ShardID 默认读取框架区服 ID,读写库名仍由 servergroup route meta 派生。
|
||||
func (c *Config) Normalize(env *appkit.Env) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
repo := &c.Repository
|
||||
repo.Driver = strings.ToLower(strings.TrimSpace(repo.Driver))
|
||||
repo.SQLitePath = strings.TrimSpace(repo.SQLitePath)
|
||||
repo.MySQLDSN = strings.TrimSpace(repo.MySQLDSN)
|
||||
repo.RedisAddress = strings.TrimSpace(repo.RedisAddress)
|
||||
repo.RedisPassword = strings.TrimSpace(repo.RedisPassword)
|
||||
repo.RedisKeyPrefix = strings.TrimSpace(repo.RedisKeyPrefix)
|
||||
repo.ShardID = strings.TrimSpace(repo.ShardID)
|
||||
repo.TablePrefix = strings.TrimSpace(repo.TablePrefix)
|
||||
repo.ServerGroupPlanFile = strings.TrimSpace(repo.ServerGroupPlanFile)
|
||||
if repo.ShardID == "" && env != nil && env.Config.Cluster.ShardID != 0 {
|
||||
repo.ShardID = fmt.Sprintf("%d", env.Config.Cluster.ShardID)
|
||||
}
|
||||
if repo.ServerGroupPlanFile == "" && env != nil {
|
||||
repo.ServerGroupPlanFile = strings.TrimSpace(env.Config.ServerGroup.PlanFile)
|
||||
}
|
||||
if repo.TablePrefix == "" {
|
||||
repo.TablePrefix = "dnf"
|
||||
}
|
||||
// An omitted driver preserves every existing installation's MySQL
|
||||
// behavior. New one-click templates opt in to SQLite explicitly.
|
||||
if repo.Driver == "" {
|
||||
repo.Driver = "mysql"
|
||||
}
|
||||
if repo.Driver == "sqlite" && repo.SQLitePath == "" {
|
||||
repo.SQLitePath = "data/dnf90.db"
|
||||
}
|
||||
if repo.MySQLMaxOpenConns <= 0 {
|
||||
repo.MySQLMaxOpenConns = 32
|
||||
}
|
||||
if repo.MySQLMaxIdleConns <= 0 {
|
||||
repo.MySQLMaxIdleConns = 8
|
||||
}
|
||||
if repo.MySQLConnMaxLifetime <= 0 {
|
||||
repo.MySQLConnMaxLifetime = 300
|
||||
}
|
||||
if repo.RedisEnabled {
|
||||
if repo.RedisAddress == "" {
|
||||
repo.RedisAddress = "127.0.0.1:6379"
|
||||
}
|
||||
if repo.RedisPoolSize <= 0 {
|
||||
repo.RedisPoolSize = 8
|
||||
}
|
||||
if repo.RedisTimeoutSeconds <= 0 {
|
||||
repo.RedisTimeoutSeconds = 2
|
||||
}
|
||||
if repo.RedisKeyPrefix == "" {
|
||||
repo.RedisKeyPrefix = "dnf:repository"
|
||||
if repo.ShardID != "" {
|
||||
repo.RedisKeyPrefix += ":" + repo.ShardID
|
||||
}
|
||||
}
|
||||
if repo.RedisTTLSeconds < 0 {
|
||||
repo.RedisTTLSeconds = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate 校验 DNF 仓储装配配置。
|
||||
// 开启仓储时必须显式配置 MySQL DSN,并通过 servergroup 提供 shard 路由。
|
||||
func (c Config) Validate() error {
|
||||
repo := c.Repository
|
||||
if !repo.Enabled {
|
||||
return nil
|
||||
}
|
||||
switch strings.ToLower(strings.TrimSpace(repo.Driver)) {
|
||||
case "mysql":
|
||||
if strings.TrimSpace(repo.MySQLDSN) == "" {
|
||||
return fmt.Errorf("%w: repository mysql_dsn is required", ErrConfigInvalid)
|
||||
}
|
||||
if strings.TrimSpace(repo.ShardID) == "" {
|
||||
return fmt.Errorf("%w: repository shard_id is required", ErrConfigInvalid)
|
||||
}
|
||||
if repo.MySQLMaxOpenConns <= 0 || repo.MySQLMaxIdleConns <= 0 || repo.MySQLConnMaxLifetime <= 0 {
|
||||
return fmt.Errorf("%w: repository mysql pool values must be positive", ErrConfigInvalid)
|
||||
}
|
||||
case "sqlite":
|
||||
if strings.TrimSpace(repo.SQLitePath) == "" {
|
||||
return fmt.Errorf("%w: repository sqlite_path is required", ErrConfigInvalid)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("%w: repository driver must be mysql or sqlite", ErrConfigInvalid)
|
||||
}
|
||||
if repo.RedisEnabled {
|
||||
if strings.TrimSpace(repo.RedisAddress) == "" {
|
||||
return fmt.Errorf("%w: repository redis_address is required", ErrConfigInvalid)
|
||||
}
|
||||
if repo.RedisDB < 0 {
|
||||
return fmt.Errorf("%w: repository redis_db must be non-negative", ErrConfigInvalid)
|
||||
}
|
||||
if repo.RedisPoolSize <= 0 || repo.RedisTimeoutSeconds <= 0 {
|
||||
return fmt.Errorf("%w: repository redis pool values must be positive", ErrConfigInvalid)
|
||||
}
|
||||
if strings.TrimSpace(repo.RedisKeyPrefix) == "" {
|
||||
return fmt.Errorf("%w: repository redis_key_prefix is required", ErrConfigInvalid)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c RepositoryConfig) connMaxLife() time.Duration {
|
||||
if c.MySQLConnMaxLifetime <= 0 {
|
||||
return 5 * time.Minute
|
||||
}
|
||||
return time.Duration(c.MySQLConnMaxLifetime) * time.Second
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Package game 装配演示游戏框架的 DNF logic 组件。
|
||||
// 该子包隔离 internal/services/logic 演示框架依赖,
|
||||
// 生产二进制(cmd/server/dnf90 经 internal/app/dnf90)只装配仓储底座,
|
||||
// 不引用本包,演示框架因此留在生产依赖闭包之外。
|
||||
package game
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
appkit "longheng.io/server/internal/platform/app"
|
||||
logicsvc "longheng.io/server/internal/services/logic"
|
||||
logicdnf "longheng.io/server/internal/services/logic/dnf"
|
||||
)
|
||||
|
||||
// Components 按外部配置构建 DNF logic 服务组件列表。
|
||||
// 仓储组件排在 logic service 前启动,后续 DNF owner 可从 Runtime 读取 repository.Group。
|
||||
func Components(env *appkit.Env) ([]appkit.Component, error) {
|
||||
path := strings.TrimSpace(os.Getenv(logicdnf.EnvConfigPath))
|
||||
if path == "" {
|
||||
path = logicdnf.DefaultConfigPath
|
||||
}
|
||||
cfg, err := logicdnf.LoadConfigForEnv(path, env)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ComponentsWithConfig(env, cfg)
|
||||
}
|
||||
|
||||
// ComponentsWithConfig 使用已解析配置构建 DNF logic 服务组件列表。
|
||||
// 该入口用于测试和项目侧自定义配置装配。
|
||||
func ComponentsWithConfig(env *appkit.Env, cfg logicdnf.Config) ([]appkit.Component, error) {
|
||||
runtime, err := logicdnf.NewRuntime(env, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
components := make([]appkit.Component, 0, 2)
|
||||
if runtime.Repository != nil {
|
||||
components = append(components, runtime.Repository)
|
||||
}
|
||||
components = append(components, logicsvc.NewWithOptions(env, logicsvc.Options{
|
||||
Game: func(e *appkit.Env) (logicsvc.GameManifest, error) {
|
||||
return GameManifest(runtime, e)
|
||||
},
|
||||
}))
|
||||
return components, nil
|
||||
}
|
||||
|
||||
// GameManifest 返回当前 DNF logic 的业务 manifest。
|
||||
// 本阶段只接入仓储底座,协议 handler 和 owner 后续在这里继续挂接。
|
||||
func GameManifest(_ *logicdnf.Runtime, env *appkit.Env) (logicsvc.GameManifest, error) {
|
||||
return logicsvc.BlankManifest(env)
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
package dnf
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
"longheng.io/server/internal/modules/dnf/repository/mysql"
|
||||
sqliterepo "longheng.io/server/internal/modules/dnf/repository/sqlite"
|
||||
appkit "longheng.io/server/internal/platform/app"
|
||||
platformdb "longheng.io/server/internal/platform/db"
|
||||
"longheng.io/server/internal/platform/servergroup"
|
||||
)
|
||||
|
||||
var openSQL = sql.Open
|
||||
|
||||
type Runtime struct {
|
||||
Repository *RepoComponent
|
||||
}
|
||||
|
||||
type RepoComponent struct {
|
||||
component repositoryLifecycle
|
||||
driver string
|
||||
db *sql.DB
|
||||
redis *platformdb.RedigoExecutor
|
||||
cacheOpts dnfrepo.RedisCacheOptions
|
||||
}
|
||||
|
||||
type repositoryLifecycle interface {
|
||||
Name() string
|
||||
Preflight(context.Context) error
|
||||
Start(context.Context) error
|
||||
Stop(context.Context) error
|
||||
Group() (dnfrepo.Group, bool)
|
||||
}
|
||||
|
||||
type RepoSnapshot struct {
|
||||
Driver string `json:"driver"`
|
||||
MySQL *mysql.ComponentSnapshot `json:"mysql,omitempty"`
|
||||
SQLite *sqliterepo.Snapshot `json:"sqlite,omitempty"`
|
||||
}
|
||||
|
||||
// NewRuntime 创建 DNF logic 运行时装配对象。
|
||||
// 它只负责项目依赖装配,不注册协议 handler,也不直接修改玩家状态。
|
||||
func NewRuntime(env *appkit.Env, cfg Config) (*Runtime, error) {
|
||||
cfg.Normalize(env)
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
runtime := &Runtime{}
|
||||
if cfg.Repository.Enabled {
|
||||
component, err := newRepoComponent(env, cfg.Repository)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
runtime.Repository = component
|
||||
}
|
||||
return runtime, nil
|
||||
}
|
||||
|
||||
// Group 返回启动后的 DNF 玩家仓储聚合。
|
||||
// owner/service 只能通过该聚合访问账号、角色、背包、技能、设置和 packet 模板。
|
||||
func (r *Runtime) Group() (dnfrepo.Group, bool) {
|
||||
if r == nil || r.Repository == nil {
|
||||
return dnfrepo.Group{}, false
|
||||
}
|
||||
return r.Repository.Group()
|
||||
}
|
||||
|
||||
// NewRepoComponent 按 DNF 配置打开 MySQL 连接池并创建仓储组件。
|
||||
// 连接池由返回的组件在 Stop 阶段关闭。
|
||||
func NewRepoComponent(env *appkit.Env, cfg RepositoryConfig) (*RepoComponent, error) {
|
||||
envCfg := Config{Repository: cfg}
|
||||
envCfg.Normalize(env)
|
||||
if err := envCfg.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg = envCfg.Repository
|
||||
return newRepoComponent(env, cfg)
|
||||
}
|
||||
|
||||
func newRepoComponent(env *appkit.Env, cfg RepositoryConfig) (*RepoComponent, error) {
|
||||
if openSQL == nil {
|
||||
return nil, fmt.Errorf("%w: sql opener is required", ErrConfigInvalid)
|
||||
}
|
||||
driver := strings.ToLower(strings.TrimSpace(cfg.Driver))
|
||||
dataSource := cfg.MySQLDSN
|
||||
if driver == "sqlite" {
|
||||
dataSource = filepath.Clean(cfg.SQLitePath)
|
||||
parent := filepath.Dir(dataSource)
|
||||
if parent != "." && parent != "" {
|
||||
if err := os.MkdirAll(parent, 0o755); err != nil {
|
||||
return nil, fmt.Errorf("create sqlite repository directory: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
db, err := openSQL(driver, dataSource)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if db == nil {
|
||||
return nil, fmt.Errorf("%w: sql db is nil", ErrConfigInvalid)
|
||||
}
|
||||
if driver == "sqlite" {
|
||||
db.SetMaxOpenConns(1)
|
||||
db.SetMaxIdleConns(1)
|
||||
} else {
|
||||
db.SetMaxOpenConns(cfg.MySQLMaxOpenConns)
|
||||
db.SetMaxIdleConns(cfg.MySQLMaxIdleConns)
|
||||
db.SetConnMaxLifetime(cfg.connMaxLife())
|
||||
}
|
||||
|
||||
var redisExec *platformdb.RedigoExecutor
|
||||
if cfg.RedisEnabled {
|
||||
redisExec = platformdb.NewRedigoExecutor(platformdb.RedigoOptions{
|
||||
Address: cfg.RedisAddress,
|
||||
Password: cfg.RedisPassword,
|
||||
DB: cfg.RedisDB,
|
||||
PoolSize: cfg.RedisPoolSize,
|
||||
Timeout: cfg.redisTimeout(),
|
||||
})
|
||||
}
|
||||
var component repositoryLifecycle
|
||||
if driver == "sqlite" {
|
||||
component = sqliterepo.NewComponent(db, dataSource)
|
||||
} else {
|
||||
var manager *servergroup.Manager
|
||||
if env != nil {
|
||||
manager = env.ServerGroup
|
||||
}
|
||||
if manager == nil && cfg.ServerGroupPlanFile != "" {
|
||||
var loadErr error
|
||||
manager, loadErr = loadServerGroupPlan(cfg.ServerGroupPlanFile)
|
||||
if loadErr != nil {
|
||||
_ = db.Close()
|
||||
return nil, loadErr
|
||||
}
|
||||
}
|
||||
component = mysql.NewComponentFromServerGroup(mysql.SQLDBAdapter{DB: db}, manager, mysql.ComponentOptions{
|
||||
ShardID: cfg.ShardID,
|
||||
TablePrefix: cfg.TablePrefix,
|
||||
AutoCreateSchema: cfg.AutoCreateSchema,
|
||||
IncludeCSharpLegacySchema: cfg.CSharpLegacySchema,
|
||||
CreateDatabases: cfg.CreateDatabases,
|
||||
Now: time.Now,
|
||||
})
|
||||
}
|
||||
return &RepoComponent{
|
||||
component: component,
|
||||
driver: driver,
|
||||
db: db,
|
||||
redis: redisExec,
|
||||
cacheOpts: dnfrepo.RedisCacheOptions{
|
||||
KeyPrefix: cfg.RedisKeyPrefix,
|
||||
TTL: cfg.redisTTL(),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// loadServerGroupPlan 从外部区服计划文件构造只读 manager。
|
||||
// DNF manifest 可能早于框架 servergroup 注入运行,仓储仍必须按同一份 route meta 派生库名。
|
||||
func loadServerGroupPlan(path string) (*servergroup.Manager, error) {
|
||||
store, err := servergroup.NewFileStore(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
plan, err := store.Load(context.Background())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return servergroup.New(plan)
|
||||
}
|
||||
|
||||
// Name 返回框架生命周期组件名。
|
||||
func (c *RepoComponent) Name() string {
|
||||
if c == nil || c.component == nil {
|
||||
return "dnf-repository"
|
||||
}
|
||||
return c.component.Name()
|
||||
}
|
||||
|
||||
// Preflight 检查 DNF 仓储启动依赖,不创建 schema。
|
||||
func (c *RepoComponent) Preflight(ctx context.Context) error {
|
||||
if c == nil || c.component == nil {
|
||||
return dnfrepo.ErrRepoMissing
|
||||
}
|
||||
if err := c.component.Preflight(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.pingRedis(ctx)
|
||||
}
|
||||
|
||||
// Start 启动 DNF 仓储组件。
|
||||
func (c *RepoComponent) Start(ctx context.Context) error {
|
||||
if c == nil || c.component == nil {
|
||||
return dnfrepo.ErrRepoMissing
|
||||
}
|
||||
if err := c.component.Start(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.pingRedis(ctx)
|
||||
}
|
||||
|
||||
// Stop 停止 DNF 仓储组件并关闭项目侧 SQL 连接池。
|
||||
func (c *RepoComponent) Stop(ctx context.Context) error {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
var errs []error
|
||||
if c.component != nil {
|
||||
errs = append(errs, c.component.Stop(ctx))
|
||||
}
|
||||
if c.db != nil {
|
||||
errs = append(errs, c.db.Close())
|
||||
}
|
||||
if c.redis != nil {
|
||||
errs = append(errs, c.redis.Close())
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// Group 返回启动后的 DNF 玩家仓储聚合。
|
||||
func (c *RepoComponent) Group() (dnfrepo.Group, bool) {
|
||||
if c == nil || c.component == nil {
|
||||
return dnfrepo.Group{}, false
|
||||
}
|
||||
group, ok := c.component.Group()
|
||||
if !ok {
|
||||
return dnfrepo.Group{}, false
|
||||
}
|
||||
if c.redis != nil {
|
||||
group = dnfrepo.NewCachedGroup(group, c.redis, c.cacheOpts)
|
||||
}
|
||||
return group, true
|
||||
}
|
||||
|
||||
// Snapshot 返回 DNF 仓储装配状态。
|
||||
func (c *RepoComponent) Snapshot() RepoSnapshot {
|
||||
if c == nil || c.component == nil {
|
||||
return RepoSnapshot{}
|
||||
}
|
||||
snapshot := RepoSnapshot{Driver: c.driver}
|
||||
switch component := c.component.(type) {
|
||||
case *mysql.Component:
|
||||
value := component.Snapshot()
|
||||
snapshot.MySQL = &value
|
||||
case *sqliterepo.Component:
|
||||
value := component.Snapshot()
|
||||
snapshot.SQLite = &value
|
||||
}
|
||||
return snapshot
|
||||
}
|
||||
|
||||
func (c *RepoComponent) pingRedis(ctx context.Context) error {
|
||||
if c == nil || c.redis == nil {
|
||||
return nil
|
||||
}
|
||||
_, err := c.redis.Do(ctx, "PING")
|
||||
return err
|
||||
}
|
||||
|
||||
func (c RepositoryConfig) redisTimeout() time.Duration {
|
||||
if c.RedisTimeoutSeconds <= 0 {
|
||||
return 2 * time.Second
|
||||
}
|
||||
return time.Duration(c.RedisTimeoutSeconds) * time.Second
|
||||
}
|
||||
|
||||
func (c RepositoryConfig) redisTTL() time.Duration {
|
||||
if c.RedisTTLSeconds <= 0 {
|
||||
return 0
|
||||
}
|
||||
return time.Duration(c.RedisTTLSeconds) * time.Second
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package dnf
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/repository"
|
||||
)
|
||||
|
||||
func TestSQLiteRepoComponentLifecycle(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "dnf90.db")
|
||||
component, err := NewRepoComponent(nil, RepositoryConfig{
|
||||
Enabled: true,
|
||||
Driver: "sqlite",
|
||||
SQLitePath: path,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
if err := component.Preflight(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := component.Start(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
group, ok := component.Group()
|
||||
if !ok {
|
||||
t.Fatal("SQLite repository group is unavailable")
|
||||
}
|
||||
if group.CharacterSettlement == nil {
|
||||
t.Fatal("SQLite repository group does not expose the atomic quest/inventory settlement")
|
||||
}
|
||||
if err := group.Account.Save(ctx, repository.AccountRecord{AccountID: "account-1"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if snapshot := component.Snapshot(); snapshot.Driver != "sqlite" || snapshot.SQLite == nil || snapshot.MySQL != nil {
|
||||
t.Fatalf("SQLite component snapshot = %+v", snapshot)
|
||||
}
|
||||
if err := component.Stop(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepositoryConfigKeepsLegacyMySQLDefault(t *testing.T) {
|
||||
cfg := Config{Repository: RepositoryConfig{
|
||||
Enabled: true,
|
||||
MySQLDSN: "root:password@tcp(127.0.0.1:3306)/dnf",
|
||||
ShardID: "9999",
|
||||
TablePrefix: "dnf",
|
||||
}}
|
||||
cfg.Normalize(nil)
|
||||
if cfg.Repository.Driver != "mysql" {
|
||||
t.Fatalf("default repository driver = %q, want mysql", cfg.Repository.Driver)
|
||||
}
|
||||
if err := cfg.Validate(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user