Commit Graph

493 Commits

Author SHA1 Message Date
lzh
1698c71d84 fix(video): P0 修复 @TableName 映射错误、字段命名、补缺列与 exist=false
@TableName 修正:
- CommonGBChannel: "video_device_channel" → "video_common_gb_channel"
- DeviceChannel: 显式加 @TableName("video_device_channel")
- PlatformChannel: 补充 @TableName("video_platform_channel")
  避免因继承 CommonGBChannel 而写入错误表

字段命名规范化(修复驼峰转 snake_case 错位):
- CommonGBChannel.recordPLan → recordPlanId(对应 record_plan_id)
- MediaServer.httpSSlPort / flvSSLPort / wsFlvSSLPort
  → httpSslPort / flvSslPort / wsFlvSslPort
  (原命名 L 大写/双大写导致转 snake 变 http_s_sl_port)
- 同步更新 Mapper SQL 中的 #{xxx} 引用和 ZLM/ABL 节点服务调用点

@TableField(exist=false) 标注(JOIN 显示字段/业务计算值/内存对象):
- DeviceChannel: parentDeviceId、parentName、ptzTypeText
- DeviceAlarm: deviceName、alarmPriorityDescription、alarmMethodDescription、
  alarmTypeDescription
- AiAlert: cameraName、roiName
- Device: sipTransactionInfo
- MobilePosition: channelDeviceId
- StreamPush: uniqueKey

补齐 SQL 缺失列(需持久化的业务状态字段):
- video_device: channel_count
- video_platform: channel_count / catalog_subscribe / alarm_subscribe
  / mobile_position_subscribe
- video_media_server: status / last_keepalive_time
- video_cloud_record: reserve
- video_device_mobile_position.channel_id: varchar(50) → bigint
  (DO 字段是 Long,作为外键引用 video_device_channel.id)

补齐 DO 缺失字段:
- AiAlgorithm: 新增 globalParams(对应 SQL global_params)
- StreamPush: 新增 status(对应 SQL status)

编译通过(mvn compile BUILD SUCCESS)。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 12:07:47 +08:00
lzh
43becf998c refactor(video): projectId 两级隔离适配 + SQL 融合到 video.sql
SQL 融合:
- sql/mysql/video.sql 作为最终主脚本(26 张表),整合用户改造版表前缀 video_
  与 cherry-pick 版的框架字段(tenant_id/creator/updater/deleted/datetime)
- 新增 video_ai_camera_snapshot(AI 抓拍)、video_common_gb_channel(国标通道抽象)
- 删除旧的 aiot-video.sql(被 video.sql 替代)
- 17 张业务表 + 6 张 AI 业务表加 project_id 列(项目级隔离)
- 2 张字典表(video_ai_algorithm/video_ai_algo_template)仅租户级
- video_media_server 全局共享,无多租户字段

代码改造:
- 21 个 DO 的 @TableName 从 wvp_* 改为 video_*
- 16 个业务 DO 改继承 ProjectBaseDO(StreamProxy/StreamPush 通过
  CommonGBChannel 自动获得),字典 DO 保留 TenantBaseDO,
  MediaServer 保留 BaseDO
- 28 个 Mapper/Provider 的 SQL 表名全部更新为 video_*
- application.yaml 新增 tenant.ignore-project-tables 配置,
  列出 video_media_server/video_ai_algorithm/video_ai_algo_template
  不参与项目隔离
- 编译通过(mvn compile BUILD SUCCESS)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 11:10:27 +08:00
lzh
fe5bbbe8c6 refactor(video): 清理 Security/User stub,替换为框架 SecurityFrameworkUtils
- 删除 SecurityUtils/LoginUser/JwtUtils 3 个空壳 stub
- 删除 UserMapper/RoleMapper/UserApiKeyMapper 3 个废弃 Mapper 及 DTO
- 删除 IUserService/IRoleService/IUserApiKeyService 及实现类
- MediaController 改用 SecurityFrameworkUtils.getLoginUserId()
- 21 个 Controller 的 JwtUtils.HEADER 替换为 "Authorization" 字面量

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 10:15:20 +08:00
lzh
3c752aeb89 refactor(video): PageHelper → PageResult 分页迁移,删除 shim 兼容类
- 54 个文件从 PageHelper.startPage() + PageInfo 迁移到 MyBatis Plus Page + PageResult
- 复杂 @Select 查询加 IPage 参数实现自动分页
- 删除 PageHelper/PageInfo/StringUtil 3 个 shim 文件

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 10:15:20 +08:00
lzh
f48c1846fb refactor(video): Media/Stream 域 ORM 改造 + 全域硬删除转逻辑删除
- MediaServer 继承 BaseDO(共享表无 tenant_id)
- StreamProxy/StreamPush/CloudRecord/RecordPlan DO 改造
- 5 个 Mapper 继承 BaseMapperX
- 32 处 @Delete 硬删除转为逻辑删除(default 方法或 @Update SET deleted=1)
- Service/Controller/RPC 适配 int→Long 类型变化

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 10:15:19 +08:00
lzh
0b0c264dca refactor(video): GB28181 域 ORM 改造 — DO 继承 TenantBaseDO + Mapper 继承 BaseMapperX
- Device/Platform/Channel/Group/Region/Alarm/MobilePosition 等 DO 改造
- 9 个 Mapper 继承 BaseMapperX,简单 CRUD 改为 default 方法
- 复杂多表 JOIN/动态 SQL 保留 @Select 注解
- Service/Controller 适配 int→Long 类型变化

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 10:15:17 +08:00
lzh
0c061ad74c refactor(video): AI 域 ORM 改造 — DO 继承 TenantBaseDO + Mapper 继承 BaseMapperX
- 8 个 AI DO 加 @TableName、继承 TenantBaseDO、主键改 Long
- 9 个 AI Mapper 继承 BaseMapperX,简单 CRUD 改为 default 方法
- Service/Controller 适配类型变化(Integer→Long、删除手动 setCreateTime)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 10:15:15 +08:00
lzh
a604471e6f feat(video): Phase 5-9 编译修复、Controller 路径、数据库 SQL
Phase 5: 全局编译修复 — Security 引用清理、JT1078 引用移除
         PageInfo shim 兼容类(后续迁移到 PageResult)
         web/custom 和 web/gb28181 补充迁移
