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

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
}