append(['status_desc']) ->order(['sort' => 'desc', 'id' => 'desc']) ->select() ->toArray(); $totalsWithSubtree = self::deptAdminCountTotalsWithSubtree(); foreach ($lists as &$row) { $row['admin_count'] = $totalsWithSubtree[(int) $row['id']] ?? 0; } unset($row); $pid = 0; if (!empty($lists)) { $pid = min(array_column($lists, 'pid')); } $tree = self::getTree($lists, $pid); return $tree; } /** * 每个部门人数(直属 + 所有下级部门),基于全表部门树汇总;管理员来自 admin_dept,排除已删除账号 * * @return array dept_id => count */ private static function deptAdminCountTotalsWithSubtree(): array { $treeRows = Dept::field(['id', 'pid'])->select()->toArray(); if ($treeRows === []) { return []; } $allIds = array_map('intval', array_column($treeRows, 'id')); $directMap = self::fetchDirectAdminCountMap($allIds); $childrenByPid = []; foreach ($treeRows as $r) { $pid = (int) $r['pid']; $id = (int) $r['id']; if (!isset($childrenByPid[$pid])) { $childrenByPid[$pid] = []; } $childrenByPid[$pid][] = $id; } $memo = []; $dfs = function (int $id) use (&$dfs, $childrenByPid, $directMap, &$memo): int { if (array_key_exists($id, $memo)) { return $memo[$id]; } $sum = $directMap[$id] ?? 0; foreach ($childrenByPid[$id] ?? [] as $cid) { $sum += $dfs((int) $cid); } $memo[$id] = $sum; return $sum; }; $out = []; foreach ($allIds as $id) { $out[$id] = $dfs($id); } return $out; } /** * 各部门直属管理员人数 * * @param array $deptIds * @return array */ private static function fetchDirectAdminCountMap(array $deptIds): array { if ($deptIds === []) { return []; } $adminTable = (new Admin())->getTable(); $rows = AdminDept::alias('ad') ->join($adminTable . ' a', 'a.id = ad.admin_id') ->whereNull('a.delete_time') ->whereIn('ad.dept_id', $deptIds) ->field('ad.dept_id, COUNT(*) AS admin_count') ->group('ad.dept_id') ->select() ->toArray(); $map = []; foreach ($rows as $r) { $map[(int) $r['dept_id']] = (int) $r['admin_count']; } return $map; } /** * 名称含「二中心」的部门 id(与业绩看板二中心规则一致)。 * * @return list */ private static function findErCenterRootDeptIds(): array { $rows = Dept::whereNull('delete_time') ->field(['id', 'name']) ->select() ->toArray(); $out = []; foreach ($rows as $r) { $name = (string) ($r['name'] ?? ''); if ($name !== '' && mb_strpos($name, '二中心') !== false) { $out[] = (int) $r['id']; } } return array_values(array_unique($out)); } /** * @param list $rootIds * * @return list */ private static function unionErCenterSubtreeDeptIds(array $rootIds): array { $set = []; foreach ($rootIds as $rid) { $rid = (int) $rid; if ($rid <= 0) { continue; } foreach (self::getSelfAndDescendantIds($rid) as $id) { $set[(int) $id] = true; } } return array_keys($set); } /** * 名称含「二中心」的部门及其全部下级 id(map),与业绩看板判定一致。 * * @return array */ public static function getErCenterSubtreeDeptIdSet(): array { $erRoots = self::findErCenterRootDeptIds(); $subtreeIds = self::unionErCenterSubtreeDeptIds($erRoots); return array_fill_keys($subtreeIds, true); } /** * 二中心复诊统计用的业务订单行(与 rollup 同源 SQL)。 * * @return list */ private static function fetchErCenterRevisitCandidateOrders(?int $orderStartTs = null, ?int $orderEndTs = null): array { $q = Db::name('tcm_prescription_order') ->alias('o') ->join('tcm_diagnosis dg', 'dg.id = o.diagnosis_id AND dg.delete_time IS NULL', 'INNER') ->whereNull('o.delete_time') ->where('o.diagnosis_id', '>', 0) ->where('dg.assistant_id', '>', 0) ->whereRaw('NOT (o.fulfillment_status <=> ?)', [4]); if ($orderStartTs !== null && $orderEndTs !== null) { $q->where('o.create_time', 'between', [$orderStartTs, $orderEndTs]); } return $q ->field(['o.diagnosis_id', 'o.create_time', 'o.id', 'dg.assistant_id']) ->order(['o.diagnosis_id' => 'asc', 'o.create_time' => 'asc', 'o.id' => 'asc']) ->select() ->toArray(); } /** * 二中心子树:各部门复诊 rollup;同时返回按诊单医助聚合的分项(供医助排行榜)。 * * @param list $subtreeDeptIds * * @return array{ * totals: array, * slots: array>, * by_assistant_slots: array> * } */ private static function computeErCenterRevisitFullPack( array $subtreeDeptIds, ?int $orderStartTs = null, ?int $orderEndTs = null ): array { $emptySlots = []; $emptyTotals = []; foreach ($subtreeDeptIds as $did) { $did = (int) $did; $emptySlots[$did] = []; $emptyTotals[$did] = 0; } if ($subtreeDeptIds === []) { return ['totals' => [], 'slots' => [], 'by_assistant_slots' => []]; } $subtreeSet = array_fill_keys($subtreeDeptIds, true); $orderRows = self::fetchErCenterRevisitCandidateOrders($orderStartTs, $orderEndTs); if ($orderRows === []) { return ['totals' => $emptyTotals, 'slots' => $emptySlots, 'by_assistant_slots' => []]; } $byPatient = []; foreach ($orderRows as $r) { $pid = (int) ($r['diagnosis_id'] ?? 0); if ($pid <= 0) { continue; } $byPatient[$pid][] = $r; } $assistantNeed = []; foreach ($byPatient as $orders) { $n = \count($orders); for ($i = 1; $i < $n; $i++) { $aid = (int) ($orders[$i]['assistant_id'] ?? 0); if ($aid > 0) { $assistantNeed[$aid] = true; } } } $assistantIds = array_keys($assistantNeed); $canonicalDept = self::buildAssistantCanonicalDeptInSubtree($assistantIds, $subtreeSet); /** @var array> $leafByDeptSlot dept_id => [ slot => cnt ],slot≥2 */ $leafByDeptSlot = []; /** @var array> $byAssistantSlot */ $byAssistantSlot = []; foreach ($byPatient as $orders) { $n = \count($orders); for ($i = 1; $i < $n; $i++) { $slot = $i + 1; $aid = (int) ($orders[$i]['assistant_id'] ?? 0); $deptId = $canonicalDept[$aid] ?? null; if ($deptId === null || !isset($subtreeSet[$deptId])) { continue; } if (!isset($leafByDeptSlot[$deptId])) { $leafByDeptSlot[$deptId] = []; } $leafByDeptSlot[$deptId][$slot] = ($leafByDeptSlot[$deptId][$slot] ?? 0) + 1; if ($aid > 0) { if (!isset($byAssistantSlot[$aid])) { $byAssistantSlot[$aid] = []; } $byAssistantSlot[$aid][$slot] = ($byAssistantSlot[$aid][$slot] ?? 0) + 1; } } } $childrenByPid = self::buildAllDeptChildrenByPid(); $rollupSlots = self::rollupRevisitSlotsBySubtree($subtreeDeptIds, $subtreeSet, $childrenByPid, $leafByDeptSlot); $rollupTotals = []; foreach ($subtreeDeptIds as $id) { $id = (int) $id; $rollupTotals[$id] = array_sum($rollupSlots[$id] ?? []); } foreach ($byAssistantSlot as $aid => $sm) { if ($sm !== []) { ksort($byAssistantSlot[$aid], SORT_NUMERIC); } } return [ 'totals' => $rollupTotals, 'slots' => $rollupSlots, 'by_assistant_slots' => $byAssistantSlot, ]; } /** * 二中心子树:各部门复诊合计与分项(第 2 笔订单=复诊2…),供业绩看板等复用。 * $orderStartTs/$orderEndTs 均非 null 时仅统计该 create_time 窗口内业务单(与看板区间一致);均为 null 时不限时间(慎用,数据量大)。 * * @return array{totals: array, slots: array>} */ public static function getErCenterRevisitRollupIndexedByDept(?int $orderStartTs = null, ?int $orderEndTs = null): array { $erRoots = self::findErCenterRootDeptIds(); $subtreeIds = self::unionErCenterSubtreeDeptIds($erRoots); if ($subtreeIds === []) { return ['totals' => [], 'slots' => []]; } $pack = self::computeErCenterRevisitFullPack($subtreeIds, $orderStartTs, $orderEndTs); return ['totals' => $pack['totals'], 'slots' => $pack['slots']]; } /** * 二中心子树内:各医助成交的复诊业务单笔数(合计 + 复诊2/3/…分项),口径与部门 rollup 同源。 * * @return array{totals: array, slots: array>} */ public static function getErCenterRevisitCountsByAssistant(?int $orderStartTs = null, ?int $orderEndTs = null): array { $erRoots = self::findErCenterRootDeptIds(); $subtreeIds = self::unionErCenterSubtreeDeptIds($erRoots); if ($subtreeIds === []) { return ['totals' => [], 'slots' => []]; } $pack = self::computeErCenterRevisitFullPack($subtreeIds, $orderStartTs, $orderEndTs); $by = $pack['by_assistant_slots']; $totals = []; $slots = []; foreach ($by as $aid => $slotMap) { $aid = (int) $aid; $totals[$aid] = (int) array_sum($slotMap); $slots[$aid] = $slotMap; } return ['totals' => $totals, 'slots' => $slots]; } /** * 某医助在区间内、计入二中心复诊的业务订单 id(用于看板侧栏按复诊笔数下钻)。 * $revisitSlot = 0 表示累计全部复诊(第 2 笔起);≥2 时仅该分项。 * * @return list */ public static function listErCenterRevisitOrderIdsForAssistant( int $assistantId, int $revisitSlot, int $orderStartTs, int $orderEndTs ): array { if ($assistantId <= 0 || $orderStartTs <= 0 || $orderEndTs < $orderStartTs) { return []; } if ($revisitSlot < 0 || $revisitSlot === 1) { return []; } $erRoots = self::findErCenterRootDeptIds(); $subtreeIds = self::unionErCenterSubtreeDeptIds($erRoots); if ($subtreeIds === []) { return []; } $subtreeSet = array_fill_keys($subtreeIds, true); $orderRows = self::fetchErCenterRevisitCandidateOrders($orderStartTs, $orderEndTs); if ($orderRows === []) { return []; } $byPatient = []; foreach ($orderRows as $r) { $pid = (int) ($r['diagnosis_id'] ?? 0); if ($pid <= 0) { continue; } $byPatient[$pid][] = $r; } $assistantNeed = []; foreach ($byPatient as $orders) { $n = \count($orders); for ($i = 1; $i < $n; $i++) { $aid = (int) ($orders[$i]['assistant_id'] ?? 0); if ($aid > 0) { $assistantNeed[$aid] = true; } } } $canonicalDept = self::buildAssistantCanonicalDeptInSubtree(array_keys($assistantNeed), $subtreeSet); $ids = []; foreach ($byPatient as $orders) { $n = \count($orders); for ($i = 1; $i < $n; $i++) { $slot = $i + 1; $aid = (int) ($orders[$i]['assistant_id'] ?? 0); if ($aid !== $assistantId) { continue; } if ($revisitSlot > 0 && $slot !== $revisitSlot) { continue; } $dcan = $canonicalDept[$aid] ?? null; if ($dcan === null || !isset($subtreeSet[$dcan])) { continue; } $oid = (int) ($orders[$i]['id'] ?? 0); if ($oid > 0) { $ids[$oid] = true; } } } return array_keys($ids); } /** * 某展示部门行(组织架构上含下级 rollup)内,二中心复诊按诊单医助拆解笔数。 * 与看板行内「复诊」列口径一致:诊单下订单序列第 2 笔起;医助 canonical 部门须落在二中心子树且在该部门行的子树内。 * * @return array{rows: list} */ public static function getErCenterRevisitAssistantBreakdownForDept( int $rootDeptId, int $revisitSlot, int $orderStartTs, int $orderEndTs ): array { if ($rootDeptId <= 0 || $orderStartTs <= 0 || $orderEndTs < $orderStartTs) { return ['rows' => []]; } if ($revisitSlot < 0 || $revisitSlot === 1) { return ['rows' => []]; } $erSubtreeSet = self::getErCenterSubtreeDeptIdSet(); if ($erSubtreeSet === []) { return ['rows' => []]; } $descIds = self::getSelfAndDescendantIds($rootDeptId); $descFlip = []; foreach ($descIds as $did) { $did = (int) $did; if ($did > 0) { $descFlip[$did] = true; } } $orderRows = self::fetchErCenterRevisitCandidateOrders($orderStartTs, $orderEndTs); if ($orderRows === []) { return ['rows' => []]; } $byPatient = []; foreach ($orderRows as $r) { $pid = (int) ($r['diagnosis_id'] ?? 0); if ($pid <= 0) { continue; } $byPatient[$pid][] = $r; } $assistantNeed = []; foreach ($byPatient as $orders) { $n = \count($orders); for ($i = 1; $i < $n; $i++) { $aid = (int) ($orders[$i]['assistant_id'] ?? 0); if ($aid > 0) { $assistantNeed[$aid] = true; } } } $canonicalDept = self::buildAssistantCanonicalDeptInSubtree(array_keys($assistantNeed), $erSubtreeSet); /** @var array $counts */ $counts = []; foreach ($byPatient as $orders) { $n = \count($orders); for ($i = 1; $i < $n; $i++) { $slot = $i + 1; if ($revisitSlot > 0 && $slot !== $revisitSlot) { continue; } $aid = (int) ($orders[$i]['assistant_id'] ?? 0); if ($aid <= 0) { continue; } $canon = $canonicalDept[$aid] ?? null; if ($canon === null || !isset($erSubtreeSet[$canon]) || !isset($descFlip[$canon])) { continue; } $counts[$aid] = ($counts[$aid] ?? 0) + 1; } } if ($counts === []) { return ['rows' => []]; } $ids = array_keys($counts); $nameRows = []; if ($ids !== []) { $nameRows = Db::name('admin') ->whereIn('id', $ids) ->whereNull('delete_time') ->column('name', 'id'); } $rows = []; foreach ($counts as $aid => $cnt) { $rows[] = [ 'admin_id' => (int) $aid, 'name' => (string) ($nameRows[$aid] ?? ('#' . $aid)), 'order_count' => (int) $cnt, ]; } usort($rows, static function (array $a, array $b): int { if ($a['order_count'] !== $b['order_count']) { return $b['order_count'] <=> $a['order_count']; } return strcmp((string) $a['name'], (string) $b['name']); }); return ['rows' => $rows]; } /** * @return array> */ private static function buildAllDeptChildrenByPid(): array { $rows = Dept::field(['id', 'pid'])->select()->toArray(); $out = []; foreach ($rows as $r) { $pid = (int) ($r['pid'] ?? 0); $id = (int) ($r['id'] ?? 0); if ($id <= 0) { continue; } if (!isset($out[$pid])) { $out[$pid] = []; } $out[$pid][] = $id; } return $out; } /** * @param array> $leafByDeptSlot * * @return array> dept_id => [ slot => count ],已含下级汇总 */ private static function rollupRevisitSlotsBySubtree( array $subtreeDeptIds, array $subtreeSet, array $childrenByPid, array $leafByDeptSlot ): array { $memo = []; $dfs = static function (int $id) use (&$dfs, $childrenByPid, $leafByDeptSlot, $subtreeSet, &$memo): array { if (isset($memo[$id])) { return $memo[$id]; } $acc = []; if (isset($leafByDeptSlot[$id])) { foreach ($leafByDeptSlot[$id] as $slot => $cnt) { $acc[(int) $slot] = (int) $cnt; } } foreach ($childrenByPid[$id] ?? [] as $cid) { $cid = (int) $cid; if (!isset($subtreeSet[$cid])) { continue; } $childAcc = $dfs($cid); foreach ($childAcc as $slot => $cnt) { $acc[$slot] = ($acc[$slot] ?? 0) + $cnt; } } if ($acc !== []) { ksort($acc, SORT_NUMERIC); } $memo[$id] = $acc; return $acc; }; $out = []; foreach ($subtreeDeptIds as $id) { $id = (int) $id; $out[$id] = $dfs($id); } return $out; } /** * @param list $assistantIds * @param array $subtreeSet * * @return array admin_id => dept_id */ private static function buildAssistantCanonicalDeptInSubtree(array $assistantIds, array $subtreeSet): array { $ids = array_values(array_unique(array_filter(array_map('intval', $assistantIds), static function (int $v): bool { return $v > 0; }))); if ($ids === []) { return []; } $subtreeIdList = array_keys($subtreeSet); $rows = AdminDept::whereIn('admin_id', $ids) ->whereIn('dept_id', $subtreeIdList) ->field(['admin_id', 'dept_id']) ->select() ->toArray(); $candidates = []; foreach ($rows as $r) { $a = (int) ($r['admin_id'] ?? 0); $d = (int) ($r['dept_id'] ?? 0); if ($a > 0 && $d > 0) { $candidates[$a][] = $d; } } $depthMap = self::buildDeptIdDepthMap(); $out = []; foreach ($candidates as $aid => $depts) { $best = null; $bestDepth = -1; foreach ($depts as $d) { $depth = (int) ($depthMap[$d] ?? 0); if ( $best === null || $depth > $bestDepth || ($depth === $bestDepth && $d < (int) $best) ) { $bestDepth = $depth; $best = $d; } } if ($best !== null) { $out[$aid] = (int) $best; } } return $out; } /** * @return array dept_id => 从根层深度 0 起的层级 */ private static function buildDeptIdDepthMap(): array { $rows = Dept::field(['id', 'pid'])->select()->toArray(); $pidById = []; foreach ($rows as $r) { $pidById[(int) $r['id']] = (int) $r['pid']; } $depth = []; $getDepth = static function (int $id) use (&$getDepth, &$depth, $pidById): int { if (isset($depth[$id])) { return $depth[$id]; } $p = (int) ($pidById[$id] ?? 0); $depth[$id] = $p > 0 ? $getDepth($p) + 1 : 0; return $depth[$id]; }; foreach (array_keys($pidById) as $id) { $getDepth((int) $id); } return $depth; } /** * @notes 列表树状结构 * @param $array * @param int $pid * @param int $level * @return array * @author 段誉 * @date 2022/5/30 15:44 */ public static function getTree($array, $pid = 0, $level = 0) { $list = []; foreach ($array as $key => $item) { if ($item['pid'] == $pid) { $item['level'] = $level; $item['children'] = self::getTree($array, $item['id'], $level + 1); $list[] = $item; } } return $list; } /** * @notes 上级部门 * @return array * @throws \think\db\exception\DataNotFoundException * @throws \think\db\exception\DbException * @throws \think\db\exception\ModelNotFoundException * @author 段誉 * @date 2022/5/26 18:36 */ public static function leaderDept() { $lists = Dept::field(['id', 'name'])->where(['status' => 1]) ->order(['sort' => 'desc', 'id' => 'desc']) ->select() ->toArray(); return $lists; } /** * @notes 添加部门 * @param array $params * @author 段誉 * @date 2022/5/25 18:20 */ public static function add(array $params) { Dept::create([ 'pid' => $params['pid'], 'name' => $params['name'], 'leader' => $params['leader'] ?? '', 'mobile' => $params['mobile'] ?? '', 'status' => $params['status'], 'sort' => $params['sort'] ?? 0 ]); } /** * @notes 编辑部门 * @param array $params * @return bool * @author 段誉 * @date 2022/5/25 18:39 */ public static function edit(array $params): bool { try { $pid = $params['pid']; $oldDeptData = Dept::findOrEmpty($params['id']); if ($oldDeptData['pid'] == 0) { $pid = 0; } Dept::update([ 'id' => $params['id'], 'pid' => $pid, 'name' => $params['name'], 'leader' => $params['leader'] ?? '', 'mobile' => $params['mobile'] ?? '', 'status' => $params['status'], 'sort' => $params['sort'] ?? 0 ]); return true; } catch (\Exception $e) { self::setError($e->getMessage()); return false; } } /** * @notes 删除部门 * @param array $params * @author 段誉 * @date 2022/5/25 18:40 */ public static function delete(array $params) { Dept::destroy($params['id']); } /** * @notes 获取部门详情 * @param $params * @return array * @author 段誉 * @date 2022/5/25 18:40 */ public static function detail($params): array { return Dept::findOrEmpty($params['id'])->toArray(); } /** * @notes 部门数据 * @return array * @throws \think\db\exception\DataNotFoundException * @throws \think\db\exception\DbException * @throws \think\db\exception\ModelNotFoundException * @author 段誉 * @date 2022/10/13 10:19 */ public static function getAllData() { // 与业绩看板等统计口径一致:含全部未软删部门(不再仅限 status=启用), // 避免外链 assistant_dept_id 在树下拉中不存在导致 TreeSelect 初始化异常。 $data = Dept::order(['sort' => 'desc', 'id' => 'desc']) ->select() ->toArray(); if ($data === []) { return []; } $pid = min(array_column($data, 'pid')); return self::getTree($data, $pid); } /** * 部门树(与 getAllData 同结构),按当前管理员角色数据权限收窄可选节点; * 保留必选祖先节点以便树形展示(与业绩看板 deptOptions 的 allowed 集合一致)。 * * @param array $adminInfo BaseAdminController::$adminInfo 形态(须含 root、role_id 等) * * @return array> */ public static function getAllDataScoped(int $adminId, array $adminInfo): array { $full = self::getAllData(); if ($full === []) { return []; } if ($adminId <= 0 || !DataScopeService::isEnabled()) { return $full; } $allowed = DataScopeService::getAllowedDeptIdSet($adminId, $adminInfo); if ($allowed === null) { return $full; } if ($allowed === []) { return []; } return self::filterDeptTreeByAllowedIds($full, $allowed); } /** * @param array> $nodes * @param array $allowedIdMap * * @return array> */ private static function filterDeptTreeByAllowedIds(array $nodes, array $allowedIdMap): array { $out = []; foreach ($nodes as $node) { $id = (int) ($node['id'] ?? 0); $rawChildren = $node['children'] ?? []; $children = \is_array($rawChildren) && $rawChildren !== [] ? self::filterDeptTreeByAllowedIds($rawChildren, $allowedIdMap) : []; $inAllowed = isset($allowedIdMap[$id]); if ($inAllowed || $children !== []) { $row = $node; $row['children'] = $children; $out[] = $row; } } return $out; } /** * 指定部门及其全部下级部门 id(含自身)。筛选时选父级可匹配子级下成员(admin_dept.dept_id)。 * * @return array */ public static function getSelfAndDescendantIds(int $rootDeptId): array { if ($rootDeptId <= 0) { return []; } $rows = Dept::field(['id', 'pid'])->select()->toArray(); if ($rows === []) { return [$rootDeptId]; } $validIds = []; foreach ($rows as $r) { $id = (int) ($r['id'] ?? 0); if ($id > 0) { $validIds[$id] = true; } } if (!isset($validIds[$rootDeptId])) { return [$rootDeptId]; } $childrenByPid = []; foreach ($rows as $r) { $pid = (int) ($r['pid'] ?? 0); $id = (int) ($r['id'] ?? 0); if ($id <= 0) { continue; } if (!isset($childrenByPid[$pid])) { $childrenByPid[$pid] = []; } $childrenByPid[$pid][] = $id; } $out = []; $queue = [$rootDeptId]; $seen = []; while ($queue !== []) { $id = array_shift($queue); if (isset($seen[$id])) { continue; } $seen[$id] = true; $out[] = $id; foreach ($childrenByPid[$id] ?? [] as $cid) { $cid = (int) $cid; if ($cid > 0 && !isset($seen[$cid])) { $queue[] = $cid; } } } return $out; } }