improve hr + agent update

This commit is contained in:
xiaomlove
2025-07-21 20:55:30 +07:00
parent 84b554f102
commit ae08039323
26 changed files with 590 additions and 23 deletions

View File

@@ -2,6 +2,15 @@
namespace App\Enums; namespace App\Enums;
use App\Events\AgentAllowCreated;
use App\Events\AgentAllowDeleted;
use App\Events\AgentAllowUpdated;
use App\Events\AgentDenyCreated;
use App\Events\AgentDenyDeleted;
use App\Events\AgentDenyUpdated;
use App\Events\HitAndRunCreated;
use App\Events\HitAndRunDeleted;
use App\Events\HitAndRunUpdated;
use App\Events\MessageCreated; use App\Events\MessageCreated;
use App\Events\NewsCreated; use App\Events\NewsCreated;
use App\Events\SnatchedUpdated; use App\Events\SnatchedUpdated;
@@ -13,6 +22,9 @@ use App\Events\UserDeleted;
use App\Events\UserDisabled; use App\Events\UserDisabled;
use App\Events\UserEnabled; use App\Events\UserEnabled;
use App\Events\UserUpdated; use App\Events\UserUpdated;
use App\Models\AgentAllow;
use App\Models\AgentDeny;
use App\Models\HitAndRun;
use App\Models\Message; use App\Models\Message;
use App\Models\News; use App\Models\News;
use App\Models\Snatch; use App\Models\Snatch;
@@ -32,6 +44,18 @@ final class ModelEventEnum {
const NEWS_CREATED = 'news_created'; const NEWS_CREATED = 'news_created';
const HIT_AND_RUN_CREATED = 'hit_and_run_created';
const HIT_AND_RUN_UPDATED = 'hit_and_run_updated';
const HIT_AND_RUN_DELETED = 'hit_and_run_deleted';
const AGENT_ALLOW_CREATED = 'agent_allow_created';
const AGENT_ALLOW_UPDATED = 'agent_allow_updated';
const AGENT_ALLOW_DELETED = 'agent_allow_deleted';
const AGENT_DENY_CREATED = 'agent_deny_created';
const AGENT_DENY_UPDATED = 'agent_deny_updated';
const AGENT_DENY_DELETED = 'agent_deny_deleted';
const SNATCHED_UPDATED = 'snatched_updated'; const SNATCHED_UPDATED = 'snatched_updated';
const MESSAGE_CREATED = 'message_created'; const MESSAGE_CREATED = 'message_created';
@@ -51,5 +75,17 @@ final class ModelEventEnum {
self::SNATCHED_UPDATED => ['event' => SnatchedUpdated::class, 'model' => Snatch::class], self::SNATCHED_UPDATED => ['event' => SnatchedUpdated::class, 'model' => Snatch::class],
self::MESSAGE_CREATED => ['event' => MessageCreated::class, 'model' => Message::class], self::MESSAGE_CREATED => ['event' => MessageCreated::class, 'model' => Message::class],
self::HIT_AND_RUN_CREATED => ['event' => HitAndRunCreated::class, 'model' => HitAndRun::class],
self::HIT_AND_RUN_UPDATED => ['event' => HitAndRunUpdated::class, 'model' => HitAndRun::class],
self::HIT_AND_RUN_DELETED => ['event' => HitAndRunDeleted::class, 'model' => HitAndRun::class],
self::AGENT_ALLOW_CREATED => ['event' => AgentAllowCreated::class, 'model' => AgentAllow::class],
self::AGENT_ALLOW_UPDATED => ['event' => AgentAllowUpdated::class, 'model' => AgentAllow::class],
self::AGENT_ALLOW_DELETED => ['event' => AgentAllowDeleted::class, 'model' => AgentAllow::class],
self::AGENT_DENY_CREATED => ['event' => AgentDenyCreated::class, 'model' => AgentDeny::class],
self::AGENT_DENY_UPDATED => ['event' => AgentDenyUpdated::class, 'model' => AgentDeny::class],
self::AGENT_DENY_DELETED => ['event' => AgentDenyDeleted::class, 'model' => AgentDeny::class],
]; ];
} }

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class AgentAllowCreated
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public ?Model $model = null;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct(Model $model)
{
$this->model = $model;
}
/**
* Get the channels the event should broadcast on.
*
* @return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn()
{
return new PrivateChannel('channel-name');
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class AgentAllowDeleted
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public ?Model $model = null;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct(Model $model)
{
$this->model = $model;
}
/**
* Get the channels the event should broadcast on.
*
* @return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn()
{
return new PrivateChannel('channel-name');
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class AgentAllowUpdated
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public ?Model $model = null;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct(Model $model)
{
$this->model = $model;
}
/**
* Get the channels the event should broadcast on.
*
* @return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn()
{
return new PrivateChannel('channel-name');
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class AgentDenyCreated
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public ?Model $model = null;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct(Model $model)
{
$this->model = $model;
}
/**
* Get the channels the event should broadcast on.
*
* @return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn()
{
return new PrivateChannel('channel-name');
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class AgentDenyDeleted
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public ?Model $model = null;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct(Model $model)
{
$this->model = $model;
}
/**
* Get the channels the event should broadcast on.
*
* @return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn()
{
return new PrivateChannel('channel-name');
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class AgentDenyUpdated
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public ?Model $model = null;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct(Model $model)
{
$this->model = $model;
}
/**
* Get the channels the event should broadcast on.
*
* @return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn()
{
return new PrivateChannel('channel-name');
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class HitAndRunCreated
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public ?Model $model = null;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct(Model $model)
{
$this->model = $model;
}
/**
* Get the channels the event should broadcast on.
*
* @return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn()
{
return new PrivateChannel('channel-name');
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class HitAndRunDeleted
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public ?Model $model = null;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct(Model $model)
{
$this->model = $model;
}
/**
* Get the channels the event should broadcast on.
*
* @return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn()
{
return new PrivateChannel('channel-name');
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class HitAndRunUpdated
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public ?Model $model = null;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct(Model $model)
{
$this->model = $model;
}
/**
* Get the channels the event should broadcast on.
*
* @return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn()
{
return new PrivateChannel('channel-name');
}
}

View File

@@ -20,6 +20,7 @@ use Illuminate\Support\Facades\Auth;
use Illuminate\Support\HtmlString; use Illuminate\Support\HtmlString;
use Filament\Infolists\Components; use Filament\Infolists\Components;
use Filament\Infolists; use Filament\Infolists;
use Nette\Utils\Html;
class HitAndRunResource extends Resource class HitAndRunResource extends Resource
{ {
@@ -146,7 +147,7 @@ class HitAndRunResource extends Resource
->label(__("label.inspect_time_left")) ->label(__("label.inspect_time_left"))
, ,
Infolists\Components\TextEntry::make('comment') Infolists\Components\TextEntry::make('comment')
->formatStateUsing(fn ($record) => nl2br($record->comment)) ->formatStateUsing(fn ($record) => new HtmlString(nl2br($record->comment)))
->label(__("label.comment")) ->label(__("label.comment"))
, ,
Infolists\Components\TextEntry::make('created_at') Infolists\Components\TextEntry::make('created_at')

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Listeners;
use App\Models\Torrent;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class ClearTorrentCache implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct()
{
//
}
/**
* Handle the event.
*/
public function handle(object $event): void
{
$torrentId = $event->model?->id ?? 0;
if ($torrentId > 0) {
$infoHash = Torrent::query()->where('id', $torrentId)->value('info_hash');
clear_torrent_cache($infoHash);
do_log("success clear torrent: $torrentId cache with info_hash: " . rawurlencode($infoHash));
} else {
do_log("no torrent id", 'error');
}
}
}

