Commit Graph

480 Commits

Author SHA1 Message Date
lzh
c807bf1fab fix(ci): 补 3 个相关隐患——backup 写死 core.yml、deploy 漏 export IMAGE_TAG、NonCPS 读 env
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
排查 PROD 误伤事故时连带发现 3 个会引发其他错误的位置:

1. backupCurrentDeployment 在远端 cp docker-compose.core.yml.backup 写死了 core 文件名
   → release 部署到 .7 时 core.yml 不存在会触发 set -e 退出。改为 cp ${env.COMPOSE_FILE}
   并加 [ -f ... ] 检查避免硬失败。

2. deployService 在 ssh 远端命令里 docker compose pull/up 之前没 export IMAGE_TAG,
   docker compose 会 fallback 到 yml 的 ${IMAGE_TAG:-latest},永远拉到 :latest 镜像
   而不是本次构建的版本 tag。这就是 PROD 容器镜像显示 :latest 的根因——本意要拉
   master-N-shortSHA 的镜像,但实际拉了 master 早先 push 的 :latest。
   修复:注入 export IMAGE_TAG=${env.IMAGE_TAG} + REGISTRY_HOST。

3. getContainerNameForService 是 @NonCPS 函数,里面访问 env.CONTAINER_NAME_SUFFIX
   在 NonCPS 上下文下 binding 不一定可达。改成把 suffix 作为参数传入,3 个调用点
   全部加上 env.CONTAINER_NAME_SUFFIX 实参。函数纯粹无副作用。
2026-04-28 17:43:38 +08:00
lzh
8148bf7471 fix(ci): 修 release/next 误部署到 PROD 的严重 bug + 容器名 -release 物理隔离
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
事故复盘:build #5 触发 release/next 部署,但 Initialize 阶段
  env.DEPLOY_HOST = env.RELEASE_DEPLOY_HOST
没有生效,DEPLOY_HOST 保持 environment 块默认值 172.17.16.14(PROD),导致
release.yml 被部署到 PROD 服务器;同时容器名与 prod 同名(aiot-gateway 等),
docker compose up -d 直接 force-recreate prod 容器,配置切到 release 库 / Nacos
namespace / Redis db1 — prod 业务断了。

根因:Jenkins declarative pipeline 的 environment 块声明的变量是 build-scope
constant,在 script 块里 env.X = ... 的赋值在某些场景不生效。

修复:
1. environment 块只声明常量 PROD_DEPLOY_HOST/PROD_DEPLOY_PATH/RELEASE_DEPLOY_HOST/
   RELEASE_DEPLOY_PATH,DEPLOY_HOST/DEPLOY_PATH/COMPOSE_FILE/CONTAINER_NAME_SUFFIX
   全部在 Initialize 阶段动态创建(不在 environment 声明则 env.X = 赋值生效)
2. 增加防呆:未知分支(既不是 master 也不是 release/next)DEPLOY_HOST 设空,
   后续 ssh 命令会因目标空直接报错,不会误伤任何机器
3. release 容器名加 -release 后缀(aiot-gateway-release 等),物理隔离:
   即便部署目标 host 错了,容器名不与 prod 重叠,docker compose 不会 recreate
   prod 同名容器
4. getContainerNameForService 改读 env.CONTAINER_NAME_SUFFIX(Initialize 阶段写入),
   不再依赖 @NonCPS 函数里访问 env.BRANCH_NAME

prod 影响:master 分支行为完全不变(DEPLOY_HOST→PROD_DEPLOY_HOST 同值、容器名
suffix='')。
2026-04-28 17:38:17 +08:00
lzh
7950d87a73 fix(ci): release Redis 用 db1 与 prod 隔离
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
prod 走 Redis 默认 db0,release 与 prod 共用同一个 Redis 实例必须靠 db 索引隔离,
否则 key 前缀冲突会读写到对方数据。

改动:docker-compose.release.yml 给 common-env 与 iot-gateway 都加:
  SPRING_DATA_REDIS_DATABASE: 1
