diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index af7781c..c679742 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -2,12 +2,13 @@ name: Build and Push Images Based on Version # 在这里定义统一版本号 env: - VERSION: "1.4.2" + VERSION: "2.0.0-beta" on: push: branches: - main + - beta jobs: check-version: diff --git a/.gitignore b/.gitignore index f547152..b21deae 100644 --- a/.gitignore +++ b/.gitignore @@ -256,4 +256,5 @@ gitee/ *.cert doraemon.jks device.id -commit.sh \ No newline at end of file +commit.sh +sql/ \ No newline at end of file diff --git a/docker-compose-v4.yml b/docker-compose-v4.yml index 38adff3..9243032 100644 --- a/docker-compose-v4.yml +++ b/docker-compose-v4.yml @@ -1,39 +1,10 @@ services: - mysql: - image: mysql:5.7 - container_name: gost-mysql - restart: unless-stopped - environment: - MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} - MYSQL_DATABASE: ${DB_NAME} - MYSQL_USER: ${DB_USER} - MYSQL_PASSWORD: ${DB_PASSWORD} - TZ: Asia/Shanghai - volumes: - - mysql_data:/var/lib/mysql - - ./gost.sql:/docker-entrypoint-initdb.d/init.sql:ro - command: > - --default-authentication-plugin=mysql_native_password - --character-set-server=utf8mb4 - --collation-server=utf8mb4_unicode_ci - --max_connections=1000 - --innodb_buffer_pool_size=256M - networks: - - gost-network - healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] - timeout: 10s - retries: 10 - backend: - image: bqlpfy/springboot-backend:1.4.2 + image: bqlpfy/springboot-backend:2.0.0-beta container_name: springboot-backend restart: unless-stopped environment: - DB_HOST: mysql - DB_NAME: ${DB_NAME} - DB_USER: ${DB_USER} - DB_PASSWORD: ${DB_PASSWORD} + DB_PATH: /app/data/gost.db JWT_SECRET: ${JWT_SECRET} LOG_DIR: /app/logs JAVA_OPTS: "-Xms256m -Xmx512m -Dfile.encoding=UTF-8 -Duser.timezone=Asia/Shanghai" @@ -41,9 +12,7 @@ services: - "${BACKEND_PORT}:6365" volumes: - backend_logs:/app/logs - depends_on: - mysql: - condition: service_healthy + - sqlite_data:/app/data networks: - gost-network healthcheck: @@ -51,10 +20,10 @@ services: interval: 30s timeout: 10s retries: 5 - start_period: 90s + start_period: 60s frontend: - image: bqlpfy/vite-frontend:1.4.2 + image: bqlpfy/vite-frontend:2.0.0-beta container_name: vite-frontend restart: unless-stopped ports: @@ -67,8 +36,8 @@ services: volumes: - mysql_data: - name: mysql_data + sqlite_data: + name: sqlite_data driver: local backend_logs: name: backend_logs diff --git a/docker-compose-v6.yml b/docker-compose-v6.yml index f552810..f930bed 100644 --- a/docker-compose-v6.yml +++ b/docker-compose-v6.yml @@ -1,39 +1,10 @@ services: - mysql: - image: mysql:5.7 - container_name: gost-mysql - restart: unless-stopped - environment: - MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} - MYSQL_DATABASE: ${DB_NAME} - MYSQL_USER: ${DB_USER} - MYSQL_PASSWORD: ${DB_PASSWORD} - TZ: Asia/Shanghai - volumes: - - mysql_data:/var/lib/mysql - - ./gost.sql:/docker-entrypoint-initdb.d/init.sql:ro - command: > - --default-authentication-plugin=mysql_native_password - --character-set-server=utf8mb4 - --collation-server=utf8mb4_unicode_ci - --max_connections=1000 - --innodb_buffer_pool_size=256M - networks: - - gost-network - healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] - timeout: 10s - retries: 10 - backend: - image: bqlpfy/springboot-backend:1.4.2 + image: bqlpfy/springboot-backend:2.0.0-beta container_name: springboot-backend restart: unless-stopped environment: - DB_HOST: mysql - DB_NAME: ${DB_NAME} - DB_USER: ${DB_USER} - DB_PASSWORD: ${DB_PASSWORD} + DB_PATH: /app/data/gost.db JWT_SECRET: ${JWT_SECRET} LOG_DIR: /app/logs JAVA_OPTS: "-Xms256m -Xmx512m -Dfile.encoding=UTF-8 -Duser.timezone=Asia/Shanghai" @@ -41,9 +12,7 @@ services: - "${BACKEND_PORT}:6365" volumes: - backend_logs:/app/logs - depends_on: - mysql: - condition: service_healthy + - sqlite_data:/app/data networks: - gost-network healthcheck: @@ -51,10 +20,10 @@ services: interval: 30s timeout: 10s retries: 5 - start_period: 90s + start_period: 60s frontend: - image: bqlpfy/vite-frontend:1.4.2 + image: bqlpfy/vite-frontend:2.0.0-beta container_name: vite-frontend restart: unless-stopped ports: @@ -67,8 +36,8 @@ services: volumes: - mysql_data: - name: mysql_data + sqlite_data: + name: sqlite_data driver: local backend_logs: name: backend_logs diff --git a/go-gost/main.go b/go-gost/main.go index 2ea5be4..64466c1 100644 --- a/go-gost/main.go +++ b/go-gost/main.go @@ -119,7 +119,7 @@ func main() { log := xlogger.NewLogger() logger.SetDefault(log) - wsReporter := socket.StartWebSocketReporterWithConfig(config.Addr, config.Secret, config.Http, config.Tls, config.Socks, "1.2.3") + wsReporter := socket.StartWebSocketReporterWithConfig(config.Addr, config.Secret, config.Http, config.Tls, config.Socks, "2.0.0") defer wsReporter.Stop() service.SetHTTPReportURL(config.Addr, config.Secret) diff --git a/go-gost/x/service/global_traffic_manager.go b/go-gost/x/service/global_traffic_manager.go new file mode 100644 index 0000000..c34ebcb --- /dev/null +++ b/go-gost/x/service/global_traffic_manager.go @@ -0,0 +1,206 @@ +package service + +import ( + "context" + "fmt" + "sync" + "time" +) + +// GlobalTrafficManager 全局流量管理器(所有服务共享) +type GlobalTrafficManager struct { + mu sync.RWMutex + serviceTraffic map[string]*ServiceTraffic // key: 服务名, value: 流量数据 + ctx context.Context + cancel context.CancelFunc + reportTicker *time.Ticker +} + +// ServiceTraffic 单个服务的流量累积 +type ServiceTraffic struct { + mu sync.Mutex + ServiceName string + UpBytes int64 // 上行流量(累积) + DownBytes int64 // 下行流量(累积) +} + +var ( + globalManager *GlobalTrafficManager + globalManagerOnce sync.Once +) + +// GetGlobalTrafficManager 获取全局流量管理器单例 +func GetGlobalTrafficManager() *GlobalTrafficManager { + globalManagerOnce.Do(func() { + ctx, cancel := context.WithCancel(context.Background()) + globalManager = &GlobalTrafficManager{ + serviceTraffic: make(map[string]*ServiceTraffic), + ctx: ctx, + cancel: cancel, + reportTicker: time.NewTicker(5 * time.Second), + } + // 启动定时上报协程 + go globalManager.startReporting() + }) + return globalManager +} + +// AddTraffic 添加流量到指定服务(由各服务调用) +func (m *GlobalTrafficManager) AddTraffic(serviceName string, upBytes, downBytes int64) { + if upBytes == 0 && downBytes == 0 { + return + } + + m.mu.Lock() + defer m.mu.Unlock() + + // 获取或创建服务流量记录 + traffic, exists := m.serviceTraffic[serviceName] + if !exists { + traffic = &ServiceTraffic{ + ServiceName: serviceName, + } + m.serviceTraffic[serviceName] = traffic + } + + // 累加流量 + traffic.mu.Lock() + traffic.UpBytes += upBytes + traffic.DownBytes += downBytes + traffic.mu.Unlock() +} + +// startReporting 启动定时上报协程(每5秒执行一次) +func (m *GlobalTrafficManager) startReporting() { + + for { + select { + case <-m.reportTicker.C: + m.collectAndReport() + + case <-m.ctx.Done(): + fmt.Printf("⏹️ 全局流量上报器已停止\n") + return + } + } +} + +// collectAndReport 收集所有服务流量并合并上报 +func (m *GlobalTrafficManager) collectAndReport() { + m.mu.Lock() + + // 如果没有流量,直接返回 + if len(m.serviceTraffic) == 0 { + m.mu.Unlock() + return + } + + // 复制当前所有流量数据(避免长时间持锁) + trafficSnapshot := make(map[string]*ServiceTraffic) + reportData := make(map[string]struct { + up int64 + down int64 + }) + + for name, traffic := range m.serviceTraffic { + traffic.mu.Lock() + if traffic.UpBytes > 0 || traffic.DownBytes > 0 { + trafficSnapshot[name] = traffic + reportData[name] = struct { + up int64 + down int64 + }{ + up: traffic.UpBytes, + down: traffic.DownBytes, + } + } + traffic.mu.Unlock() + } + m.mu.Unlock() + + // 如果没有需要上报的流量,返回 + if len(reportData) == 0 { + return + } + + // 构建上报数据数组(保持每个服务独立) + reportItems := make([]TrafficReportItem, 0, len(reportData)) + var totalUp, totalDown int64 + + for serviceName, data := range reportData { + reportItems = append(reportItems, TrafficReportItem{ + N: serviceName, // 保持服务名不变 + U: data.up, + D: data.down, + }) + totalUp += data.up + totalDown += data.down + } + + // 批量发送上报请求(一次HTTP请求包含所有服务) + success, err := sendBatchTrafficReport(m.ctx, reportItems) + if err != nil { + fmt.Printf("❌ 全局流量上报失败: %v (总流量: ↑%d ↓%d, %d个服务)\n", err, totalUp, totalDown, len(reportItems)) + return + } + + if !success { + fmt.Printf("⚠️ 全局流量上报未成功 (总流量: ↑%d ↓%d, %d个服务)\n", totalUp, totalDown, len(reportItems)) + return + } + + // 上报成功,清空已上报的流量 + m.clearReportedTraffic(reportData) +} + +// clearReportedTraffic 清空已成功上报的流量 +func (m *GlobalTrafficManager) clearReportedTraffic(reportedData map[string]struct { + up int64 + down int64 +}) { + m.mu.Lock() + defer m.mu.Unlock() + + for serviceName, reported := range reportedData { + if traffic, exists := m.serviceTraffic[serviceName]; exists { + traffic.mu.Lock() + // 减去已上报的流量 + traffic.UpBytes -= reported.up + traffic.DownBytes -= reported.down + + // 如果流量归零,从map中删除该服务记录(避免内存泄漏) + if traffic.UpBytes <= 0 && traffic.DownBytes <= 0 { + traffic.mu.Unlock() + delete(m.serviceTraffic, serviceName) + } else { + traffic.mu.Unlock() + } + } + } +} + +// Stop 停止全局流量管理器 +func (m *GlobalTrafficManager) Stop() { + if m.reportTicker != nil { + m.reportTicker.Stop() + } + if m.cancel != nil { + m.cancel() + } + fmt.Printf("🛑 全局流量管理器已停止\n") +} + +// GetServiceTraffic 获取指定服务的当前流量(用于调试) +func (m *GlobalTrafficManager) GetServiceTraffic(serviceName string) (upBytes, downBytes int64) { + m.mu.RLock() + defer m.mu.RUnlock() + + if traffic, exists := m.serviceTraffic[serviceName]; exists { + traffic.mu.Lock() + upBytes = traffic.UpBytes + downBytes = traffic.DownBytes + traffic.mu.Unlock() + } + return +} + diff --git a/go-gost/x/service/service.go b/go-gost/x/service/service.go index 5c50b20..0ae6c00 100644 --- a/go-gost/x/service/service.go +++ b/go-gost/x/service/service.go @@ -403,19 +403,15 @@ func (s *defaultService) observeStats(ctx context.Context) { TotalErrs: st.Get(stats.KindTotalErrs), }, } + + // 将流量累积到全局管理器,而不是立即上报 if outputBytes > 0 || inputBytes > 0 { - reportItems := TrafficReportItem{ - N: s.name, - U: int64(outputBytes), - D: int64(inputBytes), - } - success, err := sendTrafficReport(ctx, reportItems) - if err != nil { - fmt.Printf("发送流量报告失败: %v", err) - } else if success { - if xstats, ok := st.(*xstats.Stats); ok { - xstats.ResetTraffic(st.Get(stats.KindInputBytes)-inputBytes, st.Get(stats.KindOutputBytes)-outputBytes) - } + globalManager := GetGlobalTrafficManager() + globalManager.AddTraffic(s.name, int64(outputBytes), int64(inputBytes)) + + // 立即重置流量计数(因为已经记录到全局管理器中) + if xstats, ok := st.(*xstats.Stats); ok { + xstats.ResetTraffic(st.Get(stats.KindInputBytes)-inputBytes, st.Get(stats.KindOutputBytes)-outputBytes) } } diff --git a/go-gost/x/service/traffic_reporter.go b/go-gost/x/service/traffic_reporter.go index 1c57ebd..3e448a1 100644 --- a/go-gost/x/service/traffic_reporter.go +++ b/go-gost/x/service/traffic_reporter.go @@ -41,8 +41,8 @@ func SetHTTPReportURL(addr string, secret string) { } } -// sendTrafficReport 发送流量报告到HTTP接口 -func sendTrafficReport(ctx context.Context, reportItems TrafficReportItem) (bool, error) { +// sendBatchTrafficReport 批量发送多个服务的流量报告到HTTP接口 +func sendBatchTrafficReport(ctx context.Context, reportItems []TrafficReportItem) (bool, error) { jsonData, err := json.Marshal(reportItems) if err != nil { return false, fmt.Errorf("序列化报告数据失败: %v", err) @@ -112,6 +112,7 @@ func sendTrafficReport(ctx context.Context, reportItems TrafficReportItem) (bool } } + // sendConfigReport 发送配置报告到HTTP接口 func sendConfigReport(ctx context.Context) (bool, error) { if configReportURL == "" { diff --git a/gost.sql b/gost.sql deleted file mode 100644 index 72dd94d..0000000 --- a/gost.sql +++ /dev/null @@ -1,306 +0,0 @@ --- phpMyAdmin SQL Dump --- version 5.2.0 --- https://www.phpmyadmin.net/ --- --- 主机: localhost --- 生成日期: 2025-08-14 21:52:52 --- 服务器版本: 5.7.40-log --- PHP 版本: 7.4.33 - -SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; -START TRANSACTION; -SET time_zone = "+00:00"; - - -/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; -/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; -/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; -/*!40101 SET NAMES utf8mb4 */; - --- --- 数据库: `gost` --- - --- -------------------------------------------------------- - --- --- 表的结构 `forward` --- - -CREATE TABLE `forward` ( - `id` int(10) NOT NULL, - `user_id` int(10) NOT NULL, - `user_name` varchar(100) NOT NULL, - `name` varchar(100) NOT NULL, - `tunnel_id` int(10) NOT NULL, - `in_port` int(10) NOT NULL, - `out_port` int(10) DEFAULT NULL, - `remote_addr` longtext NOT NULL, - `strategy` varchar(100) NOT NULL DEFAULT 'fifo', - `interface_name` varchar(200) DEFAULT NULL, - `in_flow` bigint(20) NOT NULL DEFAULT '0', - `out_flow` bigint(20) NOT NULL DEFAULT '0', - `created_time` bigint(20) NOT NULL, - `updated_time` bigint(20) NOT NULL, - `status` int(10) NOT NULL, - `inx` int(10) NOT NULL DEFAULT '0' -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- -------------------------------------------------------- - --- --- 表的结构 `node` --- - -CREATE TABLE `node` ( - `id` int(10) NOT NULL, - `name` varchar(100) NOT NULL, - `secret` varchar(100) NOT NULL, - `ip` longtext, - `server_ip` varchar(100) NOT NULL, - `port_sta` int(10) NOT NULL, - `port_end` int(10) NOT NULL, - `version` varchar(100) DEFAULT NULL, - `http` int(10) NOT NULL DEFAULT '0', - `tls` int(10) NOT NULL DEFAULT '0', - `socks` int(10) NOT NULL DEFAULT '0', - `created_time` bigint(20) NOT NULL, - `updated_time` bigint(20) DEFAULT NULL, - `status` int(10) NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- -------------------------------------------------------- - --- --- 表的结构 `speed_limit` --- - -CREATE TABLE `speed_limit` ( - `id` int(10) NOT NULL, - `name` varchar(100) NOT NULL, - `speed` int(10) NOT NULL, - `tunnel_id` int(10) NOT NULL, - `tunnel_name` varchar(100) NOT NULL, - `created_time` bigint(20) NOT NULL, - `updated_time` bigint(20) DEFAULT NULL, - `status` int(10) NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- -------------------------------------------------------- - --- --- 表的结构 `statistics_flow` --- - -CREATE TABLE `statistics_flow` ( - `id` int(10) NOT NULL, - `user_id` int(10) NOT NULL, - `flow` bigint(20) NOT NULL, - `total_flow` bigint(20) NOT NULL, - `time` varchar(100) NOT NULL, - `created_time` bigint(20) NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- -------------------------------------------------------- - --- --- 表的结构 `tunnel` --- - -CREATE TABLE `tunnel` ( - `id` int(10) NOT NULL, - `name` varchar(100) NOT NULL, - `traffic_ratio` decimal(10,1) NOT NULL DEFAULT '1.0', - `in_node_id` int(10) NOT NULL, - `in_ip` varchar(100) NOT NULL, - `out_node_id` int(10) NOT NULL, - `out_ip` varchar(100) NOT NULL, - `type` int(10) NOT NULL, - `protocol` varchar(10) NOT NULL DEFAULT 'tls', - `flow` int(10) NOT NULL, - `tcp_listen_addr` varchar(100) NOT NULL DEFAULT '[::]', - `udp_listen_addr` varchar(100) NOT NULL DEFAULT '[::]', - `interface_name` varchar(200) DEFAULT NULL, - `created_time` bigint(20) NOT NULL, - `updated_time` bigint(20) NOT NULL, - `status` int(10) NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- -------------------------------------------------------- - --- --- 表的结构 `user` --- - -CREATE TABLE `user` ( - `id` int(10) NOT NULL, - `user` varchar(100) NOT NULL, - `pwd` varchar(100) NOT NULL, - `role_id` int(10) NOT NULL, - `exp_time` bigint(20) NOT NULL, - `flow` bigint(20) NOT NULL, - `in_flow` bigint(20) NOT NULL DEFAULT '0', - `out_flow` bigint(20) NOT NULL DEFAULT '0', - `flow_reset_time` bigint(20) NOT NULL, - `num` int(10) NOT NULL, - `created_time` bigint(20) NOT NULL, - `updated_time` bigint(20) DEFAULT NULL, - `status` int(10) NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- --- 转存表中的数据 `user` --- - -INSERT INTO `user` (`id`, `user`, `pwd`, `role_id`, `exp_time`, `flow`, `in_flow`, `out_flow`, `flow_reset_time`, `num`, `created_time`, `updated_time`, `status`) VALUES -(1, 'admin_user', '3c85cdebade1c51cf64ca9f3c09d182d', 0, 2727251700000, 99999, 0, 0, 1, 99999, 1748914865000, 1754011744252, 1); - --- -------------------------------------------------------- - --- --- 表的结构 `user_tunnel` --- - -CREATE TABLE `user_tunnel` ( - `id` int(10) NOT NULL, - `user_id` int(10) NOT NULL, - `tunnel_id` int(10) NOT NULL, - `speed_id` int(10) DEFAULT NULL, - `num` int(10) NOT NULL, - `flow` bigint(20) NOT NULL, - `in_flow` bigint(20) NOT NULL DEFAULT '0', - `out_flow` bigint(20) NOT NULL DEFAULT '0', - `flow_reset_time` bigint(20) NOT NULL, - `exp_time` bigint(20) NOT NULL, - `status` int(10) NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- -------------------------------------------------------- - --- --- 表的结构 `vite_config` --- - -CREATE TABLE `vite_config` ( - `id` int(10) NOT NULL, - `name` varchar(200) NOT NULL, - `value` varchar(200) NOT NULL, - `time` bigint(20) NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- --- 转存表中的数据 `vite_config` --- - -INSERT INTO `vite_config` (`id`, `name`, `value`, `time`) VALUES -(1, 'app_name', 'flux', 1755147963000); - --- --- 转储表的索引 --- - --- --- 表的索引 `forward` --- -ALTER TABLE `forward` - ADD PRIMARY KEY (`id`); - --- --- 表的索引 `node` --- -ALTER TABLE `node` - ADD PRIMARY KEY (`id`); - --- --- 表的索引 `speed_limit` --- -ALTER TABLE `speed_limit` - ADD PRIMARY KEY (`id`); - --- --- 表的索引 `statistics_flow` --- -ALTER TABLE `statistics_flow` - ADD PRIMARY KEY (`id`); - --- --- 表的索引 `tunnel` --- -ALTER TABLE `tunnel` - ADD PRIMARY KEY (`id`); - --- --- 表的索引 `user` --- -ALTER TABLE `user` - ADD PRIMARY KEY (`id`); - --- --- 表的索引 `user_tunnel` --- -ALTER TABLE `user_tunnel` - ADD PRIMARY KEY (`id`); - --- --- 表的索引 `vite_config` --- -ALTER TABLE `vite_config` - ADD PRIMARY KEY (`id`), - ADD UNIQUE KEY `name` (`name`); - --- --- 在导出的表使用AUTO_INCREMENT --- - --- --- 使用表AUTO_INCREMENT `forward` --- -ALTER TABLE `forward` - MODIFY `id` int(10) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1; - --- --- 使用表AUTO_INCREMENT `node` --- -ALTER TABLE `node` - MODIFY `id` int(10) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1; - --- --- 使用表AUTO_INCREMENT `speed_limit` --- -ALTER TABLE `speed_limit` - MODIFY `id` int(10) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1; - --- --- 使用表AUTO_INCREMENT `statistics_flow` --- -ALTER TABLE `statistics_flow` - MODIFY `id` int(10) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1; - --- --- 使用表AUTO_INCREMENT `tunnel` --- -ALTER TABLE `tunnel` - MODIFY `id` int(10) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1; - --- --- 使用表AUTO_INCREMENT `user` --- -ALTER TABLE `user` - MODIFY `id` int(10) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1; - --- --- 使用表AUTO_INCREMENT `user_tunnel` --- -ALTER TABLE `user_tunnel` - MODIFY `id` int(10) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1; - --- --- 使用表AUTO_INCREMENT `vite_config` --- -ALTER TABLE `vite_config` - MODIFY `id` int(10) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1; -COMMIT; - -/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; -/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; -/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; diff --git a/install.sh b/install.sh index 3f750e0..560fe06 100644 --- a/install.sh +++ b/install.sh @@ -1,6 +1,6 @@ #!/bin/bash # 下载地址 -DOWNLOAD_URL="https://github.com/bqlpfy/flux-panel/releases/download/1.4.2/gost" +DOWNLOAD_URL="https://github.com/bqlpfy/flux-panel/releases/download/2.0.0-beta/gost" INSTALL_DIR="/etc/gost" COUNTRY=$(curl -s https://ipinfo.io/country) if [ "$COUNTRY" = "CN" ]; then @@ -131,8 +131,8 @@ while getopts "a:s:" opt; do done # 安装功能 -install_gost() { - echo "🚀 开始安装 GOST..." +install_flux_agent() { + echo "🚀 开始安装 flux_agent..." get_config_params # 检查并安装 tcpkill @@ -142,27 +142,27 @@ install_gost() { mkdir -p "$INSTALL_DIR" # 停止并禁用已有服务 - if systemctl list-units --full -all | grep -Fq "gost.service"; then - echo "🔍 检测到已存在的gost服务" - systemctl stop gost 2>/dev/null && echo "🛑 停止服务" - systemctl disable gost 2>/dev/null && echo "🚫 禁用自启" + if systemctl list-units --full -all | grep -Fq "flux_agent.service"; then + echo "🔍 检测到已存在的flux_agent服务" + systemctl stop flux_agent 2>/dev/null && echo "🛑 停止服务" + systemctl disable flux_agent 2>/dev/null && echo "🚫 禁用自启" fi # 删除旧文件 - [[ -f "$INSTALL_DIR/gost" ]] && echo "🧹 删除旧文件 gost" && rm -f "$INSTALL_DIR/gost" + [[ -f "$INSTALL_DIR/flux_agent" ]] && echo "🧹 删除旧文件 flux_agent" && rm -f "$INSTALL_DIR/flux_agent" - # 下载 gost - echo "⬇️ 下载 gost 中..." - curl -L "$DOWNLOAD_URL" -o "$INSTALL_DIR/gost" - if [[ ! -f "$INSTALL_DIR/gost" || ! -s "$INSTALL_DIR/gost" ]]; then + # 下载 flux_agent + echo "⬇️ 下载 flux_agent 中..." + curl -L "$DOWNLOAD_URL" -o "$INSTALL_DIR/flux_agent" + if [[ ! -f "$INSTALL_DIR/flux_agent" || ! -s "$INSTALL_DIR/flux_agent" ]]; then echo "❌ 下载失败,请检查网络或下载链接。" exit 1 fi - chmod +x "$INSTALL_DIR/gost" + chmod +x "$INSTALL_DIR/flux_agent" echo "✅ 下载完成" # 打印版本 - echo "🔎 gost 版本:$($INSTALL_DIR/gost -V)" + echo "🔎 flux_agent 版本:$($INSTALL_DIR/flux_agent -V)" # 写入 config.json (安装时总是创建新的) CONFIG_FILE="$INSTALL_DIR/config.json" @@ -189,15 +189,15 @@ EOF chmod 600 "$INSTALL_DIR"/*.json # 创建 systemd 服务 - SERVICE_FILE="/etc/systemd/system/gost.service" + SERVICE_FILE="/etc/systemd/system/flux_agent.service" cat > "$SERVICE_FILE" </dev/null - systemctl disable gost 2>/dev/null + systemctl stop flux_agent 2>/dev/null + systemctl disable flux_agent 2>/dev/null fi # 删除服务文件 - if [[ -f "/etc/systemd/system/gost.service" ]]; then - rm -f "/etc/systemd/system/gost.service" + if [[ -f "/etc/systemd/system/flux_agent.service" ]]; then + rm -f "/etc/systemd/system/flux_agent.service" echo "🧹 删除服务文件" fi @@ -302,7 +302,7 @@ uninstall_gost() { main() { # 如果提供了命令行参数,直接执行安装 if [[ -n "$SERVER_ADDR" && -n "$SECRET" ]]; then - install_gost + install_flux_agent delete_self exit 0 fi @@ -310,36 +310,31 @@ main() { # 显示交互式菜单 while true; do show_menu - read -p "请输入选项 (1-5): " choice + read -p "请输入选项 (1-4): " choice case $choice in 1) - install_gost + install_flux_agent delete_self exit 0 ;; 2) - update_gost + update_flux_agent delete_self exit 0 ;; 3) - uninstall_gost + uninstall_flux_agent delete_self exit 0 ;; 4) - block_protocol - delete_self - exit 0 - ;; - 5) echo "👋 退出脚本" delete_self exit 0 ;; *) - echo "❌ 无效选项,请输入 1-5" + echo "❌ 无效选项,请输入 1-4" echo "" ;; esac diff --git a/panel_install.sh b/panel_install.sh index ccc74ab..4247cf0 100755 --- a/panel_install.sh +++ b/panel_install.sh @@ -8,16 +8,14 @@ export LC_ALL=C # 全局下载地址配置 -DOCKER_COMPOSEV4_URL="https://github.com/bqlpfy/flux-panel/releases/download/1.4.2/docker-compose-v4.yml" -DOCKER_COMPOSEV6_URL="https://github.com/bqlpfy/flux-panel/releases/download/1.4.2/docker-compose-v6.yml" -GOST_SQL_URL="https://github.com/bqlpfy/flux-panel/releases/download/1.4.2/gost.sql" +DOCKER_COMPOSEV4_URL="https://github.com/bqlpfy/flux-panel/releases/download/2.0.0-beta/docker-compose-v4.yml" +DOCKER_COMPOSEV6_URL="https://github.com/bqlpfy/flux-panel/releases/download/2.0.0-beta/docker-compose-v6.yml" COUNTRY=$(curl -s https://ipinfo.io/country) if [ "$COUNTRY" = "CN" ]; then # 拼接 URL DOCKER_COMPOSEV4_URL="https://ghfast.top/${DOCKER_COMPOSEV4_URL}" DOCKER_COMPOSEV6_URL="https://ghfast.top/${DOCKER_COMPOSEV6_URL}" - GOST_SQL_URL="https://ghfast.top/${GOST_SQL_URL}" fi @@ -149,8 +147,7 @@ show_menu() { echo "1. 安装面板" echo "2. 更新面板" echo "3. 卸载面板" - echo "4. 导出备份" - echo "5. 退出" + echo "4. 退出" echo "===============================================" } @@ -173,17 +170,13 @@ delete_self() { get_config_params() { echo "🔧 请输入配置参数:" - - read -p "前端端口(默认 6366): " FRONTEND_PORT FRONTEND_PORT=${FRONTEND_PORT:-6366} read -p "后端端口(默认 6365): " BACKEND_PORT BACKEND_PORT=${BACKEND_PORT:-6365} - DB_NAME=$(generate_random) - DB_USER=$(generate_random) - DB_PASSWORD=$(generate_random) + # 生成JWT密钥 JWT_SECRET=$(generate_random) } @@ -197,14 +190,6 @@ install_panel() { DOCKER_COMPOSE_URL=$(get_docker_compose_url) echo "📡 选择配置文件:$(basename "$DOCKER_COMPOSE_URL")" curl -L -o docker-compose.yml "$DOCKER_COMPOSE_URL" - - # 检查 gost.sql 是否已存在 - if [[ -f "gost.sql" ]]; then - echo "⏭️ 跳过下载: gost.sql (使用当前位置的文件)" - else - echo "📡 下载数据库初始化文件..." - curl -L -o gost.sql "$GOST_SQL_URL" - fi echo "✅ 文件准备完成" # 自动检测并配置 IPv6 支持 @@ -214,9 +199,6 @@ install_panel() { fi cat > .env </dev/null || echo "unknown") - if [[ "$DB_HEALTH" == "healthy" ]]; then - echo "✅ 数据库服务健康检查通过" - break - elif [[ "$DB_HEALTH" == "starting" ]]; then - # 继续等待 - : - elif [[ "$DB_HEALTH" == "unhealthy" ]]; then - echo "⚠️ 数据库健康状态:$DB_HEALTH" - fi - else - echo "⚠️ 数据库容器未找到或未运行" - DB_HEALTH="not_running" - fi - if [ $i -eq 60 ]; then - echo "❌ 数据库服务启动超时(60秒)" - echo "🔍 当前状态:$(docker inspect -f '{{.State.Health.Status}}' gost-mysql 2>/dev/null || echo '容器不存在')" - echo "🛑 更新终止" - return 1 - fi - # 每10秒显示一次进度 - if [ $((i % 10)) -eq 1 ]; then - echo "⏳ 等待数据库服务启动... ($i/60) 状态:${DB_HEALTH:-unknown}" - fi - sleep 1 - done - - # 从容器环境变量获取数据库信息 - echo "🔍 获取数据库配置信息..." - - # 等待一下让服务完全就绪 - echo "⏳ 等待服务完全就绪..." - sleep 5 - - # 先检查后端容器是否在运行 - if ! docker ps --format "{{.Names}}" | grep -q "^springboot-backend$"; then - echo "❌ 后端容器未运行,无法获取数据库配置" - echo "🔍 当前运行的容器:" - docker ps --format "table {{.Names}}\t{{.Status}}" - echo "🛑 更新终止" - return 1 - fi - - DB_INFO=$(docker exec springboot-backend env | grep "^DB_" 2>/dev/null || echo "") - - if [[ -n "$DB_INFO" ]]; then - DB_NAME=$(echo "$DB_INFO" | grep "^DB_NAME=" | cut -d'=' -f2) - DB_PASSWORD=$(echo "$DB_INFO" | grep "^DB_PASSWORD=" | cut -d'=' -f2) - DB_USER=$(echo "$DB_INFO" | grep "^DB_USER=" | cut -d'=' -f2) - DB_HOST=$(echo "$DB_INFO" | grep "^DB_HOST=" | cut -d'=' -f2) - - echo "📋 数据库配置:" - echo " 数据库名: $DB_NAME" - echo " 用户名: $DB_USER" - echo " 主机: $DB_HOST" - else - echo "❌ 无法获取数据库配置信息" - echo "🔍 尝试诊断问题:" - echo " 容器状态: $(docker inspect -f '{{.State.Status}}' springboot-backend 2>/dev/null || echo '容器不存在')" - echo " 健康状态: $(docker inspect -f '{{.State.Health.Status}}' springboot-backend 2>/dev/null || echo '无健康检查')" - - # 尝试从 .env 文件读取配置 - if [[ -f ".env" ]]; then - echo "🔄 尝试从 .env 文件读取配置..." - DB_NAME=$(grep "^DB_NAME=" .env | cut -d'=' -f2 2>/dev/null) - DB_PASSWORD=$(grep "^DB_PASSWORD=" .env | cut -d'=' -f2 2>/dev/null) - DB_USER=$(grep "^DB_USER=" .env | cut -d'=' -f2 2>/dev/null) - - if [[ -n "$DB_NAME" && -n "$DB_PASSWORD" && -n "$DB_USER" ]]; then - echo "✅ 从 .env 文件成功读取数据库配置" - echo "📋 数据库配置:" - echo " 数据库名: $DB_NAME" - echo " 用户名: $DB_USER" - else - echo "❌ .env 文件中的数据库配置不完整" - echo "🛑 更新终止" - return 1 - fi - else - echo "❌ 未找到 .env 文件" - echo "🛑 更新终止" - return 1 - fi - fi - - # 检查必要的数据库配置 - if [[ -z "$DB_PASSWORD" || -z "$DB_USER" || -z "$DB_NAME" ]]; then - echo "❌ 数据库配置不完整(缺少必要参数)" - echo "🛑 更新终止" - return 1 - fi - - # 执行数据库字段变更 - echo "🔄 执行数据库结构更新..." - - # 创建临时迁移文件(现在有了数据库信息) - cat > temp_migration.sql </dev/null; then - echo "✅ 数据库结构更新完成" - else - echo "⚠️ 使用用户密码失败,尝试root密码..." - if docker exec -i gost-mysql mysql -u root -p"$DB_PASSWORD" < temp_migration.sql 2>/dev/null; then - echo "✅ 数据库结构更新完成" - else - echo "❌ 数据库结构更新失败,请手动执行 temp_migration.sql" - echo "📁 迁移文件已保存为 temp_migration.sql" - echo "🔍 数据库容器状态: $(docker inspect -f '{{.State.Status}}' gost-mysql 2>/dev/null || echo '容器不存在')" - echo "🛑 更新终止" - return 1 - fi - fi - - # 清理临时文件 - rm -f temp_migration.sql - echo "✅ 更新完成" } -# 导出数据库备份 -export_migration_sql() { - echo "📄 开始导出数据库备份..." - - # 获取数据库配置信息 - echo "🔍 获取数据库配置信息..." - - # 先检查后端容器是否在运行 - if ! docker ps --format "{{.Names}}" | grep -q "^springboot-backend$"; then - echo "❌ 后端容器未运行,尝试从 .env 文件读取配置..." - - # 从 .env 文件读取配置 - if [[ -f ".env" ]]; then - DB_NAME=$(grep "^DB_NAME=" .env | cut -d'=' -f2 2>/dev/null) - DB_PASSWORD=$(grep "^DB_PASSWORD=" .env | cut -d'=' -f2 2>/dev/null) - DB_USER=$(grep "^DB_USER=" .env | cut -d'=' -f2 2>/dev/null) - - if [[ -n "$DB_NAME" && -n "$DB_PASSWORD" && -n "$DB_USER" ]]; then - echo "✅ 从 .env 文件读取数据库配置成功" - else - echo "❌ .env 文件中的数据库配置不完整" - return 1 - fi - else - echo "❌ 未找到 .env 文件" - return 1 - fi - else - # 从容器环境变量获取数据库信息 - DB_INFO=$(docker exec springboot-backend env | grep "^DB_" 2>/dev/null || echo "") - - if [[ -n "$DB_INFO" ]]; then - DB_NAME=$(echo "$DB_INFO" | grep "^DB_NAME=" | cut -d'=' -f2) - DB_PASSWORD=$(echo "$DB_INFO" | grep "^DB_PASSWORD=" | cut -d'=' -f2) - DB_USER=$(echo "$DB_INFO" | grep "^DB_USER=" | cut -d'=' -f2) - - echo "✅ 从容器环境变量读取数据库配置成功" - else - echo "❌ 无法从容器获取数据库配置,尝试从 .env 文件读取..." - - if [[ -f ".env" ]]; then - DB_NAME=$(grep "^DB_NAME=" .env | cut -d'=' -f2 2>/dev/null) - DB_PASSWORD=$(grep "^DB_PASSWORD=" .env | cut -d'=' -f2 2>/dev/null) - DB_USER=$(grep "^DB_USER=" .env | cut -d'=' -f2 2>/dev/null) - - if [[ -n "$DB_NAME" && -n "$DB_PASSWORD" && -n "$DB_USER" ]]; then - echo "✅ 从 .env 文件读取数据库配置成功" - else - echo "❌ .env 文件中的数据库配置不完整" - return 1 - fi - else - echo "❌ 未找到 .env 文件" - return 1 - fi - fi - fi - - # 检查必要的数据库配置 - if [[ -z "$DB_PASSWORD" || -z "$DB_USER" || -z "$DB_NAME" ]]; then - echo "❌ 数据库配置不完整(缺少必要参数)" - return 1 - fi - - echo "📋 数据库配置:" - echo " 数据库名: $DB_NAME" - echo " 用户名: $DB_USER" - - # 检查数据库容器是否运行 - if ! docker ps --format "{{.Names}}" | grep -q "^gost-mysql$"; then - echo "❌ 数据库容器未运行,无法导出数据" - echo "🔍 当前运行的容器:" - docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}" - return 1 - fi - - # 生成数据库备份文件 - SQL_FILE="database_backup_$(date +%Y%m%d_%H%M%S).sql" - echo "📝 导出数据库备份: $SQL_FILE" - - # 使用 mysqldump 导出数据库 - echo "⏳ 正在导出数据库..." - if docker exec gost-mysql mysqldump -u "$DB_USER" -p"$DB_PASSWORD" --single-transaction --routines --triggers "$DB_NAME" > "$SQL_FILE" 2>/dev/null; then - echo "✅ 数据库导出成功" - else - echo "⚠️ 使用用户密码失败,尝试root密码..." - if docker exec gost-mysql mysqldump -u root -p"$DB_PASSWORD" --single-transaction --routines --triggers "$DB_NAME" > "$SQL_FILE" 2>/dev/null; then - echo "✅ 数据库导出成功" - else - echo "❌ 数据库导出失败" - rm -f "$SQL_FILE" - return 1 - fi - fi - - # 检查文件大小 - if [[ -f "$SQL_FILE" ]] && [[ -s "$SQL_FILE" ]]; then - FILE_SIZE=$(du -h "$SQL_FILE" | cut -f1) - echo "📁 文件位置: $(pwd)/$SQL_FILE" - echo "📊 文件大小: $FILE_SIZE" - else - echo "❌ 导出的文件为空或不存在" - rm -f "$SQL_FILE" - return 1 - fi -} # 卸载功能 @@ -1068,7 +304,7 @@ uninstall_panel() { echo "🛑 停止并删除容器、镜像、卷..." $DOCKER_CMD down --rmi all --volumes --remove-orphans echo "🧹 删除配置文件..." - rm -f docker-compose.yml gost.sql .env + rm -f docker-compose.yml .env echo "✅ 卸载完成" } @@ -1097,11 +333,6 @@ main() { exit 0 ;; 4) - export_migration_sql - delete_self - exit 0 - ;; - 5) echo "👋 退出脚本" delete_self exit 0 diff --git a/springboot-backend/Dockerfile b/springboot-backend/Dockerfile index 8b2ffac..908e9f1 100644 --- a/springboot-backend/Dockerfile +++ b/springboot-backend/Dockerfile @@ -21,6 +21,7 @@ RUN sed -i 's/archive.ubuntu.com/mirrors.ustc.edu.cn/g' /etc/apt/sources.list \ && rm -rf /var/lib/apt/lists/* COPY --from=build /app/target/*.jar app.jar + EXPOSE 6365 ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar /app/app.jar"] diff --git a/springboot-backend/pom.xml b/springboot-backend/pom.xml index 132ecb0..834fd04 100644 --- a/springboot-backend/pom.xml +++ b/springboot-backend/pom.xml @@ -69,10 +69,9 @@ - com.mysql - mysql-connector-j - 8.2.0 - runtime + org.xerial + sqlite-jdbc + 3.45.0.0 diff --git a/springboot-backend/src/main/java/com/admin/CodeGenerator.java b/springboot-backend/src/main/java/com/admin/CodeGenerator.java deleted file mode 100644 index ba1f097..0000000 --- a/springboot-backend/src/main/java/com/admin/CodeGenerator.java +++ /dev/null @@ -1,128 +0,0 @@ -package com.admin; - -import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException; -import com.baomidou.mybatisplus.core.toolkit.StringPool; -import com.baomidou.mybatisplus.core.toolkit.StringUtils; -import com.baomidou.mybatisplus.generator.AutoGenerator; -import com.baomidou.mybatisplus.generator.InjectionConfig; -import com.baomidou.mybatisplus.generator.config.*; -import com.baomidou.mybatisplus.generator.config.po.TableInfo; -import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy; -import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine; - -import java.util.ArrayList; -import java.util.List; -import java.util.Scanner; - -// 演示例子,执行 main 方法控制台输入模块表名回车自动生成对应项目目录中 -public class CodeGenerator { - - /** - *

- * 读取控制台内容 - *

- */ - public static String scanner(String tip) { - Scanner scanner = new Scanner(System.in); - StringBuilder help = new StringBuilder(); - help.append("请输入" + tip + ":"); - System.out.println(help.toString()); - if (scanner.hasNext()) { - String ipt = scanner.next(); - if (StringUtils.isNotBlank(ipt)) { - return ipt; - } - } - throw new MybatisPlusException("请输入正确的" + tip + "!"); - } - - public static void main(String[] args) { - // 代码生成器 - AutoGenerator mpg = new AutoGenerator(); - - // 全局配置 - GlobalConfig gc = new GlobalConfig(); - String projectPath = System.getProperty("user.dir"); - gc.setOutputDir(projectPath + "/src/main/java"); - gc.setAuthor("QAQ"); - gc.setOpen(false); - // gc.setSwagger2(true); 实体属性 Swagger2 注解 - gc.setServiceName("%sService"); - mpg.setGlobalConfig(gc); - - // 数据源配置 - 使用环境变量 - DataSourceConfig dsc = new DataSourceConfig(); - String dbHost = System.getenv("DB_HOST"); - String dbName = System.getenv("DB_NAME"); - String dbUser = System.getenv("DB_USER"); - String dbPassword = System.getenv("DB_PASSWORD"); - - if (dbHost == null || dbName == null || dbUser == null || dbPassword == null) { - throw new MybatisPlusException("请设置数据库环境变量: DB_HOST, DB_NAME, DB_USER, DB_PASSWORD"); - } - - dsc.setUrl("jdbc:mysql://" + dbHost + "/" + dbName + "?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai"); - dsc.setDriverName("com.mysql.cj.jdbc.Driver"); - dsc.setUsername(dbUser); - dsc.setPassword(dbPassword); - mpg.setDataSource(dsc); - - // 包配置 - PackageConfig pc = new PackageConfig(); -// pc.setModuleName(scanner("模块名")); - pc.setParent("com.admin"); - mpg.setPackageInfo(pc); - - // 自定义配置 - InjectionConfig cfg = new InjectionConfig() { - @Override - public void initMap() { - // to do nothing - } - }; - - // 如果模板引擎是 freemarker - String templatePath = "/templates/mapper.xml.ftl"; - // 如果模板引擎是 velocity -// String templatePath = "/templates/mapper.xml.vm"; - - // 自定义输出配置 - List focList = new ArrayList<>(); - // 自定义配置会被优先输出 - focList.add(new FileOutConfig(templatePath) { - @Override - public String outputFile(TableInfo tableInfo) { - return projectPath + "/src/main/resources/mapper/" + pc.getModuleName() - + "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML; - } - }); - - cfg.setFileOutConfigList(focList); - mpg.setCfg(cfg); - - // 配置模板 - TemplateConfig templateConfig = new TemplateConfig(); - - - templateConfig.setXml(null); - mpg.setTemplate(templateConfig); - - // 策略配置 - StrategyConfig strategy = new StrategyConfig(); - strategy.setNaming(NamingStrategy.underline_to_camel); - strategy.setColumnNaming(NamingStrategy.underline_to_camel); - strategy.setSuperEntityClass("com.admin.entity.BaseEntity"); - strategy.setEntityLombokModel(true); - strategy.setRestControllerStyle(true); - // 公共父类 - strategy.setSuperControllerClass("com.admin.controller.BaseController"); - strategy.setSuperEntityColumns("id", "created_time", "updated_time", "status"); - strategy.setInclude(scanner("表名,多个英文逗号分割").split(",")); - strategy.setControllerMappingHyphenStyle(true); -// strategy.setTablePrefix("sys_");//动态调整 - mpg.setStrategy(strategy); - mpg.setTemplateEngine(new FreemarkerTemplateEngine()); - mpg.execute(); - } - -} \ No newline at end of file diff --git a/springboot-backend/src/main/java/com/admin/common/dto/ChainNodesItems.java b/springboot-backend/src/main/java/com/admin/common/dto/ChainNodesItems.java new file mode 100644 index 0000000..1c84f1c --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/common/dto/ChainNodesItems.java @@ -0,0 +1,15 @@ +package com.admin.common.dto; + + +import lombok.Data; + +import java.util.List; + +@Data +public class ChainNodesItems { + + private String mode; + + private List nodeIds; + +} diff --git a/springboot-backend/src/main/java/com/admin/common/dto/DiagnosisResult.java b/springboot-backend/src/main/java/com/admin/common/dto/DiagnosisResult.java new file mode 100644 index 0000000..1ef2184 --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/common/dto/DiagnosisResult.java @@ -0,0 +1,21 @@ +package com.admin.common.dto; + +import lombok.Data; + +@Data +public class DiagnosisResult { + private Long nodeId; + private String nodeName; + private String targetIp; + private Integer targetPort; + private String description; + private boolean success; + private String message; + private double averageTime; + private double packetLoss; + private long timestamp; + private Integer fromChainType; + private Integer fromInx; + private Integer toChainType; + private Integer toInx; +} diff --git a/springboot-backend/src/main/java/com/admin/common/dto/ForwardDto.java b/springboot-backend/src/main/java/com/admin/common/dto/ForwardDto.java index 4318a7f..442caa0 100644 --- a/springboot-backend/src/main/java/com/admin/common/dto/ForwardDto.java +++ b/springboot-backend/src/main/java/com/admin/common/dto/ForwardDto.java @@ -21,14 +21,4 @@ public class ForwardDto { private String remoteAddr; private String strategy; - - /** - * 入口端口(可选,为空时自动分配) - */ - @Min(value = 1, message = "端口号不能小于1") - @Max(value = 65535, message = "端口号不能大于65535") - private Integer inPort; - - private String interfaceName; - } \ No newline at end of file diff --git a/springboot-backend/src/main/java/com/admin/common/dto/ForwardPortDto.java b/springboot-backend/src/main/java/com/admin/common/dto/ForwardPortDto.java new file mode 100644 index 0000000..c49ea28 --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/common/dto/ForwardPortDto.java @@ -0,0 +1,10 @@ +package com.admin.common.dto; + + +import lombok.Data; + +@Data +public class ForwardPortDto { + private Integer node_id; + private Integer port; +} diff --git a/springboot-backend/src/main/java/com/admin/common/dto/ForwardUpdateDto.java b/springboot-backend/src/main/java/com/admin/common/dto/ForwardUpdateDto.java index 0b9361d..5b17120 100644 --- a/springboot-backend/src/main/java/com/admin/common/dto/ForwardUpdateDto.java +++ b/springboot-backend/src/main/java/com/admin/common/dto/ForwardUpdateDto.java @@ -19,22 +19,10 @@ public class ForwardUpdateDto { @NotBlank(message = "转发名称不能为空") private String name; - - @NotNull(message = "隧道ID不能为空") - private Integer tunnelId; - + @NotBlank(message = "远程地址不能为空") private String remoteAddr; private String strategy; - - /** - * 入口端口(可选,为空时自动分配) - */ - @Min(value = 1, message = "端口号不能小于1") - @Max(value = 65535, message = "端口号不能大于65535") - private Integer inPort; - @TableField(updateStrategy = FieldStrategy.IGNORED) - private String interfaceName; } \ No newline at end of file diff --git a/springboot-backend/src/main/java/com/admin/common/dto/ForwardWithTunnelDto.java b/springboot-backend/src/main/java/com/admin/common/dto/ForwardWithTunnelDto.java index 84874a2..c6e8438 100644 --- a/springboot-backend/src/main/java/com/admin/common/dto/ForwardWithTunnelDto.java +++ b/springboot-backend/src/main/java/com/admin/common/dto/ForwardWithTunnelDto.java @@ -25,6 +25,11 @@ public class ForwardWithTunnelDto { private String name; + /** + * 入口IP + */ + private String inIp; + /** * 入口端口 */ @@ -56,15 +61,10 @@ public class ForwardWithTunnelDto { * 隧道名称 */ private String tunnelName; - - /** - * 入口IP - */ - private String inIp; + private String userName; - /** * 用户ID */ @@ -87,6 +87,4 @@ public class ForwardWithTunnelDto { private String strategy; private Integer inx; - - private String interfaceName; } \ No newline at end of file diff --git a/springboot-backend/src/main/java/com/admin/common/dto/NodeDto.java b/springboot-backend/src/main/java/com/admin/common/dto/NodeDto.java index 961c71b..9e5a995 100644 --- a/springboot-backend/src/main/java/com/admin/common/dto/NodeDto.java +++ b/springboot-backend/src/main/java/com/admin/common/dto/NodeDto.java @@ -13,19 +13,16 @@ public class NodeDto { @NotBlank(message = "节点名称不能为空") private String name; - @NotBlank(message = "入口IP不能为空") - private String ip; - @NotBlank(message = "服务器ip不能为空") private String serverIp; - @NotNull(message = "起始端口不能为空") - @Min(value = 1, message = "起始端口必须大于0") - @Max(value = 65535, message = "起始端口不能超过65535") - private Integer portSta; + @NotBlank(message = "可用端口不能为空") + private String port; + + private String interfaceName; + + private String tcpListenAddr = "0.0.0.0"; + + private String udpListenAddr = "0.0.0.0"; - @NotNull(message = "结束端口不能为空") - @Min(value = 1, message = "结束端口必须大于0") - @Max(value = 65535, message = "结束端口不能超过65535") - private Integer portEnd; } \ No newline at end of file diff --git a/springboot-backend/src/main/java/com/admin/common/dto/NodeUpdateDto.java b/springboot-backend/src/main/java/com/admin/common/dto/NodeUpdateDto.java index 1d205a9..a26436e 100644 --- a/springboot-backend/src/main/java/com/admin/common/dto/NodeUpdateDto.java +++ b/springboot-backend/src/main/java/com/admin/common/dto/NodeUpdateDto.java @@ -4,8 +4,6 @@ import lombok.Data; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; -import javax.validation.constraints.Max; -import javax.validation.constraints.Min; @Data public class NodeUpdateDto { @@ -16,23 +14,18 @@ public class NodeUpdateDto { @NotBlank(message = "节点名称不能为空") private String name; - @NotBlank(message = "入口IP不能为空") - private String ip; - @NotBlank(message = "服务器ip不能为空") private String serverIp; - @NotNull(message = "起始端口不能为空") - @Min(value = 1, message = "起始端口必须大于0") - @Max(value = 65535, message = "起始端口不能超过65535") - private Integer portSta; - - @NotNull(message = "结束端口不能为空") - @Min(value = 1, message = "结束端口必须大于0") - @Max(value = 65535, message = "结束端口不能超过65535") - private Integer portEnd; + @NotBlank(message = "可用port不能为空") + private String port; + private String interfaceName; private Integer http; private Integer tls; private Integer socks; + + private String tcpListenAddr = "0.0.0.0"; + + private String udpListenAddr = "0.0.0.0"; } \ No newline at end of file diff --git a/springboot-backend/src/main/java/com/admin/common/dto/SpeedLimitUpdateDto.java b/springboot-backend/src/main/java/com/admin/common/dto/SpeedLimitUpdateDto.java index 97ba200..3cbb939 100644 --- a/springboot-backend/src/main/java/com/admin/common/dto/SpeedLimitUpdateDto.java +++ b/springboot-backend/src/main/java/com/admin/common/dto/SpeedLimitUpdateDto.java @@ -17,10 +17,4 @@ public class SpeedLimitUpdateDto { @NotNull(message = "速度限制不能为空") @Min(value = 1, message = "速度限制必须大于0") private Integer speed; - - @NotNull(message = "隧道ID不能为空") - private Long tunnelId; - - @NotBlank(message = "隧道名称不能为空") - private String tunnelName; } \ No newline at end of file diff --git a/springboot-backend/src/main/java/com/admin/common/dto/TunnelDetailDto.java b/springboot-backend/src/main/java/com/admin/common/dto/TunnelDetailDto.java new file mode 100644 index 0000000..b501654 --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/common/dto/TunnelDetailDto.java @@ -0,0 +1,43 @@ +package com.admin.common.dto; + +import com.admin.entity.ChainTunnel; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +/** + * 隧道详情DTO - 包含节点配置信息 + */ +@Data +public class TunnelDetailDto { + + private Long id; + + private String name; + + private Integer type; // 1: 端口转发, 2: 隧道转发 + + private Integer flow; // 1: 单向, 2: 双向 + + private BigDecimal trafficRatio; + + private Integer status; + + private Long createdTime; + + private Long updatedTime; + + private String inIp; + + // 入口节点列表 + private List inNodeId = new ArrayList<>(); + + // 转发链节点列表(二维数组结构) + private List> chainNodes = new ArrayList<>(); + + // 出口节点列表 + private List outNodeId = new ArrayList<>(); +} + diff --git a/springboot-backend/src/main/java/com/admin/common/dto/TunnelDto.java b/springboot-backend/src/main/java/com/admin/common/dto/TunnelDto.java index 69b52f6..45fb75c 100644 --- a/springboot-backend/src/main/java/com/admin/common/dto/TunnelDto.java +++ b/springboot-backend/src/main/java/com/admin/common/dto/TunnelDto.java @@ -1,45 +1,36 @@ package com.admin.common.dto; +import com.admin.entity.ChainTunnel; import lombok.Data; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; -import javax.validation.constraints.Min; -import javax.validation.constraints.Max; import javax.validation.constraints.DecimalMin; import javax.validation.constraints.DecimalMax; import java.math.BigDecimal; +import java.util.List; @Data public class TunnelDto { @NotBlank(message = "隧道名称不能为空") private String name; - - @NotNull(message = "入口节点不能为空") - private Long inNodeId; - // 出口节点ID,当type=1时可以为空,会自动设置为入口节点ID - private Long outNodeId; - + @NotNull(message = "入口节点不能为空") + private List inNodeId; + + private List> chainNodes; + + private List outNodeId; + + private String inIp; + @NotNull(message = "隧道类型不能为空") private Integer type; @NotNull(message = "流量计算类型不能为空") private Integer flow; - // 流量倍率,默认为1.0 @DecimalMin(value = "0.0", inclusive = false, message = "流量倍率必须大于0.0") @DecimalMax(value = "100.0", message = "流量倍率不能大于100.0") private BigDecimal trafficRatio; - - private String interfaceName; - - // 协议类型,默认为tls - private String protocol; - - // TCP监听地址,默认为0.0.0.0 - private String tcpListenAddr = "0.0.0.0"; - - // UDP监听地址,默认为0.0.0.0 - private String udpListenAddr = "0.0.0.0"; } \ No newline at end of file diff --git a/springboot-backend/src/main/java/com/admin/common/dto/TunnelListDto.java b/springboot-backend/src/main/java/com/admin/common/dto/TunnelListDto.java deleted file mode 100644 index fdf79be..0000000 --- a/springboot-backend/src/main/java/com/admin/common/dto/TunnelListDto.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.admin.common.dto; - -import lombok.Data; - -@Data -public class TunnelListDto { - - private Integer id; - - private String name; - - /** - * 入口IP - */ - private String ip; - - /** - * 入口节点端口起始范围 - */ - private Integer inNodePortSta; - - /** - * 入口节点端口结束范围 - */ - private Integer inNodePortEnd; - - /** - * 隧道类型(1-端口转发,2-隧道转发) - */ - private Integer type; - - /** - * 协议类型 - */ - private String protocol; -} diff --git a/springboot-backend/src/main/java/com/admin/common/dto/TunnelUpdateDto.java b/springboot-backend/src/main/java/com/admin/common/dto/TunnelUpdateDto.java index 9d746ba..99181bd 100644 --- a/springboot-backend/src/main/java/com/admin/common/dto/TunnelUpdateDto.java +++ b/springboot-backend/src/main/java/com/admin/common/dto/TunnelUpdateDto.java @@ -1,5 +1,6 @@ package com.admin.common.dto; +import com.admin.entity.ChainTunnel; import com.baomidou.mybatisplus.annotation.FieldStrategy; import com.baomidou.mybatisplus.annotation.TableField; import lombok.Data; @@ -10,35 +11,23 @@ import javax.validation.constraints.Max; import javax.validation.constraints.DecimalMin; import javax.validation.constraints.DecimalMax; import java.math.BigDecimal; +import java.util.List; @Data public class TunnelUpdateDto { - + @NotNull(message = "隧道ID不能为空") private Long id; - + @NotBlank(message = "隧道名称不能为空") private String name; - + @NotNull(message = "流量计算类型不能为空") private Integer flow; - - // 流量倍率 + + private String inIp; + @DecimalMin(value = "0.0", inclusive = false, message = "流量倍率必须大于0.0") @DecimalMax(value = "100.0", message = "流量倍率不能大于100.0") private BigDecimal trafficRatio; - - @NotBlank - private String protocol; - - // TCP监听地址 - @NotBlank - private String tcpListenAddr; - - // UDP监听地址 - @NotBlank - private String udpListenAddr; - - @TableField(updateStrategy = FieldStrategy.IGNORED) - private String interfaceName; -} \ No newline at end of file +} \ No newline at end of file diff --git a/springboot-backend/src/main/java/com/admin/common/dto/UserInfo.java b/springboot-backend/src/main/java/com/admin/common/dto/UserInfo.java new file mode 100644 index 0000000..f3c7241 --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/common/dto/UserInfo.java @@ -0,0 +1,10 @@ +package com.admin.common.dto; + +import lombok.Data; + +@Data +public class UserInfo { + private final Integer userId; + private final Integer roleId; + private final String userName; +} diff --git a/springboot-backend/src/main/java/com/admin/common/dto/UserPermissionResult.java b/springboot-backend/src/main/java/com/admin/common/dto/UserPermissionResult.java new file mode 100644 index 0000000..4e51daa --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/common/dto/UserPermissionResult.java @@ -0,0 +1,26 @@ +package com.admin.common.dto; + +import com.admin.entity.UserTunnel; +import com.admin.service.impl.ForwardServiceImpl; +import lombok.Data; + +@Data +public class UserPermissionResult { + public boolean hasError; + private String errorMessage; + private Integer limiter; + private UserTunnel userTunnel; + + public static UserPermissionResult success(Integer limiter, UserTunnel userTunnel) { + UserPermissionResult result = new UserPermissionResult(); + result.setLimiter(limiter); + result.setUserTunnel(userTunnel); + return result; + } + + public static UserPermissionResult error(String errorMessage) { + UserPermissionResult result = new UserPermissionResult(); + result.setErrorMessage(errorMessage); + return result; + } +} diff --git a/springboot-backend/src/main/java/com/admin/common/task/CheckGostConfigAsync.java b/springboot-backend/src/main/java/com/admin/common/task/CheckGostConfigAsync.java index df5b9ed..d07381b 100644 --- a/springboot-backend/src/main/java/com/admin/common/task/CheckGostConfigAsync.java +++ b/springboot-backend/src/main/java/com/admin/common/task/CheckGostConfigAsync.java @@ -5,8 +5,7 @@ import com.admin.common.lang.R; import com.admin.common.utils.GostUtil; import com.admin.entity.*; import com.admin.service.*; -import com.alibaba.fastjson.JSONObject; -import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.alibaba.fastjson.JSONArray; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Lazy; import org.springframework.scheduling.annotation.Async; @@ -14,6 +13,7 @@ import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Objects; @@ -33,8 +33,7 @@ public class CheckGostConfigAsync { private SpeedLimitService speedLimitService; @Resource - @Lazy - private TunnelService tunnelService; + TunnelService tunnelService; @@ -43,56 +42,51 @@ public class CheckGostConfigAsync { */ @Async public void cleanNodeConfigs(String node_id, GostConfigDto gostConfig) { - System.out.println(JSONObject.toJSONString(gostConfig)); Node node = nodeService.getById(node_id); if (node != null) { - cleanOrphanedServices(gostConfig, node); - cleanOrphanedChains(gostConfig, node); - cleanOrphanedLimiters(gostConfig, node); + cleanOrphanedServices(gostConfig.getServices(), node); + cleanOrphanedChains(gostConfig.getChains(), node); + cleanOrphanedLimiters(gostConfig.getLimiters(), node); } } /** * 清理孤立的服务 */ - private void cleanOrphanedServices(GostConfigDto gostConfig, Node node) { - if (gostConfig.getServices() == null) { - return; - } - - for (ConfigItem service : gostConfig.getServices()) { + private void cleanOrphanedServices(List configItems, Node node) { + if (configItems == null) return; + for (ConfigItem service : configItems) { safeExecute(() -> { if (!Objects.equals(service.getName(), "web_api")){ - String[] serviceIds = parseServiceName(service.getName()); - if (serviceIds.length == 4) { - String forwardId = serviceIds[0]; - String userId = serviceIds[1]; - String userTunnelId = serviceIds[2]; - String type = serviceIds[3]; + List serviceIds = parseServiceName(service.getName()); - if (Objects.equals(type, "tcp")) { // 只处理TCP,避免重复处理 - Forward forward = forwardService.getById(forwardId); - if (forward == null) { - log.info("删除孤立的服务: {} (节点: {})", service.getName(), node.getId()); - GostDto gostDto = GostUtil.DeleteService(node.getId(), forwardId + "_" + userId + "_" + userTunnelId); - System.out.println(gostDto); - } - } + JSONArray services = new JSONArray(); + if (Objects.equals(serviceIds.getLast(), "tls")){ + String forward_id = serviceIds.getFirst(); + services.add(forward_id + "_tls"); - - if (Objects.equals(type, "tls")) { - Forward forward = forwardService.getById(forwardId); - if (forward == null) { - log.info("删除孤立的服务: {} (节点: {})", service.getName(), node.getId()); - GostUtil.DeleteRemoteService(node.getId(), forwardId+"_"+userId+"_"+userTunnelId); - } + Tunnel tunnel = tunnelService.getById(forward_id); + if (tunnel == null) { + GostUtil.DeleteService(node.getId(), services); + log.info("删除孤立的服务: {} (节点: {})", service.getName(), node.getId()); } } + + if (Objects.equals(serviceIds.getLast(), "tcp")){ + String forward_id = serviceIds.getFirst(); + services.add(forward_id + "_" + serviceIds.get(1) + "_" + serviceIds.get(2) + "_tcp"); + services.add(forward_id + "_" + serviceIds.get(1) + "_" + serviceIds.get(2) + "_udp"); + + Forward forward = forwardService.getById(forward_id); + if (forward == null) { + GostUtil.DeleteService(node.getId(), services); + log.info("删除孤立的服务: {} (节点: {})", service.getName(), node.getId()); + } + } + } - - }, "清理服务 " + service.getName()); } @@ -101,28 +95,15 @@ public class CheckGostConfigAsync { /** * 清理孤立的链 */ - private void cleanOrphanedChains(GostConfigDto gostConfig, Node node) { - if (gostConfig.getChains() == null) { - return; - } - - - for (ConfigItem chain : gostConfig.getChains()) { + private void cleanOrphanedChains(List configItems, Node node) { + if (configItems == null) return; + for (ConfigItem chain : configItems) { safeExecute(() -> { - String[] serviceIds = parseServiceName(chain.getName()); - if (serviceIds.length == 4) { - String forwardId = serviceIds[0]; - String userId = serviceIds[1]; - String userTunnelId = serviceIds[2]; - String type = serviceIds[3]; - - if (Objects.equals(type, "chains")) { - Forward forward = forwardService.getById(forwardId); - if (forward == null) { - log.info("删除孤立的链: {} (节点: {})", chain.getName(), node.getId()); - GostUtil.DeleteChains(node.getId(), forwardId+"_"+userId+"_"+userTunnelId); - } - } + List serviceIds = parseServiceName(chain.getName()); + Tunnel tunnel = tunnelService.getById(serviceIds.getLast()); + if (tunnel == null) { + GostUtil.DeleteChains(node.getId(), chain.getName()); + log.info("删除孤立的链: {} (节点: {})", chain.getName(), node.getId()); } }, "清理链 " + chain.getName()); } @@ -131,69 +112,21 @@ public class CheckGostConfigAsync { /** * 清理孤立的限流器 */ - private void cleanOrphanedLimiters(GostConfigDto gostConfig, Node node) { - if (gostConfig.getLimiters() == null) { - return; - } + private void cleanOrphanedLimiters(List configItems, Node node) { + if (configItems == null) return; - for (ConfigItem limiter : gostConfig.getLimiters()) { + for (ConfigItem limiter : configItems) { safeExecute(() -> { SpeedLimit speedLimit = speedLimitService.getById(limiter.getName()); if (speedLimit == null) { - log.info("删除孤立的限流器: {} (节点: {})", limiter.getName(), node.getId()); GostUtil.DeleteLimiters(node.getId(), Long.parseLong(limiter.getName())); + log.info("删除孤立的限流器: {} (节点: {})", limiter.getName(), node.getId()); } }, "清理限流器 " + limiter.getName()); } } - /** - * 同步限流器 - */ - private void syncLimiters(GostConfigDto gostConfig, Node node) { - List tunnelList = tunnelService.list(new QueryWrapper().eq("in_node_id", node.getId())); - if (tunnelList == null || tunnelList.isEmpty()) return; - safeExecute(() -> { - StringBuilder tunnelIds = new StringBuilder(); - for (Tunnel tunnel : tunnelList) { - tunnelIds.append(tunnel.getId()).append(","); - } - String ids = tunnelIds.deleteCharAt(tunnelIds.length() - 1).toString(); - List speedLimits = speedLimitService.list(new QueryWrapper().in("tunnel_id", ids)); - if (speedLimits != null && !speedLimits.isEmpty()) { - List limiters = gostConfig.getLimiters(); - List limiters_ids = new ArrayList<>(); - List speedLimits_ids = new ArrayList<>(); - if (limiters != null){ - for (ConfigItem limiter : limiters) { - limiters_ids.add(Long.valueOf(limiter.getName())); - } - } - for (SpeedLimit speedLimit : speedLimits) { - speedLimits_ids.add(speedLimit.getId()); - } - List diff = new ArrayList<>(speedLimits_ids); - diff.removeAll(limiters_ids); - System.out.println(diff); - if (!diff.isEmpty()) { - - for (Long speed_id : diff) { - SpeedLimit speedLimit = speedLimitService.getById(speed_id); - if (speedLimit != null) { - SpeedLimitUpdateDto speedLimitUpdateDto = new SpeedLimitUpdateDto(); - speedLimitUpdateDto.setId(speed_id); - speedLimitUpdateDto.setName(speedLimit.getName()); - speedLimitUpdateDto.setSpeed(speedLimit.getSpeed()); - speedLimitUpdateDto.setTunnelId(speedLimit.getTunnelId()); - speedLimitUpdateDto.setTunnelName(speedLimit.getTunnelName()); - speedLimitService.updateSpeedLimit(speedLimitUpdateDto); - } - } - } - } - }, "同步限流器 "); - } /** * 安全执行操作,捕获异常 @@ -210,7 +143,8 @@ public class CheckGostConfigAsync { /** * 解析服务名称 */ - private String[] parseServiceName(String serviceName) { - return serviceName.split("_"); + private List parseServiceName(String serviceName) { + String[] split = serviceName.split("_"); + return new ArrayList<>(Arrays.asList(split)); } } diff --git a/springboot-backend/src/main/java/com/admin/common/task/ResetFlowAsync.java b/springboot-backend/src/main/java/com/admin/common/task/ResetFlowAsync.java index 6b05819..6f5d305 100644 --- a/springboot-backend/src/main/java/com/admin/common/task/ResetFlowAsync.java +++ b/springboot-backend/src/main/java/com/admin/common/task/ResetFlowAsync.java @@ -1,14 +1,8 @@ package com.admin.common.task; import com.admin.common.utils.GostUtil; -import com.admin.entity.Forward; -import com.admin.entity.Tunnel; -import com.admin.entity.User; -import com.admin.entity.UserTunnel; -import com.admin.service.ForwardService; -import com.admin.service.TunnelService; -import com.admin.service.UserService; -import com.admin.service.UserTunnelService; +import com.admin.entity.*; +import com.admin.service.*; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import lombok.extern.slf4j.Slf4j; @@ -38,6 +32,9 @@ public class ResetFlowAsync { @Resource TunnelService tunnelService; + @Resource + ChainTunnelService chainTunnelService; + /** * 每天0点执行流量重置任务 * 查询出用户和隧道的重置流量日期是今天的数据,将上下流量重置为0 @@ -197,12 +194,18 @@ public class ResetFlowAsync { // 查询对应转发 List forwardList = forwardService.list(new QueryWrapper().eq("user_id", user.getId()).eq("status", 1)); for (Forward forward : forwardList) { - UserTunnel userTunnel = userTunnelService.getOne(new QueryWrapper().eq("user_id", forward.getUserId()).eq("tunnel_id", forward.getTunnelId())); - if (userTunnel != null) { - pauseForwardService(forward, userTunnel.getId()); - forward.setStatus(0); - forwardService.updateById(forward); + + List chainTunnels = chainTunnelService.list(new QueryWrapper().eq("tunnel_id", forward.getTunnelId()).eq("chain_type", 1)); + for (ChainTunnel chainTunnel : chainTunnels) { + UserTunnel userTunnel = userTunnelService.getOne(new QueryWrapper().eq("user_id", forward.getUserId()).eq("tunnel_id", forward.getTunnelId())); + if (userTunnel != null) { + String name = buildServiceName(forward.getId(), forward.getUserId(), userTunnel.getId()); + GostUtil.PauseAndResumeService(chainTunnel.getNodeId(), name, "PauseService"); + } } + + forward.setStatus(0); + forwardService.updateById(forward); } user.setStatus(0); userService.updateById(user); @@ -217,25 +220,23 @@ public class ResetFlowAsync { for (UserTunnel userTunnel : user_tunnel_list) { List forwardList = forwardService.list(new QueryWrapper().eq("tunnel_id", userTunnel.getTunnelId()).eq("user_id", userTunnel.getUserId()).eq("status", 1)); for (Forward forward : forwardList) { - pauseForwardService(forward, userTunnel.getId()); + + List chainTunnels = chainTunnelService.list(new QueryWrapper().eq("tunnel_id", forward.getTunnelId()).eq("chain_type", 1)); + for (ChainTunnel chainTunnel : chainTunnels) { + String name = buildServiceName(forward.getId(), forward.getUserId(), userTunnel.getId()); + GostUtil.PauseAndResumeService(chainTunnel.getNodeId(), name, "PauseService"); + } forward.setStatus(0); forwardService.updateById(forward); } + + userTunnel.setStatus(0); userTunnelService.updateById(userTunnel); } } - private void pauseForwardService(Forward forward, Integer userTunnelId) { - Tunnel tunnel = tunnelService.getById(forward.getTunnelId()); - if (tunnel == null) return; - - GostUtil.PauseService(tunnel.getInNodeId(), buildServiceName(forward.getId(), forward.getUserId(), userTunnelId)); - if (tunnel.getType() == 2){ - GostUtil.PauseRemoteService(tunnel.getOutNodeId(), buildServiceName(forward.getId(), forward.getUserId(), userTunnelId)); - } - } private String buildServiceName(Long forwardId, Integer userId, Integer userTunnelId) { diff --git a/springboot-backend/src/main/java/com/admin/common/utils/AESCrypto.java b/springboot-backend/src/main/java/com/admin/common/utils/AESCrypto.java index 4b51b5c..a6ba337 100644 --- a/springboot-backend/src/main/java/com/admin/common/utils/AESCrypto.java +++ b/springboot-backend/src/main/java/com/admin/common/utils/AESCrypto.java @@ -42,7 +42,6 @@ public class AESCrypto { this.secretKey = new SecretKeySpec(keyBytes, ALGORITHM); this.secureRandom = new SecureRandom(); - log.info("AES加密器初始化成功"); } catch (Exception e) { log.info("AES加密器初始化失败", e); throw new RuntimeException("AES加密器初始化失败: " + e.getMessage(), e); diff --git a/springboot-backend/src/main/java/com/admin/common/utils/GostUtil.java b/springboot-backend/src/main/java/com/admin/common/utils/GostUtil.java index d28f14a..b3a7093 100644 --- a/springboot-backend/src/main/java/com/admin/common/utils/GostUtil.java +++ b/springboot-backend/src/main/java/com/admin/common/utils/GostUtil.java @@ -1,294 +1,179 @@ package com.admin.common.utils; -import com.admin.common.dto.GostConfigDto; -import com.admin.common.dto.GostDto; -import com.admin.entity.Tunnel; +import com.admin.entity.*; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import org.apache.commons.lang3.StringUtils; -import org.aspectj.apache.bcel.generic.RET; +import java.util.List; +import java.util.Map; import java.util.Objects; public class GostUtil { - public static GostDto AddLimiters(Long node_id, Long name, String speed) { + public static void AddLimiters(Long node_id, Long name, String speed) { JSONObject data = createLimiterData(name, speed); - return WebSocketServer.send_msg(node_id, data, "AddLimiters"); + WebSocketServer.send_msg(node_id, data, "AddLimiters"); } - public static GostDto UpdateLimiters(Long node_id, Long name, String speed) { + public static void UpdateLimiters(Long node_id, Long name, String speed) { JSONObject data = createLimiterData(name, speed); JSONObject req = new JSONObject(); req.put("limiter", name + ""); req.put("data", data); - return WebSocketServer.send_msg(node_id, req, "UpdateLimiters"); + WebSocketServer.send_msg(node_id, req, "UpdateLimiters"); } - public static GostDto DeleteLimiters(Long node_id, Long name) { + public static void DeleteLimiters(Long node_id, Long name) { JSONObject req = new JSONObject(); req.put("limiter", name + ""); - return WebSocketServer.send_msg(node_id, req, "DeleteLimiters"); + WebSocketServer.send_msg(node_id, req, "DeleteLimiters"); } - public static GostDto AddService(Long node_id, String name, Integer in_port, Integer limiter, String remoteAddr, Integer fow_type, Tunnel tunnel, String strategy, String interfaceName) { - JSONArray services = new JSONArray(); - String[] protocols = {"tcp", "udp"}; - for (String protocol : protocols) { - JSONObject service = createServiceConfig(name, in_port, limiter, remoteAddr, protocol, fow_type, tunnel, strategy, interfaceName); - services.add(service); - } - return WebSocketServer.send_msg(node_id, services, "AddService"); - } - - public static GostDto UpdateService(Long node_id, String name, Integer in_port, Integer limiter, String remoteAddr, Integer fow_type, Tunnel tunnel, String strategy, String interfaceName) { - JSONArray services = new JSONArray(); - String[] protocols = {"tcp", "udp"}; - for (String protocol : protocols) { - JSONObject service = createServiceConfig(name, in_port, limiter, remoteAddr, protocol, fow_type, tunnel, strategy, interfaceName); - services.add(service); - } - return WebSocketServer.send_msg(node_id, services, "UpdateService"); - } - - public static GostDto DeleteService(Long node_id, String name) { - JSONObject data = new JSONObject(); - JSONArray services = new JSONArray(); - services.add(name + "_tcp"); - services.add(name + "_udp"); - data.put("services", services); - return WebSocketServer.send_msg(node_id, data, "DeleteService"); - } - - public static GostDto AddRemoteService(Long node_id, String name, Integer out_port, String remoteAddr, String protocol, String strategy, String interfaceName) { - JSONObject data = new JSONObject(); - data.put("name", name + "_tls"); - data.put("addr", ":" + out_port); - - if (StringUtils.isNotBlank(interfaceName)) { - JSONObject metadata = new JSONObject(); - metadata.put("interface", interfaceName); - data.put("metadata", metadata); - } - - - JSONObject handler = new JSONObject(); - handler.put("type", "relay"); - data.put("handler", handler); - JSONObject listener = new JSONObject(); - listener.put("type", protocol); - data.put("listener", listener); - JSONObject forwarder = new JSONObject(); + public static void AddChains(Long node_id, List chainTunnels, Map node_s) { JSONArray nodes = new JSONArray(); + for (ChainTunnel chainTunnel : chainTunnels) { + JSONObject dialer = new JSONObject(); + dialer.put("type", chainTunnel.getProtocol()); - String[] split = remoteAddr.split(","); - int num = 1; - for (String addr : split) { + JSONObject connector = new JSONObject(); + connector.put("type", "relay"); + + Node node_info = node_s.get(chainTunnel.getNodeId()); JSONObject node = new JSONObject(); - node.put("name", "node_" + num ); - node.put("addr", addr); + node.put("name", "node_" + chainTunnel.getInx()); + node.put("addr", node_info.getServerIp() + ":" + chainTunnel.getPort()); + node.put("connector", connector); + node.put("dialer", dialer); + + if (StringUtils.isNotBlank(node_info.getInterfaceName())) { + node.put("interface", node_info.getInterfaceName()); + } + nodes.add(node); - num ++; } - if (strategy == null || strategy.equals("")){ - strategy = "fifo"; - } - forwarder.put("nodes", nodes); - JSONObject selector = new JSONObject(); - selector.put("strategy", strategy); - selector.put("maxFails", 1); - selector.put("failTimeout", "600s"); - forwarder.put("selector", selector); - - data.put("forwarder", forwarder); - JSONArray services = new JSONArray(); - services.add(data); - return WebSocketServer.send_msg(node_id, services, "AddService"); - } - - public static GostDto UpdateRemoteService(Long node_id, String name, Integer out_port, String remoteAddr,String protocol, String strategy, String interfaceName) { - JSONObject data = new JSONObject(); - data.put("name", name + "_tls"); - data.put("addr", ":" + out_port); - - if (StringUtils.isNotBlank(interfaceName)) { - JSONObject metadata = new JSONObject(); - metadata.put("interface", interfaceName); - data.put("metadata", metadata); - } - - - JSONObject handler = new JSONObject(); - handler.put("type", "relay"); - data.put("handler", handler); - JSONObject listener = new JSONObject(); - listener.put("type", protocol); - data.put("listener", listener); - JSONObject forwarder = new JSONObject(); - JSONArray nodes = new JSONArray(); - - String[] split = remoteAddr.split(","); - int num = 1; - for (String addr : split) { - JSONObject node = new JSONObject(); - node.put("name", "node_" + num ); - node.put("addr", addr); - nodes.add(node); - num ++; - } - if (strategy == null || strategy.equals("")){ - strategy = "fifo"; - } - forwarder.put("nodes", nodes); - JSONObject selector = new JSONObject(); - selector.put("strategy", strategy); - selector.put("maxFails", 1); - selector.put("failTimeout", "600s"); - forwarder.put("selector", selector); - - data.put("forwarder", forwarder); - JSONArray services = new JSONArray(); - services.add(data); - return WebSocketServer.send_msg(node_id, services, "UpdateService"); - } - - public static GostDto DeleteRemoteService(Long node_id, String name) { - JSONArray data = new JSONArray(); - data.add(name + "_tls"); - JSONObject req = new JSONObject(); - req.put("services", data); - return WebSocketServer.send_msg(node_id, req, "DeleteService"); - } - - public static GostDto PauseService(Long node_id, String name) { - JSONObject data = new JSONObject(); - JSONArray services = new JSONArray(); - services.add(name + "_tcp"); - services.add(name + "_udp"); - data.put("services", services); - return WebSocketServer.send_msg(node_id, data, "PauseService"); - } - - public static GostDto ResumeService(Long node_id, String name) { - JSONObject data = new JSONObject(); - JSONArray services = new JSONArray(); - services.add(name + "_tcp"); - services.add(name + "_udp"); - data.put("services", services); - return WebSocketServer.send_msg(node_id, data, "ResumeService"); - } - - public static GostDto PauseRemoteService(Long node_id, String name) { - JSONObject data = new JSONObject(); - JSONArray services = new JSONArray(); - services.add(name + "_tls"); - data.put("services", services); - return WebSocketServer.send_msg(node_id, data, "PauseService"); - } - - public static GostDto ResumeRemoteService(Long node_id, String name) { - JSONObject data = new JSONObject(); - JSONArray services = new JSONArray(); - services.add(name + "_tls"); - data.put("services", services); - return WebSocketServer.send_msg(node_id, data, "ResumeService"); - } - - public static GostDto AddChains(Long node_id, String name, String remoteAddr, String protocol, String interfaceName) { - JSONObject dialer = new JSONObject(); - dialer.put("type", protocol); - if (Objects.equals(protocol, "quic")){ - JSONObject metadata = new JSONObject(); - metadata.put("keepAlive", true); - metadata.put("ttl", "10s"); - dialer.put("metadata", metadata); - } - - - - - JSONObject connector = new JSONObject(); - connector.put("type", "relay"); - - JSONObject node = new JSONObject(); - node.put("name", "node-" + name); - node.put("addr", remoteAddr); - node.put("connector", connector); - node.put("dialer", dialer); - - if (StringUtils.isNotBlank(interfaceName)) { - node.put("interface", interfaceName); - } - - - JSONArray nodes = new JSONArray(); - nodes.add(node); - JSONObject hop = new JSONObject(); - hop.put("name", "hop-" + name); + hop.put("name", "hop_" + chainTunnels.getFirst().getTunnelId()); + + JSONObject selector = new JSONObject(); + selector.put("strategy", chainTunnels.getFirst().getStrategy()); + selector.put("maxFails", 1); + selector.put("failTimeout", 600000000000L); // 600 秒(纳秒单位) + + + hop.put("selector", selector); hop.put("nodes", nodes); JSONArray hops = new JSONArray(); hops.add(hop); JSONObject data = new JSONObject(); - data.put("name", name + "_chains"); + data.put("name", "chains_" + chainTunnels.getFirst().getTunnelId()); data.put("hops", hops); - return WebSocketServer.send_msg(node_id, data, "AddChains"); + WebSocketServer.send_msg(node_id, data, "AddChains"); } - public static GostDto UpdateChains(Long node_id, String name, String remoteAddr, String protocol, String interfaceName) { - JSONObject dialer = new JSONObject(); - dialer.put("type", protocol); + public static void DeleteChains(Long node_id, String name) { + JSONObject data = new JSONObject(); + data.put("chain", name); + WebSocketServer.send_msg(node_id, data, "DeleteChains"); + } - if (Objects.equals(protocol, "quic")){ + public static void AddChainService(Long node_id, ChainTunnel chainTunnel, Map node_s) { + JSONArray services = new JSONArray(); + Node node_info = node_s.get(chainTunnel.getNodeId()); + JSONObject service_item = new JSONObject(); + service_item.put("name", chainTunnel.getTunnelId() + "_tls"); + service_item.put("addr", node_info.getTcpListenAddr() + ":" + chainTunnel.getPort()); + if (StringUtils.isNotBlank(node_info.getInterfaceName())) { JSONObject metadata = new JSONObject(); - metadata.put("keepAlive", true); - metadata.put("ttl", "10s"); - dialer.put("metadata", metadata); + metadata.put("interface", node_info.getInterfaceName()); + service_item.put("metadata", metadata); } - - JSONObject connector = new JSONObject(); - connector.put("type", "relay"); - - JSONObject node = new JSONObject(); - node.put("name", "node-" + name); - node.put("addr", remoteAddr); - node.put("connector", connector); - node.put("dialer", dialer); - - if (StringUtils.isNotBlank(interfaceName)) { - node.put("interface", interfaceName); + JSONObject handler = new JSONObject(); + handler.put("type", "relay"); + if (chainTunnel.getChainType() == 2){ + handler.put("chain","chains_" + chainTunnel.getTunnelId()); } + service_item.put("handler", handler); - JSONArray nodes = new JSONArray(); - nodes.add(node); + JSONObject listener = new JSONObject(); + listener.put("type", chainTunnel.getProtocol()); + service_item.put("listener", listener); - JSONObject hop = new JSONObject(); - hop.put("name", "hop-" + name); - hop.put("nodes", nodes); + services.add(service_item); - JSONArray hops = new JSONArray(); - hops.add(hop); - - JSONObject data = new JSONObject(); - data.put("name", name + "_chains"); - data.put("hops", hops); - JSONObject req = new JSONObject(); - req.put("chain", name + "_chains"); - req.put("data", data); - return WebSocketServer.send_msg(node_id, req, "UpdateChains"); + WebSocketServer.send_msg(node_id, services, "AddService"); } - public static GostDto DeleteChains(Long node_id, String name) { + public static void DeleteChainService(Long node_id, JSONArray services) { JSONObject data = new JSONObject(); - data.put("chain", name + "_chains"); - return WebSocketServer.send_msg(node_id, data, "DeleteChains"); + data.put("services", services); + WebSocketServer.send_msg(node_id, data, "DeleteService"); } + public static void AddAndUpdateService(String name, Integer limiter, Node node, Forward forward, ForwardPort forwardPort, Tunnel tunnel, String meth) { + JSONArray services = new JSONArray(); + String[] protocols = {"tcp", "udp"}; + for (String protocol : protocols) { + JSONObject service = new JSONObject(); + service.put("name", name + "_" + protocol); + if (Objects.equals(protocol, "tcp")){ + service.put("addr", node.getTcpListenAddr() + ":" + forwardPort.getPort()); + }else { + service.put("addr", node.getUdpListenAddr() + ":" + forwardPort.getPort()); + } + + if (StringUtils.isNotBlank(node.getInterfaceName())) { + JSONObject metadata = new JSONObject(); + metadata.put("interface", node.getInterfaceName()); + service.put("metadata", metadata); + } + + // 添加限流器配置 + if (limiter != null) { + service.put("limiter", limiter.toString()); + } + + // 配置处理器 + JSONObject handler = new JSONObject(); + handler.put("type", protocol); + if (tunnel.getType() == 2){ + handler.put("chain", "chains_" + forward.getTunnelId()); + } + service.put("handler", handler); + + // 配置监听器 + JSONObject listener = createListener(protocol); + service.put("listener", listener); + + JSONObject forwarder = createForwarder(forward.getRemoteAddr(), forward.getStrategy()); + service.put("forwarder", forwarder); + + services.add(service); + } + WebSocketServer.send_msg(node.getId(), services, meth); + } + + public static void DeleteService(Long node_id, JSONArray services) { + JSONObject data = new JSONObject(); + data.put("services", services); + WebSocketServer.send_msg(node_id, data, "DeleteService"); + } + + public static void PauseAndResumeService(Long node_id, String name, String meth) { + JSONObject data = new JSONObject(); + JSONArray services = new JSONArray(); + services.add(name + "_tcp"); + services.add(name + "_udp"); + data.put("services", services); + WebSocketServer.send_msg(node_id, data, meth); + } + + private static JSONObject createLimiterData(Long name, String speed) { JSONObject data = new JSONObject(); data.put("name", name.toString()); @@ -298,59 +183,10 @@ public class GostUtil { return data; } - private static JSONObject createServiceConfig(String name, Integer in_port, Integer limiter, String remoteAddr, String protocol, Integer fow_type, Tunnel tunnel, String strategy, String interfaceName) { - JSONObject service = new JSONObject(); - service.put("name", name + "_" + protocol); - if (Objects.equals(protocol, "tcp")){ - service.put("addr", tunnel.getTcpListenAddr() + ":" + in_port); - }else { - service.put("addr", tunnel.getUdpListenAddr() + ":" + in_port); - } - - if (StringUtils.isNotBlank(interfaceName)) { - JSONObject metadata = new JSONObject(); - metadata.put("interface", interfaceName); - service.put("metadata", metadata); - } - - - // 添加限流器配置 - if (limiter != null) { - service.put("limiter", limiter.toString()); - } - - // 配置处理器 - JSONObject handler = createHandler(protocol, name, fow_type); - service.put("handler", handler); - - // 配置监听器 - JSONObject listener = createListener(protocol); - service.put("listener", listener); - - // 端口转发需要配置转发器 - if (isPortForwarding(fow_type)) { - JSONObject forwarder = createForwarder(remoteAddr, strategy); - service.put("forwarder", forwarder); - } - return service; - } - - private static JSONObject createHandler(String protocol, String name, Integer fow_type) { - JSONObject handler = new JSONObject(); - handler.put("type", protocol); - - // 隧道转发需要添加链配置 - if (isTunnelForwarding(fow_type)) { - handler.put("chain", name + "_chains"); - } - - return handler; - } - private static JSONObject createListener(String protocol) { JSONObject listener = new JSONObject(); listener.put("type", protocol); - if (Objects.equals(protocol, "udp")){ + if (Objects.equals(protocol, "udp")) { JSONObject metadata = new JSONObject(); metadata.put("keepAlive", true); listener.put("metadata", metadata); @@ -366,13 +202,13 @@ public class GostUtil { int num = 1; for (String addr : split) { JSONObject node = new JSONObject(); - node.put("name", "node_" + num ); + node.put("name", "node_" + num); node.put("addr", addr); nodes.add(node); - num ++; + num++; } - if (strategy == null || strategy.equals("")){ + if (strategy == null || strategy.equals("")) { strategy = "fifo"; } @@ -386,12 +222,5 @@ public class GostUtil { return forwarder; } - private static boolean isPortForwarding(Integer fow_type) { - return fow_type != null && fow_type == 1; - } - - private static boolean isTunnelForwarding(Integer fow_type) { - return fow_type != null && fow_type != 1; - } } diff --git a/springboot-backend/src/main/java/com/admin/config/MybatisPlusConfig.java b/springboot-backend/src/main/java/com/admin/config/MybatisPlusConfig.java index 7e711e7..7ad53a5 100644 --- a/springboot-backend/src/main/java/com/admin/config/MybatisPlusConfig.java +++ b/springboot-backend/src/main/java/com/admin/config/MybatisPlusConfig.java @@ -1,5 +1,6 @@ package com.admin.config; +import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor; @@ -15,7 +16,9 @@ public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); - interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); // 分页插件 + // 配置分页插件,指定数据库类型为 SQLite + PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.SQLITE); + interceptor.addInnerInterceptor(paginationInterceptor); // 分页插件 interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); // 防止全表更新插件 return interceptor; } diff --git a/springboot-backend/src/main/java/com/admin/config/SQLiteConfig.java b/springboot-backend/src/main/java/com/admin/config/SQLiteConfig.java new file mode 100644 index 0000000..2d6cb0c --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/config/SQLiteConfig.java @@ -0,0 +1,42 @@ +package com.admin.config; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.stereotype.Component; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.Statement; + +/** + * SQLite 数据库配置 + * 启用 WAL (Write-Ahead Logging) 模式以提高并发性能 + */ +@Slf4j +@Component +public class SQLiteConfig implements ApplicationRunner { + + private final DataSource dataSource; + + public SQLiteConfig(DataSource dataSource) { + this.dataSource = dataSource; + } + + @Override + public void run(ApplicationArguments args) throws Exception { + try (Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement()) { + + statement.execute("PRAGMA journal_mode=WAL;"); + statement.execute("PRAGMA synchronous=NORMAL;"); + statement.execute("PRAGMA cache_size=-64000;"); // 64MB 缓存 + statement.execute("PRAGMA temp_store=MEMORY;"); + statement.execute("PRAGMA busy_timeout=5000;"); // 5秒超时 + } catch (Exception e) { + log.error("Failed to configure SQLite database", e); + throw e; + } + } +} + diff --git a/springboot-backend/src/main/java/com/admin/controller/FlowController.java b/springboot-backend/src/main/java/com/admin/controller/FlowController.java index 3e98024..854361e 100644 --- a/springboot-backend/src/main/java/com/admin/controller/FlowController.java +++ b/springboot-backend/src/main/java/com/admin/controller/FlowController.java @@ -3,12 +3,12 @@ package com.admin.controller; import com.admin.common.aop.LogAnnotation; import com.admin.common.dto.FlowDto; import com.admin.common.dto.GostConfigDto; -import com.admin.common.lang.R; import com.admin.common.task.CheckGostConfigAsync; import com.admin.common.utils.AESCrypto; import com.admin.common.utils.GostUtil; import com.admin.entity.*; import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; @@ -21,7 +21,6 @@ import java.util.Date; import java.util.List; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; /** * 流量上报控制器 @@ -147,15 +146,17 @@ public class FlowController extends BaseController { String decryptedData = decryptIfNeeded(rawData, secret); // 3. 解析为FlowDto列表 - FlowDto flowDataList = JSONObject.parseObject(decryptedData, FlowDto.class); - if (Objects.equals(flowDataList.getN(), "web_api")) { - return SUCCESS_RESPONSE; - } - - // 记录日志 + JSONArray flowDataList = JSONObject.parseArray(decryptedData); log.info("节点上报流量数据{}", flowDataList); - // 4. 处理流量数据 - return processFlowData(flowDataList); + for (int i = 0; i < flowDataList.size(); i++) { + String jsonObject = flowDataList.getJSONObject(i).toJSONString(); + FlowDto flowDto = JSONObject.parseObject(jsonObject, FlowDto.class); + if (!Objects.equals(flowDto.getN(), "web_api")) { + processFlowData(flowDto); + } + } + return SUCCESS_RESPONSE; + } /** @@ -212,24 +213,30 @@ public class FlowController extends BaseController { /** * 处理流量数据的核心逻辑 */ - private String processFlowData(FlowDto flowDataList) { + private void processFlowData(FlowDto flowDataList) { String[] serviceIds = parseServiceName(flowDataList.getN()); String forwardId = serviceIds[0]; String userId = serviceIds[1]; String userTunnelId = serviceIds[2]; Forward forward = forwardService.getById(forwardId); + if (forward != null){ + Tunnel tunnel = tunnelService.getById(forward.getTunnelId()); - // 获取流量计费类型 - int flowType = getFlowType(forward); - - // 处理流量倍率及单双向计算 - FlowDto flowStats = filterFlowData(flowDataList, forward, flowType); + // 处理流量倍率及单双向计算 + BigDecimal trafficRatio = tunnel.getTrafficRatio(); + BigDecimal originalD = BigDecimal.valueOf(flowDataList.getD()); + BigDecimal originalU = BigDecimal.valueOf(flowDataList.getU()); + BigDecimal newD = originalD.multiply(trafficRatio); + BigDecimal newU = originalU.multiply(trafficRatio); + flowDataList.setD(newD.longValue() * tunnel.getFlow()); + flowDataList.setU(newU.longValue() * tunnel.getFlow()); + } // 先更新所有流量统计 - 确保流量数据的一致性 - updateForwardFlow(forwardId, flowStats); - updateUserFlow(userId, flowStats); - updateUserTunnelFlow(userTunnelId, flowStats); + updateForwardFlow(forwardId, flowDataList); + updateUserFlow(userId, flowDataList); + updateUserTunnelFlow(userTunnelId, flowDataList); // 7. 检查和服务暂停操作 String name = buildServiceName(forwardId, userId, userTunnelId); @@ -238,7 +245,6 @@ public class FlowController extends BaseController { checkUserTunnelRelatedLimits(userTunnelId, name, userId); } - return SUCCESS_RESPONSE; } private void checkUserRelatedLimits(String userId, String name) { @@ -277,7 +283,7 @@ public class FlowController extends BaseController { UserTunnel userTunnel = userTunnelService.getById(userTunnelId); if (userTunnel == null) return; long flow = userTunnel.getInFlow() + userTunnel.getOutFlow(); - if (flow >= userTunnel.getFlow() * BYTES_TO_GB) { + if (flow >= userTunnel.getFlow() * BYTES_TO_GB) { pauseSpecificForward(userTunnel.getTunnelId(), name, userId); return; } @@ -300,44 +306,17 @@ public class FlowController extends BaseController { } public void pauseService(List forwardList, String name) { - for (Forward forward : forwardList) { - Tunnel tunnel = tunnelService.getById(forward.getTunnelId()); - if (tunnel != null){ - GostUtil.PauseService(tunnel.getInNodeId(), name); - if (tunnel.getType() == 2){ - GostUtil.PauseRemoteService(tunnel.getOutNodeId(), name); - } - } - forward.setStatus(0); - forwardService.updateById(forward); - } - } - - private FlowDto filterFlowData(FlowDto flowDto, Forward forward, int flowType) { - if (forward != null) { - Tunnel tunnel = tunnelService.getById(forward.getTunnelId()); - if (tunnel != null) { - BigDecimal trafficRatio = tunnel.getTrafficRatio(); - - BigDecimal originalD = BigDecimal.valueOf(flowDto.getD()); - BigDecimal originalU = BigDecimal.valueOf(flowDto.getU()); - - BigDecimal newD = originalD.multiply(trafficRatio); - BigDecimal newU = originalU.multiply(trafficRatio); - - flowDto.setD(newD.longValue() * flowType); - flowDto.setU(newU.longValue() * flowType); - } - } - return flowDto; - } - - private int getFlowType(Forward forward) { - int defaultFlowType = 2; - if (forward == null) return defaultFlowType; - Tunnel tunnel = tunnelService.getById(forward.getTunnelId()); - if (tunnel == null) return defaultFlowType; - return tunnel.getFlow(); +// for (Forward forward : forwardList) { +// Tunnel tunnel = tunnelService.getById(forward.getTunnelId()); +// if (tunnel != null) { +// GostUtil.PauseService(tunnel.getInNodeId(), name); +// if (tunnel.getType() == 2) { +// GostUtil.PauseRemoteService(tunnel.getOutNodeId(), name); +// } +// } +// forward.setStatus(0); +// forwardService.updateById(forward); +// } } private void updateForwardFlow(String forwardId, FlowDto flowStats) { diff --git a/springboot-backend/src/main/java/com/admin/entity/ChainTunnel.java b/springboot-backend/src/main/java/com/admin/entity/ChainTunnel.java new file mode 100644 index 0000000..3338ebb --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/entity/ChainTunnel.java @@ -0,0 +1,42 @@ +package com.admin.entity; + + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; + +@Data +@EqualsAndHashCode(callSuper = false) +public class ChainTunnel implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + // 关联的隧道ID + private Long tunnelId; + + // 记录所选节点的类型: 1是入口节点 2是转发链 3是出口节点 + private Integer chainType; + + // 记录所选节点的ID + private Long nodeId; + + // 记录节点使用的端口 入口没有次配置为null + private Integer port; + + // 负载模式 入口没有次配置为null + private String strategy; + + // 只有转发链有次参数 表示是第几条 + private Integer inx; + + // 使用的协议 入口没有次配置为null + private String protocol; + + +} diff --git a/springboot-backend/src/main/java/com/admin/entity/Forward.java b/springboot-backend/src/main/java/com/admin/entity/Forward.java index d249287..bf40fc5 100644 --- a/springboot-backend/src/main/java/com/admin/entity/Forward.java +++ b/springboot-backend/src/main/java/com/admin/entity/Forward.java @@ -1,6 +1,12 @@ package com.admin.entity; import java.io.Serializable; +import java.util.List; + +import com.admin.common.dto.ForwardPortDto; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import lombok.Data; import lombok.EqualsAndHashCode; @@ -14,6 +20,7 @@ import lombok.EqualsAndHashCode; */ @Data @EqualsAndHashCode(callSuper = false) +@TableName(autoResultMap = true) public class Forward extends BaseEntity{ private static final long serialVersionUID = 1L; @@ -26,14 +33,8 @@ public class Forward extends BaseEntity{ private Integer tunnelId; - private Integer inPort; - - private Integer outPort; - private String remoteAddr; - private String interfaceName; - private String strategy; private Long inFlow; @@ -41,5 +42,4 @@ public class Forward extends BaseEntity{ private Long outFlow; private Integer inx; - } diff --git a/springboot-backend/src/main/java/com/admin/entity/ForwardPort.java b/springboot-backend/src/main/java/com/admin/entity/ForwardPort.java new file mode 100644 index 0000000..84299ac --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/entity/ForwardPort.java @@ -0,0 +1,28 @@ +package com.admin.entity; + + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +@Data +@EqualsAndHashCode(callSuper = false) +@TableName(autoResultMap = true) +public class ForwardPort{ + + /** + * 主键ID + */ + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + private Long forwardId; + + private Long nodeId; + + private Integer port; +} diff --git a/springboot-backend/src/main/java/com/admin/entity/Node.java b/springboot-backend/src/main/java/com/admin/entity/Node.java index 307b136..b61d6ab 100644 --- a/springboot-backend/src/main/java/com/admin/entity/Node.java +++ b/springboot-backend/src/main/java/com/admin/entity/Node.java @@ -22,15 +22,11 @@ public class Node extends BaseEntity { private String secret; - private String ip; - private String serverIp; private String version; - private Integer portSta; - - private Integer portEnd; + private String port; private Integer http; @@ -38,4 +34,10 @@ public class Node extends BaseEntity { private Integer socks; + private String interfaceName; + + private String tcpListenAddr; + + private String udpListenAddr; + } diff --git a/springboot-backend/src/main/java/com/admin/entity/Tunnel.java b/springboot-backend/src/main/java/com/admin/entity/Tunnel.java index 7c2f5ee..1763224 100644 --- a/springboot-backend/src/main/java/com/admin/entity/Tunnel.java +++ b/springboot-backend/src/main/java/com/admin/entity/Tunnel.java @@ -2,6 +2,12 @@ package com.admin.entity; import java.io.Serializable; import java.math.BigDecimal; +import java.util.List; + +import com.admin.common.dto.ChainNodesItems; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import lombok.Data; import lombok.EqualsAndHashCode; @@ -15,59 +21,18 @@ import lombok.EqualsAndHashCode; */ @Data @EqualsAndHashCode(callSuper = true) +@TableName(autoResultMap = true) public class Tunnel extends BaseEntity { private static final long serialVersionUID = 1L; - /** - * 隧道名称 - */ private String name; - /** - * 入口节点ID - */ - private Long inNodeId; - - /** - * 入口IP (兼容字段) - */ - private String inIp; - - /** - * 出口节点ID - */ - private Long outNodeId; - - /** - * 出口IP (兼容字段) - */ - private String outIp; - - /** - * 隧道类型(1-端口转发,2-隧道转发) - */ private Integer type; - /** - * 流量计算类型(1 单向计算上传。2 双向) - */ private int flow; - /** - * 协议类型 - */ - private String protocol; - - /** - * 流量倍率 - */ private BigDecimal trafficRatio; - - private String tcpListenAddr; - - private String udpListenAddr; - - private String interfaceName; + private String inIp; } diff --git a/springboot-backend/src/main/java/com/admin/mapper/ChainTunnelMapper.java b/springboot-backend/src/main/java/com/admin/mapper/ChainTunnelMapper.java new file mode 100644 index 0000000..e380434 --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/mapper/ChainTunnelMapper.java @@ -0,0 +1,7 @@ +package com.admin.mapper; + +import com.admin.entity.ChainTunnel; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +public interface ChainTunnelMapper extends BaseMapper { +} diff --git a/springboot-backend/src/main/java/com/admin/mapper/ForwardMapper.java b/springboot-backend/src/main/java/com/admin/mapper/ForwardMapper.java index 6c79084..6afa058 100644 --- a/springboot-backend/src/main/java/com/admin/mapper/ForwardMapper.java +++ b/springboot-backend/src/main/java/com/admin/mapper/ForwardMapper.java @@ -4,6 +4,7 @@ import com.admin.entity.Forward; import com.admin.common.dto.ForwardWithTunnelDto; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; import java.util.List; @@ -30,4 +31,5 @@ public interface ForwardMapper extends BaseMapper { */ List selectForwardsWithTunnelByUserId(@Param("userId") Integer userId); + } diff --git a/springboot-backend/src/main/java/com/admin/mapper/ForwardPortMapper.java b/springboot-backend/src/main/java/com/admin/mapper/ForwardPortMapper.java new file mode 100644 index 0000000..1233e64 --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/mapper/ForwardPortMapper.java @@ -0,0 +1,8 @@ +package com.admin.mapper; + +import com.admin.entity.Forward; +import com.admin.entity.ForwardPort; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +public interface ForwardPortMapper extends BaseMapper { +} diff --git a/springboot-backend/src/main/java/com/admin/mapper/UserMapper.java b/springboot-backend/src/main/java/com/admin/mapper/UserMapper.java index bad2e3b..3ac3e9a 100644 --- a/springboot-backend/src/main/java/com/admin/mapper/UserMapper.java +++ b/springboot-backend/src/main/java/com/admin/mapper/UserMapper.java @@ -30,10 +30,5 @@ public interface UserMapper extends BaseMapper { * @return 转发列表 */ List getUserForwardDetails(@Param("userId") Integer userId); - - /** - * 管理员查询所有隧道(流量和转发设置为99999) - * @return 隧道列表 - */ - List getAllTunnelsForAdmin(); + } diff --git a/springboot-backend/src/main/java/com/admin/service/ChainTunnelService.java b/springboot-backend/src/main/java/com/admin/service/ChainTunnelService.java new file mode 100644 index 0000000..a6cbf97 --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/service/ChainTunnelService.java @@ -0,0 +1,8 @@ +package com.admin.service; + +import com.admin.entity.ChainTunnel; +import com.admin.entity.Forward; +import com.baomidou.mybatisplus.extension.service.IService; + +public interface ChainTunnelService extends IService { +} diff --git a/springboot-backend/src/main/java/com/admin/service/ForwardPortService.java b/springboot-backend/src/main/java/com/admin/service/ForwardPortService.java new file mode 100644 index 0000000..d1b6e51 --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/service/ForwardPortService.java @@ -0,0 +1,7 @@ +package com.admin.service; + +import com.admin.entity.ForwardPort; +import com.baomidou.mybatisplus.extension.service.IService; + +public interface ForwardPortService extends IService { +} diff --git a/springboot-backend/src/main/java/com/admin/service/ForwardService.java b/springboot-backend/src/main/java/com/admin/service/ForwardService.java index ee86b28..6e7ef1c 100644 --- a/springboot-backend/src/main/java/com/admin/service/ForwardService.java +++ b/springboot-backend/src/main/java/com/admin/service/ForwardService.java @@ -6,6 +6,7 @@ import com.admin.common.lang.R; import com.admin.entity.Forward; import com.baomidou.mybatisplus.extension.service.IService; import java.util.Map; +import java.util.Set; /** *

@@ -79,7 +80,4 @@ public interface ForwardService extends IService { * @return 更新结果 */ R updateForwardOrder(Map params); - - - void updateForwardA(Forward forward); } diff --git a/springboot-backend/src/main/java/com/admin/service/NodeService.java b/springboot-backend/src/main/java/com/admin/service/NodeService.java index 05e4489..0a70730 100644 --- a/springboot-backend/src/main/java/com/admin/service/NodeService.java +++ b/springboot-backend/src/main/java/com/admin/service/NodeService.java @@ -24,8 +24,6 @@ public interface NodeService extends IService { R deleteNode(Long id); - Node getNodeById(Long id); - R getInstallCommand(Long id); } diff --git a/springboot-backend/src/main/java/com/admin/service/TunnelService.java b/springboot-backend/src/main/java/com/admin/service/TunnelService.java index f949f57..800e399 100644 --- a/springboot-backend/src/main/java/com/admin/service/TunnelService.java +++ b/springboot-backend/src/main/java/com/admin/service/TunnelService.java @@ -56,4 +56,6 @@ public interface TunnelService extends IService { * @return 诊断结果 */ R diagnoseTunnel(Long tunnelId); + + Integer getNodePort(Long nodeId, Integer type); } diff --git a/springboot-backend/src/main/java/com/admin/service/impl/ChainTunnelServiceImpl.java b/springboot-backend/src/main/java/com/admin/service/impl/ChainTunnelServiceImpl.java new file mode 100644 index 0000000..97929e6 --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/service/impl/ChainTunnelServiceImpl.java @@ -0,0 +1,41 @@ +package com.admin.service.impl; + +import com.admin.common.dto.ForwardDto; +import com.admin.common.dto.ForwardUpdateDto; +import com.admin.common.dto.ForwardWithTunnelDto; +import com.admin.common.dto.GostDto; +import com.admin.common.lang.R; +import com.admin.common.utils.GostUtil; +import com.admin.common.utils.JwtUtil; +import com.admin.common.utils.WebSocketServer; +import com.admin.entity.*; +import com.admin.mapper.ChainTunnelMapper; +import com.admin.mapper.ForwardMapper; +import com.admin.service.*; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.alibaba.fastjson.JSONObject; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.*; +import java.util.stream.Collectors; + +/** + *

+ * 端口转发服务实现类 + *

+ * + * @author QAQ + * @since 2025-06-03 + */ +@Slf4j +@Service +public class ChainTunnelServiceImpl extends ServiceImpl implements ChainTunnelService { + +} diff --git a/springboot-backend/src/main/java/com/admin/service/impl/ForwardPortServiceImpl.java b/springboot-backend/src/main/java/com/admin/service/impl/ForwardPortServiceImpl.java new file mode 100644 index 0000000..beb9398 --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/service/impl/ForwardPortServiceImpl.java @@ -0,0 +1,15 @@ +package com.admin.service.impl; + +import com.admin.entity.ChainTunnel; +import com.admin.entity.ForwardPort; +import com.admin.mapper.ChainTunnelMapper; +import com.admin.mapper.ForwardPortMapper; +import com.admin.service.ChainTunnelService; +import com.admin.service.ForwardPortService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.stereotype.Service; + +@Service +public class ForwardPortServiceImpl extends ServiceImpl implements ForwardPortService { + +} diff --git a/springboot-backend/src/main/java/com/admin/service/impl/ForwardServiceImpl.java b/springboot-backend/src/main/java/com/admin/service/impl/ForwardServiceImpl.java index 08be17a..f150687 100644 --- a/springboot-backend/src/main/java/com/admin/service/impl/ForwardServiceImpl.java +++ b/springboot-backend/src/main/java/com/admin/service/impl/ForwardServiceImpl.java @@ -1,9 +1,6 @@ package com.admin.service.impl; -import com.admin.common.dto.ForwardDto; -import com.admin.common.dto.ForwardUpdateDto; -import com.admin.common.dto.ForwardWithTunnelDto; -import com.admin.common.dto.GostDto; +import com.admin.common.dto.*; import com.admin.common.lang.R; import com.admin.common.utils.GostUtil; import com.admin.common.utils.JwtUtil; @@ -11,6 +8,7 @@ import com.admin.common.utils.WebSocketServer; import com.admin.entity.*; import com.admin.mapper.ForwardMapper; import com.admin.service.*; +import com.alibaba.fastjson.JSONArray; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.alibaba.fastjson.JSONObject; @@ -37,17 +35,6 @@ import java.util.stream.Collectors; @Service public class ForwardServiceImpl extends ServiceImpl implements ForwardService { - // 常量定义 - private static final String GOST_SUCCESS_MSG = "OK"; - private static final String GOST_NOT_FOUND_MSG = "not found"; - private static final int ADMIN_ROLE_ID = 0; - private static final int TUNNEL_TYPE_PORT_FORWARD = 1; - private static final int TUNNEL_TYPE_TUNNEL_FORWARD = 2; - private static final int FORWARD_STATUS_ACTIVE = 1; - private static final int FORWARD_STATUS_PAUSED = 0; - private static final int FORWARD_STATUS_ERROR = -1; - private static final int TUNNEL_STATUS_ACTIVE = 1; - private static final long BYTES_TO_GB = 1024L * 1024L * 1024L; @Resource @@ -63,80 +50,142 @@ public class ForwardServiceImpl extends ServiceImpl impl @Resource NodeService nodeService; + @Resource + ChainTunnelService chainTunnelService; - @Override - public R createForward(ForwardDto forwardDto) { - // 1. 获取当前用户信息 - UserInfo currentUser = getCurrentUserInfo(); - - // 2. 检查隧道是否存在和可用 - Tunnel tunnel = validateTunnel(forwardDto.getTunnelId()); - if (tunnel == null) { - return R.err("隧道不存在"); - } - if (tunnel.getStatus() != TUNNEL_STATUS_ACTIVE) { - return R.err("隧道已禁用,无法创建转发"); - } - - // 3. 普通用户权限和限制检查 - UserPermissionResult permissionResult = checkUserPermissions(currentUser, tunnel, null); - if (permissionResult.isHasError()) { - return R.err(permissionResult.getErrorMessage()); - } - - // 4. 分配端口 - PortAllocation portAllocation = allocatePorts(tunnel, forwardDto.getInPort()); - if (portAllocation.isHasError()) { - return R.err(portAllocation.getErrorMessage()); - } - - // 5. 创建并保存Forward对象 - Forward forward = createForwardEntity(forwardDto, currentUser, portAllocation); - if (!this.save(forward)) { - return R.err("端口转发创建失败"); - } - - // 6. 获取所需的节点信息 - NodeInfo nodeInfo = getRequiredNodes(tunnel); - if (nodeInfo.isHasError()) { - this.removeById(forward.getId()); - return R.err(nodeInfo.getErrorMessage()); - } - - // 7. 调用Gost服务创建转发 - R gostResult = createGostServices(forward, tunnel, permissionResult.getLimiter(), nodeInfo, permissionResult.getUserTunnel()); - - if (gostResult.getCode() != 0) { - this.removeById(forward.getId()); - return gostResult; - } - - return R.ok(); - } + @Resource + ForwardPortService forwardPortService; @Override public R getAllForwards() { UserInfo currentUser = getCurrentUserInfo(); - List forwardList; - if (currentUser.getRoleId() != ADMIN_ROLE_ID) { + if (currentUser.getRoleId() != 0) { forwardList = baseMapper.selectForwardsWithTunnelByUserId(currentUser.getUserId()); } else { forwardList = baseMapper.selectAllForwardsWithTunnel(); } + // 填充入口IP和端口信息 + for (ForwardWithTunnelDto forward : forwardList) { + // 获取隧道信息 + Tunnel tunnel = tunnelService.getById(forward.getTunnelId()); + if (tunnel == null) continue; + + // 获取该转发的所有ForwardPort记录 + List forwardPorts = forwardPortService.list( + new QueryWrapper().eq("forward_id", forward.getId()) + ); + + if (forwardPorts.isEmpty()) continue; + + // 判断是否使用隧道的inIp + boolean useTunnelInIp = tunnel.getInIp() != null && !tunnel.getInIp().trim().isEmpty(); + + // 收集所有的IP列表 + List ipList = new ArrayList<>(); + // 收集所有的端口列表 + List portList = new ArrayList<>(); + + if (useTunnelInIp) { + // 使用隧道的inIp + String[] tunnelInIps = tunnel.getInIp().split(","); + for (String ip : tunnelInIps) { + if (ip != null && !ip.trim().isEmpty()) { + ipList.add(ip.trim()); + } + } + } else { + // 使用节点的serverIp + for (ForwardPort forwardPort : forwardPorts) { + Node node = nodeService.getById(forwardPort.getNodeId()); + if (node != null && node.getServerIp() != null) { + ipList.add(node.getServerIp()); + } + } + } + + // 收集所有端口 + for (ForwardPort forwardPort : forwardPorts) { + if (forwardPort.getPort() != null) { + portList.add(forwardPort.getPort()); + } + } + + // 去重 + List uniqueIps = ipList.stream().distinct().toList(); + List uniquePorts = portList.stream().distinct().toList(); + + // 组合 IP:Port(笛卡尔积) + Set ipPortSet = new LinkedHashSet<>(); + for (String ip : uniqueIps) { + for (Integer port : uniquePorts) { + ipPortSet.add(ip + ":" + port); + } + } + + // 设置入口IP和端口 + if (!ipPortSet.isEmpty()) { + forward.setInIp(String.join(",", ipPortSet)); + // inPort设置为第一个端口(用于向后兼容) + forward.setInPort(uniquePorts.getFirst()); + } + } + return R.ok(forwardList); } + @Override + public R createForward(ForwardDto forwardDto) { + UserInfo currentUser = getCurrentUserInfo(); + + Tunnel tunnel = validateTunnel(forwardDto.getTunnelId()); + if (tunnel == null) { + return R.err("隧道不存在"); + } + + if (tunnel.getStatus() != 1) { + return R.err("隧道已禁用,无法创建转发"); + } + + UserPermissionResult permissionResult = checkUserPermissions(currentUser, tunnel, null); + if (permissionResult.isHasError()) { + return R.err(permissionResult.getErrorMessage()); + } + Forward forward = new Forward(); + BeanUtils.copyProperties(forwardDto, forward); + forward.setStatus(1); + forward.setUserId(currentUser.getUserId()); + forward.setUserName(currentUser.getUserName()); + forward.setCreatedTime(System.currentTimeMillis()); + forward.setUpdatedTime(System.currentTimeMillis()); + this.save(forward); + + List chainTunnels = chainTunnelService.list(new QueryWrapper().eq("tunnel_id", tunnel.getId()).eq("chain_type", 1)); + for (ChainTunnel chainTunnel : chainTunnels) { + Integer nodePort = tunnelService.getNodePort(chainTunnel.getNodeId(), 2); + + ForwardPort forwardPort = new ForwardPort(); + forwardPort.setForwardId(forward.getId()); + forwardPort.setNodeId(chainTunnel.getNodeId()); + forwardPort.setPort(nodePort); + forwardPortService.save(forwardPort); + String serviceName = buildServiceName(forward.getId(), forward.getUserId(), permissionResult.getUserTunnel()); + Integer limiter = permissionResult.getLimiter(); + + Node node = nodeService.getById(chainTunnel.getNodeId()); + if (node == null){ + return R.err("部分节点不存在"); + } + GostUtil.AddAndUpdateService(serviceName, limiter, node, forward, forwardPort, tunnel, "AddService"); + } + return R.ok(); + } + @Override public R updateForward(ForwardUpdateDto forwardUpdateDto) { // 1. 获取当前用户信息 UserInfo currentUser = getCurrentUserInfo(); - if (currentUser.getRoleId() != ADMIN_ROLE_ID) { - User user = userService.getById(currentUser.getUserId()); - if (user == null) return R.err("用户不存在"); - if (user.getStatus() == 0) return R.err("用户已到期或被禁用"); - } // 2. 检查转发是否存在 @@ -145,65 +194,19 @@ public class ForwardServiceImpl extends ServiceImpl impl return R.err("转发不存在"); } - // 3. 检查隧道是否存在和可用 - Tunnel tunnel = validateTunnel(forwardUpdateDto.getTunnelId()); + + Tunnel tunnel = validateTunnel(existForward.getTunnelId()); if (tunnel == null) { return R.err("隧道不存在"); } - if (tunnel.getStatus() != TUNNEL_STATUS_ACTIVE) { - return R.err("隧道已禁用,无法更新转发"); - } - boolean tunnelChanged = isTunnelChanged(existForward, forwardUpdateDto); - // 4. 检查权限和限制 - UserPermissionResult permissionResult = null; - if (tunnelChanged) { - if (currentUser.getRoleId() == ADMIN_ROLE_ID) { - // 管理员操作自己的转发时,不需要检查权限限制 - if (Objects.equals(currentUser.getUserId(), existForward.getUserId())) { - permissionResult = UserPermissionResult.success(null, null); - } else { - // 管理员操作用户转发时,需要检查原用户是否有新隧道权限 - // 获取原转发用户的信息 - User originalUser = userService.getById(existForward.getUserId()); - if (originalUser == null) { - return R.err("用户不存在"); - } - // 检查原用户是否有新隧道权限 - UserTunnel userTunnel = getUserTunnel(existForward.getUserId(), tunnel.getId().intValue()); - if (userTunnel == null) { - return R.err("用户没有该隧道权限"); - } - - if (userTunnel.getStatus() != 1) { - return R.err("隧道被禁用"); - } - - // 检查隧道权限到期时间 - if (userTunnel.getExpTime() != null && userTunnel.getExpTime() <= System.currentTimeMillis()) { - return R.err("用户的该隧道权限已到期"); - } - - // 检查原用户的流量和转发数量限制 - R quotaCheckResult = checkForwardQuota(existForward.getUserId(), tunnel.getId().intValue(), userTunnel, originalUser, forwardUpdateDto.getId()); - if (quotaCheckResult.getCode() != 0) { - return R.err("用户" + quotaCheckResult.getMsg()); - } - - permissionResult = UserPermissionResult.success(userTunnel.getSpeedId(), userTunnel); - } - } else { - // 普通用户检查自己的权限 - permissionResult = checkUserPermissions(currentUser, tunnel, forwardUpdateDto.getId()); - if (permissionResult.isHasError()) { - return R.err(permissionResult.getErrorMessage()); - } - } + UserPermissionResult permissionResult = checkUserPermissions(currentUser, tunnel, null); + if (permissionResult.isHasError()) { + return R.err(permissionResult.getErrorMessage()); } - // 5. 获取UserTunnel(即使隧道未变化也需要获取,用于构建服务名称) UserTunnel userTunnel = null; - if (currentUser.getRoleId() != ADMIN_ROLE_ID) { + if (currentUser.getRoleId() != 0) { userTunnel = getUserTunnel(currentUser.getUserId(), tunnel.getId().intValue()); if (userTunnel == null) { return R.err("你没有该隧道权限"); @@ -214,127 +217,37 @@ public class ForwardServiceImpl extends ServiceImpl impl userTunnel = getUserTunnel(existForward.getUserId(), tunnel.getId().intValue()); } - // 6. 更新Forward对象 - Forward updatedForward = updateForwardEntity(forwardUpdateDto, existForward, tunnel); + existForward.setRemoteAddr(forwardUpdateDto.getRemoteAddr()); + existForward.setName(forwardUpdateDto.getName()); + existForward.setStrategy(forwardUpdateDto.getStrategy()); + existForward.setStatus(1); + this.updateById(existForward); - // 7. 获取所需的节点信息 - NodeInfo nodeInfo = getRequiredNodes(tunnel); - if (nodeInfo.isHasError()) { - return R.err(nodeInfo.getErrorMessage()); + + List chainTunnels = chainTunnelService.list(new QueryWrapper().eq("tunnel_id", tunnel.getId()).eq("chain_type", 1)); + for (ChainTunnel chainTunnel : chainTunnels) { + String serviceName = buildServiceName(existForward.getId(), existForward.getUserId(), userTunnel); + Integer limiter = permissionResult.getLimiter(); + Node node = nodeService.getById(chainTunnel.getNodeId()); + if (node == null){ + return R.err("部分节点不存在"); + } + ForwardPort forwardPort = forwardPortService.getOne(new QueryWrapper().eq("forward_id", existForward.getId()).eq("node_id", node.getId())); + if (forwardPort == null){ + return R.err("部分节点不存在1"); + } + GostUtil.AddAndUpdateService(serviceName, limiter, node, existForward, forwardPort, tunnel, "UpdateService"); } - // 8. 调用Gost服务更新转发 - R gostResult; - if (tunnelChanged) { - // 隧道变化时:先删除原配置,再创建新配置 - gostResult = updateGostServicesWithTunnelChange(existForward, updatedForward, tunnel, permissionResult != null ? permissionResult.getLimiter() : null, nodeInfo, userTunnel); - } else { - // 隧道未变化时:直接更新配置 - gostResult = updateGostServices(updatedForward, tunnel, permissionResult != null ? permissionResult.getLimiter() : null, nodeInfo, userTunnel); - } - - if (gostResult.getCode() != 0) { - return gostResult; - } - updatedForward.setStatus(1); - // 9. 保存更新 - boolean result = this.updateById(updatedForward); - return result ? R.ok("端口转发更新成功") : R.err("端口转发更新失败"); + return R.ok(); } @Override public R deleteForward(Long id) { + // 1. 获取当前用户信息 UserInfo currentUser = getCurrentUserInfo(); - // 2. 检查转发是否存在 - Forward forward = validateForwardExists(id, currentUser); - if (forward == null) { - return R.err("端口转发不存在"); - } - - // 3. 获取隧道信息 - Tunnel tunnel = validateTunnel(forward.getTunnelId()); - if (tunnel == null) { - return R.err("隧道不存在"); - } - - // 4. 权限检查(仅普通用户需要) - UserTunnel userTunnel = null; - if (currentUser.getRoleId() != ADMIN_ROLE_ID) { - userTunnel = getUserTunnel(currentUser.getUserId(), tunnel.getId().intValue()); - if (userTunnel == null) { - return R.err("你没有该隧道权限"); - } - } else { - // 管理员删除用户记录时,需要获取对应的UserTunnel用于构建正确的服务名称 - userTunnel = getUserTunnel(forward.getUserId(), tunnel.getId().intValue()); - } - - // 5. 获取所需的节点信息 - NodeInfo nodeInfo = getRequiredNodes(tunnel); - if (nodeInfo.isHasError()) { - return R.err(nodeInfo.getErrorMessage()); - } - - // 6. 调用Gost服务删除转发 - R gostResult = deleteGostServices(forward, tunnel, nodeInfo, userTunnel); - if (gostResult.getCode() != 0) { - return gostResult; - } - - // 7. 删除转发记录 - boolean result = this.removeById(id); - if (result) { - return R.ok("端口转发删除成功"); - } else { - return R.err("端口转发删除失败"); - } - } - - @Override - public R pauseForward(Long id) { - return changeForwardStatus(id, FORWARD_STATUS_PAUSED, "暂停", "PauseService"); - } - - @Override - public R resumeForward(Long id) { - return changeForwardStatus(id, FORWARD_STATUS_ACTIVE, "恢复", "ResumeService"); - } - - @Override - public R forceDeleteForward(Long id) { - // 1. 获取当前用户信息 - UserInfo currentUser = getCurrentUserInfo(); - - // 2. 检查转发是否存在且用户有权限操作 - Forward forward = validateForwardExists(id, currentUser); - if (forward == null) { - return R.err("端口转发不存在"); - } - - // 3. 直接删除转发记录,跳过GOST服务删除 - boolean result = this.removeById(id); - if (result) { - return R.ok("端口转发强制删除成功"); - } else { - return R.err("端口转发强制删除失败"); - } - } - - /** - * 改变转发状态(暂停/恢复) - */ - private R changeForwardStatus(Long id, int targetStatus, String operation, String gostMethod) { - // 1. 获取当前用户信息 - UserInfo currentUser = getCurrentUserInfo(); - - if (currentUser.getRoleId() != ADMIN_ROLE_ID) { - User user = userService.getById(currentUser.getUserId()); - if (user == null) return R.err("用户不存在"); - if (user.getStatus() == 0) return R.err("用户已到期或被禁用"); - } - // 2. 检查转发是否存在 Forward forward = validateForwardExists(id, currentUser); @@ -342,93 +255,67 @@ public class ForwardServiceImpl extends ServiceImpl impl return R.err("转发不存在"); } - // 3. 获取隧道信息 + Tunnel tunnel = validateTunnel(forward.getTunnelId()); if (tunnel == null) { return R.err("隧道不存在"); } - // 4. 恢复服务时需要额外检查 - UserTunnel userTunnel = null; - if (targetStatus == FORWARD_STATUS_ACTIVE) { - if (tunnel.getStatus() != TUNNEL_STATUS_ACTIVE) { - return R.err("隧道已禁用,无法恢复服务"); - } - - // 普通用户需要检查流量和账户状态 - if (currentUser.getRoleId() != ADMIN_ROLE_ID) { - R flowCheckResult = checkUserFlowLimits(currentUser.getUserId(), tunnel); - if (flowCheckResult.getCode() != 0) { - return flowCheckResult; - } - - userTunnel = getUserTunnel(currentUser.getUserId(), tunnel.getId().intValue()); - if (userTunnel == null) { - return R.err("你没有该隧道权限"); - } - - if (userTunnel.getStatus() != 1) { - return R.err("隧道被禁用"); - } - } + UserPermissionResult permissionResult = checkUserPermissions(currentUser, tunnel, null); + if (permissionResult.isHasError()) { + return R.err(permissionResult.getErrorMessage()); } - - // 5. 权限检查(仅普通用户需要) - if (currentUser.getRoleId() != ADMIN_ROLE_ID && userTunnel == null) { + UserTunnel userTunnel = null; + if (currentUser.getRoleId() != 0) { userTunnel = getUserTunnel(currentUser.getUserId(), tunnel.getId().intValue()); if (userTunnel == null) { return R.err("你没有该隧道权限"); } - } - - // 6. 确保获取UserTunnel用于构建服务名称(包括管理员用户) - if (userTunnel == null) { - // 通过forward记录获取原始的用户ID来查找UserTunnel + } else { + // 管理员用户也需要获取UserTunnel(如果存在的话),用于构建正确的服务名称 + // 通过forward记录获取原始的用户ID userTunnel = getUserTunnel(forward.getUserId(), tunnel.getId().intValue()); } - // 7. 获取所需的节点信息 - NodeInfo nodeInfo = getRequiredNodes(tunnel); - if (nodeInfo.isHasError()) { - return R.err(nodeInfo.getErrorMessage()); - } + List chainTunnels = chainTunnelService.list(new QueryWrapper().eq("tunnel_id", tunnel.getId()).eq("chain_type", 1)); + for (ChainTunnel chainTunnel : chainTunnels) { - // 8. 调用Gost服务 - String serviceName = buildServiceName(forward.getId(), forward.getUserId(), userTunnel); - GostDto gostResult; - - if ("PauseService".equals(gostMethod)) { - gostResult = GostUtil.PauseService(nodeInfo.getInNode().getId(), serviceName); - - // 隧道转发需要同时暂停远端服务 - if (tunnel.getType() == TUNNEL_TYPE_TUNNEL_FORWARD && nodeInfo.getOutNode() != null) { - GostDto remoteResult = GostUtil.PauseRemoteService(nodeInfo.getOutNode().getId(), serviceName); - if (!isGostOperationSuccess(remoteResult)) { - return R.err(operation + "远端服务失败:" + remoteResult.getMsg()); - } + String serviceName = buildServiceName(forward.getId(), forward.getUserId(), userTunnel); + Node node = nodeService.getById(chainTunnel.getNodeId()); + if (node == null){ + return R.err("部分节点不存在"); } - } else { - gostResult = GostUtil.ResumeService(nodeInfo.getInNode().getId(), serviceName); - // 隧道转发需要同时恢复远端服务 - if (tunnel.getType() == TUNNEL_TYPE_TUNNEL_FORWARD && nodeInfo.getOutNode() != null) { - GostDto remoteResult = GostUtil.ResumeRemoteService(nodeInfo.getOutNode().getId(), serviceName); - if (!isGostOperationSuccess(remoteResult)) { - return R.err(operation + "远端服务失败:" + remoteResult.getMsg()); - } - } + JSONArray services = new JSONArray(); + services.add(serviceName + "_tcp"); + services.add(serviceName + "_udp"); + GostUtil.DeleteService(node.getId(), services); } + forwardPortService.remove(new QueryWrapper().eq("forward_id", id)); + this.removeById(id); + return R.ok(); + } - if (!isGostOperationSuccess(gostResult)) { - return R.err(operation + "服务失败:" + gostResult.getMsg()); + @Override + public R pauseForward(Long id) { + return changeForwardStatus(id, 0, "PauseService"); + } + + @Override + public R resumeForward(Long id) { + return changeForwardStatus(id, 1, "ResumeService"); + } + + @Override + public R forceDeleteForward(Long id) { + UserInfo currentUser = getCurrentUserInfo(); + Forward forward = validateForwardExists(id, currentUser); + if (forward == null) { + return R.err("端口转发不存在"); } - - // 9. 更新转发状态 - forward.setStatus(targetStatus); - forward.setUpdatedTime(System.currentTimeMillis()); - boolean result = this.updateById(forward); - - return result ? R.ok("服务已" + operation) : R.err("更新状态失败"); + this.removeById(id); + forwardPortService.remove(new QueryWrapper().eq("forward_id", id)); + return R.ok(); } @Override @@ -448,59 +335,164 @@ public class ForwardServiceImpl extends ServiceImpl impl return R.err("隧道不存在"); } - // 4. 获取入口节点信息 - Node inNode = nodeService.getNodeById(tunnel.getInNodeId()); - if (inNode == null) { - return R.err("入口节点不存在"); + // 4. 获取隧道的ChainTunnel信息 + List chainTunnels = chainTunnelService.list( + new QueryWrapper().eq("tunnel_id", tunnel.getId()) + ); + + if (chainTunnels.isEmpty()) { + return R.err("隧道配置不完整"); } + // 分类节点 + List inNodes = chainTunnels.stream() + .filter(ct -> ct.getChainType() == 1) + .toList(); + + Map> chainNodesMap = chainTunnels.stream() + .filter(ct -> ct.getChainType() == 2) + .collect(Collectors.groupingBy( + ct -> ct.getInx() != null ? ct.getInx() : 0, + Collectors.toList() + )); + + List> chainNodesList = chainNodesMap.entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .map(Map.Entry::getValue) + .toList(); + + List outNodes = chainTunnels.stream() + .filter(ct -> ct.getChainType() == 3) + .toList(); List results = new ArrayList<>(); String[] remoteAddresses = forward.getRemoteAddr().split(","); - // 6. 根据隧道类型执行不同的诊断策略 - if (tunnel.getType() == TUNNEL_TYPE_PORT_FORWARD) { + + // 根据隧道类型执行不同的诊断策略 + if (tunnel.getType() == 1) { // 端口转发:入口节点直接TCP ping目标地址 - for (String remoteAddress : remoteAddresses) { - // 提取IP和端口 - String targetIp = extractIpFromAddress(remoteAddress); - int targetPort = extractPortFromAddress(remoteAddress); - if (targetIp == null || targetPort == -1) { - return R.err("无法解析目标地址: " + remoteAddress); + for (ChainTunnel inNode : inNodes) { + Node node = nodeService.getById(inNode.getNodeId()); + if (node != null) { + for (String remoteAddress : remoteAddresses) { + String targetIp = extractIpFromAddress(remoteAddress); + int targetPort = extractPortFromAddress(remoteAddress); + if (targetIp != null && targetPort != -1) { + DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck( + node, targetIp, targetPort, + "入口(" + node.getName() + ")->目标(" + remoteAddress + ")" + ); + result.setFromChainType(1); + results.add(result); + } + } } - - DiagnosisResult result = performTcpPingDiagnosis(inNode, targetIp, targetPort, "转发->目标"); - results.add(result); } - } else { - // 隧道转发:入口TCP ping出口,出口TCP ping目标 - Node outNode = nodeService.getNodeById(tunnel.getOutNodeId()); - if (outNode == null) { - return R.err("出口节点不存在"); - } - - // 入口TCP ping出口(使用转发的出口端口) - DiagnosisResult inToOutResult = performTcpPingDiagnosis(inNode, outNode.getServerIp(), forward.getOutPort(), "入口->出口"); - results.add(inToOutResult); - - // 出口TCP ping目标 - for (String remoteAddress : remoteAddresses) { - // 提取IP和端口 - String targetIp = extractIpFromAddress(remoteAddress); - int targetPort = extractPortFromAddress(remoteAddress); - if (targetIp == null || targetPort == -1) { - return R.err("无法解析目标地址: " + remoteAddress); + } else if (tunnel.getType() == 2) { + // 隧道转发:测试完整链路 + // 1. 入口->第一跳(或出口) + for (ChainTunnel inNode : inNodes) { + Node fromNode = nodeService.getById(inNode.getNodeId()); + + if (fromNode != null) { + if (!chainNodesList.isEmpty()) { + for (ChainTunnel firstChainNode : chainNodesList.getFirst()) { + Node toNode = nodeService.getById(firstChainNode.getNodeId()); + if (toNode != null) { + DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck( + fromNode, toNode.getServerIp(), firstChainNode.getPort(), + "入口(" + fromNode.getName() + ")->第1跳(" + toNode.getName() + ")" + ); + result.setFromChainType(1); + result.setToChainType(2); + result.setToInx(firstChainNode.getInx()); + results.add(result); + } + } + } else if (!outNodes.isEmpty()) { + for (ChainTunnel outNode : outNodes) { + Node toNode = nodeService.getById(outNode.getNodeId()); + if (toNode != null) { + DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck( + fromNode, toNode.getServerIp(), outNode.getPort(), + "入口(" + fromNode.getName() + ")->出口(" + toNode.getName() + ")" + ); + result.setFromChainType(1); + result.setToChainType(3); + results.add(result); + } + } + } } - DiagnosisResult outToTargetResult = performTcpPingDiagnosis(outNode, targetIp, targetPort, "出口->目标"); - results.add(outToTargetResult); } + // 2. 链路测试 + for (int i = 0; i < chainNodesList.size(); i++) { + List currentHop = chainNodesList.get(i); + + for (ChainTunnel currentNode : currentHop) { + Node fromNode = nodeService.getById(currentNode.getNodeId()); + + if (fromNode != null) { + if (i + 1 < chainNodesList.size()) { + for (ChainTunnel nextNode : chainNodesList.get(i + 1)) { + Node toNode = nodeService.getById(nextNode.getNodeId()); + if (toNode != null) { + DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck( + fromNode, toNode.getServerIp(), nextNode.getPort(), + "第" + (i + 1) + "跳(" + fromNode.getName() + ")->第" + (i + 2) + "跳(" + toNode.getName() + ")" + ); + result.setFromChainType(2); + result.setFromInx(currentNode.getInx()); + result.setToChainType(2); + result.setToInx(nextNode.getInx()); + results.add(result); + } + } + } else if (!outNodes.isEmpty()) { + for (ChainTunnel outNode : outNodes) { + Node toNode = nodeService.getById(outNode.getNodeId()); + if (toNode != null) { + DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck( + fromNode, toNode.getServerIp(), outNode.getPort(), + "第" + (i + 1) + "跳(" + fromNode.getName() + ")->出口(" + toNode.getName() + ")" + ); + result.setFromChainType(2); + result.setFromInx(currentNode.getInx()); + result.setToChainType(3); + results.add(result); + } + } + } + } + } + } + + // 3. 出口->目标地址 + for (ChainTunnel outNode : outNodes) { + Node node = nodeService.getById(outNode.getNodeId()); + if (node != null) { + for (String remoteAddress : remoteAddresses) { + String targetIp = extractIpFromAddress(remoteAddress); + int targetPort = extractPortFromAddress(remoteAddress); + if (targetIp != null && targetPort != -1) { + DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck( + node, targetIp, targetPort, + "出口(" + node.getName() + ")->目标(" + remoteAddress + ")" + ); + result.setFromChainType(3); + results.add(result); + } + } + } + } } - // 7. 构建诊断报告 + // 构建诊断报告 Map diagnosisReport = new HashMap<>(); diagnosisReport.put("forwardId", id); diagnosisReport.put("forwardName", forward.getName()); - diagnosisReport.put("tunnelType", tunnel.getType() == TUNNEL_TYPE_PORT_FORWARD ? "端口转发" : "隧道转发"); + diagnosisReport.put("tunnelType", tunnel.getType() == 1 ? "端口转发" : "隧道转发"); diagnosisReport.put("results", results); diagnosisReport.put("timestamp", System.currentTimeMillis()); @@ -525,7 +517,7 @@ public class ForwardServiceImpl extends ServiceImpl impl } // 3. 验证用户权限(只能更新自己的转发) - if (currentUser.getRoleId() != ADMIN_ROLE_ID) { + if (currentUser.getRoleId() != 0) { // 普通用户只能更新自己的转发 List forwardIds = forwardsList.stream() .map(item -> Long.valueOf(item.get("id").toString())) @@ -569,10 +561,70 @@ public class ForwardServiceImpl extends ServiceImpl impl } } - /** - * 从地址字符串中提取IP地址 - * 支持格式: ip:port, [ipv6]:port, domain:port - */ + + private R changeForwardStatus(Long id, int targetStatus, String gostMethod) { + UserInfo currentUser = getCurrentUserInfo(); + if (currentUser.getRoleId() != 0) { + User user = userService.getById(currentUser.getUserId()); + if (user == null) return R.err("用户不存在"); + if (user.getStatus() == 0) return R.err("用户已到期或被禁用"); + } + Forward forward = validateForwardExists(id, currentUser); + if (forward == null) { + return R.err("转发不存在"); + } + + Tunnel tunnel = validateTunnel(forward.getTunnelId()); + if (tunnel == null) { + return R.err("隧道不存在"); + } + + UserTunnel userTunnel = null; + if (targetStatus == 1) { + if (tunnel.getStatus() != 1) { + return R.err("隧道已禁用,无法恢复服务"); + } + if (currentUser.getRoleId() != 0) { + R flowCheckResult = checkUserFlowLimits(currentUser.getUserId(), tunnel); + if (flowCheckResult.getCode() != 0) { + return flowCheckResult; + } + userTunnel = getUserTunnel(currentUser.getUserId(), tunnel.getId().intValue()); + if (userTunnel == null) { + return R.err("你没有该隧道权限"); + } + if (userTunnel.getStatus() != 1) { + return R.err("隧道被禁用"); + } + } + } + if (currentUser.getRoleId() != 0 && userTunnel == null) { + userTunnel = getUserTunnel(currentUser.getUserId(), tunnel.getId().intValue()); + if (userTunnel == null) { + return R.err("你没有该隧道权限"); + } + } + + if (userTunnel == null) { + userTunnel = getUserTunnel(forward.getUserId(), tunnel.getId().intValue()); + } + + List chainTunnels = chainTunnelService.list(new QueryWrapper().eq("tunnel_id", tunnel.getId()).eq("chain_type", 1)); + + for (ChainTunnel chainTunnel : chainTunnels) { + String serviceName = buildServiceName(forward.getId(), forward.getUserId(), userTunnel); + Node node = nodeService.getById(chainTunnel.getNodeId()); + if (node == null){ + return R.err("部分节点不存在"); + } + GostUtil.PauseAndResumeService(node.getId(), serviceName, gostMethod); + } + forward.setStatus(targetStatus); + forward.setUpdatedTime(System.currentTimeMillis()); + this.updateById(forward); + return R.ok(); + } + private String extractIpFromAddress(String address) { if (address == null || address.trim().isEmpty()) { return null; @@ -598,10 +650,6 @@ public class ForwardServiceImpl extends ServiceImpl impl return address; } - /** - * 从地址字符串中提取端口号 - * 支持格式: ip:port, [ipv6]:port, domain:port - */ private int extractPortFromAddress(String address) { if (address == null || address.trim().isEmpty()) { return -1; @@ -637,23 +685,14 @@ public class ForwardServiceImpl extends ServiceImpl impl return -1; } - /** - * 执行TCP ping诊断 - * - * @param node 执行TCP ping的节点 - * @param targetIp 目标IP地址 - * @param port 目标端口 - * @param description 诊断描述 - * @return 诊断结果 - */ private DiagnosisResult performTcpPingDiagnosis(Node node, String targetIp, int port, String description) { try { // 构建TCP ping请求数据 JSONObject tcpPingData = new JSONObject(); tcpPingData.put("ip", targetIp); tcpPingData.put("port", port); - tcpPingData.put("count", 2); - tcpPingData.put("timeout", 3000); // 5秒超时 + tcpPingData.put("count", 4); + tcpPingData.put("timeout", 5000); // 5秒超时 // 发送TCP ping命令到节点 GostDto gostResult = WebSocketServer.send_msg(node.getId(), tcpPingData, "TcpPing"); @@ -721,9 +760,26 @@ public class ForwardServiceImpl extends ServiceImpl impl } } - /** - * 获取当前用户信息 - */ + private DiagnosisResult performTcpPingDiagnosisWithConnectionCheck(Node node, String targetIp, int port, String description) { + DiagnosisResult result = new DiagnosisResult(); + result.setNodeId(node.getId()); + result.setNodeName(node.getName()); + result.setTargetIp(targetIp); + result.setTargetPort(port); + result.setDescription(description); + result.setTimestamp(System.currentTimeMillis()); + + try { + return performTcpPingDiagnosis(node, targetIp, port, description); + } catch (Exception e) { + result.setSuccess(false); + result.setMessage("连接检查异常: " + e.getMessage()); + result.setAverageTime(-1.0); + result.setPacketLoss(100.0); + return result; + } + } + private UserInfo getCurrentUserInfo() { Integer userId = JwtUtil.getUserIdFromToken(); Integer roleId = JwtUtil.getRoleIdFromToken(); @@ -731,16 +787,10 @@ public class ForwardServiceImpl extends ServiceImpl impl return new UserInfo(userId, roleId, userName); } - /** - * 验证隧道是否存在 - */ private Tunnel validateTunnel(Integer tunnelId) { return tunnelService.getById(tunnelId); } - /** - * 验证转发是否存在且用户有权限访问 - */ private Forward validateForwardExists(Long forwardId, UserInfo currentUser) { Forward forward = this.getById(forwardId); if (forward == null) { @@ -748,7 +798,7 @@ public class ForwardServiceImpl extends ServiceImpl impl } // 普通用户只能操作自己的转发 - if (currentUser.getRoleId() != ADMIN_ROLE_ID && + if (currentUser.getRoleId() != 0 && !Objects.equals(currentUser.getUserId(), forward.getUserId())) { return null; } @@ -756,31 +806,8 @@ public class ForwardServiceImpl extends ServiceImpl impl return forward; } - /** - * 获取所需的节点信息 - */ - private NodeInfo getRequiredNodes(Tunnel tunnel) { - Node inNode = nodeService.getNodeById(tunnel.getInNodeId()); - if (inNode == null) { - return NodeInfo.error("入口节点不存在"); - } - - Node outNode = null; - if (tunnel.getType() == TUNNEL_TYPE_TUNNEL_FORWARD) { - outNode = nodeService.getNodeById(tunnel.getOutNodeId()); - if (outNode == null) { - return NodeInfo.error("出口节点不存在"); - } - } - - return NodeInfo.success(inNode, outNode); - } - - /** - * 检查用户权限和限制 - */ private UserPermissionResult checkUserPermissions(UserInfo currentUser, Tunnel tunnel, Long excludeForwardId) { - if (currentUser.getRoleId() == ADMIN_ROLE_ID) { + if (currentUser.getRoleId() == 0) { return UserPermissionResult.success(null, null); } @@ -822,9 +849,6 @@ public class ForwardServiceImpl extends ServiceImpl impl return UserPermissionResult.success(userTunnel.getSpeedId(), userTunnel); } - /** - * 检查用户转发数量限制 - */ private R checkForwardQuota(Integer userId, Integer tunnelId, UserTunnel userTunnel, User userInfo, Long excludeForwardId) { // 检查用户总转发数量限制 long userForwardCount = this.count(new QueryWrapper().eq("user_id", userId)); @@ -849,9 +873,6 @@ public class ForwardServiceImpl extends ServiceImpl impl return R.ok(); } - /** - * 检查用户流量限制 - */ private R checkUserFlowLimits(Integer userId, Tunnel tunnel) { User userInfo = userService.getById(userId); if (userInfo.getExpTime() != null && userInfo.getExpTime() <= System.currentTimeMillis()) { @@ -884,539 +905,32 @@ public class ForwardServiceImpl extends ServiceImpl impl return R.ok(); } - /** - * 分配端口 - */ - private PortAllocation allocatePorts(Tunnel tunnel, Integer specifiedInPort) { - return allocatePorts(tunnel, specifiedInPort, null); - } - - /** - * 分配端口 - */ - private PortAllocation allocatePorts(Tunnel tunnel, Integer specifiedInPort, Long excludeForwardId) { - Integer inPort; - - if (specifiedInPort != null) { - // 用户指定了入口端口,需要检查是否可用 - if (!isInPortAvailable(tunnel, specifiedInPort, excludeForwardId)) { - return PortAllocation.error("指定的入口端口 " + specifiedInPort + " 已被占用或不在允许范围内"); - } - inPort = specifiedInPort; - } else { - // 用户未指定端口时自动分配 - inPort = allocateInPort(tunnel, excludeForwardId); - if (inPort == null) { - return PortAllocation.error("隧道入口端口已满,无法分配新端口"); - } - } - - Integer outPort = null; - if (tunnel.getType() == TUNNEL_TYPE_TUNNEL_FORWARD) { - outPort = allocateOutPort(tunnel, excludeForwardId); - if (outPort == null) { - return PortAllocation.error("隧道出口端口已满,无法分配新端口"); - } - } - - return PortAllocation.success(inPort, outPort); - } - - /** - * 创建Forward实体对象 - */ - private Forward createForwardEntity(ForwardDto forwardDto, UserInfo currentUser, PortAllocation portAllocation) { - Forward forward = new Forward(); - // 先复制DTO的属性,再设置其他属性,避免被覆盖 - BeanUtils.copyProperties(forwardDto, forward); - forward.setStatus(FORWARD_STATUS_ACTIVE); - forward.setInPort(portAllocation.getInPort()); - forward.setOutPort(portAllocation.getOutPort()); - forward.setUserId(currentUser.getUserId()); - forward.setUserName(currentUser.getUserName()); - forward.setCreatedTime(System.currentTimeMillis()); - forward.setUpdatedTime(System.currentTimeMillis()); - return forward; - } - - /** - * 更新Forward实体对象 - */ - private Forward updateForwardEntity(ForwardUpdateDto forwardUpdateDto, Forward existForward, Tunnel tunnel) { - Forward forward = new Forward(); - BeanUtils.copyProperties(forwardUpdateDto, forward); - - // 处理端口分配逻辑 - boolean tunnelChanged = !existForward.getTunnelId().equals(forwardUpdateDto.getTunnelId()); - boolean inPortChanged = forwardUpdateDto.getInPort() != null && - !Objects.equals(forwardUpdateDto.getInPort(), existForward.getInPort()); - - if (tunnelChanged || inPortChanged) { - // 隧道变化或入口端口变化时需要重新分配 - Integer specifiedInPort = forwardUpdateDto.getInPort(); - // 如果没有指定新端口但隧道未变化,保持原端口 - if (specifiedInPort == null && !tunnelChanged) { - specifiedInPort = existForward.getInPort(); - } - - PortAllocation portAllocation = allocatePorts(tunnel, specifiedInPort, forwardUpdateDto.getId()); - if (portAllocation.isHasError()) { - throw new RuntimeException(portAllocation.getErrorMessage()); - } - forward.setInPort(portAllocation.getInPort()); - forward.setOutPort(portAllocation.getOutPort()); - } else { - // 隧道和端口都未变化,保持原端口 - forward.setInPort(existForward.getInPort()); - forward.setOutPort(existForward.getOutPort()); - } - - forward.setUpdatedTime(System.currentTimeMillis()); - return forward; - } - - /** - * 创建Gost服务 - */ - private R createGostServices(Forward forward, Tunnel tunnel, Integer limiter, NodeInfo nodeInfo, UserTunnel userTunnel) { - String serviceName = buildServiceName(forward.getId(), forward.getUserId(), userTunnel); - - // 隧道转发需要创建链和远程服务 - if (tunnel.getType() == TUNNEL_TYPE_TUNNEL_FORWARD) { - R chainResult = createChainService(nodeInfo.getInNode(), serviceName, tunnel.getOutIp(), forward.getOutPort(), tunnel.getProtocol(), tunnel.getInterfaceName()); - if (chainResult.getCode() != 0) { - GostUtil.DeleteChains(nodeInfo.getInNode().getId(), serviceName); - return chainResult; - } - - R remoteResult = createRemoteService(nodeInfo.getOutNode(), serviceName, forward, tunnel.getProtocol(), forward.getInterfaceName()); - if (remoteResult.getCode() != 0) { - GostUtil.DeleteChains(nodeInfo.getInNode().getId(), serviceName); - GostUtil.DeleteRemoteService(nodeInfo.getOutNode().getId(), serviceName); - return remoteResult; - } - } - - String interfaceName = null; - // 创建主服务 - if (tunnel.getType() != TUNNEL_TYPE_TUNNEL_FORWARD) { // 不是隧道转发服务才会存在网络接口 - interfaceName = forward.getInterfaceName(); - } - - - R serviceResult = createMainService(nodeInfo.getInNode(), serviceName, forward, limiter, tunnel.getType(), tunnel, forward.getStrategy(), interfaceName); - if (serviceResult.getCode() != 0) { - GostUtil.DeleteChains(nodeInfo.getInNode().getId(), serviceName); - if (nodeInfo.getOutNode() != null) { - GostUtil.DeleteRemoteService(nodeInfo.getOutNode().getId(), serviceName); - } - return serviceResult; - } - return R.ok(); - } - - /** - * 更新Gost服务 - */ - private R updateGostServices(Forward forward, Tunnel tunnel, Integer limiter, NodeInfo nodeInfo, UserTunnel userTunnel) { - String serviceName = buildServiceName(forward.getId(), forward.getUserId(), userTunnel); - - // 隧道转发需要更新链和远程服务 - if (tunnel.getType() == TUNNEL_TYPE_TUNNEL_FORWARD) { - R chainResult = updateChainService(nodeInfo.getInNode(), serviceName, tunnel.getOutIp(), forward.getOutPort(), tunnel.getProtocol(), tunnel.getInterfaceName()); - if (chainResult.getCode() != 0) { - updateForwardStatusToError(forward); - return chainResult; - } - - R remoteResult = updateRemoteService(nodeInfo.getOutNode(), serviceName, forward, tunnel.getProtocol(), forward.getInterfaceName()); - if (remoteResult.getCode() != 0) { - updateForwardStatusToError(forward); - return remoteResult; - } - } - String interfaceName = null; - // 创建主服务 - if (tunnel.getType() != TUNNEL_TYPE_TUNNEL_FORWARD) { // 不是隧道转发服务才会存在网络接口 - interfaceName = forward.getInterfaceName(); - } - // 更新主服务 - R serviceResult = updateMainService(nodeInfo.getInNode(), serviceName, forward, limiter, tunnel.getType(), tunnel, forward.getStrategy(), interfaceName); - if (serviceResult.getCode() != 0) { - updateForwardStatusToError(forward); - return serviceResult; - } - - return R.ok(); - } - - /** - * 隧道变化时更新Gost服务:先删除原配置,再创建新配置 - */ - private R updateGostServicesWithTunnelChange(Forward existForward, Forward updatedForward, Tunnel newTunnel, Integer limiter, NodeInfo nodeInfo, UserTunnel userTunnel) { - // 1. 获取原隧道信息 - Tunnel oldTunnel = tunnelService.getById(existForward.getTunnelId()); - if (oldTunnel == null) { - return R.err("原隧道不存在,无法删除旧配置"); - } - - // 2. 删除原有的Gost服务配置 - R deleteResult = deleteOldGostServices(existForward, oldTunnel); - if (deleteResult.getCode() != 0) { - // 删除失败时记录日志,但不影响后续创建(可能原配置已不存在) - log.info("删除原隧道{}的Gost配置失败: {}", oldTunnel.getId(), deleteResult.getMsg()); - } - - // 3. 创建新的Gost服务配置 - R createResult = createGostServices(updatedForward, newTunnel, limiter, nodeInfo, userTunnel); - if (createResult.getCode() != 0) { - updateForwardStatusToError(updatedForward); - return R.err("创建新隧道配置失败: " + createResult.getMsg()); - } - - return R.ok(); - } - - /** - * 删除原有的Gost服务(隧道变化时专用) - */ - private R deleteOldGostServices(Forward forward, Tunnel oldTunnel) { - // 获取原隧道的用户隧道关系 - UserTunnel oldUserTunnel = getUserTunnel(forward.getUserId(), oldTunnel.getId().intValue()); - String serviceName = buildServiceName(forward.getId(), forward.getUserId(), oldUserTunnel); - - // 获取原隧道的节点信息 - NodeInfo oldNodeInfo = getRequiredNodes(oldTunnel); - - // 删除主服务(使用原隧道的入口节点) - if (!oldNodeInfo.isHasError() && oldNodeInfo.getInNode() != null) { - GostDto serviceResult = GostUtil.DeleteService(oldNodeInfo.getInNode().getId(), serviceName); - if (!isGostOperationSuccess(serviceResult)) { - log.info("删除主服务失败: {}", serviceResult.getMsg()); - } - } - - // 如果原隧道是隧道转发类型,需要删除链和远程服务 - if (oldTunnel.getType() == TUNNEL_TYPE_TUNNEL_FORWARD) { - // 删除链服务 - if (!oldNodeInfo.isHasError() && oldNodeInfo.getInNode() != null) { - GostDto chainResult = GostUtil.DeleteChains(oldNodeInfo.getInNode().getId(), serviceName); - if (!isGostOperationSuccess(chainResult)) { - log.info("删除链服务失败: {}", chainResult.getMsg()); - } - } - - // 删除远程服务(即使节点信息获取失败,也要尝试删除) - Node outNode = null; - if (!oldNodeInfo.isHasError()) { - outNode = oldNodeInfo.getOutNode(); - } else { - // 即使获取节点信息失败,也尝试直接获取出口节点来删除远程服务 - outNode = nodeService.getNodeById(oldTunnel.getOutNodeId()); - } - - if (outNode != null) { - GostDto remoteResult = GostUtil.DeleteRemoteService(outNode.getId(), serviceName); - if (!isGostOperationSuccess(remoteResult)) { - log.info("删除远程服务失败: {}", remoteResult.getMsg()); - } - } - } - - return R.ok(); - } - - /** - * 删除Gost服务 - */ - private R deleteGostServices(Forward forward, Tunnel tunnel, NodeInfo nodeInfo, UserTunnel userTunnel) { - String serviceName = buildServiceName(forward.getId(), forward.getUserId(), userTunnel); - - // 删除主服务 - GostDto serviceResult = GostUtil.DeleteService(nodeInfo.getInNode().getId(), serviceName); - if (!isGostOperationSuccess(serviceResult)) { - return R.err(serviceResult.getMsg()); - } - - // 隧道转发需要删除链和远程服务 - if (tunnel.getType() == TUNNEL_TYPE_TUNNEL_FORWARD) { - GostDto chainResult = GostUtil.DeleteChains(nodeInfo.getInNode().getId(), serviceName); - if (!isGostOperationSuccess(chainResult)) { - return R.err(chainResult.getMsg()); - } - - if (nodeInfo.getOutNode() != null) { - GostDto remoteResult = GostUtil.DeleteRemoteService(nodeInfo.getOutNode().getId(), serviceName); - if (!isGostOperationSuccess(remoteResult)) { - return R.err(remoteResult.getMsg()); - } - } - } - - return R.ok(); - } - - /** - * 创建链服务 - */ - private R createChainService(Node inNode, String serviceName, String outIp, Integer outPort, String protocol, String interfaceName) { - String remoteAddr = outIp + ":" + outPort; - if (outIp.contains(":")) { - remoteAddr = "[" + outIp + "]:" + outPort; - } - GostDto result = GostUtil.AddChains(inNode.getId(), serviceName, remoteAddr, protocol, interfaceName); - return isGostOperationSuccess(result) ? R.ok() : R.err(result.getMsg()); - } - - /** - * 创建远程服务 - */ - private R createRemoteService(Node outNode, String serviceName, Forward forward, String protocol, String interfaceName) { - GostDto result = GostUtil.AddRemoteService(outNode.getId(), serviceName, forward.getOutPort(), forward.getRemoteAddr(), protocol, forward.getStrategy(), interfaceName); - return isGostOperationSuccess(result) ? R.ok() : R.err(result.getMsg()); - } - - /** - * 创建主服务 - */ - private R createMainService(Node inNode, String serviceName, Forward forward, Integer limiter, Integer tunnelType, Tunnel tunnel, String strategy, String interfaceName) { - GostDto result = GostUtil.AddService(inNode.getId(), serviceName, forward.getInPort(), limiter, forward.getRemoteAddr(), tunnelType, tunnel, strategy, interfaceName); - return isGostOperationSuccess(result) ? R.ok() : R.err(result.getMsg()); - } - - /** - * 更新链服务 - */ - private R updateChainService(Node inNode, String serviceName, String outIp, Integer outPort, String protocol, String interfaceName) { - // 创建新链 - String remoteAddr = outIp + ":" + outPort; - if (outIp.contains(":")) { - remoteAddr = "[" + outIp + "]:" + outPort; - } - GostDto createResult = GostUtil.UpdateChains(inNode.getId(), serviceName, remoteAddr, protocol, interfaceName); - if (createResult.getMsg().contains(GOST_NOT_FOUND_MSG)) { - createResult = GostUtil.AddChains(inNode.getId(), serviceName, remoteAddr, protocol, interfaceName); - } - return isGostOperationSuccess(createResult) ? R.ok() : R.err(createResult.getMsg()); - } - - /** - * 更新远程服务 - */ - private R updateRemoteService(Node outNode, String serviceName, Forward forward, String protocol, String interfaceName) { - // 创建新远程服务 - GostDto createResult = GostUtil.UpdateRemoteService(outNode.getId(), serviceName, forward.getOutPort(), forward.getRemoteAddr(), protocol, forward.getStrategy(), interfaceName); - if (createResult.getMsg().contains(GOST_NOT_FOUND_MSG)) { - createResult = GostUtil.AddRemoteService(outNode.getId(), serviceName, forward.getOutPort(), forward.getRemoteAddr(), protocol, forward.getStrategy(), interfaceName); - } - return isGostOperationSuccess(createResult) ? R.ok() : R.err(createResult.getMsg()); - } - - /** - * 更新主服务 - */ - private R updateMainService(Node inNode, String serviceName, Forward forward, Integer limiter, Integer tunnelType, Tunnel tunnel, String strategy, String interfaceName) { - GostDto result = GostUtil.UpdateService(inNode.getId(), serviceName, forward.getInPort(), limiter, forward.getRemoteAddr(), tunnelType, tunnel, strategy, interfaceName); - - if (result.getMsg().contains(GOST_NOT_FOUND_MSG)) { - result = GostUtil.AddService(inNode.getId(), serviceName, forward.getInPort(), limiter, forward.getRemoteAddr(), tunnelType, tunnel, strategy, interfaceName); - } - - return isGostOperationSuccess(result) ? R.ok() : R.err(result.getMsg()); - } - - /** - * 更新转发状态为错误 - */ - private void updateForwardStatusToError(Forward forward) { - forward.setStatus(FORWARD_STATUS_ERROR); - this.updateById(forward); - } - - /** - * 获取用户隧道关系 - */ private UserTunnel getUserTunnel(Integer userId, Integer tunnelId) { return userTunnelService.getOne(new QueryWrapper() .eq("user_id", userId) .eq("tunnel_id", tunnelId)); } - /** - * 检查隧道是否发生变化 - */ - private boolean isTunnelChanged(Forward existForward, ForwardUpdateDto updateDto) { - return !existForward.getTunnelId().equals(updateDto.getTunnelId()); - } - - /** - * 检查Gost操作是否成功 - */ - private boolean isGostOperationSuccess(GostDto gostResult) { - return Objects.equals(gostResult.getMsg(), GOST_SUCCESS_MSG); - } - - - /** - * 检查指定的入口端口是否可用(可排除指定的转发ID) - */ - private boolean isInPortAvailable(Tunnel tunnel, Integer port, Long excludeForwardId) { - // 获取入口节点信息 - Node inNode = nodeService.getNodeById(tunnel.getInNodeId()); - if (inNode == null) { - return false; - } - - // 检查端口是否在节点允许的范围内 - if (port < inNode.getPortSta() || port > inNode.getPortEnd()) { - return false; - } - - // 获取该节点上所有已被占用的端口(包括作为入口和出口使用的端口) - Set usedPorts = getAllUsedPortsOnNode(tunnel.getInNodeId(), excludeForwardId); - - // 检查端口是否已被占用(在节点级别检查,考虑入口和出口端口) - return !usedPorts.contains(port); - } - - /** - * 为隧道分配一个可用的入口端口(可排除指定的转发ID) - */ - private Integer allocateInPort(Tunnel tunnel, Long excludeForwardId) { - return allocatePortForNode(tunnel.getInNodeId(), excludeForwardId); - } - - /** - * 为隧道分配一个可用的出口端口(可排除指定的转发ID) - */ - private Integer allocateOutPort(Tunnel tunnel, Long excludeForwardId) { - return allocatePortForNode(tunnel.getOutNodeId(), excludeForwardId); - } - - /** - * 为指定节点分配一个可用端口(通用方法) - * - * @param nodeId 节点ID - * @param excludeForwardId 要排除的转发ID - * @return 可用端口号,如果没有可用端口则返回null - */ - private Integer allocatePortForNode(Long nodeId, Long excludeForwardId) { - // 获取节点信息 - Node node = nodeService.getNodeById(nodeId); - if (node == null) { - return null; - } - - // 获取该节点上所有已被占用的端口(包括作为入口和出口使用的端口) - Set usedPorts = getAllUsedPortsOnNode(nodeId, excludeForwardId); - - // 在节点端口范围内寻找未使用的端口 - for (int port = node.getPortSta(); port <= node.getPortEnd(); port++) { - if (!usedPorts.contains(port)) { - return port; - } - } - return null; - } - - /** - * 获取指定节点上所有已被占用的端口(包括入口和出口端口) - * - * @param nodeId 节点ID - * @param excludeForwardId 要排除的转发ID - * @return 已占用的端口集合 - */ - private Set getAllUsedPortsOnNode(Long nodeId, Long excludeForwardId) { - Set usedPorts = new HashSet<>(); - - // 1. 收集该节点作为入口时占用的端口 - List inTunnels = tunnelService.list(new QueryWrapper().eq("in_node_id", nodeId)); - if (!inTunnels.isEmpty()) { - Set inTunnelIds = inTunnels.stream() - .map(Tunnel::getId) - .collect(Collectors.toSet()); - - QueryWrapper inQueryWrapper = new QueryWrapper().in("tunnel_id", inTunnelIds); - if (excludeForwardId != null) { - inQueryWrapper.ne("id", excludeForwardId); - } - - List inForwards = this.list(inQueryWrapper); - for (Forward forward : inForwards) { - if (forward.getInPort() != null) { - usedPorts.add(forward.getInPort()); - } - } - } - - // 2. 收集该节点作为出口时占用的端口 - List outTunnels = tunnelService.list(new QueryWrapper().eq("out_node_id", nodeId)); - if (!outTunnels.isEmpty()) { - Set outTunnelIds = outTunnels.stream() - .map(Tunnel::getId) - .collect(Collectors.toSet()); - - QueryWrapper outQueryWrapper = new QueryWrapper().in("tunnel_id", outTunnelIds); - if (excludeForwardId != null) { - outQueryWrapper.ne("id", excludeForwardId); - } - - List outForwards = this.list(outQueryWrapper); - for (Forward forward : outForwards) { - if (forward.getOutPort() != null) { - usedPorts.add(forward.getOutPort()); - } - } - } - - return usedPorts; - } - - - /** - * 构建服务名称,优化后减少重复查询 - */ private String buildServiceName(Long forwardId, Integer userId, UserTunnel userTunnel) { int userTunnelId = (userTunnel != null) ? userTunnel.getId() : 0; return forwardId + "_" + userId + "_" + userTunnelId; } - - public void updateForwardA(Forward forward) { - Tunnel tunnel = validateTunnel(forward.getTunnelId()); - if (tunnel == null) { - return; - } - UserTunnel userTunnel = getUserTunnel(forward.getUserId(), tunnel.getId().intValue()); - NodeInfo nodeInfo = getRequiredNodes(tunnel); - if (nodeInfo.isHasError()) { - return; - } - Integer limiter; - if (userTunnel == null) { - limiter = null; - } else { - limiter = userTunnel.getSpeedId(); - } - updateGostServices(forward, tunnel, limiter, nodeInfo, userTunnel); - } - - // ========== 内部数据类 ========== - /** - * 用户信息封装类 - */ @Data private static class UserInfo { private final Integer userId; private final Integer roleId; private final String userName; + + public UserInfo(Integer userId, Integer roleId, String userName) { + this.userId = userId; + this.roleId = roleId; + this.userName = userName; + } } - /** - * 用户权限检查结果 - */ @Data private static class UserPermissionResult { private final boolean hasError; @@ -1440,61 +954,6 @@ public class ForwardServiceImpl extends ServiceImpl impl } } - /** - * 端口分配结果 - */ - @Data - private static class PortAllocation { - private final boolean hasError; - private final String errorMessage; - private final Integer inPort; - private final Integer outPort; - - private PortAllocation(boolean hasError, String errorMessage, Integer inPort, Integer outPort) { - this.hasError = hasError; - this.errorMessage = errorMessage; - this.inPort = inPort; - this.outPort = outPort; - } - - public static PortAllocation success(Integer inPort, Integer outPort) { - return new PortAllocation(false, null, inPort, outPort); - } - - public static PortAllocation error(String errorMessage) { - return new PortAllocation(true, errorMessage, null, null); - } - } - - /** - * 节点信息封装类 - */ - @Data - private static class NodeInfo { - private final boolean hasError; - private final String errorMessage; - private final Node inNode; - private final Node outNode; - - private NodeInfo(boolean hasError, String errorMessage, Node inNode, Node outNode) { - this.hasError = hasError; - this.errorMessage = errorMessage; - this.inNode = inNode; - this.outNode = outNode; - } - - public static NodeInfo success(Node inNode, Node outNode) { - return new NodeInfo(false, null, inNode, outNode); - } - - public static NodeInfo error(String errorMessage) { - return new NodeInfo(true, errorMessage, null, null); - } - } - - /** - * 诊断结果数据类 - */ @Data public static class DiagnosisResult { private Long nodeId; @@ -1507,5 +966,12 @@ public class ForwardServiceImpl extends ServiceImpl impl private double averageTime; private double packetLoss; private long timestamp; + + // 链路类型相关字段 + private Integer fromChainType; // 1: 入口, 2: 链, 3: 出口 + private Integer fromInx; + private Integer toChainType; + private Integer toInx; } + } diff --git a/springboot-backend/src/main/java/com/admin/service/impl/NodeServiceImpl.java b/springboot-backend/src/main/java/com/admin/service/impl/NodeServiceImpl.java index 64a11bb..4b62291 100644 --- a/springboot-backend/src/main/java/com/admin/service/impl/NodeServiceImpl.java +++ b/springboot-backend/src/main/java/com/admin/service/impl/NodeServiceImpl.java @@ -7,14 +7,10 @@ import com.admin.common.dto.NodeDto; import com.admin.common.dto.NodeUpdateDto; import com.admin.common.lang.R; import com.admin.common.utils.WebSocketServer; -import com.admin.entity.Node; -import com.admin.entity.Tunnel; -import com.admin.entity.ViteConfig; +import com.admin.entity.*; import com.admin.mapper.NodeMapper; import com.admin.mapper.TunnelMapper; -import com.admin.service.NodeService; -import com.admin.service.TunnelService; -import com.admin.service.ViteConfigService; +import com.admin.service.*; import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; @@ -26,51 +22,13 @@ import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.List; import java.util.Objects; +import java.util.regex.Pattern; import org.springframework.beans.factory.annotation.Value; -/** - *

- * 节点服务实现类 - * 提供节点的增删改查功能,包括节点创建、更新、删除和查询操作 - *

- * - * @author QAQ - * @since 2025-06-03 - */ @Service public class NodeServiceImpl extends ServiceImpl implements NodeService { - // ========== 常量定义 ========== - - /** 节点默认状态:启用 */ - private static final int NODE_STATUS_ACTIVE = 0; - - /** 成功响应消息 */ - private static final String SUCCESS_CREATE_MSG = "节点创建成功"; - private static final String SUCCESS_UPDATE_MSG = "节点更新成功"; - private static final String SUCCESS_DELETE_MSG = "节点删除成功"; - - /** 错误响应消息 */ - private static final String ERROR_CREATE_MSG = "节点创建失败"; - private static final String ERROR_UPDATE_MSG = "节点更新失败"; - private static final String ERROR_DELETE_MSG = "节点删除失败"; - private static final String ERROR_NODE_NOT_FOUND = "节点不存在"; - - /** 隧道使用检查相关消息 */ - private static final String ERROR_IN_NODE_IN_USE = "该节点还有 %d 个隧道作为入口节点在使用,请先删除相关隧道"; - private static final String ERROR_OUT_NODE_IN_USE = "该节点还有 %d 个隧道作为出口节点在使用,请先删除相关隧道"; - - /** 端口范围验证相关消息 */ - private static final String ERROR_PORT_STA_REQUIRED = "起始端口不能为空"; - private static final String ERROR_PORT_END_REQUIRED = "结束端口不能为空"; - private static final String ERROR_PORT_RANGE_INVALID = "端口必须在1-65535范围内"; - private static final String ERROR_PORT_ORDER_INVALID = "结束端口不能小于起始端口"; - - // ========== 依赖注入 ========== - - @Resource - private TunnelMapper tunnelMapper; @Resource @Lazy @@ -79,52 +37,41 @@ public class NodeServiceImpl extends ServiceImpl implements No @Resource ViteConfigService viteConfigService; + @Resource + ChainTunnelService chainTunnelService; - // ========== 公共接口实现 ========== - /** - * 创建新节点 - * - * @param nodeDto 节点创建数据传输对象 - * @return 创建结果响应 - */ @Override public R createNode(NodeDto nodeDto) { - Node node = buildNewNode(nodeDto); - boolean result = this.save(node); - return result ? R.ok(SUCCESS_CREATE_MSG) : R.err(ERROR_CREATE_MSG); + validatePortRange(nodeDto.getPort()); + Node node = new Node(); + node.setSecret(IdUtil.simpleUUID()); + node.setStatus(0); + node.setPort(nodeDto.getPort()); + node.setName(nodeDto.getName()); + node.setServerIp(nodeDto.getServerIp()); + long currentTime = System.currentTimeMillis(); + node.setCreatedTime(currentTime); + node.setUpdatedTime(currentTime); + node.setInterfaceName(nodeDto.getInterfaceName()); + this.save(node); + return R.ok(); } - - - /** - * 获取所有节点列表 - * 注意:返回结果中会隐藏节点密钥信息 - * - * @return 包含所有节点的响应对象 - */ @Override public R getAllNodes() { - List nodeList = this.list(); - hideNodeSecrets(nodeList); + List nodeList = this.list(new QueryWrapper().orderByDesc("status")); + nodeList.forEach(node -> node.setSecret(null)); return R.ok(nodeList); } - /** - * 更新节点信息 - * - * @param nodeUpdateDto 节点更新数据传输对象 - * @return 更新结果响应 - */ @Override public R updateNode(NodeUpdateDto nodeUpdateDto) { - // 1. 验证节点是否存在 Node node = this.getById(nodeUpdateDto.getId()); if (node == null) { - return R.err(ERROR_NODE_NOT_FOUND); + return R.err("节点不存在"); } - //1.1 如果节点在线 且传入更新的 http/tls/socks 任意一项与数据库不一致,则通过 WS 通知节点更新设置 boolean online = node.getStatus() != null && node.getStatus() == 1; Integer newHttp = nodeUpdateDto.getHttp(); Integer newTls = nodeUpdateDto.getTls(); @@ -147,242 +94,94 @@ public class NodeServiceImpl extends ServiceImpl implements No } - // 2. 构建更新对象并执行更新 + Node updateNode = buildUpdateNode(nodeUpdateDto); - boolean result = this.updateById(updateNode); - - // 更新隧道入口ip - List inNodeId = tunnelService.list(new QueryWrapper().eq("in_node_id", updateNode.getId())); - if (!inNodeId.isEmpty()) { - for (Tunnel tunnel : inNodeId) { - tunnel.setInIp(updateNode.getIp()); - } - tunnelService.updateBatchById(inNodeId); - } - - // 更新服务器出口ip - List outNodeId = tunnelService.list(new QueryWrapper().eq("out_node_id", updateNode.getId())); - if (!outNodeId.isEmpty()) { - for (Tunnel tunnel : outNodeId) { - tunnel.setOutIp(updateNode.getServerIp()); - } - tunnelService.updateBatchById(outNodeId); - } - - return result ? R.ok(SUCCESS_UPDATE_MSG) : R.err(ERROR_UPDATE_MSG); + this.updateById(updateNode); + return R.ok(); } - /** - * 删除节点 - * 删除前会检查是否有隧道正在使用该节点 - * - * @param id 节点ID - * @return 删除结果响应 - */ @Override public R deleteNode(Long id) { - // 1. 验证节点是否存在 Node node = this.getById(id); if (node == null) { - return R.err(ERROR_NODE_NOT_FOUND); + return R.err("节点不存在"); } - // 2. 检查节点使用情况 - R usageCheckResult = checkNodeUsage(id); - if (usageCheckResult.getCode() != 0) { - return usageCheckResult; + List list = chainTunnelService.list(new QueryWrapper().eq("node_id", id).groupBy("tunnel_id")); + for (ChainTunnel tunnel : list) { + tunnelService.deleteTunnel(tunnel.getTunnelId()); } - - // 3. 执行删除操作 - boolean result = this.removeById(id); - return result ? R.ok(SUCCESS_DELETE_MSG) : R.err(ERROR_DELETE_MSG); + this.removeById(id); + return R.ok(); } - /** - * 根据ID获取节点信息 - * - * @param id 节点ID - * @return 节点对象 - * @throws RuntimeException 当节点不存在时抛出异常 - */ + @Override - public Node getNodeById(Long id) { + public R getInstallCommand(Long id) { Node node = this.getById(id); if (node == null) { - throw new RuntimeException(ERROR_NODE_NOT_FOUND); + return R.err("节点不存在"); } - return node; + ViteConfig viteConfig = viteConfigService.getOne(new QueryWrapper().eq("name", "ip")); + if (viteConfig == null) return R.err("请先前往网站配置中设置ip"); + StringBuilder command = new StringBuilder(); + command.append("curl -L https://github.com/bqlpfy/flux-panel/releases/download/2.0.0-beta/install.sh") + .append(" -o ./install.sh && chmod +x ./install.sh && "); + String processedServerAddr = processServerAddress(viteConfig.getValue()); + command.append("./install.sh") + .append(" -a ").append(processedServerAddr) // 服务器地址 + .append(" -s ").append(node.getSecret()); // 节点密钥 + return R.ok(command); + } - // ========== 私有辅助方法 ========== - /** - * 构建新节点对象 - * - * @param nodeDto 节点创建DTO - * @return 构建完成的节点对象 - */ - private Node buildNewNode(NodeDto nodeDto) { - Node node = new Node(); - BeanUtils.copyProperties(nodeDto, node); - - // 验证端口范围 - validatePortRange(node.getPortSta(), node.getPortEnd()); - - // 设置默认属性 - node.setSecret(IdUtil.simpleUUID()); - node.setStatus(NODE_STATUS_ACTIVE); - - // 设置时间戳 - long currentTime = System.currentTimeMillis(); - node.setCreatedTime(currentTime); - node.setUpdatedTime(currentTime); - - return node; - } - - /** - * 构建节点更新对象 - * - * @param nodeUpdateDto 节点更新DTO - * @return 构建完成的更新对象 - */ private Node buildUpdateNode(NodeUpdateDto nodeUpdateDto) { + validatePortRange(nodeUpdateDto.getPort()); Node node = new Node(); node.setId(nodeUpdateDto.getId()); node.setName(nodeUpdateDto.getName()); - node.setIp(nodeUpdateDto.getIp()); node.setServerIp(nodeUpdateDto.getServerIp()); - node.setPortSta(nodeUpdateDto.getPortSta()); - node.setPortEnd(nodeUpdateDto.getPortEnd()); + node.setPort(nodeUpdateDto.getPort()); node.setHttp(nodeUpdateDto.getHttp()); node.setTls(nodeUpdateDto.getTls()); node.setSocks(nodeUpdateDto.getSocks()); - // 验证端口范围 - validatePortRange(node.getPortSta(), node.getPortEnd()); - node.setUpdatedTime(System.currentTimeMillis()); + node.setInterfaceName(nodeUpdateDto.getInterfaceName()); + node.setTcpListenAddr(nodeUpdateDto.getTcpListenAddr()); + node.setUdpListenAddr(nodeUpdateDto.getUdpListenAddr()); return node; } - /** - * 隐藏节点列表中的密钥信息 - * - * @param nodeList 节点列表 - */ - private void hideNodeSecrets(List nodeList) { - nodeList.forEach(node -> node.setSecret(null)); - } - - /** - * 检查节点使用情况 - * 验证是否有隧道正在使用该节点作为入口或出口节点 - * - * @param nodeId 节点ID - * @return 检查结果响应 - */ - private R checkNodeUsage(Long nodeId) { - // 检查入口节点使用情况 - R inNodeCheckResult = checkInNodeUsage(nodeId); - if (inNodeCheckResult.getCode() != 0) { - return inNodeCheckResult; + private void validatePortRange(String port) { + Pattern PORT_PATTERN = Pattern.compile( "([0-9]{1,5})(-([0-9]{1,5}))?"); + if (port == null || port.isEmpty()) { + throw new RuntimeException("可用端口不合法"); } - - // 检查出口节点使用情况 - return checkOutNodeUsage(nodeId); - } - - /** - * 检查节点作为入口节点的使用情况 - * - * @param nodeId 节点ID - * @return 检查结果响应 - */ - private R checkInNodeUsage(Long nodeId) { - QueryWrapper query = new QueryWrapper<>(); - query.eq("in_node_id", nodeId); - - long tunnelCount = tunnelMapper.selectCount(query); - if (tunnelCount > 0) { - String errorMsg = String.format(ERROR_IN_NODE_IN_USE, tunnelCount); - return R.err(errorMsg); + String[] parts = port.split(","); + for (String part : parts) { + part = part.trim(); + if (!PORT_PATTERN.matcher(part).matches()) { + throw new RuntimeException("可用端口不合法"); + } + if (part.contains("-")) { + String[] range = part.split("-"); + int start = Integer.parseInt(range[0]); + int end = Integer.parseInt(range[1]); + if (start < 0 || end < 0 || end > 65535 || start > end) { + throw new RuntimeException("可用端口不合法"); + } + } else { + int ports = Integer.parseInt(part); + if (ports < 0 || ports > 65535) { + throw new RuntimeException("可用端口不合法"); + } + } } - - return R.ok(); } - /** - * 检查节点作为出口节点的使用情况 - * - * @param nodeId 节点ID - * @return 检查结果响应 - */ - private R checkOutNodeUsage(Long nodeId) { - QueryWrapper query = new QueryWrapper<>(); - query.eq("out_node_id", nodeId); - - long tunnelCount = tunnelMapper.selectCount(query); - if (tunnelCount > 0) { - String errorMsg = String.format(ERROR_OUT_NODE_IN_USE, tunnelCount); - return R.err(errorMsg); - } - - return R.ok(); - } - /** - * 获取节点安装命令 - * 根据节点信息生成对应的安装命令 - * - * @param id 节点ID - * @return 包含安装命令的响应对象 - */ - @Override - public R getInstallCommand(Long id) { - // 1. 验证节点是否存在 - Node node = this.getById(id); - if (node == null) { - return R.err(ERROR_NODE_NOT_FOUND); - } - - // 2. 构建安装命令 - return buildInstallCommand(node); - } - - /** - * 构建节点安装命令 - * - * @param node 节点对象 - * @return 格式化的安装命令 - */ - private R buildInstallCommand(Node node) { - ViteConfig viteConfig = viteConfigService.getOne(new QueryWrapper().eq("name", "ip")); - if (viteConfig == null) return R.err("请先前往网站配置中设置ip"); - - StringBuilder command = new StringBuilder(); - - // 第一部分:下载安装脚本 - command.append("curl -L https://github.com/bqlpfy/flux-panel/releases/download/1.4.2/install.sh") - .append(" -o ./install.sh && chmod +x ./install.sh && "); - - // 处理服务器地址,如果是IPv6需要添加方括号 - String processedServerAddr = processServerAddress(viteConfig.getValue()); - - // 第二部分:执行安装脚本(去掉-u参数) - command.append("./install.sh") - .append(" -a ").append(processedServerAddr) // 服务器地址 - .append(" -s ").append(node.getSecret()); // 节点密钥 - - return R.ok(command.toString()); - } - - /** - * 处理服务器地址,确保IPv6地址被方括号包裹 - * - * @param serverAddr 原始服务器地址,格式可能为 host:port - * @return 处理后的服务器地址 - */ private String processServerAddress(String serverAddr) { if (StrUtil.isBlank(serverAddr)) { return serverAddr; @@ -411,12 +210,7 @@ public class NodeServiceImpl extends ServiceImpl implements No return serverAddr; } - /** - * 判断是否为IPv6地址 - * - * @param address 地址字符串(不包含端口号) - * @return 是否为IPv6地址 - */ + private boolean isIPv6Address(String address) { // IPv6地址包含多个冒号,至少2个 if (!address.contains(":")) { @@ -428,33 +222,4 @@ public class NodeServiceImpl extends ServiceImpl implements No return colonCount >= 2; } - /** - * 验证端口范围的有效性 - * - * @param portSta 起始端口 - * @param portEnd 结束端口 - * @throws RuntimeException 当端口范围无效时抛出异常 - */ - private void validatePortRange(Integer portSta, Integer portEnd) { - // 检查起始端口是否为空 - if (portSta == null) { - throw new RuntimeException(ERROR_PORT_STA_REQUIRED); - } - - // 检查结束端口是否为空 - if (portEnd == null) { - throw new RuntimeException(ERROR_PORT_END_REQUIRED); - } - - // 检查端口范围是否在有效区间内 - if (portSta < 1 || portSta > 65535 || portEnd < 1 || portEnd > 65535) { - throw new RuntimeException(ERROR_PORT_RANGE_INVALID); - } - - // 检查端口顺序是否正确 - if (portEnd < portSta) { - throw new RuntimeException(ERROR_PORT_ORDER_INVALID); - } - } - } diff --git a/springboot-backend/src/main/java/com/admin/service/impl/SpeedLimitServiceImpl.java b/springboot-backend/src/main/java/com/admin/service/impl/SpeedLimitServiceImpl.java index 9a7e877..f35ef56 100644 --- a/springboot-backend/src/main/java/com/admin/service/impl/SpeedLimitServiceImpl.java +++ b/springboot-backend/src/main/java/com/admin/service/impl/SpeedLimitServiceImpl.java @@ -5,15 +5,9 @@ import com.admin.common.dto.SpeedLimitDto; import com.admin.common.dto.SpeedLimitUpdateDto; import com.admin.common.lang.R; import com.admin.common.utils.GostUtil; -import com.admin.entity.Node; -import com.admin.entity.SpeedLimit; -import com.admin.entity.Tunnel; -import com.admin.entity.UserTunnel; +import com.admin.entity.*; import com.admin.mapper.SpeedLimitMapper; -import com.admin.service.NodeService; -import com.admin.service.SpeedLimitService; -import com.admin.service.TunnelService; -import com.admin.service.UserTunnelService; +import com.admin.service.*; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import lombok.Data; @@ -22,6 +16,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; +import javax.annotation.Resource; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.List; @@ -41,354 +36,96 @@ import java.util.UUID; @Service public class SpeedLimitServiceImpl extends ServiceImpl implements SpeedLimitService { - // ========== 常量定义 ========== - - /** Gost操作成功响应消息 */ - private static final String GOST_SUCCESS_MSG = "OK"; - - /** Gost未找到资源响应消息 */ - private static final String GOST_NOT_FOUND_MSG = "not found"; - - /** 限速规则状态 */ - private static final int SPEED_LIMIT_ACTIVE_STATUS = 1; - private static final int SPEED_LIMIT_INACTIVE_STATUS = 0; - - /** 速度转换比率:比特到字节 */ - private static final double BITS_TO_BYTES_RATIO = 8.0; - - /** 成功响应消息 */ - private static final String SUCCESS_UPDATE_MSG = "限速规则更新成功"; - private static final String SUCCESS_DELETE_MSG = "限速规则删除成功"; - - /** 错误响应消息 */ - private static final String ERROR_CREATE_MSG = "限速规则创建失败"; - private static final String ERROR_UPDATE_MSG = "限速规则更新失败"; - private static final String ERROR_DELETE_MSG = "限速规则删除失败"; - private static final String ERROR_SPEED_LIMIT_NOT_FOUND = "限速规则不存在"; - private static final String ERROR_TUNNEL_NOT_FOUND = "指定的隧道不存在"; - private static final String ERROR_TUNNEL_NOT_EXISTS = "隧道不存在"; - private static final String ERROR_TUNNEL_NAME_MISMATCH = "隧道名称与隧道ID不匹配"; - private static final String ERROR_SPEED_LIMIT_IN_USE = "该限速规则还有用户在使用 请先取消分配"; - - // ========== 依赖注入 ========== - - @Autowired + @Resource @Lazy - private TunnelService tunnelService; + TunnelService tunnelService; - @Autowired - private NodeService nodeService; + @Resource + NodeService nodeService; - @Autowired - private UserTunnelService userTunnelService; + @Resource + UserTunnelService userTunnelService; - @Autowired - @Lazy - private SpeedLimitService speedLimitService; + @Resource + ChainTunnelService chainTunnelService; - // ========== 公共接口实现 ========== - /** - * 创建限速规则 - * - * @param speedLimitDto 限速规则创建数据传输对象 - * @return 创建结果响应 - */ @Override public R createSpeedLimit(SpeedLimitDto speedLimitDto) { - // 1. 验证隧道 - TunnelValidationResult tunnelValidation = validateTunnelWithResult(speedLimitDto.getTunnelId(), speedLimitDto.getTunnelName()); - if (tunnelValidation.isHasError()) { - return R.err(tunnelValidation.getErrorMessage()); - } + Tunnel tunnel = tunnelService.getById(speedLimitDto.getTunnelId()); + if (tunnel == null) return R.err("隧道不存在"); - // 2. 创建限速规则实体 - SpeedLimit speedLimit = createSpeedLimitEntity(speedLimitDto); - if (!this.save(speedLimit)) { - return R.err(ERROR_CREATE_MSG); - } + SpeedLimit speedLimit = new SpeedLimit(); + BeanUtils.copyProperties(speedLimitDto, speedLimit); + long currentTime = System.currentTimeMillis(); + speedLimit.setCreatedTime(currentTime); + speedLimit.setUpdatedTime(currentTime); + speedLimit.setStatus(1); + this.save(speedLimit); - // 3. 调用Gost API添加限速器 - R gostResult = addGostLimiter(speedLimit, tunnelValidation.getTunnel()); - if (gostResult.getCode() != 0) { - handleGostOperationFailure(speedLimit); - this.removeById(speedLimit.getId()); - return gostResult; + String speedInMBps = convertBitsToMBps(speedLimit.getSpeed()); + + List tunnelList = chainTunnelService.list(new QueryWrapper().eq("tunnel_id", speedLimit.getTunnelId())); + for (ChainTunnel chainTunnel : tunnelList) { + Node node = nodeService.getById(chainTunnel.getNodeId()); + if (node != null) { + GostUtil.AddLimiters(node.getId(),speedLimit.getId(),speedInMBps); + } } - return R.ok(); } - /** - * 获取所有限速规则 - * - * @return 包含所有限速规则的响应对象 - */ @Override public R getAllSpeedLimits() { List speedLimits = this.list(); return R.ok(speedLimits); } - /** - * 更新限速规则 - * - * @param speedLimitUpdateDto 限速规则更新数据传输对象 - * @return 更新结果响应 - */ @Override public R updateSpeedLimit(SpeedLimitUpdateDto speedLimitUpdateDto) { - // 1. 验证限速规则是否存在 SpeedLimit speedLimit = this.getById(speedLimitUpdateDto.getId()); - if (speedLimit == null) { - return R.err(ERROR_SPEED_LIMIT_NOT_FOUND); + if (speedLimit == null) return R.err("限速不存在"); + + BeanUtils.copyProperties(speedLimitUpdateDto, speedLimit); + speedLimit.setUpdatedTime(System.currentTimeMillis()); + + String speedInMBps = convertBitsToMBps(speedLimit.getSpeed()); + List tunnelList = chainTunnelService.list(new QueryWrapper().eq("tunnel_id", speedLimit.getTunnelId())); + for (ChainTunnel chainTunnel : tunnelList) { + Node node = nodeService.getById(chainTunnel.getNodeId()); + if (node != null) { + GostUtil.UpdateLimiters(node.getId(),speedLimit.getId(),speedInMBps); + } } - - // 2. 验证隧道 - TunnelValidationResult tunnelValidation = validateTunnelWithResult(speedLimitUpdateDto.getTunnelId(), speedLimitUpdateDto.getTunnelName()); - if (tunnelValidation.isHasError()) { - return R.err(tunnelValidation.getErrorMessage()); - } - - // 3. 更新限速规则数据 - updateSpeedLimitEntity(speedLimitUpdateDto, speedLimit); - - // 4. 调用Gost API更新限速器 - R gostResult = updateGostLimiter(speedLimit, tunnelValidation.getTunnel()); - if (gostResult.getCode() != 0) { - return gostResult; - } - - // 5. 保存更新 - boolean result = this.updateById(speedLimit); - return result ? R.ok(SUCCESS_UPDATE_MSG) : R.err(ERROR_UPDATE_MSG); + this.updateById(speedLimit); + return R.ok(); } - /** - * 删除限速规则 - * 删除前会检查是否有用户正在使用该限速规则 - * - * @param id 限速规则ID - * @return 删除结果响应 - */ @Override public R deleteSpeedLimit(Long id) { // 1. 验证限速规则是否存在 SpeedLimit speedLimit = this.getById(id); - if (speedLimit == null) { - return R.err(ERROR_SPEED_LIMIT_NOT_FOUND); - } - - // 2. 检查使用情况 - R usageCheckResult = checkSpeedLimitUsage(id); - if (usageCheckResult.getCode() != 0) { - return usageCheckResult; - } - - // 3. 获取隧道信息 - Tunnel tunnel = tunnelService.getById(speedLimit.getTunnelId()); - if (tunnel == null) { - this.removeById(id); - return R.ok(); - } - - // 4. 调用Gost API删除限速器 - deleteGostLimiter(id, tunnel); - - // 5. 删除限速规则 - boolean result = this.removeById(id); - return result ? R.ok(SUCCESS_DELETE_MSG) : R.err(ERROR_DELETE_MSG); - } - - // ========== 私有辅助方法 ========== - - /** - * 验证隧道是否存在且名称匹配(返回详细结果) - * - * @param tunnelId 隧道ID - * @param tunnelName 隧道名称 - * @return 隧道验证结果 - */ - private TunnelValidationResult validateTunnelWithResult(Long tunnelId, String tunnelName) { - Tunnel tunnel = tunnelService.getById(tunnelId); - if (tunnel == null) { - return TunnelValidationResult.error(ERROR_TUNNEL_NOT_FOUND); - } - - if (!tunnel.getName().equals(tunnelName)) { - return TunnelValidationResult.error(ERROR_TUNNEL_NAME_MISMATCH); - } - - return TunnelValidationResult.success(tunnel); - } - - /** - * 验证隧道是否存在且名称匹配(兼容原有方法) - * - * @param tunnelId 隧道ID - * @param tunnelName 隧道名称 - * @return 验证结果响应 - */ - private R validateTunnel(Long tunnelId, String tunnelName) { - TunnelValidationResult result = validateTunnelWithResult(tunnelId, tunnelName); - return result.isHasError() ? R.err(result.getErrorMessage()) : R.ok(result.getTunnel()); - } - - /** - * 创建限速规则实体对象 - * - * @param speedLimitDto 限速规则创建DTO - * @return 构建完成的限速规则对象 - */ - private SpeedLimit createSpeedLimitEntity(SpeedLimitDto speedLimitDto) { - SpeedLimit speedLimit = new SpeedLimit(); - BeanUtils.copyProperties(speedLimitDto, speedLimit); - - // 设置默认属性 - long currentTime = System.currentTimeMillis(); - speedLimit.setCreatedTime(currentTime); - speedLimit.setUpdatedTime(currentTime); - speedLimit.setStatus(SPEED_LIMIT_ACTIVE_STATUS); - - return speedLimit; - } - - /** - * 更新限速规则实体对象 - * - * @param speedLimitUpdateDto 限速规则更新DTO - * @param speedLimit 待更新的限速规则对象 - */ - private void updateSpeedLimitEntity(SpeedLimitUpdateDto speedLimitUpdateDto, SpeedLimit speedLimit) { - BeanUtils.copyProperties(speedLimitUpdateDto, speedLimit); - speedLimit.setUpdatedTime(System.currentTimeMillis()); - } - - /** - * 检查限速规则使用情况 - * - * @param speedLimitId 限速规则ID - * @return 检查结果响应 - */ - private R checkSpeedLimitUsage(Long speedLimitId) { - int userCount = userTunnelService.count(new QueryWrapper().eq("speed_id", speedLimitId)); - if (userCount != 0) { - return R.err(ERROR_SPEED_LIMIT_IN_USE); + if (speedLimit == null) return R.err("限速规则不存在"); + + + int userCount = userTunnelService.count(new QueryWrapper().eq("speed_id", speedLimit.getId())); + if (userCount != 0) return R.err("该限速规则还有用户在使用 请先取消分配"); + + + List tunnelList = chainTunnelService.list(new QueryWrapper().eq("tunnel_id", speedLimit.getTunnelId())); + for (ChainTunnel chainTunnel : tunnelList) { + Node node = nodeService.getById(chainTunnel.getNodeId()); + if (node != null) { + GostUtil.DeleteLimiters(node.getId(),speedLimit.getId()); + } } + this.removeById(id); return R.ok(); } - /** - * 添加Gost限速器 - * - * @param speedLimit 限速规则对象 - * @param tunnel 隧道对象 - * @return 操作结果响应 - */ - private R addGostLimiter(SpeedLimit speedLimit, Tunnel tunnel) { - String speedInMBps = convertBitsToMBps(speedLimit.getSpeed()); - Node node = nodeService.getNodeById(tunnel.getInNodeId()); - - GostDto gostResult = GostUtil.AddLimiters( - node.getId(), - speedLimit.getId(), - speedInMBps - ); - - return isGostOperationSuccess(gostResult) ? R.ok() : R.err(gostResult.getMsg()); - } - - /** - * 更新Gost限速器 - * - * @param speedLimit 限速规则对象 - * @param tunnel 隧道对象 - * @return 操作结果响应 - */ - private R updateGostLimiter(SpeedLimit speedLimit, Tunnel tunnel) { - String speedInMBps = convertBitsToMBps(speedLimit.getSpeed()); - Node node = nodeService.getNodeById(tunnel.getInNodeId()); - - // 尝试更新限速器 - GostDto gostResult = GostUtil.UpdateLimiters(node.getId(), speedLimit.getId(), speedInMBps); - - // 如果限速器不存在,则创建新的 - if (gostResult.getMsg().contains(GOST_NOT_FOUND_MSG)) { - gostResult = GostUtil.AddLimiters(node.getId(), speedLimit.getId(), speedInMBps); - } - - return isGostOperationSuccess(gostResult) ? R.ok() : R.err(gostResult.getMsg()); - } - - /** - * 删除Gost限速器 - * - * @param speedLimitId 限速规则ID - * @param tunnel 隧道对象 - * @return 操作结果响应 - */ - private R deleteGostLimiter(Long speedLimitId, Tunnel tunnel) { - Node node = nodeService.getNodeById(tunnel.getInNodeId()); - GostDto gostResult = GostUtil.DeleteLimiters(node.getId(), speedLimitId); - - return isGostOperationSuccess(gostResult) ? R.ok() : R.err(gostResult.getMsg()); - } - - /** - * 处理Gost操作失败的情况 - * - * @param speedLimit 限速规则对象 - */ - private void handleGostOperationFailure(SpeedLimit speedLimit) { - speedLimit.setStatus(SPEED_LIMIT_INACTIVE_STATUS); - speedLimitService.updateById(speedLimit); - } - - /** - * 将比特率转换为兆字节每秒 - * - * @param speedInBits 比特率速度 - * @return 兆字节每秒字符串 - */ private String convertBitsToMBps(Integer speedInBits) { - double mbs = speedInBits / BITS_TO_BYTES_RATIO; + double mbs = speedInBits / 8.0; BigDecimal bd = new BigDecimal(mbs).setScale(1, RoundingMode.HALF_UP); return bd.doubleValue() + ""; } - - /** - * 检查Gost操作是否成功 - * - * @param gostResult Gost操作结果 - * @return 是否成功 - */ - private boolean isGostOperationSuccess(GostDto gostResult) { - return Objects.equals(gostResult.getMsg(), GOST_SUCCESS_MSG); - } - - // ========== 内部数据类 ========== - - /** - * 隧道验证结果封装类 - */ - @Data - private static class TunnelValidationResult { - private final boolean hasError; - private final String errorMessage; - private final Tunnel tunnel; - - private TunnelValidationResult(boolean hasError, String errorMessage, Tunnel tunnel) { - this.hasError = hasError; - this.errorMessage = errorMessage; - this.tunnel = tunnel; - } - - public static TunnelValidationResult success(Tunnel tunnel) { - return new TunnelValidationResult(false, null, tunnel); - } - - public static TunnelValidationResult error(String errorMessage) { - return new TunnelValidationResult(true, errorMessage, null); - } - } } diff --git a/springboot-backend/src/main/java/com/admin/service/impl/TunnelServiceImpl.java b/springboot-backend/src/main/java/com/admin/service/impl/TunnelServiceImpl.java index b5038c6..2f7e2bb 100644 --- a/springboot-backend/src/main/java/com/admin/service/impl/TunnelServiceImpl.java +++ b/springboot-backend/src/main/java/com/admin/service/impl/TunnelServiceImpl.java @@ -1,866 +1,630 @@ -package com.admin.service.impl; - -import cn.hutool.core.util.StrUtil; -import com.admin.common.dto.*; - -import com.admin.common.lang.R; -import com.admin.common.utils.GostUtil; -import com.admin.common.utils.JwtUtil; -import com.admin.common.utils.WebSocketServer; -import com.admin.entity.Forward; -import com.admin.entity.Node; -import com.admin.entity.Tunnel; -import com.admin.entity.User; -import com.admin.entity.UserTunnel; -import com.admin.mapper.TunnelMapper; -import com.admin.mapper.UserTunnelMapper; -import com.admin.service.ForwardService; -import com.admin.service.NodeService; -import com.admin.service.TunnelService; -import com.admin.service.UserTunnelService; -import com.alibaba.fastjson.JSONObject; -import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import lombok.Data; -import org.apache.commons.lang3.StringUtils; -import org.springframework.beans.BeanUtils; -import org.springframework.stereotype.Service; - -import javax.annotation.Resource; -import java.math.BigDecimal; -import java.util.*; -import java.util.stream.Collectors; - -/** - *

- * 隧道服务实现类 - * 提供隧道的增删改查功能,包括隧道创建、删除和用户权限管理 - * 支持端口转发和隧道转发两种模式 - *

- * - * @author QAQ - * @since 2025-06-03 - */ -@Service -public class TunnelServiceImpl extends ServiceImpl implements TunnelService { - - // ========== 常量定义 ========== - - /** 隧道类型常量 */ - private static final int TUNNEL_TYPE_PORT_FORWARD = 1; // 端口转发 - private static final int TUNNEL_TYPE_TUNNEL_FORWARD = 2; // 隧道转发 - - /** 隧道状态常量 */ - private static final int TUNNEL_STATUS_ACTIVE = 1; // 启用状态 - - /** 节点状态常量 */ - private static final int NODE_STATUS_ONLINE = 1; // 节点在线状态 - - /** 用户角色常量 */ - private static final int ADMIN_ROLE_ID = 0; // 管理员角色ID - - /** 成功响应消息 */ - private static final String SUCCESS_CREATE_MSG = "隧道创建成功"; - private static final String SUCCESS_DELETE_MSG = "隧道删除成功"; - - /** 错误响应消息 */ - private static final String ERROR_CREATE_MSG = "隧道创建失败"; - private static final String ERROR_DELETE_MSG = "隧道删除失败"; - private static final String ERROR_TUNNEL_NOT_FOUND = "隧道不存在"; - private static final String ERROR_TUNNEL_NAME_EXISTS = "隧道名称已存在"; - private static final String ERROR_IN_NODE_NOT_FOUND = "入口节点不存在"; - private static final String ERROR_OUT_NODE_NOT_FOUND = "出口节点不存在"; - private static final String ERROR_OUT_NODE_REQUIRED = "出口节点不能为空"; - private static final String ERROR_OUT_PORT_REQUIRED = "出口端口不能为空"; - private static final String ERROR_SAME_NODE_NOT_ALLOWED = "隧道转发模式下,入口和出口不能是同一个节点"; - private static final String ERROR_IN_PORT_RANGE_INVALID = "入口端口开始不能大于结束端口"; - private static final String ERROR_OUT_PORT_RANGE_INVALID = "出口端口开始不能大于结束端口"; - private static final String ERROR_NO_AVAILABLE_TUNNELS = "暂无可用隧道"; - private static final String ERROR_IN_NODE_OFFLINE = "入口节点当前离线,请确保节点正常运行"; - private static final String ERROR_OUT_NODE_OFFLINE = "出口节点当前离线,请确保节点正常运行"; - - /** 使用检查相关消息 */ - private static final String ERROR_FORWARDS_IN_USE = "该隧道还有 %d 个转发在使用,请先删除相关转发"; - private static final String ERROR_USER_PERMISSIONS_IN_USE = "该隧道还有 %d 个用户权限关联,请先取消用户权限分配"; - - // ========== 依赖注入 ========== - - @Resource - UserTunnelMapper userTunnelMapper; - - @Resource - NodeService nodeService; - - @Resource - ForwardService forwardService; - - @Resource - UserTunnelService userTunnelService; - - // ========== 公共接口实现 ========== - - /** - * 创建隧道 - * 支持端口转发和隧道转发两种模式 - * - * @param tunnelDto 隧道创建数据传输对象 - * @return 创建结果响应 - */ - @Override - public R createTunnel(TunnelDto tunnelDto) { - // 1. 验证隧道名称唯一性 - R nameValidationResult = validateTunnelNameUniqueness(tunnelDto.getName()); - if (nameValidationResult.getCode() != 0) { - return nameValidationResult; - } - - // 2. 验证隧道转发类型的必要参数 - if (tunnelDto.getType() == TUNNEL_TYPE_TUNNEL_FORWARD) { - R tunnelForwardValidationResult = validateTunnelForwardCreate(tunnelDto); - if (tunnelForwardValidationResult.getCode() != 0) { - return tunnelForwardValidationResult; - } - } - - // 3. 验证入口节点和端口 - NodeValidationResult inNodeValidation = validateInNode(tunnelDto); - if (inNodeValidation.isHasError()) { - return R.err(inNodeValidation.getErrorMessage()); - } - - // 4. 构建隧道实体 - Tunnel tunnel = buildTunnelEntity(tunnelDto, inNodeValidation.getNode()); - - // 5. 根据隧道类型设置出口参数 - R outNodeSetupResult = setupOutNodeParameters(tunnel, tunnelDto, inNodeValidation.getNode().getServerIp()); - if (outNodeSetupResult.getCode() != 0) { - return outNodeSetupResult; - } - - // 6. 设置默认属性并保存 - setDefaultTunnelProperties(tunnel); - boolean result = this.save(tunnel); - - return result ? R.ok(SUCCESS_CREATE_MSG) : R.err(ERROR_CREATE_MSG); - } - - /** - * 获取所有隧道列表 - * - * @return 包含所有隧道的响应对象 - */ - @Override - public R getAllTunnels() { - List tunnelList = this.list(); - return R.ok(tunnelList); - } - - /** - * 更新隧道(只允许修改名称、流量计费、端口范围) - * - * @param tunnelUpdateDto 更新数据传输对象 - * @return 更新结果响应 - */ - @Override - public R updateTunnel(TunnelUpdateDto tunnelUpdateDto) { - // 1. 验证隧道是否存在 - Tunnel existingTunnel = this.getById(tunnelUpdateDto.getId()); - if (existingTunnel == null) { - return R.err(ERROR_TUNNEL_NOT_FOUND); - } - - // 2. 验证隧道名称唯一性(排除自身) - R nameValidationResult = validateTunnelNameUniquenessForUpdate(tunnelUpdateDto.getName(), tunnelUpdateDto.getId()); - if (nameValidationResult.getCode() != 0) { - return nameValidationResult; - } - int up = 0; - if (!Objects.equals(existingTunnel.getTcpListenAddr(), tunnelUpdateDto.getTcpListenAddr()) || - !Objects.equals(existingTunnel.getUdpListenAddr(), tunnelUpdateDto.getUdpListenAddr()) || - !Objects.equals(existingTunnel.getProtocol(), tunnelUpdateDto.getProtocol()) || - !Objects.equals(existingTunnel.getInterfaceName(), tunnelUpdateDto.getInterfaceName())) { - up++; - } - - - // 5. 更新允许修改的字段 - existingTunnel.setName(tunnelUpdateDto.getName()); - existingTunnel.setFlow(tunnelUpdateDto.getFlow()); - existingTunnel.setTcpListenAddr(tunnelUpdateDto.getTcpListenAddr()); - existingTunnel.setUdpListenAddr(tunnelUpdateDto.getUdpListenAddr()); - existingTunnel.setTrafficRatio(tunnelUpdateDto.getTrafficRatio()); - existingTunnel.setProtocol(tunnelUpdateDto.getProtocol()); - existingTunnel.setInterfaceName(tunnelUpdateDto.getInterfaceName()); - this.updateById(existingTunnel); - int err = 0; - if (up != 0){ - System.out.println("123123"); - List tunnel = forwardService.list(new QueryWrapper().eq("tunnel_id", tunnelUpdateDto.getId())); - if (!tunnel.isEmpty()) { - for (Forward forward : tunnel) { - ForwardUpdateDto forwardUpdateDto = new ForwardUpdateDto(); - forwardUpdateDto.setId(forward.getId()); - forwardUpdateDto.setUserId(forward.getUserId()); - forwardUpdateDto.setName(forward.getName()); - forwardUpdateDto.setTunnelId(forward.getTunnelId()); - forwardUpdateDto.setRemoteAddr(forward.getRemoteAddr()); - forwardUpdateDto.setStrategy(forward.getStrategy()); - forwardUpdateDto.setInPort(forward.getInPort()); - forwardUpdateDto.setInterfaceName(forward.getInterfaceName()); - R r = forwardService.updateForward(forwardUpdateDto); - if (r.getCode() != 0){ - err++; - } - } - } - } - - if (err != 0) { - return R.err("隧道信息更新成功,但部分转发同步更新失败"); - } - return R.ok("隧道更新成功"); - } - - /** - * 删除隧道 - * 删除前会检查是否有转发或用户权限在使用该隧道 - * - * @param id 隧道ID - * @return 删除结果响应 - */ - @Override - public R deleteTunnel(Long id) { - // 1. 验证隧道是否存在 - if (!isTunnelExists(id)) { - return R.err(ERROR_TUNNEL_NOT_FOUND); - } - - // 2. 检查隧道使用情况 - R usageCheckResult = checkTunnelUsage(id); - if (usageCheckResult.getCode() != 0) { - return usageCheckResult; - } - - // 3. 执行删除操作 - boolean result = this.removeById(id); - return result ? R.ok(SUCCESS_DELETE_MSG) : R.err(ERROR_DELETE_MSG); - } - - /** - * 获取用户可用的隧道列表 - * 管理员可以看到所有启用的隧道,普通用户只能看到有权限的启用隧道 - * - * @return 用户可用隧道列表响应 - */ - @Override - public R userTunnel() { - UserInfo currentUser = getCurrentUserInfo(); - - // 根据用户角色获取隧道列表 - List tunnelEntities = getUserAccessibleTunnels(currentUser); - - // 转换为DTO并返回 - List tunnelDtos = convertToTunnelListDtos(tunnelEntities); - return R.ok(tunnelDtos); - } - - // ========== 私有辅助方法 ========== - - /** - * 获取当前用户信息 - * - * @return 用户信息对象 - */ - private UserInfo getCurrentUserInfo() { - Integer roleId = JwtUtil.getRoleIdFromToken(); - Integer userId = JwtUtil.getUserIdFromToken(); - return new UserInfo(userId, roleId); - } - - /** - * 验证隧道名称唯一性 - * - * @param tunnelName 隧道名称 - * @return 验证结果响应 - */ - private R validateTunnelNameUniqueness(String tunnelName) { - Tunnel existTunnel = this.getOne(new QueryWrapper().eq("name", tunnelName)); - if (existTunnel != null) { - return R.err(ERROR_TUNNEL_NAME_EXISTS); - } - return R.ok(); - } - - /** - * 验证隧道名称唯一性(更新时使用,排除自身) - * - * @param tunnelName 隧道名称 - * @param tunnelId 隧道ID(要排除的隧道) - * @return 验证结果响应 - */ - private R validateTunnelNameUniquenessForUpdate(String tunnelName, Long tunnelId) { - QueryWrapper query = new QueryWrapper<>(); - query.eq("name", tunnelName); - query.ne("id", tunnelId); // 排除自身 - Tunnel existTunnel = this.getOne(query); - if (existTunnel != null) { - return R.err(ERROR_TUNNEL_NAME_EXISTS); - } - return R.ok(); - } - - - - /** - * 验证隧道转发创建时的必要参数 - * - * @param tunnelDto 隧道创建数据传输对象 - * @return 验证结果响应 - */ - private R validateTunnelForwardCreate(TunnelDto tunnelDto) { - // 验证出口节点不能为空 - if (tunnelDto.getOutNodeId() == null) { - return R.err(ERROR_OUT_NODE_REQUIRED); - } - return R.ok(); - } - - /** - * 验证入口节点和端口 - * - * @param tunnelDto 隧道创建DTO - * @return 节点验证结果 - */ - private NodeValidationResult validateInNode(TunnelDto tunnelDto) { - // 验证入口节点是否存在 - Node inNode = nodeService.getById(tunnelDto.getInNodeId()); - if (inNode == null) { - return NodeValidationResult.error(ERROR_IN_NODE_NOT_FOUND); - } - - // 验证入口节点是否在线 - if (inNode.getStatus() != NODE_STATUS_ONLINE) { - return NodeValidationResult.error(ERROR_IN_NODE_OFFLINE); - } - - return NodeValidationResult.success(inNode); - } - - /** - * 构建隧道实体对象 - * - * @param tunnelDto 隧道创建DTO - * @param inNode 入口节点 - * @return 构建完成的隧道对象 - */ - private Tunnel buildTunnelEntity(TunnelDto tunnelDto, Node inNode) { - Tunnel tunnel = new Tunnel(); - BeanUtils.copyProperties(tunnelDto, tunnel); - - // 设置入口节点信息 - tunnel.setInNodeId(tunnelDto.getInNodeId()); - tunnel.setInIp(inNode.getIp()); - - // 设置流量计算类型 - tunnel.setFlow(tunnelDto.getFlow()); - - // 设置流量倍率,如果为空则设置默认值1.0 - if (tunnelDto.getTrafficRatio() != null) { - tunnel.setTrafficRatio(tunnelDto.getTrafficRatio()); - } else { - tunnel.setTrafficRatio(new BigDecimal("1.0")); - } - - // 设置协议类型(仅隧道转发需要) - if (tunnelDto.getType() == TUNNEL_TYPE_TUNNEL_FORWARD) { - // 隧道转发时,设置协议类型,默认为tls - String protocol = StrUtil.isNotBlank(tunnelDto.getProtocol()) ? tunnelDto.getProtocol() : "tls"; - tunnel.setProtocol(protocol); - } else { - // 端口转发时,协议类型为null - tunnel.setProtocol(null); - } - - // 设置TCP和UDP监听地址 - tunnel.setTcpListenAddr(StrUtil.isNotBlank(tunnelDto.getTcpListenAddr()) ? - tunnelDto.getTcpListenAddr() : "0.0.0.0"); - tunnel.setUdpListenAddr(StrUtil.isNotBlank(tunnelDto.getUdpListenAddr()) ? - tunnelDto.getUdpListenAddr() : "0.0.0.0"); - - return tunnel; - } - - /** - * 设置出口节点参数 - * - * @param tunnel 隧道对象 - * @param tunnelDto 隧道创建DTO - * @return 设置结果响应 - */ - private R setupOutNodeParameters(Tunnel tunnel, TunnelDto tunnelDto, String server_ip) { - if (tunnelDto.getType() == TUNNEL_TYPE_PORT_FORWARD) { - // 端口转发:出口参数使用入口参数 - return setupPortForwardOutParameters(tunnel, tunnelDto, server_ip); - } else { - // 隧道转发:需要验证出口参数 - return setupTunnelForwardOutParameters(tunnel, tunnelDto); - } - } - - /** - * 设置端口转发的出口参数 - * - * @param tunnel 隧道对象 - * @param tunnelDto 隧道创建DTO - * @return 设置结果响应 - */ - private R setupPortForwardOutParameters(Tunnel tunnel, TunnelDto tunnelDto, String server_ip) { - tunnel.setOutNodeId(tunnelDto.getInNodeId()); - tunnel.setOutIp(server_ip); - return R.ok(); - } - - /** - * 设置隧道转发的出口参数 - * - * @param tunnel 隧道对象 - * @param tunnelDto 隧道创建DTO - * @return 设置结果响应 - */ - private R setupTunnelForwardOutParameters(Tunnel tunnel, TunnelDto tunnelDto) { - // 验证出口节点不能为空 - if (tunnelDto.getOutNodeId() == null) { - return R.err(ERROR_OUT_NODE_REQUIRED); - } - - // 验证入口和出口不能是同一个节点 - if (tunnelDto.getInNodeId().equals(tunnelDto.getOutNodeId())) { - return R.err(ERROR_SAME_NODE_NOT_ALLOWED); - } - - // 验证协议类型 - String protocol = tunnelDto.getProtocol(); - if (StrUtil.isBlank(protocol)) { - return R.err("协议类型必选"); - } - - // 验证出口节点是否存在 - Node outNode = nodeService.getById(tunnelDto.getOutNodeId()); - if (outNode == null) { - return R.err(ERROR_OUT_NODE_NOT_FOUND); - } - - // 验证出口节点是否在线 - if (outNode.getStatus() != NODE_STATUS_ONLINE) { - return R.err(ERROR_OUT_NODE_OFFLINE); - } - // 设置出口参数 - tunnel.setOutNodeId(tunnelDto.getOutNodeId()); - tunnel.setOutIp(outNode.getServerIp()); - - return R.ok(); - } - - /** - * 设置隧道默认属性 - * - * @param tunnel 隧道对象 - */ - private void setDefaultTunnelProperties(Tunnel tunnel) { - tunnel.setStatus(TUNNEL_STATUS_ACTIVE); - long currentTime = System.currentTimeMillis(); - tunnel.setCreatedTime(currentTime); - tunnel.setUpdatedTime(currentTime); - } - - /** - * 检查隧道是否存在 - * - * @param tunnelId 隧道ID - * @return 隧道是否存在 - */ - private boolean isTunnelExists(Long tunnelId) { - return this.getById(tunnelId) != null; - } - - /** - * 检查隧道使用情况 - * - * @param tunnelId 隧道ID - * @return 检查结果响应 - */ - private R checkTunnelUsage(Long tunnelId) { - // 检查转发使用情况 - R forwardCheckResult = checkForwardUsage(tunnelId); - if (forwardCheckResult.getCode() != 0) { - return forwardCheckResult; - } - - // 检查用户权限使用情况 - return checkUserPermissionUsage(tunnelId); - } - - /** - * 检查转发使用情况 - * - * @param tunnelId 隧道ID - * @return 检查结果响应 - */ - private R checkForwardUsage(Long tunnelId) { - QueryWrapper forwardQuery = new QueryWrapper<>(); - forwardQuery.eq("tunnel_id", tunnelId); - long forwardCount = forwardService.count(forwardQuery); - - if (forwardCount > 0) { - String errorMsg = String.format(ERROR_FORWARDS_IN_USE, forwardCount); - return R.err(errorMsg); - } - - return R.ok(); - } - - /** - * 检查用户权限使用情况 - * - * @param tunnelId 隧道ID - * @return 检查结果响应 - */ - private R checkUserPermissionUsage(Long tunnelId) { - QueryWrapper userTunnelQuery = new QueryWrapper<>(); - userTunnelQuery.eq("tunnel_id", tunnelId); - long userTunnelCount = userTunnelService.count(userTunnelQuery); - - if (userTunnelCount > 0) { - String errorMsg = String.format(ERROR_USER_PERMISSIONS_IN_USE, userTunnelCount); - return R.err(errorMsg); - } - - return R.ok(); - } - - /** - * 获取用户可访问的隧道列表 - * - * @param userInfo 用户信息 - * @return 隧道列表 - */ - private List getUserAccessibleTunnels(UserInfo userInfo) { - if (userInfo.getRoleId() == ADMIN_ROLE_ID) { - // 管理员:获取所有启用状态的隧道 - return getActiveTunnels(); - } else { - // 普通用户:根据权限获取启用状态的隧道 - return getUserAuthorizedTunnels(userInfo.getUserId()); - } - } - - /** - * 获取所有启用状态的隧道 - * - * @return 启用状态的隧道列表 - */ - private List getActiveTunnels() { - return this.list(new QueryWrapper().eq("status", TUNNEL_STATUS_ACTIVE)); - } - - /** - * 获取用户有权限的启用隧道 - * - * @param userId 用户ID - * @return 用户有权限的隧道列表 - */ - private List getUserAuthorizedTunnels(Integer userId) { - List userTunnels = userTunnelMapper.selectList( - new QueryWrapper().eq("user_id", userId) - ); - - if (userTunnels.isEmpty()) { - return java.util.Collections.emptyList(); // 返回空列表 - } - - List tunnelIds = userTunnels.stream() - .map(UserTunnel::getTunnelId) - .collect(Collectors.toList()); - - return this.list(new QueryWrapper() - .in("id", tunnelIds) - .eq("status", TUNNEL_STATUS_ACTIVE)); - } - - /** - * 将隧道实体列表转换为DTO列表 - * - * @param tunnelEntities 隧道实体列表 - * @return 隧道DTO列表 - */ - private List convertToTunnelListDtos(List tunnelEntities) { - return tunnelEntities.stream() - .map(this::convertToTunnelListDto) - .collect(Collectors.toList()); - } - - /** - * 将Tunnel实体转换为TunnelListDto - * - * @param tunnel 隧道实体 - * @return 隧道列表DTO - */ - private TunnelListDto convertToTunnelListDto(Tunnel tunnel) { - TunnelListDto dto = new TunnelListDto(); - dto.setId(tunnel.getId().intValue()); - dto.setName(tunnel.getName()); - dto.setIp(tunnel.getInIp()); - dto.setType(tunnel.getType()); - dto.setProtocol(tunnel.getProtocol()); - - // 获取入口节点的端口范围信息 - if (tunnel.getInNodeId() != null) { - Node inNode = nodeService.getById(tunnel.getInNodeId()); - if (inNode != null) { - dto.setInNodePortSta(inNode.getPortSta()); - dto.setInNodePortEnd(inNode.getPortEnd()); - } - } - - return dto; - } - - /** - * 隧道诊断功能 - * - * @param tunnelId 隧道ID - * @return 诊断结果响应 - */ - @Override - public R diagnoseTunnel(Long tunnelId) { - // 1. 验证隧道是否存在 - Tunnel tunnel = this.getById(tunnelId); - if (tunnel == null) { - return R.err(ERROR_TUNNEL_NOT_FOUND); - } - - // 2. 获取入口和出口节点信息 - Node inNode = nodeService.getById(tunnel.getInNodeId()); - if (inNode == null) { - return R.err(ERROR_IN_NODE_NOT_FOUND); - } - - Node outNode = null; - if (tunnel.getType() == TUNNEL_TYPE_TUNNEL_FORWARD) { - outNode = nodeService.getById(tunnel.getOutNodeId()); - if (outNode == null) { - return R.err(ERROR_OUT_NODE_NOT_FOUND); - } - } - - List results = new ArrayList<>(); - - // 3. 根据隧道类型执行不同的诊断策略 - if (tunnel.getType() == TUNNEL_TYPE_PORT_FORWARD) { - // 端口转发:只给入口节点发送诊断指令,TCP ping谷歌443端口 - DiagnosisResult inResult = performTcpPingDiagnosisWithConnectionCheck(inNode, "www.google.com", 443, "入口->外网"); - results.add(inResult); - } else { - // 隧道转发:入口TCP ping出口,出口TCP ping谷歌443端口 - int outNodePort = getOutNodeTcpPort(tunnel.getId()); - DiagnosisResult inToOutResult = performTcpPingDiagnosisWithConnectionCheck(inNode, outNode.getServerIp(), outNodePort, "入口->出口"); - results.add(inToOutResult); - - // 先检查出口节点的真实连接状态,然后再进行诊断 - DiagnosisResult outToExternalResult = performTcpPingDiagnosisWithConnectionCheck(outNode, "www.google.com", 443, "出口->外网"); - results.add(outToExternalResult); - } - - // 4. 构建诊断报告 - Map diagnosisReport = new HashMap<>(); - diagnosisReport.put("tunnelId", tunnelId); - diagnosisReport.put("tunnelName", tunnel.getName()); - diagnosisReport.put("tunnelType", tunnel.getType() == TUNNEL_TYPE_PORT_FORWARD ? "端口转发" : "隧道转发"); - diagnosisReport.put("results", results); - diagnosisReport.put("timestamp", System.currentTimeMillis()); - - return R.ok(diagnosisReport); - } - - /** - * 获取出口节点的TCP端口 - * 通过隧道ID查找转发服务的出口端口,如果没有则使用默认SSH端口22 - * - * @param tunnelId 隧道ID - * @return TCP端口号 - */ - private int getOutNodeTcpPort(Long tunnelId) { - List forwards = forwardService.list(new QueryWrapper().eq("tunnel_id", tunnelId).eq("status", TUNNEL_STATUS_ACTIVE)); - if (!forwards.isEmpty()) { - return forwards.get(0).getOutPort(); - } - // 如果没有转发服务,使用默认SSH端口22 - return 22; - } - - /** - * 执行TCP ping诊断 - * - * @param node 执行TCP ping的节点 - * @param targetIp 目标IP地址 - * @param port 目标端口 - * @param description 诊断描述 - * @return 诊断结果 - */ - private DiagnosisResult performTcpPingDiagnosis(Node node, String targetIp, int port, String description) { - try { - // 构建TCP ping请求数据 - JSONObject tcpPingData = new JSONObject(); - tcpPingData.put("ip", targetIp); - tcpPingData.put("port", port); - tcpPingData.put("count", 4); - tcpPingData.put("timeout", 5000); // 5秒超时 - - // 发送TCP ping命令到节点 - GostDto gostResult = WebSocketServer.send_msg(node.getId(), tcpPingData, "TcpPing"); - - DiagnosisResult result = new DiagnosisResult(); - result.setNodeId(node.getId()); - result.setNodeName(node.getName()); - result.setTargetIp(targetIp); - result.setTargetPort(port); - result.setDescription(description); - result.setTimestamp(System.currentTimeMillis()); - - if (gostResult != null && "OK".equals(gostResult.getMsg())) { - // 尝试解析TCP ping响应数据 - try { - if (gostResult.getData() != null) { - JSONObject tcpPingResponse = (JSONObject) gostResult.getData(); - boolean success = tcpPingResponse.getBooleanValue("success"); - - result.setSuccess(success); - if (success) { - result.setMessage("TCP连接成功"); - result.setAverageTime(tcpPingResponse.getDoubleValue("averageTime")); - result.setPacketLoss(tcpPingResponse.getDoubleValue("packetLoss")); - } else { - result.setMessage(tcpPingResponse.getString("errorMessage")); - result.setAverageTime(-1.0); - result.setPacketLoss(100.0); - } - } else { - // 没有详细数据,使用默认值 - result.setSuccess(true); - result.setMessage("TCP连接成功"); - result.setAverageTime(0.0); - result.setPacketLoss(0.0); - } - } catch (Exception e) { - // 解析响应数据失败,但TCP ping命令本身成功了 - result.setSuccess(true); - result.setMessage("TCP连接成功,但无法解析详细数据"); - result.setAverageTime(0.0); - result.setPacketLoss(0.0); - } - } else { - result.setSuccess(false); - result.setMessage(gostResult != null ? gostResult.getMsg() : "节点无响应"); - result.setAverageTime(-1.0); - result.setPacketLoss(100.0); - } - - return result; - } catch (Exception e) { - DiagnosisResult result = new DiagnosisResult(); - result.setNodeId(node.getId()); - result.setNodeName(node.getName()); - result.setTargetIp(targetIp); - result.setTargetPort(port); - result.setDescription(description); - result.setSuccess(false); - result.setMessage("诊断执行异常: " + e.getMessage()); - result.setTimestamp(System.currentTimeMillis()); - result.setAverageTime(-1.0); - result.setPacketLoss(100.0); - return result; - } - } - - /** - * 执行TCP ping诊断(带连接状态检查) - * - * @param node 执行TCP ping的节点 - * @param targetIp 目标IP地址 - * @param port 目标端口 - * @param description 诊断描述 - * @return 诊断结果 - */ - private DiagnosisResult performTcpPingDiagnosisWithConnectionCheck(Node node, String targetIp, int port, String description) { - DiagnosisResult result = new DiagnosisResult(); - result.setNodeId(node.getId()); - result.setNodeName(node.getName()); - result.setTargetIp(targetIp); - result.setTargetPort(port); - result.setDescription(description); - result.setTimestamp(System.currentTimeMillis()); - - try { - return performTcpPingDiagnosis(node, targetIp, port, description); - } catch (Exception e) { - result.setSuccess(false); - result.setMessage("连接检查异常: " + e.getMessage()); - result.setAverageTime(-1.0); - result.setPacketLoss(100.0); - return result; - } - } - - - // ========== 内部数据类 ========== - - /** - * 用户信息封装类 - */ - @Data - private static class UserInfo { - private final Integer userId; - private final Integer roleId; - } - - /** - * 节点验证结果封装类 - */ - @Data - private static class NodeValidationResult { - private final boolean hasError; - private final String errorMessage; - private final Node node; - - private NodeValidationResult(boolean hasError, String errorMessage, Node node) { - this.hasError = hasError; - this.errorMessage = errorMessage; - this.node = node; - } - - public static NodeValidationResult success(Node node) { - return new NodeValidationResult(false, null, node); - } - - public static NodeValidationResult error(String errorMessage) { - return new NodeValidationResult(true, errorMessage, null); - } - } - - /** - * 诊断结果数据类 - */ - @Data - public static class DiagnosisResult { - private Long nodeId; - private String nodeName; - private String targetIp; - private Integer targetPort; - private String description; - private boolean success; - private String message; - private double averageTime; - private double packetLoss; - private long timestamp; - } -} +package com.admin.service.impl; + +import com.admin.common.dto.*; + +import com.admin.common.lang.R; +import com.admin.common.utils.GostUtil; +import com.admin.common.utils.JwtUtil; +import com.admin.common.utils.WebSocketServer; +import com.admin.entity.*; +import com.admin.mapper.TunnelMapper; +import com.admin.mapper.UserTunnelMapper; +import com.admin.service.*; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.Data; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.*; +import java.util.stream.Collectors; + +/** + * + * @author QAQ + * @since 2025-06-03 + */ +@Service +public class TunnelServiceImpl extends ServiceImpl implements TunnelService { + + + @Resource + UserTunnelMapper userTunnelMapper; + + @Resource + NodeService nodeService; + + @Resource + ForwardService forwardService; + + @Resource + UserTunnelService userTunnelService; + + @Resource + ChainTunnelService chainTunnelService; + + @Resource + ForwardPortService forwardPortService; + + + @Override + public R createTunnel(TunnelDto tunnelDto) { + + int count = this.count(new QueryWrapper().eq("name", tunnelDto.getName())); + if (count > 0) return R.err("隧道名称重复"); + if (tunnelDto.getType() == 2 && tunnelDto.getOutNodeId() == null) return R.err("出口不能为空"); + + + List chainTunnels = new ArrayList<>(); + Map nodes = new HashMap<>(); + + List node_ids = new ArrayList<>(); + for (ChainTunnel in_node : tunnelDto.getInNodeId()) { + node_ids.add(in_node.getNodeId()); + chainTunnels.add(in_node); + + Node node = nodeService.getById(in_node.getNodeId()); + if (node == null) return R.err("节点不存在"); + nodes.put(node.getId(), node); + } + + if (tunnelDto.getType() == 2) { + // 处理转发链节点,为每一跳设置inx + int inx = 1; + for (List chainNode : tunnelDto.getChainNodes()) { + for (ChainTunnel chain_node : chainNode) { + node_ids.add(chain_node.getNodeId()); + Node node = nodeService.getById(chain_node.getNodeId()); + if (node == null) return R.err("节点不存在"); + nodes.put(node.getId(), node); + Integer nodePort = getNodePort(chain_node.getNodeId(), 1); + chain_node.setPort(nodePort); + chain_node.setInx(inx); // 设置转发链序号 + chainTunnels.add(chain_node); + } + inx++; // 每一跳递增 + } + for (ChainTunnel out_node : tunnelDto.getOutNodeId()) { + node_ids.add(out_node.getNodeId()); + Node node = nodeService.getById(out_node.getNodeId()); + if (node == null) return R.err("节点不存在"); + nodes.put(node.getId(), node); + Integer nodePort = getNodePort(out_node.getNodeId(), 1); + out_node.setPort(nodePort); + chainTunnels.add(out_node); + } + + } + Set set = new HashSet<>(node_ids); + boolean hasDuplicate = set.size() != node_ids.size(); + if (hasDuplicate) return R.err("节点重复"); + + List list = nodeService.list(new QueryWrapper().in("id", node_ids)); + if (list.size() != node_ids.size()) return R.err("部分节点不存在"); + for (Node node : list) { + if (node.getStatus() != 1) return R.err("部分节点不在线"); + } + + + Tunnel tunnel = new Tunnel(); + BeanUtils.copyProperties(tunnelDto, tunnel); + tunnel.setStatus(1); + long currentTime = System.currentTimeMillis(); + tunnel.setCreatedTime(currentTime); + tunnel.setUpdatedTime(currentTime); + if (StringUtils.isEmpty(tunnel.getInIp())){ + StringBuilder in_ip = new StringBuilder(); + for (ChainTunnel chainTunnel : tunnelDto.getInNodeId()) { + Node node = nodes.get(chainTunnel.getNodeId()); + in_ip.append(node.getServerIp()).append(","); + } + in_ip.deleteCharAt(in_ip.length() - 1); + tunnel.setInIp(in_ip.toString()); + } + + this.save(tunnel); + for (ChainTunnel chainTunnel : chainTunnels) { + chainTunnel.setTunnelId(tunnel.getId()); + } + chainTunnelService.saveBatch(chainTunnels); + + if (tunnel.getType() == 2) { + + for (ChainTunnel in_node : tunnelDto.getInNodeId()) { + // 创建Chain, 指向chainNode的第一跳。如果chainNode为空就是指向出口 + if (tunnelDto.getChainNodes().isEmpty()) { // 指向出口 + GostUtil.AddChains(in_node.getNodeId(), tunnelDto.getOutNodeId(), nodes); + } else { + GostUtil.AddChains(in_node.getNodeId(), tunnelDto.getChainNodes().getFirst(), nodes);// 指向第一跳 + } + } + + for (int i = 0; i < tunnelDto.getChainNodes().size(); i++) { + // 创建Chain和Service。每一条的Chain都是指向下一跳。最后一跳指向出口, Service是监听端口 + List chainTunnels1 = tunnelDto.getChainNodes().get(i); + for (ChainTunnel chainTunnel : chainTunnels1) { + int inx = i+1; + if (inx >= tunnelDto.getChainNodes().size()) { // 指向出口 + GostUtil.AddChains(chainTunnel.getNodeId(), tunnelDto.getOutNodeId(), nodes); + } else { + GostUtil.AddChains(chainTunnel.getNodeId(), tunnelDto.getChainNodes().get(inx), nodes); + } + + GostUtil.AddChainService(chainTunnel.getNodeId(), chainTunnel, nodes); + } + + } + + + for (ChainTunnel out_node : tunnelDto.getOutNodeId()) { + GostUtil.AddChainService(out_node.getNodeId(), out_node, nodes); + } + + } + return R.ok(); + } + + + @Override + public R getAllTunnels() { + List tunnelList = this.list(); + + // 查询所有隧道的ChainTunnel信息 + List tunnelIds = tunnelList.stream() + .map(Tunnel::getId) + .collect(Collectors.toList()); + + if (tunnelIds.isEmpty()) { + return R.ok(new ArrayList()); + } + + // 批量查询所有ChainTunnel记录 + List allChainTunnels = chainTunnelService.list( + new QueryWrapper().in("tunnel_id", tunnelIds) + ); + + // 按tunnelId分组 + Map> chainTunnelMap = allChainTunnels.stream() + .collect(Collectors.groupingBy(ChainTunnel::getTunnelId)); + + // 转换为TunnelDetailDto列表 + List detailDtoList = tunnelList.stream() + .map(tunnel -> { + TunnelDetailDto detailDto = new TunnelDetailDto(); + BeanUtils.copyProperties(tunnel, detailDto); + + List chainTunnels = chainTunnelMap.getOrDefault(tunnel.getId(), new ArrayList<>()); + + // 按chainType分类节点 + // 入口节点 (chainType = 1) + List inNodes = chainTunnels.stream() + .filter(ct -> ct.getChainType() != null && ct.getChainType() == 1) + .collect(Collectors.toList()); + detailDto.setInNodeId(inNodes); + + detailDto.setInIp(tunnel.getInIp()); + + // 转发链节点 (chainType = 2) - 按inx分组 + Map> chainNodesMap = chainTunnels.stream() + .filter(ct -> ct.getChainType() != null && ct.getChainType() == 2) + .collect(Collectors.groupingBy( + ct -> ct.getInx() != null ? ct.getInx() : 0, + Collectors.toList() + )); + + // 将Map转换为按inx排序的二维列表 + List> chainNodesList = chainNodesMap.entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .map(Map.Entry::getValue) + .collect(Collectors.toList()); + detailDto.setChainNodes(chainNodesList); + + // 出口节点 (chainType = 3) + List outNodes = chainTunnels.stream() + .filter(ct -> ct.getChainType() != null && ct.getChainType() == 3) + .collect(Collectors.toList()); + detailDto.setOutNodeId(outNodes); + + return detailDto; + }) + .collect(Collectors.toList()); + + return R.ok(detailDtoList); + } + + + @Override + public R updateTunnel(TunnelUpdateDto tunnelUpdateDto) { + Tunnel existingTunnel = this.getById(tunnelUpdateDto.getId()); + if (existingTunnel == null) return R.err("隧道不存在"); + Tunnel tunnel = new Tunnel(); + tunnel.setId(tunnelUpdateDto.getId()); + tunnel.setName(tunnelUpdateDto.getName()); + tunnel.setFlow(tunnelUpdateDto.getFlow()); + tunnel.setTrafficRatio(tunnelUpdateDto.getTrafficRatio()); + tunnel.setInIp(tunnelUpdateDto.getInIp()); + + if (StringUtils.isEmpty(tunnel.getInIp())){ + StringBuilder in_ip = new StringBuilder(); + List chainTunnels = chainTunnelService.list(new QueryWrapper().eq("tunnel_id", tunnel.getId()).eq("chain_type", 1)); + for (ChainTunnel chainTunnel : chainTunnels) { + Node node = nodeService.getById(chainTunnel.getNodeId()); + if (node == null)return R.err("隧道节点数据错误,部分节点不存在"); + in_ip.append(node.getServerIp()).append(","); + } + in_ip.deleteCharAt(in_ip.length() - 1); + tunnel.setInIp(in_ip.toString()); + } + + this.updateById(tunnel); + return R.ok(); + } + + + @Override + public R deleteTunnel(Long id) { + Tunnel tunnel = this.getById(id); + if (tunnel == null) return R.err("隧道不存在"); + List forwardList = forwardService.list(new QueryWrapper().eq("tunnel_id", id)); + for (Forward forward : forwardList) { + forwardService.deleteForward(forward.getId()); + } + forwardService.remove(new QueryWrapper().eq("tunnel_id", id)); + userTunnelService.remove(new QueryWrapper().eq("tunnel_id", id)); + this.removeById(id); + + List chainTunnels = chainTunnelService.list(new QueryWrapper().eq("tunnel_id", id)); + for (ChainTunnel chainTunnel : chainTunnels) { + if (chainTunnel.getChainType() == 1){ // 入口 + GostUtil.DeleteChains(chainTunnel.getNodeId(), "chains_" + chainTunnel.getTunnelId()); + } + else if (chainTunnel.getChainType() == 2){ // 链 + GostUtil.DeleteChains(chainTunnel.getNodeId(), "chains_" + chainTunnel.getTunnelId()); + JSONArray services = new JSONArray(); + services.add(chainTunnel.getTunnelId() + "_tls"); + GostUtil.DeleteChainService(chainTunnel.getNodeId(), services); + } + else { // 出口 + JSONArray services = new JSONArray(); + services.add(chainTunnel.getTunnelId() + "_tls"); + GostUtil.DeleteChainService(chainTunnel.getNodeId(), services); + } + } + chainTunnelService.remove(new QueryWrapper().eq("tunnel_id", id)); + return R.ok(); + } + + + @Override + public R userTunnel() { + List tunnelEntities; + Integer roleId = JwtUtil.getRoleIdFromToken(); + Integer userId = JwtUtil.getUserIdFromToken(); + if (roleId == 0) { + tunnelEntities = this.list(new QueryWrapper().eq("status", 1)); + } else { + tunnelEntities = java.util.Collections.emptyList(); // 返回空列表 + List userTunnels = userTunnelMapper.selectList( + new QueryWrapper().eq("user_id", userId) + ); + if (!userTunnels.isEmpty()) { + List tunnelIds = userTunnels.stream() + .map(UserTunnel::getTunnelId) + .collect(Collectors.toList()); + tunnelEntities = this.list(new QueryWrapper() + .in("id", tunnelIds) + .eq("status", 1)); + } + + } + return R.ok(tunnelEntities); + } + + + @Override + public R diagnoseTunnel(Long tunnelId) { + Tunnel tunnel = this.getById(tunnelId); + if (tunnel == null) { + return R.err("隧道不存在"); + } + + List chainTunnels = chainTunnelService.list( + new QueryWrapper().eq("tunnel_id", tunnelId) + ); + + if (chainTunnels.isEmpty()) { + return R.err("隧道配置不完整"); + } + + List inNodes = chainTunnels.stream() + .filter(ct -> ct.getChainType() == 1) + .toList(); + + Map> chainNodesMap = chainTunnels.stream() + .filter(ct -> ct.getChainType() == 2) + .collect(Collectors.groupingBy( + ct -> ct.getInx() != null ? ct.getInx() : 0, + Collectors.toList() + )); + + List> chainNodesList = chainNodesMap.entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .map(Map.Entry::getValue) + .toList(); + + List outNodes = chainTunnels.stream() + .filter(ct -> ct.getChainType() == 3) + .toList(); + + List results = new ArrayList<>(); + + if (tunnel.getType() == 1) { + for (ChainTunnel inNode : inNodes) { + Node node = nodeService.getById(inNode.getNodeId()); + if (node != null) { + DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck( + node, "www.google.com", 443, "入口(" + node.getName() + ")->外网" + ); + result.setFromChainType(1); // 入口 + results.add(result); + } + } + } else if (tunnel.getType() == 2) { + for (ChainTunnel inNode : inNodes) { + Node fromNode = nodeService.getById(inNode.getNodeId()); + + if (fromNode != null) { + if (!chainNodesList.isEmpty()) { + for (ChainTunnel firstChainNode : chainNodesList.getFirst()) { + Node toNode = nodeService.getById(firstChainNode.getNodeId()); + if (toNode != null) { + DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck( + fromNode, toNode.getServerIp(), firstChainNode.getPort(), + "入口(" + fromNode.getName() + ")->第1跳(" + toNode.getName() + ")" + ); + result.setFromChainType(1); // 入口 + result.setToChainType(2); // 链 + result.setToInx(firstChainNode.getInx()); + results.add(result); + } + } + } else if (!outNodes.isEmpty()) { + for (ChainTunnel outNode : outNodes) { + Node toNode = nodeService.getById(outNode.getNodeId()); + if (toNode != null) { + DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck( + fromNode, toNode.getServerIp(), outNode.getPort(), + "入口(" + fromNode.getName() + ")->出口(" + toNode.getName() + ")" + ); + result.setFromChainType(1); + result.setToChainType(3); + results.add(result); + } + } + } + } + } + + for (int i = 0; i < chainNodesList.size(); i++) { + List currentHop = chainNodesList.get(i); + + for (ChainTunnel currentNode : currentHop) { + Node fromNode = nodeService.getById(currentNode.getNodeId()); + + if (fromNode != null) { + if (i + 1 < chainNodesList.size()) { + for (ChainTunnel nextNode : chainNodesList.get(i + 1)) { + Node toNode = nodeService.getById(nextNode.getNodeId()); + if (toNode != null) { + DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck( + fromNode, toNode.getServerIp(), nextNode.getPort(), + "第" + (i + 1) + "跳(" + fromNode.getName() + ")->第" + (i + 2) + "跳(" + toNode.getName() + ")" + ); + result.setFromChainType(2); + result.setFromInx(currentNode.getInx()); + result.setToChainType(2); + result.setToInx(nextNode.getInx()); + results.add(result); + } + } + } else if (!outNodes.isEmpty()) { + for (ChainTunnel outNode : outNodes) { + Node toNode = nodeService.getById(outNode.getNodeId()); + if (toNode != null) { + DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck( + fromNode, toNode.getServerIp(), outNode.getPort(), + "第" + (i + 1) + "跳(" + fromNode.getName() + ")->出口(" + toNode.getName() + ")" + ); + result.setFromChainType(2); + result.setFromInx(currentNode.getInx()); + result.setToChainType(3); + results.add(result); + } + } + } + } + } + } + for (ChainTunnel outNode : outNodes) { + Node node = nodeService.getById(outNode.getNodeId()); + if (node != null) { + DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck( + node, "www.google.com", 443, "出口(" + node.getName() + ")->外网" + ); + result.setFromChainType(3); + results.add(result); + } + } + } + + Map diagnosisReport = new HashMap<>(); + diagnosisReport.put("tunnelId", tunnelId); + diagnosisReport.put("tunnelName", tunnel.getName()); + diagnosisReport.put("tunnelType", tunnel.getType() == 1 ? "端口转发" : "隧道转发"); + diagnosisReport.put("results", results); + diagnosisReport.put("timestamp", System.currentTimeMillis()); + + return R.ok(diagnosisReport); + } + + @Override + public Integer getNodePort(Long nodeId,Integer type) { + + Node node = nodeService.getById(nodeId); + if (node == null){ + throw new RuntimeException("节点不存在"); + } + + // 1. 查询隧道转发链占用的端口 + List chainTunnels = chainTunnelService.list( + new QueryWrapper().eq("node_id", nodeId) + ); + Set usedPorts = chainTunnels.stream() + .map(ChainTunnel::getPort) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + + List list = forwardPortService.list(new QueryWrapper().eq("node_id", nodeId)); + Set forwardUsedPorts = new HashSet<>(); + for (ForwardPort forwardPort : list) { + forwardUsedPorts.add(forwardPort.getPort()); + } + usedPorts.addAll(forwardUsedPorts); + + // 3. 从可用端口范围中筛选未被占用的端口 + List parsedPorts = parsePorts(node.getPort()); + List availablePorts = parsedPorts.stream() + .filter(p -> !usedPorts.contains(p)) + .toList(); + + if (availablePorts.isEmpty()) { + throw new RuntimeException("节点端口已满,无可用端口"); + } + if (type == 1) { + return availablePorts.getLast(); + }else { + return availablePorts.getFirst(); + } + } + + public static List parsePorts(String input) { + Set set = new HashSet<>(); + String[] parts = input.split(","); + for (String part : parts) { + part = part.trim(); + if (part.contains("-")) { + String[] range = part.split("-"); + int start = Integer.parseInt(range[0]); + int end = Integer.parseInt(range[1]); + for (int i = start; i <= end; i++) { + set.add(i); + } + } else { + set.add(Integer.parseInt(part)); + } + } + return set.stream().sorted().collect(Collectors.toList()); + } + + private DiagnosisResult performTcpPingDiagnosis(Node node, String targetIp, int port, String description) { + try { + // 构建TCP ping请求数据 + JSONObject tcpPingData = new JSONObject(); + tcpPingData.put("ip", targetIp); + tcpPingData.put("port", port); + tcpPingData.put("count", 4); + tcpPingData.put("timeout", 5000); // 5秒超时 + + // 发送TCP ping命令到节点 + GostDto gostResult = WebSocketServer.send_msg(node.getId(), tcpPingData, "TcpPing"); + + DiagnosisResult result = new DiagnosisResult(); + result.setNodeId(node.getId()); + result.setNodeName(node.getName()); + result.setTargetIp(targetIp); + result.setTargetPort(port); + result.setDescription(description); + result.setTimestamp(System.currentTimeMillis()); + + if (gostResult != null && "OK".equals(gostResult.getMsg())) { + // 尝试解析TCP ping响应数据 + try { + if (gostResult.getData() != null) { + JSONObject tcpPingResponse = (JSONObject) gostResult.getData(); + boolean success = tcpPingResponse.getBooleanValue("success"); + + result.setSuccess(success); + if (success) { + result.setMessage("TCP连接成功"); + result.setAverageTime(tcpPingResponse.getDoubleValue("averageTime")); + result.setPacketLoss(tcpPingResponse.getDoubleValue("packetLoss")); + } else { + result.setMessage(tcpPingResponse.getString("errorMessage")); + result.setAverageTime(-1.0); + result.setPacketLoss(100.0); + } + } else { + // 没有详细数据,使用默认值 + result.setSuccess(true); + result.setMessage("TCP连接成功"); + result.setAverageTime(0.0); + result.setPacketLoss(0.0); + } + } catch (Exception e) { + // 解析响应数据失败,但TCP ping命令本身成功了 + result.setSuccess(true); + result.setMessage("TCP连接成功,但无法解析详细数据"); + result.setAverageTime(0.0); + result.setPacketLoss(0.0); + } + } else { + result.setSuccess(false); + result.setMessage(gostResult != null ? gostResult.getMsg() : "节点无响应"); + result.setAverageTime(-1.0); + result.setPacketLoss(100.0); + } + + return result; + } catch (Exception e) { + DiagnosisResult result = new DiagnosisResult(); + result.setNodeId(node.getId()); + result.setNodeName(node.getName()); + result.setTargetIp(targetIp); + result.setTargetPort(port); + result.setDescription(description); + result.setSuccess(false); + result.setMessage("诊断执行异常: " + e.getMessage()); + result.setTimestamp(System.currentTimeMillis()); + result.setAverageTime(-1.0); + result.setPacketLoss(100.0); + return result; + } + } + + private DiagnosisResult performTcpPingDiagnosisWithConnectionCheck(Node node, String targetIp, int port, String description) { + DiagnosisResult result = new DiagnosisResult(); + result.setNodeId(node.getId()); + result.setNodeName(node.getName()); + result.setTargetIp(targetIp); + result.setTargetPort(port); + result.setDescription(description); + result.setTimestamp(System.currentTimeMillis()); + + try { + return performTcpPingDiagnosis(node, targetIp, port, description); + } catch (Exception e) { + result.setSuccess(false); + result.setMessage("连接检查异常: " + e.getMessage()); + result.setAverageTime(-1.0); + result.setPacketLoss(100.0); + return result; + } + } + + +} diff --git a/springboot-backend/src/main/java/com/admin/service/impl/UserServiceImpl.java b/springboot-backend/src/main/java/com/admin/service/impl/UserServiceImpl.java index edcaf9a..af7c34e 100644 --- a/springboot-backend/src/main/java/com/admin/service/impl/UserServiceImpl.java +++ b/springboot-backend/src/main/java/com/admin/service/impl/UserServiceImpl.java @@ -10,9 +10,7 @@ import com.admin.common.utils.GostUtil; import com.admin.common.utils.JwtUtil; import com.admin.common.utils.Md5Util; import com.admin.entity.*; -import com.admin.mapper.ForwardMapper; import com.admin.mapper.UserMapper; -import com.admin.mapper.UserTunnelMapper; import com.admin.service.*; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; @@ -20,6 +18,7 @@ import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; @@ -28,83 +27,25 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; -/** - *

- * 用户服务实现类 - * 提供用户的增删改查功能,包括用户登录、创建、更新、删除和套餐信息查询 - * 支持用户关联数据的级联删除,包括转发和Gost服务的清理 - *

- * - * @author QAQ - * @since 2025-06-03 - */ + @Slf4j @Service public class UserServiceImpl extends ServiceImpl implements UserService { - // ========== 常量定义 ========== - - /** 用户角色常量 */ - private static final int ADMIN_ROLE_ID = 0; // 管理员角色ID - private static final int USER_ROLE_ID = 1; // 普通用户角色ID - - /** 用户状态常量 */ - private static final int USER_STATUS_ACTIVE = 1; // 用户启用状态 - private static final int USER_STATUS_DISABLED = 0; // 用户停用状态 - - /** 隧道类型常量 */ - private static final int TUNNEL_TYPE_TUNNEL_FORWARD = 2; // 隧道转发类型 - - /** 成功响应消息 */ - private static final String SUCCESS_CREATE_MSG = "用户创建成功"; - private static final String SUCCESS_UPDATE_MSG = "用户更新成功"; - private static final String SUCCESS_DELETE_MSG = "用户及关联数据删除成功"; - - /** 错误响应消息 */ - private static final String ERROR_LOGIN_CREDENTIALS = "账号或密码错误"; - private static final String ERROR_ACCOUNT_DISABLED = "账户停用"; - private static final String ERROR_CREATE_FAILED = "用户创建失败"; - private static final String ERROR_UPDATE_FAILED = "用户更新失败"; - private static final String ERROR_DELETE_FAILED = "用户删除失败"; - private static final String ERROR_USER_NOT_FOUND = "用户不存在"; - private static final String ERROR_USERNAME_EXISTS = "用户名已存在"; - private static final String ERROR_USERNAME_TAKEN = "用户名已被其他用户使用"; - private static final String ERROR_CANNOT_DELETE_ADMIN = "不能删除管理员用户"; - private static final String ERROR_CANNOT_UPDATE_ADMIN = "不能修改管理员用户信息"; - private static final String ERROR_USER_NOT_LOGGED_IN = "用户未登录或token无效"; - private static final String ERROR_GET_PACKAGE_INFO_FAILED = "获取套餐信息失败"; - private static final String ERROR_CURRENT_PASSWORD_WRONG = "当前密码错误"; - private static final String ERROR_PASSWORD_NOT_MATCH = "新密码和确认密码不匹配"; + @Resource + @Lazy + ForwardService forwardService; - /** 默认账号密码 */ - private static final String DEFAULT_USERNAME = "admin_user"; - private static final String DEFAULT_PASSWORD = "admin_user"; - - /** 登录响应字段名 */ - private static final String LOGIN_TOKEN_FIELD = "token"; - private static final String LOGIN_NAME_FIELD = "name"; - private static final String LOGIN_ROLE_ID_FIELD = "role_id"; - private static final String LOGIN_REQUIRE_PASSWORD_CHANGE_FIELD = "requirePasswordChange"; + @Resource + UserMapper userMapper; - // ========== 依赖注入 ========== - @Resource - private UserMapper userMapper; + @Lazy + TunnelService tunnelService; @Resource @Lazy - private ForwardMapper forwardMapper; - - @Resource - private UserTunnelMapper userTunnelMapper; - - @Resource - @Lazy - private TunnelService tunnelService; - - @Resource - @Lazy - private NodeService nodeService; + NodeService nodeService; @Resource UserTunnelService userTunnelService; @@ -116,21 +57,15 @@ public class UserServiceImpl extends ServiceImpl implements Us StatisticsFlowService statisticsFlowService; @Resource - private ImageCaptchaApplication application; + @Lazy + ForwardPortService forwardPortService; + + @Resource + ImageCaptchaApplication application; - // ========== 公共接口实现 ========== - /** - * 用户登录 - * 验证验证码、用户名密码,检查账户状态,生成JWT令牌 - * - * @param loginDto 登录数据传输对象 - * @return 登录结果响应,包含令牌和用户信息 - */ @Override public R login(LoginDto loginDto) { - - // 1. 验证验证码 ViteConfig viteConfig = viteConfigService.getOne(new QueryWrapper().eq("name", "captcha_enabled")); if (viteConfig != null && Objects.equals(viteConfig.getValue(), "true")) { if (StringUtils.isBlank(loginDto.getCaptchaId())) return R.err("验证码校验失败"); @@ -138,216 +73,129 @@ public class UserServiceImpl extends ServiceImpl implements Us if (!valid) return R.err("验证码校验失败"); } - - - // 2. 验证用户凭据 - LoginValidationResult validationResult = validateUserCredentials(loginDto); - if (validationResult.isHasError()) { - return R.err(validationResult.getErrorMessage()); - } - - // 3. 生成令牌并返回用户信息 - User user = validationResult.getUser(); + User user = this.getOne(new QueryWrapper().eq("user", loginDto.getUsername())); + if (user == null) return R.err("账号或密码错误"); + if (!user.getPwd().equals(Md5Util.md5(loginDto.getPassword()))) return R.err("账号或密码错误"); + if (user.getStatus() == 0) return R.err("账号被停用"); String token = JwtUtil.generateToken(user); - - // 4. 检查是否使用默认账号密码 - boolean requirePasswordChange = isDefaultCredentials(loginDto.getUsername(), loginDto.getPassword()); - + boolean requirePasswordChange = Objects.equals(loginDto.getUsername(), "admin_user") || Objects.equals(loginDto.getPassword(), "admin_user"); return R.ok(MapUtil.builder() - .put(LOGIN_TOKEN_FIELD, token) - .put(LOGIN_NAME_FIELD, user.getUser()) - .put(LOGIN_ROLE_ID_FIELD, user.getRoleId()) - .put(LOGIN_REQUIRE_PASSWORD_CHANGE_FIELD, requirePasswordChange) + .put("token", token) + .put("name", user.getUser()) + .put("role_id", user.getRoleId()) + .put("requirePasswordChange", requirePasswordChange) .build()); } - /** - * 创建用户 - * 检查用户名唯一性,设置默认属性,加密密码 - * - * @param userDto 用户创建数据传输对象 - * @return 创建结果响应 - */ @Override public R createUser(UserDto userDto) { - // 1. 验证用户名唯一性 - R usernameValidationResult = validateUsernameUniqueness(userDto.getUser(), null); - if (usernameValidationResult.getCode() != 0) { - return usernameValidationResult; - } - - // 2. 构建用户实体并保存 - User user = buildNewUserEntity(userDto); - boolean result = this.save(user); - - if (result) { - // 3. 添加到期时间延时任务 - return R.ok(SUCCESS_CREATE_MSG); - } else { - return R.err(ERROR_CREATE_FAILED); - } + int count = this.count(new QueryWrapper().eq("user", userDto.getUser())); + if (count > 0) return R.err("用户名已存在"); + User user = new User(); + BeanUtils.copyProperties(userDto, user); + user.setPwd(Md5Util.md5(userDto.getPwd())); + user.setStatus(1); + user.setRoleId(1); + long currentTime = System.currentTimeMillis(); + user.setCreatedTime(currentTime); + user.setUpdatedTime(currentTime); + this.save(user); + return R.ok(); } - /** - * 获取所有用户(分页) - * 支持关键字搜索,排除管理员用户,清除密码信息 - * - * @return 分页用户列表响应 - */ @Override public R getAllUsers() { - return R.ok(this.list(new QueryWrapper().ne("role_id", ADMIN_ROLE_ID))); + List list = this.list(new QueryWrapper().ne("role_id", 0)); + return R.ok(list); } - /** - * 更新用户信息 - * 验证用户存在性和用户名唯一性,处理密码加密 - * - * @param userUpdateDto 用户更新数据传输对象 - * @return 更新结果响应 - */ @Override public R updateUser(UserUpdateDto userUpdateDto) { - // 1. 验证用户是否存在 - if (!isUserExists(userUpdateDto.getId())) { - return R.err(ERROR_USER_NOT_FOUND); - } + User user = this.getById(userUpdateDto.getId()); + if (user == null) return R.err("用户不存在"); + if (user.getRoleId() == 0) return R.err("请不要作死"); - // 2. 验证用户名唯一性 - R usernameValidationResult = validateUsernameUniqueness(userUpdateDto.getUser(), userUpdateDto.getId()); - if (usernameValidationResult.getCode() != 0) { - return usernameValidationResult; - } + int count = this.count(new QueryWrapper().eq("user", userUpdateDto.getUser()).ne("id", userUpdateDto.getId())); + if (count > 0) return R.err("用户名已存在"); - // 3. 验证更新条件 - R updateValidationResult = validateUserUpdate(userUpdateDto.getId()); - if (updateValidationResult.getCode() != 0) { - return updateValidationResult; - } - // 4. 构建更新实体并保存 - User updateUser = buildUpdateUserEntity(userUpdateDto); - boolean result = this.updateById(updateUser); - - if (result) { - // 5. 处理到期时间延时任务 - return R.ok(SUCCESS_UPDATE_MSG); + User updateUser = new User(); + BeanUtils.copyProperties(userUpdateDto, updateUser); + if (StrUtil.isNotBlank(userUpdateDto.getPwd())) { + updateUser.setPwd(Md5Util.md5(userUpdateDto.getPwd())); } else { - return R.err(ERROR_UPDATE_FAILED); + updateUser.setPwd(null); // 不更新密码字段 } + updateUser.setUpdatedTime(System.currentTimeMillis()); + this.updateById(updateUser); + return R.ok(); } - /** - * 删除用户 - * 级联删除用户相关的所有数据,包括转发、Gost服务和隧道权限 - * - * @param id 用户ID - * @return 删除结果响应 - */ @Override public R deleteUser(Long id) { - // 1. 验证删除条件 - R deleteValidationResult = validateUserDeletion(id); - if (deleteValidationResult.getCode() != 0) { - return deleteValidationResult; - } - - try { - // 2. 级联删除用户相关数据 - deleteUserRelatedData(id); - statisticsFlowService.remove(new QueryWrapper().eq("user_id", id)); - // 3. 删除用户 - boolean result = this.removeById(id); - return result ? R.ok(SUCCESS_DELETE_MSG) : R.err(ERROR_DELETE_FAILED); - - } catch (Exception e) { - e.printStackTrace(); - return R.err("删除用户时发生错误:" + e.getMessage()); + User user = this.getById(id); + if (user == null) return R.err("用户不存在"); + if (user.getRoleId() == 0) return R.err("请不要作死"); + List forwardList = forwardService.list(new QueryWrapper().eq("user_id", id)); + for (Forward forward : forwardList) { + forwardService.deleteForward(forward.getId()); } + forwardService.remove(new QueryWrapper().eq("user_id", id)); + userTunnelService.remove(new QueryWrapper().eq("user_id", id)); + statisticsFlowService.remove(new QueryWrapper().eq("user_id", id)); + this.removeById(id); + return R.ok(); } - /** - * 获取用户套餐信息 - * 包括用户基本信息、隧道权限详情和转发详情 - * - * @return 用户套餐信息响应 - */ @Override public R getUserPackageInfo() { - try { - // 1. 获取当前用户信息 - CurrentUserInfo currentUser = getCurrentUserInfo(); - if (currentUser.isHasError()) { - return R.err(currentUser.getErrorMessage()); - } - - // 2. 构建套餐信息 - UserPackageDto packageDto = buildUserPackageDto(currentUser); - - return R.ok(packageDto); - } catch (Exception e) { - e.printStackTrace(); - return R.err(ERROR_GET_PACKAGE_INFO_FAILED); - } + Integer userId = JwtUtil.getUserIdFromToken(); + User user = this.getById(userId); + if (user == null) return R.err("用户不存在"); + UserPackageDto.UserInfoDto userInfo = buildUserInfoDto(user); + List tunnelPermissions = userMapper.getUserTunnelDetails(userId); + List forwards = userMapper.getUserForwardDetails(user.getId().intValue()); + fillForwardInIpAndPort(forwards); + List statisticsFlows = getLast24HoursFlowStatistics(user.getId()); + UserPackageDto packageDto = new UserPackageDto(); + packageDto.setUserInfo(userInfo); + packageDto.setTunnelPermissions(tunnelPermissions); + packageDto.setForwards(forwards); + packageDto.setStatisticsFlows(statisticsFlows); + return R.ok(packageDto); } - /** - * 修改账号密码 - * 验证当前密码、新密码确认、用户名唯一性、更新用户账号密码 - * - * @param changePasswordDto 修改账号密码数据传输对象 - * @return 修改结果响应 - */ @Override public R updatePassword(ChangePasswordDto changePasswordDto) { - try { - // 1. 获取当前用户信息 - CurrentUserInfo currentUser = getCurrentUserInfo(); - if (currentUser.isHasError()) { - return R.err(currentUser.getErrorMessage()); - } - - // 2. 验证新密码和确认密码是否匹配 - if (!changePasswordDto.getNewPassword().equals(changePasswordDto.getConfirmPassword())) { - return R.err(ERROR_PASSWORD_NOT_MATCH); - } - - // 3. 验证当前密码是否正确 - User user = currentUser.getUser(); - String currentPasswordMd5 = Md5Util.md5(changePasswordDto.getCurrentPassword()); - if (!user.getPwd().equals(currentPasswordMd5)) { - return R.err(ERROR_CURRENT_PASSWORD_WRONG); - } - - // 4. 验证新用户名唯一性(如果与当前用户名不同) - if (!user.getUser().equals(changePasswordDto.getNewUsername())) { - R usernameValidationResult = validateUsernameUniqueness(changePasswordDto.getNewUsername(), user.getId()); - if (usernameValidationResult.getCode() != 0) { - return usernameValidationResult; - } - } - - // 5. 更新用户名和密码 - User updateUser = new User(); - updateUser.setId(user.getId()); - updateUser.setUser(changePasswordDto.getNewUsername()); - updateUser.setPwd(Md5Util.md5(changePasswordDto.getNewPassword())); - updateUser.setUpdatedTime(System.currentTimeMillis()); - - boolean result = this.updateById(updateUser); - return result ? R.ok("账号密码修改成功") : R.err(ERROR_UPDATE_FAILED); - - } catch (Exception e) { - e.printStackTrace(); - return R.err("修改账号密码时发生错误:" + e.getMessage()); + Integer userId = JwtUtil.getUserIdFromToken(); + User user = this.getById(userId); + if (user == null) return R.err("用户不存在"); + if (!changePasswordDto.getNewPassword().equals(changePasswordDto.getConfirmPassword())) { + return R.err("新密码和确认密码不匹配"); } + String currentPasswordMd5 = Md5Util.md5(changePasswordDto.getCurrentPassword()); + if (!user.getPwd().equals(currentPasswordMd5)) { + return R.err("当前密码错误"); + } + if (!user.getUser().equals(changePasswordDto.getNewUsername())) { + user.setPwd(Md5Util.md5(changePasswordDto.getNewPassword())); + int count = this.count(new QueryWrapper().eq("user", changePasswordDto.getNewUsername()).ne("id", user.getId())); + if (count > 0) return R.err("用户名已存在"); + } + User updateUser = new User(); + updateUser.setId(user.getId()); + updateUser.setUser(changePasswordDto.getNewUsername()); + updateUser.setPwd(Md5Util.md5(changePasswordDto.getNewPassword())); + updateUser.setUpdatedTime(System.currentTimeMillis()); + this.updateById(updateUser); + return R.ok(); } @Override public R reset(ResetFlowDto resetFlowDto) { if (resetFlowDto.getType() == 1){ // 清零账号流量 User user = this.getById(resetFlowDto.getId()); - if (user == null) return R.err(ERROR_USER_NOT_FOUND); + if (user == null) return R.err("用户不存在"); user.setInFlow(0L); user.setOutFlow(0L); this.updateById(user); @@ -361,339 +209,6 @@ public class UserServiceImpl extends ServiceImpl implements Us return R.ok(); } - // ========== 私有辅助方法 ========== - - - /** - * 验证用户登录凭据 - * - * @param loginDto 登录数据传输对象 - * @return 登录验证结果 - */ - private LoginValidationResult validateUserCredentials(LoginDto loginDto) { - User user = this.getOne(new QueryWrapper().eq("user", loginDto.getUsername())); - if (user == null) { - return LoginValidationResult.error(ERROR_LOGIN_CREDENTIALS); - } - - if (!user.getPwd().equals(Md5Util.md5(loginDto.getPassword()))) { - return LoginValidationResult.error(ERROR_LOGIN_CREDENTIALS); - } - - if (user.getStatus() == USER_STATUS_DISABLED) { - return LoginValidationResult.error(ERROR_ACCOUNT_DISABLED); - } - - return LoginValidationResult.success(user); - } - - /** - * 检查是否使用默认账号密码 - * - * @param username 用户名 - * @param password 密码 - * @return 是否是默认凭据 - */ - private boolean isDefaultCredentials(String username, String password) { - return DEFAULT_USERNAME.equals(username) || DEFAULT_PASSWORD.equals(password); - } - - /** - * 验证用户名唯一性 - * - * @param username 用户名 - * @param excludeUserId 排除的用户ID(用于更新时排除自己) - * @return 验证结果响应 - */ - private R validateUsernameUniqueness(String username, Long excludeUserId) { - QueryWrapper queryWrapper = new QueryWrapper().eq("user", username); - if (excludeUserId != null) { - queryWrapper.ne("id", excludeUserId); - } - - User existUser = this.getOne(queryWrapper); - if (existUser != null) { - String errorMsg = excludeUserId != null ? ERROR_USERNAME_TAKEN : ERROR_USERNAME_EXISTS; - return R.err(errorMsg); - } - - return R.ok(); - } - - /** - * 构建新用户实体对象 - * - * @param userDto 用户创建DTO - * @return 构建完成的用户对象 - */ - private User buildNewUserEntity(UserDto userDto) { - User user = new User(); - BeanUtils.copyProperties(userDto, user); - - // 设置加密密码 - user.setPwd(Md5Util.md5(userDto.getPwd())); - - // 设置默认属性 - user.setStatus(userDto.getStatus() != null ? userDto.getStatus() : USER_STATUS_ACTIVE); - user.setRoleId(USER_ROLE_ID); - - // 设置时间戳 - long currentTime = System.currentTimeMillis(); - user.setCreatedTime(currentTime); - user.setUpdatedTime(currentTime); - - return user; - } - - - - /** - * 检查用户是否存在 - * - * @param userId 用户ID - * @return 用户是否存在 - */ - private boolean isUserExists(Long userId) { - return this.getById(userId) != null; - } - - /** - * 构建用户更新实体对象 - * - * @param userUpdateDto 用户更新DTO - * @return 构建完成的更新对象 - */ - private User buildUpdateUserEntity(UserUpdateDto userUpdateDto) { - User user = new User(); - BeanUtils.copyProperties(userUpdateDto, user); - - // 处理密码更新 - if (StrUtil.isNotBlank(userUpdateDto.getPwd())) { - user.setPwd(Md5Util.md5(userUpdateDto.getPwd())); - } else { - user.setPwd(null); // 不更新密码字段 - } - - // 设置更新时间 - user.setUpdatedTime(System.currentTimeMillis()); - - return user; - } - - /** - * 验证用户删除条件 - * - * @param userId 用户ID - * @return 验证结果响应 - */ - private R validateUserDeletion(Long userId) { - User user = this.getById(userId); - if (user == null) { - return R.err(ERROR_USER_NOT_FOUND); - } - - if (user.getRoleId() == ADMIN_ROLE_ID) { - return R.err(ERROR_CANNOT_DELETE_ADMIN); - } - - return R.ok(); - } - - /** - * 验证用户更新条件 - * - * @param userId 用户ID - * @return 验证结果响应 - */ - private R validateUserUpdate(Long userId) { - User user = this.getById(userId); - if (user == null) { - return R.err(ERROR_USER_NOT_FOUND); - } - - if (user.getRoleId() == ADMIN_ROLE_ID) { - return R.err(ERROR_CANNOT_UPDATE_ADMIN); - } - - return R.ok(); - } - - /** - * 删除用户相关的所有数据 - * - * @param userId 用户ID - */ - private void deleteUserRelatedData(Long userId) { - // 1. 删除用户的所有转发和对应的Gost服务 - deleteUserForwardsAndGostServices(userId); - - // 2. 删除用户隧道权限 - deleteUserTunnelPermissions(userId); - } - - /** - * 删除用户转发和对应的Gost服务 - * - * @param userId 用户ID - */ - private void deleteUserForwardsAndGostServices(Long userId) { - QueryWrapper forwardQuery = new QueryWrapper<>(); - forwardQuery.eq("user_id", userId); - List userForwards = forwardMapper.selectList(forwardQuery); - - for (Forward forward : userForwards) { - try { - // 删除Gost服务 - deleteGostServicesForForward(forward, userId); - } catch (Exception e) { - // 记录错误但继续删除,避免因为Gost服务删除失败而阻断用户删除 - System.err.println("删除用户转发对应的Gost服务失败,转发ID: " + forward.getId() + ", 错误: " + e.getMessage()); - } - - // 删除数据库中的转发记录 - forwardMapper.deleteById(forward.getId()); - } - } - - /** - * 删除转发对应的Gost服务 - * - * @param forward 转发对象 - * @param userId 用户ID - */ - private void deleteGostServicesForForward(Forward forward, Long userId) { - Tunnel tunnel = tunnelService.getById(forward.getTunnelId()); - if (tunnel == null) return; - - Node inNode = nodeService.getNodeById(tunnel.getInNodeId()); - if (inNode == null) return; - - // 获取用户隧道关系 - UserTunnel userTunnel = getUserTunnelRelation(userId, tunnel.getId()); - if (userTunnel == null) return; - - String serviceName = buildServiceName(forward.getId(), userId, userTunnel.getId()); - - // 删除主服务 - GostUtil.DeleteService(inNode.getId(), serviceName); - - // 如果是隧道转发,还需要删除链和远程服务 - if (tunnel.getType() == TUNNEL_TYPE_TUNNEL_FORWARD) { - deleteGostTunnelForwardServices(tunnel, serviceName, inNode); - } - } - - /** - * 删除隧道转发相关的Gost服务 - * - * @param tunnel 隧道对象 - * @param serviceName 服务名称 - * @param inNode 入口节点 - */ - private void deleteGostTunnelForwardServices(Tunnel tunnel, String serviceName, Node inNode) { - Node outNode = nodeService.getNodeById(tunnel.getOutNodeId()); - if (outNode != null) { - GostUtil.DeleteChains(inNode.getId(), serviceName); - GostUtil.DeleteRemoteService(outNode.getId(), serviceName); - } - } - - /** - * 获取用户隧道关系 - * - * @param userId 用户ID - * @param tunnelId 隧道ID - * @return 用户隧道关系对象 - */ - private UserTunnel getUserTunnelRelation(Long userId, Long tunnelId) { - return userTunnelService.getOne(new QueryWrapper() - .eq("user_id", userId) - .eq("tunnel_id", tunnelId)); - } - - /** - * 构建服务名称 - * - * @param forwardId 转发ID - * @param userId 用户ID - * @param userTunnelId 用户隧道ID - * @return 服务名称 - */ - private String buildServiceName(Long forwardId, Long userId, Integer userTunnelId) { - return forwardId + "_" + userId + "_" + userTunnelId; - } - - - /** - * 删除用户隧道权限 - * - * @param userId 用户ID - */ - private void deleteUserTunnelPermissions(Long userId) { - QueryWrapper userTunnelQuery = new QueryWrapper<>(); - userTunnelQuery.eq("user_id", userId); - userTunnelMapper.delete(userTunnelQuery); - } - - /** - * 获取当前用户信息 - * - * @return 当前用户信息结果 - */ - private CurrentUserInfo getCurrentUserInfo() { - Integer userId = JwtUtil.getUserIdFromToken(); - Integer roleId = JwtUtil.getRoleIdFromToken(); - - if (userId == null) { - return CurrentUserInfo.error(ERROR_USER_NOT_LOGGED_IN); - } - - User user = this.getById(userId); - if (user == null) { - return CurrentUserInfo.error(ERROR_USER_NOT_FOUND); - } - - return CurrentUserInfo.success(user, roleId); - } - - /** - * 构建用户套餐信息DTO - * - * @param currentUser 当前用户信息 - * @return 用户套餐信息DTO - */ - private UserPackageDto buildUserPackageDto(CurrentUserInfo currentUser) { - User user = currentUser.getUser(); - Integer roleId = currentUser.getRoleId(); - - // 1. 构造用户基本信息 - UserPackageDto.UserInfoDto userInfo = buildUserInfoDto(user); - - // 2. 获取隧道权限详情 - List tunnelPermissions = getTunnelPermissions(user.getId()); - - // 3. 获取转发详情 - List forwards = userMapper.getUserForwardDetails(user.getId().intValue()); - - // 4. 查询最近24小时流量信息,没有的补0 - List statisticsFlows = getLast24HoursFlowStatistics(user.getId()); - - // 5. 构造返回结果 - UserPackageDto packageDto = new UserPackageDto(); - packageDto.setUserInfo(userInfo); - packageDto.setTunnelPermissions(tunnelPermissions); - packageDto.setForwards(forwards); - packageDto.setStatisticsFlows(statisticsFlows); - - return packageDto; - } - - /** - * 构建用户基本信息DTO - * - * @param user 用户对象 - * @return 用户基本信息DTO - */ private UserPackageDto.UserInfoDto buildUserInfoDto(User user) { UserPackageDto.UserInfoDto userInfo = new UserPackageDto.UserInfoDto(); userInfo.setId(user.getId()); @@ -710,134 +225,85 @@ public class UserServiceImpl extends ServiceImpl implements Us return userInfo; } - /** - * 获取隧道权限详情 - * - * @param userId 用户ID - * @return 隧道权限详情列表 - */ - private List getTunnelPermissions(Long userId) { - return userMapper.getUserTunnelDetails(userId.intValue()); - } - - /** - * 获取用户最近24小时的流量统计数据,没有数据的时间点补0 - * - * @param userId 用户ID - * @return 最近24小时流量统计列表 - */ private List getLast24HoursFlowStatistics(Long userId) { - // 按ID倒序查最近24条记录(ID越大越新,时间就是23:00, 22:00, 21:00...这样倒序) List recentFlows = statisticsFlowService.list( new QueryWrapper() .eq("user_id", userId) .orderByDesc("id") .last("LIMIT 24") ); - List result = new ArrayList<>(recentFlows); - - // 如果查出来的记录不足24条,需要补0和对应的时间 if (result.size() < 24) { - // 获取最早记录的时间,继续往前推 - int startHour = getCurrentHour(); + int startHour = java.time.LocalDateTime.now().getHour(); if (!result.isEmpty()) { - // 从最后一条记录的时间继续往前推 - String lastTime = result.get(result.size() - 1).getTime(); + String lastTime = result.getLast().getTime(); startHour = parseHour(lastTime) - 1; } - - // 补0到24条 while (result.size() < 24) { - if (startHour < 0) startHour = 23; // 跨天处理 - + if (startHour < 0) startHour = 23; StatisticsFlow emptyFlow = new StatisticsFlow(); emptyFlow.setUserId(userId); emptyFlow.setFlow(0L); emptyFlow.setTotalFlow(0L); emptyFlow.setTime(String.format("%02d:00", startHour)); result.add(emptyFlow); - startHour--; } } - - log.info("用户 {} 获取到 {} 条实际记录,补齐为 {} 条24小时记录", userId, recentFlows.size(), result.size()); return result; - } - /** - * 获取当前小时(0-23) - */ - private int getCurrentHour() { + private int parseHour(String timeStr) { + if (timeStr != null && timeStr.contains(":")) { + return Integer.parseInt(timeStr.split(":")[0]); + } return java.time.LocalDateTime.now().getHour(); } - /** - * 解析时间字符串获取小时数 - */ - private int parseHour(String timeStr) { - try { - if (timeStr != null && timeStr.contains(":")) { - return Integer.parseInt(timeStr.split(":")[0]); + private void fillForwardInIpAndPort(List forwards) { + for (UserPackageDto.UserForwardDetailDto forward : forwards) { + Tunnel tunnel = tunnelService.getById(forward.getTunnelId()); + if (tunnel == null) continue; + List forwardPorts = forwardPortService.list( + new QueryWrapper().eq("forward_id", forward.getId()) + ); + if (forwardPorts.isEmpty()) continue; + boolean useTunnelInIp = tunnel.getInIp() != null && !tunnel.getInIp().trim().isEmpty(); + List ipList = new ArrayList<>(); + List portList = new ArrayList<>(); + if (useTunnelInIp) { + String[] tunnelInIps = tunnel.getInIp().split(","); + for (String ip : tunnelInIps) { + if (ip != null && !ip.trim().isEmpty()) { + ipList.add(ip.trim()); + } + } + } else { + for (ForwardPort forwardPort : forwardPorts) { + Node node = nodeService.getById(forwardPort.getNodeId()); + if (node != null && node.getServerIp() != null) { + ipList.add(node.getServerIp()); + } + } + } + for (ForwardPort forwardPort : forwardPorts) { + if (forwardPort.getPort() != null) { + portList.add(forwardPort.getPort()); + } + } + List uniqueIps = ipList.stream().distinct().toList(); + List uniquePorts = portList.stream().distinct().toList(); + java.util.Set ipPortSet = new java.util.LinkedHashSet<>(); + for (String ip : uniqueIps) { + for (Integer port : uniquePorts) { + ipPortSet.add(ip + ":" + port); + } + } + if (!ipPortSet.isEmpty()) { + forward.setInIp(String.join(",", ipPortSet)); + forward.setInPort(uniquePorts.getFirst()); } - } catch (Exception e) { - // 解析失败,返回当前小时 - } - return getCurrentHour(); - } - - - // ========== 内部数据类 ========== - - /** - * 登录验证结果封装类 - */ - @Data - private static class LoginValidationResult { - private final boolean hasError; - private final String errorMessage; - private final User user; - - private LoginValidationResult(boolean hasError, String errorMessage, User user) { - this.hasError = hasError; - this.errorMessage = errorMessage; - this.user = user; - } - - public static LoginValidationResult success(User user) { - return new LoginValidationResult(false, null, user); - } - - public static LoginValidationResult error(String errorMessage) { - return new LoginValidationResult(true, errorMessage, null); } } - /** - * 当前用户信息封装类 - */ - @Data - private static class CurrentUserInfo { - private final boolean hasError; - private final String errorMessage; - private final User user; - private final Integer roleId; - - private CurrentUserInfo(boolean hasError, String errorMessage, User user, Integer roleId) { - this.hasError = hasError; - this.errorMessage = errorMessage; - this.user = user; - this.roleId = roleId; - } - - public static CurrentUserInfo success(User user, Integer roleId) { - return new CurrentUserInfo(false, null, user, roleId); - } - - public static CurrentUserInfo error(String errorMessage) { - return new CurrentUserInfo(true, errorMessage, null, null); - } - } } diff --git a/springboot-backend/src/main/java/com/admin/service/impl/UserTunnelServiceImpl.java b/springboot-backend/src/main/java/com/admin/service/impl/UserTunnelServiceImpl.java index 135d99d..aba06f7 100644 --- a/springboot-backend/src/main/java/com/admin/service/impl/UserTunnelServiceImpl.java +++ b/springboot-backend/src/main/java/com/admin/service/impl/UserTunnelServiceImpl.java @@ -1,9 +1,6 @@ package com.admin.service.impl; -import com.admin.common.dto.UserTunnelDto; -import com.admin.common.dto.UserTunnelQueryDto; -import com.admin.common.dto.UserTunnelUpdateDto; -import com.admin.common.dto.UserTunnelWithDetailDto; +import com.admin.common.dto.*; import com.admin.common.lang.R; import com.admin.entity.UserTunnel; import com.admin.mapper.TunnelMapper; @@ -42,347 +39,74 @@ import java.util.Map; @Service public class UserTunnelServiceImpl extends ServiceImpl implements UserTunnelService { - // ========== 常量定义 ========== - - /** 成功响应消息 */ - private static final String SUCCESS_ASSIGN_MSG = "用户隧道权限分配成功"; - private static final String SUCCESS_REMOVE_MSG = "用户隧道权限删除成功"; - private static final String SUCCESS_UPDATE_FLOW_MSG = "用户隧道流量限制更新成功"; - private static final String SUCCESS_UPDATE_MSG = "用户隧道权限更新成功"; - - /** 错误响应消息 */ - private static final String ERROR_ASSIGN_FAILED = "用户隧道权限分配失败"; - private static final String ERROR_PERMISSION_EXISTS = "该用户已拥有此隧道权限"; - private static final String ERROR_PERMISSION_NOT_FOUND = "未找到对应的用户隧道权限记录"; - private static final String ERROR_USER_TUNNEL_NOT_EXISTS = "用户隧道权限不存在"; - private static final String ERROR_NOT_EXISTS = "不存在"; - private static final String ERROR_UPDATE_FAILED = "用户隧道权限更新失败"; - // ========== 依赖注入 ========== - - @Autowired + @Resource @Lazy private ForwardService forwardService; - - @Autowired - @Lazy - private TunnelService tunnelService; - - @Autowired - private NodeService nodeService; - // ========== 公共接口实现 ========== - - /** - * 分配用户隧道权限 - * 检查权限是否已存在,避免重复分配 - * - * @param userTunnelDto 用户隧道权限分配数据传输对象 - * @return 分配结果响应 - */ @Override public R assignUserTunnel(UserTunnelDto userTunnelDto) { - // 1. 检查权限是否已存在 - if (isUserTunnelPermissionExists(userTunnelDto.getUserId(), userTunnelDto.getTunnelId())) { - return R.err(ERROR_PERMISSION_EXISTS); - } - - // 2. 创建用户隧道权限实体并保存 - UserTunnel userTunnel = buildUserTunnelEntity(userTunnelDto); - // 设置默认状态为启用 - userTunnel.setStatus(1); - boolean success = this.save(userTunnel); - - if (success) { - return R.ok(SUCCESS_ASSIGN_MSG); - } - - return R.err(ERROR_ASSIGN_FAILED); - } - - /** - * 获取用户隧道权限列表 - * 通过连表查询获取用户隧道权限及隧道详细信息 - * - * @param queryDto 用户隧道权限查询数据传输对象 - * @return 用户隧道权限详情列表响应 - */ - @Override - public R getUserTunnelList(UserTunnelQueryDto queryDto) { - List userTunnelDetails = getUserTunnelDetailsFromDatabase(queryDto.getUserId()); - return R.ok(userTunnelDetails); - } - - /** - * 删除用户隧道权限 - * - * @param id 用户隧道权限ID - * @return 删除结果响应 - */ - @Override - public R removeUserTunnel(Integer id) { - // 1. 获取用户隧道权限信息 - UserTunnel userTunnel = this.getById(id); - if (userTunnel == null) { - return R.err(ERROR_PERMISSION_NOT_FOUND); - } - - // 2. 删除该用户在该隧道下的所有转发 - try { - removeUserTunnelForwards(userTunnel.getUserId(), userTunnel.getTunnelId()); - } catch (Exception e) { - // 转发删除失败,记录日志但不阻止权限删除 - } - - - // 4. 删除用户隧道权限记录 - boolean success = this.removeById(id); - return success ? R.ok(SUCCESS_REMOVE_MSG) : R.err(ERROR_PERMISSION_NOT_FOUND); - } - - - /** - * 更新用户隧道权限 - * 支持更新流量限制、数量限制、流量重置时间、过期时间和限速规则 - * - * @param updateDto 用户隧道权限更新数据传输对象 - * @return 更新结果响应 - */ - @Override - public R updateUserTunnel(UserTunnelUpdateDto updateDto) { - // 1. 验证用户隧道权限是否存在 - UserTunnel existingUserTunnel = this.getById(updateDto.getId()); - if (existingUserTunnel == null) { - return R.err(ERROR_USER_TUNNEL_NOT_EXISTS); - } - - // 2. 检查是否更新了限速规则 - boolean speedChanged = hasSpeedChanged(existingUserTunnel.getSpeedId(), updateDto.getSpeedId()); - - // 3. 更新用户隧道权限属性 - updateUserTunnelProperties(existingUserTunnel, updateDto); - - // 4. 保存更新 - boolean success = this.updateById(existingUserTunnel); - - if (success) { - // 6. 如果限速规则发生变化,更新该用户隧道下的所有转发 - if (speedChanged) { - updateUserTunnelForwardsSpeed(existingUserTunnel.getUserId(), existingUserTunnel.getTunnelId(), updateDto.getSpeedId()); - } - - return R.ok(SUCCESS_UPDATE_MSG); - } - - return R.err(ERROR_UPDATE_FAILED); - } - - // ========== 私有辅助方法 ========== - - /** - * 检查用户隧道权限是否已存在 - * - * @param userId 用户ID - * @param tunnelId 隧道ID - * @return 权限是否已存在 - */ - private boolean isUserTunnelPermissionExists(Integer userId, Integer tunnelId) { - QueryWrapper queryWrapper = new QueryWrapper<>(); - queryWrapper.eq("user_id", userId).eq("tunnel_id", tunnelId); - UserTunnel existingUserTunnel = this.getOne(queryWrapper); - return existingUserTunnel != null; - } - - /** - * 构建用户隧道权限实体对象 - * - * @param userTunnelDto 用户隧道权限DTO - * @return 构建完成的用户隧道权限对象 - */ - private UserTunnel buildUserTunnelEntity(UserTunnelDto userTunnelDto) { + int count = this.count(new QueryWrapper().eq("user_id", userTunnelDto.getUserId()).eq("tunnel_id", userTunnelDto.getTunnelId())); + if (count > 0) return R.err("该用户已拥有此隧道权限"); UserTunnel userTunnel = new UserTunnel(); BeanUtils.copyProperties(userTunnelDto, userTunnel); - return userTunnel; + userTunnel.setStatus(1); + this.save(userTunnel); + return R.ok(); } - /** - * 从数据库获取用户隧道权限详情 - * - * @param userId 用户ID - * @return 用户隧道权限详情列表 - */ - private List getUserTunnelDetailsFromDatabase(Integer userId) { - return this.baseMapper.getUserTunnelWithDetails(userId); + @Override + public R getUserTunnelList(UserTunnelQueryDto queryDto) { + List userTunnelWithDetails = this.baseMapper.getUserTunnelWithDetails(queryDto.getUserId()); + return R.ok(userTunnelWithDetails); } - /** - * 更新用户隧道权限属性 - * - * @param existingUserTunnel 现有的用户隧道权限对象 - * @param updateDto 更新数据传输对象 - */ - private void updateUserTunnelProperties(UserTunnel existingUserTunnel, UserTunnelUpdateDto updateDto) { - // 更新基本属性 - existingUserTunnel.setFlow(updateDto.getFlow()); - existingUserTunnel.setNum(updateDto.getNum()); - - // 更新可选属性(仅在非空时更新) - updateOptionalProperty(existingUserTunnel::setFlowResetTime, updateDto.getFlowResetTime()); - updateOptionalProperty(existingUserTunnel::setExpTime, updateDto.getExpTime()); - updateOptionalProperty(existingUserTunnel::setStatus, updateDto.getStatus()); - - // 更新限速规则ID(允许设置为null,表示不限速) - existingUserTunnel.setSpeedId(updateDto.getSpeedId()); + @Override + public R removeUserTunnel(Integer id) { + UserTunnel userTunnel = this.getById(id); + if (userTunnel == null) return R.err("未找到对应的用户隧道权限记录"); + + List forwardList = forwardService.list(new QueryWrapper().eq("user_id", userTunnel.getUserId()).eq("tunnel_id", userTunnel.getTunnelId())); + for (Forward forward : forwardList) { + forwardService.deleteForward(forward.getId()); + } + this.removeById(id); + return R.ok(); + } + + @Override + public R updateUserTunnel(UserTunnelUpdateDto updateDto) { + UserTunnel userTunnel = this.getById(updateDto.getId()); + if (userTunnel == null) return R.err("隧道不存在"); + boolean speedChanged = hasSpeedChanged(userTunnel.getSpeedId(), updateDto.getSpeedId()); + userTunnel.setFlow(updateDto.getFlow()); + userTunnel.setNum(updateDto.getNum()); + updateOptionalProperty(userTunnel::setFlowResetTime, updateDto.getFlowResetTime()); + updateOptionalProperty(userTunnel::setExpTime, updateDto.getExpTime()); + updateOptionalProperty(userTunnel::setStatus, updateDto.getStatus()); + userTunnel.setSpeedId(updateDto.getSpeedId()); + this.updateById(userTunnel); + if (speedChanged) { + List forwardList = forwardService.list(new QueryWrapper().eq("user_id", userTunnel.getUserId()).eq("tunnel_id", userTunnel.getTunnelId())); + for (Forward forward : forwardList) { + ForwardUpdateDto forwardUpdateDto = new ForwardUpdateDto(); + forwardUpdateDto.setId(forward.getId()); + forwardUpdateDto.setUserId(forward.getUserId()); + forwardUpdateDto.setName(forward.getName()); + forwardUpdateDto.setRemoteAddr(forward.getRemoteAddr()); + forwardUpdateDto.setStrategy(forward.getStrategy()); + forwardService.updateForward(forwardUpdateDto); + } + } + return R.err("用户隧道权限更新失败"); } - /** - * 更新可选属性(仅在值非空时更新) - * - * @param setter 属性设置方法 - * @param value 属性值 - * @param 属性类型 - */ private void updateOptionalProperty(java.util.function.Consumer setter, T value) { if (value != null) { setter.accept(value); } } - - - /** - * 删除用户在指定隧道下的所有转发 - * - * @param userId 用户ID - * @param tunnelId 隧道ID - */ - private void removeUserTunnelForwards(Integer userId, Integer tunnelId) { - // 查询该用户在该隧道下的所有转发 - QueryWrapper queryWrapper = new QueryWrapper<>(); - queryWrapper.eq("user_id", userId).eq("tunnel_id", tunnelId); - - List userTunnelForwards = forwardService.list(queryWrapper); - - if (!userTunnelForwards.isEmpty()) { - // 获取用户隧道权限信息,用于构建服务名称 - UserTunnel userTunnel = getUserTunnelByUserAndTunnel(userId, tunnelId); - - for (Forward forward : userTunnelForwards) { - try { - // 先调用GostUtil删除/停止服务 - stopForwardService(forward, userId, userTunnel != null ? userTunnel.getId() : 0); - - // 然后删除数据库记录 - forwardService.removeById(forward.getId()); - - } catch (Exception e) { - // 单个转发删除失败,记录错误但继续处理其他转发 - } - } - - } - } - - /** - * 删除转发服务(按创建的反向顺序删除:主服务 -> 远端服务 -> 转发链) - * - * @param forward 转发对象 - * @param userId 用户ID - * @param userTunnelId 用户隧道ID - */ - private void stopForwardService(Forward forward, Integer userId, Integer userTunnelId) { - try { - Tunnel tunnel = tunnelService.getById(forward.getTunnelId()); - if (tunnel == null) { - return; - } - - Node inNode = nodeService.getById(tunnel.getInNodeId()); - Node outNode = nodeService.getById(tunnel.getOutNodeId()); - - String serviceName = buildServiceName(forward.getId(), Long.valueOf(userId), userTunnelId); - - // 1. 先删除主服务 - if (inNode != null) { - try { - GostUtil.DeleteService(inNode.getId(), serviceName); - } catch (Exception e) { - // 主服务删除失败,记录但继续 - } - } - - // 2. 如果是隧道转发,删除远端服务 - if (tunnel.getType() == 1 && outNode != null && !outNode.getId().equals(inNode != null ? inNode.getId() : null)) { - try { - GostUtil.DeleteRemoteService(outNode.getId(), serviceName); - } catch (Exception e) { - // 远端服务删除失败,记录但继续 - } - } - - // 3. 如果是隧道转发,最后删除转发链 - if (tunnel.getType() == 1 && inNode != null) { - try { - GostUtil.DeleteChains(inNode.getId(), serviceName); - } catch (Exception e) { - // 转发链删除失败,记录但继续 - } - } - - } catch (Exception e) { - // 服务删除失败,记录错误 - throw new RuntimeException("删除转发服务失败,转发ID:" + forward.getId() + ",错误:" + e.getMessage(), e); - } - } - - /** - * 根据用户ID和隧道ID获取用户隧道权限 - * - * @param userId 用户ID - * @param tunnelId 隧道ID - * @return 用户隧道权限对象 - */ - private UserTunnel getUserTunnelByUserAndTunnel(Integer userId, Integer tunnelId) { - try { - QueryWrapper queryWrapper = new QueryWrapper<>(); - queryWrapper.eq("user_id", userId).eq("tunnel_id", tunnelId); - return this.getOne(queryWrapper); - } catch (Exception e) { - return null; - } - } - - /** - * 构建服务名称 - * - * @param forwardId 转发ID - * @param userId 用户ID - * @param userTunnelId 用户隧道ID - * @return 服务名称 - */ - private String buildServiceName(Long forwardId, Long userId, Integer userTunnelId) { - return forwardId + "_" + userId + "_" + userTunnelId; - } - - - /** - * 检查用户隧道是否启用且有到期时间 - * - * @param userTunnel 用户隧道对象 - * @return 是否启用且有到期时间 - */ - private boolean isEnabledAndHasExpTime(UserTunnel userTunnel) { - return userTunnel.getStatus() != null && userTunnel.getStatus() == 1 - && userTunnel.getExpTime() != null; - } - - /** - * 检查限速规则是否发生变化 - * - * @param oldSpeedId 原始限速规则ID - * @param newSpeedId 新的限速规则ID - * @return 限速规则是否发生变化 - */ private boolean hasSpeedChanged(Integer oldSpeedId, Integer newSpeedId) { if (oldSpeedId == null && newSpeedId == null) { return false; @@ -392,56 +116,5 @@ public class UserTunnelServiceImpl extends ServiceImpl queryWrapper = new QueryWrapper<>(); - queryWrapper.eq("user_id", userId).eq("tunnel_id", tunnelId); - List userTunnelForwards = forwardService.list(queryWrapper); - if (userTunnelForwards.isEmpty()) { - return; - } - - // 2. 获取隧道信息 - Tunnel tunnel = tunnelService.getById(tunnelId); - if (tunnel == null) { - return; - } - - // 3. 获取用户隧道权限信息 - UserTunnel userTunnel = getUserTunnelByUserAndTunnel(userId, tunnelId); - if (userTunnel == null) { - return; - } - - // 4. 获取入口节点信息 - Node inNode = nodeService.getById(tunnel.getInNodeId()); - - if (inNode == null) { - return; - } - - // 5. 批量更新该用户在该隧道下所有转发的限速配置(只更新入口节点) - for (Forward forward : userTunnelForwards) { - String serviceName = buildServiceName(forward.getId(), Long.valueOf(userId), userTunnel.getId()); - - String interfaceName = null; - // 创建主服务 - if (tunnel.getType() != 2) { // 不是隧道转发服务才会存在网络接口 - interfaceName = forward.getInterfaceName(); - } - - // 6. 更新入口节点的主服务限速配置(使用批量UpdateService接口) - GostUtil.UpdateService(inNode.getId(), serviceName, forward.getInPort(), speedId, forward.getRemoteAddr(), tunnel.getType(), tunnel, forward.getStrategy(), interfaceName); - } - } } diff --git a/springboot-backend/src/main/java/com/admin/service/impl/ViteConfigServiceImpl.java b/springboot-backend/src/main/java/com/admin/service/impl/ViteConfigServiceImpl.java index 47bc9d5..108ebbf 100644 --- a/springboot-backend/src/main/java/com/admin/service/impl/ViteConfigServiceImpl.java +++ b/springboot-backend/src/main/java/com/admin/service/impl/ViteConfigServiceImpl.java @@ -24,24 +24,7 @@ import java.util.Map; @Service public class ViteConfigServiceImpl extends ServiceImpl implements ViteConfigService { - // ========== 常量定义 ========== - - /** 成功响应消息 */ - private static final String SUCCESS_UPDATE_MSG = "配置更新成功"; - - /** 错误响应消息 */ - private static final String ERROR_UPDATE_MSG = "配置更新失败"; - private static final String ERROR_CONFIG_NOT_FOUND = "配置不存在"; - private static final String ERROR_CONFIG_NAME_REQUIRED = "配置名称不能为空"; - private static final String ERROR_CONFIG_VALUE_REQUIRED = "配置值不能为空"; - // ========== 公共接口实现 ========== - - /** - * 获取所有网站配置 - * - * @return 包含所有配置的Map - */ @Override public R getConfigs() { List configList = this.list(); @@ -54,89 +37,48 @@ public class ViteConfigServiceImpl extends ServiceImpl queryWrapper = new QueryWrapper<>(); queryWrapper.eq("name", name); ViteConfig config = this.getOne(queryWrapper); - if (config == null) { - return R.err(ERROR_CONFIG_NOT_FOUND); - } + if (config == null) return R.err("配置不存在"); return R.ok(config); } - /** - * 批量更新网站配置 - * - * @param configMap 配置Map - * @return 更新结果响应 - */ + @Override public R updateConfigs(Map configMap) { - if (configMap == null || configMap.isEmpty()) { - return R.err("配置数据不能为空"); - } + if (configMap == null || configMap.isEmpty()) return R.err("配置数据不能为空"); - try { - for (Map.Entry entry : configMap.entrySet()) { - String name = entry.getKey(); - String value = entry.getValue(); - - if (!StringUtils.hasText(name)) { - continue; // 跳过无效的配置名 - } - - updateOrCreateConfig(name, value); + for (Map.Entry entry : configMap.entrySet()) { + String name = entry.getKey(); + String value = entry.getValue(); + + if (!StringUtils.hasText(name)) { + continue; } - return R.ok(SUCCESS_UPDATE_MSG); - } catch (Exception e) { - return R.err(ERROR_UPDATE_MSG + ": " + e.getMessage()); + + updateOrCreateConfig(name, value); } + return R.ok(); } - /** - * 更新单个配置项 - * - * @param name 配置名 - * @param value 配置值 - * @return 更新结果响应 - */ + @Override public R updateConfig(String name, String value) { - // 1. 验证必填字段 - if (!StringUtils.hasText(name)) { - return R.err(ERROR_CONFIG_NAME_REQUIRED); - } - if (!StringUtils.hasText(value)) { - return R.err(ERROR_CONFIG_VALUE_REQUIRED); - } - - try { - updateOrCreateConfig(name, value); - return R.ok(SUCCESS_UPDATE_MSG); - } catch (Exception e) { - return R.err(ERROR_UPDATE_MSG + ": " + e.getMessage()); - } + if (!StringUtils.hasText(name)) return R.err("配置名称不能为空"); + if (!StringUtils.hasText(value)) return R.err("配置值不能为空"); + updateOrCreateConfig(name, value); + return R.ok(); } - // ========== 私有辅助方法 ========== - /** - * 更新或创建配置项 - * 如果配置存在则更新,不存在则创建 - */ private void updateOrCreateConfig(String name, String value) { QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("name", name); diff --git a/springboot-backend/src/main/resources/application.yml b/springboot-backend/src/main/resources/application.yml index 3a79b83..4f5af55 100644 --- a/springboot-backend/src/main/resources/application.yml +++ b/springboot-backend/src/main/resources/application.yml @@ -1,9 +1,7 @@ spring: datasource: - driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://${DB_HOST}:3306/${DB_NAME}?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true - username: ${DB_USER} - password: ${DB_PASSWORD} + driver-class-name: org.sqlite.JDBC + url: jdbc:sqlite:${DB_PATH:/app/data/gost.db} hikari: max-lifetime: 500000 connection-timeout: 30000 @@ -12,7 +10,11 @@ spring: minimum-idle: 5 pool-name: HikariCP auto-commit: true - connection-test-query: SELECT 1 + sql: + init: + mode: always + platform: sqlite + continue-on-error: false servlet: multipart: max-file-size: 50MB diff --git a/springboot-backend/src/main/resources/data.sql b/springboot-backend/src/main/resources/data.sql new file mode 100644 index 0000000..7d73855 --- /dev/null +++ b/springboot-backend/src/main/resources/data.sql @@ -0,0 +1,7 @@ + +INSERT OR IGNORE INTO user (id, user, pwd, role_id, exp_time, flow, in_flow, out_flow, flow_reset_time, num, created_time, updated_time, status) +VALUES (1, 'admin_user', '3c85cdebade1c51cf64ca9f3c09d182d', 0, 2727251700000, 99999, 0, 0, 1, 99999, 1748914865000, 1754011744252, 1); + + +INSERT OR IGNORE INTO vite_config (id, name, value, time) +VALUES (1, 'app_name', 'flux', 1755147963000); diff --git a/springboot-backend/src/main/resources/logback-spring.xml b/springboot-backend/src/main/resources/logback-spring.xml index 12fd963..121728a 100644 --- a/springboot-backend/src/main/resources/logback-spring.xml +++ b/springboot-backend/src/main/resources/logback-spring.xml @@ -20,7 +20,7 @@ true ${FILE_PATH} - 30 + 7 diff --git a/springboot-backend/src/main/resources/mapper/ForwardMapper.xml b/springboot-backend/src/main/resources/mapper/ForwardMapper.xml index 92d430c..56b2ed8 100644 --- a/springboot-backend/src/main/resources/mapper/ForwardMapper.xml +++ b/springboot-backend/src/main/resources/mapper/ForwardMapper.xml @@ -9,8 +9,6 @@ f.user_id AS userId, f.name, f.tunnel_id AS tunnelId, - f.in_port AS inPort, - f.out_port AS outPort, f.remote_addr AS remoteAddr, f.status, f.created_time AS createdTime, @@ -20,13 +18,9 @@ f.strategy AS strategy, f.out_flow AS outFlow, f.inx AS inx, - f.interface_name AS interfaceName, t.name AS tunnelName, - t.in_ip AS inIp, - t.out_ip AS outIp, - t.type, - t.protocol - FROM + t.type + FROM forward f LEFT JOIN tunnel t ON f.tunnel_id = t.id @@ -41,8 +35,6 @@ f.user_id AS userId, f.name, f.tunnel_id AS tunnelId, - f.in_port AS inPort, - f.out_port AS outPort, f.remote_addr AS remoteAddr, f.status, f.created_time AS createdTime, @@ -52,13 +44,9 @@ f.out_flow AS outFlow, f.strategy AS strategy, f.inx AS inx, - f.interface_name AS interfaceName, t.name AS tunnelName, - t.in_ip AS inIp, - t.out_ip AS outIp, - t.type, - t.protocol - FROM + t.type + FROM forward f LEFT JOIN tunnel t ON f.tunnel_id = t.id diff --git a/springboot-backend/src/main/resources/mapper/UserMapper.xml b/springboot-backend/src/main/resources/mapper/UserMapper.xml index 9012d32..523c0a7 100644 --- a/springboot-backend/src/main/resources/mapper/UserMapper.xml +++ b/springboot-backend/src/main/resources/mapper/UserMapper.xml @@ -34,7 +34,6 @@ f.tunnel_id as tunnelId, t.name as tunnelName, t.in_ip as inIp, - f.in_port as inPort, f.remote_addr as remoteAddr, f.in_flow as inFlow, f.out_flow as outFlow, diff --git a/springboot-backend/src/main/resources/mapper/UserTunnelMapper.xml b/springboot-backend/src/main/resources/mapper/UserTunnelMapper.xml index 7d149ea..0b1092b 100644 --- a/springboot-backend/src/main/resources/mapper/UserTunnelMapper.xml +++ b/springboot-backend/src/main/resources/mapper/UserTunnelMapper.xml @@ -19,7 +19,6 @@ t.name as tunnelName, t.flow as tunnelFlow, t.in_ip as inIp, - t.out_ip as outIp, t.type, t.protocol, sl.name as speedLimitName, diff --git a/springboot-backend/src/main/resources/schema.sql b/springboot-backend/src/main/resources/schema.sql new file mode 100644 index 0000000..318af77 --- /dev/null +++ b/springboot-backend/src/main/resources/schema.sql @@ -0,0 +1,126 @@ +-- SQLite Auto-generated schema +-- This will be executed automatically on startup if tables don't exist + +CREATE TABLE IF NOT EXISTS forward ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + user_name VARCHAR(100) NOT NULL, + name VARCHAR(100) NOT NULL, + tunnel_id INTEGER NOT NULL, + remote_addr TEXT NOT NULL, + strategy VARCHAR(100) NOT NULL DEFAULT 'fifo', + in_flow INTEGER NOT NULL DEFAULT 0, + out_flow INTEGER NOT NULL DEFAULT 0, + created_time INTEGER NOT NULL, + updated_time INTEGER NOT NULL, + status INTEGER NOT NULL, + inx INTEGER NOT NULL DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS forward_port ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + forward_id INTEGER NOT NULL, + node_id INTEGER NOT NULL, + port INTEGER NOT NULL +); + +CREATE TABLE IF NOT EXISTS node ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(100) NOT NULL, + secret VARCHAR(100) NOT NULL, + server_ip VARCHAR(100) NOT NULL, + port TEXT NOT NULL, + interface_name VARCHAR(200), + version VARCHAR(100), + http INTEGER NOT NULL DEFAULT 0, + tls INTEGER NOT NULL DEFAULT 0, + socks INTEGER NOT NULL DEFAULT 0, + created_time INTEGER NOT NULL, + updated_time INTEGER, + status INTEGER NOT NULL, + tcp_listen_addr VARCHAR(100) NOT NULL DEFAULT '[::]', + udp_listen_addr VARCHAR(100) NOT NULL DEFAULT '[::]' +); + +CREATE TABLE IF NOT EXISTS speed_limit ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(100) NOT NULL, + speed INTEGER NOT NULL, + tunnel_id INTEGER NOT NULL, + tunnel_name VARCHAR(100) NOT NULL, + created_time INTEGER NOT NULL, + updated_time INTEGER, + status INTEGER NOT NULL +); + +CREATE TABLE IF NOT EXISTS statistics_flow ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + flow INTEGER NOT NULL, + total_flow INTEGER NOT NULL, + time VARCHAR(100) NOT NULL, + created_time INTEGER NOT NULL +); + +CREATE TABLE IF NOT EXISTS tunnel ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(100) NOT NULL, + traffic_ratio REAL NOT NULL DEFAULT 1.0, + type INTEGER NOT NULL, + protocol VARCHAR(10) NOT NULL DEFAULT 'tls', + flow INTEGER NOT NULL, + created_time INTEGER NOT NULL, + updated_time INTEGER NOT NULL, + status INTEGER NOT NULL, + in_ip TEXT +); + +CREATE TABLE IF NOT EXISTS chain_tunnel ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + tunnel_id INTEGER NOT NULL , + chain_type VARCHAR(10) NOT NULL, + node_id INTEGER NOT NULL , + port INTEGER, + strategy VARCHAR(10), + inx INTEGER, + protocol VARCHAR(10) +); + + +CREATE TABLE IF NOT EXISTS user ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user VARCHAR(100) NOT NULL, + pwd VARCHAR(100) NOT NULL, + role_id INTEGER NOT NULL, + exp_time INTEGER NOT NULL, + flow INTEGER NOT NULL, + in_flow INTEGER NOT NULL DEFAULT 0, + out_flow INTEGER NOT NULL DEFAULT 0, + flow_reset_time INTEGER NOT NULL, + num INTEGER NOT NULL, + created_time INTEGER NOT NULL, + updated_time INTEGER, + status INTEGER NOT NULL +); + +CREATE TABLE IF NOT EXISTS user_tunnel ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + tunnel_id INTEGER NOT NULL, + speed_id INTEGER, + num INTEGER NOT NULL, + flow INTEGER NOT NULL, + in_flow INTEGER NOT NULL DEFAULT 0, + out_flow INTEGER NOT NULL DEFAULT 0, + flow_reset_time INTEGER NOT NULL, + exp_time INTEGER NOT NULL, + status INTEGER NOT NULL +); + +CREATE TABLE IF NOT EXISTS vite_config ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(200) NOT NULL UNIQUE, + value VARCHAR(200) NOT NULL, + time INTEGER NOT NULL +); + diff --git a/vite-frontend/src/config/site.ts b/vite-frontend/src/config/site.ts index 9b22614..b0e3dbc 100644 --- a/vite-frontend/src/config/site.ts +++ b/vite-frontend/src/config/site.ts @@ -4,7 +4,7 @@ export type SiteConfig = typeof siteConfig; // 缓存相关常量 const CACHE_PREFIX = 'vite_config_'; -const VERSION = "1.4.2"; +const VERSION = "2.0.0-beta"; const APP_VERSION = "1.0.3"; const getInitialConfig = () => { diff --git a/vite-frontend/src/pages/dashboard.tsx b/vite-frontend/src/pages/dashboard.tsx index b31e043..67b8390 100644 --- a/vite-frontend/src/pages/dashboard.tsx +++ b/vite-frontend/src/pages/dashboard.tsx @@ -425,14 +425,28 @@ export default function DashboardPage() { }; const formatInAddress = (ipString: string, port: number): string => { - if (!ipString || !port) return ''; + if (!ipString) return ''; - const ips = ipString.split(',').map(ip => ip.trim()).filter(ip => ip); + const items = ipString.split(',').map(item => item.trim()).filter(item => item); + if (items.length === 0) return ''; - if (ips.length === 0) return ''; + // 检查第一项是否已经包含端口(格式:IP:端口) + const firstItem = items[0]; + const hasPort = /:\d+$/.test(firstItem); - if (ips.length === 1) { - const ip = ips[0]; + if (hasPort) { + // inIp 已经包含完整的 IP:Port 组合 + if (items.length === 1) { + return items[0]; + } + return `${items[0]} (+${items.length - 1}个)`; + } + + // inIp 只包含IP,需要添加端口(兼容旧数据) + if (!port) return ''; + + if (items.length === 1) { + const ip = items[0]; if (ip.includes(':') && !ip.startsWith('[')) { return `[${ip}]:${port}`; } else { @@ -440,16 +454,15 @@ export default function DashboardPage() { } } - const firstIp = ips[0]; + const firstIp = items[0]; let formattedFirstIp; - if (firstIp.includes(':') && !firstIp.startsWith('[')) { formattedFirstIp = `[${firstIp}]`; } else { formattedFirstIp = firstIp; } - return `${formattedFirstIp}:${port} (+${ips.length - 1})`; + return `${formattedFirstIp}:${port} (+${items.length - 1}个)`; }; const formatRemoteAddress = (remoteAddr: string): string => { @@ -479,32 +492,47 @@ export default function DashboardPage() { }; const showAddressModal = (ipString: string, port: number, title: string) => { - if (!ipString || !port) return; + if (!ipString) return; - const ips = ipString.split(',').map(ip => ip.trim()).filter(ip => ip); + const items = ipString.split(',').map(item => item.trim()).filter(item => item); - if (ips.length <= 1) { - copyToClipboard(formatInAddress(ipString, port)); + if (items.length <= 1) { + copyToClipboard(formatInAddress(ipString, port)); return; } - const formattedList = ips.map((ip, index) => { - let formattedAddress; - if (ip.includes(':') && !ip.startsWith('[')) { - formattedAddress = `[${ip}]:${port}`; - } else { - formattedAddress = `${ip}:${port}`; - } - return { + // 检查是否已经包含端口 + const hasPort = /:\d+$/.test(items[0]); + + let formattedList; + if (hasPort) { + // 已经包含完整的 IP:Port 组合,直接使用 + formattedList = items.map((item, index) => ({ id: index, - ip: ip, - address: formattedAddress, + ip: item, + address: item, copying: false - }; - }); + })); + } else { + // 只包含IP,需要添加端口 + formattedList = items.map((ip, index) => { + let formattedAddress; + if (ip.includes(':') && !ip.startsWith('[')) { + formattedAddress = `[${ip}]:${port}`; + } else { + formattedAddress = `${ip}:${port}`; + } + return { + id: index, + ip: ip, + address: formattedAddress, + copying: false + }; + }); + } setAddressList(formattedList); - setAddressModalTitle(`${title} (${ips.length}个)`); + setAddressModalTitle(`${title} (${items.length}个)`); setAddressModalOpen(true); }; diff --git a/vite-frontend/src/pages/forward.tsx b/vite-frontend/src/pages/forward.tsx index 7ba8e2d..a43a2df 100644 --- a/vite-frontend/src/pages/forward.tsx +++ b/vite-frontend/src/pages/forward.tsx @@ -103,6 +103,10 @@ interface DiagnosisResult { message?: string; averageTime?: number; packetLoss?: number; + fromChainType?: number; // 1: 入口, 2: 链, 3: 出口 + fromInx?: number; + toChainType?: number; + toInx?: number; }>; } @@ -196,7 +200,6 @@ export default function ForwardPage() { // 表单验证错误 const [errors, setErrors] = useState<{[key: string]: string}>({}); - const [selectedTunnel, setSelectedTunnel] = useState(null); useEffect(() => { loadData(); @@ -421,16 +424,6 @@ export default function ForwardPage() { } } - if (form.inPort !== null && (form.inPort < 1 || form.inPort > 65535)) { - newErrors.inPort = '端口号必须在1-65535之间'; - } - - if (selectedTunnel && selectedTunnel.inNodePortSta && selectedTunnel.inNodePortEnd && form.inPort) { - if (form.inPort < selectedTunnel.inNodePortSta || form.inPort > selectedTunnel.inNodePortEnd) { - newErrors.inPort = `端口号必须在${selectedTunnel.inNodePortSta}-${selectedTunnel.inNodePortEnd}范围内`; - } - } - setErrors(newErrors); return Object.keys(newErrors).length === 0; }; @@ -446,7 +439,6 @@ export default function ForwardPage() { interfaceName: '', strategy: 'fifo' }); - setSelectedTunnel(null); setErrors({}); setModalOpen(true); }; @@ -464,8 +456,6 @@ export default function ForwardPage() { interfaceName: forward.interfaceName || '', strategy: forward.strategy || 'fifo' }); - const tunnel = tunnels.find(t => t.id === forward.tunnelId); - setSelectedTunnel(tunnel || null); setErrors({}); setModalOpen(true); }; @@ -511,8 +501,6 @@ export default function ForwardPage() { // 处理隧道选择变化 const handleTunnelChange = (tunnelId: string) => { - const tunnel = tunnels.find(t => t.id === parseInt(tunnelId)); - setSelectedTunnel(tunnel || null); setForm(prev => ({ ...prev, tunnelId: parseInt(tunnelId) })); }; @@ -538,9 +526,7 @@ export default function ForwardPage() { userId: form.userId, name: form.name, tunnelId: form.tunnelId, - inPort: form.inPort, remoteAddr: processedRemoteAddr, - interfaceName: form.interfaceName, strategy: addressCount > 1 ? form.strategy : 'fifo' }; res = await updateForward(updateData); @@ -549,9 +535,7 @@ export default function ForwardPage() { const createData = { name: form.name, tunnelId: form.tunnelId, - inPort: form.inPort, remoteAddr: processedRemoteAddr, - interfaceName: form.interfaceName, strategy: addressCount > 1 ? form.strategy : 'fifo' }; res = await createForward(createData); @@ -694,13 +678,28 @@ export default function ForwardPage() { // 格式化入口地址 const formatInAddress = (ipString: string, port: number): string => { - if (!ipString || !port) return ''; + if (!ipString) return ''; - const ips = ipString.split(',').map(ip => ip.trim()).filter(ip => ip); - if (ips.length === 0) return ''; + const items = ipString.split(',').map(item => item.trim()).filter(item => item); + if (items.length === 0) return ''; - if (ips.length === 1) { - const ip = ips[0]; + // 检查第一项是否已经包含端口(格式:IP:端口) + const firstItem = items[0]; + const hasPort = /:\d+$/.test(firstItem); + + if (hasPort) { + // inIp 已经包含完整的 IP:Port 组合 + if (items.length === 1) { + return items[0]; + } + return `${items[0]} (+${items.length - 1}个)`; + } + + // inIp 只包含IP,需要添加端口(兼容旧数据) + if (!port) return ''; + + if (items.length === 1) { + const ip = items[0]; if (ip.includes(':') && !ip.startsWith('[')) { return `[${ip}]:${port}`; } else { @@ -708,7 +707,7 @@ export default function ForwardPage() { } } - const firstIp = ips[0]; + const firstIp = items[0]; let formattedFirstIp; if (firstIp.includes(':') && !firstIp.startsWith('[')) { formattedFirstIp = `[${firstIp}]`; @@ -716,7 +715,7 @@ export default function ForwardPage() { formattedFirstIp = firstIp; } - return `${formattedFirstIp}:${port} (+${ips.length - 1})`; + return `${formattedFirstIp}:${port} (+${items.length - 1}个)`; }; // 格式化远程地址 @@ -744,18 +743,27 @@ export default function ForwardPage() { let addresses: string[]; if (port !== null) { // 入口地址处理 - const ips = addressString.split(',').map(ip => ip.trim()).filter(ip => ip); - if (ips.length <= 1) { + const items = addressString.split(',').map(item => item.trim()).filter(item => item); + if (items.length <= 1) { copyToClipboard(formatInAddress(addressString, port), title); return; } - addresses = ips.map(ip => { - if (ip.includes(':') && !ip.startsWith('[')) { - return `[${ip}]:${port}`; - } else { - return `${ip}:${port}`; - } - }); + + // 检查是否已经包含端口 + const hasPort = /:\d+$/.test(items[0]); + if (hasPort) { + // 已经包含完整的 IP:Port 组合,直接使用 + addresses = items; + } else { + // 只包含IP,需要添加端口 + addresses = items.map(ip => { + if (ip.includes(':') && !ip.startsWith('[')) { + return `[${ip}]:${port}`; + } else { + return `${ip}:${port}`; + } + }); + } } else { // 远程地址处理 addresses = addressString.split(',').map(addr => addr.trim()).filter(addr => addr); @@ -1588,6 +1596,8 @@ export default function ForwardPage() { isInvalid={!!errors.tunnelId} errorMessage={errors.tunnelId} variant="bordered" + isDisabled={isEdit} + description={isEdit ? "编辑时无法修改关联隧道" : undefined} > {tunnels.map((tunnel) => ( @@ -1596,25 +1606,6 @@ export default function ForwardPage() { ))} - setForm(prev => ({ - ...prev, - inPort: e.target.value ? parseInt(e.target.value) : null - }))} - isInvalid={!!errors.inPort} - errorMessage={errors.inPort} - variant="bordered" - description={ - selectedTunnel && selectedTunnel.inNodePortSta && selectedTunnel.inNodePortEnd - ? `允许范围: ${selectedTunnel.inNodePortSta}-${selectedTunnel.inNodePortEnd}` - : '留空将自动分配可用端口' - } - /> -