fix: resolve PHPStan static analysis warnings

This commit is contained in:
xboard
2025-05-07 19:48:19 +08:00
parent db235c10e8
commit 97e7ffccae
86 changed files with 2335 additions and 1206 deletions

View File

@@ -2,7 +2,7 @@ name: Docker Build and Publish
on: on:
push: push:
branches: ["master"] branches: ["master", "new-dev"]
workflow_dispatch: workflow_dispatch:
env: env:
@@ -59,7 +59,12 @@ jobs:
tags: | tags: |
type=ref,event=branch type=ref,event=branch
type=sha,format=long type=sha,format=long
type=raw,value=new type=raw,value=new,enable=${{ github.ref == 'refs/heads/master' }}
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }}
type=raw,value=${{ steps.get_version.outputs.version }}
labels: |
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}
- name: Get version - name: Get version
id: get_version id: get_version
@@ -80,10 +85,8 @@ jobs:
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max
tags: | tags: ${{ steps.meta.outputs.tags }}
${{ env.REGISTRY }}/${{ github.repository_owner }}/xboard:master labels: ${{ steps.meta.outputs.labels }}
${{ env.REGISTRY }}/${{ github.repository_owner }}/xboard:new
${{ env.REGISTRY }}/${{ github.repository_owner }}/xboard:${{ steps.get_version.outputs.version }}
build-args: | build-args: |
BUILDKIT_INLINE_CACHE=1 BUILDKIT_INLINE_CACHE=1
BUILDKIT_MULTI_PLATFORM=1 BUILDKIT_MULTI_PLATFORM=1

View File

@@ -112,16 +112,13 @@ class CheckCommission extends Command
DB::rollBack(); DB::rollBack();
return false; return false;
} }
if (!CommissionLog::create([ CommissionLog::create([
'invite_user_id' => $inviteUserId, 'invite_user_id' => $inviteUserId,
'user_id' => $order->user_id, 'user_id' => $order->user_id,
'trade_no' => $order->trade_no, 'trade_no' => $order->trade_no,
'order_amount' => $order->total_amount, 'order_amount' => $order->total_amount,
'get_amount' => $commissionBalance 'get_amount' => $commissionBalance
])) { ]);
DB::rollBack();
return false;
}
$inviteUserId = $inviter->invite_user_id; $inviteUserId = $inviter->invite_user_id;
// update order actual commission balance // update order actual commission balance
$order->actual_commission_balance = $order->actual_commission_balance + $commissionBalance; $order->actual_commission_balance = $order->actual_commission_balance + $commissionBalance;

View File

@@ -19,11 +19,11 @@ class ExportV2Log extends Command
public function handle() public function handle()
{ {
$days = $this->argument('days'); $days = $this->argument('days');
$date = Carbon::now()->subDays($days)->startOfDay(); $date = Carbon::now()->subDays((float) $days)->startOfDay();
$logs = DB::table('v2_log') $logs = DB::table('v2_log')
->where('created_at', '>=', $date->timestamp) ->where('created_at', '>=', $date->timestamp)
->get(); ->get();
$fileName = "v2_logs_" . Carbon::now()->format('Y_m_d_His') . ".csv"; $fileName = "v2_logs_" . Carbon::now()->format('Y_m_d_His') . ".csv";
$handle = fopen(storage_path("logs/$fileName"), 'w'); $handle = fopen(storage_path("logs/$fileName"), 'w');
@@ -35,19 +35,19 @@ class ExportV2Log extends Command
fputcsv($handle, [ fputcsv($handle, [
$log->level, $log->level,
$log->id, $log->id,
$log->title, $log->title,
$log->host, $log->host,
$log->uri, $log->uri,
$log->method, $log->method,
$log->data, $log->data,
$log->ip, $log->ip,
$log->context, $log->context,
Carbon::createFromTimestamp($log->created_at)->toDateTimeString(), Carbon::createFromTimestamp($log->created_at)->toDateTimeString(),
Carbon::createFromTimestamp($log->updated_at)->toDateTimeString() Carbon::createFromTimestamp($log->updated_at)->toDateTimeString()
]); ]);
} }
fclose($handle); fclose($handle);
$this->info("日志成功导出到: ". storage_path("logs/$fileName")); $this->info("日志成功导出到: " . storage_path("logs/$fileName"));
} }
} }

View File

