Initial commit

This commit is contained in:
xboard 2023-11-17 14:44:01 +08:00
commit 65fe7682ff
460 changed files with 63554 additions and 0 deletions

2
.docker/.data/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

View File

@ -0,0 +1 @@
* * * * * php /www/artisan schedule:run >> /dev/null 2>&1

View File

@ -0,0 +1,40 @@
server {
listen 7001 default_server;
root /www/public/;
index index.html index.htm;
server_name _;
# 开启 brotli 压缩
brotli on;
brotli_static on;
brotli_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript;
# 开启 gzip 压缩
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
location ~* \.(jpg|jpeg|png|gif|js|css|svg|woff2|woff|ttf|eot)$ {
# 这里将.jpg、.jpeg、.png、.gif、.js和.css文件直接返回给客户端
}
location ~ .* {
proxy_pass http://127.0.0.1:7010;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-PORT $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header Scheme $scheme;
proxy_set_header Server-Protocol $server_protocol;
proxy_set_header Server-Name $server_name;
proxy_set_header Server-Addr $server_addr;
proxy_set_header Server-Port $server_port;
}
location ~ /\.ht {
deny all;
}
}

View File

@ -0,0 +1,65 @@
[supervisord]
nodaemon=true
logfile=/dev/null
logfile_maxbytes=0
pidfile=/tmp/supervisord.pid
[program:nginx]
command=nginx -g 'daemon off;'
user=root
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
autostart=true
autorestart=true
startretries=10
[program:cron]
command=crond -f -l 8
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
autostart=true
autorestart=true
startretries=10
; [program:laravels]
; command=php bin/laravels start
; directory=/www
; user=www-data
; numprocs=1
; stdout_logfile=/dev/stdout
; stdout_logfile_maxbytes=0
; stderr_logfile=/dev/stderr
; stderr_logfile_maxbytes=0
; autostart=true
; autorestart=true
; startretries=3
[program:adapterman]
command=php -c php.ini webman.php start
directory=/www
user=root
numprocs=1
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
autostart=true
autorestart=true
startretries=3
[program:xboard-queue]
command=php artisan horizon
directory=/www
user=root
stdout_logfile=/www/storage/logs/queue.log
stdout_logfile_maxbytes=0
stderr_logfile=/www/storage/logs/queue_error.log
stderr_logfile_maxbytes=0
autostart=true
autorestart=true
startretries=10
numprocs=1

25
.dockerignore Normal file
View File

@ -0,0 +1,25 @@
/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

15
.editorconfig Executable file
View File

@ -0,0 +1,15 @@
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

48
.env.example Executable file
View File

@ -0,0 +1,48 @@
APP_NAME=XBoard
APP_ENV=local
APP_KEY=base64:PZXk5vTuTinfeEVG5FpYv2l6WEhLsyvGpiWK7IgJJ60=
APP_DEBUG=false
APP_URL=http://localhost
ADMIN_SETTING_CACHE=60 #设置缓存时间(单位秒)
#LaravelS配置
LARAVELS_LISTEN_IP=0.0.0.0
LARAVELS_LISTEN_PORT=80
LARAVELS_HANDLE_STATIC=true
LARAVELS_MAX_REQUEST=1000
LARAVELS_WORKER_NUM=2
LARAVELS_TIMER=true
APP_RUNNING_IN_CONSOLE=true
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
#默认将队列驱动和缓存驱动都修改为了redis请务必安装redis
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=
# 用于阻止重复安装
INSTALLED=false

5
.gitattributes vendored Executable file
View File

@ -0,0 +1,5 @@
* text=auto
*.css linguist-vendored
*.scss linguist-vendored
*.js linguist-vendored
CHANGELOG.md export-ignore

View File

@ -0,0 +1,39 @@
---
name: Bug report | 问题反馈
about: Tell us what problems you have encountered
title: "[BUG]"
labels: ''
assignees: ''
---
🙇🙇🙇注意XrayR等非XBoard问题请前往项目方提问
🙇🙇🙇Note: XrayR and other non-XBoard issues please go to the project side to ask questions
The XBoard version number you are using
当前使用的XBoard版本号
--------
Briefly describe the problem you are experiencing
简单描述你遇到的问题
--------
Screenshot of the reported error(Please do desensitization)
报告错误的截图(请做脱敏处理)
--------
Screenshot of the reported error(Please do desensitization)
报告错误的截图(请做脱敏处理)
--------
The latest log files in the storage/logs directory report from #1 (Please do desensitization)
storage/logs 目录下最新的日志文件从 #1 开始报告(请做脱敏处理)
--------

View File

@ -0,0 +1,11 @@
---
name: Feature request | 功能请求
about: Tell us what you need
title: "[Feature request]"
labels: ''
assignees: ''
---
Please describe in detail the problems or needs you have encountered.
请详细描述你遇到的问题或需求。

96
.github/workflows/docker-publish.yml vendored Normal file
View File

@ -0,0 +1,96 @@
name: Docker
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
on:
push:
branches: [ "master" ]
# Publish semver tags as releases.
tags: [ 'v*.*.*' ]
env:
# Use docker.io for Docker Hub if empty
REGISTRY: ghcr.io
# github.repository as <account>/<repo>
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
# This is used to complete the identity challenge
# with sigstore/fulcio when running outside of PRs.
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Install cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@6e04d228eb30da1757ee4e1dd75a0ec73a653e06 #v3.1.1
with:
cosign-release: 'v2.1.1'
# Set up BuildKit Docker container builder to be able to build
# multi-platform images and export cache
# https://github.com/docker/setup-buildx-action
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Get version
id: get_version
run: echo "version=$(git describe --tags --always)" >> $GITHUB_OUTPUT
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ env.REGISTRY }}/cedar2025/xboard:latest,${{ env.REGISTRY }}/cedar2025/xboard,${{ env.REGISTRY }}/cedar2025/xboard:${{ steps.get_version.outputs.version }}
# Sign the resulting Docker image digest except on PRs.
# This will only write to the public Rekor transparency log when the Docker
# repository is public to avoid leaking data. If you would like to publish
# transparency data even for private images, pass --force to cosign below.
# https://github.com/sigstore/cosign
- name: Sign the published Docker image
if: ${{ github.event_name != 'pull_request' }}
env:
# https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable
TAGS: ${{ steps.meta.outputs.tags }}
DIGEST: ${{ steps.build-and-push.outputs.digest }}
# This step uses the identity token to provision an ephemeral certificate
# against the sigstore community Fulcio instance.
run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST}

27
.gitignore vendored Executable file
View File

@ -0,0 +1,27 @@
/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
cli-php.ini
frontend

17
Dockerfile Normal file
View File

@ -0,0 +1,17 @@
FROM phpswoole/swoole:php8.1-alpine
COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/
RUN install-php-extensions pcntl
RUN apk --no-cache add shadow supervisor nginx sqlite nginx-mod-http-brotli
#复制项目文件以及配置文件
WORKDIR /www
COPY .docker /
COPY . /www
RUN composer install --optimize-autoloader --no-cache --no-dev \
&& php artisan storage:link \
&& chmod -R 777 ./
CMD [ "/usr/bin/supervisord", "--nodaemon", "-c" ,"/etc/supervisor/supervisord.conf" ]

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
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.

View File

@ -0,0 +1,127 @@
<?php
namespace App\Console\Commands;
use App\Models\CommissionLog;
use Illuminate\Console\Command;
use App\Models\Order;
use App\Models\User;
use Illuminate\Support\Facades\DB;
class CheckCommission extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'check:commission';
/**
* The console command description.
*
* @var string
*/
protected $description = '返佣服务';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$this->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) {
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();
}
}
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->balance = $inviter->balance + $commissionBalance;
} else {
$inviter->commission_balance = $inviter->commission_balance + $commissionBalance;
}
if (!$inviter->save()) {
DB::rollBack();
return false;
}
if (!CommissionLog::create([
'invite_user_id' => $inviteUserId,
'user_id' => $order->user_id,
'trade_no' => $order->trade_no,
'order_amount' => $order->total_amount,
'get_amount' => $commissionBalance
])) {
DB::rollBack();
return false;
}
$inviteUserId = $inviter->invite_user_id;
// update order actual commission balance
$order->actual_commission_balance = $order->actual_commission_balance + $commissionBalance;
}
return true;
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace App\Console\Commands;
use App\Jobs\OrderHandleJob;
use App\Services\OrderService;
use Illuminate\Console\Command;
use App\Models\Order;
use App\Models\User;
use App\Models\Plan;
use Illuminate\Support\Facades\DB;
class CheckOrder extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'check:order';
/**
* The console command description.
*
* @var string
*/
protected $description = '订单检查任务';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
ini_set('memory_limit', -1);
$orders = Order::whereIn('status', [0, 1])
->orderBy('created_at', 'ASC')
->get();
foreach ($orders as $order) {
OrderHandleJob::dispatch($order->trade_no);
}
}
}

View File

@ -0,0 +1,65 @@
<?php
namespace App\Console\Commands;
use App\Services\ServerService;
use App\Services\TelegramService;
use App\Utils\CacheKey;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
class CheckServer extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'check:server';
/**
* The console command description.
*
* @var string
*/
protected $description = '节点检查任务';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$this->checkOffline();
}
private function checkOffline()
{
$serverService = new ServerService();
$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));
}
}
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace App\Console\Commands;
use App\Models\Ticket;
use Illuminate\Console\Command;
class CheckTicket extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'check:ticket';
/**
* The console command description.
*
* @var string
*/
protected $description = '工单检查任务';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
ini_set('memory_limit', -1);
$tickets = Ticket::where('status', 0)
->where('updated_at', '<=', time() - 24 * 3600)
->where('reply_status', 0)
->get();
foreach ($tickets as $ticket) {
if ($ticket->user_id === $ticket->last_reply_user_id) continue;
$ticket->status = 1;
$ticket->save();
}
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace App\Console\Commands;
use App\Models\Ticket;
use App\Models\User;
use Illuminate\Console\Command;
class ClearUser extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'clear:user';
/**
* The console command description.
*
* @var string
*/
protected $description = '清理用户';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$builder = User::where('plan_id', NULL)
->where('transfer_enable', 0)
->where('expired_at', 0)
->where('last_login_at', NULL);
$count = $builder->count();
if ($builder->delete()) {
$this->info("已删除${count}位没有任何数据的用户");
}
}
}

View File

@ -0,0 +1,181 @@
<?php
namespace App\Console\Commands;
use App\Models\Setting;
use Illuminate\Console\Command;
class MigrateFromV2b extends Command
{
protected $signature = 'migrateFromV2b {version?}';
protected $description = '供不同版本V2b迁移到本项目的脚本';
public function handle()
{
$version = $this->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 order_total INT COMMENT '订单合计';",
"ALTER TABLE `v2_stat` CHANGE COLUMN commission_amount commission_total INT COMMENT '佣金合计';",
"ALTER TABLE `v2_stat`
ADD COLUMN paid_count INT NULL,
ADD COLUMN paid_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()
{
$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' => $v,
]);
$this->info("配置 ${k} 迁移成功");
}
$this->info('所有配置迁移完成');
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace App\Console\Commands;
use App\Models\Log;
use App\Models\Plan;
use App\Models\StatServer;
use App\Models\StatUser;
use App\Utils\Helper;
use Illuminate\Console\Command;
use App\Models\User;
use Illuminate\Support\Facades\DB;
class ResetLog extends Command
{
protected $builder;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'reset:log';
/**
* The console command description.
*
* @var string
*/
protected $description = '清空日志';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
StatUser::where('record_at', '<', strtotime('-2 month', time()))->delete();
StatServer::where('record_at', '<', strtotime('-2 month', time()))->delete();
Log::where('created_at', '<', strtotime('-1 month', time()))->delete();
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace App\Console\Commands;
use App\Models\Plan;
use App\Utils\Helper;
use Illuminate\Console\Command;
use App\Models\User;
use Illuminate\Support\Facades\DB;
class ResetPassword extends Command
{
protected $builder;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'reset:password {email}';
/**
* The console command description.
*
* @var string
*/
protected $description = '重置用户密码';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$user = User::where('email', $this->argument('email'))->first();
if (!$user) abort(500, '邮箱不存在');
$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},请尽快修改密码。");
}
}

View File

@ -0,0 +1,164 @@
<?php
namespace App\Console\Commands;
use App\Models\Plan;
use Illuminate\Console\Command;
use App\Models\User;
use Illuminate\Support\Facades\DB;
class ResetTraffic extends Command
{
protected $builder;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'reset:traffic';
/**
* The console command description.
*
* @var string
*/
protected $description = '流量清空';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
$this->builder = User::where('expired_at', '!=', NULL)
->where('expired_at', '>', time());
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
ini_set('memory_limit', -1);
$resetMethods = Plan::select(
DB::raw("GROUP_CONCAT(`id`) as plan_ids"),
DB::raw("reset_traffic_method as method")
)
->groupBy('reset_traffic_method')
->get()
->toArray();
foreach ($resetMethods as $resetMethod) {
$planIds = explode(',', $resetMethod['plan_ids']);
switch (true) {
case ($resetMethod['method'] === NULL): {
$resetTrafficMethod = admin_setting('reset_traffic_method', 0);
$builder = with(clone($this->builder))->whereIn('plan_id', $planIds);
switch ((int)$resetTrafficMethod) {
// month first day
case 0:
$this->resetByMonthFirstDay($builder);
break;
// expire day
case 1:
$this->resetByExpireDay($builder);
break;
// no action
case 2:
break;
// year first day
case 3:
$this->resetByYearFirstDay($builder);
// year expire day
case 4:
$this->resetByExpireYear($builder);
}
break;
}
case ($resetMethod['method'] === 0): {
$builder = with(clone($this->builder))->whereIn('plan_id', $planIds);
$this->resetByMonthFirstDay($builder);
break;
}
case ($resetMethod['method'] === 1): {
$builder = with(clone($this->builder))->whereIn('plan_id', $planIds);
$this->resetByExpireDay($builder);
break;
}
case ($resetMethod['method'] === 2): {
break;
}
case ($resetMethod['method'] === 3): {
$builder = with(clone($this->builder))->whereIn('plan_id', $planIds);
$this->resetByYearFirstDay($builder);
break;
}
case ($resetMethod['method'] === 4): {
$builder = with(clone($this->builder))->whereIn('plan_id', $planIds);
$this->resetByExpireYear($builder);
break;
}
}
}
}
private function resetByExpireYear($builder):void
{
$users = [];
foreach ($builder->get() as $item) {
$expireDay = date('m-d', $item->expired_at);
$today = date('m-d');
if ($expireDay === $today) {
array_push($users, $item->id);
}
}
User::whereIn('id', $users)->update([
'u' => 0,
'd' => 0
]);
}
private function resetByYearFirstDay($builder):void
{
if ((string)date('md') === '0101') {
$builder->update([
'u' => 0,
'd' => 0
]);
}
}
private function resetByMonthFirstDay($builder):void
{
if ((string)date('d') === '01') {
$builder->update([
'u' => 0,
'd' => 0
]);
}
}
private function resetByExpireDay($builder):void
{
$lastDay = date('d', strtotime('last day of +0 months'));
$users = [];
foreach ($builder->get() as $item) {
$expireDay = date('d', $item->expired_at);
$today = date('d');
if ($expireDay === $today) {
array_push($users, $item->id);
}
if (($today === $lastDay) && $expireDay >= $lastDay) {
array_push($users, $item->id);
}
}
User::whereIn('id', $users)->update([
'u' => 0,
'd' => 0
]);
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace App\Console\Commands;
use App\Models\Plan;
use App\Utils\Helper;
use Illuminate\Console\Command;
use App\Models\User;
use Illuminate\Support\Facades\DB;
class ResetUser extends Command
{
protected $builder;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'reset:user';
/**
* The console command description.
*
* @var string
*/
protected $description = '重置所有用户信息';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if (!$this->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}的安全信息");
}
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace App\Console\Commands;
use App\Services\MailService;
use Illuminate\Console\Command;
use App\Models\User;
use App\Models\MailLog;
use App\Jobs\SendEmailJob;
class SendRemindMail extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'send:remindMail';
/**
* The console command description.
*
* @var string
*/
protected $description = '发送提醒邮件';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$users = User::all();
$mailService = new MailService();
foreach ($users as $user) {
if ($user->remind_expire) $mailService->remindExpire($user);
if ($user->remind_traffic) $mailService->remindTraffic($user);
}
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class Test extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'test';
/**
* The console command description.
*
* @var string
*/
protected $description = '';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
}
}

View File

@ -0,0 +1,192 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Encryption\Encrypter;
use App\Models\User;
use App\Utils\Helper;
use Illuminate\Support\Env;
use Illuminate\Support\Facades\DB;
class XboardInstall extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'xboard:install';
/**
* The console command description.
*
* @var string
*/
protected $description = 'xboard 初始化安装';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
try {
\Artisan::call('config:clear');
$this->info("__ __ ____ _ ");
$this->info("\ \ / /| __ ) ___ __ _ _ __ __| | ");
$this->info(" \ \/ / | __ \ / _ \ / _` | '__/ _` | ");
$this->info(" / /\ \ | |_) | (_) | (_| | | | (_| | ");
$this->info("/_/ \_\|____/ \___/ \__,_|_| \__,_| ");
if (\File::exists(base_path() . '/.env') && $this->getEnvValue('INSTALLED')) {
$securePath = admin_setting('secure_path', admin_setting('frontend_admin_path', hash('crc32b', config('app.key'))));
$this->info("访问 http(s)://你的站点/{$securePath} 进入管理面板,你可以在用户中心修改你的密码。");
abort(500, '如需重新安装请清空目录下 .env 文件的内容Docker安装方式不可以删除此文件');
\Artisan::call('config:cache');
}
// 选择是否使用Sqlite
$isSqlite = $this->ask('是否启用Sqlite代替Mysql(默认不启动 y/n)','n');
if( $isSqlite == 'y' ) {
$sqliteFile = '.docker/.data/database.sqlite';
if (!file_exists(base_path($sqliteFile))) {
// 创建空文件
if (touch(base_path($sqliteFile))) {
echo "sqlite创建成功: $sqliteFile";
} else {
echo "sqlite创建成功";
}
}
$envConfig = [
'APP_KEY' => 'base64:' . base64_encode(Encrypter::generateKey('AES-256-CBC')),
'DB_CONNECTION' => 'sqlite',
'DB_DATABASE' => $sqliteFile,
'DB_HOST' => '',
'DB_USERNAME' => '',
'DB_PASSWORD' => '',
'REDIS_HOST' => $this->ask('请输入redis地址(默认: 127.0.0.1)', '127.0.0.1'),
'REDIS_PORT'=> $this->ask('请输入redis端口(默认: 6379)', '6379'),
'REDIS_PASSWORD' => $this->ask('请输入redis密码(默认: null)', null),
'INSTALLED' => 'true'
];
if (!copy(base_path() . '/.env.example', base_path() . '/.env')) {
abort(500, '复制环境文件失败,请检查目录权限');
}
$this->saveToEnv($envConfig);
}else{
$envConfig = [
'APP_KEY' => 'base64:' . base64_encode(Encrypter::generateKey('AES-256-CBC')),
'DB_CONNECTION' => 'mysql',
'DB_HOST' => $this->ask('请输入数据库地址(默认:127.0.0.1)', '127.0.0.1'),
'DB_PORT' => $this->ask('请输入数据库端口(默认:3306)', '3306'),
'DB_DATABASE' => $this->ask('请输入数据库名', 'xboard'),
'DB_USERNAME' => $this->ask('请输入数据库用户名'),
'DB_PASSWORD' => $this->ask('请输入数据库密码'),
'REDIS_HOST' => $this->ask('请输入redis地址(默认: 127.0.0.1)', '127.0.0.1'),
'REDIS_PORT'=> $this->ask('请输入redis端口(默认: 6379)', '6379'),
'REDIS_PASSWORD' => $this->ask('请输入redis密码(默认: null)', null),
'INSTALLED' => 'true'
];
if (!copy(base_path() . '/.env.example', base_path() . '/.env')) {
abort(500, '复制环境文件失败,请检查目录权限');
}
$this->saveToEnv($envConfig);
}
\Artisan::call('config:clear');
\Artisan::call('config:cache');
\Artisan::call('cache:clear');
$this->info('正在清空数据库请稍等');
\Artisan::call('db:wipe');
$this->info('数据库清空完成');
$this->info('正在导入数据库请稍等...');
\Artisan::call("migrate");
$this->info(\Artisan::output());
$this->info('数据库导入完成');
$email = '';
while (!$email) {
$email = $this->ask('请输入管理员邮箱?');
}
$password = Helper::guid(false);
if (!$this->registerAdmin($email, $password)) {
abort(500, '管理员账号注册失败,请重试');
}
$this->info('一切就绪');
$this->info("管理员邮箱:{$email}");
$this->info("管理员密码:{$password}");
$defaultSecurePath = hash('crc32b', config('app.key'));
$this->info("访问 http(s)://你的站点/{$defaultSecurePath} 进入管理面板,你可以在用户中心修改你的密码。");
} catch (\Exception $e) {
$this->error($e->getMessage());
}
}
public 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 saveToEnv($data = [])
{
function set_env_var($key, $value)
{
if (! is_bool(strpos($value, ' '))) {
$value = '"' . $value . '"';
}
$key = strtoupper($key);
$envPath = app()->environmentFilePath();
$contents = file_get_contents($envPath);
preg_match("/^{$key}=[^\r\n]*/m", $contents, $matches);
$oldValue = count($matches) ? $matches[0] : '';
if ($oldValue) {
$contents = str_replace("{$oldValue}", "{$key}={$value}", $contents);
} else {
$contents = $contents . "\n{$key}={$value}\n";
}
$file = fopen($envPath, 'w');
fwrite($file, $contents);
return fclose($file);
}
foreach($data as $key => $value) {
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);
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class XboardRollback extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'xboard:rollback';
/**
* The console command description.
*
* @var string
*/
protected $description = 'xboard 回滚';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$this->info('正在回滚数据库请稍等...');
\Artisan::call("migrate:rollback");
$this->info(\Artisan::output());
}
}

