>,total:int,current_page:int,last_page:int,per_page:int,summary:array{income:float,expense:float,balance:float,count:int},sync:array} */ public function paginate(array $filters, string $sort, string $direction, int $page, int $perPage): array { $dataset = $this->datasets->get(self::DATASET); if (! $this->mirror->ready($dataset)) { throw new RuntimeException('store_receiving_alipay_bill_mirror_not_ready'); } $page = max($page, 1); $perPage = min(max($perPage, 1), 100); $direction = $direction === 'asc' ? 'asc' : 'desc'; $sortColumn = [ 'record_code' => 'Id', 'occurred_at' => 'happenTime', 'income' => 'inAmount', 'expense' => 'outAmount', 'balance' => 'accountAmount', ][$sort] ?? 'happenTime'; $query = $this->mirror->connection()->table($dataset->targetTable()); $this->applyFilters($query, $filters); $total = (clone $query)->count(); $summaryRow = (clone $query)->selectRaw( 'COALESCE(SUM(CASE WHEN inAmount > 0 THEN inAmount ELSE 0 END), 0) AS income, ' . 'COALESCE(SUM(ABS(COALESCE(outAmount, 0))), 0) AS expense, COUNT(*) AS aggregate_count' )->first(); $latestBalance = (clone $query) ->orderByDesc('happenTime') ->orderByDesc('Id') ->value('accountAmount'); $rows = $query ->orderBy($sortColumn, $direction) ->orderBy('Id', $direction) ->forPage($page, $perPage) ->get() ->map(fn (object $row): array => $this->mapRow($row)) ->all(); $state = $this->mirror->state($dataset) ?? []; return [ 'data' => $rows, 'total' => $total, 'current_page' => $page, 'last_page' => max((int) ceil($total / $perPage), 1), 'per_page' => $perPage, 'summary' => [ 'income' => (float) ($summaryRow->income ?? 0), 'expense' => (float) ($summaryRow->expense ?? 0), 'balance' => (float) ($latestBalance ?? 0), 'count' => (int) ($summaryRow->aggregate_count ?? 0), ], 'sync' => [ 'status' => $state['status'] ?? 'unknown', 'last_succeeded_at' => $state['last_succeeded_at'] ?? null, ], ]; } private function applyFilters(Builder $query, array $filters): void { if (($filters['store_name'] ?? '') !== '') { $query->where('shopName', $filters['store_name']); } if (($filters['alipay_account'] ?? '') !== '') { $value = '%' . $this->escapeLike((string) $filters['alipay_account']) . '%'; $query->where(function (Builder $query) use ($value): void { $query->where('appId', 'like', $value) ->orWhere('account', 'like', $value); }); } if (($filters['bill_item'] ?? '') !== '') { $query->where('tradeChannel', $filters['bill_item']); } if (($filters['business_type'] ?? '') !== '') { $query->where('businessType', $filters['business_type']); } if (($filters['start'] ?? '') !== '') { $query->where('happenTime', '>=', $filters['start'] . ' 00:00:00'); } if (($filters['end'] ?? '') !== '') { $query->where('happenTime', '<', date('Y-m-d', strtotime($filters['end'] . ' +1 day')) . ' 00:00:00'); } if (($filters['keyword'] ?? '') !== '') { $value = '%' . $this->escapeLike((string) $filters['keyword']) . '%'; $query->where(function (Builder $query) use ($value): void { $query->where('commodityName', 'like', $value) ->orWhere('accountNum', 'like', $value) ->orWhere('businessNum', 'like', $value) ->orWhere('merchantOrder', 'like', $value) ->orWhere('storeNum', 'like', $value) ->orWhere('remark', 'like', $value); }); } } private function mapRow(object $row): array { return [ 'id' => (string) $row->Id, 'record_code' => (string) $row->Id, 'store_name' => $row->shopName, 'custom_remark' => $row->buyerRemark, 'bill_item' => $row->tradeChannel, 'alipay_account' => $row->appId ?: $row->account, 'account_order_no' => $row->accountNum, 'business_serial_no' => $row->businessNum, 'merchant_order_no' => $row->merchantOrder, 'business_base_order_no' => $row->busiBasicNum, 'goods_name' => $row->commodityName, 'counterparty_account' => $row->oppAccount, 'occurred_at' => $this->dateTime($row->happenTime), 'income' => max((float) ($row->inAmount ?? 0), 0), 'expense' => abs((float) ($row->outAmount ?? 0)), 'balance' => (float) ($row->accountAmount ?? 0), 'business_type' => $row->businessType, 'remark' => $row->remark, 'online_order_no' => $row->storeNum, 'online_real_store' => $row->shopName, 'business_description' => $row->busiDescription, 'business_bill_source' => $row->busiBillSource, 'business_order_no' => $row->busiNum, 'created_at' => '', 'updated_at' => '', 'modifier' => '', ]; } private function dateTime(mixed $value): string { return $value === null || $value === '' ? '' : date('Y-m-d H:i:s', strtotime((string) $value)); } private function escapeLike(string $value): string { return str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $value); } }