mirror of
https://github.com/lkddi/nexusphp.git
synced 2026-04-24 12:07:23 +08:00
Merge pull request #400 from specialpointcentral/php8
feat: enhance torrent state scheduling and display
This commit is contained in:
@@ -5,21 +5,18 @@ namespace App\Filament\Resources\System;
|
|||||||
use Filament\Schemas\Schema;
|
use Filament\Schemas\Schema;
|
||||||
use Filament\Forms\Components\Select;
|
use Filament\Forms\Components\Select;
|
||||||
use Filament\Forms\Components\DateTimePicker;
|
use Filament\Forms\Components\DateTimePicker;
|
||||||
|
use Filament\Forms\Components\Textarea;
|
||||||
use Filament\Tables\Columns\TextColumn;
|
use Filament\Tables\Columns\TextColumn;
|
||||||
use Filament\Actions\EditAction;
|
use Filament\Actions\EditAction;
|
||||||
|
use Filament\Actions\DeleteAction;
|
||||||
|
use Filament\Actions\DeleteBulkAction;
|
||||||
use App\Filament\Resources\System\TorrentStateResource\Pages\ManageTorrentStates;
|
use App\Filament\Resources\System\TorrentStateResource\Pages\ManageTorrentStates;
|
||||||
use App\Filament\Resources\System\TorrentStateResource\Pages;
|
|
||||||
use App\Filament\Resources\System\TorrentStateResource\RelationManagers;
|
|
||||||
use App\Models\Setting;
|
|
||||||
use App\Models\Torrent;
|
use App\Models\Torrent;
|
||||||
use App\Models\TorrentState;
|
use App\Models\TorrentState;
|
||||||
use Filament\Forms;
|
use Carbon\Carbon;
|
||||||
use Filament\Resources\Resource;
|
use Filament\Resources\Resource;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
use Filament\Tables;
|
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
|
||||||
use Nexus\Database\NexusDB;
|
|
||||||
|
|
||||||
class TorrentStateResource extends Resource
|
class TorrentStateResource extends Resource
|
||||||
{
|
{
|
||||||
@@ -46,13 +43,28 @@ class TorrentStateResource extends Resource
|
|||||||
return $schema
|
return $schema
|
||||||
->components([
|
->components([
|
||||||
Select::make('global_sp_state')
|
Select::make('global_sp_state')
|
||||||
->options(Torrent::listPromotionTypes(true))
|
->options(function () {
|
||||||
|
$options = Torrent::listPromotionTypes(true);
|
||||||
|
unset($options[Torrent::PROMOTION_NORMAL]);
|
||||||
|
return $options;
|
||||||
|
})
|
||||||
->label(__('label.torrent_state.global_sp_state'))
|
->label(__('label.torrent_state.global_sp_state'))
|
||||||
->required(),
|
->required(),
|
||||||
DateTimePicker::make('begin')
|
DateTimePicker::make('begin')
|
||||||
->label(__('label.begin')),
|
->label(__('label.begin'))
|
||||||
|
->required(),
|
||||||
DateTimePicker::make('deadline')
|
DateTimePicker::make('deadline')
|
||||||
->label(__('label.deadline')),
|
->label(__('label.deadline'))
|
||||||
|
->required()
|
||||||
|
->after('begin')
|
||||||
|
->validationMessages([
|
||||||
|
'after' => __('label.torrent_state.deadline_after_begin'),
|
||||||
|
]),
|
||||||
|
Textarea::make('remark')
|
||||||
|
->label(__('label.comment'))
|
||||||
|
->rows(2)
|
||||||
|
->columnSpanFull()
|
||||||
|
->maxLength(255),
|
||||||
])->columns(1);
|
])->columns(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,21 +75,63 @@ class TorrentStateResource extends Resource
|
|||||||
TextColumn::make('global_sp_state_text')->label(__('label.torrent_state.global_sp_state')),
|
TextColumn::make('global_sp_state_text')->label(__('label.torrent_state.global_sp_state')),
|
||||||
TextColumn::make('begin')->label(__('label.begin')),
|
TextColumn::make('begin')->label(__('label.begin')),
|
||||||
TextColumn::make('deadline')->label(__('label.deadline')),
|
TextColumn::make('deadline')->label(__('label.deadline')),
|
||||||
|
TextColumn::make('promotion_status')
|
||||||
|
->label(__('label.torrent_state.status'))
|
||||||
|
->state(function (TorrentState $record) {
|
||||||
|
$now = Carbon::now();
|
||||||
|
$begin = $record->begin ? Carbon::parse($record->begin) : null;
|
||||||
|
$deadline = $record->deadline ? Carbon::parse($record->deadline) : null;
|
||||||
|
|
||||||
|
if ($deadline && $deadline->lt($now)) {
|
||||||
|
return 'expired';
|
||||||
|
}
|
||||||
|
if ($begin && $begin->gt($now)) {
|
||||||
|
return 'upcoming';
|
||||||
|
}
|
||||||
|
return 'ongoing';
|
||||||
|
})
|
||||||
|
->formatStateUsing(function (string $state) {
|
||||||
|
return match ($state) {
|
||||||
|
'expired' => __('label.torrent_state.status_expired'),
|
||||||
|
'upcoming' => __('label.torrent_state.status_upcoming'),
|
||||||
|
default => __('label.torrent_state.status_ongoing'),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
->badge()
|
||||||
|
->sortable(query: function (Builder $query, string $direction): Builder {
|
||||||
|
$now = Carbon::now()->toDateTimeString();
|
||||||
|
// expired=0, ongoing=1, upcoming=2
|
||||||
|
return $query->orderByRaw(
|
||||||
|
"CASE
|
||||||
|
WHEN deadline IS NOT NULL AND deadline < ? THEN 0
|
||||||
|
WHEN begin IS NOT NULL AND begin > ? THEN 2
|
||||||
|
ELSE 1
|
||||||
|
END {$direction}",
|
||||||
|
[$now, $now]
|
||||||
|
);
|
||||||
|
})
|
||||||
|
->color(fn (string $state) => match ($state) {
|
||||||
|
'expired' => 'danger',
|
||||||
|
'upcoming' => 'info',
|
||||||
|
default => 'success',
|
||||||
|
})
|
||||||
|
->icon(fn (string $state) => match ($state) {
|
||||||
|
'expired' => 'heroicon-o-x-circle',
|
||||||
|
'upcoming' => 'heroicon-o-clock',
|
||||||
|
default => 'heroicon-o-check-circle',
|
||||||
|
})
|
||||||
|
->iconPosition('before'),
|
||||||
|
TextColumn::make('remark')->label(__('label.comment'))->limit(50),
|
||||||
])
|
])
|
||||||
->filters([
|
->filters([
|
||||||
//
|
//
|
||||||
])
|
])
|
||||||
->recordActions([
|
->recordActions([
|
||||||
EditAction::make()->after(function () {
|
EditAction::make(),
|
||||||
do_log("cache_del: global_promotion_state");
|
DeleteAction::make(),
|
||||||
NexusDB::cache_del(Setting::TORRENT_GLOBAL_STATE_CACHE_KEY);
|
|
||||||
do_log("publish_model_event: global_promotion_state_updated");
|
|
||||||
publish_model_event("global_promotion_state_updated", 0);
|
|
||||||
}),
|
|
||||||
// Tables\Actions\DeleteAction::make(),
|
|
||||||
])
|
])
|
||||||
->toolbarActions([
|
->toolbarActions([
|
||||||
// Tables\Actions\DeleteBulkAction::make(),
|
DeleteBulkAction::make(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,8 @@
|
|||||||
namespace App\Filament\Resources\System\TorrentStateResource\Pages;
|
namespace App\Filament\Resources\System\TorrentStateResource\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\System\TorrentStateResource;
|
use App\Filament\Resources\System\TorrentStateResource;
|
||||||
use Filament\Pages\Actions;
|
use Filament\Actions\CreateAction;
|
||||||
use Filament\Resources\Pages\ManageRecords;
|
use Filament\Resources\Pages\ManageRecords;
|
||||||
use Nexus\Database\NexusDB;
|
|
||||||
|
|
||||||
class ManageTorrentStates extends ManageRecords
|
class ManageTorrentStates extends ManageRecords
|
||||||
{
|
{
|
||||||
@@ -14,7 +13,7 @@ class ManageTorrentStates extends ManageRecords
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
// Actions\CreateAction::make(),
|
CreateAction::make(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+266
-1
@@ -4,17 +4,282 @@ namespace App\Models;
|
|||||||
|
|
||||||
|
|
||||||
use App\Models\Traits\NexusActivityLogTrait;
|
use App\Models\Traits\NexusActivityLogTrait;
|
||||||
|
use App\Models\Setting;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use Illuminate\Support\Facades\Request;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Nexus\Database\NexusDB;
|
||||||
|
|
||||||
class TorrentState extends NexusModel
|
class TorrentState extends NexusModel
|
||||||
{
|
{
|
||||||
use NexusActivityLogTrait;
|
use NexusActivityLogTrait;
|
||||||
|
|
||||||
protected $fillable = ['global_sp_state', 'deadline', 'begin'];
|
protected $fillable = ['global_sp_state', 'deadline', 'begin', 'remark'];
|
||||||
|
|
||||||
protected $table = 'torrents_state';
|
protected $table = 'torrents_state';
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'begin' => 'datetime',
|
||||||
|
'deadline' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected static function booted()
|
||||||
|
{
|
||||||
|
parent::booted();
|
||||||
|
|
||||||
|
static::saving(function (TorrentState $state) {
|
||||||
|
$state->validateTimeRange();
|
||||||
|
$state->ensureNoOverlap();
|
||||||
|
});
|
||||||
|
|
||||||
|
static::saved(function () {
|
||||||
|
static::flushCache();
|
||||||
|
});
|
||||||
|
|
||||||
|
static::deleted(function () {
|
||||||
|
static::flushCache();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public function getGlobalSpStateTextAttribute()
|
public function getGlobalSpStateTextAttribute()
|
||||||
{
|
{
|
||||||
return Torrent::$promotionTypes[$this->global_sp_state]['text'] ?? '';
|
return Torrent::$promotionTypes[$this->global_sp_state]['text'] ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function scopeActive(Builder $query, ?Carbon $moment = null): Builder
|
||||||
|
{
|
||||||
|
$moment = $moment ?? Carbon::now();
|
||||||
|
|
||||||
|
return $query
|
||||||
|
->where('global_sp_state', '!=', Torrent::PROMOTION_NORMAL)
|
||||||
|
->where(function (Builder $query) use ($moment) {
|
||||||
|
$query->whereNull('begin')->orWhere('begin', '<=', $moment);
|
||||||
|
})
|
||||||
|
->where(function (Builder $query) use ($moment) {
|
||||||
|
$query->whereNull('deadline')->orWhere('deadline', '>=', $moment);
|
||||||
|
})
|
||||||
|
->orderBy('begin')
|
||||||
|
->orderBy('id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scopeUpcoming(Builder $query, ?Carbon $moment = null): Builder
|
||||||
|
{
|
||||||
|
$moment = $moment ?? Carbon::now();
|
||||||
|
|
||||||
|
return $query
|
||||||
|
->where('global_sp_state', '!=', Torrent::PROMOTION_NORMAL)
|
||||||
|
->whereNotNull('begin')
|
||||||
|
->where('begin', '>', $moment)
|
||||||
|
->orderBy('begin')
|
||||||
|
->orderBy('id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function current(?Carbon $moment = null): ?self
|
||||||
|
{
|
||||||
|
return self::query()->active($moment)->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function next(?Carbon $moment = null): ?self
|
||||||
|
{
|
||||||
|
return self::query()->upcoming($moment)->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function cachedStates(): array
|
||||||
|
{
|
||||||
|
return NexusDB::remember(Setting::TORRENT_GLOBAL_STATE_CACHE_KEY, 600, function () {
|
||||||
|
return self::query()
|
||||||
|
->where('global_sp_state', '!=', Torrent::PROMOTION_NORMAL)
|
||||||
|
->orderByRaw('begin is null')
|
||||||
|
->orderBy('begin')
|
||||||
|
->orderBy('id')
|
||||||
|
->get()
|
||||||
|
->toArray();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function flushCache(): void
|
||||||
|
{
|
||||||
|
do_log("cache_del: " . Setting::TORRENT_GLOBAL_STATE_CACHE_KEY);
|
||||||
|
NexusDB::cache_del(Setting::TORRENT_GLOBAL_STATE_CACHE_KEY);
|
||||||
|
do_log("publish_model_event: global_promotion_state_updated");
|
||||||
|
publish_model_event("global_promotion_state_updated", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function resolveTimeline(?Carbon $moment = null): array
|
||||||
|
{
|
||||||
|
$moment = $moment ?? Carbon::now();
|
||||||
|
$states = self::cachedStates();
|
||||||
|
$current = null;
|
||||||
|
$upcoming = null;
|
||||||
|
|
||||||
|
foreach ($states as $state) {
|
||||||
|
$begin = self::parseDateTimeValue($state['begin'] ?? null);
|
||||||
|
$deadline = self::parseDateTimeValue($state['deadline'] ?? null);
|
||||||
|
|
||||||
|
$hasBegun = !$begin || $begin->lessThanOrEqualTo($moment);
|
||||||
|
$notExpired = !$deadline || $deadline->greaterThanOrEqualTo($moment);
|
||||||
|
|
||||||
|
if ($hasBegun && $notExpired) {
|
||||||
|
if (!$current) {
|
||||||
|
$current = $state;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($begin && $begin->greaterThan($moment)) {
|
||||||
|
if (!$upcoming) {
|
||||||
|
$upcoming = $state;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$upcomingBegin = self::parseDateTimeValue($upcoming['begin'] ?? null);
|
||||||
|
if ($upcomingBegin && $begin->lessThan($upcomingBegin)) {
|
||||||
|
$upcoming = $state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'current' => $current,
|
||||||
|
'upcoming' => $upcoming,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function validateTimeRange(): void
|
||||||
|
{
|
||||||
|
$begin = self::parseDateTimeValue($this->begin);
|
||||||
|
$deadline = self::parseDateTimeValue($this->deadline);
|
||||||
|
|
||||||
|
if ($begin && $deadline && $deadline->lessThanOrEqualTo($begin)) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
self::errorFieldKey('deadline') => __('label.torrent_state.deadline_after_begin'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function ensureNoOverlap(): void
|
||||||
|
{
|
||||||
|
self::validateNoOverlap($this->attributesToArray(), $this->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getRangeForComparison(TorrentState $state): array
|
||||||
|
{
|
||||||
|
$min = Carbon::createFromTimestamp(0);
|
||||||
|
$max = Carbon::create(9999, 12, 31, 23, 59, 59);
|
||||||
|
|
||||||
|
$begin = self::parseDateTimeValue($state->begin) ?? $min;
|
||||||
|
|
||||||
|
$deadline = self::parseDateTimeValue($state->deadline) ?? $max;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'begin' => $begin,
|
||||||
|
'end' => $deadline,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function parseDateTimeValue(mixed $value): ?Carbon
|
||||||
|
{
|
||||||
|
if ($value instanceof Carbon) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($value) || $value === '0000-00-00 00:00:00') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Carbon::parse($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function validateNoOverlap(array $attributes, ?int $ignoreId = null): void
|
||||||
|
{
|
||||||
|
$globalState = (int) Arr::get($attributes, 'global_sp_state', Torrent::PROMOTION_NORMAL);
|
||||||
|
if ($globalState === Torrent::PROMOTION_NORMAL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$range = self::getRangeForArray($attributes);
|
||||||
|
|
||||||
|
$conflicts = self::query()
|
||||||
|
->where('global_sp_state', '!=', Torrent::PROMOTION_NORMAL)
|
||||||
|
->when($ignoreId, fn (Builder $query) => $query->whereKeyNot($ignoreId))
|
||||||
|
->get(['id', 'begin', 'deadline']);
|
||||||
|
|
||||||
|
$beginConflict = $conflicts->first(function (TorrentState $state) use ($range) {
|
||||||
|
$other = $state->getRangeForComparison($state);
|
||||||
|
return $range['begin']->greaterThanOrEqualTo($other['begin']) && $range['begin']->lessThanOrEqualTo($other['end']);
|
||||||
|
});
|
||||||
|
|
||||||
|
$endConflict = $conflicts->first(function (TorrentState $state) use ($range) {
|
||||||
|
$other = $state->getRangeForComparison($state);
|
||||||
|
return $range['end']->greaterThanOrEqualTo($other['begin']) && $range['end']->lessThanOrEqualTo($other['end']);
|
||||||
|
});
|
||||||
|
|
||||||
|
$coverageConflict = $conflicts->first(function (TorrentState $state) use ($range) {
|
||||||
|
$other = $state->getRangeForComparison($state);
|
||||||
|
return $range['begin']->lt($other['begin']) && $range['end']->gt($other['end']);
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($beginConflict || $endConflict || $coverageConflict) {
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
if ($beginConflict) {
|
||||||
|
$errors[self::errorFieldKey('begin')] = self::buildOverlapMessage($beginConflict);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($endConflict) {
|
||||||
|
$errors[self::errorFieldKey('deadline')] = self::buildOverlapMessage($endConflict);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($errors) && $coverageConflict) {
|
||||||
|
$msg = self::buildOverlapMessage($coverageConflict);
|
||||||
|
$errors[self::errorFieldKey('begin')] = $msg;
|
||||||
|
$errors[self::errorFieldKey('deadline')] = $msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($errors)) {
|
||||||
|
$msg = __('label.torrent_state.time_overlaps');
|
||||||
|
$errors[self::errorFieldKey('begin')] = $msg;
|
||||||
|
$errors[self::errorFieldKey('deadline')] = $msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw ValidationException::withMessages($errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function getRangeForArray(array $attributes): array
|
||||||
|
{
|
||||||
|
$min = Carbon::createFromTimestamp(0);
|
||||||
|
$max = Carbon::create(9999, 12, 31, 23, 59, 59);
|
||||||
|
|
||||||
|
$begin = self::parseDateTimeValue($attributes['begin'] ?? null) ?? $min;
|
||||||
|
$deadline = self::parseDateTimeValue($attributes['deadline'] ?? null) ?? $max;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'begin' => $begin,
|
||||||
|
'end' => $deadline,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function errorFieldKey(string $field): string
|
||||||
|
{
|
||||||
|
$prefix = 'mountedActions.0.data.';
|
||||||
|
|
||||||
|
return $prefix . $field;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function buildOverlapMessage(TorrentState $conflict): string
|
||||||
|
{
|
||||||
|
$begin = self::parseDateTimeValue($conflict->begin);
|
||||||
|
$deadline = self::parseDateTimeValue($conflict->deadline);
|
||||||
|
|
||||||
|
$beginText = $begin ? $begin->toDateTimeString() : '-∞';
|
||||||
|
$deadlineText = $deadline ? $deadline->toDateTimeString() : '∞';
|
||||||
|
|
||||||
|
return __('label.torrent_state.time_overlaps_with', [
|
||||||
|
'id' => $conflict->id,
|
||||||
|
'begin' => $beginText,
|
||||||
|
'end' => $deadlineText,
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class TorrentStatePolicy extends BasePolicy
|
|||||||
*/
|
*/
|
||||||
public function create(User $user)
|
public function create(User $user)
|
||||||
{
|
{
|
||||||
return false;
|
return $this->can($user);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -66,7 +66,7 @@ class TorrentStatePolicy extends BasePolicy
|
|||||||
*/
|
*/
|
||||||
public function delete(User $user, TorrentState $torrentState)
|
public function delete(User $user, TorrentState $torrentState)
|
||||||
{
|
{
|
||||||
|
return $this->can($user);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -78,7 +78,7 @@ class TorrentStatePolicy extends BasePolicy
|
|||||||
*/
|
*/
|
||||||
public function restore(User $user, TorrentState $torrentState)
|
public function restore(User $user, TorrentState $torrentState)
|
||||||
{
|
{
|
||||||
|
return $this->can($user);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -90,7 +90,7 @@ class TorrentStatePolicy extends BasePolicy
|
|||||||
*/
|
*/
|
||||||
public function forceDelete(User $user, TorrentState $torrentState)
|
public function forceDelete(User $user, TorrentState $torrentState)
|
||||||
{
|
{
|
||||||
//
|
return $this->can($user);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function can(User $user)
|
private function can(User $user)
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?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('torrents_state', function (Blueprint $table) {
|
||||||
|
if (!Schema::hasColumn('torrents_state', 'remark')) {
|
||||||
|
$table->string('remark')->nullable()->after('deadline');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('torrents_state', function (Blueprint $table) {
|
||||||
|
if (Schema::hasColumn('torrents_state', 'remark')) {
|
||||||
|
$table->dropColumn('remark');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
+27
-8
@@ -2823,15 +2823,34 @@ print '<br/>';
|
|||||||
}
|
}
|
||||||
if ($msgalert)
|
if ($msgalert)
|
||||||
{
|
{
|
||||||
$spStateGlobal = get_global_sp_state();
|
$timeline = \App\Models\TorrentState::resolveTimeline();
|
||||||
if ($spStateGlobal != \App\Models\Torrent::PROMOTION_NORMAL) {
|
$currentPromotion = $timeline['current'] ?? null;
|
||||||
$torrentGlobalStateRow = \Nexus\Database\NexusDB::cache_get(\App\Models\Setting::TORRENT_GLOBAL_STATE_CACHE_KEY);
|
$upcomingPromotion = $timeline['upcoming'] ?? null;
|
||||||
$msg = sprintf($lang_functions['full_site_promotion_in_effect'], \App\Models\Torrent::$promotionTypes[$spStateGlobal]['text']);
|
$remarkTpl = $lang_functions['full_site_promotion_remark'] ?? 'Remark: %s';
|
||||||
if (!empty($torrentGlobalStateRow['begin']) || !empty($torrentGlobalStateRow['deadline'])) {
|
|
||||||
$timeRange = sprintf($lang_functions['full_site_promotion_time_range'], $torrentGlobalStateRow['begin'] ?? '-∞', $torrentGlobalStateRow['deadline'] ?? '∞');
|
if ($currentPromotion) {
|
||||||
$msg .= $timeRange;
|
$promotionText = \App\Models\Torrent::$promotionTypes[$currentPromotion['global_sp_state']]['text'] ?? '';
|
||||||
|
$msg = sprintf($lang_functions['full_site_promotion_in_effect'], $promotionText);
|
||||||
|
if (!empty($currentPromotion['begin']) || !empty($currentPromotion['deadline'])) {
|
||||||
|
$timeRange = sprintf($lang_functions['full_site_promotion_time_range'], $currentPromotion['begin'] ?? '-∞', $currentPromotion['deadline'] ?? '∞');
|
||||||
|
$msg .= '<br/>' . $timeRange;
|
||||||
|
}
|
||||||
|
if (!empty($currentPromotion['remark'])) {
|
||||||
|
$msg .= '<br/>' . sprintf($remarkTpl, $currentPromotion['remark']);
|
||||||
}
|
}
|
||||||
msgalert("torrents.php", $msg, "green");
|
msgalert("torrents.php", $msg, "green");
|
||||||
|
}
|
||||||
|
if ($upcomingPromotion) {
|
||||||
|
$promotionText = \App\Models\Torrent::$promotionTypes[$upcomingPromotion['global_sp_state']]['text'] ?? '';
|
||||||
|
$msg = sprintf($lang_functions['full_site_promotion_upcoming'] ?? 'Upcoming full site [%s]', $promotionText);
|
||||||
|
if (!empty($upcomingPromotion['begin']) || !empty($upcomingPromotion['deadline'])) {
|
||||||
|
$timeRange = sprintf($lang_functions['full_site_promotion_time_range'], $upcomingPromotion['begin'] ?? '-∞', $upcomingPromotion['deadline'] ?? '∞');
|
||||||
|
$msg .= '<br/>' . $timeRange;
|
||||||
|
}
|
||||||
|
if (!empty($upcomingPromotion['remark'])) {
|
||||||
|
$msg .= '<br/>' . sprintf($remarkTpl, $upcomingPromotion['remark']);
|
||||||
|
}
|
||||||
|
msgalert("torrents.php", $msg, "blue");
|
||||||
}
|
}
|
||||||
if($CURUSER['leechwarn'] == 'yes')
|
if($CURUSER['leechwarn'] == 'yes')
|
||||||
{
|
{
|
||||||
@@ -5994,7 +6013,7 @@ function get_ip_location_from_geoip($ip): bool|array
|
|||||||
|
|
||||||
function msgalert($url, $text, $bgcolor = "red")
|
function msgalert($url, $text, $bgcolor = "red")
|
||||||
{
|
{
|
||||||
print("<table border=\"0\" cellspacing=\"0\" cellpadding=\"10\"><tr><td style='border: none; padding: 10px; background: ".$bgcolor."'>\n");
|
print("<table border=\"0\" cellspacing=\"0\" cellpadding=\"10\" style=\"margin: 0 auto;\"><tr><td style='border: none; padding: 10px; background: ".$bgcolor."; text-align: center;'>\n");
|
||||||
if (!empty($url)) {
|
if (!empty($url)) {
|
||||||
print("<b><a href=\"".$url."\" target='_blank'><font color=\"white\">".$text."</font></a></b>");
|
print("<b><a href=\"".$url."\" target='_blank'><font color=\"white\">".$text."</font></a></b>");
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -3,21 +3,14 @@
|
|||||||
function get_global_sp_state()
|
function get_global_sp_state()
|
||||||
{
|
{
|
||||||
static $global_promotion_state;
|
static $global_promotion_state;
|
||||||
$cacheKey = \App\Models\Setting::TORRENT_GLOBAL_STATE_CACHE_KEY;
|
|
||||||
if (is_null($global_promotion_state)) {
|
if (is_null($global_promotion_state)) {
|
||||||
$row = \Nexus\Database\NexusDB::remember($cacheKey, 600, function () use ($cacheKey) {
|
$timeline = \App\Models\TorrentState::resolveTimeline();
|
||||||
return \Nexus\Database\NexusDB::getOne('torrents_state', 1);
|
$current = $timeline['current'] ?? null;
|
||||||
});
|
|
||||||
if (is_array($row) && isset($row['deadline']) && $row['deadline'] < date('Y-m-d H:i:s')) {
|
if (is_array($current) && isset($current['global_sp_state'])) {
|
||||||
//expired
|
$global_promotion_state = $current['global_sp_state'];
|
||||||
$global_promotion_state = \App\Models\Torrent::PROMOTION_NORMAL;
|
|
||||||
} elseif (is_array($row) && isset($row['begin']) && $row['begin'] > date('Y-m-d H:i:s')) {
|
|
||||||
//Not begin
|
|
||||||
$global_promotion_state = \App\Models\Torrent::PROMOTION_NORMAL;
|
|
||||||
} elseif (is_array($row)) {
|
|
||||||
$global_promotion_state = $row["global_sp_state"];
|
|
||||||
} else {
|
} else {
|
||||||
$global_promotion_state = $row;
|
$global_promotion_state = \App\Models\Torrent::PROMOTION_NORMAL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $global_promotion_state;
|
return $global_promotion_state;
|
||||||
|
|||||||
@@ -327,6 +327,8 @@ $lang_functions = array
|
|||||||
'text_contactstaff' => '联系管理组',
|
'text_contactstaff' => '联系管理组',
|
||||||
'full_site_promotion_in_effect' => '全站 [%s] 生效中!',
|
'full_site_promotion_in_effect' => '全站 [%s] 生效中!',
|
||||||
'full_site_promotion_time_range' => '时间:%s ~ %s',
|
'full_site_promotion_time_range' => '时间:%s ~ %s',
|
||||||
|
'full_site_promotion_remark' => '备注:%s',
|
||||||
|
'full_site_promotion_upcoming' => '即将生效的全站 [%s]',
|
||||||
'text_torrent_to_approval' => '有 %s%u 个待审核的种子%s',
|
'text_torrent_to_approval' => '有 %s%u 个待审核的种子%s',
|
||||||
'std_confirm_remove' => '确定要删除吗?',
|
'std_confirm_remove' => '确定要删除吗?',
|
||||||
'select_an_user_class' => '选择一个用户等级',
|
'select_an_user_class' => '选择一个用户等级',
|
||||||
|
|||||||
@@ -334,6 +334,8 @@ $lang_functions = array
|
|||||||
'text_contactstaff' => '聯系管理組',
|
'text_contactstaff' => '聯系管理組',
|
||||||
'full_site_promotion_in_effect' => '全站 [%s] 生效中!',
|
'full_site_promotion_in_effect' => '全站 [%s] 生效中!',
|
||||||
'full_site_promotion_time_range' => '時間:%s ~ %s',
|
'full_site_promotion_time_range' => '時間:%s ~ %s',
|
||||||
|
'full_site_promotion_remark' => '備註:%s',
|
||||||
|
'full_site_promotion_upcoming' => '即將生效的全站 [%s]',
|
||||||
'text_torrent_to_approval' => '有 %s%u 個待審核的種子%s',
|
'text_torrent_to_approval' => '有 %s%u 個待審核的種子%s',
|
||||||
'std_confirm_remove' => '確定要刪除嗎?',
|
'std_confirm_remove' => '確定要刪除嗎?',
|
||||||
'select_an_user_class' => '選擇一個用戶等級',
|
'select_an_user_class' => '選擇一個用戶等級',
|
||||||
|
|||||||
@@ -335,6 +335,8 @@ $lang_functions = array
|
|||||||
'text_contactstaff' => 'Contact staff',
|
'text_contactstaff' => 'Contact staff',
|
||||||
'full_site_promotion_in_effect' => 'Full site [%s] in effect!',
|
'full_site_promotion_in_effect' => 'Full site [%s] in effect!',
|
||||||
'full_site_promotion_time_range' => 'Time range: %s ~ %s',
|
'full_site_promotion_time_range' => 'Time range: %s ~ %s',
|
||||||
|
'full_site_promotion_remark' => 'Note: %s',
|
||||||
|
'full_site_promotion_upcoming' => 'Upcoming full site [%s]',
|
||||||
'text_torrent_to_approval' => 'There %s%u not approval torrent%s.',
|
'text_torrent_to_approval' => 'There %s%u not approval torrent%s.',
|
||||||
'std_confirm_remove' => 'Are you sure you want to delete it?',
|
'std_confirm_remove' => 'Are you sure you want to delete it?',
|
||||||
'select_an_user_class' => 'Select an user class',
|
'select_an_user_class' => 'Select an user class',
|
||||||
|
|||||||
@@ -328,6 +328,13 @@ return [
|
|||||||
'torrent_state' => [
|
'torrent_state' => [
|
||||||
'label' => 'Global promotion',
|
'label' => 'Global promotion',
|
||||||
'global_sp_state' => 'Global promotion state',
|
'global_sp_state' => 'Global promotion state',
|
||||||
|
'deadline_after_begin' => 'End time must be later than start time.',
|
||||||
|
'status' => 'Status',
|
||||||
|
'status_expired' => 'Expired',
|
||||||
|
'status_ongoing' => 'In progress',
|
||||||
|
'status_upcoming' => 'Upcoming',
|
||||||
|
'time_overlaps' => 'Time overlaps with another promotion. Please adjust the window.',
|
||||||
|
'time_overlaps_with' => 'Overlaps with promotion ID :id (time: :begin ~ :end).',
|
||||||
],
|
],
|
||||||
'role' => [
|
'role' => [
|
||||||
'class' => 'Relate user class',
|
'class' => 'Relate user class',
|
||||||
|
|||||||
@@ -370,6 +370,13 @@ return [
|
|||||||
'torrent_state' => [
|
'torrent_state' => [
|
||||||
'label' => '全站优惠',
|
'label' => '全站优惠',
|
||||||
'global_sp_state' => '全站优惠',
|
'global_sp_state' => '全站优惠',
|
||||||
|
'deadline_after_begin' => '结束时间必须晚于开始时间。',
|
||||||
|
'status' => '状态',
|
||||||
|
'status_expired' => '已过期',
|
||||||
|
'status_ongoing' => '进行中',
|
||||||
|
'status_upcoming' => '未开始',
|
||||||
|
'time_overlaps' => '时间与已有活动重叠,请调整时间段。',
|
||||||
|
'time_overlaps_with' => '与活动 ID :id (时间::begin ~ :end)重叠,请调整时间段。',
|
||||||
],
|
],
|
||||||
'role' => [
|
'role' => [
|
||||||
'class' => '关联用户等级',
|
'class' => '关联用户等级',
|
||||||
|
|||||||
@@ -327,6 +327,13 @@ return [
|
|||||||
'torrent_state' => [
|
'torrent_state' => [
|
||||||
'label' => '全站優惠',
|
'label' => '全站優惠',
|
||||||
'global_sp_state' => '全站優惠',
|
'global_sp_state' => '全站優惠',
|
||||||
|
'deadline_after_begin' => '結束時間必須晚於開始時間。',
|
||||||
|
'status' => '狀態',
|
||||||
|
'status_expired' => '已過期',
|
||||||
|
'status_ongoing' => '進行中',
|
||||||
|
'status_upcoming' => '未開始',
|
||||||
|
'time_overlaps' => '時間與已有活動重疊,請調整時間段。',
|
||||||
|
'time_overlaps_with' => '與活動 ID :id (時間::begin ~ :end)重疊,請調整時間段。',
|
||||||
],
|
],
|
||||||
'role' => [
|
'role' => [
|
||||||
'class' => '關聯用户等級',
|
'class' => '關聯用户等級',
|
||||||
|
|||||||
Reference in New Issue
Block a user