@@ -5,30 +5,26 @@ namespace App\Console\Commands;
use App\Models\Plan; use App\Models\Plan;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use App\Models\User; use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
class ResetTraffic extends Command class ResetTraffic extends Command
{ {
protected $builder;
/** /**
* The name and signature of the console command. * @var Builder
* */
protected $builder;
/**
* @var string * @var string
*/ */
protected $signature = 'reset:traffic'; protected $signature = 'reset:traffic';
/** /**
* The console command description.
*
* @var string * @var string
*/ */
protected $description = '流量清空'; protected $description = '流量清空';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct() public function __construct()
{ {
parent::__construct(); parent::__construct();
@@ -37,13 +33,13 @@ class ResetTraffic extends Command
} }
/** /**
* Execute the console command. * 执行流量重置命令
*
* @return mixed
*/ */
public function handle() public function handle()
{ {
ini_set('memory_limit', -1); ini_set('memory_limit', -1);
// 按重置方法分组查询所有套餐
$resetMethods = Plan::select( $resetMethods = Plan::select(
DB::raw("GROUP_CONCAT(`id`) as plan_ids"), DB::raw("GROUP_CONCAT(`id`) as plan_ids"),
DB::raw("reset_traffic_method as method") DB::raw("reset_traffic_method as method")
@@ -51,138 +47,117 @@ class ResetTraffic extends Command
->groupBy('reset_traffic_method') ->groupBy('reset_traffic_method')
->get() ->get()
->toArray(); ->toArray();
// 使用闭包直接引用方法
$resetHandlers = [
Plan::RESET_TRAFFIC_FIRST_DAY_MONTH => fn($builder) => $this->resetByMonthFirstDay($builder),
Plan::RESET_TRAFFIC_MONTHLY => fn($builder) => $this->resetByExpireDay($builder),
Plan::RESET_TRAFFIC_NEVER => null,
Plan::RESET_TRAFFIC_FIRST_DAY_YEAR => fn($builder) => $this->resetByYearFirstDay($builder),
Plan::RESET_TRAFFIC_YEARLY => fn($builder) => $this->resetByExpireYear($builder),
];
// 处理每种重置方法
foreach ($resetMethods as $resetMethod) { foreach ($resetMethods as $resetMethod) {
$planIds = explode(',', $resetMethod['plan_ids']); $planIds = explode(',', $resetMethod['plan_ids']);
switch (true) {
case ($resetMethod['method'] === NULL): { // 获取重置方法
$resetTrafficMethod = admin_setting('reset_traffic_method', 0); $method = $resetMethod['method'];
$builder = with(clone ($this->builder))->whereIn('plan_id', $planIds); if ($method === NULL) {
switch ((int) $resetTrafficMethod) { $method = (int) admin_setting('reset_traffic_method', 0);
// month first day
case 0:
$this->resetByMonthFirstDay($builder);
break;
// expire day
case 1:
$this->resetByExpireDay($builder);
break;
// no action
case 2:
break;
// year first day
case 3:
$this->resetByYearFirstDay($builder);
// year expire day
case 4:
$this->resetByExpireYear($builder);
}
break;
}
case ($resetMethod['method'] === 0): {
$builder = with(clone ($this->builder))->whereIn('plan_id', $planIds);
$this->resetByMonthFirstDay($builder);
break;
}
case ($resetMethod['method'] === 1): {
$builder = with(clone ($this->builder))->whereIn('plan_id', $planIds);
$this->resetByExpireDay($builder);
break;
}
case ($resetMethod['method'] === 2): {
break;
}
case ($resetMethod['method'] === 3): {
$builder = with(clone ($this->builder))->whereIn('plan_id', $planIds);
$this->resetByYearFirstDay($builder);
break;
}
case ($resetMethod['method'] === 4): {
$builder = with(clone ($this->builder))->whereIn('plan_id', $planIds);
$this->resetByExpireYear($builder);
break;
}
} }
// 跳过不重置的方法
if ($method === 2) {
continue;
}
// 获取该方法的处理器
$handler = $resetHandlers[$method] ?? null;
if (!$handler) {
continue;
}
// 创建查询构建器并执行重置
$userQuery = (clone $this->builder)->whereIn('plan_id', $planIds);
$handler($userQuery);
} }
} }
private function resetByExpireYear($builder): void /**
* 按用户年度到期日重置流量
*/
private function resetByExpireYear(Builder $builder): void
{ {
$today = date('m-d');
$users = $builder->with('plan')->get(); $this->resetUsersByDateCondition($builder, function ($user) use ($today) {
$usersToUpdate = []; return date('m-d', $user->expired_at) === $today;
foreach ($users as $user) { });
$expireDay = date('m-d', $user->expired_at);
$today = date('m-d');
if ($expireDay === $today) {
$usersToUpdate[] = [
'id' => $user->id,
'transfer_enable' => $user->plan->transfer_enable
];
}
}
foreach ($usersToUpdate as $userData) {
User::where('id', $userData['id'])->update([
'transfer_enable' => (intval($userData['transfer_enable']) * 1073741824),
'u' => 0,
'd' => 0
]);
}
} }
private function resetByYearFirstDay($builder): void /**
* 按新年第一天重置流量
*/
private function resetByYearFirstDay(Builder $builder): void
{ {
$users = $builder->with('plan')->get(); $isNewYear = date('md') === '0101';
$usersToUpdate = []; if (!$isNewYear) {
foreach ($users as $user) { return;
if ((string) date('md') === '0101') {
$usersToUpdate[] = [
'id' => $user->id,
'transfer_enable' => $user->plan->transfer_enable
];
}
} }
foreach ($usersToUpdate as $userData) { $this->resetAllUsers($builder);
User::where('id', $userData['id'])->update([
'transfer_enable' => (intval($userData['transfer_enable']) * 1073741824),
'u' => 0,
'd' => 0
]);
}
} }
private function resetByMonthFirstDay($builder): void /**
* 按月初第一天重置流量
*/
private function resetByMonthFirstDay(Builder $builder): void
{ {
$users = $builder->with('plan')->get(); $isFirstDayOfMonth = date('d') === '01';
$usersToUpdate = []; if (!$isFirstDayOfMonth) {
foreach ($users as $user) { return;
if ((string) date('d') === '01') {
$usersToUpdate[] = [
'id' => $user->id,
'transfer_enable' => $user->plan->transfer_enable
];
}
} }
foreach ($usersToUpdate as $userData) { $this->resetAllUsers($builder);
User::where('id', $userData['id'])->update([
'transfer_enable' => (intval($userData['transfer_enable']) * 1073741824),
'u' => 0,
'd' => 0
]);
}
} }
private function resetByExpireDay($builder): void
/**
* 按用户到期日重置流量
*/
private function resetByExpireDay(Builder $builder): void
{ {
$lastDay = date('d', strtotime('last day of +0 months'));
$today = date('d'); $today = date('d');
$lastDay = date('d', strtotime('last day of +0 months'));
$this->resetUsersByDateCondition($builder, function ($user) use ($today, $lastDay) {
$expireDay = date('d', $user->expired_at);
return $expireDay === $today || ($today === $lastDay && $expireDay >= $today);
});
}
/**
* 重置所有符合条件的用户流量
*/
private function resetAllUsers(Builder $builder): void
{
$this->resetUsersByDateCondition($builder, function () {
return true;
});
}
/**
* 根据日期条件重置用户流量
* @param Builder $builder 用户查询构建器
* @param callable $condition 日期条件回调
*/
private function resetUsersByDateCondition(Builder $builder, callable $condition): void
{
/** @var \App\Models\User[] $users */
$users = $builder->with('plan')->get(); $users = $builder->with('plan')->get();
$usersToUpdate = []; $usersToUpdate = [];
foreach ($users as $user) { foreach ($users as $user) {
$expireDay = date('d', $user->expired_at); if ($condition($user)) {
if ($expireDay === $today || ($today === $lastDay && $expireDay >= $today)) {
$usersToUpdate[] = [ $usersToUpdate[] = [
'id' => $user->id, 'id' => $user->id,
'transfer_enable' => $user->plan->transfer_enable 'transfer_enable' => $user->plan->transfer_enable

View File

@@ -50,9 +50,9 @@ class XboardInstall extends Command
{ {
try { try {
$isDocker = file_exists('/.dockerenv'); $isDocker = file_exists('/.dockerenv');
$enableSqlite = env('ENABLE_SQLITE', false); $enableSqlite = getenv('ENABLE_SQLITE', false);
$enableRedis = env('ENABLE_REDIS', false); $enableRedis = getenv('ENABLE_REDIS', false);
$adminAccount = env('ADMIN_ACCOUNT', ''); $adminAccount = getenv('ADMIN_ACCOUNT', false);
$this->info("__ __ ____ _ "); $this->info("__ __ ____ _ ");
$this->info("\ \ / /| __ ) ___ __ _ _ __ __| | "); $this->info("\ \ / /| __ ) ___ __ _ _ __ __| | ");
$this->info(" \ \/ / | __ \ / _ \ / _` | '__/ _` | "); $this->info(" \ \/ / | __ \ / _ \ / _` | '__/ _` | ");
@@ -60,7 +60,7 @@ class XboardInstall extends Command
$this->info("/_/ \_\|____/ \___/ \__,_|_| \__,_| "); $this->info("/_/ \_\|____/ \___/ \__,_|_| \__,_| ");
if ( if (
(File::exists(base_path() . '/.env') && $this->getEnvValue('INSTALLED')) (File::exists(base_path() . '/.env') && $this->getEnvValue('INSTALLED'))
|| (env('INSTALLED', false) && $isDocker) || (getenv('INSTALLED', false) && $isDocker)
) { ) {
$securePath = admin_setting('secure_path', admin_setting('frontend_admin_path', hash('crc32b', config('app.key')))); $securePath = admin_setting('secure_path', admin_setting('frontend_admin_path', hash('crc32b', config('app.key'))));
$this->info("访问 http(s)://你的站点/{$securePath} 进入管理面板,你可以在用户中心修改你的密码。"); $this->info("访问 http(s)://你的站点/{$securePath} 进入管理面板,你可以在用户中心修改你的密码。");

View File

@@ -2,12 +2,10 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Models\StatServer;
use App\Models\StatUser;
use App\Services\StatisticalService; use App\Services\StatisticalService;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use App\Models\Stat; use App\Models\Stat;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log;
class XboardStatistics extends Command class XboardStatistics extends Command
{ {
@@ -50,67 +48,6 @@ class XboardStatistics extends Command
info('统计任务执行完毕。耗时:' . (microtime(true) - $startAt) / 1000); info('统计任务执行完毕。耗时:' . (microtime(true) - $startAt) / 1000);
} }
private function statServer()
{
try {
DB::beginTransaction();
$createdAt = time();
$recordAt = strtotime('-1 day', strtotime(date('Y-m-d')));
$statService = new StatisticalService();
$statService->setStartAt($recordAt);
$stats = $statService->getStatServer();
foreach ($stats as $stat) {
if (!StatServer::insert([
'server_id' => $stat['server_id'],
'server_type' => $stat['server_type'],
'u' => $stat['u'],
'd' => $stat['d'],
'created_at' => $createdAt,
'updated_at' => $createdAt,
'record_type' => 'd',
'record_at' => $recordAt
])) {
throw new \Exception('stat server fail');
}
}
DB::commit();
$statService->clearStatServer();
} catch (\Exception $e) {
DB::rollback();
\Log::error($e->getMessage(), ['exception' => $e]);
}
}
private function statUser()
{
try {
DB::beginTransaction();
$createdAt = time();
$recordAt = strtotime('-1 day', strtotime(date('Y-m-d')));
$statService = new StatisticalService();
$statService->setStartAt($recordAt);
$stats = $statService->getStatUser();
foreach ($stats as $stat) {
if (!StatUser::insert([
'user_id' => $stat['user_id'],
'u' => $stat['u'],
'd' => $stat['d'],
'server_rate' => $stat['server_rate'],
'created_at' => $createdAt,
'updated_at' => $createdAt,
'record_type' => 'd',
'record_at' => $recordAt
])) {
throw new \Exception('stat user fail');
}
}
DB::commit();
$statService->clearStatUser();
} catch (\Exception $e) {
DB::rollback();
\Log::error($e->getMessage(), ['exception' => $e]);
}
}
private function stat() private function stat()
{ {
@@ -132,7 +69,7 @@ class XboardStatistics extends Command
} }
Stat::create($data); Stat::create($data);
} catch (\Exception $e) { } catch (\Exception $e) {
\Log::error($e->getMessage(), ['exception' => $e]); Log::error($e->getMessage(), ['exception' => $e]);
} }
} }
} }

View File

@@ -42,9 +42,9 @@ class Kernel extends ConsoleKernel
// horizon metrics // horizon metrics
$schedule->command('horizon:snapshot')->everyFiveMinutes()->onOneServer(); $schedule->command('horizon:snapshot')->everyFiveMinutes()->onOneServer();
// backup Timing // backup Timing
if (env('ENABLE_AUTO_BACKUP_AND_UPDATE', false)) { // if (env('ENABLE_AUTO_BACKUP_AND_UPDATE', false)) {
$schedule->command('backup:database', ['true'])->daily()->onOneServer(); // $schedule->command('backup:database', ['true'])->daily()->onOneServer();
} // }
// 每分钟清理过期的在线状态 // 每分钟清理过期的在线状态
$schedule->call(function () { $schedule->call(function () {
app(UserOnlineService::class)->cleanExpiredOnlineStatus(); app(UserOnlineService::class)->cleanExpiredOnlineStatus();

View File

@@ -16,7 +16,7 @@ class Handler extends ExceptionHandler
/** /**
* A list of the exception types that are not reported. * A list of the exception types that are not reported.
* *
* @var array * @var array<int, class-string<Throwable>>
*/ */
protected $dontReport = [ protected $dontReport = [
ApiException::class, ApiException::class,
@@ -26,7 +26,7 @@ class Handler extends ExceptionHandler
/** /**
* A list of the inputs that are never flashed for validation exceptions. * A list of the inputs that are never flashed for validation exceptions.
* *
* @var array * @var array<int, string>
*/ */
protected $dontFlash = [ protected $dontFlash = [
'password', 'password',

View File

@@ -7,227 +7,89 @@ use App\Http\Controllers\Controller;
use App\Http\Requests\Passport\AuthForget; use App\Http\Requests\Passport\AuthForget;
use App\Http\Requests\Passport\AuthLogin; use App\Http\Requests\Passport\AuthLogin;
use App\Http\Requests\Passport\AuthRegister; use App\Http\Requests\Passport\AuthRegister;
use App\Jobs\SendEmailJob; use App\Services\Auth\LoginService;
use App\Models\InviteCode; use App\Services\Auth\MailLinkService;
use App\Models\Plan; use App\Services\Auth\RegisterService;
use App\Models\User;
use App\Services\AuthService; use App\Services\AuthService;
use App\Utils\CacheKey;
use App\Utils\Dict;
use App\Utils\Helper;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use ReCaptcha\ReCaptcha;
class AuthController extends Controller class AuthController extends Controller
{ {
protected MailLinkService $mailLinkService;
protected RegisterService $registerService;
protected LoginService $loginService;
public function __construct(
MailLinkService $mailLinkService,
RegisterService $registerService,
LoginService $loginService
) {
$this->mailLinkService = $mailLinkService;
$this->registerService = $registerService;
$this->loginService = $loginService;
}
/**
* 通过邮件链接登录
*/
public function loginWithMailLink(Request $request) public function loginWithMailLink(Request $request)
{ {
if (!(int)admin_setting('login_with_mail_link_enable')) {
return $this->fail([404,null]);
}
$params = $request->validate([ $params = $request->validate([
'email' => 'required|email:strict', 'email' => 'required|email:strict',
'redirect' => 'nullable' 'redirect' => 'nullable'
]); ]);
if (Cache::get(CacheKey::get('LAST_SEND_LOGIN_WITH_MAIL_LINK_TIMESTAMP', $params['email']))) { [$success, $result] = $this->mailLinkService->handleMailLink(
return $this->fail([429 ,__('Sending frequently, please try again later')]); $params['email'],
$request->input('redirect')
);
if (!$success) {
return $this->fail($result);
} }
$user = User::where('email', $params['email'])->first(); return $this->success($result);
if (!$user) {
return $this->success(true);
}
$code = Helper::guid();
$key = CacheKey::get('TEMP_TOKEN', $code);
Cache::put($key, $user->id, 300);
Cache::put(CacheKey::get('LAST_SEND_LOGIN_WITH_MAIL_LINK_TIMESTAMP', $params['email']), time(), 60);
$redirect = '/#/login?verify=' . $code . '&redirect=' . ($request->input('redirect') ? $request->input('redirect') : 'dashboard');
if (admin_setting('app_url')) {
$link = admin_setting('app_url') . $redirect;
} else {
$link = url($redirect);
}
SendEmailJob::dispatch([
'email' => $user->email,
'subject' => __('Login to :name', [
'name' => admin_setting('app_name', 'XBoard')
]),
'template_name' => 'login',
'template_value' => [
'name' => admin_setting('app_name', 'XBoard'),
'link' => $link,
'url' => admin_setting('app_url')
]
]);
return $this->success($link);
} }
/**
* 用户注册
*/
public function register(AuthRegister $request) public function register(AuthRegister $request)
{ {
if ((int)admin_setting('register_limit_by_ip_enable', 0)) { [$success, $result] = $this->registerService->register($request);
$registerCountByIP = Cache::get(CacheKey::get('REGISTER_IP_RATE_LIMIT', $request->ip())) ?? 0;
if ((int)$registerCountByIP >= (int)admin_setting('register_limit_count', 3)) { if (!$success) {
return $this->fail([429,__('Register frequently, please try again after :minute minute', [ return $this->fail($result);
'minute' => admin_setting('register_limit_expire', 60)
])]);
}
}
if ((int)admin_setting('recaptcha_enable', 0)) {
$recaptcha = new ReCaptcha(admin_setting('recaptcha_key'));
$recaptchaResp = $recaptcha->verify($request->input('recaptcha_data'));
if (!$recaptchaResp->isSuccess()) {
return $this->fail([400,__('Invalid code is incorrect')]);
}
}
if ((int)admin_setting('email_whitelist_enable', 0)) {
if (!Helper::emailSuffixVerify(
$request->input('email'),
admin_setting('email_whitelist_suffix', Dict::EMAIL_WHITELIST_SUFFIX_DEFAULT))
) {
return $this->fail([400,__('Email suffix is not in the Whitelist')]);
}
}
if ((int)admin_setting('email_gmail_limit_enable', 0)) {
$prefix = explode('@', $request->input('email'))[0];
if (strpos($prefix, '.') !== false || strpos($prefix, '+') !== false) {
return $this->fail([400,__('Gmail alias is not supported')]);
}
}
if ((int)admin_setting('stop_register', 0)) {
return $this->fail([400,__('Registration has closed')]);
}
if ((int)admin_setting('invite_force', 0)) {
if (empty($request->input('invite_code'))) {
return $this->fail([422,__('You must use the invitation code to register')]);
}
}
if ((int)admin_setting('email_verify', 0)) {
if (empty($request->input('email_code'))) {
return $this->fail([422,__('Email verification code cannot be empty')]);
}
if ((string)Cache::get(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email'))) !== (string)$request->input('email_code')) {
return $this->fail([400,__('Incorrect email verification code')]);
}
}
$email = $request->input('email');
$password = $request->input('password');
$exist = User::where('email', $email)->first();
if ($exist) {
return $this->fail([400201,__('Email already exists')]);
}
$user = new User();
$user->email = $email;
$user->password = password_hash($password, PASSWORD_DEFAULT);
$user->uuid = Helper::guid(true);
$user->token = Helper::guid();
// TODO 增加过期默认值、流量告急提醒默认值
$user->remind_expire = admin_setting('default_remind_expire',1);
$user->remind_traffic = admin_setting('default_remind_traffic',1);
if ($request->input('invite_code')) {
$inviteCode = InviteCode::where('code', $request->input('invite_code'))
->where('status', 0)
->first();
if (!$inviteCode) {
if ((int)admin_setting('invite_force', 0)) {
return $this->fail([400,__('Invalid invitation code')]);
}
} else {
$user->invite_user_id = $inviteCode->user_id ? $inviteCode->user_id : null;
if (!(int)admin_setting('invite_never_expire', 0)) {
$inviteCode->status = 1;
$inviteCode->save();
}
}
} }
// try out $authService = new AuthService($result);
if ((int)admin_setting('try_out_plan_id', 0)) { return $this->success($authService->generateAuthData());
$plan = Plan::find(admin_setting('try_out_plan_id'));
if ($plan) {
$user->transfer_enable = $plan->transfer_enable * 1073741824;
$user->plan_id = $plan->id;
$user->group_id = $plan->group_id;
$user->expired_at = time() + (admin_setting('try_out_hour', 1) * 3600);
$user->speed_limit = $plan->speed_limit;
}
}
if (!$user->save()) {
return $this->fail([500,__('Register failed')]);
}
if ((int)admin_setting('email_verify', 0)) {
Cache::forget(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email')));
}
$user->last_login_at = time();
$user->save();
if ((int)admin_setting('register_limit_by_ip_enable', 0)) {
Cache::put(
CacheKey::get('REGISTER_IP_RATE_LIMIT', $request->ip()),
(int)$registerCountByIP + 1,
(int)admin_setting('register_limit_expire', 60) * 60
);
}
$authService = new AuthService($user);
$data = $authService->generateAuthData();
return $this->success($data);
} }
/**
* 用户登录
*/
public function login(AuthLogin $request) public function login(AuthLogin $request)
{ {
$email = $request->input('email'); $email = $request->input('email');
$password = $request->input('password'); $password = $request->input('password');
if ((int)admin_setting('password_limit_enable', 1)) { [$success, $result] = $this->loginService->login($email, $password);
$passwordErrorCount = (int)Cache::get(CacheKey::get('PASSWORD_ERROR_LIMIT', $email), 0);
if ($passwordErrorCount >= (int)admin_setting('password_limit_count', 5)) { if (!$success) {
return $this->fail([429,__('There are too many password errors, please try again after :minute minutes.', [ return $this->fail($result);
'minute' => admin_setting('password_limit_expire', 60)
])]);
}
} }
$user = User::where('email', $email)->first(); $authService = new AuthService($result);
if (!$user) {
return $this->fail([400, __('Incorrect email or password')]);
}
if (!Helper::multiPasswordVerify(
$user->password_algo,
$user->password_salt,
$password,
$user->password)
) {
if ((int)admin_setting('password_limit_enable')) {
Cache::put(
CacheKey::get('PASSWORD_ERROR_LIMIT', $email),
(int)$passwordErrorCount + 1,
60 * (int)admin_setting('password_limit_expire', 60)
);
}
return $this->fail([400, __('Incorrect email or password')]);
}
if ($user->banned) {
return $this->fail([400, __('Your account has been suspended')]);
}
$authService = new AuthService($user);
return $this->success($authService->generateAuthData()); return $this->success($authService->generateAuthData());
} }
/**
* 通过token登录
*/
public function token2Login(Request $request) public function token2Login(Request $request)
{ {
// 处理直接通过token重定向
if ($token = $request->input('token')) { if ($token = $request->input('token')) {
$redirect = '/#/login?verify=' . $token . '&redirect=' . ($request->input('redirect', 'dashboard')); $redirect = '/#/login?verify=' . $token . '&redirect=' . ($request->input('redirect', 'dashboard'));
@@ -238,9 +100,9 @@ class AuthController extends Controller
); );
} }
// 处理通过验证码登录
if ($verify = $request->input('verify')) { if ($verify = $request->input('verify')) {
$key = CacheKey::get('TEMP_TOKEN', $verify); $userId = $this->mailLinkService->handleTokenLogin($verify);
$userId = Cache::get($key);
if (!$userId) { if (!$userId) {
return response()->json([ return response()->json([
@@ -248,15 +110,14 @@ class AuthController extends Controller
], 400); ], 400);
} }
$user = User::findOrFail($userId); $user = \App\Models\User::find($userId);
if ($user->banned) { if (!$user) {
return response()->json([ return response()->json([
'message' => __('Your account has been suspended') 'message' => __('User not found')
], 400); ], 400);
} }
Cache::forget($key);
$authService = new AuthService($user); $authService = new AuthService($user);
return response()->json([ return response()->json([
@@ -269,6 +130,9 @@ class AuthController extends Controller
], 400); ], 400);
} }
/**
* 获取快速登录URL
*/
public function getQuickLoginUrl(Request $request) public function getQuickLoginUrl(Request $request)
{ {
$authorization = $request->input('auth_data') ?? $request->header('authorization'); $authorization = $request->input('auth_data') ?? $request->header('authorization');
@@ -287,38 +151,25 @@ class AuthController extends Controller
], 401); ], 401);
} }
$code = Helper::guid(); $url = $this->mailLinkService->getQuickLoginUrl($user, $request->input('redirect'));
$key = CacheKey::get('TEMP_TOKEN', $code);
Cache::put($key, $user['id'], 60);
$redirect = '/#/login?verify=' . $code . '&redirect=' . ($request->input('redirect') ? $request->input('redirect') : 'dashboard');
if (admin_setting('app_url')) {
$url = admin_setting('app_url') . $redirect;
} else {
$url = url($redirect);
}
return $this->success($url); return $this->success($url);
} }
/**
* 忘记密码处理
*/
public function forget(AuthForget $request) public function forget(AuthForget $request)
{ {
$forgetRequestLimitKey = CacheKey::get('FORGET_REQUEST_LIMIT', $request->input('email')); [$success, $result] = $this->loginService->resetPassword(
$forgetRequestLimit = (int)Cache::get($forgetRequestLimitKey); $request->input('email'),
if ($forgetRequestLimit >= 3) return $this->fail([429, __('Reset failed, Please try again later')]); $request->input('email_code'),
if ((string)Cache::get(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email'))) !== (string)$request->input('email_code')) { $request->input('password')
Cache::put($forgetRequestLimitKey, $forgetRequestLimit ? $forgetRequestLimit + 1 : 1, 300); );
return $this->fail([400,__('Incorrect email verification code')]);
if (!$success) {
return $this->fail($result);
} }
$user = User::where('email', $request->input('email'))->first();
if (!$user) {
return $this->fail([400,__('This email is not registered in the system')]);
}
$user->password = password_hash($request->input('password'), PASSWORD_DEFAULT);
$user->password_algo = NULL;
$user->password_salt = NULL;
if (!$user->save()) {
return $this->fail([500,__('Reset failed')]);
}
Cache::forget(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email')));
return $this->success(true); return $this->success(true);
} }
} }

View File

@@ -15,14 +15,10 @@ use ReCaptcha\ReCaptcha;
class CommController extends Controller class CommController extends Controller
{ {
private function isEmailVerify()
{
return $this->success((int)admin_setting('email_verify', 0) ? 1 : 0);
}
public function sendEmailVerify(CommSendEmailVerify $request) public function sendEmailVerify(CommSendEmailVerify $request)
{ {
if ((int)admin_setting('recaptcha_enable', 0)) { if ((int) admin_setting('recaptcha_enable', 0)) {
$recaptcha = new ReCaptcha(admin_setting('recaptcha_key')); $recaptcha = new ReCaptcha(admin_setting('recaptcha_key'));
$recaptchaResp = $recaptcha->verify($request->input('recaptcha_data')); $recaptchaResp = $recaptcha->verify($request->input('recaptcha_data'));
if (!$recaptchaResp->isSuccess()) { if (!$recaptchaResp->isSuccess()) {
@@ -63,12 +59,4 @@ class CommController extends Controller
return $this->success(true); return $this->success(true);
} }
private function getEmailSuffix()
{
$suffix = admin_setting('email_whitelist_suffix', Dict::EMAIL_WHITELIST_SUFFIX_DEFAULT);
if (!is_array($suffix)) {
return preg_split('/,/', $suffix);
}
return $suffix;
}
} }

View File

@@ -150,6 +150,20 @@ class UniProxyController extends Controller
'socks' => [ 'socks' => [
'server_port' => (int) $serverPort, 'server_port' => (int) $serverPort,
], ],
'naive' => [
'server_port' => (int) $serverPort,
'tls' => (int) $protocolSettings['tls'],
'tls_settings' => $protocolSettings['tls_settings']
],
'http' => [
'server_port' => (int) $serverPort,
'tls' => (int) $protocolSettings['tls'],
'tls_settings' => $protocolSettings['tls_settings']
],
'mieru' => [
'server_port' => (string) $serverPort,
'protocol' => (int) $protocolSettings['protocol'],
],
default => [] default => []
}; };
@@ -163,7 +177,7 @@ class UniProxyController extends Controller
} }
$eTag = sha1(json_encode($response)); $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); return response(null, 304);
} }
return response($response)->header('ETag', "\"{$eTag}\""); return response($response)->header('ETag', "\"{$eTag}\"");

View File

@@ -1,58 +0,0 @@
<?php
namespace App\Http\Controllers\V1\Staff;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\NoticeSave;
use App\Models\Notice;
use Illuminate\Http\Request;
class NoticeController extends Controller
{
public function fetch(Request $request)
{
$data = Notice::orderBy('id', 'DESC')->get();
return $this->success($data);
}
public function save(NoticeSave $request)
{
$data = $request->only([
'title',
'content',
'img_url'
]);
if (!$request->input('id')) {
if (!Notice::create($data)) {
return $this->fail([500, '创建失败']);
}
} else {
try {
Notice::find($request->input('id'))->update($data);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500, '保存失败']);
}
}
return $this->success(true);
}
public function drop(Request $request)
{
$request->validate([
'id' => 'required'
],[
'id.required' => '公告ID不能为空'
]);
$notice = Notice::find($request->input('id'));
if (!$notice) {
return $this->fail([400202,'公告不存在']);
}
if (!$notice->delete()) {
return $this->fail([500,'公告删除失败']);
}
return $this->success(true);
}
}

View File

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

View File

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

View File

@@ -1,102 +0,0 @@
<?php
namespace App\Http\Controllers\V1\Staff;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\UserSendMail;
use App\Http\Requests\Staff\UserUpdate;
use App\Jobs\SendEmailJob;
use App\Models\Plan;
use App\Models\User;
use Illuminate\Http\Request;
class UserController extends Controller
{
public function getUserInfoById(Request $request)
{
if (empty($request->input('id'))) {
return $this->fail([422,'用户ID不能为空']);
}
$user = User::where('is_admin', 0)
->where('id', $request->input('id'))
->where('is_staff', 0)
->first();
if (!$user) return $this->fail([400202,'用户不存在']);
return $this->success($user);
}
public function update(UserUpdate $request)
{
$params = $request->validated();
$user = User::find($request->input('id'));
if (!$user) {
return $this->fail([400202,'用户不存在']);
}
if (User::where('email', $params['email'])->first() && $user->email !== $params['email']) {
return $this->fail([400201,'邮箱已被使用']);
}
if (isset($params['password'])) {
$params['password'] = password_hash($params['password'], PASSWORD_DEFAULT);
$params['password_algo'] = NULL;
} else {
unset($params['password']);
}
if (isset($params['plan_id'])) {
$plan = Plan::find($params['plan_id']);
if (!$plan) {
return $this->fail([400202,'订阅不存在']);
}
$params['group_id'] = $plan->group_id;
}
try {
$user->update($params);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500,'更新失败']);
}
return $this->success(true);
}
public function sendMail(UserSendMail $request)
{
$sortType = in_array($request->input('sort_type'), ['ASC', 'DESC']) ? $request->input('sort_type') : 'DESC';
$sort = $request->input('sort') ? $request->input('sort') : 'created_at';
$builder = User::orderBy($sort, $sortType);
$this->filter($request, $builder);
$users = $builder->get();
foreach ($users as $user) {
SendEmailJob::dispatch([
'email' => $user->email,
'subject' => $request->input('subject'),
'template_name' => 'notify',
'template_value' => [
'name' => admin_setting('app_name', 'XBoard'),
'url' => admin_setting('app_url'),
'content' => $request->input('content')
]
]);
}
return $this->success(true);
}
public function ban(Request $request)
{
$sortType = in_array($request->input('sort_type'), ['ASC', 'DESC']) ? $request->input('sort_type') : 'DESC';
$sort = $request->input('sort') ? $request->input('sort') : 'created_at';
$builder = User::orderBy($sort, $sortType);
$this->filter($request, $builder);
try {
$builder->update([
'banned' => 1
]);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500,'处理上失败']);
}
return $this->success(true);
}
}

View File

@@ -43,16 +43,15 @@ class OrderController extends Controller
$request->validate([ $request->validate([
'trade_no' => 'required|string', 'trade_no' => 'required|string',
]); ]);
$order = Order::with('payment') $order = Order::with(['payment','plan'])
->where('user_id', $request->user()->id) ->where('user_id', $request->user()->id)
->where('trade_no', $request->input('trade_no')) ->where('trade_no', $request->input('trade_no'))
->first(); ->first();
if (!$order) { if (!$order) {
return $this->fail([400, __('Order does not exist or has been paid')]); return $this->fail([400, __('Order does not exist or has been paid')]);
} }
$order['plan'] = Plan::find($order->plan_id);
$order['try_out_plan_id'] = (int) admin_setting('try_out_plan_id'); $order['try_out_plan_id'] = (int) admin_setting('try_out_plan_id');
if (!$order['plan']) { if (!$order->plan) {
return $this->fail([400, __('Subscription plan does not exist')]); return $this->fail([400, __('Subscription plan does not exist')]);
} }
if ($order->surplus_order_ids) { if ($order->surplus_order_ids) {
@@ -81,7 +80,7 @@ class OrderController extends Controller
// Validate plan purchase // Validate plan purchase
$planService->validatePurchase($user, $request->input('period')); $planService->validatePurchase($user, $request->input('period'));
return DB::transaction(function () use ($request, $plan, $user, $userService, $planService) { return DB::transaction(function () use ($request, $plan, $user, $userService) {
$period = $request->input('period'); $period = $request->input('period');
$newPeriod = PlanService::getPeriodKey($period); $newPeriod = PlanService::getPeriodKey($period);
@@ -169,12 +168,13 @@ class OrderController extends Controller
]); ]);
} }
$payment = Payment::find($method); $payment = Payment::find($method);
if (!$payment || $payment->enable !== 1) if (!$payment || !$payment->enable) {
return $this->fail([400, __('Payment method is not available')]); return $this->fail([400, __('Payment method is not available')]);
}
$paymentService = new PaymentService($payment->payment, $payment->id); $paymentService = new PaymentService($payment->payment, $payment->id);
$order->handling_amount = NULL; $order->handling_amount = NULL;
if ($payment->handling_fee_fixed || $payment->handling_fee_percent) { if ($payment->handling_fee_fixed || $payment->handling_fee_percent) {
$order->handling_amount = round(($order->total_amount * ($payment->handling_fee_percent / 100)) + $payment->handling_fee_fixed); $order->handling_amount = (int) round(($order->total_amount * ($payment->handling_fee_percent / 100)) + $payment->handling_fee_fixed);
} }
$order->payment_id = $method; $order->payment_id = $method;
if (!$order->save()) if (!$order->save())

View File

@@ -58,11 +58,13 @@ class UserController extends Controller
if (!$user) { if (!$user) {
return $this->fail([400, __('The user does not exist')]); return $this->fail([400, __('The user does not exist')]);
} }
if (!Helper::multiPasswordVerify( if (
$user->password_algo, !Helper::multiPasswordVerify(
$user->password_salt, $user->password_algo,
$request->input('old_password'), $user->password_salt,
$user->password) $request->input('old_password'),
$user->password
)
) { ) {
return $this->fail([400, __('The old password is wrong')]); return $this->fail([400, __('The old password is wrong')]);
} }

View File

@@ -64,12 +64,6 @@ class ConfigController extends Controller
return $this->success(true); return $this->success(true);
} }
private function getTemplateContent(string $filename): string
{
$path = resource_path("rules/{$filename}");
return File::exists($path) ? File::get($path) : '';
}
public function fetch(Request $request) public function fetch(Request $request)
{ {
$key = $request->input('key'); $key = $request->input('key');

View File

@@ -15,6 +15,7 @@ use App\Utils\Helper;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Log;
class OrderController extends Controller class OrderController extends Controller
{ {
@@ -27,7 +28,7 @@ class OrderController extends Controller
if ($order->surplus_order_ids) { if ($order->surplus_order_ids) {
$order['surplus_orders'] = Order::whereIn('id', $order->surplus_order_ids)->get(); $order['surplus_orders'] = Order::whereIn('id', $order->surplus_order_ids)->get();
} }
$order['period'] = PlanService::getLegacyPeriod($order->period); $order['period'] = PlanService::getLegacyPeriod((string) $order->period);
return $this->success($order); return $this->success($order);
} }
@@ -45,17 +46,21 @@ class OrderController extends Controller
$this->applyFiltersAndSorts($request, $orderModel); $this->applyFiltersAndSorts($request, $orderModel);
return response()->json( /** @var \Illuminate\Pagination\LengthAwarePaginator $paginatedResults */
$orderModel $paginatedResults = $orderModel
->latest('created_at') ->latest('created_at')
->paginate( ->paginate(
perPage: $pageSize, perPage: $pageSize,
page: $current page: $current
)->through(fn($order) => [ );
...$order->toArray(),
'period' => PlanService::getLegacyPeriod($order->period) $paginatedResults->getCollection()->transform(function($order) {
]), $orderArray = $order->toArray();
); $orderArray['period'] = PlanService::getLegacyPeriod((string) $order->period);
return $orderArray;
});
return response()->json($paginatedResults);
} }
private function applyFiltersAndSorts(Request $request, Builder $builder): void private function applyFiltersAndSorts(Request $request, Builder $builder): void
@@ -112,8 +117,8 @@ class OrderController extends Controller
'lte' => '<=', 'lte' => '<=',
'like' => 'like', 'like' => 'like',
'notlike' => 'not like', 'notlike' => 'not like',
'null' => static fn($q) => $q->whereNull($queryField), 'null' => static fn($q) => $q->whereNull($field),
'notnull' => static fn($q) => $q->whereNotNull($queryField), 'notnull' => static fn($q) => $q->whereNotNull($field),
default => 'like' default => 'like'
}, match (strtolower($operator)) { }, match (strtolower($operator)) {
'like', 'notlike' => "%{$filterValue}%", 'like', 'notlike' => "%{$filterValue}%",
@@ -184,7 +189,7 @@ class OrderController extends Controller
try { try {
$order->update($params); $order->update($params);
} catch (\Exception $e) { } catch (\Exception $e) {
\Log::error($e); Log::error($e);
return $this->fail([500, '更新失败']); return $this->fail([500, '更新失败']);
} }
@@ -215,11 +220,12 @@ class OrderController extends Controller
$orderService = new OrderService($order); $orderService = new OrderService($order);
$order->user_id = $user->id; $order->user_id = $user->id;
$order->plan_id = $plan->id; $order->plan_id = $plan->id;
$order->period = PlanService::getPeriodKey($request->input('period')); $period = $request->input('period');
$order->period = (int) PlanService::getPeriodKey((string) $period);
$order->trade_no = Helper::guid(); $order->trade_no = Helper::guid();
$order->total_amount = $request->input('total_amount'); $order->total_amount = $request->input('total_amount');
if (PlanService::getPeriodKey($order->period) === Plan::PERIOD_RESET_TRAFFIC) { if (PlanService::getPeriodKey((string) $order->period) === Plan::PERIOD_RESET_TRAFFIC) {
$order->type = Order::TYPE_RESET_TRAFFIC; $order->type = Order::TYPE_RESET_TRAFFIC;
} else if ($user->plan_id !== NULL && $order->plan_id !== $user->plan_id) { } else if ($user->plan_id !== NULL && $order->plan_id !== $user->plan_id) {
$order->type = Order::TYPE_UPGRADE; $order->type = Order::TYPE_UPGRADE;

View File

@@ -8,6 +8,7 @@ use App\Models\Plan;
use App\Models\User; use App\Models\User;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class PlanController extends Controller class PlanController extends Controller
{ {
@@ -58,7 +59,7 @@ class PlanController extends Controller
return $this->success(true); return $this->success(true);
} catch (\Exception $e) { } catch (\Exception $e) {
DB::rollBack(); DB::rollBack();
\Log::error($e); Log::error($e);
return $this->fail([500, '保存失败']); return $this->fail([500, '保存失败']);
} }
} }
@@ -76,12 +77,12 @@ class PlanController extends Controller
if (User::where('plan_id', $request->input('id'))->first()) { if (User::where('plan_id', $request->input('id'))->first()) {
return $this->fail([400201, '该订阅下存在用户无法删除']); return $this->fail([400201, '该订阅下存在用户无法删除']);
} }
if ($request->input('id')) {
$plan = Plan::find($request->input('id')); $plan = Plan::find($request->input('id'));
if (!$plan) { if (!$plan) {
return $this->fail([400202, '该订阅不存在']); return $this->fail([400202, '该订阅不存在']);
}
} }
return $this->success($plan->delete()); return $this->success($plan->delete());
} }
@@ -101,7 +102,7 @@ class PlanController extends Controller
try { try {
$plan->update($updateData); $plan->update($updateData);
} catch (\Exception $e) { } catch (\Exception $e) {
\Log::error($e); Log::error($e);
return $this->fail([500, '保存失败']); return $this->fail([500, '保存失败']);
} }
@@ -124,7 +125,7 @@ class PlanController extends Controller
DB::commit(); DB::commit();
} catch (\Exception $e) { } catch (\Exception $e) {
DB::rollBack(); DB::rollBack();
\Log::error($e); Log::error($e);
return $this->fail([500, '保存失败']); return $this->fail([500, '保存失败']);
} }
return $this->success(true); return $this->success(true);

View File

@@ -14,15 +14,15 @@ class GroupController extends Controller
{ {
public function fetch(Request $request): JsonResponse public function fetch(Request $request): JsonResponse
{ {
$serverGroups = ServerGroup::query() $serverGroups = ServerGroup::query()
->orderByDesc('id') ->orderByDesc('id')
->withCount('users') ->withCount('users')
->get() ->get();
->transform(function ($group) {
$group->server_count = $group->servers()->count(); // 只在需要时手动加载server_count
return $group; $serverGroups->each(function ($group) {
}); $group->setAttribute('server_count', $group->server_count);
});
return $this->success($serverGroups); return $this->success($serverGroups);
} }

View File

@@ -10,12 +10,13 @@ use App\Models\ServerGroup;
use App\Services\ServerService; use App\Services\ServerService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class ManageController extends Controller class ManageController extends Controller
{ {
public function getNodes(Request $request) public function getNodes(Request $request)
{ {
$servers = collect(ServerService::getAllServers())->map(function ($item) { $servers = ServerService::getAllServers()->map(function ($item) {
$item['groups'] = ServerGroup::whereIn('id', $item['group_ids'])->get(['name', 'id']); $item['groups'] = ServerGroup::whereIn('id', $item['group_ids'])->get(['name', 'id']);
$item['parent'] = $item->parent; $item['parent'] = $item->parent;
return $item; return $item;
@@ -41,7 +42,7 @@ class ManageController extends Controller
DB::commit(); DB::commit();
} catch (\Exception $e) { } catch (\Exception $e) {
DB::rollBack(); DB::rollBack();
\Log::error($e); Log::error($e);
return $this->fail([500, '保存失败']); return $this->fail([500, '保存失败']);
} }
@@ -60,7 +61,7 @@ class ManageController extends Controller
$server->update($params); $server->update($params);
return $this->success(true); return $this->success(true);
} catch (\Exception $e) { } catch (\Exception $e) {
\Log::error($e); Log::error($e);
return $this->fail([500, '保存失败']); return $this->fail([500, '保存失败']);
} }
} }
@@ -69,7 +70,7 @@ class ManageController extends Controller
Server::create($params); Server::create($params);
return $this->success(true); return $this->success(true);
} catch (\Exception $e) { } catch (\Exception $e) {
\Log::error($e); Log::error($e);
return $this->fail([500, '创建失败']); return $this->fail([500, '创建失败']);
} }
@@ -83,7 +84,7 @@ class ManageController extends Controller
'show' => 'integer', 'show' => 'integer',
]); ]);
if (Server::where('id', $request->id)->update(['show' => $request->show]) === false) { if (!Server::where('id', $request->id)->update(['show' => $request->show])) {
return $this->fail([500, '保存失败']); return $this->fail([500, '保存失败']);
} }
return $this->success(true); return $this->success(true);

View File

@@ -25,8 +25,7 @@ class StatController extends Controller
{ {
// 获取在线节点数 // 获取在线节点数
$onlineNodes = Server::all()->filter(function ($server) { $onlineNodes = Server::all()->filter(function ($server) {
$server->loadServerStatus(); return !!$server->is_online;
return $server->is_online;
})->count(); })->count();
// 获取在线设备数和在线用户数 // 获取在线设备数和在线用户数
$onlineDevices = User::where('t', '>=', time() - 600) $onlineDevices = User::where('t', '>=', time() - 600)
@@ -268,8 +267,7 @@ class StatController extends Controller
// 获取在线节点数 // 获取在线节点数
$onlineNodes = Server::all()->filter(function ($server) { $onlineNodes = Server::all()->filter(function ($server) {
$server->loadServerStatus(); return !!$server->is_online;
return $server->is_online;
})->count(); })->count();
// 获取在线设备数和在线用户数 // 获取在线设备数和在线用户数

View File

@@ -55,13 +55,10 @@ class TicketController extends Controller
if (!$ticket) { if (!$ticket) {
return $this->fail([400202, '工单不存在']); return $this->fail([400202, '工单不存在']);
} }
$ticket->user = UserController::transformUserData($ticket->user); $result = $ticket->toArray();
$ticket->messages->each(function ($message) use ($ticket) { $result['user'] = UserController::transformUserData($ticket->user);
$message->is_me = $message->user_id !== $ticket->user_id;
}); return $this->success($result);
return $this->success($ticket);
} }
/** /**
@@ -91,12 +88,16 @@ class TicketController extends Controller
perPage: $request->integer('pageSize', 10), perPage: $request->integer('pageSize', 10),
page: $request->integer('current', 1) page: $request->integer('current', 1)
); );
$tickets->getCollection()->transform(function ($ticket) {
$ticket->user = UserController::transformUserData($ticket->user); // 获取items然后映射转换
return $ticket; $items = collect($tickets->items())->map(function ($ticket) {
}); $ticketData = $ticket->toArray();
$ticketData['user'] = UserController::transformUserData($ticket->user);
return $ticketData;
})->all();
return response([ return response([
'data' => $tickets->items(), 'data' => $items,
'total' => $tickets->total() 'total' => $tickets->total()
]); ]);
} }
@@ -137,4 +138,19 @@ class TicketController extends Controller
return $this->fail([500101, '关闭失败']); return $this->fail([500101, '关闭失败']);
} }
} }
public function show($ticketId)
{
$ticket = Ticket::with([
'user',
'messages' => function ($query) {
$query->with(['user']); // 如果需要用户信息
}
])->findOrFail($ticketId);
// 自动包含 is_me 属性
return response()->json([
'data' => $ticket
]);
}
} }

View File

@@ -81,7 +81,7 @@ class UserController extends Controller
// 处理关联查询 // 处理关联查询
if (str_contains($field, '.')) { if (str_contains($field, '.')) {
[$relation, $relationField] = explode('.', $field); [$relation, $relationField] = explode('.', $field);
$query->whereHas($relation, function($q) use ($relationField, $value) { $query->whereHas($relation, function ($q) use ($relationField, $value) {
if (is_array($value)) { if (is_array($value)) {
$q->whereIn($relationField, $value); $q->whereIn($relationField, $value);
} else if (is_string($value) && str_contains($value, ':')) { } else if (is_string($value) && str_contains($value, ':')) {
@@ -163,7 +163,8 @@ class UserController extends Controller
$users = $userModel->orderBy('id', 'desc') $users = $userModel->orderBy('id', 'desc')
->paginate($pageSize, ['*'], 'page', $current); ->paginate($pageSize, ['*'], 'page', $current);
$users->getCollection()->transform(function ($user) { /** @phpstan-ignore-next-line */
$users->getCollection()->transform(function ($user): array {
return self::transformUserData($user); return self::transformUserData($user);
}); });
@@ -177,13 +178,14 @@ class UserController extends Controller
* Transform user data for response * Transform user data for response
* *
* @param User $user * @param User $user
* @return User * @return array<string, mixed>
*/ */
public static function transformUserData(User $user): User public static function transformUserData(User $user): array
{ {
$user->subscribe_url = Helper::getSubscribeUrl($user->token); $user = $user->toArray();
$user->balance = $user->balance / 100; $user['balance'] = $user['balance'] / 100;
$user->commission_balance = $user->commission_balance / 100; $user['commission_balance'] = $user['commission_balance'] / 100;
$user['subscribe_url'] = Helper::getSubscribeUrl($user['token']);
return $user; return $user;
} }
@@ -235,7 +237,7 @@ class UserController extends Controller
if (isset($params['banned']) && (int) $params['banned'] === 1) { if (isset($params['banned']) && (int) $params['banned'] === 1) {
$authService = new AuthService($user); $authService = new AuthService($user);
$authService->removeSession(); $authService->removeAllSessions();
} }
if (isset($params['balance'])) { if (isset($params['balance'])) {
$params['balance'] = $params['balance'] * 100; $params['balance'] = $params['balance'] * 100;
@@ -263,7 +265,7 @@ class UserController extends Controller
{ {
ini_set('memory_limit', '-1'); ini_set('memory_limit', '-1');
gc_enable(); // 启用垃圾回收 gc_enable(); // 启用垃圾回收
// 优化查询使用with预加载plan关系避免N+1问题 // 优化查询使用with预加载plan关系避免N+1问题
$query = User::with('plan:id,name') $query = User::with('plan:id,name')
->orderBy('id', 'asc') ->orderBy('id', 'asc')
@@ -278,18 +280,18 @@ class UserController extends Controller
'token', 'token',
'plan_id' 'plan_id'
]); ]);
$this->applyFiltersAndSorts($request, $query); $this->applyFiltersAndSorts($request, $query);
$filename = 'users_' . date('Y-m-d_His') . '.csv'; $filename = 'users_' . date('Y-m-d_His') . '.csv';
return response()->streamDownload(function() use ($query) { return response()->streamDownload(function () use ($query) {
// 打开输出流 // 打开输出流
$output = fopen('php://output', 'w'); $output = fopen('php://output', 'w');
// 添加BOM标记确保Excel正确显示中文 // 添加BOM标记确保Excel正确显示中文
fprintf($output, chr(0xEF).chr(0xBB).chr(0xBF)); fprintf($output, chr(0xEF) . chr(0xBB) . chr(0xBF));
// 写入CSV头部 // 写入CSV头部
fputcsv($output, [ fputcsv($output, [
'邮箱', '邮箱',
@@ -301,9 +303,9 @@ class UserController extends Controller
'订阅计划', '订阅计划',
'订阅地址' '订阅地址'
]); ]);
// 分批处理数据以减少内存使用 // 分批处理数据以减少内存使用
$query->chunk(500, function($users) use ($output) { $query->chunk(500, function ($users) use ($output) {
foreach ($users as $user) { foreach ($users as $user) {
try { try {
$row = [ $row = [
@@ -325,11 +327,11 @@ class UserController extends Controller
continue; // 继续处理下一条记录 continue; // 继续处理下一条记录
} }
} }
// 清理内存 // 清理内存
gc_collect_cycles(); gc_collect_cycles();
}); });
fclose($output); fclose($output);
}, $filename, [ }, $filename, [
'Content-Type' => 'text/csv; charset=UTF-8', 'Content-Type' => 'text/csv; charset=UTF-8',

View File

@@ -11,7 +11,7 @@ class Kernel extends HttpKernel
* *
* These middleware are run during every request to your application. * These middleware are run during every request to your application.
* *
* @var array * @var array<int, class-string>
*/ */
protected $middleware = [ protected $middleware = [
\Illuminate\Http\Middleware\HandleCors::class, \Illuminate\Http\Middleware\HandleCors::class,
@@ -26,7 +26,7 @@ class Kernel extends HttpKernel
/** /**
* The application's route middleware groups. * The application's route middleware groups.
* *
* @var array * @var array<string, array<int, class-string|string>>
*/ */
protected $middlewareGroups = [ protected $middlewareGroups = [
'web' => [ 'web' => [
@@ -57,7 +57,7 @@ class Kernel extends HttpKernel
* *
* These middleware may be assigned to groups or used individually. * These middleware may be assigned to groups or used individually.
* *
* @var array * @var array<string, class-string>
*/ */
protected $middlewareAliases = [ protected $middlewareAliases = [
'auth' => \App\Http\Middleware\Authenticate::class, 'auth' => \App\Http\Middleware\Authenticate::class,
@@ -84,7 +84,7 @@ class Kernel extends HttpKernel
* *
* This forces non-global middleware to always be in the given order. * This forces non-global middleware to always be in the given order.
* *
* @var array * @var array<class-string>
*/ */
protected $middlewarePriority = [ protected $middlewarePriority = [
\Illuminate\Session\Middleware\StartSession::class, \Illuminate\Session\Middleware\StartSession::class,

View File

@@ -5,6 +5,7 @@ namespace App\Http\Middleware;
use App\Exceptions\ApiException; use App\Exceptions\ApiException;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Closure; use Closure;
use App\Models\User;
class Admin class Admin
{ {
@@ -17,15 +18,13 @@ class Admin
*/ */
public function handle($request, Closure $next) public function handle($request, Closure $next)
{ {
if (!Auth::guard('sanctum')->check()) { /** @var User|null $user */
throw new ApiException('未登录或登陆已过期', 403);
}
$user = Auth::guard('sanctum')->user(); $user = Auth::guard('sanctum')->user();
if (!$user->is_admin) {
throw new ApiException('无管理员权限', 403); if (!$user || !$user->is_admin) {
return response()->json(['message' => 'Unauthorized'], 403);
} }
return $next($request); return $next($request);
} }
} }

View File

@@ -2,16 +2,17 @@
namespace App\Http\Middleware; namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode as Middleware; use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance;
class CheckForMaintenanceMode extends Middleware class CheckForMaintenanceMode extends PreventRequestsDuringMaintenance
{ {
/** /**
* The URIs that should be reachable while maintenance mode is enabled. * 维护模式白名单URI
* * @var array<int, string>
* @var array
*/ */
protected $except = [ protected $except = [
// // 示例:
// '/api/health-check',
// '/status'
]; ];
} }

View File

@@ -7,9 +7,8 @@ use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
class EncryptCookies extends Middleware class EncryptCookies extends Middleware
{ {
/** /**
* The names of the cookies that should not be encrypted. * 不需要加密的Cookie名称列表
* * @var array<int, string>
* @var array
*/ */
protected $except = [ protected $except = [
// //

View File

@@ -7,12 +7,13 @@ use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
class TrimStrings extends Middleware class TrimStrings extends Middleware
{ {
/** /**
* The names of the attributes that should not be trimmed. * 不需要去除前后空格的字段名
* * @var array<int, string>
* @var array
*/ */
protected $except = [ protected $except = [
'password', 'password',
'password_confirmation', 'password_confirmation',
'encrypted_data',
'signature'
]; ];
} }

View File

@@ -8,9 +8,8 @@ use Illuminate\Http\Request;
class TrustProxies extends Middleware class TrustProxies extends Middleware
{ {
/** /**
* The trusted proxies for this application. * 可信代理列表
* * @var array<int, string>|string|null
* @var array|string
*/ */
protected $proxies = [ protected $proxies = [
"173.245.48.0/20", "173.245.48.0/20",
@@ -36,8 +35,7 @@ class TrustProxies extends Middleware
]; ];
/** /**
* The headers that should be used to detect proxies. * 代理头映射
*
* @var int * @var int
*/ */
protected $headers = protected $headers =

View File

@@ -7,16 +7,14 @@ use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware class VerifyCsrfToken extends Middleware
{ {
/** /**
* Indicates whether the XSRF-TOKEN cookie should be set on the response. * 是否在响应中设置XSRF-TOKEN cookie
*
* @var bool * @var bool
*/ */
protected $addHttpCookie = true; protected $addHttpCookie = true;
/** /**
* The URIs that should be excluded from CSRF verification. * 不需要CSRF验证的URI列表
* * @var array<int, string>
* @var array
*/ */
protected $except = [ protected $except = [
// //

View File

@@ -54,7 +54,19 @@ class ServerSave extends FormRequest
'reality_settings.short_id' => 'nullable|string', 'reality_settings.short_id' => 'nullable|string',
], ],
'socks' => [ 'socks' => [
] ],
'naive' => [
'tls' => 'required|integer',
'tls_settings' => 'nullable|array',
],
'http' => [
'tls' => 'required|integer',
'tls_settings' => 'nullable|array',
],
'mieru' => [
'transport' => 'required|string',
'multiplexing' => 'required|string',
],
]; ];
private function getBaseRules(): array private function getBaseRules(): array

View File

@@ -2,10 +2,14 @@
namespace App\Http\Resources; namespace App\Http\Resources;
use App\Models\Order;
use App\Services\PlanService; use App\Services\PlanService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;
/**
* @mixin Order
*/
class OrderResource extends JsonResource class OrderResource extends JsonResource
{ {
/** /**
@@ -17,8 +21,8 @@ class OrderResource extends JsonResource
{ {
return [ return [
...parent::toArray($request), ...parent::toArray($request),
'period' => PlanService::getLegacyPeriod($this->period), 'period' => PlanService::getLegacyPeriod((string)$this->period),
'plan' => PlanResource::make($this->plan), 'plan' => $this->whenLoaded('plan', fn() => PlanResource::make($this->plan)),
]; ];
} }
} }

View File

@@ -1,6 +1,8 @@
<?php <?php
namespace App\Http\Routes\V1; namespace App\Http\Routes\V1;
use App\Http\Controllers\V1\Client\AppController;
use App\Http\Controllers\V1\Client\ClientController;
use Illuminate\Contracts\Routing\Registrar; use Illuminate\Contracts\Routing\Registrar;
class ClientRoute class ClientRoute
@@ -12,10 +14,10 @@ class ClientRoute
'middleware' => 'client' 'middleware' => 'client'
], function ($router) { ], function ($router) {
// Client // Client
$router->get('/subscribe', 'V1\\Client\\ClientController@subscribe')->name('client.subscribe.legacy'); $router->get('/subscribe', [ClientController::class, 'subscribe'])->name('client.subscribe.legacy');
// App // App
$router->get('/app/getConfig', 'V1\\Client\\AppController@getConfig'); $router->get('/app/getConfig', [AppController::class, 'getConfig']);
$router->get('/app/getVersion', 'V1\\Client\\AppController@getVersion'); $router->get('/app/getVersion', [AppController::class, 'getVersion']);
}); });
} }
} }

View File

@@ -1,6 +1,8 @@
<?php <?php
namespace App\Http\Routes\V1; namespace App\Http\Routes\V1;
use App\Http\Controllers\V1\Passport\AuthController;
use App\Http\Controllers\V1\Passport\CommController;
use Illuminate\Contracts\Routing\Registrar; use Illuminate\Contracts\Routing\Registrar;
class PassportRoute class PassportRoute
@@ -11,15 +13,15 @@ class PassportRoute
'prefix' => 'passport' 'prefix' => 'passport'
], function ($router) { ], function ($router) {
// Auth // Auth
$router->post('/auth/register', 'V1\\Passport\\AuthController@register'); $router->post('/auth/register', [AuthController::class, 'register']);
$router->post('/auth/login', 'V1\\Passport\\AuthController@login'); $router->post('/auth/login', [AuthController::class, 'login']);
$router->get ('/auth/token2Login', 'V1\\Passport\\AuthController@token2Login'); $router->get('/auth/token2Login', [AuthController::class, 'token2Login']);
$router->post('/auth/forget', 'V1\\Passport\\AuthController@forget'); $router->post('/auth/forget', [AuthController::class, 'forget']);
$router->post('/auth/getQuickLoginUrl', 'V1\\Passport\\AuthController@getQuickLoginUrl'); $router->post('/auth/getQuickLoginUrl', [AuthController::class, 'getQuickLoginUrl']);
$router->post('/auth/loginWithMailLink', 'V1\\Passport\\AuthController@loginWithMailLink'); $router->post('/auth/loginWithMailLink', [AuthController::class, 'loginWithMailLink']);
// Comm // Comm
$router->post('/comm/sendEmailVerify', 'V1\\Passport\\CommController@sendEmailVerify'); $router->post('/comm/sendEmailVerify', [CommController::class, 'sendEmailVerify']);
$router->post('/comm/pv', 'V1\\Passport\\CommController@pv'); $router->post('/comm/pv', [CommController::class, 'pv']);
}); });
} }
} }

View File

@@ -1,32 +0,0 @@
<?php
namespace App\Http\Routes\V1;
use Illuminate\Contracts\Routing\Registrar;
class StaffRoute
{
public function map(Registrar $router)
{
$router->group([
'prefix' => 'staff',
'middleware' => 'staff'
], function ($router) {
// Ticket
// $router->get ('/ticket/fetch', 'V1\\Staff\\TicketController@fetch');
// $router->post('/ticket/reply', 'V1\\Staff\\TicketController@reply');
// $router->post('/ticket/close', 'V1\\Staff\\TicketController@close');
// // User
// $router->post('/user/update', 'V1\\Staff\\UserController@update');
// $router->get ('/user/getUserInfoById', 'V1\\Staff\\UserController@getUserInfoById');
// $router->post('/user/sendMail', 'V1\\Staff\\UserController@sendMail');
// $router->post('/user/ban', 'V1\\Staff\\UserController@ban');
// // Plan
// $router->get ('/plan/fetch', 'V1\\Staff\\PlanController@fetch');
// // Notice
// $router->get ('/notice/fetch', 'V1\\Admin\\NoticeController@fetch');
// $router->post('/notice/save', 'V1\\Admin\\NoticeController@save');
// $router->post('/notice/update', 'V1\\Admin\\NoticeController@update');
// $router->post('/notice/drop', 'V1\\Admin\\NoticeController@drop');
});
}
}

View File

@@ -1,6 +1,18 @@
<?php <?php
namespace App\Http\Routes\V1; namespace App\Http\Routes\V1;
use App\Http\Controllers\V1\User\CommController;
use App\Http\Controllers\V1\User\CouponController;
use App\Http\Controllers\V1\User\InviteController;
use App\Http\Controllers\V1\User\KnowledgeController;
use App\Http\Controllers\V1\User\NoticeController;
use App\Http\Controllers\V1\User\OrderController;
use App\Http\Controllers\V1\User\PlanController;
use App\Http\Controllers\V1\User\ServerController;
use App\Http\Controllers\V1\User\StatController;
use App\Http\Controllers\V1\User\TelegramController;
use App\Http\Controllers\V1\User\TicketController;
use App\Http\Controllers\V1\User\UserController;
use Illuminate\Contracts\Routing\Registrar; use Illuminate\Contracts\Routing\Registrar;
class UserRoute class UserRoute
@@ -12,53 +24,53 @@ class UserRoute
'middleware' => 'user' 'middleware' => 'user'
], function ($router) { ], function ($router) {
// User // User
$router->get ('/resetSecurity', 'V1\\User\\UserController@resetSecurity'); $router->get('/resetSecurity', [UserController::class, 'resetSecurity']);
$router->get ('/info', 'V1\\User\\UserController@info'); $router->get('/info', [UserController::class, 'info']);
$router->post('/changePassword', 'V1\\User\\UserController@changePassword'); $router->post('/changePassword', [UserController::class, 'changePassword']);
$router->post('/update', 'V1\\User\\UserController@update'); $router->post('/update', [UserController::class, 'update']);
$router->get ('/getSubscribe', 'V1\\User\\UserController@getSubscribe'); $router->get('/getSubscribe', [UserController::class, 'getSubscribe']);
$router->get ('/getStat', 'V1\\User\\UserController@getStat'); $router->get('/getStat', [UserController::class, 'getStat']);
$router->get ('/checkLogin', 'V1\\User\\UserController@checkLogin'); $router->get('/checkLogin', [UserController::class, 'checkLogin']);
$router->post('/transfer', 'V1\\User\\UserController@transfer'); $router->post('/transfer', [UserController::class, 'transfer']);
$router->post('/getQuickLoginUrl', 'V1\\User\\UserController@getQuickLoginUrl'); $router->post('/getQuickLoginUrl', [UserController::class, 'getQuickLoginUrl']);
$router->get ('/getActiveSession', 'V1\\User\\UserController@getActiveSession'); $router->get('/getActiveSession', [UserController::class, 'getActiveSession']);
$router->post('/removeActiveSession', 'V1\\User\\UserController@removeActiveSession'); $router->post('/removeActiveSession', [UserController::class, 'removeActiveSession']);
// Order // Order
$router->post('/order/save', 'V1\\User\\OrderController@save'); $router->post('/order/save', [OrderController::class, 'save']);
$router->post('/order/checkout', 'V1\\User\\OrderController@checkout'); $router->post('/order/checkout', [OrderController::class, 'checkout']);
$router->get ('/order/check', 'V1\\User\\OrderController@check'); $router->get('/order/check', [OrderController::class, 'check']);
$router->get ('/order/detail', 'V1\\User\\OrderController@detail'); $router->get('/order/detail', [OrderController::class, 'detail']);
$router->get ('/order/fetch', 'V1\\User\\OrderController@fetch'); $router->get('/order/fetch', [OrderController::class, 'fetch']);
$router->get ('/order/getPaymentMethod', 'V1\\User\\OrderController@getPaymentMethod'); $router->get('/order/getPaymentMethod', [OrderController::class, 'getPaymentMethod']);
$router->post('/order/cancel', 'V1\\User\\OrderController@cancel'); $router->post('/order/cancel', [OrderController::class, 'cancel']);
// Plan // Plan
$router->get ('/plan/fetch', 'V1\\User\\PlanController@fetch'); $router->get('/plan/fetch', [PlanController::class, 'fetch']);
// Invite // Invite
$router->get ('/invite/save', 'V1\\User\\InviteController@save'); $router->get('/invite/save', [InviteController::class, 'save']);
$router->get ('/invite/fetch', 'V1\\User\\InviteController@fetch'); $router->get('/invite/fetch', [InviteController::class, 'fetch']);
$router->get ('/invite/details', 'V1\\User\\InviteController@details'); $router->get('/invite/details', [InviteController::class, 'details']);
// Notice // Notice
$router->get ('/notice/fetch', 'V1\\User\\NoticeController@fetch'); $router->get('/notice/fetch', [NoticeController::class, 'fetch']);
// Ticket // Ticket
$router->post('/ticket/reply', 'V1\\User\\TicketController@reply'); $router->post('/ticket/reply', [TicketController::class, 'reply']);
$router->post('/ticket/close', 'V1\\User\\TicketController@close'); $router->post('/ticket/close', [TicketController::class, 'close']);
$router->post('/ticket/save', 'V1\\User\\TicketController@save'); $router->post('/ticket/save', [TicketController::class, 'save']);
$router->get ('/ticket/fetch', 'V1\\User\\TicketController@fetch'); $router->get('/ticket/fetch', [TicketController::class, 'fetch']);
$router->post('/ticket/withdraw', 'V1\\User\\TicketController@withdraw'); $router->post('/ticket/withdraw', [TicketController::class, 'withdraw']);
// Server // Server
$router->get ('/server/fetch', 'V1\\User\\ServerController@fetch'); $router->get('/server/fetch', [ServerController::class, 'fetch']);
// Coupon // Coupon
$router->post('/coupon/check', 'V1\\User\\CouponController@check'); $router->post('/coupon/check', [CouponController::class, 'check']);
// Telegram // Telegram
$router->get ('/telegram/getBotInfo', 'V1\\User\\TelegramController@getBotInfo'); $router->get('/telegram/getBotInfo', [TelegramController::class, 'getBotInfo']);
// Comm // Comm
$router->get ('/comm/config', 'V1\\User\\CommController@config'); $router->get('/comm/config', [CommController::class, 'config']);
$router->Post('/comm/getStripePublicKey', 'V1\\User\\CommController@getStripePublicKey'); $router->Post('/comm/getStripePublicKey', [CommController::class, 'getStripePublicKey']);
// Knowledge // Knowledge
$router->get ('/knowledge/fetch', 'V1\\User\\KnowledgeController@fetch'); $router->get('/knowledge/fetch', [KnowledgeController::class, 'fetch']);
$router->get ('/knowledge/getCategory', 'V1\\User\\KnowledgeController@getCategory'); $router->get('/knowledge/getCategory', [KnowledgeController::class, 'getCategory']);
// Stat // Stat
$router->get ('/stat/getTrafficLog', 'V1\\User\\StatController@getTrafficLog'); $router->get('/stat/getTrafficLog', [StatController::class, 'getTrafficLog']);
}); });
} }
} }

View File

@@ -1,6 +1,8 @@
<?php <?php
namespace App\Http\Routes\V2; namespace App\Http\Routes\V2;
use App\Http\Controllers\V1\Passport\AuthController;
use App\Http\Controllers\V1\Passport\CommController;
use Illuminate\Contracts\Routing\Registrar; use Illuminate\Contracts\Routing\Registrar;
class PassportRoute class PassportRoute
@@ -11,15 +13,15 @@ class PassportRoute
'prefix' => 'passport' 'prefix' => 'passport'
], function ($router) { ], function ($router) {
// Auth // Auth
$router->post('/auth/register', 'V1\\Passport\\AuthController@register'); $router->post('/auth/register', [AuthController::class, 'register']);
$router->post('/auth/login', 'V1\\Passport\\AuthController@login'); $router->post('/auth/login', [AuthController::class, 'login']);
$router->get ('/auth/token2Login', 'V1\\Passport\\AuthController@token2Login'); $router->get ('/auth/token2Login', [AuthController::class, 'token2Login']);
$router->post('/auth/forget', 'V1\\Passport\\AuthController@forget'); $router->post('/auth/forget', [AuthController::class, 'forget']);
$router->post('/auth/getQuickLoginUrl', 'V1\\Passport\\AuthController@getQuickLoginUrl'); $router->post('/auth/getQuickLoginUrl', [AuthController::class, 'getQuickLoginUrl']);
$router->post('/auth/loginWithMailLink', 'V1\\Passport\\AuthController@loginWithMailLink'); $router->post('/auth/loginWithMailLink', [AuthController::class, 'loginWithMailLink']);
// Comm // Comm
$router->post('/comm/sendEmailVerify', 'V1\\Passport\\CommController@sendEmailVerify'); $router->post('/comm/sendEmailVerify', [CommController::class, 'sendEmailVerify']);
$router->post('/comm/pv', 'V1\\Passport\\CommController@pv'); $router->post('/comm/pv', [CommController::class, 'pv']);
}); });
} }
} }

View File

@@ -1,6 +1,7 @@
<?php <?php
namespace App\Http\Routes\V2; namespace App\Http\Routes\V2;
use App\Http\Controllers\V1\User\UserController;
use Illuminate\Contracts\Routing\Registrar; use Illuminate\Contracts\Routing\Registrar;
class UserRoute class UserRoute
@@ -12,8 +13,8 @@ class UserRoute
'middleware' => 'user' 'middleware' => 'user'
], function ($router) { ], function ($router) {
// User // User
$router->get('/resetSecurity', 'V1\\User\\UserController@resetSecurity'); $router->get('/resetSecurity', [UserController::class, 'resetSecurity']);
$router->get('/info', 'V1\\User\\UserController@info'); $router->get('/info', [UserController::class, 'info']);
}); });
} }
} }

View File

@@ -17,28 +17,28 @@ class MysqlLoggerHandler extends AbstractProcessingHandler
protected function write(LogRecord $record): void protected function write(LogRecord $record): void
{ {
$record = $record->toArray(); $record = $record->toArray();
try{ try {
if(isset($record['context']['exception']) && is_object($record['context']['exception'])){ if (isset($record['context']['exception']) && is_object($record['context']['exception'])) {
$record['context']['exception'] = (array)$record['context']['exception']; $record['context']['exception'] = (array)$record['context']['exception'];
} }
$record['request_data'] = request()->all() ??[];
$record['request_data'] = request()->all();
$log = [ $log = [
'title' => $record['message'], 'title' => $record['message'],
'level' => $record['level_name'], 'level' => $record['level_name'],
'host' => $record['request_host'] ?? request()->getSchemeAndHttpHost(), 'host' => $record['extra']['request_host'] ?? request()->getSchemeAndHttpHost(),
'uri' => $record['request_uri'] ?? request()->getRequestUri(), 'uri' => $record['extra']['request_uri'] ?? request()->getRequestUri(),
'method' => $record['request_method'] ?? request()->getMethod(), 'method' => $record['extra']['request_method'] ?? request()->getMethod(),
'ip' => request()->getClientIp(), 'ip' => request()->getClientIp(),
'data' => json_encode($record['request_data']) , 'data' => json_encode($record['request_data']),
'context' => isset($record['context']) ? json_encode($record['context']) : '', 'context' => json_encode($record['context']),
'created_at' => $record['datetime']->getTimestamp(), 'created_at' => $record['datetime']->getTimestamp(),
'updated_at' => $record['datetime']->getTimestamp(), 'updated_at' => $record['datetime']->getTimestamp(),
]; ];
LogModel::insert( LogModel::insert($log);
$log } catch (\Exception $e) {
);
}catch (\Exception $e){
// Log::channel('daily')->error($e->getMessage().$e->getFile().$e->getTraceAsString()); // Log::channel('daily')->error($e->getMessage().$e->getFile().$e->getTraceAsString());
} }
} }

View File

@@ -3,7 +3,38 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* App\Models\Order
*
* @property int $id
* @property int $user_id
* @property int $plan_id
* @property int|null $payment_id
* @property int $period
* @property string $trade_no
* @property int $total_amount
* @property int|null $handling_amount
* @property int|null $balance_amount
* @property int $type
* @property int $status
* @property array|null $surplus_order_ids
* @property int|null $coupon_id
* @property int $created_at
* @property int $updated_at
* @property int|null $commission_status
* @property int|null $invite_user_id
* @property int|null $actual_commission_balance
* @property int|null $commission_rate
* @property int|null $commission_auto_check
*
* @property-read Plan $plan
* @property-read Payment|null $payment
* @property-read User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, CommissionLog> $commission_log
*/
class Order extends Model class Order extends Model
{ {
protected $table = 'v2_order'; protected $table = 'v2_order';
@@ -12,7 +43,8 @@ class Order extends Model
protected $casts = [ protected $casts = [
'created_at' => 'timestamp', 'created_at' => 'timestamp',
'updated_at' => 'timestamp', 'updated_at' => 'timestamp',
'surplus_order_ids' => 'array' 'surplus_order_ids' => 'array',
'handling_amount' => 'integer'
]; ];
const STATUS_PENDING = 0; // 待支付 const STATUS_PENDING = 0; // 待支付
@@ -40,21 +72,34 @@ class Order extends Model
self::TYPE_RESET_TRAFFIC => '流量重置', self::TYPE_RESET_TRAFFIC => '流量重置',
]; ];
public function payment() /**
* 获取与订单关联的支付方式
*/
public function payment(): BelongsTo
{ {
return $this->belongsTo(Payment::class, 'payment_id', 'id'); return $this->belongsTo(Payment::class, 'payment_id', 'id');
} }
public function user() /**
* 获取与订单关联的用户
*/
public function user(): BelongsTo
{ {
return $this->belongsTo(User::class, 'user_id', 'id'); return $this->belongsTo(User::class, 'user_id', 'id');
} }
public function plan()
/**
* 获取与订单关联的套餐
*/
public function plan(): BelongsTo
{ {
return $this->belongsTo(Plan::class); return $this->belongsTo(Plan::class, 'plan_id', 'id');
} }
public function commission_log() /**
* 获取与订单关联的佣金记录
*/
public function commission_log(): HasMany
{ {
return $this->hasMany(CommissionLog::class, 'trade_no', 'trade_no'); return $this->hasMany(CommissionLog::class, 'trade_no', 'trade_no');
} }

View File

@@ -12,6 +12,7 @@ class Payment extends Model
protected $casts = [ protected $casts = [
'created_at' => 'timestamp', 'created_at' => 'timestamp',
'updated_at' => 'timestamp', 'updated_at' => 'timestamp',
'config' => 'array' 'config' => 'array',
'enable' => 'boolean'
]; ];
} }

View File

@@ -7,7 +7,31 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use InvalidArgumentException; use InvalidArgumentException;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Database\Eloquent\Relations\HasOne;
/**
* App\Models\Plan
*
* @property int $id
* @property string $name 套餐名称
* @property int|null $group_id 权限组ID
* @property int $transfer_enable 流量(KB)
* @property int|null $speed_limit 速度限制Mbps
* @property bool $show 是否显示
* @property bool $renew 是否允许续费
* @property bool $sell 是否允许购买
* @property array|null $prices 价格配置
* @property int $sort 排序
* @property string|null $content 套餐描述
* @property int $reset_traffic_method 流量重置方式
* @property int|null $capacity_limit 订阅人数限制
* @property int|null $device_limit 设备数量限制
* @property int $created_at
* @property int $updated_at
*
* @property-read ServerGroup|null $group 关联的权限组
* @property-read \Illuminate\Database\Eloquent\Collection<int, Order> $order 关联的订单
*/
class Plan extends Model class Plan extends Model
{ {
use HasFactory; use HasFactory;
@@ -16,12 +40,12 @@ class Plan extends Model
protected $dateFormat = 'U'; protected $dateFormat = 'U';
// 定义流量重置方式 // 定义流量重置方式
public const RESET_TRAFFIC_FOLLOW_SYSTEM = 0; // 跟随系统设置 public const RESET_TRAFFIC_FOLLOW_SYSTEM = null; // 跟随系统设置
public const RESET_TRAFFIC_FIRST_DAY_MONTH = 1; // 每月1号 public const RESET_TRAFFIC_FIRST_DAY_MONTH = 0; // 每月1号
public const RESET_TRAFFIC_MONTHLY = 2; // 按月重置 public const RESET_TRAFFIC_MONTHLY = 1; // 按月重置
public const RESET_TRAFFIC_NEVER = 3; // 不重置 public const RESET_TRAFFIC_NEVER = 2; // 不重置
public const RESET_TRAFFIC_FIRST_DAY_YEAR = 4; // 每年1月1日 public const RESET_TRAFFIC_FIRST_DAY_YEAR = 3; // 每年1月1日
public const RESET_TRAFFIC_YEARLY = 5; // 按年重置 public const RESET_TRAFFIC_YEARLY = 4; // 按年重置
// 定义价格类型 // 定义价格类型
public const PRICE_TYPE_RESET_TRAFFIC = 'reset_traffic'; // 重置流量价格 public const PRICE_TYPE_RESET_TRAFFIC = 'reset_traffic'; // 重置流量价格
@@ -346,7 +370,7 @@ class Plan extends Model
return $this->hasMany(User::class); return $this->hasMany(User::class);
} }
public function group() public function group(): HasOne
{ {
return $this->hasOne(ServerGroup::class, 'id', 'group_id'); return $this->hasOne(ServerGroup::class, 'id', 'group_id');
} }
@@ -384,4 +408,9 @@ class Plan extends Model
$prices[self::PRICE_TYPE_RESET_TRAFFIC] = max(0, $price); $prices[self::PRICE_TYPE_RESET_TRAFFIC] = max(0, $price);
$this->prices = $prices; $this->prices = $prices;
} }
public function order(): HasMany
{
return $this->hasMany(Order::class);
}
} }

View File

@@ -9,7 +9,45 @@ use Illuminate\Support\Facades\Cache;
use App\Utils\CacheKey; use App\Utils\CacheKey;
use App\Utils\Helper; use App\Utils\Helper;
use App\Models\User; use App\Models\User;
use Illuminate\Database\Eloquent\Casts\Attribute;
/**
* App\Models\Server
*
* @property int $id
* @property string $name 节点名称
* @property string $type 服务类型
* @property string $host 主机地址
* @property string $port 端口
* @property string|null $server_port 服务器端口
* @property array|null $group_ids 分组IDs
* @property array|null $route_ids 路由IDs
* @property array|null $tags 标签
* @property string|null $show 是否显示
* @property string|null $allow_insecure 是否允许不安全
* @property string|null $network 网络类型
* @property int|null $parent_id 父节点ID
* @property float|null $rate 倍率
* @property int|null $sort 排序
* @property array|null $protocol_settings 协议设置
* @property int $created_at
* @property int $updated_at
*
* @property-read Server|null $parent 父节点
* @property-read \Illuminate\Database\Eloquent\Collection<int, StatServer> $stats 节点统计
*
* @property-read int|null $last_check_at 最后检查时间Unix时间戳
* @property-read int|null $last_push_at 最后推送时间Unix时间戳
* @property-read int $online 在线用户数
* @property-read int $is_online 是否在线1在线 0离线
* @property-read string $available_status 可用状态描述
* @property-read string $cache_key 缓存键
* @property string|null $ports 端口范围
* @property string|null $password 密码
* @property int|null $u 上行流量
* @property int|null $d 下行流量
* @property int|null $total 总流量
*/
class Server extends Model class Server extends Model
{ {
public const TYPE_HYSTERIA = 'hysteria'; public const TYPE_HYSTERIA = 'hysteria';
@@ -19,6 +57,9 @@ class Server extends Model
public const TYPE_TUIC = 'tuic'; public const TYPE_TUIC = 'tuic';
public const TYPE_SHADOWSOCKS = 'shadowsocks'; public const TYPE_SHADOWSOCKS = 'shadowsocks';
public const TYPE_SOCKS = 'socks'; public const TYPE_SOCKS = 'socks';
public const TYPE_NAIVE = 'naive';
public const TYPE_HTTP = 'http';
public const TYPE_MIERU = 'mieru';
public const STATUS_OFFLINE = 0; public const STATUS_OFFLINE = 0;
public const STATUS_ONLINE_NO_PUSH = 1; public const STATUS_ONLINE_NO_PUSH = 1;
public const STATUS_ONLINE = 2; public const STATUS_ONLINE = 2;
@@ -53,6 +94,9 @@ class Server extends Model
self::TYPE_TUIC, self::TYPE_TUIC,
self::TYPE_SHADOWSOCKS, self::TYPE_SHADOWSOCKS,
self::TYPE_SOCKS, self::TYPE_SOCKS,
self::TYPE_NAIVE,
self::TYPE_HTTP,
self::TYPE_MIERU,
]; ];
protected $table = 'v2_server'; protected $table = 'v2_server';
@@ -143,6 +187,32 @@ class Server extends Model
'allow_insecure' => ['type' => 'boolean', 'default' => false] 'allow_insecure' => ['type' => 'boolean', 'default' => false]
] ]
] ]
],
self::TYPE_SOCKS => [
'tls' => ['type' => 'integer', 'default' => 0],
'tls_settings' => [
'type' => 'object',
'fields' => [
'allow_insecure' => ['type' => 'boolean', 'default' => false]
]
]
],
self::TYPE_NAIVE => [
'tls' => ['type' => 'integer', 'default' => 0],
'tls_settings' => ['type' => 'array', 'default' => null]
],
self::TYPE_HTTP => [
'tls' => ['type' => 'integer', 'default' => 0],
'tls_settings' => [
'type' => 'object',
'fields' => [
'allow_insecure' => ['type' => 'boolean', 'default' => false]
]
]
],
self::TYPE_MIERU => [
'transport' => ['type' => 'string', 'default' => 'tcp'],
'multiplexing' => ['type' => 'string', 'default' => 'MULTIPLEXING_LOW']
] ]
]; ];
@@ -174,19 +244,6 @@ class Server extends Model
return $result; return $result;
} }
private function getDefaultSettings(array $configs): array
{
$defaults = [];
foreach ($configs as $key => $config) {
if ($config['type'] === 'object') {
$defaults[$key] = $this->getDefaultSettings($config['fields']);
} else {
$defaults[$key] = $config['default'];
}
}
return $defaults;
}
public function getProtocolSettingsAttribute($value) public function getProtocolSettingsAttribute($value)
{ {
$settings = json_decode($value, true) ?? []; $settings = json_decode($value, true) ?? [];
@@ -206,53 +263,22 @@ class Server extends Model
$this->attributes['protocol_settings'] = json_encode($castedSettings); $this->attributes['protocol_settings'] = json_encode($castedSettings);
} }
public function loadParentCreatedAt(): void public function generateShadowsocksPassword(User $user): string
{
if ($this->parent_id) {
$this->created_at = $this->parent()->value('created_at');
}
}
public function loadServerStatus(): void
{
$type = strtoupper($this->type);
$serverId = $this->parent_id ?: $this->id;
$this->last_check_at = Cache::get(CacheKey::get("SERVER_{$type}_LAST_CHECK_AT", $serverId));
$this->last_push_at = Cache::get(CacheKey::get("SERVER_{$type}_LAST_PUSH_AT", $serverId));
$this->online = Cache::get(CacheKey::get("SERVER_{$type}_ONLINE_USER", $serverId)) ?? 0;
$this->is_online = (time() - 300 > $this->last_check_at) ? 0 : 1;
$this->available_status = $this->getAvailableStatus();
$this->cache_key = "{$this->type}-{$this->id}-{$this->updated_at}-{$this->is_online}";
}
public function handlePortAllocation(): void
{
if (strpos($this->port, '-') !== false) {
$this->ports = $this->port;
$this->port = Helper::randomPort($this->port);
} else {
$this->port = (int) $this->port;
}
}
public function generateShadowsocksPassword(User $user): void
{ {
if ($this->type !== self::TYPE_SHADOWSOCKS) { if ($this->type !== self::TYPE_SHADOWSOCKS) {
return; return $user->uuid;
} }
$this->password = $user->uuid;
$cipher = data_get($this, 'protocol_settings.cipher'); $cipher = data_get($this, 'protocol_settings.cipher');
if (!$cipher || !isset(self::CIPHER_CONFIGURATIONS[$cipher])) { if (!$cipher || !isset(self::CIPHER_CONFIGURATIONS[$cipher])) {
return; return $user->uuid;
} }
$config = self::CIPHER_CONFIGURATIONS[$cipher]; $config = self::CIPHER_CONFIGURATIONS[$cipher];
$serverKey = Helper::getServerKey($this->created_at, $config['serverKeySize']); $serverKey = Helper::getServerKey($this->created_at, $config['serverKeySize']);
$userKey = Helper::uuidToBase64($user->uuid, $config['userKeySize']); $userKey = Helper::uuidToBase64($user->uuid, $config['userKeySize']);
$this->password = "{$serverKey}:{$userKey}"; return "{$serverKey}:{$userKey}";
} }
public static function normalizeType(string $type): string public static function normalizeType(string $type): string
@@ -265,7 +291,7 @@ class Server extends Model
return in_array(self::normalizeType($type), self::VALID_TYPES, true); return in_array(self::normalizeType($type), self::VALID_TYPES, true);
} }
public function getAvailableStatus(): int public function getAvailableStatusAttribute(): int
{ {
$now = time(); $now = time();
if (!$this->last_check_at || ($now - self::CHECK_INTERVAL) >= $this->last_check_at) { if (!$this->last_check_at || ($now - self::CHECK_INTERVAL) >= $this->last_check_at) {
@@ -297,4 +323,84 @@ class Server extends Model
return ServerRoute::whereIn('id', $this->route_ids)->get(); return ServerRoute::whereIn('id', $this->route_ids)->get();
} }
/**
* 最后检查时间访问器
*/
protected function lastCheckAt(): Attribute
{
return Attribute::make(
get: function () {
$type = strtoupper($this->type);
$serverId = $this->parent_id ?: $this->id;
return Cache::get(CacheKey::get("SERVER_{$type}_LAST_CHECK_AT", $serverId));
}
);
}
/**
* 最后推送时间访问器
*/
protected function lastPushAt(): Attribute
{
return Attribute::make(
get: function () {
$type = strtoupper($this->type);
$serverId = $this->parent_id ?: $this->id;
return Cache::get(CacheKey::get("SERVER_{$type}_LAST_PUSH_AT", $serverId));
}
);
}
/**
* 在线用户数访问器
*/
protected function online(): Attribute
{
return Attribute::make(
get: function () {
$type = strtoupper($this->type);
$serverId = $this->parent_id ?: $this->id;
return Cache::get(CacheKey::get("SERVER_{$type}_ONLINE_USER", $serverId)) ?? 0;
}
);
}
/**
* 是否在线访问器
*/
protected function isOnline(): Attribute
{
return Attribute::make(
get: function () {
return (time() - 300 > $this->last_check_at) ? 0 : 1;
}
);
}
/**
* 缓存键访问器
*/
protected function cacheKey(): Attribute
{
return Attribute::make(
get: function () {
return "{$this->type}-{$this->id}-{$this->updated_at}-{$this->is_online}";
}
);
}
/**
* 服务器密钥访问器
*/
protected function serverKey(): Attribute
{
return Attribute::make(
get: function () {
if ($this->type === self::TYPE_SHADOWSOCKS) {
return Helper::getServerKey($this->created_at, 16);
}
return null;
}
);
}
} }

View File

@@ -4,7 +4,17 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Casts\Attribute;
/**
* App\Models\ServerGroup
*
* @property int $id
* @property string $name 分组名
* @property int $created_at
* @property int $updated_at
* @property-read int $server_count 服务器数量
*/
class ServerGroup extends Model class ServerGroup extends Model
{ {
protected $table = 'v2_server_group'; protected $table = 'v2_server_group';
@@ -23,4 +33,14 @@ class ServerGroup extends Model
{ {
return Server::whereJsonContains('group_ids', (string) $this->id)->get(); return Server::whereJsonContains('group_ids', (string) $this->id)->get();
} }
/**
* 获取服务器数量
*/
protected function serverCount(): Attribute
{
return Attribute::make(
get: fn () => Server::whereJsonContains('group_ids', (string) $this->id)->count(),
);
}
} }

View File

@@ -4,6 +4,18 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
/**
* App\Models\StatServer
*
* @property int $id
* @property int $server_id 服务器ID
* @property int $u 上行流量
* @property int $d 下行流量
* @property int $record_at 记录时间
* @property int $created_at
* @property int $updated_at
* @property-read int $value 通过SUM(u + d)计算的总流量值,仅在查询指定时可用
*/
class StatServer extends Model class StatServer extends Model
{ {
protected $table = 'v2_stat_server'; protected $table = 'v2_stat_server';

View File

@@ -4,6 +4,18 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
/**
* App\Models\StatUser
*
* @property int $id
* @property int $user_id 用户ID
* @property int $u 上行流量
* @property int $d 下行流量
* @property int $record_at 记录时间
* @property int $created_at
* @property int $updated_at
* @property-read int $value 通过SUM(u + d)计算的总流量值,仅在查询指定时可用
*/
class StatUser extends Model class StatUser extends Model
{ {
protected $table = 'v2_stat_user'; protected $table = 'v2_stat_user';

View File

@@ -3,7 +3,25 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* App\Models\Ticket
*
* @property int $id
* @property int $user_id 用户ID
* @property string $subject 工单主题
* @property string|null $level 工单等级
* @property int $status 工单状态
* @property int|null $reply_status 回复状态
* @property int|null $last_reply_user_id 最后回复人
* @property int $created_at
* @property int $updated_at
*
* @property-read User $user 关联的用户
* @property-read \Illuminate\Database\Eloquent\Collection<int, TicketMessage> $messages 关联的工单消息
*/
class Ticket extends Model class Ticket extends Model
{ {
protected $table = 'v2_ticket'; protected $table = 'v2_ticket';
@@ -21,16 +39,21 @@ class Ticket extends Model
self::STATUS_CLOSED => '关闭' self::STATUS_CLOSED => '关闭'
]; ];
public function user() public function user(): BelongsTo
{ {
return $this->belongsTo(User::class, 'user_id', 'id'); return $this->belongsTo(User::class, 'user_id', 'id');
} }
public function messages()
/**
* 关联的工单消息
*/
public function messages(): HasMany
{ {
return $this->hasMany(TicketMessage::class, 'ticket_id', 'id'); return $this->hasMany(TicketMessage::class, 'ticket_id', 'id');
} }
// 即将删除 // 即将删除
public function message() public function message(): HasMany
{ {
return $this->hasMany(TicketMessage::class, 'ticket_id', 'id'); return $this->hasMany(TicketMessage::class, 'ticket_id', 'id');
} }

View File

@@ -3,7 +3,20 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* App\Models\TicketMessage
*
* @property int $id
* @property int $ticket_id
* @property int $user_id
* @property string $message
* @property \Illuminate\Support\Carbon $created_at
* @property \Illuminate\Support\Carbon $updated_at
* @property-read \App\Models\Ticket $ticket 关联的工单
* @property-read bool $is_me 当前消息是否由工单发起人发送
*/
class TicketMessage extends Model class TicketMessage extends Model
{ {
protected $table = 'v2_ticket_message'; protected $table = 'v2_ticket_message';
@@ -13,4 +26,22 @@ class TicketMessage extends Model
'created_at' => 'timestamp', 'created_at' => 'timestamp',
'updated_at' => 'timestamp' 'updated_at' => 'timestamp'
]; ];
protected $appends = ['is_me'];
/**
* 关联的工单
*/
public function ticket(): BelongsTo
{
return $this->belongsTo(Ticket::class, 'ticket_id', 'id');
}
/**
* 判断消息是否由工单发起人发送
*/
public function getIsMeAttribute(): bool
{
return $this->ticket->user_id === $this->user_id;
}
} }

View File

@@ -2,10 +2,55 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Model; use App\Utils\Helper;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Sanctum\HasApiTokens; use Laravel\Sanctum\HasApiTokens;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* App\Models\User
*
* @property int $id 用户ID
* @property string $email 邮箱
* @property string $password 密码
* @property string|null $password_algo 加密方式
* @property string|null $password_salt 加密盐
* @property string $token 邀请码
* @property string $uuid
* @property int|null $invite_user_id 邀请人
* @property int|null $plan_id 订阅ID
* @property int|null $group_id 权限组ID
* @property int|null $transfer_enable 流量(KB)
* @property int|null $speed_limit 限速Mbps
* @property int|null $u 上行流量
* @property int|null $d 下行流量
* @property int|null $banned 是否封禁
* @property int|null $remind_expire 到期提醒
* @property int|null $remind_traffic 流量提醒
* @property int|null $expired_at 过期时间
* @property int|null $balance 余额
* @property int|null $commission_balance 佣金余额
* @property float $commission_rate 返佣比例
* @property int|null $device_limit 设备限制数量
* @property int|null $discount 折扣
* @property int|null $last_login_at 最后登录时间
* @property int|null $parent_id 父账户ID
* @property int|null $is_admin 是否管理员
* @property int $created_at
* @property int $updated_at
* @property bool $commission_auto_check 是否自动计算佣金
*
* @property-read User|null $invite_user 邀请人信息
* @property-read \App\Models\Plan|null $plan 用户订阅计划
* @property-read ServerGroup|null $group 权限组
* @property-read \Illuminate\Database\Eloquent\Collection<int, InviteCode> $codes 邀请码列表
* @property-read \Illuminate\Database\Eloquent\Collection<int, Order> $orders 订单列表
* @property-read \Illuminate\Database\Eloquent\Collection<int, StatUser> $stat 统计信息
* @property-read \Illuminate\Database\Eloquent\Collection<int, Ticket> $tickets 工单列表
* @property-read User|null $parent 父账户
* @property-read string $subscribe_url 订阅链接(动态生成)
*/
class User extends Authenticatable class User extends Authenticatable
{ {
use HasApiTokens; use HasApiTokens;
@@ -14,52 +59,72 @@ class User extends Authenticatable
protected $guarded = ['id']; protected $guarded = ['id'];
protected $casts = [ protected $casts = [
'created_at' => 'timestamp', 'created_at' => 'timestamp',
'updated_at' => 'timestamp' 'updated_at' => 'timestamp',
'banned' => 'integer',
'remind_expire' => 'boolean',
'remind_traffic' => 'boolean',
'commission_auto_check' => 'boolean',
'commission_rate' => 'float'
]; ];
protected $hidden = ['password']; protected $hidden = ['password'];
public const COMMISSION_TYPE_SYSTEM = 0;
public const COMMISSION_TYPE_PERIOD = 1;
public const COMMISSION_TYPE_ONETIME = 2;
// 获取邀请人信息 // 获取邀请人信息
public function invite_user() public function invite_user(): BelongsTo
{ {
return $this->belongsTo(self::class, 'invite_user_id', 'id'); return $this->belongsTo(self::class, 'invite_user_id', 'id');
} }
// 获取用户套餐 /**
public function plan() * 获取用户订阅计划
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function plan(): BelongsTo
{ {
return $this->belongsTo(Plan::class, 'plan_id', 'id'); return $this->belongsTo(Plan::class, 'plan_id', 'id');
} }
public function group() public function group(): BelongsTo
{ {
return $this->belongsTo(ServerGroup::class, 'group_id', 'id'); return $this->belongsTo(ServerGroup::class, 'group_id', 'id');
} }
// 获取用户邀请码列表 // 获取用户邀请码列表
public function codes() public function codes(): HasMany
{ {
return $this->hasMany(InviteCode::class, 'user_id', 'id'); return $this->hasMany(InviteCode::class, 'user_id', 'id');
} }
public function orders() public function orders(): HasMany
{ {
return $this->hasMany(Order::class, 'user_id', 'id'); return $this->hasMany(Order::class, 'user_id', 'id');
} }
public function stat() public function stat(): HasMany
{ {
return $this->hasMany(StatUser::class, 'user_id', 'id'); return $this->hasMany(StatUser::class, 'user_id', 'id');
} }
// 关联工单列表 // 关联工单列表
public function tickets() public function tickets(): HasMany
{ {
return $this->hasMany(Ticket::class, 'user_id', 'id'); return $this->hasMany(Ticket::class, 'user_id', 'id');
} }
public function parent() public function parent(): BelongsTo
{ {
return $this->belongsTo(self::class, 'parent_id', 'id'); return $this->belongsTo(self::class, 'parent_id', 'id');
} }
/**
* 获取订阅链接属性
*/
public function getSubscribeUrlAttribute(): string
{
return Helper::getSubscribeUrl($this->token);
}
} }

View File

@@ -83,7 +83,6 @@ class BTCPay implements PaymentInterface
if (!self::hashEqual($signraturHeader, $computedSignature)) { if (!self::hashEqual($signraturHeader, $computedSignature)) {
throw new ApiException('HMAC signature does not match', 400); throw new ApiException('HMAC signature does not match', 400);
return false;
} }
//get order id store in metadata //get order id store in metadata
@@ -112,8 +111,8 @@ class BTCPay implements PaymentInterface
$ch = curl_init(); $ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, 0); curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 300); curl_setopt($ch, CURLOPT_TIMEOUT, 300);
curl_setopt($ch, CURLOPT_POSTFIELDS, $params); curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
curl_setopt( curl_setopt(

118
app/Payments/BinancePay.php Normal file
View File

@@ -0,0 +1,118 @@
<?php
namespace App\Payments;
use Illuminate\Support\Facades\Log;
class BinancePay
{
protected $config;
public function __construct($config)
{
$this->config = $config;
}
public function form()
{
return [
'api_key' => [
'label' => 'API Key',
'type' => 'input',
'description' => '请输入您的 Binance API Key'
],
'secret_key' => [
'label' => 'Secret Key',
'type' => 'input',
'description' => '请输入您的 Binance Secret Key'
]
];
}
public function pay($order)
{
$timestamp = intval(microtime(true) * 1000); // Timestamp in milliseconds
$nonceStr = bin2hex(random_bytes(16)); // Generating a nonce
$request = [
"env" => [
"terminalType" => "APP"
],
'merchantTradeNo' => strval($order['trade_no']),
'fiatCurrency' => 'CNY',
'fiatAmount' => ($order["total_amount"] / 100),
'supportPayCurrency' => "USDT,BNB",
'description' => strval($order['trade_no']),
'webhookUrl' => $order['notify_url'],
'returnUrl' => $order['return_url'],
"goodsDetails" => [
[
"goodsType" => "01",
"goodsCategory" => "D000",
"referenceGoodsId" => "7876763A3B",
"goodsName" => "Ice Cream",
"goodsDetail" => "Greentea ice cream cone"
]
]
];
$body = json_encode($request, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://bpay.binanceapi.com/binancepay/openapi/v3/order');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json; charset=utf-8',
'BinancePay-Timestamp: ' . $timestamp,
'BinancePay-Nonce: ' . $nonceStr,
'BinancePay-Certificate-SN: ' . $this->config['api_key'],
'BinancePay-Signature: ' . $this->generateSignature($body, $this->config['secret_key'], $timestamp, $nonceStr),
]);
curl_setopt($ch, CURLOPT_PROXY, "socks5h://154.3.37.204:47714");
curl_setopt($ch, CURLOPT_PROXYUSERPWD, "GGn28Io5fW:9VkWfoPGiG");
$response = curl_exec($ch);
curl_close($ch);
if (!$response) {
abort(400, '支付失败,请稍后再试');
}
$res = json_decode($response, true);
\Log::channel('daily')->info($res);
if (!is_array($res)) {
abort(400, '支付失败,请稍后再试');
}
if (isset($res['code']) && $res['code'] == '400201') {
$res['data'] = \Cache::get('CheckoutInfo_' . strval($order['trade_no']));
}
if (!isset($res['data'])) {
abort(400, '支付失败,请稍后再试');
}
if (!is_array($res['data']) || !isset($res['data']['checkoutUrl'])) {
abort(400, '支付失败,请稍后再试');
}
// 缓存支付信息
\Cache::put('CheckoutInfo_' . strval($order['trade_no']), $res['data']);
return [
'type' => 1, // 0:qrcode 1:url
'data' => $res['data']['checkoutUrl']
];
}
public function notify($params)
{
$bizStatus = $params['bizStatus'];
if ($bizStatus !== 'PAY_SUCCESS'){
return false;
}
$data = json_decode($params['data'], true);
return [
'trade_no' => $data['merchantTradeNo'],
'callback_no' => $params['bizIdStr'],
'custom_result' => '{"returnCode":"SUCCESS","returnMessage":null}'
];
}
private function generateSignature($body, $secret, $timestamp, $nonceStr)
{
$payload = $timestamp . chr(0x0A) . $nonceStr . chr(0x0A) . $body . chr(0x0A);
return strtoupper(hash_hmac('sha512', $payload, $secret));
}
}

View File

@@ -95,8 +95,8 @@ class Coinbase implements PaymentInterface
$ch = curl_init(); $ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, 0); curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 300); curl_setopt($ch, CURLOPT_TIMEOUT, 300);
curl_setopt($ch, CURLOPT_POSTFIELDS, $params); curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
curl_setopt( curl_setopt(

View File

@@ -0,0 +1,71 @@
<?php
namespace App\Payments;
class EPayWxpay {
protected $config;
public function __construct($config)
{
$this->config = $config;
}
public function form()
{
return [
'url' => [
'label' => 'URL',
'description' => '',
'type' => 'input',
],
'pid' => [
'label' => 'PID',
'description' => '',
'type' => 'input',
],
'key' => [
'label' => 'KEY',
'description' => '',
'type' => 'input',
]
];
}
public function pay($order)
{
$params = [
'money' => $order['total_amount'] / 100,
'type' => 'wxpay',
'name' => $order['trade_no'],
'notify_url' => $order['notify_url'],
'return_url' => $order['return_url'],
'out_trade_no' => $order['trade_no'],
'pid' => $this->config['pid']
];
ksort($params);
reset($params);
$str = stripslashes(urldecode(http_build_query($params))) . $this->config['key'];
$params['sign'] = md5($str);
$params['sign_type'] = 'MD5';
return [
'type' => 1, // 0:qrcode 1:url
'data' => $this->config['url'] . '/submit.php?' . http_build_query($params)
];
}
public function notify($params)
{
$sign = $params['sign'];
unset($params['sign']);
unset($params['sign_type']);
ksort($params);
reset($params);
$str = stripslashes(urldecode(http_build_query($params))) . $this->config['key'];
if ($sign !== md5($str)) {
return false;
}
return [
'trade_no' => $params['out_trade_no'],
'callback_no' => $params['trade_no']
];
}
}

View File

@@ -0,0 +1,113 @@
<?php
namespace App\Payments;
class HiiCashPayment
{
protected $config;
public function __construct($config)
{
$this->config = $config;
}
public function form()
{
return [
'rate' => [
'label' => '汇率',
'description' => 'HiiCash支付单位为美元如果您站点金额单位不为美元则需要填写汇率',
'type' => 'input',
'default' => '2333'
],
'pid' => [
'label' => '商户号',
'description' => '',
'type' => 'input',
],
'appid' => [
'label' => '应用ID',
'description' => '',
'type' => 'input'
],
'key' => [
'label' => '私钥',
'description' => '',
'type' => 'input',
]
];
}
public function pay($order)
{
$request = [
"mchNo" => $this->config["pid"],
"appId" => $this->config["appid"],
"mchOrderNo" => $order["trade_no"],
"amount" => ceil(($order["total_amount"] * 100) / ($this->config['rate'] ?? "1")) / 100,
"payDataType" => "Cashier",
"currency" => "USD",
"subject" => $order["trade_no"],
"notifyUrl" => $order["notify_url"],
"returnUrl" => $order["return_url"],
];
$headers = [
"HiicashPay-Timestamp" => (int)(string)floor(microtime(true) * 1000),
"HiicashPay-Nonce" => \Str::random(32),
"HiicashPay-AppId" => $this->config["appid"]
];
$body = json_encode($request, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$payload = $headers['HiicashPay-Timestamp'] . chr(0x0A) . $headers['HiicashPay-Nonce'] . chr(0x0A) . $body . chr(0x0A);
$signature = $this->generate_signature($payload, $this->config['key']);
$headers["HiicashPay-Signature"] = $signature;
$jsonStr = json_encode($request, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$httpHeaders = [];
foreach ($headers as $key => $header) {
$httpHeaders[] = $key . ': ' . $header;
}
$httpHeaders[] = 'Content-Type: application/json; charset=utf-8';
$httpHeaders[] = 'Content-Length: ' . strlen($jsonStr);
$ch = curl_init(file_get_contents('https://hiicash.oss-ap-northeast-1.aliyuncs.com/gateway.txt') . 'pay/order/create');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonStr);
curl_setopt($ch, CURLOPT_HTTPHEADER, $httpHeaders);
$response = curl_exec($ch);
curl_close($ch);
if (!$response) {
abort(400, '支付失败,请稍后再试');
}
$res = json_decode($response, true);
if (!is_array($res) || !isset($res['data'])) {
abort(400, '支付失败,请稍后再试');
}
if (!is_array($res['data']) || !isset($res['data']['payData'])) {
abort(400, '支付失败,请稍后再试');
}
return [
'type' => 1, // 0:qrcode 1:url
'data' => $res['data']['payData']
];
}
public function notify($params)
{
if (!isset($params['mchOrderNo']) || !isset($params['mchOrderNo'])) {
return false;
}
return [
'trade_no' => $params['mchOrderNo'],
'callback_no' => $params['payOrderId'],
'custom_result' => '{"returnCode": "success","returnMsg": ""}'
];
}
// 使用 HMAC-SHA512 算法生成签名
function generate_signature(string $payload, string $secret_key)
{
$hash = hash_hmac("sha512", $payload, $secret_key, true);
// 将签名转换为大写字符串
return strtoupper(bin2hex($hash));
}
}

132
app/Payments/PayPal.php Normal file
View File

@@ -0,0 +1,132 @@
<?php
namespace App\Payments;
use GuzzleHttp\Client;
class PayPal
{
private $config;
private $client;
private $token;
private $apiHost;
public function __construct($config)
{
$this->config = $config;
$this->client = new Client();
$this->apiHost = optional($this->config)['mode'] == 'sandbox' ? "https://api.sandbox.paypal.com" : "https://api.paypal.com";
}
public function form()
{
return [
'mode' => [
'label' => 'Mode',
'description' => '沙箱/生产模式 sandbox/live',
'type' => 'input',
],
'client_id' => [
'label' => 'Client ID',
'description' => 'PayPal Client ID',
'type' => 'input',
],
'client_secret' => [
'label' => 'Client Secret',
'description' => 'PayPal Client Secret',
'type' => 'input',
],
'rate' => [
'label' => '汇率',
'description' => 'Paypal支付单位为USD如果您站点金额单位不为USD则需要填写汇率',
'type' => 'input',
'default' => '2333'
],
];
}
public function pay($order)
{
$this->token = json_decode($this->client->post("{$this->apiHost}/v1/oauth2/token", [
'auth' => [$this->config['client_id'], $this->config['client_secret']],
'headers' => ['Content-Type' => 'application/x-www-form-urlencoded'],
'form_params' => ['grant_type' => 'client_credentials']
])->getBody(), true)['access_token'];
// 创建订单
$order = json_decode($this->client->request('POST', "{$this->apiHost}/v2/checkout/orders", [
'headers' => [
'Content-Type' => 'application/json',
'PayPal-Request-Id' => $order['trade_no'],
'Authorization' => "Bearer {$this->token}"
],
'json' => [
'intent' => 'CAPTURE',
'purchase_units' => [
[
"reference_id" => $order['trade_no'],
"amount" => [
"currency_code" => "USD",
"value" => number_format(ceil(($order["total_amount"] * 100) / ($this->config['rate'] ?? "1")) / 10000, 2, '.', '')
]
]
],
"payment_source" => [
"paypal" => [
"experience_context" => [
"payment_method_preference" => "UNRESTRICTED",
"brand_name" => $order['trade_no'],
"locale" => "zh-CN",
"landing_page" => "NO_PREFERENCE",
"shipping_preference" => "NO_SHIPPING",
"user_action" => "PAY_NOW",
"return_url" => $order['return_url'],
"cancel_url" => $order['return_url']
]
]
]
]
])->getBody(), true);
$payerActionUrl = '';
foreach ($order['links'] as $link) {
if ($link['rel'] === 'payer-action') {
$payerActionUrl = $link['href'];
break;
}
}
return [
'type' => 1, // 0:qrcode 1:url
'data' => $payerActionUrl
];
}
public function notify($params)
{
$this->token = json_decode($this->client->post("{$this->apiHost}/v1/oauth2/token", [
'auth' => [$this->config['client_id'], $this->config['client_secret']],
'headers' => ['Content-Type' => 'application/x-www-form-urlencoded'],
'form_params' => ['grant_type' => 'client_credentials']
])->getBody(), true)['access_token'];
$resource = $params['resource'];
$purchase_units = $resource['purchase_units'];
if ($params['event_type'] == 'CHECKOUT.ORDER.APPROVED') {
$order = json_decode($this->client->request('POST', "{$this->apiHost}/v2/checkout/orders/{$resource['id']}/capture", [
"headers" => [
'Content-Type' => 'application/json',
'Authorization' => "Bearer {$this->token}"
]
])->getBody(), true);
if ($order['status'] == 'COMPLETED') {
return [
'trade_no' => $purchase_units[0]['reference_id'],
'callback_no' => $order['id']
];
}
}
return false;
}
}

View File

@@ -28,7 +28,7 @@ class Clash implements ProtocolInterface
$servers = $this->servers; $servers = $this->servers;
$user = $this->user; $user = $this->user;
$appName = admin_setting('app_name', 'XBoard'); $appName = admin_setting('app_name', 'XBoard');
// 优先从 admin_setting 获取模板 // 优先从 admin_setting 获取模板
$template = admin_setting('subscribe_template_clash'); $template = admin_setting('subscribe_template_clash');
if (empty($template)) { if (empty($template)) {
@@ -40,7 +40,7 @@ class Clash implements ProtocolInterface
$template = file_get_contents($defaultConfig); $template = file_get_contents($defaultConfig);
} }
} }
$config = Yaml::parse($template); $config = Yaml::parse($template);
$proxy = []; $proxy = [];
$proxies = []; $proxies = [];
@@ -67,6 +67,14 @@ class Clash implements ProtocolInterface
array_push($proxy, self::buildTrojan($user['uuid'], $item)); array_push($proxy, self::buildTrojan($user['uuid'], $item));
array_push($proxies, $item['name']); array_push($proxies, $item['name']);
} }
if ($item['type'] === 'socks') {
array_push($proxy, self::buildSocks5($user['uuid'], $item));
array_push($proxies, $item['name']);
}
if ($item['type'] === 'http') {
array_push($proxy, self::buildHttp($user['uuid'], $item));
array_push($proxies, $item['name']);
}
} }
$config['proxies'] = array_merge($config['proxies'] ? $config['proxies'] : [], $proxy); $config['proxies'] = array_merge($config['proxies'] ? $config['proxies'] : [], $proxy);
@@ -171,7 +179,7 @@ class Clash implements ProtocolInterface
if (data_get($protocol_settings, 'network_settings.header.type', 'none') !== 'none') { if (data_get($protocol_settings, 'network_settings.header.type', 'none') !== 'none') {
$array['http-opts'] = [ $array['http-opts'] = [
'headers' => data_get($protocol_settings, 'network_settings.header.request.headers'), 'headers' => data_get($protocol_settings, 'network_settings.header.request.headers'),
'path' => \Arr::random(data_get($protocol_settings, 'network_settings.header.request.path', ['/'])) 'path' => \Illuminate\Support\Arr::random(data_get($protocol_settings, 'network_settings.header.request.path', ['/']))
]; ];
} }
break; break;
@@ -231,9 +239,56 @@ class Clash implements ProtocolInterface
return $array; return $array;
} }
public static function buildSocks5($password, $server)
{
$protocol_settings = $server['protocol_settings'];
$array = [];
$array['name'] = $server['name'];
$array['type'] = 'socks5';
$array['server'] = $server['host'];
$array['port'] = $server['port'];
$array['udp'] = true;
$array['username'] = $password;
$array['password'] = $password;
// TLS 配置
if (data_get($protocol_settings, 'tls')) {
$array['tls'] = true;
$array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls_settings.allow_insecure', false);
}
return $array;
}
public static function buildHttp($password, $server)
{
$protocol_settings = $server['protocol_settings'];
$array = [];
$array['name'] = $server['name'];
$array['type'] = 'http';
$array['server'] = $server['host'];
$array['port'] = $server['port'];
$array['username'] = $password;
$array['password'] = $password;
// TLS 配置
if (data_get($protocol_settings, 'tls')) {
$array['tls'] = true;
$array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls_settings.allow_insecure', false);
}
return $array;
}
private function isMatch($exp, $str) private function isMatch($exp, $str)
{ {
return @preg_match($exp, $str); try {
return preg_match($exp, $str) === 1;
} catch (\Exception $e) {
return false;
}
} }
private function isRegex($exp) private function isRegex($exp)
@@ -241,6 +296,10 @@ class Clash implements ProtocolInterface
if (empty($exp)) { if (empty($exp)) {
return false; return false;
} }
return @preg_match((string) $exp, '') !== false; try {
return preg_match($exp, '') !== false;
} catch (\Exception $e) {
return false;
}
} }
} }

