This commit is contained in:
qaq
2025-06-12 15:32:13 +08:00
commit 959ff9e2f2
146 changed files with 35933 additions and 0 deletions
+183
View File
@@ -0,0 +1,183 @@
# ===========================================
# Java / Spring Boot 相关
# ===========================================
# 编译输出
java/target/
java/build/
java/out/
java/bin/
*.class
*.war
*.ear
*.nar
# Maven
java/.mvn/
java/mvnw
java/mvnw.cmd
# Gradle
java/.gradle/
java/gradle/
java/gradlew
java/gradlew.bat
# IDE - IntelliJ IDEA
java/.idea/
java/*.iml
java/*.ipr
java/*.iws
# IDE - Eclipse
java/.project
java/.classpath
java/.settings/
java/.metadata/
# IDE - VSCode
java/.vscode/
# Spring Boot
java/application-*.yml
java/application-*.yaml
java/application-*.properties
!java/application.yml
!java/application.yaml
!java/application.properties
# ===========================================
# Vue / Node.js 前端相关
# ===========================================
# 依赖目录
vue/node_modules/
vue/npm-debug.log*
vue/yarn-debug.log*
vue/yarn-error.log*
# 构建输出
vue/dist/
vue/build/
# IDE
vue/.vscode/
vue/.idea/
# 缓存
vue/.cache/
vue/.temp/
# ===========================================
# Go 相关
# ===========================================
# 编译输出
go/main
go/gost
go/*.exe
go/*.exe~
go/*.dll
go/*.so
go/*.dylib
# cmd目录下的可执行文件和配置
go/cmd/**/gost
go/cmd/**/gost.exe
go/cmd/**/*.exe
go/cmd/**/*.json
go/cmd/**/main
go/cmd/**/main.exe
# Go modules
go/go.sum
# IDE
go/.vscode/
go/.idea/
# ===========================================
# 日志和数据文件
# ===========================================
# 应用日志
applog/
logs/
*.log
*.log.*
# 数据库文件
*.db
*.sqlite
*.sqlite3
# ===========================================
# 系统和临时文件
# ===========================================
# macOS
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Windows
*.tmp
*.swp
*.swo
*~
# Linux
*~
# ===========================================
# 安全和配置
# ===========================================
# 配置文件(包含敏感信息)
config/database.yml
config/secrets.yml
# SSL证书
*.pem
*.key
*.crt
*.csr
# ===========================================
# 版本控制和部署
# ===========================================
# Git
.git/
.gitconfig
# Docker
.dockerignore
docker-compose.override.yml
# ===========================================
# 其他
# ===========================================
# 压缩文件
*.zip
*.tar.gz
*.rar
# 备份文件
*.bak
*.backup
*.orig
# 测试覆盖率
coverage/
*.cover
# 文档生成
docs/build/
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+146
View File
@@ -0,0 +1,146 @@
# 转发面板
本项目基于 [go-gost/gost](https://github.com/go-gost/gost) 和 [go-gost/x](https://github.com/go-gost/x) 两个开源库,实现了转发面板。
---
## 特性
- 支持按 **隧道账号级别** 管理流量转发数量,可用于用户/隧道配额控制
- 支持 **TCP** 和 **UDP** 协议的转发
- 支持两种转发模式:**端口转发** 与 **隧道转发**
- 可针对 **指定用户的指定隧道进行限速** 设置
- 支持配置 **单向或双向流量计费方式**,灵活适配不同计费模型
- 提供灵活的转发策略配置,适用于多种网络代理场景
---
## 部署流程
### 源码编译部署
```bash
暂时没时间写,能源码部署的都是大佬,不差我这点文档
```
---
### Docker Compose部署
#### 快速部署
```bash
# 1. 进入部署目录
cd deployment
# 2. 给脚本添加执行权限
chmod +x deploy.sh
# 3. 启动所有服务
./deploy.sh start
```
#### 自定义服务器地址部署
```bash
# 如果你的服务器不是localhost,可以自定义API地址
VUE_APP_API_URL="http://your-server-ip:6365/api/v1" \
VUE_APP_WS_URL="ws://your-server-ip:6365" \
./deploy.sh start
```
#### 手动部署步骤
```bash
# 1. 确保Vue项目已打包到deployment/dist目录
cd vue
npm run build
cp -r dist ../deployment/
# 2. 进入部署目录
cd ../deployment
# 3. 启动所有服务
docker-compose up -d
# 4. 查看服务状态
docker-compose ps
# 5. 查看日志
docker-compose logs -f
```
#### 常用管理命令
```bash
# 停止服务
./deploy.sh stop
# 重启服务
./deploy.sh restart
# 查看服务状态
./deploy.sh status
# 查看实时日志
./deploy.sh logs
# 重新构建前端镜像
./deploy.sh build
# 清理未使用的Docker资源
./deploy.sh clean
```
#### 服务信息
| 服务 | 端口 | 说明 |
|------|------|------|
| 前端界面 | 80 | Vue.js Web管理界面 |
| 后端API | 6365 | Spring Boot后端服务 |
| MySQL | 3306 | 数据库服务 |
| Redis | 6379 | 缓存服务 |
#### 默认管理员账号
- **用户名**: admin_user
- **密码**: admin_user
> ⚠️ 首次登录后请立即修改默认密码!
#### 访问地址
- 前端管理界面: http://localhost
- 后端API文档: http://localhost:6365
- 数据库连接: localhost:3306 (用户名: gost, 密码: pwd)
#### 故障排除
```bash
# 查看服务日志
./deploy.sh logs
# 检查端口占用
netstat -tulpn | grep :80
netstat -tulpn | grep :6365
# 重新构建前端(如果前端有问题)
./deploy.sh build
./deploy.sh restart
# 完全重启(如果数据库有问题)
./deploy.sh stop
./deploy.sh start
```
---
## 免责声明
本项目仅供个人学习与研究使用,基于开源项目进行二次开发。
使用本项目所带来的任何风险由使用者自行承担,包括但不限于:
- 配置不当或使用错误导致的服务异常或不可用;
- 使用本项目引发的网络攻击、封禁、滥用等行为;
- 服务器因使用本项目被入侵、渗透、滥用导致的数据泄露、资源消耗或损失;
- 因违反当地法律法规所产生的任何法律责任。
**作者对因使用本项目所造成的任何直接或间接损失概不负责,也不提供任何形式的担保或技术支持。**
请确保在合法、合规、安全的前提下使用本项目。
+22
View File
@@ -0,0 +1,22 @@
# 使用nginx作为基础镜像
FROM nginx:alpine
# 安装envsubst工具
RUN apk add --no-cache gettext
# 复制nginx配置模板
COPY nginx.conf.template /etc/nginx/nginx.conf.template
# 复制预打包的dist文件夹
COPY dist /usr/share/nginx/html
# 创建运行时配置脚本
COPY docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
# 暴露端口
EXPOSE 80
# 使用自定义启动脚本
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]
Binary file not shown.
+4
View File
@@ -0,0 +1,4 @@
window.APP_CONFIG = {
VUE_APP_API_URL: "${VUE_APP_API_URL}",
VUE_APP_WS_URL: "${VUE_APP_WS_URL}"
}
+222
View File
@@ -0,0 +1,222 @@
#!/bin/bash
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# 获取脚本所在目录
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# 显示帮助信息
show_help() {
echo "Gost转发面板 Docker Compose 部署脚本"
echo ""
echo "用法: $0 [命令] [选项]"
echo ""
echo "命令:"
echo " start 启动所有服务"
echo " stop 停止所有服务"
echo " restart 重启所有服务"
echo " build 重新构建前端镜像"
echo " logs 查看服务日志"
echo " status 查看服务状态"
echo " clean 清理未使用的容器和镜像"
echo ""
echo "选项:"
echo " -h, --help 显示此帮助信息"
echo ""
echo "环境变量配置:"
echo " VUE_APP_API_URL 前端API地址 (默认: http://localhost:6365/api/v1)"
echo " VUE_APP_WS_URL WebSocket地址 (默认: ws://localhost:6365)"
echo ""
echo "示例:"
echo " # 启动服务"
echo " $0 start"
echo ""
echo " # 自定义服务器地址启动"
echo " VUE_APP_API_URL=http://192.168.1.100:6365/api/v1 VUE_APP_WS_URL=ws://192.168.1.100:6365 $0 start"
}
# 检查docker和docker-compose是否安装
check_dependencies() {
if ! command -v docker &> /dev/null; then
echo -e "${RED}错误: Docker 未安装${NC}"
echo "请先安装Docker: https://docs.docker.com/get-docker/"
exit 1
fi
if ! command -v docker-compose &> /dev/null; then
echo -e "${RED}错误: Docker Compose 未安装${NC}"
echo "请先安装Docker Compose: https://docs.docker.com/compose/install/"
exit 1
fi
}
# 从docker-compose.yml读取端口配置
get_service_port() {
local service=$1
local port_line=$(grep -A 10 "^ $service:" docker-compose.yml | grep -E "^\s+ports:" -A 1 | tail -1)
if [[ $port_line =~ \"([0-9]+):[0-9]+\" ]]; then
echo "${BASH_REMATCH[1]}"
elif [[ $port_line =~ ([0-9]+):[0-9]+ ]]; then
echo $(echo $port_line | cut -d':' -f1 | tr -d ' "')
fi
}
# 从docker-compose.yml读取数据库配置
get_db_config() {
local config_key=$1
local config_value=$(grep -A 20 "environment:" docker-compose.yml | grep "$config_key:" | head -1 | cut -d':' -f2 | cut -d'#' -f1 | xargs)
echo "$config_value"
}
# 启动服务
start_services() {
echo -e "${GREEN}启动Gost转发面板服务...${NC}"
# 设置默认环境变量
export VUE_APP_API_URL=${VUE_APP_API_URL:-"http://localhost:6365/api/v1"}
export VUE_APP_WS_URL=${VUE_APP_WS_URL:-"ws://localhost:6365"}
echo -e "${YELLOW}环境变量配置:${NC}"
echo "VUE_APP_API_URL: $VUE_APP_API_URL"
echo "VUE_APP_WS_URL: $VUE_APP_WS_URL"
echo ""
# 切换到脚本目录
cd "$SCRIPT_DIR"
docker-compose up -d
if [ $? -eq 0 ]; then
echo -e "${GREEN}服务启动成功!${NC}"
echo ""
# 从配置文件读取端口信息
FRONTEND_PORT=$(get_service_port "frontend")
BACKEND_PORT=$(get_service_port "backend")
MYSQL_PORT=$(get_service_port "mysql")
REDIS_PORT=$(get_service_port "redis")
# 从配置文件读取数据库配置
DB_USER=$(get_db_config "MYSQL_USER")
DB_PASSWORD=$(get_db_config "MYSQL_PASSWORD")
echo -e "${YELLOW}访问地址:${NC}"
echo " 前端界面: http://localhost:${FRONTEND_PORT:-80}"
echo " 后端API: http://localhost:${BACKEND_PORT:-6365}"
echo " 数据库: localhost:${MYSQL_PORT:-3306} (用户名: ${DB_USER:-gost}, 密码: ${DB_PASSWORD:-pwd})"
echo " Redis: localhost:${REDIS_PORT:-6379}"
echo ""
echo -e "${YELLOW}默认管理员账号:${NC}"
echo " 用户名: admin_user"
echo " 密码: admin_user"
echo ""
echo -e "${GREEN}部署完成!${NC}"
else
echo -e "${RED}服务启动失败!${NC}"
exit 1
fi
}
# 停止服务
stop_services() {
echo -e "${YELLOW}停止所有服务...${NC}"
cd "$SCRIPT_DIR"
docker-compose down
echo -e "${GREEN}服务已停止${NC}"
}
# 重启服务
restart_services() {
echo -e "${YELLOW}重启所有服务...${NC}"
cd "$SCRIPT_DIR"
docker-compose restart
echo -e "${GREEN}服务重启完成${NC}"
}
# 重新构建前端
build_frontend() {
echo -e "${YELLOW}重新构建前端镜像...${NC}"
cd "$SCRIPT_DIR"
docker-compose build frontend --no-cache
echo -e "${GREEN}前端镜像构建完成${NC}"
}
# 查看日志
show_logs() {
cd "$SCRIPT_DIR"
docker-compose logs -f
}
# 查看状态
show_status() {
cd "$SCRIPT_DIR"
docker-compose ps
}
# 清理未使用的容器和镜像
clean_docker() {
echo -e "${YELLOW}清理未使用的Docker资源...${NC}"
docker system prune -f
echo -e "${GREEN}清理完成${NC}"
}
# 完全清理(包括数据卷)
clean_all() {
echo -e "${RED}警告: 这将删除所有数据,包括数据库!${NC}"
read -p "确定要继续吗?(y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
cd "$SCRIPT_DIR"
docker-compose down -v
docker system prune -f
echo -e "${GREEN}完全清理完成${NC}"
else
echo "操作已取消"
fi
}
# 主程序
main() {
check_dependencies
case "${1:-start}" in
start)
start_services
;;
stop)
stop_services
;;
restart)
restart_services
;;
build)
build_frontend
;;
logs)
show_logs
;;
status)
show_status
;;
clean)
clean_docker
;;
clean-all)
clean_all
;;
-h|--help)
show_help
;;
*)
echo -e "${RED}未知命令: $1${NC}"
show_help
exit 1
;;
esac
}
main "$@"
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
<!doctype html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"><link rel="icon" href="favicon.ico"><title>gost</title><script src="/config.js"></script><script defer="defer" src="js/chunk-vendors.d034d5e6.js"></script><script defer="defer" src="js/app.9855802f.js"></script><link href="css/chunk-vendors.77489a8d.css" rel="stylesheet"><link href="css/app.f163900c.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but gost doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+99
View File
@@ -0,0 +1,99 @@
services:
mysql:
image: mysql:8.0
container_name: gost-mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: rootpwd # 数据库密码
MYSQL_DATABASE: gost # 数据库名
MYSQL_USER: gost # 数据库用户
MYSQL_PASSWORD: pwd # 数据库密码
TZ: Asia/Shanghai
ports:
- "3306:3306"
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
networks:
- gost-network
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
timeout: 10s
retries: 10
redis:
image: redis:alpine
container_name: gost-redis
restart: unless-stopped
ports:
- "6379:6379"
networks:
- gost-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
backend:
image: openjdk:8-jre-alpine
container_name: gost-backend
restart: unless-stopped
environment:
DB_HOST: mysql # 数据库地址
DB_NAME: gost # 数据库名
DB_USER: gost # 数据库用户
DB_PASSWORD: pwd # 数据库密码
JWT_SECRET: test123 # JWT密钥
LOG_DIR: /app/logs # 日志目录
SERVER_ADDR: 127.0.0.1:6365 # 服务器地址 ip:port 不能套cdn 用于节点连接
JAVA_OPTS: "-Xms256m -Xmx512m -Dfile.encoding=UTF-8 -Duser.timezone=Asia/Shanghai"
ports:
- "6365:6365"
volumes:
- ./admin-0.0.1-SNAPSHOT.jar:/app/admin.jar:ro
- ./applog:/app/logs
command: sh -c "java $$JAVA_OPTS -jar /app/admin.jar"
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
networks:
- gost-network
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: 60s
frontend:
build:
context: .
dockerfile: Dockerfile.vue
container_name: gost-frontend
restart: unless-stopped
ports:
- "80:80"
environment:
BACKEND_HOST: backend # 后端地址
BACKEND_PORT: 6365 # 后端端口
VUE_APP_API_URL: http://localhost:6365/api/v1 # 前端API地址
VUE_APP_WS_URL: ws://localhost:6365 # 前端WebSocket地址
depends_on:
backend:
condition: service_healthy
networks:
- gost-network
volumes:
mysql_data:
driver: local
networks:
gost-network:
driver: bridge
+25
View File
@@ -0,0 +1,25 @@
#!/bin/sh
# 设置默认环境变量
export BACKEND_HOST=${BACKEND_HOST:-backend}
export BACKEND_PORT=${BACKEND_PORT:-6365}
export VUE_APP_API_URL=${VUE_APP_API_URL:-"http://localhost:6365/api/v1"}
export VUE_APP_WS_URL=${VUE_APP_WS_URL:-"ws://localhost:6365"}
echo "=== 容器启动配置 ==="
echo "BACKEND_HOST: $BACKEND_HOST"
echo "BACKEND_PORT: $BACKEND_PORT"
echo "VUE_APP_API_URL: $VUE_APP_API_URL"
echo "VUE_APP_WS_URL: $VUE_APP_WS_URL"
echo "===================="
# 使用envsubst替换nginx配置中的环境变量
envsubst '${BACKEND_HOST} ${BACKEND_PORT}' < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf
# 使用envsubst替换前端配置文件中的环境变量
envsubst '${VUE_APP_API_URL} ${VUE_APP_WS_URL}' < /usr/share/nginx/html/config.template.js > /usr/share/nginx/html/config.js
echo "配置文件已生成"
# 启动nginx
exec "$@"
+237
View File
@@ -0,0 +1,237 @@
-- phpMyAdmin SQL Dump
-- version 5.2.0
-- https://www.phpmyadmin.net/
--
-- 主机: localhost
-- 生成日期: 2025-06-12 15:03:23
-- 服务器版本: 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` varchar(100) NOT 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
) 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` varchar(100) NOT NULL DEFAULT '-',
`port` 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;
-- --------------------------------------------------------
--
-- 表的结构 `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;
-- --------------------------------------------------------
--
-- 表的结构 `tunnel`
--
CREATE TABLE `tunnel` (
`id` int(10) NOT NULL,
`name` varchar(100) NOT NULL,
`in_node_id` int(10) NOT NULL,
`in_ip` varchar(100) NOT NULL,
`in_port_sta` int(10) NOT NULL,
`in_port_end` int(10) NOT NULL,
`out_node_id` int(10) NOT NULL,
`out_ip` varchar(100) NOT NULL,
`out_ip_sta` int(10) NOT NULL,
`out_ip_end` int(10) NOT NULL,
`type` int(10) NOT NULL,
`flow` int(10) NOT 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,
`name` varchar(100) 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`, `name`, `user`, `pwd`, `role_id`, `exp_time`, `flow`, `in_flow`, `out_flow`, `flow_reset_time`, `num`, `created_time`, `updated_time`, `status`) VALUES
(1, 'admin_user', 'admin_user', '3c85cdebade1c51cf64ca9f3c09d182d', 0, 1780480500000, 99999, 0, 0, 1, 99999, 1748914865000, 1749707064856, 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;
--
-- 转储表的索引
--
--
-- 表的索引 `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`);
--
-- 表的索引 `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`);
--
-- 在导出的表使用AUTO_INCREMENT
--
--
-- 使用表AUTO_INCREMENT `forward`
--
ALTER TABLE `forward`
MODIFY `id` int(10) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=60;
--
-- 使用表AUTO_INCREMENT `node`
--
ALTER TABLE `node`
MODIFY `id` int(10) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=5;
--
-- 使用表AUTO_INCREMENT `speed_limit`
--
ALTER TABLE `speed_limit`
MODIFY `id` int(10) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=28;
--
-- 使用表AUTO_INCREMENT `tunnel`
--
ALTER TABLE `tunnel`
MODIFY `id` int(10) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=10;
--
-- 使用表AUTO_INCREMENT `user`
--
ALTER TABLE `user`
MODIFY `id` int(10) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=20;
--
-- 使用表AUTO_INCREMENT `user_tunnel`
--
ALTER TABLE `user_tunnel`
MODIFY `id` int(10) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=39;
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 */;
+87
View File
@@ -0,0 +1,87 @@
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# 日志格式
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log warn;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# gzip压缩
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied any;
gzip_comp_level 6;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml+rss
application/atom+xml
image/svg+xml;
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# 静态文件缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Vue Router的history模式支持
location / {
try_files $uri $uri/ /index.html;
}
# API代理到后端
location ^~ /api/v1/ {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://${BACKEND_HOST}:${BACKEND_PORT}/api/v1/;
}
# WebSocket代理 - system-info
location /system-info {
proxy_pass http://${BACKEND_HOST}:${BACKEND_PORT}/system-info;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# config.js 文件已经在容器启动时生成,这里只需要设置缓存策略
location /config.js {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}
}
}
+139
View File
@@ -0,0 +1,139 @@
module github.com/go-gost/gost
go 1.23.0
toolchain go1.23.4
require (
github.com/go-gost/core v0.3.1
github.com/go-gost/x v0.5.3
github.com/gorilla/websocket v1.5.3
github.com/judwhite/go-svc v1.2.1
github.com/shirou/gopsutil/v3 v3.24.5
)
// 添加replace指令,使用本地代码替换远程库
replace github.com/go-gost/x => ./cmd/gost/go-gost
require (
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
github.com/alessio/shellescape v1.4.1 // indirect
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/coreos/go-iptables v0.5.0 // indirect
github.com/danieljoos/wincred v1.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/cors v1.7.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.10.0 // indirect
github.com/go-gost/gosocks4 v0.0.1 // indirect
github.com/go-gost/gosocks5 v0.4.2 // indirect
github.com/go-gost/plugin v0.2.0 // indirect
github.com/go-gost/relay v0.5.0 // indirect
github.com/go-gost/tls-dissector v0.1.1 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/go-redis/redis/v8 v8.11.5 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/gopacket v1.1.19 // indirect
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gravitational/trace v1.1.16-0.20220114165159-14a9a7dd6aaf // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jonboulle/clockwork v0.2.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/klauspost/reedsolomon v1.11.8 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/miekg/dns v1.1.61 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/onsi/ginkgo/v2 v2.22.0 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pion/dtls/v2 v2.2.6 // indirect
github.com/pion/logging v0.2.2 // indirect
github.com/pion/transport/v2 v2.0.2 // indirect
github.com/pion/udp/v2 v2.0.1 // indirect
github.com/pires/go-proxyproto v0.7.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/prometheus/client_golang v1.19.1 // indirect
github.com/prometheus/client_model v0.6.0 // indirect
github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.48.2 // indirect
github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 // indirect
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
github.com/rs/xid v1.3.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/shadowsocks/go-shadowsocks2 v0.1.5 // indirect
github.com/shadowsocks/shadowsocks-go v0.0.0-20200409064450-3e585ff90601 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.19.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/templexxx/cpu v0.1.0 // indirect
github.com/templexxx/xorsimd v0.4.2 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/vishvananda/netlink v1.1.0 // indirect
github.com/vishvananda/netns v0.0.4 // indirect
github.com/vulcand/predicate v1.2.0 // indirect
github.com/xtaci/kcp-go/v5 v5.6.5 // indirect
github.com/xtaci/smux v1.5.31 // indirect
github.com/xtaci/tcpraw v1.2.25 // indirect
github.com/yl2chen/cidranger v1.0.2 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
github.com/zalando/go-keyring v0.2.4 // indirect
go.uber.org/mock v0.5.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/exp v0.0.0-20241210194714-1829a127f884 // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/term v0.27.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.28.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
google.golang.org/grpc v1.67.1 // indirect
google.golang.org/protobuf v1.35.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Executable
BIN
View File
Binary file not shown.
+93
View File
@@ -0,0 +1,93 @@
#!/bin/bash
ARCH=$(uname -m)
if [[ "$ARCH" != "x86_64" ]]; then
echo "❌ 不支持的架构: $ARCH,仅支持 x86_64。"
exit 1
fi
# 固定gost下载地址
DOWNLOAD_URL="https://raw.githubusercontent.com/bqlpfy/forward-panel/refs/heads/main/gost"
while getopts "a:p:s:" opt; do
case $opt in
a) SERVER_ADDR="$OPTARG" ;;
p) PORT="$OPTARG" ;;
s) SECRET="$OPTARG" ;;
*) echo "无效参数"; exit 1 ;;
esac
done
if [[ -z "$SERVER_ADDR" || -z "$PORT" || -z "$SECRET" ]]; then
echo "用法: $0 -a 服务器地址 -p 端口 -s 密钥"
exit 1
fi
INSTALL_DIR="/etc/gost"
mkdir -p "$INSTALL_DIR"
for FILE in gost config.json gost.json; do
if [[ -f "$INSTALL_DIR/$FILE" ]]; then
echo "删除已有文件: $INSTALL_DIR/$FILE"
rm -f "$INSTALL_DIR/$FILE"
fi
done
# 下载gost可执行文件
curl -L "$DOWNLOAD_URL" -o "$INSTALL_DIR/gost"
chmod +x "$INSTALL_DIR/gost"
cat > "$INSTALL_DIR/config.json" <<EOF
{
"addr": "$SERVER_ADDR",
"secret": "$SECRET"
}
EOF
cat > "$INSTALL_DIR/gost.json" <<EOF
{
"services": [
{
"name": "web_api",
"addr": ":$PORT",
"handler": {
"type": "api",
"auth": {
"username": "$SECRET",
"password": "$SECRET"
},
"metadata": {
"pathPrefix": "/api"
}
}
}
]
}
EOF
cat > /etc/systemd/system/gost.service <<EOF
[Unit]
Description=Gost Proxy Service
After=network.target
[Service]
WorkingDirectory=$INSTALL_DIR
ExecStart=$INSTALL_DIR/gost
Restart=on-failure
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reexec
systemctl daemon-reload
systemctl enable gost
systemctl restart gost
echo "安装完成,配置文件位于 $INSTALL_DIR,并设置为开机启动。"
+33
View File
@@ -0,0 +1,33 @@
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
+172
View File
@@ -0,0 +1,172 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.admin</groupId>
<artifactId>admin</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>admin</name>
<description>admin</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.6.13</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.43</version> <!-- 版本号可根据需要调整 -->
</dependency>
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>javax.mail</artifactId>
<version>1.6.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.github.yulichang</groupId>
<artifactId>mybatis-plus-join</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.70</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.3</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.11</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.31</version> <!-- 请根据需要选择合适的版本 -->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<mainClass>com.admin.AdminApplication</mainClass>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,21 @@
package com.admin;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@EnableAsync
public class AdminApplication {
public static void main(String[] args) {
SpringApplication.run(AdminApplication.class, args);
}
}
@@ -0,0 +1,148 @@
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) {
// 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
return projectPath + "/src/main/resources/mapper/" + pc.getModuleName()
+ "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
}
});
/*
cfg.setFileCreate(new IFileCreate() {
@Override
public boolean isCreate(ConfigBuilder configBuilder, FileType fileType, String filePath) {
// 判断自定义文件夹是否需要创建
checkDir("调用默认方法创建的目录,自定义目录用");
if (fileType == FileType.MAPPER) {
// 已经生成 mapper 文件判断存在,不想重新生成返回 false
return !new File(filePath).exists();
}
// 允许生成模板文件
return true;
}
});
*/
cfg.setFileOutConfigList(focList);
mpg.setCfg(cfg);
// 配置模板
TemplateConfig templateConfig = new TemplateConfig();
// 配置自定义输出模板
//指定自定义模板路径,注意不要带上.ftl/.vm, 会根据使用的模板引擎自动识别
// templateConfig.setEntity("templates/entity2.java");
// templateConfig.setService();
// templateConfig.setController();
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.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 权限控制注解
* 用于标记需要管理员权限的方法(role_id = 0)
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireRole {
}
@@ -0,0 +1,9 @@
package com.admin.common.aop;
import java.lang.annotation.*;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogAnnotation {}
@@ -0,0 +1,193 @@
package com.admin.common.aop;
import cn.hutool.core.util.ArrayUtil;
import com.admin.common.utils.JwtUtil;
import com.alibaba.fastjson.JSON;
import com.admin.common.utils.HttpContextUtils;
import com.admin.common.utils.IpUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.CodeSignature;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
@Component
@Aspect
@Slf4j
public class LogAspect {
@Pointcut("@annotation(com.admin.common.aop.LogAnnotation)")
public void pt() {
}
/**
* 返回后通知(@AfterReturning):在某连接点(joinpoint)
* 正常完成后执行的通知:例如,一个方法没有抛出任何异常,正常返回
* 方法执行完毕之后
* 注意在这里不能使用ProceedingJoinPoint
* 不然会报错ProceedingJoinPoint is only supported for around advice
* crmAspect()指向需要控制的方法
* returning 注解返回值
*
* @param joinPoint
* @param returnValue 返回值
* @throws Exception
*/
@AfterReturning(value = "pt()", returning = "returnValue")
public void log(JoinPoint joinPoint, Object returnValue) throws Throwable {
// 获取请求信息
HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
// 获取请求方法类型(POST/GET等)
String requestMethod = request.getMethod();
// 获取用户ID
String authorization = request.getHeader("Authorization") + "";
Object user_id = "未登录"; // 请求用户的id
if (!authorization.equals("null")) {
user_id = JwtUtil.getUserIdFromToken(authorization);
}
// 获取请求IP
String ipAddr = IpUtils.getIpAddr(request);
// 获取方法签名信息
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 获取控制器方法名
String className = joinPoint.getTarget().getClass().getName();
String methodName = signature.getName();
String controllerMethod = className + "." + methodName;
// 获取请求参数
String requestParams = getRequestParams(joinPoint);
// 获取返回参数
String responseParams = returnValue != null ? JSON.toJSONString(returnValue) : "无返回值";
// 合并为一条完整的日志信息
String logMessage = String.format(
"【请求日志】用户ID:[%s], IP地址:[%s], 请求方式:[%s], 控制器方法:[%s], 请求参数:[%s], 返回参数:[%s]", user_id, ipAddr, requestMethod, controllerMethod, requestParams, responseParams
);
// 打印单条完整日志
log.info(logMessage);
}
/**
* 抛出异常后通知(@AfterThrowing):方法抛出异常退出时执行的通知
* 注意在这里不能使用ProceedingJoinPoint
* 不然会报错ProceedingJoinPoint is only supported for around advice
* throwing注解为错误信息
*
* @param joinPoint
* @param ex
*/
@AfterThrowing(value = "pt()", throwing = "ex")
public void recordLog(JoinPoint joinPoint, Exception ex) {
try {
// 获取请求信息
HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
// 获取请求方法类型(POST/GET等)
String requestMethod = request.getMethod();
// 获取用户ID
String authorization = request.getHeader("Authorization") + "";
Object user_id = "未登录"; // 请求用户的id
if (!authorization.equals("null")) {
user_id = JwtUtil.getUserIdFromToken(authorization);
}
// 获取请求IP
String ipAddr = IpUtils.getIpAddr(request);
// 获取方法签名信息
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 获取控制器方法名
String className = joinPoint.getTarget().getClass().getName();
String methodName = signature.getName();
String controllerMethod = className + "." + methodName;
// 获取请求参数
String requestParams = getRequestParams(joinPoint);
// 获取异常信息
String exceptionMsg = ex != null ? ex.getMessage() : "未知异常";
// 合并为一条完整的异常日志信息
String errorMessage = String.format(
"【异常日志】用户ID:[%s], IP地址:[%s], 请求方式:[%s], 控制器方法:[%s], 请求参数:[%s], 异常信息:[%s]", user_id, ipAddr, requestMethod, controllerMethod, requestParams, exceptionMsg
);
// 打印单条完整异常日志
log.error(errorMessage, ex);
} catch (Exception e) {
log.error("记录异常日志时出错: {}", e.getMessage());
}
}
/**
* 获取请求参数
*/
private String getRequestParams(JoinPoint joinPoint) {
try {
Object[] args = joinPoint.getArgs();
if (args.length == 0) {
return "无参数";
} else if (args[0] != null && args[0].toString().contains("SecurityContextHolderAwareRequestWrapper")) {
return JSON.toJSONString(Arrays.toString(ArrayUtil.remove(args, 0)));
} else {
// 检查是否只有一个参数且已经是JSON字符串格式
if (args.length == 1 && args[0] != null) {
// 如果参数本身就是字符串且是JSON格式,直接返回
if (args[0] instanceof String && ((String) args[0]).startsWith("{") && ((String) args[0]).endsWith("}")) {
return (String) args[0];
}
// 如果参数是普通对象,直接序列化
try {
return JSON.toJSONString(args[0]);
} catch (Exception e) {
// 如果序列化失败,再尝试使用参数名映射
Map<String, Object> map = new HashMap<>();
String[] names = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
if (names != null) {
map.put(names[0], args[0]);
return JSON.toJSONString(map);
}
return JSON.toJSONString(args[0]);
}
} else {
// 多个参数时,使用参数名映射
Map<String, Object> map = new HashMap<>();
String[] names = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
if (names != null) {
for (int i = 0; i < names.length; i++) {
map.put(names[i], args[i]);
}
}
return JSON.toJSONString(map);
}
}
} catch (Exception e) {
return "获取参数失败: " + e.getMessage();
}
}
}
@@ -0,0 +1,49 @@
package com.admin.common.aop;
import com.admin.common.annotation.RequireRole;
import com.admin.common.lang.R;
import com.admin.common.utils.JwtUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* 权限控制切面
* 处理 @RequireRole 注解,检查管理员权限(role_id = 0)
* 注意:JWT拦截器已经验证了token的有效性,这里只需要检查权限
*/
@Aspect
@Component
public class RoleAspect {
@Around("@annotation(requireRole)")
public Object checkRole(ProceedingJoinPoint joinPoint, RequireRole requireRole) throws Throwable {
// 获取当前请求
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
return R.err(500, "无法获取请求信息");
}
HttpServletRequest request = attributes.getRequest();
String token = request.getHeader("Authorization");
// JWT拦截器已经验证过token存在且有效,这里直接获取role_id
Integer roleId = JwtUtil.getRoleIdFromToken(token);
if (roleId == null) {
return R.err(401, "无法获取用户权限信息");
}
// 检查是否为管理员(role_id = 0)
if (roleId != 0) {
return R.err(403, "权限不足,仅管理员可操作");
}
// 权限检查通过,执行原方法
return joinPoint.proceed();
}
}
@@ -0,0 +1,18 @@
package com.admin.common.dto;
import lombok.Data;
import javax.validation.constraints.NotBlank;
@Data
public class ChangePasswordDto {
@NotBlank(message = "当前密码不能为空")
private String currentPassword;
@NotBlank(message = "新密码不能为空")
private String newPassword;
@NotBlank(message = "确认密码不能为空")
private String confirmPassword;
}
@@ -0,0 +1,20 @@
package com.admin.common.dto;
import lombok.Data;
@Data
public class FlowDto {
// [{n=41_tcp, t=cc, u=73225, d=35043}, {n=41_tcp, t=conn, u=35043, d=73225}]
// 转发id_类型
private String n;
// 是请求还是接收
private String t;
// 上传流量
private Long u;
// 下载流量
private Long d;
}
@@ -0,0 +1,18 @@
package com.admin.common.dto;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@Data
public class ForwardDto {
@NotBlank(message = "转发名称不能为空")
private String name;
@NotNull(message = "隧道ID不能为空")
private Integer tunnelId;
@NotBlank(message = "远程地址不能为空")
private String remoteAddr;
}
@@ -0,0 +1,24 @@
package com.admin.common.dto;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@Data
public class ForwardUpdateDto {
@NotNull(message = "ID不能为空")
private Long id;
@NotNull(message = "用户ID不能为空")
private Integer userId;
@NotBlank(message = "转发名称不能为空")
private String name;
@NotNull(message = "隧道ID不能为空")
private Integer tunnelId;
@NotBlank(message = "远程地址不能为空")
private String remoteAddr;
}
@@ -0,0 +1,111 @@
package com.admin.common.dto;
import lombok.Data;
/**
* <p>
* 转发信息及关联隧道信息DTO
* </p>
*
* @author QAQ
* @since 2025-06-03
*/
@Data
public class ForwardWithTunnelDto {
/**
* 转发记录ID
*/
private Long id;
/**
* 转发名称
*/
private String name;
/**
* 入口端口
*/
private Integer inPort;
/**
* 远程地址
*/
private String remoteAddr;
/**
* 转发状态
*/
private Integer status;
/**
* 创建时间
*/
private Long createdTime;
/**
* 更新时间
*/
private Long updatedTime;
// 以下为隧道相关字段
/**
* 隧道名称
*/
private String tunnelName;
/**
* 入口IP
*/
private String inIp;
private String userName;
/**
* 用户ID
*/
private Integer userId;
/**
* 隧道ID
*/
private Integer tunnelId;
/**
* 入站流量(字节)
*/
private Long inFlow;
/**
* 出站流量(字节)
*/
private Long outFlow;
// /**
// * 入口端口开始
// */
// private Integer inPortSta;
//
// /**
// * 入口端口结束
// */
// private Integer inPortEnd;
//
// /**
// * 出口IP
// */
// private String outIp;
//
// /**
// * 出口端口开始
// */
// private Integer outIpSta;
//
// /**
// * 出口端口结束
// */
// private Integer outIpEnd;
}
@@ -0,0 +1,10 @@
package com.admin.common.dto;
import lombok.Data;
@Data
public class GostDto {
private Integer code;
private String msg;
}
@@ -0,0 +1,17 @@
package com.admin.common.dto;
import lombok.Data;
import javax.validation.constraints.NotBlank;
@Data
public class LoginDto {
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "密码不能为空")
private String password;
}
@@ -0,0 +1,20 @@
package com.admin.common.dto;
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 NodeDto {
@NotBlank(message = "节点名称不能为空")
private String name;
@NotNull(message = "控制端口不能为空")
@Min(value = 1, message = "端口号必须在1-65535之间")
@Max(value = 65535, message = "端口号必须在1-65535之间")
private Integer port;
}
@@ -0,0 +1,23 @@
package com.admin.common.dto;
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 {
@NotNull(message = "节点ID不能为空")
private Long id;
@NotBlank(message = "节点名称不能为空")
private String name;
@NotNull(message = "控制端口不能为空")
@Min(value = 1, message = "端口号必须在1-65535之间")
@Max(value = 65535, message = "端口号必须在1-65535之间")
private Integer port;
}
@@ -0,0 +1,22 @@
package com.admin.common.dto;
import lombok.Data;
@Data
public class PageDto {
/**
* 当前页码,默认为1
*/
private Long current = 1L;
/**
* 每页显示条数,默认为10
*/
private Long size = 10L;
/**
* 搜索关键字(可选)
*/
private String keyword;
}
@@ -0,0 +1,23 @@
package com.admin.common.dto;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Min;
@Data
public class SpeedLimitDto {
@NotBlank(message = "限速规则名称不能为空")
private String name;
@NotNull(message = "速度限制不能为空")
@Min(value = 1, message = "速度限制必须大于0")
private Integer speed;
@NotNull(message = "隧道ID不能为空")
private Long tunnelId;
@NotBlank(message = "隧道名称不能为空")
private String tunnelName;
}
@@ -0,0 +1,26 @@
package com.admin.common.dto;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Min;
@Data
public class SpeedLimitUpdateDto {
@NotNull(message = "ID不能为空")
private Long id;
@NotBlank(message = "限速规则名称不能为空")
private String name;
@NotNull(message = "速度限制不能为空")
@Min(value = 1, message = "速度限制必须大于0")
private Integer speed;
@NotNull(message = "隧道ID不能为空")
private Long tunnelId;
@NotBlank(message = "隧道名称不能为空")
private String tunnelName;
}
@@ -0,0 +1,57 @@
package com.admin.common.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 系统信息DTO
* 对应Go客户端上报的系统信息结构
*/
@Data
public class SystemInfoDto {
/**
* 主机IP地址
*/
@JsonProperty("host_ip")
private String hostIp;
/**
* 开机时间(秒)
*/
@JsonProperty("uptime")
private Long uptime;
/**
* 接收字节数
*/
@JsonProperty("bytes_received")
private Long bytesReceived;
/**
* 发送字节数
*/
@JsonProperty("bytes_transmitted")
private Long bytesTransmitted;
/**
* CPU使用率(百分比)
*/
@JsonProperty("cpu_usage")
private Double cpuUsage;
/**
* 内存使用率(百分比)
*/
@JsonProperty("memory_usage")
private Double memoryUsage;
/**
* 上报时间戳
*/
private Long timestamp;
public SystemInfoDto() {
this.timestamp = System.currentTimeMillis();
}
}
@@ -0,0 +1,42 @@
package com.admin.common.dto;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Min;
import javax.validation.constraints.Max;
@Data
public class TunnelDto {
@NotBlank(message = "隧道名称不能为空")
private String name;
@NotNull(message = "入口节点不能为空")
private Long inNodeId;
@NotNull(message = "入口端口开始不能为空")
@Min(value = 1, message = "入口端口开始必须大于0")
@Max(value = 65535, message = "入口端口开始不能超过65535")
private Integer inPortSta;
@NotNull(message = "入口端口结束不能为空")
@Min(value = 1, message = "入口端口结束必须大于0")
@Max(value = 65535, message = "入口端口结束不能超过65535")
private Integer inPortEnd;
// 出口节点ID,当type=1时可以为空,会自动设置为入口节点ID
private Long outNodeId;
// 出口端口开始,当type=1时可以为空,会自动设置为入口端口
private Integer outIpSta;
// 出口端口结束,当type=1时可以为空,会自动设置为入口端口
private Integer outIpEnd;
@NotNull(message = "隧道类型不能为空")
private Integer type;
@NotNull(message = "流量计算类型不能为空")
private Integer flow;
}
@@ -0,0 +1,13 @@
package com.admin.common.dto;
import lombok.Data;
@Data
public class TunnelListDto {
private Integer id;
private String name;
}
@@ -0,0 +1,36 @@
package com.admin.common.dto;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Min;
@Data
public class UserDto {
@NotBlank(message = "姓名不能为空")
private String name;
@NotBlank(message = "用户名不能为空")
private String user;
@NotBlank(message = "密码不能为空")
private String pwd;
@NotNull(message = "流量不能为空")
@Min(value = 0, message = "流量不能小于0")
private Long flow;
@NotNull(message = "转发数量不能为空")
@Min(value = 0, message = "转发数量不能小于0")
private Integer num;
@NotNull(message = "过期时间不能为空")
private Long expTime;
@NotNull(message = "流量重置时间不能为空")
private Long flowResetTime;
private Integer status;
}
@@ -0,0 +1,85 @@
package com.admin.common.dto;
import lombok.Data;
import java.util.List;
/**
* 用户套餐信息DTO
*/
@Data
public class UserPackageDto {
/**
* 用户基本信息
*/
private UserInfoDto userInfo;
/**
* 用户隧道权限列表
*/
private List<UserTunnelDetailDto> tunnelPermissions;
/**
* 用户转发列表
*/
private List<UserForwardDetailDto> forwards;
/**
* 用户基本信息
*/
@Data
public static class UserInfoDto {
private Long id;
private String name;
private String user;
private Integer status;
private Long flow; // 总流量配额(GB)
private Long inFlow; // 已用入站流量(字节)
private Long outFlow; // 已用出站流量(字节)
private Integer num; // 转发数量配额
private Long expTime; // 过期时间
private Long flowResetTime; // 流量重置时间
private Long createdTime;
private Long updatedTime;
}
/**
* 用户隧道权限详情
*/
@Data
public static class UserTunnelDetailDto {
private Integer id;
private Integer userId;
private Integer tunnelId;
private String tunnelName;
private Integer tunnelFlow; // 隧道流量计算类型(1-单向,2-双向)
private Long flow; // 隧道流量配额(GB)
private Long inFlow; // 隧道已用入站流量(字节)
private Long outFlow; // 隧道已用出站流量(字节)
private Integer num; // 隧道转发数量配额
private Long flowResetTime; // 流量重置时间
private Long expTime; // 隧道权限过期时间
private Integer speedId;
private String speedLimitName;
private Integer speed;
}
/**
* 用户转发详情
*/
@Data
public static class UserForwardDetailDto {
private Long id;
private String name;
private Integer tunnelId;
private String tunnelName;
private String inIp;
private Integer inPort;
private String remoteAddr;
private Long inFlow; // 转发入站流量(字节)
private Long outFlow; // 转发出站流量(字节)
private Integer status;
private Long createdTime;
}
}
@@ -0,0 +1,40 @@
package com.admin.common.dto;
import lombok.Data;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Min;
@Data
public class UserTunnelDto {
@NotNull(message = "用户ID不能为空")
private Integer userId;
@NotNull(message = "隧道ID不能为空")
private Integer tunnelId;
@NotNull(message = "流量限制不能为空")
@Min(value = 0, message = "流量限制不能小于0")
private Long flow;
@NotNull(message = "转发数量不能为空")
@Min(value = 0, message = "转发数量不能小于0")
private Integer num;
/**
* 流量重置时间(时间戳)
*/
@NotNull(message = "流量重置时间不能为空")
private Long flowResetTime;
/**
* 到期时间(时间戳)
*/
@NotNull(message = "到期时间不能为空")
private Long expTime;
/**
* 限速规则ID(可选,null表示不限速)
*/
private Integer speedId;
}
@@ -0,0 +1,13 @@
package com.admin.common.dto;
import lombok.Data;
import javax.validation.constraints.NotNull;
@Data
public class UserTunnelQueryDto {
@NotNull
private Integer userId;
}
@@ -0,0 +1,37 @@
package com.admin.common.dto;
import lombok.Data;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Min;
@Data
public class UserTunnelUpdateDto {
@NotNull(message = "用户隧道权限ID不能为空")
private Integer id;
@NotNull(message = "流量限制不能为空")
@Min(value = 0, message = "流量限制不能小于0")
private Long flow;
@NotNull(message = "转发数量不能为空")
@Min(value = 0, message = "转发数量不能小于0")
private Integer num;
/**
* 流量重置时间(时间戳)
*/
@NotNull(message = "流量重置时间不能为空")
private Long flowResetTime;
/**
* 到期时间(时间戳)
*/
@NotNull(message = "到期时间不能为空")
private Long expTime;
/**
* 限速规则ID(可选,null表示不限速)
*/
private Integer speedId;
}
@@ -0,0 +1,120 @@
package com.admin.common.dto;
import lombok.Data;
/**
* <p>
* 用户隧道权限及隧道详细信息DTO
* </p>
*
* @author QAQ
* @since 2025-06-03
*/
@Data
public class UserTunnelWithDetailDto {
/**
* 用户隧道权限记录ID
*/
private Integer id;
/**
* 用户ID
*/
private Integer userId;
/**
* 隧道ID
*/
private Integer tunnelId;
/**
* 流量限制
*/
private Integer flow;
/**
* 转发数量限制
*/
private Integer num;
/**
* 流量重置时间(时间戳)
*/
private Long flowResetTime;
/**
* 到期时间(时间戳)
*/
private Long expTime;
/**
* 限速规则ID
*/
private Integer speedId;
/**
* 限速规则名称
*/
private String speedLimitName;
/**
* 限速值
*/
private Integer speed;
/**
* 隧道名称
*/
private String tunnelName;
/**
* 隧道流量计算类型(1-单向,2-双向)
*/
private Integer tunnelFlow;
/**
* 入站流量(字节)
*/
private Long inFlow;
/**
* 出站流量(字节)
*/
private Long outFlow;
// /**
// * 入口IP
// */
// private String inIp;
//
// /**
// * 入口端口开始
// */
// private Integer inPortSta;
//
// /**
// * 入口端口结束
// */
// private Integer inPortEnd;
//
// /**
// * 出口IP
// */
// private String outIp;
//
// /**
// * 出口端口开始
// */
// private Integer outIpSta;
//
// /**
// * 出口端口结束
// */
// private Integer outIpEnd;
//
// /**
// * 隧道类型(1-端口转发,2-隧道转发)
// */
// private Integer type;
}
@@ -0,0 +1,38 @@
package com.admin.common.dto;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Min;
@Data
public class UserUpdateDto {
@NotNull(message = "用户ID不能为空")
private Long id;
@NotBlank(message = "姓名不能为空")
private String name;
@NotBlank(message = "用户名不能为空")
private String user;
private String pwd; // 更新时密码可选
@NotNull(message = "流量不能为空")
@Min(value = 0, message = "流量不能小于0")
private Long flow;
@NotNull(message = "转发数量不能为空")
@Min(value = 0, message = "转发数量不能小于0")
private Integer num;
@NotNull(message = "过期时间不能为空")
private Long expTime;
@NotNull(message = "流量重置时间不能为空")
private Long flowResetTime;
private Integer status;
}
@@ -0,0 +1,40 @@
package com.admin.common.exception;
import com.admin.common.lang.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
//
// 实体校验异常捕获
//@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public R MethodArgumentNotValidException(MethodArgumentNotValidException e) {
BindingResult result = e.getBindingResult();
ObjectError objectError = result.getAllErrors().stream().findFirst().get();
log.error("实体校验异常:----------------{}", objectError.getDefaultMessage());
return R.err(500, objectError.getDefaultMessage());
}
// 未授权异常捕获
@ExceptionHandler(value = UnauthorizedException.class)
public R handleUnauthorizedException(UnauthorizedException e) {
log.error("未授权异常:----------------{}", e.getMessage());
return R.err(401, e.getMessage());
}
@ExceptionHandler(value = Exception.class)
public R Exception(Exception e){
log.error("异常:----------------{}", e.getMessage());
return R.err(-2, "异常错误");
}
}
@@ -0,0 +1,18 @@
package com.admin.common.exception;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.web.client.ResponseErrorHandler;
import java.io.IOException;
public class HttpErrorHandler implements ResponseErrorHandler {
@Override
public boolean hasError(ClientHttpResponse response) throws IOException {
return false;
}
@Override
public void handleError(ClientHttpResponse clientHttpResponse) throws IOException {
}
}
@@ -0,0 +1,15 @@
package com.admin.common.exception;
/**
* 未授权异常类
*/
public class UnauthorizedException extends RuntimeException {
public UnauthorizedException(String message) {
super(message);
}
public UnauthorizedException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -0,0 +1,33 @@
package com.admin.common.interceptor;
import com.admin.common.exception.UnauthorizedException;
import com.admin.common.utils.JwtUtil;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* JWT拦截器,验证用户是否登录
*/
public class JwtInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String token = request.getHeader("Authorization");
if (!StringUtils.hasText(token)) {
throw new UnauthorizedException("未登录或token已过期");
}
if (!JwtUtil.validateToken(token)) {
throw new UnauthorizedException("无效的token或token已过期");
}
return true;
}
}
@@ -0,0 +1,49 @@
package com.admin.common.lang;
import lombok.Data;
@Data
public class R {
private int code = 0;
private String msg = "操作成功";
private long ts = System.currentTimeMillis();
private Object data;
public static R ok(Object data){
R m = new R();
m.setData(data);
return m;
}
public static R ok(){
return new R();
}
public static R err(int code, String msg){
R m = new R();
m.setCode(code);
m.setMsg(msg);
return m;
}
public static R err(String msg){
R m = new R();
m.setCode(-1);
m.setMsg(msg);
return m;
}
public static R err(){
R m = new R();
m.setCode(-1);
m.setMsg("请求失败");
return m;
}
}
@@ -0,0 +1,559 @@
package com.admin.common.task;
import com.admin.mapper.UserMapper;
import com.admin.mapper.ForwardMapper;
import com.admin.mapper.UserTunnelMapper;
import com.admin.mapper.TunnelMapper;
import com.admin.mapper.NodeMapper;
import com.admin.service.UserService;
import com.admin.service.UserTunnelService;
import com.admin.entity.Forward;
import com.admin.entity.Tunnel;
import com.admin.entity.Node;
import com.admin.entity.UserTunnel;
import com.admin.common.dto.GostDto;
import com.admin.common.utils.GostUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Executors;
@Component
@Slf4j
public class DelayQueueManager implements CommandLineRunner {
private final DelayQueue<DelayTask> delayQueue = new DelayQueue<>();
@Autowired
UserMapper userMapper;
@Resource
ForwardMapper forwardMapper;
@Resource
UserTunnelMapper userTunnelMapper;
@Resource
TunnelMapper tunnelMapper;
@Resource
NodeMapper nodeMapper;
/**
* 加入到延时队列中
*
* @param task
*/
public void put(DelayTask task) {
log.info("加入延时任务:{}", task);
delayQueue.put(task);
}
/**
* 取消延时任务
*
* @param task
* @return
*/
public boolean remove(DelayTask task) {
log.info("取消延时任务:{}", task);
return delayQueue.remove(task);
}
/**
* 取消延时任务
*
* @param taskid
* @return
*/
public boolean remove(String taskid) {
return remove(new DelayTask(new TaskBase(taskid), 0));
}
/**
* 取消延时任务
*
* @param taskid
* @return
*/
public boolean remove_a(String taskid) {
return remove(new DelayTask(new TaskBase(taskid), 0));
}
@Override
public void run(String... args) throws Exception {
log.info("初始化延时队列");
Executors.newSingleThreadExecutor().execute(new Thread(this::excuteThread));
// 初始化用户账号到期延时任务
initUserExpirationTasks();
// 初始化用户隧道到期延时任务
initUserTunnelExpirationTasks();
}
/**
* 延时任务执行线程
*/
private void excuteThread() {
while (true) {
try {
DelayTask task = delayQueue.take();
processTask(task);
} catch (InterruptedException e) {
break;
}
}
}
/**
* 内部执行延时任务
*
* @param task
*/
private void processTask(DelayTask task) {
log.info("执行延时任务:{}", task.getData().toString());
TaskBase data = task.getData();
switch (data.getType()){
case "1": // 账号到期延迟任务
handleUserExpiration(data.getData());
break;
case "2": // 隧道到期延迟任务
handleUserTunnelExpiration(data.getData());
break;
default:
log.error("未知延时任务类型:{}", data.getType());
break;
}
}
/**
* 处理用户账号到期
*
* @param userId 用户ID
*/
private void handleUserExpiration(String userId) {
try {
log.info("处理用户账号到期,用户ID:{}", userId);
Long userIdLong = Long.parseLong(userId);
// 获取用户信息
com.admin.entity.User user = userMapper.selectById(userIdLong);
if (user == null) {
log.warn("用户不存在,用户ID:{}", userId);
return;
}
// 检查用户是否确实已过期
if (user.getExpTime() != null && user.getExpTime() > System.currentTimeMillis()) {
log.info("用户未过期,无需处理,用户ID:{},过期时间:{}", userId, user.getExpTime());
return;
}
// 禁用用户账号
user.setStatus(0); // 设置为禁用状态
user.setUpdatedTime(System.currentTimeMillis());
int i = userMapper.updateById(user);
if (i != 0) {
log.info("用户账号已禁用,用户ID:{}", userId);
// 清理用户相关的活跃连接和服务
cleanupUserServices(userIdLong);
} else {
log.error("禁用用户账号失败,用户ID:{}", userId);
}
} catch (Exception e) {
log.error("处理用户账号到期异常,用户ID:{},错误:{}", userId, e.getMessage(), e);
}
}
/**
* 清理用户相关服务
*
* @param userId 用户ID
*/
private void cleanupUserServices(Long userId) {
try {
log.info("暂停用户相关转发服务,用户ID:{}", userId);
// 获取用户的所有转发
QueryWrapper<Forward> forwardQuery = new QueryWrapper<>();
forwardQuery.eq("user_id", userId);
List<Forward> userForwards = forwardMapper.selectList(forwardQuery);
log.info("找到用户转发数量:{},用户ID:{}", userForwards.size(), userId);
for (Forward forward : userForwards) {
try {
// 暂停转发服务
pauseForwardService(forward, userId);
} catch (Exception e) {
log.error("暂停转发服务失败,转发ID:{},用户ID:{},错误:{}", forward.getId(), userId, e.getMessage());
}
}
} catch (Exception e) {
log.error("清理用户服务失败,用户ID:{},错误:{}", userId, e.getMessage(), e);
}
}
/**
* 暂停转发服务
*
* @param forward 转发对象
* @param userId 用户ID
*/
private void pauseForwardService(Forward forward, Long userId) {
try {
Tunnel tunnel = tunnelMapper.selectById(forward.getTunnelId());
if (tunnel == null) {
log.warn("隧道不存在,跳过暂停,转发ID:{},隧道ID:{}", forward.getId(), forward.getTunnelId());
return;
}
Node inNode = nodeMapper.selectById(tunnel.getInNodeId());
if (inNode == null) {
log.warn("入口节点不存在,跳过暂停,转发ID:{},节点ID:{}", forward.getId(), tunnel.getInNodeId());
return;
}
// 获取用户隧道关系
UserTunnel userTunnel = getUserTunnelRelation(userId, tunnel.getId());
if (userTunnel == null) {
log.warn("用户隧道关系不存在,跳过暂停,用户ID:{},隧道ID:{}", userId, tunnel.getId());
return;
}
String serviceName = buildServiceName(forward.getId(), userId, userTunnel.getId());
String nodeAddress = buildNodeAddress(inNode);
// 暂停主服务
GostDto result = GostUtil.PauseService(nodeAddress, serviceName, inNode.getSecret());
// 隧道转发需要同时暂停远端服务
if (tunnel.getType() == 2) { // TUNNEL_TYPE_TUNNEL_FORWARD
Node outNode = nodeMapper.selectById(tunnel.getOutNodeId());
if (outNode != null) {
String outNodeAddress = buildNodeAddress(outNode);
GostDto remoteResult = GostUtil.PauseRemoteService(outNodeAddress, serviceName, outNode.getSecret());
if (!"OK".equals(remoteResult.getMsg())) {
log.warn("暂停远端服务失败,转发ID:{},用户ID:{},服务名:{},结果:{}",
forward.getId(), userId, serviceName, remoteResult.getMsg());
}
}
}
if ( "OK".equals(result.getMsg())) {
forward.setStatus(0);
forwardMapper.updateById(forward);
log.info("成功暂停转发服务,转发ID:{},用户ID:{},服务名:{}", forward.getId(), userId, serviceName);
} else {
log.warn("暂停转发服务失败,转发ID:{},用户ID:{},服务名:{},结果:{}",
forward.getId(), userId, serviceName, result.getMsg());
}
} catch (Exception e) {
log.error("暂停转发服务异常,转发ID:{},用户ID:{},错误:{}", forward.getId(), userId, e.getMessage(), e);
}
}
/**
* 获取用户隧道关系
*
* @param userId 用户ID
* @param tunnelId 隧道ID
* @return 用户隧道关系对象
*/
private UserTunnel getUserTunnelRelation(Long userId, Long tunnelId) {
try {
QueryWrapper<UserTunnel> query = new QueryWrapper<>();
query.eq("user_id", userId).eq("tunnel_id", tunnelId);
return userTunnelMapper.selectOne(query);
} catch (Exception e) {
log.error("获取用户隧道关系失败,用户ID:{},隧道ID:{},错误:{}", userId, tunnelId, e.getMessage());
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 node 节点对象
* @return 节点地址字符串
*/
private String buildNodeAddress(Node node) {
return node.getIp() + ":" + node.getPort();
}
/**
* 初始化用户账号到期延时任务
* 查询所有非管理员的正常用户,为有到期时间且未过期的用户创建延时任务
*/
private void initUserExpirationTasks() {
try {
log.info("开始初始化用户账号到期延时任务");
QueryWrapper<com.admin.entity.User> userQuery = new QueryWrapper<>();
userQuery.ne("role_id", 0) // 排除管理员用户
.eq("status", 1) // 只查询启用状态的用户
.isNotNull("exp_time") // 只查询有到期时间的用户
.orderBy(true, true, "exp_time"); // 按到期时间排序
List<com.admin.entity.User> users = userMapper.selectList(userQuery);
for (com.admin.entity.User user : users) {
scheduleUserExpirationTask(user);
}
} catch (Exception e) {
log.error("初始化用户账号到期延时任务失败:{}", e.getMessage(), e);
}
}
/**
* 安排用户到期延时任务
*
* @param user 用户对象
*/
private void scheduleUserExpirationTask(com.admin.entity.User user) {
try {
if (user.getExpTime() != null && user.getExpTime() > System.currentTimeMillis()) {
// 创建延时任务
TaskBase taskBase = new TaskBase(user.getId().toString());
taskBase.setType("1"); // 账号到期延迟任务
long delayTime = user.getExpTime() - System.currentTimeMillis();
DelayTask delayTask = new DelayTask(taskBase, delayTime);
put(delayTask);
log.debug("已添加用户到期延时任务,用户ID:{},到期时间:{},剩余时间:{}ms",
user.getId(), user.getExpTime(), delayTime);
}
} catch (Exception e) {
log.error("添加用户到期延时任务失败,用户ID:{},错误:{}", user.getId(), e.getMessage(), e);
}
}
/**
* 处理用户隧道到期
*
* @param userTunnelId 用户隧道ID
*/
private void handleUserTunnelExpiration(String userTunnelId) {
try {
log.info("处理用户隧道到期,用户隧道ID:{}", userTunnelId);
Integer userTunnelIdInt = Integer.parseInt(userTunnelId);
// 获取用户隧道信息
UserTunnel userTunnel = userTunnelMapper.selectById(userTunnelIdInt);
if (userTunnel == null) {
log.warn("用户隧道不存在,用户隧道ID:{}", userTunnelId);
return;
}
// 检查用户隧道是否确实已过期
if (userTunnel.getExpTime() != null && userTunnel.getExpTime() > System.currentTimeMillis()) {
log.info("用户隧道未过期,无需处理,用户隧道ID:{},过期时间:{}", userTunnelId, userTunnel.getExpTime());
return;
}
log.info("用户隧道已过期,开始处理,用户隧道ID:{},用户ID:{},隧道ID:{}",
userTunnelId, userTunnel.getUserId(), userTunnel.getTunnelId());
// 暂停该用户在该隧道上的所有转发服务
cleanupUserTunnelServices(userTunnel);
// 禁用过期的用户隧道权限(设置status为0)
userTunnel.setStatus(0);
int updateResult = userTunnelMapper.updateById(userTunnel);
if (updateResult > 0) {
log.info("已禁用过期的用户隧道权限,用户隧道ID:{}", userTunnelId);
} else {
log.error("禁用过期用户隧道权限失败,用户隧道ID:{}", userTunnelId);
}
} catch (Exception e) {
log.error("处理用户隧道到期异常,用户隧道ID:{},错误:{}", userTunnelId, e.getMessage(), e);
}
}
/**
* 清理用户隧道相关服务
*
* @param userTunnel 用户隧道对象
*/
private void cleanupUserTunnelServices(UserTunnel userTunnel) {
try {
log.info("暂停用户隧道相关转发服务,用户ID:{},隧道ID:{}", userTunnel.getUserId(), userTunnel.getTunnelId());
// 获取该用户在该隧道上的所有转发
QueryWrapper<Forward> forwardQuery = new QueryWrapper<>();
forwardQuery.eq("user_id", userTunnel.getUserId())
.eq("tunnel_id", userTunnel.getTunnelId());
List<Forward> userTunnelForwards = forwardMapper.selectList(forwardQuery);
log.info("找到用户隧道转发数量:{},用户ID:{},隧道ID:{}",
userTunnelForwards.size(), userTunnel.getUserId(), userTunnel.getTunnelId());
for (Forward forward : userTunnelForwards) {
try {
// 暂停转发服务
pauseUserTunnelForwardService(forward, userTunnel);
} catch (Exception e) {
log.error("暂停用户隧道转发服务失败,转发ID:{},用户ID:{},隧道ID:{},错误:{}",
forward.getId(), userTunnel.getUserId(), userTunnel.getTunnelId(), e.getMessage());
}
}
} catch (Exception e) {
log.error("清理用户隧道服务失败,用户ID:{},隧道ID:{},错误:{}",
userTunnel.getUserId(), userTunnel.getTunnelId(), e.getMessage(), e);
}
}
/**
* 暂停用户隧道转发服务
*
* @param forward 转发对象
* @param userTunnel 用户隧道对象
*/
private void pauseUserTunnelForwardService(Forward forward, UserTunnel userTunnel) {
try {
Tunnel tunnel = tunnelMapper.selectById(forward.getTunnelId());
if (tunnel == null) {
log.warn("隧道不存在,跳过暂停,转发ID:{},隧道ID:{}", forward.getId(), forward.getTunnelId());
return;
}
Node inNode = nodeMapper.selectById(tunnel.getInNodeId());
if (inNode == null) {
log.warn("入口节点不存在,跳过暂停,转发ID:{},节点ID:{}", forward.getId(), tunnel.getInNodeId());
return;
}
String serviceName = buildServiceName(forward.getId(), Long.valueOf(userTunnel.getUserId()), userTunnel.getId());
String nodeAddress = buildNodeAddress(inNode);
// 暂停服务
GostDto result = GostUtil.PauseService(nodeAddress, serviceName, inNode.getSecret());
// 隧道转发需要同时暂停远端服务
if (tunnel.getType() == 2) { // TUNNEL_TYPE_TUNNEL_FORWARD
Node outNode = nodeMapper.selectById(tunnel.getOutNodeId());
if (outNode != null) {
String outNodeAddress = buildNodeAddress(outNode);
GostDto remoteResult = GostUtil.PauseRemoteService(outNodeAddress, serviceName, outNode.getSecret());
if (!"OK".equals(remoteResult.getMsg())) {
log.warn("暂停远端服务失败,转发ID:{},用户ID:{},隧道ID:{},服务名:{},结果:{}",
forward.getId(), userTunnel.getUserId(), userTunnel.getTunnelId(), serviceName, remoteResult.getMsg());
}
}
}
if ("OK".equals(result.getMsg())) {
forward.setStatus(0);
forwardMapper.updateById(forward);
log.info("成功暂停用户隧道转发服务,转发ID:{},用户ID:{},隧道ID:{},服务名:{}",
forward.getId(), userTunnel.getUserId(), userTunnel.getTunnelId(), serviceName);
} else {
log.warn("暂停用户隧道转发服务失败,转发ID:{},用户ID:{},隧道ID:{},服务名:{},结果:{}",
forward.getId(), userTunnel.getUserId(), userTunnel.getTunnelId(), serviceName, result.getMsg());
}
} catch (Exception e) {
log.error("暂停用户隧道转发服务异常,转发ID:{},用户ID:{},隧道ID:{},错误:{}",
forward.getId(), userTunnel.getUserId(), userTunnel.getTunnelId(), e.getMessage(), e);
}
}
/**
* 初始化用户隧道到期延时任务
* 查询所有有到期时间且未过期的用户隧道权限,为其创建延时任务
*/
private void initUserTunnelExpirationTasks() {
try {
log.info("开始初始化用户隧道到期延时任务");
QueryWrapper<UserTunnel> userTunnelQuery = new QueryWrapper<>();
userTunnelQuery.eq("status", 1); // 按到期时间排序
List<UserTunnel> userTunnels = userTunnelMapper.selectList(userTunnelQuery);
int taskCount = 0;
for (UserTunnel userTunnel : userTunnels) {
scheduleUserTunnelExpirationTask(userTunnel);
}
log.info("完成初始化用户隧道到期延时任务,总计查询:{},添加任务:{}", userTunnels.size(), taskCount);
} catch (Exception e) {
log.error("初始化用户隧道到期延时任务失败:{}", e.getMessage(), e);
}
}
/**
* 安排用户隧道到期延时任务
*
* @param userTunnel 用户隧道对象
*/
private void scheduleUserTunnelExpirationTask(UserTunnel userTunnel) {
// 创建延时任务
TaskBase taskBase = new TaskBase(userTunnel.getId().toString());
taskBase.setType("2"); // 隧道到期延迟任务
long delayTime = userTunnel.getExpTime() - System.currentTimeMillis();
DelayTask delayTask = new DelayTask(taskBase, delayTime);
put(delayTask);
log.debug("已添加用户隧道到期延时任务,用户隧道ID:{},用户ID:{},隧道ID:{},到期时间:{},剩余时间:{}ms",
userTunnel.getId(), userTunnel.getUserId(), userTunnel.getTunnelId(),
userTunnel.getExpTime(), delayTime);
}
/**
* 添加用户隧道到期延时任务(公共方法,供其他服务调用)
*
* @param userTunnel 用户隧道对象
*/
public void addUserTunnelExpirationTask(UserTunnel userTunnel) {
scheduleUserTunnelExpirationTask(userTunnel);
}
/**
* 移除用户隧道到期延时任务(公共方法,供其他服务调用)
*
* @param userTunnelId 用户隧道ID
*/
public void removeUserTunnelExpirationTask(Integer userTunnelId) {
String taskId = userTunnelId.toString();
boolean removed = remove(taskId);
if (removed) {
log.info("已移除用户隧道到期延时任务,用户隧道ID:{}", userTunnelId);
} else {
log.debug("未找到需要移除的用户隧道到期延时任务,用户隧道ID:{}", userTunnelId);
}
}
}
@@ -0,0 +1,57 @@
package com.admin.common.task;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
/**
* 延时任务
*/
public class DelayTask implements Delayed {
//任务参数
final private TaskBase data;
//任务的延时时间,单位毫秒
final private long expire;
/**
* 构造延时任务
*
* @param data 业务数据
* @param expire 任务延时时间(ms)
*/
public DelayTask(TaskBase data, long expire) {
super();
this.data = data;
this.expire = expire + System.currentTimeMillis();
}
public TaskBase getData() {
return data;
}
public long getExpire() {
return expire;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof DelayTask) {
return this.data.getData().equals(((DelayTask) obj).getData().getData());
}
return false;
}
@Override
public String toString() {
return "{" + "data:" + data.toString() + "," + "延时时间:"+expire+"}";
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(this.expire - System.currentTimeMillis(), unit);
}
@Override
public int compareTo(Delayed o) {
long delta = getDelay(TimeUnit.NANOSECONDS) - o.getDelay(TimeUnit.NANOSECONDS);
return (int) delta;
}
}
@@ -0,0 +1,164 @@
package com.admin.common.task;
import com.admin.entity.User;
import com.admin.entity.UserTunnel;
import com.admin.service.UserService;
import com.admin.service.UserTunnelService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import javax.annotation.Resource;
import java.time.LocalDate;
import java.util.List;
@Slf4j
@Configuration
@EnableScheduling
public class ResetFlowAsync {
@Resource
UserService userService;
@Resource
UserTunnelService userTunnelService;
/**
* 每天0点执行流量重置任务
* 查询出用户和隧道的重置流量日期是今天的数据,将上下流量重置为0
* 考虑当月是29、30天,但是选择是31的这种边界情况
*
* 并发安全说明:
* - 使用setSql()进行原子SQL更新,只更新流量字段(in_flow, out_flow)
* - 不会影响DelayQueueManager的到期任务对status等其他字段的更新
* - 避免了并发修改导致的数据覆盖问题
*/
@Scheduled(cron = "0 0 0 * * ?")
public void reset_flow(){
log.info("开始执行流量重置任务");
try {
// 获取当前日期信息
LocalDate today = LocalDate.now();
int currentDay = today.getDayOfMonth(); // 当前是几号
int lastDayOfMonth = today.lengthOfMonth(); // 当月最后一天
log.info("当前日期: {}, 当月第{}天, 当月最后一天: {}", today, currentDay, lastDayOfMonth);
// 重置用户流量
resetUserFlow(currentDay, lastDayOfMonth);
// 重置用户隧道流量
resetUserTunnelFlow(currentDay, lastDayOfMonth);
log.info("流量重置任务执行完成");
} catch (Exception e) {
log.error("流量重置任务执行失败", e);
}
}
/**
* 重置用户流量
* @param currentDay 当前日期(几号)
* @param lastDayOfMonth 当月最后一天
*/
private void resetUserFlow(int currentDay, int lastDayOfMonth) {
try {
// flowResetTime字段存储的是1-31的数字,表示每月第几号重置
// 构建查询条件:重置日期等于今天,或者重置日期大于当月最大天数且今天是月末
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
if (currentDay == lastDayOfMonth) {
// 如果今天是月末,查询重置日期等于今天或者大于当月最大天数的记录
// 例如:当月30天,但用户设置31号重置,则在30号执行重置
queryWrapper.and(wrapper -> wrapper.eq("flow_reset_time", currentDay)
.or().gt("flow_reset_time", lastDayOfMonth));
} else {
// 否则只查询重置日期等于今天的记录
queryWrapper.eq("flow_reset_time", currentDay);
}
// 查询需要重置的用户
List<User> usersToReset = userService.list(queryWrapper);
if (usersToReset.isEmpty()) {
log.info("没有需要重置流量的用户");
return;
}
log.info("找到{}个需要重置流量的用户", usersToReset.size());
// 批量重置用户流量 - 使用SQL原子操作避免与到期任务的并发冲突
for (User user : usersToReset) {
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("id", user.getId())
.setSql("in_flow = 0, out_flow = 0"); // 使用SQL原子操作,只更新流量字段
boolean success = userService.update(null, updateWrapper);
if (success) {
log.info("用户[ID: {}, 用户名: {}]流量重置成功,重置日期: 每月{}号",
user.getId(), user.getUser(), user.getFlowResetTime());
} else {
log.error("用户[ID: {}, 用户名: {}]流量重置失败", user.getId(), user.getUser());
}
}
} catch (Exception e) {
log.error("重置用户流量失败", e);
}
}
/**
* 重置用户隧道流量
* @param currentDay 当前日期(几号)
* @param lastDayOfMonth 当月最后一天
*/
private void resetUserTunnelFlow(int currentDay, int lastDayOfMonth) {
try {
// flowResetTime字段存储的是1-31的数字,表示每月第几号重置
// 构建查询条件:重置日期等于今天,或者重置日期大于当月最大天数且今天是月末
QueryWrapper<UserTunnel> queryWrapper = new QueryWrapper<>();
if (currentDay == lastDayOfMonth) {
// 如果今天是月末,查询重置日期等于今天或者大于当月最大天数的记录
// 例如:当月30天,但用户设置31号重置,则在30号执行重置
queryWrapper.and(wrapper -> wrapper.eq("flow_reset_time", currentDay)
.or().gt("flow_reset_time", lastDayOfMonth));
} else {
// 否则只查询重置日期等于今天的记录
queryWrapper.eq("flow_reset_time", currentDay);
}
// 查询需要重置的用户隧道
List<UserTunnel> userTunnelsToReset = userTunnelService.list(queryWrapper);
if (userTunnelsToReset.isEmpty()) {
log.info("没有需要重置流量的用户隧道");
return;
}
log.info("找到{}个需要重置流量的用户隧道", userTunnelsToReset.size());
// 批量重置用户隧道流量 - 使用SQL原子操作避免与到期任务的并发冲突
for (UserTunnel userTunnel : userTunnelsToReset) {
UpdateWrapper<UserTunnel> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("id", userTunnel.getId())
.setSql("in_flow = 0, out_flow = 0"); // 使用SQL原子操作,只更新流量字段
boolean success = userTunnelService.update(null, updateWrapper);
if (success) {
log.info("用户隧道[ID: {}, 用户ID: {}, 隧道ID: {}]流量重置成功,重置日期: 每月{}号",
userTunnel.getId(), userTunnel.getUserId(), userTunnel.getTunnelId(), userTunnel.getFlowResetTime());
} else {
log.error("用户隧道[ID: {}, 用户ID: {}, 隧道ID: {}]流量重置失败",
userTunnel.getId(), userTunnel.getUserId(), userTunnel.getTunnelId());
}
}
} catch (Exception e) {
log.error("重置用户隧道流量失败", e);
}
}
}
@@ -0,0 +1,14 @@
package com.admin.common.task;
import lombok.Data;
@Data
public class TaskBase {
private String data;
private String type;
public TaskBase(String data) {
this.data = data;
}
}
@@ -0,0 +1,378 @@
package com.admin.common.utils;
import com.admin.common.dto.GostDto;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
public class GostUtil {
private static final String API_BASE_URL = "/api/config/";
private static final String LIMITERS_ENDPOINT = "limiters";
private static final String SERVICES_ENDPOINT = "services";
private static final String CHAINS_ENDPOINT = "chains";
/**
* 添加限流器配置
* @param addr 服务器地址
* @param name 限流器名称
* @param speed 限速值(MB)
* @param secret 认证密钥
* @return 请求结果
*/
public static GostDto AddLimiters(String addr, Long name, String speed, String secret) {
JSONObject data = createLimiterData(name, speed);
String url = buildUrl(addr, LIMITERS_ENDPOINT);
return HttpUtils.post(url, data, secret);
}
/**
* 更新限流器配置
* @param addr 服务器地址
* @param name 限流器名称
* @param speed 限速值(MB)
* @param secret 认证密钥
* @return 请求结果
*/
public static GostDto UpdateLimiters(String addr, Long name, String speed, String secret) {
JSONObject data = createLimiterData(name, speed);
String url = buildUrl(addr, LIMITERS_ENDPOINT + "/" + name);
return HttpUtils.put(url, data, secret);
}
/**
* 删除限流器配置
* @param addr 服务器地址
* @param name 限流器名称
* @param secret 认证密钥
* @return 请求结果
*/
public static GostDto DeleteLimiters(String addr, Long name, String secret) {
String url = buildUrl(addr, LIMITERS_ENDPOINT + "/" + name);
return HttpUtils.delete(url, secret);
}
/**
* 创建限流器数据
*/
private static JSONObject createLimiterData(Long name, String speed) {
JSONObject data = new JSONObject();
data.put("name", name.toString());
JSONArray limits = new JSONArray();
limits.add("$ " + speed + "MB " + speed + "MB");
data.put("limits", limits);
return data;
}
/**
* 添加服务配置(支持端口转发和隧道转发)
* @param addr 服务器地址
* @param name 服务名称
* @param in_port 监听端口
* @param limiter 限流器ID
* @param remoteAddr 远程地址(端口转发时使用)
* @param secret 认证密钥
* @param fow_type 转发类型:1=端口转发,2=隧道转发
* @return 请求结果
*/
public static GostDto AddService(String addr, String name, Integer in_port, Integer limiter, String remoteAddr, String secret, Integer fow_type) {
JSONArray services = new JSONArray();
String[] protocols = {"tcp", "udp"};
for (String protocol : protocols) {
JSONObject service = createServiceConfig(name, in_port, limiter, remoteAddr, protocol, fow_type);
services.add(service);
}
String url = buildUrl(addr, SERVICES_ENDPOINT + "/batch");
return HttpUtils.post(url, services, secret);
}
/**
* 更新服务配置(批量更新TCP和UDP服务)
* @param addr 服务器地址
* @param name 服务名称
* @param in_port 监听端口
* @param limiter 限流器ID
* @param remoteAddr 远程地址(端口转发时使用)
* @param secret 认证密钥
* @param fow_type 转发类型:1=端口转发,2=隧道转发
* @return 请求结果
*/
public static GostDto UpdateService(String addr, String name, Integer in_port, Integer limiter, String remoteAddr, String secret, Integer fow_type) {
JSONArray services = new JSONArray();
String[] protocols = {"tcp", "udp"};
for (String protocol : protocols) {
JSONObject service = createServiceConfig(name, in_port, limiter, remoteAddr, protocol, fow_type);
services.add(service);
}
String url = buildUrl(addr, SERVICES_ENDPOINT + "/batch");
return HttpUtils.put(url, services, secret);
}
/**
* 删除服务配置(批量删除TCP和UDP服务)
* @param addr 服务器地址
* @param name 服务名称
* @param secret 认证密钥
* @return 请求结果
*/
public static GostDto DeleteService(String addr, String name, String secret) {
JSONObject data = new JSONObject();
JSONArray services = new JSONArray();
services.add(name + "_tcp");
services.add(name + "_udp");
data.put("services", services);
String url = buildUrl(addr, SERVICES_ENDPOINT + "/batch");
return HttpUtils.delete(url, data, secret);
}
public static GostDto PauseService(String addr, String name, String secret) {
JSONObject data = new JSONObject();
JSONArray services = new JSONArray();
services.add(name + "_tcp");
services.add(name + "_udp");
data.put("services", services);
String url = buildUrl(addr, SERVICES_ENDPOINT + "/batch/pause");
return HttpUtils.post(url, data, secret);
}
public static GostDto ResumeService(String addr, String name, String secret) {
JSONObject data = new JSONObject();
JSONArray services = new JSONArray();
services.add(name + "_tcp");
services.add(name + "_udp");
data.put("services", services);
String url = buildUrl(addr, SERVICES_ENDPOINT + "/batch/resume");
return HttpUtils.post(url, data, secret);
}
public static GostDto PauseRemoteService(String addr, String name, String secret) {
JSONObject data = new JSONObject();
JSONArray services = new JSONArray();
services.add(name + "_tls");
data.put("services", services);
String url = buildUrl(addr, SERVICES_ENDPOINT + "/batch/pause");
return HttpUtils.post(url, data, secret);
}
public static GostDto ResumeRemoteService(String addr, String name, String secret) {
JSONObject data = new JSONObject();
JSONArray services = new JSONArray();
services.add(name + "_tls");
data.put("services", services);
String url = buildUrl(addr, SERVICES_ENDPOINT + "/batch/resume");
return HttpUtils.post(url, data, secret);
}
public static GostDto AddChains(String addr, String name, String remoteAddr, String secret) {
JSONObject dialer = new JSONObject();
dialer.put("type", "tls");
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);
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);
String url = buildUrl(addr, CHAINS_ENDPOINT);
return HttpUtils.post(url, data, secret);
}
public static GostDto UpdateChains(String addr, String name, String remoteAddr, String secret) {
JSONObject dialer = new JSONObject();
dialer.put("type", "tls");
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);
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);
String url = buildUrl(addr, CHAINS_ENDPOINT + "/" + name + "_chains");
return HttpUtils.put(url, data, secret);
}
public static GostDto DeleteChains(String addr, String name, String secret) {
String url = buildUrl(addr, CHAINS_ENDPOINT + "/" + name + "_chains");
return HttpUtils.delete(url, secret);
}
public static GostDto AddRemoteService(String addr, String name, Integer out_port, String remoteAddr, String secret) {
JSONObject data = new JSONObject();
data.put("name", name + "_tls");
data.put("addr", ":"+out_port);
JSONObject handler = new JSONObject();
handler.put("type", "relay");
data.put("handler", handler);
JSONObject listener = new JSONObject();
listener.put("type", "tls");
data.put("listener", listener);
JSONObject forwarder = new JSONObject();
JSONArray nodes = new JSONArray();
JSONObject node = new JSONObject();
node.put("name", name + "_node");
node.put("addr", remoteAddr);
nodes.add(node);
forwarder.put("nodes", nodes);
data.put("forwarder", forwarder);
String url = buildUrl(addr, SERVICES_ENDPOINT);
return HttpUtils.post(url, data, secret);
}
public static GostDto UpdateRemoteService(String addr, String name, Integer out_port, String remoteAddr, String secret) {
JSONObject data = new JSONObject();
data.put("name", name + "_tls");
data.put("addr", ":"+out_port);
JSONObject handler = new JSONObject();
handler.put("type", "relay");
data.put("handler", handler);
JSONObject listener = new JSONObject();
listener.put("type", "tls");
data.put("listener", listener);
JSONObject forwarder = new JSONObject();
JSONArray nodes = new JSONArray();
JSONObject node = new JSONObject();
node.put("name", name + "_node");
node.put("addr", remoteAddr);
nodes.add(node);
forwarder.put("nodes", nodes);
data.put("forwarder", forwarder);
String url = buildUrl(addr, SERVICES_ENDPOINT + "/" + name + "_tls");
return HttpUtils.put(url, data, secret);
}
public static GostDto DeleteRemoteService(String addr, String name, String secret) {
String url = buildUrl(addr, SERVICES_ENDPOINT + "/" + name + "_tls");
return HttpUtils.delete(url, secret);
}
/**
* 创建单个服务配置
*/
private static JSONObject createServiceConfig(String name, Integer in_port, Integer limiter, String remoteAddr, String protocol, Integer fow_type) {
JSONObject service = new JSONObject();
service.put("name", name + "_" + protocol);
service.put("addr", ":" +in_port);
// 添加限流器配置
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(protocol, remoteAddr);
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);
return listener;
}
/**
* 创建转发器配置
*/
private static JSONObject createForwarder(String protocol, String remoteAddr) {
JSONObject forwarder = new JSONObject();
JSONArray nodes = new JSONArray();
JSONObject node = new JSONObject();
node.put("name", protocol);
node.put("addr", remoteAddr);
nodes.add(node);
forwarder.put("nodes", nodes);
return forwarder;
}
/**
* 判断是否为端口转发
*/
private static boolean isPortForwarding(Integer fow_type) {
return fow_type != null && fow_type == 1;
}
/**
* 判断是否为隧道转发
*/
private static boolean isTunnelForwarding(Integer fow_type) {
return fow_type != null && fow_type != 1;
}
/**
* 构建API URL
*/
private static String buildUrl(String addr, String endpoint) {
return "http://" + addr + API_BASE_URL + endpoint;
}
}
@@ -0,0 +1,14 @@
package com.admin.common.utils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
public class HttpContextUtils {
public static HttpServletRequest getHttpServletRequest(){
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
}
}
@@ -0,0 +1,178 @@
package com.admin.common.utils;
import com.admin.common.dto.GostDto;
import com.admin.config.RestTemplateConfig;
import com.alibaba.fastjson.JSONObject;
import lombok.SneakyThrows;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.*;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestTemplate;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.*;
/**
* HTTP请求工具类
* 支持GET和POST请求,支持表单和JSON格式的请求体
*/
public class HttpUtils {
private static final Logger logger = LoggerFactory.getLogger(HttpUtils.class);
/**
* 自定义错误处理器,不抛出异常
*/
private static class NoOpResponseErrorHandler implements ResponseErrorHandler {
@Override
public boolean hasError(ClientHttpResponse response) throws IOException {
return false;
}
@Override
public void handleError(ClientHttpResponse response) throws IOException {
}
}
@SneakyThrows
public static GostDto post(String url, Object requestBody, String secret){
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
String auth = secret + ":" + secret;
String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8));
headers.set("Authorization", "Basic " + encodedAuth);
HttpEntity<Object> entity = new HttpEntity<>(requestBody, headers);
RestTemplate restTemplate = new RestTemplate(RestTemplateConfig.generateHttpRequestFactory());
restTemplate.setErrorHandler(new NoOpResponseErrorHandler());
try {
ResponseEntity<GostDto> response = restTemplate.postForEntity(url, entity, GostDto.class);
GostDto body = response.getBody();
if (body.getMsg() != null && body.getMsg().contains("exists")) {
body.setMsg("OK");
}
return body;
} catch (Exception e) {
GostDto gostDto = new GostDto();
gostDto.setCode(500);
gostDto.setMsg("请求失败");
return gostDto;
}
}
@SneakyThrows
public static GostDto put(String url, Object requestBody, String secret){
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
String auth = secret + ":" + secret;
String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8));
headers.set("Authorization", "Basic " + encodedAuth);
HttpEntity<Object> entity = new HttpEntity<>(requestBody, headers);
RestTemplate restTemplate = new RestTemplate(RestTemplateConfig.generateHttpRequestFactory());
restTemplate.setErrorHandler(new NoOpResponseErrorHandler());
try {
ResponseEntity<GostDto> response = restTemplate.exchange(
url,
HttpMethod.PUT,
entity,
GostDto.class
);
GostDto body = response.getBody();
return body;
} catch (Exception e) {
GostDto gostDto = new GostDto();
gostDto.setCode(500);
gostDto.setMsg("请求失败");
return gostDto;
}
}
@SneakyThrows
public static GostDto delete(String url, String secret) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
// Basic Auth
String auth = secret + ":" + secret;
String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8));
headers.set("Authorization", "Basic " + encodedAuth);
HttpEntity<Void> entity = new HttpEntity<>(headers);
RestTemplate restTemplate = new RestTemplate(RestTemplateConfig.generateHttpRequestFactory());
restTemplate.setErrorHandler(new NoOpResponseErrorHandler());
try {
ResponseEntity<GostDto> response = restTemplate.exchange(
url,
HttpMethod.DELETE,
entity,
GostDto.class
);
GostDto body = response.getBody();
if (body != null && body.getMsg() != null && body.getMsg().contains("not found")) {
body.setMsg("OK");
}
return body;
} catch (Exception e) {
GostDto gostDto = new GostDto();
gostDto.setCode(500);
gostDto.setMsg("请求失败");
return gostDto;
}
}
@SneakyThrows
public static GostDto delete(String url, JSONObject data, String secret) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
// Basic Auth
String auth = secret + ":" + secret;
String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8));
headers.set("Authorization", "Basic " + encodedAuth);
HttpEntity<JSONObject> entity = new HttpEntity<>(data, headers);
RestTemplate restTemplate = new RestTemplate(RestTemplateConfig.generateHttpRequestFactory());
restTemplate.setErrorHandler(new NoOpResponseErrorHandler());
try {
ResponseEntity<GostDto> response = restTemplate.exchange(
url,
HttpMethod.DELETE,
entity,
GostDto.class
);
GostDto body = response.getBody();
if (body != null && body.getMsg() != null && body.getMsg().contains("not found")) {
body.setMsg("OK");
}
return body;
} catch (Exception e) {
GostDto gostDto = new GostDto();
gostDto.setCode(500);
gostDto.setMsg("请求失败");
return gostDto;
}
}
}
@@ -0,0 +1,47 @@
package com.admin.common.utils;
import javax.servlet.http.HttpServletRequest;
import java.net.InetAddress;
import java.net.UnknownHostException;
public class IpUtils {
public static String getIpAddr(HttpServletRequest request) {
String ipAddress = null;
try {
ipAddress = request.getHeader("x-forwarded-for");
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
if (ipAddress.equals("127.0.0.1")) {
// 根据网卡取本机配置的IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
e.printStackTrace();
}
ipAddress = inet.getHostAddress();
}
}
// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
if (ipAddress != null && ipAddress.length() > 15) {
// "***.***.***.***".length()
// = 15
if (ipAddress.indexOf(",") > 0) {
ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
}
}
} catch (Exception e) {
ipAddress="";
}
return ipAddress;
}
}
@@ -0,0 +1,194 @@
package com.admin.common.utils;
import com.admin.entity.User;
import com.alibaba.fastjson2.JSON;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* JWT工具类,不使用第三方库实现
*/
@Component
public class JwtUtil {
@Value("${jwt-secret}")
private String secretKey;
private static String SECRET_KEY;
// token有效期,7天
private static final long EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000;
// 算法
private static final String ALGORITHM = "HmacSHA256";
@PostConstruct
public void init() {
SECRET_KEY = this.secretKey;
}
/**
* 生成JWT Token
*
* @param user 用户信息
* @return 生成的JWT Token
*/
public static String generateToken(User user) {
try {
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
Date expireDate = new Date(nowMillis + EXPIRE_TIME);
// Header
Map<String, Object> header = new HashMap<>();
header.put("alg", ALGORITHM);
header.put("typ", "JWT");
String headerJson = JSON.toJSONString(header);
String encodedHeader = Base64.getUrlEncoder().withoutPadding()
.encodeToString(headerJson.getBytes(StandardCharsets.UTF_8));
// Payload
Map<String, Object> payload = new HashMap<>();
payload.put("sub", user.getId().toString());
payload.put("iat", now.getTime() / 1000); // 发布时间
payload.put("exp", expireDate.getTime() / 1000); // 过期时间
payload.put("user", user.getUser());
payload.put("name", user.getName());
payload.put("role_id", user.getRoleId());
String payloadJson = JSON.toJSONString(payload);
String encodedPayload = Base64.getUrlEncoder().withoutPadding()
.encodeToString(payloadJson.getBytes(StandardCharsets.UTF_8));
// Signature
String signature = calculateSignature(encodedHeader, encodedPayload);
// Token
return encodedHeader + "." + encodedPayload + "." + signature;
} catch (Exception e) {
throw new RuntimeException("JWT token generation failed", e);
}
}
/**
* 验证JWT Token
*
* @param token JWT Token
* @return 验证是否通过
*/
public static boolean validateToken(String token) {
try {
if (token == null || token.isEmpty()) {
return false;
}
String[] parts = token.split("\\.");
if (parts.length != 3) {
return false;
}
String encodedHeader = parts[0];
String encodedPayload = parts[1];
String signature = parts[2];
// 验证签名
String expectedSignature = calculateSignature(encodedHeader, encodedPayload);
if (!expectedSignature.equals(signature)) {
return false;
}
// 验证过期时间
String decodedPayload = new String(Base64.getUrlDecoder().decode(encodedPayload), StandardCharsets.UTF_8);
Map<String, Object> payload = JSON.parseObject(decodedPayload, Map.class);
long exp = Long.parseLong(payload.get("exp").toString());
long now = System.currentTimeMillis() / 1000;
return exp > now;
} catch (Exception e) {
return false;
}
}
/**
* 从JWT Token中获取用户ID
*
* @param token JWT Token
* @return 用户ID
*/
public static Long getUserIdFromToken(String token) {
String[] parts = token.split("\\.");
String encodedPayload = parts[1];
String decodedPayload = new String(Base64.getUrlDecoder().decode(encodedPayload), StandardCharsets.UTF_8);
Map<String, Object> payload = JSON.parseObject(decodedPayload, Map.class);
return Long.parseLong(payload.get("sub").toString());
}
public static Integer getUserIdFromToken() {
String token = HttpContextUtils.getHttpServletRequest().getHeader("Authorization");
String[] parts = token.split("\\.");
String encodedPayload = parts[1];
String decodedPayload = new String(Base64.getUrlDecoder().decode(encodedPayload), StandardCharsets.UTF_8);
Map<String, Object> payload = JSON.parseObject(decodedPayload, Map.class);
return Integer.parseInt(payload.get("sub").toString());
}
public static String getNameFromToken() {
String token = HttpContextUtils.getHttpServletRequest().getHeader("Authorization");
String[] parts = token.split("\\.");
String encodedPayload = parts[1];
String decodedPayload = new String(Base64.getUrlDecoder().decode(encodedPayload), StandardCharsets.UTF_8);
Map<String, Object> payload = JSON.parseObject(decodedPayload, Map.class);
return payload.get("name").toString();
}
/**
* 从JWT Token中获取用户角色ID
*
* @param token JWT Token
* @return 角色ID
*/
public static Integer getRoleIdFromToken(String token) {
String[] parts = token.split("\\.");
String encodedPayload = parts[1];
String decodedPayload = new String(Base64.getUrlDecoder().decode(encodedPayload), StandardCharsets.UTF_8);
Map<String, Object> payload = JSON.parseObject(decodedPayload, Map.class);
return Integer.parseInt(payload.get("role_id").toString());
}
@SneakyThrows
public static Integer getRoleIdFromToken() {
String token = HttpContextUtils.getHttpServletRequest().getHeader("Authorization");
if (token == null || token.isEmpty()) throw new Exception();
String[] parts = token.split("\\.");
String encodedPayload = parts[1];
String decodedPayload = new String(Base64.getUrlDecoder().decode(encodedPayload), StandardCharsets.UTF_8);
Map<String, Object> payload = JSON.parseObject(decodedPayload, Map.class);
return Integer.parseInt(payload.get("role_id").toString());
}
/**
* 计算签名
*
* @param encodedHeader 编码后的头部
* @param encodedPayload 编码后的负载
* @return 签名
* @throws Exception 签名计算异常
*/
private static String calculateSignature(String encodedHeader, String encodedPayload) throws Exception {
String content = encodedHeader + "." + encodedPayload;
Mac hmac = Mac.getInstance(ALGORITHM);
SecretKeySpec secretKeySpec = new SecretKeySpec(SECRET_KEY.getBytes(StandardCharsets.UTF_8), ALGORITHM);
hmac.init(secretKeySpec);
byte[] signatureBytes = hmac.doFinal(content.getBytes(StandardCharsets.UTF_8));
return Base64.getUrlEncoder().withoutPadding().encodeToString(signatureBytes);
}
}
@@ -0,0 +1,172 @@
package com.admin.common.utils;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Base64;
/**
* MD5工具类
*/
public class Md5Util {
private static final String MD5_ALGORITHM = "MD5";
private static final String DEFAULT_SALT = "admin_salt_2024";
/**
* 基础MD5加密
*
* @param input 待加密字符串
* @return MD5加密后的字符串(32位小写)
*/
public static String md5(String input) {
if (input == null || input.isEmpty()) {
return null;
}
try {
MessageDigest md = MessageDigest.getInstance(MD5_ALGORITHM);
byte[] digest = md.digest(input.getBytes());
return bytesToHex(digest);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("MD5算法不可用", e);
}
}
/**
* MD5加密(使用默认盐值)
*
* @param input 待加密字符串
* @return MD5加密后的字符串
*/
public static String md5WithSalt(String input) {
return md5WithSalt(input, DEFAULT_SALT);
}
/**
* MD5加密(使用自定义盐值)
*
* @param input 待加密字符串
* @param salt 盐值
* @return MD5加密后的字符串
*/
public static String md5WithSalt(String input, String salt) {
if (input == null || input.isEmpty()) {
return null;
}
if (salt == null) {
salt = DEFAULT_SALT;
}
return md5(input + salt);
}
/**
* 生成随机盐值
*
* @param length 盐值长度
* @return 随机盐值
*/
public static String generateSalt(int length) {
SecureRandom random = new SecureRandom();
byte[] salt = new byte[length];
random.nextBytes(salt);
return Base64.getEncoder().encodeToString(salt);
}
/**
* 生成默认长度(16字节)的随机盐值
*
* @return 随机盐值
*/
public static String generateSalt() {
return generateSalt(16);
}
/**
* 验证密码
*
* @param password 原始密码
* @param hashedPassword 已加密的密码
* @return 是否匹配
*/
public static boolean verify(String password, String hashedPassword) {
if (password == null || hashedPassword == null) {
return false;
}
String encrypted = md5WithSalt(password);
return encrypted.equals(hashedPassword);
}
/**
* 验证密码(使用自定义盐值)
*
* @param password 原始密码
* @param salt 盐值
* @param hashedPassword 已加密的密码
* @return 是否匹配
*/
public static boolean verify(String password, String salt, String hashedPassword) {
if (password == null || hashedPassword == null) {
return false;
}
String encrypted = md5WithSalt(password, salt);
return encrypted.equals(hashedPassword);
}
/**
* 多次MD5加密
*
* @param input 待加密字符串
* @param times 加密次数
* @return 加密后的字符串
*/
public static String md5Multiple(String input, int times) {
if (input == null || input.isEmpty() || times <= 0) {
return input;
}
String result = input;
for (int i = 0; i < times; i++) {
result = md5(result);
}
return result;
}
/**
* 字节数组转十六进制字符串
*
* @param bytes 字节数组
* @return 十六进制字符串
*/
private static String bytesToHex(byte[] bytes) {
StringBuilder result = new StringBuilder();
for (byte b : bytes) {
result.append(String.format("%02x", b));
}
return result.toString();
}
/**
* 获取文件的MD5值
*
* @param bytes 文件字节数组
* @return MD5值
*/
public static String getFileMd5(byte[] bytes) {
if (bytes == null || bytes.length == 0) {
return null;
}
try {
MessageDigest md = MessageDigest.getInstance(MD5_ALGORITHM);
byte[] digest = md.digest(bytes);
return bytesToHex(digest);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("MD5算法不可用", e);
}
}
}
@@ -0,0 +1,616 @@
package com.admin.common.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@Component
public class RedisUtil {
@Autowired
private RedisTemplate redisTemplate;
/**
* 指定缓存失效时间
*
* @param key 键
* @param time 时间(秒)
* @return
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
*
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
*
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
//============================String=============================
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
*
* @param key 键
* @param delta 要增加几(大于0)
* @return
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
*
* @param key 键
* @param delta 要减少几(小于0)
* @return
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
//================================Map=================================
/**
* HashGet
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return 值
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
*
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
*
* @param key 键
* @param map 对应多个键值
* @return true 成功 false 失败
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
*
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除hash表中的值
*
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
*
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
* @return
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
*
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
* @return
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
//============================set=============================
/**
* 根据key获取Set中的所有值
*
* @param key 键
* @return
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
*
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
*
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
*
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0) expire(key, time);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
*
* @param key 键
* @return
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
*
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
//===============================list=================================
/**
* 获取list缓存的内容
*
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
* @return
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
*
* @param key 键
* @return
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
*
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
* @return
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0) expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0) expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
*
* @param key 键
* @param index 索引
* @param value 值
* @return
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
*
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
//================有序集合 sort set===================
/**
* 有序set添加元素
*
* @param key
* @param value
* @param score
* @return
*/
public boolean zSet(String key, Object value, double score) {
return redisTemplate.opsForZSet().add(key, value, score);
}
public long batchZSet(String key, Set<ZSetOperations.TypedTuple> typles) {
return redisTemplate.opsForZSet().add(key, typles);
}
public void zIncrementScore(String key, Object value, long delta) {
redisTemplate.opsForZSet().incrementScore(key, value, delta);
}
public void zUnionAndStore(String key, Collection otherKeys, String destKey) {
redisTemplate.opsForZSet().unionAndStore(key, otherKeys, destKey);
}
/**
* 获取zset数量
* @param key
* @param value
* @return
*/
public long getZsetScore(String key, Object value) {
Double score = redisTemplate.opsForZSet().score(key, value);
if(score==null){
return 0;
}else{
return score.longValue();
}
}
/**
* 获取有序集 key 中成员 member 的排名 。
* 其中有序集成员按 score 值递减 (从大到小) 排序。
* @param key
* @param start
* @param end
* @return
*/
public Set<ZSetOperations.TypedTuple> getZSetRank(String key, long start, long end) {
return redisTemplate.opsForZSet().reverseRangeWithScores(key, start, end);
}
}
@@ -0,0 +1,160 @@
package com.admin.common.utils;
import com.admin.entity.Node;
import com.admin.service.NodeService;
import com.alibaba.fastjson.JSONObject;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import javax.annotation.Resource;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
public class WebSocketServer extends TextWebSocketHandler {
@Resource
NodeService nodeService;
// 存储所有活跃的 WebSocket 连接
private static final CopyOnWriteArraySet<WebSocketSession> activeSessions = new CopyOnWriteArraySet<>();
// 为每个session提供锁对象,防止并发发送消息
private static final ConcurrentHashMap<String, Object> sessionLocks = new ConcurrentHashMap<>();
//接受客户端消息
@Override
public void handleTextMessage(WebSocketSession session, TextMessage message) {
try {
if (StringUtils.isNoneBlank(message.getPayload())) {
//log.info("收到消息: {}", message.getPayload());
String id = session.getAttributes().get("id").toString();
String type = session.getAttributes().get("type").toString();
// 先发送确认消息
sendToUser(session, "ok");
// 如果是节点类型,转发消息给其他会话
if (Objects.equals(type, "1")) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("id", id);
jsonObject.put("type", "info");
jsonObject.put("data", message.getPayload());
String broadcastMessage = jsonObject.toJSONString();
// 异步处理广播消息,避免阻塞当前线程
for (WebSocketSession targetSession : activeSessions) {
if (targetSession != null && targetSession.isOpen() && !targetSession.equals(session)) {
sendToUser(targetSession, broadcastMessage);
}
}
}
}
} catch (Exception e) {
log.error("处理WebSocket消息时发生异常: {}", e.getMessage(), e);
}
}
// 建立连接
@Override
public void afterConnectionEstablished(WebSocketSession session) {
try {
String id = session.getAttributes().get("id").toString();
String type = session.getAttributes().get("type").toString();
if (!Objects.equals(type, "1")) {
activeSessions.add(session);
}else {
Node byId = nodeService.getById(id);
if (byId != null) {
byId.setStatus(1);
nodeService.updateById(byId);
JSONObject res = new JSONObject();
res.put("id", id);
res.put("type", "status");
res.put("data", 1);
broadcastMessage(res.toJSONString());
}
}
log.info("WebSocket 连接建立成功 - id: {}, type: {}, 当前连接数: {}", id, type, activeSessions.size());
} catch (Exception e) {
log.error("建立连接时发生异常: {}", e.getMessage(), e);
}
}
// 连接关闭后
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
try {
String id = session.getAttributes().get("id").toString();
String type = session.getAttributes().get("type").toString();
String sessionId = session.getId();
if (!Objects.equals(type, "1")) {
activeSessions.remove(session);
}else {
Node byId = nodeService.getById(id);
if (byId != null) {
byId.setStatus(0);
nodeService.updateById(byId);
JSONObject res = new JSONObject();
res.put("id", id);
res.put("type", "status");
res.put("data", 0);
broadcastMessage(res.toJSONString());
}
}
// 清理session锁对象
sessionLocks.remove(sessionId);
log.info("WebSocket 连接关闭 - id: {}, sessionId: {}, 关闭状态: {}, 当前连接数: {}",
id, sessionId, status, activeSessions.size());
} catch (Exception e) {
log.error("关闭连接时发生异常: {}", e.getMessage(), e);
}
}
// 点对点发送消息
@SneakyThrows
public static void sendToUser(WebSocketSession socketSession, String message) {
if (socketSession != null && socketSession.isOpen()) {
String sessionId = socketSession.getId();
Object lock = sessionLocks.computeIfAbsent(sessionId, k -> new Object());
synchronized (lock) {
try {
if (socketSession.isOpen()) {
socketSession.sendMessage(new TextMessage(message));
}
} catch (Exception e) {
log.error("发送WebSocket消息失败 [sessionId={}]: {}", sessionId, e.getMessage());
activeSessions.remove(socketSession);
sessionLocks.remove(sessionId);
}
}
} else {
activeSessions.remove(socketSession);
if (socketSession != null) {
sessionLocks.remove(socketSession.getId());
}
}
}
// 广播消息
public static void broadcastMessage(String message) {
for (WebSocketSession session : activeSessions) {
sendToUser(session, message);
}
}
}
@@ -0,0 +1,27 @@
package com.admin.config;
import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@MapperScan("com.admin.mapper")
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); // 分页插件
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); // 防止全表更新插件
return interceptor;
}
@Bean
public ConfigurationCustomizer configurationCustomizer() {
return configuration -> configuration.setUseDeprecatedExecutor(false);
}
}
@@ -0,0 +1,34 @@
package com.admin.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
// 序列化redis
@Bean
RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
jackson2JsonRedisSerializer.setObjectMapper(new ObjectMapper());
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
return redisTemplate;
}
}
@@ -0,0 +1,68 @@
package com.admin.config;
import org.apache.http.HttpHost;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.ssl.TrustStrategy;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import javax.net.ssl.SSLContext;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(ClientHttpRequestFactory factory){
return new RestTemplate(factory);
}
@Bean
public ClientHttpRequestFactory simpleClientHttpRequestFactory(){
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setConnectTimeout(15000);
factory.setReadTimeout(5000);
return factory;
}
// 禁用ssl证书校验
public static HttpComponentsClientHttpRequestFactory generateHttpRequestFactory() throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException{
TrustStrategy acceptingTrustStrategy = (x509Certificates, authType) -> true;
SSLContext sslContext = SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy).build();
SSLConnectionSocketFactory connectionSocketFactory = new SSLConnectionSocketFactory(sslContext, new NoopHostnameVerifier());
HttpClientBuilder httpClientBuilder = HttpClients.custom();
httpClientBuilder.setSSLSocketFactory(connectionSocketFactory);
CloseableHttpClient httpClient = httpClientBuilder.build();
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setHttpClient(httpClient);
return factory;
}
// 禁用ssl证书校验 添加代理ip post请求
public static HttpComponentsClientHttpRequestFactory post(String url, Integer port) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException{
TrustStrategy acceptingTrustStrategy = (x509Certificates, authType) -> true;
SSLContext sslContext = SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy).build();
SSLConnectionSocketFactory connectionSocketFactory = new SSLConnectionSocketFactory(sslContext, new NoopHostnameVerifier());
HttpClientBuilder httpClientBuilder = HttpClients.custom();
httpClientBuilder.setProxy(new HttpHost(url, port, "http"));
httpClientBuilder.setSSLSocketFactory(connectionSocketFactory);
CloseableHttpClient httpClient = httpClientBuilder.build();
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setHttpClient(httpClient);
return factory;
}
}
@@ -0,0 +1,62 @@
package com.admin.config;
import com.admin.common.interceptor.JwtInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {
private CorsConfiguration buildConfig() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.addExposedHeader("Authorization");
return corsConfiguration;
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", buildConfig());
return new CorsFilter(source);
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "DELETE", "PUT")
.maxAge(3600);
}
/**
* JWT拦截器
*/
@Bean
public JwtInterceptor jwtInterceptor() {
return new JwtInterceptor();
}
/**
* 添加JWT拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 添加JWT拦截器,不拦截登录接口
registry.addInterceptor(jwtInterceptor())
.addPathPatterns("/api/**")
.excludePathPatterns("/flow/**")
.excludePathPatterns("/api/v1/user/login");
}
}
@@ -0,0 +1,36 @@
package com.admin.config;
import com.admin.common.utils.WebSocketServer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import javax.annotation.Resource;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Resource
private WebSocketInterceptor webSocketInterceptor;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
webSocketHandlerRegistry
.addHandler(myHandler(), "/system-info")
.setAllowedOrigins("*")
.addInterceptors(webSocketInterceptor);
}
@Bean
public WebSocketHandler myHandler() {
return new WebSocketServer();
}
}
@@ -0,0 +1,63 @@
package com.admin.config;
import com.admin.common.utils.JwtUtil;
import com.admin.common.utils.RedisUtil;
import com.admin.common.utils.WebSocketServer;
import com.admin.entity.Node;
import com.admin.service.NodeService;
import com.admin.service.UserService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@Configuration
@Slf4j
public class WebSocketInterceptor extends HttpSessionHandshakeInterceptor {
@Resource
NodeService nodeService;
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception ex) {
}
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
ServletServerHttpRequest serverHttpRequest = (ServletServerHttpRequest) request;
String secret = serverHttpRequest.getServletRequest().getParameter("secret");
String type = serverHttpRequest.getServletRequest().getParameter("type");
if (Objects.equals(type, "1")) {
String client_ip = serverHttpRequest.getServletRequest().getParameter("client_ip");
Node node = nodeService.getOne(new QueryWrapper<Node>().eq("secret", secret));
if (node == null) return false;
attributes.put("id", node.getId());
node.setStatus(1);
if (!Objects.equals(node.getIp(), client_ip)){
node.setIp(client_ip);
}
nodeService.updateById(node);
}else {
boolean b = JwtUtil.validateToken(secret);
if (!b) return false;
attributes.put("id", JwtUtil.getUserIdFromToken(secret));
}
attributes.put("type", type);
return true;
}
}
@@ -0,0 +1,26 @@
package com.admin.controller;
import com.admin.service.*;
import org.springframework.beans.factory.annotation.Autowired;
public class BaseController {
@Autowired
UserService userService;
@Autowired
NodeService nodeService;
@Autowired
UserTunnelService userTunnelService;
@Autowired
TunnelService tunnelService;
@Autowired
ForwardService forwardService;
@Autowired
SpeedLimitService speedLimitService;
}
@@ -0,0 +1,349 @@
package com.admin.controller;
import com.admin.common.aop.LogAnnotation;
import com.admin.common.dto.FlowDto;
import com.admin.common.lang.R;
import com.admin.common.utils.GostUtil;
import com.admin.entity.*;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* 流量上报控制器
* 处理节点上报的流量数据,更新用户和隧道的流量统计
*
* 并发安全解决方案:
* 1. 使用UpdateWrapper进行数据库层面的原子更新操作,避免读取-修改-写入的竞态条件
* 2. 使用synchronized锁确保同一用户/隧道的流量更新串行执行
* 3. 这样可以避免相同用户相同隧道不同转发同时上报时流量统计丢失的问题
*/
@RestController
@RequestMapping("/flow")
@CrossOrigin
public class FlowController extends BaseController {
// 常量定义
private static final String SUCCESS_RESPONSE = "ok";
private static final String ERROR_RESPONSE = "err1";
private static final String DEFAULT_USER_TUNNEL_ID = "0";
private static final int FLOW_TYPE_UPLOAD_ONLY = 1;
private static final int FLOW_TYPE_BIDIRECTIONAL = 2;
private static final long BYTES_TO_GB = 1024L * 1024L * 1024L;
// 用于同步相同用户和隧道的流量更新操作
private static final ConcurrentHashMap<String, Object> USER_LOCKS = new ConcurrentHashMap<>();
private static final ConcurrentHashMap<String, Object> TUNNEL_LOCKS = new ConcurrentHashMap<>();
@RequestMapping("/test")
@LogAnnotation
public String test() {
return "test";
}
/**
* 处理流量数据上报
* @param flowDataList 流量数据列表
* @param secret 节点密钥
* @return 处理结果
*/
@RequestMapping("/upload")
@LogAnnotation
public String uploadFlowData(@RequestBody List<FlowDto> flowDataList, String secret) {
// 1. 验证节点权限
if (!isValidNode(secret)) {
return ERROR_RESPONSE;
}
// 2. 过滤有效流量数据
List<FlowDto> validFlowData = filterValidFlowData(flowDataList);
if (validFlowData.isEmpty()) {
return SUCCESS_RESPONSE;
}
// 3. 解析服务名称获取ID信息
String[] serviceIds = parseServiceName(validFlowData.get(0).getN());
String forwardId = serviceIds[0];
String userId = serviceIds[1];
String userTunnelId = serviceIds[2];
// 4. 计算总流量
FlowStatistics flowStats = calculateTotalFlow(validFlowData);
// 5. 获取流量计费类型
int flowType = getFlowType(forwardId);
// 6. 更新各项流量统计
updateForwardFlow(forwardId, flowStats);
updateUserFlow(userId, flowStats, flowType);
updateUserTunnelFlow(userTunnelId, flowStats, flowType, forwardId, userId);
// 7. 检查用户总流量限制
checkUserTotalFlowLimit(userId, userTunnelId);
return SUCCESS_RESPONSE;
}
/**
* 验证节点是否有效
*/
private boolean isValidNode(String secret) {
int nodeCount = nodeService.count(new QueryWrapper<Node>().eq("secret", secret));
return nodeCount > 0;
}
/**
* 过滤有效的流量数据
*/
private List<FlowDto> filterValidFlowData(List<FlowDto> flowDataList) {
return flowDataList.stream()
.filter(flow -> flow.getU() != null && flow.getD() != null)
.filter(flow -> flow.getU() > 0 && flow.getD() > 0)
.collect(Collectors.toList());
}
/**
* 解析服务名称获取ID信息
*/
private String[] parseServiceName(String serviceName) {
return serviceName.split("_");
}
/**
* 计算总流量统计
*/
private FlowStatistics calculateTotalFlow(List<FlowDto> validFlowData) {
long totalUpload = 0L;
long totalDownload = 0L;
for (FlowDto flow : validFlowData) {
totalUpload += flow.getU();
totalDownload += flow.getD();
}
return new FlowStatistics(totalUpload, totalDownload);
}
/**
* 获取流量计费类型
*/
private int getFlowType(String forwardId) {
int defaultFlowType = FLOW_TYPE_BIDIRECTIONAL;
Forward forward = forwardService.getById(forwardId);
if (forward != null) {
Tunnel tunnel = tunnelService.getById(forward.getTunnelId());
if (tunnel != null) {
return tunnel.getFlow();
}
}
return defaultFlowType;
}
/**
* 更新转发流量统计 - 使用原子操作避免并发问题
*/
private void updateForwardFlow(String forwardId, FlowStatistics flowStats) {
UpdateWrapper<Forward> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("id", forwardId);
updateWrapper.setSql("in_flow = in_flow + " + flowStats.getDownload());
updateWrapper.setSql("out_flow = out_flow + " + flowStats.getUpload());
forwardService.update(null, updateWrapper);
}
/**
* 更新用户流量统计 - 使用原子操作避免并发问题
*/
private void updateUserFlow(String userId, FlowStatistics flowStats, int flowType) {
// 对相同用户的流量更新进行同步,避免并发覆盖
synchronized (getUserLock(userId)) {
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("id", userId);
// 使用SQL的原子更新操作,避免读取-修改-写入的并发问题
if (flowType == FLOW_TYPE_BIDIRECTIONAL) {
// 双向计费:同时更新上传和下载流量
updateWrapper.setSql("in_flow = in_flow + " + flowStats.getDownload());
updateWrapper.setSql("out_flow = out_flow + " + flowStats.getUpload());
} else {
// 仅上传计费:只更新上传流量
updateWrapper.setSql("out_flow = out_flow + " + flowStats.getUpload());
}
userService.update(null, updateWrapper);
}
}
/**
* 更新用户隧道流量统计并检查限制
*/
private void updateUserTunnelFlow(String userTunnelId, FlowStatistics flowStats,
int flowType, String forwardId, String userId) {
if (Objects.equals(userTunnelId, DEFAULT_USER_TUNNEL_ID)) {
return;
}
// 对相同用户隧道的流量更新进行同步,避免并发覆盖
synchronized (getTunnelLock(userTunnelId)) {
UpdateWrapper<UserTunnel> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("id", userTunnelId);
updateWrapper.setSql("in_flow = in_flow + " + flowStats.getDownload());
updateWrapper.setSql("out_flow = out_flow + " + flowStats.getUpload());
boolean updateSuccess = userTunnelService.update(null, updateWrapper);
if (!updateSuccess) {
return; // 更新失败,可能记录不存在
}
}
// 重新获取最新的流量数据进行限制检查
UserTunnel userTunnel = userTunnelService.getById(userTunnelId);
if (userTunnel != null) {
checkUserTunnelFlowLimit(userTunnel, flowType, forwardId, userId, userTunnelId);
}
}
/**
* 检查用户隧道流量限制
*/
private void checkUserTunnelFlowLimit(UserTunnel userTunnel, int flowType,
String forwardId, String userId, String userTunnelId) {
long currentFlow = (flowType == FLOW_TYPE_UPLOAD_ONLY) ?
userTunnel.getOutFlow() :
userTunnel.getInFlow() + userTunnel.getOutFlow();
long flowLimit = userTunnel.getFlow() * BYTES_TO_GB;
if (flowLimit < currentFlow) {
pauseServiceDueToTunnelLimit(userTunnel.getTunnelId(), forwardId, userId, userTunnelId);
}
}
/**
* 因隧道流量超限暂停服务
*/
private void pauseServiceDueToTunnelLimit(Integer tunnelId, String forwardId,
String userId, String userTunnelId) {
Tunnel tunnel = tunnelService.getById(tunnelId);
if (tunnel != null) {
Node node = nodeService.getNodeById(tunnel.getInNodeId());
if (node != null) {
String serviceName = buildServiceName(forwardId, userId, userTunnelId);
GostUtil.PauseService(node.getIp() + ":" + node.getPort(), serviceName, node.getSecret());
// 隧道转发需要同时暂停远端服务
if (tunnel.getType() == 2) { // TUNNEL_TYPE_TUNNEL_FORWARD
Node outNode = nodeService.getNodeById(tunnel.getOutNodeId());
if (outNode != null) {
GostUtil.PauseRemoteService(outNode.getIp() + ":" + outNode.getPort(), serviceName, outNode.getSecret());
}
}
}
}
// 更新转发状态为暂停
Forward forward = forwardService.getById(forwardId);
if (forward != null) {
forward.setStatus(0);
forwardService.updateById(forward);
}
}
/**
* 检查用户总流量限制
*/
private void checkUserTotalFlowLimit(String userId, String userTunnelId) {
User user = userService.getById(userId);
if (user == null) {
return;
}
long userFlowLimit = user.getFlow() * BYTES_TO_GB;
long userCurrentFlow = user.getInFlow() + user.getOutFlow();
if (userFlowLimit < userCurrentFlow) {
pauseAllUserServices(userId, userTunnelId);
}
}
/**
* 暂停用户所有服务
*/
private void pauseAllUserServices(String userId, String userTunnelId) {
List<Forward> userForwards = forwardService.list(new QueryWrapper<Forward>().eq("user_id", userId));
for (Forward forward : userForwards) {
Tunnel tunnel = tunnelService.getById(forward.getTunnelId());
if (tunnel != null) {
Node node = nodeService.getNodeById(tunnel.getInNodeId());
if (node != null) {
String serviceName = buildServiceName(String.valueOf(forward.getId()), userId, userTunnelId);
GostUtil.PauseService(node.getIp() + ":" + node.getPort(), serviceName, node.getSecret());
// 隧道转发需要同时暂停远端服务
if (tunnel.getType() == 2) { // TUNNEL_TYPE_TUNNEL_FORWARD
Node outNode = nodeService.getNodeById(tunnel.getOutNodeId());
if (outNode != null) {
GostUtil.PauseRemoteService(outNode.getIp() + ":" + outNode.getPort(), serviceName, outNode.getSecret());
}
}
}
}
forward.setStatus(0);
forwardService.updateById(forward);
}
}
/**
* 构建服务名称
*/
private String buildServiceName(String forwardId, String userId, String userTunnelId) {
return forwardId + "_" + userId + "_" + userTunnelId;
}
/**
* 获取用户锁对象
*/
private Object getUserLock(String userId) {
return USER_LOCKS.computeIfAbsent(userId, k -> new Object());
}
/**
* 获取隧道锁对象
*/
private Object getTunnelLock(String userTunnelId) {
return TUNNEL_LOCKS.computeIfAbsent(userTunnelId, k -> new Object());
}
/**
* 流量统计数据类
*/
private static class FlowStatistics {
private final long upload;
private final long download;
public FlowStatistics(long upload, long download) {
this.upload = upload;
this.download = download;
}
public long getUpload() {
return upload;
}
public long getDownload() {
return download;
}
}
}
@@ -0,0 +1,70 @@
package com.admin.controller;
import com.admin.common.aop.LogAnnotation;
import com.admin.common.annotation.RequireRole;
import com.admin.common.dto.ForwardDto;
import com.admin.common.dto.ForwardUpdateDto;
import com.admin.common.dto.PageDto;
import com.admin.common.lang.R;
import com.admin.service.ForwardService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* <p>
* 前端控制器
* </p>
*
* @author QAQ
* @since 2025-06-03
*/
@RestController
@CrossOrigin
@RequestMapping("/api/v1/forward")
public class ForwardController extends BaseController {
@LogAnnotation
@PostMapping("/create")
public R create(@Validated @RequestBody ForwardDto forwardDto) {
return forwardService.createForward(forwardDto);
}
@LogAnnotation
@PostMapping("/list")
public R readAll() {
return forwardService.getAllForwards();
}
@LogAnnotation
@PostMapping("/update")
public R update(@Validated @RequestBody ForwardUpdateDto forwardUpdateDto) {
return forwardService.updateForward(forwardUpdateDto);
}
@LogAnnotation
@PostMapping("/delete")
public R delete(@RequestBody Map<String, Object> params) {
Long id = Long.valueOf(params.get("id").toString());
return forwardService.deleteForward(id);
}
@LogAnnotation
@PostMapping("/pause")
public R pause(@RequestBody Map<String, Object> params) {
Long id = Long.valueOf(params.get("id").toString());
return forwardService.pauseForward(id);
}
@LogAnnotation
@PostMapping("/resume")
public R resume(@RequestBody Map<String, Object> params) {
Long id = Long.valueOf(params.get("id").toString());
return forwardService.resumeForward(id);
}
}
@@ -0,0 +1,65 @@
package com.admin.controller;
import com.admin.common.annotation.RequireRole;
import com.admin.common.aop.LogAnnotation;
import com.admin.common.dto.NodeDto;
import com.admin.common.dto.NodeUpdateDto;
import com.admin.common.dto.PageDto;
import com.admin.common.lang.R;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* <p>
* 前端控制器
* </p>
*
* @author QAQ
* @since 2025-06-03
*/
@RestController
@CrossOrigin
@RequestMapping("/api/v1/node")
public class NodeController extends BaseController {
@LogAnnotation
@RequireRole
@PostMapping("/create")
public R create(@Validated @RequestBody NodeDto nodeDto) {
return nodeService.createNode(nodeDto);
}
@LogAnnotation
@RequireRole
@PostMapping("/list")
public R list() {
return nodeService.getAllNodes();
}
@LogAnnotation
@RequireRole
@PostMapping("/update")
public R update(@Validated @RequestBody NodeUpdateDto nodeUpdateDto) {
return nodeService.updateNode(nodeUpdateDto);
}
@LogAnnotation
@RequireRole
@PostMapping("/delete")
public R delete(@RequestBody Map<String, Object> params) {
Long id = Long.valueOf(params.get("id").toString());
return nodeService.deleteNode(id);
}
@LogAnnotation
@RequireRole
@PostMapping("/install")
public R getInstallCommand(@RequestBody Map<String, Object> params) {
Long id = Long.valueOf(params.get("id").toString());
return nodeService.getInstallCommand(id);
}
}
@@ -0,0 +1,70 @@
package com.admin.controller;
import com.admin.common.aop.LogAnnotation;
import com.admin.common.annotation.RequireRole;
import com.admin.common.dto.SpeedLimitDto;
import com.admin.common.dto.SpeedLimitUpdateDto;
import com.admin.common.lang.R;
import com.admin.service.SpeedLimitService;
import com.admin.service.TunnelService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* <p>
* 限速规则前端控制器
* </p>
*
* @author QAQ
* @since 2025-06-04
*/
@RestController
@RequestMapping("/api/v1/speed-limit")
@CrossOrigin
public class SpeedLimitController extends BaseController {
@Autowired
private SpeedLimitService speedLimitService;
@Autowired
private TunnelService tunnelService;
@LogAnnotation
@RequireRole
@PostMapping("/create")
public R create(@Validated @RequestBody SpeedLimitDto speedLimitDto) {
return speedLimitService.createSpeedLimit(speedLimitDto);
}
@LogAnnotation
@RequireRole
@PostMapping("/list")
public R list() {
return speedLimitService.getAllSpeedLimits();
}
@LogAnnotation
@RequireRole
@PostMapping("/update")
public R update(@Validated @RequestBody SpeedLimitUpdateDto speedLimitUpdateDto) {
return speedLimitService.updateSpeedLimit(speedLimitUpdateDto);
}
@LogAnnotation
@RequireRole
@PostMapping("/delete")
public R delete(@RequestBody Map<String, Object> params) {
Long id = Long.valueOf(params.get("id").toString());
return speedLimitService.deleteSpeedLimit(id);
}
@LogAnnotation
@RequireRole
@PostMapping("/tunnels")
public R getTunnels() {
return tunnelService.getAllTunnels();
}
}
@@ -0,0 +1,135 @@
package com.admin.controller;
import com.admin.common.aop.LogAnnotation;
import com.admin.common.annotation.RequireRole;
import com.admin.common.dto.PageDto;
import com.admin.common.dto.TunnelDto;
import com.admin.common.dto.UserTunnelDto;
import com.admin.common.dto.UserTunnelQueryDto;
import com.admin.common.dto.UserTunnelUpdateDto;
import com.admin.common.lang.R;
import com.admin.service.TunnelService;
import com.admin.service.UserTunnelService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* <p>
* 隧道前端控制器
* </p>
*
* @author QAQ
* @since 2025-06-03
*/
@RestController
@CrossOrigin
@RequestMapping("/api/v1/tunnel")
public class TunnelController extends BaseController {
@Autowired
private TunnelService tunnelService;
@Autowired
private UserTunnelService userTunnelService;
@LogAnnotation
@RequireRole
@PostMapping("/create")
public R create(@Validated @RequestBody TunnelDto tunnelDto) {
return tunnelService.createTunnel(tunnelDto);
}
@LogAnnotation
@RequireRole
@PostMapping("/list")
public R readAll(@RequestBody(required = false) PageDto pageDto) {
return tunnelService.getAllTunnels();
}
@LogAnnotation
@RequireRole
@PostMapping("/delete")
public R delete(@RequestBody Map<String, Object> params) {
Long id = Long.valueOf(params.get("id").toString());
return tunnelService.deleteTunnel(id);
}
// ============ 用户隧道权限管理相关方法 ============
/**
* 分配用户隧道权限
* @param userTunnelDto 用户隧道权限数据
* @return 操作结果
*/
@LogAnnotation
@RequireRole
@PostMapping("/user/assign")
public R assignUserTunnel(@Validated @RequestBody UserTunnelDto userTunnelDto) {
return userTunnelService.assignUserTunnel(userTunnelDto);
}
/**
* 查询用户隧道权限列表
* @param queryDto 查询条件
* @return 用户隧道权限列表
*/
@LogAnnotation
@RequireRole
@PostMapping("/user/list")
public R getUserTunnelList(@RequestBody @Validated UserTunnelQueryDto queryDto) {
return userTunnelService.getUserTunnelList(queryDto);
}
/**
* 删除用户隧道权限
* @param params 包含userId和tunnelId的参数
* @return 操作结果
*/
@LogAnnotation
@RequireRole
@PostMapping("/user/remove")
public R removeUserTunnel(@RequestBody Map<String, Object> params) {
Integer id = Integer.valueOf(params.get("id").toString());
return userTunnelService.removeUserTunnel(id);
}
/**
* 更新用户隧道流量限制
* @param params 包含userId、tunnelId和flow的参数
* @return 操作结果
*/
@LogAnnotation
@RequireRole
@PostMapping("/user/updateFlow")
public R updateUserTunnelFlow(@RequestBody Map<String, Object> params) {
Integer id = Integer.valueOf(params.get("id").toString());
Long flow = Long.valueOf(params.get("flow").toString());
return userTunnelService.updateUserTunnelFlow(id, flow);
}
/**
* 更新用户隧道权限(包含流量、流量重置时间、到期时间)
* @param updateDto 更新数据
* @return 操作结果
*/
@LogAnnotation
@RequireRole
@PostMapping("/user/update")
public R updateUserTunnel(@Validated @RequestBody UserTunnelUpdateDto updateDto) {
return userTunnelService.updateUserTunnel(updateDto);
}
@LogAnnotation
@PostMapping("/user/tunnel")
public R userTunnel() {
return tunnelService.userTunnel();
}
}
@@ -0,0 +1,81 @@
package com.admin.controller;
import com.admin.common.aop.LogAnnotation;
import com.admin.common.annotation.RequireRole;
import com.admin.common.dto.ChangePasswordDto;
import com.admin.common.dto.LoginDto;
import com.admin.common.dto.PageDto;
import com.admin.common.dto.UserDto;
import com.admin.common.dto.UserUpdateDto;
import com.admin.common.lang.R;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* <p>
* 前端控制器
* </p>
*
* @author QAQ
* @since 2025-06-03
*/
@RestController
@CrossOrigin
@RequestMapping("/api/v1/user")
public class UserController extends BaseController {
@LogAnnotation
@PostMapping("/login")
public R login(@Validated @RequestBody LoginDto loginDto) {
return userService.login(loginDto);
}
@LogAnnotation
@RequireRole
@PostMapping("/create")
public R create(@Validated @RequestBody UserDto userDto) {
return userService.createUser(userDto);
}
@LogAnnotation
@RequireRole
@PostMapping("/list")
public R readAll(@RequestBody(required = false) PageDto pageDto) {
// 如果没有传分页参数,使用默认值
if (pageDto == null) {
pageDto = new PageDto();
}
return userService.getAllUsers(pageDto);
}
@LogAnnotation
@RequireRole
@PostMapping("/update")
public R update(@Validated @RequestBody UserUpdateDto userUpdateDto) {
return userService.updateUser(userUpdateDto);
}
@LogAnnotation
@RequireRole
@PostMapping("/delete")
public R delete(@RequestBody Map<String, Object> params) {
Long id = Long.valueOf(params.get("id").toString());
return userService.deleteUser(id);
}
@LogAnnotation
@PostMapping("/package")
public R getUserPackageInfo() {
return userService.getUserPackageInfo();
}
@LogAnnotation
@PostMapping("/updatePassword")
public R updatePassword(@Validated @RequestBody ChangePasswordDto changePasswordDto) {
return userService.updatePassword(changePasswordDto);
}
}
@@ -0,0 +1,37 @@
package com.admin.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import java.io.Serializable;
/**
* 基础实体类,包含公共字段
*/
@Data
public class BaseEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 创建时间(时间戳)
*/
private Long createdTime;
/**
* 更新时间(时间戳)
*/
private Long updatedTime;
/**
* 状态(0:正常,1:删除)
*/
private Integer status;
}
@@ -0,0 +1,40 @@
package com.admin.entity;
import java.io.Serializable;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* <p>
*
* </p>
*
* @author QAQ
* @since 2025-06-03
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class Forward extends BaseEntity{
private static final long serialVersionUID = 1L;
private Integer userId;
private String userName;
private String name;
private Integer tunnelId;
private Integer inPort;
private Integer outPort;
private String remoteAddr;
private Long inFlow;
private Long outFlow;
}
@@ -0,0 +1,30 @@
package com.admin.entity;
import java.io.Serializable;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* <p>
*
* </p>
*
* @author QAQ
* @since 2025-06-03
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class Node extends BaseEntity {
private static final long serialVersionUID = 1L;
private String name;
private String secret;
private String ip;
private Integer port;
}
@@ -0,0 +1,53 @@
package com.admin.entity;
import com.admin.entity.BaseEntity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
/**
* <p>
*
* </p>
*
* @author QAQ
* @since 2025-06-04
*/
@Data
public class SpeedLimit implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 创建时间(时间戳)
*/
private Long createdTime;
/**
* 更新时间(时间戳)
*/
private Long updatedTime;
/**
* 状态(0:正常,1:删除)
*/
private Integer status;
private String name;
private Integer speed;
private Long tunnelId;
private String tunnelName;
}
@@ -0,0 +1,75 @@
package com.admin.entity;
import java.io.Serializable;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* <p>
* 隧道实体类
* </p>
*
* @author QAQ
* @since 2025-06-03
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class Tunnel extends BaseEntity {
private static final long serialVersionUID = 1L;
/**
* 隧道名称
*/
private String name;
/**
* 入口节点ID
*/
private Long inNodeId;
/**
* 入口IP (兼容字段)
*/
private String inIp;
/**
* 入口端口开始
*/
private Integer inPortSta;
/**
* 入口端口结束
*/
private Integer inPortEnd;
/**
* 出口节点ID
*/
private Long outNodeId;
/**
* 出口IP (兼容字段)
*/
private String outIp;
/**
* 出口端口开始
*/
private Integer outIpSta;
/**
* 出口端口结束
*/
private Integer outIpEnd;
/**
* 隧道类型(1-端口转发,2-隧道转发)
*/
private Integer type;
/**
* 流量计算类型(1 单向计算上传。2 双向)
*/
private int flow;
}
@@ -0,0 +1,42 @@
package com.admin.entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* <p>
*
* </p>
*
* @author QAQ
* @since 2025-06-03
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class User extends BaseEntity {
private static final long serialVersionUID = 1L;
private String name;
private String user;
private String pwd;
private Integer roleId;
private Long expTime;
private Long flow;
private Long inFlow;
private Long outFlow;
private Integer num;
private Long flowResetTime;
}
@@ -0,0 +1,53 @@
package com.admin.entity;
import java.io.Serializable;
import com.baomidou.mybatisplus.annotation.FieldStrategy;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
/**
* <p>
*
* </p>
*
* @author QAQ
* @since 2025-06-03
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class UserTunnel implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
private Integer userId;
private Integer tunnelId;
private Long flow;
private Long inFlow;
private Long outFlow;
private Long flowResetTime;
private Long expTime;
@TableField(updateStrategy = FieldStrategy.IGNORED)
private Integer speedId;
private Integer num;
private Integer status;
}
@@ -0,0 +1,33 @@
package com.admin.mapper;
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 java.util.List;
/**
* <p>
* Mapper 接口
* </p>
*
* @author QAQ
* @since 2025-06-03
*/
public interface ForwardMapper extends BaseMapper<Forward> {
/**
* 查询所有转发信息(包含隧道信息)
* @return 转发信息列表
*/
List<ForwardWithTunnelDto> selectAllForwardsWithTunnel();
/**
* 根据用户ID查询转发信息(包含隧道信息)
* @param userId 用户ID
* @return 转发信息列表
*/
List<ForwardWithTunnelDto> selectForwardsWithTunnelByUserId(@Param("userId") Integer userId);
}
@@ -0,0 +1,16 @@
package com.admin.mapper;
import com.admin.entity.Node;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* Mapper 接口
* </p>
*
* @author QAQ
* @since 2025-06-03
*/
public interface NodeMapper extends BaseMapper<Node> {
}
@@ -0,0 +1,16 @@
package com.admin.mapper;
import com.admin.entity.SpeedLimit;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* Mapper 接口
* </p>
*
* @author QAQ
* @since 2025-06-04
*/
public interface SpeedLimitMapper extends BaseMapper<SpeedLimit> {
}
@@ -0,0 +1,16 @@
package com.admin.mapper;
import com.admin.entity.Tunnel;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* Mapper 接口
* </p>
*
* @author QAQ
* @since 2025-06-03
*/
public interface TunnelMapper extends BaseMapper<Tunnel> {
}
@@ -0,0 +1,39 @@
package com.admin.mapper;
import com.admin.entity.User;
import com.admin.common.dto.UserPackageDto;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* <p>
* Mapper 接口
* </p>
*
* @author QAQ
* @since 2025-06-03
*/
public interface UserMapper extends BaseMapper<User> {
/**
* 查询用户隧道权限详情
* @param userId 用户ID
* @return 隧道权限列表
*/
List<UserPackageDto.UserTunnelDetailDto> getUserTunnelDetails(@Param("userId") Integer userId);
/**
* 查询用户转发详情
* @param userId 用户ID
* @return 转发列表
*/
List<UserPackageDto.UserForwardDetailDto> getUserForwardDetails(@Param("userId") Integer userId);
/**
* 管理员查询所有隧道(流量和转发设置为99999)
* @return 隧道列表
*/
List<UserPackageDto.UserTunnelDetailDto> getAllTunnelsForAdmin();
}
@@ -0,0 +1,24 @@
package com.admin.mapper;
import com.admin.entity.UserTunnel;
import com.admin.common.dto.UserTunnelWithDetailDto;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* <p>
* Mapper 接口
* </p>
*
* @author QAQ
* @since 2025-06-03
*/
public interface UserTunnelMapper extends BaseMapper<UserTunnel> {
List<UserTunnelWithDetailDto> getUserTunnelWithDetails(@Param("userId") Integer userId);
}
@@ -0,0 +1,59 @@
package com.admin.service;
import com.admin.common.dto.ForwardDto;
import com.admin.common.dto.ForwardUpdateDto;
import com.admin.common.lang.R;
import com.admin.entity.Forward;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 服务类
* </p>
*
* @author QAQ
* @since 2025-06-03
*/
public interface ForwardService extends IService<Forward> {
/**
* 创建端口转发
* @param forwardDto 转发数据
* @return 结果
*/
R createForward(ForwardDto forwardDto);
/**
* 获取端口转发列表
* @return 结果
*/
R getAllForwards();
/**
* 更新端口转发
* @param forwardUpdateDto 更新数据
* @return 结果
*/
R updateForward(ForwardUpdateDto forwardUpdateDto);
/**
* 删除端口转发
* @param id 转发ID
* @return 结果
*/
R deleteForward(Long id);
/**
* 暂停转发服务
* @param id 转发ID
* @return 结果
*/
R pauseForward(Long id);
/**
* 恢复转发服务
* @param id 转发ID
* @return 结果
*/
R resumeForward(Long id);
}
@@ -0,0 +1,31 @@
package com.admin.service;
import com.admin.common.dto.NodeDto;
import com.admin.common.dto.NodeUpdateDto;
import com.admin.common.dto.PageDto;
import com.admin.common.lang.R;
import com.admin.entity.Node;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 服务类
* </p>
*
* @author QAQ
* @since 2025-06-03
*/
public interface NodeService extends IService<Node> {
R createNode(NodeDto nodeDto);
R getAllNodes();
R updateNode(NodeUpdateDto nodeUpdateDto);
R deleteNode(Long id);
Node getNodeById(Long id);
R getInstallCommand(Long id);
}
@@ -0,0 +1,45 @@
package com.admin.service;
import com.admin.common.dto.SpeedLimitDto;
import com.admin.common.dto.SpeedLimitUpdateDto;
import com.admin.common.lang.R;
import com.admin.entity.SpeedLimit;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 限速规则服务类
* </p>
*
* @author QAQ
* @since 2025-06-04
*/
public interface SpeedLimitService extends IService<SpeedLimit> {
/**
* 创建限速规则
* @param speedLimitDto 限速规则数据
* @return 结果
*/
R createSpeedLimit(SpeedLimitDto speedLimitDto);
/**
* 获取所有限速规则
* @return 结果
*/
R getAllSpeedLimits();
/**
* 更新限速规则
* @param speedLimitUpdateDto 更新数据
* @return 结果
*/
R updateSpeedLimit(SpeedLimitUpdateDto speedLimitUpdateDto);
/**
* 删除限速规则
* @param id 限速规则ID
* @return 结果
*/
R deleteSpeedLimit(Long id);
}

Some files were not shown because too many files have changed in this diff Show More