Files
nexusphp/app/Repositories/ClaimRepository.php
2025-05-16 02:43:45 +07:00

422 lines
19 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Repositories;
use App\Jobs\SettleClaim;
use App\Models\Claim;
use App\Models\Message;
use App\Models\Snatch;
use App\Models\Torrent;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Nexus\Database\NexusDB;
class ClaimRepository extends BaseRepository
{
const STAT_CACHE_PREFIX = 'claim_stats_';
const SETTLE_PAGINATE_STARTS = 2000;
const SETTLE_MSG_SLICE_COUNT = 1000;
public function getList(array $params)
{
$query = Claim::query()->with(['family']);
if (!empty($params['family_id'])) {
$query->where('family_id', $params['family_id']);
}
$query->orderBy('family_id', 'desc');
return $query->paginate();
}
public function store($uid, $torrentId)
{
$snatch = $this->canBeClaimByUser($torrentId, $uid);
$insert = [
'uid' => $uid,
'torrent_id' => $torrentId,
'snatched_id' => $snatch->id,
'seed_time_begin' => $snatch->seedtime,
'uploaded_begin' => $snatch->uploaded,
];
$this->clearStatsCache($uid);
return Claim::query()->create($insert);
}
public function canBeClaimByUser(int $torrentId, int $uid)
{
$isEnabled = Claim::getConfigIsEnabled();
if (!$isEnabled) {
throw new \RuntimeException(nexus_trans("torrent.claim_disabled"));
}
$exists = Claim::query()->where('uid', $uid)->where('torrent_id', $torrentId)->exists();
if ($exists) {
throw new \RuntimeException(nexus_trans("torrent.claim_already"));
}
$snatch = Snatch::query()->where('userid', $uid)->where('torrentid', $torrentId)->first();
if (!$snatch) {
throw new \RuntimeException(nexus_trans("torrent.no_snatch"));
}
$claimTorrentTTL = Claim::getConfigTorrentTTL();
$torrent = Torrent::query()->findOrFail($torrentId, Torrent::$commentFields);
if ($torrent->added->addDays($claimTorrentTTL)->gte(Carbon::now())) {
throw new \RuntimeException(nexus_trans("torrent.can_no_be_claimed_yet"));
}
if (!has_role_work_seeding($uid)) {
$max = Claim::getConfigTorrentUpLimit();
$count = Claim::query()->where('uid', $uid)->count();
if ($count >= $max) {
throw new \RuntimeException(nexus_trans("torrent.claim_number_reach_torrent_maximum"));
}
$max = Claim::getConfigUserUpLimit();
$count = Claim::query()->where('torrent_id', $torrentId)->count();
if ($count >= $max) {
throw new \RuntimeException(nexus_trans("torrent.claim_number_reach_user_maximum"));
}
}
return $snatch;
}
public function update(array $params, $id)
{
$model = Claim::query()->findOrFail($id);
$model->update($params);
return $model;
}
public function getDetail($id)
{
$model = Claim::query()->findOrFail($id);
return $model;
}
public function delete($id, $uid)
{
$isEnabled = Claim::getConfigIsEnabled();
if (!$isEnabled) {
throw new \RuntimeException(nexus_trans("torrent.claim_disabled"));
}
$model = Claim::query()->findOrFail($id);
if ($model->uid != $uid) {
throw new \RuntimeException("No permission");
}
$deductBonus = Claim::getConfigGiveUpDeductBonus();
return NexusDB::transaction(function () use ($model, $deductBonus) {
$this->clearStatsCache($model->uid);
User::query()->where('id', $model->uid)->decrement('seedbonus', $deductBonus);
do_log(sprintf("[GIVE_UP_CLAIM_TORRENT], user: %s, deduct bonus: %s", $model->uid, $deductBonus), 'alert');
return $model->delete();
});
}
public function getStats($uid)
{
$key = self::STAT_CACHE_PREFIX . $uid;
return NexusDB::remember($key, 3600, function () use ($uid) {
$max = Claim::getConfigTorrentUpLimit();
return sprintf('%s/%s', Claim::query()->where('uid', $uid)->count(), $max);
});
}
public function clearStatsCache($uid)
{
NexusDB::cache_del(self::STAT_CACHE_PREFIX . $uid);
}
public function settleCronjob(): array
{
$startOfThisMonth = Carbon::now()->startOfMonth();
$query = Claim::query()
->selectRaw("uid, count(*) as count")
->where("created_at", "<", $startOfThisMonth)
->where(function (Builder $query) use ($startOfThisMonth) {
$query->where('last_settle_at', '<', $startOfThisMonth)->orWhereNull('last_settle_at');
})
->groupBy('uid')
;
$size = 1000;
$page = 1;
$successCount = $failCount = 0;
while (true) {
$logPrefix = "size: $size, page: $page";
$result = (clone $query)->forPage($page, $size)->get();
if ($result->isEmpty()) {
do_log("$logPrefix, no more data...");
break;
}
do_log("get counts: " . $result->count());
foreach ($result as $row) {
$uid = $row->uid;
if ($row->count > self::SETTLE_PAGINATE_STARTS) {
SettleClaim::dispatch($uid);
} else {
do_log("$logPrefix, begin to settle user: $uid...");
try {
$result = $this->settleUser($uid);
do_log("$logPrefix, settle user: $uid done!, result: " . var_export($result, true));
if ($result) {
$successCount++;
} else {
$failCount++;
}
} catch (\Throwable $exception) {
do_log("$logPrefix, settle user: $uid fail!, error: " . $exception->getMessage() . $exception->getTraceAsString(), 'error');
$failCount++;
}
}
}
$page++;
}
return ['success_count' => $successCount, 'fail_count' => $failCount];
}
public function settleUser($uid, $force = false, $test = false, $paginate = false): bool
{
$logMsg = sprintf("uid: %s, force: %s, test: %s, paginate: %s", $uid, $force, $test, $paginate);
$hasRoleWorkSeeding = has_role_work_seeding($uid);
if ($hasRoleWorkSeeding) {
do_log("$logMsg, filter: user_has_role_work_seeding => true, skip");
return false;
}
$now = Carbon::now();
$startOfThisMonth = $now->clone()->startOfMonth();
$user = User::query()->with('language')->findOrFail($uid);
$baseQuery = Claim::query()
->where('uid', $uid)
->where("created_at", "<", $startOfThisMonth)
;
$totalRecordCount = (clone $baseQuery)->count();
$seedTimeRequiredHours = Claim::getConfigStandardSeedTimeHours();
$uploadedRequiredTimes = Claim::getConfigStandardUploadedTimes();
$bonusMultiplier = Claim::getConfigBonusMultiplier();
$bonusDeduct = Claim::getConfigRemoveDeductBonus();
$reachedTorrentIdArr = $unReachedTorrentIdArr = $remainTorrentIdArr = $unReachedIdArr = $toUpdateIdArr = [];
$totalSeedTime = 0;
$seedTimeCaseWhen = $uploadedCaseWhen = [];
$toDelClaimId = [];
do_log(
"$logMsg, claim torrent count: $totalRecordCount"
. ", seedTimeRequiredHours: $seedTimeRequiredHours"
. ", uploadedRequiredTimes: $uploadedRequiredTimes"
. ", bonusMultiplier: $bonusMultiplier"
. ", bonusDeduct: $bonusDeduct"
);
if ($paginate) {
$page = 1;
while (true) {
$list = (clone $baseQuery)->forPage($page, self::SETTLE_PAGINATE_STARTS)->get();
if ($list->isEmpty()) {
break;
}
$handleResult = $this->handleClaimSettlement(
$uid, $list, $startOfThisMonth, $seedTimeRequiredHours, $uploadedRequiredTimes,
$reachedTorrentIdArr,$unReachedTorrentIdArr,$remainTorrentIdArr, $seedTimeCaseWhen, $uploadedCaseWhen,
$toUpdateIdArr, $unReachedIdArr, $toDelClaimId, $totalSeedTime, $force
);
if ($handleResult === false) {
do_log("$logMsg, handleClaimSettlement result false, return");
return false;
}
$page++;
}
} else {
$list = $baseQuery->with(['snatch', 'torrent' => fn ($query) => $query->select(Torrent::$commentFields)])->get();
$handleResult = $this->handleClaimSettlement(
$uid, $list, $startOfThisMonth, $seedTimeRequiredHours, $uploadedRequiredTimes,
$reachedTorrentIdArr,$unReachedTorrentIdArr,$remainTorrentIdArr, $seedTimeCaseWhen, $uploadedCaseWhen,
$toUpdateIdArr, $unReachedIdArr, $toDelClaimId, $totalSeedTime, $force
);
if ($handleResult === false) {
do_log("$logMsg, handleClaimSettlement result false, return");
return false;
}
}
$bonusResult = calculate_seed_bonus($uid, $reachedTorrentIdArr);
$seedTimeHoursAvg = $totalSeedTime / (count($reachedTorrentIdArr) ?: 1) / 3600;
do_log(sprintf(
"reachedTorrentIdArr: %s, unReachedIdArr: %s, bonusResult: %s, seedTimeHours: %s",
json_encode($reachedTorrentIdArr), json_encode($unReachedIdArr), json_encode($bonusResult), $seedTimeHoursAvg
), 'alert');
$bonusFinal = $bonusResult['seed_points'] * $seedTimeHoursAvg * $bonusMultiplier;
do_log("bonus final: $bonusFinal", 'alert');
$totalDeduct = $bonusDeduct * count($unReachedIdArr);
do_log("totalDeduct: $totalDeduct", 'alert');
$message = $this->buildMessage(
$user, $reachedTorrentIdArr, $unReachedTorrentIdArr, $remainTorrentIdArr,
$bonusResult, $bonusFinal, $seedTimeHoursAvg, $bonusDeduct, $totalDeduct
);
do_log("message: " . nexus_json_encode($message), 'alert');
/**
* Just do a test, debug from log
*/
if ($test) {
do_log("[TEST], return");
return true;
}
//Wrap with transaction
DB::transaction(function () use ($uid, $unReachedIdArr, $toUpdateIdArr, $bonusFinal, $totalDeduct, $uploadedCaseWhen, $seedTimeCaseWhen, $message, $now) {
//Increase user bonus
User::query()->where('id', $uid)->increment('seedbonus', $bonusFinal);
do_log("Increase user bonus: $bonusFinal", 'alert');
//Handle unreached
if (!empty($unReachedIdArr)) {
Claim::query()->whereIn('id', $unReachedIdArr)->delete();
User::query()->where('id', $uid)->decrement('seedbonus', $totalDeduct);
do_log("Deduct user bonus: $totalDeduct", 'alert');
}
//Update claim `last_settle_at` and init `seed_time_begin` & `uploaded_begin`
if (!empty($toUpdateIdArr)) {
$sql = sprintf(
"update claims set uploaded_begin = case id %s end, seed_time_begin = case id %s end, last_settle_at = '%s', updated_at = '%s' where id in (%s)",
implode(' ', $uploadedCaseWhen), implode(' ', $seedTimeCaseWhen), $now->toDateTimeString(), $now->toDateTimeString(), implode(',', $toUpdateIdArr)
);
$affectedRows = DB::update($sql);
do_log("query: $sql, affectedRows: $affectedRows");
}
//Send message
Message::add($message);
});
if (!empty($toDelClaimId)) {
do_log("del claim: %s", json_encode($toDelClaimId));
Claim::query()->whereIn("id", array_keys($toDelClaimId))->delete();
}
do_log("[DONE], cost time: " . (time() - $now->timestamp) . " seconds");
return true;
}
private function buildMessage(
User $user, $reachedTorrentIdArr, $unReachedTorrentIdArr, $remainTorrentIdArr,
$bonusResult, $bonusFinal, $seedTimeHoursAvg, $deductPerTorrent, $deductTotal
) {
$now = Carbon::now();
$allTorrentIdArr = array_merge($reachedTorrentIdArr, $unReachedTorrentIdArr, $remainTorrentIdArr);
//这里不使用占位符,在 $allTorrentIdArr 过大超过3000时容易结果为空且不报异常
$torrentInfo = Torrent::query()
->whereRaw(sprintf("id in (%s)", implode(',', $allTorrentIdArr)))
->get(Torrent::$commentFields)
->keyBy('id')
;
$msg = [];
$locale = $user->locale;
do_log("build message, user: {$user->id}, locale: $locale");
$msg[] = nexus_trans('claim.msg_title', ['month' => $now->clone()->subMonths(1)->format('Y-m')], $locale);
$msg[] = nexus_trans('claim.claim_total', [ 'total' => count($allTorrentIdArr)], $locale);
//列表数据只取部分展示
$reachList = collect(array_slice($reachedTorrentIdArr, 0, self::SETTLE_MSG_SLICE_COUNT))->map(
fn($item) => sprintf("[url=details.php?id=%s]%s[/url]", $item, $torrentInfo->get($item)->name)
)->implode("\n");
$msg[] = nexus_trans("claim.claim_reached_counts", ['counts' => count($reachedTorrentIdArr)], $locale) . "\n$reachList";
$msg[] = nexus_trans(
"claim.claim_reached_summary", [
'bonus_per_hour' => number_format($bonusResult['seed_bonus'], 2),
'hours'=> number_format($seedTimeHoursAvg, 2),
'bonus_total'=> number_format($bonusFinal, 2)
], $locale
);
$remainList = collect(array_slice($remainTorrentIdArr, 0, self::SETTLE_MSG_SLICE_COUNT))->map(
fn($item) => sprintf("[url=details.php?id=%s]%s[/url]", $item, $torrentInfo->get($item)->name)
)->implode("\n");
$msg[] = nexus_trans("claim.claim_unreached_remain_counts", ['counts' => count($remainTorrentIdArr)], $locale) . "\n$remainList";
$unReachList = collect(array_slice($unReachedTorrentIdArr, 0, self::SETTLE_MSG_SLICE_COUNT))->map(
fn($item) => sprintf("[url=details.php?id=%s]%s[/url]", $item, $torrentInfo->get($item)->name)
)->implode("\n");
$msg[] = nexus_trans("claim.claim_unreached_remove_counts", ['counts' => count($unReachedTorrentIdArr)], $locale) . "\n$unReachList";
if ($deductTotal) {
$msg[] = nexus_trans(
"claim.claim_unreached_summary", [
'deduct_per_torrent' => number_format($deductPerTorrent, 2),
'deduct_total' => number_format($deductTotal, 2)
], $locale
);
}
return [
'receiver' => $user->id,
'added' => $now,
'subject' => nexus_trans('claim.msg_subject', ['month' => $now->clone()->subMonths(1)->format('Y-m')], $locale),
'msg' => implode("\n\n", $msg),
];
}
private function handleClaimSettlement(
int $uid, Collection $list, Carbon $startOfThisMonth, int $seedTimeRequiredHours, int $uploadedRequiredTimes,
array &$reachedTorrentIdArr, array &$unReachedTorrentIdArr, array &$remainTorrentIdArr, array &$seedTimeCaseWhen, array &$uploadedCaseWhen,
array &$toUpdateIdArr, array &$unReachedIdArr, array &$toDelIdArr, int &$totalSeedTime, bool $force
): bool|null
{
foreach ($list as $row) {
$logItem = "uid: $uid, claimId: {$row['id']}";
if ($row->last_settle_at && $row->last_settle_at->gte($startOfThisMonth)) {
do_log("$logItem already settle", 'alert');
if (!$force) {
do_log("$logItem,already settle, No force, return", 'alert');
return false;
}
}
if (!$row->snatch) {
$toDelIdArr[$row->id] = $row->id;
do_log("$logItem, No snatch, continue", 'alert');
continue;
}
if (!$row->torrent) {
$toDelIdArr[$row->id] = $row->id;
do_log("$logItem, No torrent, continue", 'alert');
continue;
}
if (
bcsub($row->snatch->seedtime, $row->seed_time_begin) >= $seedTimeRequiredHours * 3600
|| bcsub($row->snatch->uploaded, $row->uploaded_begin) >= $uploadedRequiredTimes * $row->torrent->size
) {
do_log("$logItem, [REACHED], uid: $uid, torrent: " . $row->torrent_id);
$reachedTorrentIdArr[] = $row->torrent_id;
$toUpdateIdArr[] = $row->id;
$totalSeedTime += (int)bcsub($row->snatch->seedtime, $row->seed_time_begin);
$seedTimeCaseWhen[] = sprintf('when %s then %s', $row->id, $row->snatch->seedtime);
$uploadedCaseWhen[] = sprintf('when %s then %s', $row->id, $row->snatch->uploaded);
} else {
$targetStartOfMonth = $row->created_at->startOfMonth();
if ($startOfThisMonth->diffInMonths($targetStartOfMonth, true) > 1) {
do_log("$logItem, [UNREACHED_REMOVE], uid: $uid, torrent: " . $row->torrent_id);
$unReachedIdArr[] = $row->id;
$unReachedTorrentIdArr[] = $row->torrent_id;
} else {
do_log("$logItem, [UNREACHED_FIRST_MONTH], uid: $uid, torrent: " . $row->torrent_id);
$seedTimeCaseWhen[] = sprintf('when %s then %s', $row->id, $row->snatch->seedtime);
$uploadedCaseWhen[] = sprintf('when %s then %s', $row->id, $row->snatch->uploaded);
$toUpdateIdArr[] = $row->id;
$remainTorrentIdArr[] = $row->torrent_id;
}
}
}
return null;
}
public function buildActionButtons($torrentId, $claimData, $reload = 0): string
{
$buttonHtml = '<button data-action="%s" data-reload="%s" data-confirm="%s" data-claim_id="%s" data-torrent_id="%s" style="width: max-content;display: %s;align-items: center"><img style="margin-right: 4px;" class="staff_%s" src="pic/trans.gif">%s</button>';
$addButton = sprintf($buttonHtml, 'addClaim', $reload, nexus_trans('claim.add_claim_confirm'), $claimData ? $claimData->id : 0, $torrentId, '%s', 'edit', nexus_trans('claim.add_claim'));
$removeButton = sprintf($buttonHtml, 'removeClaim', $reload, nexus_trans('claim.remove_claim_confirm'), $claimData ? $claimData->id : 0, $torrentId, '%s', 'delete', nexus_trans('claim.remove_claim'));
if ($claimData) {
//Only show remove
return sprintf($addButton, 'none') . sprintf($removeButton, 'flex');
} else {
//Only show add
return sprintf($addButton, 'flex') . sprintf($removeButton, 'none');
}
}
}