diff --git a/app/bootstrap/Handler.php b/app/bootstrap/Handler.php new file mode 100644 index 0000000..5334e86 --- /dev/null +++ b/app/bootstrap/Handler.php @@ -0,0 +1,26 @@ +getMessage(),$exception); + $code = $exception->getCode(); + $debug = $this->_debug ?? $this->debug; + if ($request->expectsJson()) { + $json = ['code' => $code ?: 500, 'msg' => $debug ? $exception->getMessage() : 'Server internal error', 'type' => 'failed']; + $debug && $json['traces'] = (string)$exception; + return new Response(200, ['Content-Type' => 'application/json'], + \json_encode($json, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + } + $error = $debug ? \nl2br((string)$exception) : 'Server internal error'; + return new Response(500, [], $error); + } +} \ No newline at end of file diff --git a/app/bootstrap/SqlDebug.php b/app/bootstrap/SqlDebug.php index b8de867..4854056 100644 --- a/app/bootstrap/SqlDebug.php +++ b/app/bootstrap/SqlDebug.php @@ -30,8 +30,9 @@ class SqlDebug implements Bootstrap } $appPath = app_path(); + $pluginPath = dirname($appPath) . DIRECTORY_SEPARATOR . 'plugin'; - if (Config::getInstance()->dbDebug) Db::connection()->listen(function (QueryExecuted $queryExecuted) use ($appPath) { + if (Config::getInstance()->dbDebug) Db::connection()->listen(function (QueryExecuted $queryExecuted) use ($pluginPath, $appPath) { // 过滤掉 "select 1" 这类心跳检测SQL if (isset($queryExecuted->sql) && $queryExecuted->sql !== "select 1") { $bindings = $queryExecuted->bindings; @@ -60,7 +61,7 @@ class SqlDebug implements Bootstrap // 定位产生SQL的业务文件/行号/方法 $traces = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); foreach ($traces as $trace) { - if (isset($trace['file'], $trace['function']) && str_contains($trace['file'], $appPath)) { + if (isset($trace['file'], $trace['function']) && (str_contains($trace['file'], $appPath) || str_contains($trace['file'], $pluginPath))) { // 格式化文件路径(去掉项目根目录,只保留相对路径) $file = str_replace(base_path(), '', $trace['file']); $file = ltrim($file, '/\\'); diff --git a/app/config/Config.php b/app/config/Config.php index e14f992..a7c9eb8 100644 --- a/app/config/Config.php +++ b/app/config/Config.php @@ -82,6 +82,9 @@ class Config public int $httpPort{ get => $this->httpPort; } + public bool $enableRequestTimeLog{ + get => $this->enableRequestTimeLog; + } /** * 人员检测回调函数 @@ -112,6 +115,7 @@ class Config $this->httpPort = self::getIntEnv('HTTP_PORT', 8080); $this->zlm = new ZLMConfig(); $this->personDetectionCallback = null; + $this->enableRequestTimeLog = self::getBoolEnv('ENABLE_REQUEST_TIME_LOG', false); if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' && $this->tcpServerProcessNum > 1) { $this->tcpServerProcessNum = 1; echo "Warning: TCP_SERVER_PROCESS_NUM set to 1 on Windows.\n"; diff --git a/app/controller/IndexController.php b/app/controller/IndexController.php index afc8c3a..3f6ee5f 100644 --- a/app/controller/IndexController.php +++ b/app/controller/IndexController.php @@ -3,7 +3,7 @@ namespace app\controller; use app\config\Config; -use app\utils\ModelAutoGenerator; +use plugin\admin\app\common\ModelAutoGenerator; use app\zlm\ZLMClient; use support\Request; use support\Response; @@ -144,7 +144,7 @@ class IndexController if ($httpCode !== 200 || empty($imageData)) { // 记录错误日志 - \app\utils\Logger::error("ZLM截图失败: HTTP {$httpCode}, 错误: {$error}, URL: {$url}"); + \plugin\admin\app\common\Logger::error("ZLM截图失败: HTTP {$httpCode}, 错误: {$error}, URL: {$url}"); // 返回错误提示图 return $this->generateErrorImage("截图失败: HTTP {$httpCode}"); } @@ -165,7 +165,7 @@ class IndexController ]); } catch (\Throwable $e) { - \app\utils\Logger::error("ZLM截图异常: " . $e->getMessage()); + \plugin\admin\app\common\Logger::error("ZLM截图异常: " . $e->getMessage()); return $this->generateErrorImage("截图异常: " . $e->getMessage()); } } diff --git a/app/model/AdminData.php b/app/model/AdminData.php deleted file mode 100644 index b9e2900..0000000 --- a/app/model/AdminData.php +++ /dev/null @@ -1,41 +0,0 @@ - support\exception\Handler::class, +// '' => support\exception\Handler::class, + '' => Handler::class, ]; \ No newline at end of file diff --git a/config/log.php b/config/log.php index 284f11e..586e217 100644 --- a/config/log.php +++ b/config/log.php @@ -173,6 +173,13 @@ return [ } } + // 获取日志名称 + $logger = $record['logger'] ?? ''; + if (str_starts_with($logger, 'plugin.')) { + $logger = preg_replace('/^plugin\.([^.]+)\./', '[$1] ', $logger); + } + $record['logger'] = $logger; + $record['Level'] = str_pad(strtoupper($record['level_name']), 5); return $record; } diff --git a/plugin/admin/app/common/AccessDataUtil.php b/plugin/admin/app/common/AccessDataUtil.php deleted file mode 100644 index 646f5c7..0000000 --- a/plugin/admin/app/common/AccessDataUtil.php +++ /dev/null @@ -1,395 +0,0 @@ - admin()['id'], "无法获取到管理员登录信息")->getOrNull(); - } - - public static function admin_username(): ?string - { - return runCatching(fn() => admin()['username'], "无法获取到管理员登录信息")->getOrNull(); - } - - public static function admin_nickname(): ?string - { - return runCatching(fn() => admin()['nickname'], "无法获取到管理员登录信息")->getOrNull(); - } - - - /** - * 获取&设置权限树 - */ - public static function data_permission_tree(?array $data = null): DataPermissionInterface - { - // 获取 session - try { - $data_permission_tree = session()("data_permission_tree"); - if (!empty($data_permission_tree)) return new DataPermission($data_permission_tree); - } catch (\Exception $e) { - Logger::error("获取 session data_permission_tree key 失败",$e); - } - // 如果不存在就去数据库查表 - - return []; - } -} - -/** - * 数据权限基础接口 - * 定义所有权限类必须实现的核心方法 - */ -interface DataPermissionInterface -{ - /** - * 获取权限唯一标识ID - */ - public function id(): int; - - /** - * 获取权限名称 - */ - public function name(): string; - - /** - * 转为array - * @return array - */ - public function toArray(): array; -} - -/** - * 科室权限类(你需要的 DepartmentPxx) - * 最小权限单元:管理科室下的具体权限项(如查看、编辑、删除等) - */ -class DepartmentPermission implements DataPermissionInterface -{ - // 科室ID - private int $deptId; - // 科室名称 - private string $deptName; - - public function __construct(int $deptId, string $deptName) - { - $this->deptId = $deptId; - $this->deptName = $deptName; - } - - /** - * 实现接口:获取科室ID - */ - public function id(): int - { - return $this->deptId; - } - - /** - * 实现接口:获取科室名称 - */ - public function name(): string - { - return $this->deptName; - } - - public function toArray(): array - { - $array = []; - $array['id'] = $this->deptId; - $array['name'] = $this->deptName; - return $array; - } -} - -/** - * 医院权限类 - * 内部管理:多个科室权限对象(DepartmentPermission) - */ -class HospitalPermission implements DataPermissionInterface -{ - // 医院ID - private int $hospitalId; - // 医院名称 - private string $hospitalName; - // 科室权限集合:科室ID => DepartmentPermission对象 - private array $deptPermissions = []; - - public function __construct(int $hospitalId, string $hospitalName) - { - $this->hospitalId = $hospitalId; - $this->hospitalName = $hospitalName; - } - - /** - * 实现接口:获取医院ID - */ - public function id(): int - { - return $this->hospitalId; - } - - /** - * 实现接口:获取医院名称 - */ - public function name(): string - { - return $this->hospitalName; - } - - /** - * 添加单个科室权限 - */ - public function addDept(DepartmentPermission $deptPermission): void - { - $this->deptPermissions[$deptPermission->id()] = $deptPermission; - } - - /** - * 批量添加科室权限 - */ - public function addDepts(array $deptPermissions): void - { - foreach ($deptPermissions as $dept) { - if ($dept instanceof DepartmentPermission) { - $this->addDept($dept); - } - } - } - - /** - * 获取单个科室权限 - */ - public function getDept(int $deptId): ?DepartmentPermission - { - return $this->deptPermissions[$deptId] ?? null; - } - - /** - * 判断是否包含某个科室 - */ - public function hasDept(int $deptId): bool - { - return isset($this->deptPermissions[$deptId]); - } - - /** - * 删除指定科室权限 - */ - public function removeDept(int $deptId): void - { - unset($this->deptPermissions[$deptId]); - } - - /** - * 获取所有科室权限 - */ - public function getAllDepts(): array - { - return $this->deptPermissions; - } - - /** - * 清空所有科室权限 - */ - public function clearDepts(): void - { - $this->deptPermissions = []; - } - - public function toArray(): array - { - $array = []; - $depts = []; - $array['id'] = $this->id(); - $array['name'] = $this->name(); - foreach ($this->deptPermissions as $deptPermission) { - $depts[] = $deptPermission->toArray(); - } - $array['depts'] = $depts; - return $array; - } -} - -/** - * 顶级数据权限类 - */ -class DataPermission implements DataPermissionInterface -{ - /** - * 从传入数组中获取的 ID 与名称 - */ - private int $id; - private string $name; - - /** - * 多医院集合:hospitalId => HospitalPermission - */ - private array $hospitalPermissions = []; - - /** - * 构造方法:接收 toArray() 输出的权限数组,自动还原为多医院对象结构 - * @param array $data_permission_tree 传入 DataPermission::toArray() 输出的数组 - */ - public function __construct(array $data_permission_tree) - { - $this->clear(); - - // 读取传入的 id 和 name,不硬编码 - $this->id = isset($data_permission_tree['id']) ? (int)$data_permission_tree['id'] : 0; - $this->name = isset($data_permission_tree['name']) ? (string)$data_permission_tree['name'] : ''; - - // 获取所有医院数组 - $hospitalList = $data_permission_tree['hospitals'] ?? []; - if (!is_array($hospitalList) || empty($hospitalList)) { - return; - } - - // 循环还原所有医院 + 下属科室 - foreach ($hospitalList as $hospitalData) { - if ( - !is_array($hospitalData) - || empty($hospitalData['id']) - || empty($hospitalData['name']) - ) { - continue; - } - - // 创建医院权限 - $hospitalId = (int)$hospitalData['id']; - $hospitalName = (string)$hospitalData['name']; - $hospitalPermission = new HospitalPermission($hospitalId, $hospitalName); - - // 添加下属科室 - $deptList = $hospitalData['depts'] ?? []; - foreach ($deptList as $deptData) { - if (empty($deptData['id']) || empty($deptData['name'])) { - continue; - } - $deptPermission = new DepartmentPermission( - (int)$deptData['id'], - (string)$deptData['name'] - ); - $hospitalPermission->addDept($deptPermission); - } - - // 存入多医院集合 - $this->hospitalPermissions[$hospitalId] = $hospitalPermission; - } - } - - /** - * 实现接口:获取顶级权限ID(来自传入数组) - */ - public function id(): int - { - return $this->id; - } - - /** - * 实现接口:获取顶级权限名称(来自传入数组) - */ - public function name(): string - { - return $this->name; - } - - /** - * 添加单个医院权限 - */ - public function addHospital(HospitalPermission $hospitalPermission): void - { - $this->hospitalPermissions[$hospitalPermission->id()] = $hospitalPermission; - } - - /** - * 批量添加医院 - */ - public function addHospitals(array $hospitalPermissions): void - { - foreach ($hospitalPermissions as $hospital) { - if ($hospital instanceof HospitalPermission) { - $this->addHospital($hospital); - } - } - } - - /** - * 获取单个医院 - */ - public function getHospital(int $hospitalId): ?HospitalPermission - { - return $this->hospitalPermissions[$hospitalId] ?? null; - } - - /** - * 判断是否包含某个医院 - */ - public function hasHospital(int $hospitalId): bool - { - return isset($this->hospitalPermissions[$hospitalId]); - } - - /** - * 删除指定医院 - */ - public function removeHospital(int $hospitalId): void - { - unset($this->hospitalPermissions[$hospitalId]); - } - - /** - * 获取所有医院 - */ - public function getAllHospitals(): array - { - return $this->hospitalPermissions; - } - - /** - * 获取所有医院ID - */ - public function getAllHospitalIds(): array - { - return array_keys($this->hospitalPermissions); - } - - /** - * 判断是否有医院权限 - */ - public function has(): bool - { - return !empty($this->hospitalPermissions); - } - - /** - * 清空所有医院权限 - */ - public function clear(): void - { - $this->hospitalPermissions = []; - } - - /** - * 转为array(完美支持构造方法还原) - */ - public function toArray(): array - { - $array = [ - 'id' => $this->id(), - 'name' => $this->name(), - 'hospitals' => [] - ]; - - foreach ($this->hospitalPermissions as $hospital) { - $array['hospitals'][] = $hospital->toArray(); - } - - return $array; - } -} \ No newline at end of file diff --git a/plugin/admin/app/common/ExUtil.php b/plugin/admin/app/common/ExUtil.php deleted file mode 100644 index 1c937ba..0000000 --- a/plugin/admin/app/common/ExUtil.php +++ /dev/null @@ -1,24 +0,0 @@ -withDataPermission() + ->orderBy('id') + ->get(); + + // 2. 组装树结构 + $treeData = []; + foreach ($hospitals as $hospital) { + // 医院节点 + $hospitalNode = [ + 'id' => $hospital->id, + 'title' => $hospital->organ_name, // 医院名称 + 'children' => [] + ]; + + // 3. 查询该医院下的所有科室 + $departments = OpmMwDepartment::where('organ_id', $hospital->id) + ->withDataPermission() + ->orderBy('sort_id') + ->get(); + + // 4. 组装科室节点 + foreach ($departments as $dept) { + $hospitalNode['children'][] = [ + 'id' => $dept->id, + 'title' => $dept->dept_name // 科室名称 + ]; + } + + $treeData[] = $hospitalNode; + } + + // 5. 返回 Layui 树需要的格式 + return json([ + 'code' => 0, + 'msg' => 'success', + 'data' => $treeData + ]); + } } diff --git a/plugin/admin/app/middleware/AccessDataControl.php b/plugin/admin/app/middleware/AccessDataControl.php deleted file mode 100644 index 0291cb1..0000000 --- a/plugin/admin/app/middleware/AccessDataControl.php +++ /dev/null @@ -1,57 +0,0 @@ - admin(), "无法获取到管理员登录信息")->getOrNull(); - - - - - $controller = $request->controller; - $action = $request->action; - - $code = 0; - $msg = ''; - if (!Auth::canAccess($controller, $action, $code, $msg)) { - if ($request->expectsJson()) { - $response = json(['code' => $code, 'msg' => $msg, 'data' => []]); - } else { - if ($code === 401) { - $response = admin_error_401_script(); - } else { - $request->app = ''; - $request->plugin = 'admin'; - $response = view('common/error/403')->withStatus(403); - } - } - - } else { - $response = $request->method() == 'OPTIONS' ? response('') : $handler($request); - } - - return $response; - } - -} diff --git a/plugin/admin/app/middleware/RequestTimeMiddleware.php b/plugin/admin/app/middleware/RequestTimeMiddleware.php new file mode 100644 index 0000000..3693db4 --- /dev/null +++ b/plugin/admin/app/middleware/RequestTimeMiddleware.php @@ -0,0 +1,50 @@ +enableRequestTimeLog) { + Logger::debug("{} {}", [$request->method(), $request->fullUrl()]); + } + // 2. 执行后续中间件/控制器业务逻辑 + $response = $handler($request); + + if (Config::getInstance()->enableRequestTimeLog) { + // 3. 请求处理完成后计算耗时 + $endTime = microtime(true); + $costTime = round(($endTime - $startTime) * 1000, 2); // 转换为毫秒,保留2位小数 + + // 获取请求信息(方便日志定位) + $requestMethod = $request->method(); + $requestUrl = $request->fullUrl(); + $clientIp = $request->getRealIp(); + + // 4. 打印耗时日志 + Logger::debug("[请求耗时] {} {} | IP: {} | 耗时: {} ms", [ + $requestMethod, + $requestUrl, + $clientIp, + $costTime + ]); + } + + // 5. 返回响应给客户端 + return $response; + } +} \ No newline at end of file diff --git a/plugin/admin/app/model/Base.php b/plugin/admin/app/model/Base.php index 72f56e0..7b3785c 100644 --- a/plugin/admin/app/model/Base.php +++ b/plugin/admin/app/model/Base.php @@ -2,10 +2,16 @@ namespace plugin\admin\app\model; +use app\utils\Logger; use DateTimeInterface; +use Illuminate\Database\Eloquent\Builder; +use support\Db; use support\Model; +/** + * @method static \Illuminate\Database\Eloquent\Builder|static withDataPermission() + */ class Base extends Model { /** @@ -23,4 +29,185 @@ class Base extends Model { 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; + } + } + + 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))); + } + + } diff --git a/plugin/admin/app/model/OpmMwDepartment.php b/plugin/admin/app/model/OpmMwDepartment.php index da18ef5..c91addd 100644 --- a/plugin/admin/app/model/OpmMwDepartment.php +++ b/plugin/admin/app/model/OpmMwDepartment.php @@ -1,8 +1,7 @@
-