mirror of
https://github.com/lkddi/Xboard.git
synced 2026-04-24 03:57:27 +08:00
Initial commit
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
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;
|
||||
|
||||
class AuthService
|
||||
{
|
||||
private $user;
|
||||
|
||||
public function __construct(User $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
public function generateAuthData(Request $request)
|
||||
{
|
||||
$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()
|
||||
]);
|
||||
return [
|
||||
'token' => $this->user->token,
|
||||
'is_admin' => $this->user->is_admin,
|
||||
'auth_data' => $authData
|
||||
];
|
||||
}
|
||||
|
||||
public static function decryptAuthData($jwt)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
private static function checkSession($userId, $session)
|
||||
{
|
||||
$sessions = (array)Cache::get(CacheKey::get("USER_SESSIONS", $userId)) ?? [];
|
||||
if (!in_array($session, array_keys($sessions))) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static function addSession($userId, $guid, $meta)
|
||||
{
|
||||
$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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Coupon;
|
||||
use App\Models\Order;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class CouponService
|
||||
{
|
||||
public $coupon;
|
||||
public $planId;
|
||||
public $userId;
|
||||
public $period;
|
||||
|
||||
public function __construct($code)
|
||||
{
|
||||
$this->coupon = Coupon::where('code', $code)
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
}
|
||||
|
||||
public function use(Order $order):bool
|
||||
{
|
||||
$this->setPlanId($order->plan_id);
|
||||
$this->setUserId($order->user_id);
|
||||
$this->setPeriod($order->period);
|
||||
$this->check();
|
||||
switch ($this->coupon->type) {
|
||||
case 1:
|
||||
$order->discount_amount = $this->coupon->value;
|
||||
break;
|
||||
case 2:
|
||||
$order->discount_amount = $order->total_amount * ($this->coupon->value / 100);
|
||||
break;
|
||||
}
|
||||
if ($order->discount_amount > $order->total_amount) {
|
||||
$order->discount_amount = $order->total_amount;
|
||||
}
|
||||
if ($this->coupon->limit_use !== NULL) {
|
||||
if ($this->coupon->limit_use <= 0) return false;
|
||||
$this->coupon->limit_use = $this->coupon->limit_use - 1;
|
||||
if (!$this->coupon->save()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getId()
|
||||
{
|
||||
return $this->coupon->id;
|
||||
}
|
||||
|
||||
public function getCoupon()
|
||||
{
|
||||
return $this->coupon;
|
||||
}
|
||||
|
||||
public function setPlanId($planId)
|
||||
{
|
||||
$this->planId = $planId;
|
||||
}
|
||||
|
||||
public function setUserId($userId)
|
||||
{
|
||||
$this->userId = $userId;
|
||||
}
|
||||
|
||||
public function setPeriod($period)
|
||||
{
|
||||
$this->period = $period;
|
||||
}
|
||||
|
||||
public function checkLimitUseWithUser():bool
|
||||
{
|
||||
$usedCount = Order::where('coupon_id', $this->coupon->id)
|
||||
->where('user_id', $this->userId)
|
||||
->whereNotIn('status', [0, 2])
|
||||
->count();
|
||||
if ($usedCount >= $this->coupon->limit_use_with_user) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public function check()
|
||||
{
|
||||
if (!$this->coupon || !$this->coupon->show) {
|
||||
abort(500, __('Invalid coupon'));
|
||||
}
|
||||
if ($this->coupon->limit_use <= 0 && $this->coupon->limit_use !== NULL) {
|
||||
abort(500, __('This coupon is no longer available'));
|
||||
}
|
||||
if (time() < $this->coupon->started_at) {
|
||||
abort(500, __('This coupon has not yet started'));
|
||||
}
|
||||
if (time() > $this->coupon->ended_at) {
|
||||
abort(500, __('This coupon has expired'));
|
||||
}
|
||||
if ($this->coupon->limit_plan_ids && $this->planId) {
|
||||
if (!in_array($this->planId, $this->coupon->limit_plan_ids)) {
|
||||
abort(500, __('The coupon code cannot be used for this subscription'));
|
||||
}
|
||||
}
|
||||
if ($this->coupon->limit_period && $this->period) {
|
||||
if (!in_array($this->period, $this->coupon->limit_period)) {
|
||||
abort(500, __('The coupon code cannot be used for this period'));
|
||||
}
|
||||
}
|
||||
if ($this->coupon->limit_use_with_user !== NULL && $this->userId) {
|
||||
if (!$this->checkLimitUseWithUser()) {
|
||||
abort(500, __('The coupon can only be used :limit_use_with_user per person', [
|
||||
'limit_use_with_user' => $this->coupon->limit_use_with_user
|
||||
]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Jobs\SendEmailJob;
|
||||
use App\Models\User;
|
||||
use App\Utils\CacheKey;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class MailService
|
||||
{
|
||||
public function remindTraffic (User $user)
|
||||
{
|
||||
if (!$user->remind_traffic) return;
|
||||
if (!$this->remindTrafficIsWarnValue($user->u, $user->d, $user->transfer_enable)) return;
|
||||
$flag = CacheKey::get('LAST_SEND_EMAIL_REMIND_TRAFFIC', $user->id);
|
||||
if (Cache::get($flag)) return;
|
||||
if (!Cache::put($flag, 1, 24 * 3600)) return;
|
||||
SendEmailJob::dispatch([
|
||||
'email' => $user->email,
|
||||
'subject' => __('The traffic usage in :app_name has reached 80%', [
|
||||
'app_name' => admin_setting('app_name', 'XBoard')
|
||||
]),
|
||||
'template_name' => 'remindTraffic',
|
||||
'template_value' => [
|
||||
'name' => admin_setting('app_name', 'XBoard'),
|
||||
'url' => admin_setting('app_url')
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
public function remindExpire(User $user)
|
||||
{
|
||||
if (!($user->expired_at !== NULL && ($user->expired_at - 86400) < time() && $user->expired_at > time())) return;
|
||||
SendEmailJob::dispatch([
|
||||
'email' => $user->email,
|
||||
'subject' => __('The service in :app_name is about to expire', [
|
||||
'app_name' => admin_setting('app_name', 'XBoard')
|
||||
]),
|
||||
'template_name' => 'remindExpire',
|
||||
'template_value' => [
|
||||
'name' => admin_setting('app_name', 'XBoard'),
|
||||
'url' => admin_setting('app_url')
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
private function remindTrafficIsWarnValue($u, $d, $transfer_enable)
|
||||
{
|
||||
$ud = $u + $d;
|
||||
if (!$ud) return false;
|
||||
if (!$transfer_enable) return false;
|
||||
$percentage = ($ud / $transfer_enable) * 100;
|
||||
if ($percentage < 80) return false;
|
||||
if ($percentage >= 100) return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Jobs\OrderHandleJob;
|
||||
use App\Models\Order;
|
||||
use App\Models\Plan;
|
||||
use App\Models\User;
|
||||
use App\Utils\CacheKey;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
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
|
||||
];
|
||||
public $order;
|
||||
public $user;
|
||||
|
||||
public function __construct(Order $order)
|
||||
{
|
||||
$this->order = $order;
|
||||
}
|
||||
|
||||
public function open()
|
||||
{
|
||||
$order = $this->order;
|
||||
$this->user = User::find($order->user_id);
|
||||
$plan = Plan::find($order->plan_id);
|
||||
|
||||
if ($order->refund_amount) {
|
||||
$this->user->balance = $this->user->balance + $order->refund_amount;
|
||||
}
|
||||
DB::beginTransaction();
|
||||
if ($order->surplus_order_ids) {
|
||||
try {
|
||||
Order::whereIn('id', $order->surplus_order_ids)->update([
|
||||
'status' => 4
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
DB::rollback();
|
||||
abort(500, '开通失败');
|
||||
}
|
||||
}
|
||||
switch ((string)$order->period) {
|
||||
case 'onetime_price':
|
||||
$this->buyByOneTime($plan);
|
||||
break;
|
||||
case 'reset_price':
|
||||
$this->buyByResetTraffic();
|
||||
break;
|
||||
default:
|
||||
$this->buyByPeriod($order, $plan);
|
||||
}
|
||||
|
||||
switch ((int)$order->type) {
|
||||
case 1:
|
||||
$this->openEvent(admin_setting('new_order_event_id', 0));
|
||||
break;
|
||||
case 2:
|
||||
$this->openEvent(admin_setting('renew_order_event_id', 0));
|
||||
break;
|
||||
case 3:
|
||||
$this->openEvent(admin_setting('change_order_event_id', 0));
|
||||
break;
|
||||
}
|
||||
|
||||
$this->setSpeedLimit($plan->speed_limit);
|
||||
|
||||
if (!$this->user->save()) {
|
||||
DB::rollBack();
|
||||
abort(500, '开通失败');
|
||||
}
|
||||
$order->status = 3;
|
||||
if (!$order->save()) {
|
||||
DB::rollBack();
|
||||
abort(500, '开通失败');
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
}
|
||||
|
||||
|
||||
public function setOrderType(User $user)
|
||||
{
|
||||
$order = $this->order;
|
||||
if ($order->period === 'reset_price') {
|
||||
$order->type = 4;
|
||||
} 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)) abort(500, '目前不允许更改订阅,请联系客服或提交工单操作');
|
||||
$order->type = 3;
|
||||
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;
|
||||
} else {
|
||||
$order->total_amount = $order->total_amount - $order->surplus_amount;
|
||||
}
|
||||
} else if ($user->expired_at > time() && $order->plan_id == $user->plan_id) { // 用户订阅未过期且购买订阅与当前订阅相同 === 续费
|
||||
$order->type = 2;
|
||||
} else { // 新购
|
||||
$order->type = 1;
|
||||
}
|
||||
}
|
||||
|
||||
public function setVipDiscount(User $user)
|
||||
{
|
||||
$order = $this->order;
|
||||
if ($user->discount) {
|
||||
$order->discount_amount = $order->discount_amount + ($order->total_amount * ($user->discount / 100));
|
||||
}
|
||||
$order->total_amount = $order->total_amount - $order->discount_amount;
|
||||
}
|
||||
|
||||
public function setInvite(User $user):void
|
||||
{
|
||||
$order = $this->order;
|
||||
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;
|
||||
$isCommission = false;
|
||||
switch ((int)$inviter->commission_type) {
|
||||
case 0:
|
||||
$commissionFirstTime = (int)admin_setting('commission_first_time_enable', 1);
|
||||
$isCommission = (!$commissionFirstTime || ($commissionFirstTime && !$this->haveValidOrder($user)));
|
||||
break;
|
||||
case 1:
|
||||
$isCommission = true;
|
||||
break;
|
||||
case 2:
|
||||
$isCommission = !$this->haveValidOrder($user);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!$isCommission) return;
|
||||
if ($inviter && $inviter->commission_rate) {
|
||||
$order->commission_balance = $order->total_amount * ($inviter->commission_rate / 100);
|
||||
} else {
|
||||
$order->commission_balance = $order->total_amount * (admin_setting('invite_commission', 10) / 100);
|
||||
}
|
||||
}
|
||||
|
||||
private function haveValidOrder(User $user)
|
||||
{
|
||||
return Order::where('user_id', $user->id)
|
||||
->whereNotIn('status', [0, 2])
|
||||
->first();
|
||||
}
|
||||
|
||||
private function getSurplusValue(User $user, Order $order)
|
||||
{
|
||||
if ($user->expired_at === NULL) {
|
||||
$this->getSurplusValueByOneTime($user, $order);
|
||||
} else {
|
||||
$this->getSurplusValueByPeriod($user, $order);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function getSurplusValueByOneTime(User $user, Order $order)
|
||||
{
|
||||
$lastOneTimeOrder = Order::where('user_id', $user->id)
|
||||
->where('period', 'onetime_price')
|
||||
->where('status', 3)
|
||||
->orderBy('id', 'DESC')
|
||||
->first();
|
||||
if (!$lastOneTimeOrder) return;
|
||||
$nowUserTraffic = $user->transfer_enable / 1073741824;
|
||||
if (!$nowUserTraffic) return;
|
||||
$paidTotalAmount = ($lastOneTimeOrder->total_amount + $lastOneTimeOrder->balance_amount);
|
||||
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', 3);
|
||||
$order->surplus_amount = $result > 0 ? $result : 0;
|
||||
$order->surplus_order_ids = array_column($orderModel->get()->toArray(), 'id');
|
||||
}
|
||||
|
||||
private function getSurplusValueByPeriod(User $user, Order $order)
|
||||
{
|
||||
$orders = Order::where('user_id', $user->id)
|
||||
->where('period', '!=', 'reset_price')
|
||||
->where('period', '!=', 'onetime_price')
|
||||
->where('status', 3)
|
||||
->get()
|
||||
->toArray();
|
||||
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;
|
||||
$lastValidateAt = $item['created_at'];
|
||||
$orderMonthSum = $period + $orderMonthSum;
|
||||
$orderAmountSum = $orderAmountSum + ($item['total_amount'] + $item['balance_amount'] + $item['surplus_amount'] - $item['refund_amount']);
|
||||
}
|
||||
if (!$lastValidateAt) return;
|
||||
$expiredAtByOrder = strtotime("+{$orderMonthSum} month", $lastValidateAt);
|
||||
if ($expiredAtByOrder < time()) return;
|
||||
$orderSurplusSecond = $expiredAtByOrder - time();
|
||||
$orderRangeSecond = $expiredAtByOrder - $lastValidateAt;
|
||||
$avgPrice = $orderAmountSum / $orderRangeSecond;
|
||||
$orderSurplusAmount = $avgPrice * $orderSurplusSecond;
|
||||
if (!$orderSurplusSecond || !$orderSurplusAmount) return;
|
||||
$order->surplus_amount = $orderSurplusAmount > 0 ? $orderSurplusAmount : 0;
|
||||
$order->surplus_order_ids = array_column($orders, 'id');
|
||||
}
|
||||
|
||||
public function paid(string $callbackNo)
|
||||
{
|
||||
$order = $this->order;
|
||||
if ($order->status !== 0) return true;
|
||||
$order->status = 1;
|
||||
$order->paid_at = time();
|
||||
$order->callback_no = $callbackNo;
|
||||
if (!$order->save()) return false;
|
||||
try {
|
||||
OrderHandleJob::dispatchSync($order->trade_no);
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function cancel():bool
|
||||
{
|
||||
$order = $this->order;
|
||||
DB::beginTransaction();
|
||||
$order->status = 2;
|
||||
if (!$order->save()) {
|
||||
DB::rollBack();
|
||||
return false;
|
||||
}
|
||||
if ($order->balance_amount) {
|
||||
$userService = new UserService();
|
||||
if (!$userService->addBalance($order->user_id, $order->balance_amount)) {
|
||||
DB::rollBack();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
DB::commit();
|
||||
return true;
|
||||
}
|
||||
|
||||
private function setSpeedLimit($speedLimit)
|
||||
{
|
||||
$this->user->speed_limit = $speedLimit;
|
||||
}
|
||||
|
||||
private function buyByResetTraffic()
|
||||
{
|
||||
$this->user->u = 0;
|
||||
$this->user->d = 0;
|
||||
}
|
||||
|
||||
private function buyByPeriod(Order $order, Plan $plan)
|
||||
{
|
||||
// change plan process
|
||||
if ((int)$order->type === 3) {
|
||||
$this->user->expired_at = time();
|
||||
}
|
||||
$this->user->transfer_enable = $plan->transfer_enable * 1073741824;
|
||||
// 从一次性转换到循环
|
||||
if ($this->user->expired_at === NULL) $this->buyByResetTraffic();
|
||||
// 新购
|
||||
if ($order->type === 1) $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);
|
||||
}
|
||||
|
||||
private function buyByOneTime(Plan $plan)
|
||||
{
|
||||
$this->buyByResetTraffic();
|
||||
$this->user->transfer_enable = $plan->transfer_enable * 1073741824;
|
||||
$this->user->plan_id = $plan->id;
|
||||
$this->user->group_id = $plan->group_id;
|
||||
$this->user->expired_at = NULL;
|
||||
}
|
||||
|
||||
private function getTime($str, $timestamp)
|
||||
{
|
||||
if ($timestamp < time()) {
|
||||
$timestamp = time();
|
||||
}
|
||||
switch ($str) {
|
||||
case 'month_price':
|
||||
return strtotime('+1 month', $timestamp);
|
||||
case 'quarter_price':
|
||||
return strtotime('+3 month', $timestamp);
|
||||
case 'half_year_price':
|
||||
return strtotime('+6 month', $timestamp);
|
||||
case 'year_price':
|
||||
return strtotime('+12 month', $timestamp);
|
||||
case 'two_year_price':
|
||||
return strtotime('+24 month', $timestamp);
|
||||
case 'three_year_price':
|
||||
return strtotime('+36 month', $timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
private function openEvent($eventId)
|
||||
{
|
||||
switch ((int) $eventId) {
|
||||
case 0:
|
||||
break;
|
||||
case 1:
|
||||
$this->buyByResetTraffic();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
|
||||
use App\Models\Payment;
|
||||
|
||||
class PaymentService
|
||||
{
|
||||
public $method;
|
||||
protected $class;
|
||||
protected $config;
|
||||
protected $payment;
|
||||
|
||||
public function __construct($method, $id = NULL, $uuid = NULL)
|
||||
{
|
||||
$this->method = $method;
|
||||
$this->class = '\\App\\Payments\\' . $this->method;
|
||||
if (!class_exists($this->class)) abort(500, 'gate is not found');
|
||||
if ($id) $payment = Payment::find($id)->toArray();
|
||||
if ($uuid) $payment = Payment::where('uuid', $uuid)->first()->toArray();
|
||||
$this->config = [];
|
||||
if (isset($payment)) {
|
||||
$this->config = $payment['config'];
|
||||
$this->config['enable'] = $payment['enable'];
|
||||
$this->config['id'] = $payment['id'];
|
||||
$this->config['uuid'] = $payment['uuid'];
|
||||
$this->config['notify_domain'] = $payment['notify_domain'];
|
||||
};
|
||||
$this->payment = new $this->class($this->config);
|
||||
}
|
||||
|
||||
public function notify($params)
|
||||
{
|
||||
if (!$this->config['enable']) abort(500, 'gate is not enable');
|
||||
return $this->payment->notify($params);
|
||||
}
|
||||
|
||||
public function pay($order)
|
||||
{
|
||||
// custom notify domain name
|
||||
$notifyUrl = url("/api/v1/guest/payment/notify/{$this->method}/{$this->config['uuid']}");
|
||||
if ($this->config['notify_domain']) {
|
||||
$parseUrl = parse_url($notifyUrl);
|
||||
$notifyUrl = $this->config['notify_domain'] . $parseUrl['path'];
|
||||
}
|
||||
|
||||
return $this->payment->pay([
|
||||
'notify_url' => $notifyUrl,
|
||||
'return_url' => admin_setting('app_url') . '/#/order/' . $order['trade_no'],
|
||||
'trade_no' => $order['trade_no'],
|
||||
'total_amount' => $order['total_amount'],
|
||||
'user_id' => $order['user_id'],
|
||||
'stripe_token' => $order['stripe_token']
|
||||
]);
|
||||
}
|
||||
|
||||
public function form()
|
||||
{
|
||||
$form = $this->payment->form();
|
||||
$keys = array_keys($form);
|
||||
foreach ($keys as $key) {
|
||||
if (isset($this->config[$key])) $form[$key]['value'] = $this->config[$key];
|
||||
}
|
||||
return $form;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Plan;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class PlanService
|
||||
{
|
||||
public $plan;
|
||||
|
||||
public function __construct(int $planId)
|
||||
{
|
||||
$this->plan = Plan::lockForUpdate()->find($planId);
|
||||
}
|
||||
|
||||
public function haveCapacity(): bool
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
public static function countActiveUsers()
|
||||
{
|
||||
return User::select(
|
||||
DB::raw("plan_id"),
|
||||
DB::raw("count(*) as count")
|
||||
)
|
||||
->where('plan_id', '!=', NULL)
|
||||
->where(function ($query) {
|
||||
$query->where('expired_at', '>=', time())
|
||||
->orWhere('expired_at', NULL);
|
||||
})
|
||||
->groupBy("plan_id")
|
||||
->get()
|
||||
->keyBy('plan_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\ServerHysteria;
|
||||
use App\Models\ServerLog;
|
||||
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\Facades\Cache;
|
||||
|
||||
class ServerService
|
||||
{
|
||||
public 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']);
|
||||
}
|
||||
}
|
||||
|
||||
$servers[] = $serverData;
|
||||
}
|
||||
|
||||
|
||||
return $servers;
|
||||
}
|
||||
|
||||
public 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;
|
||||
}
|
||||
|
||||
public 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;
|
||||
}
|
||||
|
||||
public 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;
|
||||
}
|
||||
|
||||
public 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'];
|
||||
}
|
||||
$servers[] = $shadowsocks[$key]->toArray();
|
||||
}
|
||||
return $servers;
|
||||
}
|
||||
|
||||
public function getAvailableServers(User $user)
|
||||
{
|
||||
$servers = Cache::remember('serversAvailable_'. $user->id, 5, function() use($user){
|
||||
return array_merge(
|
||||
$this->getAvailableShadowsocks($user),
|
||||
$this->getAvailableVmess($user),
|
||||
$this->getAvailableTrojan($user),
|
||||
$this->getAvailableHysteria($user),
|
||||
$this->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 function getAvailableUsers($groupId)
|
||||
{
|
||||
return User::whereIn('group_id', $groupId)
|
||||
->whereRaw('u + d < transfer_enable')
|
||||
->where(function ($query) {
|
||||
$query->where('expired_at', '>=', time())
|
||||
->orWhere('expired_at', NULL);
|
||||
})
|
||||
->where('banned', 0)
|
||||
->select([
|
||||
'id',
|
||||
'uuid',
|
||||
'speed_limit'
|
||||
])
|
||||
->get();
|
||||
}
|
||||
|
||||
public 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();
|
||||
}
|
||||
}
|
||||
|
||||
public function getAllShadowsocks()
|
||||
{
|
||||
$servers = ServerShadowsocks::orderBy('sort', 'ASC')
|
||||
->get()
|
||||
->toArray();
|
||||
foreach ($servers as $k => $v) {
|
||||
$servers[$k]['type'] = 'shadowsocks';
|
||||
}
|
||||
return $servers;
|
||||
}
|
||||
|
||||
public function getAllVMess()
|
||||
{
|
||||
$servers = ServerVmess::orderBy('sort', 'ASC')
|
||||
->get()
|
||||
->toArray();
|
||||
foreach ($servers as $k => $v) {
|
||||
$servers[$k]['type'] = 'vmess';
|
||||
}
|
||||
return $servers;
|
||||
}
|
||||
|
||||
public function getAllVLess()
|
||||
{
|
||||
$servers = ServerVless::orderBy('sort', 'ASC')
|
||||
->get()
|
||||
->toArray();
|
||||
foreach ($servers as $k => $v) {
|
||||
$servers[$k]['type'] = 'vless';
|
||||
}
|
||||
return $servers;
|
||||
}
|
||||
|
||||
public function getAllTrojan()
|
||||
{
|
||||
$servers = ServerTrojan::orderBy('sort', 'ASC')
|
||||
->get()
|
||||
->toArray();
|
||||
foreach ($servers as $k => $v) {
|
||||
$servers[$k]['type'] = 'trojan';
|
||||
}
|
||||
return $servers;
|
||||
}
|
||||
|
||||
public function getAllHysteria()
|
||||
{
|
||||
$servers = ServerHysteria::orderBy('sort', 'ASC')
|
||||
->get()
|
||||
->toArray();
|
||||
foreach ($servers as $k => $v) {
|
||||
$servers[$k]['type'] = 'hysteria';
|
||||
}
|
||||
return $servers;
|
||||
}
|
||||
|
||||
private 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']){
|
||||
// 获取缓存
|
||||
$onlineUsers = Cache::get(CacheKey::get('MULTI_SERVER_' . $serverType . '_ONLINE_USER', $pid)) ?? [];
|
||||
$servers[$k]['online'] = (collect($onlineUsers)->whereIn('ip', $v['ips'])->sum('online_user')) . "|{$servers[$k]['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 function getAllServers()
|
||||
{
|
||||
$servers = array_merge(
|
||||
$this->getAllShadowsocks(),
|
||||
$this->getAllVMess(),
|
||||
$this->getAllTrojan(),
|
||||
$this->getAllHysteria(),
|
||||
$this->getAllVLess()
|
||||
);
|
||||
$this->mergeData($servers);
|
||||
$tmp = array_column($servers, 'sort');
|
||||
array_multisort($tmp, SORT_ASC, $servers);
|
||||
return $servers;
|
||||
}
|
||||
|
||||
public function getRoutes(array $routeIds)
|
||||
{
|
||||
$routes = ServerRoute::select(['id', 'match', 'action', 'action_value'])->whereIn('id', $routeIds)->get();
|
||||
// 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;
|
||||
}
|
||||
// TODO: remove on 1.8.0
|
||||
return $routes;
|
||||
}
|
||||
|
||||
public 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 function getChildServer($serverId, $serverType, $nodeIp){
|
||||
switch ($serverType) {
|
||||
case 'vmess':
|
||||
return ServerVmess::query()
|
||||
->where("parent_id", $serverId)
|
||||
->whereJsonContains('ips', $nodeIp)
|
||||
->first();
|
||||
case 'shadowsocks':
|
||||
return ServerShadowsocks::query()
|
||||
->where("parent_id", $serverId)
|
||||
->whereJsonContains('ips', $nodeIp)
|
||||
->first();
|
||||
case 'trojan':
|
||||
return ServerTrojan::query()
|
||||
->where("parent_id", $serverId)
|
||||
->whereJsonContains('ips', $nodeIp)
|
||||
->first();
|
||||
case 'hysteria':
|
||||
return ServerHysteria::query()
|
||||
->where("parent_id", $serverId)
|
||||
->whereJsonContains('ips', $nodeIp)
|
||||
->first();
|
||||
case 'vless':
|
||||
return ServerVless::query()
|
||||
->where("parent_id", $serverId)
|
||||
->whereJsonContains('ips', $nodeIp)
|
||||
->first();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Setting as SettingModel;
|
||||
|
||||
class SettingService
|
||||
{
|
||||
public function get($name, $default = null)
|
||||
{
|
||||
$setting = SettingModel::where('name', $name)->first();
|
||||
return $setting ? $setting->value : $default;
|
||||
}
|
||||
|
||||
public function getAll(){
|
||||
return SettingModel::all()->pluck('value', 'name')->toArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
<?php
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\CommissionLog;
|
||||
use App\Models\Order;
|
||||
use App\Models\Stat;
|
||||
use App\Models\StatServer;
|
||||
use App\Models\StatUser;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class StatisticalService {
|
||||
protected $userStats;
|
||||
protected $startAt;
|
||||
protected $endAt;
|
||||
protected $serverStats;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
ini_set('memory_limit', -1);
|
||||
}
|
||||
|
||||
public function setStartAt($timestamp) {
|
||||
$this->startAt = $timestamp;
|
||||
}
|
||||
|
||||
public function setEndAt($timestamp) {
|
||||
$this->endAt = $timestamp;
|
||||
}
|
||||
|
||||
public function setServerStats() {
|
||||
$this->serverStats = Cache::get("stat_server_{$this->startAt}");
|
||||
$this->serverStats = json_decode($this->serverStats, true) ?? [];
|
||||
if (!is_array($this->serverStats)) {
|
||||
$this->serverStats = [];
|
||||
}
|
||||
}
|
||||
|
||||
public function setUserStats() {
|
||||
$this->userStats = Cache::get("stat_user_{$this->startAt}");
|
||||
$this->userStats = json_decode($this->userStats, true) ?? [];
|
||||
if (!is_array($this->userStats)) {
|
||||
$this->userStats = [];
|
||||
}
|
||||
}
|
||||
|
||||
public function generateStatData(): array
|
||||
{
|
||||
$startAt = $this->startAt;
|
||||
$endAt = $this->endAt;
|
||||
if (!$startAt || !$endAt) {
|
||||
$startAt = strtotime(date('Y-m-d'));
|
||||
$endAt = strtotime('+1 day', $startAt);
|
||||
}
|
||||
$data = [];
|
||||
$data['order_count'] = Order::where('created_at', '>=', $startAt)
|
||||
->where('created_at', '<', $endAt)
|
||||
->count();
|
||||
$data['order_total'] = Order::where('created_at', '>=', $startAt)
|
||||
->where('created_at', '<', $endAt)
|
||||
->sum('total_amount');
|
||||
$data['paid_count'] = Order::where('paid_at', '>=', $startAt)
|
||||
->where('paid_at', '<', $endAt)
|
||||
->whereNotIn('status', [0, 2])
|
||||
->count();
|
||||
$data['paid_total'] = Order::where('paid_at', '>=', $startAt)
|
||||
->where('paid_at', '<', $endAt)
|
||||
->whereNotIn('status', [0, 2])
|
||||
->sum('total_amount');
|
||||
$commissionLogBuilder = CommissionLog::where('created_at', '>=', $startAt)
|
||||
->where('created_at', '<', $endAt);
|
||||
$data['commission_count'] = $commissionLogBuilder->count();
|
||||
$data['commission_total'] = $commissionLogBuilder->sum('get_amount');
|
||||
$data['register_count'] = User::where('created_at', '>=', $startAt)
|
||||
->where('created_at', '<', $endAt)
|
||||
->count();
|
||||
$data['invite_count'] = User::where('created_at', '>=', $startAt)
|
||||
->where('created_at', '<', $endAt)
|
||||
->whereNotNull('invite_user_id')
|
||||
->count();
|
||||
$data['transfer_used_total'] = StatServer::where('created_at', '>=', $startAt)
|
||||
->where('created_at', '<', $endAt)
|
||||
->select(DB::raw('SUM(u) + SUM(d) as total'))
|
||||
->value('total') ?? 0;
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function statServer($serverId, $serverType, $u, $d)
|
||||
{
|
||||
$this->serverStats[$serverType] = $this->serverStats[$serverType] ?? [];
|
||||
if (isset($this->serverStats[$serverType][$serverId])) {
|
||||
$this->serverStats[$serverType][$serverId][0] += $u;
|
||||
$this->serverStats[$serverType][$serverId][1] += $d;
|
||||
} else {
|
||||
$this->serverStats[$serverType][$serverId] = [$u, $d];
|
||||
}
|
||||
Cache::set("stat_server_{$this->startAt}", json_encode($this->serverStats));
|
||||
}
|
||||
|
||||
public function statUser($rate, $userId, $u, $d)
|
||||
{
|
||||
$this->userStats[$rate] = $this->userStats[$rate] ?? [];
|
||||
if (isset($this->userStats[$rate][$userId])) {
|
||||
$this->userStats[$rate][$userId][0] += $u;
|
||||
$this->userStats[$rate][$userId][1] += $d;
|
||||
} else {
|
||||
$this->userStats[$rate][$userId] = [$u, $d];
|
||||
}
|
||||
Cache::set("stat_user_{$this->startAt}", json_encode($this->userStats));
|
||||
}
|
||||
|
||||
public function getStatUserByUserID($userId): array
|
||||
{
|
||||
$stats = [];
|
||||
foreach (array_keys($this->userStats) as $rate) {
|
||||
if (!isset($this->userStats[$rate][$userId])) continue;
|
||||
$stats[] = [
|
||||
'record_at' => $this->startAt,
|
||||
'server_rate' => $rate,
|
||||
'u' => $this->userStats[$rate][$userId][0],
|
||||
'd' => $this->userStats[$rate][$userId][1],
|
||||
'user_id' => $userId
|
||||
];
|
||||
}
|
||||
return $stats;
|
||||
}
|
||||
|
||||
public function getStatUser()
|
||||
{
|
||||
$stats = [];
|
||||
foreach ($this->userStats as $k => $v) {
|
||||
foreach (array_keys($v) as $userId) {
|
||||
if (isset($v[$userId])) {
|
||||
$stats[] = [
|
||||
'server_rate' => $k,
|
||||
'u' => $v[$userId][0],
|
||||
'd' => $v[$userId][1],
|
||||
'user_id' => $userId
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
return $stats;
|
||||
}
|
||||
|
||||
|
||||
public function getStatServer()
|
||||
{
|
||||
$stats = [];
|
||||
foreach ($this->serverStats as $serverType => $v) {
|
||||
foreach (array_keys($v) as $serverId) {
|
||||
if (isset($v[$serverId])) {
|
||||
$stats[] = [
|
||||
'server_id' => $serverId,
|
||||
'server_type' => $serverType,
|
||||
'u' => $v[$serverId][0],
|
||||
'd' => $v[$serverId][1],
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
return $stats;
|
||||
}
|
||||
|
||||
public function clearStatUser()
|
||||
{
|
||||
Cache::forget("stat_user_{$this->startAt}");
|
||||
}
|
||||
|
||||
public function clearStatServer()
|
||||
{
|
||||
Cache::forget("stat_server_{$this->startAt}");
|
||||
}
|
||||
|
||||
public function getStatRecord($type)
|
||||
{
|
||||
switch ($type) {
|
||||
case "paid_total": {
|
||||
return Stat::select([
|
||||
'*',
|
||||
DB::raw('paid_total / 100 as paid_total')
|
||||
])
|
||||
->where('record_at', '>=', $this->startAt)
|
||||
->where('record_at', '<', $this->endAt)
|
||||
->orderBy('record_at', 'ASC')
|
||||
->get();
|
||||
}
|
||||
case "commission_total": {
|
||||
return Stat::select([
|
||||
'*',
|
||||
DB::raw('commission_total / 100 as commission_total')
|
||||
])
|
||||
->where('record_at', '>=', $this->startAt)
|
||||
->where('record_at', '<', $this->endAt)
|
||||
->orderBy('record_at', 'ASC')
|
||||
->get();
|
||||
}
|
||||
case "register_count": {
|
||||
return Stat::where('record_at', '>=', $this->startAt)
|
||||
->where('record_at', '<', $this->endAt)
|
||||
->orderBy('record_at', 'ASC')
|
||||
->get();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getRanking($type, $limit = 20)
|
||||
{
|
||||
switch ($type) {
|
||||
case 'server_traffic_rank': {
|
||||
return $this->buildServerTrafficRank($limit);
|
||||
}
|
||||
case 'user_consumption_rank': {
|
||||
return $this->buildUserConsumptionRank($limit);
|
||||
}
|
||||
case 'invite_rank': {
|
||||
return $this->buildInviteRank($limit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function buildInviteRank($limit)
|
||||
{
|
||||
$stats = User::select([
|
||||
'invite_user_id',
|
||||
DB::raw('count(*) as count')
|
||||
])
|
||||
->where('created_at', '>=', $this->startAt)
|
||||
->where('created_at', '<', $this->endAt)
|
||||
->whereNotNull('invite_user_id')
|
||||
->groupBy('invite_user_id')
|
||||
->orderBy('count', 'DESC')
|
||||
->limit($limit)
|
||||
->get();
|
||||
|
||||
$users = User::whereIn('id', $stats->pluck('invite_user_id')->toArray())->get()->keyBy('id');
|
||||
foreach ($stats as $k => $v) {
|
||||
if (!isset($users[$v['invite_user_id']])) continue;
|
||||
$stats[$k]['email'] = $users[$v['invite_user_id']]['email'];
|
||||
}
|
||||
return $stats;
|
||||
}
|
||||
|
||||
private function buildUserConsumptionRank($limit)
|
||||
{
|
||||
$stats = StatUser::select([
|
||||
'user_id',
|
||||
DB::raw('sum(u) as u'),
|
||||
DB::raw('sum(d) as d'),
|
||||
DB::raw('sum(u) + sum(d) as total')
|
||||
])
|
||||
->where('record_at', '>=', $this->startAt)
|
||||
->where('record_at', '<', $this->endAt)
|
||||
->groupBy('user_id')
|
||||
->orderBy('total', 'DESC')
|
||||
->limit($limit)
|
||||
->get();
|
||||
$users = User::whereIn('id', $stats->pluck('user_id')->toArray())->get()->keyBy('id');
|
||||
foreach ($stats as $k => $v) {
|
||||
if (!isset($users[$v['user_id']])) continue;
|
||||
$stats[$k]['email'] = $users[$v['user_id']]['email'];
|
||||
}
|
||||
return $stats;
|
||||
}
|
||||
|
||||
private function buildServerTrafficRank($limit)
|
||||
{
|
||||
return StatServer::select([
|
||||
'server_id',
|
||||
'server_type',
|
||||
DB::raw('sum(u) as u'),
|
||||
DB::raw('sum(d) as d'),
|
||||
DB::raw('sum(u) + sum(d) as total')
|
||||
])
|
||||
->where('record_at', '>=', $this->startAt)
|
||||
->where('record_at', '<', $this->endAt)
|
||||
->groupBy('server_id', 'server_type')
|
||||
->orderBy('total', 'DESC')
|
||||
->limit($limit)
|
||||
->get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
namespace App\Services;
|
||||
|
||||
use App\Jobs\SendTelegramJob;
|
||||
use App\Models\User;
|
||||
use \Curl\Curl;
|
||||
use Illuminate\Mail\Markdown;
|
||||
|
||||
class TelegramService {
|
||||
protected $api;
|
||||
|
||||
public function __construct($token = '')
|
||||
{
|
||||
$this->api = 'https://api.telegram.org/bot' . admin_setting('telegram_bot_token', $token) . '/';
|
||||
}
|
||||
|
||||
public function sendMessage(int $chatId, string $text, string $parseMode = '')
|
||||
{
|
||||
if ($parseMode === 'markdown') {
|
||||
$text = str_replace('_', '\_', $text);
|
||||
}
|
||||
$this->request('sendMessage', [
|
||||
'chat_id' => $chatId,
|
||||
'text' => $text,
|
||||
'parse_mode' => $parseMode
|
||||
]);
|
||||
}
|
||||
|
||||
public function approveChatJoinRequest(int $chatId, int $userId)
|
||||
{
|
||||
$this->request('approveChatJoinRequest', [
|
||||
'chat_id' => $chatId,
|
||||
'user_id' => $userId
|
||||
]);
|
||||
}
|
||||
|
||||
public function declineChatJoinRequest(int $chatId, int $userId)
|
||||
{
|
||||
$this->request('declineChatJoinRequest', [
|
||||
'chat_id' => $chatId,
|
||||
'user_id' => $userId
|
||||
]);
|
||||
}
|
||||
|
||||
public function getMe()
|
||||
{
|
||||
return $this->request('getMe');
|
||||
}
|
||||
|
||||
public function setWebhook(string $url)
|
||||
{
|
||||
return $this->request('setWebhook', [
|
||||
'url' => $url
|
||||
]);
|
||||
}
|
||||
|
||||
private function request(string $method, array $params = [])
|
||||
{
|
||||
$curl = new Curl();
|
||||
$curl->get($this->api . $method . '?' . http_build_query($params));
|
||||
$response = $curl->response;
|
||||
$curl->close();
|
||||
if (!isset($response->ok)) abort(500, '请求失败');
|
||||
if (!$response->ok) {
|
||||
abort(500, '来自TG的错误:' . $response->description);
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function sendMessageWithAdmin($message, $isStaff = false)
|
||||
{
|
||||
if (!admin_setting('telegram_bot_enable', 0)) return;
|
||||
$users = User::where(function ($query) use ($isStaff) {
|
||||
$query->where('is_admin', 1);
|
||||
if ($isStaff) {
|
||||
$query->orWhere('is_staff', 1);
|
||||
}
|
||||
})
|
||||
->where('telegram_id', '!=', NULL)
|
||||
->get();
|
||||
foreach ($users as $user) {
|
||||
SendTelegramJob::dispatch($user->telegram_id, $message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
class ThemeService
|
||||
{
|
||||
private $path;
|
||||
private $theme;
|
||||
|
||||
public function __construct($theme)
|
||||
{
|
||||
$this->theme = $theme;
|
||||
$this->path = $path = public_path('theme/');
|
||||
}
|
||||
|
||||
public function init()
|
||||
{
|
||||
$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'] : '';
|
||||
}
|
||||
|
||||
try {
|
||||
admin_setting(["theme_{$this->theme}" => $data]);
|
||||
} catch (\Exception $e) {
|
||||
abort(500, "{$this->theme}初始化失败");
|
||||
}
|
||||
// $data = var_export($data, 1);
|
||||
// try {
|
||||
// if (!File::put(base_path() . "/config/theme/{$this->theme}.php", "<?php\n return $data ;")) {
|
||||
// abort(500, "{$this->theme}初始化失败");
|
||||
// }
|
||||
// } catch (\Exception $e) {
|
||||
// abort(500, '请检查V2Board目录权限');
|
||||
// }
|
||||
|
||||
// try {
|
||||
// Artisan::call('config:cache');
|
||||
// while (true) {
|
||||
// if (config("theme.{$this->theme}")) break;
|
||||
// }
|
||||
// } catch (\Exception $e) {
|
||||
// abort(500, "{$this->theme}初始化失败");
|
||||
// }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
namespace App\Services;
|
||||
|
||||
|
||||
use App\Jobs\SendEmailJob;
|
||||
use App\Models\Ticket;
|
||||
use App\Models\TicketMessage;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class TicketService {
|
||||
public function reply($ticket, $message, $userId)
|
||||
{
|
||||
DB::beginTransaction();
|
||||
$ticketMessage = TicketMessage::create([
|
||||
'user_id' => $userId,
|
||||
'ticket_id' => $ticket->id,
|
||||
'message' => $message
|
||||
]);
|
||||
if ($userId !== $ticket->user_id) {
|
||||
$ticket->reply_status = 0;
|
||||
} else {
|
||||
$ticket->reply_status = 1;
|
||||
}
|
||||
if (!$ticketMessage || !$ticket->save()) {
|
||||
DB::rollback();
|
||||
return false;
|
||||
}
|
||||
DB::commit();
|
||||
return $ticketMessage;
|
||||
}
|
||||
|
||||
public function replyByAdmin($ticketId, $message, $userId):void
|
||||
{
|
||||
$ticket = Ticket::where('id', $ticketId)
|
||||
->first();
|
||||
if (!$ticket) {
|
||||
abort(500, '工单不存在');
|
||||
}
|
||||
$ticket->status = 0;
|
||||
DB::beginTransaction();
|
||||
$ticketMessage = TicketMessage::create([
|
||||
'user_id' => $userId,
|
||||
'ticket_id' => $ticket->id,
|
||||
'message' => $message
|
||||
]);
|
||||
if ($userId !== $ticket->user_id) {
|
||||
$ticket->reply_status = 0;
|
||||
} else {
|
||||
$ticket->reply_status = 1;
|
||||
}
|
||||
if (!$ticketMessage || !$ticket->save()) {
|
||||
DB::rollback();
|
||||
abort(500, '工单回复失败');
|
||||
}
|
||||
DB::commit();
|
||||
$this->sendEmailNotify($ticket, $ticketMessage);
|
||||
}
|
||||
|
||||
// 半小时内不再重复通知
|
||||
private function sendEmailNotify(Ticket $ticket, TicketMessage $ticketMessage)
|
||||
{
|
||||
$user = User::find($ticket->user_id);
|
||||
$cacheKey = 'ticket_sendEmailNotify_' . $ticket->user_id;
|
||||
if (!Cache::get($cacheKey)) {
|
||||
Cache::put($cacheKey, 1, 1800);
|
||||
SendEmailJob::dispatch([
|
||||
'email' => $user->email,
|
||||
'subject' => '您在' . admin_setting('app_name', 'XBoard') . '的工单得到了回复',
|
||||
'template_name' => 'notify',
|
||||
'template_value' => [
|
||||
'name' => admin_setting('app_name', 'XBoard'),
|
||||
'url' => admin_setting('app_url'),
|
||||
'content' => "主题:{$ticket->subject}\r\n回复内容:{$ticketMessage->message}"
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Jobs\StatServerJob;
|
||||
use App\Jobs\StatUserJob;
|
||||
use App\Jobs\TrafficFetchJob;
|
||||
use App\Models\Order;
|
||||
use App\Models\Plan;
|
||||
use App\Models\User;
|
||||
|
||||
class UserService
|
||||
{
|
||||
private function calcResetDayByMonthFirstDay()
|
||||
{
|
||||
$today = date('d');
|
||||
$lastDay = date('d', strtotime('last day of +0 months'));
|
||||
return $lastDay - $today;
|
||||
}
|
||||
|
||||
private function calcResetDayByExpireDay(int $expiredAt)
|
||||
{
|
||||
$day = date('d', $expiredAt);
|
||||
$today = date('d');
|
||||
$lastDay = date('d', strtotime('last day of +0 months'));
|
||||
if ((int)$day >= (int)$today && (int)$day >= (int)$lastDay) {
|
||||
return $lastDay - $today;
|
||||
}
|
||||
if ((int)$day >= (int)$today) {
|
||||
return $day - $today;
|
||||
}
|
||||
|
||||
return $lastDay - $today + $day;
|
||||
}
|
||||
|
||||
private function calcResetDayByYearFirstDay(): int
|
||||
{
|
||||
$nextYear = strtotime(date("Y-01-01", strtotime('+1 year')));
|
||||
return (int)(($nextYear - time()) / 86400);
|
||||
}
|
||||
|
||||
private function calcResetDayByYearExpiredAt(int $expiredAt): int
|
||||
{
|
||||
$md = date('m-d', $expiredAt);
|
||||
$nowYear = strtotime(date("Y-{$md}"));
|
||||
$nextYear = strtotime('+1 year', $nowYear);
|
||||
if ($nowYear > time()) {
|
||||
return (int)(($nowYear - time()) / 86400);
|
||||
}
|
||||
return (int)(($nextYear - time()) / 86400);
|
||||
}
|
||||
|
||||
public function getResetDay(User $user)
|
||||
{
|
||||
if (!isset($user->plan)) {
|
||||
$user->plan = Plan::find($user->plan_id);
|
||||
}
|
||||
if ($user->expired_at <= time() || $user->expired_at === NULL) return null;
|
||||
// if reset method is not reset
|
||||
if ($user->plan->reset_traffic_method === 2) return null;
|
||||
switch (true) {
|
||||
case ($user->plan->reset_traffic_method === NULL): {
|
||||
$resetTrafficMethod = admin_setting('reset_traffic_method', 0);
|
||||
switch ((int)$resetTrafficMethod) {
|
||||
// month first day
|
||||
case 0:
|
||||
return $this->calcResetDayByMonthFirstDay();
|
||||
// expire day
|
||||
case 1:
|
||||
return $this->calcResetDayByExpireDay($user->expired_at);
|
||||
// no action
|
||||
case 2:
|
||||
return null;
|
||||
// year first day
|
||||
case 3:
|
||||
return $this->calcResetDayByYearFirstDay();
|
||||
// year expire day
|
||||
case 4:
|
||||
return $this->calcResetDayByYearExpiredAt($user->expired_at);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ($user->plan->reset_traffic_method === 0): {
|
||||
return $this->calcResetDayByMonthFirstDay();
|
||||
}
|
||||
case ($user->plan->reset_traffic_method === 1): {
|
||||
return $this->calcResetDayByExpireDay($user->expired_at);
|
||||
}
|
||||
case ($user->plan->reset_traffic_method === 2): {
|
||||
return null;
|
||||
}
|
||||
case ($user->plan->reset_traffic_method === 3): {
|
||||
return $this->calcResetDayByYearFirstDay();
|
||||
}
|
||||
case ($user->plan->reset_traffic_method === 4): {
|
||||
return $this->calcResetDayByYearExpiredAt($user->expired_at);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function isAvailable(User $user)
|
||||
{
|
||||
if (!$user->banned && $user->transfer_enable && ($user->expired_at > time() || $user->expired_at === NULL)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getAvailableUsers()
|
||||
{
|
||||
return User::whereRaw('u + d < transfer_enable')
|
||||
->where(function ($query) {
|
||||
$query->where('expired_at', '>=', time())
|
||||
->orWhere('expired_at', NULL);
|
||||
})
|
||||
->where('banned', 0)
|
||||
->get();
|
||||
}
|
||||
|
||||
public function getUnAvailbaleUsers()
|
||||
{
|
||||
return User::where(function ($query) {
|
||||
$query->where('expired_at', '<', time())
|
||||
->orWhere('expired_at', 0);
|
||||
})
|
||||
->where(function ($query) {
|
||||
$query->where('plan_id', NULL)
|
||||
->orWhere('transfer_enable', 0);
|
||||
})
|
||||
->get();
|
||||
}
|
||||
|
||||
public function getUsersByIds($ids)
|
||||
{
|
||||
return User::whereIn('id', $ids)->get();
|
||||
}
|
||||
|
||||
public function getAllUsers()
|
||||
{
|
||||
return User::all();
|
||||
}
|
||||
|
||||
public function addBalance(int $userId, int $balance):bool
|
||||
{
|
||||
$user = User::lockForUpdate()->find($userId);
|
||||
if (!$user) {
|
||||
return false;
|
||||
}
|
||||
$user->balance = $user->balance + $balance;
|
||||
if ($user->balance < 0) {
|
||||
return false;
|
||||
}
|
||||
if (!$user->save()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function isNotCompleteOrderByUserId(int $userId):bool
|
||||
{
|
||||
$order = Order::whereIn('status', [0, 1])
|
||||
->where('user_id', $userId)
|
||||
->first();
|
||||
if (!$order) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function trafficFetch(array $server, string $protocol, array $data, array $childServer = null)
|
||||
{
|
||||
$statService = new StatisticalService();
|
||||
$statService->setStartAt(strtotime(date('Y-m-d')));
|
||||
$statService->setUserStats();
|
||||
$statService->setServerStats();
|
||||
foreach (array_keys($data) as $userId) {
|
||||
$u = $data[$userId][0];
|
||||
$d = $data[$userId][1];
|
||||
// 如果存在子节点则使用过子节点的倍率进行进行流量计算,该计算方式依赖服务器IP地址
|
||||
if(!blank($childServer)){
|
||||
TrafficFetchJob::dispatch($u, $d, $userId, $childServer, $protocol);
|
||||
$statService->statUser($childServer['rate'], $userId, $u, $d);
|
||||
$statService->statServer($childServer['id'], $protocol, $u, $d);
|
||||
}else{
|
||||
TrafficFetchJob::dispatch($u, $d, $userId, $server, $protocol);
|
||||
$statService->statUser($server['rate'], $userId, $u, $d);
|
||||
}
|
||||
$statService->statServer($server['id'], $protocol, $u, $d);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user