Commit Graph

563 Commits

Author SHA1 Message Date
lzh
026a126824 fix(iot): 传递集成事件项目ID 2026-04-29 22:20:35 +08:00
lzh
bc040d70f8 fix(docker): Dockerfile.deps 补齐所有子模块 pom 文件
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 2.0 合入新模块,Dockerfile.deps 缺少:
1. viewsh-framework 17 个子模块 pom
2. viewsh-module-iot-rule 子模块 pom

导致 Maven 构建报错 "Child module does not exist"

修复:添加所有缺失的子模块 pom 文件 COPY 语句

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 11:32:47 +08:00
lzh
0bd2a5f664 fix(docker): Dockerfile.deps 添加 viewsh-module-video pom 文件
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
问题:根 pom.xml 引用 viewsh-module-video 模块,
      但 Dockerfile.deps 构建依赖镜像时未复制 video pom 文件,
      导致 Maven 报错 "Child module does not exist"

修复:添加 video 模块的 pom 文件复制步骤

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 11:29:14 +08:00
lzh
28ff25b6b1 fix(ci): video-server 仅在 release/next 构建,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
问题:CORE_SERVICES 包含 video-server,master 构建时尝试构建
       但代码只在 release/next,会导致 master 构建失败

修复:
- 引入 ACTIVE_SERVICES 根据分支动态设置服务列表
- master: 不含 video-server
- release/next: 含 video-server
- detectServicesToBuild 使用 ACTIVE_SERVICES 代替 CORE_SERVICES

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 11:16:56 +08:00
lzh
44bb89dbca feat(ci): Jenkinsfile 添加 video-server 构建
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
变更:
- CORE_SERVICES 添加 viewsh-module-video-server
- getModulePathForService 添加 video 模块路径
- sortServicesByDependency video 排在 ops 之后
- getContainerNameForService 添加 aiot-video-server 映射
- Release 部署时同步 zlm-config-release.ini 到部署机

性能优化:
- video-server 构建与其他服务并行(Maven 缓存卷)
- 部署时 Docker Compose 自动拉取 ZLM 官方镜像
- ZLM 不在 CORE_SERVICES 中,不触发构建

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 11:13:38 +08:00
lzh
4a8641571f feat(video): Release 环境部署独立 ZLM + video-server
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
- 添加 zlmediakit-release 容器(host 网络)
- 添加 video-server 连接本地 ZLM
- ZLM HTTP 7080 / RTSP 7554 / RTMP 7935 / RTP 30000-30500
- Hook URL 指向 127.0.0.1:48093 (video-server)
- SIP ID 末位 02 与 Prod 区分
- Secret 统一使用 u3UapOhmZa9er7S37Hc2k695NlZOBYdB

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 11:02:23 +08:00
lzh
49366eb871 feat(iot): 合并 IoT 2.0 开发分支到 release/next
合并 feat/iot-2.0 分支代码,保留该分支继续开发。

主要变更:
- Controller 补齐 (B2/B4-6/B10/B11/B12/B13)
- IoT v2.0 菜单迁移脚本(规则链 + 子系统)
- B7 BranchNode 循环依赖修复
- AutoConfiguration.imports 注册

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 09:52:53 +08:00
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
7dc00b542d feat(iot): 一期 Controller 补齐 (B2/B4-6/B10/B11/B12/B13)
对照前端 feat/iot-2.0 已固化 API 契约补齐 5 组缺失端点(发现于一期 19/19
宣称完成后前端联调阶段),归属原任务卡 Controller 层返工,不占用二期
B20+ 编号。

- B2  规则链: 补 PUT /disable /deploy /debug + POST /copy?id= +
              新增 GET /rule-chain/get?id= 返 GraphVO(保留 /get/{id})
              deployRuleChain=enable+主动 Pub/Sub evict(对齐 B8)
- B10 子系统: 新增 GET /device-count 聚合(HGETALL 返空 map 遵循 A6)
              + GET /get?id= query 别名(保留 /get/{id})
- B11 设备:   新增驼峰 PUT /bindSubsystem /batchBindSubsystem
              + 2 ReqVO,保留 kebab 兼容
- B12/B13 告警: 新增 IotAlarmRecordController(整缺)11 端点:
                page/get/ack/unack/clear/archive/batch-{ack,clear,archive}/
                history/remark;Service 补 6 方法(getPage/batchAck/
                batchClear/batchArchive/updateRemark/listHistory)
                + Mapper 2 方法 + 8 VO
- B4/5/6 节点元数据: 新增 GET /iot/rule/provider/metadata 聚合端点;
                    3 SPI 加 default getMetadata(),4 Manager 加
                    listAllMetadata(),13 具体 Provider 覆写(中文 label
                    + mdi: icon),schema MVP 空骨架 {rule:[]}

测试:
- iot-rule   191/191 全绿(+5 B2 补齐 +9 B4/5/6 补齐)
- iot-server 106 active/161 Skipped v1 遗产 全绿
            (+6 B12/B13 补齐 +3 B10 补齐)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 15:47:41 +08:00
lzh
9912b73c56 feat(iot): IoT v2.0 菜单迁移脚本(规则链 + 子系统)
使用子查询按名称取 parent_id,不写死 ID,兼容不同环境自增序列。
新增:
- 规则链管理(父: 规则引擎)+ 5 个权限按钮 + 隐藏编辑器路由
- 子系统管理(父: 设备接入)+ 4 个权限按钮
待补:告警记录 v2(IotAlarmRecordController 补齐后再执行)

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 14:39:26 +08:00
lzh
4f89b49e5a fix(iot): B7 BranchNode @Lazy 注入 NodeProviderRegistry 打破循环依赖
启动失败:Spring 检测到环形依赖
  nodeProviderRegistry → branchNode → nodeProviderRegistry

