model = DepartmentModel::class; $this->selectField = ['id', 'name', 'desc', 'status', 'pid', 'color', 'created_at', 'updated_at']; $this->queryField = ['name' => 'like', 'status' => '=', 'pid' => '=']; $this->orderBy = ['name' => 'id', 'sort' => 'asc']; } /** * 分页列表,附带每个部门的账号数 * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface */ public function list(): array { $result = $this->getPageList(); $ids = array_column($result['items'] ?? [], 'id'); $counts = $this->countAdminByDepartment($ids); foreach ($result['items'] as &$item) { $item['admin_count'] = $counts[$item['id']] ?? 0; } unset($item); return $result; } /** * 部门树(账号列表左侧筛选树、部门表单的上级选择都用这个) */ public function option(): array { $rows = DepartmentModel::where('deleted_at', 0) ->orderBy('id') ->get(['id', 'name', 'pid', 'status', 'color']) ->toArray(); return $this->utils->tree($rows); } /** * 部门树下拉(部门表单选上级用)。带 $isSelect 时首位补「顶级部门」 */ public function getTreeOption(bool $isSelect = false): array { $rows = DepartmentModel::where('deleted_at', 0) ->orderBy('id') ->get(['id', 'name', 'pid']) ->toArray(); $result = $this->utils->tree($rows); if ($isSelect) { array_unshift($result, ['id' => 0, 'name' => '顶级部门', 'pid' => 0]); } return $result; } /** * @throws Exception */ public function detail($id): mixed { return $this->getDetail($id); } /** * 新增部门 */ public function create($params): mixed { $this->assertNameUnique((string) ($params['name'] ?? ''), (int) ($params['pid'] ?? 0)); return $this->insert($params); } /** * 编辑部门 * @throws Exception */ public function update($id, $params): mixed { $id = (int) $id; if (array_key_exists('name', $params)) { $this->assertNameUnique((string) $params['name'], (int) ($params['pid'] ?? 0), $id); } // 上级不能指向自己或自己的子孙,否则树会成环、递归建树直接栈溢出 if (array_key_exists('pid', $params)) { $pid = (int) $params['pid']; if ($pid === $id) { $this->utils->errorThrow('上级部门不能是自己'); } if ($pid > 0 && in_array($pid, $this->descendantIds($id), true)) { $this->utils->errorThrow('上级部门不能是自己的下级'); } } return $this->save($id, $params); } /** * 删除部门:有子部门或仍有账号在用时拒绝,避免留下悬空引用 * @throws Exception */ public function delete($id): mixed { $ids = array_values(array_filter(array_map('intval', is_array($id) ? $id : [$id]))); if (empty($ids)) { $this->utils->errorThrow('参数错误'); } $hasChild = DepartmentModel::whereIn('pid', $ids)->where('deleted_at', 0)->exists(); if ($hasChild) { $this->utils->errorThrow('存在下级部门,请先删除下级'); } $inUse = AdminModel::whereIn('department_id', $ids)->where('deleted_at', 0)->exists(); if ($inUse) { $this->utils->errorThrow('仍有账号属于该部门,请先调整账号所属部门'); } return $this->del($ids); } /** * 停用/启用部门 * @throws Exception */ public function status($id, $status): mixed { return $this->save((int) $id, ['status' => (int) $status]); } /** * 同一父级下部门名不允许重复,否则树上出现两个同名节点没法分辨 */ private function assertNameUnique(string $name, int $pid, int $exceptId = 0): void { $name = trim($name); if ($name === '') { $this->utils->errorThrow('部门名称不能为空'); } $exists = DepartmentModel::where('name', $name) ->where('pid', $pid) ->where('deleted_at', 0) ->when($exceptId > 0, fn ($q) => $q->where('id', '<>', $exceptId)) ->exists(); if ($exists) { $this->utils->errorThrow('同级下已存在同名部门'); } } /** * 取某部门的全部子孙 id,用于成环校验 */ private function descendantIds(int $id): array { $all = DepartmentModel::where('deleted_at', 0)->get(['id', 'pid']); $childMap = []; foreach ($all as $row) { $childMap[(int) $row->pid][] = (int) $row->id; } $result = []; $stack = $childMap[$id] ?? []; while (!empty($stack)) { $current = array_pop($stack); if (in_array($current, $result, true)) { continue; } $result[] = $current; foreach ($childMap[$current] ?? [] as $child) { $stack[] = $child; } } return $result; } /** * 部门账号数:admin 在 mysql 连接、department 在 business 连接,跨连接不能 join,分两次查 */ private function countAdminByDepartment(array $departmentIds): array { if (empty($departmentIds)) { return []; } return AdminModel::whereIn('department_id', $departmentIds) ->where('deleted_at', 0) ->groupBy('department_id') ->selectRaw('department_id, COUNT(*) AS c') ->pluck('c', 'department_id') ->all(); } }