feat: 重构前后端架构,添加Go后端和Python Agent服务

- 新增 Go 语言后端服务(server/),包含用户认证、Agent管理、数据库连接等API
- 新增 Python Agent 服务(agent/),实现Agent核心逻辑和工具集
- 前端从原生HTML迁移到Vue.js框架(web/src/)
- 添加 Docker Compose 支持(docker-compose.yml)
- 添加项目架构文档(docs/ARCHITECTURE.md)
- 添加环境变量示例(.env.example)和本地启动脚本(start-local.ps1)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 16:39:42 +08:00
parent 6fe3c412f4
commit b2bc9988a9
90 changed files with 9317 additions and 469 deletions

32
server/Dockerfile Normal file
View File

@@ -0,0 +1,32 @@
# 构建阶段
FROM golang:1.21-alpine AS builder
WORKDIR /app
# 安装依赖
RUN apk add --no-cache git
# 复制 go.mod 和 go.sum
COPY go.mod go.sum ./
RUN go mod download
# 复制源代码
COPY . .
# 构建
RUN CGO_ENABLED=0 GOOS=linux go build -o /server ./cmd/api
# 运行阶段
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /app
# 复制构建产物
COPY --from=builder /server .
COPY config/ ./config/
EXPOSE 8080
CMD ["./server"]

BIN
server/api.exe Normal file

Binary file not shown.

162
server/cmd/api/main.go Normal file
View File

@@ -0,0 +1,162 @@
package main
import (
"bytes"
"io"
"log"
"os"
"path/filepath"
"time"
"x-agents/server/internal/config"
"x-agents/server/internal/handler"
"x-agents/server/internal/model"
"x-agents/server/internal/repository"
"x-agents/server/internal/service"
"github.com/gin-gonic/gin"
)
// Logger 日志记录器
type Logger struct {
successLog *log.Logger
errorLog *log.Logger
}
func NewLogger() *Logger {
// 创建日志目录
today := time.Now().Format("2006-01-02")
logDir := filepath.Join("logs", today)
os.MkdirAll(logDir, 0755)
// 成功日志
successFile, _ := os.OpenFile(filepath.Join(logDir, "success.log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
successLogger := log.New(successFile, "", log.Ldate|log.Ltime|log.Lshortfile)
// 错误日志
errorFile, _ := os.OpenFile(filepath.Join(logDir, "failure.log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
errorLogger := log.New(errorFile, "", log.Ldate|log.Ltime|log.Lshortfile)
return &Logger{
successLog: successLogger,
errorLog: errorLogger,
}
}
// LogRequest 记录请求
func (l *Logger) LogRequest(method, path, body string, status int, duration time.Duration) {
entry := "[%s] %s %s %d %v"
if status >= 400 {
l.errorLog.Printf(entry, method, path, body, status, duration)
} else {
l.successLog.Printf(entry, method, path, body, status, duration)
}
}
var logger *Logger
func main() {
// 初始化日志
logger = NewLogger()
// 1. 加载配置
cfg := config.Load()
log.Printf("=== Server starting, port=%s ===", cfg.Port)
// 2. 初始化数据库
db, err := config.InitDB(cfg)
if err != nil {
log.Fatalf("Failed to connect database: %v", err)
}
// 3. 自动迁移表
db.AutoMigrate(&model.DatabaseInfo{}, &model.SubTableInfo{})
// 4. 初始化 Repository
dbRepo := repository.NewDatabaseRepository(db)
subTableRepo := repository.NewSubTableRepository(db)
// 5. 初始化 Service
dbService := service.NewDatabaseService(dbRepo, subTableRepo)
subTableService := service.NewSubTableService(subTableRepo, dbRepo)
// 6. 初始化 Handler
dbHandler := handler.NewDatabaseHandler(dbService)
subTableHandler := handler.NewSubTableHandler(subTableService)
systemHandler := handler.NewSystemHandler()
// 7. 设置路由
r := gin.New()
// 添加日志和恢复中间件
r.Use(gin.Logger())
r.Use(gin.Recovery())
// 请求日志中间件
r.Use(func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
// 记录请求体
var requestBody []byte
if c.Request.Method == "POST" || c.Request.Method == "PUT" {
requestBody, _ = io.ReadAll(c.Request.Body)
c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody))
}
c.Next()
// 记录响应日志
latency := time.Since(start)
status := c.Writer.Status()
// 使用日志系统记录
logger.LogRequest(c.Request.Method, path, string(requestBody), status, latency)
})
// CORS 中间件
r.Use(func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
})
// 数据库管理模块
databaseGroup := r.Group("/database")
{
databaseGroup.GET("/list", dbHandler.List)
databaseGroup.GET("/:id", dbHandler.GetByID)
databaseGroup.POST("/check", dbHandler.Check)
databaseGroup.POST("/add", dbHandler.Create)
databaseGroup.PUT("/:id", dbHandler.Update)
databaseGroup.DELETE("/:id", dbHandler.Delete)
}
// 子表映射管理模块
subTableGroup := r.Group("/sub-table")
{
subTableGroup.POST("/add", subTableHandler.Create)
subTableGroup.GET("/:id", subTableHandler.GetByID)
subTableGroup.GET("/database/:database_id", subTableHandler.ListByDatabase)
subTableGroup.GET("/mapping/:database_id", subTableHandler.GetMappingFromFile)
subTableGroup.GET("/ddl/:database_id", subTableHandler.GetTablesDDL)
subTableGroup.PUT("/:id", subTableHandler.Update)
subTableGroup.DELETE("/:id", subTableHandler.Delete)
}
// 系统信息模块
r.GET("/system/info", systemHandler.GetSystemInfo)
// 8. 启动服务
log.Printf("Server starting on :%s", cfg.Port)
if err := r.Run(":" + cfg.Port); err != nil {
log.Fatalf("Failed to start server: %v", err)
}
}

View File

@@ -0,0 +1,6 @@
# 本地开发配置
port: "8082"
jwt_secret: "dev-secret-key"
# Docker 内访问用 db:3306本地访问用 localhost:6036
database_url: "root:root@tcp(localhost:6036)/x_agents?charset=utf8mb4&parseTime=True&loc=Local"
python_service_url: "http://localhost:8081"

65
server/go.mod Normal file
View File

@@ -0,0 +1,65 @@
module x-agents/server
go 1.21
require (
github.com/gin-gonic/gin v1.9.1
github.com/go-sql-driver/mysql v1.7.0
github.com/golang-jwt/jwt/v5 v5.2.0
github.com/google/uuid v1.5.0
github.com/lib/pq v1.10.9
github.com/spf13/viper v1.18.2
golang.org/x/crypto v0.16.0
gorm.io/driver/mysql v1.5.2
gorm.io/gorm v1.25.5
)
require (
github.com/bytedance/sonic v1.9.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // 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
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

169
server/go.sum Normal file
View File

@@ -0,0 +1,169 @@
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/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=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/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=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
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/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/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
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=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs=
gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8=
gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@@ -0,0 +1,61 @@
package config
import (
"fmt"
"log"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"github.com/spf13/viper"
)
type Config struct {
Port string
JWTSecret string
DatabaseURL string
PythonServiceURL string
}
func Load() *Config {
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath("./config")
viper.AddConfigPath("../config")
viper.AddConfigPath("../../config")
// 默认值
viper.SetDefault("port", "8080")
viper.SetDefault("jwt_secret", "your-secret-key-change-in-production")
viper.SetDefault("python_service_url", "http://localhost:8081")
viper.SetDefault("database_url", "root:root@tcp(localhost:3306)/x_agents?charset=utf8mb4&parseTime=True&loc=Local")
if err := viper.ReadInConfig(); err != nil {
log.Printf("Using default config: %v", err)
}
return &Config{
Port: viper.GetString("port"),
JWTSecret: viper.GetString("jwt_secret"),
DatabaseURL: viper.GetString("database_url"),
PythonServiceURL: viper.GetString("python_service_url"),
}
}
func InitDB(cfg *Config) (*gorm.DB, error) {
dsn := cfg.DatabaseURL
if dsn == "" {
return nil, fmt.Errorf("database URL is empty")
}
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
if err != nil {
return nil, fmt.Errorf("failed to connect database: %w", err)
}
log.Println("Database connected successfully")
return db, nil
}

View File

@@ -0,0 +1,80 @@
package handler
import (
"net/http"
"x-agents/server/internal/model"
"x-agents/server/internal/service"
"github.com/gin-gonic/gin"
)
type ApprovalHandler struct {
approvalService *service.ApprovalService
}
func NewApprovalHandler(approvalService *service.ApprovalService) *ApprovalHandler {
return &ApprovalHandler{approvalService: approvalService}
}
// Approve 处理审批请求
func (h *ApprovalHandler) Approve(c *gin.Context) {
var req struct {
RequestID string `json:"request_id" binding:"required"`
Approved bool `json:"approved"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
var result interface{}
var err error
if req.Approved {
result, err = h.approvalService.Approve(req.RequestID, userID.(string))
} else {
result, err = h.approvalService.Reject(req.RequestID, userID.(string))
}
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, result)
}
// GetStatus 获取审批状态
func (h *ApprovalHandler) GetStatus(c *gin.Context) {
requestID := c.Param("id")
result, err := h.approvalService.GetApproval(requestID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "request not found"})
return
}
c.JSON(http.StatusOK, result)
}
// ListPending 获取待审批列表
func (h *ApprovalHandler) ListPending(c *gin.Context) {
result, err := h.approvalService.GetPendingApprovals()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if result == nil {
result = []model.ToolApprovalRequest{}
}
c.JSON(http.StatusOK, gin.H{"pending": result})
}

View File

@@ -0,0 +1,80 @@
package handler
import (
"net/http"
"x-agents/server/internal/service"
"github.com/gin-gonic/gin"
)
type AuthHandler struct {
authService *service.AuthService
}
func NewAuthHandler(authService *service.AuthService) *AuthHandler {
return &AuthHandler{authService: authService}
}
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
type LoginResponse struct {
Token string `json:"token"`
User interface{} `json:"user"`
}
// Login 处理登录
func (h *AuthHandler) Login(c *gin.Context) {
var req LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
resp, err := h.authService.Login(service.LoginRequest{
Username: req.Username,
Password: req.Password,
})
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, LoginResponse{
Token: resp.Token,
User: gin.H{
"id": resp.User.ID,
"username": resp.User.Username,
"email": resp.User.Email,
"role": resp.User.RoleID,
},
})
}
// Register 处理注册
func (h *AuthHandler) Register(c *gin.Context) {
var req struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
Email string `json:"email"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
user, err := h.authService.Register(req.Username, req.Password, req.Email)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{
"id": user.ID,
"username": user.Username,
"email": user.Email,
})
}

View File

@@ -0,0 +1,89 @@
package handler
import (
"net/http"
"x-agents/server/internal/model"
"x-agents/server/internal/service"
"github.com/gin-gonic/gin"
)
type ChatHandler struct {
chatService *service.ChatService
}
func NewChatHandler(chatService *service.ChatService) *ChatHandler {
return &ChatHandler{chatService: chatService}
}
// Chat 处理聊天请求
func (h *ChatHandler) Chat(c *gin.Context) {
var req model.AgentRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 从上下文获取用户ID由中间件设置
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
resp, err := h.chatService.Chat(c.Request.Context(), userID.(string), req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, resp)
}
// ListAgents 获取 Agent 列表
func (h *ChatHandler) ListAgents(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
agents, err := h.chatService.ListAgents(userID.(string))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if agents == nil {
agents = []model.Agent{}
}
c.JSON(http.StatusOK, gin.H{"agents": agents})
}
// CreateAgent 创建 Agent
func (h *ChatHandler) CreateAgent(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
var req struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
agent, err := h.chatService.CreateAgent(userID.(string), req.Name, req.Description)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, agent)
}

View File

