mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-09-28 07:36:38 +08:00
Merge pull request #46 from Sagit-chu/opencode/jolly-circuit
fix: fix some bugs
This commit is contained in:
@@ -841,6 +841,13 @@ func (h *Handler) userTunnelUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch details to sync forwards
|
||||
var userID, tunnelID int64
|
||||
if err := h.repo.DB().QueryRow("SELECT user_id, tunnel_id FROM user_tunnel WHERE id = ?", id).Scan(&userID, &tunnelID); err == nil {
|
||||
h.syncUserTunnelForwards(userID, tunnelID)
|
||||
}
|
||||
|
||||
response.WriteJSON(w, response.OKEmpty())
|
||||
}
|
||||
|
||||
@@ -2279,8 +2286,8 @@ func replaceTunnelChainsTx(tx *sql.Tx, tunnelID int64, req map[string]interface{
|
||||
return pickErr
|
||||
}
|
||||
}
|
||||
_, err := tx.Exec(`INSERT INTO chain_tunnel(tunnel_id, chain_type, node_id, port, strategy, inx, protocol) VALUES(?, 3, ?, ?, NULL, 0, ?)`,
|
||||
tunnelID, nodeID, port, defaultString(asString(n["protocol"]), "tls"))
|
||||
_, err := tx.Exec(`INSERT INTO chain_tunnel(tunnel_id, chain_type, node_id, port, strategy, inx, protocol) VALUES(?, 3, ?, ?, ?, 0, ?)`,
|
||||
tunnelID, nodeID, port, defaultString(asString(n["strategy"]), "round"), defaultString(asString(n["protocol"]), "tls"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -2683,9 +2690,26 @@ func (h *Handler) upsertUserTunnel(req map[string]interface{}) error {
|
||||
|
||||
_, err = db.Exec(`UPDATE user_tunnel SET speed_id = ?, flow = ?, num = ?, exp_time = ?, flow_reset_time = ?, status = ? WHERE id = ?`,
|
||||
newSpeedID, newFlow, newNum, newExpTime, newFlowReset, newStatus, existingID)
|
||||
|
||||
if err == nil {
|
||||
h.syncUserTunnelForwards(userID, tunnelID)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *Handler) syncUserTunnelForwards(userID, tunnelID int64) {
|
||||
forwards, err := h.listForwardsByTunnel(tunnelID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for i := range forwards {
|
||||
f := &forwards[i]
|
||||
if f.UserID == userID {
|
||||
_ = h.syncForwardServices(f, "UpdateService", true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func asAnySlice(v interface{}) []interface{} {
|
||||
if v == nil {
|
||||
return nil
|
||||
|
||||
@@ -43,8 +43,8 @@ func updateChain(req updateChainRequest) error {
|
||||
|
||||
name := strings.TrimSpace(req.Chain)
|
||||
|
||||
if !registry.ChainRegistry().IsRegistered(name) {
|
||||
return errors.New("chain " + name + " not found")
|
||||
if registry.ChainRegistry().IsRegistered(name) {
|
||||
registry.ChainRegistry().Unregister(name)
|
||||
}
|
||||
|
||||
req.Data.Name = name
|
||||
@@ -54,19 +54,22 @@ func updateChain(req updateChainRequest) error {
|
||||
return errors.New("create chain " + name + " failed: " + err.Error())
|
||||
}
|
||||
|
||||
registry.ChainRegistry().Unregister(name)
|
||||
|
||||
if err := registry.ChainRegistry().Register(name, v); err != nil {
|
||||
return errors.New("chain " + name + " already exists")
|
||||
}
|
||||
|
||||
config.OnUpdate(func(c *config.Config) error {
|
||||
found := false
|
||||
for i := range c.Chains {
|
||||
if c.Chains[i].Name == name {
|
||||
c.Chains[i] = &req.Data
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
c.Chains = append(c.Chains, &req.Data)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -77,10 +80,9 @@ func deleteChain(req deleteChainRequest) error {
|
||||
|
||||
name := strings.TrimSpace(req.Chain)
|
||||
|
||||
if !registry.ChainRegistry().IsRegistered(name) {
|
||||
return errors.New("chain " + name + " not found")
|
||||
if registry.ChainRegistry().IsRegistered(name) {
|
||||
registry.ChainRegistry().Unregister(name)
|
||||
}
|
||||
registry.ChainRegistry().Unregister(name)
|
||||
|
||||
config.OnUpdate(func(c *config.Config) error {
|
||||
chains := c.Chains
|
||||
|
||||
@@ -37,27 +37,30 @@ func updateLimiter(req updateLimiterRequest) error {
|
||||
|
||||
name := strings.TrimSpace(req.Limiter)
|
||||
|
||||
if !registry.TrafficLimiterRegistry().IsRegistered(name) {
|
||||
return errors.New("limiter " + name + " not found")
|
||||
if registry.TrafficLimiterRegistry().IsRegistered(name) {
|
||||
registry.TrafficLimiterRegistry().Unregister(name)
|
||||
}
|
||||
|
||||
req.Data.Name = name
|
||||
|
||||
v := parser.ParseTrafficLimiter(&req.Data)
|
||||
|
||||
registry.TrafficLimiterRegistry().Unregister(name)
|
||||
|
||||
if err := registry.TrafficLimiterRegistry().Register(name, v); err != nil {
|
||||
return errors.New("limiter " + name + " already exists")
|
||||
}
|
||||
|
||||
config.OnUpdate(func(c *config.Config) error {
|
||||
found := false
|
||||
for i := range c.Limiters {
|
||||
if c.Limiters[i].Name == name {
|
||||
c.Limiters[i] = &req.Data
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
c.Limiters = append(c.Limiters, &req.Data)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -68,10 +71,9 @@ func deleteLimiter(req deleteLimiterRequest) error {
|
||||
|
||||
name := strings.TrimSpace(req.Limiter)
|
||||
|
||||
if !registry.TrafficLimiterRegistry().IsRegistered(name) {
|
||||
return errors.New("limiter " + name + " not found")
|
||||
if registry.TrafficLimiterRegistry().IsRegistered(name) {
|
||||
registry.TrafficLimiterRegistry().Unregister(name)
|
||||
}
|
||||
registry.TrafficLimiterRegistry().Unregister(name)
|
||||
|
||||
config.OnUpdate(func(c *config.Config) error {
|
||||
limiteres := c.Limiters
|
||||
|
||||
+41
-32
@@ -87,36 +87,32 @@ func updateServices(req updateServicesRequest) error {
|
||||
return errors.New("services list cannot be empty")
|
||||
}
|
||||
|
||||
// 第一阶段:验证所有服务存在
|
||||
for _, serviceConfig := range req.Data {
|
||||
name := strings.TrimSpace(serviceConfig.Name)
|
||||
// 第一阶段:验证所有服务名称有效性
|
||||
for i := range req.Data {
|
||||
name := strings.TrimSpace(req.Data[i].Name)
|
||||
if name == "" {
|
||||
return errors.New("service name is required")
|
||||
}
|
||||
serviceConfig.Name = name
|
||||
|
||||
old := registry.ServiceRegistry().Get(name)
|
||||
if old == nil {
|
||||
return errors.New("service " + name + " not found")
|
||||
}
|
||||
req.Data[i].Name = name
|
||||
}
|
||||
|
||||
// 第二阶段:按照原来的updateService逻辑,逐个更新服务
|
||||
for _, serviceConfig := range req.Data {
|
||||
name := strings.TrimSpace(serviceConfig.Name)
|
||||
serviceConfig.Name = name
|
||||
// 第二阶段:逐个更新服务(Upsert模式:存在则更新,不存在则创建)
|
||||
for i := range req.Data {
|
||||
serviceConfig := &req.Data[i]
|
||||
name := serviceConfig.Name
|
||||
|
||||
// 1. 获取旧服务
|
||||
old := registry.ServiceRegistry().Get(name)
|
||||
|
||||
// 2. 关闭旧服务
|
||||
old.Close()
|
||||
|
||||
// 3. 从注册表移除旧服务
|
||||
registry.ServiceRegistry().Unregister(name)
|
||||
// 2. 关闭旧服务 (如果存在)
|
||||
if old != nil {
|
||||
old.Close()
|
||||
// 3. 从注册表移除旧服务
|
||||
registry.ServiceRegistry().Unregister(name)
|
||||
}
|
||||
|
||||
// 4. 解析新服务配置
|
||||
svc, err := parser.ParseService(&serviceConfig)
|
||||
svc, err := parser.ParseService(serviceConfig)
|
||||
if err != nil {
|
||||
return errors.New("create service " + name + " failed: " + err.Error())
|
||||
}
|
||||
@@ -133,13 +129,20 @@ func updateServices(req updateServicesRequest) error {
|
||||
|
||||
// 第三阶段:更新配置
|
||||
config.OnUpdate(func(c *config.Config) error {
|
||||
for _, serviceConfig := range req.Data {
|
||||
for i := range c.Services {
|
||||
if c.Services[i].Name == serviceConfig.Name {
|
||||
c.Services[i] = &serviceConfig
|
||||
for i := range req.Data {
|
||||
// 创建副本以确保指针安全
|
||||
cfgCopy := req.Data[i]
|
||||
found := false
|
||||
for j := range c.Services {
|
||||
if c.Services[j].Name == cfgCopy.Name {
|
||||
c.Services[j] = &cfgCopy
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
c.Services = append(c.Services, &cfgCopy)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
@@ -158,22 +161,22 @@ func deleteServices(req deleteServicesRequest) error {
|
||||
name string
|
||||
service service.Service
|
||||
}
|
||||
var namesToRemove []string
|
||||
|
||||
for _, serviceName := range req.Services {
|
||||
name := strings.TrimSpace(serviceName)
|
||||
if name == "" {
|
||||
return errors.New("service name is required")
|
||||
}
|
||||
namesToRemove = append(namesToRemove, name)
|
||||
|
||||
svc := registry.ServiceRegistry().Get(name)
|
||||
if svc == nil {
|
||||
return errors.New("service " + name + " not found")
|
||||
if svc != nil {
|
||||
servicesToDelete = append(servicesToDelete, struct {
|
||||
name string
|
||||
service service.Service
|
||||
}{name, svc})
|
||||
}
|
||||
|
||||
servicesToDelete = append(servicesToDelete, struct {
|
||||
name string
|
||||
service service.Service
|
||||
}{name, svc})
|
||||
}
|
||||
|
||||
// 第二阶段:删除所有服务
|
||||
@@ -181,6 +184,12 @@ func deleteServices(req deleteServicesRequest) error {
|
||||
registry.ServiceRegistry().Unregister(std.name)
|
||||
std.service.Close()
|
||||
}
|
||||
// 确保所有请求删除的服务都从注册表中移除(即使之前未找到实例)
|
||||
for _, name := range namesToRemove {
|
||||
if registry.ServiceRegistry().IsRegistered(name) {
|
||||
registry.ServiceRegistry().Unregister(name)
|
||||
}
|
||||
}
|
||||
|
||||
// 第三阶段:更新配置
|
||||
config.OnUpdate(func(c *config.Config) error {
|
||||
@@ -188,8 +197,8 @@ func deleteServices(req deleteServicesRequest) error {
|
||||
c.Services = nil
|
||||
for _, s := range services {
|
||||
shouldDelete := false
|
||||
for _, std := range servicesToDelete {
|
||||
if s.Name == std.name {
|
||||
for _, name := range namesToRemove {
|
||||
if s.Name == name {
|
||||
shouldDelete = true
|
||||
break
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<factorypath>
|
||||
<factorypathentry kind="VARJAR" id="M2_REPO/org/projectlombok/lombok/1.18.30/lombok-1.18.30.jar" enabled="true" runInBatchMode="false"/>
|
||||
</factorypath>
|
||||
@@ -1,39 +0,0 @@
|
||||
# SPRINGBOOT BACKEND KNOWLEDGE BASE
|
||||
|
||||
**Generated:** Mon Feb 02 2026
|
||||
|
||||
## OVERVIEW
|
||||
Admin API for Flux Panel. Manages users, tunnels, nodes, forwards, quotas, and speed limits.
|
||||
**Stack:** Java 21, Spring Boot 2.7.18, SQLite, MyBatis Plus (+ join), FastJSON2.
|
||||
|
||||
## STRUCTURE
|
||||
```
|
||||
springboot-backend/
|
||||
├── src/main/java/com/admin/
|
||||
│ ├── controller/ # /api/v1/* endpoints
|
||||
│ ├── entity/ # DB models
|
||||
│ ├── mapper/ # MyBatis Plus mappers
|
||||
│ ├── service/ # Business logic
|
||||
│ ├── config/ # WebMvc/JWT/CORS/WebSocket config
|
||||
│ └── common/ # DTOs, auth, exception handling, utilities
|
||||
└── src/main/resources/
|
||||
├── application.yml # Config (DB_PATH/JWT_SECRET/LOG_DIR)
|
||||
├── mapper/ # XML mappers
|
||||
├── schema.sql # Schema
|
||||
└── data.sql # Seed data
|
||||
```
|
||||
|
||||
## CONVENTIONS
|
||||
- **DB**: SQLite URL is `jdbc:sqlite:${DB_PATH:/app/data/gost.db}` (`springboot-backend/src/main/resources/application.yml`).
|
||||
- **Auth**: JWT in `Authorization` header; enforced by `com.admin.common.interceptor.JwtInterceptor` for `/api/**` (with explicit excludes in `com.admin.config.WebMvcConfig`).
|
||||
- **Roles**: `@RequireRole` means admin-only (`role_id == 0`) via `com.admin.common.aop.RoleAspect`.
|
||||
- **Responses**: Controllers return `com.admin.common.lang.R` (`code == 0` success).
|
||||
- **CORS**: Allow-all origins; `Authorization` is exposed (`com.admin.config.WebMvcConfig`).
|
||||
|
||||
## COMMANDS
|
||||
```bash
|
||||
cd springboot-backend
|
||||
mvn clean package
|
||||
mvn test
|
||||
java -jar target/admin-0.0.1-SNAPSHOT.jar
|
||||
```
|
||||
@@ -1,27 +0,0 @@
|
||||
# 构建阶段
|
||||
FROM maven:3.9.6-eclipse-temurin-21 AS build
|
||||
WORKDIR /app
|
||||
COPY pom.xml .
|
||||
COPY src ./src
|
||||
RUN mvn clean package -DskipTests
|
||||
|
||||
# 运行阶段
|
||||
FROM eclipse-temurin:21-jre
|
||||
WORKDIR /app
|
||||
ENV JAVA_OPTS="-Xmx512m -Xms256m -Dfile.encoding=UTF-8 -Duser.timezone=Asia/Shanghai"
|
||||
ENV LANG=en_US.UTF-8
|
||||
ENV LANGUAGE=en_US:en
|
||||
ENV LC_ALL=en_US.UTF-8
|
||||
|
||||
RUN sed -i 's/archive.ubuntu.com/mirrors.ustc.edu.cn/g' /etc/apt/sources.list \
|
||||
&& sed -i 's/security.ubuntu.com/mirrors.ustc.edu.cn/g' /etc/apt/sources.list \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends fontconfig fonts-dejavu sqlite3 \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=build /app/target/*.jar app.jar
|
||||
|
||||
EXPOSE 6365
|
||||
|
||||
ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar /app/app.jar"]
|
||||
@@ -1,180 +0,0 @@
|
||||
<?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>21</java.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<spring-boot.version>2.7.18</spring-boot.version>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
</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>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.30</version>
|
||||
<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>org.xerial</groupId>
|
||||
<artifactId>sqlite-jdbc</artifactId>
|
||||
<version>3.45.0.0</version>
|
||||
</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>
|
||||
<version>4.5.14</version>
|
||||
</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>
|
||||
|
||||
<!-- tianai-captcha Spring Boot Starter -->
|
||||
<dependency>
|
||||
<groupId>cloud.tianai.captcha</groupId>
|
||||
<artifactId>tianai-captcha-springboot-starter</artifactId>
|
||||
<version>1.5.2</version>
|
||||
</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.11.0</version>
|
||||
<configuration>
|
||||
<source>21</source>
|
||||
<target>21</target>
|
||||
<encoding>UTF-8</encoding>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.30</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</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>
|
||||
@@ -1,32 +0,0 @@
|
||||
# SPRINGBOOT BACKEND (com.admin) KNOWLEDGE BASE
|
||||
|
||||
## OVERVIEW
|
||||
Primary Java code for the admin API. Controllers expose `/api/v1/*` endpoints and return `R` response envelopes.
|
||||
|
||||
## STRUCTURE
|
||||
```
|
||||
springboot-backend/src/main/java/com/admin/
|
||||
├── controller/ # REST controllers (e.g., /api/v1/user)
|
||||
├── service/ # Business logic interfaces + impl/
|
||||
├── mapper/ # MyBatis Plus mappers
|
||||
├── entity/ # DB entities
|
||||
├── config/ # WebMvc/JWT/CORS/WebSocket config
|
||||
└── common/ # DTOs, auth, exception handling, utilities
|
||||
```
|
||||
|
||||
## WHERE TO LOOK
|
||||
| Task | Location | Notes |
|
||||
|------|----------|-------|
|
||||
| User/login endpoints | `springboot-backend/src/main/java/com/admin/controller/UserController.java` | `/api/v1/user/*` |
|
||||
| Auth enforcement | `springboot-backend/src/main/java/com/admin/config/WebMvcConfig.java` | Intercepts `/api/**`, excludes login/config/captcha |
|
||||
| JWT validation | `springboot-backend/src/main/java/com/admin/common/interceptor/JwtInterceptor.java` | Requires `Authorization` header |
|
||||
| Admin-only ops | `springboot-backend/src/main/java/com/admin/common/annotation/RequireRole.java` | Enforced by `RoleAspect` |
|
||||
| Response envelope | `springboot-backend/src/main/java/com/admin/common/lang/R.java` | `code == 0` success |
|
||||
| Global error handling | `springboot-backend/src/main/java/com/admin/common/exception/GlobalExceptionHandler.java` | Maps exceptions -> `R.err(...)` |
|
||||
|
||||
## CONVENTIONS
|
||||
- Controllers are mostly `@PostMapping` (even for list/get/delete) and use `/api/v1/*` prefixes.
|
||||
- JWT is custom (no 3p lib) and includes `role_id` in payload (`springboot-backend/src/main/java/com/admin/common/utils/JwtUtil.java`).
|
||||
|
||||
## ANTI-PATTERNS
|
||||
- Do not change auth header format lightly: frontend expects `Authorization: <token>` (no `Bearer`).
|
||||
@@ -1,24 +0,0 @@
|
||||
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;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableAsync
|
||||
@EnableScheduling
|
||||
@MapperScan("com.admin.mapper")
|
||||
public class AdminApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(AdminApplication.class, args);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
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 {
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package com.admin.common.aop;
|
||||
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@Target({ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface LogAnnotation {}
|
||||
@@ -1,193 +0,0 @@
|
||||
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.info(errorMessage, ex);
|
||||
} catch (Exception e) {
|
||||
log.info("记录异常日志时出错: {}", 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class BatchChangeTunnelDto {
|
||||
|
||||
@NotEmpty(message = "转发ID列表不能为空")
|
||||
private List<Long> forwardIds;
|
||||
|
||||
@NotNull(message = "目标隧道ID不能为空")
|
||||
private Long targetTunnelId;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class BatchDeleteDto {
|
||||
|
||||
@NotEmpty(message = "ID列表不能为空")
|
||||
private List<Long> ids;
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
|
||||
@Data
|
||||
public class BatchOperationResultDto {
|
||||
|
||||
private int successCount;
|
||||
private int failCount;
|
||||
private List<FailedItem> failedItems = new ArrayList<>();
|
||||
|
||||
@Data
|
||||
public static class FailedItem {
|
||||
private Long id;
|
||||
private String reason;
|
||||
|
||||
public FailedItem() {}
|
||||
|
||||
public FailedItem(Long id, String reason) {
|
||||
this.id = id;
|
||||
this.reason = reason;
|
||||
}
|
||||
}
|
||||
|
||||
public void addFailedItem(Long id, String reason) {
|
||||
this.failedItems.add(new FailedItem(id, reason));
|
||||
this.failCount++;
|
||||
}
|
||||
|
||||
public void incrementSuccess() {
|
||||
this.successCount++;
|
||||
}
|
||||
|
||||
public boolean isAllSuccess() {
|
||||
return failCount == 0;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class BatchRedeployDto {
|
||||
|
||||
@NotEmpty(message = "ID列表不能为空")
|
||||
private List<Long> ids;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
/**
|
||||
* 验证码验证请求DTO
|
||||
*/
|
||||
@Data
|
||||
public class CaptchaVerifyDto {
|
||||
|
||||
private String id;
|
||||
|
||||
private ImageCaptchaTrack data;
|
||||
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class ChainNodesItems {
|
||||
|
||||
private String mode;
|
||||
|
||||
private List<Integer> nodeIds;
|
||||
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
@Data
|
||||
public class ChangePasswordDto {
|
||||
|
||||
@NotBlank(message = "新用户名不能为空")
|
||||
private String newUsername;
|
||||
|
||||
@NotBlank(message = "当前密码不能为空")
|
||||
private String currentPassword;
|
||||
|
||||
@NotBlank(message = "新密码不能为空")
|
||||
private String newPassword;
|
||||
|
||||
@NotBlank(message = "确认密码不能为空")
|
||||
private String confirmPassword;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ConfigItem {
|
||||
private String name;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class DiagnosisResult {
|
||||
private Long nodeId;
|
||||
private String nodeName;
|
||||
private String targetIp;
|
||||
private Integer targetPort;
|
||||
private String description;
|
||||
private boolean success;
|
||||
private String message;
|
||||
private double averageTime;
|
||||
private double packetLoss;
|
||||
private long timestamp;
|
||||
private Integer fromChainType;
|
||||
private Integer fromInx;
|
||||
private Integer toChainType;
|
||||
private Integer toInx;
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class FlowDto {
|
||||
// 转发id_类型
|
||||
private String n;
|
||||
|
||||
// 上传流量
|
||||
private Long u;
|
||||
|
||||
// 下载流量
|
||||
private Long d;
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
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 ForwardDto {
|
||||
|
||||
@NotBlank(message = "转发名称不能为空")
|
||||
private String name;
|
||||
|
||||
@NotNull(message = "隧道ID不能为空")
|
||||
private Integer tunnelId;
|
||||
|
||||
@NotBlank(message = "远程地址不能为空")
|
||||
private String remoteAddr;
|
||||
|
||||
private String strategy;
|
||||
|
||||
private Integer inPort;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ForwardPortDto {
|
||||
private Integer node_id;
|
||||
private Integer port;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
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 ForwardUpdateDto {
|
||||
|
||||
@NotNull(message = "ID不能为空")
|
||||
private Long id;
|
||||
|
||||
@NotNull(message = "用户ID不能为空")
|
||||
private Integer userId;
|
||||
|
||||
@NotBlank(message = "转发名称不能为空")
|
||||
private String name;
|
||||
|
||||
private Integer tunnelId;
|
||||
|
||||
@NotBlank(message = "远程地址不能为空")
|
||||
private String remoteAddr;
|
||||
|
||||
private String strategy;
|
||||
|
||||
private Integer inPort;
|
||||
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
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;
|
||||
|
||||
|
||||
/**
|
||||
* 入口IP
|
||||
*/
|
||||
private String inIp;
|
||||
|
||||
/**
|
||||
* 入口端口
|
||||
*/
|
||||
private Integer inPort;
|
||||
|
||||
/**
|
||||
* 远程地址
|
||||
*/
|
||||
private String remoteAddr;
|
||||
|
||||
/**
|
||||
* 转发状态
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private Long createdTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private Long updatedTime;
|
||||
|
||||
// 以下为隧道相关字段
|
||||
|
||||
/**
|
||||
* 隧道名称
|
||||
*/
|
||||
private String tunnelName;
|
||||
|
||||
|
||||
private String userName;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Integer userId;
|
||||
/**
|
||||
* 隧道ID
|
||||
*/
|
||||
private Integer tunnelId;
|
||||
|
||||
/**
|
||||
* 入站流量(字节)
|
||||
*/
|
||||
private Long inFlow;
|
||||
|
||||
/**
|
||||
* 出站流量(字节)
|
||||
*/
|
||||
private Long outFlow;
|
||||
|
||||
private String strategy;
|
||||
|
||||
private Integer inx;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class GostConfigDto {
|
||||
|
||||
private List<ConfigItem> limiters;
|
||||
|
||||
private List<ConfigItem> chains;
|
||||
|
||||
private List<ConfigItem> services;
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class GostDto {
|
||||
private Integer code;
|
||||
|
||||
private String msg;
|
||||
|
||||
private Object data; // 添加数据字段,用于存储响应的详细数据
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
@Data
|
||||
public class GroupCreateDto {
|
||||
|
||||
@NotBlank(message = "分组名称不能为空")
|
||||
private String name;
|
||||
|
||||
private Integer status;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
@Data
|
||||
public class GroupPermissionAssignDto {
|
||||
|
||||
@NotNull(message = "用户分组ID不能为空")
|
||||
private Long userGroupId;
|
||||
|
||||
@NotNull(message = "隧道分组ID不能为空")
|
||||
private Long tunnelGroupId;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class GroupPermissionDetailDto {
|
||||
private Long id;
|
||||
private Long userGroupId;
|
||||
private String userGroupName;
|
||||
private Long tunnelGroupId;
|
||||
private String tunnelGroupName;
|
||||
private Long createdTime;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
@Data
|
||||
public class GroupUpdateDto {
|
||||
|
||||
@NotNull(message = "分组ID不能为空")
|
||||
private Long id;
|
||||
|
||||
@NotBlank(message = "分组名称不能为空")
|
||||
private String name;
|
||||
|
||||
private Integer status;
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
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;
|
||||
|
||||
private String captchaId;
|
||||
|
||||
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
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;
|
||||
|
||||
@NotBlank(message = "服务器ip不能为空")
|
||||
private String serverIp;
|
||||
|
||||
private String serverIpV4;
|
||||
|
||||
private String serverIpV6;
|
||||
|
||||
@NotBlank(message = "可用端口不能为空")
|
||||
private String port;
|
||||
|
||||
private String interfaceName;
|
||||
|
||||
private String tcpListenAddr = "0.0.0.0";
|
||||
|
||||
private String udpListenAddr = "0.0.0.0";
|
||||
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
@Data
|
||||
public class NodeUpdateDto {
|
||||
|
||||
@NotNull(message = "节点ID不能为空")
|
||||
private Long id;
|
||||
|
||||
@NotBlank(message = "节点名称不能为空")
|
||||
private String name;
|
||||
|
||||
@NotBlank(message = "服务器ip不能为空")
|
||||
private String serverIp;
|
||||
|
||||
private String serverIpV4;
|
||||
|
||||
private String serverIpV6;
|
||||
|
||||
@NotBlank(message = "可用port不能为空")
|
||||
private String port;
|
||||
|
||||
private String interfaceName;
|
||||
private Integer http;
|
||||
private Integer tls;
|
||||
private Integer socks;
|
||||
|
||||
private String tcpListenAddr = "0.0.0.0";
|
||||
|
||||
private String udpListenAddr = "0.0.0.0";
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
@Data
|
||||
public class ResetFlowDto {
|
||||
|
||||
@NotNull(message = "重置账号id不能为空")
|
||||
private Integer id;
|
||||
|
||||
@NotNull(message = "重置类型不能为空")
|
||||
private Integer type;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import com.admin.entity.ChainTunnel;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 隧道详情DTO - 包含节点配置信息
|
||||
*/
|
||||
@Data
|
||||
public class TunnelDetailDto {
|
||||
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* Display/order index for drag-sorting in admin UI.
|
||||
* Lower value appears first.
|
||||
*/
|
||||
private Integer inx;
|
||||
|
||||
private String name;
|
||||
|
||||
private Integer type; // 1: 端口转发, 2: 隧道转发
|
||||
|
||||
private Integer flow; // 1: 单向, 2: 双向
|
||||
|
||||
private BigDecimal trafficRatio;
|
||||
|
||||
private Integer status;
|
||||
|
||||
private Long createdTime;
|
||||
|
||||
private Long updatedTime;
|
||||
|
||||
private String inIp;
|
||||
|
||||
// 入口节点列表
|
||||
private List<ChainTunnel> inNodeId = new ArrayList<>();
|
||||
|
||||
// 转发链节点列表(二维数组结构)
|
||||
private List<List<ChainTunnel>> chainNodes = new ArrayList<>();
|
||||
|
||||
// 出口节点列表
|
||||
private List<ChainTunnel> outNodeId = new ArrayList<>();
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import com.admin.entity.ChainTunnel;
|
||||
import lombok.Data;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.DecimalMin;
|
||||
import javax.validation.constraints.DecimalMax;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class TunnelDto {
|
||||
|
||||
@NotBlank(message = "隧道名称不能为空")
|
||||
private String name;
|
||||
|
||||
@NotNull(message = "入口节点不能为空")
|
||||
private List<ChainTunnel> inNodeId;
|
||||
|
||||
private List<List<ChainTunnel>> chainNodes;
|
||||
|
||||
private List<ChainTunnel> outNodeId;
|
||||
|
||||
private String inIp;
|
||||
|
||||
@NotNull(message = "隧道类型不能为空")
|
||||
private Integer type;
|
||||
|
||||
@NotNull(message = "流量计算类型不能为空")
|
||||
private Integer flow;
|
||||
|
||||
@DecimalMin(value = "0.0", inclusive = false, message = "流量倍率必须大于0.0")
|
||||
@DecimalMax(value = "100.0", message = "流量倍率不能大于100.0")
|
||||
private BigDecimal trafficRatio;
|
||||
}
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class TunnelGroupAssignTunnelsDto {
|
||||
|
||||
@NotNull(message = "隧道分组ID不能为空")
|
||||
private Long groupId;
|
||||
|
||||
@NotNull(message = "隧道列表不能为空")
|
||||
private List<Long> tunnelIds;
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class TunnelGroupDetailDto {
|
||||
private Long id;
|
||||
private String name;
|
||||
private Integer status;
|
||||
private Long createdTime;
|
||||
private Long updatedTime;
|
||||
private List<Long> tunnelIds = new ArrayList<>();
|
||||
private List<String> tunnelNames = new ArrayList<>();
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import com.admin.entity.ChainTunnel;
|
||||
import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import lombok.Data;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.Max;
|
||||
import javax.validation.constraints.DecimalMin;
|
||||
import javax.validation.constraints.DecimalMax;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class TunnelUpdateDto {
|
||||
|
||||
@NotNull(message = "隧道ID不能为空")
|
||||
private Long id;
|
||||
|
||||
@NotBlank(message = "隧道名称不能为空")
|
||||
private String name;
|
||||
|
||||
@NotNull(message = "流量计算类型不能为空")
|
||||
private Integer flow;
|
||||
|
||||
private String inIp;
|
||||
|
||||
@DecimalMin(value = "0.0", inclusive = false, message = "流量倍率必须大于0.0")
|
||||
@DecimalMax(value = "100.0", message = "流量倍率不能大于100.0")
|
||||
private BigDecimal trafficRatio;
|
||||
|
||||
// 入口节点配置(可选,为空时不更新节点配置)
|
||||
private List<ChainTunnel> inNodeId;
|
||||
|
||||
// 转发链节点配置(二维数组,每一跳可有多个节点)
|
||||
private List<List<ChainTunnel>> chainNodes;
|
||||
|
||||
// 出口节点配置
|
||||
private List<ChainTunnel> outNodeId;
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
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 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;
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class UserGroupAssignUsersDto {
|
||||
|
||||
@NotNull(message = "用户分组ID不能为空")
|
||||
private Long groupId;
|
||||
|
||||
@NotNull(message = "用户列表不能为空")
|
||||
private List<Long> userIds;
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class UserGroupDetailDto {
|
||||
private Long id;
|
||||
private String name;
|
||||
private Integer status;
|
||||
private Long createdTime;
|
||||
private Long updatedTime;
|
||||
private List<Long> userIds = new ArrayList<>();
|
||||
private List<String> userNames = new ArrayList<>();
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class UserInfo {
|
||||
private final Integer userId;
|
||||
private final Integer roleId;
|
||||
private final String userName;
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import com.admin.entity.StatisticsFlow;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户套餐信息DTO
|
||||
*/
|
||||
@Data
|
||||
public class UserPackageDto {
|
||||
|
||||
/**
|
||||
* 用户基本信息
|
||||
*/
|
||||
private UserInfoDto userInfo;
|
||||
|
||||
/**
|
||||
* 用户隧道权限列表
|
||||
*/
|
||||
private List<UserTunnelDetailDto> tunnelPermissions;
|
||||
|
||||
/**
|
||||
* 用户转发列表
|
||||
*/
|
||||
private List<UserForwardDetailDto> forwards;
|
||||
|
||||
/**
|
||||
* 用户转发列表
|
||||
*/
|
||||
private List<StatisticsFlow> statisticsFlows;
|
||||
|
||||
/**
|
||||
* 用户基本信息
|
||||
*/
|
||||
@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;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import com.admin.entity.UserTunnel;
|
||||
import com.admin.service.impl.ForwardServiceImpl;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class UserPermissionResult {
|
||||
public boolean hasError;
|
||||
private String errorMessage;
|
||||
private Integer limiter;
|
||||
private UserTunnel userTunnel;
|
||||
|
||||
public static UserPermissionResult success(Integer limiter, UserTunnel userTunnel) {
|
||||
UserPermissionResult result = new UserPermissionResult();
|
||||
result.setLimiter(limiter);
|
||||
result.setUserTunnel(userTunnel);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static UserPermissionResult error(String errorMessage) {
|
||||
UserPermissionResult result = new UserPermissionResult();
|
||||
result.setErrorMessage(errorMessage);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class UserTunnelBatchAssignDto {
|
||||
|
||||
@NotNull(message = "用户ID不能为空")
|
||||
private Integer userId;
|
||||
|
||||
@Valid
|
||||
@NotEmpty(message = "隧道列表不能为空")
|
||||
private List<TunnelAssignItem> tunnels;
|
||||
|
||||
@Data
|
||||
public static class TunnelAssignItem {
|
||||
@NotNull(message = "隧道ID不能为空")
|
||||
private Integer tunnelId;
|
||||
|
||||
private Integer speedId;
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
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;
|
||||
|
||||
@Min(value = 0, message = "流量限制不能小于0")
|
||||
private Long flow;
|
||||
|
||||
@Min(value = 0, message = "转发数量不能小于0")
|
||||
private Integer num;
|
||||
|
||||
private Long flowResetTime;
|
||||
|
||||
private Long expTime;
|
||||
|
||||
private Integer speedId;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
@Data
|
||||
public class UserTunnelQueryDto {
|
||||
|
||||
@NotNull
|
||||
private Integer userId;
|
||||
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
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;
|
||||
|
||||
@NotNull(message = "状态必选")
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 限速规则ID(可选,null表示不限速)
|
||||
*/
|
||||
private Integer speedId;
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
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;
|
||||
|
||||
private Integer status;
|
||||
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
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 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;
|
||||
}
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
package com.admin.common.exception;
|
||||
|
||||
import com.admin.common.lang.R;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.catalina.connector.ClientAbortException;
|
||||
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.info("实体校验异常:----------------{}", objectError.getDefaultMessage());
|
||||
return R.err(500, objectError.getDefaultMessage());
|
||||
}
|
||||
|
||||
// 未授权异常捕获
|
||||
@ExceptionHandler(value = UnauthorizedException.class)
|
||||
public R handleUnauthorizedException(UnauthorizedException e) {
|
||||
log.info("未授权异常:----------------{}", e.getMessage());
|
||||
return R.err(401, e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(value = Exception.class)
|
||||
public R Exception(Exception e){
|
||||
log.info("异常:----------------{}", e.getMessage());
|
||||
return R.err(-2, e.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
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 {
|
||||
|
||||
}
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
-71
@@ -1,71 +0,0 @@
|
||||
package com.admin.common.migration;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Lightweight SQLite schema migration.
|
||||
*
|
||||
* Spring Boot SQL init uses CREATE TABLE IF NOT EXISTS, so existing installations
|
||||
* won't automatically receive new columns. This runner adds missing columns in-place.
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
public class SqliteSchemaMigration implements ApplicationRunner {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
public SqliteSchemaMigration(JdbcTemplate jdbcTemplate) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) {
|
||||
ensureColumn("node", "inx", "INTEGER NOT NULL DEFAULT 0");
|
||||
ensureColumn("tunnel", "inx", "INTEGER NOT NULL DEFAULT 0");
|
||||
ensureTable("CREATE TABLE IF NOT EXISTS tunnel_group (id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(100) NOT NULL, created_time INTEGER NOT NULL, updated_time INTEGER NOT NULL, status INTEGER NOT NULL)");
|
||||
ensureTable("CREATE TABLE IF NOT EXISTS user_group (id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(100) NOT NULL, created_time INTEGER NOT NULL, updated_time INTEGER NOT NULL, status INTEGER NOT NULL)");
|
||||
ensureTable("CREATE TABLE IF NOT EXISTS tunnel_group_tunnel (id INTEGER PRIMARY KEY AUTOINCREMENT, tunnel_group_id INTEGER NOT NULL, tunnel_id INTEGER NOT NULL, created_time INTEGER NOT NULL)");
|
||||
ensureTable("CREATE TABLE IF NOT EXISTS user_group_user (id INTEGER PRIMARY KEY AUTOINCREMENT, user_group_id INTEGER NOT NULL, user_id INTEGER NOT NULL, created_time INTEGER NOT NULL)");
|
||||
ensureTable("CREATE TABLE IF NOT EXISTS group_permission (id INTEGER PRIMARY KEY AUTOINCREMENT, user_group_id INTEGER NOT NULL, tunnel_group_id INTEGER NOT NULL, created_time INTEGER NOT NULL)");
|
||||
ensureTable("CREATE TABLE IF NOT EXISTS group_permission_grant (id INTEGER PRIMARY KEY AUTOINCREMENT, user_group_id INTEGER NOT NULL, tunnel_group_id INTEGER NOT NULL, user_tunnel_id INTEGER NOT NULL, created_by_group INTEGER NOT NULL DEFAULT 0, created_time INTEGER NOT NULL)");
|
||||
ensureColumn("group_permission_grant", "created_by_group", "INTEGER NOT NULL DEFAULT 0");
|
||||
ensureTable("CREATE UNIQUE INDEX IF NOT EXISTS idx_tunnel_group_name ON tunnel_group(name)");
|
||||
ensureTable("CREATE UNIQUE INDEX IF NOT EXISTS idx_user_group_name ON user_group(name)");
|
||||
ensureTable("CREATE UNIQUE INDEX IF NOT EXISTS idx_tunnel_group_tunnel_unique ON tunnel_group_tunnel(tunnel_group_id, tunnel_id)");
|
||||
ensureTable("CREATE UNIQUE INDEX IF NOT EXISTS idx_user_group_user_unique ON user_group_user(user_group_id, user_id)");
|
||||
ensureTable("CREATE UNIQUE INDEX IF NOT EXISTS idx_group_permission_unique ON group_permission(user_group_id, tunnel_group_id)");
|
||||
ensureTable("CREATE UNIQUE INDEX IF NOT EXISTS idx_group_permission_grant_unique ON group_permission_grant(user_group_id, tunnel_group_id, user_tunnel_id)");
|
||||
}
|
||||
|
||||
private void ensureColumn(String table, String column, String columnDefinition) {
|
||||
Set<String> columns = new HashSet<>(
|
||||
jdbcTemplate.query(
|
||||
"PRAGMA table_info(" + table + ")",
|
||||
(rs, rowNum) -> rs.getString("name")
|
||||
)
|
||||
);
|
||||
|
||||
if (columns.contains(column)) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("Adding missing column {}.{}", table, column);
|
||||
jdbcTemplate.execute(
|
||||
"ALTER TABLE " + table + " ADD COLUMN " + column + " " + columnDefinition
|
||||
);
|
||||
}
|
||||
|
||||
private void ensureTable(String ddl) {
|
||||
jdbcTemplate.execute(ddl);
|
||||
}
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
package com.admin.common.task;
|
||||
|
||||
import com.admin.common.dto.*;
|
||||
import com.admin.common.lang.R;
|
||||
import com.admin.common.utils.GostUtil;
|
||||
import com.admin.entity.*;
|
||||
import com.admin.service.*;
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class CheckGostConfigAsync {
|
||||
|
||||
@Resource
|
||||
private NodeService nodeService;
|
||||
|
||||
@Resource
|
||||
@Lazy
|
||||
private ForwardService forwardService;
|
||||
|
||||
@Resource
|
||||
@Lazy
|
||||
private SpeedLimitService speedLimitService;
|
||||
|
||||
@Resource
|
||||
TunnelService tunnelService;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 清理孤立的Gost配置项
|
||||
*/
|
||||
@Async
|
||||
public void cleanNodeConfigs(String node_id, GostConfigDto gostConfig) {
|
||||
Node node = nodeService.getById(node_id);
|
||||
if (node != null) {
|
||||
cleanOrphanedServices(gostConfig.getServices(), node);
|
||||
cleanOrphanedChains(gostConfig.getChains(), node);
|
||||
cleanOrphanedLimiters(gostConfig.getLimiters(), node);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理孤立的服务
|
||||
*/
|
||||
private void cleanOrphanedServices(List<ConfigItem> configItems, Node node) {
|
||||
if (configItems == null) return;
|
||||
for (ConfigItem service : configItems) {
|
||||
safeExecute(() -> {
|
||||
|
||||
if (!Objects.equals(service.getName(), "web_api")){
|
||||
List<String> serviceIds = parseServiceName(service.getName());
|
||||
|
||||
JSONArray services = new JSONArray();
|
||||
if (Objects.equals(serviceIds.getLast(), "tls")){
|
||||
String forward_id = serviceIds.getFirst();
|
||||
services.add(forward_id + "_tls");
|
||||
|
||||
Tunnel tunnel = tunnelService.getById(forward_id);
|
||||
if (tunnel == null) {
|
||||
GostUtil.DeleteService(node.getId(), services);
|
||||
log.info("删除孤立的服务: {} (节点: {})", service.getName(), node.getId());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (Objects.equals(serviceIds.getLast(), "tcp")){
|
||||
String forward_id = serviceIds.getFirst();
|
||||
services.add(forward_id + "_" + serviceIds.get(1) + "_" + serviceIds.get(2) + "_tcp");
|
||||
services.add(forward_id + "_" + serviceIds.get(1) + "_" + serviceIds.get(2) + "_udp");
|
||||
|
||||
Forward forward = forwardService.getById(forward_id);
|
||||
if (forward == null) {
|
||||
GostUtil.DeleteService(node.getId(), services);
|
||||
log.info("删除孤立的服务: {} (节点: {})", service.getName(), node.getId());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}, "清理服务 " + service.getName());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理孤立的链
|
||||
*/
|
||||
private void cleanOrphanedChains(List<ConfigItem> configItems, Node node) {
|
||||
if (configItems == null) return;
|
||||
for (ConfigItem chain : configItems) {
|
||||
safeExecute(() -> {
|
||||
List<String> serviceIds = parseServiceName(chain.getName());
|
||||
Tunnel tunnel = tunnelService.getById(serviceIds.getLast());
|
||||
if (tunnel == null) {
|
||||
GostUtil.DeleteChains(node.getId(), chain.getName());
|
||||
log.info("删除孤立的链: {} (节点: {})", chain.getName(), node.getId());
|
||||
}
|
||||
}, "清理链 " + chain.getName());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理孤立的限流器
|
||||
*/
|
||||
private void cleanOrphanedLimiters(List<ConfigItem> configItems, Node node) {
|
||||
if (configItems == null) return;
|
||||
|
||||
|
||||
for (ConfigItem limiter : configItems) {
|
||||
safeExecute(() -> {
|
||||
SpeedLimit speedLimit = speedLimitService.getById(limiter.getName());
|
||||
if (speedLimit == null) {
|
||||
GostUtil.DeleteLimiters(node.getId(), Long.parseLong(limiter.getName()));
|
||||
log.info("删除孤立的限流器: {} (节点: {})", limiter.getName(), node.getId());
|
||||
}
|
||||
}, "清理限流器 " + limiter.getName());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 安全执行操作,捕获异常
|
||||
*/
|
||||
private void safeExecute(Runnable operation, String operationDesc) {
|
||||
try {
|
||||
operation.run();
|
||||
} catch (Exception e) {
|
||||
log.info("执行操作失败: {}", operationDesc, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 解析服务名称
|
||||
*/
|
||||
private List<String> parseServiceName(String serviceName) {
|
||||
String[] split = serviceName.split("_");
|
||||
return new ArrayList<>(Arrays.asList(split));
|
||||
}
|
||||
}
|
||||
@@ -1,246 +0,0 @@
|
||||
package com.admin.common.task;
|
||||
|
||||
import com.admin.common.dto.GostDto;
|
||||
import com.admin.common.utils.GostUtil;
|
||||
import com.admin.entity.*;
|
||||
import com.admin.service.*;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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.Date;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@EnableScheduling
|
||||
public class ResetFlowAsync {
|
||||
|
||||
@Resource
|
||||
UserService userService;
|
||||
|
||||
@Resource
|
||||
UserTunnelService userTunnelService;
|
||||
|
||||
@Resource
|
||||
ForwardService forwardService;
|
||||
|
||||
@Resource
|
||||
TunnelService tunnelService;
|
||||
|
||||
@Resource
|
||||
ChainTunnelService chainTunnelService;
|
||||
|
||||
/**
|
||||
* 每天0点执行流量重置任务
|
||||
* 查询出用户和隧道的重置流量日期是今天的数据,将上下流量重置为0
|
||||
* 考虑当月是29、30天,但是选择是31的这种边界情况
|
||||
*
|
||||
* 并发安全说明:
|
||||
* - 使用setSql()进行原子SQL更新,只更新流量字段(in_flow, out_flow)
|
||||
* - 不会影响DelayQueueManager的到期任务对status等其他字段的更新
|
||||
* - 避免了并发修改导致的数据覆盖问题
|
||||
*/
|
||||
@Scheduled(cron = "5 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("流量重置任务执行完成");
|
||||
|
||||
|
||||
// 处理过期账号
|
||||
user();
|
||||
|
||||
// 处理过期隧道
|
||||
userTunnel();
|
||||
|
||||
log.info("到期任务执行完成");
|
||||
|
||||
} catch (Exception e) {
|
||||
log.info("定时任务执行失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置用户流量
|
||||
* @param currentDay 当前日期(几号)
|
||||
* @param lastDayOfMonth 当月最后一天
|
||||
*/
|
||||
private void resetUserFlow(int currentDay, int lastDayOfMonth) {
|
||||
try {
|
||||
// flowResetTime字段存储的是0-31的数字,0表示不重置,1-31表示每月第几号重置
|
||||
// 构建查询条件:重置日期等于今天,或者重置日期大于当月最大天数且今天是月末
|
||||
// 排除flowResetTime为0的记录(不重置)
|
||||
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.ne("flow_reset_time", 0); // 排除不重置的用户
|
||||
|
||||
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.info("用户[ID: {}, 用户名: {}]流量重置失败", user.getId(), user.getUser());
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.info("重置用户流量失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置用户隧道流量
|
||||
* @param currentDay 当前日期(几号)
|
||||
* @param lastDayOfMonth 当月最后一天
|
||||
*/
|
||||
private void resetUserTunnelFlow(int currentDay, int lastDayOfMonth) {
|
||||
try {
|
||||
// flowResetTime字段存储的是0-31的数字,0表示不重置,1-31表示每月第几号重置
|
||||
// 构建查询条件:重置日期等于今天,或者重置日期大于当月最大天数且今天是月末
|
||||
// 排除flowResetTime为0的记录(不重置)
|
||||
QueryWrapper<UserTunnel> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.ne("flow_reset_time", 0); // 排除不重置的用户隧道
|
||||
|
||||
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.info("用户隧道[ID: {}, 用户ID: {}, 隧道ID: {}]流量重置失败",
|
||||
userTunnel.getId(), userTunnel.getUserId(), userTunnel.getTunnelId());
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.info("重置用户隧道流量失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void user(){
|
||||
// 查询过期用户
|
||||
List<User> user_list = userService.list(new QueryWrapper<User>().ne("role_id", 0).eq("status", 1).isNotNull("exp_time").lt("exp_time", new Date().getTime()));
|
||||
for (User user : user_list) {
|
||||
// 查询对应转发
|
||||
List<Forward> forwardList = forwardService.list(new QueryWrapper<Forward>().eq("user_id", user.getId()).eq("status", 1));
|
||||
for (Forward forward : forwardList) {
|
||||
|
||||
List<ChainTunnel> chainTunnels = chainTunnelService.list(new QueryWrapper<ChainTunnel>().eq("tunnel_id", forward.getTunnelId()).eq("chain_type", 1));
|
||||
for (ChainTunnel chainTunnel : chainTunnels) {
|
||||
UserTunnel userTunnel = userTunnelService.getOne(new QueryWrapper<UserTunnel>().eq("user_id", forward.getUserId()).eq("tunnel_id", forward.getTunnelId()));
|
||||
if (userTunnel != null) {
|
||||
String name = buildServiceName(forward.getId(), forward.getUserId(), userTunnel.getId());
|
||||
GostUtil.PauseAndResumeService(chainTunnel.getNodeId(), name, "PauseService");
|
||||
}
|
||||
}
|
||||
|
||||
forward.setStatus(0);
|
||||
forwardService.updateById(forward);
|
||||
}
|
||||
user.setStatus(0);
|
||||
userService.updateById(user);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void userTunnel(){
|
||||
// 查询过期隧道
|
||||
List<UserTunnel> user_tunnel_list = userTunnelService.list(new QueryWrapper<UserTunnel>().eq("status", 1).isNotNull("exp_time").lt("exp_time", new Date().getTime()));
|
||||
// 查询对应转发
|
||||
for (UserTunnel userTunnel : user_tunnel_list) {
|
||||
List<Forward> forwardList = forwardService.list(new QueryWrapper<Forward>().eq("tunnel_id", userTunnel.getTunnelId()).eq("user_id", userTunnel.getUserId()).eq("status", 1));
|
||||
for (Forward forward : forwardList) {
|
||||
|
||||
List<ChainTunnel> chainTunnels = chainTunnelService.list(new QueryWrapper<ChainTunnel>().eq("tunnel_id", forward.getTunnelId()).eq("chain_type", 1));
|
||||
for (ChainTunnel chainTunnel : chainTunnels) {
|
||||
String name = buildServiceName(forward.getId(), forward.getUserId(), userTunnel.getId());
|
||||
GostUtil.PauseAndResumeService(chainTunnel.getNodeId(), name, "PauseService");
|
||||
}
|
||||
forward.setStatus(0);
|
||||
forwardService.updateById(forward);
|
||||
}
|
||||
|
||||
|
||||
userTunnel.setStatus(0);
|
||||
userTunnelService.updateById(userTunnel);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private String buildServiceName(Long forwardId, Integer userId, Integer userTunnelId) {
|
||||
return forwardId + "_" + userId + "_" + userTunnelId;
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package com.admin.common.task;
|
||||
|
||||
|
||||
import com.admin.entity.StatisticsFlow;
|
||||
import com.admin.entity.User;
|
||||
import com.admin.service.StatisticsFlowService;
|
||||
import com.admin.service.UserService;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
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.PostConstruct;
|
||||
import javax.annotation.Resource;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@EnableScheduling
|
||||
public class StatisticsFlowAsync {
|
||||
|
||||
@Resource
|
||||
UserService userService;
|
||||
|
||||
@Resource
|
||||
StatisticsFlowService statisticsFlowService;
|
||||
|
||||
@Scheduled(cron = "0 0 * * * ?")
|
||||
public void statistics_flow() {
|
||||
LocalDateTime currentHour = LocalDateTime.now().withMinute(0).withSecond(0).withNano(0);
|
||||
String hourString = currentHour.format(DateTimeFormatter.ofPattern("HH:mm"));
|
||||
long time = new Date().getTime();
|
||||
|
||||
// 删除48小时前的数据
|
||||
long nowMs = new Date().getTime();
|
||||
long cutoffMs = nowMs - 48L * 60 * 60 * 1000;
|
||||
statisticsFlowService.remove(
|
||||
new LambdaQueryWrapper<StatisticsFlow>()
|
||||
.lt(StatisticsFlow::getCreatedTime, cutoffMs)
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
List<User> list = userService.list();
|
||||
List<StatisticsFlow> statisticsFlowList = new ArrayList<>();
|
||||
|
||||
for (User user : list) {
|
||||
long currentFlow = user.getInFlow() + user.getOutFlow();
|
||||
|
||||
// 从数据库获取上一次记录
|
||||
StatisticsFlow lastFlowRecord = statisticsFlowService.getOne(
|
||||
new LambdaQueryWrapper<StatisticsFlow>()
|
||||
.eq(StatisticsFlow::getUserId, user.getId())
|
||||
.orderByDesc(StatisticsFlow::getId)
|
||||
.last("LIMIT 1")
|
||||
);
|
||||
|
||||
long currentTotalFlow = currentFlow;
|
||||
long incrementFlow = currentTotalFlow;
|
||||
|
||||
if (lastFlowRecord != null) {
|
||||
long lastTotalFlow = lastFlowRecord.getTotalFlow();
|
||||
incrementFlow = currentTotalFlow - lastTotalFlow;
|
||||
|
||||
if (incrementFlow < 0) {
|
||||
incrementFlow = currentTotalFlow;
|
||||
}
|
||||
}
|
||||
|
||||
StatisticsFlow statisticsFlow = new StatisticsFlow();
|
||||
statisticsFlow.setUserId(user.getId());
|
||||
statisticsFlow.setFlow(incrementFlow);
|
||||
statisticsFlow.setTotalFlow(currentTotalFlow);
|
||||
statisticsFlow.setTime(hourString);
|
||||
statisticsFlow.setCreatedTime(time);
|
||||
|
||||
statisticsFlowList.add(statisticsFlow);
|
||||
}
|
||||
|
||||
statisticsFlowService.saveBatch(statisticsFlowList);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
package com.admin.common.utils;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* AES加密工具类
|
||||
* 使用AES-256-GCM模式,与Go端保持兼容
|
||||
*/
|
||||
@Slf4j
|
||||
public class AESCrypto {
|
||||
|
||||
private static final String ALGORITHM = "AES";
|
||||
private static final String TRANSFORMATION = "AES/GCM/NoPadding";
|
||||
private static final int GCM_IV_LENGTH = 12; // GCM推荐的IV长度
|
||||
private static final int GCM_TAG_LENGTH = 16; // GCM认证标签长度
|
||||
|
||||
private final SecretKeySpec secretKey;
|
||||
private final SecureRandom secureRandom;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* @param secret 密钥字符串,将使用SHA-256转换为32字节密钥
|
||||
*/
|
||||
public AESCrypto(String secret) {
|
||||
if (secret == null || secret.isEmpty()) {
|
||||
throw new IllegalArgumentException("密钥不能为空");
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用SHA-256将密码转换为32字节密钥,与Go端保持一致
|
||||
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
|
||||
byte[] keyBytes = sha256.digest(secret.getBytes(StandardCharsets.UTF_8));
|
||||
this.secretKey = new SecretKeySpec(keyBytes, ALGORITHM);
|
||||
this.secureRandom = new SecureRandom();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.info("AES加密器初始化失败", e);
|
||||
throw new RuntimeException("AES加密器初始化失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密数据
|
||||
* @param data 要加密的原始数据
|
||||
* @return Base64编码的加密数据,格式为: nonce + ciphertext
|
||||
*/
|
||||
public String encrypt(byte[] data) {
|
||||
if (data == null || data.length == 0) {
|
||||
throw new IllegalArgumentException("待加密数据不能为空");
|
||||
}
|
||||
|
||||
try {
|
||||
// 生成随机IV(nonce)
|
||||
byte[] iv = new byte[GCM_IV_LENGTH];
|
||||
secureRandom.nextBytes(iv);
|
||||
|
||||
// 创建GCM参数规范
|
||||
GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
|
||||
|
||||
// 初始化Cipher
|
||||
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmSpec);
|
||||
|
||||
// 加密数据
|
||||
byte[] ciphertext = cipher.doFinal(data);
|
||||
|
||||
// 组合IV + ciphertext,与Go端格式保持一致
|
||||
ByteBuffer buffer = ByteBuffer.allocate(iv.length + ciphertext.length);
|
||||
buffer.put(iv);
|
||||
buffer.put(ciphertext);
|
||||
|
||||
// 返回Base64编码结果
|
||||
return Base64.getEncoder().encodeToString(buffer.array());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.info("数据加密失败", e);
|
||||
throw new RuntimeException("数据加密失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密字符串
|
||||
* @param data 要加密的字符串
|
||||
* @return Base64编码的加密数据
|
||||
*/
|
||||
public String encrypt(String data) {
|
||||
if (data == null) {
|
||||
throw new IllegalArgumentException("待加密字符串不能为空");
|
||||
}
|
||||
return encrypt(data.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密数据
|
||||
* @param encryptedData Base64编码的加密数据
|
||||
* @return 解密后的原始数据
|
||||
*/
|
||||
public byte[] decrypt(String encryptedData) {
|
||||
if (encryptedData == null || encryptedData.isEmpty()) {
|
||||
throw new IllegalArgumentException("加密数据不能为空");
|
||||
}
|
||||
|
||||
try {
|
||||
// Base64解码
|
||||
byte[] encrypted = Base64.getDecoder().decode(encryptedData);
|
||||
|
||||
if (encrypted.length < GCM_IV_LENGTH) {
|
||||
throw new IllegalArgumentException("加密数据长度不足");
|
||||
}
|
||||
|
||||
// 分离IV和密文
|
||||
ByteBuffer buffer = ByteBuffer.wrap(encrypted);
|
||||
byte[] iv = new byte[GCM_IV_LENGTH];
|
||||
buffer.get(iv);
|
||||
|
||||
byte[] ciphertext = new byte[buffer.remaining()];
|
||||
buffer.get(ciphertext);
|
||||
|
||||
// 创建GCM参数规范
|
||||
GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
|
||||
|
||||
// 初始化Cipher
|
||||
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmSpec);
|
||||
|
||||
// 解密数据
|
||||
return cipher.doFinal(ciphertext);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.info("数据解密失败", e);
|
||||
throw new RuntimeException("数据解密失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密字符串
|
||||
* @param encryptedData Base64编码的加密数据
|
||||
* @return 解密后的字符串
|
||||
*/
|
||||
public String decryptString(String encryptedData) {
|
||||
byte[] decrypted = decrypt(encryptedData);
|
||||
return new String(decrypted, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建AES加密器实例
|
||||
* @param secret 密钥字符串
|
||||
* @return AES加密器实例,如果创建失败返回null
|
||||
*/
|
||||
public static AESCrypto create(String secret) {
|
||||
try {
|
||||
return new AESCrypto(secret);
|
||||
} catch (Exception e) {
|
||||
log.info("创建AES加密器失败: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,411 +0,0 @@
|
||||
package com.admin.common.utils;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.admin.common.dto.GostDto;
|
||||
import com.admin.entity.*;
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
public class GostUtil {
|
||||
|
||||
|
||||
public static GostDto AddLimiters(Long node_id, Long name, String speed) {
|
||||
JSONObject data = createLimiterData(name, speed);
|
||||
GostDto gostDto = WebSocketServer.send_msg(node_id, data, "AddLimiters");
|
||||
if (gostDto.getMsg().contains("exists")){
|
||||
gostDto.setMsg("OK");
|
||||
}
|
||||
return gostDto;
|
||||
}
|
||||
|
||||
public static GostDto UpdateLimiters(Long node_id, Long name, String speed) {
|
||||
JSONObject data = createLimiterData(name, speed);
|
||||
JSONObject req = new JSONObject();
|
||||
req.put("limiter", name + "");
|
||||
req.put("data", data);
|
||||
return WebSocketServer.send_msg(node_id, req, "UpdateLimiters");
|
||||
}
|
||||
|
||||
public static GostDto DeleteLimiters(Long node_id, Long name) {
|
||||
JSONObject req = new JSONObject();
|
||||
req.put("limiter", name + "");
|
||||
GostDto gostDto = WebSocketServer.send_msg(node_id, req, "DeleteLimiters");
|
||||
if (gostDto.getMsg().contains("not found")){
|
||||
gostDto.setMsg("OK");
|
||||
}
|
||||
return gostDto;
|
||||
}
|
||||
|
||||
public static GostDto AddChains(Long node_id, List<ChainTunnel> chainTunnels, Map<Long, Node> node_s) {
|
||||
JSONArray nodes = new JSONArray();
|
||||
Node fromNode = node_s.get(node_id);
|
||||
for (ChainTunnel chainTunnel : chainTunnels) {
|
||||
JSONObject dialer = new JSONObject();
|
||||
dialer.put("type", chainTunnel.getProtocol());
|
||||
|
||||
JSONObject connector = new JSONObject();
|
||||
connector.put("type", "relay");
|
||||
|
||||
Node node_info = node_s.get(chainTunnel.getNodeId());
|
||||
JSONObject node = new JSONObject();
|
||||
node.put("name", "node_" + chainTunnel.getInx());
|
||||
|
||||
String dialHost = (fromNode != null && node_info != null)
|
||||
? selectDialHost(fromNode, node_info)
|
||||
: (node_info != null ? node_info.getServerIp() : null);
|
||||
node.put("addr", processServerAddress(dialHost + ":" + chainTunnel.getPort()));
|
||||
node.put("connector", connector);
|
||||
node.put("dialer", dialer);
|
||||
|
||||
|
||||
|
||||
nodes.add(node);
|
||||
}
|
||||
JSONObject hop = new JSONObject();
|
||||
hop.put("name", "hop_" + chainTunnels.getFirst().getTunnelId());
|
||||
|
||||
// interface设置在转发链
|
||||
if (StringUtils.isNotBlank(node_s.get(node_id).getInterfaceName())) {
|
||||
hop.put("interface", node_s.get(node_id).getInterfaceName());
|
||||
}
|
||||
|
||||
|
||||
JSONObject selector = new JSONObject();
|
||||
selector.put("strategy", chainTunnels.getFirst().getStrategy());
|
||||
selector.put("maxFails", 1);
|
||||
selector.put("failTimeout", 600000000000L); // 600 秒(纳秒单位)
|
||||
|
||||
|
||||
hop.put("selector", selector);
|
||||
hop.put("nodes", nodes);
|
||||
|
||||
JSONArray hops = new JSONArray();
|
||||
hops.add(hop);
|
||||
|
||||
JSONObject data = new JSONObject();
|
||||
data.put("name", "chains_" + chainTunnels.getFirst().getTunnelId());
|
||||
data.put("hops", hops);
|
||||
|
||||
GostDto gostDto = WebSocketServer.send_msg(node_id, data, "AddChains");
|
||||
if (gostDto.getMsg().contains("exists")){
|
||||
gostDto.setMsg("OK");
|
||||
}
|
||||
return gostDto;
|
||||
}
|
||||
|
||||
public static GostDto DeleteChains(Long node_id, String name) {
|
||||
JSONObject data = new JSONObject();
|
||||
data.put("chain", name);
|
||||
GostDto gostDto = WebSocketServer.send_msg(node_id, data, "DeleteChains");
|
||||
if (gostDto.getMsg().contains("not found")){
|
||||
gostDto.setMsg("OK");
|
||||
}
|
||||
return gostDto;
|
||||
}
|
||||
|
||||
public static GostDto AddChainService(Long node_id, ChainTunnel chainTunnel, Map<Long, Node> node_s) {
|
||||
JSONArray services = new JSONArray();
|
||||
Node node_info = node_s.get(chainTunnel.getNodeId());
|
||||
JSONObject service_item = new JSONObject();
|
||||
service_item.put("name", chainTunnel.getTunnelId() + "_tls");
|
||||
service_item.put("addr", node_info.getTcpListenAddr() + ":" + chainTunnel.getPort());
|
||||
|
||||
// 只为出口节点(chainType=3)设置 interface
|
||||
if (chainTunnel.getChainType() == 3 && StringUtils.isNotBlank(node_s.get(node_id).getInterfaceName())) {
|
||||
JSONObject metadata = new JSONObject();
|
||||
metadata.put("interface", node_s.get(node_id).getInterfaceName());
|
||||
service_item.put("metadata", metadata);
|
||||
}
|
||||
|
||||
JSONObject handler = new JSONObject();
|
||||
handler.put("type", "relay");
|
||||
if (chainTunnel.getChainType() == 2){
|
||||
handler.put("chain","chains_" + chainTunnel.getTunnelId());
|
||||
}
|
||||
service_item.put("handler", handler);
|
||||
|
||||
JSONObject listener = new JSONObject();
|
||||
listener.put("type", chainTunnel.getProtocol());
|
||||
service_item.put("listener", listener);
|
||||
|
||||
services.add(service_item);
|
||||
|
||||
GostDto gostDto = WebSocketServer.send_msg(node_id, services, "AddService");
|
||||
if (gostDto.getMsg().contains("exists")){
|
||||
gostDto.setMsg("OK");
|
||||
}
|
||||
return gostDto;
|
||||
}
|
||||
|
||||
public static GostDto AddAndUpdateService(String name, Integer limiter, Node node, Forward forward, ForwardPort forwardPort, Tunnel tunnel, String meth) {
|
||||
JSONArray services = new JSONArray();
|
||||
String[] protocols = {"tcp", "udp"};
|
||||
for (String protocol : protocols) {
|
||||
JSONObject service = new JSONObject();
|
||||
service.put("name", name + "_" + protocol);
|
||||
if (Objects.equals(protocol, "tcp")){
|
||||
service.put("addr", node.getTcpListenAddr() + ":" + forwardPort.getPort());
|
||||
}else {
|
||||
service.put("addr", node.getUdpListenAddr() + ":" + forwardPort.getPort());
|
||||
}
|
||||
|
||||
// 只在端口转发时设置 interface(隧道转发时 interface 在转发链的节点上设置)
|
||||
if (tunnel.getType() == 1 && StringUtils.isNotBlank(node.getInterfaceName())) {
|
||||
JSONObject metadata = new JSONObject();
|
||||
metadata.put("interface", node.getInterfaceName());
|
||||
service.put("metadata", metadata);
|
||||
}
|
||||
|
||||
// 添加限流器配置
|
||||
if (limiter != null) {
|
||||
service.put("limiter", limiter.toString());
|
||||
}
|
||||
|
||||
// 配置处理器
|
||||
JSONObject handler = new JSONObject();
|
||||
handler.put("type", protocol);
|
||||
if (tunnel.getType() == 2){
|
||||
handler.put("chain", "chains_" + forward.getTunnelId());
|
||||
}
|
||||
service.put("handler", handler);
|
||||
|
||||
// 配置监听器
|
||||
JSONObject listener = createListener(protocol);
|
||||
service.put("listener", listener);
|
||||
|
||||
JSONObject forwarder = createForwarder(forward.getRemoteAddr(), forward.getStrategy());
|
||||
service.put("forwarder", forwarder);
|
||||
|
||||
services.add(service);
|
||||
}
|
||||
GostDto gostDto = WebSocketServer.send_msg(node.getId(), services, meth);
|
||||
if (gostDto.getMsg().contains("exists")){
|
||||
gostDto.setMsg("OK");
|
||||
}
|
||||
return gostDto;
|
||||
}
|
||||
|
||||
public static GostDto DeleteService(Long node_id, JSONArray services) {
|
||||
JSONObject data = new JSONObject();
|
||||
data.put("services", services);
|
||||
GostDto gostDto = WebSocketServer.send_msg(node_id, data, "DeleteService");
|
||||
if (gostDto.getMsg().contains("not found")){
|
||||
gostDto.setMsg("OK");
|
||||
}
|
||||
return gostDto;
|
||||
}
|
||||
|
||||
public static GostDto PauseAndResumeService(Long node_id, String name, String meth) {
|
||||
JSONObject data = new JSONObject();
|
||||
JSONArray services = new JSONArray();
|
||||
services.add(name + "_tcp");
|
||||
services.add(name + "_udp");
|
||||
data.put("services", services);
|
||||
return WebSocketServer.send_msg(node_id, data, meth);
|
||||
}
|
||||
|
||||
|
||||
private static JSONObject createLimiterData(Long name, String speed) {
|
||||
JSONObject data = new JSONObject();
|
||||
data.put("name", name.toString());
|
||||
JSONArray limits = new JSONArray();
|
||||
limits.add("$ " + speed + "MB " + speed + "MB");
|
||||
data.put("limits", limits);
|
||||
return data;
|
||||
}
|
||||
|
||||
private static JSONObject createListener(String protocol) {
|
||||
JSONObject listener = new JSONObject();
|
||||
listener.put("type", protocol);
|
||||
if (Objects.equals(protocol, "udp")) {
|
||||
JSONObject metadata = new JSONObject();
|
||||
metadata.put("keepAlive", true);
|
||||
listener.put("metadata", metadata);
|
||||
}
|
||||
return listener;
|
||||
}
|
||||
|
||||
private static JSONObject createForwarder(String remoteAddr, String strategy) {
|
||||
JSONObject forwarder = new JSONObject();
|
||||
JSONArray nodes = new JSONArray();
|
||||
|
||||
String[] split = remoteAddr.split(",");
|
||||
int num = 1;
|
||||
for (String addr : split) {
|
||||
JSONObject node = new JSONObject();
|
||||
node.put("name", "node_" + num);
|
||||
node.put("addr", addr);
|
||||
nodes.add(node);
|
||||
num++;
|
||||
}
|
||||
|
||||
if (strategy == null || strategy.isEmpty()) {
|
||||
strategy = "fifo";
|
||||
}
|
||||
|
||||
forwarder.put("nodes", nodes);
|
||||
|
||||
JSONObject selector = new JSONObject();
|
||||
selector.put("strategy", strategy);
|
||||
selector.put("maxFails", 1);
|
||||
selector.put("failTimeout", "600s");
|
||||
forwarder.put("selector", selector);
|
||||
return forwarder;
|
||||
}
|
||||
|
||||
public static String processServerAddress(String serverAddr) {
|
||||
if (StrUtil.isBlank(serverAddr)) {
|
||||
return serverAddr;
|
||||
}
|
||||
|
||||
// 如果已经被方括号包裹,直接返回
|
||||
if (serverAddr.startsWith("[")) {
|
||||
return serverAddr;
|
||||
}
|
||||
|
||||
// 查找最后一个冒号,分离主机和端口
|
||||
int lastColonIndex = serverAddr.lastIndexOf(':');
|
||||
if (lastColonIndex == -1) {
|
||||
// 没有端口号,直接检查是否需要包裹
|
||||
return isIPv6Address(serverAddr) ? "[" + serverAddr + "]" : serverAddr;
|
||||
}
|
||||
|
||||
String host = serverAddr.substring(0, lastColonIndex);
|
||||
String port = serverAddr.substring(lastColonIndex);
|
||||
|
||||
// 检查主机部分是否为IPv6地址
|
||||
if (isIPv6Address(host)) {
|
||||
return "[" + host + "]" + port;
|
||||
}
|
||||
|
||||
return serverAddr;
|
||||
}
|
||||
|
||||
private static boolean isIPv6Address(String address) {
|
||||
// IPv6地址包含多个冒号,至少2个
|
||||
if (!address.contains(":")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 计算冒号数量,IPv6地址至少有2个冒号
|
||||
long colonCount = address.chars().filter(ch -> ch == ':').count();
|
||||
return colonCount >= 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* v4 优先:当两端都有 v4 时选择 v4,否则尝试 v6。
|
||||
* 用于节点之间建立链路(A -> B 需要选择 B 的地址族,且 A 需要支持该地址族)。
|
||||
*/
|
||||
public static String selectDialHost(Node fromNode, Node toNode) {
|
||||
if (fromNode == null || toNode == null) {
|
||||
throw new IllegalArgumentException("node is null");
|
||||
}
|
||||
|
||||
boolean fromV4 = supportsV4(fromNode);
|
||||
boolean fromV6 = supportsV6(fromNode);
|
||||
boolean toV4 = supportsV4(toNode);
|
||||
boolean toV6 = supportsV6(toNode);
|
||||
|
||||
if (fromV4 && toV4) {
|
||||
return pickToAddressV4(toNode);
|
||||
}
|
||||
if (fromV6 && toV6) {
|
||||
return pickToAddressV6(toNode);
|
||||
}
|
||||
|
||||
throw new RuntimeException(
|
||||
"节点链路不兼容:" + safeName(fromNode) + "(v4=" + fromV4 + ",v6=" + fromV6 + ") -> "
|
||||
+ safeName(toNode) + "(v4=" + toV4 + ",v6=" + toV6 + ")"
|
||||
);
|
||||
}
|
||||
|
||||
private static String safeName(Node node) {
|
||||
if (node.getName() == null || node.getName().isBlank()) {
|
||||
return "node_" + node.getId();
|
||||
}
|
||||
return node.getName();
|
||||
}
|
||||
|
||||
private static boolean supportsV4(Node node) {
|
||||
// New dual-stack fields take precedence over legacy serverIp.
|
||||
// If user explicitly provided only v6, treat as v6-only.
|
||||
if (StrUtil.isNotBlank(node.getServerIpV4())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (StrUtil.isNotBlank(node.getServerIpV6())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String legacy = node.getServerIp();
|
||||
if (StrUtil.isBlank(legacy)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
legacy = legacy.trim();
|
||||
if (looksLikeIpv4(legacy)) {
|
||||
return true;
|
||||
}
|
||||
if (isIPv6Address(legacy)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 域名/其它:无法判断,按双栈处理以保持兼容
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean supportsV6(Node node) {
|
||||
// New dual-stack fields take precedence over legacy serverIp.
|
||||
// If user explicitly provided only v4, treat as v4-only.
|
||||
if (StrUtil.isNotBlank(node.getServerIpV6())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (StrUtil.isNotBlank(node.getServerIpV4())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String legacy = node.getServerIp();
|
||||
if (StrUtil.isBlank(legacy)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
legacy = legacy.trim();
|
||||
if (isIPv6Address(legacy)) {
|
||||
return true;
|
||||
}
|
||||
if (looksLikeIpv4(legacy)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 域名/其它:无法判断,按双栈处理以保持兼容
|
||||
return true;
|
||||
}
|
||||
|
||||
private static String pickToAddressV4(Node toNode) {
|
||||
if (StrUtil.isNotBlank(toNode.getServerIpV4())) {
|
||||
return toNode.getServerIpV4().trim();
|
||||
}
|
||||
String legacy = toNode.getServerIp();
|
||||
return legacy != null ? legacy.trim() : null;
|
||||
}
|
||||
|
||||
private static String pickToAddressV6(Node toNode) {
|
||||
if (StrUtil.isNotBlank(toNode.getServerIpV6())) {
|
||||
return toNode.getServerIpV6().trim();
|
||||
}
|
||||
String legacy = toNode.getServerIp();
|
||||
return legacy != null ? legacy.trim() : null;
|
||||
}
|
||||
|
||||
private static boolean looksLikeIpv4(String value) {
|
||||
Pattern ipv4 = Pattern.compile("^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$");
|
||||
return ipv4.matcher(value).matches();
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package com.admin.common.utils;
|
||||
|
||||
import com.admin.common.dto.GostConfigDto;
|
||||
import com.admin.common.dto.GostDto;
|
||||
import com.admin.config.RestTemplateConfig;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import lombok.SneakyThrows;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.stereotype.Component;
|
||||
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格式的请求体
|
||||
*/
|
||||
@Component
|
||||
public class HttpUtils{
|
||||
|
||||
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,194 +0,0 @@
|
||||
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 = 90L * 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.getUser());
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,496 +0,0 @@
|
||||
package com.admin.common.utils;
|
||||
|
||||
|
||||
import com.admin.common.dto.GostConfigDto;
|
||||
import com.admin.common.dto.GostDto;
|
||||
import com.admin.common.task.CheckGostConfigAsync;
|
||||
import com.admin.entity.Node;
|
||||
import com.admin.service.NodeService;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
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.CompletableFuture;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.UUID;
|
||||
|
||||
|
||||
@Slf4j
|
||||
public class WebSocketServer extends TextWebSocketHandler {
|
||||
|
||||
@Resource
|
||||
NodeService nodeService;
|
||||
|
||||
// 存储所有活跃的 WebSocket 连接(
|
||||
private static final CopyOnWriteArraySet<WebSocketSession> activeSessions = new CopyOnWriteArraySet<>();
|
||||
|
||||
// 存储节点ID和对应的WebSocket session映射
|
||||
private static final ConcurrentHashMap<Long, WebSocketSession> nodeSessions = new ConcurrentHashMap<>();
|
||||
|
||||
// 为每个session提供锁对象,防止并发发送消息
|
||||
private static final ConcurrentHashMap<String, Object> sessionLocks = new ConcurrentHashMap<>();
|
||||
|
||||
// 存储等待响应的请求,key为requestId,value为CompletableFuture
|
||||
private static final ConcurrentHashMap<String, CompletableFuture<GostDto>> pendingRequests = new ConcurrentHashMap<>();
|
||||
|
||||
// 缓存加密器实例,避免重复创建
|
||||
private static final ConcurrentHashMap<String, AESCrypto> cryptoCache = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 加密消息包装器
|
||||
*/
|
||||
public static class EncryptedMessage {
|
||||
private boolean encrypted;
|
||||
private String data;
|
||||
private Long timestamp;
|
||||
|
||||
// getters and setters
|
||||
public boolean isEncrypted() { return encrypted; }
|
||||
public void setEncrypted(boolean encrypted) { this.encrypted = encrypted; }
|
||||
public String getData() { return data; }
|
||||
public void setData(String data) { this.data = data; }
|
||||
public Long getTimestamp() { return timestamp; }
|
||||
public void setTimestamp(Long timestamp) { this.timestamp = timestamp; }
|
||||
}
|
||||
|
||||
//接受客户端消息
|
||||
@Override
|
||||
public void handleTextMessage(WebSocketSession session, TextMessage message) {
|
||||
try {
|
||||
if (StringUtils.isNoneBlank(message.getPayload())) {
|
||||
|
||||
String id = session.getAttributes().get("id").toString();
|
||||
String type = session.getAttributes().get("type").toString();
|
||||
String nodeSecret = (String) session.getAttributes().get("nodeSecret");
|
||||
|
||||
// 尝试解密消息
|
||||
String decryptedPayload = decryptMessageIfNeeded(message.getPayload(), nodeSecret);
|
||||
|
||||
if (decryptedPayload.contains("memory_usage")){
|
||||
// 先发送确认消息
|
||||
sendToUser(session, "{\"type\":\"call\"}", nodeSecret);
|
||||
}else if (decryptedPayload.contains("requestId")) {
|
||||
log.info("收到消息: {}", decryptedPayload);
|
||||
// 处理命令响应消息
|
||||
try {
|
||||
JSONObject responseJson = JSONObject.parseObject(decryptedPayload);
|
||||
String requestId = responseJson.getString("requestId");
|
||||
String responseMessage = responseJson.getString("message");
|
||||
String responseType = responseJson.getString("type");
|
||||
JSONObject responseData = responseJson.getJSONObject("data");
|
||||
|
||||
if (requestId != null) {
|
||||
CompletableFuture<GostDto> future = pendingRequests.remove(requestId);
|
||||
|
||||
if (future != null) {
|
||||
GostDto result = new GostDto();
|
||||
|
||||
// 根据响应类型处理不同的数据
|
||||
if ("PingResponse".equals(responseType) && responseData != null) {
|
||||
// 特殊处理ping响应,将完整的响应数据返回
|
||||
result.setMsg(responseMessage != null ? responseMessage : "OK");
|
||||
result.setData(responseData); // 保存ping详细结果
|
||||
} else {
|
||||
// 其他类型的响应
|
||||
result.setMsg(responseMessage != null ? responseMessage : "无响应消息");
|
||||
if (responseData != null) {
|
||||
result.setData(responseData);
|
||||
}
|
||||
}
|
||||
|
||||
future.complete(result);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.info("处理响应消息失败: {}", e.getMessage(), e);
|
||||
}
|
||||
} else {
|
||||
log.info("收到消息: {}", decryptedPayload);
|
||||
}
|
||||
|
||||
// 如果是节点类型,转发消息给其他会话
|
||||
if (Objects.equals(type, "1")) {
|
||||
JSONObject jsonObject = new JSONObject();
|
||||
jsonObject.put("id", id);
|
||||
jsonObject.put("type", "info");
|
||||
jsonObject.put("data", decryptedPayload);
|
||||
String broadcastMessage = jsonObject.toJSONString();
|
||||
|
||||
// 异步处理广播消息,避免阻塞当前线程
|
||||
for (WebSocketSession targetSession : activeSessions) {
|
||||
if (targetSession != null && targetSession.isOpen() && !targetSession.equals(session)) {
|
||||
sendToUser(targetSession, broadcastMessage, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.info("处理WebSocket消息时发生异常: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试解密消息(如果需要)
|
||||
*/
|
||||
private String decryptMessageIfNeeded(String payload, String nodeSecret) {
|
||||
if (payload == null || payload.trim().isEmpty()) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
try {
|
||||
// 尝试解析为加密消息格式
|
||||
EncryptedMessage encryptedMessage = JSON.parseObject(payload, EncryptedMessage.class);
|
||||
|
||||
if (encryptedMessage.isEncrypted() && encryptedMessage.getData() != null) {
|
||||
// 获取或创建加密器
|
||||
AESCrypto crypto = getOrCreateCrypto(nodeSecret);
|
||||
if (crypto == null) {
|
||||
log.info("⚠️ 收到加密消息但无法创建解密器,使用原始数据");
|
||||
return payload;
|
||||
}
|
||||
|
||||
// 解密数据
|
||||
String decryptedData = crypto.decryptString(encryptedMessage.getData());
|
||||
return decryptedData;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 解析失败,可能是非加密格式,直接返回原始数据
|
||||
log.info("WebSocket消息未加密或解密失败,使用原始数据: {}", e.getMessage());
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密消息(如果可能)
|
||||
*/
|
||||
private static String encryptMessageIfPossible(String message, String nodeSecret) {
|
||||
if (message == null || nodeSecret == null) {
|
||||
return message;
|
||||
}
|
||||
|
||||
try {
|
||||
AESCrypto crypto = getOrCreateCrypto(nodeSecret);
|
||||
if (crypto != null) {
|
||||
String encryptedData = crypto.encrypt(message);
|
||||
|
||||
// 创建加密消息包装器
|
||||
JSONObject encryptedMessage = new JSONObject();
|
||||
encryptedMessage.put("encrypted", true);
|
||||
encryptedMessage.put("data", encryptedData);
|
||||
encryptedMessage.put("timestamp", System.currentTimeMillis());
|
||||
|
||||
return encryptedMessage.toJSONString();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.info("⚠️ WebSocket消息加密失败,发送原始数据: {}", e.getMessage());
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取或创建加密器实例
|
||||
*/
|
||||
private static AESCrypto getOrCreateCrypto(String secret) {
|
||||
if (secret == null || secret.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return cryptoCache.computeIfAbsent(secret, AESCrypto::create);
|
||||
}
|
||||
|
||||
// 建立连接
|
||||
@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);
|
||||
log.info("管理员连接建立,sessionId: {}", session.getId());
|
||||
} else {
|
||||
// 客户端节点连接
|
||||
Long nodeId = Long.valueOf(id);
|
||||
String version = (String) session.getAttributes().get("nodeVersion");
|
||||
String http = (String) session.getAttributes().get("http");
|
||||
String tls = (String) session.getAttributes().get("tls");
|
||||
String socks = (String) session.getAttributes().get("socks");
|
||||
|
||||
log.info("节点 {} 尝试连接,开始处理连接逻辑", nodeId);
|
||||
log.info("屏蔽协议数据:http->{}, tls->{}, socks->{}", http, tls, socks);
|
||||
|
||||
// 检查是否已有该节点的连接,如果有则记录日志但直接覆盖
|
||||
WebSocketSession existingSession = nodeSessions.get(nodeId);
|
||||
if (existingSession != null && existingSession.isOpen()) {
|
||||
log.info("节点 {} 已有连接存在: {},新连接将覆盖旧连接", nodeId, existingSession.getId());
|
||||
// 清理旧连接的锁对象
|
||||
sessionLocks.remove(existingSession.getId());
|
||||
}
|
||||
|
||||
// 直接覆盖会话映射(不主动关闭旧连接,让它自然断开)
|
||||
nodeSessions.put(nodeId, session);
|
||||
|
||||
// 如果有旧连接,在覆盖映射后主动关闭它
|
||||
if (existingSession != null && existingSession.isOpen()) {
|
||||
try {
|
||||
log.info("主动关闭节点 {} 的旧连接: {}", nodeId, existingSession.getId());
|
||||
existingSession.close();
|
||||
} catch (Exception e) {
|
||||
log.info("关闭节点 {} 旧连接失败: {}", nodeId, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 更新节点状态为在线
|
||||
Node node = nodeService.getById(nodeId);
|
||||
if (node != null) {
|
||||
// 更新状态和版本信息
|
||||
node.setStatus(1);
|
||||
if (version != null) {
|
||||
node.setVersion(version);
|
||||
}
|
||||
if (http != null) {
|
||||
node.setHttp(Integer.parseInt(http));
|
||||
}
|
||||
if (tls != null) {
|
||||
node.setTls(Integer.parseInt(tls));
|
||||
}
|
||||
if (socks != null) {
|
||||
node.setSocks(Integer.parseInt(socks));
|
||||
}
|
||||
|
||||
boolean updateResult = nodeService.updateById(node);
|
||||
|
||||
if (updateResult) {
|
||||
log.info("节点 {} 连接建立成功,状态更新为在线,版本: {}", nodeId, version);
|
||||
|
||||
// 广播节点上线状态给所有管理员
|
||||
JSONObject res = new JSONObject();
|
||||
res.put("id", id);
|
||||
res.put("type", "status");
|
||||
res.put("data", 1);
|
||||
broadcastMessage(res.toJSONString());
|
||||
} else {
|
||||
log.info("节点 {} 状态更新失败", nodeId);
|
||||
}
|
||||
} else {
|
||||
log.info("节点 {} 不存在,无法更新状态", nodeId);
|
||||
// 移除无效的会话
|
||||
nodeSessions.remove(nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.info("建立连接时发生异常: {}", e.getMessage(), e);
|
||||
// 异常情况下,确保清理会话
|
||||
try {
|
||||
String id = session.getAttributes().get("id").toString();
|
||||
String type = session.getAttributes().get("type").toString();
|
||||
if (Objects.equals(type, "1")) {
|
||||
Long nodeId = Long.valueOf(id);
|
||||
nodeSessions.remove(nodeId);
|
||||
log.info("由于异常,移除节点 {} 的会话", nodeId);
|
||||
}
|
||||
} catch (Exception cleanupException) {
|
||||
log.info("清理异常会话时出错: {}", cleanupException.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 连接关闭后
|
||||
@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();
|
||||
|
||||
log.info("连接关闭,ID: {}, 类型: {}, 状态: {}", id, type, status);
|
||||
|
||||
if (!Objects.equals(type, "1")) {
|
||||
// 管理员连接关闭
|
||||
boolean removed = activeSessions.remove(session);
|
||||
log.info("管理员连接关闭,sessionId: {}, 移除结果: {}", sessionId, removed);
|
||||
} else {
|
||||
// 客户端节点连接关闭
|
||||
Long nodeId = Long.valueOf(id);
|
||||
|
||||
// 验证当前会话是否还是活跃会话(关键:这里会自动过滤掉被覆盖的旧连接)
|
||||
WebSocketSession currentSession = nodeSessions.get(nodeId);
|
||||
if (currentSession == null || !currentSession.equals(session)) {
|
||||
log.info("节点 {} 连接关闭,但已有新连接或会话不匹配,跳过状态更新", nodeId);
|
||||
sessionLocks.remove(sessionId);
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("节点 {} 当前活跃连接关闭,开始验证并更新状态", nodeId);
|
||||
|
||||
nodeSessions.remove(nodeId);
|
||||
|
||||
// 更新节点状态为离线
|
||||
Node node = nodeService.getById(nodeId);
|
||||
if (node != null) {
|
||||
node.setStatus(0);
|
||||
boolean updateResult = nodeService.updateById(node);
|
||||
|
||||
if (updateResult) {
|
||||
log.info("节点 {} 状态更新为离线成功", nodeId);
|
||||
|
||||
JSONObject res = new JSONObject();
|
||||
res.put("id", id);
|
||||
res.put("type", "status");
|
||||
res.put("data", 0);
|
||||
broadcastMessage(res.toJSONString());
|
||||
} else {
|
||||
log.info("节点 {} 状态更新为离线失败", nodeId);
|
||||
}
|
||||
} else {
|
||||
log.info("节点 {} 不存在,无法更新离线状态", nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
// 清理session锁对象
|
||||
sessionLocks.remove(sessionId);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.info("关闭连接时发生异常: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// 点对点发送消息
|
||||
@SneakyThrows
|
||||
public static void sendToUser(WebSocketSession socketSession, String message) {
|
||||
sendToUser(socketSession, message, null);
|
||||
}
|
||||
|
||||
// 点对点发送消息(支持加密)
|
||||
@SneakyThrows
|
||||
public static void sendToUser(WebSocketSession socketSession, String message, String nodeSecret) {
|
||||
if (socketSession != null && socketSession.isOpen()) {
|
||||
String sessionId = socketSession.getId();
|
||||
Object lock = sessionLocks.computeIfAbsent(sessionId, k -> new Object());
|
||||
|
||||
synchronized (lock) {
|
||||
try {
|
||||
if (socketSession.isOpen()) {
|
||||
// 如果是节点连接且有密钥,尝试加密消息
|
||||
String finalMessage = message;
|
||||
if (nodeSecret != null && !nodeSecret.isEmpty()) {
|
||||
String type = (String) socketSession.getAttributes().get("type");
|
||||
if ("1".equals(type)) { // 节点连接
|
||||
finalMessage = encryptMessageIfPossible(message, nodeSecret);
|
||||
}
|
||||
}
|
||||
socketSession.sendMessage(new TextMessage(finalMessage));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.info("发送WebSocket消息失败 [sessionId={}]: {}", sessionId, e.getMessage());
|
||||
cleanupSession(socketSession);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cleanupSession(socketSession);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理失效的session,自动识别是节点session还是管理员session
|
||||
*/
|
||||
private static void cleanupSession(WebSocketSession session) {
|
||||
if (session == null) return;
|
||||
|
||||
String sessionId = session.getId();
|
||||
|
||||
// 清理session锁
|
||||
sessionLocks.remove(sessionId);
|
||||
|
||||
boolean removedFromAdmin = activeSessions.remove(session);
|
||||
|
||||
if (!removedFromAdmin) {
|
||||
nodeSessions.entrySet().removeIf(entry -> {
|
||||
if (entry.getValue() == session) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 广播消息
|
||||
public static void broadcastMessage(String message) {
|
||||
for (WebSocketSession session : activeSessions) {
|
||||
sendToUser(session, message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static GostDto send_msg(Long node_id, Object msg, String type) {
|
||||
WebSocketSession nodeSession = nodeSessions.get(node_id);
|
||||
|
||||
if (nodeSession == null) {
|
||||
log.info("发送消息失败:节点 {} 不在线或会话不存在", node_id);
|
||||
GostDto result = new GostDto();
|
||||
result.setMsg("节点不在线");
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!nodeSession.isOpen()) {
|
||||
log.info("发送消息失败:节点 {} 连接已断开,清理会话", node_id);
|
||||
nodeSessions.remove(node_id);
|
||||
sessionLocks.remove(nodeSession.getId());
|
||||
GostDto result = new GostDto();
|
||||
result.setMsg("节点连接已断开");
|
||||
return result;
|
||||
}
|
||||
|
||||
// 生成唯一的请求ID
|
||||
String requestId = UUID.randomUUID().toString();
|
||||
|
||||
// 创建CompletableFuture用于等待响应
|
||||
CompletableFuture<GostDto> future = new CompletableFuture<>();
|
||||
pendingRequests.put(requestId, future);
|
||||
|
||||
// 获取节点密钥用于加密
|
||||
String nodeSecret = (String) nodeSession.getAttributes().get("nodeSecret");
|
||||
|
||||
try {
|
||||
JSONObject data = new JSONObject();
|
||||
data.put("type", type);
|
||||
data.put("data", msg);
|
||||
data.put("requestId", requestId);
|
||||
sendToUser(nodeSession, data.toJSONString(), nodeSecret);
|
||||
GostDto result = future.get(10, TimeUnit.SECONDS);
|
||||
|
||||
log.info("成功发送消息到节点 {} 并收到响应: {}", node_id, result.getMsg());
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
// 清理请求和映射关系
|
||||
pendingRequests.remove(requestId);
|
||||
|
||||
GostDto result = new GostDto();
|
||||
if (e instanceof java.util.concurrent.TimeoutException) {
|
||||
result.setMsg("等待响应超时");
|
||||
log.info("节点 {} 响应超时,可能存在连接问题", node_id);
|
||||
} else {
|
||||
result.setMsg("发送消息失败: " + e.getMessage());
|
||||
log.info("发送消息到节点 {} 失败: {}", node_id, e.getMessage(), e);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package com.admin.config;
|
||||
|
||||
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
|
||||
import cloud.tianai.captcha.resource.FontCache;
|
||||
import cloud.tianai.captcha.resource.ResourceStore;
|
||||
import cloud.tianai.captcha.resource.common.model.dto.Resource;
|
||||
import cloud.tianai.captcha.resource.common.model.dto.ResourceMap;
|
||||
import cloud.tianai.captcha.resource.impl.provider.ClassPathResourceProvider;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
import static cloud.tianai.captcha.common.constant.CommonConstant.DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class CaptchaResourceConfiguration {
|
||||
|
||||
private final ResourceStore resourceStore;
|
||||
|
||||
|
||||
private static final String[] OPTIONS = {
|
||||
"SLIDER", "WORD_IMAGE_CLICK", "ROTATE", "CONCAT"
|
||||
};
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
// 添加自定义背景图片
|
||||
for (int i = 1; i <= 10; i++) {
|
||||
ResourceMap template = new ResourceMap("default", 4);
|
||||
template.put("active.png", new Resource(ClassPathResourceProvider.NAME, "slide/" + i + "/1.png"));
|
||||
template.put("fixed.png", new Resource(ClassPathResourceProvider.NAME, "slide/" + i + "/2.png"));
|
||||
resourceStore.addTemplate(CaptchaTypeConstant.SLIDER, template);
|
||||
}
|
||||
|
||||
|
||||
// 添加自定义背景图片
|
||||
for (String option : OPTIONS) {
|
||||
for (int i = 1; i <= 26; i++) {
|
||||
resourceStore.addResource(option, new Resource("classpath", "bgimages/" + i + ".jpg", "default"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//添加自定义字体
|
||||
resourceStore.addResource(FontCache.FONT_TYPE, new Resource("classpath", "fonts/SIMSUN.TTC", "default"));
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package com.admin.config;
|
||||
|
||||
import com.admin.common.utils.AESCrypto;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 加密配置管理类
|
||||
* 统一管理AES加密功能,为HTTP上报和WebSocket通信提供加密支持
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class EncryptionConfig {
|
||||
|
||||
// 缓存加密器实例,避免重复创建
|
||||
private static final ConcurrentHashMap<String, AESCrypto> CRYPTO_CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 获取或创建AES加密器实例
|
||||
*
|
||||
* @param secret 密钥字符串
|
||||
* @return AES加密器实例,如果创建失败返回null
|
||||
*/
|
||||
public static AESCrypto getOrCreateCrypto(String secret) {
|
||||
if (secret == null || secret.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return CRYPTO_CACHE.computeIfAbsent(secret, AESCrypto::create);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测消息是否为加密格式
|
||||
*
|
||||
* @param message 消息内容
|
||||
* @return 如果是加密格式返回true,否则返回false
|
||||
*/
|
||||
public static boolean isEncryptedMessage(String message) {
|
||||
if (message == null || message.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 简单检查是否包含加密标识
|
||||
return message.contains("\"encrypted\":true") || message.contains("\"encrypted\": true");
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理缓存的加密器实例
|
||||
*
|
||||
* @param secret 要清理的密钥
|
||||
*/
|
||||
public static void clearCrypto(String secret) {
|
||||
if (secret != null) {
|
||||
CRYPTO_CACHE.remove(secret);
|
||||
log.info("已清理密钥对应的加密器实例");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理所有缓存的加密器实例
|
||||
*/
|
||||
public static void clearAllCrypto() {
|
||||
int size = CRYPTO_CACHE.size();
|
||||
CRYPTO_CACHE.clear();
|
||||
log.info("已清理所有加密器实例缓存,共清理 {} 个实例", size);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前缓存的加密器数量
|
||||
*
|
||||
* @return 缓存的加密器数量
|
||||
*/
|
||||
public static int getCacheSize() {
|
||||
return CRYPTO_CACHE.size();
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package com.admin.config;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
|
||||
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();
|
||||
// 配置分页插件,指定数据库类型为 SQLite
|
||||
PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.SQLITE);
|
||||
interceptor.addInnerInterceptor(paginationInterceptor); // 分页插件
|
||||
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); // 防止全表更新插件
|
||||
return interceptor;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ConfigurationCustomizer configurationCustomizer() {
|
||||
return configuration -> configuration.setUseDeprecatedExecutor(false);
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
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);
|
||||
}
|
||||
|
||||
// 10秒超时配置
|
||||
private static final int TIMEOUT_SECONDS = 10;
|
||||
private static final int TIMEOUT_MILLISECONDS = TIMEOUT_SECONDS * 1000;
|
||||
|
||||
|
||||
@Bean
|
||||
public ClientHttpRequestFactory simpleClientHttpRequestFactory(){
|
||||
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
|
||||
factory.setConnectTimeout(TIMEOUT_MILLISECONDS);
|
||||
factory.setReadTimeout(TIMEOUT_MILLISECONDS);
|
||||
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);
|
||||
// 添加超时配置
|
||||
factory.setConnectTimeout(TIMEOUT_MILLISECONDS);
|
||||
factory.setReadTimeout(TIMEOUT_MILLISECONDS);
|
||||
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);
|
||||
// 添加超时配置
|
||||
factory.setConnectTimeout(TIMEOUT_MILLISECONDS);
|
||||
factory.setReadTimeout(TIMEOUT_MILLISECONDS);
|
||||
return factory;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
package com.admin.config;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.PreDestroy;
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.Statement;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* SQLite 数据库配置
|
||||
* 启用 WAL (Write-Ahead Logging) 模式以提高并发性能
|
||||
* 添加定期 checkpoint 和优雅关闭处理
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@EnableScheduling
|
||||
public class SQLiteConfig implements ApplicationRunner {
|
||||
|
||||
private final DataSource dataSource;
|
||||
|
||||
public SQLiteConfig(DataSource dataSource) {
|
||||
this.dataSource = dataSource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) throws Exception {
|
||||
try (Connection connection = dataSource.getConnection();
|
||||
Statement statement = connection.createStatement()) {
|
||||
|
||||
statement.execute("PRAGMA journal_mode=WAL;");
|
||||
statement.execute("PRAGMA synchronous=NORMAL;");
|
||||
statement.execute("PRAGMA cache_size=-64000;"); // 64MB 缓存
|
||||
statement.execute("PRAGMA temp_store=MEMORY;");
|
||||
statement.execute("PRAGMA busy_timeout=5000;"); // 5秒超时
|
||||
statement.execute("PRAGMA wal_autocheckpoint=1000;"); // 每1000页自动checkpoint
|
||||
|
||||
ensureNodeDualStackColumns(connection);
|
||||
|
||||
log.info("SQLite WAL mode configured successfully");
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to configure SQLite database", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureNodeDualStackColumns(Connection connection) throws Exception {
|
||||
Set<String> cols = getTableColumns(connection, "node");
|
||||
if (cols.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ensureColumnIfMissing(connection, cols, "node", "server_ip_v4", "VARCHAR(100)");
|
||||
ensureColumnIfMissing(connection, cols, "node", "server_ip_v6", "VARCHAR(100)");
|
||||
|
||||
backfillNodeDualStackColumns(connection);
|
||||
}
|
||||
|
||||
private void backfillNodeDualStackColumns(Connection connection) throws Exception {
|
||||
Pattern ipv4 = Pattern.compile("^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$");
|
||||
|
||||
try (Statement statement = connection.createStatement();
|
||||
ResultSet rs = statement.executeQuery("SELECT id, server_ip, server_ip_v4, server_ip_v6 FROM node;");
|
||||
PreparedStatement updV4 = connection.prepareStatement("UPDATE node SET server_ip_v4 = ? WHERE id = ?;");
|
||||
PreparedStatement updV6 = connection.prepareStatement("UPDATE node SET server_ip_v6 = ? WHERE id = ?;")
|
||||
) {
|
||||
while (rs.next()) {
|
||||
long id = rs.getLong("id");
|
||||
String serverIp = rs.getString("server_ip");
|
||||
String v4 = rs.getString("server_ip_v4");
|
||||
String v6 = rs.getString("server_ip_v6");
|
||||
|
||||
if (serverIp == null || serverIp.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
if ((v4 != null && !v4.isBlank()) || (v6 != null && !v6.isBlank())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String trimmed = serverIp.trim();
|
||||
|
||||
if (ipv4.matcher(trimmed).matches()) {
|
||||
updV4.setString(1, trimmed);
|
||||
updV4.setLong(2, id);
|
||||
updV4.executeUpdate();
|
||||
} else {
|
||||
long colonCount = trimmed.chars().filter(ch -> ch == ':').count();
|
||||
if (colonCount >= 2) {
|
||||
updV6.setString(1, trimmed);
|
||||
updV6.setLong(2, id);
|
||||
updV6.executeUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Set<String> getTableColumns(Connection connection, String table) throws Exception {
|
||||
Set<String> cols = new HashSet<>();
|
||||
try (Statement statement = connection.createStatement();
|
||||
ResultSet rs = statement.executeQuery("PRAGMA table_info(" + table + ");")) {
|
||||
while (rs.next()) {
|
||||
String name = rs.getString("name");
|
||||
if (name != null && !name.isBlank()) {
|
||||
cols.add(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
return cols;
|
||||
}
|
||||
|
||||
private void ensureColumnIfMissing(
|
||||
Connection connection,
|
||||
Set<String> existingColumns,
|
||||
String table,
|
||||
String column,
|
||||
String type
|
||||
) throws Exception {
|
||||
if (existingColumns.contains(column)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (Statement statement = connection.createStatement()) {
|
||||
statement.execute("ALTER TABLE " + table + " ADD COLUMN " + column + " " + type + ";");
|
||||
}
|
||||
|
||||
log.info("SQLite schema updated: added {}.{}", table, column);
|
||||
}
|
||||
|
||||
/**
|
||||
* 定期执行 checkpoint,确保 WAL 文件内容写入主数据库
|
||||
* 每5分钟执行一次
|
||||
*/
|
||||
@Scheduled(fixedDelay = 300000, initialDelay = 300000)
|
||||
public void performCheckpoint() {
|
||||
try (Connection connection = dataSource.getConnection();
|
||||
Statement statement = connection.createStatement()) {
|
||||
|
||||
statement.execute("PRAGMA wal_checkpoint(TRUNCATE);");
|
||||
log.debug("SQLite WAL checkpoint completed");
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to perform SQLite checkpoint", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用关闭前执行最终的 checkpoint,确保所有数据都写入主数据库文件
|
||||
*/
|
||||
@PreDestroy
|
||||
public void onShutdown() {
|
||||
log.info("Performing final SQLite checkpoint before shutdown...");
|
||||
try (Connection connection = dataSource.getConnection();
|
||||
Statement statement = connection.createStatement()) {
|
||||
|
||||
// 强制执行 checkpoint,将所有 WAL 内容写入主数据库
|
||||
statement.execute("PRAGMA wal_checkpoint(TRUNCATE);");
|
||||
log.info("Final SQLite checkpoint completed successfully");
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to perform final SQLite checkpoint", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
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/open_api/**")
|
||||
.excludePathPatterns("/api/v1/config/get")
|
||||
.excludePathPatterns("/api/v1/user/login")
|
||||
.excludePathPatterns("/api/v1/captcha/**");
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
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 org.springframework.boot.web.servlet.ServletContextInitializer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.ServletException;
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package com.admin.config;
|
||||
|
||||
|
||||
import com.admin.common.utils.IpUtils;
|
||||
import com.admin.common.utils.JwtUtil;
|
||||
import com.admin.entity.Node;
|
||||
import com.admin.service.NodeService;
|
||||
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.WebSocketHandler;
|
||||
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.net.InetSocketAddress;
|
||||
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");
|
||||
String version = serverHttpRequest.getServletRequest().getParameter("version");
|
||||
String http = serverHttpRequest.getServletRequest().getParameter("http");
|
||||
String tls = serverHttpRequest.getServletRequest().getParameter("tls");
|
||||
String socks = serverHttpRequest.getServletRequest().getParameter("socks");
|
||||
if (Objects.equals(type, "1")) {
|
||||
System.out.println("type: " + type + " - version: " + version + " - secret: " + secret + " - IP: " + getClientIp(request));
|
||||
Node node = nodeService.getOne(new QueryWrapper<Node>().eq("secret", secret));
|
||||
if (node == null) {
|
||||
log.info("节点验证失败:未找到匹配的secret");
|
||||
return false;
|
||||
}
|
||||
attributes.put("id", node.getId());
|
||||
attributes.put("nodeSecret", secret);
|
||||
attributes.put("nodeVersion", version);
|
||||
attributes.put("http",http);
|
||||
attributes.put("tls",tls);
|
||||
attributes.put("socks",socks);
|
||||
log.info("节点 {} 通过验证,版本: {}", node.getId(), version);
|
||||
// 不在这里更新状态,等到连接建立后再统一更新
|
||||
}else {
|
||||
boolean b = JwtUtil.validateToken(secret);
|
||||
if (!b) return false;
|
||||
attributes.put("id", JwtUtil.getUserIdFromToken(secret));
|
||||
}
|
||||
attributes.put("type", type);
|
||||
return true;
|
||||
}
|
||||
|
||||
public String getClientIp(ServerHttpRequest request) {
|
||||
InetSocketAddress remoteAddress = request.getRemoteAddress();
|
||||
if (remoteAddress != null) {
|
||||
return remoteAddress.getAddress().getHostAddress();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
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
|
||||
ViteConfigService viteConfigService;
|
||||
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package com.admin.controller;
|
||||
|
||||
import cloud.tianai.captcha.application.ImageCaptchaApplication;
|
||||
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
|
||||
import cloud.tianai.captcha.common.response.ApiResponse;
|
||||
import cloud.tianai.captcha.application.vo.ImageCaptchaVO;
|
||||
import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
|
||||
import cloud.tianai.captcha.validator.common.model.dto.MatchParam;
|
||||
import com.admin.common.dto.CaptchaVerifyDto;
|
||||
import com.admin.common.lang.R;
|
||||
import com.admin.entity.ViteConfig;
|
||||
import com.admin.service.ViteConfigService;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import cloud.tianai.captcha.application.vo.CaptchaResponse;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.validation.Valid;
|
||||
import java.util.Collections;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
/**
|
||||
* 验证码控制器
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@CrossOrigin
|
||||
@RequestMapping("/api/v1/captcha")
|
||||
public class CaptchaController extends BaseController {
|
||||
|
||||
@Resource
|
||||
ImageCaptchaApplication application;
|
||||
|
||||
private static final String[] OPTIONS = {
|
||||
"SLIDER", "WORD_IMAGE_CLICK", "ROTATE", "CONCAT"
|
||||
};
|
||||
|
||||
@PostMapping("/check")
|
||||
public R check() {
|
||||
ViteConfig viteConfig = viteConfigService.getOne(new QueryWrapper<ViteConfig>().eq("name", "captcha_enabled"));
|
||||
if (viteConfig == null) return R.ok(0);
|
||||
if (!Objects.equals(viteConfig.getValue(), "true")) return R.ok(0);
|
||||
return R.ok(1);
|
||||
}
|
||||
|
||||
|
||||
@PostMapping("/generate")
|
||||
public CaptchaResponse<ImageCaptchaVO> genCaptcha() {
|
||||
ViteConfig viteConfig = viteConfigService.getOne(new QueryWrapper<ViteConfig>().eq("name", "captcha_type"));
|
||||
String captchaType;
|
||||
if (viteConfig == null || Objects.equals(viteConfig.getValue(), "RANDOM")) {
|
||||
captchaType = getRandomOption();
|
||||
}else {
|
||||
captchaType = viteConfig.getValue();
|
||||
}
|
||||
return application.generateCaptcha(captchaType);
|
||||
}
|
||||
|
||||
|
||||
@PostMapping("/verify")
|
||||
public ApiResponse<?> verify(@Valid @RequestBody CaptchaVerifyDto verifyDto) {
|
||||
ApiResponse<?> response = application.matching(verifyDto.getId(), verifyDto.getData());
|
||||
if (response.isSuccess()) {
|
||||
return ApiResponse.ofSuccess(Collections.singletonMap("validToken", verifyDto.getId()));
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
public static String getRandomOption() {
|
||||
int index = ThreadLocalRandom.current().nextInt(OPTIONS.length);
|
||||
return OPTIONS[index];
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
package com.admin.controller;
|
||||
|
||||
import org.springframework.boot.web.servlet.error.ErrorController;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
import javax.servlet.RequestDispatcher;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
@Controller
|
||||
public class CustomErrorController implements ErrorController {
|
||||
|
||||
@RequestMapping("/error")
|
||||
public void handleError(HttpServletRequest request, HttpServletResponse response) throws IOException {
|
||||
Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
|
||||
|
||||
response.setContentType("text/html; charset=UTF-8");
|
||||
|
||||
if (status != null) {
|
||||
int statusCode = Integer.valueOf(status.toString());
|
||||
|
||||
if (statusCode == HttpStatus.NOT_FOUND.value()) {
|
||||
response.setStatus(HttpStatus.NOT_FOUND.value());
|
||||
String html = generateErrorHtml(
|
||||
"404",
|
||||
"你推开了后端的大门,却发现里面只有寂寞。",
|
||||
request.getRequestURI()
|
||||
);
|
||||
response.getWriter().write(html);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String generateErrorHtml(String statusCode, String title, String path) {
|
||||
return "<!DOCTYPE html>" +
|
||||
"<html lang='zh-CN'>" +
|
||||
"<head>" +
|
||||
" <meta charset='UTF-8'>" +
|
||||
" <meta name='viewport' content='width=device-width, initial-scale=1.0'>" +
|
||||
" <title>错误 " + statusCode + "</title>" +
|
||||
" <style>" +
|
||||
" body {" +
|
||||
" font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;" +
|
||||
" margin: 0;" +
|
||||
" padding: 0;" +
|
||||
" min-height: 100vh;" +
|
||||
" display: flex;" +
|
||||
" flex-direction: column;" +
|
||||
" justify-content: center;" +
|
||||
" align-items: center;" +
|
||||
" text-align: center;" +
|
||||
" }" +
|
||||
" .error-code {" +
|
||||
" font-size: 6rem;" +
|
||||
" color: #333;" +
|
||||
" font-weight: 300;" +
|
||||
" margin: 0;" +
|
||||
" }" +
|
||||
" .error-title {" +
|
||||
" font-size: 1.2rem;" +
|
||||
" color: #666;" +
|
||||
" margin: 20px 0 0 0;" +
|
||||
" font-weight: normal;" +
|
||||
" }" +
|
||||
" </style>" +
|
||||
"</head>" +
|
||||
"<body>" +
|
||||
" <div class='error-code'>" + statusCode + "</div>" +
|
||||
" <div class='error-title'>" + title + "</div>" +
|
||||
"</body>" +
|
||||
"</html>";
|
||||
}
|
||||
}
|
||||
@@ -1,389 +0,0 @@
|
||||
package com.admin.controller;
|
||||
|
||||
import com.admin.common.aop.LogAnnotation;
|
||||
import com.admin.common.dto.FlowDto;
|
||||
import com.admin.common.dto.GostConfigDto;
|
||||
import com.admin.common.task.CheckGostConfigAsync;
|
||||
import com.admin.common.utils.AESCrypto;
|
||||
import com.admin.common.utils.GostUtil;
|
||||
import com.admin.entity.*;
|
||||
import com.admin.service.ChainTunnelService;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 流量上报控制器
|
||||
* 处理节点上报的流量数据,更新用户和隧道的流量统计
|
||||
* <p>
|
||||
* 主要功能:
|
||||
* 1. 接收并处理节点上报的流量数据
|
||||
* 2. 更新转发、用户和隧道的流量统计
|
||||
* 3. 检查用户总流量限制,超限时暂停所有服务
|
||||
* 4. 检查隧道流量限制,超限时暂停对应服务
|
||||
* 5. 检查用户到期时间,到期时暂停所有服务
|
||||
* 6. 检查隧道权限到期时间,到期时暂停对应服务
|
||||
* 7. 检查用户状态,状态不为1时暂停所有服务
|
||||
* 8. 检查转发状态,状态不为1时暂停对应转发
|
||||
* 9. 检查用户隧道权限状态,状态不为1时暂停对应转发
|
||||
* <p>
|
||||
* 并发安全解决方案:
|
||||
* 1. 使用UpdateWrapper进行数据库层面的原子更新操作,避免读取-修改-写入的竞态条件
|
||||
* 2. 使用synchronized锁确保同一用户/隧道的流量更新串行执行
|
||||
* 3. 这样可以避免相同用户相同隧道不同转发同时上报时流量统计丢失的问题
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/flow")
|
||||
@CrossOrigin
|
||||
@Slf4j
|
||||
public class FlowController extends BaseController {
|
||||
|
||||
// 常量定义
|
||||
private static final String SUCCESS_RESPONSE = "ok";
|
||||
private static final String DEFAULT_USER_TUNNEL_ID = "0";
|
||||
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<>();
|
||||
private static final ConcurrentHashMap<String, Object> FORWARD_LOCKS = new ConcurrentHashMap<>();
|
||||
|
||||
// 缓存加密器实例,避免重复创建
|
||||
private static final ConcurrentHashMap<String, AESCrypto> CRYPTO_CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
@Resource
|
||||
CheckGostConfigAsync checkGostConfigAsync;
|
||||
|
||||
@Resource
|
||||
@Lazy
|
||||
ChainTunnelService chainTunnelService;
|
||||
|
||||
/**
|
||||
* 加密消息包装器
|
||||
*/
|
||||
public static class EncryptedMessage {
|
||||
private boolean encrypted;
|
||||
private String data;
|
||||
private Long timestamp;
|
||||
|
||||
// getters and setters
|
||||
public boolean isEncrypted() {
|
||||
return encrypted;
|
||||
}
|
||||
|
||||
public void setEncrypted(boolean encrypted) {
|
||||
this.encrypted = encrypted;
|
||||
}
|
||||
|
||||
public String getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public void setData(String data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public Long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public void setTimestamp(Long timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/config")
|
||||
@LogAnnotation
|
||||
public String config(@RequestBody String rawData, String secret) {
|
||||
Node node = nodeService.getOne(new QueryWrapper<Node>().eq("secret", secret));
|
||||
if (node == null) return SUCCESS_RESPONSE;
|
||||
|
||||
try {
|
||||
// 尝试解密数据
|
||||
String decryptedData = decryptIfNeeded(rawData, secret);
|
||||
|
||||
// 解析为GostConfigDto
|
||||
GostConfigDto gostConfigDto = JSON.parseObject(decryptedData, GostConfigDto.class);
|
||||
checkGostConfigAsync.cleanNodeConfigs(node.getId().toString(), gostConfigDto);
|
||||
|
||||
log.info("🔓 节点 {} 配置数据接收成功{}", node.getId(), isEncryptedMessage(rawData) ? "(已解密)" : "");
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理节点 {} 配置数据失败: {}", node.getId(), e.getMessage());
|
||||
}
|
||||
|
||||
return SUCCESS_RESPONSE;
|
||||
}
|
||||
|
||||
@RequestMapping("/test")
|
||||
@LogAnnotation
|
||||
public String test() {
|
||||
return "test";
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理流量数据上报
|
||||
*
|
||||
* @param rawData 原始数据(可能是加密的)
|
||||
* @param secret 节点密钥
|
||||
* @return 处理结果
|
||||
*/
|
||||
@RequestMapping("/upload")
|
||||
@LogAnnotation
|
||||
public String uploadFlowData(@RequestBody String rawData, String secret) {
|
||||
// 1. 验证节点权限
|
||||
if (!isValidNode(secret)) {
|
||||
return SUCCESS_RESPONSE;
|
||||
}
|
||||
|
||||
// 2. 尝试解密数据
|
||||
String decryptedData = decryptIfNeeded(rawData, secret);
|
||||
|
||||
// 3. 解析为FlowDto列表
|
||||
JSONArray flowDataList = JSONObject.parseArray(decryptedData);
|
||||
log.info("节点上报流量数据{}", flowDataList);
|
||||
for (int i = 0; i < flowDataList.size(); i++) {
|
||||
String jsonObject = flowDataList.getJSONObject(i).toJSONString();
|
||||
FlowDto flowDto = JSONObject.parseObject(jsonObject, FlowDto.class);
|
||||
if (!Objects.equals(flowDto.getN(), "web_api")) {
|
||||
processFlowData(flowDto);
|
||||
}
|
||||
}
|
||||
return SUCCESS_RESPONSE;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测消息是否为加密格式
|
||||
*/
|
||||
private boolean isEncryptedMessage(String data) {
|
||||
try {
|
||||
JSONObject json = JSON.parseObject(data);
|
||||
return json.getBooleanValue("encrypted");
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据需要解密数据
|
||||
*/
|
||||
private String decryptIfNeeded(String rawData, String secret) {
|
||||
if (rawData == null || rawData.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("数据不能为空");
|
||||
}
|
||||
|
||||
try {
|
||||
// 尝试解析为加密消息格式
|
||||
EncryptedMessage encryptedMessage = JSON.parseObject(rawData, EncryptedMessage.class);
|
||||
|
||||
if (encryptedMessage.isEncrypted() && encryptedMessage.getData() != null) {
|
||||
// 获取或创建加密器
|
||||
AESCrypto crypto = getOrCreateCrypto(secret);
|
||||
if (crypto == null) {
|
||||
log.info("⚠️ 收到加密消息但无法创建解密器,使用原始数据");
|
||||
return rawData;
|
||||
}
|
||||
|
||||
// 解密数据
|
||||
String decryptedData = crypto.decryptString(encryptedMessage.getData());
|
||||
return decryptedData;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 解析失败,可能是非加密格式,直接返回原始数据
|
||||
log.info("数据未加密或解密失败,使用原始数据: {}", e.getMessage());
|
||||
}
|
||||
|
||||
return rawData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取或创建加密器实例
|
||||
*/
|
||||
private AESCrypto getOrCreateCrypto(String secret) {
|
||||
return CRYPTO_CACHE.computeIfAbsent(secret, AESCrypto::create);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理流量数据的核心逻辑
|
||||
*/
|
||||
private void processFlowData(FlowDto flowDataList) {
|
||||
String[] serviceIds = parseServiceName(flowDataList.getN());
|
||||
String forwardId = serviceIds[0];
|
||||
String userId = serviceIds[1];
|
||||
String userTunnelId = serviceIds[2];
|
||||
|
||||
Forward forward = forwardService.getById(forwardId);
|
||||
if (forward != null){
|
||||
Tunnel tunnel = tunnelService.getById(forward.getTunnelId());
|
||||
|
||||
// 处理流量倍率及单双向计算
|
||||
BigDecimal trafficRatio = tunnel.getTrafficRatio();
|
||||
BigDecimal originalD = BigDecimal.valueOf(flowDataList.getD());
|
||||
BigDecimal originalU = BigDecimal.valueOf(flowDataList.getU());
|
||||
BigDecimal newD = originalD.multiply(trafficRatio);
|
||||
BigDecimal newU = originalU.multiply(trafficRatio);
|
||||
flowDataList.setD(newD.longValue() * tunnel.getFlow());
|
||||
flowDataList.setU(newU.longValue() * tunnel.getFlow());
|
||||
}
|
||||
|
||||
// 先更新所有流量统计 - 确保流量数据的一致性
|
||||
updateForwardFlow(forwardId, flowDataList);
|
||||
updateUserFlow(userId, flowDataList);
|
||||
updateUserTunnelFlow(userTunnelId, flowDataList);
|
||||
|
||||
// 7. 检查和服务暂停操作
|
||||
String name = buildServiceName(forwardId, userId, userTunnelId);
|
||||
if (!Objects.equals(userTunnelId, DEFAULT_USER_TUNNEL_ID)) { // 非管理员的转发需要检测流量限制
|
||||
checkUserRelatedLimits(userId, name);
|
||||
checkUserTunnelRelatedLimits(userTunnelId, name, userId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void checkUserRelatedLimits(String userId, String name) {
|
||||
|
||||
// 重新查询用户以获取最新的流量数据
|
||||
User updatedUser = userService.getById(userId);
|
||||
if (updatedUser == null) return;
|
||||
|
||||
// 检查用户总流量限制
|
||||
long userFlowLimit = updatedUser.getFlow() * BYTES_TO_GB;
|
||||
long userCurrentFlow = updatedUser.getInFlow() + updatedUser.getOutFlow();
|
||||
if (userFlowLimit < userCurrentFlow) {
|
||||
pauseAllUserServices(userId, name);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查用户到期时间
|
||||
if (updatedUser.getExpTime() != null && updatedUser.getExpTime() <= new Date().getTime()) {
|
||||
pauseAllUserServices(userId, name);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查用户状态
|
||||
if (updatedUser.getStatus() != 1) {
|
||||
pauseAllUserServices(userId, name);
|
||||
}
|
||||
}
|
||||
|
||||
public void pauseAllUserServices(String userId, String name) {
|
||||
List<Forward> forwardList = forwardService.list(new QueryWrapper<Forward>().eq("user_id", userId));
|
||||
pauseService(forwardList, name);
|
||||
}
|
||||
|
||||
public void checkUserTunnelRelatedLimits(String userTunnelId, String name, String userId) {
|
||||
|
||||
UserTunnel userTunnel = userTunnelService.getById(userTunnelId);
|
||||
if (userTunnel == null) return;
|
||||
long flow = userTunnel.getInFlow() + userTunnel.getOutFlow();
|
||||
if (flow >= userTunnel.getFlow() * BYTES_TO_GB) {
|
||||
pauseSpecificForward(userTunnel.getTunnelId(), name, userId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (userTunnel.getExpTime() != null && userTunnel.getExpTime() <= System.currentTimeMillis()) {
|
||||
pauseSpecificForward(userTunnel.getTunnelId(), name, userId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (userTunnel.getStatus() != 1) {
|
||||
pauseSpecificForward(userTunnel.getTunnelId(), name, userId);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private void pauseSpecificForward(Integer tunnelId, String name, String userId) {
|
||||
List<Forward> forwardList = forwardService.list(new QueryWrapper<Forward>().eq("tunnel_id", tunnelId).eq("user_id", userId));
|
||||
pauseService(forwardList, name);
|
||||
}
|
||||
|
||||
public void pauseService(List<Forward> forwardList, String name) {
|
||||
for (Forward forward : forwardList) {
|
||||
List<ChainTunnel> chainTunnels = chainTunnelService.list(new QueryWrapper<ChainTunnel>().eq("tunnel_id", forward.getTunnelId()).eq("chain_type", 1));
|
||||
for (ChainTunnel chainTunnel : chainTunnels) {
|
||||
GostUtil.PauseAndResumeService(chainTunnel.getNodeId(), name, "PauseService");
|
||||
}
|
||||
forward.setStatus(0);
|
||||
forwardService.updateById(forward);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateForwardFlow(String forwardId, FlowDto flowStats) {
|
||||
// 对相同转发的流量更新进行同步,避免并发覆盖
|
||||
synchronized (getForwardLock(forwardId)) {
|
||||
UpdateWrapper<Forward> updateWrapper = new UpdateWrapper<>();
|
||||
updateWrapper.eq("id", forwardId);
|
||||
updateWrapper.setSql("in_flow = in_flow + " + flowStats.getD());
|
||||
updateWrapper.setSql("out_flow = out_flow + " + flowStats.getU());
|
||||
|
||||
forwardService.update(null, updateWrapper);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateUserFlow(String userId, FlowDto flowStats) {
|
||||
// 对相同用户的流量更新进行同步,避免并发覆盖
|
||||
synchronized (getUserLock(userId)) {
|
||||
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
|
||||
updateWrapper.eq("id", userId);
|
||||
|
||||
updateWrapper.setSql("in_flow = in_flow + " + flowStats.getD());
|
||||
updateWrapper.setSql("out_flow = out_flow + " + flowStats.getU());
|
||||
|
||||
userService.update(null, updateWrapper);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateUserTunnelFlow(String userTunnelId, FlowDto flowStats) {
|
||||
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.getD());
|
||||
updateWrapper.setSql("out_flow = out_flow + " + flowStats.getU());
|
||||
userTunnelService.update(null, updateWrapper);
|
||||
}
|
||||
}
|
||||
|
||||
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 Object getForwardLock(String forwardId) {
|
||||
return FORWARD_LOCKS.computeIfAbsent(forwardId, k -> new Object());
|
||||
}
|
||||
|
||||
private boolean isValidNode(String secret) {
|
||||
int nodeCount = nodeService.count(new QueryWrapper<Node>().eq("secret", secret));
|
||||
return nodeCount > 0;
|
||||
}
|
||||
|
||||
private String[] parseServiceName(String serviceName) {
|
||||
return serviceName.split("_");
|
||||
}
|
||||
|
||||
private String buildServiceName(String forwardId, String userId, String userTunnelId) {
|
||||
return forwardId + "_" + userId + "_" + userTunnelId;
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
package com.admin.controller;
|
||||
|
||||
import com.admin.common.aop.LogAnnotation;
|
||||
import com.admin.common.annotation.RequireRole;
|
||||
import com.admin.common.dto.BatchDeleteDto;
|
||||
import com.admin.common.dto.BatchRedeployDto;
|
||||
import com.admin.common.dto.BatchChangeTunnelDto;
|
||||
import com.admin.common.dto.ForwardDto;
|
||||
import com.admin.common.dto.ForwardUpdateDto;
|
||||
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 {
|
||||
|
||||
@Autowired
|
||||
private ForwardService forwardService;
|
||||
|
||||
@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("/force-delete")
|
||||
public R forceDelete(@RequestBody Map<String, Object> params) {
|
||||
Long id = Long.valueOf(params.get("id").toString());
|
||||
return forwardService.forceDeleteForward(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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转发诊断功能
|
||||
* @param params 包含forwardId的参数
|
||||
* @return 诊断结果
|
||||
*/
|
||||
@LogAnnotation
|
||||
@PostMapping("/diagnose")
|
||||
public R diagnoseForward(@RequestBody Map<String, Object> params) {
|
||||
Long forwardId = Long.valueOf(params.get("forwardId").toString());
|
||||
return forwardService.diagnoseForward(forwardId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新转发排序
|
||||
* @param params 包含forwards数组的参数,每个元素包含id和inx
|
||||
* @return 更新结果
|
||||
*/
|
||||
@LogAnnotation
|
||||
@PostMapping("/update-order")
|
||||
public R updateForwardOrder(@RequestBody Map<String, Object> params) {
|
||||
return forwardService.updateForwardOrder(params);
|
||||
}
|
||||
|
||||
@LogAnnotation
|
||||
@PostMapping("/batch-delete")
|
||||
public R batchDelete(@Validated @RequestBody BatchDeleteDto batchDeleteDto) {
|
||||
return forwardService.batchDeleteForwards(batchDeleteDto);
|
||||
}
|
||||
|
||||
@LogAnnotation
|
||||
@PostMapping("/batch-pause")
|
||||
public R batchPause(@Validated @RequestBody BatchDeleteDto batchDeleteDto) {
|
||||
return forwardService.batchPauseForwards(batchDeleteDto);
|
||||
}
|
||||
|
||||
@LogAnnotation
|
||||
@PostMapping("/batch-resume")
|
||||
public R batchResume(@Validated @RequestBody BatchDeleteDto batchDeleteDto) {
|
||||
return forwardService.batchResumeForwards(batchDeleteDto);
|
||||
}
|
||||
|
||||
@LogAnnotation
|
||||
@PostMapping("/batch-redeploy")
|
||||
public R batchRedeploy(@Validated @RequestBody BatchRedeployDto batchRedeployDto) {
|
||||
return forwardService.batchRedeployForwards(batchRedeployDto);
|
||||
}
|
||||
|
||||
@LogAnnotation
|
||||
@PostMapping("/batch-change-tunnel")
|
||||
public R batchChangeTunnel(@Validated @RequestBody BatchChangeTunnelDto batchChangeTunnelDto) {
|
||||
return forwardService.batchChangeTunnel(batchChangeTunnelDto);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
package com.admin.controller;
|
||||
|
||||
import com.admin.common.aop.LogAnnotation;
|
||||
import com.admin.common.annotation.RequireRole;
|
||||
import com.admin.common.dto.GroupCreateDto;
|
||||
import com.admin.common.dto.GroupPermissionAssignDto;
|
||||
import com.admin.common.dto.GroupUpdateDto;
|
||||
import com.admin.common.dto.TunnelGroupAssignTunnelsDto;
|
||||
import com.admin.common.dto.UserGroupAssignUsersDto;
|
||||
import com.admin.common.lang.R;
|
||||
import com.admin.service.GroupService;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.CrossOrigin;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@CrossOrigin
|
||||
@RequestMapping("/api/v1/group")
|
||||
public class GroupController {
|
||||
|
||||
@Resource
|
||||
private GroupService groupService;
|
||||
|
||||
@LogAnnotation
|
||||
@RequireRole
|
||||
@PostMapping("/tunnel/list")
|
||||
public R tunnelGroupList() {
|
||||
return groupService.getTunnelGroups();
|
||||
}
|
||||
|
||||
@LogAnnotation
|
||||
@RequireRole
|
||||
@PostMapping("/tunnel/create")
|
||||
public R createTunnelGroup(@Validated @RequestBody GroupCreateDto dto) {
|
||||
return groupService.createTunnelGroup(dto);
|
||||
}
|
||||
|
||||
@LogAnnotation
|
||||
@RequireRole
|
||||
@PostMapping("/tunnel/update")
|
||||
public R updateTunnelGroup(@Validated @RequestBody GroupUpdateDto dto) {
|
||||
return groupService.updateTunnelGroup(dto);
|
||||
}
|
||||
|
||||
@LogAnnotation
|
||||
@RequireRole
|
||||
@PostMapping("/tunnel/delete")
|
||||
public R deleteTunnelGroup(@RequestBody Map<String, Object> params) {
|
||||
Long id = Long.valueOf(params.get("id").toString());
|
||||
return groupService.deleteTunnelGroup(id);
|
||||
}
|
||||
|
||||
@LogAnnotation
|
||||
@RequireRole
|
||||
@PostMapping("/tunnel/assign")
|
||||
public R assignTunnels(@Validated @RequestBody TunnelGroupAssignTunnelsDto dto) {
|
||||
return groupService.assignTunnelsToGroup(dto);
|
||||
}
|
||||
|
||||
@LogAnnotation
|
||||
@RequireRole
|
||||
@PostMapping("/user/list")
|
||||
public R userGroupList() {
|
||||
return groupService.getUserGroups();
|
||||
}
|
||||
|
||||
@LogAnnotation
|
||||
@RequireRole
|
||||
@PostMapping("/user/create")
|
||||
public R createUserGroup(@Validated @RequestBody GroupCreateDto dto) {
|
||||
return groupService.createUserGroup(dto);
|
||||
}
|
||||
|
||||
@LogAnnotation
|
||||
@RequireRole
|
||||
@PostMapping("/user/update")
|
||||
public R updateUserGroup(@Validated @RequestBody GroupUpdateDto dto) {
|
||||
return groupService.updateUserGroup(dto);
|
||||
}
|
||||
|
||||
@LogAnnotation
|
||||
@RequireRole
|
||||
@PostMapping("/user/delete")
|
||||
public R deleteUserGroup(@RequestBody Map<String, Object> params) {
|
||||
Long id = Long.valueOf(params.get("id").toString());
|
||||
return groupService.deleteUserGroup(id);
|
||||
}
|
||||
|
||||
@LogAnnotation
|
||||
@RequireRole
|
||||
@PostMapping("/user/assign")
|
||||
public R assignUsers(@Validated @RequestBody UserGroupAssignUsersDto dto) {
|
||||
return groupService.assignUsersToGroup(dto);
|
||||
}
|
||||
|
||||
@LogAnnotation
|
||||
@RequireRole
|
||||
@PostMapping("/permission/list")
|
||||
public R listPermissions() {
|
||||
return groupService.getGroupPermissions();
|
||||
}
|
||||
|
||||
@LogAnnotation
|
||||
@RequireRole
|
||||
@PostMapping("/permission/assign")
|
||||
public R assignPermission(@Validated @RequestBody GroupPermissionAssignDto dto) {
|
||||
return groupService.assignGroupPermission(dto);
|
||||
}
|
||||
|
||||
@LogAnnotation
|
||||
@RequireRole
|
||||
@PostMapping("/permission/remove")
|
||||
public R removePermission(@RequestBody Map<String, Object> params) {
|
||||
Long id = Long.valueOf(params.get("id").toString());
|
||||
return groupService.removeGroupPermission(id);
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
package com.admin.controller;
|
||||
|
||||
|
||||
import com.admin.common.annotation.RequireRole;
|
||||
import com.admin.common.aop.LogAnnotation;
|
||||
import com.admin.common.dto.BatchDeleteDto;
|
||||
import com.admin.common.dto.NodeDto;
|
||||
import com.admin.common.dto.NodeUpdateDto;
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新节点排序
|
||||
* @param params 包含nodes数组的参数,每个元素包含id和inx
|
||||
* @return 更新结果
|
||||
*/
|
||||
@LogAnnotation
|
||||
@RequireRole
|
||||
@PostMapping("/update-order")
|
||||
public R updateNodeOrder(@RequestBody Map<String, Object> params) {
|
||||
return nodeService.updateNodeOrder(params);
|
||||
}
|
||||
|
||||
@LogAnnotation
|
||||
@RequireRole
|
||||
@PostMapping("/batch-delete")
|
||||
public R batchDelete(@Validated @RequestBody BatchDeleteDto batchDeleteDto) {
|
||||
return nodeService.batchDeleteNodes(batchDeleteDto);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
package com.admin.controller;
|
||||
|
||||
|
||||
import com.admin.common.aop.LogAnnotation;
|
||||
import com.admin.common.lang.R;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import com.admin.common.utils.Md5Util;
|
||||
import com.admin.entity.User;
|
||||
import com.admin.entity.UserTunnel;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@RestController
|
||||
@CrossOrigin
|
||||
@RequestMapping("/api/v1/open_api")
|
||||
public class OpenApiController extends BaseController {
|
||||
|
||||
@LogAnnotation
|
||||
@GetMapping("/sub_store")
|
||||
public Object create(
|
||||
@RequestParam("user") String user,
|
||||
@RequestParam("pwd") String pwd,
|
||||
@RequestParam(value = "tunnel", required = false, defaultValue = "-1") String tunnel,
|
||||
HttpServletResponse response) {
|
||||
JSONObject result = new JSONObject();
|
||||
result.put("upload", 0);
|
||||
result.put("download", 0);
|
||||
result.put("total", 0);
|
||||
result.put("expire", 0);
|
||||
// 校验 user 是否为空
|
||||
if (user == null || user.isEmpty()) {
|
||||
return R.err("用户不能为空");
|
||||
}
|
||||
if (pwd == null || pwd.isEmpty()) {
|
||||
return R.err("密码不能为空");
|
||||
}
|
||||
|
||||
User userInfo = userService.getOne(new QueryWrapper<User>().eq("user", user));
|
||||
if (userInfo == null) {
|
||||
return R.err("鉴权失败");
|
||||
}
|
||||
|
||||
String pwdMd5 = Md5Util.md5(pwd);
|
||||
if (!Objects.equals(pwdMd5, userInfo.getPwd())) {
|
||||
return R.err("鉴权失败");
|
||||
}
|
||||
|
||||
final long GIGA = 1024L * 1024L * 1024L;
|
||||
String headerValue;
|
||||
|
||||
if ("-1".equals(tunnel)) {
|
||||
headerValue = buildSubscriptionHeader(
|
||||
userInfo.getOutFlow(),
|
||||
userInfo.getInFlow(),
|
||||
userInfo.getFlow() * GIGA,
|
||||
userInfo.getExpTime() / 1000
|
||||
);
|
||||
} else {
|
||||
UserTunnel tunnelInfo = userTunnelService.getById(tunnel);
|
||||
if (tunnelInfo == null) return R.err("隧道不存在");
|
||||
if (!tunnelInfo.getUserId().toString().equals(userInfo.getId().toString())) return R.err("隧道不存在");
|
||||
headerValue = buildSubscriptionHeader(
|
||||
tunnelInfo.getOutFlow(),
|
||||
tunnelInfo.getInFlow(),
|
||||
tunnelInfo.getFlow() * GIGA,
|
||||
tunnelInfo.getExpTime() / 1000
|
||||
);
|
||||
}
|
||||
|
||||
response.setHeader("subscription-userinfo", headerValue);
|
||||
return headerValue;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private String buildSubscriptionHeader(long upload, long download, long total, long expire) {
|
||||
return String.format("upload=%d; download=%d; total=%d; expire=%d", download, upload, total, expire);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
package com.admin.controller;
|
||||
|
||||
import com.admin.common.aop.LogAnnotation;
|
||||
import com.admin.common.annotation.RequireRole;
|
||||
import com.admin.common.dto.BatchDeleteDto;
|
||||
import com.admin.common.dto.BatchRedeployDto;
|
||||
import com.admin.common.dto.TunnelDto;
|
||||
import com.admin.common.dto.TunnelUpdateDto;
|
||||
|
||||
import com.admin.common.dto.UserTunnelBatchAssignDto;
|
||||
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() {
|
||||
return tunnelService.getAllTunnels();
|
||||
}
|
||||
|
||||
@LogAnnotation
|
||||
@RequireRole
|
||||
@PostMapping("/update")
|
||||
public R update(@Validated @RequestBody TunnelUpdateDto tunnelUpdateDto) {
|
||||
return tunnelService.updateTunnel(tunnelUpdateDto);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@LogAnnotation
|
||||
@RequireRole
|
||||
@PostMapping("/user/batch-assign")
|
||||
public R batchAssignUserTunnel(@Validated @RequestBody UserTunnelBatchAssignDto batchAssignDto) {
|
||||
return userTunnelService.batchAssignUserTunnel(batchAssignDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询用户隧道权限列表
|
||||
* @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 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 隧道诊断功能
|
||||
* @param params 包含tunnelId的参数
|
||||
* @return 诊断结果
|
||||
*/
|
||||
@LogAnnotation
|
||||
@RequireRole
|
||||
@PostMapping("/diagnose")
|
||||
public R diagnoseTunnel(@RequestBody Map<String, Object> params) {
|
||||
Long tunnelId = Long.valueOf(params.get("tunnelId").toString());
|
||||
return tunnelService.diagnoseTunnel(tunnelId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新隧道排序
|
||||
* @param params 包含tunnels数组的参数,每个元素包含id和inx
|
||||
* @return 更新结果
|
||||
*/
|
||||
@LogAnnotation
|
||||
@RequireRole
|
||||
@PostMapping("/update-order")
|
||||
public R updateTunnelOrder(@RequestBody Map<String, Object> params) {
|
||||
return tunnelService.updateTunnelOrder(params);
|
||||
}
|
||||
|
||||
@LogAnnotation
|
||||
@RequireRole
|
||||
@PostMapping("/batch-delete")
|
||||
public R batchDelete(@Validated @RequestBody BatchDeleteDto batchDeleteDto) {
|
||||
return tunnelService.batchDeleteTunnels(batchDeleteDto);
|
||||
}
|
||||
|
||||
@LogAnnotation
|
||||
@RequireRole
|
||||
@PostMapping("/batch-redeploy")
|
||||
public R batchRedeploy(@Validated @RequestBody BatchRedeployDto batchRedeployDto) {
|
||||
return tunnelService.batchRedeployTunnels(batchRedeployDto);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
package com.admin.controller;
|
||||
|
||||
|
||||
import com.admin.common.aop.LogAnnotation;
|
||||
import com.admin.common.annotation.RequireRole;
|
||||
import com.admin.common.dto.*;
|
||||
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() {
|
||||
return userService.getAllUsers();
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@LogAnnotation
|
||||
@RequireRole
|
||||
@PostMapping("/reset")
|
||||
public R reset(@Validated @RequestBody ResetFlowDto resetFlowDto) {
|
||||
return userService.reset(resetFlowDto);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package com.admin.controller;
|
||||
|
||||
|
||||
import com.admin.common.annotation.RequireRole;
|
||||
import com.admin.common.aop.LogAnnotation;
|
||||
import com.admin.common.lang.R;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 网站配置控制器
|
||||
* </p>
|
||||
*
|
||||
* @author QAQ
|
||||
* @since 2025-07-24
|
||||
*/
|
||||
@RestController
|
||||
@CrossOrigin
|
||||
@RequestMapping("/api/v1/config")
|
||||
public class ViteConfigController extends BaseController {
|
||||
|
||||
/**
|
||||
* 获取所有网站配置
|
||||
* 前端无需权限即可访问,用于获取网站基本信息
|
||||
*/
|
||||
@LogAnnotation
|
||||
@PostMapping("/list")
|
||||
public R getConfigs() {
|
||||
return viteConfigService.getConfigs();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据配置名获取配置值
|
||||
* 前端无需权限即可访问,用于获取特定配置
|
||||
*/
|
||||
@LogAnnotation
|
||||
@PostMapping("/get")
|
||||
public R getConfigByName(@RequestBody Map<String, Object> params) {
|
||||
String name = params.get("name").toString();
|
||||
return viteConfigService.getConfigByName(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新网站配置
|
||||
* 需要管理员权限
|
||||
*/
|
||||
@LogAnnotation
|
||||
@RequireRole
|
||||
@PostMapping("/update")
|
||||
public R updateConfigs(@RequestBody Map<String, String> configMap) {
|
||||
return viteConfigService.updateConfigs(configMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新单个配置项
|
||||
* 需要管理员权限
|
||||
*/
|
||||
@LogAnnotation
|
||||
@RequireRole
|
||||
@PostMapping("/update-single")
|
||||
public R updateConfig(@RequestBody Map<String, Object> params) {
|
||||
String name = params.get("name").toString();
|
||||
String value = params.get("value").toString();
|
||||
return viteConfigService.updateConfig(name, value);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package com.admin.entity;
|
||||
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
public class ChainTunnel implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
// 关联的隧道ID
|
||||
private Long tunnelId;
|
||||
|
||||
// 记录所选节点的类型: 1是入口节点 2是转发链 3是出口节点
|
||||
private Integer chainType;
|
||||
|
||||
// 记录所选节点的ID
|
||||
private Long nodeId;
|
||||
|
||||
// 记录节点使用的端口 入口没有次配置为null
|
||||
private Integer port;
|
||||
|
||||
// 负载模式 入口没有次配置为null
|
||||
private String strategy;
|
||||
|
||||
// 只有转发链有次参数 表示是第几条
|
||||
private Integer inx;
|
||||
|
||||
// 使用的协议 入口没有次配置为null
|
||||
private String protocol;
|
||||
|
||||
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package com.admin.entity;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
import com.admin.common.dto.ForwardPortDto;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
*
|
||||
* </p>
|
||||
*
|
||||
* @author QAQ
|
||||
* @since 2025-06-03
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@TableName(autoResultMap = true)
|
||||
public class Forward extends BaseEntity{
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Integer userId;
|
||||
|
||||
private String userName;
|
||||
|
||||
private String name;
|
||||
|
||||
private Integer tunnelId;
|
||||
|
||||
private String remoteAddr;
|
||||
|
||||
private String strategy;
|
||||
|
||||
private Long inFlow;
|
||||
|
||||
private Long outFlow;
|
||||
|
||||
private Integer inx;
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package com.admin.entity;
|
||||
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@TableName(autoResultMap = true)
|
||||
public class ForwardPort{
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
private Long forwardId;
|
||||
|
||||
private Long nodeId;
|
||||
|
||||
private Integer port;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
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 GroupPermission implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
private Long userGroupId;
|
||||
|
||||
private Long tunnelGroupId;
|
||||
|
||||
private Long createdTime;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user