Phase 6: SecurityConfiguration 更新放行 Hook/SSE 路径
Phase 7: 32 个 Controller 路径 /api/ → /video/
Phase 8: aiot-video 数据库 SQL(25 张表,含多租户 tenant_id + 逻辑删除 deleted)
Phase 9: mvn compile BUILD SUCCESS

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 10:15:14 +08:00
lzh
d2c82d52c9 feat(video): Phase 0-4 WVP 代码搬迁、依赖、Redis 隔离、多租户配置
Phase 0: 前置检查完成(Java 21 语法无风险)
Phase 1: 574 个 Java 文件搬迁,包名替换 com.genersoft.iot.vmp → com.viewsh.module.video
Phase 2: pom.xml 添加 JAIN SIP/dom4j/okhttp/bouncycastle/fastjson2 等依赖
         application.yaml 添加 SIP/Media/UserSettings 配置段
Phase 3: RedisTemplate Bean 隔离(videoRedisTemplate),30+ 文件加 @Qualifier
Phase 4: 多租户配置(ignore-tables: wvp_media_server)

待 Phase 5: ORM 改造 + 编译修复

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 10:15:07 +08:00
lzh
50a826f157 docs(video): WVP-Platform 迁移提案 (proposal + tasks + design)
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 10:15:02 +08:00
lzh
948d2c6a41 feat(video): 新建 viewsh-module-video 服务模块骨架
新增视频管理模块,用于后续迁移 WVP-Platform(GB28181 视频监控平台)。
- viewsh-module-video-api: 契约层(Feign RPC 接口、枚举、错误码)
- viewsh-module-video-server: 业务层(端口 48093)
- 网关路由: video-admin-api / video-app-api
- SecurityConfiguration: 放行 Swagger/Actuator/Druid/RPC

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 10:15:02 +08:00
lzh
65ad3f35e5 Merge branch 'master' into feat/multi-tenant
吸收 master 今日 9 个工单链路修复:
- autoDispatchNext/dispatch 空闲兜底 + FOR UPDATE 并发防护
- 状态转换审计闭环(AFTER_COMMIT/AFTER_ROLLBACK)
- 队列楼层权重强优先 + 三级 baseline 兜底 + N+1 优化
- 工牌 nickname 回填
- CleanOrderAutoCancelJob 超时工单自动取消
2026-04-20 16:04:46 +08:00
lzh
c78759fd52 feat(ops): 新增保洁工单超时自动取消 Job + 集成测试
Some checks failed
Java CI with Maven / build (11) (push) Has been cancelled
Java CI with Maven / build (17) (push) Has been cancelled
Java CI with Maven / build (8) (push) Has been cancelled
背景:保洁工单偶尔因设备离线/信标丢失导致卡在非终态(如 PENDING 超 12h 没派,
DISPATCHED 超 12h 没确认),靠人工清理成本高。补一个每小时跑的 XXL-Job 扫描关单。

实现:
- CleanOrderAutoCancelJob.scanAndCancel:
  * 查询 update_time 距今超 timeoutHours(默认 12h)的 CLEAN 工单
  * 状态白名单 = PENDING/QUEUED/DISPATCHED/CONFIRMED/ARRIVED,**排除 PAUSED**
    (PAUSED 是 P0 打断的产物,应由 resumeInterruptedOrder 走状态机恢复,
    此处若把它 CANCEL,会破坏 P0 完成后的 resume 链路)
  * 调用 orderLifecycleManager.cancelOrder 走完整责任链,事件监听器负责
    TTS 停播/设备关联回收/审计日志
  * cancel 前再 selectById 做乐观校验:若 update_time 已刷新或状态已变
    (COMPLETED/CANCELLED/PAUSED),跳过;避免候选装内存到实际 cancel
    之间用户刚触达的工单被误杀
  * 单单独立 try/catch 隔离,单条失败不断批
  * batchSize 限流(默认 200),事件风暴防护