@@ -0,0 +1,112 @@
package handler
import (
"net/http"
"x-agents/server/internal/model"
"x-agents/server/internal/service"
"github.com/gin-gonic/gin"
)
type DatabaseHandler struct {
service *service.DatabaseService
}
func NewDatabaseHandler(svc *service.DatabaseService) *DatabaseHandler {
return &DatabaseHandler{service: svc}
}
// Check 检查数据库连接
func (h *DatabaseHandler) Check(c *gin.Context) {
var req model.CheckRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
result, err := h.service.Check(req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, result)
}
// Create 创建数据库信息
func (h *DatabaseHandler) Create(c *gin.Context) {
var req model.CreateDatabaseRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
info, err := h.service.Create(req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, info)
}
// GetByID 获取详情
func (h *DatabaseHandler) GetByID(c *gin.Context) {
id := c.Param("id")
info, err := h.service.GetByID(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
c.JSON(http.StatusOK, info)
}
// List 获取列表
func (h *DatabaseHandler) List(c *gin.Context) {
list, err := h.service.List()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if list == nil {
list = []model.DatabaseInfo{}
}
c.JSON(http.StatusOK, gin.H{"list": list})
}
// Update 更新
func (h *DatabaseHandler) Update(c *gin.Context) {
id := c.Param("id")
var req model.UpdateDatabaseRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
info, err := h.service.Update(id, req)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
c.JSON(http.StatusOK, info)
}
// Delete 删除
func (h *DatabaseHandler) Delete(c *gin.Context) {
id := c.Param("id")
err := h.service.Delete(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
}

View File

@@ -0,0 +1,132 @@
package handler
import (
"net/http"
"x-agents/server/internal/model"
"x-agents/server/internal/service"
"github.com/gin-gonic/gin"
)
type SubTableHandler struct {
service *service.SubTableService
}
func NewSubTableHandler(svc *service.SubTableService) *SubTableHandler {
return &SubTableHandler{service: svc}
}
// Create 创建子表信息
func (h *SubTableHandler) Create(c *gin.Context) {
var req model.CreateSubTableRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
info, err := h.service.Create(req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, info)
}
// GetByID 获取详情
func (h *SubTableHandler) GetByID(c *gin.Context) {
id := c.Param("id")
info, err := h.service.GetByID(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
c.JSON(http.StatusOK, info)
}
// ListByDatabase 获取数据库下所有子表
func (h *SubTableHandler) ListByDatabase(c *gin.Context) {
databaseID := c.Param("database_id")
list, err := h.service.ListByDatabaseID(databaseID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if list == nil {
list = []model.SubTableInfo{}
}
c.JSON(http.StatusOK, gin.H{"list": list})
}
// GetMappingFromFile 从文件获取映射
func (h *SubTableHandler) GetMappingFromFile(c *gin.Context) {
databaseID := c.Param("database_id")
mapping, err := h.service.GetMappingFromFile(databaseID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if mapping == nil {
c.JSON(http.StatusOK, gin.H{"mapping": nil, "message": "no mapping file found"})
return
}
c.JSON(http.StatusOK, gin.H{"mapping": mapping})
}
// Update 更新
func (h *SubTableHandler) Update(c *gin.Context) {
id := c.Param("id")
var req model.UpdateSubTableRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
info, err := h.service.Update(id, req)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
c.JSON(http.StatusOK, info)
}
// Delete 删除
func (h *SubTableHandler) Delete(c *gin.Context) {
id := c.Param("id")
err := h.service.Delete(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
}
// GetTablesDDL 获取数据库下所有表及DDL
func (h *SubTableHandler) GetTablesDDL(c *gin.Context) {
databaseID := c.Param("database_id")
tables, err := h.service.GetTableDDLFromDatabase(databaseID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if tables == nil {
tables = []model.TableDDLInfo{}
}
c.JSON(http.StatusOK, gin.H{"tables": tables})
}

View File

@@ -0,0 +1,62 @@
package handler
import (
"net/http"
"x-agents/server/internal/model"
"github.com/gin-gonic/gin"
)
type SystemHandler struct{}
func NewSystemHandler() *SystemHandler {
return &SystemHandler{}
}
// GetSystemInfo 获取系统信息
func (h *SystemHandler) GetSystemInfo(c *gin.Context) {
info, err := getSystemInfo()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, info)
}
// getSystemInfo 获取系统信息
func getSystemInfo() (*model.SystemInfo, error) {
// 获取CPU使用率
cpuPercent, err := getCPUPercent()
if err != nil {
return nil, err
}
// 获取CPU核心数
coreCount, err := getCPUCoreCount()
if err != nil {
coreCount = 0
}
// 获取CPU型号
modelName, err := getCPUModelName()
if err != nil {
modelName = "Unknown"
}
// 获取内存信息
memoryInfo, err := getMemoryInfo()
if err != nil {
return nil, err
}
return &model.SystemInfo{
CPU: model.CPUInfo{
Percent: cpuPercent,
CoreCount: coreCount,
ModelName: modelName,
},
Memory: *memoryInfo,
}, nil
}

View File

@@ -0,0 +1,60 @@
package handler
import (
"github.com/shirou/gopsutil/v3/cpu"
"github.com/shirou/gopsutil/v3/mem"
"x-agents/server/internal/model"
)
func getCPUPercent() (float64, error) {
percent, err := cpu.Percent(0, false)
if err != nil {
return 0, err
}
if len(percent) > 0 {
return percent[0], nil
}
return 0, nil
}
func getCPUCoreCount() (int, error) {
count, err := cpu.Counts(false)
if err != nil {
return 0, err
}
return count, nil
}
func getCPUModelName() (string, error) {
info, err := cpu.Info()
if err != nil {
return "Unknown", err
}
if len(info) > 0 {
return info[0].ModelName, nil
}
return "Unknown", nil
}
func getMemoryInfo() (*model.MemoryInfo, error) {
v, err := mem.VirtualMemory()
if err != nil {
return nil, err
}
// 计算使用率
percent := 0.0
if v.Total > 0 {
percent = float64(v.Used) / float64(v.Total) * 100
}
return &model.MemoryInfo{
Total: v.Total,
Used: v.Used,
Available: v.Available,
Percent: percent,
TotalGB: float64(v.Total) / 1024 / 1024 / 1024,
UsedGB: float64(v.Used) / 1024 / 1024 / 1024,
AvailableGB: float64(v.Available) / 1024 / 1024 / 1024,
}, nil
}

View File

@@ -0,0 +1,71 @@
package middleware
import (
"net/http"
"strings"
"x-agents/server/internal/service"
"github.com/gin-gonic/gin"
)
// CORS 中间件
func CORS() gin.HandlerFunc {
return func(c *gin.Context) {
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
c.Header("Access-Control-Allow-Headers", "Origin, Content-Type, Authorization")
c.Header("Access-Control-Max-Age", "86400")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
}
}
// Recovery 中间件 - 恢复 panic
func Recovery() gin.HandlerFunc {
return gin.Recovery()
}
// Auth 认证中间件
func Auth(jwtSecret string) gin.HandlerFunc {
return func(c *gin.Context) {
// 从 Header 获取 Token
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "authorization header required"})
c.Abort()
return
}
// 解析 Bearer Token
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid authorization format"})
c.Abort()
return
}
tokenString := parts[1]
// 验证 Token
authService := service.NewAuthService(jwtSecret, nil)
claims, err := authService.ValidateToken(tokenString)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
c.Abort()
return
}
// 将用户信息存入上下文
c.Set("user_id", claims["sub"])
c.Set("username", claims["username"])
c.Set("role", claims["role"])
c.Next()
}
}

View File

@@ -0,0 +1,53 @@
package model
import (
"time"
)
// SecurityLevel 安全等级
type SecurityLevel string
const (
SecurityLevelSafe SecurityLevel = "safe"
SecurityLevelReview SecurityLevel = "review"
SecurityLevelDanger SecurityLevel = "danger"
)
// Agent 智能体
type Agent struct {
ID string `json:"id" gorm:"primaryKey"`
Name string `json:"name" gorm:"size:100;not null"`
Description string `json:"description" gorm:"type:text"`
OwnerID string `json:"owner_id" gorm:"size:50;not null;index"`
// Agent能力配置
Capabilities []string `json:"capabilities" gorm:"type:text"` // JSON数组可用工具列表
MemoryLimit int64 `json:"memory_limit" gorm:"default:134217728"` // 128MB
Timeout int `json:"timeout" gorm:"default:60"` // 60秒
// 安全配置
SecurityLevel SecurityLevel `json:"security_level" gorm:"size:20;default:'safe'"`
AllowDangerousTools bool `json:"allow_dangerous_tools" gorm:"default:false"`
// 状态
IsActive bool `json:"is_active" gorm:"default:true"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// AgentRequest 聊天请求
type AgentRequest struct {
AgentID string `json:"agent_id" binding:"required"`
Message string `json:"message" binding:"required"`
SessionID string `json:"session_id"`
Context map[string]interface{} `json:"context"`
}
// AgentResponse 聊天响应
type AgentResponse struct {
Reply string `json:"reply"`
SessionID string `json:"session_id"`
ToolsUsed []string `json:"tools_used"`
Metadata map[string]interface{} `json:"metadata"`
}

View File

@@ -0,0 +1,76 @@
package model
import (
"encoding/json"
"time"
)
// AuditAction 审计动作
type AuditAction string
const (
AuditActionLogin AuditAction = "login"
AuditActionLogout AuditAction = "logout"
AuditActionChat AuditAction = "chat"
AuditActionToolExecute AuditAction = "tool_execute"
AuditActionToolApprove AuditAction = "tool_approve"
AuditActionToolReject AuditAction = "tool_reject"
AuditActionAgentCreate AuditAction = "agent_create"
AuditActionAgentUpdate AuditAction = "agent_update"
AuditActionAgentDelete AuditAction = "agent_delete"
)
// AuditLog 审计日志
type AuditLog struct {
ID string `json:"id" gorm:"primaryKey"`
UserID string `json:"user_id" gorm:"size:50;index"`
AgentID string `json:"agent_id" gorm:"size:50;index"`
Action AuditAction `json:"action" gorm:"size:50;index"`
Details JSONMap `json:"details" gorm:"type:jsonb"`
Result string `json:"result" gorm:"size:20"` // success, failed, rejected
IPAddress string `json:"ip_address" gorm:"size:45"`
UserAgent string `json:"user_agent" gorm:"size:255"`
CreatedAt time.Time `json:"created_at" gorm:"index"`
}
// ApprovalStatus 审批状态
type ApprovalStatus string
const (
ApprovalStatusPending ApprovalStatus = "pending"
ApprovalStatusApproved ApprovalStatus = "approved"
ApprovalStatusRejected ApprovalStatus = "rejected"
)
// ToolApprovalRequest 工具审批请求
type ToolApprovalRequest struct {
ID string `json:"id" gorm:"primaryKey"`
ToolName string `json:"tool_name" gorm:"size:100;index"`
Params JSONMap `json:"params" gorm:"type:jsonb"`
UserID string `json:"user_id" gorm:"size:50;index"`
AgentID string `json:"agent_id" gorm:"size:50"`
Reason string `json:"reason" gorm:"type:text"`
Status ApprovalStatus `json:"status" gorm:"size:20;default:'pending';index"`
ReviewedBy *string `json:"reviewed_by" gorm:"size:50"`
ReviewedAt *time.Time `json:"reviewed_at"`
Result *string `json:"result" gorm:"type:text"` // 执行结果
CreatedAt time.Time `json:"created_at" gorm:"index"`
UpdatedAt time.Time `json:"updated_at"`
}
// JSONMap JSON数据映射
type JSONMap map[string]interface{}
func (j JSONMap) MarshalJSON() ([]byte, error) {
if j == nil {
return []byte("null"), nil
}
return json.Marshal(j)
}
func (j *JSONMap) UnmarshalJSON(data []byte) error {
if j == nil {
*j = make(map[string]interface{})
}
return json.Unmarshal(data, j)
}

View File

@@ -0,0 +1,83 @@
package model
import (
"time"
)
// DatabaseInfo 数据库连接信息
type DatabaseInfo struct {
ID string `json:"id" gorm:"primaryKey;size:36"` // UUID
Name string `json:"name" gorm:"size:100;not null"` // 数据库名称
Description string `json:"description" gorm:"size:500"` // 描述
DBType string `json:"db_type" gorm:"size:20;not null"` // 数据库类型: mysql, postgres, mongodb等
Host string `json:"host" gorm:"size:255;not null"` // 主机地址
Port int `json:"port" gorm:"not null"` // 端口
Username string `json:"username" gorm:"size:100;not null"` // 用户名
Password string `json:"password" gorm:"size:255"` // 密码(建议加密存储)
Database string `json:"database" gorm:"size:100"` // 数据库名
TableCount int `json:"table_count" gorm:"default:0"` // 子表数量
// 连接选项
Charset string `json:"charset" gorm:"size:20;default:utf8mb4"` // 字符集
SSLMode string `json:"ssl_mode" gorm:"size:20"` // SSL模式
// 时间
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// TableName 表名
func (DatabaseInfo) TableName() string {
return "database_info"
}
// CreateRequest 创建数据库信息请求(支持同时保存子表配置)
type CreateDatabaseRequest struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
DBType string `json:"db_type" binding:"required"`
Host string `json:"host" binding:"required"`
Port int `json:"port" binding:"required"`
Username string `json:"username" binding:"required"`
Password string `json:"password"`
Database string `json:"database"`
Charset string `json:"charset"`
SSLMode string `json:"ssl_mode"`
SubTables []CreateSubTableRequest `json:"sub_tables"` // 可选,子表配置
}
// UpdateRequest 更新数据库信息请求
type UpdateDatabaseRequest struct {
Name string `json:"name"`
Description string `json:"description"`
DBType string `json:"db_type"`
Host string `json:"host"`
Port int `json:"port"`
Username string `json:"username"`
Password string `json:"password"`
Database string `json:"database"`
TableCount int `json:"table_count"`
Charset string `json:"charset"`
SSLMode string `json:"ssl_mode"`
}
// CheckRequest 检查连接请求
type CheckRequest struct {
DBType string `json:"db_type" binding:"required"`
Host string `json:"host" binding:"required"`
Port int `json:"port" binding:"required"`
Username string `json:"username" binding:"required"`
Password string `json:"password"`
Database string `json:"database"`
Charset string `json:"charset"`
SSLMode string `json:"ssl_mode"`
DatabaseID string `json:"database_id"` // 可选,用于获取已保存的字段映射
}
// CheckResponse 检查连接响应
type CheckResponse struct {
Success bool `json:"success"` // 是否连接成功
Message string `json:"message"` // 消息
Tables []TableDDLInfo `json:"tables,omitempty"` // 表列表(连接成功时返回)
Database string `json:"database"` // 数据库名
}

View File

@@ -0,0 +1,117 @@
package model
import (
"encoding/json"
"time"
)
// TableDDLInfo 表结构信息
type TableDDLInfo struct {
TableName string `json:"table_name"` // 表名
TableComment string `json:"table_comment"` // 表注释
Columns []ColumnInfo `json:"columns"` // 列信息
DDL string `json:"ddl"` // 建表DDL
Indexes []IndexInfo `json:"indexes"` // 索引信息
}
// ColumnInfo 列信息
type ColumnInfo struct {
ColumnName string `json:"column_name"` // 列名
DataType string `json:"data_type"` // 数据类型
ColumnType string `json:"column_type"` // 列类型(含长度)
IsNullable string `json:"is_nullable"` // 是否可空
DefaultValue string `json:"default_value"` // 默认值
ColumnKey string `json:"column_key"` // 主键/索引
Extra string `json:"extra"` // 自增等
ColumnComment string `json:"column_comment"` // 列注释
MappedName string `json:"mapped_name"` // 字段中文映射名
}
// IndexInfo 索引信息
type IndexInfo struct {
IndexName string `json:"index_name"` // 索引名
ColumnName string `json:"column_name"` // 列名
NonUnique int `json:"non_unique"` // 是否唯一
IndexType string `json:"index_type"` // 索引类型
}
// SubTableInfo 子表信息
type SubTableInfo struct {
ID string `json:"id"` // UUID
DatabaseID string `json:"database_id"` // 关联的数据库ID
ParentTable string `json:"parent_table"` // 父表名
SubTableName string `json:"sub_table_name"` // 子表名
SubTableComment string `json:"sub_table_comment"` // 子表注释
MappingType string `json:"mapping_type" gorm:"type:varchar(20)"` // 映射类型
RelationField string `json:"relation_field" gorm:"type:varchar(100)"` // 关联字段
RelationType string `json:"relation_type" gorm:"type:varchar(20)"` // 关联类型
Fields string `json:"-" gorm:"type:longtext"` // 字段映射列表JSON 格式,内部存储)
FieldsList []FieldMapping `json:"fields" gorm:"-"` // 字段映射列表(返回给前端)
DDL string `json:"ddl" gorm:"type:longtext"` // 建表 DDL
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// FieldMapping 字段映射
type FieldMapping struct {
ColumnName string `json:"column_name"` // 列名
MappedName string `json:"mapped_name"` // 中文映射名
}
// GetFields 获取字段映射列表
func (s *SubTableInfo) GetFields() []FieldMapping {
if s.Fields == "" {
return nil
}
var fields []FieldMapping
if err := json.Unmarshal([]byte(s.Fields), &fields); err != nil {
return nil
}
return fields
}
// SetFields 设置字段映射列表
func (s *SubTableInfo) SetFields(fields []FieldMapping) {
if len(fields) == 0 {
s.Fields = ""
return
}
data, _ := json.Marshal(fields)
s.Fields = string(data)
}
// TableName 表名
func (SubTableInfo) TableName() string {
return "sub_table_info"
}
// CreateSubTableRequest 创建子表请求
type CreateSubTableRequest struct {
DatabaseID string `json:"database_id" binding:"required"`
ParentTable string `json:"parent_table" binding:"required"`
SubTableName string `json:"sub_table_name" binding:"required"`
SubTableComment string `json:"sub_table_comment"`
MappingType string `json:"mapping_type"`
RelationField string `json:"relation_field"`
RelationType string `json:"relation_type"`
Fields []FieldMapping `json:"fields"` // 字段映射列表
}
// UpdateSubTableRequest 更新子表请求
type UpdateSubTableRequest struct {
ParentTable string `json:"parent_table"`
SubTableName string `json:"sub_table_name"`
SubTableComment string `json:"sub_table_comment"`
MappingType string `json:"mapping_type"`
RelationField string `json:"relation_field"`
RelationType string `json:"relation_type"`
}
// SubTableMapping 完整的子表映射配置(存储到文件的格式)
type SubTableMapping struct {
DatabaseID string `json:"database_id"`
DatabaseName string `json:"database_name"`
DBType string `json:"db_type"`
Tables []SubTableInfo `json:"tables"`
UpdatedAt time.Time `json:"updated_at"`
}

View File

@@ -0,0 +1,25 @@
package model
// SystemInfo 系统信息
type SystemInfo struct {
CPU CPUInfo `json:"cpu"`
Memory MemoryInfo `json:"memory"`
}
// CPUInfo CPU信息
type CPUInfo struct {
Percent float64 `json:"percent"` // CPU使用率
CoreCount int `json:"core_count"` // 核心数
ModelName string `json:"model_name"` // CPU型号
}
// MemoryInfo 内存信息
type MemoryInfo struct {
Total uint64 `json:"total"` // 总内存(字节)
Used uint64 `json:"used"` // 已使用(字节)
Available uint64 `json:"available"` // 可用(字节)
Percent float64 `json:"percent"` // 使用率
TotalGB float64 `json:"total_gb"` // 总内存(GB)
UsedGB float64 `json:"used_gb"` // 已使用(GB)
AvailableGB float64 `json:"available_gb"` // 可用(GB)
}

View File

@@ -0,0 +1,50 @@
package model
import (
"time"
)
// PermissionLevel 权限级别
type PermissionLevel int
const (
PermissionRead PermissionLevel = iota + 1
PermissionWrite
PermissionExecute
PermissionAdmin
)
// Role 角色
type Role struct {
ID string `json:"id" gorm:"primaryKey"`
Name string `json:"name" gorm:"uniqueIndex"`
Permissions []PermissionLevel `json:"permissions" gorm:"type:int[]"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// User 用户
type User struct {
ID string `json:"id" gorm:"primaryKey"`
Username string `json:"username" gorm:"uniqueIndex;size:50;not null"`
Password string `json:"-" gorm:"not null"`
Email string `json:"email" gorm:"index"`
RoleID string `json:"role_id" gorm:"size:50;not null"`
Role *Role `json:"role,omitempty" gorm:"foreignKey:RoleID"`
IsActive bool `json:"is_active" gorm:"default:true"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// HasPermission 检查是否有权限
func (u *User) HasPermission(level PermissionLevel) bool {
if u.Role == nil {
return false
}
for _, p := range u.Role.Permissions {
if p >= level {
return true
}
}
return false
}

View File

@@ -0,0 +1,48 @@
package repository
import (
"x-agents/server/internal/model"
"gorm.io/gorm"
)
type AgentRepository struct {
db *gorm.DB
}
func NewAgentRepository(db *gorm.DB) *AgentRepository {
return &AgentRepository{db: db}
}
func (r *AgentRepository) Create(agent *model.Agent) error {
return r.db.Create(agent).Error
}
func (r *AgentRepository) FindByID(id string) (*model.Agent, error) {
var agent model.Agent
err := r.db.First(&agent, "id = ?", id).Error
if err != nil {
return nil, err
}
return &agent, nil
}
func (r *AgentRepository) FindByOwnerID(ownerID string) ([]model.Agent, error) {
var agents []model.Agent
err := r.db.Where("owner_id = ?", ownerID).Find(&agents).Error
return agents, err
}
func (r *AgentRepository) FindAll() ([]model.Agent, error) {
var agents []model.Agent
err := r.db.Where("is_active = ?", true).Find(&agents).Error
return agents, err
}
func (r *AgentRepository) Update(agent *model.Agent) error {
return r.db.Save(agent).Error
}
func (r *AgentRepository) Delete(id string) error {
return r.db.Delete(&model.Agent{}, "id = ?", id).Error
}

View File

@@ -0,0 +1,56 @@
package repository
import (
"x-agents/server/internal/model"
"gorm.io/gorm"
)
type AuditRepository struct {
db *gorm.DB
}
func NewAuditRepository(db *gorm.DB) *AuditRepository {
return &AuditRepository{db: db}
}
func (r *AuditRepository) Create(log *model.AuditLog) error {
return r.db.Create(log).Error
}
func (r *AuditRepository) FindByUserID(userID string, limit int) ([]model.AuditLog, error) {
var logs []model.AuditLog
err := r.db.Where("user_id = ?", userID).Order("created_at DESC").Limit(limit).Find(&logs).Error
return logs, err
}
func (r *AuditRepository) FindByAgentID(agentID string, limit int) ([]model.AuditLog, error) {
var logs []model.AuditLog
err := r.db.Where("agent_id = ?", agentID).Order("created_at DESC").Limit(limit).Find(&logs).Error
return logs, err
}
// ToolApproval 工具审批仓储
func (r *AuditRepository) CreateApproval(req *model.ToolApprovalRequest) error {
return r.db.Create(req).Error
}
func (r *AuditRepository) FindApprovalByID(id string) (*model.ToolApprovalRequest, error) {
var req model.ToolApprovalRequest
err := r.db.First(&req, "id = ?", id).Error
if err != nil {
return nil, err
}
return &req, nil
}
func (r *AuditRepository) FindPendingApprovals() ([]model.ToolApprovalRequest, error) {
var reqs []model.ToolApprovalRequest
err := r.db.Where("status = ?", model.ApprovalStatusPending).Order("created_at ASC").Find(&reqs).Error
return reqs, err
}
func (r *AuditRepository) UpdateApproval(req *model.ToolApprovalRequest) error {
return r.db.Save(req).Error
}

View File

@@ -0,0 +1,47 @@
package repository
import (
"x-agents/server/internal/model"
"gorm.io/gorm"
)
type DatabaseRepository struct {
db *gorm.DB
}
func NewDatabaseRepository(db *gorm.DB) *DatabaseRepository {
return &DatabaseRepository{db: db}
}
// Create 创建数据库信息
func (r *DatabaseRepository) Create(info *model.DatabaseInfo) error {
return r.db.Create(info).Error
}
// FindByID 根据ID查询
func (r *DatabaseRepository) FindByID(id string) (*model.DatabaseInfo, error) {
var info model.DatabaseInfo
err := r.db.First(&info, "id = ?", id).Error
if err != nil {
return nil, err
}
return &info, nil
}
// FindAll 查询所有
func (r *DatabaseRepository) FindAll() ([]model.DatabaseInfo, error) {
var list []model.DatabaseInfo
err := r.db.Order("created_at DESC").Find(&list).Error
return list, err
}
// Update 更新
func (r *DatabaseRepository) Update(id string, info *model.DatabaseInfo) error {
return r.db.Model(&model.DatabaseInfo{}).Where("id = ?", id).Updates(info).Error
}
// Delete 删除
func (r *DatabaseRepository) Delete(id string) error {
return r.db.Delete(&model.DatabaseInfo{}, "id = ?", id).Error
}

View File

@@ -0,0 +1,53 @@
package repository
import (
"x-agents/server/internal/model"
"gorm.io/gorm"
)
type SubTableRepository struct {
db *gorm.DB
}
func NewSubTableRepository(db *gorm.DB) *SubTableRepository {
return &SubTableRepository{db: db}
}
// Create 创建子表信息
func (r *SubTableRepository) Create(info *model.SubTableInfo) error {
return r.db.Create(info).Error
}
// FindByID 根据ID查询
func (r *SubTableRepository) FindByID(id string) (*model.SubTableInfo, error) {
var info model.SubTableInfo
if err := r.db.Where("id = ?", id).First(&info).Error; err != nil {
return nil, err
}
return &info, nil
}
// FindByDatabaseID 根据数据库ID查询所有子表
func (r *SubTableRepository) FindByDatabaseID(databaseID string) ([]model.SubTableInfo, error) {
var list []model.SubTableInfo
if err := r.db.Where("database_id = ?", databaseID).Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
// Update 更新子表信息
func (r *SubTableRepository) Update(id string, info *model.SubTableInfo) error {
return r.db.Model(info).Where("id = ?", id).Updates(info).Error
}
// Delete 删除子表信息
func (r *SubTableRepository) Delete(id string) error {
return r.db.Where("id = ?", id).Delete(&model.SubTableInfo{}).Error
}
// DeleteByDatabaseID 删除数据库下所有子表信息
func (r *SubTableRepository) DeleteByDatabaseID(databaseID string) error {
return r.db.Where("database_id = ?", databaseID).Delete(&model.SubTableInfo{}).Error
}

View File

@@ -0,0 +1,66 @@
package repository
import (
"x-agents/server/internal/model"
"gorm.io/gorm"
)
type UserRepository struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) *UserRepository {
return &UserRepository{db: db}
}
func (r *UserRepository) Create(user *model.User) error {
return r.db.Create(user).Error
}
func (r *UserRepository) FindByID(id string) (*model.User, error) {
var user model.User
err := r.db.Preload("Role").First(&user, "id = ?", id).Error
if err != nil {
return nil, err
}
return &user, nil
}
func (r *UserRepository) FindByUsername(username string) (*model.User, error) {
var user model.User
err := r.db.Preload("Role").First(&user, "username = ?", username).Error
if err != nil {
return nil, err
}
return &user, nil
}
func (r *UserRepository) FindAll() ([]model.User, error) {
var users []model.User
err := r.db.Preload("Role").Find(&users).Error
return users, err
}
func (r *UserRepository) Update(user *model.User) error {
return r.db.Save(user).Error
}
func (r *UserRepository) Delete(id string) error {
return r.db.Delete(&model.User{}, "id = ?", id).Error
}
// FindRoleByID 根据ID查找角色
func (r *UserRepository) FindRoleByID(id string) (*model.Role, error) {
var role model.Role
err := r.db.First(&role, "id = ?", id).Error
if err != nil {
return nil, err
}
return &role, nil
}
// CreateRole 创建角色
func (r *UserRepository) CreateRole(role *model.Role) error {
return r.db.Create(role).Error
}

View File

@@ -0,0 +1,101 @@
package service
import (
"fmt"
"time"
"x-agents/server/internal/model"
"x-agents/server/internal/repository"
"github.com/google/uuid"
)
type ApprovalService struct {
auditRepo *repository.AuditRepository
}
func NewApprovalService(auditRepo *repository.AuditRepository) *ApprovalService {
return &ApprovalService{auditRepo: auditRepo}
}
// CreateApprovalRequest 创建审批请求
func (s *ApprovalService) CreateApprovalRequest(
toolName string,
params map[string]interface{},
userID string,
agentID string,
reason string,
) (*model.ToolApprovalRequest, error) {
req := &model.ToolApprovalRequest{
ID: uuid.New().String(),
ToolName: toolName,
Params: params,
UserID: userID,
AgentID: agentID,
Reason: reason,
Status: model.ApprovalStatusPending,
}
if err := s.auditRepo.CreateApproval(req); err != nil {
return nil, err
}
return req, nil
}
// Approve 批准请求
func (s *ApprovalService) Approve(requestID, reviewedBy string) (*model.ToolApprovalRequest, error) {
req, err := s.auditRepo.FindApprovalByID(requestID)
if err != nil {
return nil, fmt.Errorf("request not found: %w", err)
}
if req.Status != model.ApprovalStatusPending {
return nil, fmt.Errorf("request already processed")
}
now := time.Now()
req.Status = model.ApprovalStatusApproved
req.ReviewedBy = &reviewedBy
req.ReviewedAt = &now
if err := s.auditRepo.UpdateApproval(req); err != nil {
return nil, err
}
return req, nil
}
// Reject 拒绝请求
func (s *ApprovalService) Reject(requestID, reviewedBy string) (*model.ToolApprovalRequest, error) {
req, err := s.auditRepo.FindApprovalByID(requestID)
if err != nil {
return nil, fmt.Errorf("request not found: %w", err)
}
if req.Status != model.ApprovalStatusPending {
return nil, fmt.Errorf("request already processed")
}
now := time.Now()
req.Status = model.ApprovalStatusRejected
req.ReviewedBy = &reviewedBy
req.ReviewedAt = &now
if err := s.auditRepo.UpdateApproval(req); err != nil {
return nil, err
}
return req, nil
}
// GetApproval 获取审批状态
func (s *ApprovalService) GetApproval(requestID string) (*model.ToolApprovalRequest, error) {
return s.auditRepo.FindApprovalByID(requestID)
}
// GetPendingApprovals 获取待审批列表
func (s *ApprovalService) GetPendingApprovals() ([]model.ToolApprovalRequest, error) {
return s.auditRepo.FindPendingApprovals()
}

View File

@@ -0,0 +1,145 @@
package service
import (
"errors"
"time"
"x-agents/server/internal/model"
"x-agents/server/internal/repository"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
)
var (
ErrInvalidCredentials = errors.New("invalid credentials")
ErrUserNotFound = errors.New("user not found")
)
type AuthService struct {
jwtSecret string
userRepo *repository.UserRepository
}
func NewAuthService(jwtSecret string, userRepo *repository.UserRepository) *AuthService {
return &AuthService{
jwtSecret: jwtSecret,
userRepo: userRepo,
}
}
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
type LoginResponse struct {
Token string `json:"token"`
User *model.User `json:"user"`
}
func (s *AuthService) Login(req LoginRequest) (*LoginResponse, error) {
// 查找用户
user, err := s.userRepo.FindByUsername(req.Username)
if err != nil {
return nil, ErrInvalidCredentials
}
// 验证密码
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
return nil, ErrInvalidCredentials
}
// 生成Token
token, err := s.generateToken(user)
if err != nil {
return nil, err
}
return &LoginResponse{
Token: token,
User: user,
}, nil
}
func (s *AuthService) generateToken(user *model.User) (string, error) {
claims := jwt.MapClaims{
"sub": user.ID,
"username": user.Username,
"role": user.RoleID,
"exp": time.Now().Add(time.Hour * 24 * 7).Unix(), // 7天有效期
"iat": time.Now().Unix(),
"expires_at": time.Now().Add(time.Hour * 24 * 7).Format(time.RFC3339),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(s.jwtSecret))
}
func (s *AuthService) ValidateToken(tokenString string) (jwt.MapClaims, error) {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New("unexpected signing method")
}
return []byte(s.jwtSecret), nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
return claims, nil
}
return nil, errors.New("invalid token")
}
func (s *AuthService) Register(username, password, email string) (*model.User, error) {
// 检查用户是否已存在
_, err := s.userRepo.FindByUsername(username)
if err == nil {
return nil, errors.New("user already exists")
}
// 加密密码
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
// 创建用户
user := &model.User{
ID: uuid.New().String(),
Username: username,
Password: string(hashedPassword),
Email: email,
RoleID: "user",
IsActive: true,
}
// 如果没有用户,创建默认管理员角色
role, err := s.userRepo.FindRoleByID(user.RoleID)
if err != nil {
// 创建默认角色
role = &model.Role{
ID: "user",
Name: "user",
Permissions: []model.PermissionLevel{model.PermissionRead, model.PermissionWrite},
}
s.userRepo.CreateRole(role)
user.Role = role
}
if err := s.userRepo.Create(user); err != nil {
return nil, err
}
return user, nil
}
// GetUserByID 根据ID获取用户
func (s *AuthService) GetUserByID(id string) (*model.User, error) {
return s.userRepo.FindByID(id)
}

View File

@@ -0,0 +1,146 @@
package service
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"x-agents/server/internal/model"
"x-agents/server/internal/repository"
"github.com/google/uuid"
)
type ChatService struct {
pythonURL string
agentRepo *repository.AgentRepository
}
func NewChatService(pythonURL string, agentRepo *repository.AgentRepository) *ChatService {
return &ChatService{
pythonURL: pythonURL,
agentRepo: agentRepo,
}
}
type ChatRequest struct {
AgentID string `json:"agent_id"`
Message string `json:"message"`
SessionID string `json:"session_id"`
Context map[string]interface{} `json:"context"`
}
type ChatResponse struct {
Reply string `json:"reply"`
SessionID string `json:"session_id"`
ToolsUsed []string `json:"tools_used"`
Metadata map[string]interface{} `json:"metadata"`
}
// Chat 处理聊天请求
func (s *ChatService) Chat(ctx context.Context, userID string, req model.AgentRequest) (*model.AgentResponse, error) {
// 1. 检查 Agent 是否存在
agent, err := s.agentRepo.FindByID(req.AgentID)
if err != nil {
return nil, fmt.Errorf("agent not found: %w", err)
}
// 2. 检查用户权限
if !agent.IsActive {
return nil, fmt.Errorf("agent is not active")
}
// 3. 生成会话ID
sessionID := req.SessionID
if sessionID == "" {
sessionID = uuid.New().String()
}
// 4. 调用 Python 服务
pythonReq := ChatRequest{
AgentID: req.AgentID,
Message: req.Message,
SessionID: sessionID,
Context: req.Context,
}
pythonResp, err := s.callPythonChat(ctx, pythonReq)
if err != nil {
return nil, fmt.Errorf("failed to call python service: %w", err)
}
return &model.AgentResponse{
Reply: pythonResp.Reply,
SessionID: pythonResp.SessionID,
ToolsUsed: pythonResp.ToolsUsed,
Metadata: pythonResp.Metadata,
}, nil
}
func (s *ChatService) callPythonChat(ctx context.Context, req ChatRequest) (*ChatResponse, error) {
jsonData, err := json.Marshal(req)
if err != nil {
return nil, err
}
httpReq, err := http.NewRequestWithContext(
ctx,
"POST",
s.pythonURL+"/agent/chat",
bytes.NewBuffer(jsonData),
)
if err != nil {
return nil, err
}
httpReq.Header.Set("Content-Type", "application/json")
client := &http.Client{
Timeout: 120 * time.Second, // Agent 可能需要较长时间
}
resp, err := client.Do(httpReq)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("python service returned status: %d", resp.StatusCode)
}
var chatResp ChatResponse
if err := json.NewDecoder(resp.Body).Decode(&chatResp); err != nil {
return nil, err
}
return &chatResp, nil
}
// ListAgents 获取用户可用的 Agent 列表
func (s *ChatService) ListAgents(userID string) ([]model.Agent, error) {
return s.agentRepo.FindByOwnerID(userID)
}
// CreateAgent 创建新的 Agent
func (s *ChatService) CreateAgent(userID string, name, description string) (*model.Agent, error) {
agent := &model.Agent{
ID: uuid.New().String(),
Name: name,
Description: description,
OwnerID: userID,
SecurityLevel: model.SecurityLevelSafe,
IsActive: true,
Timeout: 60,
MemoryLimit: 134217728, // 128MB
}
if err := s.agentRepo.Create(agent); err != nil {
return nil, err
}
return agent, nil
}

View File

@@ -0,0 +1,765 @@
package service
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"log"
"os"
"strings"
"time"
_ "github.com/go-sql-driver/mysql"
_ "github.com/lib/pq"
"x-agents/server/internal/model"
"x-agents/server/internal/repository"
"github.com/google/uuid"
)
var (
ErrDatabaseNotFound = errors.New("database not found")
ErrDatabaseUnreachable = errors.New("database cannot be connected")
)
type DatabaseService struct {
repo *repository.DatabaseRepository
subTableRepo *repository.SubTableRepository
}
func NewDatabaseService(repo *repository.DatabaseRepository, subTableRepo *repository.SubTableRepository) *DatabaseService {
return &DatabaseService{
repo: repo,
subTableRepo: subTableRepo,
}
}
// TestConnection 测试数据库连通性
func (s *DatabaseService) TestConnection(info *model.DatabaseInfo) error {
log.Printf("[数据库连接测试] 开始测试连接: 类型=%s, 主机=%s, 端口=%d, 数据库=%s, 用户=%s",
info.DBType, info.Host, info.Port, info.Database, info.Username)
// 统一转换为小写处理
dbType := strings.ToLower(info.DBType)
// 构建连接字符串
dsn := s.buildDSN(info)
log.Printf("[数据库连接测试] DSN构建完成: %s", dsn)
// 设置超时
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 根据数据库类型连接
var db *sql.DB
var err error
switch dbType {
case "mysql":
db, err = sql.Open("mysql", dsn)
case "postgres", "postgresql":
db, err = sql.Open("postgres", dsn)
default:
errMsg := fmt.Sprintf("unsupported database type: %s", info.DBType)
log.Printf("[数据库连接测试] 错误: %s", errMsg)
return fmt.Errorf(errMsg)
}
if err != nil {
errMsg := fmt.Sprintf("failed to create connection: %v", err)
log.Printf("[数据库连接测试] 错误: %s", errMsg)
return fmt.Errorf(errMsg)
}
defer db.Close()
// 测试连接
if err := db.PingContext(ctx); err != nil {
errMsg := fmt.Sprintf("cannot connect to database: %v", err)
log.Printf("[数据库连接测试] 连接失败: %s", errMsg)
return fmt.Errorf(errMsg)
}
log.Printf("[数据库连接测试] 连接成功!")
return nil
}
// buildDSN 构建数据库连接字符串
func (s *DatabaseService) buildDSN(info *model.DatabaseInfo) string {
dbType := strings.ToLower(info.DBType)
switch dbType {
case "mysql":
charset := info.Charset
if charset == "" {
charset = "utf8mb4"
}
// 如果没有指定数据库名,只测试连接
dbName := info.Database
if dbName == "" {
dbName = "mysql"
}
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&timeout=5s&parseTime=True",
info.Username,
info.Password,
info.Host,
info.Port,
dbName,
charset,
)
case "postgres", "postgresql":
sslmode := "disable"
if info.SSLMode != "" {
sslmode = info.SSLMode
}
return fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s connect_timeout=5",
info.Host,
info.Port,
info.Username,
info.Password,
info.Database,
sslmode,
)
default:
return ""
}
}
// getConnection 获取数据库连接
func (s *DatabaseService) getConnection(info *model.DatabaseInfo) (*sql.DB, error) {
dsn := s.buildDSN(info)
dbType := strings.ToLower(info.DBType)
var db *sql.DB
var err error
switch dbType {
case "mysql":
db, err = sql.Open("mysql", dsn)
case "postgres", "postgresql":
db, err = sql.Open("postgres", dsn)
default:
return nil, fmt.Errorf("unsupported database type: %s", dbType)
}
if err != nil {
return nil, err
}
if err := db.Ping(); err != nil {
return nil, err
}
return db, nil
}
// getTableDDL 获取表的 DDL
func (s *DatabaseService) getTableDDL(db *sql.DB, dbType, tableName string) (string, error) {
switch dbType {
case "mysql":
query := fmt.Sprintf("SHOW CREATE TABLE `%s`", tableName)
row := db.QueryRow(query)
var tblName, createStmt string
if err := row.Scan(&tblName, &createStmt); err != nil {
return "", err
}
return createStmt, nil
case "postgres", "postgresql":
query := fmt.Sprintf("SELECT pg_get_create('%s')", tableName)
var ddl string
if err := db.QueryRow(query).Scan(&ddl); err != nil {
return "", err
}
return ddl, nil
default:
return "", fmt.Errorf("unsupported database type: %s", dbType)
}
}
// buildMappedDDL 根据字段映射生成带 COMMENT 的 DDL
func (s *DatabaseService) buildMappedDDL(originalDDL string, fields []model.FieldMapping) string {
// 构建列名到映射名的映射
columnMap := make(map[string]string)
for _, f := range fields {
if f.MappedName != "" {
columnMap[f.ColumnName] = f.MappedName
}
}
if len(columnMap) == 0 {
return originalDDL
}
// 解析原始 DDL为有映射的列添加 COMMENT
lines := strings.Split(originalDDL, "\n")
var resultLines []string
for _, line := range lines {
trimmed := strings.TrimSpace(line)
// 检查是否是列定义行(以 ` 开头,包含数据类型)
if strings.HasPrefix(trimmed, "`") {
// 提取列名
parts := strings.SplitN(trimmed, " ", 2)
if len(parts) >= 1 {
colName := strings.Trim(parts[0], "`")
// 检查是否有映射
if mappedName, ok := columnMap[colName]; ok {
// 去掉结尾的逗号(如果有)
trimmed = strings.TrimRight(trimmed, ",")
// 检查是否已经有 COMMENT
if strings.Contains(strings.ToUpper(trimmed), "COMMENT") {
// 替换已有的 COMMENT
trimmed = strings.TrimSuffix(trimmed, " COMMENT '...'")
trimmed = fmt.Sprintf("%s COMMENT '%s'", trimmed, mappedName)
} else {
// 在末尾添加 COMMENT
trimmed = fmt.Sprintf("%s COMMENT '%s'", trimmed, mappedName)
}
// 替换原始行为修改后的行
resultLines = append(resultLines, trimmed)
continue
}
}
}
resultLines = append(resultLines, line)
}
return strings.Join(resultLines, "\n")
}
// Check 检查数据库连接
func (s *DatabaseService) Check(req model.CheckRequest) (*model.CheckResponse, error) {
log.Printf("[Check] 开始检查连接: 类型=%s, 主机=%s, 端口=%d, 数据库=%s, 用户=%s",
req.DBType, req.Host, req.Port, req.Database, req.Username)
info := &model.DatabaseInfo{
DBType: req.DBType,
Host: req.Host,
Port: req.Port,
Username: req.Username,
Password: req.Password,
Database: req.Database,
Charset: req.Charset,
SSLMode: req.SSLMode,
}
if info.Charset == "" {
info.Charset = "utf8mb4"
}
// 构建连接
dsn := s.buildDSN(info)
dbType := strings.ToLower(info.DBType)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
var db *sql.DB
var err error
switch dbType {
case "mysql":
db, err = sql.Open("mysql", dsn)
case "postgres", "postgresql":
db, err = sql.Open("postgres", dsn)
default:
return &model.CheckResponse{
Success: false,
Message: fmt.Sprintf("unsupported database type: %s", req.DBType),
}, nil
}
if err != nil {
return &model.CheckResponse{
Success: false,
Message: fmt.Sprintf("failed to create connection: %v", err),
}, nil
}
defer db.Close()
// 测试连接
if err := db.PingContext(ctx); err != nil {
log.Printf("[Check] 连接失败: %v", err)
return &model.CheckResponse{
Success: false,
Message: fmt.Sprintf("cannot connect to database: %v", err),
}, nil
}
log.Printf("[Check] 连接成功,开始获取表列表...")
// 获取表列表
var tables []model.TableDDLInfo
switch dbType {
case "mysql":
tables, _ = s.getMySQLTables(db, req.Database)
case "postgres", "postgresql":
tables, _ = s.getPostgresTables(db, req.Database)
}
log.Printf("[Check] 获取到 %d 个表", len(tables))
// 如果传入了 database_id获取已保存的字段映射和 DDL 并填充到表结构中
if req.DatabaseID != "" && s.subTableRepo != nil {
s.fillFieldMappings(req.DatabaseID, tables)
s.fillDDL(req.DatabaseID, tables)
}
return &model.CheckResponse{
Success: true,
Message: "connection successful",
Tables: tables,
Database: req.Database,
}, nil
}
// getMySQLTables 获取MySQL表结构
func (s *DatabaseService) getMySQLTables(db *sql.DB, dbName string) ([]model.TableDDLInfo, error) {
rows, err := db.Query(`
SELECT TABLE_NAME, TABLE_COMMENT
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = ?
AND TABLE_TYPE = 'BASE TABLE'
`, dbName)
if err != nil {
return nil, err
}
defer rows.Close()
var tables []model.TableDDLInfo
for rows.Next() {
var tableName, tableComment string
if err := rows.Scan(&tableName, &tableComment); err != nil {
continue
}
table := model.TableDDLInfo{
TableName: tableName,
TableComment: tableComment,
}
// 获取列信息
table.Columns, _ = s.getMySQLColumns(db, dbName, tableName)
// 获取 DDL
table.DDL, _ = s.getMySQLDDL(db, tableName)
tables = append(tables, table)
}
return tables, nil
}
// getMySQLDDL 获取 MySQL 表的 DDL
func (s *DatabaseService) getMySQLDDL(db *sql.DB, tableName string) (string, error) {
// 使用反引号包裹表名,防止关键字冲突
query := fmt.Sprintf("SHOW CREATE TABLE `%s`", tableName)
row := db.QueryRow(query)
var tblName, createStmt string
if err := row.Scan(&tblName, &createStmt); err != nil {
log.Printf("[getMySQLDDL] 获取 DDL 失败: %v", err)
return "", nil
}
return createStmt, nil
}
// getMySQLColumns 获取MySQL列信息
func (s *DatabaseService) getMySQLColumns(db *sql.DB, dbName, tableName string) ([]model.ColumnInfo, error) {
rows, err := db.Query(`
SELECT
COLUMN_NAME, DATA_TYPE, COLUMN_TYPE,
IS_NULLABLE, COLUMN_DEFAULT, COLUMN_KEY,
EXTRA, COLUMN_COMMENT
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
ORDER BY ORDINAL_POSITION
`, dbName, tableName)
if err != nil {
log.Printf("[getMySQLColumns] 查询列信息失败: %v", err)
return nil, err
}
defer rows.Close()
columns := make([]model.ColumnInfo, 0)
for rows.Next() {
var col model.ColumnInfo
var defaultValue, extra, columnComment sql.NullString
if err := rows.Scan(&col.ColumnName, &col.DataType, &col.ColumnType,
&col.IsNullable, &defaultValue, &col.ColumnKey, &extra, &columnComment); err != nil {
log.Printf("[getMySQLColumns] Scan 失败: %v", err)
continue
}
col.DefaultValue = defaultValue.String
col.Extra = extra.String
col.ColumnComment = columnComment.String
columns = append(columns, col)
}
// 检查是否有迭代错误
if err := rows.Err(); err != nil {
log.Printf("[getMySQLColumns] 迭代错误: %v", err)
}
return columns, nil
}
// getPostgresTables 获取PostgreSQL表结构
func (s *DatabaseService) getPostgresTables(db *sql.DB, dbName string) ([]model.TableDDLInfo, error) {
rows, err := db.Query(`
SELECT t.table_name, obj_description((t.table_schema || '.' || t.table_name)::regclass)
FROM information_schema.tables t
WHERE t.table_schema = 'public' AND t.table_type = 'BASE TABLE'
`, dbName)
if err != nil {
return nil, err
}
defer rows.Close()
var tables []model.TableDDLInfo
for rows.Next() {
var tableName, tableComment string
if err := rows.Scan(&tableName, &tableComment); err != nil {
continue
}
table := model.TableDDLInfo{
TableName: tableName,
TableComment: tableComment,
}
// 获取列信息
table.Columns, _ = s.getPostgresColumns(db, tableName)
// 获取 DDL
table.DDL, _ = s.getPostgresDDL(db, tableName)
tables = append(tables, table)
}
return tables, nil
}
// getPostgresDDL 获取 PostgreSQL 表的 DDL
func (s *DatabaseService) getPostgresDDL(db *sql.DB, tableName string) (string, error) {
var ddl string
query := fmt.Sprintf("SELECT pg_get_create('%s')", tableName)
row := db.QueryRow(query)
if err := row.Scan(&ddl); err != nil {
log.Printf("[getPostgresDDL] 获取 DDL 失败: %v", err)
return "", nil
}
return ddl, nil
}
// getPostgresColumns 获取PostgreSQL列信息
func (s *DatabaseService) getPostgresColumns(db *sql.DB, tableName string) ([]model.ColumnInfo, error) {
rows, err := db.Query(`
SELECT
c.column_name, c.data_type, c.udt_name,
c.is_nullable, c.column_default, c.column_name,
'', c.column_comment
FROM information_schema.columns c
WHERE c.table_name = $1 AND c.table_schema = 'public'
ORDER BY c.ordinal_position
`, tableName)
if err != nil {
return nil, err
}
defer rows.Close()
var columns []model.ColumnInfo
for rows.Next() {
var col model.ColumnInfo
if err := rows.Scan(&col.ColumnName, &col.DataType, &col.ColumnType,
&col.IsNullable, &col.DefaultValue, &col.ColumnKey, &col.Extra, &col.ColumnComment); err != nil {
continue
}
columns = append(columns, col)
}
return columns, nil
}
// Create 创建数据库信息(支持同时保存子表配置)
func (s *DatabaseService) Create(req model.CreateDatabaseRequest) (*model.DatabaseInfo, error) {
log.Printf("[Create] 收到创建请求: %+v", req)
info := &model.DatabaseInfo{
ID: uuid.New().String(),
Name: req.Name,
Description: req.Description,
DBType: strings.ToLower(req.DBType), // 统一转为小写
Host: req.Host,
Port: req.Port,
Username: req.Username,
Password: req.Password,
Database: req.Database,
Charset: req.Charset,
SSLMode: req.SSLMode,
TableCount: len(req.SubTables),
}
// 默认值
if info.Charset == "" {
info.Charset = "utf8mb4"
}
// 测试数据库连通性
log.Printf("[Create] 开始测试数据库连接...")
if err := s.TestConnection(info); err != nil {
log.Printf("[Create] 数据库连接测试失败: %v", err)
return nil, fmt.Errorf("database connection failed: %v", err)
}
log.Printf("[Create] 数据库连接测试成功!")
// 保存数据库信息
if err := s.repo.Create(info); err != nil {
log.Printf("[Create] 保存数据库失败: %v", err)
return nil, err
}
// 保存子表配置(如有)
if len(req.SubTables) > 0 && s.subTableRepo != nil {
log.Printf("[Create] 保存 %d 个子表配置", len(req.SubTables))
// 获取数据库连接用于查询 DDL
db, err := s.getConnection(info)
if err != nil {
log.Printf("[Create] 获取数据库连接失败: %v", err)
} else {
defer db.Close()
}
for _, subReq := range req.SubTables {
subTable := &model.SubTableInfo{
ID: uuid.New().String(),
DatabaseID: info.ID,
ParentTable: subReq.ParentTable,
SubTableName: subReq.SubTableName,
SubTableComment: subReq.SubTableComment,
MappingType: subReq.MappingType,
RelationField: subReq.RelationField,
RelationType: subReq.RelationType,
}
// 使用 SetFields 方法保存字段映射
subTable.SetFields(subReq.Fields)
// 获取并保存 DDL
if db != nil {
ddl, err := s.getTableDDL(db, strings.ToLower(info.DBType), subReq.ParentTable)
if err != nil {
log.Printf("[Create] 获取原始 DDL 失败: %v", err)
} else {
// 如果有字段映射,生成带 COMMENT 的新 DDL
if len(subReq.Fields) > 0 {
subTable.DDL = s.buildMappedDDL(ddl, subReq.Fields)
log.Printf("[Create] 生成映射后的 DDL长度: %d", len(subTable.DDL))
} else {
subTable.DDL = ddl
}
}
}
if err := s.subTableRepo.Create(subTable); err != nil {
log.Printf("[Create] 保存子表失败: %v", err)
}
}
// 同步到文件
s.syncSubTablesToFile(info)
}
log.Printf("[Create] 创建成功, ID=%s", info.ID)
return info, nil
}
// syncSubTablesToFile 同步子表到文件
func (s *DatabaseService) syncSubTablesToFile(info *model.DatabaseInfo) {
if s.subTableRepo == nil {
return
}
tables, err := s.subTableRepo.FindByDatabaseID(info.ID)
if err != nil {
log.Printf("[syncSubTablesToFile] 查询子表失败: %v", err)
return
}
mapping := &model.SubTableMapping{
DatabaseID: info.ID,
DatabaseName: info.Name,
DBType: info.DBType,
Tables: tables,
UpdatedAt: time.Now(),
}
resourceDir := "resources/db_info"
os.MkdirAll(resourceDir, 0755)
data, err := json.MarshalIndent(mapping, "", " ")
if err != nil {
log.Printf("[syncSubTablesToFile] 序列化失败: %v", err)
return
}
filePath := fmt.Sprintf("%s/%s.json", resourceDir, info.ID)
if err := os.WriteFile(filePath, data, 0644); err != nil {
log.Printf("[syncSubTablesToFile] 写入文件失败: %v", err)
}
log.Printf("[syncSubTablesToFile] 同步成功: %s", filePath)
}
// GetByID 获取详情
func (s *DatabaseService) GetByID(id string) (*model.DatabaseInfo, error) {
log.Printf("[GetByID] 查询 ID=%s", id)
info, err := s.repo.FindByID(id)
if err != nil {
log.Printf("[GetByID] 查询失败: %v", err)
return nil, ErrDatabaseNotFound
}
return info, nil
}
// List 获取列表
func (s *DatabaseService) List() ([]model.DatabaseInfo, error) {
log.Printf("[List] 查询所有数据库列表")
return s.repo.FindAll()
}
// Update 更新
func (s *DatabaseService) Update(id string, req model.UpdateDatabaseRequest) (*model.DatabaseInfo, error) {
log.Printf("[Update] 更新 ID=%s, 数据=%+v", id, req)
// 检查是否存在
_, err := s.repo.FindByID(id)
if err != nil {
log.Printf("[Update] 不存在: %v", err)
return nil, ErrDatabaseNotFound
}
// 构建更新数据
updates := map[string]interface{}{}
if req.Name != "" {
updates["name"] = req.Name
}
if req.Description != "" {
updates["description"] = req.Description
}
if req.DBType != "" {
updates["db_type"] = req.DBType
}
if req.Host != "" {
updates["host"] = req.Host
}
if req.Port > 0 {
updates["port"] = req.Port
}
if req.Username != "" {
updates["username"] = req.Username
}
if req.Password != "" {
updates["password"] = req.Password
}
if req.Database != "" {
updates["database"] = req.Database
}
if req.TableCount > 0 {
updates["table_count"] = req.TableCount
}
if req.Charset != "" {
updates["charset"] = req.Charset
}
if req.SSLMode != "" {
updates["ssl_mode"] = req.SSLMode
}
info := &model.DatabaseInfo{}
if err := s.repo.Update(id, info); err != nil {
log.Printf("[Update] 更新失败: %v", err)
return nil, err
}
return s.repo.FindByID(id)
}
// fillFieldMappings 填充字段映射到表结构中
func (s *DatabaseService) fillFieldMappings(databaseID string, tables []model.TableDDLInfo) {
// 从数据库中获取该数据库下所有子表的字段映射
subTables, err := s.subTableRepo.FindByDatabaseID(databaseID)
if err != nil {
log.Printf("[fillFieldMappings] 查询子表失败: %v", err)
return
}
// 构建表名到字段映射的映射
tableFieldsMap := make(map[string][]model.FieldMapping)
for _, st := range subTables {
fields := st.GetFields()
if len(fields) > 0 {
tableFieldsMap[st.ParentTable] = fields
}
}
// 遍历返回的表结构,填充字段映射
for i := range tables {
tableName := tables[i].TableName
if fields, ok := tableFieldsMap[tableName]; ok {
// 构建列名到映射名的映射
columnMap := make(map[string]string)
for _, f := range fields {
columnMap[f.ColumnName] = f.MappedName
}
// 填充到每个列
for j := range tables[i].Columns {
colName := tables[i].Columns[j].ColumnName
if mappedName, ok := columnMap[colName]; ok {
tables[i].Columns[j].MappedName = mappedName
}
}
}
}
log.Printf("[fillFieldMappings] 已填充字段映射到 %d 个表", len(tables))
}
// fillDDL 填充已保存的 DDL 到表结构中
func (s *DatabaseService) fillDDL(databaseID string, tables []model.TableDDLInfo) {
// 从数据库中获取该数据库下所有子表的 DDL
subTables, err := s.subTableRepo.FindByDatabaseID(databaseID)
if err != nil {
log.Printf("[fillDDL] 查询子表失败: %v", err)
return
}
// 构建表名到 DDL 的映射
tableDDLMap := make(map[string]string)
for _, st := range subTables {
if st.DDL != "" {
tableDDLMap[st.ParentTable] = st.DDL
}
}
// 遍历返回的表结构,填充 DDL
for i := range tables {
tableName := tables[i].TableName
if ddl, ok := tableDDLMap[tableName]; ok {
tables[i].DDL = ddl
}
}
log.Printf("[fillDDL] 已填充 DDL 到 %d 个表", len(tables))
}
// Delete 删除
func (s *DatabaseService) Delete(id string) error {
log.Printf("[Delete] 删除 ID=%s", id)
_, err := s.repo.FindByID(id)
if err != nil {
log.Printf("[Delete] 不存在: %v", err)
return ErrDatabaseNotFound
}
return s.repo.Delete(id)
}

