mirror of
https://github.com/cedar2025/Xboard.git
synced 2025-01-22 10:38:14 -05:00
Compare commits
4 Commits
3bd4cf05dd
...
d2462bc683
Author | SHA1 | Date | |
---|---|---|---|
|
d2462bc683 | ||
|
b7f2af7d6a | ||
|
1f31c6b585 | ||
|
d54eabb617 |
@ -4,6 +4,8 @@ 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
|
||||
@ -85,14 +87,14 @@ class BackupDatabase extends Command
|
||||
]);
|
||||
|
||||
// 输出文件链接
|
||||
\Log::channel('backup')->info("🎉:数据库备份已上传到 Google Cloud Storage: $objectName");
|
||||
Log::channel('backup')->info("🎉:数据库备份已上传到 Google Cloud Storage: $objectName");
|
||||
$this->info("🎉:数据库备份已上传到 Google Cloud Storage: $objectName");
|
||||
\File::delete($compressedBackupPath);
|
||||
File::delete($compressedBackupPath);
|
||||
}
|
||||
}catch(\Exception $e){
|
||||
\Log::channel('backup')->error("😔:数据库备份失败 \n" . $e);
|
||||
Log::channel('backup')->error("😔:数据库备份失败 \n" . $e);
|
||||
$this->error("😔:数据库备份失败\n" . $e);
|
||||
\File::delete($compressedBackupPath);
|
||||
File::delete($compressedBackupPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ class ClearUser extends Command
|
||||
->where('last_login_at', NULL);
|
||||
$count = $builder->count();
|
||||
if ($builder->delete()) {
|
||||
$this->info("已删除${count}位没有任何数据的用户");
|
||||
$this->info("已删除{$count}位没有任何数据的用户");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ExportV2Log extends Command
|
||||
{
|
||||
@ -20,7 +21,7 @@ class ExportV2Log extends Command
|
||||
$days = $this->argument('days');
|
||||
$date = Carbon::now()->subDays($days)->startOfDay();
|
||||
|
||||
$logs = \DB::table('v2_log')
|
||||
$logs = DB::table('v2_log')
|
||||
->where('created_at', '>=', $date->timestamp)
|
||||
->get();
|
||||
|
||||
|
@ -4,6 +4,8 @@ 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
|
||||
{
|
||||
@ -132,7 +134,7 @@ class MigrateFromV2b extends Command
|
||||
try {
|
||||
foreach ($sqlCommands[$version] as $sqlCommand) {
|
||||
// Execute SQL command
|
||||
\DB::statement($sqlCommand);
|
||||
DB::statement($sqlCommand);
|
||||
}
|
||||
|
||||
$this->info('1️⃣、数据库差异矫正成功');
|
||||
@ -158,7 +160,7 @@ class MigrateFromV2b extends Command
|
||||
|
||||
public function MigrateV2ConfigToV2Settings()
|
||||
{
|
||||
\Artisan::call('config:clear');
|
||||
Artisan::call('config:clear');
|
||||
$configValue = config('v2board') ?? [];
|
||||
|
||||
foreach ($configValue as $k => $v) {
|
||||
@ -176,7 +178,7 @@ class MigrateFromV2b extends Command
|
||||
]);
|
||||
$this->info("配置 {$k} 迁移成功");
|
||||
}
|
||||
\Artisan::call('config:cache');
|
||||
Artisan::call('config:cache');
|
||||
|
||||
$this->info('所有配置迁移完成');
|
||||
}
|
||||
|
@ -7,6 +7,10 @@ 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;
|
||||
@ -55,7 +59,7 @@ class XboardInstall extends Command
|
||||
$this->info(" / /\ \ | |_) | (_) | (_| | | | (_| | ");
|
||||
$this->info("/_/ \_\|____/ \___/ \__,_|_| \__,_| ");
|
||||
if (
|
||||
(\File::exists(base_path() . '/.env') && $this->getEnvValue('INSTALLED'))
|
||||
(File::exists(base_path() . '/.env') && $this->getEnvValue('INSTALLED'))
|
||||
|| (env('INSTALLED', false) && $isDocker)
|
||||
) {
|
||||
$securePath = admin_setting('secure_path', admin_setting('frontend_admin_path', hash('crc32b', config('app.key'))));
|
||||
@ -86,11 +90,11 @@ class XboardInstall extends Command
|
||||
'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))) {
|
||||
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]);
|
||||
@ -115,16 +119,16 @@ class XboardInstall extends Command
|
||||
'DB_PASSWORD' => text(label: '请输入数据库密码', 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();
|
||||
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();
|
||||
$isMysqlValid = true;
|
||||
if (!blank(\DB::connection('mysql')->select('SHOW TABLES'))) {
|
||||
if (!blank(DB::connection('mysql')->select('SHOW TABLES'))) {
|
||||
if (confirm(label: '检测到数据库中已经存在数据,是否要清空数据库以便安装新的数据?', default: false, yes: '清空', no: '不清空')) {
|
||||
$this->info('正在清空数据库请稍等');
|
||||
$this->call('db:wipe', ['--force' => true]);
|
||||
@ -192,10 +196,10 @@ class XboardInstall extends Command
|
||||
$this->saveToEnv($envConfig);
|
||||
|
||||
$this->call('config:cache');
|
||||
\Artisan::call('cache:clear');
|
||||
Artisan::call('cache:clear');
|
||||
$this->info('正在导入数据库请稍等...');
|
||||
\Artisan::call("migrate", ['--force' => true]);
|
||||
$this->info(\Artisan::output());
|
||||
Artisan::call("migrate", ['--force' => true]);
|
||||
$this->info(Artisan::output());
|
||||
$this->info('数据库导入完成');
|
||||
$this->info('开始注册管理员账号');
|
||||
if (!$this->registerAdmin($email, $password)) {
|
||||
|
@ -47,6 +47,60 @@ class ClientController extends Controller
|
||||
|
||||
private const ALLOWED_TYPES = ['vmess', 'vless', 'trojan', 'hysteria', 'shadowsocks', 'hysteria2'];
|
||||
|
||||
/**
|
||||
* 处理浏览器访问订阅的情况
|
||||
*/
|
||||
private function handleBrowserSubscribe($user, UserService $userService)
|
||||
{
|
||||
$useTraffic = $user['u'] + $user['d'];
|
||||
$totalTraffic = $user['transfer_enable'];
|
||||
$remainingTraffic = Helper::trafficConvert($totalTraffic - $useTraffic);
|
||||
$expiredDate = $user['expired_at'] ? date('Y-m-d', $user['expired_at']) : __('Unlimited');
|
||||
$resetDay = $userService->getResetDay($user);
|
||||
|
||||
// 获取通用订阅地址
|
||||
$subscriptionUrl = Helper::getSubscribeUrl($user->token);
|
||||
|
||||
// 生成二维码
|
||||
$writer = new \BaconQrCode\Writer(
|
||||
new \BaconQrCode\Renderer\ImageRenderer(
|
||||
new \BaconQrCode\Renderer\RendererStyle\RendererStyle(200),
|
||||
new \BaconQrCode\Renderer\Image\SvgImageBackEnd()
|
||||
)
|
||||
);
|
||||
$qrCode = base64_encode($writer->writeString($subscriptionUrl));
|
||||
|
||||
$data = [
|
||||
'username' => $user->email,
|
||||
'status' => $userService->isAvailable($user) ? 'active' : 'inactive',
|
||||
'data_limit' => $totalTraffic ? Helper::trafficConvert($totalTraffic) : '∞',
|
||||
'data_used' => Helper::trafficConvert($useTraffic),
|
||||
'expired_date' => $expiredDate,
|
||||
'reset_day' => $resetDay,
|
||||
'subscription_url' => $subscriptionUrl,
|
||||
'qr_code' => $qrCode
|
||||
];
|
||||
|
||||
// 只有当 device_limit 不为 null 时才添加到返回数据中
|
||||
if ($user->device_limit !== null) {
|
||||
$data['device_limit'] = $user->device_limit;
|
||||
}
|
||||
|
||||
return response()->view('client.subscribe', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否是浏览器访问
|
||||
*/
|
||||
private function isBrowserAccess(Request $request): bool
|
||||
{
|
||||
$userAgent = strtolower($request->header('User-Agent'));
|
||||
return str_contains($userAgent, 'mozilla')
|
||||
|| str_contains($userAgent, 'chrome')
|
||||
|| str_contains($userAgent, 'safari')
|
||||
|| str_contains($userAgent, 'edge');
|
||||
}
|
||||
|
||||
public function subscribe(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
@ -62,6 +116,11 @@ class ClientController extends Controller
|
||||
return response()->json(['message' => 'Account unavailable'], 403);
|
||||
}
|
||||
|
||||
// 检测是否是浏览器访问
|
||||
if ($this->isBrowserAccess($request)) {
|
||||
return $this->handleBrowserSubscribe($user, $userService);
|
||||
}
|
||||
|
||||
$types = $this->getFilteredTypes($request->input('types', 'all'));
|
||||
$filterArr = $this->getFilterArray($request->input('filter'));
|
||||
$clientInfo = $this->getClientInfo($request);
|
||||
@ -107,7 +166,7 @@ class ClientController extends Controller
|
||||
|
||||
private function getFilterArray(?string $filter): ?array
|
||||
{
|
||||
return mb_strlen($filter ?? '') > 20 ? null :
|
||||
return mb_strlen((string) $filter) > 20 ? null :
|
||||
explode('|', str_replace(['|', '|', ','], '|', $filter));
|
||||
}
|
||||
|
||||
@ -178,7 +237,7 @@ class ClientController extends Controller
|
||||
$useTraffic = $user['u'] + $user['d'];
|
||||
$totalTraffic = $user['transfer_enable'];
|
||||
$remainingTraffic = Helper::trafficConvert($totalTraffic - $useTraffic);
|
||||
$expiredDate = $user['expired_at'] ? date('Y-m-d', $user['expired_at']) : '长期有效';
|
||||
$expiredDate = $user['expired_at'] ? date('Y-m-d', $user['expired_at']) : __('长期有效');
|
||||
$userService = new UserService();
|
||||
$resetDay = $userService->getResetDay($user);
|
||||
array_unshift($servers, array_merge($servers[0], [
|
||||
|
@ -32,7 +32,7 @@ class UniProxyController extends Controller
|
||||
$response['users'] = $users;
|
||||
|
||||
$eTag = sha1(json_encode($response));
|
||||
if (strpos($request->header('If-None-Match'), $eTag) !== false) {
|
||||
if (strpos($request->header('If-None-Match', ''), $eTag) !== false) {
|
||||
return response(null, 304);
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,7 @@ class ServerController extends Controller
|
||||
$servers = ServerService::getAvailableServers($user);
|
||||
}
|
||||
$eTag = sha1(json_encode(array_column($servers, 'cache_key')));
|
||||
if (strpos($request->header('If-None-Match'), $eTag) !== false ) {
|
||||
if (strpos($request->header('If-None-Match', ''), $eTag) !== false ) {
|
||||
return response(null,304);
|
||||
}
|
||||
$data = NodeResource::collection($servers);
|
||||
|
@ -23,6 +23,35 @@ class StatController extends Controller
|
||||
}
|
||||
public function getOverride(Request $request)
|
||||
{
|
||||
// 获取在线节点数
|
||||
$onlineNodes = Server::all()->filter(function ($server) {
|
||||
$server->loadServerStatus();
|
||||
return $server->is_online;
|
||||
})->count();
|
||||
// 获取在线设备数和在线用户数
|
||||
$onlineDevices = User::where('t', '>=', time() - 600)
|
||||
->sum('online_count');
|
||||
$onlineUsers = User::where('t', '>=', time() - 600)
|
||||
->count();
|
||||
|
||||
// 获取今日流量统计
|
||||
$todayStart = strtotime('today');
|
||||
$todayTraffic = StatServer::where('record_at', '>=', $todayStart)
|
||||
->where('record_at', '<', time())
|
||||
->selectRaw('SUM(u) as upload, SUM(d) as download, SUM(u + d) as total')
|
||||
->first();
|
||||
|
||||
// 获取本月流量统计
|
||||
$monthStart = strtotime(date('Y-m-1'));
|
||||
$monthTraffic = StatServer::where('record_at', '>=', $monthStart)
|
||||
->where('record_at', '<', time())
|
||||
->selectRaw('SUM(u) as upload, SUM(d) as download, SUM(u + d) as total')
|
||||
->first();
|
||||
|
||||
// 获取总流量统计
|
||||
$totalTraffic = StatServer::selectRaw('SUM(u) as upload, SUM(d) as download, SUM(u + d) as total')
|
||||
->first();
|
||||
|
||||
return [
|
||||
'data' => [
|
||||
'month_income' => Order::where('created_at', '>=', strtotime(date('Y-m-1')))
|
||||
@ -53,6 +82,25 @@ class StatController extends Controller
|
||||
'commission_last_month_payout' => CommissionLog::where('created_at', '>=', strtotime('-1 month', strtotime(date('Y-m-1'))))
|
||||
->where('created_at', '<', strtotime(date('Y-m-1')))
|
||||
->sum('get_amount'),
|
||||
// 新增统计数据
|
||||
'online_nodes' => $onlineNodes,
|
||||
'online_devices' => $onlineDevices,
|
||||
'online_users' => $onlineUsers,
|
||||
'today_traffic' => [
|
||||
'upload' => $todayTraffic->upload ?? 0,
|
||||
'download' => $todayTraffic->download ?? 0,
|
||||
'total' => $todayTraffic->total ?? 0
|
||||
],
|
||||
'month_traffic' => [
|
||||
'upload' => $monthTraffic->upload ?? 0,
|
||||
'download' => $monthTraffic->download ?? 0,
|
||||
'total' => $monthTraffic->total ?? 0
|
||||
],
|
||||
'total_traffic' => [
|
||||
'upload' => $totalTraffic->upload ?? 0,
|
||||
'download' => $totalTraffic->download ?? 0,
|
||||
'total' => $totalTraffic->total ?? 0
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
@ -213,11 +261,39 @@ class StatController extends Controller
|
||||
$currentMonthStart = strtotime(date('Y-m-01'));
|
||||
$lastMonthStart = strtotime('-1 month', $currentMonthStart);
|
||||
$twoMonthsAgoStart = strtotime('-2 month', $currentMonthStart);
|
||||
|
||||
|
||||
// Today's start timestamp
|
||||
$todayStart = strtotime('today');
|
||||
$yesterdayStart = strtotime('-1 day', $todayStart);
|
||||
|
||||
// 获取在线节点数
|
||||
$onlineNodes = Server::all()->filter(function ($server) {
|
||||
$server->loadServerStatus();
|
||||
return $server->is_online;
|
||||
})->count();
|
||||
|
||||
// 获取在线设备数和在线用户数
|
||||
$onlineDevices = User::where('t', '>=', time() - 600)
|
||||
->sum('online_count');
|
||||
$onlineUsers = User::where('t', '>=', time() - 600)
|
||||
->count();
|
||||
|
||||
// 获取今日流量统计
|
||||
$todayTraffic = StatServer::where('record_at', '>=', $todayStart)
|
||||
->where('record_at', '<', time())
|
||||
->selectRaw('SUM(u) as upload, SUM(d) as download, SUM(u + d) as total')
|
||||
->first();
|
||||
|
||||
// 获取本月流量统计
|
||||
$monthTraffic = StatServer::where('record_at', '>=', $currentMonthStart)
|
||||
->where('record_at', '<', time())
|
||||
->selectRaw('SUM(u) as upload, SUM(d) as download, SUM(u + d) as total')
|
||||
->first();
|
||||
|
||||
// 获取总流量统计
|
||||
$totalTraffic = StatServer::selectRaw('SUM(u) as upload, SUM(d) as download, SUM(u + d) as total')
|
||||
->first();
|
||||
|
||||
// Today's income
|
||||
$todayIncome = Order::where('created_at', '>=', $todayStart)
|
||||
->where('created_at', '<', time())
|
||||
@ -230,10 +306,6 @@ class StatController extends Controller
|
||||
->whereNotIn('status', [0, 2])
|
||||
->sum('total_amount');
|
||||
|
||||
// Online users (active in last 10 minutes)
|
||||
$onlineUsers = User::where('t', '>=', time() - 600)
|
||||
->count();
|
||||
|
||||
// Current month income
|
||||
$currentMonthIncome = Order::where('created_at', '>=', $currentMonthStart)
|
||||
->where('created_at', '<', time())
|
||||
@ -251,6 +323,11 @@ class StatController extends Controller
|
||||
->where('created_at', '<', $currentMonthStart)
|
||||
->sum('get_amount');
|
||||
|
||||
// Current month commission payout
|
||||
$currentMonthCommissionPayout = CommissionLog::where('created_at', '>=', $currentMonthStart)
|
||||
->where('created_at', '<', time())
|
||||
->sum('get_amount');
|
||||
|
||||
// Current month new users
|
||||
$currentMonthNewUsers = User::where('created_at', '>=', $currentMonthStart)
|
||||
->where('created_at', '<', time())
|
||||
@ -288,21 +365,60 @@ class StatController extends Controller
|
||||
$userGrowth = $lastMonthNewUsers > 0 ? round(($currentMonthNewUsers - $lastMonthNewUsers) / $lastMonthNewUsers * 100, 1) : 0;
|
||||
$dayIncomeGrowth = $yesterdayIncome > 0 ? round(($todayIncome - $yesterdayIncome) / $yesterdayIncome * 100, 1) : 0;
|
||||
|
||||
// 获取待处理工单和佣金数据
|
||||
$ticketPendingTotal = Ticket::where('status', 0)->count();
|
||||
$commissionPendingTotal = Order::where('commission_status', 0)
|
||||
->where('invite_user_id', '!=', NULL)
|
||||
->whereIn('status', [Order::STATUS_COMPLETED])
|
||||
->where('commission_balance', '>', 0)
|
||||
->count();
|
||||
|
||||
return [
|
||||
'data' => [
|
||||
// 收入相关
|
||||
'todayIncome' => $todayIncome,
|
||||
'onlineUsers' => $onlineUsers,
|
||||
'dayIncomeGrowth' => $dayIncomeGrowth,
|
||||
'currentMonthIncome' => $currentMonthIncome,
|
||||
'lastMonthIncome' => $lastMonthIncome,
|
||||
'monthIncomeGrowth' => $monthIncomeGrowth,
|
||||
'lastMonthIncomeGrowth' => $lastMonthIncomeGrowth,
|
||||
|
||||
// 佣金相关
|
||||
'currentMonthCommissionPayout' => $currentMonthCommissionPayout,
|
||||
'lastMonthCommissionPayout' => $lastMonthCommissionPayout,
|
||||
'commissionGrowth' => $commissionGrowth,
|
||||
'commissionPendingTotal' => $commissionPendingTotal,
|
||||
|
||||
// 用户相关
|
||||
'currentMonthNewUsers' => $currentMonthNewUsers,
|
||||
'totalUsers' => $totalUsers,
|
||||
'activeUsers' => $activeUsers,
|
||||
'monthIncomeGrowth' => $monthIncomeGrowth,
|
||||
'lastMonthIncomeGrowth' => $lastMonthIncomeGrowth,
|
||||
'commissionGrowth' => $commissionGrowth,
|
||||
'userGrowth' => $userGrowth
|
||||
'userGrowth' => $userGrowth,
|
||||
'onlineUsers' => $onlineUsers,
|
||||
'onlineDevices' => $onlineDevices,
|
||||
|
||||
// 工单相关
|
||||
'ticketPendingTotal' => $ticketPendingTotal,
|
||||
|
||||
// 节点相关
|
||||
'onlineNodes' => $onlineNodes,
|
||||
|
||||
// 流量统计
|
||||
'todayTraffic' => [
|
||||
'upload' => $todayTraffic->upload ?? 0,
|
||||
'download' => $todayTraffic->download ?? 0,
|
||||
'total' => $todayTraffic->total ?? 0
|
||||
],
|
||||
'monthTraffic' => [
|
||||
'upload' => $monthTraffic->upload ?? 0,
|
||||
'download' => $monthTraffic->download ?? 0,
|
||||
'total' => $monthTraffic->total ?? 0
|
||||
],
|
||||
'totalTraffic' => [
|
||||
'upload' => $totalTraffic->upload ?? 0,
|
||||
'download' => $totalTraffic->download ?? 0,
|
||||
'total' => $totalTraffic->total ?? 0
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
@ -6,11 +6,6 @@ use App\Http\Controllers\V2\Admin\PlanController;
|
||||
use App\Http\Controllers\V2\Admin\Server\GroupController;
|
||||
use App\Http\Controllers\V2\Admin\Server\RouteController;
|
||||
use App\Http\Controllers\V2\Admin\Server\ManageController;
|
||||
use App\Http\Controllers\V2\Admin\Server\TrojanController;
|
||||
use App\Http\Controllers\V2\Admin\Server\VmessController;
|
||||
use App\Http\Controllers\V2\Admin\Server\ShadowsocksController;
|
||||
use App\Http\Controllers\V2\Admin\Server\HysteriaController;
|
||||
use App\Http\Controllers\V2\Admin\Server\VlessController;
|
||||
use App\Http\Controllers\V2\Admin\OrderController;
|
||||
use App\Http\Controllers\V2\Admin\UserController;
|
||||
use App\Http\Controllers\V2\Admin\StatController;
|
||||
@ -118,15 +113,14 @@ class AdminRoute
|
||||
$router->group([
|
||||
'prefix' => 'stat'
|
||||
], function ($router) {
|
||||
$router->get('/getStat', [StatController::class, 'getStat']);
|
||||
$router->get('/getOverride', [StatController::class, 'getOverride']);
|
||||
$router->get('/getStats', [StatController::class, 'getStats']);
|
||||
$router->get('/getServerLastRank', [StatController::class, 'getServerLastRank']);
|
||||
$router->get('/getServerYesterdayRank', [StatController::class, 'getServerYesterdayRank']);
|
||||
$router->get('/getOrder', [StatController::class, 'getOrder']);
|
||||
$router->any('/getStatUser', [StatController::class, 'getStatUser']);
|
||||
$router->get('/getRanking', [StatController::class, 'getRanking']);
|
||||
$router->get('/getStatRecord', [StatController::class, 'getStatRecord']);
|
||||
$router->get('/getStats', [StatController::class, 'getStats']);
|
||||
$router->get('/getTrafficRank', [StatController::class, 'getTrafficRank']);
|
||||
});
|
||||
|
||||
|
@ -231,6 +231,9 @@ class Clash implements ProtocolInterface
|
||||
|
||||
private function isRegex($exp)
|
||||
{
|
||||
return @preg_match($exp, null) !== false;
|
||||
if (empty($exp)) {
|
||||
return false;
|
||||
}
|
||||
return @preg_match((string)$exp, '') !== false;
|
||||
}
|
||||
}
|
||||
|
@ -333,6 +333,9 @@ class ClashMeta implements ProtocolInterface
|
||||
|
||||
private function isRegex($exp)
|
||||
{
|
||||
return @preg_match($exp, null) !== false;
|
||||
if (empty($exp)) {
|
||||
return false;
|
||||
}
|
||||
return @preg_match($exp, '') !== false;
|
||||
}
|
||||
}
|
||||
|
@ -284,7 +284,10 @@ class Stash implements ProtocolInterface
|
||||
|
||||
private function isRegex($exp)
|
||||
{
|
||||
return @preg_match($exp, null) !== false;
|
||||
if (empty($exp)) {
|
||||
return false;
|
||||
}
|
||||
return @preg_match($exp, '') !== false;
|
||||
}
|
||||
|
||||
private function isMatch($exp, $str)
|
||||
|
@ -12,6 +12,7 @@
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"bacon/bacon-qr-code": "^2.0",
|
||||
"doctrine/dbal": "^3.7",
|
||||
"google/cloud-storage": "^1.35",
|
||||
"google/recaptcha": "^1.2",
|
||||
|
@ -100,6 +100,13 @@ return [
|
||||
'driver' => 'errorlog',
|
||||
'level' => 'debug',
|
||||
],
|
||||
|
||||
'deprecations' => [
|
||||
'driver' => 'daily',
|
||||
'path' => storage_path('logs/deprecations.log'),
|
||||
'level' => 'debug',
|
||||
'days' => 14,
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
|
@ -1,10 +1,8 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use App\Models\Plan;
|
||||
|
||||
return new class extends Migration {
|
||||
/**
|
||||
@ -39,7 +37,7 @@ return new class extends Migration {
|
||||
->count();
|
||||
|
||||
if ($unconvertedCount > 0) {
|
||||
\Log::warning("Found {$unconvertedCount} orders with unconverted period values");
|
||||
Log::warning("Found {$unconvertedCount} orders with unconverted period values");
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,4 +53,4 @@ return new class extends Migration {
|
||||
->update(['period' => $oldPeriod]);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
2
public/assets/admin/assets/index.css
vendored
2
public/assets/admin/assets/index.css
vendored
File diff suppressed because one or more lines are too long
8
public/assets/admin/assets/index.js
vendored
8
public/assets/admin/assets/index.js
vendored
File diff suppressed because one or more lines are too long
34
public/assets/admin/assets/vendor.js
vendored
34
public/assets/admin/assets/vendor.js
vendored
File diff suppressed because one or more lines are too long
@ -95,5 +95,23 @@
|
||||
"Sending frequently, please try again later": "Sending frequently, please try again later",
|
||||
"Current product is sold out": "Current product is sold out",
|
||||
"There are too many password errors, please try again after :minute minutes.": "There are too many password errors, please try again after :minute minutes.",
|
||||
"Reset failed, Please try again later": "Reset failed, Please try again later"
|
||||
"Reset failed, Please try again later": "Reset failed, Please try again later",
|
||||
"Subscribe": "Subscribe",
|
||||
"User Information": "User Information",
|
||||
"Username": "Username",
|
||||
"Status": "Status",
|
||||
"Active": "Active",
|
||||
"Inactive": "Inactive",
|
||||
"Data Used": "Data Used",
|
||||
"Data Limit": "Data Limit",
|
||||
"Expiration Date": "Expiration Date",
|
||||
"Reset In": "Reset In",
|
||||
"Days": "Days",
|
||||
"Subscription Link": "Subscription Link",
|
||||
"Copy": "Copy",
|
||||
"Copied": "Copied",
|
||||
"QR Code": "QR Code",
|
||||
"Unlimited": "Unlimited",
|
||||
"Device Limit": "Device Limit",
|
||||
"Devices": "Devices"
|
||||
}
|
||||
|
@ -95,5 +95,23 @@
|
||||
"Sending frequently, please try again later": "发送频繁,请稍后再试",
|
||||
"Current product is sold out": "当前商品已售罄",
|
||||
"There are too many password errors, please try again after :minute minutes.": "密码错误次数过多,请 :minute 分钟后再试",
|
||||
"Reset failed, Please try again later": "重置失败,请稍后再试"
|
||||
"Reset failed, Please try again later": "重置失败,请稍后再试",
|
||||
"Subscribe": "订阅信息",
|
||||
"User Information": "用户信息",
|
||||
"Username": "用户名",
|
||||
"Status": "状态",
|
||||
"Active": "正常",
|
||||
"Inactive": "未激活",
|
||||
"Data Used": "已用流量",
|
||||
"Data Limit": "流量限制",
|
||||
"Expiration Date": "到期时间",
|
||||
"Reset In": "距离重置",
|
||||
"Days": "天",
|
||||
"Subscription Link": "订阅链接",
|
||||
"Copy": "复制",
|
||||
"Copied": "已复制",
|
||||
"QR Code": "二维码",
|
||||
"Unlimited": "长期有效",
|
||||
"Device Limit": "设备限制",
|
||||
"Devices": "台设备"
|
||||
}
|
||||
|
@ -95,5 +95,23 @@
|
||||
"Sending frequently, please try again later": "發送頻繁,請稍後再試",
|
||||
"Current product is sold out": "當前商品已售罄",
|
||||
"There are too many password errors, please try again after :minute minutes.": "密碼錯誤次數過多,請 :minute 分鐘後再試",
|
||||
"Reset failed, Please try again later": "重置失敗,請稍後再試"
|
||||
"Reset failed, Please try again later": "重置失敗,請稍後再試",
|
||||
"Subscribe": "訂閱資訊",
|
||||
"User Information": "用戶資訊",
|
||||
"Username": "用戶名",
|
||||
"Status": "狀態",
|
||||
"Active": "正常",
|
||||
"Inactive": "未啟用",
|
||||
"Data Used": "已用流量",
|
||||
"Data Limit": "流量限制",
|
||||
"Expiration Date": "到期時間",
|
||||
"Reset In": "距離重置",
|
||||
"Days": "天",
|
||||
"Subscription Link": "訂閱連結",
|
||||
"Copy": "複製",
|
||||
"Copied": "已複製",
|
||||
"QR Code": "二維碼",
|
||||
"Unlimited": "長期有效",
|
||||
"Device Limit": "設備限制",
|
||||
"Devices": "台設備"
|
||||
}
|
||||
|
296
resources/views/client/subscribe.blade.php
Normal file
296
resources/views/client/subscribe.blade.php
Normal file
@ -0,0 +1,296 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ __('Subscribe') }}</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--text: #000000;
|
||||
--text-secondary: #666666;
|
||||
--primary: #2196f3;
|
||||
--success: #4caf50;
|
||||
--danger: #f44336;
|
||||
--border: #eee;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
line-height: 1.6;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.75rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.info-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
min-width: 100px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: var(--text);
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.status {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status.active {
|
||||
background: var(--success);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status.inactive {
|
||||
background: var(--danger);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.links-section {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.links-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.link-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.link-input {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
padding-right: 4rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text);
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.link-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
padding: 0.5rem;
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.copy-btn:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.copy-btn svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.copy-btn.copied {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.qr-section {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.qr-section img {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
padding: 0.75rem;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
body {
|
||||
padding: 1.5rem 1rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.qr-section img {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: #000000;
|
||||
--text: #ffffff;
|
||||
--text-secondary: #999999;
|
||||
--border: #222;
|
||||
}
|
||||
|
||||
.link-input {
|
||||
background: #111;
|
||||
border-color: var(--border);
|
||||
color: var(--text);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1 class="title">{{ __('User Information') }}</h1>
|
||||
|
||||
<div class="info-list">
|
||||
<div class="info-item">
|
||||
<div class="info-label">{{ __('Username') }}</div>
|
||||
<div class="info-value">{{ $username }}</div>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<div class="info-label">{{ __('Status') }}</div>
|
||||
<div class="info-value">
|
||||
<span class="status {{ $status }}">
|
||||
{{ $status === 'active' ? __('Active') : __('Inactive') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<div class="info-label">{{ __('Data Used') }}</div>
|
||||
<div class="info-value">{{ $data_used }}</div>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<div class="info-label">{{ __('Data Limit') }}</div>
|
||||
<div class="info-value">{{ $data_limit }}</div>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<div class="info-label">{{ __('Expiration Date') }}</div>
|
||||
<div class="info-value">{{ $expired_date }}</div>
|
||||
</div>
|
||||
|
||||
@if (isset($device_limit))
|
||||
<div class="info-item">
|
||||
<div class="info-label">{{ __('Device Limit') }}</div>
|
||||
<div class="info-value">{{ $device_limit }} {{ __('Devices') }}</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($reset_day)
|
||||
<div class="info-item">
|
||||
<div class="info-label">{{ __('Reset In') }}</div>
|
||||
<div class="info-value">{{ $reset_day }} {{ __('Days') }}</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="links-section">
|
||||
<h2 class="links-title">{{ __('Subscription Link') }}</h2>
|
||||
<div class="link-item">
|
||||
<input type="text" value="{{ $subscription_url }}" readonly id="sub_url" class="link-input" onclick="this.select()">
|
||||
<button class="copy-btn" onclick="copyToClipboard('sub_url')" title="{{ __('Copy') }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" />
|
||||
</svg>
|
||||
<span>{{ __('Copy') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="qr-section">
|
||||
<img src="data:image/svg+xml;base64,{{ $qr_code }}" alt="{{ __('QR Code') }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function copyToClipboard(elementId) {
|
||||
const element = document.getElementById(elementId);
|
||||
element.select();
|
||||
document.execCommand('copy');
|
||||
element.blur();
|
||||
|
||||
const btn = element.nextElementSibling;
|
||||
const span = btn.querySelector('span');
|
||||
const originalText = span.textContent;
|
||||
|
||||
btn.classList.add('copied');
|
||||
span.textContent = '{{ __('Copied') }}';
|
||||
|
||||
setTimeout(() => {
|
||||
btn.classList.remove('copied');
|
||||
span.textContent = originalText;
|
||||
}, 1000);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,5 +1,8 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Broadcast;
|
||||
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Broadcast Channels
|
||||
|
@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Inspiring;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
use App\Services\ThemeService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
|
@ -1,26 +0,0 @@
|
||||
<?php
|
||||
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use SwooleTW\Http\Websocket\Facades\Websocket;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Websocket Routes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here is where you can register websocket events for your application.
|
||||
|
|
||||
*/
|
||||
|
||||
Websocket::on('connect', function ($websocket, Request $request) {
|
||||
// called while socket on connect
|
||||
});
|
||||
|
||||
Websocket::on('disconnect', function ($websocket) {
|
||||
// called while socket on disconnect
|
||||
});
|
||||
|
||||
Websocket::on('example', function ($websocket, $data) {
|
||||
$websocket->emit('message', $data);
|
||||
});
|
@ -1,42 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use Illuminate\Contracts\Console\Kernel;
|
||||
use PHPUnit\Runner\AfterLastTestHook;
|
||||
use PHPUnit\Runner\BeforeFirstTestHook;
|
||||
|
||||
class Bootstrap implements BeforeFirstTestHook, AfterLastTestHook
|
||||
{
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Bootstrap The Test Environment
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| You may specify console commands that execute once before your test is
|
||||
| run. You are free to add your own additional commands or logic into
|
||||
| this file as needed in order to help your test suite run quicker.
|
||||
|
|
||||
*/
|
||||
|
||||
use CreatesApplication;
|
||||
|
||||
public function executeBeforeFirstTest(): void
|
||||
{
|
||||
$console = $this->createApplication()->make(Kernel::class);
|
||||
|
||||
$commands = [
|
||||
'config:cache',
|
||||
'event:cache',
|
||||
];
|
||||
|
||||
foreach ($commands as $command) {
|
||||
$console->call($command);
|
||||
}
|
||||
}
|
||||
|
||||
public function executeAfterLastTest(): void
|
||||
{
|
||||
array_map('unlink', glob('bootstrap/cache/*.phpunit.php'));
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use Illuminate\Contracts\Console\Kernel;
|
||||
|
||||
trait CreatesApplication
|
||||
{
|
||||
/**
|
||||
* Creates the application.
|
||||
*
|
||||
* @return \Illuminate\Foundation\Application
|
||||
*/
|
||||
public function createApplication()
|
||||
{
|
||||
$app = require __DIR__ . '/../bootstrap/app.php';
|
||||
|
||||
$app->make(Kernel::class)->bootstrap();
|
||||
|
||||
return $app;
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ExampleTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* A basic test example.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testBasicTest()
|
||||
{
|
||||
$response = $this->get('/');
|
||||
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||
|
||||
abstract class TestCase extends BaseTestCase
|
||||
{
|
||||
use CreatesApplication;
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ExampleTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* A basic test example.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testBasicTest()
|
||||
{
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user