View File

@ -0,0 +1,140 @@
<?php
namespace App\Console\Commands;
use App\Models\StatServer;
use App\Models\StatUser;
use App\Services\StatisticalService;
use Illuminate\Console\Command;
use App\Models\Stat;
use Illuminate\Support\Facades\DB;
class XboardStatistics extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'xboard:statistics';
/**
* The console command description.
*
* @var string
*/
protected $description = '统计任务';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$startAt = microtime(true);
ini_set('memory_limit', -1);
$this->statUser();
$this->statServer();
$this->stat();
info('统计任务执行完毕。耗时:' . (microtime(true) - $startAt) / 1000);
}
private function statServer()
{
try {
DB::beginTransaction();
$createdAt = time();
$recordAt = strtotime('-1 day', strtotime(date('Y-m-d')));
$statService = new StatisticalService();
$statService->setStartAt($recordAt);
$statService->setServerStats();
$stats = $statService->getStatServer();
foreach ($stats as $stat) {
if (!StatServer::insert([
'server_id' => $stat['server_id'],
'server_type' => $stat['server_type'],
'u' => $stat['u'],
'd' => $stat['d'],
'created_at' => $createdAt,
'updated_at' => $createdAt,
'record_type' => 'd',
'record_at' => $recordAt
])) {
throw new \Exception('stat server fail');
}
}
DB::commit();
$statService->clearStatServer();
} catch (\Exception $e) {
DB::rollback();
\Log::error($e->getMessage(), ['exception' => $e]);
}
}
private function statUser()
{
try {
DB::beginTransaction();
$createdAt = time();
$recordAt = strtotime('-1 day', strtotime(date('Y-m-d')));
$statService = new StatisticalService();
$statService->setStartAt($recordAt);
$statService->setUserStats();
$stats = $statService->getStatUser();
foreach ($stats as $stat) {
if (!StatUser::insert([
'user_id' => $stat['user_id'],
'u' => $stat['u'],
'd' => $stat['d'],
'server_rate' => $stat['server_rate'],
'created_at' => $createdAt,
'updated_at' => $createdAt,
'record_type' => 'd',
'record_at' => $recordAt
])) {
throw new \Exception('stat user fail');
}
}
DB::commit();
$statService->clearStatUser();
} catch (\Exception $e) {
DB::rollback();
\Log::error($e->getMessage(), ['exception' => $e]);
}
}
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]);
}
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class XboardUpdate extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'xboard:update';
/**
* The console command description.
*
* @var string
*/
protected $description = 'xboard 更新';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$this->info('正在导入数据库请稍等...');
\Artisan::call("migrate");
$this->info(\Artisan::output());
\Artisan::call('horizon:terminate');
$this->info('更新完毕,队列服务已重启,你无需进行任何操作。');
}
}

56
app/Console/Kernel.php Normal file
View File

@ -0,0 +1,56 @@
<?php
namespace App\Console;
use App\Utils\CacheKey;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use Illuminate\Support\Facades\Cache;
class Kernel extends ConsoleKernel
{
/**
* The Artisan commands provided by your application.
*
* @var array
*/
protected $commands = [
//
];
/**
* Define the application's command schedule.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
protected function schedule(Schedule $schedule)
{
Cache::put(CacheKey::get('SCHEDULE_LAST_CHECK_AT', null), time());
// v2board
$schedule->command('xboard:statistics')->dailyAt('0:10');
// check
$schedule->command('check:order')->everyMinute();
$schedule->command('check:commission')->everyMinute();
$schedule->command('check:ticket')->everyMinute();
// reset
$schedule->command('reset:traffic')->daily();
$schedule->command('reset:log')->daily();
// send
$schedule->command('send:remindMail')->dailyAt('11:30');
// horizon metrics
$schedule->command('horizon:snapshot')->everyFiveMinutes();
}
/**
* Register the commands for the application.
*
* @return void
*/
protected function commands()
{
$this->load(__DIR__ . '/Commands');
require base_path('routes/console.php');
}
}

79
app/Exceptions/Handler.php Executable file
View File

@ -0,0 +1,79 @@
<?php
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Support\Arr;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Throwable;
use Facade\Ignition\Exceptions\ViewException;
use Illuminate\Support\Facades\Log;
class Handler extends ExceptionHandler
{
/**
* A list of the exception types that are not reported.
*
* @var array
*/
protected $dontReport = [
//
];
/**
* 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) {
abort(500, "主题渲染失败。如更新主题,参数可能发生变化请重新配置主题后再试。");
}
return parent::render($request, $exception);
}
protected function convertExceptionToArray(Throwable $e)
{
Log::channel("daily")->info($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."),
];
}
}

33
app/Helpers/Functions.php Normal file
View File

@ -0,0 +1,33 @@
<?php
if (! function_exists("get_request_content")){
function get_request_content(){
return request()->getContent() ?: json_encode($_POST);
}
}
if (! function_exists('admin_setting')) {
/**
* 获取或保存配置参数.
*
* @param string|array $key
* @param mixed $default
* @return App\Support\Setting|mixed
*/
function admin_setting($key = null, $default = null)
{
if ($key === null) {
return app('setting');
}
if (is_array($key)) {
app('setting')->save($key);
return;
}
$default = config('v2board.'. $key) ?? $default;
return app('setting')->get($key, $default) ;
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
class Controller extends BaseController
{
use DispatchesJobs, ValidatesRequests;
}

View File

@ -0,0 +1,200 @@
<?php
namespace App\Http\Controllers\V1\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\ConfigSave;
use App\Jobs\SendEmailJob;
use App\Models\Setting;
use App\Services\TelegramService;
use App\Utils\Dict;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class ConfigController extends Controller
{
public function getEmailTemplate()
{
$path = resource_path('views/mail/');
$files = array_map(function ($item) use ($path) {
return str_replace($path, '', $item);
}, glob($path . '*'));
return response([
'data' => $files
]);
}
public function getThemeTemplate()
{
$path = public_path('theme/');
$files = array_map(function ($item) use ($path) {
return str_replace($path, '', $item);
}, glob($path . '*'));
return response([
'data' => $files
]);
}
public function testSendMail(Request $request)
{
$obj = new SendEmailJob([
'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' => true,
'log' => $obj->handle()
]);
}
public function setTelegramWebhook(Request $request)
{
$hookUrl = url('/api/v1/guest/telegram/webhook?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($hookUrl);
return response([
'data' => true
]);
}
public function fetch(Request $request)
{
$key = $request->input('key');
$data = [
'invite' => [
'invite_force' => (int)admin_setting('invite_force', 0),
'invite_commission' => admin_setting('invite_commission', 10),
'invite_gen_limit' => admin_setting('invite_gen_limit', 5),
'invite_never_expire' => admin_setting('invite_never_expire', 0),
'commission_first_time_enable' => admin_setting('commission_first_time_enable', 1),
'commission_auto_check_enable' => 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' => admin_setting('withdraw_close_enable', 0),
'commission_distribution_enable' => 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', '¥'),
],
'subscribe' => [
'plan_change_enable' => (int)admin_setting('plan_change_enable', 1),
'reset_traffic_method' => (int)admin_setting('reset_traffic_method', 0),
'surplus_enable' => (int)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' => (int)admin_setting('show_info_to_server_enable', 0),
'show_protocol_to_server_enable' => (int)admin_setting('show_protocol_to_server_enable', 0),
],
'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),
],
'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')
],
'telegram' => [
'telegram_bot_enable' => admin_setting('telegram_bot_enable', 0),
'telegram_bot_token' => admin_setting('telegram_bot_token'),
'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' => (int)admin_setting('email_verify', 0),
'safe_mode_enable' => (int)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' => (int)admin_setting('email_whitelist_enable', 0),
'email_whitelist_suffix' => admin_setting('email_whitelist_suffix', Dict::EMAIL_WHITELIST_SUFFIX_DEFAULT),
'email_gmail_limit_enable' => admin_setting('email_gmail_limit_enable', 0),
'recaptcha_enable' => (int)admin_setting('recaptcha_enable', 0),
'recaptcha_key' => admin_setting('recaptcha_key'),
'recaptcha_site_key' => admin_setting('recaptcha_site_key'),
'register_limit_by_ip_enable' => (int)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' => (int)admin_setting('password_limit_enable', 1),
'password_limit_count' => admin_setting('password_limit_count', 5),
'password_limit_expire' => admin_setting('password_limit_expire', 60)
]
];
if ($key && isset($data[$key])) {
return response([
'data' => [
$key => $data[$key]
]
]);
};
// TODO: default should be in Dict
return response([
'data' => $data
]);
}
public function save(ConfigSave $request)
{
$data = $request->validated();
$config = config('v2board');
foreach (ConfigSave::RULES as $k => $v) {
if (!in_array($k, array_keys(ConfigSave::RULES))) {
unset($config[$k]);
continue;
}
if (array_key_exists($k, $data)) {
$value = $data[$k];
if (is_array($value)) $value = json_encode($value);
Setting::updateOrCreate(
['name' => $k],
['name' => $k, 'value' => $value]
);
}
}
Cache::forget('admin_settings');
// \Artisan::call('horizon:terminate'); //重启队列使配置生效
return response([
'data' => true
]);
}
}

View File

@ -0,0 +1,135 @@
<?php
namespace App\Http\Controllers\V1\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\CouponGenerate;
use App\Http\Requests\Admin\CouponSave;
use App\Models\Coupon;
use App\Utils\Helper;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class CouponController extends Controller
{
public function fetch(Request $request)
{
$current = $request->input('current') ? $request->input('current') : 1;
$pageSize = $request->input('pageSize') >= 10 ? $request->input('pageSize') : 10;
$sortType = in_array($request->input('sort_type'), ['ASC', 'DESC']) ? $request->input('sort_type') : 'DESC';
$sort = $request->input('sort') ? $request->input('sort') : 'id';
$builder = Coupon::orderBy($sort, $sortType);
$total = $builder->count();
$coupons = $builder->forPage($current, $pageSize)
->get();
return response([
'data' => $coupons,
'total' => $total
]);
}
public function show(Request $request)
{
if (empty($request->input('id'))) {
abort(500, '参数有误');
}
$coupon = Coupon::find($request->input('id'));
if (!$coupon) {
abort(500, '优惠券不存在');
}
$coupon->show = $coupon->show ? 0 : 1;
if (!$coupon->save()) {
abort(500, '保存失败');
}
return response([
'data' => 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)) {
abort(500, '创建失败');
}
} else {
try {
Coupon::find($request->input('id'))->update($params);
} catch (\Exception $e) {
abort(500, '保存失败');
}
}
return response([
'data' => 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);
}
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))) {
DB::rollBack();
abort(500, '生成失败');
}
DB::commit();
$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)
{
if (empty($request->input('id'))) {
abort(500, '参数有误');
}
$coupon = Coupon::find($request->input('id'));
if (!$coupon) {
abort(500, '优惠券不存在');
}
if (!$coupon->delete()) {
abort(500, '删除失败');
}
return response([
'data' => true
]);
}
}

View File