View File

@@ -0,0 +1,602 @@
package service
import (
"database/sql"
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"time"
"x-agents/server/internal/model"
"x-agents/server/internal/repository"
_ "github.com/go-sql-driver/mysql"
_ "github.com/lib/pq"
"github.com/google/uuid"
)
type SubTableService struct {
repo *repository.SubTableRepository
dbRepo *repository.DatabaseRepository
resourceDir string
}
func NewSubTableService(repo *repository.SubTableRepository, dbRepo *repository.DatabaseRepository) *SubTableService {
return &SubTableService{
repo: repo,
dbRepo: dbRepo,
resourceDir: "resources/db_info",
}
}
// ensureDir 确保目录存在
func (s *SubTableService) ensureDir() error {
return os.MkdirAll(s.resourceDir, 0755)
}
// getFilePath 获取文件路径
func (s *SubTableService) getFilePath(databaseID string) string {
return filepath.Join(s.resourceDir, fmt.Sprintf("%s.json", databaseID))
}
// saveToFile 保存到文件
func (s *SubTableService) saveToFile(databaseID string, mapping *model.SubTableMapping) error {
if err := s.ensureDir(); err != nil {
return err
}
data, err := json.MarshalIndent(mapping, "", " ")
if err != nil {
return err
}
return os.WriteFile(s.getFilePath(databaseID), data, 0644)
}
// loadFromFile 从文件加载
func (s *SubTableService) loadFromFile(databaseID string) (*model.SubTableMapping, error) {
filePath := s.getFilePath(databaseID)
data, err := os.ReadFile(filePath)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
var mapping model.SubTableMapping
if err := json.Unmarshal(data, &mapping); err != nil {
return nil, err
}
return &mapping, nil
}
// syncToFile 同步到文件
func (s *SubTableService) syncToFile(databaseID string) error {
// 获取数据库信息
dbInfo, err := s.dbRepo.FindByID(databaseID)
if err != nil {
return err
}
// 获取所有子表信息
tables, err := s.repo.FindByDatabaseID(databaseID)
if err != nil {
return err
}
mapping := &model.SubTableMapping{
DatabaseID: databaseID,
DatabaseName: dbInfo.Name,
DBType: dbInfo.DBType,
Tables: tables,
UpdatedAt: time.Now(),
}
return s.saveToFile(databaseID, mapping)
}
// Create 创建子表信息
func (s *SubTableService) Create(req model.CreateSubTableRequest) (*model.SubTableInfo, error) {
log.Printf("[SubTable Create] 收到请求: %+v", req)
// 验证数据库是否存在
_, err := s.dbRepo.FindByID(req.DatabaseID)
if err != nil {
log.Printf("[SubTable Create] 数据库不存在: %v", err)
return nil, fmt.Errorf("database not found")
}
info := &model.SubTableInfo{
ID: uuid.New().String(),
DatabaseID: req.DatabaseID,
ParentTable: req.ParentTable,
SubTableName: req.SubTableName,
SubTableComment: req.SubTableComment,
MappingType: req.MappingType,
RelationField: req.RelationField,
RelationType: req.RelationType,
}
if err := s.repo.Create(info); err != nil {
log.Printf("[SubTable Create] 创建失败: %v", err)
return nil, err
}
// 同步到文件
if err := s.syncToFile(req.DatabaseID); err != nil {
log.Printf("[SubTable Create] 同步文件失败: %v", err)
}
log.Printf("[SubTable Create] 创建成功, ID=%s", info.ID)
return info, nil
}
// GetByID 获取详情
func (s *SubTableService) GetByID(id string) (*model.SubTableInfo, error) {
log.Printf("[SubTable GetByID] 查询 ID=%s", id)
info, err := s.repo.FindByID(id)
if err != nil {
log.Printf("[SubTable GetByID] 查询失败: %v", err)
return nil, fmt.Errorf("sub table not found")
}
return info, nil
}
// ListByDatabaseID 获取数据库下所有子表
func (s *SubTableService) ListByDatabaseID(databaseID string) ([]model.SubTableInfo, error) {
log.Printf("[SubTable ListByDatabaseID] 查询数据库ID=%s", databaseID)
tables, err := s.repo.FindByDatabaseID(databaseID)
if err != nil {
return nil, err
}
// 填充 FieldsList 字段
for i := range tables {
tables[i].FieldsList = tables[i].GetFields()
}
return tables, nil
}
// GetMappingFromFile 从文件获取映射信息
func (s *SubTableService) GetMappingFromFile(databaseID string) (*model.SubTableMapping, error) {
log.Printf("[SubTable GetMappingFromFile] 读取文件, databaseID=%s", databaseID)
return s.loadFromFile(databaseID)
}
// Update 更新
func (s *SubTableService) Update(id string, req model.UpdateSubTableRequest) (*model.SubTableInfo, error) {
log.Printf("[SubTable Update] 更新 ID=%s, 数据=%+v", id, req)
info, err := s.repo.FindByID(id)
if err != nil {
log.Printf("[SubTable Update] 不存在: %v", err)
return nil, fmt.Errorf("sub table not found")
}
if req.ParentTable != "" {
info.ParentTable = req.ParentTable
}
if req.SubTableName != "" {
info.SubTableName = req.SubTableName
}
if req.SubTableComment != "" {
info.SubTableComment = req.SubTableComment
}
if req.MappingType != "" {
info.MappingType = req.MappingType
}
if req.RelationField != "" {
info.RelationField = req.RelationField
}
if req.RelationType != "" {
info.RelationType = req.RelationType
}
if err := s.repo.Update(id, info); err != nil {
log.Printf("[SubTable Update] 更新失败: %v", err)
return nil, err
}
// 同步到文件
if err := s.syncToFile(info.DatabaseID); err != nil {
log.Printf("[SubTable Update] 同步文件失败: %v", err)
}
return info, nil
}
// Delete 删除
func (s *SubTableService) Delete(id string) error {
log.Printf("[SubTable Delete] 删除 ID=%s", id)
info, err := s.repo.FindByID(id)
if err != nil {
log.Printf("[SubTable Delete] 不存在: %v", err)
return fmt.Errorf("sub table not found")
}
databaseID := info.DatabaseID
if err := s.repo.Delete(id); err != nil {
log.Printf("[SubTable Delete] 删除失败: %v", err)
return err
}
// 同步到文件
if err := s.syncToFile(databaseID); err != nil {
log.Printf("[SubTable Delete] 同步文件失败: %v", err)
}
return nil
}
// GetTableDDLFromDatabase 从实际数据库获取表结构和DDL
func (s *SubTableService) GetTableDDLFromDatabase(databaseID string) ([]model.TableDDLInfo, error) {
log.Printf("[GetTableDDLFromDatabase] 获取数据库ID=%s 的表结构", databaseID)
// 获取数据库连接信息
dbInfo, err := s.dbRepo.FindByID(databaseID)
if err != nil {
log.Printf("[GetTableDDLFromDatabase] 数据库不存在: %v", err)
return nil, fmt.Errorf("database not found")
}
// 构建连接
dsn := s.buildDSN(dbInfo)
dbType := strings.ToLower(dbInfo.DBType)
var db *sql.DB
switch dbType {
case "mysql":
db, err = sql.Open("mysql", dsn)
case "postgres", "postgresql":
db, err = sql.Open("postgres", dsn)
default:
return nil, fmt.Errorf("unsupported database type: %s", dbInfo.DBType)
}
if err != nil {
return nil, fmt.Errorf("failed to connect: %v", err)
}
defer db.Close()
// 获取所有表
var tables []model.TableDDLInfo
switch dbType {
case "mysql":
tables, err = s.getMySQLTables(db, dbInfo.Database)
case "postgres", "postgresql":
tables, err = s.getPostgresTables(db, dbInfo.Database)
}
if err != nil {
return nil, err
}
log.Printf("[GetTableDDLFromDatabase] 获取到 %d 个表", len(tables))
return tables, nil
}
// buildDSN 构建数据库连接字符串
func (s *SubTableService) buildDSN(info *model.DatabaseInfo) string {
dbType := strings.ToLower(info.DBType)
switch dbType {
case "mysql":
charset := info.Charset
if charset == "" {
charset = "utf8mb4"
}
dbName := info.Database
if dbName == "" {
dbName = "mysql"
}
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&timeout=5s&parseTime=True",
info.Username, info.Password, info.Host, info.Port, dbName, charset)
case "postgres", "postgresql":
sslmode := "disable"
if info.SSLMode != "" {
sslmode = info.SSLMode
}
return fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s connect_timeout=5",
info.Host, info.Port, info.Username, info.Password, info.Database, sslmode)
}
return ""
}
// getMySQLTables 获取MySQL表结构
func (s *SubTableService) getMySQLTables(db *sql.DB, dbName string) ([]model.TableDDLInfo, error) {
// 获取所有表名和注释
rows, err := db.Query(`
SELECT TABLE_NAME, TABLE_COMMENT
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = ?
AND TABLE_TYPE = 'BASE TABLE'
`, dbName)
if err != nil {
return nil, err
}
defer rows.Close()
var tables []model.TableDDLInfo
for rows.Next() {
var tableName, tableComment string
if err := rows.Scan(&tableName, &tableComment); err != nil {
continue
}
table := model.TableDDLInfo{
TableName: tableName,
TableComment: tableComment,
}
// 获取列信息
table.Columns, _ = s.getMySQLColumns(db, dbName, tableName)
// 获取索引信息
table.Indexes, _ = s.getMySQLIndexes(db, dbName, tableName)
// 生成DDL
table.DDL = s.generateMySQLDDL(table)
tables = append(tables, table)
}
return tables, nil
}
// getMySQLColumns 获取MySQL列信息
func (s *SubTableService) getMySQLColumns(db *sql.DB, dbName, tableName string) ([]model.ColumnInfo, error) {
rows, err := db.Query(`
SELECT
COLUMN_NAME, DATA_TYPE, COLUMN_TYPE,
IS_NULLABLE, COLUMN_DEFAULT, COLUMN_KEY,
EXTRA, COLUMN_COMMENT
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
ORDER BY ORDINAL_POSITION
`, dbName, tableName)
if err != nil {
return nil, err
}
defer rows.Close()
var columns []model.ColumnInfo
for rows.Next() {
var col model.ColumnInfo
if err := rows.Scan(&col.ColumnName, &col.DataType, &col.ColumnType,
&col.IsNullable, &col.DefaultValue, &col.ColumnKey, &col.Extra, &col.ColumnComment); err != nil {
continue
}
columns = append(columns, col)
}
return columns, nil
}
// getMySQLIndexes 获取MySQL索引信息
func (s *SubTableService) getMySQLIndexes(db *sql.DB, dbName, tableName string) ([]model.IndexInfo, error) {
rows, err := db.Query(`
SELECT INDEX_NAME, COLUMN_NAME, NON_UNIQUE, INDEX_TYPE
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
ORDER BY SEQ_IN_INDEX
`, dbName, tableName)
if err != nil {
return nil, err
}
defer rows.Close()
var indexes []model.IndexInfo
for rows.Next() {
var idx model.IndexInfo
if err := rows.Scan(&idx.IndexName, &idx.ColumnName, &idx.NonUnique, &idx.IndexType); err != nil {
continue
}
indexes = append(indexes, idx)
}
return indexes, nil
}
// generateMySQLDDL 生成MySQL DDL
func (s *SubTableService) generateMySQLDDL(table model.TableDDLInfo) string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("CREATE TABLE `%s` (\n", table.TableName))
for i, col := range table.Columns {
sb.WriteString(fmt.Sprintf(" `%s` %s", col.ColumnName, col.ColumnType))
if col.IsNullable == "NO" {
sb.WriteString(" NOT NULL")
}
if col.DefaultValue != "" {
sb.WriteString(fmt.Sprintf(" DEFAULT %s", col.DefaultValue))
}
if col.Extra == "auto_increment" {
sb.WriteString(" AUTO_INCREMENT")
}
if col.ColumnComment != "" {
sb.WriteString(fmt.Sprintf(" COMMENT '%s'", col.ColumnComment))
}
if i < len(table.Columns)-1 {
sb.WriteString(",")
}
sb.WriteString("\n")
}
// 添加主键
var primaryKeys []string
for _, idx := range table.Indexes {
if idx.IndexName == "PRIMARY" {
primaryKeys = append(primaryKeys, fmt.Sprintf("`%s`", idx.ColumnName))
}
}
if len(primaryKeys) > 0 {
sb.WriteString(fmt.Sprintf(" PRIMARY KEY (%s)\n", strings.Join(primaryKeys, ", ")))
}
// 添加索引
var addedIndexes []string
for _, idx := range table.Indexes {
if idx.IndexName != "PRIMARY" {
unique := ""
if idx.NonUnique == 0 {
unique = "UNIQUE "
}
if !contains(addedIndexes, idx.IndexName) {
sb.WriteString(fmt.Sprintf(" %sKEY `%s` (`%s`),\n", unique, idx.IndexName, idx.ColumnName))
addedIndexes = append(addedIndexes, idx.IndexName)
}
}
}
ddl := sb.String()
ddl = strings.TrimSuffix(ddl, ",\n")
ddl += "\n)"
if table.TableComment != "" {
ddl += fmt.Sprintf(" COMMENT='%s'", table.TableComment)
}
ddl += ";\n"
return ddl
}
// contains 检查切片是否包含元素
func contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}
// getPostgresTables 获取PostgreSQL表结构
func (s *SubTableService) getPostgresTables(db *sql.DB, dbName string) ([]model.TableDDLInfo, error) {
rows, err := db.Query(`
SELECT t.table_name, obj_description((t.table_schema || '.' || t.table_name)::regclass)
FROM information_schema.tables t
WHERE t.table_schema = 'public' AND t.table_type = 'BASE TABLE'
`, dbName)
if err != nil {
return nil, err
}
defer rows.Close()
var tables []model.TableDDLInfo
for rows.Next() {
var tableName, tableComment string
if err := rows.Scan(&tableName, &tableComment); err != nil {
continue
}
table := model.TableDDLInfo{
TableName: tableName,
TableComment: tableComment,
}
// 获取列信息
table.Columns, _ = s.getPostgresColumns(db, tableName)
// 获取索引信息
table.Indexes, _ = s.getPostgresIndexes(db, tableName)
// 生成DDL
table.DDL = s.generatePostgresDDL(table)
tables = append(tables, table)
}
return tables, nil
}
// getPostgresColumns 获取PostgreSQL列信息
func (s *SubTableService) getPostgresColumns(db *sql.DB, tableName string) ([]model.ColumnInfo, error) {
rows, err := db.Query(`
SELECT
c.column_name, c.data_type, c.udt_name,
c.is_nullable, c.column_default, c.column_name,
'', c.column_comment
FROM information_schema.columns c
WHERE c.table_name = $1 AND c.table_schema = 'public'
ORDER BY c.ordinal_position
`, tableName)
if err != nil {
return nil, err
}
defer rows.Close()
var columns []model.ColumnInfo
for rows.Next() {
var col model.ColumnInfo
if err := rows.Scan(&col.ColumnName, &col.DataType, &col.ColumnType,
&col.IsNullable, &col.DefaultValue, &col.ColumnKey, &col.Extra, &col.ColumnComment); err != nil {
continue
}
columns = append(columns, col)
}
return columns, nil
}
// getPostgresIndexes 获取PostgreSQL索引信息
func (s *SubTableService) getPostgresIndexes(db *sql.DB, tableName string) ([]model.IndexInfo, error) {
rows, err := db.Query(`
SELECT indexname, indexdef
FROM pg_indexes
WHERE tablename = $1 AND schemaname = 'public'
`, tableName)
if err != nil {
return nil, err
}
defer rows.Close()
var indexes []model.IndexInfo
for rows.Next() {
var idx model.IndexInfo
var indexDef string
if err := rows.Scan(&idx.IndexName, &indexDef); err != nil {
continue
}
idx.NonUnique = 1
if strings.Contains(indexDef, "UNIQUE") {
idx.NonUnique = 0
}
indexes = append(indexes, idx)
}
return indexes, nil
}
// generatePostgresDDL 生成PostgreSQL DDL
func (s *SubTableService) generatePostgresDDL(table model.TableDDLInfo) string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("CREATE TABLE %s (\n", table.TableName))
for i, col := range table.Columns {
sb.WriteString(fmt.Sprintf(" %s %s", col.ColumnName, col.ColumnType))
if col.IsNullable == "NO" {
sb.WriteString(" NOT NULL")
}
if col.DefaultValue != "" {
sb.WriteString(fmt.Sprintf(" DEFAULT %s", col.DefaultValue))
}
if i < len(table.Columns)-1 {
sb.WriteString(",")
}
sb.WriteString("\n")
}
ddl := sb.String()
ddl = strings.TrimSuffix(ddl, ",\n")
ddl += "\n);"
return ddl
}

