refactor isSeedBox judgement

This commit is contained in:
xiaomlove
2025-05-11 02:33:22 +07:00
parent 6ff9d70ebc
commit 4b39d708d2
32 changed files with 1030 additions and 233 deletions
+3 -3
View File
@@ -9,6 +9,7 @@ use App\Models\PersonalAccessToken;
use App\Models\Torrent;
use App\Models\User;
use App\Repositories\ExamRepository;
use App\Repositories\SeedBoxRepository;
use App\Repositories\UploadRepository;
use Illuminate\Console\Command;
use NexusPlugin\Menu\Filament\MenuItemResource\Pages\ManageMenuItems;
@@ -55,9 +56,8 @@ class Test extends Command
*/
public function handle()
{
$rep = new MenuRepository();
$result = \Nexus\Plugin\Plugin::listEnabled();
dd($result);
$rep = new SeedBoxRepository();
$rep->updateCacheCronjob();
}
}
+2
View File
@@ -6,6 +6,7 @@ use App\Jobs\CheckCleanup;
use App\Jobs\CheckQueueFailedJobs;
use App\Jobs\MaintainPluginState;
use App\Jobs\ManagePlugin;
use App\Jobs\UpdateIsSeedBoxFromUserRecordsCache;
use App\Utils\ThirdPartyJob;
use Carbon\Carbon;
use Illuminate\Console\Scheduling\Event;
@@ -48,6 +49,7 @@ class Kernel extends ConsoleKernel
$schedule->job(new CheckQueueFailedJobs())->everySixHours()->withoutOverlapping();
$schedule->job(new ThirdPartyJob())->everyMinute()->withoutOverlapping();
$schedule->job(new MaintainPluginState())->everyMinute()->withoutOverlapping();
$schedule->job(new UpdateIsSeedBoxFromUserRecordsCache())->everySixHours()->withoutOverlapping();
$this->registerScheduleCleanup($schedule);
}
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace App\Enums\SeedBoxRecord;
enum IpAsnEnum: string {
case IP = "IP";
case ASN = "ASN";
}
@@ -0,0 +1,9 @@
<?php
namespace App\Enums\SeedBoxRecord;
enum IsAllowedEnum: int {
case YES = 1;
case NO = 0;
}
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace App\Enums\SeedBoxRecord;
enum TypeEnum: int {
case USER = 1;
case ADMIN = 2;
}
@@ -48,8 +48,8 @@ class SeedBoxRecordResource extends Resource
Forms\Components\TextInput::make('operator')->label(__('label.seed_box_record.operator')),
Forms\Components\TextInput::make('bandwidth')->label(__('label.seed_box_record.bandwidth'))->integer(),
Forms\Components\TextInput::make('asn')->label(__('label.seed_box_record.asn'))->integer(),
Forms\Components\TextInput::make('ip_begin')->label(__('label.seed_box_record.ip_begin')),
Forms\Components\TextInput::make('ip_end')->label(__('label.seed_box_record.ip_end')),
// Forms\Components\TextInput::make('ip_begin')->label(__('label.seed_box_record.ip_begin')),
// Forms\Components\TextInput::make('ip_end')->label(__('label.seed_box_record.ip_end')),
Forms\Components\TextInput::make('ip')->label(__('label.seed_box_record.ip'))->helperText(__('label.seed_box_record.ip_help')),
Forms\Components\Toggle::make('is_allowed')
->label(__('label.seed_box_record.is_allowed'))
@@ -2,16 +2,19 @@
namespace App\Filament\Resources\System\SeedBoxRecordResource\Pages;
use App\Exceptions\SeedBoxYesException;
use App\Filament\PageList;
use App\Filament\Resources\System\SeedBoxRecordResource;
use Filament\Pages\Actions;
use App\Repositories\SeedBoxRepository;
use Filament\Actions;
use Filament\Forms;
use Illuminate\Support\HtmlString;
class ListSeedBoxRecords extends PageList
{
protected static string $resource = SeedBoxRecordResource::class;
protected static ?array $checkResult = null;
protected function getHeaderActions(): array
{
return [
@@ -23,17 +26,40 @@ class ListSeedBoxRecords extends PageList
Forms\Components\TextInput::make('uid')->required()->label('UID'),
])
->modalHeading(__('admin.resources.seed_box_record.check_modal_header'))
->action(function ($data) {
try {
isIPSeedBox($data['ip'], $data['uid'], true, true);
send_admin_success_notification(nexus_trans("seed-box.is_seed_box_no"));
} catch (SeedBoxYesException $exception) {
send_admin_fail_notification(nexus_trans("seed-box.is_seed_box_yes", ['id' => $exception->getId()]));
} catch (\Throwable $throwable) {
do_log($throwable->getMessage() . $throwable->getTraceAsString(), "error");
send_admin_fail_notification($throwable->getMessage());
}
->action(function (array $data) {
$result = SeedBoxRepository::isSeedBoxFromUserRecords($data['uid'], $data['ip']);
self::$checkResult = $result;
// return $result;
// $this->replaceMountedAction("checkResult", ['result' => $result]);
// if ($checkResult['result']) {
// send_admin_success_notification(nexus_trans("seed-box.is_seed_box_yes", ['desc' => $checkResult['desc']]));
// } else {
// send_admin_fail_notification(nexus_trans("seed-box.is_seed_box_no", ['desc' => $checkResult['desc']]));
// }
})
->registerModalActions([
Actions\Action::make('checkResult')
->modalHeading(function () {
if (self::$checkResult !== null) {
if (self::$checkResult['result']) {
return nexus_trans("seed-box.is_seed_box_yes");
} else {
return nexus_trans("seed-box.is_seed_box_no");
}
}
return 'Unknown';
})
->action(null)
->modalSubmitAction(false)
->modalCancelAction(false)
->modalDescription(fn () => new HtmlString(self::$checkResult['desc'] ?? ''))
// ->modalContent(fn () => new HtmlString(self::$checkResult['desc'] ?? ''))
])
->after(function() {
$this->mountAction("checkResult");
})
,
];
}
}
@@ -0,0 +1,39 @@
<?php
namespace App\Jobs;
use App\Enums\SeedBoxRecord\IpAsnEnum;
use App\Enums\SeedBoxRecord\IsAllowedEnum;
use App\Repositories\SeedBoxRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class UpdateIsSeedBoxFromUserRecordsCache implements ShouldQueue
{
use Queueable;
/**
* Create a new job instance.
*/
public function __construct()
{
//
}
/**
* Execute the job.
*/
public function handle(): void
{
$rep = new SeedBoxRepository();
foreach (IpAsnEnum::cases() as $field) {
foreach (IsAllowedEnum::cases() as $isAllowed) {
$rep->updateUserCacheCronjob($isAllowed, $field);
do_log("SeedBoxRepository::updateUserCacheCronjob isAllowed: $isAllowed->name, field: $field->name success");
$rep->updateAdminCacheCronjob($isAllowed, $field);
do_log("SeedBoxRepository::updateAdminCacheCronjob isAllowed: $isAllowed->name, field: $field->name success");
}
}
do_log("UpdateIsSeedBoxFromUserRecordsCache done!");
}
}
+42
View File
@@ -2,7 +2,12 @@
namespace App\Models;
use App\Enums\SeedBoxRecord\IpAsnEnum;
use App\Enums\SeedBoxRecord\IsAllowedEnum;
use App\Enums\SeedBoxRecord\TypeEnum;
use App\Repositories\SeedBoxRepository;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Nexus\Database\NexusDB;
class SeedBoxRecord extends NexusModel
{
@@ -30,6 +35,43 @@ class SeedBoxRecord extends NexusModel
self::STATUS_DENIED => ['text' => 'Denied'],
];
protected static function booted(): void
{
static::saved(function (SeedBoxRecord $model) {
self::updateCache($model);
});
static::deleted(function (SeedBoxRecord $model) {
self::updateCache($model);
});
}
private static function updateCache(SeedBoxRecord $model): void
{
SeedBoxRepository::updateCache(
$model->type == TypeEnum::ADMIN->value ? 0 : $model->uid,
TypeEnum::from($model->type),
IsAllowedEnum::from($model->is_allowed),
!empty($model->ip) ? IpAsnEnum::IP : IpAsnEnum::ASN,
);
}
public static function getValidQuery(TypeEnum $type, IsAllowedEnum $isAllowed, IpAsnEnum $field)
{
$query = self::query()
->where('status', self::STATUS_ALLOWED)
->where('type', $type->value)
->where('is_allowed', $isAllowed->value)
;
if ($field == IpAsnEnum::IP) {
$query->whereNotNull("ip");
} elseif ($field == IpAsnEnum::ASN) {
$query->where("asn", ">", 0);
} else {
throw new \InvalidArgumentException("Invalid ipOrAsn");
}
return $query;
}
protected function typeText(): Attribute
{
return new Attribute(
+1 -1
View File
@@ -80,7 +80,7 @@ class AppPanelProvider extends PanelProvider
])
->navigationItems([
NavigationItem::make('Horizon')
->label(nexus_trans('admin.sidebar.queue_monitor', [], Auth::user() ? get_langfolder_cookie(true) : 'en'))
->label(fn () => nexus_trans('admin.sidebar.queue_monitor', [], Auth::user() ? get_langfolder_cookie(true) : 'en'))
->icon('heroicon-o-presentation-chart-line')
->group('System')
->sort(99)
+201 -11
View File
@@ -1,22 +1,30 @@
<?php
namespace App\Repositories;
use App\Events\SeedBoxRecordUpdated;
use App\Enums\SeedBoxRecord\IpAsnEnum;
use App\Enums\SeedBoxRecord\IsAllowedEnum;
use App\Enums\SeedBoxRecord\TypeEnum;
use App\Exceptions\InsufficientPermissionException;
use App\Models\Message;
use App\Models\Poll;
use App\Models\SeedBoxRecord;
use App\Models\Torrent;
use App\Models\User;
use GeoIp2\Database\Reader;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use MaxMind\Db\Reader\InvalidDatabaseException;
use Nexus\Database\NexusDB;
use PhpIP\IP;
use PhpIP\IPBlock;
class SeedBoxRepository extends BaseRepository
{
const IS_SEED_BOX_FROM_USER_RECORD_CACHE_PREFIX = "IS_SEED_BOX_FROM_USER_RECORD";
const APPROVAL_COUNT_CACHE_KEY = "SEED_BOX_RECORD_APPROVAL_NONE";
private static ?Reader $asnReader = null;
public function getList(array $params)
{
$query = Poll::query();
@@ -33,7 +41,7 @@ class SeedBoxRepository extends BaseRepository
{
$params = $this->formatParams($params);
$seedBoxRecord = SeedBoxRecord::query()->create($params);
$this->clearCache();
$this->clearApprovalCountCache();
publish_model_event("seed_box_record_created", $seedBoxRecord->id);
return $seedBoxRecord;
}
@@ -102,7 +110,7 @@ class SeedBoxRepository extends BaseRepository
$model = SeedBoxRecord::query()->findOrFail($id);
$params = $this->formatParams($params);
$model->update($params);
$this->clearCache();
$this->clearApprovalCountCache();
publish_model_event("seed_box_record_updated", $id);
return $model;
}
@@ -115,7 +123,7 @@ class SeedBoxRepository extends BaseRepository
public function delete($id, $uid)
{
$this->clearCache();
$this->clearApprovalCountCache();
publish_model_event("seed_box_record_deleted", $id);
return SeedBoxRecord::query()->whereIn('id', Arr::wrap($id))->where('uid', $uid)->delete();
}
@@ -146,7 +154,7 @@ class SeedBoxRepository extends BaseRepository
return NexusDB::transaction(function () use ($seedBoxRecord, $status, $message) {
$seedBoxRecord->status = $status;
$seedBoxRecord->save();
$this->clearCache();
$this->clearApprovalCountCache();
return Message::add($message);
});
}
@@ -173,12 +181,194 @@ class SeedBoxRepository extends BaseRepository
return '<img src="pic/misc/seed-box.png" style="vertical-align: bottom; height: 16px; margin-left: 4px" title="SeedBox" />';
}
private function clearCache()
private function clearApprovalCountCache(): void
{
NexusDB::cache_del('SEED_BOX_RECORD_APPROVAL_NONE');
// SeedBoxRecordUpdated::dispatch();
NexusDB::cache_del(self::APPROVAL_COUNT_CACHE_KEY);
}
public function updateUserCacheCronjob(IsAllowedEnum $isAllowed, IpAsnEnum $field): void
{
$size = 1000;
$page = 1;
$logPrefix = "isAllowed: $isAllowed->name, field: $field->name, page: $page, size: $size";
$selectRaw = sprintf("uid, group_concat(%s) as str", $field == IpAsnEnum::ASN ? 'asn' : 'ip');
while (true) {
$list = SeedBoxRecord::getValidQuery(TypeEnum::USER, $isAllowed, $field)
->selectRaw($selectRaw)
->groupBy('uid')
->forPage($page, $size)
->get();
if ($list->isEmpty()) {
do_log("$logPrefix, no more data ...");
break;
}
foreach ($list as $record) {
$uid = $record->uid;
$str = $record->str;
do_log("$logPrefix, handling user: $uid with $field->name: $str");
self::updateCache($record->uid, TypeEnum::USER, $isAllowed, $field, $str);
do_log("$logPrefix, handling user: $uid with $field->name: $str done!");
}
$page++;
}
do_log("$logPrefix, all done!");
}
public function updateAdminCacheCronjob(IsAllowedEnum $isAllowed, IpAsnEnum $field): void
{
$size = 1000;
$page = 1;
$logPrefix = "isAllowed: $isAllowed->name, field: $field->name, page: $page, size: $size";
$fieldName = $field == IpAsnEnum::ASN ? 'asn' : 'ip';
while (true) {
$list = SeedBoxRecord::getValidQuery(TypeEnum::ADMIN, $isAllowed, $field)
->selectRaw($fieldName)
->forPage($page, $size)
->get();
if ($list->isEmpty()) {
do_log("$logPrefix, no more data ...");
break;
}
self::updateCache(0, TypeEnum::ADMIN, $isAllowed, $field, $list->pluck($fieldName)->join(","));
$page++;
}
do_log("$logPrefix, all done!");
}
public static function updateCache(int $userId, TypeEnum $type, IsAllowedEnum $isAllowed, IpAsnEnum $field, string $ipOrAsnStr = null): void
{
if (!is_null($ipOrAsnStr)) {
$list = explode(',', $ipOrAsnStr);
} else {
$query = SeedBoxRecord::getValidQuery($type, $isAllowed, $field);
if ($userId > 0) {
$query->where('uid', $userId);
}
if ($field == IpAsnEnum::IP) {
$list = $query->pluck('ip')->toArray();
} else {
$list = $query->pluck('asn')->toArray();
}
}
$list = array_filter($list);
$key = self::getCacheKey($userId, $isAllowed, $field);
do_log("userId: $userId, type: $type->name, isAllowed: $isAllowed->name, ipOrAsn: $field->name, key: $key, list: " . json_encode($list));
NexusDB::cache_del($key);
if (!empty($list)) {
NexusDB::redis()->sadd($key, ...$list);
NexusDB::redis()->expireAt($key, time() + 86400 * 30);
}
}
public static function isSeedBoxFromUserRecords(int $userId, string $ip): array
{
$logPrefix = "userId: $userId, ip: $ip";
$redis = NexusDB::redis();
//first check from asn field
$asn = self::getAsnFromIp($ip);
$uidArr = [0, $userId];
if ($asn > 0) {
//check if allowed by asn
$logPrefix .= ", asn: $asn";
foreach($uidArr as $uid) {
$key = self::getCacheKey($uid, IsAllowedEnum::YES, IpAsnEnum::ASN);
if ($redis->sismember($key, $asn)) {
$desc = "$logPrefix, asn $asn in allowed $key, result: false";
return self::buildCheckResult(false, $desc);
}
}
foreach($uidArr as $uid) {
$key = self::getCacheKey($uid, IsAllowedEnum::NO, IpAsnEnum::ASN);
if ($redis->sismember($key, $asn)) {
$desc = ("$logPrefix, asn $asn in $key, result: true");
return self::buildCheckResult(true, $desc);
}
}
}
//then check from ip field
foreach($uidArr as $uid) {
$key = self::getCacheKey($uid, IsAllowedEnum::YES, IpAsnEnum::IP);
if ($redis->sismember($key, $ip)) {
$desc = ("$logPrefix, ip $ip in allowed $key, result: false");
return self::buildCheckResult(false, $desc);
}
}
foreach($uidArr as $uid) {
$key = self::getCacheKey($uid, IsAllowedEnum::NO, IpAsnEnum::IP);
if ($redis->sismember($key, $ip)) {
$desc = ("$logPrefix, ip $ip in $key, result: true");
return self::buildCheckResult(true, $desc);
}
}
return self::buildCheckResult(false, "not match any record, result: false");
}
private static function buildCheckResult(bool $isSeedBox, string $desc): array
{
$result = [
'result' => $isSeedBox,
'desc' => $desc,
];
do_log(json_encode($result));
return $result;
}
public static function getAsnFromIp(string $ip): int
{
//虽然 ip 对应的 asn 相对固定,但不宜设置较大的缓存时间,IP 地址较多,容易引起内存膨胀
return NexusDB::remember("IP_TO_ASN:$ip", 3600, function () use ($ip) {
$reader = self::getAsnReader();
if (is_null($reader)) {
return 0;
}
$asnObj = $reader->asn($ip);
return $asnObj->autonomousSystemNumber ?? 0;
});
}
/**
* IS_SEED_BOX_FROM_USER_RECORD:IP:INCLUDES:USER:10001
* IS_SEED_BOX_FROM_USER_RECORD:IP:EXCLUDES:USER:10001
* IS_SEED_BOX_FROM_USER_RECORD:ASN:INCLUDES:USER:10001
* IS_SEED_BOX_FROM_USER_RECORD:ASN:EXCLUDES:USER:10001
* IS_SEED_BOX_FROM_USER_RECORD:ASN:EXCLUDES:ADMIN
*
* @param int $userId
* @param int $isAllowed
* @param string $field
* @return string
*/
private static function getCacheKey(int $userId, IsAllowedEnum $isAllowed, IpAsnEnum $field): string
{
$key = sprintf(
"%s:%s:%s",
self::IS_SEED_BOX_FROM_USER_RECORD_CACHE_PREFIX,
$field->name,
$isAllowed == IsAllowedEnum::YES ? "EXCLUDES" : "INCLUDES", //允许,要排队它不是 seedBox
);
if ($userId > 0) {
$key .= ":USER:$userId";
} else {
$key .= ":ADMIN";
}
return $key;
}
/**
* @throws InvalidDatabaseException
*/
private static function getAsnReader(): ?Reader
{
if (is_null(self::$asnReader)) {
$database = nexus_env('GEOIP2_ASN_DATABASE');
if (!file_exists($database) || !is_readable($database)) {
do_log("GEOIP2_ASN_DATABASE: $database not exists or not readable", "debug");
return null;
}
self::$asnReader = new Reader($database);
}
return self::$asnReader;
}
}
+3
View File
@@ -681,6 +681,9 @@ class UserRepository extends BaseRepository
'login_logs' => 'uid',
'oauth_access_tokens' => 'user_id',
'oauth_auth_codes' => 'user_id',
'seed_box_records' => 'uid',
'user_modify_logs' => 'user_id',
'messages' => 'receiver',
];
foreach ($tables as $table => $key) {
NexusDB::statement(sprintf("delete from `%s` where `%s` in (%s)", $table, $key, $uidStr));