This commit is contained in:
zimoyin
2026-04-03 15:34:04 +08:00
parent 1a84e92384
commit 673c83109f
5 changed files with 894 additions and 247 deletions
+1
View File
@@ -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'];
@@ -0,0 +1,598 @@
<?php
namespace plugin\admin\app\common;
use Illuminate\Database\Eloquent\Builder;
use InvalidArgumentException;
use plugin\admin\app\model\OpmMwPermissionRule;
use support\Db;
/**
* 数据权限规则服务类
*
* 作用:
* 将数据库里配置的数据权限规则,动态应用到 Eloquent 查询构造器上。
*
* 规则配置示例:
* - table: opm_mw_info_data // 规则对应的主表
* - field: organ_id // 需要限制的字段
* - field_table: opm_mw_info_data // 字段属于哪个表,可选
* - admin_attr: hospitals // 当前管理员属性名
* - admin_attr_map: opm_mw_department.id...opm_mw_department.name
* - action: in
* - status: 1
* - sort: 1
*
* 说明:
* 1. 默认规则字段会拼到主表别名上;
* 2. 如果 field_table 配置了,就优先使用对应表别名;
* 3. admin_attr_map 当前按“同表映射”使用,即:
* 从 sourceField 查出 targetField 的值。
*/
class DataPermissionService
{
/**
* 当前登录管理员信息
*
* 例如:
* [
* 'id' => 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 / 123 / 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 / 123 / 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}");
}
}
}
+98 -31
View File
@@ -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
]);
}
}
+53 -177
View File
@@ -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
{
$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');
}
return $query->whereIn("{$hospitalAlias}.id", $hospitalIds);
}
// 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');
}
});
}
/**
* 解析 "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;
// 超管判断(可选,也可以在规则里配置*)
$isSuper = true;
foreach (['hospitals', 'departments'] as $attr) {
if (($admin[$attr] ?? '') !== '*') {
$isSuper = false;
break;
}
}
if ($isSuper) return $query;
return null;
// 使用服务类应用权限
$service = new DataPermissionService($admin);
return $service->apply($query);
}
/**
* 把 "1,2,3" / "123" / " 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)));
}
}
+133 -28
View File
@@ -1,60 +1,165 @@
<!DOCTYPE html>
<html lang="en">
<html lang="zh-cn">
<head>
<meta charset="UTF-8">
<title>医院科室树形结构</title>
<!-- 引入你提供的资源 -->
<title>医院科室统计</title>
<link rel="stylesheet" href="/app/admin/component/pear/css/pear.css" />
<link rel="stylesheet" href="/app/admin/admin/css/reset.css" />
<style>
html, body {
height: 100%;
overflow: hidden;
}
.container {
height: 100%;
}
.left-tree {
height: calc(100vh - 20px);
overflow-y: auto;
}
.right-content {
height: calc(100vh - 20px);
display: flex;
flex-direction: column;
}
.table-box {
flex: 1;
overflow: hidden;
}
</style>
</head>
<body>
<!-- 树形结构容器 -->
<div class="layui-row container" style="padding:10px;">
<!-- 左侧树 -->
<div class="layui-col-md3 left-tree">
<div class="layui-card">
<div class="layui-card-header">医院科室</div>
<div class="layui-card-body">
<div id="hospitalDeptTree" style="padding: 10px;"></div>
<div id="hospitalDeptTree"></div>
</div>
</div>
</div>
<script src="/app/admin/component/layui/layui.js?v=2.8.12"></script>
<!-- 右侧 -->
<div class="layui-col-md9 right-content">
<!-- 卡片 -->
<div class="layui-row" id="summaryCards"></div>
<!-- 表格 -->
<div class="table-box">
<table id="dataTable"></table>
</div>
</div>
</div>
<script src="/app/admin/component/layui/layui.js"></script>
<script src="/app/admin/component/pear/pear.js"></script>
<script src="/app/admin/admin/js/permission.js"></script>
<script src="/app/admin/admin/js/common.js"></script>
<script>
layui.use(['tree', 'layer', 'jquery'], function(){
layui.use(['tree', 'table', 'jquery'], function(){
var tree = layui.tree;
var layer = layui.layer;
var table = layui.table;
var $ = layui.jquery;
// 渲染医院科室树形结构
function renderHospitalTree() {
// 请求后端接口获取树数据
let currentType = 'hospital';
let currentId = null;
let currentHospitalId = null;
function initTree(){
$.get("/app/admin/test/tree", function(res){
if(res.code === 0){
tree.render({
elem: '#hospitalDeptTree', // 容器ID
data: res.data, // 树数据
showCheckbox: false, // 不显示复选框
onlyIconControl: true, // 仅允许图标展开/折叠
elem: '#hospitalDeptTree',
data: res.data,
onlyIconControl: true,
click: function(obj){
// 点击节点事件
layer.msg("你选择了:" + obj.data.title);
console.log("节点数据:", obj.data);
currentType = obj.data.type;
currentId = obj.data.id;
currentHospitalId = obj.data.hospital_id;
reloadAll();
}
});
} else {
layer.msg(res.msg || "加载树形结构失败");
if(res.data.length){
currentType = 'hospital';
currentId = res.data[0].id;
currentHospitalId = res.data[0].id;
reloadAll();
}
}).fail(function(){
layer.msg("接口请求失败,请检查后端服务");
});
}
// 页面加载完成渲染树
$(function(){
renderHospitalTree();
function initTable(){
table.render({
elem: '#dataTable',
id: 'dataTable',
url: '/app/admin/test/data',
page: true,
height: 'full-150',
cols: [[
{field:'organ_name', title:'医院'},
{field:'dept_name', title:'科室'},
{field:'waste_type', title:'垃圾类型'},
{field:'weight', title:'重量'},
{field:'recl_time', title:'时间'}
]]
});
}
function getParams(){
return {
type: currentType,
id: currentId,
hospital_id: currentHospitalId
};
}
function reloadAll(){
table.reload('dataTable', {
where: getParams(),
page: {curr:1}
});
loadSummary();
}
function loadSummary(){
$.get('/app/admin/test/summary', getParams(), function(res){
let html = '';
res.data.forEach(item=>{
html += `
<div class="layui-col-md3">
<div class="layui-card">
<div class="layui-card-header">${item.type || '未知'}</div>
<div class="layui-card-body">${item.total || 0} kg</div>
</div>
</div>`;
});
$('#summaryCards').html(html);
});
}
initTree();
initTable();
});
</script>
</body>
</html>