@ -0,0 +1,113 @@
<?php
namespace App\Http\Controllers\V1\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\KnowledgeSave;
use App\Http\Requests\Admin\KnowledgeSort;
use App\Models\Knowledge;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class KnowledgeController extends Controller
{
public function fetch(Request $request)
{
if ($request->input('id')) {
$knowledge = Knowledge::find($request->input('id'))->toArray();
if (!$knowledge) abort(500, '知识不存在');
return response([
'data' => $knowledge
]);
}
return response([
'data' => Knowledge::select(['title', 'id', 'updated_at', 'category', 'show'])
->orderBy('sort', 'ASC')
->get()
]);
}
public function getCategory(Request $request)
{
return response([
'data' => array_keys(Knowledge::get()->groupBy('category')->toArray())
]);
}
public function save(KnowledgeSave $request)
{
$params = $request->validated();
if (!$request->input('id')) {
if (!Knowledge::create($params)) {
abort(500, '创建失败');
}
} else {
try {
Knowledge::find($request->input('id'))->update($params);
} catch (\Exception $e) {
abort(500, '保存失败');
}
}
return response([
'data' => true
]);
}
public function show(Request $request)
{
if (empty($request->input('id'))) {
abort(500, '参数有误');
}
$knowledge = Knowledge::find($request->input('id'));
if (!$knowledge) {
abort(500, '知识不存在');
}
$knowledge->show = $knowledge->show ? 0 : 1;
if (!$knowledge->save()) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
public function sort(KnowledgeSort $request)
{
DB::beginTransaction();
try {
foreach ($request->input('knowledge_ids') as $k => $v) {
$knowledge = Knowledge::find($v);
$knowledge->timestamps = false;
$knowledge->update(['sort' => $k + 1]);
}
} catch (\Exception $e) {
DB::rollBack();
abort(500, '保存失败');
}
DB::commit();
return response([
'data' => true
]);
}
public function drop(Request $request)
{
if (empty($request->input('id'))) {
abort(500, '参数有误');
}
$knowledge = Knowledge::find($request->input('id'));
if (!$knowledge) {
abort(500, '知识不存在');
}
if (!$knowledge->delete()) {
abort(500, '删除失败');
}
return response([
'data' => true
]);
}
}

View File

@ -0,0 +1,81 @@
<?php
namespace App\Http\Controllers\V1\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\NoticeSave;
use App\Models\Notice;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class NoticeController extends Controller
{
public function fetch(Request $request)
{
return response([
'data' => Notice::orderBy('id', 'DESC')->get()
]);
}
public function save(NoticeSave $request)
{
$data = $request->only([
'title',
'content',
'img_url',
'tags'
]);
if (!$request->input('id')) {
if (!Notice::create($data)) {
abort(500, '保存失败');
}
} else {
try {
Notice::find($request->input('id'))->update($data);
} catch (\Exception $e) {
abort(500, '保存失败');
}
}
return response([
'data' => true
]);
}
public function show(Request $request)
{
if (empty($request->input('id'))) {
abort(500, '参数有误');
}
$notice = Notice::find($request->input('id'));
if (!$notice) {
abort(500, '公告不存在');
}
$notice->show = $notice->show ? 0 : 1;
if (!$notice->save()) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
public function drop(Request $request)
{
if (empty($request->input('id'))) {
abort(500, '参数错误');
}
$notice = Notice::find($request->input('id'));
if (!$notice) {
abort(500, '公告不存在');
}
if (!$notice->delete()) {
abort(500, '删除失败');
}
return response([
'data' => true
]);
}
}

View File

@ -0,0 +1,190 @@
<?php
namespace App\Http\Controllers\V1\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\OrderAssign;
use App\Http\Requests\Admin\OrderFetch;
use App\Http\Requests\Admin\OrderUpdate;
use App\Models\CommissionLog;
use App\Models\Order;
use App\Models\Plan;
use App\Models\User;
use App\Services\OrderService;
use App\Services\UserService;
use App\Utils\Helper;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class OrderController extends Controller
{
private function filter(Request $request, &$builder)
{
if ($request->input('filter')) {
foreach ($request->input('filter') as $filter) {
if ($filter['key'] === 'email') {
$user = User::where('email', "%{$filter['value']}%")->first();
if (!$user) continue;
$builder->where('user_id', $user->id);
continue;
}
if ($filter['condition'] === '模糊') {
$filter['condition'] = 'like';
$filter['value'] = "%{$filter['value']}%";
}
$builder->where($filter['key'], $filter['condition'], $filter['value']);
}
}
}
public function detail(Request $request)
{
$order = Order::find($request->input('id'));
if (!$order) abort(500, '订单不存在');
$order['commission_log'] = CommissionLog::where('trade_no', $order->trade_no)->get();
if ($order->surplus_order_ids) {
$order['surplus_orders'] = Order::whereIn('id', $order->surplus_order_ids)->get();
}
return response([
'data' => $order
]);
}
public function fetch(OrderFetch $request)
{
$current = $request->input('current') ? $request->input('current') : 1;
$pageSize = $request->input('pageSize') >= 10 ? $request->input('pageSize') : 10;
$orderModel = Order::orderBy('created_at', 'DESC');
if ($request->input('is_commission')) {
$orderModel->where('invite_user_id', '!=', NULL);
$orderModel->whereNotIn('status', [0, 2]);
$orderModel->where('commission_balance', '>', 0);
}
$this->filter($request, $orderModel);
$total = $orderModel->count();
$res = $orderModel->forPage($current, $pageSize)
->get();
$plan = Plan::get();
for ($i = 0; $i < count($res); $i++) {
for ($k = 0; $k < count($plan); $k++) {
if ($plan[$k]['id'] == $res[$i]['plan_id']) {
$res[$i]['plan_name'] = $plan[$k]['name'];
}
}
}
return response([
'data' => $res,
'total' => $total
]);
}
public function paid(Request $request)
{
$order = Order::where('trade_no', $request->input('trade_no'))
->first();
if (!$order) {
abort(500, '订单不存在');
}
if ($order->status !== 0) abort(500, '只能对待支付的订单进行操作');
$orderService = new OrderService($order);
if (!$orderService->paid('manual_operation')) {
abort(500, '更新失败');
}
return response([
'data' => true
]);
}
public function cancel(Request $request)
{
$order = Order::where('trade_no', $request->input('trade_no'))
->first();
if (!$order) {
abort(500, '订单不存在');
}
if ($order->status !== 0) abort(500, '只能对待支付的订单进行操作');
$orderService = new OrderService($order);
if (!$orderService->cancel()) {
abort(500, '更新失败');
}
return response([
'data' => true
]);
}
public function update(OrderUpdate $request)
{
$params = $request->only([
'commission_status'
]);
$order = Order::where('trade_no', $request->input('trade_no'))
->first();
if (!$order) {
abort(500, '订单不存在');
}
try {
$order->update($params);
} catch (\Exception $e) {
abort(500, '更新失败');
}
return response([
'data' => true
]);
}
public function assign(OrderAssign $request)
{
$plan = Plan::find($request->input('plan_id'));
$user = User::where('email', $request->input('email'))->first();
if (!$user) {
abort(500, '该用户不存在');
}
if (!$plan) {
abort(500, '该订阅不存在');
}
$userService = new UserService();
if ($userService->isNotCompleteOrderByUserId($user->id)) {
abort(500, '该用户还有待支付的订单,无法分配');
}
DB::beginTransaction();
$order = new Order();
$orderService = new OrderService($order);
$order->user_id = $user->id;
$order->plan_id = $plan->id;
$order->period = $request->input('period');
$order->trade_no = Helper::guid();
$order->total_amount = $request->input('total_amount');
if ($order->period === 'reset_price') {
$order->type = 4;
} else if ($user->plan_id !== NULL && $order->plan_id !== $user->plan_id) {
$order->type = 3;
} else if ($user->expired_at > time() && $order->plan_id == $user->plan_id) {
$order->type = 2;
} else {
$order->type = 1;
}
$orderService->setInvite($user);
if (!$order->save()) {
DB::rollback();
abort(500, '订单创建失败');
}
DB::commit();
return response([
'data' => $order->trade_no
]);
}
}

View File

@ -0,0 +1,133 @@
<?php
namespace App\Http\Controllers\V1\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\PaymentSave;
use App\Models\Payment;
use App\Services\PaymentService;
use App\Utils\Helper;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class PaymentController extends Controller
{
public function getPaymentMethods()
{
$methods = [];
foreach (glob(base_path('app//Payments') . '/*.php') as $file) {
array_push($methods, pathinfo($file)['filename']);
}
return response([
'data' => $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 response([
'data' => $payments
]);
}
public function getPaymentForm(Request $request)
{
$paymentService = new PaymentService($request->input('payment'), $request->input('id'));
return response([
'data' => $paymentService->form()
]);
}
public function show(Request $request)
{
$payment = Payment::find($request->input('id'));
if (!$payment) abort(500, '支付方式不存在');
$payment->enable = !$payment->enable;
if (!$payment->save()) abort(500, '保存失败');
return response([
'data' => true
]);
}
public function save(Request $request)
{
if (!admin_setting('app_url')) {
abort(500, '请在站点配置中配置站点地址');
}
$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.1,100'
], [
'name.required' => '显示名称不能为空',
'payment.required' => '网关参数不能为空',
'config.required' => '配置参数不能为空',
'notify_domain.url' => '自定义通知域名格式有误',
'handling_fee_fixed.integer' => '固定手续费格式有误',
'handling_fee_percent.between' => '百分比手续费范围须在0.1-100之间'
]);
if ($request->input('id')) {
$payment = Payment::find($request->input('id'));
if (!$payment) abort(500, '支付方式不存在');
try {
$payment->update($params);
} catch (\Exception $e) {
abort(500, $e->getMessage());
}
return response([
'data' => true
]);
}
$params['uuid'] = Helper::randomChar(8);
if (!Payment::create($params)) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
public function drop(Request $request)
{
$payment = Payment::find($request->input('id'));
if (!$payment) abort(500, '支付方式不存在');
return response([
'data' => $payment->delete()
]);
}
public function sort(Request $request)
{
$request->validate([
'ids' => 'required|array'
], [
'ids.required' => '参数有误',
'ids.array' => '参数有误'
]);
DB::beginTransaction();
foreach ($request->input('ids') as $k => $v) {
if (!Payment::find($v)->update(['sort' => $k + 1])) {
DB::rollBack();
abort(500, '保存失败');
}
}
DB::commit();
return response([
'data' => true
]);
}
}

View File

@ -0,0 +1,125 @@
<?php
namespace App\Http\Controllers\V1\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\PlanSave;
use App\Http\Requests\Admin\PlanSort;
use App\Http\Requests\Admin\PlanUpdate;
use App\Models\Order;
use App\Models\Plan;
use App\Models\User;
use App\Services\PlanService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class PlanController extends Controller
{
public function fetch(Request $request)
{
$counts = PlanService::countActiveUsers();
$plans = Plan::orderBy('sort', 'ASC')->get();
foreach ($plans as $k => $v) {
$plans[$k]->count = 0;
foreach ($counts as $kk => $vv) {
if ($plans[$k]->id === $counts[$kk]->plan_id) $plans[$k]->count = $counts[$kk]->count;
}
}
return response([
'data' => $plans
]);
}
public function save(PlanSave $request)
{
$params = $request->validated();
if ($request->input('id')) {
$plan = Plan::find($request->input('id'));
if (!$plan) {
abort(500, '该订阅不存在');
}
DB::beginTransaction();
// update user group id and transfer
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']
]);
}
$plan->update($params);
} catch (\Exception $e) {
DB::rollBack();
abort(500, '保存失败');
}
DB::commit();
return response([
'data' => true
]);
}
if (!Plan::create($params)) {
abort(500, '创建失败');
}
return response([
'data' => true
]);
}
public function drop(Request $request)
{
if (Order::where('plan_id', $request->input('id'))->first()) {
abort(500, '该订阅下存在订单无法删除');
}
if (User::where('plan_id', $request->input('id'))->first()) {
abort(500, '该订阅下存在用户无法删除');
}
if ($request->input('id')) {
$plan = Plan::find($request->input('id'));
if (!$plan) {
abort(500, '该订阅ID不存在');
}
}
return response([
'data' => $plan->delete()
]);
}
public function update(PlanUpdate $request)
{
$updateData = $request->only([
'show',
'renew'
]);
$plan = Plan::find($request->input('id'));
if (!$plan) {
abort(500, '该订阅不存在');
}
try {
$plan->update($updateData);
} catch (\Exception $e) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
public function sort(PlanSort $request)
{
DB::beginTransaction();
foreach ($request->input('plan_ids') as $k => $v) {
if (!Plan::find($v)->update(['sort' => $k + 1])) {
DB::rollBack();
abort(500, '保存失败');
}
}
DB::commit();
return response([
'data' => true
]);
}
}

View File

@ -0,0 +1,83 @@
<?php
namespace App\Http\Controllers\V1\Admin\Server;
use App\Http\Controllers\Controller;
use App\Models\Plan;
use App\Models\ServerGroup;
use App\Models\ServerVmess;
use App\Models\User;
use App\Services\ServerService;
use Illuminate\Http\Request;
class GroupController extends Controller
{
public function fetch(Request $request)
{
if ($request->input('group_id')) {
return response([
'data' => [ServerGroup::find($request->input('group_id'))]
]);
}
$serverGroups = ServerGroup::get();
$serverService = new ServerService();
$servers = $serverService->getAllServers();
foreach ($serverGroups as $k => $v) {
$serverGroups[$k]['user_count'] = User::where('group_id', $v['id'])->count();
$serverGroups[$k]['server_count'] = 0;
foreach ($servers as $server) {
if (in_array($v['id'], $server['group_id'])) {
$serverGroups[$k]['server_count'] = $serverGroups[$k]['server_count']+1;
}
}
}
return response([
'data' => $serverGroups
]);
}
public function save(Request $request)
{
if (empty($request->input('name'))) {
abort(500, '组名不能为空');
}
if ($request->input('id')) {
$serverGroup = ServerGroup::find($request->input('id'));
} else {
$serverGroup = new ServerGroup();
}
$serverGroup->name = $request->input('name');
return response([
'data' => $serverGroup->save()
]);
}
public function drop(Request $request)
{
if ($request->input('id')) {
$serverGroup = ServerGroup::find($request->input('id'));
if (!$serverGroup) {
abort(500, '组不存在');
}
}
$servers = ServerVmess::all();
foreach ($servers as $server) {
if (in_array($request->input('id'), $server->group_id)) {
abort(500, '该组已被节点所使用,无法删除');
}
}
if (Plan::where('group_id', $request->input('id'))->first()) {
abort(500, '该组已被订阅所使用,无法删除');
}
if (User::where('group_id', $request->input('id'))->first()) {
abort(500, '该组已被用户所使用,无法删除');
}
return response([
'data' => $serverGroup->delete()
]);
}
}

View File

