first commit

This commit is contained in:
CN-JS-HuiBai
2026-04-07 16:54:24 +08:00
commit 2c6a38c80d
399 changed files with 42205 additions and 0 deletions

View 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("2Mysql备份完成");
}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("2Sqlite备份完成");
}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);
}
}
}

View 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;
}
}

View 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);
});
}
}

View 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));
}
}
}
}

View 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();
});
}
}

View 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.");
}
}

View File

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

View File

@@ -0,0 +1,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);
}
}
}
}

View 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('所有配置迁移完成');
}
}

View 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();
}
}

View 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();
}
}

View 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},请尽快修改密码。");
}
}

View 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();
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace App\Console\Commands;
use App\Models\Plan;
use App\Utils\Helper;
use Illuminate\Console\Command;
use App\Models\User;
use Illuminate\Support\Facades\DB;
class ResetUser extends Command
{
protected $builder;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'reset:user';
/**
* The console command description.
*
* @var string
*/
protected $description = '重置所有用户信息';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if (!$this->confirm("确定要重置所有用户安全信息吗?")) {
return;
}
ini_set('memory_limit', -1);
$users = User::all();
foreach ($users as $user)
{
$user->token = Helper::guid();
$user->uuid = Helper::guid(true);
$user->save();
$this->info("已重置用户{$user->email}的安全信息");
}
}
}

View File

@@ -0,0 +1,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]);
}
}

View File

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

View File

@@ -0,0 +1,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}]");
}
}
}

View File

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

View File

@@ -0,0 +1,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]);
}
}
}

View 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('更新完毕,队列服务已重启,你无需进行任何操作。');
}
}

View 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');
}
}