mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-09-28 15:46:38 +08:00
Compare commits
13 Commits
1.4.2
...
2.0.6-beta
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b2974726c | |||
| 43aebdf8f4 | |||
| 37ea338a35 | |||
| 4063d39d6a | |||
| f6dd3ad657 | |||
| 252c16de29 | |||
| 4fef8eeb32 | |||
| 9146eb0375 | |||
| ad974875e4 | |||
| acd00ae413 | |||
| 65ca18d649 | |||
| a27b4d781e | |||
| ba50076468 |
@@ -2,12 +2,13 @@ name: Build and Push Images Based on Version
|
||||
|
||||
# 在这里定义统一版本号
|
||||
env:
|
||||
VERSION: "1.4.2"
|
||||
VERSION: "2.0.6-beta"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
|
||||
jobs:
|
||||
check-version:
|
||||
@@ -16,10 +17,11 @@ jobs:
|
||||
outputs:
|
||||
version: ${{ env.VERSION }}
|
||||
should_build: ${{ steps.check-tag.outputs.should_build }}
|
||||
tag_exists: ${{ steps.check-tag.outputs.tag_exists }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # 获取所有历史和 tags
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Display version
|
||||
run: |
|
||||
@@ -29,11 +31,13 @@ jobs:
|
||||
id: check-tag
|
||||
run: |
|
||||
if git rev-parse "${{ env.VERSION }}" >/dev/null 2>&1; then
|
||||
echo "Tag ${{ env.VERSION }} already exists, skip build"
|
||||
echo "Tag ${{ env.VERSION }} already exists, will only update files"
|
||||
echo "should_build=false" >> $GITHUB_OUTPUT
|
||||
echo "tag_exists=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Tag ${{ env.VERSION }} does not exist, will build all components"
|
||||
echo "should_build=true" >> $GITHUB_OUTPUT
|
||||
echo "tag_exists=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
build-gost:
|
||||
@@ -66,21 +70,37 @@ jobs:
|
||||
sudo mv upx-4.2.1-amd64_linux/upx /usr/local/bin/
|
||||
rm -rf upx-4.2.1-amd64_linux*
|
||||
|
||||
- name: Build GOST binary
|
||||
- name: Build GOST binary (AMD64)
|
||||
working-directory: ./go-gost
|
||||
run: |
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o gost
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o gost-amd64
|
||||
|
||||
- name: Compress with UPX
|
||||
- name: Build GOST binary (ARM64)
|
||||
working-directory: ./go-gost
|
||||
run: |
|
||||
upx --best --lzma gost
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o gost-arm64
|
||||
|
||||
- name: Upload GOST artifact
|
||||
- name: Compress with UPX (AMD64)
|
||||
working-directory: ./go-gost
|
||||
run: |
|
||||
upx --best --lzma gost-amd64
|
||||
|
||||
- name: Compress with UPX (ARM64)
|
||||
working-directory: ./go-gost
|
||||
run: |
|
||||
upx --best --lzma gost-arm64
|
||||
|
||||
- name: Upload GOST AMD64 artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: gost-binary
|
||||
path: ./go-gost/gost
|
||||
name: gost-binary-amd64
|
||||
path: ./go-gost/gost-amd64
|
||||
|
||||
- name: Upload GOST ARM64 artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: gost-binary-arm64
|
||||
path: ./go-gost/gost-arm64
|
||||
|
||||
build-vite:
|
||||
name: Build & Push Vite Frontend
|
||||
@@ -108,7 +128,7 @@ jobs:
|
||||
run: |
|
||||
VERSION="${{ needs.check-version.outputs.version }}"
|
||||
docker buildx build \
|
||||
--platform linux/amd64 \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
-t bqlpfy/vite-frontend:latest \
|
||||
-t bqlpfy/vite-frontend:${VERSION} \
|
||||
@@ -152,7 +172,7 @@ jobs:
|
||||
run: |
|
||||
VERSION="${{ needs.check-version.outputs.version }}"
|
||||
docker buildx build \
|
||||
--platform linux/amd64 \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
-t bqlpfy/springboot-backend:latest \
|
||||
-t bqlpfy/springboot-backend:${VERSION} \
|
||||
@@ -168,11 +188,22 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Download GOST binary
|
||||
- name: Download GOST AMD64 binary
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: gost-binary
|
||||
path: ./artifacts
|
||||
name: gost-binary-amd64
|
||||
path: ./artifacts/amd64
|
||||
|
||||
- name: Download GOST ARM64 binary
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: gost-binary-arm64
|
||||
path: ./artifacts/arm64
|
||||
|
||||
- name: Rename binaries
|
||||
run: |
|
||||
mv ./artifacts/amd64/gost-amd64 ./artifacts/gost-amd64
|
||||
mv ./artifacts/arm64/gost-arm64 ./artifacts/gost-arm64
|
||||
|
||||
- name: Create Release
|
||||
env:
|
||||
@@ -184,39 +215,63 @@ jobs:
|
||||
git tag "${VERSION}" ${{ github.sha }}
|
||||
git push origin "${VERSION}"
|
||||
|
||||
# 获取 commit 信息
|
||||
COMMIT_MSG=$(git log -1 --pretty=format:"%s")
|
||||
COMMIT_AUTHOR=$(git log -1 --pretty=format:"%an")
|
||||
COMMIT_DATE=$(git log -1 --pretty=format:"%ai")
|
||||
|
||||
# 创建 release
|
||||
gh release create "${VERSION}" \
|
||||
--title "Release ${VERSION}" \
|
||||
--notes "Version ${VERSION}
|
||||
|
||||
## Components
|
||||
- **GOST Binary** (Linux AMD64, compressed with UPX)
|
||||
- **Vite Frontend** Docker Image: \`bqlpfy/vite-frontend:${VERSION}\` / \`bqlpfy/vite-frontend:latest\`
|
||||
- **Spring Boot Backend** Docker Image: \`bqlpfy/springboot-backend:${VERSION}\` / \`bqlpfy/springboot-backend:latest\`
|
||||
|
||||
## Installation Files
|
||||
- \`install.sh\` - GOST 客户端安装脚本
|
||||
- \`panel_install.sh\` - 面板安装脚本
|
||||
- \`gost.sql\` - 数据库初始化文件
|
||||
- \`docker-compose-v4.yml\` - Docker Compose 配置 (IPv4)
|
||||
- \`docker-compose-v6.yml\` - Docker Compose 配置 (IPv6)
|
||||
|
||||
## Details
|
||||
- Build date: $(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
- Commit: ${{ github.sha }}" \
|
||||
--notes "## 📝 Commit Information
|
||||
- **Message**: ${COMMIT_MSG}
|
||||
- **Author**: ${COMMIT_AUTHOR}
|
||||
- **Date**: ${COMMIT_DATE}
|
||||
- **Commit**: [\`${GITHUB_SHA:0:7}\`](https://github.com/${{ github.repository }}/commit/${{ github.sha }})" \
|
||||
--repo ${{ github.repository }}
|
||||
|
||||
# 上传所有文件到 release
|
||||
echo "📤 上传 GOST 二进制..."
|
||||
gh release upload "${VERSION}" ./artifacts/gost --clobber
|
||||
echo "📤 上传 GOST 二进制文件..."
|
||||
gh release upload "${VERSION}" ./artifacts/gost-amd64 --clobber
|
||||
gh release upload "${VERSION}" ./artifacts/gost-arm64 --clobber
|
||||
|
||||
echo "📤 上传安装脚本..."
|
||||
gh release upload "${VERSION}" ./install.sh --clobber
|
||||
gh release upload "${VERSION}" ./panel_install.sh --clobber
|
||||
|
||||
echo "📤 上传配置文件..."
|
||||
gh release upload "${VERSION}" ./gost.sql --clobber
|
||||
echo "📤 上传 Docker Compose 配置文件..."
|
||||
gh release upload "${VERSION}" ./docker-compose-v4.yml --clobber
|
||||
gh release upload "${VERSION}" ./docker-compose-v6.yml --clobber
|
||||
|
||||
echo "✅ 所有文件已上传到 Release ${VERSION}"
|
||||
|
||||
update-release-files:
|
||||
name: Update Release Files Only
|
||||
needs: check-version
|
||||
if: needs.check-version.outputs.tag_exists == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Update Release Files
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
VERSION="${{ needs.check-version.outputs.version }}"
|
||||
|
||||
echo "📤 更新 Release ${VERSION} 中的脚本和配置文件..."
|
||||
|
||||
# 上传安装脚本(覆盖)
|
||||
echo "📤 更新安装脚本..."
|
||||
gh release upload "${VERSION}" ./install.sh --clobber
|
||||
gh release upload "${VERSION}" ./panel_install.sh --clobber
|
||||
|
||||
# 上传 Docker Compose 配置文件(覆盖)
|
||||
echo "📤 更新 Docker Compose 配置文件..."
|
||||
gh release upload "${VERSION}" ./docker-compose-v4.yml --clobber
|
||||
gh release upload "${VERSION}" ./docker-compose-v6.yml --clobber
|
||||
|
||||
echo "✅ 文件更新完成!Release ${VERSION} 中的脚本和配置已更新"
|
||||
echo "ℹ️ Docker 镜像和 GOST 二进制未重新构建"
|
||||
|
||||
+2
-1
@@ -256,4 +256,5 @@ gitee/
|
||||
*.cert
|
||||
doraemon.jks
|
||||
device.id
|
||||
commit.sh
|
||||
commit.sh
|
||||
sql/
|
||||
@@ -1,6 +1,39 @@
|
||||
|
||||
# flux-panel转发面板 哆啦A梦转发面板
|
||||
|
||||
# 赞助商
|
||||
<p align="center">
|
||||
<a href="https://vps.town" style="margin: 0 20px; text-align:center;">
|
||||
<img src="./doc/vpstown.png" width="300">
|
||||
</a>
|
||||
|
||||
<a href="https://whmcs.as211392.com" style="margin: 0 20px; text-align:center;">
|
||||
<img src="./doc/as211392.png" width="300">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
# 1.x无法无损升级到2.x请备份好数据
|
||||
1.x迁移2.x流程如下
|
||||
1.进入1.x的后台在转发页面导出所有转发保存
|
||||
2.使用1.x节点脚本卸载所有节点
|
||||
```
|
||||
curl -L https://github.com/bqlpfy/flux-panel/releases/download/1.4.2/install.sh -o install.sh && chmod +x install.sh && ./install.sh
|
||||
```
|
||||
3.使用1.x面版脚本卸载面板
|
||||
|
||||
```
|
||||
curl -L https://github.com/bqlpfy/flux-panel/releases/download/1.4.2/panel_install.sh -o panel_install.sh && chmod +x panel_install.sh && ./panel_install.sh
|
||||
```
|
||||
4.使用2.x脚本安装面板
|
||||
|
||||
```
|
||||
curl -L https://raw.githubusercontent.com/bqlpfy/flux-panel/refs/heads/main/panel_install.sh -o panel_install.sh && chmod +x panel_install.sh && ./panel_install.sh
|
||||
```
|
||||
|
||||
|
||||
使用1.x版本的请使用上面给出的脚本,不要使用最新的,最新的默认2.x
|
||||
嫌弃麻烦或者想稳定的我不推荐你升级到2.x版本 目前还在beta阶段,很多问题。
|
||||
|
||||
|
||||
本项目基于 [go-gost/gost](https://github.com/go-gost/gost) 和 [go-gost/x](https://github.com/go-gost/x) 两个开源库,实现了转发面板。
|
||||
---
|
||||
@@ -17,10 +50,26 @@
|
||||
## 部署流程
|
||||
---
|
||||
### Docker Compose部署
|
||||
#### 快速部署
|
||||
面板端(1.x):
|
||||
```bash
|
||||
curl -L https://github.com/bqlpfy/flux-panel/releases/download/1.4.2/panel_install.sh -o panel_install.sh && chmod +x panel_install.sh && ./panel_install.sh
|
||||
```
|
||||
节点端(1.x):
|
||||
```bash
|
||||
curl -L https://github.com/bqlpfy/flux-panel/releases/download/1.4.2/install.sh -o install.sh && chmod +x install.sh && ./install.sh
|
||||
|
||||
```
|
||||
|
||||
面板端(2.x):
|
||||
```bash
|
||||
curl -L https://raw.githubusercontent.com/bqlpfy/flux-panel/refs/heads/main/panel_install.sh -o panel_install.sh && chmod +x panel_install.sh && ./panel_install.sh
|
||||
```
|
||||
节点端(2.x):
|
||||
```bash
|
||||
curl -L https://raw.githubusercontent.com/bqlpfy/flux-panel/refs/heads/main/install.sh -o install.sh && chmod +x install.sh && ./install.sh
|
||||
|
||||
```
|
||||
|
||||
#### 默认管理员账号
|
||||
|
||||
@@ -55,7 +104,7 @@ curl -L https://raw.githubusercontent.com/bqlpfy/flux-panel/refs/heads/main/pane
|
||||
请务必在合法、合规、安全的前提下使用本项目。
|
||||
|
||||
---
|
||||
## ⭐ 喝杯咖啡!
|
||||
## ⭐ 喝杯咖啡!(USDT)
|
||||
|
||||
| 网络 | 地址 |
|
||||
|------------|----------------------------------------------------------------------|
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
+9
-38
@@ -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.6-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,20 +12,20 @@ services:
|
||||
- "${BACKEND_PORT}:6365"
|
||||
volumes:
|
||||
- backend_logs:/app/logs
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
- sqlite_data:/app/data
|
||||
networks:
|
||||
- gost-network
|
||||
stop_grace_period: 30s
|
||||
stop_signal: SIGTERM
|
||||
healthcheck:
|
||||
test: ["CMD", "sh", "-c", "wget --no-verbose --tries=1 --spider http://localhost:6365/flow/test || exit 1"]
|
||||
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.6-beta
|
||||
container_name: vite-frontend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
@@ -67,8 +38,8 @@ services:
|
||||
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
name: mysql_data
|
||||
sqlite_data:
|
||||
name: sqlite_data
|
||||
driver: local
|
||||
backend_logs:
|
||||
name: backend_logs
|
||||
|
||||
+9
-38
@@ -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.6-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,20 +12,20 @@ services:
|
||||
- "${BACKEND_PORT}:6365"
|
||||
volumes:
|
||||
- backend_logs:/app/logs
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
- sqlite_data:/app/data
|
||||
networks:
|
||||
- gost-network
|
||||
stop_grace_period: 30s
|
||||
stop_signal: SIGTERM
|
||||
healthcheck:
|
||||
test: ["CMD", "sh", "-c", "wget --no-verbose --tries=1 --spider http://localhost:6365/flow/test || exit 1"]
|
||||
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.6-beta
|
||||
container_name: vite-frontend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
@@ -67,8 +38,8 @@ services:
|
||||
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
name: mysql_data
|
||||
sqlite_data:
|
||||
name: sqlite_data
|
||||
driver: local
|
||||
backend_logs:
|
||||
name: backend_logs
|
||||
|
||||
+1
-1
@@ -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.1")
|
||||
defer wsReporter.Stop()
|
||||
service.SetHTTPReportURL(config.Addr, config.Secret)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -87,6 +87,9 @@ type TcpPingResponse struct {
|
||||
|
||||
type WebSocketReporter struct {
|
||||
url string
|
||||
addr string // 保存服务器地址
|
||||
secret string // 保存密钥
|
||||
version string // 保存版本号
|
||||
conn *websocket.Conn
|
||||
reconnectTime time.Duration
|
||||
pingInterval time.Duration
|
||||
@@ -195,7 +198,25 @@ func (w *WebSocketReporter) connect() error {
|
||||
w.connecting = false
|
||||
}()
|
||||
|
||||
u, err := url.Parse(w.url)
|
||||
// 重新读取 config.json 获取最新的协议配置
|
||||
type LocalConfig struct {
|
||||
Addr string `json:"addr"`
|
||||
Secret string `json:"secret"`
|
||||
Http int `json:"http"`
|
||||
Tls int `json:"tls"`
|
||||
Socks int `json:"socks"`
|
||||
}
|
||||
|
||||
var cfg LocalConfig
|
||||
if b, err := os.ReadFile("config.json"); err == nil {
|
||||
json.Unmarshal(b, &cfg)
|
||||
}
|
||||
|
||||
// 使用最新的配置重新构建 URL
|
||||
currentURL := "ws://" + w.addr + "/system-info?type=1&secret=" + w.secret + "&version=" + w.version +
|
||||
"&http=" + strconv.Itoa(cfg.Http) + "&tls=" + strconv.Itoa(cfg.Tls) + "&socks=" + strconv.Itoa(cfg.Socks)
|
||||
|
||||
u, err := url.Parse(currentURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("解析URL失败: %v", err)
|
||||
}
|
||||
@@ -225,7 +246,7 @@ func (w *WebSocketReporter) connect() error {
|
||||
return nil
|
||||
})
|
||||
|
||||
fmt.Printf("✅ WebSocket连接建立成功\n")
|
||||
fmt.Printf("✅ WebSocket连接建立成功 (http=%d, tls=%d, socks=%d)\n", cfg.Http, cfg.Tls, cfg.Socks)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1021,12 +1042,16 @@ func getMemoryInfo() MemoryInfo {
|
||||
// StartWebSocketReporterWithConfig 使用配置字段启动WebSocket报告器
|
||||
func StartWebSocketReporterWithConfig(addr string, secret string, http int, tls int, socks int, version string) *WebSocketReporter {
|
||||
|
||||
// 构建包含本机IP的WebSocket URL
|
||||
// 构建初始 WebSocket URL
|
||||
fullURL := "ws://" + addr + "/system-info?type=1&secret=" + secret + "&version=" + version + "&http=" + strconv.Itoa(http) + "&tls=" + strconv.Itoa(tls) + "&socks=" + strconv.Itoa(socks)
|
||||
|
||||
fmt.Printf("🔗 WebSocket连接URL: %s\n", fullURL)
|
||||
|
||||
reporter := NewWebSocketReporter(fullURL, secret)
|
||||
// 保存 addr, secret, version 供重连时使用
|
||||
reporter.addr = addr
|
||||
reporter.secret = secret
|
||||
reporter.version = version
|
||||
reporter.Start()
|
||||
return reporter
|
||||
}
|
||||
|
||||
@@ -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 */;
|
||||
+74
-56
@@ -1,7 +1,30 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 获取系统架构
|
||||
get_architecture() {
|
||||
ARCH=$(uname -m)
|
||||
case $ARCH in
|
||||
x86_64)
|
||||
echo "amd64"
|
||||
;;
|
||||
aarch64|arm64)
|
||||
echo "arm64"
|
||||
;;
|
||||
*)
|
||||
echo "amd64" # 默认使用 amd64
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# 构建下载地址
|
||||
build_download_url() {
|
||||
local ARCH=$(get_architecture)
|
||||
echo "https://github.com/bqlpfy/flux-panel/releases/download/2.0.6-beta/gost-${ARCH}"
|
||||
}
|
||||
|
||||
# 下载地址
|
||||
DOWNLOAD_URL="https://github.com/bqlpfy/flux-panel/releases/download/1.4.2/gost"
|
||||
INSTALL_DIR="/etc/gost"
|
||||
DOWNLOAD_URL=$(build_download_url)
|
||||
INSTALL_DIR="/etc/flux_agent"
|
||||
COUNTRY=$(curl -s https://ipinfo.io/country)
|
||||
if [ "$COUNTRY" = "CN" ]; then
|
||||
# 拼接 URL
|
||||
@@ -131,8 +154,8 @@ while getopts "a:s:" opt; do
|
||||
done
|
||||
|
||||
# 安装功能
|
||||
install_gost() {
|
||||
echo "🚀 开始安装 GOST..."
|
||||
install_flux_agent() {
|
||||
echo "🚀 开始安装 flux_agent..."
|
||||
get_config_params
|
||||
|
||||
# 检查并安装 tcpkill
|
||||
@@ -142,27 +165,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 +212,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" <<EOF
|
||||
[Unit]
|
||||
Description=Gost Proxy Service
|
||||
Description=Flux_agent Proxy Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
WorkingDirectory=$INSTALL_DIR
|
||||
ExecStart=$INSTALL_DIR/gost
|
||||
ExecStart=$INSTALL_DIR/flux_agent
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
@@ -206,27 +229,27 @@ EOF
|
||||
|
||||
# 启动服务
|
||||
systemctl daemon-reload
|
||||
systemctl enable gost
|
||||
systemctl start gost
|
||||
systemctl enable flux_agent
|
||||
systemctl start flux_agent
|
||||
|
||||
# 检查状态
|
||||
echo "🔄 检查服务状态..."
|
||||
if systemctl is-active --quiet gost; then
|
||||
echo "✅ 安装完成,gost服务已启动并设置为开机启动。"
|
||||
if systemctl is-active --quiet flux_agent; then
|
||||
echo "✅ 安装完成,flux_agent服务已启动并设置为开机启动。"
|
||||
echo "📁 配置目录: $INSTALL_DIR"
|
||||
echo "🔧 服务状态: $(systemctl is-active gost)"
|
||||
echo "🔧 服务状态: $(systemctl is-active flux_agent)"
|
||||
else
|
||||
echo "❌ gost服务启动失败,请执行以下命令查看日志:"
|
||||
echo "journalctl -u gost -f"
|
||||
echo "❌ flux_agent服务启动失败,请执行以下命令查看日志:"
|
||||
echo "journalctl -u flux_agent -f"
|
||||
fi
|
||||
}
|
||||
|
||||
# 更新功能
|
||||
update_gost() {
|
||||
echo "🔄 开始更新 GOST..."
|
||||
update_flux_agent() {
|
||||
echo "🔄 开始更新 flux_agent..."
|
||||
|
||||
if [[ ! -d "$INSTALL_DIR" ]]; then
|
||||
echo "❌ GOST 未安装,请先选择安装。"
|
||||
echo "❌ flux_agent 未安装,请先选择安装。"
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -237,52 +260,52 @@ update_gost() {
|
||||
|
||||
# 先下载新版本
|
||||
echo "⬇️ 下载最新版本..."
|
||||
curl -L "$DOWNLOAD_URL" -o "$INSTALL_DIR/gost.new"
|
||||
if [[ ! -f "$INSTALL_DIR/gost.new" || ! -s "$INSTALL_DIR/gost.new" ]]; then
|
||||
curl -L "$DOWNLOAD_URL" -o "$INSTALL_DIR/flux_agent.new"
|
||||
if [[ ! -f "$INSTALL_DIR/flux_agent.new" || ! -s "$INSTALL_DIR/flux_agent.new" ]]; then
|
||||
echo "❌ 下载失败。"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 停止服务
|
||||
if systemctl list-units --full -all | grep -Fq "gost.service"; then
|
||||
echo "🛑 停止 gost 服务..."
|
||||
systemctl stop gost
|
||||
if systemctl list-units --full -all | grep -Fq "flux_agent.service"; then
|
||||
echo "🛑 停止 flux_agent 服务..."
|
||||
systemctl stop flux_agent
|
||||
fi
|
||||
|
||||
# 替换文件
|
||||
mv "$INSTALL_DIR/gost.new" "$INSTALL_DIR/gost"
|
||||
chmod +x "$INSTALL_DIR/gost"
|
||||
mv "$INSTALL_DIR/flux_agent.new" "$INSTALL_DIR/flux_agent"
|
||||
chmod +x "$INSTALL_DIR/flux_agent"
|
||||
|
||||
# 打印版本
|
||||
echo "🔎 新版本:$($INSTALL_DIR/gost -V)"
|
||||
echo "🔎 新版本:$($INSTALL_DIR/flux_agent -V)"
|
||||
|
||||
# 重启服务
|
||||
echo "🔄 重启服务..."
|
||||
systemctl start gost
|
||||
systemctl start flux_agent
|
||||
|
||||
echo "✅ 更新完成,服务已重新启动。"
|
||||
}
|
||||
|
||||
# 卸载功能
|
||||
uninstall_gost() {
|
||||
echo "🗑️ 开始卸载 GOST..."
|
||||
uninstall_flux_agent() {
|
||||
echo "🗑️ 开始卸载 flux_agent..."
|
||||
|
||||
read -p "确认卸载 GOST 吗?此操作将删除所有相关文件 (y/N): " confirm
|
||||
read -p "确认卸载 flux_agent 吗?此操作将删除所有相关文件 (y/N): " confirm
|
||||
if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
|
||||
echo "❌ 取消卸载"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# 停止并禁用服务
|
||||
if systemctl list-units --full -all | grep -Fq "gost.service"; then
|
||||
if systemctl list-units --full -all | grep -Fq "flux_agent.service"; then
|
||||
echo "🛑 停止并禁用服务..."
|
||||
systemctl stop gost 2>/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 +325,7 @@ uninstall_gost() {
|
||||
main() {
|
||||
# 如果提供了命令行参数,直接执行安装
|
||||
if [[ -n "$SERVER_ADDR" && -n "$SECRET" ]]; then
|
||||
install_gost
|
||||
install_flux_agent
|
||||
delete_self
|
||||
exit 0
|
||||
fi
|
||||
@@ -310,36 +333,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
|
||||
|
||||
+14
-775
@@ -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.6-beta/docker-compose-v4.yml"
|
||||
DOCKER_COMPOSEV6_URL="https://github.com/bqlpfy/flux-panel/releases/download/2.0.6-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 <<EOF
|
||||
DB_NAME=$DB_NAME
|
||||
DB_USER=$DB_USER
|
||||
DB_PASSWORD=$DB_PASSWORD
|
||||
JWT_SECRET=$JWT_SECRET
|
||||
FRONTEND_PORT=$FRONTEND_PORT
|
||||
BACKEND_PORT=$BACKEND_PORT
|
||||
@@ -252,7 +234,15 @@ update_panel() {
|
||||
configure_docker_ipv6
|
||||
fi
|
||||
|
||||
echo "🛑 停止当前服务..."
|
||||
# 先发送 SIGTERM 信号,让应用优雅关闭
|
||||
docker stop -t 30 springboot-backend 2>/dev/null || true
|
||||
docker stop -t 10 vite-frontend 2>/dev/null || true
|
||||
|
||||
# 等待 WAL 文件同步
|
||||
echo "⏳ 等待数据同步..."
|
||||
sleep 5
|
||||
|
||||
# 然后再完全停止
|
||||
$DOCKER_CMD down
|
||||
|
||||
echo "⬇️ 拉取最新镜像..."
|
||||
@@ -295,755 +285,9 @@ update_panel() {
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# 检查数据库容器健康状态
|
||||
echo "🔍 检查数据库服务状态..."
|
||||
for i in {1..60}; do
|
||||
if docker ps --format "{{.Names}}" | grep -q "^gost-mysql$"; then
|
||||
DB_HEALTH=$(docker inspect -f '{{.State.Health.Status}}' gost-mysql 2>/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 <<EOF
|
||||
-- 数据库结构更新
|
||||
USE \`$DB_NAME\`;
|
||||
|
||||
-- user 表:删除 name 字段(如果存在)
|
||||
SET @sql = (
|
||||
SELECT IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'user'
|
||||
AND column_name = 'name'
|
||||
),
|
||||
'ALTER TABLE \`user\` DROP COLUMN \`name\`;',
|
||||
'SELECT "Column \`name\` not exists in \`user\`";'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- node 表:删除 port 字段、添加 server_ip 字段(如果不存在)
|
||||
SET @sql = (
|
||||
SELECT IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'node'
|
||||
AND column_name = 'port'
|
||||
),
|
||||
'ALTER TABLE \`node\` DROP COLUMN \`port\`;',
|
||||
'SELECT "Column \`port\` not exists in \`node\`";'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = (
|
||||
SELECT IF(
|
||||
NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'node'
|
||||
AND column_name = 'server_ip'
|
||||
),
|
||||
'ALTER TABLE \`node\` ADD COLUMN \`server_ip\` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;',
|
||||
'SELECT "Column \`server_ip\` already exists in \`node\`";'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 将 ip 赋值给 server_ip(如果字段都存在)
|
||||
UPDATE \`node\`
|
||||
SET \`server_ip\` = \`ip\`
|
||||
WHERE \`server_ip\` IS NULL;
|
||||
|
||||
-- node 表:修改 ip 字段类型为 longtext
|
||||
SET @sql = (
|
||||
SELECT IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'node'
|
||||
AND column_name = 'ip'
|
||||
AND data_type = 'varchar'
|
||||
),
|
||||
'ALTER TABLE \`node\` MODIFY COLUMN \`ip\` LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;',
|
||||
'SELECT "Column \`ip\` not exists or already modified in \`node\`";'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- node 表:添加 version 字段(如果不存在)
|
||||
SET @sql = (
|
||||
SELECT IF(
|
||||
NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'node'
|
||||
AND column_name = 'version'
|
||||
),
|
||||
'ALTER TABLE \`node\` ADD COLUMN \`version\` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL;',
|
||||
'SELECT "Column \`version\` already exists in \`node\`";'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- node 表:添加 port_sta 字段(如果不存在)
|
||||
SET @sql = (
|
||||
SELECT IF(
|
||||
NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'node'
|
||||
AND column_name = 'port_sta'
|
||||
),
|
||||
'ALTER TABLE \`node\` ADD COLUMN \`port_sta\` INT(10) DEFAULT 1000 COMMENT "端口起始范围";',
|
||||
'SELECT "Column \`port_sta\` already exists in \`node\`";'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- node 表:添加 port_end 字段(如果不存在)
|
||||
SET @sql = (
|
||||
SELECT IF(
|
||||
NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'node'
|
||||
AND column_name = 'port_end'
|
||||
),
|
||||
'ALTER TABLE \`node\` ADD COLUMN \`port_end\` INT(10) DEFAULT 65535 COMMENT "端口结束范围";',
|
||||
'SELECT "Column \`port_end\` already exists in \`node\`";'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 为现有节点设置默认端口范围
|
||||
UPDATE \`node\`
|
||||
SET \`port_sta\` = 1000, \`port_end\` = 65535
|
||||
WHERE \`port_sta\` IS NULL OR \`port_end\` IS NULL;
|
||||
|
||||
-- node 表:添加 http、tls、socks 字段(如果不存在)
|
||||
SET @sql = (
|
||||
SELECT IF(
|
||||
NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'node'
|
||||
AND column_name = 'http'
|
||||
),
|
||||
'ALTER TABLE \`node\` ADD COLUMN \`http\` INT(10) DEFAULT 0 COMMENT "HTTP 服务端口";',
|
||||
'SELECT "Column \`http\` already exists in \`node\`";'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = (
|
||||
SELECT IF(
|
||||
NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'node'
|
||||
AND column_name = 'tls'
|
||||
),
|
||||
'ALTER TABLE \`node\` ADD COLUMN \`tls\` INT(10) DEFAULT 0 COMMENT "TLS 服务端口";',
|
||||
'SELECT "Column \`tls\` already exists in \`node\`";'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = (
|
||||
SELECT IF(
|
||||
NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'node'
|
||||
AND column_name = 'socks'
|
||||
),
|
||||
'ALTER TABLE \`node\` ADD COLUMN \`socks\` INT(10) DEFAULT 0 COMMENT "SOCKS 服务端口";',
|
||||
'SELECT "Column \`socks\` already exists in \`node\`";'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 为现有节点设置 http、tls、socks 默认值
|
||||
UPDATE \`node\`
|
||||
SET \`http\` = IFNULL(\`http\`, 0),
|
||||
\`tls\` = IFNULL(\`tls\`, 0),
|
||||
\`socks\` = IFNULL(\`socks\`, 0);
|
||||
|
||||
-- tunnel 表:删除废弃字段(如果存在)
|
||||
SET @sql = (
|
||||
SELECT IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'tunnel'
|
||||
AND column_name = 'in_port_sta'
|
||||
),
|
||||
'ALTER TABLE \`tunnel\` DROP COLUMN \`in_port_sta\`;',
|
||||
'SELECT "Column \`in_port_sta\` not exists in \`tunnel\`";'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = (
|
||||
SELECT IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'tunnel'
|
||||
AND column_name = 'in_port_end'
|
||||
),
|
||||
'ALTER TABLE \`tunnel\` DROP COLUMN \`in_port_end\`;',
|
||||
'SELECT "Column \`in_port_end\` not exists in \`tunnel\`";'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = (
|
||||
SELECT IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'tunnel'
|
||||
AND column_name = 'out_ip_sta'
|
||||
),
|
||||
'ALTER TABLE \`tunnel\` DROP COLUMN \`out_ip_sta\`;',
|
||||
'SELECT "Column \`out_ip_sta\` not exists in \`tunnel\`";'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = (
|
||||
SELECT IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'tunnel'
|
||||
AND column_name = 'out_ip_end'
|
||||
),
|
||||
'ALTER TABLE \`tunnel\` DROP COLUMN \`out_ip_end\`;',
|
||||
'SELECT "Column \`out_ip_end\` not exists in \`tunnel\`";'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- tunnel 表:添加 tcp_listen_addr、udp_listen_addr、protocol(如果不存在)
|
||||
|
||||
-- tcp_listen_addr
|
||||
SET @sql = (
|
||||
SELECT IF(
|
||||
NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'tunnel'
|
||||
AND column_name = 'tcp_listen_addr'
|
||||
),
|
||||
'ALTER TABLE \`tunnel\` ADD COLUMN \`tcp_listen_addr\` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT "0.0.0.0";',
|
||||
'SELECT "Column \`tcp_listen_addr\` already exists in \`tunnel\`";'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- udp_listen_addr
|
||||
SET @sql = (
|
||||
SELECT IF(
|
||||
NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'tunnel'
|
||||
AND column_name = 'udp_listen_addr'
|
||||
),
|
||||
'ALTER TABLE \`tunnel\` ADD COLUMN \`udp_listen_addr\` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT "0.0.0.0";',
|
||||
'SELECT "Column \`udp_listen_addr\` already exists in \`tunnel\`";'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- protocol
|
||||
SET @sql = (
|
||||
SELECT IF(
|
||||
NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'tunnel'
|
||||
AND column_name = 'protocol'
|
||||
),
|
||||
'ALTER TABLE \`tunnel\` ADD COLUMN \`protocol\` VARCHAR(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT "tls";',
|
||||
'SELECT "Column \`protocol\` already exists in \`tunnel\`";'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- traffic_ratio (流量倍率)
|
||||
SET @sql = (
|
||||
SELECT IF(
|
||||
NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'tunnel'
|
||||
AND column_name = 'traffic_ratio'
|
||||
),
|
||||
'ALTER TABLE \`tunnel\` ADD COLUMN \`traffic_ratio\` DECIMAL(5,1) DEFAULT 1.0 COMMENT "流量倍率";',
|
||||
'SELECT "Column \`traffic_ratio\` already exists in \`tunnel\`";'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 为现有数据设置默认流量倍率
|
||||
UPDATE \`tunnel\`
|
||||
SET \`traffic_ratio\` = 1.0
|
||||
WHERE \`traffic_ratio\` IS NULL;
|
||||
|
||||
-- forward 表:删除 proxy_protocol 字段(如果存在)
|
||||
SET @sql = (
|
||||
SELECT IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'forward'
|
||||
AND column_name = 'proxy_protocol'
|
||||
),
|
||||
'ALTER TABLE \`forward\` DROP COLUMN \`proxy_protocol\`;',
|
||||
'SELECT "Column \`proxy_protocol\` not exists in \`forward\`";'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- forward 表:修改 remote_addr 字段类型为 longtext
|
||||
SET @sql = (
|
||||
SELECT IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'forward'
|
||||
AND column_name = 'remote_addr'
|
||||
AND data_type = 'varchar'
|
||||
),
|
||||
'ALTER TABLE \`forward\` MODIFY COLUMN \`remote_addr\` LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL;',
|
||||
'SELECT "Column \`remote_addr\` not exists or already modified in \`forward\`";'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- forward 表:添加 strategy 字段(负载均衡策略)
|
||||
SET @sql = (
|
||||
SELECT IF(
|
||||
NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'forward'
|
||||
AND column_name = 'strategy'
|
||||
),
|
||||
'ALTER TABLE \`forward\` ADD COLUMN \`strategy\` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT "fifo" COMMENT "负载均衡策略";',
|
||||
'SELECT "Column \`strategy\` already exists in \`forward\`";'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 为现有数据设置默认负载均衡策略
|
||||
UPDATE \`forward\`
|
||||
SET \`strategy\` = 'fifo'
|
||||
WHERE \`strategy\` IS NULL;
|
||||
|
||||
-- forward 表:添加 inx 字段(排序索引)
|
||||
SET @sql = (
|
||||
SELECT IF(
|
||||
NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'forward'
|
||||
AND column_name = 'inx'
|
||||
),
|
||||
'ALTER TABLE \`forward\` ADD COLUMN \`inx\` INT(10) DEFAULT 0 COMMENT "排序索引";',
|
||||
'SELECT "Column \`inx\` already exists in \`forward\`";'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 为现有数据设置默认排序索引
|
||||
UPDATE \`forward\`
|
||||
SET \`inx\` = 0
|
||||
WHERE \`inx\` IS NULL;
|
||||
|
||||
-- tunnel 表:添加 interface_name 字段(如果不存在)
|
||||
SET @sql = (
|
||||
SELECT IF(
|
||||
NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'tunnel'
|
||||
AND column_name = 'interface_name'
|
||||
),
|
||||
'ALTER TABLE \`tunnel\` ADD COLUMN \`interface_name\` VARCHAR(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL;',
|
||||
'SELECT "Column \`interface_name\` already exists in \`tunnel\`";'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- forward 表:添加 interface_name 字段(如果不存在)
|
||||
SET @sql = (
|
||||
SELECT IF(
|
||||
NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'forward'
|
||||
AND column_name = 'interface_name'
|
||||
),
|
||||
'ALTER TABLE \`forward\` ADD COLUMN \`interface_name\` VARCHAR(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL;',
|
||||
'SELECT "Column \`interface_name\` already exists in \`forward\`";'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 创建 vite_config 表(如果不存在)
|
||||
CREATE TABLE IF NOT EXISTS \`vite_config\` (
|
||||
\`id\` int(10) NOT NULL AUTO_INCREMENT,
|
||||
\`name\` varchar(200) NOT NULL,
|
||||
\`value\` varchar(200) NOT NULL,
|
||||
\`time\` bigint(20) NOT NULL,
|
||||
PRIMARY KEY (\`id\`),
|
||||
UNIQUE KEY \`unique_name\` (\`name\`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- 创建 statistics_flow 表(如果不存在)
|
||||
CREATE TABLE IF NOT EXISTS \`statistics_flow\` (
|
||||
\`id\` bigint(20) NOT NULL AUTO_INCREMENT,
|
||||
\`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,
|
||||
PRIMARY KEY (\`id\`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- statistics_flow 表:添加 created_time 字段(如果不存在)
|
||||
SET @sql = (
|
||||
SELECT IF(
|
||||
NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'statistics_flow'
|
||||
AND column_name = 'created_time'
|
||||
),
|
||||
'ALTER TABLE \`statistics_flow\` ADD COLUMN \`created_time\` BIGINT(20) NOT NULL DEFAULT 0 COMMENT "创建时间毫秒时间戳";',
|
||||
'SELECT "Column \`created_time\` already exists in \`statistics_flow\`";'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 为现有记录设置当前毫秒时间戳(仅当 created_time 为 0 或 NULL 时)
|
||||
UPDATE \`statistics_flow\`
|
||||
SET \`created_time\` = UNIX_TIMESTAMP() * 1000
|
||||
WHERE \`created_time\` = 0 OR \`created_time\` IS NULL;
|
||||
|
||||
EOF
|
||||
|
||||
# 检查数据库容器
|
||||
if ! docker ps --format "{{.Names}}" | grep -q "^gost-mysql$"; then
|
||||
echo "❌ 数据库容器 gost-mysql 未运行"
|
||||
echo "🔍 当前运行的容器:"
|
||||
docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"
|
||||
echo "❌ 数据库结构更新失败,请手动执行 temp_migration.sql"
|
||||
echo "📁 迁移文件已保存为 temp_migration.sql"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 执行数据库迁移
|
||||
if docker exec -i gost-mysql mysql -u "$DB_USER" -p"$DB_PASSWORD" < temp_migration.sql 2>/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 +312,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 +341,6 @@ main() {
|
||||
exit 0
|
||||
;;
|
||||
4)
|
||||
export_migration_sql
|
||||
delete_self
|
||||
exit 0
|
||||
;;
|
||||
5)
|
||||
echo "👋 退出脚本"
|
||||
delete_self
|
||||
exit 0
|
||||
|
||||
@@ -16,11 +16,12 @@ ENV LC_ALL=en_US.UTF-8
|
||||
RUN sed -i 's/archive.ubuntu.com/mirrors.ustc.edu.cn/g' /etc/apt/sources.list \
|
||||
&& sed -i 's/security.ubuntu.com/mirrors.ustc.edu.cn/g' /etc/apt/sources.list \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends fontconfig fonts-dejavu \
|
||||
&& apt-get install -y --no-install-recommends fontconfig fonts-dejavu sqlite3 \
|
||||
&& apt-get clean \
|
||||
&& 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"]
|
||||
|
||||
@@ -69,10 +69,9 @@
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<version>8.2.0</version>
|
||||
<scope>runtime</scope>
|
||||
<groupId>org.xerial</groupId>
|
||||
<artifactId>sqlite-jdbc</artifactId>
|
||||
<version>3.45.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 读取控制台内容
|
||||
* </p>
|
||||
*/
|
||||
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<FileOutConfig> 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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<Integer> nodeIds;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -21,14 +21,6 @@ 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;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ForwardPortDto {
|
||||
private Integer node_id;
|
||||
private Integer port;
|
||||
}
|
||||
@@ -19,22 +19,12 @@ 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<ChainTunnel> inNodeId = new ArrayList<>();
|
||||
|
||||
// 转发链节点列表(二维数组结构)
|
||||
private List<List<ChainTunnel>> chainNodes = new ArrayList<>();
|
||||
|
||||
// 出口节点列表
|
||||
private List<ChainTunnel> outNodeId = new ArrayList<>();
|
||||
}
|
||||
|
||||
@@ -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<ChainTunnel> inNodeId;
|
||||
|
||||
private List<List<ChainTunnel>> chainNodes;
|
||||
|
||||
private List<ChainTunnel> 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";
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<ConfigItem> 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<String> 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<ConfigItem> 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<String> 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<ConfigItem> 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<Tunnel> tunnelList = tunnelService.list(new QueryWrapper<Tunnel>().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<SpeedLimit> speedLimits = speedLimitService.list(new QueryWrapper<SpeedLimit>().in("tunnel_id", ids));
|
||||
if (speedLimits != null && !speedLimits.isEmpty()) {
|
||||
List<ConfigItem> limiters = gostConfig.getLimiters();
|
||||
List<Long> limiters_ids = new ArrayList<>();
|
||||
List<Long> 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<Long> 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<String> parseServiceName(String serviceName) {
|
||||
String[] split = serviceName.split("_");
|
||||
return new ArrayList<>(Arrays.asList(split));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
package com.admin.common.task;
|
||||
|
||||
import com.admin.common.dto.GostDto;
|
||||
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 +33,9 @@ public class ResetFlowAsync {
|
||||
@Resource
|
||||
TunnelService tunnelService;
|
||||
|
||||
@Resource
|
||||
ChainTunnelService chainTunnelService;
|
||||
|
||||
/**
|
||||
* 每天0点执行流量重置任务
|
||||
* 查询出用户和隧道的重置流量日期是今天的数据,将上下流量重置为0
|
||||
@@ -197,12 +195,18 @@ public class ResetFlowAsync {
|
||||
// 查询对应转发
|
||||
List<Forward> forwardList = forwardService.list(new QueryWrapper<Forward>().eq("user_id", user.getId()).eq("status", 1));
|
||||
for (Forward forward : forwardList) {
|
||||
UserTunnel userTunnel = userTunnelService.getOne(new QueryWrapper<UserTunnel>().eq("user_id", forward.getUserId()).eq("tunnel_id", forward.getTunnelId()));
|
||||
if (userTunnel != null) {
|
||||
pauseForwardService(forward, userTunnel.getId());
|
||||
forward.setStatus(0);
|
||||
forwardService.updateById(forward);
|
||||
|
||||
List<ChainTunnel> chainTunnels = chainTunnelService.list(new QueryWrapper<ChainTunnel>().eq("tunnel_id", forward.getTunnelId()).eq("chain_type", 1));
|
||||
for (ChainTunnel chainTunnel : chainTunnels) {
|
||||
UserTunnel userTunnel = userTunnelService.getOne(new QueryWrapper<UserTunnel>().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 +221,23 @@ public class ResetFlowAsync {
|
||||
for (UserTunnel userTunnel : user_tunnel_list) {
|
||||
List<Forward> forwardList = forwardService.list(new QueryWrapper<Forward>().eq("tunnel_id", userTunnel.getTunnelId()).eq("user_id", userTunnel.getUserId()).eq("status", 1));
|
||||
for (Forward forward : forwardList) {
|
||||
pauseForwardService(forward, userTunnel.getId());
|
||||
|
||||
List<ChainTunnel> chainTunnels = chainTunnelService.list(new QueryWrapper<ChainTunnel>().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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
package com.admin.common.utils;
|
||||
|
||||
import com.admin.common.dto.GostConfigDto;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
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 {
|
||||
@@ -15,7 +16,11 @@ public class GostUtil {
|
||||
|
||||
public static GostDto AddLimiters(Long node_id, Long name, String speed) {
|
||||
JSONObject data = createLimiterData(name, speed);
|
||||
return WebSocketServer.send_msg(node_id, data, "AddLimiters");
|
||||
GostDto gostDto = WebSocketServer.send_msg(node_id, data, "AddLimiters");
|
||||
if (gostDto.getMsg().contains("exists")){
|
||||
gostDto.setMsg("OK");
|
||||
}
|
||||
return gostDto;
|
||||
}
|
||||
|
||||
public static GostDto UpdateLimiters(Long node_id, Long name, String speed) {
|
||||
@@ -29,266 +34,177 @@ public class GostUtil {
|
||||
public static GostDto DeleteLimiters(Long node_id, Long name) {
|
||||
JSONObject req = new JSONObject();
|
||||
req.put("limiter", name + "");
|
||||
return 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);
|
||||
GostDto gostDto = WebSocketServer.send_msg(node_id, req, "DeleteLimiters");
|
||||
if (gostDto.getMsg().contains("not found")){
|
||||
gostDto.setMsg("OK");
|
||||
}
|
||||
return WebSocketServer.send_msg(node_id, services, "AddService");
|
||||
return gostDto;
|
||||
}
|
||||
|
||||
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 GostDto AddChains(Long node_id, List<ChainTunnel> chainTunnels, Map<Long, Node> 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", processServerAddress(node_info.getServerIp() + ":" + chainTunnel.getPort()));
|
||||
node.put("connector", connector);
|
||||
node.put("dialer", dialer);
|
||||
|
||||
|
||||
|
||||
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());
|
||||
|
||||
// interface设置在转发链
|
||||
if (StringUtils.isNotBlank(node_s.get(node_id).getInterfaceName())) {
|
||||
hop.put("interface", node_s.get(node_id).getInterfaceName());
|
||||
}
|
||||
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
public static GostDto UpdateChains(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);
|
||||
GostDto gostDto = WebSocketServer.send_msg(node_id, data, "AddChains");
|
||||
if (gostDto.getMsg().contains("exists")){
|
||||
gostDto.setMsg("OK");
|
||||
}
|
||||
|
||||
|
||||
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("nodes", nodes);
|
||||
|
||||
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");
|
||||
return gostDto;
|
||||
}
|
||||
|
||||
public static GostDto DeleteChains(Long node_id, String name) {
|
||||
JSONObject data = new JSONObject();
|
||||
data.put("chain", name + "_chains");
|
||||
return WebSocketServer.send_msg(node_id, data, "DeleteChains");
|
||||
data.put("chain", name);
|
||||
GostDto gostDto = WebSocketServer.send_msg(node_id, data, "DeleteChains");
|
||||
if (gostDto.getMsg().contains("not found")){
|
||||
gostDto.setMsg("OK");
|
||||
}
|
||||
return gostDto;
|
||||
}
|
||||
|
||||
public static GostDto AddChainService(Long node_id, ChainTunnel chainTunnel, Map<Long, Node> 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());
|
||||
|
||||
// 只为出口节点(chainType=3)设置 interface
|
||||
if (chainTunnel.getChainType() == 3 && StringUtils.isNotBlank(node_s.get(node_id).getInterfaceName())) {
|
||||
JSONObject metadata = new JSONObject();
|
||||
metadata.put("interface", node_s.get(node_id).getInterfaceName());
|
||||
service_item.put("metadata", metadata);
|
||||
}
|
||||
|
||||
JSONObject handler = new JSONObject();
|
||||
handler.put("type", "relay");
|
||||
if (chainTunnel.getChainType() == 2){
|
||||
handler.put("chain","chains_" + chainTunnel.getTunnelId());
|
||||
}
|
||||
service_item.put("handler", handler);
|
||||
|
||||
JSONObject listener = new JSONObject();
|
||||
listener.put("type", chainTunnel.getProtocol());
|
||||
service_item.put("listener", listener);
|
||||
|
||||
services.add(service_item);
|
||||
|
||||
GostDto gostDto = WebSocketServer.send_msg(node_id, services, "AddService");
|
||||
if (gostDto.getMsg().contains("exists")){
|
||||
gostDto.setMsg("OK");
|
||||
}
|
||||
return gostDto;
|
||||
}
|
||||
|
||||
public static GostDto 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());
|
||||
}
|
||||
|
||||
// 只在端口转发时设置 interface(隧道转发时 interface 在转发链的节点上设置)
|
||||
if (tunnel.getType() == 1 && 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);
|
||||
}
|
||||
GostDto gostDto = WebSocketServer.send_msg(node.getId(), services, meth);
|
||||
if (gostDto.getMsg().contains("exists")){
|
||||
gostDto.setMsg("OK");
|
||||
}
|
||||
return gostDto;
|
||||
}
|
||||
|
||||
public static GostDto DeleteService(Long node_id, JSONArray services) {
|
||||
JSONObject data = new JSONObject();
|
||||
data.put("services", services);
|
||||
GostDto gostDto = WebSocketServer.send_msg(node_id, data, "DeleteService");
|
||||
if (gostDto.getMsg().contains("not found")){
|
||||
gostDto.setMsg("OK");
|
||||
}
|
||||
return gostDto;
|
||||
}
|
||||
|
||||
public static GostDto 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);
|
||||
return 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 +214,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 +233,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.isEmpty()) {
|
||||
strategy = "fifo";
|
||||
}
|
||||
|
||||
@@ -386,12 +253,42 @@ public class GostUtil {
|
||||
return forwarder;
|
||||
}
|
||||
|
||||
private static boolean isPortForwarding(Integer fow_type) {
|
||||
return fow_type != null && fow_type == 1;
|
||||
public static String processServerAddress(String serverAddr) {
|
||||
if (StrUtil.isBlank(serverAddr)) {
|
||||
return serverAddr;
|
||||
}
|
||||
|
||||
// 如果已经被方括号包裹,直接返回
|
||||
if (serverAddr.startsWith("[")) {
|
||||
return serverAddr;
|
||||
}
|
||||
|
||||
// 查找最后一个冒号,分离主机和端口
|
||||
int lastColonIndex = serverAddr.lastIndexOf(':');
|
||||
if (lastColonIndex == -1) {
|
||||
// 没有端口号,直接检查是否需要包裹
|
||||
return isIPv6Address(serverAddr) ? "[" + serverAddr + "]" : serverAddr;
|
||||
}
|
||||
|
||||
String host = serverAddr.substring(0, lastColonIndex);
|
||||
String port = serverAddr.substring(lastColonIndex);
|
||||
|
||||
// 检查主机部分是否为IPv6地址
|
||||
if (isIPv6Address(host)) {
|
||||
return "[" + host + "]" + port;
|
||||
}
|
||||
|
||||
return serverAddr;
|
||||
}
|
||||
|
||||
private static boolean isTunnelForwarding(Integer fow_type) {
|
||||
return fow_type != null && fow_type != 1;
|
||||
}
|
||||
private static boolean isIPv6Address(String address) {
|
||||
// IPv6地址包含多个冒号,至少2个
|
||||
if (!address.contains(":")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 计算冒号数量,IPv6地址至少有2个冒号
|
||||
long colonCount = address.chars().filter(ch -> ch == ':').count();
|
||||
return colonCount >= 2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,6 +229,7 @@ public class WebSocketServer extends TextWebSocketHandler {
|
||||
String socks = (String) session.getAttributes().get("socks");
|
||||
|
||||
log.info("节点 {} 尝试连接,开始处理连接逻辑", nodeId);
|
||||
log.info("屏蔽协议数据:http->{}, tls->{}, socks->{}", http, tls, socks);
|
||||
|
||||
// 检查是否已有该节点的连接,如果有则记录日志但直接覆盖
|
||||
WebSocketSession existingSession = nodeSessions.get(nodeId);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.admin.config;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.PreDestroy;
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
import java.sql.Statement;
|
||||
|
||||
/**
|
||||
* SQLite 数据库配置
|
||||
* 启用 WAL (Write-Ahead Logging) 模式以提高并发性能
|
||||
* 添加定期 checkpoint 和优雅关闭处理
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@EnableScheduling
|
||||
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秒超时
|
||||
statement.execute("PRAGMA wal_autocheckpoint=1000;"); // 每1000页自动checkpoint
|
||||
|
||||
log.info("SQLite WAL mode configured successfully");
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to configure SQLite database", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 定期执行 checkpoint,确保 WAL 文件内容写入主数据库
|
||||
* 每5分钟执行一次
|
||||
*/
|
||||
@Scheduled(fixedDelay = 300000, initialDelay = 300000)
|
||||
public void performCheckpoint() {
|
||||
try (Connection connection = dataSource.getConnection();
|
||||
Statement statement = connection.createStatement()) {
|
||||
|
||||
statement.execute("PRAGMA wal_checkpoint(TRUNCATE);");
|
||||
log.debug("SQLite WAL checkpoint completed");
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to perform SQLite checkpoint", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用关闭前执行最终的 checkpoint,确保所有数据都写入主数据库文件
|
||||
*/
|
||||
@PreDestroy
|
||||
public void onShutdown() {
|
||||
log.info("Performing final SQLite checkpoint before shutdown...");
|
||||
try (Connection connection = dataSource.getConnection();
|
||||
Statement statement = connection.createStatement()) {
|
||||
|
||||
// 强制执行 checkpoint,将所有 WAL 内容写入主数据库
|
||||
statement.execute("PRAGMA wal_checkpoint(TRUNCATE);");
|
||||
log.info("Final SQLite checkpoint completed successfully");
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to perform final SQLite checkpoint", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Forward> 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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<ChainTunnel> {
|
||||
}
|
||||
@@ -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<Forward> {
|
||||
*/
|
||||
List<ForwardWithTunnelDto> selectForwardsWithTunnelByUserId(@Param("userId") Integer userId);
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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<ForwardPort> {
|
||||
}
|
||||
@@ -30,10 +30,5 @@ public interface UserMapper extends BaseMapper<User> {
|
||||
* @return 转发列表
|
||||
*/
|
||||
List<UserPackageDto.UserForwardDetailDto> getUserForwardDetails(@Param("userId") Integer userId);
|
||||
|
||||
/**
|
||||
* 管理员查询所有隧道(流量和转发设置为99999)
|
||||
* @return 隧道列表
|
||||
*/
|
||||
List<UserPackageDto.UserTunnelDetailDto> getAllTunnelsForAdmin();
|
||||
|
||||
}
|
||||
|
||||
@@ -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<ChainTunnel> {
|
||||
}
|
||||
@@ -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<ForwardPort> {
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
@@ -79,7 +80,4 @@ public interface ForwardService extends IService<Forward> {
|
||||
* @return 更新结果
|
||||
*/
|
||||
R updateForwardOrder(Map<String, Object> params);
|
||||
|
||||
|
||||
void updateForwardA(Forward forward);
|
||||
}
|
||||
|
||||
@@ -24,8 +24,6 @@ public interface NodeService extends IService<Node> {
|
||||
|
||||
R deleteNode(Long id);
|
||||
|
||||
Node getNodeById(Long id);
|
||||
|
||||
R getInstallCommand(Long id);
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 端口转发服务实现类
|
||||
* </p>
|
||||
*
|
||||
* @author QAQ
|
||||
* @since 2025-06-03
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class ChainTunnelServiceImpl extends ServiceImpl<ChainTunnelMapper, ChainTunnel> implements ChainTunnelService {
|
||||
|
||||
}
|
||||
@@ -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<ForwardPortMapper, ForwardPort> implements ForwardPortService {
|
||||
|
||||
}
|
||||
+597
-1015
File diff suppressed because it is too large
Load Diff
@@ -6,15 +6,12 @@ import com.admin.common.dto.GostDto;
|
||||
import com.admin.common.dto.NodeDto;
|
||||
import com.admin.common.dto.NodeUpdateDto;
|
||||
import com.admin.common.lang.R;
|
||||
import com.admin.common.utils.GostUtil;
|
||||
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 +23,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;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 节点服务实现类
|
||||
* 提供节点的增删改查功能,包括节点创建、更新、删除和查询操作
|
||||
* </p>
|
||||
*
|
||||
* @author QAQ
|
||||
* @since 2025-06-03
|
||||
*/
|
||||
@Service
|
||||
public class NodeServiceImpl extends ServiceImpl<NodeMapper, Node> 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 +38,41 @@ public class NodeServiceImpl extends ServiceImpl<NodeMapper, Node> 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<Node> nodeList = this.list();
|
||||
hideNodeSecrets(nodeList);
|
||||
List<Node> nodeList = this.list(new QueryWrapper<Node>().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,314 +95,94 @@ public class NodeServiceImpl extends ServiceImpl<NodeMapper, Node> implements No
|
||||
}
|
||||
|
||||
|
||||
// 2. 构建更新对象并执行更新
|
||||
|
||||
Node updateNode = buildUpdateNode(nodeUpdateDto);
|
||||
boolean result = this.updateById(updateNode);
|
||||
|
||||
// 更新隧道入口ip
|
||||
List<Tunnel> inNodeId = tunnelService.list(new QueryWrapper<Tunnel>().eq("in_node_id", updateNode.getId()));
|
||||
if (!inNodeId.isEmpty()) {
|
||||
for (Tunnel tunnel : inNodeId) {
|
||||
tunnel.setInIp(updateNode.getIp());
|
||||
}
|
||||
tunnelService.updateBatchById(inNodeId);
|
||||
}
|
||||
|
||||
// 更新服务器出口ip
|
||||
List<Tunnel> outNodeId = tunnelService.list(new QueryWrapper<Tunnel>().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<ChainTunnel> list = chainTunnelService.list(new QueryWrapper<ChainTunnel>().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<ViteConfig>().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.6-beta/install.sh")
|
||||
.append(" -o ./install.sh && chmod +x ./install.sh && ");
|
||||
String processedServerAddr = GostUtil.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<Node> nodeList) {
|
||||
nodeList.forEach(node -> node.setSecret(null));
|
||||
|
||||
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("可用端口不合法");
|
||||
}
|
||||
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("可用端口不合法");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 检查节点使用情况
|
||||
* 验证是否有隧道正在使用该节点作为入口或出口节点
|
||||
*
|
||||
* @param nodeId 节点ID
|
||||
* @return 检查结果响应
|
||||
*/
|
||||
private R checkNodeUsage(Long nodeId) {
|
||||
// 检查入口节点使用情况
|
||||
R inNodeCheckResult = checkInNodeUsage(nodeId);
|
||||
if (inNodeCheckResult.getCode() != 0) {
|
||||
return inNodeCheckResult;
|
||||
}
|
||||
|
||||
// 检查出口节点使用情况
|
||||
return checkOutNodeUsage(nodeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查节点作为入口节点的使用情况
|
||||
*
|
||||
* @param nodeId 节点ID
|
||||
* @return 检查结果响应
|
||||
*/
|
||||
private R checkInNodeUsage(Long nodeId) {
|
||||
QueryWrapper<Tunnel> 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);
|
||||
}
|
||||
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查节点作为出口节点的使用情况
|
||||
*
|
||||
* @param nodeId 节点ID
|
||||
* @return 检查结果响应
|
||||
*/
|
||||
private R checkOutNodeUsage(Long nodeId) {
|
||||
QueryWrapper<Tunnel> 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<ViteConfig>().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;
|
||||
}
|
||||
|
||||
// 如果已经被方括号包裹,直接返回
|
||||
if (serverAddr.startsWith("[")) {
|
||||
return serverAddr;
|
||||
}
|
||||
|
||||
// 查找最后一个冒号,分离主机和端口
|
||||
int lastColonIndex = serverAddr.lastIndexOf(':');
|
||||
if (lastColonIndex == -1) {
|
||||
// 没有端口号,直接检查是否需要包裹
|
||||
return isIPv6Address(serverAddr) ? "[" + serverAddr + "]" : serverAddr;
|
||||
}
|
||||
|
||||
String host = serverAddr.substring(0, lastColonIndex);
|
||||
String port = serverAddr.substring(lastColonIndex);
|
||||
|
||||
// 检查主机部分是否为IPv6地址
|
||||
if (isIPv6Address(host)) {
|
||||
return "[" + host + "]" + port;
|
||||
}
|
||||
|
||||
return serverAddr;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为IPv6地址
|
||||
*
|
||||
* @param address 地址字符串(不包含端口号)
|
||||
* @return 是否为IPv6地址
|
||||
*/
|
||||
private boolean isIPv6Address(String address) {
|
||||
// IPv6地址包含多个冒号,至少2个
|
||||
if (!address.contains(":")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 计算冒号数量,IPv6地址至少有2个冒号
|
||||
long colonCount = address.chars().filter(ch -> ch == ':').count();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+73
-320
@@ -5,15 +5,10 @@ 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.alibaba.fastjson.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import lombok.Data;
|
||||
@@ -22,8 +17,10 @@ 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.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
@@ -41,354 +38,110 @@ import java.util.UUID;
|
||||
@Service
|
||||
public class SpeedLimitServiceImpl extends ServiceImpl<SpeedLimitMapper, SpeedLimit> 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<Long> limit_success = new ArrayList<>();
|
||||
|
||||
List<ChainTunnel> tunnelList = chainTunnelService.list(new QueryWrapper<ChainTunnel>().eq("tunnel_id", speedLimit.getTunnelId()));
|
||||
for (ChainTunnel chainTunnel : tunnelList) {
|
||||
Node node = nodeService.getById(chainTunnel.getNodeId());
|
||||
if (node != null) {
|
||||
GostDto gostDto = GostUtil.AddLimiters(node.getId(), speedLimit.getId(), speedInMBps);
|
||||
if (Objects.equals(gostDto.getMsg(), "OK")){
|
||||
limit_success.add(node.getId());
|
||||
}else {
|
||||
this.removeById(speedLimit.getId());
|
||||
for (Long node_id : limit_success) {
|
||||
GostDto deleteLimiters = GostUtil.DeleteLimiters(node_id, speedLimit.getId());
|
||||
System.out.println(deleteLimiters);
|
||||
}
|
||||
return R.err(gostDto.getMsg());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有限速规则
|
||||
*
|
||||
* @return 包含所有限速规则的响应对象
|
||||
*/
|
||||
@Override
|
||||
public R getAllSpeedLimits() {
|
||||
List<SpeedLimit> 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<ChainTunnel> tunnelList = chainTunnelService.list(new QueryWrapper<ChainTunnel>().eq("tunnel_id", speedLimit.getTunnelId()));
|
||||
for (ChainTunnel chainTunnel : tunnelList) {
|
||||
Node node = nodeService.getById(chainTunnel.getNodeId());
|
||||
if (node != null) {
|
||||
GostDto gostDto = GostUtil.UpdateLimiters(node.getId(), speedLimit.getId(), speedInMBps);
|
||||
if (!Objects.equals(gostDto.getMsg(), "OK")) return R.err(gostDto.getMsg());
|
||||
}
|
||||
}
|
||||
|
||||
// 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<UserTunnel>().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<UserTunnel>().eq("speed_id", speedLimit.getId()));
|
||||
if (userCount != 0) return R.err("该限速规则还有用户在使用 请先取消分配");
|
||||
|
||||
|
||||
List<ChainTunnel> tunnelList = chainTunnelService.list(new QueryWrapper<ChainTunnel>().eq("tunnel_id", speedLimit.getTunnelId()));
|
||||
for (ChainTunnel chainTunnel : tunnelList) {
|
||||
Node node = nodeService.getById(chainTunnel.getNodeId());
|
||||
if (node != null) {
|
||||
GostDto gostDto = GostUtil.DeleteLimiters(node.getId(), speedLimit.getId());
|
||||
if (!Objects.equals(gostDto.getMsg(), "OK"))return R.err(gostDto.getMsg());
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 用户服务实现类
|
||||
* 提供用户的增删改查功能,包括用户登录、创建、更新、删除和套餐信息查询
|
||||
* 支持用户关联数据的级联删除,包括转发和Gost服务的清理
|
||||
* </p>
|
||||
*
|
||||
* @author QAQ
|
||||
* @since 2025-06-03
|
||||
*/
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class UserServiceImpl extends ServiceImpl<UserMapper, User> 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<UserMapper, User> 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<ViteConfig>().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<UserMapper, User> 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<User>().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<User>().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<User>().ne("role_id", ADMIN_ROLE_ID)));
|
||||
List<User> list = this.list(new QueryWrapper<User>().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<User>().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<StatisticsFlow>().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<Forward> forwardList = forwardService.list(new QueryWrapper<Forward>().eq("user_id", id));
|
||||
for (Forward forward : forwardList) {
|
||||
forwardService.deleteForward(forward.getId());
|
||||
}
|
||||
forwardService.remove(new QueryWrapper<Forward>().eq("user_id", id));
|
||||
userTunnelService.remove(new QueryWrapper<UserTunnel>().eq("user_id", id));
|
||||
statisticsFlowService.remove(new QueryWrapper<StatisticsFlow>().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<UserPackageDto.UserTunnelDetailDto> tunnelPermissions = userMapper.getUserTunnelDetails(userId);
|
||||
List<UserPackageDto.UserForwardDetailDto> forwards = userMapper.getUserForwardDetails(user.getId().intValue());
|
||||
fillForwardInIpAndPort(forwards);
|
||||
List<StatisticsFlow> 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<User>().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<UserMapper, User> implements Us
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
// ========== 私有辅助方法 ==========
|
||||
|
||||
|
||||
/**
|
||||
* 验证用户登录凭据
|
||||
*
|
||||
* @param loginDto 登录数据传输对象
|
||||
* @return 登录验证结果
|
||||
*/
|
||||
private LoginValidationResult validateUserCredentials(LoginDto loginDto) {
|
||||
User user = this.getOne(new QueryWrapper<User>().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<User> queryWrapper = new QueryWrapper<User>().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<Forward> forwardQuery = new QueryWrapper<>();
|
||||
forwardQuery.eq("user_id", userId);
|
||||
List<Forward> 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<UserTunnel>()
|
||||
.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<UserTunnel> 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<UserPackageDto.UserTunnelDetailDto> tunnelPermissions = getTunnelPermissions(user.getId());
|
||||
|
||||
// 3. 获取转发详情
|
||||
List<UserPackageDto.UserForwardDetailDto> forwards = userMapper.getUserForwardDetails(user.getId().intValue());
|
||||
|
||||
// 4. 查询最近24小时流量信息,没有的补0
|
||||
List<StatisticsFlow> 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,102 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
||||
return userInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取隧道权限详情
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 隧道权限详情列表
|
||||
*/
|
||||
private List<UserPackageDto.UserTunnelDetailDto> getTunnelPermissions(Long userId) {
|
||||
return userMapper.getUserTunnelDetails(userId.intValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户最近24小时的流量统计数据,没有数据的时间点补0
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 最近24小时流量统计列表
|
||||
*/
|
||||
private List<StatisticsFlow> getLast24HoursFlowStatistics(Long userId) {
|
||||
// 按ID倒序查最近24条记录(ID越大越新,时间就是23:00, 22:00, 21:00...这样倒序)
|
||||
List<StatisticsFlow> recentFlows = statisticsFlowService.list(
|
||||
new QueryWrapper<StatisticsFlow>()
|
||||
.eq("user_id", userId)
|
||||
.orderByDesc("id")
|
||||
.last("LIMIT 24")
|
||||
);
|
||||
|
||||
List<StatisticsFlow> 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<UserPackageDto.UserForwardDetailDto> forwards) {
|
||||
for (UserPackageDto.UserForwardDetailDto forward : forwards) {
|
||||
Tunnel tunnel = tunnelService.getById(forward.getTunnelId());
|
||||
if (tunnel == null) continue;
|
||||
|
||||
List<ForwardPort> forwardPorts = forwardPortService.list(
|
||||
new QueryWrapper<ForwardPort>().eq("forward_id", forward.getId())
|
||||
);
|
||||
if (forwardPorts.isEmpty()) continue;
|
||||
|
||||
boolean useTunnelInIp = tunnel.getInIp() != null && !tunnel.getInIp().trim().isEmpty();
|
||||
java.util.Set<String> ipPortSet = new java.util.LinkedHashSet<>();
|
||||
|
||||
if (useTunnelInIp) {
|
||||
// 使用隧道的inIp(求笛卡尔积)
|
||||
List<String> ipList = new ArrayList<>();
|
||||
List<Integer> portList = new ArrayList<>();
|
||||
|
||||
String[] tunnelInIps = tunnel.getInIp().split(",");
|
||||
for (String ip : tunnelInIps) {
|
||||
if (ip != null && !ip.trim().isEmpty()) {
|
||||
ipList.add(ip.trim());
|
||||
}
|
||||
}
|
||||
|
||||
for (ForwardPort forwardPort : forwardPorts) {
|
||||
if (forwardPort.getPort() != null) {
|
||||
portList.add(forwardPort.getPort());
|
||||
}
|
||||
}
|
||||
|
||||
List<String> uniqueIps = ipList.stream().distinct().toList();
|
||||
List<Integer> uniquePorts = portList.stream().distinct().toList();
|
||||
|
||||
for (String ip : uniqueIps) {
|
||||
for (Integer port : uniquePorts) {
|
||||
ipPortSet.add(ip + ":" + port);
|
||||
}
|
||||
}
|
||||
|
||||
if (!uniquePorts.isEmpty()) {
|
||||
forward.setInPort(uniquePorts.getFirst());
|
||||
}
|
||||
} else {
|
||||
// 使用节点的serverIp(一对一,不求笛卡尔积)
|
||||
for (ForwardPort forwardPort : forwardPorts) {
|
||||
Node node = nodeService.getById(forwardPort.getNodeId());
|
||||
if (node != null && node.getServerIp() != null && forwardPort.getPort() != null) {
|
||||
ipPortSet.add(node.getServerIp() + ":" + forwardPort.getPort());
|
||||
}
|
||||
}
|
||||
|
||||
if (!forwardPorts.isEmpty() && forwardPorts.getFirst().getPort() != null) {
|
||||
forward.setInPort(forwardPorts.getFirst().getPort());
|
||||
}
|
||||
}
|
||||
|
||||
if (!ipPortSet.isEmpty()) {
|
||||
forward.setInIp(String.join(",", ipPortSet));
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+49
-376
@@ -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<UserTunnelMapper, UserTunnel> 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<UserTunnelWithDetailDto> 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<UserTunnel> 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<UserTunnel>().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<UserTunnelWithDetailDto> getUserTunnelDetailsFromDatabase(Integer userId) {
|
||||
return this.baseMapper.getUserTunnelWithDetails(userId);
|
||||
@Override
|
||||
public R getUserTunnelList(UserTunnelQueryDto queryDto) {
|
||||
List<UserTunnelWithDetailDto> 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<Forward> forwardList = forwardService.list(new QueryWrapper<Forward>().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<Forward> forwardList = forwardService.list(new QueryWrapper<Forward>().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 <T> 属性类型
|
||||
*/
|
||||
private <T> void updateOptionalProperty(java.util.function.Consumer<T> setter, T value) {
|
||||
if (value != null) {
|
||||
setter.accept(value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 删除用户在指定隧道下的所有转发
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param tunnelId 隧道ID
|
||||
*/
|
||||
private void removeUserTunnelForwards(Integer userId, Integer tunnelId) {
|
||||
// 查询该用户在该隧道下的所有转发
|
||||
QueryWrapper<Forward> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("user_id", userId).eq("tunnel_id", tunnelId);
|
||||
|
||||
List<Forward> 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<UserTunnel> 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<UserTunnelMapper, UserTun
|
||||
}
|
||||
return !oldSpeedId.equals(newSpeedId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户隧道下所有转发的限速规则
|
||||
* 管理员操作,不需要权限检查,直接查出该用户在该隧道下的所有转发并应用新的限速
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param tunnelId 隧道ID
|
||||
* @param speedId 新的限速规则ID
|
||||
*/
|
||||
private void updateUserTunnelForwardsSpeed(Integer userId, Integer tunnelId, Integer speedId) {
|
||||
// 1. 查询该用户在该隧道下的所有转发
|
||||
QueryWrapper<Forward> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("user_id", userId).eq("tunnel_id", tunnelId);
|
||||
List<Forward> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+19
-77
@@ -24,24 +24,7 @@ import java.util.Map;
|
||||
@Service
|
||||
public class ViteConfigServiceImpl extends ServiceImpl<ViteConfigMapper, ViteConfig> 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<ViteConfig> configList = this.list();
|
||||
@@ -54,89 +37,48 @@ public class ViteConfigServiceImpl extends ServiceImpl<ViteConfigMapper, ViteCon
|
||||
return R.ok(configMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据配置名称获取配置值
|
||||
*
|
||||
* @param name 配置名称
|
||||
* @return 配置响应对象
|
||||
*/
|
||||
|
||||
@Override
|
||||
public R getConfigByName(String name) {
|
||||
if (!StringUtils.hasText(name)) {
|
||||
return R.err(ERROR_CONFIG_NAME_REQUIRED);
|
||||
}
|
||||
if (!StringUtils.hasText(name)) return R.err("配置名称不能为空");
|
||||
|
||||
QueryWrapper<ViteConfig> 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<String, String> configMap) {
|
||||
if (configMap == null || configMap.isEmpty()) {
|
||||
return R.err("配置数据不能为空");
|
||||
}
|
||||
if (configMap == null || configMap.isEmpty()) return R.err("配置数据不能为空");
|
||||
|
||||
try {
|
||||
for (Map.Entry<String, String> entry : configMap.entrySet()) {
|
||||
String name = entry.getKey();
|
||||
String value = entry.getValue();
|
||||
|
||||
if (!StringUtils.hasText(name)) {
|
||||
continue; // 跳过无效的配置名
|
||||
}
|
||||
|
||||
updateOrCreateConfig(name, value);
|
||||
for (Map.Entry<String, String> 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<ViteConfig> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("name", name);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
@@ -20,7 +20,7 @@
|
||||
<Prudent>true</Prudent>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<FileNamePattern>${FILE_PATH}</FileNamePattern>
|
||||
<maxHistory>30</maxHistory>
|
||||
<maxHistory>7</maxHistory>
|
||||
</rollingPolicy>
|
||||
<layout class="ch.qos.logback.classic.PatternLayout">
|
||||
<Pattern>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ export type SiteConfig = typeof siteConfig;
|
||||
|
||||
// 缓存相关常量
|
||||
const CACHE_PREFIX = 'vite_config_';
|
||||
const VERSION = "1.4.2";
|
||||
const VERSION = "2.0.6-beta";
|
||||
const APP_VERSION = "1.0.3";
|
||||
|
||||
const getInitialConfig = () => {
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
+367
-130
@@ -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<Tunnel | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
@@ -403,6 +406,14 @@ export default function ForwardPage() {
|
||||
newErrors.tunnelId = '请选择关联隧道';
|
||||
}
|
||||
|
||||
// 验证入口端口(可选,如果填写则验证)
|
||||
if (form.inPort !== null && form.inPort !== undefined) {
|
||||
const port = Number(form.inPort);
|
||||
if (isNaN(port) || port < 1 || port > 65535) {
|
||||
newErrors.inPort = '端口必须在 1-65535 之间';
|
||||
}
|
||||
}
|
||||
|
||||
if (!form.remoteAddr.trim()) {
|
||||
newErrors.remoteAddr = '请输入远程地址';
|
||||
} else {
|
||||
@@ -421,16 +432,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 +447,6 @@ export default function ForwardPage() {
|
||||
interfaceName: '',
|
||||
strategy: 'fifo'
|
||||
});
|
||||
setSelectedTunnel(null);
|
||||
setErrors({});
|
||||
setModalOpen(true);
|
||||
};
|
||||
@@ -464,8 +464,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 +509,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) }));
|
||||
};
|
||||
|
||||
@@ -540,7 +536,6 @@ export default function ForwardPage() {
|
||||
tunnelId: form.tunnelId,
|
||||
inPort: form.inPort,
|
||||
remoteAddr: processedRemoteAddr,
|
||||
interfaceName: form.interfaceName,
|
||||
strategy: addressCount > 1 ? form.strategy : 'fifo'
|
||||
};
|
||||
res = await updateForward(updateData);
|
||||
@@ -551,7 +546,6 @@ export default function ForwardPage() {
|
||||
tunnelId: form.tunnelId,
|
||||
inPort: form.inPort,
|
||||
remoteAddr: processedRemoteAddr,
|
||||
interfaceName: form.interfaceName,
|
||||
strategy: addressCount > 1 ? form.strategy : 'fifo'
|
||||
};
|
||||
res = await createForward(createData);
|
||||
@@ -694,13 +688,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 +717,7 @@ export default function ForwardPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const firstIp = ips[0];
|
||||
const firstIp = items[0];
|
||||
let formattedFirstIp;
|
||||
if (firstIp.includes(':') && !firstIp.startsWith('[')) {
|
||||
formattedFirstIp = `[${firstIp}]`;
|
||||
@@ -716,7 +725,7 @@ export default function ForwardPage() {
|
||||
formattedFirstIp = firstIp;
|
||||
}
|
||||
|
||||
return `${formattedFirstIp}:${port} (+${ips.length - 1})`;
|
||||
return `${formattedFirstIp}:${port} (+${items.length - 1}个)`;
|
||||
};
|
||||
|
||||
// 格式化远程地址
|
||||
@@ -744,18 +753,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 +1606,8 @@ export default function ForwardPage() {
|
||||
isInvalid={!!errors.tunnelId}
|
||||
errorMessage={errors.tunnelId}
|
||||
variant="bordered"
|
||||
isDisabled={isEdit}
|
||||
description={isEdit ? "编辑时无法修改关联隧道" : undefined}
|
||||
>
|
||||
{tunnels.map((tunnel) => (
|
||||
<SelectItem key={tunnel.id} >
|
||||
@@ -1595,24 +1615,23 @@ export default function ForwardPage() {
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
|
||||
<Input
|
||||
label="入口端口"
|
||||
placeholder="留空自动分配"
|
||||
placeholder="留空则自动分配可用端口"
|
||||
type="number"
|
||||
value={form.inPort?.toString() || ''}
|
||||
onChange={(e) => setForm(prev => ({
|
||||
...prev,
|
||||
inPort: e.target.value ? parseInt(e.target.value) : null
|
||||
}))}
|
||||
value={form.inPort !== null ? form.inPort.toString() : ''}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setForm(prev => ({
|
||||
...prev,
|
||||
inPort: value ? parseInt(value) : null
|
||||
}));
|
||||
}}
|
||||
isInvalid={!!errors.inPort}
|
||||
errorMessage={errors.inPort}
|
||||
variant="bordered"
|
||||
description={
|
||||
selectedTunnel && selectedTunnel.inNodePortSta && selectedTunnel.inNodePortEnd
|
||||
? `允许范围: ${selectedTunnel.inNodePortSta}-${selectedTunnel.inNodePortEnd}`
|
||||
: '留空将自动分配可用端口'
|
||||
}
|
||||
description="指定入口端口,留空则从节点可用端口中自动分配"
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
@@ -1628,17 +1647,6 @@ export default function ForwardPage() {
|
||||
maxRows={6}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="出口网卡名或IP"
|
||||
placeholder="请输入出口网卡名或IP"
|
||||
value={form.interfaceName}
|
||||
onChange={(e) => setForm(prev => ({ ...prev, interfaceName: e.target.value }))}
|
||||
isInvalid={!!errors.interfaceName}
|
||||
errorMessage={errors.interfaceName}
|
||||
variant="bordered"
|
||||
description="用于多IP服务器指定使用那个IP请求远程地址,不懂的默认为空就行"
|
||||
/>
|
||||
|
||||
{getAddressCount(form.remoteAddr) > 1 && (
|
||||
<Select
|
||||
label="负载策略"
|
||||
@@ -2017,16 +2025,21 @@ export default function ForwardPage() {
|
||||
<Modal
|
||||
isOpen={diagnosisModalOpen}
|
||||
onOpenChange={setDiagnosisModalOpen}
|
||||
|
||||
size="2xl"
|
||||
scrollBehavior="outside"
|
||||
backdrop="blur"
|
||||
placement="center"
|
||||
size="4xl"
|
||||
scrollBehavior="inside"
|
||||
backdrop="blur"
|
||||
placement="center"
|
||||
classNames={{
|
||||
base: "rounded-2xl",
|
||||
header: "rounded-t-2xl",
|
||||
body: "rounded-none",
|
||||
footer: "rounded-b-2xl"
|
||||
}}
|
||||
>
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
<>
|
||||
<ModalHeader className="flex flex-col gap-1">
|
||||
<ModalHeader className="flex flex-col gap-1 bg-content1 border-b border-divider">
|
||||
<h2 className="text-xl font-bold">转发诊断结果</h2>
|
||||
{currentDiagnosisForward && (
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
@@ -2042,89 +2055,313 @@ export default function ForwardPage() {
|
||||
</div>
|
||||
)}
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<ModalBody className="bg-content1">
|
||||
{diagnosisLoading ? (
|
||||
<div className="flex items-center justify-center py-16">
|
||||
<div className="flex items-center gap-3">
|
||||
<Spinner size="sm" />
|
||||
<span className="text-default-600">正在诊断转发连接...</span>
|
||||
<span className="text-default-600">正在诊断...</span>
|
||||
</div>
|
||||
</div>
|
||||
) : diagnosisResult ? (
|
||||
<div className="space-y-4">
|
||||
{diagnosisResult.results.map((result, index) => {
|
||||
const quality = getQualityDisplay(result.averageTime, result.packetLoss);
|
||||
|
||||
return (
|
||||
<Card key={index} className={`shadow-sm border ${result.success ? 'border-success' : 'border-danger'}`}>
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-foreground">{result.description}</h3>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<span className="text-small text-default-500">节点: {result.nodeName}</span>
|
||||
{/* 统计摘要 */}
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="text-center p-3 bg-default-100 dark:bg-gray-800 rounded-lg border border-divider">
|
||||
<div className="text-2xl font-bold text-foreground">{diagnosisResult.results.length}</div>
|
||||
<div className="text-xs text-default-500 mt-1">总测试数</div>
|
||||
</div>
|
||||
<div className="text-center p-3 bg-success-50 dark:bg-success-900/20 rounded-lg border border-success-200 dark:border-success-700">
|
||||
<div className="text-2xl font-bold text-success-600 dark:text-success-400">
|
||||
{diagnosisResult.results.filter(r => r.success).length}
|
||||
</div>
|
||||
<div className="text-xs text-success-600 dark:text-success-400/80 mt-1">成功</div>
|
||||
</div>
|
||||
<div className="text-center p-3 bg-danger-50 dark:bg-danger-900/20 rounded-lg border border-danger-200 dark:border-danger-700">
|
||||
<div className="text-2xl font-bold text-danger-600 dark:text-danger-400">
|
||||
{diagnosisResult.results.filter(r => !r.success).length}
|
||||
</div>
|
||||
<div className="text-xs text-danger-600 dark:text-danger-400/80 mt-1">失败</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 桌面端表格展示 */}
|
||||
<div className="hidden md:block space-y-3">
|
||||
{(() => {
|
||||
// 使用后端返回的 chainType 和 inx 字段进行分组
|
||||
const groupedResults = {
|
||||
entry: diagnosisResult.results.filter(r => r.fromChainType === 1),
|
||||
chains: {} as Record<number, typeof diagnosisResult.results>,
|
||||
exit: diagnosisResult.results.filter(r => r.fromChainType === 3)
|
||||
};
|
||||
|
||||
// 按 inx 分组链路测试
|
||||
diagnosisResult.results.forEach(r => {
|
||||
if (r.fromChainType === 2 && r.fromInx != null) {
|
||||
if (!groupedResults.chains[r.fromInx]) {
|
||||
groupedResults.chains[r.fromInx] = [];
|
||||
}
|
||||
groupedResults.chains[r.fromInx].push(r);
|
||||
}
|
||||
});
|
||||
|
||||
const renderTableSection = (title: string, results: typeof diagnosisResult.results) => {
|
||||
if (results.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div key={title} className="border border-divider rounded-lg overflow-hidden bg-white dark:bg-gray-800">
|
||||
<div className="bg-primary/10 dark:bg-primary/20 px-3 py-2 border-b border-divider">
|
||||
<h3 className="text-sm font-semibold text-primary">{title}</h3>
|
||||
</div>
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-default-100 dark:bg-gray-700">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left font-semibold text-xs">路径</th>
|
||||
<th className="px-3 py-2 text-center font-semibold text-xs w-20">状态</th>
|
||||
<th className="px-3 py-2 text-center font-semibold text-xs w-24">延迟(ms)</th>
|
||||
<th className="px-3 py-2 text-center font-semibold text-xs w-24">丢包率</th>
|
||||
<th className="px-3 py-2 text-center font-semibold text-xs w-20">质量</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-divider bg-white dark:bg-gray-800">
|
||||
{results.map((result, index) => {
|
||||
const quality = getQualityDisplay(result.averageTime, result.packetLoss);
|
||||
|
||||
return (
|
||||
<tr key={index} className={`hover:bg-default-50 dark:hover:bg-gray-700/50 ${
|
||||
result.success ? 'bg-white dark:bg-gray-800' : 'bg-danger-50 dark:bg-danger-900/30'
|
||||
}`}>
|
||||
<td className="px-3 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`w-5 h-5 rounded-full flex items-center justify-center text-xs ${
|
||||
result.success
|
||||
? 'bg-success text-white'
|
||||
: 'bg-danger text-white'
|
||||
}`}>
|
||||
{result.success ? '✓' : '✗'}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-foreground truncate">{result.description}</div>
|
||||
<div className="text-xs text-default-500 truncate">
|
||||
{result.targetIp}:{result.targetPort}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-center">
|
||||
<Chip
|
||||
color={result.success ? 'success' : 'danger'}
|
||||
variant="flat"
|
||||
variant="flat"
|
||||
size="sm"
|
||||
className="min-w-[50px]"
|
||||
>
|
||||
{result.success ? '连接成功' : '连接失败'}
|
||||
{result.success ? '成功' : '失败'}
|
||||
</Chip>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-center">
|
||||
{result.success ? (
|
||||
<span className="font-semibold text-primary">
|
||||
{result.averageTime?.toFixed(0)}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-default-400">-</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-center">
|
||||
{result.success ? (
|
||||
<span className={`font-semibold ${
|
||||
(result.packetLoss || 0) > 0 ? 'text-warning' : 'text-success'
|
||||
}`}>
|
||||
{result.packetLoss?.toFixed(1)}%
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-default-400">-</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-center">
|
||||
{result.success && quality ? (
|
||||
<Chip
|
||||
color={quality.color as any}
|
||||
variant="flat"
|
||||
size="sm"
|
||||
className="text-xs"
|
||||
>
|
||||
{quality.text}
|
||||
</Chip>
|
||||
) : (
|
||||
<span className="text-default-400">-</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</CardHeader>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 入口测试 */}
|
||||
{renderTableSection('🚪 入口测试', groupedResults.entry)}
|
||||
|
||||
{/* 链路测试(按跳数排序) */}
|
||||
{Object.keys(groupedResults.chains)
|
||||
.map(Number)
|
||||
.sort((a, b) => a - b)
|
||||
.map(hop => renderTableSection(`🔗 转发链 - 第${hop}跳`, groupedResults.chains[hop]))}
|
||||
|
||||
{/* 出口测试 */}
|
||||
{renderTableSection('🚀 出口测试', groupedResults.exit)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{/* 移动端卡片展示 */}
|
||||
<div className="md:hidden space-y-3">
|
||||
{(() => {
|
||||
// 使用后端返回的 chainType 和 inx 字段进行分组
|
||||
const groupedResults = {
|
||||
entry: diagnosisResult.results.filter(r => r.fromChainType === 1),
|
||||
chains: {} as Record<number, typeof diagnosisResult.results>,
|
||||
exit: diagnosisResult.results.filter(r => r.fromChainType === 3)
|
||||
};
|
||||
|
||||
// 按 inx 分组链路测试
|
||||
diagnosisResult.results.forEach(r => {
|
||||
if (r.fromChainType === 2 && r.fromInx != null) {
|
||||
if (!groupedResults.chains[r.fromInx]) {
|
||||
groupedResults.chains[r.fromInx] = [];
|
||||
}
|
||||
groupedResults.chains[r.fromInx].push(r);
|
||||
}
|
||||
});
|
||||
|
||||
const renderCardSection = (title: string, results: typeof diagnosisResult.results) => {
|
||||
if (results.length === 0) return null;
|
||||
|
||||
<CardBody className="pt-0">
|
||||
{result.success ? (
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-primary">{result.averageTime?.toFixed(0)}</div>
|
||||
<div className="text-small text-default-500">平均延迟(ms)</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-warning">{result.packetLoss?.toFixed(1)}</div>
|
||||
<div className="text-small text-default-500">丢包率(%)</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
{quality && (
|
||||
<>
|
||||
<Chip color={quality.color as any} variant="flat" size="lg">
|
||||
{quality.text}
|
||||
</Chip>
|
||||
<div className="text-small text-default-500 mt-1">连接质量</div>
|
||||
</>
|
||||
return (
|
||||
<div key={title} className="space-y-2">
|
||||
<div className="px-2 py-1.5 bg-primary/10 dark:bg-primary/20 rounded-lg border border-primary/30">
|
||||
<h3 className="text-sm font-semibold text-primary">{title}</h3>
|
||||
</div>
|
||||
{results.map((result, index) => {
|
||||
const quality = getQualityDisplay(result.averageTime, result.packetLoss);
|
||||
|
||||
return (
|
||||
<div key={index} className={`border rounded-lg p-3 ${
|
||||
result.success
|
||||
? 'border-divider bg-white dark:bg-gray-800'
|
||||
: 'border-danger-200 dark:border-danger-300/30 bg-danger-50 dark:bg-danger-900/30'
|
||||
}`}>
|
||||
<div className="flex items-start gap-2 mb-2">
|
||||
<span className={`w-6 h-6 rounded-full flex items-center justify-center text-xs flex-shrink-0 ${
|
||||
result.success ? 'bg-success text-white' : 'bg-danger text-white'
|
||||
}`}>
|
||||
{result.success ? '✓' : '✗'}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-semibold text-sm text-foreground break-words">
|
||||
{result.description}
|
||||
</div>
|
||||
<div className="text-xs text-default-500 mt-0.5 break-all">
|
||||
{result.targetIp}:{result.targetPort}
|
||||
</div>
|
||||
</div>
|
||||
<Chip
|
||||
color={result.success ? 'success' : 'danger'}
|
||||
variant="flat"
|
||||
size="sm"
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
{result.success ? '成功' : '失败'}
|
||||
</Chip>
|
||||
</div>
|
||||
|
||||
{result.success ? (
|
||||
<div className="grid grid-cols-3 gap-2 mt-2 pt-2 border-t border-divider">
|
||||
<div className="text-center">
|
||||
<div className="text-lg font-bold text-primary">
|
||||
{result.averageTime?.toFixed(0)}
|
||||
</div>
|
||||
<div className="text-xs text-default-500">延迟(ms)</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className={`text-lg font-bold ${
|
||||
(result.packetLoss || 0) > 0 ? 'text-warning' : 'text-success'
|
||||
}`}>
|
||||
{result.packetLoss?.toFixed(1)}%
|
||||
</div>
|
||||
<div className="text-xs text-default-500">丢包率</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
{quality && (
|
||||
<>
|
||||
<Chip
|
||||
color={quality.color as any}
|
||||
variant="flat"
|
||||
size="sm"
|
||||
className="text-xs"
|
||||
>
|
||||
{quality.text}
|
||||
</Chip>
|
||||
<div className="text-xs text-default-500 mt-0.5">质量</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-2 pt-2 border-t border-divider">
|
||||
<div className="text-xs text-danger">
|
||||
{result.message || '连接失败'}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-small text-default-500 flex items-center gap-1">
|
||||
<span className="flex-shrink-0">目标地址:</span>
|
||||
<code className="font-mono truncate min-w-0" title={`${result.targetIp}${result.targetPort ? ':' + result.targetPort : ''}`}>
|
||||
{result.targetIp}{result.targetPort ? ':' + result.targetPort : ''}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<div className="text-small text-default-500 flex items-center gap-1">
|
||||
<span className="flex-shrink-0">目标地址:</span>
|
||||
<code className="font-mono truncate min-w-0" title={`${result.targetIp}${result.targetPort ? ':' + result.targetPort : ''}`}>
|
||||
{result.targetIp}{result.targetPort ? ':' + result.targetPort : ''}
|
||||
</code>
|
||||
</div>
|
||||
<Alert
|
||||
color="danger"
|
||||
variant="flat"
|
||||
title="错误详情"
|
||||
description={result.message}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 入口测试 */}
|
||||
{renderCardSection('🚪 入口测试', groupedResults.entry)}
|
||||
|
||||
{/* 链路测试(按跳数排序) */}
|
||||
{Object.keys(groupedResults.chains)
|
||||
.map(Number)
|
||||
.sort((a, b) => a - b)
|
||||
.map(hop => renderCardSection(`🔗 转发链 - 第${hop}跳`, groupedResults.chains[hop]))}
|
||||
|
||||
{/* 出口测试 */}
|
||||
{renderCardSection('🚀 出口测试', groupedResults.exit)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{/* 失败详情(仅桌面端显示,移动端已在卡片中显示) */}
|
||||
{diagnosisResult.results.some(r => !r.success) && (
|
||||
<div className="space-y-2 hidden md:block">
|
||||
<h4 className="text-sm font-semibold text-danger">失败详情</h4>
|
||||
<div className="space-y-2">
|
||||
{diagnosisResult.results.filter(r => !r.success).map((result, index) => (
|
||||
<Alert
|
||||
key={index}
|
||||
color="danger"
|
||||
variant="flat"
|
||||
title={result.description}
|
||||
description={result.message || '连接失败'}
|
||||
className="text-xs"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-16">
|
||||
@@ -2137,7 +2374,7 @@ export default function ForwardPage() {
|
||||
</div>
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<ModalFooter className="bg-content1 border-t border-divider">
|
||||
<Button variant="light" onPress={onClose}>
|
||||
关闭
|
||||
</Button>
|
||||
|
||||
@@ -404,6 +404,8 @@ export default function LimitPage() {
|
||||
isInvalid={!!errors.tunnelId}
|
||||
errorMessage={errors.tunnelId}
|
||||
variant="bordered"
|
||||
isDisabled={isEdit}
|
||||
description={isEdit ? "编辑时无法修改绑定隧道" : undefined}
|
||||
>
|
||||
{tunnels.map((tunnel) => (
|
||||
<SelectItem key={tunnel.id}>
|
||||
|
||||
+218
-189
@@ -9,6 +9,7 @@ import { Switch } from "@heroui/switch";
|
||||
import { Spinner } from "@heroui/spinner";
|
||||
import { Alert } from "@heroui/alert";
|
||||
import { Progress } from "@heroui/progress";
|
||||
import { Accordion, AccordionItem } from "@heroui/accordion";
|
||||
import toast from 'react-hot-toast';
|
||||
import axios from 'axios';
|
||||
|
||||
@@ -26,8 +27,9 @@ interface Node {
|
||||
name: string;
|
||||
ip: string;
|
||||
serverIp: string;
|
||||
portSta: number;
|
||||
portEnd: number;
|
||||
port: string;
|
||||
tcpListenAddr?: string;
|
||||
udpListenAddr?: string;
|
||||
version?: string;
|
||||
http?: number; // 0 关 1 开
|
||||
tls?: number; // 0 关 1 开
|
||||
@@ -49,10 +51,11 @@ interface Node {
|
||||
interface NodeForm {
|
||||
id: number | null;
|
||||
name: string;
|
||||
ipString: string;
|
||||
serverIp: string;
|
||||
portSta: number;
|
||||
portEnd: number;
|
||||
port: string;
|
||||
tcpListenAddr: string;
|
||||
udpListenAddr: string;
|
||||
interfaceName: string;
|
||||
http: number; // 0 关 1 开
|
||||
tls: number; // 0 关 1 开
|
||||
socks: number; // 0 关 1 开
|
||||
@@ -73,10 +76,11 @@ export default function NodePage() {
|
||||
const [form, setForm] = useState<NodeForm>({
|
||||
id: null,
|
||||
name: '',
|
||||
ipString: '',
|
||||
serverIp: '',
|
||||
portSta: 1000,
|
||||
portEnd: 65535,
|
||||
port: '1000-65535',
|
||||
tcpListenAddr: '[::]',
|
||||
udpListenAddr: '[::]',
|
||||
interfaceName: '',
|
||||
http: 0,
|
||||
tls: 0,
|
||||
socks: 0
|
||||
@@ -362,6 +366,57 @@ export default function NodePage() {
|
||||
return domainRegex.test(trimmedIp) || singleLabelDomain.test(trimmedIp);
|
||||
};
|
||||
|
||||
// 验证端口格式:支持 80,443,100-600
|
||||
const validatePort = (portStr: string): { valid: boolean; error?: string } => {
|
||||
if (!portStr || !portStr.trim()) {
|
||||
return { valid: false, error: '请输入端口' };
|
||||
}
|
||||
|
||||
const trimmed = portStr.trim();
|
||||
const parts = trimmed.split(',').map(p => p.trim()).filter(p => p);
|
||||
|
||||
if (parts.length === 0) {
|
||||
return { valid: false, error: '请输入有效的端口' };
|
||||
}
|
||||
|
||||
for (const part of parts) {
|
||||
// 检查是否是端口范围 (如 100-600)
|
||||
if (part.includes('-')) {
|
||||
const range = part.split('-').map(p => p.trim());
|
||||
if (range.length !== 2) {
|
||||
return { valid: false, error: `端口范围格式错误: ${part}` };
|
||||
}
|
||||
|
||||
const start = parseInt(range[0]);
|
||||
const end = parseInt(range[1]);
|
||||
|
||||
if (isNaN(start) || isNaN(end)) {
|
||||
return { valid: false, error: `端口必须是数字: ${part}` };
|
||||
}
|
||||
|
||||
if (start < 1 || start > 65535 || end < 1 || end > 65535) {
|
||||
return { valid: false, error: `端口范围必须在 1-65535 之间: ${part}` };
|
||||
}
|
||||
|
||||
if (start >= end) {
|
||||
return { valid: false, error: `起始端口必须小于结束端口: ${part}` };
|
||||
}
|
||||
} else {
|
||||
// 单个端口
|
||||
const port = parseInt(part);
|
||||
if (isNaN(port)) {
|
||||
return { valid: false, error: `端口必须是数字: ${part}` };
|
||||
}
|
||||
|
||||
if (port < 1 || port > 65535) {
|
||||
return { valid: false, error: `端口必须在 1-65535 之间: ${part}` };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
};
|
||||
|
||||
// 表单验证
|
||||
const validateForm = (): boolean => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
@@ -374,36 +429,15 @@ export default function NodePage() {
|
||||
newErrors.name = '节点名称长度不能超过50位';
|
||||
}
|
||||
|
||||
if (!form.ipString.trim()) {
|
||||
newErrors.ipString = '请输入入口IP地址';
|
||||
} else {
|
||||
const ips = form.ipString.split('\n').map(ip => ip.trim()).filter(ip => ip);
|
||||
if (ips.length === 0) {
|
||||
newErrors.ipString = '请输入至少一个有效IP地址';
|
||||
} else {
|
||||
for (let i = 0; i < ips.length; i++) {
|
||||
if (!validateIp(ips[i])) {
|
||||
newErrors.ipString = `第${i + 1}行IP地址格式错误: ${ips[i]}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!form.serverIp.trim()) {
|
||||
newErrors.serverIp = '请输入服务器IP地址';
|
||||
} else if (!validateIp(form.serverIp.trim())) {
|
||||
newErrors.serverIp = '请输入有效的IPv4、IPv6地址或域名';
|
||||
}
|
||||
|
||||
if (!form.portSta || form.portSta < 1 || form.portSta > 65535) {
|
||||
newErrors.portSta = '端口范围必须在1-65535之间';
|
||||
}
|
||||
|
||||
if (!form.portEnd || form.portEnd < 1 || form.portEnd > 65535) {
|
||||
newErrors.portEnd = '端口范围必须在1-65535之间';
|
||||
} else if (form.portEnd < form.portSta) {
|
||||
newErrors.portEnd = '结束端口不能小于起始端口';
|
||||
const portValidation = validatePort(form.port);
|
||||
if (!portValidation.valid) {
|
||||
newErrors.port = portValidation.error || '端口格式错误';
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
@@ -427,10 +461,11 @@ export default function NodePage() {
|
||||
setForm({
|
||||
id: node.id,
|
||||
name: node.name,
|
||||
ipString: node.ip ? node.ip.split(',').map(ip => ip.trim()).join('\n') : '',
|
||||
serverIp: node.serverIp || '',
|
||||
portSta: node.portSta,
|
||||
portEnd: node.portEnd,
|
||||
port: node.port || '1000-65535',
|
||||
tcpListenAddr: node.tcpListenAddr || '[::]',
|
||||
udpListenAddr: node.udpListenAddr || '[::]',
|
||||
interfaceName: (node as any).interfaceName || '',
|
||||
http: typeof node.http === 'number' ? node.http : 1,
|
||||
tls: typeof node.tls === 'number' ? node.tls : 1,
|
||||
socks: typeof node.socks === 'number' ? node.socks : 1
|
||||
@@ -516,28 +551,9 @@ export default function NodePage() {
|
||||
setSubmitLoading(true);
|
||||
|
||||
try {
|
||||
const ipString = form.ipString
|
||||
.split('\n')
|
||||
.map(ip => ip.trim())
|
||||
.filter(ip => ip)
|
||||
.join(',');
|
||||
|
||||
const submitData = {
|
||||
...form,
|
||||
ip: ipString
|
||||
};
|
||||
delete (submitData as any).ipString;
|
||||
|
||||
const apiCall = isEdit ? updateNode : createNode;
|
||||
const data = isEdit ? submitData : {
|
||||
name: form.name,
|
||||
ip: ipString,
|
||||
serverIp: form.serverIp,
|
||||
portSta: form.portSta,
|
||||
portEnd: form.portEnd,
|
||||
http: form.http,
|
||||
tls: form.tls,
|
||||
socks: form.socks
|
||||
const data = {
|
||||
...form
|
||||
};
|
||||
|
||||
const res = await apiCall(data);
|
||||
@@ -550,10 +566,11 @@ export default function NodePage() {
|
||||
n.id === form.id ? {
|
||||
...n,
|
||||
name: form.name,
|
||||
ip: ipString,
|
||||
serverIp: form.serverIp,
|
||||
portSta: form.portSta,
|
||||
portEnd: form.portEnd,
|
||||
port: form.port,
|
||||
tcpListenAddr: form.tcpListenAddr,
|
||||
udpListenAddr: form.udpListenAddr,
|
||||
interfaceName: form.interfaceName,
|
||||
http: form.http,
|
||||
tls: form.tls,
|
||||
socks: form.socks
|
||||
@@ -577,10 +594,11 @@ export default function NodePage() {
|
||||
setForm({
|
||||
id: null,
|
||||
name: '',
|
||||
ipString: '',
|
||||
serverIp: '',
|
||||
portSta: 1000,
|
||||
portEnd: 65535,
|
||||
port: '1000-65535',
|
||||
tcpListenAddr: '[::]',
|
||||
udpListenAddr: '[::]',
|
||||
interfaceName: '',
|
||||
http: 0,
|
||||
tls: 0,
|
||||
socks: 0
|
||||
@@ -643,7 +661,6 @@ export default function NodePage() {
|
||||
<div className="flex justify-between items-start w-full">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-semibold text-foreground truncate text-sm">{node.name}</h3>
|
||||
<p className="text-xs text-default-500 truncate">{node.serverIp}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 ml-2">
|
||||
<Chip
|
||||
@@ -662,25 +679,13 @@ export default function NodePage() {
|
||||
{/* 基础信息 */}
|
||||
<div className="space-y-2 mb-4">
|
||||
<div className="flex justify-between items-center text-sm min-w-0">
|
||||
<span className="text-default-600 flex-shrink-0">入口IP</span>
|
||||
<span className="text-default-600 flex-shrink-0">IP</span>
|
||||
<div className="text-right text-xs min-w-0 flex-1 ml-2">
|
||||
{node.ip ? (
|
||||
node.ip.split(',').length > 1 ? (
|
||||
<span className="font-mono truncate block" title={node.ip.split(',')[0].trim()}>
|
||||
{node.ip.split(',')[0].trim()} +{node.ip.split(',').length - 1}个
|
||||
</span>
|
||||
) : (
|
||||
<span className="font-mono truncate block" title={node.ip.trim()}>
|
||||
{node.ip.trim()}
|
||||
</span>
|
||||
)
|
||||
) : '-'}
|
||||
<span className="font-mono truncate block" title={node.serverIp.trim()}>
|
||||
{node.serverIp.trim()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-default-600">端口</span>
|
||||
<span className="text-xs">{node.portSta}-{node.portEnd}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-default-600">版本</span>
|
||||
<span className="text-xs">{node.version || '未知'}</span>
|
||||
@@ -857,131 +862,155 @@ export default function NodePage() {
|
||||
variant="bordered"
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
label="入口IP"
|
||||
placeholder="一行一个IP地址或域名,例如: 192.168.1.100 example.com"
|
||||
value={form.ipString}
|
||||
onChange={(e) => setForm(prev => ({ ...prev, ipString: e.target.value }))}
|
||||
isInvalid={!!errors.ipString}
|
||||
errorMessage={errors.ipString}
|
||||
<Input
|
||||
label="可用端口"
|
||||
placeholder="例如: 80,443,1000-65535"
|
||||
value={form.port}
|
||||
onChange={(e) => setForm(prev => ({ ...prev, port: e.target.value }))}
|
||||
isInvalid={!!errors.port}
|
||||
errorMessage={errors.port}
|
||||
variant="bordered"
|
||||
minRows={3}
|
||||
maxRows={5}
|
||||
description="支持多个IP,每行一个地址"
|
||||
description="支持单个端口(80)、多个端口(80,443)或端口范围(1000-65535),多个可用逗号分隔"
|
||||
classNames={{
|
||||
input: "font-mono"
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Input
|
||||
label="起始端口"
|
||||
type="number"
|
||||
placeholder="1000"
|
||||
value={form.portSta.toString()}
|
||||
onChange={(e) => setForm(prev => ({ ...prev, portSta: parseInt(e.target.value) || 1000 }))}
|
||||
isInvalid={!!errors.portSta}
|
||||
errorMessage={errors.portSta}
|
||||
variant="bordered"
|
||||
min={1}
|
||||
max={65535}
|
||||
/>
|
||||
{/* 高级配置 */}
|
||||
<Accordion variant="bordered">
|
||||
<AccordionItem
|
||||
key="advanced"
|
||||
aria-label="高级配置"
|
||||
title="高级配置"
|
||||
>
|
||||
<div className="space-y-4 pb-2">
|
||||
<Input
|
||||
label="出口网卡名或IP"
|
||||
placeholder="请输入出口网卡名或IP"
|
||||
value={form.interfaceName}
|
||||
onChange={(e) => setForm(prev => ({ ...prev, interfaceName: e.target.value }))}
|
||||
isInvalid={!!errors.interfaceName}
|
||||
errorMessage={errors.interfaceName}
|
||||
variant="bordered"
|
||||
description="用于多IP服务器指定使用那个IP请求远程地址,不懂的默认为空就行"
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="结束端口"
|
||||
type="number"
|
||||
placeholder="65535"
|
||||
value={form.portEnd.toString()}
|
||||
onChange={(e) => setForm(prev => ({ ...prev, portEnd: parseInt(e.target.value) || 65535 }))}
|
||||
isInvalid={!!errors.portEnd}
|
||||
errorMessage={errors.portEnd}
|
||||
variant="bordered"
|
||||
min={1}
|
||||
max={65535}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Input
|
||||
label="TCP监听地址"
|
||||
placeholder="请输入TCP监听地址"
|
||||
value={form.tcpListenAddr}
|
||||
onChange={(e) => setForm(prev => ({ ...prev, tcpListenAddr: e.target.value }))}
|
||||
isInvalid={!!errors.tcpListenAddr}
|
||||
errorMessage={errors.tcpListenAddr}
|
||||
variant="bordered"
|
||||
startContent={
|
||||
<div className="pointer-events-none flex items-center">
|
||||
<span className="text-default-400 text-small">TCP</span>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 屏蔽协议 */}
|
||||
<div className="mt-1">
|
||||
<div className="text-sm font-medium text-default-700">屏蔽协议</div>
|
||||
<div className="text-xs text-default-500 mb-2">开启开关以屏蔽对应协议</div>
|
||||
{protocolDisabled && (
|
||||
<Alert
|
||||
color="warning"
|
||||
variant="flat"
|
||||
description={protocolDisabledReason || '等待节点上线后再设置'}
|
||||
className="mb-2"
|
||||
/>
|
||||
)}
|
||||
<div className={`grid grid-cols-1 sm:grid-cols-3 gap-3 bg-default-50 dark:bg-default-100 p-3 rounded-md border border-default-200 dark:border-default-100/30 ${protocolDisabled ? 'opacity-70' : ''}`}>
|
||||
{/* HTTP tile */}
|
||||
<div className="px-3 py-3 rounded-lg bg-white dark:bg-default-50 border border-default-200 dark:border-default-100/30 hover:border-primary-200 transition-colors">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<svg className="w-4 h-4 text-default-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M2 10h20"/></svg>
|
||||
<div className="text-sm font-medium text-default-700">HTTP</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-xs text-default-500">禁用/启用</div>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={form.http === 1}
|
||||
isDisabled={protocolDisabled}
|
||||
onValueChange={(v) => setForm(prev => ({ ...prev, http: v ? 1 : 0 }))}
|
||||
<Input
|
||||
label="UDP监听地址"
|
||||
placeholder="请输入UDP监听地址"
|
||||
value={form.udpListenAddr}
|
||||
onChange={(e) => setForm(prev => ({ ...prev, udpListenAddr: e.target.value }))}
|
||||
isInvalid={!!errors.udpListenAddr}
|
||||
errorMessage={errors.udpListenAddr}
|
||||
variant="bordered"
|
||||
startContent={
|
||||
<div className="pointer-events-none flex items-center">
|
||||
<span className="text-default-400 text-small">UDP</span>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-default-400">{form.http === 1 ? '已开启' : '已关闭'}</div>
|
||||
</div>
|
||||
{/* 屏蔽协议 */}
|
||||
<div>
|
||||
<div className="text-sm font-medium text-default-700 mb-2">屏蔽协议</div>
|
||||
<div className="text-xs text-default-500 mb-2">开启开关以屏蔽对应协议</div>
|
||||
{protocolDisabled && (
|
||||
<Alert
|
||||
color="warning"
|
||||
variant="flat"
|
||||
description={protocolDisabledReason || '等待节点上线后再设置'}
|
||||
className="mb-2"
|
||||
/>
|
||||
)}
|
||||
<div className={`grid grid-cols-1 sm:grid-cols-3 gap-3 bg-default-50 dark:bg-default-100 p-3 rounded-md border border-default-200 dark:border-default-100/30 ${protocolDisabled ? 'opacity-70' : ''}`}>
|
||||
{/* HTTP tile */}
|
||||
<div className="px-3 py-3 rounded-lg bg-white dark:bg-default-50 border border-default-200 dark:border-default-100/30 hover:border-primary-200 transition-colors">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<svg className="w-4 h-4 text-default-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M2 10h20"/></svg>
|
||||
<div className="text-sm font-medium text-default-700">HTTP</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-xs text-default-500">禁用/启用</div>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={form.http === 1}
|
||||
isDisabled={protocolDisabled}
|
||||
onValueChange={(v) => setForm(prev => ({ ...prev, http: v ? 1 : 0 }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-default-400">{form.http === 1 ? '已开启' : '已关闭'}</div>
|
||||
</div>
|
||||
|
||||
{/* TLS tile */}
|
||||
<div className="px-3 py-3 rounded-lg bg-white dark:bg-default-50 border border-default-200 dark:border-default-100/30 hover:border-primary-200 transition-colors">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<svg className="w-4 h-4 text-default-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M6 10V7a6 6 0 1 1 12 0v3"/><rect x="4" y="10" width="16" height="10" rx="2"/></svg>
|
||||
<div className="text-sm font-medium text-default-700">TLS</div>
|
||||
{/* TLS tile */}
|
||||
<div className="px-3 py-3 rounded-lg bg-white dark:bg-default-50 border border-default-200 dark:border-default-100/30 hover:border-primary-200 transition-colors">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<svg className="w-4 h-4 text-default-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M6 10V7a6 6 0 1 1 12 0v3"/><rect x="4" y="10" width="16" height="10" rx="2"/></svg>
|
||||
<div className="text-sm font-medium text-default-700">TLS</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-xs text-default-500">禁用/启用</div>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={form.tls === 1}
|
||||
isDisabled={protocolDisabled}
|
||||
onValueChange={(v) => setForm(prev => ({ ...prev, tls: v ? 1 : 0 }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-default-400">{form.tls === 1 ? '已开启' : '已关闭'}</div>
|
||||
</div>
|
||||
|
||||
{/* SOCKS tile */}
|
||||
<div className="px-3 py-3 rounded-lg bg-white dark:bg-default-50 border border-default-200 dark:border-default-100/30 hover:border-primary-200 transition-colors">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<svg className="w-4 h-4 text-default-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||
<div className="text-sm font-medium text-default-700">SOCKS</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-xs text-default-500">禁用/启用</div>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={form.socks === 1}
|
||||
isDisabled={protocolDisabled}
|
||||
onValueChange={(v) => setForm(prev => ({ ...prev, socks: v ? 1 : 0 }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-default-400">{form.socks === 1 ? '已开启' : '已关闭'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-xs text-default-500">禁用/启用</div>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={form.tls === 1}
|
||||
isDisabled={protocolDisabled}
|
||||
onValueChange={(v) => setForm(prev => ({ ...prev, tls: v ? 1 : 0 }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-default-400">{form.tls === 1 ? '已开启' : '已关闭'}</div>
|
||||
</div>
|
||||
|
||||
{/* SOCKS tile */}
|
||||
<div className="px-3 py-3 rounded-lg bg-white dark:bg-default-50 border border-default-200 dark:border-default-100/30 hover:border-primary-200 transition-colors">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<svg className="w-4 h-4 text-default-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||
<div className="text-sm font-medium text-default-700">SOCKS</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-xs text-default-500">禁用/启用</div>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={form.socks === 1}
|
||||
isDisabled={protocolDisabled}
|
||||
onValueChange={(v) => setForm(prev => ({ ...prev, socks: v ? 1 : 0 }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-default-400">{form.socks === 1 ? '已开启' : '已关闭'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<Alert
|
||||
<Alert
|
||||
color="danger"
|
||||
variant="flat"
|
||||
description="请不要在出口节点执行屏蔽协议,否则可能影响转发;屏蔽协议仅需在入口节点执行。"
|
||||
className="mt-3"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
||||
<Alert
|
||||
color="primary"
|
||||
variant="flat"
|
||||
description="服务器ip是你要添加的服务器的ip地址,不是面板的ip地址。入口ip是用于展示在转发页面,面向用户的访问地址。实在理解不到说明你没这个需求,都填节点的服务器ip就行!"
|
||||
className="mt-4"
|
||||
/>
|
||||
color="primary"
|
||||
variant="flat"
|
||||
description="服务器ip是你要添加的服务器的ip地址,不是面板的ip地址。"
|
||||
className="mt-4"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
|
||||
+924
-338
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user