164 lines
8.1 KiB
PHP
164 lines
8.1 KiB
PHP
<?php
|
|
|
|
namespace App\Business;
|
|
|
|
use App\Models\Business\CourierAgreementSignature;
|
|
use Illuminate\Pagination\LengthAwarePaginator;
|
|
use Illuminate\Support\Carbon;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Throwable;
|
|
|
|
/** Read the signed snapshot directly: no duplicate contracts, attachment copies or archive backfill. */
|
|
class CourierContractPageData
|
|
{
|
|
public const REMINDER_DAYS = 30;
|
|
|
|
public const STATUSES = [
|
|
'due' => '即将到期',
|
|
'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);
|
|
}
|
|
}
|