>, * total: int, * page: int, * per_page: int, * } */ public function list( AdminUser $admin, int $page, int $perPage, SettlementOperationsListFilters $filters, ): array { $stubQueries = []; if ($filters->includesPayments()) { $paymentStub = $this->paymentStubQuery($admin, $filters); if ($paymentStub !== null) { $stubQueries[] = $paymentStub; } } if ($filters->includesAdjustments()) { $adjustmentStub = $this->adjustmentStubQuery($admin, $filters); if ($adjustmentStub !== null) { $stubQueries[] = $adjustmentStub; } } if ($stubQueries === []) { return [ 'items' => [], 'total' => 0, 'page' => $page, 'per_page' => $perPage, ]; } $offset = max(0, ($page - 1) * $perPage); if (count($stubQueries) === 1) { $base = $stubQueries[0]; $total = (int) (clone $base)->count(); $stubs = (clone $base) ->orderByDesc('sort_at') ->orderByDesc('record_id') ->offset($offset) ->limit($perPage) ->get(); } else { $union = null; foreach ($stubQueries as $stubQuery) { $union = $union === null ? $stubQuery : $union->unionAll($stubQuery); } $wrapped = DB::query()->fromSub($union, 'operations_page'); $total = (int) (clone $wrapped)->count(); $stubs = $wrapped ->orderByDesc('sort_at') ->orderByDesc('record_id') ->offset($offset) ->limit($perPage) ->get(); } return [ 'items' => $this->hydrateStubs($stubs), 'total' => $total, 'page' => $page, 'per_page' => $perPage, ]; } private function paymentStubQuery(AdminUser $admin, SettlementOperationsListFilters $filters): ?Builder { $query = DB::table('payment_records as pr') ->join('settlement_bills as sb', 'sb.id', '=', 'pr.settlement_bill_id') ->join('settlement_periods as sp', 'sp.id', '=', 'sb.settlement_period_id') ->selectRaw("'payment' as op_kind") ->selectRaw('pr.id as record_id') ->selectRaw('pr.settlement_bill_id as bill_id') ->selectRaw('COALESCE(pr.confirmed_at, pr.created_at) as sort_at'); $this->applySiteAndPeriodFilters($query, $admin, $filters, 'sp', 'sb.settlement_period_id'); AdminAgentSettlementScope::applyDirectEdgeScopeToBillsQuery($query, $admin, 'sb'); if ($filters->billId !== null) { $query->where('pr.settlement_bill_id', $filters->billId); } $this->applyPaymentKeywordFilter($query, $filters->keyword); return $query; } private function adjustmentStubQuery(AdminUser $admin, SettlementOperationsListFilters $filters): ?Builder { $query = DB::table('settlement_adjustments as sa') ->leftJoin('settlement_periods as sp', 'sp.id', '=', 'sa.settlement_period_id') ->leftJoin('settlement_bills as sb', 'sb.id', '=', 'sa.original_bill_id') ->selectRaw("CASE sa.adjustment_type WHEN 'bad_debt' THEN 'bad_debt' WHEN 'reversal' THEN 'reversal' ELSE 'adjustment' END as op_kind") ->selectRaw('sa.id as record_id') ->selectRaw('sa.original_bill_id as bill_id') ->selectRaw('sa.created_at as sort_at'); $this->applySiteAndPeriodFilters($query, $admin, $filters, 'sp', 'sa.settlement_period_id'); if ($filters->billId !== null) { $query->where('sa.original_bill_id', $filters->billId); } if ($filters->operationType === 'adjustment') { $query->where('sa.adjustment_type', 'adjustment'); } elseif ($filters->operationType === 'reversal') { $query->where('sa.adjustment_type', 'reversal'); } elseif ($filters->operationType === 'bad_debt') { $query->where('sa.adjustment_type', 'bad_debt'); } $actorId = AdminAgentSettlementScope::boundAgentNodeId($admin); if ($actorId !== null) { $query->where(function (Builder $outer) use ($admin): void { $outer->whereNull('sa.original_bill_id') ->orWhereExists(function (Builder $exists) use ($admin): void { $exists->selectRaw('1') ->from('settlement_bills as sb') ->whereColumn('sb.id', 'sa.original_bill_id'); AdminAgentSettlementScope::applyDirectEdgeScopeToBillsQuery($exists, $admin, 'sb'); }); }); } $this->applyAdjustmentKeywordFilter($query, $filters->keyword); return $query; } private function applySiteAndPeriodFilters( Builder $query, AdminUser $admin, SettlementOperationsListFilters $filters, string $periodsAlias, string $periodColumn, ): void { if ($filters->settlementPeriodId !== null) { $query->where($periodColumn, $filters->settlementPeriodId); } if ($filters->adminSiteId !== null) { $query->where($periodsAlias.'.admin_site_id', $filters->adminSiteId); } $siteIds = $admin->accessibleAdminSiteIds(); if ($siteIds !== null) { if ($siteIds === []) { $query->whereRaw('0 = 1'); return; } $query->whereIn($periodsAlias.'.admin_site_id', $siteIds); } } private function applyPaymentKeywordFilter(Builder $query, ?string $keyword): void { if ($keyword === null) { return; } $like = '%'.addcslashes(strtolower($keyword), '%_\\').'%'; $query->where(function (Builder $match) use ($like, $keyword): void { $match->whereRaw('LOWER(COALESCE(pr.method, \'\')) LIKE ?', [$like]) ->orWhereRaw('LOWER(COALESCE(pr.proof, \'\')) LIKE ?', [$like]) ->orWhereRaw('LOWER(COALESCE(pr.remark, \'\')) LIKE ?', [$like]) ->orWhereRaw('LOWER(CONCAT(pr.payer_type, \'#\', pr.payer_id, \' \', pr.payee_type, \'#\', pr.payee_id)) LIKE ?', [$like]) ->orWhereRaw('LOWER(CONCAT(\'payment\')) LIKE ?', [$like]); if (ctype_digit($keyword)) { $match->orWhere('pr.id', (int) $keyword) ->orWhere('pr.settlement_bill_id', (int) $keyword); } }); } private function applyAdjustmentKeywordFilter(Builder $query, ?string $keyword): void { if ($keyword === null) { return; } $like = '%'.addcslashes(strtolower($keyword), '%_\\').'%'; $query->where(function (Builder $match) use ($like, $keyword): void { $match->whereRaw('LOWER(COALESCE(sa.reason, \'\')) LIKE ?', [$like]) ->orWhereRaw('LOWER(sa.adjustment_type) LIKE ?', [$like]); if (ctype_digit($keyword)) { $match->orWhere('sa.id', (int) $keyword) ->orWhere('sa.original_bill_id', (int) $keyword); } }); } /** * @param Collection $stubs * @return list> */ private function hydrateStubs(Collection $stubs): array { if ($stubs->isEmpty()) { return []; } $paymentIds = []; $adjustmentIds = []; foreach ($stubs as $stub) { $kind = (string) $stub->op_kind; $id = (int) $stub->record_id; if ($kind === 'payment') { $paymentIds[] = $id; } else { $adjustmentIds[] = $id; } } $payments = $paymentIds === [] ? collect() : DB::table('payment_records')->whereIn('id', $paymentIds)->get()->keyBy('id'); $adjustments = $adjustmentIds === [] ? collect() : DB::table('settlement_adjustments')->whereIn('id', $adjustmentIds)->get()->keyBy('id'); $items = []; foreach ($stubs as $stub) { $kind = (string) $stub->op_kind; $id = (int) $stub->record_id; if ($kind === 'payment') { $row = $payments->get($id); if ($row === null) { continue; } $items[] = $this->formatPaymentRow($row); } else { $row = $adjustments->get($id); if ($row === null) { continue; } $items[] = $this->formatAdjustmentRow($row, $kind); } } return $items; } /** * @return array */ private function formatPaymentRow(object $row): array { $payer = (string) $row->payer_type === 'platform' ? 'platform' : (string) $row->payer_type.'#'.(int) $row->payer_id; $payee = (string) $row->payee_type === 'platform' ? 'platform' : (string) $row->payee_type.'#'.(int) $row->payee_id; $detailParts = [$payer.' → '.$payee]; if (trim((string) ($row->proof ?? '')) !== '') { $detailParts[] = (string) $row->proof; } if (trim((string) ($row->remark ?? '')) !== '') { $detailParts[] = (string) $row->remark; } $sortAt = $row->confirmed_at ?? $row->created_at; return [ 'kind' => 'payment', 'record_id' => (int) $row->id, 'bill_id' => (int) $row->settlement_bill_id, 'amount' => (int) $row->amount, 'summary' => trim((string) ($row->method ?? '')) !== '' ? (string) $row->method : '—', 'detail' => implode(' · ', $detailParts), 'sort_at' => $sortAt !== null ? Carbon::parse($sortAt)->toIso8601String() : null, ]; } /** * @return array */ private function formatAdjustmentRow(object $row, string $kind): array { return [ 'kind' => $kind, 'record_id' => (int) $row->id, 'bill_id' => (int) ($row->original_bill_id ?? 0), 'amount' => (int) $row->amount, 'summary' => trim((string) ($row->reason ?? '')) !== '' ? (string) $row->reason : '—', 'detail' => null, 'sort_at' => $row->created_at !== null ? Carbon::parse($row->created_at)->toIso8601String() : null, ]; } }