'即将到期', 'expired' => '约定期限已届满', 'active' => '有效期内', 'upcoming' => '未到开始日期', 'unknown' => '期限待核对', ]; public function page(bool $includeRows = true): array { $search = mb_substr(trim((string) request()->query('contract_search', '')), 0, 255); $status = (string) request()->query('contract_status', ''); $status = array_key_exists($status, self::STATUSES) ? $status : ''; $from = $this->date(request()->query('contract_end_from')); $to = $this->date(request()->query('contract_end_to')); if ($from && $to && $from > $to) { [$from, $to] = [$to, $from]; } $filters = ['contract_search' => $search, 'contract_status' => $status, 'contract_end_from' => $from ?? '', 'contract_end_to' => $to ?? '']; $limit = min(max((int) setting('default.list_limit', 25), 1), 100); $page = max((int) request()->query('page', 1), 1); $options = ['path' => request()->url(), 'query' => request()->except('page')]; $data = [ 'courierContractFilters' => $filters, 'courierContractRows' => new LengthAwarePaginator([], 0, $limit, $page, $options), 'courierContractCounts' => array_fill_keys(array_keys(self::STATUSES), 0), 'courierContractUnreadable' => 0, 'courierContractError' => null, ]; try { if (! Schema::hasTable('business_courier_agreement_signatures')) { $data['courierContractError'] = '协议签署数据表尚未建立,合同管理暂时无法读取。'; return $data; } $matched = []; $today = Carbon::today(config('app.timezone')); // Dates and partner types are encrypted. Stream bounded chunks instead of SQL JSON queries // or persisting a second, potentially stale copy of the signed business snapshot. $signatures = CourierAgreementSignature::query()->where('company_id', (int) company_id()) ->where('document_type', 'cooperation-contract')->where('status', CourierAgreementSignature::STATUS_ARCHIVED) ->select(['id', 'company_id', 'business_no', 'courier_name', 'payload', 'document_type', 'status', 'signed_pdf_path', 'certificate_path', 'archived_at', 'created_by', 'created_at']) ->lazyById(250); foreach ($signatures as $signature) { $payload = $signature->archivedCourierContractPayload(); if ($payload === null) { try { $signature->payload; } catch (Throwable) { $data['courierContractUnreadable']++; } continue; } $row = $this->snapshot($signature, $payload, $today); $data['courierContractCounts'][$row['status']]++; if (! $includeRows || ($status !== '' && $status !== $row['status'])) { continue; } if (($from && (! $row['end_date'] || $row['end_date'] < $from)) || ($to && (! $row['end_date'] || $row['end_date'] > $to))) { continue; } if ($search !== '' && ! str_contains(mb_strtolower(implode(' ', [ $row['courier_name'], $row['number'], $row['business_no'], $row['party_a'], $row['party_b'], ])), mb_strtolower($search))) { continue; } $matched[] = $row; } if ($includeRows) { $priority = ['due' => 0, 'expired' => 1, 'unknown' => 2, 'active' => 3, 'upcoming' => 4]; usort($matched, static fn (array $a, array $b): int => [$priority[$a['status']], $a['end_date'] ?? '9999-12-31', -$a['id']] <=> [$priority[$b['status']], $b['end_date'] ?? '9999-12-31', -$b['id']]); $total = count($matched); $items = array_slice($matched, ($page - 1) * $limit, $limit); $owners = user_model_class()::query()->whereIn('id', array_filter(array_column($items, 'created_by'))) ->pluck('name', 'id'); foreach ($items as &$row) { $row['creator'] = $owners[$row['created_by']] ?? '未记录'; $row['has_signed_file'] = $this->listedFileAvailable($row['signed_path']); $row['has_certificate'] = $this->listedFileAvailable($row['certificate_path']); unset($row['signed_path'], $row['certificate_path']); } unset($row); $data['courierContractRows'] = new LengthAwarePaginator($items, $total, $limit, $page, $options); } } catch (Throwable $exception) { report($exception); $data['courierContractCounts'] = array_fill_keys(array_keys(self::STATUSES), 0); $data['courierContractError'] = '归档合同暂时无法读取,请稍后重试。'; } return $data; } private function snapshot(CourierAgreementSignature $signature, array $payload, Carbon $today): array { $start = $this->date($payload['cooperation_start_date'] ?? null); $end = $this->date($payload['cooperation_end_date'] ?? null); $days = $end ? (int) $today->diffInDays(Carbon::parse($end, config('app.timezone')), false) : null; $status = match (true) { ! $start || ! $end || $start > $end => 'unknown', $end < $today->toDateString() => 'expired', $start > $today->toDateString() => 'upcoming', $days <= self::REMINDER_DAYS => 'due', default => 'active', }; $person = ($payload['counterparty_signer_type'] ?? 'organization') === 'person'; return [ 'id' => (int) $signature->id, 'courier_name' => (string) $signature->courier_name, 'business_no' => (string) $signature->business_no, 'number' => trim((string) ($payload['cooperation_contract_number'] ?? '')), 'party_a' => trim((string) ($payload['cooperation_party_a_name'] ?? '')), 'party_b' => trim((string) ($payload['cooperation_party_b_name'] ?? '')), 'legal_representative' => $person ? '不适用(个人签署)' : trim((string) ($payload['cooperation_party_b_legal_representative'] ?? '')), 'start_date' => $start, 'end_date' => $end, 'days' => $status === 'unknown' ? null : $days, 'status' => $status, 'created_by' => $signature->created_by, 'created_at' => $signature->created_at, 'archived_at' => $signature->archived_at, 'signed_path' => $signature->signed_pdf_path, 'certificate_path' => $signature->certificate_path, ]; } private function date(mixed $value): ?string { if (! is_string($value) || ! preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) { return null; } [$year, $month, $day] = array_map('intval', explode('-', $value)); return checkdate($month, $day, $year) ? $value : null; } private function listedFileAvailable(?string $path): bool { if (blank($path)) { return false; } // Do not issue up to 50 sequential object-storage HEAD requests for one list page. // Downloads always verify the actual file; local files can be checked cheaply here. return config('filesystems.disks.uploads.driver') !== 'local' || Storage::disk('uploads')->exists($path); } }