BIN
server/main.exe Normal file

Binary file not shown.

View File

@@ -0,0 +1,7 @@
{
"database_id": "053388bf-d0c3-4cd9-b78f-539858705a65",
"database_name": "test-db",
"db_type": "mysql",
"tables": [],
"updated_at": "2026-03-06T15:46:22.8598923+08:00"
}

View File

@@ -0,0 +1,22 @@
{
"database_id": "101fbee1-8400-46ae-b83b-e3898e4888b6",
"database_name": "123",
"db_type": "mysql",
"tables": [
{
"id": "042db4ca-512f-4ee9-aacb-2d7ff1bc2193",
"database_id": "101fbee1-8400-46ae-b83b-e3898e4888b6",
"parent_table": "scores",
"sub_table_name": "scores",
"sub_table_comment": "",
"mapping_type": "",
"relation_field": "",
"relation_type": "",
"fields": null,
"ddl": "CREATE TABLE `scores` (\n`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '分数id'\n `student_id` int(10) unsigned NOT NULL,\n `subject` varchar(50) NOT NULL COMMENT '科目',\n `score` double DEFAULT NULL COMMENT '分数',\n `teacher_id` int(10) unsigned DEFAULT NULL,\n `exam_date` date DEFAULT NULL COMMENT '考试日期',\n `created_at` datetime DEFAULT CURRENT_TIMESTAMP,\n PRIMARY KEY (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=48 DEFAULT CHARSET=utf8mb4",
"created_at": "2026-03-06T16:23:31.097+08:00",
"updated_at": "2026-03-06T16:23:31.097+08:00"
}
],
"updated_at": "2026-03-06T16:23:31.1477776+08:00"
}

