diff --git a/Xboard/.docker/.data/.gitignore b/Xboard/.docker/.data/.gitignore
deleted file mode 100644
index 0377233..0000000
--- a/Xboard/.docker/.data/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-*
-!.gitignore
-!redis
\ No newline at end of file
diff --git a/Xboard/.docker/.data/redis/.gitignore b/Xboard/.docker/.data/redis/.gitignore
deleted file mode 100644
index c96a04f..0000000
--- a/Xboard/.docker/.data/redis/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-*
-!.gitignore
\ No newline at end of file
diff --git a/Xboard/.docker/supervisor/supervisord.conf b/Xboard/.docker/supervisor/supervisord.conf
deleted file mode 100644
index c516e4e..0000000
--- a/Xboard/.docker/supervisor/supervisord.conf
+++ /dev/null
@@ -1,81 +0,0 @@
-[supervisord]
-nodaemon=true
-user=root
-logfile=/dev/stdout
-logfile_maxbytes=0
-pidfil=/www/storage/logs/supervisor/supervisord.pid
-loglevel=info
-
-[program:octane]
-process_name=%(program_name)s_%(process_num)02d
-command=php /www/artisan octane:start --host=0.0.0.0 --port=7001
-autostart=%(ENV_ENABLE_WEB)s
-autorestart=true
-user=www
-redirect_stderr=true
-stdout_logfile=/dev/stdout
-stdout_logfile_maxbytes=0
-stdout_logfile_backups=0
-numprocs=1
-stopwaitsecs=10
-stopsignal=QUIT
-stopasgroup=true
-killasgroup=true
-priority=100
-
-[program:horizon]
-process_name=%(program_name)s_%(process_num)02d
-command=php /www/artisan horizon
-autostart=%(ENV_ENABLE_HORIZON)s
-autorestart=true
-user=www
-redirect_stderr=true
-stdout_logfile=/dev/stdout
-stdout_logfile_maxbytes=0
-stdout_logfile_backups=0
-numprocs=1
-stopwaitsecs=3
-stopsignal=SIGINT
-stopasgroup=true
-killasgroup=true
-priority=200
-
-[program:redis]
-process_name=%(program_name)s_%(process_num)02d
-command=redis-server --dir /data
- --dbfilename dump.rdb
- --save 900 1
- --save 300 10
- --save 60 10000
- --unixsocket /data/redis.sock
- --unixsocketperm 777
-autostart=%(ENV_ENABLE_REDIS)s
-autorestart=true
-user=redis
-redirect_stderr=true
-stdout_logfile=/dev/stdout
-stdout_logfile_maxbytes=0
-stdout_logfile_backups=0
-numprocs=1
-stopwaitsecs=3
-stopsignal=TERM
-stopasgroup=true
-killasgroup=true
-priority=300
-
-[program:ws-server]
-process_name=%(program_name)s_%(process_num)02d
-command=php /www/artisan ws-server start
-autostart=%(ENV_ENABLE_WS_SERVER)s
-autorestart=true
-user=www
-redirect_stderr=true
-stdout_logfile=/dev/stdout
-stdout_logfile_maxbytes=0
-stdout_logfile_backups=0
-numprocs=1
-stopwaitsecs=5
-stopsignal=SIGINT
-stopasgroup=true
-killasgroup=true
-priority=400
\ No newline at end of file
diff --git a/Xboard/.dockerignore b/Xboard/.dockerignore
deleted file mode 100644
index 3b0ee11..0000000
--- a/Xboard/.dockerignore
+++ /dev/null
@@ -1,26 +0,0 @@
-/node_modules
-/config/v2board.php
-/public/hot
-/public/storage
-/public/env.example.js
-/storage/*.key
-/vendor
-.env
-.env.backup
-.phpunit.result.cache
-.idea
-.lock
-Homestead.json
-Homestead.yaml
-npm-debug.log
-yarn-error.log
-composer.phar
-composer.lock
-yarn.lock
-docker-compose.yml
-.DS_Store
-/docker
-storage/laravels.conf
-storage/laravels.pid
-storage/laravels-timer-process.pid
-/frontend
diff --git a/Xboard/.editorconfig b/Xboard/.editorconfig
deleted file mode 100644
index 6537ca4..0000000
--- a/Xboard/.editorconfig
+++ /dev/null
@@ -1,15 +0,0 @@
-root = true
-
-[*]
-charset = utf-8
-end_of_line = lf
-insert_final_newline = true
-indent_style = space
-indent_size = 4
-trim_trailing_whitespace = true
-
-[*.md]
-trim_trailing_whitespace = false
-
-[*.{yml,yaml}]
-indent_size = 2
diff --git a/Xboard/.env.example b/Xboard/.env.example
deleted file mode 100644
index 251858b..0000000
--- a/Xboard/.env.example
+++ /dev/null
@@ -1,41 +0,0 @@
-APP_NAME=XBoard
-APP_ENV=production
-APP_KEY=base64:PZXk5vTuTinfeEVG5FpYv2l6WEhLsyvGpiWK7IgJJ60=
-APP_DEBUG=false
-APP_URL=http://localhost
-
-LOG_CHANNEL=stack
-
-DB_CONNECTION=mysql
-DB_HOST=127.0.0.1
-DB_PORT=3306
-DB_DATABASE=xboard
-DB_USERNAME=root
-DB_PASSWORD=
-
-REDIS_HOST=127.0.0.1
-REDIS_PASSWORD=null
-REDIS_PORT=6379
-
-BROADCAST_DRIVER=log
-CACHE_DRIVER=redis
-QUEUE_CONNECTION=redis
-
-MAIL_DRIVER=smtp
-MAIL_HOST=smtp.mailtrap.io
-MAIL_PORT=2525
-MAIL_USERNAME=null
-MAIL_PASSWORD=null
-MAIL_ENCRYPTION=null
-MAIL_FROM_ADDRESS=null
-MAIL_FROM_NAME=null
-MAILGUN_DOMAIN=
-MAILGUN_SECRET=
-
-# google cloud storage
-ENABLE_AUTO_BACKUP_AND_UPDATE=false
-GOOGLE_CLOUD_KEY_FILE=config/googleCloudStorageKey.json
-GOOGLE_CLOUD_STORAGE_BUCKET=
-
-# Prevent reinstallation
-INSTALLED=false
\ No newline at end of file
diff --git a/Xboard/.gitattributes b/Xboard/.gitattributes
deleted file mode 100644
index 967315d..0000000
--- a/Xboard/.gitattributes
+++ /dev/null
@@ -1,5 +0,0 @@
-* text=auto
-*.css linguist-vendored
-*.scss linguist-vendored
-*.js linguist-vendored
-CHANGELOG.md export-ignore
diff --git a/Xboard/.github/ISSUE_TEMPLATE/bug-report.md b/Xboard/.github/ISSUE_TEMPLATE/bug-report.md
deleted file mode 100644
index 271b8df..0000000
--- a/Xboard/.github/ISSUE_TEMPLATE/bug-report.md
+++ /dev/null
@@ -1,39 +0,0 @@
----
-name: 🐛 问题反馈 | Bug Report
-about: 提交使用过程中遇到的问题 | Report an issue
-title: "Bug Report:"
-labels: '🐛 bug'
-assignees: ''
----
-
-
-
-
-> ⚠️ 请务必按照模板填写完整信息,没有详细描述的issue可能会被忽略或关闭
-> ⚠️ Please follow the template to provide complete information, issues without detailed description may be ignored or closed
-
-**基本信息 | Basic Info**
-```yaml
-XBoard版本 | Version:
-部署方式 | Deployment: [Docker/手动部署]
-PHP版本 | Version:
-数据库 | Database:
-```
-
-**问题描述 | Description**
-
-
-
-**复现步骤 | Steps**
-
-1.
-2.
-
-**相关截图 | Screenshots**
-
-
-**日志信息 | Logs**
-
-```log
-// 粘贴日志内容到这里
-```
\ No newline at end of file
diff --git a/Xboard/.github/ISSUE_TEMPLATE/feature-request.md b/Xboard/.github/ISSUE_TEMPLATE/feature-request.md
deleted file mode 100644
index 5428f95..0000000
--- a/Xboard/.github/ISSUE_TEMPLATE/feature-request.md
+++ /dev/null
@@ -1,28 +0,0 @@
----
-name: ✨ 功能请求 | Feature Request
-about: 提交新功能建议或改进意见 | Suggest an idea
-title: "Feature Request:"
-labels: '✨ enhancement'
-assignees: ''
----
-
-> ⚠️ 请务必按照模板详细描述你的需求,没有详细描述的issue可能会被忽略或关闭
-> ⚠️ Please follow the template to describe your request in detail, issues without detailed description may be ignored or closed
-
-**需求描述 | Description**
-
-
-
-**使用场景 | Use Case**
-
-
-
-**功能建议 | Suggestion**
-
-```yaml
-功能形式 | Type: [新功能/功能优化/界面改进]
-预期效果 | Expected:
-```
-
-**补充说明 | Additional**
-
\ No newline at end of file
diff --git a/Xboard/.github/workflows/docker-publish.yml b/Xboard/.github/workflows/docker-publish.yml
deleted file mode 100644
index 9898924..0000000
--- a/Xboard/.github/workflows/docker-publish.yml
+++ /dev/null
@@ -1,100 +0,0 @@
-name: Docker Build and Publish
-
-on:
- push:
- branches: ["master", "new-dev"]
- workflow_dispatch:
-
-env:
- REGISTRY: ghcr.io
- IMAGE_NAME: ${{ github.repository }}
-
-jobs:
- build:
- runs-on: ubuntu-latest
- permissions:
- contents: read
- packages: write
- id-token: write
-
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
- with:
- fetch-depth: 1
-
- - name: Set up QEMU
- uses: docker/setup-qemu-action@v3
- with:
- platforms: 'arm64,amd64'
-
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
- with:
- platforms: linux/amd64,linux/arm64
- driver-opts: |
- image=moby/buildkit:v0.20.0
- network=host
-
- - name: Free Disk Space
- run: |
- sudo rm -rf /usr/share/dotnet
- sudo rm -rf /usr/local/lib/android
- sudo rm -rf /opt/ghc
- sudo rm -rf /opt/hostedtoolcache/CodeQL
- sudo docker image prune -af
-
- - name: Login to registry
- uses: docker/login-action@v3
- with:
- registry: ${{ env.REGISTRY }}
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Extract metadata
- id: meta
- uses: docker/metadata-action@v5
- with:
- images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- tags: |
- type=ref,event=branch
- type=sha,format=short,prefix=,enable=true
- type=raw,value=new,enable=${{ github.ref == 'refs/heads/master' }}
- type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }}
- type=raw,value=${{ steps.get_version.outputs.version }}
- labels: |
- org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
- org.opencontainers.image.revision=${{ github.sha }}
-
- - name: Get version
- id: get_version
- run: echo "version=$(git describe --tags --always)" >> $GITHUB_OUTPUT
-
- - name: Update version in app.php
- run: |
- VERSION=$(date '+%Y%m%d')-$(git rev-parse --short HEAD)
- sed -i "s/'version' => '.*'/'version' => '$VERSION'/g" config/app.php
- echo "Updated version to: $VERSION"
-
- - name: Build and push
- id: build-and-push
- uses: docker/build-push-action@v5
- with:
- context: .
- push: true
- platforms: linux/amd64,linux/arm64
- cache-from: type=gha
- cache-to: type=gha,mode=max
- tags: ${{ steps.meta.outputs.tags }}
- labels: ${{ steps.meta.outputs.labels }}
- build-args: |
- BUILDKIT_INLINE_CACHE=1
- BUILDKIT_MULTI_PLATFORM=1
- CACHEBUST=${{ github.sha }}
- REPO_URL=https://github.com/${{ github.repository }}
- BRANCH_NAME=${{ github.ref_name }}
- provenance: false
- outputs: type=registry,push=true
- allow: |
- network.host
-
diff --git a/Xboard/.gitignore b/Xboard/.gitignore
deleted file mode 100644
index 2715539..0000000
--- a/Xboard/.gitignore
+++ /dev/null
@@ -1,34 +0,0 @@
-/node_modules
-/config/v2board.php
-/config/googleCloudStorageKey.json
-/public/hot
-/public/storage
-/public/env.example.js
-*.user.ini
-/storage/*.key
-/vendor
-.env
-.env.backup
-.phpunit.result.cache
-.idea
-.lock
-Homestead.json
-Homestead.yaml
-npm-debug.log
-yarn-error.log
-composer.phar
-composer.lock
-yarn.lock
-docker-compose.yml
-.DS_Store
-/docker
-storage/laravels.conf
-storage/laravels.pid
-storage/update_pending
-storage/laravels-timer-process.pid
-cli-php.ini
-frontend
-docker-compose.yaml
-bun.lockb
-compose.yaml
-.scribe
\ No newline at end of file
diff --git a/Xboard/.gitmodules b/Xboard/.gitmodules
deleted file mode 100644
index 060ba10..0000000
--- a/Xboard/.gitmodules
+++ /dev/null
@@ -1,3 +0,0 @@
-[submodule "public/assets/admin"]
- path = public/assets/admin
- url = https://github.com/cedar2025/xboard-admin-dist.git
diff --git a/Xboard/Dockerfile b/Xboard/Dockerfile
deleted file mode 100644
index ba41fe1..0000000
--- a/Xboard/Dockerfile
+++ /dev/null
@@ -1,47 +0,0 @@
-FROM phpswoole/swoole:php8.2-alpine
-
-COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/
-
-# Install PHP extensions one by one with lower optimization level for ARM64 compatibility
-RUN CFLAGS="-O0" install-php-extensions pcntl && \
- CFLAGS="-O0 -g0" install-php-extensions bcmath && \
- install-php-extensions zip && \
- install-php-extensions redis && \
- apk --no-cache add shadow sqlite mysql-client mysql-dev mariadb-connector-c git patch supervisor redis && \
- addgroup -S -g 1000 www && adduser -S -G www -u 1000 www && \
- (getent group redis || addgroup -S redis) && \
- (getent passwd redis || adduser -S -G redis -H -h /data redis)
-
-WORKDIR /www
-
-COPY .docker /
-
-# Add build arguments
-ARG CACHEBUST
-ARG REPO_URL
-ARG BRANCH_NAME
-
-RUN echo "Attempting to clone branch: ${BRANCH_NAME} from ${REPO_URL} with CACHEBUST: ${CACHEBUST}" && \
- rm -rf ./* && \
- rm -rf .git && \
- git config --global --add safe.directory /www && \
- git clone --depth 1 --branch ${BRANCH_NAME} ${REPO_URL} . && \
- git submodule update --init --recursive --force
-
-COPY .docker/supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
-
-RUN composer install --no-cache --no-dev \
- && php artisan storage:link \
- && cp -r plugins/ /opt/default-plugins/ \
- && chown -R www:www /www \
- && chmod -R 775 /www \
- && mkdir -p /data \
- && chown redis:redis /data
-
-ENV ENABLE_WEB=true \
- ENABLE_HORIZON=true \
- ENABLE_REDIS=false \
- ENABLE_WS_SERVER=false
-
-EXPOSE 7001
-CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
diff --git a/Xboard/LICENSE b/Xboard/LICENSE
deleted file mode 100644
index 8d23873..0000000
--- a/Xboard/LICENSE
+++ /dev/null
@@ -1,21 +0,0 @@
-MIT License
-
-Copyright (c) 2019 Tokumeikoi
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
\ No newline at end of file
diff --git a/Xboard/README.md b/Xboard/README.md
deleted file mode 100644
index 4d6a618..0000000
--- a/Xboard/README.md
+++ /dev/null
@@ -1,100 +0,0 @@
-# Xboard
-
-
-
-[](https://t.me/XboardOfficial)
-
-
-[](LICENSE)
-
-
-
-## 📖 Introduction
-
-Xboard is a modern panel system built on Laravel 11, focusing on providing a clean and efficient user experience.
-
-## ✨ Features
-
-- 🚀 Built with Laravel 12 + Octane for significant performance gains
-- 🎨 Redesigned admin interface (React + Shadcn UI)
-- 📱 Modern user frontend (Vue3 + TypeScript)
-- 🐳 Ready-to-use Docker deployment solution
-- 🎯 Optimized system architecture for better maintainability
-
-## 🚀 Quick Start
-
-```bash
-git clone -b compose --depth 1 https://github.com/cedar2025/Xboard && \
-cd Xboard && \
-docker compose run -it --rm \
- -e ENABLE_SQLITE=true \
- -e ENABLE_REDIS=true \
- -e ADMIN_ACCOUNT=admin@demo.com \
- web php artisan xboard:install && \
-docker compose up -d
-```
-
-> After installation, visit: http://SERVER_IP:7001
-> ⚠️ Make sure to save the admin credentials shown during installation
-
-## 📖 Documentation
-
-### 🔄 Upgrade Notice
-> 🚨 **Important:** This version involves significant changes. Please strictly follow the upgrade documentation and backup your database before upgrading. Note that upgrading and migration are different processes, do not confuse them.
-
-### Development Guides
-- [Plugin Development Guide](./docs/en/development/plugin-development-guide.md) - Complete guide for developing XBoard plugins
-
-### Deployment Guides
-- [Deploy with 1Panel](./docs/en/installation/1panel.md)
-- [Deploy with Docker Compose](./docs/en/installation/docker-compose.md)
-- [Deploy with aaPanel](./docs/en/installation/aapanel.md)
-- [Deploy with aaPanel + Docker](./docs/en/installation/aapanel-docker.md) (Recommended)
-
-### Migration Guides
-- [Migrate from v2board dev](./docs/en/migration/v2board-dev.md)
-- [Migrate from v2board 1.7.4](./docs/en/migration/v2board-1.7.4.md)
-- [Migrate from v2board 1.7.3](./docs/en/migration/v2board-1.7.3.md)
-
-## 🛠️ Tech Stack
-
-- Backend: Laravel 11 + Octane
-- Admin Panel: React + Shadcn UI + TailwindCSS
-- User Frontend: Vue3 + TypeScript + NaiveUI
-- Deployment: Docker + Docker Compose
-- Caching: Redis + Octane Cache
-
-## 📷 Preview
-
-
-
-
-## ⚠️ Disclaimer
-
-This project is for learning and communication purposes only. Users are responsible for any consequences of using this project.
-
-## 🌟 Maintenance Notice
-
-This project is currently under light maintenance. We will:
-- Fix critical bugs and security issues
-- Review and merge important pull requests
-- Provide necessary updates for compatibility
-
-However, new feature development may be limited.
-
-## 🔔 Important Notes
-
-1. Restart required after modifying admin path:
-```bash
-docker compose restart
-```
-
-2. For aaPanel installations, restart the Octane daemon process
-
-## 🤝 Contributing
-
-Issues and Pull Requests are welcome to help improve the project.
-
-## 📈 Star History
-
-[](https://starchart.cc/cedar2025/Xboard)
diff --git a/Xboard/app/Console/Commands/BackupDatabase.php b/Xboard/app/Console/Commands/BackupDatabase.php
deleted file mode 100644
index 14c82ee..0000000
--- a/Xboard/app/Console/Commands/BackupDatabase.php
+++ /dev/null
@@ -1,100 +0,0 @@
-argument('upload');
- // 如果是上传到云端则判断是否存在必要配置
- if($isUpload){
- $requiredConfigs = ['database.connections.mysql', 'cloud_storage.google_cloud.key_file', 'cloud_storage.google_cloud.storage_bucket'];
- foreach ($requiredConfigs as $config) {
- if (blank(config($config))) {
- $this->error("❌:缺少必要配置项: $config , 取消备份");
- return;
- }
- }
- }
-
- // 数据库备份逻辑
- try{
- if (config('database.default') === 'mysql'){
- $databaseBackupPath = storage_path('backup/' . now()->format('Y-m-d_H-i-s') . '_' . config('database.connections.mysql.database') . '_database_backup.sql');
- $this->info("1️⃣:开始备份Mysql");
- \Spatie\DbDumper\Databases\MySql::create()
- ->setHost(config('database.connections.mysql.host'))
- ->setPort(config('database.connections.mysql.port'))
- ->setDbName(config('database.connections.mysql.database'))
- ->setUserName(config('database.connections.mysql.username'))
- ->setPassword(config('database.connections.mysql.password'))
- ->dumpToFile($databaseBackupPath);
- $this->info("2️⃣:Mysql备份完成");
- }elseif(config('database.default') === 'sqlite'){
- $databaseBackupPath = storage_path('backup/' . now()->format('Y-m-d_H-i-s') . '_sqlite' . '_database_backup.sql');
- $this->info("1️⃣:开始备份Sqlite");
- \Spatie\DbDumper\Databases\Sqlite::create()
- ->setDbName(config('database.connections.sqlite.database'))
- ->dumpToFile($databaseBackupPath);
- $this->info("2️⃣:Sqlite备份完成");
- }else{
- $this->error('备份失败,你的数据库不是sqlite或者mysql');
- return;
- }
- $this->info('3️⃣:开始压缩备份文件');
- // 使用 gzip 压缩备份文件
- $compressedBackupPath = $databaseBackupPath . '.gz';
- $gzipCommand = new Process(["gzip", "-c", $databaseBackupPath]);
- $gzipCommand->run();
-
- // 检查压缩是否成功
- if ($gzipCommand->isSuccessful()) {
- // 压缩成功,你可以删除原始备份文件
- file_put_contents($compressedBackupPath, $gzipCommand->getOutput());
- $this->info('4️⃣:文件压缩成功');
- unlink($databaseBackupPath);
- } else {
- // 压缩失败,处理错误
- echo $gzipCommand->getErrorOutput();
- $this->error('😔:文件压缩失败');
- unlink($databaseBackupPath);
- return;
- }
- if (!$isUpload){
- $this->info("🎉:数据库成功备份到:$compressedBackupPath");
- }else{
- // 传到云盘
- $this->info("5️⃣:开始将备份上传到Google Cloud");
- // Google Cloud Storage 配置
- $storage = new StorageClient([
- 'keyFilePath' => config('cloud_storage.google_cloud.key_file'),
- ]);
- $bucket = $storage->bucket(config('cloud_storage.google_cloud.storage_bucket'));
- $objectName = 'backup/' . now()->format('Y-m-d_H-i-s') . '_database_backup.sql.gz';
- // 上传文件
- $bucket->upload(fopen($compressedBackupPath, 'r'), [
- 'name' => $objectName,
- ]);
-
- // 输出文件链接
- Log::channel('backup')->info("🎉:数据库备份已上传到 Google Cloud Storage: $objectName");
- $this->info("🎉:数据库备份已上传到 Google Cloud Storage: $objectName");
- File::delete($compressedBackupPath);
- }
- }catch(\Exception $e){
- Log::channel('backup')->error("😔:数据库备份失败 \n" . $e);
- $this->error("😔:数据库备份失败\n" . $e);
- File::delete($compressedBackupPath);
- }
- }
-}
diff --git a/Xboard/app/Console/Commands/CheckCommission.php b/Xboard/app/Console/Commands/CheckCommission.php
deleted file mode 100644
index 0eabc74..0000000
--- a/Xboard/app/Console/Commands/CheckCommission.php
+++ /dev/null
@@ -1,129 +0,0 @@
-autoCheck();
- $this->autoPayCommission();
- }
-
- public function autoCheck()
- {
- if ((int)admin_setting('commission_auto_check_enable', 1)) {
- Order::where('commission_status', 0)
- ->where('invite_user_id', '!=', NULL)
- ->where('status', 3)
- ->where('updated_at', '<=', strtotime('-3 day', time()))
- ->update([
- 'commission_status' => 1
- ]);
- }
- }
-
- public function autoPayCommission()
- {
- $orders = Order::where('commission_status', 1)
- ->where('invite_user_id', '!=', NULL)
- ->get();
- foreach ($orders as $order) {
- try{
- DB::beginTransaction();
- if (!$this->payHandle($order->invite_user_id, $order)) {
- DB::rollBack();
- continue;
- }
- $order->commission_status = 2;
- if (!$order->save()) {
- DB::rollBack();
- continue;
- }
- DB::commit();
- } catch (\Exception $e){
- DB::rollBack();
- throw $e;
- }
- }
- }
-
- public function payHandle($inviteUserId, Order $order)
- {
- $level = 3;
- if ((int)admin_setting('commission_distribution_enable', 0)) {
- $commissionShareLevels = [
- 0 => (int)admin_setting('commission_distribution_l1'),
- 1 => (int)admin_setting('commission_distribution_l2'),
- 2 => (int)admin_setting('commission_distribution_l3')
- ];
- } else {
- $commissionShareLevels = [
- 0 => 100
- ];
- }
- for ($l = 0; $l < $level; $l++) {
- $inviter = User::find($inviteUserId);
- if (!$inviter) continue;
- if (!isset($commissionShareLevels[$l])) continue;
- $commissionBalance = $order->commission_balance * ($commissionShareLevels[$l] / 100);
- if (!$commissionBalance) continue;
- if ((int)admin_setting('withdraw_close_enable', 0)) {
- $inviter->increment('balance', $commissionBalance);
- } else {
- $inviter->increment('commission_balance', $commissionBalance);
- }
- if (!$inviter->save()) {
- DB::rollBack();
- return false;
- }
- CommissionLog::create([
- 'invite_user_id' => $inviteUserId,
- 'user_id' => $order->user_id,
- 'trade_no' => $order->trade_no,
- 'order_amount' => $order->total_amount,
- 'get_amount' => $commissionBalance
- ]);
- $inviteUserId = $inviter->invite_user_id;
- // update order actual commission balance
- $order->actual_commission_balance = $order->actual_commission_balance + $commissionBalance;
- }
- return true;
- }
-
-}
diff --git a/Xboard/app/Console/Commands/CheckOrder.php b/Xboard/app/Console/Commands/CheckOrder.php
deleted file mode 100644
index 7d03f58..0000000
--- a/Xboard/app/Console/Commands/CheckOrder.php
+++ /dev/null
@@ -1,53 +0,0 @@
-orderBy('created_at', 'ASC')
- ->lazyById(200)
- ->each(function ($order) {
- OrderHandleJob::dispatch($order->trade_no);
- });
- }
-}
diff --git a/Xboard/app/Console/Commands/CheckServer.php b/Xboard/app/Console/Commands/CheckServer.php
deleted file mode 100644
index 2d3dae9..0000000
--- a/Xboard/app/Console/Commands/CheckServer.php
+++ /dev/null
@@ -1,64 +0,0 @@
-checkOffline();
- }
-
- private function checkOffline()
- {
- $servers = ServerService::getAllServers();
- foreach ($servers as $server) {
- if ($server['parent_id']) continue;
- if ($server['last_check_at'] && (time() - $server['last_check_at']) > 1800) {
- $telegramService = new TelegramService();
- $message = sprintf(
- "节点掉线通知\r\n----\r\n节点名称:%s\r\n节点地址:%s\r\n",
- $server['name'],
- $server['host']
- );
- $telegramService->sendMessageWithAdmin($message);
- Cache::forget(CacheKey::get(sprintf("SERVER_%s_LAST_CHECK_AT", strtoupper($server['type'])), $server->id));
- }
- }
- }
-}
diff --git a/Xboard/app/Console/Commands/CheckTicket.php b/Xboard/app/Console/Commands/CheckTicket.php
deleted file mode 100644
index 51d4539..0000000
--- a/Xboard/app/Console/Commands/CheckTicket.php
+++ /dev/null
@@ -1,51 +0,0 @@
-where('updated_at', '<=', time() - 24 * 3600)
- ->where('reply_status', 0)
- ->lazyById(200)
- ->each(function ($ticket) {
- if ($ticket->user_id === $ticket->last_reply_user_id) return;
- $ticket->status = Ticket::STATUS_CLOSED;
- $ticket->save();
- });
- }
-}
diff --git a/Xboard/app/Console/Commands/CheckTrafficExceeded.php b/Xboard/app/Console/Commands/CheckTrafficExceeded.php
deleted file mode 100644
index 35a1db6..0000000
--- a/Xboard/app/Console/Commands/CheckTrafficExceeded.php
+++ /dev/null
@@ -1,63 +0,0 @@
-whereIn('id', $pendingUserIds)
- ->whereRaw('u + d >= transfer_enable')
- ->where('transfer_enable', '>', 0)
- ->where('banned', 0)
- ->select(['id', 'group_id'])
- ->get();
-
- if ($exceededUsers->isEmpty()) {
- return;
- }
-
- $groupedUsers = $exceededUsers->groupBy('group_id');
- $notifiedCount = 0;
-
- foreach ($groupedUsers as $groupId => $users) {
- if (!$groupId) {
- continue;
- }
-
- $userIdsInGroup = $users->pluck('id')->toArray();
- $servers = Server::whereJsonContains('group_ids', (string) $groupId)->get();
-
- foreach ($servers as $server) {
- if (!NodeSyncService::isNodeOnline($server->id)) {
- continue;
- }
-
- NodeSyncService::push($server->id, 'sync.user.delta', [
- 'action' => 'remove',
- 'users' => array_map(fn($id) => ['id' => $id], $userIdsInGroup),
- ]);
- $notifiedCount++;
- }
- }
-
- $this->info("Checked " . count($pendingUserIds) . " users, notified {$notifiedCount} nodes for " . $exceededUsers->count() . " exceeded users.");
- }
-}
diff --git a/Xboard/app/Console/Commands/ClearUser.php b/Xboard/app/Console/Commands/ClearUser.php
deleted file mode 100644
index cd64abe..0000000
--- a/Xboard/app/Console/Commands/ClearUser.php
+++ /dev/null
@@ -1,51 +0,0 @@
-where('transfer_enable', 0)
- ->where('expired_at', 0)
- ->where('last_login_at', NULL);
- $count = $builder->count();
- if ($builder->delete()) {
- $this->info("已删除{$count}位没有任何数据的用户");
- }
- }
-}
diff --git a/Xboard/app/Console/Commands/HookList.php b/Xboard/app/Console/Commands/HookList.php
deleted file mode 100644
index cec2bfd..0000000
--- a/Xboard/app/Console/Commands/HookList.php
+++ /dev/null
@@ -1,42 +0,0 @@
-filter(fn($f) => Str::endsWith($f, '.php'));
- foreach ($files as $file) {
- $content = @file_get_contents($file);
- if ($content && preg_match_all($pattern, $content, $matches)) {
- foreach ($matches[2] as $hook) {
- $hooks->push($hook);
- }
- }
- }
- }
- $hooks = $hooks->unique()->sort()->values();
- if ($hooks->isEmpty()) {
- $this->info('未扫描到任何 hook');
- } else {
- $this->info('All Supported Hooks:');
- foreach ($hooks as $hook) {
- $this->line(' ' . $hook);
- }
- }
- }
-}
\ No newline at end of file
diff --git a/Xboard/app/Console/Commands/MigrateFromV2b.php b/Xboard/app/Console/Commands/MigrateFromV2b.php
deleted file mode 100644
index ace9c85..0000000
--- a/Xboard/app/Console/Commands/MigrateFromV2b.php
+++ /dev/null
@@ -1,186 +0,0 @@
-argument('version');
- if($version === 'config'){
- $this->MigrateV2ConfigToV2Settings();
- return;
- }
-
- // Define your SQL commands based on versions
- $sqlCommands = [
- 'dev231027' => [
- // SQL commands for version Dev 2023/10/27
- 'ALTER TABLE v2_order ADD COLUMN surplus_order_ids TEXT NULL;',
- 'ALTER TABLE v2_plan DROP COLUMN daily_unit_price, DROP COLUMN transfer_unit_price;',
- 'ALTER TABLE v2_server_hysteria DROP COLUMN ignore_client_bandwidth, DROP COLUMN obfs_type;'
- ],
- '1.7.4' => [
- 'CREATE TABLE `v2_server_vless` (
- `id` INT AUTO_INCREMENT PRIMARY KEY,
- `group_id` TEXT NOT NULL,
- `route_id` TEXT NULL,
- `name` VARCHAR(255) NOT NULL,
- `parent_id` INT NULL,
- `host` VARCHAR(255) NOT NULL,
- `port` INT NOT NULL,
- `server_port` INT NOT NULL,
- `tls` BOOLEAN NOT NULL,
- `tls_settings` TEXT NULL,
- `flow` VARCHAR(64) NULL,
- `network` VARCHAR(11) NOT NULL,
- `network_settings` TEXT NULL,
- `tags` TEXT NULL,
- `rate` VARCHAR(11) NOT NULL,
- `show` BOOLEAN DEFAULT 0,
- `sort` INT NULL,
- `created_at` INT NOT NULL,
- `updated_at` INT NOT NULL
- );'
- ],
- '1.7.3' => [
- 'ALTER TABLE `v2_stat_order` RENAME TO `v2_stat`;',
- "ALTER TABLE `v2_stat` CHANGE COLUMN order_amount paid_total INT COMMENT '订单合计';",
- "ALTER TABLE `v2_stat` CHANGE COLUMN order_count paid_count INT COMMENT '邀请佣金';",
- "ALTER TABLE `v2_stat` CHANGE COLUMN commission_amount commission_total INT COMMENT '佣金合计';",
- "ALTER TABLE `v2_stat`
- ADD COLUMN order_count INT NULL,
- ADD COLUMN order_total INT NULL,
- ADD COLUMN register_count INT NULL,
- ADD COLUMN invite_count INT NULL,
- ADD COLUMN transfer_used_total VARCHAR(32) NULL;
- ",
- "CREATE TABLE `v2_log` (
- `id` INT AUTO_INCREMENT PRIMARY KEY,
- `title` TEXT NOT NULL,
- `level` VARCHAR(11) NULL,
- `host` VARCHAR(255) NULL,
- `uri` VARCHAR(255) NOT NULL,
- `method` VARCHAR(11) NOT NULL,
- `data` TEXT NULL,
- `ip` VARCHAR(128) NULL,
- `context` TEXT NULL,
- `created_at` INT NOT NULL,
- `updated_at` INT NOT NULL
- );",
- 'CREATE TABLE `v2_server_hysteria` (
- `id` INT AUTO_INCREMENT PRIMARY KEY,
- `group_id` VARCHAR(255) NOT NULL,
- `route_id` VARCHAR(255) NULL,
- `name` VARCHAR(255) NOT NULL,
- `parent_id` INT NULL,
- `host` VARCHAR(255) NOT NULL,
- `port` VARCHAR(11) NOT NULL,
- `server_port` INT NOT NULL,
- `tags` VARCHAR(255) NULL,
- `rate` VARCHAR(11) NOT NULL,
- `show` BOOLEAN DEFAULT FALSE,
- `sort` INT NULL,
- `up_mbps` INT NOT NULL,
- `down_mbps` INT NOT NULL,
- `server_name` VARCHAR(64) NULL,
- `insecure` BOOLEAN DEFAULT FALSE,
- `created_at` INT NOT NULL,
- `updated_at` INT NOT NULL
- );',
- "CREATE TABLE `v2_server_vless` (
- `id` INT AUTO_INCREMENT PRIMARY KEY,
- `group_id` TEXT NOT NULL,
- `route_id` TEXT NULL,
- `name` VARCHAR(255) NOT NULL,
- `parent_id` INT NULL,
- `host` VARCHAR(255) NOT NULL,
- `port` INT NOT NULL,
- `server_port` INT NOT NULL,
- `tls` BOOLEAN NOT NULL,
- `tls_settings` TEXT NULL,
- `flow` VARCHAR(64) NULL,
- `network` VARCHAR(11) NOT NULL,
- `network_settings` TEXT NULL,
- `tags` TEXT NULL,
- `rate` VARCHAR(11) NOT NULL,
- `show` BOOLEAN DEFAULT FALSE,
- `sort` INT NULL,
- `created_at` INT NOT NULL,
- `updated_at` INT NOT NULL
- );",
- ],
- 'wyx2685' => [
- "ALTER TABLE `v2_plan` DROP COLUMN `device_limit`;",
- "ALTER TABLE `v2_server_hysteria` DROP COLUMN `version`, DROP COLUMN `obfs`, DROP COLUMN `obfs_password`;",
- "ALTER TABLE `v2_server_trojan` DROP COLUMN `network`, DROP COLUMN `network_settings`;",
- "ALTER TABLE `v2_user` DROP COLUMN `device_limit`;"
- ]
- ];
-
- if (!$version) {
- $version = $this->choice('请选择你迁移前的V2board版本:', array_keys($sqlCommands));
- }
-
- if (array_key_exists($version, $sqlCommands)) {
-
- try {
- foreach ($sqlCommands[$version] as $sqlCommand) {
- // Execute SQL command
- DB::statement($sqlCommand);
- }
-
- $this->info('1️⃣、数据库差异矫正成功');
-
- // 初始化数据库迁移
- $this->call('db:seed', ['--class' => 'OriginV2bMigrationsTableSeeder']);
- $this->info('2️⃣、数据库迁移记录初始化成功');
-
- $this->call('xboard:update');
- $this->info('3️⃣、更新成功');
-
- $this->info("🎉:成功从 $version 迁移到Xboard");
- } catch (\Exception $e) {
- // An error occurred, rollback the transaction
- $this->error('迁移失败'. $e->getMessage() );
- }
-
-
- } else {
- $this->error("你所输入的版本未找到");
- }
- }
-
- public function MigrateV2ConfigToV2Settings()
- {
- Artisan::call('config:clear');
- $configValue = config('v2board') ?? [];
-
- foreach ($configValue as $k => $v) {
- // 检查记录是否已存在
- $existingSetting = Setting::where('name', $k)->first();
-
- // 如果记录不存在,则插入
- if ($existingSetting) {
- $this->warn("配置 {$k} 在数据库已经存在, 忽略");
- continue;
- }
- Setting::create([
- 'name' => $k,
- 'value' => is_array($v)? json_encode($v) : $v,
- ]);
- $this->info("配置 {$k} 迁移成功");
- }
- Artisan::call('config:cache');
-
- $this->info('所有配置迁移完成');
- }
-}
diff --git a/Xboard/app/Console/Commands/NodeWebSocketServer.php b/Xboard/app/Console/Commands/NodeWebSocketServer.php
deleted file mode 100644
index cbad533..0000000
--- a/Xboard/app/Console/Commands/NodeWebSocketServer.php
+++ /dev/null
@@ -1,34 +0,0 @@
-argument('action');
-
- $argv[1] = $action;
- if ($this->option('d')) {
- $argv[2] = '-d';
- }
-
- $host = $this->option('host');
- $port = $this->option('port');
-
- $worker = new NodeWorker($host, $port);
- $worker->run();
- }
-}
diff --git a/Xboard/app/Console/Commands/ResetLog.php b/Xboard/app/Console/Commands/ResetLog.php
deleted file mode 100644
index 89ddfb3..0000000
--- a/Xboard/app/Console/Commands/ResetLog.php
+++ /dev/null
@@ -1,48 +0,0 @@
-delete();
- StatServer::where('record_at', '<', strtotime('-2 month', time()))->delete();
- AdminAuditLog::where('created_at', '<', strtotime('-3 month', time()))->delete();
- }
-}
diff --git a/Xboard/app/Console/Commands/ResetPassword.php b/Xboard/app/Console/Commands/ResetPassword.php
deleted file mode 100644
index 3e131f2..0000000
--- a/Xboard/app/Console/Commands/ResetPassword.php
+++ /dev/null
@@ -1,55 +0,0 @@
-argument('password') ;
- $user = User::byEmail($this->argument('email'))->first();
- if (!$user) abort(500, '邮箱不存在');
- $password = $password ?? Helper::guid(false);
- $user->password = password_hash($password, PASSWORD_DEFAULT);
- $user->password_algo = null;
- if (!$user->save()) abort(500, '重置失败');
- $this->info("!!!重置成功!!!");
- $this->info("新密码为:{$password},请尽快修改密码。");
- }
-}
diff --git a/Xboard/app/Console/Commands/ResetTraffic.php b/Xboard/app/Console/Commands/ResetTraffic.php
deleted file mode 100644
index 5152077..0000000
--- a/Xboard/app/Console/Commands/ResetTraffic.php
+++ /dev/null
@@ -1,289 +0,0 @@
-option('fix-null');
- $force = $this->option('force');
-
- $this->info('🚀 开始执行流量重置任务...');
-
- if ($fixNull) {
- $this->warn('🔧 修正模式 - 将重新计算next_reset_at为null的用户');
- } elseif ($force) {
- $this->warn('⚡ 强制模式 - 将重新计算所有用户的重置时间');
- }
-
- try {
- $result = $fixNull ? $this->performFix() : ($force ? $this->performForce() : $this->performReset());
- $this->displayResults($result, $fixNull || $force);
- return self::SUCCESS;
-
- } catch (\Exception $e) {
- $this->error("❌ 任务执行失败: {$e->getMessage()}");
-
- Log::error('流量重置命令执行失败', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString(),
- ]);
-
- return self::FAILURE;
- }
- }
-
- private function displayResults(array $result, bool $isSpecialMode): void
- {
- $this->info("✅ 任务完成!\n");
-
- if ($isSpecialMode) {
- $this->displayFixResults($result);
- } else {
- $this->displayExecutionResults($result);
- }
- }
-
- private function displayFixResults(array $result): void
- {
- $this->info("📊 修正结果统计:");
- $this->info("🔍 发现用户总数: {$result['total_found']}");
- $this->info("✅ 成功修正数量: {$result['total_fixed']}");
- $this->info("⏱️ 总执行时间: {$result['duration']} 秒");
-
- if ($result['error_count'] > 0) {
- $this->warn("⚠️ 错误数量: {$result['error_count']}");
- $this->warn("详细错误信息请查看日志");
- } else {
- $this->info("✨ 无错误发生");
- }
-
- if ($result['total_found'] > 0) {
- $avgTime = round($result['duration'] / $result['total_found'], 4);
- $this->info("⚡ 平均处理速度: {$avgTime} 秒/用户");
- }
- }
-
-
-
- private function displayExecutionResults(array $result): void
- {
- $this->info("📊 执行结果统计:");
- $this->info("👥 处理用户总数: {$result['total_processed']}");
- $this->info("🔄 重置用户数量: {$result['total_reset']}");
- $this->info("⏱️ 总执行时间: {$result['duration']} 秒");
-
- if ($result['error_count'] > 0) {
- $this->warn("⚠️ 错误数量: {$result['error_count']}");
- $this->warn("详细错误信息请查看日志");
- } else {
- $this->info("✨ 无错误发生");
- }
-
- if ($result['total_processed'] > 0) {
- $avgTime = round($result['duration'] / $result['total_processed'], 4);
- $this->info("⚡ 平均处理速度: {$avgTime} 秒/用户");
- }
- }
-
- private function performReset(): array
- {
- $startTime = microtime(true);
- $totalResetCount = 0;
- $errors = [];
-
- $users = $this->getResetQuery()->get();
-
- if ($users->isEmpty()) {
- $this->info("😴 当前没有需要重置的用户");
- return [
- 'total_processed' => 0,
- 'total_reset' => 0,
- 'error_count' => 0,
- 'duration' => round(microtime(true) - $startTime, 2),
- ];
- }
-
- $this->info("找到 {$users->count()} 个需要重置的用户");
-
- foreach ($users as $user) {
- try {
- $totalResetCount += (int) $this->trafficResetService->checkAndReset($user, TrafficResetLog::SOURCE_CRON);
- } catch (\Exception $e) {
- $errors[] = [
- 'user_id' => $user->id,
- 'email' => $user->email,
- 'error' => $e->getMessage(),
- ];
- Log::error('用户流量重置失败', [
- 'user_id' => $user->id,
- 'error' => $e->getMessage(),
- ]);
- }
- }
-
- return [
- 'total_processed' => $users->count(),
- 'total_reset' => $totalResetCount,
- 'error_count' => count($errors),
- 'duration' => round(microtime(true) - $startTime, 2),
- ];
- }
-
- private function performFix(): array
- {
- $startTime = microtime(true);
- $nullUsers = $this->getNullResetTimeUsers();
-
- if ($nullUsers->isEmpty()) {
- $this->info("✅ 没有发现next_reset_at为null的用户");
- return [
- 'total_found' => 0,
- 'total_fixed' => 0,
- 'error_count' => 0,
- 'duration' => round(microtime(true) - $startTime, 2),
- ];
- }
-
- $this->info("🔧 发现 {$nullUsers->count()} 个next_reset_at为null的用户,开始修正...");
-
- $fixedCount = 0;
- $errors = [];
-
- foreach ($nullUsers as $user) {
- try {
- $nextResetTime = $this->trafficResetService->calculateNextResetTime($user);
- if ($nextResetTime) {
- $user->next_reset_at = $nextResetTime->timestamp;
- $user->save();
- $fixedCount++;
- }
- } catch (\Exception $e) {
- $errors[] = [
- 'user_id' => $user->id,
- 'email' => $user->email,
- 'error' => $e->getMessage(),
- ];
- Log::error('修正用户next_reset_at失败', [
- 'user_id' => $user->id,
- 'error' => $e->getMessage(),
- ]);
- }
- }
-
- return [
- 'total_found' => $nullUsers->count(),
- 'total_fixed' => $fixedCount,
- 'error_count' => count($errors),
- 'duration' => round(microtime(true) - $startTime, 2),
- ];
- }
-
- private function performForce(): array
- {
- $startTime = microtime(true);
- $allUsers = $this->getAllUsers();
-
- if ($allUsers->isEmpty()) {
- $this->info("✅ 没有发现需要处理的用户");
- return [
- 'total_found' => 0,
- 'total_fixed' => 0,
- 'error_count' => 0,
- 'duration' => round(microtime(true) - $startTime, 2),
- ];
- }
-
- $this->info("⚡ 发现 {$allUsers->count()} 个用户,开始重新计算重置时间...");
-
- $fixedCount = 0;
- $errors = [];
-
- foreach ($allUsers as $user) {
- try {
- $nextResetTime = $this->trafficResetService->calculateNextResetTime($user);
- if ($nextResetTime) {
- $user->next_reset_at = $nextResetTime->timestamp;
- $user->save();
- $fixedCount++;
- }
- } catch (\Exception $e) {
- $errors[] = [
- 'user_id' => $user->id,
- 'email' => $user->email,
- 'error' => $e->getMessage(),
- ];
- Log::error('强制重新计算用户next_reset_at失败', [
- 'user_id' => $user->id,
- 'error' => $e->getMessage(),
- ]);
- }
- }
-
- return [
- 'total_found' => $allUsers->count(),
- 'total_fixed' => $fixedCount,
- 'error_count' => count($errors),
- 'duration' => round(microtime(true) - $startTime, 2),
- ];
- }
-
-
-
- private function getResetQuery()
- {
- return User::where('next_reset_at', '<=', time())
- ->whereNotNull('next_reset_at')
- ->where(function ($query) {
- $query->where('expired_at', '>', time())
- ->orWhereNull('expired_at');
- })
- ->where('banned', 0)
- ->whereNotNull('plan_id');
- }
-
-
-
- private function getNullResetTimeUsers()
- {
- return User::whereNull('next_reset_at')
- ->whereNotNull('plan_id')
- ->where(function ($query) {
- $query->where('expired_at', '>', time())
- ->orWhereNull('expired_at');
- })
- ->where('banned', 0)
- ->with('plan:id,name,reset_traffic_method')
- ->get();
- }
-
- private function getAllUsers()
- {
- return User::whereNotNull('plan_id')
- ->where(function ($query) {
- $query->where('expired_at', '>', time())
- ->orWhereNull('expired_at');
- })
- ->where('banned', 0)
- ->with('plan:id,name,reset_traffic_method')
- ->get();
- }
-
-}
\ No newline at end of file
diff --git a/Xboard/app/Console/Commands/ResetUser.php b/Xboard/app/Console/Commands/ResetUser.php
deleted file mode 100644
index 51197ae..0000000
--- a/Xboard/app/Console/Commands/ResetUser.php
+++ /dev/null
@@ -1,58 +0,0 @@
-confirm("确定要重置所有用户安全信息吗?")) {
- return;
- }
- ini_set('memory_limit', -1);
- $users = User::all();
- foreach ($users as $user)
- {
- $user->token = Helper::guid();
- $user->uuid = Helper::guid(true);
- $user->save();
- $this->info("已重置用户{$user->email}的安全信息");
- }
- }
-}
diff --git a/Xboard/app/Console/Commands/SendRemindMail.php b/Xboard/app/Console/Commands/SendRemindMail.php
deleted file mode 100644
index 5fdb2d1..0000000
--- a/Xboard/app/Console/Commands/SendRemindMail.php
+++ /dev/null
@@ -1,103 +0,0 @@
-warn('邮件提醒功能未启用');
- return 0;
- }
-
- $chunkSize = max(100, min(2000, (int) $this->option('chunk-size')));
- $mailService = new MailService();
-
- $totalUsers = $mailService->getTotalUsersNeedRemind();
- if ($totalUsers === 0) {
- $this->info('没有需要发送提醒邮件的用户');
- return 0;
- }
-
- $this->displayInfo($totalUsers, $chunkSize);
-
- if (!$this->option('force') && !$this->confirm("确定要发送提醒邮件给 {$totalUsers} 个用户吗?")) {
- return 0;
- }
-
- $startTime = microtime(true);
- $progressBar = $this->output->createProgressBar((int) ceil($totalUsers / $chunkSize));
- $progressBar->start();
-
- $statistics = $mailService->processUsersInChunks($chunkSize, function () use ($progressBar) {
- $progressBar->advance();
- });
-
- $progressBar->finish();
- $this->newLine();
-
- $this->displayResults($statistics, microtime(true) - $startTime);
- $this->logResults($statistics);
-
- return 0;
- }
-
- private function displayInfo(int $totalUsers, int $chunkSize): void
- {
- $this->table(['项目', '值'], [
- ['需要处理的用户', number_format($totalUsers)],
- ['批次大小', $chunkSize],
- ['预计批次', ceil($totalUsers / $chunkSize)],
- ]);
- }
-
- private function displayResults(array $stats, float $duration): void
- {
- $this->info('✅ 提醒邮件发送完成!');
-
- $this->table(['统计项', '数量'], [
- ['总处理用户', number_format($stats['processed_users'])],
- ['过期提醒邮件', number_format($stats['expire_emails'])],
- ['流量提醒邮件', number_format($stats['traffic_emails'])],
- ['跳过用户', number_format($stats['skipped'])],
- ['错误数量', number_format($stats['errors'])],
- ['总耗时', round($duration, 2) . ' 秒'],
- ['平均速度', round($stats['processed_users'] / max($duration, 0.1), 1) . ' 用户/秒'],
- ]);
-
- if ($stats['errors'] > 0) {
- $this->warn("⚠️ 有 {$stats['errors']} 个用户的邮件发送失败,请检查日志");
- }
- }
-
- private function logResults(array $statistics): void
- {
- Log::info('SendRemindMail命令执行完成', ['statistics' => $statistics]);
- }
-}
diff --git a/Xboard/app/Console/Commands/Test.php b/Xboard/app/Console/Commands/Test.php
deleted file mode 100644
index 667e616..0000000
--- a/Xboard/app/Console/Commands/Test.php
+++ /dev/null
@@ -1,41 +0,0 @@
-info("__ __ ____ _ ");
- $this->info("\ \ / /| __ ) ___ __ _ _ __ __| | ");
- $this->info(" \ \/ / | __ \ / _ \ / _` | '__/ _` | ");
- $this->info(" / /\ \ | |_) | (_) | (_| | | | (_| | ");
- $this->info("/_/ \_\|____/ \___/ \__,_|_| \__,_| ");
- if (
- (File::exists(base_path() . '/.env') && $this->getEnvValue('INSTALLED'))
- || (getenv('INSTALLED', false) && $isDocker)
- ) {
- $securePath = admin_setting('secure_path', admin_setting('frontend_admin_path', hash('crc32b', config('app.key'))));
- $this->info("访问 http(s)://你的站点/{$securePath} 进入管理面板,你可以在用户中心修改你的密码。");
- $this->warn("如需重新安装请清空目录下 .env 文件的内容(Docker安装方式不可以删除此文件)");
- $this->warn("快捷清空.env命令:");
- note('rm .env && touch .env');
- return;
- }
- if (is_dir(base_path() . '/.env')) {
- $this->error('😔:安装失败,Docker环境下安装请保留空的 .env 文件');
- return;
- }
- // 选择数据库类型
- $dbType = $enableSqlite ? 'sqlite' : select(
- label: '请选择数据库类型',
- options: [
- 'sqlite' => 'SQLite (无需额外安装)',
- 'mysql' => 'MySQL',
- 'postgresql' => 'PostgreSQL'
- ],
- default: 'sqlite'
- );
-
- // 使用 match 表达式配置数据库
- $envConfig = match ($dbType) {
- 'sqlite' => $this->configureSqlite(),
- 'mysql' => $this->configureMysql(),
- 'postgresql' => $this->configurePostgresql(),
- default => throw new \InvalidArgumentException("不支持的数据库类型: {$dbType}")
- };
-
- if (is_null($envConfig)) {
- return; // 用户选择退出安装
- }
- $envConfig['APP_KEY'] = 'base64:' . base64_encode(Encrypter::generateKey('AES-256-CBC'));
- $isReidsValid = false;
- while (!$isReidsValid) {
- // 判断是否为Docker环境
- if ($isDocker == 'true' && ($enableRedis || confirm(label: '是否启用Docker内置的Redis', default: true, yes: '启用', no: '不启用'))) {
- $envConfig['REDIS_HOST'] = '/data/redis.sock';
- $envConfig['REDIS_PORT'] = 0;
- $envConfig['REDIS_PASSWORD'] = null;
- } else {
- $envConfig['REDIS_HOST'] = text(label: '请输入Redis地址', default: '127.0.0.1', required: true);
- $envConfig['REDIS_PORT'] = text(label: '请输入Redis端口', default: '6379', required: true);
- $envConfig['REDIS_PASSWORD'] = text(label: '请输入redis密码(默认: null)', default: '');
- }
- $redisConfig = [
- 'client' => 'phpredis',
- 'default' => [
- 'host' => $envConfig['REDIS_HOST'],
- 'password' => $envConfig['REDIS_PASSWORD'],
- 'port' => $envConfig['REDIS_PORT'],
- 'database' => 0,
- ],
- ];
- try {
- $redis = new \Illuminate\Redis\RedisManager(app(), 'phpredis', $redisConfig);
- $redis->ping();
- $isReidsValid = true;
- } catch (\Exception $e) {
- // 连接失败,输出错误消息
- $this->error("redis连接失败:" . $e->getMessage());
- $this->info("请重新输入REDIS配置");
- $enableRedis = false;
- sleep(1);
- }
- }
-
- if (!copy(base_path() . '/.env.example', base_path() . '/.env')) {
- abort(500, '复制环境文件失败,请检查目录权限');
- }
- ;
- $email = !empty($adminAccount) ? $adminAccount : text(
- label: '请输入管理员账号',
- default: 'admin@demo.com',
- required: true,
- validate: fn(string $email): ?string => match (true) {
- !filter_var($email, FILTER_VALIDATE_EMAIL) => '请输入有效的邮箱地址.',
- default => null,
- }
- );
- $password = Helper::guid(false);
- $this->saveToEnv($envConfig);
-
- $this->call('config:cache');
- Artisan::call('cache:clear');
- $this->info('正在导入数据库请稍等...');
- Artisan::call("migrate", ['--force' => true]);
- $this->info(Artisan::output());
- $this->info('数据库导入完成');
- $this->info('开始注册管理员账号');
- if (!self::registerAdmin($email, $password)) {
- abort(500, '管理员账号注册失败,请重试');
- }
- self::restoreProtectedPlugins($this);
- $this->info('正在安装默认插件...');
- PluginManager::installDefaultPlugins();
- $this->info('默认插件安装完成');
-
- $this->info('🎉:一切就绪');
- $this->info("管理员邮箱:{$email}");
- $this->info("管理员密码:{$password}");
-
- $defaultSecurePath = hash('crc32b', config('app.key'));
- $this->info("访问 http(s)://你的站点/{$defaultSecurePath} 进入管理面板,你可以在用户中心修改你的密码。");
- $envConfig['INSTALLED'] = true;
- $this->saveToEnv($envConfig);
- } catch (\Exception $e) {
- $this->error($e);
- }
- }
-
- public static function registerAdmin($email, $password)
- {
- $user = new User();
- $user->email = $email;
- if (strlen($password) < 8) {
- abort(500, '管理员密码长度最小为8位字符');
- }
- $user->password = password_hash($password, PASSWORD_DEFAULT);
- $user->uuid = Helper::guid(true);
- $user->token = Helper::guid();
- $user->is_admin = 1;
- return $user->save();
- }
-
- private function set_env_var($key, $value)
- {
- $value = !strpos($value, ' ') ? $value : '"' . $value . '"';
- $key = strtoupper($key);
-
- $envPath = app()->environmentFilePath();
- $contents = file_get_contents($envPath);
-
- if (preg_match("/^{$key}=[^\r\n]*/m", $contents, $matches)) {
- $contents = str_replace($matches[0], "{$key}={$value}", $contents);
- } else {
- $contents .= "\n{$key}={$value}\n";
- }
-
- return file_put_contents($envPath, $contents) !== false;
- }
-
- private function saveToEnv($data = [])
- {
- foreach ($data as $key => $value) {
- self::set_env_var($key, $value);
- }
- return true;
- }
-
- function getEnvValue($key, $default = null)
- {
- $dotenv = \Dotenv\Dotenv::createImmutable(base_path());
- $dotenv->load();
-
- return Env::get($key, $default);
- }
-
- /**
- * 配置 SQLite 数据库
- *
- * @return array|null
- */
- private function configureSqlite(): ?array
- {
- $sqliteFile = '.docker/.data/database.sqlite';
- if (!file_exists(base_path($sqliteFile))) {
- // 创建空文件
- if (!touch(base_path($sqliteFile))) {
- $this->info("sqlite创建成功: $sqliteFile");
- }
- }
-
- $envConfig = [
- 'DB_CONNECTION' => 'sqlite',
- 'DB_DATABASE' => $sqliteFile,
- 'DB_HOST' => '',
- 'DB_USERNAME' => '',
- 'DB_PASSWORD' => '',
- ];
-
- try {
- Config::set("database.default", 'sqlite');
- Config::set("database.connections.sqlite.database", base_path($envConfig['DB_DATABASE']));
- DB::purge('sqlite');
- DB::connection('sqlite')->getPdo();
-
- if (!blank(DB::connection('sqlite')->getPdo()->query("SELECT name FROM sqlite_master WHERE type='table'")->fetchAll(\PDO::FETCH_COLUMN))) {
- if (confirm(label: '检测到数据库中已经存在数据,是否要清空数据库以便安装新的数据?', default: false, yes: '清空', no: '退出安装')) {
- $this->info('正在清空数据库请稍等');
- $this->call('db:wipe', ['--force' => true]);
- $this->info('数据库清空完成');
- } else {
- return null;
- }
- }
- } catch (\Exception $e) {
- $this->error("SQLite数据库连接失败:" . $e->getMessage());
- return null;
- }
-
- return $envConfig;
- }
-
- /**
- * 配置 MySQL 数据库
- *
- * @return array
- */
- private function configureMysql(): array
- {
- while (true) {
- $envConfig = [
- 'DB_CONNECTION' => 'mysql',
- 'DB_HOST' => text(label: "请输入MySQL数据库地址", default: '127.0.0.1', required: true),
- 'DB_PORT' => text(label: '请输入MySQL数据库端口', default: '3306', required: true),
- 'DB_DATABASE' => text(label: '请输入MySQL数据库名', default: 'xboard', required: true),
- 'DB_USERNAME' => text(label: '请输入MySQL数据库用户名', default: 'root', required: true),
- 'DB_PASSWORD' => text(label: '请输入MySQL数据库密码', required: false),
- ];
-
- try {
- Config::set("database.default", 'mysql');
- Config::set("database.connections.mysql.host", $envConfig['DB_HOST']);
- Config::set("database.connections.mysql.port", $envConfig['DB_PORT']);
- Config::set("database.connections.mysql.database", $envConfig['DB_DATABASE']);
- Config::set("database.connections.mysql.username", $envConfig['DB_USERNAME']);
- Config::set("database.connections.mysql.password", $envConfig['DB_PASSWORD']);
- DB::purge('mysql');
- DB::connection('mysql')->getPdo();
-
- if (!blank(DB::connection('mysql')->select('SHOW TABLES'))) {
- if (confirm(label: '检测到数据库中已经存在数据,是否要清空数据库以便安装新的数据?', default: false, yes: '清空', no: '不清空')) {
- $this->info('正在清空数据库请稍等');
- $this->call('db:wipe', ['--force' => true]);
- $this->info('数据库清空完成');
- return $envConfig;
- } else {
- continue; // 重新输入配置
- }
- }
-
- return $envConfig;
- } catch (\Exception $e) {
- $this->error("MySQL数据库连接失败:" . $e->getMessage());
- $this->info("请重新输入MySQL数据库配置");
- }
- }
- }
-
- /**
- * 配置 PostgreSQL 数据库
- *
- * @return array
- */
- private function configurePostgresql(): array
- {
- while (true) {
- $envConfig = [
- 'DB_CONNECTION' => 'pgsql',
- 'DB_HOST' => text(label: "请输入PostgreSQL数据库地址", default: '127.0.0.1', required: true),
- 'DB_PORT' => text(label: '请输入PostgreSQL数据库端口', default: '5432', required: true),
- 'DB_DATABASE' => text(label: '请输入PostgreSQL数据库名', default: 'xboard', required: true),
- 'DB_USERNAME' => text(label: '请输入PostgreSQL数据库用户名', default: 'postgres', required: true),
- 'DB_PASSWORD' => text(label: '请输入PostgreSQL数据库密码', required: false),
- ];
-
- try {
- Config::set("database.default", 'pgsql');
- Config::set("database.connections.pgsql.host", $envConfig['DB_HOST']);
- Config::set("database.connections.pgsql.port", $envConfig['DB_PORT']);
- Config::set("database.connections.pgsql.database", $envConfig['DB_DATABASE']);
- Config::set("database.connections.pgsql.username", $envConfig['DB_USERNAME']);
- Config::set("database.connections.pgsql.password", $envConfig['DB_PASSWORD']);
- DB::purge('pgsql');
- DB::connection('pgsql')->getPdo();
-
- // 检查PostgreSQL数据库是否有表
- $tables = DB::connection('pgsql')->select("SELECT tablename FROM pg_tables WHERE schemaname = 'public'");
- if (!blank($tables)) {
- if (confirm(label: '检测到数据库中已经存在数据,是否要清空数据库以便安装新的数据?', default: false, yes: '清空', no: '不清空')) {
- $this->info('正在清空数据库请稍等');
- $this->call('db:wipe', ['--force' => true]);
- $this->info('数据库清空完成');
- return $envConfig;
- } else {
- continue; // 重新输入配置
- }
- }
-
- return $envConfig;
- } catch (\Exception $e) {
- $this->error("PostgreSQL数据库连接失败:" . $e->getMessage());
- $this->info("请重新输入PostgreSQL数据库配置");
- }
- }
- }
-
- /**
- * 还原内置受保护插件(可在安装和更新时调用)
- * Docker 部署时 plugins/ 目录被外部挂载覆盖,需要从镜像备份中还原默认插件
- */
- public static function restoreProtectedPlugins(Command $console = null)
- {
- $backupBase = '/opt/default-plugins';
- $pluginsBase = base_path('plugins');
-
- if (!File::isDirectory($backupBase)) {
- $console?->info('非 Docker 环境或备份目录不存在,跳过插件还原。');
- return;
- }
-
- foreach (Plugin::PROTECTED_PLUGINS as $pluginCode) {
- $dirName = Str::studly($pluginCode);
- $source = "{$backupBase}/{$dirName}";
- $target = "{$pluginsBase}/{$dirName}";
-
- if (!File::isDirectory($source)) {
- continue;
- }
-
- // 先清除旧文件再复制,避免重命名后残留旧文件
- File::deleteDirectory($target);
- File::copyDirectory($source, $target);
- $console?->info("已同步默认插件 [{$dirName}]");
- }
- }
-}
diff --git a/Xboard/app/Console/Commands/XboardRollback.php b/Xboard/app/Console/Commands/XboardRollback.php
deleted file mode 100644
index f9e5f46..0000000
--- a/Xboard/app/Console/Commands/XboardRollback.php
+++ /dev/null
@@ -1,45 +0,0 @@
-info('正在回滚数据库请稍等...');
- \Artisan::call("migrate:rollback");
- $this->info(\Artisan::output());
- }
-}
diff --git a/Xboard/app/Console/Commands/XboardStatistics.php b/Xboard/app/Console/Commands/XboardStatistics.php
deleted file mode 100644
index 0bd4736..0000000
--- a/Xboard/app/Console/Commands/XboardStatistics.php
+++ /dev/null
@@ -1,75 +0,0 @@
-statUser();
- // $this->statServer();
- $this->stat();
- info('统计任务执行完毕。耗时:' . (microtime(true) - $startAt) / 1000);
- }
-
-
- private function stat()
- {
- try {
- $endAt = strtotime(date('Y-m-d'));
- $startAt = strtotime('-1 day', $endAt);
- $statisticalService = new StatisticalService();
- $statisticalService->setStartAt($startAt);
- $statisticalService->setEndAt($endAt);
- $data = $statisticalService->generateStatData();
- $data['record_at'] = $startAt;
- $data['record_type'] = 'd';
- $statistic = Stat::where('record_at', $startAt)
- ->where('record_type', 'd')
- ->first();
- if ($statistic) {
- $statistic->update($data);
- return;
- }
- Stat::create($data);
- } catch (\Exception $e) {
- Log::error($e->getMessage(), ['exception' => $e]);
- }
- }
-}
diff --git a/Xboard/app/Console/Commands/XboardUpdate.php b/Xboard/app/Console/Commands/XboardUpdate.php
deleted file mode 100644
index 65c95da..0000000
--- a/Xboard/app/Console/Commands/XboardUpdate.php
+++ /dev/null
@@ -1,65 +0,0 @@
-info('正在导入数据库请稍等...');
- Artisan::call("migrate", ['--force' => true]);
- $this->info(Artisan::output());
- $this->info('正在检查内置插件文件...');
- XboardInstall::restoreProtectedPlugins($this);
- $this->info('正在检查并安装默认插件...');
- PluginManager::installDefaultPlugins();
- $this->info('默认插件检查完成');
- // Artisan::call('reset:traffic', ['--fix-null' => true]);
- $this->info('正在重新计算所有用户的重置时间...');
- Artisan::call('reset:traffic', ['--force' => true]);
- $updateService = new UpdateService();
- $updateService->updateVersionCache();
- $themeService = app(ThemeService::class);
- $themeService->refreshCurrentTheme();
- Artisan::call('horizon:terminate');
- $this->info('更新完毕,队列服务已重启,你无需进行任何操作。');
- }
-}
diff --git a/Xboard/app/Console/Kernel.php b/Xboard/app/Console/Kernel.php
deleted file mode 100644
index 77b1810..0000000
--- a/Xboard/app/Console/Kernel.php
+++ /dev/null
@@ -1,68 +0,0 @@
-command('xboard:statistics')->dailyAt('0:10')->onOneServer();
- // check
- $schedule->command('check:order')->everyMinute()->onOneServer()->withoutOverlapping(5);
- $schedule->command('check:commission')->everyMinute()->onOneServer()->withoutOverlapping(5);
- $schedule->command('check:ticket')->everyMinute()->onOneServer()->withoutOverlapping(5);
- $schedule->command('check:traffic-exceeded')->everyMinute()->onOneServer()->withoutOverlapping(10)->runInBackground();
- // reset
- $schedule->command('reset:traffic')->everyMinute()->onOneServer()->withoutOverlapping(10);
- $schedule->command('reset:log')->daily()->onOneServer();
- // send
- $schedule->command('send:remindMail', ['--force'])->dailyAt('11:30')->onOneServer();
- // horizon metrics
- $schedule->command('horizon:snapshot')->everyFiveMinutes()->onOneServer();
- // backup Timing
- // if (env('ENABLE_AUTO_BACKUP_AND_UPDATE', false)) {
- // $schedule->command('backup:database', ['true'])->daily()->onOneServer();
- // }
- app(PluginManager::class)->registerPluginSchedules($schedule);
-
- }
-
- /**
- * Register the commands for the application.
- *
- * @return void
- */
- protected function commands()
- {
- $this->load(__DIR__ . '/Commands');
-
- try {
- app(PluginManager::class)->initializeEnabledPlugins();
- } catch (\Exception $e) {
- }
- require base_path('routes/console.php');
- }
-}
diff --git a/Xboard/app/Contracts/PaymentInterface.php b/Xboard/app/Contracts/PaymentInterface.php
deleted file mode 100644
index 2661362..0000000
--- a/Xboard/app/Contracts/PaymentInterface.php
+++ /dev/null
@@ -1,10 +0,0 @@
-message = $message;
- $this->code = $code;
- $this->errors = $errors;
- }
- public function errors(){
- return $this->errors;
- }
-
-}
diff --git a/Xboard/app/Exceptions/BusinessException.php b/Xboard/app/Exceptions/BusinessException.php
deleted file mode 100644
index 2079495..0000000
--- a/Xboard/app/Exceptions/BusinessException.php
+++ /dev/null
@@ -1,19 +0,0 @@
->
- */
- protected $dontReport = [
- ApiException::class,
- InterceptResponseException::class
- ];
-
- /**
- * A list of the inputs that are never flashed for validation exceptions.
- *
- * @var array
- */
- protected $dontFlash = [
- 'password',
- 'password_confirmation',
- ];
-
- /**
- * Report or log an exception.
- *
- * @param \Throwable $exception
- * @return void
- *
- * @throws \Throwable
- */
- public function report(Throwable $exception)
- {
- parent::report($exception);
- }
-
- /**
- * Render an exception into an HTTP response.
- *
- * @param \Illuminate\Http\Request $request
- * @param \Throwable $exception
- * @return \Symfony\Component\HttpFoundation\Response
- *
- * @throws \Throwable
- */
- public function render($request, Throwable $exception)
- {
- if ($exception instanceof ViewException) {
- return $this->fail([500, '主题渲染失败。如更新主题,参数可能发生变化请重新配置主题后再试。']);
- }
- // ApiException主动抛出错误
- if ($exception instanceof ApiException) {
- $code = $exception->getCode();
- $message = $exception->getMessage();
- $errors = $exception->errors();
- return $this->fail([$code, $message],null,$errors);
- }
- return parent::render($request, $exception);
- }
-
- /**
- * Register the exception handling callbacks for the application.
- */
- public function register(): void
- {
- $this->reportable(function (Throwable $e) {
- //
- });
-
- $this->renderable(function (InterceptResponseException $e) {
- return $e->getResponse();
- });
- }
-
- protected function convertExceptionToArray(Throwable $e)
- {
- return config('app.debug') ? [
- 'message' => $e->getMessage(),
- 'exception' => get_class($e),
- 'file' => $e->getFile(),
- 'line' => $e->getLine(),
- 'trace' => collect($e->getTrace())->map(function ($trace) {
- return Arr::except($trace, ['args']);
- })->all(),
- ] : [
- 'message' => $this->isHttpException($e) ? $e->getMessage() : __("Uh-oh, we've had some problems, we're working on it."),
- ];
- }
-}
diff --git a/Xboard/app/Helpers/ApiResponse.php b/Xboard/app/Helpers/ApiResponse.php
deleted file mode 100644
index 4820eb4..0000000
--- a/Xboard/app/Helpers/ApiResponse.php
+++ /dev/null
@@ -1,79 +0,0 @@
-jsonResponse('success', $codeResponse, $data, null);
- }
-
- /**
- * 失败
- * @param array $codeResponse
- * @param mixed $data
- * @param mixed $error
- * @return JsonResponse
- */
- public function fail($codeResponse = ResponseEnum::HTTP_ERROR, $data = null, $error = null): JsonResponse
- {
- return $this->jsonResponse('fail', $codeResponse, $data, $error);
- }
-
- /**
- * json响应
- * @param $status
- * @param $codeResponse
- * @param $data
- * @param $error
- * @return JsonResponse
- */
- private function jsonResponse($status, $codeResponse, $data, $error): JsonResponse
- {
- list($code, $message) = $codeResponse;
- return response()
- ->json([
- 'status' => $status,
- // 'code' => $code,
- 'message' => $message,
- 'data' => $data ?? null,
- 'error' => $error,
- ], (int) substr(((string) $code), 0, 3));
- }
-
-
- public function paginate(LengthAwarePaginator $page)
- {
- return response()->json([
- 'total' => $page->total(),
- 'current_page' => $page->currentPage(),
- 'per_page' => $page->perPage(),
- 'last_page' => $page->lastPage(),
- 'data' => $page->items()
- ]);
- }
-
- /**
- * 业务异常返回
- * @param array $codeResponse
- * @param string $info
- * @throws BusinessException
- */
- public function throwBusinessException(array $codeResponse = ResponseEnum::HTTP_ERROR, string $info = '')
- {
- throw new BusinessException($codeResponse, $info);
- }
-}
\ No newline at end of file
diff --git a/Xboard/app/Helpers/Functions.php b/Xboard/app/Helpers/Functions.php
deleted file mode 100644
index 282d62e..0000000
--- a/Xboard/app/Helpers/Functions.php
+++ /dev/null
@@ -1,82 +0,0 @@
-toArray();
- }
-
- if (is_array($key)) {
- $setting->save($key);
- return '';
- }
-
- $default = config('v2board.' . $key) ?? $default;
- return $setting->get($key) ?? $default;
- }
-}
-
-if (!function_exists('subscribe_template')) {
- /**
- * Get subscribe template content by protocol name.
- */
- function subscribe_template(string $name): ?string
- {
- return \App\Models\SubscribeTemplate::getContent($name);
- }
-}
-
-if (!function_exists('admin_settings_batch')) {
- /**
- * 批量获取配置参数,性能优化版本
- *
- * @param array $keys 配置键名数组
- * @return array 返回键值对数组
- */
- function admin_settings_batch(array $keys): array
- {
- return app(Setting::class)->getBatch($keys);
- }
-}
-
-if (!function_exists('source_base_url')) {
- /**
- * 获取来源基础URL,优先Referer,其次Host
- * @param string $path
- * @return string
- */
- function source_base_url(string $path = ''): string
- {
- $baseUrl = '';
- $referer = request()->header('Referer');
-
- if ($referer) {
- $parsedUrl = parse_url($referer);
- if (isset($parsedUrl['scheme']) && isset($parsedUrl['host'])) {
- $baseUrl = $parsedUrl['scheme'] . '://' . $parsedUrl['host'];
- if (isset($parsedUrl['port'])) {
- $baseUrl .= ':' . $parsedUrl['port'];
- }
- }
- }
-
- if (!$baseUrl) {
- $baseUrl = request()->getSchemeAndHttpHost();
- }
-
- $baseUrl = rtrim($baseUrl, '/');
- $path = ltrim($path, '/');
- return $baseUrl . '/' . $path;
- }
-}
diff --git a/Xboard/app/Helpers/ResponseEnum.php b/Xboard/app/Helpers/ResponseEnum.php
deleted file mode 100644
index f749ad3..0000000
--- a/Xboard/app/Helpers/ResponseEnum.php
+++ /dev/null
@@ -1,81 +0,0 @@
-isPluginEnabled()) {
- return [400, '插件未启用'];
- }
- return null;
- }
-}
\ No newline at end of file
diff --git a/Xboard/app/Http/Controllers/V1/Client/AppController.php b/Xboard/app/Http/Controllers/V1/Client/AppController.php
deleted file mode 100644
index 4637b08..0000000
--- a/Xboard/app/Http/Controllers/V1/Client/AppController.php
+++ /dev/null
@@ -1,90 +0,0 @@
-user();
- $userService = new UserService();
- if ($userService->isAvailable($user)) {
- $servers = ServerService::getAvailableServers($user);
- }
- $defaultConfig = base_path() . '/resources/rules/app.clash.yaml';
- $customConfig = base_path() . '/resources/rules/custom.app.clash.yaml';
- if (File::exists($customConfig)) {
- $config = Yaml::parseFile($customConfig);
- } else {
- $config = Yaml::parseFile($defaultConfig);
- }
- $proxy = [];
- $proxies = [];
-
- foreach ($servers as $item) {
- $protocol_settings = $item['protocol_settings'];
- if ($item['type'] === 'shadowsocks'
- && in_array(data_get($protocol_settings, 'cipher'), [
- 'aes-128-gcm',
- 'aes-192-gcm',
- 'aes-256-gcm',
- 'chacha20-ietf-poly1305'
- ])
- ) {
- array_push($proxy, \App\Protocols\Clash::buildShadowsocks($user['uuid'], $item));
- array_push($proxies, $item['name']);
- }
- if ($item['type'] === 'vmess') {
- array_push($proxy, \App\Protocols\Clash::buildVmess($user['uuid'], $item));
- array_push($proxies, $item['name']);
- }
- if ($item['type'] === 'trojan') {
- array_push($proxy, \App\Protocols\Clash::buildTrojan($user['uuid'], $item));
- array_push($proxies, $item['name']);
- }
- }
-
- $config['proxies'] = array_merge($config['proxies'] ? $config['proxies'] : [], $proxy);
- foreach ($config['proxy-groups'] as $k => $v) {
- $config['proxy-groups'][$k]['proxies'] = array_merge($config['proxy-groups'][$k]['proxies'], $proxies);
- }
- return(Yaml::dump($config));
- }
-
- public function getVersion(Request $request)
- {
- if (strpos($request->header('user-agent'), 'tidalab/4.0.0') !== false
- || strpos($request->header('user-agent'), 'tunnelab/4.0.0') !== false
- ) {
- if (strpos($request->header('user-agent'), 'Win64') !== false) {
- $data = [
- 'version' => admin_setting('windows_version'),
- 'download_url' => admin_setting('windows_download_url')
- ];
- } else {
- $data = [
- 'version' => admin_setting('macos_version'),
- 'download_url' => admin_setting('macos_download_url')
- ];
- }
- }else{
- $data = [
- 'windows_version' => admin_setting('windows_version'),
- 'windows_download_url' => admin_setting('windows_download_url'),
- 'macos_version' => admin_setting('macos_version'),
- 'macos_download_url' => admin_setting('macos_download_url'),
- 'android_version' => admin_setting('android_version'),
- 'android_download_url' => admin_setting('android_download_url')
- ];
- }
- return $this->success($data);
- }
-}
diff --git a/Xboard/app/Http/Controllers/V1/Client/ClientController.php b/Xboard/app/Http/Controllers/V1/Client/ClientController.php
deleted file mode 100644
index e4437bc..0000000
--- a/Xboard/app/Http/Controllers/V1/Client/ClientController.php
+++ /dev/null
@@ -1,247 +0,0 @@
- [
- 1 => '[Hy]',
- 2 => '[Hy2]'
- ],
- 'vless' => '[vless]',
- 'shadowsocks' => '[ss]',
- 'vmess' => '[vmess]',
- 'trojan' => '[trojan]',
- 'tuic' => '[tuic]',
- 'socks' => '[socks]',
- 'anytls' => '[anytls]'
- ];
-
-
- public function subscribe(Request $request)
- {
- HookManager::call('client.subscribe.before');
- $request->validate([
- 'types' => ['nullable', 'string'],
- 'filter' => ['nullable', 'string'],
- 'flag' => ['nullable', 'string'],
- ]);
-
- $user = $request->user();
- $userService = new UserService();
-
- if (!$userService->isAvailable($user)) {
- HookManager::call('client.subscribe.unavailable');
- return response('', 403, ['Content-Type' => 'text/plain']);
- }
-
- return $this->doSubscribe($request, $user);
- }
-
- public function doSubscribe(Request $request, $user, $servers = null)
- {
- if ($servers === null) {
- $servers = ServerService::getAvailableServers($user);
- $servers = HookManager::filter('client.subscribe.servers', $servers, $user, $request);
- }
-
- $clientInfo = $this->getClientInfo($request);
-
- $requestedTypes = $this->parseRequestedTypes($request->input('types'));
- $filterKeywords = $this->parseFilterKeywords($request->input('filter'));
-
- $protocolClassName = app('protocols.manager')->matchProtocolClassName($clientInfo['flag'])
- ?? General::class;
-
- $serversFiltered = $this->filterServers(
- servers: $servers,
- allowedTypes: $requestedTypes,
- filterKeywords: $filterKeywords
- );
-
- $this->setSubscribeInfoToServers($serversFiltered, $user, count($servers) - count($serversFiltered));
- $serversFiltered = $this->addPrefixToServerName($serversFiltered);
-
- // Instantiate the protocol class with filtered servers and client info
- $protocolInstance = app()->make($protocolClassName, [
- 'user' => $user,
- 'servers' => $serversFiltered,
- 'clientName' => $clientInfo['name'] ?? null,
- 'clientVersion' => $clientInfo['version'] ?? null,
- 'userAgent' => $clientInfo['flag'] ?? null
- ]);
-
- return $protocolInstance->handle();
- }
-
- /**
- * Parses the input string for requested server types.
- */
- private function parseRequestedTypes(?string $typeInputString): array
- {
- if (blank($typeInputString) || $typeInputString === 'all') {
- return Server::VALID_TYPES;
- }
-
- $requested = collect(preg_split('/[|,|]+/', $typeInputString))
- ->map(fn($type) => trim($type))
- ->filter() // Remove empty strings that might result from multiple delimiters
- ->all();
-
- return array_values(array_intersect($requested, Server::VALID_TYPES));
- }
-
- /**
- * Parses the input string for filter keywords.
- */
- private function parseFilterKeywords(?string $filterInputString): ?array
- {
- if (blank($filterInputString) || mb_strlen($filterInputString) > 20) {
- return null;
- }
-
- return collect(preg_split('/[|,|]+/', $filterInputString))
- ->map(fn($keyword) => trim($keyword))
- ->filter() // Remove empty strings
- ->all();
- }
-
- /**
- * Filters servers based on allowed types and keywords.
- */
- private function filterServers(array $servers, array $allowedTypes, ?array $filterKeywords): array
- {
- return collect($servers)->filter(function ($server) use ($allowedTypes, $filterKeywords) {
- // Condition 1: Server type must be in the list of allowed types
- if ($allowedTypes && !in_array($server['type'], $allowedTypes)) {
- return false; // Filter out (don't keep)
- }
-
- // Condition 2: If filterKeywords are provided, at least one keyword must match
- if (!empty($filterKeywords)) { // Check if $filterKeywords is not empty
- $keywordMatch = collect($filterKeywords)->contains(function ($keyword) use ($server) {
- return stripos($server['name'], $keyword) !== false
- || in_array($keyword, $server['tags'] ?? []);
- });
- if (!$keywordMatch) {
- return false; // Filter out if no keywords match
- }
- }
- // Keep the server if its type is allowed AND (no filter keywords OR at least one keyword matched)
- return true;
- })->values()->all();
- }
-
- private function getClientInfo(Request $request): array
- {
- $flag = strtolower($request->input('flag') ?? $request->header('User-Agent', ''));
-
- $clientName = null;
- $clientVersion = null;
-
- if (preg_match('/([a-zA-Z0-9\-_]+)[\/\s]+(v?[0-9]+(?:\.[0-9]+){0,2})/', $flag, $matches)) {
- $potentialName = strtolower($matches[1]);
- $clientVersion = preg_replace('/^v/', '', $matches[2]);
-
- if (in_array($potentialName, app('protocols.flags'))) {
- $clientName = $potentialName;
- }
- }
-
- if (!$clientName) {
- $flags = collect(app('protocols.flags'))->sortByDesc(fn($f) => strlen($f))->values()->all();
- foreach ($flags as $name) {
- if (stripos($flag, $name) !== false) {
- $clientName = $name;
- if (!$clientVersion) {
- $pattern = '/' . preg_quote($name, '/') . '[\/\s]+(v?[0-9]+(?:\.[0-9]+){0,2})/i';
- if (preg_match($pattern, $flag, $vMatches)) {
- $clientVersion = preg_replace('/^v/', '', $vMatches[1]);
- }
- }
- break;
- }
- }
- }
-
- if (!$clientVersion) {
- if (preg_match('/\/v?(\d+(?:\.\d+){0,2})/', $flag, $matches)) {
- $clientVersion = $matches[1];
- }
- }
-
- return [
- 'flag' => $flag,
- 'name' => $clientName,
- 'version' => $clientVersion
- ];
- }
-
- private function setSubscribeInfoToServers(&$servers, $user, $rejectServerCount = 0)
- {
- if (!isset($servers[0]))
- return;
- if ($rejectServerCount > 0) {
- array_unshift($servers, array_merge($servers[0], [
- 'name' => "过滤掉{$rejectServerCount}条线路",
- ]));
- }
- if (!(int) admin_setting('show_info_to_server_enable', 0))
- return;
- $useTraffic = $user['u'] + $user['d'];
- $totalTraffic = $user['transfer_enable'];
- $remainingTraffic = Helper::trafficConvert($totalTraffic - $useTraffic);
- $expiredDate = $user['expired_at'] ? date('Y-m-d', $user['expired_at']) : __('长期有效');
- $userService = new UserService();
- $resetDay = $userService->getResetDay($user);
- array_unshift($servers, array_merge($servers[0], [
- 'name' => "套餐到期:{$expiredDate}",
- ]));
- if ($resetDay) {
- array_unshift($servers, array_merge($servers[0], [
- 'name' => "距离下次重置剩余:{$resetDay} 天",
- ]));
- }
- array_unshift($servers, array_merge($servers[0], [
- 'name' => "剩余流量:{$remainingTraffic}",
- ]));
- }
-
- private function addPrefixToServerName(array $servers): array
- {
- if (!admin_setting('show_protocol_to_server_enable', false)) {
- return $servers;
- }
- return collect($servers)
- ->map(function (array $server): array {
- $server['name'] = $this->getPrefixedServerName($server);
- return $server;
- })
- ->all();
- }
-
- private function getPrefixedServerName(array $server): string
- {
- $type = $server['type'] ?? '';
- if (!isset(self::PROTOCOL_PREFIXES[$type])) {
- return $server['name'] ?? '';
- }
- $prefix = is_array(self::PROTOCOL_PREFIXES[$type])
- ? self::PROTOCOL_PREFIXES[$type][$server['protocol_settings']['version'] ?? 1] ?? ''
- : self::PROTOCOL_PREFIXES[$type];
- return $prefix . ($server['name'] ?? '');
- }
-}
diff --git a/Xboard/app/Http/Controllers/V1/Guest/CommController.php b/Xboard/app/Http/Controllers/V1/Guest/CommController.php
deleted file mode 100644
index 00c05c7..0000000
--- a/Xboard/app/Http/Controllers/V1/Guest/CommController.php
+++ /dev/null
@@ -1,39 +0,0 @@
- admin_setting('tos_url'),
- 'is_email_verify' => (int) admin_setting('email_verify', 0) ? 1 : 0,
- 'is_invite_force' => (int) admin_setting('invite_force', 0) ? 1 : 0,
- 'email_whitelist_suffix' => (int) admin_setting('email_whitelist_enable', 0)
- ? Helper::getEmailSuffix()
- : 0,
- 'is_captcha' => (int) admin_setting('captcha_enable', 0) ? 1 : 0,
- 'captcha_type' => admin_setting('captcha_type', 'recaptcha'),
- 'recaptcha_site_key' => admin_setting('recaptcha_site_key'),
- 'recaptcha_v3_site_key' => admin_setting('recaptcha_v3_site_key'),
- 'recaptcha_v3_score_threshold' => admin_setting('recaptcha_v3_score_threshold', 0.5),
- 'turnstile_site_key' => admin_setting('turnstile_site_key'),
- 'app_description' => admin_setting('app_description'),
- 'app_url' => admin_setting('app_url'),
- 'logo' => admin_setting('logo'),
- // 保持向后兼容
- 'is_recaptcha' => (int) admin_setting('captcha_enable', 0) ? 1 : 0,
- ];
-
- $data = HookManager::filter('guest_comm_config', $data);
-
- return $this->success($data);
- }
-}
diff --git a/Xboard/app/Http/Controllers/V1/Guest/PaymentController.php b/Xboard/app/Http/Controllers/V1/Guest/PaymentController.php
deleted file mode 100644
index 31f444e..0000000
--- a/Xboard/app/Http/Controllers/V1/Guest/PaymentController.php
+++ /dev/null
@@ -1,52 +0,0 @@
-notify($request->input());
- if (!$verify) {
- HookManager::call('payment.notify.failed', [$method, $uuid, $request]);
- return $this->fail([422, 'verify error']);
- }
- HookManager::call('payment.notify.verified', $verify);
- if (!$this->handle($verify['trade_no'], $verify['callback_no'])) {
- return $this->fail([400, 'handle error']);
- }
- return (isset($verify['custom_result']) ? $verify['custom_result'] : 'success');
- } catch (\Exception $e) {
- Log::error($e);
- return $this->fail([500, 'fail']);
- }
- }
-
- private function handle($tradeNo, $callbackNo)
- {
- $order = Order::where('trade_no', $tradeNo)->first();
- if (!$order) {
- return $this->fail([400202, 'order is not found']);
- }
- if ($order->status !== Order::STATUS_PENDING)
- return true;
- $orderService = new OrderService($order);
- if (!$orderService->paid($callbackNo)) {
- return false;
- }
-
- HookManager::call('payment.notify.success', $order);
- return true;
- }
-}
diff --git a/Xboard/app/Http/Controllers/V1/Guest/PlanController.php b/Xboard/app/Http/Controllers/V1/Guest/PlanController.php
deleted file mode 100644
index 4348a25..0000000
--- a/Xboard/app/Http/Controllers/V1/Guest/PlanController.php
+++ /dev/null
@@ -1,25 +0,0 @@
-planService = $planService;
- }
- public function fetch(Request $request)
- {
- $plan = $this->planService->getAvailablePlans();
- return $this->success(PlanResource::collection($plan));
- }
-}
diff --git a/Xboard/app/Http/Controllers/V1/Guest/TelegramController.php b/Xboard/app/Http/Controllers/V1/Guest/TelegramController.php
deleted file mode 100644
index 01369fa..0000000
--- a/Xboard/app/Http/Controllers/V1/Guest/TelegramController.php
+++ /dev/null
@@ -1,126 +0,0 @@
-telegramService = $telegramService;
- $this->userService = $userService;
- }
-
- public function webhook(Request $request): void
- {
- $expectedToken = md5(admin_setting('telegram_bot_token'));
- if ($request->input('access_token') !== $expectedToken) {
- throw new ApiException('access_token is error', 401);
- }
-
- $data = $request->json()->all();
-
- $this->formatMessage($data);
- $this->formatChatJoinRequest($data);
- $this->handle();
- }
-
- private function handle(): void
- {
- if (!$this->msg)
- return;
- $msg = $this->msg;
- $this->processBotName($msg);
- try {
- HookManager::call('telegram.message.before', [$msg]);
- $handled = HookManager::filter('telegram.message.handle', false, [$msg]);
- if (!$handled) {
- HookManager::call('telegram.message.unhandled', [$msg]);
- }
- HookManager::call('telegram.message.after', [$msg]);
- } catch (\Exception $e) {
- HookManager::call('telegram.message.error', [$msg, $e]);
- $this->telegramService->sendMessage($msg->chat_id, $e->getMessage());
- }
- }
-
- private function processBotName(object $msg): void
- {
- $commandParts = explode('@', $msg->command);
-
- if (count($commandParts) === 2) {
- $botName = $this->getBotName();
- if ($commandParts[1] === $botName) {
- $msg->command = $commandParts[0];
- }
- }
- }
-
- private function getBotName(): string
- {
- $response = $this->telegramService->getMe();
- return $response->result->username;
- }
-
- private function formatMessage(array $data): void
- {
- if (!isset($data['message']['text']))
- return;
-
- $message = $data['message'];
- $text = explode(' ', $message['text']);
-
- $this->msg = (object) [
- 'command' => $text[0],
- 'args' => array_slice($text, 1),
- 'chat_id' => $message['chat']['id'],
- 'message_id' => $message['message_id'],
- 'message_type' => 'message',
- 'text' => $message['text'],
- 'is_private' => $message['chat']['type'] === 'private',
- ];
-
- if (isset($message['reply_to_message']['text'])) {
- $this->msg->message_type = 'reply_message';
- $this->msg->reply_text = $message['reply_to_message']['text'];
- }
- }
-
- private function formatChatJoinRequest(array $data): void
- {
- $joinRequest = $data['chat_join_request'] ?? null;
- if (!$joinRequest)
- return;
-
- $chatId = $joinRequest['chat']['id'] ?? null;
- $userId = $joinRequest['from']['id'] ?? null;
-
- if (!$chatId || !$userId)
- return;
-
- $user = User::where('telegram_id', $userId)->first();
-
- if (!$user) {
- $this->telegramService->declineChatJoinRequest($chatId, $userId);
- return;
- }
-
- if (!$this->userService->isAvailable($user)) {
- $this->telegramService->declineChatJoinRequest($chatId, $userId);
- return;
- }
-
- $this->telegramService->approveChatJoinRequest($chatId, $userId);
- }
-}
diff --git a/Xboard/app/Http/Controllers/V1/Passport/AuthController.php b/Xboard/app/Http/Controllers/V1/Passport/AuthController.php
deleted file mode 100644
index 5464426..0000000
--- a/Xboard/app/Http/Controllers/V1/Passport/AuthController.php
+++ /dev/null
@@ -1,175 +0,0 @@
-mailLinkService = $mailLinkService;
- $this->registerService = $registerService;
- $this->loginService = $loginService;
- }
-
- /**
- * 通过邮件链接登录
- */
- public function loginWithMailLink(Request $request)
- {
- $params = $request->validate([
- 'email' => 'required|email:strict',
- 'redirect' => 'nullable'
- ]);
-
- [$success, $result] = $this->mailLinkService->handleMailLink(
- $params['email'],
- $request->input('redirect')
- );
-
- if (!$success) {
- return $this->fail($result);
- }
-
- return $this->success($result);
- }
-
- /**
- * 用户注册
- */
- public function register(AuthRegister $request)
- {
- [$success, $result] = $this->registerService->register($request);
-
- if (!$success) {
- return $this->fail($result);
- }
-
- $authService = new AuthService($result);
- return $this->success($authService->generateAuthData());
- }
-
- /**
- * 用户登录
- */
- public function login(AuthLogin $request)
- {
- $email = $request->input('email');
- $password = $request->input('password');
-
- [$success, $result] = $this->loginService->login($email, $password);
-
- if (!$success) {
- return $this->fail($result);
- }
-
- $authService = new AuthService($result);
- return $this->success($authService->generateAuthData());
- }
-
- /**
- * 通过token登录
- */
- public function token2Login(Request $request)
- {
- // 处理直接通过token重定向
- if ($token = $request->input('token')) {
- $redirect = '/#/login?verify=' . $token . '&redirect=' . ($request->input('redirect', 'dashboard'));
-
- return redirect()->to(
- admin_setting('app_url')
- ? admin_setting('app_url') . $redirect
- : url($redirect)
- );
- }
-
- // 处理通过验证码登录
- if ($verify = $request->input('verify')) {
- $userId = $this->mailLinkService->handleTokenLogin($verify);
-
- if (!$userId) {
- return response()->json([
- 'message' => __('Token error')
- ], 400);
- }
-
- $user = \App\Models\User::find($userId);
-
- if (!$user) {
- return response()->json([
- 'message' => __('User not found')
- ], 400);
- }
-
- $authService = new AuthService($user);
-
- return response()->json([
- 'data' => $authService->generateAuthData()
- ]);
- }
-
- return response()->json([
- 'message' => __('Invalid request')
- ], 400);
- }
-
- /**
- * 获取快速登录URL
- */
- public function getQuickLoginUrl(Request $request)
- {
- $authorization = $request->input('auth_data') ?? $request->header('authorization');
-
- if (!$authorization) {
- return response()->json([
- 'message' => ResponseEnum::CLIENT_HTTP_UNAUTHORIZED
- ], 401);
- }
-
- $user = AuthService::findUserByBearerToken($authorization);
-
- if (!$user) {
- return response()->json([
- 'message' => ResponseEnum::CLIENT_HTTP_UNAUTHORIZED_EXPIRED
- ], 401);
- }
-
- $url = $this->loginService->generateQuickLoginUrl($user, $request->input('redirect'));
- return $this->success($url);
- }
-
- /**
- * 忘记密码处理
- */
- public function forget(AuthForget $request)
- {
- [$success, $result] = $this->loginService->resetPassword(
- $request->input('email'),
- $request->input('email_code'),
- $request->input('password')
- );
-
- if (!$success) {
- return $this->fail($result);
- }
-
- return $this->success(true);
- }
-}
diff --git a/Xboard/app/Http/Controllers/V1/Passport/CommController.php b/Xboard/app/Http/Controllers/V1/Passport/CommController.php
deleted file mode 100644
index 663badf..0000000
--- a/Xboard/app/Http/Controllers/V1/Passport/CommController.php
+++ /dev/null
@@ -1,76 +0,0 @@
-verify($request);
- if (!$captchaValid) {
- return $this->fail($captchaError);
- }
-
- $email = $request->input('email');
-
- // 检查白名单后缀限制
- if ((int) admin_setting('email_whitelist_enable', 0)) {
- $isRegisteredEmail = User::byEmail($email)->exists();
- if (!$isRegisteredEmail) {
- $allowedSuffixes = Helper::getEmailSuffix();
- $emailSuffix = substr(strrchr($email, '@'), 1);
-
- if (!in_array($emailSuffix, $allowedSuffixes)) {
- return $this->fail([400, __('Email suffix is not in whitelist')]);
- }
- }
- }
-
- if (Cache::get(CacheKey::get('LAST_SEND_EMAIL_VERIFY_TIMESTAMP', $email))) {
- return $this->fail([400, __('Email verification code has been sent, please request again later')]);
- }
- $code = rand(100000, 999999);
- $subject = admin_setting('app_name', 'XBoard') . __('Email verification code');
-
- SendEmailJob::dispatch([
- 'email' => $email,
- 'subject' => $subject,
- 'template_name' => 'verify',
- 'template_value' => [
- 'name' => admin_setting('app_name', 'XBoard'),
- 'code' => $code,
- 'url' => admin_setting('app_url')
- ]
- ]);
-
- Cache::put(CacheKey::get('EMAIL_VERIFY_CODE', $email), $code, 300);
- Cache::put(CacheKey::get('LAST_SEND_EMAIL_VERIFY_TIMESTAMP', $email), time(), 60);
- return $this->success(true);
- }
-
- public function pv(Request $request)
- {
- $inviteCode = InviteCode::where('code', $request->input('invite_code'))->first();
- if ($inviteCode) {
- $inviteCode->pv = $inviteCode->pv + 1;
- $inviteCode->save();
- }
-
- return $this->success(true);
- }
-
-}
diff --git a/Xboard/app/Http/Controllers/V1/Server/ShadowsocksTidalabController.php b/Xboard/app/Http/Controllers/V1/Server/ShadowsocksTidalabController.php
deleted file mode 100644
index 62e5af9..0000000
--- a/Xboard/app/Http/Controllers/V1/Server/ShadowsocksTidalabController.php
+++ /dev/null
@@ -1,65 +0,0 @@
-attributes->get('node_info');
- Cache::put(CacheKey::get('SERVER_SHADOWSOCKS_LAST_CHECK_AT', $server->id), time(), 3600);
- $users = ServerService::getAvailableUsers($server);
- $result = [];
- foreach ($users as $user) {
- array_push($result, [
- 'id' => $user->id,
- 'port' => $server->server_port,
- 'cipher' => $server->cipher,
- 'secret' => $user->uuid
- ]);
- }
- $eTag = sha1(json_encode($result));
- if (strpos($request->header('If-None-Match'), $eTag) !== false ) {
- return response(null,304);
- }
- return response([
- 'data' => $result
- ])->header('ETag', "\"{$eTag}\"");
- }
-
- // 后端提交数据
- public function submit(Request $request)
- {
- $server = $request->attributes->get('node_info');
- $data = json_decode(request()->getContent(), true);
- Cache::put(CacheKey::get('SERVER_SHADOWSOCKS_ONLINE_USER', $server->id), count($data), 3600);
- Cache::put(CacheKey::get('SERVER_SHADOWSOCKS_LAST_PUSH_AT', $server->id), time(), 3600);
- $userService = new UserService();
- $formatData = [];
-
- foreach ($data as $item) {
- $formatData[$item['user_id']] = [$item['u'], $item['d']];
- }
- $userService->trafficFetch($server, 'shadowsocks', $formatData);
-
- return response([
- 'ret' => 1,
- 'msg' => 'ok'
- ]);
- }
-}
\ No newline at end of file
diff --git a/Xboard/app/Http/Controllers/V1/Server/TrojanTidalabController.php b/Xboard/app/Http/Controllers/V1/Server/TrojanTidalabController.php
deleted file mode 100644
index ceff48f..0000000
--- a/Xboard/app/Http/Controllers/V1/Server/TrojanTidalabController.php
+++ /dev/null
@@ -1,108 +0,0 @@
-attributes->get('node_info');
- if ($server->type !== 'trojan') {
- return $this->fail([400, '节点不存在']);
- }
- Cache::put(CacheKey::get('SERVER_TROJAN_LAST_CHECK_AT', $server->id), time(), 3600);
- $users = ServerService::getAvailableUsers($server);
- $result = [];
- foreach ($users as $user) {
- $user->trojan_user = [
- "password" => $user->uuid,
- ];
- unset($user->uuid);
- array_push($result, $user);
- }
- $eTag = sha1(json_encode($result));
- if (strpos($request->header('If-None-Match'), $eTag) !== false) {
- return response(null, 304);
- }
- return response([
- 'msg' => 'ok',
- 'data' => $result,
- ])->header('ETag', "\"{$eTag}\"");
- }
-
- // 后端提交数据
- public function submit(Request $request)
- {
- $server = $request->attributes->get('node_info');
- if ($server->type !== 'trojan') {
- return $this->fail([400, '节点不存在']);
- }
- $data = json_decode(request()->getContent(), true);
- Cache::put(CacheKey::get('SERVER_TROJAN_ONLINE_USER', $server->id), count($data), 3600);
- Cache::put(CacheKey::get('SERVER_TROJAN_LAST_PUSH_AT', $server->id), time(), 3600);
- $userService = new UserService();
- $formatData = [];
- foreach ($data as $item) {
- $formatData[$item['user_id']] = [$item['u'], $item['d']];
- }
- $userService->trafficFetch($server, 'trojan', $formatData);
-
- return response([
- 'ret' => 1,
- 'msg' => 'ok'
- ]);
- }
-
- // 后端获取配置
- public function config(Request $request)
- {
- $server = $request->attributes->get('node_info');
- if ($server->type !== 'trojan') {
- return $this->fail([400, '节点不存在']);
- }
- $request->validate([
- 'node_id' => 'required',
- 'local_port' => 'required'
- ], [
- 'node_id.required' => '节点ID不能为空',
- 'local_port.required' => '本地端口不能为空'
- ]);
- try {
- $json = $this->getTrojanConfig($server, $request->input('local_port'));
- } catch (\Exception $e) {
- \Log::error($e);
- return $this->fail([500, '配置获取失败']);
- }
-
- return (json_encode($json, JSON_UNESCAPED_UNICODE));
- }
-
- private function getTrojanConfig($server, int $localPort)
- {
- $protocolSettings = $server->protocol_settings;
- $json = json_decode(self::TROJAN_CONFIG);
- $json->local_port = $server->server_port;
- $json->ssl->sni = data_get($protocolSettings, 'server_name', $server->host);
- $json->ssl->cert = "/root/.cert/server.crt";
- $json->ssl->key = "/root/.cert/server.key";
- $json->api->api_port = $localPort;
- return $json;
- }
-}
\ No newline at end of file
diff --git a/Xboard/app/Http/Controllers/V1/Server/UniProxyController.php b/Xboard/app/Http/Controllers/V1/Server/UniProxyController.php
deleted file mode 100644
index 5901c4b..0000000
--- a/Xboard/app/Http/Controllers/V1/Server/UniProxyController.php
+++ /dev/null
@@ -1,178 +0,0 @@
-attributes->get('node_info');
- }
-
- // 后端获取用户
- public function user(Request $request)
- {
- ini_set('memory_limit', -1);
- $node = $this->getNodeInfo($request);
- $nodeType = $node->type;
- $nodeId = $node->id;
- Cache::put(CacheKey::get('SERVER_' . strtoupper($nodeType) . '_LAST_CHECK_AT', $nodeId), time(), 3600);
- $users = ServerService::getAvailableUsers($node);
-
- $response['users'] = $users;
-
- $eTag = sha1(json_encode($response));
- if (strpos($request->header('If-None-Match', ''), $eTag) !== false) {
- return response(null, 304);
- }
-
- return response($response)->header('ETag', "\"{$eTag}\"");
- }
-
- // 后端提交数据
- public function push(Request $request)
- {
- $res = json_decode(request()->getContent(), true);
- if (!is_array($res)) {
- return $this->fail([422, 'Invalid data format']);
- }
- $data = array_filter($res, function ($item) {
- return is_array($item)
- && count($item) === 2
- && is_numeric($item[0])
- && is_numeric($item[1]);
- });
- if (empty($data)) {
- return $this->success(true);
- }
- $node = $this->getNodeInfo($request);
- $nodeType = $node->type;
- $nodeId = $node->id;
-
- Cache::put(
- CacheKey::get('SERVER_' . strtoupper($nodeType) . '_ONLINE_USER', $nodeId),
- count($data),
- 3600
- );
- Cache::put(
- CacheKey::get('SERVER_' . strtoupper($nodeType) . '_LAST_PUSH_AT', $nodeId),
- time(),
- 3600
- );
-
- $userService = new UserService();
- $userService->trafficFetch($node, $nodeType, $data);
- return $this->success(true);
- }
-
- // 后端获取配置
- public function config(Request $request)
- {
- $node = $this->getNodeInfo($request);
- $response = ServerService::buildNodeConfig($node);
-
- $response['base_config'] = [
- 'push_interval' => (int) admin_setting('server_push_interval', 60),
- 'pull_interval' => (int) admin_setting('server_pull_interval', 60)
- ];
-
- $eTag = sha1(json_encode($response));
- if (strpos($request->header('If-None-Match', ''), $eTag) !== false) {
- return response(null, 304);
- }
- return response($response)->header('ETag', "\"{$eTag}\"");
- }
-
- // 获取在线用户数据
- public function alivelist(Request $request): JsonResponse
- {
- $node = $this->getNodeInfo($request);
- $deviceLimitUsers = ServerService::getAvailableUsers($node)
- ->where('device_limit', '>', 0);
-
- $alive = $this->deviceStateService->getAliveList(collect($deviceLimitUsers));
-
- return response()->json(['alive' => (object) $alive]);
- }
-
- // 后端提交在线数据
- public function alive(Request $request): JsonResponse
- {
- $node = $this->getNodeInfo($request);
- $data = json_decode(request()->getContent(), true);
- if ($data === null) {
- return response()->json([
- 'error' => 'Invalid online data'
- ], 400);
- }
-
- foreach ($data as $uid => $ips) {
- $this->deviceStateService->setDevices((int) $uid, $node->id, $ips);
- }
-
- return response()->json(['data' => true]);
- }
-
- // 提交节点负载状态
- public function status(Request $request): JsonResponse
- {
- $node = $this->getNodeInfo($request);
-
- $data = $request->validate([
- 'cpu' => 'required|numeric|min:0|max:100',
- 'mem.total' => 'required|integer|min:0',
- 'mem.used' => 'required|integer|min:0',
- 'swap.total' => 'required|integer|min:0',
- 'swap.used' => 'required|integer|min:0',
- 'disk.total' => 'required|integer|min:0',
- 'disk.used' => 'required|integer|min:0',
- ]);
-
- $nodeType = $node->type;
- $nodeId = $node->id;
-
- $statusData = [
- 'cpu' => (float) $data['cpu'],
- 'mem' => [
- 'total' => (int) $data['mem']['total'],
- 'used' => (int) $data['mem']['used'],
- ],
- 'swap' => [
- 'total' => (int) $data['swap']['total'],
- 'used' => (int) $data['swap']['used'],
- ],
- 'disk' => [
- 'total' => (int) $data['disk']['total'],
- 'used' => (int) $data['disk']['used'],
- ],
- 'updated_at' => now()->timestamp,
- ];
-
- $cacheTime = max(300, (int) admin_setting('server_push_interval', 60) * 3);
- cache([
- CacheKey::get('SERVER_' . strtoupper($nodeType) . '_LOAD_STATUS', $nodeId) => $statusData,
- CacheKey::get('SERVER_' . strtoupper($nodeType) . '_LAST_LOAD_AT', $nodeId) => now()->timestamp,
- ], $cacheTime);
-
- return response()->json(['data' => true, "code" => 0, "message" => "success"]);
- }
-}
diff --git a/Xboard/app/Http/Controllers/V1/User/CommController.php b/Xboard/app/Http/Controllers/V1/User/CommController.php
deleted file mode 100644
index fa0ae27..0000000
--- a/Xboard/app/Http/Controllers/V1/User/CommController.php
+++ /dev/null
@@ -1,39 +0,0 @@
- (int)admin_setting('telegram_bot_enable', 0),
- 'telegram_discuss_link' => admin_setting('telegram_discuss_link'),
- 'stripe_pk' => admin_setting('stripe_pk_live'),
- 'withdraw_methods' => admin_setting('commission_withdraw_method', Dict::WITHDRAW_METHOD_WHITELIST_DEFAULT),
- 'withdraw_close' => (int)admin_setting('withdraw_close_enable', 0),
- 'currency' => admin_setting('currency', 'CNY'),
- 'currency_symbol' => admin_setting('currency_symbol', '¥'),
- 'commission_distribution_enable' => (int)admin_setting('commission_distribution_enable', 0),
- 'commission_distribution_l1' => admin_setting('commission_distribution_l1'),
- 'commission_distribution_l2' => admin_setting('commission_distribution_l2'),
- 'commission_distribution_l3' => admin_setting('commission_distribution_l3')
- ];
- return $this->success($data);
- }
-
- public function getStripePublicKey(Request $request)
- {
- $payment = Payment::where('id', $request->input('id'))
- ->where('payment', 'StripeCredit')
- ->first();
- if (!$payment) throw new ApiException('payment is not found');
- return $this->success($payment->config['stripe_pk_live']);
- }
-}
diff --git a/Xboard/app/Http/Controllers/V1/User/CouponController.php b/Xboard/app/Http/Controllers/V1/User/CouponController.php
deleted file mode 100644
index b7c091f..0000000
--- a/Xboard/app/Http/Controllers/V1/User/CouponController.php
+++ /dev/null
@@ -1,25 +0,0 @@
-input('code'))) {
- return $this->fail([422, __('Coupon cannot be empty')]);
- }
- $couponService = new CouponService($request->input('code'));
- $couponService->setPlanId($request->input('plan_id'));
- $couponService->setUserId($request->user()->id);
- $couponService->setPeriod($request->input('period'));
- $couponService->check();
- return $this->success(CouponResource::make($couponService->getCoupon()));
- }
-}
diff --git a/Xboard/app/Http/Controllers/V1/User/GiftCardController.php b/Xboard/app/Http/Controllers/V1/User/GiftCardController.php
deleted file mode 100644
index 507c164..0000000
--- a/Xboard/app/Http/Controllers/V1/User/GiftCardController.php
+++ /dev/null
@@ -1,193 +0,0 @@
-input('code'));
- $giftCardService->setUser($request->user());
-
- // 1. 验证礼品卡本身是否有效 (如不存在、已过期、已禁用)
- $giftCardService->validateIsActive();
-
- // 2. 检查用户是否满足使用条件,但不在此处抛出异常
- $eligibility = $giftCardService->checkUserEligibility();
-
- // 3. 获取卡片信息和奖励预览
- $codeInfo = $giftCardService->getCodeInfo();
- $rewardPreview = $giftCardService->previewRewards();
-
- return $this->success([
- 'code_info' => $codeInfo, // 这里面已经包含 plan_info
- 'reward_preview' => $rewardPreview,
- 'can_redeem' => $eligibility['can_redeem'],
- 'reason' => $eligibility['reason'],
- ]);
-
- } catch (ApiException $e) {
- // 这里只捕获 validateIsActive 抛出的异常
- return $this->fail([400, $e->getMessage()]);
- } catch (\Exception $e) {
- Log::error('礼品卡查询失败', [
- 'code' => $request->input('code'),
- 'user_id' => $request->user()->id,
- 'error' => $e->getMessage(),
- ]);
- return $this->fail([500, '查询失败,请稍后重试']);
- }
- }
-
- /**
- * 使用兑换码
- */
- public function redeem(GiftCardRedeemRequest $request)
- {
- try {
- $giftCardService = new GiftCardService($request->input('code'));
- $giftCardService->setUser($request->user());
- $giftCardService->validate();
-
- // 使用礼品卡
- $result = $giftCardService->redeem([
- // 'ip_address' => $request->ip(),
- 'user_agent' => $request->userAgent(),
- ]);
-
- Log::info('礼品卡使用成功', [
- 'code' => $request->input('code'),
- 'user_id' => $request->user()->id,
- 'rewards' => $result['rewards'],
- ]);
-
- return $this->success([
- 'message' => '兑换成功!',
- 'rewards' => $result['rewards'],
- 'invite_rewards' => $result['invite_rewards'],
- 'template_name' => $result['template_name'],
- ]);
-
- } catch (ApiException $e) {
- return $this->fail([400, $e->getMessage()]);
- } catch (\Exception $e) {
- Log::error('礼品卡使用失败', [
- 'code' => $request->input('code'),
- 'user_id' => $request->user()->id,
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString(),
- ]);
- return $this->fail([500, '兑换失败,请稍后重试']);
- }
- }
-
- /**
- * 获取用户兑换记录
- */
- public function history(Request $request)
- {
- $request->validate([
- 'page' => 'integer|min:1',
- 'per_page' => 'integer|min:1|max:100',
- ]);
-
- $perPage = $request->input('per_page', 15);
-
- $usages = GiftCardUsage::with(['template', 'code'])
- ->where('user_id', $request->user()->id)
- ->orderBy('created_at', 'desc')
- ->paginate($perPage);
-
- $data = $usages->getCollection()->map(function (GiftCardUsage $usage) {
- return [
- 'id' => $usage->id,
- 'code' => ($usage->code instanceof \App\Models\GiftCardCode && $usage->code->code)
- ? (substr($usage->code->code, 0, 8) . '****')
- : '',
- 'template_name' => $usage->template->name ?? '',
- 'template_type' => $usage->template->type ?? '',
- 'template_type_name' => $usage->template->type_name ?? '',
- 'rewards_given' => $usage->rewards_given,
- 'invite_rewards' => $usage->invite_rewards,
- 'multiplier_applied' => $usage->multiplier_applied,
- 'created_at' => $usage->created_at,
- ];
- })->values();
- return response()->json([
- 'data' => $data,
- 'pagination' => [
- 'current_page' => $usages->currentPage(),
- 'last_page' => $usages->lastPage(),
- 'per_page' => $usages->perPage(),
- 'total' => $usages->total(),
- ],
- ]);
- }
-
- /**
- * 获取兑换记录详情
- */
- public function detail(Request $request)
- {
- $request->validate([
- 'id' => 'required|integer|exists:v2_gift_card_usage,id',
- ]);
-
- $usage = GiftCardUsage::with(['template', 'code', 'inviteUser'])
- ->where('user_id', $request->user()->id)
- ->where('id', $request->input('id'))
- ->first();
-
- if (!$usage) {
- return $this->fail([404, '记录不存在']);
- }
-
- return $this->success([
- 'id' => $usage->id,
- 'code' => $usage->code->code ?? '',
- 'template' => [
- 'name' => $usage->template->name ?? '',
- 'description' => $usage->template->description ?? '',
- 'type' => $usage->template->type ?? '',
- 'type_name' => $usage->template->type_name ?? '',
- 'icon' => $usage->template->icon ?? '',
- 'theme_color' => $usage->template->theme_color ?? '',
- ],
- 'rewards_given' => $usage->rewards_given,
- 'invite_rewards' => $usage->invite_rewards,
- 'invite_user' => $usage->inviteUser ? [
- 'id' => $usage->inviteUser->id ?? '',
- 'email' => isset($usage->inviteUser->email) ? (substr($usage->inviteUser->email, 0, 3) . '***@***') : '',
- ] : null,
- 'user_level_at_use' => $usage->user_level_at_use,
- 'plan_id_at_use' => $usage->plan_id_at_use,
- 'multiplier_applied' => $usage->multiplier_applied,
- // 'ip_address' => $usage->ip_address,
- 'notes' => $usage->notes,
- 'created_at' => $usage->created_at,
- ]);
- }
-
- /**
- * 获取可用的礼品卡类型
- */
- public function types(Request $request)
- {
- return $this->success([
- 'types' => \App\Models\GiftCardTemplate::getTypeMap(),
- ]);
- }
-}
diff --git a/Xboard/app/Http/Controllers/V1/User/InviteController.php b/Xboard/app/Http/Controllers/V1/User/InviteController.php
deleted file mode 100644
index cbde315..0000000
--- a/Xboard/app/Http/Controllers/V1/User/InviteController.php
+++ /dev/null
@@ -1,79 +0,0 @@
-user()->id)->where('status', 0)->count() >= admin_setting('invite_gen_limit', 5)) {
- return $this->fail([400,__('The maximum number of creations has been reached')]);
- }
- $inviteCode = new InviteCode();
- $inviteCode->user_id = $request->user()->id;
- $inviteCode->code = Helper::randomChar(8);
- return $this->success($inviteCode->save());
- }
-
- public function details(Request $request)
- {
- $current = $request->input('current') ? $request->input('current') : 1;
- $pageSize = $request->input('page_size') >= 10 ? $request->input('page_size') : 10;
- $builder = CommissionLog::where('invite_user_id', $request->user()->id)
- ->where('get_amount', '>', 0)
- ->orderBy('created_at', 'DESC');
- $total = $builder->count();
- $details = $builder->forPage($current, $pageSize)
- ->get();
- return response([
- 'data' => ComissionLogResource::collection($details),
- 'total' => $total
- ]);
- }
-
- public function fetch(Request $request)
- {
- $commission_rate = admin_setting('invite_commission', 10);
- $user = User::find($request->user()->id)
- ->load(['codes' => fn($query) => $query->where('status', 0)]);
- if ($user->commission_rate) {
- $commission_rate = $user->commission_rate;
- }
- $uncheck_commission_balance = (int)Order::where('status', 3)
- ->where('commission_status', 0)
- ->where('invite_user_id', $user->id)
- ->sum('commission_balance');
- if (admin_setting('commission_distribution_enable', 0)) {
- $uncheck_commission_balance = $uncheck_commission_balance * (admin_setting('commission_distribution_l1') / 100);
- }
- $stat = [
- //已注册用户数
- (int)User::where('invite_user_id', $user->id)->count(),
- //有效的佣金
- (int)CommissionLog::where('invite_user_id', $user->id)
- ->sum('get_amount'),
- //确认中的佣金
- $uncheck_commission_balance,
- //佣金比例
- (int)$commission_rate,
- //可用佣金
- (int)$user->commission_balance
- ];
- $data = [
- 'codes' => InviteCodeResource::collection($user->codes),
- 'stat' => $stat
- ];
- return $this->success($data);
- }
-}
diff --git a/Xboard/app/Http/Controllers/V1/User/KnowledgeController.php b/Xboard/app/Http/Controllers/V1/User/KnowledgeController.php
deleted file mode 100644
index e3646c9..0000000
--- a/Xboard/app/Http/Controllers/V1/User/KnowledgeController.php
+++ /dev/null
@@ -1,150 +0,0 @@
-userService = $userService;
- }
-
- public function fetch(Request $request)
- {
- $request->validate([
- 'id' => 'nullable|sometimes|integer|min:1',
- 'language' => 'nullable|sometimes|string|max:10',
- 'keyword' => 'nullable|sometimes|string|max:255',
- ]);
-
- return $request->input('id')
- ? $this->fetchSingle($request)
- : $this->fetchList($request);
- }
-
- private function fetchSingle(Request $request)
- {
- $knowledge = $this->buildKnowledgeQuery()
- ->where('id', $request->input('id'))
- ->first();
-
- if (!$knowledge) {
- return $this->fail([500, __('Article does not exist')]);
- }
-
- $knowledge = $knowledge->toArray();
- $knowledge = $this->processKnowledgeContent($knowledge, $request->user());
-
- return $this->success(KnowledgeResource::make($knowledge));
- }
-
- private function fetchList(Request $request)
- {
- $builder = $this->buildKnowledgeQuery(['id', 'category', 'title', 'updated_at', 'body'])
- ->where('language', $request->input('language'))
- ->orderBy('sort', 'ASC');
-
- $keyword = $request->input('keyword');
- if ($keyword) {
- $builder = $builder->where(function ($query) use ($keyword) {
- $query->where('title', 'LIKE', "%{$keyword}%")
- ->orWhere('body', 'LIKE', "%{$keyword}%");
- });
- }
-
- $knowledges = $builder->get()
- ->map(function ($knowledge) use ($request) {
- $knowledge = $knowledge->toArray();
- $knowledge = $this->processKnowledgeContent($knowledge, $request->user());
- return KnowledgeResource::make($knowledge);
- })
- ->groupBy('category');
-
- return $this->success($knowledges);
- }
-
- private function buildKnowledgeQuery(array $select = ['*'])
- {
- return Knowledge::select($select)->where('show', 1);
- }
-
- private function processKnowledgeContent(array $knowledge, User $user): array
- {
- if (!isset($knowledge['body'])) {
- return $knowledge;
- }
-
- if (!$this->userService->isAvailable($user)) {
- $this->formatAccessData($knowledge['body']);
- }
- $subscribeUrl = Helper::getSubscribeUrl($user['token']);
- $knowledge['body'] = $this->replacePlaceholders($knowledge['body'], $subscribeUrl);
-
- return $knowledge;
- }
-
- private function formatAccessData(&$body): void
- {
- $rules = [
- [
- 'type' => 'regex',
- 'pattern' => '/(.*?)/s',
- 'replacement' => '' . __('You must have a valid subscription to view content in this area') . '
'
- ]
- ];
-
- $this->applyReplacementRules($body, $rules);
- }
-
- private function replacePlaceholders(string $body, string $subscribeUrl): string
- {
- $rules = [
- [
- 'type' => 'string',
- 'search' => '{{siteName}}',
- 'replacement' => admin_setting('app_name', 'XBoard')
- ],
- [
- 'type' => 'string',
- 'search' => '{{subscribeUrl}}',
- 'replacement' => $subscribeUrl
- ],
- [
- 'type' => 'string',
- 'search' => '{{urlEncodeSubscribeUrl}}',
- 'replacement' => urlencode($subscribeUrl)
- ],
- [
- 'type' => 'string',
- 'search' => '{{safeBase64SubscribeUrl}}',
- 'replacement' => str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($subscribeUrl))
- ]
- ];
-
- $this->applyReplacementRules($body, $rules);
- return $body;
- }
-
- private function applyReplacementRules(string &$body, array $rules): void
- {
- foreach ($rules as $rule) {
- if ($rule['type'] === 'regex') {
- $body = preg_replace($rule['pattern'], $rule['replacement'], $body);
- } else {
- $body = str_replace($rule['search'], $rule['replacement'], $body);
- }
- }
- }
-}
diff --git a/Xboard/app/Http/Controllers/V1/User/NoticeController.php b/Xboard/app/Http/Controllers/V1/User/NoticeController.php
deleted file mode 100644
index 9382e06..0000000
--- a/Xboard/app/Http/Controllers/V1/User/NoticeController.php
+++ /dev/null
@@ -1,26 +0,0 @@
-input('current') ? $request->input('current') : 1;
- $pageSize = 5;
- $model = Notice::orderBy('sort', 'ASC')
- ->orderBy('id', 'DESC')
- ->where('show', true);
- $total = $model->count();
- $res = $model->forPage($current, $pageSize)
- ->get();
- return response([
- 'data' => $res,
- 'total' => $total
- ]);
- }
-}
diff --git a/Xboard/app/Http/Controllers/V1/User/OrderController.php b/Xboard/app/Http/Controllers/V1/User/OrderController.php
deleted file mode 100644
index 7b28128..0000000
--- a/Xboard/app/Http/Controllers/V1/User/OrderController.php
+++ /dev/null
@@ -1,212 +0,0 @@
-validate([
- 'status' => 'nullable|integer|in:0,1,2,3',
- ]);
- $orders = Order::with('plan')
- ->where('user_id', $request->user()->id)
- ->when($request->input('status') !== null, function ($query) use ($request) {
- $query->where('status', $request->input('status'));
- })
- ->orderBy('created_at', 'DESC')
- ->get();
-
- return $this->success(OrderResource::collection($orders));
- }
-
- public function detail(Request $request)
- {
- $request->validate([
- 'trade_no' => 'required|string',
- ]);
- $order = Order::with(['payment', 'plan'])
- ->where('user_id', $request->user()->id)
- ->where('trade_no', $request->input('trade_no'))
- ->first();
- if (!$order) {
- return $this->fail([400, __('Order does not exist or has been paid')]);
- }
- $order['try_out_plan_id'] = (int) admin_setting('try_out_plan_id');
- if (!$order->plan) {
- return $this->fail([400, __('Subscription plan does not exist')]);
- }
- if ($order->surplus_order_ids) {
- $order['surplus_orders'] = Order::whereIn('id', $order->surplus_order_ids)->get();
- }
- return $this->success(OrderResource::make($order));
- }
-
- public function save(OrderSave $request)
- {
- $request->validate([
- 'plan_id' => 'required|exists:App\Models\Plan,id',
- 'period' => 'required|string'
- ]);
-
- $user = User::findOrFail($request->user()->id);
- $userService = app(UserService::class);
-
- if ($userService->isNotCompleteOrderByUserId($user->id)) {
- throw new ApiException(__('You have an unpaid or pending order, please try again later or cancel it'));
- }
-
- $plan = Plan::findOrFail($request->input('plan_id'));
- $planService = new PlanService($plan);
-
- $planService->validatePurchase($user, $request->input('period'));
-
- $order = OrderService::createFromRequest(
- $user,
- $plan,
- $request->input('period'),
- $request->input('coupon_code')
- );
-
- return $this->success($order->trade_no);
- }
-
- protected function applyCoupon(Order $order, string $couponCode): void
- {
- $couponService = new CouponService($couponCode);
- if (!$couponService->use($order)) {
- throw new ApiException(__('Coupon failed'));
- }
- $order->coupon_id = $couponService->getId();
- }
-
- protected function handleUserBalance(Order $order, User $user, UserService $userService): void
- {
- $remainingBalance = $user->balance - $order->total_amount;
-
- if ($remainingBalance > 0) {
- if (!$userService->addBalance($order->user_id, -$order->total_amount)) {
- throw new ApiException(__('Insufficient balance'));
- }
- $order->balance_amount = $order->total_amount;
- $order->total_amount = 0;
- } else {
- if (!$userService->addBalance($order->user_id, -$user->balance)) {
- throw new ApiException(__('Insufficient balance'));
- }
- $order->balance_amount = $user->balance;
- $order->total_amount = $order->total_amount - $user->balance;
- }
- }
-
- public function checkout(Request $request)
- {
- $tradeNo = $request->input('trade_no');
- $method = $request->input('method');
- $order = Order::where('trade_no', $tradeNo)
- ->where('user_id', $request->user()->id)
- ->where('status', 0)
- ->first();
- if (!$order) {
- return $this->fail([400, __('Order does not exist or has been paid')]);
- }
- // free process
- if ($order->total_amount <= 0) {
- $orderService = new OrderService($order);
- if (!$orderService->paid($order->trade_no))
- return $this->fail([400, '支付失败']);
- return response([
- 'type' => -1,
- 'data' => true
- ]);
- }
- $payment = Payment::find($method);
- if (!$payment || !$payment->enable) {
- return $this->fail([400, __('Payment method is not available')]);
- }
- $paymentService = new PaymentService($payment->payment, $payment->id);
- $order->handling_amount = NULL;
- if ($payment->handling_fee_fixed || $payment->handling_fee_percent) {
- $order->handling_amount = (int) round(($order->total_amount * ($payment->handling_fee_percent / 100)) + $payment->handling_fee_fixed);
- }
- $order->payment_id = $method;
- if (!$order->save())
- return $this->fail([400, __('Request failed, please try again later')]);
- $result = $paymentService->pay([
- 'trade_no' => $tradeNo,
- 'total_amount' => isset($order->handling_amount) ? ($order->total_amount + $order->handling_amount) : $order->total_amount,
- 'user_id' => $order->user_id,
- 'stripe_token' => $request->input('token')
- ]);
- return response([
- 'type' => $result['type'],
- 'data' => $result['data']
- ]);
- }
-
- public function check(Request $request)
- {
- $tradeNo = $request->input('trade_no');
- $order = Order::where('trade_no', $tradeNo)
- ->where('user_id', $request->user()->id)
- ->first();
- if (!$order) {
- return $this->fail([400, __('Order does not exist')]);
- }
- return $this->success($order->status);
- }
-
- public function getPaymentMethod()
- {
- $methods = Payment::select([
- 'id',
- 'name',
- 'payment',
- 'icon',
- 'handling_fee_fixed',
- 'handling_fee_percent'
- ])
- ->where('enable', 1)
- ->orderBy('sort', 'ASC')
- ->get();
-
- return $this->success($methods);
- }
-
- public function cancel(Request $request)
- {
- if (empty($request->input('trade_no'))) {
- return $this->fail([422, __('Invalid parameter')]);
- }
- $order = Order::where('trade_no', $request->input('trade_no'))
- ->where('user_id', $request->user()->id)
- ->first();
- if (!$order) {
- return $this->fail([400, __('Order does not exist')]);
- }
- if ($order->status !== 0) {
- return $this->fail([400, __('You can only cancel pending orders')]);
- }
- $orderService = new OrderService($order);
- if (!$orderService->cancel()) {
- return $this->fail([400, __('Cancel failed')]);
- }
- return $this->success(true);
- }
-}
diff --git a/Xboard/app/Http/Controllers/V1/User/PlanController.php b/Xboard/app/Http/Controllers/V1/User/PlanController.php
deleted file mode 100644
index 7db7aad..0000000
--- a/Xboard/app/Http/Controllers/V1/User/PlanController.php
+++ /dev/null
@@ -1,38 +0,0 @@
-planService = $planService;
- }
- public function fetch(Request $request)
- {
- $user = User::find($request->user()->id);
- if ($request->input('id')) {
- $plan = Plan::where('id', $request->input('id'))->first();
- if (!$plan) {
- return $this->fail([400, __('Subscription plan does not exist')]);
- }
- if (!$this->planService->isPlanAvailableForUser($plan, $user)) {
- return $this->fail([400, __('Subscription plan does not exist')]);
- }
- return $this->success(PlanResource::make($plan));
- }
-
- $plans = $this->planService->getAvailablePlans();
- return $this->success(PlanResource::collection($plans));
- }
-}
diff --git a/Xboard/app/Http/Controllers/V1/User/ServerController.php b/Xboard/app/Http/Controllers/V1/User/ServerController.php
deleted file mode 100644
index f12b005..0000000
--- a/Xboard/app/Http/Controllers/V1/User/ServerController.php
+++ /dev/null
@@ -1,31 +0,0 @@
-user()->id);
- $servers = [];
- $userService = new UserService();
- if ($userService->isAvailable($user)) {
- $servers = ServerService::getAvailableServers($user);
- }
- $eTag = sha1(json_encode(array_column($servers, 'cache_key')));
- if (strpos($request->header('If-None-Match', ''), $eTag) !== false ) {
- return response(null,304);
- }
- $data = NodeResource::collection($servers);
- return response([
- 'data' => $data
- ])->header('ETag', "\"{$eTag}\"");
- }
-}
diff --git a/Xboard/app/Http/Controllers/V1/User/StatController.php b/Xboard/app/Http/Controllers/V1/User/StatController.php
deleted file mode 100644
index 11bb9c0..0000000
--- a/Xboard/app/Http/Controllers/V1/User/StatController.php
+++ /dev/null
@@ -1,26 +0,0 @@
-startOfMonth()->timestamp;
- $records = StatUser::query()
- ->where('user_id', $request->user()->id)
- ->where('record_at', '>=', $startDate)
- ->orderBy('record_at', 'DESC')
- ->get();
-
- $data = TrafficLogResource::collection(collect($records));
- return $this->success($data);
- }
-}
diff --git a/Xboard/app/Http/Controllers/V1/User/TelegramController.php b/Xboard/app/Http/Controllers/V1/User/TelegramController.php
deleted file mode 100644
index 2cf65c7..0000000
--- a/Xboard/app/Http/Controllers/V1/User/TelegramController.php
+++ /dev/null
@@ -1,26 +0,0 @@
-getMe();
- $data = [
- 'username' => $response->result->username
- ];
- return $this->success($data);
- }
-
- public function unbind(Request $request)
- {
- $user = User::where('user_id', $request->user()->id)->first();
- }
-}
diff --git a/Xboard/app/Http/Controllers/V1/User/TicketController.php b/Xboard/app/Http/Controllers/V1/User/TicketController.php
deleted file mode 100644
index 05ca915..0000000
--- a/Xboard/app/Http/Controllers/V1/User/TicketController.php
+++ /dev/null
@@ -1,154 +0,0 @@
-input('id')) {
- $ticket = Ticket::where('id', $request->input('id'))
- ->where('user_id', $request->user()->id)
- ->first()
- ->load('message');
- if (!$ticket) {
- return $this->fail([400, __('Ticket does not exist')]);
- }
- $ticket['message'] = TicketMessage::where('ticket_id', $ticket->id)->get();
- $ticket['message']->each(function ($message) use ($ticket) {
- $message['is_me'] = ($message['user_id'] == $ticket->user_id);
- });
- return $this->success(TicketResource::make($ticket)->additional(['message' => true]));
- }
- $ticket = Ticket::where('user_id', $request->user()->id)
- ->orderBy('created_at', 'DESC')
- ->get();
- return $this->success(TicketResource::collection($ticket));
- }
-
- public function save(TicketSave $request)
- {
- $ticketService = new TicketService();
- $ticket = $ticketService->createTicket(
- $request->user()->id,
- $request->input('subject'),
- $request->input('level'),
- $request->input('message')
- );
- HookManager::call('ticket.create.after', $ticket);
- return $this->success(true);
-
- }
-
- public function reply(Request $request)
- {
- if (empty($request->input('id'))) {
- return $this->fail([400, __('Invalid parameter')]);
- }
- if (empty($request->input('message'))) {
- return $this->fail([400, __('Message cannot be empty')]);
- }
- $ticket = Ticket::where('id', $request->input('id'))
- ->where('user_id', $request->user()->id)
- ->first();
- if (!$ticket) {
- return $this->fail([400, __('Ticket does not exist')]);
- }
- if ($ticket->status) {
- return $this->fail([400, __('The ticket is closed and cannot be replied')]);
- }
- if ((int) admin_setting('ticket_must_wait_reply', 0) && $request->user()->id == $this->getLastMessage($ticket->id)->user_id) {
- return $this->fail(codeResponse: [400, __('Please wait for the technical enginneer to reply')]);
- }
- $ticketService = new TicketService();
- if (
- !$ticketService->reply(
- $ticket,
- $request->input('message'),
- $request->user()->id
- )
- ) {
- return $this->fail([400, __('Ticket reply failed')]);
- }
- HookManager::call('ticket.reply.user.after', $ticket);
- return $this->success(true);
- }
-
-
- public function close(Request $request)
- {
- if (empty($request->input('id'))) {
- return $this->fail([422, __('Invalid parameter')]);
- }
- $ticket = Ticket::where('id', $request->input('id'))
- ->where('user_id', $request->user()->id)
- ->first();
- if (!$ticket) {
- return $this->fail([400, __('Ticket does not exist')]);
- }
- $ticket->status = Ticket::STATUS_CLOSED;
- if (!$ticket->save()) {
- return $this->fail([500, __('Close failed')]);
- }
- return $this->success(true);
- }
-
- private function getLastMessage($ticketId)
- {
- return TicketMessage::where('ticket_id', $ticketId)
- ->orderBy('id', 'DESC')
- ->first();
- }
-
- public function withdraw(TicketWithdraw $request)
- {
- if ((int) admin_setting('withdraw_close_enable', 0)) {
- return $this->fail([400, 'Unsupported withdraw']);
- }
- if (
- !in_array(
- $request->input('withdraw_method'),
- admin_setting('commission_withdraw_method', Dict::WITHDRAW_METHOD_WHITELIST_DEFAULT)
- )
- ) {
- return $this->fail([422, __('Unsupported withdrawal method')]);
- }
- $user = User::find($request->user()->id);
- $limit = admin_setting('commission_withdraw_limit', 100);
- if ($limit > ($user->commission_balance / 100)) {
- return $this->fail([422, __('The current required minimum withdrawal commission is :limit', ['limit' => $limit])]);
- }
- try {
- $ticketService = new TicketService();
- $subject = __('[Commission Withdrawal Request] This ticket is opened by the system');
- $message = sprintf(
- "%s\r\n%s",
- __('Withdrawal method') . ":" . $request->input('withdraw_method'),
- __('Withdrawal account') . ":" . $request->input('withdraw_account')
- );
- $ticket = $ticketService->createTicket(
- $request->user()->id,
- $subject,
- 2,
- $message
- );
- } catch (\Exception $e) {
- throw $e;
- }
- HookManager::call('ticket.create.after', $ticket);
- return $this->success(true);
- }
-}
diff --git a/Xboard/app/Http/Controllers/V1/User/UserController.php b/Xboard/app/Http/Controllers/V1/User/UserController.php
deleted file mode 100644
index c561988..0000000
--- a/Xboard/app/Http/Controllers/V1/User/UserController.php
+++ /dev/null
@@ -1,223 +0,0 @@
-loginService = $loginService;
- }
-
- public function getActiveSession(Request $request)
- {
- $user = $request->user();
- $authService = new AuthService($user);
- return $this->success($authService->getSessions());
- }
-
- public function removeActiveSession(Request $request)
- {
- $user = $request->user();
- $authService = new AuthService($user);
- return $this->success($authService->removeSession($request->input('session_id')));
- }
-
- public function checkLogin(Request $request)
- {
- $data = [
- 'is_login' => $request->user()?->id ? true : false
- ];
- if ($request->user()?->is_admin) {
- $data['is_admin'] = true;
- }
- return $this->success($data);
- }
-
- public function changePassword(UserChangePassword $request)
- {
- $user = $request->user();
- if (
- !Helper::multiPasswordVerify(
- $user->password_algo,
- $user->password_salt,
- $request->input('old_password'),
- $user->password
- )
- ) {
- return $this->fail([400, __('The old password is wrong')]);
- }
- $user->password = password_hash($request->input('new_password'), PASSWORD_DEFAULT);
- $user->password_algo = NULL;
- $user->password_salt = NULL;
- if (!$user->save()) {
- return $this->fail([400, __('Save failed')]);
- }
-
- $currentToken = $user->currentAccessToken();
- if ($currentToken) {
- $user->tokens()->where('id', '!=', $currentToken->id)->delete();
- } else {
- $user->tokens()->delete();
- }
-
- return $this->success(true);
- }
-
- public function info(Request $request)
- {
- $user = User::where('id', $request->user()->id)
- ->select([
- 'email',
- 'transfer_enable',
- 'last_login_at',
- 'created_at',
- 'banned',
- 'remind_expire',
- 'remind_traffic',
- 'expired_at',
- 'balance',
- 'commission_balance',
- 'plan_id',
- 'discount',
- 'commission_rate',
- 'telegram_id',
- 'uuid'
- ])
- ->first();
- if (!$user) {
- return $this->fail([400, __('The user does not exist')]);
- }
- $user['avatar_url'] = 'https://cdn.v2ex.com/gravatar/' . md5($user->email) . '?s=64&d=identicon';
- return $this->success($user);
- }
-
- public function getStat(Request $request)
- {
- $stat = [
- Order::where('status', 0)
- ->where('user_id', $request->user()->id)
- ->count(),
- Ticket::where('status', 0)
- ->where('user_id', $request->user()->id)
- ->count(),
- User::where('invite_user_id', $request->user()->id)
- ->count()
- ];
- return $this->success($stat);
- }
-
- public function getSubscribe(Request $request)
- {
- $user = User::where('id', $request->user()->id)
- ->select([
- 'plan_id',
- 'token',
- 'expired_at',
- 'u',
- 'd',
- 'transfer_enable',
- 'email',
- 'uuid',
- 'device_limit',
- 'speed_limit',
- 'next_reset_at'
- ])
- ->first();
- if (!$user) {
- return $this->fail([400, __('The user does not exist')]);
- }
- if ($user->plan_id) {
- $user['plan'] = Plan::find($user->plan_id);
- if (!$user['plan']) {
- return $this->fail([400, __('Subscription plan does not exist')]);
- }
- }
- $user['subscribe_url'] = Helper::getSubscribeUrl($user['token']);
- $userService = new UserService();
- $user['reset_day'] = $userService->getResetDay($user);
- $user = HookManager::filter('user.subscribe.response', $user);
- return $this->success($user);
- }
-
- public function resetSecurity(Request $request)
- {
- $user = $request->user();
- $user->uuid = Helper::guid(true);
- $user->token = Helper::guid();
- if (!$user->save()) {
- return $this->fail([400, __('Reset failed')]);
- }
- return $this->success(Helper::getSubscribeUrl($user->token));
- }
-
- public function update(UserUpdate $request)
- {
- $updateData = $request->only([
- 'remind_expire',
- 'remind_traffic'
- ]);
-
- $user = $request->user();
- try {
- $user->update($updateData);
- } catch (\Exception $e) {
- return $this->fail([400, __('Save failed')]);
- }
-
- return $this->success(true);
- }
-
- public function transfer(UserTransfer $request)
- {
- $amount = $request->input('transfer_amount');
- try {
- DB::transaction(function () use ($request, $amount) {
- $user = User::lockForUpdate()->find($request->user()->id);
- if (!$user) {
- throw new \Exception(__('The user does not exist'));
- }
- if ($amount > $user->commission_balance) {
- throw new \Exception(__('Insufficient commission balance'));
- }
- $user->commission_balance -= $amount;
- $user->balance += $amount;
- if (!$user->save()) {
- throw new \Exception(__('Transfer failed'));
- }
- });
- } catch (\Exception $e) {
- return $this->fail([400, $e->getMessage()]);
- }
- return $this->success(true);
- }
-
- public function getQuickLoginUrl(Request $request)
- {
- $user = $request->user();
-
- $url = $this->loginService->generateQuickLoginUrl($user, $request->input('redirect'));
- return $this->success($url);
- }
-}
diff --git a/Xboard/app/Http/Controllers/V2/Admin/ConfigController.php b/Xboard/app/Http/Controllers/V2/Admin/ConfigController.php
deleted file mode 100644
index 81c5570..0000000
--- a/Xboard/app/Http/Controllers/V2/Admin/ConfigController.php
+++ /dev/null
@@ -1,300 +0,0 @@
-success($files);
- }
-
- public function getThemeTemplate()
- {
- $path = public_path('theme/');
- $files = array_map(function ($item) use ($path) {
- return str_replace($path, '', $item);
- }, glob($path . '*'));
- return $this->success($files);
- }
-
- public function testSendMail(Request $request)
- {
- $mailLog = MailService::sendEmail([
- 'email' => $request->user()->email,
- 'subject' => 'This is xboard test email',
- 'template_name' => 'notify',
- 'template_value' => [
- 'name' => admin_setting('app_name', 'XBoard'),
- 'content' => 'This is xboard test email',
- 'url' => admin_setting('app_url')
- ]
- ]);
- return response([
- 'data' => $mailLog,
- ]);
- }
- public function setTelegramWebhook(Request $request)
- {
- $hookUrl = $this->resolveTelegramWebhookUrl();
- if (blank($hookUrl)) {
- return $this->fail([422, 'Telegram Webhook地址未配置']);
- }
- $hookUrl .= '?' . http_build_query([
- 'access_token' => md5(admin_setting('telegram_bot_token', $request->input('telegram_bot_token')))
- ]);
- $telegramService = new TelegramService($request->input('telegram_bot_token'));
- $telegramService->getMe();
- $telegramService->setWebhook(url: $hookUrl);
- $telegramService->registerBotCommands();
- return $this->success([
- 'success' => true,
- 'webhook_url' => $hookUrl,
- 'webhook_base_url' => $this->getTelegramWebhookBaseUrl(),
- ]);
- }
-
- public function fetch(Request $request)
- {
- $key = $request->input('key');
- $configMappings = $this->getConfigMappings();
- if ($key && isset($configMappings[$key])) {
- return $this->success([$key => $configMappings[$key]]);
- }
-
- return $this->success($configMappings);
- }
-
- /**
- * 获取配置映射数据
- *
- * @return array 配置映射数组
- */
- private function getConfigMappings(): array
- {
- return [
- 'invite' => [
- 'invite_force' => (bool) admin_setting('invite_force', 0),
- 'invite_commission' => admin_setting('invite_commission', 10),
- 'invite_gen_limit' => admin_setting('invite_gen_limit', 5),
- 'invite_never_expire' => (bool) admin_setting('invite_never_expire', 0),
- 'commission_first_time_enable' => (bool) admin_setting('commission_first_time_enable', 1),
- 'commission_auto_check_enable' => (bool) admin_setting('commission_auto_check_enable', 1),
- 'commission_withdraw_limit' => admin_setting('commission_withdraw_limit', 100),
- 'commission_withdraw_method' => admin_setting('commission_withdraw_method', Dict::WITHDRAW_METHOD_WHITELIST_DEFAULT),
- 'withdraw_close_enable' => (bool) admin_setting('withdraw_close_enable', 0),
- 'commission_distribution_enable' => (bool) admin_setting('commission_distribution_enable', 0),
- 'commission_distribution_l1' => admin_setting('commission_distribution_l1'),
- 'commission_distribution_l2' => admin_setting('commission_distribution_l2'),
- 'commission_distribution_l3' => admin_setting('commission_distribution_l3')
- ],
- 'site' => [
- 'logo' => admin_setting('logo'),
- 'force_https' => (int) admin_setting('force_https', 0),
- 'stop_register' => (int) admin_setting('stop_register', 0),
- 'app_name' => admin_setting('app_name', 'XBoard'),
- 'app_description' => admin_setting('app_description', 'XBoard is best!'),
- 'app_url' => admin_setting('app_url'),
- 'subscribe_url' => admin_setting('subscribe_url'),
- 'try_out_plan_id' => (int) admin_setting('try_out_plan_id', 0),
- 'try_out_hour' => (int) admin_setting('try_out_hour', 1),
- 'tos_url' => admin_setting('tos_url'),
- 'currency' => admin_setting('currency', 'CNY'),
- 'currency_symbol' => admin_setting('currency_symbol', '¥'),
- 'ticket_must_wait_reply' => (bool) admin_setting('ticket_must_wait_reply', 0),
- ],
- 'subscribe' => [
- 'plan_change_enable' => (bool) admin_setting('plan_change_enable', 1),
- 'reset_traffic_method' => (int) admin_setting('reset_traffic_method', 0),
- 'surplus_enable' => (bool) admin_setting('surplus_enable', 1),
- 'new_order_event_id' => (int) admin_setting('new_order_event_id', 0),
- 'renew_order_event_id' => (int) admin_setting('renew_order_event_id', 0),
- 'change_order_event_id' => (int) admin_setting('change_order_event_id', 0),
- 'show_info_to_server_enable' => (bool) admin_setting('show_info_to_server_enable', 0),
- 'show_protocol_to_server_enable' => (bool) admin_setting('show_protocol_to_server_enable', 0),
- 'default_remind_expire' => (bool) admin_setting('default_remind_expire', 1),
- 'default_remind_traffic' => (bool) admin_setting('default_remind_traffic', 1),
- 'subscribe_path' => admin_setting('subscribe_path', 's'),
- ],
- 'frontend' => [
- 'frontend_theme' => admin_setting('frontend_theme', 'Xboard'),
- 'frontend_theme_sidebar' => admin_setting('frontend_theme_sidebar', 'light'),
- 'frontend_theme_header' => admin_setting('frontend_theme_header', 'dark'),
- 'frontend_theme_color' => admin_setting('frontend_theme_color', 'default'),
- 'frontend_background_url' => admin_setting('frontend_background_url'),
- ],
- 'server' => [
- 'server_token' => admin_setting('server_token'),
- 'server_pull_interval' => admin_setting('server_pull_interval', 60),
- 'server_push_interval' => admin_setting('server_push_interval', 60),
- 'device_limit_mode' => (int) admin_setting('device_limit_mode', 0),
- 'server_ws_enable' => (bool) admin_setting('server_ws_enable', 1),
- 'server_ws_url' => admin_setting('server_ws_url', ''),
- ],
- 'email' => [
- 'email_template' => admin_setting('email_template', 'default'),
- 'email_host' => admin_setting('email_host'),
- 'email_port' => admin_setting('email_port'),
- 'email_username' => admin_setting('email_username'),
- 'email_password' => admin_setting('email_password'),
- 'email_encryption' => admin_setting('email_encryption'),
- 'email_from_address' => admin_setting('email_from_address'),
- 'remind_mail_enable' => (bool) admin_setting('remind_mail_enable', false),
- ],
- 'telegram' => [
- 'telegram_bot_enable' => (bool) admin_setting('telegram_bot_enable', 0),
- 'telegram_bot_token' => admin_setting('telegram_bot_token'),
- 'telegram_webhook_url' => admin_setting('telegram_webhook_url'),
- 'telegram_discuss_link' => admin_setting('telegram_discuss_link')
- ],
- 'app' => [
- 'windows_version' => admin_setting('windows_version', ''),
- 'windows_download_url' => admin_setting('windows_download_url', ''),
- 'macos_version' => admin_setting('macos_version', ''),
- 'macos_download_url' => admin_setting('macos_download_url', ''),
- 'android_version' => admin_setting('android_version', ''),
- 'android_download_url' => admin_setting('android_download_url', '')
- ],
- 'safe' => [
- 'email_verify' => (bool) admin_setting('email_verify', 0),
- 'safe_mode_enable' => (bool) admin_setting('safe_mode_enable', 0),
- 'secure_path' => admin_setting('secure_path', admin_setting('frontend_admin_path', hash('crc32b', config('app.key')))),
- 'email_whitelist_enable' => (bool) admin_setting('email_whitelist_enable', 0),
- 'email_whitelist_suffix' => admin_setting('email_whitelist_suffix', Dict::EMAIL_WHITELIST_SUFFIX_DEFAULT),
- 'email_gmail_limit_enable' => (bool) admin_setting('email_gmail_limit_enable', 0),
- 'captcha_enable' => (bool) admin_setting('captcha_enable', 0),
- 'captcha_type' => admin_setting('captcha_type', 'recaptcha'),
- 'recaptcha_key' => admin_setting('recaptcha_key', ''),
- 'recaptcha_site_key' => admin_setting('recaptcha_site_key', ''),
- 'recaptcha_v3_secret_key' => admin_setting('recaptcha_v3_secret_key', ''),
- 'recaptcha_v3_site_key' => admin_setting('recaptcha_v3_site_key', ''),
- 'recaptcha_v3_score_threshold' => admin_setting('recaptcha_v3_score_threshold', 0.5),
- 'turnstile_secret_key' => admin_setting('turnstile_secret_key', ''),
- 'turnstile_site_key' => admin_setting('turnstile_site_key', ''),
- 'register_limit_by_ip_enable' => (bool) admin_setting('register_limit_by_ip_enable', 0),
- 'register_limit_count' => admin_setting('register_limit_count', 3),
- 'register_limit_expire' => admin_setting('register_limit_expire', 60),
- 'password_limit_enable' => (bool) admin_setting('password_limit_enable', 1),
- 'password_limit_count' => admin_setting('password_limit_count', 5),
- 'password_limit_expire' => admin_setting('password_limit_expire', 60),
- // 保持向后兼容
- 'recaptcha_enable' => (bool) admin_setting('captcha_enable', 0)
- ],
- 'subscribe_template' => [
- 'subscribe_template_singbox' => $this->formatTemplateContent(
- subscribe_template('singbox') ?? '',
- 'json'
- ),
- 'subscribe_template_clash' => subscribe_template('clash') ?? '',
- 'subscribe_template_clashmeta' => subscribe_template('clashmeta') ?? '',
- 'subscribe_template_stash' => subscribe_template('stash') ?? '',
- 'subscribe_template_surge' => subscribe_template('surge') ?? '',
- 'subscribe_template_surfboard' => subscribe_template('surfboard') ?? ''
- ]
- ];
- }
-
- public function save(ConfigSave $request)
- {
- $data = $request->validated();
-
- $templateKeys = [
- 'subscribe_template_singbox' => 'singbox',
- 'subscribe_template_clash' => 'clash',
- 'subscribe_template_clashmeta' => 'clashmeta',
- 'subscribe_template_stash' => 'stash',
- 'subscribe_template_surge' => 'surge',
- 'subscribe_template_surfboard' => 'surfboard',
- ];
-
- foreach ($data as $k => $v) {
- if (isset($templateKeys[$k])) {
- SubscribeTemplate::setContent($templateKeys[$k], $v);
- continue;
- }
- if ($k == 'frontend_theme') {
- $themeService = app(ThemeService::class);
- $themeService->switch($v);
- }
- admin_setting([$k => $v]);
- }
-
- return $this->success(true);
- }
-
- /**
- * 格式化模板内容
- *
- * @param mixed $content 模板内容
- * @param string $format 输出格式 (json|string)
- * @return string 格式化后的内容
- */
- private function formatTemplateContent(mixed $content, string $format = 'string'): string
- {
- return match ($format) {
- 'json' => match (true) {
- is_array($content) => json_encode(
- value: $content,
- flags: JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
- ),
-
- is_string($content) && str($content)->isJson() => rescue(
- callback: fn() => json_encode(
- value: json_decode($content, associative: true, flags: JSON_THROW_ON_ERROR),
- flags: JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
- ),
- rescue: $content,
- report: false
- ),
-
- default => str($content)->toString()
- },
-
- default => str($content)->toString()
- };
- }
-
- private function getTelegramWebhookBaseUrl(): ?string
- {
- $customUrl = trim((string) admin_setting('telegram_webhook_url', ''));
- if ($customUrl !== '') {
- return rtrim($customUrl, '/');
- }
-
- $appUrl = trim((string) admin_setting('app_url', ''));
- if ($appUrl !== '') {
- return rtrim($appUrl, '/');
- }
-
- return null;
- }
-
- private function resolveTelegramWebhookUrl(): ?string
- {
- $baseUrl = $this->getTelegramWebhookBaseUrl();
- if (!$baseUrl) {
- return null;
- }
-
- if (str_contains($baseUrl, '/api/v1/guest/telegram/webhook')) {
- return $baseUrl;
- }
-
- return $baseUrl . '/api/v1/guest/telegram/webhook';
- }
-}
diff --git a/Xboard/app/Http/Controllers/V2/Admin/CouponController.php b/Xboard/app/Http/Controllers/V2/Admin/CouponController.php
deleted file mode 100644
index 364dff4..0000000
--- a/Xboard/app/Http/Controllers/V2/Admin/CouponController.php
+++ /dev/null
@@ -1,186 +0,0 @@
-has('filter')) {
- collect($request->input('filter'))->each(function ($filter) use ($builder) {
- $key = $filter['id'];
- $value = $filter['value'];
- $builder->where(function ($query) use ($key, $value) {
- if (is_array($value)) {
- $query->whereIn($key, $value);
- } else {
- $query->where($key, 'like', "%{$value}%");
- }
- });
- });
- }
-
- if ($request->has('sort')) {
- collect($request->input('sort'))->each(function ($sort) use ($builder) {
- $key = $sort['id'];
- $value = $sort['desc'] ? 'DESC' : 'ASC';
- $builder->orderBy($key, $value);
- });
- }
- }
- public function fetch(Request $request)
- {
- $current = $request->input('current', 1);
- $pageSize = $request->input('pageSize', 10);
- $builder = Coupon::query();
- $this->applyFiltersAndSorts($request, $builder);
- $coupons = $builder
- ->orderBy('created_at', 'desc')
- ->paginate($pageSize, ["*"], 'page', $current);
- return $this->paginate($coupons);
- }
-
- public function update(Request $request)
- {
- $params = $request->validate([
- 'id' => 'required|numeric',
- 'show' => 'nullable|boolean'
- ], [
- 'id.required' => '优惠券ID不能为空',
- 'id.numeric' => '优惠券ID必须为数字'
- ]);
- try {
- DB::beginTransaction();
- $coupon = Coupon::find($request->input('id'));
- if (!$coupon) {
- throw new ApiException(400201, '优惠券不存在');
- }
- $coupon->update($params);
- DB::commit();
- } catch (\Exception $e) {
- \Log::error($e);
- return $this->fail([500, '保存失败']);
- }
- }
-
- public function show(Request $request)
- {
- $request->validate([
- 'id' => 'required|numeric'
- ], [
- 'id.required' => '优惠券ID不能为空',
- 'id.numeric' => '优惠券ID必须为数字'
- ]);
- $coupon = Coupon::find($request->input('id'));
- if (!$coupon) {
- return $this->fail([400202, '优惠券不存在']);
- }
- $coupon->show = !$coupon->show;
- if (!$coupon->save()) {
- return $this->fail([500, '保存失败']);
- }
- return $this->success(true);
- }
-
- public function generate(CouponGenerate $request)
- {
- if ($request->input('generate_count')) {
- $this->multiGenerate($request);
- return;
- }
-
- $params = $request->validated();
- if (!$request->input('id')) {
- if (!isset($params['code'])) {
- $params['code'] = Helper::randomChar(8);
- }
- if (!Coupon::create($params)) {
- return $this->fail([500, '创建失败']);
- }
- } else {
- try {
- Coupon::find($request->input('id'))->update($params);
- } catch (\Exception $e) {
- \Log::error($e);
- return $this->fail([500, '保存失败']);
- }
- }
-
- return $this->success(true);
- }
-
- private function multiGenerate(CouponGenerate $request)
- {
- $coupons = [];
- $coupon = $request->validated();
- $coupon['created_at'] = $coupon['updated_at'] = time();
- $coupon['show'] = 1;
- unset($coupon['generate_count']);
- for ($i = 0; $i < $request->input('generate_count'); $i++) {
- $coupon['code'] = Helper::randomChar(8);
- array_push($coupons, $coupon);
- }
- try {
- DB::beginTransaction();
- if (
- !Coupon::insert(array_map(function ($item) use ($coupon) {
- // format data
- if (isset($item['limit_plan_ids']) && is_array($item['limit_plan_ids'])) {
- $item['limit_plan_ids'] = json_encode($coupon['limit_plan_ids']);
- }
- if (isset($item['limit_period']) && is_array($item['limit_period'])) {
- $item['limit_period'] = json_encode($coupon['limit_period']);
- }
- return $item;
- }, $coupons))
- ) {
- throw new \Exception();
- }
- DB::commit();
- } catch (\Exception $e) {
- DB::rollBack();
- return $this->fail([500, '生成失败']);
- }
-
- $data = "名称,类型,金额或比例,开始时间,结束时间,可用次数,可用于订阅,券码,生成时间\r\n";
- foreach ($coupons as $coupon) {
- $type = ['', '金额', '比例'][$coupon['type']];
- $value = ['', ($coupon['value'] / 100), $coupon['value']][$coupon['type']];
- $startTime = date('Y-m-d H:i:s', $coupon['started_at']);
- $endTime = date('Y-m-d H:i:s', $coupon['ended_at']);
- $limitUse = $coupon['limit_use'] ?? '不限制';
- $createTime = date('Y-m-d H:i:s', $coupon['created_at']);
- $limitPlanIds = isset($coupon['limit_plan_ids']) ? implode("/", $coupon['limit_plan_ids']) : '不限制';
- $data .= "{$coupon['name']},{$type},{$value},{$startTime},{$endTime},{$limitUse},{$limitPlanIds},{$coupon['code']},{$createTime}\r\n";
- }
- echo $data;
- }
-
- public function drop(Request $request)
- {
- $request->validate([
- 'id' => 'required|numeric'
- ], [
- 'id.required' => '优惠券ID不能为空',
- 'id.numeric' => '优惠券ID必须为数字'
- ]);
- $coupon = Coupon::find($request->input('id'));
- if (!$coupon) {
- return $this->fail([400202, '优惠券不存在']);
- }
- if (!$coupon->delete()) {
- return $this->fail([500, '删除失败']);
- }
-
- return $this->success(true);
- }
-}
diff --git a/Xboard/app/Http/Controllers/V2/Admin/GiftCardController.php b/Xboard/app/Http/Controllers/V2/Admin/GiftCardController.php
deleted file mode 100644
index 2c8ecf5..0000000
--- a/Xboard/app/Http/Controllers/V2/Admin/GiftCardController.php
+++ /dev/null
@@ -1,622 +0,0 @@
-validate([
- 'type' => 'integer|min:1|max:10',
- 'status' => 'integer|in:0,1',
- 'page' => 'integer|min:1',
- 'per_page' => 'integer|min:1|max:1000',
- ]);
-
- $query = GiftCardTemplate::query();
-
- if ($request->has('type')) {
- $query->where('type', $request->input('type'));
- }
-
- if ($request->has('status')) {
- $query->where('status', $request->input('status'));
- }
-
- $perPage = $request->input('per_page', 15);
- $templates = $query->orderBy('sort', 'asc')
- ->orderBy('created_at', 'desc')
- ->paginate($perPage);
-
- $data = $templates->getCollection()->map(function ($template) {
- return [
- 'id' => $template->id,
- 'name' => $template->name,
- 'description' => $template->description,
- 'type' => $template->type,
- 'type_name' => $template->type_name,
- 'status' => $template->status,
- 'conditions' => $template->conditions,
- 'rewards' => $template->rewards,
- 'limits' => $template->limits,
- 'special_config' => $template->special_config,
- 'icon' => $template->icon,
- 'background_image' => $template->background_image,
- 'theme_color' => $template->theme_color,
- 'sort' => $template->sort,
- 'admin_id' => $template->admin_id,
- 'created_at' => $template->created_at,
- 'updated_at' => $template->updated_at,
- // 统计信息
- 'codes_count' => $template->codes()->count(),
- 'used_count' => $template->usages()->count(),
- ];
- })->values();
-
- return $this->paginate( $templates);
- }
-
- /**
- * 创建礼品卡模板
- */
- public function createTemplate(Request $request)
- {
- $request->validate([
- 'name' => 'required|string|max:255',
- 'description' => 'nullable|string',
- 'type' => [
- 'required',
- 'integer',
- Rule::in(array_keys(GiftCardTemplate::getTypeMap()))
- ],
- 'status' => 'boolean',
- 'conditions' => 'nullable|array',
- 'rewards' => 'required|array',
- 'limits' => 'nullable|array',
- 'special_config' => 'nullable|array',
- 'icon' => 'nullable|string|max:255',
- 'background_image' => 'nullable|string|url|max:255',
- 'theme_color' => 'nullable|string|regex:/^#[0-9A-Fa-f]{6}$/',
- 'sort' => 'integer|min:0',
- ], [
- 'name.required' => '礼品卡名称不能为空',
- 'type.required' => '礼品卡类型不能为空',
- 'type.in' => '无效的礼品卡类型',
- 'rewards.required' => '奖励配置不能为空',
- 'theme_color.regex' => '主题色格式不正确',
- 'background_image.url' => '背景图片必须是有效的URL',
- ]);
-
- try {
- $template = GiftCardTemplate::create([
- 'name' => $request->input('name'),
- 'description' => $request->input('description'),
- 'type' => $request->input('type'),
- 'status' => $request->input('status', true),
- 'conditions' => $request->input('conditions'),
- 'rewards' => $request->input('rewards'),
- 'limits' => $request->input('limits'),
- 'special_config' => $request->input('special_config'),
- 'icon' => $request->input('icon'),
- 'background_image' => $request->input('background_image'),
- 'theme_color' => $request->input('theme_color', '#1890ff'),
- 'sort' => $request->input('sort', 0),
- 'admin_id' => $request->user()->id,
- 'created_at' => time(),
- 'updated_at' => time(),
- ]);
-
- return $this->success($template);
- } catch (\Exception $e) {
- Log::error('创建礼品卡模板失败', [
- 'admin_id' => $request->user()->id,
- 'data' => $request->all(),
- 'error' => $e->getMessage(),
- ]);
- return $this->fail([500, '创建失败']);
- }
- }
-
- /**
- * 更新礼品卡模板
- */
- public function updateTemplate(Request $request)
- {
- $validatedData = $request->validate([
- 'id' => 'required|integer|exists:v2_gift_card_template,id',
- 'name' => 'sometimes|required|string|max:255',
- 'description' => 'sometimes|nullable|string',
- 'type' => [
- 'sometimes',
- 'required',
- 'integer',
- Rule::in(array_keys(GiftCardTemplate::getTypeMap()))
- ],
- 'status' => 'sometimes|boolean',
- 'conditions' => 'sometimes|nullable|array',
- 'rewards' => 'sometimes|required|array',
- 'limits' => 'sometimes|nullable|array',
- 'special_config' => 'sometimes|nullable|array',
- 'icon' => 'sometimes|nullable|string|max:255',
- 'background_image' => 'sometimes|nullable|string|url|max:255',
- 'theme_color' => 'sometimes|nullable|string|regex:/^#[0-9A-Fa-f]{6}$/',
- 'sort' => 'sometimes|integer|min:0',
- ]);
-
- $template = GiftCardTemplate::find($validatedData['id']);
- if (!$template) {
- return $this->fail([404, '模板不存在']);
- }
-
- try {
- $updateData = collect($validatedData)->except('id')->all();
-
- if (empty($updateData)) {
- return $this->success($template);
- }
-
- $updateData['updated_at'] = time();
-
- $template->update($updateData);
-
- return $this->success($template->fresh());
- } catch (\Exception $e) {
- Log::error('更新礼品卡模板失败', [
- 'admin_id' => $request->user()->id,
- 'template_id' => $template->id,
- 'error' => $e->getMessage(),
- ]);
- return $this->fail([500, '更新失败']);
- }
- }
-
- /**
- * 删除礼品卡模板
- */
- public function deleteTemplate(Request $request)
- {
- $request->validate([
- 'id' => 'required|integer|exists:v2_gift_card_template,id',
- ]);
-
- $template = GiftCardTemplate::find($request->input('id'));
- if (!$template) {
- return $this->fail([404, '模板不存在']);
- }
-
- // 检查是否有关联的兑换码
- if ($template->codes()->exists()) {
- return $this->fail([400, '该模板下存在兑换码,无法删除']);
- }
-
- try {
- $template->delete();
- return $this->success(true);
- } catch (\Exception $e) {
- Log::error('删除礼品卡模板失败', [
- 'admin_id' => $request->user()->id,
- 'template_id' => $template->id,
- 'error' => $e->getMessage(),
- ]);
- return $this->fail([500, '删除失败']);
- }
- }
-
- /**
- * 生成兑换码
- */
- public function generateCodes(Request $request)
- {
- $request->validate([
- 'template_id' => 'required|integer|exists:v2_gift_card_template,id',
- 'count' => 'required|integer|min:1|max:10000',
- 'prefix' => 'nullable|string|max:10|regex:/^[A-Z0-9]*$/',
- 'expires_hours' => 'nullable|integer|min:1',
- 'max_usage' => 'integer|min:1|max:1000',
- ], [
- 'template_id.required' => '请选择礼品卡模板',
- 'count.required' => '请指定生成数量',
- 'count.max' => '单次最多生成10000个兑换码',
- 'prefix.regex' => '前缀只能包含大写字母和数字',
- ]);
-
- $template = GiftCardTemplate::find($request->input('template_id'));
- if (!$template->isAvailable()) {
- return $this->fail([400, '模板已被禁用']);
- }
-
- try {
- $options = [
- 'prefix' => $request->input('prefix', 'GC'),
- 'max_usage' => $request->input('max_usage', 1),
- ];
-
- if ($request->has('expires_hours')) {
- $options['expires_at'] = time() + ($request->input('expires_hours') * 3600);
- }
-
- $batchId = GiftCardCode::batchGenerate(
- $request->input('template_id'),
- $request->input('count'),
- $options
- );
-
- // 查询本次生成的所有兑换码
- $codes = GiftCardCode::where('batch_id', $batchId)->get();
-
- // 判断是否导出 CSV
- if ($request->input('download_csv')) {
- $headers = [
- 'Content-Type' => 'text/csv',
- 'Content-Disposition' => 'attachment; filename="gift_codes.csv"',
- ];
- $callback = function () use ($codes, $template) {
- $handle = fopen('php://output', 'w');
- // 表头
- fputcsv($handle, [
- '兑换码',
- '前缀',
- '有效期',
- '最大使用次数',
- '批次号',
- '创建时间',
- '模板名称',
- '模板类型',
- '模板奖励',
- '状态',
- '使用者',
- '使用时间',
- '备注'
- ]);
- foreach ($codes as $code) {
- $expireDate = $code->expires_at ? date('Y-m-d H:i:s', $code->expires_at) : '长期有效';
- $createDate = date('Y-m-d H:i:s', $code->created_at);
- $templateName = $template->name ?? '';
- $templateType = $template->type ?? '';
- $templateRewards = $template->rewards ? json_encode($template->rewards, JSON_UNESCAPED_UNICODE) : '';
- // 状态判断
- $status = $code->status_name;
- $usedBy = $code->user_id ?? '';
- $usedAt = $code->used_at ? date('Y-m-d H:i:s', $code->used_at) : '';
- $remark = $code->remark ?? '';
- fputcsv($handle, [
- $code->code,
- $code->prefix ?? '',
- $expireDate,
- $code->max_usage,
- $code->batch_id,
- $createDate,
- $templateName,
- $templateType,
- $templateRewards,
- $status,
- $usedBy,
- $usedAt,
- $remark,
- ]);
- }
- fclose($handle);
- };
- return response()->streamDownload($callback, 'gift_codes.csv', $headers);
- }
-
- Log::info('批量生成兑换码', [
- 'admin_id' => $request->user()->id,
- 'template_id' => $request->input('template_id'),
- 'count' => $request->input('count'),
- 'batch_id' => $batchId,
- ]);
-
- return $this->success([
- 'batch_id' => $batchId,
- 'count' => $request->input('count'),
- 'message' => '生成成功',
- ]);
- } catch (\Exception $e) {
- Log::error('生成兑换码失败', [
- 'admin_id' => $request->user()->id,
- 'data' => $request->all(),
- 'error' => $e->getMessage(),
- ]);
- return $this->fail([500, '生成失败']);
- }
- }
-
- /**
- * 获取兑换码列表
- */
- public function codes(Request $request)
- {
- $request->validate([
- 'template_id' => 'integer|exists:v2_gift_card_template,id',
- 'batch_id' => 'string',
- 'status' => 'integer|in:0,1,2,3',
- 'page' => 'integer|min:1',
- 'per_page' => 'integer|min:1|max:500',
- ]);
-
- $query = GiftCardCode::with(['template', 'user']);
-
- if ($request->has('template_id')) {
- $query->where('template_id', $request->input('template_id'));
- }
-
- if ($request->has('batch_id')) {
- $query->where('batch_id', $request->input('batch_id'));
- }
-
- if ($request->has('status')) {
- $query->where('status', $request->input('status'));
- }
-
- $perPage = $request->input('per_page', 15);
- $codes = $query->orderBy('created_at', 'desc')->paginate($perPage);
-
- $data = $codes->getCollection()->map(function ($code) {
- return [
- 'id' => $code->id,
- 'template_id' => $code->template_id,
- 'template_name' => $code->template->name ?? '',
- 'code' => $code->code,
- 'batch_id' => $code->batch_id,
- 'status' => $code->status,
- 'status_name' => $code->status_name,
- 'user_id' => $code->user_id,
- 'user_email' => $code->user ? (substr($code->user->email ?? '', 0, 3) . '***@***') : null,
- 'used_at' => $code->used_at,
- 'expires_at' => $code->expires_at,
- 'usage_count' => $code->usage_count,
- 'max_usage' => $code->max_usage,
- 'created_at' => $code->created_at,
- ];
- })->values();
-
- return $this->paginate($codes);
- }
-
- /**
- * 禁用/启用兑换码
- */
- public function toggleCode(Request $request)
- {
- $request->validate([
- 'id' => 'required|integer|exists:v2_gift_card_code,id',
- 'action' => 'required|string|in:disable,enable',
- ]);
-
- $code = GiftCardCode::find($request->input('id'));
- if (!$code) {
- return $this->fail([404, '兑换码不存在']);
- }
-
- try {
- if ($request->input('action') === 'disable') {
- $code->markAsDisabled();
- } else {
- if ($code->status === GiftCardCode::STATUS_DISABLED) {
- $code->status = GiftCardCode::STATUS_UNUSED;
- $code->save();
- }
- }
-
- return $this->success([
- 'message' => $request->input('action') === 'disable' ? '已禁用' : '已启用',
- ]);
- } catch (\Exception $e) {
- return $this->fail([500, '操作失败']);
- }
- }
-
- /**
- * 导出兑换码
- */
- public function exportCodes(Request $request)
- {
- $request->validate([
- 'batch_id' => 'required|string|exists:v2_gift_card_code,batch_id',
- ]);
-
- $codes = GiftCardCode::where('batch_id', $request->input('batch_id'))
- ->orderBy('created_at', 'asc')
- ->get(['code']);
-
- $content = $codes->pluck('code')->implode("\n");
-
- return response($content)
- ->header('Content-Type', 'text/plain')
- ->header('Content-Disposition', 'attachment; filename="gift_cards_' . $request->input('batch_id') . '.txt"');
- }
-
- /**
- * 获取使用记录
- */
- public function usages(Request $request)
- {
- $request->validate([
- 'template_id' => 'integer|exists:v2_gift_card_template,id',
- 'user_id' => 'integer|exists:v2_user,id',
- 'page' => 'integer|min:1',
- 'per_page' => 'integer|min:1|max:500',
- ]);
-
- $query = GiftCardUsage::with(['template', 'code', 'user', 'inviteUser']);
-
- if ($request->has('template_id')) {
- $query->where('template_id', $request->input('template_id'));
- }
-
- if ($request->has('user_id')) {
- $query->where('user_id', $request->input('user_id'));
- }
-
- $perPage = $request->input('per_page', 15);
- $usages = $query->orderBy('created_at', 'desc')->paginate($perPage);
-
- $usages->transform(function ($usage) {
- return [
- 'id' => $usage->id,
- 'code' => $usage->code->code ?? '',
- 'template_name' => $usage->template->name ?? '',
- 'user_email' => $usage->user->email ?? '',
- 'invite_user_email' => $usage->inviteUser ? (substr($usage->inviteUser->email ?? '', 0, 3) . '***@***') : null,
- 'rewards_given' => $usage->rewards_given,
- 'invite_rewards' => $usage->invite_rewards,
- 'multiplier_applied' => $usage->multiplier_applied,
- 'created_at' => $usage->created_at,
- ];
- })->values();
- return $this->paginate($usages);
- }
-
- /**
- * 获取统计数据
- */
- public function statistics(Request $request)
- {
- $request->validate([
- 'start_date' => 'date_format:Y-m-d',
- 'end_date' => 'date_format:Y-m-d',
- ]);
-
- $startDate = $request->input('start_date', date('Y-m-d', strtotime('-30 days')));
- $endDate = $request->input('end_date', date('Y-m-d'));
-
- // 总体统计
- $totalStats = [
- 'templates_count' => GiftCardTemplate::count(),
- 'active_templates_count' => GiftCardTemplate::where('status', 1)->count(),
- 'codes_count' => GiftCardCode::count(),
- 'used_codes_count' => GiftCardCode::where('status', GiftCardCode::STATUS_USED)->count(),
- 'usages_count' => GiftCardUsage::count(),
- ];
-
- // 每日使用统计
- $driver = DB::connection()->getDriverName();
- $dateExpression = "date(created_at, 'unixepoch')"; // Default for SQLite
- if ($driver === 'mysql') {
- $dateExpression = 'DATE(FROM_UNIXTIME(created_at))';
- } elseif ($driver === 'pgsql') {
- $dateExpression = 'date(to_timestamp(created_at))';
- }
-
- $dailyUsages = GiftCardUsage::selectRaw("{$dateExpression} as date, COUNT(*) as count")
- ->whereRaw("{$dateExpression} BETWEEN ? AND ?", [$startDate, $endDate])
- ->groupBy('date')
- ->orderBy('date')
- ->get();
-
- // 类型统计
- $typeStats = GiftCardUsage::with('template')
- ->selectRaw('template_id, COUNT(*) as count')
- ->groupBy('template_id')
- ->get()
- ->map(function ($item) {
- return [
- 'template_name' => $item->template->name ?? '',
- 'type_name' => $item->template->type_name ?? '',
- 'count' => $item->count ?? 0,
- ];
- });
-
- return $this->success([
- 'total_stats' => $totalStats,
- 'daily_usages' => $dailyUsages,
- 'type_stats' => $typeStats,
- ]);
- }
-
- /**
- * 获取所有可用的礼品卡类型
- */
- public function types()
- {
- return $this->success(GiftCardTemplate::getTypeMap());
- }
-
- /**
- * 更新单个兑换码
- */
- public function updateCode(Request $request)
- {
- $validatedData = $request->validate([
- 'id' => 'required|integer|exists:v2_gift_card_code,id',
- 'expires_at' => 'sometimes|nullable|integer',
- 'max_usage' => 'sometimes|integer|min:1|max:1000',
- 'status' => 'sometimes|integer|in:0,1,2,3',
- ]);
-
- $code = GiftCardCode::find($validatedData['id']);
- if (!$code) {
- return $this->fail([404, '礼品卡不存在']);
- }
-
- try {
- $updateData = collect($validatedData)->except('id')->all();
-
- if (empty($updateData)) {
- return $this->success($code);
- }
-
- $updateData['updated_at'] = time();
- $code->update($updateData);
-
- return $this->success($code->fresh());
- } catch (\Exception $e) {
- Log::error('更新礼品卡信息失败', [
- 'admin_id' => $request->user()->id,
- 'code_id' => $code->id,
- 'error' => $e->getMessage(),
- ]);
- return $this->fail([500, '更新失败']);
- }
- }
-
- /**
- * 删除礼品卡
- */
- public function deleteCode(Request $request)
- {
- $request->validate([
- 'id' => 'required|integer|exists:v2_gift_card_code,id',
- ]);
-
- $code = GiftCardCode::find($request->input('id'));
- if (!$code) {
- return $this->fail([404, '礼品卡不存在']);
- }
-
- // 检查是否已被使用
- if ($code->status === GiftCardCode::STATUS_USED) {
- return $this->fail([400, '该礼品卡已被使用,无法删除']);
- }
-
- try {
- // 检查是否有关联的使用记录
- if ($code->usages()->exists()) {
- return $this->fail([400, '该礼品卡存在使用记录,无法删除']);
- }
-
- $code->delete();
- return $this->success(['message' => '删除成功']);
- } catch (\Exception $e) {
- Log::error('删除礼品卡失败', [
- 'admin_id' => $request->user()->id,
- 'code_id' => $code->id,
- 'error' => $e->getMessage(),
- ]);
- return $this->fail([500, '删除失败']);
- }
- }
-}
diff --git a/Xboard/app/Http/Controllers/V2/Admin/KnowledgeController.php b/Xboard/app/Http/Controllers/V2/Admin/KnowledgeController.php
deleted file mode 100644
index d773a8d..0000000
--- a/Xboard/app/Http/Controllers/V2/Admin/KnowledgeController.php
+++ /dev/null
@@ -1,113 +0,0 @@
-input('id')) {
- $knowledge = Knowledge::find($request->input('id'))->toArray();
- if (!$knowledge)
- return $this->fail([400202, '知识不存在']);
- return $this->success($knowledge);
- }
- $data = Knowledge::select(['title', 'id', 'updated_at', 'category', 'show'])
- ->orderBy('sort', 'ASC')
- ->get();
- return $this->success($data);
- }
-
- public function getCategory(Request $request)
- {
- return $this->success(array_keys(Knowledge::get()->groupBy('category')->toArray()));
- }
-
- public function save(KnowledgeSave $request)
- {
- $params = $request->validated();
-
- if (!$request->input('id')) {
- if (!Knowledge::create($params)) {
- return $this->fail([500, '创建失败']);
- }
- } else {
- try {
- Knowledge::find($request->input('id'))->update($params);
- } catch (\Exception $e) {
- \Log::error($e);
- return $this->fail([500, '创建失败']);
- }
- }
-
- return $this->success(true);
- }
-
- public function show(Request $request)
- {
- $request->validate([
- 'id' => 'required|numeric'
- ], [
- 'id.required' => '知识库ID不能为空'
- ]);
- $knowledge = Knowledge::find($request->input('id'));
- if (!$knowledge) {
- throw new ApiException('知识不存在');
- }
- $knowledge->show = !$knowledge->show;
- if (!$knowledge->save()) {
- throw new ApiException('保存失败');
- }
-
- return $this->success(true);
- }
-
- public function sort(Request $request)
- {
- $request->validate([
- 'ids' => 'required|array'
- ], [
- 'ids.required' => '参数有误',
- 'ids.array' => '参数有误'
- ]);
- try {
- DB::beginTransaction();
- foreach ($request->input('ids') as $k => $v) {
- $knowledge = Knowledge::find($v);
- $knowledge->timestamps = false;
- $knowledge->update(['sort' => $k + 1]);
- }
- DB::commit();
- } catch (\Exception $e) {
- DB::rollBack();
- throw new ApiException('保存失败');
- }
- return $this->success(true);
- }
-
- public function drop(Request $request)
- {
- $request->validate([
- 'id' => 'required|numeric'
- ], [
- 'id.required' => '知识库ID不能为空'
- ]);
- $knowledge = Knowledge::find($request->input('id'));
- if (!$knowledge) {
- return $this->fail([400202, '知识不存在']);
- }
- if (!$knowledge->delete()) {
- return $this->fail([500, '删除失败']);
- }
-
- return $this->success(true);
- }
-}
diff --git a/Xboard/app/Http/Controllers/V2/Admin/NoticeController.php b/Xboard/app/Http/Controllers/V2/Admin/NoticeController.php
deleted file mode 100644
index 854b313..0000000
--- a/Xboard/app/Http/Controllers/V2/Admin/NoticeController.php
+++ /dev/null
@@ -1,101 +0,0 @@
-success(
- Notice::orderBy('sort', 'ASC')
- ->orderBy('id', 'DESC')
- ->get()
- );
- }
-
- public function save(NoticeSave $request)
- {
- $data = $request->only([
- 'title',
- 'content',
- 'img_url',
- 'tags',
- 'show',
- 'popup'
- ]);
- if (!$request->input('id')) {
- if (!Notice::create($data)) {
- return $this->fail([500, '保存失败']);
- }
- } else {
- try {
- Notice::find($request->input('id'))->update($data);
- } catch (\Exception $e) {
- return $this->fail([500, '保存失败']);
- }
- }
- return $this->success(true);
- }
-
-
-
- public function show(Request $request)
- {
- if (empty($request->input('id'))) {
- return $this->fail([500, '公告ID不能为空']);
- }
- $notice = Notice::find($request->input('id'));
- if (!$notice) {
- return $this->fail([400202, '公告不存在']);
- }
- $notice->show = $notice->show ? 0 : 1;
- if (!$notice->save()) {
- return $this->fail([500, '保存失败']);
- }
-
- return $this->success(true);
- }
-
- public function drop(Request $request)
- {
- if (empty($request->input('id'))) {
- return $this->fail([422, '公告ID不能为空']);
- }
- $notice = Notice::find($request->input('id'));
- if (!$notice) {
- return $this->fail([400202, '公告不存在']);
- }
- if (!$notice->delete()) {
- return $this->fail([500, '删除失败']);
- }
- return $this->success(true);
- }
-
- public function sort(Request $request)
- {
- $params = $request->validate([
- 'ids' => 'required|array'
- ]);
-
- try {
- DB::beginTransaction();
- foreach ($params['ids'] as $k => $v) {
- $notice = Notice::findOrFail($v);
- $notice->update(['sort' => $k + 1]);
- }
- DB::commit();
- return $this->success(true);
- } catch (\Exception $e) {
- DB::rollBack();
- \Log::error($e);
- return $this->fail([500, '排序保存失败']);
- }
- }
-}
diff --git a/Xboard/app/Http/Controllers/V2/Admin/OrderController.php b/Xboard/app/Http/Controllers/V2/Admin/OrderController.php
deleted file mode 100644
index c06c7c8..0000000
--- a/Xboard/app/Http/Controllers/V2/Admin/OrderController.php
+++ /dev/null
@@ -1,252 +0,0 @@
-find($request->input('id'));
- if (!$order)
- return $this->fail([400202, '订单不存在']);
- if ($order->surplus_order_ids) {
- $order['surplus_orders'] = Order::whereIn('id', $order->surplus_order_ids)->get();
- }
- $order['period'] = PlanService::getLegacyPeriod((string) $order->period);
- return $this->success($order);
- }
-
- public function fetch(Request $request)
- {
- $current = $request->input('current', 1);
- $pageSize = $request->input('pageSize', 10);
- $orderModel = Order::with('plan:id,name');
-
- if ($request->boolean('is_commission')) {
- $orderModel->whereNotNull('invite_user_id')
- ->whereNotIn('status', [0, 2])
- ->where('commission_balance', '>', 0);
- }
-
- $this->applyFiltersAndSorts($request, $orderModel);
-
- /** @var \Illuminate\Pagination\LengthAwarePaginator $paginatedResults */
- $paginatedResults = $orderModel
- ->latest('created_at')
- ->paginate(
- perPage: $pageSize,
- page: $current
- );
-
- $paginatedResults->getCollection()->transform(function ($order) {
- $orderArray = $order->toArray();
- $orderArray['period'] = PlanService::getLegacyPeriod((string) $order->period);
- return $orderArray;
- });
-
- return $this->paginate($paginatedResults);
- }
-
- private function applyFiltersAndSorts(Request $request, Builder $builder): void
- {
- $this->applyFilters($request, $builder);
- $this->applySorting($request, $builder);
- }
-
- private function applyFilters(Request $request, Builder $builder): void
- {
- if (!$request->has('filter')) {
- return;
- }
-
- collect($request->input('filter'))->each(function ($filter) use ($builder) {
- $field = $filter['id'];
- $value = $filter['value'];
-
- $builder->where(function ($query) use ($field, $value) {
- $this->buildFilterQuery($query, $field, $value);
- });
- });
- }
-
- private function buildFilterQuery(Builder $query, string $field, mixed $value): void
- {
- // Handle array values for 'in' operations
- if (is_array($value)) {
- $query->whereIn($field, $value);
- return;
- }
-
- // Handle operator-based filtering
- if (!is_string($value) || !str_contains($value, ':')) {
- $query->where($field, 'like', "%{$value}%");
- return;
- }
-
- [$operator, $filterValue] = explode(':', $value, 2);
-
- // Convert numeric strings to appropriate type
- if (is_numeric($filterValue)) {
- $filterValue = strpos($filterValue, '.') !== false
- ? (float) $filterValue
- : (int) $filterValue;
- }
-
- // Apply operator
- $query->where($field, match (strtolower($operator)) {
- 'eq' => '=',
- 'gt' => '>',
- 'gte' => '>=',
- 'lt' => '<',
- 'lte' => '<=',
- 'like' => 'like',
- 'notlike' => 'not like',
- 'null' => static fn($q) => $q->whereNull($field),
- 'notnull' => static fn($q) => $q->whereNotNull($field),
- default => 'like'
- }, match (strtolower($operator)) {
- 'like', 'notlike' => "%{$filterValue}%",
- 'null', 'notnull' => null,
- default => $filterValue
- });
- }
-
- private function applySorting(Request $request, Builder $builder): void
- {
- if (!$request->has('sort')) {
- return;
- }
-
- collect($request->input('sort'))->each(function ($sort) use ($builder) {
- $field = $sort['id'];
- $direction = $sort['desc'] ? 'DESC' : 'ASC';
- $builder->orderBy($field, $direction);
- });
- }
-
- public function paid(Request $request)
- {
- $order = Order::where('trade_no', $request->input('trade_no'))
- ->first();
- if (!$order) {
- return $this->fail([400202, '订单不存在']);
- }
- if ($order->status !== 0)
- return $this->fail([400, '只能对待支付的订单进行操作']);
-
- $orderService = new OrderService($order);
- if (!$orderService->paid('manual_operation')) {
- return $this->fail([500, '更新失败']);
- }
- return $this->success(true);
- }
-
- public function cancel(Request $request)
- {
- $order = Order::where('trade_no', $request->input('trade_no'))
- ->first();
- if (!$order) {
- return $this->fail([400202, '订单不存在']);
- }
- if ($order->status !== 0)
- return $this->fail([400, '只能对待支付的订单进行操作']);
-
- $orderService = new OrderService($order);
- if (!$orderService->cancel()) {
- return $this->fail([400, '更新失败']);
- }
- return $this->success(true);
- }
-
- public function update(OrderUpdate $request)
- {
- $params = $request->only([
- 'commission_status'
- ]);
-
- $order = Order::where('trade_no', $request->input('trade_no'))
- ->first();
- if (!$order) {
- return $this->fail([400202, '订单不存在']);
- }
-
- try {
- $order->update($params);
- } catch (\Exception $e) {
- Log::error($e);
- return $this->fail([500, '更新失败']);
- }
-
- return $this->success(true);
- }
-
- public function assign(OrderAssign $request)
- {
- $plan = Plan::find($request->input('plan_id'));
- $user = User::byEmail($request->input('email'))->first();
-
- if (!$user) {
- return $this->fail([400202, '该用户不存在']);
- }
-
- if (!$plan) {
- return $this->fail([400202, '该订阅不存在']);
- }
-
- $userService = new UserService();
- if ($userService->isNotCompleteOrderByUserId($user->id)) {
- return $this->fail([400, '该用户还有待支付的订单,无法分配']);
- }
-
- try {
- DB::beginTransaction();
- $order = new Order();
- $orderService = new OrderService($order);
- $order->user_id = $user->id;
- $order->plan_id = $plan->id;
- $period = $request->input('period');
- $order->period = PlanService::getPeriodKey((string) $period);
- $order->trade_no = Helper::guid();
- $order->total_amount = $request->input('total_amount');
-
- if (PlanService::getPeriodKey((string) $order->period) === Plan::PERIOD_RESET_TRAFFIC) {
- $order->type = Order::TYPE_RESET_TRAFFIC;
- } else if ($user->plan_id !== NULL && $order->plan_id !== $user->plan_id) {
- $order->type = Order::TYPE_UPGRADE;
- } else if ($user->expired_at > time() && $order->plan_id == $user->plan_id) {
- $order->type = Order::TYPE_RENEWAL;
- } else {
- $order->type = Order::TYPE_NEW_PURCHASE;
- }
-
- $orderService->setInvite($user);
-
- if (!$order->save()) {
- DB::rollBack();
- return $this->fail([500, '订单创建失败']);
- }
- DB::commit();
- } catch (\Exception $e) {
- DB::rollBack();
- throw $e;
- }
-
- return $this->success($order->trade_no);
- }
-}
diff --git a/Xboard/app/Http/Controllers/V2/Admin/PaymentController.php b/Xboard/app/Http/Controllers/V2/Admin/PaymentController.php
deleted file mode 100644
index 6649aaa..0000000
--- a/Xboard/app/Http/Controllers/V2/Admin/PaymentController.php
+++ /dev/null
@@ -1,133 +0,0 @@
-success(array_unique($methods));
- }
-
- public function fetch()
- {
- $payments = Payment::orderBy('sort', 'ASC')->get();
- foreach ($payments as $k => $v) {
- $notifyUrl = url("/api/v1/guest/payment/notify/{$v->payment}/{$v->uuid}");
- if ($v->notify_domain) {
- $parseUrl = parse_url($notifyUrl);
- $notifyUrl = $v->notify_domain . $parseUrl['path'];
- }
- $payments[$k]['notify_url'] = $notifyUrl;
- }
- return $this->success($payments);
- }
-
- public function getPaymentForm(Request $request)
- {
- try {
- $paymentService = new PaymentService($request->input('payment'), $request->input('id'));
- return $this->success(collect($paymentService->form()));
- } catch (\Exception $e) {
- return $this->fail([400, '支付方式不存在或未启用']);
- }
- }
-
- public function show(Request $request)
- {
- $payment = Payment::find($request->input('id'));
- if (!$payment)
- return $this->fail([400202, '支付方式不存在']);
- $payment->enable = !$payment->enable;
- if (!$payment->save())
- return $this->fail([500, '保存失败']);
- return $this->success(true);
- }
-
- public function save(Request $request)
- {
- if (!admin_setting('app_url')) {
- return $this->fail([400, '请在站点配置中配置站点地址']);
- }
- $params = $request->validate([
- 'name' => 'required',
- 'icon' => 'nullable',
- 'payment' => 'required',
- 'config' => 'required',
- 'notify_domain' => 'nullable|url',
- 'handling_fee_fixed' => 'nullable|integer',
- 'handling_fee_percent' => 'nullable|numeric|between:0,100'
- ], [
- 'name.required' => '显示名称不能为空',
- 'payment.required' => '网关参数不能为空',
- 'config.required' => '配置参数不能为空',
- 'notify_domain.url' => '自定义通知域名格式有误',
- 'handling_fee_fixed.integer' => '固定手续费格式有误',
- 'handling_fee_percent.between' => '百分比手续费范围须在0-100之间'
- ]);
- if ($request->input('id')) {
- $payment = Payment::find($request->input('id'));
- if (!$payment)
- return $this->fail([400202, '支付方式不存在']);
- try {
- $payment->update($params);
- } catch (\Exception $e) {
- Log::error($e);
- return $this->fail([500, '保存失败']);
- }
- return $this->success(true);
- }
- $params['uuid'] = Helper::randomChar(8);
- if (!Payment::create($params)) {
- return $this->fail([500, '保存失败']);
- }
- return $this->success(true);
- }
-
- public function drop(Request $request)
- {
- $payment = Payment::find($request->input('id'));
- if (!$payment)
- return $this->fail([400202, '支付方式不存在']);
- return $this->success($payment->delete());
- }
-
-
- public function sort(Request $request)
- {
- $request->validate([
- 'ids' => 'required|array'
- ], [
- 'ids.required' => '参数有误',
- 'ids.array' => '参数有误'
- ]);
- try {
- DB::beginTransaction();
- foreach ($request->input('ids') as $k => $v) {
- if (!Payment::find($v)->update(['sort' => $k + 1])) {
- throw new \Exception();
- }
- }
- DB::commit();
- } catch (\Exception $e) {
- DB::rollBack();
- return $this->fail([500, '保存失败']);
- }
-
- return $this->success(true);
- }
-}
diff --git a/Xboard/app/Http/Controllers/V2/Admin/PlanController.php b/Xboard/app/Http/Controllers/V2/Admin/PlanController.php
deleted file mode 100644
index 6a39f9f..0000000
--- a/Xboard/app/Http/Controllers/V2/Admin/PlanController.php
+++ /dev/null
@@ -1,132 +0,0 @@
-with([
- 'group:id,name'
- ])
- ->withCount([
- 'users',
- 'users as active_users_count' => function ($query) {
- $query->where(function ($q) {
- $q->where('expired_at', '>', time())
- ->orWhereNull('expired_at');
- });
- }
- ])
- ->get();
-
- return $this->success($plans);
- }
-
- public function save(PlanSave $request)
- {
- $params = $request->validated();
-
- if ($request->input('id')) {
- $plan = Plan::find($request->input('id'));
- if (!$plan) {
- return $this->fail([400202, '该订阅不存在']);
- }
-
- DB::beginTransaction();
- try {
- if ($request->input('force_update')) {
- User::where('plan_id', $plan->id)->update([
- 'group_id' => $params['group_id'],
- 'transfer_enable' => $params['transfer_enable'] * 1073741824,
- 'speed_limit' => $params['speed_limit'],
- 'device_limit' => $params['device_limit'],
- ]);
- }
- $plan->update($params);
- DB::commit();
- return $this->success(true);
- } catch (\Exception $e) {
- DB::rollBack();
- Log::error($e);
- return $this->fail([500, '保存失败']);
- }
- }
- if (!Plan::create($params)) {
- return $this->fail([500, '创建失败']);
- }
- return $this->success(true);
- }
-
- public function drop(Request $request)
- {
- if (Order::where('plan_id', $request->input('id'))->first()) {
- return $this->fail([400201, '该订阅下存在订单无法删除']);
- }
- if (User::where('plan_id', $request->input('id'))->first()) {
- return $this->fail([400201, '该订阅下存在用户无法删除']);
- }
-
- $plan = Plan::find($request->input('id'));
- if (!$plan) {
- return $this->fail([400202, '该订阅不存在']);
- }
-
- return $this->success($plan->delete());
- }
-
- public function update(Request $request)
- {
- $updateData = $request->only([
- 'show',
- 'renew',
- 'sell'
- ]);
-
- $plan = Plan::find($request->input('id'));
- if (!$plan) {
- return $this->fail([400202, '该订阅不存在']);
- }
-
- try {
- $plan->update($updateData);
- } catch (\Exception $e) {
- Log::error($e);
- return $this->fail([500, '保存失败']);
- }
-
- return $this->success(true);
- }
-
- public function sort(Request $request)
- {
- $params = $request->validate([
- 'ids' => 'required|array'
- ]);
-
- try {
- DB::beginTransaction();
- foreach ($params['ids'] as $k => $v) {
- if (!Plan::find($v)->update(['sort' => $k + 1])) {
- throw new \Exception();
- }
- }
- DB::commit();
- } catch (\Exception $e) {
- DB::rollBack();
- Log::error($e);
- return $this->fail([500, '保存失败']);
- }
- return $this->success(true);
- }
-}
diff --git a/Xboard/app/Http/Controllers/V2/Admin/PluginController.php b/Xboard/app/Http/Controllers/V2/Admin/PluginController.php
deleted file mode 100644
index da41477..0000000
--- a/Xboard/app/Http/Controllers/V2/Admin/PluginController.php
+++ /dev/null
@@ -1,333 +0,0 @@
-pluginManager = $pluginManager;
- $this->configService = $configService;
- }
-
- /**
- * 获取所有插件类型
- */
- public function types()
- {
- return response()->json([
- 'data' => [
- [
- 'value' => Plugin::TYPE_FEATURE,
- 'label' => '功能',
- 'description' => '提供功能扩展的插件,如Telegram登录、邮件通知等',
- 'icon' => '🔧'
- ],
- [
- 'value' => Plugin::TYPE_PAYMENT,
- 'label' => '支付方式',
- 'description' => '提供支付接口的插件,如支付宝、微信支付等',
- 'icon' => '💳'
- ]
- ]
- ]);
- }
-
- /**
- * 获取插件列表
- */
- public function index(Request $request)
- {
- $type = $request->query('type');
-
- $installedPlugins = Plugin::when($type, function ($query) use ($type) {
- return $query->byType($type);
- })
- ->get()
- ->keyBy('code')
- ->toArray();
-
- $pluginPath = base_path('plugins');
- $plugins = [];
-
- if (File::exists($pluginPath)) {
- $directories = File::directories($pluginPath);
- foreach ($directories as $directory) {
- $pluginName = basename($directory);
- $configFile = $directory . '/config.json';
- if (File::exists($configFile)) {
- $config = json_decode(File::get($configFile), true);
- $code = $config['code'];
- $pluginType = $config['type'] ?? Plugin::TYPE_FEATURE;
-
- // 如果指定了类型,过滤插件
- if ($type && $pluginType !== $type) {
- continue;
- }
-
- $installed = isset($installedPlugins[$code]);
- $pluginConfig = $installed ? $this->configService->getConfig($code) : ($config['config'] ?? []);
- $readmeFile = collect(['README.md', 'readme.md'])
- ->map(fn($f) => $directory . '/' . $f)
- ->first(fn($path) => File::exists($path));
- $readmeContent = $readmeFile ? File::get($readmeFile) : '';
- $needUpgrade = false;
- if ($installed) {
- $installedVersion = $installedPlugins[$code]['version'] ?? null;
- $localVersion = $config['version'] ?? null;
- if ($installedVersion && $localVersion && version_compare($localVersion, $installedVersion, '>')) {
- $needUpgrade = true;
- }
- }
- $plugins[] = [
- 'code' => $config['code'],
- 'name' => $config['name'],
- 'version' => $config['version'],
- 'description' => $config['description'],
- 'author' => $config['author'],
- 'type' => $pluginType,
- 'is_installed' => $installed,
- 'is_enabled' => $installed ? $installedPlugins[$code]['is_enabled'] : false,
- 'is_protected' => in_array($code, Plugin::PROTECTED_PLUGINS),
- 'can_be_deleted' => !in_array($code, Plugin::PROTECTED_PLUGINS),
- 'config' => $pluginConfig,
- 'readme' => $readmeContent,
- 'need_upgrade' => $needUpgrade,
- ];
- }
- }
- }
-
- return response()->json([
- 'data' => $plugins
- ]);
- }
-
- /**
- * 安装插件
- */
- public function install(Request $request)
- {
- $request->validate([
- 'code' => 'required|string'
- ]);
-
- try {
- $this->pluginManager->install($request->input('code'));
- return response()->json([
- 'message' => '插件安装成功'
- ]);
- } catch (\Exception $e) {
- return response()->json([
- 'message' => '插件安装失败:' . $e->getMessage()
- ], 400);
- }
- }
-
- /**
- * 卸载插件
- */
- public function uninstall(Request $request)
- {
- $request->validate([
- 'code' => 'required|string'
- ]);
-
- $code = $request->input('code');
- $plugin = Plugin::where('code', $code)->first();
- if ($plugin && $plugin->is_enabled) {
- return response()->json([
- 'message' => '请先禁用插件后再卸载'
- ], 400);
- }
-
- try {
- $this->pluginManager->uninstall($code);
- return response()->json([
- 'message' => '插件卸载成功'
- ]);
- } catch (\Exception $e) {
- return response()->json([
- 'message' => '插件卸载失败:' . $e->getMessage()
- ], 400);
- }
- }
-
- /**
- * 升级插件
- */
- public function upgrade(Request $request)
- {
- $request->validate([
- 'code' => 'required|string',
- ]);
- try {
- $this->pluginManager->update($request->input('code'));
- return response()->json([
- 'message' => '插件升级成功'
- ]);
- } catch (\Exception $e) {
- return response()->json([
- 'message' => '插件升级失败:' . $e->getMessage()
- ], 400);
- }
- }
-
- /**
- * 启用插件
- */
- public function enable(Request $request)
- {
- $request->validate([
- 'code' => 'required|string'
- ]);
-
- try {
- $this->pluginManager->enable($request->input('code'));
- return response()->json([
- 'message' => '插件启用成功'
- ]);
- } catch (\Exception $e) {
- return response()->json([
- 'message' => '插件启用失败:' . $e->getMessage()
- ], 400);
- }
- }
-
- /**
- * 禁用插件
- */
- public function disable(Request $request)
- {
- $request->validate([
- 'code' => 'required|string'
- ]);
-
- $this->pluginManager->disable($request->input('code'));
- return response()->json([
- 'message' => '插件禁用成功'
- ]);
-
- }
-
- /**
- * 获取插件配置
- */
- public function getConfig(Request $request)
- {
- $request->validate([
- 'code' => 'required|string'
- ]);
-
- try {
- $config = $this->configService->getConfig($request->input('code'));
- return response()->json([
- 'data' => $config
- ]);
- } catch (\Exception $e) {
- return response()->json([
- 'message' => '获取配置失败:' . $e->getMessage()
- ], 400);
- }
- }
-
- /**
- * 更新插件配置
- */
- public function updateConfig(Request $request)
- {
- $request->validate([
- 'code' => 'required|string',
- 'config' => 'required|array'
- ]);
-
- try {
- $this->configService->updateConfig(
- $request->input('code'),
- $request->input('config')
- );
-
- return response()->json([
- 'message' => '配置更新成功'
- ]);
- } catch (\Exception $e) {
- return response()->json([
- 'message' => '配置更新失败:' . $e->getMessage()
- ], 400);
- }
- }
-
- /**
- * 上传插件
- */
- public function upload(Request $request)
- {
- $request->validate([
- 'file' => [
- 'required',
- 'file',
- 'mimes:zip',
- 'max:10240', // 最大10MB
- ]
- ], [
- 'file.required' => '请选择插件包文件',
- 'file.file' => '无效的文件类型',
- 'file.mimes' => '插件包必须是zip格式',
- 'file.max' => '插件包大小不能超过10MB'
- ]);
-
- try {
- $this->pluginManager->upload($request->file('file'));
- return response()->json([
- 'message' => '插件上传成功'
- ]);
- } catch (\Exception $e) {
- return response()->json([
- 'message' => '插件上传失败:' . $e->getMessage()
- ], 400);
- }
- }
-
- /**
- * 删除插件
- */
- public function delete(Request $request)
- {
- $request->validate([
- 'code' => 'required|string'
- ]);
-
- $code = $request->input('code');
-
- // 检查是否为受保护的插件
- if (in_array($code, Plugin::PROTECTED_PLUGINS)) {
- return response()->json([
- 'message' => '该插件为系统默认插件,不允许删除'
- ], 403);
- }
-
- try {
- $this->pluginManager->delete($code);
- return response()->json([
- 'message' => '插件删除成功'
- ]);
- } catch (\Exception $e) {
- return response()->json([
- 'message' => '插件删除失败:' . $e->getMessage()
- ], 400);
- }
- }
-}
\ No newline at end of file
diff --git a/Xboard/app/Http/Controllers/V2/Admin/Server/GroupController.php b/Xboard/app/Http/Controllers/V2/Admin/Server/GroupController.php
deleted file mode 100644
index 83a53ac..0000000
--- a/Xboard/app/Http/Controllers/V2/Admin/Server/GroupController.php
+++ /dev/null
@@ -1,66 +0,0 @@
-orderByDesc('id')
- ->withCount('users')
- ->get();
-
- // 只在需要时手动加载server_count
- $serverGroups->each(function ($group) {
- $group->setAttribute('server_count', $group->server_count);
- });
-
- return $this->success($serverGroups);
- }
-
- public function save(Request $request)
- {
- if (empty($request->input('name'))) {
- return $this->fail([422, '组名不能为空']);
- }
-
- if ($request->input('id')) {
- $serverGroup = ServerGroup::find($request->input('id'));
- } else {
- $serverGroup = new ServerGroup();
- }
-
- $serverGroup->name = $request->input('name');
- return $this->success($serverGroup->save());
- }
-
- public function drop(Request $request)
- {
- $groupId = $request->input('id');
-
- $serverGroup = ServerGroup::find($groupId);
- if (!$serverGroup) {
- return $this->fail([400202, '组不存在']);
- }
- if (Server::whereJsonContains('group_ids', $groupId)->exists()) {
- return $this->fail([400, '该组已被节点所使用,无法删除']);
- }
-
- if (Plan::where('group_id', $groupId)->exists()) {
- return $this->fail([400, '该组已被订阅所使用,无法删除']);
- }
- if (User::where('group_id', $groupId)->exists()) {
- return $this->fail([400, '该组已被用户所使用,无法删除']);
- }
- return $this->success($serverGroup->delete());
- }
-}
diff --git a/Xboard/app/Http/Controllers/V2/Admin/Server/ManageController.php b/Xboard/app/Http/Controllers/V2/Admin/Server/ManageController.php
deleted file mode 100644
index 41a4bac..0000000
--- a/Xboard/app/Http/Controllers/V2/Admin/Server/ManageController.php
+++ /dev/null
@@ -1,219 +0,0 @@
-map(function ($item) {
- $item['groups'] = ServerGroup::whereIn('id', $item['group_ids'])->get(['name', 'id']);
- $item['parent'] = $item->parent;
- return $item;
- });
- return $this->success($servers);
- }
-
- public function sort(Request $request)
- {
- ini_set('post_max_size', '1m');
- $params = $request->validate([
- '*.id' => 'numeric',
- '*.order' => 'numeric'
- ]);
-
- try {
- DB::beginTransaction();
- collect($params)->each(function ($item) {
- if (isset($item['id']) && isset($item['order'])) {
- Server::where('id', $item['id'])->update(['sort' => $item['order']]);
- }
- });
- DB::commit();
- } catch (\Exception $e) {
- DB::rollBack();
- Log::error($e);
- return $this->fail([500, '保存失败']);
-
- }
- return $this->success(true);
- }
-
- public function save(ServerSave $request)
- {
- $params = $request->validated();
- if ($request->input('id')) {
- $server = Server::find($request->input('id'));
- if (!$server) {
- return $this->fail([400202, '服务器不存在']);
- }
- try {
- $server->update($params);
- return $this->success(true);
- } catch (\Exception $e) {
- Log::error($e);
- return $this->fail([500, '保存失败']);
- }
- }
-
- try {
- Server::create($params);
- return $this->success(true);
- } catch (\Exception $e) {
- Log::error($e);
- return $this->fail([500, '创建失败']);
- }
-
-
- }
-
- public function update(Request $request)
- {
- $request->validate([
- 'id' => 'required|integer',
- 'show' => 'integer',
- ]);
-
- $server = Server::find($request->id);
- if (!$server) {
- return $this->fail([400202, '服务器不存在']);
- }
- $server->show = (int) $request->show;
- if (!$server->save()) {
- return $this->fail([500, '保存失败']);
- }
- return $this->success(true);
- }
-
- /**
- * 删除
- * @param \Illuminate\Http\Request $request
- * @return \Illuminate\Http\JsonResponse
- */
- public function drop(Request $request)
- {
- $request->validate([
- 'id' => 'required|integer',
- ]);
- if (Server::where('id', $request->id)->delete() === false) {
- return $this->fail([500, '删除失败']);
- }
- return $this->success(true);
- }
-
- /**
- * 批量删除节点
- * @param \Illuminate\Http\Request $request
- * @return \Illuminate\Http\JsonResponse
- */
- public function batchDelete(Request $request)
- {
- $request->validate([
- 'ids' => 'required|array',
- 'ids.*' => 'integer',
- ]);
-
- $ids = $request->input('ids');
- if (empty($ids)) {
- return $this->fail([400, '请选择要删除的节点']);
- }
-
- try {
- $deleted = Server::whereIn('id', $ids)->delete();
- if ($deleted === false) {
- return $this->fail([500, '批量删除失败']);
- }
- return $this->success(true);
- } catch (\Exception $e) {
- Log::error($e);
- return $this->fail([500, '批量删除失败']);
- }
- }
-
- /**
- * 重置节点流量
- * @param \Illuminate\Http\Request $request
- * @return \Illuminate\Http\JsonResponse
- */
- public function resetTraffic(Request $request)
- {
- $request->validate([
- 'id' => 'required|integer',
- ]);
-
- $server = Server::find($request->id);
- if (!$server) {
- return $this->fail([400202, '服务器不存在']);
- }
-
- try {
- $server->u = 0;
- $server->d = 0;
- $server->save();
-
- Log::info("Server {$server->id} ({$server->name}) traffic reset by admin");
- return $this->success(true);
- } catch (\Exception $e) {
- Log::error($e);
- return $this->fail([500, '重置失败']);
- }
- }
-
- /**
- * 批量重置节点流量
- * @param \Illuminate\Http\Request $request
- * @return \Illuminate\Http\JsonResponse
- */
- public function batchResetTraffic(Request $request)
- {
- $request->validate([
- 'ids' => 'required|array',
- 'ids.*' => 'integer',
- ]);
-
- $ids = $request->input('ids');
- if (empty($ids)) {
- return $this->fail([400, '请选择要重置的节点']);
- }
-
- try {
- Server::whereIn('id', $ids)->update([
- 'u' => 0,
- 'd' => 0,
- ]);
-
- Log::info("Servers " . implode(',', $ids) . " traffic reset by admin");
- return $this->success(true);
- } catch (\Exception $e) {
- Log::error($e);
- return $this->fail([500, '批量重置失败']);
- }
- }
-
- /**
- * 复制节点
- * @param \Illuminate\Http\Request $request
- * @return \Illuminate\Http\JsonResponse
- */
- public function copy(Request $request)
- {
- $server = Server::find($request->input('id'));
- if (!$server) {
- return $this->fail([400202, '服务器不存在']);
- }
- $server->show = 0;
- $server->code = null;
- Server::create($server->toArray());
- return $this->success(true);
- }
-}
diff --git a/Xboard/app/Http/Controllers/V2/Admin/Server/RouteController.php b/Xboard/app/Http/Controllers/V2/Admin/Server/RouteController.php
deleted file mode 100644
index 7f155ec..0000000
--- a/Xboard/app/Http/Controllers/V2/Admin/Server/RouteController.php
+++ /dev/null
@@ -1,64 +0,0 @@
- $routes
- ];
- }
-
- public function save(Request $request)
- {
- $params = $request->validate([
- 'remarks' => 'required',
- 'match' => 'required|array',
- 'action' => 'required|in:block,direct,dns,proxy',
- 'action_value' => 'nullable'
- ], [
- 'remarks.required' => '备注不能为空',
- 'match.required' => '匹配值不能为空',
- 'action.required' => '动作类型不能为空',
- 'action.in' => '动作类型参数有误'
- ]);
- $params['match'] = array_filter($params['match']);
- // TODO: remove on 1.8.0
- if ($request->input('id')) {
- try {
- $route = ServerRoute::find($request->input('id'));
- $route->update($params);
- return $this->success(true);
- } catch (\Exception $e) {
- Log::error($e);
- return $this->fail([500,'保存失败']);
- }
- }
- try{
- ServerRoute::create($params);
- return $this->success(true);
- }catch(\Exception $e){
- Log::error($e);
- return $this->fail([500,'创建失败']);
- }
- }
-
- public function drop(Request $request)
- {
- $route = ServerRoute::find($request->input('id'));
- if (!$route) throw new ApiException('路由不存在');
- if (!$route->delete()) throw new ApiException('删除失败');
- return [
- 'data' => true
- ];
- }
-}
diff --git a/Xboard/app/Http/Controllers/V2/Admin/StatController.php b/Xboard/app/Http/Controllers/V2/Admin/StatController.php
deleted file mode 100644
index 805fa94..0000000
--- a/Xboard/app/Http/Controllers/V2/Admin/StatController.php
+++ /dev/null
@@ -1,508 +0,0 @@
-service = $service;
- }
- public function getOverride(Request $request)
- {
- // 获取在线节点数
- $onlineNodes = Server::all()->filter(function ($server) {
- return !!$server->is_online;
- })->count();
- // 获取在线设备数和在线用户数
- $onlineDevices = User::where('t', '>=', time() - 600)
- ->sum('online_count');
- $onlineUsers = User::where('t', '>=', time() - 600)
- ->count();
-
- // 获取今日流量统计
- $todayStart = strtotime('today');
- $todayTraffic = StatServer::where('record_at', '>=', $todayStart)
- ->where('record_at', '<', time())
- ->selectRaw('SUM(u) as upload, SUM(d) as download, SUM(u + d) as total')
- ->first();
-
- // 获取本月流量统计
- $monthStart = strtotime(date('Y-m-1'));
- $monthTraffic = StatServer::where('record_at', '>=', $monthStart)
- ->where('record_at', '<', time())
- ->selectRaw('SUM(u) as upload, SUM(d) as download, SUM(u + d) as total')
- ->first();
-
- // 获取总流量统计
- $totalTraffic = StatServer::selectRaw('SUM(u) as upload, SUM(d) as download, SUM(u + d) as total')
- ->first();
-
- return [
- 'data' => [
- 'month_income' => Order::where('created_at', '>=', strtotime(date('Y-m-1')))
- ->where('created_at', '<', time())
- ->whereNotIn('status', [0, 2])
- ->sum('total_amount'),
- 'month_register_total' => User::where('created_at', '>=', strtotime(date('Y-m-1')))
- ->where('created_at', '<', time())
- ->count(),
- 'ticket_pending_total' => Ticket::where('status', 0)
- ->count(),
- 'commission_pending_total' => Order::where('commission_status', 0)
- ->where('invite_user_id', '!=', NULL)
- ->whereNotIn('status', [0, 2])
- ->where('commission_balance', '>', 0)
- ->count(),
- 'day_income' => Order::where('created_at', '>=', strtotime(date('Y-m-d')))
- ->where('created_at', '<', time())
- ->whereNotIn('status', [0, 2])
- ->sum('total_amount'),
- 'last_month_income' => Order::where('created_at', '>=', strtotime('-1 month', strtotime(date('Y-m-1'))))
- ->where('created_at', '<', strtotime(date('Y-m-1')))
- ->whereNotIn('status', [0, 2])
- ->sum('total_amount'),
- 'commission_month_payout' => CommissionLog::where('created_at', '>=', strtotime(date('Y-m-1')))
- ->where('created_at', '<', time())
- ->sum('get_amount'),
- 'commission_last_month_payout' => CommissionLog::where('created_at', '>=', strtotime('-1 month', strtotime(date('Y-m-1'))))
- ->where('created_at', '<', strtotime(date('Y-m-1')))
- ->sum('get_amount'),
- // 新增统计数据
- 'online_nodes' => $onlineNodes,
- 'online_devices' => $onlineDevices,
- 'online_users' => $onlineUsers,
- 'today_traffic' => [
- 'upload' => $todayTraffic->upload ?? 0,
- 'download' => $todayTraffic->download ?? 0,
- 'total' => $todayTraffic->total ?? 0
- ],
- 'month_traffic' => [
- 'upload' => $monthTraffic->upload ?? 0,
- 'download' => $monthTraffic->download ?? 0,
- 'total' => $monthTraffic->total ?? 0
- ],
- 'total_traffic' => [
- 'upload' => $totalTraffic->upload ?? 0,
- 'download' => $totalTraffic->download ?? 0,
- 'total' => $totalTraffic->total ?? 0
- ]
- ]
- ];
- }
-
- /**
- * Get order statistics with filtering and pagination
- *
- * @param Request $request
- * @return array
- */
- public function getOrder(Request $request)
- {
- $request->validate([
- 'start_date' => 'nullable|date_format:Y-m-d',
- 'end_date' => 'nullable|date_format:Y-m-d',
- 'type' => 'nullable|in:paid_total,paid_count,commission_total,commission_count',
- ]);
-
- $query = Stat::where('record_type', 'd');
-
- // Apply date filters
- if ($request->input('start_date')) {
- $query->where('record_at', '>=', strtotime($request->input('start_date')));
- }
- if ($request->input('end_date')) {
- $query->where('record_at', '<=', strtotime($request->input('end_date') . ' 23:59:59'));
- }
-
- $statistics = $query->orderBy('record_at', 'DESC')
- ->get();
-
- $summary = [
- 'paid_total' => 0,
- 'paid_count' => 0,
- 'commission_total' => 0,
- 'commission_count' => 0,
- 'start_date' => $request->input('start_date', date('Y-m-d', $statistics->last()?->record_at)),
- 'end_date' => $request->input('end_date', date('Y-m-d', $statistics->first()?->record_at)),
- 'avg_paid_amount' => 0,
- 'avg_commission_amount' => 0
- ];
-
- $dailyStats = [];
- foreach ($statistics as $statistic) {
- $date = date('Y-m-d', $statistic['record_at']);
-
- // Update summary
- $summary['paid_total'] += $statistic['paid_total'];
- $summary['paid_count'] += $statistic['paid_count'];
- $summary['commission_total'] += $statistic['commission_total'];
- $summary['commission_count'] += $statistic['commission_count'];
-
- // Calculate daily stats
- $dailyData = [
- 'date' => $date,
- 'paid_total' => $statistic['paid_total'],
- 'paid_count' => $statistic['paid_count'],
- 'commission_total' => $statistic['commission_total'],
- 'commission_count' => $statistic['commission_count'],
- 'avg_order_amount' => $statistic['paid_count'] > 0 ? round($statistic['paid_total'] / $statistic['paid_count'], 2) : 0,
- 'avg_commission_amount' => $statistic['commission_count'] > 0 ? round($statistic['commission_total'] / $statistic['commission_count'], 2) : 0
- ];
-
- if ($request->input('type')) {
- $dailyStats[] = [
- 'date' => $date,
- 'value' => $statistic[$request->input('type')],
- 'type' => $this->getTypeLabel($request->input('type'))
- ];
- } else {
- $dailyStats[] = $dailyData;
- }
- }
-
- // Calculate averages for summary
- if ($summary['paid_count'] > 0) {
- $summary['avg_paid_amount'] = round($summary['paid_total'] / $summary['paid_count'], 2);
- }
- if ($summary['commission_count'] > 0) {
- $summary['avg_commission_amount'] = round($summary['commission_total'] / $summary['commission_count'], 2);
- }
-
- // Add percentage calculations to summary
- $summary['commission_rate'] = $summary['paid_total'] > 0
- ? round(($summary['commission_total'] / $summary['paid_total']) * 100, 2)
- : 0;
-
- return [
- 'code' => 0,
- 'message' => 'success',
- 'data' => [
- 'list' => array_reverse($dailyStats),
- 'summary' => $summary,
- ]
- ];
- }
-
- /**
- * Get human readable label for statistic type
- *
- * @param string $type
- * @return string
- */
- private function getTypeLabel(string $type): string
- {
- return match ($type) {
- 'paid_total' => '收款金额',
- 'paid_count' => '收款笔数',
- 'commission_total' => '佣金金额(已发放)',
- 'commission_count' => '佣金笔数(已发放)',
- default => $type
- };
- }
-
- // 获取当日实时流量排行
- public function getServerLastRank()
- {
- $data = $this->service->getServerRank();
- return $this->success(data: $data);
- }
- // 获取昨日节点流量排行
- public function getServerYesterdayRank()
- {
- $data = $this->service->getServerRank('yesterday');
- return $this->success($data);
- }
-
- public function getStatUser(Request $request)
- {
- $request->validate([
- 'user_id' => 'required|integer'
- ]);
-
- $pageSize = $request->input('pageSize', 10);
- $records = StatUser::orderBy('record_at', 'DESC')
- ->where('user_id', $request->input('user_id'))
- ->paginate($pageSize);
-
- $data = $records->items();
- return [
- 'data' => $data,
- 'total' => $records->total(),
- ];
- }
-
- public function getStatRecord(Request $request)
- {
- return [
- 'data' => $this->service->getStatRecord($request->input('type'))
- ];
- }
-
- /**
- * Get comprehensive statistics data including income, users, and growth rates
- */
- public function getStats()
- {
- $currentMonthStart = strtotime(date('Y-m-01'));
- $lastMonthStart = strtotime('-1 month', $currentMonthStart);
- $twoMonthsAgoStart = strtotime('-2 month', $currentMonthStart);
-
- // Today's start timestamp
- $todayStart = strtotime('today');
- $yesterdayStart = strtotime('-1 day', $todayStart);
-
- // 获取在线节点数
- $onlineNodes = Server::all()->filter(function ($server) {
- return !!$server->is_online;
- })->count();
-
- // 获取在线设备数和在线用户数
- $onlineDevices = User::where('t', '>=', time() - 600)
- ->sum('online_count');
- $onlineUsers = User::where('t', '>=', time() - 600)
- ->count();
-
- // 获取今日流量统计
- $todayTraffic = StatServer::where('record_at', '>=', $todayStart)
- ->where('record_at', '<', time())
- ->selectRaw('SUM(u) as upload, SUM(d) as download, SUM(u + d) as total')
- ->first();
-
- // 获取本月流量统计
- $monthTraffic = StatServer::where('record_at', '>=', $currentMonthStart)
- ->where('record_at', '<', time())
- ->selectRaw('SUM(u) as upload, SUM(d) as download, SUM(u + d) as total')
- ->first();
-
- // 获取总流量统计
- $totalTraffic = StatServer::selectRaw('SUM(u) as upload, SUM(d) as download, SUM(u + d) as total')
- ->first();
-
- // Today's income
- $todayIncome = Order::where('created_at', '>=', $todayStart)
- ->where('created_at', '<', time())
- ->whereNotIn('status', [0, 2])
- ->sum('total_amount');
-
- // Yesterday's income for day growth calculation
- $yesterdayIncome = Order::where('created_at', '>=', $yesterdayStart)
- ->where('created_at', '<', $todayStart)
- ->whereNotIn('status', [0, 2])
- ->sum('total_amount');
-
- // Current month income
- $currentMonthIncome = Order::where('created_at', '>=', $currentMonthStart)
- ->where('created_at', '<', time())
- ->whereNotIn('status', [0, 2])
- ->sum('total_amount');
-
- // Last month income
- $lastMonthIncome = Order::where('created_at', '>=', $lastMonthStart)
- ->where('created_at', '<', $currentMonthStart)
- ->whereNotIn('status', [0, 2])
- ->sum('total_amount');
-
- // Last month commission payout
- $lastMonthCommissionPayout = CommissionLog::where('created_at', '>=', $lastMonthStart)
- ->where('created_at', '<', $currentMonthStart)
- ->sum('get_amount');
-
- // Current month commission payout
- $currentMonthCommissionPayout = CommissionLog::where('created_at', '>=', $currentMonthStart)
- ->where('created_at', '<', time())
- ->sum('get_amount');
-
- // Current month new users
- $currentMonthNewUsers = User::where('created_at', '>=', $currentMonthStart)
- ->where('created_at', '<', time())
- ->count();
-
- // Total users
- $totalUsers = User::count();
-
- // Active users (users with valid subscription)
- $activeUsers = User::where(function ($query) {
- $query->where('expired_at', '>=', time())
- ->orWhere('expired_at', NULL);
- })->count();
-
- // Previous month income for growth calculation
- $twoMonthsAgoIncome = Order::where('created_at', '>=', $twoMonthsAgoStart)
- ->where('created_at', '<', $lastMonthStart)
- ->whereNotIn('status', [0, 2])
- ->sum('total_amount');
-
- // Previous month commission for growth calculation
- $twoMonthsAgoCommission = CommissionLog::where('created_at', '>=', $twoMonthsAgoStart)
- ->where('created_at', '<', $lastMonthStart)
- ->sum('get_amount');
-
- // Previous month users for growth calculation
- $lastMonthNewUsers = User::where('created_at', '>=', $lastMonthStart)
- ->where('created_at', '<', $currentMonthStart)
- ->count();
-
- // Calculate growth rates
- $monthIncomeGrowth = $lastMonthIncome > 0 ? round(($currentMonthIncome - $lastMonthIncome) / $lastMonthIncome * 100, 1) : 0;
- $lastMonthIncomeGrowth = $twoMonthsAgoIncome > 0 ? round(($lastMonthIncome - $twoMonthsAgoIncome) / $twoMonthsAgoIncome * 100, 1) : 0;
- $commissionGrowth = $twoMonthsAgoCommission > 0 ? round(($lastMonthCommissionPayout - $twoMonthsAgoCommission) / $twoMonthsAgoCommission * 100, 1) : 0;
- $userGrowth = $lastMonthNewUsers > 0 ? round(($currentMonthNewUsers - $lastMonthNewUsers) / $lastMonthNewUsers * 100, 1) : 0;
- $dayIncomeGrowth = $yesterdayIncome > 0 ? round(($todayIncome - $yesterdayIncome) / $yesterdayIncome * 100, 1) : 0;
-
- // 获取待处理工单和佣金数据
- $ticketPendingTotal = Ticket::where('status', 0)->count();
- $commissionPendingTotal = Order::where('commission_status', 0)
- ->where('invite_user_id', '!=', NULL)
- ->whereIn('status', [Order::STATUS_COMPLETED])
- ->where('commission_balance', '>', 0)
- ->count();
-
- return [
- 'data' => [
- // 收入相关
- 'todayIncome' => $todayIncome,
- 'dayIncomeGrowth' => $dayIncomeGrowth,
- 'currentMonthIncome' => $currentMonthIncome,
- 'lastMonthIncome' => $lastMonthIncome,
- 'monthIncomeGrowth' => $monthIncomeGrowth,
- 'lastMonthIncomeGrowth' => $lastMonthIncomeGrowth,
-
- // 佣金相关
- 'currentMonthCommissionPayout' => $currentMonthCommissionPayout,
- 'lastMonthCommissionPayout' => $lastMonthCommissionPayout,
- 'commissionGrowth' => $commissionGrowth,
- 'commissionPendingTotal' => $commissionPendingTotal,
-
- // 用户相关
- 'currentMonthNewUsers' => $currentMonthNewUsers,
- 'totalUsers' => $totalUsers,
- 'activeUsers' => $activeUsers,
- 'userGrowth' => $userGrowth,
- 'onlineUsers' => $onlineUsers,
- 'onlineDevices' => $onlineDevices,
-
- // 工单相关
- 'ticketPendingTotal' => $ticketPendingTotal,
-
- // 节点相关
- 'onlineNodes' => $onlineNodes,
-
- // 流量统计
- 'todayTraffic' => [
- 'upload' => $todayTraffic->upload ?? 0,
- 'download' => $todayTraffic->download ?? 0,
- 'total' => $todayTraffic->total ?? 0
- ],
- 'monthTraffic' => [
- 'upload' => $monthTraffic->upload ?? 0,
- 'download' => $monthTraffic->download ?? 0,
- 'total' => $monthTraffic->total ?? 0
- ],
- 'totalTraffic' => [
- 'upload' => $totalTraffic->upload ?? 0,
- 'download' => $totalTraffic->download ?? 0,
- 'total' => $totalTraffic->total ?? 0
- ]
- ]
- ];
- }
-
- /**
- * Get traffic ranking data for nodes or users
- *
- * @param Request $request
- * @return array
- */
- public function getTrafficRank(Request $request)
- {
- $request->validate([
- 'type' => 'required|in:node,user',
- 'start_time' => 'nullable|integer|min:1000000000|max:9999999999',
- 'end_time' => 'nullable|integer|min:1000000000|max:9999999999'
- ]);
-
- $type = $request->input('type');
- $startDate = $request->input('start_time', strtotime('-7 days'));
- $endDate = $request->input('end_time', time());
- $previousStartDate = $startDate - ($endDate - $startDate);
- $previousEndDate = $startDate;
-
- if ($type === 'node') {
- // Get node traffic data
- $currentData = StatServer::selectRaw('server_id as id, SUM(u + d) as value')
- ->where('record_at', '>=', $startDate)
- ->where('record_at', '<=', $endDate)
- ->groupBy('server_id')
- ->orderBy('value', 'DESC')
- ->limit(10)
- ->get();
-
- // Get previous period data for comparison
- $previousData = StatServer::selectRaw('server_id as id, SUM(u + d) as value')
- ->where('record_at', '>=', $previousStartDate)
- ->where('record_at', '<', $previousEndDate)
- ->whereIn('server_id', $currentData->pluck('id'))
- ->groupBy('server_id')
- ->get()
- ->keyBy('id');
-
- } else {
- // Get user traffic data
- $currentData = StatUser::selectRaw('user_id as id, SUM(u + d) as value')
- ->where('record_at', '>=', $startDate)
- ->where('record_at', '<=', $endDate)
- ->groupBy('user_id')
- ->orderBy('value', 'DESC')
- ->limit(10)
- ->get();
-
- // Get previous period data for comparison
- $previousData = StatUser::selectRaw('user_id as id, SUM(u + d) as value')
- ->where('record_at', '>=', $previousStartDate)
- ->where('record_at', '<', $previousEndDate)
- ->whereIn('user_id', $currentData->pluck('id'))
- ->groupBy('user_id')
- ->get()
- ->keyBy('id');
- }
-
- $result = [];
- $ids = $currentData->pluck('id');
- $names = $type === 'node'
- ? Server::whereIn('id', $ids)->pluck('name', 'id')
- : User::whereIn('id', $ids)->pluck('email', 'id');
-
- foreach ($currentData as $data) {
- $previousValue = isset($previousData[$data->id]) ? $previousData[$data->id]->value : 0;
- $change = $previousValue > 0 ? round(($data->value - $previousValue) / $previousValue * 100, 1) : 0;
-
- $result[] = [
- 'id' => (string) $data->id,
- 'name' => $names[$data->id] ?? ($type === 'node' ? "Node {$data->id}" : "User {$data->id}"),
- 'value' => $data->value,
- 'previousValue' => $previousValue,
- 'change' => $change,
- 'timestamp' => date('c', $endDate)
- ];
- }
-
- return [
- 'timestamp' => date('c'),
- 'data' => $result
- ];
- }
-}
diff --git a/Xboard/app/Http/Controllers/V2/Admin/SystemController.php b/Xboard/app/Http/Controllers/V2/Admin/SystemController.php
deleted file mode 100644
index 2ba35b5..0000000
--- a/Xboard/app/Http/Controllers/V2/Admin/SystemController.php
+++ /dev/null
@@ -1,144 +0,0 @@
- $this->getScheduleStatus(),
- 'horizon' => $this->getHorizonStatus(),
- 'schedule_last_runtime' => Cache::get(CacheKey::get('SCHEDULE_LAST_CHECK_AT', null)),
- ];
- return $this->success($data);
- }
-
- public function getQueueWorkload(WorkloadRepository $workload)
- {
- return $this->success(collect($workload->get())->sortBy('name')->values()->toArray());
- }
-
- protected function getScheduleStatus(): bool
- {
- return (time() - 120) < Cache::get(CacheKey::get('SCHEDULE_LAST_CHECK_AT', null));
- }
-
- protected function getHorizonStatus(): bool
- {
- if (!$masters = app(MasterSupervisorRepository::class)->all()) {
- return false;
- }
-
- return collect($masters)->contains(function ($master) {
- return $master->status === 'paused';
- }) ? false : true;
- }
-
- public function getQueueStats()
- {
- $data = [
- 'failedJobs' => app(JobRepository::class)->countRecentlyFailed(),
- 'jobsPerMinute' => app(MetricsRepository::class)->jobsProcessedPerMinute(),
- 'pausedMasters' => $this->totalPausedMasters(),
- 'periods' => [
- 'failedJobs' => config('horizon.trim.recent_failed', config('horizon.trim.failed')),
- 'recentJobs' => config('horizon.trim.recent'),
- ],
- 'processes' => $this->totalProcessCount(),
- 'queueWithMaxRuntime' => app(MetricsRepository::class)->queueWithMaximumRuntime(),
- 'queueWithMaxThroughput' => app(MetricsRepository::class)->queueWithMaximumThroughput(),
- 'recentJobs' => app(JobRepository::class)->countRecent(),
- 'status' => $this->getHorizonStatus(),
- 'wait' => collect(app(WaitTimeCalculator::class)->calculate())->take(1),
- ];
- return $this->success($data);
- }
-
- /**
- * Get the total process count across all supervisors.
- *
- * @return int
- */
- protected function totalProcessCount()
- {
- $supervisors = app(SupervisorRepository::class)->all();
-
- return collect($supervisors)->reduce(function ($carry, $supervisor) {
- return $carry + collect($supervisor->processes)->sum();
- }, 0);
- }
-
- /**
- * Get the number of master supervisors that are currently paused.
- *
- * @return int
- */
- protected function totalPausedMasters()
- {
- if (!$masters = app(MasterSupervisorRepository::class)->all()) {
- return 0;
- }
-
- return collect($masters)->filter(function ($master) {
- return $master->status === 'paused';
- })->count();
- }
-
- public function getAuditLog(Request $request)
- {
- $current = max(1, (int) $request->input('current', 1));
- $pageSize = max(10, (int) $request->input('page_size', 10));
-
- $builder = AdminAuditLog::with('admin:id,email')
- ->orderBy('id', 'DESC')
- ->when($request->input('action'), fn($q, $v) => $q->where('action', $v))
- ->when($request->input('admin_id'), fn($q, $v) => $q->where('admin_id', $v))
- ->when($request->input('keyword'), function ($q, $keyword) {
- $q->where(function ($q) use ($keyword) {
- $q->where('uri', 'like', '%' . $keyword . '%')
- ->orWhere('request_data', 'like', '%' . $keyword . '%');
- });
- });
-
- $total = $builder->count();
- $res = $builder->forPage($current, $pageSize)->get();
-
- return response(['data' => $res, 'total' => $total]);
- }
-
- public function getHorizonFailedJobs(Request $request, JobRepository $jobRepository)
- {
- $current = max(1, (int) $request->input('current', 1));
- $pageSize = max(10, (int) $request->input('page_size', 20));
- $offset = ($current - 1) * $pageSize;
-
- $failedJobs = collect($jobRepository->getFailed())
- ->sortByDesc('failed_at')
- ->slice($offset, $pageSize)
- ->values();
-
- $total = $jobRepository->countFailed();
-
- return response()->json([
- 'data' => $failedJobs,
- 'total' => $total,
- 'current' => $current,
- 'page_size' => $pageSize,
- ]);
- }
-
-}
diff --git a/Xboard/app/Http/Controllers/V2/Admin/ThemeController.php b/Xboard/app/Http/Controllers/V2/Admin/ThemeController.php
deleted file mode 100644
index 727cd27..0000000
--- a/Xboard/app/Http/Controllers/V2/Admin/ThemeController.php
+++ /dev/null
@@ -1,150 +0,0 @@
-themeService = $themeService;
- }
-
- /**
- * 上传新主题
- *
- * @throws ApiException
- */
- public function upload(Request $request)
- {
- $request->validate([
- 'file' => [
- 'required',
- 'file',
- 'mimes:zip',
- 'max:10240', // 最大10MB
- ]
- ], [
- 'file.required' => '请选择主题包文件',
- 'file.file' => '无效的文件类型',
- 'file.mimes' => '主题包必须是zip格式',
- 'file.max' => '主题包大小不能超过10MB'
- ]);
-
- try {
- // 检查上传目录权限
- $uploadPath = storage_path('tmp');
- if (!File::exists($uploadPath)) {
- File::makeDirectory($uploadPath, 0755, true);
- }
-
- if (!is_writable($uploadPath)) {
- throw new ApiException('上传目录无写入权限');
- }
-
- // 检查主题目录权限
- $themePath = base_path('theme');
- if (!is_writable($themePath)) {
- throw new ApiException('主题目录无写入权限');
- }
-
- $file = $request->file('file');
-
- // 检查文件MIME类型
- $mimeType = $file->getMimeType();
- if (!in_array($mimeType, ['application/zip', 'application/x-zip-compressed'])) {
- throw new ApiException('无效的文件类型,仅支持ZIP格式');
- }
-
- // 检查文件名安全性
- $originalName = $file->getClientOriginalName();
- if (!preg_match('/^[a-zA-Z0-9\-\_\.]+\.zip$/', $originalName)) {
- throw new ApiException('主题包文件名只能包含字母、数字、下划线、中划线和点');
- }
-
- $this->themeService->upload($file);
- return $this->success(true);
-
- } catch (ApiException $e) {
- throw $e;
- } catch (\Exception $e) {
- Log::error('Theme upload failed', [
- 'error' => $e->getMessage(),
- 'file' => $request->file('file')?->getClientOriginalName()
- ]);
- throw new ApiException('主题上传失败:' . $e->getMessage());
- }
- }
-
- /**
- * 删除主题
- */
- public function delete(Request $request)
- {
- $payload = $request->validate([
- 'name' => 'required'
- ]);
- $this->themeService->delete($payload['name']);
- return $this->success(true);
- }
-
- /**
- * 获取所有主题和其配置列
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function getThemes()
- {
- $data = [
- 'themes' => $this->themeService->getList(),
- 'active' => admin_setting('frontend_theme', 'Xboard')
- ];
- return $this->success($data);
- }
-
- /**
- * 切换主题
- */
- public function switchTheme(Request $request)
- {
- $payload = $request->validate([
- 'name' => 'required'
- ]);
- $this->themeService->switch($payload['name']);
- return $this->success(true);
- }
-
- /**
- * 获取主题配置
- */
- public function getThemeConfig(Request $request)
- {
- $payload = $request->validate([
- 'name' => 'required'
- ]);
- $data = $this->themeService->getConfig($payload['name']);
- return $this->success($data);
- }
-
- /**
- * 保存主题配置
- */
- public function saveThemeConfig(Request $request)
- {
- $payload = $request->validate([
- 'name' => 'required',
- 'config' => 'required'
- ]);
- $this->themeService->updateConfig($payload['name'], $payload['config']);
- $config = $this->themeService->getConfig($payload['name']);
- return $this->success($config);
- }
-}
diff --git a/Xboard/app/Http/Controllers/V2/Admin/TicketController.php b/Xboard/app/Http/Controllers/V2/Admin/TicketController.php
deleted file mode 100644
index ca6d8c4..0000000
--- a/Xboard/app/Http/Controllers/V2/Admin/TicketController.php
+++ /dev/null
@@ -1,156 +0,0 @@
-has('filter')) {
- collect($request->input('filter'))->each(function ($filter) use ($builder) {
- $key = $filter['id'];
- $value = $filter['value'];
- $builder->where(function ($query) use ($key, $value) {
- if (is_array($value)) {
- $query->whereIn($key, $value);
- } else {
- $query->where($key, 'like', "%{$value}%");
- }
- });
- });
- }
-
- if ($request->has('sort')) {
- collect($request->input('sort'))->each(function ($sort) use ($builder) {
- $key = $sort['id'];
- $value = $sort['desc'] ? 'DESC' : 'ASC';
- $builder->orderBy($key, $value);
- });
- }
- }
- public function fetch(Request $request)
- {
- if ($request->input('id')) {
- return $this->fetchTicketById($request);
- } else {
- return $this->fetchTickets($request);
- }
- }
-
- /**
- * Summary of fetchTicketById
- * @param \Illuminate\Http\Request $request
- * @return \Illuminate\Http\JsonResponse
- */
- private function fetchTicketById(Request $request)
- {
- $ticket = Ticket::with('messages', 'user')->find($request->input('id'));
-
- if (!$ticket) {
- return $this->fail([400202, '工单不存在']);
- }
- $result = $ticket->toArray();
- $result['user'] = UserController::transformUserData($ticket->user);
-
- return $this->success($result);
- }
-
- /**
- * Summary of fetchTickets
- * @param \Illuminate\Http\Request $request
- * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response
- */
- private function fetchTickets(Request $request)
- {
- $ticketModel = Ticket::with('user')
- ->when($request->has('status'), function ($query) use ($request) {
- $query->where('status', $request->input('status'));
- })
- ->when($request->has('reply_status'), function ($query) use ($request) {
- $query->whereIn('reply_status', $request->input('reply_status'));
- })
- ->when($request->has('email'), function ($query) use ($request) {
- $query->whereHas('user', function ($q) use ($request) {
- $q->where('email', $request->input('email'));
- });
- });
-
- $this->applyFiltersAndSorts($request, $ticketModel);
- $tickets = $ticketModel
- ->latest('updated_at')
- ->paginate(
- perPage: $request->integer('pageSize', 10),
- page: $request->integer('current', 1)
- );
-
- // 获取items然后映射转换
- $items = collect($tickets->items())->map(function ($ticket) {
- $ticketData = $ticket->toArray();
- $ticketData['user'] = UserController::transformUserData($ticket->user);
- return $ticketData;
- })->all();
-
- return response([
- 'data' => $items,
- 'total' => $tickets->total()
- ]);
- }
-
- public function reply(Request $request)
- {
- $request->validate([
- 'id' => 'required|numeric',
- 'message' => 'required|string'
- ], [
- 'id.required' => '工单ID不能为空',
- 'message.required' => '消息不能为空'
- ]);
- $ticketService = new TicketService();
- $ticketService->replyByAdmin(
- $request->input('id'),
- $request->input('message'),
- $request->user()->id
- );
- return $this->success(true);
- }
-
- public function close(Request $request)
- {
- $request->validate([
- 'id' => 'required|numeric'
- ], [
- 'id.required' => '工单ID不能为空'
- ]);
- try {
- $ticket = Ticket::findOrFail($request->input('id'));
- $ticket->status = Ticket::STATUS_CLOSED;
- $ticket->save();
- return $this->success(true);
- } catch (ModelNotFoundException $e) {
- return $this->fail([400202, '工单不存在']);
- } catch (\Exception $e) {
- return $this->fail([500101, '关闭失败']);
- }
- }
-
- public function show($ticketId)
- {
- $ticket = Ticket::with([
- 'user',
- 'messages' => function ($query) {
- $query->with(['user']); // 如果需要用户信息
- }
- ])->findOrFail($ticketId);
-
- // 自动包含 is_me 属性
- return response()->json([
- 'data' => $ticket
- ]);
- }
-}
diff --git a/Xboard/app/Http/Controllers/V2/Admin/TrafficResetController.php b/Xboard/app/Http/Controllers/V2/Admin/TrafficResetController.php
deleted file mode 100644
index 53e4f54..0000000
--- a/Xboard/app/Http/Controllers/V2/Admin/TrafficResetController.php
+++ /dev/null
@@ -1,235 +0,0 @@
-trafficResetService = $trafficResetService;
- }
-
- /**
- * 获取流量重置日志列表
- */
- public function logs(Request $request): JsonResponse
- {
- $request->validate([
- 'user_id' => 'nullable|integer',
- 'user_email' => 'nullable|string',
- 'reset_type' => 'nullable|string|in:' . implode(',', array_keys(TrafficResetLog::getResetTypeNames())),
- 'trigger_source' => 'nullable|string|in:' . implode(',', array_keys(TrafficResetLog::getSourceNames())),
- 'start_date' => 'nullable|date',
- 'end_date' => 'nullable|date|after_or_equal:start_date',
- 'per_page' => 'nullable|integer|min:1|max:10000',
- 'page' => 'nullable|integer|min:1',
- ]);
-
- $query = TrafficResetLog::with(['user:id,email'])
- ->orderBy('reset_time', 'desc');
-
- // 筛选条件
- if ($request->filled('user_id')) {
- $query->where('user_id', $request->user_id);
- }
-
- if ($request->filled('user_email')) {
- $query->whereHas('user', function ($query) use ($request) {
- $query->where('email', 'like', '%' . $request->user_email . '%');
- });
- }
-
- if ($request->filled('reset_type')) {
- $query->where('reset_type', $request->reset_type);
- }
-
- if ($request->filled('trigger_source')) {
- $query->where('trigger_source', $request->trigger_source);
- }
-
- if ($request->filled('start_date')) {
- $query->where('reset_time', '>=', $request->start_date);
- }
-
- if ($request->filled('end_date')) {
- $query->where('reset_time', '<=', $request->end_date . ' 23:59:59');
- }
-
- $perPage = $request->get('per_page', 20);
- $logs = $query->paginate($perPage);
-
- // 格式化数据
- $formattedLogs = $logs->getCollection()->map(function (TrafficResetLog $log) {
- return [
- 'id' => $log->id,
- 'user_id' => $log->user_id,
- 'user_email' => $log->user->email ?? 'N/A',
- 'reset_type' => $log->reset_type,
- 'reset_type_name' => $log->getResetTypeName(),
- 'reset_time' => $log->reset_time,
- 'old_traffic' => [
- 'upload' => $log->old_upload,
- 'download' => $log->old_download,
- 'total' => $log->old_total,
- 'formatted' => $log->formatTraffic($log->old_total),
- ],
- 'new_traffic' => [
- 'upload' => $log->new_upload,
- 'download' => $log->new_download,
- 'total' => $log->new_total,
- 'formatted' => $log->formatTraffic($log->new_total),
- ],
- 'trigger_source' => $log->trigger_source,
- 'trigger_source_name' => $log->getSourceName(),
- 'metadata' => $log->metadata,
- 'created_at' => $log->created_at,
- ];
- });
-
- return response()->json([
- 'data' => $formattedLogs->toArray(),
- 'pagination' => [
- 'current_page' => $logs->currentPage(),
- 'last_page' => $logs->lastPage(),
- 'per_page' => $logs->perPage(),
- 'total' => $logs->total(),
- ],
- ]);
- }
-
- /**
- * 获取流量重置统计信息
- */
- public function stats(Request $request): JsonResponse
- {
- $request->validate([
- 'days' => 'nullable|integer|min:1|max:365',
- ]);
-
- $days = $request->get('days', 30);
- $startDate = now()->subDays($days)->startOfDay();
-
- $stats = [
- 'total_resets' => TrafficResetLog::where('reset_time', '>=', $startDate)->count(),
- 'auto_resets' => TrafficResetLog::where('reset_time', '>=', $startDate)
- ->where('trigger_source', TrafficResetLog::SOURCE_AUTO)
- ->count(),
- 'manual_resets' => TrafficResetLog::where('reset_time', '>=', $startDate)
- ->where('trigger_source', TrafficResetLog::SOURCE_MANUAL)
- ->count(),
- 'cron_resets' => TrafficResetLog::where('reset_time', '>=', $startDate)
- ->where('trigger_source', TrafficResetLog::SOURCE_CRON)
- ->count(),
- ];
-
- return response()->json([
- 'data' => $stats
- ]);
- }
-
- /**
- * 手动重置用户流量
- */
- public function resetUser(Request $request): JsonResponse
- {
- $request->validate([
- 'user_id' => 'required|integer|exists:v2_user,id',
- 'reason' => 'nullable|string|max:255',
- ]);
-
- $user = User::find($request->user_id);
-
- if (!$this->trafficResetService->canReset($user)) {
- return response()->json([
- 'message' => __('traffic_reset.user_cannot_reset')
- ], 400);
- }
-
- $metadata = [];
- if ($request->filled('reason')) {
- $metadata['reason'] = $request->reason;
- $metadata['admin_id'] = auth()->user()?->id;
- }
-
- $success = $this->trafficResetService->manualReset($user, $metadata);
-
- if (!$success) {
- return response()->json([
- 'message' => __('traffic_reset.reset_failed')
- ], 500);
- }
-
- return response()->json([
- 'message' => __('traffic_reset.reset_success'),
- 'data' => [
- 'user_id' => $user->id,
- 'email' => $user->email,
- 'reset_time' => now(),
- 'next_reset_at' => $user->fresh()->next_reset_at,
- ]
- ]);
- }
-
-
-
- /**
- * 获取用户重置历史
- */
- public function userHistory(Request $request, int $userId): JsonResponse
- {
- $request->validate([
- 'limit' => 'nullable|integer|min:1|max:50',
- ]);
-
- $user = User::findOrFail($userId);
- $limit = $request->get('limit', 10);
-
- $history = $this->trafficResetService->getUserResetHistory($user, $limit);
-
- /** @var \Illuminate\Database\Eloquent\Collection $history */
- $data = $history->map(function (TrafficResetLog $log) {
- return [
- 'id' => $log->id,
- 'reset_type' => $log->reset_type,
- 'reset_type_name' => $log->getResetTypeName(),
- 'reset_time' => $log->reset_time,
- 'old_traffic' => [
- 'upload' => $log->old_upload,
- 'download' => $log->old_download,
- 'total' => $log->old_total,
- 'formatted' => $log->formatTraffic($log->old_total),
- ],
- 'trigger_source' => $log->trigger_source,
- 'trigger_source_name' => $log->getSourceName(),
- 'metadata' => $log->metadata,
- ];
- });
-
- return response()->json([
- "data" => [
- 'user' => [
- 'id' => $user->id,
- 'email' => $user->email,
- 'reset_count' => $user->reset_count,
- 'last_reset_at' => $user->last_reset_at,
- 'next_reset_at' => $user->next_reset_at,
- ],
- 'history' => $data,
- ]
- ]);
- }
-
-
-}
\ No newline at end of file
diff --git a/Xboard/app/Http/Controllers/V2/Admin/UpdateController.php b/Xboard/app/Http/Controllers/V2/Admin/UpdateController.php
deleted file mode 100644
index d846466..0000000
--- a/Xboard/app/Http/Controllers/V2/Admin/UpdateController.php
+++ /dev/null
@@ -1,28 +0,0 @@
-updateService = $updateService;
- }
-
- public function checkUpdate()
- {
- return $this->success($this->updateService->checkForUpdates());
- }
-
- public function executeUpdate()
- {
- $result = $this->updateService->executeUpdate();
- return $result['success'] ? $this->success($result) : $this->fail([500, $result['message']]);
- }
-}
\ No newline at end of file
diff --git a/Xboard/app/Http/Controllers/V2/Admin/UserController.php b/Xboard/app/Http/Controllers/V2/Admin/UserController.php
deleted file mode 100644
index b135270..0000000
--- a/Xboard/app/Http/Controllers/V2/Admin/UserController.php
+++ /dev/null
@@ -1,682 +0,0 @@
-input('id'));
- if (!$user)
- return $this->fail([400202, '用户不存在']);
- $user->token = Helper::guid();
- $user->uuid = Helper::guid(true);
- return $this->success($user->save());
- }
-
- // Apply filters and sorts to the query builder.
- private function applyFiltersAndSorts(Request $request, Builder|QueryBuilder $builder): void
- {
- $this->applyFilters($request, $builder);
- $this->applySorting($request, $builder);
- }
-
- // Apply filters to the query builder.
- private function applyFilters(Request $request, Builder|QueryBuilder $builder): void
- {
- if (!$request->has('filter')) {
- return;
- }
-
- collect($request->input('filter'))->each(function ($filter) use ($builder) {
- $field = $filter['id'];
- $value = $filter['value'];
- $logic = strtolower($filter['logic'] ?? 'and');
-
- if ($logic === 'or') {
- $builder->orWhere(function ($query) use ($field, $value) {
- $this->buildFilterQuery($query, $field, $value);
- });
- } else {
- $builder->where(function ($query) use ($field, $value) {
- $this->buildFilterQuery($query, $field, $value);
- });
- }
- });
- }
-
- // Build one filter query condition.
- private function buildFilterQuery(Builder|QueryBuilder $query, string $field, mixed $value): void
- {
- // 处理关联查询
- if (str_contains($field, '.')) {
- if (!method_exists($query, 'whereHas')) {
- return;
- }
- [$relation, $relationField] = explode('.', $field);
- $query->whereHas($relation, function ($q) use ($relationField, $value) {
- if (is_array($value)) {
- $q->whereIn($relationField, $value);
- } else if (is_string($value) && str_contains($value, ':')) {
- [$operator, $filterValue] = explode(':', $value, 2);
- $this->applyQueryCondition($q, $relationField, $operator, $filterValue);
- } else {
- $q->where($relationField, 'like', "%{$value}%");
- }
- });
- return;
- }
-
- // 处理数组值的 'in' 操作
- if (is_array($value)) {
- $query->whereIn($field === 'group_ids' ? 'group_id' : $field, $value);
- return;
- }
-
- // 处理基于运算符的过滤
- if (!is_string($value) || !str_contains($value, ':')) {
- $query->where($field, 'like', "%{$value}%");
- return;
- }
-
- [$operator, $filterValue] = explode(':', $value, 2);
-
- // 转换数字字符串为适当的类型
- if (is_numeric($filterValue)) {
- $filterValue = strpos($filterValue, '.') !== false
- ? (float) $filterValue
- : (int) $filterValue;
- }
-
- // 处理计算字段
- $queryField = match ($field) {
- 'total_used' => DB::raw('(u + d)'),
- default => $field
- };
-
- $this->applyQueryCondition($query, $queryField, $operator, $filterValue);
- }
-
- // Apply sorting rules to the query builder.
- private function applySorting(Request $request, Builder|QueryBuilder $builder): void
- {
- if (!$request->has('sort')) {
- return;
- }
-
- collect($request->input('sort'))->each(function ($sort) use ($builder) {
- $field = $sort['id'];
- $direction = $sort['desc'] ? 'DESC' : 'ASC';
- $builder->orderBy($field, $direction);
- });
- }
-
- // Resolve bulk operation scope and normalize user_ids.
- private function resolveScope(Request $request): array
- {
- $scope = $request->input('scope');
- $userIds = $request->input('user_ids');
-
- $hasSelection = is_array($userIds) && count(array_filter($userIds, static fn($v) => is_numeric($v))) > 0;
- $hasFilter = $request->has('filter') && !empty($request->input('filter'));
-
- if (!in_array($scope, ['selected', 'filtered', 'all'], true)) {
- if ($hasSelection) {
- $scope = 'selected';
- } elseif ($hasFilter) {
- $scope = 'filtered';
- } else {
- $scope = 'all';
- }
- }
-
- $normalizedIds = [];
- if ($scope === 'selected') {
- $normalizedIds = is_array($userIds) ? $userIds : [];
- $normalizedIds = array_values(array_unique(array_map(static function ($v) {
- return is_numeric($v) ? (int) $v : null;
- }, $normalizedIds)));
- $normalizedIds = array_values(array_filter($normalizedIds, static fn($v) => is_int($v)));
- }
-
- return [
- 'scope' => $scope,
- 'user_ids' => $normalizedIds,
- ];
- }
-
- // Fetch paginated user list (filters + sorting).
- public function fetch(Request $request)
- {
- $current = $request->input('current', 1);
- $pageSize = $request->input('pageSize', 10);
-
- $userModel = User::query()
- ->with(['plan:id,name', 'invite_user:id,email', 'group:id,name'])
- ->select((new User())->getTable() . '.*')
- ->selectRaw('(u + d) as total_used');
-
- $this->applyFiltersAndSorts($request, $userModel);
-
- $users = $userModel->orderBy('id', 'desc')
- ->paginate($pageSize, ['*'], 'page', $current);
-
- $users->getCollection()->transform(function ($user): array {
- return self::transformUserData($user);
- });
-
- return $this->paginate($users);
- }
-
- // Transform user fields for API response.
- public static function transformUserData(User $user): array
- {
- $user = $user->toArray();
- $user['balance'] = $user['balance'] / 100;
- $user['commission_balance'] = $user['commission_balance'] / 100;
- $user['subscribe_url'] = Helper::getSubscribeUrl($user['token']);
- return $user;
- }
-
- public function getUserInfoById(Request $request)
- {
- $request->validate([
- 'id' => 'required|numeric'
- ], [
- 'id.required' => '用户ID不能为空'
- ]);
- $user = User::find($request->input('id'))->load('invite_user');
- return $this->success($user);
- }
-
- public function update(UserUpdate $request)
- {
- $params = $request->validated();
-
- $user = User::find($request->input('id'));
- if (!$user) {
- return $this->fail([400202, '用户不存在']);
- }
- if (isset($params['email'])) {
- if (User::byEmail($params['email'])->first() && $user->email !== $params['email']) {
- return $this->fail([400201, '邮箱已被使用']);
- }
- }
- // 处理密码
- if (isset($params['password'])) {
- $params['password'] = password_hash($params['password'], PASSWORD_DEFAULT);
- $params['password_algo'] = NULL;
- } else {
- unset($params['password']);
- }
- // 处理订阅计划
- if (isset($params['plan_id'])) {
- $plan = Plan::find($params['plan_id']);
- if (!$plan) {
- return $this->fail([400202, '订阅计划不存在']);
- }
- $params['group_id'] = $plan->group_id;
- }
- // 处理邀请用户
- if ($request->input('invite_user_email') && $inviteUser = User::byEmail($request->input('invite_user_email'))->first()) {
- $params['invite_user_id'] = $inviteUser->id;
- } else {
- $params['invite_user_id'] = null;
- }
-
- if (isset($params['banned']) && (int) $params['banned'] === 1) {
- $authService = new AuthService($user);
- $authService->removeAllSessions();
- }
- if (isset($params['balance'])) {
- $params['balance'] = $params['balance'] * 100;
- }
- if (isset($params['commission_balance'])) {
- $params['commission_balance'] = $params['commission_balance'] * 100;
- }
-
- try {
- $user->update($params);
- } catch (\Exception $e) {
- Log::error($e);
- return $this->fail([500, '保存失败']);
- }
- return $this->success(true);
- }
-
- // Export users to CSV.
- public function dumpCSV(Request $request)
- {
- ini_set('memory_limit', '-1');
- gc_enable(); // 启用垃圾回收
-
- $scopeInfo = $this->resolveScope($request);
- $scope = $scopeInfo['scope'];
- $userIds = $scopeInfo['user_ids'];
-
- if ($scope === 'selected') {
- if (empty($userIds)) {
- return $this->fail([422, 'user_ids不能为空']);
- }
- }
-
- // 优化查询:使用with预加载plan关系,避免N+1问题
- $query = User::query()
- ->with('plan:id,name')
- ->orderBy('id', 'asc')
- ->select([
- 'email',
- 'balance',
- 'commission_balance',
- 'transfer_enable',
- 'u',
- 'd',
- 'expired_at',
- 'token',
- 'plan_id'
- ]);
-
- if ($scope === 'selected') {
- $query->whereIn('id', $userIds);
- } elseif ($scope === 'filtered') {
- $this->applyFiltersAndSorts($request, $query);
- } // all: ignore filter/sort
-
- $filename = 'users_' . date('Y-m-d_His') . '.csv';
-
- return response()->streamDownload(function () use ($query) {
- // 打开输出流
- $output = fopen('php://output', 'w');
-
- // 添加BOM标记,确保Excel正确显示中文
- fprintf($output, chr(0xEF) . chr(0xBB) . chr(0xBF));
-
- // 写入CSV头部
- fputcsv($output, [
- '邮箱',
- '余额',
- '推广佣金',
- '总流量',
- '剩余流量',
- '套餐到期时间',
- '订阅计划',
- '订阅地址'
- ]);
-
- // 分批处理数据以减少内存使用
- $query->chunk(500, function ($users) use ($output) {
- foreach ($users as $user) {
- try {
- $row = [
- $user->email,
- number_format($user->balance / 100, 2),
- number_format($user->commission_balance / 100, 2),
- Helper::trafficConvert($user->transfer_enable),
- Helper::trafficConvert($user->transfer_enable - ($user->u + $user->d)),
- $user->expired_at ? date('Y-m-d H:i:s', $user->expired_at) : '长期有效',
- $user->plan ? $user->plan->name : '无订阅',
- Helper::getSubscribeUrl($user->token)
- ];
- fputcsv($output, $row);
- } catch (\Exception $e) {
- Log::error('CSV导出错误: ' . $e->getMessage(), [
- 'user_id' => $user->id,
- 'email' => $user->email
- ]);
- continue; // 继续处理下一条记录
- }
- }
-
- // 清理内存
- gc_collect_cycles();
- });
-
- fclose($output);
- }, $filename, [
- 'Content-Type' => 'text/csv; charset=UTF-8',
- 'Content-Disposition' => 'attachment; filename="' . $filename . '"'
- ]);
- }
-
- public function generate(UserGenerate $request)
- {
- if ($request->input('email_prefix')) {
- // If generate_count is specified with email_prefix, generate multiple users with incremented emails
- if ($request->input('generate_count')) {
- return $this->multiGenerateWithPrefix($request);
- }
-
- // Single user generation with email_prefix
- $email = $request->input('email_prefix') . '@' . $request->input('email_suffix');
-
- if (User::byEmail($email)->exists()) {
- return $this->fail([400201, '邮箱已存在于系统中']);
- }
-
- $userService = app(UserService::class);
- $user = $userService->createUser([
- 'email' => $email,
- 'password' => $request->input('password') ?? $email,
- 'plan_id' => $request->input('plan_id'),
- 'expired_at' => $request->input('expired_at'),
- ]);
-
- if (!$user->save()) {
- return $this->fail([500, '生成失败']);
- }
- return $this->success(true);
- }
-
- if ($request->input('generate_count')) {
- return $this->multiGenerate($request);
- }
- }
-
- private function multiGenerate(Request $request)
- {
- $userService = app(UserService::class);
- $usersData = [];
-
- for ($i = 0; $i < $request->input('generate_count'); $i++) {
- $email = Helper::randomChar(6) . '@' . $request->input('email_suffix');
- $usersData[] = [
- 'email' => $email,
- 'password' => $request->input('password') ?? $email,
- 'plan_id' => $request->input('plan_id'),
- 'expired_at' => $request->input('expired_at'),
- ];
- }
-
-
-
- try {
- DB::beginTransaction();
- $users = [];
- foreach ($usersData as $userData) {
- $user = $userService->createUser($userData);
- $user->save();
- $users[] = $user;
- }
- DB::commit();
- } catch (\Exception $e) {
- DB::rollBack();
- return $this->fail([500, '生成失败']);
- }
-
- // 判断是否导出 CSV
- if ($request->input('download_csv')) {
- $headers = [
- 'Content-Type' => 'text/csv',
- 'Content-Disposition' => 'attachment; filename="users.csv"',
- ];
- $callback = function () use ($users, $request) {
- $handle = fopen('php://output', 'w');
- fputcsv($handle, ['账号', '密码', '过期时间', 'UUID', '创建时间', '订阅地址']);
- foreach ($users as $user) {
- $user = $user->refresh();
- $expireDate = $user['expired_at'] === NULL ? '长期有效' : date('Y-m-d H:i:s', $user['expired_at']);
- $createDate = date('Y-m-d H:i:s', $user['created_at']);
- $password = $request->input('password') ?? $user['email'];
- $subscribeUrl = Helper::getSubscribeUrl($user['token']);
- fputcsv($handle, [$user['email'], $password, $expireDate, $user['uuid'], $createDate, $subscribeUrl]);
- }
- fclose($handle);
- };
- return response()->streamDownload($callback, 'users.csv', $headers);
- }
-
- // 默认返回 JSON
- $data = collect($users)->map(function ($user) use ($request) {
- return [
- 'email' => $user['email'],
- 'password' => $request->input('password') ?? $user['email'],
- 'expired_at' => $user['expired_at'] === NULL ? '长期有效' : date('Y-m-d H:i:s', $user['expired_at']),
- 'uuid' => $user['uuid'],
- 'created_at' => date('Y-m-d H:i:s', $user['created_at']),
- 'subscribe_url' => Helper::getSubscribeUrl($user['token']),
- ];
- });
- return response()->json([
- 'code' => 0,
- 'message' => '批量生成成功',
- 'data' => $data,
- ]);
- }
-
- private function multiGenerateWithPrefix(Request $request)
- {
- $userService = app(UserService::class);
- $usersData = [];
- $emailPrefix = $request->input('email_prefix');
- $emailSuffix = $request->input('email_suffix');
- $generateCount = $request->input('generate_count');
-
- // Check if any of the emails with prefix already exist
- for ($i = 1; $i <= $generateCount; $i++) {
- $email = $emailPrefix . '_' . $i . '@' . $emailSuffix;
- if (User::where('email', $email)->exists()) {
- return $this->fail([400201, '邮箱 ' . $email . ' 已存在于系统中']);
- }
- }
-
- // Generate user data for batch creation
- for ($i = 1; $i <= $generateCount; $i++) {
- $email = $emailPrefix . '_' . $i . '@' . $emailSuffix;
- $usersData[] = [
- 'email' => $email,
- 'password' => $request->input('password') ?? $email,
- 'plan_id' => $request->input('plan_id'),
- 'expired_at' => $request->input('expired_at'),
- ];
- }
-
- try {
- DB::beginTransaction();
- $users = [];
- foreach ($usersData as $userData) {
- $user = $userService->createUser($userData);
- $user->save();
- $users[] = $user;
- }
- DB::commit();
- } catch (\Exception $e) {
- DB::rollBack();
- return $this->fail([500, '生成失败']);
- }
-
- // 判断是否导出 CSV
- if ($request->input('download_csv')) {
- $headers = [
- 'Content-Type' => 'text/csv',
- 'Content-Disposition' => 'attachment; filename="users.csv"',
- ];
- $callback = function () use ($users, $request) {
- $handle = fopen('php://output', 'w');
- fputcsv($handle, ['账号', '密码', '过期时间', 'UUID', '创建时间', '订阅地址']);
- foreach ($users as $user) {
- $user = $user->refresh();
- $expireDate = $user['expired_at'] === NULL ? '长期有效' : date('Y-m-d H:i:s', $user['expired_at']);
- $createDate = date('Y-m-d H:i:s', $user['created_at']);
- $password = $request->input('password') ?? $user['email'];
- $subscribeUrl = Helper::getSubscribeUrl($user['token']);
- fputcsv($handle, [$user['email'], $password, $expireDate, $user['uuid'], $createDate, $subscribeUrl]);
- }
- fclose($handle);
- };
- return response()->streamDownload($callback, 'users.csv', $headers);
- }
-
- // 默认返回 JSON
- $data = collect($users)->map(function ($user) use ($request) {
- return [
- 'email' => $user['email'],
- 'password' => $request->input('password') ?? $user['email'],
- 'expired_at' => $user['expired_at'] === NULL ? '长期有效' : date('Y-m-d H:i:s', $user['expired_at']),
- 'uuid' => $user['uuid'],
- 'created_at' => date('Y-m-d H:i:s', $user['created_at']),
- 'subscribe_url' => Helper::getSubscribeUrl($user['token']),
- ];
- });
- return response()->json([
- 'code' => 0,
- 'message' => '批量生成成功',
- 'data' => $data,
- ]);
- }
-
- public function sendMail(UserSendMail $request)
- {
- ini_set('memory_limit', '-1');
- $scopeInfo = $this->resolveScope($request);
- $scope = $scopeInfo['scope'];
- $userIds = $scopeInfo['user_ids'];
-
- if ($scope === 'selected') {
- if (empty($userIds)) {
- return $this->fail([422, 'user_ids不能为空']);
- }
- }
-
- $sortType = in_array($request->input('sort_type'), ['ASC', 'DESC']) ? $request->input('sort_type') : 'DESC';
- $sort = $request->input('sort') ? $request->input('sort') : 'created_at';
-
- $builder = User::query()
- ->with('plan:id,name')
- ->orderBy('id', 'desc');
-
- if ($scope === 'filtered') {
- // filtered: apply filters/sort
- $builder->orderBy($sort, $sortType);
- $this->applyFiltersAndSorts($request, $builder);
- } elseif ($scope === 'selected') {
- $builder->whereIn('id', $userIds);
- } // all: ignore filter/sort
-
- $subject = $request->input('subject');
- $content = $request->input('content');
- $appName = admin_setting('app_name', 'XBoard');
- $appUrl = admin_setting('app_url');
-
- $chunkSize = 1000;
-
- $builder->chunk($chunkSize, function ($users) use ($subject, $content, $appName, $appUrl) {
- foreach ($users as $user) {
- $vars = [
- 'app.name' => $appName,
- 'app.url' => $appUrl,
- 'now' => now()->format('Y-m-d H:i:s'),
- 'user.id' => $user->id,
- 'user.email' => $user->email,
- 'user.uuid' => $user->uuid,
- 'user.plan_name' => $user->plan?->name ?? '',
- 'user.expired_at' => $user->expired_at ? date('Y-m-d H:i:s', $user->expired_at) : '',
- 'user.transfer_enable' => (int) ($user->transfer_enable ?? 0),
- 'user.transfer_used' => (int) (($user->u ?? 0) + ($user->d ?? 0)),
- 'user.transfer_left' => (int) (($user->transfer_enable ?? 0) - (($user->u ?? 0) + ($user->d ?? 0))),
- ];
-
- $templateValue = [
- 'name' => $appName,
- 'url' => $appUrl,
- 'content' => $content,
- 'vars' => $vars,
- 'content_mode' => 'text',
- ];
-
- dispatch(new SendEmailJob([
- 'email' => $user->email,
- 'subject' => $subject,
- 'template_name' => 'notify',
- 'template_value' => $templateValue
- ], 'send_email_mass'));
- }
- });
-
- return $this->success(true);
- }
-
- public function ban(Request $request)
- {
- $scopeInfo = $this->resolveScope($request);
- $scope = $scopeInfo['scope'];
- $userIds = $scopeInfo['user_ids'];
-
- if ($scope === 'selected') {
- if (empty($userIds)) {
- return $this->fail([422, 'user_ids不能为空']);
- }
- }
-
- $sortType = in_array($request->input('sort_type'), ['ASC', 'DESC']) ? $request->input('sort_type') : 'DESC';
- $sort = $request->input('sort') ? $request->input('sort') : 'created_at';
-
- $builder = User::query()->orderBy('id', 'desc');
-
- if ($scope === 'filtered') {
- // filtered: keep current semantics
- $builder->orderBy($sort, $sortType);
- $this->applyFiltersAndSorts($request, $builder);
- } elseif ($scope === 'selected') {
- $builder->whereIn('id', $userIds);
- } // all: ignore filter/sort
-
- try {
- $builder->update([
- 'banned' => 1
- ]);
- } catch (\Exception $e) {
- Log::error($e);
- return $this->fail([500, '处理失败']);
- }
- // Full refresh not implemented.
- return $this->success(true);
- }
-
- // Delete user and related data.
- public function destroy(Request $request)
- {
- $request->validate([
- 'id' => 'required|exists:App\Models\User,id'
- ], [
- 'id.required' => '用户ID不能为空',
- 'id.exists' => '用户不存在'
- ]);
- $user = User::find($request->input('id'));
- try {
- DB::beginTransaction();
- $user->orders()->delete();
- $user->codes()->delete();
- $user->stat()->delete();
- $user->tickets()->delete();
- $user->delete();
- DB::commit();
- return $this->success(true);
- } catch (\Exception $e) {
- DB::rollBack();
- Log::error($e);
- return $this->fail([500, '删除失败']);
- }
- }
-}
diff --git a/Xboard/app/Http/Controllers/V2/Client/AppController.php b/Xboard/app/Http/Controllers/V2/Client/AppController.php
deleted file mode 100644
index 85ec531..0000000
--- a/Xboard/app/Http/Controllers/V2/Client/AppController.php
+++ /dev/null
@@ -1,153 +0,0 @@
- [
- 'app_name' => admin_setting('app_name', 'XB加速器'), // 应用名称
- 'app_description' => admin_setting('app_description', '专业的网络加速服务'), // 应用描述
- 'app_url' => admin_setting('app_url', 'https://app.example.com'), // 应用官网 URL
- 'logo' => admin_setting('logo', 'https://example.com/logo.png'), // 应用 Logo URL
- 'version' => admin_setting('app_version', '1.0.0'), // 应用版本号
- ],
- 'features' => [
- 'enable_register' => (bool) admin_setting('app_enable_register', true), // 是否开启注册功能
- 'enable_invite_system' => (bool) admin_setting('app_enable_invite_system', true), // 是否开启邀请系统
- 'enable_telegram_bot' => (bool) admin_setting('telegram_bot_enable', false), // 是否开启 Telegram 机器人
- 'enable_ticket_system' => (bool) admin_setting('app_enable_ticket_system', true), // 是否开启工单系统
- 'ticket_must_wait_reply' => (bool) admin_setting('ticket_must_wait_reply', 0), // 工单是否需要等待管理员回复后才可继续发消息
- 'enable_commission_system' => (bool) admin_setting('app_enable_commission_system', true), // 是否开启佣金系统
- 'enable_traffic_log' => (bool) admin_setting('app_enable_traffic_log', true), // 是否开启流量日志
- 'enable_knowledge_base' => (bool) admin_setting('app_enable_knowledge_base', true), // 是否开启知识库
- 'enable_announcements' => (bool) admin_setting('app_enable_announcements', true), // 是否开启公告系统
- 'enable_auto_renewal' => (bool) admin_setting('app_enable_auto_renewal', false), // 是否开启自动续费
- 'enable_coupon_system' => (bool) admin_setting('app_enable_coupon_system', true), // 是否开启优惠券系统
- 'enable_speed_test' => (bool) admin_setting('app_enable_speed_test', true), // 是否开启测速功能
- 'enable_server_ping' => (bool) admin_setting('app_enable_server_ping', true), // 是否开启服务器延迟检测
- ],
- 'ui_config' => [
- 'theme' => [
- 'primary_color' => admin_setting('app_primary_color', '#00C851'), // 主色调 (十六进制)
- 'secondary_color' => admin_setting('app_secondary_color', '#007E33'), // 辅助色 (十六进制)
- 'accent_color' => admin_setting('app_accent_color', '#FF6B35'), // 强调色 (十六进制)
- 'background_color' => admin_setting('app_background_color', '#F5F5F5'), // 背景色 (十六进制)
- 'text_color' => admin_setting('app_text_color', '#333333'), // 文字色 (十六进制)
- ],
- 'home_screen' => [
- 'show_speed_test' => (bool) admin_setting('app_show_speed_test', true), // 是否显示测速
- 'show_traffic_chart' => (bool) admin_setting('app_show_traffic_chart', true), // 是否显示流量图表
- 'show_server_ping' => (bool) admin_setting('app_show_server_ping', true), // 是否显示服务器延迟
- 'default_server_sort' => admin_setting('app_default_server_sort', 'ping'), // 默认服务器排序方式
- 'show_connection_status' => (bool) admin_setting('app_show_connection_status', true), // 是否显示连接状态
- ],
- 'server_list' => [
- 'show_country_flags' => (bool) admin_setting('app_show_country_flags', true), // 是否显示国家旗帜
- 'show_ping_values' => (bool) admin_setting('app_show_ping_values', true), // 是否显示延迟值
- 'show_traffic_usage' => (bool) admin_setting('app_show_traffic_usage', true), // 是否显示流量使用
- 'group_by_country' => (bool) admin_setting('app_group_by_country', false), // 是否按国家分组
- 'show_server_status' => (bool) admin_setting('app_show_server_status', true), // 是否显示服务器状态
- ],
- ],
- 'business_rules' => [
- 'min_password_length' => (int) admin_setting('app_min_password_length', 8), // 最小密码长度
- 'max_login_attempts' => (int) admin_setting('app_max_login_attempts', 5), // 最大登录尝试次数
- 'session_timeout_minutes' => (int) admin_setting('app_session_timeout_minutes', 30), // 会话超时时间(分钟)
- 'auto_disconnect_after_minutes' => (int) admin_setting('app_auto_disconnect_after_minutes', 60), // 自动断开连接时间(分钟)
- 'max_concurrent_connections' => (int) admin_setting('app_max_concurrent_connections', 3), // 最大并发连接数
- 'traffic_warning_threshold' => (float) admin_setting('app_traffic_warning_threshold', 0.8), // 流量警告阈值(0-1)
- 'subscription_reminder_days' => admin_setting('app_subscription_reminder_days', [7, 3, 1]), // 订阅到期提醒天数
- 'connection_timeout_seconds' => (int) admin_setting('app_connection_timeout_seconds', 10), // 连接超时时间(秒)
- 'health_check_interval_seconds' => (int) admin_setting('app_health_check_interval_seconds', 30), // 健康检查间隔(秒)
- ],
- 'server_config' => [
- 'default_kernel' => admin_setting('app_default_kernel', 'clash'), // 默认内核 (clash/singbox)
- 'auto_select_fastest' => (bool) admin_setting('app_auto_select_fastest', true), // 是否自动选择最快服务器
- 'fallback_servers' => admin_setting('app_fallback_servers', ['server1', 'server2']), // 备用服务器列表
- 'enable_auto_switch' => (bool) admin_setting('app_enable_auto_switch', true), // 是否开启自动切换
- 'switch_threshold_ms' => (int) admin_setting('app_switch_threshold_ms', 1000), // 切换阈值(毫秒)
- ],
- 'security_config' => [
- 'tos_url' => admin_setting('tos_url', 'https://example.com/tos'), // 服务条款 URL
- 'privacy_policy_url' => admin_setting('app_privacy_policy_url', 'https://example.com/privacy'), // 隐私政策 URL
- 'is_email_verify' => (int) admin_setting('email_verify', 1), // 是否开启邮箱验证 (0/1)
- 'is_invite_force' => (int) admin_setting('invite_force', 0), // 是否强制邀请码 (0/1)
- 'email_whitelist_suffix' => (int) admin_setting('email_whitelist_suffix', 0), // 邮箱白名单后缀 (0/1)
- 'is_captcha' => (int) admin_setting('captcha_enable', 1), // 是否开启验证码 (0/1)
- 'captcha_type' => admin_setting('captcha_type', 'recaptcha'), // 验证码类型 (recaptcha/turnstile)
- 'recaptcha_site_key' => admin_setting('recaptcha_site_key', '6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI'), // reCAPTCHA 站点密钥
- 'recaptcha_v3_site_key' => admin_setting('recaptcha_v3_site_key', '6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI'), // reCAPTCHA v3 站点密钥
- 'recaptcha_v3_score_threshold' => (float) admin_setting('recaptcha_v3_score_threshold', 0.5), // reCAPTCHA v3 分数阈值
- 'turnstile_site_key' => admin_setting('turnstile_site_key', '0x4AAAAAAAABkMYinukE8nzUg'), // Turnstile 站点密钥
- ],
- 'payment_config' => [
- 'currency' => admin_setting('currency', 'CNY'), // 货币类型
- 'currency_symbol' => admin_setting('currency_symbol', '¥'), // 货币符号
- 'withdraw_methods' => admin_setting('app_withdraw_methods', ['alipay', 'wechat', 'bank']), // 提现方式列表
- 'min_withdraw_amount' => (int) admin_setting('app_min_withdraw_amount', 100), // 最小提现金额(分)
- 'withdraw_fee_rate' => (float) admin_setting('app_withdraw_fee_rate', 0.01), // 提现手续费率
- ],
- 'notification_config' => [
- 'enable_push_notifications' => (bool) admin_setting('app_enable_push_notifications', true), // 是否开启推送通知
- 'enable_email_notifications' => (bool) admin_setting('app_enable_email_notifications', true), // 是否开启邮件通知
- 'enable_sms_notifications' => (bool) admin_setting('app_enable_sms_notifications', false), // 是否开启短信通知
- 'notification_schedule' => [
- 'traffic_warning' => (bool) admin_setting('app_notification_traffic_warning', true), // 流量警告通知
- 'subscription_expiry' => (bool) admin_setting('app_notification_subscription_expiry', true), // 订阅到期通知
- 'server_maintenance' => (bool) admin_setting('app_notification_server_maintenance', true), // 服务器维护通知
- 'promotional_offers' => (bool) admin_setting('app_notification_promotional_offers', false), // 促销优惠通知
- ],
- ],
- 'cache_config' => [
- 'config_cache_duration' => (int) admin_setting('app_config_cache_duration', 3600), // 配置缓存时长(秒)
- 'server_list_cache_duration' => (int) admin_setting('app_server_list_cache_duration', 1800), // 服务器列表缓存时长(秒)
- 'user_info_cache_duration' => (int) admin_setting('app_user_info_cache_duration', 900), // 用户信息缓存时长(秒)
- ],
- 'last_updated' => time(), // 最后更新时间戳
- ];
- $config['config_hash'] = md5(json_encode($config)); // 配置哈希值(用于校验)
-
- $config = $config ?? [];
- return response()->json(['data' => $config]);
- }
-
- public function getVersion(Request $request)
- {
- if (
- strpos($request->header('user-agent'), 'tidalab/4.0.0') !== false
- || strpos($request->header('user-agent'), 'tunnelab/4.0.0') !== false
- ) {
- if (strpos($request->header('user-agent'), 'Win64') !== false) {
- $data = [
- 'version' => admin_setting('windows_version'),
- 'download_url' => admin_setting('windows_download_url')
- ];
- } else {
- $data = [
- 'version' => admin_setting('macos_version'),
- 'download_url' => admin_setting('macos_download_url')
- ];
- }
- } else {
- $data = [
- 'windows_version' => admin_setting('windows_version'),
- 'windows_download_url' => admin_setting('windows_download_url'),
- 'macos_version' => admin_setting('macos_version'),
- 'macos_download_url' => admin_setting('macos_download_url'),
- 'android_version' => admin_setting('android_version'),
- 'android_download_url' => admin_setting('android_download_url')
- ];
- }
- return $this->success($data);
- }
-}
diff --git a/Xboard/app/Http/Controllers/V2/Server/ServerController.php b/Xboard/app/Http/Controllers/V2/Server/ServerController.php
deleted file mode 100644
index ff58951..0000000
--- a/Xboard/app/Http/Controllers/V2/Server/ServerController.php
+++ /dev/null
@@ -1,138 +0,0 @@
- false];
-
- if ((bool) admin_setting('server_ws_enable', 1)) {
- $customUrl = trim((string) admin_setting('server_ws_url', ''));
-
- if ($customUrl !== '') {
- $wsUrl = rtrim($customUrl, '/');
- } else {
- $wsScheme = $request->isSecure() ? 'wss' : 'ws';
- $wsUrl = "{$wsScheme}://{$request->getHost()}:8076";
- }
-
- $websocket = [
- 'enabled' => true,
- 'ws_url' => $wsUrl,
- ];
- }
-
- return response()->json([
- 'websocket' => $websocket
- ]);
- }
-
- /**
- * node report api - merge traffic + alive + status
- * POST /api/v2/server/node/report
- */
- public function report(Request $request): JsonResponse
- {
- $node = $request->attributes->get('node_info');
- $nodeType = $node->type;
- $nodeId = $node->id;
-
- Cache::put(CacheKey::get('SERVER_' . strtoupper($nodeType) . '_LAST_CHECK_AT', $nodeId), time(), 3600);
-
- // hanle traffic data
- $traffic = $request->input('traffic');
- if (is_array($traffic) && !empty($traffic)) {
- $data = array_filter($traffic, function ($item) {
- return is_array($item)
- && count($item) === 2
- && is_numeric($item[0])
- && is_numeric($item[1]);
- });
-
- if (!empty($data)) {
- Cache::put(
- CacheKey::get('SERVER_' . strtoupper($nodeType) . '_ONLINE_USER', $nodeId),
- count($data),
- 3600
- );
- Cache::put(
- CacheKey::get('SERVER_' . strtoupper($nodeType) . '_LAST_PUSH_AT', $nodeId),
- time(),
- 3600
- );
- $userService = new UserService();
- $userService->trafficFetch($node, $nodeType, $data);
- }
- }
-
- // handle alive data
- $alive = $request->input('alive');
- if (is_array($alive) && !empty($alive)) {
- $deviceStateService = app(DeviceStateService::class);
- foreach ($alive as $uid => $ips) {
- $deviceStateService->setDevices((int) $uid, $nodeId, (array) $ips);
- }
- }
-
- // handle active connections
- $online = $request->input('online');
- if (is_array($online) && !empty($online)) {
- $cacheTime = max(300, (int) admin_setting('server_push_interval', 60) * 3);
- foreach ($online as $uid => $conn) {
- $cacheKey = CacheKey::get("USER_ONLINE_CONN_{$nodeType}_{$nodeId}", $uid);
- Cache::put($cacheKey, (int) $conn, $cacheTime);
- }
- }
-
- // handle node status
- $status = $request->input('status');
- if (is_array($status) && !empty($status)) {
- $statusData = [
- 'cpu' => (float) ($status['cpu'] ?? 0),
- 'mem' => [
- 'total' => (int) ($status['mem']['total'] ?? 0),
- 'used' => (int) ($status['mem']['used'] ?? 0),
- ],
- 'swap' => [
- 'total' => (int) ($status['swap']['total'] ?? 0),
- 'used' => (int) ($status['swap']['used'] ?? 0),
- ],
- 'disk' => [
- 'total' => (int) ($status['disk']['total'] ?? 0),
- 'used' => (int) ($status['disk']['used'] ?? 0),
- ],
- 'updated_at' => now()->timestamp,
- 'kernel_status' => $status['kernel_status'] ?? null,
- ];
-
- $cacheTime = max(300, (int) admin_setting('server_push_interval', 60) * 3);
- cache([
- CacheKey::get('SERVER_' . strtoupper($nodeType) . '_LOAD_STATUS', $nodeId) => $statusData,
- CacheKey::get('SERVER_' . strtoupper($nodeType) . '_LAST_LOAD_AT', $nodeId) => now()->timestamp,
- ], $cacheTime);
- }
-
- // handle node metrics (Metrics)
- $metrics = $request->input('metrics');
- if (is_array($metrics) && !empty($metrics)) {
- ServerService::updateMetrics($node, $metrics);
- }
-
- return response()->json(['data' => true]);
- }
-}
diff --git a/Xboard/app/Http/Kernel.php b/Xboard/app/Http/Kernel.php
deleted file mode 100644
index 6fd86ab..0000000
--- a/Xboard/app/Http/Kernel.php
+++ /dev/null
@@ -1,99 +0,0 @@
-
- */
- protected $middleware = [
- \Illuminate\Http\Middleware\HandleCors::class,
- \App\Http\Middleware\TrustProxies::class,
- \App\Http\Middleware\CheckForMaintenanceMode::class,
- \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
- \App\Http\Middleware\TrimStrings::class,
- \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
- \App\Http\Middleware\InitializePlugins::class,
- ];
-
- /**
- * The application's route middleware groups.
- *
- * @var array>
- */
- protected $middlewareGroups = [
- 'web' => [
- // \App\Http\Middleware\EncryptCookies::class,
-// \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
-// \Illuminate\Session\Middleware\StartSession::class,
- // \Illuminate\Session\Middleware\AuthenticateSession::class,
-// \Illuminate\View\Middleware\ShareErrorsFromSession::class,
-// \App\Http\Middleware\VerifyCsrfToken::class,
-// \Illuminate\Routing\Middleware\SubstituteBindings::class,
- \App\Http\Middleware\ApplyRuntimeSettings::class,
- ],
-
- 'api' => [
- // \App\Http\Middleware\EncryptCookies::class,
-// \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
-// \Illuminate\Session\Middleware\StartSession::class,
- // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
- // \Illuminate\Routing\Middleware\ThrottleRequests::class . ':api',
- // \Illuminate\Routing\Middleware\SubstituteBindings::class,
- \App\Http\Middleware\ApplyRuntimeSettings::class,
- \App\Http\Middleware\ForceJson::class,
- \App\Http\Middleware\Language::class,
- 'bindings',
- ],
- ];
-
- /**
- * The application's route middleware.
- *
- * These middleware may be assigned to groups or used individually.
- *
- * @var array
- */
- protected $middlewareAliases = [
- 'auth' => \App\Http\Middleware\Authenticate::class,
- 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
- 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
- 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
- 'can' => \Illuminate\Auth\Middleware\Authorize::class,
- 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
- 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
- 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
- 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
- 'user' => \App\Http\Middleware\User::class,
- 'admin' => \App\Http\Middleware\Admin::class,
- 'client' => \App\Http\Middleware\Client::class,
- 'staff' => \App\Http\Middleware\Staff::class,
- 'log' => \App\Http\Middleware\RequestLog::class,
- 'server' => \App\Http\Middleware\Server::class,
- 'abilities' => \Laravel\Sanctum\Http\Middleware\CheckAbilities::class,
- 'ability' => \Laravel\Sanctum\Http\Middleware\CheckForAnyAbility::class,
- ];
-
- /**
- * The priority-sorted list of middleware.
- *
- * This forces non-global middleware to always be in the given order.
- *
- * @var array
- */
- protected $middlewarePriority = [
- \Illuminate\Session\Middleware\StartSession::class,
- \Illuminate\View\Middleware\ShareErrorsFromSession::class,
- \Illuminate\Routing\Middleware\ThrottleRequests::class,
- \Illuminate\Session\Middleware\AuthenticateSession::class,
- \Illuminate\Routing\Middleware\SubstituteBindings::class,
- \Illuminate\Auth\Middleware\Authorize::class,
- ];
-}
diff --git a/Xboard/app/Http/Middleware/Admin.php b/Xboard/app/Http/Middleware/Admin.php
deleted file mode 100644
index 1a63b4f..0000000
--- a/Xboard/app/Http/Middleware/Admin.php
+++ /dev/null
@@ -1,30 +0,0 @@
-user();
-
- if (!$user || !$user->is_admin) {
- return response()->json(['message' => 'Unauthorized'], 403);
- }
-
- return $next($request);
- }
-}
diff --git a/Xboard/app/Http/Middleware/ApplyRuntimeSettings.php b/Xboard/app/Http/Middleware/ApplyRuntimeSettings.php
deleted file mode 100644
index 2c3e96a..0000000
--- a/Xboard/app/Http/Middleware/ApplyRuntimeSettings.php
+++ /dev/null
@@ -1,25 +0,0 @@
-expectsJson() ? null : null;
- }
-}
diff --git a/Xboard/app/Http/Middleware/CheckForMaintenanceMode.php b/Xboard/app/Http/Middleware/CheckForMaintenanceMode.php
deleted file mode 100644
index 53fcdd5..0000000
--- a/Xboard/app/Http/Middleware/CheckForMaintenanceMode.php
+++ /dev/null
@@ -1,18 +0,0 @@
-
- */
- protected $except = [
- // 示例:
- // '/api/health-check',
- // '/status'
- ];
-}
diff --git a/Xboard/app/Http/Middleware/Client.php b/Xboard/app/Http/Middleware/Client.php
deleted file mode 100644
index 77645e0..0000000
--- a/Xboard/app/Http/Middleware/Client.php
+++ /dev/null
@@ -1,33 +0,0 @@
-input('token', $request->route('token'));
- if (empty($token)) {
- throw new ApiException('token is null',403);
- }
- $user = User::where('token', $token)->first();
- if (!$user) {
- throw new ApiException('token is error',403);
- }
-
- Auth::setUser($user);
- return $next($request);
- }
-}
diff --git a/Xboard/app/Http/Middleware/EncryptCookies.php b/Xboard/app/Http/Middleware/EncryptCookies.php
deleted file mode 100644
index 31e9d1a..0000000
--- a/Xboard/app/Http/Middleware/EncryptCookies.php
+++ /dev/null
@@ -1,16 +0,0 @@
-
- */
- protected $except = [
- //
- ];
-}
diff --git a/Xboard/app/Http/Middleware/EnsureTransactionState.php b/Xboard/app/Http/Middleware/EnsureTransactionState.php
deleted file mode 100644
index 595dc50..0000000
--- a/Xboard/app/Http/Middleware/EnsureTransactionState.php
+++ /dev/null
@@ -1,29 +0,0 @@
- 0) {
- DB::rollBack();
- }
- }
- }
-}
diff --git a/Xboard/app/Http/Middleware/ForceJson.php b/Xboard/app/Http/Middleware/ForceJson.php
deleted file mode 100644
index ef87160..0000000
--- a/Xboard/app/Http/Middleware/ForceJson.php
+++ /dev/null
@@ -1,22 +0,0 @@
-headers->set('accept', 'application/json');
- return $next($request);
- }
-}
diff --git a/Xboard/app/Http/Middleware/InitializePlugins.php b/Xboard/app/Http/Middleware/InitializePlugins.php
deleted file mode 100644
index 0c5ae8d..0000000
--- a/Xboard/app/Http/Middleware/InitializePlugins.php
+++ /dev/null
@@ -1,37 +0,0 @@
-pluginManager = $pluginManager;
- }
-
- /**
- * Handle an incoming request.
- *
- * @param \Illuminate\Http\Request $request
- * @param \Closure $next
- * @return mixed
- */
- public function handle(Request $request, Closure $next)
- {
- // This single method call handles loading and booting all enabled plugins.
- // It's safe to call multiple times, as it will only run once per request.
- $this->pluginManager->initializeEnabledPlugins();
-
- return $next($request);
- }
-}
\ No newline at end of file
diff --git a/Xboard/app/Http/Middleware/Language.php b/Xboard/app/Http/Middleware/Language.php
deleted file mode 100644
index 8bb51e7..0000000
--- a/Xboard/app/Http/Middleware/Language.php
+++ /dev/null
@@ -1,17 +0,0 @@
-header('content-language')) {
- App::setLocale($request->header('content-language'));
- }
- return $next($request);
- }
-}
diff --git a/Xboard/app/Http/Middleware/RedirectIfAuthenticated.php b/Xboard/app/Http/Middleware/RedirectIfAuthenticated.php
deleted file mode 100644
index a7ef27c..0000000
--- a/Xboard/app/Http/Middleware/RedirectIfAuthenticated.php
+++ /dev/null
@@ -1,26 +0,0 @@
-check()) {
- return redirect('/home');
- }
-
- return $next($request);
- }
-}
diff --git a/Xboard/app/Http/Middleware/RequestLog.php b/Xboard/app/Http/Middleware/RequestLog.php
deleted file mode 100644
index 62dded3..0000000
--- a/Xboard/app/Http/Middleware/RequestLog.php
+++ /dev/null
@@ -1,60 +0,0 @@
-method() !== 'POST') {
- return $next($request);
- }
-
- $response = $next($request);
-
- try {
- $admin = $request->user();
- if (!$admin || !$admin->is_admin) {
- return $response;
- }
-
- $action = $this->resolveAction($request->path());
- $data = collect($request->all())->except(self::SENSITIVE_KEYS)->toArray();
-
- AdminAuditLog::insert([
- 'admin_id' => $admin->id,
- 'action' => $action,
- 'method' => $request->method(),
- 'uri' => $request->getRequestUri(),
- 'request_data' => json_encode($data, JSON_UNESCAPED_UNICODE),
- 'ip' => $request->getClientIp(),
- 'created_at' => time(),
- 'updated_at' => time(),
- ]);
- } catch (\Throwable $e) {
- \Log::warning('Audit log write failed: ' . $e->getMessage());
- }
-
- return $response;
- }
-
- private function resolveAction(string $path): string
- {
- // api/v2/{secure_path}/user/update → user.update
- $path = preg_replace('#^api/v[12]/[^/]+/#', '', $path);
- // gift-card/create-template → gift_card.create_template
- $path = str_replace('-', '_', $path);
- // user/update → user.update, server/manage/sort → server_manage.sort
- $segments = explode('/', $path);
- $method = array_pop($segments);
- $resource = implode('_', $segments);
-
- return $resource . '.' . $method;
- }
-}
-
diff --git a/Xboard/app/Http/Middleware/Server.php b/Xboard/app/Http/Middleware/Server.php
deleted file mode 100644
index 15dd494..0000000
--- a/Xboard/app/Http/Middleware/Server.php
+++ /dev/null
@@ -1,59 +0,0 @@
-validateRequest($request);
- $nodeType = $request->input('node_type', $nodeType);
- $normalizedNodeType = ServerModel::normalizeType($nodeType);
- $serverInfo = ServerService::getServer(
- $request->input('node_id'),
- $normalizedNodeType
- );
- if (!$serverInfo) {
- throw new ApiException('Server does not exist');
- }
-
- $request->attributes->set('node_info', $serverInfo);
- return $next($request);
- }
-
- private function validateRequest(Request $request): void
- {
- $request->validate([
- 'token' => [
- 'string',
- 'required',
- function ($attribute, $value, $fail) {
- if ($value !== admin_setting('server_token')) {
- $fail("Invalid {$attribute}");
- }
- },
- ],
- 'node_id' => 'required',
- 'node_type' => [
- 'nullable',
- function ($attribute, $value, $fail) use ($request) {
- if ($value === "v2node") {
- $value = null;
- }
- if (!ServerModel::isValidType($value)) {
- $fail("Invalid node type specified");
- return;
- }
- $request->merge([$attribute => ServerModel::normalizeType($value)]);
- },
- ]
- ]);
- }
-}
diff --git a/Xboard/app/Http/Middleware/Staff.php b/Xboard/app/Http/Middleware/Staff.php
deleted file mode 100644
index 5c700c1..0000000
--- a/Xboard/app/Http/Middleware/Staff.php
+++ /dev/null
@@ -1,30 +0,0 @@
-input('auth_data') ?? $request->header('authorization');
- if (!$authorization) throw new ApiException( '未登录或登陆已过期', 403);
-
- $user = AuthService::decryptAuthData($authorization);
- if (!$user || !$user['is_staff']) throw new ApiException('未登录或登陆已过期', 403);
- $request->merge([
- 'user' => $user
- ]);
- return $next($request);
- }
-}
diff --git a/Xboard/app/Http/Middleware/TrimStrings.php b/Xboard/app/Http/Middleware/TrimStrings.php
deleted file mode 100644
index fb507a4..0000000
--- a/Xboard/app/Http/Middleware/TrimStrings.php
+++ /dev/null
@@ -1,19 +0,0 @@
-
- */
- protected $except = [
- 'password',
- 'password_confirmation',
- 'encrypted_data',
- 'signature'
- ];
-}
diff --git a/Xboard/app/Http/Middleware/TrustProxies.php b/Xboard/app/Http/Middleware/TrustProxies.php
deleted file mode 100644
index 83c5400..0000000
--- a/Xboard/app/Http/Middleware/TrustProxies.php
+++ /dev/null
@@ -1,47 +0,0 @@
-|string|null
- */
- protected $proxies = [
- "173.245.48.0/20",
- "103.21.244.0/22",
- "103.22.200.0/22",
- "103.31.4.0/22",
- "141.101.64.0/18",
- "108.162.192.0/18",
- "190.93.240.0/20",
- "188.114.96.0/20",
- "197.234.240.0/22",
- "198.41.128.0/17",
- "162.158.0.0/15",
- "104.16.0.0/13",
- "104.24.0.0/14",
- "172.64.0.0/13",
- "131.0.72.0/22",
- "10.0.0.0/8",
- "172.16.0.0/12",
- "192.168.0.0/16",
- "169.254.0.0/16",
- "127.0.0.0/8",
- ];
-
- /**
- * 代理头映射
- * @var int
- */
- protected $headers =
- Request::HEADER_X_FORWARDED_FOR |
- Request::HEADER_X_FORWARDED_HOST |
- Request::HEADER_X_FORWARDED_PORT |
- Request::HEADER_X_FORWARDED_PROTO |
- Request::HEADER_X_FORWARDED_AWS_ELB;
-}
diff --git a/Xboard/app/Http/Middleware/User.php b/Xboard/app/Http/Middleware/User.php
deleted file mode 100644
index 14f3049..0000000
--- a/Xboard/app/Http/Middleware/User.php
+++ /dev/null
@@ -1,27 +0,0 @@
-check()) {
- throw new ApiException('未登录或登陆已过期', 403);
- }
- return $next($request);
- }
-}
diff --git a/Xboard/app/Http/Middleware/VerifyCsrfToken.php b/Xboard/app/Http/Middleware/VerifyCsrfToken.php
deleted file mode 100644
index 9e7c0bd..0000000
--- a/Xboard/app/Http/Middleware/VerifyCsrfToken.php
+++ /dev/null
@@ -1,22 +0,0 @@
-
- */
- protected $except = [
- //
- ];
-}
diff --git a/Xboard/app/Http/Requests/Admin/ConfigSave.php b/Xboard/app/Http/Requests/Admin/ConfigSave.php
deleted file mode 100644
index bec69a6..0000000
--- a/Xboard/app/Http/Requests/Admin/ConfigSave.php
+++ /dev/null
@@ -1,146 +0,0 @@
- '',
- 'invite_commission' => 'integer|nullable',
- 'invite_gen_limit' => 'integer|nullable',
- 'invite_never_expire' => '',
- 'commission_first_time_enable' => '',
- 'commission_auto_check_enable' => '',
- 'commission_withdraw_limit' => 'nullable|numeric',
- 'commission_withdraw_method' => 'nullable|array',
- 'withdraw_close_enable' => '',
- 'commission_distribution_enable' => '',
- 'commission_distribution_l1' => 'nullable|numeric',
- 'commission_distribution_l2' => 'nullable|numeric',
- 'commission_distribution_l3' => 'nullable|numeric',
- // site
- 'logo' => 'nullable|url',
- 'force_https' => '',
- 'stop_register' => '',
- 'app_name' => '',
- 'app_description' => '',
- 'app_url' => 'nullable|url',
- 'subscribe_url' => 'nullable',
- 'try_out_enable' => '',
- 'try_out_plan_id' => 'integer',
- 'try_out_hour' => 'numeric',
- 'tos_url' => 'nullable|url',
- 'currency' => '',
- 'currency_symbol' => '',
- 'ticket_must_wait_reply' => '',
- // subscribe
- 'plan_change_enable' => '',
- 'reset_traffic_method' => 'in:0,1,2,3,4',
- 'surplus_enable' => '',
- 'new_order_event_id' => '',
- 'renew_order_event_id' => '',
- 'change_order_event_id' => '',
- 'show_info_to_server_enable' => '',
- 'show_protocol_to_server_enable' => '',
- 'subscribe_path' => '',
- // server
- 'server_token' => 'nullable|min:16',
- 'server_pull_interval' => 'integer',
- 'server_push_interval' => 'integer',
- 'device_limit_mode' => 'integer',
- 'server_ws_enable' => 'boolean',
- 'server_ws_url' => 'nullable|url',
- // frontend
- 'frontend_theme' => '',
- 'frontend_theme_sidebar' => 'nullable|in:dark,light',
- 'frontend_theme_header' => 'nullable|in:dark,light',
- 'frontend_theme_color' => 'nullable|in:default,darkblue,black,green',
- 'frontend_background_url' => 'nullable|url',
- // email
- 'email_template' => '',
- 'email_host' => '',
- 'email_port' => '',
- 'email_username' => '',
- 'email_password' => '',
- 'email_encryption' => '',
- 'email_from_address' => '',
- 'remind_mail_enable' => '',
- // telegram
- 'telegram_bot_enable' => '',
- 'telegram_bot_token' => '',
- 'telegram_webhook_url' => 'nullable|url',
- 'telegram_discuss_id' => '',
- 'telegram_channel_id' => '',
- 'telegram_discuss_link' => 'nullable|url',
- // app
- 'windows_version' => '',
- 'windows_download_url' => '',
- 'macos_version' => '',
- 'macos_download_url' => '',
- 'android_version' => '',
- 'android_download_url' => '',
- // safe
- 'email_whitelist_enable' => 'boolean',
- 'email_whitelist_suffix' => 'nullable|array',
- 'email_gmail_limit_enable' => 'boolean',
- 'captcha_enable' => 'boolean',
- 'captcha_type' => 'in:recaptcha,turnstile,recaptcha-v3',
- 'recaptcha_enable' => 'boolean',
- 'recaptcha_key' => '',
- 'recaptcha_site_key' => '',
- 'recaptcha_v3_secret_key' => '',
- 'recaptcha_v3_site_key' => '',
- 'recaptcha_v3_score_threshold' => 'numeric|min:0|max:1',
- 'turnstile_secret_key' => '',
- 'turnstile_site_key' => '',
- 'email_verify' => 'bool',
- 'safe_mode_enable' => 'boolean',
- 'register_limit_by_ip_enable' => 'boolean',
- 'register_limit_count' => 'integer',
- 'register_limit_expire' => 'integer',
- 'secure_path' => 'min:8|regex:/^[\w-]*$/',
- 'password_limit_enable' => 'boolean',
- 'password_limit_count' => 'integer',
- 'password_limit_expire' => 'integer',
- 'default_remind_expire' => 'boolean',
- 'default_remind_traffic' => 'boolean',
- 'subscribe_template_singbox' => 'nullable',
- 'subscribe_template_clash' => 'nullable',
- 'subscribe_template_clashmeta' => 'nullable',
- 'subscribe_template_stash' => 'nullable',
- 'subscribe_template_surge' => 'nullable',
- 'subscribe_template_surfboard' => 'nullable'
- ];
- /**
- * Get the validation rules that apply to the request.
- *
- * @return array
- */
- public function rules()
- {
- return self::RULES;
- }
-
- public function messages()
- {
- // illiteracy prompt
- return [
- 'app_url.url' => '站点URL格式不正确,必须携带http(s)://',
- 'subscribe_url.url' => '订阅URL格式不正确,必须携带http(s)://',
- 'server_token.min' => '通讯密钥长度必须大于16位',
- 'tos_url.url' => '服务条款URL格式不正确,必须携带http(s)://',
- 'telegram_webhook_url.url' => 'Telegram Webhook地址格式不正确,必须携带http(s)://',
- 'telegram_discuss_link.url' => 'Telegram群组地址必须为URL格式,必须携带http(s)://',
- 'logo.url' => 'LOGO URL格式不正确,必须携带https(s)://',
- 'secure_path.min' => '后台路径长度最小为8位',
- 'secure_path.regex' => '后台路径只能为字母或数字',
- 'captcha_type.in' => '人机验证类型只能选择 recaptcha、turnstile 或 recaptcha-v3',
- 'recaptcha_v3_score_threshold.numeric' => 'reCAPTCHA v3 分数阈值必须为数字',
- 'recaptcha_v3_score_threshold.min' => 'reCAPTCHA v3 分数阈值不能小于0',
- 'recaptcha_v3_score_threshold.max' => 'reCAPTCHA v3 分数阈值不能大于1'
- ];
- }
-}
diff --git a/Xboard/app/Http/Requests/Admin/CouponGenerate.php b/Xboard/app/Http/Requests/Admin/CouponGenerate.php
deleted file mode 100644
index 70f3efc..0000000
--- a/Xboard/app/Http/Requests/Admin/CouponGenerate.php
+++ /dev/null
@@ -1,51 +0,0 @@
- 'nullable|integer|max:500',
- 'name' => 'required',
- 'type' => 'required|in:1,2',
- 'value' => 'required|integer',
- 'started_at' => 'required|integer',
- 'ended_at' => 'required|integer',
- 'limit_use' => 'nullable|integer',
- 'limit_use_with_user' => 'nullable|integer',
- 'limit_plan_ids' => 'nullable|array',
- 'limit_period' => 'nullable|array',
- 'code' => ''
- ];
- }
-
- public function messages()
- {
- return [
- 'generate_count.integer' => '生成数量必须为数字',
- 'generate_count.max' => '生成数量最大为500个',
- 'name.required' => '名称不能为空',
- 'type.required' => '类型不能为空',
- 'type.in' => '类型格式有误',
- 'value.required' => '金额或比例不能为空',
- 'value.integer' => '金额或比例格式有误',
- 'started_at.required' => '开始时间不能为空',
- 'started_at.integer' => '开始时间格式有误',
- 'ended_at.required' => '结束时间不能为空',
- 'ended_at.integer' => '结束时间格式有误',
- 'limit_use.integer' => '最大使用次数格式有误',
- 'limit_use_with_user.integer' => '限制用户使用次数格式有误',
- 'limit_plan_ids.array' => '指定订阅格式有误',
- 'limit_period.array' => '指定周期格式有误'
- ];
- }
-}
diff --git a/Xboard/app/Http/Requests/Admin/KnowledgeCategorySave.php b/Xboard/app/Http/Requests/Admin/KnowledgeCategorySave.php
deleted file mode 100644
index 9aabb7e..0000000
--- a/Xboard/app/Http/Requests/Admin/KnowledgeCategorySave.php
+++ /dev/null
@@ -1,29 +0,0 @@
- 'required',
- 'language' => 'required'
- ];
- }
-
- public function messages()
- {
- return [
- 'name.required' => '分类名称不能为空',
- 'language.required' => '分类语言不能为空'
- ];
- }
-}
diff --git a/Xboard/app/Http/Requests/Admin/KnowledgeCategorySort.php b/Xboard/app/Http/Requests/Admin/KnowledgeCategorySort.php
deleted file mode 100644
index c76f810..0000000
--- a/Xboard/app/Http/Requests/Admin/KnowledgeCategorySort.php
+++ /dev/null
@@ -1,28 +0,0 @@
- 'required|array'
- ];
- }
-
- public function messages()
- {
- return [
- 'knowledge_category_ids.required' => '分类不能为空',
- 'knowledge_category_ids.array' => '分类格式有误'
- ];
- }
-}
diff --git a/Xboard/app/Http/Requests/Admin/KnowledgeSave.php b/Xboard/app/Http/Requests/Admin/KnowledgeSave.php
deleted file mode 100644
index 296ddbb..0000000
--- a/Xboard/app/Http/Requests/Admin/KnowledgeSave.php
+++ /dev/null
@@ -1,35 +0,0 @@
- 'required',
- 'language' => 'required',
- 'title' => 'required',
- 'body' => 'required',
- 'show' => 'nullable|boolean'
- ];
- }
-
- public function messages()
- {
- return [
- 'title.required' => '标题不能为空',
- 'category.required' => '分类不能为空',
- 'body.required' => '内容不能为空',
- 'language.required' => '语言不能为空',
- 'show.boolean' => '显示状态必须为布尔值'
- ];
- }
-}
diff --git a/Xboard/app/Http/Requests/Admin/KnowledgeSort.php b/Xboard/app/Http/Requests/Admin/KnowledgeSort.php
deleted file mode 100644
index d29a899..0000000
--- a/Xboard/app/Http/Requests/Admin/KnowledgeSort.php
+++ /dev/null
@@ -1,28 +0,0 @@
- 'required|array'
- ];
- }
-
- public function messages()
- {
- return [
- 'knowledge_ids.required' => '知识ID不能为空',
- 'knowledge_ids.array' => '知识ID格式有误'
- ];
- }
-}
diff --git a/Xboard/app/Http/Requests/Admin/MailSend.php b/Xboard/app/Http/Requests/Admin/MailSend.php
deleted file mode 100644
index 86247a3..0000000
--- a/Xboard/app/Http/Requests/Admin/MailSend.php
+++ /dev/null
@@ -1,34 +0,0 @@
- 'required|in:1,2,3,4',
- 'subject' => 'required',
- 'content' => 'required',
- 'receiver' => 'array'
- ];
- }
-
- public function messages()
- {
- return [
- 'type.required' => '发送类型不能为空',
- 'type.in' => '发送类型格式有误',
- 'subject.required' => '主题不能为空',
- 'content.required' => '内容不能为空',
- 'receiver.array' => '收件人格式有误'
- ];
- }
-}
diff --git a/Xboard/app/Http/Requests/Admin/NoticeSave.php b/Xboard/app/Http/Requests/Admin/NoticeSave.php
deleted file mode 100644
index 0f6dc0b..0000000
--- a/Xboard/app/Http/Requests/Admin/NoticeSave.php
+++ /dev/null
@@ -1,33 +0,0 @@
- 'required',
- 'content' => 'required',
- 'img_url' => 'nullable|url',
- 'tags' => 'nullable|array'
- ];
- }
-
- public function messages()
- {
- return [
- 'title.required' => '标题不能为空',
- 'content.required' => '内容不能为空',
- 'img_url.url' => '图片URL格式不正确',
- 'tags.array' => '标签格式不正确'
- ];
- }
-}
diff --git a/Xboard/app/Http/Requests/Admin/OrderAssign.php b/Xboard/app/Http/Requests/Admin/OrderAssign.php
deleted file mode 100644
index 4b259a0..0000000
--- a/Xboard/app/Http/Requests/Admin/OrderAssign.php
+++ /dev/null
@@ -1,34 +0,0 @@
- 'required',
- 'email' => 'required',
- 'total_amount' => 'required',
- 'period' => 'required|in:month_price,quarter_price,half_year_price,year_price,two_year_price,three_year_price,onetime_price,reset_price'
- ];
- }
-
- public function messages()
- {
- return [
- 'plan_id.required' => '订阅不能为空',
- 'email.required' => '邮箱不能为空',
- 'total_amount.required' => '支付金额不能为空',
- 'period.required' => '订阅周期不能为空',
- 'period.in' => '订阅周期格式有误'
- ];
- }
-}
diff --git a/Xboard/app/Http/Requests/Admin/OrderFetch.php b/Xboard/app/Http/Requests/Admin/OrderFetch.php
deleted file mode 100644
index 9c4765b..0000000
--- a/Xboard/app/Http/Requests/Admin/OrderFetch.php
+++ /dev/null
@@ -1,32 +0,0 @@
- 'required|in:email,trade_no,status,commission_status,user_id,invite_user_id,callback_no,commission_balance',
- 'filter.*.condition' => 'required|in:>,<,=,>=,<=,模糊,!=',
- 'filter.*.value' => ''
- ];
- }
-
- public function messages()
- {
- return [
- 'filter.*.key.required' => '过滤键不能为空',
- 'filter.*.key.in' => '过滤键参数有误',
- 'filter.*.condition.required' => '过滤条件不能为空',
- 'filter.*.condition.in' => '过滤条件参数有误',
- ];
- }
-}
diff --git a/Xboard/app/Http/Requests/Admin/OrderUpdate.php b/Xboard/app/Http/Requests/Admin/OrderUpdate.php
deleted file mode 100644
index 8a38d10..0000000
--- a/Xboard/app/Http/Requests/Admin/OrderUpdate.php
+++ /dev/null
@@ -1,29 +0,0 @@
- 'in:0,1,2,3',
- 'commission_status' => 'in:0,1,3'
- ];
- }
-
- public function messages()
- {
- return [
- 'status.in' => '销售状态格式不正确',
- 'commission_status.in' => '佣金状态格式不正确'
- ];
- }
-}
diff --git a/Xboard/app/Http/Requests/Admin/PlanSave.php b/Xboard/app/Http/Requests/Admin/PlanSave.php
deleted file mode 100644
index c35e4a7..0000000
--- a/Xboard/app/Http/Requests/Admin/PlanSave.php
+++ /dev/null
@@ -1,157 +0,0 @@
- 'nullable|integer',
- 'name' => 'required|string|max:255',
- 'content' => 'nullable|string',
- 'reset_traffic_method' => 'integer|nullable',
- 'transfer_enable' => 'integer|required|min:1',
- 'prices' => 'nullable|array',
- 'prices.*' => 'nullable|numeric|min:0',
- 'group_id' => 'integer|nullable',
- 'speed_limit' => 'integer|nullable|min:0',
- 'device_limit' => 'integer|nullable|min:0',
- 'capacity_limit' => 'integer|nullable|min:0',
- 'tags' => 'array|nullable',
- ];
- }
-
- /**
- * Configure the validator instance.
- */
- public function withValidator(Validator $validator): void
- {
- $validator->after(function (Validator $validator) {
- $this->validatePrices($validator);
- });
- }
-
- /**
- * 验证价格配置
- */
- protected function validatePrices(Validator $validator): void
- {
- $prices = $this->input('prices', []);
-
- if (empty($prices)) {
- return;
- }
-
- // 获取所有有效的周期
- $validPeriods = array_keys(Plan::getAvailablePeriods());
-
- foreach ($prices as $period => $price) {
- // 验证周期是否有效
- if (!in_array($period, $validPeriods)) {
- $validator->errors()->add(
- "prices.{$period}",
- "不支持的订阅周期: {$period}"
- );
- continue;
- }
-
- // 价格可以为 null、空字符串或大于 0 的数字
- if ($price !== null && $price !== '') {
- // 转换为数字进行验证
- $numericPrice = is_numeric($price) ? (float) $price : null;
-
- if ($numericPrice === null) {
- $validator->errors()->add(
- "prices.{$period}",
- "价格必须是数字格式"
- );
- } elseif ($numericPrice < 0) {
- $validator->errors()->add(
- "prices.{$period}",
- "价格必须大于等于 0(如不需要此周期请留空)"
- );
- }
- }
- }
- }
-
- /**
- * 处理验证后的数据
- */
- protected function passedValidation(): void
- {
- // 清理和格式化价格数据
- $prices = $this->input('prices', []);
- $cleanedPrices = [];
-
- foreach ($prices as $period => $price) {
- // 只保留有效的正数价格
- if ($price !== null && $price !== '' && is_numeric($price)) {
- $numericPrice = (float) $price;
- if ($numericPrice > 0) {
- // 转换为浮点数并保留两位小数
- $cleanedPrices[$period] = round($numericPrice, 2);
- }
- }
- }
-
- // 更新请求中的价格数据
- $this->merge(['prices' => $cleanedPrices]);
- }
-
- /**
- * Get custom error messages for validator errors.
- */
- public function messages(): array
- {
- return [
- 'name.required' => '套餐名称不能为空',
- 'name.max' => '套餐名称不能超过 255 个字符',
- 'transfer_enable.required' => '流量配额不能为空',
- 'transfer_enable.integer' => '流量配额必须是整数',
- 'transfer_enable.min' => '流量配额必须大于 0',
- 'prices.array' => '价格配置格式错误',
- 'prices.*.numeric' => '价格必须是数字',
- 'prices.*.min' => '价格不能为负数',
- 'group_id.integer' => '权限组ID必须是整数',
- 'speed_limit.integer' => '速度限制必须是整数',
- 'speed_limit.min' => '速度限制不能为负数',
- 'device_limit.integer' => '设备限制必须是整数',
- 'device_limit.min' => '设备限制不能为负数',
- 'capacity_limit.integer' => '容量限制必须是整数',
- 'capacity_limit.min' => '容量限制不能为负数',
- 'tags.array' => '标签格式必须是数组',
- ];
- }
-
- /**
- * Handle a failed validation attempt.
- */
- protected function failedValidation(Validator $validator): void
- {
- throw new HttpResponseException(
- response()->json([
- 'data' => false,
- 'message' => $validator->errors()->first(),
- 'errors' => $validator->errors()->toArray()
- ], 422)
- );
- }
-}
diff --git a/Xboard/app/Http/Requests/Admin/PlanSort.php b/Xboard/app/Http/Requests/Admin/PlanSort.php
deleted file mode 100644
index eb7987a..0000000
--- a/Xboard/app/Http/Requests/Admin/PlanSort.php
+++ /dev/null
@@ -1,28 +0,0 @@
- 'required|array'
- ];
- }
-
- public function messages()
- {
- return [
- 'plan_ids.required' => '订阅计划ID不能为空',
- 'plan_ids.array' => '订阅计划ID格式有误'
- ];
- }
-}
diff --git a/Xboard/app/Http/Requests/Admin/PlanUpdate.php b/Xboard/app/Http/Requests/Admin/PlanUpdate.php
deleted file mode 100644
index d9463e2..0000000
--- a/Xboard/app/Http/Requests/Admin/PlanUpdate.php
+++ /dev/null
@@ -1,29 +0,0 @@
- 'in:0,1',
- 'renew' => 'in:0,1'
- ];
- }
-
- public function messages()
- {
- return [
- 'show.in' => '销售状态格式不正确',
- 'renew.in' => '续费状态格式不正确'
- ];
- }
-}
diff --git a/Xboard/app/Http/Requests/Admin/ServerSave.php b/Xboard/app/Http/Requests/Admin/ServerSave.php
deleted file mode 100644
index bd1f6b2..0000000
--- a/Xboard/app/Http/Requests/Admin/ServerSave.php
+++ /dev/null
@@ -1,212 +0,0 @@
- 'nullable|boolean',
- 'utls.fingerprint' => 'nullable|string',
- ];
-
- private const MULTIPLEX_RULES = [
- 'multiplex.enabled' => 'nullable|boolean',
- 'multiplex.protocol' => 'nullable|string',
- 'multiplex.max_connections' => 'nullable|integer',
- 'multiplex.min_streams' => 'nullable|integer',
- 'multiplex.max_streams' => 'nullable|integer',
- 'multiplex.padding' => 'nullable|boolean',
- 'multiplex.brutal.enabled' => 'nullable|boolean',
- 'multiplex.brutal.up_mbps' => 'nullable|integer',
- 'multiplex.brutal.down_mbps' => 'nullable|integer',
- ];
-
- private const PROTOCOL_RULES = [
- 'shadowsocks' => [
- 'cipher' => 'required|string',
- 'obfs' => 'nullable|string',
- 'obfs_settings.path' => 'nullable|string',
- 'obfs_settings.host' => 'nullable|string',
- 'plugin' => 'nullable|string',
- 'plugin_opts' => 'nullable|string',
- ],
- 'vmess' => [
- 'tls' => 'required|integer',
- 'network' => 'required|string',
- 'network_settings' => 'nullable|array',
- 'tls_settings.server_name' => 'nullable|string',
- 'tls_settings.allow_insecure' => 'nullable|boolean',
- ],
- 'trojan' => [
- 'tls' => 'nullable|integer',
- 'network' => 'required|string',
- 'network_settings' => 'nullable|array',
- 'server_name' => 'nullable|string',
- 'allow_insecure' => 'nullable|boolean',
- 'reality_settings.allow_insecure' => 'nullable|boolean',
- 'reality_settings.server_name' => 'nullable|string',
- 'reality_settings.server_port' => 'nullable|integer',
- 'reality_settings.public_key' => 'nullable|string',
- 'reality_settings.private_key' => 'nullable|string',
- 'reality_settings.short_id' => 'nullable|string',
- ],
- 'hysteria' => [
- 'version' => 'required|integer',
- 'alpn' => 'nullable|string',
- 'obfs.open' => 'nullable|boolean',
- 'obfs.type' => 'string|nullable',
- 'obfs.password' => 'string|nullable',
- 'tls.server_name' => 'nullable|string',
- 'tls.allow_insecure' => 'nullable|boolean',
- 'bandwidth.up' => 'nullable|integer',
- 'bandwidth.down' => 'nullable|integer',
- 'hop_interval' => 'integer|nullable',
- ],
- 'vless' => [
- 'tls' => 'required|integer',
- 'network' => 'required|string',
- 'network_settings' => 'nullable|array',
- 'flow' => 'nullable|string',
- 'encryption' => 'nullable|array',
- 'encryption.enabled' => 'nullable|boolean',
- 'encryption.encryption' => 'nullable|string',
- 'encryption.decryption' => 'nullable|string',
- 'tls_settings.server_name' => 'nullable|string',
- 'tls_settings.allow_insecure' => 'nullable|boolean',
- 'reality_settings.allow_insecure' => 'nullable|boolean',
- 'reality_settings.server_name' => 'nullable|string',
- 'reality_settings.server_port' => 'nullable|integer',
- 'reality_settings.public_key' => 'nullable|string',
- 'reality_settings.private_key' => 'nullable|string',
- 'reality_settings.short_id' => 'nullable|string',
- ],
- 'socks' => [
- ],
- 'naive' => [
- 'tls' => 'required|integer',
- 'tls_settings' => 'nullable|array',
- ],
- 'http' => [
- 'tls' => 'required|integer',
- 'tls_settings' => 'nullable|array',
- ],
- 'mieru' => [
- 'transport' => 'required|string|in:TCP,UDP',
- 'traffic_pattern' => 'string'
- ],
- 'anytls' => [
- 'tls' => 'nullable|array',
- 'alpn' => 'nullable|string',
- 'padding_scheme' => 'nullable|array',
- ],
- ];
-
- private function getBaseRules(): array
- {
- return [
- 'type' => 'required|in:' . implode(',', Server::VALID_TYPES),
- 'spectific_key' => 'nullable|string',
- 'code' => 'nullable|string',
- 'show' => '',
- 'name' => 'required|string',
- 'group_ids' => 'nullable|array',
- 'route_ids' => 'nullable|array',
- 'parent_id' => 'nullable|integer',
- 'host' => 'required',
- 'port' => 'required',
- 'server_port' => 'required',
- 'tags' => 'nullable|array',
- 'excludes' => 'nullable|array',
- 'ips' => 'nullable|array',
- 'rate' => 'required|numeric',
- 'rate_time_enable' => 'nullable|boolean',
- 'rate_time_ranges' => 'nullable|array',
- 'custom_outbounds' => 'nullable|array',
- 'custom_routes' => 'nullable|array',
- 'cert_config' => 'nullable|array',
- 'rate_time_ranges.*.start' => 'required_with:rate_time_ranges|string|date_format:H:i',
- 'rate_time_ranges.*.end' => 'required_with:rate_time_ranges|string|date_format:H:i',
- 'rate_time_ranges.*.rate' => 'required_with:rate_time_ranges|numeric|min:0',
- 'protocol_settings' => 'array',
- 'transfer_enable' => 'nullable|integer|min:0',
- ];
- }
-
- public function rules(): array
- {
- $type = $this->input('type');
- $rules = $this->getBaseRules();
-
- $protocolRules = self::PROTOCOL_RULES[$type] ?? [];
- if (in_array($type, ['vmess', 'vless', 'trojan', 'mieru'])) {
- $protocolRules = array_merge($protocolRules, self::MULTIPLEX_RULES, self::UTLS_RULES);
- }
-
- foreach ($protocolRules as $field => $rule) {
- $rules['protocol_settings.' . $field] = $rule;
- }
-
- return $rules;
- }
-
- public function attributes(): array
- {
- return [
- 'protocol_settings.cipher' => '加密方式',
- 'protocol_settings.obfs' => '混淆类型',
- 'protocol_settings.network' => '传输协议',
- 'protocol_settings.port_range' => '端口范围',
- 'protocol_settings.traffic_pattern' => 'Traffic Pattern',
- 'protocol_settings.transport' => '传输方式',
- 'protocol_settings.version' => '协议版本',
- 'protocol_settings.password' => '密码',
- 'protocol_settings.handshake.server' => '握手服务器',
- 'protocol_settings.handshake.server_port' => '握手端口',
- 'protocol_settings.multiplex.enabled' => '多路复用',
- 'protocol_settings.multiplex.protocol' => '复用协议',
- 'protocol_settings.multiplex.max_connections' => '最大连接数',
- 'protocol_settings.multiplex.min_streams' => '最小流数',
- 'protocol_settings.multiplex.max_streams' => '最大流数',
- 'protocol_settings.multiplex.padding' => '复用填充',
- 'protocol_settings.multiplex.brutal.enabled' => 'Brutal加速',
- 'protocol_settings.multiplex.brutal.up_mbps' => 'Brutal上行速率',
- 'protocol_settings.multiplex.brutal.down_mbps' => 'Brutal下行速率',
- 'protocol_settings.utls.enabled' => 'uTLS',
- 'protocol_settings.utls.fingerprint' => 'uTLS指纹',
- ];
- }
-
- public function messages()
- {
- return [
- 'name.required' => '节点名称不能为空',
- 'group_ids.required' => '权限组不能为空',
- 'group_ids.array' => '权限组格式不正确',
- 'route_ids.array' => '路由组格式不正确',
- 'parent_id.integer' => '父ID格式不正确',
- 'host.required' => '节点地址不能为空',
- 'port.required' => '连接端口不能为空',
- 'server_port.required' => '后端服务端口不能为空',
- 'tls.required' => 'TLS不能为空',
- 'tags.array' => '标签格式不正确',
- 'rate.required' => '倍率不能为空',
- 'rate.numeric' => '倍率格式不正确',
- 'network.required' => '传输协议不能为空',
- 'network.in' => '传输协议格式不正确',
- 'networkSettings.array' => '传输协议配置有误',
- 'ruleSettings.array' => '规则配置有误',
- 'tlsSettings.array' => 'tls配置有误',
- 'dnsSettings.array' => 'dns配置有误',
- 'protocol_settings.*.required' => ':attribute 不能为空',
- 'protocol_settings.*.string' => ':attribute 必须是字符串',
- 'protocol_settings.*.integer' => ':attribute 必须是整数',
- 'protocol_settings.*.in' => ':attribute 的值不合法',
- 'transfer_enable.integer' => '流量上限必须是整数',
- 'transfer_enable.min' => '流量上限不能小于0',
- ];
- }
-}
diff --git a/Xboard/app/Http/Requests/Admin/UserFetch.php b/Xboard/app/Http/Requests/Admin/UserFetch.php
deleted file mode 100644
index 899c6a9..0000000
--- a/Xboard/app/Http/Requests/Admin/UserFetch.php
+++ /dev/null
@@ -1,33 +0,0 @@
- 'required|in:id,email,transfer_enable,d,expired_at,uuid,token,invite_by_email,invite_user_id,plan_id,banned,remarks,is_admin',
- 'filter.*.condition' => 'required|in:>,<,=,>=,<=,模糊,!=',
- 'filter.*.value' => 'required'
- ];
- }
-
- public function messages()
- {
- return [
- 'filter.*.key.required' => '过滤键不能为空',
- 'filter.*.key.in' => '过滤键参数有误',
- 'filter.*.condition.required' => '过滤条件不能为空',
- 'filter.*.condition.in' => '过滤条件参数有误',
- 'filter.*.value.required' => '过滤值不能为空'
- ];
- }
-}
diff --git a/Xboard/app/Http/Requests/Admin/UserGenerate.php b/Xboard/app/Http/Requests/Admin/UserGenerate.php
deleted file mode 100644
index 41b0722..0000000
--- a/Xboard/app/Http/Requests/Admin/UserGenerate.php
+++ /dev/null
@@ -1,33 +0,0 @@
- 'nullable|integer|max:500',
- 'expired_at' => 'nullable|integer',
- 'plan_id' => 'nullable|integer',
- 'email_prefix' => 'nullable',
- 'email_suffix' => 'required',
- 'password' => 'nullable'
- ];
- }
-
- public function messages()
- {
- return [
- 'generate_count.integer' => '生成数量必须为数字',
- 'generate_count.max' => '生成数量最大为500个'
- ];
- }
-}
diff --git a/Xboard/app/Http/Requests/Admin/UserSendMail.php b/Xboard/app/Http/Requests/Admin/UserSendMail.php
deleted file mode 100644
index f885c36..0000000
--- a/Xboard/app/Http/Requests/Admin/UserSendMail.php
+++ /dev/null
@@ -1,29 +0,0 @@
- 'required',
- 'content' => 'required',
- ];
- }
-
- public function messages()
- {
- return [
- 'subject.required' => '主题不能为空',
- 'content.required' => '发送内容不能为空'
- ];
- }
-}
diff --git a/Xboard/app/Http/Requests/Admin/UserUpdate.php b/Xboard/app/Http/Requests/Admin/UserUpdate.php
deleted file mode 100644
index afbf922..0000000
--- a/Xboard/app/Http/Requests/Admin/UserUpdate.php
+++ /dev/null
@@ -1,69 +0,0 @@
- 'required|integer',
- 'email' => 'email:strict',
- 'password' => 'nullable|min:8',
- 'transfer_enable' => 'numeric',
- 'expired_at' => 'nullable|integer',
- 'banned' => 'bool',
- 'plan_id' => 'nullable|integer',
- 'commission_rate' => 'nullable|integer|min:0|max:100',
- 'discount' => 'nullable|integer|min:0|max:100',
- 'is_admin' => 'boolean',
- 'is_staff' => 'boolean',
- 'u' => 'integer',
- 'd' => 'integer',
- 'balance' => 'numeric',
- 'commission_type' => 'integer',
- 'commission_balance' => 'numeric',
- 'remarks' => 'nullable',
- 'speed_limit' => 'nullable|integer',
- 'device_limit' => 'nullable|integer'
- ];
- }
-
- public function messages()
- {
- return [
- 'email.required' => '邮箱不能为空',
- 'email.email' => '邮箱格式不正确',
- 'transfer_enable.numeric' => '流量格式不正确',
- 'expired_at.integer' => '到期时间格式不正确',
- 'banned.in' => '是否封禁格式不正确',
- 'is_admin.required' => '是否管理员不能为空',
- 'is_admin.in' => '是否管理员格式不正确',
- 'is_staff.required' => '是否员工不能为空',
- 'is_staff.in' => '是否员工格式不正确',
- 'plan_id.integer' => '订阅计划格式不正确',
- 'commission_rate.integer' => '推荐返利比例格式不正确',
- 'commission_rate.nullable' => '推荐返利比例格式不正确',
- 'commission_rate.min' => '推荐返利比例最小为0',
- 'commission_rate.max' => '推荐返利比例最大为100',
- 'discount.integer' => '专属折扣比例格式不正确',
- 'discount.nullable' => '专属折扣比例格式不正确',
- 'discount.min' => '专属折扣比例最小为0',
- 'discount.max' => '专属折扣比例最大为100',
- 'u.integer' => '上行流量格式不正确',
- 'd.integer' => '下行流量格式不正确',
- 'balance.integer' => '余额格式不正确',
- 'commission_balance.integer' => '佣金格式不正确',
- 'password.min' => '密码长度最小8位',
- 'speed_limit.integer' => '限速格式不正确',
- 'device_limit.integer' => '设备数量格式不正确'
- ];
- }
-}
diff --git a/Xboard/app/Http/Requests/Passport/AuthForget.php b/Xboard/app/Http/Requests/Passport/AuthForget.php
deleted file mode 100644
index 8106f28..0000000
--- a/Xboard/app/Http/Requests/Passport/AuthForget.php
+++ /dev/null
@@ -1,33 +0,0 @@
- 'required|email:strict',
- 'password' => 'required|min:8',
- 'email_code' => 'required'
- ];
- }
-
- public function messages()
- {
- return [
- 'email.required' => __('Email can not be empty'),
- 'email.email' => __('Email format is incorrect'),
- 'password.required' => __('Password can not be empty'),
- 'password.min' => __('Password must be greater than 8 digits'),
- 'email_code.required' => __('Email verification code cannot be empty')
- ];
- }
-}
diff --git a/Xboard/app/Http/Requests/Passport/AuthLogin.php b/Xboard/app/Http/Requests/Passport/AuthLogin.php
deleted file mode 100644
index 6aa832c..0000000
--- a/Xboard/app/Http/Requests/Passport/AuthLogin.php
+++ /dev/null
@@ -1,31 +0,0 @@
- 'required|email:strict',
- 'password' => 'required|min:8'
- ];
- }
-
- public function messages()
- {
- return [
- 'email.required' => __('Email can not be empty'),
- 'email.email' => __('Email format is incorrect'),
- 'password.required' => __('Password can not be empty'),
- 'password.min' => __('Password must be greater than 8 digits')
- ];
- }
-}
diff --git a/Xboard/app/Http/Requests/Passport/AuthRegister.php b/Xboard/app/Http/Requests/Passport/AuthRegister.php
deleted file mode 100644
index 63e053a..0000000
--- a/Xboard/app/Http/Requests/Passport/AuthRegister.php
+++ /dev/null
@@ -1,31 +0,0 @@
- 'required|email:strict',
- 'password' => 'required|min:8'
- ];
- }
-
- public function messages()
- {
- return [
- 'email.required' => __('Email can not be empty'),
- 'email.email' => __('Email format is incorrect'),
- 'password.required' => __('Password can not be empty'),
- 'password.min' => __('Password must be greater than 8 digits')
- ];
- }
-}
diff --git a/Xboard/app/Http/Requests/Passport/CommSendEmailVerify.php b/Xboard/app/Http/Requests/Passport/CommSendEmailVerify.php
deleted file mode 100644
index ff5ecdd..0000000
--- a/Xboard/app/Http/Requests/Passport/CommSendEmailVerify.php
+++ /dev/null
@@ -1,28 +0,0 @@
- 'required|email:strict'
- ];
- }
-
- public function messages()
- {
- return [
- 'email.required' => __('Email can not be empty'),
- 'email.email' => __('Email format is incorrect')
- ];
- }
-}
diff --git a/Xboard/app/Http/Requests/Staff/UserUpdate.php b/Xboard/app/Http/Requests/Staff/UserUpdate.php
deleted file mode 100644
index be22144..0000000
--- a/Xboard/app/Http/Requests/Staff/UserUpdate.php
+++ /dev/null
@@ -1,56 +0,0 @@
- 'required|email:strict',
- 'password' => 'nullable',
- 'transfer_enable' => 'numeric',
- 'expired_at' => 'nullable|integer',
- 'banned' => 'required|in:0,1',
- 'plan_id' => 'nullable|integer',
- 'commission_rate' => 'nullable|integer|min:0|max:100',
- 'discount' => 'nullable|integer|min:0|max:100',
- 'u' => 'integer',
- 'd' => 'integer',
- 'balance' => 'integer',
- 'commission_balance' => 'integer'
- ];
- }
-
- public function messages()
- {
- return [
- 'email.required' => '邮箱不能为空',
- 'email.email' => '邮箱格式不正确',
- 'transfer_enable.numeric' => '流量格式不正确',
- 'expired_at.integer' => '到期时间格式不正确',
- 'banned.required' => '是否封禁不能为空',
- 'banned.in' => '是否封禁格式不正确',
- 'plan_id.integer' => '订阅计划格式不正确',
- 'commission_rate.integer' => '推荐返利比例格式不正确',
- 'commission_rate.nullable' => '推荐返利比例格式不正确',
- 'commission_rate.min' => '推荐返利比例最小为0',
- 'commission_rate.max' => '推荐返利比例最大为100',
- 'discount.integer' => '专属折扣比例格式不正确',
- 'discount.nullable' => '专属折扣比例格式不正确',
- 'discount.min' => '专属折扣比例最小为0',
- 'discount.max' => '专属折扣比例最大为100',
- 'u.integer' => '上行流量格式不正确',
- 'd.integer' => '下行流量格式不正确',
- 'balance.integer' => '余额格式不正确',
- 'commission_balance.integer' => '佣金格式不正确'
- ];
- }
-}
diff --git a/Xboard/app/Http/Requests/User/GiftCardCheckRequest.php b/Xboard/app/Http/Requests/User/GiftCardCheckRequest.php
deleted file mode 100644
index ed0b5e3..0000000
--- a/Xboard/app/Http/Requests/User/GiftCardCheckRequest.php
+++ /dev/null
@@ -1,28 +0,0 @@
-|string>
- */
- public function rules(): array
- {
- return [
- //
- ];
- }
-}
diff --git a/Xboard/app/Http/Requests/User/GiftCardRedeemRequest.php b/Xboard/app/Http/Requests/User/GiftCardRedeemRequest.php
deleted file mode 100644
index 7feb4b4..0000000
--- a/Xboard/app/Http/Requests/User/GiftCardRedeemRequest.php
+++ /dev/null
@@ -1,44 +0,0 @@
- 'required|string|min:8|max:32',
- ];
- }
-
- /**
- * Get custom messages for validator errors.
- *
- * @return array
- */
- public function messages()
- {
- return [
- 'code.required' => '请输入兑换码',
- 'code.min' => '兑换码长度不能少于8位',
- 'code.max' => '兑换码长度不能超过32位',
- ];
- }
-}
diff --git a/Xboard/app/Http/Requests/User/OrderSave.php b/Xboard/app/Http/Requests/User/OrderSave.php
deleted file mode 100644
index 449bcaa..0000000
--- a/Xboard/app/Http/Requests/User/OrderSave.php
+++ /dev/null
@@ -1,30 +0,0 @@
- 'required',
- 'period' => 'required|in:month_price,quarter_price,half_year_price,year_price,two_year_price,three_year_price,onetime_price,reset_price'
- ];
- }
-
- public function messages()
- {
- return [
- 'plan_id.required' => __('Plan ID cannot be empty'),
- 'period.required' => __('Plan period cannot be empty'),
- 'period.in' => __('Wrong plan period')
- ];
- }
-}
diff --git a/Xboard/app/Http/Requests/User/TicketSave.php b/Xboard/app/Http/Requests/User/TicketSave.php
deleted file mode 100644
index 412778f..0000000
--- a/Xboard/app/Http/Requests/User/TicketSave.php
+++ /dev/null
@@ -1,32 +0,0 @@
- 'required',
- 'level' => 'required|in:0,1,2',
- 'message' => 'required'
- ];
- }
-
- public function messages()
- {
- return [
- 'subject.required' => __('Ticket subject cannot be empty'),
- 'level.required' => __('Ticket level cannot be empty'),
- 'level.in' => __('Incorrect ticket level format'),
- 'message.required' => __('Message cannot be empty')
- ];
- }
-}
diff --git a/Xboard/app/Http/Requests/User/TicketWithdraw.php b/Xboard/app/Http/Requests/User/TicketWithdraw.php
deleted file mode 100644
index d0da905..0000000
--- a/Xboard/app/Http/Requests/User/TicketWithdraw.php
+++ /dev/null
@@ -1,29 +0,0 @@
- 'required',
- 'withdraw_account' => 'required'
- ];
- }
-
- public function messages()
- {
- return [
- 'withdraw_method.required' => __('The withdrawal method cannot be empty'),
- 'withdraw_account.required' => __('The withdrawal account cannot be empty')
- ];
- }
-}
diff --git a/Xboard/app/Http/Requests/User/UserChangePassword.php b/Xboard/app/Http/Requests/User/UserChangePassword.php
deleted file mode 100644
index 04e70c7..0000000
--- a/Xboard/app/Http/Requests/User/UserChangePassword.php
+++ /dev/null
@@ -1,30 +0,0 @@
- 'required',
- 'new_password' => 'required|min:8'
- ];
- }
-
- public function messages()
- {
- return [
- 'old_password.required' => __('Old password cannot be empty'),
- 'new_password.required' => __('New password cannot be empty'),
- 'new_password.min' => __('Password must be greater than 8 digits')
- ];
- }
-}
diff --git a/Xboard/app/Http/Requests/User/UserTransfer.php b/Xboard/app/Http/Requests/User/UserTransfer.php
deleted file mode 100644
index 478c825..0000000
--- a/Xboard/app/Http/Requests/User/UserTransfer.php
+++ /dev/null
@@ -1,29 +0,0 @@
- 'required|integer|min:1'
- ];
- }
-
- public function messages()
- {
- return [
- 'transfer_amount.required' => __('The transfer amount cannot be empty'),
- 'transfer_amount.integer' => __('The transfer amount parameter is wrong'),
- 'transfer_amount.min' => __('The transfer amount parameter is wrong')
- ];
- }
-}
diff --git a/Xboard/app/Http/Requests/User/UserUpdate.php b/Xboard/app/Http/Requests/User/UserUpdate.php
deleted file mode 100644
index 5ba6604..0000000
--- a/Xboard/app/Http/Requests/User/UserUpdate.php
+++ /dev/null
@@ -1,29 +0,0 @@
- 'in:0,1',
- 'remind_traffic' => 'in:0,1'
- ];
- }
-
- public function messages()
- {
- return [
- 'show.in' => __('Incorrect format of expiration reminder'),
- 'renew.in' => __('Incorrect traffic alert format')
- ];
- }
-}
diff --git a/Xboard/app/Http/Resources/ComissionLogResource.php b/Xboard/app/Http/Resources/ComissionLogResource.php
deleted file mode 100644
index 8d86769..0000000
--- a/Xboard/app/Http/Resources/ComissionLogResource.php
+++ /dev/null
@@ -1,25 +0,0 @@
-
- */
- public function toArray(Request $request): array
- {
- return [
- "id"=> $this['id'],
- "order_amount" => $this['order_amount'],
- "trade_no" => $this['trade_no'],
- "get_amount" => $this['get_amount'],
- "created_at" => $this['created_at']
- ];
- }
-}
diff --git a/Xboard/app/Http/Resources/CouponResource.php b/Xboard/app/Http/Resources/CouponResource.php
deleted file mode 100644
index 049a61f..0000000
--- a/Xboard/app/Http/Resources/CouponResource.php
+++ /dev/null
@@ -1,38 +0,0 @@
- 转换后的数组
- */
- public function toArray(Request $request): array
- {
- return [
- ...$this->resource->toArray(),
- 'limit_plan_ids' => empty($this->limit_plan_ids) ? null : collect($this->limit_plan_ids)
- ->map(fn(mixed $id): string => (string) $id)
- ->values()
- ->all(),
- 'limit_period' => empty($this->limit_period) ? null : collect($this->limit_period)
- ->map(fn(mixed $period): string => (string) PlanService::convertToLegacyPeriod($period))
- ->values()
- ->all(),
- ];
- }
-}
diff --git a/Xboard/app/Http/Resources/InviteCodeResource.php b/Xboard/app/Http/Resources/InviteCodeResource.php
deleted file mode 100644
index 927c782..0000000
--- a/Xboard/app/Http/Resources/InviteCodeResource.php
+++ /dev/null
@@ -1,28 +0,0 @@
-
- */
- public function toArray(Request $request): array
- {
- $data = [
- "user_id" => $this['user_id'],
- "code" => $this['code'],
- "pv" => $this['pv'],
- "status" => $this['status'],
- "created_at" => $this['created_at'],
- "updated_at" => $this['updated_at']
- ];
- if(!config('hidden_features.enable_exposed_user_count_fix')) $data['user_id']= $this['user_id'];
- return $data;
- }
-}
diff --git a/Xboard/app/Http/Resources/KnowledgeResource.php b/Xboard/app/Http/Resources/KnowledgeResource.php
deleted file mode 100644
index cda796c..0000000
--- a/Xboard/app/Http/Resources/KnowledgeResource.php
+++ /dev/null
@@ -1,28 +0,0 @@
-
- */
- public function toArray(Request $request): array
- {
- $data = [
- 'id' => $this['id'],
- 'category' => $this['category'],
- 'title' => $this['title'],
- 'body' => $this->when(isset($this['body']), $this['body']),
- 'updated_at' => $this['updated_at'],
- ];
-
- return HookManager::filter('user.knowledge.resource', $data, $request, $this);
- }
-}
diff --git a/Xboard/app/Http/Resources/MessageResource.php b/Xboard/app/Http/Resources/MessageResource.php
deleted file mode 100644
index 9a72e1c..0000000
--- a/Xboard/app/Http/Resources/MessageResource.php
+++ /dev/null
@@ -1,26 +0,0 @@
-
- */
- public function toArray(Request $request): array
- {
- return [
- "id" => $this['id'],
- "ticket_id" => $this['ticket_id'],
- "is_me" => $this['is_from_user'],
- "message" => $this["message"],
- "created_at" => $this['created_at'],
- "updated_at" => $this['updated_at']
- ];
- }
-}
diff --git a/Xboard/app/Http/Resources/NodeResource.php b/Xboard/app/Http/Resources/NodeResource.php
deleted file mode 100644
index d7f90af..0000000
--- a/Xboard/app/Http/Resources/NodeResource.php
+++ /dev/null
@@ -1,29 +0,0 @@
-
- */
- public function toArray(Request $request): array
- {
- return [
- 'id' => $this['id'],
- 'type' => $this['type'],
- 'version' => $this['version'] ?? null,
- 'name' => $this['name'],
- 'rate' => $this['rate'],
- 'tags' => $this['tags'],
- 'is_online' => $this['is_online'],
- 'cache_key' => $this['cache_key'],
- 'last_check_at' => $this['last_check_at']
- ];
- }
-}
diff --git a/Xboard/app/Http/Resources/OrderResource.php b/Xboard/app/Http/Resources/OrderResource.php
deleted file mode 100644
index ae3e6e4..0000000
--- a/Xboard/app/Http/Resources/OrderResource.php
+++ /dev/null
@@ -1,28 +0,0 @@
-
- */
- public function toArray(Request $request): array
- {
- return [
- ...parent::toArray($request),
- 'period' => PlanService::getLegacyPeriod((string)$this->period),
- 'plan' => $this->whenLoaded('plan', fn() => PlanResource::make($this->plan)),
- ];
- }
-}
diff --git a/Xboard/app/Http/Resources/PlanResource.php b/Xboard/app/Http/Resources/PlanResource.php
deleted file mode 100644
index b78583a..0000000
--- a/Xboard/app/Http/Resources/PlanResource.php
+++ /dev/null
@@ -1,122 +0,0 @@
-
- */
- public function toArray(Request $request): array
- {
- return [
- 'id' => $this->resource['id'],
- 'group_id' => $this->resource['group_id'],
- 'name' => $this->resource['name'],
- 'tags' => $this->resource['tags'],
- 'content' => $this->formatContent(),
- ...$this->getPeriodPrices(),
- 'capacity_limit' => $this->getFormattedCapacityLimit(),
- 'transfer_enable' => $this->resource['transfer_enable'],
- 'speed_limit' => $this->resource['speed_limit'],
- 'device_limit' => $this->resource['device_limit'],
- 'show' => (bool) $this->resource['show'],
- 'sell' => (bool) $this->resource['sell'],
- 'renew' => (bool) $this->resource['renew'],
- 'reset_traffic_method' => $this->resource['reset_traffic_method'],
- 'sort' => $this->resource['sort'],
- 'created_at' => $this->resource['created_at'],
- 'updated_at' => $this->resource['updated_at']
- ];
- }
-
- /**
- * Get transformed period prices using Plan mapping
- *
- * @return array
- */
- protected function getPeriodPrices(): array
- {
- return collect(Plan::LEGACY_PERIOD_MAPPING)
- ->mapWithKeys(function (string $newPeriod, string $legacyPeriod): array {
- $price = $this->resource['prices'][$newPeriod] ?? null;
- return [
- $legacyPeriod => $price !== null
- ? (float) $price * self::PRICE_MULTIPLIER
- : null
- ];
- })
- ->all();
- }
-
- /**
- * Get formatted capacity limit value
- *
- * @return int|string|null
- */
- protected function getFormattedCapacityLimit(): int|string|null
- {
- $limit = $this->resource['capacity_limit'];
-
- return match (true) {
- $limit === null => null,
- $limit <= 0 => __('Sold out'),
- default => (int) $limit,
- };
- }
-
- /**
- * Format content with template variables
- *
- * @return string
- */
- protected function formatContent(): string
- {
- $content = $this->resource['content'] ?? '';
-
- $replacements = [
- '{{transfer}}' => $this->resource['transfer_enable'],
- '{{speed}}' => $this->resource['speed_limit'] === NULL ? __('No Limit') : $this->resource['speed_limit'],
- '{{devices}}' => $this->resource['device_limit'] === NULL ? __('No Limit') : $this->resource['device_limit'],
- '{{reset_method}}' => $this->getResetMethodText(),
- ];
-
- return str_replace(
- array_keys($replacements),
- array_values($replacements),
- $content
- );
- }
-
- /**
- * Get reset method text
- *
- * @return string
- */
- protected function getResetMethodText(): string
- {
- $method = $this->resource['reset_traffic_method'];
-
- if ($method === Plan::RESET_TRAFFIC_FOLLOW_SYSTEM) {
- $method = admin_setting('reset_traffic_method', Plan::RESET_TRAFFIC_MONTHLY);
- }
- return match ($method) {
- Plan::RESET_TRAFFIC_FIRST_DAY_MONTH => __('First Day of Month'),
- Plan::RESET_TRAFFIC_MONTHLY => __('Monthly'),
- Plan::RESET_TRAFFIC_NEVER => __('Never'),
- Plan::RESET_TRAFFIC_FIRST_DAY_YEAR => __('First Day of Year'),
- Plan::RESET_TRAFFIC_YEARLY => __('Yearly'),
- default => __('Monthly')
- };
- }
-}
\ No newline at end of file
diff --git a/Xboard/app/Http/Resources/TicketResource.php b/Xboard/app/Http/Resources/TicketResource.php
deleted file mode 100644
index 9b9a773..0000000
--- a/Xboard/app/Http/Resources/TicketResource.php
+++ /dev/null
@@ -1,31 +0,0 @@
-
- */
- public function toArray(Request $request): array
- {
- $data = [
- "id" => $this['id'],
- "level" => $this['level'],
- "reply_status" => $this['reply_status'],
- "status" => $this['status'],
- "subject" => $this['subject'],
- "message" => array_key_exists('message',$this->additional) ? MessageResource::collection($this['message']) : null,
- "created_at" => $this['created_at'],
- "updated_at" => $this['updated_at']
- ];
- if(!config('hidden_features.enable_exposed_user_count_fix')) $data['user_id']= $this['user_id'];
- return $data;
-
- }
-}
diff --git a/Xboard/app/Http/Resources/TrafficLogResource.php b/Xboard/app/Http/Resources/TrafficLogResource.php
deleted file mode 100644
index 798ea7e..0000000
--- a/Xboard/app/Http/Resources/TrafficLogResource.php
+++ /dev/null
@@ -1,26 +0,0 @@
-
- */
- public function toArray(Request $request): array
- {
- $data = [
- "d" => $this['d'],
- "u" => $this['u'],
- "record_at" => $this['record_at'],
- "server_rate" => $this['server_rate'],
- ];
- if(!config('hidden_features.enable_exposed_user_count_fix')) $data['user_id']= $this['user_id'];
- return $data;
- }
-}
diff --git a/Xboard/app/Http/Routes/V1/ClientRoute.php b/Xboard/app/Http/Routes/V1/ClientRoute.php
deleted file mode 100644
index ad13989..0000000
--- a/Xboard/app/Http/Routes/V1/ClientRoute.php
+++ /dev/null
@@ -1,23 +0,0 @@
-group([
- 'prefix' => 'client',
- 'middleware' => 'client'
- ], function ($router) {
- // Client
- $router->get('/subscribe', [ClientController::class, 'subscribe'])->name('client.subscribe.legacy');
- // App
- $router->get('/app/getConfig', [AppController::class, 'getConfig']);
- $router->get('/app/getVersion', [AppController::class, 'getVersion']);
- });
- }
-}
diff --git a/Xboard/app/Http/Routes/V1/GuestRoute.php b/Xboard/app/Http/Routes/V1/GuestRoute.php
deleted file mode 100644
index 3c4f571..0000000
--- a/Xboard/app/Http/Routes/V1/GuestRoute.php
+++ /dev/null
@@ -1,27 +0,0 @@
-group([
- 'prefix' => 'guest'
- ], function ($router) {
- // Plan
- $router->get('/plan/fetch', [PlanController::class, 'fetch']);
- // Telegram
- $router->post('/telegram/webhook', [TelegramController::class, 'webhook']);
- // Payment
- $router->match(['get', 'post'], '/payment/notify/{method}/{uuid}', [PaymentController::class, 'notify']);
- // Comm
- $router->get('/comm/config', [CommController::class, 'config']);
- });
- }
-}
diff --git a/Xboard/app/Http/Routes/V1/PassportRoute.php b/Xboard/app/Http/Routes/V1/PassportRoute.php
deleted file mode 100644
index 3134b96..0000000
--- a/Xboard/app/Http/Routes/V1/PassportRoute.php
+++ /dev/null
@@ -1,27 +0,0 @@
-group([
- 'prefix' => 'passport'
- ], function ($router) {
- // Auth
- $router->post('/auth/register', [AuthController::class, 'register']);
- $router->post('/auth/login', [AuthController::class, 'login']);
- $router->get('/auth/token2Login', [AuthController::class, 'token2Login']);
- $router->post('/auth/forget', [AuthController::class, 'forget']);
- $router->post('/auth/getQuickLoginUrl', [AuthController::class, 'getQuickLoginUrl']);
- $router->post('/auth/loginWithMailLink', [AuthController::class, 'loginWithMailLink']);
- // Comm
- $router->post('/comm/sendEmailVerify', [CommController::class, 'sendEmailVerify']);
- $router->post('/comm/pv', [CommController::class, 'pv']);
- });
- }
-}
diff --git a/Xboard/app/Http/Routes/V1/ServerRoute.php b/Xboard/app/Http/Routes/V1/ServerRoute.php
deleted file mode 100644
index 42f7b22..0000000
--- a/Xboard/app/Http/Routes/V1/ServerRoute.php
+++ /dev/null
@@ -1,45 +0,0 @@
-group([
- 'prefix' => 'server',
- ], function ($router) {
- $router->group([
- 'prefix' => 'UniProxy',
- 'middleware' => 'server'
- ], function ($route) {
- $route->get('config', [UniProxyController::class, 'config']);
- $route->get('user', [UniProxyController::class, 'user']);
- $route->post('push', [UniProxyController::class, 'push']);
- $route->post('alive', [UniProxyController::class, 'alive']);
- $route->get('alivelist', [UniProxyController::class, 'alivelist']);
- $route->post('status', [UniProxyController::class, 'status']);
- });
- $router->group([
- 'prefix' => 'ShadowsocksTidalab',
- 'middleware' => 'server:shadowsocks'
- ], function ($route) {
- $route->get('user', [ShadowsocksTidalabController::class, 'user']);
- $route->post('submit', [ShadowsocksTidalabController::class, 'submit']);
- });
- $router->group([
- 'prefix' => 'TrojanTidalab',
- 'middleware' => 'server:trojan'
- ], function ($route) {
- $route->get('config', [TrojanTidalabController::class, 'config']);
- $route->get('user', [TrojanTidalabController::class, 'user']);
- $route->post('submit', [TrojanTidalabController::class, 'submit']);
- });
- });
- }
-}
diff --git a/Xboard/app/Http/Routes/V1/UserRoute.php b/Xboard/app/Http/Routes/V1/UserRoute.php
deleted file mode 100644
index 92c3605..0000000
--- a/Xboard/app/Http/Routes/V1/UserRoute.php
+++ /dev/null
@@ -1,83 +0,0 @@
-group([
- 'prefix' => 'user',
- 'middleware' => 'user'
- ], function ($router) {
- // User
- $router->get('/resetSecurity', [UserController::class, 'resetSecurity']);
- $router->get('/info', [UserController::class, 'info']);
- $router->post('/changePassword', [UserController::class, 'changePassword']);
- $router->post('/update', [UserController::class, 'update']);
- $router->get('/getSubscribe', [UserController::class, 'getSubscribe']);
- $router->get('/getStat', [UserController::class, 'getStat']);
- $router->get('/checkLogin', [UserController::class, 'checkLogin']);
- $router->post('/transfer', [UserController::class, 'transfer']);
- $router->post('/getQuickLoginUrl', [UserController::class, 'getQuickLoginUrl']);
- $router->get('/getActiveSession', [UserController::class, 'getActiveSession']);
- $router->post('/removeActiveSession', [UserController::class, 'removeActiveSession']);
- // Order
- $router->post('/order/save', [OrderController::class, 'save']);
- $router->post('/order/checkout', [OrderController::class, 'checkout']);
- $router->get('/order/check', [OrderController::class, 'check']);
- $router->get('/order/detail', [OrderController::class, 'detail']);
- $router->get('/order/fetch', [OrderController::class, 'fetch']);
- $router->get('/order/getPaymentMethod', [OrderController::class, 'getPaymentMethod']);
- $router->post('/order/cancel', [OrderController::class, 'cancel']);
- // Plan
- $router->get('/plan/fetch', [PlanController::class, 'fetch']);
- // Invite
- $router->get('/invite/save', [InviteController::class, 'save']);
- $router->get('/invite/fetch', [InviteController::class, 'fetch']);
- $router->get('/invite/details', [InviteController::class, 'details']);
- // Notice
- $router->get('/notice/fetch', [NoticeController::class, 'fetch']);
- // Ticket
- $router->post('/ticket/reply', [TicketController::class, 'reply']);
- $router->post('/ticket/close', [TicketController::class, 'close']);
- $router->post('/ticket/save', [TicketController::class, 'save']);
- $router->get('/ticket/fetch', [TicketController::class, 'fetch']);
- $router->post('/ticket/withdraw', [TicketController::class, 'withdraw']);
- // Server
- $router->get('/server/fetch', [ServerController::class, 'fetch']);
- // Coupon
- $router->post('/coupon/check', [CouponController::class, 'check']);
- // Gift Card
- $router->post('/gift-card/check', [GiftCardController::class, 'check']);
- $router->post('/gift-card/redeem', [GiftCardController::class, 'redeem']);
- $router->get('/gift-card/history', [GiftCardController::class, 'history']);
- $router->get('/gift-card/detail', [GiftCardController::class, 'detail']);
- $router->get('/gift-card/types', [GiftCardController::class, 'types']);
- // Telegram
- $router->get('/telegram/getBotInfo', [TelegramController::class, 'getBotInfo']);
- // Comm
- $router->get('/comm/config', [CommController::class, 'config']);
- $router->Post('/comm/getStripePublicKey', [CommController::class, 'getStripePublicKey']);
- // Knowledge
- $router->get('/knowledge/fetch', [KnowledgeController::class, 'fetch']);
- $router->get('/knowledge/getCategory', [KnowledgeController::class, 'getCategory']);
- // Stat
- $router->get('/stat/getTrafficLog', [StatController::class, 'getTrafficLog']);
- });
- }
-}
diff --git a/Xboard/app/Http/Routes/V2/AdminRoute.php b/Xboard/app/Http/Routes/V2/AdminRoute.php
deleted file mode 100644
index 3d8c910..0000000
--- a/Xboard/app/Http/Routes/V2/AdminRoute.php
+++ /dev/null
@@ -1,276 +0,0 @@
-group([
- 'prefix' => admin_setting('secure_path', admin_setting('frontend_admin_path', hash('crc32b', config('app.key')))),
- 'middleware' => ['admin', 'log'],
- ], function ($router) {
- // Config
- $router->group([
- 'prefix' => 'config'
- ], function ($router) {
- $router->get('/fetch', [ConfigController::class, 'fetch']);
- $router->post('/save', [ConfigController::class, 'save']);
- $router->get('/getEmailTemplate', [ConfigController::class, 'getEmailTemplate']);
- $router->get('/getThemeTemplate', [ConfigController::class, 'getThemeTemplate']);
- $router->post('/setTelegramWebhook', [ConfigController::class, 'setTelegramWebhook']);
- $router->post('/testSendMail', [ConfigController::class, 'testSendMail']);
- });
-
- // Plan
- $router->group([
- 'prefix' => 'plan'
- ], function ($router) {
- $router->get('/fetch', [PlanController::class, 'fetch']);
- $router->post('/save', [PlanController::class, 'save']);
- $router->post('/drop', [PlanController::class, 'drop']);
- $router->post('/update', [PlanController::class, 'update']);
- $router->post('/sort', [PlanController::class, 'sort']);
- });
-
- // Server
- $router->group([
- 'prefix' => 'server/group'
- ], function ($router) {
- $router->get('/fetch', [GroupController::class, 'fetch']);
- $router->post('/save', [GroupController::class, 'save']);
- $router->post('/drop', [GroupController::class, 'drop']);
- });
- $router->group([
- 'prefix' => 'server/route'
- ], function ($router) {
- $router->get('/fetch', [RouteController::class, 'fetch']);
- $router->post('/save', [RouteController::class, 'save']);
- $router->post('/drop', [RouteController::class, 'drop']);
- });
- $router->group([
- 'prefix' => 'server/manage'
- ], function ($router) {
- $router->get('/getNodes', [ManageController::class, 'getNodes']);
- $router->post('/sort', [ManageController::class, 'sort']);
- });
-
- // 节点更新接口
- $router->group([
- 'prefix' => 'server/manage'
- ], function ($router) {
- $router->post('/update', [ManageController::class, 'update']);
- $router->post('/save', [ManageController::class, 'save']);
- $router->post('/drop', [ManageController::class, 'drop']);
- $router->post('/copy', [ManageController::class, 'copy']);
- $router->post('/sort', [ManageController::class, 'sort']);
- $router->post('/batchDelete', [ManageController::class, 'batchDelete']);
- $router->post('/resetTraffic', [ManageController::class, 'resetTraffic']);
- $router->post('/batchResetTraffic', [ManageController::class, 'batchResetTraffic']);
- });
-
- // Order
- $router->group([
- 'prefix' => 'order'
- ], function ($router) {
- $router->any('/fetch', [OrderController::class, 'fetch']);
- $router->post('/update', [OrderController::class, 'update']);
- $router->post('/assign', [OrderController::class, 'assign']);
- $router->post('/paid', [OrderController::class, 'paid']);
- $router->post('/cancel', [OrderController::class, 'cancel']);
- $router->post('/detail', [OrderController::class, 'detail']);
- });
-
- // User
- $router->group([
- 'prefix' => 'user'
- ], function ($router) {
- $router->any('/fetch', [UserController::class, 'fetch']);
- $router->post('/update', [UserController::class, 'update']);
- $router->get('/getUserInfoById', [UserController::class, 'getUserInfoById']);
- $router->post('/generate', [UserController::class, 'generate']);
- $router->post('/dumpCSV', [UserController::class, 'dumpCSV']);
- $router->post('/sendMail', [UserController::class, 'sendMail']);
- $router->post('/ban', [UserController::class, 'ban']);
- $router->post('/resetSecret', [UserController::class, 'resetSecret']);
- $router->post('/setInviteUser', [UserController::class, 'setInviteUser']);
- $router->post('/destroy', [UserController::class, 'destroy']);
- });
-
- // Stat
- $router->group([
- 'prefix' => 'stat'
- ], function ($router) {
- $router->get('/getOverride', [StatController::class, 'getOverride']);
- $router->get('/getStats', [StatController::class, 'getStats']);
- $router->get('/getServerLastRank', [StatController::class, 'getServerLastRank']);
- $router->get('/getServerYesterdayRank', [StatController::class, 'getServerYesterdayRank']);
- $router->get('/getOrder', [StatController::class, 'getOrder']);
- $router->any('/getStatUser', [StatController::class, 'getStatUser']);
- $router->get('/getRanking', [StatController::class, 'getRanking']);
- $router->get('/getStatRecord', [StatController::class, 'getStatRecord']);
- $router->get('/getTrafficRank', [StatController::class, 'getTrafficRank']);
- });
-
- // Notice
- $router->group([
- 'prefix' => 'notice'
- ], function ($router) {
- $router->get('/fetch', [NoticeController::class, 'fetch']);
- $router->post('/save', [NoticeController::class, 'save']);
- $router->post('/update', [NoticeController::class, 'update']);
- $router->post('/drop', [NoticeController::class, 'drop']);
- $router->post('/show', [NoticeController::class, 'show']);
- $router->post('/sort', [NoticeController::class, 'sort']);
- });
-
- // Ticket
- $router->group([
- 'prefix' => 'ticket'
- ], function ($router) {
- $router->any('/fetch', [TicketController::class, 'fetch']);
- $router->post('/reply', [TicketController::class, 'reply']);
- $router->post('/close', [TicketController::class, 'close']);
- });
-
- // Coupon
- $router->group([
- 'prefix' => 'coupon'
- ], function ($router) {
- $router->any('/fetch', [CouponController::class, 'fetch']);
- $router->post('/generate', [CouponController::class, 'generate']);
- $router->post('/drop', [CouponController::class, 'drop']);
- $router->post('/show', [CouponController::class, 'show']);
- $router->post('/update', [CouponController::class, 'update']);
- });
-
- // Gift Card
- $router->group([
- 'prefix' => 'gift-card'
- ], function ($router) {
- // Template management
- $router->any('/templates', [GiftCardController::class, 'templates']);
- $router->post('/create-template', [GiftCardController::class, 'createTemplate']);
- $router->post('/update-template', [GiftCardController::class, 'updateTemplate']);
- $router->post('/delete-template', [GiftCardController::class, 'deleteTemplate']);
-
- // Code management
- $router->post('/generate-codes', [GiftCardController::class, 'generateCodes']);
- $router->any('/codes', [GiftCardController::class, 'codes']);
- $router->post('/toggle-code', [GiftCardController::class, 'toggleCode']);
- $router->get('/export-codes', [GiftCardController::class, 'exportCodes']);
- $router->post('/update-code', [GiftCardController::class, 'updateCode']);
- $router->post('/delete-code', [GiftCardController::class, 'deleteCode']);
-
- // Usage records
- $router->any('/usages', [GiftCardController::class, 'usages']);
-
- // Statistics
- $router->any('/statistics', [GiftCardController::class, 'statistics']);
- $router->get('/types', [GiftCardController::class, 'types']);
- });
-
- // Knowledge
- $router->group([
- 'prefix' => 'knowledge'
- ], function ($router) {
- $router->get('/fetch', [KnowledgeController::class, 'fetch']);
- $router->get('/getCategory', [KnowledgeController::class, 'getCategory']);
- $router->post('/save', [KnowledgeController::class, 'save']);
- $router->post('/show', [KnowledgeController::class, 'show']);
- $router->post('/drop', [KnowledgeController::class, 'drop']);
- $router->post('/sort', [KnowledgeController::class, 'sort']);
- });
-
- // Payment
- $router->group([
- 'prefix' => 'payment'
- ], function ($router) {
- $router->get('/fetch', [PaymentController::class, 'fetch']);
- $router->get('/getPaymentMethods', [PaymentController::class, 'getPaymentMethods']);
- $router->post('/getPaymentForm', [PaymentController::class, 'getPaymentForm']);
- $router->post('/save', [PaymentController::class, 'save']);
- $router->post('/drop', [PaymentController::class, 'drop']);
- $router->post('/show', [PaymentController::class, 'show']);
- $router->post('/sort', [PaymentController::class, 'sort']);
- });
-
- // System
- $router->group([
- 'prefix' => 'system'
- ], function ($router) {
- $router->get('/getSystemStatus', [SystemController::class, 'getSystemStatus']);
- $router->get('/getQueueStats', [SystemController::class, 'getQueueStats']);
- $router->get('/getQueueWorkload', [SystemController::class, 'getQueueWorkload']);
- $router->get('/getQueueMasters', '\\Laravel\\Horizon\\Http\\Controllers\\MasterSupervisorController@index');
- $router->get('/getHorizonFailedJobs', [SystemController::class, 'getHorizonFailedJobs']);
- $router->any('/getAuditLog', [SystemController::class, 'getAuditLog']);
- });
-
- // Update
- // $router->group([
- // 'prefix' => 'update'
- // ], function ($router) {
- // $router->get('/check', [UpdateController::class, 'checkUpdate']);
- // $router->post('/execute', [UpdateController::class, 'executeUpdate']);
- // });
-
- // Theme
- $router->group([
- 'prefix' => 'theme'
- ], function ($router) {
- $router->get('/getThemes', [ThemeController::class, 'getThemes']);
- $router->post('/upload', [ThemeController::class, 'upload']);
- $router->post('/delete', [ThemeController::class, 'delete']);
- $router->post('/saveThemeConfig', [ThemeController::class, 'saveThemeConfig']);
- $router->post('/getThemeConfig', [ThemeController::class, 'getThemeConfig']);
- });
-
- // Plugin
- $router->group([
- 'prefix' => 'plugin'
- ], function ($router) {
- $router->get('/types', [\App\Http\Controllers\V2\Admin\PluginController::class, 'types']);
- $router->get('/getPlugins', [\App\Http\Controllers\V2\Admin\PluginController::class, 'index']);
- $router->post('/upload', [\App\Http\Controllers\V2\Admin\PluginController::class, 'upload']);
- $router->post('/delete', [\App\Http\Controllers\V2\Admin\PluginController::class, 'delete']);
- $router->post('install', [\App\Http\Controllers\V2\Admin\PluginController::class, 'install']);
- $router->post('uninstall', [\App\Http\Controllers\V2\Admin\PluginController::class, 'uninstall']);
- $router->post('enable', [\App\Http\Controllers\V2\Admin\PluginController::class, 'enable']);
- $router->post('disable', [\App\Http\Controllers\V2\Admin\PluginController::class, 'disable']);
- $router->get('config', [\App\Http\Controllers\V2\Admin\PluginController::class, 'getConfig']);
- $router->post('config', [\App\Http\Controllers\V2\Admin\PluginController::class, 'updateConfig']);
- $router->post('upgrade', [\App\Http\Controllers\V2\Admin\PluginController::class, 'upgrade']);
- });
-
- // 流量重置管理
- $router->group([
- 'prefix' => 'traffic-reset'
- ], function ($router) {
- $router->get('logs', [TrafficResetController::class, 'logs']);
- $router->get('stats', [TrafficResetController::class, 'stats']);
- $router->get('user/{userId}/history', [TrafficResetController::class, 'userHistory']);
- $router->post('reset-user', [TrafficResetController::class, 'resetUser']);
- });
- });
-
- }
-}
diff --git a/Xboard/app/Http/Routes/V2/ClientRoute.php b/Xboard/app/Http/Routes/V2/ClientRoute.php
deleted file mode 100644
index 693a40d..0000000
--- a/Xboard/app/Http/Routes/V2/ClientRoute.php
+++ /dev/null
@@ -1,20 +0,0 @@
-group([
- 'prefix' => 'client',
- 'middleware' => 'client'
- ], function ($router) {
- // App
- $router->get('/app/getConfig', [AppController::class, 'getConfig']);
- $router->get('/app/getVersion', [AppController::class, 'getVersion']);
- });
- }
-}
diff --git a/Xboard/app/Http/Routes/V2/PassportRoute.php b/Xboard/app/Http/Routes/V2/PassportRoute.php
deleted file mode 100644
index ed91d81..0000000
--- a/Xboard/app/Http/Routes/V2/PassportRoute.php
+++ /dev/null
@@ -1,27 +0,0 @@
-group([
- 'prefix' => 'passport'
- ], function ($router) {
- // Auth
- $router->post('/auth/register', [AuthController::class, 'register']);
- $router->post('/auth/login', [AuthController::class, 'login']);
- $router->get ('/auth/token2Login', [AuthController::class, 'token2Login']);
- $router->post('/auth/forget', [AuthController::class, 'forget']);
- $router->post('/auth/getQuickLoginUrl', [AuthController::class, 'getQuickLoginUrl']);
- $router->post('/auth/loginWithMailLink', [AuthController::class, 'loginWithMailLink']);
- // Comm
- $router->post('/comm/sendEmailVerify', [CommController::class, 'sendEmailVerify']);
- $router->post('/comm/pv', [CommController::class, 'pv']);
- });
- }
-}
diff --git a/Xboard/app/Http/Routes/V2/ServerRoute.php b/Xboard/app/Http/Routes/V2/ServerRoute.php
deleted file mode 100644
index 9742d31..0000000
--- a/Xboard/app/Http/Routes/V2/ServerRoute.php
+++ /dev/null
@@ -1,29 +0,0 @@
-group([
- 'prefix' => 'server',
- 'middleware' => 'server'
- ], function ($route) {
- $route->post('handshake', [ServerController::class, 'handshake']);
- $route->post('report', [ServerController::class, 'report']);
- $route->get('config', [UniProxyController::class, 'config']);
- $route->get('user', [UniProxyController::class, 'user']);
- $route->post('push', [UniProxyController::class, 'push']);
- $route->post('alive', [UniProxyController::class, 'alive']);
- $route->get('alivelist', [UniProxyController::class, 'alivelist']);
- $route->post('status', [UniProxyController::class, 'status']);
- });
- }
-}
diff --git a/Xboard/app/Http/Routes/V2/UserRoute.php b/Xboard/app/Http/Routes/V2/UserRoute.php
deleted file mode 100644
index 38bc12f..0000000
--- a/Xboard/app/Http/Routes/V2/UserRoute.php
+++ /dev/null
@@ -1,20 +0,0 @@
-group([
- 'prefix' => 'user',
- 'middleware' => 'user'
- ], function ($router) {
- // User
- $router->get('/resetSecurity', [UserController::class, 'resetSecurity']);
- $router->get('/info', [UserController::class, 'info']);
- });
- }
-}
diff --git a/Xboard/app/Jobs/NodeUserSyncJob.php b/Xboard/app/Jobs/NodeUserSyncJob.php
deleted file mode 100644
index f49e108..0000000
--- a/Xboard/app/Jobs/NodeUserSyncJob.php
+++ /dev/null
@@ -1,45 +0,0 @@
-onQueue('node_sync');
- }
-
- public function handle(): void
- {
- $user = User::find($this->userId);
-
- if ($this->action === 'updated' || $this->action === 'created') {
- if ($this->oldGroupId) {
- NodeSyncService::notifyUserRemovedFromGroup($this->userId, $this->oldGroupId);
- }
- if ($user) {
- NodeSyncService::notifyUserChanged($user);
- }
- } elseif ($this->action === 'deleted') {
- if ($this->oldGroupId) {
- NodeSyncService::notifyUserRemovedFromGroup($this->userId, $this->oldGroupId);
- }
- }
- }
-}
diff --git a/Xboard/app/Jobs/OrderHandleJob.php b/Xboard/app/Jobs/OrderHandleJob.php
deleted file mode 100644
index 960f659..0000000
--- a/Xboard/app/Jobs/OrderHandleJob.php
+++ /dev/null
@@ -1,56 +0,0 @@
-onQueue('order_handle');
- $this->tradeNo = $tradeNo;
- }
-
- /**
- * Execute the job.
- *
- * @return void
- */
- public function handle()
- {
- $order = Order::where('trade_no', $this->tradeNo)
- ->lockForUpdate()
- ->first();
- if (!$order) return;
- $orderService = new OrderService($order);
- switch ($order->status) {
- // cancel
- case Order::STATUS_PENDING:
- if ($order->created_at <= (time() - 3600 * 2)) {
- $orderService->cancel();
- }
- break;
- case Order::STATUS_PROCESSING:
- $orderService->open();
- break;
- }
- }
-}
diff --git a/Xboard/app/Jobs/SendEmailJob.php b/Xboard/app/Jobs/SendEmailJob.php
deleted file mode 100644
index d998f41..0000000
--- a/Xboard/app/Jobs/SendEmailJob.php
+++ /dev/null
@@ -1,42 +0,0 @@
-onQueue($queue);
- $this->params = $params;
- }
-
- /**
- * Execute the job.
- *
- * @return void
- */
- public function handle()
- {
- $mailLog = MailService::sendEmail($this->params);
- if ($mailLog['error']) {
- $this->release(); //发送失败将触发重试
- }
- }
-}
diff --git a/Xboard/app/Jobs/SendTelegramJob.php b/Xboard/app/Jobs/SendTelegramJob.php
deleted file mode 100644
index 8d30344..0000000
--- a/Xboard/app/Jobs/SendTelegramJob.php
+++ /dev/null
@@ -1,43 +0,0 @@
-onQueue('send_telegram');
- $this->telegramId = $telegramId;
- $this->text = $text;
- }
-
- /**
- * Execute the job.
- *
- * @return void
- */
- public function handle()
- {
- $telegramService = new TelegramService();
- $telegramService->sendMessage($this->telegramId, $this->text, 'markdown');
- }
-}
diff --git a/Xboard/app/Jobs/StatServerJob.php b/Xboard/app/Jobs/StatServerJob.php
deleted file mode 100644
index c055ca5..0000000
--- a/Xboard/app/Jobs/StatServerJob.php
+++ /dev/null
@@ -1,174 +0,0 @@
-onQueue('stat');
- $this->data = $data;
- $this->server = $server;
- $this->protocol = $protocol;
- $this->recordType = $recordType;
- }
-
- public function handle(): void
- {
- $recordAt = $this->recordType === 'm'
- ? strtotime(date('Y-m-01'))
- : strtotime(date('Y-m-d'));
-
- $u = $d = 0;
- foreach ($this->data as $traffic) {
- $u += $traffic[0];
- $d += $traffic[1];
- }
-
- try {
- $this->processServerStat($u, $d, $recordAt);
- $this->updateServerTraffic($u, $d);
- } catch (\Exception $e) {
- Log::error('StatServerJob failed for server ' . $this->server['id'] . ': ' . $e->getMessage());
- throw $e;
- }
- }
-
- protected function updateServerTraffic(int $u, int $d): void
- {
- DB::table('v2_server')
- ->where('id', $this->server['id'])
- ->incrementEach(
- ['u' => $u, 'd' => $d],
- ['updated_at' => Carbon::now()]
- );
- }
-
- protected function processServerStat(int $u, int $d, int $recordAt): void
- {
- $driver = config('database.default');
- if ($driver === 'sqlite') {
- $this->processServerStatForSqlite($u, $d, $recordAt);
- } elseif ($driver === 'pgsql') {
- $this->processServerStatForPostgres($u, $d, $recordAt);
- } else {
- $this->processServerStatForOtherDatabases($u, $d, $recordAt);
- }
- }
-
- protected function processServerStatForSqlite(int $u, int $d, int $recordAt): void
- {
- DB::transaction(function () use ($u, $d, $recordAt) {
- $existingRecord = StatServer::where([
- 'record_at' => $recordAt,
- 'server_id' => $this->server['id'],
- 'server_type' => $this->protocol,
- 'record_type' => $this->recordType,
- ])->first();
-
- if ($existingRecord) {
- $existingRecord->update([
- 'u' => $existingRecord->u + $u,
- 'd' => $existingRecord->d + $d,
- 'updated_at' => time(),
- ]);
- } else {
- StatServer::create([
- 'record_at' => $recordAt,
- 'server_id' => $this->server['id'],
- 'server_type' => $this->protocol,
- 'record_type' => $this->recordType,
- 'u' => $u,
- 'd' => $d,
- 'created_at' => time(),
- 'updated_at' => time(),
- ]);
- }
- }, 3);
- }
-
- protected function processServerStatForOtherDatabases(int $u, int $d, int $recordAt): void
- {
- StatServer::upsert(
- [
- 'record_at' => $recordAt,
- 'server_id' => $this->server['id'],
- 'server_type' => $this->protocol,
- 'record_type' => $this->recordType,
- 'u' => $u,
- 'd' => $d,
- 'created_at' => time(),
- 'updated_at' => time(),
- ],
- ['server_id', 'server_type', 'record_at', 'record_type'],
- [
- 'u' => DB::raw("u + VALUES(u)"),
- 'd' => DB::raw("d + VALUES(d)"),
- 'updated_at' => time(),
- ]
- );
- }
-
- /**
- * PostgreSQL upsert with arithmetic increments using ON CONFLICT ... DO UPDATE
- */
- protected function processServerStatForPostgres(int $u, int $d, int $recordAt): void
- {
- $table = (new StatServer())->getTable();
- $now = time();
-
- // Use parameter binding to avoid SQL injection and keep maintainability
- $sql = "INSERT INTO {$table} (record_at, server_id, server_type, record_type, u, d, created_at, updated_at)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
- ON CONFLICT (server_id, server_type, record_at)
- DO UPDATE SET
- u = {$table}.u + EXCLUDED.u,
- d = {$table}.d + EXCLUDED.d,
- updated_at = EXCLUDED.updated_at";
-
- DB::statement($sql, [
- $recordAt,
- $this->server['id'],
- $this->protocol,
- $this->recordType,
- $u,
- $d,
- $now,
- $now,
- ]);
- }
-}
diff --git a/Xboard/app/Jobs/StatUserJob.php b/Xboard/app/Jobs/StatUserJob.php
deleted file mode 100644
index 620443c..0000000
--- a/Xboard/app/Jobs/StatUserJob.php
+++ /dev/null
@@ -1,158 +0,0 @@
-onQueue('stat');
- $this->data = $data;
- $this->server = $server;
- $this->protocol = $protocol;
- $this->recordType = $recordType;
- }
-
- public function handle(): void
- {
- $recordAt = $this->recordType === 'm'
- ? strtotime(date('Y-m-01'))
- : strtotime(date('Y-m-d'));
-
- foreach ($this->data as $uid => $v) {
- try {
- $this->processUserStat($uid, $v, $recordAt);
- } catch (\Exception $e) {
- Log::error('StatUserJob failed for user ' . $uid . ': ' . $e->getMessage());
- throw $e;
- }
- }
- }
-
- protected function processUserStat(int $uid, array $v, int $recordAt): void
- {
- $driver = config('database.default');
- if ($driver === 'sqlite') {
- $this->processUserStatForSqlite($uid, $v, $recordAt);
- } elseif ($driver === 'pgsql') {
- $this->processUserStatForPostgres($uid, $v, $recordAt);
- } else {
- $this->processUserStatForOtherDatabases($uid, $v, $recordAt);
- }
- }
-
- protected function processUserStatForSqlite(int $uid, array $v, int $recordAt): void
- {
- DB::transaction(function () use ($uid, $v, $recordAt) {
- $existingRecord = StatUser::where([
- 'user_id' => $uid,
- 'server_rate' => $this->server['rate'],
- 'record_at' => $recordAt,
- 'record_type' => $this->recordType,
- ])->first();
-
- if ($existingRecord) {
- $existingRecord->update([
- 'u' => $existingRecord->u + intval($v[0] * $this->server['rate']),
- 'd' => $existingRecord->d + intval($v[1] * $this->server['rate']),
- 'updated_at' => time(),
- ]);
- } else {
- StatUser::create([
- 'user_id' => $uid,
- 'server_rate' => $this->server['rate'],
- 'record_at' => $recordAt,
- 'record_type' => $this->recordType,
- 'u' => intval($v[0] * $this->server['rate']),
- 'd' => intval($v[1] * $this->server['rate']),
- 'created_at' => time(),
- 'updated_at' => time(),
- ]);
- }
- }, 3);
- }
-
- protected function processUserStatForOtherDatabases(int $uid, array $v, int $recordAt): void
- {
- StatUser::upsert(
- [
- 'user_id' => $uid,
- 'server_rate' => $this->server['rate'],
- 'record_at' => $recordAt,
- 'record_type' => $this->recordType,
- 'u' => intval($v[0] * $this->server['rate']),
- 'd' => intval($v[1] * $this->server['rate']),
- 'created_at' => time(),
- 'updated_at' => time(),
- ],
- ['user_id', 'server_rate', 'record_at', 'record_type'],
- [
- 'u' => DB::raw("u + VALUES(u)"),
- 'd' => DB::raw("d + VALUES(d)"),
- 'updated_at' => time(),
- ]
- );
- }
-
- /**
- * PostgreSQL upsert with arithmetic increments using ON CONFLICT ... DO UPDATE
- */
- protected function processUserStatForPostgres(int $uid, array $v, int $recordAt): void
- {
- $table = (new StatUser())->getTable();
- $now = time();
- $u = intval($v[0] * $this->server['rate']);
- $d = intval($v[1] * $this->server['rate']);
-
- $sql = "INSERT INTO {$table} (user_id, server_rate, record_at, record_type, u, d, created_at, updated_at)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
- ON CONFLICT (user_id, server_rate, record_at)
- DO UPDATE SET
- u = {$table}.u + EXCLUDED.u,
- d = {$table}.d + EXCLUDED.d,
- updated_at = EXCLUDED.updated_at";
-
- DB::statement($sql, [
- $uid,
- $this->server['rate'],
- $recordAt,
- $this->recordType,
- $u,
- $d,
- $now,
- $now,
- ]);
- }
-}
\ No newline at end of file
diff --git a/Xboard/app/Jobs/TrafficFetchJob.php b/Xboard/app/Jobs/TrafficFetchJob.php
deleted file mode 100644
index 1e398aa..0000000
--- a/Xboard/app/Jobs/TrafficFetchJob.php
+++ /dev/null
@@ -1,51 +0,0 @@
-onQueue('traffic_fetch');
- $this->server = $server;
- $this->data = $data;
- $this->protocol = $protocol;
- $this->timestamp = $timestamp;
- }
-
- public function handle(): void
- {
- $userIds = array_keys($this->data);
-
- foreach ($this->data as $uid => $v) {
- User::where('id', $uid)
- ->incrementEach(
- [
- 'u' => $v[0] * $this->server['rate'],
- 'd' => $v[1] * $this->server['rate'],
- ],
- ['t' => time()]
- );
- }
-
- if (!empty($userIds)) {
- Redis::sadd('traffic:pending_check', ...$userIds);
- }
- }
-}
diff --git a/Xboard/app/Models/AdminAuditLog.php b/Xboard/app/Models/AdminAuditLog.php
deleted file mode 100644
index 9e9404c..0000000
--- a/Xboard/app/Models/AdminAuditLog.php
+++ /dev/null
@@ -1,21 +0,0 @@
- 'timestamp',
- 'updated_at' => 'timestamp',
- ];
-
- public function admin()
- {
- return $this->belongsTo(User::class, 'admin_id');
- }
-}
diff --git a/Xboard/app/Models/CommissionLog.php b/Xboard/app/Models/CommissionLog.php
deleted file mode 100644
index 744c5d5..0000000
--- a/Xboard/app/Models/CommissionLog.php
+++ /dev/null
@@ -1,16 +0,0 @@
- 'timestamp',
- 'updated_at' => 'timestamp'
- ];
-}
diff --git a/Xboard/app/Models/Coupon.php b/Xboard/app/Models/Coupon.php
deleted file mode 100644
index e6271b8..0000000
--- a/Xboard/app/Models/Coupon.php
+++ /dev/null
@@ -1,28 +0,0 @@
- 'timestamp',
- 'updated_at' => 'timestamp',
- 'limit_plan_ids' => 'array',
- 'limit_period' => 'array',
- 'show' => 'boolean',
- ];
-
- public function getLimitPeriodAttribute($value)
- {
- return collect(json_decode((string) $value, true))->map(function ($item) {
- return PlanService::getPeriodKey($item);
- })->toArray();
- }
-
-}
diff --git a/Xboard/app/Models/GiftCardCode.php b/Xboard/app/Models/GiftCardCode.php
deleted file mode 100644
index dd09e8e..0000000
--- a/Xboard/app/Models/GiftCardCode.php
+++ /dev/null
@@ -1,260 +0,0 @@
- 'timestamp',
- 'updated_at' => 'timestamp',
- 'used_at' => 'timestamp',
- 'expires_at' => 'timestamp',
- 'actual_rewards' => 'array',
- 'metadata' => 'array'
- ];
-
- /**
- * 获取状态映射
- */
- public static function getStatusMap(): array
- {
- return [
- self::STATUS_UNUSED => '未使用',
- self::STATUS_USED => '已使用',
- self::STATUS_EXPIRED => '已过期',
- self::STATUS_DISABLED => '已禁用',
- ];
- }
-
- /**
- * 获取状态名称
- */
- public function getStatusNameAttribute(): string
- {
- return self::getStatusMap()[$this->status] ?? '未知状态';
- }
-
- /**
- * 关联礼品卡模板
- */
- public function template(): BelongsTo
- {
- return $this->belongsTo(GiftCardTemplate::class, 'template_id');
- }
-
- /**
- * 关联使用用户
- */
- public function user(): BelongsTo
- {
- return $this->belongsTo(User::class, 'user_id');
- }
-
- /**
- * 关联使用记录
- */
- public function usages(): HasMany
- {
- return $this->hasMany(GiftCardUsage::class, 'code_id');
- }
-
- /**
- * 检查是否可用
- */
- public function isAvailable(): bool
- {
- // 检查状态
- if (in_array($this->status, [self::STATUS_EXPIRED, self::STATUS_DISABLED])) {
- return false;
- }
-
- // 检查是否过期
- if ($this->expires_at && $this->expires_at < time()) {
- return false;
- }
-
- // 检查使用次数
- if ($this->usage_count >= $this->max_usage) {
- return false;
- }
-
- return true;
- }
-
- /**
- * 检查是否已过期
- */
- public function isExpired(): bool
- {
- return $this->expires_at && $this->expires_at < time();
- }
-
- /**
- * 标记为已使用
- */
- public function markAsUsed(User $user): bool
- {
- $this->status = self::STATUS_USED;
- $this->user_id = $user->id;
- $this->used_at = time();
- $this->usage_count += 1;
-
- return $this->save();
- }
-
- /**
- * 标记为已过期
- */
- public function markAsExpired(): bool
- {
- $this->status = self::STATUS_EXPIRED;
- return $this->save();
- }
-
- /**
- * 标记为已禁用
- */
- public function markAsDisabled(): bool
- {
- $this->status = self::STATUS_DISABLED;
- return $this->save();
- }
-
- /**
- * 生成兑换码
- */
- public static function generateCode(string $prefix = 'GC'): string
- {
- do {
- $safePrefix = (string) $prefix;
- $code = $safePrefix . strtoupper(substr(md5(uniqid($safePrefix . mt_rand(), true)), 0, 12));
- } while (self::where('code', $code)->exists());
-
- return $code;
- }
-
- /**
- * 批量生成兑换码
- */
- public static function batchGenerate(int $templateId, int $count, array $options = []): string
- {
- $batchId = uniqid('batch_');
- $prefix = $options['prefix'] ?? 'GC';
- $expiresAt = $options['expires_at'] ?? null;
- $maxUsage = $options['max_usage'] ?? 1;
-
- $codes = [];
- for ($i = 0; $i < $count; $i++) {
- $codes[] = [
- 'template_id' => $templateId,
- 'code' => self::generateCode($prefix),
- 'batch_id' => $batchId,
- 'status' => self::STATUS_UNUSED,
- 'expires_at' => $expiresAt,
- 'max_usage' => $maxUsage,
- 'created_at' => time(),
- 'updated_at' => time(),
- ];
- }
-
- self::insert($codes);
-
- return $batchId;
- }
-
- /**
- * 设置实际奖励(用于盲盒等)
- */
- public function setActualRewards(array $rewards): bool
- {
- $this->actual_rewards = $rewards;
- return $this->save();
- }
-
- /**
- * 获取实际奖励
- */
- public function getActualRewards(): array
- {
- return $this->actual_rewards ?? $this->template->rewards ?? [];
- }
-
- /**
- * 检查兑换码格式
- */
- public static function validateCodeFormat(string $code): bool
- {
- // 基本格式验证:字母数字组合,长度8-32
- return preg_match('/^[A-Z0-9]{8,32}$/', $code);
- }
-
- /**
- * 根据批次ID获取兑换码
- */
- public static function getByBatchId(string $batchId)
- {
- return self::where('batch_id', $batchId)->get();
- }
-
- /**
- * 清理过期兑换码
- */
- public static function cleanupExpired(): int
- {
- $count = self::where('status', self::STATUS_UNUSED)
- ->where('expires_at', '<', time())
- ->count();
-
- self::where('status', self::STATUS_UNUSED)
- ->where('expires_at', '<', time())
- ->update(['status' => self::STATUS_EXPIRED]);
-
- return $count;
- }
-}
\ No newline at end of file
diff --git a/Xboard/app/Models/GiftCardTemplate.php b/Xboard/app/Models/GiftCardTemplate.php
deleted file mode 100644
index 87a04de..0000000
--- a/Xboard/app/Models/GiftCardTemplate.php
+++ /dev/null
@@ -1,254 +0,0 @@
- 'timestamp',
- 'updated_at' => 'timestamp',
- 'conditions' => 'array',
- 'rewards' => 'array',
- 'limits' => 'array',
- 'special_config' => 'array',
- 'status' => 'boolean'
- ];
-
- /**
- * 获取卡片类型映射
- */
- public static function getTypeMap(): array
- {
- return [
- self::TYPE_GENERAL => '通用礼品卡',
- self::TYPE_PLAN => '套餐礼品卡',
- self::TYPE_MYSTERY => '盲盒礼品卡',
- ];
- }
-
- /**
- * 获取类型名称
- */
- public function getTypeNameAttribute(): string
- {
- return self::getTypeMap()[$this->type] ?? '未知类型';
- }
-
- /**
- * 关联兑换码
- */
- public function codes(): HasMany
- {
- return $this->hasMany(GiftCardCode::class, 'template_id');
- }
-
- /**
- * 关联使用记录
- */
- public function usages(): HasMany
- {
- return $this->hasMany(GiftCardUsage::class, 'template_id');
- }
-
- /**
- * 关联统计数据
- */
- public function stats(): HasMany
- {
- return $this->hasMany(GiftCardUsage::class, 'template_id');
- }
-
- /**
- * 检查是否可用
- */
- public function isAvailable(): bool
- {
- return $this->status;
- }
-
- /**
- * 检查用户是否满足使用条件
- */
- public function checkUserConditions(User $user): bool
- {
- switch ($this->type) {
- case self::TYPE_GENERAL:
- $rewards = $this->rewards ?? [];
- if (isset($rewards['transfer_enable']) || isset($rewards['expire_days']) || isset($rewards['reset_package'])) {
- if (!$user->plan_id) {
- return false;
- }
- }
- break;
- case self::TYPE_PLAN:
- if ($user->isActive()) {
- return false;
- }
- break;
- }
-
- $conditions = $this->conditions ?? [];
-
- // 检查新用户条件
- if (isset($conditions['new_user_only']) && $conditions['new_user_only']) {
- $maxDays = $conditions['new_user_max_days'] ?? 7;
- if ($user->created_at < (time() - ($maxDays * 86400))) {
- return false;
- }
- }
-
- // 检查付费用户条件
- if (isset($conditions['paid_user_only']) && $conditions['paid_user_only']) {
- $paidOrderExists = $user->orders()->where('status', Order::STATUS_COMPLETED)->exists();
- if (!$paidOrderExists) {
- return false;
- }
- }
-
- // 检查允许的套餐
- if (isset($conditions['allowed_plans']) && $user->plan_id) {
- if (!in_array($user->plan_id, $conditions['allowed_plans'])) {
- return false;
- }
- }
-
- // 检查是否需要邀请人
- if (isset($conditions['require_invite']) && $conditions['require_invite']) {
- if (!$user->invite_user_id) {
- return false;
- }
- }
-
- return true;
- }
-
- /**
- * 计算实际奖励
- */
- public function calculateActualRewards(User $user): array
- {
- $baseRewards = $this->rewards;
- $actualRewards = $baseRewards;
-
- // 处理盲盒随机奖励
- if ($this->type === self::TYPE_MYSTERY && isset($this->rewards['random_rewards'])) {
- $randomRewards = $this->rewards['random_rewards'];
- $totalWeight = array_sum(array_column($randomRewards, 'weight'));
- $random = mt_rand(1, $totalWeight);
- $currentWeight = 0;
-
- foreach ($randomRewards as $reward) {
- $currentWeight += $reward['weight'];
- if ($random <= $currentWeight) {
- $actualRewards = array_merge($actualRewards, $reward);
- unset($actualRewards['weight']);
- break;
- }
- }
- }
-
- // 处理节日等特殊奖励(通用逻辑)
- if (isset($this->special_config['festival_bonus'])) {
- $now = time();
- $festivalConfig = $this->special_config;
-
- if (isset($festivalConfig['start_time']) && isset($festivalConfig['end_time'])) {
- if ($now >= $festivalConfig['start_time'] && $now <= $festivalConfig['end_time']) {
- $bonus = data_get($festivalConfig, 'festival_bonus', 1.0);
- if ($bonus > 1.0) {
- foreach ($actualRewards as $key => &$value) {
- if (is_numeric($value)) {
- $value = intval($value * $bonus);
- }
- }
- unset($value); // 解除引用
- }
- }
- }
- }
-
- return $actualRewards;
- }
-
- /**
- * 检查使用频率限制
- */
- public function checkUsageLimit(User $user): bool
- {
- $limits = $this->limits ?? [];
-
- // 检查每用户最大使用次数
- if (isset($limits['max_use_per_user'])) {
- $usedCount = $this->usages()
- ->where('user_id', $user->id)
- ->count();
- if ($usedCount >= $limits['max_use_per_user']) {
- return false;
- }
- }
-
- // 检查冷却时间
- if (isset($limits['cooldown_hours'])) {
- $lastUsage = $this->usages()
- ->where('user_id', $user->id)
- ->orderBy('created_at', 'desc')
- ->first();
-
- if ($lastUsage && isset($lastUsage->created_at)) {
- $cooldownTime = $lastUsage->created_at + ($limits['cooldown_hours'] * 3600);
- if (time() < $cooldownTime) {
- return false;
- }
- }
- }
-
- return true;
- }
-}
\ No newline at end of file
diff --git a/Xboard/app/Models/GiftCardUsage.php b/Xboard/app/Models/GiftCardUsage.php
deleted file mode 100644
index 69337b9..0000000
--- a/Xboard/app/Models/GiftCardUsage.php
+++ /dev/null
@@ -1,112 +0,0 @@
- 'timestamp',
- 'rewards_given' => 'array',
- 'invite_rewards' => 'array',
- 'multiplier_applied' => 'float'
- ];
-
- /**
- * 关联兑换码
- */
- public function code(): BelongsTo
- {
- return $this->belongsTo(GiftCardCode::class, 'code_id');
- }
-
- /**
- * 关联模板
- */
- public function template(): BelongsTo
- {
- return $this->belongsTo(GiftCardTemplate::class, 'template_id');
- }
-
- /**
- * 关联使用用户
- */
- public function user(): BelongsTo
- {
- return $this->belongsTo(User::class, 'user_id');
- }
-
- /**
- * 关联邀请人
- */
- public function inviteUser(): BelongsTo
- {
- return $this->belongsTo(User::class, 'invite_user_id');
- }
-
- /**
- * 创建使用记录
- */
- public static function createRecord(
- GiftCardCode $code,
- User $user,
- array $rewards,
- array $options = []
- ): self {
- return self::create([
- 'code_id' => $code->id,
- 'template_id' => $code->template_id,
- 'user_id' => $user->id,
- 'invite_user_id' => $user->invite_user_id,
- 'rewards_given' => $rewards,
- 'invite_rewards' => $options['invite_rewards'] ?? null,
- 'user_level_at_use' => $user->plan ? $user->plan->sort : null,
- 'plan_id_at_use' => $user->plan_id,
- 'multiplier_applied' => $options['multiplier'] ?? 1.0,
- // 'ip_address' => $options['ip_address'] ?? null,
- 'user_agent' => $options['user_agent'] ?? null,
- 'notes' => $options['notes'] ?? null,
- 'created_at' => time(),
- ]);
- }
-}
\ No newline at end of file
diff --git a/Xboard/app/Models/InviteCode.php b/Xboard/app/Models/InviteCode.php
deleted file mode 100644
index 7d2716b..0000000
--- a/Xboard/app/Models/InviteCode.php
+++ /dev/null
@@ -1,19 +0,0 @@
- 'timestamp',
- 'updated_at' => 'timestamp',
- 'status' => 'boolean',
- ];
-
- const STATUS_UNUSED = 0;
- const STATUS_USED = 1;
-}
diff --git a/Xboard/app/Models/Knowledge.php b/Xboard/app/Models/Knowledge.php
deleted file mode 100644
index 91e7736..0000000
--- a/Xboard/app/Models/Knowledge.php
+++ /dev/null
@@ -1,17 +0,0 @@
- 'boolean',
- 'created_at' => 'timestamp',
- 'updated_at' => 'timestamp',
- ];
-}
diff --git a/Xboard/app/Models/MailLog.php b/Xboard/app/Models/MailLog.php
deleted file mode 100644
index 0aef380..0000000
--- a/Xboard/app/Models/MailLog.php
+++ /dev/null
@@ -1,16 +0,0 @@
- 'timestamp',
- 'updated_at' => 'timestamp'
- ];
-}
diff --git a/Xboard/app/Models/Notice.php b/Xboard/app/Models/Notice.php
deleted file mode 100644
index 3ae0cb0..0000000
--- a/Xboard/app/Models/Notice.php
+++ /dev/null
@@ -1,18 +0,0 @@
- 'timestamp',
- 'updated_at' => 'timestamp',
- 'tags' => 'array',
- 'show' => 'boolean',
- ];
-}
diff --git a/Xboard/app/Models/Order.php b/Xboard/app/Models/Order.php
deleted file mode 100644
index dda0581..0000000
--- a/Xboard/app/Models/Order.php
+++ /dev/null
@@ -1,120 +0,0 @@
- $commission_log
- */
-class Order extends Model
-{
- protected $table = 'v2_order';
- protected $dateFormat = 'U';
- protected $guarded = ['id'];
- protected $casts = [
- 'created_at' => 'timestamp',
- 'updated_at' => 'timestamp',
- 'surplus_order_ids' => 'array',
- 'handling_amount' => 'integer'
- ];
-
- const STATUS_PENDING = 0; // 待支付
- const STATUS_PROCESSING = 1; // 开通中
- const STATUS_CANCELLED = 2; // 已取消
- const STATUS_COMPLETED = 3; // 已完成
- const STATUS_DISCOUNTED = 4; // 已折抵
-
- public static $statusMap = [
- self::STATUS_PENDING => '待支付',
- self::STATUS_PROCESSING => '开通中',
- self::STATUS_CANCELLED => '已取消',
- self::STATUS_COMPLETED => '已完成',
- self::STATUS_DISCOUNTED => '已折抵',
- ];
-
- const TYPE_NEW_PURCHASE = 1; // 新购
- const TYPE_RENEWAL = 2; // 续费
- const TYPE_UPGRADE = 3; // 升级
- const TYPE_RESET_TRAFFIC = 4; //流量重置包
- public static $typeMap = [
- self::TYPE_NEW_PURCHASE => '新购',
- self::TYPE_RENEWAL => '续费',
- self::TYPE_UPGRADE => '升级',
- self::TYPE_RESET_TRAFFIC => '流量重置',
- ];
-
- /**
- * 获取与订单关联的支付方式
- */
- public function payment(): BelongsTo
- {
- return $this->belongsTo(Payment::class, 'payment_id', 'id');
- }
-
- /**
- * 获取与订单关联的用户
- */
- public function user(): BelongsTo
- {
- return $this->belongsTo(User::class, 'user_id', 'id');
- }
-
- /**
- * 获取邀请人
- */
- public function invite_user(): BelongsTo
- {
- return $this->belongsTo(User::class, 'invite_user_id', 'id');
- }
-
- /**
- * 获取与订单关联的套餐
- */
- public function plan(): BelongsTo
- {
- return $this->belongsTo(Plan::class, 'plan_id', 'id');
- }
-
- /**
- * 获取与订单关联的佣金记录
- */
- public function commission_log(): HasMany
- {
- return $this->hasMany(CommissionLog::class, 'trade_no', 'trade_no');
- }
-}
diff --git a/Xboard/app/Models/Payment.php b/Xboard/app/Models/Payment.php
deleted file mode 100644
index fec8b00..0000000
--- a/Xboard/app/Models/Payment.php
+++ /dev/null
@@ -1,18 +0,0 @@
- 'timestamp',
- 'updated_at' => 'timestamp',
- 'config' => 'array',
- 'enable' => 'boolean'
- ];
-}
diff --git a/Xboard/app/Models/Plan.php b/Xboard/app/Models/Plan.php
deleted file mode 100644
index 13d66f2..0000000
--- a/Xboard/app/Models/Plan.php
+++ /dev/null
@@ -1,353 +0,0 @@
- $order 关联的订单
- */
-class Plan extends Model
-{
- use HasFactory;
-
- protected $table = 'v2_plan';
- protected $dateFormat = 'U';
-
- // 定义流量重置方式
- public const RESET_TRAFFIC_FOLLOW_SYSTEM = null; // 跟随系统设置
- public const RESET_TRAFFIC_FIRST_DAY_MONTH = 0; // 每月1号
- public const RESET_TRAFFIC_MONTHLY = 1; // 按月重置
- public const RESET_TRAFFIC_NEVER = 2; // 不重置
- public const RESET_TRAFFIC_FIRST_DAY_YEAR = 3; // 每年1月1日
- public const RESET_TRAFFIC_YEARLY = 4; // 按年重置
-
- // 定义价格类型
- public const PRICE_TYPE_RESET_TRAFFIC = 'reset_traffic'; // 重置流量价格
-
- // 定义可用的订阅周期
- public const PERIOD_MONTHLY = 'monthly';
- public const PERIOD_QUARTERLY = 'quarterly';
- public const PERIOD_HALF_YEARLY = 'half_yearly';
- public const PERIOD_YEARLY = 'yearly';
- public const PERIOD_TWO_YEARLY = 'two_yearly';
- public const PERIOD_THREE_YEARLY = 'three_yearly';
- public const PERIOD_ONETIME = 'onetime';
- public const PERIOD_RESET_TRAFFIC = 'reset_traffic';
-
- // 定义旧版周期映射
- public const LEGACY_PERIOD_MAPPING = [
- 'month_price' => self::PERIOD_MONTHLY,
- 'quarter_price' => self::PERIOD_QUARTERLY,
- 'half_year_price' => self::PERIOD_HALF_YEARLY,
- 'year_price' => self::PERIOD_YEARLY,
- 'two_year_price' => self::PERIOD_TWO_YEARLY,
- 'three_year_price' => self::PERIOD_THREE_YEARLY,
- 'onetime_price' => self::PERIOD_ONETIME,
- 'reset_price' => self::PERIOD_RESET_TRAFFIC
- ];
-
- protected $fillable = [
- 'group_id',
- 'transfer_enable',
- 'name',
- 'speed_limit',
- 'show',
- 'sort',
- 'renew',
- 'content',
- 'prices',
- 'reset_traffic_method',
- 'capacity_limit',
- 'sell',
- 'device_limit',
- 'tags'
- ];
-
- protected $casts = [
- 'show' => 'boolean',
- 'renew' => 'boolean',
- 'created_at' => 'timestamp',
- 'updated_at' => 'timestamp',
- 'group_id' => 'integer',
- 'prices' => 'array',
- 'tags' => 'array',
- 'reset_traffic_method' => 'integer',
- ];
-
- /**
- * 获取所有可用的流量重置方式
- *
- * @return array
- */
- public static function getResetTrafficMethods(): array
- {
- return [
- self::RESET_TRAFFIC_FOLLOW_SYSTEM => '跟随系统设置',
- self::RESET_TRAFFIC_FIRST_DAY_MONTH => '每月1号',
- self::RESET_TRAFFIC_MONTHLY => '按月重置',
- self::RESET_TRAFFIC_NEVER => '不重置',
- self::RESET_TRAFFIC_FIRST_DAY_YEAR => '每年1月1日',
- self::RESET_TRAFFIC_YEARLY => '按年重置',
- ];
- }
-
- /**
- * 获取所有可用的订阅周期
- *
- * @return array
- */
- public static function getAvailablePeriods(): array
- {
- return [
- self::PERIOD_MONTHLY => [
- 'name' => '月付',
- 'days' => 30,
- 'value' => 1
- ],
- self::PERIOD_QUARTERLY => [
- 'name' => '季付',
- 'days' => 90,
- 'value' => 3
- ],
- self::PERIOD_HALF_YEARLY => [
- 'name' => '半年付',
- 'days' => 180,
- 'value' => 6
- ],
- self::PERIOD_YEARLY => [
- 'name' => '年付',
- 'days' => 365,
- 'value' => 12
- ],
- self::PERIOD_TWO_YEARLY => [
- 'name' => '两年付',
- 'days' => 730,
- 'value' => 24
- ],
- self::PERIOD_THREE_YEARLY => [
- 'name' => '三年付',
- 'days' => 1095,
- 'value' => 36
- ],
- self::PERIOD_ONETIME => [
- 'name' => '一次性',
- 'days' => -1,
- 'value' => -1
- ],
- self::PERIOD_RESET_TRAFFIC => [
- 'name' => '重置流量',
- 'days' => -1,
- 'value' => -1
- ],
- ];
- }
-
- /**
- * 获取指定周期的价格
- *
- * @param string $period
- * @return int|null
- */
- public function getPriceByPeriod(string $period): ?int
- {
- return $this->prices[$period] ?? null;
- }
-
- /**
- * 获取所有已设置价格的周期
- *
- * @return array
- */
- public function getActivePeriods(): array
- {
- return array_filter(
- self::getAvailablePeriods(),
- fn($period) => isset($this->prices[$period])
- && $this->prices[$period] > 0,
- ARRAY_FILTER_USE_KEY
- );
- }
-
- /**
- * 设置指定周期的价格
- *
- * @param string $period
- * @param int $price
- * @return void
- * @throws InvalidArgumentException
- */
- public function setPeriodPrice(string $period, int $price): void
- {
- if (!array_key_exists($period, self::getAvailablePeriods())) {
- throw new InvalidArgumentException("Invalid period: {$period}");
- }
-
- $prices = $this->prices ?? [];
- $prices[$period] = $price;
- $this->prices = $prices;
- }
-
- /**
- * 移除指定周期的价格
- *
- * @param string $period
- * @return void
- */
- public function removePeriodPrice(string $period): void
- {
- $prices = $this->prices ?? [];
- unset($prices[$period]);
- $this->prices = $prices;
- }
-
- /**
- * 获取所有价格及其对应的周期信息
- *
- * @return array
- */
- public function getPriceList(): array
- {
- $prices = $this->prices ?? [];
- $periods = self::getAvailablePeriods();
-
- $priceList = [];
- foreach ($prices as $period => $price) {
- if (isset($periods[$period]) && $price > 0) {
- $priceList[$period] = [
- 'period' => $periods[$period],
- 'price' => $price,
- 'average_price' => $periods[$period]['value'] > 0
- ? round($price / $periods[$period]['value'], 2)
- : $price
- ];
- }
- }
-
- return $priceList;
- }
-
- /**
- * 检查是否可以重置流量
- *
- * @return bool
- */
- public function canResetTraffic(): bool
- {
- return $this->reset_traffic_method !== self::RESET_TRAFFIC_NEVER
- && $this->getResetTrafficPrice() > 0;
- }
-
- /**
- * 获取重置流量的价格
- *
- * @return int
- */
- public function getResetTrafficPrice(): int
- {
- return $this->prices[self::PRICE_TYPE_RESET_TRAFFIC] ?? 0;
- }
-
- /**
- * 计算指定周期的有效天数
- *
- * @param string $period
- * @return int -1表示永久有效
- * @throws InvalidArgumentException
- */
- public static function getPeriodDays(string $period): int
- {
- $periods = self::getAvailablePeriods();
- if (!isset($periods[$period])) {
- throw new InvalidArgumentException("Invalid period: {$period}");
- }
-
- return $periods[$period]['days'];
- }
-
- /**
- * 检查周期是否有效
- *
- * @param string $period
- * @return bool
- */
- public static function isValidPeriod(string $period): bool
- {
- return array_key_exists($period, self::getAvailablePeriods());
- }
-
- public function users(): HasMany
- {
- return $this->hasMany(User::class);
- }
-
- public function group(): HasOne
- {
- return $this->hasOne(ServerGroup::class, 'id', 'group_id');
- }
-
- public function orders(): HasMany
- {
- return $this->hasMany(Order::class);
- }
-
- /**
- * 设置流量重置方式
- *
- * @param int $method
- * @return void
- * @throws InvalidArgumentException
- */
- public function setResetTrafficMethod(int $method): void
- {
- if (!array_key_exists($method, self::getResetTrafficMethods())) {
- throw new InvalidArgumentException("Invalid reset traffic method: {$method}");
- }
-
- $this->reset_traffic_method = $method;
- }
-
- /**
- * 设置重置流量价格
- *
- * @param int $price
- * @return void
- */
- public function setResetTrafficPrice(int $price): void
- {
- $prices = $this->prices ?? [];
- $prices[self::PRICE_TYPE_RESET_TRAFFIC] = max(0, $price);
- $this->prices = $prices;
- }
-
- public function order(): HasMany
- {
- return $this->hasMany(Order::class);
- }
-}
\ No newline at end of file
diff --git a/Xboard/app/Models/Plugin.php b/Xboard/app/Models/Plugin.php
deleted file mode 100644
index 0425558..0000000
--- a/Xboard/app/Models/Plugin.php
+++ /dev/null
@@ -1,77 +0,0 @@
- 'boolean',
- ];
-
- public function scopeByType(Builder $query, string $type): Builder
- {
- return $query->where('type', $type);
- }
-
- public function isFeaturePlugin(): bool
- {
- return $this->type === self::TYPE_FEATURE;
- }
-
- public function isPaymentPlugin(): bool
- {
- return $this->type === self::TYPE_PAYMENT;
- }
-
- public function isProtected(): bool
- {
- return in_array($this->code, self::PROTECTED_PLUGINS);
- }
-
- public function canBeDeleted(): bool
- {
- return !$this->isProtected();
- }
-
-}
diff --git a/Xboard/app/Models/Server.php b/Xboard/app/Models/Server.php
deleted file mode 100644
index 19e2b5e..0000000
--- a/Xboard/app/Models/Server.php
+++ /dev/null
@@ -1,563 +0,0 @@
- $stats 节点统计
- *
- * @property-read int|null $last_check_at 最后检查时间(Unix时间戳)
- * @property-read int|null $last_push_at 最后推送时间(Unix时间戳)
- * @property-read int $online 在线用户数
- * @property-read int $online_conn 在线连接数
- * @property-read array|null $metrics 节点指标指标
- * @property-read int $is_online 是否在线(1在线 0离线)
- * @property-read string $available_status 可用状态描述
- * @property-read string $cache_key 缓存键
- * @property string|null $ports 端口范围
- * @property string|null $password 密码
- * @property int|null $u 上行流量
- * @property int|null $d 下行流量
- * @property int|null $total 总流量
- * @property-read array|null $load_status 负载状态(包含CPU、内存、交换区、磁盘信息)
- *
- * @property int $transfer_enable 流量上限,0或者null表示不限制
- * @property int $u 当前上传流量
- * @property int $d 当前下载流量
- */
-class Server extends Model
-{
- public const TYPE_HYSTERIA = 'hysteria';
- public const TYPE_VLESS = 'vless';
- public const TYPE_TROJAN = 'trojan';
- public const TYPE_VMESS = 'vmess';
- public const TYPE_TUIC = 'tuic';
- public const TYPE_SHADOWSOCKS = 'shadowsocks';
- public const TYPE_ANYTLS = 'anytls';
- public const TYPE_SOCKS = 'socks';
- public const TYPE_NAIVE = 'naive';
- public const TYPE_HTTP = 'http';
- public const TYPE_MIERU = 'mieru';
- public const STATUS_OFFLINE = 0;
- public const STATUS_ONLINE_NO_PUSH = 1;
- public const STATUS_ONLINE = 2;
-
- public const CHECK_INTERVAL = 300; // 5 minutes in seconds
-
- private const CIPHER_CONFIGURATIONS = [
- '2022-blake3-aes-128-gcm' => [
- 'serverKeySize' => 16,
- 'userKeySize' => 16,
- ],
- '2022-blake3-aes-256-gcm' => [
- 'serverKeySize' => 32,
- 'userKeySize' => 32,
- ],
- '2022-blake3-chacha20-poly1305' => [
- 'serverKeySize' => 32,
- 'userKeySize' => 32,
- ]
- ];
-
- public const TYPE_ALIASES = [
- 'v2ray' => self::TYPE_VMESS,
- 'hysteria2' => self::TYPE_HYSTERIA,
- ];
-
- public const VALID_TYPES = [
- self::TYPE_HYSTERIA,
- self::TYPE_VLESS,
- self::TYPE_TROJAN,
- self::TYPE_VMESS,
- self::TYPE_TUIC,
- self::TYPE_SHADOWSOCKS,
- self::TYPE_ANYTLS,
- self::TYPE_SOCKS,
- self::TYPE_NAIVE,
- self::TYPE_HTTP,
- self::TYPE_MIERU,
- ];
-
- protected $table = 'v2_server';
-
- protected $guarded = ['id'];
- protected $casts = [
- 'group_ids' => 'array',
- 'route_ids' => 'array',
- 'tags' => 'array',
- 'protocol_settings' => 'array',
- 'custom_outbounds' => 'array',
- 'custom_routes' => 'array',
- 'cert_config' => 'array',
- 'last_check_at' => 'integer',
- 'last_push_at' => 'integer',
- 'show' => 'boolean',
- 'created_at' => 'timestamp',
- 'updated_at' => 'timestamp',
- 'rate_time_ranges' => 'array',
- 'rate_time_enable' => 'boolean',
- 'transfer_enable' => 'integer',
- 'u' => 'integer',
- 'd' => 'integer',
- ];
-
- private const MULTIPLEX_CONFIGURATION = [
- 'multiplex' => [
- 'type' => 'object',
- 'fields' => [
- 'enabled' => ['type' => 'boolean', 'default' => false],
- 'protocol' => ['type' => 'string', 'default' => 'yamux'],
- 'max_connections' => ['type' => 'integer', 'default' => null],
- // 'min_streams' => ['type' => 'integer', 'default' => null],
- // 'max_streams' => ['type' => 'integer', 'default' => null],
- 'padding' => ['type' => 'boolean', 'default' => false],
- 'brutal' => [
- 'type' => 'object',
- 'fields' => [
- 'enabled' => ['type' => 'boolean', 'default' => false],
- 'up_mbps' => ['type' => 'integer', 'default' => null],
- 'down_mbps' => ['type' => 'integer', 'default' => null],
- ]
- ]
- ]
- ]
- ];
-
- private const REALITY_CONFIGURATION = [
- 'reality_settings' => [
- 'type' => 'object',
- 'fields' => [
- 'server_name' => ['type' => 'string', 'default' => null],
- 'server_port' => ['type' => 'string', 'default' => null],
- 'public_key' => ['type' => 'string', 'default' => null],
- 'private_key' => ['type' => 'string', 'default' => null],
- 'short_id' => ['type' => 'string', 'default' => null],
- 'allow_insecure' => ['type' => 'boolean', 'default' => false],
- ]
- ]
- ];
-
- private const UTLS_CONFIGURATION = [
- 'utls' => [
- 'type' => 'object',
- 'fields' => [
- 'enabled' => ['type' => 'boolean', 'default' => false],
- 'fingerprint' => ['type' => 'string', 'default' => 'chrome'],
- ]
- ]
- ];
-
- private const PROTOCOL_CONFIGURATIONS = [
- self::TYPE_TROJAN => [
- 'tls' => ['type' => 'integer', 'default' => 1],
- 'network' => ['type' => 'string', 'default' => null],
- 'network_settings' => ['type' => 'array', 'default' => null],
- 'server_name' => ['type' => 'string', 'default' => null],
- 'allow_insecure' => ['type' => 'boolean', 'default' => false],
- ...self::REALITY_CONFIGURATION,
- ...self::MULTIPLEX_CONFIGURATION,
- ...self::UTLS_CONFIGURATION
- ],
- self::TYPE_VMESS => [
- 'tls' => ['type' => 'integer', 'default' => 0],
- 'network' => ['type' => 'string', 'default' => null],
- 'rules' => ['type' => 'array', 'default' => null],
- 'network_settings' => ['type' => 'array', 'default' => null],
- 'tls_settings' => ['type' => 'array', 'default' => null],
- ...self::MULTIPLEX_CONFIGURATION,
- ...self::UTLS_CONFIGURATION
- ],
- self::TYPE_VLESS => [
- 'tls' => ['type' => 'integer', 'default' => 0],
- 'tls_settings' => ['type' => 'array', 'default' => null],
- 'flow' => ['type' => 'string', 'default' => null],
- 'encryption' => [
- 'type' => 'object',
- 'default' => null,
- 'fields' => [
- 'enabled' => ['type' => 'boolean', 'default' => false],
- 'encryption' => ['type' => 'string', 'default' => null], // 客户端公钥
- 'decryption' => ['type' => 'string', 'default' => null], // 服务端私钥
- ]
- ],
- 'network' => ['type' => 'string', 'default' => null],
- 'network_settings' => ['type' => 'array', 'default' => null],
- ...self::REALITY_CONFIGURATION,
- ...self::MULTIPLEX_CONFIGURATION,
- ...self::UTLS_CONFIGURATION
- ],
- self::TYPE_SHADOWSOCKS => [
- 'cipher' => ['type' => 'string', 'default' => null],
- 'obfs' => ['type' => 'string', 'default' => null],
- 'obfs_settings' => ['type' => 'array', 'default' => null],
- 'plugin' => ['type' => 'string', 'default' => null],
- 'plugin_opts' => ['type' => 'string', 'default' => null]
- ],
- self::TYPE_HYSTERIA => [
- 'version' => ['type' => 'integer', 'default' => 2],
- 'bandwidth' => [
- 'type' => 'object',
- 'fields' => [
- 'up' => ['type' => 'integer', 'default' => null],
- 'down' => ['type' => 'integer', 'default' => null]
- ]
- ],
- 'obfs' => [
- 'type' => 'object',
- 'fields' => [
- 'open' => ['type' => 'boolean', 'default' => false],
- 'type' => ['type' => 'string', 'default' => 'salamander'],
- 'password' => ['type' => 'string', 'default' => null]
- ]
- ],
- 'tls' => [
- 'type' => 'object',
- 'fields' => [
- 'server_name' => ['type' => 'string', 'default' => null],
- 'allow_insecure' => ['type' => 'boolean', 'default' => false]
- ]
- ],
- 'hop_interval' => ['type' => 'integer', 'default' => null]
- ],
- self::TYPE_TUIC => [
- 'version' => ['type' => 'integer', 'default' => 5],
- 'congestion_control' => ['type' => 'string', 'default' => 'cubic'],
- 'alpn' => ['type' => 'array', 'default' => ['h3']],
- 'udp_relay_mode' => ['type' => 'string', 'default' => 'native'],
- 'tls' => [
- 'type' => 'object',
- 'fields' => [
- 'server_name' => ['type' => 'string', 'default' => null],
- 'allow_insecure' => ['type' => 'boolean', 'default' => false]
- ]
- ]
- ],
- self::TYPE_ANYTLS => [
- 'padding_scheme' => [
- 'type' => 'array',
- 'default' => [
- "stop=8",
- "0=30-30",
- "1=100-400",
- "2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000",
- "3=9-9,500-1000",
- "4=500-1000",
- "5=500-1000",
- "6=500-1000",
- "7=500-1000"
- ]
- ],
- 'tls' => [
- 'type' => 'object',
- 'fields' => [
- 'server_name' => ['type' => 'string', 'default' => null],
- 'allow_insecure' => ['type' => 'boolean', 'default' => false]
- ]
- ]
- ],
- self::TYPE_SOCKS => [
- 'tls' => ['type' => 'integer', 'default' => 0],
- 'tls_settings' => [
- 'type' => 'object',
- 'fields' => [
- 'allow_insecure' => ['type' => 'boolean', 'default' => false]
- ]
- ]
- ],
- self::TYPE_NAIVE => [
- 'tls' => ['type' => 'integer', 'default' => 0],
- 'tls_settings' => ['type' => 'array', 'default' => null]
- ],
- self::TYPE_HTTP => [
- 'tls' => ['type' => 'integer', 'default' => 0],
- 'tls_settings' => [
- 'type' => 'object',
- 'fields' => [
- 'allow_insecure' => ['type' => 'boolean', 'default' => false],
- 'server_name' => ['type' => 'string', 'default' => null]
- ]
- ]
- ],
- self::TYPE_MIERU => [
- 'transport' => ['type' => 'string', 'default' => 'TCP'],
- 'traffic_pattern' => ['type' => 'string', 'default' => ''],
- ...self::MULTIPLEX_CONFIGURATION,
- ]
- ];
-
- private function castValueWithConfig($value, array $config)
- {
- if ($value === null && $config['type'] !== 'object') {
- return $config['default'] ?? null;
- }
-
- return match ($config['type']) {
- 'integer' => (int) $value,
- 'boolean' => (bool) $value,
- 'string' => (string) $value,
- 'array' => (array) $value,
- 'object' => is_array($value) ?
- $this->castSettingsWithConfig($value, $config['fields']) :
- $config['default'] ?? null,
- default => $value
- };
- }
-
- private function castSettingsWithConfig(array $settings, array $configs): array
- {
- $result = [];
- foreach ($configs as $key => $config) {
- $value = $settings[$key] ?? null;
- $result[$key] = $this->castValueWithConfig($value, $config);
- }
- return $result;
- }
-
- public function getProtocolSettingsAttribute($value)
- {
- $settings = json_decode($value, true) ?? [];
- $configs = self::PROTOCOL_CONFIGURATIONS[$this->type] ?? [];
- return $this->castSettingsWithConfig($settings, $configs);
- }
-
- public function setProtocolSettingsAttribute($value)
- {
- if (is_string($value)) {
- $value = json_decode($value, true);
- }
-
- $configs = self::PROTOCOL_CONFIGURATIONS[$this->type] ?? [];
- $castedSettings = $this->castSettingsWithConfig($value ?? [], $configs);
-
- $this->attributes['protocol_settings'] = json_encode($castedSettings);
- }
-
- public function generateServerPassword(User $user): string
- {
- if ($this->type !== self::TYPE_SHADOWSOCKS) {
- return $user->uuid;
- }
-
-
- $cipher = data_get($this, 'protocol_settings.cipher');
- if (!$cipher || !isset(self::CIPHER_CONFIGURATIONS[$cipher])) {
- return $user->uuid;
- }
-
- $config = self::CIPHER_CONFIGURATIONS[$cipher];
- // Use parent's created_at if this is a child node
- $serverCreatedAt = $this->parent_id ? $this->parent->created_at : $this->created_at;
- $serverKey = Helper::getServerKey($serverCreatedAt, $config['serverKeySize']);
- $userKey = Helper::uuidToBase64($user->uuid, $config['userKeySize']);
- return "{$serverKey}:{$userKey}";
- }
-
- public static function normalizeType(?string $type): string | null
- {
- return $type ? strtolower(self::TYPE_ALIASES[$type] ?? $type) : null;
- }
-
- public static function isValidType(?string $type): bool
- {
- return $type ? in_array(self::normalizeType($type), self::VALID_TYPES, true) : true;
- }
-
- public function getAvailableStatusAttribute(): int
- {
- $now = time();
- if (!$this->last_check_at || ($now - self::CHECK_INTERVAL) >= $this->last_check_at) {
- return self::STATUS_OFFLINE;
- }
- if (!$this->last_push_at || ($now - self::CHECK_INTERVAL) >= $this->last_push_at) {
- return self::STATUS_ONLINE_NO_PUSH;
- }
- return self::STATUS_ONLINE;
- }
-
- public function parent(): BelongsTo
- {
- return $this->belongsTo(self::class, 'parent_id', 'id');
- }
-
- public function stats(): HasMany
- {
- return $this->hasMany(StatServer::class, 'server_id', 'id');
- }
-
- public function groups()
- {
- return ServerGroup::whereIn('id', $this->group_ids)->get();
- }
-
- public function routes()
- {
- return ServerRoute::whereIn('id', $this->route_ids)->get();
- }
-
- /**
- * 最后检查时间访问器
- */
- protected function lastCheckAt(): Attribute
- {
- return Attribute::make(
- get: function () {
- $type = strtoupper($this->type);
- $serverId = $this->parent_id ?: $this->id;
- return Cache::get(CacheKey::get("SERVER_{$type}_LAST_CHECK_AT", $serverId));
- }
- );
- }
-
- /**
- * 最后推送时间访问器
- */
- protected function lastPushAt(): Attribute
- {
- return Attribute::make(
- get: function () {
- $type = strtoupper($this->type);
- $serverId = $this->parent_id ?: $this->id;
- return Cache::get(CacheKey::get("SERVER_{$type}_LAST_PUSH_AT", $serverId));
- }
- );
- }
-
- /**
- * 在线用户数访问器
- */
- protected function online(): Attribute
- {
- return Attribute::make(
- get: function () {
- $type = strtoupper($this->type);
- $serverId = $this->parent_id ?: $this->id;
- return Cache::get(CacheKey::get("SERVER_{$type}_ONLINE_USER", $serverId)) ?? 0;
- }
- );
- }
-
- /**
- * 是否在线访问器
- */
- protected function isOnline(): Attribute
- {
- return Attribute::make(
- get: function () {
- return (time() - 300 > $this->last_check_at) ? 0 : 1;
- }
- );
- }
-
- /**
- * 缓存键访问器
- */
- protected function cacheKey(): Attribute
- {
- return Attribute::make(
- get: function () {
- return "{$this->type}-{$this->id}-{$this->updated_at}-{$this->is_online}";
- }
- );
- }
-
- /**
- * 服务器密钥访问器
- */
- protected function serverKey(): Attribute
- {
- return Attribute::make(
- get: function () {
- if ($this->type === self::TYPE_SHADOWSOCKS) {
- return Helper::getServerKey($this->created_at, 16);
- }
- return null;
- }
- );
- }
-
- /**
- * 指标指标访问器
- */
- protected function metrics(): Attribute
- {
- return Attribute::make(
- get: function () {
- $type = strtoupper($this->type);
- $serverId = $this->parent_id ?: $this->id;
- return Cache::get(CacheKey::get("SERVER_{$type}_METRICS", $serverId));
- }
- );
- }
-
- /**
- * 在线连接数访问器
- */
- protected function onlineConn(): Attribute
- {
- return Attribute::make(
- get: function () {
- return $this->metrics['active_connections'] ?? 0;
- }
- );
- }
-
- /**
- * 负载状态访问器
- */
- protected function loadStatus(): Attribute
- {
- return Attribute::make(
- get: function () {
- $type = strtoupper($this->type);
- $serverId = $this->parent_id ?: $this->id;
- return Cache::get(CacheKey::get("SERVER_{$type}_LOAD_STATUS", $serverId));
- }
- );
- }
-
- public function getCurrentRate(): float
- {
- if (!$this->rate_time_enable) {
- return (float) $this->rate;
- }
-
- $now = now()->format('H:i');
- $ranges = $this->rate_time_ranges ?? [];
- $matchedRange = collect($ranges)
- ->first(fn($range) => $now >= $range['start'] && $now <= $range['end']);
-
- return $matchedRange ? (float) $matchedRange['rate'] : (float) $this->rate;
- }
-}
diff --git a/Xboard/app/Models/ServerGroup.php b/Xboard/app/Models/ServerGroup.php
deleted file mode 100644
index 57f3514..0000000
--- a/Xboard/app/Models/ServerGroup.php
+++ /dev/null
@@ -1,46 +0,0 @@
- 'timestamp',
- 'updated_at' => 'timestamp'
- ];
-
- public function users(): HasMany
- {
- return $this->hasMany(User::class, 'group_id', 'id');
- }
-
- public function servers()
- {
- return Server::whereJsonContains('group_ids', (string) $this->id)->get();
- }
-
- /**
- * 获取服务器数量
- */
- protected function serverCount(): Attribute
- {
- return Attribute::make(
- get: fn () => Server::whereJsonContains('group_ids', (string) $this->id)->count(),
- );
- }
-}
diff --git a/Xboard/app/Models/ServerLog.php b/Xboard/app/Models/ServerLog.php
deleted file mode 100644
index ef3590c..0000000
--- a/Xboard/app/Models/ServerLog.php
+++ /dev/null
@@ -1,16 +0,0 @@
- 'timestamp',
- 'updated_at' => 'timestamp'
- ];
-}
diff --git a/Xboard/app/Models/ServerRoute.php b/Xboard/app/Models/ServerRoute.php
deleted file mode 100644
index 024a7e4..0000000
--- a/Xboard/app/Models/ServerRoute.php
+++ /dev/null
@@ -1,17 +0,0 @@
- 'timestamp',
- 'updated_at' => 'timestamp',
- 'match' => 'array'
- ];
-}
diff --git a/Xboard/app/Models/ServerStat.php b/Xboard/app/Models/ServerStat.php
deleted file mode 100644
index 5006ded..0000000
--- a/Xboard/app/Models/ServerStat.php
+++ /dev/null
@@ -1,16 +0,0 @@
- 'timestamp',
- 'updated_at' => 'timestamp'
- ];
-}
diff --git a/Xboard/app/Models/Setting.php b/Xboard/app/Models/Setting.php
deleted file mode 100644
index b24a471..0000000
--- a/Xboard/app/Models/Setting.php
+++ /dev/null
@@ -1,68 +0,0 @@
- 'string',
- 'value' => 'string',
- ];
-
- /**
- * 获取实际内容值
- */
- public function getContentValue()
- {
- $rawValue = $this->attributes['value'] ?? null;
-
- if ($rawValue === null) {
- return null;
- }
-
- // 如果已经是数组,直接返回
- if (is_array($rawValue)) {
- return $rawValue;
- }
-
- // 如果是数字字符串,返回原值
- if (is_numeric($rawValue) && !preg_match('/[^\d.]/', $rawValue)) {
- return $rawValue;
- }
-
- // 尝试解析 JSON
- if (is_string($rawValue)) {
- $decodedValue = json_decode($rawValue, true);
- if (json_last_error() === JSON_ERROR_NONE) {
- return $decodedValue;
- }
- }
-
- return $rawValue;
- }
-
- /**
- * 兼容性:保持原有的 value 访问器
- */
- public function getValueAttribute($value)
- {
- return $this->getContentValue();
- }
-
- /**
- * 创建或更新设置项
- */
- public static function createOrUpdate(string $name, $value): self
- {
- $processedValue = is_array($value) ? json_encode($value) : $value;
-
- return self::updateOrCreate(
- ['name' => $name],
- ['value' => $processedValue]
- );
- }
-}
diff --git a/Xboard/app/Models/Stat.php b/Xboard/app/Models/Stat.php
deleted file mode 100644
index b0ed9ec..0000000
--- a/Xboard/app/Models/Stat.php
+++ /dev/null
@@ -1,16 +0,0 @@
- 'timestamp',
- 'updated_at' => 'timestamp'
- ];
-}
diff --git a/Xboard/app/Models/StatServer.php b/Xboard/app/Models/StatServer.php
deleted file mode 100644
index efa1fc6..0000000
--- a/Xboard/app/Models/StatServer.php
+++ /dev/null
@@ -1,33 +0,0 @@
- 'timestamp',
- 'updated_at' => 'timestamp'
- ];
-
- public function server()
- {
- return $this->belongsTo(Server::class, 'server_id');
- }
-}
diff --git a/Xboard/app/Models/StatUser.php b/Xboard/app/Models/StatUser.php
deleted file mode 100644
index a956bd7..0000000
--- a/Xboard/app/Models/StatUser.php
+++ /dev/null
@@ -1,28 +0,0 @@
- 'timestamp',
- 'updated_at' => 'timestamp'
- ];
-}
diff --git a/Xboard/app/Models/SubscribeTemplate.php b/Xboard/app/Models/SubscribeTemplate.php
deleted file mode 100644
index fec76e0..0000000
--- a/Xboard/app/Models/SubscribeTemplate.php
+++ /dev/null
@@ -1,46 +0,0 @@
- 'string',
- 'content' => 'string',
- ];
-
- private static string $cachePrefix = 'subscribe_template:';
-
- public static function getContent(string $name): ?string
- {
- $cacheKey = self::$cachePrefix . $name;
-
- return Cache::store('redis')->remember($cacheKey, 3600, function () use ($name) {
- return self::where('name', $name)->value('content');
- });
- }
-
- public static function setContent(string $name, ?string $content): void
- {
- self::updateOrCreate(
- ['name' => $name],
- ['content' => $content]
- );
- Cache::store('redis')->forget(self::$cachePrefix . $name);
- }
-
- public static function getAllContents(): array
- {
- return self::pluck('content', 'name')->toArray();
- }
-
- public static function flushCache(string $name): void
- {
- Cache::store('redis')->forget(self::$cachePrefix . $name);
- }
-}
diff --git a/Xboard/app/Models/Ticket.php b/Xboard/app/Models/Ticket.php
deleted file mode 100644
index 86ba9b6..0000000
--- a/Xboard/app/Models/Ticket.php
+++ /dev/null
@@ -1,60 +0,0 @@
- $messages 关联的工单消息
- */
-class Ticket extends Model
-{
- protected $table = 'v2_ticket';
- protected $dateFormat = 'U';
- protected $guarded = ['id'];
- protected $casts = [
- 'created_at' => 'timestamp',
- 'updated_at' => 'timestamp'
- ];
-
- const STATUS_OPENING = 0;
- const STATUS_CLOSED = 1;
- public static $statusMap = [
- self::STATUS_OPENING => '开启',
- self::STATUS_CLOSED => '关闭'
- ];
-
- public function user(): BelongsTo
- {
- return $this->belongsTo(User::class, 'user_id', 'id');
- }
-
- /**
- * 关联的工单消息
- */
- public function messages(): HasMany
- {
- return $this->hasMany(TicketMessage::class, 'ticket_id', 'id');
- }
-
- // 即将删除
- public function message(): HasMany
- {
- return $this->hasMany(TicketMessage::class, 'ticket_id', 'id');
- }
-}
diff --git a/Xboard/app/Models/TicketMessage.php b/Xboard/app/Models/TicketMessage.php
deleted file mode 100644
index c9d45a8..0000000
--- a/Xboard/app/Models/TicketMessage.php
+++ /dev/null
@@ -1,56 +0,0 @@
- 'timestamp',
- 'updated_at' => 'timestamp'
- ];
-
- protected $appends = ['is_from_user', 'is_from_admin'];
-
- /**
- * 关联的工单
- */
- public function ticket(): BelongsTo
- {
- return $this->belongsTo(Ticket::class, 'ticket_id', 'id');
- }
-
- /**
- * 判断消息是否由工单发起人发送
- */
- public function getIsFromUserAttribute(): bool
- {
- return $this->ticket->user_id === $this->user_id;
- }
-
- /**
- * 判断消息是否由管理员发送
- */
- public function getIsFromAdminAttribute(): bool
- {
- return $this->ticket->user_id !== $this->user_id;
- }
-}
diff --git a/Xboard/app/Models/TrafficResetLog.php b/Xboard/app/Models/TrafficResetLog.php
deleted file mode 100644
index 3b05a84..0000000
--- a/Xboard/app/Models/TrafficResetLog.php
+++ /dev/null
@@ -1,149 +0,0 @@
- 'datetime',
- 'metadata' => 'array',
- 'created_at' => 'datetime',
- 'updated_at' => 'datetime',
- ];
-
- // 重置类型常量
- public const TYPE_MONTHLY = 'monthly';
- public const TYPE_FIRST_DAY_MONTH = 'first_day_month';
- public const TYPE_YEARLY = 'yearly';
- public const TYPE_FIRST_DAY_YEAR = 'first_day_year';
- public const TYPE_MANUAL = 'manual';
- public const TYPE_PURCHASE = 'purchase';
-
- // 触发来源常量
- public const SOURCE_AUTO = 'auto';
- public const SOURCE_MANUAL = 'manual';
- public const SOURCE_API = 'api';
- public const SOURCE_CRON = 'cron';
- public const SOURCE_USER_ACCESS = 'user_access';
- public const SOURCE_ORDER = 'order';
- public const SOURCE_GIFT_CARD = 'gift_card';
-
- /**
- * 获取重置类型的多语言名称
- */
- public static function getResetTypeNames(): array
- {
- return [
- self::TYPE_MONTHLY => __('traffic_reset.reset_type.monthly'),
- self::TYPE_FIRST_DAY_MONTH => __('traffic_reset.reset_type.first_day_month'),
- self::TYPE_YEARLY => __('traffic_reset.reset_type.yearly'),
- self::TYPE_FIRST_DAY_YEAR => __('traffic_reset.reset_type.first_day_year'),
- self::TYPE_MANUAL => __('traffic_reset.reset_type.manual'),
- self::TYPE_PURCHASE => __('traffic_reset.reset_type.purchase'),
- ];
- }
-
- /**
- * 获取触发来源的多语言名称
- */
- public static function getSourceNames(): array
- {
- return [
- self::SOURCE_AUTO => __('traffic_reset.source.auto'),
- self::SOURCE_MANUAL => __('traffic_reset.source.manual'),
- self::SOURCE_API => __('traffic_reset.source.api'),
- self::SOURCE_CRON => __('traffic_reset.source.cron'),
- self::SOURCE_USER_ACCESS => __('traffic_reset.source.user_access'),
- ];
- }
-
- /**
- * 关联用户
- */
- public function user(): BelongsTo
- {
- return $this->belongsTo(User::class, 'user_id', 'id');
- }
-
- /**
- * 获取重置类型名称
- */
- public function getResetTypeName(): string
- {
- return self::getResetTypeNames()[$this->reset_type] ?? $this->reset_type;
- }
-
- /**
- * 获取触发来源名称
- */
- public function getSourceName(): string
- {
- return self::getSourceNames()[$this->trigger_source] ?? $this->trigger_source;
- }
-
- /**
- * 获取重置的流量差值
- */
- public function getTrafficDiff(): array
- {
- return [
- 'upload_diff' => $this->new_upload - $this->old_upload,
- 'download_diff' => $this->new_download - $this->old_download,
- 'total_diff' => $this->new_total - $this->old_total,
- ];
- }
-
- /**
- * 格式化流量大小
- */
- public function formatTraffic(int $bytes): string
- {
- $units = ['B', 'KB', 'MB', 'GB', 'TB'];
- $bytes = max($bytes, 0);
- $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
- $pow = min($pow, count($units) - 1);
-
- $bytes /= (1 << (10 * $pow));
-
- return round($bytes, 2) . ' ' . $units[$pow];
- }
-}
\ No newline at end of file
diff --git a/Xboard/app/Models/User.php b/Xboard/app/Models/User.php
deleted file mode 100644
index a826e4a..0000000
--- a/Xboard/app/Models/User.php
+++ /dev/null
@@ -1,215 +0,0 @@
- $codes 邀请码列表
- * @property-read \Illuminate\Database\Eloquent\Collection $orders 订单列表
- * @property-read \Illuminate\Database\Eloquent\Collection $stat 统计信息
- * @property-read \Illuminate\Database\Eloquent\Collection $tickets 工单列表
- * @property-read \Illuminate\Database\Eloquent\Collection $trafficResetLogs 流量重置记录
- * @property-read User|null $parent 父账户
- * @property-read string $subscribe_url 订阅链接(动态生成)
- */
-class User extends Authenticatable
-{
- use HasApiTokens;
- protected $table = 'v2_user';
- protected $dateFormat = 'U';
- protected $guarded = ['id'];
- protected $casts = [
- 'created_at' => 'timestamp',
- 'updated_at' => 'timestamp',
- 'banned' => 'boolean',
- 'is_admin' => 'boolean',
- 'is_staff' => 'boolean',
- 'remind_expire' => 'boolean',
- 'remind_traffic' => 'boolean',
- 'commission_auto_check' => 'boolean',
- 'commission_rate' => 'float',
- 'next_reset_at' => 'timestamp',
- 'last_reset_at' => 'timestamp',
- ];
- protected $hidden = ['password'];
-
- public const COMMISSION_TYPE_SYSTEM = 0;
- public const COMMISSION_TYPE_PERIOD = 1;
- public const COMMISSION_TYPE_ONETIME = 2;
- protected function email(): Attribute
- {
- return Attribute::make(
- set: fn (string $value) => strtolower(trim($value)),
- );
- }
-
- /**
- * 按邮箱查询(大小写不敏感,兼容所有数据库)
- */
- public function scopeByEmail(Builder $query, string $email): Builder
- {
- return $query->where('email', strtolower(trim($email)));
- }
-
- // 获取邀请人信息
- public function invite_user(): BelongsTo
- {
- return $this->belongsTo(self::class, 'invite_user_id', 'id');
- }
-
- /**
- * 获取用户订阅计划
- * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
- */
- public function plan(): BelongsTo
- {
- return $this->belongsTo(Plan::class, 'plan_id', 'id');
- }
-
- public function group(): BelongsTo
- {
- return $this->belongsTo(ServerGroup::class, 'group_id', 'id');
- }
-
- // 获取用户邀请码列表
- public function codes(): HasMany
- {
- return $this->hasMany(InviteCode::class, 'user_id', 'id');
- }
-
- public function orders(): HasMany
- {
- return $this->hasMany(Order::class, 'user_id', 'id');
- }
-
- public function stat(): HasMany
- {
- return $this->hasMany(StatUser::class, 'user_id', 'id');
- }
-
- // 关联工单列表
- public function tickets(): HasMany
- {
- return $this->hasMany(Ticket::class, 'user_id', 'id');
- }
-
- public function parent(): BelongsTo
- {
- return $this->belongsTo(self::class, 'parent_id', 'id');
- }
-
- /**
- * 关联流量重置记录
- */
- public function trafficResetLogs(): HasMany
- {
- return $this->hasMany(TrafficResetLog::class, 'user_id', 'id');
- }
-
- /**
- * 检查用户是否处于活跃状态
- */
- public function isActive(): bool
- {
- return !$this->banned &&
- ($this->expired_at === null || $this->expired_at > time()) &&
- $this->plan_id !== null;
- }
-
- /**
- * 检查用户是否可用节点流量且充足
- */
- public function isAvailable(): bool
- {
- return $this->isActive() && $this->getRemainingTraffic() > 0;
- }
-
- /**
- * 检查是否需要重置流量
- */
- public function shouldResetTraffic(): bool
- {
- return $this->isActive() &&
- $this->next_reset_at !== null &&
- $this->next_reset_at <= time();
- }
-
- /**
- * 获取总使用流量
- */
- public function getTotalUsedTraffic(): int
- {
- return ($this->u ?? 0) + ($this->d ?? 0);
- }
-
- /**
- * 获取剩余流量
- */
- public function getRemainingTraffic(): int
- {
- $used = $this->getTotalUsedTraffic();
- $total = $this->transfer_enable ?? 0;
- return max(0, $total - $used);
- }
-
- /**
- * 获取流量使用百分比
- */
- public function getTrafficUsagePercentage(): float
- {
- $total = $this->transfer_enable ?? 0;
- if ($total <= 0) {
- return 0;
- }
-
- $used = $this->getTotalUsedTraffic();
- return min(100, ($used / $total) * 100);
- }
-}
diff --git a/Xboard/app/Observers/PlanObserver.php b/Xboard/app/Observers/PlanObserver.php
deleted file mode 100644
index 19dcef4..0000000
--- a/Xboard/app/Observers/PlanObserver.php
+++ /dev/null
@@ -1,35 +0,0 @@
-isDirty('reset_traffic_method')) {
- return;
- }
- $trafficResetService = app(TrafficResetService::class);
- User::where('plan_id', $plan->id)
- ->where('banned', 0)
- ->where(function ($query) {
- $query->where('expired_at', '>', time())
- ->orWhereNull('expired_at');
- })
- ->lazyById(500)
- ->each(function (User $user) use ($trafficResetService) {
- $nextResetTime = $trafficResetService->calculateNextResetTime($user);
- $user->update([
- 'next_reset_at' => $nextResetTime?->timestamp,
- ]);
- });
- }
-}
-
diff --git a/Xboard/app/Observers/ServerObserver.php b/Xboard/app/Observers/ServerObserver.php
deleted file mode 100644
index 0f1ce9d..0000000
--- a/Xboard/app/Observers/ServerObserver.php
+++ /dev/null
@@ -1,37 +0,0 @@
-isDirty([
- 'group_ids',
- ])
- ) {
- NodeSyncService::notifyUsersUpdatedByGroup($server->id);
- } else if (
- $server->isDirty([
- 'server_port',
- 'protocol_settings',
- 'type',
- 'route_ids',
- 'custom_outbounds',
- 'custom_routes',
- 'cert_config',
- ])
- ) {
- NodeSyncService::notifyConfigUpdated($server->id);
- }
- }
-
- public function deleted(Server $server): void
- {
- NodeSyncService::notifyConfigUpdated($server->id);
- }
-}
diff --git a/Xboard/app/Observers/ServerRouteObserver.php b/Xboard/app/Observers/ServerRouteObserver.php
deleted file mode 100644
index f8457d7..0000000
--- a/Xboard/app/Observers/ServerRouteObserver.php
+++ /dev/null
@@ -1,31 +0,0 @@
-notifyAffectedNodes($route->id);
- }
-
- public function deleted(ServerRoute $route): void
- {
- $this->notifyAffectedNodes($route->id);
- }
-
- private function notifyAffectedNodes(int $routeId): void
- {
- $servers = Server::where('show', 1)->get()->filter(
- fn ($s) => in_array($routeId, $s->route_ids ?? [])
- );
-
- foreach ($servers as $server) {
- NodeSyncService::notifyConfigUpdated($server->id);
- }
- }
-}
diff --git a/Xboard/app/Observers/UserObserver.php b/Xboard/app/Observers/UserObserver.php
deleted file mode 100644
index ffa74d0..0000000
--- a/Xboard/app/Observers/UserObserver.php
+++ /dev/null
@@ -1,53 +0,0 @@
-isDirty(['plan_id', 'expired_at'])) {
- $this->recalculateNextResetAt($user);
- }
-
- if ($user->isDirty(['group_id', 'uuid', 'speed_limit', 'device_limit', 'banned', 'expired_at', 'transfer_enable', 'u', 'd', 'plan_id'])) {
- $oldGroupId = $user->isDirty('group_id') ? $user->getOriginal('group_id') : null;
- NodeUserSyncJob::dispatch($user->id, 'updated', $oldGroupId);
- }
- }
-
- public function created(User $user): void
- {
- $this->recalculateNextResetAt($user);
- NodeUserSyncJob::dispatch($user->id, 'created');
- }
-
- public function deleted(User $user): void
- {
- if ($user->group_id) {
- NodeUserSyncJob::dispatch($user->id, 'deleted', $user->group_id);
- }
- }
-
- /**
- * 根据当前用户状态重新计算 next_reset_at
- */
- private function recalculateNextResetAt(User $user): void
- {
- $user->refresh();
- User::withoutEvents(function () use ($user) {
- $nextResetTime = $this->trafficResetService->calculateNextResetTime($user);
- $user->next_reset_at = $nextResetTime?->timestamp;
- $user->save();
- });
- }
-}
\ No newline at end of file
diff --git a/Xboard/app/Protocols/Clash.php b/Xboard/app/Protocols/Clash.php
deleted file mode 100644
index f84d99f..0000000
--- a/Xboard/app/Protocols/Clash.php
+++ /dev/null
@@ -1,332 +0,0 @@
-servers;
- $user = $this->user;
- $appName = admin_setting('app_name', 'XBoard');
-
- // 优先从数据库配置中获取模板
- $template = subscribe_template('clash');
-
- $config = Yaml::parse($template);
- $proxy = [];
- $proxies = [];
-
- foreach ($servers as $item) {
-
- if (
- $item['type'] === Server::TYPE_SHADOWSOCKS
- && in_array(data_get($item['protocol_settings'], 'cipher'), [
- 'aes-128-gcm',
- 'aes-192-gcm',
- 'aes-256-gcm',
- 'chacha20-ietf-poly1305'
- ])
- ) {
- array_push($proxy, self::buildShadowsocks($item['password'], $item));
- array_push($proxies, $item['name']);
- }
- if ($item['type'] === Server::TYPE_VMESS) {
- array_push($proxy, self::buildVmess($item['password'], $item));
- array_push($proxies, $item['name']);
- }
- if ($item['type'] === Server::TYPE_TROJAN) {
- array_push($proxy, self::buildTrojan($item['password'], $item));
- array_push($proxies, $item['name']);
- }
- if ($item['type'] === Server::TYPE_SOCKS) {
- array_push($proxy, self::buildSocks5($item['password'], $item));
- array_push($proxies, $item['name']);
- }
- if ($item['type'] === Server::TYPE_HTTP) {
- array_push($proxy, self::buildHttp($item['password'], $item));
- array_push($proxies, $item['name']);
- }
- }
-
- $config['proxies'] = array_merge($config['proxies'] ? $config['proxies'] : [], $proxy);
- foreach ($config['proxy-groups'] as $k => $v) {
- if (!is_array($config['proxy-groups'][$k]['proxies']))
- $config['proxy-groups'][$k]['proxies'] = [];
- $isFilter = false;
- foreach ($config['proxy-groups'][$k]['proxies'] as $src) {
- foreach ($proxies as $dst) {
- if (!$this->isRegex($src))
- continue;
- $isFilter = true;
- $config['proxy-groups'][$k]['proxies'] = array_values(array_diff($config['proxy-groups'][$k]['proxies'], [$src]));
- if ($this->isMatch($src, $dst)) {
- array_push($config['proxy-groups'][$k]['proxies'], $dst);
- }
- }
- if ($isFilter)
- continue;
- }
- if ($isFilter)
- continue;
- $config['proxy-groups'][$k]['proxies'] = array_merge($config['proxy-groups'][$k]['proxies'], $proxies);
- }
-
- $config['proxy-groups'] = array_filter($config['proxy-groups'], function ($group) {
- return $group['proxies'];
- });
- $config['proxy-groups'] = array_values($config['proxy-groups']);
-
- $config = $this->buildRules($config);
-
-
- $yaml = Yaml::dump($config, 2, 4, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE);
- $yaml = str_replace('$app_name', admin_setting('app_name', 'XBoard'), $yaml);
- return response($yaml)
- ->header('content-type', 'text/yaml')
- ->header('subscription-userinfo', "upload={$user['u']}; download={$user['d']}; total={$user['transfer_enable']}; expire={$user['expired_at']}")
- ->header('profile-update-interval', '24')
- ->header('content-disposition', 'attachment;filename*=UTF-8\'\'' . rawurlencode($appName))
- ->header('profile-web-page-url', admin_setting('app_url'));
- }
-
- /**
- * Build the rules for Clash.
- */
- public function buildRules($config)
- {
- // Force the current subscription domain to be a direct rule
- $subsDomain = request()->header('Host');
- if ($subsDomain) {
- array_unshift($config['rules'], "DOMAIN,{$subsDomain},DIRECT");
- }
-
- return $config;
- }
-
- public static function buildShadowsocks($uuid, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $array = [];
- $array['name'] = $server['name'];
- $array['type'] = 'ss';
- $array['server'] = $server['host'];
- $array['port'] = $server['port'];
- $array['cipher'] = data_get($protocol_settings, 'cipher');
- $array['password'] = $uuid;
- $array['udp'] = true;
- if (data_get($protocol_settings, 'plugin') && data_get($protocol_settings, 'plugin_opts')) {
- $plugin = data_get($protocol_settings, 'plugin');
- $pluginOpts = data_get($protocol_settings, 'plugin_opts', '');
- $array['plugin'] = $plugin;
-
- // 解析插件选项
- $parsedOpts = collect(explode(';', $pluginOpts))
- ->filter()
- ->mapWithKeys(function ($pair) {
- if (!str_contains($pair, '=')) {
- return [];
- }
- [$key, $value] = explode('=', $pair, 2);
- return [trim($key) => trim($value)];
- })
- ->all();
-
- // 根据插件类型进行字段映射
- switch ($plugin) {
- case 'obfs':
- $array['plugin-opts'] = [
- 'mode' => $parsedOpts['obfs'] ?? data_get($protocol_settings, 'obfs', 'http'),
- 'host' => $parsedOpts['obfs-host'] ?? data_get($protocol_settings, 'obfs_settings.host', ''),
- ];
-
- if (isset($parsedOpts['path'])) {
- $array['plugin-opts']['path'] = $parsedOpts['path'];
- }
- break;
- case 'v2ray-plugin':
- $array['plugin-opts'] = [
- 'mode' => $parsedOpts['mode'] ?? 'websocket',
- 'tls' => isset($parsedOpts['tls']) && $parsedOpts['tls'] == 'true',
- 'host' => $parsedOpts['host'] ?? '',
- 'path' => $parsedOpts['path'] ?? '/',
- ];
- break;
- default:
- // 对于其他插件,直接使用解析出的键值对
- $array['plugin-opts'] = $parsedOpts;
- }
- }
- return $array;
- }
-
- public static function buildVmess($uuid, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $array = [];
- $array['name'] = $server['name'];
- $array['type'] = 'vmess';
- $array['server'] = $server['host'];
- $array['port'] = $server['port'];
- $array['uuid'] = $uuid;
- $array['alterId'] = 0;
- $array['cipher'] = 'auto';
- $array['udp'] = true;
-
- if (data_get($protocol_settings, 'tls')) {
- $array['tls'] = true;
- $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls_settings.allow_insecure');
- if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) {
- $array['servername'] = $serverName;
- }
- }
-
- switch (data_get($protocol_settings, 'network')) {
- case 'tcp':
- $headerType = data_get($protocol_settings, 'network_settings.header.type', 'none');
- $array['network'] = ($headerType === 'http') ? 'http' : 'tcp';
- if ($headerType === 'http') {
- if ($httpOpts = array_filter([
- 'headers' => data_get($protocol_settings, 'network_settings.header.request.headers'),
- 'path' => data_get($protocol_settings, 'network_settings.header.request.path', ['/'])
- ])) {
- $array['http-opts'] = $httpOpts;
- }
- }
- break;
- case 'ws':
- $array['network'] = 'ws';
- if ($path = data_get($protocol_settings, 'network_settings.path'))
- $array['ws-opts']['path'] = $path;
- if ($host = data_get($protocol_settings, 'network_settings.headers.Host'))
- $array['ws-opts']['headers'] = ['Host' => $host];
- break;
- case 'grpc':
- $array['network'] = 'grpc';
- if ($serviceName = data_get($protocol_settings, 'network_settings.serviceName'))
- $array['grpc-opts']['grpc-service-name'] = $serviceName;
- break;
- default:
- break;
- }
- return $array;
- }
-
- public static function buildTrojan($password, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $array = [];
- $array['name'] = $server['name'];
- $array['type'] = 'trojan';
- $array['server'] = $server['host'];
- $array['port'] = $server['port'];
- $array['password'] = $password;
- $array['udp'] = true;
- if ($serverName = data_get($protocol_settings, 'server_name')) {
- $array['sni'] = $serverName;
- }
- $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'allow_insecure');
-
- switch (data_get($protocol_settings, 'network')) {
- case 'tcp':
- $array['network'] = 'tcp';
- break;
- case 'ws':
- $array['network'] = 'ws';
- if ($path = data_get($protocol_settings, 'network_settings.path'))
- $array['ws-opts']['path'] = $path;
- if ($host = data_get($protocol_settings, 'network_settings.headers.Host'))
- $array['ws-opts']['headers'] = ['Host' => $host];
- break;
- case 'grpc':
- $array['network'] = 'grpc';
- if ($serviceName = data_get($protocol_settings, 'network_settings.serviceName'))
- $array['grpc-opts']['grpc-service-name'] = $serviceName;
- break;
- default:
- $array['network'] = 'tcp';
- break;
- }
- return $array;
- }
-
- public static function buildSocks5($password, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $array = [];
- $array['name'] = $server['name'];
- $array['type'] = 'socks5';
- $array['server'] = $server['host'];
- $array['port'] = $server['port'];
- $array['udp'] = true;
-
- $array['username'] = $password;
- $array['password'] = $password;
-
- // TLS 配置
- if (data_get($protocol_settings, 'tls')) {
- $array['tls'] = true;
- $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls_settings.allow_insecure', false);
- }
-
- return $array;
- }
-
- public static function buildHttp($password, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $array = [];
- $array['name'] = $server['name'];
- $array['type'] = 'http';
- $array['server'] = $server['host'];
- $array['port'] = $server['port'];
-
- $array['username'] = $password;
- $array['password'] = $password;
-
- // TLS 配置
- if (data_get($protocol_settings, 'tls')) {
- $array['tls'] = true;
- $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls_settings.allow_insecure', false);
- }
-
- return $array;
- }
-
- private function isMatch($exp, $str)
- {
- try {
- return preg_match($exp, $str) === 1;
- } catch (\Exception $e) {
- return false;
- }
- }
-
- private function isRegex($exp)
- {
- if (empty($exp)) {
- return false;
- }
- try {
- return preg_match($exp, '') !== false;
- } catch (\Exception $e) {
- return false;
- }
- }
-}
diff --git a/Xboard/app/Protocols/ClashMeta.php b/Xboard/app/Protocols/ClashMeta.php
deleted file mode 100644
index feda7d0..0000000
--- a/Xboard/app/Protocols/ClashMeta.php
+++ /dev/null
@@ -1,708 +0,0 @@
- [
- 'whitelist' => [
- 'tcp' => '0.0.0',
- 'ws' => '0.0.0',
- 'grpc' => '0.0.0',
- 'http' => '0.0.0',
- 'h2' => '0.0.0',
- 'httpupgrade' => '0.0.0',
- ],
- 'strict' => true,
- ],
- 'nekobox.hysteria.protocol_settings.version' => [
- 1 => '0.0.0',
- 2 => '1.2.7',
- ],
- 'clashmetaforandroid.hysteria.protocol_settings.version' => [
- 2 => '2.9.0',
- ],
- 'nekoray.hysteria.protocol_settings.version' => [
- 2 => '3.24',
- ],
- 'verge.hysteria.protocol_settings.version' => [
- 2 => '1.3.8',
- ],
- 'ClashX Meta.hysteria.protocol_settings.version' => [
- 2 => '1.3.5',
- ],
- 'flclash.hysteria.protocol_settings.version' => [
- 2 => '0.8.0',
- ],
- ];
-
- public function handle()
- {
- $servers = $this->servers;
- $user = $this->user;
- $appName = admin_setting('app_name', 'XBoard');
-
- $template = subscribe_template('clashmeta');
-
- $config = Yaml::parse($template);
- $proxy = [];
- $proxies = [];
-
- foreach ($servers as $item) {
- if ($item['type'] === Server::TYPE_SHADOWSOCKS) {
- array_push($proxy, self::buildShadowsocks($item['password'], $item));
- array_push($proxies, $item['name']);
- }
- if ($item['type'] === Server::TYPE_VMESS) {
- array_push($proxy, self::buildVmess($item['password'], $item));
- array_push($proxies, $item['name']);
- }
- if ($item['type'] === Server::TYPE_TROJAN) {
- array_push($proxy, self::buildTrojan($item['password'], $item));
- array_push($proxies, $item['name']);
- }
- if ($item['type'] === Server::TYPE_VLESS) {
- array_push($proxy, self::buildVless($item['password'], $item));
- array_push($proxies, $item['name']);
- }
- if ($item['type'] === Server::TYPE_HYSTERIA) {
- array_push($proxy, self::buildHysteria($item['password'], $item, $user));
- array_push($proxies, $item['name']);
- }
- if ($item['type'] === Server::TYPE_TUIC) {
- array_push($proxy, self::buildTuic($item['password'], $item));
- array_push($proxies, $item['name']);
- }
- if ($item['type'] === Server::TYPE_ANYTLS) {
- array_push($proxy, self::buildAnyTLS($item['password'], $item));
- array_push($proxies, $item['name']);
- }
- if ($item['type'] === Server::TYPE_SOCKS) {
- array_push($proxy, self::buildSocks5($item['password'], $item));
- array_push($proxies, $item['name']);
- }
- if ($item['type'] === Server::TYPE_HTTP) {
- array_push($proxy, self::buildHttp($item['password'], $item));
- array_push($proxies, $item['name']);
- }
- if ($item['type'] === Server::TYPE_MIERU) {
- array_push($proxy, self::buildMieru($item['password'], $item));
- array_push($proxies, $item['name']);
- }
- }
-
- $config['proxies'] = array_merge($config['proxies'] ? $config['proxies'] : [], $proxy);
- foreach ($config['proxy-groups'] as $k => $v) {
- if (!is_array($config['proxy-groups'][$k]['proxies']))
- $config['proxy-groups'][$k]['proxies'] = [];
- $isFilter = false;
- foreach ($config['proxy-groups'][$k]['proxies'] as $src) {
- foreach ($proxies as $dst) {
- if (!$this->isRegex($src))
- continue;
- $isFilter = true;
- $config['proxy-groups'][$k]['proxies'] = array_values(array_diff($config['proxy-groups'][$k]['proxies'], [$src]));
- if ($this->isMatch($src, $dst)) {
- array_push($config['proxy-groups'][$k]['proxies'], $dst);
- }
- }
- if ($isFilter)
- continue;
- }
- if ($isFilter)
- continue;
- $config['proxy-groups'][$k]['proxies'] = array_merge($config['proxy-groups'][$k]['proxies'], $proxies);
- }
- $config['proxy-groups'] = array_filter($config['proxy-groups'], function ($group) {
- return $group['proxies'];
- });
- $config['proxy-groups'] = array_values($config['proxy-groups']);
- $config = $this->buildRules($config);
-
- $yaml = Yaml::dump($config, 2, 4, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE);
- $yaml = str_replace('$app_name', admin_setting('app_name', 'XBoard'), $yaml);
- return response($yaml)
- ->header('content-type', 'text/yaml')
- ->header('subscription-userinfo', "upload={$user['u']}; download={$user['d']}; total={$user['transfer_enable']}; expire={$user['expired_at']}")
- ->header('profile-update-interval', '24')
- ->header('content-disposition', 'attachment;filename*=UTF-8\'\'' . rawurlencode($appName));
- }
-
- /**
- * Build the rules for Clash.
- */
- public function buildRules($config)
- {
- // Force the current subscription domain to be a direct rule
- $subsDomain = request()->header('Host');
- if ($subsDomain) {
- array_unshift($config['rules'], "DOMAIN,{$subsDomain},DIRECT");
- }
- // // Force the nodes ip to be a direct rule
- // collect($this->servers)->pluck('host')->map(function ($host) {
- // $host = trim($host);
- // return filter_var($host, FILTER_VALIDATE_IP) ? [$host] : Helper::getIpByDomainName($host);
- // })->flatten()->unique()->each(function ($nodeIP) use (&$config) {
- // array_unshift($config['rules'], "IP-CIDR,{$nodeIP}/32,DIRECT,no-resolve");
- // });
-
- return $config;
- }
-
- public static function buildShadowsocks($password, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $array = [];
- $array['name'] = $server['name'];
- $array['type'] = 'ss';
- $array['server'] = $server['host'];
- $array['port'] = $server['port'];
- $array['cipher'] = data_get($server['protocol_settings'], 'cipher');
- $array['password'] = data_get($server, 'password', $password);
- $array['udp'] = true;
- if (data_get($protocol_settings, 'plugin') && data_get($protocol_settings, 'plugin_opts')) {
- $plugin = data_get($protocol_settings, 'plugin');
- $pluginOpts = data_get($protocol_settings, 'plugin_opts', '');
- $array['plugin'] = $plugin;
-
- // 解析插件选项
- $parsedOpts = collect(explode(';', $pluginOpts))
- ->filter()
- ->mapWithKeys(function ($pair) {
- if (!str_contains($pair, '=')) {
- return [trim($pair) => true];
- }
- [$key, $value] = explode('=', $pair, 2);
- return [trim($key) => trim($value)];
- })
- ->all();
-
- // 根据插件类型进行字段映射
- switch ($plugin) {
- case 'obfs':
- case 'obfs-local':
- $array['plugin'] = 'obfs';
- $array['plugin-opts'] = array_filter([
- 'mode' => $parsedOpts['obfs'] ?? ($parsedOpts['mode'] ?? 'http'),
- 'host' => $parsedOpts['obfs-host'] ?? ($parsedOpts['host'] ?? 'www.bing.com'),
- ]);
- break;
-
- case 'v2ray-plugin':
- $array['plugin-opts'] = array_filter([
- 'mode' => $parsedOpts['mode'] ?? 'websocket',
- 'tls' => isset($parsedOpts['tls']) || isset($parsedOpts['server']),
- 'host' => $parsedOpts['host'] ?? null,
- 'path' => $parsedOpts['path'] ?? '/',
- 'mux' => isset($parsedOpts['mux']) ? true : null,
- 'headers' => isset($parsedOpts['host']) ? ['Host' => $parsedOpts['host']] : null
- ], fn($v) => $v !== null);
- break;
-
- case 'shadow-tls':
- $array['plugin-opts'] = array_filter([
- 'host' => $parsedOpts['host'] ?? null,
- 'password' => $parsedOpts['password'] ?? null,
- 'version' => isset($parsedOpts['version']) ? (int) $parsedOpts['version'] : 2
- ], fn($v) => $v !== null);
- break;
-
- case 'restls':
- $array['plugin-opts'] = array_filter([
- 'host' => $parsedOpts['host'] ?? null,
- 'password' => $parsedOpts['password'] ?? null,
- 'restls-script' => $parsedOpts['restls-script'] ?? '123'
- ], fn($v) => $v !== null);
- break;
-
- default:
- $array['plugin-opts'] = $parsedOpts;
- }
- }
- return $array;
- }
-
- public static function buildVmess($uuid, $server)
- {
- $protocol_settings = data_get($server, 'protocol_settings', []);
- $array = [
- 'name' => $server['name'],
- 'type' => 'vmess',
- 'server' => $server['host'],
- 'port' => $server['port'],
- 'uuid' => $uuid,
- 'alterId' => 0,
- 'cipher' => 'auto',
- 'udp' => true
- ];
-
- if (data_get($protocol_settings, 'tls')) {
- $array['tls'] = (bool) data_get($protocol_settings, 'tls');
- $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls_settings.allow_insecure', false);
- $array['servername'] = data_get($protocol_settings, 'tls_settings.server_name');
- }
-
- self::appendUtls($array, $protocol_settings);
- self::appendMultiplex($array, $protocol_settings);
-
- switch (data_get($protocol_settings, 'network')) {
- case 'tcp':
- $headerType = data_get($protocol_settings, 'network_settings.header.type', 'none');
- $array['network'] = ($headerType === 'http') ? 'http' : 'tcp';
- if ($headerType === 'http') {
- if (
- $httpOpts = array_filter([
- 'headers' => data_get($protocol_settings, 'network_settings.header.request.headers'),
- 'path' => data_get($protocol_settings, 'network_settings.header.request.path', ['/'])
- ])
- ) {
- $array['http-opts'] = $httpOpts;
- }
- }
- break;
- case 'ws':
- $array['network'] = 'ws';
- if ($path = data_get($protocol_settings, 'network_settings.path'))
- $array['ws-opts']['path'] = $path;
- if ($host = data_get($protocol_settings, 'network_settings.headers.Host'))
- $array['ws-opts']['headers'] = ['Host' => $host];
- break;
- case 'grpc':
- $array['network'] = 'grpc';
- if ($serviceName = data_get($protocol_settings, 'network_settings.serviceName'))
- $array['grpc-opts']['grpc-service-name'] = $serviceName;
- break;
- case 'h2':
- $array['network'] = 'h2';
- $array['h2-opts'] = [];
- if ($path = data_get($protocol_settings, 'network_settings.path'))
- $array['h2-opts']['path'] = $path;
- if ($host = data_get($protocol_settings, 'network_settings.host'))
- $array['h2-opts']['host'] = is_array($host) ? $host : [$host];
- break;
- case 'httpupgrade':
- $array['network'] = 'ws';
- $array['ws-opts'] = ['v2ray-http-upgrade' => true];
- if ($path = data_get($protocol_settings, 'network_settings.path'))
- $array['ws-opts']['path'] = $path;
- if ($host = data_get($protocol_settings, 'network_settings.host'))
- $array['ws-opts']['headers'] = ['Host' => $host];
- break;
- default:
- break;
- }
-
- return $array;
- }
-
- public static function buildVless($password, $server)
- {
- $protocol_settings = data_get($server, 'protocol_settings', []);
- $array = [
- 'name' => $server['name'],
- 'type' => 'vless',
- 'server' => $server['host'],
- 'port' => $server['port'],
- 'uuid' => $password,
- 'alterId' => 0,
- 'cipher' => 'auto',
- 'udp' => true,
- 'flow' => data_get($protocol_settings, 'flow'),
- 'encryption' => match (data_get($protocol_settings, 'encryption.enabled')) {
- true => data_get($protocol_settings, 'encryption.encryption', 'none'),
- default => 'none'
- },
- 'tls' => false
- ];
-
- switch (data_get($protocol_settings, 'tls')) {
- case 1:
- $array['tls'] = true;
- $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls_settings.allow_insecure', false);
- if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) {
- $array['servername'] = $serverName;
- }
- self::appendUtls($array, $protocol_settings);
- break;
- case 2:
- $array['tls'] = true;
- $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'reality_settings.allow_insecure', false);
- $array['servername'] = data_get($protocol_settings, 'reality_settings.server_name');
- $array['reality-opts'] = [
- 'public-key' => data_get($protocol_settings, 'reality_settings.public_key'),
- 'short-id' => data_get($protocol_settings, 'reality_settings.short_id')
- ];
- self::appendUtls($array, $protocol_settings);
- break;
- default:
- break;
- }
-
- switch (data_get($protocol_settings, 'network')) {
- case 'tcp':
- $array['network'] = 'tcp';
- $headerType = data_get($protocol_settings, 'network_settings.header.type', 'none');
- if ($headerType === 'http') {
- $array['network'] = 'http';
- if (
- $httpOpts = array_filter([
- 'headers' => data_get($protocol_settings, 'network_settings.header.request.headers'),
- 'path' => data_get($protocol_settings, 'network_settings.header.request.path', ['/'])
- ])
- ) {
- $array['http-opts'] = $httpOpts;
- }
- }
- break;
- case 'ws':
- $array['network'] = 'ws';
- if ($path = data_get($protocol_settings, 'network_settings.path'))
- $array['ws-opts']['path'] = $path;
- if ($host = data_get($protocol_settings, 'network_settings.headers.Host'))
- $array['ws-opts']['headers'] = ['Host' => $host];
- break;
- case 'grpc':
- $array['network'] = 'grpc';
- if ($serviceName = data_get($protocol_settings, 'network_settings.serviceName'))
- $array['grpc-opts']['grpc-service-name'] = $serviceName;
- break;
- case 'h2':
- $array['network'] = 'h2';
- $array['h2-opts'] = [];
- if ($path = data_get($protocol_settings, 'network_settings.path'))
- $array['h2-opts']['path'] = $path;
- if ($host = data_get($protocol_settings, 'network_settings.host'))
- $array['h2-opts']['host'] = is_array($host) ? $host : [$host];
- break;
- case 'httpupgrade':
- $array['network'] = 'ws';
- $array['ws-opts'] = ['v2ray-http-upgrade' => true];
- if ($path = data_get($protocol_settings, 'network_settings.path'))
- $array['ws-opts']['path'] = $path;
- if ($host = data_get($protocol_settings, 'network_settings.host'))
- $array['ws-opts']['headers'] = ['Host' => $host];
- break;
- default:
- break;
- }
-
- self::appendMultiplex($array, $protocol_settings);
-
- return $array;
- }
-
- public static function buildTrojan($password, $server)
- {
- $protocol_settings = data_get($server, 'protocol_settings', []);
- $array = [
- 'name' => $server['name'],
- 'type' => 'trojan',
- 'server' => $server['host'],
- 'port' => $server['port'],
- 'password' => $password,
- 'udp' => true,
- ];
-
- $tlsMode = (int) data_get($protocol_settings, 'tls', 1);
- switch ($tlsMode) {
- case 2: // Reality
- $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'reality_settings.allow_insecure', false);
- if ($serverName = data_get($protocol_settings, 'reality_settings.server_name')) {
- $array['sni'] = $serverName;
- }
- $array['reality-opts'] = [
- 'public-key' => data_get($protocol_settings, 'reality_settings.public_key'),
- 'short-id' => data_get($protocol_settings, 'reality_settings.short_id'),
- ];
- break;
- default: // Standard TLS
- $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'allow_insecure', false);
- if ($serverName = data_get($protocol_settings, 'server_name')) {
- $array['sni'] = $serverName;
- }
- break;
- }
-
- self::appendUtls($array, $protocol_settings);
- self::appendMultiplex($array, $protocol_settings);
-
- switch (data_get($protocol_settings, 'network')) {
- case 'tcp':
- $array['network'] = 'tcp';
- break;
- case 'ws':
- $array['network'] = 'ws';
- if ($path = data_get($protocol_settings, 'network_settings.path'))
- $array['ws-opts']['path'] = $path;
- if ($host = data_get($protocol_settings, 'network_settings.headers.Host'))
- $array['ws-opts']['headers'] = ['Host' => $host];
- break;
- case 'grpc':
- $array['network'] = 'grpc';
- if ($serviceName = data_get($protocol_settings, 'network_settings.serviceName'))
- $array['grpc-opts']['grpc-service-name'] = $serviceName;
- break;
- case 'h2':
- $array['network'] = 'h2';
- $array['h2-opts'] = [];
- if ($path = data_get($protocol_settings, 'network_settings.path'))
- $array['h2-opts']['path'] = $path;
- if ($host = data_get($protocol_settings, 'network_settings.host'))
- $array['h2-opts']['host'] = is_array($host) ? $host : [$host];
- break;
- case 'httpupgrade':
- $array['network'] = 'ws';
- $array['ws-opts'] = ['v2ray-http-upgrade' => true];
- if ($path = data_get($protocol_settings, 'network_settings.path'))
- $array['ws-opts']['path'] = $path;
- if ($host = data_get($protocol_settings, 'network_settings.host'))
- $array['ws-opts']['headers'] = ['Host' => $host];
- break;
- default:
- $array['network'] = 'tcp';
- break;
- }
-
- return $array;
- }
-
- public static function buildHysteria($password, $server, $user)
- {
- $protocol_settings = data_get($server, 'protocol_settings', []);
- $array = [
- 'name' => $server['name'],
- 'server' => $server['host'],
- 'port' => $server['port'],
- 'sni' => data_get($protocol_settings, 'tls.server_name'),
- 'up' => data_get($protocol_settings, 'bandwidth.up'),
- 'down' => data_get($protocol_settings, 'bandwidth.down'),
- 'skip-cert-verify' => (bool) data_get($protocol_settings, 'tls.allow_insecure', false),
- ];
- if (isset($server['ports'])) {
- $array['ports'] = $server['ports'];
- }
- if ($hopInterval = data_get($protocol_settings, 'hop_interval')) {
- $array['hop-interval'] = (int) $hopInterval;
- }
- switch (data_get($protocol_settings, 'version')) {
- case 1:
- $array['type'] = 'hysteria';
- $array['auth_str'] = $password;
- $array['protocol'] = 'udp'; // 支持 udp/wechat-video/faketcp
- if (data_get($protocol_settings, 'obfs.open')) {
- $array['obfs'] = data_get($protocol_settings, 'obfs.password');
- }
- $array['fast-open'] = true;
- $array['disable_mtu_discovery'] = true;
- break;
- case 2:
- $array['type'] = 'hysteria2';
- $array['password'] = $password;
- if (data_get($protocol_settings, 'obfs.open')) {
- $array['obfs'] = data_get($protocol_settings, 'obfs.type');
- $array['obfs-password'] = data_get($protocol_settings, 'obfs.password');
- }
- break;
- }
-
- return $array;
- }
-
- public static function buildTuic($password, $server)
- {
- $protocol_settings = data_get($server, 'protocol_settings', []);
- $array = [
- 'name' => $server['name'],
- 'type' => 'tuic',
- 'server' => $server['host'],
- 'port' => $server['port'],
- 'udp' => true,
- ];
-
- if (data_get($protocol_settings, 'version') === 4) {
- $array['token'] = $password;
- } else {
- $array['uuid'] = $password;
- $array['password'] = $password;
- }
-
- $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls.allow_insecure', false);
- if ($serverName = data_get($protocol_settings, 'tls.server_name')) {
- $array['sni'] = $serverName;
- }
-
- if ($alpn = data_get($protocol_settings, 'alpn')) {
- $array['alpn'] = $alpn;
- }
-
- $array['congestion-controller'] = data_get($protocol_settings, 'congestion_control', 'cubic');
- $array['udp-relay-mode'] = data_get($protocol_settings, 'udp_relay_mode', 'native');
-
- return $array;
- }
-
- public static function buildAnyTLS($password, $server)
- {
-
- $protocol_settings = data_get($server, 'protocol_settings', []);
- $array = [
- 'name' => $server['name'],
- 'type' => 'anytls',
- 'server' => $server['host'],
- 'port' => $server['port'],
- 'password' => $password,
- 'udp' => true,
- ];
-
- if ($serverName = data_get($protocol_settings, 'tls.server_name')) {
- $array['sni'] = $serverName;
- }
- if ($allowInsecure = data_get($protocol_settings, 'tls.allow_insecure')) {
- $array['skip-cert-verify'] = (bool) $allowInsecure;
- }
-
- return $array;
- }
-
- public static function buildMieru($password, $server)
- {
- $protocol_settings = data_get($server, 'protocol_settings', []);
- $array = [
- 'name' => $server['name'],
- 'type' => 'mieru',
- 'server' => $server['host'],
- 'port' => $server['port'],
- 'username' => $password,
- 'password' => $password,
- 'transport' => strtoupper(data_get($protocol_settings, 'transport', 'TCP'))
- ];
-
- // 如果配置了端口范围
- if (isset($server['ports'])) {
- $array['port-range'] = $server['ports'];
- }
-
- return $array;
- }
-
- public static function buildSocks5($password, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $array = [];
- $array['name'] = $server['name'];
- $array['type'] = 'socks5';
- $array['server'] = $server['host'];
- $array['port'] = $server['port'];
- $array['udp'] = true;
-
- $array['username'] = $password;
- $array['password'] = $password;
-
- // TLS 配置
- if (data_get($protocol_settings, 'tls')) {
- $array['tls'] = true;
- $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls_settings.allow_insecure', false);
- }
-
- return $array;
- }
-
- public static function buildHttp($password, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $array = [];
- $array['name'] = $server['name'];
- $array['type'] = 'http';
- $array['server'] = $server['host'];
- $array['port'] = $server['port'];
-
- $array['username'] = $password;
- $array['password'] = $password;
-
- // TLS 配置
- if (data_get($protocol_settings, 'tls')) {
- $array['tls'] = true;
- $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls_settings.allow_insecure', false);
- }
-
- return $array;
- }
-
- private function isMatch($exp, $str)
- {
- try {
- return preg_match($exp, $str) === 1;
- } catch (\Exception $e) {
- return false;
- }
- }
-
- private function isRegex($exp)
- {
- if (empty($exp)) {
- return false;
- }
- try {
- return preg_match($exp, '') !== false;
- } catch (\Exception $e) {
- return false;
- }
- }
-
- protected static function appendMultiplex(&$array, $protocol_settings)
- {
- if ($multiplex = data_get($protocol_settings, 'multiplex')) {
- if (data_get($multiplex, 'enabled')) {
- $array['smux'] = array_filter([
- 'enabled' => true,
- 'protocol' => data_get($multiplex, 'protocol', 'yamux'),
- 'max-connections' => data_get($multiplex, 'max_connections'),
- // 'min-streams' => data_get($multiplex, 'min_streams'),
- // 'max-streams' => data_get($multiplex, 'max_streams'),
- 'padding' => data_get($multiplex, 'padding') ? true : null,
- ]);
-
- if (data_get($multiplex, 'brutal.enabled')) {
- $array['smux']['brutal-opts'] = [
- 'enabled' => true,
- 'up' => data_get($multiplex, 'brutal.up_mbps'),
- 'down' => data_get($multiplex, 'brutal.down_mbps'),
- ];
- }
- }
- }
- }
-
- protected static function appendUtls(&$array, $protocol_settings)
- {
- if ($utls = data_get($protocol_settings, 'utls')) {
- if (data_get($utls, 'enabled')) {
- $array['client-fingerprint'] = Helper::getTlsFingerprint($utls);
- }
- }
- }
-}
\ No newline at end of file
diff --git a/Xboard/app/Protocols/General.php b/Xboard/app/Protocols/General.php
deleted file mode 100644
index 5071027..0000000
--- a/Xboard/app/Protocols/General.php
+++ /dev/null
@@ -1,448 +0,0 @@
- [2 => '1.9.5'],
- 'v2rayn.hysteria.protocol_settings.version' => [2 => '6.31'],
- ];
-
- public function handle()
- {
- $servers = $this->servers;
- $user = $this->user;
- $uri = '';
-
- foreach ($servers as $item) {
- $uri .= match ($item['type']) {
- Server::TYPE_VMESS => self::buildVmess($item['password'], $item),
- Server::TYPE_VLESS => self::buildVless($item['password'], $item),
- Server::TYPE_SHADOWSOCKS => self::buildShadowsocks($item['password'], $item),
- Server::TYPE_TROJAN => self::buildTrojan($item['password'], $item),
- Server::TYPE_HYSTERIA => self::buildHysteria($item['password'], $item),
- Server::TYPE_ANYTLS => self::buildAnyTLS($item['password'], $item),
- Server::TYPE_SOCKS => self::buildSocks($item['password'], $item),
- Server::TYPE_TUIC => self::buildTuic($item['password'], $item),
- Server::TYPE_HTTP => self::buildHttp($item['password'], $item),
- default => '',
- };
- }
- return response(base64_encode($uri))
- ->header('content-type', 'text/plain')
- ->header('subscription-userinfo', "upload={$user['u']}; download={$user['d']}; total={$user['transfer_enable']}; expire={$user['expired_at']}");
- }
-
- public static function buildShadowsocks($password, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $name = rawurlencode($server['name']);
- $password = data_get($server, 'password', $password);
- $str = str_replace(
- ['+', '/', '='],
- ['-', '_', ''],
- base64_encode(data_get($protocol_settings, 'cipher') . ":{$password}")
- );
- $addr = Helper::wrapIPv6($server['host']);
- $plugin = data_get($protocol_settings, 'plugin');
- $plugin_opts = data_get($protocol_settings, 'plugin_opts');
- $url = "ss://{$str}@{$addr}:{$server['port']}";
- if ($plugin && $plugin_opts) {
- $url .= '/?' . 'plugin=' . rawurlencode($plugin . ';' . $plugin_opts);
- }
- $url .= "#{$name}\r\n";
- return $url;
- }
-
- public static function buildVmess($uuid, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $config = [
- "v" => "2",
- "ps" => $server['name'],
- "add" => $server['host'],
- "port" => (string) $server['port'],
- "id" => $uuid,
- "aid" => '0',
- "net" => data_get($server, 'protocol_settings.network'),
- "type" => "none",
- "host" => "",
- "path" => "",
- "tls" => data_get($protocol_settings, 'tls') ? "tls" : "",
- ];
- if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) {
- $config['sni'] = $serverName;
- }
- if ($fp = Helper::getTlsFingerprint(data_get($protocol_settings, 'utls'))) {
- $config['fp'] = $fp;
- }
-
- switch (data_get($protocol_settings, 'network')) {
- case 'tcp':
- if (data_get($protocol_settings, 'network_settings.header.type', 'none') !== 'none') {
- $config['type'] = data_get($protocol_settings, 'network_settings.header.type', 'http');
- $config['path'] = Arr::random(data_get($protocol_settings, 'network_settings.header.request.path', ['/']));
- $config['host'] =
- data_get($protocol_settings, 'network_settings.header.request.headers.Host')
- ? Arr::random(data_get($protocol_settings, 'network_settings.header.request.headers.Host', ['/']), )
- : null;
- }
- break;
- case 'ws':
- $config['type'] = 'ws';
- if ($path = data_get($protocol_settings, 'network_settings.path'))
- $config['path'] = $path;
- if ($host = data_get($protocol_settings, 'network_settings.headers.Host'))
- $config['host'] = $host;
- break;
- case 'grpc':
- $config['type'] = 'grpc';
- if ($path = data_get($protocol_settings, 'network_settings.serviceName'))
- $config['path'] = $path;
- break;
- case 'h2':
- $config['net'] = 'h2';
- $config['type'] = 'h2';
- if ($path = data_get($protocol_settings, 'network_settings.path'))
- $config['path'] = $path;
- if ($host = data_get($protocol_settings, 'network_settings.host'))
- $config['host'] = is_array($host) ? implode(',', $host) : $host;
- break;
- case 'httpupgrade':
- $config['net'] = 'httpupgrade';
- $config['type'] = 'httpupgrade';
- if ($path = data_get($protocol_settings, 'network_settings.path'))
- $config['path'] = $path;
- $config['host'] = data_get($protocol_settings, 'network_settings.host', $server['host']);
- break;
- default:
- break;
- }
- return "vmess://" . base64_encode(json_encode($config)) . "\r\n";
- }
-
- public static function buildVless($uuid, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $host = $server['host']; //节点地址
- $port = $server['port']; //节点端口
- $name = $server['name']; //节点名称
-
- $config = [
- 'mode' => 'multi', //grpc传输模式
- 'security' => '', //传输层安全 tls/reality
- 'encryption' => match (data_get($protocol_settings, 'encryption.enabled')) {
- true => data_get($protocol_settings, 'encryption.encryption', 'none'),
- default => 'none'
- },
- 'type' => data_get($server, 'protocol_settings.network'), //传输协议
- 'flow' => data_get($protocol_settings, 'flow'),
- ];
- // 处理TLS
- switch (data_get($server, 'protocol_settings.tls')) {
- case 1:
- $config['security'] = "tls";
- if ($fp = Helper::getTlsFingerprint(data_get($protocol_settings, 'utls'))) {
- $config['fp'] = $fp;
- }
- if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) {
- $config['sni'] = $serverName;
- }
- if (data_get($protocol_settings, 'tls_settings.allow_insecure')) {
- $config['allowInsecure'] = '1';
- }
- break;
- case 2: //reality
- $config['security'] = "reality";
- $config['pbk'] = data_get($protocol_settings, 'reality_settings.public_key');
- $config['sid'] = data_get($protocol_settings, 'reality_settings.short_id');
- $config['sni'] = data_get($protocol_settings, 'reality_settings.server_name');
- $config['servername'] = data_get($protocol_settings, 'reality_settings.server_name');
- $config['spx'] = "/";
- if ($fp = Helper::getTlsFingerprint(data_get($protocol_settings, 'utls'))) {
- $config['fp'] = $fp;
- }
- break;
- default:
- break;
- }
- // 处理传输协议
- switch (data_get($server, 'protocol_settings.network')) {
- case 'ws':
- if ($path = data_get($protocol_settings, 'network_settings.path'))
- $config['path'] = $path;
- if ($wsHost = data_get($protocol_settings, 'network_settings.headers.Host'))
- $config['host'] = $wsHost;
- break;
- case 'grpc':
- if ($path = data_get($protocol_settings, 'network_settings.serviceName'))
- $config['serviceName'] = $path;
- break;
- case 'h2':
- $config['type'] = 'http';
- if ($path = data_get($protocol_settings, 'network_settings.path'))
- $config['path'] = $path;
- if ($h2Host = data_get($protocol_settings, 'network_settings.host'))
- $config['host'] = is_array($h2Host) ? implode(',', $h2Host) : $h2Host;
- break;
- case 'kcp':
- if ($path = data_get($protocol_settings, 'network_settings.seed'))
- $config['path'] = $path;
- $config['type'] = data_get($protocol_settings, 'network_settings.header.type', 'none');
- break;
- case 'httpupgrade':
- if ($path = data_get($protocol_settings, 'network_settings.path'))
- $config['path'] = $path;
- $config['host'] = data_get($protocol_settings, 'network_settings.host', $server['host']);
- break;
- case 'xhttp':
- $config['path'] = data_get($protocol_settings, 'network_settings.path');
- $config['host'] = data_get($protocol_settings, 'network_settings.host', $server['host']);
- $config['mode'] = data_get($protocol_settings, 'network_settings.mode', 'auto');
- $config['extra'] = json_encode(data_get($protocol_settings, 'network_settings.extra'));
- break;
- }
-
- $user = $uuid . '@' . Helper::wrapIPv6($host) . ':' . $port;
- $query = http_build_query($config);
- $fragment = urlencode($name);
- $link = sprintf("vless://%s?%s#%s\r\n", $user, $query, $fragment);
- return $link;
- }
-
- public static function buildTrojan($password, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $name = rawurlencode($server['name']);
- $array = [];
- $tlsMode = (int) data_get($protocol_settings, 'tls', 1);
-
- switch ($tlsMode) {
- case 2: // Reality
- $array['security'] = 'reality';
- $array['pbk'] = data_get($protocol_settings, 'reality_settings.public_key');
- $array['sid'] = data_get($protocol_settings, 'reality_settings.short_id');
- $array['sni'] = data_get($protocol_settings, 'reality_settings.server_name');
- if ($fp = Helper::getTlsFingerprint(data_get($protocol_settings, 'utls'))) {
- $array['fp'] = $fp;
- }
- break;
- default: // Standard TLS
- $array['allowInsecure'] = data_get($protocol_settings, 'allow_insecure', false);
- if ($serverName = data_get($protocol_settings, 'server_name')) {
- $array['peer'] = $serverName;
- $array['sni'] = $serverName;
- }
- if ($fp = Helper::getTlsFingerprint(data_get($protocol_settings, 'utls'))) {
- $array['fp'] = $fp;
- }
- break;
- }
-
- switch (data_get($server, 'protocol_settings.network')) {
- case 'ws':
- $array['type'] = 'ws';
- if ($path = data_get($protocol_settings, 'network_settings.path'))
- $array['path'] = $path;
- if ($host = data_get($protocol_settings, 'network_settings.headers.Host'))
- $array['host'] = $host;
- break;
- case 'grpc':
- // Follow V2rayN family standards
- $array['type'] = 'grpc';
- if ($serviceName = data_get($protocol_settings, 'network_settings.serviceName'))
- $array['serviceName'] = $serviceName;
- break;
- case 'h2':
- $array['type'] = 'http';
- if ($path = data_get($protocol_settings, 'network_settings.path'))
- $array['path'] = $path;
- if ($host = data_get($protocol_settings, 'network_settings.host'))
- $array['host'] = is_array($host) ? implode(',', $host) : $host;
- break;
- case 'httpupgrade':
- $array['type'] = 'httpupgrade';
- if ($path = data_get($protocol_settings, 'network_settings.path'))
- $array['path'] = $path;
- $array['host'] = data_get($protocol_settings, 'network_settings.host', $server['host']);
- break;
- default:
- break;
- }
- $query = http_build_query($array);
- $addr = Helper::wrapIPv6($server['host']);
-
- $uri = "trojan://{$password}@{$addr}:{$server['port']}?{$query}#{$name}";
- $uri .= "\r\n";
- return $uri;
- }
-
- public static function buildHysteria($password, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $params = [];
- $version = data_get($protocol_settings, 'version', 2);
-
- if ($serverName = data_get($protocol_settings, 'tls.server_name')) {
- $params['sni'] = $serverName;
- }
- $params['insecure'] = data_get($protocol_settings, 'tls.allow_insecure') ? '1' : '0';
-
- $name = rawurlencode($server['name']);
- $addr = Helper::wrapIPv6($server['host']);
-
- if ($version === 2) {
- if (data_get($protocol_settings, 'obfs.open')) {
- $params['obfs'] = 'salamander';
- $params['obfs-password'] = data_get($protocol_settings, 'obfs.password');
- }
- if (isset($server['ports'])) {
- $params['mport'] = $server['ports'];
- }
-
- $query = http_build_query($params);
- $uri = "hysteria2://{$password}@{$addr}:{$server['port']}?{$query}#{$name}";
- } else {
- $params['protocol'] = 'udp';
- $params['auth'] = $password;
- if ($upMbps = data_get($protocol_settings, 'bandwidth.up'))
- $params['upmbps'] = $upMbps;
- if ($downMbps = data_get($protocol_settings, 'bandwidth.down'))
- $params['downmbps'] = $downMbps;
- if (data_get($protocol_settings, 'obfs.open') && ($obfsPassword = data_get($protocol_settings, 'obfs.password'))) {
- $params['obfs'] = 'xplus';
- $params['obfsParam'] = $obfsPassword;
- }
-
- $query = http_build_query($params);
- $uri = "hysteria://{$addr}:{$server['port']}?{$query}#{$name}";
- }
- $uri .= "\r\n";
-
- return $uri;
- }
-
-
- public static function buildTuic($password, $server)
- {
- $protocol_settings = data_get($server, 'protocol_settings', []);
- $name = rawurlencode($server['name']);
- $addr = Helper::wrapIPv6($server['host']);
- $port = $server['port'];
- $uuid = $password; // v2rayN格式里,uuid和password都是密码部分
- $pass = $password;
-
- $queryParams = [];
-
- // 填充sni参数
- if ($sni = data_get($protocol_settings, 'tls.server_name')) {
- $queryParams['sni'] = $sni;
- }
-
- // alpn参数,支持多值时用逗号连接
- if ($alpn = data_get($protocol_settings, 'alpn')) {
- if (is_array($alpn)) {
- $queryParams['alpn'] = implode(',', $alpn);
- } else {
- $queryParams['alpn'] = $alpn;
- }
- }
-
- // congestion_controller参数,默认cubic
- $congestion = data_get($protocol_settings, 'congestion_control', 'cubic');
- $queryParams['congestion_control'] = $congestion;
-
- // udp_relay_mode参数,默认native
- $udpRelay = data_get($protocol_settings, 'udp_relay_mode', 'native');
- $queryParams['udp-relay-mode'] = $udpRelay;
-
- if (data_get($protocol_settings, 'tls.allow_insecure')) {
- $queryParams['insecure'] = '1';
- }
-
- $query = http_build_query($queryParams);
-
- // 构造完整URI,格式:
- // Tuic://uuid:password@host:port?sni=xxx&alpn=xxx&congestion_controller=xxx&udp_relay_mode=xxx#别名
- $uri = "tuic://{$uuid}:{$pass}@{$addr}:{$port}";
-
- if (!empty($query)) {
- $uri .= "?{$query}";
- }
-
- $uri .= "#{$name}\r\n";
-
- return $uri;
- }
-
-
-
-
-
- public static function buildAnyTLS($password, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $name = rawurlencode($server['name']);
- $params = [
- 'sni' => data_get($protocol_settings, 'tls.server_name'),
- 'insecure' => data_get($protocol_settings, 'tls.allow_insecure')
- ];
- $query = http_build_query($params);
- $addr = Helper::wrapIPv6($server['host']);
- $uri = "anytls://{$password}@{$addr}:{$server['port']}?{$query}#{$name}";
- $uri .= "\r\n";
- return $uri;
- }
-
- public static function buildSocks($password, $server)
- {
- $name = rawurlencode($server['name']);
- $credentials = base64_encode("{$password}:{$password}");
- $addr = Helper::wrapIPv6($server['host']);
- return "socks://{$credentials}@{$addr}:{$server['port']}#{$name}\r\n";
- }
-
- public static function buildHttp($password, $server)
- {
- $protocol_settings = data_get($server, 'protocol_settings', []);
- $name = rawurlencode($server['name']);
- $addr = Helper::wrapIPv6($server['host']);
- $credentials = base64_encode("{$password}:{$password}");
-
- $params = [];
- if (data_get($protocol_settings, 'tls')) {
- $params['security'] = 'tls';
- if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) {
- $params['sni'] = $serverName;
- }
- $params['allowInsecure'] = data_get($protocol_settings, 'tls_settings.allow_insecure') ? '1' : '0';
- }
-
- $uri = "http://{$credentials}@{$addr}:{$server['port']}";
- if (!empty($params)) {
- $uri .= '?' . http_build_query($params);
- }
- $uri .= "#{$name}\r\n";
- return $uri;
- }
-}
diff --git a/Xboard/app/Protocols/Loon.php b/Xboard/app/Protocols/Loon.php
deleted file mode 100644
index 56df854..0000000
--- a/Xboard/app/Protocols/Loon.php
+++ /dev/null
@@ -1,357 +0,0 @@
- [2 => '637'],
- 'loon.trojan.protocol_settings.tls' => [0 => '3.2.1', 1 => '3.2.1',2 => '999.9.9'],
- ];
-
- public function handle()
- {
- $servers = $this->servers;
- $user = $this->user;
-
- $uri = '';
-
- foreach ($servers as $item) {
- if (
- $item['type'] === Server::TYPE_SHADOWSOCKS
- ) {
- $uri .= self::buildShadowsocks($item['password'], $item);
- }
- if ($item['type'] === Server::TYPE_VMESS) {
- $uri .= self::buildVmess($item['password'], $item);
- }
- if ($item['type'] === Server::TYPE_TROJAN) {
- $uri .= self::buildTrojan($item['password'], $item);
- }
- if ($item['type'] === Server::TYPE_HYSTERIA) {
- $uri .= self::buildHysteria($item['password'], $item, $user);
- }
- if ($item['type'] === Server::TYPE_VLESS) {
- $uri .= self::buildVless($item['password'], $item);
- }
- if ($item['type'] === Server::TYPE_ANYTLS) {
- $uri .= self::buildAnyTLS($item['password'], $item);
- }
- }
- return response($uri)
- ->header('content-type', 'text/plain')
- ->header('Subscription-Userinfo', "upload={$user['u']}; download={$user['d']}; total={$user['transfer_enable']}; expire={$user['expired_at']}");
- }
-
-
- public static function buildShadowsocks($password, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $cipher = data_get($protocol_settings, 'cipher');
-
- $config = [
- "{$server['name']}=Shadowsocks",
- "{$server['host']}",
- "{$server['port']}",
- "{$cipher}",
- "{$password}",
- 'fast-open=false',
- 'udp=true'
- ];
-
- if (data_get($protocol_settings, 'plugin') && data_get($protocol_settings, 'plugin_opts')) {
- $plugin = data_get($protocol_settings, 'plugin');
- $pluginOpts = data_get($protocol_settings, 'plugin_opts', '');
- // 解析插件选项
- $parsedOpts = collect(explode(';', $pluginOpts))
- ->filter()
- ->mapWithKeys(function ($pair) {
- if (!str_contains($pair, '=')) {
- return [];
- }
- [$key, $value] = explode('=', $pair, 2);
- return [trim($key) => trim($value)];
- })
- ->all();
- switch ($plugin) {
- case 'obfs':
- $config[] = "obfs-name={$parsedOpts['obfs']}";
- if (isset($parsedOpts['obfs-host'])) {
- $config[] = "obfs-host={$parsedOpts['obfs-host']}";
- }
- if (isset($parsedOpts['path'])) {
- $config[] = "obfs-uri={$parsedOpts['path']}";
- }
- break;
- }
- }
-
- $config = array_filter($config);
- $uri = implode(',', $config) . "\r\n";
- return $uri;
- }
-
- public static function buildVmess($uuid, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $config = [
- "{$server['name']}=vmess",
- "{$server['host']}",
- "{$server['port']}",
- 'auto',
- "{$uuid}",
- 'fast-open=false',
- 'udp=true',
- "alterId=0"
- ];
-
- if (data_get($protocol_settings, 'tls')) {
- $config[] = 'over-tls=true';
- if (data_get($protocol_settings, 'tls_settings')) {
- $tls_settings = data_get($protocol_settings, 'tls_settings');
- $config[] = 'skip-cert-verify=' . (data_get($tls_settings, 'allow_insecure') ? 'true' : 'false');
- if (data_get($tls_settings, 'server_name'))
- $config[] = "tls-name={$tls_settings['server_name']}";
- }
- }
-
- switch (data_get($server['protocol_settings'], 'network')) {
- case 'tcp':
- $config[] = 'transport=tcp';
- $tcpSettings = data_get($protocol_settings, 'network_settings');
- if (data_get($tcpSettings, 'header.type'))
- $config = str_replace('transport=tcp', "transport={$tcpSettings['header']['type']}", $config);
- if (data_get($tcpSettings, key: 'header.request.path')) {
- $paths = data_get($tcpSettings, key: 'header.request.path');
- $path = $paths[array_rand($paths)];
- $config[] = "path={$path}";
- }
- if (data_get($tcpSettings, key: 'header.request.headers.Host')) {
- $hosts = data_get($tcpSettings, key: 'header.request.headers.Host');
- $host = $hosts[array_rand($hosts)];
- $config[] = "host={$host}";
- }
- break;
- case 'ws':
- $config[] = 'transport=ws';
- $wsSettings = data_get($protocol_settings, 'network_settings');
- if (data_get($wsSettings, key: 'path'))
- $config[] = "path={$wsSettings['path']}";
- if (data_get($wsSettings, key: 'headers.Host'))
- $config[] = "host={$wsSettings['headers']['Host']}";
- break;
- case 'grpc':
- $config[] = 'transport=grpc';
- if ($serviceName = data_get($protocol_settings, 'network_settings.serviceName'))
- $config[] = "grpc-service-name={$serviceName}";
- break;
- case 'h2':
- $config[] = 'transport=h2';
- if ($path = data_get($protocol_settings, 'network_settings.path'))
- $config[] = "path={$path}";
- if ($host = data_get($protocol_settings, 'network_settings.host'))
- $config[] = "host=" . (is_array($host) ? $host[0] : $host);
- break;
- case 'httpupgrade':
- $config[] = 'transport=httpupgrade';
- if ($path = data_get($protocol_settings, 'network_settings.path'))
- $config[] = "path={$path}";
- if ($host = data_get($protocol_settings, 'network_settings.headers.Host'))
- $config[] = "host={$host}";
- break;
- }
-
- $uri = implode(',', $config);
- $uri .= "\r\n";
- return $uri;
- }
-
- public static function buildTrojan($password, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $config = [
- "{$server['name']}=trojan",
- "{$server['host']}",
- "{$server['port']}",
- "{$password}",
- ];
-
- $tlsMode = (int) data_get($protocol_settings, 'tls', 1);
- switch ($tlsMode) {
- case 2: // Reality
- if ($serverName = data_get($protocol_settings, 'reality_settings.server_name')) {
- $config[] = "tls-name={$serverName}";
- }
- if ($pubkey = data_get($protocol_settings, 'reality_settings.public_key')) {
- $config[] = "public-key={$pubkey}";
- }
- if ($shortid = data_get($protocol_settings, 'reality_settings.short_id')) {
- $config[] = "short-id={$shortid}";
- }
- $config[] = 'skip-cert-verify=' . (data_get($protocol_settings, 'reality_settings.allow_insecure', false) ? 'true' : 'false');
- break;
- default: // Standard TLS
- if ($serverName = data_get($protocol_settings, 'server_name')) {
- $config[] = "tls-name={$serverName}";
- }
- $config[] = 'skip-cert-verify=' . (data_get($protocol_settings, 'allow_insecure') ? 'true' : 'false');
- break;
- }
-
- switch (data_get($protocol_settings, 'network', 'tcp')) {
- case 'ws':
- $config[] = 'transport=ws';
- if ($path = data_get($protocol_settings, 'network_settings.path'))
- $config[] = "path={$path}";
- if ($host = data_get($protocol_settings, 'network_settings.headers.Host'))
- $config[] = "host={$host}";
- break;
- case 'grpc':
- $config[] = 'transport=grpc';
- if ($serviceName = data_get($protocol_settings, 'network_settings.serviceName'))
- $config[] = "grpc-service-name={$serviceName}";
- break;
- }
-
- $config = array_filter($config);
- $uri = implode(',', $config);
- $uri .= "\r\n";
- return $uri;
- }
-
- public static function buildVless($password, $server)
- {
- $protocol_settings = data_get($server, 'protocol_settings', []);
-
- $config = [
- "{$server['name']}=VLESS",
- "{$server['host']}",
- "{$server['port']}",
- "{$password}",
- "alterId=0",
- "udp=true"
- ];
-
- // flow
- if ($flow = data_get($protocol_settings, 'flow')) {
- $config[] = "flow={$flow}";
- }
-
- // TLS/Reality
- switch (data_get($protocol_settings, 'tls')) {
- case 1:
- $config[] = "over-tls=true";
- $config[] = "skip-cert-verify=" . (data_get($protocol_settings, 'tls_settings.allow_insecure', false) ? "true" : "false");
- if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) {
- $config[] = "sni={$serverName}";
- }
- break;
- case 2:
- $config[] = "over-tls=true";
- $config[] = "skip-cert-verify=" . (data_get($protocol_settings, 'reality_settings.allow_insecure', false) ? "true" : "false");
- if ($serverName = data_get($protocol_settings, 'reality_settings.server_name')) {
- $config[] = "sni={$serverName}";
- }
- if ($pubkey = data_get($protocol_settings, 'reality_settings.public_key')) {
- $config[] = "public-key={$pubkey}";
- }
- if ($shortid = data_get($protocol_settings, 'reality_settings.short_id')) {
- $config[] = "short-id={$shortid}";
- }
- break;
- default:
- $config[] = "over-tls=false";
- break;
- }
-
- // network
- switch (data_get($protocol_settings, 'network')) {
- case 'ws':
- $config[] = "transport=ws";
- if ($path = data_get($protocol_settings, 'network_settings.path')) {
- $config[] = "path={$path}";
- }
- if ($host = data_get($protocol_settings, 'network_settings.headers.Host')) {
- $config[] = "host={$host}";
- }
- break;
- case 'grpc':
- $config[] = "transport=grpc";
- if ($serviceName = data_get($protocol_settings, 'network_settings.serviceName')) {
- $config[] = "grpc-service-name={$serviceName}";
- }
- break;
- default:
- $config[] = "transport=tcp";
- break;
- }
-
- $config = array_filter($config);
- $uri = implode(',', $config) . "\r\n";
- return $uri;
- }
-
- public static function buildHysteria($password, $server, $user)
- {
- $protocol_settings = $server['protocol_settings'];
- if ($protocol_settings['version'] != 2) {
- return;
- }
- $config = [
- "{$server['name']}=Hysteria2",
- $server['host'],
- $server['port'],
- $password,
- $protocol_settings['tls']['server_name'] ? "sni={$protocol_settings['tls']['server_name']}" : "(null)"
- ];
- if (data_get($protocol_settings, 'tls.allow_insecure'))
- $config[] = "skip-cert-verify=true";
- if ($down = data_get($protocol_settings, 'bandwidth.down')) {
- $config[] = "download-bandwidth={$down}";
- }
- $config[] = "udp=true";
- $config = array_filter($config);
- $uri = implode(',', $config);
- $uri .= "\r\n";
- return $uri;
- }
-
- public static function buildAnyTLS($password, $server)
- {
- $protocol_settings = data_get($server, 'protocol_settings', []);
-
- $config = [
- "{$server['name']}=anytls",
- "{$server['host']}",
- "{$server['port']}",
- "{$password}",
- "udp=true"
- ];
-
- if ($serverName = data_get($protocol_settings, 'tls.server_name')) {
- $config[] = "sni={$serverName}";
- }
- // ✅ 跳过证书校验
- if (data_get($protocol_settings, 'tls.allow_insecure')) {
- $config[] = 'skip-cert-verify=true';
- }
-
- $config = array_filter($config);
-
- return implode(',', $config) . "\r\n";
- }
-}
diff --git a/Xboard/app/Protocols/QuantumultX.php b/Xboard/app/Protocols/QuantumultX.php
deleted file mode 100644
index 8488e0a..0000000
--- a/Xboard/app/Protocols/QuantumultX.php
+++ /dev/null
@@ -1,232 +0,0 @@
-servers;
- $user = $this->user;
- $uri = '';
- foreach ($servers as $item) {
- $uri .= match ($item['type']) {
- Server::TYPE_SHADOWSOCKS => self::buildShadowsocks($item['password'], $item),
- Server::TYPE_VMESS => self::buildVmess($item['password'], $item),
- Server::TYPE_VLESS => self::buildVless($item['password'], $item),
- Server::TYPE_TROJAN => self::buildTrojan($item['password'], $item),
- Server::TYPE_SOCKS => self::buildSocks5($item['password'], $item),
- Server::TYPE_HTTP => self::buildHttp($item['password'], $item),
- default => ''
- };
- }
- return response(base64_encode($uri))
- ->header('content-type', 'text/plain')
- ->header('subscription-userinfo', "upload={$user['u']}; download={$user['d']}; total={$user['transfer_enable']}; expire={$user['expired_at']}");
- }
-
- public static function buildShadowsocks($password, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $password = data_get($server, 'password', $password);
- $addr = Helper::wrapIPv6($server['host']);
- $config = [
- "shadowsocks={$addr}:{$server['port']}",
- "method=" . data_get($protocol_settings, 'cipher'),
- "password={$password}",
- ];
-
- if (data_get($protocol_settings, 'plugin') && data_get($protocol_settings, 'plugin_opts')) {
- $plugin = data_get($protocol_settings, 'plugin');
- $pluginOpts = data_get($protocol_settings, 'plugin_opts', '');
- $parsedOpts = collect(explode(';', $pluginOpts))
- ->filter()
- ->mapWithKeys(function ($pair) {
- if (!str_contains($pair, '=')) {
- return [];
- }
- [$key, $value] = explode('=', $pair, 2);
- return [trim($key) => trim($value)];
- })
- ->all();
- if ($plugin === 'obfs') {
- if (isset($parsedOpts['obfs'])) {
- $config[] = "obfs={$parsedOpts['obfs']}";
- }
- if (isset($parsedOpts['obfs-host'])) {
- $config[] = "obfs-host={$parsedOpts['obfs-host']}";
- }
- if (isset($parsedOpts['path'])) {
- $config[] = "obfs-uri={$parsedOpts['path']}";
- }
- }
- }
-
- self::applyCommonSettings($config, $server);
-
- return implode(',', array_filter($config)) . "\r\n";
- }
-
- public static function buildVmess($uuid, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $addr = Helper::wrapIPv6($server['host']);
- $config = [
- "vmess={$addr}:{$server['port']}",
- "method=" . data_get($protocol_settings, 'cipher', 'auto'),
- "password={$uuid}",
- ];
-
- self::applyTransportSettings($config, $protocol_settings);
- self::applyCommonSettings($config, $server);
-
- return implode(',', array_filter($config)) . "\r\n";
- }
-
- public static function buildVless($uuid, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $addr = Helper::wrapIPv6($server['host']);
- $config = [
- "vless={$addr}:{$server['port']}",
- 'method=none',
- "password={$uuid}",
- ];
-
- self::applyTransportSettings($config, $protocol_settings);
-
- if ($flow = data_get($protocol_settings, 'flow')) {
- $config[] = "vless-flow={$flow}";
- }
-
- self::applyCommonSettings($config, $server);
-
- return implode(',', array_filter($config)) . "\r\n";
- }
-
- private static function applyTransportSettings(&$config, $settings, bool $nativeTls = false, ?array $tlsData = null)
- {
- $tlsMode = (int) data_get($settings, 'tls', 0);
- $network = data_get($settings, 'network', 'tcp');
- $host = null;
- $isWs = $network === 'ws';
-
- switch ($network) {
- case 'ws':
- $config[] = $tlsMode ? 'obfs=wss' : 'obfs=ws';
- if ($path = data_get($settings, 'network_settings.path')) {
- $config[] = "obfs-uri={$path}";
- }
- $host = data_get($settings, 'network_settings.headers.Host');
- break;
- case 'tcp':
- $headerType = data_get($settings, 'network_settings.header.type', 'tcp');
- if ($headerType === 'http') {
- $config[] = 'obfs=http';
- $paths = data_get($settings, 'network_settings.header.request.path', ['/']);
- $config[] = 'obfs-uri=' . (is_array($paths) ? ($paths[0] ?? '/') : $paths);
- $hostVal = data_get($settings, 'network_settings.header.request.headers.Host');
- $host = is_array($hostVal) ? ($hostVal[0] ?? null) : $hostVal;
- } elseif ($tlsMode) {
- $config[] = $nativeTls ? 'over-tls=true' : 'obfs=over-tls';
- }
- break;
- }
-
- switch ($tlsMode) {
- case 2: // Reality
- $host = $host ?? data_get($settings, 'reality_settings.server_name');
- if ($pubKey = data_get($settings, 'reality_settings.public_key')) {
- $config[] = "reality-base64-pubkey={$pubKey}";
- }
- if ($shortId = data_get($settings, 'reality_settings.short_id')) {
- $config[] = "reality-hex-shortid={$shortId}";
- }
- break;
- case 1: // TLS
- $resolved = $tlsData ?? (array) data_get($settings, 'tls_settings', []);
- $allowInsecure = (bool) ($resolved['allow_insecure'] ?? false);
- $config[] = 'tls-verification=' . ($allowInsecure ? 'false' : 'true');
- $host = $host ?? ($resolved['server_name'] ?? null);
- break;
- }
-
- if ($host) {
- $config[] = ($nativeTls && !$isWs) ? "tls-host={$host}" : "obfs-host={$host}";
- }
- }
-
- private static function applyCommonSettings(&$config, $server)
- {
- $config[] = 'fast-open=true';
- if ($server['type'] !== Server::TYPE_HTTP) {
- $config[] = 'udp-relay=true';
- }
- $config[] = "tag={$server['name']}";
- }
-
- public static function buildTrojan($password, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $addr = Helper::wrapIPv6($server['host']);
- $config = [
- "trojan={$addr}:{$server['port']}",
- "password={$password}",
- ];
-
- $tlsData = [
- 'allow_insecure' => data_get($protocol_settings, 'allow_insecure', false),
- 'server_name' => data_get($protocol_settings, 'server_name'),
- ];
- self::applyTransportSettings($config, $protocol_settings, true, $tlsData);
- self::applyCommonSettings($config, $server);
-
- return implode(',', array_filter($config)) . "\r\n";
- }
-
- public static function buildSocks5($password, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $addr = Helper::wrapIPv6($server['host']);
- $config = [
- "socks5={$addr}:{$server['port']}",
- "username={$password}",
- "password={$password}",
- ];
-
- self::applyTransportSettings($config, $protocol_settings, true);
- self::applyCommonSettings($config, $server);
-
- return implode(',', array_filter($config)) . "\r\n";
- }
-
- public static function buildHttp($password, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $addr = Helper::wrapIPv6($server['host']);
- $config = [
- "http={$addr}:{$server['port']}",
- "username={$password}",
- "password={$password}",
- ];
-
- self::applyTransportSettings($config, $protocol_settings, true);
- self::applyCommonSettings($config, $server);
-
- return implode(',', array_filter($config)) . "\r\n";
- }
-}
diff --git a/Xboard/app/Protocols/Shadowrocket.php b/Xboard/app/Protocols/Shadowrocket.php
deleted file mode 100644
index 37028e3..0000000
--- a/Xboard/app/Protocols/Shadowrocket.php
+++ /dev/null
@@ -1,415 +0,0 @@
- [2 => '1993'],
- 'shadowrocket.anytls.base_version' => '2592',
- ];
-
- public function handle()
- {
- $servers = $this->servers;
- $user = $this->user;
-
- $uri = '';
- //display remaining traffic and expire date
- $upload = round($user['u'] / (1024 * 1024 * 1024), 2);
- $download = round($user['d'] / (1024 * 1024 * 1024), 2);
- $totalTraffic = round($user['transfer_enable'] / (1024 * 1024 * 1024), 2);
- $expiredDate = $user['expired_at'] === null ? 'N/A' : date('Y-m-d', $user['expired_at']);
- $uri .= "STATUS=🚀↑:{$upload}GB,↓:{$download}GB,TOT:{$totalTraffic}GB💡Expires:{$expiredDate}\r\n";
- foreach ($servers as $item) {
- if ($item['type'] === Server::TYPE_SHADOWSOCKS) {
- $uri .= self::buildShadowsocks($item['password'], $item);
- }
- if ($item['type'] === Server::TYPE_VMESS) {
- $uri .= self::buildVmess($item['password'], $item);
- }
- if ($item['type'] === Server::TYPE_VLESS) {
- $uri .= self::buildVless($item['password'], $item);
- }
- if ($item['type'] === Server::TYPE_TROJAN) {
- $uri .= self::buildTrojan($item['password'], $item);
- }
- if ($item['type'] === Server::TYPE_HYSTERIA) {
- $uri .= self::buildHysteria($item['password'], $item);
- }
- if ($item['type'] === Server::TYPE_TUIC) {
- $uri .= self::buildTuic($item['password'], $item);
- }
- if ($item['type'] === Server::TYPE_ANYTLS) {
- $uri .= self::buildAnyTLS($item['password'], $item);
- }
- if ($item['type'] === Server::TYPE_SOCKS) {
- $uri .= self::buildSocks($item['password'], $item);
- }
- }
- return response(base64_encode($uri))
- ->header('content-type', 'text/plain');
- }
-
-
- public static function buildShadowsocks($password, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $name = rawurlencode($server['name']);
- $password = data_get($server, 'password', $password);
- $str = str_replace(
- ['+', '/', '='],
- ['-', '_', ''],
- base64_encode(data_get($protocol_settings, 'cipher') . ":{$password}")
- );
- $addr = Helper::wrapIPv6($server['host']);
-
- $uri = "ss://{$str}@{$addr}:{$server['port']}";
- $plugin = data_get($protocol_settings, 'plugin') == 'obfs' ? 'obfs-local' : data_get($protocol_settings, 'plugin');
- $plugin_opts = data_get($protocol_settings, 'plugin_opts');
- if ($plugin && $plugin_opts) {
- $uri .= '/?' . 'plugin=' . $plugin . ';' . rawurlencode($plugin_opts);
- }
- return $uri . "#{$name}\r\n";
- }
-
- public static function buildVmess($uuid, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $userinfo = base64_encode('auto:' . $uuid . '@' . Helper::wrapIPv6($server['host']) . ':' . $server['port']);
- $config = [
- 'tfo' => 1,
- 'remark' => $server['name'],
- 'alterId' => 0
- ];
- if (data_get($protocol_settings, 'tls')) {
- $config['tls'] = 1;
- if (data_get($protocol_settings, 'tls_settings')) {
- if (!!data_get($protocol_settings, 'tls_settings.allow_insecure'))
- $config['allowInsecure'] = (int) data_get($protocol_settings, 'tls_settings.allow_insecure');
- if (!!data_get($protocol_settings, 'tls_settings.server_name'))
- $config['peer'] = data_get($protocol_settings, 'tls_settings.server_name');
- }
- }
-
- switch (data_get($protocol_settings, 'network')) {
- case 'tcp':
- if (data_get($protocol_settings, 'network_settings.header.type', 'none') !== 'none') {
- $config['obfs'] = data_get($protocol_settings, 'network_settings.header.type');
- $config['path'] = \Illuminate\Support\Arr::random(data_get($protocol_settings, 'network_settings.header.request.path', ['/']));
- $config['obfsParam'] = \Illuminate\Support\Arr::random(data_get($protocol_settings, 'network_settings.header.request.headers.Host', ['www.example.com']));
- }
- break;
- case 'ws':
- $config['obfs'] = "websocket";
- $config['path'] = data_get($protocol_settings, 'network_settings.path');
- if ($host = data_get($protocol_settings, 'network_settings.headers.Host')) {
- $config['obfsParam'] = $host;
- }
- break;
- case 'grpc':
- $config['obfs'] = "grpc";
- $config['path'] = data_get($protocol_settings, 'network_settings.serviceName');
- $config['host'] = data_get($protocol_settings, 'tls_settings.server_name') ?? $server['host'];
- break;
- case 'httpupgrade':
- $config['obfs'] = "httpupgrade";
- if ($path = data_get($protocol_settings, 'network_settings.path')) {
- $config['path'] = $path;
- }
- if ($host = data_get($protocol_settings, 'network_settings.host', $server['host'])) {
- $config['obfsParam'] = $host;
- }
- break;
- case 'h2':
- $config['obfs'] = "h2";
- if ($path = data_get($protocol_settings, 'network_settings.path')) {
- $config['path'] = $path;
- }
- if ($host = data_get($protocol_settings, 'network_settings.host')) {
- $config['obfsParam'] = $host[0] ?? $server['host'];
- $config['peer'] = $host [0] ?? $server['host'];
- }
- break;
- }
- $query = http_build_query($config, '', '&', PHP_QUERY_RFC3986);
- $uri = "vmess://{$userinfo}?{$query}";
- $uri .= "\r\n";
- return $uri;
- }
-
- public static function buildVless($uuid, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $userinfo = base64_encode('auto:' . $uuid . '@' . Helper::wrapIPv6($server['host']) . ':' . $server['port']);
- $config = [
- 'tfo' => 1,
- 'remark' => $server['name'],
- 'alterId' => 0
- ];
-
- // 判断是否开启xtls
- if (data_get($protocol_settings, 'flow')) {
- $xtlsMap = [
- 'none' => 0,
- 'xtls-rprx-direct' => 1,
- 'xtls-rprx-vision' => 2
- ];
- if (array_key_exists(data_get($protocol_settings, 'flow'), $xtlsMap)) {
- $config['tls'] = 1;
- $config['xtls'] = $xtlsMap[data_get($protocol_settings, 'flow')];
- }
- }
- switch (data_get($protocol_settings, 'tls')) {
- case 1:
- $config['tls'] = 1;
- $config['allowInsecure'] = (int) data_get($protocol_settings, 'tls_settings.allow_insecure');
- if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) {
- $config['peer'] = $serverName;
- }
- if ($fp = Helper::getTlsFingerprint(data_get($protocol_settings, 'utls'))) {
- $config['fp'] = $fp;
- }
- break;
- case 2:
- $config['tls'] = 1;
- $config['sni'] = data_get($protocol_settings, 'reality_settings.server_name');
- $config['pbk'] = data_get($protocol_settings, 'reality_settings.public_key');
- $config['sid'] = data_get($protocol_settings, 'reality_settings.short_id');
- if ($fp = Helper::getTlsFingerprint(data_get($protocol_settings, 'utls'))) {
- $config['fp'] = $fp;
- }
- break;
- default:
- break;
- }
- switch (data_get($protocol_settings, 'network')) {
- case 'tcp':
- if (data_get($protocol_settings, 'network_settings.header.type', 'none') !== 'none') {
- $config['obfs'] = data_get($protocol_settings, 'network_settings.header.type');
- $config['path'] = \Illuminate\Support\Arr::random(data_get($protocol_settings, 'network_settings.header.request.path', ['/']));
- $config['obfsParam'] = \Illuminate\Support\Arr::random(data_get($protocol_settings, 'network_settings.header.request.headers.Host', ['www.example.com']));
- }
- break;
- case 'ws':
- $config['obfs'] = "websocket";
- if (data_get($protocol_settings, 'network_settings.path')) {
- $config['path'] = data_get($protocol_settings, 'network_settings.path');
- }
-
- if ($host = data_get($protocol_settings, 'network_settings.headers.Host')) {
- $config['obfsParam'] = $host;
- }
- break;
- case 'grpc':
- $config['obfs'] = "grpc";
- $config['path'] = data_get($protocol_settings, 'network_settings.serviceName');
- $config['host'] = data_get($protocol_settings, 'tls_settings.server_name') ?? $server['host'];
- break;
- case 'kcp':
- $config['obfs'] = "kcp";
- if ($seed = data_get($protocol_settings, 'network_settings.seed')) {
- $config['path'] = $seed;
- }
- $config['type'] = data_get($protocol_settings, 'network_settings.header.type', 'none');
- break;
- case 'h2':
- $config['obfs'] = "h2";
- if ($path = data_get($protocol_settings, 'network_settings.path')) {
- $config['path'] = $path;
- }
- if ($host = data_get($protocol_settings, 'network_settings.host', $server['host'])) {
- $config['obfsParam'] = $host;
- }
- break;
- case 'httpupgrade':
- $config['obfs'] = "httpupgrade";
- if ($path = data_get($protocol_settings, 'network_settings.path')) {
- $config['path'] = $path;
- }
- if ($host = data_get($protocol_settings, 'network_settings.host', $server['host'])) {
- $config['obfsParam'] = $host;
- }
- break;
- case 'xhttp':
- $config['obfs'] = "xhttp";
- if ($path = data_get($protocol_settings, 'network_settings.path')) {
- $config['path'] = $path;
- }
- if ($host = data_get($protocol_settings, 'network_settings.host', $server['host'])) {
- $config['obfsParam'] = $host;
- }
- if ($mode = data_get($protocol_settings, 'network_settings.mode', 'auto')) {
- $config['mode'] = $mode;
- }
- break;
- }
-
- $query = http_build_query($config, '', '&', PHP_QUERY_RFC3986);
- $uri = "vless" . "://{$userinfo}?{$query}";
- $uri .= "\r\n";
- return $uri;
- }
-
- public static function buildTrojan($password, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $name = rawurlencode($server['name']);
- $params = [];
- $tlsMode = (int) data_get($protocol_settings, 'tls', 1);
-
- switch ($tlsMode) {
- case 2: // Reality
- $params['security'] = 'reality';
- $params['pbk'] = data_get($protocol_settings, 'reality_settings.public_key');
- $params['sid'] = data_get($protocol_settings, 'reality_settings.short_id');
- $params['sni'] = data_get($protocol_settings, 'reality_settings.server_name');
- break;
- default: // Standard TLS
- $params['allowInsecure'] = data_get($protocol_settings, 'allow_insecure');
- if ($serverName = data_get($protocol_settings, 'server_name')) {
- $params['peer'] = $serverName;
- }
- break;
- }
-
- switch (data_get($protocol_settings, 'network')) {
- case 'grpc':
- $params['obfs'] = 'grpc';
- $params['path'] = data_get($protocol_settings, 'network_settings.serviceName');
- break;
- case 'ws':
- $host = data_get($protocol_settings, 'network_settings.headers.Host');
- $path = data_get($protocol_settings, 'network_settings.path');
- $params['plugin'] = "obfs-local;obfs=websocket;obfs-host={$host};obfs-uri={$path}";
- break;
- }
- $query = http_build_query($params);
- $addr = Helper::wrapIPv6($server['host']);
-
- $uri = "trojan://{$password}@{$addr}:{$server['port']}?{$query}&tfo=1#{$name}";
- $uri .= "\r\n";
- return $uri;
- }
-
- public static function buildHysteria($password, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $uri = ''; // 初始化变量
-
- switch (data_get($protocol_settings, 'version')) {
- case 1:
- $params = [
- "auth" => $password,
- "upmbps" => data_get($protocol_settings, 'bandwidth.up'),
- "downmbps" => data_get($protocol_settings, 'bandwidth.down'),
- "protocol" => 'udp',
- "fastopen" => 1,
- ];
- if ($serverName = data_get($protocol_settings, 'tls.server_name')) {
- $params['peer'] = $serverName;
- }
- if (data_get($protocol_settings, 'obfs.open')) {
- $params["obfs"] = "xplus";
- $params["obfsParam"] = data_get($protocol_settings, 'obfs.password');
- }
- $params['insecure'] = data_get($protocol_settings, 'tls.allow_insecure');
- if (isset($server['ports']))
- $params['mport'] = $server['ports'];
- $query = http_build_query($params);
- $addr = Helper::wrapIPv6($server['host']);
-
- $uri = "hysteria://{$addr}:{$server['port']}?{$query}#{$server['name']}";
- $uri .= "\r\n";
- break;
- case 2:
- $params = [
- "obfs" => 'none',
- "fastopen" => 1
- ];
- if ($serverName = data_get($protocol_settings, 'tls.server_name')) {
- $params['peer'] = $serverName;
- }
- if (data_get($protocol_settings, 'obfs.open')) {
- $params['obfs'] = data_get($protocol_settings, 'obfs.type');
- $params['obfs-password'] = data_get($protocol_settings, 'obfs.password');
- }
- $params['insecure'] = data_get($protocol_settings, 'tls.allow_insecure');
- if (isset($protocol_settings['hop_interval'])) {
- $params['keepalive'] = data_get($protocol_settings, 'hop_interval');
- }
- if (isset($server['ports'])) {
- $params['mport'] = $server['ports'];
- }
- $query = http_build_query($params);
- $addr = Helper::wrapIPv6($server['host']);
-
- $uri = "hysteria2://{$password}@{$addr}:{$server['port']}?{$query}#{$server['name']}";
- $uri .= "\r\n";
- break;
- }
- return $uri;
- }
- public static function buildTuic($password, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $name = rawurlencode($server['name']);
- $params = [
- 'alpn' => data_get($protocol_settings, 'alpn'),
- 'sni' => data_get($protocol_settings, 'tls.server_name'),
- 'insecure' => data_get($protocol_settings, 'tls.allow_insecure')
- ];
- if (data_get($protocol_settings, 'version') === 4) {
- $params['token'] = $password;
- } else {
- $params['uuid'] = $password;
- $params['password'] = $password;
- }
- $query = http_build_query($params);
- $addr = Helper::wrapIPv6($server['host']);
- $uri = "tuic://{$addr}:{$server['port']}?{$query}#{$name}";
- $uri .= "\r\n";
- return $uri;
- }
-
- public static function buildAnyTLS($password, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $name = rawurlencode($server['name']);
- $params = [
- 'sni' => data_get($protocol_settings, 'tls.server_name'),
- 'insecure' => data_get($protocol_settings, 'tls.allow_insecure')
- ];
- $query = http_build_query($params);
- $addr = Helper::wrapIPv6($server['host']);
- $uri = "anytls://{$password}@{$addr}:{$server['port']}?{$query}#{$name}";
- $uri .= "\r\n";
- return $uri;
- }
-
- public static function buildSocks($password, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $name = rawurlencode($server['name']);
- $addr = Helper::wrapIPv6($server['host']);
- $uri = 'socks://' . base64_encode("{$password}:{$password}@{$addr}:{$server['port']}") . "?method=auto#{$name}";
- $uri .= "\r\n";
- return $uri;
- }
-}
diff --git a/Xboard/app/Protocols/Shadowsocks.php b/Xboard/app/Protocols/Shadowsocks.php
deleted file mode 100644
index 36f2d2a..0000000
--- a/Xboard/app/Protocols/Shadowsocks.php
+++ /dev/null
@@ -1,60 +0,0 @@
-servers;
- $user = $this->user;
-
- $configs = [];
- $subs = [];
- $subs['servers'] = [];
- $subs['bytes_used'] = '';
- $subs['bytes_remaining'] = '';
-
- $bytesUsed = $user['u'] + $user['d'];
- $bytesRemaining = $user['transfer_enable'] - $bytesUsed;
-
- foreach ($servers as $item) {
- if (
- $item['type'] === 'shadowsocks'
- && in_array(data_get($item, 'protocol_settings.cipher'), ['aes-128-gcm', 'aes-256-gcm', 'aes-192-gcm', 'chacha20-ietf-poly1305'])
- ) {
- array_push($configs, self::SIP008($item, $user));
- }
- }
-
- $subs['version'] = 1;
- $subs['bytes_used'] = $bytesUsed;
- $subs['bytes_remaining'] = $bytesRemaining;
- $subs['servers'] = array_merge($subs['servers'], $configs);
-
- return response()->json($subs)
- ->header('content-type', 'application/json');
- }
-
- public static function SIP008($server, $user)
- {
- $config = [
- "id" => $server['id'],
- "remarks" => $server['name'],
- "server" => $server['host'],
- "server_port" => $server['port'],
- "password" => $server['password'],
- "method" => data_get($server, 'protocol_settings.cipher')
- ];
- return $config;
- }
-}
diff --git a/Xboard/app/Protocols/SingBox.php b/Xboard/app/Protocols/SingBox.php
deleted file mode 100644
index b412787..0000000
--- a/Xboard/app/Protocols/SingBox.php
+++ /dev/null
@@ -1,757 +0,0 @@
- [
- 'vless' => [
- 'base_version' => '1.5.0',
- 'protocol_settings.flow' => [
- 'xtls-rprx-vision' => '1.5.0'
- ],
- 'protocol_settings.tls' => [
- '2' => '1.6.0' // Reality
- ]
- ],
- 'hysteria' => [
- 'base_version' => '1.5.0',
- 'protocol_settings.version' => [
- '2' => '1.5.0' // Hysteria 2
- ]
- ],
- 'tuic' => [
- 'base_version' => '1.5.0'
- ],
- 'ssh' => [
- 'base_version' => '1.8.0'
- ],
- 'juicity' => [
- 'base_version' => '1.7.0'
- ],
- 'wireguard' => [
- 'base_version' => '1.5.0'
- ],
- 'anytls' => [
- 'base_version' => '1.12.0'
- ],
- ]
- ];
-
- public function handle()
- {
- $appName = admin_setting('app_name', 'XBoard');
- $this->config = $this->loadConfig();
- $this->buildOutbounds();
- $this->buildRule();
- $this->adaptConfigForVersion();
- $user = $this->user;
-
- return response()
- ->json($this->config)
- ->header('profile-title', 'base64:' . base64_encode($appName))
- ->header('subscription-userinfo', "upload={$user['u']}; download={$user['d']}; total={$user['transfer_enable']}; expire={$user['expired_at']}")
- ->header('profile-update-interval', '24');
- }
-
- protected function loadConfig()
- {
- $jsonData = subscribe_template('singbox');
-
- return is_array($jsonData) ? $jsonData : json_decode($jsonData, true);
- }
-
- protected function buildOutbounds()
- {
- $outbounds = $this->config['outbounds'];
- $proxies = [];
- foreach ($this->servers as $item) {
- $protocol_settings = $item['protocol_settings'];
- if ($item['type'] === Server::TYPE_SHADOWSOCKS) {
- $ssConfig = $this->buildShadowsocks($item['password'], $item);
- $proxies[] = $ssConfig;
- }
- if ($item['type'] === Server::TYPE_TROJAN) {
- $trojanConfig = $this->buildTrojan($this->user['uuid'], $item);
- $proxies[] = $trojanConfig;
- }
- if ($item['type'] === Server::TYPE_VMESS) {
- $vmessConfig = $this->buildVmess($this->user['uuid'], $item);
- $proxies[] = $vmessConfig;
- }
- if (
- $item['type'] === Server::TYPE_VLESS
- && in_array(data_get($protocol_settings, 'network'), ['tcp', 'ws', 'grpc', 'http', 'quic', 'httpupgrade'])
- ) {
- $vlessConfig = $this->buildVless($this->user['uuid'], $item);
- $proxies[] = $vlessConfig;
- }
- if ($item['type'] === Server::TYPE_HYSTERIA) {
- $hysteriaConfig = $this->buildHysteria($this->user['uuid'], $item);
- $proxies[] = $hysteriaConfig;
- }
- if ($item['type'] === Server::TYPE_TUIC) {
- $tuicConfig = $this->buildTuic($this->user['uuid'], $item);
- $proxies[] = $tuicConfig;
- }
- if ($item['type'] === Server::TYPE_ANYTLS) {
- $anytlsConfig = $this->buildAnyTLS($this->user['uuid'], $item);
- $proxies[] = $anytlsConfig;
- }
- if ($item['type'] === Server::TYPE_SOCKS) {
- $socksConfig = $this->buildSocks($this->user['uuid'], $item);
- $proxies[] = $socksConfig;
- }
- if ($item['type'] === Server::TYPE_HTTP) {
- $httpConfig = $this->buildHttp($this->user['uuid'], $item);
- $proxies[] = $httpConfig;
- }
- }
- foreach ($outbounds as &$outbound) {
- if (in_array($outbound['type'], ['urltest', 'selector'])) {
- array_push($outbound['outbounds'], ...array_column($proxies, 'tag'));
- }
- }
-
- $outbounds = array_merge($outbounds, $proxies);
- $this->config['outbounds'] = $outbounds;
- return $outbounds;
- }
-
- /**
- * Build rule
- */
- protected function buildRule()
- {
- $rules = $this->config['route']['rules'];
- $this->config['route']['rules'] = $rules;
- }
-
- /**
- * 根据客户端版本自适应配置格式
- * 模板基准格式: 1.13.0+ (最新)
- */
- protected function adaptConfigForVersion(): void
- {
- $coreVersion = $this->getSingBoxCoreVersion();
- if (empty($coreVersion)) {
- return;
- }
-
- // >= 1.13.0: 移除已删除的 block/dns 出站
- if (version_compare($coreVersion, '1.13.0', '>=')) {
- $this->upgradeSpecialOutboundsToActions();
- }
-
- // < 1.11.0: rule action 降级为旧出站; 恢复废弃字段
- if (version_compare($coreVersion, '1.11.0', '<')) {
- $this->downgradeActionsToSpecialOutbounds();
- $this->restoreDeprecatedInboundFields();
- }
-
- // < 1.12.0: DNS type+server → 旧 address 格式
- if (version_compare($coreVersion, '1.12.0', '<')) {
- $this->convertDnsServersToLegacy();
- }
-
- // < 1.10.0: tun address 数组 → inet4_address/inet6_address
- if (version_compare($coreVersion, '1.10.0', '<')) {
- $this->convertTunAddressToLegacy();
- }
- }
-
- /**
- * 获取核心版本 (Hiddify/SFM 等映射到内核版本)
- */
- private function getSingBoxCoreVersion(): ?string
- {
- // 优先从 UA 提取核心版本
- if (!empty($this->userAgent)) {
- if (preg_match('/sing-box\s+v?(\d+(?:\.\d+){0,2})/i', $this->userAgent, $matches)) {
- return $matches[1];
- }
- }
-
- if (empty($this->clientVersion)) {
- return null;
- }
-
- if ($this->clientName === 'sing-box') {
- return $this->clientVersion;
- }
-
- return '1.13.0';
- }
-
- /**
- * sing-box >= 1.13.0: block/dns 出站升级为 action
- */
- private function upgradeSpecialOutboundsToActions(): void
- {
- $removedTags = [];
- $this->config['outbounds'] = array_values(array_filter(
- $this->config['outbounds'] ?? [],
- function ($outbound) use (&$removedTags) {
- if (in_array($outbound['type'] ?? '', ['block', 'dns'])) {
- $removedTags[$outbound['tag']] = $outbound['type'];
- return false;
- }
- return true;
- }
- ));
-
- if (empty($removedTags)) {
- return;
- }
-
- if (isset($this->config['route']['rules'])) {
- foreach ($this->config['route']['rules'] as &$rule) {
- if (!isset($rule['outbound']) || !isset($removedTags[$rule['outbound']])) {
- continue;
- }
- $type = $removedTags[$rule['outbound']];
- unset($rule['outbound']);
- $rule['action'] = $type === 'dns' ? 'hijack-dns' : 'reject';
- }
- unset($rule);
- }
- }
-
- /**
- * sing-box < 1.11.0: rule action 降级为旧 block/dns 出站
- */
- private function downgradeActionsToSpecialOutbounds(): void
- {
- $needsDnsOutbound = false;
- $needsBlockOutbound = false;
-
- if (isset($this->config['route']['rules'])) {
- foreach ($this->config['route']['rules'] as &$rule) {
- if (!isset($rule['action'])) {
- continue;
- }
- switch ($rule['action']) {
- case 'hijack-dns':
- unset($rule['action']);
- $rule['outbound'] = 'dns-out';
- $needsDnsOutbound = true;
- break;
- case 'reject':
- unset($rule['action']);
- $rule['outbound'] = 'block';
- $needsBlockOutbound = true;
- break;
- }
- }
- unset($rule);
- }
-
- if ($needsBlockOutbound) {
- $this->config['outbounds'][] = ['type' => 'block', 'tag' => 'block'];
- }
- if ($needsDnsOutbound) {
- $this->config['outbounds'][] = ['type' => 'dns', 'tag' => 'dns-out'];
- }
- }
-
- /**
- * sing-box < 1.11.0: 恢复废弃的入站字段
- */
- private function restoreDeprecatedInboundFields(): void
- {
- if (!isset($this->config['inbounds'])) {
- return;
- }
- foreach ($this->config['inbounds'] as &$inbound) {
- if ($inbound['type'] === 'tun') {
- $inbound['endpoint_independent_nat'] = true;
- }
- if (!empty($inbound['sniff'])) {
- $inbound['sniff_override_destination'] = true;
- }
- }
- }
-
- /**
- * sing-box < 1.12.0: 将新 DNS server type+server 格式转换为旧 address 格式
- */
- private function convertDnsServersToLegacy(): void
- {
- if (!isset($this->config['dns']['servers'])) {
- return;
- }
- foreach ($this->config['dns']['servers'] as &$server) {
- if (!isset($server['type'])) {
- continue;
- }
- $type = $server['type'];
- $host = $server['server'] ?? null;
- switch ($type) {
- case 'https':
- $server['address'] = "https://{$host}/dns-query";
- break;
- case 'tls':
- $server['address'] = "tls://{$host}";
- break;
- case 'tcp':
- $server['address'] = "tcp://{$host}";
- break;
- case 'quic':
- $server['address'] = "quic://{$host}";
- break;
- case 'udp':
- $server['address'] = $host;
- break;
- case 'block':
- $server['address'] = 'rcode://refused';
- break;
- case 'rcode':
- $server['address'] = 'rcode://' . ($server['rcode'] ?? 'success');
- unset($server['rcode']);
- break;
- default:
- $server['address'] = $host;
- break;
- }
- unset($server['type'], $server['server']);
- }
- unset($server);
- }
-
- /**
- * sing-box < 1.10.0: 将 tun address 数组转换为 inet4_address/inet6_address
- */
- private function convertTunAddressToLegacy(): void
- {
- if (!isset($this->config['inbounds'])) {
- return;
- }
- foreach ($this->config['inbounds'] as &$inbound) {
- if ($inbound['type'] !== 'tun' || !isset($inbound['address'])) {
- continue;
- }
- foreach ($inbound['address'] as $addr) {
- if (str_contains($addr, ':')) {
- $inbound['inet6_address'] = $addr;
- } else {
- $inbound['inet4_address'] = $addr;
- }
- }
- unset($inbound['address']);
- }
- }
-
- protected function buildShadowsocks($password, $server)
- {
- $protocol_settings = data_get($server, 'protocol_settings');
- $array = [];
- $array['tag'] = $server['name'];
- $array['type'] = 'shadowsocks';
- $array['server'] = $server['host'];
- $array['server_port'] = $server['port'];
- $array['method'] = data_get($protocol_settings, 'cipher');
- $array['password'] = data_get($server, 'password', $password);
- if (data_get($protocol_settings, 'plugin') && data_get($protocol_settings, 'plugin_opts')) {
- $array['plugin'] = data_get($protocol_settings, 'plugin');
- $array['plugin_opts'] = data_get($protocol_settings, 'plugin_opts', '');
- }
-
- return $array;
- }
-
-
- protected function buildVmess($uuid, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $array = [
- 'tag' => $server['name'],
- 'type' => 'vmess',
- 'server' => $server['host'],
- 'server_port' => $server['port'],
- 'uuid' => $uuid,
- 'security' => 'auto',
- 'alter_id' => 0,
- ];
-
- if ($protocol_settings['tls']) {
- $array['tls'] = [
- 'enabled' => true,
- 'insecure' => (bool) data_get($protocol_settings, 'tls_settings.allow_insecure'),
- ];
-
- $this->appendUtls($array['tls'], $protocol_settings);
-
- if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) {
- $array['tls']['server_name'] = $serverName;
- }
- }
-
- $this->appendMultiplex($array, $protocol_settings);
-
- if ($transport = $this->buildTransport($protocol_settings, $server)) {
- $array['transport'] = $transport;
- }
- return $array;
- }
-
- protected function buildVless($password, $server)
- {
- $protocol_settings = data_get($server, 'protocol_settings', []);
- $array = [
- "type" => "vless",
- "tag" => $server['name'],
- "server" => $server['host'],
- "server_port" => $server['port'],
- "uuid" => $password,
- "packet_encoding" => "xudp",
- ];
- if ($flow = data_get($protocol_settings, 'flow')) {
- $array['flow'] = $flow;
- }
-
- if (data_get($protocol_settings, 'tls')) {
- $tlsMode = (int) data_get($protocol_settings, 'tls', 0);
- $tlsConfig = [
- 'enabled' => true,
- 'insecure' => $tlsMode === 2
- ? (bool) data_get($protocol_settings, 'reality_settings.allow_insecure', false)
- : (bool) data_get($protocol_settings, 'tls_settings.allow_insecure', false),
- ];
-
- $this->appendUtls($tlsConfig, $protocol_settings);
-
- switch ($tlsMode) {
- case 1:
- if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) {
- $tlsConfig['server_name'] = $serverName;
- }
- break;
- case 2:
- $tlsConfig['server_name'] = data_get($protocol_settings, 'reality_settings.server_name');
- $tlsConfig['reality'] = [
- 'enabled' => true,
- 'public_key' => data_get($protocol_settings, 'reality_settings.public_key'),
- 'short_id' => data_get($protocol_settings, 'reality_settings.short_id')
- ];
- break;
- }
-
- $array['tls'] = $tlsConfig;
- }
-
- $this->appendMultiplex($array, $protocol_settings);
-
- if ($transport = $this->buildTransport($protocol_settings, $server)) {
- $array['transport'] = $transport;
- }
-
- return $array;
- }
-
- protected function buildTrojan($password, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $array = [
- 'tag' => $server['name'],
- 'type' => 'trojan',
- 'server' => $server['host'],
- 'server_port' => $server['port'],
- 'password' => $password,
- ];
-
- $tlsMode = (int) data_get($protocol_settings, 'tls', 1);
- $tlsConfig = ['enabled' => true];
-
- switch ($tlsMode) {
- case 2: // Reality
- $tlsConfig['insecure'] = (bool) data_get($protocol_settings, 'reality_settings.allow_insecure', false);
- $tlsConfig['server_name'] = data_get($protocol_settings, 'reality_settings.server_name');
- $tlsConfig['reality'] = [
- 'enabled' => true,
- 'public_key' => data_get($protocol_settings, 'reality_settings.public_key'),
- 'short_id' => data_get($protocol_settings, 'reality_settings.short_id'),
- ];
- break;
- default: // Standard TLS
- $tlsConfig['insecure'] = (bool) data_get($protocol_settings, 'allow_insecure', false);
- if ($serverName = data_get($protocol_settings, 'server_name')) {
- $tlsConfig['server_name'] = $serverName;
- }
- break;
- }
-
- $this->appendUtls($tlsConfig, $protocol_settings);
- $array['tls'] = $tlsConfig;
-
- $this->appendMultiplex($array, $protocol_settings);
-
- if ($transport = $this->buildTransport($protocol_settings, $server)) {
- $array['transport'] = $transport;
- }
- return $array;
- }
-
- protected function buildHysteria($password, $server): array
- {
- $protocol_settings = $server['protocol_settings'];
- $baseConfig = [
- 'server' => $server['host'],
- 'server_port' => $server['port'],
- 'tag' => $server['name'],
- 'tls' => [
- 'enabled' => true,
- 'insecure' => (bool) data_get($protocol_settings, 'tls.allow_insecure', false),
- ]
- ];
- // 支持 1.11.0 版本及以上 `server_ports` 和 `hop_interval` 配置
- if ($this->supportsFeature('sing-box', '1.11.0')) {
- if (isset($server['ports'])) {
- $baseConfig['server_ports'] = [str_replace('-', ':', $server['ports'])];
- }
- if (isset($protocol_settings['hop_interval'])) {
- $baseConfig['hop_interval'] = "{$protocol_settings['hop_interval']}s";
- }
- }
-
- if ($serverName = data_get($protocol_settings, 'tls.server_name')) {
- $baseConfig['tls']['server_name'] = $serverName;
- }
- $speedConfig = [
- 'up_mbps' => data_get($protocol_settings, 'bandwidth.up'),
- 'down_mbps' => data_get($protocol_settings, 'bandwidth.down'),
- ];
- $versionConfig = match (data_get($protocol_settings, 'version', 1)) {
- 2 => [
- 'type' => 'hysteria2',
- 'password' => $password,
- 'obfs' => data_get($protocol_settings, 'obfs.open') ? [
- 'type' => data_get($protocol_settings, 'obfs.type'),
- 'password' => data_get($protocol_settings, 'obfs.password')
- ] : null,
- ],
- default => [
- 'type' => 'hysteria',
- 'auth_str' => $password,
- 'obfs' => data_get($protocol_settings, 'obfs.password'),
- 'disable_mtu_discovery' => true,
- ]
- };
-
- return array_filter(
- array_merge($baseConfig, $speedConfig, $versionConfig),
- fn($v) => !is_null($v)
- );
- }
-
- protected function buildTuic($password, $server): array
- {
- $protocol_settings = data_get($server, 'protocol_settings', []);
- $array = [
- 'type' => 'tuic',
- 'tag' => $server['name'],
- 'server' => $server['host'],
- 'server_port' => $server['port'],
- 'congestion_control' => data_get($protocol_settings, 'congestion_control', 'cubic'),
- 'udp_relay_mode' => data_get($protocol_settings, 'udp_relay_mode', 'native'),
- 'zero_rtt_handshake' => true,
- 'heartbeat' => '10s',
- 'tls' => [
- 'enabled' => true,
- 'insecure' => (bool) data_get($protocol_settings, 'tls.allow_insecure', false),
- 'alpn' => data_get($protocol_settings, 'alpn', ['h3']),
- ]
- ];
-
- if ($serverName = data_get($protocol_settings, 'tls.server_name')) {
- $array['tls']['server_name'] = $serverName;
- }
-
- if (data_get($protocol_settings, 'version') === 4) {
- $array['token'] = $password;
- } else {
- $array['uuid'] = $password;
- $array['password'] = $password;
- }
-
- return $array;
- }
-
- protected function buildAnyTLS($password, $server): array
- {
- $protocol_settings = data_get($server, 'protocol_settings', []);
- $array = [
- 'type' => 'anytls',
- 'tag' => $server['name'],
- 'server' => $server['host'],
- 'password' => $password,
- 'server_port' => $server['port'],
- 'tls' => [
- 'enabled' => true,
- 'insecure' => (bool) data_get($protocol_settings, 'tls.allow_insecure', false),
- 'alpn' => data_get($protocol_settings, 'alpn', ['h3']),
- ]
- ];
-
- if ($serverName = data_get($protocol_settings, 'tls.server_name')) {
- $array['tls']['server_name'] = $serverName;
- }
-
- return $array;
- }
-
- protected function buildSocks($password, $server): array
- {
- $protocol_settings = data_get($server, 'protocol_settings', []);
- $array = [
- 'type' => 'socks',
- 'tag' => $server['name'],
- 'server' => $server['host'],
- 'server_port' => $server['port'],
- 'version' => '5', // 默认使用 socks5
- 'username' => $password,
- 'password' => $password,
- ];
-
- if (data_get($protocol_settings, 'udp_over_tcp')) {
- $array['udp_over_tcp'] = true;
- }
-
- return $array;
- }
-
- protected function buildHttp($password, $server): array
- {
- $protocol_settings = data_get($server, 'protocol_settings', []);
- $array = [
- 'type' => 'http',
- 'tag' => $server['name'],
- 'server' => $server['host'],
- 'server_port' => $server['port'],
- 'username' => $password,
- 'password' => $password,
- ];
-
- if ($path = data_get($protocol_settings, 'path')) {
- $array['path'] = $path;
- }
-
- if ($headers = data_get($protocol_settings, 'headers')) {
- $array['headers'] = $headers;
- }
-
- if (data_get($protocol_settings, 'tls')) {
- $array['tls'] = [
- 'enabled' => true,
- 'insecure' => (bool) data_get($protocol_settings, 'tls_settings.allow_insecure', false),
- ];
-
- if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) {
- $array['tls']['server_name'] = $serverName;
- }
- }
-
- return $array;
- }
-
- protected function buildTransport(array $protocol_settings, array $server): ?array
- {
- $transport = match (data_get($protocol_settings, 'network')) {
- 'tcp' => data_get($protocol_settings, 'network_settings.header.type') === 'http' ? [
- 'type' => 'http',
- 'path' => Arr::random(data_get($protocol_settings, 'network_settings.header.request.path', ['/'])),
- 'host' => data_get($protocol_settings, 'network_settings.header.request.headers.Host', [])
- ] : null,
- 'ws' => [
- 'type' => 'ws',
- 'path' => data_get($protocol_settings, 'network_settings.path'),
- 'headers' => ($host = data_get($protocol_settings, 'network_settings.headers.Host')) ? ['Host' => $host] : null,
- 'max_early_data' => 0,
- // 'early_data_header_name' => 'Sec-WebSocket-Protocol'
- ],
- 'grpc' => [
- 'type' => 'grpc',
- 'service_name' => data_get($protocol_settings, 'network_settings.serviceName')
- ],
- 'h2' => [
- 'type' => 'http',
- 'host' => data_get($protocol_settings, 'network_settings.host'),
- 'path' => data_get($protocol_settings, 'network_settings.path')
- ],
- 'httpupgrade' => [
- 'type' => 'httpupgrade',
- 'path' => data_get($protocol_settings, 'network_settings.path'),
- 'host' => data_get($protocol_settings, 'network_settings.host', $server['host']),
- 'headers' => data_get($protocol_settings, 'network_settings.headers')
- ],
- 'quic' => ['type' => 'quic'],
- default => null
- };
-
- if (!$transport) {
- return null;
- }
-
- return array_filter($transport, fn($v) => !is_null($v));
- }
-
- protected function appendMultiplex(&$array, $protocol_settings)
- {
- if ($multiplex = data_get($protocol_settings, 'multiplex')) {
- if (data_get($multiplex, 'enabled')) {
- $array['multiplex'] = [
- 'enabled' => true,
- 'protocol' => data_get($multiplex, 'protocol', 'yamux'),
- 'max_connections' => data_get($multiplex, 'max_connections'),
- 'min_streams' => data_get($multiplex, 'min_streams'),
- 'max_streams' => data_get($multiplex, 'max_streams'),
- 'padding' => (bool) data_get($multiplex, 'padding', false),
- ];
- if (data_get($multiplex, 'brutal.enabled')) {
- $array['multiplex']['brutal'] = [
- 'enabled' => true,
- 'up_mbps' => data_get($multiplex, 'brutal.up_mbps'),
- 'down_mbps' => data_get($multiplex, 'brutal.down_mbps'),
- ];
- }
- $array['multiplex'] = array_filter($array['multiplex'], fn($v) => !is_null($v));
- }
- }
- }
-
- protected function appendUtls(&$tlsConfig, $protocol_settings)
- {
- if ($utls = data_get($protocol_settings, 'utls')) {
- if (data_get($utls, 'enabled')) {
- $tlsConfig['utls'] = [
- 'enabled' => true,
- 'fingerprint' => Helper::getTlsFingerprint($utls)
- ];
- }
- }
- }
-}
diff --git a/Xboard/app/Protocols/Stash.php b/Xboard/app/Protocols/Stash.php
deleted file mode 100644
index 8a3eadc..0000000
--- a/Xboard/app/Protocols/Stash.php
+++ /dev/null
@@ -1,587 +0,0 @@
- [
- 'trojan' => [
- 'protocol_settings.tls' => [
- '2' => '9999.0.0', // Trojan Reality not supported in Stash
- ],
- ],
- 'vmess' => [
- 'protocol_settings.network' => [
- 'httpupgrade' => '9999.0.0', // httpupgrade not supported in Stash
- ],
- ],
- ],
- 'stash' => [
- 'anytls' => [
- 'base_version' => '3.3.0' // AnyTLS 协议在3.3.0版本中添加
- ],
- 'vless' => [
- 'protocol_settings.tls' => [
- '2' => '3.1.0' // Reality 在3.1.0版本中添加
- ],
- 'protocol_settings.flow' => [
- 'xtls-rprx-vision' => '3.1.0',
- ]
- ],
- 'hysteria' => [
- 'base_version' => '2.0.0',
- 'protocol_settings.version' => [
- '1' => '2.0.0', // Hysteria 1
- '2' => '2.5.0' // Hysteria 2,2.5.0 版本开始支持(2023年11月8日)
- ],
- // 'protocol_settings.ports' => [
- // 'true' => '2.6.4' // Hysteria 2 端口跳转功能于2.6.4版本支持(2024年8月4日)
- // ]
- ],
- 'tuic' => [
- 'base_version' => '2.3.0' // TUIC 协议自身需要 2.3.0+
- ],
- 'shadowsocks' => [
- 'base_version' => '2.0.0',
- // ShadowSocks2022 在3.0.0版本中添加(2025年4月2日)
- 'protocol_settings.cipher' => [
- '2022-blake3-aes-128-gcm' => '3.0.0',
- '2022-blake3-aes-256-gcm' => '3.0.0',
- '2022-blake3-chacha20-poly1305' => '3.0.0'
- ]
- ],
- 'shadowtls' => [
- 'base_version' => '3.0.0' // ShadowTLS 在3.0.0版本中添加(2025年4月2日)
- ],
- 'ssh' => [
- 'base_version' => '2.6.4' // SSH 协议在2.6.4中添加(2024年8月4日)
- ],
- 'juicity' => [
- 'base_version' => '2.6.4' // Juicity 协议在2.6.4中添加(2024年8月4日)
- ]
- ]
- ];
-
- const CUSTOM_TEMPLATE_FILE = 'resources/rules/custom.stash.yaml';
- const CUSTOM_CLASH_TEMPLATE_FILE = 'resources/rules/custom.clash.yaml';
- const DEFAULT_TEMPLATE_FILE = 'resources/rules/default.clash.yaml';
-
- public function handle()
- {
- $servers = $this->servers;
- $user = $this->user;
- $appName = admin_setting('app_name', 'XBoard');
-
- $template = subscribe_template('stash');
-
- $config = Yaml::parse($template);
- $proxy = [];
- $proxies = [];
-
- foreach ($servers as $item) {
- if ($item['type'] === Server::TYPE_SHADOWSOCKS) {
- array_push($proxy, self::buildShadowsocks($item['password'], $item));
- array_push($proxies, $item['name']);
- }
- if ($item['type'] === Server::TYPE_VMESS) {
- array_push($proxy, self::buildVmess($item['password'], $item));
- array_push($proxies, $item['name']);
- }
- if ($item['type'] === Server::TYPE_VLESS) {
- array_push($proxy, $this->buildVless($item['password'], $item));
- array_push($proxies, $item['name']);
- }
- if ($item['type'] === Server::TYPE_HYSTERIA) {
- array_push($proxy, self::buildHysteria($item['password'], $item));
- array_push($proxies, $item['name']);
- }
- if ($item['type'] === Server::TYPE_TROJAN) {
- array_push($proxy, self::buildTrojan($item['password'], $item));
- array_push($proxies, $item['name']);
- }
- if ($item['type'] === Server::TYPE_TUIC) {
- array_push($proxy, self::buildTuic($item['password'], $item));
- array_push($proxies, $item['name']);
- }
- if ($item['type'] === Server::TYPE_ANYTLS) {
- array_push($proxy, self::buildAnyTLS($item['password'], $item));
- array_push($proxies, $item['name']);
- }
- if ($item['type'] === Server::TYPE_SOCKS) {
- array_push($proxy, self::buildSocks5($item['password'], $item));
- array_push($proxies, $item['name']);
- }
- if ($item['type'] === Server::TYPE_HTTP) {
- array_push($proxy, self::buildHttp($item['password'], $item));
- array_push($proxies, $item['name']);
- }
- }
-
- $config['proxies'] = array_merge($config['proxies'] ? $config['proxies'] : [], $proxy);
- foreach ($config['proxy-groups'] as $k => $v) {
- if (!is_array($config['proxy-groups'][$k]['proxies']))
- $config['proxy-groups'][$k]['proxies'] = [];
- $isFilter = false;
- foreach ($config['proxy-groups'][$k]['proxies'] as $src) {
- foreach ($proxies as $dst) {
- if (!$this->isRegex($src))
- continue;
- $isFilter = true;
- $config['proxy-groups'][$k]['proxies'] = array_values(array_diff($config['proxy-groups'][$k]['proxies'], [$src]));
- if ($this->isMatch($src, $dst)) {
- array_push($config['proxy-groups'][$k]['proxies'], $dst);
- }
- }
- if ($isFilter)
- continue;
- }
- if ($isFilter)
- continue;
- $config['proxy-groups'][$k]['proxies'] = array_merge($config['proxy-groups'][$k]['proxies'], $proxies);
- }
- $config['proxy-groups'] = array_filter($config['proxy-groups'], function ($group) {
- return $group['proxies'];
- });
- $config['proxy-groups'] = array_values($config['proxy-groups']);
- // Force the current subscription domain to be a direct rule
- $subsDomain = request()->header('Host');
- if ($subsDomain) {
- array_unshift($config['rules'], "DOMAIN,{$subsDomain},DIRECT");
- }
-
- $yaml = Yaml::dump($config, 2, 4, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE);
- $yaml = str_replace('$app_name', admin_setting('app_name', 'XBoard'), $yaml);
- return response($yaml)
- ->header('content-type', 'text/yaml')
- ->header('subscription-userinfo', "upload={$user['u']}; download={$user['d']}; total={$user['transfer_enable']}; expire={$user['expired_at']}")
- ->header('profile-update-interval', '24')
- ->header('content-disposition', 'attachment;filename*=UTF-8\'\'' . rawurlencode($appName));
- }
-
- public static function buildShadowsocks($uuid, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $array = [];
- $array['name'] = $server['name'];
- $array['type'] = 'ss';
- $array['server'] = $server['host'];
- $array['port'] = $server['port'];
- $array['cipher'] = data_get($protocol_settings, 'cipher');
- $array['password'] = $uuid;
- $array['udp'] = true;
- if (data_get($protocol_settings, 'plugin') && data_get($protocol_settings, 'plugin_opts')) {
- $plugin = data_get($protocol_settings, 'plugin');
- $pluginOpts = data_get($protocol_settings, 'plugin_opts', '');
- $array['plugin'] = $plugin;
-
- // 解析插件选项
- $parsedOpts = collect(explode(';', $pluginOpts))
- ->filter()
- ->mapWithKeys(function ($pair) {
- if (!str_contains($pair, '=')) {
- return [];
- }
- [$key, $value] = explode('=', $pair, 2);
- return [trim($key) => trim($value)];
- })
- ->all();
-
- // 根据插件类型进行字段映射
- switch ($plugin) {
- case 'obfs':
- $array['plugin-opts'] = [
- 'mode' => $parsedOpts['obfs'],
- 'host' => $parsedOpts['obfs-host'],
- ];
-
- // 可选path参数
- if (isset($parsedOpts['path'])) {
- $array['plugin-opts']['path'] = $parsedOpts['path'];
- }
- break;
-
- case 'v2ray-plugin':
- $array['plugin-opts'] = [
- 'mode' => $parsedOpts['mode'] ?? 'websocket',
- 'tls' => isset($parsedOpts['tls']) && $parsedOpts['tls'] == 'true',
- 'host' => $parsedOpts['host'] ?? '',
- 'path' => $parsedOpts['path'] ?? '/',
- ];
- break;
-
- default:
- // 对于其他插件,直接使用解析出的键值对
- $array['plugin-opts'] = $parsedOpts;
- }
- }
- return $array;
- }
-
- public static function buildVmess($uuid, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $array = [];
- $array['name'] = $server['name'];
- $array['type'] = 'vmess';
- $array['server'] = $server['host'];
- $array['port'] = $server['port'];
- $array['uuid'] = $uuid;
- $array['alterId'] = 0;
- $array['cipher'] = 'auto';
- $array['udp'] = true;
-
- $array['tls'] = (bool) data_get($protocol_settings, 'tls');
- $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls_settings.allow_insecure', false);
- if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) {
- $array['servername'] = $serverName;
- }
-
- switch (data_get($protocol_settings, 'network')) {
- case 'tcp':
- $headerType = data_get($protocol_settings, 'network_settings.header.type', 'tcp');
- $array['network'] = ($headerType === 'http') ? 'http' : 'tcp';
- if ($headerType === 'http') {
- $array['http-opts']['path'] = data_get($protocol_settings, 'network_settings.header.request.path', ['/']);
- if ($host = data_get($protocol_settings, 'network_settings.header.request.headers.Host')) {
- $array['http-opts']['headers']['Host'] = $host;
- }
- }
- break;
- case 'ws':
- $array['network'] = 'ws';
- $array['ws-opts']['path'] = data_get($protocol_settings, 'network_settings.path');
- if ($host = data_get($protocol_settings, 'network_settings.headers.Host')) {
- $array['ws-opts']['headers'] = ['Host' => $host];
- }
- break;
- case 'grpc':
- $array['network'] = 'grpc';
- $array['grpc-opts'] = [];
- $array['grpc-opts']['grpc-service-name'] = data_get($protocol_settings, 'network_settings.serviceName');
- break;
- case 'h2':
- $array['network'] = 'h2';
- $array['tls'] = true;
- $array['h2-opts'] = [];
- if ($path = data_get($protocol_settings, 'network_settings.path'))
- $array['h2-opts']['path'] = $path;
- if ($host = data_get($protocol_settings, 'network_settings.host'))
- $array['h2-opts']['host'] = is_array($host) ? $host : [$host];
- break;
- default:
- break;
- }
- return $array;
- }
-
- public function buildVless($uuid, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $array = [];
- $array['name'] = $server['name'];
- $array['type'] = 'vless';
- $array['server'] = $server['host'];
- $array['port'] = $server['port'];
- $array['uuid'] = $uuid;
- $array['udp'] = true;
-
- if ($fingerprint = Helper::getTlsFingerprint(data_get($protocol_settings, 'utls'))) {
- $array['client-fingerprint'] = $fingerprint;
- }
-
- switch (data_get($protocol_settings, 'tls')) {
- case 1:
- $array['tls'] = true;
- $array['skip-cert-verify'] = data_get($protocol_settings, 'tls_settings.allow_insecure');
- if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) {
- $array['servername'] = $serverName;
- }
- break;
- case 2:
- $array['tls'] = true;
- $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'reality_settings.allow_insecure', false);
- if ($serverName = data_get($protocol_settings, 'reality_settings.server_name')) {
- $array['servername'] = $serverName;
- $array['sni'] = $serverName;
- }
- $array['flow'] = data_get($protocol_settings, 'flow');
- $array['reality-opts'] = [
- 'public-key' => data_get($protocol_settings, 'reality_settings.public_key'),
- 'short-id' => data_get($protocol_settings, 'reality_settings.short_id')
- ];
- break;
- }
-
- switch (data_get($protocol_settings, 'network')) {
- case 'tcp':
- $headerType = data_get($protocol_settings, 'network_settings.header.type', 'tcp');
- $array['network'] = ($headerType === 'http') ? 'http' : 'tcp';
- if ($headerType === 'http') {
- if (
- $httpOpts = array_filter([
- 'headers' => data_get($protocol_settings, 'network_settings.header.request.headers'),
- 'path' => data_get($protocol_settings, 'network_settings.header.request.path', ['/'])
- ])
- ) {
- $array['http-opts'] = $httpOpts;
- }
- }
- break;
- case 'ws':
- $array['network'] = 'ws';
- $array['ws-opts']['path'] = data_get($protocol_settings, 'network_settings.path');
- if ($host = data_get($protocol_settings, 'network_settings.headers.Host')) {
- $array['ws-opts']['headers'] = ['Host' => $host];
- }
- break;
- case 'grpc':
- $array['network'] = 'grpc';
- $array['grpc-opts']['grpc-service-name'] = data_get($protocol_settings, 'network_settings.serviceName');
- break;
- case 'h2':
- $array['network'] = 'h2';
- $array['h2-opts'] = [];
- if ($path = data_get($protocol_settings, 'network_settings.path'))
- $array['h2-opts']['path'] = $path;
- if ($host = data_get($protocol_settings, 'network_settings.host'))
- $array['h2-opts']['host'] = is_array($host) ? $host : [$host];
- break;
- }
-
- return $array;
- }
-
- public static function buildTrojan($password, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $array = [
- 'name' => $server['name'],
- 'type' => 'trojan',
- 'server' => $server['host'],
- 'port' => $server['port'],
- 'password' => $password,
- 'udp' => true,
- ];
-
- $tlsMode = (int) data_get($protocol_settings, 'tls', 1);
- switch ($tlsMode) {
- case 2: // Reality
- $array['tls'] = true;
- $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'reality_settings.allow_insecure', false);
- if ($serverName = data_get($protocol_settings, 'reality_settings.server_name')) {
- $array['sni'] = $serverName;
- }
- $array['reality-opts'] = [
- 'public-key' => data_get($protocol_settings, 'reality_settings.public_key'),
- 'short-id' => data_get($protocol_settings, 'reality_settings.short_id'),
- ];
- break;
- default: // Standard TLS
- if ($serverName = data_get($protocol_settings, 'server_name')) {
- $array['sni'] = $serverName;
- }
- $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'allow_insecure', false);
- break;
- }
-
- switch (data_get($protocol_settings, 'network')) {
- case 'tcp':
- $headerType = data_get($protocol_settings, 'network_settings.header.type', 'tcp');
- $array['network'] = ($headerType === 'http') ? 'http' : 'tcp';
- if ($headerType === 'http') {
- $array['http-opts']['path'] = data_get($protocol_settings, 'network_settings.header.request.path', ['/']);
- }
- break;
- case 'ws':
- $array['network'] = 'ws';
- $array['ws-opts']['path'] = data_get($protocol_settings, 'network_settings.path');
- if ($host = data_get($protocol_settings, 'network_settings.headers.Host')) {
- $array['ws-opts']['headers'] = ['Host' => $host];
- }
- break;
- case 'grpc':
- $array['network'] = 'grpc';
- if ($serviceName = data_get($protocol_settings, 'network_settings.serviceName'))
- $array['grpc-opts']['grpc-service-name'] = $serviceName;
- break;
- }
-
- return $array;
- }
-
- public static function buildHysteria($password, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $array['name'] = $server['name'];
- $array['server'] = $server['host'];
- $array['port'] = $server['port'];
- $array['up-speed'] = data_get($protocol_settings, 'bandwidth.up');
- $array['down-speed'] = data_get($protocol_settings, 'bandwidth.down');
- $array['skip-cert-verify'] = data_get($protocol_settings, 'tls.allow_insecure');
- if ($serverName = data_get($protocol_settings, 'tls.server_name')) {
- $array['sni'] = $serverName;
- }
- if (isset($server['ports'])) {
- $array['ports'] = $server['ports'];
- }
- switch (data_get($protocol_settings, 'version')) {
- case 1:
- $array['type'] = 'hysteria';
- $array['auth-str'] = $password;
- $array['protocol'] = 'udp';
- if (data_get($protocol_settings, 'obfs.open')) {
- $array['obfs'] = data_get($protocol_settings, 'obfs.password');
- }
- break;
- case 2:
- $array['type'] = 'hysteria2';
- $array['auth'] = $password;
- $array['fast-open'] = true;
- if (data_get($protocol_settings, 'obfs.open')) {
- $array['obfs'] = data_get($protocol_settings, 'obfs.type', 'salamander');
- $array['obfs-password'] = data_get($protocol_settings, 'obfs.password');
- }
- break;
- }
- return $array;
- }
-
- public static function buildTuic($password, $server)
- {
- $protocol_settings = data_get($server, 'protocol_settings', []);
- $array = [
- 'name' => $server['name'],
- 'type' => 'tuic',
- 'server' => $server['host'],
- 'port' => $server['port'],
- 'congestion-controller' => data_get($protocol_settings, 'congestion_control', 'cubic'),
- 'udp-relay-mode' => data_get($protocol_settings, 'udp_relay_mode', 'native'),
- 'alpn' => data_get($protocol_settings, 'alpn', ['h3']),
- 'reduce-rtt' => true,
- 'fast-open' => true,
- 'heartbeat-interval' => 10000,
- 'request-timeout' => 8000,
- 'max-udp-relay-packet-size' => 1500,
- 'version' => data_get($protocol_settings, 'version', 5),
- ];
-
- if (data_get($protocol_settings, 'version') === 4) {
- $array['token'] = $password;
- } else {
- $array['uuid'] = $password;
- $array['password'] = $password;
- }
-
- $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls.allow_insecure', false);
- if ($serverName = data_get($protocol_settings, 'tls.server_name')) {
- $array['sni'] = $serverName;
- }
-
- return $array;
- }
-
- public static function buildAnyTLS($password, $server)
- {
- $protocol_settings = data_get($server, 'protocol_settings', []);
- $array = [
- 'name' => $server['name'],
- 'type' => 'anytls',
- 'server' => $server['host'],
- 'port' => $server['port'],
- 'password' => $password,
- 'sni' => data_get($protocol_settings, 'tls.server_name'),
- 'skip-cert-verify' => (bool) data_get($protocol_settings, 'tls.allow_insecure', false),
- 'udp' => true,
- ];
-
- return $array;
- }
-
- public static function buildSocks5($password, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $array = [
- 'name' => $server['name'],
- 'type' => 'socks5',
- 'server' => $server['host'],
- 'port' => $server['port'],
- 'username' => $password,
- 'password' => $password,
- 'udp' => true,
- ];
-
- if (data_get($protocol_settings, 'tls')) {
- $array['tls'] = true;
- $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls_settings.allow_insecure', false);
- if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) {
- $array['sni'] = $serverName;
- }
- }
-
- return $array;
- }
-
- public static function buildHttp($password, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $array = [
- 'name' => $server['name'],
- 'type' => 'http',
- 'server' => $server['host'],
- 'port' => $server['port'],
- 'username' => $password,
- 'password' => $password,
- ];
-
- if (data_get($protocol_settings, 'tls')) {
- $array['tls'] = true;
- $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls_settings.allow_insecure', false);
- if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) {
- $array['sni'] = $serverName;
- }
- }
-
- return $array;
- }
-
- private function isRegex($exp)
- {
- if (empty($exp)) {
- return false;
- }
- try {
- return preg_match($exp, '') !== false;
- } catch (\Exception $e) {
- return false;
- }
- }
-
- private function isMatch($exp, $str)
- {
- try {
- return preg_match($exp, $str);
- } catch (\Exception $e) {
- return false;
- }
- }
-}
diff --git a/Xboard/app/Protocols/Surfboard.php b/Xboard/app/Protocols/Surfboard.php
deleted file mode 100644
index 23fcf09..0000000
--- a/Xboard/app/Protocols/Surfboard.php
+++ /dev/null
@@ -1,229 +0,0 @@
-servers;
- $user = $this->user;
-
- $appName = admin_setting('app_name', 'XBoard');
-
- $proxies = '';
- $proxyGroup = '';
-
- foreach ($servers as $item) {
- if (
- $item['type'] === Server::TYPE_SHADOWSOCKS
- && in_array(data_get($item, 'protocol_settings.cipher'), [
- 'aes-128-gcm',
- 'aes-192-gcm',
- 'aes-256-gcm',
- 'chacha20-ietf-poly1305',
- '2022-blake3-aes-128-gcm',
- '2022-blake3-aes-256-gcm',
- '2022-blake3-chacha20-poly1305'
- ])
- ) {
- // [Proxy]
- $proxies .= self::buildShadowsocks($item['password'], $item);
- // [Proxy Group]
- $proxyGroup .= $item['name'] . ', ';
- }
- if ($item['type'] === Server::TYPE_VMESS) {
- // [Proxy]
- $proxies .= self::buildVmess($item['password'], $item);
- // [Proxy Group]
- $proxyGroup .= $item['name'] . ', ';
- }
- if ($item['type'] === Server::TYPE_TROJAN) {
- // [Proxy]
- $proxies .= self::buildTrojan($item['password'], $item);
- // [Proxy Group]
- $proxyGroup .= $item['name'] . ', ';
- }
- if ($item['type'] === Server::TYPE_ANYTLS) {
- $proxies .= self::buildAnyTLS($item['password'], $item);
- $proxyGroup .= $item['name'] . ', ';
- }
- }
-
- $config = subscribe_template('surfboard');
- // Subscription link
- $subsURL = Helper::getSubscribeUrl($user['token']);
- $subsDomain = request()->header('Host');
-
- $config = str_replace('$subs_link', $subsURL, $config);
- $config = str_replace('$subs_domain', $subsDomain, $config);
- $config = str_replace('$proxies', $proxies, $config);
- $config = str_replace('$proxy_group', rtrim($proxyGroup, ', '), $config);
-
- $upload = round($user['u'] / (1024 * 1024 * 1024), 2);
- $download = round($user['d'] / (1024 * 1024 * 1024), 2);
- $useTraffic = $upload + $download;
- $totalTraffic = round($user['transfer_enable'] / (1024 * 1024 * 1024), 2);
- $unusedTraffic = $totalTraffic - $useTraffic;
- $expireDate = $user['expired_at'] === NULL ? '长期有效' : date('Y-m-d H:i:s', $user['expired_at']);
- $subscribeInfo = "title={$appName}订阅信息, content=上传流量:{$upload}GB\\n下载流量:{$download}GB\\n剩余流量:{$unusedTraffic}GB\\n套餐流量:{$totalTraffic}GB\\n到期时间:{$expireDate}";
- $config = str_replace('$subscribe_info', $subscribeInfo, $config);
-
- return response($config, 200)
- ->header('content-disposition', "attachment;filename*=UTF-8''" . rawurlencode($appName) . ".conf");
- }
-
-
- public static function buildShadowsocks($password, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $config = [
- "{$server['name']}=ss",
- "{$server['host']}",
- "{$server['port']}",
- "encrypt-method=" . data_get($protocol_settings, 'cipher'),
- "password={$password}",
- 'tfo=true',
- 'udp-relay=true'
- ];
-
-
- if (data_get($protocol_settings, 'plugin') && data_get($protocol_settings, 'plugin_opts')) {
- $plugin = data_get($protocol_settings, 'plugin');
- $pluginOpts = data_get($protocol_settings, 'plugin_opts', '');
- // 解析插件选项
- $parsedOpts = collect(explode(';', $pluginOpts))
- ->filter()
- ->mapWithKeys(function ($pair) {
- if (!str_contains($pair, '=')) {
- return [];
- }
- [$key, $value] = explode('=', $pair, 2);
- return [trim($key) => trim($value)];
- })
- ->all();
- switch ($plugin) {
- case 'obfs':
- $config[] = "obfs={$parsedOpts['obfs']}";
- if (isset($parsedOpts['obfs-host'])) {
- $config[] = "obfs-host={$parsedOpts['obfs-host']}";
- }
- if (isset($parsedOpts['path'])) {
- $config[] = "obfs-uri={$parsedOpts['path']}";
- }
- break;
- }
- }
-
- $config = array_filter($config);
- $uri = implode(',', $config);
- $uri .= "\r\n";
- return $uri;
- }
-
- public static function buildVmess($uuid, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $config = [
- "{$server['name']}=vmess",
- "{$server['host']}",
- "{$server['port']}",
- "username={$uuid}",
- "vmess-aead=true",
- 'tfo=true',
- 'udp-relay=true'
- ];
-
- if (data_get($protocol_settings, 'tls')) {
- array_push($config, 'tls=true');
- if (data_get($protocol_settings, 'tls_settings')) {
- $tlsSettings = data_get($protocol_settings, 'tls_settings');
- if (data_get($tlsSettings, 'allow_insecure')) {
- array_push($config, 'skip-cert-verify=' . ($tlsSettings['allow_insecure'] ? 'true' : 'false'));
- }
- if ($sni = data_get($tlsSettings, 'server_name')) {
- array_push($config, "sni={$sni}");
- }
- }
- }
- if (data_get($protocol_settings, 'network') === 'ws') {
- array_push($config, 'ws=true');
- if (data_get($protocol_settings, 'network_settings')) {
- $wsSettings = data_get($protocol_settings, 'network_settings');
- if (isset($wsSettings['path']) && !empty($wsSettings['path']))
- array_push($config, "ws-path={$wsSettings['path']}");
- if (isset($wsSettings['headers']['Host']) && !empty($wsSettings['headers']['Host']))
- array_push($config, "ws-headers=Host:{$wsSettings['headers']['Host']}");
- }
- }
-
- $uri = implode(',', $config);
- $uri .= "\r\n";
- return $uri;
- }
-
- public static function buildTrojan($password, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $config = [
- "{$server['name']}=trojan",
- "{$server['host']}",
- "{$server['port']}",
- "password={$password}",
- data_get($protocol_settings, 'server_name') ? "sni=" . data_get($protocol_settings, 'server_name') : "",
- 'tfo=true',
- 'udp-relay=true'
- ];
- if (data_get($protocol_settings, 'allow_insecure')) {
- array_push($config, !!data_get($protocol_settings, 'allow_insecure') ? 'skip-cert-verify=true' : 'skip-cert-verify=false');
- }
- $config = array_filter($config);
- $uri = implode(',', $config);
- $uri .= "\r\n";
- return $uri;
- }
-
- public static function buildAnyTLS($password, $server)
- {
- $protocol_settings = data_get($server, 'protocol_settings', []);
-
- $config = [
- "{$server['name']}=anytls",
- "{$server['host']}",
- "{$server['port']}",
- "password={$password}",
- "tfo=true",
- "udp-relay=true"
- ];
-
- // SNI
- if ($serverName = data_get($protocol_settings, 'tls.server_name')) {
- $config[] = "sni={$serverName}";
- }
-
- // 跳过证书校验
- if (data_get($protocol_settings, 'tls.allow_insecure')) {
- $config[] = "skip-cert-verify=true";
- }
-
- $config = array_filter($config);
-
- return implode(',', $config) . "\r\n";
- }
-}
diff --git a/Xboard/app/Protocols/Surge.php b/Xboard/app/Protocols/Surge.php
deleted file mode 100644
index ee1ea62..0000000
--- a/Xboard/app/Protocols/Surge.php
+++ /dev/null
@@ -1,319 +0,0 @@
- [2 => '2398'],
- ];
-
- public function handle()
- {
- $servers = $this->servers;
- $user = $this->user;
-
- $appName = admin_setting('app_name', 'XBoard');
-
- $proxies = '';
- $proxyGroup = '';
-
- foreach ($servers as $item) {
- if (
- $item['type'] === Server::TYPE_SHADOWSOCKS
- && in_array(data_get($item, 'protocol_settings.cipher'), [
- 'aes-128-gcm',
- 'aes-192-gcm',
- 'aes-256-gcm',
- 'chacha20-ietf-poly1305',
- '2022-blake3-aes-128-gcm',
- '2022-blake3-aes-256-gcm'
- ])
- ) {
- $proxies .= self::buildShadowsocks($item['password'], $item);
- $proxyGroup .= $item['name'] . ', ';
- }
- if ($item['type'] === Server::TYPE_VMESS) {
- $proxies .= self::buildVmess($item['password'], $item);
- $proxyGroup .= $item['name'] . ', ';
- }
- if ($item['type'] === Server::TYPE_TROJAN) {
- $proxies .= self::buildTrojan($item['password'], $item);
- $proxyGroup .= $item['name'] . ', ';
- }
- if ($item['type'] === Server::TYPE_HYSTERIA) {
- $proxies .= self::buildHysteria($item['password'], $item);
- $proxyGroup .= $item['name'] . ', ';
- }
- if ($item['type'] === Server::TYPE_ANYTLS) {
- $proxies .= self::buildAnyTLS($item['password'], $item);
- $proxyGroup .= $item['name'] . ', ';
- }
- if ($item['type'] === Server::TYPE_SOCKS) {
- $proxies .= self::buildSocks($item['password'], $item);
- $proxyGroup .= $item['name'] . ', ';
- }
- if ($item['type'] === Server::TYPE_HTTP) {
- $proxies .= self::buildHttp($item['password'], $item);
- $proxyGroup .= $item['name'] . ', ';
- }
- }
-
-
- $config = subscribe_template('surge');
-
- // Subscription link
- $subsDomain = request()->header('Host');
- $subsURL = Helper::getSubscribeUrl($user['token'], $subsDomain ? 'https://' . $subsDomain : null);
-
- $config = str_replace('$subs_link', $subsURL, $config);
- $config = str_replace('$subs_domain', $subsDomain, $config);
- $config = str_replace('$proxies', $proxies, $config);
- $config = str_replace('$proxy_group', rtrim($proxyGroup, ', '), $config);
-
- $upload = round($user['u'] / (1024 * 1024 * 1024), 2);
- $download = round($user['d'] / (1024 * 1024 * 1024), 2);
- $useTraffic = $upload + $download;
- $totalTraffic = round($user['transfer_enable'] / (1024 * 1024 * 1024), 2);
- $unusedTraffic = $totalTraffic - $useTraffic;
- $expireDate = $user['expired_at'] === NULL ? '长期有效' : date('Y-m-d H:i:s', $user['expired_at']);
- $subscribeInfo = "title={$appName}订阅信息, content=上传流量:{$upload}GB\\n下载流量:{$download}GB\\n剩余流量:{$unusedTraffic}GB\\n套餐流量:{$totalTraffic}GB\\n到期时间:{$expireDate}";
- $config = str_replace('$subscribe_info', $subscribeInfo, $config);
-
- return response($config, 200)
- ->header('content-type', 'application/octet-stream')
- ->header('content-disposition', "attachment;filename*=UTF-8''" . rawurlencode($appName) . ".conf");
- }
-
-
- public static function buildShadowsocks($password, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $config = [
- "{$server['name']} = ss",
- "{$server['host']}",
- "{$server['port']}",
- "encrypt-method={$protocol_settings['cipher']}",
- "password={$password}",
- 'tfo=true',
- 'udp-relay=true'
- ];
- if (data_get($protocol_settings, 'plugin') && data_get($protocol_settings, 'plugin_opts')) {
- $plugin = data_get($protocol_settings, 'plugin');
- $pluginOpts = data_get($protocol_settings, 'plugin_opts', '');
- // 解析插件选项
- $parsedOpts = collect(explode(';', $pluginOpts))
- ->filter()
- ->mapWithKeys(function ($pair) {
- if (!str_contains($pair, '=')) {
- return [];
- }
- [$key, $value] = explode('=', $pair, 2);
- return [trim($key) => trim($value)];
- })
- ->all();
- switch ($plugin) {
- case 'obfs':
- $config[] = "obfs={$parsedOpts['obfs']}";
- if (isset($parsedOpts['obfs-host'])) {
- $config[] = "obfs-host={$parsedOpts['obfs-host']}";
- }
- if (isset($parsedOpts['path'])) {
- $config[] = "obfs-uri={$parsedOpts['path']}";
- }
- break;
- }
- }
- $config = array_filter($config);
- $uri = implode(',', $config);
- $uri .= "\r\n";
- return $uri;
- }
-
- public static function buildVmess($uuid, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $config = [
- "{$server['name']} = vmess",
- "{$server['host']}",
- "{$server['port']}",
- "username={$uuid}",
- "vmess-aead=true",
- 'tfo=true',
- 'udp-relay=true'
- ];
-
- if (data_get($protocol_settings, 'tls')) {
- array_push($config, 'tls=true');
- if (data_get($protocol_settings, 'tls_settings')) {
- $tlsSettings = data_get($protocol_settings, 'tls_settings');
- if (data_get($tlsSettings, 'allow_insecure'))
- array_push($config, 'skip-cert-verify=' . ($tlsSettings['allow_insecure'] ? 'true' : 'false'));
- if (data_get($tlsSettings, 'server_name'))
- array_push($config, "sni={$tlsSettings['server_name']}");
- }
- }
- if (data_get($protocol_settings, 'network') === 'ws') {
- array_push($config, 'ws=true');
- if (data_get($protocol_settings, 'network_settings')) {
- $wsSettings = data_get($protocol_settings, 'network_settings');
- if (data_get($wsSettings, 'path'))
- array_push($config, "ws-path={$wsSettings['path']}");
- if (data_get($wsSettings, 'headers.Host'))
- array_push($config, "ws-headers=Host:{$wsSettings['headers']['Host']}");
- }
- }
-
- $uri = implode(',', $config);
- $uri .= "\r\n";
- return $uri;
- }
-
- public static function buildTrojan($password, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- $config = [
- "{$server['name']} = trojan",
- "{$server['host']}",
- "{$server['port']}",
- "password={$password}",
- data_get($protocol_settings, 'server_name') ? "sni=" . data_get($protocol_settings, 'server_name') : "",
- 'tfo=true',
- 'udp-relay=true'
- ];
- if (!empty($protocol_settings['allow_insecure'])) {
- array_push($config, !!data_get($protocol_settings, 'allow_insecure') ? 'skip-cert-verify=true' : 'skip-cert-verify=false');
- }
- $config = array_filter($config);
- $uri = implode(',', $config);
- $uri .= "\r\n";
- return $uri;
- }
-
- //参考文档: https://manual.nssurge.com/policy/proxy.html
- public static function buildAnyTLS($password, $server)
- {
- $protocol_settings = data_get($server, 'protocol_settings', []);
- $config = [
- "{$server['name']} = anytls",
- "{$server['host']}",
- "{$server['port']}",
- "password={$password}",
- ];
- if ($serverName = data_get($protocol_settings, 'tls.server_name')) {
- $config[] = "sni={$serverName}";
- }
- if (data_get($protocol_settings, 'tls.allow_insecure')) {
- $config[] = 'skip-cert-verify=true';
- }
- $config = array_filter($config);
- $uri = implode(',', $config);
- $uri .= "\r\n";
- return $uri;
- }
-
- //参考文档: https://manual.nssurge.com/policy/proxy.html
- public static function buildHysteria($password, $server)
- {
- $protocol_settings = $server['protocol_settings'];
- if ($protocol_settings['version'] != 2)
- return '';
- $config = [
- "{$server['name']} = hysteria2",
- "{$server['host']}",
- "{$server['port']}",
- "password={$password}",
- $protocol_settings['tls']['server_name'] ? "sni={$protocol_settings['tls']['server_name']}" : "",
- // 'tfo=true',
- 'udp-relay=true'
- ];
- if (data_get($protocol_settings, 'bandwidth.up')) {
- $config[] = "upload-bandwidth={$protocol_settings['bandwidth']['up']}";
- }
- if (data_get($protocol_settings, 'bandwidth.down')) {
- $config[] = "download-bandwidth={$protocol_settings['bandwidth']['down']}";
- }
- if (data_get($protocol_settings, 'tls.allow_insecure')) {
- $config[] = !!data_get($protocol_settings, 'tls.allow_insecure') ? 'skip-cert-verify=true' : 'skip-cert-verify=false';
- }
- $config = array_filter($config);
- $uri = implode(',', $config);
- $uri .= "\r\n";
- return $uri;
- }
-
- //参考文档: https://manual.nssurge.com/policy/proxy.html
- public static function buildSocks($password, $server)
- {
- $protocol_settings = data_get($server, 'protocol_settings', []);
- $type = data_get($protocol_settings, 'tls') ? 'socks5-tls' : 'socks5';
- $config = [
- "{$server['name']} = {$type}",
- "{$server['host']}",
- "{$server['port']}",
- "{$password}",
- "{$password}",
- ];
-
- if (data_get($protocol_settings, 'tls')) {
- if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) {
- $config[] = "sni={$serverName}";
- }
- if (data_get($protocol_settings, 'tls_settings.allow_insecure')) {
- $config[] = 'skip-cert-verify=true';
- }
- }
- $config[] = 'udp-relay=true';
-
- $config = array_filter($config);
- $uri = implode(',', $config);
- $uri .= "\r\n";
- return $uri;
- }
-
- //参考文档: https://manual.nssurge.com/policy/proxy.html
- public static function buildHttp($password, $server)
- {
- $protocol_settings = data_get($server, 'protocol_settings', []);
- $type = data_get($protocol_settings, 'tls') ? 'https' : 'http';
- $config = [
- "{$server['name']} = {$type}",
- "{$server['host']}",
- "{$server['port']}",
- "{$password}",
- "{$password}",
- ];
-
- if (data_get($protocol_settings, 'tls')) {
- if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) {
- $config[] = "sni={$serverName}";
- }
- if (data_get($protocol_settings, 'tls_settings.allow_insecure')) {
- $config[] = 'skip-cert-verify=true';
- }
- }
-
- $config = array_filter($config);
- $uri = implode(',', $config);
- $uri .= "\r\n";
- return $uri;
- }
-}
diff --git a/Xboard/app/Providers/AuthServiceProvider.php b/Xboard/app/Providers/AuthServiceProvider.php
deleted file mode 100644
index b92a7dd..0000000
--- a/Xboard/app/Providers/AuthServiceProvider.php
+++ /dev/null
@@ -1,28 +0,0 @@
-
- */
- protected $policies = [
- // 'App\Model' => 'App\Policies\ModelPolicy',
- ];
-
- /**
- * 注册任何认证/授权服务
- * @return void
- */
- public function boot()
- {
- $this->registerPolicies();
-
- //
- }
-}
diff --git a/Xboard/app/Providers/BroadcastServiceProvider.php b/Xboard/app/Providers/BroadcastServiceProvider.php
deleted file mode 100644
index 395c518..0000000
--- a/Xboard/app/Providers/BroadcastServiceProvider.php
+++ /dev/null
@@ -1,21 +0,0 @@
->
- */
- protected $listen = [
- ];
-
- /**
- * 注册任何事件
- * @return void
- */
- public function boot()
- {
- parent::boot();
-
- User::observe(UserObserver::class);
- Plan::observe(PlanObserver::class);
- Server::observe(ServerObserver::class);
- ServerRoute::observe(ServerRouteObserver::class);
-
-
- }
-}
diff --git a/Xboard/app/Providers/HorizonServiceProvider.php b/Xboard/app/Providers/HorizonServiceProvider.php
deleted file mode 100644
index 119fd69..0000000
--- a/Xboard/app/Providers/HorizonServiceProvider.php
+++ /dev/null
@@ -1,43 +0,0 @@
-email, [
- //
- ]);
- });
- }
-}
diff --git a/Xboard/app/Providers/OctaneServiceProvider.php b/Xboard/app/Providers/OctaneServiceProvider.php
deleted file mode 100644
index 4cdd91c..0000000
--- a/Xboard/app/Providers/OctaneServiceProvider.php
+++ /dev/null
@@ -1,43 +0,0 @@
-app->runningInConsole()) {
- return;
- }
- if ($this->app->bound('octane')) {
- $this->app['events']->listen(WorkerStarting::class, function () {
- app(UpdateService::class)->updateVersionCache();
- HookManager::reset();
- });
- }
- // 每半钟执行一次调度检查
- Octane::tick('scheduler', function () {
- $lock = Cache::lock('scheduler-lock', 30);
-
- if ($lock->get()) {
- try {
- Artisan::call('schedule:run');
- } finally {
- $lock->release();
- }
- }
- })->seconds(30);
- }
-}
\ No newline at end of file
diff --git a/Xboard/app/Providers/PluginServiceProvider.php b/Xboard/app/Providers/PluginServiceProvider.php
deleted file mode 100644
index f0352ec..0000000
--- a/Xboard/app/Providers/PluginServiceProvider.php
+++ /dev/null
@@ -1,29 +0,0 @@
-app->scoped(PluginManager::class, function ($app) {
- return new PluginManager();
- });
- }
-
- public function boot(): void
- {
- if (!file_exists(base_path('plugins'))) {
- mkdir(base_path('plugins'), 0755, true);
- }
- }
-}
\ No newline at end of file
diff --git a/Xboard/app/Providers/ProtocolServiceProvider.php b/Xboard/app/Providers/ProtocolServiceProvider.php
deleted file mode 100644
index dfa80ee..0000000
--- a/Xboard/app/Providers/ProtocolServiceProvider.php
+++ /dev/null
@@ -1,50 +0,0 @@
-app->scoped('protocols.manager', function ($app) {
- return new ProtocolManager($app);
- });
-
- $this->app->scoped('protocols.flags', function ($app) {
- return $app->make('protocols.manager')->getAllFlags();
- });
- }
-
- /**
- * 启动服务
- *
- * @return void
- */
- public function boot()
- {
- // 在启动时预加载协议类并缓存
- $this->app->make('protocols.manager')->registerAllProtocols();
-
- }
-
- /**
- * 提供的服务
- *
- * @return array
- */
- public function provides()
- {
- return [
- 'protocols.manager',
- 'protocols.flags',
- ];
- }
-}
\ No newline at end of file
diff --git a/Xboard/app/Providers/RouteServiceProvider.php b/Xboard/app/Providers/RouteServiceProvider.php
deleted file mode 100644
index d26a599..0000000
--- a/Xboard/app/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,87 +0,0 @@
-mapApiRoutes();
- $this->mapWebRoutes();
-
- //
- }
-
- /**
- * Define the "web" routes for the application.
- *
- * These routes all receive session state, CSRF protection, etc.
- *
- * @return void
- */
- protected function mapWebRoutes()
- {
- Route::middleware('web')
- ->namespace($this->namespace)
- ->group(base_path('routes/web.php'));
- }
-
- /**
- * Define the "api" routes for the application.
- *
- * These routes are typically stateless.
- *
- * @return void
- */
- protected function mapApiRoutes()
- {
- Route::group([
- 'prefix' => '/api/v1',
- 'middleware' => 'api',
- 'namespace' => $this->namespace
- ], function ($router) {
- foreach (glob(app_path('Http//Routes//V1') . '/*.php') as $file) {
- $this->app->make('App\\Http\\Routes\\V1\\' . basename($file, '.php'))->map($router);
- }
- });
-
-
- Route::group([
- 'prefix' => '/api/v2',
- 'middleware' => 'api',
- 'namespace' => $this->namespace
- ], function ($router) {
- foreach (glob(app_path('Http//Routes//V2') . '/*.php') as $file) {
- $this->app->make('App\\Http\\Routes\\V2\\' . basename($file, '.php'))->map($router);
- }
- });
- }
-}
diff --git a/Xboard/app/Providers/SettingServiceProvider.php b/Xboard/app/Providers/SettingServiceProvider.php
deleted file mode 100644
index eee267f..0000000
--- a/Xboard/app/Providers/SettingServiceProvider.php
+++ /dev/null
@@ -1,33 +0,0 @@
-app->scoped(Setting::class, function (Application $app) {
- return new Setting();
- });
-
- }
-
- /**
- * Bootstrap services.
- *
- * @return void
- */
- public function boot()
- {
- // App URL is forced per-request via middleware (Octane-safe).
- }
-}
diff --git a/Xboard/app/Scope/FilterScope.php b/Xboard/app/Scope/FilterScope.php
deleted file mode 100644
index fcb92a5..0000000
--- a/Xboard/app/Scope/FilterScope.php
+++ /dev/null
@@ -1,50 +0,0 @@
-validate([
- 'filter.*.key' => "required|in:{$allowKeys}",
- 'filter.*.condition' => 'required|in:in,is,not,like,lt,gt',
- 'filter.*.value' => 'required'
- ]);
- $filters = $request->input('filter');
- if ($filters) {
- foreach ($filters as $k => $filter) {
- if ($filter['condition'] === 'in') {
- $builder->whereIn($filter['key'], $filter['value']);
- continue;
- }
- if ($filter['condition'] === 'is') {
- $builder->where($filter['key'], $filter['value']);
- continue;
- }
- if ($filter['condition'] === 'not') {
- $builder->where($filter['key'], '!=', $filter['value']);
- continue;
- }
- if ($filter['condition'] === 'gt') {
- $builder->where($filter['key'], '>', $filter['value']);
- continue;
- }
- if ($filter['condition'] === 'lt') {
- $builder->where($filter['key'], '<', $filter['value']);
- continue;
- }
- if ($filter['condition'] === 'like') {
- $builder->where($filter['key'], 'like', "%{$filter['value']}%");
- continue;
- }
- }
- }
- return $builder;
- }
-}
\ No newline at end of file
diff --git a/Xboard/app/Services/Auth/LoginService.php b/Xboard/app/Services/Auth/LoginService.php
deleted file mode 100644
index 363e82b..0000000
--- a/Xboard/app/Services/Auth/LoginService.php
+++ /dev/null
@@ -1,154 +0,0 @@
-= (int) admin_setting('password_limit_count', 5)) {
- return [
- false,
- [
- 429,
- __('There are too many password errors, please try again after :minute minutes.', [
- 'minute' => admin_setting('password_limit_expire', 60)
- ])
- ]
- ];
- }
- }
-
- // 查找用户
- $user = User::byEmail($email)->first();
- if (!$user) {
- return [false, [400, __('Incorrect email or password')]];
- }
-
- // 验证密码
- if (
- !Helper::multiPasswordVerify(
- $user->password_algo,
- $user->password_salt,
- $password,
- $user->password
- )
- ) {
- // 增加密码错误计数
- if ((int) admin_setting('password_limit_enable', true)) {
- $passwordErrorCount = (int) Cache::get(CacheKey::get('PASSWORD_ERROR_LIMIT', $email), 0);
- Cache::put(
- CacheKey::get('PASSWORD_ERROR_LIMIT', $email),
- (int) $passwordErrorCount + 1,
- 60 * (int) admin_setting('password_limit_expire', 60)
- );
- }
- return [false, [400, __('Incorrect email or password')]];
- }
-
- // 检查账户状态
- if ($user->banned) {
- return [false, [400, __('Your account has been suspended')]];
- }
-
- // 更新最后登录时间
- $user->last_login_at = time();
- $user->save();
-
- HookManager::call('user.login.after', $user);
- return [true, $user];
- }
-
- /**
- * 处理密码重置
- *
- * @param string $email 用户邮箱
- * @param string $emailCode 邮箱验证码
- * @param string $password 新密码
- * @return array [成功状态, 结果或错误信息]
- */
- public function resetPassword(string $email, string $emailCode, string $password): array
- {
- // 检查重置请求限制
- $forgetRequestLimitKey = CacheKey::get('FORGET_REQUEST_LIMIT', $email);
- $forgetRequestLimit = (int) Cache::get($forgetRequestLimitKey);
- if ($forgetRequestLimit >= 3) {
- return [false, [429, __('Reset failed, Please try again later')]];
- }
-
- // 验证邮箱验证码
- if ((string) Cache::get(CacheKey::get('EMAIL_VERIFY_CODE', $email)) !== (string) $emailCode) {
- Cache::put($forgetRequestLimitKey, $forgetRequestLimit ? $forgetRequestLimit + 1 : 1, 300);
- return [false, [400, __('Incorrect email verification code')]];
- }
-
- // 查找用户
- $user = User::byEmail($email)->first();
- if (!$user) {
- return [false, [400, __('This email is not registered in the system')]];
- }
-
- // 更新密码
- $user->password = password_hash($password, PASSWORD_DEFAULT);
- $user->password_algo = NULL;
- $user->password_salt = NULL;
-
- if (!$user->save()) {
- return [false, [500, __('Reset failed')]];
- }
-
- HookManager::call('user.password.reset.after', $user);
-
- // 清除邮箱验证码
- Cache::forget(CacheKey::get('EMAIL_VERIFY_CODE', $email));
-
- return [true, true];
- }
-
-
- /**
- * 生成临时登录令牌和快速登录URL
- *
- * @param User $user 用户对象
- * @param string $redirect 重定向路径
- * @return string|null 快速登录URL
- */
- public function generateQuickLoginUrl(User $user, ?string $redirect = null): ?string
- {
- if (!$user || !$user->exists) {
- return null;
- }
-
- $code = Helper::guid();
- $key = CacheKey::get('TEMP_TOKEN', $code);
-
- Cache::put($key, $user->id, 60);
-
- $redirect = $redirect ?: 'dashboard';
- $loginRedirect = '/#/login?verify=' . $code . '&redirect=' . rawurlencode($redirect);
-
- if (admin_setting('app_url')) {
- $url = admin_setting('app_url') . $loginRedirect;
- } else {
- $url = url($loginRedirect);
- }
-
- return $url;
- }
-}
\ No newline at end of file
diff --git a/Xboard/app/Services/Auth/MailLinkService.php b/Xboard/app/Services/Auth/MailLinkService.php
deleted file mode 100644
index 259ced7..0000000
--- a/Xboard/app/Services/Auth/MailLinkService.php
+++ /dev/null
@@ -1,100 +0,0 @@
-first();
- if (!$user) {
- return [true, true]; // 成功但用户不存在,保护用户隐私
- }
-
- $code = Helper::guid();
- $key = CacheKey::get('TEMP_TOKEN', $code);
- Cache::put($key, $user->id, 300);
- Cache::put(CacheKey::get('LAST_SEND_LOGIN_WITH_MAIL_LINK_TIMESTAMP', $email), time(), 60);
-
- $redirectUrl = '/#/login?verify=' . $code . '&redirect=' . ($redirect ? $redirect : 'dashboard');
- if (admin_setting('app_url')) {
- $link = admin_setting('app_url') . $redirectUrl;
- } else {
- $link = url($redirectUrl);
- }
-
- $this->sendMailLinkEmail($user, $link);
-
- return [true, $link];
- }
-
- /**
- * 发送邮件链接登录邮件
- *
- * @param User $user 用户对象
- * @param string $link 登录链接
- * @return void
- */
- private function sendMailLinkEmail(User $user, string $link): void
- {
- SendEmailJob::dispatch([
- 'email' => $user->email,
- 'subject' => __('Login to :name', [
- 'name' => admin_setting('app_name', 'XBoard')
- ]),
- 'template_name' => 'login',
- 'template_value' => [
- 'name' => admin_setting('app_name', 'XBoard'),
- 'link' => $link,
- 'url' => admin_setting('app_url')
- ]
- ]);
- }
-
- /**
- * 处理Token登录
- *
- * @param string $token 登录令牌
- * @return int|null 用户ID或null
- */
- public function handleTokenLogin(string $token): ?int
- {
- $key = CacheKey::get('TEMP_TOKEN', $token);
- $userId = Cache::get($key);
-
- if (!$userId) {
- return null;
- }
-
- $user = User::find($userId);
-
- if (!$user || $user->banned) {
- return null;
- }
-
- Cache::forget($key);
-
- return $userId;
- }
-}
\ No newline at end of file
diff --git a/Xboard/app/Services/Auth/RegisterService.php b/Xboard/app/Services/Auth/RegisterService.php
deleted file mode 100644
index 78d207e..0000000
--- a/Xboard/app/Services/Auth/RegisterService.php
+++ /dev/null
@@ -1,193 +0,0 @@
-ip())) ?? 0;
- if ((int) $registerCountByIP >= (int) admin_setting('register_limit_count', 3)) {
- return [
- false,
- [
- 429,
- __('Register frequently, please try again after :minute minute', [
- 'minute' => admin_setting('register_limit_expire', 60)
- ])
- ]
- ];
- }
- }
-
- // 检查验证码
- $captchaService = app(CaptchaService::class);
- [$captchaValid, $captchaError] = $captchaService->verify($request);
- if (!$captchaValid) {
- return [false, $captchaError];
- }
-
- // 检查邮箱白名单
- if ((int) admin_setting('email_whitelist_enable', 0)) {
- if (
- !Helper::emailSuffixVerify(
- $request->input('email'),
- admin_setting('email_whitelist_suffix', Dict::EMAIL_WHITELIST_SUFFIX_DEFAULT)
- )
- ) {
- return [false, [400, __('Email suffix is not in the Whitelist')]];
- }
- }
-
- // 检查Gmail限制
- if ((int) admin_setting('email_gmail_limit_enable', 0)) {
- $prefix = explode('@', $request->input('email'))[0];
- if (strpos($prefix, '.') !== false || strpos($prefix, '+') !== false) {
- return [false, [400, __('Gmail alias is not supported')]];
- }
- }
-
- // 检查是否关闭注册
- if ((int) admin_setting('stop_register', 0)) {
- return [false, [400, __('Registration has closed')]];
- }
-
- // 检查邀请码要求
- if ((int) admin_setting('invite_force', 0)) {
- if (empty($request->input('invite_code'))) {
- return [false, [422, __('You must use the invitation code to register')]];
- }
- }
-
- // 检查邮箱验证
- if ((int) admin_setting('email_verify', 0)) {
- if (empty($request->input('email_code'))) {
- return [false, [422, __('Email verification code cannot be empty')]];
- }
- if ((string) Cache::get(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email'))) !== (string) $request->input('email_code')) {
- return [false, [400, __('Incorrect email verification code')]];
- }
- }
-
- // 检查邮箱是否存在
- $exist = User::byEmail($request->input('email'))->first();
- if ($exist) {
- return [false, [400201, __('Email already exists')]];
- }
-
- return [true, null];
- }
-
- /**
- * 处理邀请码
- *
- * @param string $inviteCode 邀请码
- * @return int|null 邀请人ID
- */
- public function handleInviteCode(string $inviteCode): int|null
- {
- $inviteCodeModel = InviteCode::where('code', $inviteCode)
- ->where('status', InviteCode::STATUS_UNUSED)
- ->first();
-
- if (!$inviteCodeModel) {
- if ((int) admin_setting('invite_force', 0)) {
- throw new ApiException(__('Invalid invitation code'));
- }
- return null;
- }
-
- if (!(int) admin_setting('invite_never_expire', 0)) {
- $inviteCodeModel->status = InviteCode::STATUS_USED;
- $inviteCodeModel->save();
- }
-
- return $inviteCodeModel->user_id;
- }
-
-
-
- /**
- * 注册用户
- *
- * @param Request $request 请求对象
- * @return array [成功状态, 用户对象或错误信息]
- */
- public function register(Request $request): array
- {
- // 验证注册数据
- [$valid, $error] = $this->validateRegister($request);
- if (!$valid) {
- return [false, $error];
- }
-
- HookManager::call('user.register.before', $request);
-
- $email = $request->input('email');
- $password = $request->input('password');
- $inviteCode = $request->input('invite_code');
-
- // 处理邀请码获取邀请人ID
- $inviteUserId = null;
- if ($inviteCode) {
- $inviteUserId = $this->handleInviteCode($inviteCode);
- }
-
- // 创建用户
- $userService = app(UserService::class);
- $user = $userService->createUser([
- 'email' => $email,
- 'password' => $password,
- 'invite_user_id' => $inviteUserId,
- ]);
-
- // 保存用户
- if (!$user->save()) {
- return [false, [500, __('Register failed')]];
- }
-
- HookManager::call('user.register.after', $user);
-
- // 清除邮箱验证码
- if ((int) admin_setting('email_verify', 0)) {
- Cache::forget(CacheKey::get('EMAIL_VERIFY_CODE', $email));
- }
-
- // 更新最近登录时间
- $user->last_login_at = time();
- $user->save();
-
- // 更新IP注册计数
- if ((int) admin_setting('register_limit_by_ip_enable', 0)) {
- $registerCountByIP = Cache::get(CacheKey::get('REGISTER_IP_RATE_LIMIT', $request->ip())) ?? 0;
- Cache::put(
- CacheKey::get('REGISTER_IP_RATE_LIMIT', $request->ip()),
- (int) $registerCountByIP + 1,
- (int) admin_setting('register_limit_expire', 60) * 60
- );
- }
-
- return [true, $user];
- }
-}
\ No newline at end of file
diff --git a/Xboard/app/Services/AuthService.php b/Xboard/app/Services/AuthService.php
deleted file mode 100644
index 299f610..0000000
--- a/Xboard/app/Services/AuthService.php
+++ /dev/null
@@ -1,87 +0,0 @@
-user = $user;
- }
-
- public function generateAuthData(): array
- {
- // Create a new Sanctum token with device info
- $token = $this->user->createToken(
- Str::random(20), // token name (device identifier)
- ['*'], // abilities
- now()->addYear() // expiration
- );
-
- // Format token: remove ID prefix and add Bearer
- $tokenParts = explode('|', $token->plainTextToken);
- $formattedToken = 'Bearer ' . ($tokenParts[1] ?? $tokenParts[0]);
-
- return [
- 'token' => $this->user->token,
- 'auth_data' => $formattedToken,
- 'is_admin' => $this->user->is_admin,
- ];
- }
-
- public function getSessions(): array
- {
- return $this->user->tokens()->get()->toArray();
- }
-
- public function removeSession(string $sessionId): bool
- {
- $this->user->tokens()->where('id', $sessionId)->delete();
- return true;
- }
-
- public function removeAllSessions(): bool
- {
- $this->user->tokens()->delete();
- return true;
- }
-
- public static function findUserByBearerToken(string $bearerToken): ?User
- {
- $token = str_replace('Bearer ', '', $bearerToken);
-
- $accessToken = PersonalAccessToken::findToken($token);
-
- $tokenable = $accessToken?->tokenable;
-
- return $tokenable instanceof User ? $tokenable : null;
- }
-
- /**
- * 解密认证数据
- *
- * @param string $authorization
- * @return array|null 用户数据或null
- */
- public static function decryptAuthData(string $authorization): ?array
- {
- $user = self::findUserByBearerToken($authorization);
-
- if (!$user) {
- return null;
- }
-
- return [
- 'id' => $user->id,
- 'email' => $user->email,
- 'is_admin' => (bool)$user->is_admin,
- 'is_staff' => (bool)$user->is_staff
- ];
- }
-}
diff --git a/Xboard/app/Services/CaptchaService.php b/Xboard/app/Services/CaptchaService.php
deleted file mode 100644
index b6a4f05..0000000
--- a/Xboard/app/Services/CaptchaService.php
+++ /dev/null
@@ -1,112 +0,0 @@
- $this->verifyTurnstile($request),
- 'recaptcha-v3' => $this->verifyRecaptchaV3($request),
- 'recaptcha' => $this->verifyRecaptcha($request),
- default => [false, [400, __('Invalid captcha type')]]
- };
- }
-
- /**
- * 验证 Cloudflare Turnstile
- *
- * @param Request $request
- * @return array
- */
- private function verifyTurnstile(Request $request): array
- {
- $turnstileToken = $request->input('turnstile_token');
- if (!$turnstileToken) {
- return [false, [400, __('Invalid code is incorrect')]];
- }
-
- $response = Http::post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [
- 'secret' => admin_setting('turnstile_secret_key'),
- 'response' => $turnstileToken,
- 'remoteip' => $request->ip()
- ]);
-
- $result = $response->json();
- if (!$result['success']) {
- return [false, [400, __('Invalid code is incorrect')]];
- }
-
- return [true, null];
- }
-
- /**
- * 验证 Google reCAPTCHA v3
- *
- * @param Request $request
- * @return array
- */
- private function verifyRecaptchaV3(Request $request): array
- {
- $recaptchaV3Token = $request->input('recaptcha_v3_token');
- if (!$recaptchaV3Token) {
- return [false, [400, __('Invalid code is incorrect')]];
- }
-
- $recaptcha = new ReCaptcha(admin_setting('recaptcha_v3_secret_key'));
- $recaptchaResp = $recaptcha->verify($recaptchaV3Token, $request->ip());
-
- if (!$recaptchaResp->isSuccess()) {
- return [false, [400, __('Invalid code is incorrect')]];
- }
-
- // 检查分数阈值(如果有的话)
- $score = $recaptchaResp->getScore();
- $threshold = admin_setting('recaptcha_v3_score_threshold', 0.5);
- if ($score < $threshold) {
- return [false, [400, __('Invalid code is incorrect')]];
- }
-
- return [true, null];
- }
-
- /**
- * 验证 Google reCAPTCHA v2
- *
- * @param Request $request
- * @return array
- */
- private function verifyRecaptcha(Request $request): array
- {
- $recaptchaData = $request->input('recaptcha_data');
- if (!$recaptchaData) {
- return [false, [400, __('Invalid code is incorrect')]];
- }
-
- $recaptcha = new ReCaptcha(admin_setting('recaptcha_key'));
- $recaptchaResp = $recaptcha->verify($recaptchaData);
-
- if (!$recaptchaResp->isSuccess()) {
- return [false, [400, __('Invalid code is incorrect')]];
- }
-
- return [true, null];
- }
-}
\ No newline at end of file
diff --git a/Xboard/app/Services/CouponService.php b/Xboard/app/Services/CouponService.php
deleted file mode 100644
index e4bdae7..0000000
--- a/Xboard/app/Services/CouponService.php
+++ /dev/null
@@ -1,122 +0,0 @@
-coupon = Coupon::where('code', $code)
- ->lockForUpdate()
- ->first();
- }
-
- public function use(Order $order): bool
- {
- $this->setPlanId($order->plan_id);
- $this->setUserId($order->user_id);
- $this->setPeriod($order->period);
- $this->check();
- switch ($this->coupon->type) {
- case 1:
- $order->discount_amount = $this->coupon->value;
- break;
- case 2:
- $order->discount_amount = $order->total_amount * ($this->coupon->value / 100);
- break;
- }
- if ($order->discount_amount > $order->total_amount) {
- $order->discount_amount = $order->total_amount;
- }
- if ($this->coupon->limit_use !== NULL) {
- if ($this->coupon->limit_use <= 0)
- return false;
- $this->coupon->limit_use = $this->coupon->limit_use - 1;
- if (!$this->coupon->save()) {
- return false;
- }
- }
- return true;
- }
-
- public function getId()
- {
- return $this->coupon->id;
- }
-
- public function getCoupon()
- {
- return $this->coupon;
- }
-
- public function setPlanId($planId)
- {
- $this->planId = $planId;
- }
-
- public function setUserId($userId)
- {
- $this->userId = $userId;
- }
-
- public function setPeriod($period)
- {
- if ($period) {
- $this->period = PlanService::getPeriodKey($period);
- }
- }
-
- public function checkLimitUseWithUser(): bool
- {
- $usedCount = Order::where('coupon_id', $this->coupon->id)
- ->where('user_id', $this->userId)
- ->whereNotIn('status', [0, 2])
- ->count();
- if ($usedCount >= $this->coupon->limit_use_with_user)
- return false;
- return true;
- }
-
- public function check()
- {
- if (!$this->coupon || !$this->coupon->show) {
- throw new ApiException(__('Invalid coupon'));
- }
- if ($this->coupon->limit_use <= 0 && $this->coupon->limit_use !== NULL) {
- throw new ApiException(__('This coupon is no longer available'));
- }
- if (time() < $this->coupon->started_at) {
- throw new ApiException(__('This coupon has not yet started'));
- }
- if (time() > $this->coupon->ended_at) {
- throw new ApiException(__('This coupon has expired'));
- }
- if ($this->coupon->limit_plan_ids && $this->planId) {
- if (!in_array($this->planId, $this->coupon->limit_plan_ids)) {
- throw new ApiException(__('The coupon code cannot be used for this subscription'));
- }
- }
- if ($this->coupon->limit_period && $this->period) {
- if (!in_array($this->period, $this->coupon->limit_period)) {
- throw new ApiException(__('The coupon code cannot be used for this period'));
- }
- }
- if ($this->coupon->limit_use_with_user !== NULL && $this->userId) {
- if (!$this->checkLimitUseWithUser()) {
- throw new ApiException(__('The coupon can only be used :limit_use_with_user per person', [
- 'limit_use_with_user' => $this->coupon->limit_use_with_user
- ]));
- }
- }
- }
-}
diff --git a/Xboard/app/Services/DeviceStateService.php b/Xboard/app/Services/DeviceStateService.php
deleted file mode 100644
index 09e106c..0000000
--- a/Xboard/app/Services/DeviceStateService.php
+++ /dev/null
@@ -1,187 +0,0 @@
-removeNodeDevices($nodeId, $userId);
-
- if (!empty($ips)) {
- $fields = [];
- foreach ($ips as $ip) {
- $fields["{$nodeId}:{$ip}"] = $timestamp;
- }
- Redis::hMset($key, $fields);
- Redis::expire($key, self::TTL);
- }
-
- $this->notifyUpdate($userId);
- }
-
- /**
- * 获取某节点的所有设备数据
- * 返回: {userId: [ip1, ip2, ...], ...}
- */
- public function getNodeDevices(int $nodeId): array
- {
- $keys = Redis::keys(self::PREFIX . '*');
- $prefix = "{$nodeId}:";
- $result = [];
- foreach ($keys as $key) {
- $actualKey = $this->removeRedisPrefix($key);
- $uid = (int) substr($actualKey, strlen(self::PREFIX));
- $data = Redis::hgetall($actualKey);
- foreach ($data as $field => $timestamp) {
- if (str_starts_with($field, $prefix)) {
- $ip = substr($field, strlen($prefix));
- $result[$uid][] = $ip;
- }
- }
- }
-
- return $result;
- }
-
- /**
- * 删除某节点某用户的设备
- */
- public function removeNodeDevices(int $nodeId, int $userId): void
- {
- $key = self::PREFIX . $userId;
- $prefix = "{$nodeId}:";
-
- foreach (Redis::hkeys($key) as $field) {
- if (str_starts_with($field, $prefix)) {
- Redis::hdel($key, $field);
- }
- }
- }
-
- /**
- * 清除节点所有设备数据(用于节点断开连接)
- */
- public function clearAllNodeDevices(int $nodeId): array
- {
- $oldDevices = $this->getNodeDevices($nodeId);
- $prefix = "{$nodeId}:";
-
- foreach ($oldDevices as $userId => $ips) {
- $key = self::PREFIX . $userId;
- foreach (Redis::hkeys($key) as $field) {
- if (str_starts_with($field, $prefix)) {
- Redis::hdel($key, $field);
- }
- }
- }
-
- return array_keys($oldDevices);
- }
-
- /**
- * get user device count (deduplicated by IP, filter expired data)
- */
- public function getDeviceCount(int $userId): int
- {
- $data = Redis::hgetall(self::PREFIX . $userId);
- $now = time();
- $ips = [];
-
- foreach ($data as $field => $timestamp) {
- if ($now - $timestamp <= self::TTL) {
- $ips[] = substr($field, strpos($field, ':') + 1);
- }
- }
-
- return count(array_unique($ips));
- }
-
- /**
- * get user device count (for alivelist interface)
- */
- public function getAliveList(Collection $users): array
- {
- if ($users->isEmpty()) {
- return [];
- }
-
- $result = [];
- foreach ($users as $user) {
- $count = $this->getDeviceCount($user->id);
- if ($count > 0) {
- $result[$user->id] = $count;
- }
- }
-
- return $result;
- }
-
- /**
- * get devices of multiple users (for sync.devices, filter expired data)
- */
- public function getUsersDevices(array $userIds): array
- {
- $result = [];
- $now = time();
- foreach ($userIds as $userId) {
- $data = Redis::hgetall(self::PREFIX . $userId);
- if (!empty($data)) {
- $ips = [];
- foreach ($data as $field => $timestamp) {
- if ($now - $timestamp <= self::TTL) {
- $ips[] = substr($field, strpos($field, ':') + 1);
- }
- }
- if (!empty($ips)) {
- $result[$userId] = array_unique($ips);
- }
- }
- }
-
- return $result;
- }
-
- /**
- * notify update (throttle control)
- */
- public function notifyUpdate(int $userId): void
- {
- $dbThrottleKey = "device:db_throttle:{$userId}";
-
- // if (Redis::setnx($dbThrottleKey, 1)) {
- // Redis::expire($dbThrottleKey, self::DB_THROTTLE);
-
- User::query()
- ->whereKey($userId)
- ->update([
- 'online_count' => $this->getDeviceCount($userId),
- 'last_online_at' => now(),
- ]);
- // }
- }
-}
diff --git a/Xboard/app/Services/GiftCardService.php b/Xboard/app/Services/GiftCardService.php
deleted file mode 100644
index 8f14cbe..0000000
--- a/Xboard/app/Services/GiftCardService.php
+++ /dev/null
@@ -1,334 +0,0 @@
-code = GiftCardCode::where('code', $code)->first()
- ?? throw new ApiException('兑换码不存在');
-
- $this->template = $this->code->template;
- }
-
- /**
- * 设置使用用户
- */
- public function setUser(User $user): self
- {
- $this->user = $user;
- return $this;
- }
-
- /**
- * 验证兑换码
- */
- public function validate(): self
- {
- $this->validateIsActive();
-
- $eligibility = $this->checkUserEligibility();
- if (!$eligibility['can_redeem']) {
- throw new ApiException($eligibility['reason']);
- }
-
- return $this;
- }
-
- /**
- * 验证礼品卡本身是否可用 (不检查用户条件)
- * @throws ApiException
- */
- public function validateIsActive(): self
- {
- if (!$this->template->isAvailable()) {
- throw new ApiException('该礼品卡类型已停用');
- }
-
- if (!$this->code->isAvailable()) {
- throw new ApiException('兑换码不可用:' . $this->code->status_name);
- }
- return $this;
- }
-
- /**
- * 检查用户是否满足兑换条件 (不抛出异常)
- */
- public function checkUserEligibility(): array
- {
- if (!$this->user) {
- return [
- 'can_redeem' => false,
- 'reason' => '用户信息未提供'
- ];
- }
-
- if (!$this->template->checkUserConditions($this->user)) {
- return [
- 'can_redeem' => false,
- 'reason' => '您不满足此礼品卡的使用条件'
- ];
- }
-
- if (!$this->template->checkUsageLimit($this->user)) {
- return [
- 'can_redeem' => false,
- 'reason' => '您已达到此礼品卡的使用限制'
- ];
- }
-
- return ['can_redeem' => true, 'reason' => null];
- }
-
- /**
- * 使用礼品卡
- */
- public function redeem(array $options = []): array
- {
- if (!$this->user) {
- throw new ApiException('未设置使用用户');
- }
-
- return DB::transaction(function () use ($options) {
- $actualRewards = $this->template->calculateActualRewards($this->user);
-
- if ($this->template->type === GiftCardTemplate::TYPE_MYSTERY) {
- $this->code->setActualRewards($actualRewards);
- }
-
- $this->giveRewards($actualRewards);
-
- $inviteRewards = null;
- if ($this->user->invite_user_id && isset($actualRewards['invite_reward_rate'])) {
- $inviteRewards = $this->giveInviteRewards($actualRewards);
- }
-
- $this->code->markAsUsed($this->user);
-
- GiftCardUsage::createRecord(
- $this->code,
- $this->user,
- $actualRewards,
- array_merge($options, [
- 'invite_rewards' => $inviteRewards,
- 'multiplier' => $this->calculateMultiplier(),
- ])
- );
-
- return [
- 'rewards' => $actualRewards,
- 'invite_rewards' => $inviteRewards,
- 'code' => $this->code->code,
- 'template_name' => $this->template->name,
- ];
- });
- }
-
- /**
- * 发放奖励
- */
- protected function giveRewards(array $rewards): void
- {
- $userService = app(UserService::class);
-
- if (isset($rewards['balance']) && $rewards['balance'] > 0) {
- if (!$userService->addBalance($this->user->id, $rewards['balance'])) {
- throw new ApiException('余额发放失败');
- }
- }
-
- if (isset($rewards['transfer_enable']) && $rewards['transfer_enable'] > 0) {
- $this->user->transfer_enable = ($this->user->transfer_enable ?? 0) + $rewards['transfer_enable'];
- }
-
- if (isset($rewards['device_limit']) && $rewards['device_limit'] > 0) {
- $this->user->device_limit = ($this->user->device_limit ?? 0) + $rewards['device_limit'];
- }
-
- if (isset($rewards['reset_package']) && $rewards['reset_package']) {
- if ($this->user->plan_id) {
- app(TrafficResetService::class)->performReset($this->user, TrafficResetLog::SOURCE_GIFT_CARD);
- }
- }
-
- if (isset($rewards['plan_id'])) {
- $plan = Plan::find($rewards['plan_id']);
- if ($plan) {
- $userService->assignPlan(
- $this->user,
- $plan,
- $rewards['plan_validity_days'] ?? 0
- );
- }
- } else {
- // 只有在不是套餐卡的情况下,才处理独立的有效期奖励
- if (isset($rewards['expire_days']) && $rewards['expire_days'] > 0) {
- $userService->extendSubscription($this->user, $rewards['expire_days']);
- }
- }
-
- // 保存用户更改
- if (!$this->user->save()) {
- throw new ApiException('用户信息更新失败');
- }
- }
-
- /**
- * 发放邀请人奖励
- */
- protected function giveInviteRewards(array $rewards): ?array
- {
- if (!$this->user->invite_user_id) {
- return null;
- }
-
- $inviteUser = User::find($this->user->invite_user_id);
- if (!$inviteUser) {
- return null;
- }
-
- $rate = $rewards['invite_reward_rate'] ?? 0.2;
- $inviteRewards = [];
-
- $userService = app(UserService::class);
-
- // 邀请人余额奖励
- if (isset($rewards['balance']) && $rewards['balance'] > 0) {
- $inviteBalance = intval($rewards['balance'] * $rate);
- if ($inviteBalance > 0) {
- $userService->addBalance($inviteUser->id, $inviteBalance);
- $inviteRewards['balance'] = $inviteBalance;
- }
- }
-
- // 邀请人流量奖励
- if (isset($rewards['transfer_enable']) && $rewards['transfer_enable'] > 0) {
- $inviteTransfer = intval($rewards['transfer_enable'] * $rate);
- if ($inviteTransfer > 0) {
- $inviteUser->transfer_enable = ($inviteUser->transfer_enable ?? 0) + $inviteTransfer;
- $inviteUser->save();
- $inviteRewards['transfer_enable'] = $inviteTransfer;
- }
- }
-
- return $inviteRewards;
- }
-
- /**
- * 计算倍率
- */
- protected function calculateMultiplier(): float
- {
- return $this->getFestivalBonus();
- }
-
- /**
- * 获取节日加成倍率
- */
- private function getFestivalBonus(): float
- {
- $festivalConfig = $this->template->special_config ?? [];
- $now = time();
-
- if (
- isset($festivalConfig['start_time'], $festivalConfig['end_time']) &&
- $now >= $festivalConfig['start_time'] &&
- $now <= $festivalConfig['end_time']
- ) {
- return $festivalConfig['festival_bonus'] ?? 1.0;
- }
-
- return 1.0;
- }
-
- /**
- * 获取兑换码信息(不包含敏感信息)
- */
- public function getCodeInfo(): array
- {
- $info = [
- 'code' => $this->code->code,
- 'template' => [
- 'name' => $this->template->name,
- 'description' => $this->template->description,
- 'type' => $this->template->type,
- 'type_name' => $this->template->type_name,
- 'icon' => $this->template->icon,
- 'background_image' => $this->template->background_image,
- 'theme_color' => $this->template->theme_color,
- ],
- 'status' => $this->code->status,
- 'status_name' => $this->code->status_name,
- 'expires_at' => $this->code->expires_at,
- 'usage_count' => $this->code->usage_count,
- 'max_usage' => $this->code->max_usage,
- ];
- if ($this->template->type === GiftCardTemplate::TYPE_PLAN) {
- $plan = Plan::find($this->code->template->rewards['plan_id']);
- if ($plan) {
- $info['plan_info'] = PlanResource::make($plan)->toArray(request());
- }
- }
- return $info;
- }
-
- /**
- * 预览奖励(不实际发放)
- */
- public function previewRewards(): array
- {
- if (!$this->user) {
- throw new ApiException('未设置使用用户');
- }
-
- return $this->template->calculateActualRewards($this->user);
- }
-
- /**
- * 获取兑换码
- */
- public function getCode(): GiftCardCode
- {
- return $this->code;
- }
-
- /**
- * 获取模板
- */
- public function getTemplate(): GiftCardTemplate
- {
- return $this->template;
- }
-
- /**
- * 记录日志
- */
- protected function logUsage(string $action, array $data = []): void
- {
- Log::info('礼品卡使用记录', [
- 'action' => $action,
- 'code' => $this->code->code,
- 'template_id' => $this->template->id,
- 'user_id' => $this->user?->id,
- 'data' => $data,
- 'ip' => request()->ip(),
- 'user_agent' => request()->userAgent(),
- ]);
- }
-}
diff --git a/Xboard/app/Services/MailService.php b/Xboard/app/Services/MailService.php
deleted file mode 100644
index 7630111..0000000
--- a/Xboard/app/Services/MailService.php
+++ /dev/null
@@ -1,295 +0,0 @@
-where('remind_expire', true)
- ->orWhere('remind_traffic', true);
- })
- ->where('banned', false)
- ->whereNotNull('email')
- ->count();
- }
-
- /**
- * 分块处理用户提醒邮件
- */
- public function processUsersInChunks(int $chunkSize, ?callable $progressCallback = null): array
- {
- $statistics = [
- 'processed_users' => 0,
- 'expire_emails' => 0,
- 'traffic_emails' => 0,
- 'errors' => 0,
- 'skipped' => 0,
- ];
-
- User::select('id', 'email', 'expired_at', 'transfer_enable', 'u', 'd', 'remind_expire', 'remind_traffic')
- ->where(function ($query) {
- $query->where('remind_expire', true)
- ->orWhere('remind_traffic', true);
- })
- ->where('banned', false)
- ->whereNotNull('email')
- ->chunk($chunkSize, function ($users) use (&$statistics, $progressCallback) {
- $this->processUserChunk($users, $statistics);
-
- if ($progressCallback) {
- $progressCallback();
- }
-
- // 定期清理内存
- if ($statistics['processed_users'] % 2500 === 0) {
- gc_collect_cycles();
- }
- });
-
- return $statistics;
- }
-
- /**
- * 处理用户块
- */
- private function processUserChunk($users, array &$statistics): void
- {
- foreach ($users as $user) {
- try {
- $statistics['processed_users']++;
- $emailsSent = 0;
-
- // 检查并发送过期提醒
- if ($user->remind_expire && $this->shouldSendExpireRemind($user)) {
- $this->remindExpire($user);
- $statistics['expire_emails']++;
- $emailsSent++;
- }
-
- // 检查并发送流量提醒
- if ($user->remind_traffic && $this->shouldSendTrafficRemind($user)) {
- $this->remindTraffic($user);
- $statistics['traffic_emails']++;
- $emailsSent++;
- }
-
- if ($emailsSent === 0) {
- $statistics['skipped']++;
- }
-
- } catch (\Exception $e) {
- $statistics['errors']++;
-
- Log::error('发送提醒邮件失败', [
- 'user_id' => $user->id,
- 'email' => $user->email,
- 'error' => $e->getMessage()
- ]);
- }
- }
- }
-
- /**
- * 检查是否应该发送过期提醒
- */
- private function shouldSendExpireRemind(User $user): bool
- {
- if ($user->expired_at === NULL) {
- return false;
- }
- $expiredAt = $user->expired_at;
- $now = time();
- if (($expiredAt - 86400) < $now && $expiredAt > $now) {
- return true;
- }
- return false;
- }
-
- /**
- * 检查是否应该发送流量提醒
- */
- private function shouldSendTrafficRemind(User $user): bool
- {
- if ($user->transfer_enable <= 0) {
- return false;
- }
-
- $usedBytes = $user->u + $user->d;
- $usageRatio = $usedBytes / $user->transfer_enable;
-
- // 流量使用超过80%时发送提醒
- return $usageRatio >= 0.8;
- }
-
- public function remindTraffic(User $user)
- {
- if (!$user->remind_traffic)
- return;
- if (!$this->remindTrafficIsWarnValue($user->u, $user->d, $user->transfer_enable))
- return;
- $flag = CacheKey::get('LAST_SEND_EMAIL_REMIND_TRAFFIC', $user->id);
- if (Cache::get($flag))
- return;
- if (!Cache::put($flag, 1, 24 * 3600))
- return;
-
- SendEmailJob::dispatch([
- 'email' => $user->email,
- 'subject' => __('The traffic usage in :app_name has reached 80%', [
- 'app_name' => admin_setting('app_name', 'XBoard')
- ]),
- 'template_name' => 'remindTraffic',
- 'template_value' => [
- 'name' => admin_setting('app_name', 'XBoard'),
- 'url' => admin_setting('app_url')
- ]
- ]);
- }
-
- public function remindExpire(User $user)
- {
- if (!$this->shouldSendExpireRemind($user)) {
- return;
- }
-
- SendEmailJob::dispatch([
- 'email' => $user->email,
- 'subject' => __('The service in :app_name is about to expire', [
- 'app_name' => admin_setting('app_name', 'XBoard')
- ]),
- 'template_name' => 'remindExpire',
- 'template_value' => [
- 'name' => admin_setting('app_name', 'XBoard'),
- 'url' => admin_setting('app_url')
- ]
- ]);
- }
-
- private function remindTrafficIsWarnValue($u, $d, $transfer_enable)
- {
- $ud = $u + $d;
- if (!$ud)
- return false;
- if (!$transfer_enable)
- return false;
- $percentage = ($ud / $transfer_enable) * 100;
- if ($percentage < 80)
- return false;
- if ($percentage >= 100)
- return false;
- return true;
- }
-
- /**
- * 发送邮件
- *
- * @param array $params 包含邮件参数的数组,必须包含以下字段:
- * - email: 收件人邮箱地址
- * - subject: 邮件主题
- * - template_name: 邮件模板名称,例如 "welcome" 或 "password_reset"
- * - template_value: 邮件模板变量,一个关联数组,包含模板中需要替换的变量和对应的值
- * @return array 包含邮件发送结果的数组,包含以下字段:
- * - email: 收件人邮箱地址
- * - subject: 邮件主题
- * - template_name: 邮件模板名称
- * - error: 如果邮件发送失败,包含错误信息;否则为 null
- * @throws \InvalidArgumentException 如果 $params 参数缺少必要的字段,抛出此异常
- */
- public static function sendEmail(array $params)
- {
- if (admin_setting('email_host')) {
- Config::set('mail.host', admin_setting('email_host', config('mail.host')));
- Config::set('mail.port', admin_setting('email_port', config('mail.port')));
- Config::set('mail.encryption', admin_setting('email_encryption', config('mail.encryption')));
- Config::set('mail.username', admin_setting('email_username', config('mail.username')));
- Config::set('mail.password', admin_setting('email_password', config('mail.password')));
- Config::set('mail.from.address', admin_setting('email_from_address', config('mail.from.address')));
- Config::set('mail.from.name', admin_setting('app_name', 'XBoard'));
- }
- $email = $params['email'];
- $subject = $params['subject'];
-
- $templateValue = $params['template_value'] ?? [];
- $vars = is_array($templateValue) ? ($templateValue['vars'] ?? []) : [];
- $contentMode = is_array($templateValue) ? ($templateValue['content_mode'] ?? null) : null;
-
- if (is_array($vars) && !empty($vars)) {
- $subject = self::renderPlaceholders((string) $subject, $vars);
-
- if (is_array($templateValue) && isset($templateValue['content']) && is_string($templateValue['content'])) {
- $templateValue['content'] = self::renderPlaceholders($templateValue['content'], $vars);
- }
- }
-
- // Mass mail default: treat admin content as plain text and escape.
- if ($contentMode === 'text' && is_array($templateValue) && isset($templateValue['content']) && is_string($templateValue['content'])) {
- $templateValue['content'] = e($templateValue['content']);
- }
-
- $params['template_value'] = $templateValue;
- $params['template_name'] = 'mail.' . admin_setting('email_template', 'default') . '.' . $params['template_name'];
- try {
- Mail::send(
- $params['template_name'],
- $params['template_value'],
- function ($message) use ($email, $subject) {
- $message->to($email)->subject($subject);
- }
- );
- $error = null;
- } catch (\Exception $e) {
- Log::error($e);
- $error = $e->getMessage();
- }
- $log = [
- 'email' => $params['email'],
- 'subject' => $params['subject'],
- 'template_name' => $params['template_name'],
- 'error' => $error,
- 'config' => config('mail')
- ];
- MailLog::create($log);
- return $log;
- }
-}
diff --git a/Xboard/app/Services/NodeRegistry.php b/Xboard/app/Services/NodeRegistry.php
deleted file mode 100644
index c0f7d13..0000000
--- a/Xboard/app/Services/NodeRegistry.php
+++ /dev/null
@@ -1,77 +0,0 @@
- nodeId → connection */
- private static array $connections = [];
-
- public static function add(int $nodeId, TcpConnection $conn): void
- {
- // Close existing connection for this node (if reconnecting)
- if (isset(self::$connections[$nodeId])) {
- self::$connections[$nodeId]->close();
- }
- self::$connections[$nodeId] = $conn;
- }
-
- public static function remove(int $nodeId): void
- {
- unset(self::$connections[$nodeId]);
- }
-
- public static function get(int $nodeId): ?TcpConnection
- {
- return self::$connections[$nodeId] ?? null;
- }
-
- /**
- * Send a JSON message to a specific node.
- */
- public static function send(int $nodeId, string $event, array $data): bool
- {
- $conn = self::get($nodeId);
- if (!$conn) {
- return false;
- }
-
- $payload = json_encode([
- 'event' => $event,
- 'data' => $data,
- 'timestamp' => time(),
- ]);
-
- $conn->send($payload);
- return true;
- }
-
- /**
- * Get the connection for a node by ID, checking if it's still alive.
- */
- public static function isOnline(int $nodeId): bool
- {
- $conn = self::get($nodeId);
- return $conn !== null && $conn->getStatus() === TcpConnection::STATUS_ESTABLISHED;
- }
-
- /**
- * Get all connected node IDs.
- * @return int[]
- */
- public static function getConnectedNodeIds(): array
- {
- return array_keys(self::$connections);
- }
-
- public static function count(): int
- {
- return count(self::$connections);
- }
-}
diff --git a/Xboard/app/Services/NodeSyncService.php b/Xboard/app/Services/NodeSyncService.php
deleted file mode 100644
index ebab769..0000000
--- a/Xboard/app/Services/NodeSyncService.php
+++ /dev/null
@@ -1,143 +0,0 @@
- ServerService::buildNodeConfig($node)]);
- }
-
- /**
- * Push all users to all nodes in the group
- */
- public static function notifyUsersUpdatedByGroup(int $groupId): void
- {
- $servers = Server::whereJsonContains('group_ids', (string) $groupId)
- ->get();
-
- foreach ($servers as $server) {
- if (!self::isNodeOnline($server->id))
- continue;
-
- $users = ServerService::getAvailableUsers($server)->toArray();
- self::push($server->id, 'sync.users', ['users' => $users]);
- }
- }
-
- /**
- * Push user changes (add/remove) to affected nodes
- */
- public static function notifyUserChanged(User $user): void
- {
- if (!$user->group_id)
- return;
-
- $servers = Server::whereJsonContains('group_ids', (string) $user->group_id)->get();
- foreach ($servers as $server) {
- if (!self::isNodeOnline($server->id))
- continue;
-
- if ($user->isAvailable()) {
- self::push($server->id, 'sync.user.delta', [
- 'action' => 'add',
- 'users' => [
- [
- 'id' => $user->id,
- 'uuid' => $user->uuid,
- 'speed_limit' => $user->speed_limit,
- 'device_limit' => $user->device_limit,
- ]
- ],
- ]);
- } else {
- self::push($server->id, 'sync.user.delta', [
- 'action' => 'remove',
- 'users' => [['id' => $user->id]],
- ]);
- }
- }
- }
-
- /**
- * Push user removal from a specific group's nodes
- */
- public static function notifyUserRemovedFromGroup(int $userId, int $groupId): void
- {
- $servers = Server::whereJsonContains('group_ids', (string) $groupId)
- ->get();
-
- foreach ($servers as $server) {
- if (!self::isNodeOnline($server->id))
- continue;
-
- self::push($server->id, 'sync.user.delta', [
- 'action' => 'remove',
- 'users' => [['id' => $userId]],
- ]);
- }
- }
-
- /**
- * Full sync: push config + users to a node
- */
- public static function notifyFullSync(int $nodeId): void
- {
- if (!self::isNodeOnline($nodeId))
- return;
-
- $node = Server::find($nodeId);
- if (!$node)
- return;
-
- self::push($nodeId, 'sync.config', ['config' => ServerService::buildNodeConfig($node)]);
-
- $users = ServerService::getAvailableUsers($node)->toArray();
- self::push($nodeId, 'sync.users', ['users' => $users]);
- }
-
- /**
- * Publish a push command to Redis — picked up by the Workerman WS server
- */
- public static function push(int $nodeId, string $event, array $data): void
- {
- try {
- Redis::publish('node:push', json_encode([
- 'node_id' => $nodeId,
- 'event' => $event,
- 'data' => $data,
- ]));
- } catch (\Throwable $e) {
- Log::warning("[NodePush] Redis publish failed: {$e->getMessage()}", [
- 'node_id' => $nodeId,
- 'event' => $event,
- ]);
- }
- }
-}
diff --git a/Xboard/app/Services/OrderService.php b/Xboard/app/Services/OrderService.php
deleted file mode 100644
index 900be59..0000000
--- a/Xboard/app/Services/OrderService.php
+++ /dev/null
@@ -1,429 +0,0 @@
- 1,
- Plan::PERIOD_QUARTERLY => 3,
- Plan::PERIOD_HALF_YEARLY => 6,
- Plan::PERIOD_YEARLY => 12,
- Plan::PERIOD_TWO_YEARLY => 24,
- Plan::PERIOD_THREE_YEARLY => 36
- ];
- public $order;
- public $user;
-
- public function __construct(Order $order)
- {
- $this->order = $order;
- }
-
- /**
- * Create an order from a request.
- *
- * @param User $user
- * @param Plan $plan
- * @param string $period
- * @param string|null $couponCode
- * @return Order
- * @throws ApiException
- */
- public static function createFromRequest(
- User $user,
- Plan $plan,
- string $period,
- ?string $couponCode = null,
- ): Order {
- $userService = app(UserService::class);
- $planService = new PlanService($plan);
-
- $planService->validatePurchase($user, $period);
- HookManager::call('order.create.before', [$user, $plan, $period, $couponCode]);
-
- return DB::transaction(function () use ($user, $plan, $period, $couponCode, $userService) {
- $newPeriod = PlanService::getPeriodKey($period);
-
- $order = new Order([
- 'user_id' => $user->id,
- 'plan_id' => $plan->id,
- 'period' => $newPeriod,
- 'trade_no' => Helper::generateOrderNo(),
- 'total_amount' => (int) (optional($plan->prices)[$newPeriod] * 100),
- ]);
-
- $orderService = new self($order);
-
- if ($couponCode) {
- $orderService->applyCoupon($couponCode);
- }
-
- $orderService->setVipDiscount($user);
- $orderService->setOrderType($user);
- $orderService->setInvite(user: $user);
-
- if ($user->balance && $order->total_amount > 0) {
- $orderService->handleUserBalance($user, $userService);
- }
-
- if (!$order->save()) {
- throw new ApiException(__('Failed to create order'));
- }
-
- HookManager::call('order.create.after', $order);
- // 兼容旧钩子
- HookManager::call('order.after_create', $order);
-
- return $order;
- });
- }
-
- public function open(): void
- {
- $order = $this->order;
- $plan = Plan::find($order->plan_id);
-
- HookManager::call('order.open.before', $order);
-
-
- DB::transaction(function () use ($order, $plan) {
- $this->user = User::lockForUpdate()->find($order->user_id);
-
- if ($order->refund_amount) {
- $this->user->balance += $order->refund_amount;
- }
-
- if ($order->surplus_order_ids) {
- Order::whereIn('id', $order->surplus_order_ids)
- ->update(['status' => Order::STATUS_DISCOUNTED]);
- }
-
- match ((string) $order->period) {
- Plan::PERIOD_ONETIME => $this->buyByOneTime($plan),
- Plan::PERIOD_RESET_TRAFFIC => app(TrafficResetService::class)->performReset($this->user, TrafficResetLog::SOURCE_ORDER),
- default => $this->buyByPeriod($order, $plan),
- };
-
- $this->setSpeedLimit($plan->speed_limit);
- $this->setDeviceLimit($plan->device_limit);
-
- if (!$this->user->save()) {
- throw new \RuntimeException('用户信息保存失败');
- }
-
- $order->status = Order::STATUS_COMPLETED;
- if (!$order->save()) {
- throw new \RuntimeException('订单信息保存失败');
- }
- });
-
- $eventId = match ((int) $order->type) {
- Order::STATUS_PROCESSING => admin_setting('new_order_event_id', 0),
- Order::TYPE_RENEWAL => admin_setting('renew_order_event_id', 0),
- Order::TYPE_UPGRADE => admin_setting('change_order_event_id', 0),
- default => 0,
- };
-
- if ($eventId) {
- $this->openEvent($eventId);
- }
-
- HookManager::call('order.open.after', $order);
- }
-
-
- public function setOrderType(User $user)
- {
- $order = $this->order;
- if ($order->period === Plan::PERIOD_RESET_TRAFFIC) {
- $order->type = Order::TYPE_RESET_TRAFFIC;
- } else if ($user->plan_id !== NULL && $order->plan_id !== $user->plan_id && ($user->expired_at > time() || $user->expired_at === NULL)) {
- if (!(int) admin_setting('plan_change_enable', 1))
- throw new ApiException('目前不允许更改订阅,请联系客服或提交工单操作');
- $order->type = Order::TYPE_UPGRADE;
- if ((int) admin_setting('surplus_enable', 1))
- $this->getSurplusValue($user, $order);
- if ($order->surplus_amount >= $order->total_amount) {
- $order->refund_amount = (int) ($order->surplus_amount - $order->total_amount);
- $order->total_amount = 0;
- } else {
- $order->total_amount = (int) ($order->total_amount - $order->surplus_amount);
- }
- } else if (($user->expired_at === null || $user->expired_at > time()) && $order->plan_id == $user->plan_id) { // 用户订阅未过期或按流量订阅 且购买订阅与当前订阅相同 === 续费
- $order->type = Order::TYPE_RENEWAL;
- } else { // 新购
- $order->type = Order::TYPE_NEW_PURCHASE;
- }
- }
-
- public function setVipDiscount(User $user)
- {
- $order = $this->order;
- if ($user->discount) {
- $order->discount_amount = $order->discount_amount + ($order->total_amount * ($user->discount / 100));
- }
- $order->total_amount = $order->total_amount - $order->discount_amount;
- }
-
- public function setInvite(User $user): void
- {
- $order = $this->order;
- if ($user->invite_user_id && ($order->total_amount <= 0))
- return;
- $order->invite_user_id = $user->invite_user_id;
- $inviter = User::find($user->invite_user_id);
- if (!$inviter)
- return;
- $commissionType = (int) $inviter->commission_type;
- if ($commissionType === User::COMMISSION_TYPE_SYSTEM) {
- $commissionType = (bool) admin_setting('commission_first_time_enable', true) ? User::COMMISSION_TYPE_ONETIME : User::COMMISSION_TYPE_PERIOD;
- }
- $isCommission = false;
- switch ($commissionType) {
- case User::COMMISSION_TYPE_PERIOD:
- $isCommission = true;
- break;
- case User::COMMISSION_TYPE_ONETIME:
- $isCommission = !$this->haveValidOrder($user);
- break;
- }
-
- if (!$isCommission)
- return;
- if ($inviter->commission_rate) {
- $order->commission_balance = $order->total_amount * ($inviter->commission_rate / 100);
- } else {
- $order->commission_balance = $order->total_amount * (admin_setting('invite_commission', 10) / 100);
- }
- }
-
- private function haveValidOrder(User $user): Order|null
- {
- return Order::where('user_id', $user->id)
- ->whereNotIn('status', [Order::STATUS_PENDING, Order::STATUS_CANCELLED])
- ->first();
- }
-
- private function getSurplusValue(User $user, Order $order)
- {
- if ($user->expired_at === NULL) {
- $lastOneTimeOrder = Order::where('user_id', $user->id)
- ->where('period', Plan::PERIOD_ONETIME)
- ->where('status', Order::STATUS_COMPLETED)
- ->orderBy('id', 'DESC')
- ->first();
- if (!$lastOneTimeOrder)
- return;
- $nowUserTraffic = Helper::transferToGB($user->transfer_enable);
- if (!$nowUserTraffic)
- return;
- $paidTotalAmount = ($lastOneTimeOrder->total_amount + $lastOneTimeOrder->balance_amount);
- if (!$paidTotalAmount)
- return;
- $trafficUnitPrice = $paidTotalAmount / $nowUserTraffic;
- $notUsedTraffic = $nowUserTraffic - Helper::transferToGB($user->u + $user->d);
- $result = $trafficUnitPrice * $notUsedTraffic;
- $order->surplus_amount = (int) ($result > 0 ? $result : 0);
- $order->surplus_order_ids = Order::where('user_id', $user->id)
- ->where('period', '!=', Plan::PERIOD_RESET_TRAFFIC)
- ->where('status', Order::STATUS_COMPLETED)
- ->pluck('id')
- ->all();
- } else {
- $orders = Order::query()
- ->where('user_id', $user->id)
- ->whereNotIn('period', [Plan::PERIOD_RESET_TRAFFIC, Plan::PERIOD_ONETIME])
- ->where('status', Order::STATUS_COMPLETED)
- ->get();
-
- if ($orders->isEmpty()) {
- $order->surplus_amount = 0;
- $order->surplus_order_ids = [];
- return;
- }
-
- $orderAmountSum = $orders->sum(fn($item) => $item->total_amount + $item->balance_amount + $item->surplus_amount - $item->refund_amount);
- $orderMonthSum = $orders->sum(fn($item) => self::STR_TO_TIME[PlanService::getPeriodKey($item->period)] ?? 0);
- $firstOrderAt = $orders->min('created_at');
- $expiredAt = Carbon::createFromTimestamp($firstOrderAt)->addMonths($orderMonthSum);
-
- $now = now();
- $totalSeconds = $expiredAt->timestamp - $firstOrderAt;
- $remainSeconds = max(0, $expiredAt->timestamp - $now->timestamp);
- $cycleRatio = $totalSeconds > 0 ? $remainSeconds / $totalSeconds : 0;
-
- $plan = Plan::find($user->plan_id);
- $totalTraffic = $plan?->transfer_enable * $orderMonthSum;
- $usedTraffic = Helper::transferToGB($user->u + $user->d);
- $remainTraffic = max(0, $totalTraffic - $usedTraffic);
- $trafficRatio = $totalTraffic > 0 ? $remainTraffic / $totalTraffic : 0;
-
- $ratio = $cycleRatio;
- if (admin_setting('change_order_event_id', 0) == 1) {
- $ratio = min($cycleRatio, $trafficRatio);
- }
-
-
- $order->surplus_amount = (int) max(0, $orderAmountSum * $ratio);
- $order->surplus_order_ids = $orders->pluck('id')->all();
- }
- }
-
- public function paid(string $callbackNo)
- {
- $order = $this->order;
- if ($order->status !== Order::STATUS_PENDING)
- return true;
- $order->status = Order::STATUS_PROCESSING;
- $order->paid_at = time();
- $order->callback_no = $callbackNo;
- if (!$order->save())
- return false;
- try {
- OrderHandleJob::dispatchSync($order->trade_no);
- } catch (\Exception $e) {
- Log::error($e);
- return false;
- }
- return true;
- }
-
- public function cancel(): bool
- {
- $order = $this->order;
- HookManager::call('order.cancel.before', $order);
- try {
- DB::beginTransaction();
- $order->status = Order::STATUS_CANCELLED;
- if (!$order->save()) {
- throw new \Exception('Failed to save order status.');
- }
- if ($order->balance_amount) {
- $userService = new UserService();
- if (!$userService->addBalance($order->user_id, $order->balance_amount)) {
- throw new \Exception('Failed to add balance.');
- }
- }
- DB::commit();
- HookManager::call('order.cancel.after', $order);
- return true;
- } catch (\Exception $e) {
- DB::rollBack();
- Log::error($e);
- return false;
- }
- }
-
- private function setSpeedLimit($speedLimit)
- {
- $this->user->speed_limit = $speedLimit;
- }
-
- private function setDeviceLimit($deviceLimit)
- {
- $this->user->device_limit = $deviceLimit;
- }
-
- private function buyByPeriod(Order $order, Plan $plan)
- {
- // change plan process
- if ((int) $order->type === Order::TYPE_UPGRADE) {
- $this->user->expired_at = time();
- }
- $this->user->transfer_enable = $plan->transfer_enable * 1073741824;
- // 从一次性转换到循环或者新购的时候,重置流量
- if ($this->user->expired_at === NULL || $order->type === Order::TYPE_NEW_PURCHASE)
- app(TrafficResetService::class)->performReset($this->user, TrafficResetLog::SOURCE_ORDER);
- $this->user->plan_id = $plan->id;
- $this->user->group_id = $plan->group_id;
- $this->user->expired_at = $this->getTime($order->period, $this->user->expired_at);
- }
-
- private function buyByOneTime(Plan $plan)
- {
- app(TrafficResetService::class)->performReset($this->user, TrafficResetLog::SOURCE_ORDER);
- $this->user->transfer_enable = $plan->transfer_enable * 1073741824;
- $this->user->plan_id = $plan->id;
- $this->user->group_id = $plan->group_id;
- $this->user->expired_at = NULL;
- }
-
- /**
- * 计算套餐到期时间
- * @param string $periodKey
- * @param int $timestamp
- * @return int
- * @throws ApiException
- */
- private function getTime(string $periodKey, ?int $timestamp = null): int
- {
- $timestamp = $timestamp < time() ? time() : $timestamp;
- $periodKey = PlanService::getPeriodKey($periodKey);
-
- if (isset(self::STR_TO_TIME[$periodKey])) {
- $months = self::STR_TO_TIME[$periodKey];
- return Carbon::createFromTimestamp($timestamp)->addMonths($months)->timestamp;
- }
-
- throw new ApiException('无效的套餐周期');
- }
-
- private function openEvent($eventId)
- {
- switch ((int) $eventId) {
- case 0:
- break;
- case 1:
- app(TrafficResetService::class)->performReset($this->user, TrafficResetLog::SOURCE_ORDER);
- break;
- }
- }
-
- protected function applyCoupon(string $couponCode): void
- {
- $couponService = new CouponService($couponCode);
- if (!$couponService->use($this->order)) {
- throw new ApiException(__('Coupon failed'));
- }
- $this->order->coupon_id = $couponService->getId();
- }
-
- /**
- * Summary of handleUserBalance
- * @param User $user
- * @param UserService $userService
- * @return void
- */
- protected function handleUserBalance(User $user, UserService $userService): void
- {
- $remainingBalance = $user->balance - $this->order->total_amount;
-
- if ($remainingBalance >= 0) {
- if (!$userService->addBalance($this->order->user_id, -$this->order->total_amount)) {
- throw new ApiException(__('Insufficient balance'));
- }
- $this->order->balance_amount = $this->order->total_amount;
- $this->order->total_amount = 0;
- } else {
- if (!$userService->addBalance($this->order->user_id, -$user->balance)) {
- throw new ApiException(__('Insufficient balance'));
- }
- $this->order->balance_amount = $user->balance;
- $this->order->total_amount = $this->order->total_amount - $user->balance;
- }
- }
-}
diff --git a/Xboard/app/Services/PaymentService.php b/Xboard/app/Services/PaymentService.php
deleted file mode 100644
index c496d38..0000000
--- a/Xboard/app/Services/PaymentService.php
+++ /dev/null
@@ -1,135 +0,0 @@
-method = $method;
- $this->pluginManager = app(PluginManager::class);
-
- if ($method === 'temp') {
- return;
- }
-
- if ($id) {
- $paymentModel = Payment::find($id);
- if (!$paymentModel) {
- throw new ApiException('payment not found');
- }
- $payment = $paymentModel->toArray();
- }
- if ($uuid) {
- $paymentModel = Payment::where('uuid', $uuid)->first();
- if (!$paymentModel) {
- throw new ApiException('payment not found');
- }
- $payment = $paymentModel->toArray();
- }
-
- $this->config = [];
- if (isset($payment)) {
- $this->config = is_string($payment['config']) ? json_decode($payment['config'], true) : $payment['config'];
- $this->config['enable'] = $payment['enable'];
- $this->config['id'] = $payment['id'];
- $this->config['uuid'] = $payment['uuid'];
- $this->config['notify_domain'] = $payment['notify_domain'] ?? '';
- }
-
- $paymentMethods = $this->getAvailablePaymentMethods();
- if (isset($paymentMethods[$this->method])) {
- $pluginCode = $paymentMethods[$this->method]['plugin_code'];
- $paymentPlugins = $this->pluginManager->getEnabledPaymentPlugins();
- foreach ($paymentPlugins as $plugin) {
- if ($plugin->getPluginCode() === $pluginCode) {
- $plugin->setConfig($this->config);
- $this->payment = $plugin;
- return;
- }
- }
- }
-
- $this->payment = new $this->class($this->config);
- }
-
- public function notify($params)
- {
- if (!$this->config['enable'])
- throw new ApiException('gate is not enable');
- return $this->payment->notify($params);
- }
-
- public function pay($order)
- {
- // custom notify domain name
- $notifyUrl = url("/api/v1/guest/payment/notify/{$this->method}/{$this->config['uuid']}");
- if ($this->config['notify_domain']) {
- $parseUrl = parse_url($notifyUrl);
- $notifyUrl = $this->config['notify_domain'] . $parseUrl['path'];
- }
-
- return $this->payment->pay([
- 'notify_url' => $notifyUrl,
- 'return_url' => source_base_url('/#/order/' . $order['trade_no']),
- 'trade_no' => $order['trade_no'],
- 'total_amount' => $order['total_amount'],
- 'user_id' => $order['user_id'],
- 'stripe_token' => $order['stripe_token']
- ]);
- }
-
- public function form()
- {
- $form = $this->payment->form();
- $result = [];
- foreach ($form as $key => $field) {
- $result[$key] = [
- 'type' => $field['type'],
- 'label' => $field['label'] ?? '',
- 'placeholder' => $field['placeholder'] ?? '',
- 'description' => $field['description'] ?? '',
- 'value' => $this->config[$key] ?? $field['default'] ?? '',
- 'options' => $field['select_options'] ?? $field['options'] ?? []
- ];
- }
- return $result;
- }
-
- /**
- * 获取所有可用的支付方式
- */
- public function getAvailablePaymentMethods(): array
- {
- $methods = [];
-
- $methods = HookManager::filter('available_payment_methods', $methods);
-
- return $methods;
- }
-
- /**
- * 获取所有支付方式名称列表(用于管理后台)
- */
- public static function getAllPaymentMethodNames(): array
- {
- $pluginManager = app(PluginManager::class);
- $pluginManager->initializeEnabledPlugins();
-
- $instance = new self('temp');
- $methods = $instance->getAvailablePaymentMethods();
-
- return array_keys($methods);
- }
-}
diff --git a/Xboard/app/Services/PlanService.php b/Xboard/app/Services/PlanService.php
deleted file mode 100644
index e67363d..0000000
--- a/Xboard/app/Services/PlanService.php
+++ /dev/null
@@ -1,194 +0,0 @@
-plan = $plan;
- }
-
- /**
- * 获取所有可销售的订阅计划列表
- * 条件:show 和 sell 为 true,且容量充足
- *
- * @return Collection
- */
- public function getAvailablePlans(): Collection
- {
- return Plan::where('show', true)
- ->where('sell', true)
- ->orderBy('sort')
- ->get()
- ->filter(function ($plan) {
- return $this->hasCapacity($plan);
- });
- }
-
- /**
- * 获取指定订阅计划的可用状态
- * 条件:renew 和 sell 为 true
- *
- * @param int $planId
- * @return Plan|null
- */
- public function getAvailablePlan(int $planId): ?Plan
- {
- return Plan::where('id', $planId)
- ->where('sell', true)
- ->where('renew', true)
- ->first();
- }
-
- /**
- * 检查指定计划是否可用于指定用户
- *
- * @param Plan $plan
- * @param User $user
- * @return bool
- */
- public function isPlanAvailableForUser(Plan $plan, User $user): bool
- {
- // 如果是续费
- if ($user->plan_id === $plan->id) {
- return $plan->renew;
- }
-
- // 如果是新购
- return $plan->show && $plan->sell && $this->hasCapacity($plan);
- }
-
- public function validatePurchase(User $user, string $period): void
- {
- if (!$this->plan) {
- throw new ApiException(__('Subscription plan does not exist'));
- }
-
- // 转换周期格式为新版格式
- $periodKey = self::getPeriodKey($period);
- $price = $this->plan->prices[$periodKey] ?? null;
-
- if ($price === null) {
- throw new ApiException(__('This payment period cannot be purchased, please choose another period'));
- }
-
- if ($periodKey === Plan::PERIOD_RESET_TRAFFIC) {
- $this->validateResetTrafficPurchase($user);
- return;
- }
-
- if ($user->plan_id !== $this->plan->id && !$this->hasCapacity($this->plan)) {
- throw new ApiException(__('Current product is sold out'));
- }
-
- $this->validatePlanAvailability($user);
- }
-
- /**
- * 智能转换周期格式为新版格式
- * 如果是新版格式直接返回,如果是旧版格式则转换为新版格式
- *
- * @param string $period
- * @return string
- */
- public static function getPeriodKey(string $period): string
- {
- // 如果是新版格式直接返回
- if (in_array($period, self::getNewPeriods())) {
- return $period;
- }
-
- // 如果是旧版格式则转换为新版格式
- return Plan::LEGACY_PERIOD_MAPPING[$period] ?? $period;
- }
- /**
- * 只能转换周期格式为旧版本
- */
- public static function convertToLegacyPeriod(string $period): string
- {
- $flippedMapping = array_flip(Plan::LEGACY_PERIOD_MAPPING);
- return $flippedMapping[$period] ?? $period;
- }
-
- /**
- * 获取所有支持的新版周期格式
- *
- * @return array
- */
- public static function getNewPeriods(): array
- {
- return array_values(Plan::LEGACY_PERIOD_MAPPING);
- }
-
- /**
- * 获取旧版周期格式
- *
- * @param string $period
- * @return string
- */
- public static function getLegacyPeriod(string $period): string
- {
- $flipped = array_flip(Plan::LEGACY_PERIOD_MAPPING);
- return $flipped[$period] ?? $period;
- }
-
- protected function validateResetTrafficPurchase(User $user): void
- {
- if (!app(UserService::class)->isAvailable($user) || $this->plan->id !== $user->plan_id) {
- throw new ApiException(__('Subscription has expired or no active subscription, unable to purchase Data Reset Package'));
- }
- }
-
- protected function validatePlanAvailability(User $user): void
- {
- if ((!$this->plan->show && !$this->plan->renew) || (!$this->plan->show && $user->plan_id !== $this->plan->id)) {
- throw new ApiException(__('This subscription has been sold out, please choose another subscription'));
- }
-
- if (!$this->plan->renew && $user->plan_id == $this->plan->id) {
- throw new ApiException(__('This subscription cannot be renewed, please change to another subscription'));
- }
-
- if (!$this->plan->show && $this->plan->renew && !app(UserService::class)->isAvailable($user)) {
- throw new ApiException(__('This subscription has expired, please change to another subscription'));
- }
- }
-
- public function hasCapacity(Plan $plan): bool
- {
- if ($plan->capacity_limit === null) {
- return true;
- }
-
- $activeUserCount = User::where('plan_id', $plan->id)
- ->where(function ($query) {
- $query->where('expired_at', '>=', time())
- ->orWhereNull('expired_at');
- })
- ->count();
-
- return ($plan->capacity_limit - $activeUserCount) > 0;
- }
-
- public function getAvailablePeriods(Plan $plan): array
- {
- return array_filter(
- $plan->getActivePeriods(),
- fn($period) => isset($plan->prices[$period]) && $plan->prices[$period] > 0
- );
- }
-
- public function canResetTraffic(Plan $plan): bool
- {
- return $plan->reset_traffic_method !== Plan::RESET_TRAFFIC_NEVER
- && $plan->getResetTrafficPrice() > 0;
- }
-}
diff --git a/Xboard/app/Services/Plugin/AbstractPlugin.php b/Xboard/app/Services/Plugin/AbstractPlugin.php
deleted file mode 100644
index 23da9a3..0000000
--- a/Xboard/app/Services/Plugin/AbstractPlugin.php
+++ /dev/null
@@ -1,222 +0,0 @@
-pluginCode = $pluginCode;
- $this->namespace = 'Plugin\\' . Str::studly($pluginCode);
- $reflection = new \ReflectionClass($this);
- $this->basePath = dirname($reflection->getFileName());
- }
-
- /**
- * 获取插件代码
- */
- public function getPluginCode(): string
- {
- return $this->pluginCode;
- }
-
- /**
- * 获取插件命名空间
- */
- public function getNamespace(): string
- {
- return $this->namespace;
- }
-
- /**
- * 获取插件基础路径
- */
- public function getBasePath(): string
- {
- return $this->basePath;
- }
-
- /**
- * 设置配置
- */
- public function setConfig(array $config): void
- {
- $this->config = $config;
- }
-
- /**
- * 获取配置
- */
- public function getConfig(?string $key = null, $default = null): mixed
- {
- $config = $this->config;
- if ($key) {
- $config = $config[$key] ?? $default;
- }
- return $config;
- }
-
- /**
- * 获取视图
- */
- protected function view(string $view, array $data = [], array $mergeData = []): \Illuminate\Contracts\View\View
- {
- return view(Str::studly($this->pluginCode) . '::' . $view, $data, $mergeData);
- }
-
- /**
- * 注册动作钩子监听器
- */
- protected function listen(string $hook, callable $callback, int $priority = 20): void
- {
- HookManager::register($hook, $callback, $priority);
- }
-
- /**
- * 注册过滤器钩子
- */
- protected function filter(string $hook, callable $callback, int $priority = 20): void
- {
- HookManager::registerFilter($hook, $callback, $priority);
- }
-
- /**
- * 移除事件监听器
- */
- protected function removeListener(string $hook): void
- {
- HookManager::remove($hook);
- }
-
- /**
- * 注册 Artisan 命令
- */
- protected function registerCommand(string $commandClass): void
- {
- if (class_exists($commandClass)) {
- app('Illuminate\Contracts\Console\Kernel')->registerCommand(new $commandClass());
- }
- }
-
- /**
- * 注册插件命令目录
- */
- public function registerCommands(): void
- {
- $commandsPath = $this->basePath . '/Commands';
- if (File::exists($commandsPath)) {
- $files = File::glob($commandsPath . '/*.php');
- foreach ($files as $file) {
- $className = pathinfo($file, PATHINFO_FILENAME);
- $commandClass = $this->namespace . '\\Commands\\' . $className;
-
- if (class_exists($commandClass)) {
- $this->registerCommand($commandClass);
- }
- }
- }
- }
-
- /**
- * 中断当前请求并返回新的响应
- *
- * @param Response|string|array $response
- * @return never
- */
- protected function intercept(Response|string|array $response): never
- {
- HookManager::intercept($response);
- }
-
- /**
- * 插件启动时调用
- */
- public function boot(): void
- {
- // 插件启动时的初始化逻辑
- }
-
- /**
- * 插件安装时调用
- */
- public function install(): void
- {
- // 插件安装时的初始化逻辑
- }
-
- /**
- * 插件卸载时调用
- */
- public function cleanup(): void
- {
- // 插件卸载时的清理逻辑
- }
-
- /**
- * 插件更新时调用
- */
- public function update(string $oldVersion, string $newVersion): void
- {
- // 插件更新时的迁移逻辑
- }
-
- /**
- * 获取插件资源URL
- */
- protected function asset(string $path): string
- {
- return asset('plugins/' . $this->pluginCode . '/' . ltrim($path, '/'));
- }
-
- /**
- * 获取插件配置项
- */
- protected function getConfigValue(string $key, $default = null)
- {
- return $this->config[$key] ?? $default;
- }
-
- /**
- * 获取插件数据库迁移路径
- */
- protected function getMigrationsPath(): string
- {
- return $this->basePath . '/database/migrations';
- }
-
- /**
- * 获取插件视图路径
- */
- protected function getViewsPath(): string
- {
- return $this->basePath . '/resources/views';
- }
-
- /**
- * 获取插件资源路径
- */
- protected function getAssetsPath(): string
- {
- return $this->basePath . '/resources/assets';
- }
-
- /**
- * Register plugin scheduled tasks. Plugins can override this method.
- *
- * @param \Illuminate\Console\Scheduling\Schedule $schedule
- * @return void
- */
- public function schedule(\Illuminate\Console\Scheduling\Schedule $schedule): void
- {
- // Plugin can override this method to register scheduled tasks
- }
-}
\ No newline at end of file
diff --git a/Xboard/app/Services/Plugin/HookManager.php b/Xboard/app/Services/Plugin/HookManager.php
deleted file mode 100644
index 5b6e9af..0000000
--- a/Xboard/app/Services/Plugin/HookManager.php
+++ /dev/null
@@ -1,286 +0,0 @@
-json($response);
- }
-
- throw new InterceptResponseException($response);
- }
-
- /**
- * Trigger action hook
- *
- * @param string $hook Hook name
- * @param mixed $payload Data passed to hook
- * @return void
- */
- public static function call(string $hook, mixed $payload = null): void
- {
- $actions = self::getActions();
-
- if (!isset($actions[$hook])) {
- return;
- }
-
- ksort($actions[$hook]);
-
- foreach ($actions[$hook] as $callbacks) {
- foreach ($callbacks as $callback) {
- $callback($payload);
- }
- }
- }
-
- /**
- * Trigger filter hook
- *
- * @param string $hook Hook name
- * @param mixed $value Value to filter
- * @param mixed ...$args Other parameters
- * @return mixed
- */
- public static function filter(string $hook, mixed $value, mixed ...$args): mixed
- {
- $filters = self::getFilters();
-
- if (!isset($filters[$hook])) {
- return $value;
- }
-
- ksort($filters[$hook]);
-
- $result = $value;
- foreach ($filters[$hook] as $callbacks) {
- foreach ($callbacks as $callback) {
- $result = $callback($result, ...$args);
- }
- }
-
- return $result;
- }
-
- /**
- * Register action hook listener
- *
- * @param string $hook Hook name
- * @param callable $callback Callback function
- * @param int $priority Priority
- * @return void
- */
- public static function register(string $hook, callable $callback, int $priority = 20): void
- {
- $actions = self::getActions();
-
- if (!isset($actions[$hook])) {
- $actions[$hook] = [];
- }
-
- if (!isset($actions[$hook][$priority])) {
- $actions[$hook][$priority] = [];
- }
-
- $actions[$hook][$priority][self::getCallableId($callback)] = $callback;
-
- self::setActions($actions);
- }
-
- /**
- * Register filter hook
- *
- * @param string $hook Hook name
- * @param callable $callback Callback function
- * @param int $priority Priority
- * @return void
- */
- public static function registerFilter(string $hook, callable $callback, int $priority = 20): void
- {
- $filters = self::getFilters();
-
- if (!isset($filters[$hook])) {
- $filters[$hook] = [];
- }
-
- if (!isset($filters[$hook][$priority])) {
- $filters[$hook][$priority] = [];
- }
-
- $filters[$hook][$priority][self::getCallableId($callback)] = $callback;
-
- self::setFilters($filters);
- }
-
- /**
- * Remove hook listener
- *
- * @param string $hook Hook name
- * @param callable|null $callback Callback function
- * @return void
- */
- public static function remove(string $hook, ?callable $callback = null): void
- {
- $actions = self::getActions();
- $filters = self::getFilters();
-
- if ($callback === null) {
- if (isset($actions[$hook])) {
- unset($actions[$hook]);
- self::setActions($actions);
- }
-
- if (isset($filters[$hook])) {
- unset($filters[$hook]);
- self::setFilters($filters);
- }
-
- return;
- }
-
- $callbackId = self::getCallableId($callback);
-
- if (isset($actions[$hook])) {
- foreach ($actions[$hook] as $priority => $callbacks) {
- if (isset($callbacks[$callbackId])) {
- unset($actions[$hook][$priority][$callbackId]);
-
- if (empty($actions[$hook][$priority])) {
- unset($actions[$hook][$priority]);
- }
-
- if (empty($actions[$hook])) {
- unset($actions[$hook]);
- }
- }
- }
- self::setActions($actions);
- }
-
- if (isset($filters[$hook])) {
- foreach ($filters[$hook] as $priority => $callbacks) {
- if (isset($callbacks[$callbackId])) {
- unset($filters[$hook][$priority][$callbackId]);
-
- if (empty($filters[$hook][$priority])) {
- unset($filters[$hook][$priority]);
- }
-
- if (empty($filters[$hook])) {
- unset($filters[$hook]);
- }
- }
- }
- self::setFilters($filters);
- }
- }
-
- /**
- * Check if hook exists
- *
- * @param string $hook Hook name
- * @return bool
- */
- public static function hasHook(string $hook): bool
- {
- $actions = self::getActions();
- $filters = self::getFilters();
-
- return isset($actions[$hook]) || isset($filters[$hook]);
- }
-
- /**
- * Clear all hooks (called when Octane resets)
- */
- public static function reset(): void
- {
- App::instance('hook.actions', []);
- App::instance('hook.filters', []);
- }
-}
\ No newline at end of file
diff --git a/Xboard/app/Services/Plugin/InterceptResponseException.php b/Xboard/app/Services/Plugin/InterceptResponseException.php
deleted file mode 100644
index 298d8d3..0000000
--- a/Xboard/app/Services/Plugin/InterceptResponseException.php
+++ /dev/null
@@ -1,22 +0,0 @@
-response = $response;
- }
-
- public function getResponse(): Response
- {
- return $this->response;
- }
-}
\ No newline at end of file
diff --git a/Xboard/app/Services/Plugin/PluginConfigService.php b/Xboard/app/Services/Plugin/PluginConfigService.php
deleted file mode 100644
index 977614b..0000000
--- a/Xboard/app/Services/Plugin/PluginConfigService.php
+++ /dev/null
@@ -1,111 +0,0 @@
-pluginManager = app(PluginManager::class);
- }
-
- /**
- * 获取插件配置
- *
- * @param string $pluginCode
- * @return array
- */
- public function getConfig(string $pluginCode): array
- {
- $defaultConfig = $this->getDefaultConfig($pluginCode);
- if (empty($defaultConfig)) {
- return [];
- }
- $dbConfig = $this->getDbConfig($pluginCode);
-
- $result = [];
- foreach ($defaultConfig as $key => $item) {
- $result[$key] = [
- 'type' => $item['type'],
- 'label' => $item['label'] ?? '',
- 'placeholder' => $item['placeholder'] ?? '',
- 'description' => $item['description'] ?? '',
- 'value' => $dbConfig[$key] ?? $item['default'],
- 'options' => $item['options'] ?? []
- ];
- }
-
- return $result;
- }
-
- /**
- * 更新插件配置
- *
- * @param string $pluginCode
- * @param array $config
- * @return bool
- */
- public function updateConfig(string $pluginCode, array $config): bool
- {
- $defaultConfig = $this->getDefaultConfig($pluginCode);
- if (empty($defaultConfig)) {
- throw new \Exception('插件配置结构不存在');
- }
- $values = [];
- foreach ($config as $key => $value) {
- if (!isset($defaultConfig[$key])) {
- continue;
- }
- $values[$key] = $value;
- }
- Plugin::query()
- ->where('code', $pluginCode)
- ->update([
- 'config' => json_encode($values),
- 'updated_at' => now()
- ]);
-
- return true;
- }
-
- /**
- * 获取插件默认配置
- *
- * @param string $pluginCode
- * @return array
- */
- protected function getDefaultConfig(string $pluginCode): array
- {
- $configFile = $this->pluginManager->getPluginPath($pluginCode) . '/config.json';
- if (!File::exists($configFile)) {
- return [];
- }
-
- $config = json_decode(File::get($configFile), true);
- return $config['config'] ?? [];
- }
-
- /**
- * 获取数据库中的配置
- *
- * @param string $pluginCode
- * @return array
- */
- public function getDbConfig(string $pluginCode): array
- {
- $plugin = Plugin::query()
- ->where('code', $pluginCode)
- ->first();
-
- if (!$plugin || empty($plugin->config)) {
- return [];
- }
-
- return json_decode($plugin->config, true);
- }
-}
\ No newline at end of file
diff --git a/Xboard/app/Services/Plugin/PluginManager.php b/Xboard/app/Services/Plugin/PluginManager.php
deleted file mode 100644
index 7859da0..0000000
--- a/Xboard/app/Services/Plugin/PluginManager.php
+++ /dev/null
@@ -1,727 +0,0 @@
-pluginPath = base_path('plugins');
- }
-
- /**
- * 获取插件的命名空间
- */
- public function getPluginNamespace(string $pluginCode): string
- {
- return 'Plugin\\' . Str::studly($pluginCode);
- }
-
- /**
- * 获取插件的基础路径
- */
- public function getPluginPath(string $pluginCode): string
- {
- return $this->pluginPath . '/' . Str::studly($pluginCode);
- }
-
- /**
- * 加载插件类
- */
- protected function loadPlugin(string $pluginCode): ?AbstractPlugin
- {
- if (isset($this->loadedPlugins[$pluginCode])) {
- return $this->loadedPlugins[$pluginCode];
- }
-
- $pluginClass = $this->getPluginNamespace($pluginCode) . '\\Plugin';
-
- if (!class_exists($pluginClass)) {
- $pluginFile = $this->getPluginPath($pluginCode) . '/Plugin.php';
- if (!File::exists($pluginFile)) {
- Log::warning("Plugin class file not found: {$pluginFile}");
- Plugin::query()->where('code', $pluginCode)->delete();
- return null;
- }
- require_once $pluginFile;
- }
-
- if (!class_exists($pluginClass)) {
- Log::error("Plugin class not found: {$pluginClass}");
- return null;
- }
-
- $plugin = new $pluginClass($pluginCode);
- $this->loadedPlugins[$pluginCode] = $plugin;
-
- return $plugin;
- }
-
- /**
- * 注册插件的服务提供者
- */
- protected function registerServiceProvider(string $pluginCode): void
- {
- $providerClass = $this->getPluginNamespace($pluginCode) . '\\Providers\\PluginServiceProvider';
-
- if (class_exists($providerClass)) {
- app()->register($providerClass);
- }
- }
-
- /**
- * 加载插件的路由
- */
- protected function loadRoutes(string $pluginCode): void
- {
- $routesPath = $this->getPluginPath($pluginCode) . '/routes';
- if (File::exists($routesPath)) {
- $webRouteFile = $routesPath . '/web.php';
- $apiRouteFile = $routesPath . '/api.php';
- if (File::exists($webRouteFile)) {
- Route::middleware('web')
- ->namespace($this->getPluginNamespace($pluginCode) . '\\Controllers')
- ->group(function () use ($webRouteFile) {
- require $webRouteFile;
- });
- }
- if (File::exists($apiRouteFile)) {
- Route::middleware('api')
- ->namespace($this->getPluginNamespace($pluginCode) . '\\Controllers')
- ->group(function () use ($apiRouteFile) {
- require $apiRouteFile;
- });
- }
- }
- }
-
- /**
- * 加载插件的视图
- */
- protected function loadViews(string $pluginCode): void
- {
- $viewsPath = $this->getPluginPath($pluginCode) . '/resources/views';
- if (File::exists($viewsPath)) {
- View::addNamespace(Str::studly($pluginCode), $viewsPath);
- return;
- }
- }
-
- /**
- * 注册插件命令
- */
- protected function registerPluginCommands(string $pluginCode, AbstractPlugin $pluginInstance): void
- {
- try {
- // 调用插件的命令注册方法
- $pluginInstance->registerCommands();
- } catch (\Exception $e) {
- Log::error("Failed to register commands for plugin '{$pluginCode}': " . $e->getMessage());
- }
- }
-
- /**
- * 安装插件
- */
- public function install(string $pluginCode): bool
- {
- $configFile = $this->getPluginPath($pluginCode) . '/config.json';
-
- if (!File::exists($configFile)) {
- throw new \Exception('Plugin config file not found');
- }
-
- $config = json_decode(File::get($configFile), true);
- if (!$this->validateConfig($config)) {
- throw new \Exception('Invalid plugin config');
- }
-
- // 检查插件是否已安装
- if (Plugin::where('code', $pluginCode)->exists()) {
- throw new \Exception('Plugin already installed');
- }
-
- // 检查依赖
- if (!$this->checkDependencies($config['require'] ?? [])) {
- throw new \Exception('Dependencies not satisfied');
- }
-
- // 运行数据库迁移
- $this->runMigrations(pluginCode: $pluginCode);
-
- DB::beginTransaction();
- try {
- // 提取配置默认值
- $defaultValues = $this->extractDefaultConfig($config);
-
- // 创建插件实例
- $plugin = $this->loadPlugin($pluginCode);
-
- // 注册到数据库
- Plugin::create([
- 'code' => $pluginCode,
- 'name' => $config['name'],
- 'version' => $config['version'],
- 'type' => $config['type'] ?? Plugin::TYPE_FEATURE,
- 'is_enabled' => false,
- 'config' => json_encode($defaultValues),
- 'installed_at' => now(),
- ]);
-
- // 运行插件安装方法
- if (method_exists($plugin, 'install')) {
- $plugin->install();
- }
-
- // 发布插件资源
- $this->publishAssets($pluginCode);
-
- DB::commit();
- return true;
- } catch (\Exception $e) {
- if (DB::transactionLevel() > 0) {
- DB::rollBack();
- }
- throw $e;
- }
- }
-
- /**
- * 提取插件默认配置
- */
- protected function extractDefaultConfig(array $config): array
- {
- $defaultValues = [];
- if (isset($config['config']) && is_array($config['config'])) {
- foreach ($config['config'] as $key => $item) {
- if (is_array($item)) {
- $defaultValues[$key] = $item['default'] ?? null;
- } else {
- $defaultValues[$key] = $item;
- }
- }
- }
- return $defaultValues;
- }
-
- /**
- * 运行插件数据库迁移
- */
- protected function runMigrations(string $pluginCode): void
- {
- $migrationsPath = $this->getPluginPath($pluginCode) . '/database/migrations';
-
- if (File::exists($migrationsPath)) {
- Artisan::call('migrate', [
- '--path' => "plugins/" . Str::studly($pluginCode) . "/database/migrations",
- '--force' => true
- ]);
- }
- }
-
- /**
- * 回滚插件数据库迁移
- */
- protected function runMigrationsRollback(string $pluginCode): void
- {
- $migrationsPath = $this->getPluginPath($pluginCode) . '/database/migrations';
-
- if (File::exists($migrationsPath)) {
- Artisan::call('migrate:rollback', [
- '--path' => "plugins/" . Str::studly($pluginCode) . "/database/migrations",
- '--force' => true
- ]);
- }
- }
-
- /**
- * 发布插件资源
- */
- protected function publishAssets(string $pluginCode): void
- {
- $assetsPath = $this->getPluginPath($pluginCode) . '/resources/assets';
- if (File::exists($assetsPath)) {
- $publishPath = public_path('plugins/' . $pluginCode);
- File::ensureDirectoryExists($publishPath);
- File::copyDirectory($assetsPath, $publishPath);
- }
- }
-
- /**
- * 验证配置文件
- */
- protected function validateConfig(array $config): bool
- {
- $requiredFields = [
- 'name',
- 'code',
- 'version',
- 'description',
- 'author'
- ];
-
- foreach ($requiredFields as $field) {
- if (!isset($config[$field]) || empty($config[$field])) {
- return false;
- }
- }
-
- // 验证插件代码格式
- if (!preg_match('/^[a-z0-9_]+$/', $config['code'])) {
- return false;
- }
-
- // 验证版本号格式
- if (!preg_match('/^\d+\.\d+\.\d+$/', $config['version'])) {
- return false;
- }
-
- // 验证插件类型
- if (isset($config['type'])) {
- $validTypes = ['feature', 'payment'];
- if (!in_array($config['type'], $validTypes)) {
- return false;
- }
- }
-
- return true;
- }
-
- /**
- * 启用插件
- */
- public function enable(string $pluginCode): bool
- {
- $plugin = $this->loadPlugin($pluginCode);
-
- if (!$plugin) {
- Plugin::where('code', $pluginCode)->delete();
- throw new \Exception('Plugin not found: ' . $pluginCode);
- }
-
- // 获取插件配置
- $dbPlugin = Plugin::query()
- ->where('code', $pluginCode)
- ->first();
-
- if ($dbPlugin && !empty($dbPlugin->config)) {
- $values = json_decode($dbPlugin->config, true) ?: [];
- $values = $this->castConfigValuesByType($pluginCode, $values);
- $plugin->setConfig($values);
- }
-
- // 注册服务提供者
- $this->registerServiceProvider($pluginCode);
-
- // 加载路由
- $this->loadRoutes($pluginCode);
-
- // 加载视图
- $this->loadViews($pluginCode);
-
- // 更新数据库状态
- Plugin::query()
- ->where('code', $pluginCode)
- ->update([
- 'is_enabled' => true,
- 'updated_at' => now(),
- ]);
- // 初始化插件
- $plugin->boot();
-
- return true;
- }
-
- /**
- * 禁用插件
- */
- public function disable(string $pluginCode): bool
- {
- $plugin = $this->loadPlugin($pluginCode);
- if (!$plugin) {
- throw new \Exception('Plugin not found');
- }
-
- Plugin::query()
- ->where('code', $pluginCode)
- ->update([
- 'is_enabled' => false,
- 'updated_at' => now(),
- ]);
-
- $plugin->cleanup();
-
- return true;
- }
-
- /**
- * 卸载插件
- */
- public function uninstall(string $pluginCode): bool
- {
- $this->disable($pluginCode);
- $this->runMigrationsRollback($pluginCode);
- Plugin::query()->where('code', $pluginCode)->delete();
-
- return true;
- }
-
- /**
- * 删除插件
- *
- * @param string $pluginCode
- * @return bool
- * @throws \Exception
- */
- public function delete(string $pluginCode): bool
- {
- // 先卸载插件
- if (Plugin::where('code', $pluginCode)->exists()) {
- $this->uninstall($pluginCode);
- }
-
- $pluginPath = $this->getPluginPath($pluginCode);
- if (!File::exists($pluginPath)) {
- throw new \Exception('插件不存在');
- }
-
- // 删除插件目录
- File::deleteDirectory($pluginPath);
-
- return true;
- }
-
- /**
- * 检查依赖关系
- */
- protected function checkDependencies(array $requires): bool
- {
- foreach ($requires as $package => $version) {
- if ($package === 'xboard') {
- // 检查xboard版本
- // 实现版本比较逻辑
- }
- }
- return true;
- }
-
- /**
- * 升级插件
- *
- * @param string $pluginCode
- * @return bool
- * @throws \Exception
- */
- public function update(string $pluginCode): bool
- {
- $dbPlugin = Plugin::where('code', $pluginCode)->first();
- if (!$dbPlugin) {
- throw new \Exception('Plugin not installed: ' . $pluginCode);
- }
-
- // 获取插件配置文件中的最新版本
- $configFile = $this->getPluginPath($pluginCode) . '/config.json';
- if (!File::exists($configFile)) {
- throw new \Exception('Plugin config file not found');
- }
-
- $config = json_decode(File::get($configFile), true);
- if (!$config || !isset($config['version'])) {
- throw new \Exception('Invalid plugin config or missing version');
- }
-
- $newVersion = $config['version'];
- $oldVersion = $dbPlugin->version;
-
- if (version_compare($newVersion, $oldVersion, '<=')) {
- throw new \Exception('Plugin is already up to date');
- }
-
- $this->disable($pluginCode);
- $this->runMigrations($pluginCode);
-
- $plugin = $this->loadPlugin($pluginCode);
- if ($plugin) {
- if (!empty($dbPlugin->config)) {
- $values = json_decode($dbPlugin->config, true) ?: [];
- $values = $this->castConfigValuesByType($pluginCode, $values);
- $plugin->setConfig($values);
- }
-
- $plugin->update($oldVersion, $newVersion);
- }
-
- $dbPlugin->update([
- 'version' => $newVersion,
- 'updated_at' => now(),
- ]);
-
- $this->enable($pluginCode);
-
- return true;
- }
-
- /**
- * 上传插件
- *
- * @param \Illuminate\Http\UploadedFile $file
- * @return bool
- * @throws \Exception
- */
- public function upload($file): bool
- {
- $tmpPath = storage_path('tmp/plugins');
- if (!File::exists($tmpPath)) {
- File::makeDirectory($tmpPath, 0755, true);
- }
-
- $extractPath = $tmpPath . '/' . uniqid();
- $zip = new \ZipArchive();
-
- if ($zip->open($file->path()) !== true) {
- throw new \Exception('无法打开插件包文件');
- }
-
- $zip->extractTo($extractPath);
- $zip->close();
-
- $configFile = File::glob($extractPath . '/*/config.json');
- if (empty($configFile)) {
- $configFile = File::glob($extractPath . '/config.json');
- }
-
- if (empty($configFile)) {
- File::deleteDirectory($extractPath);
- throw new \Exception('插件包格式错误:缺少配置文件');
- }
-
- $pluginPath = dirname(reset($configFile));
- $config = json_decode(File::get($pluginPath . '/config.json'), true);
-
- if (!$this->validateConfig($config)) {
- File::deleteDirectory($extractPath);
- throw new \Exception('插件配置文件格式错误');
- }
-
- $targetPath = $this->pluginPath . '/' . Str::studly($config['code']);
- if (File::exists($targetPath)) {
- $installedConfigPath = $targetPath . '/config.json';
- if (!File::exists($installedConfigPath)) {
- throw new \Exception('已安装插件缺少配置文件,无法判断是否可升级');
- }
- $installedConfig = json_decode(File::get($installedConfigPath), true);
-
- $oldVersion = $installedConfig['version'] ?? null;
- $newVersion = $config['version'] ?? null;
- if (!$oldVersion || !$newVersion) {
- throw new \Exception('插件缺少版本号,无法判断是否可升级');
- }
- if (version_compare($newVersion, $oldVersion, '<=')) {
- throw new \Exception('上传插件版本不高于已安装版本,无法升级');
- }
-
- File::deleteDirectory($targetPath);
- }
-
- File::copyDirectory($pluginPath, $targetPath);
- File::deleteDirectory($pluginPath);
- File::deleteDirectory($extractPath);
-
- if (Plugin::where('code', $config['code'])->exists()) {
- return $this->update($config['code']);
- }
-
- return true;
- }
-
- /**
- * Initializes all enabled plugins from the database.
- * This method ensures that plugins are loaded, and their routes, views,
- * and service providers are registered only once per request cycle.
- */
- public function initializeEnabledPlugins(): void
- {
- if ($this->pluginsInitialized) {
- return;
- }
-
- $enabledPlugins = Plugin::where('is_enabled', true)->get();
-
- foreach ($enabledPlugins as $dbPlugin) {
- try {
- $pluginCode = $dbPlugin->code;
-
- $pluginInstance = $this->loadPlugin($pluginCode);
- if (!$pluginInstance) {
- continue;
- }
-
- if (!empty($dbPlugin->config)) {
- $values = json_decode($dbPlugin->config, true) ?: [];
- $values = $this->castConfigValuesByType($pluginCode, $values);
- $pluginInstance->setConfig($values);
- }
-
- $this->registerServiceProvider($pluginCode);
- $this->loadRoutes($pluginCode);
- $this->loadViews($pluginCode);
- $this->registerPluginCommands($pluginCode, $pluginInstance);
-
- $pluginInstance->boot();
-
- } catch (\Exception $e) {
- Log::error("Failed to initialize plugin '{$dbPlugin->code}': " . $e->getMessage());
- }
- }
-
- $this->pluginsInitialized = true;
- }
-
- /**
- * Register scheduled tasks for all enabled plugins.
- * Called from Console Kernel. Only loads main plugin class and config for scheduling.
- * Avoids full HTTP/plugin boot overhead.
- *
- * @param \Illuminate\Console\Scheduling\Schedule $schedule
- */
- public function registerPluginSchedules(Schedule $schedule): void
- {
- Plugin::where('is_enabled', true)
- ->get()
- ->each(function ($dbPlugin) use ($schedule) {
- try {
- $pluginInstance = $this->loadPlugin($dbPlugin->code);
- if (!$pluginInstance) {
- return;
- }
- if (!empty($dbPlugin->config)) {
- $values = json_decode($dbPlugin->config, true) ?: [];
- $values = $this->castConfigValuesByType($dbPlugin->code, $values);
- $pluginInstance->setConfig($values);
- }
- $pluginInstance->schedule($schedule);
-
- } catch (\Exception $e) {
- Log::error("Failed to register schedule for plugin '{$dbPlugin->code}': " . $e->getMessage());
- }
- });
- }
-
- /**
- * Get all enabled plugin instances.
- *
- * This method ensures that all enabled plugins are initialized and then returns them.
- * It's the central point for accessing active plugins.
- *
- * @return array
- */
- public function getEnabledPlugins(): array
- {
- $this->initializeEnabledPlugins();
-
- $enabledPluginCodes = Plugin::where('is_enabled', true)
- ->pluck('code')
- ->all();
-
- return array_intersect_key($this->loadedPlugins, array_flip($enabledPluginCodes));
- }
-
- /**
- * Get enabled plugins by type
- */
- public function getEnabledPluginsByType(string $type): array
- {
- $this->initializeEnabledPlugins();
-
- $enabledPluginCodes = Plugin::where('is_enabled', true)
- ->byType($type)
- ->pluck('code')
- ->all();
-
- return array_intersect_key($this->loadedPlugins, array_flip($enabledPluginCodes));
- }
-
- /**
- * Get enabled payment plugins
- */
- public function getEnabledPaymentPlugins(): array
- {
- return $this->getEnabledPluginsByType('payment');
- }
-
- /**
- * install default plugins
- */
- public static function installDefaultPlugins(): void
- {
- foreach (Plugin::PROTECTED_PLUGINS as $pluginCode) {
- if (!Plugin::where('code', $pluginCode)->exists()) {
- $pluginManager = app(self::class);
- $pluginManager->install($pluginCode);
- $pluginManager->enable($pluginCode);
- Log::info("Installed and enabled default plugin: {$pluginCode}");
- }
- }
- }
-
- /**
- * 根据 config.json 的类型信息对配置值进行类型转换(仅处理 type=json 键)。
- */
- protected function castConfigValuesByType(string $pluginCode, array $values): array
- {
- $types = $this->getConfigTypes($pluginCode);
- foreach ($values as $key => $value) {
- $type = $types[$key] ?? null;
-
- if ($type === 'json') {
- if (is_array($value)) {
- continue;
- }
-
- if (is_string($value) && $value !== '') {
- $decoded = json_decode($value, true);
- if (json_last_error() === JSON_ERROR_NONE) {
- $values[$key] = $decoded;
- }
- }
- }
- }
- return $values;
- }
-
- /**
- * 读取并缓存插件 config.json 中的键类型映射。
- */
- protected function getConfigTypes(string $pluginCode): array
- {
- if (isset($this->configTypesCache[$pluginCode])) {
- return $this->configTypesCache[$pluginCode];
- }
- $types = [];
- $configFile = $this->getPluginPath($pluginCode) . '/config.json';
- if (File::exists($configFile)) {
- $config = json_decode(File::get($configFile), true);
- $fields = $config['config'] ?? [];
- foreach ($fields as $key => $meta) {
- $types[$key] = is_array($meta) ? ($meta['type'] ?? 'string') : 'string';
- }
- }
- $this->configTypesCache[$pluginCode] = $types;
- return $types;
- }
-}
\ No newline at end of file
diff --git a/Xboard/app/Services/ServerService.php b/Xboard/app/Services/ServerService.php
deleted file mode 100644
index 00491ca..0000000
--- a/Xboard/app/Services/ServerService.php
+++ /dev/null
@@ -1,296 +0,0 @@
-get()->append([
- 'last_check_at',
- 'last_push_at',
- 'online',
- 'is_online',
- 'available_status',
- 'cache_key',
- 'load_status',
- 'metrics',
- 'online_conn'
- ]);
- }
-
- /**
- * 获取指定用户可用的服务器列表
- * @param User $user
- * @return array
- */
- public static function getAvailableServers(User $user): array
- {
- $servers = Server::whereJsonContains('group_ids', (string) $user->group_id)
- ->where('show', true)
- ->where(function ($query) {
- $query->whereNull('transfer_enable')
- ->orWhere('transfer_enable', 0)
- ->orWhereRaw('u + d < transfer_enable');
- })
- ->orderBy('sort', 'ASC')
- ->get()
- ->append(['last_check_at', 'last_push_at', 'online', 'is_online', 'available_status', 'cache_key', 'server_key']);
-
- $servers = collect($servers)->map(function ($server) use ($user) {
- // 判断动态端口
- if (str_contains($server->port, '-')) {
- $port = $server->port;
- $server->port = (int) Helper::randomPort($port);
- $server->ports = $port;
- } else {
- $server->port = (int) $server->port;
- }
- $server->password = $server->generateServerPassword($user);
- $server->rate = $server->getCurrentRate();
- return $server;
- })->toArray();
-
- return $servers;
- }
-
- /**
- * 根据权限组获取可用的用户列表
- * @param array $groupIds
- * @return Collection
- */
- public static function getAvailableUsers(Server $node)
- {
- $users = User::toBase()
- ->whereIn('group_id', $node->group_ids)
- ->whereRaw('u + d < transfer_enable')
- ->where(function ($query) {
- $query->where('expired_at', '>=', time())
- ->orWhere('expired_at', NULL);
- })
- ->where('banned', 0)
- ->select([
- 'id',
- 'uuid',
- 'speed_limit',
- 'device_limit'
- ])
- ->get();
- return HookManager::filter('server.users.get', $users, $node);
- }
-
- // 获取路由规则
- public static function getRoutes(array $routeIds)
- {
- $routes = ServerRoute::select(['id', 'match', 'action', 'action_value'])->whereIn('id', $routeIds)->get();
- return $routes;
- }
-
- /**
- * Update node metrics and load status
- */
- public static function updateMetrics(Server $node, array $metrics): void
- {
- $nodeType = strtoupper($node->type);
- $nodeId = $node->id;
- $cacheTime = max(300, (int) admin_setting('server_push_interval', 60) * 3);
-
- $metricsData = [
- 'uptime' => (int) ($metrics['uptime'] ?? 0),
- 'goroutines' => (int) ($metrics['goroutines'] ?? 0),
- 'active_connections' => (int) ($metrics['active_connections'] ?? 0),
- 'total_connections' => (int) ($metrics['total_connections'] ?? 0),
- 'total_users' => (int) ($metrics['total_users'] ?? 0),
- 'active_users' => (int) ($metrics['active_users'] ?? 0),
- 'inbound_speed' => (int) ($metrics['inbound_speed'] ?? 0),
- 'outbound_speed' => (int) ($metrics['outbound_speed'] ?? 0),
- 'cpu_per_core' => $metrics['cpu_per_core'] ?? [],
- 'load' => $metrics['load'] ?? [],
- 'speed_limiter' => $metrics['speed_limiter'] ?? [],
- 'gc' => $metrics['gc'] ?? [],
- 'api' => $metrics['api'] ?? [],
- 'ws' => $metrics['ws'] ?? [],
- 'limits' => $metrics['limits'] ?? [],
- 'updated_at' => now()->timestamp,
- 'kernel_status' => (bool) ($metrics['kernel_status'] ?? false),
- ];
-
- \Illuminate\Support\Facades\Cache::put(
- \App\Utils\CacheKey::get('SERVER_' . $nodeType . '_METRICS', $nodeId),
- $metricsData,
- $cacheTime
- );
- }
-
- public static function buildNodeConfig(Server $node): array
- {
- $nodeType = $node->type;
- $protocolSettings = $node->protocol_settings;
- $serverPort = $node->server_port;
- $host = $node->host;
-
- $baseConfig = [
- 'protocol' => $nodeType,
- 'listen_ip' => '0.0.0.0',
- 'server_port' => (int) $serverPort,
- 'network' => data_get($protocolSettings, 'network'),
- 'networkSettings' => data_get($protocolSettings, 'network_settings') ?: null,
- ];
-
- $response = match ($nodeType) {
- 'shadowsocks' => [
- ...$baseConfig,
- 'cipher' => $protocolSettings['cipher'],
- 'plugin' => $protocolSettings['plugin'],
- 'plugin_opts' => $protocolSettings['plugin_opts'],
- 'server_key' => match ($protocolSettings['cipher']) {
- '2022-blake3-aes-128-gcm' => Helper::getServerKey($node->created_at, 16),
- '2022-blake3-aes-256-gcm' => Helper::getServerKey($node->created_at, 32),
- default => null,
- },
- ],
- 'vmess' => [
- ...$baseConfig,
- 'tls' => (int) $protocolSettings['tls'],
- 'multiplex' => data_get($protocolSettings, 'multiplex'),
- ],
- 'trojan' => [
- ...$baseConfig,
- 'host' => $host,
- 'server_name' => $protocolSettings['server_name'],
- 'multiplex' => data_get($protocolSettings, 'multiplex'),
- 'tls' => (int) $protocolSettings['tls'],
- 'tls_settings' => match ((int) $protocolSettings['tls']) {
- 2 => $protocolSettings['reality_settings'],
- default => null,
- },
- ],
- 'vless' => [
- ...$baseConfig,
- 'tls' => (int) $protocolSettings['tls'],
- 'flow' => $protocolSettings['flow'],
- 'decryption' => data_get($protocolSettings, 'encryption.decryption'),
- 'tls_settings' => match ((int) $protocolSettings['tls']) {
- 2 => $protocolSettings['reality_settings'],
- default => $protocolSettings['tls_settings'],
- },
- 'multiplex' => data_get($protocolSettings, 'multiplex'),
- ],
- 'hysteria' => [
- ...$baseConfig,
- 'server_port' => (int) $serverPort,
- 'version' => (int) $protocolSettings['version'],
- 'host' => $host,
- 'server_name' => $protocolSettings['tls']['server_name'],
- 'up_mbps' => (int) $protocolSettings['bandwidth']['up'],
- 'down_mbps' => (int) $protocolSettings['bandwidth']['down'],
- ...match ((int) $protocolSettings['version']) {
- 1 => ['obfs' => $protocolSettings['obfs']['password'] ?? null],
- 2 => [
- 'obfs' => $protocolSettings['obfs']['open'] ? $protocolSettings['obfs']['type'] : null,
- 'obfs-password' => $protocolSettings['obfs']['password'] ?? null,
- ],
- default => [],
- },
- ],
- 'tuic' => [
- ...$baseConfig,
- 'version' => (int) $protocolSettings['version'],
- 'server_port' => (int) $serverPort,
- 'server_name' => $protocolSettings['tls']['server_name'],
- 'congestion_control' => $protocolSettings['congestion_control'],
- 'tls_settings' => data_get($protocolSettings, 'tls_settings'),
- 'auth_timeout' => '3s',
- 'zero_rtt_handshake' => false,
- 'heartbeat' => '3s',
- ],
- 'anytls' => [
- ...$baseConfig,
- 'server_port' => (int) $serverPort,
- 'server_name' => $protocolSettings['tls']['server_name'],
- 'padding_scheme' => $protocolSettings['padding_scheme'],
- ],
- 'socks' => [
- ...$baseConfig,
- 'server_port' => (int) $serverPort,
- ],
- 'naive' => [
- ...$baseConfig,
- 'server_port' => (int) $serverPort,
- 'tls' => (int) $protocolSettings['tls'],
- 'tls_settings' => $protocolSettings['tls_settings'],
- ],
- 'http' => [
- ...$baseConfig,
- 'server_port' => (int) $serverPort,
- 'tls' => (int) $protocolSettings['tls'],
- 'tls_settings' => $protocolSettings['tls_settings'],
- ],
- 'mieru' => [
- ...$baseConfig,
- 'server_port' => (int) $serverPort,
- 'transport' => data_get($protocolSettings, 'transport', 'TCP'),
- 'traffic_pattern' => $protocolSettings['traffic_pattern'],
- // 'multiplex' => data_get($protocolSettings, 'multiplex'),
- ],
- default => [],
- };
-
- // $response = array_filter(
- // $response,
- // static fn ($value) => $value !== null
- // );
-
- if (!empty($node['route_ids'])) {
- $response['routes'] = self::getRoutes($node['route_ids']);
- }
-
- if (!empty($node['custom_outbounds'])) {
- $response['custom_outbounds'] = $node['custom_outbounds'];
- }
-
- if (!empty($node['custom_routes'])) {
- $response['custom_routes'] = $node['custom_routes'];
- }
-
- if (!empty($node['cert_config']) && data_get($node['cert_config'],'cert_mode') !== 'none' ) {
- $response['cert_config'] = $node['cert_config'];
- }
-
- return $response;
- }
-
- /**
- * 根据协议类型和标识获取服务器
- * @param int $serverId
- * @param string $serverType
- * @return Server|null
- */
- public static function getServer($serverId, ?string $serverType = null): Server | null
- {
- return Server::query()
- ->when($serverType, function ($query) use ($serverType) {
- $query->where('type', Server::normalizeType($serverType));
- })
- ->where(function ($query) use ($serverId) {
- $query->where('code', $serverId)
- ->orWhere('id', $serverId);
- })
- ->orderByRaw('CASE WHEN code = ? THEN 0 ELSE 1 END', [$serverId])
- ->first();
- }
-}
diff --git a/Xboard/app/Services/SettingService.php b/Xboard/app/Services/SettingService.php
deleted file mode 100644
index 37e34d9..0000000
--- a/Xboard/app/Services/SettingService.php
+++ /dev/null
@@ -1,18 +0,0 @@
-first();
- return $setting ? $setting->value : $default;
- }
-
- public function getAll(){
- return SettingModel::all()->pluck('value', 'name')->toArray();
- }
-}
diff --git a/Xboard/app/Services/StatisticalService.php b/Xboard/app/Services/StatisticalService.php
deleted file mode 100644
index e57d6ae..0000000
--- a/Xboard/app/Services/StatisticalService.php
+++ /dev/null
@@ -1,378 +0,0 @@
-redis = Redis::connection();
-
- }
-
- public function setStartAt($timestamp)
- {
- $this->startAt = $timestamp;
- $this->statServerKey = "stat_server_{$this->startAt}";
- $this->statUserKey = "stat_user_{$this->startAt}";
- }
-
- public function setEndAt($timestamp)
- {
- $this->endAt = $timestamp;
- }
-
- /**
- * 生成统计报表
- */
- public function generateStatData(): array
- {
- $startAt = $this->startAt;
- $endAt = $this->endAt;
- if (!$startAt || !$endAt) {
- $startAt = strtotime(date('Y-m-d'));
- $endAt = strtotime('+1 day', $startAt);
- }
- $data = [];
- $data['order_count'] = Order::where('created_at', '>=', $startAt)
- ->where('created_at', '<', $endAt)
- ->count();
- $data['order_total'] = Order::where('created_at', '>=', $startAt)
- ->where('created_at', '<', $endAt)
- ->sum('total_amount');
- $data['paid_count'] = Order::where('paid_at', '>=', $startAt)
- ->where('paid_at', '<', $endAt)
- ->whereNotIn('status', [0, 2])
- ->count();
- $data['paid_total'] = Order::where('paid_at', '>=', $startAt)
- ->where('paid_at', '<', $endAt)
- ->whereNotIn('status', [0, 2])
- ->sum('total_amount');
- $commissionLogBuilder = CommissionLog::where('created_at', '>=', $startAt)
- ->where('created_at', '<', $endAt);
- $data['commission_count'] = $commissionLogBuilder->count();
- $data['commission_total'] = $commissionLogBuilder->sum('get_amount');
- $data['register_count'] = User::where('created_at', '>=', $startAt)
- ->where('created_at', '<', $endAt)
- ->count();
- $data['invite_count'] = User::where('created_at', '>=', $startAt)
- ->where('created_at', '<', $endAt)
- ->whereNotNull('invite_user_id')
- ->count();
- $data['transfer_used_total'] = StatServer::where('created_at', '>=', $startAt)
- ->where('created_at', '<', $endAt)
- ->select(DB::raw('SUM(u) + SUM(d) as total'))
- ->value('total') ?? 0;
- return $data;
- }
-
- /**
- * 往服务器报表缓存正追加流量使用数据
- */
- public function statServer($serverId, $serverType, $u, $d)
- {
- $u_menber = "{$serverType}_{$serverId}_u"; //储存上传流量的集合成员
- $d_menber = "{$serverType}_{$serverId}_d"; //储存下载流量的集合成员
- $this->redis->zincrby($this->statServerKey, $u, $u_menber);
- $this->redis->zincrby($this->statServerKey, $d, $d_menber);
- }
-
- /**
- * 追加用户使用流量
- */
- public function statUser($rate, $userId, $u, $d)
- {
- $u_menber = "{$rate}_{$userId}_u"; //储存上传流量的集合成员
- $d_menber = "{$rate}_{$userId}_d"; //储存下载流量的集合成员
- $this->redis->zincrby($this->statUserKey, $u, $u_menber);
- $this->redis->zincrby($this->statUserKey, $d, $d_menber);
- }
-
- /**
- * 获取指定用户的流量使用情况
- */
- public function getStatUserByUserID(int|string $userId): array
- {
-
- $stats = [];
- $statsUser = $this->redis->zrange($this->statUserKey, 0, -1, true);
- foreach ($statsUser as $member => $value) {
- list($rate, $uid, $type) = explode('_', $member);
- if (intval($uid) !== intval($userId))
- continue;
- $key = "{$rate}_{$uid}";
- $stats[$key] = $stats[$key] ?? [
- 'record_at' => $this->startAt,
- 'server_rate' => number_format((float) $rate, 2, '.', ''),
- 'u' => 0,
- 'd' => 0,
- 'user_id' => intval($userId),
- ];
- $stats[$key][$type] += $value;
- }
- return array_values($stats);
- }
-
- /**
- * 获取缓存中的用户报表
- */
- public function getStatUser()
- {
- $stats = [];
- $statsUser = $this->redis->zrange($this->statUserKey, 0, -1, true);
- foreach ($statsUser as $member => $value) {
- list($rate, $uid, $type) = explode('_', $member);
- $key = "{$rate}_{$uid}";
- $stats[$key] = $stats[$key] ?? [
- 'record_at' => $this->startAt,
- 'server_rate' => $rate,
- 'u' => 0,
- 'd' => 0,
- 'user_id' => intval($uid),
- ];
- $stats[$key][$type] += $value;
- }
- return array_values($stats);
- }
-
-
- /**
- * Retrieve server statistics from Redis cache.
- *
- * @return array
- */
- public function getStatServer(): array
- {
- /** @var array $stats */
- $stats = [];
- $statsServer = $this->redis->zrange($this->statServerKey, 0, -1, true);
-
- foreach ($statsServer as $member => $value) {
- $parts = explode('_', $member);
- if (count($parts) !== 3) {
- continue; // Skip malformed members
- }
- [$serverType, $serverId, $type] = $parts;
-
- if (!in_array($type, ['u', 'd'], true)) {
- continue; // Skip invalid types
- }
-
- $key = "{$serverType}_{$serverId}";
- if (!isset($stats[$key])) {
- $stats[$key] = [
- 'server_id' => (int) $serverId,
- 'server_type' => $serverType,
- 'u' => 0.0,
- 'd' => 0.0,
- ];
- }
- $stats[$key][$type] += (float) $value;
- }
-
- return array_values($stats);
- }
-
- /**
- * 清除用户报表缓存数据
- */
- public function clearStatUser()
- {
- $this->redis->del($this->statUserKey);
- }
-
- /**
- * 清除服务器报表缓存数据
- */
- public function clearStatServer()
- {
- $this->redis->del($this->statServerKey);
- }
-
- public function getStatRecord($type)
- {
- switch ($type) {
- case "paid_total": {
- return Stat::select([
- '*',
- DB::raw('paid_total / 100 as paid_total')
- ])
- ->where('record_at', '>=', $this->startAt)
- ->where('record_at', '<', $this->endAt)
- ->orderBy('record_at', 'ASC')
- ->get();
- }
- case "commission_total": {
- return Stat::select([
- '*',
- DB::raw('commission_total / 100 as commission_total')
- ])
- ->where('record_at', '>=', $this->startAt)
- ->where('record_at', '<', $this->endAt)
- ->orderBy('record_at', 'ASC')
- ->get();
- }
- case "register_count": {
- return Stat::where('record_at', '>=', $this->startAt)
- ->where('record_at', '<', $this->endAt)
- ->orderBy('record_at', 'ASC')
- ->get();
- }
- }
- }
-
- public function getRanking($type, $limit = 20)
- {
- switch ($type) {
- case 'server_traffic_rank': {
- return $this->buildServerTrafficRank($limit);
- }
- case 'user_consumption_rank': {
- return $this->buildUserConsumptionRank($limit);
- }
- case 'invite_rank': {
- return $this->buildInviteRank($limit);
- }
- }
- }
-
- /**
- * 获取指定日期范围内的节点流量排行
- * @param mixed ...$times 可选值:'today', 'tomorrow', 'last_week',或指定日期范围,格式:timestamp
- * @return array
- */
-
- public static function getServerRank(...$times)
- {
- $startAt = 0;
- $endAt = Carbon::tomorrow()->endOfDay()->timestamp;
-
- if (count($times) == 1) {
- switch ($times[0]) {
- case 'today':
- $startAt = Carbon::today()->startOfDay()->timestamp;
- $endAt = Carbon::today()->endOfDay()->timestamp;
- break;
- case 'yesterday':
- $startAt = Carbon::yesterday()->startOfDay()->timestamp;
- $endAt = Carbon::yesterday()->endOfDay()->timestamp;
- break;
- case 'last_week':
- $startAt = Carbon::now()->subWeek()->startOfWeek()->timestamp;
- $endAt = Carbon::now()->endOfDay()->timestamp;
- break;
- }
- } else if (count($times) == 2) {
- $startAt = $times[0];
- $endAt = $times[1];
- }
-
- $statistics = Server::whereHas(
- 'stats',
- function ($query) use ($startAt, $endAt) {
- $query->where('record_at', '>=', $startAt)
- ->where('record_at', '<', $endAt)
- ->where('record_type', 'd');
- }
- )
- ->withSum('stats as u', 'u') // 预加载 u 的总和
- ->withSum('stats as d', 'd') // 预加载 d 的总和
- ->get()
- ->map(function ($item) {
- return [
- 'server_name' => optional($item->parent)->name ?? $item->name,
- 'server_id' => $item->id,
- 'server_type' => $item->type,
- 'u' => (int) $item->u,
- 'd' => (int) $item->d,
- 'total' => (int) $item->u + (int) $item->d,
- ];
- })
- ->sortByDesc('total')
- ->values()
- ->toArray();
- return $statistics;
- }
-
- private function buildInviteRank($limit)
- {
- $stats = User::select([
- 'invite_user_id',
- DB::raw('count(*) as count')
- ])
- ->where('created_at', '>=', $this->startAt)
- ->where('created_at', '<', $this->endAt)
- ->whereNotNull('invite_user_id')
- ->groupBy('invite_user_id')
- ->orderBy('count', 'DESC')
- ->limit($limit)
- ->get();
-
- $users = User::whereIn('id', $stats->pluck('invite_user_id')->toArray())->get()->keyBy('id');
- foreach ($stats as $k => $v) {
- if (!isset($users[$v['invite_user_id']]))
- continue;
- $stats[$k]['email'] = $users[$v['invite_user_id']]['email'];
- }
- return $stats;
- }
-
- private function buildUserConsumptionRank($limit)
- {
- $stats = StatUser::select([
- 'user_id',
- DB::raw('sum(u) as u'),
- DB::raw('sum(d) as d'),
- DB::raw('sum(u) + sum(d) as total')
- ])
- ->where('record_at', '>=', $this->startAt)
- ->where('record_at', '<', $this->endAt)
- ->groupBy('user_id')
- ->orderBy('total', 'DESC')
- ->limit($limit)
- ->get();
- $users = User::whereIn('id', $stats->pluck('user_id')->toArray())->get()->keyBy('id');
- foreach ($stats as $k => $v) {
- if (!isset($users[$v['user_id']]))
- continue;
- $stats[$k]['email'] = $users[$v['user_id']]['email'];
- }
- return $stats;
- }
-
- private function buildServerTrafficRank($limit)
- {
- return StatServer::select([
- 'server_id',
- 'server_type',
- DB::raw('sum(u) as u'),
- DB::raw('sum(d) as d'),
- DB::raw('sum(u) + sum(d) as total')
- ])
- ->where('record_at', '>=', $this->startAt)
- ->where('record_at', '<', $this->endAt)
- ->groupBy('server_id', 'server_type')
- ->orderBy('total', 'DESC')
- ->limit($limit)
- ->get();
- }
-}
diff --git a/Xboard/app/Services/TelegramService.php b/Xboard/app/Services/TelegramService.php
deleted file mode 100644
index 83f52c7..0000000
--- a/Xboard/app/Services/TelegramService.php
+++ /dev/null
@@ -1,160 +0,0 @@
-apiUrl = "https://api.telegram.org/bot{$botToken}/";
-
- $this->http = Http::timeout(30)
- ->retry(3, 1000)
- ->withHeaders([
- 'Accept' => 'application/json',
- ]);
- }
-
- public function sendMessage(int $chatId, string $text, string $parseMode = ''): void
- {
- $text = $parseMode === 'markdown' ? str_replace('_', '\_', $text) : $text;
-
- $this->request('sendMessage', [
- 'chat_id' => $chatId,
- 'text' => $text,
- 'parse_mode' => $parseMode ?: null,
- ]);
- }
-
- public function approveChatJoinRequest(int $chatId, int $userId): void
- {
- $this->request('approveChatJoinRequest', [
- 'chat_id' => $chatId,
- 'user_id' => $userId,
- ]);
- }
-
- public function declineChatJoinRequest(int $chatId, int $userId): void
- {
- $this->request('declineChatJoinRequest', [
- 'chat_id' => $chatId,
- 'user_id' => $userId,
- ]);
- }
-
- public function getMe(): object
- {
- return $this->request('getMe');
- }
-
- public function setWebhook(string $url): object
- {
- $result = $this->request('setWebhook', ['url' => $url]);
- return $result;
- }
-
- /**
- * 注册 Bot 命令列表
- */
- public function registerBotCommands(): void
- {
- try {
- $commands = HookManager::filter('telegram.bot.commands', []);
-
- if (empty($commands)) {
- Log::warning('没有找到任何 Telegram Bot 命令');
- return;
- }
-
- $this->request('setMyCommands', [
- 'commands' => json_encode($commands),
- 'scope' => json_encode(['type' => 'default'])
- ]);
-
- Log::info('Telegram Bot 命令注册成功', [
- 'commands_count' => count($commands),
- 'commands' => $commands
- ]);
-
- } catch (\Exception $e) {
- Log::error('Telegram Bot 命令注册失败', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
- }
- }
-
- /**
- * 获取当前注册的命令列表
- */
- public function getMyCommands(): object
- {
- return $this->request('getMyCommands');
- }
-
- /**
- * 删除所有命令
- */
- public function deleteMyCommands(): object
- {
- return $this->request('deleteMyCommands');
- }
-
- public function sendMessageWithAdmin(string $message, bool $isStaff = false): void
- {
- $query = User::where('telegram_id', '!=', null);
- $query->where(
- fn($q) => $q->where('is_admin', 1)
- ->when($isStaff, fn($q) => $q->orWhere('is_staff', 1))
- );
- $users = $query->get();
- foreach ($users as $user) {
- SendTelegramJob::dispatch($user->telegram_id, $message);
- }
- }
-
- protected function request(string $method, array $params = []): object
- {
- try {
- $response = $this->http->get($this->apiUrl . $method, $params);
-
- if (!$response->successful()) {
- throw new ApiException("HTTP 请求失败: {$response->status()}");
- }
-
- $data = $response->object();
-
- if (!isset($data->ok)) {
- throw new ApiException('无效的 Telegram API 响应');
- }
-
- if (!$data->ok) {
- $description = $data->description ?? '未知错误';
- throw new ApiException("Telegram API 错误: {$description}");
- }
-
- return $data;
-
- } catch (\Exception $e) {
- Log::error('Telegram API 请求失败', [
- 'method' => $method,
- 'params' => $params,
- 'error' => $e->getMessage(),
- ]);
-
- throw new ApiException("Telegram 服务错误: {$e->getMessage()}");
- }
- }
-}
diff --git a/Xboard/app/Services/ThemeService.php b/Xboard/app/Services/ThemeService.php
deleted file mode 100644
index 232682d..0000000
--- a/Xboard/app/Services/ThemeService.php
+++ /dev/null
@@ -1,424 +0,0 @@
-registerThemeViewPaths();
- }
-
- /**
- * Register theme view paths
- */
- private function registerThemeViewPaths(): void
- {
- $systemPath = base_path(self::SYSTEM_THEME_DIR);
- if (File::exists($systemPath)) {
- View::addNamespace('theme', $systemPath);
- }
-
- $userPath = base_path(self::USER_THEME_DIR);
- if (File::exists($userPath)) {
- View::prependNamespace('theme', $userPath);
- }
- }
-
- /**
- * Get theme view path
- */
- public function getThemeViewPath(string $theme): ?string
- {
- $themePath = $this->getThemePath($theme);
- if (!$themePath) {
- return null;
- }
- return $themePath . '/dashboard.blade.php';
- }
-
- /**
- * Get all available themes
- */
- public function getList(): array
- {
- $themes = [];
-
- // 获取系统主题
- $systemPath = base_path(self::SYSTEM_THEME_DIR);
- if (File::exists($systemPath)) {
- $themes = $this->getThemesFromPath($systemPath, false);
- }
-
- // 获取用户主题
- $userPath = base_path(self::USER_THEME_DIR);
- if (File::exists($userPath)) {
- $themes = array_merge($themes, $this->getThemesFromPath($userPath, true));
- }
-
- return $themes;
- }
-
- /**
- * Get themes from specified path
- */
- private function getThemesFromPath(string $path, bool $canDelete): array
- {
- return collect(File::directories($path))
- ->mapWithKeys(function ($dir) use ($canDelete) {
- $name = basename($dir);
- if (
- !File::exists($dir . '/' . self::CONFIG_FILE) ||
- !File::exists($dir . '/dashboard.blade.php')
- ) {
- return [];
- }
- $config = $this->readConfigFile($name);
- if (!$config) {
- return [];
- }
-
- $config['can_delete'] = $canDelete && $name !== admin_setting('current_theme');
- $config['is_system'] = !$canDelete;
- return [$name => $config];
- })->toArray();
- }
-
- /**
- * Upload new theme
- */
- public function upload(UploadedFile $file): bool
- {
- $zip = new ZipArchive;
- $tmpPath = storage_path('tmp/' . uniqid());
-
- try {
- if ($zip->open($file->path()) !== true) {
- throw new Exception('Invalid theme package');
- }
-
- $configEntry = collect(range(0, $zip->numFiles - 1))
- ->map(fn($i) => $zip->getNameIndex($i))
- ->first(fn($name) => basename($name) === self::CONFIG_FILE);
-
- if (!$configEntry) {
- throw new Exception('Theme config file not found');
- }
-
- $zip->extractTo($tmpPath);
- $zip->close();
-
- $sourcePath = $tmpPath . '/' . rtrim(dirname($configEntry), '.');
- $configFile = $sourcePath . '/' . self::CONFIG_FILE;
-
- if (!File::exists($configFile)) {
- throw new Exception('Theme config file not found');
- }
-
- $config = json_decode(File::get($configFile), true);
- if (empty($config['name'])) {
- throw new Exception('Theme name not configured');
- }
-
- if (in_array($config['name'], self::SYSTEM_THEMES)) {
- throw new Exception('Cannot upload theme with same name as system theme');
- }
-
- if (!File::exists($sourcePath . '/dashboard.blade.php')) {
- throw new Exception('Missing required theme file: dashboard.blade.php');
- }
-
- $userThemePath = base_path(self::USER_THEME_DIR);
- if (!File::exists($userThemePath)) {
- File::makeDirectory($userThemePath, 0755, true);
- }
-
- $targetPath = $userThemePath . $config['name'];
- if (File::exists($targetPath)) {
- $oldConfigFile = $targetPath . '/config.json';
- if (!File::exists($oldConfigFile)) {
- throw new Exception('Existing theme missing config file');
- }
- $oldConfig = json_decode(File::get($oldConfigFile), true);
- $oldVersion = $oldConfig['version'] ?? '0.0.0';
- $newVersion = $config['version'] ?? '0.0.0';
- if (version_compare($newVersion, $oldVersion, '>')) {
- $this->cleanupThemeFiles($config['name']);
- File::deleteDirectory($targetPath);
- File::copyDirectory($sourcePath, $targetPath);
- // 更新主题时保留用户配置
- $this->initConfig($config['name'], true);
- return true;
- } else {
- throw new Exception('Theme exists and not a newer version');
- }
- }
-
- File::copyDirectory($sourcePath, $targetPath);
- $this->initConfig($config['name']);
-
- return true;
-
- } catch (Exception $e) {
- throw $e;
- } finally {
- if (File::exists($tmpPath)) {
- File::deleteDirectory($tmpPath);
- }
- }
- }
-
- /**
- * Switch theme
- */
- public function switch(string|null $theme): bool
- {
- if ($theme === null) {
- return true;
- }
-
- $currentTheme = admin_setting('current_theme');
-
- try {
- $themePath = $this->getThemePath($theme);
- if (!$themePath) {
- throw new Exception('Theme not found');
- }
-
- if (!File::exists($this->getThemeViewPath($theme))) {
- throw new Exception('Theme view file not found');
- }
-
- if ($currentTheme && $currentTheme !== $theme) {
- $this->cleanupThemeFiles($currentTheme);
- }
-
- $targetPath = public_path('theme/' . $theme);
- if (!File::copyDirectory($themePath, $targetPath)) {
- throw new Exception('Failed to copy theme files');
- }
-
- admin_setting(['current_theme' => $theme]);
- return true;
-
- } catch (Exception $e) {
- Log::error('Theme switch failed', ['theme' => $theme, 'error' => $e->getMessage()]);
- throw $e;
- }
- }
-
- /**
- * Delete theme
- */
- public function delete(string $theme): bool
- {
- try {
- if (in_array($theme, self::SYSTEM_THEMES)) {
- throw new Exception('System theme cannot be deleted');
- }
-
- if ($theme === admin_setting('current_theme')) {
- throw new Exception('Current theme cannot be deleted');
- }
-
- $themePath = base_path(self::USER_THEME_DIR . $theme);
- if (!File::exists($themePath)) {
- throw new Exception('Theme not found');
- }
-
- $this->cleanupThemeFiles($theme);
- File::deleteDirectory($themePath);
- admin_setting([self::SETTING_PREFIX . $theme => null]);
- return true;
-
- } catch (Exception $e) {
- Log::error('Theme deletion failed', ['theme' => $theme, 'error' => $e->getMessage()]);
- throw $e;
- }
- }
-
- /**
- * Check if theme exists
- */
- public function exists(string $theme): bool
- {
- return $this->getThemePath($theme) !== null;
- }
-
- /**
- * Get theme path
- */
- public function getThemePath(string $theme): ?string
- {
- $systemPath = base_path(self::SYSTEM_THEME_DIR . $theme);
- if (File::exists($systemPath)) {
- return $systemPath;
- }
-
- $userPath = base_path(self::USER_THEME_DIR . $theme);
- if (File::exists($userPath)) {
- return $userPath;
- }
-
- return null;
- }
-
- /**
- * Get theme config
- */
- public function getConfig(string $theme): ?array
- {
- $config = admin_setting(self::SETTING_PREFIX . $theme);
- if ($config === null) {
- $this->initConfig($theme);
- $config = admin_setting(self::SETTING_PREFIX . $theme);
- }
- return $config;
- }
-
- /**
- * Update theme config
- */
- public function updateConfig(string $theme, array $config): bool
- {
- try {
- if (!$this->getThemePath($theme)) {
- throw new Exception('Theme not found');
- }
-
- $schema = $this->readConfigFile($theme);
- if (!$schema) {
- throw new Exception('Invalid theme config file');
- }
-
- $validFields = collect($schema['configs'] ?? [])->pluck('field_name')->toArray();
- $validConfig = collect($config)
- ->only($validFields)
- ->toArray();
-
- $currentConfig = $this->getConfig($theme) ?? [];
- $newConfig = array_merge($currentConfig, $validConfig);
-
- admin_setting([self::SETTING_PREFIX . $theme => $newConfig]);
- return true;
-
- } catch (Exception $e) {
- Log::error('Config update failed', ['theme' => $theme, 'error' => $e->getMessage()]);
- throw $e;
- }
- }
-
- /**
- * Read theme config file
- */
- private function readConfigFile(string $theme): ?array
- {
- $themePath = $this->getThemePath($theme);
- if (!$themePath) {
- return null;
- }
-
- $file = $themePath . '/' . self::CONFIG_FILE;
- return File::exists($file) ? json_decode(File::get($file), true) : null;
- }
-
- /**
- * Clean up theme files including public directory
- */
- public function cleanupThemeFiles(string $theme): void
- {
- try {
- $publicThemePath = public_path('theme/' . $theme);
- if (File::exists($publicThemePath)) {
- File::deleteDirectory($publicThemePath);
- Log::info('Cleaned up public theme files', ['theme' => $theme, 'path' => $publicThemePath]);
- }
-
- $cacheKey = "theme_{$theme}_assets";
- if (cache()->has($cacheKey)) {
- cache()->forget($cacheKey);
- Log::info('Cleaned up theme cache', ['theme' => $theme, 'cache_key' => $cacheKey]);
- }
-
- } catch (Exception $e) {
- Log::warning('Failed to cleanup theme files', [
- 'theme' => $theme,
- 'error' => $e->getMessage()
- ]);
- }
- }
-
- /**
- * Force refresh current theme public files
- */
- public function refreshCurrentTheme(): bool
- {
- try {
- $currentTheme = admin_setting('current_theme');
- if (!$currentTheme) {
- return false;
- }
-
- $this->cleanupThemeFiles($currentTheme);
-
- $themePath = $this->getThemePath($currentTheme);
- if (!$themePath) {
- throw new Exception('Current theme path not found');
- }
-
- $targetPath = public_path('theme/' . $currentTheme);
- if (!File::copyDirectory($themePath, $targetPath)) {
- throw new Exception('Failed to copy theme files');
- }
-
- Log::info('Refreshed current theme files', ['theme' => $currentTheme]);
- return true;
-
- } catch (Exception $e) {
- Log::error('Failed to refresh current theme', [
- 'theme' => $currentTheme,
- 'error' => $e->getMessage()
- ]);
- return false;
- }
- }
-
- /**
- * Initialize theme config
- *
- * @param string $theme 主题名称
- * @param bool $preserveExisting 是否保留现有配置(更新主题时使用)
- */
- private function initConfig(string $theme, bool $preserveExisting = false): void
- {
- $config = $this->readConfigFile($theme);
- if (!$config) {
- return;
- }
-
- $defaults = collect($config['configs'] ?? [])
- ->mapWithKeys(fn($col) => [$col['field_name'] => $col['default_value'] ?? ''])
- ->toArray();
-
- if ($preserveExisting) {
- $existingConfig = admin_setting(self::SETTING_PREFIX . $theme) ?? [];
- $mergedConfig = array_merge($defaults, $existingConfig);
- admin_setting([self::SETTING_PREFIX . $theme => $mergedConfig]);
- } else {
- admin_setting([self::SETTING_PREFIX . $theme => $defaults]);
- }
- }
-}
diff --git a/Xboard/app/Services/TicketService.php b/Xboard/app/Services/TicketService.php
deleted file mode 100644
index f99d7c7..0000000
--- a/Xboard/app/Services/TicketService.php
+++ /dev/null
@@ -1,125 +0,0 @@
- $userId,
- 'ticket_id' => $ticket->id,
- 'message' => $message
- ]);
- if ($userId !== $ticket->user_id) {
- $ticket->reply_status = Ticket::STATUS_OPENING;
- } else {
- $ticket->reply_status = Ticket::STATUS_CLOSED;
- }
- if (!$ticketMessage || !$ticket->save()) {
- throw new \Exception();
- }
- DB::commit();
- return $ticketMessage;
- } catch (\Exception $e) {
- DB::rollback();
- return false;
- }
- }
-
- public function replyByAdmin($ticketId, $message, $userId): void
- {
- $ticket = Ticket::where('id', $ticketId)
- ->first();
- if (!$ticket) {
- throw new ApiException('工单不存在');
- }
- $ticket->status = Ticket::STATUS_OPENING;
- try {
- DB::beginTransaction();
- $ticketMessage = TicketMessage::create([
- 'user_id' => $userId,
- 'ticket_id' => $ticket->id,
- 'message' => $message
- ]);
- if ($userId !== $ticket->user_id) {
- $ticket->reply_status = Ticket::STATUS_OPENING;
- } else {
- $ticket->reply_status = Ticket::STATUS_CLOSED;
- }
- if (!$ticketMessage || !$ticket->save()) {
- throw new ApiException('工单回复失败');
- }
- DB::commit();
- HookManager::call('ticket.reply.admin.after', [$ticket, $ticketMessage]);
- } catch (\Exception $e) {
- DB::rollBack();
- throw $e;
- }
- $this->sendEmailNotify($ticket, $ticketMessage);
- }
-
- public function createTicket($userId, $subject, $level, $message)
- {
- try {
- DB::beginTransaction();
- if (Ticket::where('status', 0)->where('user_id', $userId)->lockForUpdate()->first()) {
- DB::rollBack();
- throw new ApiException('存在未关闭的工单');
- }
- $ticket = Ticket::create([
- 'user_id' => $userId,
- 'subject' => $subject,
- 'level' => $level
- ]);
- if (!$ticket) {
- throw new ApiException('工单创建失败');
- }
- $ticketMessage = TicketMessage::create([
- 'user_id' => $userId,
- 'ticket_id' => $ticket->id,
- 'message' => $message
- ]);
- if (!$ticketMessage) {
- DB::rollBack();
- throw new ApiException('工单消息创建失败');
- }
- DB::commit();
- return $ticket;
- } catch (\Exception $e) {
- DB::rollBack();
- throw $e;
- }
- }
-
- // 半小时内不再重复通知
- private function sendEmailNotify(Ticket $ticket, TicketMessage $ticketMessage)
- {
- $user = User::find($ticket->user_id);
- $cacheKey = 'ticket_sendEmailNotify_' . $ticket->user_id;
- if (!Cache::get($cacheKey)) {
- Cache::put($cacheKey, 1, 1800);
- SendEmailJob::dispatch([
- 'email' => $user->email,
- 'subject' => '您在' . admin_setting('app_name', 'XBoard') . '的工单得到了回复',
- 'template_name' => 'notify',
- 'template_value' => [
- 'name' => admin_setting('app_name', 'XBoard'),
- 'url' => admin_setting('app_url'),
- 'content' => "主题:{$ticket->subject}\r\n回复内容:{$ticketMessage->message}"
- ]
- ]);
- }
- }
-}
diff --git a/Xboard/app/Services/TrafficResetService.php b/Xboard/app/Services/TrafficResetService.php
deleted file mode 100644
index 256fd69..0000000
--- a/Xboard/app/Services/TrafficResetService.php
+++ /dev/null
@@ -1,415 +0,0 @@
-shouldResetTraffic()) {
- return false;
- }
-
- return $this->performReset($user, $triggerSource);
- }
-
- /**
- * Perform the traffic reset for a user.
- */
- public function performReset(User $user, string $triggerSource = TrafficResetLog::SOURCE_MANUAL): bool
- {
- try {
- return DB::transaction(function () use ($user, $triggerSource) {
- $oldUpload = $user->u ?? 0;
- $oldDownload = $user->d ?? 0;
- $oldTotal = $oldUpload + $oldDownload;
-
- $nextResetTime = $this->calculateNextResetTime($user);
-
- $user->update([
- 'u' => 0,
- 'd' => 0,
- 'last_reset_at' => time(),
- 'reset_count' => $user->reset_count + 1,
- 'next_reset_at' => $nextResetTime ? $nextResetTime->timestamp : null,
- ]);
-
- $this->recordResetLog($user, [
- 'reset_type' => $this->getResetTypeFromPlan($user->plan),
- 'trigger_source' => $triggerSource,
- 'old_upload' => $oldUpload,
- 'old_download' => $oldDownload,
- 'old_total' => $oldTotal,
- 'new_upload' => 0,
- 'new_download' => 0,
- 'new_total' => 0,
- ]);
-
- $this->clearUserCache($user);
- HookManager::call('traffic.reset.after', $user);
- return true;
- });
- } catch (\Exception $e) {
- Log::error(__('traffic_reset.reset_failed'), [
- 'user_id' => $user->id,
- 'email' => $user->email,
- 'error' => $e->getMessage(),
- 'trigger_source' => $triggerSource,
- ]);
-
- return false;
- }
- }
-
- /**
- * Calculate the next traffic reset time for a user.
- */
- public function calculateNextResetTime(User $user): ?Carbon
- {
- if (
- !$user->plan
- || $user->plan->reset_traffic_method === Plan::RESET_TRAFFIC_NEVER
- || ($user->plan->reset_traffic_method === Plan::RESET_TRAFFIC_FOLLOW_SYSTEM
- && (int) admin_setting('reset_traffic_method', Plan::RESET_TRAFFIC_MONTHLY) === Plan::RESET_TRAFFIC_NEVER)
- || $user->expired_at === NULL
- ) {
- return null;
- }
-
- $resetMethod = $user->plan->reset_traffic_method;
-
- if ($resetMethod === Plan::RESET_TRAFFIC_FOLLOW_SYSTEM) {
- $resetMethod = (int) admin_setting('reset_traffic_method', Plan::RESET_TRAFFIC_MONTHLY);
- }
-
- $now = Carbon::now(config('app.timezone'));
-
- return match ($resetMethod) {
- Plan::RESET_TRAFFIC_FIRST_DAY_MONTH => $this->getNextMonthFirstDay($now),
- Plan::RESET_TRAFFIC_MONTHLY => $this->getNextMonthlyReset($user, $now),
- Plan::RESET_TRAFFIC_FIRST_DAY_YEAR => $this->getNextYearFirstDay($now),
- Plan::RESET_TRAFFIC_YEARLY => $this->getNextYearlyReset($user, $now),
- default => null,
- };
- }
-
- /**
- * Get the first day of the next month.
- */
- private function getNextMonthFirstDay(Carbon $from): Carbon
- {
- return $from->copy()->addMonth()->startOfMonth();
- }
-
- /**
- * Get the next monthly reset time based on the user's expiration date.
- *
- * Logic:
- * 1. If the user has no expiration date, reset on the 1st of each month.
- * 2. If the user has an expiration date, use the day of that date as the monthly reset day.
- * 3. Prioritize the reset day in the current month if it has not passed yet.
- * 4. Handle cases where the day does not exist in a month (e.g., 31st in February).
- */
- private function getNextMonthlyReset(User $user, Carbon $from): Carbon
- {
- $expiredAt = Carbon::createFromTimestamp($user->expired_at, config('app.timezone'));
- $resetDay = $expiredAt->day;
- $resetTime = [$expiredAt->hour, $expiredAt->minute, $expiredAt->second];
-
- $currentMonthTarget = $from->copy()->day($resetDay)->setTime(...$resetTime);
- if ($currentMonthTarget->timestamp > $from->timestamp) {
- return $currentMonthTarget;
- }
-
- $nextMonthTarget = $from->copy()->startOfMonth()->addMonths(1)->day($resetDay)->setTime(...$resetTime);
-
- if ($nextMonthTarget->month !== ($from->month % 12) + 1) {
- $nextMonth = ($from->month % 12) + 1;
- $nextYear = $from->year + ($from->month === 12 ? 1 : 0);
- $lastDayOfNextMonth = Carbon::create($nextYear, $nextMonth, 1)->endOfMonth()->day;
- $targetDay = min($resetDay, $lastDayOfNextMonth);
- $nextMonthTarget = Carbon::create($nextYear, $nextMonth, $targetDay)->setTime(...$resetTime);
- }
-
- return $nextMonthTarget;
- }
-
- /**
- * Get the first day of the next year.
- */
- private function getNextYearFirstDay(Carbon $from): Carbon
- {
- return $from->copy()->addYear()->startOfYear();
- }
-
- /**
- * Get the next yearly reset time based on the user's expiration date.
- *
- * Logic:
- * 1. If the user has no expiration date, reset on January 1st of each year.
- * 2. If the user has an expiration date, use the month and day of that date as the yearly reset date.
- * 3. Prioritize the reset date in the current year if it has not passed yet.
- * 4. Handle the case of February 29th in a leap year.
- */
- private function getNextYearlyReset(User $user, Carbon $from): Carbon
- {
- $expiredAt = Carbon::createFromTimestamp($user->expired_at, config('app.timezone'));
- $resetMonth = $expiredAt->month;
- $resetDay = $expiredAt->day;
- $resetTime = [$expiredAt->hour, $expiredAt->minute, $expiredAt->second];
-
- $currentYearTarget = $from->copy()->month($resetMonth)->day($resetDay)->setTime(...$resetTime);
- if ($currentYearTarget->timestamp > $from->timestamp) {
- return $currentYearTarget;
- }
-
- $nextYearTarget = $from->copy()->startOfYear()->addYears(1)->month($resetMonth)->day($resetDay)->setTime(...$resetTime);
-
- if ($nextYearTarget->month !== $resetMonth) {
- $nextYear = $from->year + 1;
- $lastDayOfMonth = Carbon::create($nextYear, $resetMonth, 1)->endOfMonth()->day;
- $targetDay = min($resetDay, $lastDayOfMonth);
- $nextYearTarget = Carbon::create($nextYear, $resetMonth, $targetDay)->setTime(...$resetTime);
- }
-
- return $nextYearTarget;
- }
-
-
- /**
- * Record the traffic reset log.
- */
- private function recordResetLog(User $user, array $data): void
- {
- TrafficResetLog::create([
- 'user_id' => $user->id,
- 'reset_type' => $data['reset_type'],
- 'reset_time' => now(),
- 'old_upload' => $data['old_upload'],
- 'old_download' => $data['old_download'],
- 'old_total' => $data['old_total'],
- 'new_upload' => $data['new_upload'],
- 'new_download' => $data['new_download'],
- 'new_total' => $data['new_total'],
- 'trigger_source' => $data['trigger_source'],
- 'metadata' => $data['metadata'] ?? null,
- ]);
- }
-
- /**
- * Get the reset type from the user's plan.
- */
- private function getResetTypeFromPlan(?Plan $plan): string
- {
- if (!$plan) {
- return TrafficResetLog::TYPE_MANUAL;
- }
-
- $resetMethod = $plan->reset_traffic_method;
-
- if ($resetMethod === Plan::RESET_TRAFFIC_FOLLOW_SYSTEM) {
- $resetMethod = (int) admin_setting('reset_traffic_method', Plan::RESET_TRAFFIC_MONTHLY);
- }
-
- return match ($resetMethod) {
- Plan::RESET_TRAFFIC_FIRST_DAY_MONTH => TrafficResetLog::TYPE_FIRST_DAY_MONTH,
- Plan::RESET_TRAFFIC_MONTHLY => TrafficResetLog::TYPE_MONTHLY,
- Plan::RESET_TRAFFIC_FIRST_DAY_YEAR => TrafficResetLog::TYPE_FIRST_DAY_YEAR,
- Plan::RESET_TRAFFIC_YEARLY => TrafficResetLog::TYPE_YEARLY,
- Plan::RESET_TRAFFIC_NEVER => TrafficResetLog::TYPE_MANUAL,
- default => TrafficResetLog::TYPE_MANUAL,
- };
- }
-
- /**
- * Clear user-related cache.
- */
- private function clearUserCache(User $user): void
- {
- $cacheKeys = [
- "user_traffic_{$user->id}",
- "user_reset_status_{$user->id}",
- "user_subscription_{$user->token}",
- ];
-
- foreach ($cacheKeys as $key) {
- Cache::forget($key);
- }
- }
-
- /**
- * Batch check and reset users. Processes all eligible users in batches.
- */
- public function batchCheckReset(int $batchSize = 100, ?callable $progressCallback = null): array
- {
- $startTime = microtime(true);
- $totalResetCount = 0;
- $totalProcessedCount = 0;
- $batchNumber = 1;
- $errors = [];
- $lastProcessedId = 0;
-
- try {
- do {
- $users = User::where('next_reset_at', '<=', time())
- ->whereNotNull('next_reset_at')
- ->where('id', '>', $lastProcessedId)
- ->where(function ($query) {
- $query->where('expired_at', '>', time())
- ->orWhereNull('expired_at');
- })
- ->where('banned', 0)
- ->whereNotNull('plan_id')
- ->orderBy('id')
- ->limit($batchSize)
- ->get();
-
- if ($users->isEmpty()) {
- break;
- }
-
- $batchResetCount = 0;
-
- if ($progressCallback) {
- $progressCallback([
- 'batch_number' => $batchNumber,
- 'batch_size' => $users->count(),
- 'total_processed' => $totalProcessedCount,
- ]);
- }
-
- foreach ($users as $user) {
- try {
- if ($this->checkAndReset($user, TrafficResetLog::SOURCE_CRON)) {
- $batchResetCount++;
- $totalResetCount++;
- }
- $totalProcessedCount++;
- $lastProcessedId = $user->id;
- } catch (\Exception $e) {
- $error = [
- 'user_id' => $user->id,
- 'email' => $user->email,
- 'error' => $e->getMessage(),
- 'batch' => $batchNumber,
- 'timestamp' => now()->toDateTimeString(),
- ];
- $batchErrors[] = $error;
- $errors[] = $error;
-
- Log::error('User traffic reset failed', $error);
-
- $totalProcessedCount++;
- $lastProcessedId = $user->id;
- }
- }
-
- $batchNumber++;
-
- if ($batchNumber % 10 === 0) {
- gc_collect_cycles();
- }
-
- if ($batchNumber % 5 === 0) {
- usleep(100000);
- }
-
- } while (true);
-
- } catch (\Exception $e) {
- Log::error('Batch traffic reset task failed with an exception', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString(),
- 'total_processed' => $totalProcessedCount,
- 'total_reset' => $totalResetCount,
- 'last_processed_id' => $lastProcessedId,
- ]);
-
- $errors[] = [
- 'type' => 'system_error',
- 'error' => $e->getMessage(),
- 'batch' => $batchNumber,
- 'last_processed_id' => $lastProcessedId,
- 'timestamp' => now()->toDateTimeString(),
- ];
- }
-
- $totalDuration = round(microtime(true) - $startTime, 2);
-
- $result = [
- 'total_processed' => $totalProcessedCount,
- 'total_reset' => $totalResetCount,
- 'total_batches' => $batchNumber - 1,
- 'error_count' => count($errors),
- 'errors' => $errors,
- 'duration' => $totalDuration,
- 'batch_size' => $batchSize,
- 'last_processed_id' => $lastProcessedId,
- 'completed_at' => now()->toDateTimeString(),
- ];
-
- return $result;
- }
-
- /**
- * Set the initial reset time for a new user.
- */
- public function setInitialResetTime(User $user): void
- {
- if ($user->next_reset_at !== null) {
- return;
- }
-
- $nextResetTime = $this->calculateNextResetTime($user);
-
- if ($nextResetTime) {
- $user->update(['next_reset_at' => $nextResetTime->timestamp]);
- }
- }
-
- /**
- * Get the user's traffic reset history.
- */
- public function getUserResetHistory(User $user, int $limit = 10): \Illuminate\Database\Eloquent\Collection
- {
- return $user->trafficResetLogs()
- ->orderBy('reset_time', 'desc')
- ->limit($limit)
- ->get();
- }
-
- /**
- * Check if the user is eligible for traffic reset.
- */
- public function canReset(User $user): bool
- {
- return $user->isActive() && $user->plan !== null;
- }
-
- /**
- * Manually reset a user's traffic (Admin function).
- */
- public function manualReset(User $user, array $metadata = []): bool
- {
- if (!$this->canReset($user)) {
- return false;
- }
-
- return $this->performReset($user, TrafficResetLog::SOURCE_MANUAL);
- }
-}
\ No newline at end of file
diff --git a/Xboard/app/Services/UpdateService.php b/Xboard/app/Services/UpdateService.php
deleted file mode 100644
index de063cd..0000000
--- a/Xboard/app/Services/UpdateService.php
+++ /dev/null
@@ -1,458 +0,0 @@
-getCurrentCommit();
- });
- return $date . '-' . $hash;
- }
-
- /**
- * Update version cache
- */
- public function updateVersionCache(): void
- {
- try {
- $result = Process::run('git log -1 --format=%cd:%H --date=format:%Y%m%d');
- if ($result->successful()) {
- list($date, $hash) = explode(':', trim($result->output()));
- Cache::forever(self::CACHE_VERSION_DATE, $date);
- Cache::forever(self::CACHE_VERSION, substr($hash, 0, 7));
- // Log::info('Version cache updated: ' . $date . '-' . substr($hash, 0, 7));
- return;
- }
- } catch (\Exception $e) {
- Log::error('Failed to get version with date: ' . $e->getMessage());
- }
-
- // Fallback
- Cache::forever(self::CACHE_VERSION_DATE, date('Ymd'));
- $fallbackHash = $this->getCurrentCommit();
- Cache::forever(self::CACHE_VERSION, $fallbackHash);
- Log::info('Version cache updated (fallback): ' . date('Ymd') . '-' . $fallbackHash);
- }
-
- public function checkForUpdates(): array
- {
- try {
- // Get current version commit
- $currentCommit = $this->getCurrentCommit();
- if ($currentCommit === 'unknown') {
- // If unable to get current commit, try to get the first commit
- $currentCommit = $this->getFirstCommit();
- }
- // Get local git logs
- $localLogs = $this->getLocalGitLogs();
- if (empty($localLogs)) {
- Log::error('Failed to get local git logs');
- return $this->getCachedUpdateInfo();
- }
-
- // Get remote latest commits
- $response = Http::withHeaders([
- 'Accept' => 'application/vnd.github.v3+json',
- 'User-Agent' => 'XBoard-Update-Checker'
- ])->get(self::GITHUB_API_URL . '?per_page=50');
-
- if ($response->successful()) {
- $commits = $response->json();
-
- if (empty($commits) || !is_array($commits)) {
- Log::error('Invalid GitHub response format');
- return $this->getCachedUpdateInfo();
- }
-
- $latestCommit = $this->formatCommitHash($commits[0]['sha']);
- $currentIndex = -1;
- $updateLogs = [];
-
- // First, find the current version position in remote commit history
- foreach ($commits as $index => $commit) {
- $shortSha = $this->formatCommitHash($commit['sha']);
- if ($shortSha === $currentCommit) {
- $currentIndex = $index;
- break;
- }
- }
-
- // Check local version status
- $isLocalNewer = false;
- if ($currentIndex === -1) {
- // Current version not found in remote history, check local commits
- foreach ($localLogs as $localCommit) {
- $localHash = $this->formatCommitHash($localCommit['hash']);
- // If latest remote commit found, local is not newer
- if ($localHash === $latestCommit) {
- $isLocalNewer = false;
- break;
- }
- // Record additional local commits
- $updateLogs[] = [
- 'version' => $localHash,
- 'message' => $localCommit['message'],
- 'author' => $localCommit['author'],
- 'date' => $localCommit['date'],
- 'is_local' => true
- ];
- $isLocalNewer = true;
- }
- }
-
- // If local is not newer, collect commits that need to be updated
- if (!$isLocalNewer && $currentIndex > 0) {
- $updateLogs = [];
- // Collect all commits between current version and latest version
- for ($i = 0; $i < $currentIndex; $i++) {
- $commit = $commits[$i];
- $updateLogs[] = [
- 'version' => $this->formatCommitHash($commit['sha']),
- 'message' => $commit['commit']['message'],
- 'author' => $commit['commit']['author']['name'],
- 'date' => $commit['commit']['author']['date'],
- 'is_local' => false
- ];
- }
- }
-
- $hasUpdate = !$isLocalNewer && $currentIndex > 0;
-
- $updateInfo = [
- 'has_update' => $hasUpdate,
- 'is_local_newer' => $isLocalNewer,
- 'latest_version' => $isLocalNewer ? $currentCommit : $latestCommit,
- 'current_version' => $currentCommit,
- 'update_logs' => $updateLogs,
- 'download_url' => $commits[0]['html_url'] ?? '',
- 'published_at' => $commits[0]['commit']['author']['date'] ?? '',
- 'author' => $commits[0]['commit']['author']['name'] ?? '',
- ];
-
- // Cache check results
- $this->setLastCheckTime();
- Cache::put(self::CACHE_UPDATE_INFO, $updateInfo, now()->addHours(24));
-
- return $updateInfo;
- }
-
- return $this->getCachedUpdateInfo();
- } catch (\Exception $e) {
- Log::error('Update check failed: ' . $e->getMessage());
- return $this->getCachedUpdateInfo();
- }
- }
-
- public function executeUpdate(): array
- {
- // Check for new version first
- $updateInfo = $this->checkForUpdates();
- if ($updateInfo['is_local_newer']) {
- return [
- 'success' => false,
- 'message' => __('update.local_newer')
- ];
- }
- if (!$updateInfo['has_update']) {
- return [
- 'success' => false,
- 'message' => __('update.already_latest')
- ];
- }
-
- // Check for update lock
- if (Cache::get(self::CACHE_UPDATE_LOCK)) {
- return [
- 'success' => false,
- 'message' => __('update.process_running')
- ];
- }
-
- try {
- // Set update lock
- Cache::put(self::CACHE_UPDATE_LOCK, true, now()->addMinutes(30));
-
- // 1. Backup database
- $this->backupDatabase();
-
- // 2. Pull latest code
- $result = $this->pullLatestCode();
- if (!$result['success']) {
- throw new \Exception($result['message']);
- }
-
- // 3. Run database migrations
- $this->runMigrations();
-
- // 4. Clear cache
- $this->clearCache();
-
- // 5. Create update flag
- $this->createUpdateFlag();
-
- // 6. Restart Octane if running
- $this->restartOctane();
-
- // Remove update lock
- Cache::forget(self::CACHE_UPDATE_LOCK);
-
- // Format update logs
- $logMessages = array_map(function($log) {
- return sprintf("- %s (%s): %s",
- $log['version'],
- date('Y-m-d H:i', strtotime($log['date'])),
- $log['message']
- );
- }, $updateInfo['update_logs']);
-
- return [
- 'success' => true,
- 'message' => __('update.success', [
- 'from' => $updateInfo['current_version'],
- 'to' => $updateInfo['latest_version']
- ]),
- 'version' => $updateInfo['latest_version'],
- 'update_info' => [
- 'from_version' => $updateInfo['current_version'],
- 'to_version' => $updateInfo['latest_version'],
- 'update_logs' => $logMessages,
- 'author' => $updateInfo['author'],
- 'published_at' => $updateInfo['published_at']
- ]
- ];
-
- } catch (\Exception $e) {
- Log::error('Update execution failed: ' . $e->getMessage());
- Cache::forget(self::CACHE_UPDATE_LOCK);
-
- return [
- 'success' => false,
- 'message' => __('update.failed', ['error' => $e->getMessage()])
- ];
- }
- }
-
- protected function getCurrentCommit(): string
- {
- try {
- // Ensure git configuration is correct
- Process::run(sprintf('git config --global --add safe.directory %s', base_path()));
- $result = Process::run('git rev-parse HEAD');
- $fullHash = trim($result->output());
- return $fullHash ? $this->formatCommitHash($fullHash) : 'unknown';
- } catch (\Exception $e) {
- Log::error('Failed to get current commit: ' . $e->getMessage());
- return 'unknown';
- }
- }
-
- protected function getFirstCommit(): string
- {
- try {
- // Get first commit hash
- $result = Process::run('git rev-list --max-parents=0 HEAD');
- $fullHash = trim($result->output());
- return $fullHash ? $this->formatCommitHash($fullHash) : 'unknown';
- } catch (\Exception $e) {
- Log::error('Failed to get first commit: ' . $e->getMessage());
- return 'unknown';
- }
- }
-
- protected function formatCommitHash(string $hash): string
- {
- // Use 7 characters for commit hash
- return substr($hash, 0, 7);
- }
-
- protected function backupDatabase(): void
- {
- try {
- // Use existing backup command
- Process::run('php artisan backup:database');
-
- if (!Process::result()->successful()) {
- throw new \Exception(__('update.backup_failed', ['error' => Process::result()->errorOutput()]));
- }
- } catch (\Exception $e) {
- Log::error('Database backup failed: ' . $e->getMessage());
- throw $e;
- }
- }
-
- protected function pullLatestCode(): array
- {
- try {
- // Get current project root directory
- $basePath = base_path();
-
- // Ensure git configuration is correct
- Process::run(sprintf('git config --global --add safe.directory %s', $basePath));
-
- // Pull latest code
- Process::run('git fetch origin master');
- Process::run('git reset --hard origin/master');
-
- // Update dependencies
- Process::run('composer install --no-dev --optimize-autoloader');
-
- // Update version cache after pulling new code
- $this->updateVersionCache();
-
- return ['success' => true];
- } catch (\Exception $e) {
- return [
- 'success' => false,
- 'message' => __('update.code_update_failed', ['error' => $e->getMessage()])
- ];
- }
- }
-
- protected function runMigrations(): void
- {
- try {
- Process::run('php artisan migrate --force');
- } catch (\Exception $e) {
- Log::error('Migration failed: ' . $e->getMessage());
- throw new \Exception(__('update.migration_failed', ['error' => $e->getMessage()]));
- }
- }
-
- protected function clearCache(): void
- {
- try {
- $commands = [
- 'php artisan config:clear',
- 'php artisan cache:clear',
- 'php artisan view:clear',
- 'php artisan route:clear'
- ];
-
- foreach ($commands as $command) {
- Process::run($command);
- }
- } catch (\Exception $e) {
- Log::error('Cache clearing failed: ' . $e->getMessage());
- throw new \Exception(__('update.cache_clear_failed', ['error' => $e->getMessage()]));
- }
- }
-
- protected function createUpdateFlag(): void
- {
- try {
- // Create update flag file for external script to detect and restart container
- $flagFile = storage_path('update_pending');
- File::put($flagFile, date('Y-m-d H:i:s'));
- } catch (\Exception $e) {
- Log::error('Failed to create update flag: ' . $e->getMessage());
- throw new \Exception(__('update.flag_create_failed', ['error' => $e->getMessage()]));
- }
- }
-
- protected function restartOctane(): void
- {
- try {
- if (!config('octane.server')) {
- return;
- }
-
- // Check Octane running status
- $statusResult = Process::run('php artisan octane:status');
- if (!$statusResult->successful()) {
- Log::info('Octane is not running, skipping restart.');
- return;
- }
-
- $output = $statusResult->output();
- if (str_contains($output, 'Octane server is running')) {
- Log::info('Restarting Octane server after update...');
- // Update version cache before restart
- $this->updateVersionCache();
- Process::run('php artisan octane:stop');
- Log::info('Octane server restarted successfully.');
- } else {
- Log::info('Octane is not running, skipping restart.');
- }
- } catch (\Exception $e) {
- Log::error('Failed to restart Octane server: ' . $e->getMessage());
- // Non-fatal error, don't throw exception
- }
- }
-
- public function getLastCheckTime()
- {
- return Cache::get(self::CACHE_LAST_CHECK, null);
- }
-
- protected function setLastCheckTime(): void
- {
- Cache::put(self::CACHE_LAST_CHECK, now()->timestamp, now()->addDays(30));
- }
-
- public function getCachedUpdateInfo(): array
- {
- return Cache::get(self::CACHE_UPDATE_INFO, [
- 'has_update' => false,
- 'latest_version' => $this->getCurrentCommit(),
- 'current_version' => $this->getCurrentCommit(),
- 'update_logs' => [],
- 'download_url' => '',
- 'published_at' => '',
- 'author' => '',
- ]);
- }
-
- protected function getLocalGitLogs(int $limit = 50): array
- {
- try {
- // 获取本地git log
- $result = Process::run(
- sprintf('git log -%d --pretty=format:"%%H||%%s||%%an||%%ai"', $limit)
- );
-
- if (!$result->successful()) {
- return [];
- }
-
- $logs = [];
- $lines = explode("\n", trim($result->output()));
- foreach ($lines as $line) {
- $parts = explode('||', $line);
- if (count($parts) === 4) {
- $logs[] = [
- 'hash' => $parts[0],
- 'message' => $parts[1],
- 'author' => $parts[2],
- 'date' => $parts[3]
- ];
- }
- }
- return $logs;
- } catch (\Exception $e) {
- Log::error('Failed to get local git logs: ' . $e->getMessage());
- return [];
- }
- }
-}
\ No newline at end of file
diff --git a/Xboard/app/Services/UserService.php b/Xboard/app/Services/UserService.php
deleted file mode 100644
index c068b67..0000000
--- a/Xboard/app/Services/UserService.php
+++ /dev/null
@@ -1,288 +0,0 @@
-calculateNextResetTime($user);
-
- if (!$nextResetTime) {
- return null;
- }
-
- // Calculate the remaining days from now to the next reset time
- $now = time();
- $resetTimestamp = $nextResetTime->timestamp;
-
- if ($resetTimestamp <= $now) {
- return 0; // Reset time has passed or is now
- }
-
- // Calculate the difference in days (rounded up)
- $daysDifference = ceil(($resetTimestamp - $now) / 86400);
-
- return (int) $daysDifference;
- }
-
- public function isAvailable(User $user)
- {
- if (!$user->banned && $user->transfer_enable && ($user->expired_at > time() || $user->expired_at === NULL)) {
- return true;
- }
- return false;
- }
-
- public function getAvailableUsers()
- {
- return User::whereRaw('u + d < transfer_enable')
- ->where(function ($query) {
- $query->where('expired_at', '>=', time())
- ->orWhere('expired_at', NULL);
- })
- ->where('banned', 0)
- ->get();
- }
-
- public function getUnAvailbaleUsers()
- {
- return User::where(function ($query) {
- $query->where('expired_at', '<', time())
- ->orWhere('expired_at', 0);
- })
- ->where(function ($query) {
- $query->where('plan_id', NULL)
- ->orWhere('transfer_enable', 0);
- })
- ->get();
- }
-
- public function getUsersByIds($ids)
- {
- return User::whereIn('id', $ids)->get();
- }
-
- public function getAllUsers()
- {
- return User::all();
- }
-
- public function addBalance(int $userId, int $balance): bool
- {
- $user = User::lockForUpdate()->find($userId);
- if (!$user) {
- return false;
- }
- $user->balance = $user->balance + $balance;
- if ($user->balance < 0) {
- return false;
- }
- if (!$user->save()) {
- return false;
- }
- return true;
- }
-
- public function isNotCompleteOrderByUserId(int $userId): bool
- {
- $order = Order::whereIn('status', [0, 1])
- ->where('user_id', $userId)
- ->first();
- if (!$order) {
- return false;
- }
- return true;
- }
-
- public function trafficFetch(Server $server, string $protocol, array $data)
- {
- $server->rate = $server->getCurrentRate();
- $server = $server->toArray();
-
- list($server, $protocol, $data) = HookManager::filter('traffic.process.before', [$server, $protocol, $data]);
- // Compatible with legacy hook
- list($server, $protocol, $data) = HookManager::filter('traffic.before_process', [$server, $protocol, $data]);
-
- $timestamp = strtotime(date('Y-m-d'));
- collect($data)->chunk(1000)->each(function ($chunk) use ($timestamp, $server, $protocol) {
- TrafficFetchJob::dispatch($server, $chunk->toArray(), $protocol, $timestamp);
- StatUserJob::dispatch($server, $chunk->toArray(), $protocol, 'd');
- StatServerJob::dispatch($server, $chunk->toArray(), $protocol, 'd');
- });
- }
-
- /**
- * 获取用户流量信息(增加重置检查)
- */
- public function getUserTrafficInfo(User $user): array
- {
- // 检查是否需要重置流量
- app(TrafficResetService::class)->checkAndReset($user, TrafficResetLog::SOURCE_USER_ACCESS);
-
- // 重新获取用户数据(可能已被重置)
- $user->refresh();
-
- return [
- 'upload' => $user->u ?? 0,
- 'download' => $user->d ?? 0,
- 'total_used' => $user->getTotalUsedTraffic(),
- 'total_available' => $user->transfer_enable ?? 0,
- 'remaining' => $user->getRemainingTraffic(),
- 'usage_percentage' => $user->getTrafficUsagePercentage(),
- 'next_reset_at' => $user->next_reset_at,
- 'last_reset_at' => $user->last_reset_at,
- 'reset_count' => $user->reset_count,
- ];
- }
-
- /**
- * 创建用户
- */
- public function createUser(array $data): User
- {
- $user = new User();
-
- // 基本信息
- $user->email = $data['email'];
- $user->password = isset($data['password'])
- ? Hash::make($data['password'])
- : Hash::make($data['email']);
- $user->uuid = Helper::guid(true);
- $user->token = Helper::guid();
-
- // 默认设置
- $user->remind_expire = admin_setting('default_remind_expire', 1);
- $user->remind_traffic = admin_setting('default_remind_traffic', 1);
- $user->expired_at = null;
-
- // 可选字段
- $this->setOptionalFields($user, $data);
-
- // 处理计划
- if (isset($data['plan_id'])) {
- $this->setPlanForUser($user, $data['plan_id'], $data['expired_at'] ?? null);
- } else {
- $this->setTryOutPlan(user: $user);
- }
-
- return $user;
- }
-
- /**
- * 设置可选字段
- */
- private function setOptionalFields(User $user, array $data): void
- {
- $optionalFields = [
- 'invite_user_id',
- 'telegram_id',
- 'group_id',
- 'speed_limit',
- 'expired_at',
- 'transfer_enable'
- ];
-
- foreach ($optionalFields as $field) {
- if (array_key_exists($field, $data)) {
- $user->{$field} = $data[$field];
- }
- }
- }
-
- /**
- * 为用户设置计划
- */
- private function setPlanForUser(User $user, int $planId, ?int $expiredAt = null): void
- {
- $plan = Plan::find($planId);
- if (!$plan)
- return;
-
- $user->plan_id = $plan->id;
- $user->group_id = $plan->group_id;
- $user->transfer_enable = $plan->transfer_enable * 1073741824;
- $user->speed_limit = $plan->speed_limit;
-
- if ($expiredAt) {
- $user->expired_at = $expiredAt;
- }
- }
-
- /**
- * 为用户分配一个新套餐或续费现有套餐
- *
- * @param User $user 用户模型
- * @param Plan $plan 套餐模型
- * @param int $validityDays 购买天数
- * @return User 更新后的用户模型
- */
- public function assignPlan(User $user, Plan $plan, int $validityDays): User
- {
- $user->plan_id = $plan->id;
- $user->group_id = $plan->group_id;
- $user->transfer_enable = $plan->transfer_enable * 1073741824;
- $user->speed_limit = $plan->speed_limit;
- $user->device_limit = $plan->device_limit;
-
- if ($validityDays > 0) {
- $user = $this->extendSubscription($user, $validityDays);
- }
-
- $user->save();
- return $user;
- }
-
- /**
- * 延长用户的订阅有效期
- *
- * @param User $user 用户模型
- * @param int $days 延长天数
- * @return User 更新后的用户模型
- */
- public function extendSubscription(User $user, int $days): User
- {
- $currentExpired = $user->expired_at ?? time();
- $user->expired_at = max($currentExpired, time()) + ($days * 86400);
-
- return $user;
- }
-
- /**
- * 设置试用计划
- */
- private function setTryOutPlan(User $user): void
- {
- if (!(int) admin_setting('try_out_plan_id', 0))
- return;
-
- $plan = Plan::find(admin_setting('try_out_plan_id'));
- if (!$plan)
- return;
-
- $user->transfer_enable = $plan->transfer_enable * 1073741824;
- $user->plan_id = $plan->id;
- $user->group_id = $plan->group_id;
- $user->expired_at = time() + (admin_setting('try_out_hour', 1) * 3600);
- $user->speed_limit = $plan->speed_limit;
- }
-}
diff --git a/Xboard/app/Support/AbstractProtocol.php b/Xboard/app/Support/AbstractProtocol.php
deleted file mode 100644
index 78a2c0d..0000000
--- a/Xboard/app/Support/AbstractProtocol.php
+++ /dev/null
@@ -1,262 +0,0 @@
-user = $user;
- $this->servers = $servers;
- $this->clientName = $clientName;
- $this->clientVersion = $clientVersion;
- $this->userAgent = $userAgent;
- $this->protocolRequirements = $this->normalizeProtocolRequirements($this->protocolRequirements);
- $this->servers = HookManager::filter('protocol.servers.filtered', $this->filterServersByVersion());
- }
-
- /**
- * 获取协议标识
- *
- * @return array
- */
- public function getFlags(): array
- {
- return $this->flags;
- }
-
- /**
- * 处理请求
- *
- * @return mixed
- */
- abstract public function handle();
-
- /**
- * 根据客户端版本过滤不兼容的服务器
- *
- * @return array
- */
- protected function filterServersByVersion()
- {
- $this->filterByAllowedProtocols();
- $hasGlobalConfig = isset($this->protocolRequirements['*']);
- $hasClientConfig = isset($this->protocolRequirements[$this->clientName]);
-
- if ((blank($this->clientName) || blank($this->clientVersion)) && !$hasGlobalConfig) {
- return $this->servers;
- }
-
- if (!$hasGlobalConfig && !$hasClientConfig) {
- return $this->servers;
- }
-
- return collect($this->servers)
- ->filter(fn($server) => $this->isCompatible($server))
- ->values()
- ->all();
- }
-
- /**
- * 检查服务器是否与当前客户端兼容
- *
- * @param array $server 服务器信息
- * @return bool
- */
- protected function isCompatible($server)
- {
- $serverType = $server['type'] ?? null;
- if (isset($this->protocolRequirements['*'][$serverType])) {
- $globalRequirements = $this->protocolRequirements['*'][$serverType];
- if (!$this->checkRequirements($globalRequirements, $server)) {
- return false;
- }
- }
-
- if (!isset($this->protocolRequirements[$this->clientName][$serverType])) {
- return true;
- }
-
- $requirements = $this->protocolRequirements[$this->clientName][$serverType];
- return $this->checkRequirements($requirements, $server);
- }
-
- /**
- * 检查版本要求
- *
- * @param array $requirements 要求配置
- * @param array $server 服务器信息
- * @return bool
- */
- private function checkRequirements(array $requirements, array $server): bool
- {
- foreach ($requirements as $field => $filterRule) {
- if (in_array($field, ['base_version', 'incompatible'])) {
- continue;
- }
-
- $actualValue = data_get($server, $field);
-
- if (is_array($filterRule) && isset($filterRule['whitelist'])) {
- $allowedValues = $filterRule['whitelist'];
- $strict = $filterRule['strict'] ?? false;
- if ($strict) {
- if ($actualValue === null) {
- return false;
- }
- if (!is_string($actualValue) && !is_int($actualValue)) {
- return false;
- }
- if (!isset($allowedValues[$actualValue])) {
- return false;
- }
- $requiredVersion = $allowedValues[$actualValue];
- if ($requiredVersion !== '0.0.0' && version_compare($this->clientVersion, $requiredVersion, '<')) {
- return false;
- }
- continue;
- }
- } else {
- $allowedValues = $filterRule;
- $strict = false;
- }
-
- if ($actualValue === null) {
- continue;
- }
- if (!is_string($actualValue) && !is_int($actualValue)) {
- continue;
- }
- if (!isset($allowedValues[$actualValue])) {
- continue;
- }
- $requiredVersion = $allowedValues[$actualValue];
- if ($requiredVersion !== '0.0.0' && version_compare($this->clientVersion, $requiredVersion, '<')) {
- return false;
- }
- }
-
- return true;
- }
-
- /**
- * 检查当前客户端是否支持特定功能
- *
- * @param string $clientName 客户端名称
- * @param string $minVersion 最低版本要求
- * @param array $additionalConditions 额外条件检查
- * @return bool
- */
- protected function supportsFeature(string $clientName, string $minVersion, array $additionalConditions = []): bool
- {
- // 检查客户端名称
- if ($this->clientName !== $clientName) {
- return false;
- }
-
- // 检查版本号
- if (empty($this->clientVersion) || version_compare($this->clientVersion, $minVersion, '<')) {
- return false;
- }
-
- // 检查额外条件
- foreach ($additionalConditions as $condition) {
- if (!$condition) {
- return false;
- }
- }
-
- return true;
- }
-
- /**
- * 根据白名单过滤服务器
- *
- * @return void
- */
- protected function filterByAllowedProtocols(): void
- {
- if (!empty($this->allowedProtocols)) {
- $this->servers = collect($this->servers)
- ->filter(fn($server) => in_array($server['type'], $this->allowedProtocols))
- ->values()
- ->all();
- }
- }
-
- /**
- * 将平铺的协议需求转换为树形结构
- *
- * @param array $flat 平铺的协议需求
- * @return array 树形结构的协议需求
- */
- protected function normalizeProtocolRequirements(array $flat): array
- {
- $result = [];
- foreach ($flat as $key => $value) {
- if (!str_contains($key, '.')) {
- $result[$key] = $value;
- continue;
- }
- $segments = explode('.', $key, 3);
- if (count($segments) < 3) {
- $result[$segments[0]][$segments[1] ?? '*'][''] = $value;
- continue;
- }
- [$client, $type, $field] = $segments;
- $result[$client][$type][$field] = $value;
- }
- return $result;
- }
-}
\ No newline at end of file
diff --git a/Xboard/app/Support/ProtocolManager.php b/Xboard/app/Support/ProtocolManager.php
deleted file mode 100644
index e1fb4cd..0000000
--- a/Xboard/app/Support/ProtocolManager.php
+++ /dev/null
@@ -1,162 +0,0 @@
-container = $container;
- }
-
- /**
- * 发现并注册所有协议类
- *
- * @return self
- */
- public function registerAllProtocols()
- {
- if (empty($this->protocolClasses)) {
- $files = glob(app_path('Protocols') . '/*.php');
-
- foreach ($files as $file) {
- $className = 'App\\Protocols\\' . basename($file, '.php');
-
- if (class_exists($className) && is_subclass_of($className, AbstractProtocol::class)) {
- $this->protocolClasses[] = $className;
- }
- }
- }
-
- return $this;
- }
-
- /**
- * 获取所有注册的协议类
- *
- * @return array
- */
- public function getProtocolClasses()
- {
- if (empty($this->protocolClasses)) {
- $this->registerAllProtocols();
- }
-
- return $this->protocolClasses;
- }
-
- /**
- * 获取所有协议的标识
- *
- * @return array
- */
- public function getAllFlags()
- {
- return collect($this->getProtocolClasses())
- ->map(function ($class) {
- try {
- $reflection = new \ReflectionClass($class);
- if (!$reflection->isInstantiable()) {
- return [];
- }
- // 'flags' is a public property with a default value in AbstractProtocol
- $instanceForFlags = $reflection->newInstanceWithoutConstructor();
- return $instanceForFlags->flags;
- } catch (\ReflectionException $e) {
- // Log or handle error if a class is problematic
- report($e);
- return [];
- }
- })
- ->flatten()
- ->unique()
- ->values()
- ->all();
- }
-
- /**
- * 根据标识匹配合适的协议处理器类名
- *
- * @param string $flag 请求标识
- * @return string|null 协议类名或null
- */
- public function matchProtocolClassName(string $flag): ?string
- {
- // 按照相反顺序,使最新定义的协议有更高优先级
- foreach (array_reverse($this->getProtocolClasses()) as $protocolClassString) {
- try {
- $reflection = new \ReflectionClass($protocolClassString);
-
- if (!$reflection->isInstantiable() || !$reflection->isSubclassOf(AbstractProtocol::class)) {
- continue;
- }
-
- // 'flags' is a public property in AbstractProtocol
- $instanceForFlags = $reflection->newInstanceWithoutConstructor();
- $flags = $instanceForFlags->flags;
-
- if (collect($flags)->contains(fn($f) => stripos($flag, (string) $f) !== false)) {
- return $protocolClassString; // 返回类名字符串
- }
- } catch (\ReflectionException $e) {
- report($e); // Consider logging this error
- continue;
- }
- }
- return null;
- }
-
- /**
- * 根据标识匹配合适的协议处理器实例 (原有逻辑,如果还需要的话)
- *
- * @param string $flag 请求标识
- * @param array $user 用户信息
- * @param array $servers 服务器列表
- * @param array $clientInfo 客户端信息
- * @return AbstractProtocol|null
- */
- public function matchProtocol($flag, $user, $servers, $clientInfo = [])
- {
- $protocolClassName = $this->matchProtocolClassName($flag);
- if ($protocolClassName) {
- return $this->makeProtocolInstance($protocolClassName, [
- 'user' => $user,
- 'servers' => $servers,
- 'clientName' => $clientInfo['name'] ?? null,
- 'clientVersion' => $clientInfo['version'] ?? null
- ]);
- }
- return null;
- }
-
- /**
- * 创建协议实例的通用方法,兼容不同版本的Laravel容器
- *
- * @param string $class 类名
- * @param array $parameters 构造参数
- * @return object 实例
- */
- protected function makeProtocolInstance($class, array $parameters)
- {
- // Laravel's make method can accept an array of parameters as its second argument.
- // These will be used when resolving the class's dependencies.
- return $this->container->make($class, $parameters);
- }
-}
\ No newline at end of file
diff --git a/Xboard/app/Support/Setting.php b/Xboard/app/Support/Setting.php
deleted file mode 100644
index 76adf8f..0000000
--- a/Xboard/app/Support/Setting.php
+++ /dev/null
@@ -1,140 +0,0 @@
-cache = Cache::store('redis');
- }
-
- /**
- * 获取配置.
- */
- public function get(string $key, mixed $default = null): mixed
- {
- $this->load();
- return Arr::get($this->loadedSettings, strtolower($key), $default);
- }
-
- /**
- * 设置配置信息.
- */
- public function set(string $key, mixed $value = null): bool
- {
- SettingModel::createOrUpdate(strtolower($key), $value);
- $this->flush();
- return true;
- }
-
- /**
- * 保存配置到数据库.
- */
- public function save(array $settings): bool
- {
- foreach ($settings as $key => $value) {
- SettingModel::createOrUpdate(strtolower($key), $value);
- }
- $this->flush();
- return true;
- }
-
- /**
- * 删除配置信息
- */
- public function remove(string $key): bool
- {
- SettingModel::where('name', $key)->delete();
- $this->flush();
- return true;
- }
-
- /**
- * 更新单个设置项
- */
- public function update(string $key, $value): bool
- {
- return $this->set($key, $value);
- }
-
- /**
- * 批量获取配置项
- */
- public function getBatch(array $keys): array
- {
- $this->load();
- $result = [];
-
- foreach ($keys as $index => $item) {
- $isNumericIndex = is_numeric($index);
- $key = strtolower($isNumericIndex ? $item : $index);
- $default = $isNumericIndex ? config('v2board.' . $item) : (config('v2board.' . $key) ?? $item);
-
- $result[$item] = Arr::get($this->loadedSettings, $key, $default);
- }
-
- return $result;
- }
-
- /**
- * 将所有设置转换为数组
- */
- public function toArray(): array
- {
- $this->load();
- return $this->loadedSettings;
- }
-
- /**
- * 加载配置到请求内缓存
- */
- private function load(): void
- {
- if ($this->loadedSettings !== null) {
- return;
- }
-
- try {
- $settings = $this->cache->rememberForever(self::CACHE_KEY, function (): array {
- return array_change_key_case(
- SettingModel::pluck('value', 'name')->toArray(),
- CASE_LOWER
- );
- });
-
- // 处理JSON格式的值
- foreach ($settings as $key => $value) {
- if (is_string($value)) {
- $decoded = json_decode($value, true);
- if (json_last_error() === JSON_ERROR_NONE) {
- $settings[$key] = $decoded;
- }
- }
- }
-
- $this->loadedSettings = $settings;
- } catch (\Throwable) {
- $this->loadedSettings = [];
- }
- }
-
- /**
- * 清空缓存
- */
- private function flush(): void
- {
- $this->cache->forget(self::CACHE_KEY);
- $this->loadedSettings = null;
- }
-}
diff --git a/Xboard/app/Traits/HasPluginConfig.php b/Xboard/app/Traits/HasPluginConfig.php
deleted file mode 100644
index a44e2fc..0000000
--- a/Xboard/app/Traits/HasPluginConfig.php
+++ /dev/null
@@ -1,144 +0,0 @@
-getPluginConfig();
-
- if ($key) {
- return $config[$key] ?? $default;
- }
-
- return $config;
- }
-
- /**
- * 获取完整的插件配置
- */
- protected function getPluginConfig(): array
- {
- if ($this->pluginConfig === null) {
- $pluginCode = $this->getPluginCode();
-
- \Log::channel('daily')->info('Telegram Login: 获取插件配置', [
- 'plugin_code' => $pluginCode
- ]);
-
- $this->pluginConfig = Cache::remember(
- "plugin_config_{$pluginCode}",
- 3600,
- function () use ($pluginCode) {
- $plugin = Plugin::where('code', $pluginCode)
- ->where('is_enabled', true)
- ->first();
-
- if (!$plugin || !$plugin->config) {
- return [];
- }
-
- return json_decode($plugin->config, true) ?? [];
- }
- );
- }
-
- return $this->pluginConfig;
- }
-
- /**
- * 获取插件代码
- */
- public function getPluginCode(): string
- {
- if ($this->pluginCode === null) {
- $this->pluginCode = $this->autoDetectPluginCode();
- }
-
- return $this->pluginCode;
- }
-
- /**
- * 设置插件代码(如果自动检测不准确可以手动设置)
- */
- public function setPluginCode(string $pluginCode): void
- {
- $this->pluginCode = $pluginCode;
- $this->pluginConfig = null; // 重置配置缓存
- $this->pluginEnabled = null;
- }
-
- /**
- * 自动检测插件代码
- */
- protected function autoDetectPluginCode(): string
- {
- $reflection = new \ReflectionClass($this);
- $namespace = $reflection->getNamespaceName();
-
- // 从命名空间提取插件代码
- // 例如: Plugin\TelegramLogin\Controllers => telegram_login
- if (preg_match('/^Plugin\\\\(.+?)\\\\/', $namespace, $matches)) {
- return $this->convertToKebabCase($matches[1]);
- }
-
- throw new \RuntimeException('Unable to detect plugin code from namespace: ' . $namespace);
- }
-
- /**
- * 将 StudlyCase 转换为 kebab-case
- */
- protected function convertToKebabCase(string $string): string
- {
- return strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $string));
- }
-
- /**
- * 检查插件是否启用
- */
- public function isPluginEnabled(): bool
- {
- if ($this->pluginEnabled !== null) {
- return $this->pluginEnabled;
- }
-
- $pluginCode = $this->getPluginCode();
- $isEnabled = Plugin::where('code', $pluginCode)->value('is_enabled');
- $this->pluginEnabled = (bool) $isEnabled;
-
- return $this->pluginEnabled;
- }
-
- /**
- * 清除插件配置缓存
- */
- public function clearConfigCache(): void
- {
- $pluginCode = $this->getPluginCode();
- Cache::forget("plugin_config_{$pluginCode}");
- $this->pluginConfig = null;
- $this->pluginEnabled = null;
- }
-}
\ No newline at end of file
diff --git a/Xboard/app/Traits/QueryOperators.php b/Xboard/app/Traits/QueryOperators.php
deleted file mode 100644
index 706d5c5..0000000
--- a/Xboard/app/Traits/QueryOperators.php
+++ /dev/null
@@ -1,68 +0,0 @@
- '=',
- 'gt' => '>',
- 'gte' => '>=',
- 'lt' => '<',
- 'lte' => '<=',
- 'like' => 'like',
- 'notlike' => 'not like',
- 'null' => 'null',
- 'notnull' => 'notnull',
- default => 'like'
- };
- }
-
- /**
- * 获取查询值格式化
- *
- * @param string $operator
- * @param mixed $value
- * @return mixed
- */
- protected function formatQueryValue(string $operator, mixed $value): mixed
- {
- return match (strtolower($operator)) {
- 'like', 'notlike' => "%{$value}%",
- 'null', 'notnull' => null,
- default => $value
- };
- }
-
- /**
- * 应用查询条件
- *
- * @param \Illuminate\Database\Eloquent\Builder $query
- * @param string $field
- * @param string $operator
- * @param mixed $value
- * @return void
- */
- protected function applyQueryCondition($query, array|Expression|string $field, string $operator, mixed $value): void
- {
- $queryOperator = $this->getQueryOperator($operator);
-
- if ($queryOperator === 'null') {
- $query->whereNull($field);
- } elseif ($queryOperator === 'notnull') {
- $query->whereNotNull($field);
- } else {
- $query->where($field, $queryOperator, $this->formatQueryValue($operator, $value));
- }
- }
-}
\ No newline at end of file
diff --git a/Xboard/app/Utils/CacheKey.php b/Xboard/app/Utils/CacheKey.php
deleted file mode 100644
index 6795c6c..0000000
--- a/Xboard/app/Utils/CacheKey.php
+++ /dev/null
@@ -1,69 +0,0 @@
- '邮箱验证码',
- 'LAST_SEND_EMAIL_VERIFY_TIMESTAMP' => '最后一次发送邮箱验证码时间',
- 'TEMP_TOKEN' => '临时令牌',
- 'LAST_SEND_EMAIL_REMIND_TRAFFIC' => '最后发送流量邮件提醒',
- 'SCHEDULE_LAST_CHECK_AT' => '计划任务最后检查时间',
- 'REGISTER_IP_RATE_LIMIT' => '注册频率限制',
- 'LAST_SEND_LOGIN_WITH_MAIL_LINK_TIMESTAMP' => '最后一次发送登入链接时间',
- 'PASSWORD_ERROR_LIMIT' => '密码错误次数限制',
- 'USER_SESSIONS' => '用户session',
- 'FORGET_REQUEST_LIMIT' => '找回密码次数限制'
- ];
-
- // 允许的缓存键模式(支持通配符)
- const ALLOWED_PATTERNS = [
- 'SERVER_*_ONLINE_USER', // 节点在线用户
- 'MULTI_SERVER_*_ONLINE_USER', // 多服务器在线用户
- 'SERVER_*_LAST_CHECK_AT', // 节点最后检查时间
- 'SERVER_*_LAST_PUSH_AT', // 节点最后推送时间
- 'SERVER_*_LOAD_STATUS', // 节点负载状态
- 'SERVER_*_LAST_LOAD_AT', // 节点最后负载提交时间
- 'SERVER_*_METRICS', // 节点指标数据
- 'USER_ONLINE_CONN_*_*', // 用户在线连接数 (特定节点类型_ID)
- ];
-
- /**
- * 生成缓存键
- */
- public static function get(string $key, mixed $uniqueValue = null): string
- {
- // 检查是否为核心键
- if (array_key_exists($key, self::CORE_KEYS)) {
- return $uniqueValue ? $key . '_' . $uniqueValue : $key;
- }
-
- // 检查是否匹配允许的模式
- if (self::matchesPattern($key)) {
- return $uniqueValue ? $key . '_' . $uniqueValue : $key;
- }
-
- // 开发环境下记录警告,生产环境允许通过
- if (app()->environment('local', 'development')) {
- logger()->warning("Unknown cache key used: {$key}");
- }
-
- return $uniqueValue ? $key . '_' . $uniqueValue : $key;
- }
-
- /**
- * 检查键名是否匹配允许的模式
- */
- private static function matchesPattern(string $key): bool
- {
- foreach (self::ALLOWED_PATTERNS as $pattern) {
- $regex = '/^' . str_replace('*', '[A-Za-z0-9_]+', $pattern) . '$/';
- if (preg_match($regex, $key)) {
- return true;
- }
- }
- return false;
- }
-}
diff --git a/Xboard/app/Utils/Dict.php b/Xboard/app/Utils/Dict.php
deleted file mode 100644
index e0e6fe6..0000000
--- a/Xboard/app/Utils/Dict.php
+++ /dev/null
@@ -1,23 +0,0 @@
-", "~", "+", "=", ",", "."
- ));
- }
-
- $charsLen = count($chars) - 1;
- shuffle($chars);
- $str = '';
- for ($i = 0; $i < $len; $i++) {
- $str .= $chars[mt_rand(0, $charsLen)];
- }
- return $str;
- }
-
- public static function wrapIPv6($addr) {
- if (filter_var($addr, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
- return "[$addr]";
- } else {
- return $addr;
- }
- }
-
- public static function multiPasswordVerify($algo, $salt, $password, $hash)
- {
- switch($algo) {
- case 'md5': return md5($password) === $hash;
- case 'sha256': return hash('sha256', $password) === $hash;
- case 'md5salt': return md5($password . $salt) === $hash;
- case 'sha256salt': return hash('sha256', $password . $salt) === $hash;
- default: return password_verify($password, $hash);
- }
- }
-
- public static function emailSuffixVerify($email, $suffixs)
- {
- $suffix = preg_split('/@/', $email)[1];
- if (!$suffix) return false;
- if (!is_array($suffixs)) {
- $suffixs = preg_split('/,/', $suffixs);
- }
- if (!in_array($suffix, $suffixs)) return false;
- return true;
- }
-
- public static function trafficConvert(float $byte)
- {
- $kb = 1024;
- $mb = 1048576;
- $gb = 1073741824;
- if ($byte > $gb) {
- return round($byte / $gb, 2) . ' GB';
- } else if ($byte > $mb) {
- return round($byte / $mb, 2) . ' MB';
- } else if ($byte > $kb) {
- return round($byte / $kb, 2) . ' KB';
- } else if ($byte < 0) {
- return 0;
- } else {
- return round($byte, 2) . ' B';
- }
- }
-
- public static function getSubscribeUrl(string $token, $subscribeUrl = null)
- {
- $path = route('client.subscribe', ['token' => $token], false);
-
- if ($subscribeUrl) {
- $finalUrl = rtrim($subscribeUrl, '/') . $path;
- return HookManager::filter('subscribe.url', $finalUrl);
- }
-
- $urlString = (string)admin_setting('subscribe_url', '');
- $subscribeUrlList = $urlString ? explode(',', $urlString) : [];
-
- if (empty($subscribeUrlList)) {
- return HookManager::filter('subscribe.url', url($path));
- }
-
- $selectedUrl = self::replaceByPattern(Arr::random($subscribeUrlList));
- $finalUrl = rtrim($selectedUrl, '/') . $path;
-
- return HookManager::filter('subscribe.url', $finalUrl);
- }
-
- public static function randomPort($range): int {
- $portRange = explode('-', (string) $range, 2);
- $min = (int) ($portRange[0] ?? 0);
- $max = (int) ($portRange[1] ?? $portRange[0] ?? 0);
- if ($min > $max) {
- [$min, $max] = [$max, $min];
- }
- return random_int($min, $max);
- }
-
- public static function base64EncodeUrlSafe($data)
- {
- $encoded = base64_encode($data);
- return str_replace(['+', '/', '='], ['-', '_', ''], $encoded);
- }
-
- /**
- * 根据规则替换域名中对应的字符串
- *
- * @param string $input 用户输入的字符串
- * @return string 替换后的字符串
- */
- public static function replaceByPattern($input)
- {
- $patterns = [
- '/\[(\d+)-(\d+)\]/' => function ($matches) {
- $min = intval($matches[1]);
- $max = intval($matches[2]);
- if ($min > $max) {
- list($min, $max) = [$max, $min];
- }
- $randomNumber = rand($min, $max);
- return $randomNumber;
- },
- '/\[uuid\]/' => function () {
- return self::guid(true);
- }
- ];
- foreach ($patterns as $pattern => $callback) {
- $input = preg_replace_callback($pattern, $callback, $input);
- }
- return $input;
- }
-
- public static function getIpByDomainName($domain) {
- return gethostbynamel($domain) ?: [];
- }
-
- public static function getTlsFingerprint($utls = null)
- {
-
- if (is_array($utls) || is_object($utls)) {
- if (!data_get($utls, 'enabled')) {
- return null;
- }
- $fingerprint = data_get($utls, 'fingerprint', 'chrome');
- if ($fingerprint !== 'random') {
- return $fingerprint;
- }
- }
-
- $fingerprints = ['chrome', 'firefox', 'safari', 'ios', 'edge', 'qq'];
- return Arr::random($fingerprints);
- }
-
- public static function encodeURIComponent($str) {
- $revert = array('%21'=>'!', '%2A'=>'*', '%27'=>"'", '%28'=>'(', '%29'=>')');
- return strtr(rawurlencode($str), $revert);
- }
-
- public static function getEmailSuffix(): array|bool
- {
- $suffix = admin_setting('email_whitelist_suffix', Dict::EMAIL_WHITELIST_SUFFIX_DEFAULT);
- if (!is_array($suffix)) {
- return preg_split('/,/', $suffix);
- }
- return $suffix;
- }
-
- /**
- * convert the transfer_enable to GB
- * @param float $transfer_enable
- * @return float
- */
- public static function transferToGB(float $transfer_enable): float
- {
- return $transfer_enable / 1073741824;
- }
-
- /**
- * 转义 Telegram Markdown 特殊字符
- * @param string $text
- * @return string
- */
- public static function escapeMarkdown(string $text): string
- {
- return str_replace(['_', '*', '`', '['], ['\_', '\*', '\`', '\['], $text);
- }
-}
diff --git a/Xboard/app/WebSocket/NodeEventHandlers.php b/Xboard/app/WebSocket/NodeEventHandlers.php
deleted file mode 100644
index 75521f8..0000000
--- a/Xboard/app/WebSocket/NodeEventHandlers.php
+++ /dev/null
@@ -1,144 +0,0 @@
-type);
- Cache::put(\App\Utils\CacheKey::get('SERVER_' . $nodeType . '_LAST_CHECK_AT', $nodeId), time(), 3600);
- ServerService::updateMetrics($node, $data);
-
- Log::debug("[WS] Node#{$nodeId} status updated");
- }
-
- /**
- * Handle device report from node
- *
- * 数据格式: {"event": "report.devices", "data": {userId: [ip1, ip2, ...], ...}}
- */
- public static function handleDeviceReport(TcpConnection $conn, int $nodeId, array $data): void
- {
- $service = app(DeviceStateService::class);
-
- // Get old data
- $oldDevices = $service->getNodeDevices($nodeId);
-
- // Calculate diff
- $removedUsers = array_diff_key($oldDevices, $data);
- $newDevices = [];
-
- foreach ($data as $userId => $ips) {
- if (is_numeric($userId) && is_array($ips)) {
- $newDevices[(int) $userId] = $ips;
- }
- }
-
- // Handle removed users
- foreach ($removedUsers as $userId => $ips) {
- $service->removeNodeDevices($nodeId, $userId);
- $service->notifyUpdate($userId);
- }
-
- // Handle new/updated users
- foreach ($newDevices as $userId => $ips) {
- $service->setDevices($userId, $nodeId, $ips);
- }
-
- // Mark for push
- Redis::sadd('device:push_pending_nodes', $nodeId);
-
- Log::debug("[WS] Node#{$nodeId} synced " . count($newDevices) . " users, removed " . count($removedUsers));
- }
-
- /**
- * Handle device state request from node
- */
- public static function handleDeviceRequest(TcpConnection $conn, int $nodeId, array $data = []): void
- {
- $node = Server::find($nodeId);
- if (!$node) return;
-
- $users = ServerService::getAvailableUsers($node);
- $userIds = $users->pluck('id')->toArray();
-
- $service = app(DeviceStateService::class);
- $devices = $service->getUsersDevices($userIds);
-
- $conn->send(json_encode([
- 'event' => 'sync.devices',
- 'data' => ['users' => $devices],
- ]));
-
- Log::debug("[WS] Node#{$nodeId} requested devices, sent " . count($devices) . " users");
- }
-
- /**
- * Push device state to node
- */
- public static function pushDeviceStateToNode(int $nodeId, DeviceStateService $service): void
- {
- $node = Server::find($nodeId);
- if (!$node) return;
-
- $users = ServerService::getAvailableUsers($node);
- $userIds = $users->pluck('id')->toArray();
- $devices = $service->getUsersDevices($userIds);
-
- NodeRegistry::send($nodeId, 'sync.devices', [
- 'users' => $devices
- ]);
-
- Log::debug("[WS] Pushed device state to node#{$nodeId}: " . count($devices) . " users");
- }
-
- /**
- * Push full config + users to newly connected node
- */
- public static function pushFullSync(TcpConnection $conn, Server $node): void
- {
- $nodeId = $conn->nodeId;
-
- // Push config
- $config = ServerService::buildNodeConfig($node);
- $conn->send(json_encode([
- 'event' => 'sync.config',
- 'data' => ['config' => $config]
- ]));
-
- // Push users
- $users = ServerService::getAvailableUsers($node)->toArray();
- $conn->send(json_encode([
- 'event' => 'sync.users',
- 'data' => ['users' => $users]
- ]));
-
- Log::info("[WS] Full sync pushed to node#{$nodeId}", [
- 'users' => count($users),
- ]);
- }
-}
diff --git a/Xboard/app/WebSocket/NodeWorker.php b/Xboard/app/WebSocket/NodeWorker.php
deleted file mode 100644
index d45dd22..0000000
--- a/Xboard/app/WebSocket/NodeWorker.php
+++ /dev/null
@@ -1,249 +0,0 @@
- [NodeEventHandlers::class, 'handlePong'],
- 'node.status' => [NodeEventHandlers::class, 'handleNodeStatus'],
- 'report.devices' => [NodeEventHandlers::class, 'handleDeviceReport'],
- 'request.devices' => [NodeEventHandlers::class, 'handleDeviceRequest'],
- ];
-
- public function __construct(string $host, int $port)
- {
- $this->worker = new Worker("websocket://{$host}:{$port}");
- $this->worker->count = 1;
- $this->worker->name = 'xboard-ws-server';
- }
-
- public function run(): void
- {
- $this->setupLogging();
- $this->setupCallbacks();
- Worker::runAll();
- }
-
- private function setupLogging(): void
- {
- $logPath = storage_path('logs');
- if (!is_dir($logPath)) {
- mkdir($logPath, 0777, true);
- }
- Worker::$logFile = $logPath . '/xboard-ws-server.log';
- Worker::$pidFile = $logPath . '/xboard-ws-server.pid';
- }
-
- private function setupCallbacks(): void
- {
- $this->worker->onWorkerStart = [$this, 'onWorkerStart'];
- $this->worker->onConnect = [$this, 'onConnect'];
- $this->worker->onWebSocketConnect = [$this, 'onWebSocketConnect'];
- $this->worker->onMessage = [$this, 'onMessage'];
- $this->worker->onClose = [$this, 'onClose'];
- }
-
- public function onWorkerStart(Worker $worker): void
- {
- Log::info("[WS] Worker started, pid={$worker->id}");
- $this->subscribeRedis();
- $this->setupTimers();
- }
-
- private function setupTimers(): void
- {
- // Ping timer
- Timer::add(self::PING_INTERVAL, function () {
- foreach (NodeRegistry::getConnectedNodeIds() as $nodeId) {
- $conn = NodeRegistry::get($nodeId);
- if ($conn) {
- $conn->send(json_encode(['event' => 'ping']));
- }
- }
- });
-
- // Device state push timer
- Timer::add(10, function () {
- $pendingNodeIds = Redis::spop('device:push_pending_nodes', 100);
- if (empty($pendingNodeIds)) {
- return;
- }
-
- $service = app(DeviceStateService::class);
- foreach ($pendingNodeIds as $nodeId) {
- $nodeId = (int) $nodeId;
- if (NodeRegistry::get($nodeId) !== null) {
- NodeEventHandlers::pushDeviceStateToNode($nodeId, $service);
- }
- }
- });
- }
-
- public function onConnect(TcpConnection $conn): void
- {
- $conn->authTimer = Timer::add(self::AUTH_TIMEOUT, function () use ($conn) {
- if (empty($conn->nodeId)) {
- $conn->close(json_encode([
- 'event' => 'error',
- 'data' => ['message' => 'auth timeout'],
- ]));
- }
- }, [], false);
- }
-
- public function onWebSocketConnect(TcpConnection $conn, $httpMessage): void
- {
- $queryString = '';
- if (is_string($httpMessage)) {
- $queryString = parse_url($httpMessage, PHP_URL_QUERY) ?? '';
- } elseif ($httpMessage instanceof \Workerman\Protocols\Http\Request) {
- $queryString = $httpMessage->queryString();
- }
-
- parse_str($queryString, $params);
- $token = $params['token'] ?? '';
- $nodeId = (int) ($params['node_id'] ?? 0);
-
- // Authenticate
- $serverToken = admin_setting('server_token', '');
- if ($token === '' || $serverToken === '' || !hash_equals($serverToken, $token)) {
- $conn->close(json_encode([
- 'event' => 'error',
- 'data' => ['message' => 'invalid token'],
- ]));
- return;
- }
-
- $node = ServerService::getServer($nodeId, null);
- if (!$node) {
- $conn->close(json_encode([
- 'event' => 'error',
- 'data' => ['message' => 'node not found'],
- ]));
- return;
- }
-
- // Auth passed
- if (isset($conn->authTimer)) {
- Timer::del($conn->authTimer);
- }
-
- $conn->nodeId = $nodeId;
- NodeRegistry::add($nodeId, $conn);
- Cache::put("node_ws_alive:{$nodeId}", true, 86400);
-
- // Clear old device data
- app(DeviceStateService::class)->clearAllNodeDevices($nodeId);
-
- Log::debug("[WS] Node#{$nodeId} connected", [
- 'remote' => $conn->getRemoteIp(),
- 'total' => NodeRegistry::count(),
- ]);
-
- // Send auth success
- $conn->send(json_encode([
- 'event' => 'auth.success',
- 'data' => ['node_id' => $nodeId],
- ]));
-
- // Push full sync
- NodeEventHandlers::pushFullSync($conn, $node);
- }
-
- public function onMessage(TcpConnection $conn, $data): void
- {
- $msg = json_decode($data, true);
- if (!is_array($msg)) {
- return;
- }
-
- $event = $msg['event'] ?? '';
- $nodeId = $conn->nodeId ?? null;
-
- if (isset($this->handlers[$event]) && $nodeId) {
- $handler = $this->handlers[$event];
- $handler($conn, $nodeId, $msg['data'] ?? []);
- }
- }
-
- public function onClose(TcpConnection $conn): void
- {
- if (!empty($conn->nodeId)) {
- $nodeId = $conn->nodeId;
- NodeRegistry::remove($nodeId);
- Cache::forget("node_ws_alive:{$nodeId}");
-
- $service = app(DeviceStateService::class);
- $affectedUserIds = $service->clearAllNodeDevices($nodeId);
- foreach ($affectedUserIds as $userId) {
- $service->notifyUpdate($userId);
- }
-
- Log::debug("[WS] Node#{$nodeId} disconnected", [
- 'total' => NodeRegistry::count(),
- 'affected_users' => count($affectedUserIds),
- ]);
- }
- }
-
- private function subscribeRedis(): void
- {
- $host = config('database.redis.default.host', '127.0.0.1');
- $port = config('database.redis.default.port', 6379);
-
- if (str_starts_with($host, '/')) {
- $redisUri = "unix://{$host}";
- } else {
- $redisUri = "redis://{$host}:{$port}";
- }
-
- $redis = new \Workerman\Redis\Client($redisUri);
-
- $password = config('database.redis.default.password');
- if ($password) {
- $redis->auth($password);
- }
-
- $prefix = config('database.redis.options.prefix', '');
- $channel = $prefix . 'node:push';
-
- $redis->subscribe([$channel], function ($chan, $message) {
- $payload = json_decode($message, true);
- if (!is_array($payload)) {
- return;
- }
-
- $nodeId = $payload['node_id'] ?? null;
- $event = $payload['event'] ?? '';
- $data = $payload['data'] ?? [];
-
- if (!$nodeId || !$event) {
- return;
- }
-
- $sent = NodeRegistry::send((int) $nodeId, $event, $data);
- if ($sent) {
- Log::debug("[WS] Pushed {$event} to node#{$nodeId}");
- }
- });
-
- Log::info("[WS] Subscribed to Redis channel: {$channel}");
- }
-}
diff --git a/Xboard/artisan b/Xboard/artisan
deleted file mode 100644
index 5c23e2e..0000000
--- a/Xboard/artisan
+++ /dev/null
@@ -1,53 +0,0 @@
-#!/usr/bin/env php
-make(Illuminate\Contracts\Console\Kernel::class);
-
-$status = $kernel->handle(
- $input = new Symfony\Component\Console\Input\ArgvInput,
- new Symfony\Component\Console\Output\ConsoleOutput
-);
-
-/*
-|--------------------------------------------------------------------------
-| Shutdown The Application
-|--------------------------------------------------------------------------
-|
-| Once Artisan has finished running, we will fire off the shutdown events
-| so that any final work may be done by the application before we shut
-| down the process. This is the last thing to happen to the request.
-|
-*/
-
-$kernel->terminate($input, $status);
-
-exit($status);
diff --git a/Xboard/bootstrap/app.php b/Xboard/bootstrap/app.php
deleted file mode 100644
index 037e17d..0000000
--- a/Xboard/bootstrap/app.php
+++ /dev/null
@@ -1,55 +0,0 @@
-singleton(
- Illuminate\Contracts\Http\Kernel::class,
- App\Http\Kernel::class
-);
-
-$app->singleton(
- Illuminate\Contracts\Console\Kernel::class,
- App\Console\Kernel::class
-);
-
-$app->singleton(
- Illuminate\Contracts\Debug\ExceptionHandler::class,
- App\Exceptions\Handler::class
-);
-
-/*
-|--------------------------------------------------------------------------
-| Return The Application
-|--------------------------------------------------------------------------
-|
-| This script returns the application instance. The instance is given to
-| the calling script so we can separate the building of the instances
-| from the actual running of the application and sending responses.
-|
-*/
-
-return $app;
diff --git a/Xboard/bootstrap/cache/.gitignore b/Xboard/bootstrap/cache/.gitignore
deleted file mode 100644
index d6b7ef3..0000000
--- a/Xboard/bootstrap/cache/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-*
-!.gitignore
diff --git a/Xboard/compose.sample.yaml b/Xboard/compose.sample.yaml
deleted file mode 100644
index b955027..0000000
--- a/Xboard/compose.sample.yaml
+++ /dev/null
@@ -1,44 +0,0 @@
-services:
- web:
- image: ghcr.io/cedar2025/xboard:new
- volumes:
- - redis-data:/data
- - ./:/www/
- environment:
- - docker=true
- depends_on:
- - redis
- network_mode: host
- command: php artisan octane:start --port=7001 --host=0.0.0.0
- restart: always
- horizon:
- image: ghcr.io/cedar2025/xboard:new
- volumes:
- - redis-data:/data
- - ./:/www/
- restart: always
- network_mode: host
- command: php artisan horizon
- depends_on:
- - redis
- ws-server:
- image: ghcr.io/cedar2025/xboard:new
- volumes:
- - redis-data:/data
- - ./:/www/
- restart: always
- network_mode: host
- command: php artisan ws-server start
- depends_on:
- - redis
- redis:
- image: redis:7-alpine
- command: redis-server --unixsocket /data/redis.sock --unixsocketperm 777
- restart: unless-stopped
- volumes:
- - redis-data:/data
- sysctls:
- net.core.somaxconn: 1024
-
-volumes:
- redis-data:
diff --git a/Xboard/composer.json b/Xboard/composer.json
deleted file mode 100644
index ce7762d..0000000
--- a/Xboard/composer.json
+++ /dev/null
@@ -1,98 +0,0 @@
-{
- "name": "xboard/xboard",
- "type": "project",
- "description": "xboard is a proxy protocol manage.",
- "keywords": [
- "xboard",
- "v2ray",
- "shadowsocks",
- "trojan",
- "laravel"
- ],
- "license": "MIT",
- "require": {
- "php": "^8.2",
- "bacon/bacon-qr-code": "^2.0",
- "doctrine/dbal": "^4.0",
- "google/cloud-storage": "^1.35",
- "google/recaptcha": "^1.2",
- "guzzlehttp/guzzle": "^7.8",
- "laravel/framework": "^12.0",
- "laravel/horizon": "^5.30",
- "laravel/octane": "2.11.*",
- "laravel/prompts": "^0.3",
- "laravel/sanctum": "^4.0",
- "laravel/tinker": "^2.10",
- "linfo/linfo": "^4.0",
- "paragonie/sodium_compat": "^1.20",
- "php-curl-class/php-curl-class": "^8.6",
- "spatie/db-dumper": "^3.4",
- "stripe/stripe-php": "^7.36.1",
- "symfony/http-client": "^7.0",
- "symfony/mailgun-mailer": "^7.0",
- "symfony/yaml": "*",
- "webmozart/assert": "*",
- "workerman/redis": "^2.0",
- "workerman/workerman": "^5.1",
- "zoujingli/ip2region": "^2.0"
- },
- "require-dev": {
- "barryvdh/laravel-debugbar": "^3.9",
- "fakerphp/faker": "^1.9.1",
- "larastan/larastan": "^3.0",
- "mockery/mockery": "^1.6",
- "nunomaduro/collision": "^8.0",
- "orangehill/iseed": "^3.0",
- "phpunit/phpunit": "^11.0",
- "spatie/laravel-ignition": "^2.4"
- },
- "config": {
- "optimize-autoloader": true,
- "preferred-install": "dist",
- "sort-packages": true
- },
- "extra": {
- "laravel": {
- "dont-discover": []
- }
- },
- "autoload": {
- "psr-4": {
- "App\\": "app/",
- "Library\\": "library/",
- "Plugin\\": "plugins/"
- },
- "classmap": [
- "database/seeders",
- "database/factories"
- ],
- "files": [
- "app/Helpers/Functions.php"
- ]
- },
- "autoload-dev": {
- "psr-4": {
- "Tests\\": "tests/"
- }
- },
- "minimum-stability": "stable",
- "prefer-stable": true,
- "scripts": {
- "post-autoload-dump": [
- "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
- "@php artisan package:discover --ansi"
- ],
- "post-root-package-install": [
- "@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
- ],
- "post-create-project-cmd": [
- "@php artisan key:generate --ansi"
- ]
- },
- "repositories": [
- {
- "type": "composer",
- "url": "https://packagist.org"
- }
- ]
-}
diff --git a/Xboard/config/app.php b/Xboard/config/app.php
deleted file mode 100644
index 3ed7fe4..0000000
--- a/Xboard/config/app.php
+++ /dev/null
@@ -1,193 +0,0 @@
- env('APP_NAME', 'Laravel'),
-
- /*
- |--------------------------------------------------------------------------
- | Application Environment
- |--------------------------------------------------------------------------
- |
- | This value determines the "environment" your application is currently
- | running in. This may determine how you prefer to configure various
- | services the application utilizes. Set this in your ".env" file.
- |
- */
-
- 'env' => env('APP_ENV', 'production'),
-
- /*
- |--------------------------------------------------------------------------
- | Application Debug Mode
- |--------------------------------------------------------------------------
- |
- | When your application is in debug mode, detailed error messages with
- | stack traces will be shown on every error that occurs within your
- | application. If disabled, a simple generic error page is shown.
- |
- */
-
- 'debug' => env('APP_DEBUG', false),
-
- /*
- |--------------------------------------------------------------------------
- | Application URL
- |--------------------------------------------------------------------------
- |
- | This URL is used by the console to properly generate URLs when using
- | the Artisan command line tool. You should set this to the root of
- | your application so that it is used when running Artisan tasks.
- |
- */
-
- 'url' => env('APP_URL', 'http://localhost'),
-
- 'asset_url' => env('ASSET_URL', null),
-
- /*
- |--------------------------------------------------------------------------
- | Application Timezone
- |--------------------------------------------------------------------------
- |
- | Here you may specify the default timezone for your application, which
- | will be used by the PHP date and date-time functions. We have gone
- | ahead and set this to a sensible default for you out of the box.
- |
- */
-
- 'timezone' => 'Asia/Shanghai',
-
- /*
- |--------------------------------------------------------------------------
- | Application Locale Configuration
- |--------------------------------------------------------------------------
- |
- | The application locale determines the default locale that will be used
- | by the translation service provider. You are free to set this value
- | to any of the locales which will be supported by the application.
- |
- */
-
- 'locale' => 'zh-CN',
-
- /*
- |--------------------------------------------------------------------------
- | Application Fallback Locale
- |--------------------------------------------------------------------------
- |
- | The fallback locale determines the locale to use when the current one
- | is not available. You may change the value to correspond to any of
- | the language folders that are provided through your application.
- |
- */
-
- 'fallback_locale' => 'zh-CN',
-
- /*
- |--------------------------------------------------------------------------
- | Faker Locale
- |--------------------------------------------------------------------------
- |
- | This locale will be used by the Faker PHP library when generating fake
- | data for your database seeds. For example, this will be used to get
- | localized telephone numbers, street address information and more.
- |
- */
-
- 'faker_locale' => 'zh-CN',
-
- /*
- |--------------------------------------------------------------------------
- | Encryption Key
- |--------------------------------------------------------------------------
- |
- | This key is used by the Illuminate encrypter service and should be set
- | to a random, 32 character string, otherwise these encrypted strings
- | will not be safe. Please do this before deploying an application!
- |
- */
-
- 'key' => env('APP_KEY'),
-
- 'cipher' => 'AES-256-CBC',
-
- /*
- |--------------------------------------------------------------------------
- | Autoloaded Service Providers
- |--------------------------------------------------------------------------
- |
- | The service providers listed here will be automatically loaded on the
- | request to your application. Feel free to add your own services to
- | this array to grant expanded functionality to your applications.
- |
- */
-
- 'providers' => [
-
- /*
- * Laravel Framework Service Providers...
- */
- Illuminate\Auth\AuthServiceProvider::class,
- Illuminate\Broadcasting\BroadcastServiceProvider::class,
- Illuminate\Bus\BusServiceProvider::class,
- Illuminate\Cache\CacheServiceProvider::class,
- Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,
- Illuminate\Cookie\CookieServiceProvider::class,
- Illuminate\Database\DatabaseServiceProvider::class,
- Illuminate\Encryption\EncryptionServiceProvider::class,
- Illuminate\Filesystem\FilesystemServiceProvider::class,
- Illuminate\Foundation\Providers\FoundationServiceProvider::class,
- Illuminate\Hashing\HashServiceProvider::class,
- Illuminate\Mail\MailServiceProvider::class,
- Illuminate\Notifications\NotificationServiceProvider::class,
- Illuminate\Pagination\PaginationServiceProvider::class,
- Illuminate\Pipeline\PipelineServiceProvider::class,
- Illuminate\Queue\QueueServiceProvider::class,
- Illuminate\Redis\RedisServiceProvider::class,
- Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
- Illuminate\Session\SessionServiceProvider::class,
- Illuminate\Translation\TranslationServiceProvider::class,
- Illuminate\Validation\ValidationServiceProvider::class,
- Illuminate\View\ViewServiceProvider::class,
-
- /*
- * Package Service Providers...
- */
-
- /*
- * Application Service Providers...
- */
- App\Providers\AuthServiceProvider::class,
- // App\Providers\BroadcastServiceProvider::class,
- App\Providers\EventServiceProvider::class,
- App\Providers\HorizonServiceProvider::class,
- App\Providers\RouteServiceProvider::class,
- App\Providers\SettingServiceProvider::class,
- App\Providers\OctaneServiceProvider::class,
- App\Providers\PluginServiceProvider::class,
- App\Providers\ProtocolServiceProvider::class,
-
- ],
-
- /*
- |--------------------------------------------------------------------------
- | V2board version
- |--------------------------------------------------------------------------
- |
- | The only modification by laravel config
- |
- */
- 'version' => '1.0.0'
-];
diff --git a/Xboard/config/auth.php b/Xboard/config/auth.php
deleted file mode 100644
index c75105f..0000000
--- a/Xboard/config/auth.php
+++ /dev/null
@@ -1,103 +0,0 @@
- [
- 'guard' => 'api',
- 'passwords' => 'users',
- ],
-
- /*
- |--------------------------------------------------------------------------
- | Authentication Guards
- |--------------------------------------------------------------------------
- |
- | Next, you may define every authentication guard for your application.
- | Of course, a great default configuration has been defined for you
- | here which uses session storage and the Eloquent user provider.
- |
- | All authentication drivers have a user provider. This defines how the
- | users are actually retrieved out of your database or other storage
- | mechanisms used by this application to persist your user's data.
- |
- | Supported: "session", "token"
- |
- */
-
- 'guards' => [
- 'web' => [
- 'driver' => 'session',
- 'provider' => 'users',
- ],
-
- 'api' => [
- 'driver' => 'sanctum',
- 'provider' => 'users',
- 'hash' => false,
- ],
- ],
-
- /*
- |--------------------------------------------------------------------------
- | User Providers
- |--------------------------------------------------------------------------
- |
- | All authentication drivers have a user provider. This defines how the
- | users are actually retrieved out of your database or other storage
- | mechanisms used by this application to persist your user's data.
- |
- | If you have multiple user tables or models you may configure multiple
- | sources which represent each model / table. These sources may then
- | be assigned to any extra authentication guards you have defined.
- |
- | Supported: "database", "eloquent"
- |
- */
-
- 'providers' => [
- 'users' => [
- 'driver' => 'eloquent',
- 'model' => App\Models\User::class,
- ],
-
- // 'users' => [
- // 'driver' => 'database',
- // 'table' => 'users',
- // ],
- ],
-
- /*
- |--------------------------------------------------------------------------
- | Resetting Passwords
- |--------------------------------------------------------------------------
- |
- | You may specify multiple password reset configurations if you have more
- | than one user table or model in the application and you want to have
- | separate password reset settings based on the specific user types.
- |
- | The expire time is the number of minutes that the reset token should be
- | considered valid. This security feature keeps tokens short-lived so
- | they have less time to be guessed. You may change this as needed.
- |
- */
-
- 'passwords' => [
- 'users' => [
- 'provider' => 'users',
- 'table' => 'password_resets',
- 'expire' => 60,
- ],
- ],
-
-];
diff --git a/Xboard/config/broadcasting.php b/Xboard/config/broadcasting.php
deleted file mode 100644
index 3bba110..0000000
--- a/Xboard/config/broadcasting.php
+++ /dev/null
@@ -1,59 +0,0 @@
- env('BROADCAST_DRIVER', 'null'),
-
- /*
- |--------------------------------------------------------------------------
- | Broadcast Connections
- |--------------------------------------------------------------------------
- |
- | Here you may define all of the broadcast connections that will be used
- | to broadcast events to other systems or over websockets. Samples of
- | each available type of connection are provided inside this array.
- |
- */
-
- 'connections' => [
-
- 'pusher' => [
- 'driver' => 'pusher',
- 'key' => env('PUSHER_APP_KEY'),
- 'secret' => env('PUSHER_APP_SECRET'),
- 'app_id' => env('PUSHER_APP_ID'),
- 'options' => [
- 'cluster' => env('PUSHER_APP_CLUSTER'),
- 'useTLS' => true,
- ],
- ],
-
- 'redis' => [
- 'driver' => 'redis',
- 'connection' => 'default',
- ],
-
- 'log' => [
- 'driver' => 'log',
- ],
-
- 'null' => [
- 'driver' => 'null',
- ],
-
- ],
-
-];
diff --git a/Xboard/config/cache.php b/Xboard/config/cache.php
deleted file mode 100644
index b97535c..0000000
--- a/Xboard/config/cache.php
+++ /dev/null
@@ -1,107 +0,0 @@
- env('CACHE_DRIVER', 'file'),
-
- /*
- |--------------------------------------------------------------------------
- | Cache Stores
- |--------------------------------------------------------------------------
- |
- | Here you may define all of the cache "stores" for your application as
- | well as their drivers. You may even define multiple stores for the
- | same cache driver to group types of items stored in your caches.
- |
- */
-
- 'stores' => [
-
- 'apc' => [
- 'driver' => 'apc',
- ],
-
- 'array' => [
- 'driver' => 'array',
- ],
-
- 'database' => [
- 'driver' => 'database',
- 'table' => 'cache',
- 'connection' => null,
- ],
-
- 'file' => [
- 'driver' => 'file',
- 'path' => storage_path('framework/cache/data'),
- ],
-
- 'memcached' => [
- 'driver' => 'memcached',
- 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
- 'sasl' => [
- env('MEMCACHED_USERNAME'),
- env('MEMCACHED_PASSWORD'),
- ],
- 'options' => [
- // Memcached::OPT_CONNECT_TIMEOUT => 2000,
- ],
- 'servers' => [
- [
- 'host' => env('MEMCACHED_HOST', '127.0.0.1'),
- 'port' => env('MEMCACHED_PORT', 11211),
- 'weight' => 100,
- ],
- ],
- ],
-
- 'redis' => [
- 'driver' => 'redis',
- 'connection' => 'cache',
- ],
-
- 'octane' => [
- 'driver' => 'octane'
- ],
-
- 'dynamodb' => [
- 'driver' => 'dynamodb',
- 'key' => env('AWS_ACCESS_KEY_ID'),
- 'secret' => env('AWS_SECRET_ACCESS_KEY'),
- 'region' => env('AWS_V2BOARD_REGION', 'us-east-1'),
- 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
- 'endpoint' => env('DYNAMODB_ENDPOINT'),
- ],
-
- ],
-
- /*
- |--------------------------------------------------------------------------
- | Cache Key Prefix
- |--------------------------------------------------------------------------
- |
- | When utilizing a RAM based store such as APC or Memcached, there might
- | be other applications utilizing the same cache. So, we'll specify a
- | value to get prefixed to all our keys so we can avoid collisions.
- |
- */
-
- 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_') . '_cache'),
-
-];
diff --git a/Xboard/config/cloud_storage.php b/Xboard/config/cloud_storage.php
deleted file mode 100644
index 7ba0747..0000000
--- a/Xboard/config/cloud_storage.php
+++ /dev/null
@@ -1,10 +0,0 @@
- [
- 'key_file' => env('GOOGLE_CLOUD_KEY_FILE') ? base_path(env('GOOGLE_CLOUD_KEY_FILE')) : null,
- 'storage_bucket' => env('GOOGLE_CLOUD_STORAGE_BUCKET'),
- ],
-
-];
\ No newline at end of file
diff --git a/Xboard/config/cors.php b/Xboard/config/cors.php
deleted file mode 100644
index 558369d..0000000
--- a/Xboard/config/cors.php
+++ /dev/null
@@ -1,34 +0,0 @@
- ['api/*'],
-
- 'allowed_methods' => ['*'],
-
- 'allowed_origins' => ['*'],
-
- 'allowed_origins_patterns' => [],
-
- 'allowed_headers' => ['*'],
-
- 'exposed_headers' => [],
-
- 'max_age' => 0,
-
- 'supports_credentials' => false,
-
-];
diff --git a/Xboard/config/database.php b/Xboard/config/database.php
deleted file mode 100644
index 1929d5e..0000000
--- a/Xboard/config/database.php
+++ /dev/null
@@ -1,157 +0,0 @@
- env('DB_CONNECTION', 'mysql'),
-
- /*
- |--------------------------------------------------------------------------
- | Database Connections
- |--------------------------------------------------------------------------
- |
- | Here are each of the database connections setup for your application.
- | Of course, examples of configuring each database platform that is
- | supported by Laravel is shown below to make development simple.
- |
- |
- | All database work in Laravel is done through the PHP PDO facilities
- | so make sure you have the driver for your particular database of
- | choice installed on your machine before you begin development.
- |
- */
-
- 'connections' => [
-
- 'sqlite' => [
- 'driver' => 'sqlite',
- 'url' => env('DATABASE_URL'),
- 'database' => env('DB_DATABASE') ? base_path(env('DB_DATABASE')) : database_path('database.sqlite'),
- 'prefix' => '',
- 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
- 'busy_timeout' => env('DB_BUSY_TIMEOUT', 30000),
- 'journal_mode' => env('DB_JOURNAL_MODE', 'wal'),
- 'synchronous' => env('DB_SYNCHRONOUS', 'normal'),
- ],
-
- 'mysql' => [
- 'driver' => 'mysql',
- 'url' => env('DATABASE_URL'),
- 'host' => env('DB_HOST', '127.0.0.1'),
- 'port' => env('DB_PORT', '3306'),
- 'database' => env('DB_DATABASE', 'forge'),
- 'username' => env('DB_USERNAME', 'forge'),
- 'password' => env('DB_PASSWORD', ''),
- 'unix_socket' => env('DB_SOCKET', ''),
- 'charset' => 'utf8mb4',
- 'collation' => 'utf8mb4_unicode_ci',
- 'prefix' => '',
- 'prefix_indexes' => true,
- 'strict' => true,
- 'engine' => null,
- 'options' => (extension_loaded('pdo_mysql') ? array_filter([
- PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
- PDO::ATTR_PERSISTENT => false,
- ]) : []),
- 'pool' => [
- 'min_connections' => 1,
- 'max_connections' => 10,
- 'idle_timeout' => 60,
- ],
- ],
-
- 'pgsql' => [
- 'driver' => 'pgsql',
- 'url' => env('DATABASE_URL'),
- 'host' => env('DB_HOST', '127.0.0.1'),
- 'port' => env('DB_PORT', '5432'),
- 'database' => env('DB_DATABASE', 'forge'),
- 'username' => env('DB_USERNAME', 'forge'),
- 'password' => env('DB_PASSWORD', ''),
- 'charset' => 'utf8',
- 'prefix' => '',
- 'prefix_indexes' => true,
- 'search_path' => 'public',
- 'sslmode' => 'prefer',
- ],
-
- 'sqlsrv' => [
- 'driver' => 'sqlsrv',
- 'url' => env('DATABASE_URL'),
- 'host' => env('DB_HOST', 'localhost'),
- 'port' => env('DB_PORT', '1433'),
- 'database' => env('DB_DATABASE', 'forge'),
- 'username' => env('DB_USERNAME', 'forge'),
- 'password' => env('DB_PASSWORD', ''),
- 'charset' => 'utf8',
- 'prefix' => '',
- 'prefix_indexes' => true,
- ],
-
- ],
-
- /*
- |--------------------------------------------------------------------------
- | Migration Repository Table
- |--------------------------------------------------------------------------
- |
- | This table keeps track of all the migrations that have already run for
- | your application. Using this information, we can determine which of
- | the migrations on disk haven't actually been run in the database.
- |
- */
-
- 'migrations' => 'migrations',
-
- /*
- |--------------------------------------------------------------------------
- | Redis Databases
- |--------------------------------------------------------------------------
- |
- | Redis is an open source, fast, and advanced key-value store that also
- | provides a richer body of commands than a typical key-value system
- | such as APC or Memcached. Laravel makes it easy to dig right in.
- |
- */
-
- 'redis' => [
-
- 'client' => env('REDIS_CLIENT', 'phpredis'),
-
- 'options' => [
- 'cluster' => env('REDIS_CLUSTER', 'redis'),
- 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_') . '_database_'),
- ],
-
- 'default' => [
- 'url' => env('REDIS_URL'),
- 'host' => env('REDIS_HOST', '127.0.0.1'),
- 'password' => env('REDIS_PASSWORD', null),
- 'port' => env('REDIS_PORT', 6379),
- 'database' => env('REDIS_DB', 0),
- 'persistent' => true, // 开启持久连接
- ],
-
- 'cache' => [
- 'url' => env('REDIS_URL'),
- 'host' => env('REDIS_HOST', '127.0.0.1'),
- 'password' => env('REDIS_PASSWORD', null),
- 'port' => env('REDIS_PORT', 6379),
- 'database' => env('REDIS_CACHE_DB', 1),
- ],
-
- ],
-
-];
diff --git a/Xboard/config/debugbar.php b/Xboard/config/debugbar.php
deleted file mode 100644
index fe3b192..0000000
--- a/Xboard/config/debugbar.php
+++ /dev/null
@@ -1,275 +0,0 @@
- env('DEBUGBAR_ENABLED', null),
- 'except' => [
- 'telescope*',
- 'horizon*',
- ],
-
- /*
- |--------------------------------------------------------------------------
- | Storage settings
- |--------------------------------------------------------------------------
- |
- | DebugBar stores data for session/ajax requests.
- | You can disable this, so the debugbar stores data in headers/session,
- | but this can cause problems with large data collectors.
- | By default, file storage (in the storage folder) is used. Redis and PDO
- | can also be used. For PDO, run the package migrations first.
- |
- */
- 'storage' => [
- 'enabled' => true,
- 'driver' => 'file', // redis, file, pdo, socket, custom
- 'path' => storage_path('debugbar'), // For file driver
- 'connection' => null, // Leave null for default connection (Redis/PDO)
- 'provider' => '', // Instance of StorageInterface for custom driver
- 'hostname' => '127.0.0.1', // Hostname to use with the "socket" driver
- 'port' => 2304, // Port to use with the "socket" driver
- ],
-
- /*
- |--------------------------------------------------------------------------
- | Editor
- |--------------------------------------------------------------------------
- |
- | Choose your preferred editor to use when clicking file name.
- |
- | Supported: "phpstorm", "vscode", "vscode-insiders", "vscode-remote",
- | "vscode-insiders-remote", "vscodium", "textmate", "emacs",
- | "sublime", "atom", "nova", "macvim", "idea", "netbeans",
- | "xdebug", "espresso"
- |
- */
-
- 'editor' => env('DEBUGBAR_EDITOR', 'phpstorm'),
-
- /*
- |--------------------------------------------------------------------------
- | Remote Path Mapping
- |--------------------------------------------------------------------------
- |
- | If you are using a remote dev server, like Laravel Homestead, Docker, or
- | even a remote VPS, it will be necessary to specify your path mapping.
- |
- | Leaving one, or both of these, empty or null will not trigger the remote
- | URL changes and Debugbar will treat your editor links as local files.
- |
- | "remote_sites_path" is an absolute base path for your sites or projects
- | in Homestead, Vagrant, Docker, or another remote development server.
- |
- | Example value: "/home/vagrant/Code"
- |
- | "local_sites_path" is an absolute base path for your sites or projects
- | on your local computer where your IDE or code editor is running on.
- |
- | Example values: "/Users//Code", "C:\Users\\Documents\Code"
- |
- */
-
- 'remote_sites_path' => env('DEBUGBAR_REMOTE_SITES_PATH', ''),
- 'local_sites_path' => env('DEBUGBAR_LOCAL_SITES_PATH', ''),
-
- /*
- |--------------------------------------------------------------------------
- | Vendors
- |--------------------------------------------------------------------------
- |
- | Vendor files are included by default, but can be set to false.
- | This can also be set to 'js' or 'css', to only include javascript or css vendor files.
- | Vendor files are for css: font-awesome (including fonts) and highlight.js (css files)
- | and for js: jquery and and highlight.js
- | So if you want syntax highlighting, set it to true.
- | jQuery is set to not conflict with existing jQuery scripts.
- |
- */
-
- 'include_vendors' => true,
-
- /*
- |--------------------------------------------------------------------------
- | Capture Ajax Requests
- |--------------------------------------------------------------------------
- |
- | The Debugbar can capture Ajax requests and display them. If you don't want this (ie. because of errors),
- | you can use this option to disable sending the data through the headers.
- |
- | Optionally, you can also send ServerTiming headers on ajax requests for the Chrome DevTools.
- |
- | Note for your request to be identified as ajax requests they must either send the header
- | X-Requested-With with the value XMLHttpRequest (most JS libraries send this), or have application/json as a Accept header.
- */
-
- 'capture_ajax' => true,
- 'add_ajax_timing' => false,
-
- /*
- |--------------------------------------------------------------------------
- | Custom Error Handler for Deprecated warnings
- |--------------------------------------------------------------------------
- |
- | When enabled, the Debugbar shows deprecated warnings for Symfony components
- | in the Messages tab.
- |
- */
- 'error_handler' => false,
-
- /*
- |--------------------------------------------------------------------------
- | Clockwork integration
- |--------------------------------------------------------------------------
- |
- | The Debugbar can emulate the Clockwork headers, so you can use the Chrome
- | Extension, without the server-side code. It uses Debugbar collectors instead.
- |
- */
- 'clockwork' => false,
-
- /*
- |--------------------------------------------------------------------------
- | DataCollectors
- |--------------------------------------------------------------------------
- |
- | Enable/disable DataCollectors
- |
- */
-
- 'collectors' => [
- 'phpinfo' => true, // Php version
- 'messages' => true, // Messages
- 'time' => true, // Time Datalogger
- 'memory' => true, // Memory usage
- 'exceptions' => true, // Exception displayer
- 'log' => true, // Logs from Monolog (merged in messages if enabled)
- 'db' => true, // Show database (PDO) queries and bindings
- 'views' => true, // Views with their data
- 'route' => true, // Current route information
- 'auth' => false, // Display Laravel authentication status
- 'gate' => true, // Display Laravel Gate checks
- 'session' => true, // Display session data
- 'symfony_request' => true, // Only one can be enabled..
- 'mail' => true, // Catch mail messages
- 'laravel' => false, // Laravel version and environment
- 'events' => false, // All events fired
- 'default_request' => false, // Regular or special Symfony request logger
- 'logs' => false, // Add the latest log messages
- 'files' => false, // Show the included files
- 'config' => false, // Display config settings
- 'cache' => false, // Display cache events
- 'models' => true, // Display models
- 'livewire' => true, // Display Livewire (when available)
- ],
-
- /*
- |--------------------------------------------------------------------------
- | Extra options
- |--------------------------------------------------------------------------
- |
- | Configure some DataCollectors
- |
- */
-
- 'options' => [
- 'auth' => [
- 'show_name' => true, // Also show the users name/email in the debugbar
- ],
- 'db' => [
- 'with_params' => true, // Render SQL with the parameters substituted
- 'backtrace' => true, // Use a backtrace to find the origin of the query in your files.
- 'backtrace_exclude_paths' => [], // Paths to exclude from backtrace. (in addition to defaults)
- 'timeline' => false, // Add the queries to the timeline
- 'duration_background' => true, // Show shaded background on each query relative to how long it took to execute.
- 'explain' => [ // Show EXPLAIN output on queries
- 'enabled' => false,
- 'types' => ['SELECT'], // Deprecated setting, is always only SELECT
- ],
- 'hints' => false, // Show hints for common mistakes
- 'show_copy' => false, // Show copy button next to the query
- ],
- 'mail' => [
- 'full_log' => false,
- ],
- 'views' => [
- 'timeline' => false, // Add the views to the timeline (Experimental)
- 'data' => false, //Note: Can slow down the application, because the data can be quite large..
- ],
- 'route' => [
- 'label' => true, // show complete route on bar
- ],
- 'logs' => [
- 'file' => null,
- ],
- 'cache' => [
- 'values' => true, // collect cache values
- ],
- ],
-
- /*
- |--------------------------------------------------------------------------
- | Inject Debugbar in Response
- |--------------------------------------------------------------------------
- |
- | Usually, the debugbar is added just before