fix: resolve PHPStan static analysis warnings

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

View File

@@ -2,7 +2,7 @@ name: Docker Build and Publish
on:
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

View File

@@ -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;

View File

@@ -19,7 +19,7 @@ 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)

View File

@@ -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);
// 获取重置方法
$method = $resetMethod['method'];
if ($method === NULL) {
$method = (int) admin_setting('reset_traffic_method', 0);
}
break;
}
case ($resetMethod['method'] === 0): {
$builder = with(clone ($this->builder))->whereIn('plan_id', $planIds);
$this->resetByMonthFirstDay($builder);
break;
}
case ($resetMethod['method'] === 1): {
$builder = with(clone ($this->builder))->whereIn('plan_id', $planIds);
$this->resetByExpireDay($builder);
break;
}
case ($resetMethod['method'] === 2): {
break;
}
case ($resetMethod['method'] === 3): {
$builder = with(clone ($this->builder))->whereIn('plan_id', $planIds);
$this->resetByYearFirstDay($builder);
break;
}
case ($resetMethod['method'] === 4): {
$builder = with(clone ($this->builder))->whereIn('plan_id', $planIds);
$this->resetByExpireYear($builder);
break;
// 跳过不重置的方法
if ($method === 2) {
continue;
}
// 获取该方法的处理器
$handler = $resetHandlers[$method] ?? null;
if (!$handler) {
continue;
}
// 创建查询构建器并执行重置
$userQuery = (clone $this->builder)->whereIn('plan_id', $planIds);
$handler($userQuery);
}
}
private function resetByExpireYear($builder): void
/**
* 按用户年度到期日重置流量
*/
private function resetByExpireYear(Builder $builder): void
{
$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
];
}
$this->resetUsersByDateCondition($builder, function ($user) use ($today) {
return date('m-d', $user->expired_at) === $today;
});
}
foreach ($usersToUpdate as $userData) {
User::where('id', $userData['id'])->update([
'transfer_enable' => (intval($userData['transfer_enable']) * 1073741824),
'u' => 0,
'd' => 0
]);
}
}
private function resetByYearFirstDay($builder): void
/**
* 按新年第一天重置流量
*/
private function resetByYearFirstDay(Builder $builder): void
{
$users = $builder->with('plan')->get();
$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

View File

@@ -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} 进入管理面板,你可以在用户中心修改你的密码。");

View File

@@ -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]);
}
}
}

View File

@@ -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();

View File

@@ -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',

View File

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

View File

@@ -15,10 +15,6 @@ 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)
{
@@ -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;
}
}

View File

@@ -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}\"");

View File

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

View File

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

View File

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

View File

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

View File

@@ -43,16 +43,15 @@ class OrderController extends Controller
$request->validate([
'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())

View File

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

View File

@@ -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');

View File

@@ -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
/** @var \Illuminate\Pagination\LengthAwarePaginator $paginatedResults */
$paginatedResults = $orderModel
->latest('created_at')
->paginate(
perPage: $pageSize,
page: $current
)->through(fn($order) => [
...$order->toArray(),
'period' => PlanService::getLegacyPeriod($order->period)
]),
);
$paginatedResults->getCollection()->transform(function($order) {
$orderArray = $order->toArray();
$orderArray['period'] = PlanService::getLegacyPeriod((string) $order->period);
return $orderArray;
});
return response()->json($paginatedResults);
}
private function applyFiltersAndSorts(Request $request, Builder $builder): void
@@ -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;

View File

@@ -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, '该订阅不存在']);
}
}
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);

View File

@@ -14,14 +14,14 @@ 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);

View File

@@ -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);

View File

@@ -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();
// 获取在线设备数和在线用户数

View File

@@ -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
]);
}
}

View File

@@ -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;

View File

@@ -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,

View File

@@ -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,13 +18,11 @@ 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);

View File

@@ -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'
];
}

View File

@@ -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 = [
//

View File

@@ -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'
];
}

View File

@@ -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 =

View File

@@ -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 = [
//

View File

@@ -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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,18 @@
<?php
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']);
});
}
}

View File

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

View File

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

View File

@@ -21,23 +21,23 @@ class MysqlLoggerHandler extends AbstractProcessingHandler
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']) : '',
'context' => json_encode($record['context']),
'created_at' => $record['datetime']->getTimestamp(),
'updated_at' => $record['datetime']->getTimestamp(),
];
LogModel::insert(
$log
);
LogModel::insert($log);
} catch (\Exception $e) {
// Log::channel('daily')->error($e->getMessage().$e->getFile().$e->getTraceAsString());
}

View File

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

View File

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

View File

@@ -7,7 +7,31 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use 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);
}
}

View File

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

View File

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

View File

@@ -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';

View File

@@ -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';

View File

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

View File

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

View File

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

View File

@@ -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
View File

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

View File

@@ -95,8 +95,8 @@ class Coinbase implements PaymentInterface
$ch = curl_init();
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(

View File

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

View File

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

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

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

View File

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

View File

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

View File

@@ -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";
}
}

View File

@@ -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;

View File

@@ -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 = [

View File

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

View File

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

View File

@@ -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)

View File

@@ -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);

View File

@@ -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);

View File

@@ -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()

View File

@@ -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()

View File

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

View File

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

View File

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

View File

@@ -40,7 +40,13 @@ class AuthService
return $this->user->tokens()->get()->toArray();
}
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
];
}
}

View File

@@ -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])

View File

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

View File

@@ -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
}
}

View File

@@ -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,27 +36,24 @@ 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']);
$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();
}
})->toArray();
/**
* 加
*/
return $servers;
}
/**
* 根据权限组获取可用的用户列表

View File

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

View File

@@ -156,13 +156,17 @@ class UserOnlineService
}
/**
* 计算设备数量
* Calculate the number of devices based on IPs array and device limit mode.
*
* @param array $ipsArray Array containing IP data
* @return int Number of devices
*/
private function calculateDeviceCount(array $ipsArray): int
{
// 设备限制模式
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"),
};
}
}

View File

@@ -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,57 +51,36 @@ 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)
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): {
// 前置条件检查
if ($user->expired_at <= time() || $user->expired_at === null) {
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);
}
}
// 获取重置方式逻辑统一
$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)
{
if (!$user->banned && $user->transfer_enable && ($user->expired_at > time() || $user->expired_at === NULL)) {

View File

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

View File

@@ -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' => '计划任务最后检查时间',

View File

@@ -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)

View File

@@ -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
View File

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

File diff suppressed because one or more lines are too long