936 lines
30 KiB
PHP
936 lines
30 KiB
PHP
<?php
|
||
// +----------------------------------------------------------------------
|
||
// | likeadmin快速开发前后端分离管理后台(PHP版)
|
||
// +----------------------------------------------------------------------
|
||
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
|
||
// | 开源版本可自由商用,可去除界面版权logo
|
||
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
|
||
// | github下载:https://github.com/likeshop-github/likeadmin
|
||
// | 访问官网:https://www.likeadmin.cn
|
||
// | likeadmin团队 版权所有 拥有最终解释权
|
||
// +----------------------------------------------------------------------
|
||
// | author: likeadminTeam
|
||
// +----------------------------------------------------------------------
|
||
|
||
namespace app\adminapi\logic\dept;
|
||
|
||
use app\common\logic\BaseLogic;
|
||
use app\common\model\auth\Admin;
|
||
use app\common\model\auth\AdminDept;
|
||
use app\common\model\dept\Dept;
|
||
use app\common\service\DataScope\DataScopeService;
|
||
use think\facade\Db;
|
||
|
||
|
||
/**
|
||
* 部门管理逻辑
|
||
* Class DeptLogic
|
||
* @package app\adminapi\logic\dept
|
||
*/
|
||
class DeptLogic extends BaseLogic
|
||
{
|
||
|
||
/**
|
||
* @notes 部门列表
|
||
* @param $params
|
||
* @return array
|
||
* @throws \think\db\exception\DataNotFoundException
|
||
* @throws \think\db\exception\DbException
|
||
* @throws \think\db\exception\ModelNotFoundException
|
||
* @author 段誉
|
||
* @date 2022/5/30 15:44
|
||
*/
|
||
public static function lists($params)
|
||
{
|
||
$where = [];
|
||
if (!empty($params['name'])) {
|
||
$where[] = ['name', 'like', '%' . $params['name'] . '%'];
|
||
}
|
||
if (isset($params['status']) && $params['status'] != '') {
|
||
$where[] = ['status', '=', $params['status']];
|
||
}
|
||
$lists = Dept::where($where)
|
||
->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<int, int> 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<int, int> $deptIds
|
||
* @return array<int, int>
|
||
*/
|
||
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<int>
|
||
*/
|
||
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<int> $rootIds
|
||
*
|
||
* @return list<int>
|
||
*/
|
||
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<int, true>
|
||
*/
|
||
public static function getErCenterSubtreeDeptIdSet(): array
|
||
{
|
||
$erRoots = self::findErCenterRootDeptIds();
|
||
$subtreeIds = self::unionErCenterSubtreeDeptIds($erRoots);
|
||
|
||
return array_fill_keys($subtreeIds, true);
|
||
}
|
||
|
||
/**
|
||
* 二中心复诊统计用的业务订单行(与 rollup 同源 SQL)。
|
||
*
|
||
* @return list<array{diagnosis_id: int, create_time: int, id: int, assistant_id: int}>
|
||
*/
|
||
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', 'o.creator_id'])
|
||
->order(['o.diagnosis_id' => 'asc', 'o.create_time' => 'asc', 'o.id' => 'asc'])
|
||
->select()
|
||
->toArray();
|
||
}
|
||
|
||
/**
|
||
* 二中心子树:各部门复诊 rollup(按订单创建人 creator_id 归属)。
|
||
* 候选订单仍要求 dg.assistant_id > 0(保持「er-center 案例 = 有医助的诊单」的定义),
|
||
* 但**复诊归属**改为订单创建人——leaf 部门归属取 creator_id 在二中心子树内的 canonical 部门,
|
||
* 并产出 by_creator_slots 供医助排行榜复诊列复用,与诊金 / 接诊诊单口径完全一致。
|
||
*
|
||
* @param list<int> $subtreeDeptIds
|
||
*
|
||
* @return array{
|
||
* totals: array<int, int>,
|
||
* slots: array<int, array<int, int>>,
|
||
* by_creator_slots: array<int, array<int, int>>
|
||
* }
|
||
*/
|
||
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_creator_slots' => []];
|
||
}
|
||
$subtreeSet = array_fill_keys($subtreeDeptIds, true);
|
||
$orderRows = self::fetchErCenterRevisitCandidateOrders($orderStartTs, $orderEndTs);
|
||
if ($orderRows === []) {
|
||
return ['totals' => $emptyTotals, 'slots' => $emptySlots, 'by_creator_slots' => []];
|
||
}
|
||
|
||
$byPatient = [];
|
||
foreach ($orderRows as $r) {
|
||
$pid = (int) ($r['diagnosis_id'] ?? 0);
|
||
if ($pid <= 0) {
|
||
continue;
|
||
}
|
||
$byPatient[$pid][] = $r;
|
||
}
|
||
|
||
$creatorNeed = [];
|
||
foreach ($byPatient as $orders) {
|
||
$n = \count($orders);
|
||
for ($i = 1; $i < $n; $i++) {
|
||
$cid = (int) ($orders[$i]['creator_id'] ?? 0);
|
||
if ($cid > 0) {
|
||
$creatorNeed[$cid] = true;
|
||
}
|
||
}
|
||
}
|
||
$creatorIds = array_keys($creatorNeed);
|
||
/** 复用 buildAssistantCanonicalDeptInSubtree —— 该方法实际是 admin → 子树内 canonical 部门,与 admin 角色无关 */
|
||
$canonicalDept = self::buildAssistantCanonicalDeptInSubtree($creatorIds, $subtreeSet);
|
||
|
||
/** @var array<int, array<int, int>> $leafByDeptSlot dept_id => [ slot => cnt ],slot≥2 */
|
||
$leafByDeptSlot = [];
|
||
/** @var array<int, array<int, int>> $byCreatorSlot 复诊按订单创建人聚合(与诊金/接诊诊单口径一致) */
|
||
$byCreatorSlot = [];
|
||
foreach ($byPatient as $orders) {
|
||
$n = \count($orders);
|
||
for ($i = 1; $i < $n; $i++) {
|
||
$slot = $i + 1;
|
||
$cid = (int) ($orders[$i]['creator_id'] ?? 0);
|
||
$deptId = $canonicalDept[$cid] ?? null;
|
||
if ($deptId === null || !isset($subtreeSet[$deptId])) {
|
||
continue;
|
||
}
|
||
if (!isset($leafByDeptSlot[$deptId])) {
|
||
$leafByDeptSlot[$deptId] = [];
|
||
}
|
||
$leafByDeptSlot[$deptId][$slot] = ($leafByDeptSlot[$deptId][$slot] ?? 0) + 1;
|
||
if ($cid > 0) {
|
||
if (!isset($byCreatorSlot[$cid])) {
|
||
$byCreatorSlot[$cid] = [];
|
||
}
|
||
$byCreatorSlot[$cid][$slot] = ($byCreatorSlot[$cid][$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 ($byCreatorSlot as $cid => $sm) {
|
||
if ($sm !== []) {
|
||
ksort($byCreatorSlot[$cid], SORT_NUMERIC);
|
||
}
|
||
}
|
||
|
||
return [
|
||
'totals' => $rollupTotals,
|
||
'slots' => $rollupSlots,
|
||
'by_creator_slots' => $byCreatorSlot,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 二中心子树:各部门复诊合计与分项(第 2 笔订单=复诊2…),供业绩看板等复用。
|
||
* $orderStartTs/$orderEndTs 均非 null 时仅统计该 create_time 窗口内业务单(与看板区间一致);均为 null 时不限时间(慎用,数据量大)。
|
||
*
|
||
* @return array{totals: array<int, int>, slots: array<int, array<int, int>>}
|
||
*/
|
||
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']];
|
||
}
|
||
|
||
/**
|
||
* 二中心子树复诊:按订单创建人 creator_id 聚合(与部门 rollup 同源,与诊金/接诊诊单口径一致)。
|
||
*
|
||
* @return array{totals: array<int, int>, slots: array<int, array<int, int>>}
|
||
*/
|
||
public static function getErCenterRevisitCountsByCreator(?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_creator_slots'];
|
||
$totals = [];
|
||
$slots = [];
|
||
foreach ($by as $cid => $slotMap) {
|
||
$cid = (int) $cid;
|
||
$totals[$cid] = (int) array_sum($slotMap);
|
||
$slots[$cid] = $slotMap;
|
||
}
|
||
|
||
return ['totals' => $totals, 'slots' => $slots];
|
||
}
|
||
|
||
/**
|
||
* 二中心子树复诊:列出某「订单创建人」在区间内、复诊序列(slot≥2)命中的业务订单 id(用于看板侧栏按复诊笔数下钻)。
|
||
* $revisitSlot = 0 表示累计全部复诊(第 2 笔起);≥2 时仅该分项。
|
||
*
|
||
* @return list<int>
|
||
*/
|
||
public static function listErCenterRevisitOrderIdsForCreator(
|
||
int $creatorId,
|
||
int $revisitSlot,
|
||
int $orderStartTs,
|
||
int $orderEndTs
|
||
): array {
|
||
if ($creatorId <= 0 || $orderStartTs <= 0 || $orderEndTs < $orderStartTs) {
|
||
return [];
|
||
}
|
||
if ($revisitSlot < 0 || $revisitSlot === 1) {
|
||
return [];
|
||
}
|
||
$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;
|
||
}
|
||
$ids = [];
|
||
foreach ($byPatient as $orders) {
|
||
$n = \count($orders);
|
||
for ($i = 1; $i < $n; $i++) {
|
||
$slot = $i + 1;
|
||
if ($revisitSlot > 0 && $slot !== $revisitSlot) {
|
||
continue;
|
||
}
|
||
$cid = (int) ($orders[$i]['creator_id'] ?? 0);
|
||
if ($cid !== $creatorId) {
|
||
continue;
|
||
}
|
||
$oid = (int) ($orders[$i]['id'] ?? 0);
|
||
if ($oid > 0) {
|
||
$ids[$oid] = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
return array_keys($ids);
|
||
}
|
||
|
||
/**
|
||
* 某展示部门行(组织架构上含下级 rollup)内,二中心复诊按**订单创建人**拆解笔数。
|
||
* 与看板行内「复诊」列口径一致:订单序列第 2 笔起;创建人 canonical 部门须落在二中心子树且在该部门行的子树内。
|
||
*
|
||
* @return array{rows: list<array{admin_id: int, name: string, order_count: int}>}
|
||
*/
|
||
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;
|
||
}
|
||
$creatorNeed = [];
|
||
foreach ($byPatient as $orders) {
|
||
$n = \count($orders);
|
||
for ($i = 1; $i < $n; $i++) {
|
||
$cid = (int) ($orders[$i]['creator_id'] ?? 0);
|
||
if ($cid > 0) {
|
||
$creatorNeed[$cid] = true;
|
||
}
|
||
}
|
||
}
|
||
$canonicalDept = self::buildAssistantCanonicalDeptInSubtree(array_keys($creatorNeed), $erSubtreeSet);
|
||
/** @var array<int, int> $counts */
|
||
$counts = [];
|
||
foreach ($byPatient as $orders) {
|
||
$n = \count($orders);
|
||
for ($i = 1; $i < $n; $i++) {
|
||
$slot = $i + 1;
|
||
if ($revisitSlot > 0 && $slot !== $revisitSlot) {
|
||
continue;
|
||
}
|
||
$cid = (int) ($orders[$i]['creator_id'] ?? 0);
|
||
if ($cid <= 0) {
|
||
continue;
|
||
}
|
||
$canon = $canonicalDept[$cid] ?? null;
|
||
if ($canon === null || !isset($erSubtreeSet[$canon]) || !isset($descFlip[$canon])) {
|
||
continue;
|
||
}
|
||
$counts[$cid] = ($counts[$cid] ?? 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 $cid => $cnt) {
|
||
$rows[] = [
|
||
'admin_id' => (int) $cid,
|
||
'name' => (string) ($nameRows[$cid] ?? ('#' . $cid)),
|
||
'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<int, list<int>>
|
||
*/
|
||
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<int, array<int, int>> $leafByDeptSlot
|
||
*
|
||
* @return array<int, array<int, int>> 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<int> $assistantIds
|
||
* @param array<int, true> $subtreeSet
|
||
*
|
||
* @return array<int, int> 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<int, int> 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<string, mixed> $adminInfo BaseAdminController::$adminInfo 形态(须含 root、role_id 等)
|
||
*
|
||
* @return array<int, array<string, mixed>>
|
||
*/
|
||
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<int, array<string, mixed>> $nodes
|
||
* @param array<int, true> $allowedIdMap
|
||
*
|
||
* @return array<int, array<string, mixed>>
|
||
*/
|
||
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<int>
|
||
*/
|
||
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;
|
||
}
|
||
|
||
} |