- application.yaml 补默认配置:viewsh.ops.clean.auto-cancel.{timeout-hours, batch-size}
- CleanOrderAutoCancelJobTest 覆盖 6 条不变量:
  无候选零计数、全成功、部分失败不中断、乐观锁跳过 stale、终态跳过、PAUSED 跳过

XXL-Job 配置建议:
- JobHandler: cleanOrderAutoCancelJob
- Cron: 0 17 * * * ? (每小时 :17,避开整点尖峰)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 15:21:33 +08:00
lzh
ba6f94a279 fix(ops): review 复盘补齐 FOR UPDATE 覆盖面 + 清理注解/日志死角
今日 review 发现 Bug #2 的 FOR UPDATE 防线只装在 dispatch() 上,但同文件另有两条
路径绕过它:

1. P1 — DispatchEngineImpl.autoDispatchNext 调 transition() 派发队列下一单,
   不走 FOR UPDATE。idle 校验和 transition 之间存在竞争窗口,能再次让同 assignee
   挂两条 DISPATCHED。改调 dispatch(),天然继承串行化。
   补测 autoDispatchNext_whenDispatchingFromQueue_shouldGoThroughDispatchNotTransition
   锁定该不变量。

2. P2 — OrderLifecycleManagerImpl.resumeOrder/resumeInterruptedOrder 同样走
   transition(),P0 恢复与并发派发竞争时可能产生两条 DISPATCHED。改为先
   selectById 取 assigneeId,改调 dispatch() 让同一检查生效。

顺手清理 3 个误导:

- DispatchEngineImpl.executePushAndEnqueue 原先忽略内部 dispatch 的返回值,
  并发场景下会输出假的“已推送等待任务”日志误导运维,改为按 result.isSuccess()
  分支打印。
- OrderTransitionAuditListener.writeRollbackAudit 的 @Transactional(REQUIRES_NEW)
  是死注解(由 onAfterRollback 自调用,Spring 代理无法拦截;且 AFTER_ROLLBACK
  本就无事务),移除并更新 Javadoc 说明实际行为。
- OrderQueueServiceEnhanced.triggerQueueRebuildAfterCommit 的自调用绕过
  @Transactional 是设计意图(最终一致即可),补 Javadoc 解释事务边界,
  避免后续误判为 bug。