根因:BranchNode 既是 NodeProvider(被 Registry 收集),又依赖 Registry
dispatch 子节点 — 典型 "collect vs dispatch" 死结。

修复:构造参数加 @Lazy,Spring 注入代理,首次方法调用才解析 Registry,
构造阶段打破环。运行期行为等价。

rule 模块 177/177 测试全绿。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 14:35:30 +08:00
lzh
ce2e6cc32a fix(iot): 清理 iot-rule spring.factories 旧 EnableAutoConfiguration 条目
Boot 3.x 已弃用,实际加载改由 AutoConfiguration.imports 负责,
spring.factories 中的有效条目移除,仅保留注释说明。

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 14:22:47 +08:00
lzh
04eba61067 fix(iot): iot-rule 注册 AutoConfiguration.imports(Spring Boot 3.x)
spring.factories 中的 EnableAutoConfiguration 在 Spring Boot 3.x 已被移除,
须改用 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports。
缺失此文件导致 IotRuleEngineVersionAutoConfiguration 未加载,
RuleEngineVersionProperties bean 找不到,服务启动失败。

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 14:20:25 +08:00
lzh
66e57e4d5e Merge branch 'feat/multi-tenant' into feat/iot-2.0
同步 multi-tenant 分支最新变更:
- feat(system): SSO 回调换 Token + OAuth2 客户端 platform 过滤菜单
- chore(tenant): Tenant RPC Feign 引入 ProjectCommonApi
- feat(web): API 访问日志 exclude-paths 过滤
- fix(iot): 轨迹检测防抖 + eventTime 修正
- perf(system): 项目授权校验优化 + isSuperAdmin 缓存切换
- feat(system): 超管绕过 user_project + 项目成员分页 API
- chore(ci): 部署磁盘预检 + 自动清理镜像
- chore: .gitattributes 统一行尾 LF

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 13:59:50 +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
4386a69a4a feat(system): 新增内部 SSO 回调换 Token 接口
面向业务/物联运维平台之间的互跳场景:已登录一端 → /system/oauth2/authorize 拿 code
→ 浏览器重定向 → 另一端调用本接口用 code 换 access_token。

安全要点:
- body 传参而非 query,code/state 不落 nginx access log 和浏览器历史
- client_secret 不传:secret 仅存 DB,验证安全性来自 OAuth2 code 一次性 +
  redirect_uri 白名单 + state 一致性 + short TTL
- state 入参改为必填(@NotBlank),强制 CSRF 防护
- 日志中 code 截断(前 6 + ***+ 末 2),state 只记录长度不暴露值

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 13:32:40 +08:00
lzh
cbbb048a4d feat(system): 按 OAuth2 客户端 platform 过滤菜单,支持业务/物联双前端
问题:业务平台(biz)和物联运维平台(iot)共用一套用户体系和菜单表,但每个前端
只该看到自己域的菜单。原来没有按客户端过滤的机制。

方案:在 OAuth2 客户端维度打 platform 标签(biz/iot/NULL),菜单也打同样标签,
登录时下发菜单按二者匹配过滤。

链路:
- OAuth2AccessTokenCheckRespDTO / LoginUser(framework + gateway)新增 clientId 字段
- TokenAuthenticationFilter(framework + gateway)把 accessToken.clientId 带进 LoginUser
- WebFrameworkUtils.HEADER_CLIENT_ID="X-Client-Id":登录/refresh 等"无 token 入口"
  允许前端声明 client,避免硬编码 default
- AdminAuthServiceImpl.resolveClientId:未传 Header 时回退 OAuth2ClientConstants.CLIENT_ID_DEFAULT
- MenuDO / OAuth2ClientDO 各加 platform 列
- MenuService.filterMenusByPlatform:platform 为空(全平台共用)或匹配即保留

SQL 迁移按字母序编号:
- _01_oauth2_client_platform.sql:加列 + 给 default/iot-client 客户端打标 + 递归标
  IoT 菜单子树(root id=4000)为 iot
- _02_bulk_mark_biz_menus.sql:其余 platform=NULL 的菜单兜底标 biz
- 顺序依赖:_01 标完 iot 后 _02 才动剩余 NULL,避免 _02 把 IoT 菜单错标 biz

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 13:32:26 +08:00
lzh
4564eec893 chore(tenant): Tenant RPC Feign 客户端列表引入 ProjectCommonApi
框架层 starter-biz-tenant 的 @EnableFeignClients 补齐 ProjectCommonApi,
供租户隔离检查时跨服务拉取项目元数据使用。后续如体量增长可考虑拆独立
ViewshProjectRpcAutoConfiguration。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 13:31:55 +08:00
lzh
04961ee614 feat(web): API 访问日志支持 exclude-paths 过滤高频心跳刷屏
- 新增 viewsh.access-log.exclude-paths 配置(Ant Pattern),命中路径默认不打 INFO
- 异常或 HTTP 4xx/5xx 时仍打 WARN,保证"默认安静、出错必响"
- ApiAccessLogInterceptor 保留无参构造,兼容老用法
- gateway application.yaml 补注释:ZLM Hook 走 video-server 直连而非网关,理由说明

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 13:31:44 +08:00
lzh
ca575d6297 chore: 加 .gitattributes 统一 Java/SQL/shell 等文本文件行尾为 LF
仓库历史存在零星 CRLF 文件(Windows 编辑器保存导致),每次他人修改都会出现
整文件 diff 污染 git blame。显式规则让 git add 自动规范化,避免复现。