View File

@@ -2,6 +2,8 @@
namespace App\Models; namespace App\Models;
use App\Enums\ModelEventEnum;
class AgentAllow extends NexusModel class AgentAllow extends NexusModel
{ {
protected $table = 'agent_allowed_family'; protected $table = 'agent_allowed_family';
@@ -20,6 +22,19 @@ class AgentAllow extends NexusModel
self::MATCH_TYPE_HEX => 'hex', self::MATCH_TYPE_HEX => 'hex',
]; ];
protected static function booted()
{
static::created(function ($model) {
fire_event(ModelEventEnum::AGENT_ALLOW_CREATED, $model);
});
static::updated(function ($model) {
fire_event(ModelEventEnum::AGENT_ALLOW_UPDATED, $model);
});
static::deleted(function ($model) {
fire_event(ModelEventEnum::AGENT_ALLOW_DELETED, $model);
});
}
public function denies(): \Illuminate\Database\Eloquent\Relations\HasMany public function denies(): \Illuminate\Database\Eloquent\Relations\HasMany
{ {
return $this->hasMany(AgentDeny::class, 'family_id'); return $this->hasMany(AgentDeny::class, 'family_id');

View File

@@ -2,6 +2,8 @@
namespace App\Models; namespace App\Models;
use App\Enums\ModelEventEnum;
class AgentDeny extends NexusModel class AgentDeny extends NexusModel
{ {
protected $table = 'agent_allowed_exception'; protected $table = 'agent_allowed_exception';
@@ -10,6 +12,19 @@ class AgentDeny extends NexusModel
'family_id', 'name', 'peer_id', 'agent', 'comment' 'family_id', 'name', 'peer_id', 'agent', 'comment'
]; ];
protected static function booted()
{
static::created(function ($model) {
fire_event(ModelEventEnum::AGENT_DENY_CREATED, $model);
});
static::updated(function ($model) {
fire_event(ModelEventEnum::AGENT_DENY_UPDATED, $model);
});
static::deleted(function ($model) {
fire_event(ModelEventEnum::AGENT_DENY_DELETED, $model);
});
}
public function family(): \Illuminate\Database\Eloquent\Relations\BelongsTo public function family(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{ {
return $this->belongsTo(AgentAllow::class, 'family_id'); return $this->belongsTo(AgentAllow::class, 'family_id');

View File

@@ -2,9 +2,10 @@
namespace App\Models; namespace App\Models;
use App\Enums\ModelEventEnum;
use Carbon\Carbon; use Carbon\Carbon;
use Carbon\Exceptions\InvalidArgumentException;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Nexus\Database\NexusDB;
class HitAndRun extends NexusModel class HitAndRun extends NexusModel
{ {
@@ -43,6 +44,31 @@ class HitAndRun extends NexusModel
const MINIMUM_IGNORE_USER_CLASS = User::CLASS_VIP; const MINIMUM_IGNORE_USER_CLASS = User::CLASS_VIP;
protected static function booted()
{
static::saved(function ($model) {
self::clearCache($model);
});
static::deleted(function ($model) {
self::clearCache($model, ModelEventEnum::HIT_AND_RUN_DELETED);
});
}
public static function getCacheKey(int $userId, int $torrentId): string
{
return sprintf("hit_and_run:user:%d:torrent:%d", $userId, $torrentId);
}
public static function clearCache(HitAndRun $hitAndRun, string $event = ModelEventEnum::HIT_AND_RUN_UPDATED): void
{
NexusDB::cache_del(self::getCacheKey($hitAndRun->uid, $hitAndRun->torrent_id));
fire_event($event, $hitAndRun);
do_log(sprintf(
"userId: %s, torrentId: %s hit and run cache cleared, and trigger event: %s",
$hitAndRun->uid, $hitAndRun->torrent_id, $event
));
}
protected function seedTimeRequired(): Attribute protected function seedTimeRequired(): Attribute
{ {
return new Attribute( return new Attribute(

View File

@@ -8,6 +8,7 @@ use App\Events\TorrentDeleted;
use App\Events\TorrentUpdated; use App\Events\TorrentUpdated;
use App\Events\UserDeleted; use App\Events\UserDeleted;
use App\Events\UserDisabled; use App\Events\UserDisabled;
use App\Listeners\ClearTorrentCache;
use App\Listeners\DeductUserBonusWhenTorrentDeleted; use App\Listeners\DeductUserBonusWhenTorrentDeleted;
use App\Listeners\FetchTorrentImdb; use App\Listeners\FetchTorrentImdb;
use App\Listeners\FetchTorrentPTGen; use App\Listeners\FetchTorrentPTGen;
@@ -48,6 +49,7 @@ class EventServiceProvider extends ServiceProvider
SyncTorrentToElasticsearch::class, SyncTorrentToElasticsearch::class,
SyncTorrentToMeilisearch::class, SyncTorrentToMeilisearch::class,
SendEmailNotificationWhenTorrentCreated::class, SendEmailNotificationWhenTorrentCreated::class,
ClearTorrentCache::class,
], ],
TorrentDeleted::class => [ TorrentDeleted::class => [
DeductUserBonusWhenTorrentDeleted::class, DeductUserBonusWhenTorrentDeleted::class,

View File

@@ -1,6 +1,7 @@
<?php <?php
namespace App\Repositories; namespace App\Repositories;
use App\Enums\ModelEventEnum;
use App\Models\HitAndRun; use App\Models\HitAndRun;
use App\Models\Message; use App\Models\Message;
use App\Models\SearchBox; use App\Models\SearchBox;
@@ -60,16 +61,27 @@ class HitAndRunRepository extends BaseRepository
{ {
$model = HitAndRun::query()->findOrFail($id); $model = HitAndRun::query()->findOrFail($id);
$result = $model->delete(); $result = $model->delete();
HitAndRun::clearCache($model, ModelEventEnum::HIT_AND_RUN_DELETED);
return $result; return $result;
} }
public function bulkDelete(array $params, User $user) public function bulkDelete(array $params, User $user)
{ {
$result = $this->getBulkQuery($params)->delete(); $baseQuery = $this->getBulkQuery($params);
$list = $baseQuery->clone()->get();
if ($list->isEmpty()) {
return 0;
}
$result = $baseQuery->delete();
do_log(sprintf( do_log(sprintf(
'user: %s bulk delete by filter: %s, result: %s', 'user: %s bulk delete by filter: %s, result: %s',
$user->id, json_encode($params), json_encode($result) $user->id, json_encode($params), json_encode($result)
), 'alert'); ), 'alert');
if ($result) {
foreach ($list as $record) {
HitAndRun::clearCache($record, ModelEventEnum::HIT_AND_RUN_DELETED);
}
}
return $result; return $result;
} }
@@ -204,7 +216,8 @@ class HitAndRunRepository extends BaseRepository
//check leech time //check leech time
if (isset($setting['leech_time_minimum']) && $setting['leech_time_minimum'] > 0) { if (isset($setting['leech_time_minimum']) && $setting['leech_time_minimum'] > 0) {
$targetLeechTime = $row->snatch->leechtime; // $targetLeechTime = $row->snatch->leechtime;
$targetLeechTime = $row->leech_time_no_seeder;//使用自身记录的值
$requireLeechTime = bcmul($setting['leech_time_minimum'], 3600); $requireLeechTime = bcmul($setting['leech_time_minimum'], 3600);
do_log("$currentLog, targetLeechTime: $targetLeechTime, requireLeechTime: $requireLeechTime"); do_log("$currentLog, targetLeechTime: $targetLeechTime, requireLeechTime: $requireLeechTime");
if ($targetLeechTime >= $requireLeechTime) { if ($targetLeechTime >= $requireLeechTime) {
@@ -331,6 +344,7 @@ class HitAndRunRepository extends BaseRepository
} else { } else {
do_log($hitAndRun->toJson() . ", [$logPrefix], user do not accept hr_reached notification", 'notice'); do_log($hitAndRun->toJson() . ", [$logPrefix], user do not accept hr_reached notification", 'notice');
} }
HitAndRun::clearCache($hitAndRun);
return true; return true;
} }
@@ -369,7 +383,7 @@ class HitAndRunRepository extends BaseRepository
], $hitAndRun->user->locale), ], $hitAndRun->user->locale),
]; ];
Message::query()->insert($message); Message::query()->insert($message);
HitAndRun::clearCache($hitAndRun);
return true; return true;
} }
@@ -423,6 +437,7 @@ class HitAndRunRepository extends BaseRepository
'reason' => $comment 'reason' => $comment
]; ];
UserBanLog::query()->insert($userBanLog); UserBanLog::query()->insert($userBanLog);
fire_event(ModelEventEnum::USER_UPDATED, $user);
do_log("Disable user: " . nexus_json_encode($userBanLog)); do_log("Disable user: " . nexus_json_encode($userBanLog));
} }
} }
@@ -499,16 +514,25 @@ class HitAndRunRepository extends BaseRepository
public function bulkPardon(array $params, User $user): int public function bulkPardon(array $params, User $user): int
{ {
$query = $this->getBulkQuery($params)->whereIn('status', $this->getCanPardonStatus()); $baseQuery = $this->getBulkQuery($params)->whereIn('status', $this->getCanPardonStatus());
$list = $baseQuery->clone()->get();
if ($list->isEmpty()) {
return 0;
}
$update = [ $update = [
'status' => HitAndRun::STATUS_PARDONED, 'status' => HitAndRun::STATUS_PARDONED,
'comment' => $this->getCommentUpdateRaw(addslashes('Pardon by ' . $user->username)), 'comment' => $this->getCommentUpdateRaw(addslashes('Pardon by ' . $user->username)),
]; ];
$affected = $query->update($update); $affected = $baseQuery->update($update);
do_log(sprintf( do_log(sprintf(
'user: %s bulk pardon by filter: %s, affected: %s', 'user: %s bulk pardon by filter: %s, affected: %s',
$user->id, json_encode($params), $affected $user->id, json_encode($params), $affected
), 'alert'); ), 'alert');
if ($affected) {
foreach ($list as $item) {
HitAndRun::clearCache($item);
}
}
return $affected; return $affected;
} }

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('hit_and_runs', function (Blueprint $table) {
$table->bigInteger('leech_time_no_seeder')->default(0);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('hit_and_runs', function (Blueprint $table) {
$table->dropColumn('leech_time_no_seeder');
});
}
};

