type = $type; } /** * @notes 搜索条件 * @return array */ public function setSearch(): array { return []; } /** * @notes 获取列表 * @return array */ public function lists(): array { if ($this->type === 'dept') { return $this->getDeptStatistics(); } $doctorId = $this->params['doctor_id'] ?? ''; $timeType = $this->params['time_type'] ?? 'today'; $startDate = $this->params['start_date'] ?? ''; $endDate = $this->params['end_date'] ?? ''; // 计算时间范围 list($startDate, $endDate, $timeRangeText) = $this->getTimeRange($timeType, $startDate, $endDate); // 获取医生列表(role_id=1 表示医生角色) $doctorAdminIds = \app\common\model\auth\AdminRole::where('role_id', 1)->column('admin_id'); if (empty($doctorAdminIds)) { return []; } // 查询1:获取预约统计数据(快速查询,只查appointment表) $statisticsData = \think\facade\Db::name('doctor_appointment') ->field([ 'doctor_id', 'COUNT(*) as total_count', 'SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as registered_count', 'SUM(CASE WHEN status = 3 THEN 1 ELSE 0 END) as completed_count', 'SUM(CASE WHEN status = 4 THEN 1 ELSE 0 END) as missed_count', 'SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as cancelled_count' ]) ->whereIn('doctor_id', $doctorAdminIds) ->where('appointment_date', '>=', $startDate) ->where('appointment_date', '<=', $endDate); if ($doctorId) { $statisticsData->where('doctor_id', $doctorId); } $statisticsData = $statisticsData->group('doctor_id')->select()->toArray(); // 将统计数据按 doctor_id 索引 $statsMap = []; foreach ($statisticsData as $stat) { $statsMap[$stat['doctor_id']] = $stat; } // 诊单数:统计期内该医生挂号对应的 distinct 诊单(patient_id 存的是诊单 id) $diagnosisCountQuery = \think\facade\Db::name('doctor_appointment') ->field('doctor_id, COUNT(DISTINCT patient_id) as diagnosis_count') ->whereIn('doctor_id', $doctorAdminIds) ->where('appointment_date', '>=', $startDate) ->where('appointment_date', '<=', $endDate); if ($doctorId) { $diagnosisCountQuery->where('doctor_id', $doctorId); } $diagnosisCountRows = $diagnosisCountQuery->group('doctor_id')->select()->toArray(); $diagnosisCountMap = []; foreach ($diagnosisCountRows as $row) { $diagnosisCountMap[(int) $row['doctor_id']] = (int) $row['diagnosis_count']; } // 成交单:上述诊单中存在有效处方(未删除、未作废)的数量 $dealCountQuery = \think\facade\Db::name('doctor_appointment')->alias('apt') ->join('tcm_prescription rx', 'rx.diagnosis_id = apt.patient_id') ->field('apt.doctor_id, COUNT(DISTINCT apt.patient_id) as deal_count') ->whereIn('apt.doctor_id', $doctorAdminIds) ->where('apt.appointment_date', '>=', $startDate) ->where('apt.appointment_date', '<=', $endDate) ->whereNull('rx.delete_time') ->whereRaw('IFNULL(rx.void_status, 0) <> 1'); if ($doctorId) { $dealCountQuery->where('apt.doctor_id', $doctorId); } $dealCountRows = $dealCountQuery->group('apt.doctor_id')->select()->toArray(); $dealCountMap = []; foreach ($dealCountRows as $row) { $dealCountMap[(int) $row['doctor_id']] = (int) $row['deal_count']; } // 获取医生信息 $doctorQuery = Admin::field('id,name')->whereIn('id', $doctorAdminIds); if ($doctorId) { $doctorQuery->where('id', $doctorId); } $doctors = $doctorQuery->select()->toArray(); // 查询2:获取部门统计信息(通过assistant_id直接关联,按部门和状态分组统计数量) $doctorIdsWithAppointments = array_keys($statsMap); $deptStatsMap = []; $deptStatusMap = []; $channelStatsMap = []; $channelStatusMap = []; if (!empty($doctorIdsWithAppointments)) { // 通过 assistant_id → admin_dept → dept 获取部门统计(按状态分组) $deptStats = \think\facade\Db::name('doctor_appointment') ->alias('apt') ->leftJoin('admin_dept ad', 'apt.assistant_id = ad.admin_id') ->leftJoin('dept dept', 'ad.dept_id = dept.id') ->field([ 'apt.doctor_id', 'dept.id as dept_id', 'dept.name as dept_name', 'apt.status', 'COUNT(*) as count' ]) ->whereIn('apt.doctor_id', $doctorIdsWithAppointments) ->where('apt.appointment_date', '>=', $startDate) ->where('apt.appointment_date', '<=', $endDate) ->whereNotNull('dept.id') ->group('apt.doctor_id, dept.id, apt.status') ->select() ->toArray(); // 组装部门统计数据:医生ID => "部门1(数量1) 部门2(数量2)" // 同时组装部门状态数据:医生ID => 部门ID => 状态 => 数量 foreach ($deptStats as $stat) { $doctorId = $stat['doctor_id']; $deptId = $stat['dept_id']; $deptName = $stat['dept_name']; $status = $stat['status']; $count = $stat['count']; // 组装部门统计数据 if (!isset($deptStatsMap[$doctorId])) { $deptStatsMap[$doctorId] = []; } if (!isset($deptStatsMap[$doctorId][$deptId])) { $deptStatsMap[$doctorId][$deptId] = [ 'dept_name' => $deptName, 'total_count' => 0 ]; } $deptStatsMap[$doctorId][$deptId]['total_count'] += $count; // 组装部门状态数据 if (!isset($deptStatusMap[$doctorId])) { $deptStatusMap[$doctorId] = []; } if (!isset($deptStatusMap[$doctorId][$deptId])) { $deptStatusMap[$doctorId][$deptId] = [ 'dept_name' => $deptName, 'statuses' => [] ]; } $deptStatusMap[$doctorId][$deptId]['statuses'][$status] = $count; } // 格式化部门统计数据 foreach ($deptStatsMap as $doctorId => &$depts) { $formattedDepts = []; foreach ($depts as $dept) { $formattedDepts[] = $dept['dept_name'] . '(' . $dept['total_count'] . ')'; } $deptStatsMap[$doctorId] = $formattedDepts; } // 获取渠道字典(dict_data.value => name),与挂号创建时 channel_source 存字典 value 一致 $channelDict = \think\facade\Db::name('dict_data') ->where('type_value', 'channels') ->column('name', 'value'); // 渠道:业务保存的是 channel_source(字典 value 字符串,见 AppointmentLogic::create);旧库可能仅有 channels(tinyint) $channelBuckets = []; $channelStatusBuckets = []; try { $channelStats = \think\facade\Db::name('doctor_appointment') ->field('doctor_id, channel_source, status, COUNT(*) as channel_count') ->whereIn('doctor_id', $doctorIdsWithAppointments) ->where('appointment_date', '>=', $startDate) ->where('appointment_date', '<=', $endDate) ->where('channel_source', '<>', '') ->group('doctor_id, channel_source, status') ->select() ->toArray(); foreach ($channelStats as $stat) { $did = (int) $stat['doctor_id']; $src = trim((string) ($stat['channel_source'] ?? '')); $status = $stat['status']; $cnt = (int) ($stat['channel_count'] ?? 0); if ($src === '' || $cnt < 1) { continue; } // 组装渠道统计数据 if (!isset($channelBuckets[$did])) { $channelBuckets[$did] = []; } if (!isset($channelBuckets[$did][$src])) { $channelBuckets[$did][$src] = 0; } $channelBuckets[$did][$src] += $cnt; // 组装渠道状态数据 if (!isset($channelStatusBuckets[$did])) { $channelStatusBuckets[$did] = []; } if (!isset($channelStatusBuckets[$did][$src])) { $channelStatusBuckets[$did][$src] = []; } $channelStatusBuckets[$did][$src][$status] = $cnt; } } catch (\Throwable) { // 无 channel_source 字段等 } $mergeLegacyChannels = static function (array &$buckets, array &$statusBuckets, array $rows): void { foreach ($rows as $stat) { $did = (int) $stat['doctor_id']; $key = (string) (int) ($stat['channels'] ?? 0); $status = $stat['status']; $cnt = (int) ($stat['channel_count'] ?? 0); if ($key === '0' || $cnt < 1) { continue; } // 组装渠道统计数据 if (!isset($buckets[$did])) { $buckets[$did] = []; } if (!isset($buckets[$did][$key])) { $buckets[$did][$key] = 0; } $buckets[$did][$key] += $cnt; // 组装渠道状态数据 if (!isset($statusBuckets[$did])) { $statusBuckets[$did] = []; } if (!isset($statusBuckets[$did][$key])) { $statusBuckets[$did][$key] = []; } $statusBuckets[$did][$key][$status] = $cnt; } }; try { $legacyStats = \think\facade\Db::name('doctor_appointment') ->field('doctor_id, channels, status, COUNT(*) as channel_count') ->whereIn('doctor_id', $doctorIdsWithAppointments) ->where('appointment_date', '>=', $startDate) ->where('appointment_date', '<=', $endDate) ->where('channels', '>', 0) ->where(function ($q) { $q->whereNull('channel_source')->whereOr('channel_source', '=', ''); }) ->group('doctor_id, channels, status') ->select() ->toArray(); $mergeLegacyChannels($channelBuckets, $channelStatusBuckets, $legacyStats); } catch (\Throwable) { try { $legacyStats = \think\facade\Db::name('doctor_appointment') ->field('doctor_id, channels, status, COUNT(*) as channel_count') ->whereIn('doctor_id', $doctorIdsWithAppointments) ->where('appointment_date', '>=', $startDate) ->where('appointment_date', '<=', $endDate) ->where('channels', '>', 0) ->group('doctor_id, channels, status') ->select() ->toArray(); $mergeLegacyChannels($channelBuckets, $channelStatusBuckets, $legacyStats); } catch (\Throwable) { // 无 channels 字段 } } foreach ($channelBuckets as $did => $byKey) { $parts = []; foreach ($byKey as $key => $cnt) { $label = $channelDict[$key] ?? $channelDict[(string) $key] ?? $key; $parts[] = $label . '(' . $cnt . ')'; } $channelStatsMap[$did] = $parts; } // 组装渠道状态明细数据 foreach ($channelStatusBuckets as $did => $byKey) { foreach ($byKey as $key => $statuses) { $label = $channelDict[$key] ?? $channelDict[(string) $key] ?? $key; if (!isset($channelStatusMap[$did])) { $channelStatusMap[$did] = []; } $channelStatusMap[$did][] = [ 'channel_name' => $label, 'statuses' => $statuses ]; } } } // 组装结果 $result = []; foreach ($doctors as $doctor) { $stat = $statsMap[$doctor['id']] ?? [ 'total_count' => 0, 'registered_count' => 0, 'completed_count' => 0, 'missed_count' => 0, 'cancelled_count' => 0 ]; $did = (int) $doctor['id']; $diagnosisCount = $diagnosisCountMap[$did] ?? 0; $dealCount = $dealCountMap[$did] ?? 0; $dealRate = $diagnosisCount > 0 ? round(($dealCount / $diagnosisCount) * 100, 2) : 0; // 计算完成率 $completionRate = $stat['total_count'] > 0 ? round(($stat['completed_count'] / $stat['total_count']) * 100, 2) : 0; // 格式化部门状态明细 $deptStatusDetails = []; if (isset($deptStatusMap[$doctor['id']])) { foreach ($deptStatusMap[$doctor['id']] as $deptId => $deptInfo) { $deptStatusDetails[] = [ 'dept_id' => $deptId, 'dept_name' => $deptInfo['dept_name'], 'statuses' => $deptInfo['statuses'] ]; } } // 格式化渠道状态明细 $channelStatusDetails = []; if (isset($channelStatusMap[$doctor['id']])) { $channelStatusDetails = $channelStatusMap[$doctor['id']]; } $result[] = [ 'doctor_id' => $doctor['id'], 'doctor_name' => $doctor['name'], 'total_count' => (int)$stat['total_count'] ?? 0, 'registered_count' => (int)$stat['registered_count'] ?? 0, 'completed_count' => (int)$stat['completed_count'] ?? 0, 'missed_count' => (int)$stat['missed_count'] ?? 0, 'cancelled_count' => (int)$stat['cancelled_count'] ?? 0, 'diagnosis_count' => $diagnosisCount ?? 0, 'deal_count' => $dealCount ?? 0, 'deal_rate' => $dealRate ?? 0, 'completion_rate' => $completionRate ?? 0, 'dept_status_details' => $deptStatusDetails, 'channel_status_details' => $channelStatusDetails, 'time_range' => $timeRangeText ]; } return $result; } /** * @notes 获取部门统计数据 * @return array */ private function getDeptStatistics() { $deptId = $this->params['dept_id'] ?? ''; $timeType = $this->params['time_type'] ?? 'today'; $startDate = $this->params['start_date'] ?? ''; $endDate = $this->params['end_date'] ?? ''; // 计算时间范围 list($startDate, $endDate, $timeRangeText) = $this->getTimeRange($timeType, $startDate, $endDate); // 查询部门统计数据 $deptStatsQuery = \think\facade\Db::name('doctor_appointment') ->alias('apt') ->leftJoin('zyt_admin_dept ad', 'apt.assistant_id = ad.admin_id') ->leftJoin('zyt_dept dept', 'ad.dept_id = dept.id') ->field([ 'dept.id as dept_id', 'dept.name as dept_name', 'COUNT(*) as total_count', 'SUM(CASE WHEN apt.status = 1 THEN 1 ELSE 0 END) as registered_count', 'SUM(CASE WHEN apt.status = 3 THEN 1 ELSE 0 END) as completed_count', 'SUM(CASE WHEN apt.status = 2 THEN 1 ELSE 0 END) as canceled_count', 'SUM(CASE WHEN apt.status = 4 THEN 1 ELSE 0 END) as expired_count' ]) ->where('apt.appointment_date', '>=', $startDate) ->where('apt.appointment_date', '<=', $endDate) ->whereNotNull('dept.id'); if ($deptId) { $deptStatsQuery->where('dept.id', $deptId); } $deptStats = $deptStatsQuery->group('dept.id')->select()->toArray(); // 组装结果 $result = []; foreach ($deptStats as $stat) { // 计算完成率 $completionRate = $stat['total_count'] > 0 ? round(($stat['completed_count'] / $stat['total_count']) * 100, 2) : 0; $result[] = [ 'dept_id' => $stat['dept_id'], 'dept_name' => $stat['dept_name'], 'total_count' => (int)$stat['total_count'] ?? 0, 'registered_count' => (int)$stat['registered_count'] ?? 0, 'completed_count' => (int)$stat['completed_count'] ?? 0, 'canceled_count' => (int)$stat['canceled_count'] ?? 0, 'expired_count' => (int)$stat['expired_count'] ?? 0, 'completion_rate' => $completionRate, 'time_range' => $timeRangeText ]; } return $result; } /** * @notes 获取时间范围 * @param string $timeType * @param string $customStartDate * @param string $customEndDate * @return array */ private function getTimeRange($timeType, $customStartDate, $customEndDate) { $today = date('Y-m-d'); switch ($timeType) { case 'today': $startDate = $today; $endDate = $today; $timeRangeText = '今天 (' . $today . ')'; break; case 'week': $startDate = date('Y-m-d', strtotime('-6 days')); $endDate = $today; $timeRangeText = '最近7天 (' . $startDate . ' 至 ' . $endDate . ')'; break; case 'month': $startDate = date('Y-m-d', strtotime('-29 days')); $endDate = $today; $timeRangeText = '最近30天 (' . $startDate . ' 至 ' . $endDate . ')'; break; case 'custom': if ($customStartDate && $customEndDate) { $startDate = $customStartDate; $endDate = $customEndDate; $timeRangeText = '自定义 (' . $startDate . ' 至 ' . $endDate . ')'; } else { $startDate = $today; $endDate = $today; $timeRangeText = '今天 (' . $today . ')'; } break; default: $startDate = $today; $endDate = $today; $timeRangeText = '今天 (' . $today . ')'; } return [$startDate, $endDate, $timeRangeText]; } /** * @notes 获取数量 * @return int */ public function count(): int { return count($this->lists()); } }