View File

@@ -0,0 +1,20 @@
{
"database_id": "456b6a60-c5a5-46e4-8f5e-9c07c4c08510",
"database_name": "123",
"db_type": "mysql",
"tables": [
{
"id": "8b7f6a2f-3788-4499-8d3a-fe9140ccdfe1",
"database_id": "456b6a60-c5a5-46e4-8f5e-9c07c4c08510",
"parent_table": "scores",
"sub_table_name": "scores",
"sub_table_comment": "",
"mapping_type": "",
"relation_field": "",
"relation_type": "",
"created_at": "2026-03-06T15:12:45.607+08:00",
"updated_at": "2026-03-06T15:12:45.607+08:00"
}
],
"updated_at": "2026-03-06T15:12:45.6597943+08:00"
}

View File

@@ -0,0 +1,22 @@
{
"database_id": "58f7171d-6906-4f85-b27a-20bb2f982fc4",
"database_name": "123",
"db_type": "mysql",
"tables": [
{
"id": "12298a11-fe00-4e6a-a37e-e4c0b5de6a51",
"database_id": "58f7171d-6906-4f85-b27a-20bb2f982fc4",
"parent_table": "scores",
"sub_table_name": "scores",
"sub_table_comment": "",
"mapping_type": "",
"relation_field": "",
"relation_type": "",
"fields": null,
"ddl": "CREATE TABLE `scores` (\n`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'fenshu id'\n `student_id` int(10) unsigned NOT NULL,\n `subject` varchar(50) NOT NULL COMMENT '科目',\n `score` double DEFAULT NULL COMMENT '分数',\n `teacher_id` int(10) unsigned DEFAULT NULL,\n `exam_date` date DEFAULT NULL COMMENT '考试日期',\n `created_at` datetime DEFAULT CURRENT_TIMESTAMP,\n PRIMARY KEY (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=48 DEFAULT CHARSET=utf8mb4",
"created_at": "2026-03-06T16:26:15.44+08:00",
"updated_at": "2026-03-06T16:26:15.44+08:00"
}
],
"updated_at": "2026-03-06T16:26:15.4936638+08:00"
}

