소스 검색

docs(house-fault): 方案修正——复用现有 reportAlarm 报警闭环(alarm表/双端列表/短信电话/消警),弃用 reportCloudAlarm 群消息+新建告警类型

核实:reportAlarm→报警责任链→alarm表→front/operate列表+短信电话通知+静音/消警=完整闭环;
状态码 0正常/1异常/-1跳过;运行期串口/相机异常已接此闭环。短信电话保留并复用。
启动排除舱改走 SerialBinController.ReportAlarmController(相机坏photoState=1/串口坏comState=1)。
删除新建 HOUSE_* alarmTypeKey 及 Java 字典登记跨端依赖。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
huangjie 2 일 전
부모
커밋
c2686f7

+ 37 - 22
项目文档/开发计划/2026-06-23-舱室故障隔离与双端故障提示-实现计划.md

@@ -679,14 +679,18 @@ Expected: PASS(2 绿)。
 ```
 > `snapshot` 为该方法内构造的 `MonitorSnapshot` 局部变量名;若现有代码用对象初始化器一次性 `return new MonitorSnapshot{...}`,改为先赋给局部 `var snapshot = new MonitorSnapshot{...};` 再补上面映射,最后 `return snapshot;`。需要 `using System.Linq;`(AppData 已用 Linq)。
 
-- [ ] **Step 7: AppData 加 ReportStartupFaults(复用现有告警通道)**
+- [ ] **Step 7: AppData 加 ReportStartupFaults(复用现有 `reportAlarm` 报警闭环 —— 写 alarm 表/双端列表/短信电话通知/可消警)**
+
+> **2026-06-23 修正**:走 `SerialBinController.ReportAlarmController`(→ `/reportAlarm` → 报警责任链 → `alarm` 表 → front/operate 报警列表 + **短信/电话通知** + 静音/消警),**不走** `reportCloudAlarm`(那只发 IM 群消息、不入闭环)。状态码语义:每维度 **0=正常 / 1=异常 / -1=跳过**(`AlarmDTO` 接口确认;运行期 ComStateEvent 即按此只填 comState、其余 -1)。**无新增 alarmTypeKey、无 Java 改动。**
+>
+> `ReportAlarmController` 签名(`SerialBinController.cs`):`ReportAlarmController(string tlSn, int housesn, int houseState, int comState, int photoState, int wellSN, int airSwapState)`;经 `AppData.SerialBinController` 调用(与运行期 `HouseBin_HouseStateEvent` 同一入口)。
 
-参照现有 `MqttAlarm`(`AppData.cs:1350`,走 `HttpService.AlarmApi1`)与 `AlarmApi`(`HttpService.cs:54`,POST `/api/tl/control/alarm/reportCloudAlarm`,带 houseSn/wellSn)。新增:
 ```csharp
         /// <summary>
