first commit
This commit is contained in:
100
Xboard/app/Console/Commands/BackupDatabase.php
Normal file
100
Xboard/app/Console/Commands/BackupDatabase.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Google\Cloud\Storage\StorageClient;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Symfony\Component\Process\Process;
|
||||
|
||||
class BackupDatabase extends Command
|
||||
{
|
||||
protected $signature = 'backup:database {upload?}';
|
||||
protected $description = '备份数据库并上传到 Google Cloud Storage';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$isUpload = $this->argument('upload');
|
||||
// 如果是上传到云端则判断是否存在必要配置
|
||||
if($isUpload){
|
||||
$requiredConfigs = ['database.connections.mysql', 'cloud_storage.google_cloud.key_file', 'cloud_storage.google_cloud.storage_bucket'];
|
||||
foreach ($requiredConfigs as $config) {
|
||||
if (blank(config($config))) {
|
||||
$this->error("❌:缺少必要配置项: $config , 取消备份");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 数据库备份逻辑
|
||||
try{
|
||||
if (config('database.default') === 'mysql'){
|
||||
$databaseBackupPath = storage_path('backup/' . now()->format('Y-m-d_H-i-s') . '_' . config('database.connections.mysql.database') . '_database_backup.sql');
|
||||
$this->info("1️⃣:开始备份Mysql");
|
||||
\Spatie\DbDumper\Databases\MySql::create()
|
||||
->setHost(config('database.connections.mysql.host'))
|
||||
->setPort(config('database.connections.mysql.port'))
|
||||
->setDbName(config('database.connections.mysql.database'))
|
||||
->setUserName(config('database.connections.mysql.username'))
|
||||
->setPassword(config('database.connections.mysql.password'))
|
||||
->dumpToFile($databaseBackupPath);
|
||||
$this->info("2️⃣:Mysql备份完成");
|
||||
}elseif(config('database.default') === 'sqlite'){
|
||||
$databaseBackupPath = storage_path('backup/' . now()->format('Y-m-d_H-i-s') . '_sqlite' . '_database_backup.sql');
|
||||
$this->info("1️⃣:开始备份Sqlite");
|
||||
\Spatie\DbDumper\Databases\Sqlite::create()
|
||||
->setDbName(config('database.connections.sqlite.database'))
|
||||
->dumpToFile($databaseBackupPath);
|
||||
$this->info("2️⃣:Sqlite备份完成");
|
||||
}else{
|
||||
$this->error('备份失败,你的数据库不是sqlite或者mysql');
|
||||
return;
|
||||
}
|
||||
$this->info('3️⃣:开始压缩备份文件');
|
||||
// 使用 gzip 压缩备份文件
|
||||
$compressedBackupPath = $databaseBackupPath . '.gz';
|
||||
$gzipCommand = new Process(["gzip", "-c", $databaseBackupPath]);
|
||||
$gzipCommand->run();
|
||||
|
||||
// 检查压缩是否成功
|
||||
if ($gzipCommand->isSuccessful()) {
|
||||
// 压缩成功,你可以删除原始备份文件
|
||||
file_put_contents($compressedBackupPath, $gzipCommand->getOutput());
|
||||
$this->info('4️⃣:文件压缩成功');
|
||||
unlink($databaseBackupPath);
|
||||
} else {
|
||||
// 压缩失败,处理错误
|
||||
echo $gzipCommand->getErrorOutput();
|
||||
$this->error('😔:文件压缩失败');
|
||||
unlink($databaseBackupPath);
|
||||
return;
|
||||
}
|
||||
if (!$isUpload){
|
||||
$this->info("🎉:数据库成功备份到:$compressedBackupPath");
|
||||
}else{
|
||||
// 传到云盘
|
||||
$this->info("5️⃣:开始将备份上传到Google Cloud");
|
||||
// Google Cloud Storage 配置
|
||||
$storage = new StorageClient([
|
||||
'keyFilePath' => config('cloud_storage.google_cloud.key_file'),
|
||||
]);
|
||||
$bucket = $storage->bucket(config('cloud_storage.google_cloud.storage_bucket'));
|
||||
$objectName = 'backup/' . now()->format('Y-m-d_H-i-s') . '_database_backup.sql.gz';
|
||||
// 上传文件
|
||||
$bucket->upload(fopen($compressedBackupPath, 'r'), [
|
||||
'name' => $objectName,
|
||||
]);
|
||||
|
||||
// 输出文件链接
|
||||
Log::channel('backup')->info("🎉:数据库备份已上传到 Google Cloud Storage: $objectName");
|
||||
$this->info("🎉:数据库备份已上传到 Google Cloud Storage: $objectName");
|
||||
File::delete($compressedBackupPath);
|
||||
}
|
||||
}catch(\Exception $e){
|
||||
Log::channel('backup')->error("😔:数据库备份失败 \n" . $e);
|
||||
$this->error("😔:数据库备份失败\n" . $e);
|
||||
File::delete($compressedBackupPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
129
Xboard/app/Console/Commands/CheckCommission.php
Normal file
129
Xboard/app/Console/Commands/CheckCommission.php
Normal file
@@ -0,0 +1,129 @@
|
||||
<?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) {
|
||||
try{
|
||||
DB::beginTransaction();
|
||||
if (!$this->payHandle($order->invite_user_id, $order)) {
|
||||
DB::rollBack();
|
||||
continue;
|
||||
}
|
||||
$order->commission_status = 2;
|
||||
if (!$order->save()) {
|
||||
DB::rollBack();
|
||||
continue;
|
||||
}
|
||||
DB::commit();
|
||||
} catch (\Exception $e){
|
||||
DB::rollBack();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function payHandle($inviteUserId, Order $order)
|
||||
{
|
||||
$level = 3;
|
||||
if ((int)admin_setting('commission_distribution_enable', 0)) {
|
||||
$commissionShareLevels = [
|
||||
0 => (int)admin_setting('commission_distribution_l1'),
|
||||
1 => (int)admin_setting('commission_distribution_l2'),
|
||||
2 => (int)admin_setting('commission_distribution_l3')
|
||||
];
|
||||
} else {
|
||||
$commissionShareLevels = [
|
||||
0 => 100
|
||||
];
|
||||
}
|
||||
for ($l = 0; $l < $level; $l++) {
|
||||
$inviter = User::find($inviteUserId);
|
||||
if (!$inviter) continue;
|
||||
if (!isset($commissionShareLevels[$l])) continue;
|
||||
$commissionBalance = $order->commission_balance * ($commissionShareLevels[$l] / 100);
|
||||
if (!$commissionBalance) continue;
|
||||
if ((int)admin_setting('withdraw_close_enable', 0)) {
|
||||
$inviter->increment('balance', $commissionBalance);
|
||||
} else {
|
||||
$inviter->increment('commission_balance', $commissionBalance);
|
||||
}
|
||||
if (!$inviter->save()) {
|
||||
DB::rollBack();
|
||||
return false;
|
||||
}
|
||||
CommissionLog::create([
|
||||
'invite_user_id' => $inviteUserId,
|
||||
'user_id' => $order->user_id,
|
||||
'trade_no' => $order->trade_no,
|
||||
'order_amount' => $order->total_amount,
|
||||
'get_amount' => $commissionBalance
|
||||
]);
|
||||
$inviteUserId = $inviter->invite_user_id;
|
||||
// update order actual commission balance
|
||||
$order->actual_commission_balance = $order->actual_commission_balance + $commissionBalance;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
53
Xboard/app/Console/Commands/CheckOrder.php
Normal file
53
Xboard/app/Console/Commands/CheckOrder.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?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()
|
||||
{
|
||||
Order::whereIn('status', [Order::STATUS_PENDING, Order::STATUS_PROCESSING])
|
||||
->orderBy('created_at', 'ASC')
|
||||
->lazyById(200)
|
||||
->each(function ($order) {
|
||||
OrderHandleJob::dispatch($order->trade_no);
|
||||
});
|
||||
}
|
||||
}
|
||||
64
Xboard/app/Console/Commands/CheckServer.php
Normal file
64
Xboard/app/Console/Commands/CheckServer.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?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()
|
||||
{
|
||||
$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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
51
Xboard/app/Console/Commands/CheckTicket.php
Normal file
51
Xboard/app/Console/Commands/CheckTicket.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?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()
|
||||
{
|
||||
Ticket::where('status', 0)
|
||||
->where('updated_at', '<=', time() - 24 * 3600)
|
||||
->where('reply_status', 0)
|
||||
->lazyById(200)
|
||||
->each(function ($ticket) {
|
||||
if ($ticket->user_id === $ticket->last_reply_user_id) return;
|
||||
$ticket->status = Ticket::STATUS_CLOSED;
|
||||
$ticket->save();
|
||||
});
|
||||
}
|
||||
}
|
||||
63
Xboard/app/Console/Commands/CheckTrafficExceeded.php
Normal file
63
Xboard/app/Console/Commands/CheckTrafficExceeded.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
use App\Services\NodeSyncService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
class CheckTrafficExceeded extends Command
|
||||
{
|
||||
protected $signature = 'check:traffic-exceeded';
|
||||
protected $description = '检查流量超标用户并通知节点';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$count = Redis::scard('traffic:pending_check');
|
||||
if ($count <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$pendingUserIds = array_map('intval', Redis::spop('traffic:pending_check', $count));
|
||||
|
||||
$exceededUsers = User::toBase()
|
||||
->whereIn('id', $pendingUserIds)
|
||||
->whereRaw('u + d >= transfer_enable')
|
||||
->where('transfer_enable', '>', 0)
|
||||
->where('banned', 0)
|
||||
->select(['id', 'group_id'])
|
||||
->get();
|
||||
|
||||
if ($exceededUsers->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$groupedUsers = $exceededUsers->groupBy('group_id');
|
||||
$notifiedCount = 0;
|
||||
|
||||
foreach ($groupedUsers as $groupId => $users) {
|
||||
if (!$groupId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$userIdsInGroup = $users->pluck('id')->toArray();
|
||||
$servers = Server::whereJsonContains('group_ids', (string) $groupId)->get();
|
||||
|
||||
foreach ($servers as $server) {
|
||||
if (!NodeSyncService::isNodeOnline($server->id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
NodeSyncService::push($server->id, 'sync.user.delta', [
|
||||
'action' => 'remove',
|
||||
'users' => array_map(fn($id) => ['id' => $id], $userIdsInGroup),
|
||||
]);
|
||||
$notifiedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
$this->info("Checked " . count($pendingUserIds) . " users, notified {$notifiedCount} nodes for " . $exceededUsers->count() . " exceeded users.");
|
||||
}
|
||||
}
|
||||
51
Xboard/app/Console/Commands/ClearUser.php
Normal file
51
Xboard/app/Console/Commands/ClearUser.php
Normal 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}位没有任何数据的用户");
|
||||
}
|
||||
}
|
||||
}
|
||||
42
Xboard/app/Console/Commands/HookList.php
Normal file
42
Xboard/app/Console/Commands/HookList.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class HookList extends Command
|
||||
{
|
||||
protected $signature = 'hook:list';
|
||||
protected $description = '列出系统支持的所有 hooks(静态扫描代码)';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$paths = [base_path('app'), base_path('plugins')];
|
||||
$hooks = collect();
|
||||
$pattern = '/HookManager::(call|filter|register|registerFilter)\([\'\"]([a-zA-Z0-9_.-]+)[\'\"]/';
|
||||
|
||||
foreach ($paths as $path) {
|
||||
$files = collect(
|
||||
is_dir($path) ? (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path))) : []
|
||||
)->filter(fn($f) => Str::endsWith($f, '.php'));
|
||||
foreach ($files as $file) {
|
||||
$content = @file_get_contents($file);
|
||||
if ($content && preg_match_all($pattern, $content, $matches)) {
|
||||
foreach ($matches[2] as $hook) {
|
||||
$hooks->push($hook);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$hooks = $hooks->unique()->sort()->values();
|
||||
if ($hooks->isEmpty()) {
|
||||
$this->info('未扫描到任何 hook');
|
||||
} else {
|
||||
$this->info('All Supported Hooks:');
|
||||
foreach ($hooks as $hook) {
|
||||
$this->line(' ' . $hook);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
186
Xboard/app/Console/Commands/MigrateFromV2b.php
Normal file
186
Xboard/app/Console/Commands/MigrateFromV2b.php
Normal file
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
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 paid_total INT COMMENT '订单合计';",
|
||||
"ALTER TABLE `v2_stat` CHANGE COLUMN order_count paid_count INT COMMENT '邀请佣金';",
|
||||
"ALTER TABLE `v2_stat` CHANGE COLUMN commission_amount commission_total INT COMMENT '佣金合计';",
|
||||
"ALTER TABLE `v2_stat`
|
||||
ADD COLUMN order_count INT NULL,
|
||||
ADD COLUMN order_total INT NULL,
|
||||
ADD COLUMN register_count INT NULL,
|
||||
ADD COLUMN invite_count INT NULL,
|
||||
ADD COLUMN transfer_used_total VARCHAR(32) NULL;
|
||||
",
|
||||
"CREATE TABLE `v2_log` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`title` TEXT NOT NULL,
|
||||
`level` VARCHAR(11) NULL,
|
||||
`host` VARCHAR(255) NULL,
|
||||
`uri` VARCHAR(255) NOT NULL,
|
||||
`method` VARCHAR(11) NOT NULL,
|
||||
`data` TEXT NULL,
|
||||
`ip` VARCHAR(128) NULL,
|
||||
`context` TEXT NULL,
|
||||
`created_at` INT NOT NULL,
|
||||
`updated_at` INT NOT NULL
|
||||
);",
|
||||
'CREATE TABLE `v2_server_hysteria` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`group_id` VARCHAR(255) NOT NULL,
|
||||
`route_id` VARCHAR(255) NULL,
|
||||
`name` VARCHAR(255) NOT NULL,
|
||||
`parent_id` INT NULL,
|
||||
`host` VARCHAR(255) NOT NULL,
|
||||
`port` VARCHAR(11) NOT NULL,
|
||||
`server_port` INT NOT NULL,
|
||||
`tags` VARCHAR(255) NULL,
|
||||
`rate` VARCHAR(11) NOT NULL,
|
||||
`show` BOOLEAN DEFAULT FALSE,
|
||||
`sort` INT NULL,
|
||||
`up_mbps` INT NOT NULL,
|
||||
`down_mbps` INT NOT NULL,
|
||||
`server_name` VARCHAR(64) NULL,
|
||||
`insecure` BOOLEAN DEFAULT FALSE,
|
||||
`created_at` INT NOT NULL,
|
||||
`updated_at` INT NOT NULL
|
||||
);',
|
||||
"CREATE TABLE `v2_server_vless` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`group_id` TEXT NOT NULL,
|
||||
`route_id` TEXT NULL,
|
||||
`name` VARCHAR(255) NOT NULL,
|
||||
`parent_id` INT NULL,
|
||||
`host` VARCHAR(255) NOT NULL,
|
||||
`port` INT NOT NULL,
|
||||
`server_port` INT NOT NULL,
|
||||
`tls` BOOLEAN NOT NULL,
|
||||
`tls_settings` TEXT NULL,
|
||||
`flow` VARCHAR(64) NULL,
|
||||
`network` VARCHAR(11) NOT NULL,
|
||||
`network_settings` TEXT NULL,
|
||||
`tags` TEXT NULL,
|
||||
`rate` VARCHAR(11) NOT NULL,
|
||||
`show` BOOLEAN DEFAULT FALSE,
|
||||
`sort` INT NULL,
|
||||
`created_at` INT NOT NULL,
|
||||
`updated_at` INT NOT NULL
|
||||
);",
|
||||
],
|
||||
'wyx2685' => [
|
||||
"ALTER TABLE `v2_plan` DROP COLUMN `device_limit`;",
|
||||
"ALTER TABLE `v2_server_hysteria` DROP COLUMN `version`, DROP COLUMN `obfs`, DROP COLUMN `obfs_password`;",
|
||||
"ALTER TABLE `v2_server_trojan` DROP COLUMN `network`, DROP COLUMN `network_settings`;",
|
||||
"ALTER TABLE `v2_user` DROP COLUMN `device_limit`;"
|
||||
]
|
||||
];
|
||||
|
||||
if (!$version) {
|
||||
$version = $this->choice('请选择你迁移前的V2board版本:', array_keys($sqlCommands));
|
||||
}
|
||||
|
||||
if (array_key_exists($version, $sqlCommands)) {
|
||||
|
||||
try {
|
||||
foreach ($sqlCommands[$version] as $sqlCommand) {
|
||||
// Execute SQL command
|
||||
DB::statement($sqlCommand);
|
||||
}
|
||||
|
||||
$this->info('1️⃣、数据库差异矫正成功');
|
||||
|
||||
// 初始化数据库迁移
|
||||
$this->call('db:seed', ['--class' => 'OriginV2bMigrationsTableSeeder']);
|
||||
$this->info('2️⃣、数据库迁移记录初始化成功');
|
||||
|
||||
$this->call('xboard:update');
|
||||
$this->info('3️⃣、更新成功');
|
||||
|
||||
$this->info("🎉:成功从 $version 迁移到Xboard");
|
||||
} catch (\Exception $e) {
|
||||
// An error occurred, rollback the transaction
|
||||
$this->error('迁移失败'. $e->getMessage() );
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
$this->error("你所输入的版本未找到");
|
||||
}
|
||||
}
|
||||
|
||||
public function MigrateV2ConfigToV2Settings()
|
||||
{
|
||||
Artisan::call('config:clear');
|
||||
$configValue = config('v2board') ?? [];
|
||||
|
||||
foreach ($configValue as $k => $v) {
|
||||
// 检查记录是否已存在
|
||||
$existingSetting = Setting::where('name', $k)->first();
|
||||
|
||||
// 如果记录不存在,则插入
|
||||
if ($existingSetting) {
|
||||
$this->warn("配置 {$k} 在数据库已经存在, 忽略");
|
||||
continue;
|
||||
}
|
||||
Setting::create([
|
||||
'name' => $k,
|
||||
'value' => is_array($v)? json_encode($v) : $v,
|
||||
]);
|
||||
$this->info("配置 {$k} 迁移成功");
|
||||
}
|
||||
Artisan::call('config:cache');
|
||||
|
||||
$this->info('所有配置迁移完成');
|
||||
}
|
||||
}
|
||||
34
Xboard/app/Console/Commands/NodeWebSocketServer.php
Normal file
34
Xboard/app/Console/Commands/NodeWebSocketServer.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\WebSocket\NodeWorker;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class NodeWebSocketServer extends Command
|
||||
{
|
||||
protected $signature = 'ws-server
|
||||
{action=start : start | stop | restart | reload | status}
|
||||
{--d : Start in daemon mode}
|
||||
{--host=0.0.0.0 : Listen address}
|
||||
{--port=8076 : Listen port}';
|
||||
|
||||
protected $description = 'Start the WebSocket server for node-panel synchronization';
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
global $argv;
|
||||
$action = $this->argument('action');
|
||||
|
||||
$argv[1] = $action;
|
||||
if ($this->option('d')) {
|
||||
$argv[2] = '-d';
|
||||
}
|
||||
|
||||
$host = $this->option('host');
|
||||
$port = $this->option('port');
|
||||
|
||||
$worker = new NodeWorker($host, $port);
|
||||
$worker->run();
|
||||
}
|
||||
}
|
||||
48
Xboard/app/Console/Commands/ResetLog.php
Normal file
48
Xboard/app/Console/Commands/ResetLog.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\AdminAuditLog;
|
||||
use App\Models\StatServer;
|
||||
use App\Models\StatUser;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
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();
|
||||
AdminAuditLog::where('created_at', '<', strtotime('-3 month', time()))->delete();
|
||||
}
|
||||
}
|
||||
55
Xboard/app/Console/Commands/ResetPassword.php
Normal file
55
Xboard/app/Console/Commands/ResetPassword.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?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} {password?}';
|
||||
|
||||
/**
|
||||
* 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()
|
||||
{
|
||||
$password = $this->argument('password') ;
|
||||
$user = User::byEmail($this->argument('email'))->first();
|
||||
if (!$user) abort(500, '邮箱不存在');
|
||||
$password = $password ?? Helper::guid(false);
|
||||
$user->password = password_hash($password, PASSWORD_DEFAULT);
|
||||
$user->password_algo = null;
|
||||
if (!$user->save()) abort(500, '重置失败');
|
||||
$this->info("!!!重置成功!!!");
|
||||
$this->info("新密码为:{$password},请尽快修改密码。");
|
||||
}
|
||||
}
|
||||
289
Xboard/app/Console/Commands/ResetTraffic.php
Normal file
289
Xboard/app/Console/Commands/ResetTraffic.php
Normal file
@@ -0,0 +1,289 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\TrafficResetLog;
|
||||
use App\Services\TrafficResetService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ResetTraffic extends Command
|
||||
{
|
||||
protected $signature = 'reset:traffic {--fix-null : 修正模式,重新计算next_reset_at为null的用户} {--force : 强制模式,重新计算所有用户的重置时间}';
|
||||
|
||||
protected $description = '流量重置 - 处理所有需要重置的用户';
|
||||
|
||||
public function __construct(
|
||||
private readonly TrafficResetService $trafficResetService
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$fixNull = $this->option('fix-null');
|
||||
$force = $this->option('force');
|
||||
|
||||
$this->info('🚀 开始执行流量重置任务...');
|
||||
|
||||
if ($fixNull) {
|
||||
$this->warn('🔧 修正模式 - 将重新计算next_reset_at为null的用户');
|
||||
} elseif ($force) {
|
||||
$this->warn('⚡ 强制模式 - 将重新计算所有用户的重置时间');
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $fixNull ? $this->performFix() : ($force ? $this->performForce() : $this->performReset());
|
||||
$this->displayResults($result, $fixNull || $force);
|
||||
return self::SUCCESS;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->error("❌ 任务执行失败: {$e->getMessage()}");
|
||||
|
||||
Log::error('流量重置命令执行失败', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
private function displayResults(array $result, bool $isSpecialMode): void
|
||||
{
|
||||
$this->info("✅ 任务完成!\n");
|
||||
|
||||
if ($isSpecialMode) {
|
||||
$this->displayFixResults($result);
|
||||
} else {
|
||||
$this->displayExecutionResults($result);
|
||||
}
|
||||
}
|
||||
|
||||
private function displayFixResults(array $result): void
|
||||
{
|
||||
$this->info("📊 修正结果统计:");
|
||||
$this->info("🔍 发现用户总数: {$result['total_found']}");
|
||||
$this->info("✅ 成功修正数量: {$result['total_fixed']}");
|
||||
$this->info("⏱️ 总执行时间: {$result['duration']} 秒");
|
||||
|
||||
if ($result['error_count'] > 0) {
|
||||
$this->warn("⚠️ 错误数量: {$result['error_count']}");
|
||||
$this->warn("详细错误信息请查看日志");
|
||||
} else {
|
||||
$this->info("✨ 无错误发生");
|
||||
}
|
||||
|
||||
if ($result['total_found'] > 0) {
|
||||
$avgTime = round($result['duration'] / $result['total_found'], 4);
|
||||
$this->info("⚡ 平均处理速度: {$avgTime} 秒/用户");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private function displayExecutionResults(array $result): void
|
||||
{
|
||||
$this->info("📊 执行结果统计:");
|
||||
$this->info("👥 处理用户总数: {$result['total_processed']}");
|
||||
$this->info("🔄 重置用户数量: {$result['total_reset']}");
|
||||
$this->info("⏱️ 总执行时间: {$result['duration']} 秒");
|
||||
|
||||
if ($result['error_count'] > 0) {
|
||||
$this->warn("⚠️ 错误数量: {$result['error_count']}");
|
||||
$this->warn("详细错误信息请查看日志");
|
||||
} else {
|
||||
$this->info("✨ 无错误发生");
|
||||
}
|
||||
|
||||
if ($result['total_processed'] > 0) {
|
||||
$avgTime = round($result['duration'] / $result['total_processed'], 4);
|
||||
$this->info("⚡ 平均处理速度: {$avgTime} 秒/用户");
|
||||
}
|
||||
}
|
||||
|
||||
private function performReset(): array
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
$totalResetCount = 0;
|
||||
$errors = [];
|
||||
|
||||
$users = $this->getResetQuery()->get();
|
||||
|
||||
if ($users->isEmpty()) {
|
||||
$this->info("😴 当前没有需要重置的用户");
|
||||
return [
|
||||
'total_processed' => 0,
|
||||
'total_reset' => 0,
|
||||
'error_count' => 0,
|
||||
'duration' => round(microtime(true) - $startTime, 2),
|
||||
];
|
||||
}
|
||||
|
||||
$this->info("找到 {$users->count()} 个需要重置的用户");
|
||||
|
||||
foreach ($users as $user) {
|
||||
try {
|
||||
$totalResetCount += (int) $this->trafficResetService->checkAndReset($user, TrafficResetLog::SOURCE_CRON);
|
||||
} catch (\Exception $e) {
|
||||
$errors[] = [
|
||||
'user_id' => $user->id,
|
||||
'email' => $user->email,
|
||||
'error' => $e->getMessage(),
|
||||
];
|
||||
Log::error('用户流量重置失败', [
|
||||
'user_id' => $user->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'total_processed' => $users->count(),
|
||||
'total_reset' => $totalResetCount,
|
||||
'error_count' => count($errors),
|
||||
'duration' => round(microtime(true) - $startTime, 2),
|
||||
];
|
||||
}
|
||||
|
||||
private function performFix(): array
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
$nullUsers = $this->getNullResetTimeUsers();
|
||||
|
||||
if ($nullUsers->isEmpty()) {
|
||||
$this->info("✅ 没有发现next_reset_at为null的用户");
|
||||
return [
|
||||
'total_found' => 0,
|
||||
'total_fixed' => 0,
|
||||
'error_count' => 0,
|
||||
'duration' => round(microtime(true) - $startTime, 2),
|
||||
];
|
||||
}
|
||||
|
||||
$this->info("🔧 发现 {$nullUsers->count()} 个next_reset_at为null的用户,开始修正...");
|
||||
|
||||
$fixedCount = 0;
|
||||
$errors = [];
|
||||
|
||||
foreach ($nullUsers as $user) {
|
||||
try {
|
||||
$nextResetTime = $this->trafficResetService->calculateNextResetTime($user);
|
||||
if ($nextResetTime) {
|
||||
$user->next_reset_at = $nextResetTime->timestamp;
|
||||
$user->save();
|
||||
$fixedCount++;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$errors[] = [
|
||||
'user_id' => $user->id,
|
||||
'email' => $user->email,
|
||||
'error' => $e->getMessage(),
|
||||
];
|
||||
Log::error('修正用户next_reset_at失败', [
|
||||
'user_id' => $user->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'total_found' => $nullUsers->count(),
|
||||
'total_fixed' => $fixedCount,
|
||||
'error_count' => count($errors),
|
||||
'duration' => round(microtime(true) - $startTime, 2),
|
||||
];
|
||||
}
|
||||
|
||||
private function performForce(): array
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
$allUsers = $this->getAllUsers();
|
||||
|
||||
if ($allUsers->isEmpty()) {
|
||||
$this->info("✅ 没有发现需要处理的用户");
|
||||
return [
|
||||
'total_found' => 0,
|
||||
'total_fixed' => 0,
|
||||
'error_count' => 0,
|
||||
'duration' => round(microtime(true) - $startTime, 2),
|
||||
];
|
||||
}
|
||||
|
||||
$this->info("⚡ 发现 {$allUsers->count()} 个用户,开始重新计算重置时间...");
|
||||
|
||||
$fixedCount = 0;
|
||||
$errors = [];
|
||||
|
||||
foreach ($allUsers as $user) {
|
||||
try {
|
||||
$nextResetTime = $this->trafficResetService->calculateNextResetTime($user);
|
||||
if ($nextResetTime) {
|
||||
$user->next_reset_at = $nextResetTime->timestamp;
|
||||
$user->save();
|
||||
$fixedCount++;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$errors[] = [
|
||||
'user_id' => $user->id,
|
||||
'email' => $user->email,
|
||||
'error' => $e->getMessage(),
|
||||
];
|
||||
Log::error('强制重新计算用户next_reset_at失败', [
|
||||
'user_id' => $user->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'total_found' => $allUsers->count(),
|
||||
'total_fixed' => $fixedCount,
|
||||
'error_count' => count($errors),
|
||||
'duration' => round(microtime(true) - $startTime, 2),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
|
||||
private function getResetQuery()
|
||||
{
|
||||
return User::where('next_reset_at', '<=', time())
|
||||
->whereNotNull('next_reset_at')
|
||||
->where(function ($query) {
|
||||
$query->where('expired_at', '>', time())
|
||||
->orWhereNull('expired_at');
|
||||
})
|
||||
->where('banned', 0)
|
||||
->whereNotNull('plan_id');
|
||||
}
|
||||
|
||||
|
||||
|
||||
private function getNullResetTimeUsers()
|
||||
{
|
||||
return User::whereNull('next_reset_at')
|
||||
->whereNotNull('plan_id')
|
||||
->where(function ($query) {
|
||||
$query->where('expired_at', '>', time())
|
||||
->orWhereNull('expired_at');
|
||||
})
|
||||
->where('banned', 0)
|
||||
->with('plan:id,name,reset_traffic_method')
|
||||
->get();
|
||||
}
|
||||
|
||||
private function getAllUsers()
|
||||
{
|
||||
return User::whereNotNull('plan_id')
|
||||
->where(function ($query) {
|
||||
$query->where('expired_at', '>', time())
|
||||
->orWhereNull('expired_at');
|
||||
})
|
||||
->where('banned', 0)
|
||||
->with('plan:id,name,reset_traffic_method')
|
||||
->get();
|
||||
}
|
||||
|
||||
}
|
||||
58
Xboard/app/Console/Commands/ResetUser.php
Normal file
58
Xboard/app/Console/Commands/ResetUser.php
Normal 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}的安全信息");
|
||||
}
|
||||
}
|
||||
}
|
||||
103
Xboard/app/Console/Commands/SendRemindMail.php
Normal file
103
Xboard/app/Console/Commands/SendRemindMail.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\MailService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class SendRemindMail extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'send:remindMail
|
||||
{--chunk-size=500 : 每批处理的用户数量}
|
||||
{--force : 强制执行,跳过确认}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = '发送提醒邮件';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
if (!admin_setting('remind_mail_enable', false)) {
|
||||
$this->warn('邮件提醒功能未启用');
|
||||
return 0;
|
||||
}
|
||||
|
||||
$chunkSize = max(100, min(2000, (int) $this->option('chunk-size')));
|
||||
$mailService = new MailService();
|
||||
|
||||
$totalUsers = $mailService->getTotalUsersNeedRemind();
|
||||
if ($totalUsers === 0) {
|
||||
$this->info('没有需要发送提醒邮件的用户');
|
||||
return 0;
|
||||
}
|
||||
|
||||
$this->displayInfo($totalUsers, $chunkSize);
|
||||
|
||||
if (!$this->option('force') && !$this->confirm("确定要发送提醒邮件给 {$totalUsers} 个用户吗?")) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$startTime = microtime(true);
|
||||
$progressBar = $this->output->createProgressBar((int) ceil($totalUsers / $chunkSize));
|
||||
$progressBar->start();
|
||||
|
||||
$statistics = $mailService->processUsersInChunks($chunkSize, function () use ($progressBar) {
|
||||
$progressBar->advance();
|
||||
});
|
||||
|
||||
$progressBar->finish();
|
||||
$this->newLine();
|
||||
|
||||
$this->displayResults($statistics, microtime(true) - $startTime);
|
||||
$this->logResults($statistics);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function displayInfo(int $totalUsers, int $chunkSize): void
|
||||
{
|
||||
$this->table(['项目', '值'], [
|
||||
['需要处理的用户', number_format($totalUsers)],
|
||||
['批次大小', $chunkSize],
|
||||
['预计批次', ceil($totalUsers / $chunkSize)],
|
||||
]);
|
||||
}
|
||||
|
||||
private function displayResults(array $stats, float $duration): void
|
||||
{
|
||||
$this->info('✅ 提醒邮件发送完成!');
|
||||
|
||||
$this->table(['统计项', '数量'], [
|
||||
['总处理用户', number_format($stats['processed_users'])],
|
||||
['过期提醒邮件', number_format($stats['expire_emails'])],
|
||||
['流量提醒邮件', number_format($stats['traffic_emails'])],
|
||||
['跳过用户', number_format($stats['skipped'])],
|
||||
['错误数量', number_format($stats['errors'])],
|
||||
['总耗时', round($duration, 2) . ' 秒'],
|
||||
['平均速度', round($stats['processed_users'] / max($duration, 0.1), 1) . ' 用户/秒'],
|
||||
]);
|
||||
|
||||
if ($stats['errors'] > 0) {
|
||||
$this->warn("⚠️ 有 {$stats['errors']} 个用户的邮件发送失败,请检查日志");
|
||||
}
|
||||
}
|
||||
|
||||
private function logResults(array $statistics): void
|
||||
{
|
||||
Log::info('SendRemindMail命令执行完成', ['statistics' => $statistics]);
|
||||
}
|
||||
}
|
||||
41
Xboard/app/Console/Commands/Test.php
Normal file
41
Xboard/app/Console/Commands/Test.php
Normal 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()
|
||||
{
|
||||
}
|
||||
}
|
||||
397
Xboard/app/Console/Commands/XboardInstall.php
Normal file
397
Xboard/app/Console/Commands/XboardInstall.php
Normal file
@@ -0,0 +1,397 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\Plugin\PluginManager;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Encryption\Encrypter;
|
||||
use App\Models\User;
|
||||
use App\Utils\Helper;
|
||||
use Illuminate\Support\Env;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use function Laravel\Prompts\confirm;
|
||||
use function Laravel\Prompts\text;
|
||||
use function Laravel\Prompts\note;
|
||||
use function Laravel\Prompts\select;
|
||||
use App\Models\Plugin;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
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 {
|
||||
$isDocker = file_exists('/.dockerenv');
|
||||
$enableSqlite = getenv('ENABLE_SQLITE', false);
|
||||
$enableRedis = getenv('ENABLE_REDIS', false);
|
||||
$adminAccount = getenv('ADMIN_ACCOUNT', false);
|
||||
$this->info("__ __ ____ _ ");
|
||||
$this->info("\ \ / /| __ ) ___ __ _ _ __ __| | ");
|
||||
$this->info(" \ \/ / | __ \ / _ \ / _` | '__/ _` | ");
|
||||
$this->info(" / /\ \ | |_) | (_) | (_| | | | (_| | ");
|
||||
$this->info("/_/ \_\|____/ \___/ \__,_|_| \__,_| ");
|
||||
if (
|
||||
(File::exists(base_path() . '/.env') && $this->getEnvValue('INSTALLED'))
|
||||
|| (getenv('INSTALLED', false) && $isDocker)
|
||||
) {
|
||||
$securePath = admin_setting('secure_path', admin_setting('frontend_admin_path', hash('crc32b', config('app.key'))));
|
||||
$this->info("访问 http(s)://你的站点/{$securePath} 进入管理面板,你可以在用户中心修改你的密码。");
|
||||
$this->warn("如需重新安装请清空目录下 .env 文件的内容(Docker安装方式不可以删除此文件)");
|
||||
$this->warn("快捷清空.env命令:");
|
||||
note('rm .env && touch .env');
|
||||
return;
|
||||
}
|
||||
if (is_dir(base_path() . '/.env')) {
|
||||
$this->error('😔:安装失败,Docker环境下安装请保留空的 .env 文件');
|
||||
return;
|
||||
}
|
||||
// 选择数据库类型
|
||||
$dbType = $enableSqlite ? 'sqlite' : select(
|
||||
label: '请选择数据库类型',
|
||||
options: [
|
||||
'sqlite' => 'SQLite (无需额外安装)',
|
||||
'mysql' => 'MySQL',
|
||||
'postgresql' => 'PostgreSQL'
|
||||
],
|
||||
default: 'sqlite'
|
||||
);
|
||||
|
||||
// 使用 match 表达式配置数据库
|
||||
$envConfig = match ($dbType) {
|
||||
'sqlite' => $this->configureSqlite(),
|
||||
'mysql' => $this->configureMysql(),
|
||||
'postgresql' => $this->configurePostgresql(),
|
||||
default => throw new \InvalidArgumentException("不支持的数据库类型: {$dbType}")
|
||||
};
|
||||
|
||||
if (is_null($envConfig)) {
|
||||
return; // 用户选择退出安装
|
||||
}
|
||||
$envConfig['APP_KEY'] = 'base64:' . base64_encode(Encrypter::generateKey('AES-256-CBC'));
|
||||
$isReidsValid = false;
|
||||
while (!$isReidsValid) {
|
||||
// 判断是否为Docker环境
|
||||
if ($isDocker == 'true' && ($enableRedis || confirm(label: '是否启用Docker内置的Redis', default: true, yes: '启用', no: '不启用'))) {
|
||||
$envConfig['REDIS_HOST'] = '/data/redis.sock';
|
||||
$envConfig['REDIS_PORT'] = 0;
|
||||
$envConfig['REDIS_PASSWORD'] = null;
|
||||
} else {
|
||||
$envConfig['REDIS_HOST'] = text(label: '请输入Redis地址', default: '127.0.0.1', required: true);
|
||||
$envConfig['REDIS_PORT'] = text(label: '请输入Redis端口', default: '6379', required: true);
|
||||
$envConfig['REDIS_PASSWORD'] = text(label: '请输入redis密码(默认: null)', default: '');
|
||||
}
|
||||
$redisConfig = [
|
||||
'client' => 'phpredis',
|
||||
'default' => [
|
||||
'host' => $envConfig['REDIS_HOST'],
|
||||
'password' => $envConfig['REDIS_PASSWORD'],
|
||||
'port' => $envConfig['REDIS_PORT'],
|
||||
'database' => 0,
|
||||
],
|
||||
];
|
||||
try {
|
||||
$redis = new \Illuminate\Redis\RedisManager(app(), 'phpredis', $redisConfig);
|
||||
$redis->ping();
|
||||
$isReidsValid = true;
|
||||
} catch (\Exception $e) {
|
||||
// 连接失败,输出错误消息
|
||||
$this->error("redis连接失败:" . $e->getMessage());
|
||||
$this->info("请重新输入REDIS配置");
|
||||
$enableRedis = false;
|
||||
sleep(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (!copy(base_path() . '/.env.example', base_path() . '/.env')) {
|
||||
abort(500, '复制环境文件失败,请检查目录权限');
|
||||
}
|
||||
;
|
||||
$email = !empty($adminAccount) ? $adminAccount : text(
|
||||
label: '请输入管理员账号',
|
||||
default: 'admin@demo.com',
|
||||
required: true,
|
||||
validate: fn(string $email): ?string => match (true) {
|
||||
!filter_var($email, FILTER_VALIDATE_EMAIL) => '请输入有效的邮箱地址.',
|
||||
default => null,
|
||||
}
|
||||
);
|
||||
$password = Helper::guid(false);
|
||||
$this->saveToEnv($envConfig);
|
||||
|
||||
$this->call('config:cache');
|
||||
Artisan::call('cache:clear');
|
||||
$this->info('正在导入数据库请稍等...');
|
||||
Artisan::call("migrate", ['--force' => true]);
|
||||
$this->info(Artisan::output());
|
||||
$this->info('数据库导入完成');
|
||||
$this->info('开始注册管理员账号');
|
||||
if (!self::registerAdmin($email, $password)) {
|
||||
abort(500, '管理员账号注册失败,请重试');
|
||||
}
|
||||
self::restoreProtectedPlugins($this);
|
||||
$this->info('正在安装默认插件...');
|
||||
PluginManager::installDefaultPlugins();
|
||||
$this->info('默认插件安装完成');
|
||||
|
||||
$this->info('🎉:一切就绪');
|
||||
$this->info("管理员邮箱:{$email}");
|
||||
$this->info("管理员密码:{$password}");
|
||||
|
||||
$defaultSecurePath = hash('crc32b', config('app.key'));
|
||||
$this->info("访问 http(s)://你的站点/{$defaultSecurePath} 进入管理面板,你可以在用户中心修改你的密码。");
|
||||
$envConfig['INSTALLED'] = true;
|
||||
$this->saveToEnv($envConfig);
|
||||
} catch (\Exception $e) {
|
||||
$this->error($e);
|
||||
}
|
||||
}
|
||||
|
||||
public static function registerAdmin($email, $password)
|
||||
{
|
||||
$user = new User();
|
||||
$user->email = $email;
|
||||
if (strlen($password) < 8) {
|
||||
abort(500, '管理员密码长度最小为8位字符');
|
||||
}
|
||||
$user->password = password_hash($password, PASSWORD_DEFAULT);
|
||||
$user->uuid = Helper::guid(true);
|
||||
$user->token = Helper::guid();
|
||||
$user->is_admin = 1;
|
||||
return $user->save();
|
||||
}
|
||||
|
||||
private function set_env_var($key, $value)
|
||||
{
|
||||
$value = !strpos($value, ' ') ? $value : '"' . $value . '"';
|
||||
$key = strtoupper($key);
|
||||
|
||||
$envPath = app()->environmentFilePath();
|
||||
$contents = file_get_contents($envPath);
|
||||
|
||||
if (preg_match("/^{$key}=[^\r\n]*/m", $contents, $matches)) {
|
||||
$contents = str_replace($matches[0], "{$key}={$value}", $contents);
|
||||
} else {
|
||||
$contents .= "\n{$key}={$value}\n";
|
||||
}
|
||||
|
||||
return file_put_contents($envPath, $contents) !== false;
|
||||
}
|
||||
|
||||
private function saveToEnv($data = [])
|
||||
{
|
||||
foreach ($data as $key => $value) {
|
||||
self::set_env_var($key, $value);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function getEnvValue($key, $default = null)
|
||||
{
|
||||
$dotenv = \Dotenv\Dotenv::createImmutable(base_path());
|
||||
$dotenv->load();
|
||||
|
||||
return Env::get($key, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置 SQLite 数据库
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
private function configureSqlite(): ?array
|
||||
{
|
||||
$sqliteFile = '.docker/.data/database.sqlite';
|
||||
if (!file_exists(base_path($sqliteFile))) {
|
||||
// 创建空文件
|
||||
if (!touch(base_path($sqliteFile))) {
|
||||
$this->info("sqlite创建成功: $sqliteFile");
|
||||
}
|
||||
}
|
||||
|
||||
$envConfig = [
|
||||
'DB_CONNECTION' => 'sqlite',
|
||||
'DB_DATABASE' => $sqliteFile,
|
||||
'DB_HOST' => '',
|
||||
'DB_USERNAME' => '',
|
||||
'DB_PASSWORD' => '',
|
||||
];
|
||||
|
||||
try {
|
||||
Config::set("database.default", 'sqlite');
|
||||
Config::set("database.connections.sqlite.database", base_path($envConfig['DB_DATABASE']));
|
||||
DB::purge('sqlite');
|
||||
DB::connection('sqlite')->getPdo();
|
||||
|
||||
if (!blank(DB::connection('sqlite')->getPdo()->query("SELECT name FROM sqlite_master WHERE type='table'")->fetchAll(\PDO::FETCH_COLUMN))) {
|
||||
if (confirm(label: '检测到数据库中已经存在数据,是否要清空数据库以便安装新的数据?', default: false, yes: '清空', no: '退出安装')) {
|
||||
$this->info('正在清空数据库请稍等');
|
||||
$this->call('db:wipe', ['--force' => true]);
|
||||
$this->info('数据库清空完成');
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->error("SQLite数据库连接失败:" . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
|
||||
return $envConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置 MySQL 数据库
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function configureMysql(): array
|
||||
{
|
||||
while (true) {
|
||||
$envConfig = [
|
||||
'DB_CONNECTION' => 'mysql',
|
||||
'DB_HOST' => text(label: "请输入MySQL数据库地址", default: '127.0.0.1', required: true),
|
||||
'DB_PORT' => text(label: '请输入MySQL数据库端口', default: '3306', required: true),
|
||||
'DB_DATABASE' => text(label: '请输入MySQL数据库名', default: 'xboard', required: true),
|
||||
'DB_USERNAME' => text(label: '请输入MySQL数据库用户名', default: 'root', required: true),
|
||||
'DB_PASSWORD' => text(label: '请输入MySQL数据库密码', required: false),
|
||||
];
|
||||
|
||||
try {
|
||||
Config::set("database.default", 'mysql');
|
||||
Config::set("database.connections.mysql.host", $envConfig['DB_HOST']);
|
||||
Config::set("database.connections.mysql.port", $envConfig['DB_PORT']);
|
||||
Config::set("database.connections.mysql.database", $envConfig['DB_DATABASE']);
|
||||
Config::set("database.connections.mysql.username", $envConfig['DB_USERNAME']);
|
||||
Config::set("database.connections.mysql.password", $envConfig['DB_PASSWORD']);
|
||||
DB::purge('mysql');
|
||||
DB::connection('mysql')->getPdo();
|
||||
|
||||
if (!blank(DB::connection('mysql')->select('SHOW TABLES'))) {
|
||||
if (confirm(label: '检测到数据库中已经存在数据,是否要清空数据库以便安装新的数据?', default: false, yes: '清空', no: '不清空')) {
|
||||
$this->info('正在清空数据库请稍等');
|
||||
$this->call('db:wipe', ['--force' => true]);
|
||||
$this->info('数据库清空完成');
|
||||
return $envConfig;
|
||||
} else {
|
||||
continue; // 重新输入配置
|
||||
}
|
||||
}
|
||||
|
||||
return $envConfig;
|
||||
} catch (\Exception $e) {
|
||||
$this->error("MySQL数据库连接失败:" . $e->getMessage());
|
||||
$this->info("请重新输入MySQL数据库配置");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置 PostgreSQL 数据库
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function configurePostgresql(): array
|
||||
{
|
||||
while (true) {
|
||||
$envConfig = [
|
||||
'DB_CONNECTION' => 'pgsql',
|
||||
'DB_HOST' => text(label: "请输入PostgreSQL数据库地址", default: '127.0.0.1', required: true),
|
||||
'DB_PORT' => text(label: '请输入PostgreSQL数据库端口', default: '5432', required: true),
|
||||
'DB_DATABASE' => text(label: '请输入PostgreSQL数据库名', default: 'xboard', required: true),
|
||||
'DB_USERNAME' => text(label: '请输入PostgreSQL数据库用户名', default: 'postgres', required: true),
|
||||
'DB_PASSWORD' => text(label: '请输入PostgreSQL数据库密码', required: false),
|
||||
];
|
||||
|
||||
try {
|
||||
Config::set("database.default", 'pgsql');
|
||||
Config::set("database.connections.pgsql.host", $envConfig['DB_HOST']);
|
||||
Config::set("database.connections.pgsql.port", $envConfig['DB_PORT']);
|
||||
Config::set("database.connections.pgsql.database", $envConfig['DB_DATABASE']);
|
||||
Config::set("database.connections.pgsql.username", $envConfig['DB_USERNAME']);
|
||||
Config::set("database.connections.pgsql.password", $envConfig['DB_PASSWORD']);
|
||||
DB::purge('pgsql');
|
||||
DB::connection('pgsql')->getPdo();
|
||||
|
||||
// 检查PostgreSQL数据库是否有表
|
||||
$tables = DB::connection('pgsql')->select("SELECT tablename FROM pg_tables WHERE schemaname = 'public'");
|
||||
if (!blank($tables)) {
|
||||
if (confirm(label: '检测到数据库中已经存在数据,是否要清空数据库以便安装新的数据?', default: false, yes: '清空', no: '不清空')) {
|
||||
$this->info('正在清空数据库请稍等');
|
||||
$this->call('db:wipe', ['--force' => true]);
|
||||
$this->info('数据库清空完成');
|
||||
return $envConfig;
|
||||
} else {
|
||||
continue; // 重新输入配置
|
||||
}
|
||||
}
|
||||
|
||||
return $envConfig;
|
||||
} catch (\Exception $e) {
|
||||
$this->error("PostgreSQL数据库连接失败:" . $e->getMessage());
|
||||
$this->info("请重新输入PostgreSQL数据库配置");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 还原内置受保护插件(可在安装和更新时调用)
|
||||
* Docker 部署时 plugins/ 目录被外部挂载覆盖,需要从镜像备份中还原默认插件
|
||||
*/
|
||||
public static function restoreProtectedPlugins(Command $console = null)
|
||||
{
|
||||
$backupBase = '/opt/default-plugins';
|
||||
$pluginsBase = base_path('plugins');
|
||||
|
||||
if (!File::isDirectory($backupBase)) {
|
||||
$console?->info('非 Docker 环境或备份目录不存在,跳过插件还原。');
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (Plugin::PROTECTED_PLUGINS as $pluginCode) {
|
||||
$dirName = Str::studly($pluginCode);
|
||||
$source = "{$backupBase}/{$dirName}";
|
||||
$target = "{$pluginsBase}/{$dirName}";
|
||||
|
||||
if (!File::isDirectory($source)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 先清除旧文件再复制,避免重命名后残留旧文件
|
||||
File::deleteDirectory($target);
|
||||
File::copyDirectory($source, $target);
|
||||
$console?->info("已同步默认插件 [{$dirName}]");
|
||||
}
|
||||
}
|
||||
}
|
||||
45
Xboard/app/Console/Commands/XboardRollback.php
Normal file
45
Xboard/app/Console/Commands/XboardRollback.php
Normal 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());
|
||||
}
|
||||
}
|
||||
75
Xboard/app/Console/Commands/XboardStatistics.php
Normal file
75
Xboard/app/Console/Commands/XboardStatistics.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\StatisticalService;
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\Stat;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
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 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
65
Xboard/app/Console/Commands/XboardUpdate.php
Normal file
65
Xboard/app/Console/Commands/XboardUpdate.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\ThemeService;
|
||||
use App\Services\UpdateService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use App\Services\Plugin\PluginManager;
|
||||
use App\Models\Plugin;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Console\Commands\XboardInstall;
|
||||
|
||||
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", ['--force' => true]);
|
||||
$this->info(Artisan::output());
|
||||
$this->info('正在检查内置插件文件...');
|
||||
XboardInstall::restoreProtectedPlugins($this);
|
||||
$this->info('正在检查并安装默认插件...');
|
||||
PluginManager::installDefaultPlugins();
|
||||
$this->info('默认插件检查完成');
|
||||
// Artisan::call('reset:traffic', ['--fix-null' => true]);
|
||||
$this->info('正在重新计算所有用户的重置时间...');
|
||||
Artisan::call('reset:traffic', ['--force' => true]);
|
||||
$updateService = new UpdateService();
|
||||
$updateService->updateVersionCache();
|
||||
$themeService = app(ThemeService::class);
|
||||
$themeService->refreshCurrentTheme();
|
||||
Artisan::call('horizon:terminate');
|
||||
$this->info('更新完毕,队列服务已重启,你无需进行任何操作。');
|
||||
}
|
||||
}
|
||||
68
Xboard/app/Console/Kernel.php
Normal file
68
Xboard/app/Console/Kernel.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console;
|
||||
|
||||
use App\Services\Plugin\PluginManager;
|
||||
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): void
|
||||
{
|
||||
Cache::put(CacheKey::get('SCHEDULE_LAST_CHECK_AT', null), time());
|
||||
// v2board
|
||||
$schedule->command('xboard:statistics')->dailyAt('0:10')->onOneServer();
|
||||
// check
|
||||
$schedule->command('check:order')->everyMinute()->onOneServer()->withoutOverlapping(5);
|
||||
$schedule->command('check:commission')->everyMinute()->onOneServer()->withoutOverlapping(5);
|
||||
$schedule->command('check:ticket')->everyMinute()->onOneServer()->withoutOverlapping(5);
|
||||
$schedule->command('check:traffic-exceeded')->everyMinute()->onOneServer()->withoutOverlapping(10)->runInBackground();
|
||||
// reset
|
||||
$schedule->command('reset:traffic')->everyMinute()->onOneServer()->withoutOverlapping(10);
|
||||
$schedule->command('reset:log')->daily()->onOneServer();
|
||||
// send
|
||||
$schedule->command('send:remindMail', ['--force'])->dailyAt('11:30')->onOneServer();
|
||||
// horizon metrics
|
||||
$schedule->command('horizon:snapshot')->everyFiveMinutes()->onOneServer();
|
||||
// backup Timing
|
||||
// if (env('ENABLE_AUTO_BACKUP_AND_UPDATE', false)) {
|
||||
// $schedule->command('backup:database', ['true'])->daily()->onOneServer();
|
||||
// }
|
||||
app(PluginManager::class)->registerPluginSchedules($schedule);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the commands for the application.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function commands()
|
||||
{
|
||||
$this->load(__DIR__ . '/Commands');
|
||||
|
||||
try {
|
||||
app(PluginManager::class)->initializeEnabledPlugins();
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
require base_path('routes/console.php');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user