环境准备

mysql搭建

确保你的系统上已经安装了Docker。你可以从Docker官网下载并安装Docker Desktop(对于Windows和Mac用户)或者通过命令行安装Docker Engine(对于Linux用户)。

拉取MySQL镜像

使用Docker拉取官方的MySQL镜像。打开终端或命令行界面,然后运行以下命令:

1
2
3
4
5
6
docker pull mysql:latest

# 通过代理拉取
docker pull m.daocloud.io/docker.io/mysql:latest
# 修改tag
docker tag m.daocloud.io/docker.io/mysql:latest mysql:latest

运行MySQL容器

在运行MySQL容器之前,你需要确定几个参数,比如数据库的版本、root用户的密码、数据存储的位置等。以下是一个运行MySQL容器的示例命令:

1
2
3
4
docker run --name king-mysql \
-e MYSQL_ROOT_PASSWORD=king-pass \
-p 3306:3306 \
-d mysql:latest

这里是一些参数的解释:

  • --name king-mysql:为你的容器指定一个名字,这里命名为king-mysql
  • -e MYSQL_ROOT_PASSWORD=king-pass:设置root用户的密码为king-pass
  • -d:在后台运行容器。
  • -p:端口映射,将MySQL容器的3306端口映射到本机的3306端口
  • mysql:latest:使用最新版本的MySQL镜像。

redis 搭建

确保你的系统上已经安装了Docker。你可以从Docker官网下载并安装Docker Desktop(对于Windows和Mac用户)或者通过命令行安装Docker Engine(对于Linux用户)。

拉取REDIS镜像

使用Docker拉取官方的REDIS镜像。打开终端或命令行界面,然后运行以下命令:

1
2
3
4
5
6
docker pull redis:latest

# 通过代理拉取
docker pull m.daocloud.io/docker.io/redis:latest
# 修改tag
docker tag m.daocloud.io/docker.io/redis:latest redis:latest

运行REDIS容器

在运行REDIS容器之前,你需要确定几个参数,比如数据库的版本、root用户的密码、数据存储的位置等。以下是一个运行REDIS容器的示例命令:

1
2
3
4
docker run --name king-redis \
-p 6379:6379 \
-d redis:latest \
redis-server --save 60 1 --loglevel warning --requirepass king-pass

这里是一些参数的解释:

  • -p 6379:6379:把容器 6379 映射到本地 6379,宿主机才能访问。
  • --requirepass king-pass:给 Redis 加密码,防止裸奔。

ollama部署llm

可以参考之前的文章进行:https://hua-ri.cn/2025/08/llm/ollama/ollama-bu-shu-qwen25-7b/

安装Ollama

首先,从 Ollama 官网 下载安装包,并按照提示完成安装。

img.png

部署llm

在 Ollama 官网的 Models 页面 中,可以找到 Ollama 支持的大模型列表。

1
2
3
4
5
# 下载ollama模型
ollama pull qwen3:8b

# 运行ollama模型(如果本地不存在,会自动拉取)
ollama run qwen3:8b

image-20250904230432068

基础模块

配置模块

采用viper模块进行配置的管理。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
package config

import (
"github.com/spf13/viper"
)

type Config struct {
AppName string `mapstructure:"app_name"`
Env string `mapstructure:"env"`
Server ServerConfig `mapstructure:"server"`
Llm LLMConfig `mapstructure:"llm"`
Log LogConfig `mapstructure:"log"`
Mysql MysqlConfig `mapstructure:"mysql"`
Redis RedisConfig `mapstructure:"redis"`
}

type ServerConfig struct {
Port int `mapstructure:"port"`
Mode string `mapstructure:"mode"`
}

type LLMConfig struct {
LlmApiKey string `mapstructure:"llm_api_key"`
LlmModel string `mapstructure:"llm_model"`
LlmBaseUrl string `mapstructure:"llm_base_url"`
}

type LogConfig struct {
LogName string `mapstructure:"logname"`
Level string `mapstructure:"level"`
Format string `mapstructure:"format"`
Suffix string `mapstructure:"suffix"`
ShowLine bool `mapstructure:"showline"`
EncodeLevel string `mapstructure:"encodelevel"`
StacktraceKey string `mapstructure:"stacktracekey"`
LogInConsole bool `mapstructure:"loginconsole"`
MaxSize int `mapstructure:"maxsize"`
MaxBackups int `mapstructure:"maxbackups"`
MaxAge int `mapstructure:"maxage"`
Compress bool `mapstructure:"compress"`
CallerKey string `mapstructure:"callerKey"`
}

type MysqlConfig struct {
Driver string `mapstructure:"driver"`
Dsn string `mapstructure:"dsn"`
LogMode string `mapstructure:"logMode"`
Prefix string `mapstructure:"prefix"`
}

type RedisConfig struct {
Addr string `mapstructure:"addr"`
Password string `mapstructure:"password"`
DB int `mapstructure:"db"`
PoolSize int `mapstructure:"pool"`
}

var cfg = &Config{}

func NewViper(configPath string) (*Config, error) {
viper.SetConfigFile(configPath)
if err := viper.ReadInConfig(); err != nil {
return nil, err
}
if err := viper.Unmarshal(cfg); err != nil {
return nil, err
}
return cfg, nil
}

func GetConfig() *Config {
return cfg
}

配置示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
env: local  # 本地开发环境
server:
port: 8080
mode: debug # debug | release | test

llm:
llm_api_key: sk-ollama
llm_model: qwen3:8b
llm_base_url: http://localhost:11434/v1

