Add SQLite support and install page
This commit is contained in:
+75
-4
@@ -21,6 +21,7 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"gorm.io/driver/postgres"
|
"gorm.io/driver/postgres"
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -38,8 +39,23 @@ func main() {
|
|||||||
}
|
}
|
||||||
defer logger.Sync()
|
defer logger.Sync()
|
||||||
|
|
||||||
|
// 检查是否已安装
|
||||||
|
if !cfg.Install.Installed {
|
||||||
|
logger.Info("系统未安装,启动安装模式")
|
||||||
|
startInstallMode(cfg, logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 连接数据库
|
// 连接数据库
|
||||||
db, err := gorm.Open(postgres.Open(cfg.Database.DSN()), &gorm.Config{})
|
var db *gorm.DB
|
||||||
|
if cfg.Database.Type == "sqlite" {
|
||||||
|
// SQLite 连接
|
||||||
|
db, err = gorm.Open(sqlite.Open(cfg.Database.DSN()), &gorm.Config{})
|
||||||
|
} else {
|
||||||
|
// PostgreSQL 连接
|
||||||
|
db, err = gorm.Open(postgres.Open(cfg.Database.DSN()), &gorm.Config{})
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatal("连接数据库失败", zap.Error(err))
|
logger.Fatal("连接数据库失败", zap.Error(err))
|
||||||
}
|
}
|
||||||
@@ -53,6 +69,7 @@ func main() {
|
|||||||
&models.IPChangeLog{},
|
&models.IPChangeLog{},
|
||||||
&models.ConnectionLog{},
|
&models.ConnectionLog{},
|
||||||
&models.IPRefreshRule{},
|
&models.IPRefreshRule{},
|
||||||
|
&models.InstallStatus{},
|
||||||
); err != nil {
|
); err != nil {
|
||||||
logger.Fatal("数据库迁移失败", zap.Error(err))
|
logger.Fatal("数据库迁移失败", zap.Error(err))
|
||||||
}
|
}
|
||||||
@@ -100,7 +117,7 @@ func main() {
|
|||||||
go startHealthCheck(ctx, repos.Node, healthChecker, logger, time.Duration(cfg.Scheduler.HealthCheckInterval)*time.Second)
|
go startHealthCheck(ctx, repos.Node, healthChecker, logger, time.Duration(cfg.Scheduler.HealthCheckInterval)*time.Second)
|
||||||
|
|
||||||
// 启动 API 服务器
|
// 启动 API 服务器
|
||||||
apiServer := NewAPIServer(cfg, repos, logger)
|
apiServer := NewAPIServer(cfg, repos, db, logger)
|
||||||
go func() {
|
go func() {
|
||||||
if err := apiServer.Start(); err != nil && err != http.ErrServerClosed {
|
if err := apiServer.Start(); err != nil && err != http.ErrServerClosed {
|
||||||
logger.Error("API 服务器错误", zap.Error(err))
|
logger.Error("API 服务器错误", zap.Error(err))
|
||||||
@@ -128,6 +145,51 @@ func main() {
|
|||||||
logger.Info("服务器已关闭")
|
logger.Info("服务器已关闭")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// startInstallMode 启动安装模式
|
||||||
|
func startInstallMode(cfg *config.Config, logger *zap.Logger) {
|
||||||
|
gin.SetMode("release")
|
||||||
|
router := gin.New()
|
||||||
|
router.Use(gin.Recovery())
|
||||||
|
|
||||||
|
// 安装相关路由
|
||||||
|
api := router.Group("/api/v1")
|
||||||
|
{
|
||||||
|
api.GET("/install/check", handler.CheckInstall())
|
||||||
|
api.POST("/install", handler.DoInstall(logger))
|
||||||
|
api.GET("/install/status", handler.GetInstallStatus(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 静态文件服务(前端)
|
||||||
|
router.Static("/assets", "web/dist/assets")
|
||||||
|
router.StaticFile("/", "web/dist/index.html")
|
||||||
|
router.StaticFile("/favicon.ico", "web/dist/favicon.ico")
|
||||||
|
|
||||||
|
// 前端路由回退
|
||||||
|
router.NoRoute(func(c *gin.Context) {
|
||||||
|
c.File("web/dist/index.html")
|
||||||
|
})
|
||||||
|
|
||||||
|
server := &http.Server{
|
||||||
|
Addr: fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port),
|
||||||
|
Handler: router,
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("安装模式启动", zap.String("addr", server.Addr))
|
||||||
|
|
||||||
|
// 等待中断信号
|
||||||
|
quit := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
logger.Error("服务器错误", zap.Error(err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
<-quit
|
||||||
|
logger.Info("安装模式关闭")
|
||||||
|
}
|
||||||
|
|
||||||
// SimpleAuthenticator 简单认证器
|
// SimpleAuthenticator 简单认证器
|
||||||
type SimpleAuthenticator struct {
|
type SimpleAuthenticator struct {
|
||||||
userRepo *repository.UserRepository
|
userRepo *repository.UserRepository
|
||||||
@@ -253,18 +315,20 @@ func startHealthCheck(ctx context.Context, nodeRepo *repository.NodeRepository,
|
|||||||
type APIServer struct {
|
type APIServer struct {
|
||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
repos *repository.Repositories
|
repos *repository.Repositories
|
||||||
|
db *gorm.DB
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
server *http.Server
|
server *http.Server
|
||||||
router *gin.Engine
|
router *gin.Engine
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAPIServer(cfg *config.Config, repos *repository.Repositories, logger *zap.Logger) *APIServer {
|
func NewAPIServer(cfg *config.Config, repos *repository.Repositories, db *gorm.DB, logger *zap.Logger) *APIServer {
|
||||||
gin.SetMode(cfg.Server.Mode)
|
gin.SetMode(cfg.Server.Mode)
|
||||||
router := gin.New()
|
router := gin.New()
|
||||||
|
|
||||||
server := &APIServer{
|
server := &APIServer{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
repos: repos,
|
repos: repos,
|
||||||
|
db: db,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
router: router,
|
router: router,
|
||||||
server: &http.Server{
|
server: &http.Server{
|
||||||
@@ -288,6 +352,13 @@ func (s *APIServer) setupRoutes() {
|
|||||||
// API 路由组
|
// API 路由组
|
||||||
api := s.router.Group("/api/v1")
|
api := s.router.Group("/api/v1")
|
||||||
{
|
{
|
||||||
|
// 安装相关
|
||||||
|
install := api.Group("/install")
|
||||||
|
{
|
||||||
|
install.GET("/check", handler.CheckInstall())
|
||||||
|
install.GET("/status", handler.GetInstallStatus(s.db))
|
||||||
|
}
|
||||||
|
|
||||||
// 用户相关
|
// 用户相关
|
||||||
users := api.Group("/users")
|
users := api.Group("/users")
|
||||||
{
|
{
|
||||||
@@ -342,4 +413,4 @@ func (s *APIServer) Start() error {
|
|||||||
|
|
||||||
func (s *APIServer) Shutdown(ctx context.Context) {
|
func (s *APIServer) Shutdown(ctx context.Context) {
|
||||||
s.server.Shutdown(ctx)
|
s.server.Shutdown(ctx)
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-24
@@ -1,41 +1,34 @@
|
|||||||
# 调度中心配置
|
|
||||||
|
|
||||||
server:
|
server:
|
||||||
host: "0.0.0.0"
|
host: 0.0.0.0
|
||||||
port: 8080
|
port: 8080
|
||||||
mode: "debug" # debug, release
|
mode: release
|
||||||
|
|
||||||
socks5:
|
socks5:
|
||||||
host: "0.0.0.0"
|
host: 0.0.0.0
|
||||||
port: 1080
|
port: 1080
|
||||||
max_connections: 10000
|
max_connections: 10000
|
||||||
timeout: 30 # 秒
|
timeout: 30
|
||||||
|
|
||||||
database:
|
database:
|
||||||
host: "localhost"
|
type: sqlite
|
||||||
port: 5432
|
path: data/proxy.db
|
||||||
user: "postgres"
|
|
||||||
password: "postgres"
|
|
||||||
database: "proxy_platform"
|
|
||||||
sslmode: "disable"
|
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
host: "localhost"
|
enabled: false
|
||||||
|
host: localhost
|
||||||
port: 6379
|
port: 6379
|
||||||
password: ""
|
password: ""
|
||||||
db: 0
|
db: 0
|
||||||
|
|
||||||
scheduler:
|
scheduler:
|
||||||
# 负载均衡策略
|
strategy: least_latency
|
||||||
strategy: "least_latency" # least_latency, least_connections, weighted_round_robin
|
health_check_interval: 60
|
||||||
# 健康检查间隔
|
unlock_check_interval: 300
|
||||||
health_check_interval: 10 # 秒
|
node_timeout: 120
|
||||||
# 解锁检测间隔
|
|
||||||
unlock_check_interval: 300 # 秒
|
|
||||||
# 节点超时阈值
|
|
||||||
node_timeout: 30 # 秒
|
|
||||||
|
|
||||||
logging:
|
logging:
|
||||||
level: "info" # debug, info, warn, error
|
level: info
|
||||||
output: "stdout" # stdout, file
|
output: stdout
|
||||||
file: "logs/scheduler.log"
|
|
||||||
|
install:
|
||||||
|
installed: false
|
||||||
|
|||||||
@@ -1,29 +1,31 @@
|
|||||||
# 节点 Agent Dockerfile
|
# 构建阶段
|
||||||
FROM golang:1.21-alpine AS builder
|
FROM golang:1.22-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /build
|
||||||
|
|
||||||
RUN apk add --no-cache git
|
RUN apk add --no-cache git make gcc musl-dev
|
||||||
|
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN CGO_ENABLED=0 GOOS=linux go build -o /agent ./cmd/agent
|
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o agent ./cmd/agent
|
||||||
|
|
||||||
FROM alpine:latest
|
# 运行阶段
|
||||||
|
FROM alpine:3.19
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apk --no-cache add ca-certificates tzdata curl bash
|
RUN apk add --no-cache ca-certificates tzdata curl
|
||||||
|
|
||||||
# 安装 Cloudflare WARP
|
COPY --from=builder /build/agent .
|
||||||
RUN curl -fsSL https://pkg.cloudflareclient.com/install.sh | bash
|
COPY --from=builder /build/configs ./configs
|
||||||
|
|
||||||
COPY --from=builder /agent /app/agent
|
RUN mkdir -p /app/data
|
||||||
COPY --from=builder /app/configs /app/configs
|
|
||||||
|
|
||||||
EXPOSE 1080
|
EXPOSE 1080
|
||||||
|
|
||||||
ENTRYPOINT ["/app/agent"]
|
ENV TZ=Asia/Shanghai
|
||||||
|
|
||||||
|
CMD ["./agent"]
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
# 调度中心 Dockerfile
|
# 构建阶段
|
||||||
FROM golang:1.21-alpine AS builder
|
FROM golang:1.22-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /build
|
||||||
|
|
||||||
# 安装依赖
|
# 安装依赖
|
||||||
RUN apk add --no-cache git
|
RUN apk add --no-cache git make gcc musl-dev
|
||||||
|
|
||||||
# 复制 go.mod 和 go.sum
|
# 复制 go mod 文件
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
||||||
@@ -14,22 +14,29 @@ RUN go mod download
|
|||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# 构建
|
# 构建
|
||||||
RUN CGO_ENABLED=0 GOOS=linux go build -o /scheduler ./cmd/scheduler
|
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o scheduler ./cmd/scheduler
|
||||||
|
|
||||||
# 运行镜像
|
# 运行阶段
|
||||||
FROM alpine:latest
|
FROM alpine:3.19
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# 安装 ca-certificates(用于 HTTPS)
|
# 安装必要工具
|
||||||
RUN apk --no-cache add ca-certificates tzdata
|
RUN apk add --no-cache ca-certificates tzdata
|
||||||
|
|
||||||
# 从构建阶段复制二进制文件
|
# 从构建阶段复制二进制文件
|
||||||
COPY --from=builder /scheduler /app/scheduler
|
COPY --from=builder /build/scheduler .
|
||||||
COPY --from=builder /app/configs /app/configs
|
COPY --from=builder /build/configs ./configs
|
||||||
|
COPY --from=builder /build/web/dist ./web/dist
|
||||||
|
|
||||||
|
# 创建数据目录
|
||||||
|
RUN mkdir -p /app/data
|
||||||
|
|
||||||
# 暴露端口
|
# 暴露端口
|
||||||
EXPOSE 8080 1080
|
EXPOSE 8080 1080
|
||||||
|
|
||||||
# 运行
|
# 设置时区
|
||||||
ENTRYPOINT ["/app/scheduler"]
|
ENV TZ=Asia/Shanghai
|
||||||
|
|
||||||
|
# 启动命令
|
||||||
|
CMD ["./scheduler"]
|
||||||
|
|||||||
@@ -1,71 +1,29 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
|
||||||
image: postgres:15-alpine
|
|
||||||
container_name: proxy-postgres
|
|
||||||
environment:
|
|
||||||
POSTGRES_USER: postgres
|
|
||||||
POSTGRES_PASSWORD: postgres
|
|
||||||
POSTGRES_DB: proxy_platform
|
|
||||||
volumes:
|
|
||||||
- postgres_data:/var/lib/postgresql/data
|
|
||||||
ports:
|
|
||||||
- "5432:5432"
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
|
|
||||||
redis:
|
|
||||||
image: redis:7-alpine
|
|
||||||
container_name: proxy-redis
|
|
||||||
volumes:
|
|
||||||
- redis_data:/data
|
|
||||||
ports:
|
|
||||||
- "6379:6379"
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "redis-cli", "ping"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
|
|
||||||
scheduler:
|
scheduler:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: ..
|
||||||
dockerfile: deployments/Dockerfile.scheduler
|
dockerfile: Dockerfile.scheduler
|
||||||
container_name: proxy-scheduler
|
image: ${SCHEDULER_IMAGE:-proxy-scheduler:latest}
|
||||||
depends_on:
|
container_name: ${SCHEDULER_CONTAINER:-proxy-scheduler}
|
||||||
postgres:
|
restart: unless-stopped
|
||||||
condition: service_healthy
|
|
||||||
redis:
|
|
||||||
condition: service_healthy
|
|
||||||
environment:
|
|
||||||
- CONFIG_PATH=/app/configs/scheduler.yaml
|
|
||||||
volumes:
|
|
||||||
- ./configs:/app/configs
|
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080" # API
|
- "${API_PORT:-8080}:8080"
|
||||||
- "1080:1080" # SOCKS5
|
- "${SOCKS5_PORT:-1080}:1080"
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
# 示例节点 Agent(可选)
|
|
||||||
agent:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: deployments/Dockerfile.agent
|
|
||||||
container_name: proxy-agent
|
|
||||||
depends_on:
|
|
||||||
- scheduler
|
|
||||||
environment:
|
|
||||||
- CONFIG_PATH=/app/configs/agent.yaml
|
|
||||||
volumes:
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
- ./logs:/app/logs
|
||||||
- ./configs:/app/configs
|
- ./configs:/app/configs
|
||||||
restart: unless-stopped
|
environment:
|
||||||
profiles:
|
- TZ=Asia/Shanghai
|
||||||
- agent # 使用 --profile agent 启动
|
networks:
|
||||||
|
- proxy-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-q", "-O", "-", "http://localhost:8080/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
|
|
||||||
volumes:
|
networks:
|
||||||
postgres_data:
|
proxy-network:
|
||||||
redis_data:
|
driver: bridge
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ go 1.21
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gin-gonic/gin v1.9.1
|
github.com/gin-gonic/gin v1.9.1
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
|
||||||
github.com/spf13/viper v1.18.2
|
github.com/spf13/viper v1.18.2
|
||||||
go.uber.org/zap v1.26.0
|
go.uber.org/zap v1.26.0
|
||||||
golang.org/x/crypto v0.17.0
|
golang.org/x/crypto v0.17.0
|
||||||
golang.org/x/net v0.19.0
|
golang.org/x/net v0.19.0
|
||||||
gorm.io/driver/postgres v1.5.4
|
gorm.io/driver/postgres v1.5.4
|
||||||
gorm.io/gorm v1.25.5
|
gorm.io/driver/sqlite v1.6.0
|
||||||
|
gorm.io/gorm v1.30.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -35,6 +35,7 @@ require (
|
|||||||
github.com/leodido/go-urn v1.2.4 // indirect
|
github.com/leodido/go-urn v1.2.4 // indirect
|
||||||
github.com/magiconair/properties v1.8.7 // indirect
|
github.com/magiconair/properties v1.8.7 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
@@ -51,9 +52,9 @@ require (
|
|||||||
go.uber.org/multierr v1.10.0 // indirect
|
go.uber.org/multierr v1.10.0 // indirect
|
||||||
golang.org/x/arch v0.3.0 // indirect
|
golang.org/x/arch v0.3.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||||
golang.org/x/sync v0.5.0 // indirect
|
golang.org/x/sync v0.9.0 // indirect
|
||||||
golang.org/x/sys v0.15.0 // indirect
|
golang.org/x/sys v0.15.0 // indirect
|
||||||
golang.org/x/text v0.14.0 // indirect
|
golang.org/x/text v0.20.0 // indirect
|
||||||
google.golang.org/protobuf v1.31.0 // indirect
|
google.golang.org/protobuf v1.31.0 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg
|
|||||||
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
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/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
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.5/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 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
@@ -64,6 +62,8 @@ github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0V
|
|||||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
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 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
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/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-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
@@ -124,14 +124,14 @@ golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjs
|
|||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
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 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
|
||||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
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.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
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.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 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||||
@@ -146,6 +146,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo=
|
gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo=
|
||||||
gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0=
|
gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0=
|
||||||
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
|
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||||
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
||||||
|
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
|
||||||
|
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||||
|
|||||||
+54
-23
@@ -8,12 +8,13 @@ import (
|
|||||||
|
|
||||||
// Config 调度中心配置
|
// Config 调度中心配置
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Server ServerConfig `mapstructure:"server"`
|
Server ServerConfig `mapstructure:"server"`
|
||||||
SOCKS5 SOCKS5Config `mapstructure:"socks5"`
|
SOCKS5 SOCKS5Config `mapstructure:"socks5"`
|
||||||
Database DatabaseConfig `mapstructure:"database"`
|
Database DatabaseConfig `mapstructure:"database"`
|
||||||
Redis RedisConfig `mapstructure:"redis"`
|
Redis RedisConfig `mapstructure:"redis"`
|
||||||
Scheduler SchedulerConfig `mapstructure:"scheduler"`
|
Scheduler SchedulerConfig `mapstructure:"scheduler"`
|
||||||
Logging LoggingConfig `mapstructure:"logging"`
|
Logging LoggingConfig `mapstructure:"logging"`
|
||||||
|
Install InstallConfig `mapstructure:"install"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServerConfig struct {
|
type ServerConfig struct {
|
||||||
@@ -30,20 +31,26 @@ type SOCKS5Config struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DatabaseConfig struct {
|
type DatabaseConfig struct {
|
||||||
|
Type string `mapstructure:"type"` // sqlite, postgres
|
||||||
Host string `mapstructure:"host"`
|
Host string `mapstructure:"host"`
|
||||||
Port int `mapstructure:"port"`
|
Port int `mapstructure:"port"`
|
||||||
User string `mapstructure:"user"`
|
User string `mapstructure:"user"`
|
||||||
Password string `mapstructure:"password"`
|
Password string `mapstructure:"password"`
|
||||||
Database string `mapstructure:"database"`
|
Database string `mapstructure:"database"`
|
||||||
SSLMode string `mapstructure:"sslmode"`
|
SSLMode string `mapstructure:"sslmode"`
|
||||||
|
Path string `mapstructure:"path"` // SQLite 数据库文件路径
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c DatabaseConfig) DSN() string {
|
func (c DatabaseConfig) DSN() string {
|
||||||
|
if c.Type == "sqlite" {
|
||||||
|
return c.Path
|
||||||
|
}
|
||||||
return fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
|
return fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
|
||||||
c.Host, c.Port, c.User, c.Password, c.Database, c.SSLMode)
|
c.Host, c.Port, c.User, c.Password, c.Database, c.SSLMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
type RedisConfig struct {
|
type RedisConfig struct {
|
||||||
|
Enabled bool `mapstructure:"enabled"`
|
||||||
Host string `mapstructure:"host"`
|
Host string `mapstructure:"host"`
|
||||||
Port int `mapstructure:"port"`
|
Port int `mapstructure:"port"`
|
||||||
Password string `mapstructure:"password"`
|
Password string `mapstructure:"password"`
|
||||||
@@ -55,10 +62,10 @@ func (c RedisConfig) Addr() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SchedulerConfig struct {
|
type SchedulerConfig struct {
|
||||||
Strategy string `mapstructure:"strategy"`
|
Strategy string `mapstructure:"strategy"`
|
||||||
HealthCheckInterval int `mapstructure:"health_check_interval"`
|
HealthCheckInterval int `mapstructure:"health_check_interval"`
|
||||||
UnlockCheckInterval int `mapstructure:"unlock_check_interval"`
|
UnlockCheckInterval int `mapstructure:"unlock_check_interval"`
|
||||||
NodeTimeout int `mapstructure:"node_timeout"`
|
NodeTimeout int `mapstructure:"node_timeout"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LoggingConfig struct {
|
type LoggingConfig struct {
|
||||||
@@ -67,6 +74,10 @@ type LoggingConfig struct {
|
|||||||
File string `mapstructure:"file"`
|
File string `mapstructure:"file"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type InstallConfig struct {
|
||||||
|
Installed bool `mapstructure:"installed"`
|
||||||
|
}
|
||||||
|
|
||||||
// Load 加载配置
|
// Load 加载配置
|
||||||
func Load(configPath string) (*Config, error) {
|
func Load(configPath string) (*Config, error) {
|
||||||
viper.SetConfigFile(configPath)
|
viper.SetConfigFile(configPath)
|
||||||
@@ -76,6 +87,10 @@ func Load(configPath string) (*Config, error) {
|
|||||||
viper.SetDefault("server.host", "0.0.0.0")
|
viper.SetDefault("server.host", "0.0.0.0")
|
||||||
viper.SetDefault("server.port", 8080)
|
viper.SetDefault("server.port", 8080)
|
||||||
viper.SetDefault("server.mode", "release")
|
viper.SetDefault("server.mode", "release")
|
||||||
|
viper.SetDefault("database.type", "sqlite")
|
||||||
|
viper.SetDefault("database.path", "data/proxy.db")
|
||||||
|
viper.SetDefault("redis.enabled", false)
|
||||||
|
viper.SetDefault("install.installed", false)
|
||||||
|
|
||||||
if err := viper.ReadInConfig(); err != nil {
|
if err := viper.ReadInConfig(); err != nil {
|
||||||
return nil, fmt.Errorf("读取配置文件失败: %w", err)
|
return nil, fmt.Errorf("读取配置文件失败: %w", err)
|
||||||
@@ -89,15 +104,31 @@ func Load(configPath string) (*Config, error) {
|
|||||||
return &config, nil
|
return &config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save 保存配置
|
||||||
|
func Save(configPath string, cfg *Config) error {
|
||||||
|
viper.Set("server", cfg.Server)
|
||||||
|
viper.Set("socks5", cfg.SOCKS5)
|
||||||
|
viper.Set("database", cfg.Database)
|
||||||
|
viper.Set("redis", cfg.Redis)
|
||||||
|
viper.Set("scheduler", cfg.Scheduler)
|
||||||
|
viper.Set("logging", cfg.Logging)
|
||||||
|
viper.Set("install", cfg.Install)
|
||||||
|
|
||||||
|
if err := viper.WriteConfig(); err != nil {
|
||||||
|
return fmt.Errorf("写入配置文件失败: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// AgentConfig 节点 Agent 配置
|
// AgentConfig 节点 Agent 配置
|
||||||
type AgentConfig struct {
|
type AgentConfig struct {
|
||||||
Agent AgentSettings `mapstructure:"agent"`
|
Agent AgentSettings `mapstructure:"agent"`
|
||||||
Scheduler SchedulerConn `mapstructure:"scheduler"`
|
Scheduler SchedulerConn `mapstructure:"scheduler"`
|
||||||
WARP WARPConfig `mapstructure:"warp"`
|
WARP WARPConfig `mapstructure:"warp"`
|
||||||
SOCKS5 SOCKS5Config `mapstructure:"socks5"`
|
SOCKS5 SOCKS5Config `mapstructure:"socks5"`
|
||||||
Unlock UnlockConfig `mapstructure:"unlock"`
|
Unlock UnlockConfig `mapstructure:"unlock"`
|
||||||
Routing RoutingConfig `mapstructure:"routing"`
|
Routing RoutingConfig `mapstructure:"routing"`
|
||||||
Logging LoggingConfig `mapstructure:"logging"`
|
Logging LoggingConfig `mapstructure:"logging"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentSettings struct {
|
type AgentSettings struct {
|
||||||
@@ -107,10 +138,10 @@ type AgentSettings struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SchedulerConn struct {
|
type SchedulerConn struct {
|
||||||
Host string `mapstructure:"host"`
|
Host string `mapstructure:"host"`
|
||||||
APIKey string `mapstructure:"api_key"`
|
APIKey string `mapstructure:"api_key"`
|
||||||
HeartbeatInterval int `mapstructure:"heartbeat_interval"`
|
HeartbeatInterval int `mapstructure:"heartbeat_interval"`
|
||||||
ReportInterval int `mapstructure:"report_interval"`
|
ReportInterval int `mapstructure:"report_interval"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type WARPConfig struct {
|
type WARPConfig struct {
|
||||||
@@ -128,10 +159,10 @@ type UnlockConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ServiceConfig struct {
|
type ServiceConfig struct {
|
||||||
Name string `mapstructure:"name"`
|
Name string `mapstructure:"name"`
|
||||||
URL string `mapstructure:"url"`
|
URL string `mapstructure:"url"`
|
||||||
SuccessKeywords []string `mapstructure:"success_keywords"`
|
SuccessKeywords []string `mapstructure:"success_keywords"`
|
||||||
FailKeywords []string `mapstructure:"fail_keywords"`
|
FailKeywords []string `mapstructure:"fail_keywords"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RoutingConfig struct {
|
type RoutingConfig struct {
|
||||||
|
|||||||
@@ -0,0 +1,264 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"proxy-platform/internal/config"
|
||||||
|
"proxy-platform/internal/models"
|
||||||
|
"proxy-platform/internal/repository"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CheckInstall 检查安装状态
|
||||||
|
func CheckInstall() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
// 检查数据库是否存在安装记录
|
||||||
|
dbPath := "data/proxy.db"
|
||||||
|
installed := false
|
||||||
|
|
||||||
|
if _, err := os.Stat(dbPath); err == nil {
|
||||||
|
// 数据库文件存在,检查安装状态
|
||||||
|
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
|
||||||
|
if err == nil {
|
||||||
|
var status models.InstallStatus
|
||||||
|
if result := db.First(&status); result.Error == nil {
|
||||||
|
installed = status.Installed
|
||||||
|
}
|
||||||
|
sqlDB, _ := db.DB()
|
||||||
|
sqlDB.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"installed": installed,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InstallRequest 安装请求
|
||||||
|
type InstallRequest struct {
|
||||||
|
DbType string `json:"db_type" binding:"required"` // sqlite, postgres
|
||||||
|
AdminUser string `json:"admin_user" binding:"required"`
|
||||||
|
AdminPass string `json:"admin_pass" binding:"required"`
|
||||||
|
PlatformName string `json:"platform_name"`
|
||||||
|
|
||||||
|
// SQLite 配置
|
||||||
|
SqlitePath string `json:"sqlite_path"`
|
||||||
|
|
||||||
|
// PostgreSQL 配置(可选)
|
||||||
|
PgHost string `json:"pg_host"`
|
||||||
|
PgPort int `json:"pg_port"`
|
||||||
|
PgUser string `json:"pg_user"`
|
||||||
|
PgPassword string `json:"pg_password"`
|
||||||
|
PgDatabase string `json:"pg_database"`
|
||||||
|
|
||||||
|
// Redis 配置(可选)
|
||||||
|
RedisEnabled bool `json:"redis_enabled"`
|
||||||
|
RedisHost string `json:"redis_host"`
|
||||||
|
RedisPort int `json:"redis_port"`
|
||||||
|
RedisPassword string `json:"redis_password"`
|
||||||
|
RedisDB int `json:"redis_db"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DoInstall 执行安装
|
||||||
|
func DoInstall(logger *zap.Logger) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
var req InstallRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证必填项
|
||||||
|
if req.AdminUser == "" || req.AdminPass == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "用户名和密码不能为空"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.DbType != "sqlite" && req.DbType != "postgres" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "数据库类型只能是 sqlite 或 postgres"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.DbType == "postgres" {
|
||||||
|
if req.PgHost == "" || req.PgDatabase == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "PostgreSQL 配置不完整"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确定数据库路径
|
||||||
|
dbPath := req.SqlitePath
|
||||||
|
if dbPath == "" {
|
||||||
|
dbPath = "data/proxy.db"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建数据目录
|
||||||
|
dbDir := filepath.Dir(dbPath)
|
||||||
|
if err := os.MkdirAll(dbDir, 0755); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建数据目录失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化数据库
|
||||||
|
var db *gorm.DB
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if req.DbType == "sqlite" {
|
||||||
|
db, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
|
||||||
|
} else {
|
||||||
|
// PostgreSQL 支持(需要额外配置 postgres driver)
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "PostgreSQL 支持需要额外配置,请使用 SQLite"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "数据库连接失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动迁移
|
||||||
|
if err := db.AutoMigrate(
|
||||||
|
&models.User{},
|
||||||
|
&models.Node{},
|
||||||
|
&models.NodeGroup{},
|
||||||
|
&models.UnlockStatus{},
|
||||||
|
&models.IPChangeLog{},
|
||||||
|
&models.ConnectionLog{},
|
||||||
|
&models.IPRefreshRule{},
|
||||||
|
&models.InstallStatus{},
|
||||||
|
); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "数据库迁移失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建管理员用户
|
||||||
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.AdminPass), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "密码加密失败"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
adminUser := &models.User{
|
||||||
|
Username: req.AdminUser,
|
||||||
|
PasswordHash: string(hashedPassword),
|
||||||
|
Status: "active",
|
||||||
|
TrafficQuota: 0, // 无限制
|
||||||
|
}
|
||||||
|
|
||||||
|
userRepo := repository.NewUserRepository(db)
|
||||||
|
if err := userRepo.Create(adminUser); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建管理员失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建安装状态记录
|
||||||
|
platformName := req.PlatformName
|
||||||
|
if platformName == "" {
|
||||||
|
platformName = "代理管理平台"
|
||||||
|
}
|
||||||
|
|
||||||
|
installStatus := &models.InstallStatus{
|
||||||
|
Installed: true,
|
||||||
|
DbType: req.DbType,
|
||||||
|
RedisEnabled: req.RedisEnabled,
|
||||||
|
AdminUser: req.AdminUser,
|
||||||
|
PlatformName: platformName,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Create(installStatus).Error; err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存安装状态失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成配置文件
|
||||||
|
cfg := &config.Config{
|
||||||
|
Server: config.ServerConfig{
|
||||||
|
Host: "0.0.0.0",
|
||||||
|
Port: 8080,
|
||||||
|
Mode: "release",
|
||||||
|
},
|
||||||
|
SOCKS5: config.SOCKS5Config{
|
||||||
|
Host: "0.0.0.0",
|
||||||
|
Port: 1080,
|
||||||
|
MaxConnections: 10000,
|
||||||
|
Timeout: 30,
|
||||||
|
},
|
||||||
|
Database: config.DatabaseConfig{
|
||||||
|
Type: req.DbType,
|
||||||
|
Path: dbPath,
|
||||||
|
},
|
||||||
|
Redis: config.RedisConfig{
|
||||||
|
Enabled: req.RedisEnabled,
|
||||||
|
Host: req.RedisHost,
|
||||||
|
Port: req.RedisPort,
|
||||||
|
Password: req.RedisPassword,
|
||||||
|
DB: req.RedisDB,
|
||||||
|
},
|
||||||
|
Scheduler: config.SchedulerConfig{
|
||||||
|
Strategy: "least_latency",
|
||||||
|
HealthCheckInterval: 60,
|
||||||
|
UnlockCheckInterval: 300,
|
||||||
|
NodeTimeout: 120,
|
||||||
|
},
|
||||||
|
Logging: config.LoggingConfig{
|
||||||
|
Level: "info",
|
||||||
|
Output: "stdout",
|
||||||
|
},
|
||||||
|
Install: config.InstallConfig{
|
||||||
|
Installed: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存配置文件
|
||||||
|
configPath := "configs/scheduler.yaml"
|
||||||
|
if err := config.Save(configPath, cfg); err != nil {
|
||||||
|
logger.Error("保存配置文件失败", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭数据库连接
|
||||||
|
sqlDB, _ := db.DB()
|
||||||
|
sqlDB.Close()
|
||||||
|
|
||||||
|
logger.Info("安装完成",
|
||||||
|
zap.String("db_type", req.DbType),
|
||||||
|
zap.Bool("redis_enabled", req.RedisEnabled),
|
||||||
|
zap.String("admin_user", req.AdminUser),
|
||||||
|
)
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": "安装成功",
|
||||||
|
"db_type": req.DbType,
|
||||||
|
"redis_enabled": req.RedisEnabled,
|
||||||
|
"admin_user": req.AdminUser,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInstallStatus 获取安装状态(从数据库)
|
||||||
|
func GetInstallStatus(db *gorm.DB) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
var status models.InstallStatus
|
||||||
|
if result := db.First(&status); result.Error != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"installed": false,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"installed": status.Installed,
|
||||||
|
"db_type": status.DbType,
|
||||||
|
"redis_enabled": status.RedisEnabled,
|
||||||
|
"platform_name": status.PlatformName,
|
||||||
|
"admin_user": status.AdminUser,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InstallStatus 安装状态
|
||||||
|
type InstallStatus struct {
|
||||||
|
ID uint `gorm:"primaryKey" json:"id"`
|
||||||
|
Installed bool `gorm:"default:false" json:"installed"`
|
||||||
|
DbType string `gorm:"type:varchar(20);default:'sqlite'" json:"db_type"` // sqlite, postgres
|
||||||
|
RedisEnabled bool `gorm:"default:false" json:"redis_enabled"`
|
||||||
|
AdminUser string `gorm:"size:64" json:"admin_user"`
|
||||||
|
PlatformName string `gorm:"size:128;default:'代理管理平台'" json:"platform_name"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 指定表名
|
||||||
|
func (InstallStatus) TableName() string {
|
||||||
|
return "install_status"
|
||||||
|
}
|
||||||
Generated
+4849
File diff suppressed because it is too large
Load Diff
+16
-16
@@ -4,34 +4,34 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vue-tsc && vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix"
|
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"vue": "^3.4.0",
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
"vue-router": "^4.2.5",
|
|
||||||
"pinia": "^2.1.7",
|
|
||||||
"element-plus": "^2.4.4",
|
|
||||||
"@element-plus/icons-vue": "^2.3.1",
|
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"echarts": "^5.4.3",
|
"echarts": "^5.4.3",
|
||||||
"vue-echarts": "^6.6.5"
|
"element-plus": "^2.4.4",
|
||||||
|
"pinia": "^2.1.7",
|
||||||
|
"vue": "^3.4.0",
|
||||||
|
"vue-echarts": "^6.6.5",
|
||||||
|
"vue-router": "^4.2.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^4.5.2",
|
|
||||||
"@vue/tsconfig": "^0.5.1",
|
|
||||||
"typescript": "~5.3.0",
|
|
||||||
"vite": "^5.0.10",
|
|
||||||
"vue-tsc": "^1.8.25",
|
|
||||||
"eslint": "^8.55.0",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^6.16.0",
|
"@typescript-eslint/eslint-plugin": "^6.16.0",
|
||||||
"@typescript-eslint/parser": "^6.16.0",
|
"@typescript-eslint/parser": "^6.16.0",
|
||||||
"eslint-plugin-vue": "^9.19.2",
|
"@vitejs/plugin-vue": "^4.5.2",
|
||||||
"@vue/eslint-config-typescript": "^12.0.0",
|
"@vue/eslint-config-typescript": "^12.0.0",
|
||||||
|
"@vue/tsconfig": "^0.5.1",
|
||||||
|
"eslint": "^8.55.0",
|
||||||
|
"eslint-plugin-vue": "^9.19.2",
|
||||||
|
"sass": "^1.69.5",
|
||||||
|
"typescript": "~5.3.0",
|
||||||
"unplugin-auto-import": "^0.17.2",
|
"unplugin-auto-import": "^0.17.2",
|
||||||
"unplugin-vue-components": "^0.26.0",
|
"unplugin-vue-components": "^0.26.0",
|
||||||
"sass": "^1.69.5"
|
"vite": "^5.0.10",
|
||||||
|
"vue-tsc": "^1.8.25"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||||
|
<rect width="32" height="32" rx="6" fill="#409eff"/>
|
||||||
|
<text x="16" y="22" text-anchor="middle" fill="white" font-size="16" font-weight="bold">P</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 222 B |
Vendored
+87
@@ -0,0 +1,87 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/* prettier-ignore */
|
||||||
|
// @ts-nocheck
|
||||||
|
// noinspection JSUnusedGlobalSymbols
|
||||||
|
// Generated by unplugin-auto-import
|
||||||
|
export {}
|
||||||
|
declare global {
|
||||||
|
const EffectScope: typeof import('vue')['EffectScope']
|
||||||
|
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
|
||||||
|
const computed: typeof import('vue')['computed']
|
||||||
|
const createApp: typeof import('vue')['createApp']
|
||||||
|
const createPinia: typeof import('pinia')['createPinia']
|
||||||
|
const customRef: typeof import('vue')['customRef']
|
||||||
|
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||||
|
const defineComponent: typeof import('vue')['defineComponent']
|
||||||
|
const defineStore: typeof import('pinia')['defineStore']
|
||||||
|
const effectScope: typeof import('vue')['effectScope']
|
||||||
|
const getActivePinia: typeof import('pinia')['getActivePinia']
|
||||||
|
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||||
|
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||||
|
const h: typeof import('vue')['h']
|
||||||
|
const inject: typeof import('vue')['inject']
|
||||||
|
const isProxy: typeof import('vue')['isProxy']
|
||||||
|
const isReactive: typeof import('vue')['isReactive']
|
||||||
|
const isReadonly: typeof import('vue')['isReadonly']
|
||||||
|
const isRef: typeof import('vue')['isRef']
|
||||||
|
const mapActions: typeof import('pinia')['mapActions']
|
||||||
|
const mapGetters: typeof import('pinia')['mapGetters']
|
||||||
|
const mapState: typeof import('pinia')['mapState']
|
||||||
|
const mapStores: typeof import('pinia')['mapStores']
|
||||||
|
const mapWritableState: typeof import('pinia')['mapWritableState']
|
||||||
|
const markRaw: typeof import('vue')['markRaw']
|
||||||
|
const nextTick: typeof import('vue')['nextTick']
|
||||||
|
const onActivated: typeof import('vue')['onActivated']
|
||||||
|
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||||
|
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
|
||||||
|
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
|
||||||
|
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||||
|
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||||
|
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||||
|
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||||
|
const onMounted: typeof import('vue')['onMounted']
|
||||||
|
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||||
|
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||||
|
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||||
|
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||||
|
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||||
|
const onUpdated: typeof import('vue')['onUpdated']
|
||||||
|
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
|
||||||
|
const provide: typeof import('vue')['provide']
|
||||||
|
const reactive: typeof import('vue')['reactive']
|
||||||
|
const readonly: typeof import('vue')['readonly']
|
||||||
|
const ref: typeof import('vue')['ref']
|
||||||
|
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||||
|
const setActivePinia: typeof import('pinia')['setActivePinia']
|
||||||
|
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
|
||||||
|
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||||
|
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||||
|
const shallowRef: typeof import('vue')['shallowRef']
|
||||||
|
const storeToRefs: typeof import('pinia')['storeToRefs']
|
||||||
|
const toRaw: typeof import('vue')['toRaw']
|
||||||
|
const toRef: typeof import('vue')['toRef']
|
||||||
|
const toRefs: typeof import('vue')['toRefs']
|
||||||
|
const toValue: typeof import('vue')['toValue']
|
||||||
|
const triggerRef: typeof import('vue')['triggerRef']
|
||||||
|
const unref: typeof import('vue')['unref']
|
||||||
|
const useAttrs: typeof import('vue')['useAttrs']
|
||||||
|
const useCssModule: typeof import('vue')['useCssModule']
|
||||||
|
const useCssVars: typeof import('vue')['useCssVars']
|
||||||
|
const useId: typeof import('vue')['useId']
|
||||||
|
const useLink: typeof import('vue-router')['useLink']
|
||||||
|
const useModel: typeof import('vue')['useModel']
|
||||||
|
const useRoute: typeof import('vue-router')['useRoute']
|
||||||
|
const useRouter: typeof import('vue-router')['useRouter']
|
||||||
|
const useSlots: typeof import('vue')['useSlots']
|
||||||
|
const useTemplateRef: typeof import('vue')['useTemplateRef']
|
||||||
|
const watch: typeof import('vue')['watch']
|
||||||
|
const watchEffect: typeof import('vue')['watchEffect']
|
||||||
|
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||||
|
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||||
|
}
|
||||||
|
// for type re-export
|
||||||
|
declare global {
|
||||||
|
// @ts-ignore
|
||||||
|
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||||
|
import('vue')
|
||||||
|
}
|
||||||
Vendored
+57
@@ -0,0 +1,57 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/* prettier-ignore */
|
||||||
|
// @ts-nocheck
|
||||||
|
// Generated by unplugin-vue-components
|
||||||
|
// Read more: https://github.com/vuejs/core/pull/3399
|
||||||
|
export {}
|
||||||
|
|
||||||
|
declare module 'vue' {
|
||||||
|
export interface GlobalComponents {
|
||||||
|
ElAlert: typeof import('element-plus/es')['ElAlert']
|
||||||
|
ElAside: typeof import('element-plus/es')['ElAside']
|
||||||
|
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
||||||
|
ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
|
ElCard: typeof import('element-plus/es')['ElCard']
|
||||||
|
ElCol: typeof import('element-plus/es')['ElCol']
|
||||||
|
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
||||||
|
ElContainer: typeof import('element-plus/es')['ElContainer']
|
||||||
|
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||||
|
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
|
||||||
|
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
|
||||||
|
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||||
|
ElDivider: typeof import('element-plus/es')['ElDivider']
|
||||||
|
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||||
|
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||||
|
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
||||||
|
ElForm: typeof import('element-plus/es')['ElForm']
|
||||||
|
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||||
|
ElHeader: typeof import('element-plus/es')['ElHeader']
|
||||||
|
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||||
|
ElInput: typeof import('element-plus/es')['ElInput']
|
||||||
|
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||||
|
ElMain: typeof import('element-plus/es')['ElMain']
|
||||||
|
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||||
|
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||||
|
ElOption: typeof import('element-plus/es')['ElOption']
|
||||||
|
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||||
|
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||||
|
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||||
|
ElRow: typeof import('element-plus/es')['ElRow']
|
||||||
|
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||||
|
ElSpace: typeof import('element-plus/es')['ElSpace']
|
||||||
|
ElStatistic: typeof import('element-plus/es')['ElStatistic']
|
||||||
|
ElStep: typeof import('element-plus/es')['ElStep']
|
||||||
|
ElSteps: typeof import('element-plus/es')['ElSteps']
|
||||||
|
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||||
|
ElTable: typeof import('element-plus/es')['ElTable']
|
||||||
|
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||||
|
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||||
|
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||||
|
ElTag: typeof import('element-plus/es')['ElTag']
|
||||||
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
}
|
||||||
|
export interface ComponentCustomProperties {
|
||||||
|
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -92,8 +92,6 @@ const handleLogout = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@use '@/styles/variables.scss' as *;
|
|
||||||
|
|
||||||
.layout-container {
|
.layout-container {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
@@ -105,7 +103,7 @@ const handleLogout = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
height: $header-height;
|
height: 60px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -146,12 +144,12 @@ const handleLogout = () => {
|
|||||||
|
|
||||||
.layout-header {
|
.layout-header {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border-bottom: 1px solid $border-color;
|
border-bottom: 1px solid #e4e7ed;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
box-shadow: $shadow-light;
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-left {
|
.header-left {
|
||||||
@@ -183,7 +181,7 @@ const handleLogout = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.layout-main {
|
.layout-main {
|
||||||
background-color: $bg-color;
|
background-color: #f5f7fa;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
@@ -197,4 +195,4 @@ const handleLogout = () => {
|
|||||||
.fade-leave-to {
|
.fade-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+14
-2
@@ -3,6 +3,12 @@ import type { RouteRecordRaw } from 'vue-router'
|
|||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
const routes: RouteRecordRaw[] = [
|
const routes: RouteRecordRaw[] = [
|
||||||
|
{
|
||||||
|
path: '/install',
|
||||||
|
name: 'Install',
|
||||||
|
component: () => import('@/views/install/index.vue'),
|
||||||
|
meta: { title: '安装向导', requiresAuth: false },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/login',
|
path: '/login',
|
||||||
name: 'Login',
|
name: 'Login',
|
||||||
@@ -60,12 +66,18 @@ const router = createRouter({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 路由守卫
|
// 路由守卫
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach(async (to, from, next) => {
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
// 设置页面标题
|
// 设置页面标题
|
||||||
document.title = to.meta.title ? `${to.meta.title} - 代理管理平台` : '代理管理平台'
|
document.title = to.meta.title ? `${to.meta.title} - 代理管理平台` : '代理管理平台'
|
||||||
|
|
||||||
|
// 安装页面不需要检查登录
|
||||||
|
if (to.path === '/install') {
|
||||||
|
next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 检查是否需要登录
|
// 检查是否需要登录
|
||||||
if (to.meta.requiresAuth !== false && !userStore.isLoggedIn) {
|
if (to.meta.requiresAuth !== false && !userStore.isLoggedIn) {
|
||||||
next({ name: 'Login', query: { redirect: to.fullPath } })
|
next({ name: 'Login', query: { redirect: to.fullPath } })
|
||||||
@@ -74,4 +86,4 @@ router.beforeEach((to, from, next) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
@@ -36,13 +36,13 @@ body {
|
|||||||
|
|
||||||
// Element Plus 自定义
|
// Element Plus 自定义
|
||||||
.el-table {
|
.el-table {
|
||||||
--el-table-border-color: #{$border-color};
|
--el-table-border-color: #e4e7ed;
|
||||||
--el-table-header-bg-color: #{$bg-color};
|
--el-table-header-bg-color: #f5f7fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-card {
|
.el-card {
|
||||||
border: none;
|
border: none;
|
||||||
box-shadow: $shadow;
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 通用样式
|
// 通用样式
|
||||||
@@ -56,7 +56,7 @@ body {
|
|||||||
.page-title {
|
.page-title {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: $text-primary;
|
color: #303133;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-stat {
|
.card-stat {
|
||||||
@@ -64,11 +64,11 @@ body {
|
|||||||
.stat-value {
|
.stat-value {
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: $primary-color;
|
color: #409eff;
|
||||||
}
|
}
|
||||||
.stat-label {
|
.stat-label {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: $text-secondary;
|
color: #909399;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,4 +94,4 @@ body {
|
|||||||
&.locked {
|
&.locked {
|
||||||
color: #f56c6c;
|
color: #f56c6c;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,7 @@
|
|||||||
// 变量定义
|
$header-height: 60px;
|
||||||
$primary-color: #409eff;
|
|
||||||
$success-color: #67c23a;
|
|
||||||
$warning-color: #e6a23c;
|
|
||||||
$danger-color: #f56c6c;
|
|
||||||
$info-color: #909399;
|
|
||||||
|
|
||||||
$text-primary: #303133;
|
|
||||||
$text-regular: #606266;
|
|
||||||
$text-secondary: #909399;
|
|
||||||
$text-placeholder: #c0c4cc;
|
|
||||||
|
|
||||||
$border-color: #ebeef5;
|
|
||||||
$border-color-light: #e4e7ed;
|
|
||||||
$border-color-lighter: #ebeef5;
|
|
||||||
$border-color-extra-light: #f2f6fc;
|
|
||||||
|
|
||||||
$bg-color: #f5f7fa;
|
$bg-color: #f5f7fa;
|
||||||
$bg-color-light: #fafafa;
|
$border-color: #e4e7ed;
|
||||||
|
$shadow-light: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
$shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
$text-primary: #303133;
|
||||||
$shadow-light: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04);
|
$text-secondary: #909399;
|
||||||
$shadow-base: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04);
|
$primary-color: #409eff;
|
||||||
|
|
||||||
$radius: 4px;
|
|
||||||
$radius-medium: 6px;
|
|
||||||
$radius-large: 8px;
|
|
||||||
|
|
||||||
$transition: all 0.3s;
|
|
||||||
|
|
||||||
$menu-width: 210px;
|
|
||||||
$header-height: 60px;
|
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@use '@/styles/variables.scss' as *;
|
|
||||||
|
|
||||||
.dashboard {
|
.dashboard {
|
||||||
.stat-row {
|
.stat-row {
|
||||||
@@ -277,12 +277,12 @@ onMounted(() => {
|
|||||||
.stat-value {
|
.stat-value {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: $text-primary;
|
color: #303133;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-label {
|
.stat-label {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: $text-secondary;
|
color: #909399;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,313 @@
|
|||||||
|
<template>
|
||||||
|
<div class="install-container">
|
||||||
|
<el-card class="install-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<h2>{{ form.platform_name || '代理管理平台' }}</h2>
|
||||||
|
<p>安装向导 - Installation Wizard</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-steps :active="currentStep" finish-status="success" simple>
|
||||||
|
<el-step title="数据库配置" />
|
||||||
|
<el-step title="管理员设置" />
|
||||||
|
<el-step title="完成安装" />
|
||||||
|
</el-steps>
|
||||||
|
|
||||||
|
<div class="step-content" v-show="currentStep === 0">
|
||||||
|
<el-form :model="form" label-width="150px">
|
||||||
|
<el-divider content-position="left">数据库配置</el-divider>
|
||||||
|
|
||||||
|
<el-form-item label="数据库类型">
|
||||||
|
<el-radio-group v-model="form.db_type">
|
||||||
|
<el-radio value="sqlite">
|
||||||
|
<el-icon><Database /></el-icon>
|
||||||
|
SQLite(推荐)
|
||||||
|
</el-radio>
|
||||||
|
<el-radio value="postgres">
|
||||||
|
PostgreSQL
|
||||||
|
</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
<div class="form-tip">
|
||||||
|
SQLite 无需额外配置,适合大多数场景;PostgreSQL 适合大规模部署
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<template v-if="form.db_type === 'sqlite'">
|
||||||
|
<el-form-item label="数据文件路径">
|
||||||
|
<el-input v-model="form.sqlite_path" placeholder="data/proxy.db">
|
||||||
|
<template #prepend>./</template>
|
||||||
|
</el-input>
|
||||||
|
<div class="form-tip">默认存储在 data 目录下</div>
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="form.db_type === 'postgres'">
|
||||||
|
<el-form-item label="主机地址">
|
||||||
|
<el-input v-model="form.pg_host" placeholder="localhost" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="端口">
|
||||||
|
<el-input-number v-model="form.pg_port" :min="1" :max="65535" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="用户名">
|
||||||
|
<el-input v-model="form.pg_user" placeholder="postgres" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="密码">
|
||||||
|
<el-input v-model="form.pg_password" type="password" show-password />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="数据库">
|
||||||
|
<el-input v-model="form.pg_database" placeholder="proxy_platform" />
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-divider content-position="left">Redis 缓存(可选)</el-divider>
|
||||||
|
|
||||||
|
<el-form-item label="启用 Redis">
|
||||||
|
<el-switch v-model="form.redis_enabled" />
|
||||||
|
<span class="switch-label">{{ form.redis_enabled ? '已启用' : '未启用' }}</span>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<template v-if="form.redis_enabled">
|
||||||
|
<el-form-item label="Redis 主机">
|
||||||
|
<el-input v-model="form.redis_host" placeholder="localhost" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="Redis 端口">
|
||||||
|
<el-input-number v-model="form.redis_port" :min="1" :max="65535" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="Redis 密码">
|
||||||
|
<el-input v-model="form.redis_password" type="password" show-password placeholder="无密码请留空" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="Redis DB">
|
||||||
|
<el-input-number v-model="form.redis_db" :min="0" :max="15" />
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="nextStep">下一步</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="step-content" v-show="currentStep === 1">
|
||||||
|
<el-form :model="form" label-width="150px">
|
||||||
|
<el-divider content-position="left">管理员设置</el-divider>
|
||||||
|
|
||||||
|
<el-form-item label="平台名称">
|
||||||
|
<el-input v-model="form.platform_name" placeholder="代理管理平台" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="管理员用户名">
|
||||||
|
<el-input v-model="form.admin_user" placeholder="admin" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="管理员密码">
|
||||||
|
<el-input v-model="form.admin_pass" type="password" show-password placeholder="请输入密码" />
|
||||||
|
<div class="form-tip">密码长度至少 6 位</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="确认密码">
|
||||||
|
<el-input v-model="form.admin_pass_confirm" type="password" show-password placeholder="请再次输入密码" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button @click="prevStep">上一步</el-button>
|
||||||
|
<el-button type="primary" @click="checkAndInstall" :loading="installing">
|
||||||
|
开始安装
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="step-content" v-show="currentStep === 2">
|
||||||
|
<div class="install-success">
|
||||||
|
<el-icon :size="80" color="#67C23A"><SuccessFilled /></el-icon>
|
||||||
|
<h3>安装成功!</h3>
|
||||||
|
<p>您的代理管理平台已经完成安装</p>
|
||||||
|
|
||||||
|
<el-descriptions :column="1" border>
|
||||||
|
<el-descriptions-item label="数据库类型">{{ form.db_type }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="Redis 缓存">{{ form.redis_enabled ? '已启用' : '未启用' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="管理员">{{ form.admin_user }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
|
||||||
|
<el-button type="primary" size="large" @click="goToLogin" class="login-btn">
|
||||||
|
进入登录页面
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { Database, SuccessFilled } from '@element-plus/icons-vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const currentStep = ref(0)
|
||||||
|
const installing = ref(false)
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
db_type: 'sqlite',
|
||||||
|
sqlite_path: 'data/proxy.db',
|
||||||
|
pg_host: 'localhost',
|
||||||
|
pg_port: 5432,
|
||||||
|
pg_user: 'postgres',
|
||||||
|
pg_password: '',
|
||||||
|
pg_database: 'proxy_platform',
|
||||||
|
redis_enabled: false,
|
||||||
|
redis_host: 'localhost',
|
||||||
|
redis_port: 6379,
|
||||||
|
redis_password: '',
|
||||||
|
redis_db: 0,
|
||||||
|
platform_name: '代理管理平台',
|
||||||
|
admin_user: 'admin',
|
||||||
|
admin_pass: '',
|
||||||
|
admin_pass_confirm: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const nextStep = () => {
|
||||||
|
currentStep.value++
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevStep = () => {
|
||||||
|
currentStep.value--
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkAndInstall = async () => {
|
||||||
|
// 验证表单
|
||||||
|
if (!form.admin_user) {
|
||||||
|
ElMessage.error('请输入管理员用户名')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.admin_pass || form.admin_pass.length < 6) {
|
||||||
|
ElMessage.error('密码长度至少 6 位')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (form.admin_pass !== form.admin_pass_confirm) {
|
||||||
|
ElMessage.error('两次输入的密码不一致')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (form.db_type === 'postgres') {
|
||||||
|
if (!form.pg_host || !form.pg_database) {
|
||||||
|
ElMessage.error('PostgreSQL 配置不完整')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
installing.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await axios.post('/api/v1/install', form)
|
||||||
|
|
||||||
|
if (res.data.message) {
|
||||||
|
ElMessage.success(res.data.message)
|
||||||
|
currentStep.value = 2
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
const errorMsg = error.response?.data?.error || '安装失败'
|
||||||
|
ElMessage.error(errorMsg)
|
||||||
|
} finally {
|
||||||
|
installing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToLogin = () => {
|
||||||
|
router.push('/login')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否已安装
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const res = await axios.get('/api/v1/install/check')
|
||||||
|
if (res.data.installed) {
|
||||||
|
ElMessage.info('系统已安装,跳转到登录页面')
|
||||||
|
router.push('/login')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 未安装,继续显示安装页面
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.install-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-card {
|
||||||
|
width: 600px;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
text-align: center;
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #303133;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-content {
|
||||||
|
padding: 20px 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-tip {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-label {
|
||||||
|
margin-left: 12px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-success {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 0;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 20px 0 10px;
|
||||||
|
color: #67C23A;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: #909399;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-descriptions {
|
||||||
|
margin: 20px auto;
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn {
|
||||||
|
margin-top: 30px;
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-radio__label) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user