408 lines
13 KiB
PHP
408 lines
13 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace app\adminapi\logic\stats;
|
||
|
||
use app\adminapi\logic\dept\DeptLogic;
|
||
use app\common\model\auth\AdminDept;
|
||
use app\common\model\auth\AdminRole;
|
||
use app\common\model\auth\SystemRole;
|
||
use app\common\model\dept\Dept;
|
||
use app\common\service\DataScope\DataScopeService;
|
||
|
||
/**
|
||
* 首页 KPI 数据范围:按角色而不是单纯按 data_scope。
|
||
*
|
||
* 医助=仅本人;组长=本小组全部成员;经理=本部门及下级;管理员/超管=全部。
|
||
* 本人业绩卡片始终按登录账号单独统计,不走这套范围。
|
||
*/
|
||
class PerformanceDashboardScope
|
||
{
|
||
public const KIND_ADMIN = 'admin';
|
||
public const KIND_MANAGER = 'manager';
|
||
public const KIND_GROUP_LEADER = 'group_leader';
|
||
public const KIND_ASSISTANT = 'assistant';
|
||
|
||
/**
|
||
* @return array{
|
||
* kind: string,
|
||
* label: string,
|
||
* metric_admin_ids: array<int>|null
|
||
* }
|
||
*/
|
||
public static function resolve(int $adminId, array $adminInfo): array
|
||
{
|
||
$roleNames = self::roleNames($adminId);
|
||
$kind = self::classify((int) ($adminInfo['root'] ?? 0) === 1, $roleNames, $adminInfo);
|
||
$metricAdminIds = self::metricAdminIds($kind, $adminId, $adminInfo);
|
||
|
||
return [
|
||
'kind' => $kind,
|
||
'label' => self::kindLabel($kind),
|
||
'metric_admin_ids' => $metricAdminIds,
|
||
];
|
||
}
|
||
|
||
public static function kindLabel(string $kind): string
|
||
{
|
||
return [
|
||
self::KIND_ADMIN => '全部数据',
|
||
self::KIND_MANAGER => '本部门',
|
||
self::KIND_GROUP_LEADER => '本小组',
|
||
self::KIND_ASSISTANT => '仅本人',
|
||
][$kind] ?? '数据范围';
|
||
}
|
||
|
||
/**
|
||
* @param string[] $roleNames
|
||
*/
|
||
public static function classify(bool $isRoot, array $roleNames, array $adminInfo = []): string
|
||
{
|
||
if ($isRoot) {
|
||
return self::KIND_ADMIN;
|
||
}
|
||
if (self::roleNamesMatch($roleNames, ['管理员'])) {
|
||
return self::KIND_ADMIN;
|
||
}
|
||
if (self::roleNamesMatch($roleNames, ['经理'])) {
|
||
return self::KIND_MANAGER;
|
||
}
|
||
if (self::roleNamesMatch($roleNames, ['诊室组长', '组长'])) {
|
||
return self::KIND_GROUP_LEADER;
|
||
}
|
||
// 医助角色固定仅本人,不因部门负责人或 data_scope 放大到小组。
|
||
if (self::roleNamesMatch($roleNames, ['医助'])) {
|
||
return self::KIND_ASSISTANT;
|
||
}
|
||
|
||
$scope = DataScopeService::getEffectiveScope($adminInfo);
|
||
return match ($scope) {
|
||
DataScopeService::SCOPE_ALL => self::KIND_ADMIN,
|
||
DataScopeService::SCOPE_DEPT_AND_CHILD => self::KIND_MANAGER,
|
||
DataScopeService::SCOPE_DEPT => self::KIND_GROUP_LEADER,
|
||
default => self::KIND_ASSISTANT,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* @return array<int>|null
|
||
*/
|
||
private static function metricAdminIds(string $kind, int $adminId, array $adminInfo = []): ?array
|
||
{
|
||
if ($kind === self::KIND_ADMIN) {
|
||
return null;
|
||
}
|
||
if ($kind === self::KIND_ASSISTANT || $adminId <= 0) {
|
||
return $adminId > 0 ? [$adminId] : [];
|
||
}
|
||
if ($kind === self::KIND_GROUP_LEADER) {
|
||
$ids = self::adminsInGroup($adminId, $adminInfo);
|
||
return $ids !== [] ? $ids : ($adminId > 0 ? [$adminId] : []);
|
||
}
|
||
|
||
$ids = self::adminsInOwnDeptTree($adminId);
|
||
if ($ids === []) {
|
||
return $adminId > 0 ? [$adminId] : [];
|
||
}
|
||
|
||
return $ids;
|
||
}
|
||
|
||
/**
|
||
* 组长小组:只取本人最深的部门(不含一中心/二中心整棵树),并并入其担任负责人的部门。
|
||
*
|
||
* @return int[]
|
||
*/
|
||
private static function adminsInGroup(int $adminId, array $adminInfo): array
|
||
{
|
||
$ownDeptIds = self::ownDeptIds($adminId);
|
||
$leafDeptIds = self::leafDeptIds($ownDeptIds);
|
||
$ledDeptIds = self::ledDeptIds($adminId, $adminInfo, $ownDeptIds);
|
||
$groupDeptIds = array_values(array_unique(array_merge($leafDeptIds, $ledDeptIds)));
|
||
$groupDeptIds = self::dropCenterRootsIfHasDeeper($groupDeptIds);
|
||
if ($groupDeptIds === []) {
|
||
$groupDeptIds = $leafDeptIds !== [] ? $leafDeptIds : $ownDeptIds;
|
||
}
|
||
|
||
$deptIds = [];
|
||
foreach ($groupDeptIds as $deptId) {
|
||
foreach (DeptLogic::getSelfAndDescendantIds((int) $deptId) as $id) {
|
||
$id = (int) $id;
|
||
if ($id > 0) {
|
||
$deptIds[] = $id;
|
||
}
|
||
}
|
||
}
|
||
$deptIds = array_values(array_unique($deptIds));
|
||
if ($deptIds === []) {
|
||
return $adminId > 0 ? [$adminId] : [];
|
||
}
|
||
|
||
$adminIds = array_values(array_unique(array_filter(
|
||
array_map('intval', AdminDept::whereIn('dept_id', $deptIds)->column('admin_id')),
|
||
static fn (int $id): bool => $id > 0
|
||
)));
|
||
if ($adminId > 0 && !in_array($adminId, $adminIds, true)) {
|
||
$adminIds[] = $adminId;
|
||
}
|
||
|
||
return $adminIds;
|
||
}
|
||
|
||
/**
|
||
* @return int[]
|
||
*/
|
||
private static function ownDeptIds(int $adminId): array
|
||
{
|
||
return array_values(array_unique(array_filter(
|
||
array_map('intval', AdminDept::where('admin_id', $adminId)->column('dept_id')),
|
||
static fn (int $id): bool => $id > 0
|
||
)));
|
||
}
|
||
|
||
/**
|
||
* 在本人所属部门里只留最深的节点,避免挂在「一中心」上就把整个中心当成小组。
|
||
*
|
||
* @param int[] $ownDeptIds
|
||
* @return int[]
|
||
*/
|
||
private static function leafDeptIds(array $ownDeptIds): array
|
||
{
|
||
if ($ownDeptIds === []) {
|
||
return [];
|
||
}
|
||
$deptById = [];
|
||
$rows = Dept::whereNull('delete_time')->field(['id', 'pid', 'name'])->select()->toArray();
|
||
foreach ($rows as $row) {
|
||
$id = (int) ($row['id'] ?? 0);
|
||
if ($id > 0) {
|
||
$deptById[$id] = [
|
||
'pid' => (int) ($row['pid'] ?? 0),
|
||
'name' => (string) ($row['name'] ?? ''),
|
||
];
|
||
}
|
||
}
|
||
|
||
$ownSet = array_fill_keys($ownDeptIds, true);
|
||
$leaves = [];
|
||
foreach ($ownDeptIds as $id) {
|
||
$hasOwnDescendant = false;
|
||
foreach ($ownDeptIds as $other) {
|
||
if ($other === $id) {
|
||
continue;
|
||
}
|
||
if (self::isAncestorOf($id, $other, $deptById)) {
|
||
$hasOwnDescendant = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!$hasOwnDescendant && isset($ownSet[$id])) {
|
||
$leaves[] = $id;
|
||
}
|
||
}
|
||
|
||
return array_values(array_unique($leaves));
|
||
}
|
||
|
||
/**
|
||
* @param array<int, array{pid: int, name: string}> $deptById
|
||
*/
|
||
private static function isAncestorOf(int $ancestorId, int $nodeId, array $deptById): bool
|
||
{
|
||
$current = $nodeId;
|
||
$seen = [];
|
||
while ($current > 0 && isset($deptById[$current]) && !isset($seen[$current])) {
|
||
$seen[$current] = true;
|
||
$pid = $deptById[$current]['pid'];
|
||
if ($pid === $ancestorId) {
|
||
return true;
|
||
}
|
||
$current = $pid;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 部门负责人姓名匹配当前组长时,把该部门算进小组。仅用于已判定为组长的账号。
|
||
*
|
||
* @param int[] $ownDeptIds
|
||
* @return int[]
|
||
*/
|
||
private static function ledDeptIds(int $adminId, array $adminInfo, array $ownDeptIds): array
|
||
{
|
||
$name = self::normalizePersonName((string) ($adminInfo['name'] ?? ''));
|
||
if ($adminId <= 0 || $name === '') {
|
||
return [];
|
||
}
|
||
$rows = Dept::whereNull('delete_time')->field(['id', 'pid', 'leader'])->select()->toArray();
|
||
$ownSet = array_fill_keys($ownDeptIds, true);
|
||
$led = [];
|
||
foreach ($rows as $row) {
|
||
$deptId = (int) ($row['id'] ?? 0);
|
||
$leaderName = self::normalizePersonName((string) ($row['leader'] ?? ''));
|
||
if ($deptId <= 0 || $leaderName === '' || $leaderName !== $name) {
|
||
continue;
|
||
}
|
||
if ($ownSet === [] || isset($ownSet[$deptId]) || self::deptUnderOwnTree($deptId, $ownDeptIds)) {
|
||
$led[] = $deptId;
|
||
}
|
||
}
|
||
|
||
return array_values(array_unique($led));
|
||
}
|
||
|
||
/**
|
||
* @param int[] $ownDeptIds
|
||
*/
|
||
private static function deptUnderOwnTree(int $deptId, array $ownDeptIds): bool
|
||
{
|
||
foreach ($ownDeptIds as $rootId) {
|
||
$ids = DeptLogic::getSelfAndDescendantIds((int) $rootId);
|
||
foreach ($ids as $id) {
|
||
if ((int) $id === $deptId) {
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* @param int[] $deptIds
|
||
* @return int[]
|
||
*/
|
||
private static function dropCenterRootsIfHasDeeper(array $deptIds): array
|
||
{
|
||
$names = [];
|
||
if ($deptIds !== []) {
|
||
$names = Dept::whereIn('id', $deptIds)->whereNull('delete_time')->column('name', 'id');
|
||
}
|
||
$hasDeeper = false;
|
||
foreach ($deptIds as $id) {
|
||
$name = (string) ($names[$id] ?? '');
|
||
if ($name !== '' && mb_strpos($name, '一中心') === false && mb_strpos($name, '二中心') === false) {
|
||
$hasDeeper = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!$hasDeeper) {
|
||
return $deptIds;
|
||
}
|
||
|
||
$kept = [];
|
||
foreach ($deptIds as $id) {
|
||
$name = (string) ($names[$id] ?? '');
|
||
if ($name !== '' && (mb_strpos($name, '一中心') !== false || mb_strpos($name, '二中心') !== false)) {
|
||
continue;
|
||
}
|
||
$kept[] = $id;
|
||
}
|
||
|
||
return $kept;
|
||
}
|
||
|
||
private static function normalizePersonName(string $raw): string
|
||
{
|
||
$value = trim($raw);
|
||
if ($value === '') {
|
||
return '';
|
||
}
|
||
$value = preg_replace('/[((][^))]*[))]/u', '', $value) ?? $value;
|
||
$value = preg_replace('/[\s\x{3000}]+/u', '', $value) ?? $value;
|
||
$suffixes = ['组长', '负责人', '主管', '主任', '医师', '医生', '医助', '老师'];
|
||
foreach ($suffixes as $suffix) {
|
||
$len = mb_strlen($suffix);
|
||
while (mb_strlen($value) > $len && mb_substr($value, -$len) === $suffix) {
|
||
$value = mb_substr($value, 0, mb_strlen($value) - $len);
|
||
}
|
||
}
|
||
|
||
return trim($value);
|
||
}
|
||
|
||
/**
|
||
* @return int[]
|
||
*/
|
||
private static function adminsInOwnDeptTree(int $adminId): array
|
||
{
|
||
$ownDeptIds = array_values(array_unique(array_filter(
|
||
array_map('intval', AdminDept::where('admin_id', $adminId)->column('dept_id')),
|
||
static fn (int $id): bool => $id > 0
|
||
)));
|
||
if ($ownDeptIds === []) {
|
||
return $adminId > 0 ? [$adminId] : [];
|
||
}
|
||
|
||
$deptIds = [];
|
||
foreach ($ownDeptIds as $deptId) {
|
||
foreach (DeptLogic::getSelfAndDescendantIds($deptId) as $id) {
|
||
$id = (int) $id;
|
||
if ($id > 0) {
|
||
$deptIds[] = $id;
|
||
}
|
||
}
|
||
}
|
||
$deptIds = array_values(array_unique($deptIds));
|
||
if ($deptIds === []) {
|
||
return [$adminId];
|
||
}
|
||
|
||
$adminIds = array_values(array_unique(array_filter(
|
||
array_map('intval', AdminDept::whereIn('dept_id', $deptIds)->column('admin_id')),
|
||
static fn (int $id): bool => $id > 0
|
||
)));
|
||
if ($adminId > 0 && !in_array($adminId, $adminIds, true)) {
|
||
$adminIds[] = $adminId;
|
||
}
|
||
|
||
return $adminIds;
|
||
}
|
||
|
||
/**
|
||
* @return string[]
|
||
*/
|
||
private static function roleNames(int $adminId): array
|
||
{
|
||
if ($adminId <= 0) {
|
||
return [];
|
||
}
|
||
$roleIds = array_values(array_unique(array_filter(
|
||
array_map('intval', AdminRole::where('admin_id', $adminId)->column('role_id')),
|
||
static fn (int $id): bool => $id > 0
|
||
)));
|
||
if ($roleIds === []) {
|
||
return [];
|
||
}
|
||
|
||
$names = SystemRole::whereIn('id', $roleIds)
|
||
->whereNull('delete_time')
|
||
->column('name');
|
||
|
||
return array_values(array_filter(array_map('strval', $names)));
|
||
}
|
||
|
||
/**
|
||
* @param string[] $roleNames
|
||
* @param string[] $needles
|
||
*/
|
||
private static function roleNamesMatch(array $roleNames, array $needles): bool
|
||
{
|
||
foreach ($roleNames as $name) {
|
||
$name = trim($name);
|
||
if ($name === '') {
|
||
continue;
|
||
}
|
||
foreach ($needles as $needle) {
|
||
if ($name === $needle || mb_strpos($name, $needle) !== false) {
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
}
|