Files
zyt/server/app/adminapi/logic/stats/PerformanceDashboardScope.php
T
2026-08-17 09:06:49 +08:00

408 lines
13 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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;
}
}