Merge branch 'cedar2025:dev' into dev

This commit is contained in:
Micah123321
2026-02-22 03:20:08 +08:00
committed by GitHub
47 changed files with 613 additions and 272 deletions
+3
View File
@@ -38,4 +38,7 @@ server {
location ~ /\.ht {
deny all;
}
access_log off;
error_log /dev/null crit;
}
+6
View File
@@ -17,6 +17,7 @@ directory=/www
command=sh -c "chown -R www:www /www && chmod -R 775 /www"
autostart=true
autorestart=false
priority=1
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
@@ -25,6 +26,7 @@ stderr_logfile_maxbytes=0
[program:nginx]
command=nginx -g 'daemon off;'
user=root
priority=5
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
@@ -36,6 +38,7 @@ startretries=10
[program:cron]
command=crond -f -l 8
user=root
priority=4
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
@@ -62,6 +65,8 @@ command=php -c php.ini webman.php start
directory=/www
user=www
numprocs=1
priority=2
startsecs=3
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
@@ -74,6 +79,7 @@ startretries=10
command=php artisan horizon
directory=/www
user=www
priority=3
stdout_logfile=/www/storage/logs/queue.log
stdout_logfile_maxbytes=0
stderr_logfile=/www/storage/logs/queue_error.log
+4
View File
@@ -1,3 +1,7 @@
unixsocket /run/redis-socket/redis.sock
unixsocketperm 777
port 0
save 900 1
save 300 10
save 60 10000
Executable → Regular
View File
+67 -61
View File
@@ -1,88 +1,94 @@
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.
name: Docker Build and Publish
on:
push:
branches: [ "dev" ]
# Publish semver tags as releases.
tags: [ 'v*.*.*' ]
branches: ["legacy", "dev"]
workflow_dispatch:
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
- uses: satackey/action-docker-layer-caching@v0.0.11
continue-on-error: true
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: 'arm64,amd64'
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
platforms: linux/amd64,linux/arm64
driver-opts: |
image=moby/buildkit:latest
- name: Login to registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=sha,format=long
type=raw,value=new
- name: Get version
id: get_version
run: echo "version=$(git describe --tags --always)" >> $GITHUB_OUTPUT
- name: Update version in app.php
run: |
VERSION=$(date '+%Y%m%d')-$(git rev-parse --short HEAD)
sed -i "s/'version' => '.*'/'version' => '$VERSION'/g" config/app.php
echo "Updated version to: $VERSION"
- name: Build and push
id: build-and-push
uses: docker/build-push-action@v5
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
cache-from: type=gha
cache-to: type=gha,mode=max
tags: |
${{ env.REGISTRY }}/${{ github.repository_owner }}/xboard:legacy
${{ env.REGISTRY }}/${{ github.repository_owner }}/xboard:dev
${{ env.REGISTRY }}/${{ github.repository_owner }}/xboard
${{ env.REGISTRY }}/${{ github.repository_owner }}/xboard:latest
${{ env.REGISTRY }}/${{ github.repository_owner }}/xboard:${{ steps.get_version.outputs.version }}
build-args: |
BUILDKIT_INLINE_CACHE=1
provenance: false
- name: Install cosign
uses: sigstore/cosign-installer@v3.4.0
with:
cosign-release: 'v2.2.2'
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.2.0
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@v3.1.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5.5.1
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Get version
id: get_version
run: echo "version=$(git describe --tags --always)" >> $GITHUB_OUTPUT
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v5.3.0
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ env.REGISTRY }}/${{ github.repository_owner }}/xboard:latest,${{ env.REGISTRY }}/${{ github.repository_owner }}/xboard,${{ env.REGISTRY }}/${{ github.repository_owner }}/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
- name: Sign image
if: steps.build-and-push.outputs.digest != ''
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}
COSIGN_EXPERIMENTAL: 1
run: |
echo "${{ steps.meta.outputs.tags }}" | xargs -I {} cosign sign --yes "{}@${{ steps.build-and-push.outputs.digest }}"
+2 -1
View File
@@ -11,7 +11,8 @@ COPY .docker /
COPY . /www
RUN composer install --optimize-autoloader --no-cache --no-dev \
&& php artisan storage:link \
&& cp /www/.env.example /www/.env \
&& chown -R www:www /www \
&& chmod -R 775 /www
CMD /usr/bin/supervisord --nodaemon -c /etc/supervisor/supervisord.conf
CMD ["/usr/bin/supervisord", "--nodaemon", "-c", "/etc/supervisor/supervisord.conf"]
+46 -46
View File
@@ -1,27 +1,27 @@
# 关于Xboard
Xboard是基于V2board二次开发,在性能上和功能上都有大部分增强的**面板
# About Xboard
Xboard is a panel based on V2board's secondary development, with significant enhancements in both performance and functionality.
# 免责声明
本项目只是本人个人学习开发并维护,本人不保证任何可用性,也不对使用本软件造成的任何后果负责。
# Disclaimer
This project is personally developed and maintained by me for learning purposes. I do not guarantee any availability and am not responsible for any consequences resulting from the use of this software.
# Xboard 特点
基于V2board 二次开发,增加了以下特性
- 升级Laravel10
- 适配Laravels (提升至10+倍并发)
- 适配Webman (比laravels快50%左右)
- 修改配置从数据库中获取
- 支持Docker部署、分布式部署
- 支持根据用户IP归属地来下发订阅
- 增加Hy2支持
- 增加sing-box下发
- 支持直接从cloudflare获取访问者真实IP
- 支持根据客户端版本自动下发新协议
- 支持线路筛选(订阅地址后面增加 &filter=香港|美国)
- 支持Sqlite安装(代替Mysql,自用用户福音)
- 使用Vue3 + TypeScript + NaiveUI + Unocss + Pinia重构用户前端
- 修复大量BUG
# Xboard Features
Based on V2board's secondary development, with the following added features:
- Upgraded to Laravel 10
- Adapted to Laravels (10+ times concurrent improvement)
- Adapted to Webman (about 50% faster than laravels)
- Modified configuration retrieval from database
- Support for Docker deployment and distributed deployment
- Support for subscription distribution based on user IP location
- Added Hy2 support
- Added sing-box distribution
- Support for obtaining real visitor IP directly from Cloudflare
- Support for automatic new protocol distribution based on client version
- Support for route filtering (add &filter=HongKong|USA after subscription URL)
- Support for Sqlite installation (alternative to MySQL, great for personal use)
- User frontend rebuilt using Vue3 + TypeScript + NaiveUI + Unocss + Pinia
- Fixed numerous bugs
# **系统架构**
# **System Architecture**
- PHP8.1+
- Composer
@@ -29,36 +29,36 @@ Xboard是基于V2board二次开发,在性能上和功能上都有大部分增
- Redis
- Laravel
## 性能对比 [查看详情](./docs/性能对比.md)
> xboard 无论前端还是后端性能都有巨大的提升
## Performance Comparison [View Details](./docs/性能对比.md)
> xboard shows tremendous performance improvements in both frontend and backend
|场景 | php-fpm(传统) | php-fpm(传统开启opcache) | laravels | webman(docker)|
|---- | ---- |---- |----| ---|
|首页 | 6请求/秒 | 157请求/秒 | 477请求/秒 | 803请求/秒 |
|用户订阅 | 6请求/秒 | 196请求/秒 | 586请求/秒 | 1064请求/秒 |
|用户首页延迟| 308ms | 110ms | 101ms | 98ms |
|Scenario | php-fpm(traditional) | php-fpm(traditional with opcache) | laravels | webman(docker)|
|---- | ---- |---- |---- | ---|
|Homepage | 6 req/s | 157 req/s | 477 req/s| 803 req/s|
|User Subscription| 6 req/s | 196 req/s | 586 req/s| 1064 req/s|
|User Homepage Latency| 308ms | 110ms | 101ms | 98ms|
## 页面展示
![示例图片](./docs/images/dashboard.png)
## Page Display
![Example Image](./docs/images/dashboard.png)
## 安装 / 更新 / 回滚
你可以点击查看下列方式的**安装、更新**步骤
- [1panel 部署](./docs/1panel安装指南.md)
- [Docker Compose 纯命令行快速部署](./docs/docker-compose安装指南.md)
- [aapanel + Docker Compose (推荐)](./docs/aapanel+docker安装指南.md)
- [aapanel 部署](./docs/aapanel安装指南.md)
### 从其他版本迁移
#### 数据库迁移
**根据你的版本查看对应的迁移指南进行迁移**
- v2board dev 23/10/27的版本 [点击跳转迁移指南](./docs/v2b_dev迁移指南.md)
- v2board 1.7.4 [点击跳转迁移指南](./docs/v2b_1.7.4迁移指南.md)
- v2board 1.7.3 [点击跳转迁移指南](./docs/v2b_1.7.3迁移指南.md)
- v2board wyx2685 [点击跳转迁移指南](./docs/v2b_wyx2685迁移指南.md)
## Installation / Update / Rollback
You can click to view the **installation and update** steps for the following methods:
- [1panel Deployment](./docs/1panel安装指南.md)
- [Docker Compose Command-line Quick Deployment](./docs/docker-compose安装指南.md)
- [aapanel + Docker Compose (Recommended)](./docs/aapanel+docker安装指南.md)
- [aapanel Deployment](./docs/aapanel安装指南.md)
### Migrating from Other Versions
#### Database Migration
**Check the corresponding migration guide according to your version**
- v2board dev version 23/10/27 [Jump to Migration Guide](./docs/v2b_dev迁移指南.md)
- v2board 1.7.4 [Jump to Migration Guide](./docs/v2b_1.7.4迁移指南.md)
- v2board 1.7.3 [Jump to Migration Guide](./docs/v2b_1.7.3迁移指南.md)
- v2board wyx2685 [Jump to Migration Guide](./docs/v2b_wyx2685迁移指南.md)
### 注意
> 修改后台路径需要重启才能生效
### Note
> Modifying the admin path requires a restart to take effect
```
docker compose restart
```
> 如果是是aapanel安装则需要重启 webman守护进程
> If using aapanel installation, you need to restart the webman daemon process
+2 -1
View File
@@ -26,9 +26,10 @@ class BackupDatabase extends Command
}
// 数据库备份逻辑
$databaseBackupPath = storage_path('backup/' . now()->format('Y-m-d_H-i-s') . '_' . config('database.connections.mysql.database') . '_database_backup.sql');
$compressedBackupPath = $databaseBackupPath . '.gz';
try{
if (config('database.default') === 'mysql'){
$databaseBackupPath = storage_path('backup/' . now()->format('Y-m-d_H-i-s') . '_' . config('database.connections.mysql.database') . '_database_backup.sql');
$this->info("1️⃣:开始备份Mysql");
\Spatie\DbDumper\Databases\MySql::create()
->setHost(config('database.connections.mysql.host'))
+4 -3
View File
@@ -51,11 +51,12 @@ class MigrateFromV2b extends Command
],
'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 order_amount paid_total INT COMMENT '订单合计';",
"ALTER TABLE `v2_stat` CHANGE COLUMN order_count paid_count INT COMMENT '邀请佣金';",
"ALTER TABLE `v2_stat` CHANGE COLUMN commission_amount commission_total INT COMMENT '佣金合计';",
"ALTER TABLE `v2_stat`
ADD COLUMN paid_count INT NULL,
ADD COLUMN paid_total INT NULL,
ADD COLUMN order_count INT NULL,
ADD COLUMN order_total INT NULL,
ADD COLUMN register_count INT NULL,
ADD COLUMN invite_count INT NULL,
ADD COLUMN transfer_used_total VARCHAR(32) NULL;
+6 -3
View File
@@ -46,6 +46,9 @@ class XboardInstall extends Command
{
try {
$isDocker = env('docker', false);
$enableSqlite = env('enable_sqlite', false);
$enableRedis = env('enable_redis', false);
$adminAccount = env('admin_account', '');
$this->info("__ __ ____ _ ");
$this->info("\ \ / /| __ ) ___ __ _ _ __ __| | ");
$this->info(" \ \/ / | __ \ / _ \ / _` | '__/ _` | ");
@@ -67,7 +70,7 @@ class XboardInstall extends Command
return;
}
// 选择是否使用Sqlite
if (confirm(label: '是否启用Sqlite(无需额外安装)代替Mysql', default: false, yes: '启用', no: '不启用')) {
if ($enableSqlite || confirm(label: '是否启用Sqlite(无需额外安装)代替Mysql', default: false, yes: '启用', no: '不启用')) {
$sqliteFile = '.docker/.data/database.sqlite';
if (!file_exists(base_path($sqliteFile))) {
// 创建空文件
@@ -142,7 +145,7 @@ class XboardInstall extends Command
$isReidsValid = false;
while (!$isReidsValid) {
// 判断是否为Docker环境
if ($isDocker == 'true' && (confirm(label: '是否启用Docker内置的Redis', default: true, yes: '启用', no: '不启用'))) {
if ($isDocker == 'true' && ($enableRedis || confirm(label: '是否启用Docker内置的Redis', default: true, yes: '启用', no: '不启用'))) {
$envConfig['REDIS_HOST'] = '/run/redis-socket/redis.sock';
$envConfig['REDIS_PORT'] = 0;
$envConfig['REDIS_PASSWORD'] = null;
@@ -175,7 +178,7 @@ class XboardInstall extends Command
abort(500, '复制环境文件失败,请检查目录权限');
}
;
$email = text(
$email = !empty($adminAccount) ? $adminAccount : text(
label: '请输入管理员账号',
default: 'admin@demo.com',
required: true,
@@ -73,7 +73,7 @@ class UserController extends Controller
$res[$i]['plan_name'] = $plan[$k]['name'];
}
}
$res[$i]['subscribe_url'] = Helper::getSubscribeUrl('/api/v1/client/subscribe?token=' . $res[$i]['token']);
$res[$i]['subscribe_url'] = Helper::getSubscribeUrl( $res[$i]['token']);
}
return response([
'data' => $res,
@@ -162,7 +162,7 @@ class UserController extends Controller
$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']);
$subscribeUrl = Helper::getSubscribeUrl($user['token']);
$data .= "{$user['email']},{$balance},{$commissionBalance},{$transferEnable},{$notUseFlow},{$expireDate},{$planName},{$subscribeUrl}\r\n";
}
echo "\xEF\xBB\xBF" . $data;
@@ -240,7 +240,7 @@ class UserController extends Controller
$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']);
$subscribeUrl = Helper::getSubscribeUrl($user['token']);
$data .= "{$user['email']},{$password},{$expireDate},{$user['uuid']},{$createDate},{$subscribeUrl}\r\n";
}
echo $data;
@@ -24,6 +24,7 @@ class ClientController extends Controller
'ClashX Meta' => '1.3.5',
'Hiddify' => '0.1.0',
'loon' => '637',
'v2rayng' => '1.9.5',
'v2rayN' => '6.31',
'surge' => '2398'
];
@@ -25,7 +25,7 @@ class KnowledgeController extends Controller
if (!$userService->isAvailable($user)) {
$this->formatAccessData($knowledge['body']);
}
$subscribeUrl = Helper::getSubscribeUrl("/api/v1/client/subscribe?token={$user['token']}");
$subscribeUrl = Helper::getSubscribeUrl($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']);
@@ -13,7 +13,7 @@ class StatController extends Controller
{
public function getTrafficLog(Request $request)
{
$startDate = now()->startOfMonth();
$startDate = now()->startOfMonth()->timestamp;
$records = StatUser::query()
->where('user_id', $request->user['id'])
->where('record_at', '>=', $startDate)
@@ -140,7 +140,7 @@ class UserController extends Controller
return $this->fail([400, __('Subscription plan does not exist')]);
}
}
$user['subscribe_url'] = Helper::getSubscribeUrl("/api/v1/client/subscribe?token={$user['token']}");
$user['subscribe_url'] = Helper::getSubscribeUrl($user['token']);
$userService = new UserService();
$user['reset_day'] = $userService->getResetDay($user);
return $this->success($user);
@@ -157,7 +157,7 @@ class UserController extends Controller
if (!$user->save()) {
return $this->fail([400, __('Reset failed')]);
}
return $this->success(Helper::getSubscribeUrl('/api/v1/client/subscribe?token=' . $user->token));
return $this->success(Helper::getSubscribeUrl($user->token));
}
public function update(UserUpdate $request)
+3 -1
View File
@@ -31,6 +31,7 @@ class Server
$request->validate([
'token' => [
"string",
"required",
function ($attribute, $value, $fail) {
if ($value !== admin_setting('server_token')) {
$fail('The ' . $attribute . ' is invalid.');
@@ -39,10 +40,11 @@ class Server
],
'node_id' => 'required',
'node_type' => [
'required',
'nullable',
'regex:/^(?i)(hysteria|hysteria2|vless|trojan|vmess|v2ray|tuic|shadowsocks|shadowsocks-plugin)$/',
function ($attribute, $value, $fail) use ($aliasTypes, $request) {
$request->merge([$attribute => strtolower(isset ($aliasTypes[$value]) ? $aliasTypes[$value] : $value)]);
$request->merge([$attribute => strtolower(isset($aliasTypes[$value]) ? $aliasTypes[$value] : $value)]);
},
]
], [
+1 -1
View File
@@ -12,7 +12,7 @@ class ClientRoute
'middleware' => 'client'
], function ($router) {
// Client
$router->get('/subscribe', 'V1\\Client\\ClientController@subscribe');
$router->get('/subscribe', 'V1\\Client\\ClientController@subscribe')->name('client.subscribe');
// App
$router->get('/app/getConfig', 'V1\\Client\\AppController@getConfig');
$router->get('/app/getVersion', 'V1\\Client\\AppController@getVersion');
+10 -27
View File
@@ -2,6 +2,7 @@
namespace App\Jobs;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
@@ -17,7 +18,7 @@ class BatchTrafficFetchJob implements ShouldQueue
protected $protocol;
protected $timestamp;
public $tries = 1;
public $timeout = 10;
public $timeout = 20;
/**
* Create a new job instance.
@@ -36,34 +37,16 @@ class BatchTrafficFetchJob implements ShouldQueue
public function handle(): void
{
// 获取子节点
$targetServer = $this->childServer ?? $this->server;
foreach ($this->data as $uid => $v) {
$u = $v[0];
$d = $v[1];
$result = \DB::transaction(function () use ($uid, $u, $d, $targetServer) {
$user = \DB::table('v2_user')->lockForUpdate()->where('id', $uid)->first();
if (!$user) {
return true;
}
$newTime = time();
$newU = $user->u + ($u * $targetServer['rate']);
$newD = $user->d + ($d * $targetServer['rate']);
$rows = \DB::table('v2_user')
->where('id', $uid)
->update([
't' => $newTime,
'u' => $newU,
'd' => $newD,
]);
if ($rows === 0) {
return false;
}
return true;
}, 3);
if (!$result) {
TrafficFetchJob::dispatch($u, $d, $uid, $targetServer, $this->protocol);
}
User::where('id', $uid)
->incrementEach(
[
'u' => $v[0] * $targetServer['rate'],
'd' => $v[1] * $targetServer['rate'],
],
['t' => time()]
);
}
}
}
+2 -3
View File
@@ -76,14 +76,13 @@ class BTCPay
//NOT BTCPay-Sig
//API doc is WRONG!
$headerName = 'Btcpay-Sig';
$signraturHeader = isset($headers[$headerName]) ? $headers[$headerName] : '';
$signatureHeader = isset($headers[$headerName]) ? $headers[$headerName] : '';
$json_param = json_decode($payload, true);
$computedSignature = "sha256=" . \hash_hmac('sha256', $payload, $this->config['btcpay_webhook_key']);
if (!self::hashEqual($signraturHeader, $computedSignature)) {
if (!self::hashEqual($signatureHeader, $computedSignature)) {
throw new ApiException('HMAC signature does not match', 400);
return false;
}
//get order id store in metadata
+9 -1
View File
@@ -27,7 +27,12 @@ class EPay
'label' => 'KEY',
'description' => '',
'type' => 'input',
]
],
'type' => [
'label' => 'TYPE',
'description' => 'alipay / qqpay / wxpay',
'type' => 'input',
],
];
}
@@ -41,6 +46,9 @@ class EPay
'out_trade_no' => $order['trade_no'],
'pid' => $this->config['pid']
];
if(optional($this->config)['type']){
$params['type'] = $this->config['type'];
}
ksort($params);
reset($params);
$str = stripslashes(urldecode(http_build_query($params))) . $this->config['key'];
+210
View File
@@ -0,0 +1,210 @@
<?php
/**
* 自己写别抄,抄NMB抄
*/
namespace App\Payments;
use App\Exceptions\ApiException;
class StripeALLInOne {
public function __construct($config)
{
$this->config = $config;
}
public function form()
{
return [
'currency' => [
'label' => '货币单位',
'description' => '请使用符合ISO 4217标准的三位字母,例如GBP',
'type' => 'input',
],
'stripe_sk_live' => [
'label' => 'SK_LIVE',
'description' => '',
'type' => 'input',
],
'stripe_webhook_key' => [
'label' => 'WebHook密钥签名',
'description' => 'whsec_....',
'type' => 'input',
],
'description' => [
'label' => '自定义商品介绍',
'description' => '',
'type' => 'input',
],
'payment_method' => [
'label' => '支付方式',
'description' => '请输入alipay, wechat_pay, cards',
'type' => 'input',
]
];
}
public function pay($order)
{
$currency = $this->config['currency'];
$exchange = $this->exchange('CNY', strtoupper($currency));
if (!$exchange) {
throw new ApiException('Currency conversion has timed out, please try again later', 500);
}
//jump url
$jumpUrl = null;
$actionType = 0;
$stripe = new \Stripe\StripeClient($this->config['stripe_sk_live']);
if ($this->config['payment_method'] != "cards"){
$stripePaymentMethod = $stripe->paymentMethods->create([
'type' => $this->config['payment_method'],
]);
// 准备支付意图的基础参数
$params = [
'amount' => floor($order['total_amount'] * $exchange),
'currency' => $currency,
'confirm' => true,
'payment_method' => $stripePaymentMethod->id,
'automatic_payment_methods' => ['enabled' => true],
'statement_descriptor_suffix' => 'sub-' . $order['user_id'] . '-' . substr($order['trade_no'], -8),
'description' => $this->config['description'],
'metadata' => [
'user_id' => $order['user_id'],
'out_trade_no' => $order['trade_no'],
'identifier' => ''
],
'return_url' => $order['return_url']
];
// 如果支付方式为 wechat_pay,添加相应的支付方式选项
if ($this->config['payment_method'] === 'wechat_pay') {
$params['payment_method_options'] = [
'wechat_pay' => [
'client' => 'web'
],
];
}
//更新支持最新的paymentIntents方法,Sources API将在今年被彻底替
$stripeIntents = $stripe->paymentIntents->create($params);
$nextAction = null;
if (!$stripeIntents['next_action']) {
throw new ApiException(__('Payment gateway request failed'));
}else {
$nextAction = $stripeIntents['next_action'];
}
switch ($this->config['payment_method']){
case "alipay":
if (isset($nextAction['alipay_handle_redirect'])){
$jumpUrl = $nextAction['alipay_handle_redirect']['url'];
$actionType = 1;
}else {
throw new ApiException('unable get Alipay redirect url', 500);
}
break;
case "wechat_pay":
if (isset($nextAction['wechat_pay_display_qr_code'])){
$jumpUrl = $nextAction['wechat_pay_display_qr_code']['data'];
}else {
throw new ApiException('unable get WeChat Pay redirect url', 500);
}
}
} else {
$creditCheckOut = $stripe->checkout->sessions->create([
'success_url' => $order['return_url'],
'client_reference_id' => $order['trade_no'],
'payment_method_types' => ['card'],
'line_items' => [
[
'price_data' => [
'currency' => $currency,
'unit_amount' => floor($order['total_amount'] * $exchange),
'product_data' => [
'name' => 'sub-' . $order['user_id'] . '-' . substr($order['trade_no'], -8),
'description' => $this->config['description'],
]
],
'quantity' => 1,
],
],
'mode' => 'payment',
]);
$jumpUrl = $creditCheckOut['url'];
$actionType = 1;
}
return [
'type' => $actionType,
'data' => $jumpUrl
];
}
public function notify($params)
{
try {
\Stripe\Stripe::setApiKey($this->config['stripe_sk_live']);
//Workerman不支持使用php://input, stripe同时要求验证签名的payload不能经过修改,所以使用这个方法
$payload = $GLOBALS['HTTP_RAW_POST_DATA'];
$headers = getallheaders();
$headerName = 'Stripe-Signature';
$signatureHeader = $headers[$headerName] ?? '';
$event = \Stripe\Webhook::constructEvent(
$payload,
$signatureHeader,
$this->config['stripe_webhook_key']
);
} catch (\UnexpectedValueException $e){
throw new ApiException('Error parsing payload', 400);
}
catch (\Stripe\Exception\SignatureVerificationException $e) {
throw new ApiException('signature not match', 400);
}
switch ($event->type) {
case 'payment_intent.succeeded':
$object = $event->data->object;
if ($object->status === 'succeeded') {
if (!isset($object->metadata->out_trade_no)) {
return('order error');
}
$metaData = $object->metadata;
$tradeNo = $metaData->out_trade_no;
return [
'trade_no' => $tradeNo,
'callback_no' => $object->id
];
}
break;
case 'checkout.session.completed':
$object = $event->data->object;
if ($object->payment_status === 'paid') {
return [
'trade_no' => $object->client_reference_id,
'callback_no' => $object->payment_intent
];
}
break;
case 'checkout.session.async_payment_succeeded':
$object = $event->data->object;
return [
'trade_no' => $object->client_reference_id,
'callback_no' => $object->payment_intent
];
break;
default:
throw new ApiException('event is not support');
}
return('success');
}
private function exchange($from, $to)
{
$from = strtolower($from);
$to = strtolower($to);
$result = file_get_contents("https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/" . $from . ".min.json");
$result = json_decode($result, true);
return $result[$from][$to];
}
}
@@ -7,7 +7,7 @@ use App\Plugins\Telegram\Telegram;
class GetLatestUrl extends Telegram {
public $command = '/getlatesturl';
public $description = '将Telegram账号绑定到网站';
public $description = '获取网站最新网址';
public function handle($message, $match = []) {
$telegramService = $this->telegramService;
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace App\Plugins\Telegram\Commands;
use App\Plugins\Telegram\Telegram;
class Start extends Telegram {
public $command = '/start';
public $description = 'telegram机器人初始化';
public function handle($message, $match = []) {
if (!$message->is_private) return;
$telegramService = $this->telegramService;
$text = "/start 显示所有可用指令\n /bind+空格+订阅链接,将telegram绑定至账户\n /traffic 获取当前使用流量 \n /getlatesturl 获取网站最新网址 \n /unbind 解绑telegram账户";
$telegramService->sendMessage($message->chat_id, $text, 'markdown');
}
}
+11 -5
View File
@@ -12,11 +12,17 @@ class UnBind extends Telegram {
public function handle($message, $match = []) {
if (!$message->is_private) return;
$user = User::where('telegram_id', $message->chat_id)->first();
$telegramService = $this->telegramService;
if (!$user) {
$telegramService->sendMessage($message->chat_id, '没有查询到您的用户信息,请先绑定账号', 'markdown');
return;
if (!isset($message->args[0])) {
$user = User::where('telegram_id', $message->chat_id)->first();
} else {
$chat = User::where('telegram_id', $message->chat_id)->first();
if (!$chat) return;
if (!($chat->is_admin || $chat->is_staff)) return;
if (strpos($message->args[0], '@') !== true) {
$user = User::where('email', $message->args[0])->first();
} else {
$user = User::where('telegram_id', $message->args[0])->first();
}
}
$user->telegram_id = NULL;
if (!$user->save()) {
+6 -9
View File
@@ -88,7 +88,8 @@ class ClashMeta
return response($yaml, 200)
->header('subscription-userinfo', "upload={$user['u']}; download={$user['d']}; total={$user['transfer_enable']}; expire={$user['expired_at']}")
->header('profile-update-interval', '24')
->header('content-disposition', 'attachment;filename*=UTF-8\'\'' . rawurlencode($appName));
->header('content-disposition', 'attachment;filename*=UTF-8\'\'' . rawurlencode($appName))
->header('profile-web-page-url', admin_setting('app_url'));
}
/**
@@ -194,8 +195,6 @@ class ClashMeta
$array['server'] = $server['host'];
$array['port'] = $server['port'];
$array['uuid'] = $password;
$array['alterId'] = 0;
$array['cipher'] = 'auto';
$array['udp'] = true;
// XTLS流控算法
@@ -207,8 +206,8 @@ class ClashMeta
$array['tls'] = true;
if ($server['tls_settings']) {
$tlsSettings = $server['tls_settings'];
if (isset($tlsSettings['allowInsecure']) && !empty($tlsSettings['allowInsecure']))
$array['skip-cert-verify'] = ($tlsSettings['allowInsecure'] ? true : false);
if (isset($tlsSettings['allow_insecure']) && !empty($tlsSettings['allow_insecure']))
$array['skip-cert-verify'] = ($tlsSettings['allow_insecure'] ? true : false);
if (isset($tlsSettings['server_name']) && !empty($tlsSettings['server_name']))
$array['servername'] = $tlsSettings['server_name'];
}
@@ -242,10 +241,8 @@ class ClashMeta
$array['ws-opts']['path'] = $wsSettings['path'];
if (isset($wsSettings['headers']['Host']) && !empty($wsSettings['headers']['Host']))
$array['ws-opts']['headers'] = ['Host' => $wsSettings['headers']['Host']];
if (isset($wsSettings['path']) && !empty($wsSettings['path']))
$array['ws-path'] = $wsSettings['path'];
if (isset($wsSettings['headers']['Host']) && !empty($wsSettings['headers']['Host']))
$array['ws-headers'] = ['Host' => $wsSettings['headers']['Host']];
$array['ws-opts']['max-early-data'] = 2560;
$array['ws-opts']['early-data-header-name'] = 'Sec-WebSocket-Protocol';
}
}
if ($server['network'] === 'grpc') {
+32
View File
@@ -36,6 +36,9 @@ class General
if ($item['type'] === 'trojan') {
$uri .= self::buildTrojan($user['uuid'], $item);
}
if ($item['type'] === 'hysteria') {
$uri .= self::buildHysteria($user['uuid'], $item);
}
}
return base64_encode($uri);
}
@@ -170,4 +173,33 @@ class General
return $uri;
}
public static function buildHysteria($password, $server)
{
$params = [];
// Return empty if version is not 2
if ($server['version'] !== 2) {
return '';
}
if ($server['server_name']) {
$params['sni'] = $server['server_name'];
$params['security'] = 'tls';
}
if ($server['is_obfs']) {
$params['obfs'] = 'salamander';
$params['obfs-password'] = $server['server_key'];
}
$params['insecure'] = $server['insecure'] ? 1 : 0;
$query = http_build_query($params);
$name = rawurlencode($server['name']);
$uri = "hysteria2://{$password}@{$server['host']}:{$server['port']}?{$query}#{$name}";
$uri .= "\r\n";
return $uri;
}
}
+1 -1
View File
@@ -151,7 +151,7 @@ class Loon
$server['host'],
$server['port'],
$password,
$server['server_name'] ? "tls={$server['server_name']}" : "(null)"
$server['server_name'] ? "sni={$server['server_name']}" : "(null)"
];
if ($server['insecure']) $config[] = "skip-cert-verify=true";
$config[] = "download-bandwidth=" . ($user->speed_limit ? min($server['down_mbps'], $user->speed_limit) : $server['down_mbps']);
+3
View File
@@ -34,6 +34,9 @@ class Passwall
if ($item['type'] === 'trojan') {
$uri .= self::buildTrojan($user['uuid'], $item);
}
if ($item['type'] === 'hysteria') {
$uri .= General::buildHysteria($user['uuid'], $item);
}
}
return base64_encode($uri);
}
+3
View File
@@ -34,6 +34,9 @@ class SSRPlus
if ($item['type'] === 'trojan') {
$uri .= self::buildTrojan($user['uuid'], $item);
}
if ($item['type'] === 'hysteria') {
$uri .= General::buildHysteria($user['uuid'], $item);
}
}
return base64_encode($uri);
}
+1 -1
View File
@@ -190,7 +190,7 @@ class Shadowrocket
if ($server['network_settings']) {
$wsSettings = $server['network_settings'];
if (isset($wsSettings['path']) && !empty($wsSettings['path']))
$config['path'] = $wsSettings['path'];
$config['path'] = $wsSettings['path'] . '?ed=2560';
if (isset($wsSettings['headers']['Host']) && !empty($wsSettings['headers']['Host']))
$config['obfsParam'] = $wsSettings['headers']['Host'];
}
+6 -3
View File
@@ -26,6 +26,7 @@ class SingBox
return response()
->json($this->config)
->header('profile-title', 'base64:'. base64_encode($appName))
->header('subscription-userinfo', "upload={$user['u']}; download={$user['d']}; total={$user['transfer_enable']}; expire={$user['expired_at']}")
->header('profile-update-interval', '24');
}
@@ -148,7 +149,7 @@ class SingBox
$array['transport']['path'] = $wsSettings['path'];
if (isset($wsSettings['headers']['Host']) && !empty($wsSettings['headers']['Host']))
$array['transport']['headers'] = ['Host' => array($wsSettings['headers']['Host'])];
$array['transport']['max_early_data'] = 2048;
$array['transport']['max_early_data'] = 2560;
$array['transport']['early_data_header_name'] = 'Sec-WebSocket-Protocol';
}
}
@@ -216,7 +217,7 @@ class SingBox
$array['transport']['path'] = $wsSettings['path'];
if (isset($wsSettings['headers']['Host']) && !empty($wsSettings['headers']['Host']))
$array['transport']['headers'] = ['Host' => array($wsSettings['headers']['Host'])];
$array['transport']['max_early_data'] = 2048;
$array['transport']['max_early_data'] = 2560;
$array['transport']['early_data_header_name'] = 'Sec-WebSocket-Protocol';
}
}
@@ -271,7 +272,7 @@ class SingBox
if (isset($server['network_settings']['headers']['Host'])) {
$array['transport']['headers'] = ['Host' => array($server['network_settings']['headers']['Host'])];
}
$array['transport']['max_early_data'] = 2048;
$array['transport']['max_early_data'] = 2560;
$array['transport']['early_data_header_name'] = 'Sec-WebSocket-Protocol';
}
}
@@ -309,6 +310,8 @@ class SingBox
$array['tag'] = $server['name'];
$array['type'] = 'hysteria2';
$array['password'] = $password;
$array['up_mbps'] = $user->speed_limit ? min($server['down_mbps'], $user->speed_limit) : $server['down_mbps'];
$array['down_mbps'] = $user->speed_limit ? min($server['up_mbps'], $user->speed_limit) : $server['up_mbps'];
if ($server['is_obfs']) {
$array['obfs']['type'] = 'salamander';
+1 -1
View File
@@ -63,7 +63,7 @@ class Surfboard
}
// Subscription link
$subsURL = Helper::getSubscribeUrl("/api/v1/client/subscribe?token={$user['token']}");
$subsURL = Helper::getSubscribeUrl($user['token']);
$subsDomain = request()->header('Host');
$config = str_replace('$subs_link', $subsURL, $config);
+4 -3
View File
@@ -32,7 +32,9 @@ class Surge
'aes-128-gcm',
'aes-192-gcm',
'aes-256-gcm',
'chacha20-ietf-poly1305'
'chacha20-ietf-poly1305',
'2022-blake3-aes-128-gcm',
'2022-blake3-aes-256-gcm',
])
) {
// [Proxy]
@@ -69,9 +71,8 @@ class Surge
}
// Subscription link
$subsURL = Helper::getSubscribeUrl("/api/v1/client/subscribe?token={$user['token']}");
$subsDomain = request()->header('Host');
$subsURL = 'https://' . $subsDomain . '/api/v1/client/subscribe?token=' . $user['token'];
$subsURL = Helper::getSubscribeUrl($user['token'], $subsDomain ? 'https://' . $subsDomain : null);
$config = str_replace('$subs_link', $subsURL, $config);
$config = str_replace('$subs_domain', $subsDomain, $config);
+1 -21
View File
@@ -37,7 +37,7 @@ class V2rayN
$uri .= self::buildTrojan($user['uuid'], $item);
}
if ($item['type'] === 'hysteria') {
$uri .= self::buildHysteria($user['uuid'], $item);
$uri .= General::buildHysteria($user['uuid'], $item);
}
}
@@ -192,25 +192,5 @@ class V2rayN
return $uri;
}
public static function buildHysteria($password, $server)
{
$name = rawurlencode($server['name']);
$params = [];
if ($server['server_name']) $params['sni'] = $server['server_name'];
$params['insecure'] = $server['insecure'] ? 1 : 0;
if($server['is_obfs']) {
$params['obfs'] = 'salamander';
$params['obfs-password'] = $server['server_key'];
}
$query = http_build_query($params);
if ($server['version'] == 2) {
$uri = "hysteria2://{$password}@{$server['host']}:{$server['port']}?{$query}#{$name}";
$uri .= "\r\n";
} else {
// V2rayN似乎不支持v1, 返回空
$uri = "";
}
return $uri;
}
}
+3 -1
View File
@@ -34,6 +34,9 @@ class V2rayNG
if ($item['type'] === 'vless') {
$uri .= self::buildVless($user['uuid'], $item);
}
if ($item['type'] === 'hysteria') {
$uri .= General::buildHysteria($user['uuid'], $item);
}
}
return base64_encode($uri);
}
@@ -190,5 +193,4 @@ class V2rayNG
return $uri;
}
}
+6 -4
View File
@@ -196,7 +196,7 @@ class ServerService
// 获取可用的用户列表
public static function getAvailableUsers($groupId): Collection
{
return \DB::table('v2_user')
return User::toBase()
->whereIn('group_id', $groupId)
->whereRaw('u + d < transfer_enable')
->where(function ($query) {
@@ -314,9 +314,11 @@ class ServerService
$servers[$k]['online'] = Cache::get(CacheKey::get("SERVER_{$serverType}_ONLINE_USER", $v['parent_id'] ?? $v['id'])) ?? 0;
// 如果是子节点,先尝试从缓存中获取
if($pid = $v['parent_id']){
// 获取缓存
$onlineUsers = Cache::get(CacheKey::get('MULTI_SERVER_' . $serverType . '_ONLINE_USER', $pid)) ?? [];
$servers[$k]['online'] = (collect($onlineUsers)->whereIn('ip', $v['ips'])->sum('online_user')) . "|{$servers[$k]['online']}";
$cacheKey = CacheKey::get('MULTI_SERVER_' . $serverType . '_ONLINE_USER', $pid);
$onlineUsers = Cache::get($cacheKey) ?? [];
$onlineUserSum = collect($onlineUsers)->whereIn('ip', $v['ips'])->sum('online_user');
$online = ($onlineUserSum > 0 ? $onlineUserSum . "|" : "") . $servers[$k]['online'];
$servers[$k]['online'] = $online;
}
$servers[$k]['last_check_at'] = Cache::get(CacheKey::get("SERVER_{$serverType}_LAST_CHECK_AT", $v['parent_id'] ?? $v['id']));
$servers[$k]['last_push_at'] = Cache::get(CacheKey::get("SERVER_{$serverType}_LAST_PUSH_AT", $v['parent_id'] ?? $v['id']));
+2 -2
View File
@@ -108,14 +108,14 @@ class StatisticalService
/**
* 获取指定用户的流量使用情况
*/
public function getStatUserByUserID($userId): array
public function getStatUserByUserID(int|string $userId): array
{
$stats = [];
$statsUser = $this->redis->zrange($this->statUserKey, 0, -1, true);
foreach ($statsUser as $member => $value) {
list($rate, $uid, $type) = explode('_', $member);
if ($uid !== $userId)
if (intval($uid) !== intval($userId))
continue;
$key = "{$rate}_{$uid}";
$stats[$key] = $stats[$key] ?? [
+17 -6
View File
@@ -108,13 +108,24 @@ class Helper
}
}
public static function getSubscribeUrl($path)
public static function getSubscribeUrl(string $token, $subscribeUrl = null)
{
$subscribeUrls = explode(',', admin_setting('subscribe_url'));
$subscribeUrl = $subscribeUrls[array_rand($subscribeUrls)];
$subscribeUrl = self::replaceByPattern($subscribeUrl);
if ($subscribeUrl) return $subscribeUrl . $path;
return url($path);
$strs = 'QWERTYUIOPASDFGHJKLZXCVBNM1234567890qwertyuiopasdfghjklzxcvbnm';
$randstr = substr(str_shuffle($strs), 0, rand(4,8));
$path = route('client.subscribe', ['token' => $token], false);
if(!$subscribeUrl){
$subscribeUrls = explode(',', admin_setting('subscribe_url'));
$subscribeUrl = \Arr::random($subscribeUrls);
$subscribeUrl = self::replaceByPattern($subscribeUrl);
if (strpos($subscribeUrl, "*") !== false) {
$subscribeUrl = str_replace("*", $randstr, $subscribeUrl);
} elseif (strpos($subscribeUrl, '{uuid}') !== false) {
$user = User::where('token', $token)->first();
$subscribeUrl = str_replace('{uuid}', $user->uuid, $subscribeUrl);
}
}
return $subscribeUrl ? rtrim($subscribeUrl, '/') . $path : url($path);
}
public static function randomPort($range) {
+2 -2
View File
@@ -21,14 +21,14 @@
"guzzlehttp/guzzle": "^7.4.3",
"hhxsv5/laravel-s": "~3.7.0",
"joanhey/adapterman": "^0.6.1",
"laravel/framework": "^10.0",
"laravel/framework": "10.48.22",
"laravel/horizon": "^5.9.6",
"laravel/tinker": "^2.5",
"linfo/linfo": "^4.0",
"paragonie/sodium_compat": "^1.20",
"php-curl-class/php-curl-class": "^8.6",
"spatie/db-dumper": "^3.4",
"stripe/stripe-php": "^7.36.1",
"stripe/stripe-php": "^v14.9.0",
"symfony/http-client": "^6.4",
"symfony/mailgun-mailer": "^6.4",
"symfony/yaml": "*",
+1 -1
View File
@@ -3,7 +3,7 @@ services:
xboard:
# build:
# context: .
image: ghcr.io/cedar2025/xboard:latest
image: ghcr.io/cedar2025/xboard:legacy
volumes:
- ./:/www/
# - ./.env:/www/.env
+2
View File
@@ -106,6 +106,8 @@ docker compose up -d
🎉: 到这里,你已经可以通过域名访问你的站点了。
⚠️: 请务必开启防火墙防止7001端口暴露到公网当中。
## 更新
1. 通过 SSH 登录到服务器后,访问站点路径如:`/opt/1panel/apps/openresty/openresty/www/sites/xboard/index`,然后在站点目录中执行以下命令:
+4 -1
View File
@@ -6,6 +6,7 @@
```
# 安装Docker
curl -sSL https://get.docker.com | bash
# Centos系统可能还需要执行下面命令来启动Docker
systemctl enable docker
systemctl start docker
```
@@ -38,7 +39,7 @@ rm -rf .htaccess 404.html 502.html index.html .user.ini
```
> 执行命令从 Github 克隆到当前目录。
```
git clone https://github.com/cedar2025/Xboard.git ./
git clone -b dev https://github.com/cedar2025/Xboard.git ./
```
> 复制一份docker-compose.yaml文件
```
@@ -81,6 +82,8 @@ location ^~ / {
🎉: 到这里,你可以已经可以通过域名访问你的站点了
⚠️: 请务必开启防火墙防止7001端口暴露到公网当中。
### 更新
1. 更新代码
>通过SSH登录到服务器后访问站点路径如:/www/wwwroot/你的站点域名。
+1 -1
View File
@@ -48,7 +48,7 @@ rm -rf .htaccess 404.html 502.html index.html .user.ini
```
> 执行命令从 Github 克隆到当前目录。
```
git clone https://github.com/cedar2025/Xboard.git ./
git clone -b dev https://github.com/cedar2025/Xboard.git ./
```
> 执行命令安装依赖包以及V2board
```
+92 -43
View File
@@ -1,70 +1,119 @@
## Docker-Compose 部署教程
本文教你如何在命令行使用docker-compose + sqlite来快速部署Xboard
如果你需要使用Mysql,你需要自行处理好Mysql的安装。
### 部署 (使用docker-compose 2分钟部署)
> 在此提供Xboard安装、快速体验Xboard的步骤。
使用docker compose + sqlite 快速部署站点(**无需安装Mysql以及redis**
1. 安装docker
```
## Docker-Compose 快速部署指南
### 环境要求
- Docker (最新稳定版)
- 至少 1GB 可用内存
- 至少 10GB 可用磁盘空间
- 系统支持: Linux/macOS/Windows
- 开放端口: 7001 (默认)
### 部署步骤
#### 1. 安装 Docker
```bash
# 安装 Docker
curl -sSL https://get.docker.com | bash
# CentOS 系统需要执行以下命令启动 Docker
systemctl enable docker
systemctl start docker
```
2. 获取Docker compose 文件
```
git clone -b docker-compose --depth 1 https://github.com/cedar2025/Xboard
#### 2. 获取部署文件
```bash
git clone -b docker-compose --depth 1 https://github.com/cedar2025/Xboard
cd Xboard
```
3. 执行数据库安装命令
> 选择 **启用sqlite** 和 **Docker内置的Redis**
#### 3. 初始化安装
> 提供两种安装方式,选择其一即可:
**方式一:快速安装** (推荐)
```bash
# 使用 SQLite + Docker内置Redis
docker compose run -it --rm \
-e enable_sqlite=true \
-e enable_redis=true \
-e admin_account=admin@demo.com \
xboard php artisan xboard:install
```
**方式二:自定义安装**
```bash
# 根据提示自定义配置
docker compose run -it --rm xboard php artisan xboard:install
```
> 执行这条命令之后,会返回你的后台地址和管理员账号密码(你需要记录下来)
> 你需要执行下面的 **启动xborad** 步骤之后才能访问后台
4. 启动Xboard
```
> **重要提示:**
> - 安装完成后会显示后台地址和管理员账号密码,请务必保存
> - 如需使用 MySQL,请先自行安装并配置 MySQL 后再部署
#### 4. 启动服务
```bash
docker compose up -d
```
> 安装完成之后即可访问你的站点
5. 访问站点
> 启动之后网站端口默认为7001, 你可以配置nginx反向代理使用80端口
网站地址: http://你的IP:7001/
在此你已经成功部署了, 你可以访问网址体验Xboard的完整功能,
#### 5. 访问站点
- 网站地址:`http://服务器IP:7001`
- 后台地址:安装时提供的地址
> 如果你需要使用mysql,请自行安装Mysql后重新部署
### 更新指南
### **更新**
1. 修改版本
```
#### 方式一:快速更新(保持最新版本)
```bash
cd Xboard
vi docker-compose.yaml
```
> 修改docker-compose.yaml 当中image后面的版本号为你需要的版本
> 如果为版本为latest 则可以忽略这一步,直接进行第二步
2. 更新数据库(可以执行多次都是安全的)
```
docker compose pull
docker compose down
docker compose run -it --rm xboard php artisan xboard:update
docker compose up -d
```
> 即可更新成功
### **回滚**
> 此回滚不回滚数据库,是否回滚数据库请查看相关文档
1. 回退版本
```
#### 方式二:更新至指定版本
1. 修改版本号
```bash
# 编辑 docker-compose.yaml,修改 image 的版本号
vi docker-compose.yaml
```
> 修改docker-compose.yaml 当中image后面的版本号为更新前的版本号
2. 启动
```
2. 执行更新
```bash
docker compose pull
docker compose down
docker compose run -it --rm xboard php artisan xboard:update
docker compose up -d
```
### 注意
启用webman后做的任何代码修改都需要重启生效
### 版本回滚
```bash
# 1. 修改 docker-compose.yaml 中的版本号为目标版本
vi docker-compose.yaml
# 2. 重启服务
docker compose up -d
```
### 常见问题
1. **端口配置**
- 默认端口为 7001
- 可通过 Nginx 反向代理使用 80/443 端口
- 如需修改端口,请编辑 docker-compose.yaml
2. **数据持久化**
- 数据默认存储在 ./data 目录
- 建议定期备份 data 目录
3. **性能优化**
- 启用 webman 后的代码修改需要重启服务才能生效
- 可根据实际需求调整容器资源限制
### 安全建议
1. 及时更新到最新版本
2. 修改默认管理员账号
3. 使用强密码
4. 建议配置 SSL 证书
5. 定期备份数据
### 技术支持
- GitHub Issues: https://github.com/cedar2025/Xboard/issues
- 官方文档:[文档链接]
+4 -2
View File
@@ -49,8 +49,10 @@
"auto_route": true,
"domain_strategy": "prefer_ipv4",
"endpoint_independent_nat": true,
"inet4_address": "172.19.0.1/30",
"inet6_address": "2001:0470:f9da:fdfa::1/64",
"address": [
"172.19.0.1/30",
"2001:0470:f9da:fdfa::1/64"
],
"mtu": 9000,
"sniff": true,
"sniff_override_destination": true,
+1 -1
View File
@@ -11,7 +11,7 @@ if ! command -v git &> /dev/null; then
fi
git config --global --add safe.directory $(pwd)
git fetch --all && git reset --hard origin/dev && git pull origin dev
git fetch --all && git reset --hard origin/legacy && git pull origin legacy
rm -rf composer.lock composer.phar
wget https://github.com/composer/composer/releases/latest/download/composer.phar -O composer.phar
php composer.phar update -vvv