diff --git a/plugin/admin/app/common/DataPermissionService.php b/plugin/admin/app/common/DataPermissionService.php index d3aeb59..90db62f 100644 --- a/plugin/admin/app/common/DataPermissionService.php +++ b/plugin/admin/app/common/DataPermissionService.php @@ -2,10 +2,11 @@ namespace plugin\admin\app\common; -use Illuminate\Database\Eloquent\Builder; use InvalidArgumentException; use plugin\admin\app\model\OpmMwPermissionRule; use support\Db; +use Illuminate\Database\Eloquent\Builder as EloquentBuilder; +use Illuminate\Database\Query\Builder as QueryBuilder; /** * 数据权限规则服务类 @@ -62,40 +63,75 @@ class DataPermissionService * 3. 收集查询中所有表别名; * 4. 逐条应用权限规则; * - * @param Builder|string $query 查询对象 - * @return Builder + * @param QueryBuilder|EloquentBuilder $query 查询对象 + * @return QueryBuilder|EloquentBuilder */ - public function apply(Builder|string $query): Builder + public function apply(QueryBuilder|EloquentBuilder $query): QueryBuilder|EloquentBuilder { - // 兼容传入 Builder 的情况;这里统一取底层 Query Builder - $baseQuery = $query->getQuery(); + // 统一拿 baseQuery(核心修复点) + if ($query instanceof EloquentBuilder) { + $baseQuery = $query->getQuery(); + $tableRaw = $query->getModel()->getTable(); + } else { + $baseQuery = $query; + $tableRaw = $query->from; + } - // 解析主表名和主表别名,例如:table as t => [table, t] - [$fromTable, $fromAlias] = $this->parseTableAlias((string)($baseQuery->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; +// } /** * 加载指定表的启用权限规则 @@ -131,18 +167,18 @@ class DataPermissionService * 5. 得到完整字段名; * 6. 根据 action 生成 where 条件。 * - * @param Builder $query 查询对象 + * @param QueryBuilder|EloquentBuilder $query 查询对象 * @param array $rule 单条规则 * @param string $mainAlias 主表别名 * @param array $tableAliases 查询内所有表别名映射 - * @return Builder + * @return QueryBuilder|EloquentBuilder */ protected function applySingleRule( - Builder $query, + QueryBuilder|EloquentBuilder $query, array $rule, string $mainAlias, array $tableAliases - ): Builder + ): QueryBuilder|EloquentBuilder { $field = trim((string)($rule['field'] ?? '')); $adminAttr = trim((string)($rule['admin_attr'] ?? '')); @@ -197,6 +233,66 @@ class DataPermissionService 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]); +// } + /** * 解析管理员属性值 * @@ -293,13 +389,13 @@ class DataPermissionService * 1. 数组型条件:in / not in * 2. 单值条件:= / > / < / like / is null 等 * - * @param Builder $query 查询对象 + * @param QueryBuilder|EloquentBuilder $query 查询对象 * @param string $field 完整字段名 * @param string $action 操作符 * @param mixed $value 条件值 - * @return Builder + * @return QueryBuilder|EloquentBuilder */ - protected function buildCondition(Builder $query, string $field, string $action, $value): Builder + protected function buildCondition(QueryBuilder|EloquentBuilder $query, string $field, string $action, $value): QueryBuilder|EloquentBuilder { // 数组条件 if (is_array($value)) { @@ -326,6 +422,33 @@ class DataPermissionService }; } +// 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}"), +// }; +// } + /** * 获取完整字段名 * diff --git a/plugin/admin/app/controller/OpmMwInfoDatumController.php b/plugin/admin/app/controller/OpmMwInfoDatumController.php new file mode 100644 index 0000000..25791a1 --- /dev/null +++ b/plugin/admin/app/controller/OpmMwInfoDatumController.php @@ -0,0 +1,68 @@ +model = new OpmMwInfoDatum; + } + + /** + * 浏览 + * @return Response + */ + public function index(): Response + { + return view('opm-mw-info-datum/index'); + } + + /** + * 插入 + * @param Request $request + * @return Response + * @throws BusinessException + */ + public function insert(Request $request): Response + { + if ($request->method() === 'POST') { + return parent::insert($request); + } + return view('opm-mw-info-datum/insert'); + } + + /** + * 更新 + * @param Request $request + * @return Response + * @throws BusinessException + */ + public function update(Request $request): Response + { + if ($request->method() === 'POST') { + return parent::update($request); + } + return view('opm-mw-info-datum/update'); + } + +} diff --git a/plugin/admin/app/controller/OpmMwPermissionRuleController.php b/plugin/admin/app/controller/OpmMwPermissionRuleController.php new file mode 100644 index 0000000..c6d78c1 --- /dev/null +++ b/plugin/admin/app/controller/OpmMwPermissionRuleController.php @@ -0,0 +1,167 @@ +model = new OpmMwPermissionRule; + } + + /** + * 浏览 + */ + public function index(): Response + { + return view('opm-mw-permission-rule/index'); + } + + /** + * 插入 + */ + public function insert(Request $request): Response + { + if ($request->method() === 'POST') { + return parent::insert($request); + } + return view('opm-mw-permission-rule/insert'); + } + + /** + * 更新 + */ + public function update(Request $request): Response + { + if ($request->method() === 'POST') { + return parent::update($request); + } + return view('opm-mw-permission-rule/update'); + } + + /** + * 获取已配置的表名列表 + */ + public function getTables(Request $request): Response + { + $tables = OpmMwPermissionRule::query() + ->distinct() + ->pluck('table') + ->toArray(); + + if (empty($tables)) { + $tables = ['opm_mw_info_data', 'opm_mw_hospital', 'opm_mw_department']; + } + + sort($tables); + + return json(['code' => 0, 'data' => $tables]); + } + + /** + * SQL 预览 API + * + * 说明: + * - 这里使用 Db::table(),不要再用匿名 model + * - 否则有些环境下 from 会被污染,导致表名解析异常 + */ + public function previewSql(Request $request): Response + { + $admin = runCatching(fn() => admin(), '无法获取登录状态')->getOrDefault([]); + $tableName = (string)$request->get('table', 'opm_mw_info_data'); + + $admin = $this->compatibleAdminAttr($admin); + + if (!preg_match('/^[A-Za-z0-9_]+$/', $tableName)) { + $tableName = 'opm_mw_info_data'; + } + + $builderQuery = Db::table($tableName); + + $originalSql = $this->getSqlFromQuery($builderQuery); + + try { + $service = new DataPermissionService($admin); + $queryWithPermission = $service->apply($builderQuery); + $permissionSql = $this->getSqlFromQuery($queryWithPermission); + } catch (\Throwable $e) { + return json([ + 'code' => 1, + 'msg' => 'SQL 预览失败:' . $e->getMessage(), + 'data' => [ + 'table' => $tableName, + 'original' => $originalSql, + 'permission' => '', + 'admin_attr' => array_intersect_key($admin, array_flip(['hospitals', 'departments', 'data', 'datum'])) + ] + ]); + } + + return json([ + 'code' => 0, + 'data' => [ + 'table' => $tableName, + 'original' => $originalSql, + 'permission' => $permissionSql, + 'admin_attr' => array_intersect_key($admin, array_flip(['hospitals', 'departments', 'data', 'datum'])) + ] + ]); + } + + /** + * 兼容用户属性键 + */ + protected function compatibleAdminAttr(array $admin): array + { + $pluralToSingular = ['data' => 'datum']; + + foreach ($pluralToSingular as $plural => $singular) { + if (!isset($admin[$plural]) && isset($admin[$singular])) { + $admin[$plural] = $admin[$singular]; + } + } + + return $admin; + } + + /** + * 从查询构造器获取 SQL + */ + protected function getSqlFromQuery($query): string + { + $sql = $query->toSql(); + $bindings = $query->getBindings(); + + foreach ($bindings as $binding) { + if (is_bool($binding)) { + $value = $binding ? '1' : '0'; + } elseif (is_null($binding)) { + $value = 'null'; + } elseif (is_numeric($binding)) { + $value = (string)$binding; + } else { + $value = "'" . addslashes((string)$binding) . "'"; + } + + $sql = preg_replace('/\?/', $value, $sql, 1); + } + + return $sql; + } +} \ No newline at end of file diff --git a/plugin/admin/app/model/OpmMwInfoDatum.php b/plugin/admin/app/model/OpmMwInfoDatum.php new file mode 100644 index 0000000..25ea652 --- /dev/null +++ b/plugin/admin/app/model/OpmMwInfoDatum.php @@ -0,0 +1,71 @@ +,<,in,like等) + * @property integer $sort 排序(越小越靠前) + * @property integer $status 状态:1启用 0禁用 + * @property string $remark 备注说明 + * @property mixed $created_at 创建时间 + * @property mixed $updated_at 更新时间 + */ +class OpmMwPermissionRule extends Base +{ + /** + * The table associated with the model. + * + * @var string + */ + protected $table = 'opm_mw_permission_rules'; + + /** + * The primary key associated with the table. + * + * @var string + */ + protected $primaryKey = 'id'; + + + +} diff --git a/plugin/admin/app/view/opm-mw-info-datum/index.html b/plugin/admin/app/view/opm-mw-info-datum/index.html new file mode 100644 index 0000000..ad0a236 --- /dev/null +++ b/plugin/admin/app/view/opm-mw-info-datum/index.html @@ -0,0 +1,356 @@ + + + +
+ +
+ 这里负责定义“谁能看哪张表、哪些数据”。
+ 一条规则可以理解成:当前登录人的某个属性,去匹配目标表的某个字段,从而自动限制可见范围。
+
| 场景 | +table | +field | +admin_attr | +admin_attr_map | +action | +
|---|---|---|---|---|---|
| 只看自己医院的数据 | +opm_mw_info_data | +organ_id | +hospitals | +- | +in | +
| 科室 ID 转科室名称过滤 | +opm_mw_info_data | +dept_name | +departments | +opm_mw_department.id...opm_mw_department.name | +in | +
| 跨表 join 取医院名称 | +opm_mw_info_data | +hospital_name | +hospitals | +opm_mw_department.id:organ_id...opm_mw_hospital.name:id | +in | +
| 医院表自身权限 | +opm_mw_hospital | +id | +hospitals | +- | +in | +