Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b2974726c | |||
| 43aebdf8f4 | |||
| 37ea338a35 | |||
| 4063d39d6a | |||
| f6dd3ad657 | |||
| 252c16de29 | |||
| 4fef8eeb32 | |||
| 9146eb0375 | |||
| ad974875e4 | |||
| acd00ae413 | |||
| 65ca18d649 | |||
| a27b4d781e | |||
| ba50076468 | |||
| b83f6c0023 | |||
| d0822fc3ef | |||
| f83d4d76be | |||
| 70ff6969e7 | |||
| 374bafec57 | |||
| d9992d433d |
@@ -1,43 +1,59 @@
|
||||
name: Build and Push Vue or Spring Boot Images
|
||||
name: Build and Push Images Based on Version
|
||||
|
||||
# 在这里定义统一版本号
|
||||
env:
|
||||
VERSION: "2.0.6-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 }}
|
||||
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
|
||||
id: check-tag
|
||||
run: |
|
||||
if git rev-parse "${{ env.VERSION }}" >/dev/null 2>&1; then
|
||||
echo "Tag ${{ env.VERSION }} already exists, will only update files"
|
||||
echo "should_build=false" >> $GITHUB_OUTPUT
|
||||
echo "tag_exists=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Tag ${{ env.VERSION }} does not exist, will build all components"
|
||||
echo "should_build=true" >> $GITHUB_OUTPUT
|
||||
echo "tag_exists=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
build-gost:
|
||||
name: Build & Compress GOST Binary
|
||||
needs: check-version
|
||||
if: needs.check-version.outputs.should_build == '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 +64,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 +156,122 @@ 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-files:
|
||||
name: Update Release Files Only
|
||||
needs: check-version
|
||||
if: needs.check-version.outputs.tag_exists == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Update Release Files
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
VERSION="${{ needs.check-version.outputs.version }}"
|
||||
|
||||
echo "📤 更新 Release ${VERSION} 中的脚本和配置文件..."
|
||||
|
||||
# 上传安装脚本(覆盖)
|
||||
echo "📤 更新安装脚本..."
|
||||
gh release upload "${VERSION}" ./install.sh --clobber
|
||||
gh release upload "${VERSION}" ./panel_install.sh --clobber
|
||||
|
||||
# 上传 Docker Compose 配置文件(覆盖)
|
||||
echo "📤 更新 Docker Compose 配置文件..."
|
||||
gh release upload "${VERSION}" ./docker-compose-v4.yml --clobber
|
||||
gh release upload "${VERSION}" ./docker-compose-v6.yml --clobber
|
||||
|
||||
echo "✅ 文件更新完成!Release ${VERSION} 中的脚本和配置已更新"
|
||||
echo "ℹ️ Docker 镜像和 GOST 二进制未重新构建"
|
||||
|
||||
@@ -254,8 +254,7 @@ gitee/
|
||||
# 证书文件
|
||||
*.crt
|
||||
*.cert
|
||||
android-app/
|
||||
ios-app/
|
||||
doraemon.jks
|
||||
flux.ipa
|
||||
device.id
|
||||
device.id
|
||||
commit.sh
|
||||
sql/
|
||||
@@ -1,6 +1,39 @@
|
||||
|
||||
# flux-panel转发面板 哆啦A梦转发面板
|
||||
|
||||
# 赞助商
|
||||
<p align="center">
|
||||
<a href="https://vps.town" style="margin: 0 20px; text-align:center;">
|
||||
<img src="./doc/vpstown.png" width="300">
|
||||
</a>
|
||||
|
||||
<a href="https://whmcs.as211392.com" style="margin: 0 20px; text-align:center;">
|
||||
<img src="./doc/as211392.png" width="300">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
# 1.x无法无损升级到2.x请备份好数据
|
||||
1.x迁移2.x流程如下
|
||||
1.进入1.x的后台在转发页面导出所有转发保存
|
||||
2.使用1.x节点脚本卸载所有节点
|
||||
```
|
||||
curl -L https://github.com/bqlpfy/flux-panel/releases/download/1.4.2/install.sh -o install.sh && chmod +x install.sh && ./install.sh
|
||||
```
|
||||
3.使用1.x面版脚本卸载面板
|
||||
|
||||
```
|
||||
curl -L https://github.com/bqlpfy/flux-panel/releases/download/1.4.2/panel_install.sh -o panel_install.sh && chmod +x panel_install.sh && ./panel_install.sh
|
||||
```
|
||||
4.使用2.x脚本安装面板
|
||||
|
||||
```
|
||||
curl -L https://raw.githubusercontent.com/bqlpfy/flux-panel/refs/heads/main/panel_install.sh -o panel_install.sh && chmod +x panel_install.sh && ./panel_install.sh
|
||||
```
|
||||
|
||||
|
||||
使用1.x版本的请使用上面给出的脚本,不要使用最新的,最新的默认2.x
|
||||
嫌弃麻烦或者想稳定的我不推荐你升级到2.x版本 目前还在beta阶段,很多问题。
|
||||
|
||||
|
||||
本项目基于 [go-gost/gost](https://github.com/go-gost/gost) 和 [go-gost/x](https://github.com/go-gost/x) 两个开源库,实现了转发面板。
|
||||
---
|
||||
@@ -17,10 +50,26 @@
|
||||
## 部署流程
|
||||
---
|
||||
### Docker Compose部署
|
||||
#### 快速部署
|
||||
面板端(1.x):
|
||||
```bash
|
||||
curl -L https://github.com/bqlpfy/flux-panel/releases/download/1.4.2/panel_install.sh -o panel_install.sh && chmod +x panel_install.sh && ./panel_install.sh
|
||||
```
|
||||
节点端(1.x):
|
||||
```bash
|
||||
curl -L https://github.com/bqlpfy/flux-panel/releases/download/1.4.2/install.sh -o install.sh && chmod +x install.sh && ./install.sh
|
||||
|
||||
```
|
||||
|
||||
面板端(2.x):
|
||||
```bash
|
||||
curl -L https://raw.githubusercontent.com/bqlpfy/flux-panel/refs/heads/main/panel_install.sh -o panel_install.sh && chmod +x panel_install.sh && ./panel_install.sh
|
||||
```
|
||||
节点端(2.x):
|
||||
```bash
|
||||
curl -L https://raw.githubusercontent.com/bqlpfy/flux-panel/refs/heads/main/install.sh -o install.sh && chmod +x install.sh && ./install.sh
|
||||
|
||||
```
|
||||
|
||||
#### 默认管理员账号
|
||||
|
||||
@@ -55,7 +104,7 @@ curl -L https://raw.githubusercontent.com/bqlpfy/flux-panel/refs/heads/main/pane
|
||||
请务必在合法、合规、安全的前提下使用本项目。
|
||||
|
||||
---
|
||||
## ⭐ 喝杯咖啡!
|
||||
## ⭐ 喝杯咖啡!(USDT)
|
||||
|
||||
| 网络 | 地址 |
|
||||
|------------|----------------------------------------------------------------------|
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -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'
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
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>
|
||||
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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" "$@"
|
||||
@@ -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
|
||||
@@ -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'
|
||||
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
@@ -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.6-beta
|
||||
container_name: springboot-backend
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
DB_HOST: mysql
|
||||
DB_NAME: ${DB_NAME}
|
||||
DB_USER: ${DB_USER}
|
||||
DB_PASSWORD: ${DB_PASSWORD}
|
||||
DB_PATH: /app/data/gost.db
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
LOG_DIR: /app/logs
|
||||
JAVA_OPTS: "-Xms256m -Xmx512m -Dfile.encoding=UTF-8 -Duser.timezone=Asia/Shanghai"
|
||||
@@ -41,20 +12,20 @@ services:
|
||||
- "${BACKEND_PORT}:6365"
|
||||
volumes:
|
||||
- backend_logs:/app/logs
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
- sqlite_data:/app/data
|
||||
networks:
|
||||
- gost-network
|
||||
stop_grace_period: 30s
|
||||
stop_signal: SIGTERM
|
||||
healthcheck:
|
||||
test: ["CMD", "sh", "-c", "wget --no-verbose --tries=1 --spider http://localhost:6365/flow/test || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 90s
|
||||
start_period: 60s
|
||||
|
||||
frontend:
|
||||
image: bqlpfy/vite-frontend:latest
|
||||
image: bqlpfy/vite-frontend:2.0.6-beta
|
||||
container_name: vite-frontend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
@@ -67,8 +38,8 @@ services:
|
||||
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
name: mysql_data
|
||||
sqlite_data:
|
||||
name: sqlite_data
|
||||
driver: local
|
||||
backend_logs:
|
||||
name: backend_logs
|
||||
|
||||
@@ -1,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.6-beta
|
||||
container_name: springboot-backend
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
DB_HOST: mysql
|
||||
DB_NAME: ${DB_NAME}
|
||||
DB_USER: ${DB_USER}
|
||||
DB_PASSWORD: ${DB_PASSWORD}
|
||||
DB_PATH: /app/data/gost.db
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
LOG_DIR: /app/logs
|
||||
JAVA_OPTS: "-Xms256m -Xmx512m -Dfile.encoding=UTF-8 -Duser.timezone=Asia/Shanghai"
|
||||
@@ -41,20 +12,20 @@ services:
|
||||
- "${BACKEND_PORT}:6365"
|
||||
volumes:
|
||||
- backend_logs:/app/logs
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
- sqlite_data:/app/data
|
||||
networks:
|
||||
- gost-network
|
||||
stop_grace_period: 30s
|
||||
stop_signal: SIGTERM
|
||||
healthcheck:
|
||||
test: ["CMD", "sh", "-c", "wget --no-verbose --tries=1 --spider http://localhost:6365/flow/test || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 90s
|
||||
start_period: 60s
|
||||
|
||||
frontend:
|
||||
image: bqlpfy/vite-frontend:latest
|
||||
image: bqlpfy/vite-frontend:2.0.6-beta
|
||||
container_name: vite-frontend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
@@ -67,8 +38,8 @@ services:
|
||||
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
name: mysql_data
|
||||
sqlite_data:
|
||||
name: sqlite_data
|
||||
driver: local
|
||||
backend_logs:
|
||||
name: backend_logs
|
||||
|
||||
@@ -119,7 +119,7 @@ func main() {
|
||||
log := xlogger.NewLogger()
|
||||
logger.SetDefault(log)
|
||||
|
||||
wsReporter := socket.StartWebSocketReporterWithConfig(config.Addr, config.Secret, config.Http, config.Tls, config.Socks, "1.2.3")
|
||||
wsReporter := socket.StartWebSocketReporterWithConfig(config.Addr, config.Secret, config.Http, config.Tls, config.Socks, "2.0.1")
|
||||
defer wsReporter.Stop()
|
||||
service.SetHTTPReportURL(config.Addr, config.Secret)
|
||||
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GlobalTrafficManager 全局流量管理器(所有服务共享)
|
||||
type GlobalTrafficManager struct {
|
||||
mu sync.RWMutex
|
||||
serviceTraffic map[string]*ServiceTraffic // key: 服务名, value: 流量数据
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
reportTicker *time.Ticker
|
||||
}
|
||||
|
||||
// ServiceTraffic 单个服务的流量累积
|
||||
type ServiceTraffic struct {
|
||||
mu sync.Mutex
|
||||
ServiceName string
|
||||
UpBytes int64 // 上行流量(累积)
|
||||
DownBytes int64 // 下行流量(累积)
|
||||
}
|
||||
|
||||
var (
|
||||
globalManager *GlobalTrafficManager
|
||||
globalManagerOnce sync.Once
|
||||
)
|
||||
|
||||
// GetGlobalTrafficManager 获取全局流量管理器单例
|
||||
func GetGlobalTrafficManager() *GlobalTrafficManager {
|
||||
globalManagerOnce.Do(func() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
globalManager = &GlobalTrafficManager{
|
||||
serviceTraffic: make(map[string]*ServiceTraffic),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
reportTicker: time.NewTicker(5 * time.Second),
|
||||
}
|
||||
// 启动定时上报协程
|
||||
go globalManager.startReporting()
|
||||
})
|
||||
return globalManager
|
||||
}
|
||||
|
||||
// AddTraffic 添加流量到指定服务(由各服务调用)
|
||||
func (m *GlobalTrafficManager) AddTraffic(serviceName string, upBytes, downBytes int64) {
|
||||
if upBytes == 0 && downBytes == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
// 获取或创建服务流量记录
|
||||
traffic, exists := m.serviceTraffic[serviceName]
|
||||
if !exists {
|
||||
traffic = &ServiceTraffic{
|
||||
ServiceName: serviceName,
|
||||
}
|
||||
m.serviceTraffic[serviceName] = traffic
|
||||
}
|
||||
|
||||
// 累加流量
|
||||
traffic.mu.Lock()
|
||||
traffic.UpBytes += upBytes
|
||||
traffic.DownBytes += downBytes
|
||||
traffic.mu.Unlock()
|
||||
}
|
||||
|
||||
// startReporting 启动定时上报协程(每5秒执行一次)
|
||||
func (m *GlobalTrafficManager) startReporting() {
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-m.reportTicker.C:
|
||||
m.collectAndReport()
|
||||
|
||||
case <-m.ctx.Done():
|
||||
fmt.Printf("⏹️ 全局流量上报器已停止\n")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// collectAndReport 收集所有服务流量并合并上报
|
||||
func (m *GlobalTrafficManager) collectAndReport() {
|
||||
m.mu.Lock()
|
||||
|
||||
// 如果没有流量,直接返回
|
||||
if len(m.serviceTraffic) == 0 {
|
||||
m.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// 复制当前所有流量数据(避免长时间持锁)
|
||||
trafficSnapshot := make(map[string]*ServiceTraffic)
|
||||
reportData := make(map[string]struct {
|
||||
up int64
|
||||
down int64
|
||||
})
|
||||
|
||||
for name, traffic := range m.serviceTraffic {
|
||||
traffic.mu.Lock()
|
||||
if traffic.UpBytes > 0 || traffic.DownBytes > 0 {
|
||||
trafficSnapshot[name] = traffic
|
||||
reportData[name] = struct {
|
||||
up int64
|
||||
down int64
|
||||
}{
|
||||
up: traffic.UpBytes,
|
||||
down: traffic.DownBytes,
|
||||
}
|
||||
}
|
||||
traffic.mu.Unlock()
|
||||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
// 如果没有需要上报的流量,返回
|
||||
if len(reportData) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// 构建上报数据数组(保持每个服务独立)
|
||||
reportItems := make([]TrafficReportItem, 0, len(reportData))
|
||||
var totalUp, totalDown int64
|
||||
|
||||
for serviceName, data := range reportData {
|
||||
reportItems = append(reportItems, TrafficReportItem{
|
||||
N: serviceName, // 保持服务名不变
|
||||
U: data.up,
|
||||
D: data.down,
|
||||
})
|
||||
totalUp += data.up
|
||||
totalDown += data.down
|
||||
}
|
||||
|
||||
// 批量发送上报请求(一次HTTP请求包含所有服务)
|
||||
success, err := sendBatchTrafficReport(m.ctx, reportItems)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ 全局流量上报失败: %v (总流量: ↑%d ↓%d, %d个服务)\n", err, totalUp, totalDown, len(reportItems))
|
||||
return
|
||||
}
|
||||
|
||||
if !success {
|
||||
fmt.Printf("⚠️ 全局流量上报未成功 (总流量: ↑%d ↓%d, %d个服务)\n", totalUp, totalDown, len(reportItems))
|
||||
return
|
||||
}
|
||||
|
||||
// 上报成功,清空已上报的流量
|
||||
m.clearReportedTraffic(reportData)
|
||||
}
|
||||
|
||||
// clearReportedTraffic 清空已成功上报的流量
|
||||
func (m *GlobalTrafficManager) clearReportedTraffic(reportedData map[string]struct {
|
||||
up int64
|
||||
down int64
|
||||
}) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
for serviceName, reported := range reportedData {
|
||||
if traffic, exists := m.serviceTraffic[serviceName]; exists {
|
||||
traffic.mu.Lock()
|
||||
// 减去已上报的流量
|
||||
traffic.UpBytes -= reported.up
|
||||
traffic.DownBytes -= reported.down
|
||||
|
||||
// 如果流量归零,从map中删除该服务记录(避免内存泄漏)
|
||||
if traffic.UpBytes <= 0 && traffic.DownBytes <= 0 {
|
||||
traffic.mu.Unlock()
|
||||
delete(m.serviceTraffic, serviceName)
|
||||
} else {
|
||||
traffic.mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop 停止全局流量管理器
|
||||
func (m *GlobalTrafficManager) Stop() {
|
||||
if m.reportTicker != nil {
|
||||
m.reportTicker.Stop()
|
||||
}
|
||||
if m.cancel != nil {
|
||||
m.cancel()
|
||||
}
|
||||
fmt.Printf("🛑 全局流量管理器已停止\n")
|
||||
}
|
||||
|
||||
// GetServiceTraffic 获取指定服务的当前流量(用于调试)
|
||||
func (m *GlobalTrafficManager) GetServiceTraffic(serviceName string) (upBytes, downBytes int64) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
if traffic, exists := m.serviceTraffic[serviceName]; exists {
|
||||
traffic.mu.Lock()
|
||||
upBytes = traffic.UpBytes
|
||||
downBytes = traffic.DownBytes
|
||||
traffic.mu.Unlock()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -403,19 +403,15 @@ func (s *defaultService) observeStats(ctx context.Context) {
|
||||
TotalErrs: st.Get(stats.KindTotalErrs),
|
||||
},
|
||||
}
|
||||
|
||||
// 将流量累积到全局管理器,而不是立即上报
|
||||
if outputBytes > 0 || inputBytes > 0 {
|
||||
reportItems := TrafficReportItem{
|
||||
N: s.name,
|
||||
U: int64(outputBytes),
|
||||
D: int64(inputBytes),
|
||||
}
|
||||
success, err := sendTrafficReport(ctx, reportItems)
|
||||
if err != nil {
|
||||
fmt.Printf("发送流量报告失败: %v", err)
|
||||
} else if success {
|
||||
if xstats, ok := st.(*xstats.Stats); ok {
|
||||
xstats.ResetTraffic(st.Get(stats.KindInputBytes)-inputBytes, st.Get(stats.KindOutputBytes)-outputBytes)
|
||||
}
|
||||
globalManager := GetGlobalTrafficManager()
|
||||
globalManager.AddTraffic(s.name, int64(outputBytes), int64(inputBytes))
|
||||
|
||||
// 立即重置流量计数(因为已经记录到全局管理器中)
|
||||
if xstats, ok := st.(*xstats.Stats); ok {
|
||||
xstats.ResetTraffic(st.Get(stats.KindInputBytes)-inputBytes, st.Get(stats.KindOutputBytes)-outputBytes)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,8 +41,8 @@ func SetHTTPReportURL(addr string, secret string) {
|
||||
}
|
||||
}
|
||||
|
||||
// sendTrafficReport 发送流量报告到HTTP接口
|
||||
func sendTrafficReport(ctx context.Context, reportItems TrafficReportItem) (bool, error) {
|
||||
// sendBatchTrafficReport 批量发送多个服务的流量报告到HTTP接口
|
||||
func sendBatchTrafficReport(ctx context.Context, reportItems []TrafficReportItem) (bool, error) {
|
||||
jsonData, err := json.Marshal(reportItems)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("序列化报告数据失败: %v", err)
|
||||
@@ -112,6 +112,7 @@ func sendTrafficReport(ctx context.Context, reportItems TrafficReportItem) (bool
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// sendConfigReport 发送配置报告到HTTP接口
|
||||
func sendConfigReport(ctx context.Context) (bool, error) {
|
||||
if configReportURL == "" {
|
||||
|
||||
@@ -87,6 +87,9 @@ type TcpPingResponse struct {
|
||||
|
||||
type WebSocketReporter struct {
|
||||
url string
|
||||
addr string // 保存服务器地址
|
||||
secret string // 保存密钥
|
||||
version string // 保存版本号
|
||||
conn *websocket.Conn
|
||||
reconnectTime time.Duration
|
||||
pingInterval time.Duration
|
||||
@@ -195,7 +198,25 @@ func (w *WebSocketReporter) connect() error {
|
||||
w.connecting = false
|
||||
}()
|
||||
|
||||
u, err := url.Parse(w.url)
|
||||
// 重新读取 config.json 获取最新的协议配置
|
||||
type LocalConfig struct {
|
||||
Addr string `json:"addr"`
|
||||
Secret string `json:"secret"`
|
||||
Http int `json:"http"`
|
||||
Tls int `json:"tls"`
|
||||
Socks int `json:"socks"`
|
||||
}
|
||||
|
||||
var cfg LocalConfig
|
||||
if b, err := os.ReadFile("config.json"); err == nil {
|
||||
json.Unmarshal(b, &cfg)
|
||||
}
|
||||
|
||||
// 使用最新的配置重新构建 URL
|
||||
currentURL := "ws://" + w.addr + "/system-info?type=1&secret=" + w.secret + "&version=" + w.version +
|
||||
"&http=" + strconv.Itoa(cfg.Http) + "&tls=" + strconv.Itoa(cfg.Tls) + "&socks=" + strconv.Itoa(cfg.Socks)
|
||||
|
||||
u, err := url.Parse(currentURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("解析URL失败: %v", err)
|
||||
}
|
||||
@@ -225,7 +246,7 @@ func (w *WebSocketReporter) connect() error {
|
||||
return nil
|
||||
})
|
||||
|
||||
fmt.Printf("✅ WebSocket连接建立成功\n")
|
||||
fmt.Printf("✅ WebSocket连接建立成功 (http=%d, tls=%d, socks=%d)\n", cfg.Http, cfg.Tls, cfg.Socks)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1021,12 +1042,16 @@ func getMemoryInfo() MemoryInfo {
|
||||
// StartWebSocketReporterWithConfig 使用配置字段启动WebSocket报告器
|
||||
func StartWebSocketReporterWithConfig(addr string, secret string, http int, tls int, socks int, version string) *WebSocketReporter {
|
||||
|
||||
// 构建包含本机IP的WebSocket URL
|
||||
// 构建初始 WebSocket URL
|
||||
fullURL := "ws://" + addr + "/system-info?type=1&secret=" + secret + "&version=" + version + "&http=" + strconv.Itoa(http) + "&tls=" + strconv.Itoa(tls) + "&socks=" + strconv.Itoa(socks)
|
||||
|
||||
fmt.Printf("🔗 WebSocket连接URL: %s\n", fullURL)
|
||||
|
||||
reporter := NewWebSocketReporter(fullURL, secret)
|
||||
// 保存 addr, secret, version 供重连时使用
|
||||
reporter.addr = addr
|
||||
reporter.secret = secret
|
||||
reporter.version = version
|
||||
reporter.Start()
|
||||
return reporter
|
||||
}
|
||||
|
||||
@@ -1,306 +0,0 @@
|
||||
-- phpMyAdmin SQL Dump
|
||||
-- version 5.2.0
|
||||
-- https://www.phpmyadmin.net/
|
||||
--
|
||||
-- 主机: localhost
|
||||
-- 生成日期: 2025-08-14 21:52:52
|
||||
-- 服务器版本: 5.7.40-log
|
||||
-- PHP 版本: 7.4.33
|
||||
|
||||
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
|
||||
START TRANSACTION;
|
||||
SET time_zone = "+00:00";
|
||||
|
||||
|
||||
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||
/*!40101 SET NAMES utf8mb4 */;
|
||||
|
||||
--
|
||||
-- 数据库: `gost`
|
||||
--
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- 表的结构 `forward`
|
||||
--
|
||||
|
||||
CREATE TABLE `forward` (
|
||||
`id` int(10) NOT NULL,
|
||||
`user_id` int(10) NOT NULL,
|
||||
`user_name` varchar(100) NOT NULL,
|
||||
`name` varchar(100) NOT NULL,
|
||||
`tunnel_id` int(10) NOT NULL,
|
||||
`in_port` int(10) NOT NULL,
|
||||
`out_port` int(10) DEFAULT NULL,
|
||||
`remote_addr` longtext NOT NULL,
|
||||
`strategy` varchar(100) NOT NULL DEFAULT 'fifo',
|
||||
`interface_name` varchar(200) DEFAULT NULL,
|
||||
`in_flow` bigint(20) NOT NULL DEFAULT '0',
|
||||
`out_flow` bigint(20) NOT NULL DEFAULT '0',
|
||||
`created_time` bigint(20) NOT NULL,
|
||||
`updated_time` bigint(20) NOT NULL,
|
||||
`status` int(10) NOT NULL,
|
||||
`inx` int(10) NOT NULL DEFAULT '0'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- 表的结构 `node`
|
||||
--
|
||||
|
||||
CREATE TABLE `node` (
|
||||
`id` int(10) NOT NULL,
|
||||
`name` varchar(100) NOT NULL,
|
||||
`secret` varchar(100) NOT NULL,
|
||||
`ip` longtext,
|
||||
`server_ip` varchar(100) NOT NULL,
|
||||
`port_sta` int(10) NOT NULL,
|
||||
`port_end` int(10) NOT NULL,
|
||||
`version` varchar(100) DEFAULT NULL,
|
||||
`http` int(10) NOT NULL DEFAULT '0',
|
||||
`tls` int(10) NOT NULL DEFAULT '0',
|
||||
`socks` int(10) NOT NULL DEFAULT '0',
|
||||
`created_time` bigint(20) NOT NULL,
|
||||
`updated_time` bigint(20) DEFAULT NULL,
|
||||
`status` int(10) NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- 表的结构 `speed_limit`
|
||||
--
|
||||
|
||||
CREATE TABLE `speed_limit` (
|
||||
`id` int(10) NOT NULL,
|
||||
`name` varchar(100) NOT NULL,
|
||||
`speed` int(10) NOT NULL,
|
||||
`tunnel_id` int(10) NOT NULL,
|
||||
`tunnel_name` varchar(100) NOT NULL,
|
||||
`created_time` bigint(20) NOT NULL,
|
||||
`updated_time` bigint(20) DEFAULT NULL,
|
||||
`status` int(10) NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- 表的结构 `statistics_flow`
|
||||
--
|
||||
|
||||
CREATE TABLE `statistics_flow` (
|
||||
`id` int(10) NOT NULL,
|
||||
`user_id` int(10) NOT NULL,
|
||||
`flow` bigint(20) NOT NULL,
|
||||
`total_flow` bigint(20) NOT NULL,
|
||||
`time` varchar(100) NOT NULL,
|
||||
`created_time` bigint(20) NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- 表的结构 `tunnel`
|
||||
--
|
||||
|
||||
CREATE TABLE `tunnel` (
|
||||
`id` int(10) NOT NULL,
|
||||
`name` varchar(100) NOT NULL,
|
||||
`traffic_ratio` decimal(10,1) NOT NULL DEFAULT '1.0',
|
||||
`in_node_id` int(10) NOT NULL,
|
||||
`in_ip` varchar(100) NOT NULL,
|
||||
`out_node_id` int(10) NOT NULL,
|
||||
`out_ip` varchar(100) NOT NULL,
|
||||
`type` int(10) NOT NULL,
|
||||
`protocol` varchar(10) NOT NULL DEFAULT 'tls',
|
||||
`flow` int(10) NOT NULL,
|
||||
`tcp_listen_addr` varchar(100) NOT NULL DEFAULT '[::]',
|
||||
`udp_listen_addr` varchar(100) NOT NULL DEFAULT '[::]',
|
||||
`interface_name` varchar(200) DEFAULT NULL,
|
||||
`created_time` bigint(20) NOT NULL,
|
||||
`updated_time` bigint(20) NOT NULL,
|
||||
`status` int(10) NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- 表的结构 `user`
|
||||
--
|
||||
|
||||
CREATE TABLE `user` (
|
||||
`id` int(10) NOT NULL,
|
||||
`user` varchar(100) NOT NULL,
|
||||
`pwd` varchar(100) NOT NULL,
|
||||
`role_id` int(10) NOT NULL,
|
||||
`exp_time` bigint(20) NOT NULL,
|
||||
`flow` bigint(20) NOT NULL,
|
||||
`in_flow` bigint(20) NOT NULL DEFAULT '0',
|
||||
`out_flow` bigint(20) NOT NULL DEFAULT '0',
|
||||
`flow_reset_time` bigint(20) NOT NULL,
|
||||
`num` int(10) NOT NULL,
|
||||
`created_time` bigint(20) NOT NULL,
|
||||
`updated_time` bigint(20) DEFAULT NULL,
|
||||
`status` int(10) NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
--
|
||||
-- 转存表中的数据 `user`
|
||||
--
|
||||
|
||||
INSERT INTO `user` (`id`, `user`, `pwd`, `role_id`, `exp_time`, `flow`, `in_flow`, `out_flow`, `flow_reset_time`, `num`, `created_time`, `updated_time`, `status`) VALUES
|
||||
(1, 'admin_user', '3c85cdebade1c51cf64ca9f3c09d182d', 0, 2727251700000, 99999, 0, 0, 1, 99999, 1748914865000, 1754011744252, 1);
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- 表的结构 `user_tunnel`
|
||||
--
|
||||
|
||||
CREATE TABLE `user_tunnel` (
|
||||
`id` int(10) NOT NULL,
|
||||
`user_id` int(10) NOT NULL,
|
||||
`tunnel_id` int(10) NOT NULL,
|
||||
`speed_id` int(10) DEFAULT NULL,
|
||||
`num` int(10) NOT NULL,
|
||||
`flow` bigint(20) NOT NULL,
|
||||
`in_flow` bigint(20) NOT NULL DEFAULT '0',
|
||||
`out_flow` bigint(20) NOT NULL DEFAULT '0',
|
||||
`flow_reset_time` bigint(20) NOT NULL,
|
||||
`exp_time` bigint(20) NOT NULL,
|
||||
`status` int(10) NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- 表的结构 `vite_config`
|
||||
--
|
||||
|
||||
CREATE TABLE `vite_config` (
|
||||
`id` int(10) NOT NULL,
|
||||
`name` varchar(200) NOT NULL,
|
||||
`value` varchar(200) NOT NULL,
|
||||
`time` bigint(20) NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
--
|
||||
-- 转存表中的数据 `vite_config`
|
||||
--
|
||||
|
||||
INSERT INTO `vite_config` (`id`, `name`, `value`, `time`) VALUES
|
||||
(1, 'app_name', 'flux', 1755147963000);
|
||||
|
||||
--
|
||||
-- 转储表的索引
|
||||
--
|
||||
|
||||
--
|
||||
-- 表的索引 `forward`
|
||||
--
|
||||
ALTER TABLE `forward`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- 表的索引 `node`
|
||||
--
|
||||
ALTER TABLE `node`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- 表的索引 `speed_limit`
|
||||
--
|
||||
ALTER TABLE `speed_limit`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- 表的索引 `statistics_flow`
|
||||
--
|
||||
ALTER TABLE `statistics_flow`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- 表的索引 `tunnel`
|
||||
--
|
||||
ALTER TABLE `tunnel`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- 表的索引 `user`
|
||||
--
|
||||
ALTER TABLE `user`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- 表的索引 `user_tunnel`
|
||||
--
|
||||
ALTER TABLE `user_tunnel`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- 表的索引 `vite_config`
|
||||
--
|
||||
ALTER TABLE `vite_config`
|
||||
ADD PRIMARY KEY (`id`),
|
||||
ADD UNIQUE KEY `name` (`name`);
|
||||
|
||||
--
|
||||
-- 在导出的表使用AUTO_INCREMENT
|
||||
--
|
||||
|
||||
--
|
||||
-- 使用表AUTO_INCREMENT `forward`
|
||||
--
|
||||
ALTER TABLE `forward`
|
||||
MODIFY `id` int(10) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1;
|
||||
|
||||
--
|
||||
-- 使用表AUTO_INCREMENT `node`
|
||||
--
|
||||
ALTER TABLE `node`
|
||||
MODIFY `id` int(10) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1;
|
||||
|
||||
--
|
||||
-- 使用表AUTO_INCREMENT `speed_limit`
|
||||
--
|
||||
ALTER TABLE `speed_limit`
|
||||
MODIFY `id` int(10) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1;
|
||||
|
||||
--
|
||||
-- 使用表AUTO_INCREMENT `statistics_flow`
|
||||
--
|
||||
ALTER TABLE `statistics_flow`
|
||||
MODIFY `id` int(10) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1;
|
||||
|
||||
--
|
||||
-- 使用表AUTO_INCREMENT `tunnel`
|
||||
--
|
||||
ALTER TABLE `tunnel`
|
||||
MODIFY `id` int(10) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1;
|
||||
|
||||
--
|
||||
-- 使用表AUTO_INCREMENT `user`
|
||||
--
|
||||
ALTER TABLE `user`
|
||||
MODIFY `id` int(10) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1;
|
||||
|
||||
--
|
||||
-- 使用表AUTO_INCREMENT `user_tunnel`
|
||||
--
|
||||
ALTER TABLE `user_tunnel`
|
||||
MODIFY `id` int(10) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1;
|
||||
|
||||
--
|
||||
-- 使用表AUTO_INCREMENT `vite_config`
|
||||
--
|
||||
ALTER TABLE `vite_config`
|
||||
MODIFY `id` int(10) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1;
|
||||
COMMIT;
|
||||
|
||||
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||
@@ -1,7 +1,30 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 获取系统架构
|
||||
get_architecture() {
|
||||
ARCH=$(uname -m)
|
||||
case $ARCH in
|
||||
x86_64)
|
||||
echo "amd64"
|
||||
;;
|
||||
aarch64|arm64)
|
||||
echo "arm64"
|
||||
;;
|
||||
*)
|
||||
echo "amd64" # 默认使用 amd64
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# 构建下载地址
|
||||
build_download_url() {
|
||||
local ARCH=$(get_architecture)
|
||||
echo "https://github.com/bqlpfy/flux-panel/releases/download/2.0.6-beta/gost-${ARCH}"
|
||||
}
|
||||
|
||||
# 下载地址
|
||||
DOWNLOAD_URL="https://github.com/bqlpfy/flux-panel/releases/download/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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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.6-beta/docker-compose-v4.yml"
|
||||
DOCKER_COMPOSEV6_URL="https://github.com/bqlpfy/flux-panel/releases/download/2.0.6-beta/docker-compose-v6.yml"
|
||||
|
||||
COUNTRY=$(curl -s https://ipinfo.io/country)
|
||||
if [ "$COUNTRY" = "CN" ]; then
|
||||
# 拼接 URL
|
||||
DOCKER_COMPOSEV4_URL="https://ghfast.top/${DOCKER_COMPOSEV4_URL}"
|
||||
DOCKER_COMPOSEV6_URL="https://ghfast.top/${DOCKER_COMPOSEV6_URL}"
|
||||
GOST_SQL_URL="https://ghfast.top/${GOST_SQL_URL}"
|
||||
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
|
||||
|
||||
@@ -16,11 +16,12 @@ ENV LC_ALL=en_US.UTF-8
|
||||
RUN sed -i 's/archive.ubuntu.com/mirrors.ustc.edu.cn/g' /etc/apt/sources.list \
|
||||
&& sed -i 's/security.ubuntu.com/mirrors.ustc.edu.cn/g' /etc/apt/sources.list \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends fontconfig fonts-dejavu \
|
||||
&& apt-get install -y --no-install-recommends fontconfig fonts-dejavu sqlite3 \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=build /app/target/*.jar app.jar
|
||||
|
||||
EXPOSE 6365
|
||||
|
||||
ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar /app/app.jar"]
|
||||
|
||||
@@ -69,10 +69,9 @@
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<version>8.2.0</version>
|
||||
<scope>runtime</scope>
|
||||
<groupId>org.xerial</groupId>
|
||||
<artifactId>sqlite-jdbc</artifactId>
|
||||
<version>3.45.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
package com.admin;
|
||||
|
||||
import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
|
||||
import com.baomidou.mybatisplus.core.toolkit.StringPool;
|
||||
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
|
||||
import com.baomidou.mybatisplus.generator.AutoGenerator;
|
||||
import com.baomidou.mybatisplus.generator.InjectionConfig;
|
||||
import com.baomidou.mybatisplus.generator.config.*;
|
||||
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
|
||||
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
|
||||
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Scanner;
|
||||
|
||||
// 演示例子,执行 main 方法控制台输入模块表名回车自动生成对应项目目录中
|
||||
public class CodeGenerator {
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 读取控制台内容
|
||||
* </p>
|
||||
*/
|
||||
public static String scanner(String tip) {
|
||||
Scanner scanner = new Scanner(System.in);
|
||||
StringBuilder help = new StringBuilder();
|
||||
help.append("请输入" + tip + ":");
|
||||
System.out.println(help.toString());
|
||||
if (scanner.hasNext()) {
|
||||
String ipt = scanner.next();
|
||||
if (StringUtils.isNotBlank(ipt)) {
|
||||
return ipt;
|
||||
}
|
||||
}
|
||||
throw new MybatisPlusException("请输入正确的" + tip + "!");
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
// 代码生成器
|
||||
AutoGenerator mpg = new AutoGenerator();
|
||||
|
||||
// 全局配置
|
||||
GlobalConfig gc = new GlobalConfig();
|
||||
String projectPath = System.getProperty("user.dir");
|
||||
gc.setOutputDir(projectPath + "/src/main/java");
|
||||
gc.setAuthor("QAQ");
|
||||
gc.setOpen(false);
|
||||
// gc.setSwagger2(true); 实体属性 Swagger2 注解
|
||||
gc.setServiceName("%sService");
|
||||
mpg.setGlobalConfig(gc);
|
||||
|
||||
// 数据源配置 - 使用环境变量
|
||||
DataSourceConfig dsc = new DataSourceConfig();
|
||||
String dbHost = System.getenv("DB_HOST");
|
||||
String dbName = System.getenv("DB_NAME");
|
||||
String dbUser = System.getenv("DB_USER");
|
||||
String dbPassword = System.getenv("DB_PASSWORD");
|
||||
|
||||
if (dbHost == null || dbName == null || dbUser == null || dbPassword == null) {
|
||||
throw new MybatisPlusException("请设置数据库环境变量: DB_HOST, DB_NAME, DB_USER, DB_PASSWORD");
|
||||
}
|
||||
|
||||
dsc.setUrl("jdbc:mysql://" + dbHost + "/" + dbName + "?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai");
|
||||
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
|
||||
dsc.setUsername(dbUser);
|
||||
dsc.setPassword(dbPassword);
|
||||
mpg.setDataSource(dsc);
|
||||
|
||||
// 包配置
|
||||
PackageConfig pc = new PackageConfig();
|
||||
// pc.setModuleName(scanner("模块名"));
|
||||
pc.setParent("com.admin");
|
||||
mpg.setPackageInfo(pc);
|
||||
|
||||
// 自定义配置
|
||||
InjectionConfig cfg = new InjectionConfig() {
|
||||
@Override
|
||||
public void initMap() {
|
||||
// to do nothing
|
||||
}
|
||||
};
|
||||
|
||||
// 如果模板引擎是 freemarker
|
||||
String templatePath = "/templates/mapper.xml.ftl";
|
||||
// 如果模板引擎是 velocity
|
||||
// String templatePath = "/templates/mapper.xml.vm";
|
||||
|
||||
// 自定义输出配置
|
||||
List<FileOutConfig> focList = new ArrayList<>();
|
||||
// 自定义配置会被优先输出
|
||||
focList.add(new FileOutConfig(templatePath) {
|
||||
@Override
|
||||
public String outputFile(TableInfo tableInfo) {
|
||||
return projectPath + "/src/main/resources/mapper/" + pc.getModuleName()
|
||||
+ "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
|
||||
}
|
||||
});
|
||||
|
||||
cfg.setFileOutConfigList(focList);
|
||||
mpg.setCfg(cfg);
|
||||
|
||||
// 配置模板
|
||||
TemplateConfig templateConfig = new TemplateConfig();
|
||||
|
||||
|
||||
templateConfig.setXml(null);
|
||||
mpg.setTemplate(templateConfig);
|
||||
|
||||
// 策略配置
|
||||
StrategyConfig strategy = new StrategyConfig();
|
||||
strategy.setNaming(NamingStrategy.underline_to_camel);
|
||||
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
|
||||
strategy.setSuperEntityClass("com.admin.entity.BaseEntity");
|
||||
strategy.setEntityLombokModel(true);
|
||||
strategy.setRestControllerStyle(true);
|
||||
// 公共父类
|
||||
strategy.setSuperControllerClass("com.admin.controller.BaseController");
|
||||
strategy.setSuperEntityColumns("id", "created_time", "updated_time", "status");
|
||||
strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
|
||||
strategy.setControllerMappingHyphenStyle(true);
|
||||
// strategy.setTablePrefix("sys_");//动态调整
|
||||
mpg.setStrategy(strategy);
|
||||
mpg.setTemplateEngine(new FreemarkerTemplateEngine());
|
||||
mpg.execute();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class ChainNodesItems {
|
||||
|
||||
private String mode;
|
||||
|
||||
private List<Integer> nodeIds;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class DiagnosisResult {
|
||||
private Long nodeId;
|
||||
private String nodeName;
|
||||
private String targetIp;
|
||||
private Integer targetPort;
|
||||
private String description;
|
||||
private boolean success;
|
||||
private String message;
|
||||
private double averageTime;
|
||||
private double packetLoss;
|
||||
private long timestamp;
|
||||
private Integer fromChainType;
|
||||
private Integer fromInx;
|
||||
private Integer toChainType;
|
||||
private Integer toInx;
|
||||
}
|
||||
@@ -21,14 +21,6 @@ public class ForwardDto {
|
||||
private String remoteAddr;
|
||||
|
||||
private String strategy;
|
||||
|
||||
/**
|
||||
* 入口端口(可选,为空时自动分配)
|
||||
*/
|
||||
@Min(value = 1, message = "端口号不能小于1")
|
||||
@Max(value = 65535, message = "端口号不能大于65535")
|
||||
|
||||
private Integer inPort;
|
||||
|
||||
private String interfaceName;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ForwardPortDto {
|
||||
private Integer node_id;
|
||||
private Integer port;
|
||||
}
|
||||
@@ -19,22 +19,12 @@ public class ForwardUpdateDto {
|
||||
|
||||
@NotBlank(message = "转发名称不能为空")
|
||||
private String name;
|
||||
|
||||
@NotNull(message = "隧道ID不能为空")
|
||||
private Integer tunnelId;
|
||||
|
||||
|
||||
@NotBlank(message = "远程地址不能为空")
|
||||
private String remoteAddr;
|
||||
|
||||
private String strategy;
|
||||
|
||||
/**
|
||||
* 入口端口(可选,为空时自动分配)
|
||||
*/
|
||||
@Min(value = 1, message = "端口号不能小于1")
|
||||
@Max(value = 65535, message = "端口号不能大于65535")
|
||||
|
||||
private Integer inPort;
|
||||
|
||||
@TableField(updateStrategy = FieldStrategy.IGNORED)
|
||||
private String interfaceName;
|
||||
}
|
||||
@@ -25,6 +25,11 @@ public class ForwardWithTunnelDto {
|
||||
private String name;
|
||||
|
||||
|
||||
/**
|
||||
* 入口IP
|
||||
*/
|
||||
private String inIp;
|
||||
|
||||
/**
|
||||
* 入口端口
|
||||
*/
|
||||
@@ -56,15 +61,10 @@ public class ForwardWithTunnelDto {
|
||||
* 隧道名称
|
||||
*/
|
||||
private String tunnelName;
|
||||
|
||||
/**
|
||||
* 入口IP
|
||||
*/
|
||||
private String inIp;
|
||||
|
||||
|
||||
private String userName;
|
||||
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
@@ -87,6 +87,4 @@ public class ForwardWithTunnelDto {
|
||||
private String strategy;
|
||||
|
||||
private Integer inx;
|
||||
|
||||
private String interfaceName;
|
||||
}
|
||||
@@ -13,19 +13,16 @@ public class NodeDto {
|
||||
@NotBlank(message = "节点名称不能为空")
|
||||
private String name;
|
||||
|
||||
@NotBlank(message = "入口IP不能为空")
|
||||
private String ip;
|
||||
|
||||
@NotBlank(message = "服务器ip不能为空")
|
||||
private String serverIp;
|
||||
|
||||
@NotNull(message = "起始端口不能为空")
|
||||
@Min(value = 1, message = "起始端口必须大于0")
|
||||
@Max(value = 65535, message = "起始端口不能超过65535")
|
||||
private Integer portSta;
|
||||
@NotBlank(message = "可用端口不能为空")
|
||||
private String port;
|
||||
|
||||
private String interfaceName;
|
||||
|
||||
private String tcpListenAddr = "0.0.0.0";
|
||||
|
||||
private String udpListenAddr = "0.0.0.0";
|
||||
|
||||
@NotNull(message = "结束端口不能为空")
|
||||
@Min(value = 1, message = "结束端口必须大于0")
|
||||
@Max(value = 65535, message = "结束端口不能超过65535")
|
||||
private Integer portEnd;
|
||||
}
|
||||
@@ -4,8 +4,6 @@ import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Max;
|
||||
import javax.validation.constraints.Min;
|
||||
|
||||
@Data
|
||||
public class NodeUpdateDto {
|
||||
@@ -16,23 +14,18 @@ public class NodeUpdateDto {
|
||||
@NotBlank(message = "节点名称不能为空")
|
||||
private String name;
|
||||
|
||||
@NotBlank(message = "入口IP不能为空")
|
||||
private String ip;
|
||||
|
||||
@NotBlank(message = "服务器ip不能为空")
|
||||
private String serverIp;
|
||||
|
||||
@NotNull(message = "起始端口不能为空")
|
||||
@Min(value = 1, message = "起始端口必须大于0")
|
||||
@Max(value = 65535, message = "起始端口不能超过65535")
|
||||
private Integer portSta;
|
||||
|
||||
@NotNull(message = "结束端口不能为空")
|
||||
@Min(value = 1, message = "结束端口必须大于0")
|
||||
@Max(value = 65535, message = "结束端口不能超过65535")
|
||||
private Integer portEnd;
|
||||
@NotBlank(message = "可用port不能为空")
|
||||
private String port;
|
||||
|
||||
private String interfaceName;
|
||||
private Integer http;
|
||||
private Integer tls;
|
||||
private Integer socks;
|
||||
|
||||
private String tcpListenAddr = "0.0.0.0";
|
||||
|
||||
private String udpListenAddr = "0.0.0.0";
|
||||
}
|
||||
@@ -17,10 +17,4 @@ public class SpeedLimitUpdateDto {
|
||||
@NotNull(message = "速度限制不能为空")
|
||||
@Min(value = 1, message = "速度限制必须大于0")
|
||||
private Integer speed;
|
||||
|
||||
@NotNull(message = "隧道ID不能为空")
|
||||
private Long tunnelId;
|
||||
|
||||
@NotBlank(message = "隧道名称不能为空")
|
||||
private String tunnelName;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import com.admin.entity.ChainTunnel;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 隧道详情DTO - 包含节点配置信息
|
||||
*/
|
||||
@Data
|
||||
public class TunnelDetailDto {
|
||||
|
||||
private Long id;
|
||||
|
||||
private String name;
|
||||
|
||||
private Integer type; // 1: 端口转发, 2: 隧道转发
|
||||
|
||||
private Integer flow; // 1: 单向, 2: 双向
|
||||
|
||||
private BigDecimal trafficRatio;
|
||||
|
||||
private Integer status;
|
||||
|
||||
private Long createdTime;
|
||||
|
||||
private Long updatedTime;
|
||||
|
||||
private String inIp;
|
||||
|
||||
// 入口节点列表
|
||||
private List<ChainTunnel> inNodeId = new ArrayList<>();
|
||||
|
||||
// 转发链节点列表(二维数组结构)
|
||||
private List<List<ChainTunnel>> chainNodes = new ArrayList<>();
|
||||
|
||||
// 出口节点列表
|
||||
private List<ChainTunnel> outNodeId = new ArrayList<>();
|
||||
}
|
||||
|
||||
@@ -1,45 +1,36 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import com.admin.entity.ChainTunnel;
|
||||
import lombok.Data;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.Max;
|
||||
import javax.validation.constraints.DecimalMin;
|
||||
import javax.validation.constraints.DecimalMax;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class TunnelDto {
|
||||
|
||||
@NotBlank(message = "隧道名称不能为空")
|
||||
private String name;
|
||||
|
||||
@NotNull(message = "入口节点不能为空")
|
||||
private Long inNodeId;
|
||||
|
||||
// 出口节点ID,当type=1时可以为空,会自动设置为入口节点ID
|
||||
private Long outNodeId;
|
||||
|
||||
@NotNull(message = "入口节点不能为空")
|
||||
private List<ChainTunnel> inNodeId;
|
||||
|
||||
private List<List<ChainTunnel>> chainNodes;
|
||||
|
||||
private List<ChainTunnel> outNodeId;
|
||||
|
||||
private String inIp;
|
||||
|
||||
@NotNull(message = "隧道类型不能为空")
|
||||
private Integer type;
|
||||
|
||||
@NotNull(message = "流量计算类型不能为空")
|
||||
private Integer flow;
|
||||
|
||||
// 流量倍率,默认为1.0
|
||||
@DecimalMin(value = "0.0", inclusive = false, message = "流量倍率必须大于0.0")
|
||||
@DecimalMax(value = "100.0", message = "流量倍率不能大于100.0")
|
||||
private BigDecimal trafficRatio;
|
||||
|
||||
private String interfaceName;
|
||||
|
||||
// 协议类型,默认为tls
|
||||
private String protocol;
|
||||
|
||||
// TCP监听地址,默认为0.0.0.0
|
||||
private String tcpListenAddr = "0.0.0.0";
|
||||
|
||||
// UDP监听地址,默认为0.0.0.0
|
||||
private String udpListenAddr = "0.0.0.0";
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class TunnelListDto {
|
||||
|
||||
private Integer id;
|
||||
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 入口IP
|
||||
*/
|
||||
private String ip;
|
||||
|
||||
/**
|
||||
* 入口节点端口起始范围
|
||||
*/
|
||||
private Integer inNodePortSta;
|
||||
|
||||
/**
|
||||
* 入口节点端口结束范围
|
||||
*/
|
||||
private Integer inNodePortEnd;
|
||||
|
||||
/**
|
||||
* 隧道类型(1-端口转发,2-隧道转发)
|
||||
*/
|
||||
private Integer type;
|
||||
|
||||
/**
|
||||
* 协议类型
|
||||
*/
|
||||
private String protocol;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import com.admin.entity.ChainTunnel;
|
||||
import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import lombok.Data;
|
||||
@@ -10,35 +11,23 @@ import javax.validation.constraints.Max;
|
||||
import javax.validation.constraints.DecimalMin;
|
||||
import javax.validation.constraints.DecimalMax;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class TunnelUpdateDto {
|
||||
|
||||
|
||||
@NotNull(message = "隧道ID不能为空")
|
||||
private Long id;
|
||||
|
||||
|
||||
@NotBlank(message = "隧道名称不能为空")
|
||||
private String name;
|
||||
|
||||
|
||||
@NotNull(message = "流量计算类型不能为空")
|
||||
private Integer flow;
|
||||
|
||||
// 流量倍率
|
||||
|
||||
private String inIp;
|
||||
|
||||
@DecimalMin(value = "0.0", inclusive = false, message = "流量倍率必须大于0.0")
|
||||
@DecimalMax(value = "100.0", message = "流量倍率不能大于100.0")
|
||||
private BigDecimal trafficRatio;
|
||||
|
||||
@NotBlank
|
||||
private String protocol;
|
||||
|
||||
// TCP监听地址
|
||||
@NotBlank
|
||||
private String tcpListenAddr;
|
||||
|
||||
// UDP监听地址
|
||||
@NotBlank
|
||||
private String udpListenAddr;
|
||||
|
||||
@TableField(updateStrategy = FieldStrategy.IGNORED)
|
||||
private String interfaceName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class UserInfo {
|
||||
private final Integer userId;
|
||||
private final Integer roleId;
|
||||
private final String userName;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import com.admin.entity.UserTunnel;
|
||||
import com.admin.service.impl.ForwardServiceImpl;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class UserPermissionResult {
|
||||
public boolean hasError;
|
||||
private String errorMessage;
|
||||
private Integer limiter;
|
||||
private UserTunnel userTunnel;
|
||||
|
||||
public static UserPermissionResult success(Integer limiter, UserTunnel userTunnel) {
|
||||
UserPermissionResult result = new UserPermissionResult();
|
||||
result.setLimiter(limiter);
|
||||
result.setUserTunnel(userTunnel);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static UserPermissionResult error(String errorMessage) {
|
||||
UserPermissionResult result = new UserPermissionResult();
|
||||
result.setErrorMessage(errorMessage);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,7 @@ import com.admin.common.lang.R;
|
||||
import com.admin.common.utils.GostUtil;
|
||||
import com.admin.entity.*;
|
||||
import com.admin.service.*;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
@@ -14,6 +13,7 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -33,8 +33,7 @@ public class CheckGostConfigAsync {
|
||||
private SpeedLimitService speedLimitService;
|
||||
|
||||
@Resource
|
||||
@Lazy
|
||||
private TunnelService tunnelService;
|
||||
TunnelService tunnelService;
|
||||
|
||||
|
||||
|
||||
@@ -43,56 +42,51 @@ public class CheckGostConfigAsync {
|
||||
*/
|
||||
@Async
|
||||
public void cleanNodeConfigs(String node_id, GostConfigDto gostConfig) {
|
||||
System.out.println(JSONObject.toJSONString(gostConfig));
|
||||
Node node = nodeService.getById(node_id);
|
||||
if (node != null) {
|
||||
cleanOrphanedServices(gostConfig, node);
|
||||
cleanOrphanedChains(gostConfig, node);
|
||||
cleanOrphanedLimiters(gostConfig, node);
|
||||
cleanOrphanedServices(gostConfig.getServices(), node);
|
||||
cleanOrphanedChains(gostConfig.getChains(), node);
|
||||
cleanOrphanedLimiters(gostConfig.getLimiters(), node);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理孤立的服务
|
||||
*/
|
||||
private void cleanOrphanedServices(GostConfigDto gostConfig, Node node) {
|
||||
if (gostConfig.getServices() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (ConfigItem service : gostConfig.getServices()) {
|
||||
private void cleanOrphanedServices(List<ConfigItem> configItems, Node node) {
|
||||
if (configItems == null) return;
|
||||
for (ConfigItem service : configItems) {
|
||||
safeExecute(() -> {
|
||||
|
||||
if (!Objects.equals(service.getName(), "web_api")){
|
||||
String[] serviceIds = parseServiceName(service.getName());
|
||||
if (serviceIds.length == 4) {
|
||||
String forwardId = serviceIds[0];
|
||||
String userId = serviceIds[1];
|
||||
String userTunnelId = serviceIds[2];
|
||||
String type = serviceIds[3];
|
||||
List<String> serviceIds = parseServiceName(service.getName());
|
||||
|
||||
if (Objects.equals(type, "tcp")) { // 只处理TCP,避免重复处理
|
||||
Forward forward = forwardService.getById(forwardId);
|
||||
if (forward == null) {
|
||||
log.info("删除孤立的服务: {} (节点: {})", service.getName(), node.getId());
|
||||
GostDto gostDto = GostUtil.DeleteService(node.getId(), forwardId + "_" + userId + "_" + userTunnelId);
|
||||
System.out.println(gostDto);
|
||||
}
|
||||
}
|
||||
JSONArray services = new JSONArray();
|
||||
if (Objects.equals(serviceIds.getLast(), "tls")){
|
||||
String forward_id = serviceIds.getFirst();
|
||||
services.add(forward_id + "_tls");
|
||||
|
||||
|
||||
if (Objects.equals(type, "tls")) {
|
||||
Forward forward = forwardService.getById(forwardId);
|
||||
if (forward == null) {
|
||||
log.info("删除孤立的服务: {} (节点: {})", service.getName(), node.getId());
|
||||
GostUtil.DeleteRemoteService(node.getId(), forwardId+"_"+userId+"_"+userTunnelId);
|
||||
}
|
||||
Tunnel tunnel = tunnelService.getById(forward_id);
|
||||
if (tunnel == null) {
|
||||
GostUtil.DeleteService(node.getId(), services);
|
||||
log.info("删除孤立的服务: {} (节点: {})", service.getName(), node.getId());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (Objects.equals(serviceIds.getLast(), "tcp")){
|
||||
String forward_id = serviceIds.getFirst();
|
||||
services.add(forward_id + "_" + serviceIds.get(1) + "_" + serviceIds.get(2) + "_tcp");
|
||||
services.add(forward_id + "_" + serviceIds.get(1) + "_" + serviceIds.get(2) + "_udp");
|
||||
|
||||
Forward forward = forwardService.getById(forward_id);
|
||||
if (forward == null) {
|
||||
GostUtil.DeleteService(node.getId(), services);
|
||||
log.info("删除孤立的服务: {} (节点: {})", service.getName(), node.getId());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}, "清理服务 " + service.getName());
|
||||
}
|
||||
|
||||
@@ -101,28 +95,15 @@ public class CheckGostConfigAsync {
|
||||
/**
|
||||
* 清理孤立的链
|
||||
*/
|
||||
private void cleanOrphanedChains(GostConfigDto gostConfig, Node node) {
|
||||
if (gostConfig.getChains() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
for (ConfigItem chain : gostConfig.getChains()) {
|
||||
private void cleanOrphanedChains(List<ConfigItem> configItems, Node node) {
|
||||
if (configItems == null) return;
|
||||
for (ConfigItem chain : configItems) {
|
||||
safeExecute(() -> {
|
||||
String[] serviceIds = parseServiceName(chain.getName());
|
||||
if (serviceIds.length == 4) {
|
||||
String forwardId = serviceIds[0];
|
||||
String userId = serviceIds[1];
|
||||
String userTunnelId = serviceIds[2];
|
||||
String type = serviceIds[3];
|
||||
|
||||
if (Objects.equals(type, "chains")) {
|
||||
Forward forward = forwardService.getById(forwardId);
|
||||
if (forward == null) {
|
||||
log.info("删除孤立的链: {} (节点: {})", chain.getName(), node.getId());
|
||||
GostUtil.DeleteChains(node.getId(), forwardId+"_"+userId+"_"+userTunnelId);
|
||||
}
|
||||
}
|
||||
List<String> serviceIds = parseServiceName(chain.getName());
|
||||
Tunnel tunnel = tunnelService.getById(serviceIds.getLast());
|
||||
if (tunnel == null) {
|
||||
GostUtil.DeleteChains(node.getId(), chain.getName());
|
||||
log.info("删除孤立的链: {} (节点: {})", chain.getName(), node.getId());
|
||||
}
|
||||
}, "清理链 " + chain.getName());
|
||||
}
|
||||
@@ -131,69 +112,21 @@ public class CheckGostConfigAsync {
|
||||
/**
|
||||
* 清理孤立的限流器
|
||||
*/
|
||||
private void cleanOrphanedLimiters(GostConfigDto gostConfig, Node node) {
|
||||
if (gostConfig.getLimiters() == null) {
|
||||
return;
|
||||
}
|
||||
private void cleanOrphanedLimiters(List<ConfigItem> configItems, Node node) {
|
||||
if (configItems == null) return;
|
||||
|
||||
|
||||
for (ConfigItem limiter : gostConfig.getLimiters()) {
|
||||
for (ConfigItem limiter : configItems) {
|
||||
safeExecute(() -> {
|
||||
SpeedLimit speedLimit = speedLimitService.getById(limiter.getName());
|
||||
if (speedLimit == null) {
|
||||
log.info("删除孤立的限流器: {} (节点: {})", limiter.getName(), node.getId());
|
||||
GostUtil.DeleteLimiters(node.getId(), Long.parseLong(limiter.getName()));
|
||||
log.info("删除孤立的限流器: {} (节点: {})", limiter.getName(), node.getId());
|
||||
}
|
||||
}, "清理限流器 " + limiter.getName());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步限流器
|
||||
*/
|
||||
private void syncLimiters(GostConfigDto gostConfig, Node node) {
|
||||
List<Tunnel> tunnelList = tunnelService.list(new QueryWrapper<Tunnel>().eq("in_node_id", node.getId()));
|
||||
if (tunnelList == null || tunnelList.isEmpty()) return;
|
||||
safeExecute(() -> {
|
||||
StringBuilder tunnelIds = new StringBuilder();
|
||||
for (Tunnel tunnel : tunnelList) {
|
||||
tunnelIds.append(tunnel.getId()).append(",");
|
||||
}
|
||||
String ids = tunnelIds.deleteCharAt(tunnelIds.length() - 1).toString();
|
||||
List<SpeedLimit> speedLimits = speedLimitService.list(new QueryWrapper<SpeedLimit>().in("tunnel_id", ids));
|
||||
if (speedLimits != null && !speedLimits.isEmpty()) {
|
||||
List<ConfigItem> limiters = gostConfig.getLimiters();
|
||||
List<Long> limiters_ids = new ArrayList<>();
|
||||
List<Long> speedLimits_ids = new ArrayList<>();
|
||||
if (limiters != null){
|
||||
for (ConfigItem limiter : limiters) {
|
||||
limiters_ids.add(Long.valueOf(limiter.getName()));
|
||||
}
|
||||
}
|
||||
for (SpeedLimit speedLimit : speedLimits) {
|
||||
speedLimits_ids.add(speedLimit.getId());
|
||||
}
|
||||
List<Long> diff = new ArrayList<>(speedLimits_ids);
|
||||
diff.removeAll(limiters_ids);
|
||||
System.out.println(diff);
|
||||
if (!diff.isEmpty()) {
|
||||
|
||||
for (Long speed_id : diff) {
|
||||
SpeedLimit speedLimit = speedLimitService.getById(speed_id);
|
||||
if (speedLimit != null) {
|
||||
SpeedLimitUpdateDto speedLimitUpdateDto = new SpeedLimitUpdateDto();
|
||||
speedLimitUpdateDto.setId(speed_id);
|
||||
speedLimitUpdateDto.setName(speedLimit.getName());
|
||||
speedLimitUpdateDto.setSpeed(speedLimit.getSpeed());
|
||||
speedLimitUpdateDto.setTunnelId(speedLimit.getTunnelId());
|
||||
speedLimitUpdateDto.setTunnelName(speedLimit.getTunnelName());
|
||||
speedLimitService.updateSpeedLimit(speedLimitUpdateDto);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, "同步限流器 ");
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全执行操作,捕获异常
|
||||
@@ -210,7 +143,8 @@ public class CheckGostConfigAsync {
|
||||
/**
|
||||
* 解析服务名称
|
||||
*/
|
||||
private String[] parseServiceName(String serviceName) {
|
||||
return serviceName.split("_");
|
||||
private List<String> parseServiceName(String serviceName) {
|
||||
String[] split = serviceName.split("_");
|
||||
return new ArrayList<>(Arrays.asList(split));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
package com.admin.common.task;
|
||||
|
||||
import com.admin.common.dto.GostDto;
|
||||
import com.admin.common.utils.GostUtil;
|
||||
import com.admin.entity.Forward;
|
||||
import com.admin.entity.Tunnel;
|
||||
import com.admin.entity.User;
|
||||
import com.admin.entity.UserTunnel;
|
||||
import com.admin.service.ForwardService;
|
||||
import com.admin.service.TunnelService;
|
||||
import com.admin.service.UserService;
|
||||
import com.admin.service.UserTunnelService;
|
||||
import com.admin.entity.*;
|
||||
import com.admin.service.*;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -38,6 +33,9 @@ public class ResetFlowAsync {
|
||||
@Resource
|
||||
TunnelService tunnelService;
|
||||
|
||||
@Resource
|
||||
ChainTunnelService chainTunnelService;
|
||||
|
||||
/**
|
||||
* 每天0点执行流量重置任务
|
||||
* 查询出用户和隧道的重置流量日期是今天的数据,将上下流量重置为0
|
||||
@@ -197,12 +195,18 @@ public class ResetFlowAsync {
|
||||
// 查询对应转发
|
||||
List<Forward> forwardList = forwardService.list(new QueryWrapper<Forward>().eq("user_id", user.getId()).eq("status", 1));
|
||||
for (Forward forward : forwardList) {
|
||||
UserTunnel userTunnel = userTunnelService.getOne(new QueryWrapper<UserTunnel>().eq("user_id", forward.getUserId()).eq("tunnel_id", forward.getTunnelId()));
|
||||
if (userTunnel != null) {
|
||||
pauseForwardService(forward, userTunnel.getId());
|
||||
forward.setStatus(0);
|
||||
forwardService.updateById(forward);
|
||||
|
||||
List<ChainTunnel> chainTunnels = chainTunnelService.list(new QueryWrapper<ChainTunnel>().eq("tunnel_id", forward.getTunnelId()).eq("chain_type", 1));
|
||||
for (ChainTunnel chainTunnel : chainTunnels) {
|
||||
UserTunnel userTunnel = userTunnelService.getOne(new QueryWrapper<UserTunnel>().eq("user_id", forward.getUserId()).eq("tunnel_id", forward.getTunnelId()));
|
||||
if (userTunnel != null) {
|
||||
String name = buildServiceName(forward.getId(), forward.getUserId(), userTunnel.getId());
|
||||
GostUtil.PauseAndResumeService(chainTunnel.getNodeId(), name, "PauseService");
|
||||
}
|
||||
}
|
||||
|
||||
forward.setStatus(0);
|
||||
forwardService.updateById(forward);
|
||||
}
|
||||
user.setStatus(0);
|
||||
userService.updateById(user);
|
||||
@@ -217,25 +221,23 @@ public class ResetFlowAsync {
|
||||
for (UserTunnel userTunnel : user_tunnel_list) {
|
||||
List<Forward> forwardList = forwardService.list(new QueryWrapper<Forward>().eq("tunnel_id", userTunnel.getTunnelId()).eq("user_id", userTunnel.getUserId()).eq("status", 1));
|
||||
for (Forward forward : forwardList) {
|
||||
pauseForwardService(forward, userTunnel.getId());
|
||||
|
||||
List<ChainTunnel> chainTunnels = chainTunnelService.list(new QueryWrapper<ChainTunnel>().eq("tunnel_id", forward.getTunnelId()).eq("chain_type", 1));
|
||||
for (ChainTunnel chainTunnel : chainTunnels) {
|
||||
String name = buildServiceName(forward.getId(), forward.getUserId(), userTunnel.getId());
|
||||
GostUtil.PauseAndResumeService(chainTunnel.getNodeId(), name, "PauseService");
|
||||
}
|
||||
forward.setStatus(0);
|
||||
forwardService.updateById(forward);
|
||||
}
|
||||
|
||||
|
||||
userTunnel.setStatus(0);
|
||||
userTunnelService.updateById(userTunnel);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void pauseForwardService(Forward forward, Integer userTunnelId) {
|
||||
Tunnel tunnel = tunnelService.getById(forward.getTunnelId());
|
||||
if (tunnel == null) return;
|
||||
|
||||
GostUtil.PauseService(tunnel.getInNodeId(), buildServiceName(forward.getId(), forward.getUserId(), userTunnelId));
|
||||
if (tunnel.getType() == 2){
|
||||
GostUtil.PauseRemoteService(tunnel.getOutNodeId(), buildServiceName(forward.getId(), forward.getUserId(), userTunnelId));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private String buildServiceName(Long forwardId, Integer userId, Integer userTunnelId) {
|
||||
|
||||
@@ -42,7 +42,6 @@ public class AESCrypto {
|
||||
this.secretKey = new SecretKeySpec(keyBytes, ALGORITHM);
|
||||
this.secureRandom = new SecureRandom();
|
||||
|
||||
log.info("AES加密器初始化成功");
|
||||
} catch (Exception e) {
|
||||
log.info("AES加密器初始化失败", e);
|
||||
throw new RuntimeException("AES加密器初始化失败: " + e.getMessage(), e);
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
package com.admin.common.utils;
|
||||
|
||||
import com.admin.common.dto.GostConfigDto;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.admin.common.dto.GostDto;
|
||||
import com.admin.entity.Tunnel;
|
||||
import com.admin.entity.*;
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.aspectj.apache.bcel.generic.RET;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
public class GostUtil {
|
||||
@@ -15,7 +16,11 @@ public class GostUtil {
|
||||
|
||||
public static GostDto AddLimiters(Long node_id, Long name, String speed) {
|
||||
JSONObject data = createLimiterData(name, speed);
|
||||
return WebSocketServer.send_msg(node_id, data, "AddLimiters");
|
||||
GostDto gostDto = WebSocketServer.send_msg(node_id, data, "AddLimiters");
|
||||
if (gostDto.getMsg().contains("exists")){
|
||||
gostDto.setMsg("OK");
|
||||
}
|
||||
return gostDto;
|
||||
}
|
||||
|
||||
public static GostDto UpdateLimiters(Long node_id, Long name, String speed) {
|
||||
@@ -29,266 +34,177 @@ public class GostUtil {
|
||||
public static GostDto DeleteLimiters(Long node_id, Long name) {
|
||||
JSONObject req = new JSONObject();
|
||||
req.put("limiter", name + "");
|
||||
return WebSocketServer.send_msg(node_id, req, "DeleteLimiters");
|
||||
}
|
||||
|
||||
public static GostDto AddService(Long node_id, String name, Integer in_port, Integer limiter, String remoteAddr, Integer fow_type, Tunnel tunnel, String strategy, String interfaceName) {
|
||||
JSONArray services = new JSONArray();
|
||||
String[] protocols = {"tcp", "udp"};
|
||||
for (String protocol : protocols) {
|
||||
JSONObject service = createServiceConfig(name, in_port, limiter, remoteAddr, protocol, fow_type, tunnel, strategy, interfaceName);
|
||||
services.add(service);
|
||||
GostDto gostDto = WebSocketServer.send_msg(node_id, req, "DeleteLimiters");
|
||||
if (gostDto.getMsg().contains("not found")){
|
||||
gostDto.setMsg("OK");
|
||||
}
|
||||
return WebSocketServer.send_msg(node_id, services, "AddService");
|
||||
return gostDto;
|
||||
}
|
||||
|
||||
public static GostDto UpdateService(Long node_id, String name, Integer in_port, Integer limiter, String remoteAddr, Integer fow_type, Tunnel tunnel, String strategy, String interfaceName) {
|
||||
JSONArray services = new JSONArray();
|
||||
String[] protocols = {"tcp", "udp"};
|
||||
for (String protocol : protocols) {
|
||||
JSONObject service = createServiceConfig(name, in_port, limiter, remoteAddr, protocol, fow_type, tunnel, strategy, interfaceName);
|
||||
services.add(service);
|
||||
}
|
||||
return WebSocketServer.send_msg(node_id, services, "UpdateService");
|
||||
}
|
||||
|
||||
public static GostDto DeleteService(Long node_id, String name) {
|
||||
JSONObject data = new JSONObject();
|
||||
JSONArray services = new JSONArray();
|
||||
services.add(name + "_tcp");
|
||||
services.add(name + "_udp");
|
||||
data.put("services", services);
|
||||
return WebSocketServer.send_msg(node_id, data, "DeleteService");
|
||||
}
|
||||
|
||||
public static GostDto AddRemoteService(Long node_id, String name, Integer out_port, String remoteAddr, String protocol, String strategy, String interfaceName) {
|
||||
JSONObject data = new JSONObject();
|
||||
data.put("name", name + "_tls");
|
||||
data.put("addr", ":" + out_port);
|
||||
|
||||
if (StringUtils.isNotBlank(interfaceName)) {
|
||||
JSONObject metadata = new JSONObject();
|
||||
metadata.put("interface", interfaceName);
|
||||
data.put("metadata", metadata);
|
||||
}
|
||||
|
||||
|
||||
JSONObject handler = new JSONObject();
|
||||
handler.put("type", "relay");
|
||||
data.put("handler", handler);
|
||||
JSONObject listener = new JSONObject();
|
||||
listener.put("type", protocol);
|
||||
data.put("listener", listener);
|
||||
JSONObject forwarder = new JSONObject();
|
||||
public static GostDto AddChains(Long node_id, List<ChainTunnel> chainTunnels, Map<Long, Node> node_s) {
|
||||
JSONArray nodes = new JSONArray();
|
||||
for (ChainTunnel chainTunnel : chainTunnels) {
|
||||
JSONObject dialer = new JSONObject();
|
||||
dialer.put("type", chainTunnel.getProtocol());
|
||||
|
||||
String[] split = remoteAddr.split(",");
|
||||
int num = 1;
|
||||
for (String addr : split) {
|
||||
JSONObject connector = new JSONObject();
|
||||
connector.put("type", "relay");
|
||||
|
||||
Node node_info = node_s.get(chainTunnel.getNodeId());
|
||||
JSONObject node = new JSONObject();
|
||||
node.put("name", "node_" + num );
|
||||
node.put("addr", addr);
|
||||
node.put("name", "node_" + chainTunnel.getInx());
|
||||
node.put("addr", processServerAddress(node_info.getServerIp() + ":" + chainTunnel.getPort()));
|
||||
node.put("connector", connector);
|
||||
node.put("dialer", dialer);
|
||||
|
||||
|
||||
|
||||
nodes.add(node);
|
||||
num ++;
|
||||
}
|
||||
if (strategy == null || strategy.equals("")){
|
||||
strategy = "fifo";
|
||||
}
|
||||
forwarder.put("nodes", nodes);
|
||||
JSONObject selector = new JSONObject();
|
||||
selector.put("strategy", strategy);
|
||||
selector.put("maxFails", 1);
|
||||
selector.put("failTimeout", "600s");
|
||||
forwarder.put("selector", selector);
|
||||
|
||||
data.put("forwarder", forwarder);
|
||||
JSONArray services = new JSONArray();
|
||||
services.add(data);
|
||||
return WebSocketServer.send_msg(node_id, services, "AddService");
|
||||
}
|
||||
|
||||
public static GostDto UpdateRemoteService(Long node_id, String name, Integer out_port, String remoteAddr,String protocol, String strategy, String interfaceName) {
|
||||
JSONObject data = new JSONObject();
|
||||
data.put("name", name + "_tls");
|
||||
data.put("addr", ":" + out_port);
|
||||
|
||||
if (StringUtils.isNotBlank(interfaceName)) {
|
||||
JSONObject metadata = new JSONObject();
|
||||
metadata.put("interface", interfaceName);
|
||||
data.put("metadata", metadata);
|
||||
}
|
||||
|
||||
|
||||
JSONObject handler = new JSONObject();
|
||||
handler.put("type", "relay");
|
||||
data.put("handler", handler);
|
||||
JSONObject listener = new JSONObject();
|
||||
listener.put("type", protocol);
|
||||
data.put("listener", listener);
|
||||
JSONObject forwarder = new JSONObject();
|
||||
JSONArray nodes = new JSONArray();
|
||||
|
||||
String[] split = remoteAddr.split(",");
|
||||
int num = 1;
|
||||
for (String addr : split) {
|
||||
JSONObject node = new JSONObject();
|
||||
node.put("name", "node_" + num );
|
||||
node.put("addr", addr);
|
||||
nodes.add(node);
|
||||
num ++;
|
||||
}
|
||||
if (strategy == null || strategy.equals("")){
|
||||
strategy = "fifo";
|
||||
}
|
||||
forwarder.put("nodes", nodes);
|
||||
JSONObject selector = new JSONObject();
|
||||
selector.put("strategy", strategy);
|
||||
selector.put("maxFails", 1);
|
||||
selector.put("failTimeout", "600s");
|
||||
forwarder.put("selector", selector);
|
||||
|
||||
data.put("forwarder", forwarder);
|
||||
JSONArray services = new JSONArray();
|
||||
services.add(data);
|
||||
return WebSocketServer.send_msg(node_id, services, "UpdateService");
|
||||
}
|
||||
|
||||
public static GostDto DeleteRemoteService(Long node_id, String name) {
|
||||
JSONArray data = new JSONArray();
|
||||
data.add(name + "_tls");
|
||||
JSONObject req = new JSONObject();
|
||||
req.put("services", data);
|
||||
return WebSocketServer.send_msg(node_id, req, "DeleteService");
|
||||
}
|
||||
|
||||
public static GostDto PauseService(Long node_id, String name) {
|
||||
JSONObject data = new JSONObject();
|
||||
JSONArray services = new JSONArray();
|
||||
services.add(name + "_tcp");
|
||||
services.add(name + "_udp");
|
||||
data.put("services", services);
|
||||
return WebSocketServer.send_msg(node_id, data, "PauseService");
|
||||
}
|
||||
|
||||
public static GostDto ResumeService(Long node_id, String name) {
|
||||
JSONObject data = new JSONObject();
|
||||
JSONArray services = new JSONArray();
|
||||
services.add(name + "_tcp");
|
||||
services.add(name + "_udp");
|
||||
data.put("services", services);
|
||||
return WebSocketServer.send_msg(node_id, data, "ResumeService");
|
||||
}
|
||||
|
||||
public static GostDto PauseRemoteService(Long node_id, String name) {
|
||||
JSONObject data = new JSONObject();
|
||||
JSONArray services = new JSONArray();
|
||||
services.add(name + "_tls");
|
||||
data.put("services", services);
|
||||
return WebSocketServer.send_msg(node_id, data, "PauseService");
|
||||
}
|
||||
|
||||
public static GostDto ResumeRemoteService(Long node_id, String name) {
|
||||
JSONObject data = new JSONObject();
|
||||
JSONArray services = new JSONArray();
|
||||
services.add(name + "_tls");
|
||||
data.put("services", services);
|
||||
return WebSocketServer.send_msg(node_id, data, "ResumeService");
|
||||
}
|
||||
|
||||
public static GostDto AddChains(Long node_id, String name, String remoteAddr, String protocol, String interfaceName) {
|
||||
JSONObject dialer = new JSONObject();
|
||||
dialer.put("type", protocol);
|
||||
if (Objects.equals(protocol, "quic")){
|
||||
JSONObject metadata = new JSONObject();
|
||||
metadata.put("keepAlive", true);
|
||||
metadata.put("ttl", "10s");
|
||||
dialer.put("metadata", metadata);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
JSONObject connector = new JSONObject();
|
||||
connector.put("type", "relay");
|
||||
|
||||
JSONObject node = new JSONObject();
|
||||
node.put("name", "node-" + name);
|
||||
node.put("addr", remoteAddr);
|
||||
node.put("connector", connector);
|
||||
node.put("dialer", dialer);
|
||||
|
||||
if (StringUtils.isNotBlank(interfaceName)) {
|
||||
node.put("interface", interfaceName);
|
||||
}
|
||||
|
||||
|
||||
JSONArray nodes = new JSONArray();
|
||||
nodes.add(node);
|
||||
|
||||
JSONObject hop = new JSONObject();
|
||||
hop.put("name", "hop-" + name);
|
||||
hop.put("name", "hop_" + chainTunnels.getFirst().getTunnelId());
|
||||
|
||||
// interface设置在转发链
|
||||
if (StringUtils.isNotBlank(node_s.get(node_id).getInterfaceName())) {
|
||||
hop.put("interface", node_s.get(node_id).getInterfaceName());
|
||||
}
|
||||
|
||||
|
||||
JSONObject selector = new JSONObject();
|
||||
selector.put("strategy", chainTunnels.getFirst().getStrategy());
|
||||
selector.put("maxFails", 1);
|
||||
selector.put("failTimeout", 600000000000L); // 600 秒(纳秒单位)
|
||||
|
||||
|
||||
hop.put("selector", selector);
|
||||
hop.put("nodes", nodes);
|
||||
|
||||
JSONArray hops = new JSONArray();
|
||||
hops.add(hop);
|
||||
|
||||
JSONObject data = new JSONObject();
|
||||
data.put("name", name + "_chains");
|
||||
data.put("name", "chains_" + chainTunnels.getFirst().getTunnelId());
|
||||
data.put("hops", hops);
|
||||
|
||||
return WebSocketServer.send_msg(node_id, data, "AddChains");
|
||||
}
|
||||
|
||||
public static GostDto UpdateChains(Long node_id, String name, String remoteAddr, String protocol, String interfaceName) {
|
||||
JSONObject dialer = new JSONObject();
|
||||
dialer.put("type", protocol);
|
||||
|
||||
if (Objects.equals(protocol, "quic")){
|
||||
JSONObject metadata = new JSONObject();
|
||||
metadata.put("keepAlive", true);
|
||||
metadata.put("ttl", "10s");
|
||||
dialer.put("metadata", metadata);
|
||||
GostDto gostDto = WebSocketServer.send_msg(node_id, data, "AddChains");
|
||||
if (gostDto.getMsg().contains("exists")){
|
||||
gostDto.setMsg("OK");
|
||||
}
|
||||
|
||||
|
||||
JSONObject connector = new JSONObject();
|
||||
connector.put("type", "relay");
|
||||
|
||||
JSONObject node = new JSONObject();
|
||||
node.put("name", "node-" + name);
|
||||
node.put("addr", remoteAddr);
|
||||
node.put("connector", connector);
|
||||
node.put("dialer", dialer);
|
||||
|
||||
if (StringUtils.isNotBlank(interfaceName)) {
|
||||
node.put("interface", interfaceName);
|
||||
}
|
||||
|
||||
JSONArray nodes = new JSONArray();
|
||||
nodes.add(node);
|
||||
|
||||
JSONObject hop = new JSONObject();
|
||||
hop.put("name", "hop-" + name);
|
||||
hop.put("nodes", nodes);
|
||||
|
||||
JSONArray hops = new JSONArray();
|
||||
hops.add(hop);
|
||||
|
||||
JSONObject data = new JSONObject();
|
||||
data.put("name", name + "_chains");
|
||||
data.put("hops", hops);
|
||||
JSONObject req = new JSONObject();
|
||||
req.put("chain", name + "_chains");
|
||||
req.put("data", data);
|
||||
return WebSocketServer.send_msg(node_id, req, "UpdateChains");
|
||||
return gostDto;
|
||||
}
|
||||
|
||||
public static GostDto DeleteChains(Long node_id, String name) {
|
||||
JSONObject data = new JSONObject();
|
||||
data.put("chain", name + "_chains");
|
||||
return WebSocketServer.send_msg(node_id, data, "DeleteChains");
|
||||
data.put("chain", name);
|
||||
GostDto gostDto = WebSocketServer.send_msg(node_id, data, "DeleteChains");
|
||||
if (gostDto.getMsg().contains("not found")){
|
||||
gostDto.setMsg("OK");
|
||||
}
|
||||
return gostDto;
|
||||
}
|
||||
|
||||
public static GostDto AddChainService(Long node_id, ChainTunnel chainTunnel, Map<Long, Node> node_s) {
|
||||
JSONArray services = new JSONArray();
|
||||
Node node_info = node_s.get(chainTunnel.getNodeId());
|
||||
JSONObject service_item = new JSONObject();
|
||||
service_item.put("name", chainTunnel.getTunnelId() + "_tls");
|
||||
service_item.put("addr", node_info.getTcpListenAddr() + ":" + chainTunnel.getPort());
|
||||
|
||||
// 只为出口节点(chainType=3)设置 interface
|
||||
if (chainTunnel.getChainType() == 3 && StringUtils.isNotBlank(node_s.get(node_id).getInterfaceName())) {
|
||||
JSONObject metadata = new JSONObject();
|
||||
metadata.put("interface", node_s.get(node_id).getInterfaceName());
|
||||
service_item.put("metadata", metadata);
|
||||
}
|
||||
|
||||
JSONObject handler = new JSONObject();
|
||||
handler.put("type", "relay");
|
||||
if (chainTunnel.getChainType() == 2){
|
||||
handler.put("chain","chains_" + chainTunnel.getTunnelId());
|
||||
}
|
||||
service_item.put("handler", handler);
|
||||
|
||||
JSONObject listener = new JSONObject();
|
||||
listener.put("type", chainTunnel.getProtocol());
|
||||
service_item.put("listener", listener);
|
||||
|
||||
services.add(service_item);
|
||||
|
||||
GostDto gostDto = WebSocketServer.send_msg(node_id, services, "AddService");
|
||||
if (gostDto.getMsg().contains("exists")){
|
||||
gostDto.setMsg("OK");
|
||||
}
|
||||
return gostDto;
|
||||
}
|
||||
|
||||
public static GostDto AddAndUpdateService(String name, Integer limiter, Node node, Forward forward, ForwardPort forwardPort, Tunnel tunnel, String meth) {
|
||||
JSONArray services = new JSONArray();
|
||||
String[] protocols = {"tcp", "udp"};
|
||||
for (String protocol : protocols) {
|
||||
JSONObject service = new JSONObject();
|
||||
service.put("name", name + "_" + protocol);
|
||||
if (Objects.equals(protocol, "tcp")){
|
||||
service.put("addr", node.getTcpListenAddr() + ":" + forwardPort.getPort());
|
||||
}else {
|
||||
service.put("addr", node.getUdpListenAddr() + ":" + forwardPort.getPort());
|
||||
}
|
||||
|
||||
// 只在端口转发时设置 interface(隧道转发时 interface 在转发链的节点上设置)
|
||||
if (tunnel.getType() == 1 && StringUtils.isNotBlank(node.getInterfaceName())) {
|
||||
JSONObject metadata = new JSONObject();
|
||||
metadata.put("interface", node.getInterfaceName());
|
||||
service.put("metadata", metadata);
|
||||
}
|
||||
|
||||
// 添加限流器配置
|
||||
if (limiter != null) {
|
||||
service.put("limiter", limiter.toString());
|
||||
}
|
||||
|
||||
// 配置处理器
|
||||
JSONObject handler = new JSONObject();
|
||||
handler.put("type", protocol);
|
||||
if (tunnel.getType() == 2){
|
||||
handler.put("chain", "chains_" + forward.getTunnelId());
|
||||
}
|
||||
service.put("handler", handler);
|
||||
|
||||
// 配置监听器
|
||||
JSONObject listener = createListener(protocol);
|
||||
service.put("listener", listener);
|
||||
|
||||
JSONObject forwarder = createForwarder(forward.getRemoteAddr(), forward.getStrategy());
|
||||
service.put("forwarder", forwarder);
|
||||
|
||||
services.add(service);
|
||||
}
|
||||
GostDto gostDto = WebSocketServer.send_msg(node.getId(), services, meth);
|
||||
if (gostDto.getMsg().contains("exists")){
|
||||
gostDto.setMsg("OK");
|
||||
}
|
||||
return gostDto;
|
||||
}
|
||||
|
||||
public static GostDto DeleteService(Long node_id, JSONArray services) {
|
||||
JSONObject data = new JSONObject();
|
||||
data.put("services", services);
|
||||
GostDto gostDto = WebSocketServer.send_msg(node_id, data, "DeleteService");
|
||||
if (gostDto.getMsg().contains("not found")){
|
||||
gostDto.setMsg("OK");
|
||||
}
|
||||
return gostDto;
|
||||
}
|
||||
|
||||
public static GostDto PauseAndResumeService(Long node_id, String name, String meth) {
|
||||
JSONObject data = new JSONObject();
|
||||
JSONArray services = new JSONArray();
|
||||
services.add(name + "_tcp");
|
||||
services.add(name + "_udp");
|
||||
data.put("services", services);
|
||||
return WebSocketServer.send_msg(node_id, data, meth);
|
||||
}
|
||||
|
||||
|
||||
private static JSONObject createLimiterData(Long name, String speed) {
|
||||
JSONObject data = new JSONObject();
|
||||
data.put("name", name.toString());
|
||||
@@ -298,59 +214,10 @@ public class GostUtil {
|
||||
return data;
|
||||
}
|
||||
|
||||
private static JSONObject createServiceConfig(String name, Integer in_port, Integer limiter, String remoteAddr, String protocol, Integer fow_type, Tunnel tunnel, String strategy, String interfaceName) {
|
||||
JSONObject service = new JSONObject();
|
||||
service.put("name", name + "_" + protocol);
|
||||
if (Objects.equals(protocol, "tcp")){
|
||||
service.put("addr", tunnel.getTcpListenAddr() + ":" + in_port);
|
||||
}else {
|
||||
service.put("addr", tunnel.getUdpListenAddr() + ":" + in_port);
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(interfaceName)) {
|
||||
JSONObject metadata = new JSONObject();
|
||||
metadata.put("interface", interfaceName);
|
||||
service.put("metadata", metadata);
|
||||
}
|
||||
|
||||
|
||||
// 添加限流器配置
|
||||
if (limiter != null) {
|
||||
service.put("limiter", limiter.toString());
|
||||
}
|
||||
|
||||
// 配置处理器
|
||||
JSONObject handler = createHandler(protocol, name, fow_type);
|
||||
service.put("handler", handler);
|
||||
|
||||
// 配置监听器
|
||||
JSONObject listener = createListener(protocol);
|
||||
service.put("listener", listener);
|
||||
|
||||
// 端口转发需要配置转发器
|
||||
if (isPortForwarding(fow_type)) {
|
||||
JSONObject forwarder = createForwarder(remoteAddr, strategy);
|
||||
service.put("forwarder", forwarder);
|
||||
}
|
||||
return service;
|
||||
}
|
||||
|
||||
private static JSONObject createHandler(String protocol, String name, Integer fow_type) {
|
||||
JSONObject handler = new JSONObject();
|
||||
handler.put("type", protocol);
|
||||
|
||||
// 隧道转发需要添加链配置
|
||||
if (isTunnelForwarding(fow_type)) {
|
||||
handler.put("chain", name + "_chains");
|
||||
}
|
||||
|
||||
return handler;
|
||||
}
|
||||
|
||||
private static JSONObject createListener(String protocol) {
|
||||
JSONObject listener = new JSONObject();
|
||||
listener.put("type", protocol);
|
||||
if (Objects.equals(protocol, "udp")){
|
||||
if (Objects.equals(protocol, "udp")) {
|
||||
JSONObject metadata = new JSONObject();
|
||||
metadata.put("keepAlive", true);
|
||||
listener.put("metadata", metadata);
|
||||
@@ -366,13 +233,13 @@ public class GostUtil {
|
||||
int num = 1;
|
||||
for (String addr : split) {
|
||||
JSONObject node = new JSONObject();
|
||||
node.put("name", "node_" + num );
|
||||
node.put("name", "node_" + num);
|
||||
node.put("addr", addr);
|
||||
nodes.add(node);
|
||||
num ++;
|
||||
num++;
|
||||
}
|
||||
|
||||
if (strategy == null || strategy.equals("")){
|
||||
if (strategy == null || strategy.isEmpty()) {
|
||||
strategy = "fifo";
|
||||
}
|
||||
|
||||
@@ -386,12 +253,42 @@ public class GostUtil {
|
||||
return forwarder;
|
||||
}
|
||||
|
||||
private static boolean isPortForwarding(Integer fow_type) {
|
||||
return fow_type != null && fow_type == 1;
|
||||
public static String processServerAddress(String serverAddr) {
|
||||
if (StrUtil.isBlank(serverAddr)) {
|
||||
return serverAddr;
|
||||
}
|
||||
|
||||
// 如果已经被方括号包裹,直接返回
|
||||
if (serverAddr.startsWith("[")) {
|
||||
return serverAddr;
|
||||
}
|
||||
|
||||
// 查找最后一个冒号,分离主机和端口
|
||||
int lastColonIndex = serverAddr.lastIndexOf(':');
|
||||
if (lastColonIndex == -1) {
|
||||
// 没有端口号,直接检查是否需要包裹
|
||||
return isIPv6Address(serverAddr) ? "[" + serverAddr + "]" : serverAddr;
|
||||
}
|
||||
|
||||
String host = serverAddr.substring(0, lastColonIndex);
|
||||
String port = serverAddr.substring(lastColonIndex);
|
||||
|
||||
// 检查主机部分是否为IPv6地址
|
||||
if (isIPv6Address(host)) {
|
||||
return "[" + host + "]" + port;
|
||||
}
|
||||
|
||||
return serverAddr;
|
||||
}
|
||||
|
||||
private static boolean isTunnelForwarding(Integer fow_type) {
|
||||
return fow_type != null && fow_type != 1;
|
||||
}
|
||||
private static boolean isIPv6Address(String address) {
|
||||
// IPv6地址包含多个冒号,至少2个
|
||||
if (!address.contains(":")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 计算冒号数量,IPv6地址至少有2个冒号
|
||||
long colonCount = address.chars().filter(ch -> ch == ':').count();
|
||||
return colonCount >= 2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,6 +229,7 @@ public class WebSocketServer extends TextWebSocketHandler {
|
||||
String socks = (String) session.getAttributes().get("socks");
|
||||
|
||||
log.info("节点 {} 尝试连接,开始处理连接逻辑", nodeId);
|
||||
log.info("屏蔽协议数据:http->{}, tls->{}, socks->{}", http, tls, socks);
|
||||
|
||||
// 检查是否已有该节点的连接,如果有则记录日志但直接覆盖
|
||||
WebSocketSession existingSession = nodeSessions.get(nodeId);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.admin.config;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
|
||||
@@ -15,7 +16,9 @@ public class MybatisPlusConfig {
|
||||
@Bean
|
||||
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); // 分页插件
|
||||
// 配置分页插件,指定数据库类型为 SQLite
|
||||
PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.SQLITE);
|
||||
interceptor.addInnerInterceptor(paginationInterceptor); // 分页插件
|
||||
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); // 防止全表更新插件
|
||||
return interceptor;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.admin.config;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.PreDestroy;
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
import java.sql.Statement;
|
||||
|
||||
/**
|
||||
* SQLite 数据库配置
|
||||
* 启用 WAL (Write-Ahead Logging) 模式以提高并发性能
|
||||
* 添加定期 checkpoint 和优雅关闭处理
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@EnableScheduling
|
||||
public class SQLiteConfig implements ApplicationRunner {
|
||||
|
||||
private final DataSource dataSource;
|
||||
|
||||
public SQLiteConfig(DataSource dataSource) {
|
||||
this.dataSource = dataSource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) throws Exception {
|
||||
try (Connection connection = dataSource.getConnection();
|
||||
Statement statement = connection.createStatement()) {
|
||||
|
||||
statement.execute("PRAGMA journal_mode=WAL;");
|
||||
statement.execute("PRAGMA synchronous=NORMAL;");
|
||||
statement.execute("PRAGMA cache_size=-64000;"); // 64MB 缓存
|
||||
statement.execute("PRAGMA temp_store=MEMORY;");
|
||||
statement.execute("PRAGMA busy_timeout=5000;"); // 5秒超时
|
||||
statement.execute("PRAGMA wal_autocheckpoint=1000;"); // 每1000页自动checkpoint
|
||||
|
||||
log.info("SQLite WAL mode configured successfully");
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to configure SQLite database", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 定期执行 checkpoint,确保 WAL 文件内容写入主数据库
|
||||
* 每5分钟执行一次
|
||||
*/
|
||||
@Scheduled(fixedDelay = 300000, initialDelay = 300000)
|
||||
public void performCheckpoint() {
|
||||
try (Connection connection = dataSource.getConnection();
|
||||
Statement statement = connection.createStatement()) {
|
||||
|
||||
statement.execute("PRAGMA wal_checkpoint(TRUNCATE);");
|
||||
log.debug("SQLite WAL checkpoint completed");
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to perform SQLite checkpoint", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用关闭前执行最终的 checkpoint,确保所有数据都写入主数据库文件
|
||||
*/
|
||||
@PreDestroy
|
||||
public void onShutdown() {
|
||||
log.info("Performing final SQLite checkpoint before shutdown...");
|
||||
try (Connection connection = dataSource.getConnection();
|
||||
Statement statement = connection.createStatement()) {
|
||||
|
||||
// 强制执行 checkpoint,将所有 WAL 内容写入主数据库
|
||||
statement.execute("PRAGMA wal_checkpoint(TRUNCATE);");
|
||||
log.info("Final SQLite checkpoint completed successfully");
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to perform final SQLite checkpoint", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ package com.admin.controller;
|
||||
import com.admin.common.aop.LogAnnotation;
|
||||
import com.admin.common.dto.FlowDto;
|
||||
import com.admin.common.dto.GostConfigDto;
|
||||
import com.admin.common.lang.R;
|
||||
import com.admin.common.task.CheckGostConfigAsync;
|
||||
import com.admin.common.utils.AESCrypto;
|
||||
import com.admin.common.utils.GostUtil;
|
||||
import com.admin.entity.*;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
@@ -21,7 +21,6 @@ import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 流量上报控制器
|
||||
@@ -147,15 +146,17 @@ public class FlowController extends BaseController {
|
||||
String decryptedData = decryptIfNeeded(rawData, secret);
|
||||
|
||||
// 3. 解析为FlowDto列表
|
||||
FlowDto flowDataList = JSONObject.parseObject(decryptedData, FlowDto.class);
|
||||
if (Objects.equals(flowDataList.getN(), "web_api")) {
|
||||
return SUCCESS_RESPONSE;
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
JSONArray flowDataList = JSONObject.parseArray(decryptedData);
|
||||
log.info("节点上报流量数据{}", flowDataList);
|
||||
// 4. 处理流量数据
|
||||
return processFlowData(flowDataList);
|
||||
for (int i = 0; i < flowDataList.size(); i++) {
|
||||
String jsonObject = flowDataList.getJSONObject(i).toJSONString();
|
||||
FlowDto flowDto = JSONObject.parseObject(jsonObject, FlowDto.class);
|
||||
if (!Objects.equals(flowDto.getN(), "web_api")) {
|
||||
processFlowData(flowDto);
|
||||
}
|
||||
}
|
||||
return SUCCESS_RESPONSE;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -212,24 +213,30 @@ public class FlowController extends BaseController {
|
||||
/**
|
||||
* 处理流量数据的核心逻辑
|
||||
*/
|
||||
private String processFlowData(FlowDto flowDataList) {
|
||||
private void processFlowData(FlowDto flowDataList) {
|
||||
String[] serviceIds = parseServiceName(flowDataList.getN());
|
||||
String forwardId = serviceIds[0];
|
||||
String userId = serviceIds[1];
|
||||
String userTunnelId = serviceIds[2];
|
||||
|
||||
Forward forward = forwardService.getById(forwardId);
|
||||
if (forward != null){
|
||||
Tunnel tunnel = tunnelService.getById(forward.getTunnelId());
|
||||
|
||||
// 获取流量计费类型
|
||||
int flowType = getFlowType(forward);
|
||||
|
||||
// 处理流量倍率及单双向计算
|
||||
FlowDto flowStats = filterFlowData(flowDataList, forward, flowType);
|
||||
// 处理流量倍率及单双向计算
|
||||
BigDecimal trafficRatio = tunnel.getTrafficRatio();
|
||||
BigDecimal originalD = BigDecimal.valueOf(flowDataList.getD());
|
||||
BigDecimal originalU = BigDecimal.valueOf(flowDataList.getU());
|
||||
BigDecimal newD = originalD.multiply(trafficRatio);
|
||||
BigDecimal newU = originalU.multiply(trafficRatio);
|
||||
flowDataList.setD(newD.longValue() * tunnel.getFlow());
|
||||
flowDataList.setU(newU.longValue() * tunnel.getFlow());
|
||||
}
|
||||
|
||||
// 先更新所有流量统计 - 确保流量数据的一致性
|
||||
updateForwardFlow(forwardId, flowStats);
|
||||
updateUserFlow(userId, flowStats);
|
||||
updateUserTunnelFlow(userTunnelId, flowStats);
|
||||
updateForwardFlow(forwardId, flowDataList);
|
||||
updateUserFlow(userId, flowDataList);
|
||||
updateUserTunnelFlow(userTunnelId, flowDataList);
|
||||
|
||||
// 7. 检查和服务暂停操作
|
||||
String name = buildServiceName(forwardId, userId, userTunnelId);
|
||||
@@ -238,7 +245,6 @@ public class FlowController extends BaseController {
|
||||
checkUserTunnelRelatedLimits(userTunnelId, name, userId);
|
||||
}
|
||||
|
||||
return SUCCESS_RESPONSE;
|
||||
}
|
||||
|
||||
private void checkUserRelatedLimits(String userId, String name) {
|
||||
@@ -277,7 +283,7 @@ public class FlowController extends BaseController {
|
||||
UserTunnel userTunnel = userTunnelService.getById(userTunnelId);
|
||||
if (userTunnel == null) return;
|
||||
long flow = userTunnel.getInFlow() + userTunnel.getOutFlow();
|
||||
if (flow >= userTunnel.getFlow() * BYTES_TO_GB) {
|
||||
if (flow >= userTunnel.getFlow() * BYTES_TO_GB) {
|
||||
pauseSpecificForward(userTunnel.getTunnelId(), name, userId);
|
||||
return;
|
||||
}
|
||||
@@ -300,44 +306,17 @@ public class FlowController extends BaseController {
|
||||
}
|
||||
|
||||
public void pauseService(List<Forward> forwardList, String name) {
|
||||
for (Forward forward : forwardList) {
|
||||
Tunnel tunnel = tunnelService.getById(forward.getTunnelId());
|
||||
if (tunnel != null){
|
||||
GostUtil.PauseService(tunnel.getInNodeId(), name);
|
||||
if (tunnel.getType() == 2){
|
||||
GostUtil.PauseRemoteService(tunnel.getOutNodeId(), name);
|
||||
}
|
||||
}
|
||||
forward.setStatus(0);
|
||||
forwardService.updateById(forward);
|
||||
}
|
||||
}
|
||||
|
||||
private FlowDto filterFlowData(FlowDto flowDto, Forward forward, int flowType) {
|
||||
if (forward != null) {
|
||||
Tunnel tunnel = tunnelService.getById(forward.getTunnelId());
|
||||
if (tunnel != null) {
|
||||
BigDecimal trafficRatio = tunnel.getTrafficRatio();
|
||||
|
||||
BigDecimal originalD = BigDecimal.valueOf(flowDto.getD());
|
||||
BigDecimal originalU = BigDecimal.valueOf(flowDto.getU());
|
||||
|
||||
BigDecimal newD = originalD.multiply(trafficRatio);
|
||||
BigDecimal newU = originalU.multiply(trafficRatio);
|
||||
|
||||
flowDto.setD(newD.longValue() * flowType);
|
||||
flowDto.setU(newU.longValue() * flowType);
|
||||
}
|
||||
}
|
||||
return flowDto;
|
||||
}
|
||||
|
||||
private int getFlowType(Forward forward) {
|
||||
int defaultFlowType = 2;
|
||||
if (forward == null) return defaultFlowType;
|
||||
Tunnel tunnel = tunnelService.getById(forward.getTunnelId());
|
||||
if (tunnel == null) return defaultFlowType;
|
||||
return tunnel.getFlow();
|
||||
// for (Forward forward : forwardList) {
|
||||
// Tunnel tunnel = tunnelService.getById(forward.getTunnelId());
|
||||
// if (tunnel != null) {
|
||||
// GostUtil.PauseService(tunnel.getInNodeId(), name);
|
||||
// if (tunnel.getType() == 2) {
|
||||
// GostUtil.PauseRemoteService(tunnel.getOutNodeId(), name);
|
||||
// }
|
||||
// }
|
||||
// forward.setStatus(0);
|
||||
// forwardService.updateById(forward);
|
||||
// }
|
||||
}
|
||||
|
||||
private void updateForwardFlow(String forwardId, FlowDto flowStats) {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.admin.entity;
|
||||
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
public class ChainTunnel implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
// 关联的隧道ID
|
||||
private Long tunnelId;
|
||||
|
||||
// 记录所选节点的类型: 1是入口节点 2是转发链 3是出口节点
|
||||
private Integer chainType;
|
||||
|
||||
// 记录所选节点的ID
|
||||
private Long nodeId;
|
||||
|
||||
// 记录节点使用的端口 入口没有次配置为null
|
||||
private Integer port;
|
||||
|
||||
// 负载模式 入口没有次配置为null
|
||||
private String strategy;
|
||||
|
||||
// 只有转发链有次参数 表示是第几条
|
||||
private Integer inx;
|
||||
|
||||
// 使用的协议 入口没有次配置为null
|
||||
private String protocol;
|
||||
|
||||
|
||||
}
|
||||
@@ -1,6 +1,12 @@
|
||||
package com.admin.entity;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
import com.admin.common.dto.ForwardPortDto;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@@ -14,6 +20,7 @@ import lombok.EqualsAndHashCode;
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@TableName(autoResultMap = true)
|
||||
public class Forward extends BaseEntity{
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
@@ -26,14 +33,8 @@ public class Forward extends BaseEntity{
|
||||
|
||||
private Integer tunnelId;
|
||||
|
||||
private Integer inPort;
|
||||
|
||||
private Integer outPort;
|
||||
|
||||
private String remoteAddr;
|
||||
|
||||
private String interfaceName;
|
||||
|
||||
private String strategy;
|
||||
|
||||
private Long inFlow;
|
||||
@@ -41,5 +42,4 @@ public class Forward extends BaseEntity{
|
||||
private Long outFlow;
|
||||
|
||||
private Integer inx;
|
||||
|
||||
}
|
||||
|
||||