feat: 初始化多机器人服务端项目框架
基于Go语言构建多机器人服务端框架,包含配置管理、事件总线、依赖注入等核心模块 添加项目基础结构、README、gitignore和初始代码实现
This commit is contained in:
143
internal/config/config.go
Normal file
143
internal/config/config.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// Config 应用配置结构
|
||||
type Config struct {
|
||||
Server ServerConfig `toml:"server"`
|
||||
Log LogConfig `toml:"log"`
|
||||
Protocol ProtocolConfig `toml:"protocol"`
|
||||
}
|
||||
|
||||
// ServerConfig 服务器配置
|
||||
type ServerConfig struct {
|
||||
Host string `toml:"host"`
|
||||
Port int `toml:"port"`
|
||||
}
|
||||
|
||||
// LogConfig 日志配置
|
||||
type LogConfig struct {
|
||||
Level string `toml:"level"`
|
||||
Output string `toml:"output"`
|
||||
MaxSize int `toml:"max_size"`
|
||||
MaxBackups int `toml:"max_backups"`
|
||||
MaxAge int `toml:"max_age"`
|
||||
}
|
||||
|
||||
// ProtocolConfig 协议配置
|
||||
type ProtocolConfig struct {
|
||||
Name string `toml:"name"`
|
||||
Version string `toml:"version"`
|
||||
Options map[string]string `toml:"options"`
|
||||
}
|
||||
|
||||
// ConfigManager 配置管理器
|
||||
type ConfigManager struct {
|
||||
configPath string
|
||||
config *Config
|
||||
logger *zap.Logger
|
||||
mu sync.RWMutex
|
||||
callbacks []func(*Config)
|
||||
}
|
||||
|
||||
// NewConfigManager 创建配置管理器
|
||||
func NewConfigManager(configPath string, logger *zap.Logger) *ConfigManager {
|
||||
return &ConfigManager{
|
||||
configPath: configPath,
|
||||
logger: logger,
|
||||
callbacks: make([]func(*Config), 0),
|
||||
}
|
||||
}
|
||||
|
||||
// Load 加载配置文件
|
||||
func (cm *ConfigManager) Load() error {
|
||||
cm.mu.Lock()
|
||||
defer cm.mu.Unlock()
|
||||
|
||||
var cfg Config
|
||||
if _, err := toml.DecodeFile(cm.configPath, &cfg); err != nil {
|
||||
return fmt.Errorf("failed to decode config: %w", err)
|
||||
}
|
||||
|
||||
cm.config = &cfg
|
||||
cm.logger.Info("Config loaded successfully",
|
||||
zap.String("path", cm.configPath),
|
||||
zap.String("server", fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)),
|
||||
)
|
||||
|
||||
// 触发回调
|
||||
for _, cb := range cm.callbacks {
|
||||
cb(cm.config)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get 获取当前配置
|
||||
func (cm *ConfigManager) Get() *Config {
|
||||
cm.mu.RLock()
|
||||
defer cm.mu.RUnlock()
|
||||
return cm.config
|
||||
}
|
||||
|
||||
// Reload 重新加载配置
|
||||
func (cm *ConfigManager) Reload() error {
|
||||
return cm.Load()
|
||||
}
|
||||
|
||||
// RegisterCallback 注册配置变更回调
|
||||
func (cm *ConfigManager) RegisterCallback(callback func(*Config)) {
|
||||
cm.mu.Lock()
|
||||
defer cm.mu.Unlock()
|
||||
cm.callbacks = append(cm.callbacks, callback)
|
||||
}
|
||||
|
||||
// Watch 监听配置文件变化
|
||||
func (cm *ConfigManager) Watch() error {
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create watcher: %w", err)
|
||||
}
|
||||
|
||||
if err := watcher.Add(cm.configPath); err != nil {
|
||||
return fmt.Errorf("failed to watch config file: %w", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-watcher.Events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if event.Op&fsnotify.Write == fsnotify.Write {
|
||||
cm.logger.Info("Config file changed, reloading...",
|
||||
zap.String("file", event.Name))
|
||||
if err := cm.Reload(); err != nil {
|
||||
cm.logger.Error("Failed to reload config",
|
||||
zap.Error(err))
|
||||
}
|
||||
}
|
||||
case err, ok := <-watcher.Errors:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
cm.logger.Error("Watcher error", zap.Error(err))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close 关闭配置管理器
|
||||
func (cm *ConfigManager) Close() error {
|
||||
return nil
|
||||
}
|
||||
113
internal/config/config_test.go
Normal file
113
internal/config/config_test.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestConfigManager_Load(t *testing.T) {
|
||||
// 创建临时配置文件
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "test_config.toml")
|
||||
configContent := `
|
||||
[server]
|
||||
host = "127.0.0.1"
|
||||
port = 8080
|
||||
|
||||
[log]
|
||||
level = "debug"
|
||||
output = "stdout"
|
||||
max_size = 100
|
||||
max_backups = 3
|
||||
max_age = 7
|
||||
|
||||
[protocol]
|
||||
name = "test"
|
||||
version = "1.0"
|
||||
|
||||
[protocol.options]
|
||||
key = "value"
|
||||
`
|
||||
err := os.WriteFile(configPath, []byte(configContent), 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create config file: %v", err)
|
||||
}
|
||||
|
||||
logger := zap.NewNop()
|
||||
cm := NewConfigManager(configPath, logger)
|
||||
|
||||
err = cm.Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load config: %v", err)
|
||||
}
|
||||
|
||||
cfg := cm.Get()
|
||||
if cfg == nil {
|
||||
t.Fatal("Config is nil")
|
||||
}
|
||||
|
||||
if cfg.Server.Host != "127.0.0.1" {
|
||||
t.Errorf("Expected host '127.0.0.1', got '%s'", cfg.Server.Host)
|
||||
}
|
||||
|
||||
if cfg.Server.Port != 8080 {
|
||||
t.Errorf("Expected port 8080, got %d", cfg.Server.Port)
|
||||
}
|
||||
|
||||
if cfg.Log.Level != "debug" {
|
||||
t.Errorf("Expected log level 'debug', got '%s'", cfg.Log.Level)
|
||||
}
|
||||
|
||||
if cfg.Protocol.Name != "test" {
|
||||
t.Errorf("Expected protocol name 'test', got '%s'", cfg.Protocol.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitLogger(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg *LogConfig
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "stdout logger",
|
||||
cfg: &LogConfig{
|
||||
Level: "info",
|
||||
Output: "stdout",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "stderr logger",
|
||||
cfg: &LogConfig{
|
||||
Level: "error",
|
||||
Output: "stderr",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "file logger",
|
||||
cfg: &LogConfig{
|
||||
Level: "debug",
|
||||
Output: filepath.Join(t.TempDir(), "test.log"),
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
logger, err := InitLogger(tt.cfg)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("InitLogger() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !tt.wantErr && logger == nil {
|
||||
t.Error("Expected non-nil logger")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
69
internal/config/logger.go
Normal file
69
internal/config/logger.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
// InitLogger 初始化日志
|
||||
func InitLogger(cfg *LogConfig) (*zap.Logger, error) {
|
||||
encoderConfig := zapcore.EncoderConfig{
|
||||
TimeKey: "time",
|
||||
LevelKey: "level",
|
||||
NameKey: "logger",
|
||||
CallerKey: "caller",
|
||||
MessageKey: "msg",
|
||||
StacktraceKey: "stacktrace",
|
||||
LineEnding: zapcore.DefaultLineEnding,
|
||||
EncodeLevel: zapcore.LowercaseLevelEncoder,
|
||||
EncodeTime: zapcore.ISO8601TimeEncoder,
|
||||
EncodeDuration: zapcore.SecondsDurationEncoder,
|
||||
EncodeCaller: zapcore.ShortCallerEncoder,
|
||||
}
|
||||
|
||||
var writer zapcore.WriteSyncer
|
||||
switch cfg.Output {
|
||||
case "stdout":
|
||||
writer = zapcore.AddSync(os.Stdout)
|
||||
case "stderr":
|
||||
writer = zapcore.AddSync(os.Stderr)
|
||||
default:
|
||||
// 创建日志目录
|
||||
if err := os.MkdirAll(filepath.Dir(cfg.Output), 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create log directory: %w", err)
|
||||
}
|
||||
file, err := os.OpenFile(cfg.Output, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open log file: %w", err)
|
||||
}
|
||||
writer = zapcore.AddSync(file)
|
||||
}
|
||||
|
||||
// 解析日志级别
|
||||
var level zapcore.Level
|
||||
switch cfg.Level {
|
||||
case "debug":
|
||||
level = zapcore.DebugLevel
|
||||
case "info":
|
||||
level = zapcore.InfoLevel
|
||||
case "warn":
|
||||
level = zapcore.WarnLevel
|
||||
case "error":
|
||||
level = zapcore.ErrorLevel
|
||||
default:
|
||||
level = zapcore.InfoLevel
|
||||
}
|
||||
|
||||
core := zapcore.NewCore(
|
||||
zapcore.NewJSONEncoder(encoderConfig),
|
||||
writer,
|
||||
level,
|
||||
)
|
||||
|
||||
logger := zap.New(core, zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel))
|
||||
return logger, nil
|
||||
}
|
||||
Reference in New Issue
Block a user