Compare commits

...

27 Commits

Author SHA1 Message Date
qaq 761db1844d 修复流量暂停问题 2026-01-06 11:30:39 +08:00
qaq ca76cc29de 修复流量暂停问题 2026-01-06 11:26:43 +08:00
qaq c20a78a196 修复流量问题 2026-01-06 11:23:16 +08:00
qaq 2053cbec32 优化版本管理 2025-11-25 09:56:20 +08:00
bqlpfy 687e9c78ad Fix punctuation in README warning about version 2.0 2025-11-24 22:48:51 +08:00
bqlpfy 07105309fb Add warning for version 2.0 installation
Added warning about not installing version 2.0 due to issues.
2025-11-24 22:47:52 +08:00
qaq 31290137fe 优化gost执行诊断时计入DNS解析时间的问题 2025-11-21 16:27:58 +08:00
qaq 4a23f3b45a 优化gost执行诊断时计入DNS解析时间的问题 2025-11-21 16:24:45 +08:00
qaq 0b2974726c 修修复转发链v6格式化错误的问题 2025-11-21 10:17:53 +08:00
qaq 43aebdf8f4 修复agent时携带错误的屏蔽协议数据,编辑转发时无法使用原分配端口 2025-11-20 13:51:48 +08:00
qaq 37ea338a35 修复WAL模式下丢失数据问题 2025-11-20 12:15:30 +08:00
qaq 4063d39d6a 添加赞助商 2025-11-20 11:37:21 +08:00
qaq f6dd3ad657 修复v6,重复分配端口,指定网卡失败的问题 2025-11-20 11:27:58 +08:00
qaq 252c16de29 统一agent命名规则 2025-11-19 17:30:39 +08:00
qaq 4fef8eeb32 修复出口协议无法选择,加入自定义转发端口,增加arm支持 2025-11-19 16:32:48 +08:00
qaq 9146eb0375 修复节点安装脚本 2025-11-19 14:56:25 +08:00
qaq ad974875e4 2.0.0-beta测试版本 2025-11-19 14:36:42 +08:00
qaq acd00ae413 2.0版本测试 2025-11-19 14:31:38 +08:00
qaq 65ca18d649 2.0版本测试 2025-11-19 14:28:45 +08:00
qaq a27b4d781e chore: bump version to 1.4.2 2025-11-14 11:19:54 +08:00
qaq ba50076468 chore: bump version to 1.4.2 2025-11-14 10:30:37 +08:00
qaq b83f6c0023 添加版本管理 2025-11-14 10:06:29 +08:00
qaq d0822fc3ef test 2025-11-13 16:49:20 +08:00
qaq f83d4d76be test 2025-11-13 16:40:55 +08:00
qaq 70ff6969e7 test 2025-11-13 16:35:12 +08:00
qaq 374bafec57 test 2025-11-13 16:26:49 +08:00
qaq d9992d433d 测试版管理 2025-11-13 16:24:06 +08:00
134 changed files with 6940 additions and 6607 deletions
+233 -87
View File
@@ -1,43 +1,79 @@
name: Build and Push Vue or Spring Boot Images
name: Build and Push Images Based on Version
# 在这里定义统一版本号
env:
VERSION: "2.0.7-beta"
on:
push:
branches:
- main
paths:
- 'vue-frontend/**'
- 'vite-frontend/**'
- 'springboot-backend/**'
- 'go-gost/**'
- 'android-app/**'
- '.github/workflows/docker-build.yml'
- beta
jobs:
build-gost:
if: contains(github.event.head_commit.message, '[skip gost]') == false
name: Build & Compress GOST Binary
check-version:
name: Check Version and Decide Build
runs-on: ubuntu-latest
outputs:
version: ${{ env.VERSION }}
should_build: ${{ steps.check-tag.outputs.should_build }}
should_build_gost: ${{ steps.check-tag.outputs.should_build_gost }}
tag_exists: ${{ steps.check-tag.outputs.tag_exists }}
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Display version
run: |
echo "Current version: ${{ env.VERSION }}"
- name: Check if tag exists and detect changes
id: check-tag
run: |
if git rev-parse "${{ env.VERSION }}" >/dev/null 2>&1; then
echo "Tag ${{ env.VERSION }} already exists"
echo "tag_exists=true" >> $GITHUB_OUTPUT
echo "should_build=false" >> $GITHUB_OUTPUT
# 检查从tag到现在,go-gost目录下的文件是否有变化
TAG_COMMIT=$(git rev-list -n 1 "${{ env.VERSION }}")
echo "Tag commit: $TAG_COMMIT"
echo "Current HEAD: $(git rev-parse HEAD)"
# 检查 go-gost 目录下是否有实际的文件内容变化
# 使用 --ignore-all-space 和 --ignore-blank-lines 来忽略空白差异
# 使用 --exit-code: 0表示无差异,1表示有差异
if git diff --quiet --ignore-all-space --ignore-blank-lines $TAG_COMMIT HEAD -- go-gost/ 2>/dev/null; then
echo "✅ GOST files unchanged since tag (no content changes)"
echo "should_build_gost=false" >> $GITHUB_OUTPUT
else
# 显示差异以便调试
echo "🔄 Detected changes in go-gost directory:"
git diff --stat $TAG_COMMIT HEAD -- go-gost/ || true
echo "should_build_gost=true" >> $GITHUB_OUTPUT
fi
else
echo "Tag ${{ env.VERSION }} does not exist, will build all components"
echo "should_build=true" >> $GITHUB_OUTPUT
echo "should_build_gost=true" >> $GITHUB_OUTPUT
echo "tag_exists=false" >> $GITHUB_OUTPUT
fi
build-gost:
name: Build & Compress GOST Binary
needs: check-version
if: needs.check-version.outputs.should_build_gost == 'true'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v3
- name: Check for GOST changes
uses: dorny/paths-filter@v2
id: gost-changes
with:
filters: |
gost:
- 'go-gost/**'
- name: Set up Go
if: steps.gost-changes.outputs.gost == 'true'
uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: Cache Go modules
if: steps.gost-changes.outputs.gost == 'true'
uses: actions/cache@v3
with:
path: |
@@ -48,118 +84,91 @@ jobs:
${{ runner.os }}-go-
- name: Install UPX
if: steps.gost-changes.outputs.gost == 'true'
run: |
wget https://github.com/upx/upx/releases/download/v4.2.1/upx-4.2.1-amd64_linux.tar.xz
tar -xf upx-4.2.1-amd64_linux.tar.xz
sudo mv upx-4.2.1-amd64_linux/upx /usr/local/bin/
rm -rf upx-4.2.1-amd64_linux*
- name: Build GOST binary
if: steps.gost-changes.outputs.gost == 'true'
- name: Build GOST binary (AMD64)
working-directory: ./go-gost
run: |
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o gost
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o gost-amd64
- name: Compress with UPX
if: steps.gost-changes.outputs.gost == 'true'
- name: Build GOST binary (ARM64)
working-directory: ./go-gost
run: |
upx --best --lzma gost
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o gost-arm64
- name: Create or Update Release
if: steps.gost-changes.outputs.gost == 'true'
env:
GH_TOKEN: ${{ github.token }}
- name: Compress with UPX (AMD64)
working-directory: ./go-gost
run: |
# 确保 tag 指向当前 commit
git tag -f gost-latest ${{ github.sha }}
git push origin gost-latest --force
upx --best --lzma gost-amd64
# 检查 release 是否存在,不存在则创建
if gh release view gost-latest 2>/dev/null; then
echo "Release exists, will update assets"
else
echo "Release not found, creating new release"
gh release create gost-latest gost-latest \
--title "GOST Binary Latest" \
--notes "GOST binary compiled from commit ${{ github.sha }}
- Platform: Linux AMD64
- Compressed with UPX
- Build date: ${{ github.event.head_commit.timestamp }}" \
--repo ${{ github.repository }}
fi
- name: Compress with UPX (ARM64)
working-directory: ./go-gost
run: |
upx --best --lzma gost-arm64
# 上传 asset 并覆盖同名文件
gh release upload gost-latest ./go-gost/gost --clobber
- name: Upload GOST AMD64 artifact
uses: actions/upload-artifact@v4
with:
name: gost-binary-amd64
path: ./go-gost/gost-amd64
- name: Upload GOST ARM64 artifact
uses: actions/upload-artifact@v4
with:
name: gost-binary-arm64
path: ./go-gost/gost-arm64
build-vite:
if: contains(github.event.head_commit.message, '[skip vite]') == false
name: Build & Push Vite Frontend
needs: check-version
if: needs.check-version.outputs.should_build == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Check for Vite changes
uses: dorny/paths-filter@v2
id: vite-changes
with:
filters: |
vite:
- 'vite-frontend/**'
- name: Set up Node.js
if: steps.vite-changes.outputs.vite == 'true'
uses: actions/setup-node@v4
with:
node-version: 20
- name: Set up Docker Buildx
if: steps.vite-changes.outputs.vite == 'true'
uses: docker/setup-buildx-action@v2
- name: Log in to DockerHub
if: steps.vite-changes.outputs.vite == 'true'
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Build and push Vite multi-arch image
if: steps.vite-changes.outputs.vite == 'true'
- name: Build and push Vite Docker images
run: |
VERSION="${{ needs.check-version.outputs.version }}"
docker buildx build \
--platform linux/amd64 \
--platform linux/amd64,linux/arm64 \
--push \
-t bqlpfy/vite-frontend \
-t bqlpfy/vite-frontend:latest \
-t bqlpfy/vite-frontend:${VERSION} \
./vite-frontend
build-java:
if: contains(github.event.head_commit.message, '[skip java]') == false
name: Build & Push Spring Boot Backend
needs: check-version
if: needs.check-version.outputs.should_build == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Check for Java changes
uses: dorny/paths-filter@v2
id: java-changes
with:
filters: |
java:
- 'springboot-backend/**'
- name: Set up JDK and Maven
if: steps.java-changes.outputs.java == 'true'
uses: actions/setup-java@v3
with:
java-version: 21
distribution: 'temurin'
- name: Cache Maven dependencies
if: steps.java-changes.outputs.java == 'true'
uses: actions/cache@v3
with:
path: ~/.m2
@@ -167,26 +176,163 @@ jobs:
restore-keys: ${{ runner.os }}-m2
- name: Build Java JAR
if: steps.java-changes.outputs.java == 'true'
working-directory: ./springboot-backend
run: mvn clean package -DskipTests
- name: Set up Docker Buildx
if: steps.java-changes.outputs.java == 'true'
uses: docker/setup-buildx-action@v2
- name: Log in to DockerHub
if: steps.java-changes.outputs.java == 'true'
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Build and push Java multi-arch image
if: steps.java-changes.outputs.java == 'true'
- name: Build and push Java Docker images
run: |
VERSION="${{ needs.check-version.outputs.version }}"
docker buildx build \
--platform linux/amd64 \
--platform linux/amd64,linux/arm64 \
--push \
-t bqlpfy/springboot-backend \
-t bqlpfy/springboot-backend:latest \
-t bqlpfy/springboot-backend:${VERSION} \
./springboot-backend
create-release:
name: Create Release and Tag
needs: [check-version, build-gost, build-vite, build-java]
if: needs.check-version.outputs.should_build == 'true'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v3
- name: Download GOST AMD64 binary
uses: actions/download-artifact@v4
with:
name: gost-binary-amd64
path: ./artifacts/amd64
- name: Download GOST ARM64 binary
uses: actions/download-artifact@v4
with:
name: gost-binary-arm64
path: ./artifacts/arm64
- name: Rename binaries
run: |
mv ./artifacts/amd64/gost-amd64 ./artifacts/gost-amd64
mv ./artifacts/arm64/gost-arm64 ./artifacts/gost-arm64
- name: Create Release
env:
GH_TOKEN: ${{ github.token }}
run: |
VERSION="${{ needs.check-version.outputs.version }}"
# 创建并推送 tag
git tag "${VERSION}" ${{ github.sha }}
git push origin "${VERSION}"
# 获取 commit 信息
COMMIT_MSG=$(git log -1 --pretty=format:"%s")
COMMIT_AUTHOR=$(git log -1 --pretty=format:"%an")
COMMIT_DATE=$(git log -1 --pretty=format:"%ai")
# 创建 release
gh release create "${VERSION}" \
--title "Release ${VERSION}" \
--notes "## 📝 Commit Information
- **Message**: ${COMMIT_MSG}
- **Author**: ${COMMIT_AUTHOR}
- **Date**: ${COMMIT_DATE}
- **Commit**: [\`${GITHUB_SHA:0:7}\`](https://github.com/${{ github.repository }}/commit/${{ github.sha }})" \
--repo ${{ github.repository }}
# 上传所有文件到 release
echo "📤 上传 GOST 二进制文件..."
gh release upload "${VERSION}" ./artifacts/gost-amd64 --clobber
gh release upload "${VERSION}" ./artifacts/gost-arm64 --clobber
echo "📤 上传安装脚本..."
gh release upload "${VERSION}" ./install.sh --clobber
gh release upload "${VERSION}" ./panel_install.sh --clobber
echo "📤 上传 Docker Compose 配置文件..."
gh release upload "${VERSION}" ./docker-compose-v4.yml --clobber
gh release upload "${VERSION}" ./docker-compose-v6.yml --clobber
echo "✅ 所有文件已上传到 Release ${VERSION}"
update-release-gost:
name: Update GOST Binaries in Release
needs: [check-version, build-gost]
if: needs.check-version.outputs.should_build == 'false' && needs.check-version.outputs.should_build_gost == 'true'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v3
- name: Download GOST AMD64 binary
uses: actions/download-artifact@v4
with:
name: gost-binary-amd64
path: ./artifacts/amd64
- name: Download GOST ARM64 binary
uses: actions/download-artifact@v4
with:
name: gost-binary-arm64
path: ./artifacts/arm64
- name: Rename binaries
run: |
mv ./artifacts/amd64/gost-amd64 ./artifacts/gost-amd64
mv ./artifacts/arm64/gost-arm64 ./artifacts/gost-arm64
- name: Update GOST binaries in Release
env:
GH_TOKEN: ${{ github.token }}
run: |
VERSION="${{ needs.check-version.outputs.version }}"
echo "🔄 更新 Release ${VERSION} 中的 GOST 二进制文件..."
gh release upload "${VERSION}" ./artifacts/gost-amd64 --clobber
gh release upload "${VERSION}" ./artifacts/gost-arm64 --clobber
echo "✅ GOST 二进制文件更新完成!"
echo "ℹ️ Docker 镜像未重新构建"
update-release-files:
name: Update Release Files Only
needs: check-version
if: needs.check-version.outputs.tag_exists == 'true' && needs.check-version.outputs.should_build == 'false' && needs.check-version.outputs.should_build_gost == 'false'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v3
- name: Update Release Files
env:
GH_TOKEN: ${{ github.token }}
run: |
VERSION="${{ needs.check-version.outputs.version }}"
echo "📤 更新 Release ${VERSION} 中的脚本和配置文件..."
# 上传安装脚本(覆盖)
echo "📤 更新安装脚本..."
gh release upload "${VERSION}" ./install.sh --clobber
gh release upload "${VERSION}" ./panel_install.sh --clobber
# 上传 Docker Compose 配置文件(覆盖)
echo "📤 更新 Docker Compose 配置文件..."
gh release upload "${VERSION}" ./docker-compose-v4.yml --clobber
gh release upload "${VERSION}" ./docker-compose-v6.yml --clobber
echo "✅ 文件更新完成!Release ${VERSION} 中的脚本和配置已更新"
echo "ℹ️ Docker 镜像和 GOST 二进制未重新构建"
+3 -4
View File
@@ -254,8 +254,7 @@ gitee/
# 证书文件
*.crt
*.cert
android-app/
ios-app/
doraemon.jks
flux.ipa
device.id
device.id
commit.sh
sql/
+29 -5
View File
@@ -1,6 +1,16 @@
# flux-panel转发面板 哆啦A梦转发面板
# 赞助商
<p align="center">
<a href="https://vps.town" style="margin: 0 20px; text-align:center;">
<img src="./doc/vpstown.png" width="300">
</a>
<a href="https://whmcs.as211392.com" style="margin: 0 20px; text-align:center;">
<img src="./doc/as211392.png" width="300">
</a>
</p>
本项目基于 [go-gost/gost](https://github.com/go-gost/gost) 和 [go-gost/x](https://github.com/go-gost/x) 两个开源库,实现了转发面板。
---
@@ -17,10 +27,26 @@
## 部署流程
---
### Docker Compose部署
#### 快速部署
面板端(稳定版):
```bash
curl -L https://raw.githubusercontent.com/bqlpfy/flux-panel/refs/heads/main/panel_install.sh -o panel_install.sh && chmod +x panel_install.sh && ./panel_install.sh
```
节点端(稳定版):
```bash
curl -L https://raw.githubusercontent.com/bqlpfy/flux-panel/refs/heads/main/install.sh -o install.sh && chmod +x install.sh && ./install.sh
```
面板端(开发版):
```bash
curl -L https://raw.githubusercontent.com/bqlpfy/flux-panel/refs/heads/beta/panel_install.sh -o panel_install.sh && chmod +x panel_install.sh && ./panel_install.sh
```
节点端(开发版):
```bash
curl -L https://raw.githubusercontent.com/bqlpfy/flux-panel/refs/heads/beta/install.sh -o install.sh && chmod +x install.sh && ./install.sh
```
#### 默认管理员账号
@@ -55,7 +81,7 @@ curl -L https://raw.githubusercontent.com/bqlpfy/flux-panel/refs/heads/main/pane
请务必在合法、合规、安全的前提下使用本项目。
---
## ⭐ 喝杯咖啡!
## ⭐ 喝杯咖啡!(USDT)
| 网络 | 地址 |
|------------|----------------------------------------------------------------------|
@@ -63,6 +89,4 @@ curl -L https://raw.githubusercontent.com/bqlpfy/flux-panel/refs/heads/main/pane
| TRC20 | `TYh2L3xxXpuJhAcBWnt3yiiADiCSJLgUm7` |
| Aptos | `0xf2f9fb14749457748506a8281628d556e8540d1eb586d202cd8b02b99d369ef8` |
[![Star History Chart](https://api.star-history.com/svg?repos=bqlpfy/flux-panel&type=Date)](https://www.star-history.com/#bqlpfy/flux-panel&Date)
[![Star History Chart](https://api.star-history.com/svg?repos=bqlpfy/flux-panel&type=Date)](https://www.star-history.com/#bqlpfy/flux-panel&Date)
+15
View File
@@ -0,0 +1,15 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
+1
View File
@@ -0,0 +1 @@
/build
+45
View File
@@ -0,0 +1,45 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}
android {
namespace 'com.flux'
compileSdk 34
defaultConfig {
applicationId "com.flux"
minSdk 24
targetSdk 34
versionCode 1
versionName "1.0.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.appcompat:appcompat:1.7.1'
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation 'androidx.webkit:webkit:1.8.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.3.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0'
}
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
Binary file not shown.
@@ -0,0 +1,20 @@
{
"version": 3,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "com.flux",
"variantName": "release",
"elements": [
{
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 1,
"versionName": "1.0.1",
"outputFile": "app-release.apk"
}
],
"elementType": "File"
}
@@ -0,0 +1,24 @@
package com.flux
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.flux", appContext.packageName)
}
}
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.flux" >
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Flux"
android:usesCleartextTraffic="true"
tools:targetApi="31" >
<activity
android:name="com.flux.MainActivity"
android:exported="true"
android:configChanges="uiMode|orientation|screenSize" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Binary file not shown.

After

Width:  |  Height:  |  Size: 406 KiB

@@ -0,0 +1,214 @@
package com.flux
import androidx.core.content.edit
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.view.View
import android.view.WindowInsetsController
import androidx.appcompat.app.AppCompatActivity
import android.webkit.*
import android.content.res.Configuration
import org.json.JSONArray
import org.json.JSONObject
class MainActivity : AppCompatActivity() {
private lateinit var webView: WebView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
webView = findViewById(R.id.webView)
WebView.setWebContentsDebuggingEnabled(true)
setupWebView()
loadUrl()
setupSystemBars()
setupRootBackground()
}
private fun setupWebView() {
val settings = webView.settings
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
settings.databaseEnabled = true
settings.loadWithOverviewMode = true
settings.useWideViewPort = true
settings.setSupportZoom(true)
settings.builtInZoomControls = true
settings.displayZoomControls = false
// 隐藏滚动条但不影响滚动
webView.isVerticalScrollBarEnabled = false
webView.isHorizontalScrollBarEnabled = false
webView.scrollBarStyle = View.SCROLLBARS_INSIDE_OVERLAY
webView.overScrollMode = View.OVER_SCROLL_NEVER
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
}
val isDarkTheme = resources.configuration.uiMode and
Configuration.UI_MODE_NIGHT_MASK ==
Configuration.UI_MODE_NIGHT_YES
webView.setBackgroundColor(if (isDarkTheme) Color.BLACK else Color.WHITE)
// 添加JavaScript接口
webView.addJavascriptInterface(object {
@android.webkit.JavascriptInterface
fun getPanelAddresses(callback: String) {
val sharedPrefs = getSharedPreferences("panel_config", MODE_PRIVATE)
val addresses = sharedPrefs.getString("panel_addresses", "[]")
webView.post {
webView.evaluateJavascript("window.$callback($addresses);", null)
}
}
@android.webkit.JavascriptInterface
fun savePanelAddress(name: String, address: String) {
val json = JSONObject()
json.put("name", name)
json.put("address", address)
json.put("inx", false)
val sharedPrefs = getSharedPreferences("panel_config", MODE_PRIVATE)
val addresses = sharedPrefs.getString("panel_addresses", "[]")
val jsonArray = JSONArray(addresses)
jsonArray.put(json)
sharedPrefs.edit {
putString("panel_addresses", jsonArray.toString())
}
webView.post {
webView.evaluateJavascript("window.setPanelAddresses(${jsonArray.toString()});", null)
}
}
@android.webkit.JavascriptInterface
fun setCurrentPanelAddress(name: String) {
val sharedPrefs = getSharedPreferences("panel_config", MODE_PRIVATE)
val addresses = sharedPrefs.getString("panel_addresses", "[]")
val jsonArray = JSONArray(addresses)
for (i in 0 until jsonArray.length()) {
val obj = jsonArray.getJSONObject(i)
if (name == obj.getString("name")) {
obj.put("inx", true)
}else{
obj.put("inx", false)
}
}
sharedPrefs.edit {
putString("panel_addresses", jsonArray.toString())
}
webView.post {
webView.evaluateJavascript("window.setPanelAddresses(${jsonArray.toString()});", null)
}
}
@android.webkit.JavascriptInterface
fun deletePanelAddress(name: String) {
val jsonArray = JSONArray()
val sharedPrefs = getSharedPreferences("panel_config", MODE_PRIVATE)
val addresses = sharedPrefs.getString("panel_addresses", "[]")
val jsonArraya = JSONArray(addresses)
for (i in 0 until jsonArraya.length()) {
val obj = jsonArraya.getJSONObject(i)
if (name != obj.getString("name")) {
jsonArray.put(obj)
}
}
sharedPrefs.edit {
putString("panel_addresses", jsonArray.toString())
}
webView.post {
webView.evaluateJavascript("window.setPanelAddresses(${jsonArray.toString()});", null)
}
}
}, "JsInterface")
}
private fun loadUrl() {
//webView.loadUrl("http://192.168.100.9:3000")
webView.loadUrl("file:///android_asset/index.html")
}
private fun setupSystemBars() {
val isDarkTheme = resources.configuration.uiMode and
Configuration.UI_MODE_NIGHT_MASK ==
Configuration.UI_MODE_NIGHT_YES
if (isDarkTheme) {
window.statusBarColor = Color.BLACK
window.navigationBarColor = Color.BLACK
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.insetsController?.setSystemBarsAppearance(
0,
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS or
WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS
)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@Suppress("DEPRECATION")
window.decorView.systemUiVisibility =
window.decorView.systemUiVisibility and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@Suppress("DEPRECATION")
window.decorView.systemUiVisibility =
window.decorView.systemUiVisibility and View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.inv()
}
}
} else {
window.statusBarColor = Color.WHITE
window.navigationBarColor = Color.WHITE
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.insetsController?.setSystemBarsAppearance(
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS or
WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS,
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS or
WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS
)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@Suppress("DEPRECATION")
window.decorView.systemUiVisibility =
window.decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@Suppress("DEPRECATION")
window.decorView.systemUiVisibility =
window.decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
}
}
}
}
override fun onBackPressed() {
if (webView.canGoBack()) webView.goBack() else super.onBackPressed()
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
setupSystemBars()
setupWebViewBackground()
setupRootBackground()
}
private fun setupWebViewBackground() {
val isDarkTheme = resources.configuration.uiMode and
Configuration.UI_MODE_NIGHT_MASK ==
Configuration.UI_MODE_NIGHT_YES
webView.setBackgroundColor(if (isDarkTheme) Color.BLACK else Color.WHITE)
}
private fun setupRootBackground() {
val isDarkTheme = resources.configuration.uiMode and
Configuration.UI_MODE_NIGHT_MASK ==
Configuration.UI_MODE_NIGHT_YES
val rootView = findViewById<View>(android.R.id.content)
rootView.setBackgroundColor(if (isDarkTheme) Color.BLACK else Color.WHITE)
}
}
@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
android:height="108dp"
android:width="108dp"
android:viewportHeight="108"
android:viewportWidth="108"
xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z"/>
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
</vector>
@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<WebView
android:id="@+id/webView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

@@ -0,0 +1,9 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.Flux" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your dark theme here. -->
<!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
</style>
</resources>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
@@ -0,0 +1,3 @@
<resources>
<string name="app_name">Flux</string>
</resources>
@@ -0,0 +1,11 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.Flux" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your light theme here. -->
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
</style>
<style name="Theme.Flux" parent="Base.Theme.Flux" />
</resources>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older that API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>
@@ -0,0 +1,17 @@
package com.flux
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
+5
View File
@@ -0,0 +1,5 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '8.2.2' apply false
id 'org.jetbrains.kotlin.android' version '1.8.0' apply false
}
+23
View File
@@ -0,0 +1,23 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
+6
View File
@@ -0,0 +1,6 @@
#Wed Aug 20 09:15:27 CST 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored Executable
+185
View File
@@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"
+89
View File
@@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
+17
View File
@@ -0,0 +1,17 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "Flux"
include ':app'
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

+9 -38
View File
@@ -1,39 +1,10 @@
services:
mysql:
image: mysql:5.7
container_name: gost-mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
MYSQL_DATABASE: ${DB_NAME}
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASSWORD}
TZ: Asia/Shanghai
volumes:
- mysql_data:/var/lib/mysql
- ./gost.sql:/docker-entrypoint-initdb.d/init.sql:ro
command: >
--default-authentication-plugin=mysql_native_password
--character-set-server=utf8mb4
--collation-server=utf8mb4_unicode_ci
--max_connections=1000
--innodb_buffer_pool_size=256M
networks:
- gost-network
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
timeout: 10s
retries: 10
backend:
image: bqlpfy/springboot-backend:latest
image: bqlpfy/springboot-backend:2.0.7-beta
container_name: springboot-backend
restart: unless-stopped
environment:
DB_HOST: mysql
DB_NAME: ${DB_NAME}
DB_USER: ${DB_USER}
DB_PASSWORD: ${DB_PASSWORD}
DB_PATH: /app/data/gost.db
JWT_SECRET: ${JWT_SECRET}
LOG_DIR: /app/logs
JAVA_OPTS: "-Xms256m -Xmx512m -Dfile.encoding=UTF-8 -Duser.timezone=Asia/Shanghai"
@@ -41,20 +12,20 @@ services:
- "${BACKEND_PORT}:6365"
volumes:
- backend_logs:/app/logs
depends_on:
mysql:
condition: service_healthy
- sqlite_data:/app/data
networks:
- gost-network
stop_grace_period: 30s
stop_signal: SIGTERM
healthcheck:
test: ["CMD", "sh", "-c", "wget --no-verbose --tries=1 --spider http://localhost:6365/flow/test || exit 1"]
interval: 30s
timeout: 10s
retries: 5
start_period: 90s
start_period: 60s
frontend:
image: bqlpfy/vite-frontend:latest
image: bqlpfy/vite-frontend:2.0.7-beta
container_name: vite-frontend
restart: unless-stopped
ports:
@@ -67,8 +38,8 @@ services:
volumes:
mysql_data:
name: mysql_data
sqlite_data:
name: sqlite_data
driver: local
backend_logs:
name: backend_logs
+9 -38
View File
@@ -1,39 +1,10 @@
services:
mysql:
image: mysql:5.7
container_name: gost-mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
MYSQL_DATABASE: ${DB_NAME}
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASSWORD}
TZ: Asia/Shanghai
volumes:
- mysql_data:/var/lib/mysql
- ./gost.sql:/docker-entrypoint-initdb.d/init.sql:ro
command: >
--default-authentication-plugin=mysql_native_password
--character-set-server=utf8mb4
--collation-server=utf8mb4_unicode_ci
--max_connections=1000
--innodb_buffer_pool_size=256M
networks:
- gost-network
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
timeout: 10s
retries: 10
backend:
image: bqlpfy/springboot-backend:latest
image: bqlpfy/springboot-backend:2.0.7-beta
container_name: springboot-backend
restart: unless-stopped
environment:
DB_HOST: mysql
DB_NAME: ${DB_NAME}
DB_USER: ${DB_USER}
DB_PASSWORD: ${DB_PASSWORD}
DB_PATH: /app/data/gost.db
JWT_SECRET: ${JWT_SECRET}
LOG_DIR: /app/logs
JAVA_OPTS: "-Xms256m -Xmx512m -Dfile.encoding=UTF-8 -Duser.timezone=Asia/Shanghai"
@@ -41,20 +12,20 @@ services:
- "${BACKEND_PORT}:6365"
volumes:
- backend_logs:/app/logs
depends_on:
mysql:
condition: service_healthy
- sqlite_data:/app/data
networks:
- gost-network
stop_grace_period: 30s
stop_signal: SIGTERM
healthcheck:
test: ["CMD", "sh", "-c", "wget --no-verbose --tries=1 --spider http://localhost:6365/flow/test || exit 1"]
interval: 30s
timeout: 10s
retries: 5
start_period: 90s
start_period: 60s
frontend:
image: bqlpfy/vite-frontend:latest
image: bqlpfy/vite-frontend:2.0.7-beta
container_name: vite-frontend
restart: unless-stopped
ports:
@@ -67,8 +38,8 @@ services:
volumes:
mysql_data:
name: mysql_data
sqlite_data:
name: sqlite_data
driver: local
backend_logs:
name: backend_logs
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -119,7 +119,7 @@ func main() {
log := xlogger.NewLogger()
logger.SetDefault(log)
wsReporter := socket.StartWebSocketReporterWithConfig(config.Addr, config.Secret, config.Http, config.Tls, config.Socks, "1.2.3")
wsReporter := socket.StartWebSocketReporterWithConfig(config.Addr, config.Secret, config.Http, config.Tls, config.Socks, "2.0.2")
defer wsReporter.Stop()
service.SetHTTPReportURL(config.Addr, config.Secret)
+206
View File
@@ -0,0 +1,206 @@
package service
import (
"context"
"fmt"
"sync"
"time"
)
// GlobalTrafficManager 全局流量管理器(所有服务共享)
type GlobalTrafficManager struct {
mu sync.RWMutex
serviceTraffic map[string]*ServiceTraffic // key: 服务名, value: 流量数据
ctx context.Context
cancel context.CancelFunc
reportTicker *time.Ticker
}
// ServiceTraffic 单个服务的流量累积
type ServiceTraffic struct {
mu sync.Mutex
ServiceName string
UpBytes int64 // 上行流量(累积)
DownBytes int64 // 下行流量(累积)
}
var (
globalManager *GlobalTrafficManager
globalManagerOnce sync.Once
)
// GetGlobalTrafficManager 获取全局流量管理器单例
func GetGlobalTrafficManager() *GlobalTrafficManager {
globalManagerOnce.Do(func() {
ctx, cancel := context.WithCancel(context.Background())
globalManager = &GlobalTrafficManager{
serviceTraffic: make(map[string]*ServiceTraffic),
ctx: ctx,
cancel: cancel,
reportTicker: time.NewTicker(5 * time.Second),
}
// 启动定时上报协程
go globalManager.startReporting()
})
return globalManager
}
// AddTraffic 添加流量到指定服务(由各服务调用)
func (m *GlobalTrafficManager) AddTraffic(serviceName string, upBytes, downBytes int64) {
if upBytes == 0 && downBytes == 0 {
return
}
m.mu.Lock()
defer m.mu.Unlock()
// 获取或创建服务流量记录
traffic, exists := m.serviceTraffic[serviceName]
if !exists {
traffic = &ServiceTraffic{
ServiceName: serviceName,
}
m.serviceTraffic[serviceName] = traffic
}
// 累加流量
traffic.mu.Lock()
traffic.UpBytes += upBytes
traffic.DownBytes += downBytes
traffic.mu.Unlock()
}
// startReporting 启动定时上报协程(每5秒执行一次)
func (m *GlobalTrafficManager) startReporting() {
for {
select {
case <-m.reportTicker.C:
m.collectAndReport()
case <-m.ctx.Done():
fmt.Printf("⏹️ 全局流量上报器已停止\n")
return
}
}
}
// collectAndReport 收集所有服务流量并合并上报
func (m *GlobalTrafficManager) collectAndReport() {
m.mu.Lock()
// 如果没有流量,直接返回
if len(m.serviceTraffic) == 0 {
m.mu.Unlock()
return
}
// 复制当前所有流量数据(避免长时间持锁)
trafficSnapshot := make(map[string]*ServiceTraffic)
reportData := make(map[string]struct {
up int64
down int64
})
for name, traffic := range m.serviceTraffic {
traffic.mu.Lock()
if traffic.UpBytes > 0 || traffic.DownBytes > 0 {
trafficSnapshot[name] = traffic
reportData[name] = struct {
up int64
down int64
}{
up: traffic.UpBytes,
down: traffic.DownBytes,
}
}
traffic.mu.Unlock()
}
m.mu.Unlock()
// 如果没有需要上报的流量,返回
if len(reportData) == 0 {
return
}
// 构建上报数据数组(保持每个服务独立)
reportItems := make([]TrafficReportItem, 0, len(reportData))
var totalUp, totalDown int64
for serviceName, data := range reportData {
reportItems = append(reportItems, TrafficReportItem{
N: serviceName, // 保持服务名不变
U: data.up,
D: data.down,
})
totalUp += data.up
totalDown += data.down
}
// 批量发送上报请求(一次HTTP请求包含所有服务)
success, err := sendBatchTrafficReport(m.ctx, reportItems)
if err != nil {
fmt.Printf("❌ 全局流量上报失败: %v (总流量: ↑%d ↓%d, %d个服务)\n", err, totalUp, totalDown, len(reportItems))
return
}
if !success {
fmt.Printf("⚠️ 全局流量上报未成功 (总流量: ↑%d ↓%d, %d个服务)\n", totalUp, totalDown, len(reportItems))
return
}
// 上报成功,清空已上报的流量
m.clearReportedTraffic(reportData)
}
// clearReportedTraffic 清空已成功上报的流量
func (m *GlobalTrafficManager) clearReportedTraffic(reportedData map[string]struct {
up int64
down int64
}) {
m.mu.Lock()
defer m.mu.Unlock()
for serviceName, reported := range reportedData {
if traffic, exists := m.serviceTraffic[serviceName]; exists {
traffic.mu.Lock()
// 减去已上报的流量
traffic.UpBytes -= reported.up
traffic.DownBytes -= reported.down
// 如果流量归零,从map中删除该服务记录(避免内存泄漏)
if traffic.UpBytes <= 0 && traffic.DownBytes <= 0 {
traffic.mu.Unlock()
delete(m.serviceTraffic, serviceName)
} else {
traffic.mu.Unlock()
}
}
}
}
// Stop 停止全局流量管理器
func (m *GlobalTrafficManager) Stop() {
if m.reportTicker != nil {
m.reportTicker.Stop()
}
if m.cancel != nil {
m.cancel()
}
fmt.Printf("🛑 全局流量管理器已停止\n")
}
// GetServiceTraffic 获取指定服务的当前流量(用于调试)
func (m *GlobalTrafficManager) GetServiceTraffic(serviceName string) (upBytes, downBytes int64) {
m.mu.RLock()
defer m.mu.RUnlock()
if traffic, exists := m.serviceTraffic[serviceName]; exists {
traffic.mu.Lock()
upBytes = traffic.UpBytes
downBytes = traffic.DownBytes
traffic.mu.Unlock()
}
return
}
+8 -12
View File
@@ -403,19 +403,15 @@ func (s *defaultService) observeStats(ctx context.Context) {
TotalErrs: st.Get(stats.KindTotalErrs),
},
}
// 将流量累积到全局管理器,而不是立即上报
if outputBytes > 0 || inputBytes > 0 {
reportItems := TrafficReportItem{
N: s.name,
U: int64(outputBytes),
D: int64(inputBytes),
}
success, err := sendTrafficReport(ctx, reportItems)
if err != nil {
fmt.Printf("发送流量报告失败: %v", err)
} else if success {
if xstats, ok := st.(*xstats.Stats); ok {
xstats.ResetTraffic(st.Get(stats.KindInputBytes)-inputBytes, st.Get(stats.KindOutputBytes)-outputBytes)
}
globalManager := GetGlobalTrafficManager()
globalManager.AddTraffic(s.name, int64(outputBytes), int64(inputBytes))
// 立即重置流量计数(因为已经记录到全局管理器中)
if xstats, ok := st.(*xstats.Stats); ok {
xstats.ResetTraffic(st.Get(stats.KindInputBytes)-inputBytes, st.Get(stats.KindOutputBytes)-outputBytes)
}
}
+3 -2
View File
@@ -41,8 +41,8 @@ func SetHTTPReportURL(addr string, secret string) {
}
}
// sendTrafficReport 发送流量报告到HTTP接口
func sendTrafficReport(ctx context.Context, reportItems TrafficReportItem) (bool, error) {
// sendBatchTrafficReport 批量发送多个服务的流量报告到HTTP接口
func sendBatchTrafficReport(ctx context.Context, reportItems []TrafficReportItem) (bool, error) {
jsonData, err := json.Marshal(reportItems)
if err != nil {
return false, fmt.Errorf("序列化报告数据失败: %v", err)
@@ -112,6 +112,7 @@ func sendTrafficReport(ctx context.Context, reportItems TrafficReportItem) (bool
}
}
// sendConfigReport 发送配置报告到HTTP接口
func sendConfigReport(ctx context.Context) (bool, error) {
if configReportURL == "" {
+117 -66
View File
@@ -14,8 +14,8 @@ import (
"time"
"github.com/go-gost/x/config"
"github.com/go-gost/x/service"
"github.com/go-gost/x/internal/util/crypto"
"github.com/go-gost/x/service"
"github.com/gorilla/websocket"
"github.com/shirou/gopsutil/v3/cpu"
"github.com/shirou/gopsutil/v3/host"
@@ -87,6 +87,9 @@ type TcpPingResponse struct {
type WebSocketReporter struct {
url string
addr string // 保存服务器地址
secret string // 保存密钥
version string // 保存版本号
conn *websocket.Conn
reconnectTime time.Duration
pingInterval time.Duration
@@ -195,7 +198,25 @@ func (w *WebSocketReporter) connect() error {
w.connecting = false
}()
u, err := url.Parse(w.url)
// 重新读取 config.json 获取最新的协议配置
type LocalConfig struct {
Addr string `json:"addr"`
Secret string `json:"secret"`
Http int `json:"http"`
Tls int `json:"tls"`
Socks int `json:"socks"`
}
var cfg LocalConfig
if b, err := os.ReadFile("config.json"); err == nil {
json.Unmarshal(b, &cfg)
}
// 使用最新的配置重新构建 URL
currentURL := "ws://" + w.addr + "/system-info?type=1&secret=" + w.secret + "&version=" + w.version +
"&http=" + strconv.Itoa(cfg.Http) + "&tls=" + strconv.Itoa(cfg.Tls) + "&socks=" + strconv.Itoa(cfg.Socks)
u, err := url.Parse(currentURL)
if err != nil {
return fmt.Errorf("解析URL失败: %v", err)
}
@@ -225,7 +246,7 @@ func (w *WebSocketReporter) connect() error {
return nil
})
fmt.Printf("✅ WebSocket连接建立成功\n")
fmt.Printf("✅ WebSocket连接建立成功 (http=%d, tls=%d, socks=%d)\n", cfg.Http, cfg.Tls, cfg.Socks)
return nil
}
@@ -775,81 +796,81 @@ func (w *WebSocketReporter) handleDeleteLimiter(data interface{}) error {
// handleSetProtocol 处理设置屏蔽协议的命令
func (w *WebSocketReporter) handleSetProtocol(data interface{}) error {
jsonData, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("序列化协议设置失败: %v", err)
}
jsonData, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("序列化协议设置失败: %v", err)
}
// 支持 {"http":0/1, "tls":0/1, "socks":0/1}
var req struct {
HTTP *int `json:"http"`
TLS *int `json:"tls"`
SOCKS *int `json:"socks"`
}
if err := json.Unmarshal(jsonData, &req); err != nil {
return fmt.Errorf("解析协议设置失败: %v", err)
}
// 支持 {"http":0/1, "tls":0/1, "socks":0/1}
var req struct {
HTTP *int `json:"http"`
TLS *int `json:"tls"`
SOCKS *int `json:"socks"`
}
if err := json.Unmarshal(jsonData, &req); err != nil {
return fmt.Errorf("解析协议设置失败: %v", err)
}
// 读取当前值作为默认
httpVal, tlsVal, socksVal := 0, 0, 0
// 读取当前值作为默认
httpVal, tlsVal, socksVal := 0, 0, 0
if req.HTTP != nil {
if *req.HTTP != 0 && *req.HTTP != 1 {
return fmt.Errorf("http 取值必须为0或1")
}
httpVal = *req.HTTP
}
if req.TLS != nil {
if *req.TLS != 0 && *req.TLS != 1 {
return fmt.Errorf("tls 取值必须为0或1")
}
tlsVal = *req.TLS
}
if req.SOCKS != nil {
if *req.SOCKS != 0 && *req.SOCKS != 1 {
return fmt.Errorf("socks 取值必须为0或1")
}
socksVal = *req.SOCKS
}
if req.HTTP != nil {
if *req.HTTP != 0 && *req.HTTP != 1 {
return fmt.Errorf("http 取值必须为0或1")
}
httpVal = *req.HTTP
}
if req.TLS != nil {
if *req.TLS != 0 && *req.TLS != 1 {
return fmt.Errorf("tls 取值必须为0或1")
}
tlsVal = *req.TLS
}
if req.SOCKS != nil {
if *req.SOCKS != 0 && *req.SOCKS != 1 {
return fmt.Errorf("socks 取值必须为0或1")
}
socksVal = *req.SOCKS
}
// 设置至 service,全量传递(未提供的值沿用0)
service.SetProtocolBlock(httpVal, tlsVal, socksVal)
// 设置至 service,全量传递(未提供的值沿用0)
service.SetProtocolBlock(httpVal, tlsVal, socksVal)
// 同步写入本地 config.json
if err := updateLocalConfigJSON(httpVal, tlsVal, socksVal); err != nil {
return fmt.Errorf("写入config.json失败: %v", err)
}
return nil
// 同步写入本地 config.json
if err := updateLocalConfigJSON(httpVal, tlsVal, socksVal); err != nil {
return fmt.Errorf("写入config.json失败: %v", err)
}
return nil
}
// updateLocalConfigJSON 将 http/tls/socks 写入工作目录下的 config.json
func updateLocalConfigJSON(httpVal int, tlsVal int, socksVal int) error {
path := "config.json"
path := "config.json"
// 读取现有配置
type LocalConfig struct {
Addr string `json:"addr"`
Secret string `json:"secret"`
Http int `json:"http"`
Tls int `json:"tls"`
Socks int `json:"socks"`
}
// 读取现有配置
type LocalConfig struct {
Addr string `json:"addr"`
Secret string `json:"secret"`
Http int `json:"http"`
Tls int `json:"tls"`
Socks int `json:"socks"`
}
var cfg LocalConfig
if b, err := os.ReadFile(path); err == nil {
_ = json.Unmarshal(b, &cfg)
}
var cfg LocalConfig
if b, err := os.ReadFile(path); err == nil {
_ = json.Unmarshal(b, &cfg)
}
cfg.Http = httpVal
cfg.Tls = tlsVal
cfg.Socks = socksVal
cfg.Http = httpVal
cfg.Tls = tlsVal
cfg.Socks = socksVal
// 写回
data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return err
}
return os.WriteFile(path, data, 0644)
// 写回
data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return err
}
return os.WriteFile(path, data, 0644)
}
// handleCall 处理服务端的call回调消息
@@ -1021,12 +1042,16 @@ func getMemoryInfo() MemoryInfo {
// StartWebSocketReporterWithConfig 使用配置字段启动WebSocket报告器
func StartWebSocketReporterWithConfig(addr string, secret string, http int, tls int, socks int, version string) *WebSocketReporter {
// 构建包含本机IP的WebSocket URL
// 构建初始 WebSocket URL
fullURL := "ws://" + addr + "/system-info?type=1&secret=" + secret + "&version=" + version + "&http=" + strconv.Itoa(http) + "&tls=" + strconv.Itoa(tls) + "&socks=" + strconv.Itoa(socks)
fmt.Printf("🔗 WebSocket连接URL: %s\n", fullURL)
reporter := NewWebSocketReporter(fullURL, secret)
// 保存 addr, secret, version 供重连时使用
reporter.addr = addr
reporter.secret = secret
reporter.version = version
reporter.Start()
return reporter
}
@@ -1107,6 +1132,32 @@ func tcpPingHost(ip string, port int, count int, timeoutMs int) (float64, float6
fmt.Printf("🔍 开始TCP ping测试: %s,次数: %d,超时: %dms\n", target, count, timeoutMs)
// 如果是域名,先解析一次DNS,避免每次连接都重新解析导致延迟累加
if net.ParseIP(ip) == nil {
// 是域名,需要解析
fmt.Printf("🔍 检测到域名,正在解析DNS...\n")
dnsStart := time.Now()
addrs, err := net.LookupHost(ip)
dnsDuration := time.Since(dnsStart)
if err != nil {
return 0, 100.0, fmt.Errorf("DNS解析失败: %v", err)
}
if len(addrs) == 0 {
return 0, 100.0, fmt.Errorf("DNS解析未返回任何IP地址")
}
fmt.Printf("✅ DNS解析完成 (%.2fms),解析到 %d 个IP: %v\n",
dnsDuration.Seconds()*1000, len(addrs), addrs)
// 使用第一个解析到的IP进行测试
target = net.JoinHostPort(addrs[0], fmt.Sprintf("%d", port))
fmt.Printf("🎯 使用IP地址进行测试: %s\n", target)
} else {
fmt.Printf("🎯 使用IP地址进行测试: %s\n", target)
}
for i := 0; i < count; i++ {
start := time.Now()
-306
View File
@@ -1,306 +0,0 @@
-- phpMyAdmin SQL Dump
-- version 5.2.0
-- https://www.phpmyadmin.net/
--
-- 主机: localhost
-- 生成日期: 2025-08-14 21:52:52
-- 服务器版本: 5.7.40-log
-- PHP 版本: 7.4.33
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
START TRANSACTION;
SET time_zone = "+00:00";
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
--
-- 数据库: `gost`
--
-- --------------------------------------------------------
--
-- 表的结构 `forward`
--
CREATE TABLE `forward` (
`id` int(10) NOT NULL,
`user_id` int(10) NOT NULL,
`user_name` varchar(100) NOT NULL,
`name` varchar(100) NOT NULL,
`tunnel_id` int(10) NOT NULL,
`in_port` int(10) NOT NULL,
`out_port` int(10) DEFAULT NULL,
`remote_addr` longtext NOT NULL,
`strategy` varchar(100) NOT NULL DEFAULT 'fifo',
`interface_name` varchar(200) DEFAULT NULL,
`in_flow` bigint(20) NOT NULL DEFAULT '0',
`out_flow` bigint(20) NOT NULL DEFAULT '0',
`created_time` bigint(20) NOT NULL,
`updated_time` bigint(20) NOT NULL,
`status` int(10) NOT NULL,
`inx` int(10) NOT NULL DEFAULT '0'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- --------------------------------------------------------
--
-- 表的结构 `node`
--
CREATE TABLE `node` (
`id` int(10) NOT NULL,
`name` varchar(100) NOT NULL,
`secret` varchar(100) NOT NULL,
`ip` longtext,
`server_ip` varchar(100) NOT NULL,
`port_sta` int(10) NOT NULL,
`port_end` int(10) NOT NULL,
`version` varchar(100) DEFAULT NULL,
`http` int(10) NOT NULL DEFAULT '0',
`tls` int(10) NOT NULL DEFAULT '0',
`socks` int(10) NOT NULL DEFAULT '0',
`created_time` bigint(20) NOT NULL,
`updated_time` bigint(20) DEFAULT NULL,
`status` int(10) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- --------------------------------------------------------
--
-- 表的结构 `speed_limit`
--
CREATE TABLE `speed_limit` (
`id` int(10) NOT NULL,
`name` varchar(100) NOT NULL,
`speed` int(10) NOT NULL,
`tunnel_id` int(10) NOT NULL,
`tunnel_name` varchar(100) NOT NULL,
`created_time` bigint(20) NOT NULL,
`updated_time` bigint(20) DEFAULT NULL,
`status` int(10) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- --------------------------------------------------------
--
-- 表的结构 `statistics_flow`
--
CREATE TABLE `statistics_flow` (
`id` int(10) NOT NULL,
`user_id` int(10) NOT NULL,
`flow` bigint(20) NOT NULL,
`total_flow` bigint(20) NOT NULL,
`time` varchar(100) NOT NULL,
`created_time` bigint(20) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- --------------------------------------------------------
--
-- 表的结构 `tunnel`
--
CREATE TABLE `tunnel` (
`id` int(10) NOT NULL,
`name` varchar(100) NOT NULL,
`traffic_ratio` decimal(10,1) NOT NULL DEFAULT '1.0',
`in_node_id` int(10) NOT NULL,
`in_ip` varchar(100) NOT NULL,
`out_node_id` int(10) NOT NULL,
`out_ip` varchar(100) NOT NULL,
`type` int(10) NOT NULL,
`protocol` varchar(10) NOT NULL DEFAULT 'tls',
`flow` int(10) NOT NULL,
`tcp_listen_addr` varchar(100) NOT NULL DEFAULT '[::]',
`udp_listen_addr` varchar(100) NOT NULL DEFAULT '[::]',
`interface_name` varchar(200) DEFAULT NULL,
`created_time` bigint(20) NOT NULL,
`updated_time` bigint(20) NOT NULL,
`status` int(10) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- --------------------------------------------------------
--
-- 表的结构 `user`
--
CREATE TABLE `user` (
`id` int(10) NOT NULL,
`user` varchar(100) NOT NULL,
`pwd` varchar(100) NOT NULL,
`role_id` int(10) NOT NULL,
`exp_time` bigint(20) NOT NULL,
`flow` bigint(20) NOT NULL,
`in_flow` bigint(20) NOT NULL DEFAULT '0',
`out_flow` bigint(20) NOT NULL DEFAULT '0',
`flow_reset_time` bigint(20) NOT NULL,
`num` int(10) NOT NULL,
`created_time` bigint(20) NOT NULL,
`updated_time` bigint(20) DEFAULT NULL,
`status` int(10) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
--
-- 转存表中的数据 `user`
--
INSERT INTO `user` (`id`, `user`, `pwd`, `role_id`, `exp_time`, `flow`, `in_flow`, `out_flow`, `flow_reset_time`, `num`, `created_time`, `updated_time`, `status`) VALUES
(1, 'admin_user', '3c85cdebade1c51cf64ca9f3c09d182d', 0, 2727251700000, 99999, 0, 0, 1, 99999, 1748914865000, 1754011744252, 1);
-- --------------------------------------------------------
--
-- 表的结构 `user_tunnel`
--
CREATE TABLE `user_tunnel` (
`id` int(10) NOT NULL,
`user_id` int(10) NOT NULL,
`tunnel_id` int(10) NOT NULL,
`speed_id` int(10) DEFAULT NULL,
`num` int(10) NOT NULL,
`flow` bigint(20) NOT NULL,
`in_flow` bigint(20) NOT NULL DEFAULT '0',
`out_flow` bigint(20) NOT NULL DEFAULT '0',
`flow_reset_time` bigint(20) NOT NULL,
`exp_time` bigint(20) NOT NULL,
`status` int(10) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- --------------------------------------------------------
--
-- 表的结构 `vite_config`
--
CREATE TABLE `vite_config` (
`id` int(10) NOT NULL,
`name` varchar(200) NOT NULL,
`value` varchar(200) NOT NULL,
`time` bigint(20) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
--
-- 转存表中的数据 `vite_config`
--
INSERT INTO `vite_config` (`id`, `name`, `value`, `time`) VALUES
(1, 'app_name', 'flux', 1755147963000);
--
-- 转储表的索引
--
--
-- 表的索引 `forward`
--
ALTER TABLE `forward`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `node`
--
ALTER TABLE `node`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `speed_limit`
--
ALTER TABLE `speed_limit`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `statistics_flow`
--
ALTER TABLE `statistics_flow`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `tunnel`
--
ALTER TABLE `tunnel`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `user`
--
ALTER TABLE `user`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `user_tunnel`
--
ALTER TABLE `user_tunnel`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `vite_config`
--
ALTER TABLE `vite_config`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `name` (`name`);
--
-- 在导出的表使用AUTO_INCREMENT
--
--
-- 使用表AUTO_INCREMENT `forward`
--
ALTER TABLE `forward`
MODIFY `id` int(10) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1;
--
-- 使用表AUTO_INCREMENT `node`
--
ALTER TABLE `node`
MODIFY `id` int(10) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1;
--
-- 使用表AUTO_INCREMENT `speed_limit`
--
ALTER TABLE `speed_limit`
MODIFY `id` int(10) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1;
--
-- 使用表AUTO_INCREMENT `statistics_flow`
--
ALTER TABLE `statistics_flow`
MODIFY `id` int(10) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1;
--
-- 使用表AUTO_INCREMENT `tunnel`
--
ALTER TABLE `tunnel`
MODIFY `id` int(10) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1;
--
-- 使用表AUTO_INCREMENT `user`
--
ALTER TABLE `user`
MODIFY `id` int(10) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1;
--
-- 使用表AUTO_INCREMENT `user_tunnel`
--
ALTER TABLE `user_tunnel`
MODIFY `id` int(10) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1;
--
-- 使用表AUTO_INCREMENT `vite_config`
--
ALTER TABLE `vite_config`
MODIFY `id` int(10) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1;
COMMIT;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
+74 -56
View File
@@ -1,7 +1,30 @@
#!/bin/bash
# 获取系统架构
get_architecture() {
ARCH=$(uname -m)
case $ARCH in
x86_64)
echo "amd64"
;;
aarch64|arm64)
echo "arm64"
;;
*)
echo "amd64" # 默认使用 amd64
;;
esac
}
# 构建下载地址
build_download_url() {
local ARCH=$(get_architecture)
echo "https://github.com/bqlpfy/flux-panel/releases/download/2.0.7-beta/gost-${ARCH}"
}
# 下载地址
DOWNLOAD_URL="https://github.com/bqlpfy/flux-panel/releases/download/gost-latest/gost"
INSTALL_DIR="/etc/gost"
DOWNLOAD_URL=$(build_download_url)
INSTALL_DIR="/etc/flux_agent"
COUNTRY=$(curl -s https://ipinfo.io/country)
if [ "$COUNTRY" = "CN" ]; then
# 拼接 URL
@@ -131,8 +154,8 @@ while getopts "a:s:" opt; do
done
# 安装功能
install_gost() {
echo "🚀 开始安装 GOST..."
install_flux_agent() {
echo "🚀 开始安装 flux_agent..."
get_config_params
# 检查并安装 tcpkill
@@ -142,27 +165,27 @@ install_gost() {
mkdir -p "$INSTALL_DIR"
# 停止并禁用已有服务
if systemctl list-units --full -all | grep -Fq "gost.service"; then
echo "🔍 检测到已存在的gost服务"
systemctl stop gost 2>/dev/null && echo "🛑 停止服务"
systemctl disable gost 2>/dev/null && echo "🚫 禁用自启"
if systemctl list-units --full -all | grep -Fq "flux_agent.service"; then
echo "🔍 检测到已存在的flux_agent服务"
systemctl stop flux_agent 2>/dev/null && echo "🛑 停止服务"
systemctl disable flux_agent 2>/dev/null && echo "🚫 禁用自启"
fi
# 删除旧文件
[[ -f "$INSTALL_DIR/gost" ]] && echo "🧹 删除旧文件 gost" && rm -f "$INSTALL_DIR/gost"
[[ -f "$INSTALL_DIR/flux_agent" ]] && echo "🧹 删除旧文件 flux_agent" && rm -f "$INSTALL_DIR/flux_agent"
# 下载 gost
echo "⬇️ 下载 gost 中..."
curl -L "$DOWNLOAD_URL" -o "$INSTALL_DIR/gost"
if [[ ! -f "$INSTALL_DIR/gost" || ! -s "$INSTALL_DIR/gost" ]]; then
# 下载 flux_agent
echo "⬇️ 下载 flux_agent 中..."
curl -L "$DOWNLOAD_URL" -o "$INSTALL_DIR/flux_agent"
if [[ ! -f "$INSTALL_DIR/flux_agent" || ! -s "$INSTALL_DIR/flux_agent" ]]; then
echo "❌ 下载失败,请检查网络或下载链接。"
exit 1
fi
chmod +x "$INSTALL_DIR/gost"
chmod +x "$INSTALL_DIR/flux_agent"
echo "✅ 下载完成"
# 打印版本
echo "🔎 gost 版本:$($INSTALL_DIR/gost -V)"
echo "🔎 flux_agent 版本:$($INSTALL_DIR/flux_agent -V)"
# 写入 config.json (安装时总是创建新的)
CONFIG_FILE="$INSTALL_DIR/config.json"
@@ -189,15 +212,15 @@ EOF
chmod 600 "$INSTALL_DIR"/*.json
# 创建 systemd 服务
SERVICE_FILE="/etc/systemd/system/gost.service"
SERVICE_FILE="/etc/systemd/system/flux_agent.service"
cat > "$SERVICE_FILE" <<EOF
[Unit]
Description=Gost Proxy Service
Description=Flux_agent Proxy Service
After=network.target
[Service]
WorkingDirectory=$INSTALL_DIR
ExecStart=$INSTALL_DIR/gost
ExecStart=$INSTALL_DIR/flux_agent
Restart=on-failure
[Install]
@@ -206,27 +229,27 @@ EOF
# 启动服务
systemctl daemon-reload
systemctl enable gost
systemctl start gost
systemctl enable flux_agent
systemctl start flux_agent
# 检查状态
echo "🔄 检查服务状态..."
if systemctl is-active --quiet gost; then
echo "✅ 安装完成,gost服务已启动并设置为开机启动。"
if systemctl is-active --quiet flux_agent; then
echo "✅ 安装完成,flux_agent服务已启动并设置为开机启动。"
echo "📁 配置目录: $INSTALL_DIR"
echo "🔧 服务状态: $(systemctl is-active gost)"
echo "🔧 服务状态: $(systemctl is-active flux_agent)"
else
echo "❌ gost服务启动失败,请执行以下命令查看日志:"
echo "journalctl -u gost -f"
echo "❌ flux_agent服务启动失败,请执行以下命令查看日志:"
echo "journalctl -u flux_agent -f"
fi
}
# 更新功能
update_gost() {
echo "🔄 开始更新 GOST..."
update_flux_agent() {
echo "🔄 开始更新 flux_agent..."
if [[ ! -d "$INSTALL_DIR" ]]; then
echo "❌ GOST 未安装,请先选择安装。"
echo "❌ flux_agent 未安装,请先选择安装。"
return 1
fi
@@ -237,52 +260,52 @@ update_gost() {
# 先下载新版本
echo "⬇️ 下载最新版本..."
curl -L "$DOWNLOAD_URL" -o "$INSTALL_DIR/gost.new"
if [[ ! -f "$INSTALL_DIR/gost.new" || ! -s "$INSTALL_DIR/gost.new" ]]; then
curl -L "$DOWNLOAD_URL" -o "$INSTALL_DIR/flux_agent.new"
if [[ ! -f "$INSTALL_DIR/flux_agent.new" || ! -s "$INSTALL_DIR/flux_agent.new" ]]; then
echo "❌ 下载失败。"
return 1
fi
# 停止服务
if systemctl list-units --full -all | grep -Fq "gost.service"; then
echo "🛑 停止 gost 服务..."
systemctl stop gost
if systemctl list-units --full -all | grep -Fq "flux_agent.service"; then
echo "🛑 停止 flux_agent 服务..."
systemctl stop flux_agent
fi
# 替换文件
mv "$INSTALL_DIR/gost.new" "$INSTALL_DIR/gost"
chmod +x "$INSTALL_DIR/gost"
mv "$INSTALL_DIR/flux_agent.new" "$INSTALL_DIR/flux_agent"
chmod +x "$INSTALL_DIR/flux_agent"
# 打印版本
echo "🔎 新版本:$($INSTALL_DIR/gost -V)"
echo "🔎 新版本:$($INSTALL_DIR/flux_agent -V)"
# 重启服务
echo "🔄 重启服务..."
systemctl start gost
systemctl start flux_agent
echo "✅ 更新完成,服务已重新启动。"
}
# 卸载功能
uninstall_gost() {
echo "🗑️ 开始卸载 GOST..."
uninstall_flux_agent() {
echo "🗑️ 开始卸载 flux_agent..."
read -p "确认卸载 GOST 吗?此操作将删除所有相关文件 (y/N): " confirm
read -p "确认卸载 flux_agent 吗?此操作将删除所有相关文件 (y/N): " confirm
if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
echo "❌ 取消卸载"
return 0
fi
# 停止并禁用服务
if systemctl list-units --full -all | grep -Fq "gost.service"; then
if systemctl list-units --full -all | grep -Fq "flux_agent.service"; then
echo "🛑 停止并禁用服务..."
systemctl stop gost 2>/dev/null
systemctl disable gost 2>/dev/null
systemctl stop flux_agent 2>/dev/null
systemctl disable flux_agent 2>/dev/null
fi
# 删除服务文件
if [[ -f "/etc/systemd/system/gost.service" ]]; then
rm -f "/etc/systemd/system/gost.service"
if [[ -f "/etc/systemd/system/flux_agent.service" ]]; then
rm -f "/etc/systemd/system/flux_agent.service"
echo "🧹 删除服务文件"
fi
@@ -302,7 +325,7 @@ uninstall_gost() {
main() {
# 如果提供了命令行参数,直接执行安装
if [[ -n "$SERVER_ADDR" && -n "$SECRET" ]]; then
install_gost
install_flux_agent
delete_self
exit 0
fi
@@ -310,36 +333,31 @@ main() {
# 显示交互式菜单
while true; do
show_menu
read -p "请输入选项 (1-5): " choice
read -p "请输入选项 (1-4): " choice
case $choice in
1)
install_gost
install_flux_agent
delete_self
exit 0
;;
2)
update_gost
update_flux_agent
delete_self
exit 0
;;
3)
uninstall_gost
uninstall_flux_agent
delete_self
exit 0
;;
4)
block_protocol
delete_self
exit 0
;;
5)
echo "👋 退出脚本"
delete_self
exit 0
;;
*)
echo "❌ 无效选项,请输入 1-5"
echo "❌ 无效选项,请输入 1-4"
echo ""
;;
esac
+563
View File
@@ -0,0 +1,563 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 77;
objects = {
/* Begin PBXContainerItemProxy section */
296FEB8A2E56AAF900DC77A3 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 296FEB742E56AAF700DC77A3 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 296FEB7B2E56AAF700DC77A3;
remoteInfo = flux;
};
296FEB942E56AAF900DC77A3 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 296FEB742E56AAF700DC77A3 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 296FEB7B2E56AAF700DC77A3;
remoteInfo = flux;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
296FEB7C2E56AAF700DC77A3 /* flux.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = flux.app; sourceTree = BUILT_PRODUCTS_DIR; };
296FEB892E56AAF900DC77A3 /* fluxTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = fluxTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
296FEB932E56AAF900DC77A3 /* fluxUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = fluxUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
296FEB7E2E56AAF700DC77A3 /* flux */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = flux;
sourceTree = "<group>";
};
296FEB8C2E56AAF900DC77A3 /* fluxTests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = fluxTests;
sourceTree = "<group>";
};
296FEB962E56AAF900DC77A3 /* fluxUITests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = fluxUITests;
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
296FEB792E56AAF700DC77A3 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
296FEB862E56AAF900DC77A3 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
296FEB902E56AAF900DC77A3 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
296FEB732E56AAF700DC77A3 = {
isa = PBXGroup;
children = (
296FEB7E2E56AAF700DC77A3 /* flux */,
296FEB8C2E56AAF900DC77A3 /* fluxTests */,
296FEB962E56AAF900DC77A3 /* fluxUITests */,
296FEB7D2E56AAF700DC77A3 /* Products */,
);
sourceTree = "<group>";
};
296FEB7D2E56AAF700DC77A3 /* Products */ = {
isa = PBXGroup;
children = (
296FEB7C2E56AAF700DC77A3 /* flux.app */,
296FEB892E56AAF900DC77A3 /* fluxTests.xctest */,
296FEB932E56AAF900DC77A3 /* fluxUITests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
296FEB7B2E56AAF700DC77A3 /* flux */ = {
isa = PBXNativeTarget;
buildConfigurationList = 296FEB9D2E56AAF900DC77A3 /* Build configuration list for PBXNativeTarget "flux" */;
buildPhases = (
296FEB782E56AAF700DC77A3 /* Sources */,
296FEB792E56AAF700DC77A3 /* Frameworks */,
296FEB7A2E56AAF700DC77A3 /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
296FEB7E2E56AAF700DC77A3 /* flux */,
);
name = flux;
packageProductDependencies = (
);
productName = flux;
productReference = 296FEB7C2E56AAF700DC77A3 /* flux.app */;
productType = "com.apple.product-type.application";
};
296FEB882E56AAF900DC77A3 /* fluxTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 296FEBA02E56AAF900DC77A3 /* Build configuration list for PBXNativeTarget "fluxTests" */;
buildPhases = (
296FEB852E56AAF900DC77A3 /* Sources */,
296FEB862E56AAF900DC77A3 /* Frameworks */,
296FEB872E56AAF900DC77A3 /* Resources */,
);
buildRules = (
);
dependencies = (
296FEB8B2E56AAF900DC77A3 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
296FEB8C2E56AAF900DC77A3 /* fluxTests */,
);
name = fluxTests;
packageProductDependencies = (
);
productName = fluxTests;
productReference = 296FEB892E56AAF900DC77A3 /* fluxTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
296FEB922E56AAF900DC77A3 /* fluxUITests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 296FEBA32E56AAF900DC77A3 /* Build configuration list for PBXNativeTarget "fluxUITests" */;
buildPhases = (
296FEB8F2E56AAF900DC77A3 /* Sources */,
296FEB902E56AAF900DC77A3 /* Frameworks */,
296FEB912E56AAF900DC77A3 /* Resources */,
);
buildRules = (
);
dependencies = (
296FEB952E56AAF900DC77A3 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
296FEB962E56AAF900DC77A3 /* fluxUITests */,
);
name = fluxUITests;
packageProductDependencies = (
);
productName = fluxUITests;
productReference = 296FEB932E56AAF900DC77A3 /* fluxUITests.xctest */;
productType = "com.apple.product-type.bundle.ui-testing";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
296FEB742E56AAF700DC77A3 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1640;
LastUpgradeCheck = 1640;
TargetAttributes = {
296FEB7B2E56AAF700DC77A3 = {
CreatedOnToolsVersion = 16.4;
};
296FEB882E56AAF900DC77A3 = {
CreatedOnToolsVersion = 16.4;
TestTargetID = 296FEB7B2E56AAF700DC77A3;
};
296FEB922E56AAF900DC77A3 = {
CreatedOnToolsVersion = 16.4;
TestTargetID = 296FEB7B2E56AAF700DC77A3;
};
};
};
buildConfigurationList = 296FEB772E56AAF700DC77A3 /* Build configuration list for PBXProject "flux" */;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 296FEB732E56AAF700DC77A3;
minimizedProjectReferenceProxies = 1;
preferredProjectObjectVersion = 77;
productRefGroup = 296FEB7D2E56AAF700DC77A3 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
296FEB7B2E56AAF700DC77A3 /* flux */,
296FEB882E56AAF900DC77A3 /* fluxTests */,
296FEB922E56AAF900DC77A3 /* fluxUITests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
296FEB7A2E56AAF700DC77A3 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
296FEB872E56AAF900DC77A3 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
296FEB912E56AAF900DC77A3 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
296FEB782E56AAF700DC77A3 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
296FEB852E56AAF900DC77A3 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
296FEB8F2E56AAF900DC77A3 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
296FEB8B2E56AAF900DC77A3 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 296FEB7B2E56AAF700DC77A3 /* flux */;
targetProxy = 296FEB8A2E56AAF900DC77A3 /* PBXContainerItemProxy */;
};
296FEB952E56AAF900DC77A3 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 296FEB7B2E56AAF700DC77A3 /* flux */;
targetProxy = 296FEB942E56AAF900DC77A3 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
296FEB9B2E56AAF900DC77A3 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
296FEB9C2E56AAF900DC77A3 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
296FEB9E2E56AAF900DC77A3 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = XRXD4YN2Y6;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = bqlpfy.flux;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
296FEB9F2E56AAF900DC77A3 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = XRXD4YN2Y6;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = bqlpfy.flux;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
296FEBA12E56AAF900DC77A3 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = bqlpfy.fluxTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/flux.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/flux";
};
name = Debug;
};
296FEBA22E56AAF900DC77A3 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = bqlpfy.fluxTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/flux.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/flux";
};
name = Release;
};
296FEBA42E56AAF900DC77A3 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = bqlpfy.fluxUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_TARGET_NAME = flux;
};
name = Debug;
};
296FEBA52E56AAF900DC77A3 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = bqlpfy.fluxUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_TARGET_NAME = flux;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
296FEB772E56AAF700DC77A3 /* Build configuration list for PBXProject "flux" */ = {
isa = XCConfigurationList;
buildConfigurations = (
296FEB9B2E56AAF900DC77A3 /* Debug */,
296FEB9C2E56AAF900DC77A3 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
296FEB9D2E56AAF900DC77A3 /* Build configuration list for PBXNativeTarget "flux" */ = {
isa = XCConfigurationList;
buildConfigurations = (
296FEB9E2E56AAF900DC77A3 /* Debug */,
296FEB9F2E56AAF900DC77A3 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
296FEBA02E56AAF900DC77A3 /* Build configuration list for PBXNativeTarget "fluxTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
296FEBA12E56AAF900DC77A3 /* Debug */,
296FEBA22E56AAF900DC77A3 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
296FEBA32E56AAF900DC77A3 /* Build configuration list for PBXNativeTarget "fluxUITests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
296FEBA42E56AAF900DC77A3 /* Debug */,
296FEBA52E56AAF900DC77A3 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 296FEB742E56AAF700DC77A3 /* Project object */;
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
@@ -0,0 +1,217 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>top-level-items</key>
<array>
<dict>
<key>children</key>
<array>
<dict>
<key>children</key>
<array>
<dict>
<key>destination</key>
<dict>
<key>rebasable-url</key>
<dict>
<key>base</key>
<string>workspace</string>
<key>payload</key>
<dict>
<key>relative-path</key>
<string>flux/dist/assets/bg-Ck8MrDZ5.jpg</string>
</dict>
</dict>
<key>type</key>
<string>DVTDocumentLocation</string>
</dict>
<key>type</key>
<string>bookmark</string>
</dict>
<dict>
<key>destination</key>
<dict>
<key>rebasable-url</key>
<dict>
<key>base</key>
<string>workspace</string>
<key>payload</key>
<dict>
<key>relative-path</key>
<string>flux/dist/assets/favicon-DI5FVxIz.ico</string>
</dict>
</dict>
<key>type</key>
<string>DVTDocumentLocation</string>
</dict>
<key>type</key>
<string>bookmark</string>
</dict>
<dict>
<key>destination</key>
<dict>
<key>rebasable-url</key>
<dict>
<key>base</key>
<string>workspace</string>
<key>payload</key>
<dict>
<key>relative-path</key>
<string>flux/dist/assets/index-Bf4rZ0gS.css</string>
</dict>
</dict>
<key>type</key>
<string>DVTDocumentLocation</string>
</dict>
<key>type</key>
<string>bookmark</string>
</dict>
<dict>
<key>destination</key>
<dict>
<key>rebasable-url</key>
<dict>
<key>base</key>
<string>workspace</string>
<key>payload</key>
<dict>
<key>relative-path</key>
<string>flux/dist/assets/index-ByTRlFFk.js</string>
</dict>
</dict>
<key>type</key>
<string>DVTDocumentLocation</string>
</dict>
<key>type</key>
<string>bookmark</string>
</dict>
<dict>
<key>destination</key>
<dict>
<key>rebasable-url</key>
<dict>
<key>base</key>
<string>workspace</string>
<key>payload</key>
<dict>
<key>relative-path</key>
<string>flux/dist/assets/index-Drx943H4.js</string>
</dict>
</dict>
<key>type</key>
<string>DVTDocumentLocation</string>
</dict>
<key>type</key>
<string>bookmark</string>
</dict>
<dict>
<key>destination</key>
<dict>
<key>rebasable-url</key>
<dict>
<key>base</key>
<string>workspace</string>
<key>payload</key>
<dict>
<key>relative-path</key>
<string>flux/dist/assets/index-legacy-BYgRAl5I.js</string>
</dict>
</dict>
<key>type</key>
<string>DVTDocumentLocation</string>
</dict>
<key>type</key>
<string>bookmark</string>
</dict>
<dict>
<key>destination</key>
<dict>
<key>rebasable-url</key>
<dict>
<key>base</key>
<string>workspace</string>
<key>payload</key>
<dict>
<key>relative-path</key>
<string>flux/dist/assets/index-legacy-DUPUAMxs.js</string>
</dict>
</dict>
<key>type</key>
<string>DVTDocumentLocation</string>
</dict>
<key>type</key>
<string>bookmark</string>
</dict>
<dict>
<key>destination</key>
<dict>
<key>rebasable-url</key>
<dict>
<key>base</key>
<string>workspace</string>
<key>payload</key>
<dict>
<key>relative-path</key>
<string>flux/dist/assets/polyfills-legacy-DIE4kOxF.js</string>
</dict>
</dict>
<key>type</key>
<string>DVTDocumentLocation</string>
</dict>
<key>type</key>
<string>bookmark</string>
</dict>
</array>
<key>title</key>
<string>assets</string>
<key>type</key>
<string>group</string>
</dict>
<dict>
<key>destination</key>
<dict>
<key>rebasable-url</key>
<dict>
<key>base</key>
<string>workspace</string>
<key>payload</key>
<dict>
<key>relative-path</key>
<string>flux/dist/index.html</string>
</dict>
</dict>
<key>type</key>
<string>DVTDocumentLocation</string>
</dict>
<key>type</key>
<string>bookmark</string>
</dict>
<dict>
<key>destination</key>
<dict>
<key>rebasable-url</key>
<dict>
<key>base</key>
<string>workspace</string>
<key>payload</key>
<dict>
<key>relative-path</key>
<string>flux/dist/vite.svg</string>
</dict>
</dict>
<key>type</key>
<string>DVTDocumentLocation</string>
</dict>
<key>type</key>
<string>bookmark</string>
</dict>
</array>
<key>title</key>
<string>dist</string>
<key>type</key>
<string>group</string>
</dict>
</array>
</dict>
</plist>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>SchemeUserState</key>
<dict>
<key>flux.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
</dict>
</dict>
</dict>
</plist>
@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,35 @@
{
"images" : [
{
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
+220
View File
@@ -0,0 +1,220 @@
import SwiftUI
import WebKit
struct WebView: UIViewRepresentable {
let fileName: String?
let urlString: String?
func makeCoordinator() -> Coordinator {
Coordinator()
}
func makeUIView(context: Context) -> WKWebView {
let config = WKWebViewConfiguration()
let userContentController = WKUserContentController()
// 注册 JS -> Native 消息通道
userContentController.add(context.coordinator, name: "getPanelAddresses")
userContentController.add(context.coordinator, name: "savePanelAddress")
userContentController.add(context.coordinator, name: "setCurrentPanelAddress")
userContentController.add(context.coordinator, name: "deletePanelAddress")
// 注入 CSS 隐藏滚动条
let css = "body::-webkit-scrollbar { display: none; }"
let cssJS = """
var style = document.createElement('style');
style.innerHTML = '\(css)';
document.head.appendChild(style);
"""
let cssScript = WKUserScript(source: cssJS, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
userContentController.addUserScript(cssScript)
config.userContentController = userContentController
let webView = WKWebView(frame: .zero, configuration: config)
context.coordinator.webView = webView
// 去掉滚动条和弹性
webView.scrollView.showsHorizontalScrollIndicator = false
webView.scrollView.showsVerticalScrollIndicator = false
webView.scrollView.bounces = false
// 加载本地 HTML 或远程 URL
if let fileName = fileName,
let htmlURL = Bundle.main.url(forResource: fileName, withExtension: "html") {
webView.loadFileURL(htmlURL, allowingReadAccessTo: htmlURL.deletingLastPathComponent())
} else if let urlString = urlString,
let url = URL(string: urlString) {
webView.load(URLRequest(url: url))
}
return webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {}
class Coordinator: NSObject, WKScriptMessageHandler {
weak var webView: WKWebView?
private let storageKey = "panel_addresses"
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
switch message.name {
case "getPanelAddresses":
handleGetPanelAddresses(message: message)
case "savePanelAddress":
handleSavePanelAddress(message: message)
case "setCurrentPanelAddress":
handleSetCurrentPanelAddress(message: message)
case "deletePanelAddress":
handleDeletePanelAddress(message: message)
default:
break
}
}
private func handleGetPanelAddresses(message: WKScriptMessage) {
let callbackName: String
if let cb = message.body as? String, !cb.isEmpty {
callbackName = cb
} else {
callbackName = "setPanelAddresses"
}
let addresses = loadAddressesJSONString()
let js = "window.\(callbackName)(\(addresses));"
evaluate(js: js)
}
private func handleSavePanelAddress(message: WKScriptMessage) {
var name: String?
var address: String?
if let dict = message.body as? [String: Any] {
name = dict["name"] as? String
address = dict["address"] as? String
} else if let arr = message.body as? [Any], arr.count >= 2 {
name = arr[0] as? String
address = arr[1] as? String
} else if let jsonString = message.body as? String,
let data = jsonString.data(using: .utf8),
let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
name = dict["name"] as? String
address = dict["address"] as? String
}
guard let panelName = name, let panelAddress = address, !panelName.isEmpty, !panelAddress.isEmpty else {
return
}
var array = loadAddressesArray()
let newItem: [String: Any] = [
"name": panelName,
"address": panelAddress,
"inx": false
]
array.append(newItem)
saveAddressesArray(array)
let js = "window.setPanelAddresses(\(toJSONString(array)));"
evaluate(js: js)
}
private func handleSetCurrentPanelAddress(message: WKScriptMessage) {
var name: String?
if let str = message.body as? String {
name = str
} else if let dict = message.body as? [String: Any] {
name = dict["name"] as? String
}
guard let targetName = name else { return }
var array = loadAddressesArray()
for i in 0..<array.count {
var obj = array[i]
if let n = obj["name"] as? String, n == targetName {
obj["inx"] = true
} else {
obj["inx"] = false
}
array[i] = obj
}
saveAddressesArray(array)
let js = "window.setPanelAddresses(\(toJSONString(array)));"
evaluate(js: js)
}
private func handleDeletePanelAddress(message: WKScriptMessage) {
var name: String?
if let str = message.body as? String {
name = str
} else if let dict = message.body as? [String: Any] {
name = dict["name"] as? String
}
guard let targetName = name else { return }
let oldArray = loadAddressesArray()
let newArray = oldArray.filter { obj in
guard let n = obj["name"] as? String else { return true }
return n != targetName
}
saveAddressesArray(newArray)
let js = "window.setPanelAddresses(\(toJSONString(newArray)));"
evaluate(js: js)
}
private func loadAddressesJSONString() -> String {
let defaults = UserDefaults.standard
if let s = defaults.string(forKey: storageKey), !s.isEmpty {
return s
}
return "[]"
}
private func loadAddressesArray() -> [[String: Any]] {
let s = loadAddressesJSONString()
guard let data = s.data(using: .utf8),
let json = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else {
return []
}
return json
}
private func saveAddressesArray(_ array: [[String: Any]]) {
let jsonString = toJSONString(array)
let defaults = UserDefaults.standard
defaults.set(jsonString, forKey: storageKey)
}
private func toJSONString(_ array: [[String: Any]]) -> String {
if let data = try? JSONSerialization.data(withJSONObject: array, options: []),
let s = String(data: data, encoding: .utf8) {
return s
}
return "[]"
}
private func evaluate(js: String) {
DispatchQueue.main.async { [weak self] in
self?.webView?.evaluateJavaScript(js, completionHandler: nil)
}
}
}
}
struct ContentView: View {
var body: some View {
//WebView(fileName: nil, urlString: "http://192.168.100.9:3000")
WebView(fileName: "index", urlString: nil)
}
}
#Preview {
ContentView()
}
+17
View File
@@ -0,0 +1,17 @@
//
// fluxApp.swift
// flux
//
// Created by 何磊 on 21/08/2025.
//
import SwiftUI
@main
struct fluxApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
+17
View File
@@ -0,0 +1,17 @@
//
// fluxTests.swift
// fluxTests
//
// Created by 何磊 on 21/08/2025.
//
import Testing
@testable import flux
struct fluxTests {
@Test func example() async throws {
// Write your test here and use APIs like `#expect(...)` to check expected conditions.
}
}
+41
View File
@@ -0,0 +1,41 @@
//
// fluxUITests.swift
// fluxUITests
//
// Created by 何磊 on 21/08/2025.
//
import XCTest
final class fluxUITests: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
// In UI tests it is usually best to stop immediately when a failure occurs.
continueAfterFailure = false
// In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
@MainActor
func testExample() throws {
// UI tests must launch the application that they test.
let app = XCUIApplication()
app.launch()
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
@MainActor
func testLaunchPerformance() throws {
// This measures how long it takes to launch your application.
measure(metrics: [XCTApplicationLaunchMetric()]) {
XCUIApplication().launch()
}
}
}
@@ -0,0 +1,33 @@
//
// fluxUITestsLaunchTests.swift
// fluxUITests
//
// Created by 何磊 on 21/08/2025.
//
import XCTest
final class fluxUITestsLaunchTests: XCTestCase {
override class var runsForEachTargetApplicationUIConfiguration: Bool {
true
}
override func setUpWithError() throws {
continueAfterFailure = false
}
@MainActor
func testLaunch() throws {
let app = XCUIApplication()
app.launch()
// Insert steps here to perform after app launch but before taking a screenshot,
// such as logging into a test account or navigating somewhere in the app
let attachment = XCTAttachment(screenshot: app.screenshot())
attachment.name = "Launch Screen"
attachment.lifetime = .keepAlways
add(attachment)
}
}
+14 -777
View File
@@ -8,18 +8,14 @@ export LC_ALL=C
# 全局下载地址配置
DOCKER_COMPOSEV4_URL="https://raw.githubusercontent.com/bqlpfy/flux-panel/refs/heads/main/docker-compose-v4.yml"
DOCKER_COMPOSEV6_URL="https://raw.githubusercontent.com/bqlpfy/flux-panel/refs/heads/main/docker-compose-v6.yml"
GOST_SQL_URL="https://raw.githubusercontent.com/bqlpfy/flux-panel/refs/heads/main/gost.sql"
PROXY_SH_URL="https://raw.githubusercontent.com/bqlpfy/flux-panel/refs/heads/main/proxy.sh"
DOCKER_COMPOSEV4_URL="https://github.com/bqlpfy/flux-panel/releases/download/2.0.7-beta/docker-compose-v4.yml"
DOCKER_COMPOSEV6_URL="https://github.com/bqlpfy/flux-panel/releases/download/2.0.7-beta/docker-compose-v6.yml"
COUNTRY=$(curl -s https://ipinfo.io/country)
if [ "$COUNTRY" = "CN" ]; then
# 拼接 URL
DOCKER_COMPOSEV4_URL="https://ghfast.top/${DOCKER_COMPOSEV4_URL}"
DOCKER_COMPOSEV6_URL="https://ghfast.top/${DOCKER_COMPOSEV6_URL}"
GOST_SQL_URL="https://ghfast.top/${GOST_SQL_URL}"
PROXY_SH_URL="https://ghfast.top/${PROXY_SH_URL}"
fi
@@ -151,8 +147,7 @@ show_menu() {
echo "1. 安装面板"
echo "2. 更新面板"
echo "3. 卸载面板"
echo "4. 导出备份"
echo "5. 退出"
echo "4. 退出"
echo "==============================================="
}
@@ -175,17 +170,13 @@ delete_self() {
get_config_params() {
echo "🔧 请输入配置参数:"
read -p "前端端口(默认 6366): " FRONTEND_PORT
FRONTEND_PORT=${FRONTEND_PORT:-6366}
read -p "后端端口(默认 6365): " BACKEND_PORT
BACKEND_PORT=${BACKEND_PORT:-6365}
DB_NAME=$(generate_random)
DB_USER=$(generate_random)
DB_PASSWORD=$(generate_random)
# 生成JWT密钥
JWT_SECRET=$(generate_random)
}
@@ -199,14 +190,6 @@ install_panel() {
DOCKER_COMPOSE_URL=$(get_docker_compose_url)
echo "📡 选择配置文件:$(basename "$DOCKER_COMPOSE_URL")"
curl -L -o docker-compose.yml "$DOCKER_COMPOSE_URL"
# 检查 gost.sql 是否已存在
if [[ -f "gost.sql" ]]; then
echo "⏭️ 跳过下载: gost.sql (使用当前位置的文件)"
else
echo "📡 下载数据库初始化文件..."
curl -L -o gost.sql "$GOST_SQL_URL"
fi
echo "✅ 文件准备完成"
# 自动检测并配置 IPv6 支持
@@ -216,9 +199,6 @@ install_panel() {
fi
cat > .env <<EOF
DB_NAME=$DB_NAME
DB_USER=$DB_USER
DB_PASSWORD=$DB_PASSWORD
JWT_SECRET=$JWT_SECRET
FRONTEND_PORT=$FRONTEND_PORT
BACKEND_PORT=$BACKEND_PORT
@@ -254,7 +234,15 @@ update_panel() {
configure_docker_ipv6
fi
echo "🛑 停止当前服务..."
# 先发送 SIGTERM 信号,让应用优雅关闭
docker stop -t 30 springboot-backend 2>/dev/null || true
docker stop -t 10 vite-frontend 2>/dev/null || true
# 等待 WAL 文件同步
echo "⏳ 等待数据同步..."
sleep 5
# 然后再完全停止
$DOCKER_CMD down
echo "⬇️ 拉取最新镜像..."
@@ -297,755 +285,9 @@ update_panel() {
sleep 1
done
# 检查数据库容器健康状态
echo "🔍 检查数据库服务状态..."
for i in {1..60}; do
if docker ps --format "{{.Names}}" | grep -q "^gost-mysql$"; then
DB_HEALTH=$(docker inspect -f '{{.State.Health.Status}}' gost-mysql 2>/dev/null || echo "unknown")
if [[ "$DB_HEALTH" == "healthy" ]]; then
echo "✅ 数据库服务健康检查通过"
break
elif [[ "$DB_HEALTH" == "starting" ]]; then
# 继续等待
:
elif [[ "$DB_HEALTH" == "unhealthy" ]]; then
echo "⚠️ 数据库健康状态:$DB_HEALTH"
fi
else
echo "⚠️ 数据库容器未找到或未运行"
DB_HEALTH="not_running"
fi
if [ $i -eq 60 ]; then
echo "❌ 数据库服务启动超时(60秒)"
echo "🔍 当前状态:$(docker inspect -f '{{.State.Health.Status}}' gost-mysql 2>/dev/null || echo '容器不存在')"
echo "🛑 更新终止"
return 1
fi
# 每10秒显示一次进度
if [ $((i % 10)) -eq 1 ]; then
echo "⏳ 等待数据库服务启动... ($i/60) 状态:${DB_HEALTH:-unknown}"
fi
sleep 1
done
# 从容器环境变量获取数据库信息
echo "🔍 获取数据库配置信息..."
# 等待一下让服务完全就绪
echo "⏳ 等待服务完全就绪..."
sleep 5
# 先检查后端容器是否在运行
if ! docker ps --format "{{.Names}}" | grep -q "^springboot-backend$"; then
echo "❌ 后端容器未运行,无法获取数据库配置"
echo "🔍 当前运行的容器:"
docker ps --format "table {{.Names}}\t{{.Status}}"
echo "🛑 更新终止"
return 1
fi
DB_INFO=$(docker exec springboot-backend env | grep "^DB_" 2>/dev/null || echo "")
if [[ -n "$DB_INFO" ]]; then
DB_NAME=$(echo "$DB_INFO" | grep "^DB_NAME=" | cut -d'=' -f2)
DB_PASSWORD=$(echo "$DB_INFO" | grep "^DB_PASSWORD=" | cut -d'=' -f2)
DB_USER=$(echo "$DB_INFO" | grep "^DB_USER=" | cut -d'=' -f2)
DB_HOST=$(echo "$DB_INFO" | grep "^DB_HOST=" | cut -d'=' -f2)
echo "📋 数据库配置:"
echo " 数据库名: $DB_NAME"
echo " 用户名: $DB_USER"
echo " 主机: $DB_HOST"
else
echo "❌ 无法获取数据库配置信息"
echo "🔍 尝试诊断问题:"
echo " 容器状态: $(docker inspect -f '{{.State.Status}}' springboot-backend 2>/dev/null || echo '容器不存在')"
echo " 健康状态: $(docker inspect -f '{{.State.Health.Status}}' springboot-backend 2>/dev/null || echo '无健康检查')"
# 尝试从 .env 文件读取配置
if [[ -f ".env" ]]; then
echo "🔄 尝试从 .env 文件读取配置..."
DB_NAME=$(grep "^DB_NAME=" .env | cut -d'=' -f2 2>/dev/null)
DB_PASSWORD=$(grep "^DB_PASSWORD=" .env | cut -d'=' -f2 2>/dev/null)
DB_USER=$(grep "^DB_USER=" .env | cut -d'=' -f2 2>/dev/null)
if [[ -n "$DB_NAME" && -n "$DB_PASSWORD" && -n "$DB_USER" ]]; then
echo "✅ 从 .env 文件成功读取数据库配置"
echo "📋 数据库配置:"
echo " 数据库名: $DB_NAME"
echo " 用户名: $DB_USER"
else
echo "❌ .env 文件中的数据库配置不完整"
echo "🛑 更新终止"
return 1
fi
else
echo "❌ 未找到 .env 文件"
echo "🛑 更新终止"
return 1
fi
fi
# 检查必要的数据库配置
if [[ -z "$DB_PASSWORD" || -z "$DB_USER" || -z "$DB_NAME" ]]; then
echo "❌ 数据库配置不完整(缺少必要参数)"
echo "🛑 更新终止"
return 1
fi
# 执行数据库字段变更
echo "🔄 执行数据库结构更新..."
# 创建临时迁移文件(现在有了数据库信息)
cat > temp_migration.sql <<EOF
-- 数据库结构更新
USE \`$DB_NAME\`;
-- user 表:删除 name 字段(如果存在)
SET @sql = (
SELECT IF(
EXISTS (
SELECT 1
FROM information_schema.COLUMNS
WHERE table_schema = DATABASE()
AND table_name = 'user'
AND column_name = 'name'
),
'ALTER TABLE \`user\` DROP COLUMN \`name\`;',
'SELECT "Column \`name\` not exists in \`user\`";'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- node 表:删除 port 字段、添加 server_ip 字段(如果不存在)
SET @sql = (
SELECT IF(
EXISTS (
SELECT 1
FROM information_schema.COLUMNS
WHERE table_schema = DATABASE()
AND table_name = 'node'
AND column_name = 'port'
),
'ALTER TABLE \`node\` DROP COLUMN \`port\`;',
'SELECT "Column \`port\` not exists in \`node\`";'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql = (
SELECT IF(
NOT EXISTS (
SELECT 1
FROM information_schema.COLUMNS
WHERE table_schema = DATABASE()
AND table_name = 'node'
AND column_name = 'server_ip'
),
'ALTER TABLE \`node\` ADD COLUMN \`server_ip\` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;',
'SELECT "Column \`server_ip\` already exists in \`node\`";'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 将 ip 赋值给 server_ip(如果字段都存在)
UPDATE \`node\`
SET \`server_ip\` = \`ip\`
WHERE \`server_ip\` IS NULL;
-- node 表:修改 ip 字段类型为 longtext
SET @sql = (
SELECT IF(
EXISTS (
SELECT 1
FROM information_schema.COLUMNS
WHERE table_schema = DATABASE()
AND table_name = 'node'
AND column_name = 'ip'
AND data_type = 'varchar'
),
'ALTER TABLE \`node\` MODIFY COLUMN \`ip\` LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;',
'SELECT "Column \`ip\` not exists or already modified in \`node\`";'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- node 表:添加 version 字段(如果不存在)
SET @sql = (
SELECT IF(
NOT EXISTS (
SELECT 1
FROM information_schema.COLUMNS
WHERE table_schema = DATABASE()
AND table_name = 'node'
AND column_name = 'version'
),
'ALTER TABLE \`node\` ADD COLUMN \`version\` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL;',
'SELECT "Column \`version\` already exists in \`node\`";'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- node 表:添加 port_sta 字段(如果不存在)
SET @sql = (
SELECT IF(
NOT EXISTS (
SELECT 1
FROM information_schema.COLUMNS
WHERE table_schema = DATABASE()
AND table_name = 'node'
AND column_name = 'port_sta'
),
'ALTER TABLE \`node\` ADD COLUMN \`port_sta\` INT(10) DEFAULT 1000 COMMENT "端口起始范围";',
'SELECT "Column \`port_sta\` already exists in \`node\`";'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- node 表:添加 port_end 字段(如果不存在)
SET @sql = (
SELECT IF(
NOT EXISTS (
SELECT 1
FROM information_schema.COLUMNS
WHERE table_schema = DATABASE()
AND table_name = 'node'
AND column_name = 'port_end'
),
'ALTER TABLE \`node\` ADD COLUMN \`port_end\` INT(10) DEFAULT 65535 COMMENT "端口结束范围";',
'SELECT "Column \`port_end\` already exists in \`node\`";'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 为现有节点设置默认端口范围
UPDATE \`node\`
SET \`port_sta\` = 1000, \`port_end\` = 65535
WHERE \`port_sta\` IS NULL OR \`port_end\` IS NULL;
-- node 表:添加 http、tls、socks 字段(如果不存在)
SET @sql = (
SELECT IF(
NOT EXISTS (
SELECT 1
FROM information_schema.COLUMNS
WHERE table_schema = DATABASE()
AND table_name = 'node'
AND column_name = 'http'
),
'ALTER TABLE \`node\` ADD COLUMN \`http\` INT(10) DEFAULT 0 COMMENT "HTTP 服务端口";',
'SELECT "Column \`http\` already exists in \`node\`";'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql = (
SELECT IF(
NOT EXISTS (
SELECT 1
FROM information_schema.COLUMNS
WHERE table_schema = DATABASE()
AND table_name = 'node'
AND column_name = 'tls'
),
'ALTER TABLE \`node\` ADD COLUMN \`tls\` INT(10) DEFAULT 0 COMMENT "TLS 服务端口";',
'SELECT "Column \`tls\` already exists in \`node\`";'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql = (
SELECT IF(
NOT EXISTS (
SELECT 1
FROM information_schema.COLUMNS
WHERE table_schema = DATABASE()
AND table_name = 'node'
AND column_name = 'socks'
),
'ALTER TABLE \`node\` ADD COLUMN \`socks\` INT(10) DEFAULT 0 COMMENT "SOCKS 服务端口";',
'SELECT "Column \`socks\` already exists in \`node\`";'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 为现有节点设置 http、tls、socks 默认值
UPDATE \`node\`
SET \`http\` = IFNULL(\`http\`, 0),
\`tls\` = IFNULL(\`tls\`, 0),
\`socks\` = IFNULL(\`socks\`, 0);
-- tunnel 表:删除废弃字段(如果存在)
SET @sql = (
SELECT IF(
EXISTS (
SELECT 1
FROM information_schema.COLUMNS
WHERE table_schema = DATABASE()
AND table_name = 'tunnel'
AND column_name = 'in_port_sta'
),
'ALTER TABLE \`tunnel\` DROP COLUMN \`in_port_sta\`;',
'SELECT "Column \`in_port_sta\` not exists in \`tunnel\`";'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql = (
SELECT IF(
EXISTS (
SELECT 1
FROM information_schema.COLUMNS
WHERE table_schema = DATABASE()
AND table_name = 'tunnel'
AND column_name = 'in_port_end'
),
'ALTER TABLE \`tunnel\` DROP COLUMN \`in_port_end\`;',
'SELECT "Column \`in_port_end\` not exists in \`tunnel\`";'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql = (
SELECT IF(
EXISTS (
SELECT 1
FROM information_schema.COLUMNS
WHERE table_schema = DATABASE()
AND table_name = 'tunnel'
AND column_name = 'out_ip_sta'
),
'ALTER TABLE \`tunnel\` DROP COLUMN \`out_ip_sta\`;',
'SELECT "Column \`out_ip_sta\` not exists in \`tunnel\`";'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql = (
SELECT IF(
EXISTS (
SELECT 1
FROM information_schema.COLUMNS
WHERE table_schema = DATABASE()
AND table_name = 'tunnel'
AND column_name = 'out_ip_end'
),
'ALTER TABLE \`tunnel\` DROP COLUMN \`out_ip_end\`;',
'SELECT "Column \`out_ip_end\` not exists in \`tunnel\`";'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- tunnel 表:添加 tcp_listen_addr、udp_listen_addr、protocol(如果不存在)
-- tcp_listen_addr
SET @sql = (
SELECT IF(
NOT EXISTS (
SELECT 1
FROM information_schema.COLUMNS
WHERE table_schema = DATABASE()
AND table_name = 'tunnel'
AND column_name = 'tcp_listen_addr'
),
'ALTER TABLE \`tunnel\` ADD COLUMN \`tcp_listen_addr\` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT "0.0.0.0";',
'SELECT "Column \`tcp_listen_addr\` already exists in \`tunnel\`";'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- udp_listen_addr
SET @sql = (
SELECT IF(
NOT EXISTS (
SELECT 1
FROM information_schema.COLUMNS
WHERE table_schema = DATABASE()
AND table_name = 'tunnel'
AND column_name = 'udp_listen_addr'
),
'ALTER TABLE \`tunnel\` ADD COLUMN \`udp_listen_addr\` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT "0.0.0.0";',
'SELECT "Column \`udp_listen_addr\` already exists in \`tunnel\`";'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- protocol
SET @sql = (
SELECT IF(
NOT EXISTS (
SELECT 1
FROM information_schema.COLUMNS
WHERE table_schema = DATABASE()
AND table_name = 'tunnel'
AND column_name = 'protocol'
),
'ALTER TABLE \`tunnel\` ADD COLUMN \`protocol\` VARCHAR(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT "tls";',
'SELECT "Column \`protocol\` already exists in \`tunnel\`";'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- traffic_ratio (流量倍率)
SET @sql = (
SELECT IF(
NOT EXISTS (
SELECT 1
FROM information_schema.COLUMNS
WHERE table_schema = DATABASE()
AND table_name = 'tunnel'
AND column_name = 'traffic_ratio'
),
'ALTER TABLE \`tunnel\` ADD COLUMN \`traffic_ratio\` DECIMAL(5,1) DEFAULT 1.0 COMMENT "流量倍率";',
'SELECT "Column \`traffic_ratio\` already exists in \`tunnel\`";'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 为现有数据设置默认流量倍率
UPDATE \`tunnel\`
SET \`traffic_ratio\` = 1.0
WHERE \`traffic_ratio\` IS NULL;
-- forward 表:删除 proxy_protocol 字段(如果存在)
SET @sql = (
SELECT IF(
EXISTS (
SELECT 1
FROM information_schema.COLUMNS
WHERE table_schema = DATABASE()
AND table_name = 'forward'
AND column_name = 'proxy_protocol'
),
'ALTER TABLE \`forward\` DROP COLUMN \`proxy_protocol\`;',
'SELECT "Column \`proxy_protocol\` not exists in \`forward\`";'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- forward 表:修改 remote_addr 字段类型为 longtext
SET @sql = (
SELECT IF(
EXISTS (
SELECT 1
FROM information_schema.COLUMNS
WHERE table_schema = DATABASE()
AND table_name = 'forward'
AND column_name = 'remote_addr'
AND data_type = 'varchar'
),
'ALTER TABLE \`forward\` MODIFY COLUMN \`remote_addr\` LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL;',
'SELECT "Column \`remote_addr\` not exists or already modified in \`forward\`";'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- forward 表:添加 strategy 字段(负载均衡策略)
SET @sql = (
SELECT IF(
NOT EXISTS (
SELECT 1
FROM information_schema.COLUMNS
WHERE table_schema = DATABASE()
AND table_name = 'forward'
AND column_name = 'strategy'
),
'ALTER TABLE \`forward\` ADD COLUMN \`strategy\` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT "fifo" COMMENT "负载均衡策略";',
'SELECT "Column \`strategy\` already exists in \`forward\`";'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 为现有数据设置默认负载均衡策略
UPDATE \`forward\`
SET \`strategy\` = 'fifo'
WHERE \`strategy\` IS NULL;
-- forward 表:添加 inx 字段(排序索引)
SET @sql = (
SELECT IF(
NOT EXISTS (
SELECT 1
FROM information_schema.COLUMNS
WHERE table_schema = DATABASE()
AND table_name = 'forward'
AND column_name = 'inx'
),
'ALTER TABLE \`forward\` ADD COLUMN \`inx\` INT(10) DEFAULT 0 COMMENT "排序索引";',
'SELECT "Column \`inx\` already exists in \`forward\`";'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 为现有数据设置默认排序索引
UPDATE \`forward\`
SET \`inx\` = 0
WHERE \`inx\` IS NULL;
-- tunnel 表:添加 interface_name 字段(如果不存在)
SET @sql = (
SELECT IF(
NOT EXISTS (
SELECT 1
FROM information_schema.COLUMNS
WHERE table_schema = DATABASE()
AND table_name = 'tunnel'
AND column_name = 'interface_name'
),
'ALTER TABLE \`tunnel\` ADD COLUMN \`interface_name\` VARCHAR(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL;',
'SELECT "Column \`interface_name\` already exists in \`tunnel\`";'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- forward 表:添加 interface_name 字段(如果不存在)
SET @sql = (
SELECT IF(
NOT EXISTS (
SELECT 1
FROM information_schema.COLUMNS
WHERE table_schema = DATABASE()
AND table_name = 'forward'
AND column_name = 'interface_name'
),
'ALTER TABLE \`forward\` ADD COLUMN \`interface_name\` VARCHAR(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL;',
'SELECT "Column \`interface_name\` already exists in \`forward\`";'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 创建 vite_config 表(如果不存在)
CREATE TABLE IF NOT EXISTS \`vite_config\` (
\`id\` int(10) NOT NULL AUTO_INCREMENT,
\`name\` varchar(200) NOT NULL,
\`value\` varchar(200) NOT NULL,
\`time\` bigint(20) NOT NULL,
PRIMARY KEY (\`id\`),
UNIQUE KEY \`unique_name\` (\`name\`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 创建 statistics_flow 表(如果不存在)
CREATE TABLE IF NOT EXISTS \`statistics_flow\` (
\`id\` bigint(20) NOT NULL AUTO_INCREMENT,
\`user_id\` int(10) NOT NULL,
\`flow\` bigint(20) NOT NULL,
\`total_flow\` bigint(20) NOT NULL,
\`time\` varchar(100) NOT NULL,
\`created_time\` bigint(20) NOT NULL,
PRIMARY KEY (\`id\`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- statistics_flow 表:添加 created_time 字段(如果不存在)
SET @sql = (
SELECT IF(
NOT EXISTS (
SELECT 1
FROM information_schema.COLUMNS
WHERE table_schema = DATABASE()
AND table_name = 'statistics_flow'
AND column_name = 'created_time'
),
'ALTER TABLE \`statistics_flow\` ADD COLUMN \`created_time\` BIGINT(20) NOT NULL DEFAULT 0 COMMENT "创建时间毫秒时间戳";',
'SELECT "Column \`created_time\` already exists in \`statistics_flow\`";'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 为现有记录设置当前毫秒时间戳(仅当 created_time 为 0 或 NULL 时)
UPDATE \`statistics_flow\`
SET \`created_time\` = UNIX_TIMESTAMP() * 1000
WHERE \`created_time\` = 0 OR \`created_time\` IS NULL;
EOF
# 检查数据库容器
if ! docker ps --format "{{.Names}}" | grep -q "^gost-mysql$"; then
echo "❌ 数据库容器 gost-mysql 未运行"
echo "🔍 当前运行的容器:"
docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"
echo "❌ 数据库结构更新失败,请手动执行 temp_migration.sql"
echo "📁 迁移文件已保存为 temp_migration.sql"
return 1
fi
# 执行数据库迁移
if docker exec -i gost-mysql mysql -u "$DB_USER" -p"$DB_PASSWORD" < temp_migration.sql 2>/dev/null; then
echo "✅ 数据库结构更新完成"
else
echo "⚠️ 使用用户密码失败,尝试root密码..."
if docker exec -i gost-mysql mysql -u root -p"$DB_PASSWORD" < temp_migration.sql 2>/dev/null; then
echo "✅ 数据库结构更新完成"
else
echo "❌ 数据库结构更新失败,请手动执行 temp_migration.sql"
echo "📁 迁移文件已保存为 temp_migration.sql"
echo "🔍 数据库容器状态: $(docker inspect -f '{{.State.Status}}' gost-mysql 2>/dev/null || echo '容器不存在')"
echo "🛑 更新终止"
return 1
fi
fi
# 清理临时文件
rm -f temp_migration.sql
echo "✅ 更新完成"
}
# 导出数据库备份
export_migration_sql() {
echo "📄 开始导出数据库备份..."
# 获取数据库配置信息
echo "🔍 获取数据库配置信息..."
# 先检查后端容器是否在运行
if ! docker ps --format "{{.Names}}" | grep -q "^springboot-backend$"; then
echo "❌ 后端容器未运行,尝试从 .env 文件读取配置..."
# 从 .env 文件读取配置
if [[ -f ".env" ]]; then
DB_NAME=$(grep "^DB_NAME=" .env | cut -d'=' -f2 2>/dev/null)
DB_PASSWORD=$(grep "^DB_PASSWORD=" .env | cut -d'=' -f2 2>/dev/null)
DB_USER=$(grep "^DB_USER=" .env | cut -d'=' -f2 2>/dev/null)
if [[ -n "$DB_NAME" && -n "$DB_PASSWORD" && -n "$DB_USER" ]]; then
echo "✅ 从 .env 文件读取数据库配置成功"
else
echo "❌ .env 文件中的数据库配置不完整"
return 1
fi
else
echo "❌ 未找到 .env 文件"
return 1
fi
else
# 从容器环境变量获取数据库信息
DB_INFO=$(docker exec springboot-backend env | grep "^DB_" 2>/dev/null || echo "")
if [[ -n "$DB_INFO" ]]; then
DB_NAME=$(echo "$DB_INFO" | grep "^DB_NAME=" | cut -d'=' -f2)
DB_PASSWORD=$(echo "$DB_INFO" | grep "^DB_PASSWORD=" | cut -d'=' -f2)
DB_USER=$(echo "$DB_INFO" | grep "^DB_USER=" | cut -d'=' -f2)
echo "✅ 从容器环境变量读取数据库配置成功"
else
echo "❌ 无法从容器获取数据库配置,尝试从 .env 文件读取..."
if [[ -f ".env" ]]; then
DB_NAME=$(grep "^DB_NAME=" .env | cut -d'=' -f2 2>/dev/null)
DB_PASSWORD=$(grep "^DB_PASSWORD=" .env | cut -d'=' -f2 2>/dev/null)
DB_USER=$(grep "^DB_USER=" .env | cut -d'=' -f2 2>/dev/null)
if [[ -n "$DB_NAME" && -n "$DB_PASSWORD" && -n "$DB_USER" ]]; then
echo "✅ 从 .env 文件读取数据库配置成功"
else
echo "❌ .env 文件中的数据库配置不完整"
return 1
fi
else
echo "❌ 未找到 .env 文件"
return 1
fi
fi
fi
# 检查必要的数据库配置
if [[ -z "$DB_PASSWORD" || -z "$DB_USER" || -z "$DB_NAME" ]]; then
echo "❌ 数据库配置不完整(缺少必要参数)"
return 1
fi
echo "📋 数据库配置:"
echo " 数据库名: $DB_NAME"
echo " 用户名: $DB_USER"
# 检查数据库容器是否运行
if ! docker ps --format "{{.Names}}" | grep -q "^gost-mysql$"; then
echo "❌ 数据库容器未运行,无法导出数据"
echo "🔍 当前运行的容器:"
docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"
return 1
fi
# 生成数据库备份文件
SQL_FILE="database_backup_$(date +%Y%m%d_%H%M%S).sql"
echo "📝 导出数据库备份: $SQL_FILE"
# 使用 mysqldump 导出数据库
echo "⏳ 正在导出数据库..."
if docker exec gost-mysql mysqldump -u "$DB_USER" -p"$DB_PASSWORD" --single-transaction --routines --triggers "$DB_NAME" > "$SQL_FILE" 2>/dev/null; then
echo "✅ 数据库导出成功"
else
echo "⚠️ 使用用户密码失败,尝试root密码..."
if docker exec gost-mysql mysqldump -u root -p"$DB_PASSWORD" --single-transaction --routines --triggers "$DB_NAME" > "$SQL_FILE" 2>/dev/null; then
echo "✅ 数据库导出成功"
else
echo "❌ 数据库导出失败"
rm -f "$SQL_FILE"
return 1
fi
fi
# 检查文件大小
if [[ -f "$SQL_FILE" ]] && [[ -s "$SQL_FILE" ]]; then
FILE_SIZE=$(du -h "$SQL_FILE" | cut -f1)
echo "📁 文件位置: $(pwd)/$SQL_FILE"
echo "📊 文件大小: $FILE_SIZE"
else
echo "❌ 导出的文件为空或不存在"
rm -f "$SQL_FILE"
return 1
fi
}
# 卸载功能
@@ -1070,7 +312,7 @@ uninstall_panel() {
echo "🛑 停止并删除容器、镜像、卷..."
$DOCKER_CMD down --rmi all --volumes --remove-orphans
echo "🧹 删除配置文件..."
rm -f docker-compose.yml gost.sql .env
rm -f docker-compose.yml .env
echo "✅ 卸载完成"
}
@@ -1099,11 +341,6 @@ main() {
exit 0
;;
4)
export_migration_sql
delete_self
exit 0
;;
5)
echo "👋 退出脚本"
delete_self
exit 0
+2 -1
View File
@@ -16,11 +16,12 @@ ENV LC_ALL=en_US.UTF-8
RUN sed -i 's/archive.ubuntu.com/mirrors.ustc.edu.cn/g' /etc/apt/sources.list \
&& sed -i 's/security.ubuntu.com/mirrors.ustc.edu.cn/g' /etc/apt/sources.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends fontconfig fonts-dejavu \
&& apt-get install -y --no-install-recommends fontconfig fonts-dejavu sqlite3 \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
COPY --from=build /app/target/*.jar app.jar
EXPOSE 6365
ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar /app/app.jar"]
+3 -4
View File
@@ -69,10 +69,9 @@
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.2.0</version>
<scope>runtime</scope>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.45.0.0</version>
</dependency>
<dependency>
@@ -1,128 +0,0 @@
package com.admin;
import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.InjectionConfig;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
// 演示例子,执行 main 方法控制台输入模块表名回车自动生成对应项目目录中
public class CodeGenerator {
/**
* <p>
* 读取控制台内容
* </p>
*/
public static String scanner(String tip) {
Scanner scanner = new Scanner(System.in);
StringBuilder help = new StringBuilder();
help.append("请输入" + tip + ":");
System.out.println(help.toString());
if (scanner.hasNext()) {
String ipt = scanner.next();
if (StringUtils.isNotBlank(ipt)) {
return ipt;
}
}
throw new MybatisPlusException("请输入正确的" + tip + "!");
}
public static void main(String[] args) {
// 代码生成器
AutoGenerator mpg = new AutoGenerator();
// 全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
gc.setOutputDir(projectPath + "/src/main/java");
gc.setAuthor("QAQ");
gc.setOpen(false);
// gc.setSwagger2(true); 实体属性 Swagger2 注解
gc.setServiceName("%sService");
mpg.setGlobalConfig(gc);
// 数据源配置 - 使用环境变量
DataSourceConfig dsc = new DataSourceConfig();
String dbHost = System.getenv("DB_HOST");
String dbName = System.getenv("DB_NAME");
String dbUser = System.getenv("DB_USER");
String dbPassword = System.getenv("DB_PASSWORD");
if (dbHost == null || dbName == null || dbUser == null || dbPassword == null) {
throw new MybatisPlusException("请设置数据库环境变量: DB_HOST, DB_NAME, DB_USER, DB_PASSWORD");
}
dsc.setUrl("jdbc:mysql://" + dbHost + "/" + dbName + "?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai");
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername(dbUser);
dsc.setPassword(dbPassword);
mpg.setDataSource(dsc);
// 包配置
PackageConfig pc = new PackageConfig();
// pc.setModuleName(scanner("模块名"));
pc.setParent("com.admin");
mpg.setPackageInfo(pc);
// 自定义配置
InjectionConfig cfg = new InjectionConfig() {
@Override
public void initMap() {
// to do nothing
}
};
// 如果模板引擎是 freemarker
String templatePath = "/templates/mapper.xml.ftl";
// 如果模板引擎是 velocity
// String templatePath = "/templates/mapper.xml.vm";
// 自定义输出配置
List<FileOutConfig> focList = new ArrayList<>();
// 自定义配置会被优先输出
focList.add(new FileOutConfig(templatePath) {
@Override
public String outputFile(TableInfo tableInfo) {
return projectPath + "/src/main/resources/mapper/" + pc.getModuleName()
+ "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
}
});
cfg.setFileOutConfigList(focList);
mpg.setCfg(cfg);
// 配置模板
TemplateConfig templateConfig = new TemplateConfig();
templateConfig.setXml(null);
mpg.setTemplate(templateConfig);
// 策略配置
StrategyConfig strategy = new StrategyConfig();
strategy.setNaming(NamingStrategy.underline_to_camel);
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
strategy.setSuperEntityClass("com.admin.entity.BaseEntity");
strategy.setEntityLombokModel(true);
strategy.setRestControllerStyle(true);
// 公共父类
strategy.setSuperControllerClass("com.admin.controller.BaseController");
strategy.setSuperEntityColumns("id", "created_time", "updated_time", "status");
strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
strategy.setControllerMappingHyphenStyle(true);
// strategy.setTablePrefix("sys_");//动态调整
mpg.setStrategy(strategy);
mpg.setTemplateEngine(new FreemarkerTemplateEngine());
mpg.execute();
}
}
@@ -0,0 +1,15 @@
package com.admin.common.dto;
import lombok.Data;
import java.util.List;
@Data
public class ChainNodesItems {
private String mode;
private List<Integer> nodeIds;
}
@@ -0,0 +1,21 @@
package com.admin.common.dto;
import lombok.Data;
@Data
public class DiagnosisResult {
private Long nodeId;
private String nodeName;
private String targetIp;
private Integer targetPort;
private String description;
private boolean success;
private String message;
private double averageTime;
private double packetLoss;
private long timestamp;
private Integer fromChainType;
private Integer fromInx;
private Integer toChainType;
private Integer toInx;
}
@@ -21,14 +21,6 @@ public class ForwardDto {
private String remoteAddr;
private String strategy;
/**
* 入口端口(可选,为空时自动分配)
*/
@Min(value = 1, message = "端口号不能小于1")
@Max(value = 65535, message = "端口号不能大于65535")
private Integer inPort;
private String interfaceName;
}
@@ -0,0 +1,10 @@
package com.admin.common.dto;
import lombok.Data;
@Data
public class ForwardPortDto {
private Integer node_id;
private Integer port;
}
@@ -19,22 +19,12 @@ public class ForwardUpdateDto {
@NotBlank(message = "转发名称不能为空")
private String name;
@NotNull(message = "隧道ID不能为空")
private Integer tunnelId;
@NotBlank(message = "远程地址不能为空")
private String remoteAddr;
private String strategy;
/**
* 入口端口(可选,为空时自动分配)
*/
@Min(value = 1, message = "端口号不能小于1")
@Max(value = 65535, message = "端口号不能大于65535")
private Integer inPort;
@TableField(updateStrategy = FieldStrategy.IGNORED)
private String interfaceName;
}
@@ -25,6 +25,11 @@ public class ForwardWithTunnelDto {
private String name;
/**
* 入口IP
*/
private String inIp;
/**
* 入口端口
*/
@@ -56,15 +61,10 @@ public class ForwardWithTunnelDto {
* 隧道名称
*/
private String tunnelName;
/**
* 入口IP
*/
private String inIp;
private String userName;
/**
* 用户ID
*/
@@ -87,6 +87,4 @@ public class ForwardWithTunnelDto {
private String strategy;
private Integer inx;
private String interfaceName;
}
@@ -13,19 +13,16 @@ public class NodeDto {
@NotBlank(message = "节点名称不能为空")
private String name;
@NotBlank(message = "入口IP不能为空")
private String ip;
@NotBlank(message = "服务器ip不能为空")
private String serverIp;
@NotNull(message = "起始端口不能为空")
@Min(value = 1, message = "起始端口必须大于0")
@Max(value = 65535, message = "起始端口不能超过65535")
private Integer portSta;
@NotBlank(message = "可用端口不能为空")
private String port;
private String interfaceName;
private String tcpListenAddr = "0.0.0.0";
private String udpListenAddr = "0.0.0.0";
@NotNull(message = "结束端口不能为空")
@Min(value = 1, message = "结束端口必须大于0")
@Max(value = 65535, message = "结束端口不能超过65535")
private Integer portEnd;
}
@@ -4,8 +4,6 @@ import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
@Data
public class NodeUpdateDto {
@@ -16,23 +14,18 @@ public class NodeUpdateDto {
@NotBlank(message = "节点名称不能为空")
private String name;
@NotBlank(message = "入口IP不能为空")
private String ip;
@NotBlank(message = "服务器ip不能为空")
private String serverIp;
@NotNull(message = "起始端口不能为空")
@Min(value = 1, message = "起始端口必须大于0")
@Max(value = 65535, message = "起始端口不能超过65535")
private Integer portSta;
@NotNull(message = "结束端口不能为空")
@Min(value = 1, message = "结束端口必须大于0")
@Max(value = 65535, message = "结束端口不能超过65535")
private Integer portEnd;
@NotBlank(message = "可用port不能为空")
private String port;
private String interfaceName;
private Integer http;
private Integer tls;
private Integer socks;
private String tcpListenAddr = "0.0.0.0";
private String udpListenAddr = "0.0.0.0";
}
@@ -17,10 +17,4 @@ public class SpeedLimitUpdateDto {
@NotNull(message = "速度限制不能为空")
@Min(value = 1, message = "速度限制必须大于0")
private Integer speed;
@NotNull(message = "隧道ID不能为空")
private Long tunnelId;
@NotBlank(message = "隧道名称不能为空")
private String tunnelName;
}
@@ -0,0 +1,43 @@
package com.admin.common.dto;
import com.admin.entity.ChainTunnel;
import lombok.Data;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
/**
* 隧道详情DTO - 包含节点配置信息
*/
@Data
public class TunnelDetailDto {
private Long id;
private String name;
private Integer type; // 1: 端口转发, 2: 隧道转发
private Integer flow; // 1: 单向, 2: 双向
private BigDecimal trafficRatio;
private Integer status;
private Long createdTime;
private Long updatedTime;
private String inIp;
// 入口节点列表
private List<ChainTunnel> inNodeId = new ArrayList<>();
// 转发链节点列表(二维数组结构)
private List<List<ChainTunnel>> chainNodes = new ArrayList<>();
// 出口节点列表
private List<ChainTunnel> outNodeId = new ArrayList<>();
}
@@ -1,45 +1,36 @@
package com.admin.common.dto;
import com.admin.entity.ChainTunnel;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Min;
import javax.validation.constraints.Max;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.DecimalMax;
import java.math.BigDecimal;
import java.util.List;
@Data
public class TunnelDto {
@NotBlank(message = "隧道名称不能为空")
private String name;
@NotNull(message = "入口节点不能为空")
private Long inNodeId;
// 出口节点ID,当type=1时可以为空,会自动设置为入口节点ID
private Long outNodeId;
@NotNull(message = "入口节点不能为空")
private List<ChainTunnel> inNodeId;
private List<List<ChainTunnel>> chainNodes;
private List<ChainTunnel> outNodeId;
private String inIp;
@NotNull(message = "隧道类型不能为空")
private Integer type;
@NotNull(message = "流量计算类型不能为空")
private Integer flow;
// 流量倍率,默认为1.0
@DecimalMin(value = "0.0", inclusive = false, message = "流量倍率必须大于0.0")
@DecimalMax(value = "100.0", message = "流量倍率不能大于100.0")
private BigDecimal trafficRatio;
private String interfaceName;
// 协议类型,默认为tls
private String protocol;
// TCP监听地址,默认为0.0.0.0
private String tcpListenAddr = "0.0.0.0";
// UDP监听地址,默认为0.0.0.0
private String udpListenAddr = "0.0.0.0";
}
@@ -1,36 +0,0 @@
package com.admin.common.dto;
import lombok.Data;
@Data
public class TunnelListDto {
private Integer id;
private String name;
/**
* 入口IP
*/
private String ip;
/**
* 入口节点端口起始范围
*/
private Integer inNodePortSta;
/**
* 入口节点端口结束范围
*/
private Integer inNodePortEnd;
/**
* 隧道类型(1-端口转发,2-隧道转发)
*/
private Integer type;
/**
* 协议类型
*/
private String protocol;
}
@@ -1,5 +1,6 @@
package com.admin.common.dto;
import com.admin.entity.ChainTunnel;
import com.baomidou.mybatisplus.annotation.FieldStrategy;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
@@ -10,35 +11,23 @@ import javax.validation.constraints.Max;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.DecimalMax;
import java.math.BigDecimal;
import java.util.List;
@Data
public class TunnelUpdateDto {
@NotNull(message = "隧道ID不能为空")
private Long id;
@NotBlank(message = "隧道名称不能为空")
private String name;
@NotNull(message = "流量计算类型不能为空")
private Integer flow;
// 流量倍率
private String inIp;
@DecimalMin(value = "0.0", inclusive = false, message = "流量倍率必须大于0.0")
@DecimalMax(value = "100.0", message = "流量倍率不能大于100.0")
private BigDecimal trafficRatio;
@NotBlank
private String protocol;
// TCP监听地址
@NotBlank
private String tcpListenAddr;
// UDP监听地址
@NotBlank
private String udpListenAddr;
@TableField(updateStrategy = FieldStrategy.IGNORED)
private String interfaceName;
}
}
@@ -0,0 +1,10 @@
package com.admin.common.dto;
import lombok.Data;
@Data
public class UserInfo {
private final Integer userId;
private final Integer roleId;
private final String userName;
}
@@ -0,0 +1,26 @@
package com.admin.common.dto;
import com.admin.entity.UserTunnel;
import com.admin.service.impl.ForwardServiceImpl;
import lombok.Data;
@Data
public class UserPermissionResult {
public boolean hasError;
private String errorMessage;
private Integer limiter;
private UserTunnel userTunnel;
public static UserPermissionResult success(Integer limiter, UserTunnel userTunnel) {
UserPermissionResult result = new UserPermissionResult();
result.setLimiter(limiter);
result.setUserTunnel(userTunnel);
return result;
}
public static UserPermissionResult error(String errorMessage) {
UserPermissionResult result = new UserPermissionResult();
result.setErrorMessage(errorMessage);
return result;
}
}
@@ -5,8 +5,7 @@ import com.admin.common.lang.R;
import com.admin.common.utils.GostUtil;
import com.admin.entity.*;
import com.admin.service.*;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.alibaba.fastjson.JSONArray;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.annotation.Async;
@@ -14,6 +13,7 @@ import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
@@ -33,8 +33,7 @@ public class CheckGostConfigAsync {
private SpeedLimitService speedLimitService;
@Resource
@Lazy
private TunnelService tunnelService;
TunnelService tunnelService;
@@ -43,56 +42,51 @@ public class CheckGostConfigAsync {
*/
@Async
public void cleanNodeConfigs(String node_id, GostConfigDto gostConfig) {
System.out.println(JSONObject.toJSONString(gostConfig));
Node node = nodeService.getById(node_id);
if (node != null) {
cleanOrphanedServices(gostConfig, node);
cleanOrphanedChains(gostConfig, node);
cleanOrphanedLimiters(gostConfig, node);
cleanOrphanedServices(gostConfig.getServices(), node);
cleanOrphanedChains(gostConfig.getChains(), node);
cleanOrphanedLimiters(gostConfig.getLimiters(), node);
}
}
/**
* 清理孤立的服务
*/
private void cleanOrphanedServices(GostConfigDto gostConfig, Node node) {
if (gostConfig.getServices() == null) {
return;
}
for (ConfigItem service : gostConfig.getServices()) {
private void cleanOrphanedServices(List<ConfigItem> configItems, Node node) {
if (configItems == null) return;
for (ConfigItem service : configItems) {
safeExecute(() -> {
if (!Objects.equals(service.getName(), "web_api")){
String[] serviceIds = parseServiceName(service.getName());
if (serviceIds.length == 4) {
String forwardId = serviceIds[0];
String userId = serviceIds[1];
String userTunnelId = serviceIds[2];
String type = serviceIds[3];
List<String> serviceIds = parseServiceName(service.getName());
if (Objects.equals(type, "tcp")) { // 只处理TCP,避免重复处理
Forward forward = forwardService.getById(forwardId);
if (forward == null) {
log.info("删除孤立的服务: {} (节点: {})", service.getName(), node.getId());
GostDto gostDto = GostUtil.DeleteService(node.getId(), forwardId + "_" + userId + "_" + userTunnelId);
System.out.println(gostDto);
}
}
JSONArray services = new JSONArray();
if (Objects.equals(serviceIds.getLast(), "tls")){
String forward_id = serviceIds.getFirst();
services.add(forward_id + "_tls");
if (Objects.equals(type, "tls")) {
Forward forward = forwardService.getById(forwardId);
if (forward == null) {
log.info("删除孤立的服务: {} (节点: {})", service.getName(), node.getId());
GostUtil.DeleteRemoteService(node.getId(), forwardId+"_"+userId+"_"+userTunnelId);
}
Tunnel tunnel = tunnelService.getById(forward_id);
if (tunnel == null) {
GostUtil.DeleteService(node.getId(), services);
log.info("删除孤立的服务: {} (节点: {})", service.getName(), node.getId());
}
}
if (Objects.equals(serviceIds.getLast(), "tcp")){
String forward_id = serviceIds.getFirst();
services.add(forward_id + "_" + serviceIds.get(1) + "_" + serviceIds.get(2) + "_tcp");
services.add(forward_id + "_" + serviceIds.get(1) + "_" + serviceIds.get(2) + "_udp");
Forward forward = forwardService.getById(forward_id);
if (forward == null) {
GostUtil.DeleteService(node.getId(), services);
log.info("删除孤立的服务: {} (节点: {})", service.getName(), node.getId());
}
}
}
}, "清理服务 " + service.getName());
}
@@ -101,28 +95,15 @@ public class CheckGostConfigAsync {
/**
* 清理孤立的链
*/
private void cleanOrphanedChains(GostConfigDto gostConfig, Node node) {
if (gostConfig.getChains() == null) {
return;
}
for (ConfigItem chain : gostConfig.getChains()) {
private void cleanOrphanedChains(List<ConfigItem> configItems, Node node) {
if (configItems == null) return;
for (ConfigItem chain : configItems) {
safeExecute(() -> {
String[] serviceIds = parseServiceName(chain.getName());
if (serviceIds.length == 4) {
String forwardId = serviceIds[0];
String userId = serviceIds[1];
String userTunnelId = serviceIds[2];
String type = serviceIds[3];
if (Objects.equals(type, "chains")) {
Forward forward = forwardService.getById(forwardId);
if (forward == null) {
log.info("删除孤立的链: {} (节点: {})", chain.getName(), node.getId());
GostUtil.DeleteChains(node.getId(), forwardId+"_"+userId+"_"+userTunnelId);
}
}
List<String> serviceIds = parseServiceName(chain.getName());
Tunnel tunnel = tunnelService.getById(serviceIds.getLast());
if (tunnel == null) {
GostUtil.DeleteChains(node.getId(), chain.getName());
log.info("删除孤立的链: {} (节点: {})", chain.getName(), node.getId());
}
}, "清理链 " + chain.getName());
}
@@ -131,69 +112,21 @@ public class CheckGostConfigAsync {
/**
* 清理孤立的限流器
*/
private void cleanOrphanedLimiters(GostConfigDto gostConfig, Node node) {
if (gostConfig.getLimiters() == null) {
return;
}
private void cleanOrphanedLimiters(List<ConfigItem> configItems, Node node) {
if (configItems == null) return;
for (ConfigItem limiter : gostConfig.getLimiters()) {
for (ConfigItem limiter : configItems) {
safeExecute(() -> {
SpeedLimit speedLimit = speedLimitService.getById(limiter.getName());
if (speedLimit == null) {
log.info("删除孤立的限流器: {} (节点: {})", limiter.getName(), node.getId());
GostUtil.DeleteLimiters(node.getId(), Long.parseLong(limiter.getName()));
log.info("删除孤立的限流器: {} (节点: {})", limiter.getName(), node.getId());
}
}, "清理限流器 " + limiter.getName());
}
}
/**
* 同步限流器
*/
private void syncLimiters(GostConfigDto gostConfig, Node node) {
List<Tunnel> tunnelList = tunnelService.list(new QueryWrapper<Tunnel>().eq("in_node_id", node.getId()));
if (tunnelList == null || tunnelList.isEmpty()) return;
safeExecute(() -> {
StringBuilder tunnelIds = new StringBuilder();
for (Tunnel tunnel : tunnelList) {
tunnelIds.append(tunnel.getId()).append(",");
}
String ids = tunnelIds.deleteCharAt(tunnelIds.length() - 1).toString();
List<SpeedLimit> speedLimits = speedLimitService.list(new QueryWrapper<SpeedLimit>().in("tunnel_id", ids));
if (speedLimits != null && !speedLimits.isEmpty()) {
List<ConfigItem> limiters = gostConfig.getLimiters();
List<Long> limiters_ids = new ArrayList<>();
List<Long> speedLimits_ids = new ArrayList<>();
if (limiters != null){
for (ConfigItem limiter : limiters) {
limiters_ids.add(Long.valueOf(limiter.getName()));
}
}
for (SpeedLimit speedLimit : speedLimits) {
speedLimits_ids.add(speedLimit.getId());
}
List<Long> diff = new ArrayList<>(speedLimits_ids);
diff.removeAll(limiters_ids);
System.out.println(diff);
if (!diff.isEmpty()) {
for (Long speed_id : diff) {
SpeedLimit speedLimit = speedLimitService.getById(speed_id);
if (speedLimit != null) {
SpeedLimitUpdateDto speedLimitUpdateDto = new SpeedLimitUpdateDto();
speedLimitUpdateDto.setId(speed_id);
speedLimitUpdateDto.setName(speedLimit.getName());
speedLimitUpdateDto.setSpeed(speedLimit.getSpeed());
speedLimitUpdateDto.setTunnelId(speedLimit.getTunnelId());
speedLimitUpdateDto.setTunnelName(speedLimit.getTunnelName());
speedLimitService.updateSpeedLimit(speedLimitUpdateDto);
}
}
}
}
}, "同步限流器 ");
}
/**
* 安全执行操作,捕获异常
@@ -210,7 +143,8 @@ public class CheckGostConfigAsync {
/**
* 解析服务名称
*/
private String[] parseServiceName(String serviceName) {
return serviceName.split("_");
private List<String> parseServiceName(String serviceName) {
String[] split = serviceName.split("_");
return new ArrayList<>(Arrays.asList(split));
}
}
@@ -1,14 +1,9 @@
package com.admin.common.task;
import com.admin.common.dto.GostDto;
import com.admin.common.utils.GostUtil;
import com.admin.entity.Forward;
import com.admin.entity.Tunnel;
import com.admin.entity.User;
import com.admin.entity.UserTunnel;
import com.admin.service.ForwardService;
import com.admin.service.TunnelService;
import com.admin.service.UserService;
import com.admin.service.UserTunnelService;
import com.admin.entity.*;
import com.admin.service.*;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import lombok.extern.slf4j.Slf4j;
@@ -38,6 +33,9 @@ public class ResetFlowAsync {
@Resource
TunnelService tunnelService;
@Resource
ChainTunnelService chainTunnelService;
/**
* 每天0点执行流量重置任务
* 查询出用户和隧道的重置流量日期是今天的数据,将上下流量重置为0
@@ -197,12 +195,18 @@ public class ResetFlowAsync {
// 查询对应转发
List<Forward> forwardList = forwardService.list(new QueryWrapper<Forward>().eq("user_id", user.getId()).eq("status", 1));
for (Forward forward : forwardList) {
UserTunnel userTunnel = userTunnelService.getOne(new QueryWrapper<UserTunnel>().eq("user_id", forward.getUserId()).eq("tunnel_id", forward.getTunnelId()));
if (userTunnel != null) {
pauseForwardService(forward, userTunnel.getId());
forward.setStatus(0);
forwardService.updateById(forward);
List<ChainTunnel> chainTunnels = chainTunnelService.list(new QueryWrapper<ChainTunnel>().eq("tunnel_id", forward.getTunnelId()).eq("chain_type", 1));
for (ChainTunnel chainTunnel : chainTunnels) {
UserTunnel userTunnel = userTunnelService.getOne(new QueryWrapper<UserTunnel>().eq("user_id", forward.getUserId()).eq("tunnel_id", forward.getTunnelId()));
if (userTunnel != null) {
String name = buildServiceName(forward.getId(), forward.getUserId(), userTunnel.getId());
GostUtil.PauseAndResumeService(chainTunnel.getNodeId(), name, "PauseService");
}
}
forward.setStatus(0);
forwardService.updateById(forward);
}
user.setStatus(0);
userService.updateById(user);
@@ -217,25 +221,23 @@ public class ResetFlowAsync {
for (UserTunnel userTunnel : user_tunnel_list) {
List<Forward> forwardList = forwardService.list(new QueryWrapper<Forward>().eq("tunnel_id", userTunnel.getTunnelId()).eq("user_id", userTunnel.getUserId()).eq("status", 1));
for (Forward forward : forwardList) {
pauseForwardService(forward, userTunnel.getId());
List<ChainTunnel> chainTunnels = chainTunnelService.list(new QueryWrapper<ChainTunnel>().eq("tunnel_id", forward.getTunnelId()).eq("chain_type", 1));
for (ChainTunnel chainTunnel : chainTunnels) {
String name = buildServiceName(forward.getId(), forward.getUserId(), userTunnel.getId());
GostUtil.PauseAndResumeService(chainTunnel.getNodeId(), name, "PauseService");
}
forward.setStatus(0);
forwardService.updateById(forward);
}
userTunnel.setStatus(0);
userTunnelService.updateById(userTunnel);
}
}
private void pauseForwardService(Forward forward, Integer userTunnelId) {
Tunnel tunnel = tunnelService.getById(forward.getTunnelId());
if (tunnel == null) return;
GostUtil.PauseService(tunnel.getInNodeId(), buildServiceName(forward.getId(), forward.getUserId(), userTunnelId));
if (tunnel.getType() == 2){
GostUtil.PauseRemoteService(tunnel.getOutNodeId(), buildServiceName(forward.getId(), forward.getUserId(), userTunnelId));
}
}
private String buildServiceName(Long forwardId, Integer userId, Integer userTunnelId) {
@@ -42,7 +42,6 @@ public class AESCrypto {
this.secretKey = new SecretKeySpec(keyBytes, ALGORITHM);
this.secureRandom = new SecureRandom();
log.info("AES加密器初始化成功");
} catch (Exception e) {
log.info("AES加密器初始化失败", e);
throw new RuntimeException("AES加密器初始化失败: " + e.getMessage(), e);
@@ -1,13 +1,14 @@
package com.admin.common.utils;
import com.admin.common.dto.GostConfigDto;
import cn.hutool.core.util.StrUtil;
import com.admin.common.dto.GostDto;
import com.admin.entity.Tunnel;
import com.admin.entity.*;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.apache.bcel.generic.RET;
import java.util.List;
import java.util.Map;
import java.util.Objects;
public class GostUtil {
@@ -15,7 +16,11 @@ public class GostUtil {
public static GostDto AddLimiters(Long node_id, Long name, String speed) {
JSONObject data = createLimiterData(name, speed);
return WebSocketServer.send_msg(node_id, data, "AddLimiters");
GostDto gostDto = WebSocketServer.send_msg(node_id, data, "AddLimiters");
if (gostDto.getMsg().contains("exists")){
gostDto.setMsg("OK");
}
return gostDto;
}
public static GostDto UpdateLimiters(Long node_id, Long name, String speed) {
@@ -29,266 +34,177 @@ public class GostUtil {
public static GostDto DeleteLimiters(Long node_id, Long name) {
JSONObject req = new JSONObject();
req.put("limiter", name + "");
return WebSocketServer.send_msg(node_id, req, "DeleteLimiters");
}
public static GostDto AddService(Long node_id, String name, Integer in_port, Integer limiter, String remoteAddr, Integer fow_type, Tunnel tunnel, String strategy, String interfaceName) {
JSONArray services = new JSONArray();
String[] protocols = {"tcp", "udp"};
for (String protocol : protocols) {
JSONObject service = createServiceConfig(name, in_port, limiter, remoteAddr, protocol, fow_type, tunnel, strategy, interfaceName);
services.add(service);
GostDto gostDto = WebSocketServer.send_msg(node_id, req, "DeleteLimiters");
if (gostDto.getMsg().contains("not found")){
gostDto.setMsg("OK");
}
return WebSocketServer.send_msg(node_id, services, "AddService");
return gostDto;
}
public static GostDto UpdateService(Long node_id, String name, Integer in_port, Integer limiter, String remoteAddr, Integer fow_type, Tunnel tunnel, String strategy, String interfaceName) {
JSONArray services = new JSONArray();
String[] protocols = {"tcp", "udp"};
for (String protocol : protocols) {
JSONObject service = createServiceConfig(name, in_port, limiter, remoteAddr, protocol, fow_type, tunnel, strategy, interfaceName);
services.add(service);
}
return WebSocketServer.send_msg(node_id, services, "UpdateService");
}
public static GostDto DeleteService(Long node_id, String name) {
JSONObject data = new JSONObject();
JSONArray services = new JSONArray();
services.add(name + "_tcp");
services.add(name + "_udp");
data.put("services", services);
return WebSocketServer.send_msg(node_id, data, "DeleteService");
}
public static GostDto AddRemoteService(Long node_id, String name, Integer out_port, String remoteAddr, String protocol, String strategy, String interfaceName) {
JSONObject data = new JSONObject();
data.put("name", name + "_tls");
data.put("addr", ":" + out_port);
if (StringUtils.isNotBlank(interfaceName)) {
JSONObject metadata = new JSONObject();
metadata.put("interface", interfaceName);
data.put("metadata", metadata);
}
JSONObject handler = new JSONObject();
handler.put("type", "relay");
data.put("handler", handler);
JSONObject listener = new JSONObject();
listener.put("type", protocol);
data.put("listener", listener);
JSONObject forwarder = new JSONObject();
public static GostDto AddChains(Long node_id, List<ChainTunnel> chainTunnels, Map<Long, Node> node_s) {
JSONArray nodes = new JSONArray();
for (ChainTunnel chainTunnel : chainTunnels) {
JSONObject dialer = new JSONObject();
dialer.put("type", chainTunnel.getProtocol());
String[] split = remoteAddr.split(",");
int num = 1;
for (String addr : split) {
JSONObject connector = new JSONObject();
connector.put("type", "relay");
Node node_info = node_s.get(chainTunnel.getNodeId());
JSONObject node = new JSONObject();
node.put("name", "node_" + num );
node.put("addr", addr);
node.put("name", "node_" + chainTunnel.getInx());
node.put("addr", processServerAddress(node_info.getServerIp() + ":" + chainTunnel.getPort()));
node.put("connector", connector);
node.put("dialer", dialer);
nodes.add(node);
num ++;
}
if (strategy == null || strategy.equals("")){
strategy = "fifo";
}
forwarder.put("nodes", nodes);
JSONObject selector = new JSONObject();
selector.put("strategy", strategy);
selector.put("maxFails", 1);
selector.put("failTimeout", "600s");
forwarder.put("selector", selector);
data.put("forwarder", forwarder);
JSONArray services = new JSONArray();
services.add(data);
return WebSocketServer.send_msg(node_id, services, "AddService");
}
public static GostDto UpdateRemoteService(Long node_id, String name, Integer out_port, String remoteAddr,String protocol, String strategy, String interfaceName) {
JSONObject data = new JSONObject();
data.put("name", name + "_tls");
data.put("addr", ":" + out_port);
if (StringUtils.isNotBlank(interfaceName)) {
JSONObject metadata = new JSONObject();
metadata.put("interface", interfaceName);
data.put("metadata", metadata);
}
JSONObject handler = new JSONObject();
handler.put("type", "relay");
data.put("handler", handler);
JSONObject listener = new JSONObject();
listener.put("type", protocol);
data.put("listener", listener);
JSONObject forwarder = new JSONObject();
JSONArray nodes = new JSONArray();
String[] split = remoteAddr.split(",");
int num = 1;
for (String addr : split) {
JSONObject node = new JSONObject();
node.put("name", "node_" + num );
node.put("addr", addr);
nodes.add(node);
num ++;
}
if (strategy == null || strategy.equals("")){
strategy = "fifo";
}
forwarder.put("nodes", nodes);
JSONObject selector = new JSONObject();
selector.put("strategy", strategy);
selector.put("maxFails", 1);
selector.put("failTimeout", "600s");
forwarder.put("selector", selector);
data.put("forwarder", forwarder);
JSONArray services = new JSONArray();
services.add(data);
return WebSocketServer.send_msg(node_id, services, "UpdateService");
}
public static GostDto DeleteRemoteService(Long node_id, String name) {
JSONArray data = new JSONArray();
data.add(name + "_tls");
JSONObject req = new JSONObject();
req.put("services", data);
return WebSocketServer.send_msg(node_id, req, "DeleteService");
}
public static GostDto PauseService(Long node_id, String name) {
JSONObject data = new JSONObject();
JSONArray services = new JSONArray();
services.add(name + "_tcp");
services.add(name + "_udp");
data.put("services", services);
return WebSocketServer.send_msg(node_id, data, "PauseService");
}
public static GostDto ResumeService(Long node_id, String name) {
JSONObject data = new JSONObject();
JSONArray services = new JSONArray();
services.add(name + "_tcp");
services.add(name + "_udp");
data.put("services", services);
return WebSocketServer.send_msg(node_id, data, "ResumeService");
}
public static GostDto PauseRemoteService(Long node_id, String name) {
JSONObject data = new JSONObject();
JSONArray services = new JSONArray();
services.add(name + "_tls");
data.put("services", services);
return WebSocketServer.send_msg(node_id, data, "PauseService");
}
public static GostDto ResumeRemoteService(Long node_id, String name) {
JSONObject data = new JSONObject();
JSONArray services = new JSONArray();
services.add(name + "_tls");
data.put("services", services);
return WebSocketServer.send_msg(node_id, data, "ResumeService");
}
public static GostDto AddChains(Long node_id, String name, String remoteAddr, String protocol, String interfaceName) {
JSONObject dialer = new JSONObject();
dialer.put("type", protocol);
if (Objects.equals(protocol, "quic")){
JSONObject metadata = new JSONObject();
metadata.put("keepAlive", true);
metadata.put("ttl", "10s");
dialer.put("metadata", metadata);
}
JSONObject connector = new JSONObject();
connector.put("type", "relay");
JSONObject node = new JSONObject();
node.put("name", "node-" + name);
node.put("addr", remoteAddr);
node.put("connector", connector);
node.put("dialer", dialer);
if (StringUtils.isNotBlank(interfaceName)) {
node.put("interface", interfaceName);
}
JSONArray nodes = new JSONArray();
nodes.add(node);
JSONObject hop = new JSONObject();
hop.put("name", "hop-" + name);
hop.put("name", "hop_" + chainTunnels.getFirst().getTunnelId());
// interface设置在转发链
if (StringUtils.isNotBlank(node_s.get(node_id).getInterfaceName())) {
hop.put("interface", node_s.get(node_id).getInterfaceName());
}
JSONObject selector = new JSONObject();
selector.put("strategy", chainTunnels.getFirst().getStrategy());
selector.put("maxFails", 1);
selector.put("failTimeout", 600000000000L); // 600 秒(纳秒单位)
hop.put("selector", selector);
hop.put("nodes", nodes);
JSONArray hops = new JSONArray();
hops.add(hop);
JSONObject data = new JSONObject();
data.put("name", name + "_chains");
data.put("name", "chains_" + chainTunnels.getFirst().getTunnelId());
data.put("hops", hops);
return WebSocketServer.send_msg(node_id, data, "AddChains");
}
public static GostDto UpdateChains(Long node_id, String name, String remoteAddr, String protocol, String interfaceName) {
JSONObject dialer = new JSONObject();
dialer.put("type", protocol);
if (Objects.equals(protocol, "quic")){
JSONObject metadata = new JSONObject();
metadata.put("keepAlive", true);
metadata.put("ttl", "10s");
dialer.put("metadata", metadata);
GostDto gostDto = WebSocketServer.send_msg(node_id, data, "AddChains");
if (gostDto.getMsg().contains("exists")){
gostDto.setMsg("OK");
}
JSONObject connector = new JSONObject();
connector.put("type", "relay");
JSONObject node = new JSONObject();
node.put("name", "node-" + name);
node.put("addr", remoteAddr);
node.put("connector", connector);
node.put("dialer", dialer);
if (StringUtils.isNotBlank(interfaceName)) {
node.put("interface", interfaceName);
}
JSONArray nodes = new JSONArray();
nodes.add(node);
JSONObject hop = new JSONObject();
hop.put("name", "hop-" + name);
hop.put("nodes", nodes);
JSONArray hops = new JSONArray();
hops.add(hop);
JSONObject data = new JSONObject();
data.put("name", name + "_chains");
data.put("hops", hops);
JSONObject req = new JSONObject();
req.put("chain", name + "_chains");
req.put("data", data);
return WebSocketServer.send_msg(node_id, req, "UpdateChains");
return gostDto;
}
public static GostDto DeleteChains(Long node_id, String name) {
JSONObject data = new JSONObject();
data.put("chain", name + "_chains");
return WebSocketServer.send_msg(node_id, data, "DeleteChains");
data.put("chain", name);
GostDto gostDto = WebSocketServer.send_msg(node_id, data, "DeleteChains");
if (gostDto.getMsg().contains("not found")){
gostDto.setMsg("OK");
}
return gostDto;
}
public static GostDto AddChainService(Long node_id, ChainTunnel chainTunnel, Map<Long, Node> node_s) {
JSONArray services = new JSONArray();
Node node_info = node_s.get(chainTunnel.getNodeId());
JSONObject service_item = new JSONObject();
service_item.put("name", chainTunnel.getTunnelId() + "_tls");
service_item.put("addr", node_info.getTcpListenAddr() + ":" + chainTunnel.getPort());
// 只为出口节点(chainType=3)设置 interface
if (chainTunnel.getChainType() == 3 && StringUtils.isNotBlank(node_s.get(node_id).getInterfaceName())) {
JSONObject metadata = new JSONObject();
metadata.put("interface", node_s.get(node_id).getInterfaceName());
service_item.put("metadata", metadata);
}
JSONObject handler = new JSONObject();
handler.put("type", "relay");
if (chainTunnel.getChainType() == 2){
handler.put("chain","chains_" + chainTunnel.getTunnelId());
}
service_item.put("handler", handler);
JSONObject listener = new JSONObject();
listener.put("type", chainTunnel.getProtocol());
service_item.put("listener", listener);
services.add(service_item);
GostDto gostDto = WebSocketServer.send_msg(node_id, services, "AddService");
if (gostDto.getMsg().contains("exists")){
gostDto.setMsg("OK");
}
return gostDto;
}
public static GostDto AddAndUpdateService(String name, Integer limiter, Node node, Forward forward, ForwardPort forwardPort, Tunnel tunnel, String meth) {
JSONArray services = new JSONArray();
String[] protocols = {"tcp", "udp"};
for (String protocol : protocols) {
JSONObject service = new JSONObject();
service.put("name", name + "_" + protocol);
if (Objects.equals(protocol, "tcp")){
service.put("addr", node.getTcpListenAddr() + ":" + forwardPort.getPort());
}else {
service.put("addr", node.getUdpListenAddr() + ":" + forwardPort.getPort());
}
// 只在端口转发时设置 interface(隧道转发时 interface 在转发链的节点上设置)
if (tunnel.getType() == 1 && StringUtils.isNotBlank(node.getInterfaceName())) {
JSONObject metadata = new JSONObject();
metadata.put("interface", node.getInterfaceName());
service.put("metadata", metadata);
}
// 添加限流器配置
if (limiter != null) {
service.put("limiter", limiter.toString());
}
// 配置处理器
JSONObject handler = new JSONObject();
handler.put("type", protocol);
if (tunnel.getType() == 2){
handler.put("chain", "chains_" + forward.getTunnelId());
}
service.put("handler", handler);
// 配置监听器
JSONObject listener = createListener(protocol);
service.put("listener", listener);
JSONObject forwarder = createForwarder(forward.getRemoteAddr(), forward.getStrategy());
service.put("forwarder", forwarder);
services.add(service);
}
GostDto gostDto = WebSocketServer.send_msg(node.getId(), services, meth);
if (gostDto.getMsg().contains("exists")){
gostDto.setMsg("OK");
}
return gostDto;
}
public static GostDto DeleteService(Long node_id, JSONArray services) {
JSONObject data = new JSONObject();
data.put("services", services);
GostDto gostDto = WebSocketServer.send_msg(node_id, data, "DeleteService");
if (gostDto.getMsg().contains("not found")){
gostDto.setMsg("OK");
}
return gostDto;
}
public static GostDto PauseAndResumeService(Long node_id, String name, String meth) {
JSONObject data = new JSONObject();
JSONArray services = new JSONArray();
services.add(name + "_tcp");
services.add(name + "_udp");
data.put("services", services);
return WebSocketServer.send_msg(node_id, data, meth);
}
private static JSONObject createLimiterData(Long name, String speed) {
JSONObject data = new JSONObject();
data.put("name", name.toString());
@@ -298,59 +214,10 @@ public class GostUtil {
return data;
}
private static JSONObject createServiceConfig(String name, Integer in_port, Integer limiter, String remoteAddr, String protocol, Integer fow_type, Tunnel tunnel, String strategy, String interfaceName) {
JSONObject service = new JSONObject();
service.put("name", name + "_" + protocol);
if (Objects.equals(protocol, "tcp")){
service.put("addr", tunnel.getTcpListenAddr() + ":" + in_port);
}else {
service.put("addr", tunnel.getUdpListenAddr() + ":" + in_port);
}
if (StringUtils.isNotBlank(interfaceName)) {
JSONObject metadata = new JSONObject();
metadata.put("interface", interfaceName);
service.put("metadata", metadata);
}
// 添加限流器配置
if (limiter != null) {
service.put("limiter", limiter.toString());
}
// 配置处理器
JSONObject handler = createHandler(protocol, name, fow_type);
service.put("handler", handler);
// 配置监听器
JSONObject listener = createListener(protocol);
service.put("listener", listener);
// 端口转发需要配置转发器
if (isPortForwarding(fow_type)) {
JSONObject forwarder = createForwarder(remoteAddr, strategy);
service.put("forwarder", forwarder);
}
return service;
}
private static JSONObject createHandler(String protocol, String name, Integer fow_type) {
JSONObject handler = new JSONObject();
handler.put("type", protocol);
// 隧道转发需要添加链配置
if (isTunnelForwarding(fow_type)) {
handler.put("chain", name + "_chains");
}
return handler;
}
private static JSONObject createListener(String protocol) {
JSONObject listener = new JSONObject();
listener.put("type", protocol);
if (Objects.equals(protocol, "udp")){
if (Objects.equals(protocol, "udp")) {
JSONObject metadata = new JSONObject();
metadata.put("keepAlive", true);
listener.put("metadata", metadata);
@@ -366,13 +233,13 @@ public class GostUtil {
int num = 1;
for (String addr : split) {
JSONObject node = new JSONObject();
node.put("name", "node_" + num );
node.put("name", "node_" + num);
node.put("addr", addr);
nodes.add(node);
num ++;
num++;
}
if (strategy == null || strategy.equals("")){
if (strategy == null || strategy.isEmpty()) {
strategy = "fifo";
}
@@ -386,12 +253,42 @@ public class GostUtil {
return forwarder;
}
private static boolean isPortForwarding(Integer fow_type) {
return fow_type != null && fow_type == 1;
public static String processServerAddress(String serverAddr) {
if (StrUtil.isBlank(serverAddr)) {
return serverAddr;
}
// 如果已经被方括号包裹,直接返回
if (serverAddr.startsWith("[")) {
return serverAddr;
}
// 查找最后一个冒号,分离主机和端口
int lastColonIndex = serverAddr.lastIndexOf(':');
if (lastColonIndex == -1) {
// 没有端口号,直接检查是否需要包裹
return isIPv6Address(serverAddr) ? "[" + serverAddr + "]" : serverAddr;
}
String host = serverAddr.substring(0, lastColonIndex);
String port = serverAddr.substring(lastColonIndex);
// 检查主机部分是否为IPv6地址
if (isIPv6Address(host)) {
return "[" + host + "]" + port;
}
return serverAddr;
}
private static boolean isTunnelForwarding(Integer fow_type) {
return fow_type != null && fow_type != 1;
}
private static boolean isIPv6Address(String address) {
// IPv6地址包含多个冒号,至少2个
if (!address.contains(":")) {
return false;
}
// 计算冒号数量,IPv6地址至少有2个冒号
long colonCount = address.chars().filter(ch -> ch == ':').count();
return colonCount >= 2;
}
}
@@ -229,6 +229,7 @@ public class WebSocketServer extends TextWebSocketHandler {
String socks = (String) session.getAttributes().get("socks");
log.info("节点 {} 尝试连接,开始处理连接逻辑", nodeId);
log.info("屏蔽协议数据:http->{}, tls->{}, socks->{}", http, tls, socks);
// 检查是否已有该节点的连接,如果有则记录日志但直接覆盖
WebSocketSession existingSession = nodeSessions.get(nodeId);
@@ -1,5 +1,6 @@
package com.admin.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
@@ -15,7 +16,9 @@ public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); // 分页插件
// 配置分页插件,指定数据库类型为 SQLite
PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.SQLITE);
interceptor.addInnerInterceptor(paginationInterceptor); // 分页插件
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); // 防止全表更新插件
return interceptor;
}
@@ -0,0 +1,83 @@
package com.admin.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.PreDestroy;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.Statement;
/**
* SQLite 数据库配置
* 启用 WAL (Write-Ahead Logging) 模式以提高并发性能
* 添加定期 checkpoint 和优雅关闭处理
*/
@Slf4j
@Component
@EnableScheduling
public class SQLiteConfig implements ApplicationRunner {
private final DataSource dataSource;
public SQLiteConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public void run(ApplicationArguments args) throws Exception {
try (Connection connection = dataSource.getConnection();
Statement statement = connection.createStatement()) {
statement.execute("PRAGMA journal_mode=WAL;");
statement.execute("PRAGMA synchronous=NORMAL;");
statement.execute("PRAGMA cache_size=-64000;"); // 64MB 缓存
statement.execute("PRAGMA temp_store=MEMORY;");
statement.execute("PRAGMA busy_timeout=5000;"); // 5秒超时
statement.execute("PRAGMA wal_autocheckpoint=1000;"); // 每1000页自动checkpoint
log.info("SQLite WAL mode configured successfully");
} catch (Exception e) {
log.error("Failed to configure SQLite database", e);
throw e;
}
}
/**
* 定期执行 checkpoint,确保 WAL 文件内容写入主数据库
* 每5分钟执行一次
*/
@Scheduled(fixedDelay = 300000, initialDelay = 300000)
public void performCheckpoint() {
try (Connection connection = dataSource.getConnection();
Statement statement = connection.createStatement()) {
statement.execute("PRAGMA wal_checkpoint(TRUNCATE);");
log.debug("SQLite WAL checkpoint completed");
} catch (Exception e) {
log.error("Failed to perform SQLite checkpoint", e);
}
}
/**
* 应用关闭前执行最终的 checkpoint,确保所有数据都写入主数据库文件
*/
@PreDestroy
public void onShutdown() {
log.info("Performing final SQLite checkpoint before shutdown...");
try (Connection connection = dataSource.getConnection();
Statement statement = connection.createStatement()) {
// 强制执行 checkpoint,将所有 WAL 内容写入主数据库
statement.execute("PRAGMA wal_checkpoint(TRUNCATE);");
log.info("Final SQLite checkpoint completed successfully");
} catch (Exception e) {
log.error("Failed to perform final SQLite checkpoint", e);
}
}
}
@@ -3,15 +3,17 @@ package com.admin.controller;
import com.admin.common.aop.LogAnnotation;
import com.admin.common.dto.FlowDto;
import com.admin.common.dto.GostConfigDto;
import com.admin.common.lang.R;
import com.admin.common.task.CheckGostConfigAsync;
import com.admin.common.utils.AESCrypto;
import com.admin.common.utils.GostUtil;
import com.admin.entity.*;
import com.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;
@@ -21,7 +23,6 @@ import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* 流量上报控制器
@@ -65,6 +66,10 @@ public class FlowController extends BaseController {
@Resource
CheckGostConfigAsync checkGostConfigAsync;
@Resource
@Lazy
ChainTunnelService chainTunnelService;
/**
* 加密消息包装器
*/
@@ -147,15 +152,17 @@ public class FlowController extends BaseController {
String decryptedData = decryptIfNeeded(rawData, secret);
// 3. 解析为FlowDto列表
FlowDto flowDataList = JSONObject.parseObject(decryptedData, FlowDto.class);
if (Objects.equals(flowDataList.getN(), "web_api")) {
return SUCCESS_RESPONSE;
}
// 记录日志
JSONArray flowDataList = JSONObject.parseArray(decryptedData);
log.info("节点上报流量数据{}", flowDataList);
// 4. 处理流量数据
return processFlowData(flowDataList);
for (int i = 0; i < flowDataList.size(); i++) {
String jsonObject = flowDataList.getJSONObject(i).toJSONString();
FlowDto flowDto = JSONObject.parseObject(jsonObject, FlowDto.class);
if (!Objects.equals(flowDto.getN(), "web_api")) {
processFlowData(flowDto);
}
}
return SUCCESS_RESPONSE;
}
/**
@@ -212,24 +219,30 @@ public class FlowController extends BaseController {
/**
* 处理流量数据的核心逻辑
*/
private String processFlowData(FlowDto flowDataList) {
private void processFlowData(FlowDto flowDataList) {
String[] serviceIds = parseServiceName(flowDataList.getN());
String forwardId = serviceIds[0];
String userId = serviceIds[1];
String userTunnelId = serviceIds[2];
Forward forward = forwardService.getById(forwardId);
if (forward != null){
Tunnel tunnel = tunnelService.getById(forward.getTunnelId());
// 获取流量计费类型
int flowType = getFlowType(forward);
// 处理流量倍率及单双向计算
FlowDto flowStats = filterFlowData(flowDataList, forward, flowType);
// 处理流量倍率及单双向计算
BigDecimal trafficRatio = tunnel.getTrafficRatio();
BigDecimal originalD = BigDecimal.valueOf(flowDataList.getD());
BigDecimal originalU = BigDecimal.valueOf(flowDataList.getU());
BigDecimal newD = originalD.multiply(trafficRatio);
BigDecimal newU = originalU.multiply(trafficRatio);
flowDataList.setD(newD.longValue() * tunnel.getFlow());
flowDataList.setU(newU.longValue() * tunnel.getFlow());
}
// 先更新所有流量统计 - 确保流量数据的一致性
updateForwardFlow(forwardId, flowStats);
updateUserFlow(userId, flowStats);
updateUserTunnelFlow(userTunnelId, flowStats);
updateForwardFlow(forwardId, flowDataList);
updateUserFlow(userId, flowDataList);
updateUserTunnelFlow(userTunnelId, flowDataList);
// 7. 检查和服务暂停操作
String name = buildServiceName(forwardId, userId, userTunnelId);
@@ -238,7 +251,6 @@ public class FlowController extends BaseController {
checkUserTunnelRelatedLimits(userTunnelId, name, userId);
}
return SUCCESS_RESPONSE;
}
private void checkUserRelatedLimits(String userId, String name) {
@@ -277,7 +289,7 @@ public class FlowController extends BaseController {
UserTunnel userTunnel = userTunnelService.getById(userTunnelId);
if (userTunnel == null) return;
long flow = userTunnel.getInFlow() + userTunnel.getOutFlow();
if (flow >= userTunnel.getFlow() * BYTES_TO_GB) {
if (flow >= userTunnel.getFlow() * BYTES_TO_GB) {
pauseSpecificForward(userTunnel.getTunnelId(), name, userId);
return;
}
@@ -301,45 +313,15 @@ public class FlowController extends BaseController {
public void pauseService(List<Forward> forwardList, String name) {
for (Forward forward : forwardList) {
Tunnel tunnel = tunnelService.getById(forward.getTunnelId());
if (tunnel != null){
GostUtil.PauseService(tunnel.getInNodeId(), name);
if (tunnel.getType() == 2){
GostUtil.PauseRemoteService(tunnel.getOutNodeId(), name);
}
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 FlowDto filterFlowData(FlowDto flowDto, Forward forward, int flowType) {
if (forward != null) {
Tunnel tunnel = tunnelService.getById(forward.getTunnelId());
if (tunnel != null) {
BigDecimal trafficRatio = tunnel.getTrafficRatio();
BigDecimal originalD = BigDecimal.valueOf(flowDto.getD());
BigDecimal originalU = BigDecimal.valueOf(flowDto.getU());
BigDecimal newD = originalD.multiply(trafficRatio);
BigDecimal newU = originalU.multiply(trafficRatio);
flowDto.setD(newD.longValue() * flowType);
flowDto.setU(newU.longValue() * flowType);
}
}
return flowDto;
}
private int getFlowType(Forward forward) {
int defaultFlowType = 2;
if (forward == null) return defaultFlowType;
Tunnel tunnel = tunnelService.getById(forward.getTunnelId());
if (tunnel == null) return defaultFlowType;
return tunnel.getFlow();
}
private void updateForwardFlow(String forwardId, FlowDto flowStats) {
// 对相同转发的流量更新进行同步,避免并发覆盖
synchronized (getForwardLock(forwardId)) {
@@ -0,0 +1,42 @@
package com.admin.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
@Data
@EqualsAndHashCode(callSuper = false)
public class ChainTunnel implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
// 关联的隧道ID
private Long tunnelId;
// 记录所选节点的类型: 1是入口节点 2是转发链 3是出口节点
private Integer chainType;
// 记录所选节点的ID
private Long nodeId;
// 记录节点使用的端口 入口没有次配置为null
private Integer port;
// 负载模式 入口没有次配置为null
private String strategy;
// 只有转发链有次参数 表示是第几条
private Integer inx;
// 使用的协议 入口没有次配置为null
private String protocol;
}
@@ -1,6 +1,12 @@
package com.admin.entity;
import java.io.Serializable;
import java.util.List;
import com.admin.common.dto.ForwardPortDto;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -14,6 +20,7 @@ import lombok.EqualsAndHashCode;
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName(autoResultMap = true)
public class Forward extends BaseEntity{
private static final long serialVersionUID = 1L;
@@ -26,14 +33,8 @@ public class Forward extends BaseEntity{
private Integer tunnelId;
private Integer inPort;
private Integer outPort;
private String remoteAddr;
private String interfaceName;
private String strategy;
private Long inFlow;
@@ -41,5 +42,4 @@ public class Forward extends BaseEntity{
private Long outFlow;
private Integer inx;
}

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