mirror of
https://github.com/lkddi/Xboard.git
synced 2026-05-01 00:47:28 +08:00
feat: new xboard
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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']))
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user