2026-04-28 17:09:24 +08:00
lzh
db91e9503e refactor(ci): release 走独立 compose 文件,prod 文件回滚成历史原版
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
之前的参数化(docker-compose.core.yml 用 \${VAR:-default} + .env 注入)让运维要在
两台部署机分别维护 .env,体验跟 prod 现状不一致。改回与 prod 同款:每个环境一个
独立的 compose 文件,配置直接硬编码在 yml 里。

改动:
- 新增 docker-compose.release.yml(release 专用:MySQL aiot-platform-release 库、
  Nacos namespace e635b215-...、TDengine database aiot_platform_release、
  XXL-Job executor IP=.7、appname 加 -release 后缀、RocketMQ 内网 .7:9876)
- docker-compose.core.yml 完全恢复到 master 版本(prod 文件未做任何改动)
- 删除 env/ 目录(prod.env.example / release.env.example / .gitignore 都不需要了)
- Jenkinsfile:
  - Initialize 阶段按分支选 COMPOSE_FILE:master→core.yml、release/next→release.yml
  - 所有 docker compose 命令统一用 -f \${env.COMPOSE_FILE}
  - Pre-deploy 移除 .env 文件存在性检查
  - 删除 checkRemoteEnvFileOrFail helper(不再使用)

application.yaml 里的 \${XXL_JOB_EXECUTOR_APPNAME_SUFFIX:} 与 application-prod.yaml
里的 \${TDENGINE_DATABASE:aiot_platform} 保留——默认值与历史一致,prod 行为零变化,
但给 release.yml 注入这两个变量留了入口。
2026-04-28 17:00:24 +08:00
lzh
516259b540 fix(ci): docker compose --env-file 仅 release/next 启用,prod 完全不动
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
前一版改动会让 master→prod 部署也走 --env-file .env / Pre-deploy 强制 .env 检查,
若 prod 部署机(172.17.16.14)没准备 .env 会直接 fail,破坏现有 prod 部署。

改动:
- Initialize 阶段按分支设置 COMPOSE_ENV_FILE_ARG:
    release/next → '--env-file .env'
    master/其他   → ''
- 所有 docker compose 命令用 ${env.COMPOSE_ENV_FILE_ARG} 拼接
- Pre-deploy Check 的 .env 文件存在性校验仅 release/next 触发

行为:
- master → prod 完全沿用历史路径(docker-compose.core.yml 内嵌默认值兜底)
- release/next → release 强制注入 .env(环境隔离 + 凭据脱离 git)
2026-04-28 16:55:46 +08:00
lzh
7c45f56804 chore(ci): 统一预发环境命名 staging → release
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
- env/staging.env.example → env/release.env.example(git mv 保留历史)
- Jenkinsfile:STAGING_DEPLOY_HOST/PATH → RELEASE_*,日志和注释同步
- docker-compose.core.yml、5 个 application.yaml 注释里的 staging → release
- TDengine database:aiot_platform_staging → aiot_platform_release
- XXL-Job appname 后缀:-staging → -release

仅命名调整,不动任何运行行为。
2026-04-28 16:45:11 +08:00
lzh
602217274c build(ci): docker-compose 多环境参数化 + staging 中间件配置隔离
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
问题:docker-compose.core.yml 把 MySQL/Redis/Nacos/RocketMQ/TDengine 等连接信息
全写死成 prod 值,无论 master→PROD 还是 release/next→STAGING 都用同一份,
staging 容器会直接连 prod 数据库写脏数据。

改动:
- docker-compose.core.yml 全参数化(${VAR:-prod_default}),用 YAML anchor
  抽公共 env,未注入 .env 时行为与历史一致(不破坏 prod 当前部署)
- 新增 env/prod.env.example、env/staging.env.example 模板(占位密码进 git)
  和 env/.gitignore(真实 .env 不进 git,由部署机手工维护)
- Jenkinsfile:所有 docker compose 命令加 --env-file .env,并在 Pre-deploy
  Check 阶段验证部署机 .env 文件存在性,缺失直接 fail(防止连错中间件)
- 5 个核心服务 application.yaml 的 xxl-job appname 加 SUFFIX 变量:
    appname: ${spring.application.name}${XXL_JOB_EXECUTOR_APPNAME_SUFFIX:}
  staging 设为 -staging,prod 留空。否则 staging 与 prod 注册到同一个执行器
  组,admin 调度任务会随机打到任一边
