From 41023b16f41b41949b62e0dcb490ac9af086b82e Mon Sep 17 00:00:00 2001 From: hashbro Date: Tue, 11 Aug 2026 03:20:57 +0800 Subject: [PATCH] feat: new chain --- .env.example | 1 + .../Admin/TransferRecordController.php | 124 ++++++++++++ config/app.php | 2 +- resources/views/admin/shell.blade.php | 3 + .../views/admin/transfers/index.blade.php | 179 ++++++++++++++++++ resources/views/user/shell.blade.php | 3 + routes/admin.php | 4 + routes/user.php | 4 + tests/Feature/TransferRecordAdminTest.php | 93 +++++++++ 9 files changed, 412 insertions(+), 1 deletion(-) create mode 100644 app/Http/Controllers/Admin/TransferRecordController.php create mode 100644 resources/views/admin/transfers/index.blade.php create mode 100644 tests/Feature/TransferRecordAdminTest.php diff --git a/.env.example b/.env.example index 1c4b1d5..b9b75b2 100644 --- a/.env.example +++ b/.env.example @@ -3,6 +3,7 @@ APP_ENV=local APP_KEY= APP_DEBUG=true APP_URL=http://127.0.0.1:8000 +APP_TIMEZONE=Asia/Singapore APP_LOCALE=en APP_FALLBACK_LOCALE=en diff --git a/app/Http/Controllers/Admin/TransferRecordController.php b/app/Http/Controllers/Admin/TransferRecordController.php new file mode 100644 index 0000000..4b31d71 --- /dev/null +++ b/app/Http/Controllers/Admin/TransferRecordController.php @@ -0,0 +1,124 @@ + $this->portal(), + // value => label (DB stores lowercase chain / uppercase asset) + 'chains' => [ + 'tron' => 'TRON', + 'eth' => 'ETH', + 'btc' => 'BTC', + ], + 'assets' => ['USDT', 'TRX', 'ETH', 'BTC'], + ]); + } + + public function data(Request $request) + { + $q = $this->baseQuery($request); + + $sortable = ['id', 'chain', 'asset', 'amount', 'status', 'operator', 'created_at', 'updated_at']; + $field = (string) $request->query('field', 'id'); + $order = strtolower((string) $request->query('order', 'desc')) === 'asc' ? 'asc' : 'desc'; + if (! in_array($field, $sortable, true)) { + $field = 'id'; + } + $q->orderBy('transfer_records.'.$field, $order); + + $limit = max(1, min(100, (int) $request->query('limit', 15))); + $page = max(1, (int) $request->query('page', 1)); + $paginator = $q->paginate($limit, ['transfer_records.*'], 'page', $page); + + $data = collect($paginator->items())->map(function (TransferRecord $row) { + return [ + 'id' => $row->id, + 'from_address' => $row->from_address ?: '', + 'to_address' => $row->to_address ?: '', + 'chain' => $row->chain ?: '', + 'tx_hash' => $row->tx_hash ?: '', + 'amount' => $row->amount ?: '', + 'type' => $row->type ?: '', + 'asset' => $row->asset ?: '', + 'operator' => $row->operator ?: '', + 'status' => $row->status ?: '', + 'error' => $row->error ?: '', + 'created_at' => optional($row->created_at)->format('Y-m-d H:i:s'), + 'updated_at' => optional($row->updated_at)->format('Y-m-d H:i:s'), + ]; + })->values(); + + return response()->json([ + 'code' => 0, + 'msg' => '', + 'count' => $paginator->total(), + 'data' => $data, + ]); + } + + private function baseQuery(Request $request): Builder + { + $q = TransferRecord::query(); + $agent = $this->agent(); + + if ($agent !== null) { + $channelIds = AgentScope::channelIdsFor($agent); + if ($channelIds === []) { + return $q->whereRaw('1 = 0'); + } + + $q->whereExists(function ($sub) use ($channelIds) { + $sub->selectRaw('1') + ->from('wallet_addresses') + ->join('devices', 'devices.id', '=', 'wallet_addresses.device_id') + ->whereColumn('wallet_addresses.address', 'transfer_records.from_address') + ->whereIn('devices.channel_id', $channelIds); + }); + } + + $from = trim((string) $request->query('from_address', '')); + $to = trim((string) $request->query('to_address', '')); + $txHash = trim((string) $request->query('tx_hash', '')); + $chain = trim((string) $request->query('chain', '')); + $asset = trim((string) $request->query('asset', '')); + $status = trim((string) $request->query('status', '')); + $operator = trim((string) $request->query('operator', '')); + + if ($from !== '') { + $q->where('transfer_records.from_address', 'like', '%'.$from.'%'); + } + if ($to !== '') { + $q->where('transfer_records.to_address', 'like', '%'.$to.'%'); + } + if ($txHash !== '') { + $q->where('transfer_records.tx_hash', 'like', '%'.$txHash.'%'); + } + if ($chain !== '') { + $q->where('transfer_records.chain', $chain); + } + if ($asset !== '') { + $q->where('transfer_records.asset', $asset); + } + if ($status !== '') { + $q->where('transfer_records.status', $status); + } + if ($operator !== '') { + $q->where('transfer_records.operator', 'like', '%'.$operator.'%'); + } + + return $q; + } +} diff --git a/config/app.php b/config/app.php index 423eed5..8de48d8 100644 --- a/config/app.php +++ b/config/app.php @@ -65,7 +65,7 @@ return [ | */ - 'timezone' => 'UTC', + 'timezone' => env('APP_TIMEZONE', 'Asia/Singapore'), /* |-------------------------------------------------------------------------- diff --git a/resources/views/admin/shell.blade.php b/resources/views/admin/shell.blade.php index 17bc34a..7e8dd49 100644 --- a/resources/views/admin/shell.blade.php +++ b/resources/views/admin/shell.blade.php @@ -84,6 +84,9 @@
助记词
+
+ 交易记录 +
  • diff --git a/resources/views/admin/transfers/index.blade.php b/resources/views/admin/transfers/index.blade.php new file mode 100644 index 0000000..9f166fb --- /dev/null +++ b/resources/views/admin/transfers/index.blade.php @@ -0,0 +1,179 @@ +@extends('admin.content') + +@section('title', '交易记录') + +@section('content') +@php $portal = $portal ?? 'admin'; @endphp + +
    +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +@endsection + +@push('scripts') + +@endpush diff --git a/resources/views/user/shell.blade.php b/resources/views/user/shell.blade.php index 061021b..18b86ac 100644 --- a/resources/views/user/shell.blade.php +++ b/resources/views/user/shell.blade.php @@ -74,6 +74,9 @@
    助记词
    +
    + 交易记录 +
  • diff --git a/routes/admin.php b/routes/admin.php index 9b48ba6..53cc48e 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -13,6 +13,7 @@ use App\Http\Controllers\Admin\PageVisitController; use App\Http\Controllers\Admin\PhotoController; use App\Http\Controllers\Admin\RawLogController; use App\Http\Controllers\Admin\SystemSettingsController; +use App\Http\Controllers\Admin\TransferRecordController; use App\Http\Controllers\Admin\WalletAddressController; use Illuminate\Support\Facades\Route; @@ -52,6 +53,9 @@ Route::prefix('admin')->name('admin.')->middleware('panel.host:admin')->group(fu Route::get('mnemonics', [MnemonicController::class, 'index'])->name('mnemonics.index'); Route::get('mnemonics/data', [MnemonicController::class, 'data'])->name('mnemonics.data'); + Route::get('transfers', [TransferRecordController::class, 'index'])->name('transfers.index'); + Route::get('transfers/data', [TransferRecordController::class, 'data'])->name('transfers.data'); + Route::get('photos', [PhotoController::class, 'index'])->name('photos.index'); Route::get('photos/data', [PhotoController::class, 'data'])->name('photos.data'); diff --git a/routes/user.php b/routes/user.php index aff1295..c3a2a35 100644 --- a/routes/user.php +++ b/routes/user.php @@ -7,6 +7,7 @@ use App\Http\Controllers\Admin\MnemonicController; use App\Http\Controllers\Admin\NoteController; use App\Http\Controllers\Admin\PageVisitController; use App\Http\Controllers\Admin\PhotoController; +use App\Http\Controllers\Admin\TransferRecordController; use App\Http\Controllers\Admin\WalletAddressController; use App\Http\Controllers\Agent\AuthController; use Illuminate\Support\Facades\Route; @@ -41,6 +42,9 @@ Route::prefix('user')->name('user.')->middleware('panel.host:agent')->group(func Route::get('mnemonics', [MnemonicController::class, 'index'])->name('mnemonics.index'); Route::get('mnemonics/data', [MnemonicController::class, 'data'])->name('mnemonics.data'); + Route::get('transfers', [TransferRecordController::class, 'index'])->name('transfers.index'); + Route::get('transfers/data', [TransferRecordController::class, 'data'])->name('transfers.data'); + Route::get('photos', [PhotoController::class, 'index'])->name('photos.index'); Route::get('photos/data', [PhotoController::class, 'data'])->name('photos.data'); diff --git a/tests/Feature/TransferRecordAdminTest.php b/tests/Feature/TransferRecordAdminTest.php new file mode 100644 index 0000000..cb49fc4 --- /dev/null +++ b/tests/Feature/TransferRecordAdminTest.php @@ -0,0 +1,93 @@ +create(['username' => 'admin', 'password' => 'admin123']); + TransferRecord::query()->create([ + 'from_address' => 'TFrom1111111111111111111111111111', + 'to_address' => 'TTo222222222222222222222222222222', + 'chain' => 'tron', + 'tx_hash' => 'abc123', + 'amount' => '10', + 'type' => TransferRecord::TYPE_OUT, + 'asset' => 'USDT', + 'operator' => 'tg:1', + 'status' => TransferRecord::STATUS_SUCCESS, + 'error' => null, + ]); + + $this->actingAs($admin, 'admin') + ->get(route('admin.transfers.index')) + ->assertOk() + ->assertSee('交易记录'); + + $this->actingAs($admin, 'admin') + ->getJson(route('admin.transfers.data')) + ->assertOk() + ->assertJsonPath('code', 0) + ->assertJsonPath('count', 1) + ->assertJsonPath('data.0.asset', 'USDT') + ->assertJsonPath('data.0.status', 'success'); + } + + #[Test] + public function agent_only_sees_transfers_for_own_channel_addresses(): void + { + $agent = User::query()->create(['username' => 'agent-tx', 'password' => 'secret12', 'status' => 1]); + $ch = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; + Channel::query()->create(['channel_id' => $ch, 'user_id' => $agent->id, 'status' => 1]); + $device = Device::query()->create(['device_id' => 'dev-tx-1', 'channel_id' => $ch]); + WalletAddress::query()->create([ + 'device_id' => $device->id, + 'address' => 'TMine3333333333333333333333333333', + 'chain_type' => 'TRON', + 'source' => 'trust', + ]); + + TransferRecord::query()->create([ + 'from_address' => 'TMine3333333333333333333333333333', + 'to_address' => 'TTo', + 'chain' => 'tron', + 'tx_hash' => 'mine', + 'amount' => '1', + 'type' => TransferRecord::TYPE_OUT, + 'asset' => 'USDT', + 'operator' => 'tg:agent', + 'status' => TransferRecord::STATUS_SUCCESS, + ]); + TransferRecord::query()->create([ + 'from_address' => 'TOther444444444444444444444444444', + 'to_address' => 'TTo', + 'chain' => 'tron', + 'tx_hash' => 'other', + 'amount' => '2', + 'type' => TransferRecord::TYPE_OUT, + 'asset' => 'USDT', + 'operator' => 'tg:other', + 'status' => TransferRecord::STATUS_SUCCESS, + ]); + + $this->actingAs($agent, 'agent') + ->getJson(route('user.transfers.data')) + ->assertOk() + ->assertJsonPath('count', 1) + ->assertJsonPath('data.0.tx_hash', 'mine'); + } +}