['prescription_id', 'diagnosis_id', 'fulfillment_status', 'prescription_audit_status', 'payment_slip_audit_status'], '%like%' => ['order_no'], 'between_time' => 'create_time', ]; } /** * 列表/统计条数:含全部搜索条件(含创建时间区间) */ private function buildFilteredQuery(): Query { $query = PrescriptionOrder::where($this->searchWhere)->whereNull('delete_time'); $this->applyPermissionAndExtraFilters($query); return $query; } /** * 今日汇总:与列表相同的「非时间」条件(医生/医助/患者/状态/单号等),不含创建时间区间,避免与「今日」语义重复 * * @return array */ private function searchWhereWithoutCreateTimeRange(): array { return array_values(array_filter( $this->searchWhere, static function ($w): bool { if (!\is_array($w) || \count($w) < 2) { return true; } if (($w[0] ?? '') === 'create_time' && ($w[1] ?? '') === 'between') { return false; } return true; } )); } /** * 重点看板统计:去掉审核/履约状态筛选,保留其它筛选(时间、医生、医助、患者、供货方式等) * * @return array */ private function searchWhereWithoutStatusFilters(): array { return array_values(array_filter( $this->searchWhere, static function ($w): bool { if (!\is_array($w) || \count($w) < 2) { return true; } $field = (string) ($w[0] ?? ''); if (!in_array($field, ['prescription_audit_status', 'payment_slip_audit_status', 'fulfillment_status'], true)) { return true; } $op = strtolower((string) ($w[1] ?? '')); return $op !== '='; } )); } private function applyPermissionAndExtraFilters($query): void { $this->applyDoctorAssistantFilters($query); $this->applyPatientKeywordFilter($query); $this->applyPatientIdFilter($query); $this->applyExpressCompanyFilter($query); $this->applyExpressKeywordFilter($query); $this->applySupplyModeFilter($query); if (!$this->shouldBypassListVisibilityForDiagnosisEdit()) { $this->applyCreatorOrOwnPrescriptionVisibility($query); $this->applyDataScopeForPrescriptionOrder($query); } // 业绩看板侧栏等:不展示履约已取消(4),与 stats 业绩统计口径一致 if ((int) ($this->params['exclude_fulfillment_cancelled'] ?? 0) === 1) { YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($query, ''); } $this->applyYejiErCenterRevisitOrderFilter($query); YejiStatsLogic::applyYejiDrawerChannelFilterToPrescriptionOrderQuery($query, $this->params); } /** * 业绩看板:仅列出二中心复诊口径下的业务订单(须同时传 assistant_id、start_time、end_time;revisit_slot=0 为全部复诊分项合计)。 * admin 维度按订单创建人 o.creator_id(与部门表复诊列、排行榜复诊列同口径)。 */ private function applyYejiErCenterRevisitOrderFilter($query): void { if ((int) ($this->params['yeji_er_center_revisit_only'] ?? 0) !== 1) { return; } $assistantId = (int) ($this->params['assistant_id'] ?? 0); $slot = (int) ($this->params['yeji_er_center_revisit_slot'] ?? 0); if ($assistantId <= 0) { return; } $st = trim((string) ($this->params['start_time'] ?? '')); $et = trim((string) ($this->params['end_time'] ?? '')); if ($st === '' || $et === '') { return; } $t0 = strtotime($st); $t1 = strtotime($et); if ($t0 === false || $t1 === false) { return; } $ids = DeptLogic::listErCenterRevisitOrderIdsForCreator( $assistantId, $slot, (int) $t0, (int) $t1 ); if ($ids === []) { $query->whereRaw('0 = 1'); return; } $query->whereIn('id', $ids); } /** * 按快递公司筛选(点击快捷标签:顺丰 / 京东) * * 说明:主表 express_company 常为 auto/空;快递100 回写承运商在 zyt_express_tracking,需一并匹配。 */ private function applyExpressCompanyFilter($query): void { $company = strtolower(trim((string) ($this->params['express_company'] ?? ''))); if (!in_array($company, ['sf', 'jd'], true)) { return; } $poTbl = (new PrescriptionOrder())->getTable(); $etTbl = (new ExpressTracking())->getTable(); if ($company === 'sf') { $po = "(LOWER(TRIM(IFNULL(`{$poTbl}`.`express_company`,''))) IN ('sf','shunfeng')" . " OR `{$poTbl}`.`express_company` LIKE '%顺丰%'" . " OR (TRIM(IFNULL(`{$poTbl}`.`tracking_number`,'')) <> ''" . " AND (`{$poTbl}`.`tracking_number` LIKE 'SF%' OR `{$poTbl}`.`tracking_number` LIKE 'sf%')))"; $et = "EXISTS (SELECT 1 FROM `{$etTbl}` et WHERE et.`delete_time` IS NULL" . " AND et.`order_id` = `{$poTbl}`.`id` AND et.`order_type` = 'prescription'" . " AND (LOWER(TRIM(IFNULL(et.`express_company`,''))) IN ('sf','shunfeng')" . " OR et.`express_company_name` LIKE '%顺丰%'" . " OR (TRIM(IFNULL(et.`tracking_number`,'')) <> ''" . " AND (et.`tracking_number` LIKE 'SF%' OR et.`tracking_number` LIKE 'sf%'))))"; $query->whereRaw("(($po) OR ($et))"); return; } $po = "(LOWER(TRIM(IFNULL(`{$poTbl}`.`express_company`,''))) IN ('jd','jingdong')" . " OR `{$poTbl}`.`express_company` LIKE '%京东%'" . " OR (TRIM(IFNULL(`{$poTbl}`.`tracking_number`,'')) <> ''" . " AND (`{$poTbl}`.`tracking_number` LIKE 'JD%' OR `{$poTbl}`.`tracking_number` LIKE 'jd%'" . " OR `{$poTbl}`.`tracking_number` LIKE 'JDVB%' OR `{$poTbl}`.`tracking_number` LIKE 'jdvb%')))"; $et = "EXISTS (SELECT 1 FROM `{$etTbl}` et WHERE et.`delete_time` IS NULL" . " AND et.`order_id` = `{$poTbl}`.`id` AND et.`order_type` = 'prescription'" . " AND (LOWER(TRIM(IFNULL(et.`express_company`,''))) IN ('jd','jingdong')" . " OR et.`express_company_name` LIKE '%京东%'" . " OR (TRIM(IFNULL(et.`tracking_number`,'')) <> ''" . " AND (et.`tracking_number` LIKE 'JD%' OR et.`tracking_number` LIKE 'jd%'" . " OR et.`tracking_number` LIKE 'JDVB%' OR et.`tracking_number` LIKE 'jdvb%'))))"; $query->whereRaw("(($po) OR ($et))"); } /** * 供货方式:甘草(已上传甘草药方单号)/ 自营(无甘草单号,走药房直发等) * * 入参 supply_mode:gancao | self,空表示不限 */ private function applySupplyModeFilter($query): void { $mode = strtolower(trim((string) ($this->params['supply_mode'] ?? ''))); if ($mode === '') { return; } if ($mode === 'gancao') { $query->whereRaw("TRIM(IFNULL(`gancao_reciperl_order_no`,'')) <> ''"); return; } if ($mode === 'self') { $query->whereRaw("TRIM(IFNULL(`gancao_reciperl_order_no`,'')) = ''"); return; } } /** * 非「业务订单全量」角色:默认仅本人创建;若持有 viewOrdersForOwnPrescription 则包含关联处方开方人为本人的订单; * 另含「关联诊单医助为本人」以便医助跟进出单与履约。 * 显式筛选 assistant_id 时:若该医助落在当前账号数据域内,则允许按「诊单医助 / 订单创建人」命中(与 applyDoctorAssistantFilters 一致)。 */ private function applyCreatorOrOwnPrescriptionVisibility($query): void { if (PrescriptionOrderLogic::canSeeAllPrescriptionOrders($this->adminInfo)) { return; } $adminId = (int) $this->adminId; $poTbl = (new PrescriptionOrder())->getTable(); $rxTbl = (new Prescription())->getTable(); $diagTbl = (new Diagnosis())->getTable(); $explicitAssistant = (int) ($this->params['assistant_id'] ?? 0); $allowExplicitAssistantVisibility = false; if ($explicitAssistant > 0) { if (!$this->dataScopeShouldApply()) { $allowExplicitAssistantVisibility = true; } else { $visibleIds = $this->getDataScopeVisibleAdminIds(); if ($visibleIds === null || in_array($explicitAssistant, $visibleIds, true)) { $allowExplicitAssistantVisibility = true; } } } $query->where(function ($q) use ( $poTbl, $rxTbl, $diagTbl, $adminId, $explicitAssistant, $allowExplicitAssistantVisibility ) { if (PrescriptionOrderLogic::canViewOrdersForOwnPrescription($this->adminInfo)) { $q->where(function ($qq) use ($poTbl, $rxTbl, $diagTbl, $adminId) { $qq->where('creator_id', $adminId); $qq->whereOrRaw( "EXISTS (SELECT 1 FROM `{$rxTbl}` rx WHERE rx.`id` = `{$poTbl}`.`prescription_id`" . " AND rx.`delete_time` IS NULL AND rx.`creator_id` = {$adminId})" ); $qq->whereOrRaw( "EXISTS (SELECT 1 FROM `{$diagTbl}` dg WHERE dg.`id` = `{$poTbl}`.`diagnosis_id`" . " AND dg.`delete_time` IS NULL AND dg.`assistant_id` = {$adminId})" ); }); } else { $q->where(function ($qq) use ($poTbl, $diagTbl, $adminId) { $qq->where('creator_id', $adminId); $qq->whereOrRaw( "EXISTS (SELECT 1 FROM `{$diagTbl}` dg WHERE dg.`id` = `{$poTbl}`.`diagnosis_id`" . " AND dg.`delete_time` IS NULL AND dg.`assistant_id` = {$adminId})" ); }); } if ($allowExplicitAssistantVisibility) { $q->whereOr('creator_id', $explicitAssistant); $q->whereOrRaw( "EXISTS (SELECT 1 FROM `{$diagTbl}` dg WHERE dg.`id` = `{$poTbl}`.`diagnosis_id`" . " AND dg.`delete_time` IS NULL AND dg.`assistant_id` = {$explicitAssistant})" ); } }); } /** * 数据隔离:创建者 ∈ 可见 admin,或 关联诊单医助 ∈ 可见 admin */ private function applyDataScopeForPrescriptionOrder($query): void { if (!$this->dataScopeShouldApply()) { return; } $ids = $this->getDataScopeVisibleAdminIds(); if ($ids === null) { return; } if ($ids === []) { $query->whereRaw('0 = 1'); return; } $poTbl = (new PrescriptionOrder())->getTable(); $diagTbl = (new Diagnosis())->getTable(); $inList = implode(',', $ids); $query->where(function ($q) use ($poTbl, $diagTbl, $inList) { $q->whereIn('creator_id', explode(',', $inList)); $q->whereOrRaw( "EXISTS (SELECT 1 FROM `{$diagTbl}` dg WHERE dg.`id` = `{$poTbl}`.`diagnosis_id` " . "AND dg.`delete_time` IS NULL AND dg.`assistant_id` IN ({$inList}))" ); }); } /** * 仅诊单编辑页内的患者订单查询可跳过列表页权限,但必须显式带场景且保留患者/诊单范围收敛。 */ private function shouldBypassListVisibilityForDiagnosisEdit(): bool { $scene = trim((string) ($this->params['scene'] ?? '')); if ($scene !== self::SCENE_DIAGNOSIS_EDIT) { return false; } $patientId = (int) ($this->params['patient_id'] ?? 0); $contextDiagnosisId = (int) ($this->params['context_diagnosis_id'] ?? 0); if ($patientId <= 0 || $contextDiagnosisId <= 0) { return false; } $diagnosisPatientId = (int) Diagnosis::where('id', $contextDiagnosisId) ->whereNull('delete_time') ->value('patient_id'); return $diagnosisPatientId > 0 && $diagnosisPatientId === $patientId; } /** * 医助:仅「诊单医助=本人」的订单计业绩 */ private function applyAssistantDiagnosisOnlyFilter($query): void { $poTbl = (new PrescriptionOrder())->getTable(); $diagTbl = (new Diagnosis())->getTable(); $aid = (int) $this->adminId; $query->whereExists( "SELECT 1 FROM `{$diagTbl}` dg WHERE dg.`id` = `{$poTbl}`.`diagnosis_id` AND dg.`delete_time` IS NULL AND dg.`assistant_id` = {$aid}" ); } /** * 统计用查询骨架:不含 create_time 区间;其它筛选项 + 统计专用数据域(不重复 where) * * @param bool $applyYejiDrawerChannel 是否应用业绩侧栏渠道收窄(合计业绩统计须传 false) */ private function buildBaseQueryForStats(bool $applyYejiDrawerChannel = true): Query { $query = PrescriptionOrder::where($this->searchWhereWithoutCreateTimeRange())->whereNull('delete_time'); $this->applyDoctorAssistantFilters($query); $this->applyPatientKeywordFilter($query); if (PrescriptionOrderLogic::canViewOrderListStatsAllScope($this->adminInfo)) { $this->applyDataScopeForPrescriptionOrder($query); } else { $assistantRid = (int) Config::get('project.prescription_order_stats_assistant_role_id', 2); $myRoles = array_map('intval', $this->adminInfo['role_id'] ?? []); if ($assistantRid > 0 && in_array($assistantRid, $myRoles, true)) { $this->applyAssistantDiagnosisOnlyFilter($query); $this->applyDataScopeForPrescriptionOrder($query); } else { if (!PrescriptionOrderLogic::canSeeAllPrescriptionOrders($this->adminInfo)) { $this->applyCreatorOrOwnPrescriptionVisibility($query); } $this->applyDataScopeForPrescriptionOrder($query); } } // 与看板「合计业绩」一致:NULL 安全剔除履约已取消(4),勿用 `<>` 以免误丢 NULL 状态行 if ((int) ($this->params['exclude_fulfillment_cancelled'] ?? 0) === 1) { YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($query, ''); } if ($applyYejiDrawerChannel) { YejiStatsLogic::applyYejiDrawerChannelFilterToPrescriptionOrderQuery($query, $this->params); } return $query; } /** * 统计所对应的时间:与入参 start_time / end_time 一致;未传则按自然日「今天」 * * @return array{0: int, 1: int, 2: string, 3: string} */ private function resolveStatsTimeWindow(): array { $st = trim((string) ($this->params['start_time'] ?? '')); $et = trim((string) ($this->params['end_time'] ?? '')); if ($st !== '' && $et !== '') { $t0 = strtotime($st); $t1 = strtotime($et); if ($t0 !== false && $t1 !== false) { return [(int) $t0, (int) $t1, $st, $et]; } } $t0 = strtotime(date('Y-m-d 00:00:00')); $t1 = strtotime(date('Y-m-d 23:59:59')); if ($t0 === false || $t1 === false) { return [0, 0, '', '']; } return [ (int) $t0, (int) $t1, date('Y-m-d 00:00:00'), date('Y-m-d 23:59:59'), ]; } public function lists(): array { $query = $this->buildFilteredQuery(); $lists = $query ->order('id', 'desc') ->limit($this->limitOffset, $this->limitLength) ->select() ->toArray(); $poIds = array_values(array_filter(array_map('intval', array_column($lists, 'id')))); $paidSumByPo = []; if ($poIds !== []) { $links = PrescriptionOrderPayOrder::whereIn('prescription_order_id', $poIds) ->field(['prescription_order_id', 'pay_order_id']) ->select() ->toArray(); $byPo = []; foreach ($links as $l) { $poid = (int) ($l['prescription_order_id'] ?? 0); $oid = (int) ($l['pay_order_id'] ?? 0); if ($poid > 0 && $oid > 0) { $byPo[$poid][] = $oid; } } $allOids = []; foreach ($byPo as $oids) { $allOids = array_merge($allOids, $oids); } $allOids = array_values(array_unique($allOids)); $amountByOid = []; if ($allOids !== []) { $amountByOid = Order::whereIn('id', $allOids)->whereNull('delete_time')->column('amount', 'id'); } foreach ($poIds as $pid) { $s = 0.0; foreach ($byPo[$pid] ?? [] as $oid) { $s += (float) ($amountByOid[$oid] ?? 0); } $paidSumByPo[$pid] = round($s, 2); } } // 获取创建人姓名 $creatorIds = array_values(array_unique(array_filter(array_map('intval', array_column($lists, 'creator_id'))))); $creatorNames = []; if ($creatorIds !== []) { $creatorNames = \app\common\model\auth\Admin::whereIn('id', $creatorIds)->column('name', 'id'); } // 获取处方ID对应的开方人姓名 $prescriptionIds = array_values(array_unique(array_filter(array_map('intval', array_column($lists, 'prescription_id'))))); $prescriptionCreators = []; if ($prescriptionIds !== []) { $prescriptions = \app\common\model\tcm\Prescription::whereIn('id', $prescriptionIds) ->whereNull('delete_time') ->field(['id', 'creator_id', 'doctor_name', 'phone']) ->select() ->toArray(); $doctorIds = []; foreach ($prescriptions as $rx) { $rxId = (int) ($rx['id'] ?? 0); $doctorId = (int) ($rx['creator_id'] ?? 0); $doctorName = (string) ($rx['doctor_name'] ?? ''); if ($rxId > 0) { $prescriptionCreators[$rxId] = [ 'doctor_id' => $doctorId, 'doctor_name' => $doctorName, 'phone' => (string) ($rx['phone'] ?? ''), ]; if ($doctorId > 0) { $doctorIds[] = $doctorId; } } } // 如果 doctor_name 为空,从 Admin 表获取 $doctorIds = array_values(array_unique($doctorIds)); if ($doctorIds !== []) { $doctorNamesFromAdmin = \app\common\model\auth\Admin::whereIn('id', $doctorIds)->column('name', 'id'); foreach ($prescriptionCreators as &$pc) { if (empty($pc['doctor_name']) && $pc['doctor_id'] > 0) { $pc['doctor_name'] = $doctorNamesFromAdmin[$pc['doctor_id']] ?? ''; } } unset($pc); } } $depMin = PrescriptionOrderLogic::depositMinAmount(); $diagIdsForAssist = array_values(array_unique(array_filter(array_map('intval', array_column($lists, 'diagnosis_id'))))); $assistantByDiag = []; if ($diagIdsForAssist !== []) { $assistantByDiag = Diagnosis::whereIn('id', $diagIdsForAssist)->whereNull('delete_time')->column('assistant_id', 'id'); } $assistantIds = array_values(array_unique(array_filter(array_map('intval', array_values($assistantByDiag))))); $assistantNames = $assistantIds !== [] ? \app\common\model\auth\Admin::whereIn('id', $assistantIds)->column('name', 'id') : []; foreach ($lists as &$item) { PrescriptionOrderLogic::maskInternalCostIfNeeded($item, $this->adminInfo); PrescriptionOrderLogic::maskRemarkExtraIfNeeded($item, $this->adminInfo); $pid = (int) ($item['id'] ?? 0); $item['linked_pay_order_count'] = (int) PrescriptionOrderPayOrder::where( 'prescription_order_id', $pid )->count(); $item['linked_pay_paid_total'] = $paidSumByPo[$pid] ?? 0.0; $item['deposit_min_amount'] = $depMin; $item['can_upload_gancao_reciperl'] = PrescriptionOrderLogic::canUploadGancaoRecipel( $item, $this->adminId, $this->adminInfo, $assistantByDiag ); // 添加创建人姓名 $creatorId = (int) ($item['creator_id'] ?? 0); $item['creator_name'] = $creatorId > 0 ? ($creatorNames[$creatorId] ?? '') : ''; // 添加医助(来自诊单 assistant_id) $diagIdForAssist = (int) ($item['diagnosis_id'] ?? 0); $assistantIdForItem = (int) ($assistantByDiag[$diagIdForAssist] ?? 0); $item['assistant_id'] = $assistantIdForItem; $item['assistant_name'] = $assistantIdForItem > 0 ? ($assistantNames[$assistantIdForItem] ?? '') : ''; // 添加开方人姓名、处方登记手机(与业务订单收货手机比对用) $rxId = (int) ($item['prescription_id'] ?? 0); $item['doctor_name'] = $rxId > 0 ? ($prescriptionCreators[$rxId]['doctor_name'] ?? '') : ''; $item['prescription_phone'] = $rxId > 0 ? ($prescriptionCreators[$rxId]['phone'] ?? '') : ''; } unset($item); if ((int) ($this->params['yeji_order_drawer'] ?? 0) === 1) { PrescriptionOrderLogic::appendLinkedAppointmentsToListRows($lists, $this->params); } return $lists; } public function extend(): array { $s = $this->computeListStatsForExtend(); $focus = $this->computeFocusCountsForExtend(); $base = [ 'deposit_min_amount' => PrescriptionOrderLogic::depositMinAmount(), 'gancao_scm_enabled' => GancaoScmRecipelService::isConfigured(), 'stats_order_amount' => $s['order_amount'], 'stats_order_amount_not_cancelled' => $s['order_amount_not_cancelled'], 'stats_order_amount_cancelled' => $s['order_amount_cancelled'], /** 业绩:业务订单金额,剔除履约已取消(fulfillment_status=4),其余状态全部计入 */ 'stats_order_amount_performance' => $s['order_amount_not_cancelled'], 'stats_linked_pay_amount' => $s['linked_pay_amount'], 'stats_linked_pay_amount_not_cancelled' => $s['linked_pay_amount_not_cancelled'], 'stats_linked_pay_amount_cancelled' => $s['linked_pay_amount_cancelled'], /** 业绩-关联实付:剔除履约已取消订单的关联支付金额 */ 'stats_linked_pay_amount_performance' => $s['linked_pay_amount_not_cancelled'], 'stats_time_start' => $s['label_start'], 'stats_time_end' => $s['label_end'], /** 统计口径:all=支付审核白名单全量;assistant=医助仅诊单协助业绩;restricted=与列表同域 */ 'stats_scope' => $s['scope'], 'focus_pending_rx_count' => $focus['pending_rx'], 'focus_pending_pay_count' => $focus['pending_pay'], 'focus_pending_ship_count' => $focus['pending_ship'], 'focus_risk_count' => $focus['risk'], ]; $yejiConsult = $this->computeYejiDrawerConsultCountIfRequested(); if ($yejiConsult !== null) { $base['stats_yeji_consult_count'] = $yejiConsult; } $yejiDrawerTotal = $this->computeYejiDrawerPerformanceTotalForExtend(); if ($yejiDrawerTotal !== null) { $base['stats_yeji_drawer_total_performance_amount'] = $yejiDrawerTotal['amount']; $base['stats_yeji_drawer_total_performance_order_count'] = $yejiDrawerTotal['order_count']; } return $base; } /** * 业绩看板侧栏专用:约诊/接诊条数,与 YejiStatsLogic::applyConsultFiltersAlignedWithAppointmentLists 同口径 * (doctor_appointment.status=3,appointment_date 落入区间,有效医助 COALESCE(挂号.assistant_id,诊单.assistant_id))。 * 请求须带 yeji_order_drawer=1,且 assistant_id 或 assistant_dept_id 与起止时间有效。 */ private function computeYejiDrawerConsultCountIfRequested(): ?int { if ((int) ($this->params['yeji_order_drawer'] ?? 0) !== 1) { return null; } $assistantId = (int) ($this->params['assistant_id'] ?? 0); $deptRootId = (int) ($this->params['assistant_dept_id'] ?? 0); if ($assistantId <= 0 && $deptRootId <= 0) { return null; } [$startDate, $endDate] = $this->resolveYejiConsultAppointmentDateRange(); if ($startDate === '' || $endDate === '') { return null; } $eff = 'COALESCE(NULLIF(a.assistant_id, 0), NULLIF(u.assistant_id, 0))'; $query = Db::name('doctor_appointment')->alias('a') ->leftJoin('tcm_diagnosis u', 'a.patient_id = u.id') ->where('a.status', 3) ->whereBetween('a.appointment_date', [$startDate, $endDate]) ->whereRaw('(u.id IS NULL OR u.delete_time IS NULL)'); if ($assistantId > 0) { $query->whereRaw("({$eff}) = " . $assistantId); } else { $deptIds = DeptLogic::getSelfAndDescendantIds($deptRootId); $deptIds = array_values(array_filter(array_map('intval', $deptIds), static function (int $id): bool { return $id > 0; })); if ($deptIds === []) { return 0; } $adTbl = (new AdminDept())->getTable(); $inList = implode(',', $deptIds); $query->whereRaw( "EXISTS (SELECT 1 FROM `{$adTbl}` ad WHERE ad.admin_id = {$eff} AND ad.dept_id IN ({$inList}))" ); } return (int) $query->count(); } /** * @return array{0: string, 1: string} Y-m-d */ private function resolveYejiConsultAppointmentDateRange(): array { $st = trim((string) ($this->params['start_time'] ?? '')); $et = trim((string) ($this->params['end_time'] ?? '')); if ($st === '' || $et === '') { return ['', '']; } $t0 = strtotime($st); $t1 = strtotime($et); if ($t0 === false || $t1 === false) { return ['', '']; } if ($t1 < $t0) { $tmp = $t0; $t0 = $t1; $t1 = $tmp; } return [date('Y-m-d', $t0), date('Y-m-d', $t1)]; } /** * 侧栏已选渠道时:「合计业绩」金额与订单数(与看板列同口径,不限渠道收窄) * * @return array{amount: float, order_count: int}|null */ private function computeYejiDrawerPerformanceTotalForExtend(): ?array { if ((int) ($this->params['yeji_order_drawer'] ?? 0) !== 1) { return null; } if (trim((string) ($this->params['channel_code'] ?? '')) === '') { return null; } [$t0, $t1] = $this->resolveStatsTimeWindow(); if ($t0 <= 0 || $t1 <= 0) { return null; } if ($t1 < $t0) { $tmp = $t0; $t0 = $t1; $t1 = $tmp; } $q = $this->buildBaseQueryForStats(false)->whereBetween('create_time', [$t0, $t1]); // 全量「合计业绩」与看板列同口径:始终剔除履约已取消(4),不依赖 exclude 参数 YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($q, ''); $amount = round((float) (clone $q)->sum('amount'), 2); $count = (int) (clone $q)->count(); return ['amount' => $amount, 'order_count' => $count]; } /** * 重点看板计数(基于“当前筛选(去除审核/履约状态)”后的全量命中) * * @return array{pending_rx:int,pending_pay:int,pending_ship:int,risk:int} */ private function computeFocusCountsForExtend(): array { $query = PrescriptionOrder::where($this->searchWhereWithoutStatusFilters())->whereNull('delete_time'); $this->applyPermissionAndExtraFilters($query); $pendingRxQuery = clone $query; YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($pendingRxQuery, ''); return [ // 处方审核待处理(不含履约已取消) 'pending_rx' => (int) $pendingRxQuery->where('prescription_audit_status', 0)->count(), // 支付审核待处理(前提:处方已通过) 'pending_pay' => (int) (clone $query)->where('prescription_audit_status', 1)->where('payment_slip_audit_status', 0)->count(), // 履约待发货 'pending_ship' => (int) (clone $query)->where('fulfillment_status', 2)->count(), // 重点异常:支付审核驳回(与前端「重点异常」快捷筛选口径一致) 'risk' => (int) (clone $query)->where('payment_slip_audit_status', 2)->count(), ]; } /** * 关联实付按业务订单 id 列表汇总 * * @param array $poIds */ private function sumLinkedPayForPrescriptionOrderIds(array $poIds): float { if ($poIds === []) { return 0.0; } $sum = (float) PrescriptionOrderPayOrder::alias('l') ->join('order o', 'l.pay_order_id = o.id') ->whereIn('l.prescription_order_id', $poIds) ->whereNull('o.delete_time') ->sum('o.amount'); return round($sum, 2); } /** * @return array{order_amount: float, order_amount_not_cancelled: float, order_amount_cancelled: float, linked_pay_amount: float, linked_pay_amount_not_cancelled: float, linked_pay_amount_cancelled: float, label_start: string, label_end: string, scope: string} */ private function computeListStatsForExtend(): array { [$t0, $t1, $l0, $l1] = $this->resolveStatsTimeWindow(); if ($t0 <= 0 || $t1 <= 0) { return $this->statsZeroPayload(); } if ($t1 < $t0) { $tmp = $t0; $t0 = $t1; $t1 = $tmp; } $q = $this->buildBaseQueryForStats()->whereBetween('create_time', [$t0, $t1]); $orderTotal = round((float) (clone $q)->sum('amount'), 2); $orderCancelled = round((float) (clone $q)->where('fulfillment_status', 4)->sum('amount'), 2); $orderNotCancelled = round($orderTotal - $orderCancelled, 2); $idsAll = (clone $q)->column('id'); $idsAll = array_values(array_filter(array_map('intval', $idsAll), static function (int $id): bool { return $id > 0; })); if ($idsAll === []) { return $this->statsExtendPayload( $orderTotal, $orderNotCancelled, $orderCancelled, 0.0, 0.0, 0.0, $l0, $l1 ); } $idsCancel = (clone $q)->where('fulfillment_status', 4)->column('id'); $idsCancel = array_values(array_filter(array_map('intval', $idsCancel), static function (int $id): bool { return $id > 0; })); $idsNotCancelled = array_values(array_diff($idsAll, $idsCancel)); $linkedTotal = $this->sumLinkedPayForPrescriptionOrderIds($idsAll); $linkedNotCancelled = $this->sumLinkedPayForPrescriptionOrderIds($idsNotCancelled); $linkedCancelled = $this->sumLinkedPayForPrescriptionOrderIds($idsCancel); return $this->statsExtendPayload( $orderTotal, $orderNotCancelled, $orderCancelled, $linkedTotal, $linkedNotCancelled, $linkedCancelled, $l0, $l1 ); } /** * @return array{order_amount: float, order_amount_not_cancelled: float, order_amount_cancelled: float, linked_pay_amount: float, linked_pay_amount_not_cancelled: float, linked_pay_amount_cancelled: float, label_start: string, label_end: string, scope: string} */ private function statsZeroPayload(): array { return $this->statsExtendPayload(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, '', ''); } /** * @return array{order_amount: float, order_amount_not_cancelled: float, order_amount_cancelled: float, linked_pay_amount: float, linked_pay_amount_not_cancelled: float, linked_pay_amount_cancelled: float, label_start: string, label_end: string, scope: string} */ private function statsExtendPayload( float $orderTotal, float $orderNotCancelled, float $orderCancelled, float $linkedTotal, float $linkedNotCancelled, float $linkedCancelled, string $l0, string $l1 ): array { $scope = 'restricted'; if (PrescriptionOrderLogic::canViewOrderListStatsAllScope($this->adminInfo)) { $scope = 'all'; } else { $ar = (int) Config::get('project.prescription_order_stats_assistant_role_id', 2); if ($ar > 0 && in_array($ar, array_map('intval', $this->adminInfo['role_id'] ?? []), true)) { $scope = 'assistant'; } elseif (PrescriptionOrderLogic::canViewOrdersForOwnPrescription($this->adminInfo)) { $scope = 'own_rx'; } } return [ 'order_amount' => $orderTotal, 'order_amount_not_cancelled' => $orderNotCancelled, 'order_amount_cancelled' => $orderCancelled, 'linked_pay_amount' => $linkedTotal, 'linked_pay_amount_not_cancelled' => $linkedNotCancelled, 'linked_pay_amount_cancelled' => $linkedCancelled, 'label_start' => $l0, 'label_end' => $l1, 'scope' => $scope, ]; } public function count(): int { $query = $this->buildFilteredQuery(); return (int) $query->count(); } /** * 按开方医生(关联处方 creator_id)/ 诊单医助筛选 * * 医助:诊单 assistant_id 与业务订单 creator_id 并举——医助常代建单但诊单未写 assistant_id。 */ private function applyDoctorAssistantFilters($query): void { $poTbl = (new PrescriptionOrder())->getTable(); if (isset($this->params['doctor_id']) && (int) $this->params['doctor_id'] > 0) { $doctorId = (int) $this->params['doctor_id']; $rxTbl = (new Prescription())->getTable(); $query->whereExists("SELECT 1 FROM {$rxTbl} rx WHERE rx.id = {$poTbl}.prescription_id AND rx.delete_time IS NULL AND rx.creator_id = {$doctorId}"); } /** 业绩看板入口(yeji_order_drawer=1)所有筛选统一按订单创建人;其它入口(处方订单列表 / 患者跟进等)保留旧口径(创建人 ∪ 诊单医助) */ $yejiPanel = (int) ($this->params['yeji_order_drawer'] ?? 0) === 1; if (isset($this->params['assistant_id']) && (int) $this->params['assistant_id'] > 0) { $assistantId = (int) $this->params['assistant_id']; if ($yejiPanel) { $query->where('creator_id', $assistantId); } else { $diagTbl = (new Diagnosis())->getTable(); $query->where(function ($q) use ($poTbl, $diagTbl, $assistantId) { $q->where('creator_id', $assistantId); $q->whereOrRaw( "EXISTS (SELECT 1 FROM `{$diagTbl}` dg WHERE dg.`id` = `{$poTbl}`.`diagnosis_id`" . " AND dg.`delete_time` IS NULL AND dg.`assistant_id` = {$assistantId})" ); }); } } if (isset($this->params['assistant_dept_id']) && $this->params['assistant_dept_id'] !== '' && (int) $this->params['assistant_dept_id'] > 0) { if ((int) ($this->params['yeji_drawer_match_table_performance'] ?? 0) === 1) { $rowDeptId = (int) $this->params['assistant_dept_id']; $adminIds = YejiStatsLogic::collectAdminIdsAttributedToYejiTableRow( $rowDeptId, $this->params['dept_ids'] ?? null ); if ($adminIds === []) { $query->whereRaw('0 = 1'); return; } $this->applyPerformanceAttributionAdminInFilter($query, $adminIds); return; } $yejiTableRowDeptIds = []; if (isset($this->params['yeji_table_row_dept_ids']) && trim((string) $this->params['yeji_table_row_dept_ids']) !== '') { foreach (explode(',', (string) $this->params['yeji_table_row_dept_ids']) as $p) { $v = (int) trim((string) $p); if ($v > 0) { $yejiTableRowDeptIds[] = $v; } } } if ((int) ($this->params['yeji_order_drawer'] ?? 0) === 1 && $yejiTableRowDeptIds !== []) { YejiStatsLogic::applyPrescriptionOrderListAlignedWinnerDeptFilter( $query, (int) $this->params['assistant_dept_id'], $yejiTableRowDeptIds, $this->params['dept_ids'] ?? null ); return; } $rootDeptId = (int) $this->params['assistant_dept_id']; $deptIds = DeptLogic::getSelfAndDescendantIds($rootDeptId); $deptIds = array_values(array_filter(array_map('intval', $deptIds), static function (int $id): bool { return $id > 0; })); if ($deptIds === []) { $query->whereRaw('0 = 1'); return; } $inList = implode(',', $deptIds); $adTbl = (new AdminDept())->getTable(); if ($yejiPanel) { /** 业绩看板部门侧栏:仅订单创建人人事部门 ∈ 子树(与「合计业绩」/「接诊诊单」/「复诊」聚合同口径) */ $query->whereExists( "SELECT 1 FROM `{$adTbl}` adc WHERE adc.`admin_id` = `{$poTbl}`.`creator_id` AND adc.`dept_id` IN ({$inList})" ); } else { $diagTbl = (new Diagnosis())->getTable(); $query->where(function ($q) use ($poTbl, $diagTbl, $adTbl, $inList) { $q->whereExists( "SELECT 1 FROM `{$adTbl}` ad INNER JOIN `{$diagTbl}` dg ON ad.admin_id = dg.assistant_id AND dg.delete_time IS NULL " . "WHERE dg.`id` = `{$poTbl}`.`diagnosis_id` AND ad.dept_id IN ({$inList})" ); $q->whereExists( "SELECT 1 FROM `{$adTbl}` adc WHERE adc.`admin_id` = `{$poTbl}`.`creator_id` AND adc.`dept_id` IN ({$inList})", 'OR' ); }); } } } /** * 业绩看板侧栏:只保留「业绩归属医助」落在给定 admin_id 集合的订单 * (与 YejiStatsLogic::sqlPerformanceAttributionAdminExpr 一致——按订单创建人)。 * * @param int[] $adminIds */ private function applyPerformanceAttributionAdminInFilter($query, array $adminIds): void { $adminIds = array_values(array_unique(array_filter(array_map('intval', $adminIds), static function (int $x): bool { return $x > 0; }))); if ($adminIds === []) { $query->whereRaw('0 = 1'); return; } $query->whereIn('creator_id', $adminIds); } /** * 按诊单患者姓名 / 手机号模糊检索(关联 diagnosis_id) */ private function applyPatientKeywordFilter($query): void { $kw = trim((string) ($this->params['patient_keyword'] ?? '')); if ($kw === '') { return; } $poTbl = (new PrescriptionOrder())->getTable(); $diagTbl = (new Diagnosis())->getTable(); $likeInner = str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $kw); $like = '%' . $likeInner . '%'; $likeSql = str_replace("'", "''", $like); $query->whereExists( "SELECT 1 FROM `{$diagTbl}` dg WHERE dg.`id` = `{$poTbl}`.`diagnosis_id` AND dg.`delete_time` IS NULL " . "AND (dg.`patient_name` LIKE '{$likeSql}' OR dg.`phone` LIKE '{$likeSql}')" ); } /** * 按真实患者ID精确过滤(tcm_diagnosis.patient_id 是跨诊单的患者唯一ID) * 用于诊单编辑抽屉「业务订单」tab 列出该患者所有诊单产生的处方业务订单。 */ private function applyPatientIdFilter($query): void { $patientId = (int) ($this->params['patient_id'] ?? 0); if ($patientId <= 0) { return; } $poTbl = (new PrescriptionOrder())->getTable(); $diagTbl = (new Diagnosis())->getTable(); $query->whereExists( "SELECT 1 FROM `{$diagTbl}` dg WHERE dg.`id` = `{$poTbl}`.`diagnosis_id` " . "AND dg.`delete_time` IS NULL AND dg.`patient_id` = {$patientId}" ); } /** * 按快递单号 / 快递公司 模糊检索 */ private function applyExpressKeywordFilter($query): void { $kw = trim((string) ($this->params['express_keyword'] ?? '')); if ($kw === '') { return; } $likeInner = str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $kw); $like = '%' . $likeInner . '%'; $likeSql = str_replace("'", "''", $like); $poTbl = (new PrescriptionOrder())->getTable(); $etTbl = (new ExpressTracking())->getTable(); $query->whereRaw( "(IFNULL(`{$poTbl}`.`tracking_number`,'') LIKE '{$likeSql}'" . " OR IFNULL(`{$poTbl}`.`express_company`,'') LIKE '{$likeSql}'" . " OR EXISTS (SELECT 1 FROM `{$etTbl}` et WHERE et.`delete_time` IS NULL" . " AND et.`order_id` = `{$poTbl}`.`id` AND et.`order_type` = 'prescription'" . " AND (IFNULL(et.`tracking_number`,'') LIKE '{$likeSql}'" . " OR IFNULL(et.`express_company`,'') LIKE '{$likeSql}'" . " OR IFNULL(et.`express_company_name`,'') LIKE '{$likeSql}'))" . ")" ); } }