diff --git a/.gitignore b/.gitignore index 516299c..f2f3dd0 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ .env /tests/tmp /tests/.phpunit.result.cache +/ZLM/ +/ffmpeg_cache/ diff --git a/app/config/Config.php b/app/config/Config.php index a7c9eb8..741b5d6 100644 --- a/app/config/Config.php +++ b/app/config/Config.php @@ -19,6 +19,10 @@ class Config get => $this->errorLogRotationTimeByDay; } + public bool $consoleLog { + get => $this->consoleLog; + } + public DatabaseConfig $database { get => $this->database; } @@ -52,14 +56,14 @@ class Config /** * TCP Server 进程数量 */ - public int $tcpServerProcessNum = 1 { + public int $tcpServerProcessNum { get => $this->tcpServerProcessNum; } /** * TCP_SERVER_PORT */ - public int $tcpServerPort = 50000 { + public int $tcpServerPort { get => $this->tcpServerPort; } @@ -97,6 +101,7 @@ class Config $this->database = new DatabaseConfig(); $this->dbDebug = self::getBoolEnv('DB_DEBUG'); $this->logFilter = self::getStringArrayEnv('LOG_FILTER', []); + $this->consoleLog = self::getBoolEnv('CONSOLE_LOG', false); $this->logLevel = match (strtoupper(self::getStringEnv('LOG_LEVEL', 'DEBUG'))) { 'INFO' => \Monolog\Logger::INFO, 'WARNING' => \Monolog\Logger::WARNING, @@ -116,6 +121,7 @@ class Config $this->zlm = new ZLMConfig(); $this->personDetectionCallback = null; $this->enableRequestTimeLog = self::getBoolEnv('ENABLE_REQUEST_TIME_LOG', false); + $this->tcpServerProcessNum = self::getIntEnv('TCP_SERVER_PROCESS_NUM', 1); 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/config/DatabaseConfig.php b/app/config/DatabaseConfig.php index 21f771a..f319332 100644 --- a/app/config/DatabaseConfig.php +++ b/app/config/DatabaseConfig.php @@ -9,32 +9,64 @@ use support\Db; class DatabaseConfig { public string $host = 'localhost' { - get { - return $this->host; - } + get => $this->host; } public string $username = 'root' { - get { - return $this->username; - } + get => $this->username; } public string $password = '' { - get { - return $this->password; - } + get => $this->password; } - public string $database = 'opm_ectms' { - get { - return $this->database; - } + public string $database = 'ds2' { + get => $this->database; + } + public int $port = 3306{ + get => $this->port; + } + + public string $charset = 'utf8mb4'{ + get => $this->charset; + } + //collation + public string $collation = 'utf8mb4_general_ci'{ + get => $this->collation; + } + //driver + public string $driver = 'mysql'{ + get => $this->driver; + } + + public int $max_connections = 5{ + get => $this->max_connections; + } + public int $min_connections = 1{ + get => $this->min_connections; + } + public int $wait_timeout = 3{ + get => $this->wait_timeout; + } + public int $idle_timeout = 60{ + get => $this->idle_timeout; + } + public int $heartbeat_interval = 50{ + get => $this->heartbeat_interval; } public function __construct() { - $this->host = getenv('DB_HOST'); - $this->username = getenv('DB_USER'); - $this->password = getenv('DB_PASSWORD'); - $this->database = getenv('DB_NAME'); + $this->host = getenv('DB_HOST', $this->host); + $this->username = getenv('DB_USER', $this->username); + $this->password = getenv('DB_PASSWORD', $this->password); + $this->database = getenv('DB_NAME', $this->database); + $this->port = getenv('DB_PORT', $this->port); + $this->charset = getenv('DB_CHARSET', $this->charset); + $this->collation = getenv('DB_COLLATION', $this->collation); + $this->driver = getenv('DB_DRIVER', $this->driver); + $this->max_connections = getenv('DB_MAX_CONNECTIONS', $this->max_connections); + $this->min_connections = getenv('DB_MIN_CONNECTIONS', $this->min_connections); + $this->wait_timeout = getenv('DB_WAIT_TIMEOUT', $this->wait_timeout); + $this->idle_timeout = getenv('DB_IDLE_TIMEOUT', $this->idle_timeout); + $this->heartbeat_interval = getenv('DB_HEARTBEAT_INTERVAL', $this->heartbeat_interval); } /** diff --git a/config/database.php b/config/database.php index 07c0fb5..1dced8e 100644 --- a/config/database.php +++ b/config/database.php @@ -7,11 +7,11 @@ return [ 'connections' => [ 'mysql' => [ 'driver' => 'mysql', - 'host' => Config::getStringEnv('DB_HOST', 'localhost'), - 'port' => Config::getIntEnv('DB_PORT',3306), - 'database' => Config::getStringEnv('DB_NAME', 'opm_ectms'), - 'username' => Config::getStringEnv('DB_USER', 'root'), - 'password' => Config::getStringEnv('DB_PASSWORD'), + 'host' => Config::getInstance()->database->host, + 'port' => Config::getInstance()->database->port, + 'database' => Config::getInstance()->database->database, + 'username' => Config::getInstance()->database->username, + 'password' => Config::getInstance()->database->password, 'charset' => 'utf8mb4', 'collation' => 'utf8mb4_general_ci', 'prefix' => '', diff --git a/config/log.php b/config/log.php index 2987540..2e7b440 100644 --- a/config/log.php +++ b/config/log.php @@ -15,7 +15,7 @@ return [ [ 'class' => StreamHandler::class, 'constructor' => [ - 'php://stdout', + Config::getInstance()->consoleLog ? 'php://stdout' : '/dev/null', Config::getInstance()->logLevel, ], 'formatter' => [ diff --git a/ffmpeg_push.bat b/ffmpeg_push.bat new file mode 100644 index 0000000..6c4d79e --- /dev/null +++ b/ffmpeg_push.bat @@ -0,0 +1,103 @@ +@echo off +setlocal enabledelayedexpansion +chcp 65001 >nul +title RTMP推流工具 - 图片幻灯片推流(带时间戳) + +:: ================== 配置区域 ================== +set "PICTURE_DIR=C:\Users\zimoa\Pictures" :: 图片目录 +set "STREAM_URL=rtmp://127.0.0.1/live/stream" :: RTMP推流地址 +set "DURATION_PER_IMAGE=5" :: 每张图片显示秒数 +set "WIDTH=1280" :: 输出宽度 +set "HEIGHT=720" :: 输出高度 +set "VIDEO_BITRATE=3000k" :: 视频码率 +set "AUDIO_BITRATE=160k" :: 音频码率 + +:: 时间显示配置 +set "FONT_FILE=C\:/Windows/Fonts/Arial.ttf" :: 字体文件路径(Windows 常见字体) +set "FONT_SIZE=24" :: 字体大小 +set "FONT_COLOR=white" :: 字体颜色 +set "TIME_X=10" :: 水平位置(像素) +set "TIME_Y=10" :: 垂直位置(像素) +:: ============================================= + +:: 获取脚本所在目录 +set "SCRIPT_DIR=%~dp0" +set "CACHE_DIR=%SCRIPT_DIR%ffmpeg_cache" +set "LIST_FILE=%CACHE_DIR%\slideshow_list.txt" + +:: 创建缓存目录(如果不存在) +if not exist "%CACHE_DIR%" mkdir "%CACHE_DIR%" + +:: 检查FFmpeg是否可用 +ffmpeg -version >nul 2>&1 +if %errorlevel% neq 0 ( + echo [错误] 未找到FFmpeg,请确保已添加到系统环境变量! + pause + exit /b +) + +:: 检查图片目录是否存在 +if not exist "%PICTURE_DIR%" ( + echo [错误] 目录不存在: %PICTURE_DIR% + pause + exit /b +) + +:: 如果缓存列表文件不存在,则生成(只生成一次,自动跳过无效图片) +if not exist "%LIST_FILE%" ( + echo [信息] 未找到缓存列表,正在生成(首次运行,将跳过无效图片)... + echo [信息] 扫描图片目录: %PICTURE_DIR% + + pushd "%PICTURE_DIR%" 2>nul || ( + echo [错误] 无法进入目录: %PICTURE_DIR% + pause + exit /b + ) + + set "TEMP_LIST=%CACHE_DIR%\temp_list_%RANDOM%.txt" + set "VALID_COUNT=0" + + for %%f in (*.jpg *.jpeg *.png *.bmp *.gif *.webp) do ( + :: 使用 ffprobe 检查图片有效性 + ffprobe -v error -select_streams v:0 -show_entries stream=codec_type -of default=noprint_wrappers=1:nokey=1 "%%f" >nul 2>&1 + if !errorlevel! equ 0 ( + echo file '%%~ff' >> "!TEMP_LIST!" + echo duration %DURATION_PER_IMAGE% >> "!TEMP_LIST!" + set /a VALID_COUNT+=1 + ) else ( + echo [警告] 跳过无效或损坏的图片: %%f + ) + ) + + popd + + if !VALID_COUNT! equ 0 ( + echo [错误] 没有找到任何有效的图片文件! + del "!TEMP_LIST!" 2>nul + pause + exit /b + ) + + move /y "!TEMP_LIST!" "%LIST_FILE%" >nul + echo [信息] 已生成缓存列表: %LIST_FILE% + echo [信息] 共找到 !VALID_COUNT! 张有效图片 +) else ( + echo [信息] 使用已有缓存列表: %LIST_FILE% + for /f "tokens=2 delims= " %%a in ('find /c "file " "%LIST_FILE%"') do set "IMG_COUNT=%%a" + echo [信息] 缓存中包含 %IMG_COUNT% 张有效图片(如需重新扫描请手动删除 %LIST_FILE%) +) + +echo [信息] 每张图片显示 %DURATION_PER_IMAGE% 秒 +echo [信息] 正在启动RTMP推流(幻灯片循环播放,画面叠加当前时间)... +echo [提示] 按 Ctrl+C 可停止推流 + +:: 执行推流(添加 drawtext 显示当前时间) +ffmpeg -stream_loop -1 -f concat -safe 0 -i "%LIST_FILE%" ^ + -f lavfi -i "aevalsrc=sin(440*2*PI*t):sample_rate=44100" ^ + -vf "scale=%WIDTH%:%HEIGHT%:force_original_aspect_ratio=1,pad=%WIDTH%:%HEIGHT%:(ow-iw)/2:(oh-ih)/2,fps=30,format=yuv420p,drawtext=fontfile='%FONT_FILE%':text='%%{localtime\:%%Y-%%m-%%d %%H\:%%M\:%%S}':x=%TIME_X%:y=%TIME_Y%:fontsize=%FONT_SIZE%:fontcolor=%FONT_COLOR%:shadowx=2:shadowy=2" ^ + -c:v libx264 -preset veryfast -maxrate %VIDEO_BITRATE% -bufsize 6000k ^ + -g 60 -c:a aac -b:a %AUDIO_BITRATE% -ar 44100 ^ + -f flv "%STREAM_URL%" + +echo [信息] 推流已停止。 +pause \ No newline at end of file diff --git a/pkg.bat b/pkg.bat new file mode 100644 index 0000000..9774375 --- /dev/null +++ b/pkg.bat @@ -0,0 +1,160 @@ +@echo off +setlocal enabledelayedexpansion + +:: 定义基础变量 +set "BUILD_DIR=build" +set "OPM_ECTMS=opm_ectms" +set "WEBMAN_BIN=webman.bin" +@REM set "PHP_PATH=php" :: 默认值(系统环境变量中的 PHP) +set "PHP_PATH=D:\AppData\sdk\php-8.4\php.exe" :: 默认值(系统环境变量中的 PHP) + +:: 关键修复:处理 PowerShell 传递的参数(自动清理引号、兼容特殊路径) +if not "%~1"=="" ( + :: 接收传入参数,自动去除 PowerShell 可能添加的单/双引号 + set "INPUT_PHP=%~1" + set "INPUT_PHP=!INPUT_PHP:'=!" :: 去除单引号 + set "INPUT_PHP=!INPUT_PHP:"=!" :: 去除双引号 + set "PHP_PATH=!INPUT_PHP!" + + :: 验证 PHP 路径是否存在(兼容带空格/特殊字符的路径) + if not exist "!PHP_PATH!" ( + echo ❌ 错误:传入的 PHP 路径不存在!实际路径:!PHP_PATH! + pause + exit /b 1 + ) + echo ℹ️ 已使用传入的 PHP 路径:!PHP_PATH! +) else ( + echo ℹ️ 未传入 PHP 路径,使用系统默认 PHP(需添加到环境变量) +) + +:: 切换到脚本所在目录(兼容 PowerShell 中调用的路径问题) +cd /d "%~dp0" 2>nul || ( + echo ❌ 错误:无法切换到脚本所在目录,请检查脚本路径是否正确! + pause + exit /b 1 +) + +:: 检查并创建 build 文件夹 +if not exist "%BUILD_DIR%" ( + echo ⚠️ 未找到 build 目录,正在自动创建... + md "%BUILD_DIR%" 2>nul || ( + echo ❌ 错误:build 目录创建失败,请检查权限! + pause + exit /b 1 + ) + echo ✅ build 目录创建成功! +) + +:: 1. 清理旧文件 +echo. +echo 1 🧹 清理旧文件... +if exist "%BUILD_DIR%\%WEBMAN_BIN%" ( + del /f /q "%BUILD_DIR%\%WEBMAN_BIN%" 2>nul && ( + echo ✅ 已删除旧的 %WEBMAN_BIN% 文件 + ) || ( + echo ⚠️ 警告:无法删除旧的 %WEBMAN_BIN% 文件(可能被占用) + ) +) else ( + echo ℹ️ 未找到旧的 %WEBMAN_BIN% 文件,无需清理 +) + +:: 2. 运行 PHP 构建命令(核心修复:用 call 确保 errorlevel 正确传递) +echo. +echo 2 🔧 执行构建命令:"!PHP_PATH!" -d phar.readonly=0 ./webman build:bin +echo ℹ️ 当前使用的 PHP 路径:!PHP_PATH! +call "!PHP_PATH!" -d phar.readonly=0 ./webman build:bin + +:: 关键修复:用更兼容的 errorlevel 判断方式(避免 PowerShell 解析冲突) +if errorlevel 1 ( + echo. + echo ❌ 错误:PHP 构建命令执行失败! + echo ℹ️ 可能原因: + echo 1. PHP 路径不正确(当前路径:!PHP_PATH!) + echo 2. PHP 未添加到系统环境变量(未传入路径时) + echo 3. webman 脚本不存在或项目依赖缺失 + echo 4. 缺少 phar 扩展或权限不足 + pause + exit /b 1 +) + +:: 检查构建结果 +if not exist "%BUILD_DIR%\%WEBMAN_BIN%" ( + echo. + echo ❌ 错误:构建失败!未生成 %WEBMAN_BIN% 文件 + pause + exit /b 1 +) +echo ✅ 构建成功,已生成 %WEBMAN_BIN% 文件 + +:: 3. 备份旧文件 +echo. +echo 3 🔧 备份旧的 %OPM_ECTMS% 文件... +if exist "%BUILD_DIR%\%OPM_ECTMS%" ( + :: 获取文件修改时间(兼容 PowerShell 中的日期格式) + for /f "delims=" %%a in ('powershell -Command "(Get-Item -Path '%BUILD_DIR%\%OPM_ECTMS%' -ErrorAction SilentlyContinue).LastWriteTime.ToString('yyyyMMdd-HH')"') do ( + set "FILE_MOD_TIME=%%a" + ) + :: 处理时间获取失败的情况 + if "!FILE_MOD_TIME!"=="" set "FILE_MOD_TIME=%date:~0,4%%date:~5,2%%date:~8,2%-%time:~0,2%" + set "OLD_OPM_ECTMS=%OPM_ECTMS%.!FILE_MOD_TIME!" + + :: 避免备份文件重名 + if exist "%BUILD_DIR%\!OLD_OPM_ECTMS!" set "OLD_OPM_ECTMS=!OLD_OPM_ECTMS!.bak" + + :: 执行备份(兼容带空格的文件名) + ren "%BUILD_DIR%\%OPM_ECTMS%" "!OLD_OPM_ECTMS!" 2>nul && ( + echo ✅ 旧文件已备份为:!OLD_OPM_ECTMS! + ) || ( + echo ⚠️ 警告:旧文件备份失败(可能被占用),将直接覆盖! + ) +) else ( + echo ℹ️ 未找到旧的 %OPM_ECTMS% 文件,无需备份 +) + +:: 4. 重命名文件 +echo. +echo 4 🔄 重命名文件:%WEBMAN_BIN% -> %OPM_ECTMS% +ren "%BUILD_DIR%\%WEBMAN_BIN%" "%OPM_ECTMS%" 2>nul && ( + echo ✅ 重命名成功! +) || ( + echo ❌ 错误:重命名失败(目标文件可能被占用) + pause + exit /b 1 +) + +:: 5. 权限提示(Windows 环境) +echo. +echo 5 🔑 配置文件权限... +echo ✅ Windows 环境下可执行权限已默认启用 +echo ℹ️ 文件路径:%BUILD_DIR%\%OPM_ECTMS% + +:: 6. 计算 SHA256 哈希(兼容 PowerShell 中的文件路径) +echo. +echo 6 📊 计算 %OPM_ECTMS% 的 SHA256 哈希值... +set "HASH_VALUE=" +for /f "delims=" %%a in ('powershell -Command "(Get-FileHash -Path '%BUILD_DIR%\%OPM_ECTMS%' -Algorithm SHA256 -ErrorAction SilentlyContinue).Hash"') do ( + set "HASH_VALUE=%%a" +) +if "!HASH_VALUE!"=="" ( + echo ⚠️ 警告:哈希值计算失败(文件可能不存在或无权限) +) else ( + echo ✅ 哈希值计算完成: + echo SHA256: !HASH_VALUE! +) + +:: (可选)保存哈希值到文件 +:: if "!HASH_VALUE!" neq "" ( +:: echo !HASH_VALUE! > "%BUILD_DIR%\%OPM_ECTMS%.sha256" +:: echo ℹ️ 哈希值已保存到:%BUILD_DIR%\%OPM_ECTMS%.sha256 +:: ) + +:: 最终结果展示 +echo. +echo. +echo ============================================== +echo ✅ 所有操作已全部执行完毕! +echo 📁 最终文件位置:%BUILD_DIR%\%OPM_ECTMS% +echo 📊 文件 SHA256:!HASH_VALUE! +echo 🖥️ 使用的 PHP 路径:!PHP_PATH! +echo ============================================== +pause \ No newline at end of file diff --git a/plugin/admin/app/controller/DeviceController.php b/plugin/admin/app/controller/DeviceController.php index f046f55..d83e12f 100644 --- a/plugin/admin/app/controller/DeviceController.php +++ b/plugin/admin/app/controller/DeviceController.php @@ -50,6 +50,21 @@ class DeviceController extends Crud return raw_view('device/update'); } + /** + * 设备下拉选项(可按 hospital_id 筛选) + */ + public function options(Request $request): Response + { + $query = $this->repository->scopedQuery(); + if ($hospitalId = $request->get('hospital_id')) { + $query->where('hospital_id', $hospitalId); + } + $data = $query->select(['id', 'name'])->get() + ->map(fn($item) => ['id' => $item->id, 'name' => $item->name]) + ->toArray(); + return json(['code' => 0, 'msg' => 'ok', 'data' => $data]); + } + protected function afterQuery($items) { $statusMap = [1 => '正常', 2 => '离线', 3 => '报警']; diff --git a/plugin/admin/app/repository/DeviceStatusRepository.php b/plugin/admin/app/repository/DeviceStatusRepository.php index f8e158e..8e73f76 100644 --- a/plugin/admin/app/repository/DeviceStatusRepository.php +++ b/plugin/admin/app/repository/DeviceStatusRepository.php @@ -129,12 +129,13 @@ class DeviceStatusRepository extends BaseRepository } /** - * 获取设备所有周期列表(操作记录) + * 获取设备周期列表(操作记录,限制最多200条避免数据过大) */ - public function getAllCyclesByDevice(int $deviceId): \Illuminate\Database\Eloquent\Collection + public function getAllCyclesByDevice(int $deviceId, int $limit = 200): \Illuminate\Database\Eloquent\Collection { return OpmDsCycle::where('device_id', $deviceId) ->orderBy('id', 'desc') + ->limit($limit) ->get(); } } diff --git a/plugin/admin/app/service/DeviceStatusService.php b/plugin/admin/app/service/DeviceStatusService.php index b58e577..9c9e3ee 100644 --- a/plugin/admin/app/service/DeviceStatusService.php +++ b/plugin/admin/app/service/DeviceStatusService.php @@ -127,15 +127,17 @@ class DeviceStatusService extends BaseService $deviceId = $this->resolveDeviceId($deviceId); if (!$deviceId) return []; + // 先查一次设备名称,避免 N+1 + $device = OpmDsDevice::find($deviceId); + $deviceName = $device->name ?? ''; + $cycles = $this->repository->getAllCyclesByDevice($deviceId); - return $cycles->map(function ($item) { - $device = OpmDsDevice::find($item->device_id); - + return $cycles->map(function ($item) use ($deviceName) { return [ 'id' => $item->id, 'operation' => $item->operation, - 'name' => $device->name ?? '', + 'name' => $deviceName, 'created_at' => $item->created_at, 'start_time' => $item->start_time, 'pack' => $item->pack, diff --git a/plugin/admin/config/database.php b/plugin/admin/config/database.php index 853ab8e..80aaa87 100644 --- a/plugin/admin/config/database.php +++ b/plugin/admin/config/database.php @@ -1,14 +1,17 @@ 'mysql', 'connections' => [ 'mysql' => [ 'driver' => 'mysql', - 'host' => '127.0.0.1', - 'port' => '3306', - 'database' => 'ds2', - 'username' => 'root', - 'password' => 'user', + 'host' => Config::getInstance()->database->host, + 'port' => Config::getInstance()->database->port, + 'database' => Config::getInstance()->database->database, + 'username' => Config::getInstance()->database->username, + 'password' => Config::getInstance()->database->password, 'charset' => 'utf8mb4', 'collation' => 'utf8mb4_general_ci', 'prefix' => '', diff --git a/数据权限.md b/数据权限.md deleted file mode 100644 index 1134b81..0000000 --- a/数据权限.md +++ /dev/null @@ -1,11 +0,0 @@ - - - - - -访问某个接口或者页面 - - -先经过 AccessData 表,如果可以的话,就走到 AccessDataControl - -AccessDataControl 用于获取账户的数据权限列表,写入到 \ No newline at end of file