*.bat/*.cmd/*.ps1 保留 CRLF(Windows 原生脚本语义要求),二进制类型显式 binary
阻止任何转换。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 13:31:32 +08:00
lzh
5756f23ed7 Merge branch 'master' into feat/multi-tenant 2026-04-24 11:45:57 +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
bac4f216fc feat(iot): B15 告警传播(iot_alarm_propagation 关联表)
沿资产层级(SUBSYSTEM→PROJECT→TENANT)向上传播,替代原 JSON_CONTAINS 全表扫描。

关键实现:
- IotAlarmPropagationMapper:INSERT IGNORE 幂等批量插入;selectAlarmIdsByAsset
  命中 idx_asset(asset_type, asset_id, tenant_id),毫秒级响应
- IotAlarmPropagationServiceImpl:三层传播逻辑(无 project_id 时仅 2 层)
- IotAlarmRecordServiceImpl:首次触发(existing==null)时调用传播,
  @Lazy 注入避免循环依赖
- IotProjectMapper:最小化 Mapper(B10 有 DO 无 Mapper)
- 7 个单元测试(3层/2层/无subsystem/重复幂等/分页查询)

测试:iot-server 258/258 全绿(含 B14/B15 新增 23 用例)

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 11:08:36 +08:00
lzh
171f201384 feat(iot): Wave 5 Round 2 — B9/B14/B16 统一消费入口 + 告警分布式锁 + 通知集成
B9 IotRuleEngineMessageHandler(统一消费入口)
- 新消费者 v2 统一入口,@PostConstruct 注册到 IotMessageBus
- versionResolver.shouldUseV2 三态路由(V1/V2/HYBRID),绝不双跑
- device null 时 WARN + skip;RuleEngine 异常 try-catch 吞掉防重试风暴
- v1 三消费者(DataRule/SceneRule/CleanRule)增加前置 v2 bypass 判断
- 6 个单元测试(global-v1/v2/hybrid 白名单命中/未命中/device-null/引擎异常)

B14 告警缓存 + SET NX PX 分布式锁 + 有效性判断
- IotAlarmLockService:SET NX PX + Lua 原子解锁,锁冲突抛 ALARM_LOCK_CONFLICT
- IotAlarmCacheService:Redis Hash iot:alarm:state:{id},TTL 7天,cache miss 从DB重建
- AlarmStateValidator:isEffectiveTrigger/isEffectiveClear 时序校验,防旧消息重置已清除告警
- IotAlarmRecordServiceImpl:trigger/clear/ack/archive 全部在锁内,DB写后立即同步缓存
- 新增 ALARM_LOCK_CONFLICT 错误码;AlarmTriggerRequest 增加 timestamp 字段
- 17 个单元测试(锁 4 + 缓存 5 + 校验 9 + 集成 3)

B16 NotifyAction 4 通道集成 + 模板解析
- NotifyChannel SPI 接口 + Sms/Email/InApp/Webhook 四实现(@Component 注册)
- WebhookNotifyChannel:JDK 17 HttpClient 10s 超时 + SSRF 强制 HTTPS 校验
- NotifyDispatcher:独立 ForkJoinPool(8) 并行分发,30s 整体超时,部分失败不阻塞
- 模板变量统一走 TemplateResolver(评审 C5),缺失变量降级为空串
- NotifyAction 移除 stub,委托 NotifyDispatcher
- viewsh-module-system-api 依赖引入;13 个测试(Dispatcher 7 + Webhook SSRF 6)

测试:iot-rule 177/177 全绿;iot-server 251/251 全绿(含 Skipped 161 旧 v1 测试)

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 11:00:38 +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
8e7631987f feat(iot): Wave 5 Round 1 — B8/B13 规则链缓存 + AlarmHistory 时序 DAO
B8 规则链全量缓存 + Redis Pub/Sub + 版本拉模式兜底:
- CompiledRuleChainFactory:IotRuleChainGraphVO→CompiledRuleChain
- RuleChainCache(@PostConstruct loadAll + evict + reload + B48 钩子)
  · TenantUtils.executeIgnore 跨租户全量加载;TenantUtils.execute 逐租户切换
  · ConcurrentHashMap.compute 保证 reload 串行(避免并发 DB 查询)
  · 超 500 条规则链打 WARN 日志
- RuleChainCacheListener:Redis Pub/Sub 订阅 iot:rule:cache:evict,收到后 evict+reload
- RuleChainVersionChecker:5 分钟拉模式兜底,version drift 时 reload + metric
- RuleChainCacheConfiguration:@EnableScheduling + RedisMessageListenerContainer
- IotRuleChainMapper 新增 selectAllEnabledTenantIds()(跨租户查询)
- IotRuleChainServiceImpl.updateRuleChain 末尾发布 Pub/Sub 驱逐事件
- 5 单元测试全绿(含 version drift 检测 + 容量告警)

B13 AlarmHistory 时序表 DAO 双实现:
- AlarmHistoryDO(时序对象:ts/device/severity/ack/clear/archived/eventType 等)
- IotTsDbAlarmHistoryDao 接口(insert/queryByAlarmRecord/queryLatestByDevice)
- CtsdbAlarmHistoryDaoImpl(CTSDB/InfluxDB 协议,@ConditionalOnProperty)
- TdengineAlarmHistoryDaoImpl(TDengine JDBC,@ConditionalOnProperty)
- IotAlarmHistoryService(协调 TSDB 写;异步 @Async;写失败不影响主流程)
- TsDbAutoConfiguration 注册 IotAlarmHistoryService
- 5 单元测试全绿(含 TSDB 失败降级 + 异步写验证)

测试总计:rule 模块 164/164 ✓,server 模块 B13 5/5 ✓

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 10:37:07 +08:00
lzh
ec3981195d feat(iot): B17 SceneRule → DAG 自动转换工具 + dry-run/execute
- SceneRuleToChainMapper:v1→v2 纯转换逻辑
  · trigger type 映射(1→device_state 等 4 种 + timer)
  · action type 映射(1→device_property_set / 2→device_service_invoke / 100→alarm_trigger / 101→alarm_clear)
  · SpEL→Aviator:#root.x → ${data.x};含 T(/instanceof/new 标记 WARNING 不中断
  · 线性 DAG:Trigger → [Condition] → Action×N,临时 key -1/-2/-3...
- SceneRuleMigrator:干运行 + 分批执行(50条/批)+ 幂等(force 覆盖重迁)
- SceneRuleMigrationController:3 端点 dry-run/execute/mapping
- MigrationDryRunResultVO / MigrationExecuteReqVO
- 8 单元测试全绿(含 spel→aviator / unsupported_spel / idempotent / force)

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 10:21:52 +08:00
lzh
24c486900a feat(iot): Wave 4 Round 2 — B6/B7/B18 ActionProvider + 分支执行 + DataRule迁移
B6 ActionProvider SPI + 5 核心动作(alarm/notify/device-ctrl):
- ActionProvider 接口(extends NodeProvider,默认 bridge execute)
- ActionResult record(SUCCESS/FAILURE/SKIP + output + message)
- ActionProviderManager(Spring 自动收集 + fail-fast 重复 type)
- AlarmTriggerAction(调用 IotAlarmRecordApi.triggerAlarm,模板变量解析)
- AlarmClearAction(alarmId 从 config 或 ctx.metadata 解析,幂等)
- NotifyAction(4 通道并发 + 部分失败不阻塞,第一期 stub)
- DeviceServiceInvokeAction(调用 IotDeviceControlApi.invokeService)
- DevicePropertySetAction(第一期 stub,B27 补全 Redis/MySQL)
- IotAlarmRecordApi + DTO(rule 模块→server 跨模块接口)
- IotAlarmRecordApiImpl(server 端 FeignClient 实现,委托 Service)
- 14 单元测试全绿

B7 分支执行逻辑(executeAnyway if/else-if/else):
- BranchConfiguration POJO(branches[] + executeAnyway + BranchCondition)
- BranchExecutor(核心语义:else/executeAnyway/条件异常短路/action 异常隔离)
- BranchNode NodeProvider(ACTION/"branch",内联执行命中 branch actions)
- DagExecutor 最小扩展(ctx.metadata 传递 CompiledRuleChain 供 BranchNode 使用)
- 9 单元测试全绿(含 validate else 位置校验)

B18 DataRule → DAG 自动转换工具:
- DataRuleToChainMapper(v1→v2 映射,6 种 Sink,合并/拆分多 source)
- DataRuleMigrator(dry-run + execute + 幂等映射表)
- DataRuleMigrationController(3 端点:dry-run/execute/mapping)
- 8 单元测试全绿

测试总计:rule 模块 159/159 ✓,server 模块 8/8(B18)✓

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 10:10:04 +08:00
lzh
42466363c7 feat(iot): Wave 4 Round 1 — B12/B4/B5 告警状态机 + 规则引擎 SPI
主会话 Opus:
- B12 iot_alarm_record 正交状态机(ack_state + clear_state + archived)
  * V2.0.4__iot_alarm_record.sql:主表 + iot_alarm_propagation 关联表
  * 评审 C1 正交三字段(替代线性 4 枚举,表达"已清除未确认")
  * 评审 C2 联合 UK (device_id, alarm_config_id, tenant_id, deleted)
  * 评审 C3 传播关联表(替代 propagated_to JSON 查询)
  * Service 5 方法:triggerAlarm / ackAlarm / unackAlarm / clearAlarm / archiveAlarm
  * 幂等 upsert(trigger_count++)+ 归档后禁止修改
  * 13 单元测试全绿
  * TODO B14 分布式锁 / B15 传播 / B16 通知

Sonnet subagent B4:TriggerProvider SPI + 5 内置触发器
  * spi/TriggerProvider + TriggerProviderManager(@Component + getType 索引,fail-fast 重复 type)
  * trigger/DeviceState / DeviceProperty / DeviceEvent / DeviceService / Timer(Spring TaskScheduler)
  * 评审 A3 落地:禁 ServiceLoader / @SPI
  * 44 单元测试全绿

Sonnet subagent B5:ConditionEvaluator SPI + 3 条件 + 统一模板变量
  * spi/ConditionEvaluator + condition/Manager
  * condition/Expression(Aviator + LRU(256) 编译缓存)
  * condition/TimeRange(跨午夜支持)
  * condition/DeviceState(Redis 查询,空值按 offline)
  * template/TemplateResolver:\${namespace.key},拒绝 \$[...] 旧语法(评审 B5)
  * TODO B44 完整 8 层 Aviator 沙箱
  * 50 单元测试全绿(TemplateResolver 16 + 条件 3x ≈ 34)

测试汇总:rule 136 全绿 / server 13 新增全绿

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet (subagent) <noreply@anthropic.com>
2026-04-24 00:35:14 +08:00
lzh
1f87d599c0 feat(iot): B11 iot_device.subsystem_id + 设备归属绑定 API(一期允许 NULL)
- 新增 sql/iot/V2.0.3__iot_device_add_subsystem.sql(ALTER + idx_subsystem)
- 新增 sql/iot/V2.1.0__iot_device_subsystem_not_null.sql(二期预留,带 "勿执行" 注释)
- IotDeviceDO 加 subsystemId(一期可 NULL,二期改 NOT NULL)
- IotDeviceService 加 bindDeviceToSubsystem / batchBind / unbind / selectCountBySubsystemId
- IotDeviceServiceImpl.createDevice 强校验 subsystemId + 同租户 + Redis HINCRBY +1
- 绑定变更按 TransactionSynchronizationManager afterCommit 同步 Redis(-1 / +1,避免脏状态)
- IotDeviceMapper 加 selectCountBySubsystemId / updateSubsystemId 等
- IotSubsystemServiceImpl 加 incrementDeviceCount/decrementDeviceCount;deleteSubsystem 改用 DB 计数兜底(更可靠)
- IotDeviceController 加 PUT /bindSubsystem + /batchBindSubsystem(@PreAuthorize iot:device:update)
- IotDevicePageReqVO 加 subsystemId 过滤参数(null 可走 IS NULL 查未归属)
- api ErrorCodeConstants 加 DEVICE_SUBSYSTEM_REQUIRED / DEVICE_SUBSYSTEM_CROSS_TENANT(1_050_003_009/010)
- 测试:IotDeviceServiceImplTest 8/8 + B10 IotSubsystemServiceImplTest 补 mock deviceMapper 后 8/8 全绿
- Known Pitfalls 落地:
  ⚠️ 评审 A2:一期允许 NULL,V2.1.0 预留二期 NOT NULL
  ⚠️ Redis 计数:事务提交后同步(TransactionSynchronizationManager.afterCommit)
  ⚠️ 跨租户:校验 subsystem 属于当前租户,不然抛 DEVICE_SUBSYSTEM_CROSS_TENANT
  ⚠️ 索引 idx_subsystem (tenant_id, subsystem_id, deleted) 最左匹配;IS NULL 查询走全表扫,文档已提示

Co-Authored-By: Claude Sonnet (B11 subagent) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context, orchestrator) <noreply@anthropic.com>
2026-04-24 00:03:57 +08:00
lzh
ae74b4752a feat(iot): B3 RuleEngine 执行器(DAG + 链级隔离 + 三层匹配去重)
rule 模块 engine/ 新增 14 个核心类 + 4 个测试:

- RuleEngine / DefaultRuleEngine:对外入口,链级 try-catch 隔离(决议 #3)
- DagExecutor:BFS 遍历,按 relation_type 选 outgoing links,RuntimeException 转 FAILURE
- ChainIndex:三层绑定(subsystem/product/device)4 种 key 匹配 + LinkedHashMap 去重 + priority ASC 排序
- RuleChainCompiler:DO → CompiledRuleChain,含单 Trigger + DAG 无环 + 非法枚举兜底
- NodeProvider / NodeProviderRegistry:SPI + Spring @Component 路由(禁用 ServiceLoader/@SPI)
- RuleContext / NodeResult / CompiledRuleChain/Node/Link / RuleEngineResult / RuleChainException

测试覆盖(42/42 全绿):
- DagExecutorTest: 线性链 / 分支(TRUE/FALSE)/ 并行动作 / 异常转 FAILURE / metadata 传递 / SKIP 截断 / 缺 Trigger
- ChainIndexTest: 4 种 wildcard 组合 / 去重 / priority 排序 / 租户隔离 / evict
- RuleChainCompilerTest: 正常编译 / 单 Trigger 兜底 / DAG 无环 / 非法 category & relation_type / 连线 sortOrder
- DefaultRuleEngineTest: 链级异常隔离(chain1+chain3 成功,chain2 失败,counters 各 1 次)

补齐依赖:
- rule/pom.xml 加 io.micrometer:micrometer-core(节点执行 Timer + 失败 Counter)
- RuleNodeCategory 加 of(String) 静态查找方法(配合 RuleLinkRelationType.of 一致风格)

Known Pitfalls 落地:
  ⚠️ 评审 B1:ShakeLimit 节点 hook 留在 DagExecutor(B48 补)
  ⚠️ 评审 B3:LinkedHashMap 去重保留顺序(ChainIndex.match)
  ⚠️ 评审 B4:relation_type 严格封闭 6 值(RuleLinkRelationType + isValid)
  ⚠️ 评审 A5:chains 顺序 for 循环 + try-catch;不使用 Reactor flatMap 并发
  ⚠️ 评审 B10:单 Trigger 兜底在 Compiler 层(Service 层 + Compiler 双重保障)
  ⚠️ Metrics 基数:tag 含 chainId + nodeType + outcome;规则链 ≤ 500 控制
  ⚠️ DAG 兜底 MAX_NODES_PER_EXECUTION=1000 防脏数据绕过无环校验

未实现(留给后续任务):
- 具体 Provider(B4/B5/B6 Trigger/Condition/Action 实现)
- 全量缓存加载 + Pub/Sub 驱逐(B8)
- JMH Benchmark(任务卡 §6.4 + AC9 p99 < 50ms,第二期补)
- @SpringBootTest 集成测试(B9 Handler 就位后补)

Co-Authored-By: Claude Opus 4.7 (1M context, main session) <noreply@anthropic.com>
2026-04-23 23:58:48 +08:00
lzh
66647e19dd feat(iot): B19 规则引擎 v1/v2/hybrid 版本解析器 + 管理 API
- config/RuleEngineVersion.java(三态枚举 V1/V2/HYBRID)
- config/RuleEngineVersionProperties.java(@ConfigurationProperties, prefix=viewsh.iot.rule.engine)
- config/IotRuleEngineVersionResolver.java(volatile Set 本地缓存 < 1μs + Redis 动态白名单 + 降级)
- config/RuleEngineVersionAdminController.java(5 端点:add/remove/list/version/refresh,@PreAuthorize)
- config/IotRuleEngineVersionAutoConfiguration.java(@AutoConfiguration + @EnableConfigurationProperties)
- spring.factories 注册 AutoConfiguration
- 测试:IotRuleEngineVersionResolverTest 8 用例全绿(含 Redis 降级 + 动态刷新)
- Known Pitfalls 落地:
  ⚠️ 评审 B11:三态枚举 + switch 全覆盖
  ⚠️ 性能:volatile + Set.contains,无 I/O
  ⚠️ Redis 降级:try-catch + log.warn,不抛出
  ⚠️ subsystemId=null 走 v1(存量未归属设备安全默认)

Co-Authored-By: Claude Sonnet (B19 subagent) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context, orchestrator) <noreply@anthropic.com>
2026-04-23 23:40:25 +08:00
lzh
48e605b3c9 fix(iot): 恢复 B2 Controller/VO 的 @PreAuthorize/@Schema/@Valid 注解
Wave 2 三个 subagent 并行时,B19 subagent 看不到主会话后补的
rule/pom.xml web/security 依赖,把 B2 已写的注解全注释掉作为编译兜底。
rule/pom.xml 现已含 web/security/biz-tenant starter,恢复注解。

- IotRuleChainController: 7 @PreAuthorize + @Tag + 7 @Operation + @Parameter + @Valid + @Validated
- IotRuleChainSaveReqVO: @Schema + @NotEmpty/@NotNull/@Valid(含 NodeVO/LinkVO 子 VO)
- IotRuleChainRespVO: @Schema
- IotRuleChainGraphVO: @Schema(含子类)
- IotRuleChainPageReqVO: @Schema

落地 B2 AC9(REST API 所有端点带权限校验)。
测试:mvn test -pl viewsh-module-iot/viewsh-module-iot-rule → 17/17 全绿

Co-Authored-By: Claude Sonnet (annotation-restore subagent) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context, orchestrator) <noreply@anthropic.com>
2026-04-23 23:39:26 +08:00
lzh
962e69290b feat(iot): B2 RuleChain/Node/Link 数据模型 + CRUD(单 Trigger/DAG 无环/乐观锁)
- 新增 sql/iot/V2.0.1__iot_rule_chain.sql(iot_rule_chain/node/link 三表 + idx_binding 索引)
- 新增 rule 模块 dal/(3 个 DO + 4 个封闭枚举 + 3 个 Mapper)
- 新增 rule 模块 service/(CRUD + 单 Trigger 校验 + DAG DFS 无环 + 乐观锁 + 级联软删)
- 新增 rule 模块 controller/admin/(7 REST 端点 + @PreAuthorize + VO)
- 新增 resources/mapper/rule/(3 个 MyBatis XML)
- api 模块 ErrorCodeConstants 新增规则链段(1-050-030-xxx)
- **补 B1 遗漏依赖**:rule/pom.xml 追加 viewsh-spring-boot-starter-{web,security,biz-tenant}
- 测试:8 个单元用例全绿(BaseMockitoUnitTest)
- Known Pitfalls 落地:
  ⚠️ 评审 B4:relation_type VARCHAR + 应用层 RuleLinkRelationType.isValid 校验
  ⚠️ 评审 B9:updateWithVersion 乐观锁原子 SQL + idx_update_time 索引支撑 B9 拉模式兜底扫描
  ⚠️ 评审 B10:单 Trigger 校验在 Service 层(validateSingleTrigger)
  ⚠️ 评审 A4:name UK(name, tenant_id, deleted)
  ⚠️ 评审 §十一-B:idx_binding (tenant_id, subsystem_id, product_id, device_id) 最左匹配

Co-Authored-By: Claude Sonnet (B2 subagent) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context, orchestrator) <noreply@anthropic.com>
2026-04-23 21:09:54 +08:00
lzh
6649e1abb6 feat(iot): B10 iot_subsystem 表 + CRUD + Redis 设备计数聚合
- 新增 sql/iot/V2.0.2__iot_subsystem.sql(iot_project + iot_subsystem)
- 新增 server 模块 subsystem/ 下 DO + Mapper + Service + Controller + VO(7 端点)
- 新增 IotSubsystemDeviceCountRedisDAO(HINCRBY + rebuild + ApplicationReadyEvent 触发)
- api 模块 ErrorCodeConstants 新增子系统段(1-050-020-xxx)
- server 模块 RedisKeyConstants 新增 SUBSYSTEM_DEVCOUNT
- 测试:8 个单元用例全绿(mvn test IotSubsystemServiceImplTest)
- Known Pitfalls 落地:
  ⚠️ 评审 A4:UK(name, tenant_id, project_id, deleted) + 应用层 existsByNameAndProject 兜底 NULL
  ⚠️ 评审 A6:device-stats 走 Redis Hash,避免 GROUP BY
  ⚠️ 评审 A7:simple-list 权限码 iot:device:query,返回字段仅 id/name/code
  ⚠️ 删除校验:Redis 计数 > 0 抛 SUBSYSTEM_HAS_DEVICES
  ⚠️ Redis 重建:ApplicationReadyEvent + try/catch + log.warn 不阻塞启动

说明:iot_device 当前无 subsystem_id 列(rebuild 逻辑标 TODO B11),
待 B11 加列后启用 DB 重建查询。

Co-Authored-By: Claude Sonnet (B10 subagent) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context, orchestrator) <noreply@anthropic.com>
2026-04-23 21:08:00 +08:00
lzh
4614737d51 feat(iot): B1 新建 viewsh-module-iot-rule Maven 模块骨架
- 新增 viewsh-module-iot-rule/pom.xml(仅依赖 iot-api + iot-core + Aviator 5.3.3 + mybatis/redis starter)
- 新增 package-info.java / ModuleSmokeTest.java / spring.factories 占位
- viewsh-module-iot/pom.xml <modules> 新增 rule(core 之后、server 之前)
- viewsh-module-iot-server/pom.xml 新增 rule 依赖(core 之后、业务组件之前)
- Known Pitfalls 落地:
  ⚠️ 评审 A3:依赖方向 rule → core → api,严禁反向(dependency:tree 验证)
  ⚠️ 评审 R6:server 对 rule 的依赖按项目一贯顺序插入
  ⚠️ Aviator 5.3.3 首次引入,rule 模块 pom 内显式声明 version

Acceptance Criteria 全 8 项通过(见任务卡 Notes §8.2026-04-23 执行记录)

Co-Authored-By: Claude Sonnet (B1 subagent) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context, orchestrator) <noreply@anthropic.com>
2026-04-23 20:50:31 +08:00
lzh
a72c96692e feat(iot-v2): 拆分 AGENTS.md / CLAUDE.md(方案 C)
- AGENTS.md 作为 agents.md 标准通用规范(所有 AI agent 可读)
- .gitignore 放开 AGENTS.md(删除 AGENTS.md 忽略规则)
- CLAUDE.md 保持 gitignored(Claude Code 本地配置)

用于 IoT v2.0 第一期任务的跨 agent 规范:
- 项目背景 / 任务卡系统 / 硬约束 / 技术约定 / 测试命令 → AGENTS.md
- /loop / subagent 并行 / checkpoint / PushNotification → CLAUDE.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 17:48:40 +08:00
lzh
9784d7dd8e perf(system): 项目授权校验改单行查询 + AuthController 切 FromCache
背景:feat/multi-tenant-project 联调发现 /admin-api/** 全线变慢,
尤其 get-permission-info 经常超时。根因是 ProjectSecurityWebFilter
每请求需校验"用户对项目是否授权",原实现走"拿全量 authorizedIds 再
contains"路径,超管分支还得 selectList 全表项目。Framework 层虽有
60s 本地缓存,cache miss 时仍要走 Feign HTTP 自调用 + 两次 DB。

优化:
1. 新增 ProjectService.isProjectAuthorized(userId, projectId) 单项校验:
   - 超管直通返回 true(不查任何表)
   - 普通用户走 (user_id, project_id) 唯一索引的 selectCount 单行查询
2. ProjectCommonApi / ProjectApiImpl / ProjectFrameworkService(Impl)
   全链路新增 isProjectAuthorized Feign 接口
3. ProjectFrameworkServiceImpl 为 isProjectAuthorized 加 60s 本地
   Guava 缓存(key=(userId,projectId));invalidateAuthorizedProjectCache
   同步清理本用户所有条目
4. ProjectSecurityWebFilter 改调 isProjectAuthorized,消除每请求
   拉全量列表的开销
5. ProjectServiceImpl.getDefaultProjectId 的 N 次 selectById
   改成一次 selectByIds 批量
6. AuthController.getPermissionInfo 第 107 行
   getUserRoleIdListByUserId → FromCache(yudao 原生小瑕疵顺手修)

预期收益:
- Filter 热路径在 cache 命中时 0 次 DB,cache miss 时 1 次单行查询
- get-permission-info 消除一次无缓存 user_role DB 查询

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 16:20:29 +08:00
lzh
317f1cd02f perf(system): isSuperAdmin 切到 getUserRoleIdListByUserIdFromCache
修复 /admin-api/system/auth/get-permission-info 等接口大面积超时:

原因:ProjectSecurityWebFilter 每次 admin-api 请求都调一次
ProjectService.getAuthorizedProjectIds(userId),我之前在里面塞的
isSuperAdmin 用了无缓存的 getUserRoleIdListByUserId,
每请求一次 SELECT system_user_role,并发下直接打爆 DB。

切到 getUserRoleIdListByUserIdFromCache(@Cacheable 走 Redis
USER_ROLE_ID_LIST),首次查 DB、后续命中缓存,该缓存在
assignUserRole / processUserDeleted / updateUserRole 等写入点
都已正确 CacheEvict。

同时修正 UserProjectServiceImpl.isSuperAdmin 同样问题。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 16:10:41 +08:00
lzh
fa8e814af4 feat(iot): 抽象时序数据库访问层,支持 TDengine + CTSDB(InfluxDB) 双引擎
将原有 TDengine 强耦合的 Mapper 层重构为统一的 TsDb 抽象接口:
- 新增 IotTsDbDeviceMessageDao / IotTsDbDevicePropertyDao 接口
- 实现 TDengine 和 CTSDB(InfluxDB) 两套适配器
- 通过 viewsh.iot.tsdb.type 配置项切换时序数据库引擎
- Service 层从直接依赖 TDengine Mapper 改为依赖抽象 Dao 接口
- 新增 influxdb-client-java 7.2.0 依赖
- 删除旧的 TDengineTableInitRunner,统一由 TsDbTableInitRunner 管理

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 15:57:04 +08:00
lzh
5dbc6c5e79 feat(system): 超管绕过 user_project + 项目成员分页/增量 API
后端为配合前端"项目管理成员"从 Modal 改 Drawer 重构接口:
- ProjectServiceImpl.getAuthorizedProjectIds 新增超管分支:
  若 hasAnySuperAdmin(userRoleIds) 成立,直接返回本租户全部项目 ID
  连带影响 getAuthorizedEnabledProjects / getDefaultProjectId /
  ProjectSecurityWebFilter.authorizedProjectIds.contains 全部自动生效
- 新增 UserProjectService 三个方法:
  * getProjectUserPage(reqVO) 分页返回成员 AdminUserDO,过滤超管
  * addProjectUsers(projectId, userIds) 增量添加,已在的用户跳过
  * removeProjectUser(projectId, userId) 单删,带超管/自踢守卫
- 新增 Controller 三个端点:
  * GET  /system/user-project/project-user-page
  * POST /system/user-project/add-project-users
  * DELETE /system/user-project/remove-project-user
- 新增 VO:UserProjectPageReqVO / UserProjectAddProjectUsersReqVO
- 权限点沿用 system:project:assign-user

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 15:48:18 +08:00
lzh
db31462774 refactor(video): JT1078 模块未迁移时的软依赖降级收敛
- JT1078 相关两个 stub 接口 Ijt1078Service / Ijt1078PlayService
  目前没有 @Service 实现(等待从 WVP 后续迁移),直接 @Autowired
  会导致 video server 启动失败。
- GbChannelPlayServiceImpl:原本只声明了 jt1078PlayService 字段,
  从未在任何方法体里引用,属于死代码,直接删掉 import + field。
- MediaServiceImpl:保留引用(checkStreamFromJt 有真实调用),
  改成 @Autowired(required=false) 做软依赖;在 "1078" app 分支
  入口加 null guard,接口缺失时直接返回 false 拒绝请求;
  补注释说明两处处理不对称的原因,并留 TODO 指向 JT1078
  迁移完成后的收尾动作(去掉 required=false / 引入
  JtModuleProperties.enabled 开关统一管理)。
2026-04-23 15:16:50 +08:00
lzh
10ea5e5eee chore(video): ZLM 凭证挪出默认配置 + 连接池 keep-alive + 日志/队列开关
- application.yaml 里的 media.id/ip/http-port/secret/auto-config 改用
  ${ZLM_*:占位} 形式,secret 默认值 please-override-in-env,防止
  真实凭证写进默认 profile 被全环境继承;开发机实际值搬到
  application-local.yaml,继续支持 env var 覆盖。已泄露到 git 历史
  的 secret 需运维侧单独旋转。
- application-local.yaml druid 池:min-evictable-idle 从 10min 降到
  5min + 打开 keep-alive,配合 eviction 扫描主动 ping,解决远程
  MySQL 经 NAT/云防火墙 60~80s 静默断开导致
  Communications link failure / last packet received X ms ago。
- application.yaml 补两个 video 模块运维开关:
    · viewsh.access-log.exclude-paths 屏蔽 /index/hook/on_server_keepalive
      心跳刷屏,出错仍会 WARN;
    · video.sip-queue.enabled 管控上一笔 commit 引入的三个
      SIP 消息 QueueScheduler,默认开启,关闭后队列会在内存堆积
      需要运维兜底(注释已提醒慎用)。
2026-04-23 15:14:00 +08:00
lzh
42d53bb02d fix(video): 收紧 Redis Jackson default typing 白名单,修补反序列化攻击面
- 本模块通过 ZLM hook / GB28181 信令把大量外部输入写入 Redis,
  必须保证 default typing 的白名单足够紧。
- 新增 buildRedisObjectMapper:
    · 注册 JavaTimeModule 支持 LocalDateTime / LocalDate 等 Java 8
      时间类型,避免含 createTime 的对象序列化报错;
    · PropertyAccessor 从 ALL/ANY 收窄到 FIELD/ANY,配合 Lombok getter
      够用,同时降低经 setter 触发 gadget chain 的面;
    · BasicPolymorphicTypeValidator 的 allowIfSubType 原先是 Object.class
      (等价 LaissezFaireSubTypeValidator,Jackson 官方警告有 CVE 级
      反序列化风险,如 jackson-databind #2367 / #2996),改成按包名
      白名单:com.viewsh.* / com.alibaba.fastjson2.JSONObject|JSONArray
      / java.util|time|lang|math / 数组,严格收敛到业务真实存取范围。
- 两个 RedisTemplate Bean 都改用自构建的 ObjectMapper。
- 后续若遇到"新 DTO 反序列化不了",优先放进 com.viewsh 包下,
  不要回退到 allowIfSubType(Object.class)。
2026-04-23 15:09:04 +08:00