fix(ops): 修复工牌绑定/手动派单/审计日志三处缺陷 #2

Merged
lzh merged 3 commits from fix/badge-online-and-manual-dispatch into master 2026-04-22 18:10:33 +08:00
Owner

背景

线上日志(aiot-ops-server-20260422164407.log)暴露 3 个互相串联的问题:

  1. 设备上线后"可分配工牌"列表迟迟不出现该设备
  2. 设备绑定区域后工单不会派给该工牌
  3. 手动派单后工牌按键查询持续返回"没有工单"

根因定位:

  • BadgeDeviceStatusEventHandler 在写 Redis 前用 isBadgeDevice() 校验 ops_area_device_relation;设备先上线后绑定时事件被丢弃,且无回填机制(线上 deviceId=58 在绑定后 30 分钟才被对账修正:reason=定时对账修正-上线)。
  • ManualOrderActionFacade.dispatch()setAssigneeId/Name 放在 transition() 之后,导致事务内同步发的 OrderStateChangedEvent 触发 BadgeDeviceStatusEventListenerassigneeId 仍为 null,listener 跳过 → Redis currentOpsOrderId 永远不写;同时 assigneeDeviceId 也始终为 null,audit 查询永远落空。
  • OrderTransitionAuditListener 每次成功都向 bus_log 写一条"状态转换成功"镜像,与各条线 EventListenerORDER_DISPATCHED 等)和 ops_order_event 表内容重复,淹没真正的失败审计。

改动(3 个 commit)

Commit 类型 说明
8c664a47 refactor OrderTransitionAuditListener 成功路径不再写 bus_log;失败/被拒/回滚仍记录
6c4153fe fix ManualOrderActionFacade.dispatch() 把执行人字段(含新增 setAssigneeDeviceId)更新前置到 transition() 之前
3cfd3423 fix 新增 AreaDeviceBoundEvent / AreaDeviceUnboundEventbindDevice/unbindDevice 后通过 ApplicationEventPublisher 发布;environment-biz 新增 BadgeAreaBoundEventListener@TransactionalEventListener(AFTER_COMMIT) + @Async),BADGE 类型绑定后立即调 IotDeviceQueryApi.getDevice 单次取齐 state+nickname+deviceName 回写 Redis;解绑后 deleteBadgeStatus 清理 Redis

依赖方向遵循 environment-biz → ops-biz,通过事件解耦,与现有 OrderStateChangedEvent 模式一致。

测试

  • viewsh-module-ops-bizviewsh-module-environment-biz 全模块 clean compile 通过
  • AreaDeviceRelationServiceTest 10/10 通过(顺手补齐 iotDeviceQueryApi mock,让 master 上原本因缺 mock 而 RED 的 testBindDevice_TrafficCounter_Success 转绿;新增对 bound/unbound 事件 publishEvent 的 verify)

上线后验证清单

  • 新绑定 BADGE 设备后立即在"可分配工牌"列表出现
  • 解绑后 BADGE 设备立即从列表消失
  • 管理员手动派单 → 工牌按键确认能正常流转
  • bus_log 不再有"状态转换成功"行;失败/回滚仍可见
  • OrderTransitionAuditListener WARN/ERROR 日志不变
## 背景 线上日志(`aiot-ops-server-20260422164407.log`)暴露 3 个互相串联的问题: 1. 设备上线后"可分配工牌"列表迟迟不出现该设备 2. 设备绑定区域后工单不会派给该工牌 3. 手动派单后工牌按键查询持续返回"没有工单" 根因定位: - `BadgeDeviceStatusEventHandler` 在写 Redis 前用 `isBadgeDevice()` 校验 `ops_area_device_relation`;设备先上线后绑定时事件被丢弃,且无回填机制(线上 deviceId=58 在绑定后 30 分钟才被对账修正:`reason=定时对账修正-上线`)。 - `ManualOrderActionFacade.dispatch()` 把 `setAssigneeId/Name` 放在 `transition()` 之后,导致事务内同步发的 `OrderStateChangedEvent` 触发 `BadgeDeviceStatusEventListener` 时 `assigneeId` 仍为 null,listener 跳过 → Redis `currentOpsOrderId` 永远不写;同时 `assigneeDeviceId` 也始终为 null,audit 查询永远落空。 - `OrderTransitionAuditListener` 每次成功都向 `bus_log` 写一条"状态转换成功"镜像,与各条线 `EventListener`(`ORDER_DISPATCHED` 等)和 `ops_order_event` 表内容重复,淹没真正的失败审计。 ## 改动(3 个 commit) | Commit | 类型 | 说明 | |---|---|---| | `8c664a47` | refactor | `OrderTransitionAuditListener` 成功路径不再写 bus_log;失败/被拒/回滚仍记录 | | `6c4153fe` | fix | `ManualOrderActionFacade.dispatch()` 把执行人字段(含新增 `setAssigneeDeviceId`)更新前置到 `transition()` 之前 | | `3cfd3423` | fix | 新增 `AreaDeviceBoundEvent` / `AreaDeviceUnboundEvent`,`bindDevice`/`unbindDevice` 后通过 `ApplicationEventPublisher` 发布;`environment-biz` 新增 `BadgeAreaBoundEventListener`(`@TransactionalEventListener(AFTER_COMMIT) + @Async`),BADGE 类型绑定后立即调 `IotDeviceQueryApi.getDevice` 单次取齐 state+nickname+deviceName 回写 Redis;解绑后 `deleteBadgeStatus` 清理 Redis | 依赖方向遵循 `environment-biz → ops-biz`,通过事件解耦,与现有 `OrderStateChangedEvent` 模式一致。 ## 测试 - `viewsh-module-ops-biz`、`viewsh-module-environment-biz` 全模块 `clean compile` 通过 - `AreaDeviceRelationServiceTest` 10/10 通过(顺手补齐 `iotDeviceQueryApi` mock,让 master 上原本因缺 mock 而 RED 的 `testBindDevice_TrafficCounter_Success` 转绿;新增对 bound/unbound 事件 `publishEvent` 的 verify) ## 上线后验证清单 - [ ] 新绑定 BADGE 设备后立即在"可分配工牌"列表出现 - [ ] 解绑后 BADGE 设备立即从列表消失 - [ ] 管理员手动派单 → 工牌按键确认能正常流转 - [ ] `bus_log` 不再有"状态转换成功"行;失败/回滚仍可见 - [ ] `OrderTransitionAuditListener` `WARN`/`ERROR` 日志不变
lzh added 3 commits 2026-04-22 18:09:13 +08:00
问题:每次 transition() 成功 commit 后,OrderTransitionAuditListener 都会
向 bus_log 写一条"状态转换成功: X -> Y"的镜像记录。该信息已由各条线
EventListener(CleanOrderEventListener 的 ORDER_DISPATCHED 等)和
ops_order_event 表完整覆盖,bus_log 里这条镜像形成噪声且与业务日志重复,
线上一次工单流转能产出 4-5 条同义日志,运维抓异常时被淹没。

