improve paid torrent + hit and run

This commit is contained in:
xiaomlove
2025-06-17 20:54:18 +07:00
parent ee4757cdc9
commit 3db4537153
21 changed files with 326 additions and 70 deletions
+7 -2
View File
@@ -4,6 +4,7 @@ namespace App\Console\Commands;
use App\Jobs\CheckQueueFailedJobs;
use App\Jobs\SettleClaim;
use App\Jobs\UpdateUserDownloadPrivilege;
use App\Models\ExamUser;
use App\Models\Language;
use App\Models\PersonalAccessToken;
@@ -61,7 +62,7 @@ class Test extends Command
*/
public function handle()
{
// $failedJob = DB::table('failed_jobs')->find(555);
// $failedJob = DB::table('failed_jobs')->find(569);
//
// $payload = json_decode($failedJob->payload, true);
// dd($payload);
@@ -71,7 +72,11 @@ class Test extends Command
//
// dd($job);
CheckQueueFailedJobs::dispatch();
// UpdateUserDownloadPrivilege::dispatch(1, "yes", "test_key");
// $res = unserialize("O:36:\"App\\Jobs\\UpdateUserDownloadPrivilege\":3:{s:6:\"userId\";i:1;s:6:\"status\";s:3:\"yes\";s:9:\"reasonKey\";s:8:\"test_key\";}");
// $res = unserialize("O:36:\"App\\Jobs\\UpdateUserDownloadPrivilege\":3:{s:6:\"userId\";i:1;s:6:\"status\";s:3:\"yes\";s:9:\"reasonKey\";s:8:\"test_key\";}");
// dd($res);
UpdateUserDownloadPrivilege::dispatch(1, "yes", "test_key");
}
}
@@ -0,0 +1,31 @@
<?php
namespace App\Console\Commands\Upgrade;
use Illuminate\Console\Command;
use Nexus\Database\NexusDB;
class MigrateSnatchedHitAndRunId extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'upgrade:migrate_snatched_hr_id';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Execute the console command.
*/
public function handle()
{
NexusDB::statement("update snatched inner join hit_and_runs on snatched.userid = hit_and_runs.uid and snatched.torrentid = hit_and_runs.torrent_id set snatched.hit_and_run_id = hit_and_runs.id");
}
}
@@ -0,0 +1,31 @@
<?php
namespace App\Console\Commands\Upgrade;
use Illuminate\Console\Command;
use Nexus\Database\NexusDB;
class UpdateSnatchedBuyLogId extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'upgrade:migrate_snatched_buy_log_id';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Execute the console command.
*/
public function handle()
{
NexusDB::statement("UPDATE snatched INNER JOIN torrent_buy_logs ON snatched.userid = torrent_buy_logs.uid AND snatched.torrentid = torrent_buy_logs.torrent_id SET snatched.buy_log_id = torrent_buy_logs.id");
}
}
+1 -1
View File
@@ -48,7 +48,7 @@ class Kernel extends ConsoleKernel
$schedule->command('meilisearch:import')->weeklyOn(1, "03:00");
$schedule->command('torrent:load_pieces_hash')->dailyAt("01:00");
$schedule->job(new CheckQueueFailedJobs())->everySixHours();
$schedule->job(new ThirdPartyJob())->everyMinute();
// $schedule->job(new ThirdPartyJob())->everyMinute();
$schedule->job(new MaintainPluginState())->everyMinute();
$schedule->job(new UpdateIsSeedBoxFromUserRecordsCache())->everySixHours();
$schedule->job(new CheckCleanup())->everyFifteenMinutes();
+6
View File
@@ -3,6 +3,7 @@
namespace App\Enums;
use App\Events\NewsCreated;
use App\Events\SnatchedUpdated;
use App\Events\TorrentCreated;
use App\Events\TorrentDeleted;
use App\Events\TorrentUpdated;
@@ -12,6 +13,7 @@ use App\Events\UserDisabled;
use App\Events\UserEnabled;
use App\Events\UserUpdated;
use App\Models\News;
use App\Models\Snatch;
use App\Models\Torrent;
use App\Models\User;
@@ -28,6 +30,8 @@ final class ModelEventEnum {
const NEWS_CREATED = 'news_created';
const SNATCHED_UPDATED = 'snatched_updated';
public static array $eventMaps = [
self::TORRENT_CREATED => ['event' => TorrentCreated::class, 'model' => Torrent::class],
self::TORRENT_UPDATED => ['event' => TorrentUpdated::class, 'model' => Torrent::class],
@@ -40,5 +44,7 @@ final class ModelEventEnum {
self::USER_DISABLED => ['event' => UserDisabled::class, 'model' => User::class],
self::NEWS_CREATED => ['event' => NewsCreated::class, 'model' => News::class],
self::SNATCHED_UPDATED => ['event' => SnatchedUpdated::class, 'model' => Snatch::class],
];
}
+41
View File
@@ -0,0 +1,41 @@
<?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 SnatchedUpdated
{
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 array<int, \Illuminate\Broadcasting\Channel>
*/
public function broadcastOn(): array
{
return [
new PrivateChannel('channel-name'),
];
}
}
+8 -6
View File
@@ -34,6 +34,7 @@ class BuyTorrent implements ShouldQueue
* Execute the job.
*
* @return void
* @throws \Throwable
*/
public function handle()
{
@@ -42,27 +43,28 @@ class BuyTorrent implements ShouldQueue
$userId = $this->userId;
$torrentId = $this->torrentId;
$hasBuy = TorrentBuyLog::query()
$buyLog = TorrentBuyLog::query()
->where("uid", $userId)
->where("torrent_id", $torrentId)
->exists()
->first();
;
if ($hasBuy) {
if ($buyLog) {
//标记购买成功
do_log("$logPrefix, already bought");
$torrentRep->addBuySuccessCache($userId, $torrentId);
$torrentRep->addBuySuccessCache($userId, $torrentId, $buyLog->id);
return;
}
try {
$bonusRep = new BonusRepository();
$bonusRep->consumeToBuyTorrent($this->userId, $this->torrentId);
$buyLog = $bonusRep->consumeToBuyTorrent($this->userId, $this->torrentId);
//标记购买成功
do_log("$logPrefix, buy torrent success");
$torrentRep->addBuySuccessCache($userId, $torrentId);
$torrentRep->addBuySuccessCache($userId, $torrentId, $buyLog->id);
} catch (\Throwable $throwable) {
//标记购买失败,缓存 3600 秒,这个时间内不能再次购买
do_log("$logPrefix, buy torrent fail: " . $throwable->getMessage(), "error");
$torrentRep->addBuyFailCache($userId, $torrentId);
throw $throwable;
}
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace App\Jobs;
use App\Repositories\UserRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class UpdateUserDownloadPrivilege implements ShouldQueue
{
use Queueable;
/**
* Create a new job instance.
*/
public function __construct(public int $userId, public string $status, public string $reasonKey)
{
//
}
/**
* Execute the job.
*/
public function handle(): void
{
$rep = new UserRepository();
$rep->updateDownloadPrivileges(null, $this->userId, $this->status, $this->reasonKey);
do_log("Updating user download privilege for user {$this->userId} to {$this->status} by reason {$this->reasonKey}");
}
}
+1 -1
View File
@@ -13,7 +13,7 @@ class Snatch extends NexusModel
protected $fillable = [
'torrentid', 'userid', 'ip', 'port', 'uploaded', 'downloaded', 'to_go', 'seedtime', 'leechtime',
'last_action', 'startdat', 'completedat', 'finished'
'last_action', 'startdat', 'completedat', 'finished', 'hit_and_run_id', 'buy_log_id',
];
protected $casts = [
+4 -6
View File
@@ -252,11 +252,11 @@ class BonusRepository extends BaseRepository
}
public function consumeToBuyTorrent($uid, $torrentId, $channel = 'Web'): bool
public function consumeToBuyTorrent($uid, $torrentId, $channel = 'Web'): TorrentBuyLog
{
$torrent = Torrent::query()->findOrFail($torrentId, Torrent::$commentFields);
$requireBonus = $torrent->price;
NexusDB::transaction(function () use ($requireBonus, $torrent, $channel, $uid) {
return NexusDB::transaction(function () use ($requireBonus, $torrent, $channel, $uid) {
$userQuery = User::query();
if ($requireBonus > 0) {
$userQuery = $userQuery->lockForUpdate();
@@ -269,7 +269,7 @@ class BonusRepository extends BaseRepository
], $buyerLocale);
do_log("comment: $comment");
$this->consumeUserBonus($user, $requireBonus, BonusLogs::BUSINESS_TYPE_BUY_TORRENT, $comment);
TorrentBuyLog::query()->create([
$buyLog = TorrentBuyLog::query()->create([
'uid' => $user->id,
'torrent_id' => $torrent->id,
'price' => $requireBonus,
@@ -314,10 +314,8 @@ class BonusRepository extends BaseRepository
], $buyerLocale),
];
Message::add($buyTorrentSuccessMessage);
return $buyLog;
});
return true;
}
public function consumeUserBonus($user, $requireBonus, $logBusinessType, $logComment = '', array $userUpdates = [])
+3
View File
@@ -257,6 +257,9 @@ LUA;
"lastcleantime5" => "five",
];
$avps = Avp::query()->get()->keyBy("arg");
if ($avps->isEmpty()) {
return;
}
foreach ($arvToLevel as $arg => $level) {
/** @var NexusModel $value */
$value = $avps->get($arg);
+41 -8
View File
@@ -3,6 +3,7 @@
namespace App\Repositories;
use App\Auth\Permission;
use App\Enums\ModelEventEnum;
use App\Exceptions\InsufficientPermissionException;
use App\Exceptions\NexusException;
use App\Http\Resources\TorrentResource;
@@ -843,20 +844,51 @@ HTML;
}
/**
* 购买成功缓存,保存为 hash,一个种子一个 hash,永久有效
* 购买成功缓存 30 天并更新到 snatched 上
* @param $uid
* @param $torrentId
* @return void
* @throws \RedisException
*/
public function addBuySuccessCache($uid, $torrentId): void
public function addBuySuccessCache($uid, $torrentId, $buyLogId): void
{
NexusDB::redis()->hSet($this->getBoughtUserCacheKey($torrentId), $uid, 1);
NexusDB::redis()->set($this->getBoughtUserCacheKey($torrentId, $uid), 1, ['NX', 'EX' => 86400*30]);
$record = Snatch::query()
->where("torrentid", $torrentId)
->where("userid", $uid)
->first();
if ($record) {
$record->buy_log_id = $buyLogId;
$record->save();
publish_model_event(ModelEventEnum::SNATCHED_UPDATED, $record->id);
} else {
do_log("addBuySuccessCache, uid: $uid, torrentId: $torrentId, buyLogId: $buyLogId, snatched not exists", 'error');
}
}
public function hasBuySuccessCache($uid, $torrentId): bool
{
return NexusDB::redis()->hGet($this->getBoughtUserCacheKey($torrentId), $uid) == 1;
$key = $this->getBoughtUserCacheKey($torrentId, $uid);
if (NexusDB::redis()->exists($key)) {
return true;
}
return false;
}
public function hasBuySuccess($uid, $torrentId): bool
{
if ($this->hasBuySuccessCache($uid, $torrentId)) {
return true;
}
$buyLog = TorrentBuyLog::query()
->where("torrent_id", $torrentId)
->where("uid", $uid)
->first();
if ($buyLog) {
$this->addBuySuccessCache($uid, $torrentId, $buyLog->id);
}
return $buyLog != null;
}
/**
@@ -868,8 +900,8 @@ HTML;
*/
public function getBuyStatus($uid, $torrentId): int
{
//查询是否已经购买
if ($this->hasBuySuccessCache($uid, $torrentId)) {
//从缓存中判断是否购买
if ($this->hasBuySuccess($uid, $torrentId)) {
return self::BUY_STATUS_SUCCESS;
}
//是否购买失败过
@@ -913,12 +945,13 @@ HTML;
/**
* 购买成功缓存 key
* @update 改为使用字符串判断键是否存在即可
* @param $torrentId
* @return string
*/
public function getBoughtUserCacheKey($torrentId): string
public function getBoughtUserCacheKey($torrentId, $userId): string
{
return sprintf("%s:%s", self::BOUGHT_USER_CACHE_KEY_PREFIX, $torrentId);
return sprintf("%s:%s:%s", self::BOUGHT_USER_CACHE_KEY_PREFIX, $torrentId, $userId);
}
/**