View File

@@ -0,0 +1,21 @@
{
"database_id": "5eee8840-c268-4cf1-8f86-a0d13eaf9b16",
"database_name": "test-db-3",
"db_type": "mysql",
"tables": [
{
"id": "2a52c3a0-0019-4634-a4b3-3627a02153ba",
"database_id": "5eee8840-c268-4cf1-8f86-a0d13eaf9b16",
"parent_table": "database_info",
"sub_table_name": "DB<44><42>Ϣ",
"sub_table_comment": "",
"mapping_type": "",
"relation_field": "",
"relation_type": "",
"ddl": "CREATE TABLE `database_info` (\n`id` varchar(36) NOT NULL, COMMENT 'ID'\n`name` varchar(100) NOT NULL, COMMENT '<27><><EFBFBD><EFBFBD>'\n `description` varchar(500) DEFAULT NULL,\n `db_type` varchar(20) NOT NULL,\n`host` varchar(255) NOT NULL, COMMENT '<27><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ַ'\n `port` bigint NOT NULL,\n `username` varchar(100) NOT NULL,\n `password` varchar(255) DEFAULT NULL,\n `database` varchar(100) DEFAULT NULL,\n `table_count` bigint DEFAULT '0',\n `charset` varchar(20) DEFAULT 'utf8mb4',\n `ssl_mode` varchar(20) DEFAULT NULL,\n `created_at` datetime(3) DEFAULT NULL,\n `updated_at` datetime(3) DEFAULT NULL,\n PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci",
"created_at": "2026-03-06T15:57:24.011+08:00",
"updated_at": "2026-03-06T15:57:24.011+08:00"
}
],
"updated_at": "2026-03-06T15:57:24.0628515+08:00"
}

