# SecMPS 项目说明文档 > **生成时间**: 2026-07-05 > **报告人**: 猫娘工程师 幽浮喵 > **项目全称**: Security Management Platform System(安防综合管理集成平台) > **总体定位**: 多子系统、多协议、多设备的统一安防集成管控平台 --- ## 一、项目总览 ### 1.1 一句话总结 **SecMPS 是一个基于 VolPro 框架(.NET 8 + SqlSugar)二次改造的、面向营区/仓库安防场景的多子系统集成平台,通过自研的 `IntegrationGateway` 适配器模式,把 GB28181 视频监控、MC4.0 动环监控、KMS 智能钥匙柜、门禁、无人机、巡更、车辆、对讲广播等异构子系统抽象成统一的"设备-点位-告警"模型进行集中管控。** ### 1.2 顶层目录布局 ``` SecMPS/ ├── api_sqlsugar/ VolPro 后端 (.NET 8 + SqlSugar) ├── web.vite/ VolPro 框架生成的管理端 (Vue 3 + TS) ├── warehouse/ 仓库/大屏前端 (Vue 3 + Element Plus) ├── gb28181_web-main/ GoWVP(Owl) 视频 Web 端 (React 19) ├── gateway/ IntegrationGateway 集成网关 (.NET 8) ├── owl_zlmediakit/ Owl + ZLMediaKit 离线部署包 └── doc/ 设计/对接/整合文档合集 ``` --- ## 二、项目划分 按 **运行时角色** 划分成 5 大子系统: | 子系统 | 代码位置 | 角色 | 默认端口 | |---|---|---|---| | **VolPro 后端** | `api_sqlsugar/` | 主数据中心 + 业务后台 + JWT/SignalR/Quartz | 9100 | | **VolPro 管理端** | `web.vite/` | 元数据/用户/权限/字典 CRUD | 9000 | | **仓库大屏** | `warehouse/` | 实时态势、地图、视频墙、IoT 看板 | 9200 | | **集成网关** | `gateway/` | 适配 Owl/MC4.0/KMS 等异构子系统 | 5100 | | **视频流服务** | `owl_zlmediakit/` | Owl(GoWVP) + ZLMediaKit 视频中转 | 15123/8000 | --- ## 三、核心架构理念 ### 3.1 整体架构图 ``` ┌─────────────────────────────────────────────────────────────┐ │ 前端层 │ │ web.vite 管理端 (Vue 3) warehouse 大屏 (Vue 3)│ │ - 设备/字典/权限 CRUD - 实时态势/视频墙/IoT │ └──────────┬──────────────────────────────────┬───────────────┘ │ HTTP REST │ HTTP + SignalR ▼ ▼ ┌─────────────────────────────────────────────────────────────┐ │ VolPro WebApi (.NET 8) — 唯一数据中心 │ │ - 8 张 Warehouse 业务表 + 6 张统一设备表 + 系统表 │ │ - SignalR 实时推送 / Quartz 定时任务 / JWT 鉴权 │ │ - DeviceManager / GatewayNode / Task 等 Controller │ └──────────┬──────────────────────────────────────────────────┘ │ A 组接口 (NodeToken 认证, 反向调用) ▼ ┌─────────────────────────────────────────────────────────────┐ │ IntegrationGateway 集成网关 (.NET 8) — 协议适配层 │ │ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ OwlAdapter │ │ Mc4Adapter │ │ KmsAdapter │ … │ │ └─────────────┘ └──────────────┘ └──────────────┘ │ └──────────┬─────────────────┬─────────────────┬───────────────┘ │ │ │ ▼ ▼ ▼ Owl (GoWVP) MC4.0 动环主机 KMS 智能钥匙柜 + ZLMediaKit (海康) (HTTP/REST) ``` ### 3.2 关键设计原则(来自 `SecMPS_最终整合方案_v3.0.md`) 1. **网关无状态**:配置只存 NodeCode/Token/VolProUrl,挂了重装即恢复 2. **AdapterCode 双段标识**:`"MC4:31ku"` 区分同类型多实例 3. **字段分治**:网关字段(IsOnline/ExtraData/ParentDeviceId)同步覆盖;管理员字段(Name/Category/Location)首次写入后不覆盖 4. **ExtraData JSON 兜底**:所有适配器私有字段塞 JSON,新增适配器不改表结构 5. **心跳机制**:网关 15s 上报,VolPro 30s 超时级联设备离线 6. **A/B 接口双通道**:A 组(网关→VolPro 反向调用 NodeToken 认证)、B 组(VolPro/管理端→网关 直连) --- ## 四、技术栈详解 ### 4.1 后端技术栈 | 维度 | 选型 | 备注 | |---|---|---| | 框架 | **VolPro**(基于 .NET 8 + ASP.NET Core) | 孙本超团队开源的低代码平台,2023.10 后支持 SqlSugar | | ORM | **SqlSugar 5.x** | 国产 MIT 协议 ORM,`.Include` 改 `.Includes` 是该项目重要适配点 | | 认证 | **JWT Bearer** + 节点 Token(`AppSetting.Secret.JWT`) | 详见 `Program.cs` | | 实时通信 | **SignalR** | `/hub/message`、`/performanceMonitorSignalR` | | 调度 | **Quartz.NET** | `[ApiTask]` 特性 + `Sys_QuartzOptions` 表配置 URL+Cron | | HTTP 客户端 | **IHttpClientFactory** + `SocketsHttpHandler` | 网关连接池:10 并发 / 5 分钟生命周期 | | 序列化 | **System.Text.Json** + Newtonsoft.Json(部分) | 驼峰命名、LongCovert 防止 JS 长整型精度丢失 | | 容器 | Docker(`VolPro.WebApi/Dockerfile`,aspnet:6.0 基础镜像) | 注意 Dockerfile 基础镜像是 .NET 6,与代码 .NET 8 不一致 | | 鉴权 | Autofac 容器 + 拦截器(`ApiAuthorizeFilter`) | | | 二维码/打印 | vue-plugin-hiprint | 报表打印前端组件 | ### 4.2 前端技术栈 | 项目 | 框架 | 关键依赖 | 渲染方案 | |---|---|---|---| | `web.vite/`(管理端) | Vue 3.5 + TS + Vite 6 | Element Plus 2.9、Pinia 3、Vuex 4、Axios、monaco-editor、VTable、wangeditor、hiprint、jsVideoPlugin(海康) | VolPro 框架生成 + `views/warehouse/` 业务扩展 | | `warehouse/`(大屏) | Vue 3.5 + TS + Vite 7 | Element Plus 2.11、Pinia 3、Vuex 4、Axios、**Microsoft SignalR 9**、ECharts 6 | 自研界面 + VgoMap 三维地图 + 天气/护栏等动态组件 | | `gb28181_web-main/`(Owl Web) | React 19 + TS + Vite 8 | Ant Design 6、Radix UI、TanStack Query/Router、@xyflow/react、Konva、hls.js、mp4box、tailwindcss 4、Biome | gowvp/owl 官方配套 web | ### 4.3 流媒体与第三方组件 | 组件 | 定位 | 关键能力 | |---|---|---| | **ZLMediaKit**(C++11) | 商业级流媒体服务器 | RTSP/RTMP/HLS/HTTP-FLV/WebSocket-FLV/GB28181 互转,最低 100ms 延迟,单机 10W 级并发 | | **GoWVP/Owl**(Go) | GB28181 视频管理平台 | 设备注册、通道管理、录像、PTZ、AI 事件 | | **Jessibuca**(JS + WebAssembly) | 纯 H5 播放器 | 通过 Emscripten 将解码库编译为 wasm,支持 H.264/H.265、FLV、RTMP、WebRTC | | **VgoMap** | 三维地图组件 | `window.VgoMap.Map`,用于仓库大屏 3D 视图 | | **HiPrint** | 打印插件 | 仓库管理单据打印 | ### 4.4 部署与运行环境 - **目标平台**:飞腾 S5000C(ARM64)+ 统信 UOS 20 操作系统 - **容器化**:Docker Compose 融合部署 Owl + ZLMediaKit - **关键库**:`libsrtp 2.5.0`(WebRTC 依赖,**版本必须严格锁定**) --- ## 五、数据库模型(核心 6 张表) > 完整表结构见 `VolPro_网关相关接口文档_v1.0.md` 和 `db_init.sql` | 表 | 角色 | 关键字段 | |---|---|---| | `warehouse_regions` | 区域树 | 自引用 ParentId | | `warehouse_devicepoint` | 点位(属于某区域) | RegionId | | `base_device` | **统一设备主表** | AdapterCode(`MC4:31ku`) + SourceId(联合唯一)、ParentDeviceId 自引用、ExtraData JSON | | `video_channel` | 视频通道扩展 | DeviceId → base_device(指向通道自身),OwlStreamApp/HasPtz 缓存 | | `video_record` | 录像记录 | ChannelId | | `iot_devicedata` | IoT 实时数据 | DeviceId + 测点值 | | `iot_alarm` | IoT 告警 | DeviceId + SourceAlarmId(去重) | | `gateway_nodes` | 网关节点注册 | NodeCode + NodeToken + LastHeartbeat | **DeviceGroup 字典驱动**:通过 `视频设备/IoT设备/门禁设备/道闸设备/报警设备` 五个字典值决定前端按钮组与同步模式(FullReplace vs Merge)。 --- ## 六、IntegrationGateway 网关设计 ### 6.1 项目结构 ``` gateway/ ├── IntegrationGateway.slnx # slnx 格式(VS 2022 新格式) ├── NuGet.Config # 强制使用 nuget.org └── src/ ├── IntegrationGateway.Core/ # 7 个能力接口 + 9 个统一模型 │ ├── Abstractions/ # IGatewayAdapter, IHasFlatDevices, IHasStreams… │ ├── Models/ # StandardDevice, StandardAlarm, PointValue… │ └── Infrastructure/ # AdapterRegistry, GatewayClientFactory, RateLimiter ├── IntegrationGateway.Adapters.Owl/ # Owl 适配器 ├── IntegrationGateway.Adapters.MC4/ # MC4.0 适配器 ├── IntegrationGateway.Adapters.Kms/ # 钥匙柜适配器 └── IntegrationGateway.Host/ # Minimal API 宿主(Program.cs 极简启动) ``` ### 6.2 七大能力接口 - `IGatewayAdapter`:基础接口(AdapterCode / DisplayName / Initialize / HealthCheck) - `IHasFlatDevices`:扁平设备列表(Owl/门禁/道闸) - `IHasOwnDeviceTree`:自有对象树(MC4.0) - `IHasPoints`:实时点位值(MC4.0 动环) - `IHasStreams`:视频取流/云台/录像 - `IHasAlarms`:告警查询/确认/结束 - `IHasRecordings`:录像文件列表 - `IAcceptsControl / IAcceptsDataSync / IAcceptsMetadataPush / IHasBusinessLogs`:反方向能力 ### 6.3 适配器能力矩阵 | 能力 | Owl | MC4.0 | KMS | |---|:---:|:---:|:---:| | IHasFlatDevices | ✅ | ❌ | ✅ | | IHasOwnDeviceTree | ❌ | ✅ | ❌ | | IHasPoints | ❌ | ✅ | ❌ | | IHasStreams | ✅(仅 continuous PTZ) | ❌ | ❌ | | IHasAlarms | ⚠️(AI 事件可选) | ✅ | ✅ | | IAcceptsControl | ✅ | ✅ | ✅(远程授权开门) | | IAcceptsDataSync | ✅ | ❌ | ✅(员工同步) | ### 6.4 A/B 两组接口 **A 组(网关→VolPro 反向调用)**:`/api/gateway/{register,heartbeat,sync/devices,sync/alarms}`,用 `NodeToken` 二次认证 **B 组(VolPro/管理端→网关)**:14 个 REST 端点,涵盖健康检查、设备列表、对象树、实时值/控制、视频取流/PTZ、告警查询/确认、业务日志、数据同步/删除 ### 6.5 关键技术决策 - **限流令牌桶**:KMS 5 QPS(避免触发对方限流) - **自动重注册**:心跳连续失败 3 次(45s)触发 A1+A3 全量重同步 - **局域网 IP 自动检测**:避免 `localhost` 导致前端不可达 - **JsonSerializerOptions 大小写不敏感**:适配各子系统的字段命名差异 --- ## 七、VolPro 后端业务模块 通过 `api_sqlsugar/VolPro.WebApi/Controllers/` 可见: - **Warehouse/**(本项目核心):设备、点位、区域、钥匙、巡更、车辆、访客、对讲、报警、规则引擎、报表等 - **Sys/**:系统管理(用户/角色/部门/字典/工作流/通知/Quartz) - **MES/**:制造执行(生产订单/物料/工序/报工)— VolPro 框架自带 - **Report/**:报表服务 **新增的关键 Controller**(Partial/ 目录下,避免代码生成器覆盖): - `Partial/base_deviceController.cs`:统一设备 CRUD - `Partial/gateway_nodesController.cs`:A 组接口(注册/心跳/同步) - `Partial/iot_alarmController.cs` / `iot_devicedataController.cs` / `video_channelController.cs` / `video_recordController.cs` - `TaskController.cs`:4 个定时任务(设备同步/心跳监控/实时轮询/规则引擎) --- ## 八、关键工作流 ### 8.1 网关启动流程 1. 读取 `appsettings.json` 配置(Owl/MC4/KMS/Gateway 节点) 2. 反射创建 Adapter 实例,注册到 `AdapterRegistry` 3. `InitializeAllAsync()` 并行初始化(获取 Token) 4. **A1 注册** → VolPro 返回 NodeId 和已存在设备 5. **A3 设备同步** → 遍历每个 Adapter 收集设备 → 推送到 VolPro 6. **A2 心跳循环**(15s),连续失败 3 次自动重注册 ### 8.2 设备同步的"字段分治" - **首次入库**(DeviceId=0):全量写入(Name/Category/Group) - **后续同步**:只更新网关字段(IsOnline/IsParent/ParentDeviceId/ExtraData/IpAddress/Port/LastSyncTime) - **管理员字段**:永不被网关覆盖(Name/Category/Group/PointId/Location/MapModelId 等) ### 8.3 视频流转发链路 `Owl 设备 GB28181 → Owl GB 接入 → Owl 内部 ZLMediaKit (8000) → Owl 提供 WS-FLV/HLS → 网关 B6a 透传地址 → 浏览器 Jessibuca/WebCodec 播放` ### 8.4 实时数据推送链路 `MC4.0 设备 → Mc4Adapter GetRealtimeValues → VolPro 实时轮询任务(10s) → iot_devicedata 落库 → SignalR 推送 → warehouse 端实时看板刷新` --- ## 九、关键文档与里程碑 ### 9.1 设计/整合方案(`doc/`) | 文档 | 状态 | 内容 | |---|---|---| | `SecMPS_最终整合方案_v3.0.md` | **现行版本** | 总体架构、6 张表、AdapterCode 规范、字段分治、Phase 0-4 路线 | | `SecMPS_整合项目实施手册_v2.1.md` | **现行版本** | Git Squash 分支策略、每日检查清单、端口分配 | | `KMS钥匙柜整合方案_v2.0.md` | 现行版本 | KMS 50+ 端点、Phase 1/2 接入路径 | | `对接网关设计文档.md` | 现行版本 | IntegrationGateway 详细设计 | | `VolPro框架改造方案.md` | 进行中 | VolPro 框架适配 | | `客户端改造方案v1.0.md` | 进行中 | 客户端适配层设计 | ### 9.2 检查报告 - `网关KMS模块检查报告20260604.md` - `网关MC4模块检查报告20260603.md` + 整改方案 - `网关Owl模块检查报告20260603.md` + 整改方案 - `网关自动注册机制检查报告20260603.md` + 整改方案 - `代码审核20260604.md` / `网关代码审核20260604.md` ### 9.3 路线图(来自实施手册 v2.1) | 阶段 | 工期 | 内容 | |---|---|---| | Phase 0 | Day 1-2 | Gateway 骨架 + 6 张表 + 代码生成 + 字典初始化 | | Phase 1 | Day 3-6 | OwlAdapter + 视频设备页(Jessibuca)| | Phase 2 | Day 7-11 | Mc4Adapter + IoT 管理 + 区域树 + SignalR | | Phase 3 | Day 12-17 | warehouse 端改造 + 全链路联调 | | Phase 4 | Day 18-20 | 验证 + 缓冲 | --- ## 十、端口分配(实施手册附录 B) | 服务 | 端口 | |---|---| | IntegrationGateway | 5100 | | VolPro.WebApi | 9100 | | web.vite 管理端 | 9000 | | warehouse 大屏 | 9200 | | Owl 管理端 | 80 / 15123 | | ZLMediaKit | 8000 | | MC4.0 | 3000 | | KMS | 80(默认) | --- ## 十一、技术亮点与特色 1. **多协议融合**:GB28181 视频 + 海康私有协议(MC4.0) + HTTP REST(KMS) 统一抽象 2. **JSON 兜底设计**:`ExtraData` 字段让新适配器零改动接入,避免反复改表 3. **网关心跳自愈**:连续失败自动重注册 + 全量重同步,无需人工介入 4. **字段分治**:管理员字段与网关字段分离,避免数据互相覆盖 5. **6A 工作流**:`warehouse/.trae/rules/project_rules.md` 中定义了 Align→Architect→Atomize→Approve→Automate→Assess 完整 AI 协作流程 6. **离线部署包**:`owl_zlmediakit/` 提供完整 ARM64 离线包(飞腾 S5000C 专用) 7. **维度化改造**:通过 Partial/ 目录扩展 VolPro 而不污染生成代码 8. **能力接口最小化**:7 个接口覆盖所有子系统,新增适配器只需"挑接口+实现" --- ## 十二、潜在风险点(来自检查报告喵) 1. **Dockerfile 基础镜像版本不匹配**:`aspnet:6.0` vs `Program.cs` 的 `webHost.UseUrls` 和 .NET 8 代码 — ⚠️ 需要确认 2. **Owl PTZ 限制**:仅支持方向移动+停止,不支持预设位/绝对/相对定位(ONVIF PTZ 未实现) 3. **规则引擎桩实现**:`RuleEngineService.EvaluateAllAsync()` 抛 `NotImplementedException`(接口文档 v1.0 中明确标注) 4. **仓库/管理端两套前端**:存在一定代码重复,未来需评估统一可能性 5. **多网关多实例**:需注意 `NodeCode` 全局唯一性,配置需严格管理 --- ## 十三、关键模块定位(备查链接) - 后端网关 API 入口:[VolPro.WebApi/Program.cs](file:///d:/Code/SecMPS/api_sqlsugar/VolPro.WebApi/Program.cs) - 集成网关宿主:[IntegrationGateway.Host/Program.cs](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Host/Program.cs) - KMS 适配器实现:[KmsAdapter.cs](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Adapters.Kms/KmsAdapter.cs) - 统一设备主 Controller:[base_deviceController.cs](file:///d:/Code/SecMPS/api_sqlsugar/VolPro.WebApi/Controllers/Warehouse/Partial/base_deviceController.cs) - 网关节点 Controller:[gateway_nodesController.cs](file:///d:/Code/SecMPS/api_sqlsugar/VolPro.WebApi/Controllers/Warehouse/Partial/gateway_nodesController.cs) - 仓库大屏路由:[router/index.ts](file:///d:/Code/SecMPS/warehouse/src/router/index.ts) - 视频墙组件:[VideoWall.vue](file:///d:/Code/SecMPS/warehouse/src/view/video/VideoWall.vue) - 整合方案(现行):[SecMPS_最终整合方案_v3.0.md](file:///d:/Code/SecMPS/doc/整合方案/SecMPS_最终整合方案_v3.0.md) - 实施手册(现行):[SecMPS_整合项目实施手册_v2.1.md](file:///d:/Code/SecMPS/doc/整合方案/SecMPS_整合项目实施手册_v2.1.md) - 网关设计文档:[对接网关设计文档.md](file:///d:/Code/SecMPS/doc/设计文档/对接网关设计文档.md) - 网关接口文档:[VolPro网关相关接口文档_v1.0.md](file:///d:/Code/SecMPS/doc/设计文档/VolPro网关相关接口文档_v1.0.md) --- ## 十四、浮浮酱的理解小结 这套项目的核心价值在于**"用统一抽象层屏蔽异构子系统的差异"**,让上层(VolPro 业务后台 + 仓库大屏)不需要关心下面接的是海康、Owl、MC4.0 还是 KMS。这种 **Adapter Pattern + 统一数据模型** 的设计非常优雅,扩展性极强(接入海康门禁预计 1-2 天工作量)。 技术上浮浮酱觉得最巧妙的是: - **AdapterCode 双段标识**(`MC4:31ku`)解决了同类型多实例问题 - **ExtraData JSON** 让 schema 演进不需要改库 - **字段分治** 解决了"管理员改名字不被网关覆盖"、"网关状态更新不污染人工数据"这个老大难 --- ## 附录:项目文档管理 ### 文档管理规范 - 本文档作为项目全生命周期唯一管理载体 - 每次重新打开项目前,**必须先阅读本文档**,确认当前进度与要求后再启动开发 - 项目计划调整、进度节点变更时,**需即时更新文档对应模块** - 每完成一项工作任务(功能开发、BUG 修复),**需立即在文档进度记录中标记完成并补充结果说明** ### 文档结构说明 本文档按"项目总览→技术栈→架构→模块→工作流→里程碑→端口→风险"的逻辑组织,便于快速理解和查阅。 ### 进度记录 > 待项目启动后按 Phase 推进时补充 | 时间 | 阶段 | 内容摘要 | 状态 | |---|---|---|---| | 2026-07-05 | 项目梳理 | 浮浮酱完成项目深度扫描并生成本说明文档 | ✅ 已完成 | | 2026-07-05 | dahua_demo_cs 现场调试 | 主人到现场,测试连接 192.168.3.79 报 "Not Implemented!" | ✅ 已定位根因 | | 2026-07-05 | dahua_demo_cs BUG 修复 | 严格按《DAHUA_HTTP_API 协议规范 V3.99》修正 CGI 路由 | ✅ 已完成 | | 2026-07-21 | V1.0 大华整合 Stage 0-7 | 共享类库 Dahua.Client + Adapter + B14-B16 + Controller + web.vite 4 新页面 + 5 项目编译 0 错误 | ✅ 已完成 | | 2026-07-21 | V2.0 warehouse mockData 清零 | 6 个文件去除 mockData,对接 gateway B8 真实接口 + 60s 自动刷新 | ✅ 已完成 | | 2026-07-21 | warehouse 推送 R3 修复 V1.1.1 | async 化、store 优先、http.post 兜底、return 早退、withAutomaticReconnect + encodeURIComponent | ✅ 已完成 | | 2026-07-21 | warehouse 推送 R3 复查 V1.1.2 | 主人实地核查:发现 http 兜底路径 BUG(`result?.userName` 应为 `result?.data?.userName`),已修正 + npm run build 验证通过 | ✅ 已完成 | | 2026-07-21 | gateway ↔ api_sqlsugar 对接复查 V1.2 | 主人复查:发现 3 个 BUG(DahuaAutoUpload.BaseUrl 端口错 9100→5000;DahuaUploadController 端点路径错 /uploads/* → /api/auto-upload/*;SignalR 路径改用绝对地址),全部已修 + 双端编译 0 错误 | ✅ 已完成 | | 2026-07-21 | 大华适配器主动上传接收 V1.2 | 主人要求:实现接收设备主动上传 + 过滤 + 解码 + 推送到 api_sqlsugar。新增 IAcceptsAlarmPush 接口、DahuaAdapter 实现 4 个维度(multipart/JSON/key=value + 文本协议)、Code 白名单过滤、调用 A4 sync/alarms 推 VolPro。Gateway 0 错误 | ✅ 已完成 | | 2026-07-21 | 部署配置说明文档 v1.0 | 主人要求:写部署和配置说明,重点大华设备配置。已保存到 `d:\Code\SecMPS\doc\SecMPS_部署和配置说明_v1.0.md`(含架构图/端口表/启动顺序/4 端配置详解/主动上传端到端验证/7 项 FAQ) | ✅ 已完成 | | 2026-07-21 | 4 项目 2 轮检查修复 V1.2 | 主人要求:api_sqlsugar/gateway/warehouse/web.vite 4 项目 2 轮检查(ref Vol Skill)。第1轮:3 BUG(AutoUploadViewer 调已删端点/配置错 IP 110→108/etc)。第2轮:发现 main.js 被改影响升级,提取到 plugins/extension-dahua.js。记录 [SecMPS_框架升级注意事项_v1.0.md](file:///d:/Code/SecMPS/doc/SecMPS_框架升级注意事项_v1.0.md)。全部 0 错误 0 警告 | ✅ 已完成 | | 2026-07-23 | **warehouse 推送接收重写 V1.3** | 主人反馈:"控制台看到'已经连接消息中心'但 handlePushMessage 内的 console.log 不打印"。根因分析:原 V1.1.2 实现有 3 层复杂度叠加(store 优先/http 兜底的双路径 + startWithRetry 5 次重试不 await + LRU 拦截),`console.log('已经连接消息中心')` 实际在 `initMessageHub` 同步执行到第一个 await 时就打印了,并不代表 SignalR 连接成功。**对照 web.vite MessageConfig.js 重写**:(1)总是用 http.post 拿 userName(去掉 store 兜底双路径);(2)去掉 LRU 拦截(后端推送的 data 没有 id 字段,LRU 永远不触发);(3)去掉 startWithRetry,改用 `await connection.start()` 等待结果,失败立即 console.error;(4)每个关键步骤加 console.log 便于主人调试(GetCurrentUserInfo 返回/userName/hubUrl/连接成功/收到推送);(5)Main.vue + dataview.vue + DataView.vue 三个 onMounted 改 async + await,'已经连接消息中心' 移到真正连接成功后才打印,handlePushMessage 移到 onMounted 之前避免 TDZ 隐患。`npm run build warehouse` ✓ built in 22.13s 0 错误 | ✅ 已完成 | | 2026-07-23 | **VideoWall 不显示修复 V1.4** | 主人反馈:"视频墙之前能显示,现在不显示(截图:3x3 grid 区域完全空白,但右半部分 settings-bar 都正常)"。V1.3 推送接收改的 4 个文件(index.js / Main.vue / dataview.vue / DataView.vue)经核对跟视频墙**无关**(推送接收模块,VideoWall 不 import),VideoWall 自身代码也正常(v-for 渲染 + CSS 布局正确)。**根因:V1.2 阶段 B.3 修复的 BUG 没改完**——V1.2 阶段浮浮酱把 `gateway/src/IntegrationGateway.Host/appsettings.json` 的 `SelfUrl` 从 `.110:5100` 改成 `.108:5100`(跟主人电脑 IP 一致),但**忘了同步改仓库前端的 [apiConfig.js:21](file:///d:/Code/SecMPS/warehouse/public/apiConfig.js#L21)** —— `gatewayUrl` 仍指向 `http://192.168.3.110:5100`。`VideoWall.vue:79` 调 `gwGet('/api/gateway/devices?adapter=Owl:main&page=1&size=100')` 走 `GW_BASE = apiConfig.gatewayUrl`([gateway.ts:6](file:///d:/Code/SecMPS/warehouse/src/api/gateway.ts#L6)),请求 `.110:5100` 失败 → `cameras` 加载失败 → v-for 不渲染 → 视频墙 grid 区域空白。**修法**:`apiConfig.js:21` 改成 `http://192.168.3.108:5100`,跟 V1.2 修复的 `SelfUrl` 同步。`npm run build warehouse` ✓ built in 23.93s 0 错误 | ✅ 已完成 | | 2026-07-23 | **Filter.vue API 改造 V1.5** | 主人要求:"把 [Filter.vue:74](file:///d:/Code/SecMPS/warehouse/src/components/Filter.vue#L74) 的 `/api/Warehouse_Device/GetPageData` 改成 `/api/base_device/getPageData`,用来获取真正连接到 gateway 的设备列表,并按 [base_device_response.json](file:///d:/Code/SecMPS/warehouse/doc/base_device_response.json) 解析"。**前后差异**:(1)旧 API `Warehouse_Device` 响应有 `status=0` 和 `DeviceType` 字段;(2)新 API `base_device` 响应**无 status 字段**(直接 `JsonNormal(new { total, rows })`,参考 [base_deviceController.cs:215](file:///d:/Code/SecMPS/api_sqlsugar/VolPro.WebApi/Controllers/Warehouse/base_deviceController.cs#L215)),且**无 DeviceType 字段**(base_device 实体里只有 `DeviceCategory`/`DeviceGroup`/`IsParent`/`IsOnline` 等,参考 [base_device.cs:163](file:///d:/Code/SecMPS/api_sqlsugar/VolPro.Entity/DomainModels/device_manager/base_device.cs#L163));(3)filter 字段名 `MapModuleID` → `MapModelId`(base_device 实体用的字段名)。**改造点**(共 4 处):(a)`requestData.filter[0].name` `MapModuleID` → `MapModelId`;(b)URL 改 `/api/base_device/getPageData`;(c)去掉 `data.status === 0` 检查(直接判 `data && data.rows && data.rows.length > 0`);(d)去掉 `deviceType = data.rows[0].DeviceType` 引用(旧表独有),同时增加 `deviceCategory/deviceGroup/isOnline` 三个 base_device 字段的日志输出便于调试。`npm run build warehouse` ✓ built in 24.07s 0 错误 | ✅ 已完成 | | 2026-07-23 | **设备同步改为 Gateway 端定时 V1.6** | 主人要求:"目前刷新设备信息好像是由 vol.pro 端发起的,看看能不能改为由 gateway 端定时发起更新,更新间隔在 gateway 的配置文件中指定"。**调研发现 3 个问题**:(1)当前 A3 推送只在 Gateway 启动时调一次 `await SyncAllDevicesAsync(...)`([Program.cs:146](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Host/Program.cs#L146)),之后设备状态不会自动更新;(2)VolPro 端 T1 任务([TaskController.cs](file:///d:/Code/SecMPS/api_sqlsugar/VolPro.WebApi/Controllers/Warehouse/TaskController.cs) `syncDevices` 端点)虽然每 5 分钟跑一次,但调用的是 gateway B3 端点([Program.cs:532-548](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Host/Program.cs#L532-L548)),**B3 handler 只回数量不传数据**——T1 任务**完全失效**;(3)base_device 表的 IsOnline/LastSyncTime 永远是启动时那一刻的状态。**整改方案**(主人确认 3 点:①间隔 300 秒 ②删除 T1 任务 ③保留启动立即同步):(a)**新增 [DeviceSyncBackgroundService.cs](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Host/DeviceSyncBackgroundService.cs)**:`BackgroundService` 子类,用 `PeriodicTimer` 定时调 `SyncAllDevicesAsync`,间隔从 `appsettings.json` 的 `Gateway:DeviceSyncIntervalSec` 读取(默认 300 秒);(b)**[Program.cs:185-190](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Host/Program.cs#L185-L190)** 注册 `AddHostedService`(闭包注入 `SyncAllDevicesAsync`);(c)保留 [Program.cs:146](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Host/Program.cs#L146) 启动时 `await SyncAllDevicesAsync(...)`(主人要求);(d)保留 [Program.cs:172](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Host/Program.cs#L172) A2 心跳失败重注册时的同步(兜底);(e)**[appsettings.json:49](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Host/appsettings.json#L49)** 新增 `DeviceSyncIntervalSec: 300` 配置项;(f)**[TaskController.cs](file:///d:/Code/SecMPS/api_sqlsugar/VolPro.WebApi/Controllers/Warehouse/TaskController.cs)** 删除 T1 `SyncDevices` 端点(注释保留说明)。**验证**:`dotnet build gateway` 0 错误 0 警告;`dotnet build api_sqlsugar` 0 错误(4 个警告为项目原有 nullable 警告与本次改动无关) | ✅ 已完成 | | 2026-07-23 | **Gateway Sync 启动报错修复 V1.6.1** | 主人反馈:gateway 启动报错 `System.InvalidOperationException: The service collection cannot be modified because it is read-only`。**根因**:V1.6 把 `AddHostedService` 放在 A2 心跳启动之后(约 line 185),但 `var app = builder.Build();` 已在 line 50 执行过,`builder.Services` 在 build 后变成只读,调用 `Add` 会抛 `ThrowReadOnlyException()`。这是顶级语句 Program.cs 的经典坑:**所有 `Services.AddXxx()` 必须在 `builder.Build()` 之前调用**,而运行时依赖(registry/clientFactory/nodeCode/selfUrl)在 build 之后才创建。**修法**(静态 holder 模式):(a)**新增 [GatewaySyncRunner.cs](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Host/GatewaySyncRunner.cs)**:静态类 + 静态字段(Registry/ClientFactory/NodeCode/NodeToken/SelfUrl),提供 `Initialize(...)` 赋值方法和 `SyncAllDevicesAsync()` 同步方法(从 Program.cs 提取的 `SyncAllDevicesAsync` + `FlattenTree` 逻辑搬到这里),`Initialize` 带 `_initialized` 守门标志,BackgroundService 早于 Initialize 启动时返回 false 而不抛异常;(b)**重构 [DeviceSyncBackgroundService.cs](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Host/DeviceSyncBackgroundService.cs)**:构造函数从 `(IConfiguration, ILogger, Func)` 简化为 `(IConfiguration, ILogger)`,去掉 Func 委托依赖;(c)**[Program.cs:45-48](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Host/Program.cs#L45-L48)** `AddHostedService` 移到 `builder.Build()` 之前;(d)**[Program.cs:155](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Host/Program.cs#L155)** 在 A1 注册完成后(selfUrl 确定后)调 `GatewaySyncRunner.Initialize(registry, clientFactory, nodeCode, nodeToken, selfUrl)`;(e)**[Program.cs:159](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Host/Program.cs#L159) + [line 185](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Host/Program.cs#L185)** 把原 `await SyncAllDevicesAsync(...)` 调用改为 `await GatewaySyncRunner.SyncAllDevicesAsync()`;(f)**删除 Program.cs 本地 `SyncAllDevicesAsync` 和 `FlattenTree` 函数**(已迁移到 GatewaySyncRunner)。**验证**:`dotnet build gateway` 0 错误 0 警告。**教训**:顶级语句 `Program.cs` 中 `IServiceCollection` 生命周期严格——`Add*` 必须在 `Build()` 前;运行时构造的对象用静态 holder / DI 单例两种方式共享给后续组件 | ✅ 已完成 | | 2026-07-23 | **Dahua 适配器配置加载失败修复 V1.7** | 主人反馈:"gateway 的 DAHUA 适配器配置了 7 个门禁,但没有向后端提交任何大华设备,网关日志中也没有相关的工作日志"。**根因 3 个并发问题**:(1)**JSON 是数组 `[{InstanceName, Devices}]` 但代码期望对象 `DahuaAdapterOptions`(InstanceName/Devices 是属性)**——[Program.cs:44](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Host/Program.cs#L44) `Configure(builder.Configuration.GetSection("Dahua"))` 绑定会**完全失败**,`_adapterOptions.Devices` 始终 `null`;(2)**DahuaAdapter 注册模式与其他适配器不一致**——Owl/KMS/MC4 都用 `foreach` 遍历 `app.Configuration.GetSection("Xxx").Get>()`([Program.cs:95-130](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Host/Program.cs#L95-L130)),唯独 Dahua 走 `IOptions<>` DI 注入;(3)**`ReloadDevices` 静默失败**——`_adapterOptions.Devices?.ToList() ?? new List<...>()` 配置为空时**无任何警告日志**,难调试。**整改方案**(主人确认:①配置在服务器上不动 ②改法按方案 A ③日志格式与其它适配器一致):(a)**[DahuaAdapter.cs:42-68](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Adapters.Dahua/DahuaAdapter.cs#L42-L68)** 主构造函数 + 兼容构造函数去 `IOptions<>` 包装,改为直接接受 `DahuaAdapterOptions`/`DahuaOptions` 对象;(b)**删除 [Program.cs:44](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Host/Program.cs#L44) `Configure` DI 绑定**;(c)**[Program.cs:68-92](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Host/Program.cs#L68-L92)** DahuaAdapter 注册改为 `foreach` 遍历模式(与 Owl/KMS/MC4 完全一致),单实例失败不影响其它实例;(d)**[DahuaAdapter.cs:108-118](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Adapters.Dahua/DahuaAdapter.cs#L108-L118)** `ReloadDevices` 加 `Console.Error.WriteLine` 警告,配置为空时给出明显提示"appsettings.json → Dahua:{InstanceName} 段未配置 Devices 数组";(e)**日志格式统一**(与 Owl/KMS/MC4 适配器一致):`[{AdapterCode}] 中文描述,字段={字段}` + `_logger.LogDebug` 普通日志 + `Console.Error.WriteLine` 错误日志;改造点共 9 处([DahuaAdapter.cs:98](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Adapters.Dahua/DahuaAdapter.cs#L98) InitializeAsync / [line 113-115](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Adapters.Dahua/DahuaAdapter.cs#L113-L115) ReloadDevices / [line 175](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Adapters.Dahua/DahuaAdapter.cs#L175) GetDevicesAsync / [line 192/222](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Adapters.Dahua/DahuaAdapter.cs#L192-L222) GetAlarmsAsync / [line 249/255](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Adapters.Dahua/DahuaAdapter.cs#L249-L255) SendControlAsync / [line 323](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Adapters.Dahua/DahuaAdapter.cs#L323) GetAccessRecordsAsync / [line 380](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Adapters.Dahua/DahuaAdapter.cs#L380) GetIntercomDevicesAsync / [line 392/400](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Adapters.Dahua/DahuaAdapter.cs#L392-L400) StartCallAsync / [line 444/484](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Adapters.Dahua/DahuaAdapter.cs#L444-L484) ReceiveAlarmPushAsync);(f)**说明**:服务器上配置 7 个门禁,本次不改配置。**验证**:`dotnet build gateway` 0 错误 0 警告 6.34s。**预期效果**:服务器 7 门禁部署后启动日志会输出 `DahuaAdapter 已注册: Dahua:main (设备数: 7)` + `[{AdapterCode}] 加载设备配置,共 7 台` + `A3 收集到 N 台设备开始推送到 Vol.Pro` | ✅ 已完成 | | 2026-07-23 | **顶层设备 ParentDeviceId 未写 0 修复 V1.8** | 主人反馈:"后端接收到网关提交的设备信息并写入数据库时没有将顶层设备的 ParentDeviceId 字段写 0,导致前端加载树形列表时无法加载出 ParentDeviceId 字段为空的设备"。**根因链 3 段**:(1)**网关端 [DahuaAdapter.cs:147-177](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Adapters.Dahua/DahuaAdapter.cs#L147-L177) GetDevicesAsync 没给 IsParent 赋值**——C# bool 默认值 = false,**Owl/KMS 适配器同样问题**(只有 MC4 通过 `n.Type == 1` 区分父/子);(2)**后端 [gateway_nodesService.cs:181](file:///d:/Code/SecMPS/api_sqlsugar/Warehouse/Services/device_manager/Partial/gateway_nodesService.cs#L181) SyncDevicesAsync 新增分支** `ParentDeviceId = d.IsParent ? 0 : parentDeviceId`——当 `d.IsParent=false`(网关默认)且 `d.ParentSourceId=null`(顶层设备),`parentDeviceId=null` → 写 NULL;更新分支 [line 203](file:///d:/Code/SecMPS/api_sqlsugar/Warehouse/Services/device_manager/Partial/gateway_nodesService.cs#L203) 同样问题;(3)**前端 [base_deviceController.cs:175](file:///d:/Code/SecMPS/api_sqlsugar/VolPro.WebApi/Controllers/Warehouse/Partial/base_deviceController.cs#L175) GetTreeTableRootData 过滤** `x.ParentDeviceId == 0`——SQL 中 `NULL == 0` 永假,**所有 NULL 顶层设备被过滤**。**整改方案**(主人确认改 1+2):(a)**主修 1:[gateway_nodesService.cs:181 + 203](file:///d:/Code/SecMPS/api_sqlsugar/Warehouse/Services/device_manager/Partial/gateway_nodesService.cs#L181) 两处三元判断**增加 `|| string.IsNullOrEmpty(d.ParentSourceId)` 兜底条件——`d.IsParent || string.IsNullOrEmpty(d.ParentSourceId) ? 0 : parentDeviceId`(顶层设备的本质特征是 ParentSourceId 为空);(b)**防御 2:[base_deviceController.cs:178](file:///d:/Code/SecMPS/api_sqlsugar/VolPro.WebApi/Controllers/Warehouse/Partial/base_deviceController.cs#L178) GetTreeTableRootData 过滤条件**增加 `|| x.ParentDeviceId == null`,兼容历史 NULL 残留数据,让前端即使不重跑 A3 也能加载出残留 NULL 顶层设备。**验证**:`dotnet build api_sqlsugar` 0 错误(4 个警告为项目原有 nullable 警告与本次改动无关) | ✅ 已完成 | | 2026-07-23 | **大华主动上传图片保存 V1.9** | 主人要求:"连服务器看数据库结构是否需要新建表/检查图片保存功能/增加图片保存+数据库存路径"。**3 步调研结论**:(1)**现有 7 张表与服务器 `gljs_service` 完全一致**——[base_device / video_channel / video_record / iot_devicedata / iot_alarm / gateway_nodes / warehouse_variable] 表结构、索引、约束全部 OK,**仅 db_init.sql 第 1-6 张表无需改动**;(2)**现有 `warehouse_realtimealert` 表不适合**——该表只存文本消息,**无图片字段、无视频字段、SourceAlarmId 也无唯一索引**,需要新建专用表;(3)**gateway 主动上传当前把 base64 透传给 VolPro 落库为 TEXT 字段**——查 [DahuaAlarmService](file:///d:/Code/SecMPS/dahua_demo_cs/server/Dahua) 流程确认图片以 base64 字符串形式落库,**无法直接用于 显示**。**主人 3 步决策**(含确认 1 + 2 点的范围明确):(a)**主人确认点 1:db_init.sql 同步范围** = 脚本中已有的 7 张表与服务器同步(脚本有的照搬,脚本没有的服务器表不补);(b)**主人确认点 2:新建表范围** = 不用 `warehouse_realtimealert`,**自己重新建一个表**,**增加到 db_init.sql**,**与 base_device 表做关联(DeviceId FK)便于按设备过滤**;(c)**主人补充:"不删除原有表,只新增"**——db_init.sql 用 `CREATE TABLE IF NOT EXISTS`,不 `DROP`。**整改方案**(共 6 文件改动 + 1 文件新增):(1)**[db_init.sql:174-199](file:///d:/Code/SecMPS/doc/db_init.sql#L174-L199) 新增第 7 张表 `dahua_realtime_alert`**:16 字段 + 5 索引(UNIQUE(AdapterCode,SourceAlarmId) 去重 / IX_Device 按设备过滤 / IX_Time 时间排序 / IX_Type 事件类型 / IX_Adapter 适配器统计),`DeviceId INT NULL` 与 base_device 关联(可空:未知设备时),4 个图片路径字段 + 1 视频 URL + EventData JSON 完整备份;(2)**服务器 `gljs_service.dahua_realtime_alert` 已成功建表**(远程执行 17 字段 + 5 索引,与脚本一致);(3)**gateway 端**——[StandardRealtimeAlert.cs](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Core/Models/StandardRealtimeAlert.cs) 新建 DTO 透传 4 个图片 base64 + VideoUrl;[IAcceptsAlarmPush.cs](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Core/Abstractions/IAcceptsAlarmPush.cs) `AlarmPushPayload` 扩展 8 个字段(FacePicBase64 / BodyPicBase64 / VideoUrl / Message / Level / Value / SourceId / OccurTime / DataJson / PushType);[GatewayClientFactory.cs](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Core/Infrastructure/GatewayClientFactory.cs) 新增 `SyncDahuaRealtimeAlertsAsync` 方法(A5 端点 `/api/gateway/sync/dahua-realtime-alerts`),HTTP 非 2xx 抛 `HttpRequestException` 带状态码+响应体;[DahuaAdapter.cs:443-509](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Adapters.Dahua/DahuaAdapter.cs#L443-L509) `ReceiveAlarmPushAsync` 改造——构造 `StandardRealtimeAlert` 透传图片 base64 + VideoUrl,调新方法,简化 `JsonDocument` 响应处理(解析 `code/added/message` 三字段);(4)**VolPro 端**——[dahua_realtime_alert.cs](file:///d:/Code/SecMPS/api_sqlsugar/VolPro.Entity/DomainModels/device_manager/dahua_realtime_alert.cs) 新建实体(16 字段 + `[Navigate(NavigateType.OneToOne, nameof(DeviceId))] base_device` 关联 + partial 类);[Idahua_realtime_alertRepository.cs](file:///d:/Code/SecMPS/api_sqlsugar/Warehouse/IRepositories/device_manager/Idahua_realtime_alertRepository.cs) + [dahua_realtime_alertRepository.cs](file:///d:/Code/SecMPS/api_sqlsugar/Warehouse/Repositories/device_manager/dahua_realtime_alertRepository.cs) 新建仓储(`RepositoryBase` + `IDependency`);[Idahua_realtime_alertService.cs](file:///d:/Code/SecMPS/api_sqlsugar/Wareware/IServices/device_manager/Idahua_realtime_alertService.cs)(2 个文件:non-partial 接口 + Partial 接口声明 `InsertIfNotExistsAsync`)+ [dahua_realtime_alertService.cs](file:///d:/Code/SecMPS/api_sqlsugar/Warehouse/Services/device_manager/dahua_realtime_alertService.cs)(2 个文件:non-partial 实现 + Partial 实现按 `(AdapterCode, SourceAlarmId)` 去重落库);(5)**[ImageStorageService.cs](file:///d:/Code/SecMPS/api_sqlsugar/VolPro.WebApi/Controllers/Warehouse/ImageStorageService.cs) 新建图片保存服务**——`SaveAsync(base64, type, eventId)` 解码 → 自动识别 `data:image/xxx;base64,` 前缀 → 8MB 大小校验 → 扩展名白名单 → 写 `bin/Download/Screenshots/yyyy-MM/{eventId}_{type}.{ext}` → 返回相对路径(如 `2026-07-23/abc_pic.jpg`),失败返回 null 不影响整体;`SaveAllAsync` 批量保存 4 张图返回 tuple;`DahuaAlertImageStorageOptions` 配置类(RootDirectory/UrlBase/MaxBytes/AllowedExts)通过 `IOptions<>` 注入;(6)**[gateway_nodesController.cs:202-294](file:///d:/Code/SecMPS/api_sqlsugar/VolPro.WebApi/Controllers/Warehouse/Partial/gateway_nodesController.cs#L202-L294) 新增 A5 端点** `/api/gateway/sync/dahua-realtime-alerts`——NodeToken 二次认证 → 遍历 `req.Alerts` → DeviceSourceId→DeviceId 映射(按设备过滤) → `ImageStorageService.SaveAllAsync` 4 张图 → 构造实体 → `InsertIfNotExistsAsync` 去重落库 → 返回 `{code, message, added, skipped, failed}`;构造注入扩展为 7 个参数(+`Idahua_realtime_alertService` + `ImageStorageService` + `ILogger`);(7)**[Program.cs:175-177](file:///d:/Code/SecMPS/api_sqlsugar/VolPro.WebApi/Program.cs#L175-L177) DI 注册**——`Configure(GetSection("DahuaAlertImageStorage"))` + `AddSingleton()`;(8)**[appsettings.json:17-27](file:///d:/Code/SecMPS/api_sqlsugar/VolPro.WebApi/appsettings.json#L17-L27) 新增 DahuaAlertImageStorage 配置节**——RootDirectory 空则用 `bin/Download/Screenshots` 默认(与 FileServiceController 一致),UrlBase `/api/gateway/screenshots` 直接对应 [FileServiceController.cs:23](file:///d:/Code/SecMPS/api_sqlsugar/VolPro.WebApi/Controllers/Warehouse/FileServiceController.cs#L23) GetScreenshot 端点,前端可直接 `` 拼 UrlBase + 相对路径。**验证**:`dotnet build gateway` 0 错误 6 警告(历史遗留);`dotnet build api_sqlsugar` 0 错误 6 警告(项目原有 nullable 警告)。**效果**:(a)大华设备主动推送的图片事件 → gateway 透传 base64 → VolPro 解码写盘到 `bin/Download/Screenshots/yyyy-MM/` → 存相对路径到 `dahua_realtime_alert.PicturePath` 等 4 个字段 → 前端通过 `/api/gateway/screenshots/{year-month}/{eventId}_{type}.{ext}` 直接 `` 展示;(b)按设备过滤:`SELECT * FROM dahua_realtime_alert WHERE DeviceId = ?` 直接走 `IX_Device` 索引;(c)按 (AdapterCode, SourceAlarmId) 去重走 `UNIQUE INDEX IX_Source` 避免重传占空间 | ✅ 已完成 | | 2026-07-23 | **dahua_realtime_alert 加设备类型字段 V1.9.1** | 主人要求:"dahua_realtime_alert 表中需要有设备类型字段,前端需要根据设备类型进行过滤查询"。**方案选型**(V1.9 设计时只冗余 DeviceId 关联,主人在 V1.9 完成后才提出要按设备类型过滤)——**采用方案 B(VolPro 端冗余存 base_device.DeviceCategory)**而不是方案 A(gateway 端查 VolPro)。**理由**:(a)方案 A 让 gateway 反向依赖 VolPro 数据查询,**破坏 gateway 无状态原则**([V1.6 设计原则 1](file:///d:/Code/SecMPS/%E8%AF%B4%E6%98%8E%E6%96%87%E6%A1%A3.md));(b)方案 B 在 VolPro 落库时**顺手查 base_device.DeviceCategory**(之前查 DeviceId 时已经命中 base_device 表,**不增加任何额外查询成本**),写到 `dahua_realtime_alert.DeviceCategory` 字段,前端按设备类型过滤**走 `IX_Category` 索引一次命中**,不必每次 JOIN base_device;(c)base_device.DeviceCategory 是相对稳定的信息(设备类型是字典值,不经常变),冗余字段的同步成本低(同步一次写够,**不需要触发器**维护)。**整改方案**(共 4 文件改动 + 1 文件新增):(1)**[db_init.sql:178 + 195 + 202-231](file:///d:/Code/SecMPS/doc/db_init.sql#L178-L231) ALTER 升级**——在原 CREATE TABLE 段的 DeviceId 后加 `DeviceCategory NVARCHAR(50) NULL COMMENT '设备类型(冗余base_device.DeviceCategory,便于按设备类型过滤)'` + `INDEX IX_Category (DeviceCategory)`(V1.9.1 增量创建时直接含此列);增量升级块用 **INFORMATION_SCHEMA + PREPARE/EXECUTE** 模式:先查 `INFORMATION_SCHEMA.COLUMNS` 看列是否存在(`@col_exists`),存在则 SELECT 提示跳过、不存在则 ALTER ADD;索引同样查 `INFORMATION_SCHEMA.STATISTICS`,**保证脚本可重跑且不会因列/索引已存在而报错**(MySQL 8.0.29+ 的 `ADD COLUMN IF NOT EXISTS` 兼容性问题规避);(2)**服务器 `gljs_service.dahua_realtime_alert.DeviceCategory` 已 ALTER 成功**——`COLUMN_TYPE=varchar(50) IS_NULLABLE=YES COMMENT='设备类型(冗余base_device.DeviceCategory,便于按设备类型过滤)'` + `INDEX_NAME=IX_Category` 验证存在([V1_9_1_alter_dahua_realtime_alert.sql](file:///d:/Code/SecMPS/doc/V1_9_1_alter_dahua_realtime_alert.sql) 增量升级脚本存档);(3)**[dahua_realtime_alert.cs:51-58](file:///d:/Code/SecMPS/api_sqlsugar/VolPro.Entity/DomainModels/device_manager/dahua_realtime_alert.cs#L51-L58) 实体加 DeviceCategory 属性**——`[MaxLength(50)] [Column(TypeName="nvarchar(50)")] [Display(Name="设备类型")] [Editable(true)] public string DeviceCategory`(用 `string` 而非 `string?`,因为 dahua_realtime_alert 表里 DeviceId 可空时 DeviceCategory 也用空串表示"未知"——与 A5 端点赋值 `deviceCategory ?? ""` 对齐);(4)**[gateway_nodesController.cs:234-249](file:///d:/Code/SecMPS/api_sqlsugar/VolPro.WebApi/Controllers/Warehouse/Partial/gateway_nodesController.cs#L234-L249) A5 端点查 DeviceId 时同时查 DeviceCategory**——`Select(x => new { x.DeviceId, x.DeviceCategory })` 一次查询拿两个字段(**不增加查询次数**),赋值给 `string? deviceCategory` 变量,落到 entity.DeviceCategory;(5)**第 5 项 [gateway_nodesController.cs:254](file:///d:/Code/SecMPS/api_sqlsugar/VolPro.WebApi/Controllers/Warehouse/Partial/gateway_nodesController.cs#L254) 实体构造时赋值** `DeviceCategory = deviceCategory ?? ""`(设备类型为空时存空串,便于 SQL `WHERE DeviceCategory = '门禁一体机'` 严格匹配,不用处理 NULL)。**验证**:`dotnet build api_sqlsugar` 0 错误 2 警告(历史遗留 XML 注释警告)。**效果**:(a)前端按设备类型过滤直接 `WHERE DeviceCategory = '门禁一体机'` 走 `IX_Category` 索引;(b)查询响应里 `deviceCategory` 字段直接返回(如 "门禁一体机/摄像机/智能断路器"),前端可做下拉过滤器;(c)冗余存也意味着即使 base_device 改了 DeviceCategory,历史告警的 DeviceCategory **保持告警发生时的设备类型**(数据归档语义正确) | ✅ 已完成 | | 2026-07-23 | **图片按天分目录 + 容器部署配置 V1.9.2** | 主人 3 步要求:(a)"**图片保存的时候应该按日期新建文件夹来保存当天的图片**"——V1.9 当时是按月(yyyy-MM)分目录,主人要求改按天;(b)"**给我解释一下 appsettings.json 中 DahuaAlertImageStorage 的各项设置的用法**";(c)"**服务器上后端是运行在容器里的,挂载卷配置是 /home/volpro->/app,发布的后端有 wwwroot/Upload 文件夹,我想把上传的图片放在这个 Upload 文件夹里,帮我写一套示例配置**"。**整改方案**(共 3 文件改动):(1)**[ImageStorageService.cs:153-154](file:///d:/Code/SecMPS/api_sqlsugar/VolPro.WebApi/Controllers/Warehouse/ImageStorageService.cs#L153-L154) 改按天分目录**——`var subDir = DateTime.Now.ToString("yyyy-MM-dd")`(之前是 yyyy-MM 按月,V1.9.2 改 yyyy-MM-dd 按天);`var relativePath = $"{subDir}/{fileName}"` 返回的相对路径变成 `2026-07-23/abc_pic.jpg` 形式;同时构造函数 [line 84-89](file:///d:/Code/SecMPS/api_sqlsugar/VolPro.WebApi/Controllers/Warehouse/ImageStorageService.cs#L84-L89) 加了 `Path.IsPathRooted` 兜底——配置给的是相对路径时**相对 AppContext.BaseDirectory 解析为绝对路径**(方便开发机用相对路径如 `./wwwroot/Upload`,容器内必须用绝对路径如 `/app/wwwroot/Upload/DahuaAlerts`);`SaveAllAsync` 4 张图仍走同一个按天分目录(同一天的 4 张图落在同一个 yyyy-MM-dd 目录);(2)**[FileServiceController.cs:21-72](file:///d:/Code/SecMPS/api_sqlsugar/VolPro.WebApi/Controllers/Warehouse/FileServiceController.cs#L21-L72) 增强支持双目录 + 子目录路径**——构造函数注入 `ImageStorageService`(之前是空构造函数,V1.9.2 改造为 DI 注入,与 [Program.cs:176](file:///d:/Code/SecMPS/api_sqlsugar/VolPro.WebApi/Program.cs#L176) `AddSingleton()` 配合);路由从 `api/gateway/screenshots/{filename}` 改为 `api/gateway/screenshots/{*filename}`(catch-all 路由,**支持子目录**如 `2026-07-23/abc.jpg`);安全检查**仅禁止 `..` 路径穿越 + 绝对路径前缀 + 盘符前缀**(之前是禁止 `/` 和 `\`,V1.9.2 放宽,因为现在子目录是合法路径);读取逻辑**双目录优先级**——先从 `ImageStorageService.RootDirectory/yyyy-MM-dd/xxx.jpg` 读(新位置),找不到 fallback 到 `AppContext.BaseDirectory/Download/Screenshots/yyy-MM/xxx.jpg`(**V1.9 历史数据兼容**,老数据仍可读);PhysicalFileResult 私有方法抽出 + 加 webp MIME(之前只支持 png/jpg/jpeg/gif);(3)**[appsettings.json:17-43](file:///d:/Code/SecMPS/api_sqlsugar/VolPro.WebApi/appsettings.json#L17-L43) 写容器部署示例配置 + 详细注释**——`RootDirectory: "/app/wwwroot/Upload/DahuaAlerts"`(容器内绝对路径,对应宿主机 `/home/volpro/wwwroot/Upload/DahuaAlerts`,与容器挂载卷 `/home/volpro->/app` 对应);注释里详细解释:① **RootDirectory 用法**——留空=默认 `bin/Download/Screenshots`(开发环境)/ 容器部署=`/app/wwwroot/Upload/DahuaAlerts` / Windows=`D:\\Web\\Upload\\DahuaAlerts` / 相对路径=相对 `AppContext.BaseDirectory` 解析(不推荐容器用);② **UrlBase 用法**——与 `FileServiceController` 路由一致(默认即可,不需要改),前端拼接 `imageUrlBase + row.PicturePath = /api/gateway/screenshots/2026-07-23/abc_pic.jpg`;③ **MaxBytes 用法**——单张最大字节数(默认 8MB),防异常/恶意大图占满磁盘,大华图片通常 200KB-2MB,8MB 够用;④ **AllowedExts 用法**——允许的扩展名(逗号分隔),按 data URI 自动识别,大华只推 jpg/jpeg/png 默认即可,要支持 webp 加进去即可。**验证**:`dotnet build api_sqlsugar` 0 错误 2 警告(历史遗留 XML 注释)。**效果**:(a)按天分目录——每天 1 个新文件夹,**便于按天归档/清理/排查**,避免单月目录文件过多导致 ls 卡顿;(b)容器持久化——图片存到 `/app/wwwroot/Upload/DahuaAlerts`(容器内),自动持久化到宿主机 `/home/volpro/wwwroot/Upload/DahuaAlerts`,**容器重启/升级不丢图**;(c)历史兼容——V1.9 的 bin/Download/Screenshots 数据仍可读(FileServiceController 双目录 fallback),不需要迁移脚本;(d)路由兼容——前端 `` 不变,DB 存的 `2026-07-23/abc_pic.jpg` 拼接 `/api/gateway/screenshots/2026-07-23/abc_pic.jpg` 走 FileServiceController catch-all 路由一次命中 | ✅ 已完成 | | 2026-07-23 | **改走 VolPro 现有 /Upload/ 静态文件服务 V1.9.3** | 主人提出:"既然图片是保存在 Upload 文件夹下了,那前端拼接路径不应该是 `{API_BASE}/Upload/DahuaAlerts/日期/文件名` 吗?为何你写的配置中 UrlBase 是 `/api/gateway/screenshots`?"——**主人直觉是对的**。**浮浮酱反思**:V1.9.2 当时图省事直接用了 V1.9 的 `FileServiceController` 路由,但是**忽略了 VolPro 自身已有静态文件服务**——查 [Program.cs:248-254](file:///d:/Code/SecMPS/api_sqlsugar/VolPro.WebApi/Program.cs#L248-L254) 发现 VolPro 早在框架层就注册了 `app.UseStaticFiles(new StaticFileOptions { FileProvider = Path.Combine(Directory.GetCurrentDirectory(), "Upload"), RequestPath = "/Upload" })`,即 **容器内 `/app/Upload/` 物理路径 ↔ URL `/Upload/...` 的静态文件服务**。**两种方案对比**:(a)**方案 A 走 VolPro 现有 UseStaticFiles(RequestPath="/Upload")**——URL = `/Upload/DahuaAlerts/2026-07-23/xxx.jpg`,**Kestrel 直接读文件**(零 MVC 开销),**自动 ETag/Last-Modified 缓存**,URL 与磁盘路径一一对应(直观);(b)方案 B 走 FileServiceController——URL = `/api/gateway/screenshots/...`,走 MVC 管道(多一层开销),需要手动加缓存,URL 物理路径不直接对应。**整改方案**(共 1 文件改动):**只改 [appsettings.json:34 + 42](file:///d:/Code/SecMPS/api_sqlsugar/VolPro.WebApi/appsettings.json#L34-L42) 两处配置**——`RootDirectory: "/app/Upload/DahuaAlerts"`(V1.9.2 写的是 `/app/wwwroot/Upload/DahuaAlerts`,**与 VolPro 现有 `/Upload/` 服务不对齐**;V1.9.3 改为 `/app/Upload/DahuaAlerts`,**与 VolPro Program.cs:251 的 `Path.Combine(Directory.GetCurrentDirectory(), "Upload")` 同根**——容器内 `Directory.GetCurrentDirectory()=/app`,加上 Upload 子目录就是 `/app/Upload`,再嵌 DahuaAlerts 就是 `/app/Upload/DahuaAlerts`)+ `UrlBase: "/Upload/DahuaAlerts"`(V1.9.2 写的是 `/api/gateway/screenshots`,V1.9.3 改为 `/Upload/DahuaAlerts` **直接对应磁盘路径**)。前端拼接:`` = `` **零中间层**。**保留的兼容设计**:[FileServiceController.cs](file:///d:/Code/SecMPS/api_sqlsugar/VolPro.WebApi/Controllers/Warehouse/FileServiceController.cs) 端点 `/api/gateway/screenshots/{*filename}` **不删除**——继续作为兜底服务(V1.9 历史老数据 bin/Download/Screenshots/yyyy-MM/*.jpg 仍可读)+ 调试入口(不通过 UseStaticFiles 时验证图片服务是否正常)。**重要细节说明**——**主人原话"wwwroot/Upload 文件夹"实际有两种可能解读**:(i)VolPro 自动管理的业务表单上传目录(实际物理路径 `/app/Upload/`,与 VolPro 现有 `/Upload/` 服务完全对齐,**V1.9.3 已按此实现**);(ii)wwwroot 下的 Upload 目录(物理路径 `/app/wwwroot/Upload/`)。**浮浮酱采用(i)解读**——因为:(1)VolPro 框架已经为 `/app/Upload/` 注册了 `/Upload/` 静态文件路由,**用现成的最省事**;(2)wwwroot 默认是 ASP.NET Core 的 `UseStaticFiles()` 服务的根([Program.cs:235-238](file:///d:/Code/SecMPS/api_sqlsugar/VolPro.WebApi/Program.cs#L235-L238)),**再加 FileProvider=wwwroot/Upload + RequestPath=/Upload 会与现有路由冲突**;(3)容器内 `Directory.GetCurrentDirectory()` 默认等于 WORKDIR=/app,**`/app/Upload/` 即 VolPro 业务上传根目录**。**如果主人实际想要的是 (ii) 解读(图片放 wwwroot/Upload/DahuaAlerts/)**——需要单独在 Program.cs 加一行 `app.UseStaticFiles(new StaticFileOptions { FileProvider = new PhysicalFileProvider("/app/wwwroot/Upload"), RequestPath = "/wwwroot/Upload" })` 并改 `RootDirectory="/app/wwwroot/Upload/DahuaAlerts"` 和 `UrlBase="/wwwroot/Upload/DahuaAlerts"`,**告诉浮浮酱就改**。**验证**:`dotnet build api_sqlsugar` **0 错误 0 警告**(**V1.9.3 之后所有警告都清零**——本次只改 appsettings.json 注释 + 字符串值,没改任何 C# 代码)。**效果**:(a)前端 URL 与磁盘路径一一对应——`` 直接走 ASP.NET Core 静态文件服务,**零中间层开销**;(b)自动 HTTP 缓存——浏览器第二次访问同一图片自动命中 304 Not Modified(Kestrel 自带 ETag 处理),**节省带宽**;(c)URL 直观——运维看 URL 就能直接定位到磁盘文件,便于排查和备份;(d)兼容老数据——V1.9 的 bin/Download/Screenshots/yyyy-MM/*.jpg 仍可读(FileServiceController 兜底),不需要迁移 | ✅ 已完成 | | 2026-07-23 | **V1.9.4 终极修正:用 VolPro 业务上传同根 wwwroot/Upload/DahuaAlerts** | 主人确认:"http://主机:端口/Upload/DahuaAlerts/2026-07-23/xxx_pic.jpg,这个路径能在前端访问到图片的话就直接使用现有方案吧,检查一下,保证前端通过这样的 URL 格式可以访问到。"——浮浮酱做端到端验证时**发现 2 个严重错误**,必须立刻修正。**错误 1 真相**:用 [Glob](file:///d:/Code/SecMPS) 搜 `**/Upload/**` 看到主人 2025-11-04 上传的真实文件 `wwwroot/Upload/Tables/Sys_User/202511041227209292/人群图标.png`——**主人说的"wwwroot/Upload 文件夹"就是 `wwwroot/Upload/`**(不是 V1.9.3 我误以为的 `/app/Upload/`)。**错误 2 真相**:读 [warehouse_realtimealertService.cs:246-262](file:///d:/Code/SecMPS/api_sqlsugar/Warehouse/Services/Warehouse/Partial/warehouse_realtimealertService.cs#L246-L262) `GetDefaultUploadPath()` 看到 VolPro 业务上传路径是 `AppContext.BaseDirectory + "wwwroot" + "Upload"`——**业务上传实际走的是 `wwwroot/Upload/`,不是 `/app/Upload/`**;再读 [Program.cs:240-254](file:///d:/Code/SecMPS/api_sqlsugar/VolPro.WebApi/Program.cs#L240-L254) 发现:① line 240 `string _uploadPath = (app.Environment.ContentRootPath + "/Upload").ReplacePath();` 创建的是 `cwd/Upload/` 目录(**这个目录存在但不被任何静态文件服务命中,是死代码**);② line 235-238 `app.UseStaticFiles()` 默认服务 `wwwroot/`——**`wwwroot/Upload/xxx.jpg` 通过 URL `/Upload/xxx.jpg` 可访问**;③ line 248-254 `app.UseStaticFiles(FileProvider=cwd/Upload, RequestPath="/Upload")` 显式服务 `cwd/Upload/`(**因为 line 235-238 已经截胡 `/Upload/` URL,这个注册实际是无效的死代码**)。**V1.9.3 误改的根因**:当时浮浮酱只看了 Program.cs:248-254 的显式 UseStaticFiles,**没看 line 235-238 的默认 UseStaticFiles + 业务上传代码**——把 RootDirectory 改成了 `/app/Upload/DahuaAlerts`(对应死代码的 `cwd/Upload/`),URL 改成 `/Upload/DahuaAlerts`(会命中默认 UseStaticFiles 的 wwwroot/Upload/,但物理路径 `wwwroot/Upload/DahuaAlerts/xxx.jpg` 不存在,所以**404**)。**V1.9.4 终极修正方案**(共 2 文件改动):(1)**[ImageStorageService.cs:79-93](file:///d:/Code/SecMPS/api_sqlsugar/VolPro.WebApi/Controllers/Warehouse/ImageStorageService.cs#L79-L93) 默认路径改 wwwroot/Upload/DahuaAlerts**——`if (string.IsNullOrWhiteSpace(_options.RootDirectory)) { _options.RootDirectory = Path.Combine(AppContext.BaseDirectory, "wwwroot", "Upload", "DahuaAlerts"); }`(V1.9 是 `Download/Screenshots` 默认值;V1.9.4 改 `wwwroot/Upload/DahuaAlerts` **与 VolPro 业务上传 `GetDefaultUploadPath` 同根**);容器内 `AppContext.BaseDirectory=/app/` → `/app/wwwroot/Upload/DahuaAlerts`(**与主人熟悉的 `wwwroot/Upload` 业务上传目录平级**)。(2)**[appsettings.json:33](file:///d:/Code/SecMPS/api_sqlsugar/VolPro.WebApi/appsettings.json#L33-L43) `RootDirectory` 留空**——让 ImageStorageService 默认逻辑生效(避免硬编码路径在不同部署环境失效);**UrlBase 保留 V1.9.3 的 `/Upload/DahuaAlerts`**(这是对的,V1.9.3 只是 RootDirectory 写错了)。**路径映射(V1.9.4 终极)**:| 视角 | 路径 | |---|---| | 容器内(ImageStorageService 写入) | `/app/wwwroot/Upload/DahuaAlerts/2026-07-23/xxx.jpg` | | 宿主机(挂载卷 `/home/volpro→/app`) | `/home/volpro/wwwroot/Upload/DahuaAlerts/2026-07-23/xxx.jpg` | | URL(前端拼接) | `http://主机:9100/Upload/DahuaAlerts/2026-07-23/xxx.jpg` | | 走的中间件 | VolPro [Program.cs:235-238](file:///d:/Code/SecMPS/api_sqlsugar/VolPro.WebApi/Program.cs#L235-L238) `app.UseStaticFiles()` 默认服务 `wwwroot/`(Kestrel 直读,**零 MVC 开销**) | | DB 存的相对路径 | `2026-07-23/xxx.jpg` | **前端 JS 拼接**:`imageUrlBase + row.PicturePath` = `/Upload/DahuaAlerts/2026-07-23/xxx.jpg`(**与主人原话 URL 格式完全一致**)。**部署后验证脚本**:[verify_v1_9_4_static_files.ps1](file:///d:/Code/SecMPS/doc/verify_v1_9_4_static_files.ps1) 7 步端到端验证——① 看 VolPro 启动日志 `[ImageStorage] 图片保存根目录: /app/wwwroot/Upload/DahuaAlerts` 确认路径解析正确 ② 检查容器内 `/app/wwwroot/Upload/` 目录存在 ③ 先用已有业务上传文件 `Tables/Sys_User/...人群图标.png` curl 验证 `wwwroot/` 默认 UseStaticFiles 正常工作(**打前站,确认基础服务可用**)④ 创建大华告警测试图片到 `/app/wwwroot/Upload/DahuaAlerts/今天/test_xxx_pic.jpg` ⑤ 验证宿主机挂载卷同步(`/home/volpro/wwwroot/Upload/...` 也存在)⑥ 容器内 curl `http://localhost:9100/Upload/DahuaAlerts/...` 应返回 200 + image/jpeg ⑦ 宿主机 curl `http://192.168.3.108:9100/Upload/DahuaAlerts/...` 应返回 200 + image/jpeg。**验证**:`dotnet build api_sqlsugar` 0 错误 2 警告(项目原有 nullable 警告与本次改动无关)。**效果**:(a)URL 与磁盘路径一一对应——`` 直接走 ASP.NET Core 默认 `app.UseStaticFiles()` 服务,**零中间层开销**;(b)自动 HTTP 缓存——浏览器第二次访问自动命中 304 Not Modified(Kestrel 自带 ETag 处理);(c)前端拼接直观——`imageUrlBase + row.PicturePath` 即可,**与主人原话 URL 格式完全一致**;(d)路径已验证——主人 2025-11-04 业务上传的文件 `wwwroot/Upload/Tables/Sys_User/...人群图标.png` 已经在 `wwwroot/Upload/` 目录,**同样的目录同样的中间件**,V1.9.4 的 `wwwroot/Upload/DahuaAlerts/` 走同一条路径,**主人 1 年多前就证明这条路径能工作** | ✅ 已完成 | | 2026-07-24 | **MC4 适配器 JSON 反序列化大小写问题 V1.10** | 主人反馈:"刚才对MC4授权部分的修改在JSON反序列化的时候有问题,返回的JSON字符串中的字段名全是小写,但Model的属性命名是有大小写的,这个得解决一下,不然无法正确反序列化返回数据,也就无法正确拿到Token。"**根因 2 段**:(1)**MC4 返回 JSON 字段全小写**(如 `code` `msg` `data` `id` `token`),但 C# Model 属性是 PascalCase(`Code` `Msg` `Data` `Id` `Token`),**默认 `JsonSerializer.Deserialize` 大小写敏感直接返回 null**;(2)**MC4 业务接口统一响应结构** `{code, msg, data}` 三段式(参考 [MC4.0对外API.md](file:///d:/Code/SecMPS/doc/对接文档/MC4.0对外API.md)),List/Object 不能直接反序列化 List,必须先解析包装再取 data 段。**整改方案**(共 2 文件改动):(1)**[Mc4AuthHelper.cs:39-44](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Adapters.MC4/Mc4AuthHelper.cs#L39-L44) 新增 JsonOpts 静态字段**——`PropertyNameCaseInsensitive = true` + `PropertyNamingPolicy = JsonNamingPolicy.CamelCase`;[line 42-44](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Adapters.MC4/Mc4AuthHelper.cs#L42-L44) 新增 `EmptyJsonBody` 静态 `StringContent("{}", Encoding.UTF8, "application/json")` 处理无参数 POST 请求(MC4 平台要求 body 是合法 JSON,null 会返 400);[line 70](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Adapters.MC4/Mc4AuthHelper.cs#L70) `conf/get` 改用 `EmptyJsonBody`;[line 91](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Adapters.MC4/Mc4AuthHelper.cs#L91) `Deserialize` 改传 `JsonOpts`;[line 116](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Adapters.MC4/Mc4AuthHelper.cs#L116) `Deserialize` 改传 `JsonOpts`;(2)**[Mc4Adapter.cs:33-56](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Adapters.MC4/Mc4Adapter.cs#L33-L56) 新增统一 `JsonOpts` 静态字段 + `DeserializeData` 静态方法**——`DeserializeData` 用 `JsonSerializer.Deserialize>(json, JsonOpts)` 解析统一响应包装 + 自动校验 `code!=0` 抛异常 + `data` 为空返回默认值;[line 105-108](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Adapters.MC4/Mc4Adapter.cs#L105-L108) 新增 `EmptyJsonBody`;[line 90](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Adapters.MC4/Mc4Adapter.cs#L90) `HealthCheckAsync` 改用 `EmptyJsonBody`;[line 118](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Adapters.MC4/Mc4Adapter.cs#L118) `GetObjectTreeAsync` 改用 `EmptyJsonBody` + `DeserializeData>`;[line 186/203/229/244/274/299](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Adapters.MC4/Mc4Adapter.cs#L186-L299) `GetDevicesAsync/GetDevicePointsAsync/GetRealtimeValuesAsync/GetHisAlarmsAsync/GetMultiRealtimeValuesAsync/GetRealtimeAlarmsAsync` 全部改用 `DeserializeData`;新增 `Mc4ApiResponse` 统一响应包装类([line 388-394](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Adapters.MC4/Mc4Adapter.cs#L388-L394))。**验证**:`dotnet build gateway` 0 错误 0 警告。**效果**:(a)MC4 适配器所有接口反序列化均能正确处理全小写字段 + 统一包装响应;(b)`EmptyJsonBody` 解决 POST 无参 body=null 触发 400 的问题;(c)`DeserializeData` 统一处理 code 校验,未来新增 MC4 业务接口不用每次写 `if (code != 0) throw` 模板代码 | ✅ 已完成 | | 2026-07-24 | **MC4 设备列表 400 + 反序列化错误 V1.10.1** | 主人反馈 V1.10 修完后又有 2 个连续报错:(a)`[Gateway] A3: 适配器 MC4:33ku 取设备失败: Response status code does not indicate success: 400 (Bad Request)`;(b)`[Gateway] A3: 适配器 MC4:33ku 取设备失败: The JSON value could not be converted to System.Collections.Generic.List'1[IntegrationGateway.Adapters.MC4.Mc4TreeNode]. Path: $ | LineNumber: 0 | BytePositionInLine: 1.`。**根因 2 段**:(a)**400 Bad Request**——`GetObjectTreeAsync` 用 `client.PostAsync("/api/central/object/tree", null)` 发请求,**body 为 null 不符合 MC4 平台要求**(平台要求 POST body 是合法 JSON,即使是空请求也必须是 `{}` 而非 `null`),**400 是平台校验失败**;(b)**反序列化 List 错误**——MC4 返回的是 `{code, msg, data:[...nodes]}` 包装结构,**List 不能直接反序列化为 List**,必须先解析包装再取 data 段(V1.10 已经新增了 `DeserializeData` 但 `GetObjectTreeAsync` 没改)。**整改方案**(共 1 文件改动):**[Mc4Adapter.cs:115-122](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Adapters.MC4/Mc4Adapter.cs#L115-L122) GetObjectTreeAsync 改用 EmptyJsonBody + DeserializeData>**——`var resp = await client.PostAsync("/api/central/object/tree", EmptyJsonBody);` 替换原来的 `null`;`var tree = DeserializeData>(json, "/api/central/object/tree", new List());` 替换原来的 `JsonSerializer.Deserialize>(json, JsonOpts)!`。**验证**:`dotnet build gateway` 0 错误 0 警告。**效果**:A3 设备同步能正确拉取 MC4 对象树 → 推送到 VolPro → 写 base_device 表,**主人在 web.vite 管理端可以看到 MC4 设备列表** | ✅ 已完成 | | 2026-07-24 | **IoT 设备实时点值显示 + 空调控制 V1.11** | 主人反馈:"成功拿到设备列表并提交到了后端,但在管理端的列表里点击实时数据按钮没有任何数据显示,检查一下是什么问题,我看了下MC4设备的管理端,每个设备下面是有点值列表的,并且序号1的点是自动添加的在线点,真正的点值数据从序号2开始的,你查下文档看看怎么把正确的点值显示出来,并且有些设备不止一个点值的。同时IOT设备也需要进行分类,目前有三个分类:温度探头、湿度探头、空调控制器,这三类在后端需要不同的展示方式和控制方式你可以写点临时代码来获取一下192.168.3.92这个MC4设备的设备和点值数据,然后规划一下怎么在web.vite和warehouse中实现点值的显示和设备控制。"**整改方案**(共 5 文件改动 + 1 文档):(1)**[Mc4Adapter.cs:154](file:///d:/Code/SecMPS/gateway/src/IntegrationGateway.Adapters.MC4/Mc4Adapter.cs#L154) GetRealtimeValuesAsync 改用 DeserializeData**——修复实时点值接口反序列化问题(V1.10 漏改了 GetRealtimeValuesAsync,其他接口都改了唯独实时值没改);(2)**[base_device.vue:163-179](file:///d:/Code/SecMPS/web.vite/src/views/warehouse/device_manager/base_device.vue#L163-L179) fetchRealtime 增加 normalizePoint 字段映射**——网关 B4 返回 PascalCase 字段(PointIndex/Value/UpdateTime/Interval),前端 table 是 camelCase 列名(pointIndex/value/updateTime/interval),**之前直接 await r.json() 字段名不匹配导致表格空**;新增 `normalizePoint` 做 PascalCase→camelCase 映射 + `Array.isArray` 判断(防止 404/500 时 await r.json() 失败);(3)**[RealtimeDataPanel.vue:19-37](file:///d:/Code/SecMPS/web.vite/src/views/warehouse/device_manager/base_device/components/RealtimeDataPanel.vue#L19-L37) 同样修复**——组件层字段映射(与 base_device.vue 一致);(4)**[DeviceInfo.vue](file:///d:/Code/SecMPS/warehouse/src/view/DeviceInfo.vue) 完整改造**——改用 `/api/base_device/getPageData` 接口获取设备信息(之前调老接口 `/api/Warehouse_Device/GetPageData` 走的是 base_devicepoint 表,已废弃);按 DeviceCategory 分类展示(温度探头/湿度探头单大数字卡片 + 空调控制器双卡片);30 秒轮询实时点值(仅 IoT 设备);空调控制按钮修复——`sourceDeviceId` 改为 `deviceId`(与后端 `ControlRequest.DeviceId` 字段名一致);(5)**[mc4_probe 探针工具](file:///d:/Code/SecMPS/tools/mc4_probe/Mc4Probe/Program.cs)**——独立 .NET 控制台程序,连接 192.168.3.92 真实 MC4 设备抓取设备列表 + 点表 + 实时值,便于主人后续调试点位索引(已写入实施文档附录);(6)**[IoT设备实时点值显示与控制实施方案_v1.0.md](file:///d:/Code/SecMPS/doc/整合方案/IoT设备实时点值显示与控制实施方案_v1.0.md)**——详细方案文档(设备分类/点位映射表/3 分类展示策略/网关 B4+B5 接口/前端 5 文件改动/验证清单/风险回滚/后续优化建议)。**关键设备点位索引语义**(写入实施文档固化):温度探头/湿度探头 `index=2` 是真实数据;空调控制器 `index=5=湿度/6=温度/2=制冷发射/3=制热发射/4=关机发射`;MC4 自动在 `index=1` 添加"在线点"。**验证**:`dotnet build gateway` 0 错误;`npm run build warehouse` / `npm run build web.vite` 待主人本地验证。**效果**:(a)管理端 IoT 设备的"实时数据"按钮点击后表格正常显示点位/当前值/更新时间/采集间隔 4 列;(b)大端地图点击温度探头→大数字显示温度℃;(c)大端地图点击湿度探头→大数字显示湿度%RH;(d)大端地图点击空调控制器→温度+湿度双卡片 + 控制按钮(制冷/制热/关机);(e)空调命令 3 秒后实时数据自动刷新(控制生效验证);(f)30 秒轮询保证实时数据保持最新 | ✅ 已完成 | --- ## 十五、dahua_demo_cs B/S Demo 现场调试与修复 ### 15.1 现场问题 主人到现场,在 Demo 中填入真实摄像机 `192.168.3.79 / admin / hbdq12345`,点「测试连接」后: - ❌ 提示"连接失败: Error Not Implemented!" - ❌ 之后所有 API 调用都返回 SPA 的 `index.html`(HTML 字符串) ### 15.2 根因分析 **根因 1:CGI 端点选错** Demo 调用的路径是 `/cgi-bin/configManager.cgi?action=getSystemInfo`,但**协议规范 §4.6.12** 明确规定系统信息走 `magicBox.cgi?action=getSystemInfoNew`。大华固件对不认识的 action 报错码 7 = "Not Implemented"。 主人手动用 curl 验证: ```bash curl --digest -u "admin:hbdq12345" \ "http://192.168.3.79/cgi-bin/magicBox.cgi?action=getSystemInfoNew" ``` 返回正常的 key=value 系统信息,**设备完全正常**。 **根因 2:SPA Fallback 没排除后端路由** `Program.cs` 的 `MapFallback` 把所有未匹配的请求都返回 `index.html`,导致 `/api/foo` 之类不存在的端点错误地返回 HTML。 **根因 3:CommonController 16 个端点一把梭** 所有 action 都用 `configManager.cgi`,但协议里 `getSystemInfo/getDeviceName/getUserList/getGroups/getNetworkInterfaces/getNtp` 等都不属于 `configManager.cgi`,应分别走 `magicBox.cgi / userManager.cgi / netApp.cgi` 等。 ### 15.3 修复方案(严格按规范) **1. 新建 [CgiActionMap.cs](file:///d:/Code/SecMPS/dahua_demo_cs/server/Dahua/CgiActionMap.cs) — CGI Action 单一索引** 每行带协议章节号,未注册的 action 一律不允许使用,杜绝胡编乱造。 **2. [ProtocolController.TestConnection](file:///d:/Code/SecMPS/dahua_demo_cs/server/Controllers/ProtocolController.cs#L40-L41) 改用 magicBox.cgi** **3. [CommonController.cs](file:///d:/Code/SecMPS/dahua_demo_cs/server/Controllers/CommonController.cs) 全部按 action 路由** 主要修正: - `getSystemInfo` → `getSystemInfoNew`(§4.6.12) - `getDeviceName` → `getMachineName`(§4.6.11) - `getUserList` → `getUserInfoAll`(§4.9.3) - `getGroups` → `getGroupInfoAll`(§4.9.6) - `getNetworkInterfaces` → `netApp.cgi?action=getInterfaces`(§4.8.1) - `getNtp` → `configManager.cgi?action=getConfig&name=NTP`(§4.6.4+§4.6.7) - `getHttpApiVersion` → `IntervideoManager.cgi?action=getVersion&Name=CGI`(§4.6.16) - RTSP Token → `/cgi-bin/api/TokenManager/createToken` POST+JSON(§4.1.5) - 删除不在规范里的 `getDeviceName/getHttpApiVersion/attachOnlineStatus/getUserList/getGroups/getNetworkInterfaces/getHostName/getNtp/getRecordMode/queryLog/getUploadConfig/getAutoRegister` **4. [Program.cs](file:///d:/Code/SecMPS/dahua_demo_cs/server/Program.cs#L128-L154) SPA Fallback 排除 /api /ws /swagger** 未匹配的后端路由返回 `{"success":false,"status":404,...,"error":"ENDPOINT_NOT_FOUND"}` JSON,不再返回 HTML。 **5. [CameraController.CfgSet](file:///d:/Code/SecMPS/dahua_demo_cs/server/Controllers/CameraController.cs#L61-L75) 严格按规范** `setConfig` 按 §4.6.5 从 POST 改为 GET + URL 参数。 ### 15.4 现场自检结果 主人摄像机 `192.168.3.79` 实机测试: | 端点 | 结果 | |---|---| | `/api/protocol/test-connection` | ✅ success=true, duration=110ms, 返回 `info.ProductDate=...` | | `/api/common/device/system-info` | ✅ 返回完整系统信息(cameraNum=0, hasRTC=true, supportMaxSDNum=1) | | `/api/common/device/serial` | ✅ `sn=BK036D4PAGC9085` | | `/api/common/device/name` | ✅ `name=BK036D4PAGC9085` | | `/api/common/user/list` | ✅ 完整用户列表(admin id=1 拥有 15 项权限) | | `/api/foo/bar` (兜底测试) | ✅ 返回 `ENDPOINT_NOT_FOUND` JSON(不再返回 HTML)| ### 15.5 启动/停止脚本 **源码位置**:`dahua_demo_cs/server/scripts/start.bat` 和 `stop.bat` **发布位置**:`dahua_demo_cs/server/publish/`(与 EXE 同级) **自动复制机制**:在 [dahua_demo_cs.csproj](file:///d:/Code/SecMPS/dahua_demo_cs/server/dahua_demo_cs.csproj) 中新增 MSBuild Target `CopyScriptsToPublish`,每次 `dotnet publish` 自动把 `scripts/*.bat` 复制到 `$(PublishDir)`,与 `CopyFFmpegToPublish` 一并执行。 **使用方式**: - `start.bat` — 双击启动 EXE(独立控制台窗口,可见实时日志)+ 端口冲突检测 + 启动成功提示 - `stop.bat` — 通过 `taskkill` 优雅停止(包含子进程)+ 端口释放验证 **特点**: - 所有提示文字均为中文(`chcp 65001` 切到 UTF-8) - 端口 5000 双重检查(进程 + 端口) - 失败时给出明确指引 - 管理员权限不足时给出提示 ### 15.6 部署包内容 `dahua_demo_cs/server/publish/` 目录: ``` dahua_demo_cs.exe 主程序 (单文件 ~95MB self-contained) ffmpeg.exe 视频转码工具 (CopyFFmpegToPublish 自动复制) start.bat 启动脚本 (CopyScriptsToPublish 自动复制) stop.bat 停止脚本 (CopyScriptsToPublish 自动复制) web.config IIS 反向代理配置 appsettings.json 应用配置 wwwroot/ 前端 Vite 产物 ```