1, * 'name' => 'admin', * 'hospitals' => '1,2,3' * ] */ protected array $admin; /** * 构造函数 * * @param array $admin 当前管理员信息 */ public function __construct(array $admin) { $this->admin = $admin; } /** * 将权限规则应用到查询构造器 * * 逻辑: * 1. 从查询中解析主表和别名; * 2. 读取该表对应的权限规则; * 3. 收集查询中所有表别名; * 4. 逐条应用权限规则; * * @param QueryBuilder|EloquentBuilder $query 查询对象 * @return QueryBuilder|EloquentBuilder */ public function apply(QueryBuilder|EloquentBuilder $query): QueryBuilder|EloquentBuilder { // 统一拿 baseQuery(核心修复点) if ($query instanceof EloquentBuilder) { $baseQuery = $query->getQuery(); $tableRaw = $query->getModel()->getTable(); } else { $baseQuery = $query; $tableRaw = $query->from; } // 解析主表 & 别名 [$fromTable, $fromAlias] = $this->parseTableAlias((string)($tableRaw ?? '')); if ($fromTable === '') { return $query; } // 加载规则 $rules = $this->loadRules($fromTable); if (empty($rules)) { return $query; } // 获取所有表别名 $tableAliases = $this->getAllTableAliases($baseQuery); // 应用规则 foreach ($rules as $rule) { $query = $this->applySingleRule($query, $rule, $fromAlias, $tableAliases); } return $query; } // public function apply(EloquentBuilder $query): Builder // { // // 兼容传入 Builder 的情况;这里统一取底层 Query Builder // $baseQuery = $query->getQuery(); // // // 解析主表名和主表别名,例如:table as t => [table, t] // [$fromTable, $fromAlias] = $this->parseTableAlias((string)($baseQuery->from ?? '')); // // // 如果无法识别主表,直接返回,不处理 // if ($fromTable === '') { // return $query; // } // // // 加载该主表对应的启用规则 // $rules = $this->loadRules($fromTable); // // // 没有规则则不做任何限制 // if (empty($rules)) { // return $query; // } // // // 获取查询里所有表的别名映射,后续用于拼字段 // $tableAliases = $this->getAllTableAliases($baseQuery); // // // 逐条应用规则 // foreach ($rules as $rule) { // $query = $this->applySingleRule($query, $rule, $fromAlias, $tableAliases); // } // // return $query; // } /** * 加载指定表的启用权限规则 * * 只读取: * - table = $tableName * - status = 1 * 并按照 sort 升序排列 * * @param string $tableName 表名 * @return array */ protected function loadRules(string $tableName): array { $this->validateIdentifierPath($tableName, '表名'); return OpmMwPermissionRule::query() ->where('table', $tableName) ->where('status', 1) ->orderBy('sort', 'asc') ->get() ->toArray(); } /** * 应用单条权限规则 * * 核心流程: * 1. 读取 rule 配置; * 2. 取出管理员对应属性; * 3. 如为 * 则表示拥有全部权限,直接跳过; * 4. 根据 admin_attr_map 解析映射值; * 5. 得到完整字段名; * 6. 根据 action 生成 where 条件。 * * @param QueryBuilder|EloquentBuilder $query 查询对象 * @param array $rule 单条规则 * @param string $mainAlias 主表别名 * @param array $tableAliases 查询内所有表别名映射 * @return QueryBuilder|EloquentBuilder */ protected function applySingleRule( QueryBuilder|EloquentBuilder $query, array $rule, string $mainAlias, array $tableAliases ): QueryBuilder|EloquentBuilder { $field = trim((string)($rule['field'] ?? '')); $adminAttr = trim((string)($rule['admin_attr'] ?? '')); $adminAttrMap = trim((string)($rule['admin_attr_map'] ?? '')); $action = strtolower(trim((string)($rule['action'] ?? 'in'))); $fieldTable = trim((string)($rule['field_table'] ?? '')); // 规则字段或管理员属性为空,视为无效规则,直接跳过 if ($field === '' || $adminAttr === '') { return $query; } // 校验字段写法是否合法 $this->validateFieldPath($field, '规则字段'); // 如果配置了字段所属表,也要校验 if ($fieldTable !== '') { $this->validateIdentifierPath($fieldTable, '字段所属表'); } // 取管理员对应属性值 $rawValue = $this->admin[$adminAttr] ?? ''; // * 表示不受限制 if ($rawValue === '*') { return $query; } // 解析管理员属性值,必要时通过映射转换成目标值 $resolvedValues = $this->resolveAdminAttrValue($rawValue, $adminAttrMap); // 如果最终解析不到任何可用值,则直接返回空结果 if (empty($resolvedValues) && !in_array('0', $resolvedValues, true)) { return $query->whereRaw('1 = 0'); } // 获取规则字段的完整字段名,如 dept.id / t.organ_id $fullField = $this->getFullFieldName($field, $mainAlias, $tableAliases, $fieldTable); // in / not in 这类操作符需要数组值 if ($this->isArrayOperator($action)) { return $this->buildCondition($query, $fullField, $action, $resolvedValues); } // 单值操作符只能对应一个结果,否则说明配置有问题 if (count($resolvedValues) !== 1) { throw new InvalidArgumentException( "规则配置错误:操作符 {$action} 只能对应单个值,但当前解析出 " . count($resolvedValues) . " 个值" ); } return $this->buildCondition($query, $fullField, $action, $resolvedValues[0]); } // protected function applySingleRule( // Builder $query, // array $rule, // string $mainAlias, // array $tableAliases // ): Builder // { // $field = trim((string)($rule['field'] ?? '')); // $adminAttr = trim((string)($rule['admin_attr'] ?? '')); // $adminAttrMap = trim((string)($rule['admin_attr_map'] ?? '')); // $action = strtolower(trim((string)($rule['action'] ?? 'in'))); // $fieldTable = trim((string)($rule['field_table'] ?? '')); // // // 规则字段或管理员属性为空,视为无效规则,直接跳过 // if ($field === '' || $adminAttr === '') { // return $query; // } // // // 校验字段写法是否合法 // $this->validateFieldPath($field, '规则字段'); // // // 如果配置了字段所属表,也要校验 // if ($fieldTable !== '') { // $this->validateIdentifierPath($fieldTable, '字段所属表'); // } // // // 取管理员对应属性值 // $rawValue = $this->admin[$adminAttr] ?? ''; // // // * 表示不受限制 // if ($rawValue === '*') { // return $query; // } // // // 解析管理员属性值,必要时通过映射转换成目标值 // $resolvedValues = $this->resolveAdminAttrValue($rawValue, $adminAttrMap); // // // 如果最终解析不到任何可用值,则直接返回空结果 // if (empty($resolvedValues) && !in_array('0', $resolvedValues, true)) { // return $query->whereRaw('1 = 0'); // } // // // 获取规则字段的完整字段名,如 dept.id / t.organ_id // $fullField = $this->getFullFieldName($field, $mainAlias, $tableAliases, $fieldTable); // // // in / not in 这类操作符需要数组值 // if ($this->isArrayOperator($action)) { // return $this->buildCondition($query, $fullField, $action, $resolvedValues); // } // // // 单值操作符只能对应一个结果,否则说明配置有问题 // if (count($resolvedValues) !== 1) { // throw new InvalidArgumentException( // "规则配置错误:操作符 {$action} 只能对应单个值,但当前解析出 " . count($resolvedValues) . " 个值" // ); // } // // return $this->buildCondition($query, $fullField, $action, $resolvedValues[0]); // } /** * 解析管理员属性值 * * 支持: * - 字符串:1,2,3 / 1,2,3 / 1 2 3 * - 数组:['1', '2'] * * admin_attr_map 格式: * - source_table.source_field...target_table.target_field * * 示例: * - opm_mw_department.id...opm_mw_department.name * * 说明: * - 当前实现按照“同表映射”使用 sourceField -> targetField * - sourceTable 主要用于配置语义和校验 * * @param mixed $rawValue 管理员原始属性值 * @param string $mapStr 映射配置字符串 * @return array */ protected function resolveAdminAttrValue($rawValue, string $mapStr): array { // 先把原始值拆成列表 $values = $this->parseListValues($rawValue); // 没有任何值,直接返回空数组 if (empty($values)) { return []; } // 没有映射配置,则直接返回原始值列表 if ($mapStr === '') { return $values; } // 映射格式必须包含 ... if (!str_contains($mapStr, '...')) { throw new InvalidArgumentException("无效的 admin_attr_map 格式:{$mapStr}"); } [$sourcePart, $targetPart] = explode('...', $mapStr, 2); $sourcePart = trim($sourcePart); $targetPart = trim($targetPart); if ($sourcePart === '' || $targetPart === '') { throw new InvalidArgumentException("无效的 admin_attr_map 格式:{$mapStr}"); } if (!str_contains($sourcePart, '.') || !str_contains($targetPart, '.')) { throw new InvalidArgumentException("admin_attr_map 必须包含表名与字段名:{$mapStr}"); } [$sourceTable, $sourceField] = explode('.', $sourcePart, 2); [$targetTable, $targetField] = explode('.', $targetPart, 2); $sourceTable = trim($sourceTable); $sourceField = trim($sourceField); $targetTable = trim($targetTable); $targetField = trim($targetField); // 校验表名和字段名合法性,防止非法 SQL 标识符 $this->validateIdentifierPath($sourceTable, '映射源表'); $this->validateIdentifierPath($sourceField, '映射源字段'); $this->validateIdentifierPath($targetTable, '映射目标表'); $this->validateIdentifierPath($targetField, '映射目标字段'); if ($sourceTable !== $targetTable) { // 当前版本先按同表映射处理,避免复杂 join 映射带来歧义 // 如果后面要扩展跨表映射,可以在这里补 join / 子查询 方案 // admin_attr_map 本身是将 admin.sourceField 进行了一个跨表 // 如果 admin_attr_map 内部再次跨表逻辑会更加混乱 } // 通过 sourceField 找到对应 targetField 的值 $results = Db::table($targetTable) ->whereIn($sourceField, $values) ->pluck($targetField) ->toArray(); // 统一转成字符串,去除空值 $results = array_map(static fn($v) => trim((string)$v), $results); $results = array_filter($results, static fn($v) => $v !== ''); // 去重并重新排序 return array_values(array_unique($results)); } /** * 构建 WHERE 条件 * * 支持两类: * 1. 数组型条件:in / not in * 2. 单值条件:= / > / < / like / is null 等 * * @param QueryBuilder|EloquentBuilder $query 查询对象 * @param string $field 完整字段名 * @param string $action 操作符 * @param mixed $value 条件值 * @return QueryBuilder|EloquentBuilder */ protected function buildCondition(QueryBuilder|EloquentBuilder $query, string $field, string $action, $value): QueryBuilder|EloquentBuilder { // 数组条件 if (is_array($value)) { return match ($action) { 'in' => $query->whereIn($field, $value), 'not in', '<>in' => $query->whereNotIn($field, $value), default => throw new InvalidArgumentException("不支持的数组操作符:{$action}"), }; } // 单值条件 return match ($action) { '=', 'eq' => $query->where($field, '=', $value), '>', 'gt' => $query->where($field, '>', $value), '<', 'lt' => $query->where($field, '<', $value), '>=', 'gte' => $query->where($field, '>=', $value), '<=', 'lte' => $query->where($field, '<=', $value), '<>', '!=', 'ne' => $query->where($field, '<>', $value), 'like' => $query->where($field, 'like', $this->normalizeLikeValue((string)$value)), 'not like' => $query->where($field, 'not like', $this->normalizeLikeValue((string)$value)), 'is null' => $query->whereNull($field), 'is not null' => $query->whereNotNull($field), default => throw new InvalidArgumentException("不支持的操作符:{$action}"), }; } // protected function buildCondition(Builder $query, string $field, string $action, $value): Builder // { // // 数组条件 // if (is_array($value)) { // return match ($action) { // 'in' => $query->whereIn($field, $value), // 'not in', '<>in' => $query->whereNotIn($field, $value), // default => throw new InvalidArgumentException("不支持的数组操作符:{$action}"), // }; // } // // // 单值条件 // return match ($action) { // '=', 'eq' => $query->where($field, '=', $value), // '>', 'gt' => $query->where($field, '>', $value), // '<', 'lt' => $query->where($field, '<', $value), // '>=', 'gte' => $query->where($field, '>=', $value), // '<=', 'lte' => $query->where($field, '<=', $value), // '<>', '!=', 'ne' => $query->where($field, '<>', $value), // 'like' => $query->where($field, 'like', $this->normalizeLikeValue((string)$value)), // 'not like' => $query->where($field, 'not like', $this->normalizeLikeValue((string)$value)), // 'is null' => $query->whereNull($field), // 'is not null' => $query->whereNotNull($field), // default => throw new InvalidArgumentException("不支持的操作符:{$action}"), // }; // } /** * 获取完整字段名 * * 优先级: * 1. 字段已带表名:直接返回 * 2. 配置了 field_table:使用对应表别名 * 3. 默认使用主表别名 * * @param string $field 字段名 * @param string $mainAlias 主表别名 * @param array $tableAliases 查询中的表别名映射 * @param string $fieldTable 字段所属表 * @return string */ protected function getFullFieldName(string $field, string $mainAlias, array $tableAliases, string $fieldTable = ''): string { // 如果 field 已经是 table.field 格式,直接返回 if (str_contains($field, '.')) { $this->validateFieldPath($field, '规则字段'); return $field; } // 默认使用主表别名 $alias = $mainAlias; // 如果规则里指定了字段所属表,则按该表找别名 if ($fieldTable !== '') { $alias = $this->resolveTableAlias($fieldTable, $tableAliases, $mainAlias); } if ($alias === '') { $alias = $mainAlias; } // 校验别名是否合法 $this->validateIdentifierPath($alias, '表别名'); return "{$alias}.{$field}"; } /** * 获取查询中所有表的别名映射 * * 返回格式: * [ * 'opm_mw_info_data' => 'data', * 'data' => 'data', * 'opm_mw_department' => 'dept', * 'dept' => 'dept', * ] * * @param mixed $baseQuery 基础查询对象 * @return array */ protected function getAllTableAliases($baseQuery): array { $aliases = []; // 主表别名 [$fromTable, $fromAlias] = $this->parseTableAlias((string)($baseQuery->from ?? '')); if ($fromTable !== '') { $aliases[$fromTable] = $fromAlias; $aliases[$fromAlias] = $fromAlias; } // 关联表别名 foreach (($baseQuery->joins ?? []) as $join) { [$joinTable, $joinAlias] = $this->parseTableAlias((string)($join->table ?? '')); if ($joinTable !== '') { $aliases[$joinTable] = $joinAlias; $aliases[$joinAlias] = $joinAlias; } } return $aliases; } /** * 解析表名和别名 * * 支持: * - table * - table as t * - table t * * @param string $table 表表达式 * @return array [tableName, alias] */ protected function parseTableAlias(string $table): array { // 去掉反引号并去首尾空格 $table = trim(str_replace('`', '', $table)); if ($table === '') { return ['', '']; } // 子查询或复杂表达式不处理 if (str_starts_with($table, '(')) { return ['', '']; } // 处理 as 写法 if (stripos($table, ' as ') !== false) { [$name, $alias] = preg_split('/\s+as\s+/i', $table, 2); $name = trim((string)$name); $alias = trim((string)$alias); return [$name, $alias !== '' ? $alias : $name]; } // 处理 table alias 写法 $parts = preg_split('/\s+/', $table) ?: []; if (count($parts) >= 2) { return [trim($parts[0]), trim($parts[1])]; } // 没有别名时,默认表名即别名 return [$table, $table]; } /** * 解析列表值 * * 支持: * - 数组 * - 字符串:1,2,3 / 1,2,3 / 1 2 3 * * 返回值统一为字符串数组,避免把非数字内容强制转 int 后出错 * * @param mixed $raw 原始值 * @return array */ protected function parseListValues($raw): array { if (is_array($raw)) { $values = []; array_walk_recursive($raw, static function ($item) use (&$values) { $values[] = trim((string)$item); }); } else { $raw = trim((string)$raw); // 空值或 * 视为无值 if ($raw === '' || $raw === '*') { return []; } // 支持英文逗号、中文逗号、空格分隔 $values = preg_split('/[,\s,]+/u', $raw) ?: []; $values = array_map(static fn($v) => trim((string)$v), $values); } // 清理空项 $values = array_filter($values, static fn($v) => $v !== ''); // 去重并重建索引 return array_values(array_unique($values)); } /** * 判断是否为数组型操作符 * * @param string $action 操作符 * @return bool */ protected function isArrayOperator(string $action): bool { return in_array($action, ['in', 'not in', '<>in'], true); } /** * like 值自动补 % * * 规则: * - 如果用户已经写了 %,则保持原样 * - 如果没有写 %,则默认转为 %xxx% * * @param string $value 原始值 * @return string */ protected function normalizeLikeValue(string $value): string { if ($value === '') { return $value; } if (!str_contains($value, '%')) { return '%' . $value . '%'; } return $value; } /** * 解析表名或别名到实际别名 * * @param string $tableOrAlias 表名或别名 * @param array $tableAliases 别名映射 * @param string $mainAlias 主表别名 * @return string */ protected function resolveTableAlias(string $tableOrAlias, array $tableAliases, string $mainAlias): string { $tableOrAlias = trim($tableOrAlias); if ($tableOrAlias === '') { return $mainAlias; } if (isset($tableAliases[$tableOrAlias])) { return $tableAliases[$tableOrAlias]; } return $tableOrAlias; } /** * 校验字段路径 * * 支持: * - field * - table.field * * @param string $field 字段路径 * @param string $label 错误提示标签 * @return void */ protected function validateFieldPath(string $field, string $label = '字段'): void { $field = trim($field); if ($field === '') { throw new InvalidArgumentException("{$label}不能为空"); } $parts = explode('.', $field); foreach ($parts as $part) { $this->validateIdentifierPath($part, $label); } } /** * 校验单个标识符 * * 仅允许: * - 字母 * - 数字 * - 下划线 * * 且不能以数字开头。 * * @param string $value 标识符 * @param string $label 错误提示标签 * @return void */ protected function validateIdentifierPath(string $value, string $label = '标识符'): void { $value = trim($value); if ($value === '') { throw new InvalidArgumentException("{$label}不能为空"); } // 仅允许字母、数字、下划线,且首字符不能为数字 if (!preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $value)) { throw new InvalidArgumentException("{$label}不合法:{$value}"); } } }