204 lines
6.4 KiB
PHP
204 lines
6.4 KiB
PHP
<?php
|
||
|
||
namespace App\Service;
|
||
|
||
use App\BaseApp\BaseService;
|
||
use App\Models\AdminModel;
|
||
use App\Models\business\DepartmentModel;
|
||
use Exception;
|
||
use Psr\Container\ContainerExceptionInterface;
|
||
use Psr\Container\NotFoundExceptionInterface;
|
||
|
||
/**
|
||
* 部门服务
|
||
*
|
||
* 老后台的 department/option 返回的是扁平列表,前端 BasicTree 拿到后其实渲染不出层级,
|
||
* pid 白存了。这里 option 直接返回真正的树,账号列表左侧的部门树才能用。
|
||
*/
|
||
class DepartmentService extends BaseService
|
||
{
|
||
public function __construct()
|
||
{
|
||
parent::__construct();
|
||
$this->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();
|
||
}
|
||
}
|