View File

@@ -0,0 +1,21 @@
{
"database_id": "68b6fb60-eae2-495b-b248-9c46c8d8d6cb",
"database_name": "test-db-4",
"db_type": "mysql",
"tables": [
{
"id": "5107d64f-9949-4550-9030-e7e14585f080",
"database_id": "68b6fb60-eae2-495b-b248-9c46c8d8d6cb",
"parent_table": "database_info",
"sub_table_name": "DB<44><42>",
"sub_table_comment": "",
"mapping_type": "",
"relation_field": "",
"relation_type": "",
"ddl": "CREATE TABLE `database_info` (\n`id` varchar(36) NOT NULL COMMENT '<27><><EFBFBD><EFBFBD>ID'\n`name` varchar(100) NOT NULL COMMENT '<27><><EFBFBD>ݿ<EFBFBD><DDBF><EFBFBD>'\n `description` varchar(500) DEFAULT NULL,\n `db_type` varchar(20) NOT NULL,\n`host` varchar(255) NOT NULL COMMENT '<27><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>'\n `port` bigint NOT NULL,\n `username` varchar(100) NOT NULL,\n `password` varchar(255) DEFAULT NULL,\n `database` varchar(100) DEFAULT NULL,\n `table_count` bigint DEFAULT '0',\n `charset` varchar(20) DEFAULT 'utf8mb4',\n `ssl_mode` varchar(20) DEFAULT NULL,\n `created_at` datetime(3) DEFAULT NULL,\n `updated_at` datetime(3) DEFAULT NULL,\n PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci",
"created_at": "2026-03-06T16:00:34.065+08:00",
"updated_at": "2026-03-06T16:00:34.065+08:00"
}
],
"updated_at": "2026-03-06T16:00:34.1176551+08:00"
}

View File

@@ -0,0 +1,44 @@
{
"database_id": "7eb66808-db8b-428e-8548-2f754c4fc688",
"database_name": "123",
"db_type": "mysql",
"tables": [
{
"id": "66026752-77b6-4cba-a4d1-1bf3b07e920c",
"database_id": "7eb66808-db8b-428e-8548-2f754c4fc688",
"parent_table": "teachers",
"sub_table_name": "teachers",
"sub_table_comment": "",
"mapping_type": "",
"relation_field": "",
"relation_type": "",
"created_at": "2026-03-06T15:12:24.217+08:00",
"updated_at": "2026-03-06T15:12:24.217+08:00"
},
{
"id": "be59cb63-c5cf-46bf-b77a-46ce1fcb374b",
"database_id": "7eb66808-db8b-428e-8548-2f754c4fc688",
"parent_table": "scores",
"sub_table_name": "scores",
"sub_table_comment": "",
"mapping_type": "",
"relation_field": "",
"relation_type": "",
"created_at": "2026-03-06T15:12:24.112+08:00",
"updated_at": "2026-03-06T15:12:24.112+08:00"
},
{
"id": "d91e5cd4-09c9-40a8-9c42-8f5ce0f059e1",
"database_id": "7eb66808-db8b-428e-8548-2f754c4fc688",
"parent_table": "students",
"sub_table_name": "students",
"sub_table_comment": "",
"mapping_type": "",
"relation_field": "",
"relation_type": "",
"created_at": "2026-03-06T15:12:24.166+08:00",
"updated_at": "2026-03-06T15:12:24.166+08:00"
}
],
"updated_at": "2026-03-06T15:12:24.2696469+08:00"
}

