mirror of
https://github.com/lkddi/Xboard.git
synced 2026-04-03 10:30:51 +08:00
fix: resolve PHPStan static analysis warnings
This commit is contained in:
15
.github/workflows/docker-publish.yml
vendored
15
.github/workflows/docker-publish.yml
vendored
@@ -2,7 +2,7 @@ name: Docker Build and Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["master"]
|
||||
branches: ["master", "new-dev"]
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
@@ -59,7 +59,12 @@ jobs:
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
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
|
||||
id: get_version
|
||||
@@ -80,10 +85,8 @@ jobs:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ github.repository_owner }}/xboard:master
|
||||
${{ env.REGISTRY }}/${{ github.repository_owner }}/xboard:new
|
||||
${{ env.REGISTRY }}/${{ github.repository_owner }}/xboard:${{ steps.get_version.outputs.version }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
BUILDKIT_INLINE_CACHE=1
|
||||
BUILDKIT_MULTI_PLATFORM=1
|
||||
|
||||
@@ -112,16 +112,13 @@ class CheckCommission extends Command
|
||||
DB::rollBack();
|
||||
return false;
|
||||
}
|
||||
if (!CommissionLog::create([
|
||||
CommissionLog::create([
|
||||
'invite_user_id' => $inviteUserId,
|
||||
'user_id' => $order->user_id,
|
||||
'trade_no' => $order->trade_no,
|
||||
'order_amount' => $order->total_amount,
|
||||
'get_amount' => $commissionBalance
|
||||
])) {
|
||||
DB::rollBack();
|
||||
return false;
|
||||
}
|
||||
]);
|
||||
$inviteUserId = $inviter->invite_user_id;
|
||||
// update order actual commission balance
|
||||
$order->actual_commission_balance = $order->actual_commission_balance + $commissionBalance;
|
||||
|
||||
@@ -19,11 +19,11 @@ class ExportV2Log extends Command
|
||||
public function handle()
|
||||
{
|
||||
$days = $this->argument('days');
|
||||
$date = Carbon::now()->subDays($days)->startOfDay();
|
||||
$date = Carbon::now()->subDays((float) $days)->startOfDay();
|
||||
|
||||
$logs = DB::table('v2_log')
|
||||
->where('created_at', '>=', $date->timestamp)
|
||||
->get();
|
||||
->where('created_at', '>=', $date->timestamp)
|
||||
->get();
|
||||
|
||||
$fileName = "v2_logs_" . Carbon::now()->format('Y_m_d_His') . ".csv";
|
||||
$handle = fopen(storage_path("logs/$fileName"), 'w');
|
||||
@@ -35,19 +35,19 @@ class ExportV2Log extends Command
|
||||
fputcsv($handle, [
|
||||
$log->level,
|
||||
$log->id,
|
||||
$log->title,
|
||||
$log->host,
|
||||
$log->uri,
|
||||
$log->method,
|
||||
$log->data,
|
||||
$log->ip,
|
||||
$log->context,
|
||||
Carbon::createFromTimestamp($log->created_at)->toDateTimeString(),
|
||||
$log->title,
|
||||
$log->host,
|
||||
$log->uri,
|
||||
$log->method,
|
||||
$log->data,
|
||||
$log->ip,
|
||||
$log->context,
|
||||
Carbon::createFromTimestamp($log->created_at)->toDateTimeString(),
|
||||
Carbon::createFromTimestamp($log->updated_at)->toDateTimeString()
|
||||
]);
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
$this->info("日志成功导出到: ". storage_path("logs/$fileName"));
|
||||
$this->info("日志成功导出到: " . storage_path("logs/$fileName"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,30 +5,26 @@ namespace App\Console\Commands;
|
||||
use App\Models\Plan;
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ResetTraffic extends Command
|
||||
{
|
||||
protected $builder;
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var Builder
|
||||
*/
|
||||
protected $builder;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'reset:traffic';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = '流量清空';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
@@ -37,13 +33,13 @@ class ResetTraffic extends Command
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
* 执行流量重置命令
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
ini_set('memory_limit', -1);
|
||||
|
||||
// 按重置方法分组查询所有套餐
|
||||
$resetMethods = Plan::select(
|
||||
DB::raw("GROUP_CONCAT(`id`) as plan_ids"),
|
||||
DB::raw("reset_traffic_method as method")
|
||||
@@ -51,138 +47,117 @@ class ResetTraffic extends Command
|
||||
->groupBy('reset_traffic_method')
|
||||
->get()
|
||||
->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) {
|
||||
$planIds = explode(',', $resetMethod['plan_ids']);
|
||||
switch (true) {
|
||||
case ($resetMethod['method'] === NULL): {
|
||||
$resetTrafficMethod = admin_setting('reset_traffic_method', 0);
|
||||
$builder = with(clone ($this->builder))->whereIn('plan_id', $planIds);
|
||||
switch ((int) $resetTrafficMethod) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 获取重置方法
|
||||
$method = $resetMethod['method'];
|
||||
if ($method === NULL) {
|
||||
$method = (int) admin_setting('reset_traffic_method', 0);
|
||||
}
|
||||
|
||||
// 跳过不重置的方法
|
||||
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
|
||||
{
|
||||
|
||||
$users = $builder->with('plan')->get();
|
||||
$usersToUpdate = [];
|
||||
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
|
||||
]);
|
||||
}
|
||||
$today = date('m-d');
|
||||
$this->resetUsersByDateCondition($builder, function ($user) use ($today) {
|
||||
return date('m-d', $user->expired_at) === $today;
|
||||
});
|
||||
}
|
||||
|
||||
private function resetByYearFirstDay($builder): void
|
||||
/**
|
||||
* 按新年第一天重置流量
|
||||
*/
|
||||
private function resetByYearFirstDay(Builder $builder): void
|
||||
{
|
||||
$users = $builder->with('plan')->get();
|
||||
$usersToUpdate = [];
|
||||
foreach ($users as $user) {
|
||||
if ((string) date('md') === '0101') {
|
||||
$usersToUpdate[] = [
|
||||
'id' => $user->id,
|
||||
'transfer_enable' => $user->plan->transfer_enable
|
||||
];
|
||||
}
|
||||
$isNewYear = date('md') === '0101';
|
||||
if (!$isNewYear) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($usersToUpdate as $userData) {
|
||||
User::where('id', $userData['id'])->update([
|
||||
'transfer_enable' => (intval($userData['transfer_enable']) * 1073741824),
|
||||
'u' => 0,
|
||||
'd' => 0
|
||||
]);
|
||||
}
|
||||
$this->resetAllUsers($builder);
|
||||
}
|
||||
|
||||
private function resetByMonthFirstDay($builder): void
|
||||
/**
|
||||
* 按月初第一天重置流量
|
||||
*/
|
||||
private function resetByMonthFirstDay(Builder $builder): void
|
||||
{
|
||||
$users = $builder->with('plan')->get();
|
||||
$usersToUpdate = [];
|
||||
foreach ($users as $user) {
|
||||
if ((string) date('d') === '01') {
|
||||
$usersToUpdate[] = [
|
||||
'id' => $user->id,
|
||||
'transfer_enable' => $user->plan->transfer_enable
|
||||
];
|
||||
}
|
||||
$isFirstDayOfMonth = date('d') === '01';
|
||||
if (!$isFirstDayOfMonth) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($usersToUpdate as $userData) {
|
||||
User::where('id', $userData['id'])->update([
|
||||
'transfer_enable' => (intval($userData['transfer_enable']) * 1073741824),
|
||||
'u' => 0,
|
||||
'd' => 0
|
||||
]);
|
||||
}
|
||||
$this->resetAllUsers($builder);
|
||||
}
|
||||
private function resetByExpireDay($builder): void
|
||||
|
||||
/**
|
||||
* 按用户到期日重置流量
|
||||
*/
|
||||
private function resetByExpireDay(Builder $builder): void
|
||||
{
|
||||
$lastDay = date('d', strtotime('last day of +0 months'));
|
||||
$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();
|
||||
$usersToUpdate = [];
|
||||
|
||||
foreach ($users as $user) {
|
||||
$expireDay = date('d', $user->expired_at);
|
||||
if ($expireDay === $today || ($today === $lastDay && $expireDay >= $today)) {
|
||||
if ($condition($user)) {
|
||||
$usersToUpdate[] = [
|
||||
'id' => $user->id,
|
||||
'transfer_enable' => $user->plan->transfer_enable
|
||||
|
||||
@@ -50,9 +50,9 @@ class XboardInstall extends Command
|
||||
{
|
||||
try {
|
||||
$isDocker = file_exists('/.dockerenv');
|
||||
$enableSqlite = env('ENABLE_SQLITE', false);
|
||||
$enableRedis = env('ENABLE_REDIS', false);
|
||||
$adminAccount = env('ADMIN_ACCOUNT', '');
|
||||
$enableSqlite = getenv('ENABLE_SQLITE', false);
|
||||
$enableRedis = getenv('ENABLE_REDIS', false);
|
||||
$adminAccount = getenv('ADMIN_ACCOUNT', false);
|
||||
$this->info("__ __ ____ _ ");
|
||||
$this->info("\ \ / /| __ ) ___ __ _ _ __ __| | ");
|
||||
$this->info(" \ \/ / | __ \ / _ \ / _` | '__/ _` | ");
|
||||
@@ -60,7 +60,7 @@ class XboardInstall extends Command
|
||||
$this->info("/_/ \_\|____/ \___/ \__,_|_| \__,_| ");
|
||||
if (
|
||||
(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'))));
|
||||
$this->info("访问 http(s)://你的站点/{$securePath} 进入管理面板,你可以在用户中心修改你的密码。");
|
||||
|
||||
@@ -2,12 +2,10 @@
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\StatServer;
|
||||
use App\Models\StatUser;
|
||||
use App\Services\StatisticalService;
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\Stat;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class XboardStatistics extends Command
|
||||
{
|
||||
@@ -50,67 +48,6 @@ class XboardStatistics extends Command
|
||||
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()
|
||||
{
|
||||
@@ -132,7 +69,7 @@ class XboardStatistics extends Command
|
||||
}
|
||||
Stat::create($data);
|
||||
} catch (\Exception $e) {
|
||||
\Log::error($e->getMessage(), ['exception' => $e]);
|
||||
Log::error($e->getMessage(), ['exception' => $e]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,9 +42,9 @@ class Kernel extends ConsoleKernel
|
||||
// horizon metrics
|
||||
$schedule->command('horizon:snapshot')->everyFiveMinutes()->onOneServer();
|
||||
// backup Timing
|
||||
if (env('ENABLE_AUTO_BACKUP_AND_UPDATE', false)) {
|
||||
$schedule->command('backup:database', ['true'])->daily()->onOneServer();
|
||||
}
|
||||
// if (env('ENABLE_AUTO_BACKUP_AND_UPDATE', false)) {
|
||||
// $schedule->command('backup:database', ['true'])->daily()->onOneServer();
|
||||
// }
|
||||
// 每分钟清理过期的在线状态
|
||||
$schedule->call(function () {
|
||||
app(UserOnlineService::class)->cleanExpiredOnlineStatus();
|
||||
|
||||
@@ -16,7 +16,7 @@ class Handler extends ExceptionHandler
|
||||
/**
|
||||
* A list of the exception types that are not reported.
|
||||
*
|
||||
* @var array
|
||||
* @var array<int, class-string<Throwable>>
|
||||
*/
|
||||
protected $dontReport = [
|
||||
ApiException::class,
|
||||
@@ -26,7 +26,7 @@ class Handler extends ExceptionHandler
|
||||
/**
|
||||
* A list of the inputs that are never flashed for validation exceptions.
|
||||
*
|
||||
* @var array
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $dontFlash = [
|
||||
'password',
|
||||
|
||||
@@ -7,227 +7,89 @@ use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Passport\AuthForget;
|
||||
use App\Http\Requests\Passport\AuthLogin;
|
||||
use App\Http\Requests\Passport\AuthRegister;
|
||||
use App\Jobs\SendEmailJob;
|
||||
use App\Models\InviteCode;
|
||||
use App\Models\Plan;
|
||||
use App\Models\User;
|
||||
use App\Services\Auth\LoginService;
|
||||
use App\Services\Auth\MailLinkService;
|
||||
use App\Services\Auth\RegisterService;
|
||||
use App\Services\AuthService;
|
||||
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 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)
|
||||
{
|
||||
if (!(int)admin_setting('login_with_mail_link_enable')) {
|
||||
return $this->fail([404,null]);
|
||||
}
|
||||
$params = $request->validate([
|
||||
'email' => 'required|email:strict',
|
||||
'redirect' => 'nullable'
|
||||
]);
|
||||
|
||||
if (Cache::get(CacheKey::get('LAST_SEND_LOGIN_WITH_MAIL_LINK_TIMESTAMP', $params['email']))) {
|
||||
return $this->fail([429 ,__('Sending frequently, please try again later')]);
|
||||
[$success, $result] = $this->mailLinkService->handleMailLink(
|
||||
$params['email'],
|
||||
$request->input('redirect')
|
||||
);
|
||||
|
||||
if (!$success) {
|
||||
return $this->fail($result);
|
||||
}
|
||||
|
||||
$user = User::where('email', $params['email'])->first();
|
||||
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);
|
||||
|
||||
return $this->success($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*/
|
||||
public function register(AuthRegister $request)
|
||||
{
|
||||
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 $this->fail([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 $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();
|
||||
}
|
||||
}
|
||||
[$success, $result] = $this->registerService->register($request);
|
||||
|
||||
if (!$success) {
|
||||
return $this->fail($result);
|
||||
}
|
||||
|
||||
// try out
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
$authService = new AuthService($result);
|
||||
return $this->success($authService->generateAuthData());
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*/
|
||||
public function login(AuthLogin $request)
|
||||
{
|
||||
$email = $request->input('email');
|
||||
$password = $request->input('password');
|
||||
|
||||
if ((int)admin_setting('password_limit_enable', 1)) {
|
||||
$passwordErrorCount = (int)Cache::get(CacheKey::get('PASSWORD_ERROR_LIMIT', $email), 0);
|
||||
if ($passwordErrorCount >= (int)admin_setting('password_limit_count', 5)) {
|
||||
return $this->fail([429,__('There are too many password errors, please try again after :minute minutes.', [
|
||||
'minute' => admin_setting('password_limit_expire', 60)
|
||||
])]);
|
||||
}
|
||||
[$success, $result] = $this->loginService->login($email, $password);
|
||||
|
||||
if (!$success) {
|
||||
return $this->fail($result);
|
||||
}
|
||||
|
||||
$user = User::where('email', $email)->first();
|
||||
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);
|
||||
$authService = new AuthService($result);
|
||||
return $this->success($authService->generateAuthData());
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过token登录
|
||||
*/
|
||||
public function token2Login(Request $request)
|
||||
{
|
||||
// 处理直接通过token重定向
|
||||
if ($token = $request->input('token')) {
|
||||
$redirect = '/#/login?verify=' . $token . '&redirect=' . ($request->input('redirect', 'dashboard'));
|
||||
|
||||
@@ -238,9 +100,9 @@ class AuthController extends Controller
|
||||
);
|
||||
}
|
||||
|
||||
// 处理通过验证码登录
|
||||
if ($verify = $request->input('verify')) {
|
||||
$key = CacheKey::get('TEMP_TOKEN', $verify);
|
||||
$userId = Cache::get($key);
|
||||
$userId = $this->mailLinkService->handleTokenLogin($verify);
|
||||
|
||||
if (!$userId) {
|
||||
return response()->json([
|
||||
@@ -248,15 +110,14 @@ class AuthController extends Controller
|
||||
], 400);
|
||||
}
|
||||
|
||||
$user = User::findOrFail($userId);
|
||||
$user = \App\Models\User::find($userId);
|
||||
|
||||
if ($user->banned) {
|
||||
if (!$user) {
|
||||
return response()->json([
|
||||
'message' => __('Your account has been suspended')
|
||||
'message' => __('User not found')
|
||||
], 400);
|
||||
}
|
||||
|
||||
Cache::forget($key);
|
||||
$authService = new AuthService($user);
|
||||
|
||||
return response()->json([
|
||||
@@ -269,6 +130,9 @@ class AuthController extends Controller
|
||||
], 400);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取快速登录URL
|
||||
*/
|
||||
public function getQuickLoginUrl(Request $request)
|
||||
{
|
||||
$authorization = $request->input('auth_data') ?? $request->header('authorization');
|
||||
@@ -287,38 +151,25 @@ class AuthController extends Controller
|
||||
], 401);
|
||||
}
|
||||
|
||||
$code = Helper::guid();
|
||||
$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);
|
||||
}
|
||||
$url = $this->mailLinkService->getQuickLoginUrl($user, $request->input('redirect'));
|
||||
return $this->success($url);
|
||||
}
|
||||
|
||||
/**
|
||||
* 忘记密码处理
|
||||
*/
|
||||
public function forget(AuthForget $request)
|
||||
{
|
||||
$forgetRequestLimitKey = CacheKey::get('FORGET_REQUEST_LIMIT', $request->input('email'));
|
||||
$forgetRequestLimit = (int)Cache::get($forgetRequestLimitKey);
|
||||
if ($forgetRequestLimit >= 3) return $this->fail([429, __('Reset failed, Please try again later')]);
|
||||
if ((string)Cache::get(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email'))) !== (string)$request->input('email_code')) {
|
||||
Cache::put($forgetRequestLimitKey, $forgetRequestLimit ? $forgetRequestLimit + 1 : 1, 300);
|
||||
return $this->fail([400,__('Incorrect email verification code')]);
|
||||
[$success, $result] = $this->loginService->resetPassword(
|
||||
$request->input('email'),
|
||||
$request->input('email_code'),
|
||||
$request->input('password')
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,14 +15,10 @@ use ReCaptcha\ReCaptcha;
|
||||
|
||||
class CommController extends Controller
|
||||
{
|
||||
private function isEmailVerify()
|
||||
{
|
||||
return $this->success((int)admin_setting('email_verify', 0) ? 1 : 0);
|
||||
}
|
||||
|
||||
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'));
|
||||
$recaptchaResp = $recaptcha->verify($request->input('recaptcha_data'));
|
||||
if (!$recaptchaResp->isSuccess()) {
|
||||
@@ -63,12 +59,4 @@ class CommController extends Controller
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,6 +150,20 @@ class UniProxyController extends Controller
|
||||
'socks' => [
|
||||
'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 => []
|
||||
};
|
||||
|
||||
@@ -163,7 +177,7 @@ class UniProxyController extends Controller
|
||||
}
|
||||
|
||||
$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($response)->header('ETag', "\"{$eTag}\"");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -43,16 +43,15 @@ class OrderController extends Controller
|
||||
$request->validate([
|
||||
'trade_no' => 'required|string',
|
||||
]);
|
||||
$order = Order::with('payment')
|
||||
$order = Order::with(['payment','plan'])
|
||||
->where('user_id', $request->user()->id)
|
||||
->where('trade_no', $request->input('trade_no'))
|
||||
->first();
|
||||
if (!$order) {
|
||||
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');
|
||||
if (!$order['plan']) {
|
||||
if (!$order->plan) {
|
||||
return $this->fail([400, __('Subscription plan does not exist')]);
|
||||
}
|
||||
if ($order->surplus_order_ids) {
|
||||
@@ -81,7 +80,7 @@ class OrderController extends Controller
|
||||
// Validate plan purchase
|
||||
$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');
|
||||
$newPeriod = PlanService::getPeriodKey($period);
|
||||
|
||||
@@ -169,12 +168,13 @@ class OrderController extends Controller
|
||||
]);
|
||||
}
|
||||
$payment = Payment::find($method);
|
||||
if (!$payment || $payment->enable !== 1)
|
||||
if (!$payment || !$payment->enable) {
|
||||
return $this->fail([400, __('Payment method is not available')]);
|
||||
}
|
||||
$paymentService = new PaymentService($payment->payment, $payment->id);
|
||||
$order->handling_amount = NULL;
|
||||
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;
|
||||
if (!$order->save())
|
||||
|
||||
@@ -58,11 +58,13 @@ class UserController extends Controller
|
||||
if (!$user) {
|
||||
return $this->fail([400, __('The user does not exist')]);
|
||||
}
|
||||
if (!Helper::multiPasswordVerify(
|
||||
$user->password_algo,
|
||||
$user->password_salt,
|
||||
$request->input('old_password'),
|
||||
$user->password)
|
||||
if (
|
||||
!Helper::multiPasswordVerify(
|
||||
$user->password_algo,
|
||||
$user->password_salt,
|
||||
$request->input('old_password'),
|
||||
$user->password
|
||||
)
|
||||
) {
|
||||
return $this->fail([400, __('The old password is wrong')]);
|
||||
}
|
||||
|
||||
@@ -64,12 +64,6 @@ class ConfigController extends Controller
|
||||
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)
|
||||
{
|
||||
$key = $request->input('key');
|
||||
|
||||
@@ -15,6 +15,7 @@ use App\Utils\Helper;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class OrderController extends Controller
|
||||
{
|
||||
@@ -27,7 +28,7 @@ class OrderController extends Controller
|
||||
if ($order->surplus_order_ids) {
|
||||
$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);
|
||||
}
|
||||
|
||||
@@ -45,17 +46,21 @@ class OrderController extends Controller
|
||||
|
||||
$this->applyFiltersAndSorts($request, $orderModel);
|
||||
|
||||
return response()->json(
|
||||
$orderModel
|
||||
->latest('created_at')
|
||||
->paginate(
|
||||
perPage: $pageSize,
|
||||
page: $current
|
||||
)->through(fn($order) => [
|
||||
...$order->toArray(),
|
||||
'period' => PlanService::getLegacyPeriod($order->period)
|
||||
]),
|
||||
);
|
||||
/** @var \Illuminate\Pagination\LengthAwarePaginator $paginatedResults */
|
||||
$paginatedResults = $orderModel
|
||||
->latest('created_at')
|
||||
->paginate(
|
||||
perPage: $pageSize,
|
||||
page: $current
|
||||
);
|
||||
|
||||
$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
|
||||
@@ -112,8 +117,8 @@ class OrderController extends Controller
|
||||
'lte' => '<=',
|
||||
'like' => 'like',
|
||||
'notlike' => 'not like',
|
||||
'null' => static fn($q) => $q->whereNull($queryField),
|
||||
'notnull' => static fn($q) => $q->whereNotNull($queryField),
|
||||
'null' => static fn($q) => $q->whereNull($field),
|
||||
'notnull' => static fn($q) => $q->whereNotNull($field),
|
||||
default => 'like'
|
||||
}, match (strtolower($operator)) {
|
||||
'like', 'notlike' => "%{$filterValue}%",
|
||||
@@ -184,7 +189,7 @@ class OrderController extends Controller
|
||||
try {
|
||||
$order->update($params);
|
||||
} catch (\Exception $e) {
|
||||
\Log::error($e);
|
||||
Log::error($e);
|
||||
return $this->fail([500, '更新失败']);
|
||||
}
|
||||
|
||||
@@ -215,11 +220,12 @@ class OrderController extends Controller
|
||||
$orderService = new OrderService($order);
|
||||
$order->user_id = $user->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->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;
|
||||
} else if ($user->plan_id !== NULL && $order->plan_id !== $user->plan_id) {
|
||||
$order->type = Order::TYPE_UPGRADE;
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Models\Plan;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class PlanController extends Controller
|
||||
{
|
||||
@@ -58,7 +59,7 @@ class PlanController extends Controller
|
||||
return $this->success(true);
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
\Log::error($e);
|
||||
Log::error($e);
|
||||
return $this->fail([500, '保存失败']);
|
||||
}
|
||||
}
|
||||
@@ -76,12 +77,12 @@ class PlanController extends Controller
|
||||
if (User::where('plan_id', $request->input('id'))->first()) {
|
||||
return $this->fail([400201, '该订阅下存在用户无法删除']);
|
||||
}
|
||||
if ($request->input('id')) {
|
||||
$plan = Plan::find($request->input('id'));
|
||||
if (!$plan) {
|
||||
return $this->fail([400202, '该订阅不存在']);
|
||||
}
|
||||
|
||||
$plan = Plan::find($request->input('id'));
|
||||
if (!$plan) {
|
||||
return $this->fail([400202, '该订阅不存在']);
|
||||
}
|
||||
|
||||
return $this->success($plan->delete());
|
||||
}
|
||||
|
||||
@@ -101,7 +102,7 @@ class PlanController extends Controller
|
||||
try {
|
||||
$plan->update($updateData);
|
||||
} catch (\Exception $e) {
|
||||
\Log::error($e);
|
||||
Log::error($e);
|
||||
return $this->fail([500, '保存失败']);
|
||||
}
|
||||
|
||||
@@ -124,7 +125,7 @@ class PlanController extends Controller
|
||||
DB::commit();
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
\Log::error($e);
|
||||
Log::error($e);
|
||||
return $this->fail([500, '保存失败']);
|
||||
}
|
||||
return $this->success(true);
|
||||
|
||||
@@ -14,15 +14,15 @@ class GroupController extends Controller
|
||||
{
|
||||
public function fetch(Request $request): JsonResponse
|
||||
{
|
||||
|
||||
$serverGroups = ServerGroup::query()
|
||||
->orderByDesc('id')
|
||||
->withCount('users')
|
||||
->get()
|
||||
->transform(function ($group) {
|
||||
$group->server_count = $group->servers()->count();
|
||||
return $group;
|
||||
});
|
||||
->get();
|
||||
|
||||
// 只在需要时手动加载server_count
|
||||
$serverGroups->each(function ($group) {
|
||||
$group->setAttribute('server_count', $group->server_count);
|
||||
});
|
||||
|
||||
return $this->success($serverGroups);
|
||||
}
|
||||
|
||||
@@ -10,12 +10,13 @@ use App\Models\ServerGroup;
|
||||
use App\Services\ServerService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ManageController extends Controller
|
||||
{
|
||||
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['parent'] = $item->parent;
|
||||
return $item;
|
||||
@@ -41,7 +42,7 @@ class ManageController extends Controller
|
||||
DB::commit();
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
\Log::error($e);
|
||||
Log::error($e);
|
||||
return $this->fail([500, '保存失败']);
|
||||
|
||||
}
|
||||
@@ -60,7 +61,7 @@ class ManageController extends Controller
|
||||
$server->update($params);
|
||||
return $this->success(true);
|
||||
} catch (\Exception $e) {
|
||||
\Log::error($e);
|
||||
Log::error($e);
|
||||
return $this->fail([500, '保存失败']);
|
||||
}
|
||||
}
|
||||
@@ -69,7 +70,7 @@ class ManageController extends Controller
|
||||
Server::create($params);
|
||||
return $this->success(true);
|
||||
} catch (\Exception $e) {
|
||||
\Log::error($e);
|
||||
Log::error($e);
|
||||
return $this->fail([500, '创建失败']);
|
||||
}
|
||||
|
||||
@@ -83,7 +84,7 @@ class ManageController extends Controller
|
||||
'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->success(true);
|
||||
|
||||
@@ -25,8 +25,7 @@ class StatController extends Controller
|
||||
{
|
||||
// 获取在线节点数
|
||||
$onlineNodes = Server::all()->filter(function ($server) {
|
||||
$server->loadServerStatus();
|
||||
return $server->is_online;
|
||||
return !!$server->is_online;
|
||||
})->count();
|
||||
// 获取在线设备数和在线用户数
|
||||
$onlineDevices = User::where('t', '>=', time() - 600)
|
||||
@@ -268,8 +267,7 @@ class StatController extends Controller
|
||||
|
||||
// 获取在线节点数
|
||||
$onlineNodes = Server::all()->filter(function ($server) {
|
||||
$server->loadServerStatus();
|
||||
return $server->is_online;
|
||||
return !!$server->is_online;
|
||||
})->count();
|
||||
|
||||
// 获取在线设备数和在线用户数
|
||||
|
||||
@@ -55,13 +55,10 @@ class TicketController extends Controller
|
||||
if (!$ticket) {
|
||||
return $this->fail([400202, '工单不存在']);
|
||||
}
|
||||
$ticket->user = UserController::transformUserData($ticket->user);
|
||||
$ticket->messages->each(function ($message) use ($ticket) {
|
||||
$message->is_me = $message->user_id !== $ticket->user_id;
|
||||
$result = $ticket->toArray();
|
||||
$result['user'] = UserController::transformUserData($ticket->user);
|
||||
|
||||
});
|
||||
|
||||
return $this->success($ticket);
|
||||
return $this->success($result);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,12 +88,16 @@ class TicketController extends Controller
|
||||
perPage: $request->integer('pageSize', 10),
|
||||
page: $request->integer('current', 1)
|
||||
);
|
||||
$tickets->getCollection()->transform(function ($ticket) {
|
||||
$ticket->user = UserController::transformUserData($ticket->user);
|
||||
return $ticket;
|
||||
});
|
||||
|
||||
// 获取items然后映射转换
|
||||
$items = collect($tickets->items())->map(function ($ticket) {
|
||||
$ticketData = $ticket->toArray();
|
||||
$ticketData['user'] = UserController::transformUserData($ticket->user);
|
||||
return $ticketData;
|
||||
})->all();
|
||||
|
||||
return response([
|
||||
'data' => $tickets->items(),
|
||||
'data' => $items,
|
||||
'total' => $tickets->total()
|
||||
]);
|
||||
}
|
||||
@@ -137,4 +138,19 @@ class TicketController extends Controller
|
||||
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
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ class UserController extends Controller
|
||||
// 处理关联查询
|
||||
if (str_contains($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)) {
|
||||
$q->whereIn($relationField, $value);
|
||||
} else if (is_string($value) && str_contains($value, ':')) {
|
||||
@@ -163,7 +163,8 @@ class UserController extends Controller
|
||||
$users = $userModel->orderBy('id', 'desc')
|
||||
->paginate($pageSize, ['*'], 'page', $current);
|
||||
|
||||
$users->getCollection()->transform(function ($user) {
|
||||
/** @phpstan-ignore-next-line */
|
||||
$users->getCollection()->transform(function ($user): array {
|
||||
return self::transformUserData($user);
|
||||
});
|
||||
|
||||
@@ -177,13 +178,14 @@ class UserController extends Controller
|
||||
* Transform user data for response
|
||||
*
|
||||
* @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->balance = $user->balance / 100;
|
||||
$user->commission_balance = $user->commission_balance / 100;
|
||||
$user = $user->toArray();
|
||||
$user['balance'] = $user['balance'] / 100;
|
||||
$user['commission_balance'] = $user['commission_balance'] / 100;
|
||||
$user['subscribe_url'] = Helper::getSubscribeUrl($user['token']);
|
||||
return $user;
|
||||
}
|
||||
|
||||
@@ -235,7 +237,7 @@ class UserController extends Controller
|
||||
|
||||
if (isset($params['banned']) && (int) $params['banned'] === 1) {
|
||||
$authService = new AuthService($user);
|
||||
$authService->removeSession();
|
||||
$authService->removeAllSessions();
|
||||
}
|
||||
if (isset($params['balance'])) {
|
||||
$params['balance'] = $params['balance'] * 100;
|
||||
@@ -263,7 +265,7 @@ class UserController extends Controller
|
||||
{
|
||||
ini_set('memory_limit', '-1');
|
||||
gc_enable(); // 启用垃圾回收
|
||||
|
||||
|
||||
// 优化查询:使用with预加载plan关系,避免N+1问题
|
||||
$query = User::with('plan:id,name')
|
||||
->orderBy('id', 'asc')
|
||||
@@ -278,18 +280,18 @@ class UserController extends Controller
|
||||
'token',
|
||||
'plan_id'
|
||||
]);
|
||||
|
||||
|
||||
$this->applyFiltersAndSorts($request, $query);
|
||||
|
||||
|
||||
$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');
|
||||
|
||||
|
||||
// 添加BOM标记,确保Excel正确显示中文
|
||||
fprintf($output, chr(0xEF).chr(0xBB).chr(0xBF));
|
||||
|
||||
fprintf($output, chr(0xEF) . chr(0xBB) . chr(0xBF));
|
||||
|
||||
// 写入CSV头部
|
||||
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) {
|
||||
try {
|
||||
$row = [
|
||||
@@ -325,11 +327,11 @@ class UserController extends Controller
|
||||
continue; // 继续处理下一条记录
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 清理内存
|
||||
gc_collect_cycles();
|
||||
});
|
||||
|
||||
|
||||
fclose($output);
|
||||
}, $filename, [
|
||||
'Content-Type' => 'text/csv; charset=UTF-8',
|
||||
|
||||
@@ -11,7 +11,7 @@ class Kernel extends HttpKernel
|
||||
*
|
||||
* These middleware are run during every request to your application.
|
||||
*
|
||||
* @var array
|
||||
* @var array<int, class-string>
|
||||
*/
|
||||
protected $middleware = [
|
||||
\Illuminate\Http\Middleware\HandleCors::class,
|
||||
@@ -26,7 +26,7 @@ class Kernel extends HttpKernel
|
||||
/**
|
||||
* The application's route middleware groups.
|
||||
*
|
||||
* @var array
|
||||
* @var array<string, array<int, class-string|string>>
|
||||
*/
|
||||
protected $middlewareGroups = [
|
||||
'web' => [
|
||||
@@ -57,7 +57,7 @@ class Kernel extends HttpKernel
|
||||
*
|
||||
* These middleware may be assigned to groups or used individually.
|
||||
*
|
||||
* @var array
|
||||
* @var array<string, class-string>
|
||||
*/
|
||||
protected $middlewareAliases = [
|
||||
'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.
|
||||
*
|
||||
* @var array
|
||||
* @var array<class-string>
|
||||
*/
|
||||
protected $middlewarePriority = [
|
||||
\Illuminate\Session\Middleware\StartSession::class,
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Http\Middleware;
|
||||
use App\Exceptions\ApiException;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Closure;
|
||||
use App\Models\User;
|
||||
|
||||
class Admin
|
||||
{
|
||||
@@ -17,15 +18,13 @@ class Admin
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
if (!Auth::guard('sanctum')->check()) {
|
||||
throw new ApiException('未登录或登陆已过期', 403);
|
||||
}
|
||||
|
||||
/** @var User|null $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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,17 @@
|
||||
|
||||
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.
|
||||
*
|
||||
* @var array
|
||||
* 维护模式白名单URI
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $except = [
|
||||
//
|
||||
// 示例:
|
||||
// '/api/health-check',
|
||||
// '/status'
|
||||
];
|
||||
}
|
||||
|
||||
@@ -7,9 +7,8 @@ use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
|
||||
class EncryptCookies extends Middleware
|
||||
{
|
||||
/**
|
||||
* The names of the cookies that should not be encrypted.
|
||||
*
|
||||
* @var array
|
||||
* 不需要加密的Cookie名称列表
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $except = [
|
||||
//
|
||||
|
||||
@@ -7,12 +7,13 @@ use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
|
||||
class TrimStrings extends Middleware
|
||||
{
|
||||
/**
|
||||
* The names of the attributes that should not be trimmed.
|
||||
*
|
||||
* @var array
|
||||
* 不需要去除前后空格的字段名
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $except = [
|
||||
'password',
|
||||
'password_confirmation',
|
||||
'encrypted_data',
|
||||
'signature'
|
||||
];
|
||||
}
|
||||
|
||||
@@ -8,9 +8,8 @@ use Illuminate\Http\Request;
|
||||
class TrustProxies extends Middleware
|
||||
{
|
||||
/**
|
||||
* The trusted proxies for this application.
|
||||
*
|
||||
* @var array|string
|
||||
* 可信代理列表
|
||||
* @var array<int, string>|string|null
|
||||
*/
|
||||
protected $proxies = [
|
||||
"173.245.48.0/20",
|
||||
@@ -36,8 +35,7 @@ class TrustProxies extends Middleware
|
||||
];
|
||||
|
||||
/**
|
||||
* The headers that should be used to detect proxies.
|
||||
*
|
||||
* 代理头映射
|
||||
* @var int
|
||||
*/
|
||||
protected $headers =
|
||||
|
||||
@@ -7,16 +7,14 @@ use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
|
||||
class VerifyCsrfToken extends Middleware
|
||||
{
|
||||
/**
|
||||
* Indicates whether the XSRF-TOKEN cookie should be set on the response.
|
||||
*
|
||||
* 是否在响应中设置XSRF-TOKEN cookie
|
||||
* @var bool
|
||||
*/
|
||||
protected $addHttpCookie = true;
|
||||
|
||||
/**
|
||||
* The URIs that should be excluded from CSRF verification.
|
||||
*
|
||||
* @var array
|
||||
* 不需要CSRF验证的URI列表
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $except = [
|
||||
//
|
||||
|
||||
@@ -54,7 +54,19 @@ class ServerSave extends FormRequest
|
||||
'reality_settings.short_id' => 'nullable|string',
|
||||
],
|
||||
'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
|
||||
|
||||
@@ -2,10 +2,14 @@
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use App\Models\Order;
|
||||
use App\Services\PlanService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/**
|
||||
* @mixin Order
|
||||
*/
|
||||
class OrderResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
@@ -17,8 +21,8 @@ class OrderResource extends JsonResource
|
||||
{
|
||||
return [
|
||||
...parent::toArray($request),
|
||||
'period' => PlanService::getLegacyPeriod($this->period),
|
||||
'plan' => PlanResource::make($this->plan),
|
||||
'period' => PlanService::getLegacyPeriod((string)$this->period),
|
||||
'plan' => $this->whenLoaded('plan', fn() => PlanResource::make($this->plan)),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<?php
|
||||
namespace App\Http\Routes\V1;
|
||||
|
||||
use App\Http\Controllers\V1\Client\AppController;
|
||||
use App\Http\Controllers\V1\Client\ClientController;
|
||||
use Illuminate\Contracts\Routing\Registrar;
|
||||
|
||||
class ClientRoute
|
||||
@@ -12,10 +14,10 @@ class ClientRoute
|
||||
'middleware' => 'client'
|
||||
], function ($router) {
|
||||
// Client
|
||||
$router->get('/subscribe', 'V1\\Client\\ClientController@subscribe')->name('client.subscribe.legacy');
|
||||
$router->get('/subscribe', [ClientController::class, 'subscribe'])->name('client.subscribe.legacy');
|
||||
// App
|
||||
$router->get('/app/getConfig', 'V1\\Client\\AppController@getConfig');
|
||||
$router->get('/app/getVersion', 'V1\\Client\\AppController@getVersion');
|
||||
$router->get('/app/getConfig', [AppController::class, 'getConfig']);
|
||||
$router->get('/app/getVersion', [AppController::class, 'getVersion']);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<?php
|
||||
namespace App\Http\Routes\V1;
|
||||
|
||||
use App\Http\Controllers\V1\Passport\AuthController;
|
||||
use App\Http\Controllers\V1\Passport\CommController;
|
||||
use Illuminate\Contracts\Routing\Registrar;
|
||||
|
||||
class PassportRoute
|
||||
@@ -11,15 +13,15 @@ class PassportRoute
|
||||
'prefix' => 'passport'
|
||||
], function ($router) {
|
||||
// Auth
|
||||
$router->post('/auth/register', 'V1\\Passport\\AuthController@register');
|
||||
$router->post('/auth/login', 'V1\\Passport\\AuthController@login');
|
||||
$router->get ('/auth/token2Login', 'V1\\Passport\\AuthController@token2Login');
|
||||
$router->post('/auth/forget', 'V1\\Passport\\AuthController@forget');
|
||||
$router->post('/auth/getQuickLoginUrl', 'V1\\Passport\\AuthController@getQuickLoginUrl');
|
||||
$router->post('/auth/loginWithMailLink', 'V1\\Passport\\AuthController@loginWithMailLink');
|
||||
$router->post('/auth/register', [AuthController::class, 'register']);
|
||||
$router->post('/auth/login', [AuthController::class, 'login']);
|
||||
$router->get('/auth/token2Login', [AuthController::class, 'token2Login']);
|
||||
$router->post('/auth/forget', [AuthController::class, 'forget']);
|
||||
$router->post('/auth/getQuickLoginUrl', [AuthController::class, 'getQuickLoginUrl']);
|
||||
$router->post('/auth/loginWithMailLink', [AuthController::class, 'loginWithMailLink']);
|
||||
// Comm
|
||||
$router->post('/comm/sendEmailVerify', 'V1\\Passport\\CommController@sendEmailVerify');
|
||||
$router->post('/comm/pv', 'V1\\Passport\\CommController@pv');
|
||||
$router->post('/comm/sendEmailVerify', [CommController::class, 'sendEmailVerify']);
|
||||
$router->post('/comm/pv', [CommController::class, 'pv']);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,18 @@
|
||||
<?php
|
||||
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;
|
||||
|
||||
class UserRoute
|
||||
@@ -12,53 +24,53 @@ class UserRoute
|
||||
'middleware' => 'user'
|
||||
], function ($router) {
|
||||
// User
|
||||
$router->get ('/resetSecurity', 'V1\\User\\UserController@resetSecurity');
|
||||
$router->get ('/info', 'V1\\User\\UserController@info');
|
||||
$router->post('/changePassword', 'V1\\User\\UserController@changePassword');
|
||||
$router->post('/update', 'V1\\User\\UserController@update');
|
||||
$router->get ('/getSubscribe', 'V1\\User\\UserController@getSubscribe');
|
||||
$router->get ('/getStat', 'V1\\User\\UserController@getStat');
|
||||
$router->get ('/checkLogin', 'V1\\User\\UserController@checkLogin');
|
||||
$router->post('/transfer', 'V1\\User\\UserController@transfer');
|
||||
$router->post('/getQuickLoginUrl', 'V1\\User\\UserController@getQuickLoginUrl');
|
||||
$router->get ('/getActiveSession', 'V1\\User\\UserController@getActiveSession');
|
||||
$router->post('/removeActiveSession', 'V1\\User\\UserController@removeActiveSession');
|
||||
$router->get('/resetSecurity', [UserController::class, 'resetSecurity']);
|
||||
$router->get('/info', [UserController::class, 'info']);
|
||||
$router->post('/changePassword', [UserController::class, 'changePassword']);
|
||||
$router->post('/update', [UserController::class, 'update']);
|
||||
$router->get('/getSubscribe', [UserController::class, 'getSubscribe']);
|
||||
$router->get('/getStat', [UserController::class, 'getStat']);
|
||||
$router->get('/checkLogin', [UserController::class, 'checkLogin']);
|
||||
$router->post('/transfer', [UserController::class, 'transfer']);
|
||||
$router->post('/getQuickLoginUrl', [UserController::class, 'getQuickLoginUrl']);
|
||||
$router->get('/getActiveSession', [UserController::class, 'getActiveSession']);
|
||||
$router->post('/removeActiveSession', [UserController::class, 'removeActiveSession']);
|
||||
// Order
|
||||
$router->post('/order/save', 'V1\\User\\OrderController@save');
|
||||
$router->post('/order/checkout', 'V1\\User\\OrderController@checkout');
|
||||
$router->get ('/order/check', 'V1\\User\\OrderController@check');
|
||||
$router->get ('/order/detail', 'V1\\User\\OrderController@detail');
|
||||
$router->get ('/order/fetch', 'V1\\User\\OrderController@fetch');
|
||||
$router->get ('/order/getPaymentMethod', 'V1\\User\\OrderController@getPaymentMethod');
|
||||
$router->post('/order/cancel', 'V1\\User\\OrderController@cancel');
|
||||
$router->post('/order/save', [OrderController::class, 'save']);
|
||||
$router->post('/order/checkout', [OrderController::class, 'checkout']);
|
||||
$router->get('/order/check', [OrderController::class, 'check']);
|
||||
$router->get('/order/detail', [OrderController::class, 'detail']);
|
||||
$router->get('/order/fetch', [OrderController::class, 'fetch']);
|
||||
$router->get('/order/getPaymentMethod', [OrderController::class, 'getPaymentMethod']);
|
||||
$router->post('/order/cancel', [OrderController::class, 'cancel']);
|
||||
// Plan
|
||||
$router->get ('/plan/fetch', 'V1\\User\\PlanController@fetch');
|
||||
$router->get('/plan/fetch', [PlanController::class, 'fetch']);
|
||||
// Invite
|
||||
$router->get ('/invite/save', 'V1\\User\\InviteController@save');
|
||||
$router->get ('/invite/fetch', 'V1\\User\\InviteController@fetch');
|
||||
$router->get ('/invite/details', 'V1\\User\\InviteController@details');
|
||||
$router->get('/invite/save', [InviteController::class, 'save']);
|
||||
$router->get('/invite/fetch', [InviteController::class, 'fetch']);
|
||||
$router->get('/invite/details', [InviteController::class, 'details']);
|
||||
// Notice
|
||||
$router->get ('/notice/fetch', 'V1\\User\\NoticeController@fetch');
|
||||
$router->get('/notice/fetch', [NoticeController::class, 'fetch']);
|
||||
// Ticket
|
||||
$router->post('/ticket/reply', 'V1\\User\\TicketController@reply');
|
||||
$router->post('/ticket/close', 'V1\\User\\TicketController@close');
|
||||
$router->post('/ticket/save', 'V1\\User\\TicketController@save');
|
||||
$router->get ('/ticket/fetch', 'V1\\User\\TicketController@fetch');
|
||||
$router->post('/ticket/withdraw', 'V1\\User\\TicketController@withdraw');
|
||||
$router->post('/ticket/reply', [TicketController::class, 'reply']);
|
||||
$router->post('/ticket/close', [TicketController::class, 'close']);
|
||||
$router->post('/ticket/save', [TicketController::class, 'save']);
|
||||
$router->get('/ticket/fetch', [TicketController::class, 'fetch']);
|
||||
$router->post('/ticket/withdraw', [TicketController::class, 'withdraw']);
|
||||
// Server
|
||||
$router->get ('/server/fetch', 'V1\\User\\ServerController@fetch');
|
||||
$router->get('/server/fetch', [ServerController::class, 'fetch']);
|
||||
// Coupon
|
||||
$router->post('/coupon/check', 'V1\\User\\CouponController@check');
|
||||
$router->post('/coupon/check', [CouponController::class, 'check']);
|
||||
// Telegram
|
||||
$router->get ('/telegram/getBotInfo', 'V1\\User\\TelegramController@getBotInfo');
|
||||
$router->get('/telegram/getBotInfo', [TelegramController::class, 'getBotInfo']);
|
||||
// Comm
|
||||
$router->get ('/comm/config', 'V1\\User\\CommController@config');
|
||||
$router->Post('/comm/getStripePublicKey', 'V1\\User\\CommController@getStripePublicKey');
|
||||
$router->get('/comm/config', [CommController::class, 'config']);
|
||||
$router->Post('/comm/getStripePublicKey', [CommController::class, 'getStripePublicKey']);
|
||||
// Knowledge
|
||||
$router->get ('/knowledge/fetch', 'V1\\User\\KnowledgeController@fetch');
|
||||
$router->get ('/knowledge/getCategory', 'V1\\User\\KnowledgeController@getCategory');
|
||||
$router->get('/knowledge/fetch', [KnowledgeController::class, 'fetch']);
|
||||
$router->get('/knowledge/getCategory', [KnowledgeController::class, 'getCategory']);
|
||||
// Stat
|
||||
$router->get ('/stat/getTrafficLog', 'V1\\User\\StatController@getTrafficLog');
|
||||
$router->get('/stat/getTrafficLog', [StatController::class, 'getTrafficLog']);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<?php
|
||||
namespace App\Http\Routes\V2;
|
||||
|
||||
use App\Http\Controllers\V1\Passport\AuthController;
|
||||
use App\Http\Controllers\V1\Passport\CommController;
|
||||
use Illuminate\Contracts\Routing\Registrar;
|
||||
|
||||
class PassportRoute
|
||||
@@ -11,15 +13,15 @@ class PassportRoute
|
||||
'prefix' => 'passport'
|
||||
], function ($router) {
|
||||
// Auth
|
||||
$router->post('/auth/register', 'V1\\Passport\\AuthController@register');
|
||||
$router->post('/auth/login', 'V1\\Passport\\AuthController@login');
|
||||
$router->get ('/auth/token2Login', 'V1\\Passport\\AuthController@token2Login');
|
||||
$router->post('/auth/forget', 'V1\\Passport\\AuthController@forget');
|
||||
$router->post('/auth/getQuickLoginUrl', 'V1\\Passport\\AuthController@getQuickLoginUrl');
|
||||
$router->post('/auth/loginWithMailLink', 'V1\\Passport\\AuthController@loginWithMailLink');
|
||||
$router->post('/auth/register', [AuthController::class, 'register']);
|
||||
$router->post('/auth/login', [AuthController::class, 'login']);
|
||||
$router->get ('/auth/token2Login', [AuthController::class, 'token2Login']);
|
||||
$router->post('/auth/forget', [AuthController::class, 'forget']);
|
||||
$router->post('/auth/getQuickLoginUrl', [AuthController::class, 'getQuickLoginUrl']);
|
||||
$router->post('/auth/loginWithMailLink', [AuthController::class, 'loginWithMailLink']);
|
||||
// Comm
|
||||
$router->post('/comm/sendEmailVerify', 'V1\\Passport\\CommController@sendEmailVerify');
|
||||
$router->post('/comm/pv', 'V1\\Passport\\CommController@pv');
|
||||
$router->post('/comm/sendEmailVerify', [CommController::class, 'sendEmailVerify']);
|
||||
$router->post('/comm/pv', [CommController::class, 'pv']);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
namespace App\Http\Routes\V2;
|
||||
|
||||
use App\Http\Controllers\V1\User\UserController;
|
||||
use Illuminate\Contracts\Routing\Registrar;
|
||||
|
||||
class UserRoute
|
||||
@@ -12,8 +13,8 @@ class UserRoute
|
||||
'middleware' => 'user'
|
||||
], function ($router) {
|
||||
// User
|
||||
$router->get('/resetSecurity', 'V1\\User\\UserController@resetSecurity');
|
||||
$router->get('/info', 'V1\\User\\UserController@info');
|
||||
$router->get('/resetSecurity', [UserController::class, 'resetSecurity']);
|
||||
$router->get('/info', [UserController::class, 'info']);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,28 +17,28 @@ class MysqlLoggerHandler extends AbstractProcessingHandler
|
||||
protected function write(LogRecord $record): void
|
||||
{
|
||||
$record = $record->toArray();
|
||||
try{
|
||||
if(isset($record['context']['exception']) && is_object($record['context']['exception'])){
|
||||
try {
|
||||
if (isset($record['context']['exception']) && is_object($record['context']['exception'])) {
|
||||
$record['context']['exception'] = (array)$record['context']['exception'];
|
||||
}
|
||||
$record['request_data'] = request()->all() ??[];
|
||||
|
||||
$record['request_data'] = request()->all();
|
||||
|
||||
$log = [
|
||||
'title' => $record['message'],
|
||||
'level' => $record['level_name'],
|
||||
'host' => $record['request_host'] ?? request()->getSchemeAndHttpHost(),
|
||||
'uri' => $record['request_uri'] ?? request()->getRequestUri(),
|
||||
'method' => $record['request_method'] ?? request()->getMethod(),
|
||||
'host' => $record['extra']['request_host'] ?? request()->getSchemeAndHttpHost(),
|
||||
'uri' => $record['extra']['request_uri'] ?? request()->getRequestUri(),
|
||||
'method' => $record['extra']['request_method'] ?? request()->getMethod(),
|
||||
'ip' => request()->getClientIp(),
|
||||
'data' => json_encode($record['request_data']) ,
|
||||
'context' => isset($record['context']) ? json_encode($record['context']) : '',
|
||||
'data' => json_encode($record['request_data']),
|
||||
'context' => json_encode($record['context']),
|
||||
'created_at' => $record['datetime']->getTimestamp(),
|
||||
'updated_at' => $record['datetime']->getTimestamp(),
|
||||
];
|
||||
|
||||
LogModel::insert(
|
||||
$log
|
||||
);
|
||||
}catch (\Exception $e){
|
||||
LogModel::insert($log);
|
||||
} catch (\Exception $e) {
|
||||
// Log::channel('daily')->error($e->getMessage().$e->getFile().$e->getTraceAsString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,38 @@
|
||||
namespace App\Models;
|
||||
|
||||
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
|
||||
{
|
||||
protected $table = 'v2_order';
|
||||
@@ -12,7 +43,8 @@ class Order extends Model
|
||||
protected $casts = [
|
||||
'created_at' => 'timestamp',
|
||||
'updated_at' => 'timestamp',
|
||||
'surplus_order_ids' => 'array'
|
||||
'surplus_order_ids' => 'array',
|
||||
'handling_amount' => 'integer'
|
||||
];
|
||||
|
||||
const STATUS_PENDING = 0; // 待支付
|
||||
@@ -40,21 +72,34 @@ class Order extends Model
|
||||
self::TYPE_RESET_TRAFFIC => '流量重置',
|
||||
];
|
||||
|
||||
public function payment()
|
||||
/**
|
||||
* 获取与订单关联的支付方式
|
||||
*/
|
||||
public function payment(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Payment::class, 'payment_id', 'id');
|
||||
}
|
||||
|
||||
public function user()
|
||||
/**
|
||||
* 获取与订单关联的用户
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
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');
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ class Payment extends Model
|
||||
protected $casts = [
|
||||
'created_at' => 'timestamp',
|
||||
'updated_at' => 'timestamp',
|
||||
'config' => 'array'
|
||||
'config' => 'array',
|
||||
'enable' => 'boolean'
|
||||
];
|
||||
}
|
||||
|
||||
@@ -7,7 +7,31 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use InvalidArgumentException;
|
||||
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
|
||||
{
|
||||
use HasFactory;
|
||||
@@ -16,12 +40,12 @@ class Plan extends Model
|
||||
protected $dateFormat = 'U';
|
||||
|
||||
// 定义流量重置方式
|
||||
public const RESET_TRAFFIC_FOLLOW_SYSTEM = 0; // 跟随系统设置
|
||||
public const RESET_TRAFFIC_FIRST_DAY_MONTH = 1; // 每月1号
|
||||
public const RESET_TRAFFIC_MONTHLY = 2; // 按月重置
|
||||
public const RESET_TRAFFIC_NEVER = 3; // 不重置
|
||||
public const RESET_TRAFFIC_FIRST_DAY_YEAR = 4; // 每年1月1日
|
||||
public const RESET_TRAFFIC_YEARLY = 5; // 按年重置
|
||||
public const RESET_TRAFFIC_FOLLOW_SYSTEM = null; // 跟随系统设置
|
||||
public const RESET_TRAFFIC_FIRST_DAY_MONTH = 0; // 每月1号
|
||||
public const RESET_TRAFFIC_MONTHLY = 1; // 按月重置
|
||||
public const RESET_TRAFFIC_NEVER = 2; // 不重置
|
||||
public const RESET_TRAFFIC_FIRST_DAY_YEAR = 3; // 每年1月1日
|
||||
public const RESET_TRAFFIC_YEARLY = 4; // 按年重置
|
||||
|
||||
// 定义价格类型
|
||||
public const PRICE_TYPE_RESET_TRAFFIC = 'reset_traffic'; // 重置流量价格
|
||||
@@ -346,7 +370,7 @@ class Plan extends Model
|
||||
return $this->hasMany(User::class);
|
||||
}
|
||||
|
||||
public function group()
|
||||
public function group(): HasOne
|
||||
{
|
||||
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);
|
||||
$this->prices = $prices;
|
||||
}
|
||||
|
||||
public function order(): HasMany
|
||||
{
|
||||
return $this->hasMany(Order::class);
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,45 @@ use Illuminate\Support\Facades\Cache;
|
||||
use App\Utils\CacheKey;
|
||||
use App\Utils\Helper;
|
||||
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
|
||||
{
|
||||
public const TYPE_HYSTERIA = 'hysteria';
|
||||
@@ -19,6 +57,9 @@ class Server extends Model
|
||||
public const TYPE_TUIC = 'tuic';
|
||||
public const TYPE_SHADOWSOCKS = 'shadowsocks';
|
||||
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_ONLINE_NO_PUSH = 1;
|
||||
public const STATUS_ONLINE = 2;
|
||||
@@ -53,6 +94,9 @@ class Server extends Model
|
||||
self::TYPE_TUIC,
|
||||
self::TYPE_SHADOWSOCKS,
|
||||
self::TYPE_SOCKS,
|
||||
self::TYPE_NAIVE,
|
||||
self::TYPE_HTTP,
|
||||
self::TYPE_MIERU,
|
||||
];
|
||||
|
||||
protected $table = 'v2_server';
|
||||
@@ -143,6 +187,32 @@ class Server extends Model
|
||||
'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;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
$settings = json_decode($value, true) ?? [];
|
||||
@@ -206,53 +263,22 @@ class Server extends Model
|
||||
$this->attributes['protocol_settings'] = json_encode($castedSettings);
|
||||
}
|
||||
|
||||
public function loadParentCreatedAt(): void
|
||||
{
|
||||
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
|
||||
public function generateShadowsocksPassword(User $user): string
|
||||
{
|
||||
if ($this->type !== self::TYPE_SHADOWSOCKS) {
|
||||
return;
|
||||
return $user->uuid;
|
||||
}
|
||||
|
||||
$this->password = $user->uuid;
|
||||
|
||||
$cipher = data_get($this, 'protocol_settings.cipher');
|
||||
if (!$cipher || !isset(self::CIPHER_CONFIGURATIONS[$cipher])) {
|
||||
return;
|
||||
return $user->uuid;
|
||||
}
|
||||
|
||||
$config = self::CIPHER_CONFIGURATIONS[$cipher];
|
||||
$serverKey = Helper::getServerKey($this->created_at, $config['serverKeySize']);
|
||||
$userKey = Helper::uuidToBase64($user->uuid, $config['userKeySize']);
|
||||
$this->password = "{$serverKey}:{$userKey}";
|
||||
return "{$serverKey}:{$userKey}";
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public function getAvailableStatus(): int
|
||||
public function getAvailableStatusAttribute(): int
|
||||
{
|
||||
$now = time();
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 最后检查时间访问器
|
||||
*/
|
||||
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;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,17 @@ namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
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
|
||||
{
|
||||
protected $table = 'v2_server_group';
|
||||
@@ -23,4 +33,14 @@ class ServerGroup extends Model
|
||||
{
|
||||
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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,18 @@ namespace App\Models;
|
||||
|
||||
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
|
||||
{
|
||||
protected $table = 'v2_stat_server';
|
||||
|
||||
@@ -4,6 +4,18 @@ namespace App\Models;
|
||||
|
||||
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
|
||||
{
|
||||
protected $table = 'v2_stat_user';
|
||||
|
||||
@@ -3,7 +3,25 @@
|
||||
namespace App\Models;
|
||||
|
||||
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
|
||||
{
|
||||
protected $table = 'v2_ticket';
|
||||
@@ -21,16 +39,21 @@ class Ticket extends Model
|
||||
self::STATUS_CLOSED => '关闭'
|
||||
];
|
||||
|
||||
public function user()
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id', 'id');
|
||||
}
|
||||
public function messages()
|
||||
|
||||
/**
|
||||
* 关联的工单消息
|
||||
*/
|
||||
public function messages(): HasMany
|
||||
{
|
||||
return $this->hasMany(TicketMessage::class, 'ticket_id', 'id');
|
||||
}
|
||||
|
||||
// 即将删除
|
||||
public function message()
|
||||
public function message(): HasMany
|
||||
{
|
||||
return $this->hasMany(TicketMessage::class, 'ticket_id', 'id');
|
||||
}
|
||||
|
||||
@@ -3,7 +3,20 @@
|
||||
namespace App\Models;
|
||||
|
||||
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
|
||||
{
|
||||
protected $table = 'v2_ticket_message';
|
||||
@@ -13,4 +26,22 @@ class TicketMessage extends Model
|
||||
'created_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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,55 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Utils\Helper;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
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
|
||||
{
|
||||
use HasApiTokens;
|
||||
@@ -14,52 +59,72 @@ class User extends Authenticatable
|
||||
protected $guarded = ['id'];
|
||||
protected $casts = [
|
||||
'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'];
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
// 获取用户套餐
|
||||
public function plan()
|
||||
/**
|
||||
* 获取用户订阅计划
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function plan(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Plan::class, 'plan_id', 'id');
|
||||
}
|
||||
|
||||
public function group()
|
||||
public function group(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ServerGroup::class, 'group_id', 'id');
|
||||
}
|
||||
|
||||
// 获取用户邀请码列表
|
||||
public function codes()
|
||||
public function codes(): HasMany
|
||||
{
|
||||
return $this->hasMany(InviteCode::class, 'user_id', 'id');
|
||||
}
|
||||
|
||||
public function orders()
|
||||
public function orders(): HasMany
|
||||
{
|
||||
return $this->hasMany(Order::class, 'user_id', 'id');
|
||||
}
|
||||
|
||||
public function stat()
|
||||
public function stat(): HasMany
|
||||
{
|
||||
return $this->hasMany(StatUser::class, 'user_id', 'id');
|
||||
}
|
||||
|
||||
// 关联工单列表
|
||||
public function tickets()
|
||||
public function tickets(): HasMany
|
||||
{
|
||||
return $this->hasMany(Ticket::class, 'user_id', 'id');
|
||||
}
|
||||
|
||||
public function parent()
|
||||
public function parent(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(self::class, 'parent_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取订阅链接属性
|
||||
*/
|
||||
public function getSubscribeUrlAttribute(): string
|
||||
{
|
||||
return Helper::getSubscribeUrl($this->token);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,6 @@ class BTCPay implements PaymentInterface
|
||||
|
||||
if (!self::hashEqual($signraturHeader, $computedSignature)) {
|
||||
throw new ApiException('HMAC signature does not match', 400);
|
||||
return false;
|
||||
}
|
||||
|
||||
//get order id store in metadata
|
||||
@@ -112,8 +111,8 @@ class BTCPay implements PaymentInterface
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_HEADER, 0);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_HEADER, false);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 300);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
|
||||
curl_setopt(
|
||||
|
||||
118
app/Payments/BinancePay.php
Normal file
118
app/Payments/BinancePay.php
Normal 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));
|
||||
}
|
||||
}
|
||||
@@ -95,8 +95,8 @@ class Coinbase implements PaymentInterface
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_HEADER, 0);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_HEADER, false);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 300);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
|
||||
curl_setopt(
|
||||
|
||||
71
app/Payments/EPayWxpay.php
Normal file
71
app/Payments/EPayWxpay.php
Normal 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']
|
||||
];
|
||||
}
|
||||
}
|
||||
113
app/Payments/HiiCashPayment.php
Normal file
113
app/Payments/HiiCashPayment.php
Normal 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
132
app/Payments/PayPal.php
Normal 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;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ class Clash implements ProtocolInterface
|
||||
$servers = $this->servers;
|
||||
$user = $this->user;
|
||||
$appName = admin_setting('app_name', 'XBoard');
|
||||
|
||||
|
||||
// 优先从 admin_setting 获取模板
|
||||
$template = admin_setting('subscribe_template_clash');
|
||||
if (empty($template)) {
|
||||
@@ -40,7 +40,7 @@ class Clash implements ProtocolInterface
|
||||
$template = file_get_contents($defaultConfig);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$config = Yaml::parse($template);
|
||||
$proxy = [];
|
||||
$proxies = [];
|
||||
@@ -67,6 +67,14 @@ class Clash implements ProtocolInterface
|
||||
array_push($proxy, self::buildTrojan($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);
|
||||
@@ -171,7 +179,7 @@ class Clash implements ProtocolInterface
|
||||
if (data_get($protocol_settings, 'network_settings.header.type', 'none') !== 'none') {
|
||||
$array['http-opts'] = [
|
||||
'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;
|
||||
@@ -231,9 +239,56 @@ class Clash implements ProtocolInterface
|
||||
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)
|
||||
{
|
||||
return @preg_match($exp, $str);
|
||||
try {
|
||||
return preg_match($exp, $str) === 1;
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function isRegex($exp)
|
||||
@@ -241,6 +296,10 @@ class Clash implements ProtocolInterface
|
||||
if (empty($exp)) {
|
||||
return false;
|
||||
}
|
||||
return @preg_match((string) $exp, '') !== false;
|
||||
try {
|
||||
return preg_match($exp, '') !== false;
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,11 @@ class ClashMeta implements ProtocolInterface
|
||||
private $servers;
|
||||
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->servers = $servers;
|
||||
@@ -78,6 +82,18 @@ class ClashMeta implements ProtocolInterface
|
||||
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']);
|
||||
}
|
||||
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);
|
||||
@@ -176,7 +192,7 @@ class ClashMeta implements ProtocolInterface
|
||||
if (data_get($protocol_settings, 'network_settings.header.type', 'none') !== 'none') {
|
||||
$array['http-opts'] = [
|
||||
'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;
|
||||
@@ -369,9 +385,78 @@ class ClashMeta implements ProtocolInterface
|
||||
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)
|
||||
{
|
||||
return @preg_match($exp, $str);
|
||||
try {
|
||||
return preg_match($exp, $str) === 1;
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function isRegex($exp)
|
||||
@@ -379,6 +464,10 @@ class ClashMeta implements ProtocolInterface
|
||||
if (empty($exp)) {
|
||||
return false;
|
||||
}
|
||||
return @preg_match($exp, '') !== false;
|
||||
try {
|
||||
return preg_match($exp, '') !== false;
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace App\Protocols;
|
||||
|
||||
use App\Contracts\ProtocolInterface;
|
||||
use App\Utils\Helper;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
class General implements ProtocolInterface
|
||||
{
|
||||
public $flags = ['general', 'v2rayn', 'v2rayng', 'passwall', 'ssrplus', 'sagernet'];
|
||||
@@ -45,6 +45,9 @@ class General implements ProtocolInterface
|
||||
if ($item['type'] === 'hysteria') {
|
||||
$uri .= self::buildHysteria($user['uuid'], $item);
|
||||
}
|
||||
if ($item['type'] === 'socks') {
|
||||
$uri .= self::buildSocks($user['uuid'], $item);
|
||||
}
|
||||
}
|
||||
return base64_encode($uri);
|
||||
}
|
||||
@@ -87,8 +90,11 @@ class General implements ProtocolInterface
|
||||
case 'tcp':
|
||||
if (data_get($protocol_settings, 'network_settings.header.type', 'none') !== 'none') {
|
||||
$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['host'] = data_get($protocol_settings, 'network_settings.headers.Host') ? \Arr::random(data_get($protocol_settings, 'network_settings.headers.Host'), ['/']) : null;
|
||||
$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;
|
||||
}
|
||||
break;
|
||||
case 'ws':
|
||||
@@ -249,4 +255,11 @@ class General implements ProtocolInterface
|
||||
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";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -53,7 +53,6 @@ class QuantumultX implements ProtocolInterface
|
||||
'udp-relay=true',
|
||||
"tag={$server['name']}"
|
||||
];
|
||||
$config = array_filter($config);
|
||||
$uri = implode(',', $config);
|
||||
$uri .= "\r\n";
|
||||
return $uri;
|
||||
|
||||
@@ -89,9 +89,9 @@ class Shadowrocket implements ProtocolInterface
|
||||
if ($protocol_settings['tls']) {
|
||||
$config['tls'] = 1;
|
||||
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');
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -100,8 +100,8 @@ class Shadowrocket implements ProtocolInterface
|
||||
case 'tcp':
|
||||
if (data_get($protocol_settings, 'network_settings.header.type', 'none') !== 'none') {
|
||||
$config['obfs'] = data_get($protocol_settings, 'network_settings.header.type');
|
||||
$config['path'] = \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['path'] = \Illuminate\Support\Arr::random(data_get($protocol_settings, 'network_settings.header.request.path', ['/']));
|
||||
$config['obfsParam'] = \Illuminate\Support\Arr::random(data_get($protocol_settings, 'network_settings.header.request.headers.Host', ['www.example.com']));
|
||||
}
|
||||
break;
|
||||
case 'ws':
|
||||
@@ -168,8 +168,8 @@ class Shadowrocket implements ProtocolInterface
|
||||
case 'tcp':
|
||||
if (data_get($protocol_settings, 'network_settings.header.type', 'none') !== 'none') {
|
||||
$config['obfs'] = data_get($protocol_settings, 'network_settings.header.type');
|
||||
$config['path'] = \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['path'] = \Illuminate\Support\Arr::random(data_get($protocol_settings, 'network_settings.header.request.path', ['/']));
|
||||
$config['obfsParam'] = \Illuminate\Support\Arr::random(data_get($protocol_settings, 'network_settings.header.request.headers.Host', ['www.example.com']));
|
||||
}
|
||||
break;
|
||||
case 'ws':
|
||||
@@ -225,6 +225,8 @@ class Shadowrocket implements ProtocolInterface
|
||||
public static function buildHysteria($password, $server)
|
||||
{
|
||||
$protocol_settings = $server['protocol_settings'];
|
||||
$uri = ''; // 初始化变量
|
||||
|
||||
switch (data_get($protocol_settings, 'version')) {
|
||||
case 1:
|
||||
$params = [
|
||||
|
||||
@@ -47,7 +47,7 @@ class Shadowsocks implements ProtocolInterface
|
||||
$subs['version'] = 1;
|
||||
$subs['bytes_used'] = $bytesUsed;
|
||||
$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);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ class SingBox implements ProtocolInterface
|
||||
private $user;
|
||||
private $config;
|
||||
|
||||
public function __construct($user, $servers, array $options = null)
|
||||
public function __construct($user, $servers)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->servers = $servers;
|
||||
@@ -84,6 +84,14 @@ class SingBox implements ProtocolInterface
|
||||
$tuicConfig = $this->buildTuic($this->user['uuid'], $item);
|
||||
$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) {
|
||||
if (in_array($outbound['type'], ['urltest', 'selector'])) {
|
||||
@@ -361,4 +369,58 @@ class SingBox implements ProtocolInterface
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,18 @@ class Stash implements ProtocolInterface
|
||||
array_push($proxy, self::buildTrojan($user['uuid'], $item));
|
||||
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);
|
||||
@@ -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)
|
||||
{
|
||||
if (empty($exp)) {
|
||||
return false;
|
||||
}
|
||||
return @preg_match($exp, '') !== false;
|
||||
try {
|
||||
return preg_match($exp, '') !== false;
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function isMatch($exp, $str)
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Protocols;
|
||||
|
||||
use App\Utils\Helper;
|
||||
use App\Contracts\ProtocolInterface;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
class Surfboard implements ProtocolInterface
|
||||
{
|
||||
@@ -63,7 +64,7 @@ class Surfboard implements ProtocolInterface
|
||||
|
||||
$defaultConfig = base_path() . '/resources/rules/default.surfboard.conf';
|
||||
$customConfig = base_path() . '/resources/rules/custom.surfboard.conf';
|
||||
if (\File::exists($customConfig)) {
|
||||
if (File::exists($customConfig)) {
|
||||
$config = file_get_contents("$customConfig");
|
||||
} else {
|
||||
$config = file_get_contents("$defaultConfig");
|
||||
@@ -127,9 +128,9 @@ class Surfboard implements ProtocolInterface
|
||||
array_push($config, 'tls=true');
|
||||
if (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'));
|
||||
if (isset($tlsSettings['serverName']) && !empty($tlsSettings['serverName']))
|
||||
if (!!data_get($tlsSettings, 'serverName'))
|
||||
array_push($config, "sni={$tlsSettings['serverName']}");
|
||||
}
|
||||
}
|
||||
@@ -161,8 +162,8 @@ class Surfboard implements ProtocolInterface
|
||||
'tfo=true',
|
||||
'udp-relay=true'
|
||||
];
|
||||
if (!empty($protocol_settings['allow_insecure'])) {
|
||||
array_push($config, $protocol_settings['allow_insecure'] ? 'skip-cert-verify=true' : 'skip-cert-verify=false');
|
||||
if (data_get($protocol_settings, 'allow_insecure')) {
|
||||
array_push($config, !!data_get($protocol_settings, 'allow_insecure') ? 'skip-cert-verify=true' : 'skip-cert-verify=false');
|
||||
}
|
||||
$config = array_filter($config);
|
||||
$uri = implode(',', $config);
|
||||
|
||||
@@ -164,7 +164,7 @@ class Surge implements ProtocolInterface
|
||||
'udp-relay=true'
|
||||
];
|
||||
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);
|
||||
$uri = implode(',', $config);
|
||||
@@ -189,7 +189,7 @@ class Surge implements ProtocolInterface
|
||||
'udp-relay=true'
|
||||
];
|
||||
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);
|
||||
$uri = implode(',', $config);
|
||||
|
||||
@@ -8,17 +8,15 @@ use Illuminate\Support\Facades\Gate;
|
||||
class AuthServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* The policy mappings for the application.
|
||||
*
|
||||
* @var array
|
||||
* 策略映射
|
||||
* @var array<class-string, class-string>
|
||||
*/
|
||||
protected $policies = [
|
||||
// 'App\Model' => 'App\Policies\ModelPolicy',
|
||||
];
|
||||
|
||||
/**
|
||||
* Register any authentication / authorization services.
|
||||
*
|
||||
* 注册任何认证/授权服务
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
|
||||
@@ -8,16 +8,14 @@ use Illuminate\Support\Facades\Event;
|
||||
class EventServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* The event listener mappings for the application.
|
||||
*
|
||||
* @var array
|
||||
* 事件监听器映射
|
||||
* @var array<string, array<int, class-string>>
|
||||
*/
|
||||
protected $listen = [
|
||||
];
|
||||
|
||||
/**
|
||||
* Register any events for your application.
|
||||
*
|
||||
* 注册任何事件
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
|
||||
111
app/Services/Auth/LoginService.php
Normal file
111
app/Services/Auth/LoginService.php
Normal 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];
|
||||
}
|
||||
}
|
||||
122
app/Services/Auth/MailLinkService.php
Normal file
122
app/Services/Auth/MailLinkService.php
Normal 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;
|
||||
}
|
||||
}
|
||||
209
app/Services/Auth/RegisterService.php
Normal file
209
app/Services/Auth/RegisterService.php
Normal 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];
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,13 @@ class AuthService
|
||||
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();
|
||||
return true;
|
||||
@@ -54,4 +60,26 @@ class AuthService
|
||||
|
||||
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
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,30 +127,30 @@ class OrderService
|
||||
$inviter = User::find($user->invite_user_id);
|
||||
if (!$inviter)
|
||||
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;
|
||||
switch ((int) $inviter->commission_type) {
|
||||
case 0:
|
||||
$commissionFirstTime = (int) admin_setting('commission_first_time_enable', 1);
|
||||
$isCommission = (!$commissionFirstTime || ($commissionFirstTime && !$this->haveValidOrder($user)));
|
||||
break;
|
||||
case 1:
|
||||
switch ($commissionType) {
|
||||
case User::COMMISSION_TYPE_PERIOD:
|
||||
$isCommission = true;
|
||||
break;
|
||||
case 2:
|
||||
case User::COMMISSION_TYPE_ONETIME:
|
||||
$isCommission = !$this->haveValidOrder($user);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!$isCommission)
|
||||
return;
|
||||
if ($inviter && $inviter->commission_rate) {
|
||||
if ($inviter->commission_rate) {
|
||||
$order->commission_balance = $order->total_amount * ($inviter->commission_rate / 100);
|
||||
} else {
|
||||
$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)
|
||||
->whereNotIn('status', [0, 2])
|
||||
|
||||
@@ -74,10 +74,9 @@ class PlanService
|
||||
|
||||
// 转换周期格式为新版格式
|
||||
$periodKey = self::getPeriodKey($period);
|
||||
$price = $this->plan->prices[$periodKey] ?? null;
|
||||
|
||||
|
||||
// 检查价格时使用新版格式
|
||||
if (!isset($this->plan->prices[$periodKey]) || $this->plan->prices[$periodKey] === NULL) {
|
||||
if ($price === null) {
|
||||
throw new ApiException(__('This payment period cannot be purchased, please choose another period'));
|
||||
}
|
||||
|
||||
|
||||
@@ -45,9 +45,16 @@ class HookManager
|
||||
* @param mixed ...$args 其他参数
|
||||
* @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::removeFilter($hook, $callback);
|
||||
}
|
||||
|
||||
private static function hasHook(string $hook): bool
|
||||
{
|
||||
// Implementation of hasHook method
|
||||
return true; // Placeholder return, actual implementation needed
|
||||
}
|
||||
}
|
||||
@@ -15,14 +15,18 @@ class ServerService
|
||||
* 获取所有服务器列表
|
||||
* @return Collection
|
||||
*/
|
||||
public static function getAllServers()
|
||||
public static function getAllServers(): Collection
|
||||
{
|
||||
return Server::orderBy('sort', 'ASC')
|
||||
->get()
|
||||
->transform(function (Server $server) {
|
||||
$server->loadServerStatus();
|
||||
return $server;
|
||||
});
|
||||
$query = Server::orderBy('sort', 'ASC');
|
||||
|
||||
return $query->get()->append([
|
||||
'last_check_at',
|
||||
'last_push_at',
|
||||
'online',
|
||||
'is_online',
|
||||
'available_status',
|
||||
'cache_key'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,28 +36,25 @@ class ServerService
|
||||
*/
|
||||
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)
|
||||
->orderBy('sort', 'ASC')
|
||||
->get()
|
||||
->transform(function (Server $server) use ($user) {
|
||||
$server->loadParentCreatedAt();
|
||||
$server->handlePortAllocation();
|
||||
$server->loadServerStatus();
|
||||
if ($server->type === 'shadowsocks') {
|
||||
$server->server_key = Helper::getServerKey($server->created_at, 16);
|
||||
}
|
||||
$server->generateShadowsocksPassword($user);
|
||||
->append(['last_check_at', 'last_push_at', 'online', 'is_online', 'available_status', 'cache_key', 'server_key']);
|
||||
|
||||
return $server;
|
||||
})
|
||||
->toArray();
|
||||
$servers = collect($servers)->map(function ($server) use ($user) {
|
||||
// 判断动态端口
|
||||
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
|
||||
|
||||
@@ -122,7 +122,7 @@ class StatisticalService
|
||||
$key = "{$rate}_{$uid}";
|
||||
$stats[$key] = $stats[$key] ?? [
|
||||
'record_at' => $this->startAt,
|
||||
'server_rate' => number_format($rate, 2, '.', ''),
|
||||
'server_rate' => number_format((float) $rate, 2, '.', ''),
|
||||
'u' => 0,
|
||||
'd' => 0,
|
||||
'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 = [];
|
||||
$statsServer = $this->redis->zrange($this->statServerKey, 0, -1, true);
|
||||
|
||||
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}";
|
||||
if (!isset($stats[$key])) {
|
||||
$stats[$key] = [
|
||||
'server_id' => intval($serverId),
|
||||
'server_id' => (int) $serverId,
|
||||
'server_type' => $serverType,
|
||||
'u' => 0,
|
||||
'd' => 0,
|
||||
'u' => 0.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');
|
||||
}
|
||||
)
|
||||
->withSum('stats as u', 'u') // 预加载 u 的总和
|
||||
->withSum('stats as d', 'd') // 预加载 d 的总和
|
||||
->get()
|
||||
->each(function ($item) {
|
||||
$item->u = (int) $item->stats()->sum('u');
|
||||
$item->d = (int) $item->stats()->sum('d');
|
||||
$item->total = (int) $item->u + $item->d;
|
||||
$item->server_name = optional($item->parent)->name ?? $item->name;
|
||||
$item->server_id = $item->id;
|
||||
$item->server_type = $item->type;
|
||||
->map(function ($item) {
|
||||
return [
|
||||
'server_name' => optional($item->parent)->name ?? $item->name,
|
||||
'server_id' => $item->id,
|
||||
'server_type' => $item->type,
|
||||
'u' => (int) $item->u,
|
||||
'd' => (int) $item->d,
|
||||
'total' => (int) $item->u + (int) $item->d,
|
||||
];
|
||||
})
|
||||
->sortByDesc('total')
|
||||
->select([
|
||||
'server_name',
|
||||
'server_id',
|
||||
'server_type',
|
||||
'u',
|
||||
'd',
|
||||
'total'
|
||||
])
|
||||
->values()->toArray();
|
||||
->values()
|
||||
->toArray();
|
||||
return $statistics;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
// 设备限制模式
|
||||
return match ((int) admin_setting('device_limit_mode', 0)) {
|
||||
// 宽松模式
|
||||
$mode = (int) admin_setting('device_limit_mode', 0);
|
||||
|
||||
return match ($mode) {
|
||||
// Loose mode: Count unique IPs (ignoring suffixes after '_')
|
||||
1 => collect($ipsArray)
|
||||
->filter(fn(mixed $data): bool => is_array($data) && isset($data['aliveips']))
|
||||
->flatMap(
|
||||
@@ -173,9 +177,12 @@ class UserOnlineService
|
||||
)
|
||||
->unique()
|
||||
->count(),
|
||||
// Strict mode: Sum total number of alive IPs
|
||||
0 => collect($ipsArray)
|
||||
->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"),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ use App\Services\Plugin\HookManager;
|
||||
|
||||
class UserService
|
||||
{
|
||||
private function calcResetDayByMonthFirstDay()
|
||||
private function calcResetDayByMonthFirstDay(): int
|
||||
{
|
||||
$today = date('d');
|
||||
$lastDay = date('d', strtotime('last day of +0 months'));
|
||||
@@ -51,55 +51,34 @@ class UserService
|
||||
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;
|
||||
// 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)
|
||||
|
||||
@@ -32,10 +32,11 @@ class Setting
|
||||
/**
|
||||
* 设置配置信息.
|
||||
*
|
||||
* @param array $data
|
||||
* @return $this
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @return bool 设置是否成功
|
||||
*/
|
||||
public function set($key, $value = null): bool
|
||||
public function set(string $key, $value = null): bool
|
||||
{
|
||||
if (is_array($value)) {
|
||||
$value = json_encode($value);
|
||||
@@ -50,12 +51,12 @@ class Setting
|
||||
/**
|
||||
* 保存配置到数据库.
|
||||
*
|
||||
* @param array $data
|
||||
* @return $this
|
||||
* @param array $settings 要保存的设置数组
|
||||
* @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);
|
||||
}
|
||||
|
||||
@@ -99,4 +100,22 @@ class Setting
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,22 @@ class CacheKey
|
||||
'MULTI_SERVER_TUIC_ONLINE_USER' => 'TUIC节点多服务器在线用户',
|
||||
'SERVER_TUIC_LAST_CHECK_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' => '临时令牌',
|
||||
'LAST_SEND_EMAIL_REMIND_TRAFFIC' => '最后发送流量邮件提醒',
|
||||
'SCHEDULE_LAST_CHECK_AT' => '计划任务最后检查时间',
|
||||
|
||||
@@ -134,7 +134,7 @@ class Helper
|
||||
|
||||
public static function randomPort($range): int {
|
||||
$portRange = explode('-', $range);
|
||||
return random_int($portRange[0], $portRange[1]);
|
||||
return random_int((int)$portRange[0], (int)$portRange[1]);
|
||||
}
|
||||
|
||||
public static function base64EncodeUrlSafe($data)
|
||||
|
||||
@@ -37,8 +37,10 @@
|
||||
"require-dev": {
|
||||
"barryvdh/laravel-debugbar": "^3.9",
|
||||
"fakerphp/faker": "^1.9.1",
|
||||
"larastan/larastan": "^3.0",
|
||||
"mockery/mockery": "^1.6",
|
||||
"nunomaduro/collision": "^8.0",
|
||||
"nunomaduro/larastan": "^3.1",
|
||||
"orangehill/iseed": "^3.0",
|
||||
"phpunit/phpunit": "^10.5",
|
||||
"spatie/laravel-ignition": "^2.4"
|
||||
|
||||
17
phpstan.neon
Normal file
17
phpstan.neon
Normal 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
|
||||
18
public/assets/admin/assets/index.js
vendored
18
public/assets/admin/assets/index.js
vendored
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user