Files
zyt/server/app/adminapi/lists/firstvisit/MyPatientLists.php
T
2026-09-09 15:47:48 +08:00

386 lines
15 KiB
PHP

<?php
declare(strict_types=1);
namespace app\adminapi\lists\firstvisit;
use app\adminapi\lists\BaseAdminDataLists;
use app\adminapi\logic\firstvisit\MyPatientLogic;
use app\common\enum\AppointmentTypeEnum;
use app\common\lists\ListsExtendInterface;
use app\common\lists\ListsSearchInterface;
use app\common\model\DiagnosisViewRecord;
use app\common\model\auth\Admin;
use app\common\model\doctor\Appointment;
use app\common\model\tcm\Diagnosis;
use think\db\Query;
use think\facade\Db;
class MyPatientLists extends BaseAdminDataLists implements ListsSearchInterface, ListsExtendInterface
{
private const EFFECTIVE_APPOINTMENT_STATUSES = [1, 3, 4];
public function setSearch(): array
{
return [];
}
public function lists(): array
{
$query = $this->buildQuery(true, true);
$appointmentTable = (new Appointment())->getTable();
$today = date('Y-m-d');
$upcomingSql = "SELECT MIN(CONCAT(sort_apt.appointment_date, ' ', IFNULL(NULLIF(TRIM(sort_apt.appointment_time), ''), '00:00:00')))"
. " FROM {$appointmentTable} sort_apt"
. ' WHERE sort_apt.patient_id = d.id'
. ' AND sort_apt.status IN (1,4)'
. " AND sort_apt.appointment_date >= '{$today}'";
$rows = $query
->field([
'd.id', 'd.patient_id', 'd.patient_name', 'd.phone', 'd.id_card', 'd.gender', 'd.age',
'd.diagnosis_date', 'd.diagnosis_type', 'd.syndrome_type', 'd.assistant_id',
'd.assign_read_at', 'd.create_time',
])
->orderRaw("CASE WHEN ({$upcomingSql}) IS NULL THEN 1 ELSE 0 END ASC")
->orderRaw("IFNULL(({$upcomingSql}), '9999-12-31 23:59:59') ASC")
->order('d.id', 'desc')
->limit($this->limitOffset, $this->limitLength)
->select()
->toArray();
return $this->appendRelations($rows);
}
public function count(): int
{
return (int) $this->buildQuery(true, true)->count('d.id');
}
public function extend(): array
{
$today = date('Y-m-d');
$tomorrow = date('Y-m-d', strtotime('+1 day'));
$dayAfter = date('Y-m-d', strtotime('+2 days'));
return [
'summary' => [
'today' => $this->countByAppointmentDate($today),
'tomorrow' => $this->countByAppointmentDate($tomorrow),
'day_after' => $this->countByAppointmentDate($dayAfter),
],
'scope' => MyPatientLogic::scopeMeta($this->adminId, $this->adminInfo),
'dates' => [
'today' => $today,
'tomorrow' => $tomorrow,
'day_after' => $dayAfter,
],
];
}
private function buildQuery(bool $applyStatusFilter, bool $applyDateFilter): Query
{
$diagnosisTable = (new Diagnosis())->getTable();
$query = Db::table($diagnosisTable)
->alias('d')
->whereNull('d.delete_time')
->where('d.status', 1);
MyPatientLogic::applyScope($query, $this->adminId, $this->adminInfo);
$this->applyKeyword($query);
$statusFilter = $applyStatusFilter ? trim((string) ($this->params['status_filter'] ?? '')) : '';
$appointmentTable = (new Appointment())->getTable();
if ($statusFilter === 'unbooked') {
$query->whereNotExists(
"SELECT 1 FROM {$appointmentTable} unbooked_apt"
. ' WHERE unbooked_apt.patient_id = d.id'
. ' AND unbooked_apt.status IN (' . implode(',', self::EFFECTIVE_APPOINTMENT_STATUSES) . ')'
);
}
$appointmentStatuses = self::EFFECTIVE_APPOINTMENT_STATUSES;
if (in_array($statusFilter, ['pending_interview', 'booked'], true)) {
$appointmentStatuses = [1];
} elseif ($statusFilter === 'completed') {
$appointmentStatuses = [3];
} elseif ($statusFilter === 'missed') {
$appointmentStatuses = [4];
}
$needsAppointmentFilter = in_array(
$statusFilter,
['pending_interview', 'booked', 'completed', 'missed'],
true
);
[$startDate, $endDate] = $applyDateFilter ? $this->dateRange() : ['', ''];
if ($startDate !== '' || $endDate !== '') {
$needsAppointmentFilter = true;
}
if ($needsAppointmentFilter) {
$conditions = [
'filter_apt.patient_id = d.id',
'filter_apt.status IN (' . implode(',', $appointmentStatuses) . ')',
];
if ($startDate !== '') {
$conditions[] = "filter_apt.appointment_date >= '{$startDate}'";
}
if ($endDate !== '') {
$conditions[] = "filter_apt.appointment_date <= '{$endDate}'";
}
$query->whereExists(
"SELECT 1 FROM {$appointmentTable} filter_apt WHERE " . implode(' AND ', $conditions)
);
}
return $query;
}
private function applyKeyword(Query $query): void
{
$keyword = trim((string) ($this->params['keyword'] ?? ''));
if ($keyword === '') {
return;
}
$needle = addslashes($keyword);
$adminTable = (new Admin())->getTable();
$appointmentTable = (new Appointment())->getTable();
$query->whereRaw(
"(d.patient_name LIKE '%{$needle}%'"
. " OR d.phone LIKE '%{$needle}%'"
. " OR EXISTS (SELECT 1 FROM {$adminTable} assistant_admin"
. ' WHERE assistant_admin.id = CAST(d.assistant_id AS UNSIGNED)'
. ' AND assistant_admin.delete_time IS NULL'
. " AND assistant_admin.name LIKE '%{$needle}%')"
. " OR EXISTS (SELECT 1 FROM {$appointmentTable} keyword_apt"
. " INNER JOIN {$adminTable} doctor_admin ON doctor_admin.id = keyword_apt.doctor_id"
. ' AND doctor_admin.delete_time IS NULL'
. ' WHERE keyword_apt.patient_id = d.id'
. ' AND keyword_apt.status IN (1,3,4)'
. " AND doctor_admin.name LIKE '%{$needle}%'))"
);
}
/** @return array{0:string,1:string} */
private function dateRange(): array
{
$startDate = $this->normalizeDate($this->params['start_date'] ?? '');
$endDate = $this->normalizeDate($this->params['end_date'] ?? '');
if ($startDate === '' && $endDate !== '') {
$startDate = $endDate;
}
if ($endDate === '' && $startDate !== '') {
$endDate = $startDate;
}
if ($startDate !== '' && $endDate !== '' && $startDate > $endDate) {
[$startDate, $endDate] = [$endDate, $startDate];
}
return [$startDate, $endDate];
}
private function normalizeDate($value): string
{
$value = trim((string) $value);
return preg_match('/^\d{4}-\d{2}-\d{2}$/', $value) ? $value : '';
}
private function countByAppointmentDate(string $date): int
{
$query = $this->buildQuery(false, false);
$appointmentTable = (new Appointment())->getTable();
$query->whereExists(
"SELECT 1 FROM {$appointmentTable} summary_apt"
. ' WHERE summary_apt.patient_id = d.id'
. " AND summary_apt.appointment_date = '{$date}'"
. ' AND summary_apt.status IN (1,3,4)'
);
return (int) $query->count('d.id');
}
/**
* @param array<int, array<string, mixed>> $rows
* @return array<int, array<string, mixed>>
*/
private function appendRelations(array $rows): array
{
if ($rows === []) {
return [];
}
$diagnosisIds = array_values(array_unique(array_map('intval', array_column($rows, 'id'))));
$assistantIds = array_values(array_unique(array_filter(array_map('intval', array_column($rows, 'assistant_id')))));
$appointmentTable = (new Appointment())->getTable();
$appointments = Db::table($appointmentTable)
->whereIn('patient_id', $diagnosisIds)
->whereIn('status', self::EFFECTIVE_APPOINTMENT_STATUSES)
->field(['id', 'patient_id', 'doctor_id', 'appointment_date', 'appointment_time', 'appointment_type', 'status'])
->order('appointment_date', 'asc')
->order('appointment_time', 'asc')
->order('id', 'asc')
->select()
->toArray();
$doctorIds = array_values(array_unique(array_filter(array_map('intval', array_column($appointments, 'doctor_id')))));
$adminIds = array_values(array_unique(array_merge($assistantIds, $doctorIds)));
$adminNames = $adminIds === [] ? [] : Admin::whereIn('id', $adminIds)->whereNull('delete_time')->column('name', 'id');
$appointmentMap = [];
foreach ($appointments as $appointment) {
$diagnosisId = (int) ($appointment['patient_id'] ?? 0);
if ($diagnosisId > 0) {
$appointmentMap[$diagnosisId][] = $appointment;
}
}
$viewTable = (new DiagnosisViewRecord())->getTable();
$confirmedIds = Db::table($viewTable)
->whereIn('diagnosis_id', $diagnosisIds)
->where('is_confirmed', 1)
->whereNull('delete_time')
->column('diagnosis_id');
$confirmedSet = array_fill_keys(array_map('intval', $confirmedIds), true);
[$rangeStart, $rangeEnd] = $this->dateRange();
$today = date('Y-m-d');
$statusFilter = trim((string) ($this->params['status_filter'] ?? ''));
$preferredStatuses = [
'pending_interview' => [1],
'booked' => [1],
'completed' => [3],
'missed' => [4],
][$statusFilter] ?? [];
foreach ($rows as &$row) {
$diagnosisId = (int) $row['id'];
$rowAppointments = $appointmentMap[$diagnosisId] ?? [];
$primary = $this->pickPrimaryAppointment(
$rowAppointments,
$rangeStart,
$rangeEnd,
$today,
$preferredStatuses
);
$completedCount = count(array_filter($rowAppointments, static function (array $appointment): bool {
return (int) ($appointment['status'] ?? 0) === 3;
}));
$assistantId = (int) ($row['assistant_id'] ?? 0);
$row['diagnosis_id'] = $diagnosisId;
$row['source_patient_id'] = (int) ($row['patient_id'] ?? 0);
$row['phone_masked'] = $this->maskPhone((string) ($row['phone'] ?? ''));
unset($row['phone']);
$row['has_id_card'] = trim((string) ($row['id_card'] ?? '')) !== '' ? 1 : 0;
unset($row['id_card']);
$row['gender_desc'] = (int) ($row['gender'] ?? 0) === 1 ? '男' : '女';
$row['diagnosis_date_text'] = $this->formatDiagnosisDate($row['diagnosis_date'] ?? '');
$row['assistant_name'] = (string) ($adminNames[$assistantId] ?? '未分配');
$row['confirmed'] = isset($confirmedSet[$diagnosisId]) ? 1 : 0;
$row['confirmation_text'] = $row['confirmed'] ? '已确认' : '待确认';
$row['visit_count'] = $completedCount;
$row['revisit_count'] = max(0, $completedCount - 1);
$this->appendPrimaryAppointmentSummary($row, $primary, $adminNames);
}
unset($row);
return $rows;
}
/** 所有挂号展示字段取自同一条主挂号;无挂号时不能补成视频类型。 */
private function appendPrimaryAppointmentSummary(array &$row, ?array $primary, array $adminNames): void
{
$row['appointment_id'] = $primary ? (int) $primary['id'] : 0;
$row['appointment_status'] = $primary ? (int) $primary['status'] : 0;
$row['appointment_status_text'] = $this->appointmentStatusText((int) ($primary['status'] ?? 0));
$row['appointment_doctor_id'] = $primary ? (int) $primary['doctor_id'] : 0;
$row['appointment_doctor_name'] = $primary
? (string) ($adminNames[(int) $primary['doctor_id']] ?? '未知医生')
: '未预约';
$row['appointment_time_text'] = $primary ? $this->appointmentTimeText($primary) : '';
$row['appointment_type'] = $primary !== null
? AppointmentTypeEnum::normalizeStored($primary['appointment_type'] ?? null)
: null;
$row['appointment_type_desc'] = $primary !== null
? AppointmentTypeEnum::description($row['appointment_type'])
: '';
$row['has_appointment'] = $primary !== null ? 1 : 0;
}
/**
* @param array<int, array<string, mixed>> $appointments
* @return array<string, mixed>|null
*/
private function pickPrimaryAppointment(
array $appointments,
string $rangeStart,
string $rangeEnd,
string $today,
array $preferredStatuses = []
): ?array
{
if ($appointments === []) {
return null;
}
$candidates = $appointments;
if ($preferredStatuses !== []) {
$candidates = array_values(array_filter($candidates, static function (array $appointment) use ($preferredStatuses): bool {
return in_array((int) ($appointment['status'] ?? 0), $preferredStatuses, true);
}));
}
if ($rangeStart !== '' || $rangeEnd !== '') {
$candidates = array_values(array_filter($candidates, static function (array $appointment) use ($rangeStart, $rangeEnd): bool {
$date = (string) ($appointment['appointment_date'] ?? '');
return ($rangeStart === '' || $date >= $rangeStart) && ($rangeEnd === '' || $date <= $rangeEnd);
}));
}
if ($candidates === []) {
$candidates = $appointments;
}
foreach ($candidates as $appointment) {
$status = (int) ($appointment['status'] ?? 0);
$date = (string) ($appointment['appointment_date'] ?? '');
if (in_array($status, [1, 4], true) && $date >= $today) {
return $appointment;
}
}
return $candidates[count($candidates) - 1] ?? null;
}
private function maskPhone(string $phone): string
{
return preg_replace('/^(\d{3})\d{4}(\d{4})$/', '$1****$2', $phone) ?: $phone;
}
private function formatDiagnosisDate($value): string
{
if (is_numeric($value)) {
return (int) $value > 0 ? date('Y-m-d', (int) $value) : '';
}
$text = trim((string) $value);
return $text === '' ? '' : substr($text, 0, 10);
}
private function appointmentTimeText(array $appointment): string
{
$time = trim((string) ($appointment['appointment_time'] ?? ''));
if (strlen($time) > 5) {
$time = substr($time, 0, 5);
}
return trim((string) ($appointment['appointment_date'] ?? '') . ' ' . $time);
}
private function appointmentStatusText(int $status): string
{
return [1 => '待面诊', 3 => '已完成', 4 => '已过号'][$status] ?? '未预约';
}
}