whereNull('delete_time') ->field('id,name,data_scope') ->select() ->toArray(); return self::mergeRoleScopes($roleRows); } /** * 合并多个角色的数据范围。 * * 历史迁移曾把全部旧角色默认成 scope=1;如果账号同时具有有限范围角色,普通的 * scope=1 视为功能角色而不参与放大,防止“医生 + 医助”等组合意外获得全站数据。 * 2/3/4 是嵌套授权,取最小值即可表达多个有效数据角色的可见范围并集。 * * @param array> $roleRows */ private static function mergeRoleScopes(array $roleRows): int { $validRows = []; foreach ($roleRows as $roleRow) { $scope = (int) ($roleRow['data_scope'] ?? 0); if ($scope < self::SCOPE_ALL || $scope > self::SCOPE_SELF) { continue; } $validRows[] = [ 'name' => trim((string) ($roleRow['name'] ?? '')), 'scope' => $scope, ]; } // 角色存在但库中无有效 data_scope(缺失/脏数据/已删角色):宁可收窄到本人。 if ($validRows === []) { return self::SCOPE_SELF; } foreach ($validRows as $roleRow) { if ($roleRow['scope'] === self::SCOPE_ALL && in_array($roleRow['name'], self::ALL_SCOPE_ROLE_NAMES, true)) { return self::SCOPE_ALL; } } $boundedScopes = array_column(array_values(array_filter( $validRows, static fn (array $roleRow): bool => $roleRow['scope'] > self::SCOPE_ALL )), 'scope'); if ($boundedScopes !== []) { return (int) min($boundedScopes); } // 只有普通 scope=1 角色时保持原有“全部数据”行为。 return self::SCOPE_ALL; } /** * 统一解析 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|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|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] ?? '全部'; } /** * 当前 admin 持有的全部角色 id:优先取登录令牌缓存中的 role_id,缺失时查 admin_role。 * * @return int[] */ public static function roleIdsOf(int $adminId, array $adminInfo = []): array { $roleIds = self::normalizeRoleIds($adminInfo['role_id'] ?? null); if ($roleIds === [] && $adminId > 0) { $roleIds = self::normalizeRoleIds(AdminRole::where('admin_id', $adminId)->column('role_id')); } return array_values(array_unique($roleIds)); } /** * 一线角色 id(默认医生 1、医助 2),可由 project.data_scope.frontline_roles 覆盖。 * * @return int[] */ public static function frontlineRoleIds(): array { $ids = Config::get('project.data_scope.frontline_roles', self::DEFAULT_FRONTLINE_ROLE_IDS); return self::normalizeRoleIds(\is_array($ids) && $ids !== [] ? $ids : self::DEFAULT_FRONTLINE_ROLE_IDS); } /** * 是否持有团队/管理角色。 * * 多角色按并集取最宽:只要有一个角色能看团队数据,就不再因为同时挂了医生/医助而收窄到本人。 * 判定口径:root;角色在 project.data_scope.team_roles 或 exempt_roles 中; * 角色 data_scope 为「本部门及下级」/「仅本部门」;或 data_scope=1 且为内置管理员角色名。 * 新建角色默认 data_scope=1(全部)且不参与放大,主管类角色须把数据范围设为 2/3 才会被识别为团队角色。 */ public static function hasTeamRole(int $adminId, array $adminInfo = [], ?array $currentRoleIds = null): bool { if ((int) ($adminInfo['root'] ?? 0) === 1) { return true; } // 内部授权入口可传已核验的当前有效角色;空数组是明确无角色,不能回退到缓存或旧快照。 // 禁止把请求参数直接传入 $currentRoleIds。 $roleIds = $currentRoleIds === null ? self::roleIdsOf($adminId, $adminInfo) : self::normalizeRoleIds($currentRoleIds); if ($roleIds === []) { return false; } $configured = self::normalizeRoleIds(array_merge( (array) (Config::get('project.data_scope.team_roles', self::DEFAULT_TEAM_ROLE_IDS) ?: []), (array) (Config::get('project.data_scope.exempt_roles', []) ?: []) )); $roleRows = SystemRole::whereIn('id', $roleIds) ->whereNull('delete_time') ->field('id,name,data_scope') ->lock($currentRoleIds !== null) ->select() ->toArray(); // 配置中的团队角色也必须仍然存在,已删角色的遗留关系不能放大范围。 if (array_intersect(array_column($roleRows, 'id'), $configured) !== []) { return true; } foreach ($roleRows as $row) { $scope = (int) ($row['data_scope'] ?? 0); if ($scope === self::SCOPE_DEPT_AND_CHILD || $scope === self::SCOPE_DEPT) { return true; } if ($scope === self::SCOPE_ALL && in_array(trim((string) ($row['name'] ?? '')), self::ALL_SCOPE_ROLE_NAMES, true)) { return true; } } return false; } /** * 当前 admin 生效的“仅本人”一线角色(医生/医助)子集。 * * root 或持有团队角色时返回 [](范围交给 DataScope 按部门决定);否则返回其持有的一线角色。 * 业务代码用 in_array(1|2, 本方法结果) 代替直接 in_array(1|2, 全部角色),多角色即取最宽范围。 * * @return int[] */ public static function selfOnlyRoleIds(int $adminId, array $adminInfo = []): array { if (self::hasTeamRole($adminId, $adminInfo)) { return []; } return array_values(array_intersect(self::roleIdsOf($adminId, $adminInfo), self::frontlineRoleIds())); } }