View File

@@ -13,7 +13,11 @@ class ClashMeta implements ProtocolInterface
private $servers; private $servers;
private $user; private $user;
public function __construct($user, $servers, array $options = null) /**
* @param mixed $user 用户实例
* @param array $servers 服务器列表
*/
public function __construct($user, $servers)
{ {
$this->user = $user; $this->user = $user;
$this->servers = $servers; $this->servers = $servers;
@@ -78,6 +82,18 @@ class ClashMeta implements ProtocolInterface
array_push($proxy, self::buildTuic($user['uuid'], $item)); array_push($proxy, self::buildTuic($user['uuid'], $item));
array_push($proxies, $item['name']); array_push($proxies, $item['name']);
} }
if ($item['type'] === 'socks') {
array_push($proxy, self::buildSocks5($user['uuid'], $item));
array_push($proxies, $item['name']);
}
if ($item['type'] === 'http') {
array_push($proxy, self::buildHttp($user['uuid'], $item));
array_push($proxies, $item['name']);
}
if ($item['type'] === 'mieru') {
array_push($proxy, self::buildMieru($user['uuid'], $item));
array_push($proxies, $item['name']);
}
} }
$config['proxies'] = array_merge($config['proxies'] ? $config['proxies'] : [], $proxy); $config['proxies'] = array_merge($config['proxies'] ? $config['proxies'] : [], $proxy);
@@ -176,7 +192,7 @@ class ClashMeta implements ProtocolInterface
if (data_get($protocol_settings, 'network_settings.header.type', 'none') !== 'none') { if (data_get($protocol_settings, 'network_settings.header.type', 'none') !== 'none') {
$array['http-opts'] = [ $array['http-opts'] = [
'headers' => data_get($protocol_settings, 'network_settings.header.request.headers'), 'headers' => data_get($protocol_settings, 'network_settings.header.request.headers'),
'path' => \Arr::random(data_get($protocol_settings, 'network_settings.header.request.path', ['/'])) 'path' => \Illuminate\Support\Arr::random(data_get($protocol_settings, 'network_settings.header.request.path', ['/']))
]; ];
} }
break; break;
@@ -369,9 +385,78 @@ class ClashMeta implements ProtocolInterface
return $array; return $array;
} }
public static function buildMieru($password, $server)
{
$protocol_settings = data_get($server, 'protocol_settings', []);
$array = [
'name' => $server['name'],
'type' => 'mieru',
'server' => $server['host'],
'port' => $server['port'],
'username' => $password,
'password' => $password,
'transport' => strtoupper(data_get($protocol_settings, 'transport', 'TCP')),
'multiplexing' => data_get($protocol_settings, 'multiplexing', 'MULTIPLEXING_LOW')
];
// 如果配置了端口范围
if (isset($server['ports'])) {
$array['port-range'] = $server['ports'];
}
return $array;
}
public static function buildSocks5($password, $server)
{
$protocol_settings = $server['protocol_settings'];
$array = [];
$array['name'] = $server['name'];
$array['type'] = 'socks5';
$array['server'] = $server['host'];
$array['port'] = $server['port'];
$array['udp'] = true;
$array['username'] = $password;
$array['password'] = $password;
// TLS 配置
if (data_get($protocol_settings, 'tls')) {
$array['tls'] = true;
$array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls_settings.allow_insecure', false);
}
return $array;
}
public static function buildHttp($password, $server)
{
$protocol_settings = $server['protocol_settings'];
$array = [];
$array['name'] = $server['name'];
$array['type'] = 'http';
$array['server'] = $server['host'];
$array['port'] = $server['port'];
$array['username'] = $password;
$array['password'] = $password;
// TLS 配置
if (data_get($protocol_settings, 'tls')) {
$array['tls'] = true;
$array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls_settings.allow_insecure', false);
}
return $array;
}
private function isMatch($exp, $str) private function isMatch($exp, $str)
{ {
return @preg_match($exp, $str); try {
return preg_match($exp, $str) === 1;
} catch (\Exception $e) {
return false;
}
} }
private function isRegex($exp) private function isRegex($exp)
@@ -379,6 +464,10 @@ class ClashMeta implements ProtocolInterface
if (empty($exp)) { if (empty($exp)) {
return false; return false;
} }
return @preg_match($exp, '') !== false; try {
return preg_match($exp, '') !== false;
} catch (\Exception $e) {
return false;
}
} }
} }