log:
logname: king-agent.log
level: info
format: console # 输出格式:console 或 json
suffix: king-agent # 日志文件后缀
show-line: true # 是否显示代码行号
encode-level: LowercaseLevelEncoder # 级别编码:lowercase / uppercase / capital
stacktrace-key: stacktrace # zap 堆栈字段名
log-in-console: true # 是否同时输出到控制台
max-size: 50 # MB
max-backups: 3
max-age: 30 # 天
compress: false # 是否压缩旧日志
caller-key: "" # zap caller 字段名

mysql:
driver: mysql
dsn: "root:king-pass@tcp(127.0.0.1:3306)/al_smart_library?charset=utf8mb4&parseTime=True&loc=Local"
logMode: info
prefix: ""

redis:
addr: "127.0.0.1:6379"
password: "king-pass"
db: 0
pool: 10

日志模块

logger.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package logger

import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"king-agent/pkg/config"
)

func NewLogger(appName string, opt *config.LogConfig) (*zap.Logger, error) {
level := parseLevel(opt.Level)
writer := getWriteSyncer(appName, opt)
encoder := getEncoder(opt)

core := zapcore.NewCore(encoder, writer, level)
logger := zap.New(core, zap.AddCaller())
if opt.ShowLine {
logger = logger.WithOptions(zap.AddCaller())
}
zap.ReplaceGlobals(logger)
return logger, nil
}

support.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
package logger

import (
"fmt"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
"king-agent/pkg/config"
"os"
"os/user"
"path"
"runtime"
"time"
)

func getLogDir(appName string) string {
var dir string
if u, err := user.Current(); err != nil || runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
dir = path.Join(u.HomeDir, "logs", appName)
} else {
dir = fmt.Sprintf("/home/admin/logs/%s", appName)
}
_ = os.MkdirAll(dir, 0755)
return dir
}

func getLogFilePath(appName string, opt *config.LogConfig) string {
return path.Join(getLogDir(appName), opt.LogName+opt.Suffix)
}

func parseLevel(l string) zapcore.Level {
switch l {
case "debug":
return zapcore.DebugLevel
case "warn":
return zapcore.WarnLevel
case "error":
return zapcore.ErrorLevel
case "panic":
return zapcore.PanicLevel
case "fatal":
return zapcore.FatalLevel
default:
return zapcore.InfoLevel
}
}

func getEncoder(opt *config.LogConfig) zapcore.Encoder {
cfg := zapcore.EncoderConfig{
MessageKey: "msg",
LevelKey: "level",
TimeKey: "ts",
NameKey: "logger",
CallerKey: opt.CallerKey,
StacktraceKey: opt.StacktraceKey,
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.LowercaseLevelEncoder,
EncodeTime: customTimeEncoder(opt.Suffix),
EncodeDuration: zapcore.SecondsDurationEncoder,
EncodeCaller: zapcore.FullCallerEncoder,
}
if opt.Format == "json" {
return zapcore.NewJSONEncoder(cfg)
}
return zapcore.NewConsoleEncoder(cfg)
}

func customTimeEncoder(suffix string) zapcore.TimeEncoder {
return func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
enc.AppendString(t.Format("2006-01-02 15:04:05.000" + suffix))
}
}

func getWriteSyncer(appName string, opt *config.LogConfig) zapcore.WriteSyncer {
logPath := getLogFilePath(appName, opt)
lj := &lumberjack.Logger{
Filename: logPath,
MaxSize: opt.MaxSize,
MaxBackups: opt.MaxBackups,
MaxAge: opt.MaxAge,
Compress: opt.Compress,
}
var syncers []zapcore.WriteSyncer
syncers = append(syncers, zapcore.AddSync(lj))
if opt.LogInConsole {
syncers = append(syncers, zapcore.AddSync(os.Stdout))
}
return zapcore.NewMultiWriteSyncer(syncers...)
}

database

mysql

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package mysql_database

import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
"king-agent/pkg/config"
"sync"
)

func NewMysqlDatabase(cfg *config.Config) (*gorm.DB, error) {
return gorm.Open(mysql.Open(cfg.Mysql.Dsn), &gorm.Config{
Logger: parserGormLogMode(cfg.Mysql.LogMode),
NamingStrategy: schema.NamingStrategy{
SingularTable: true, // 表名不用复数
TablePrefix: cfg.Mysql.Prefix,
},
})
}

var (
migrateModels []interface{}
mu sync.Mutex
)

// Register 注册一个或多个需要 AutoMigrate 的模型。
// 建议在包级 init 函数里调用,例如:
//
// func init() {
// db.Register(&User{}, &Role{})
// }
func Register(models ...interface{}) {
mu.Lock()
defer mu.Unlock()
migrateModels = append(migrateModels, models...)
}

// AutoMigrate 根据已注册的模型一次性执行迁移。
// 通常只在程序启动时调用一次。
func AutoMigrate(db *gorm.DB) error {
mu.Lock()
defer mu.Unlock()
if len(migrateModels) == 0 {
return nil
}
return db.AutoMigrate(migrateModels...)
}

func parserGormLogMode(logMode string) logger.Interface {
var logLevel logger.LogLevel
switch logMode {
case "silent":
logLevel = logger.Silent
case "error":
logLevel = logger.Error
case "warn":
logLevel = logger.Warn
default:
logLevel = logger.Info
}

return logger.Default.LogMode(logLevel)
}

redis

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package redis_database

import (
"github.com/redis/go-redis/v9"
"king-agent/pkg/config"
)

func NewRedisDatabase(cfg *config.Config) (*redis.Client, error) {
return redis.NewClient(&redis.Options{
Addr: cfg.Redis.Addr,
Password: cfg.Redis.Password,
DB: cfg.Redis.DB,
PoolSize: cfg.Redis.PoolSize,
}), nil
}