Commit Graph

135 Commits

Author SHA1 Message Date
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
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
5756f23ed7 Merge branch 'master' into feat/multi-tenant 2026-04-24 11:45:57 +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
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
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
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
11dcb57ff3 feat(trajectory): 新增轨迹检测与 Beacon 注册表 2026-03-31 22:53:06 +08:00
lzh
90fa54dc11 build(ci): add stage deployment pipeline and configs 2026-03-30 22:53:46 +08:00
lzh
fef3e13ff4 fix(iot): IoT 事件发布补充 tenantId 并修复租户上下文缺陷
7 个事件构建点补充 .tenantId(TenantContextHolder.getTenantId()):
- TrafficThresholdRuleProcessor: CleanOrderCreateEvent
- BeaconDetectionRuleProcessor: CleanOrderArriveEvent, CleanOrderAuditEvent
- SignalLossRuleProcessor: CleanOrderCompleteEvent, CleanOrderAuditEvent
- ButtonEventRuleProcessor: confirm/query 事件 Map

其他修复:
- IotSceneRuleMessageHandler: 添加 TenantUtils.execute() 包裹
- SignalLossRuleProcessor: 硬编码 execute(1L) 改为从设备动态获取
- 更新 SignalLossRuleProcessorTest 和 RssiSlidingWindowDetectorTest

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 11:41:41 +08:00
lzh
df2d14ce26 feat(ops): 新增 OpsRedisKeyBuilder 统一管理 Redis Key 租户隔离
新建 OpsRedisKeyBuilder 集中式工具类,所有 Ops 模块 Redis Key 统一使用
:t{tenantId} 格式实现多租户隔离。迁移以下服务的 Key 构建:

- RedisOrderQueueServiceImpl(派单队列/信息/锁)
- UserDispatchStatusServiceImpl(调度状态)
- BadgeDeviceStatusServiceImpl(工牌状态)
- TrafficActiveOrderRedisDAO(客流活跃工单)
- TtsQueueConsumer(TTS 队列/锁/循环)
- OrderCodeGenerator(工单编码序号)
- AreaDeviceServiceImpl(区域设备配置缓存)
- TrafficStatisticsPersistJob(持久化锁)
- BadgeDeviceStatusRedisDAO(IoT 侧工牌状态)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 11:36:18 +08:00
lzh
b02059a08a feat(iot): 信标检测支持 iBeacon 三元组匹配(uuid+major+minor)
BeaconPresenceConfig 新增 beaconUuid/major/minor 字段;
RssiSlidingWindowDetector.extractTargetRssi 优先 MAC 匹配,
降级为 iBeacon 三元组匹配,兼容两种设备上报格式。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 14:59:15 +08:00
lzh
af1e0c0989 fix(iot): 暂时取消作业时长不足抑制自动完成逻辑
信号丢失超时后不再校验最小有效作业时长,所有情况均直接触发自动完成。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 22:32:16 +08:00
lzh
26c4ce07eb feat(iot,ops): 区域设备关联接口返回更多设备信息,修复 N+1 和代码质量问题
- IotDeviceSimpleRespDTO 新增 nickname、serialNumber、state、deviceType 字段
- IotDeviceQueryApi 新增 batchGetDevices 批量查询接口
- IotDeviceQueryApiImpl 提取 toSimpleDTO 统一转换、通过产品缓存解析 productName、
  移除 blanket try-catch 让异常正确传播、删除无用 import
- AreaDeviceRelationRespVO 新增 nickname、serialNumber、deviceState、deviceType 字段
- AreaDeviceRelationServiceImpl.listByAreaId 改为批量查询避免 N+1 RPC、
  增加 null 防护;bindDevice 改为 fail-fast 不再存脏数据
- ErrorCodeConstants 新增 IOT_SERVICE_UNAVAILABLE 错误码

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 21:06:10 +08:00
lzh
7d1012bba7 fix(iot,ops): 修复退出检测停滞、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
1. 蓝牙信号缺失补偿:设备属性上报不含 bluetoothDevices 时注入 null,
   避免 RSSI 滑动窗口因无数据停滞导致退出检测延迟