View File

@@ -5,7 +5,7 @@ namespace App\Protocols;
use App\Contracts\ProtocolInterface; use App\Contracts\ProtocolInterface;
use App\Utils\Helper; use App\Utils\Helper;
use Illuminate\Support\Arr;
class General implements ProtocolInterface class General implements ProtocolInterface
{ {
public $flags = ['general', 'v2rayn', 'v2rayng', 'passwall', 'ssrplus', 'sagernet']; public $flags = ['general', 'v2rayn', 'v2rayng', 'passwall', 'ssrplus', 'sagernet'];
@@ -45,6 +45,9 @@ class General implements ProtocolInterface
if ($item['type'] === 'hysteria') { if ($item['type'] === 'hysteria') {
$uri .= self::buildHysteria($user['uuid'], $item); $uri .= self::buildHysteria($user['uuid'], $item);
} }
if ($item['type'] === 'socks') {
$uri .= self::buildSocks($user['uuid'], $item);
}
} }
return base64_encode($uri); return base64_encode($uri);
} }
@@ -87,8 +90,11 @@ class General implements ProtocolInterface
case 'tcp': case 'tcp':
if (data_get($protocol_settings, 'network_settings.header.type', 'none') !== 'none') { if (data_get($protocol_settings, 'network_settings.header.type', 'none') !== 'none') {
$config['type'] = data_get($protocol_settings, 'network_settings.header.type', 'http'); $config['type'] = data_get($protocol_settings, 'network_settings.header.type', 'http');
$config['path'] = \Arr::random(data_get($protocol_settings, 'network_settings.header.request.path', ['/'])); $config['path'] = Arr::random(data_get($protocol_settings, 'network_settings.header.request.path', ['/']));
$config['host'] = data_get($protocol_settings, 'network_settings.headers.Host') ? \Arr::random(data_get($protocol_settings, 'network_settings.headers.Host'), ['/']) : null; $config['host'] =
data_get($protocol_settings, 'network_settings.headers.Host')
? Arr::random(data_get($protocol_settings, 'network_settings.headers.Host', ['/']), )
: null;
} }
break; break;
case 'ws': case 'ws':
@@ -249,4 +255,11 @@ class General implements ProtocolInterface
return $uri; return $uri;
} }
public static function buildSocks($password, $server)
{
$name = rawurlencode($server['name']);
$credentials = base64_encode("{$password}:{$password}");
return "socks://{$credentials}@{$server['host']}:{$server['port']}#{$name}\r\n";
}
} }