View File

@@ -1,6 +1,6 @@
<?php <?php
defined('VERSION_NUMBER') || define('VERSION_NUMBER', '1.9.7'); defined('VERSION_NUMBER') || define('VERSION_NUMBER', '1.9.7');
defined('RELEASE_DATE') || define('RELEASE_DATE', '2025-07-18'); defined('RELEASE_DATE') || define('RELEASE_DATE', '2025-07-21');
defined('IN_TRACKER') || define('IN_TRACKER', false); defined('IN_TRACKER') || define('IN_TRACKER', false);
defined('PROJECTNAME') || define("PROJECTNAME","NexusPHP"); defined('PROJECTNAME') || define("PROJECTNAME","NexusPHP");
defined('NEXUSPHPURL') || define("NEXUSPHPURL","https://nexusphp.org"); defined('NEXUSPHPURL') || define("NEXUSPHPURL","https://nexusphp.org");

View File

@@ -5935,11 +5935,14 @@ function build_medal_image(\Illuminate\Support\Collection $medals, $maxHeight =
function insert_torrent_tags($torrentId, $tagIdArr, $sync = false) function insert_torrent_tags($torrentId, $tagIdArr, $sync = false)
{ {
$specialTags = \App\Models\Tag::listSpecial(); $specialTags = \App\Models\Tag::listSpecial();
$canSetSpecialTag = user_can('torrent-set-special-tag'); $canSetSpecialTag = \App\Auth\Permission::canSetTorrentSpecialTag();
$dateTimeStringNow = date('Y-m-d H:i:s'); $dateTimeStringNow = date('Y-m-d H:i:s');
if ($sync) { if ($sync) {
\App\Models\TorrentTag::query()->where("torrent_id", $torrentId)->delete(); $delQuery = \App\Models\TorrentTag::query()->where("torrent_id", $torrentId);
// sql_query("delete from torrent_tags where torrent_id = $torrentId"); if (!$canSetSpecialTag) {
$delQuery->whereNotIn("tag_id", $specialTags);
}
$delQuery->delete();
} }
if (empty($tagIdArr)) { if (empty($tagIdArr)) {
return; return;
@@ -5956,7 +5959,6 @@ function insert_torrent_tags($torrentId, $tagIdArr, $sync = false)
$insertTagsSql .= implode(', ', $values); $insertTagsSql .= implode(', ', $values);
do_log("[INSERT_TAGS], torrent: $torrentId with tags: " . nexus_json_encode($tagIdArr)); do_log("[INSERT_TAGS], torrent: $torrentId with tags: " . nexus_json_encode($tagIdArr));
\Nexus\Database\NexusDB::statement($insertTagsSql); \Nexus\Database\NexusDB::statement($insertTagsSql);
// sql_query($insertTagsSql);
} }
function get_smile($num) function get_smile($num)

View File

@@ -1208,6 +1208,19 @@ function clear_agent_allow_deny_cache()
\Nexus\Database\NexusDB::cache_del($denyCacheKey . $suffix); \Nexus\Database\NexusDB::cache_del($denyCacheKey . $suffix);
} }
} }
/**
* @see announce.php
* @param $infoHash
* @return void
*/
function clear_torrent_cache($infoHash)
{
do_log("clear_torrent_cache");
\Nexus\Database\NexusDB::cache_del('torrent_hash_'.$infoHash.'_content');
\Nexus\Database\NexusDB::cache_del("torrent_not_exists:$infoHash");
}
function user_can($permission, $fail = false, $uid = 0): bool function user_can($permission, $fail = false, $uid = 0): bool
{ {
$log = "permission: $permission, fail: $fail, user: $uid"; $log = "permission: $permission, fail: $fail, user: $uid";
@@ -1338,6 +1351,7 @@ function is_danger_url($url): bool
return false; return false;
} }
//here must retrieve the real time info, no cache!!!
function get_snatch_info($torrentId, $userId) function get_snatch_info($torrentId, $userId)
{ {
return mysql_fetch_assoc(sql_query(sprintf('select * from snatched where torrentid = %s and userid = %s order by id desc limit 1', $torrentId, $userId))); return mysql_fetch_assoc(sql_query(sprintf('select * from snatched where torrentid = %s and userid = %s order by id desc limit 1', $torrentId, $userId)));

View File

@@ -562,9 +562,11 @@ if (($left > 0 || $event == "completed") && $az['class'] < \App\Models\HitAndRun
$hrMode = \App\Models\HitAndRun::getConfig('mode', $torrent['mode']); $hrMode = \App\Models\HitAndRun::getConfig('mode', $torrent['mode']);
$hrLog = sprintf("[HR_LOG] user: %d, torrent: %d, hrMode: %s", $userid, $torrentid, $hrMode); $hrLog = sprintf("[HR_LOG] user: %d, torrent: %d, hrMode: %s", $userid, $torrentid, $hrMode);
if ($hrMode == \App\Models\HitAndRun::MODE_GLOBAL || ($hrMode == \App\Models\HitAndRun::MODE_MANUAL && $torrent['hr'] == \App\Models\Torrent::HR_YES)) { if ($hrMode == \App\Models\HitAndRun::MODE_GLOBAL || ($hrMode == \App\Models\HitAndRun::MODE_MANUAL && $torrent['hr'] == \App\Models\Torrent::HR_YES)) {
$hrCacheKey = sprintf("hit_and_run:%d:%d", $userid, $torrentid); //change key to expire cache, so ttl don't set too long
$hrExists = \Nexus\Database\NexusDB::remember($hrCacheKey, mt_rand(86400*365*5, 86400*365*10), function () use ($torrentid, $userid) { $hrCacheKey = \App\Models\HitAndRun::getCacheKey( $userid, $torrentid);
return \App\Models\HitAndRun::query()->where("uid", $userid)->where("torrent_id", $torrentid)->exists() ? 1 : 0; $hrExists = \Nexus\Database\NexusDB::remember($hrCacheKey, mt_rand(86400, 86400*3), function () use ($torrentid, $userid) {
$record = \App\Models\HitAndRun::query()->where("uid", $userid)->where("torrent_id", $torrentid)->first();
return $record ? $record->toJson() : null;
}); });
$hrLog .= ", hrExists: $hrExists"; $hrLog .= ", hrExists: $hrExists";
if (!$hrExists) { if (!$hrExists) {
@@ -589,12 +591,30 @@ if (($left > 0 || $event == "completed") && $az['class'] < \App\Models\HitAndRun
do_log("$hrLog, total downloaded: {$snatchInfo['downloaded']} >= required: $requiredDownloaded, [INSERT_H&R], sql: $sql, affectedRows: $affectedRows, hitAndRunId: $hitAndRunId"); do_log("$hrLog, total downloaded: {$snatchInfo['downloaded']} >= required: $requiredDownloaded, [INSERT_H&R], sql: $sql, affectedRows: $affectedRows, hitAndRunId: $hitAndRunId");
if ($hitAndRunId > 0) { if ($hitAndRunId > 0) {
sql_query("update snatched set hit_and_run_id = $hitAndRunId where id = {$snatchInfo['id']}"); sql_query("update snatched set hit_and_run_id = $hitAndRunId where id = {$snatchInfo['id']}");
$hitAndRunRecord = \App\Models\HitAndRun::query()->where("uid", $userid)->where("torrent_id", $torrentid)->first();
fire_event(\App\Enums\ModelEventEnum::HIT_AND_RUN_CREATED, $hitAndRunRecord);
} }
} else { } else {
do_log("$hrLog, total downloaded: {$snatchInfo['downloaded']} < required: $requiredDownloaded", "debug"); do_log("$hrLog, total downloaded: {$snatchInfo['downloaded']} < required: $requiredDownloaded", "debug");
} }
} else { } else {
do_log("$hrLog, already exists", "debug"); $hrLog .= ", already exists";
if (isset($self) && $torrent['seeders'] <= 0) {
$hrLeechTimeMin = \App\Models\HitAndRun::getConfig('leech_time_minimum', $torrent['mode']);
if ($hrLeechTimeMin > 0) {
$hrLog .= ", enable hrLeechTimeMin: $hrLeechTimeMin";
$hrInfo = json_decode($hrExists, true);
if ($hrInfo['status'] == \App\Models\HitAndRun::STATUS_INSPECTING) {
sql_query("update hit_and_runs set leech_time_no_seeder = leech_time_no_seeder + {$self['announcetime']} where id = {$hrInfo['id']} limit 1");
} else {
do_log("$hrLog, hr status != STATUS_INSPECTING", "debug");
}
} else {
do_log("$hrLog, not enable hrLeechTimeMin", "debug");
}
} else {
do_log("$hrLog, no self or seeders({$torrent['seeders']}) > 0", "debug");
}
} }
} else { } else {
do_log("$hrLog, not match", "debug"); do_log("$hrLog, not match", "debug");

View File

@@ -164,7 +164,7 @@ JS;
} else { } else {
list($pagertop, $pagerbottom, $limit) = pager($pageSize, $number, "?id=$id&menu=$menuSelected&"); list($pagertop, $pagerbottom, $limit) = pager($pageSize, $number, "?id=$id&menu=$menuSelected&");
$haremAdditionFactor = (float)get_setting('bonus.harem_addition'); $haremAdditionFactor = (float)get_setting('bonus.harem_addition');
$ret = sql_query("SELECT id, username, email, uploaded, downloaded, status, warned, enabled, donor, email,seeding_torrent_count, seeding_torrent_size, last_announce_at FROM users WHERE $whereStr $limit") or sqlerr(); $ret = sql_query("SELECT id, username, email, uploaded, downloaded, status, warned, enabled, donor, email, seed_points_per_hour, seeding_torrent_count, seeding_torrent_size, last_announce_at FROM users WHERE $whereStr $limit") or sqlerr();
$num = mysql_num_rows($ret); $num = mysql_num_rows($ret);
print("<tr> print("<tr>

View File

@@ -82,8 +82,8 @@ return [
'inspect_time_help' => 'The duration of the examination is calculated from the completion of the download, in hours', 'inspect_time_help' => 'The duration of the examination is calculated from the completion of the download, in hours',
'seed_time_minimum' => 'Seed time minimum', 'seed_time_minimum' => 'Seed time minimum',
'seed_time_minimum_help' => 'The shortest time to do the seeds to meet the standard, in hours, must be less than the length of the expedition', 'seed_time_minimum_help' => 'The shortest time to do the seeds to meet the standard, in hours, must be less than the length of the expedition',
'leech_time_minimum' => 'Leech time minimum', 'leech_time_minimum' => 'Leech time minimum(no seeder)',
'leech_time_minimum_help' => 'The minimum download time required to meet the standard, in hours, must be less than the length of the expedition. Set to 0 to disable', 'leech_time_minimum_help' => 'The minimum download time required to meet the standard when no seeder, in hours, must be less than the length of the expedition. Set to 0 to disable',
'ignore_when_ratio_reach' => 'Achievement Sharing Rate', 'ignore_when_ratio_reach' => 'Achievement Sharing Rate',
'ignore_when_ratio_reach_help' => 'The minimum sharing rate to meet the standard', 'ignore_when_ratio_reach_help' => 'The minimum sharing rate to meet the standard',
'ban_user_when_counts_reach' => 'H&R counts limit', 'ban_user_when_counts_reach' => 'H&R counts limit',

View File

@@ -82,8 +82,8 @@ return [
'inspect_time_help' => '考察时长自下载完成后开始计算,单位:小时', 'inspect_time_help' => '考察时长自下载完成后开始计算,单位:小时',
'seed_time_minimum' => '达标做种时长', 'seed_time_minimum' => '达标做种时长',
'seed_time_minimum_help' => '达标的最短做种时长,单位:小时,必须小于考察时长', 'seed_time_minimum_help' => '达标的最短做种时长,单位:小时,必须小于考察时长',
'leech_time_minimum' => '达标下载时长', 'leech_time_minimum' => '达标下载时长(死种时)',
'leech_time_minimum_help' => '达标的最短下载时长,单位:小时,必须小于考察时长。设置为 0 不启用', 'leech_time_minimum_help' => '达标的最短下载时长,是死种时的下载时长,单位:小时,必须小于考察时长。设置为 0 不启用',
'ignore_when_ratio_reach' => '达标分享率', 'ignore_when_ratio_reach' => '达标分享率',
'ignore_when_ratio_reach_help' => '达标的最小分享率', 'ignore_when_ratio_reach_help' => '达标的最小分享率',
'ban_user_when_counts_reach' => 'H&R 数量上限', 'ban_user_when_counts_reach' => 'H&R 数量上限',

View File

@@ -82,8 +82,8 @@ return [
'inspect_time_help' => '考察時長自下載完成後開始計算,單位:小時', 'inspect_time_help' => '考察時長自下載完成後開始計算,單位:小時',
'seed_time_minimum' => '達標做種時長', 'seed_time_minimum' => '達標做種時長',
'seed_time_minimum_help' => '達標的最短做種時長,單位:小時,必須小於考察時長', 'seed_time_minimum_help' => '達標的最短做種時長,單位:小時,必須小於考察時長',
'leech_time_minimum' => '達標下載時長', 'leech_time_minimum' => '達標下載時長(死種時)',
'leech_time_minimum_help' => '達標的最短下載時長,單位:小時,必須小於考察時長。設置爲 0 不啓用', 'leech_time_minimum_help' => '達標的最短下載時長,是死種時的下載時長,單位:小時,必須小於考察時長。設置爲 0 不啓用',
'ignore_when_ratio_reach' => '達標分享率', 'ignore_when_ratio_reach' => '達標分享率',
'ignore_when_ratio_reach_help' => '達標的最小分享率', 'ignore_when_ratio_reach_help' => '達標的最小分享率',
'ban_user_when_counts_reach' => 'H&R 數量上限', 'ban_user_when_counts_reach' => 'H&R 數量上限',