- iot-server application-prod.yaml TDengine database 参数化:
    /aiot_platform → /${TDENGINE_DATABASE:aiot_platform}
  staging 用独立 database aiot_platform_staging,避免共享 prod 时序数据

staging 中间件方案:
- MySQL 同实例(172.17.16.8)独立库 aiot-platform-release
- Nacos 同实例独立 namespace e635b215-913e-4bc8-8867-2fbf7d5134aa
- Redis 同 prod 实例(短期,靠 application 层 key 前缀隔离)
- RocketMQ 改用 staging 服务器本地实例 172.17.16.7:9876(内网)
- TDengine 同 prod 实例独立 database(CTSDB 切换为 follow-up)
- XXL-Job admin 共用,executor IP=.7、appname 加 -staging 后缀
2026-04-28 16:37:27 +08:00
lzh
14c239054f fix(ci): Dockerfile 改用官方 maven 镜像,修复 aliyun 下架 3.9.14 导致 404
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
- 根因:aliyun 镜像站只保留 Maven 最新小版本,3.9.14 被下架
  之前依赖 Docker 层缓存掩盖,最近清理本地镜像后暴露
- 方案:Dockerfile.deps / Dockerfile.template 均切到
  maven:3.9-eclipse-temurin-17 官方镜像,删除自建 wget/tar/ln 逻辑
- 运行阶段仍用 eclipse-temurin:17-jre-alpine,Prod 镜像体积不变

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 13:55:25 +08:00
lzh
8c5c5ef44a chore(ci): 部署加磁盘预检 + 部署后自动清理 Prod 本地镜像与 Registry
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
- 新增 Pre-deploy Check:SSH 到 Prod/Registry 读根分区空闲,<5% 直接 fail(规避磁盘满时 sshd 连带崩溃导致的 scp 失败),5~10% 仅告警
- 新增 Cleanup Old Images stage:部署成功后每服务保留最近 3 个镜像
  * Prod 侧调用 scripts/cleanup.sh
  * Registry 侧调用 scripts/registry-cleanup.py + 触发容器内 garbage-collect