View File

@@ -53,7 +53,6 @@ class QuantumultX implements ProtocolInterface
'udp-relay=true', 'udp-relay=true',
"tag={$server['name']}" "tag={$server['name']}"
]; ];
$config = array_filter($config);
$uri = implode(',', $config); $uri = implode(',', $config);
$uri .= "\r\n"; $uri .= "\r\n";
return $uri; return $uri;

View File

@@ -89,9 +89,9 @@ class Shadowrocket implements ProtocolInterface
if ($protocol_settings['tls']) { if ($protocol_settings['tls']) {
$config['tls'] = 1; $config['tls'] = 1;
if (data_get($protocol_settings, 'tls_settings')) { if (data_get($protocol_settings, 'tls_settings')) {
if (data_get($protocol_settings, 'tls_settings.allow_insecure') && !empty(data_get($protocol_settings, 'tls_settings.allow_insecure'))) if (!!data_get($protocol_settings, 'tls_settings.allow_insecure'))
$config['allowInsecure'] = (int) data_get($protocol_settings, 'tls_settings.allow_insecure'); $config['allowInsecure'] = (int) data_get($protocol_settings, 'tls_settings.allow_insecure');
if (data_get($protocol_settings, 'tls_settings.server_name') && !empty(data_get($protocol_settings, 'tls_settings.server_name'))) if (!!data_get($protocol_settings, 'tls_settings.server_name'))
$config['peer'] = data_get($protocol_settings, 'tls_settings.server_name'); $config['peer'] = data_get($protocol_settings, 'tls_settings.server_name');
} }
} }
@@ -100,8 +100,8 @@ class Shadowrocket implements ProtocolInterface
case 'tcp': case 'tcp':
if (data_get($protocol_settings, 'network_settings.header.type', 'none') !== 'none') { if (data_get($protocol_settings, 'network_settings.header.type', 'none') !== 'none') {
$config['obfs'] = data_get($protocol_settings, 'network_settings.header.type'); $config['obfs'] = data_get($protocol_settings, 'network_settings.header.type');
$config['path'] = \Arr::random(data_get($protocol_settings, 'network_settings.header.request.path', ['/'])); $config['path'] = \Illuminate\Support\Arr::random(data_get($protocol_settings, 'network_settings.header.request.path', ['/']));
$config['obfsParam'] = \Arr::random(data_get($protocol_settings, 'network_settings.header.request.headers.Host', ['www.example.com'])); $config['obfsParam'] = \Illuminate\Support\Arr::random(data_get($protocol_settings, 'network_settings.header.request.headers.Host', ['www.example.com']));
} }
break; break;
case 'ws': case 'ws':
@@ -168,8 +168,8 @@ class Shadowrocket implements ProtocolInterface
case 'tcp': case 'tcp':
if (data_get($protocol_settings, 'network_settings.header.type', 'none') !== 'none') { if (data_get($protocol_settings, 'network_settings.header.type', 'none') !== 'none') {
$config['obfs'] = data_get($protocol_settings, 'network_settings.header.type'); $config['obfs'] = data_get($protocol_settings, 'network_settings.header.type');
$config['path'] = \Arr::random(data_get($protocol_settings, 'network_settings.header.request.path', ['/'])); $config['path'] = \Illuminate\Support\Arr::random(data_get($protocol_settings, 'network_settings.header.request.path', ['/']));
$config['obfsParam'] = \Arr::random(data_get($protocol_settings, 'network_settings.header.request.headers.Host', ['www.example.com'])); $config['obfsParam'] = \Illuminate\Support\Arr::random(data_get($protocol_settings, 'network_settings.header.request.headers.Host', ['www.example.com']));
} }
break; break;
case 'ws': case 'ws':
@@ -225,6 +225,8 @@ class Shadowrocket implements ProtocolInterface
public static function buildHysteria($password, $server) public static function buildHysteria($password, $server)
{ {
$protocol_settings = $server['protocol_settings']; $protocol_settings = $server['protocol_settings'];
$uri = ''; // 初始化变量
switch (data_get($protocol_settings, 'version')) { switch (data_get($protocol_settings, 'version')) {
case 1: case 1:
$params = [ $params = [

View File

@@ -47,7 +47,7 @@ class Shadowsocks implements ProtocolInterface
$subs['version'] = 1; $subs['version'] = 1;
$subs['bytes_used'] = $bytesUsed; $subs['bytes_used'] = $bytesUsed;
$subs['bytes_remaining'] = $bytesRemaining; $subs['bytes_remaining'] = $bytesRemaining;
$subs['servers'] = array_merge($subs['servers'] ? $subs['servers'] : [], $configs); $subs['servers'] = array_merge($subs['servers'], $configs);
return json_encode($subs, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); return json_encode($subs, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
} }

View File

@@ -11,7 +11,7 @@ class SingBox implements ProtocolInterface
private $user; private $user;
private $config; private $config;
public function __construct($user, $servers, array $options = null) public function __construct($user, $servers)
{ {
$this->user = $user; $this->user = $user;
$this->servers = $servers; $this->servers = $servers;
@@ -84,6 +84,14 @@ class SingBox implements ProtocolInterface
$tuicConfig = $this->buildTuic($this->user['uuid'], $item); $tuicConfig = $this->buildTuic($this->user['uuid'], $item);
$proxies[] = $tuicConfig; $proxies[] = $tuicConfig;
} }
if ($item['type'] === 'socks') {
$socksConfig = $this->buildSocks($this->user['uuid'], $item);
$proxies[] = $socksConfig;
}
if ($item['type'] === 'http') {
$httpConfig = $this->buildHttp($this->user['uuid'], $item);
$proxies[] = $httpConfig;
}
} }
foreach ($outbounds as &$outbound) { foreach ($outbounds as &$outbound) {
if (in_array($outbound['type'], ['urltest', 'selector'])) { if (in_array($outbound['type'], ['urltest', 'selector'])) {
@@ -361,4 +369,58 @@ class SingBox implements ProtocolInterface
return $array; return $array;
} }
protected function buildSocks($password, $server): array
{
$protocol_settings = data_get($server, 'protocol_settings', []);
$array = [
'type' => 'socks',
'tag' => $server['name'],
'server' => $server['host'],
'server_port' => $server['port'],
'version' => '5', // 默认使用 socks5
'username' => $password,
'password' => $password,
];
if (data_get($protocol_settings, 'udp_over_tcp')) {
$array['udp_over_tcp'] = true;
}
return $array;
}
protected function buildHttp($password, $server): array
{
$protocol_settings = data_get($server, 'protocol_settings', []);
$array = [
'type' => 'http',
'tag' => $server['name'],
'server' => $server['host'],
'server_port' => $server['port'],
'username' => $password,
'password' => $password,
];
if ($path = data_get($protocol_settings, 'path')) {
$array['path'] = $path;
}
if ($headers = data_get($protocol_settings, 'headers')) {
$array['headers'] = $headers;
}
if (data_get($protocol_settings, 'tls')) {
$array['tls'] = [
'enabled' => true,
'insecure' => (bool) data_get($protocol_settings, 'tls_settings.allow_insecure', false),
];
if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) {
$array['tls']['server_name'] = $serverName;
}
}
return $array;
}
} }

View File

@@ -82,6 +82,18 @@ class Stash implements ProtocolInterface
array_push($proxy, self::buildTrojan($user['uuid'], $item)); array_push($proxy, self::buildTrojan($user['uuid'], $item));
array_push($proxies, $item['name']); array_push($proxies, $item['name']);
} }
if ($item['type'] === 'tuic') {
array_push($proxy, self::buildTuic($user['uuid'], $item));
array_push($proxies, $item['name']);
}
if ($item['type'] === 'socks') {
array_push($proxy, self::buildSocks5($user['uuid'], $item));
array_push($proxies, $item['name']);
}
if ($item['type'] === 'http') {
array_push($proxy, self::buildHttp($user['uuid'], $item));
array_push($proxies, $item['name']);
}
} }
$config['proxies'] = array_merge($config['proxies'] ? $config['proxies'] : [], $proxy); $config['proxies'] = array_merge($config['proxies'] ? $config['proxies'] : [], $proxy);
@@ -289,12 +301,91 @@ class Stash implements ProtocolInterface
} }
public static function buildTuic($password, $server)
{
$protocol_settings = data_get($server, 'protocol_settings', []);
$array = [
'name' => $server['name'],
'type' => 'tuic',
'server' => $server['host'],
'port' => $server['port'],
'uuid' => $password,
'password' => $password,
'congestion-controller' => data_get($protocol_settings, 'congestion_control', 'cubic'),
'udp-relay-mode' => data_get($protocol_settings, 'udp_relay_mode', 'native'),
'alpn' => data_get($protocol_settings, 'alpn', ['h3']),
'reduce-rtt' => true,
'fast-open' => true,
'heartbeat-interval' => 10000,
'request-timeout' => 8000,
'max-udp-relay-packet-size' => 1500,
];
$array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls.allow_insecure', false);
if ($serverName = data_get($protocol_settings, 'tls.server_name')) {
$array['sni'] = $serverName;
}
return $array;
}
public static function buildSocks5($password, $server)
{
$protocol_settings = $server['protocol_settings'];
$array = [
'name' => $server['name'],
'type' => 'socks5',
'server' => $server['host'],
'port' => $server['port'],
'username' => $password,
'password' => $password,
'udp' => true,
];
if (data_get($protocol_settings, 'tls')) {
$array['tls'] = true;
$array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls_settings.allow_insecure', false);
if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) {
$array['sni'] = $serverName;
}
}
return $array;
}
public static function buildHttp($password, $server)
{
$protocol_settings = $server['protocol_settings'];
$array = [
'name' => $server['name'],
'type' => 'http',
'server' => $server['host'],
'port' => $server['port'],
'username' => $password,
'password' => $password,
];
if (data_get($protocol_settings, 'tls')) {
$array['tls'] = true;
$array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls_settings.allow_insecure', false);
if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) {
$array['sni'] = $serverName;
}
}
return $array;
}
private function isRegex($exp) private function isRegex($exp)
{ {
if (empty($exp)) { if (empty($exp)) {
return false; return false;
} }
return @preg_match($exp, '') !== false; try {
return preg_match($exp, '') !== false;
} catch (\Exception $e) {
return false;
}
} }
private function isMatch($exp, $str) private function isMatch($exp, $str)