2. TTS 多租户去重:TtsQueueMessage 携带 tenantId,processSingleQueue
   过滤非当前租户消息,解决 @TenantJob 导致同一播报被不同租户重复下发
3. 循环播报日志精简:仅在 broadcastLoop 启动时记录一次 TTS_SENT,
   后续重复播报不再写入 ops_business_event_log
4. 移除离岗 TTS 警告和入队语音播报,减少不必要的设备干扰

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 13:09:56 +08:00
lzh
b4de9d0df8 feat(config): 修改测试环境地址 2026-02-26 17:16:24 +08:00
lzh
5ee039b0bf feat(ops,iot): 工单语音播报循环机制 + 统一按键逻辑
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
核心改动:
- 新增循环播报机制:DISPATCHED 状态持续播报"工单来啦"直到按键确认
- 统一按键逻辑:confirmKeyId 和 queryKeyId 都路由到同一处理逻辑,
  根据工单状态智能判断行为(确认/查询/无工单提示)
- ARRIVED/COMPLETED 状态静默不播报,CANCELLED 保留取消播报
- 修复 P0:确认去重后按键不再静默,改为发查询事件给反馈
- 修复 P0:PAUSED 状态(P0打断)时停止被打断工单的循环播报
- 修复 P1:handleCompleted 补全 deviceId 兜底逻辑
- 修复 P1:stopLoop 只移除循环消息,保留非循环消息

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 17:13:03 +08:00
lzh
3e54094c3d fix(iot): 修复客流阈值触发时 IoT 模块未将区域配置回写至 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
2026-02-24 18:01:39 +08:00
lzh
a68ce9a28a fix(xxl-job): 配置executor IP和端口解决跨服务器回调失败
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
XXL-Job Admin部署在Infra服务器,executor运行在Prod服务器的Docker容器中,
容器内部IP不可达,需指定宿主机IP和独立端口供Admin回调。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 17:03:39 +08:00
lzh
bec46c2919 fix(rocketmq): 修正ACL配置位置到producer/consumer节点下
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
rocketmq-spring-boot-starter的access-key/secret-key需配置在
producer和consumer节点下而非rocketmq根节点,同时为所有
@RocketMQMessageListener注解添加accessKey/secretKey属性。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 16:19:42 +08:00
lzh
748b09d355 fix(rocketmq): 添加腾讯云TDMQ ACL认证配置解决连接失败
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
修复RocketMQ发送消息报"No accessKey is configured"错误,
统一各模块环境变量名为ROCKETMQ_NAMESRV_ADDR。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 15:50:44 +08:00
lzh
91de356a67 fix(iot-gateway): 修正RPC环境变量名为VIEWSH_IOT_GATEWAY_RPC_URL
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
Spring Boot relaxed binding将viewsh.iot.gateway.rpc.url映射为
VIEWSH_IOT_GATEWAY_RPC_URL,而非VIEWSH_GATEWAY_RPC_URL。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 15:17:06 +08:00
lzh
e15ebfd3d4 fix(iot-gateway): 修正RPC环境变量名不匹配导致连接失败
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中VIEWSH_IOT_GATEWAY_RPC_URL与yaml期望的VIEWSH_GATEWAY_RPC_URL不一致,
导致iot-gateway回退到默认值127.0.0.1:48091,容器间无法通信。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 11:39:37 +08:00
lzh
26e909cce9 fix(deploy): 迁移Nacos/TDengine/XXL-Job至Infra服务器(172.17.16.7)
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服务器(172.17.16.14)统一迁移至Infra服务器(172.17.16.7):
- Nacos: 172.17.16.14:8848 → 172.17.16.7:8848
- TDengine: 172.17.16.14:6041 → 172.17.16.7:6041
- XXL-Job: 172.17.16.14:19090 → 172.17.16.7:19090

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 11:22:02 +08:00
lzh
547da7cfd2 refactor(deploy): 迁移CI/CD至双服务器架构
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: Registry改为Infra内网172.17.16.7:5000,部署目标改为Prod内网172.17.16.14
- docker-compose: 镜像源改为172.17.16.7:5000,MySQL改为172.17.16.8,Redis改为172.17.16.13,RocketMQ改为腾讯云TDMQ
- 所有模块application-prod.yaml: 统一更新MySQL/Redis/RocketMQ默认连接地址
- deploy.sh: Registry地址同步更新

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 10:41:54 +08:00
lzh
631612951c fix(iot): 修复客流统计持久化无数据和租户隔离问题
1. 解耦统计采集与工单触发:将 incrementDaily() 提前到配置检查之前,
   即使设备未配置工单触发规则,统计数据也能正常写入 Redis