测试:ops-biz 56 个相关用例全部通过,含新增的 P1 锁定测试。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 14:51:32 +08:00
lzh
9f3ca9c6f2 test(ops): 补齐工单链路 5 个修复点的集成测试
与 4d85659…a5f916c 的 5 次修复对齐,用 Mockito 风格覆盖状态链路关键分支:
- DispatchEngineIdleCheckTest:autoDispatchNext 空闲兜底 + executeDispatch
  MySQL 活跃态降级(Bug #1/#4),ENQUEUE_ONLY 路径不触发兜底查询避免开销浪费
- DispatchEngineConflictFallbackTest:FOR UPDATE 冲突分支(Bug #2),
  PENDING → 降级入队、QUEUED → 保持排队、其他错误码 → 硬失败
- OrderTransitionAuditListenerTest:审计闭环(Bug #7),AFTER_COMMIT 成功/WARN/ERROR
  分支 + AFTER_ROLLBACK 强制视为失败 + 7 种目标状态映射
- QueueScoreCalculatorEnhancedTest:楼层权重 G+B,锁死"FLOOR×10 > AGING×240"
  不变量,验证 base/target 任一 null → score=0,移除旧 +600 罚分后语义对称

22 个新测试全部通过;模块内 115/117 测试通过,2 个 pre-existing 失败
(VspNotifyClient/AreaDeviceRelation) 依赖外部服务,与本次改动无关。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 14:24:07 +08:00
lzh
323ddf27fb fix(ops): 对账回填工牌 nickname,修复重启后派单人名降级为 deviceCode
根因:BadgeDeviceStatusSyncJob 硬编码 nickname=null,依赖 Redis 已有值。
重启后若 ops:badge:device:{deviceId} 的 nickname 丢失(TTL/清理/首次写入),
BadgeDeviceAreaAssignStrategy 会降级用 deviceCode,导致 assigneeName 变成 "43607737587"。

- SyncJob 注入 IotDeviceQueryApi,批量拉 IotDeviceSimpleRespDTO.nickname 做回填
- 状态一致但 Redis 缺 nickname 时也补写一次,覆盖最常见的重启路径
- AreaAssignStrategy 降级兜底改为 "工牌-尾号",避免再把裸 deviceCode 当人名暴露

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 13:50:00 +08:00
lzh
a5f916c62a fix(ops): 队列楼层权重修复——强楼层优先 + 闭环基准兜底 + N+1 优化
问题:楼层差在分数公式中本该主导同优先级排序,但有四个缺陷导致效果不稳:

1. 有 base 无 target 时给 +600 罚分,无 base 时则全免罚——同一工单在
   保洁员忙/闲时排序不单调(B)。
2. 基准楼层只在 user 有 PROCESSING 时生效,空闲时完全无楼层信号(A)。
3. enqueue 瞬间 score 不含楼层,要等下一轮 rebuild 才补上(H)。
4. aging 上限 720 > floorDiff 上限 600,等满 4 小时可反超同优先级 10 层差
   任务,削弱"强楼层优先"语义(G)。
5. rebuild 内 for 循环对每条 WAITING 单独 selectById(order)+selectById(area),
   N+1 问题(F)。

修复:

1. QueueScoreCalculator(B + G)
   - FLOOR_WEIGHT 60 → 100:上限 1000 > aging 上限 720,4 小时老化不再反超
     同优先级的近楼层任务。
   - 删除"有 base 无 target +600"分支:任一侧缺失即 score=0,语义对称。

2. OpsOrderMapper.selectLatestCompletedAreaIdByAssignee(A 二级兜底)
   查最近 24h 内已完成工单的 area,用来推断空闲保洁员的物理位置。
   超过 24h 视为跨班次、轨迹失效。

3. OrderQueueServiceEnhanced.resolveBaselineAreaId(A 三级兜底)
   PROCESSING.area → 最近 24h COMPLETED.area → 调用方传的 fallbackAreaId。

4. OrderQueueServiceEnhanced.enqueue(H)
   事务提交后 triggerQueueRebuildAfterCommit(userId, null),新入队工单
   立即按楼层差参与排序,不依赖下一次 autoDispatchNext 触发。

5. OrderQueueServiceEnhanced.rebuildWaitingTasksByUserId(F)
   批量 selectBatchIds(orders) + selectBatchIds(areas),100 条 WAITING
   从 200 次 SELECT 降到 2 次。

权重直观对比(P2=priority×1500=3000):
             旧分数         新分数
同层刚入队    3000          3000
差5层刚入队   3000+300=3300 3000+500=3500
差5层等2小时  3000+300-360=2940 3000+500-360=3140
同层等4小时   3000+0-720=2280   3000+0-720=2280

新权重下"差5层等2小时"仍大于"同层刚入队",楼层稳定主导排序;
极端 aging(>4h)仍能让同层任务被近楼层任务压制优先执行。

测试:QueueScoreCalculatorTest(3)、OrderQueueServiceEnhancedTest(1,
已按 selectBatchIds + selectActiveListByUserId 更新 mock)、QueueSyncServiceTest
全绿。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 13:32:24 +08:00
lzh
3e248fee8c fix(ops): 补齐状态转换审计闭环,回滚场景也留痕到 bus_log
问题:ops_order_event 在主事务内写,事务 rollback 则整段记录消失;
若状态机转换抛异常或并发冲突被拒,线上只有控制台日志而无数据库审计,
运维难以追溯"是谁、在什么时候、尝试做了什么转换、为什么失败"。

设计:中央事件发布 + TransactionalEventListener 双阶段落盘

1. OrderTransitionAttemptedEvent(新)
   覆盖 transition 成功、失败、FOR UPDATE 被拒三种情况,携带 orderId、
   fromStatus、targetStatus、errorCode、errorMessage、causeSummary 等。

2. OrderLifecycleManagerImpl
   - transition 成功分支:publishAttempt(success=true)
   - transition 失败分支(context.hasError):publishAttempt(success=false,
     errorCode=INVALID_TRANSITION, cause=摘要)
   - dispatch FOR UPDATE 命中分支:publishAttempt(success=false,
     errorCode=ASSIGNEE_HAS_ACTIVE_ORDER)
   publishAttempt 内部 try/catch,审计失败不影响主流程。

3. OrderTransitionAuditListener(新)
   - @TransactionalEventListener(AFTER_COMMIT, fallbackExecution=true)
     主事务已提交,按事件本身的 success 写 bus_log;INFO 级。
   - @TransactionalEventListener(AFTER_ROLLBACK) + @Transactional(REQUIRES_NEW)
     主事务已回滚,事件里声称的 success 强制视为失败;独立事务写 bus_log
     避免因主事务回滚而日志同样丢失。
   - errorCode、fromStatus、targetStatus、reason、cause 全部落 payload。
   - 冲突(ASSIGNEE_HAS_ACTIVE_ORDER)→ WARN;其他失败 → ERROR。

4. LogType 新增 TRANSITION_FAILED、DISPATCH_REJECTED。
5. EventLogRecorder 接口补 recordSync(实现类已有同名方法)。

运维查询:按 eventDomain='dispatch' + eventLevel IN ('WARN','ERROR')
即可一眼看出所有"尝试但未成功"的状态转换。errorCode 留在 payload JSON 内,
未升级为一等字段(后续如需聚合统计再迁移)。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 13:11:28 +08:00
lzh
b534d79434 fix(ops): 派发入口加 FOR UPDATE 并发兜底,冲突时降级入队避免悬空
业务不变量:同一执行人在任一时刻最多只有 1 条活跃工单
(DISPATCHED/CONFIRMED/ARRIVED)。PAUSED 不纳入——P0 打断恢复
走 PAUSED→DISPATCHED,此处必须放行。

实现:

1. OpsOrderMapper.selectActiveByAssigneeForUpdate
   查询 assignee 活跃工单并对命中行加 FOR UPDATE 排他锁。必须在
   事务中调用。

2. OrderLifecycleManagerImpl.dispatch 入口校验
   事务开启后立即执行 FOR UPDATE 查询,命中则返回带错误码
   ASSIGNEE_HAS_ACTIVE_ORDER 的失败结果,不再执行责任链,
   事务 commit 空操作、锁释放;并发竞争的第二个线程会阻塞到
   第一个 commit 后看到活跃单,失败退出。

3. 新增 TransitionErrorCode 枚举 + OrderTransitionResult.errorCode
   调用方可区分需降级的冲突与硬失败,避免把"可降级"的结果
   直接抛给用户。

4. DispatchEngineImpl.executeDirectDispatch 降级逻辑
   - 冲突 + 原状态 PENDING → 调 executeEnqueueOnly 降级到 QUEUED,
     工单不悬空,等下一轮 autoDispatchNext 重挑。
   - 冲突 + 原状态已是 QUEUED(并发另一路抢先派发时回滚保留)
     → 返回 fail 但不重复入队,天然等下一轮。
   - 其他失败 → 照常 fail。

职责划分:
- 生命周期层负责"拒绝违反不变量的转换"
- 编排层负责"失败后给工单安置归宿"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 11:52:38 +08:00
lzh
c24b1eb641 fix(ops): 直接派发加空闲兜底 + 队列同步按活跃状态过滤
1. 直接派发空闲兜底(补 autoDispatchNext 之外的另一条派发入口)
   DispatchEngineImpl.executeDispatch 在 DIRECT_DISPATCH/PUSH_AND_ENQUEUE
   前增加 MySQL 兜底校验:若执行人仍挂活跃工单(Redis 判空闲但 MySQL
   不一致的场景),强制降级为 ENQUEUE_ONLY 让任务进队列等待下一轮
   autoDispatchNext 接力。避免同一设备再次出现并行多单。

2. 队列同步按活跃状态过滤
   syncUserQueueToRedis / getTasksByUserId 的 MySQL 回填路径此前调用
   selectListByUserId 不过滤状态,会把历史 REMOVED 记录一并同步到
   Redis(线上观察到设备 31 的 Redis ZSet 塞了 206 条、其中 205 条是
   REMOVED)。新增 OpsOrderQueueMapper.selectActiveListByUserId,只返
   回 WAITING/PROCESSING/PAUSED,两条同步链路改走此方法。原 selectList
   ByUserId 保留给审计/统计场景。

未清理历史 REMOVED 记录,保留审计追溯。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 11:22:18 +08:00
lzh
4d85659277 fix(ops): 修复同一工牌并行多单的状态错乱
线上观察:管理员手动取消一个僵尸 DISPATCHED 单会引发"越清越多"——
系统顺势派队列首条给仍在工作的保洁员,监听器再用"旧工单残留"机制
尝试取消当前正在执行的工单,该取消走 REQUIRES_NEW 独立事务且吞异常,
最终新单落地、旧单残留,同一设备挂多个非终态工单。

修复两处:

1. DispatchEngineImpl.autoDispatchNext 入口加设备空闲校验:
   若执行人名下还有 DISPATCHED/CONFIRMED/ARRIVED/PAUSED 工单(排除
   completedOrderId),直接早返回,不再派发。所有调用方(保洁/安保
   handleCancelled、asyncCompleteAndDispatchNext、xxl-job 空闲扫描)
   自动受保护。新增 OpsOrderMapper.selectActiveByAssignee。

2. BadgeDeviceStatusEventListener.handleDispatched 移除"残留取消":
   旧逻辑用 REQUIRES_NEW 事务 + 吞异常,是对数据已错乱场景的暴力兜底,
   失败时导致误杀。改为只打 ERROR 告警暴露问题,仅清理 Redis 关联。
   真正的防线在 DispatchEngine 入口。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 10:54:54 +08:00
lzh
423bf3ec3f feat(tenant): 实现 ProjectSecurityWebFilter 项目权限集合校验
新增 ProjectSecurityWebFilter:
- 集合校验: user.authorizedProjectIds.contains(header.projectId)
- 默认项目选择: DEFAULT编码 → 最小ID → 单项目自动选中 → 无授权403
- @ProjectIgnore URL 自动跳过
- 注册在 WebFilterOrderEnum.PROJECT_SECURITY_FILTER (-98)

框架层:
- ProjectCommonApi: 新增 getAuthorizedProjectIds, getDefaultProjectId
- ProjectFrameworkService: 新增授权查询 + Caffeine 缓存(60s/1000条)
- ViewshTenantAutoConfiguration: 注册 Filter + 扫描 @ProjectIgnore

业务层:
- ProjectService: 新增 getAuthorizedProjectIds, getDefaultProjectId
- ProjectServiceImpl: 默认项目3级回退逻辑
- ProjectApiImpl: 实现 Feign 端点

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 23:35:56 +08:00
lzh
c85f84ea46 test(tenant): Phase 4 — 项目隔离集成测试
新增测试 (33个全部通过):
- ProjectContextHolderTest (8 tests): set/get/clear/ignore/线程隔离
- ProjectUtilsTest (14 tests): execute/executeIgnore/嵌套调用/异常恢复
- DualInterceptorTest (11 tests): 双拦截器 SQL 注入验证 (已有)

DO 迁移验证:
- 15 个 DO 已迁移到 ProjectBaseDO (grep 确认)
- 3 个非迁移 DO 正确保持 TenantBaseDO

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 22:52:19 +08:00
lzh
a2f500fa20 feat(tenant): 租户-项目两级架构 Phase 2 — IoT + Ops 业务迁移
DO 迁移 (15个 TenantBaseDO → ProjectBaseDO):
- IoT: IotDeviceDO
- Ops 核心: OpsOrderDO, OpsOrderEventDO, OpsOrderDispatchDO, OpsOrderQueueDO,
  OpsBusAreaDO, OpsAreaDeviceRelationDO, OpsDeviceTrajectoryDO
- Ops 保洁: OpsOrderCleanExtDO, OpsCleanerStatusDO, OpsCleanerPerformanceMonthlyDO,
  OpsInspectionRecordDO, OpsInspectionRecordItemDO
- Ops 安保: OpsOrderSecurityExtDO, OpsAreaSecurityUserDO

IoT 适配:
- IotDeviceRespDTO 新增 projectId 字段
- IotDeviceMessage 新增 projectId 字段
- IotDeviceMessageServiceImpl.appendDeviceMessage() 设置 projectId
- IotCleanRuleMessageHandler 嵌套 ProjectUtils.execute() 设置项目上下文

缓存改造:
- ProjectRedisCacheManager extends TenantRedisCacheManager,追加 :projectId 后缀
- ViewshTenantAutoConfiguration 替换为 ProjectRedisCacheManager

SQL 迁移脚本 (sql/mysql/project/):
- 01-create-tables.sql: system_project + system_user_project 建表
- 02-default-data.sql: 默认项目 + 用户关联回填
- 03-alter-business-tables.sql: 15 张表添加 project_id (NULL → 回填 → NOT NULL → 索引)
- 04-index-audit.sql: 现有索引审计 + project_id 补充建议
- 99-rollback.sql: 完整回滚方案

附带修复:
- fix(ops): UserDispatchStatusServiceImpl 添加缺失的 KEY_PREFIX 常量

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 22:27:34 +08:00
lzh
87beb1228e feat(tenant): 租户-项目两级架构 Phase 0+1 — 基础框架层
Phase 0 技术验证:
- ProjectBaseDO extends TenantBaseDO,新增 projectId 字段
- ProjectContextHolder (TransmittableThreadLocal) 项目上下文管理
- ProjectDatabaseInterceptor 实现 TenantLineHandler,返回 project_id 列
- 注册第二个 TenantLineInnerInterceptor,通过 @Qualifier 保证初始化顺序
- DualInterceptorTest 11 个用例验证双拦截器 SQL 注入(SELECT/INSERT/UPDATE/DELETE + JOIN + 子查询)

Phase 1 基础框架层:
- @ProjectIgnore 注解 + ProjectIgnoreAspect (SpEL 条件支持)
- ProjectUtils 工具类 (execute/executeIgnore)
- ProjectContextWebFilter 从请求 Header 解析 project-id
- WebFrameworkUtils 扩展 HEADER_PROJECT_ID + getProjectId()
- WebFilterOrderEnum 新增 PROJECT_CONTEXT_FILTER、PROJECT_SECURITY_FILTER
- RPC: TenantRequestInterceptor 自动透传 project-id
- MQ: Kafka/RocketMQ/RabbitMQ/Redis 全部支持 project-id 发送与消费
- @ProjectJob + ProjectJobAspect (@Order(2) 内层,配合 @TenantJob 使用)
- TenantJobAspect 补充 @Order(1) 外层标记
- ProjectDO + UserProjectDO + Mapper + ProjectService + ProjectController
- ProjectCommonApi (Feign) + ProjectApiImpl + ProjectFrameworkServiceImpl (Guava 缓存)
- TenantServiceImpl.createTenant() 联动创建默认项目
- ErrorCodeConstants 新增 1-002-030-xxx 项目错误码

Review 修复:
- Bean 初始化顺序: projectLineInnerInterceptor 依赖 @Qualifier 确保顺序
- computeIgnoreTable: @ProjectIgnore 检查优先于 isAssignableFrom
- ProjectFrameworkServiceImpl 注册为 Spring Bean
- RocketMQ SendHook: project-id 独立于 tenantId 传播
- createDefaultProject 移入 TenantUtils.execute 事务块内
- 全部 MQ/RPC 统一使用 HEADER_PROJECT_ID 常量
- ProjectJobAspect 增加租户上下文防御校验
- 移除 ProjectDO/UserProjectDO 无效的 @KeySequence
- ProjectServiceImpl/ProjectApiImpl 移除冗余 TenantUtils.execute 嵌套

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 19:22:57 +08:00
lzh
73e67dd3ec Merge branch 'master' into feat/multi-tenant
Resolve conflicts by accepting master changes for:
- Jenkinsfile (CI/CD release/next branch support)
- OrderCodeGenerator (Redis seq sync fix)
- OrderCodeGeneratorTest (updated tests)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 18:08:14 +08:00
lzh
40a1593bb0 chore: init gsd 2026-04-16 15:28:49 +08:00
lzh
6bbd49355d fix(ops): 修复工单编号生成器 Redis 序号与数据库不同步导致的重复编号问题
Some checks failed
Java CI with Maven / build (11) (push) Has been cancelled
Java CI with Maven / build (17) (push) Has been cancelled
Java CI with Maven / build (8) (push) Has been cancelled
问题:Redis 重启或 key 过期后序号从 1 重新计数,与数据库已有编号冲突。

修复方案:
- 应用启动后首次生成时,从数据库查询当天最大序号校准 Redis
- 使用 Lua 脚本原子操作(校准 + 自增),避免并发竞态
- 后续调用走纯 Redis INCR,无额外数据库开销
- SQL 使用 deleted = b'0' 兼容 bit(1) 列类型
- LIKE 查询转义 % 和 _ 通配符
- 校准异常向上抛出,避免静默产生重复
- calibratedKeys 跨天自动清理旧条目

同步更新单元测试,覆盖校准、纯 Redis 自增、异常处理、SQL 转义等 13 个用例。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 23:19:20 +08:00
lzh
7707455a24 feat(ops): 手动派单放宽校验,支持跨区域和向忙碌设备派单
移除 canAcceptNewOrder、区域绑定和区域匹配校验,仅保留在线检查。
手动派单由调度员人工判断合理性,自动派单的校验仍在 BadgeDeviceAreaAssignStrategy 中完成。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 23:18:54 +08:00
lzh
ea374d131a feat(ops): 工牌状态返回昵称字段,手动派单支持传入设备名称
- BadgeStatusRespDTO 新增 nickname 字段,透传设备昵称
- CleanManualDispatchReqDTO 新增 assigneeName,派单时携带设备显示名
- CleanWorkOrderServiceImpl 将 assigneeName 传递给派单引擎

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 23:18:30 +08:00
lzh
a32a4375bc build(ci): CI/CD 支持 release/next 预发布分支
Some checks failed
Java CI with Maven / build (11) (push) Has been cancelled
Java CI with Maven / build (17) (push) Has been cancelled
Java CI with Maven / build (8) (push) Has been cancelled
- Jenkinsfile: Deploy 和 Health Check 阶段支持 release/next 分支
- release/next 部署到 staging 服务器(172.17.16.7),master 部署到 prod
- 仅 master 分支推送 latest 镜像标签,避免预发布覆盖生产镜像
- GitHub Actions 添加 release/next 分支触发构建

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 14:44:20 +08:00
lzh
705717a5b1 Merge branch 'master' into feat/multi-tenant
# Conflicts:
#	viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/dal/redis/TrafficActiveOrderRedisDAO.java
#	viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/badge/BadgeDeviceStatusServiceImpl.java
#	viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/dispatch/UserDispatchStatusServiceImpl.java
2026-04-13 14:35:27 +08:00
lzh
1ca472ea93 feat(iot): 客流计数器支持累计值上报模式(CUMULATIVE)
Some checks failed
Java CI with Maven / build (11) (push) Has been cancelled
Java CI with Maven / build (17) (push) Has been cancelled
Java CI with Maven / build (8) (push) Has been cancelled
TrafficThresholdConfig 新增 reportMode 字段,支持 INCREMENTAL(默认)和 CUMULATIVE 两种模式。
累计值设备通过 Redis 存储上次值自动算差值,处理首次上报跳过和设备重启归零场景。
现有增量设备无需改配置,行为完全兼容。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 13:59:20 +08:00
lzh
c8ba3e63cb feat(iot): 新增恒华D5客流摄像机编解码器,对接拌线人数统计(type=1)
Some checks failed
Java CI with Maven / build (11) (push) Has been cancelled
Java CI with Maven / build (17) (push) Has been cancelled
Java CI with Maven / build (8) (push) Has been cancelled
走通用路由,新增 IotHenghuaD5Codec 解析 form-urlencoded 格式数据,
映射 InNum/OutNum 到 people_in/people_out,业务层完全复用现有客流阈值逻辑。
IotHttpUpstreamHandler 增加恒华D5 专用简洁响应。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 14:59:53 +08:00
lzh
04c61a41db fix(ops): 修复 CleanBadgeServiceImpl 调用不存在的 queryAreaNameById 方法导致编译失败
Some checks failed
Java CI with Maven / build (11) (push) Has been cancelled
Java CI with Maven / build (17) (push) Has been cancelled
Java CI with Maven / build (8) (push) Has been cancelled
改用 OpsBusAreaDO.getAreaName() 获取区域名称

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 10:45:10 +08:00
lzh
b379fc6741 feat(ops): timeline 接口 deviceId 改为可选,支持全设备查询
Some checks failed
Java CI with Maven / build (11) (push) Has been cancelled
Java CI with Maven / build (17) (push) Has been cancelled
Java CI with Maven / build (8) (push) Has been cancelled
不传 deviceId 时查询该日期所有设备的轨迹记录,复用
selectByDateAndDevice 的 LIMIT 5000 安全上限。

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 15:27:05 +08:00
lzh
54f78f8066 feat(ops): 工牌实时状态增加物理位置、电量和工单信息
BadgeRealtimeStatusRespDTO 新增物理位置(IoT 轨迹检测 RPC)、
电量(IoT 设备属性 RPC)、当前工单信息三个维度。
RPC 调用改为串行执行避免占用 ForkJoinPool 公共线程。
设备状态写入 Redis 时同步写入区域名称。

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 15:26:43 +08:00
lzh
9ffaac5c91 feat(ops): 新增轨迹统计接口 summary/hourly-trend/area-stay-stats
- summary: KPI 卡片(作业时长、覆盖区域数、事件数、平均停留)
- hourly-trend: 按小时聚合出入趋势
- area-stay-stats: 区域停留分布(含 fullAreaName,按时长降序)
- deviceId 可选,不传则汇总全部设备
- selectByDateAndDevice 加 LIMIT 5000 安全上限
- 删除无调用方的 selectTimeline 方法
- enrichWithAreaInfo 改用 buildPaths 批量构建路径

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 15:26:14 +08:00
lzh
368fa90156 refactor(ops): 轨迹区域展示改用 fullAreaName 替代 buildingName/floorNo
TrajectoryRespDTO 移除 buildingName、floorNo 字段,新增 fullAreaName
(完整路径如"A园区/A栋/3层/男卫")。AreaPathBuilder 新增 buildPaths
批量方法,一次查询所有父级区域避免 N+1;正则预编译为静态常量。

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 15:25:47 +08:00
lzh
9780d6c3f7 fix(ops): 区域设备 RPC 接口添加 @TenantIgnore 解决定时任务调用时租户上下文缺失
Some checks failed
Java CI with Maven / build (11) (push) Has been cancelled
Java CI with Maven / build (17) (push) Has been cancelled
Java CI with Maven / build (8) (push) Has been cancelled
IoT 模块 BeaconRegistryServiceImpl 每30分钟通过 Feign 调用 /beacons/all 接口,
因定时任务无租户上下文导致 TenantContextHolder NPE。对跨租户查询的方法添加
@TenantIgnore 注解忽略多租户过滤。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 09:23:27 +08:00
lzh
da00f08262 fix(environment): 对账修复后同步清理 TTS 循环播报
Some checks failed
Java CI with Maven / build (11) (push) Has been cancelled
Java CI with Maven / build (17) (push) Has been cancelled
Java CI with Maven / build (8) (push) Has been cancelled
- BadgeDeviceStatusSyncJob 在修复设备工单一致性后额外停止 TTS 循环\n- 避免工单已清除但语音循环标记残留,导致设备继续播报\n- 对 TTS 清理失败增加 warn 日志,避免影响主对账流程
2026-03-31 22:58:40 +08:00
lzh
5d46502fb9 fix(ops): 启动时恢复工单队列缓存
- 新增 OrderQueueInitializer\n- 服务启动时调用 QueueSyncService.forceSyncAll()\n- 在 Redis 队列数据丢失或过期后,自动用 MySQL 数据回填 Sorted Set
2026-03-31 22:58:25 +08:00
lzh
306303ab16 fix(ops): 启动时校准人员调度状态
- 为 UserDispatchStatusService 增加基于 DB 的重建能力\n- 扫描 Redis 中的人员调度 key,按实际活跃工单数修正 status、activeOrderCount、waitingTaskCount\n- 新增启动初始化器,服务启动时自动执行一次校准,缓解事件丢失导致的 BUSY 残留
2026-03-31 22:58:09 +08:00
lzh
1696aeb287 fix(clean): 取消工单前先清理客流活跃标记
- 调整 CANCELLED 事件处理顺序\n- 先移除 area 级活跃工单 Redis 标记,再执行后续取消逻辑\n- 避免后续取消处理异常时遗留错误的活跃状态
2026-03-31 22:57:44 +08:00
lzh
f0fa5f1c46 fix(clean): 补齐客流活跃工单缓存自愈逻辑
- 为客流活跃工单 Redis 标记补充 TTL,避免长期残留\n- 创建工单前命中 Redis 时回查 DB,自动清理终态脏数据并刷新过期状态\n- 新增启动校准器,服务启动时批量清理或刷新 area 级活跃工单缓存
2026-03-31 22:57:28 +08:00
lzh
d3eecc63ef feat(trajectory): 新增轨迹后台查询与实时位置接口
- 新增轨迹分页、时间线、统计摘要等查询 DTO\n- 提供轨迹后台控制器,支持工牌下拉、轨迹查询、实时位置查询\n- 接入 TrajectoryStateApi 的 Feign 配置,打通 Ops 对 IoT 实时位置状态的读取
2026-03-31 22:56:49 +08:00
lzh
bf5aa21648 feat(trajectory): 新增轨迹事件消费与落库模型
- 新增 ops_device_trajectory 表及轨迹数据对象、Mapper\n- 消费 trajectory-enter / trajectory-leave 事件并做幂等处理\n- 落地设备进入/离开区域记录,补充停留时长与离开原因字段\n- 在服务层封装轨迹写入、关闭未离场记录等核心逻辑
2026-03-31 22:56:18 +08:00
lzh
11dcb57ff3 feat(trajectory): 新增轨迹检测与 Beacon 注册表 2026-03-31 22:53:06 +08:00
lzh
4c3350377e build(ci): hardcode stage deploy target 2026-03-30 23:37:02 +08:00
lzh
9e2b510cb8 build(ci): limit latest image pushes to master 2026-03-30 23:23:44 +08:00