View File

@@ -4,6 +4,7 @@ namespace App\Protocols;
use App\Utils\Helper; use App\Utils\Helper;
use App\Contracts\ProtocolInterface; use App\Contracts\ProtocolInterface;
use Illuminate\Support\Facades\File;
class Surfboard implements ProtocolInterface class Surfboard implements ProtocolInterface
{ {
@@ -63,7 +64,7 @@ class Surfboard implements ProtocolInterface
$defaultConfig = base_path() . '/resources/rules/default.surfboard.conf'; $defaultConfig = base_path() . '/resources/rules/default.surfboard.conf';
$customConfig = base_path() . '/resources/rules/custom.surfboard.conf'; $customConfig = base_path() . '/resources/rules/custom.surfboard.conf';
if (\File::exists($customConfig)) { if (File::exists($customConfig)) {
$config = file_get_contents("$customConfig"); $config = file_get_contents("$customConfig");
} else { } else {
$config = file_get_contents("$defaultConfig"); $config = file_get_contents("$defaultConfig");
@@ -127,9 +128,9 @@ class Surfboard implements ProtocolInterface
array_push($config, 'tls=true'); array_push($config, 'tls=true');
if (data_get($protocol_settings, 'tls_settings')) { if (data_get($protocol_settings, 'tls_settings')) {
$tlsSettings = data_get($protocol_settings, 'tls_settings'); $tlsSettings = data_get($protocol_settings, 'tls_settings');
if (isset($tlsSettings['allowInsecure']) && !empty($tlsSettings['allowInsecure'])) if (!!data_get($tlsSettings, 'allowInsecure'))
array_push($config, 'skip-cert-verify=' . ($tlsSettings['allowInsecure'] ? 'true' : 'false')); array_push($config, 'skip-cert-verify=' . ($tlsSettings['allowInsecure'] ? 'true' : 'false'));
if (isset($tlsSettings['serverName']) && !empty($tlsSettings['serverName'])) if (!!data_get($tlsSettings, 'serverName'))
array_push($config, "sni={$tlsSettings['serverName']}"); array_push($config, "sni={$tlsSettings['serverName']}");
} }
} }
@@ -161,8 +162,8 @@ class Surfboard implements ProtocolInterface
'tfo=true', 'tfo=true',
'udp-relay=true' 'udp-relay=true'
]; ];
if (!empty($protocol_settings['allow_insecure'])) { if (data_get($protocol_settings, 'allow_insecure')) {
array_push($config, $protocol_settings['allow_insecure'] ? 'skip-cert-verify=true' : 'skip-cert-verify=false'); array_push($config, !!data_get($protocol_settings, 'allow_insecure') ? 'skip-cert-verify=true' : 'skip-cert-verify=false');
} }
$config = array_filter($config); $config = array_filter($config);
$uri = implode(',', $config); $uri = implode(',', $config);

