From 673c83109f4459eba16d65e003e38b33f0573fa7 Mon Sep 17 00:00:00 2001 From: zimoyin <2556608754@qq.com> Date: Fri, 3 Apr 2026 15:34:04 +0800 Subject: [PATCH] init --- config/log.php | 1 + .../app/common/DataPermissionService.php | 598 ++++++++++++++++++ .../admin/app/controller/TestController.php | 131 +++- plugin/admin/app/model/Base.php | 236 ++----- plugin/admin/app/view/test/index.html | 175 ++++- 5 files changed, 894 insertions(+), 247 deletions(-) create mode 100644 plugin/admin/app/common/DataPermissionService.php diff --git a/config/log.php b/config/log.php index 586e217..2987540 100644 --- a/config/log.php +++ b/config/log.php @@ -105,6 +105,7 @@ return [ && str_contains($step['file'], 'support') === false && str_contains($step['file'], 'monolog') === false && str_contains($step['file'], 'Logger') === false + && str_contains($step['file'], 'vendor') === false ) { // 获取文件路径(简化为项目内相对路径) $file = $step['file']; diff --git a/plugin/admin/app/common/DataPermissionService.php b/plugin/admin/app/common/DataPermissionService.php new file mode 100644 index 0000000..d3aeb59 --- /dev/null +++ b/plugin/admin/app/common/DataPermissionService.php @@ -0,0 +1,598 @@ + 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 Builder|string $query 查询对象 + * @return Builder + */ + public function apply(Builder|string $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 Builder $query 查询对象 + * @param array $rule 单条规则 + * @param string $mainAlias 主表别名 + * @param array $tableAliases 查询内所有表别名映射 + * @return Builder + */ + 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 Builder $query 查询对象 + * @param string $field 完整字段名 + * @param string $action 操作符 + * @param mixed $value 条件值 + * @return Builder + */ + 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}"); + } + } +} \ No newline at end of file diff --git a/plugin/admin/app/controller/TestController.php b/plugin/admin/app/controller/TestController.php index a4b4ca2..8d9e279 100644 --- a/plugin/admin/app/controller/TestController.php +++ b/plugin/admin/app/controller/TestController.php @@ -4,69 +4,136 @@ namespace plugin\admin\app\controller; use plugin\admin\app\model\OpmMwDepartment; use plugin\admin\app\model\OpmMwHospital; -use app\utils\Logger; +use plugin\admin\app\model\OpmMwInfoDatum; // 引入新模型 +use support\Request; use support\Response; -use Throwable; class TestController extends Crud { - - - /** - * 浏览 - * @return Response - * @throws Throwable - */ public function index(): Response { return raw_view('test/index'); } - /** - * 获取医院科室树形结构 - * @return Response + * 树结构(已应用规则引擎) */ public function tree(): Response { - // 1. 查询所有医院 - $hospitals = OpmMwHospital::where('is_true', 1) // 只查有效医院 - ->withDataPermission() + // 医院查询:自动应用规则引擎 + $hospitals = OpmMwHospital::where('is_true', 1) + ->withDataPermission() // 规则引擎 ->orderBy('id') ->get(); - // 2. 组装树结构 - $treeData = []; + $tree = []; + foreach ($hospitals as $hospital) { - // 医院节点 - $hospitalNode = [ + $node = [ 'id' => $hospital->id, - 'title' => $hospital->organ_name, // 医院名称 + 'title' => $hospital->organ_name, + 'type' => 'hospital', + 'hospital_id' => $hospital->id, 'children' => [] ]; - // 3. 查询该医院下的所有科室 - $departments = OpmMwDepartment::where('organ_id', $hospital->id) - ->withDataPermission() + // 科室查询:自动应用规则引擎 + $depts = OpmMwDepartment::where('organ_id', $hospital->id) + ->withDataPermission() // 规则引擎 ->orderBy('sort_id') ->get(); - // 4. 组装科室节点 - foreach ($departments as $dept) { - $hospitalNode['children'][] = [ + foreach ($depts as $dept) { + $node['children'][] = [ 'id' => $dept->id, - 'title' => $dept->dept_name // 科室名称 + 'title' => $dept->dept_name, + 'type' => 'dept', + 'hospital_id' => $hospital->id ]; } - $treeData[] = $hospitalNode; + $tree[] = $node; } - // 5. 返回 Layui 树需要的格式 return json([ 'code' => 0, - 'msg' => 'success', - 'data' => $treeData + 'data' => $tree ]); } -} + + /** + * 表格数据(已应用规则引擎) + */ + public function data(Request $request): Response + { + $type = $request->get('type'); + $id = $request->get('id'); + $hospitalId = $request->get('hospital_id'); + + // 改用模型查询 + 规则引擎 + $query = OpmMwInfoDatum::withDataPermission(); // 规则引擎 + + // 业务过滤:医院 + if ($hospitalId) { + $hospital = OpmMwHospital::find($hospitalId); + if ($hospital) { + $query->where('organ_name', $hospital->organ_name); + } + } + + // 业务过滤:科室 + if ($type === 'dept' && $id) { + $dept = OpmMwDepartment::find($id); + if ($dept) { + $query->where('dept_name', $dept->dept_name); + } + } + + $list = $query->orderBy('id', 'desc')->paginate(20); + + return json([ + 'code' => 0, + 'count' => $list->total(), + 'data' => $list->items() + ]); + } + + /** + * 统计卡片(已应用规则引擎) + */ + public function summary(Request $request): Response + { + $type = $request->get('type'); + $id = $request->get('id'); + $hospitalId = $request->get('hospital_id'); + + // 改用模型查询 + 规则引擎 + $query = OpmMwInfoDatum::withDataPermission(); // 规则引擎 + + // 业务过滤:医院 + if ($hospitalId) { + $hospital = OpmMwHospital::find($hospitalId); + if ($hospital) { + $query->where('organ_name', $hospital->organ_name); + } + } + + // 业务过滤:科室 + if ($type === 'dept' && $id) { + $dept = OpmMwDepartment::find($id); + if ($dept) { + $query->where('dept_name', $dept->dept_name); + } + } + + $data = $query + ->selectRaw("waste_type as type, SUM(CAST(weight AS DECIMAL(10,2))) as total") + ->groupBy('waste_type') + ->get(); + + return json([ + 'code' => 0, + 'data' => $data + ]); + } +} \ No newline at end of file diff --git a/plugin/admin/app/model/Base.php b/plugin/admin/app/model/Base.php index 7b3785c..0152840 100644 --- a/plugin/admin/app/model/Base.php +++ b/plugin/admin/app/model/Base.php @@ -5,10 +5,10 @@ namespace plugin\admin\app\model; use app\utils\Logger; use DateTimeInterface; use Illuminate\Database\Eloquent\Builder; +use plugin\admin\app\common\DataPermissionService; use support\Db; use support\Model; - /** * @method static \Illuminate\Database\Eloquent\Builder|static withDataPermission() */ @@ -20,194 +20,70 @@ class Base extends Model protected $connection = 'plugin.admin.mysql'; /** - * 格式化日期 - * - * @param DateTimeInterface $date - * @return string + * -------------------------- + * 【核心配置】权限规则配置 + * 新增规则只需在这里加一项,无需改下面的逻辑 + * -------------------------- */ - protected function serializeDate(DateTimeInterface $date) + protected function getPermissionRules(): array + { + return [ + // 规则1:医院权限 + 'hospital' => [ + 'table' => 'opm_mw_hospital', // 表名 + 'admin_attr' => 'hospitals', // 用户属性里的键($admin['hospitals']) + 'permission_field'=> 'id', // 表中用于权限过滤的字段 + 'related_field' => null, // 关联上级权限的字段(如科室关联医院的organ_id) + 'related_rule' => null, // 关联的上级规则key(对应上面的'hospital') + ], + // 规则2:科室权限 + 'department' => [ + 'table' => 'opm_mw_department', + 'admin_attr' => 'departments', + 'permission_field'=> 'id', + 'related_field' => 'organ_id', // 科室通过organ_id关联医院 + 'related_rule' => 'hospital', // 关联上级规则:医院 + ], + // 规则3:数据权限 + // 这个需要绑定 医院的.id + // 这个需要绑定 科室的.id + 'data' => [ + 'table' => 'opm_mw_info_data', + 'admin_attr' => 'data', + 'permission_field'=> 'id', + 'related_field' => null, + 'related_rule' => null, + ], + + ]; + } + + /** + * 格式化日期 + */ + protected function serializeDate(DateTimeInterface $date): string { return $date->format('Y-m-d H:i:s'); } - public function scopeWithDataPermission(Builder $query): Builder + + public function scopeWithDataPermission(Builder $query): Builder { - $admin = runCatching(fn() => admin(),"无法获取登录状态")->getOrDefault([]); + $admin = runCatching(fn() => admin(), "无法获取登录状态")->getOrDefault([]); - $hospitalRaw = trim((string)($admin['hospitals'] ?? '')); - $departmentRaw = trim((string)($admin['departments'] ?? '')); - - if ($hospitalRaw === '*' && $departmentRaw === '*') { - return $query; - } - - $hospitalIds = $this->parseVisibleIds($hospitalRaw); - $departmentIds = $this->parseVisibleIds($departmentRaw); - - if (empty($hospitalIds) && empty($departmentIds)) { - return $query->whereRaw('1 = 0'); - } - - $baseQuery = $query->getQuery(); - - // 主表 - [$fromTable, $fromAlias] = $this->parseTableAlias((string)($baseQuery->from ?? '')); - - // 识别 join 里的医院/科室表别名 - $hospitalAlias = $this->findTableAlias($baseQuery, 'opm_mw_hospital'); - $departmentAlias = $this->findTableAlias($baseQuery, 'opm_mw_department'); - - Logger::debug('scopeWithDataPermission fromTable:{} fromAlias:{} hospitalAlias:{} departmentAlias:{} hospitalIds:{} departmentIds:{}', [ - $fromTable, - $fromAlias, - $hospitalAlias, - $departmentAlias, - $hospitalIds, - $departmentIds, - ]); - - // 1) 主表就是医院表:只加医院权限 - if ($fromTable === 'opm_mw_hospital' && $hospitalAlias) { - if (empty($hospitalIds)) { - return $query->whereRaw('1 = 0'); + // 超管判断(可选,也可以在规则里配置*) + $isSuper = true; + foreach (['hospitals', 'departments'] as $attr) { + if (($admin[$attr] ?? '') !== '*') { + $isSuper = false; + break; } - - return $query->whereIn("{$hospitalAlias}.id", $hospitalIds); } + if ($isSuper) return $query; - // 2) 主表就是科室表:只加科室权限 - if ($fromTable === 'opm_mw_department' && $departmentAlias) { - return $query->where(function (Builder $q) use ($departmentAlias, $hospitalIds, $departmentIds) { - $has = false; - - // 科室权限:department.id - if (!empty($departmentIds)) { - $q->whereIn("{$departmentAlias}.id", $departmentIds); - $has = true; - } - - // 医院权限:department.organ_id - if (!empty($hospitalIds)) { - if ($has) { - $q->whereIn("{$departmentAlias}.organ_id", $hospitalIds); - } else { - $q->orWhereIn("{$departmentAlias}.organ_id", $hospitalIds); - } - $has = true; - } - - if (!$has) { - $q->whereRaw('1 = 0'); - } - }); - } - - // 3) 主表不是这两个,但 join 里有它们:按别名补条件 - return $query->where(function (Builder $q) use ( - $hospitalAlias, - $departmentAlias, - $hospitalIds, - $departmentIds - ) { - $hasAny = false; - - if ($hospitalAlias && !empty($hospitalIds)) { - $q->whereIn("{$hospitalAlias}.id", $hospitalIds); - $hasAny = true; - } - - if ($departmentAlias) { - $q->where(function (Builder $dq) use ($departmentAlias, $hospitalIds, $departmentIds) { - $has = false; - - if (!empty($departmentIds)) { - $dq->whereIn("{$departmentAlias}.id", $departmentIds); - $has = true; - } - - if (!empty($hospitalIds)) { - if ($has) { - $dq->orWhereIn("{$departmentAlias}.organ_id", $hospitalIds); - } else { - $dq->whereIn("{$departmentAlias}.organ_id", $hospitalIds); - } - $has = true; - } - - if (!$has) { - $dq->whereRaw('1 = 0'); - } - }); - - $hasAny = true; - } - - if (!$hasAny) { - $q->whereRaw('1 = 0'); - } - }); + // 使用服务类应用权限 + $service = new DataPermissionService($admin); + return $service->apply($query); } - /** - * 解析 "table as t" / "table t" - */ - protected function parseTableAlias(string $table): array - { - $table = trim(str_replace('`', '', $table)); - - if ($table === '') { - return ['', '']; - } - - if (stripos($table, ' as ') !== false) { - [$name, $alias] = preg_split('/\s+as\s+/i', $table, 2); - return [trim($name), trim($alias)]; - } - - $parts = preg_split('/\s+/', $table); - if (count($parts) >= 2) { - return [trim($parts[0]), trim($parts[1])]; - } - - return [$table, $table]; - } - - /** - * 在 from / joins 中找指定表的别名 - */ - protected function findTableAlias($baseQuery, string $tableName): ?string - { - [$fromTable, $fromAlias] = $this->parseTableAlias((string)($baseQuery->from ?? '')); - if ($fromTable === $tableName) { - return $fromAlias; - } - - foreach (($baseQuery->joins ?? []) as $join) { - [$joinTable, $joinAlias] = $this->parseTableAlias((string)($join->table ?? '')); - if ($joinTable === $tableName) { - return $joinAlias; - } - } - - return null; - } - - /** - * 把 "1,2,3" / "1,2,3" / " 1 , 2 " 转成数组 - */ - protected function parseVisibleIds(string $raw): array - { - $raw = trim($raw); - - if ($raw === '' || $raw === '*') { - return []; - } - - $parts = preg_split('/[,\s,]+/u', $raw) ?: []; - $parts = array_filter($parts, static fn($v) => $v !== ''); - - return array_values(array_unique(array_map('intval', $parts))); - } - - -} +} \ No newline at end of file diff --git a/plugin/admin/app/view/test/index.html b/plugin/admin/app/view/test/index.html index d072956..da4afaf 100644 --- a/plugin/admin/app/view/test/index.html +++ b/plugin/admin/app/view/test/index.html @@ -1,60 +1,165 @@ - +
-