-        /// 上报启动期坏舱告警(每坏舱一条)。复用 HttpService.AlarmApi(reportCloudAlarm)。
-        /// alarmTypeKey 按故障类型映射;paramList 携带原因/阶段。全 try 兜底,失败不影响启动。
-        /// 注:新 alarmTypeKey 需在 Java 告警字典登记(否则落库被拒)—— 见计划「跨端依赖」。
+        /// 上报启动期坏舱告警(每坏舱一条),复用现有 reportAlarm 报警闭环:
+        /// 写 alarm 表 → front/operate 报警列表 + 短信/电话通知 + 可静音/恢复自动消警。
+        /// 维度码 0正常/1异常/-1跳过:相机类故障→photoState=1,串口/编号类→comState=1,其余维度 -1。
+        /// 全 try 兜底,失败不影响启动。无新增 alarmTypeKey、无 Java 改动。
         /// </summary>
         public void ReportStartupFaults()
         {
@@ -695,24 +699,32 @@ Expected: PASS(2 绿)。
             try { tlSn = TLSetting?.tlSn ?? ""; } catch { }
             foreach (var f in StartupFaults)
             {
+                if (f.HouseSn <= 0) continue; // 舱号未知(相机/串口级)无法定位到舱,跳过上报(仍在 /status Faults 里可见)
                 try
                 {
-                    string key = f.Type == HouseFaultType.CcdSnMissing || f.Type == HouseFaultType.CameraDuplicateSn || f.Type == HouseFaultType.CameraReadFailed
-                        ? "HOUSE_CAMERA_FAULT"
-                        : (f.Type == HouseFaultType.SerialReadException ? "HOUSE_SERIAL_FAULT" : "HOUSE_INIT_EXCLUDED");
-                    HttpService.AlarmApi(tlSn, f.HouseSn > 0 ? f.HouseSn : 0, 0, key,
-                        new List<string> { f.Type.ToString(), f.Reason, f.Stage });
+                    // 相机类故障 → 拍照状态异常;串口/编号类 → 串口状态异常;其余维度 -1 跳过(不误清别的告警)。
+                    bool cameraFault = f.Type == HouseFaultType.CcdSnMissing
+                                    || f.Type == HouseFaultType.CcdSnDuplicate
+                                    || f.Type == HouseFaultType.CameraDuplicateSn
+                                    || f.Type == HouseFaultType.CameraReadFailed;
+                    int comState = cameraFault ? -1 : 1;   // 串口/编号/Init 异常
+                    int photoState = cameraFault ? 1 : -1; // 相机异常
+                    SerialBinController.ReportAlarmController(tlSn, f.HouseSn,
+                        houseState: -1, comState: comState, photoState: photoState,
+                        wellSN: -1, airSwapState: -1);
                 }
                 catch (Exception ex) { ExLog(ex, "ReportStartupFaults"); }
             }
         }
 ```
+> ⚠ **真机确认点(Task7)**:报警链部分 handler 按 houseSn 查 `house`/`tlSetting`;被排除的坏舱可能未写入 house 表 → 若 alarm 未落库,Task7 记录并补一步(确保 house 行存在 / 或对排除舱另走内部 reportAlarmData)。
 
 - [ ] **Step 8: 编译 + 跑全量单测**
 
 Run: `dotnet build ivf_tl_operate_2.0/control/ivf_tl_Control.sln -c Debug` → 0 错
 Run: `dotnet test ivf_tl_operate_2.0/control/IvfTl.ControlHost.Tests/IvfTl.ControlHost.Tests.csproj`
 Expected: 既有 27 + 新增(HouseFault 2 + Policy 6 + Snapshot 2)= **37 绿 0 失败**。
+**无新增 alarmTypeKey、无 Java 字典登记依赖**(复用现有 reportAlarm 闭环)。
 
 - [ ] **Step 9: 提交**
 
@@ -755,20 +767,22 @@ git commit -m "docs(house-fault): 第一阶段 control 坏舱剔除真机拔插
 
 # 后续阶段(提纲,第一阶段落地后单独拆细)
 
-## 第二阶段:运行期故障升级 + 去抖(control,可自主)
-- `HouseBin/BufferBottleBin.MainThread`、`ComBin.StartSendCommandThread` 的 `catch` 里:同舱连续失败计数达阈值(去抖,避免每圈刷屏)→ 升级一条 `HouseFault{RuntimeFault}` 进 `AppData.StartupFaults`(改名/或新增 `RuntimeFaults`)+ `ReportStartupFaults` 同款上报 `HOUSE_RUNTIME_FAULT`;失败计数清零条件 = 该舱恢复一次成功通讯。
-- 纯逻辑(计数/阈值/去抖)可 TDD;升级动作真机验证(运行中拔串口/相机)。
+## 第二阶段:运行期故障升级(control,**先核实再决定是否做**)
+- **2026-06-23 修正**:核实发现**运行期串口/相机异常已接现有报警闭环**(`HouseBin.ComBin_ComStateEvent` → `HouseStateEvent` → `ReportAlarmController` → 报警链 → alarm 表 + 短信电话)。故本阶段**不再造并行通道**。
+- 真机阶段先核实现有运行期上报是否有缺口(如:某维度故障没触发 ComStateEvent、或失败需去抖防刷屏);**确有缺口才补**(在对应 catch 里按阈值/去抖调同一个 `ReportAlarmController`,维度码 0/1/-1)。无缺口则本阶段仅做真机确认、不改码。
+- 纯逻辑(计数/阈值/去抖)若需补,可 TDD;升级动作真机验证(运行中拔串口/相机)。
 
 ## 第三阶段:operate 监控页"舱故障"区(真机门控)
-- `ivf_tl_Operate` 监控页(读 `ControlClient` → `/status` 的 `snapshot.Faults`)新增"舱故障"区:红色高亮列出每故障舱(舱号/类型/时间/已隔离),与现有监控三块并列。
+- operate 报警走现有 alarm 闭环 → 顶部"系统异常(N)"+报警列表自动反映(无需改)。
+- 另:`ivf_tl_Operate` 监控页(读 `ControlClient` → `/status` 的 `snapshot.Faults`)新增"舱故障"区:红色高亮列出每**启动被隔离**舱(舱号/类型/时间/已隔离),补充 alarm 表不直接表达的"被隔离"信息。
 - 复用现有轮询;UI 真机门控(需 operate 外壳跑起来,受僵尸 operate Mutex 影响,需先清)。
 
-## 第四阶段:front 告警展示(真机门控)+ 跨端依赖
-- **跨端依赖(必须先确认):** 新 alarmTypeKey(`HOUSE_INIT_EXCLUDED`/`HOUSE_CAMERA_FAULT`/`HOUSE_SERIAL_FAULT`/`HOUSE_RUNTIME_FAULT`)需在 Java 告警字典/告警类型配置登记,否则 `reportCloudAlarm` 落库被拒、front 也无从展示。Task 7 Step 4 已先探;此阶段落地登记
-- front 舱位图/告警列表:故障舱标红 + 统一文案("X 号舱相机故障,已隔离,其余舱正常");接现有 alarm_data + MQTT 流。
+## 第四阶段:front 告警展示(真机门控)
+- **2026-06-23 修正**:复用现有 `reportAlarm` 闭环 = **复用现成告警类型(串口异常/相机异常等),无新建 alarmTypeKey、无 Java 字典登记依赖**。坏舱告警进 alarm 表后,front 报警列表/舱位图**本来就会展示并标红**
+- 本阶段只需真机确认:坏舱(尤其启动被排除舱)的告警在 front 列表正确显示(舱号/类型/时间)、文案达意;若展示文案需微调再动 front。接现有 alarm + MQTT 流。
 
 ## 第五阶段:真机拔插整体验收(spec §48-51 三条)
-- ① 启动期半坏舱排除、其余正常;② 运行期拔串口/相机隔离、其余采集不中断;③ 双端(operate 监控页 + front)都明确显示故障舱(舱号/类型/时间)。
+- ① 启动期半坏舱排除、其余正常;② 运行期拔串口/相机隔离、其余采集不中断;③ 双端闭环(alarm 表落坏舱告警 → front 报警列表 + operate"系统异常"可见 + 监控页"舱故障"区显示被隔离舱;短信/电话按配置通知;恢复后消警)。
 - 逐条回写 `待验证清单.md`。
 
 ---
@@ -776,9 +790,10 @@ git commit -m "docs(house-fault): 第一阶段 control 坏舱剔除真机拔插
 ## 自查(写完对照 spec)
 
 - **spec §27 启动期按舱容错** → Task 2(Policy 剔除)+ Task 3(SerialBin 登记)+ Task 4(InitTL 不再一刀切)+ Task 5(InitHouse 逐舱兜底)。✓
-- **spec §28 双端明确提示** → Task 6(快照 Faults + ReportStartupFaults 上报)覆盖 control 侧出口;operate/front 展示在第三/四阶段。✓(第一阶段做到"故障可被双端拿到",展示分阶段)
+- **spec §28 双端明确提示** → Task 6(快照 Faults + ReportStartupFaults 走 reportAlarm 闭环)覆盖 control 侧出口;operate/front 展示在第三/四阶段(复用现有 alarm 列表)。✓
 - **spec §33-35 设计要点(区分致命/单舱、剔除坏舱、逐舱 try-catch)** → Task 2/3/4/5 一一对应。✓
-- **spec §38-41 统一上报(复用 alarm_data+MQTT、/status 加舱故障字段、运行期去抖)** → Task 6(复用 reportCloudAlarm + 快照字段)+ 第二阶段(去抖)。✓
-- **spec §48-51 真机验收** → Task 7(启动期)+ 第五阶段(运行期+双端)。✓
-- **类型一致性自查:** `HouseFault`(Entity)字段贯穿 SerialBin/InitTL/AppData;`HouseFaultRow`(MonitorSnapshot)与 AppData 映射字段名一致(HouseSn/FaultType/Reason/Stage/At/Isolated);`StartupFaultPolicy.RunnableHouses/IsFatal/BadHouseSns` 签名在 Task2 定义、Task4 调用一致;`AppData.StartupFaults`/`ReportStartupFaults` Task4 调用、Task6 定义一致(执行注:先做 Task6 再回填 Task4 调用,保证中途可编译)。✓
+- **spec §4.2 统一上报(2026-06-23 修正:复用现有 reportAlarm 报警闭环——alarm 表/双端列表/短信电话/消警,不走 reportCloudAlarm 群消息、无新建告警类型)** → Task 6(ReportStartupFaults 走 ReportAlarmController + 快照 Faults 字段)+ 第二阶段(运行期已接闭环,仅按需补)。✓
+- **spec §48-51 真机验收** → Task 7(启动期 + alarm 落库确认)+ 第五阶段(运行期+双端闭环)。✓
+- **类型一致性自查:** `HouseFault`(Entity)字段贯穿 SerialBin/InitTL/AppData;`HouseFaultRow`(MonitorSnapshot)与 AppData 映射字段名一致(HouseSn/FaultType/Reason/Stage/At/Isolated);`StartupFaultPolicy.RunnableHouses/IsFatal/BadHouseSns` 签名在 Task2 定义、Task4 调用一致;`AppData.StartupFaults`/`ReportStartupFaults` Task4 调用、Task6 定义一致;`ReportStartupFaults` 走 `SerialBinController.ReportAlarmController(tlSn,housesn,houseState,comState,photoState,wellSN,airSwapState)`(与运行期同入口)。**执行注:先做 Task6 再回填 Task4 调用,保证中途可编译。** ✓
+- **告警通道修正自查:** 已全文废弃 `reportCloudAlarm`/新建 `HOUSE_*` alarmTypeKey/Java 字典登记依赖;改走现有 `reportAlarm` 闭环(状态码 0/1/-1)。短信电话报警**保留并复用**(报警链内置 getNotifier→短信/电话模板)。✓
 - **占位扫描:** 无 TBD/TODO;每代码步均有完整代码块。真机行为类步骤(Task3/4/5/7)给出确切编辑点 file:line 与验证预期,非"自行处理"。✓

+ 14 - 8
项目文档/需求文档/specs/2026-06-23-舱室故障隔离与双端故障提示-design.md

@@ -35,20 +35,26 @@
 - `InitHouse`:大 try-catch 内**逐舱构造/StartTask 各自 try-catch**,单舱构造抛异常只跳过该舱。
 - 坏舱清单贯穿到状态快照(`MonitorSnapshot`)与告警上报。
 
-### 4.2 统一故障上报(复用现有管道)
-- 接**现有 `alarm_data` + MQTT 上报**机制(D1-07 已验通)。新增舱级故障告警类型:`HOUSE_INIT_EXCLUDED`(启动排除)、`HOUSE_RUNTIME_FAULT`(运行期串口/相机失联)、`HOUSE_CAMERA_FAULT`、`HOUSE_SERIAL_FAULT` 等。
-- control 的 `/status` 快照增"舱故障列表"字段(houseSn/类型/原因/时间/是否已隔离),供 operate 监控页轮询展示。
-- 运行期 `MainThread`/`ComBin` 的 catch 里:连续失败达阈值 → 升级为舱级故障告警(避免每圈刷屏,做去抖/阈值)。
+### 4.2 统一故障上报(复用**现有舱室报警闭环**,2026-06-23 核实修正)
+> **核实修正**:经逐符号核实,系统已有完整的舱室异常报警闭环,本专项**接它、不另起新通道**:
+> - **闭环路径**:control `SerialBinController.ReportAlarmController(tlSn, houseSn, houseState, comState, photoState, wellSn, airSwapState)` → Java `/api/tl/control/alarm/reportAlarm` → **报警责任链** `AlarmChain.reportAlarmChain`(挂 温度/气压/舱门/**串口 HousePort**/舱态/**相机 Photo**/排气 等 handler)→ 写 **`alarm` 表** → **front 报警列表 + operate 顶部"系统异常(N)" 都读这张表** + 短信/电话通知 + `muteAlarm` 静音 + 恢复正常自动消警(`stopAlarm`)。
+> - **状态码语义(`AlarmDTO` 接口确认)**:每维度 **0=正常 / 1=异常 / -1=跳过该维度**(不评估)。
+> - **运行期串口/相机异常本来就接在此闭环**(`HouseBin.ComBin_ComStateEvent` → `HouseStateEvent` → `ReportAlarmController`),故运行期无需另造并行上报。
+> - ❌ **废弃**原设想的 `reportCloudAlarm` + 新建 `HOUSE_*` 告警类型方案:`reportCloudAlarm` 实测只 `sendGroupMessage` 发 IM 群消息,**不入 `alarm` 表、不进报警列表、不能消警**——形不成闭环;且新类型要在 Java 字典登记=多余跨端依赖。**全部弃用**。
+
+- **启动期被排除的坏舱**(无 HouseBin、不会自己触发 HouseStateEvent):由 `AppData` 在排除时**主动调 `ReportAlarmController`**,按坏的维度填异常码(相机类故障 CcdSnMissing/CcdSnDuplicate/Camera* → `photoState=1`;串口/编号类 SerialReadException/HouseSnDuplicate → `comState/portState=1`;其余维度填 `-1` 跳过)→ 进同一条报警链 → front/operate 现成展示 + 可消警 + 通知。**无新增 alarmTypeKey、无 Java 改动。**
+- control 的 `/status` 快照增"舱故障列表"字段(`MonitorSnapshot.Faults`:houseSn/类型/原因/时间/是否已隔离)——作 alarm 表(按维度报异常)之外的**结构化隔离态补充**,供 operate 监控页"舱故障"区直观展示"哪个舱启动时被隔离、因为啥";主报警闭环仍走 alarm 表。
+- ⚠ **待真机确认**:报警链部分 handler 按 houseSn 查 `house`/`tlSetting` 表;被排除的坏舱**可能未写入 house 表** → "排除舱能否正常进 alarm 表"需 Task7 真机验证,查不到则补一步(如确保 house 行存在 / 或对排除舱走 reportAlarmData 内部通道)。
 
 ### 4.3 双端展示(硬约束落地)
-- **operate**:监控页新增"舱故障"区,红色高亮列出每个故障舱(舱号/故障类型/时间/已隔离),与现有监控三块并列。
-- **front**:接告警流,故障舱在舱位图/告警列表明确标红 + 文案("X 号舱相机故障,已隔离,其余舱正常")。
+- **operate**:① 报警走现有 alarm 闭环 → 顶部"系统异常(N)"+报警列表自动反映;② 监控页另加"舱故障"区(读 `/status` 的 `MonitorSnapshot.Faults`),红色高亮列出每个**启动被隔离**舱(舱号/类型/时间/已隔离),补充"被隔离"这层 alarm 表不直接表达的信息
+- **front**:接现有告警流(alarm 表 + MQTT),故障舱在舱位图/告警列表明确标红 + 文案("X 号舱相机故障,已隔离,其余舱正常")。**复用现成展示,不新建。**
 - 两端文案统一、可定位(舱号 + 故障类型 + 时间),满足"一眼知道哪坏了"。
 
 ## 五、验收(真机)
 1. **启动期**:制造一个舱"半坏"(如拔该舱相机 / 让 CCDSN 不匹配)→ control 启动:该舱被排除、**其余舱全部正常 StartAsync 培养**;control 不中止。
-2. **运行期**:运行中拔某舱串口/相机 → 该舱告警上报、被隔离,**其余舱采集不中断**。
-3. **双端提示**:上述两种情况,**operate 监控页 + front 都明确显示故障舱(舱号/类型/时间)**
+2. **运行期**:运行中拔某舱串口/相机 → 该舱告警上报、被隔离,**其余舱采集不中断**(运行期走现有闭环)
+3. **双端提示(闭环)**:上述两种情况,**alarm 表落到该坏舱告警(front 报警列表 + operate"系统异常"可见)+ operate 监控页"舱故障"区显示被隔离舱**;恢复后能消警
 
 ## 六、与其他任务关系
 - **独立于 D2-02**(调试命令代理):本专项是 control 启动/运行健壮性 + 双端可观测性。