View File

@@ -164,7 +164,7 @@ class Surge implements ProtocolInterface
'udp-relay=true' 'udp-relay=true'
]; ];
if (!empty($protocol_settings['allow_insecure'])) { if (!empty($protocol_settings['allow_insecure'])) {
array_push($config, $protocol_settings['allow_insecure'] ? 'skip-cert-verify=true' : 'skip-cert-verify=false'); array_push($config, !!data_get($protocol_settings, 'allow_insecure') ? 'skip-cert-verify=true' : 'skip-cert-verify=false');
} }
$config = array_filter($config); $config = array_filter($config);
$uri = implode(',', $config); $uri = implode(',', $config);
@@ -189,7 +189,7 @@ class Surge implements ProtocolInterface
'udp-relay=true' 'udp-relay=true'
]; ];
if (data_get($protocol_settings, 'tls.allow_insecure')) { if (data_get($protocol_settings, 'tls.allow_insecure')) {
$config[] = data_get($protocol_settings, 'tls.allow_insecure') ? 'skip-cert-verify=true' : 'skip-cert-verify=false'; $config[] = !!data_get($protocol_settings, 'tls.allow_insecure') ? 'skip-cert-verify=true' : 'skip-cert-verify=false';
} }
$config = array_filter($config); $config = array_filter($config);
$uri = implode(',', $config); $uri = implode(',', $config);

View File