@ -0,0 +1,114 @@
<?php
namespace App\Http\Controllers\V1\Admin\Server;
use App\Http\Controllers\Controller;
use App\Models\ServerHysteria;
use Illuminate\Http\Request;
class HysteriaController extends Controller
{
public function save(Request $request)
{
$params = $request->validate([
'show' => '',
'name' => 'required',
'group_id' => 'required|array',
'route_id' => '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',
'up_mbps' => 'required|numeric|min:1',
'down_mbps' => 'required|numeric|min:1',
'server_name' => 'nullable',
'insecure' => 'required|in:0,1',
'alpn' => 'nullable|in:0,1,2,3',
'version' => 'nullable|in:1,2',
'is_obfs' => 'nullable'
]);
if ($request->input('id')) {
$server = ServerHysteria::find($request->input('id'));
if (!$server) {
abort(500, '服务器不存在');
}
try {
$server->update($params);
} catch (\Exception $e) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
if (!ServerHysteria::create($params)) {
abort(500, '创建失败');
}
return response([
'data' => true
]);
}
public function drop(Request $request)
{
if ($request->input('id')) {
$server = ServerHysteria::find($request->input('id'));
if (!$server) {
abort(500, '节点ID不存在');
}
}
return response([
'data' => $server->delete()
]);
}
public function update(Request $request)
{
$request->validate([
'show' => 'in:0,1'
], [
'show.in' => '显示状态格式不正确'
]);
$params = $request->only([
'show',
]);
$server = ServerHysteria::find($request->input('id'));
if (!$server) {
abort(500, '该服务器不存在');
}
try {
$server->update($params);
} catch (\Exception $e) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
public function copy(Request $request)
{
$server = ServerHysteria::find($request->input('id'));
$server->show = 0;
if (!$server) {
abort(500, '服务器不存在');
}
if (!ServerHysteria::create($server->toArray())) {
abort(500, '复制失败');
}
return response([
'data' => true
]);
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace App\Http\Controllers\V1\Admin\Server;
use App\Http\Controllers\Controller;
use App\Services\ServerService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class ManageController extends Controller
{
public function getNodes(Request $request)
{
$serverService = new ServerService();
return response([
'data' => $serverService->getAllServers()
]);
}
public function sort(Request $request)
{
ini_set('post_max_size', '1m');
$params = $request->only(
'shadowsocks',
'vmess',
'trojan',
'hysteria',
'vless'
) ?? [];
DB::beginTransaction();
foreach ($params as $k => $v) {
$model = 'App\\Models\\Server' . ucfirst($k);
foreach($v as $id => $sort) {
if (!$model::find($id)->update(['sort' => $sort])) {
DB::rollBack();
abort(500, '保存失败');
}
}
}
DB::commit();
return response([
'data' => true
]);
}
}

View File

@ -0,0 +1,68 @@
<?php
namespace App\Http\Controllers\V1\Admin\Server;
use App\Http\Controllers\Controller;
use App\Models\ServerRoute;
use Illuminate\Http\Request;
class RouteController extends Controller
{
public function fetch(Request $request)
{
$routes = ServerRoute::get();
// TODO: remove on 1.8.0
foreach ($routes as $k => $route) {
$array = json_decode($route->match, true);
if (is_array($array)) $routes[$k]['match'] = $array;
}
// TODO: remove on 1.8.0
return [
'data' => $routes
];
}
public function save(Request $request)
{
$params = $request->validate([
'remarks' => 'required',
'match' => 'required|array',
'action' => 'required|in:block,dns',
'action_value' => 'nullable'
], [
'remarks.required' => '备注不能为空',
'match.required' => '匹配值不能为空',
'action.required' => '动作类型不能为空',
'action.in' => '动作类型参数有误'
]);
$params['match'] = array_filter($params['match']);
// TODO: remove on 1.8.0
$params['match'] = json_encode($params['match']);
// TODO: remove on 1.8.0
if ($request->input('id')) {
try {
$route = ServerRoute::find($request->input('id'));
$route->update($params);
return [
'data' => true
];
} catch (\Exception $e) {
abort(500, '保存失败');
}
}
if (!ServerRoute::create($params)) abort(500, '创建失败');
return [
'data' => true
];
}
public function drop(Request $request)
{
$route = ServerRoute::find($request->input('id'));
if (!$route) abort(500, '路由不存在');
if (!$route->delete()) abort(500, '删除失败');
return [
'data' => true
];
}
}

View File

@ -0,0 +1,90 @@
<?php
namespace App\Http\Controllers\V1\Admin\Server;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\ServerShadowsocksSave;
use App\Http\Requests\Admin\ServerShadowsocksUpdate;
use App\Models\ServerShadowsocks;
use Illuminate\Http\Request;
class ShadowsocksController extends Controller
{
public function save(ServerShadowsocksSave $request)
{
$params = $request->validated();
if ($request->input('id')) {
$server = ServerShadowsocks::find($request->input('id'));
if (!$server) {
abort(500, '服务器不存在');
}
try {
$server->update($params);
} catch (\Exception $e) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
if (!ServerShadowsocks::create($params)) {
abort(500, '创建失败');
}
return response([
'data' => true
]);
}
public function drop(Request $request)
{
if ($request->input('id')) {
$server = ServerShadowsocks::find($request->input('id'));
if (!$server) {
abort(500, '节点ID不存在');
}
}
return response([
'data' => $server->delete()
]);
}
public function update(ServerShadowsocksUpdate $request)
{
$params = $request->only([
'show',
]);
$server = ServerShadowsocks::find($request->input('id'));
if (!$server) {
abort(500, '该服务器不存在');
}
try {
$server->update($params);
} catch (\Exception $e) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
public function copy(Request $request)
{
$server = ServerShadowsocks::find($request->input('id'));
$server->show = 0;
if (!$server) {
abort(500, '服务器不存在');
}
if (!ServerShadowsocks::create($server->toArray())) {
abort(500, '复制失败');
}
return response([
'data' => true
]);
}
}

View File

@ -0,0 +1,99 @@
<?php
namespace App\Http\Controllers\V1\Admin\Server;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\ServerTrojanSave;
use App\Http\Requests\Admin\ServerTrojanUpdate;
use App\Models\ServerTrojan;
use App\Services\ServerService;
use Illuminate\Http\Request;
class TrojanController extends Controller
{
public function save(ServerTrojanSave $request)
{
$params = $request->validated();
if ($request->input('id')) {
$server = ServerTrojan::find($request->input('id'));
if (!$server) {
abort(500, '服务器不存在');
}
try {
$server->update($params);
} catch (\Exception $e) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
if (!ServerTrojan::create($params)) {
abort(500, '创建失败');
}
return response([
'data' => true
]);
}
public function drop(Request $request)
{
if ($request->input('id')) {
$server = ServerTrojan::find($request->input('id'));
if (!$server) {
abort(500, '节点ID不存在');
}
}
return response([
'data' => $server->delete()
]);
}
public function update(ServerTrojanUpdate $request)
{
$params = $request->only([
'show',
]);
$server = ServerTrojan::find($request->input('id'));
if (!$server) {
abort(500, '该服务器不存在');
}
try {
$server->update($params);
} catch (\Exception $e) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
public function copy(Request $request)
{
$server = ServerTrojan::find($request->input('id'));
$server->show = 0;
if (!$server) {
abort(500, '服务器不存在');
}
if (!ServerTrojan::create($server->toArray())) {
abort(500, '复制失败');
}
return response([
'data' => true
]);
}
public function viewConfig(Request $request)
{
$serverService = new ServerService();
$config = $serverService->getTrojanConfig($request->input('node_id'), 23333);
return response([
'data' => $config
]);
}
}

View File

@ -0,0 +1,127 @@
<?php
namespace App\Http\Controllers\V1\Admin\Server;
use App\Http\Controllers\Controller;
use App\Models\ServerVless;
use Illuminate\Http\Request;
use ParagonIE_Sodium_Compat as SodiumCompat;
use App\Utils\Helper;
class VlessController extends Controller
{
public function save(Request $request)
{
$params = $request->validate([
'group_id' => 'required',
'route_id' => 'nullable|array',
'name' => 'required',
'parent_id' => 'nullable|integer',
'host' => 'required',
'port' => 'required',
'server_port' => 'required',
'tls' => 'required|in:0,1,2',
'tls_settings' => 'nullable|array',
'flow' => 'nullable|in:xtls-rprx-vision',
'network' => 'required',
'network_settings' => 'nullable|array',
'tags' => 'nullable|array',
'excludes' => 'nullable|array',
'ips' => 'nullable|array',
'rate' => 'required',
'show' => 'nullable|in:0,1',
'sort' => 'nullable'
]);
if (isset($params['tls']) && (int)$params['tls'] === 2) {
$keyPair = SodiumCompat::crypto_box_keypair();
$params['tls_settings'] = $params['tls_settings'] ?? [];
if (!isset($params['tls_settings']['public_key'])) {
$params['tls_settings']['public_key'] = Helper::base64EncodeUrlSafe(SodiumCompat::crypto_box_publickey($keyPair));
}
if (!isset($params['tls_settings']['private_key'])) {
$params['tls_settings']['private_key'] = Helper::base64EncodeUrlSafe(SodiumCompat::crypto_box_secretkey($keyPair));
}
if (!isset($params['tls_settings']['short_id'])) {
$params['tls_settings']['short_id'] = substr(sha1($params['tls_settings']['private_key']), 0, 8);
}
if (!isset($params['tls_settings']['server_port'])) {
$params['tls_settings']['server_port'] = "443";
}
}
if ($request->input('id')) {
$server = ServerVless::find($request->input('id'));
if (!$server) {
abort(500, '服务器不存在');
}
try {
$server->update($params);
} catch (\Exception $e) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
if (!ServerVless::create($params)) {
abort(500, '创建失败');
}
return response([
'data' => true
]);
}
public function drop(Request $request)
{
if ($request->input('id')) {
$server = ServerVless::find($request->input('id'));
if (!$server) {
abort(500, '节点ID不存在');
}
}
return response([
'data' => $server->delete()
]);
}
public function update(Request $request)
{
$params = $request->validate([
'show' => 'nullable|in:0,1',
]);
$server = ServerVless::find($request->input('id'));
if (!$server) {
abort(500, '该服务器不存在');
}
try {
$server->update($params);
} catch (\Exception $e) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
public function copy(Request $request)
{
$server = ServerVless::find($request->input('id'));
$server->show = 0;
if (!$server) {
abort(500, '服务器不存在');
}
if (!ServerVless::create($server->toArray())) {
abort(500, '复制失败');
}
return response([
'data' => true
]);
}
}

View File

@ -0,0 +1,91 @@
<?php
namespace App\Http\Controllers\V1\Admin\Server;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\ServerVmessSave;
use App\Http\Requests\Admin\ServerVmessUpdate;
use App\Models\ServerVmess;
use Illuminate\Http\Request;
class VmessController extends Controller
{
public function save(ServerVmessSave $request)
{
$params = $request->validated();
if ($request->input('id')) {
$server = ServerVmess::find($request->input('id'));
if (!$server) {
abort(500, '服务器不存在');
}
try {
$server->update($params);
} catch (\Exception $e) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
if (!ServerVmess::create($params)) {
abort(500, '创建失败');
}
return response([
'data' => true
]);
}
public function drop(Request $request)
{
if ($request->input('id')) {
$server = ServerVmess::find($request->input('id'));
if (!$server) {
abort(500, '节点ID不存在');
}
}
return response([
'data' => $server->delete()
]);
}
public function update(ServerVmessUpdate $request)
{
$params = $request->only([
'show',
]);
$server = ServerVmess::find($request->input('id'));
if (!$server) {
abort(500, '该服务器不存在');
}
try {
$server->update($params);
} catch (\Exception $e) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
public function copy(Request $request)
{
$server = ServerVmess::find($request->input('id'));
$server->show = 0;
if (!$server) {
abort(500, '服务器不存在');
}
if (!ServerVmess::create($server->toArray())) {
abort(500, '复制失败');
}
return response([
'data' => true
]);
}
}

View File

@ -0,0 +1,209 @@
<?php
namespace App\Http\Controllers\V1\Admin;
use App\Http\Controllers\Controller;
use App\Models\CommissionLog;
use App\Models\Order;
use App\Models\ServerHysteria;
use App\Models\ServerVless;
use App\Models\ServerShadowsocks;
use App\Models\ServerTrojan;
use App\Models\ServerVmess;
use App\Models\Stat;
use App\Models\StatServer;
use App\Models\StatUser;
use App\Models\Ticket;
use App\Models\User;
use App\Services\StatisticalService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class StatController extends Controller
{
public function getOverride(Request $request)
{
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'),
]
];
}
public function getOrder(Request $request)
{
$statistics = Stat::where('record_type', 'd')
->limit(31)
->orderBy('record_at', 'DESC')
->get()
->toArray();
$result = [];
foreach ($statistics as $statistic) {
$date = date('m-d', $statistic['record_at']);
$result[] = [
'type' => '收款金额',
'date' => $date,
'value' => $statistic['paid_total'] / 100
];
$result[] = [
'type' => '收款笔数',
'date' => $date,
'value' => $statistic['paid_count']
];
$result[] = [
'type' => '佣金金额(已发放)',
'date' => $date,
'value' => $statistic['commission_total'] / 100
];
$result[] = [
'type' => '佣金笔数(已发放)',
'date' => $date,
'value' => $statistic['commission_count']
];
}
$result = array_reverse($result);
return [
'data' => $result
];
}
// 获取当日实时流量排行
public function getServerLastRank()
{
$servers = [
'shadowsocks' => ServerShadowsocks::with(['parent'])->get()->toArray(),
'v2ray' => ServerVmess::with(['parent'])->get()->toArray(),
'trojan' => ServerTrojan::with(['parent'])->get()->toArray(),
'vmess' => ServerVmess::with(['parent'])->get()->toArray(),
'hysteria' => ServerHysteria::with(['parent'])->get()->toArray(),
'vless' => ServerVless::with(['parent'])->get()->toArray(),
];
$recordAt = strtotime(date('Y-m-d'));
$statService = new StatisticalService();
$statService->setStartAt($recordAt);
$statService->setServerStats();
$stats = $statService->getStatServer();
$statistics = collect($stats)->map(function ($item){
$item['total'] = $item['u'] + $item['d'];
return $item;
})->sortByDesc('total')->values()->all();
// return json_encode($statistics);
foreach ($statistics as $k => $v) {
foreach ($servers[$v['server_type']] as $server) {
if ($server['id'] === $v['server_id']) {
$statistics[$k]['server_name'] = $server['name'];
if($server['parent']) $statistics[$k]['server_name'] .= "({$server['parent']['name']})";
}
}
$statistics[$k]['total'] = $statistics[$k]['total'] / 1073741824;
}
array_multisort(array_column($statistics, 'total'), SORT_DESC, $statistics);
return [
'data' => $statistics
];
}
// 获取昨日节点流量排行
public function getServerYesterdayRank()
{
$servers = [
'shadowsocks' => ServerShadowsocks::with(['parent'])->get()->toArray(),
'v2ray' => ServerVmess::with(['parent'])->get()->toArray(),
'trojan' => ServerTrojan::with(['parent'])->get()->toArray(),
'vmess' => ServerVmess::with(['parent'])->get()->toArray(),
'hysteria' => ServerHysteria::with(['parent'])->get()->toArray(),
'vless' => ServerVless::with(['parent'])->get()->toArray(),
];
$startAt = strtotime('-1 day', strtotime(date('Y-m-d')));
$endAt = strtotime(date('Y-m-d'));
$statistics = StatServer::select([
'server_id',
'server_type',
'u',
'd',
DB::raw('(u+d) as total')
])
->where('record_at', '>=', $startAt)
->where('record_at', '<', $endAt)
->where('record_type', 'd')
->limit(15)
->orderBy('total', 'DESC')
->get()
->toArray();
foreach ($statistics as $k => $v) {
foreach ($servers[$v['server_type']] as $server) {
if ($server['id'] === $v['server_id']) {
$statistics[$k]['server_name'] = $server['name'];
if($server['parent']) $statistics[$k]['server_name'] .= "({$server['parent']['name']})";
}
}
$statistics[$k]['total'] = $statistics[$k]['total'] / 1073741824;
}
array_multisort(array_column($statistics, 'total'), SORT_DESC, $statistics);
return [
'data' => $statistics
];
}
public function getStatUser(Request $request)
{
$request->validate([
'user_id' => 'required|integer'
]);
$current = $request->input('current') ? $request->input('current') : 1;
$pageSize = $request->input('pageSize') >= 10 ? $request->input('pageSize') : 10;
$builder = StatUser::orderBy('record_at', 'DESC')->where('user_id', $request->input('user_id'));
$total = $builder->count();
$records = $builder->forPage($current, $pageSize)
->get();
// 追加当天流量
$recordAt = strtotime(date('Y-m-d'));
$statService = new StatisticalService();
$statService->setStartAt($recordAt);
$statService->setUserStats();
$todayTraffics = $statService->getStatUserByUserID($request->input('user_id'));
if (($current == 1) && count($todayTraffics) > 0) {
foreach ($todayTraffics as $todayTraffic){
$todayTraffic['server_rate'] = number_format($todayTraffic['server_rate'], 2);
$records->prepend($todayTraffic);
}
};
return [
'data' => $records,
'total' => $total + count($todayTraffics),
];
}
}

View File

@ -0,0 +1,119 @@
<?php
namespace App\Http\Controllers\V1\Admin;
use App\Http\Controllers\Controller;
use App\Models\Log as LogModel;
use App\Utils\CacheKey;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Laravel\Horizon\Contracts\JobRepository;
use Laravel\Horizon\Contracts\MasterSupervisorRepository;
use Laravel\Horizon\Contracts\MetricsRepository;
use Laravel\Horizon\Contracts\SupervisorRepository;
use Laravel\Horizon\Contracts\WorkloadRepository;
use Laravel\Horizon\WaitTimeCalculator;
class SystemController extends Controller
{
public function getSystemStatus()
{
return response([
'data' => [
'schedule' => $this->getScheduleStatus(),
'horizon' => $this->getHorizonStatus(),
'schedule_last_runtime' => Cache::get(CacheKey::get('SCHEDULE_LAST_CHECK_AT', null))
]
]);
}
public function getQueueWorkload(WorkloadRepository $workload)
{
return response([
'data' => 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()
{
return response([
'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),
]
]);
}
/**
* 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 getSystemLog(Request $request) {
$current = $request->input('current') ? $request->input('current') : 1;
$pageSize = $request->input('page_size') >= 10 ? $request->input('page_size') : 10;
$builder = LogModel::orderBy('created_at', 'DESC')
->setFilterAllowKeys('level');
$total = $builder->count();
$res = $builder->forPage($current, $pageSize)
->get();
return response([
'data' => $res,
'total' => $total
]);
}
}

View File

@ -0,0 +1,87 @@
<?php
namespace App\Http\Controllers\V1\Admin;
use App\Http\Controllers\Controller;
use App\Services\ThemeService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\File;
class ThemeController extends Controller
{
private $themes;
private $path;
public function __construct()
{
$this->path = $path = public_path('theme/');
$this->themes = array_map(function ($item) use ($path) {
return str_replace($path, '', $item);
}, glob($path . '*'));
}
public function getThemes()
{
$themeConfigs = [];
foreach ($this->themes as $theme) {
$themeConfigFile = $this->path . "{$theme}/config.json";
if (!File::exists($themeConfigFile)) continue;
$themeConfig = json_decode(File::get($themeConfigFile), true);
if (!isset($themeConfig['configs']) || !is_array($themeConfig)) continue;
$themeConfigs[$theme] = $themeConfig;
if (admin_setting("theme_{$theme}")) continue;
$themeService = new ThemeService($theme);
$themeService->init();
}
return response([
'data' => [
'themes' => $themeConfigs,
'active' => admin_setting('frontend_theme', 'Xboard')
]
]);
}
public function getThemeConfig(Request $request)
{
$payload = $request->validate([
'name' => 'required|in:' . join(',', $this->themes)
]);
return response([
'data' => admin_setting("theme_{$payload['name']}") ?? config("theme.{$payload['name']}")
]);
}
public function saveThemeConfig(Request $request)
{
$payload = $request->validate([
'name' => 'required|in:' . join(',', $this->themes),
'config' => 'required'
]);
$payload['config'] = json_decode(base64_decode($payload['config']), true);
if (!$payload['config'] || !is_array($payload['config'])) abort(500, '参数有误');
$themeConfigFile = public_path("theme/{$payload['name']}/config.json");
if (!File::exists($themeConfigFile)) abort(500, '主题不存在');
$themeConfig = json_decode(File::get($themeConfigFile), true);
if (!isset($themeConfig['configs']) || !is_array($themeConfig)) abort(500, '主题配置文件有误');
$validateFields = array_column($themeConfig['configs'], 'field_name');
$config = [];
foreach ($validateFields as $validateField) {
$config[$validateField] = isset($payload['config'][$validateField]) ? $payload['config'][$validateField] : '';
}
File::ensureDirectoryExists(base_path() . '/config/theme/');
// $data = var_export($config, 1);
try {
admin_setting(["theme_{$payload['name']}" => $config]);
// sleep(2);
} catch (\Exception $e) {
abort(500, '保存失败');
}
return response([
'data' => $config
]);
}
}

View File

@ -0,0 +1,95 @@
<?php
namespace App\Http\Controllers\V1\Admin;
use App\Http\Controllers\Controller;
use App\Models\Ticket;
use App\Models\TicketMessage;
use App\Models\User;
use App\Services\TicketService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class TicketController extends Controller
{
public function fetch(Request $request)
{
if ($request->input('id')) {
$ticket = Ticket::where('id', $request->input('id'))
->first();
if (!$ticket) {
abort(500, '工单不存在');
}
$ticket['message'] = TicketMessage::where('ticket_id', $ticket->id)->get();
for ($i = 0; $i < count($ticket['message']); $i++) {
if ($ticket['message'][$i]['user_id'] !== $ticket->user_id) {
$ticket['message'][$i]['is_me'] = true;
} else {
$ticket['message'][$i]['is_me'] = false;
}
}
return response([
'data' => $ticket
]);
}
$current = $request->input('current') ? $request->input('current') : 1;
$pageSize = $request->input('pageSize') >= 10 ? $request->input('pageSize') : 10;
$model = Ticket::orderBy('updated_at', 'DESC');
if ($request->input('status') !== NULL) {
$model->where('status', $request->input('status'));
}
if ($request->input('reply_status') !== NULL) {
$model->whereIn('reply_status', $request->input('reply_status'));
}
if ($request->input('email') !== NULL) {
$user = User::where('email', $request->input('email'))->first();
if ($user) $model->where('user_id', $user->id);
}
$total = $model->count();
$res = $model->forPage($current, $pageSize)
->get();
return response([
'data' => $res,
'total' => $total
]);
}
public function reply(Request $request)
{
if (empty($request->input('id'))) {
abort(500, '参数错误');
}
if (empty($request->input('message'))) {
abort(500, '消息不能为空');
}
$ticketService = new TicketService();
$ticketService->replyByAdmin(
$request->input('id'),
$request->input('message'),
$request->user['id']
);
return response([
'data' => true
]);
}
public function close(Request $request)
{
if (empty($request->input('id'))) {
abort(500, '参数错误');
}
$ticket = Ticket::where('id', $request->input('id'))
->first();
if (!$ticket) {
abort(500, '工单不存在');
}
$ticket->status = 1;
if (!$ticket->save()) {
abort(500, '关闭失败');
}
return response([
'data' => true
]);
}
}

View File

@ -0,0 +1,293 @@
<?php
namespace App\Http\Controllers\V1\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\UserFetch;
use App\Http\Requests\Admin\UserGenerate;
use App\Http\Requests\Admin\UserSendMail;
use App\Http\Requests\Admin\UserUpdate;
use App\Jobs\SendEmailJob;
use App\Models\Plan;
use App\Models\User;
use App\Services\AuthService;
use App\Utils\Helper;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class UserController extends Controller
{
public function resetSecret(Request $request)
{
$user = User::find($request->input('id'));
if (!$user) abort(500, '用户不存在');
$user->token = Helper::guid();
$user->uuid = Helper::guid(true);
return response([
'data' => $user->save()
]);
}
private function filter(Request $request, $builder)
{
$filters = $request->input('filter');
if ($filters) {
foreach ($filters as $k => $filter) {
if ($filter['condition'] === '模糊') {
$filter['condition'] = 'like';
$filter['value'] = "%{$filter['value']}%";
}
if ($filter['key'] === 'd' || $filter['key'] === 'transfer_enable') {
$filter['value'] = $filter['value'] * 1073741824;
}
if ($filter['key'] === 'invite_by_email') {
$user = User::where('email', $filter['condition'], $filter['value'])->first();
$inviteUserId = isset($user->id) ? $user->id : 0;
$builder->where('invite_user_id', $inviteUserId);
unset($filters[$k]);
continue;
}
$builder->where($filter['key'], $filter['condition'], $filter['value']);
}
}
}
public function fetch(UserFetch $request)
{
$current = $request->input('current') ? $request->input('current') : 1;
$pageSize = $request->input('pageSize') >= 10 ? $request->input('pageSize') : 10;
$sortType = in_array($request->input('sort_type'), ['ASC', 'DESC']) ? $request->input('sort_type') : 'DESC';
$sort = $request->input('sort') ? $request->input('sort') : 'created_at';
$userModel = User::select(
DB::raw('*'),
DB::raw('(u+d) as total_used')
)
->orderBy($sort, $sortType);
$this->filter($request, $userModel);
$total = $userModel->count();
$res = $userModel->forPage($current, $pageSize)
->get();
$plan = Plan::get();
for ($i = 0; $i < count($res); $i++) {
for ($k = 0; $k < count($plan); $k++) {
if ($plan[$k]['id'] == $res[$i]['plan_id']) {
$res[$i]['plan_name'] = $plan[$k]['name'];
}
}
$res[$i]['subscribe_url'] = Helper::getSubscribeUrl('/api/v1/client/subscribe?token=' . $res[$i]['token']);
}
return response([
'data' => $res,
'total' => $total
]);
}
public function getUserInfoById(Request $request)
{
if (empty($request->input('id'))) {
abort(500, '参数错误');
}
$user = User::find($request->input('id'));
if ($user->invite_user_id) {
$user['invite_user'] = User::find($user->invite_user_id);
}
return response([
'data' => $user
]);
}
public function update(UserUpdate $request)
{
$params = $request->validated();
$user = User::find($request->input('id'));
if (!$user) {
abort(500, '用户不存在');
}
if (User::where('email', $params['email'])->first() && $user->email !== $params['email']) {
abort(500, '邮箱已被使用');
}
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) {
abort(500, '订阅计划不存在');
}
$params['group_id'] = $plan->group_id;
}
if ($request->input('invite_user_email')) {
$inviteUser = User::where('email', $request->input('invite_user_email'))->first();
if ($inviteUser) {
$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->removeAllSession();
}
try {
$user->update($params);
} catch (\Exception $e) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
public function dumpCSV(Request $request)
{
$userModel = User::orderBy('id', 'asc');
$this->filter($request, $userModel);
$res = $userModel->get();
$plan = Plan::get();
for ($i = 0; $i < count($res); $i++) {
for ($k = 0; $k < count($plan); $k++) {
if ($plan[$k]['id'] == $res[$i]['plan_id']) {
$res[$i]['plan_name'] = $plan[$k]['name'];
}
}
}
$data = "邮箱,余额,推广佣金,总流量,剩余流量,套餐到期时间,订阅计划,订阅地址\r\n";
foreach($res as $user) {
$expireDate = $user['expired_at'] === NULL ? '长期有效' : date('Y-m-d H:i:s', $user['expired_at']);
$balance = $user['balance'] / 100;
$commissionBalance = $user['commission_balance'] / 100;
$transferEnable = $user['transfer_enable'] ? $user['transfer_enable'] / 1073741824 : 0;
$notUseFlow = (($user['transfer_enable'] - ($user['u'] + $user['d'])) / 1073741824) ?? 0;
$planName = $user['plan_name'] ?? '无订阅';
$subscribeUrl = Helper::getSubscribeUrl('/api/v1/client/subscribe?token=' . $user['token']);
$data .= "{$user['email']},{$balance},{$commissionBalance},{$transferEnable},{$notUseFlow},{$expireDate},{$planName},{$subscribeUrl}\r\n";
}
echo "\xEF\xBB\xBF" . $data;
}
public function generate(UserGenerate $request)
{
if ($request->input('email_prefix')) {
if ($request->input('plan_id')) {
$plan = Plan::find($request->input('plan_id'));
if (!$plan) {
abort(500, '订阅计划不存在');
}
}
$user = [
'email' => $request->input('email_prefix') . '@' . $request->input('email_suffix'),
'plan_id' => isset($plan->id) ? $plan->id : NULL,
'group_id' => isset($plan->group_id) ? $plan->group_id : NULL,
'transfer_enable' => isset($plan->transfer_enable) ? $plan->transfer_enable * 1073741824 : 0,
'expired_at' => $request->input('expired_at') ?? NULL,
'uuid' => Helper::guid(true),
'token' => Helper::guid()
];
if (User::where('email', $user['email'])->first()) {
abort(500, '邮箱已存在于系统中');
}
$user['password'] = password_hash($request->input('password') ?? $user['email'], PASSWORD_DEFAULT);
if (!User::create($user)) {
abort(500, '生成失败');
}
return response([
'data' => true
]);
}
if ($request->input('generate_count')) {
$this->multiGenerate($request);
}
}
private function multiGenerate(Request $request)
{
if ($request->input('plan_id')) {
$plan = Plan::find($request->input('plan_id'));
if (!$plan) {
abort(500, '订阅计划不存在');
}
}
$users = [];
for ($i = 0;$i < $request->input('generate_count');$i++) {
$user = [
'email' => Helper::randomChar(6) . '@' . $request->input('email_suffix'),
'plan_id' => isset($plan->id) ? $plan->id : NULL,
'group_id' => isset($plan->group_id) ? $plan->group_id : NULL,
'transfer_enable' => isset($plan->transfer_enable) ? $plan->transfer_enable * 1073741824 : 0,
'expired_at' => $request->input('expired_at') ?? NULL,
'uuid' => Helper::guid(true),
'token' => Helper::guid(),
'created_at' => time(),
'updated_at' => time()
];
$user['password'] = password_hash($request->input('password') ?? $user['email'], PASSWORD_DEFAULT);
array_push($users, $user);
}
DB::beginTransaction();
if (!User::insert($users)) {
DB::rollBack();
abort(500, '生成失败');
}
DB::commit();
$data = "账号,密码,过期时间,UUID,创建时间,订阅地址\r\n";
foreach($users as $user) {
$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('/api/v1/client/subscribe?token=' . $user['token']);
$data .= "{$user['email']},{$password},{$expireDate},{$user['uuid']},{$createDate},{$subscribeUrl}\r\n";
}
echo $data;
}
public function sendMail(UserSendMail $request)
{
$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::orderBy($sort, $sortType);
$this->filter($request, $builder);
$users = $builder->get();
foreach ($users as $user) {
SendEmailJob::dispatch([
'email' => $user->email,
'subject' => $request->input('subject'),
'template_name' => 'notify',
'template_value' => [
'name' => admin_setting('app_name', 'XBoard'),
'url' => admin_setting('app_url'),
'content' => $request->input('content')
]
],
'send_email_mass');
}
return response([
'data' => true
]);
}
public function ban(Request $request)
{
$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::orderBy($sort, $sortType);
$this->filter($request, $builder);
try {
$builder->update([
'banned' => 1
]);
} catch (\Exception $e) {
abort(500, '处理失败');
}
return response([
'data' => true
]);
}
}

View File

@ -0,0 +1,95 @@
<?php
namespace App\Http\Controllers\V1\Client;
use App\Http\Controllers\Controller;
use App\Services\ServerService;
use App\Services\UserService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\File;
use Symfony\Component\Yaml\Yaml;
class AppController extends Controller
{
public function getConfig(Request $request)
{
$servers = [];
$user = $request->user;
$userService = new UserService();
if ($userService->isAvailable($user)) {
$serverService = new ServerService();
$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) {
if ($item['type'] === 'shadowsocks'
&& in_array($item['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\Http\Controllers\V1\Client\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) {
return response([
'data' => [
'version' => admin_setting('windows_version'),
'download_url' => admin_setting('windows_download_url')
]
]);
} else {
return response([
'data' => [
'version' => admin_setting('macos_version'),
'download_url' => admin_setting('macos_download_url')
]
]);
}
return;
}
return response([
'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')
]
]);
}
}

View File

@ -0,0 +1,193 @@
<?php
namespace App\Http\Controllers\V1\Client;
use App\Http\Controllers\Controller;
use App\Protocols\General;
use App\Services\ServerService;
use App\Services\UserService;
use App\Utils\Helper;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class ClientController extends Controller
{
public function subscribe(Request $request)
{
// 节点类型筛选
$allowedTypes = ['vmess', 'vless', 'trojan', 'hysteria', 'hysteria2', 'shadowsocks'];
$types = $request->input('types', "vmess|vless|trojan|hysteria|shadowsocks");
$typesArr = $types ? collect(explode('|', str_replace(['|','',','], "|" , $types)))->reject(function($type) use ($allowedTypes){
return !in_array($type, $allowedTypes);
})->values()->all() : [];
// 节点关键词筛选字段获取
$filterArr = (mb_strlen($request->input('filter')) > 20) ? null : explode("|" ,str_replace(['|','',','], "|" , $request->input('filter')));
$flag = $request->input('flag') ?? $request->header('User-Agent', '');
$ip = $request->input('ip') ?? $request->ip();
preg_match('/(?<=\/)(\d+\.\d+\.\d+)|(?<=\/)(\d+)/', $flag, $matches);
$version = $matches[0]??null;
$isNekoBox = (stripos($flag, 'NekoBox') !== false); //判断是否为Nekobox客户端
$isSingBox = (stripos($flag, 'sing-box') !== false); //判断是否为stash客户端
$isShadowsocket = (stripos($flag, 'Shadowrocket') !== false); //判断是否为shadowsocket客户端
$isStash = (strpos($flag, 'Stash') !== false);
if(config('app.debug')){
Log::channel('daily')->info($flag);
}
$flag = strtolower($flag);
$user = $request->user;
// account not expired and is not banned.
$userService = new UserService();
if ($userService->isAvailable($user)) {
// 获取IP地址信息
$ip2region = new \Ip2Region();
$geo = filter_var($ip,FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) ? $ip2region->memorySearch($ip) : [];
$region = $geo['region'] ?? null;
// 获取服务器列表
$serverService = new ServerService();
$servers = $serverService->getAvailableServers($user);
// 判断不满足,不满足的直接过滤掉
$serversFiltered = collect($servers)->reject(function ($server) use ($typesArr, $filterArr, $region, $isNekoBox, $isStash, $version, $isSingBox, $isShadowsocket){
// 过滤类型
if($typesArr){
// 默认过滤掉hysteria2 线路
if($server['type'] == "hysteria" && $server['version'] == 2 && !in_array('hysteria2', $typesArr)
&& !($isNekoBox && $this->versionCompare($version, '1.2.7')) //1.2.7<=版本 自动下发hy2
&& !($isSingBox && $this->versionCompare($version, '1.5.0')) //1.5.0<=版本 自动下发hy2
&& !($isStash && $this->versionCompare($version, '2.5.0' )) //2.5.0或者以上版本自动下发hy2
&& !($isShadowsocket && $this->versionCompare($version, 1993)) //1993 版本或者以上的shadowsocket下发hy2
){
return true;
}
if(!in_array($server['type'], $typesArr) && !($server['type'] == "hysteria" && $server['version'] == 2 && in_array('hysteria2', $typesArr))) return true;
}
// 过滤关键词
if($filterArr){
$rejectFlag = true;
foreach($filterArr as $filter){
if(strpos($server['name'],$filter) !== false) $rejectFlag = false;
}
if($rejectFlag) return true;
}
// 过滤地区
if(strpos($region, '中国') !== false){
$excludes = $server['excludes'];
if(blank($excludes)) return false;
foreach($excludes as $v){
$excludeList = explode("|",str_replace(["",","," ",""],"|",$v));
$rejectFlag = false;
foreach($excludeList as $needle){
if(strpos($region, $needle) !== false){
return true;
}
}
};
}
})->values()->all();
$this->setSubscribeInfoToServers($serversFiltered, $user, count($servers) - count($serversFiltered));
$servers = $serversFiltered;
// 线路名称增加协议类型
if (admin_setting('show_protocol_to_server_enable')){
$typePrefixes = [
'hysteria' => [1 => '[Hy]', 2 => '[Hy2]'],
'vless' => '[vless]',
'shadowsocks' => '[ss]',
'vmess' => '[vmess]',
'trojan' => '[trojan]',
];
$servers = collect($servers)->map(function($server)use ($typePrefixes){
if (isset($typePrefixes[$server['type']])) {
// 如果是 hysteria 类型,根据版本选择前缀
$prefix = is_array($typePrefixes[$server['type']]) ? $typePrefixes[$server['type']][$server['version']] : $typePrefixes[$server['type']];
// 设置服务器名称
$server['name'] = $prefix . $server['name'];
}
return $server;
})->toArray();
}
if ($flag) {
foreach (array_reverse(glob(app_path('Protocols') . '/*.php')) as $file) {
$file = 'App\\Protocols\\' . basename($file, '.php');
$class = new $file($user, $servers);
$classFlags = explode(',', $class->flag);
$isMatch = function() use ($classFlags, $flag){
foreach ($classFlags as $classFlag){
if(strpos($flag, $classFlag) !== false) return true;
}
return false;
};
// 判断是否匹配
if ($isMatch()) {
return $class->handle();
}
}
}
$class = new General($user, $servers);
return $class->handle();
}
}
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}",
]));
}
/**
* 判断版本号
*/
function versionCompare($version1, $version2) {
if (!preg_match('/^\d+\.\d+\.\d+$|\d/', $version1) || !preg_match('/^\d+\.\d+\.\d+$|\d/', $version2)) {
return false;
}
$v1Parts = explode('.', $version1);
$v2Parts = explode('.', $version2);
$maxParts = max(count($v1Parts), count($v2Parts));
for ($i = 0; $i < $maxParts; $i++) {
$part1 = isset($v1Parts[$i]) ? (int)$v1Parts[$i] : 0;
$part2 = isset($v2Parts[$i]) ? (int)$v2Parts[$i] : 0;
if ($part1 < $part2) {
return false;
} elseif ($part1 > $part2) {
return true;
}
}
// 版本号相等
return true;
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace App\Http\Controllers\V1\Guest;
use App\Http\Controllers\Controller;
use App\Utils\Dict;
use Illuminate\Support\Facades\Http;
class CommController extends Controller
{
public function config()
{
return response([
'data' => [
'tos_url' => 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)
? $this->getEmailSuffix()
: 0,
'is_recaptcha' => (int)admin_setting('recaptcha_enable', 0) ? 1 : 0,
'recaptcha_site_key' => admin_setting('recaptcha_site_key'),
'app_description' => admin_setting('app_description'),
'app_url' => admin_setting('app_url'),
'logo' => admin_setting('logo'),
]
]);
}
private function getEmailSuffix()
{
$suffix = admin_setting('email_whitelist_suffix', Dict::EMAIL_WHITELIST_SUFFIX_DEFAULT);
if (!is_array($suffix)) {
return preg_split('/,/', $suffix);
}
return $suffix;
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace App\Http\Controllers\V1\Guest;
use App\Http\Controllers\Controller;
use App\Models\Order;
use App\Services\OrderService;
use App\Services\PaymentService;
use App\Services\TelegramService;
use Illuminate\Http\Request;
class PaymentController extends Controller
{
public function notify($method, $uuid, Request $request)
{
try {
$paymentService = new PaymentService($method, null, $uuid);
$verify = $paymentService->notify($request->input());
if (!$verify) abort(500, 'verify error');
if (!$this->handle($verify['trade_no'], $verify['callback_no'])) {
abort(500, 'handle error');
}
return(isset($verify['custom_result']) ? $verify['custom_result'] : 'success');
} catch (\Exception $e) {
abort(500, 'fail');
}
}
private function handle($tradeNo, $callbackNo)
{
$order = Order::where('trade_no', $tradeNo)->first();
if (!$order) {
abort(500, 'order is not found');
}
if ($order->status !== 0) return true;
$orderService = new OrderService($order);
if (!$orderService->paid($callbackNo)) {
return false;
}
$telegramService = new TelegramService();
$message = sprintf(
"💰成功收款%s元\n———————————————\n订单号:%s",
$order->total_amount / 100,
$order->trade_no
);
$telegramService->sendMessageWithAdmin($message);
return true;
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace App\Http\Controllers\V1\Guest;
use App\Http\Controllers\Controller;
use App\Models\Plan;
use Illuminate\Http\Request;
class PlanController extends Controller
{
public function fetch(Request $request)
{
$plan = Plan::where('show', 1)->get();
return response([
'data' => $plan
]);
}
}

View File

@ -0,0 +1,123 @@
<?php
namespace App\Http\Controllers\V1\Guest;
use App\Http\Controllers\Controller;
use App\Services\TelegramService;
use Illuminate\Http\Request;
class TelegramController extends Controller
{
protected $msg;
protected $commands = [];
protected $telegramService;
public function __construct(Request $request)
{
if ($request->input('access_token') !== md5(admin_setting('telegram_bot_token'))) {
abort(401);
}
$this->telegramService = new TelegramService();
}
public function webhook(Request $request)
{
$this->formatMessage($request->input());
$this->formatChatJoinRequest($request->input());
$this->handle();
}
public function handle()
{
if (!$this->msg) return;
$msg = $this->msg;
$commandName = explode('@', $msg->command);
// To reduce request, only commands contains @ will get the bot name
if (count($commandName) == 2) {
$botName = $this->getBotName();
if ($commandName[1] === $botName){
$msg->command = $commandName[0];
}
}
try {
foreach (glob(base_path('app//Plugins//Telegram//Commands') . '/*.php') as $file) {
$command = basename($file, '.php');
$class = '\\App\\Plugins\\Telegram\\Commands\\' . $command;
if (!class_exists($class)) continue;
$instance = new $class();
if ($msg->message_type === 'message') {
if (!isset($instance->command)) continue;
if ($msg->command !== $instance->command) continue;
$instance->handle($msg);
return;
}
if ($msg->message_type === 'reply_message') {
if (!isset($instance->regex)) continue;
if (!preg_match($instance->regex, $msg->reply_text, $match)) continue;
$instance->handle($msg, $match);
return;
}
}
} catch (\Exception $e) {
$this->telegramService->sendMessage($msg->chat_id, $e->getMessage());
}
}
public function getBotName()
{
$response = $this->telegramService->getMe();
return $response->result->username;
}
private function formatMessage(array $data)
{
if (!isset($data['message'])) return;
if (!isset($data['message']['text'])) return;
$obj = new \StdClass();
$text = explode(' ', $data['message']['text']);
$obj->command = $text[0];
$obj->args = array_slice($text, 1);
$obj->chat_id = $data['message']['chat']['id'];
$obj->message_id = $data['message']['message_id'];
$obj->message_type = 'message';
$obj->text = $data['message']['text'];
$obj->is_private = $data['message']['chat']['type'] === 'private';
if (isset($data['message']['reply_to_message']['text'])) {
$obj->message_type = 'reply_message';
$obj->reply_text = $data['message']['reply_to_message']['text'];
}
$this->msg = $obj;
}
private function formatChatJoinRequest(array $data)
{
if (!isset($data['chat_join_request'])) return;
if (!isset($data['chat_join_request']['from']['id'])) return;
if (!isset($data['chat_join_request']['chat']['id'])) return;
$user = \App\Models\User::where('telegram_id', $data['chat_join_request']['from']['id'])
->first();
if (!$user) {
$this->telegramService->declineChatJoinRequest(
$data['chat_join_request']['chat']['id'],
$data['chat_join_request']['from']['id']
);
return;
}
$userService = new \App\Services\UserService();
if (!$userService->isAvailable($user)) {
$this->telegramService->declineChatJoinRequest(
$data['chat_join_request']['chat']['id'],
$data['chat_join_request']['from']['id']
);
return;
}
$userService = new \App\Services\UserService();
$this->telegramService->approveChatJoinRequest(
$data['chat_join_request']['chat']['id'],
$data['chat_join_request']['from']['id']
);
}
}

View File

@ -0,0 +1,311 @@
<?php
namespace App\Http\Controllers\V1\Passport;
use App\Http\Controllers\Controller;
use App\Http\Requests\Passport\AuthForget;
use App\Http\Requests\Passport\AuthLogin;
use App\Http\Requests\Passport\AuthRegister;
use App\Jobs\SendEmailJob;
use App\Models\InviteCode;
use App\Models\Plan;
use App\Models\User;
use App\Services\AuthService;
use App\Utils\CacheKey;
use App\Utils\Dict;
use App\Utils\Helper;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use ReCaptcha\ReCaptcha;
class AuthController extends Controller
{
public function loginWithMailLink(Request $request)
{
if (!(int)admin_setting('login_with_mail_link_enable')) {
abort(404);
}
$params = $request->validate([
'email' => 'required|email:strict',
'redirect' => 'nullable'
]);
if (Cache::get(CacheKey::get('LAST_SEND_LOGIN_WITH_MAIL_LINK_TIMESTAMP', $params['email']))) {
abort(500, __('Sending frequently, please try again later'));
}
$user = User::where('email', $params['email'])->first();
if (!$user) {
return response([
'data' => 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', $params['email']), time(), 60);
$redirect = '/#/login?verify=' . $code . '&redirect=' . ($request->input('redirect') ? $request->input('redirect') : 'dashboard');
if (admin_setting('app_url')) {
$link = admin_setting('app_url') . $redirect;
} else {
$link = url($redirect);
}
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')
]
]);
return response([
'data' => $link
]);
}
public function register(AuthRegister $request)
{
if ((int)admin_setting('register_limit_by_ip_enable', 0)) {
$registerCountByIP = Cache::get(CacheKey::get('REGISTER_IP_RATE_LIMIT', $request->ip())) ?? 0;
if ((int)$registerCountByIP >= (int)admin_setting('register_limit_count', 3)) {
abort(500, __('Register frequently, please try again after :minute minute', [
'minute' => admin_setting('register_limit_expire', 60)
]));
}
}
if ((int)admin_setting('recaptcha_enable', 0)) {
$recaptcha = new ReCaptcha(admin_setting('recaptcha_key'));
$recaptchaResp = $recaptcha->verify($request->input('recaptcha_data'));
if (!$recaptchaResp->isSuccess()) {
abort(500, __('Invalid code is incorrect'));
}
}
if ((int)admin_setting('email_whitelist_enable', 0)) {
if (!Helper::emailSuffixVerify(
$request->input('email'),
admin_setting('email_whitelist_suffix', Dict::EMAIL_WHITELIST_SUFFIX_DEFAULT))
) {
abort(500, __('Email suffix is not in the Whitelist'));
}
}
if ((int)admin_setting('email_gmail_limit_enable', 0)) {
$prefix = explode('@', $request->input('email'))[0];
if (strpos($prefix, '.') !== false || strpos($prefix, '+') !== false) {
abort(500, __('Gmail alias is not supported'));
}
}
if ((int)admin_setting('stop_register', 0)) {
abort(500, __('Registration has closed'));
}
if ((int)admin_setting('invite_force', 0)) {
if (empty($request->input('invite_code'))) {
abort(500, __('You must use the invitation code to register'));
}
}
if ((int)admin_setting('email_verify', 0)) {
if (empty($request->input('email_code'))) {
abort(500, __('Email verification code cannot be empty'));
}
if ((string)Cache::get(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email'))) !== (string)$request->input('email_code')) {
abort(500, __('Incorrect email verification code'));
}
}
$email = $request->input('email');
$password = $request->input('password');
$exist = User::where('email', $email)->first();
if ($exist) {
abort(500, __('Email already exists'));
}
$user = new User();
$user->email = $email;
$user->password = password_hash($password, PASSWORD_DEFAULT);
$user->uuid = Helper::guid(true);
$user->token = Helper::guid();
if ($request->input('invite_code')) {
$inviteCode = InviteCode::where('code', $request->input('invite_code'))
->where('status', 0)
->first();
if (!$inviteCode) {
if ((int)admin_setting('invite_force', 0)) {
abort(500, __('Invalid invitation code'));
}
} else {
$user->invite_user_id = $inviteCode->user_id ? $inviteCode->user_id : null;
if (!(int)admin_setting('invite_never_expire', 0)) {
$inviteCode->status = 1;
$inviteCode->save();
}
}
}
// try out
if ((int)admin_setting('try_out_plan_id', 0)) {
$plan = Plan::find(admin_setting('try_out_plan_id'));
if ($plan) {
$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;
}
}
if (!$user->save()) {
abort(500, __('Register failed'));
}
if ((int)admin_setting('email_verify', 0)) {
Cache::forget(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email')));
}
$user->last_login_at = time();
$user->save();
if ((int)admin_setting('register_limit_by_ip_enable', 0)) {
Cache::put(
CacheKey::get('REGISTER_IP_RATE_LIMIT', $request->ip()),
(int)$registerCountByIP + 1,
(int)admin_setting('register_limit_expire', 60) * 60
);
}
$authService = new AuthService($user);
return response()->json([
'data' => $authService->generateAuthData($request)
]);
}
public function login(AuthLogin $request)
{
$email = $request->input('email');
$password = $request->input('password');
if ((int)admin_setting('password_limit_enable', 1)) {
$passwordErrorCount = (int)Cache::get(CacheKey::get('PASSWORD_ERROR_LIMIT', $email), 0);
if ($passwordErrorCount >= (int)admin_setting('password_limit_count', 5)) {
abort(500, __('There are too many password errors, please try again after :minute minutes.', [
'minute' => admin_setting('password_limit_expire', 60)
]));
}
}
$user = User::where('email', $email)->first();
if (!$user) {
abort(500, __('Incorrect email or password'));
}
if (!Helper::multiPasswordVerify(
$user->password_algo,
$user->password_salt,
$password,
$user->password)
) {
if ((int)admin_setting('password_limit_enable')) {
Cache::put(
CacheKey::get('PASSWORD_ERROR_LIMIT', $email),
(int)$passwordErrorCount + 1,
60 * (int)admin_setting('password_limit_expire', 60)
);
}
abort(500, __('Incorrect email or password'));
}
if ($user->banned) {
abort(500, __('Your account has been suspended'));
}
$authService = new AuthService($user);
return response([
'data' => $authService->generateAuthData($request)
]);
}
public function token2Login(Request $request)
{
if ($request->input('token')) {
$redirect = '/#/login?verify=' . $request->input('token') . '&redirect=' . ($request->input('redirect') ? $request->input('redirect') : 'dashboard');
if (admin_setting('app_url')) {
$location = admin_setting('app_url') . $redirect;
} else {
$location = url($redirect);
}
return redirect()->to($location)->send();
}
if ($request->input('verify')) {
$key = CacheKey::get('TEMP_TOKEN', $request->input('verify'));
$userId = Cache::get($key);
if (!$userId) {
abort(500, __('Token error'));
}
$user = User::find($userId);
if (!$user) {
abort(500, __('The user does not '));
}
if ($user->banned) {
abort(500, __('Your account has been suspended'));
}
Cache::forget($key);
$authService = new AuthService($user);
return response([
'data' => $authService->generateAuthData($request)
]);
}
}
public function getQuickLoginUrl(Request $request)
{
$authorization = $request->input('auth_data') ?? $request->header('authorization');
if (!$authorization) abort(403, '未登录或登陆已过期');
$user = AuthService::decryptAuthData($authorization);
if (!$user) abort(403, '未登录或登陆已过期');
$code = Helper::guid();
$key = CacheKey::get('TEMP_TOKEN', $code);
Cache::put($key, $user['id'], 60);
$redirect = '/#/login?verify=' . $code . '&redirect=' . ($request->input('redirect') ? $request->input('redirect') : 'dashboard');
if (admin_setting('app_url')) {
$url = admin_setting('app_url') . $redirect;
} else {
$url = url($redirect);
}
return response([
'data' => $url
]);
}
public function forget(AuthForget $request)
{
$forgetRequestLimitKey = CacheKey::get('FORGET_REQUEST_LIMIT', $request->input('email'));
$forgetRequestLimit = (int)Cache::get($forgetRequestLimitKey);
if ($forgetRequestLimit >= 3) abort(500, __('Reset failed, Please try again later'));
if ((string)Cache::get(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email'))) !== (string)$request->input('email_code')) {
Cache::put($forgetRequestLimitKey, $forgetRequestLimit ? $forgetRequestLimit + 1 : 1, 300);
abort(500, __('Incorrect email verification code'));
}
$user = User::where('email', $request->input('email'))->first();
if (!$user) {
abort(500, __('This email is not registered in the system'));
}
$user->password = password_hash($request->input('password'), PASSWORD_DEFAULT);
$user->password_algo = NULL;
$user->password_salt = NULL;
if (!$user->save()) {
abort(500, __('Reset failed'));
}
Cache::forget(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email')));
return response([
'data' => true
]);
}
}

View File

@ -0,0 +1,82 @@
<?php
namespace App\Http\Controllers\V1\Passport;
use App\Http\Controllers\Controller;
use App\Http\Requests\Passport\CommSendEmailVerify;
use App\Jobs\SendEmailJob;
use App\Models\InviteCode;
use App\Models\User;
use App\Utils\CacheKey;
use App\Utils\Dict;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Mail;
use ReCaptcha\ReCaptcha;
class CommController extends Controller
{
private function isEmailVerify()
{
return response([
'data' => (int)admin_setting('email_verify', 0) ? 1 : 0
]);
}
public function sendEmailVerify(CommSendEmailVerify $request)
{
if ((int)admin_setting('recaptcha_enable', 0)) {
$recaptcha = new ReCaptcha(admin_setting('recaptcha_key'));
$recaptchaResp = $recaptcha->verify($request->input('recaptcha_data'));
if (!$recaptchaResp->isSuccess()) {
abort(500, __('Invalid code is incorrect'));
}
}
$email = $request->input('email');
if (Cache::get(CacheKey::get('LAST_SEND_EMAIL_VERIFY_TIMESTAMP', $email))) {
abort(500, __('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 response([
'data' => 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 response([
'data' => true
]);
}
private function getEmailSuffix()
{
$suffix = admin_setting('email_whitelist_suffix', Dict::EMAIL_WHITELIST_SUFFIX_DEFAULT);
if (!is_array($suffix)) {
return preg_split('/,/', $suffix);
}
return $suffix;
}
}

View File

@ -0,0 +1,231 @@
<?php
namespace App\Http\Controllers\V1\Server;
use App\Http\Controllers\Controller;
use App\Models\ServerVmess;
use App\Services\ServerService;
use App\Services\UserService;
use App\Utils\CacheKey;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
/*
* V2ray Aurora
* Github: https://github.com/tokumeikoi/aurora
*/
class DeepbworkController extends Controller
{
CONST V2RAY_CONFIG = '{"log":{"loglevel":"debug","access":"access.log","error":"error.log"},"api":{"services":["HandlerService","StatsService"],"tag":"api"},"dns":{},"stats":{},"inbounds":[{"port":443,"protocol":"vmess","settings":{"clients":[]},"sniffing":{"enabled":true,"destOverride":["http","tls"]},"streamSettings":{"network":"tcp"},"tag":"proxy"},{"listen":"127.0.0.1","port":23333,"protocol":"dokodemo-door","settings":{"address":"0.0.0.0"},"tag":"api"}],"outbounds":[{"protocol":"freedom","settings":{}},{"protocol":"blackhole","settings":{},"tag":"block"}],"routing":{"rules":[{"type":"field","inboundTag":"api","outboundTag":"api"}]},"policy":{"levels":{"0":{"handshake":4,"connIdle":300,"uplinkOnly":5,"downlinkOnly":30,"statsUserUplink":true,"statsUserDownlink":true}}}}';
public function __construct(Request $request)
{
$token = $request->input('token');
if (empty($token)) {
abort(500, 'token is null');
}
if ($token !== admin_setting('server_token')) {
abort(500, 'token is error');
}
}
// 后端获取用户
public function user(Request $request)
{
ini_set('memory_limit', -1);
$nodeId = $request->input('node_id');
$server = ServerVmess::find($nodeId);
if (!$server) {
abort(500, 'fail');
}
Cache::put(CacheKey::get('SERVER_VMESS_LAST_CHECK_AT', $server->id), time(), 3600);
$serverService = new ServerService();
$users = $serverService->getAvailableUsers($server->group_id);
$result = [];
foreach ($users as $user) {
$user->v2ray_user = [
"uuid" => $user->uuid,
"email" => sprintf("%s@v2board.user", $user->uuid),
"alter_id" => 0,
"level" => 0,
];
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 = ServerVmess::find($request->input('node_id'));
if (!$server) {
return response([
'ret' => 0,
'msg' => 'server is not found'
]);
}
$data = get_request_content();
$data = json_decode($data, true);
Cache::put(CacheKey::get('SERVER_VMESS_ONLINE_USER', $server->id), count($data), 3600);
Cache::put(CacheKey::get('SERVER_VMESS_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->toArray(), 'vmess', $formatData);
return response([
'ret' => 1,
'msg' => 'ok'
]);
}
// 后端获取配置
public function config(Request $request)
{
$nodeId = $request->input('node_id');
$localPort = $request->input('local_port');
if (empty($nodeId) || empty($localPort)) {
abort(500, '参数错误');
}
try {
$json = $this->getV2RayConfig($nodeId, $localPort);
} catch (\Exception $e) {
abort(500, $e->getMessage());
}
return(json_encode($json, JSON_UNESCAPED_UNICODE));
}
private function getV2RayConfig(int $nodeId, int $localPort)
{
$server = ServerVmess::find($nodeId);
if (!$server) {
abort(500, '节点不存在');
}
$json = json_decode(self::V2RAY_CONFIG);
$json->log->loglevel = (int)admin_setting('server_log_enable') ? 'debug' : 'none';
$json->inbounds[1]->port = (int)$localPort;
$json->inbounds[0]->port = (int)$server->server_port;
$json->inbounds[0]->streamSettings->network = $server->network;
$this->setDns($server, $json);
$this->setNetwork($server, $json);
$this->setRule($server, $json);
$this->setTls($server, $json);
return $json;
}
private function setDns(ServerVmess $server, object $json)
{
if ($server->dnsSettings) {
$dns = $server->dnsSettings;
if (isset($dns->servers)) {
array_push($dns->servers, '1.1.1.1');
array_push($dns->servers, 'localhost');
}
$json->dns = $dns;
$json->outbounds[0]->settings->domainStrategy = 'UseIP';
}
}
private function setNetwork(ServerVmess $server, object $json)
{
if ($server->networkSettings) {
switch ($server->network) {
case 'tcp':
$json->inbounds[0]->streamSettings->tcpSettings = $server->networkSettings;
break;
case 'kcp':
$json->inbounds[0]->streamSettings->kcpSettings = $server->networkSettings;
break;
case 'ws':
$json->inbounds[0]->streamSettings->wsSettings = $server->networkSettings;
break;
case 'http':
$json->inbounds[0]->streamSettings->httpSettings = $server->networkSettings;
break;
case 'domainsocket':
$json->inbounds[0]->streamSettings->dsSettings = $server->networkSettings;
break;
case 'quic':
$json->inbounds[0]->streamSettings->quicSettings = $server->networkSettings;
break;
case 'grpc':
$json->inbounds[0]->streamSettings->grpcSettings = $server->networkSettings;
break;
}
}
}
private function setRule(ServerVmess $server, object $json)
{
$domainRules = array_filter(explode(PHP_EOL, admin_setting('server_v2ray_domain')));
$protocolRules = array_filter(explode(PHP_EOL, admin_setting('server_v2ray_protocol')));
if ($server->ruleSettings) {
$ruleSettings = $server->ruleSettings;
// domain
if (isset($ruleSettings->domain)) {
$ruleSettings->domain = array_filter($ruleSettings->domain);
if (!empty($ruleSettings->domain)) {
$domainRules = array_merge($domainRules, $ruleSettings->domain);
}
}
// protocol
if (isset($ruleSettings->protocol)) {
$ruleSettings->protocol = array_filter($ruleSettings->protocol);
if (!empty($ruleSettings->protocol)) {
$protocolRules = array_merge($protocolRules, $ruleSettings->protocol);
}
}
}
if (!empty($domainRules)) {
$domainObj = new \StdClass();
$domainObj->type = 'field';
$domainObj->domain = $domainRules;
$domainObj->outboundTag = 'block';
array_push($json->routing->rules, $domainObj);
}
if (!empty($protocolRules)) {
$protocolObj = new \StdClass();
$protocolObj->type = 'field';
$protocolObj->protocol = $protocolRules;
$protocolObj->outboundTag = 'block';
array_push($json->routing->rules, $protocolObj);
}
if (empty($domainRules) && empty($protocolRules)) {
$json->inbounds[0]->sniffing->enabled = false;
}
}
private function setTls(ServerVMess $server, object $json)
{
if ((int)$server->tls) {
$tlsSettings = $server->tlsSettings;
$json->inbounds[0]->streamSettings->security = 'tls';
$tls = (object)[
'certificateFile' => '/root/.cert/server.crt',
'keyFile' => '/root/.cert/server.key'
];
$json->inbounds[0]->streamSettings->tlsSettings = new \StdClass();
if (isset($tlsSettings->serverName)) {
$json->inbounds[0]->streamSettings->tlsSettings->serverName = (string)$tlsSettings->serverName;
}
if (isset($tlsSettings->allowInsecure)) {
$json->inbounds[0]->streamSettings->tlsSettings->allowInsecure = (int)$tlsSettings->allowInsecure ? true : false;
}
$json->inbounds[0]->streamSettings->tlsSettings->certificates[0] = $tls;
}
}
}

View File

@ -0,0 +1,87 @@
<?php
namespace App\Http\Controllers\V1\Server;
use App\Http\Controllers\Controller;
use App\Models\ServerShadowsocks;
use App\Services\ServerService;
use App\Services\UserService;
use App\Utils\CacheKey;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
/*
* Tidal Lab Shadowsocks
* Github: https://github.com/tokumeikoi/tidalab-ss
*/
class ShadowsocksTidalabController extends Controller
{
public function __construct(Request $request)
{
$token = $request->input('token');
if (empty($token)) {
abort(500, 'token is null');
}
if ($token !== admin_setting('server_token')) {
abort(500, 'token is error');
}
}
// 后端获取用户
public function user(Request $request)
{
ini_set('memory_limit', -1);
$nodeId = $request->input('node_id');
$server = ServerShadowsocks::find($nodeId);
if (!$server) {
abort(500, 'fail');
}
Cache::put(CacheKey::get('SERVER_SHADOWSOCKS_LAST_CHECK_AT', $server->id), time(), 3600);
$serverService = new ServerService();
$users = $serverService->getAvailableUsers($server->group_id);
$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 = ServerShadowsocks::find($request->input('node_id'));
if (!$server) {
return response([
'ret' => 0,
'msg' => 'server is not found'
]);
}
$data = get_request_content();
$data = json_decode($data, 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->toArray(), 'shadowsocks', $formatData);
return response([
'ret' => 1,
'msg' => 'ok'
]);
}
}

View File

@ -0,0 +1,122 @@
<?php
namespace App\Http\Controllers\V1\Server;
use App\Http\Controllers\Controller;
use App\Models\ServerTrojan;
use App\Services\ServerService;
use App\Services\UserService;
use App\Utils\CacheKey;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
/*
* Tidal Lab Trojan
* Github: https://github.com/tokumeikoi/tidalab-trojan
*/
class TrojanTidalabController extends Controller
{
CONST TROJAN_CONFIG = '{"run_type":"server","local_addr":"0.0.0.0","local_port":443,"remote_addr":"www.taobao.com","remote_port":80,"password":[],"ssl":{"cert":"server.crt","key":"server.key","sni":"domain.com"},"api":{"enabled":true,"api_addr":"127.0.0.1","api_port":10000}}';
public function __construct(Request $request)
{
$token = $request->input('token');
if (empty($token)) {
abort(500, 'token is null');
}
if ($token !== admin_setting('server_token')) {
abort(500, 'token is error');
}
}
// 后端获取用户
public function user(Request $request)
{
ini_set('memory_limit', -1);
$nodeId = $request->input('node_id');
$server = ServerTrojan::find($nodeId);
if (!$server) {
abort(500, 'fail');
}
Cache::put(CacheKey::get('SERVER_TROJAN_LAST_CHECK_AT', $server->id), time(), 3600);
$serverService = new ServerService();
$users = $serverService->getAvailableUsers($server->group_id);
$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 = ServerTrojan::find($request->input('node_id'));
if (!$server) {
return response([
'ret' => 0,
'msg' => 'server is not found'
]);
}
$data = get_request_content();
$data = json_decode($data, 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->toArray(), 'trojan', $formatData);
return response([
'ret' => 1,
'msg' => 'ok'
]);
}
// 后端获取配置
public function config(Request $request)
{
$nodeId = $request->input('node_id');
$localPort = $request->input('local_port');
if (empty($nodeId) || empty($localPort)) {
abort(500, '参数错误');
}
try {
$json = $this->getTrojanConfig($nodeId, $localPort);
} catch (\Exception $e) {
abort(500, $e->getMessage());
}
return(json_encode($json, JSON_UNESCAPED_UNICODE));
}
private function getTrojanConfig(int $nodeId, int $localPort)
{
$server = ServerTrojan::find($nodeId);
if (!$server) {
abort(500, '节点不存在');
}
$json = json_decode(self::TROJAN_CONFIG);
$json->local_port = $server->server_port;
$json->ssl->sni = $server->server_name ? $server->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;
}
}

View File

@ -0,0 +1,176 @@
<?php
namespace App\Http\Controllers\V1\Server;
use App\Http\Controllers\Controller;
use App\Services\ServerService;
use App\Services\UserService;
use App\Utils\CacheKey;
use App\Utils\Helper;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
class UniProxyController extends Controller
{
private $nodeType;
private $nodeInfo;
private $nodeId;
private $serverService;
public function __construct(ServerService $serverService, Request $request)
{
$this->serverService = $serverService;
$this->nodeId = $request->input('node_id');
$this->nodeType = $request->input('node_type') === 'v2ray' ? 'vmess' : $request->input('node_type');
$this->nodeInfo = $this->serverService->getServer($this->nodeId, $this->nodeType);
if(!$this->nodeInfo) {
Log::channel("daily")->info("$this->nodeId $this->nodeType $this->nodeInfo");
throw new \Exception('server is not exist', 500);
};
}
// 后端获取用户
public function user(Request $request)
{
ini_set('memory_limit', -1);
Cache::put(CacheKey::get('SERVER_' . strtoupper($this->nodeType) . '_LAST_CHECK_AT', $this->nodeInfo->id), time(), 3600);
$users = $this->serverService->getAvailableUsers($this->nodeInfo->group_id);
$users = $users->toArray();
$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)
{
$data = get_request_content();
$data = json_decode($data, true);
// 增加单节点多服务器统计在线人数
$ip = $request->ip();
$id = $request->input("id");
$time = time();
$cacheKey = CacheKey::get('MULTI_SERVER_' . strtoupper($this->nodeType) . '_ONLINE_USER', $this->nodeInfo->id);
// 1、获取节点节点在线人数缓存
$onlineUsers = Cache::get($cacheKey) ?? [];
$onlineCollection = collect($onlineUsers);
// 过滤掉超过600秒的记录
$onlineCollection = $onlineCollection->reject(function ($item) use ($time) {
return $item['time'] < ($time - 600);
});
// 定义数据
$updatedItem = [
'id' => $id ?? $ip,
'ip' => $ip,
'online_user' => count($data),
'time' => $time
];
$existingItemIndex = $onlineCollection->search(function ($item) use ($updatedItem) {
return ($item['id'] ?? '') === $updatedItem['id'];
});
if ($existingItemIndex !== false) {
$onlineCollection[$existingItemIndex] = $updatedItem;
} else {
$onlineCollection->push($updatedItem);
}
$onlineUsers = $onlineCollection->all();
Cache::put($cacheKey, $onlineUsers, 3600);
$online_user = $onlineCollection->sum('online_user');
Cache::put(CacheKey::get('SERVER_' . strtoupper($this->nodeType) . '_ONLINE_USER', $this->nodeInfo->id), $online_user, 3600);
Cache::put(CacheKey::get('SERVER_' . strtoupper($this->nodeType) . '_LAST_PUSH_AT', $this->nodeInfo->id), time(), 3600);
// 查询是否存在子节点
$childServer = null;
if ($this->nodeInfo->parent_id == null) $childServer = $this->serverService->getChildServer($this->nodeId, $this->nodeType, $ip);
$userService = new UserService();
$userService->trafficFetch($this->nodeInfo->toArray(), $this->nodeType, $data, $childServer ? $childServer->toArray() : null);
return response([
'data' => true
]);
}
// 后端获取配置
public function config(Request $request)
{
switch ($this->nodeType) {
case 'shadowsocks':
$response = [
'server_port' => $this->nodeInfo->server_port,
'cipher' => $this->nodeInfo->cipher,
'obfs' => $this->nodeInfo->obfs,
'obfs_settings' => $this->nodeInfo->obfs_settings
];
if ($this->nodeInfo->cipher === '2022-blake3-aes-128-gcm') {
$response['server_key'] = Helper::getServerKey($this->nodeInfo->created_at, 16);
}
if ($this->nodeInfo->cipher === '2022-blake3-aes-256-gcm') {
$response['server_key'] = Helper::getServerKey($this->nodeInfo->created_at, 32);
}
break;
case 'vmess':
$response = [
'server_port' => $this->nodeInfo->server_port,
'network' => $this->nodeInfo->network,
'networkSettings' => $this->nodeInfo->networkSettings,
'tls' => $this->nodeInfo->tls
];
break;
case 'trojan':
$response = [
'host' => $this->nodeInfo->host,
'server_port' => $this->nodeInfo->server_port,
'server_name' => $this->nodeInfo->server_name,
'network' => $this->nodeInfo->network,
'networkSettings' => $this->nodeInfo->networkSettings,
];
break;
case 'hysteria':
$response = [
'version' => $this->nodeInfo->version,
'host' => $this->nodeInfo->host,
'server_port' => $this->nodeInfo->server_port,
'server_name' => $this->nodeInfo->server_name,
'up_mbps' => $this->nodeInfo->up_mbps,
'down_mbps' => $this->nodeInfo->down_mbps,
'obfs' => $this->nodeInfo->is_obfs ? Helper::getServerKey($this->nodeInfo->created_at, 16) : null
];
break;
case "vless":
$response = [
'server_port' => $this->nodeInfo->server_port,
'network' => $this->nodeInfo->network,
'network_settings' => $this->nodeInfo->network_settings,
'tls' => $this->nodeInfo->tls,
'flow' => $this->nodeInfo->flow,
'tls_settings' => $this->nodeInfo->tls_settings
];
break;
}
$response['base_config'] = [
'push_interval' => (int)admin_setting('server_push_interval', 60),
'pull_interval' => (int)admin_setting('server_pull_interval', 60)
];
if ($this->nodeInfo['route_id']) {
$response['routes'] = $this->serverService->getRoutes($this->nodeInfo['route_id']);
}
$eTag = sha1(json_encode($response));
if (strpos($request->header('If-None-Match'), $eTag) !== false ) {
return response(null,304);
}
return response($response)->header('ETag', "\"{$eTag}\"");
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace App\Http\Controllers\V1\Staff;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\NoticeSave;
use App\Models\Notice;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class NoticeController extends Controller
{
public function fetch(Request $request)
{
return response([
'data' => Notice::orderBy('id', 'DESC')->get()
]);
}
public function save(NoticeSave $request)
{
$data = $request->only([
'title',
'content',
'img_url'
]);
if (!$request->input('id')) {
if (!Notice::create($data)) {
abort(500, '保存失败');
}
} else {
try {
Notice::find($request->input('id'))->update($data);
} catch (\Exception $e) {
abort(500, '保存失败');
}
}
return response([
'data' => true
]);
}
public function drop(Request $request)
{
if (empty($request->input('id'))) {
abort(500, '参数错误');
}
$notice = Notice::find($request->input('id'));
if (!$notice) {
abort(500, '公告不存在');
}
if (!$notice->delete()) {
abort(500, '删除失败');
}
return response([
'data' => true
]);
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace App\Http\Controllers\V1\Staff;
use App\Http\Controllers\Controller;
use App\Models\Plan;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class PlanController extends Controller
{
public function fetch(Request $request)
{
$counts = User::select(
DB::raw("plan_id"),
DB::raw("count(*) as count")
)
->where('plan_id', '!=', NULL)
->where(function ($query) {
$query->where('expired_at', '>=', time())
->orWhere('expired_at', NULL);
})
->groupBy("plan_id")
->get();
$plans = Plan::orderBy('sort', 'ASC')->get();
foreach ($plans as $k => $v) {
$plans[$k]->count = 0;
foreach ($counts as $kk => $vv) {
if ($plans[$k]->id === $counts[$kk]->plan_id) $plans[$k]->count = $counts[$kk]->count;
}
}
return response([
'data' => $plans
]);
}
}

View File

@ -0,0 +1,85 @@
<?php
namespace App\Http\Controllers\V1\Staff;
use App\Http\Controllers\Controller;
use App\Models\Ticket;
use App\Models\TicketMessage;
use App\Services\TicketService;
use Illuminate\Http\Request;
class TicketController extends Controller
{
public function fetch(Request $request)
{
if ($request->input('id')) {
$ticket = Ticket::where('id', $request->input('id'))
->first();
if (!$ticket) {
abort(500, '工单不存在');
}
$ticket['message'] = TicketMessage::where('ticket_id', $ticket->id)->get();
for ($i = 0; $i < count($ticket['message']); $i++) {
if ($ticket['message'][$i]['user_id'] !== $ticket->user_id) {
$ticket['message'][$i]['is_me'] = true;
} else {
$ticket['message'][$i]['is_me'] = false;
}
}
return response([
'data' => $ticket
]);
}
$current = $request->input('current') ? $request->input('current') : 1;
$pageSize = $request->input('pageSize') >= 10 ? $request->input('pageSize') : 10;
$model = Ticket::orderBy('created_at', 'DESC');
if ($request->input('status') !== NULL) {
$model->where('status', $request->input('status'));
}
$total = $model->count();
$res = $model->forPage($current, $pageSize)
->get();
return response([
'data' => $res,
'total' => $total
]);
}
public function reply(Request $request)
{
if (empty($request->input('id'))) {
abort(500, '参数错误');
}
if (empty($request->input('message'))) {
abort(500, '消息不能为空');
}
$ticketService = new TicketService();
$ticketService->replyByAdmin(
$request->input('id'),
$request->input('message'),
$request->user['id']
);
return response([
'data' => true
]);
}
public function close(Request $request)
{
if (empty($request->input('id'))) {
abort(500, '参数错误');
}
$ticket = Ticket::where('id', $request->input('id'))
->first();
if (!$ticket) {
abort(500, '工单不存在');
}
$ticket->status = 1;
if (!$ticket->save()) {
abort(500, '关闭失败');
}
return response([
'data' => true
]);
}
}

View File

@ -0,0 +1,107 @@
<?php
namespace App\Http\Controllers\V1\Staff;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\UserSendMail;
use App\Http\Requests\Staff\UserUpdate;
use App\Jobs\SendEmailJob;
use App\Models\Plan;
use App\Models\User;
use Illuminate\Http\Request;
class UserController extends Controller
{
public function getUserInfoById(Request $request)
{
if (empty($request->input('id'))) {
abort(500, '参数错误');
}
$user = User::where('is_admin', 0)
->where('id', $request->input('id'))
->where('is_staff', 0)
->first();
if (!$user) abort(500, '用户不存在');
return response([
'data' => $user
]);
}
public function update(UserUpdate $request)
{
$params = $request->validated();
$user = User::find($request->input('id'));
if (!$user) {
abort(500, '用户不存在');
}
if (User::where('email', $params['email'])->first() && $user->email !== $params['email']) {
abort(500, '邮箱已被使用');
}
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) {
abort(500, '订阅计划不存在');
}
$params['group_id'] = $plan->group_id;
}
try {
$user->update($params);
} catch (\Exception $e) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
public function sendMail(UserSendMail $request)
{
$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::orderBy($sort, $sortType);
$this->filter($request, $builder);
$users = $builder->get();
foreach ($users as $user) {
SendEmailJob::dispatch([
'email' => $user->email,
'subject' => $request->input('subject'),
'template_name' => 'notify',
'template_value' => [
'name' => admin_setting('app_name', 'XBoard'),
'url' => admin_setting('app_url'),
'content' => $request->input('content')
]
]);
}
return response([
'data' => true
]);
}
public function ban(Request $request)
{
$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::orderBy($sort, $sortType);
$this->filter($request, $builder);
try {
$builder->update([
'banned' => 1
]);
} catch (\Exception $e) {
abort(500, '处理失败');
}
return response([
'data' => true
]);
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace App\Http\Controllers\V1\User;
use App\Http\Controllers\Controller;
use App\Models\Payment;
use App\Utils\Dict;
use Illuminate\Http\Request;
class CommController extends Controller
{
public function config()
{
return response([
'data' => [
'is_telegram' => (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')
]
]);
}
public function getStripePublicKey(Request $request)
{
$payment = Payment::where('id', $request->input('id'))
->where('payment', 'StripeCredit')
->first();
if (!$payment) abort(500, 'payment is not found');
return response([
'data' => $payment->config['stripe_pk_live']
]);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Http\Controllers\V1\User;
use App\Http\Controllers\Controller;
use App\Services\CouponService;
use Illuminate\Http\Request;
class CouponController extends Controller
{
public function check(Request $request)
{
if (empty($request->input('code'))) {
abort(500, __('Coupon cannot be empty'));
}
$couponService = new CouponService($request->input('code'));
$couponService->setPlanId($request->input('plan_id'));
$couponService->setUserId($request->user['id']);
$couponService->check();
return response([
'data' => $couponService->getCoupon()
]);
}
}

View File

@ -0,0 +1,88 @@
<?php
namespace App\Http\Controllers\V1\User;
use App\Http\Controllers\Controller;
use App\Models\CommissionLog;
use App\Models\InviteCode;
use App\Models\Order;
use App\Models\User;
use App\Utils\Helper;
use Illuminate\Http\Request;
class InviteController extends Controller
{
public function save(Request $request)
{
if (InviteCode::where('user_id', $request->user['id'])->where('status', 0)->count() >= admin_setting('invite_gen_limit', 5)) {
abort(500, __('The maximum number of creations has been reached'));
}
$inviteCode = new InviteCode();
$inviteCode->user_id = $request->user['id'];
$inviteCode->code = Helper::randomChar(8);
return response([
'data' => $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)
->select([
'id',
'trade_no',
'order_amount',
'get_amount',
'created_at'
])
->orderBy('created_at', 'DESC');
$total = $builder->count();
$details = $builder->forPage($current, $pageSize)
->get();
return response([
'data' => $details,
'total' => $total
]);
}
public function fetch(Request $request)
{
$codes = InviteCode::where('user_id', $request->user['id'])
->where('status', 0)
->get();
$commission_rate = admin_setting('invite_commission', 10);
$user = User::find($request->user['id']);
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', $request->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', $request->user['id'])->count(),
//有效的佣金
(int)CommissionLog::where('invite_user_id', $request->user['id'])
->sum('get_amount'),
//确认中的佣金
$uncheck_commission_balance,
//佣金比例
(int)$commission_rate,
//可用佣金
(int)$user->commission_balance
];
return response([
'data' => [
'codes' => $codes,
'stat' => $stat
]
]);
}
}

View File

@ -0,0 +1,73 @@
<?php
namespace App\Http\Controllers\V1\User;
use App\Http\Controllers\Controller;
use App\Models\Knowledge;
use App\Models\User;
use App\Services\UserService;
use App\Utils\Helper;
use Illuminate\Http\Request;
class KnowledgeController extends Controller
{
public function fetch(Request $request)
{
if ($request->input('id')) {
$knowledge = Knowledge::where('id', $request->input('id'))
->where('show', 1)
->first()
->toArray();
if (!$knowledge) abort(500, __('Article does not exist'));
$user = User::find($request->user['id']);
$userService = new UserService();
if (!$userService->isAvailable($user)) {
$this->formatAccessData($knowledge['body']);
}
$subscribeUrl = Helper::getSubscribeUrl("/api/v1/client/subscribe?token={$user['token']}");
$knowledge['body'] = str_replace('{{siteName}}', admin_setting('app_name', 'XBoard'), $knowledge['body']);
$knowledge['body'] = str_replace('{{subscribeUrl}}', $subscribeUrl, $knowledge['body']);
$knowledge['body'] = str_replace('{{urlEncodeSubscribeUrl}}', urlencode($subscribeUrl), $knowledge['body']);
$knowledge['body'] = str_replace(
'{{safeBase64SubscribeUrl}}',
str_replace(
array('+', '/', '='),
array('-', '_', ''),
base64_encode($subscribeUrl)
),
$knowledge['body']
);
return response([
'data' => $knowledge
]);
}
$builder = Knowledge::select(['id', 'category', 'title', 'updated_at'])
->where('language', $request->input('language'))
->where('show', 1)
->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()
->groupBy('category');
return response([
'data' => $knowledges
]);
}
private function formatAccessData(&$body)
{
function getBetween($input, $start, $end){$substr = substr($input, strlen($start)+strpos($input, $start),(strlen($input) - strpos($input, $end))*(-1));return $start . $substr . $end;}
while (strpos($body, '<!--access start-->') !== false) {
$accessData = getBetween($body, '<!--access start-->', '<!--access end-->');
if ($accessData) {
$body = str_replace($accessData, '<div class="v2board-no-access">'. __('You must have a valid subscription to view content in this area') .'</div>', $body);
}
}
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Http\Controllers\V1\User;
use App\Http\Controllers\Controller;
use App\Models\Notice;
use Illuminate\Http\Request;
class NoticeController extends Controller
{
public function fetch(Request $request)
{
$current = $request->input('current') ? $request->input('current') : 1;
$pageSize = 5;
$model = Notice::orderBy('created_at', 'DESC')
->where('show', 1);
$total = $model->count();
$res = $model->forPage($current, $pageSize)
->get();
return response([
'data' => $res,
'total' => $total
]);
}
}

View File

@ -0,0 +1,266 @@
<?php
namespace App\Http\Controllers\V1\User;
use App\Http\Controllers\Controller;
use App\Http\Requests\User\OrderSave;
use App\Models\Order;
use App\Models\Payment;
use App\Models\Plan;
use App\Models\User;
use App\Services\CouponService;
use App\Services\OrderService;
use App\Services\PaymentService;
use App\Services\PlanService;
use App\Services\UserService;
use App\Utils\Helper;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Library\BitpayX;
use Library\Epay;
use Library\MGate;
use Omnipay\Omnipay;
use Stripe\Source;
use Stripe\Stripe;
class OrderController extends Controller
{
public function fetch(Request $request)
{
$model = Order::where('user_id', $request->user['id'])
->orderBy('created_at', 'DESC');
if ($request->input('status') !== null) {
$model->where('status', $request->input('status'));
}
$order = $model->get();
$plan = Plan::get();
for ($i = 0; $i < count($order); $i++) {
for ($x = 0; $x < count($plan); $x++) {
if ($order[$i]['plan_id'] === $plan[$x]['id']) {
$order[$i]['plan'] = $plan[$x];
}
}
}
return response([
'data' => $order->makeHidden(['id', 'user_id'])
]);
}
public function detail(Request $request)
{
$order = Order::where('user_id', $request->user['id'])
->where('trade_no', $request->input('trade_no'))
->first();
if (!$order) {
abort(500, __('Order does not exist or has been paid'));
}
$order['plan'] = Plan::find($order->plan_id);
$order['try_out_plan_id'] = (int)admin_setting('try_out_plan_id');
if (!$order['plan']) {
abort(500, __('Subscription plan does not exist'));
}
if ($order->surplus_order_ids) {
$order['surplus_orders'] = Order::whereIn('id', $order->surplus_order_ids)->get();
}
return response([
'data' => $order
]);
}
public function save(OrderSave $request)
{
$userService = new UserService();
if ($userService->isNotCompleteOrderByUserId($request->user['id'])) {
abort(500, __('You have an unpaid or pending order, please try again later or cancel it'));
}
$planService = new PlanService($request->input('plan_id'));
$plan = $planService->plan;
$user = User::find($request->user['id']);
if (!$plan) {
abort(500, __('Subscription plan does not exist'));
}
if ($user->plan_id !== $plan->id && !$planService->haveCapacity() && $request->input('period') !== 'reset_price') {
abort(500, __('Current product is sold out'));
}
if ($plan[$request->input('period')] === NULL) {
abort(500, __('This payment period cannot be purchased, please choose another period'));
}
if ($request->input('period') === 'reset_price') {
if (!$userService->isAvailable($user) || $plan->id !== $user->plan_id) {
abort(500, __('Subscription has expired or no active subscription, unable to purchase Data Reset Package'));
}
}
if ((!$plan->show && !$plan->renew) || (!$plan->show && $user->plan_id !== $plan->id)) {
if ($request->input('period') !== 'reset_price') {
abort(500, __('This subscription has been sold out, please choose another subscription'));
}
}
if (!$plan->renew && $user->plan_id == $plan->id && $request->input('period') !== 'reset_price') {
abort(500, __('This subscription cannot be renewed, please change to another subscription'));
}
if (!$plan->show && $plan->renew && !$userService->isAvailable($user)) {
abort(500, __('This subscription has expired, please change to another subscription'));
}
DB::beginTransaction();
$order = new Order();
$orderService = new OrderService($order);
$order->user_id = $request->user['id'];
$order->plan_id = $plan->id;
$order->period = $request->input('period');
$order->trade_no = Helper::generateOrderNo();
$order->total_amount = $plan[$request->input('period')];
if ($request->input('coupon_code')) {
$couponService = new CouponService($request->input('coupon_code'));
if (!$couponService->use($order)) {
DB::rollBack();
abort(500, __('Coupon failed'));
}
$order->coupon_id = $couponService->getId();
}
$orderService->setVipDiscount($user);
$orderService->setOrderType($user);
$orderService->setInvite($user);
if ($user->balance && $order->total_amount > 0) {
$remainingBalance = $user->balance - $order->total_amount;
$userService = new UserService();
if ($remainingBalance > 0) {
if (!$userService->addBalance($order->user_id, - $order->total_amount)) {
DB::rollBack();
abort(500, __('Insufficient balance'));
}
$order->balance_amount = $order->total_amount;
$order->total_amount = 0;
} else {
if (!$userService->addBalance($order->user_id, - $user->balance)) {
DB::rollBack();
abort(500, __('Insufficient balance'));
}
$order->balance_amount = $user->balance;
$order->total_amount = $order->total_amount - $user->balance;
}
}
if (!$order->save()) {
DB::rollback();
abort(500, __('Failed to create order'));
}
DB::commit();
return response([
'data' => $order->trade_no
]);
}
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) {
abort(500, __('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)) abort(500, '');
return response([
'type' => -1,
'data' => true
]);
}
$payment = Payment::find($method);
if (!$payment || $payment->enable !== 1) abort(500, __('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 = round(($order->total_amount * ($payment->handling_fee_percent / 100)) + $payment->handling_fee_fixed);
}
$order->payment_id = $method;
if (!$order->save()) abort(500, __('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) {
abort(500, __('Order does not exist'));
}
return response([
'data' => $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 response([
'data' => $methods
]);
}
public function cancel(Request $request)
{
if (empty($request->input('trade_no'))) {
abort(500, __('Invalid parameter'));
}
$order = Order::where('trade_no', $request->input('trade_no'))
->where('user_id', $request->user['id'])
->first();
if (!$order) {
abort(500, __('Order does not exist'));
}
if ($order->status !== 0) {
abort(500, __('You can only cancel pending orders'));
}
$orderService = new OrderService($order);
if (!$orderService->cancel()) {
abort(500, __('Cancel failed'));
}
return response([
'data' => true
]);
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Http\Controllers\V1\User;
use App\Http\Controllers\Controller;
use App\Models\Plan;
use App\Models\User;
use App\Services\PlanService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class PlanController extends Controller
{
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) {
abort(500, __('Subscription plan does not exist'));
}
if ((!$plan->show && !$plan->renew) || (!$plan->show && $user->plan_id !== $plan->id)) {
abort(500, __('Subscription plan does not exist'));
}
return response([
'data' => $plan
]);
}
$counts = PlanService::countActiveUsers();
$plans = Plan::where('show', 1)
->orderBy('sort', 'ASC')
->get();
foreach ($plans as $k => $v) {
if ($plans[$k]->capacity_limit === NULL) continue;
if (!isset($counts[$plans[$k]->id])) continue;
$plans[$k]->capacity_limit = $plans[$k]->capacity_limit - $counts[$plans[$k]->id]->count;
}
return response([
'data' => $plans
]);
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Http\Controllers\V1\User;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Services\ServerService;
use App\Services\UserService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class ServerController extends Controller
{
public function fetch(Request $request)
{
$user = User::find($request->user['id']);
$servers = [];
$userService = new UserService();
if ($userService->isAvailable($user)) {
$serverService = new ServerService();
$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);
}
return response([
'data' => $servers
])->header('ETag', "\"{$eTag}\"");
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Http\Controllers\V1\User;
use App\Http\Controllers\Controller;
use App\Models\StatUser;
use App\Services\StatisticalService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class StatController extends Controller
{
public function getTrafficLog(Request $request)
{
$records = StatUser::select([
'u',
'd',
'record_at',
'user_id',
'server_rate'
])
->where('user_id', $request->user['id'])
->where('record_at', '>=', strtotime(date('Y-m-1')))
->orderBy('record_at', 'DESC')
->get();
// 追加当天流量
$recordAt = strtotime(date('Y-m-d'));
$statService = new StatisticalService();
$statService->setStartAt($recordAt);
$statService->setUserStats();
$todayTraffics = $statService->getStatUserByUserID($request->user['id']);
if (count($todayTraffics) > 0) {
foreach ($todayTraffics as $todayTraffic){
$todayTraffic['server_rate'] = number_format($todayTraffic['server_rate'], 2);
$records->prepend($todayTraffic);
}
};
return response([
'data' => $records
]);
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Http\Controllers\V1\User;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Services\TelegramService;
use Illuminate\Http\Request;
class TelegramController extends Controller
{
public function getBotInfo()
{
$telegramService = new TelegramService();
$response = $telegramService->getMe();
return response([
'data' => [
'username' => $response->result->username
]
]);
}
public function unbind(Request $request)
{
$user = User::where('user_id', $request->user['id'])->first();
}
}

View File

@ -0,0 +1,194 @@
<?php
namespace App\Http\Controllers\V1\User;
use App\Http\Controllers\Controller;
use App\Http\Requests\User\TicketSave;
use App\Http\Requests\User\TicketWithdraw;
use App\Models\Ticket;
use App\Models\TicketMessage;
use App\Models\User;
use App\Services\TelegramService;
use App\Services\TicketService;
use App\Utils\Dict;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class TicketController extends Controller
{
public function fetch(Request $request)
{
if ($request->input('id')) {
$ticket = Ticket::where('id', $request->input('id'))
->where('user_id', $request->user['id'])
->first();
if (!$ticket) {
abort(500, __('Ticket does not exist'));
}
$ticket['message'] = TicketMessage::where('ticket_id', $ticket->id)->get();
for ($i = 0; $i < count($ticket['message']); $i++) {
if ($ticket['message'][$i]['user_id'] == $ticket->user_id) {
$ticket['message'][$i]['is_me'] = true;
} else {
$ticket['message'][$i]['is_me'] = false;
}
}
return response([
'data' => $ticket
]);
}
$ticket = Ticket::where('user_id', $request->user['id'])
->orderBy('created_at', 'DESC')
->get();
return response([
'data' => $ticket
]);
}
public function save(TicketSave $request)
{
DB::beginTransaction();
if ((int)Ticket::where('status', 0)->where('user_id', $request->user['id'])->lockForUpdate()->count()) {
abort(500, __('There are other unresolved tickets'));
}
$ticket = Ticket::create(array_merge($request->only([
'subject',
'level'
]), [
'user_id' => $request->user['id']
]));
if (!$ticket) {
DB::rollback();
abort(500, __('Failed to open ticket'));
}
$ticketMessage = TicketMessage::create([
'user_id' => $request->user['id'],
'ticket_id' => $ticket->id,
'message' => $request->input('message')
]);
if (!$ticketMessage) {
DB::rollback();
abort(500, __('Failed to open ticket'));
}
DB::commit();
$this->sendNotify($ticket, $request->input('message'));
return response([
'data' => true
]);
}
public function reply(Request $request)
{
if (empty($request->input('id'))) {
abort(500, __('Invalid parameter'));
}
if (empty($request->input('message'))) {
abort(500, __('Message cannot be empty'));
}
$ticket = Ticket::where('id', $request->input('id'))
->where('user_id', $request->user['id'])
->first();
if (!$ticket) {
abort(500, __('Ticket does not exist'));
}
if ($ticket->status) {
abort(500, __('The ticket is closed and cannot be replied'));
}
if ($request->user['id'] == $this->getLastMessage($ticket->id)->user_id) {
abort(500, __('Please wait for the technical enginneer to reply'));
}
$ticketService = new TicketService();
if (!$ticketService->reply(
$ticket,
$request->input('message'),
$request->user['id']
)) {
abort(500, __('Ticket reply failed'));
}
$this->sendNotify($ticket, $request->input('message'));
return response([
'data' => true
]);
}
public function close(Request $request)
{
if (empty($request->input('id'))) {
abort(500, __('Invalid parameter'));
}
$ticket = Ticket::where('id', $request->input('id'))
->where('user_id', $request->user['id'])
->first();
if (!$ticket) {
abort(500, __('Ticket does not exist'));
}
$ticket->status = 1;
if (!$ticket->save()) {
abort(500, __('Close failed'));
}
return response([
'data' => 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)) {
abort(500, 'user.ticket.withdraw.not_support_withdraw');
}
if (!in_array(
$request->input('withdraw_method'),
admin_setting('commission_withdraw_method',Dict::WITHDRAW_METHOD_WHITELIST_DEFAULT)
)) {
abort(500, __('Unsupported withdrawal method'));
}
$user = User::find($request->user['id']);
$limit = admin_setting('commission_withdraw_limit', 100);
if ($limit > ($user->commission_balance / 100)) {
abort(500, __('The current required minimum withdrawal commission is :limit', ['limit' => $limit]));
}
DB::beginTransaction();
$subject = __('[Commission Withdrawal Request] This ticket is opened by the system');
$ticket = Ticket::create([
'subject' => $subject,
'level' => 2,
'user_id' => $request->user['id']
]);
if (!$ticket) {
DB::rollback();
abort(500, __('Failed to open ticket'));
}
$message = sprintf("%s\r\n%s",
__('Withdrawal method') . "" . $request->input('withdraw_method'),
__('Withdrawal account') . "" . $request->input('withdraw_account')
);
$ticketMessage = TicketMessage::create([
'user_id' => $request->user['id'],
'ticket_id' => $ticket->id,
'message' => $message
]);
if (!$ticketMessage) {
DB::rollback();
abort(500, __('Failed to open ticket'));
}
DB::commit();
$this->sendNotify($ticket, $message);
return response([
'data' => true
]);
}
private function sendNotify(Ticket $ticket, string $message)
{
$telegramService = new TelegramService();
$telegramService->sendMessageWithAdmin("📮工单提醒 #{$ticket->id}\n———————————————\n主题:\n`{$ticket->subject}`\n内容:\n`{$message}`", true);
}
}

View File

@ -0,0 +1,239 @@
<?php
namespace App\Http\Controllers\V1\User;
use App\Http\Controllers\Controller;
use App\Http\Requests\User\UserChangePassword;
use App\Http\Requests\User\UserTransfer;
use App\Http\Requests\User\UserUpdate;
use App\Models\Order;
use App\Models\Plan;
use App\Models\Ticket;
use App\Models\User;
use App\Services\AuthService;
use App\Services\UserService;
use App\Utils\CacheKey;
use App\Utils\Helper;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class UserController extends Controller
{
public function getActiveSession(Request $request)
{
$user = User::find($request->user['id']);
if (!$user) {
abort(500, __('The user does not exist'));
}
$authService = new AuthService($user);
return response([
'data' => $authService->getSessions()
]);
}
public function removeActiveSession(Request $request)
{
$user = User::find($request->user['id']);
if (!$user) {
abort(500, __('The user does not exist'));
}
$authService = new AuthService($user);
return response([
'data' => $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 response([
'data' => $data
]);
}
public function changePassword(UserChangePassword $request)
{
$user = User::find($request->user['id']);
if (!$user) {
abort(500, __('The user does not exist'));
}
if (!Helper::multiPasswordVerify(
$user->password_algo,
$user->password_salt,
$request->input('old_password'),
$user->password)
) {
abort(500, __('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()) {
abort(500, __('Save failed'));
}
return response([
'data' => 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) {
abort(500, __('The user does not exist'));
}
$user['avatar_url'] = 'https://cdn.v2ex.com/gravatar/' . md5($user->email) . '?s=64&d=identicon';
return response([
'data' => $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 response([
'data' => $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'
])
->first();
if (!$user) {
abort(500, __('The user does not exist'));
}
if ($user->plan_id) {
$user['plan'] = Plan::find($user->plan_id);
if (!$user['plan']) {
abort(500, __('Subscription plan does not exist'));
}
}
$user['subscribe_url'] = Helper::getSubscribeUrl("/api/v1/client/subscribe?token={$user['token']}");
$userService = new UserService();
$user['reset_day'] = $userService->getResetDay($user);
return response([
'data' => $user
]);
}
public function resetSecurity(Request $request)
{
$user = User::find($request->user['id']);
if (!$user) {
abort(500, __('The user does not exist'));
}
$user->uuid = Helper::guid(true);
$user->token = Helper::guid();
if (!$user->save()) {
abort(500, __('Reset failed'));
}
return response([
'data' => Helper::getSubscribeUrl('/api/v1/client/subscribe?token=' . $user->token)
]);
}
public function update(UserUpdate $request)
{
$updateData = $request->only([
'remind_expire',
'remind_traffic'
]);
$user = User::find($request->user['id']);
if (!$user) {
abort(500, __('The user does not exist'));
}
try {
$user->update($updateData);
} catch (\Exception $e) {
abort(500, __('Save failed'));
}
return response([
'data' => true
]);
}
public function transfer(UserTransfer $request)
{
$user = User::find($request->user['id']);
if (!$user) {
abort(500, __('The user does not exist'));
}
if ($request->input('transfer_amount') > $user->commission_balance) {
abort(500, __('Insufficient commission balance'));
}
$user->commission_balance = $user->commission_balance - $request->input('transfer_amount');
$user->balance = $user->balance + $request->input('transfer_amount');
if (!$user->save()) {
abort(500, __('Transfer failed'));
}
return response([
'data' => true
]);
}
public function getQuickLoginUrl(Request $request)
{
$user = User::find($request->user['id']);
if (!$user) {
abort(500, __('The user does not exist'));
}
$code = Helper::guid();
$key = CacheKey::get('TEMP_TOKEN', $code);
Cache::put($key, $user->id, 60);
$redirect = '/#/login?verify=' . $code . '&redirect=' . ($request->input('redirect') ? $request->input('redirect') : 'dashboard');
if (admin_setting('app_url')) {
$url = admin_setting('app_url') . $redirect;
} else {
$url = url($redirect);
}
return response([
'data' => $url
]);
}
}

View File

@ -0,0 +1,92 @@
<?php
namespace App\Http\Controllers\V2\Admin;
use App\Http\Controllers\Controller;
use App\Models\CommissionLog;
use App\Models\Order;
use App\Models\ServerShadowsocks;
use App\Models\ServerTrojan;
use App\Models\ServerVmess;
use App\Models\Stat;
use App\Models\StatServer;
use App\Models\StatUser;
use App\Models\Ticket;
use App\Models\User;
use App\Services\StatisticalService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class StatController extends Controller
{
public function override(Request $request)
{
$params = $request->validate([
'start_at' => '',
'end_at' => ''
]);
if (isset($params['start_at']) && isset($params['end_at'])) {
$stats = Stat::where('record_at', '>=', $params['start_at'])
->where('record_at', '<', $params['end_at'])
->get()
->makeHidden(['record_at', 'created_at', 'updated_at', 'id', 'record_type'])
->toArray();
} else {
$statisticalService = new StatisticalService();
return [
'data' => $statisticalService->generateStatData()
];
}
$stats = array_reduce($stats, function($carry, $item) {
foreach($item as $key => $value) {
if(isset($carry[$key]) && $carry[$key]) {
$carry[$key] += $value;
} else {
$carry[$key] = $value;
}
}
return $carry;
}, []);
return [
'data' => $stats
];
}
public function record(Request $request)
{
$request->validate([
'type' => 'required|in:paid_total,commission_total,register_count',
'start_at' => '',
'end_at' => ''
]);
$statisticalService = new StatisticalService();
$statisticalService->setStartAt($request->input('start_at'));
$statisticalService->setEndAt($request->input('end_at'));
return [
'data' => $statisticalService->getStatRecord($request->input('type'))
];
}
public function ranking(Request $request)
{
$request->validate([
'type' => 'required|in:server_traffic_rank,user_consumption_rank,invite_rank',
'start_at' => '',
'end_at' => '',
'limit' => 'nullable|integer'
]);
$statisticalService = new StatisticalService();
$statisticalService->setStartAt($request->input('start_at'));
$statisticalService->setEndAt($request->input('end_at'));
return [
'data' => $statisticalService->getRanking($request->input('type'), $request->input('limit') ?? 20)
];
}
}

93
app/Http/Kernel.php Executable file
View File

@ -0,0 +1,93 @@
<?php
namespace App\Http;
use Fruitcake\Cors\HandleCors;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array
*/
protected $middleware = [
\App\Http\Middleware\CORS::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,
];
/**
* 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,
],
'api' => [
// \App\Http\Middleware\EncryptCookies::class,
// \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
// \Illuminate\Session\Middleware\StartSession::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,
];
/**
* 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,
\App\Http\Middleware\Authenticate::class,
\Illuminate\Routing\Middleware\ThrottleRequests::class,
\Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\Illuminate\Auth\Middleware\Authorize::class,
];
}

30
app/Http/Middleware/Admin.php Executable file
View File

@ -0,0 +1,30 @@
<?php
namespace App\Http\Middleware;
use App\Services\AuthService;
use Closure;
use Illuminate\Support\Facades\Cache;
class Admin
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$authorization = $request->input('auth_data') ?? $request->header('authorization');
if (!$authorization) abort(403, '未登录或登陆已过期');
$user = AuthService::decryptAuthData($authorization);
if (!$user || !$user['is_admin']) abort(403, '未登录或登陆已过期');
$request->merge([
'user' => $user
]);
return $next($request);
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
class Authenticate extends Middleware
{
/**
* Get the path the user should be redirected to when they are not authenticated.
*
* @param \Illuminate\Http\Request $request
* @return string
*/
protected function redirectTo($request)
{
if (!$request->expectsJson()) {
return route('login');
}
}
}

27
app/Http/Middleware/CORS.php Executable file
View File

@ -0,0 +1,27 @@
<?php
namespace App\Http\Middleware;
use Closure;
class CORS
{
public function handle($request, Closure $next)
{
$origin = $request->header('origin');
if (empty($origin)) {
$referer = $request->header('referer');
if (!empty($referer) && preg_match("/^((https|http):\/\/)?([^\/]+)/i", $referer, $matches)) {
$origin = $matches[0];
}
}
$response = $next($request);
$response->header('Access-Control-Allow-Origin', preg_replace('~(?:^/|/$)~', '', "/".$origin));
$response->header('Access-Control-Allow-Methods', 'GET,POST,OPTIONS,HEAD');
$response->header('Access-Control-Allow-Headers', 'Origin,Content-Type,Accept,Authorization,X-Request-With');
$response->header('Access-Control-Allow-Credentials', 'true');
$response->header('Access-Control-Max-Age', 10080);
return $response;
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode as Middleware;
class CheckForMaintenanceMode extends Middleware
{
/**
* The URIs that should be reachable while maintenance mode is enabled.
*
* @var array
*/
protected $except = [
//
];
}

34
app/Http/Middleware/Client.php Executable file
View File

@ -0,0 +1,34 @@
<?php
namespace App\Http\Middleware;
use App\Utils\CacheKey;
use Closure;
use App\Models\User;
use Illuminate\Support\Facades\Cache;
class Client
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$token = $request->input('token');
if (empty($token)) {
abort(403, 'token is null');
}
$user = User::where('token', $token)->first();
if (!$user) {
abort(403, 'token is error');
}
$request->merge([
'user' => $user
]);
return $next($request);
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
class EncryptCookies extends Middleware
{
/**
* The names of the cookies that should not be encrypted.
*
* @var array
*/
protected $except = [
//
];
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Http\Middleware;
use Closure;
class ForceJson
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null $guard
* @return mixed
*/
public function handle($request, Closure $next, $guard = null)
{
$request->headers->set('accept', 'application/json');
return $next($request);
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\App;
class Language
{
public function handle($request, Closure $next)
{
if ($request->header('content-language')) {
App::setLocale($request->header('content-language'));
}
return $next($request);
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Auth;
class RedirectIfAuthenticated
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null $guard
* @return mixed
*/
public function handle($request, Closure $next, $guard = null)
{
if (Auth::guard($guard)->check()) {
return redirect('/home');
}
return $next($request);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Http\Middleware;
use Closure;
class RequestLog
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if ($request->method() === 'POST') {
$path = $request->path();
info("POST {$path}");
};
return $next($request);
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class Server
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/
public function handle(Request $request, Closure $next)
{
$request->validate([
'token' => ['required','string',function ($attribute, $value, $fail) {
if ($value != admin_setting('server_token')) {
$fail("The $attribute is error.");
}
}],
'node_type' => [
'required',
'string',
'regex:/^(?i)(hysteria|vless|trojan|vmess|v2ray|tuic|shadowsocks|shadowsocks-plugin)$/',
function ($attribute, $value, $fail) {
// 将值转换为小写
request()->merge([$attribute => strtolower($value)]);
},
],
'node_id' => 'required'
]);
return $next($request);
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Http\Middleware;
use App\Services\AuthService;
use Closure;
class Staff
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$authorization = $request->input('auth_data') ?? $request->header('authorization');
if (!$authorization) abort(403, '未登录或登陆已过期');
$user = AuthService::decryptAuthData($authorization);
if (!$user || !$user['is_staff']) abort(403, '未登录或登陆已过期');
$request->merge([
'user' => $user
]);
return $next($request);
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
class TrimStrings extends Middleware
{
/**
* The names of the attributes that should not be trimmed.
*
* @var array
*/
protected $except = [
'password',
'password_confirmation',
];
}

View File

@ -0,0 +1,49 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustProxies as Middleware;
use Illuminate\Http\Request;
class TrustProxies extends Middleware
{
/**
* The trusted proxies for this application.
*
* @var array|string
*/
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",
];
/**
* The headers that should be used to detect proxies.
*
* @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;
}

30
app/Http/Middleware/User.php Executable file
View File

@ -0,0 +1,30 @@
<?php
namespace App\Http\Middleware;
use App\Services\AuthService;
use Closure;
use Illuminate\Support\Facades\Cache;
class User
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$authorization = $request->input('auth_data') ?? $request->header('authorization');
if (!$authorization) abort(403, '未登录或登陆已过期');
$user = AuthService::decryptAuthData($authorization);
if (!$user) abort(403, '未登录或登陆已过期');
$request->merge([
'user' => $user
]);
return $next($request);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
/**
* Indicates whether the XSRF-TOKEN cookie should be set on the response.
*
* @var bool
*/
protected $addHttpCookie = true;
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array
*/
protected $except = [
//
];
}

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