改动:onAfterCommit 在 event.isSuccess()=true 时直接 return;
失败 / 派发被拒(DISPATCH_REJECTED)/ 回滚三类异常路径继续写,
保留运维真正关心的"为什么失败"审计闭环。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
问题:管理员手动派单后,工牌按键查询持续返回"没有工单",TTS 循环播报"工单
来啦"但用户无法响应(线上日志可见 deviceId=58 一段时间内连续 8 次按键查询
全部命中 CleanOrderAuditEventHandler.handleQueryEvent 的"没有工单"分支)。

根因:ManualOrderActionFacade.dispatch() 原顺序是
  1. transition() —— 事务内同步发布 OrderStateChangedEvent,
     BadgeDeviceStatusEventListener.onOrderStateChanged 重新 selectById 拿
     assigneeId 决定是否写 Redis 工单关联;
  2. update assigneeId / assigneeName。

第 1 步事件触发时 assigneeId 仍是 null,listener 走"工单未关联设备,跳过
处理"分支,Redis ops:badge:status:{deviceId} 的 currentOpsOrderId 永远不
会写入;同时主表 assigneeDeviceId 也始终为 null,
CleanOrderAuditEventHandler.handleQueryEvent 用
"WHERE assignee_device_id=?" 查工单永远落空 → "没有工单"。

修复:把执行人字段更新前置到 transition() 之前,并补 setAssigneeDeviceId
(与 OrderLifecycleManagerImpl.dispatch() 自动派单路径一致)。
事件 listener 此时 selectById 拿得到 deviceId,正常落 Redis;audit
查询也命中,按键路径恢复。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
问题:
1. 设备先上线、后绑定为 BADGE 时,"可分配工牌"列表迟迟不出现该设备;
2. 已绑定区域的设备收不到工单(被分派策略过滤掉)。

根因:实时上线路径 BadgeDeviceStatusEventHandler.onMessage 在写
ops:badge:status:{deviceId} Redis 缓存前,先 isBadgeDevice() 校验
ops_area_device_relation 是否存在 BADGE 关系。设备如果在绑定前就上线,
事件被丢弃;建立关系后又没有任何机制回填 Redis,得等 5/30 分钟的
BadgeDeviceStatusSyncJob 对账才会被发现(线上日志可见 deviceId=58 在
绑定后 30 分钟才被对账修正:reason=定时对账修正-上线)。
解绑同样有反向缺口:关系记录删了但 Redis 缓存得等 24h TTL 自然过期,
期间 listAvailableBadges 仍可能返回该设备。

修复:在 ops-biz 引入 AreaDeviceBoundEvent / AreaDeviceUnboundEvent,
bindDevice / unbindDevice 成功后通过 ApplicationEventPublisher 发布;
environment-biz 新增 BadgeAreaBoundEventListener 仅订阅 BADGE 类型,
使用 @TransactionalEventListener(AFTER_COMMIT) + @Async 确保事务提交后
异步执行不阻塞接口:
  - 绑定:单次调 IotDeviceQueryApi.getDevice 取齐 state + nickname +
          deviceName,根据状态写 Redis(IDLE/OFFLINE);
  - 解绑:直接调 BadgeDeviceStatusService.deleteBadgeStatus 清理 Redis。

依赖方向遵循 environment-biz → ops-biz;ops-biz 不反向依赖条线模块,
通过事件解耦,与现有 OrderStateChangedEvent 模式一致。

测试:
- AreaDeviceRelationServiceTest 补 iotDeviceQueryApi mock 让原本因缺
  mock 而 RED 的 testBindDevice_TrafficCounter_Success 转绿;
- 新增对 bound / unbound 事件 publishEvent 调用的 verify。
- 全套 10/10 通过。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
lzh merged commit d6f625151c into master 2026-04-22 18:10:33 +08:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: XW-AIOT/aiot-platform-cloud#2
No description provided.