223 lines
7.1 KiB
PHP
223 lines
7.1 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace app\common\service\DataScope;
|
||
|
||
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 think\facade\Config;
|
||
|
||
/**
|
||
* 数据范围(数据隔离)工具服务。
|
||
*
|
||
* 设计约定:
|
||
* - ALL (1) = 全部数据,不附加过滤
|
||
* - DEPT_AND_CHILD (2) = 本部门及所有子部门(取 admin 全部部门的并集)
|
||
* - DEPT (3) = 仅本部门(取 admin 全部部门的并集,不含子孙)
|
||
* - SELF (4) = 仅本人
|
||
*
|
||
* 多角色时取「最严格」可见范围 = data_scope 最大值(1=全部 … 4=仅本人),
|
||
* 与常见「数据权限取交集」一致,避免挂了一个「全部」角色就把其它角色的部门范围冲掉。
|
||
* root 管理员固定为 ALL。未挂任何部门时,范围退化为 SELF(可由 config 关闭)。
|
||
*
|
||
* 关键返回:`getVisibleAdminIds` 返回 int[](可见 admin_id 集合)或 null(ALL = 不过滤)。
|
||
*/
|
||
class DataScopeService
|
||
{
|
||
public const SCOPE_ALL = 1;
|
||
public const SCOPE_DEPT_AND_CHILD = 2;
|
||
public const SCOPE_DEPT = 3;
|
||
public const SCOPE_SELF = 4;
|
||
|
||
/**
|
||
* 计算当前 admin 的有效数据范围。
|
||
*/
|
||
public static function getEffectiveScope(array $adminInfo): int
|
||
{
|
||
if (!self::isEnabled()) {
|
||
return self::SCOPE_ALL;
|
||
}
|
||
if ((int) ($adminInfo['root'] ?? 0) === 1) {
|
||
return self::SCOPE_ALL;
|
||
}
|
||
$roleIds = self::normalizeRoleIds($adminInfo['role_id'] ?? null);
|
||
$exempt = array_map('intval', Config::get('project.data_scope.exempt_roles', []) ?: []);
|
||
if ($roleIds !== [] && array_intersect($roleIds, $exempt) !== []) {
|
||
return self::SCOPE_ALL;
|
||
}
|
||
if ($roleIds === []) {
|
||
return self::SCOPE_SELF;
|
||
}
|
||
$scopes = SystemRole::whereIn('id', $roleIds)
|
||
->whereNull('delete_time')
|
||
->column('data_scope');
|
||
$scopes = array_values(array_filter(array_map('intval', $scopes), static function (int $v): bool {
|
||
return $v >= self::SCOPE_ALL && $v <= self::SCOPE_SELF;
|
||
}));
|
||
// 角色存在但库中无有效 data_scope(缺失/脏数据/已删角色):宁可收窄到「仅本人」,避免误放开到全站
|
||
if ($scopes === []) {
|
||
return self::SCOPE_SELF;
|
||
}
|
||
|
||
return (int) max($scopes);
|
||
}
|
||
|
||
/**
|
||
* 统一解析 token/cache 中的 role_id(数组 | 单整数 | JSON 字符串)。
|
||
*
|
||
* @return int[]
|
||
*/
|
||
private static function normalizeRoleIds(mixed $raw): array
|
||
{
|
||
if ($raw === null || $raw === '') {
|
||
return [];
|
||
}
|
||
if (\is_int($raw) || \is_float($raw)) {
|
||
$v = (int) $raw;
|
||
|
||
return $v > 0 ? [$v] : [];
|
||
}
|
||
if (\is_string($raw) && is_numeric($raw)) {
|
||
$v = (int) $raw;
|
||
|
||
return $v > 0 ? [$v] : [];
|
||
}
|
||
if (\is_string($raw)) {
|
||
$decoded = json_decode($raw, true);
|
||
if (\is_array($decoded)) {
|
||
$raw = $decoded;
|
||
} else {
|
||
return [];
|
||
}
|
||
}
|
||
if (!\is_array($raw)) {
|
||
return [];
|
||
}
|
||
|
||
return array_values(array_filter(array_map(
|
||
static fn ($v): int => (int) $v,
|
||
$raw
|
||
), static fn (int $v): bool => $v > 0));
|
||
}
|
||
|
||
/**
|
||
* 可见 admin id 集合;null = 不过滤(ALL)
|
||
*
|
||
* @return array<int>|null
|
||
*/
|
||
public static function getVisibleAdminIds(int $adminId, array $adminInfo): ?array
|
||
{
|
||
$scope = self::getEffectiveScope($adminInfo);
|
||
if ($scope === self::SCOPE_ALL) {
|
||
return null;
|
||
}
|
||
|
||
if ($scope === self::SCOPE_SELF) {
|
||
return $adminId > 0 ? [$adminId] : [];
|
||
}
|
||
|
||
$myDeptIds = AdminDept::where('admin_id', $adminId)->column('dept_id');
|
||
$myDeptIds = array_values(array_filter(array_map('intval', $myDeptIds), static function (int $v): bool {
|
||
return $v > 0;
|
||
}));
|
||
|
||
if ($myDeptIds === []) {
|
||
$fallback = (bool) Config::get('project.data_scope.no_dept_fallback_self', true);
|
||
|
||
return $fallback ? [$adminId] : [];
|
||
}
|
||
|
||
$targetDeptIds = [];
|
||
if ($scope === self::SCOPE_DEPT) {
|
||
$targetDeptIds = $myDeptIds;
|
||
} else {
|
||
foreach ($myDeptIds as $did) {
|
||
foreach (DeptLogic::getSelfAndDescendantIds($did) as $id) {
|
||
$id = (int) $id;
|
||
if ($id > 0) {
|
||
$targetDeptIds[$id] = true;
|
||
}
|
||
}
|
||
}
|
||
$targetDeptIds = array_keys($targetDeptIds);
|
||
}
|
||
|
||
if ($targetDeptIds === []) {
|
||
return $adminId > 0 ? [$adminId] : [];
|
||
}
|
||
|
||
$ids = AdminDept::whereIn('dept_id', $targetDeptIds)->column('admin_id');
|
||
$ids = array_values(array_unique(array_filter(array_map('intval', $ids), static function (int $v): bool {
|
||
return $v > 0;
|
||
})));
|
||
if ($adminId > 0 && !in_array($adminId, $ids, true)) {
|
||
$ids[] = $adminId;
|
||
}
|
||
|
||
return $ids;
|
||
}
|
||
|
||
public static function isEnabled(): bool
|
||
{
|
||
return (bool) Config::get('project.data_scope.enabled', true);
|
||
}
|
||
|
||
public static function isAll(array $adminInfo): bool
|
||
{
|
||
return self::getEffectiveScope($adminInfo) === self::SCOPE_ALL;
|
||
}
|
||
|
||
/**
|
||
* 数据范围下:可见成员所在部门及其下级部门 id(与业绩看板 deptOptions、部门类下拉收窄一致)。
|
||
*
|
||
* @return array<int, true>|null null 表示不限制;[] 表示无可选部门
|
||
*/
|
||
public static function getAllowedDeptIdSet(int $adminId, array $adminInfo): ?array
|
||
{
|
||
if ($adminId <= 0 || !self::isEnabled()) {
|
||
return null;
|
||
}
|
||
$visibleIds = self::getVisibleAdminIds($adminId, $adminInfo);
|
||
if ($visibleIds === null) {
|
||
return null;
|
||
}
|
||
if ($visibleIds === []) {
|
||
return [];
|
||
}
|
||
$set = [];
|
||
foreach ($visibleIds as $aid) {
|
||
$deptRows = AdminDept::where('admin_id', (int) $aid)->column('dept_id');
|
||
foreach ($deptRows as $d) {
|
||
$d = (int) $d;
|
||
if ($d <= 0) {
|
||
continue;
|
||
}
|
||
foreach (DeptLogic::getSelfAndDescendantIds($d) as $x) {
|
||
$x = (int) $x;
|
||
if ($x > 0) {
|
||
$set[$x] = true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return $set;
|
||
}
|
||
|
||
/**
|
||
* 文字描述(日志 / 接口返回可选使用)
|
||
*/
|
||
public static function scopeLabel(int $scope): string
|
||
{
|
||
return [
|
||
self::SCOPE_ALL => '全部',
|
||
self::SCOPE_DEPT_AND_CHILD => '本部门及下级',
|
||
self::SCOPE_DEPT => '仅本部门',
|
||
self::SCOPE_SELF => '仅本人',
|
||
][$scope] ?? '全部';
|
||
}
|
||
}
|