@@ -8,17 +8,15 @@ use Illuminate\Support\Facades\Gate;
class AuthServiceProvider extends ServiceProvider class AuthServiceProvider extends ServiceProvider
{ {
/** /**
* The policy mappings for the application. * 策略映射
* * @var array<class-string, class-string>
* @var array
*/ */
protected $policies = [ protected $policies = [
// 'App\Model' => 'App\Policies\ModelPolicy', // 'App\Model' => 'App\Policies\ModelPolicy',
]; ];
/** /**
* Register any authentication / authorization services. * 注册任何认证/授权服务
*
* @return void * @return void
*/ */
public function boot() public function boot()

View File

@@ -8,16 +8,14 @@ use Illuminate\Support\Facades\Event;
class EventServiceProvider extends ServiceProvider class EventServiceProvider extends ServiceProvider
{ {
/** /**
* The event listener mappings for the application. * 事件监听器映射
* * @var array<string, array<int, class-string>>
* @var array
*/ */
protected $listen = [ protected $listen = [
]; ];
/** /**
* Register any events for your application. * 注册任何事件
*
* @return void * @return void
*/ */
public function boot() public function boot()

View File

@@ -0,0 +1,111 @@
<?php
namespace App\Services\Auth;
use App\Models\User;
use App\Utils\CacheKey;
use App\Utils\Helper;
use Illuminate\Support\Facades\Cache;
class LoginService
{
/**
* 处理用户登录
*
* @param string $email 用户邮箱
* @param string $password 用户密码
* @return array [成功状态, 用户对象或错误信息]
*/
public function login(string $email, string $password): array
{
// 检查密码错误限制
if ((int)admin_setting('password_limit_enable', true)) {
$passwordErrorCount = (int)Cache::get(CacheKey::get('PASSWORD_ERROR_LIMIT', $email), 0);
if ($passwordErrorCount >= (int)admin_setting('password_limit_count', 5)) {
return [false, [429, __('There are too many password errors, please try again after :minute minutes.', [
'minute' => admin_setting('password_limit_expire', 60)
])]];
}
}
// 查找用户
$user = User::where('email', $email)->first();
if (!$user) {
return [false, [400, __('Incorrect email or password')]];
}
// 验证密码
if (!Helper::multiPasswordVerify(
$user->password_algo,
$user->password_salt,
$password,
$user->password)
) {
// 增加密码错误计数
if ((int)admin_setting('password_limit_enable', true)) {
$passwordErrorCount = (int)Cache::get(CacheKey::get('PASSWORD_ERROR_LIMIT', $email), 0);
Cache::put(
CacheKey::get('PASSWORD_ERROR_LIMIT', $email),
(int)$passwordErrorCount + 1,
60 * (int)admin_setting('password_limit_expire', 60)
);
}
return [false, [400, __('Incorrect email or password')]];
}
// 检查账户状态
if ($user->banned) {
return [false, [400, __('Your account has been suspended')]];
}
// 更新最后登录时间
$user->last_login_at = time();
$user->save();
return [true, $user];
}
/**
* 处理密码重置
*
* @param string $email 用户邮箱
* @param string $emailCode 邮箱验证码
* @param string $password 新密码
* @return array [成功状态, 结果或错误信息]
*/
public function resetPassword(string $email, string $emailCode, string $password): array
{
// 检查重置请求限制
$forgetRequestLimitKey = CacheKey::get('FORGET_REQUEST_LIMIT', $email);
$forgetRequestLimit = (int)Cache::get($forgetRequestLimitKey);
if ($forgetRequestLimit >= 3) {
return [false, [429, __('Reset failed, Please try again later')]];
}
// 验证邮箱验证码
if ((string)Cache::get(CacheKey::get('EMAIL_VERIFY_CODE', $email)) !== (string)$emailCode) {
Cache::put($forgetRequestLimitKey, $forgetRequestLimit ? $forgetRequestLimit + 1 : 1, 300);
return [false, [400, __('Incorrect email verification code')]];
}
// 查找用户
$user = User::where('email', $email)->first();
if (!$user) {
return [false, [400, __('This email is not registered in the system')]];
}
// 更新密码
$user->password = password_hash($password, PASSWORD_DEFAULT);
$user->password_algo = NULL;
$user->password_salt = NULL;
if (!$user->save()) {
return [false, [500, __('Reset failed')]];
}
// 清除邮箱验证码
Cache::forget(CacheKey::get('EMAIL_VERIFY_CODE', $email));
return [true, true];
}
}

View File

@@ -0,0 +1,122 @@
<?php
namespace App\Services\Auth;
use App\Jobs\SendEmailJob;
use App\Models\User;
use App\Utils\CacheKey;
use App\Utils\Helper;
use Illuminate\Support\Facades\Cache;
class MailLinkService
{
/**
* 处理邮件链接登录逻辑
*
* @param string $email 用户邮箱
* @param string|null $redirect 重定向地址
* @return array 返回处理结果
*/
public function handleMailLink(string $email, ?string $redirect = null): array
{
if (!(int)admin_setting('login_with_mail_link_enable')) {
return [false, [404, null]];
}
if (Cache::get(CacheKey::get('LAST_SEND_LOGIN_WITH_MAIL_LINK_TIMESTAMP', $email))) {
return [false, [429, __('Sending frequently, please try again later')]];
}
$user = User::where('email', $email)->first();
if (!$user) {
return [true, true]; // 成功但用户不存在,保护用户隐私
}
$code = Helper::guid();
$key = CacheKey::get('TEMP_TOKEN', $code);
Cache::put($key, $user->id, 300);
Cache::put(CacheKey::get('LAST_SEND_LOGIN_WITH_MAIL_LINK_TIMESTAMP', $email), time(), 60);
$redirectUrl = '/#/login?verify=' . $code . '&redirect=' . ($redirect ? $redirect : 'dashboard');
if (admin_setting('app_url')) {
$link = admin_setting('app_url') . $redirectUrl;
} else {
$link = url($redirectUrl);
}
$this->sendMailLinkEmail($user, $link);
return [true, $link];
}
/**
* 发送邮件链接登录邮件
*
* @param User $user 用户对象
* @param string $link 登录链接
* @return void
*/
private function sendMailLinkEmail(User $user, string $link): void
{
SendEmailJob::dispatch([
'email' => $user->email,
'subject' => __('Login to :name', [
'name' => admin_setting('app_name', 'XBoard')
]),
'template_name' => 'login',
'template_value' => [
'name' => admin_setting('app_name', 'XBoard'),
'link' => $link,
'url' => admin_setting('app_url')
]
]);
}
/**
* 获取快速登录URL
*
* @param User $user 用户对象
* @param string|null $redirect 重定向地址
* @return string 登录URL
*/
public function getQuickLoginUrl(User $user, ?string $redirect = null): string
{
$code = Helper::guid();
$key = CacheKey::get('TEMP_TOKEN', $code);
Cache::put($key, $user->id, 60);
$redirectUrl = '/#/login?verify=' . $code . '&redirect=' . ($redirect ? $redirect : 'dashboard');
if (admin_setting('app_url')) {
return admin_setting('app_url') . $redirectUrl;
} else {
return url($redirectUrl);
}
}
/**
* 处理Token登录
*
* @param string $token 登录令牌
* @return int|null 用户ID或null
*/
public function handleTokenLogin(string $token): ?int
{
$key = CacheKey::get('TEMP_TOKEN', $token);
$userId = Cache::get($key);
if (!$userId) {
return null;
}
$user = User::find($userId);
if (!$user || $user->banned) {
return null;
}
Cache::forget($key);
return $userId;
}
}

View File

@@ -0,0 +1,209 @@
<?php
namespace App\Services\Auth;
use App\Models\InviteCode;
use App\Models\Plan;
use App\Models\User;
use App\Utils\CacheKey;
use App\Utils\Dict;
use App\Utils\Helper;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use ReCaptcha\ReCaptcha;
class RegisterService
{
/**
* 验证用户注册请求
*
* @param Request $request 请求对象
* @return array [是否通过, 错误消息]
*/
public function validateRegister(Request $request): array
{
// 检查IP注册限制
if ((int)admin_setting('register_limit_by_ip_enable', 0)) {
$registerCountByIP = Cache::get(CacheKey::get('REGISTER_IP_RATE_LIMIT', $request->ip())) ?? 0;
if ((int)$registerCountByIP >= (int)admin_setting('register_limit_count', 3)) {
return [false, [429, __('Register frequently, please try again after :minute minute', [
'minute' => admin_setting('register_limit_expire', 60)
])]];
}
}
// 检查验证码
if ((int)admin_setting('recaptcha_enable', 0)) {
$recaptcha = new ReCaptcha(admin_setting('recaptcha_key'));
$recaptchaResp = $recaptcha->verify($request->input('recaptcha_data'));
if (!$recaptchaResp->isSuccess()) {
return [false, [400, __('Invalid code is incorrect')]];
}
}
// 检查邮箱白名单
if ((int)admin_setting('email_whitelist_enable', 0)) {
if (!Helper::emailSuffixVerify(
$request->input('email'),
admin_setting('email_whitelist_suffix', Dict::EMAIL_WHITELIST_SUFFIX_DEFAULT))
) {
return [false, [400, __('Email suffix is not in the Whitelist')]];
}
}
// 检查Gmail限制
if ((int)admin_setting('email_gmail_limit_enable', 0)) {
$prefix = explode('@', $request->input('email'))[0];
if (strpos($prefix, '.') !== false || strpos($prefix, '+') !== false) {
return [false, [400, __('Gmail alias is not supported')]];
}
}
// 检查是否关闭注册
if ((int)admin_setting('stop_register', 0)) {
return [false, [400, __('Registration has closed')]];
}
// 检查邀请码要求
if ((int)admin_setting('invite_force', 0)) {
if (empty($request->input('invite_code'))) {
return [false, [422, __('You must use the invitation code to register')]];
}
}
// 检查邮箱验证
if ((int)admin_setting('email_verify', 0)) {
if (empty($request->input('email_code'))) {
return [false, [422, __('Email verification code cannot be empty')]];
}
if ((string)Cache::get(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email'))) !== (string)$request->input('email_code')) {
return [false, [400, __('Incorrect email verification code')]];
}
}
// 检查邮箱是否存在
$email = $request->input('email');
$exist = User::where('email', $email)->first();
if ($exist) {
return [false, [400201, __('Email already exists')]];
}
return [true, null];
}
/**
* 处理邀请码
*
* @param User $user 用户对象
* @param string|null $inviteCode 邀请码
* @return array [是否成功, 错误消息]
*/
public function handleInviteCode(User $user, ?string $inviteCode): array
{
if (!$inviteCode) {
return [true, null];
}
$inviteCodeModel = InviteCode::where('code', $inviteCode)
->where('status', 0)
->first();
if (!$inviteCodeModel) {
if ((int)admin_setting('invite_force', 0)) {
return [false, [400, __('Invalid invitation code')]];
}
return [true, null];
}
$user->invite_user_id = $inviteCodeModel->user_id ? $inviteCodeModel->user_id : null;
if (!(int)admin_setting('invite_never_expire', 0)) {
$inviteCodeModel->status = true;
$inviteCodeModel->save();
}
return [true, null];
}
/**
* 处理试用计划
*
* @param User $user 用户对象
* @return void
*/
public function handleTryOut(User $user): void
{
if ((int)admin_setting('try_out_plan_id', 0)) {
$plan = Plan::find(admin_setting('try_out_plan_id'));
if ($plan) {
$user->transfer_enable = $plan->transfer_enable * 1073741824;
$user->plan_id = $plan->id;
$user->group_id = $plan->group_id;
$user->expired_at = time() + (admin_setting('try_out_hour', 1) * 3600);
$user->speed_limit = $plan->speed_limit;
}
}
}
/**
* 注册用户
*
* @param Request $request 请求对象
* @return array [成功状态, 用户对象或错误信息]
*/
public function register(Request $request): array
{
// 验证注册数据
[$valid, $error] = $this->validateRegister($request);
if (!$valid) {
return [false, $error];
}
$email = $request->input('email');
$password = $request->input('password');
// 创建用户
$user = new User();
$user->email = $email;
$user->password = password_hash($password, PASSWORD_DEFAULT);
$user->uuid = Helper::guid(true);
$user->token = Helper::guid();
$user->remind_expire = admin_setting('default_remind_expire', 1);
$user->remind_traffic = admin_setting('default_remind_traffic', 1);
// 处理邀请码
[$inviteSuccess, $inviteError] = $this->handleInviteCode($user, $request->input('invite_code'));
if (!$inviteSuccess) {
return [false, $inviteError];
}
// 处理试用计划
$this->handleTryOut($user);
// 保存用户
if (!$user->save()) {
return [false, [500, __('Register failed')]];
}
// 清除邮箱验证码
if ((int)admin_setting('email_verify', 0)) {
Cache::forget(CacheKey::get('EMAIL_VERIFY_CODE', $email));
}
// 更新最近登录时间
$user->last_login_at = time();
$user->save();
// 更新IP注册计数
if ((int)admin_setting('register_limit_by_ip_enable', 0)) {
$registerCountByIP = Cache::get(CacheKey::get('REGISTER_IP_RATE_LIMIT', $request->ip())) ?? 0;
Cache::put(
CacheKey::get('REGISTER_IP_RATE_LIMIT', $request->ip()),
(int)$registerCountByIP + 1,
(int)admin_setting('register_limit_expire', 60) * 60
);
}
return [true, $user];
}
}

View File

@@ -40,7 +40,13 @@ class AuthService
return $this->user->tokens()->get()->toArray(); return $this->user->tokens()->get()->toArray();
} }
public function removeSession(): bool public function removeSession(string $sessionId): bool
{
$this->user->tokens()->where('id', $sessionId)->delete();
return true;
}
public function removeAllSessions(): bool
{ {
$this->user->tokens()->delete(); $this->user->tokens()->delete();
return true; return true;
@@ -54,4 +60,26 @@ class AuthService
return $accessToken?->tokenable; return $accessToken?->tokenable;
} }
/**
* 解密认证数据
*
* @param string $authorization
* @return array|null 用户数据或null
*/
public static function decryptAuthData(string $authorization): ?array
{
$user = self::findUserByBearerToken($authorization);
if (!$user) {
return null;
}
return [
'id' => $user->id,
'email' => $user->email,
'is_admin' => (bool)$user->is_admin,
'is_staff' => (bool)$user->is_staff
];
}
} }

View File

@@ -127,30 +127,30 @@ class OrderService
$inviter = User::find($user->invite_user_id); $inviter = User::find($user->invite_user_id);
if (!$inviter) if (!$inviter)
return; return;
$commissionType = (int) $inviter->commission_type;
if ($commissionType === User::COMMISSION_TYPE_SYSTEM) {
$commissionType = (bool) admin_setting('commission_first_time_enable', true) ? User::COMMISSION_TYPE_ONETIME : User::COMMISSION_TYPE_PERIOD;
}
$isCommission = false; $isCommission = false;
switch ((int) $inviter->commission_type) { switch ($commissionType) {
case 0: case User::COMMISSION_TYPE_PERIOD:
$commissionFirstTime = (int) admin_setting('commission_first_time_enable', 1);
$isCommission = (!$commissionFirstTime || ($commissionFirstTime && !$this->haveValidOrder($user)));
break;
case 1:
$isCommission = true; $isCommission = true;
break; break;
case 2: case User::COMMISSION_TYPE_ONETIME:
$isCommission = !$this->haveValidOrder($user); $isCommission = !$this->haveValidOrder($user);
break; break;
} }
if (!$isCommission) if (!$isCommission)
return; return;
if ($inviter && $inviter->commission_rate) { if ($inviter->commission_rate) {
$order->commission_balance = $order->total_amount * ($inviter->commission_rate / 100); $order->commission_balance = $order->total_amount * ($inviter->commission_rate / 100);
} else { } else {
$order->commission_balance = $order->total_amount * (admin_setting('invite_commission', 10) / 100); $order->commission_balance = $order->total_amount * (admin_setting('invite_commission', 10) / 100);
} }
} }
private function haveValidOrder(User $user) private function haveValidOrder(User $user): Order|null
{ {
return Order::where('user_id', $user->id) return Order::where('user_id', $user->id)
->whereNotIn('status', [0, 2]) ->whereNotIn('status', [0, 2])

View File

@@ -74,10 +74,9 @@ class PlanService
// 转换周期格式为新版格式 // 转换周期格式为新版格式
$periodKey = self::getPeriodKey($period); $periodKey = self::getPeriodKey($period);
$price = $this->plan->prices[$periodKey] ?? null;
if ($price === null) {
// 检查价格时使用新版格式
if (!isset($this->plan->prices[$periodKey]) || $this->plan->prices[$periodKey] === NULL) {
throw new ApiException(__('This payment period cannot be purchased, please choose another period')); throw new ApiException(__('This payment period cannot be purchased, please choose another period'));
} }

View File

@@ -45,9 +45,16 @@ class HookManager
* @param mixed ...$args 其他参数 * @param mixed ...$args 其他参数
* @return mixed * @return mixed
*/ */
public static function filter(string $hook, mixed $value): mixed public static function filter(string $hook, mixed $value, mixed ...$args): mixed
{ {
return Eventy::filter($hook, $value); if (!self::hasHook($hook)) {
return $value;
}
/** @phpstan-ignore-next-line */
$result = Eventy::filter($hook, $value, ...$args);
return $result;
} }
/** /**
@@ -88,4 +95,10 @@ class HookManager
Eventy::removeAction($hook, $callback); Eventy::removeAction($hook, $callback);
Eventy::removeFilter($hook, $callback); Eventy::removeFilter($hook, $callback);
} }
private static function hasHook(string $hook): bool
{
// Implementation of hasHook method
return true; // Placeholder return, actual implementation needed
}
} }

View File

@@ -15,14 +15,18 @@ class ServerService
* 获取所有服务器列表 * 获取所有服务器列表
* @return Collection * @return Collection
*/ */
public static function getAllServers() public static function getAllServers(): Collection
{ {
return Server::orderBy('sort', 'ASC') $query = Server::orderBy('sort', 'ASC');
->get()
->transform(function (Server $server) { return $query->get()->append([
$server->loadServerStatus(); 'last_check_at',
return $server; 'last_push_at',
}); 'online',
'is_online',
'available_status',
'cache_key'
]);
} }
/** /**
@@ -32,28 +36,25 @@ class ServerService
*/ */
public static function getAvailableServers(User $user): array public static function getAvailableServers(User $user): array
{ {
return Server::whereJsonContains('group_ids', (string) $user->group_id) $servers = Server::whereJsonContains('group_ids', (string) $user->group_id)
->where('show', true) ->where('show', true)
->orderBy('sort', 'ASC') ->orderBy('sort', 'ASC')
->get() ->get()
->transform(function (Server $server) use ($user) { ->append(['last_check_at', 'last_push_at', 'online', 'is_online', 'available_status', 'cache_key', 'server_key']);
$server->loadParentCreatedAt();
$server->handlePortAllocation();
$server->loadServerStatus();
if ($server->type === 'shadowsocks') {
$server->server_key = Helper::getServerKey($server->created_at, 16);
}
$server->generateShadowsocksPassword($user);
return $server; $servers = collect($servers)->map(function ($server) use ($user) {
}) // 判断动态端口
->toArray(); if (str_contains($server->port, '-')) {
$server->port = (string) Helper::randomPort($server->port);
$server->ports = $server->port;
}
$server->password = $server->generateShadowsocksPassword($user);
return $server;
})->toArray();
return $servers;
} }
/**
* 加
*/
/** /**
* 根据权限组获取可用的用户列表 * 根据权限组获取可用的用户列表
* @param array $groupIds * @param array $groupIds

View File

@@ -122,7 +122,7 @@ class StatisticalService
$key = "{$rate}_{$uid}"; $key = "{$rate}_{$uid}";
$stats[$key] = $stats[$key] ?? [ $stats[$key] = $stats[$key] ?? [
'record_at' => $this->startAt, 'record_at' => $this->startAt,
'server_rate' => number_format($rate, 2, '.', ''), 'server_rate' => number_format((float) $rate, 2, '.', ''),
'u' => 0, 'u' => 0,
'd' => 0, 'd' => 0,
'user_id' => intval($userId), 'user_id' => intval($userId),
@@ -156,27 +156,40 @@ class StatisticalService
/** /**
* 获取缓存中的服务器爆表 * Retrieve server statistics from Redis cache.
*
* @return array<int, array{server_id: int, server_type: string, u: float, d: float}>
*/ */
public function getStatServer() public function getStatServer(): array
{ {
/** @var array<string, array{server_id: int, server_type: string, u: float, d: float}> $stats */
$stats = []; $stats = [];
$statsServer = $this->redis->zrange($this->statServerKey, 0, -1, true); $statsServer = $this->redis->zrange($this->statServerKey, 0, -1, true);
foreach ($statsServer as $member => $value) { foreach ($statsServer as $member => $value) {
list($serverType, $serverId, $type) = explode('_', $member); $parts = explode('_', $member);
if (count($parts) !== 3) {
continue; // Skip malformed members
}
[$serverType, $serverId, $type] = $parts;
if (!in_array($type, ['u', 'd'], true)) {
continue; // Skip invalid types
}
$key = "{$serverType}_{$serverId}"; $key = "{$serverType}_{$serverId}";
if (!isset($stats[$key])) { if (!isset($stats[$key])) {
$stats[$key] = [ $stats[$key] = [
'server_id' => intval($serverId), 'server_id' => (int) $serverId,
'server_type' => $serverType, 'server_type' => $serverType,
'u' => 0, 'u' => 0.0,
'd' => 0, 'd' => 0.0,
]; ];
} }
$stats[$key][$type] += $value; $stats[$key][$type] += (float) $value;
} }
return array_values($stats);
return array_values($stats);
} }
/** /**
@@ -281,25 +294,22 @@ class StatisticalService
->where('record_type', 'd'); ->where('record_type', 'd');
} }
) )
->withSum('stats as u', 'u') // 预加载 u 的总和
->withSum('stats as d', 'd') // 预加载 d 的总和
->get() ->get()
->each(function ($item) { ->map(function ($item) {
$item->u = (int) $item->stats()->sum('u'); return [
$item->d = (int) $item->stats()->sum('d'); 'server_name' => optional($item->parent)->name ?? $item->name,
$item->total = (int) $item->u + $item->d; 'server_id' => $item->id,
$item->server_name = optional($item->parent)->name ?? $item->name; 'server_type' => $item->type,
$item->server_id = $item->id; 'u' => (int) $item->u,
$item->server_type = $item->type; 'd' => (int) $item->d,
'total' => (int) $item->u + (int) $item->d,
];
}) })
->sortByDesc('total') ->sortByDesc('total')
->select([ ->values()
'server_name', ->toArray();
'server_id',
'server_type',
'u',
'd',
'total'
])
->values()->toArray();
return $statistics; return $statistics;
} }

View File

@@ -156,13 +156,17 @@ class UserOnlineService
} }
/** /**
* 计算设备数量 * Calculate the number of devices based on IPs array and device limit mode.
*
* @param array $ipsArray Array containing IP data
* @return int Number of devices
*/ */
private function calculateDeviceCount(array $ipsArray): int private function calculateDeviceCount(array $ipsArray): int
{ {
// 设备限制模式 $mode = (int) admin_setting('device_limit_mode', 0);
return match ((int) admin_setting('device_limit_mode', 0)) {
// 宽松模式 return match ($mode) {
// Loose mode: Count unique IPs (ignoring suffixes after '_')
1 => collect($ipsArray) 1 => collect($ipsArray)
->filter(fn(mixed $data): bool => is_array($data) && isset($data['aliveips'])) ->filter(fn(mixed $data): bool => is_array($data) && isset($data['aliveips']))
->flatMap( ->flatMap(
@@ -173,9 +177,12 @@ class UserOnlineService
) )
->unique() ->unique()
->count(), ->count(),
// Strict mode: Sum total number of alive IPs
0 => collect($ipsArray) 0 => collect($ipsArray)
->filter(fn(mixed $data): bool => is_array($data) && isset($data['aliveips'])) ->filter(fn(mixed $data): bool => is_array($data) && isset($data['aliveips']))
->sum(fn(array $data): int => count($data['aliveips'])) ->sum(fn(array $data): int => count($data['aliveips'])),
// Handle invalid modes
default => throw new \InvalidArgumentException("Invalid device limit mode: $mode"),
}; };
} }
} }

View File

@@ -12,7 +12,7 @@ use App\Services\Plugin\HookManager;
class UserService class UserService
{ {
private function calcResetDayByMonthFirstDay() private function calcResetDayByMonthFirstDay(): int
{ {
$today = date('d'); $today = date('d');
$lastDay = date('d', strtotime('last day of +0 months')); $lastDay = date('d', strtotime('last day of +0 months'));
@@ -51,55 +51,34 @@ class UserService
return (int) (($nextYear - time()) / 86400); return (int) (($nextYear - time()) / 86400);
} }
public function getResetDay(User $user) public function getResetDay(User $user): ?int
{ {
if (!isset($user->plan)) { // 前置条件检查
$user->plan = Plan::find($user->plan_id); if ($user->expired_at <= time() || $user->expired_at === null) {
}
if ($user->expired_at <= time() || $user->expired_at === NULL)
return null; return null;
// if reset method is not reset
if ($user->plan->reset_traffic_method === 2)
return null;
switch (true) {
case ($user->plan->reset_traffic_method === NULL): {
$resetTrafficMethod = admin_setting('reset_traffic_method', 0);
switch ((int) $resetTrafficMethod) {
// month first day
case 0:
return $this->calcResetDayByMonthFirstDay();
// expire day
case 1:
return $this->calcResetDayByExpireDay($user->expired_at);
// no action
case 2:
return null;
// year first day
case 3:
return $this->calcResetDayByYearFirstDay();
// year expire day
case 4:
return $this->calcResetDayByYearExpiredAt($user->expired_at);
}
break;
}
case ($user->plan->reset_traffic_method === 0): {
return $this->calcResetDayByMonthFirstDay();
}
case ($user->plan->reset_traffic_method === 1): {
return $this->calcResetDayByExpireDay($user->expired_at);
}
case ($user->plan->reset_traffic_method === 2): {
return null;
}
case ($user->plan->reset_traffic_method === 3): {
return $this->calcResetDayByYearFirstDay();
}
case ($user->plan->reset_traffic_method === 4): {
return $this->calcResetDayByYearExpiredAt($user->expired_at);
}
} }
return null;
// 获取重置方式逻辑统一
$resetMethod = $user->plan->reset_traffic_method === Plan::RESET_TRAFFIC_FOLLOW_SYSTEM
? (int)admin_setting('reset_traffic_method', 0)
: $user->plan->reset_traffic_method;
// 验证重置方式有效性
if (!in_array($resetMethod, array_keys(Plan::getResetTrafficMethods()), true)) {
return null;
}
// 方法映射表
$methodHandlers = [
Plan::RESET_TRAFFIC_FIRST_DAY_MONTH => fn() => $this->calcResetDayByMonthFirstDay(),
Plan::RESET_TRAFFIC_MONTHLY => fn() => $this->calcResetDayByExpireDay($user->expired_at),
Plan::RESET_TRAFFIC_FIRST_DAY_YEAR => fn() => $this->calcResetDayByYearFirstDay(),
Plan::RESET_TRAFFIC_YEARLY => fn() => $this->calcResetDayByYearExpiredAt($user->expired_at),
];
$handler = $methodHandlers[$resetMethod] ?? null;
return $handler ? $handler() : null;
} }
public function isAvailable(User $user) public function isAvailable(User $user)

View File

@@ -32,10 +32,11 @@ class Setting
/** /**
* 设置配置信息. * 设置配置信息.
* *
* @param array $data * @param string $key
* @return $this * @param mixed $value
* @return bool 设置是否成功
*/ */
public function set($key, $value = null): bool public function set(string $key, $value = null): bool
{ {
if (is_array($value)) { if (is_array($value)) {
$value = json_encode($value); $value = json_encode($value);
@@ -50,12 +51,12 @@ class Setting
/** /**
* 保存配置到数据库. * 保存配置到数据库.
* *
* @param array $data * @param array $settings 要保存的设置数组
* @return $this * @return bool 保存是否成功
*/ */
public function save(array $data = []): bool public function save(array $settings): bool
{ {
foreach ($data as $key => $value) { foreach ($settings as $key => $value) {
$this->set($key, $value); $this->set($key, $value);
} }
@@ -99,4 +100,22 @@ class Setting
{ {
return $this->fromDatabase(); return $this->fromDatabase();
} }
/**
* 更新单个设置项
*
* @param string $key 设置键名
* @param mixed $value 设置值
* @return bool 更新是否成功
*/
public function update(string $key, $value): bool
{
if (is_array($value)) {
$value = json_encode($value);
}
$key = strtolower($key);
SettingModel::updateOrCreate(['name' => $key], ['value' => $value]);
$this->cache->forget(self::CACHE_KEY);
return true;
}
} }

View File

@@ -31,6 +31,22 @@ class CacheKey
'MULTI_SERVER_TUIC_ONLINE_USER' => 'TUIC节点多服务器在线用户', 'MULTI_SERVER_TUIC_ONLINE_USER' => 'TUIC节点多服务器在线用户',
'SERVER_TUIC_LAST_CHECK_AT' => 'TUIC节点最后检查时间', 'SERVER_TUIC_LAST_CHECK_AT' => 'TUIC节点最后检查时间',
'SERVER_TUIC_LAST_PUSH_AT' => 'TUIC节点最后推送时间', 'SERVER_TUIC_LAST_PUSH_AT' => 'TUIC节点最后推送时间',
'SERVER_SOCKS_ONLINE_USER' => 'socks节点在线用户',
'MULTI_SERVER_SOCKS_ONLINE_USER' => 'socks节点多服务器在线用户',
'SERVER_SOCKS_LAST_CHECK_AT' => 'socks节点最后检查时间',
'SERVER_SOCKS_LAST_PUSH_AT' => 'socks节点最后推送时间',
'SERVER_NAIVE_ONLINE_USER' => 'naive节点在线用户',
'MULTI_SERVER_NAIVE_ONLINE_USER' => 'naive节点多服务器在线用户',
'SERVER_NAIVE_LAST_CHECK_AT' => 'naive节点最后检查时间',
'SERVER_NAIVE_LAST_PUSH_AT' => 'naive节点最后推送时间',
'SERVER_HTTP_ONLINE_USER' => 'http节点在线用户',
'MULTI_SERVER_HTTP_ONLINE_USER' => 'http节点多服务器在线用户',
'SERVER_HTTP_LAST_CHECK_AT' => 'http节点最后检查时间',
'SERVER_HTTP_LAST_PUSH_AT' => 'http节点最后推送时间',
'SERVER_MIERU_ONLINE_USER' => 'mieru节点在线用户',
'MULTI_SERVER_MIERU_ONLINE_USER' => 'mieru节点多服务器在线用户',
'SERVER_MIERU_LAST_CHECK_AT' => 'mieru节点最后检查时间',
'SERVER_MIERU_LAST_PUSH_AT' => 'mieru节点最后推送时间',
'TEMP_TOKEN' => '临时令牌', 'TEMP_TOKEN' => '临时令牌',
'LAST_SEND_EMAIL_REMIND_TRAFFIC' => '最后发送流量邮件提醒', 'LAST_SEND_EMAIL_REMIND_TRAFFIC' => '最后发送流量邮件提醒',
'SCHEDULE_LAST_CHECK_AT' => '计划任务最后检查时间', 'SCHEDULE_LAST_CHECK_AT' => '计划任务最后检查时间',

View File

@@ -134,7 +134,7 @@ class Helper
public static function randomPort($range): int { public static function randomPort($range): int {
$portRange = explode('-', $range); $portRange = explode('-', $range);
return random_int($portRange[0], $portRange[1]); return random_int((int)$portRange[0], (int)$portRange[1]);
} }
public static function base64EncodeUrlSafe($data) public static function base64EncodeUrlSafe($data)

View File

@@ -37,8 +37,10 @@
"require-dev": { "require-dev": {
"barryvdh/laravel-debugbar": "^3.9", "barryvdh/laravel-debugbar": "^3.9",
"fakerphp/faker": "^1.9.1", "fakerphp/faker": "^1.9.1",
"larastan/larastan": "^3.0",
"mockery/mockery": "^1.6", "mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.0", "nunomaduro/collision": "^8.0",
"nunomaduro/larastan": "^3.1",
"orangehill/iseed": "^3.0", "orangehill/iseed": "^3.0",
"phpunit/phpunit": "^10.5", "phpunit/phpunit": "^10.5",
"spatie/laravel-ignition": "^2.4" "spatie/laravel-ignition": "^2.4"

17
phpstan.neon Normal file
View File

@@ -0,0 +1,17 @@
includes:
- vendor/larastan/larastan/extension.neon
- vendor/nesbot/carbon/extension.neon
parameters:
paths:
- app/
# Level 10 is the highest level
level: 5
ignoreErrors:
- '#Negated boolean expression is always false\.#'
# excludePaths:
# - ./*/*/FileToBeExcluded.php

File diff suppressed because one or more lines are too long