- scripts/cleanup.sh:去掉 volume prune 的交互 read(CI 下会卡住),支持 --keep/--prune-volumes/--registry 参数
- scripts/registry-cleanup.py:按 tag 内数字降序保留最新 N 个;覆盖 Docker v2/OCI 多种 manifest Accept;多 tag 指向同一 digest 去重;失败不影响发布

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 11:20:37 +08:00
lzh
acd7a35e1d fix(iot): 轨迹检测防抖 + eventTime 用 reportTime 避免回放挤压
- 事件 eventTime 透传设备 reportTime,修复 TDengine/消息总线恢复后堆积回放导致 ENTER/LEAVE 全部塞进同一秒的问题
- 区域切换加 5dB 滞回 + 进入后 5s 最小停留,压制 RSSI 抖动造成的瞬态 AREA_SWITCH 与 SIGNAL_LOSS
- 滞回兜底改用窗口内最近一次非 -999 样本,避免当前信标短暂漏扫时滞回被缺失哨兵破坏
- reportTime 为空时记录 warn,便于追踪上游漏传的调用链

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 10:59:27 +08:00
lzh
d6f625151c Merge pull request 'fix(ops): 修复工牌绑定/手动派单/审计日志三处缺陷' (#2) from fix/badge-online-and-manual-dispatch into master
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
Reviewed-on: #2
2026-04-22 18:10:33 +08:00
lzh
3cfd342318 fix(ops): BADGE 绑定/解绑后即时同步工牌缓存
问题:
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>
2026-04-22 18:04:47 +08:00
lzh
6c4153fe23 fix(ops): 手动派单提前写执行人字段,修复按键报"没有工单"
问题:管理员手动派单后,工牌按键查询持续返回"没有工单",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>
2026-04-22 18:04:10 +08:00
lzh
8c664a479d refactor(ops): 状态转换成功不再镜像写 bus_log
问题:每次 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>
2026-04-22 18:03:51 +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
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
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
a9941a29a9 fix(ops): 状态机允许 CONFIRMED → COMPLETED,支持安保确认后直接完单
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
安保工单不需要信标到岗检测(ARRIVED),确认接单后可直接提交处理结果完成。
原规则 CONFIRMED → {ARRIVED, CANCELLED} 缺少 COMPLETED,导致安保人员完单报错:
"非法状态转换: CONFIRMED -> COMPLETED"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 17:11:49 +08:00
lzh
edf0a3e645 fix(clean): 修复 CleanOrderEndToEndTest 编译错误
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
sendPriorityUpgradeNotification 已从 CleanOrderEventListener 移至
CleanOrderNotificationService,测试中 verify 目标未同步更新。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:38:56 +08:00
lzh
55ef659364 feat(ops): 手动派单遵循执行人忙碌状态,忙碌时入队等待
ManualOrderActionFacade.dispatch:
- 新增 strategy.isAssigneeIdle() 判断,空闲→DISPATCHED,忙碌→QUEUED
- 不再无条件直接派发

OrderBusinessStrategy:
- 新增 isAssigneeIdle() 默认方法,默认返回 true

CleanOrderBusinessStrategy:
- isAssigneeIdle 通过 BadgeDeviceStatusService.isBusy() 判断设备忙碌

SecurityOrderBusinessStrategy:
- isAssigneeIdle 通过 UserDispatchStatusService.isIdle() 判断人员忙碌

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:37:30 +08:00
lzh
78396cf35a fix(ops): 调度状态补偿 QUEUED→终态跳过 DISPATCHED 场景
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
UserDispatchStatusEventListener:
- assigneeId 兜底从工单主表获取(forceComplete 等 payload 缺失场景)
- QUEUED→COMPLETED/CANCELLED 补偿 decrementWaitingTaskCount

UserDispatchStatusServiceImpl:
- 新增 LUA_DECREMENT_WAITING 脚本,安全递减 waitingTaskCount(不低于 0)

OpsOrderEventService:新增 8 参数 recordEvent 重载(含 operatorName)
DispatchEngineServiceAdapter:reason 文案统一为"手动派单"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:09:30 +08:00
lzh
06d701ed5e refactor(ops): 通用控制器收口,手动动作移至条线控制器
OpsOrderController:
- 移除 create/assign/complete/cancel 接口,避免绕过条线入口
- 保留查询、元数据维护、设备/人员驱动的状态操作(accept/pause/resume)
- 新增 business-logs 查询接口

CleanWorkOrderController:新增手动创建/派单/取消端点
SecurityOrderController:新增手动派单/取消/升级优先级端点
ErrorCodeConstants:新增安保条线错误码

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:09:07 +08:00
lzh
1d09a50643 feat(security): 安保条线接入 ManualOrderActionFacade
SecurityOrderBusinessStrategy:
- validateDispatch: 去除区域绑定强限制,允许跨区域派单,保留姓名回填
- afterUpgradePriority: 队列分数重算

SecurityOrderServiceImpl:
- manualDispatch: resolveUser 一次查询同时拿姓名和手机号
- manualCancel/upgradePriority: 委托 facade,传入 operatorName
- 手动创建走 OrderAuditService 审计(告警触发仅写业务日志)

SecurityOrderEventListener:
- 3 处 resolveOperatorName() 改为从 event.payload.operatorName 读取
- 移除 AdminUserApi 依赖,消除重复远程调用

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:08:48 +08:00
lzh
5d91097e75 feat(clean): 保洁条线接入 ManualOrderActionFacade
CleanOrderBusinessStrategy:
- validateDispatch: 校验设备在线/可接单/区域匹配
- afterUpgradePriority: adjustPriority + rebuildWaitingTasks + P0 通知

CleanWorkOrderServiceImpl:
- manualCreateOrder: 主表+扩展表+创建事件+审计日志
- manualDispatch/manualCancelOrder/manualCompleteOrder/upgradePriority: 委托 facade
- 所有入口统一 resolveUserName 传入 operatorName

UpgradePriorityReqDTO:新增 newPriority/operatorId 字段

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:08:20 +08:00