2. 修复租户隔离:Redis Hash 中写入 tenantId,持久化任务读取后在
   正确的租户上下文中执行 upsert 和区域查询
3. 修复清理任务:使用 TenantUtils.executeIgnore() 避免 XXL-Job
   线程无租户上下文导致 NPE

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 13:18:48 +08:00
lzh
1147ae4503 fix(iot-gateway): 修改3d11正确ProductKey
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
2026-02-10 10:14:54 +08:00
lzh
41ffffe431 fix(iot): 修复 3D11 编解码器上报时间和数据过滤
- reportTime 改用 time 字段(设备上传时间)而非 endTime
- 新增 24 小时数据过滤,超时数据舍弃并返回 null
- 数据舍弃时仍返回成功响应,避免设备反复重试

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 13:52:18 +08:00
lzh
cc6b11f4e9 feat(iot): 对接 3D11 单目客流计数器
在 IoT Gateway 的 Vert.x Router 上注册 /api/camera/* 专用路由,
桥接 3D11 摄像头的心跳和数据上报到现有消息总线和编解码体系。

- 新建 Camera3D11 DTO(心跳请求、数据上报请求、统一响应)
- 新建 IotCamera3D11Codec 编解码器(TYPE=CAMERA_3D11)
- 新建 IotCameraUpstreamHandler 处理心跳和数据上报
- productKey 通过 application.yaml 配置,未配置时不注册路由
- 心跳上报间隔设为 1 分钟

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 00:23:44 +08:00
lzh
5edbc9f287 fix(ops): 修复工牌关机重启后工单状态不一致漏洞
问题场景:
1. 工牌有执行中工单(ARRIVED)后关机
2. 工牌重启,Redis状态丢失/过期,设备变为IDLE
3. 系统推送新工单
4. 信标检测仍在用旧工单配置,导致状态混乱

修复方案:
1. 派发新工单前检查并清理/取消旧工单残留
2. 设备离线时自动取消未完成的工单
3. 信标检测器增加工单切换检测,清理旧检测状态

涉及文件:
- BadgeDeviceStatusEventListener: 增加旧工单清理和离线事件监听
- BadgeDeviceStatusServiceImpl: 设备离线时发布事件
- BeaconDetectionRuleProcessor: 工单切换检测

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 22:59:06 +08:00
lzh
13571faa59 refactor(iot,ops): 重构客流计数器重置为按区域删除阈值 key
- ResetTrafficCounterReqDTO: 废弃 newBaseValue 字段
- IotDeviceControlApiImpl: 重置逻辑改为通过区域关联查询后删除阈值 key
- CleanOrderEventListener: 简化异步重置调用,移除 triggerData 依赖

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 15:37:23 +08:00
lzh
46024fd043 refactor(iot): 重构客流计数器为增量累加模式,支持 people_out
- 删除旧 TrafficCounterBaseRedisDAO(基准值模式),新增 TrafficCounterRedisDAO
  支持阈值计数器(达标后重置)和当日累积统计(用于报表)
- TrafficThresholdRuleProcessor 改为增量原子累加,消除基准值校准逻辑
- CleanRuleProcessorManager 路由增加 people_out 支持
- TrafficCounterBaseResetJob 改为每日清除阈值计数器,持久化职责移交 Ops 模块
- 使用 SCAN 替代 KEYS 避免阻塞 Redis

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 15:34:03 +08:00