feat: new xboard

This commit is contained in:
xboard
2025-01-21 14:57:54 +08:00
parent de18cfe596
commit 0f43fff242
373 changed files with 17923 additions and 20264 deletions
+26 -73
View File
@@ -2,103 +2,56 @@
namespace App\Services;
use App\Utils\CacheKey;
use App\Utils\Helper;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use App\Models\User;
use Illuminate\Support\Facades\Cache;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Laravel\Sanctum\PersonalAccessToken;
class AuthService
{
private $user;
private User $user;
public function __construct(User $user)
{
$this->user = $user;
}
public function generateAuthData(Request $request)
public function generateAuthData(): array
{
$guid = Helper::guid();
$authData = JWT::encode([
'id' => $this->user->id,
'session' => $guid,
], config('app.key'), 'HS256');
self::addSession($this->user->id, $guid, [
'ip' => $request->ip(),
'login_at' => time(),
'ua' => $request->userAgent()
]);
// Create a new Sanctum token with device info
$token = $this->user->createToken(
Str::random(20), // token name (device identifier)
['*'], // abilities
now()->addYear() // expiration
);
// Format token: remove ID prefix and add Bearer
$tokenParts = explode('|', $token->plainTextToken);
$formattedToken = 'Bearer ' . ($tokenParts[1] ?? $tokenParts[0]);
return [
'token' => $this->user->token,
'auth_data' => $formattedToken,
'is_admin' => $this->user->is_admin,
'auth_data' => $authData
];
}
public static function decryptAuthData($jwt)
public function getSessions(): array
{
try {
if (!Cache::has($jwt)) {
$data = (array)JWT::decode($jwt, new Key(config('app.key'), 'HS256'));
if (!self::checkSession($data['id'], $data['session'])) return false;
$user = User::select([
'id',
'email',
'is_admin',
'is_staff'
])
->find($data['id']);
if (!$user) return false;
Cache::put($jwt, $user->toArray(), 3600);
}
return Cache::get($jwt);
} catch (\Exception $e) {
return false;
}
return $this->user->tokens()->get()->toArray();
}
private static function checkSession($userId, $session)
public function removeSession(): bool
{
$sessions = (array)Cache::get(CacheKey::get("USER_SESSIONS", $userId)) ?? [];
if (!in_array($session, array_keys($sessions))) return false;
$this->user->tokens()->delete();
return true;
}
private static function addSession($userId, $guid, $meta)
public static function findUserByBearerToken(string $bearerToken): ?User
{
$cacheKey = CacheKey::get("USER_SESSIONS", $userId);
$sessions = (array)Cache::get($cacheKey, []);
$sessions[$guid] = $meta;
if (!Cache::put(
$cacheKey,
$sessions
)) return false;
return true;
}
public function getSessions()
{
return (array)Cache::get(CacheKey::get("USER_SESSIONS", $this->user->id), []);
}
public function removeSession($sessionId)
{
$cacheKey = CacheKey::get("USER_SESSIONS", $this->user->id);
$sessions = (array)Cache::get($cacheKey, []);
unset($sessions[$sessionId]);
if (!Cache::put(
$cacheKey,
$sessions
)) return false;
return true;
}
public function removeAllSession()
{
$cacheKey = CacheKey::get("USER_SESSIONS", $this->user->id);
return Cache::forget($cacheKey);
$token = str_replace('Bearer ', '', $bearerToken);
$accessToken = PersonalAccessToken::findToken($token);
return $accessToken?->tokenable;
}
}
+75 -50
View File
@@ -11,13 +11,13 @@ use Illuminate\Support\Facades\DB;
class OrderService
{
CONST STR_TO_TIME = [
'month_price' => 1,
'quarter_price' => 3,
'half_year_price' => 6,
'year_price' => 12,
'two_year_price' => 24,
'three_year_price' => 36
const STR_TO_TIME = [
Plan::PERIOD_MONTHLY => 1,
Plan::PERIOD_QUARTERLY => 3,
Plan::PERIOD_HALF_YEARLY => 6,
Plan::PERIOD_YEARLY => 12,
Plan::PERIOD_TWO_YEARLY => 24,
Plan::PERIOD_THREE_YEARLY => 36
];
public $order;
public $user;
@@ -36,37 +36,38 @@ class OrderService
if ($order->refund_amount) {
$this->user->balance = $this->user->balance + $order->refund_amount;
}
try{
try {
DB::beginTransaction();
if ($order->surplus_order_ids) {
Order::whereIn('id', $order->surplus_order_ids)->update([
'status' => Order::STATUS_DISCOUNTED
]);
}
switch ((string)$order->period) {
case 'onetime_price':
switch ((string) $order->period) {
case Plan::PERIOD_ONETIME:
$this->buyByOneTime($plan);
break;
case 'reset_price':
case Plan::PERIOD_RESET_TRAFFIC:
$this->buyByResetTraffic();
break;
default:
$this->buyByPeriod($order, $plan);
}
switch ((int)$order->type) {
case 1:
switch ((int) $order->type) {
case Order::STATUS_PROCESSING:
$this->openEvent(admin_setting('new_order_event_id', 0));
break;
case 2:
case Order::TYPE_RENEWAL:
$this->openEvent(admin_setting('renew_order_event_id', 0));
break;
case 3:
case Order::TYPE_UPGRADE:
$this->openEvent(admin_setting('change_order_event_id', 0));
break;
}
$this->setSpeedLimit($plan->speed_limit);
$this->setDeviceLimit($plan->device_limit);
if (!$this->user->save()) {
throw new \Exception('用户信息保存失败');
@@ -76,7 +77,7 @@ class OrderService
throw new \Exception('订单信息保存失败');
}
DB::commit();
}catch(\Exception $e){
} catch (\Exception $e) {
DB::rollBack();
\Log::error($e);
throw new ApiException('开通失败');
@@ -87,12 +88,14 @@ class OrderService
public function setOrderType(User $user)
{
$order = $this->order;
if ($order->period === 'reset_price') {
if ($order->period === Plan::PERIOD_RESET_TRAFFIC) {
$order->type = Order::TYPE_RESET_TRAFFIC;
} else if ($user->plan_id !== NULL && $order->plan_id !== $user->plan_id && ($user->expired_at > time() || $user->expired_at === NULL)) {
if (!(int)admin_setting('plan_change_enable', 1)) throw new ApiException('目前不允许更改订阅,请联系客服或提交工单操作');
if (!(int) admin_setting('plan_change_enable', 1))
throw new ApiException('目前不允许更改订阅,请联系客服或提交工单操作');
$order->type = Order::TYPE_UPGRADE;
if ((int)admin_setting('surplus_enable', 1)) $this->getSurplusValue($user, $order);
if ((int) admin_setting('surplus_enable', 1))
$this->getSurplusValue($user, $order);
if ($order->surplus_amount >= $order->total_amount) {
$order->refund_amount = $order->surplus_amount - $order->total_amount;
$order->total_amount = 0;
@@ -115,17 +118,19 @@ class OrderService
$order->total_amount = $order->total_amount - $order->discount_amount;
}
public function setInvite(User $user):void
public function setInvite(User $user): void
{
$order = $this->order;
if ($user->invite_user_id && ($order->total_amount <= 0)) return;
if ($user->invite_user_id && ($order->total_amount <= 0))
return;
$order->invite_user_id = $user->invite_user_id;
$inviter = User::find($user->invite_user_id);
if (!$inviter) return;
if (!$inviter)
return;
$isCommission = false;
switch ((int)$inviter->commission_type) {
switch ((int) $inviter->commission_type) {
case 0:
$commissionFirstTime = (int)admin_setting('commission_first_time_enable', 1);
$commissionFirstTime = (int) admin_setting('commission_first_time_enable', 1);
$isCommission = (!$commissionFirstTime || ($commissionFirstTime && !$this->haveValidOrder($user)));
break;
case 1:
@@ -136,7 +141,8 @@ class OrderService
break;
}
if (!$isCommission) return;
if (!$isCommission)
return;
if ($inviter && $inviter->commission_rate) {
$order->commission_balance = $order->total_amount * ($inviter->commission_rate / 100);
} else {
@@ -164,19 +170,22 @@ class OrderService
private function getSurplusValueByOneTime(User $user, Order $order)
{
$lastOneTimeOrder = Order::where('user_id', $user->id)
->where('period', 'onetime_price')
->where('period', Plan::PERIOD_ONETIME)
->where('status', Order::STATUS_COMPLETED)
->orderBy('id', 'DESC')
->first();
if (!$lastOneTimeOrder) return;
if (!$lastOneTimeOrder)
return;
$nowUserTraffic = $user->transfer_enable / 1073741824;
if (!$nowUserTraffic) return;
if (!$nowUserTraffic)
return;
$paidTotalAmount = ($lastOneTimeOrder->total_amount + $lastOneTimeOrder->balance_amount);
if (!$paidTotalAmount) return;
if (!$paidTotalAmount)
return;
$trafficUnitPrice = $paidTotalAmount / $nowUserTraffic;
$notUsedTraffic = $nowUserTraffic - (($user->u + $user->d) / 1073741824);
$result = $trafficUnitPrice * $notUsedTraffic;
$orderModel = Order::where('user_id', $user->id)->where('period', '!=', 'reset_price')->where('status', Order::STATUS_COMPLETED);
$orderModel = Order::where('user_id', $user->id)->where('period', '!=', Plan::PERIOD_RESET_TRAFFIC)->where('status', Order::STATUS_COMPLETED);
$order->surplus_amount = $result > 0 ? $result : 0;
$order->surplus_order_ids = array_column($orderModel->get()->toArray(), 'id');
}
@@ -184,29 +193,34 @@ class OrderService
private function getSurplusValueByPeriod(User $user, Order $order)
{
$orders = Order::where('user_id', $user->id)
->whereNotIn('period', ['reset_price', 'onetime_price'])
->whereNotIn('period', [Plan::PERIOD_RESET_TRAFFIC, Plan::PERIOD_ONETIME])
->where('status', Order::STATUS_COMPLETED)
->get()
->toArray();
if (!$orders) return;
if (!$orders)
return;
$orderAmountSum = 0;
$orderMonthSum = 0;
$lastValidateAt = 0;
foreach ($orders as $item) {
$period = self::STR_TO_TIME[$item['period']];
if (strtotime("+{$period} month", $item['created_at']) < time()) continue;
$period = self::STR_TO_TIME[PlanService::getPeriodKey($item['period'])];
if (strtotime("+{$period} month", $item['created_at']) < time())
continue;
$lastValidateAt = $item['created_at'];
$orderMonthSum = $period + $orderMonthSum;
$orderAmountSum = $orderAmountSum + ($item['total_amount'] + $item['balance_amount'] + $item['surplus_amount'] - $item['refund_amount']);
}
if (!$lastValidateAt) return;
if (!$lastValidateAt)
return;
$expiredAtByOrder = strtotime("+{$orderMonthSum} month", $lastValidateAt);
if ($expiredAtByOrder < time()) return;
if ($expiredAtByOrder < time())
return;
$orderSurplusSecond = $expiredAtByOrder - time();
$orderRangeSecond = $expiredAtByOrder - $lastValidateAt;
$avgPrice = $orderAmountSum / $orderRangeSecond;
$orderSurplusAmount = $avgPrice * $orderSurplusSecond;
if (!$orderSurplusSecond || !$orderSurplusAmount) return;
if (!$orderSurplusSecond || !$orderSurplusAmount)
return;
$order->surplus_amount = $orderSurplusAmount > 0 ? $orderSurplusAmount : 0;
$order->surplus_order_ids = array_column($orders, 'id');
}
@@ -214,20 +228,23 @@ class OrderService
public function paid(string $callbackNo)
{
$order = $this->order;
if ($order->status !== Order::STATUS_PENDING) return true;
if ($order->status !== Order::STATUS_PENDING)
return true;
$order->status = Order::STATUS_PROCESSING;
$order->paid_at = time();
$order->callback_no = $callbackNo;
if (!$order->save()) return false;
if (!$order->save())
return false;
try {
OrderHandleJob::dispatchSync($order->trade_no);
} catch (\Exception $e) {
\Log::error($e);
return false;
}
return true;
}
public function cancel():bool
public function cancel(): bool
{
$order = $this->order;
try {
@@ -244,7 +261,7 @@ class OrderService
}
DB::commit();
return true;
}catch(\Exception $e){
} catch (\Exception $e) {
DB::rollBack();
\Log::error($e);
return false;
@@ -256,6 +273,11 @@ class OrderService
$this->user->speed_limit = $speedLimit;
}
private function setDeviceLimit($deviceLimit)
{
$this->user->device_limit = $deviceLimit;
}
private function buyByResetTraffic()
{
$this->user->u = 0;
@@ -265,14 +287,16 @@ class OrderService
private function buyByPeriod(Order $order, Plan $plan)
{
// change plan process
if ((int)$order->type === 3) {
if ((int) $order->type === Order::TYPE_UPGRADE) {
$this->user->expired_at = time();
}
$this->user->transfer_enable = $plan->transfer_enable * 1073741824;
// 从一次性转换到循环
if ($this->user->expired_at === NULL) $this->buyByResetTraffic();
if ($this->user->expired_at === NULL)
$this->buyByResetTraffic();
// 新购
if ($order->type === 1) $this->buyByResetTraffic();
if ($order->type === Order::TYPE_NEW_PURCHASE)
$this->buyByResetTraffic();
$this->user->plan_id = $plan->id;
$this->user->group_id = $plan->group_id;
$this->user->expired_at = $this->getTime($order->period, $this->user->expired_at);
@@ -292,18 +316,19 @@ class OrderService
if ($timestamp < time()) {
$timestamp = time();
}
$str = PlanService::getPeriodKey($str);
switch ($str) {
case 'month_price':
case Plan::PERIOD_MONTHLY:
return strtotime('+1 month', $timestamp);
case 'quarter_price':
case Plan::PERIOD_QUARTERLY:
return strtotime('+3 month', $timestamp);
case 'half_year_price':
case Plan::PERIOD_HALF_YEARLY:
return strtotime('+6 month', $timestamp);
case 'year_price':
case Plan::PERIOD_YEARLY:
return strtotime('+12 month', $timestamp);
case 'two_year_price':
case Plan::PERIOD_TWO_YEARLY:
return strtotime('+24 month', $timestamp);
case 'three_year_price':
case Plan::PERIOD_THREE_YEARLY:
return strtotime('+36 month', $timestamp);
}
}
+10 -2
View File
@@ -60,8 +60,16 @@ class PaymentService
{
$form = $this->payment->form();
$keys = array_keys($form);
foreach ($keys as $key) {
if (isset($this->config[$key])) $form[$key]['value'] = $this->config[$key];
foreach ($form as $key => $field) {
$form[$key] = [
'label' => $field['label'],
'field_name' => $key,
'field_type' => $field['type'],
'type' => $field['type'],
'placeholder' => $field['placeholder'] ?? '',
'value' => $this->config[$key] ?? '',
'select_options' => $field['select_options'] ?? [],
];
}
return $form;
}
+171 -19
View File
@@ -4,38 +4,190 @@ namespace App\Services;
use App\Models\Plan;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use App\Exceptions\ApiException;
use Illuminate\Database\Eloquent\Collection;
class PlanService
{
public $plan;
public Plan $plan;
public function __construct(int $planId)
public function __construct(Plan $plan)
{
$this->plan = Plan::lockForUpdate()->find($planId);
$this->plan = $plan;
}
public function haveCapacity(): bool
/**
* 获取所有可销售的订阅计划列表
* 条件:show 和 sell 为 true,且容量充足
*
* @return Collection
*/
public function getAvailablePlans(): Collection
{
if ($this->plan->capacity_limit === NULL) return true;
$count = self::countActiveUsers();
$count = $count[$this->plan->id]['count'] ?? 0;
return ($this->plan->capacity_limit - $count) > 0;
return Plan::where('show', true)
->where('sell', true)
->orderBy('sort')
->get()
->filter(function ($plan) {
return $this->hasCapacity($plan);
});
}
public static function countActiveUsers()
/**
* 获取指定订阅计划的可用状态
* 条件:renew 和 sell 为 true
*
* @param int $planId
* @return Plan|null
*/
public function getAvailablePlan(int $planId): ?Plan
{
return User::select(
DB::raw("plan_id"),
DB::raw("count(*) as count")
)
->where('plan_id', '!=', NULL)
return Plan::where('id', $planId)
->where('sell', true)
->where('renew', true)
->first();
}
/**
* 检查指定计划是否可用于指定用户
*
* @param Plan $plan
* @param User $user
* @return bool
*/
public function isPlanAvailableForUser(Plan $plan, User $user): bool
{
// 如果是续费
if ($user->plan_id === $plan->id) {
return $plan->renew;
}
// 如果是新购
return $plan->show && $plan->sell && $this->hasCapacity($plan);
}
public function validatePurchase(User $user, string $period): void
{
if (!$this->plan) {
throw new ApiException(__('Subscription plan does not exist'));
}
// 转换周期格式为新版格式
$periodKey = self::getPeriodKey($period);
if ($periodKey === Plan::PERIOD_RESET_TRAFFIC) {
$this->validateResetTrafficPurchase($user);
return;
}
// 检查价格时使用新版格式
if (!isset($this->plan->prices[$periodKey])) {
throw new ApiException(__('This payment period cannot be purchased, please choose another period'));
}
if ($user->plan_id !== $this->plan->id && !$this->hasCapacity($this->plan)) {
throw new ApiException(__('Current product is sold out'));
}
$this->validatePlanAvailability($user);
}
/**
* 智能转换周期格式为新版格式
* 如果是新版格式直接返回,如果是旧版格式则转换为新版格式
*
* @param string $period
* @return string
*/
public static function getPeriodKey(string $period): string
{
// 如果是新版格式直接返回
if (in_array($period, self::getNewPeriods())) {
return $period;
}
// 如果是旧版格式则转换为新版格式
return Plan::LEGACY_PERIOD_MAPPING[$period] ?? $period;
}
/**
* 只能转换周期格式为旧版本
*/
public static function convertToLegacyPeriod(string $period): string
{
return Plan::LEGACY_PERIOD_MAPPING[$period] ?? $period;
}
/**
* 获取所有支持的新版周期格式
*
* @return array
*/
public static function getNewPeriods(): array
{
return array_values(Plan::LEGACY_PERIOD_MAPPING);
}
/**
* 获取旧版周期格式
*
* @param string $period
* @return string
*/
public static function getLegacyPeriod(string $period): string
{
$flipped = array_flip(Plan::LEGACY_PERIOD_MAPPING);
return $flipped[$period] ?? $period;
}
protected function validateResetTrafficPurchase(User $user): void
{
if (!app(UserService::class)->isAvailable($user) || $this->plan->id !== $user->plan_id) {
throw new ApiException(__('Subscription has expired or no active subscription, unable to purchase Data Reset Package'));
}
}
protected function validatePlanAvailability(User $user): void
{
if ((!$this->plan->show && !$this->plan->renew) || (!$this->plan->show && $user->plan_id !== $this->plan->id)) {
throw new ApiException(__('This subscription has been sold out, please choose another subscription'));
}
if (!$this->plan->renew && $user->plan_id == $this->plan->id) {
throw new ApiException(__('This subscription cannot be renewed, please change to another subscription'));
}
if (!$this->plan->show && $this->plan->renew && !app(UserService::class)->isAvailable($user)) {
throw new ApiException(__('This subscription has expired, please change to another subscription'));
}
}
public function hasCapacity(Plan $plan): bool
{
if ($plan->capacity_limit === null) {
return true;
}
$activeUserCount = User::where('plan_id', $plan->id)
->where(function ($query) {
$query->where('expired_at', '>=', time())
->orWhere('expired_at', NULL);
->orWhereNull('expired_at');
})
->groupBy("plan_id")
->get()
->keyBy('plan_id');
->count();
return ($plan->capacity_limit - $activeUserCount) > 0;
}
public function getAvailablePeriods(Plan $plan): array
{
return array_filter(
$plan->getActivePeriods(),
fn($period) => isset($plan->prices[$period]) && $plan->prices[$period] > 0
);
}
public function canResetTraffic(Plan $plan): bool
{
return $plan->reset_traffic_method !== Plan::RESET_TRAFFIC_NEVER
&& $plan->getResetTrafficPrice() > 0;
}
}
+56
View File
@@ -0,0 +1,56 @@
<?php
namespace App\Services\Plugin;
abstract class AbstractPlugin
{
protected array $config = [];
/**
* 插件启动时调用
*/
public function boot(): void
{
// 子类实现具体逻辑
}
/**
* 插件禁用时调用
*/
public function cleanup(): void
{
// 子类实现具体逻辑
}
/**
* 设置配置
*/
public function setConfig(array $config): void
{
$this->config = $config;
}
/**
* 获取配置
*/
public function getConfig(): array
{
return $this->config;
}
/**
* 注册事件监听器
*/
protected function listen(string $hook, callable $callback): void
{
HookManager::register($hook, $callback);
}
/**
* 移除事件监听器
*/
protected function removeListener(string $hook): void
{
HookManager::remove($hook);
}
}
+43
View File
@@ -0,0 +1,43 @@
<?php
namespace App\Services\Plugin;
use Illuminate\Support\Facades\Event;
class HookManager
{
/**
* 触发钩子
*
* @param string $hook 钩子名称
* @param mixed $payload 传递给钩子的数据
* @return mixed
*/
public static function call(string $hook, mixed $payload = null): mixed
{
return Event::dispatch($hook, [$payload]);
}
/**
* 注册钩子监听器
*
* @param string $hook 钩子名称
* @param callable $callback 回调函数
* @return void
*/
public static function register(string $hook, callable $callback): void
{
Event::listen($hook, $callback);
}
/**
* 移除钩子监听器
*
* @param string $hook 钩子名称
* @return void
*/
public static function remove(string $hook): void
{
Event::forget($hook);
}
}
+103
View File
@@ -0,0 +1,103 @@
<?php
namespace App\Services\Plugin;
use App\Models\Plugin;
use Illuminate\Support\Facades\File;
class PluginConfigService
{
/**
* 获取插件配置
*
* @param string $pluginCode
* @return array
*/
public function getConfig(string $pluginCode): array
{
$defaultConfig = $this->getDefaultConfig($pluginCode);
if (empty($defaultConfig)) {
return [];
}
$dbConfig = $this->getDbConfig($pluginCode);
$result = [];
foreach ($defaultConfig as $key => $item) {
$result[$key] = [
'type' => $item['type'],
'label' => $item['label'] ?? '',
'placeholder' => $item['placeholder'] ?? '',
'description' => $item['description'] ?? '',
'value' => $dbConfig[$key] ?? $item['default']
];
}
return $result;
}
/**
* 更新插件配置
*
* @param string $pluginCode
* @param array $config
* @return bool
*/
public function updateConfig(string $pluginCode, array $config): bool
{
$defaultConfig = $this->getDefaultConfig($pluginCode);
if (empty($defaultConfig)) {
throw new \Exception('插件配置结构不存在');
}
$values = [];
foreach ($config as $key => $value) {
if (!isset($defaultConfig[$key])) {
continue;
}
$values[$key] = $value;
}
Plugin::query()
->where('code', $pluginCode)
->update([
'config' => json_encode($values),
'updated_at' => now()
]);
return true;
}
/**
* 获取插件默认配置
*
* @param string $pluginCode
* @return array
*/
protected function getDefaultConfig(string $pluginCode): array
{
$configFile = base_path("plugins/{$pluginCode}/config.json");
if (!File::exists($configFile)) {
return [];
}
$config = json_decode(File::get($configFile), true);
return $config['config'] ?? [];
}
/**
* 获取数据库中的配置
*
* @param string $pluginCode
* @return array
*/
protected function getDbConfig(string $pluginCode): array
{
$plugin = Plugin::query()
->where('code', $pluginCode)
->first();
if (!$plugin || empty($plugin->config)) {
return [];
}
return json_decode($plugin->config, true);
}
}
+180
View File
@@ -0,0 +1,180 @@
<?php
namespace App\Services\Plugin;
use App\Models\Plugin;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
class PluginManager
{
protected string $pluginPath;
public function __construct()
{
$this->pluginPath = base_path('plugins');
}
/**
* 安装插件
*/
public function install(string $pluginCode): bool
{
$configFile = $this->pluginPath . '/' . $pluginCode . '/config.json';
if (!File::exists($configFile)) {
throw new \Exception('Plugin config file not found');
}
$config = json_decode(File::get($configFile), true);
if (!$this->validateConfig($config)) {
throw new \Exception('Invalid plugin config');
}
// 检查依赖
if (!$this->checkDependencies($config['require'] ?? [])) {
throw new \Exception('Dependencies not satisfied');
}
// 提取配置默认值
$defaultValues = [];
if (isset($config['config']) && is_array($config['config'])) {
foreach ($config['config'] as $key => $item) {
$defaultValues[$key] = $item['default'] ?? null;
}
}
// 注册到数据库
Plugin::create([
'code' => $pluginCode,
'name' => $config['name'],
'version' => $config['version'],
'is_enabled' => false,
'config' => json_encode($defaultValues),
'installed_at' => now(),
]);
return true;
}
/**
* 启用插件
*/
public function enable(string $pluginCode): bool
{
$plugin = $this->loadPlugin($pluginCode);
if (!$plugin) {
throw new \Exception('Plugin not found');
}
// 获取插件配置
$dbPlugin = Plugin::query()
->where('code', $pluginCode)
->first();
if ($dbPlugin && !empty($dbPlugin->config)) {
$plugin->setConfig(json_decode($dbPlugin->config, true));
}
// 更新数据库状态
Plugin::query()
->where('code', $pluginCode)
->update([
'is_enabled' => true,
'updated_at' => now(),
]);
// 加载路由
$routesFile = $this->pluginPath . '/' . $pluginCode . '/routes/web.php';
if (File::exists($routesFile)) {
require $routesFile;
}
// 初始化插件
if (method_exists($plugin, 'boot')) {
$plugin->boot();
}
return true;
}
/**
* 禁用插件
*/
public function disable(string $pluginCode): bool
{
$plugin = $this->loadPlugin($pluginCode);
if (!$plugin) {
throw new \Exception('Plugin not found');
}
// 更新数据库状态
Plugin::query()
->where('code', $pluginCode)
->update([
'is_enabled' => false,
'updated_at' => now(),
]);
// 清理插件
if (method_exists($plugin, 'cleanup')) {
$plugin->cleanup();
}
return true;
}
/**
* 卸载插件
*/
public function uninstall(string $pluginCode): bool
{
// 先禁用插件
$this->disable($pluginCode);
// 删除数据库记录
Plugin::query()->where('code', $pluginCode)->delete();
return true;
}
/**
* 加载插件实例
*/
protected function loadPlugin(string $pluginCode)
{
$pluginFile = $this->pluginPath . '/' . $pluginCode . '/Plugin.php';
if (!File::exists($pluginFile)) {
return null;
}
require_once $pluginFile;
$className = "Plugin\\{$pluginCode}\\Plugin";
return new $className();
}
/**
* 验证配置文件
*/
protected function validateConfig(array $config): bool
{
return isset($config['code'])
&& isset($config['version'])
&& isset($config['description'])
&& isset($config['author']);
}
/**
* 检查依赖关系
*/
protected function checkDependencies(array $requires): bool
{
foreach ($requires as $package => $version) {
if ($package === 'xboard') {
// 检查xboard版本
// 实现版本比较逻辑
}
}
return true;
}
}
+64 -367
View File
@@ -2,202 +2,67 @@
namespace App\Services;
use App\Models\ServerHysteria;
use App\Models\ServerLog;
use App\Models\Server;
use App\Models\ServerRoute;
use App\Models\ServerShadowsocks;
use App\Models\ServerVless;
use App\Models\User;
use App\Models\ServerVmess;
use App\Models\ServerTrojan;
use App\Utils\CacheKey;
use App\Utils\Helper;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
class ServerService
{
// 获取可用的 VLESS 服务器列表
public static function getAvailableVless(User $user): array
{
$servers = [];
$model = ServerVless::orderBy('sort', 'ASC');
$server = $model->get();
foreach ($server as $key => $v) {
if (!$v['show']) continue;
$serverData = $v->toArray();
$serverData['type'] = 'vless';
if (!in_array($user->group_id, $serverData['group_id'])) continue;
if (strpos($serverData['port'], '-') !== false) {
$serverData['port'] = Helper::randomPort($serverData['port']);
}
if ($serverData['parent_id']) {
$serverData['last_check_at'] = Cache::get(CacheKey::get('SERVER_VLESS_LAST_CHECK_AT', $serverData['parent_id']));
} else {
$serverData['last_check_at'] = Cache::get(CacheKey::get('SERVER_VLESS_LAST_CHECK_AT', $serverData['id']));
}
if (isset($serverData['tls_settings'])) {
if (isset($serverData['tls_settings']['private_key'])) {
unset($serverData['tls_settings']['private_key']);
/**
* 获取所有服务器列表
* @return Collection
*/
public static function getAllServers()
{
return Server::orderBy('sort', 'ASC')
->get()
->transform(function (Server $server) {
$server->loadServerStatus();
return $server;
});
}
/**
* 获取指定用户可用的服务器列表
* @param User $user
* @return array
*/
public static function getAvailableServers(User $user): array
{
return 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);
$servers[] = $serverData;
}
return $servers;
return $server;
})
->toArray();
}
// 获取可用的 VMESS 服务器列表
public static function getAvailableVmess(User $user): array
{
$servers = [];
$model = ServerVmess::orderBy('sort', 'ASC');
$vmess = $model->get();
foreach ($vmess as $key => $v) {
if (!$v['show']) continue;
$vmess[$key]['type'] = 'vmess';
if (!in_array($user->group_id, $vmess[$key]['group_id'])) continue;
if (strpos($vmess[$key]['port'], '-') !== false) {
$vmess[$key]['port'] = Helper::randomPort($vmess[$key]['port']);
}
if ($vmess[$key]['parent_id']) {
$vmess[$key]['last_check_at'] = Cache::get(CacheKey::get('SERVER_VMESS_LAST_CHECK_AT', $vmess[$key]['parent_id']));
} else {
$vmess[$key]['last_check_at'] = Cache::get(CacheKey::get('SERVER_VMESS_LAST_CHECK_AT', $vmess[$key]['id']));
}
$servers[] = $vmess[$key]->toArray();
}
/**
* 加
*/
return $servers;
}
// 获取可用的 TROJAN 服务器列表
public static function getAvailableTrojan(User $user): array
{
$servers = [];
$model = ServerTrojan::orderBy('sort', 'ASC');
$trojan = $model->get();
foreach ($trojan as $key => $v) {
if (!$v['show']) continue;
$trojan[$key]['type'] = 'trojan';
if (!in_array($user->group_id, $trojan[$key]['group_id'])) continue;
if (strpos($trojan[$key]['port'], '-') !== false) {
$trojan[$key]['port'] = Helper::randomPort($trojan[$key]['port']);
}
if ($trojan[$key]['parent_id']) {
$trojan[$key]['last_check_at'] = Cache::get(CacheKey::get('SERVER_TROJAN_LAST_CHECK_AT', $trojan[$key]['parent_id']));
} else {
$trojan[$key]['last_check_at'] = Cache::get(CacheKey::get('SERVER_TROJAN_LAST_CHECK_AT', $trojan[$key]['id']));
}
$servers[] = $trojan[$key]->toArray();
}
return $servers;
}
// 获取可用的 HYSTERIA 服务器列表
public static function getAvailableHysteria(User $user)
{
$availableServers = [];
$model = ServerHysteria::orderBy('sort', 'ASC');
$servers = $model->get()->keyBy('id');
foreach ($servers as $key => $v) {
if (!$v['show']) continue;
$servers[$key]['type'] = 'hysteria';
$servers[$key]['last_check_at'] = Cache::get(CacheKey::get('SERVER_HYSTERIA_LAST_CHECK_AT', $v['id']));
if (!in_array($user->group_id, $v['group_id'])) continue;
if (strpos($v['port'], '-') !== false) {
$servers[$key]['ports'] = $v['port'];
$servers[$key]['port'] = Helper::randomPort($v['port']);
}
if (isset($servers[$v['parent_id']])) {
$servers[$key]['last_check_at'] = Cache::get(CacheKey::get('SERVER_HYSTERIA_LAST_CHECK_AT', $v['parent_id']));
$servers[$key]['created_at'] = $servers[$v['parent_id']]['created_at'];
}
$servers[$key]['server_key'] = Helper::getServerKey($servers[$key]['created_at'], 16);
$availableServers[] = $servers[$key]->toArray();
}
return $availableServers;
}
// 获取可用的 SHADOWSOCKS 服务器列表
public static function getAvailableShadowsocks(User $user)
{
$servers = [];
$model = ServerShadowsocks::orderBy('sort', 'ASC');
$shadowsocks = $model->get()->keyBy('id');
foreach ($shadowsocks as $key => $v) {
if (!$v['show']) continue;
$shadowsocks[$key]['type'] = 'shadowsocks';
$shadowsocks[$key]['last_check_at'] = Cache::get(CacheKey::get('SERVER_SHADOWSOCKS_LAST_CHECK_AT', $v['id']));
if (!in_array($user->group_id, $v['group_id'])) continue;
if (strpos($v['port'], '-') !== false) {
$shadowsocks[$key]['port'] = Helper::randomPort($v['port']);
}
if (isset($shadowsocks[$v['parent_id']])) {
$shadowsocks[$key]['last_check_at'] = Cache::get(CacheKey::get('SERVER_SHADOWSOCKS_LAST_CHECK_AT', $v['parent_id']));
$shadowsocks[$key]['created_at'] = $shadowsocks[$v['parent_id']]['created_at'];
}
// 处理ss2022密码
$cipherConfiguration = [
'2022-blake3-aes-128-gcm' => [
'serverKeySize' => 16,
'userKeySize' => 16,
],
'2022-blake3-aes-256-gcm' => [
'serverKeySize' => 32,
'userKeySize' => 32,
],
'2022-blake3-chacha20-poly1305' => [
'serverKeySize' => 32,
'userKeySize' => 32,
]
];
$shadowsocks[$key]['password'] = $user['uuid'];
if (array_key_exists($cipher = $v['cipher'], $cipherConfiguration)) {
$config = $cipherConfiguration[$cipher];
$serverKey = Helper::getServerKey($v['created_at'], $config['serverKeySize']);
$userKey = Helper::uuidToBase64($user['uuid'], $config['userKeySize']);
$shadowsocks[$key]['password'] = "{$serverKey}:{$userKey}";
}
if ($v['obfs'] === 'http') {
$shadowsocks[$key]['obfs'] = 'http';
$shadowsocks[$key]['obfs-host'] = $v['obfs_settings']['host'];
$shadowsocks[$key]['obfs-path'] = $v['obfs_settings']['path'];
}
$servers[] = $shadowsocks[$key]->toArray();
}
return $servers;
}
// 获取可用的服务器列表
public static function getAvailableServers(User $user)
{
$servers = Cache::remember('serversAvailable_'. $user->id, 5, function() use($user){
return array_merge(
self::getAvailableShadowsocks($user),
self::getAvailableVmess($user),
self::getAvailableTrojan($user),
self::getAvailableHysteria($user),
self::getAvailableVless($user)
);
});
$tmp = array_column($servers, 'sort');
array_multisort($tmp, SORT_ASC, $servers);
return array_map(function ($server) {
$server['port'] = (int)$server['port'];
$server['is_online'] = (time() - 300 > $server['last_check_at']) ? 0 : 1;
$server['cache_key'] = "{$server['type']}-{$server['id']}-{$server['updated_at']}-{$server['is_online']}";
return $server;
}, $servers);
}
// 获取可用的用户列表
public static function getAvailableUsers($groupId): Collection
/**
* 根据权限组获取可用的用户列表
* @param array $groupIds
* @return Collection
*/
public static function getAvailableUsers(array $groupIds)
{
return User::toBase()
->whereIn('group_id', $groupId)
->whereIn('group_id', $groupIds)
->whereRaw('u + d < transfer_enable')
->where(function ($query) {
$query->where('expired_at', '>=', time())
@@ -207,147 +72,12 @@ class ServerService
->select([
'id',
'uuid',
'speed_limit'
'speed_limit',
'device_limit'
])
->get();
}
// 记录流量日志
public static function log(int $userId, int $serverId, int $u, int $d, float $rate, string $method)
{
if (($u + $d) < 10240) return true;
$timestamp = strtotime(date('Y-m-d'));
$serverLog = ServerLog::where('log_at', '>=', $timestamp)
->where('log_at', '<', $timestamp + 3600)
->where('server_id', $serverId)
->where('user_id', $userId)
->where('rate', $rate)
->where('method', $method)
->first();
if ($serverLog) {
try {
$serverLog->increment('u', $u);
$serverLog->increment('d', $d);
return true;
} catch (\Exception $e) {
return false;
}
} else {
$serverLog = new ServerLog();
$serverLog->user_id = $userId;
$serverLog->server_id = $serverId;
$serverLog->u = $u;
$serverLog->d = $d;
$serverLog->rate = $rate;
$serverLog->log_at = $timestamp;
$serverLog->method = $method;
return $serverLog->save();
}
}
// 获取所有 SHADOWSOCKS 服务器列表
public static function getAllShadowsocks()
{
$servers = ServerShadowsocks::orderBy('sort', 'ASC')
->get()
->toArray();
foreach ($servers as $k => $v) {
$servers[$k]['type'] = 'shadowsocks';
}
return $servers;
}
// 获取所有 VMESS 服务器列表
public static function getAllVMess()
{
$servers = ServerVmess::orderBy('sort', 'ASC')
->get()
->toArray();
foreach ($servers as $k => $v) {
$servers[$k]['type'] = 'vmess';
}
return $servers;
}
// 获取所有 VLESS 服务器列表
public static function getAllVLess()
{
$servers = ServerVless::orderBy('sort', 'ASC')
->get()
->toArray();
foreach ($servers as $k => $v) {
$servers[$k]['type'] = 'vless';
}
return $servers;
}
// 获取所有 TROJAN 服务器列表
public static function getAllTrojan()
{
$servers = ServerTrojan::orderBy('sort', 'ASC')
->get()
->toArray();
foreach ($servers as $k => $v) {
$servers[$k]['type'] = 'trojan';
}
return $servers;
}
// 获取所有 HYSTERIA 服务器列表
public static function getAllHysteria()
{
$servers = ServerHysteria::orderBy('sort', 'ASC')
->get()
->toArray();
foreach ($servers as $k => $v) {
$servers[$k]['type'] = 'hysteria';
}
return $servers;
}
// 合并数据
private static function mergeData(&$servers)
{
foreach ($servers as $k => $v) {
$serverType = strtoupper($v['type']);
$servers[$k]['online'] = Cache::get(CacheKey::get("SERVER_{$serverType}_ONLINE_USER", $v['parent_id'] ?? $v['id'])) ?? 0;
// 如果是子节点,先尝试从缓存中获取
if($pid = $v['parent_id']){
$cacheKey = CacheKey::get('MULTI_SERVER_' . $serverType . '_ONLINE_USER', $pid);
$onlineUsers = Cache::get($cacheKey) ?? [];
$onlineUserSum = collect($onlineUsers)->whereIn('ip', $v['ips'])->sum('online_user');
$online = ($onlineUserSum > 0 ? $onlineUserSum . "|" : "") . $servers[$k]['online'];
$servers[$k]['online'] = $online;
}
$servers[$k]['last_check_at'] = Cache::get(CacheKey::get("SERVER_{$serverType}_LAST_CHECK_AT", $v['parent_id'] ?? $v['id']));
$servers[$k]['last_push_at'] = Cache::get(CacheKey::get("SERVER_{$serverType}_LAST_PUSH_AT", $v['parent_id'] ?? $v['id']));
if ((time() - 300) >= $servers[$k]['last_check_at']) {
$servers[$k]['available_status'] = 0;
} else if ((time() - 300) >= $servers[$k]['last_push_at']) {
$servers[$k]['available_status'] = 1;
} else {
$servers[$k]['available_status'] = 2;
}
}
}
// 获取所有服务器列表
public static function getAllServers()
{
$servers = array_merge(
self::getAllShadowsocks(),
self::getAllVMess(),
self::getAllTrojan(),
self::getAllHysteria(),
self::getAllVLess()
);
self::mergeData($servers);
$tmp = array_column($servers, 'sort');
array_multisort($tmp, SORT_ASC, $servers);
return $servers;
}
// 获取路由规则
public static function getRoutes(array $routeIds)
{
@@ -355,61 +85,28 @@ class ServerService
// TODO: remove on 1.8.0
foreach ($routes as $k => $route) {
$array = json_decode($route->match, true);
if (is_array($array)) $routes[$k]['match'] = $array;
if (is_array($array))
$routes[$k]['match'] = $array;
}
// TODO: remove on 1.8.0
return $routes;
}
// 获取服务器
/**
* 根据协议类型和标识获取服务器
* @param int $serverId
* @param string $serverType
* @return Server|null
*/
public static function getServer($serverId, $serverType)
{
switch ($serverType) {
case 'vmess':
return ServerVmess::find($serverId);
case 'shadowsocks':
return ServerShadowsocks::find($serverId);
case 'trojan':
return ServerTrojan::find($serverId);
case 'hysteria':
return ServerHysteria::find($serverId);
case 'vless':
return ServerVless::find($serverId);
default:
return false;
}
}
// 根据节点IP和父级别节点ID查询子节点
public static function getChildServer($serverId, $serverType, $nodeIp){
switch ($serverType) {
case 'vmess':
return ServerVmess::query()
->where("parent_id", $serverId)
->where('ips',"like", "%\"$nodeIp\"%")
->first();
case 'shadowsocks':
return ServerShadowsocks::query()
->where("parent_id", $serverId)
->where('ips',"like", "%\"$nodeIp\"%")
->first();
case 'trojan':
return ServerTrojan::query()
->where("parent_id", $serverId)
->where('ips',"like", "%\"$nodeIp\"%")
->first();
case 'hysteria':
return ServerHysteria::query()
->where("parent_id", $serverId)
->where('ips',"like", "%\"$nodeIp\"%")
->first();
case 'vless':
return ServerVless::query()
->where("parent_id", $serverId)
->where('ips',"like", "%\"$nodeIp\"%")
->first();
default:
return null;
}
return Server::query()
->where('type', Server::normalizeType($serverType))
->where(function ($query) use ($serverId) {
$query->where('code', $serverId)
->orWhere('id', $serverId);
})
->orderByRaw('CASE WHEN code = ? THEN 0 ELSE 1 END', [$serverId])
->first();
}
}
+64 -1
View File
@@ -3,10 +3,12 @@ namespace App\Services;
use App\Models\CommissionLog;
use App\Models\Order;
use App\Models\Server;
use App\Models\Stat;
use App\Models\StatServer;
use App\Models\StatUser;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Redis;
@@ -120,7 +122,7 @@ class StatisticalService
$key = "{$rate}_{$uid}";
$stats[$key] = $stats[$key] ?? [
'record_at' => $this->startAt,
'server_rate' => floatval($rate),
'server_rate' => number_format($rate, 2, '.', ''),
'u' => 0,
'd' => 0,
'user_id' => intval($userId),
@@ -240,6 +242,67 @@ class StatisticalService
}
}
/**
* 获取指定日期范围内的节点流量排行
* @param mixed ...$times 可选值:'today', 'tomorrow', 'last_week',或指定日期范围,格式:timestamp
* @return array
*/
public static function getServerRank(...$times)
{
$startAt = 0;
$endAt = Carbon::tomorrow()->endOfDay()->timestamp;
if (count($times) == 1) {
switch ($times[0]) {
case 'today':
$startAt = Carbon::today()->startOfDay()->timestamp;
$endAt = Carbon::today()->endOfDay()->timestamp;
break;
case 'yesterday':
$startAt = Carbon::yesterday()->startOfDay()->timestamp;
$endAt = Carbon::yesterday()->endOfDay()->timestamp;
break;
case 'last_week':
$startAt = Carbon::now()->subWeek()->startOfWeek()->timestamp;
$endAt = Carbon::now()->endOfDay()->timestamp;
break;
}
} else if (count($times) == 2) {
$startAt = $times[0];
$endAt = $times[1];
}
$statistics = Server::whereHas(
'stats',
function ($query) use ($startAt, $endAt) {
$query->where('record_at', '>=', $startAt)
->where('record_at', '<', $endAt)
->where('record_type', '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;
})
->sortByDesc('total')
->select([
'server_name',
'server_id',
'server_type',
'u',
'd',
'total'
])
->values()->toArray();
return $statistics;
}
private function buildInviteRank($limit)
{
$stats = User::select([
+346 -17
View File
@@ -3,34 +3,363 @@
namespace App\Services;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\View;
use Illuminate\Http\UploadedFile;
use Exception;
use ZipArchive;
class ThemeService
{
private $path;
private $theme;
private const SYSTEM_THEME_DIR = 'theme/';
private const USER_THEME_DIR = '/storage/theme/';
private const CONFIG_FILE = 'config.json';
private const SETTING_PREFIX = 'theme_';
private const SYSTEM_THEMES = ['Xboard', 'v2board'];
public function __construct($theme)
public function __construct()
{
$this->theme = $theme;
$this->path = $path = public_path('theme/');
$this->registerThemeViewPaths();
}
public function init()
/**
* 注册主题视图路径
*/
private function registerThemeViewPaths(): void
{
$themeConfigFile = $this->path . "{$this->theme}/config.json";
if (!File::exists($themeConfigFile)) abort(500, "{$this->theme}主题不存在");
$themeConfig = json_decode(File::get($themeConfigFile), true);
if (!isset($themeConfig['configs']) || !is_array($themeConfig)) abort(500, "{$this->theme}主题配置文件有误");
$configs = $themeConfig['configs'];
$data = [];
foreach ($configs as $config) {
$data[$config['field_name']] = isset($config['default_value']) ? $config['default_value'] : '';
// 系统主题路径
$systemPath = base_path(self::SYSTEM_THEME_DIR);
if (File::exists($systemPath)) {
View::addNamespace('theme', $systemPath);
}
// 用户主题路径
$userPath = base_path(self::USER_THEME_DIR);
if (File::exists($userPath)) {
View::prependNamespace('theme', $userPath);
}
}
/**
* 获取主题视图路径
*/
public function getThemeViewPath(string $theme): ?string
{
$themePath = $this->getThemePath($theme);
if (!$themePath) {
return null;
}
return $themePath . '/dashboard.blade.php';
}
/**
* 获取所有可用主题列表
*/
public function getList(): array
{
$themes = [];
// 获取系统主题
$systemPath = base_path(self::SYSTEM_THEME_DIR);
if (File::exists($systemPath)) {
$themes = $this->getThemesFromPath($systemPath, false);
}
// 获取用户主题
$userPath = base_path(self::USER_THEME_DIR);
if (File::exists($userPath)) {
$themes = array_merge($themes, $this->getThemesFromPath($userPath, true));
}
return $themes;
}
/**
* 从指定路径获取主题列表
*/
private function getThemesFromPath(string $path, bool $canDelete): array
{
return collect(File::directories($path))
->mapWithKeys(function ($dir) use ($canDelete) {
$name = basename($dir);
// 检查必要文件是否存在
if (
!File::exists($dir . '/' . self::CONFIG_FILE) ||
!File::exists($dir . '/dashboard.blade.php')
) {
return [];
}
$config = $this->readConfigFile($name);
if (!$config) {
return [];
}
$config['can_delete'] = $canDelete && $name !== admin_setting('current_theme');
$config['is_system'] = !$canDelete;
return [$name => $config];
})->toArray();
}
/**
* 上传新主题
*/
public function upload(UploadedFile $file): bool
{
$zip = new ZipArchive;
$tmpPath = storage_path('tmp/' . uniqid());
try {
if ($zip->open($file->path()) !== true) {
throw new Exception('无效的主题包');
}
// 查找配置文件
$configEntry = collect(range(0, $zip->numFiles - 1))
->map(fn($i) => $zip->getNameIndex($i))
->first(fn($name) => basename($name) === self::CONFIG_FILE);
if (!$configEntry) {
throw new Exception('主题配置文件不存在');
}
// 解压并读取配置
$zip->extractTo($tmpPath);
$zip->close();
$sourcePath = $tmpPath . '/' . rtrim(dirname($configEntry), '.');
$configFile = $sourcePath . '/' . self::CONFIG_FILE;
if (!File::exists($configFile)) {
throw new Exception('主题配置文件不存在');
}
$config = json_decode(File::get($configFile), true);
if (empty($config['name'])) {
throw new Exception('主题名称未配置');
}
// 检查是否为系统主题
if (in_array($config['name'], self::SYSTEM_THEMES)) {
throw new Exception('不能上传与系统主题同名的主题');
}
// 检查必要文件
if (!File::exists($sourcePath . '/dashboard.blade.php')) {
throw new Exception('缺少必要的主题文件:dashboard.blade.php');
}
// 确保目标目录存在
$userThemePath = base_path(self::USER_THEME_DIR);
if (!File::exists($userThemePath)) {
File::makeDirectory($userThemePath, 0755, true);
}
$targetPath = $userThemePath . $config['name'];
if (File::exists($targetPath)) {
throw new Exception('主题已存在');
}
File::copyDirectory($sourcePath, $targetPath);
$this->initConfig($config['name']);
return true;
} catch (Exception $e) {
throw $e;
} finally {
// 清理临时文件
if (File::exists($tmpPath)) {
File::deleteDirectory($tmpPath);
}
}
}
/**
* 切换主题
*/
public function switch(string $theme): bool
{
$currentTheme = admin_setting('current_theme');
if ($theme === $currentTheme) {
return true;
}
try {
admin_setting(["theme_{$this->theme}" => $data]);
} catch (\Exception $e) {
abort(500, "{$this->theme}初始化失败");
// 验证主题是否存在
$themePath = $this->getThemePath($theme);
if (!$themePath) {
throw new Exception('主题不存在');
}
// 验证视图文件是否存在
if (!File::exists($this->getThemeViewPath($theme))) {
throw new Exception('主题视图文件不存在');
}
// 复制主题文件到public目录
$targetPath = public_path('theme/' . $theme);
if (!File::copyDirectory($themePath, $targetPath)) {
throw new Exception('复制主题文件失败');
}
// 清理旧主题文件
if ($currentTheme) {
$oldPath = public_path('theme/' . $currentTheme);
if (File::exists($oldPath)) {
File::deleteDirectory($oldPath);
}
}
admin_setting(['current_theme' => $theme]);
return true;
} catch (Exception $e) {
Log::error('Theme switch failed', ['theme' => $theme, 'error' => $e->getMessage()]);
throw $e;
}
}
/**
* 删除主题
*/
public function delete(string $theme): bool
{
try {
// 检查是否为系统主题
if (in_array($theme, self::SYSTEM_THEMES)) {
throw new Exception('系统主题不能删除');
}
// 检查是否为当前使用的主题
if ($theme === admin_setting('current_theme')) {
throw new Exception('当前使用的主题不能删除');
}
// 获取主题路径
$themePath = base_path(self::USER_THEME_DIR . $theme);
if (!File::exists($themePath)) {
throw new Exception('主题不存在');
}
// 删除主题文件
File::deleteDirectory($themePath);
// 删除public目录下的主题文件
$publicPath = public_path('theme/' . $theme);
if (File::exists($publicPath)) {
File::deleteDirectory($publicPath);
}
// 清理主题配置
admin_setting([self::SETTING_PREFIX . $theme => null]);
return true;
} catch (Exception $e) {
Log::error('Theme deletion failed', ['theme' => $theme, 'error' => $e->getMessage()]);
throw $e;
}
}
/**
* 检查主题是否存在
*/
public function exists(string $theme): bool
{
return $this->getThemePath($theme) !== null;
}
/**
* 获取主题路径
*/
public function getThemePath(string $theme): ?string
{
$systemPath = base_path(self::SYSTEM_THEME_DIR . $theme);
if (File::exists($systemPath)) {
return $systemPath;
}
$userPath = base_path(self::USER_THEME_DIR . $theme);
if (File::exists($userPath)) {
return $userPath;
}
return null;
}
/**
* 获取主题配置
*/
public function getConfig(string $theme): ?array
{
$config = admin_setting(self::SETTING_PREFIX . $theme);
if ($config === null) {
$this->initConfig($theme);
$config = admin_setting(self::SETTING_PREFIX . $theme);
}
return $config;
}
/**
* 更新主题配置
*/
public function updateConfig(string $theme, array $config): bool
{
try {
// 验证主题是否存在
if (!$this->getThemePath($theme)) {
throw new Exception('主题不存在');
}
$schema = $this->readConfigFile($theme);
if (!$schema) {
throw new Exception('主题配置文件无效');
}
// 只保留有效的配置字段
$validFields = collect($schema['configs'] ?? [])->pluck('field_name')->toArray();
$validConfig = collect($config)
->only($validFields)
->toArray();
$currentConfig = $this->getConfig($theme) ?? [];
$newConfig = array_merge($currentConfig, $validConfig);
admin_setting([self::SETTING_PREFIX . $theme => $newConfig]);
return true;
} catch (Exception $e) {
Log::error('Config update failed', ['theme' => $theme, 'error' => $e->getMessage()]);
throw $e;
}
}
/**
* 读取主题配置文件
*/
private function readConfigFile(string $theme): ?array
{
$themePath = $this->getThemePath($theme);
if (!$themePath) {
return null;
}
$file = $themePath . '/' . self::CONFIG_FILE;
return File::exists($file) ? json_decode(File::get($file), true) : null;
}
/**
* 初始化主题配置
*/
private function initConfig(string $theme): void
{
$config = $this->readConfigFile($theme);
if (!$config) {
return;
}
$defaults = collect($config['configs'] ?? [])
->mapWithKeys(fn($col) => [$col['field_name'] => $col['default_value'] ?? ''])
->toArray();
admin_setting([self::SETTING_PREFIX . $theme => $defaults]);
}
}
+181
View File
@@ -0,0 +1,181 @@
<?php
namespace App\Services;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
use App\Jobs\SyncUserOnlineStatusJob;
class UserOnlineService
{
/**
* 缓存相关常量
*/
private const CACHE_PREFIX = 'ALIVE_IP_USER_';
private const CACHE_TTL = 120;
private const NODE_DATA_EXPIRY = 100;
/**
* 获取所有限制设备用户的在线数量
*/
public function getAliveList(Collection $deviceLimitUsers): array
{
if ($deviceLimitUsers->isEmpty()) {
return [];
}
$cacheKeys = $deviceLimitUsers->pluck('id')
->map(fn(int $id): string => self::CACHE_PREFIX . $id)
->all();
return collect(cache()->many($cacheKeys))
->filter()
->map(fn(array $data): ?int => $data['alive_ip'] ?? null)
->filter()
->mapWithKeys(fn(int $count, string $key): array => [
(int) Str::after($key, self::CACHE_PREFIX) => $count
])
->all();
}
/**
* 获取指定用户的在线设备信息
*/
public static function getUserDevices(int $userId): array
{
$data = cache()->get(self::CACHE_PREFIX . $userId, []);
if (empty($data)) {
return ['total_count' => 0, 'devices' => []];
}
$devices = collect($data)
->filter(fn(mixed $item): bool => is_array($item) && isset($item['aliveips']))
->flatMap(function (array $nodeData, string $nodeKey): array {
return collect($nodeData['aliveips'])
->mapWithKeys(function (string $ipNodeId) use ($nodeData, $nodeKey): array {
$ip = Str::before($ipNodeId, '_');
return [
$ip => [
'ip' => $ip,
'last_seen' => $nodeData['lastupdateAt'],
'node_type' => Str::before($nodeKey, (string) $nodeData['lastupdateAt'])
]
];
})
->all();
})
->values()
->all();
return [
'total_count' => $data['alive_ip'] ?? 0,
'devices' => $devices
];
}
/**
* 更新用户在线数据
*/
public function updateAliveData(array $data, string $nodeType, int $nodeId): void
{
$updateAt = now()->timestamp;
$nodeKey = $nodeType . $nodeId;
$userUpdates = [];
foreach ($data as $uid => $ips) {
$cacheKey = self::CACHE_PREFIX . $uid;
$ipsArray = cache()->get($cacheKey, []);
$ipsArray = [
...collect($ipsArray)
->filter(
fn(mixed $value): bool =>
is_array($value) &&
($updateAt - ($value['lastupdateAt'] ?? 0) <= self::NODE_DATA_EXPIRY)
),
$nodeKey => [
'aliveips' => $ips,
'lastupdateAt' => $updateAt
]
];
$count = $this->calculateDeviceCount($ipsArray);
$ipsArray['alive_ip'] = $count;
cache()->put($cacheKey, $ipsArray, now()->addSeconds(self::CACHE_TTL));
$userUpdates[] = [
'id' => $uid,
'count' => $count,
];
}
// 使用队列异步更新数据库
if (!empty($userUpdates)) {
dispatch(new SyncUserOnlineStatusJob($userUpdates))
->onQueue('online_sync')
->afterCommit();
}
}
/**
* 批量获取用户在线设备数
*/
public function getOnlineCounts(array $userIds): array
{
$cacheKeys = collect($userIds)
->map(fn(int $id): string => self::CACHE_PREFIX . $id)
->all();
return collect(cache()->many($cacheKeys))
->filter()
->map(fn(array $data): int => $data['alive_ip'] ?? 0)
->all();
}
/**
* 获取用户在线设备数
*/
public function getOnlineCount(int $userId): int
{
$data = cache()->get(self::CACHE_PREFIX . $userId, []);
return $data['alive_ip'] ?? 0;
}
/**
* 清理过期的在线记录
*/
public function cleanExpiredOnlineStatus(): void
{
dispatch(function () {
User::query()
->where('last_online_at', '<', now()->subMinutes(5))
->update(['online_count' => 0]);
})->onQueue('online_sync');
}
/**
* 计算设备数量
*/
private function calculateDeviceCount(array $ipsArray): int
{
// 设备限制模式
return match ((int) admin_setting('device_limit_mode', 0)) {
// 宽松模式
1 => collect($ipsArray)
->filter(fn(mixed $data): bool => is_array($data) && isset($data['aliveips']))
->flatMap(
fn(array $data): array => collect($data['aliveips'])
->map(fn(string $ipNodeId): string => Str::before($ipNodeId, '_'))
->unique()
->all()
)
->unique()
->count(),
0 => collect($ipsArray)
->filter(fn(mixed $data): bool => is_array($data) && isset($data['aliveips']))
->sum(fn(array $data): int => count($data['aliveips']))
};
}
}
+8 -19
View File
@@ -2,11 +2,12 @@
namespace App\Services;
use App\Jobs\BatchTrafficFetchJob;
use App\Jobs\StatServerJob;
use App\Jobs\StatUserJob;
use App\Jobs\TrafficFetchJob;
use App\Models\Order;
use App\Models\Plan;
use App\Models\User;
use Illuminate\Support\Facades\Bus;
class UserService
{
@@ -169,26 +170,14 @@ class UserService
return true;
}
public function trafficFetch(array $server, string $protocol, array $data, string $nodeIp = null)
public function trafficFetch(array $server, string $protocol, array $data)
{
$timestamp = strtotime(date('Y-m-d'));
$statService = new StatisticalService();
$statService->setStartAt($timestamp);
// 获取子节点
$childServer = ($server['parent_id'] == null && $nodeIp) ? ServerService::getChildServer($server['id'], $protocol, $nodeIp) : null;
foreach ($data as $uid => $v) {
$u = $v[0];
$d = $v[1];
$targetServer = $childServer ?? $server;
$statService->statUser($targetServer['rate'], $uid, $u, $d); //如果存在子节点则使用子节点的倍率
if (!blank($childServer)) { //如果存在子节点,则给子节点计算流量
$statService->statServer($childServer['id'], $protocol, $u, $d);
}
$statService->statServer($server['id'], $protocol, $u, $d);
}
collect($data)->chunk(1000)->each(function ($chunk) use ($timestamp, $server, $protocol, $childServer) {
BatchTrafficFetchJob::dispatch($server, $chunk->toArray(), $protocol, $timestamp, $childServer);
collect($data)->chunk(1000)->each(function ($chunk) use ($timestamp, $server, $protocol) {
TrafficFetchJob::dispatch($server, $chunk->toArray(), $protocol, $timestamp);
StatUserJob::dispatch($server, $chunk->toArray(), $protocol, 'd');
StatServerJob::dispatch($server, $chunk->toArray(), $protocol, 'd');
});
}
}