admin_role.role_id = 1 且管理员 未软删(delete_time 为空);再按账号「数据范围」收窄。 * * - 系统/手动开方:tcm_prescription.prescription_date ∈ [start,end];渠道/标签与业绩渠道列同源 EXISTS / 诊单标签 * - 成交:订单 create_time、排除履约(4),按处方 creator_id;渠道/标签同 sumPerformance 口径 * - 挂号:appointment_date ∈ [start,end];选渠道时挂号 channels 命中字典值;标签渠道时 patient_id ∈ 标签诊单集 */ class DoctorDailyStatsLogic { /** * @param array{ * start_date?:string, * end_date?:string, * channel_code?:string, * tag_id?:string, * doctor_id?:int|string * } $params * * @return array{start_date:string,end_date:string,rows:array,total:array} */ public static function overview(array $params, int $viewerAdminId = 0, array $viewerAdminInfo = []): array { // 医生统计不参与「展示部门」检索:忽略 dept_ids,避免与业绩表部门筛选联动 $paramsForCtx = $params; unset($paramsForCtx['dept_ids']); $ctx = YejiStatsLogic::resolveSharedYejiFilterContext($paramsForCtx, $viewerAdminId, $viewerAdminInfo); $startDate = $ctx['startDate']; $endDate = $ctx['endDate']; $startTs = $ctx['startTs']; $endTs = $ctx['endTs']; $appointmentChannelValues = $ctx['appointmentChannelValues']; $channelFilterActive = $ctx['channelFilterActive']; $tagDiagIds = $ctx['tagDiagIds']; $tagAssistantIds = $ctx['tagAssistantIds']; $tagFallback = $ctx['tagFallback']; $filterDoctorId = (int) ($params['doctor_id'] ?? 0); $doctorIds = self::resolveDoctorAdminIdsForStats($viewerAdminId, $viewerAdminInfo); if ($filterDoctorId > 0) { $doctorIds = in_array($filterDoctorId, $doctorIds, true) ? [$filterDoctorId] : []; } if ($doctorIds === []) { return [ 'start_date' => $startDate, 'end_date' => $endDate, 'rows' => [], 'total' => self::emptyTotals(), ]; } $rxMap = self::loadPrescriptionCounts( $startDate, $endDate, $doctorIds, $appointmentChannelValues, $channelFilterActive, $tagDiagIds, $tagAssistantIds, $tagFallback ); $orderMap = self::loadOrderAggregates( $startTs, $endTs, $doctorIds, $appointmentChannelValues, $channelFilterActive, $tagDiagIds, $tagAssistantIds, $tagFallback ); $apptMap = self::loadAppointmentAggregates( $startDate, $endDate, $doctorIds, $appointmentChannelValues, $channelFilterActive, $tagDiagIds ); $adminRows = Admin::whereIn('id', $doctorIds) ->whereNull('delete_time') ->field(['id', 'name']) ->order('id', 'asc') ->select() ->toArray(); $nameById = []; foreach ($adminRows as $r) { $nameById[(int) $r['id']] = (string) ($r['name'] ?? ''); } $rows = []; foreach ($doctorIds as $aid) { $rx = $rxMap[$aid] ?? ['system' => 0, 'manual' => 0]; $ord = $orderMap[$aid] ?? ['amount' => 0.0, 'count' => 0]; $ap = $apptMap[$aid] ?? ['completed' => 0, 'missed' => 0, 'cancelled' => 0]; $cnt = (int) $ord['count']; $amt = round((float) $ord['amount'], 2); $rows[] = [ 'admin_id' => $aid, 'doctor_name' => $nameById[$aid] ?? ('#' . $aid), 'system_prescription_count' => (int) $rx['system'], 'manual_prescription_count' => (int) $rx['manual'], 'deal_amount' => $amt, 'deal_order_count' => $cnt, 'avg_deal_amount' => $cnt > 0 ? round($amt / $cnt, 2) : null, 'appointment_completed' => (int) $ap['completed'], 'appointment_missed' => (int) $ap['missed'], 'appointment_cancelled' => (int) $ap['cancelled'], ]; } usort($rows, static function (array $a, array $b): int { if (($a['deal_amount'] ?? 0) != ($b['deal_amount'] ?? 0)) { return ($b['deal_amount'] ?? 0) <=> ($a['deal_amount'] ?? 0); } return strcmp((string) ($a['doctor_name'] ?? ''), (string) ($b['doctor_name'] ?? '')); }); return [ 'start_date' => $startDate, 'end_date' => $endDate, 'rows' => $rows, 'total' => self::sumTotals($rows), ]; } /** * 医生角色(role_id=1)、管理员未删除、且在数据范围内的 admin_id。 * * @return int[] */ private static function resolveDoctorAdminIdsForStats(int $viewerAdminId, array $viewerAdminInfo): array { $roleDoctors = Db::name('admin_role')->alias('ar') ->join('admin a', 'a.id = ar.admin_id') ->where('ar.role_id', 1) ->whereNull('a.delete_time') ->column('ar.admin_id'); $doctorIds = array_values(array_unique(array_filter(array_map('intval', $roleDoctors), static function (int $v): bool { return $v > 0; }))); sort($doctorIds); if ($viewerAdminId > 0 && DataScopeService::isEnabled()) { $visibleIds = DataScopeService::getVisibleAdminIds($viewerAdminId, $viewerAdminInfo); if ($visibleIds !== null) { if ($visibleIds === []) { return []; } $flip = array_flip($visibleIds); $doctorIds = array_values(array_filter($doctorIds, static function (int $id) use ($flip): bool { return isset($flip[$id]); })); } } return $doctorIds; } /** * @return array */ private static function emptyTotals(): array { return [ 'system_prescription_count' => 0, 'manual_prescription_count' => 0, 'deal_amount' => 0.0, 'deal_order_count' => 0, 'avg_deal_amount' => null, 'appointment_completed' => 0, 'appointment_missed' => 0, 'appointment_cancelled' => 0, ]; } /** * @param array> $rows * * @return array */ private static function sumTotals(array $rows): array { $t = self::emptyTotals(); foreach ($rows as $r) { $t['system_prescription_count'] += (int) ($r['system_prescription_count'] ?? 0); $t['manual_prescription_count'] += (int) ($r['manual_prescription_count'] ?? 0); $t['deal_amount'] += (float) ($r['deal_amount'] ?? 0); $t['deal_order_count'] += (int) ($r['deal_order_count'] ?? 0); $t['appointment_completed'] += (int) ($r['appointment_completed'] ?? 0); $t['appointment_missed'] += (int) ($r['appointment_missed'] ?? 0); $t['appointment_cancelled'] += (int) ($r['appointment_cancelled'] ?? 0); } $t['deal_amount'] = round((float) $t['deal_amount'], 2); $dc = (int) $t['deal_order_count']; $t['avg_deal_amount'] = $dc > 0 ? round((float) $t['deal_amount'] / $dc, 2) : null; return $t; } /** * @param int[] $appointmentChannelValues * @param int[]|null $tagDiagIds * @param array|null $tagAssistantIds * * @return array */ private static function loadPrescriptionCounts( string $startDate, string $endDate, array $doctorIds, array $appointmentChannelValues, bool $channelFilterActive, ?array $tagDiagIds, ?array $tagAssistantIds, bool $tagFallback ): array { if (!self::tagScopeNonEmpty($tagDiagIds, $tagAssistantIds)) { return []; } $query = Db::name('tcm_prescription') ->alias('rx') ->whereNull('rx.delete_time') ->whereRaw('IFNULL(rx.void_status, 0) <> 1') ->whereBetween('rx.prescription_date', [$startDate, $endDate]) ->whereIn('rx.creator_id', $doctorIds) ->where('rx.diagnosis_id', '>', 0); self::applyPrescriptionChannelTagFilter( $query, 'rx', $appointmentChannelValues, $channelFilterActive, $tagDiagIds, $tagAssistantIds, $tagFallback ); $query->field([ 'rx.creator_id', Db::raw('SUM(CASE WHEN IFNULL(rx.is_system_auto, 0) = 1 THEN 1 ELSE 0 END) AS system_cnt'), Db::raw('SUM(CASE WHEN IFNULL(rx.is_system_auto, 0) <> 1 THEN 1 ELSE 0 END) AS manual_cnt'), ])->group('rx.creator_id'); $out = []; foreach ($query->select()->toArray() as $r) { $id = (int) ($r['creator_id'] ?? 0); if ($id <= 0) { continue; } $out[$id] = [ 'system' => (int) ($r['system_cnt'] ?? 0), 'manual' => (int) ($r['manual_cnt'] ?? 0), ]; } return $out; } /** * @param int[] $appointmentChannelValues * @param int[]|null $tagDiagIds * @param array|null $tagAssistantIds * * @return array */ private static function loadOrderAggregates( int $startTs, int $endTs, array $doctorIds, array $appointmentChannelValues, bool $channelFilterActive, ?array $tagDiagIds, ?array $tagAssistantIds, bool $tagFallback ): array { if (!self::tagScopeNonEmpty($tagDiagIds, $tagAssistantIds)) { return []; } $q = Db::name('tcm_prescription_order') ->alias('o') ->join('tcm_prescription rx', 'rx.id = o.prescription_id AND rx.delete_time IS NULL', 'INNER') ->whereNull('o.delete_time') ->whereBetween('o.create_time', [$startTs, $endTs]) ->whereRaw('NOT (o.fulfillment_status <=> 4)') ->whereIn('rx.creator_id', $doctorIds) ->where('o.diagnosis_id', '>', 0); $normCh = self::normalizeAppointmentChannelInts($appointmentChannelValues); if ($normCh !== []) { $apTable = self::tableWithPrefix('doctor_appointment'); $adminRoleTable = self::tableWithPrefix('admin_role'); $ph = implode(',', array_fill(0, count($normCh), '?')); $strVals = array_values(array_unique(array_map(static fn (int $v): string => (string) $v, $normCh))); $channelCond = "ap.channels IN ({$ph})"; $existsSql = "EXISTS (SELECT 1 FROM {$apTable} ap INNER JOIN {$adminRoleTable} ar " . "ON ar.admin_id = ap.assistant_id AND ar.role_id = 2 WHERE ap.patient_id = o.diagnosis_id " . "AND ap.status = 3 AND {$channelCond})"; $q->whereRaw($existsSql, $strVals); } elseif ($channelFilterActive) { self::applyOrderTagFilter($q, 'o', 'rx', $tagDiagIds, $tagAssistantIds, $tagFallback); } $q->field([ 'rx.creator_id', Db::raw('SUM(o.amount) AS amount_sum'), Db::raw('COUNT(*) AS order_cnt'), ])->group('rx.creator_id'); $out = []; foreach ($q->select()->toArray() as $r) { $id = (int) ($r['creator_id'] ?? 0); if ($id <= 0) { continue; } $out[$id] = [ 'amount' => (float) ($r['amount_sum'] ?? 0), 'count' => (int) ($r['order_cnt'] ?? 0), ]; } return $out; } /** * 订单标签条件(无字典渠道映射时,与业绩 tag 分支一致;开方人维度用 rx.creator_id 兜底)。 * * @param \think\db\Query $q */ private static function applyOrderTagFilter( $q, string $orderAlias, string $rxAlias, ?array $tagDiagIds, ?array $tagAssistantIds, bool $tagFallback ): void { if ($tagDiagIds !== null) { $q->whereIn("{$orderAlias}.diagnosis_id", $tagDiagIds); } if ($tagAssistantIds !== null) { $ids = array_keys($tagAssistantIds); if ($tagFallback) { $q->whereIn("{$rxAlias}.creator_id", array_map('intval', $ids)); } else { $q->whereIn("{$orderAlias}.creator_id", array_map('intval', $ids)); } } } /** * @param \think\db\Query $query rx 别名查询 * @param string $rxAlias * @param int[] $appointmentChannelValues * @param int[]|null $tagDiagIds * @param array|null $tagAssistantIds */ private static function applyPrescriptionChannelTagFilter( $query, string $rxAlias, array $appointmentChannelValues, bool $channelFilterActive, ?array $tagDiagIds, ?array $tagAssistantIds, bool $tagFallback ): void { $normCh = self::normalizeAppointmentChannelInts($appointmentChannelValues); if ($normCh !== []) { $apTable = self::tableWithPrefix('doctor_appointment'); $adminRoleTable = self::tableWithPrefix('admin_role'); $ph = implode(',', array_fill(0, count($normCh), '?')); $strVals = array_values(array_unique(array_map(static fn (int $v): string => (string) $v, $normCh))); $channelCond = "ap.channels IN ({$ph})"; $existsSql = "EXISTS (SELECT 1 FROM {$apTable} ap INNER JOIN {$adminRoleTable} ar " . "ON ar.admin_id = ap.assistant_id AND ar.role_id = 2 WHERE ap.patient_id = {$rxAlias}.diagnosis_id " . "AND ap.status = 3 AND {$channelCond})"; $query->whereRaw($existsSql, $strVals); } elseif ($channelFilterActive) { if ($tagDiagIds !== null) { $query->whereIn("{$rxAlias}.diagnosis_id", $tagDiagIds); } if ($tagAssistantIds !== null && $tagFallback) { $query->whereIn("{$rxAlias}.creator_id", array_map('intval', array_keys($tagAssistantIds))); } } } /** * 标签范围显式为空(0 条诊单/医助)时整段统计不再查询。 */ private static function tagScopeNonEmpty(?array $tagDiagIds, ?array $tagAssistantIds): bool { if ($tagDiagIds !== null && $tagDiagIds === []) { return false; } if ($tagAssistantIds !== null && $tagAssistantIds === []) { return false; } return true; } /** * @param int[] $appointmentChannelValues * @param int[]|null $tagDiagIds * * @return array */ private static function loadAppointmentAggregates( string $startDate, string $endDate, array $doctorIds, array $appointmentChannelValues, bool $channelFilterActive, ?array $tagDiagIds ): array { $q = Db::name('doctor_appointment') ->whereBetween('appointment_date', [$startDate, $endDate]) ->whereIn('doctor_id', $doctorIds); $normCh = self::normalizeAppointmentChannelInts($appointmentChannelValues); if ($normCh !== []) { $strVals = array_values(array_unique(array_map(static fn (int $v): string => (string) $v, $normCh))); $q->whereIn('channels', $strVals); } elseif ($channelFilterActive && $tagDiagIds !== null) { if ($tagDiagIds === []) { return []; } $q->whereIn('patient_id', $tagDiagIds); } $q->field([ 'doctor_id', Db::raw('SUM(CASE WHEN status = 3 THEN 1 ELSE 0 END) AS completed'), Db::raw('SUM(CASE WHEN status = 4 THEN 1 ELSE 0 END) AS missed'), Db::raw('SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) AS cancelled'), ])->group('doctor_id'); $out = []; foreach ($q->select()->toArray() as $r) { $id = (int) ($r['doctor_id'] ?? 0); if ($id <= 0) { continue; } $out[$id] = [ 'completed' => (int) ($r['completed'] ?? 0), 'missed' => (int) ($r['missed'] ?? 0), 'cancelled' => (int) ($r['cancelled'] ?? 0), ]; } return $out; } /** * @return int[] */ private static function normalizeAppointmentChannelInts(array $raw): array { $out = []; foreach ($raw as $v) { $i = (int) $v; if ($i > 0) { $out[] = $i; } } return array_values(array_unique($out)); } private static function tableWithPrefix(string $table): string { $prefix = (string) (Db::getConfig('connections.mysql.prefix') ?: 'zyt_'); return $prefix . $table; } }