feat: run.sh四模式(dev/docker/build/binary) + CLI flags + Redka内嵌Redis + Agent复制修复 + 服务器编辑IP字段 + 安装Agent按钮

This commit is contained in:
attacker committed 2026-06-18 03:36:04 +00:00
1 parent d0b94d39c0
commit 2354959811
14 files changed
+1355 -575

No files matched your search

+101 -82
View File
@@ -1,82 +1,101 @@
# ============================================================
# AIOps Backend + Agent 多阶段 Dockerfile
# Stage 1: 编译后端 (Go 1.21)
# Stage 2: 交叉编译 Agent (Go 1.24, linux-amd64/arm64 + darwin-arm64)
# Stage 3: 运行时 (Alpine)
# 署名 Logan.Li
# ============================================================
# ==================== Stage 1: Backend Builder ====================
FROM golang:1.21-alpine AS builder
WORKDIR /app
RUN apk add --no-cache git ca-certificates tzdata
ENV TZ=Asia/Shanghai
ENV GOPROXY=https://goproxy.cn,direct
ENV GOSUMDB=off
# 复制后端 go.mod(利用 Docker 层缓存)
COPY backend/go.mod backend/go.sum ./
RUN go mod download
# 复制后端源码
COPY backend/ ./
# 构建后端二进制
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
-ldflags="-w -s" \
-o ezops ./cmd/
# ==================== Stage 2: Agent Builder ====================
FROM golang:1.24-alpine AS agent-builder
WORKDIR /agent
ENV GOPROXY=https://goproxy.cn,direct
ENV GOSUMDB=off
# 复制 agent go.mod
COPY agent/go.mod agent/go.sum ./
RUN go mod download
# 复制 agent 源码
COPY agent/ ./
# 交叉编译三个平台的 agent 二进制
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o bin/aiops-agent-linux-amd64 . && \
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o bin/aiops-agent-linux-arm64 . && \
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" -o bin/aiops-agent-darwin-arm64 .
# ==================== Stage 3: Runtime ====================
FROM alpine:latest
# 安装运行时依赖(curl=健康检查,openssh-client=SSH远程安装agent)
RUN apk --no-cache add ca-certificates tzdata curl openssh-client
ENV TZ=Asia/Shanghai
WORKDIR /app
# 复制后端二进制
COPY --from=builder /app/ezops .
# 创建运行时目录
RUN mkdir -p /app/logs /app/uploads /app/agent/bin /app/agent/deploy
# 复制 agent 二进制(三个平台)
COPY --from=agent-builder /agent/bin/aiops-agent-linux-amd64 /app/agent/bin/
COPY --from=agent-builder /agent/bin/aiops-agent-linux-arm64 /app/agent/bin/
COPY --from=agent-builder /agent/bin/aiops-agent-darwin-arm64 /app/agent/bin/
# 复制 agent 安装脚本和 systemd service 模板
COPY agent/deploy/install.sh /app/agent/deploy/
COPY agent/deploy/aiops-agent.service /app/agent/deploy/
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
CMD ["./ezops"]
1|# ============================================================
2|# AIOps Backend + Agent 多阶段 Dockerfile
3|# Stage 1: 编译后端 (Go 1.21)
4|# Stage 2: 交叉编译 Agent (Go 1.24, linux-amd64/arm64 + darwin-arm64)
5|# Stage 3: 运行时 (Alpine)
6|# 署名 Logan.Li
7|# ============================================================
8|
9|# ==================== Stage 1: Backend Builder ====================
10|FROM golang:1.21-alpine AS builder
11|
12|WORKDIR /app
13|
14|<<<<<<< HEAD
15|=======
16|# 使用国内 Alpine 镜像源加速
17|RUN sed -i 's|dl-cdn.alpinelinux.org|mirrors.aliyun.com|g' /etc/apk/repositories
18|
19|# 安装构建依赖
20|>>>>>>> ca18ead (feat: run.sh四模式(dev/docker/build/binary) + CLI flags + Redka内嵌Redis + Agent复制修复 + 服务器编辑IP字段 + 安装Agent按钮)
21|RUN apk add --no-cache git ca-certificates tzdata
22|
23|ENV TZ=Asia/Shanghai
24|ENV GOPROXY=https://goproxy.cn,direct
25|# 跳过 toolchain 校验(GOTOOLCHAIN 自动下载 Go 1.22)
26|ENV GONOSUMCHECK=*
27|# 自动下载 Go 1.22 工具链(Redka 需要)
28|ENV GOTOOLCHAIN=go1.22.2
29|
30|# 复制后端 go.mod(利用 Docker 层缓存)
31|COPY backend/go.mod backend/go.sum ./
32|RUN go mod download
33|
34|# 复制后端源码
35|COPY backend/ ./
36|
37|# 构建后端二进制
38|RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
39| -ldflags="-w -s" \
40| -o ezops ./cmd/
41|
42|# ==================== Stage 2: Agent Builder ====================
43|FROM golang:1.24-alpine AS agent-builder
44|
45|WORKDIR /agent
46|
47|ENV GOPROXY=https://goproxy.cn,direct
48|ENV GOSUMDB=off
49|
50|# 复制 agent go.mod
51|COPY agent/go.mod agent/go.sum ./
52|RUN go mod download
53|
54|# 复制 agent 源码
55|COPY agent/ ./
56|
57|# 交叉编译三个平台的 agent 二进制
58|RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o bin/aiops-agent-linux-amd64 . && \
59| CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o bin/aiops-agent-linux-arm64 . && \
60| CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" -o bin/aiops-agent-darwin-arm64 .
61|
62|# ==================== Stage 3: Runtime ====================
63|FROM alpine:latest
64|
65|<<<<<<< HEAD
66|# 安装运行时依赖(curl=健康检查,openssh-client=SSH远程安装agent)
67|RUN apk --no-cache add ca-certificates tzdata curl openssh-client
68|=======
69|# 使用国内 Alpine 镜像源加速
70|RUN sed -i 's|dl-cdn.alpinelinux.org|mirrors.aliyun.com|g' /etc/apk/repositories
71|
72|# 安装运行时依赖
73|RUN apk --no-cache add ca-certificates tzdata curl
74|>>>>>>> ca18ead (feat: run.sh四模式(dev/docker/build/binary) + CLI flags + Redka内嵌Redis + Agent复制修复 + 服务器编辑IP字段 + 安装Agent按钮)
75|
76|ENV TZ=Asia/Shanghai
77|
78|WORKDIR /app
79|
80|# 复制后端二进制
81|COPY --from=builder /app/ezops .
82|
83|# 创建运行时目录
84|RUN mkdir -p /app/logs /app/uploads /app/agent/bin /app/agent/deploy
85|
86|# 复制 agent 二进制(三个平台)
87|COPY --from=agent-builder /agent/bin/aiops-agent-linux-amd64 /app/agent/bin/
88|COPY --from=agent-builder /agent/bin/aiops-agent-linux-arm64 /app/agent/bin/
89|COPY --from=agent-builder /agent/bin/aiops-agent-darwin-arm64 /app/agent/bin/
90|
91|# 复制 agent 安装脚本和 systemd service 模板
92|COPY agent/deploy/install.sh /app/agent/deploy/
93|COPY agent/deploy/aiops-agent.service /app/agent/deploy/
94|
95|EXPOSE 8080
96|
97|HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
98| CMD curl -f http://localhost:8080/health || exit 1
99|
100|CMD ["./ezops"]
101|
+189 -13
View File
@@ -2,6 +2,8 @@ package main
import (
"context"
"flag"
"fmt"
"net/http"
"os"
"os/signal"
@@ -12,18 +14,50 @@ import (
"ezops/internal/config"
"ezops/pkg/database"
"ezops/pkg/logger"
ezredis "ezops/pkg/redis"
"github.com/gin-gonic/gin"
"github.com/spf13/viper"
"go.uber.org/zap"
)
// 版本信息(通过编译时 ldflags 注入)
// 版本信息(通过编译时 ldflags 注入)// Logan.Li
var (
Version = "dev"
BuildTime = "unknown"
GitCommit = "unknown"
)
// CLI 参数定义 // Logan.Li
var (
flagPort = flag.String("port", "", "服务端口 (默认: 8080)")
flagConfig = flag.String("config", "", "配置文件路径 (默认: config.yaml)")
flagMode = flag.String("mode", "", "运行模式: development, production, release")
flagLogLevel = flag.String("log-level", "", "日志级别: debug, info, warn, error")
// 数据库参数
flagDBDriver = flag.String("db-driver", "", "数据库驱动: sqlite, mysql, postgres (默认: sqlite)")
flagDBHost = flag.String("db-host", "", "数据库主机")
flagDBPort = flag.Int("db-port", 0, "数据库端口")
flagDBUser = flag.String("db-user", "", "数据库用户名")
flagDBPassword = flag.String("db-password", "", "数据库密码")
flagDBName = flag.String("db-name", "", "数据库名 / SQLite 文件路径")
flagDBCharset = flag.String("db-charset", "", "数据库字符集")
// Redis 参数
flagRedisHost = flag.String("redis-host", "", "Redis 主机")
flagRedisPort = flag.Int("redis-port", 0, "Redis 端口")
flagRedisPassword = flag.String("redis-password", "", "Redis 密码")
flagRedisDB = flag.Int("redis-db", 0, "Redis 数据库编号")
// JWT 参数
flagJWTSecret = flag.String("jwt-secret", "", "JWT 密钥")
flagJWTExpire = flag.Int("jwt-expire", 0, "JWT 过期时间 (小时)")
// 显示版本
flagVersion = flag.Bool("version", false, "显示版本信息")
)
// @title AIOps运维平台 API
// @version 1.0
// @description AIOps智能运维平台后端API文档
@@ -45,32 +79,67 @@ var (
// @description Type "Bearer" followed by a space and JWT token.
func main() {
// 加载配置
cfg := config.Load()
// 解析 CLI 参数 // Logan.Li
flag.Parse()
// 显示版本信息 // Logan.Li
if *flagVersion {
fmt.Printf("AIOps 智能运维平台\n")
fmt.Printf(" 版本: %s\n", Version)
fmt.Printf(" 构建: %s\n", BuildTime)
fmt.Printf(" 提交: %s\n", GitCommit)
os.Exit(0)
}
// 加载配置 // Logan.Li
cfg := loadConfig()
// 初始化日志
log := logger.New(cfg.LogLevel)
defer log.Sync()
log.Info("AIOps 启动中",
zap.String("version", Version),
zap.String("mode", cfg.Mode),
zap.String("port", cfg.Port),
)
// 设置Gin模式
if cfg.Mode == "production" {
if cfg.Mode == "production" || cfg.Mode == "release" {
gin.SetMode(gin.ReleaseMode)
}
// 初始化数据库
db, err := database.New(cfg.Database)
if err != nil {
log.Fatal("failed to connect database", zap.Error(err))
log.Fatal("数据库连接失败", zap.Error(err))
}
// 自动迁移
if err := database.AutoMigrate(db); err != nil {
log.Fatal("failed to migrate database", zap.Error(err))
log.Fatal("数据库迁移失败", zap.Error(err))
}
// 初始化种子数据
seedData(db)
// 初始化缓存(Redka 内嵌 / Redis 外部)// Logan.Li
cache, err := ezredis.New(ezredis.Config{
Driver: "redka", // 默认内嵌 Redka,零依赖
Host: cfg.Redis.Host,
Port: cfg.Redis.Port,
Password: cfg.Redis.Password,
DB: cfg.Redis.DB,
DataDir: "data/redka",
})
if err != nil {
log.Warn("缓存初始化失败,继续启动", zap.Error(err))
cache = nil
} else {
log.Info("缓存已就绪", zap.String("driver", "redka"))
defer cache.Close()
}
// 创建Gin实例
r := gin.New()
@@ -85,11 +154,13 @@ func main() {
"version": Version,
"build_time": BuildTime,
"git_commit": GitCommit,
"mode": cfg.Mode,
"db_driver": cfg.Database.Driver,
})
})
// 注册路由
routes.Register(r, db, log, cfg)
routes.Register(r, db, log, cfg, cache)
// 创建HTTP服务器
srv := &http.Server{
@@ -99,28 +170,133 @@ func main() {
// 优雅关闭
go func() {
log.Info("服务已启动", zap.String("addr", srv.Addr))
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal("failed to start server", zap.Error(err))
log.Fatal("服务启动失败", zap.Error(err))
}
}()
log.Info("server started", zap.String("port", cfg.Port))
// 等待中断信号
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Info("shutting down server...")
log.Info("正在关闭服务...")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("server forced to shutdown", zap.Error(err))
log.Fatal("强制关闭服务", zap.Error(err))
}
log.Info("server exited")
log.Info("服务已退出")
}
// loadConfig 加载配置(CLI flags 优先于 config.yaml)// Logan.Li
func loadConfig() *config.Config {
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath("./configs")
viper.AddConfigPath(".")
// 如果指定了配置文件路径,使用它 // Logan.Li
if *flagConfig != "" {
viper.SetConfigFile(*flagConfig)
}
// 默认值(独立二进制模式:SQLite + 无 Redis)// Logan.Li
viper.SetDefault("mode", "release")
viper.SetDefault("port", "8080")
viper.SetDefault("log_level", "info")
viper.SetDefault("database.driver", "sqlite")
viper.SetDefault("database.host", "localhost")
viper.SetDefault("database.port", 3306)
viper.SetDefault("database.user", "")
viper.SetDefault("database.password", "")
viper.SetDefault("database.dbname", "data/aiops.db")
viper.SetDefault("database.charset", "utf8mb4")
viper.SetDefault("database.max_conn", 100)
viper.SetDefault("database.max_idle", 10)
viper.SetDefault("redis.host", "localhost")
viper.SetDefault("redis.port", 6379)
viper.SetDefault("redis.password", "")
viper.SetDefault("redis.db", 0)
viper.SetDefault("jwt.secret", "aiops-jwt-secret-change-in-production")
viper.SetDefault("jwt.expire_hour", 24)
viper.SetDefault("websocket.read_buffer_size", 1024)
viper.SetDefault("websocket.write_buffer_size", 1024)
// 读取配置文件(不存在也不报错)// Logan.Li
if err := viper.ReadInConfig(); err != nil {
// 配置文件不存在是正常的(独立二进制模式)
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
fmt.Fprintf(os.Stderr, "警告: 读取配置文件失败: %v,使用默认配置\n", err)
}
}
// 环境变量覆盖 // Logan.Li
viper.AutomaticEnv()
// CLI flags 覆盖(最高优先级)// Logan.Li
applyFlags()
// 解析配置 // Logan.Li
var cfg config.Config
if err := viper.Unmarshal(&cfg); err != nil {
fmt.Fprintf(os.Stderr, "配置解析失败: %v\n", err)
os.Exit(1)
}
return &cfg
}
// applyFlags 将 CLI 参数映射到 viper(覆盖 config.yaml 和默认值)// Logan.Li
func applyFlags() {
// 检查是否有任何 flag 被显式设置 // Logan.Li
flag.Visit(func(f *flag.Flag) {
switch f.Name {
case "port":
viper.Set("port", *flagPort)
case "mode":
viper.Set("mode", *flagMode)
case "log-level":
viper.Set("log_level", *flagLogLevel)
case "db-driver":
viper.Set("database.driver", *flagDBDriver)
case "db-host":
viper.Set("database.host", *flagDBHost)
case "db-port":
viper.Set("database.port", *flagDBPort)
case "db-user":
viper.Set("database.user", *flagDBUser)
case "db-password":
viper.Set("database.password", *flagDBPassword)
case "db-name":
viper.Set("database.dbname", *flagDBName)
case "db-charset":
viper.Set("database.charset", *flagDBCharset)
case "redis-host":
viper.Set("redis.host", *flagRedisHost)
case "redis-port":
viper.Set("redis.port", *flagRedisPort)
case "redis-password":
viper.Set("redis.password", *flagRedisPassword)
case "redis-db":
viper.Set("redis.db", *flagRedisDB)
case "jwt-secret":
viper.Set("jwt.secret", *flagJWTSecret)
case "jwt-expire":
viper.Set("jwt.expire_hour", *flagJWTExpire)
}
})
}
// CORS 跨域中间件
+8 -2
View File
@@ -1,12 +1,17 @@
module ezops
go 1.21
go 1.22
toolchain go1.22.2
require (
github.com/gin-gonic/gin v1.9.1
github.com/go-ldap/ldap/v3 v3.4.10
github.com/golang-jwt/jwt/v5 v5.2.0
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
github.com/nalgeon/redka v0.2.0
github.com/redis/go-redis/v9 v9.7.0
github.com/spf13/viper v1.18.2
go.uber.org/zap v1.26.0
golang.org/x/crypto v0.31.0
@@ -19,7 +24,9 @@ require (
require (
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/bytedance/sonic v1.9.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
@@ -29,7 +36,6 @@ require (
github.com/go-playground/validator/v10 v10.16.0 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
+13
View File
@@ -2,9 +2,15 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
@@ -12,6 +18,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
@@ -102,11 +110,15 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/nalgeon/redka v0.2.0 h1:9Hjp5aHPHPLE44T1m41LKzx+b/MquMXt8ggw69eVu6U=
github.com/nalgeon/redka v0.2.0/go.mod h1:vLxjY3XS9IwBID2YEFWeeMiN4Ar/DtKd4JW62JTAxuU=
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
@@ -207,6 +219,7 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+3
View File
@@ -236,6 +236,9 @@ func (h *ServerHandler) UpdateServer(c *gin.Context) {
if req.Hostname != "" {
updates["hostname"] = req.Hostname
}
if req.IP != "" {
updates["ip"] = req.IP
}
if req.PublicIP != "" {
updates["public_ip"] = req.PublicIP
}
+3 -2
View File
@@ -12,8 +12,9 @@ import (
"time"
)
// Register 注册所有路由
func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config) {
// Register 注册所有路由 // Logan.Li
// cache 参数为可选 Redis 缓存实例(支持 redka/redis 双后端)
func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, cache interface{}) {
// API版本前缀
api := r.Group("/api/v1")
+1
View File
@@ -155,6 +155,7 @@ type CreateServerRequest struct {
// UpdateServerRequest 更新服务器请求
type UpdateServerRequest struct {
Hostname string `json:"hostname"`
IP string `json:"ip" binding:"omitempty,ip"`
PublicIP string `json:"public_ip" binding:"omitempty,ip"`
Port int `json:"port" binding:"omitempty,min=1,max=65535"`
Username string `json:"username"`
+66
View File
@@ -0,0 +1,66 @@
// go-redis 外部 Redis 服务器实现
// 用于 Docker Compose / 生产环境部署 // Logan.Li
package redis
import (
"context"
"fmt"
"time"
goredis "github.com/redis/go-redis/v9"
)
// goRedisCache 基于 go-redis 的缓存实现 // Logan.Li
type goRedisCache struct {
client *goredis.Client
}
func newGoRedis(cfg Config) (Cache, error) {
client := goredis.NewClient(&goredis.Options{
Addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port),
Password: cfg.Password,
DB: cfg.DB,
})
// 连接测试
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := client.Ping(ctx).Err(); err != nil {
return nil, fmt.Errorf("redis: 连接失败 %s:%d: %w", cfg.Host, cfg.Port, err)
}
return &goRedisCache{client: client}, nil
}
func (r *goRedisCache) Get(ctx context.Context, key string) (string, error) {
val, err := r.client.Get(ctx, key).Result()
if err == goredis.Nil {
return "", nil
}
return val, err
}
func (r *goRedisCache) Set(ctx context.Context, key string, value string, expiration time.Duration) error {
return r.client.Set(ctx, key, value, expiration).Err()
}
func (r *goRedisCache) Del(ctx context.Context, keys ...string) error {
return r.client.Del(ctx, keys...).Err()
}
func (r *goRedisCache) Exists(ctx context.Context, keys ...string) (int64, error) {
return r.client.Exists(ctx, keys...).Result()
}
func (r *goRedisCache) Expire(ctx context.Context, key string, expiration time.Duration) error {
return r.client.Expire(ctx, key, expiration).Err()
}
func (r *goRedisCache) Ping(ctx context.Context) error {
return r.client.Ping(ctx).Err()
}
func (r *goRedisCache) Close() error {
return r.client.Close()
}
+58
View File
@@ -0,0 +1,58 @@
// Package redis 提供统一的 Redis 缓存抽象层
// 支持两种后端:
// - redka: 内嵌 Redis 兼容存储(零依赖,独立二进制模式)// Logan.Li
// - redis: 外部 Redis 服务器(Docker / 生产环境)
//
// 使用方式:
//
// cache, err := redis.New(redis.Config{
// Driver: "redka", // 或 "redis"
// Host: "localhost",
// Port: 6379,
// Password: "",
// DB: 0,
// DataDir: "data/redka", // redka 数据目录(仅 redka 驱动使用)
// })
//
// cache.Set(ctx, "key", "value", 0)
// val, _ := cache.Get(ctx, "key")
package redis
import (
"context"
"fmt"
"time"
)
// Config Redis 配置 // Logan.Li
type Config struct {
Driver string // "redka" 或 "redis"
Host string
Port int
Password string
DB int
DataDir string // redka 数据目录
}
// Cache Redis 缓存接口 // Logan.Li
type Cache interface {
Get(ctx context.Context, key string) (string, error)
Set(ctx context.Context, key string, value string, expiration time.Duration) error
Del(ctx context.Context, keys ...string) error
Exists(ctx context.Context, keys ...string) (int64, error)
Expire(ctx context.Context, key string, expiration time.Duration) error
Ping(ctx context.Context) error
Close() error
}
// New 创建缓存实例(根据驱动自动选择后端)// Logan.Li
func New(cfg Config) (Cache, error) {
switch cfg.Driver {
case "redka", "":
return newRedka(cfg)
case "redis":
return newGoRedis(cfg)
default:
return nil, fmt.Errorf("redis: 不支持的驱动: %s (支持: redka, redis)", cfg.Driver)
}
}
+98
View File
@@ -0,0 +1,98 @@
// redka 内嵌 Redis 兼容存储实现(v0.2.x API)
// 使用 github.com/nalgeon/redka — 基于 SQLite 的 Redis 替代方案 // Logan.Li
// 零外部依赖,数据存在本地文件中,非常适合独立二进制部署
package redis
import (
"context"
"time"
"github.com/nalgeon/redka"
)
// redkaCache 基于 Redka v0.2.x 的缓存实现 // Logan.Li
type redkaCache struct {
db *redka.DB
}
func newRedka(cfg Config) (Cache, error) {
dataDir := cfg.DataDir
if dataDir == "" {
dataDir = "data/redka"
}
// Redka v0.2.x: Open(path, opts) // Logan.Li
db, err := redka.Open(dataDir+"/redka.db", nil)
if err != nil {
return nil, err
}
return &redkaCache{db: db}, nil
}
func (r *redkaCache) Get(ctx context.Context, key string) (string, error) {
// Redka v0.2.x: Str().Get() 返回 core.Value // Logan.Li
val, err := r.db.Str().Get(key)
if err == redka.ErrNotFound {
return "", nil
}
if err != nil {
return "", err
}
return val.String(), nil
}
func (r *redkaCache) Set(ctx context.Context, key string, value string, expiration time.Duration) error {
// Redka v0.2.x: Str().Set() 返回 error // Logan.Li
if err := r.db.Str().Set(key, value); err != nil {
return err
}
if expiration > 0 {
_, err := r.db.Key().Expire(key, expiration)
return err
}
return nil
}
func (r *redkaCache) Del(ctx context.Context, keys ...string) error {
for _, key := range keys {
if _, err := r.db.Key().Delete(key); err != nil {
return err
}
}
return nil
}
func (r *redkaCache) Exists(ctx context.Context, keys ...string) (int64, error) {
var count int64
for _, key := range keys {
ok, err := r.db.Key().Exists(key)
if err != nil {
return 0, err
}
if ok {
count++
}
}
return count, nil
}
func (r *redkaCache) Expire(ctx context.Context, key string, expiration time.Duration) error {
ok, err := r.db.Key().Expire(key, expiration)
if err != nil {
return err
}
if !ok {
return redka.ErrNotFound
}
return nil
}
func (r *redkaCache) Ping(ctx context.Context) error {
// Redka 始终可用(内嵌数据库)// Logan.Li
return nil
}
func (r *redkaCache) Close() error {
return r.db.Close()
}