232 lines
8.1 KiB
PHP
232 lines
8.1 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Admin;
|
|
|
|
use App\Http\Controllers\Concerns\PortalAware;
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\TransferRecord;
|
|
use App\Models\WalletAddress;
|
|
use App\Support\AgentScope;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class TransferRecordController extends Controller
|
|
{
|
|
use PortalAware;
|
|
|
|
public function index()
|
|
{
|
|
return view('admin.transfers.index', [
|
|
'portal' => $this->portal(),
|
|
// value => label (DB stores lowercase chain / uppercase asset)
|
|
'chains' => [
|
|
'tron' => 'TRON',
|
|
'eth' => 'ETH',
|
|
'bsc' => 'BSC',
|
|
'btc' => 'BTC',
|
|
],
|
|
'assets' => ['USDT', 'TRX', 'ETH', 'BTC', 'BNB'],
|
|
]);
|
|
}
|
|
|
|
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', 20)));
|
|
$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,
|
|
'summary' => $this->successSummary($request),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Successful totals under the same top filters. Sweep vs fee-topup are split
|
|
* by operator (`fee-topup…`); status is always success.
|
|
*
|
|
* @return array{totals: list<array{asset: string, amount: string}>, fee_totals: list<array{asset: string, amount: string}>}
|
|
*/
|
|
private function successSummary(Request $request): array
|
|
{
|
|
$base = $this->baseQuery($request, ignoreStatus: true)
|
|
->where('transfer_records.status', TransferRecord::STATUS_SUCCESS);
|
|
|
|
return [
|
|
'totals' => $this->sumByAsset(
|
|
(clone $base)->where(function (Builder $q) {
|
|
$q->whereNull('transfer_records.operator')
|
|
->orWhere('transfer_records.operator', 'not like', 'fee-topup%');
|
|
})
|
|
),
|
|
'fee_totals' => $this->sumByAsset(
|
|
(clone $base)->where('transfer_records.operator', 'like', 'fee-topup%')
|
|
),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return list<array{asset: string, amount: string}>
|
|
*/
|
|
private function sumByAsset(Builder $q): array
|
|
{
|
|
$amountExpr = DB::connection()->getDriverName() === 'sqlite'
|
|
? 'CAST(transfer_records.amount AS REAL)'
|
|
: 'CAST(transfer_records.amount AS DECIMAL(32, 18))';
|
|
|
|
$rows = $q->selectRaw('UPPER(transfer_records.asset) as asset, COALESCE(SUM('.$amountExpr.'), 0) as total')
|
|
->groupByRaw('UPPER(transfer_records.asset)')
|
|
->get();
|
|
|
|
$byAsset = [];
|
|
foreach ($rows as $row) {
|
|
$asset = strtoupper(trim((string) $row->asset));
|
|
if ($asset === '') {
|
|
continue;
|
|
}
|
|
$formatted = WalletAddress::formatAmount(strtolower($asset), $row->total);
|
|
$byAsset[$asset] = $formatted === '' ? '0' : $formatted;
|
|
}
|
|
|
|
$order = ['USDT', 'ETH', 'TRX', 'BNB', 'BTC'];
|
|
$totals = [];
|
|
foreach ($order as $asset) {
|
|
$totals[] = [
|
|
'asset' => $asset,
|
|
'amount' => $byAsset[$asset] ?? '0',
|
|
];
|
|
unset($byAsset[$asset]);
|
|
}
|
|
ksort($byAsset);
|
|
foreach ($byAsset as $asset => $amount) {
|
|
$totals[] = ['asset' => $asset, 'amount' => $amount];
|
|
}
|
|
|
|
return $totals;
|
|
}
|
|
|
|
private function baseQuery(Request $request, bool $ignoreStatus = false): 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 = $ignoreStatus ? '' : trim((string) $request->query('status', ''));
|
|
$operator = trim((string) $request->query('operator', ''));
|
|
[$dateFrom, $dateTo] = $this->dateRange($request);
|
|
|
|
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.'%');
|
|
}
|
|
if ($dateFrom !== null) {
|
|
$q->where('transfer_records.created_at', '>=', $dateFrom);
|
|
}
|
|
if ($dateTo !== null) {
|
|
$q->where('transfer_records.created_at', '<=', $dateTo);
|
|
}
|
|
|
|
return $q;
|
|
}
|
|
|
|
/**
|
|
* @return array{0: ?Carbon, 1: ?Carbon}
|
|
*/
|
|
private function dateRange(Request $request): array
|
|
{
|
|
$from = trim((string) $request->query('date_from', ''));
|
|
$to = trim((string) $request->query('date_to', ''));
|
|
$range = trim((string) $request->query('date_range', ''));
|
|
if (($from === '' || $to === '') && $range !== '') {
|
|
if (preg_match('/^(\d{4}-\d{2}-\d{2})\s*-\s*(\d{4}-\d{2}-\d{2})$/', $range, $m) === 1) {
|
|
$from = $m[1];
|
|
$to = $m[2];
|
|
}
|
|
}
|
|
|
|
$start = null;
|
|
$end = null;
|
|
try {
|
|
if ($from !== '') {
|
|
$start = Carbon::parse($from)->startOfDay();
|
|
}
|
|
if ($to !== '') {
|
|
$end = Carbon::parse($to)->endOfDay();
|
|
}
|
|
} catch (\Throwable) {
|
|
return [null, null];
|
|
}
|
|
|
|
return [$start, $end];
|
|
}
|
|
}
|