View File

@@ -0,0 +1,21 @@
{
"database_id": "96d39e69-c96b-4d22-9b29-6456de71c6c1",
"database_name": "189数据库",
"db_type": "mysql",
"tables": [
{
"id": "d56ef61e-ac0d-439d-a2e8-133d766cbdd9",
"database_id": "96d39e69-c96b-4d22-9b29-6456de71c6c1",
"parent_table": "scores",
"sub_table_name": "scores",
"sub_table_comment": "",
"mapping_type": "",
"relation_field": "",
"relation_type": "",
"ddl": "CREATE TABLE `scores` (\n`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '分数id'\n `student_id` int(10) unsigned NOT NULL,\n `subject` varchar(50) NOT NULL COMMENT '科目',\n `score` double DEFAULT NULL COMMENT '分数',\n `teacher_id` int(10) unsigned DEFAULT NULL,\n `exam_date` date DEFAULT NULL COMMENT '考试日期',\n `created_at` datetime DEFAULT CURRENT_TIMESTAMP,\n PRIMARY KEY (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=48 DEFAULT CHARSET=utf8mb4",
"created_at": "2026-03-06T16:07:17.146+08:00",
"updated_at": "2026-03-06T16:07:17.146+08:00"
}
],
"updated_at": "2026-03-06T16:07:17.1980788+08:00"
}

View File

@@ -0,0 +1,21 @@
{
"database_id": "a58e6c1e-b39b-4248-8de9-b172f134197b",
"database_name": "test-db-2",
"db_type": "mysql",
"tables": [
{
"id": "613c5bb7-2d42-4b75-8f19-43a2b345de8b",
"database_id": "a58e6c1e-b39b-4248-8de9-b172f134197b",
"parent_table": "database_info",
"sub_table_name": "<22><><EFBFBD>ݿ<EFBFBD><DDBF><EFBFBD>Ϣ<EFBFBD><CFA2>",
"sub_table_comment": "",
"mapping_type": "",
"relation_field": "",
"relation_type": "",
"ddl": "CREATE TABLE `database_info` (\n `id` varchar(36) NOT NULL,\n `name` varchar(100) NOT NULL,\n `description` varchar(500) DEFAULT NULL,\n `db_type` varchar(20) NOT NULL,\n `host` varchar(255) NOT NULL,\n `port` bigint NOT NULL,\n `username` varchar(100) NOT NULL,\n `password` varchar(255) DEFAULT NULL,\n `database` varchar(100) DEFAULT NULL,\n `table_count` bigint DEFAULT '0',\n `charset` varchar(20) DEFAULT 'utf8mb4',\n `ssl_mode` varchar(20) DEFAULT NULL,\n `created_at` datetime(3) DEFAULT NULL,\n `updated_at` datetime(3) DEFAULT NULL,\n PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci",
"created_at": "2026-03-06T15:51:28.706+08:00",
"updated_at": "2026-03-06T15:51:28.706+08:00"
}
],
"updated_at": "2026-03-06T15:51:28.762063+08:00"
}

View File

@@ -0,0 +1,20 @@
{
"database_id": "b5fc80da-b681-4f6f-a35a-73e73dee50d0",
"database_name": "123",
"db_type": "mysql",
"tables": [
{
"id": "49805b02-3204-44b9-9d28-3b5053ca7a1e",
"database_id": "b5fc80da-b681-4f6f-a35a-73e73dee50d0",
"parent_table": "scores",
"sub_table_name": "scores",
"sub_table_comment": "",
"mapping_type": "",
"relation_field": "",
"relation_type": "",
"created_at": "2026-03-06T15:12:36.292+08:00",
"updated_at": "2026-03-06T15:12:36.292+08:00"
}
],
"updated_at": "2026-03-06T15:12:36.3469958+08:00"
}

View File

@@ -0,0 +1,7 @@
{
"database_id": "d022a68d-cb75-405b-bee3-e923a8b5a283",
"database_name": "123",
"db_type": "mysql",
"tables": [],
"updated_at": "2026-03-06T15:32:13.1521688+08:00"
}

View File

@@ -0,0 +1,22 @@
{
"database_id": "d44fa121-5964-439f-8c5d-0384ba27b411",
"database_name": "123",
"db_type": "mysql",
"tables": [
{
"id": "694da06f-d6b7-4915-8502-c4c38addf059",
"database_id": "d44fa121-5964-439f-8c5d-0384ba27b411",
"parent_table": "scores",
"sub_table_name": "scores",
"sub_table_comment": "",
"mapping_type": "",
"relation_field": "",
"relation_type": "",
"fields": null,
"ddl": "CREATE TABLE `scores` (\n`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'fenshu id'\n `student_id` int(10) unsigned NOT NULL,\n `subject` varchar(50) NOT NULL COMMENT '科目',\n `score` double DEFAULT NULL COMMENT '分数',\n `teacher_id` int(10) unsigned DEFAULT NULL,\n `exam_date` date DEFAULT NULL COMMENT '考试日期',\n `created_at` datetime DEFAULT CURRENT_TIMESTAMP,\n PRIMARY KEY (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=48 DEFAULT CHARSET=utf8mb4",
"created_at": "2026-03-06T16:30:41.589+08:00",
"updated_at": "2026-03-06T16:30:41.589+08:00"
}
],
"updated_at": "2026-03-06T16:30:41.639042+08:00"
}

122
server/temp_add_data2.go Normal file
View File

@@ -0,0 +1,122 @@
package main
import (
"fmt"
"math/rand"
"time"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
type Teacher struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"size:50;charset=utf8mb4"`
Subject string `gorm:"size:50;charset=utf8mb4"`
Phone string `gorm:"size:20"`
CreatedAt time.Time
}
type Student struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"size:50;charset=utf8mb4"`
Age int
Gender string `gorm:"size:10;charset=utf8mb4"`
Class string `gorm:"size:50;charset=utf8mb4"`
Phone string `gorm:"size:20"`
CreatedAt time.Time
}
type Score struct {
ID uint `gorm:"primaryKey"`
StudentID uint
Subject string `gorm:"size:50;charset=utf8mb4"`
Score float64
TeacherID uint
ExamDate time.Time
CreatedAt time.Time
}
func main() {
dsn := "root:881116142@tcp(10.10.10.189:3306)/students?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("连接数据库失败: " + err.Error())
}
// 自动迁移表
db.AutoMigrate(&Teacher{}, &Student{}, &Score{})
// 清理旧数据
db.Exec("DELETE FROM scores")
db.Exec("DELETE FROM students")
db.Exec("DELETE FROM teachers")
rand.Seed(time.Now().UnixNano())
// 创建教师
teachers := []Teacher{
{Name: "张老师", Subject: "数学", Phone: "13800001001"},
{Name: "李老师", Subject: "语文", Phone: "13800001002"},
{Name: "王老师", Subject: "英语", Phone: "13800001003"},
{Name: "刘老师", Subject: "物理", Phone: "13800001004"},
{Name: "陈老师", Subject: "化学", Phone: "13800001005"},
{Name: "杨老师", Subject: "生物", Phone: "13800001006"},
{Name: "赵老师", Subject: "历史", Phone: "13800001007"},
{Name: "周老师", Subject: "地理", Phone: "13800001008"},
}
db.Create(&teachers)
// 创建30个学生
names := []string{"张三", "李四", "王五", "刘六", "陈七", "杨八", "赵九", "钱十",
"孙一", "周二", "吴三", "郑四", "冯五", "褚六", "卫七", "蒋八",
"沈九", "韩十", "朱十一", "秦十二", "许十三", "何十四", "吕十五", "施十六",
"张十七", "孔十八", "曹十九", "严二十", "华二十一", "金二十二"}
genders := []string{"男", "女"}
classes := []string{"高一(1)班", "高一(2)班", "高一(3)班", "高二(1)班", "高二(2)班"}
students := make([]Student, 30)
for i := 0; i < 30; i++ {
students[i] = Student{
Name: names[i],
Age: 15 + rand.Intn(3),
Gender: genders[rand.Intn(len(genders))],
Class: classes[rand.Intn(len(classes))],
Phone: fmt.Sprintf("139%08d", 10000000+rand.Intn(90000000)),
}
}
db.Create(&students)
// 为每个学生创建成绩记录
subjects := []string{"数学", "语文", "英语", "物理", "化学", "生物", "历史", "地理"}
scores := make([]Score, 0)
for i := 0; i < 30; i++ {
numSubjects := 4 + rand.Intn(3)
selectedSubjects := make(map[string]bool)
for len(selectedSubjects) < numSubjects {
subj := subjects[rand.Intn(len(subjects))]
if !selectedSubjects[subj] {
selectedSubjects[subj] = true
teacherID := uint(1 + rand.Intn(len(teachers)))
examDate := time.Now().AddDate(0, -rand.Intn(6), -rand.Intn(30))
score := Score{
StudentID: students[i].ID,
Subject: subj,
Score: 60 + rand.Float64()*40,
TeacherID: teacherID,
ExamDate: examDate,
}
scores = append(scores, score)
}
}
}
db.Create(&scores)
fmt.Println("数据创建成功!")
fmt.Printf("教师: %d 条\n", len(teachers))
fmt.Printf("学生: %d 条\n", len(students))
fmt.Printf("成绩: %d 条\n", len(scores))
}

27
server/temp_check.go Normal file
View File

@@ -0,0 +1,27 @@
package main
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func main() {
dsn := "root:881116142@tcp(10.10.10.189:3306)/mysql?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("连接失败: " + err.Error())
}
type User struct {
User string
Host string
}
var users []User
db.Raw("SELECT User, Host FROM mysql.user WHERE User='root'").Scan(&users)
println("Root 用户列表:")
for _, u := range users {
println("- User: " + u.User + ", Host: " + u.Host)
}
}

32
server/temp_native.go Normal file
View File

@@ -0,0 +1,32 @@
package main
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func main() {
dsn := "root:881116142@tcp(10.10.10.189:3306)/mysql?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("连接失败: " + err.Error())
}
// 使用 mysql_native_password 插件重建用户
sqls := []string{
"DROP USER IF EXISTS 'root'@'%'",
"CREATE USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '881116142'",
"GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION",
"FLUSH PRIVILEGES",
}
for _, sql := range sqls {
if err := db.Exec(sql).Error; err != nil {
println("执行: " + sql + " - 错误: " + err.Error())
} else {
println("成功: " + sql)
}
}
println("完成! 用 mysql_native_password 插件重建了 root 用户")
}

41
server/temp_newuser.go Normal file
View File

@@ -0,0 +1,41 @@
package main
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func main() {
// 尝试用 root 用户连接,但指定 IP
dsn := "root:881116142@tcp(127.0.0.1:3306)/mysql?charset=utf8mb4&parseTime=True&loc=Local&multiStatements=true"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
// 尝试其他方式
dsn2 := "root:881116142@tcp(localhost:3306)/mysql?charset=utf8mb4&parseTime=True&loc=Local&multiStatements=true"
db, err = gorm.Open(mysql.Open(dsn2), &gorm.Config{})
if err != nil {
println("无法连接,请通过其他方式在 MySQL 服务器上执行:")
println("CREATE USER IF NOT EXISTS 'root'@'%' IDENTIFIED BY '881116142';")
println("GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION;")
println("FLUSH PRIVILEGES;")
panic("连接失败: " + err.Error())
}
}
// 创建新用户
sqls := []string{
"CREATE USER IF NOT EXISTS 'admin'@'%' IDENTIFIED BY 'admin123'",
"GRANT ALL PRIVILEGES ON *.* TO 'admin'@'%' WITH GRANT OPTION",
"FLUSH PRIVILEGES",
}
for _, sql := range sqls {
if err := db.Exec(sql).Error; err != nil {
println("执行: " + sql + " - 错误: " + err.Error())
} else {
println("执行成功: " + sql)
}
}
println("创建了新用户 admin可以用这个连接 Navicat")
}

32
server/temp_regrant.go Normal file
View File

@@ -0,0 +1,32 @@
package main
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func main() {
dsn := "root:881116142@tcp(10.10.10.189:3306)/mysql?charset=utf8mb4&parseTime=True&loc=Local&multiStatements=true"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("连接失败: " + err.Error())
}
// 重建 root@% 用户并设置密码
sqls := []string{
"DROP USER IF EXISTS 'root'@'%'",
"CREATE USER 'root'@'%' IDENTIFIED BY '881116142'",
"GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION",
"FLUSH PRIVILEGES",
}
for _, sql := range sqls {
if err := db.Exec(sql).Error; err != nil {
println("执行: " + sql + " - 错误: " + err.Error())
} else {
println("执行成功: " + sql)
}
}
println("完成!")
}

35
server/temp_reset.go Normal file
View File

@@ -0,0 +1,35 @@
package main
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func main() {
dsn := "root:881116142@tcp(10.10.10.189:3306)/mysql?charset=utf8mb4&parseTime=True&loc=Local&allowOldStrings=true"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("连接失败: " + err.Error())
}
// 删除所有 root 用户
sqls := []string{
"DROP USER IF EXISTS 'root'@'%'",
"DROP USER IF EXISTS 'root'@'10.10.10.122'",
"DROP USER IF EXISTS 'root'@'localhost'",
"DROP USER IF EXISTS 'root'@'127.0.0.1'",
"CREATE USER 'root'@'%' IDENTIFIED BY '881116142'",
"GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION",
"FLUSH PRIVILEGES",
}
for _, sql := range sqls {
if err := db.Exec(sql).Error; err != nil {
println("执行: " + sql + " - 错误: " + err.Error())
} else {
println("成功: " + sql)
}
}
println("完成!")
}