diff --git a/bin/script/send_modbus_and_register.py b/bin/script/send_modbus_and_register.py index 0c56064c..7eea8590 100644 --- a/bin/script/send_modbus_and_register.py +++ b/bin/script/send_modbus_and_register.py @@ -6,14 +6,17 @@ from binascii import unhexlify TARGET_HOST = 'localhost' # 请根据实际情况修改 TARGET_PORT = 8888 # 请根据实际情况修改 -# 元数组 modbus 指令 +# 元数组 modbus 指令 DEF7B23FD985 D9C6983D30F8 MODBUS_DATA_HEX = ( - "7e80D87D8A07BCC47e", - "01031c41aa0c7941d78000402000220000000042b121523fb1fb373eb5668c9cd8", - "02031c419b24c741e18000400bf3910000000042a83e6840824127c0877958c7d0", - "03031c418eb64a41cb00003ff8d8130000000042aaaa05bf95093cc090a1de8803" + "7e80DEF7B23FD9857e" + "04031c41aa0c7941d78000402000220000000042b121523fb1fb373eb5668c4CDA", + "05031c419b24c741e18000400bf3910000000042a83e6840824127c08779583613", + "06031c418eb64a41cb00003ff8d8130000000042aaaa05bf95093cc090a1de5801" + ) + + # 重要-四川远德MQTT推送 (倾角计) # "7e803042593632517e", # "01033041011b7b0000000041eaafb742d19bd74513d545450dd9a4450ec53d00000000420ccf64420c74a9420c74a9c2d200009d99" @@ -27,6 +30,10 @@ MODBUS_DATA_HEX = ( # "01031c41aa0c7941d78000402000220000000042b121523fb1fb373eb5668c9cd8", # "02031c419b24c741e18000400bf3910000000042a83e6840824127c0877958c7d0", # "03031c418eb64a41cb00003ff8d8130000000042aaaa05bf95093cc090a1de8803" +# "04031c41aa0c7941d78000402000220000000042b121523fb1fb373eb5668c4CDA", +# "05031c419b24c741e18000400bf3910000000042a83e6840824127c08779583613", +# "06031c418eb64a41cb00003ff8d8130000000042aaaa05bf95093cc090a1de5801" + # 01 03 1c 4268 442f 41d3 0000 418b 6685 0000 0000 4026 f4ca c00d 1fd3 c2ad 2a9d 6311 # 从机地址 功能码 字节长度 2个字节 CRC校验 @@ -47,6 +54,8 @@ def main(): print("已连接。") for hex_bytes in MODBUS_DATA_HEX: + # 替换所有的空格 + hex_bytes = hex_bytes.replace(" ", "") # 0.将十六进制字符串转换为字节 modbus_bytes = unhexlify(hex_bytes) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index fa154571..ed2c0423 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -72,7 +72,7 @@ services: logging: driver: "json-file" options: - max-size: "10m" + max-size: "50m" max-file: "2" ports: - 8080:8080 diff --git a/docs/knowledge-base/07-TraceId 链路追踪实现.md b/docs/knowledge-base/07-TraceId 链路追踪实现.md new file mode 100644 index 00000000..56095386 --- /dev/null +++ b/docs/knowledge-base/07-TraceId 链路追踪实现.md @@ -0,0 +1,1213 @@ +# 任务:TraceId 链路追踪与上下文传播能力 + +你现在需要为我的 Java 单体项目实现一套“统一 TraceId 链路追踪基础设施”。 + +## 一、项目背景 + +当前项目是 Java 单体应用,技术栈: + +- JDK 8 +- Spring Boot 2.5.x +- Spring MVC +- Logback / SLF4J +- MySQL +- Redis +- Quartz +- MQ +- Netty +- MQTT +- 后续会接入 Python AI 服务、LLM、微服务、OpenTelemetry + +项目未来会逐步发展为: + +```text +单体应用 + ↓ +IoT + SaaS + AI + ↓ +微服务 + ↓ +OpenTelemetry / 分布式链路追踪 +``` + +因此这次实现不能只是简单地增加一个 MDC 字段,而要设计成可长期演进的基础框架。 + +--- + +# 二、核心目标 + +实现统一的: + +```text +TraceContext +``` + +核心字段至少包括: + +```text +traceId +tenantId +userId +``` + +其中当前阶段最重要的是: + +```text +traceId +``` + +核心原则: + +> 一次完整业务执行链使用同一个 traceId。 + +例如: + +```text +前端请求 + ↓ +Controller + ↓ +Service + ↓ +线程池 + ↓ +MQ + ↓ +MQ Consumer + ↓ +AI 服务 + ↓ +数据库 + ↓ +通知 +``` + +如果它们属于同一次业务执行,则全部使用: + +```text +traceId = A +``` + +--- + +# 三、TraceId 生命周期规则 + +必须严格遵循以下规则。 + +## 1. 有上游 TraceId + +优先继承。 + +例如 HTTP: + +```http +X-Trace-Id: 01KABC... +``` + +后端收到后: + +```text +TraceId = 01KABC... +``` + +不要重新生成。 + +--- + +## 2. 没有上游 TraceId + +自动创建新的 TraceId。 + +例如: + +```text +浏览器 + ↓ +HTTP + ↓ +Java +``` + +如果 HTTP 没有: + +```http +X-Trace-Id +``` + +则 Java 自动生成。 + +--- + +## 3. 定时任务没有上游 TraceId + +Quartz / @Scheduled 等任务启动时自动生成: + +```text +traceId +``` + +例如: + +```text +traceId=T202609150001 +``` + +整个任务执行过程保持相同 TraceId。 + +--- + +# 四、TraceId 生成器 + +设计独立: + +```java +TraceIdGenerator +``` + +不要把 TraceId 生成逻辑写死在 Filter 中。 + +要求: + +```text +TraceContext +TraceIdGenerator +``` + +职责分离。 + +优先考虑 ULID 或项目中已有的高质量 ID 生成方案。 + +要求: + +- 唯一 +- 高并发安全 +- 尽量时间有序 +- 适合日志检索 +- 不依赖数据库 +- 后续可以替换实现 + +--- + +# 五、TraceContext 设计 + +创建统一: + +```java +TraceContext +``` + +例如: + +```text +TraceContext +├── getTraceId() +├── setTraceId() +├── getOrCreateTraceId() +├── set() +├── get() +├── clear() +└── snapshot() +``` + +不要让业务代码直接到处: + +```java +MDC.put("traceId", ...) +``` + +业务代码应该依赖: + +```java +TraceContext +``` + +MDC 只是 TraceContext 的一种底层日志传播实现。 + +核心原则: + +```text +TraceContext ≠ MDC +``` + +而应该是: + +```text +TraceContext + ↓ +MDC +HTTP Header +MQ Header +ThreadPool +Future +``` + +--- + +# 六、Web 请求 + +实现 Spring MVC Filter。 + +建议: + +```java +OncePerRequestFilter +``` + +功能: + +1. 从 HTTP Header 获取: + +```http +X-Trace-Id +``` + +2. 如果存在且合法: + +```text +继续使用 +``` + +3. 如果不存在: + +```text +生成 TraceId +``` + +4. 放入: + +```text +TraceContext +MDC +``` + +5. 将 TraceId 写入 HTTP Response Header: + +```http +X-Trace-Id: 01KABC... +``` + +6. 请求结束后必须清理: + +```text +MDC +TraceContext +``` + +防止 Tomcat 线程池复用造成 TraceId 污染。 + +--- + +# 七、日志输出 + +修改 Logback 日志格式,使所有日志自动包含: + +```text +traceId +``` + +例如: + +```text +2026-09-15 11:20:31.123 INFO +[http-nio-8080-exec-5] +traceId=01KABC123 +DeviceService +- +查询设备成功 deviceId=DEV001 +``` + +要求: + +- 控制台日志包含 traceId +- 普通文件日志包含 traceId +- error 日志包含 traceId +- 不要求业务代码修改每一条 log.info() +- 使用 MDC 自动完成 + +同时考虑 JSON 日志扩展能力。 + +--- + +# 八、线程池上下文传播 + +这是本任务的重点。 + +普通: + +```java +ThreadPoolExecutor +``` + +或者: + +```java +CompletableFuture +``` + +不能保证 MDC 自动传播。 + +必须实现统一的上下文传播机制。 + +例如: + +```text +TraceContext + ↓ +snapshot + ↓ +Runnable / Callable + ↓ +restore + ↓ +执行 + ↓ +clear +``` + +要求实现类似: + +```text +TraceContextRunnable +TraceContextCallable +TraceableExecutor +``` + +或者使用 Spring 提供的: + +```text +TaskDecorator +``` + +如果项目使用 Spring ThreadPoolTaskExecutor,优先通过: + +```text +TaskDecorator +``` + +实现。 + +目标: + +```text +Web线程 +traceId=A + ↓ +线程池 +traceId=A +``` + +例如: + +```java +executor.execute(() -> { + log.info("异步处理"); +}); +``` + +日志必须仍然是: + +```text +traceId=A +``` + +--- + +# 九、CompletableFuture + +重点处理: + +```java +CompletableFuture.runAsync(...) +CompletableFuture.supplyAsync(...) +thenApply(...) +thenAccept(...) +thenRun(...) +``` + +分析项目中现有异步代码。 + +不要简单粗暴地修改所有业务代码。 + +优先通过统一线程池: + +```text +TraceableExecutor +``` + +解决。 + +如果无法完全覆盖 CompletableFuture 的默认线程池,需要明确指出风险,并提供统一解决方案。 + +禁止让业务代码到处手动: + +```java +MDC.put(...) +MDC.clear(...) +``` + +--- + +# 十、MQ TraceId 传播 + +这是第二个重点。 + +当 Java 发送 MQ 消息时: + +必须把当前: + +```text +traceId +``` + +写入 MQ Message Header / Properties。 + +例如: + +```text +traceId=A +``` + +发送: + +```text +MQ Message +├── Header +│ └── traceId=A +└── Body +``` + +Consumer 收到消息: + +```text +MQ Header + ↓ +读取 traceId + ↓ +TraceContext.setTraceId() + ↓ +MDC + ↓ +业务处理 +``` + +这样: + +```text +Web +traceId=A + ↓ +MQ Producer +traceId=A + ↓ +MQ +traceId=A + ↓ +Consumer +traceId=A +``` + +如果 MQ 消息没有 TraceId: + +```text +Consumer + ↓ +自动生成新的 TraceId +``` + +但正常情况下必须继承 Producer 的 TraceId。 + +--- + +# 十一、MQ 不同消息中间件 + +先检查当前项目实际使用的 MQ。 + +不要凭空假设。 + +如果项目目前只有一种 MQ: + +优先实现当前实际使用的 MQ。 + +同时将设计抽象成: + +```text +TraceMessageInterceptor +``` + +未来可以支持: + +```text +RabbitMQ +Kafka +RocketMQ +Redis Stream +其他 MQ +``` + +不要为了扩展性一次性实现大量没有使用的 MQ。 + +--- + +# 十二、HTTP 调用链 + +如果 Java 单体项目会调用其他服务: + +例如: + +```text +Java + ↓ HTTP +Python AI +``` + +必须自动传递: + +```http +X-Trace-Id: A +``` + +如果项目使用: + +```text +RestTemplate +WebClient +OpenFeign +HttpClient +OkHttp +``` + +检查实际使用情况。 + +优先通过统一 Interceptor 实现。 + +例如: + +```text +Java +traceId=A + ↓ +HTTP Header +X-Trace-Id=A + ↓ +Python AI +traceId=A +``` + +这样 Java 和 Python 日志可以使用同一个 TraceId 检索。 + +--- + +# 十三、Netty / MQTT / TCP / UDP + +这是 IoT 项目的重要场景。 + +检查项目现有: + +```text +Netty +MQTT +TCP +UDP +CoAP +``` + +入口。 + +对于每一个独立的消息 / 请求: + +如果存在上游 TraceId: + +```text +继承 +``` + +否则: + +```text +生成 +``` + +然后进入: + +```text +TraceContext +``` + +例如: + +```text +设备 DEV001 + ↓ +MQTT + ↓ +traceId=M001 + ↓ +数据解析 + ↓ +物模型 + ↓ +数据库 + ↓ +告警 +``` + +整条处理链使用: + +```text +traceId=M001 +``` + +注意: + +> 设备每次独立上报消息属于独立业务事件,因此通常应该生成新的 TraceId,而不是让一个设备永久使用同一个 TraceId。 + +--- + +# 十五、SaaS 上下文 + +项目未来是 SaaS,因此 TraceContext 预留: + +```text +traceId +tenantId +userId +``` + +如果当前项目已经有: + +```text +tenantId +userId +``` + +则在请求认证成功后自动放入上下文。 + +例如: + +```text +traceId=A +tenantId=10001 +userId=9527 +``` + +但不要因为本次任务而大范围修改现有权限系统。 + +只需要提供统一上下文承载能力。 + +--- + +# 十六、线程安全 + +这是重点。 + +必须考虑: + +```text +Tomcat线程池复用 +Quartz线程池 +业务线程池 +CompletableFuture +MQ Consumer线程 +Netty EventLoop +``` + +任何任务结束后都必须: + +```text +clear +``` + +禁止出现: + +```text +请求A +traceId=A + +请求结束 + +请求B +traceId=A +``` + +这种严重的数据串链问题。 + +--- + +# 十七、异常处理 + +异常情况下 TraceId 也必须保留。 + +例如: + +```text +traceId=A +ERROR +设备数据处理失败 +``` + +完整异常堆栈必须能够通过: + +```text +traceId=A +``` + +检索出来。 + +Web 异常响应最好也返回: + +```http +X-Trace-Id: A +``` + +这样前端报错: + +```text +TraceId: A +``` + +我就可以直接去 Kibana / 日志文件搜索: + +```text +traceId=A +``` + +定位问题。 + +--- + +# 十八、日志检索目标 + +最终必须支持: + +```bash +grep "traceId=01KABC" app.log +``` + +或者 Kibana: + +```text +traceId:"01KABC" +``` + +一次找到: + +```text +HTTP请求 +Service +数据库 +线程池 +MQ Producer +MQ Consumer +AI +异常 +``` + +--- + +# 十九、代码结构要求 + +根据当前项目实际结构,合理放置。 + +如果存在类似: + +```text +common +framework +core +starter +``` + +优先放到公共基础模块。 + +推荐结构: + +```text +xxx-core +└── trace + ├── TraceContext + ├── TraceIdGenerator + ├── TraceContextSnapshot + ├── TraceContextRunnable + └── TraceContextCallable + +xxx-web-starter +└── trace + └── TraceIdFilter + +xxx-log-starter +└── trace + └── MDC / Logback + +xxx-schedule-starter +└── trace + └── Quartz Trace + +xxx-mq-starter +└── trace + ├── Producer Trace + └── Consumer Trace +``` + +具体目录必须根据当前项目实际结构调整,不要机械套用。 + +--- + +# 二十、不要过度设计 + +当前项目是单体项目。 + +第一阶段: + +必须实现: + +```text +TraceId +MDC +Web +ThreadPool +Quartz +MQ +HTTP +``` + +暂时不要自行实现完整: + +```text +Span +Trace Graph +Jaeger +Zipkin +SkyWalking +OpenTelemetry Collector +``` + +但是代码设计要为未来 OpenTelemetry 留出扩展点。 + +也就是说: + +```text +现在: +TraceId + MDC + +未来: +TraceId + SpanId + ParentSpanId + OpenTelemetry +``` + +不要把当前 TraceContext 设计成未来无法迁移的死结构。 + +--- + +# 二十一、需要先做代码分析 + +在开始修改代码之前: + +1. 扫描整个项目结构。 +2. 找到 Spring Boot 启动模块。 +3. 找到 Web 请求入口。 +4. 找到现有 Filter / Interceptor。 +5. 找到现有线程池。 +6. 找到 CompletableFuture。 +7. 找到 Quartz / @Scheduled。 +8. 找到 MQ Producer。 +9. 找到 MQ Consumer。 +10. 找到 RestTemplate / Feign / HTTP Client。 +11. 找到 Netty / MQTT / TCP / UDP 入口。 +12. 找到 Logback 配置。 +13. 找到现有 MDC / ThreadLocal / RequestContext 实现。 +14. 检查项目是否已经存在类似 TraceId / RequestId 机制。 + +如果已经存在类似能力: + +> 优先重构和统一,不要重复造轮子。 + +--- + +# 二十二、实现原则 + +遵守: + +```text +低侵入 +高复用 +自动传播 +自动清理 +统一入口 +统一出口 +``` + +业务代码尽可能不需要修改。 + +理想效果: + +原来的代码: + +```java +log.info("开始处理设备"); +``` + +完全不用修改。 + +自动变成: + +```text +traceId=A 开始处理设备 +``` + +--- + +# 二十三、测试要求 + +实现完成后必须增加测试。 + +至少测试: + +### Test 1:HTTP + +```text +请求进入 +↓ +生成 TraceId +↓ +Controller +↓ +Service +↓ +日志 +↓ +Response Header +``` + +确认 TraceId 一致。 + +### Test 2:HTTP携带 TraceId + +```text +X-Trace-Id=A +``` + +确认后端仍然使用: + +```text +A +``` + +### Test 3:线程池 + +```text +Web traceId=A +↓ +ThreadPool +``` + +确认: + +```text +traceId=A +``` + +### Test 4:MQ + +```text +Producer traceId=A +↓ +MQ Header +↓ +Consumer +``` + +确认 Consumer: + +```text +traceId=A +``` + +### Test 5:MQ无 TraceId + +确认 Consumer 自动生成。 + +### Test 6:定时任务 + +确认: + +```text +TASK traceId=A +``` + +整个任务一致。 + +### Test 7:线程池污染 + +执行: + +```text +请求A +请求B +请求C +``` + +确认: + +```text +A ≠ B ≠ C +``` + +并且不会出现线程复用导致 TraceId 串线。 + +### Test 8:异常 + +异常发生后: + +```text +traceId +``` + +仍然存在于 ERROR 日志。 + +### Test 9:并发 + +同时执行大量请求: + +```text +100+ +1000+ +``` + +确认 TraceId 不串线。 + +--- + +# 二十四、最终输出要求 + +完成后请给我: + +## 1. 修改文件列表 + +例如: + +```text +新增: +xxx/TraceContext.java + +修改: +xxx/Logback.xml +xxx/xxxFilter.java +``` + +## 2. 架构说明 + +说明: + +```text +Web + ↓ +TraceContext + ↓ +MDC + +ThreadPool + ↓ +Context Propagation + +MQ + ↓ +Message Header + +HTTP + ↓ +X-Trace-Id +``` + +## 3. 使用示例 + +告诉我业务代码以后应该怎么写。 + +目标是: + +```java +log.info("开始处理"); +``` + +而不是让业务代码手动管理 TraceId。 + +## 4. 测试结果 + +说明所有测试是否通过。 + +## 5. 风险 + +明确指出: + +- 哪些场景已经支持 +- 哪些场景暂未支持 +- 是否存在 MDC 丢失风险 +- 是否存在线程池上下文污染风险 +- 是否存在 MQ TraceId 丢失风险 + +## 6. 后续演进建议 + +最后说明未来如何从: + +```text +TraceId + MDC +``` + +演进到: + +```text +TraceId ++ +SpanId ++ +ParentSpanId ++ +OpenTelemetry +``` + +但本次不要提前实现完整分布式链路追踪系统。 + +--- + +# 最终验收标准 + +只有同时满足以下条件才算完成: + +```text + traceId=A + │ + ┌───────┴───────┐ + │ │ + Web Task + │ + ▼ + Service + │ + ┌───────┴───────┐ + ▼ ▼ + ThreadPool MQ + │ │ + │ ▼ + │ Consumer + │ │ + └───────┬───────┘ + ▼ + AI/HTTP + │ + ▼ + DB +``` + +同一业务链可以通过: + +```text +traceId=A +``` + +在日志文件、IDEA Console、Kibana 中完整检索。 + +尤其必须保证: + +> **跨线程、跨线程池、跨 MQ、跨 HTTP 的 TraceId 不丢失;任务执行完成后上下文必须清理,不允许线程复用造成 TraceId 污染。** + +请先分析当前代码,再实施修改。 + +不要在没有检查项目现有实现的情况下直接创建重复的 TraceId / MDC / ThreadLocal 体系。 \ No newline at end of file diff --git a/fastbee-admin/src/main/java/com/fastbee/web/task/LogCleanTask.java b/fastbee-admin/src/main/java/com/fastbee/web/task/LogCleanTask.java new file mode 100644 index 00000000..d0ae5e0c --- /dev/null +++ b/fastbee-admin/src/main/java/com/fastbee/web/task/LogCleanTask.java @@ -0,0 +1,94 @@ +package com.fastbee.web.task; + +import com.fastbee.iot.platform.service.IPlatformPushLogService; +import com.fastbee.iot.service.IAlertLogService; +import com.fastbee.iot.service.IEventLogService; +import com.fastbee.notify.service.INotifyLogService; +import com.fastbee.quartz.service.ISysJobLogService; +import com.fastbee.system.service.ISysLogininforService; +import com.fastbee.system.service.ISysOperLogService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * 日志清理定时任务 + * + * @author fastbee + */ +@Component("logCleanTask") +public class LogCleanTask { + private static final Logger log = LoggerFactory.getLogger(LogCleanTask.class); + + @Autowired + private IEventLogService eventLogService; + + @Autowired + private ISysOperLogService operLogService; + + @Autowired + private ISysLogininforService logininforService; + + @Autowired + private IAlertLogService alertLogService; + + @Autowired + private ISysJobLogService jobLogService; + + @Autowired + private INotifyLogService notifyLogService; + + @Autowired + private IPlatformPushLogService platformPushLogService; + + // 之后还有清理的表往下写 + + /** + * 清理日志(默认清理15天前) + */ + public void cleanLogs() { + cleanLogs(15); + } + + /** + * 清理日志 + * + * @param days 保留天数 + */ + public void cleanLogs(int days) { + log.info("========== 开始清理日志数据,保留最近 {} 天的记录 ==========", days); + try { + // 清理 IoT 事件日志 + eventLogService.cleanEventLog(days); + //log.info("已清理 iot_event_log 表 {} 天前的数据", days); + + // 清理系统操作日志 + operLogService.cleanOperLog(days); + //log.info("已清理 sys_oper_log 表 {} 天前的数据", days); + + // 清理登录日志 + logininforService.cleanLogininfor(days); + //log.info("已清理 sys_logininfor 表 {} 天前的数据", days); + + // 清理设备告警日志 + alertLogService.cleanAlertLog(days); + //log.info("已清理 iot_alert_log 表 {} 天前的数据", days); + + // 清理定时任务日志 + jobLogService.cleanJobLog(days); + //log.info("已清理 sys_job_log 表 {} 天前的数据", days); + + // 清理通知日志 + notifyLogService.cleanNotifyLog(days); + //log.info("已清理 notify_log 表 {} 天前的数据", days); + + platformPushLogService.cleanPushLog(days); + + log.info("========== 日志清理任务执行完成 =========="); + } catch (Exception e) { + log.error("日志清理任务执行失败", e); + throw e; + } + } +} \ No newline at end of file diff --git a/fastbee-common/src/main/java/com/fastbee/common/config/DeviceTask.java b/fastbee-common/src/main/java/com/fastbee/common/config/DeviceTask.java index f2d1afb2..67f2d431 100644 --- a/fastbee-common/src/main/java/com/fastbee/common/config/DeviceTask.java +++ b/fastbee-common/src/main/java/com/fastbee/common/config/DeviceTask.java @@ -1,6 +1,7 @@ package com.fastbee.common.config; import com.fastbee.common.constant.FastBeeConstant; +import com.fastbee.common.trace.MdcTaskDecorator; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; @@ -60,7 +61,11 @@ public class DeviceTask { return builder(FastBeeConstant.TASK.FUNCTION_INVOKE_TASK); } - /*内部消费线程*/ + /** + * 内部消费线程 + * 监听循环从消息恢复 TraceId 并逐条清理; + * TaskDecorator 用于让 @Async 消费方法继承监听线程的 TraceId + */ @Bean(FastBeeConstant.TASK.MESSAGE_CONSUME_TASK) public Executor messageConsumeTaskExecutor() { return builder(FastBeeConstant.TASK.MESSAGE_CONSUME_TASK); @@ -104,7 +109,7 @@ public class DeviceTask { } - /*组装线程池*/ + /*组装线程池(带 TaskDecorator,用于跨线程传播 TraceId)*/ private ThreadPoolTaskExecutor builder(String threadNamePrefix) { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(coreSize); @@ -115,6 +120,10 @@ public class DeviceTask { executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy()); //线程池名的前缀 executor.setThreadNamePrefix(threadNamePrefix); + + // 配置 TaskDecorator,自动传播 TraceId + executor.setTaskDecorator(new MdcTaskDecorator()); + executor.initialize(); return executor; } diff --git a/fastbee-common/src/main/java/com/fastbee/common/constant/FastBeeConstant.java b/fastbee-common/src/main/java/com/fastbee/common/constant/FastBeeConstant.java index 89be5860..d8eebe75 100644 --- a/fastbee-common/src/main/java/com/fastbee/common/constant/FastBeeConstant.java +++ b/fastbee-common/src/main/java/com/fastbee/common/constant/FastBeeConstant.java @@ -15,6 +15,8 @@ public interface FastBeeConstant { String MQTT = "mqtt"; String PORT = "port"; String ADAPTER = "adapter"; + /** 链路追踪处理器名 */ + String TRACE = "trace"; String FRAMEDECODER = "frameDecoder"; String DISPATCHER = "dispatcher"; String DECODER = "decoder"; diff --git a/fastbee-common/src/main/java/com/fastbee/common/core/mq/DeviceReplyBo.java b/fastbee-common/src/main/java/com/fastbee/common/core/mq/DeviceReplyBo.java index e45f1ea8..c849844a 100644 --- a/fastbee-common/src/main/java/com/fastbee/common/core/mq/DeviceReplyBo.java +++ b/fastbee-common/src/main/java/com/fastbee/common/core/mq/DeviceReplyBo.java @@ -8,6 +8,9 @@ import lombok.Data; @Data public class DeviceReplyBo { + /** 链路追踪ID */ + private String traceId; + /*设备下发消息id*/ private String messageId; /*标识符*/ diff --git a/fastbee-common/src/main/java/com/fastbee/common/core/mq/DeviceReportBo.java b/fastbee-common/src/main/java/com/fastbee/common/core/mq/DeviceReportBo.java index d4506ccd..edbce754 100644 --- a/fastbee-common/src/main/java/com/fastbee/common/core/mq/DeviceReportBo.java +++ b/fastbee-common/src/main/java/com/fastbee/common/core/mq/DeviceReportBo.java @@ -24,6 +24,9 @@ import java.util.List; @AllArgsConstructor public class DeviceReportBo { + /** 链路追踪ID */ + private String traceId; + /*设备编号或IMEI号*/ private String serialNumber; /*产品ID*/ diff --git a/fastbee-common/src/main/java/com/fastbee/common/core/mq/DeviceStatusBo.java b/fastbee-common/src/main/java/com/fastbee/common/core/mq/DeviceStatusBo.java index 6bcd9adc..68567cf2 100644 --- a/fastbee-common/src/main/java/com/fastbee/common/core/mq/DeviceStatusBo.java +++ b/fastbee-common/src/main/java/com/fastbee/common/core/mq/DeviceStatusBo.java @@ -18,6 +18,10 @@ import java.util.Date; @Data @Builder public class DeviceStatusBo { + + /** 链路追踪ID */ + private String traceId; + /** * 设备客户端id */ diff --git a/fastbee-common/src/main/java/com/fastbee/common/core/mq/DeviceTestReportBo.java b/fastbee-common/src/main/java/com/fastbee/common/core/mq/DeviceTestReportBo.java index b7e3e7ff..cd43f370 100644 --- a/fastbee-common/src/main/java/com/fastbee/common/core/mq/DeviceTestReportBo.java +++ b/fastbee-common/src/main/java/com/fastbee/common/core/mq/DeviceTestReportBo.java @@ -11,6 +11,9 @@ import java.util.List; @Data public class DeviceTestReportBo { + /** 链路追踪ID */ + private String traceId; + private Long productId; /** * 设备编号 diff --git a/fastbee-common/src/main/java/com/fastbee/common/core/mq/InvokeReqDto.java b/fastbee-common/src/main/java/com/fastbee/common/core/mq/InvokeReqDto.java index acbc8ed5..386c7457 100644 --- a/fastbee-common/src/main/java/com/fastbee/common/core/mq/InvokeReqDto.java +++ b/fastbee-common/src/main/java/com/fastbee/common/core/mq/InvokeReqDto.java @@ -17,6 +17,9 @@ import java.util.Map; @Data public class InvokeReqDto { + /** 链路追踪ID */ + private String traceId; + @ApiModelProperty(value = "设备编号") private String serialNumber; diff --git a/fastbee-common/src/main/java/com/fastbee/common/core/mq/MQSendMessageBo.java b/fastbee-common/src/main/java/com/fastbee/common/core/mq/MQSendMessageBo.java index f302c489..c5d2f9cc 100644 --- a/fastbee-common/src/main/java/com/fastbee/common/core/mq/MQSendMessageBo.java +++ b/fastbee-common/src/main/java/com/fastbee/common/core/mq/MQSendMessageBo.java @@ -14,6 +14,9 @@ import lombok.NoArgsConstructor; @NoArgsConstructor public class MQSendMessageBo { + /** 链路追踪ID */ + private String traceId; + public Boolean isShadow; public Long userId; public Long delay; diff --git a/fastbee-common/src/main/java/com/fastbee/common/core/mq/ota/OtaUpgradeBo.java b/fastbee-common/src/main/java/com/fastbee/common/core/mq/ota/OtaUpgradeBo.java index fc6db91a..9ddf34ed 100644 --- a/fastbee-common/src/main/java/com/fastbee/common/core/mq/ota/OtaUpgradeBo.java +++ b/fastbee-common/src/main/java/com/fastbee/common/core/mq/ota/OtaUpgradeBo.java @@ -17,6 +17,9 @@ import java.math.BigDecimal; @JsonInclude(JsonInclude.Include.NON_EMPTY) public class OtaUpgradeBo { + /** 链路追踪ID */ + private String traceId; + // 设备编码 private String serialNumber; // 升级任务ID diff --git a/fastbee-common/src/main/java/com/fastbee/common/core/mq/ota/OtaUpgradeDelayTask.java b/fastbee-common/src/main/java/com/fastbee/common/core/mq/ota/OtaUpgradeDelayTask.java index ae5e3d50..96585439 100644 --- a/fastbee-common/src/main/java/com/fastbee/common/core/mq/ota/OtaUpgradeDelayTask.java +++ b/fastbee-common/src/main/java/com/fastbee/common/core/mq/ota/OtaUpgradeDelayTask.java @@ -17,6 +17,11 @@ import java.util.concurrent.TimeUnit; @Data public class OtaUpgradeDelayTask implements Delayed { + /** + * 链路追踪ID + */ + private String traceId; + /** * 固件id */ diff --git a/fastbee-common/src/main/java/com/fastbee/common/trace/MdcTaskDecorator.java b/fastbee-common/src/main/java/com/fastbee/common/trace/MdcTaskDecorator.java new file mode 100644 index 00000000..26758f30 --- /dev/null +++ b/fastbee-common/src/main/java/com/fastbee/common/trace/MdcTaskDecorator.java @@ -0,0 +1,75 @@ +package com.fastbee.common.trace; + +import com.fastbee.common.trace.TraceContext.TraceSnapshot; +import org.springframework.core.task.TaskDecorator; + +import java.util.concurrent.Callable; + +/** + * 线程池 TraceId 传播装饰器 + * 解决 @Async 异步任务 TraceId 丢失问题 + * + *

使用方法: + *

+ * ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+ * executor.setTaskDecorator(new MdcTaskDecorator());
+ * 
+ * + *

工作原理: + *

    + *
  1. 在任务提交时,捕获当前线程的 TraceContext 快照
  2. + *
  3. 在任务执行时,恢复快照到新线程(MDC 由 TraceContext 统一同步)
  4. + *
  5. 在任务完成后,清理上下文防止线程池污染
  6. + *
+ * + *

说明:MDC 的写入/清理全部由 {@link TraceContext} 统一管理, + * 本装饰器只负责快照与恢复,避免 MDC 与 TraceContext 双重操作导致的不一致。 + * + * @author FastBee + */ +public class MdcTaskDecorator implements TaskDecorator { + + @Override + public Runnable decorate(Runnable runnable) { + // 捕获当前线程的 TraceContext 快照(跨线程传播的唯一依据) + TraceSnapshot snapshot = TraceContext.snapshot(); + + return () -> { + if (snapshot != null) { + TraceContext.restore(snapshot); + } else { + // 父线程无上下文,生成新的 TraceId + TraceContext.getOrCreateTraceId(); + } + try { + runnable.run(); + } finally { + // 清理上下文,防止线程池复用造成污染 + TraceContext.clear(); + } + }; + } + + /** + * 装饰 Callable + * + * @param task Callable 任务 + * @return 装饰后的 Callable + */ + public Callable decorate(Callable task) { + TraceSnapshot snapshot = TraceContext.snapshot(); + + return () -> { + if (snapshot != null) { + TraceContext.restore(snapshot); + } else { + TraceContext.getOrCreateTraceId(); + } + try { + return task.call(); + } finally { + TraceContext.clear(); + } + }; + } +} \ No newline at end of file diff --git a/fastbee-common/src/main/java/com/fastbee/common/trace/TraceContext.java b/fastbee-common/src/main/java/com/fastbee/common/trace/TraceContext.java new file mode 100644 index 00000000..7c807628 --- /dev/null +++ b/fastbee-common/src/main/java/com/fastbee/common/trace/TraceContext.java @@ -0,0 +1,270 @@ +package com.fastbee.common.trace; + +import lombok.Data; +import org.slf4j.MDC; + +import java.io.Serializable; + +/** + * 统一追踪上下文 + * 管理 traceId, tenantId, userId 等追踪信息 + * + *

核心特性: + *

+ * + *

使用示例: + *

+ * // Web请求入口
+ * TraceContext.setTraceId(TraceIdGenerator.generate());
+ *
+ * // 异步任务(通过 TaskDecorator 自动传播)
+ * @Async
+ * public void asyncTask() {
+ *     log.info("处理中"); // 日志自动包含 traceId
+ * }
+ *
+ * // 手动传播(用于非 Spring 管理的线程池)
+ * TraceSnapshot snapshot = TraceContext.snapshot();
+ * executor.submit(() -> {
+ *     TraceContext.restore(snapshot);
+ *     try {
+ *         // 业务逻辑
+ *     } finally {
+ *         TraceContext.clear();
+ *     }
+ * });
+ * 
+ * + * @author FastBee + */ +public class TraceContext { + + /** + * TraceId 在 MDC 中的 key + */ + public static final String TRACE_ID_KEY = "traceId"; + + /** + * TenantId 在 MDC 中的 key + * 预留字段,暂未使用 + */ + public static final String TENANT_ID_KEY = "tenantId"; + + /** + * UserId 在 MDC 中的 key + * 预留字段,暂未使用 + */ + public static final String USER_ID_KEY = "userId"; + + /** + * 线程本地上下文存储 + */ + private static final ThreadLocal CONTEXT = new ThreadLocal<>(); + + /** + * 追踪上下文快照 + * 用于跨线程传递 + */ + @Data + public static class TraceSnapshot implements Serializable { + private static final long serialVersionUID = 1L; + + private String traceId; + private Long tenantId; // 预留字段 + private Long userId; // 预留字段 + + /** + * 将快照应用到 MDC + * 先移除旧值再写入,保证 MDC 状态与快照精确一致(包括 null 字段) + */ + public void applyToMdc() { + MDC.remove(TRACE_ID_KEY); + MDC.remove(TENANT_ID_KEY); + MDC.remove(USER_ID_KEY); + if (traceId != null) { + MDC.put(TRACE_ID_KEY, traceId); + } + if (tenantId != null) { + MDC.put(TENANT_ID_KEY, String.valueOf(tenantId)); + } + if (userId != null) { + MDC.put(USER_ID_KEY, String.valueOf(userId)); + } + } + } + + // ================== Get Methods ================== + + /** + * 获取当前 TraceId + * + * @return TraceId,如果不存在返回 null + */ + public static String getTraceId() { + TraceSnapshot snapshot = CONTEXT.get(); + return snapshot != null ? snapshot.getTraceId() : null; + } + + /** + * 获取当前 TenantId(预留) + * + * @return TenantId,如果不存在返回 null + */ + public static Long getTenantId() { + TraceSnapshot snapshot = CONTEXT.get(); + return snapshot != null ? snapshot.getTenantId() : null; + } + + /** + * 获取当前 UserId(预留) + * + * @return UserId,如果不存在返回 null + */ + public static Long getUserId() { + TraceSnapshot snapshot = CONTEXT.get(); + return snapshot != null ? snapshot.getUserId() : null; + } + + // ================== Set Methods ================== + + /** + * 设置 TraceId + * 同时更新 ThreadLocal 和 MDC + * + * @param traceId TraceId + */ + public static void setTraceId(String traceId) { + TraceSnapshot snapshot = CONTEXT.get(); + if (snapshot == null) { + snapshot = new TraceSnapshot(); + CONTEXT.set(snapshot); + } + snapshot.setTraceId(traceId); + if (traceId != null) { + MDC.put(TRACE_ID_KEY, traceId); + } else { + MDC.remove(TRACE_ID_KEY); + } + } + + /** + * 设置 TenantId(预留) + * 同时更新 ThreadLocal 和 MDC + * + * @param tenantId TenantId + */ + public static void setTenantId(Long tenantId) { + TraceSnapshot snapshot = CONTEXT.get(); + if (snapshot == null) { + snapshot = new TraceSnapshot(); + CONTEXT.set(snapshot); + } + snapshot.setTenantId(tenantId); + if (tenantId != null) { + MDC.put(TENANT_ID_KEY, String.valueOf(tenantId)); + } else { + MDC.remove(TENANT_ID_KEY); + } + } + + /** + * 设置 UserId(预留) + * 同时更新 ThreadLocal 和 MDC + * + * @param userId UserId + */ + public static void setUserId(Long userId) { + TraceSnapshot snapshot = CONTEXT.get(); + if (snapshot == null) { + snapshot = new TraceSnapshot(); + CONTEXT.set(snapshot); + } + snapshot.setUserId(userId); + if (userId != null) { + MDC.put(USER_ID_KEY, String.valueOf(userId)); + } else { + MDC.remove(USER_ID_KEY); + } + } + + // ================== Convenience Methods ================== + + /** + * 获取或创建 TraceId + * 如果当前没有 TraceId,自动生成一个新的 + * + * @return TraceId + */ + public static String getOrCreateTraceId() { + String traceId = getTraceId(); + if (traceId == null || traceId.isEmpty()) { + traceId = TraceIdGenerator.generate(); + setTraceId(traceId); + } + return traceId; + } + + /** + * 确保当前有 TraceId + * 如果传入的 traceId 不为空,使用传入的;否则生成新的 + * + * @param traceId 可选的 TraceId + * @return 最终的 TraceId + */ + public static String ensureTraceId(String traceId) { + if (traceId != null && !traceId.isEmpty()) { + setTraceId(traceId); + return traceId; + } + return getOrCreateTraceId(); + } + + // ================== Snapshot & Restore ================== + + /** + * 创建当前上下文的快照 + * 用于跨线程传递 + * + * @return TraceSnapshot 快照,如果当前无上下文返回 null + */ + public static TraceSnapshot snapshot() { + TraceSnapshot current = CONTEXT.get(); + if (current == null) { + return null; + } + TraceSnapshot copy = new TraceSnapshot(); + copy.setTraceId(current.getTraceId()); + copy.setTenantId(current.getTenantId()); + copy.setUserId(current.getUserId()); + return copy; + } + + /** + * 从快照恢复上下文 + * + * @param snapshot 快照,如果为 null 则清理上下文 + */ + public static void restore(TraceSnapshot snapshot) { + if (snapshot == null) { + clear(); + return; + } + CONTEXT.set(snapshot); + snapshot.applyToMdc(); + } + + /** + * 清除当前上下文 + * 同时清除 ThreadLocal 和 MDC + */ + public static void clear() { + CONTEXT.remove(); + MDC.remove(TRACE_ID_KEY); + MDC.remove(TENANT_ID_KEY); + MDC.remove(USER_ID_KEY); + } +} \ No newline at end of file diff --git a/fastbee-common/src/main/java/com/fastbee/common/trace/TraceIdGenerator.java b/fastbee-common/src/main/java/com/fastbee/common/trace/TraceIdGenerator.java new file mode 100644 index 00000000..755780bb --- /dev/null +++ b/fastbee-common/src/main/java/com/fastbee/common/trace/TraceIdGenerator.java @@ -0,0 +1,106 @@ +package com.fastbee.common.trace; + +import org.slf4j.MDC; + +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicLong; + +/** + * TraceId 生成器 + * 生成纯数字格式的 TraceId + * + *

格式:基于时间戳 + 序列号 + 随机数 + *

示例:752431931275776594 + * + *

特点: + *

+ * + * @author FastBee + */ +public class TraceIdGenerator { + + /** + * 起始时间戳 (2023-01-01 00:00:00) + * 用于减少时间戳位数,延长 ID 可用年限 + */ + private static final long START_TIMESTAMP = 1672531200000L; + + /** + * 序列号掩码:16 bit(0 ~ 65535) + */ + private static final long SEQUENCE_MASK = 0xFFFFL; + + /** + * 序列号,同一毫秒内递增 + */ + private static final AtomicLong sequence = new AtomicLong(0); + + /** + * 上次生成时间戳 + */ + private static volatile long lastTimestamp = -1L; + + /** + * 生成纯数字格式的 TraceId + * + * @return 纯数字 TraceId 字符串 + */ + public static String generate() { + return String.valueOf(generateLong()); + } + + /** + * 生成 long 类型的 TraceId + * 使用 synchronized 保证线程安全 + * + *

位域分配(低到高): + *

+ * + * @return long 类型 TraceId + */ + public static synchronized long generateLong() { + long timestamp = System.currentTimeMillis(); + + // 时钟回拨保护:时间不允许倒退,沿用上次时间戳 + if (timestamp < lastTimestamp) { + timestamp = lastTimestamp; + } + + if (timestamp == lastTimestamp) { + // 同毫秒:序列号递增并回写(& MASK 防止溢出侵入时间位域) + sequence.set((sequence.get() + 1) & SEQUENCE_MASK); + if (sequence.get() == 0) { + // 序列号用尽,等待下一毫秒 + timestamp = tilNextMillis(lastTimestamp); + } + } else { + sequence.set(0); + } + lastTimestamp = timestamp; + + // 时间差(高位) | 序列号(中位) | 随机数(低位 6 bit) + long timeDiff = timestamp - START_TIMESTAMP; + long random = ThreadLocalRandom.current().nextInt(64); + return (timeDiff << 22) | (sequence.get() << 6) | random; + } + + /** + * 等待下一毫秒 + */ + private static long tilNextMillis(long lastTimestamp) { + long timestamp = System.currentTimeMillis(); + while (timestamp <= lastTimestamp) { + timestamp = System.currentTimeMillis(); + } + return timestamp; + } +} \ No newline at end of file diff --git a/fastbee-common/src/main/java/com/fastbee/common/utils/StringUtils.java b/fastbee-common/src/main/java/com/fastbee/common/utils/StringUtils.java index 3b5b0e7b..ce9e00d4 100644 --- a/fastbee-common/src/main/java/com/fastbee/common/utils/StringUtils.java +++ b/fastbee-common/src/main/java/com/fastbee/common/utils/StringUtils.java @@ -295,21 +295,19 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils { /** * 判断给定的set列表中是否包含数组array 判断给定的数组array中是否包含给定的元素value * - * @param set 给定的集合 + * @param collection 给定的集合 * @param array 给定的数组 * @return boolean 结果 */ public static boolean containsAny(Collection collection, String... array) { - if (isEmpty(collection) || isEmpty(array)) { - return false; - } else { + if (!isEmpty(collection) && !isEmpty(array)) { for (String str : array) { if (collection.contains(str)) { return true; } } - return false; } + return false; } /** diff --git a/fastbee-common/src/main/java/com/fastbee/common/utils/TraceIdUtils.java b/fastbee-common/src/main/java/com/fastbee/common/utils/TraceIdUtils.java index fdd7138c..c05442f7 100644 --- a/fastbee-common/src/main/java/com/fastbee/common/utils/TraceIdUtils.java +++ b/fastbee-common/src/main/java/com/fastbee/common/utils/TraceIdUtils.java @@ -1,188 +1,64 @@ package com.fastbee.common.utils; -import com.fastbee.common.utils.uuid.IdUtils; -import org.slf4j.MDC; +import com.fastbee.common.trace.TraceContext; +import com.fastbee.common.trace.TraceContext.TraceSnapshot; -import java.util.Map; import java.util.concurrent.Callable; -import java.util.concurrent.ThreadLocalRandom; /** * 日志追踪ID工具类 - * 统一生成TraceId的逻辑,使用IdWorker和ThreadLocalRandom + * 提供向后兼容的 API,内部委托给 TraceContext * * @author Sumuxi + * 建议直接使用 {@link TraceContext} */ public class TraceIdUtils { /** - * TraceId在MDC中的key - */ - public static final String TRACE_ID_KEY = "traceId"; - - /** - * IdWorker实例(单例模式) - */ - private static volatile IdUtils idWorker; - - /** - * 获取IdWorker实例(懒加载单例) + * 包装 Runnable:透传 TraceContext(MDC 由 TraceContext 统一同步) + * 用于非 Spring 管理的线程池(如 ScheduledExecutorService) * - * @return IdWorker实例 - */ - private static IdUtils getIdWorker() { - if (idWorker == null) { - synchronized (TraceIdUtils.class) { - if (idWorker == null) { - // 使用机器ID,可以根据实际情况配置 - // 这里使用1作为默认workerId,可以通过配置文件读取 - // IdUtils构造函数:workerId, datacenterId,-1表示随机生成 - idWorker = new IdUtils(-1L, -1L); - } - } - } - return idWorker; - } - - /** - * 生成TraceId - * 格式:IdWorker.nextId() * 1000 + ThreadLocalRandom.nextInt(1000) - * 参考:generateClueId() 的实现方式 - * 使用Math.abs确保TraceId为正数 - * - * @return TraceId - */ - public static long generateTraceId() { - long id = getIdWorker().nextId(); - // 使用绝对值确保TraceId为正数,避免负数显示 - // 注意:Math.abs(id) * 1000 可能会溢出,所以需要对最终结果再次使用Math.abs() - long traceId = Math.abs(id) * 1000 + ThreadLocalRandom.current().nextInt(1000); - // 确保最终结果为非负数(处理溢出情况) - traceId = Math.abs(traceId); - MDC.put(TRACE_ID_KEY, String.valueOf(traceId)); - return traceId; - } - - /** - * 生成TraceId并放入MDC - * 返回字符串格式的TraceId - * - * @return TraceId字符串 - */ - public static String generateTraceIdString() { - long traceId = generateTraceId(); - return String.valueOf(traceId); - } - - /** - * 设置TraceId到MDC - * - * @param traceId TraceId - */ - public static void setTraceId(String traceId) { - if (traceId != null && !traceId.isEmpty()) { - MDC.put(TRACE_ID_KEY, traceId); - } - } - - /** - * 设置TraceId到MDC(long类型) - * - * @param traceId TraceId - */ - public static void setTraceId(long traceId) { - MDC.put(TRACE_ID_KEY, String.valueOf(traceId)); - } - - /** - * 从MDC获取TraceId - * - * @return TraceId - */ - public static String getTraceId() { - return MDC.get(TRACE_ID_KEY); - } - - /** - * 确保当前线程 MDC 中存在 TraceId。 - * - 已存在:直接返回 - * - 不存在:生成并写入 MDC - */ - public static String ensureTraceId() { - String traceId = getTraceId(); - if (traceId == null || traceId.isEmpty()) { - traceId = generateTraceIdString(); - } - return traceId; - } - - /** - * 使用指定 traceId 覆盖当前线程 MDC(为空则生成)。 - * 主要用于跨线程/跨进程透传后的落地。 - */ - public static String ensureTraceId(String traceId) { - if (traceId == null || traceId.isEmpty()) { - return ensureTraceId(); - } - setTraceId(traceId); - return traceId; - } - - /** - * 包装 Runnable:透传提交线程的 MDC,上下文中无 traceId 则补齐;执行完自动清理/还原。 + * @param task Runnable 任务 + * @return 包装后的 Runnable */ public static Runnable wrapWithMdc(Runnable task) { - final Map captured = MDC.getCopyOfContextMap(); + final TraceSnapshot snapshot = TraceContext.snapshot(); + return () -> { - Map previous = MDC.getCopyOfContextMap(); + if (snapshot != null) { + TraceContext.restore(snapshot); + } else { + TraceContext.getOrCreateTraceId(); + } try { - if (captured == null || captured.isEmpty()) { - MDC.clear(); - } else { - MDC.setContextMap(captured); - } - ensureTraceId(); task.run(); } finally { - if (previous == null || previous.isEmpty()) { - MDC.clear(); - } else { - MDC.setContextMap(previous); - } + TraceContext.clear(); } }; } /** - * 包装 Callable:透传提交线程的 MDC,上下文中无 traceId 则补齐;执行完自动清理/还原。 + * 包装 Callable:透传 TraceContext(MDC 由 TraceContext 统一同步) + * 用于非 Spring 管理的线程池(如 ScheduledExecutorService) + * + * @param task Callable 任务 + * @return 包装后的 Callable */ public static Callable wrapWithMdc(Callable task) { - final Map captured = MDC.getCopyOfContextMap(); + final TraceSnapshot snapshot = TraceContext.snapshot(); + return () -> { - Map previous = MDC.getCopyOfContextMap(); + if (snapshot != null) { + TraceContext.restore(snapshot); + } else { + TraceContext.getOrCreateTraceId(); + } try { - if (captured == null || captured.isEmpty()) { - MDC.clear(); - } else { - MDC.setContextMap(captured); - } - ensureTraceId(); return task.call(); } finally { - if (previous == null || previous.isEmpty()) { - MDC.clear(); - } else { - MDC.setContextMap(previous); - } + TraceContext.clear(); } }; } - - /** - * 清除MDC中的TraceId - */ - public static void clearTraceId() { - MDC.remove(TRACE_ID_KEY); - } -} - +} \ No newline at end of file diff --git a/fastbee-common/src/main/java/com/fastbee/common/utils/gateway/CRC16Utils.java b/fastbee-common/src/main/java/com/fastbee/common/utils/gateway/CRC16Utils.java index 1a638536..d9a3f00f 100644 --- a/fastbee-common/src/main/java/com/fastbee/common/utils/gateway/CRC16Utils.java +++ b/fastbee-common/src/main/java/com/fastbee/common/utils/gateway/CRC16Utils.java @@ -133,7 +133,8 @@ public class CRC16Utils { public static void main(String[] args) throws Exception { - String hex = "01030400110018AA3C"; + String hex = "01060004001E"; + hex = hex.replace(" ", ""); byte[] bytes = ByteUtils.hexToByte(hex); String crc = getCRC(bytes); System.out.println(crc); diff --git a/fastbee-common/src/main/java/com/fastbee/common/utils/gateway/CRC8Utils.java b/fastbee-common/src/main/java/com/fastbee/common/utils/gateway/CRC8Utils.java index 48a26cc5..35e98b4c 100644 --- a/fastbee-common/src/main/java/com/fastbee/common/utils/gateway/CRC8Utils.java +++ b/fastbee-common/src/main/java/com/fastbee/common/utils/gateway/CRC8Utils.java @@ -25,12 +25,12 @@ public class CRC8Utils { */ public static byte calcCrc8_E5(byte[] data) { byte crc = 0; - for (int j = 0; j < data.length; j++) { - crc ^= data[j]; + for (byte datum : data) { + crc ^= datum; for (int i = 0; i < 8; i++) { if ((crc & 0x80) != 0) { crc = (byte) ((crc) << 1); - crc ^= 0xE5; + crc ^= (byte) 0xE5; } else { crc = (byte) ((crc) << 1); } diff --git a/fastbee-common/src/main/java/com/fastbee/common/utils/gateway/protocol/ByteUtils.java b/fastbee-common/src/main/java/com/fastbee/common/utils/gateway/protocol/ByteUtils.java index c6ce73e0..1ac13ad5 100644 --- a/fastbee-common/src/main/java/com/fastbee/common/utils/gateway/protocol/ByteUtils.java +++ b/fastbee-common/src/main/java/com/fastbee/common/utils/gateway/protocol/ByteUtils.java @@ -78,8 +78,7 @@ public class ByteUtils { byte[] write = new byte[length]; int index = 0; - for (int i = 0; i < bytes.size(); i++) { - byte[] values = bytes.get(i); + for (byte[] values : bytes) { addBytes(write, values, index); index += values.length; } @@ -312,8 +311,9 @@ public class ByteUtils { if (StringUtils.isBlank(hexStr)) { return null; } - if (hexStr.length() % 2 != 0) {//长度为单数 - hexStr = "0" + hexStr;//前面补0 + // 长度为单数 + if (hexStr.length() % 2 != 0) { + hexStr = "0" + hexStr; // 前面补0 } char[] chars = hexStr.toCharArray(); diff --git a/fastbee-framework/src/main/java/com/fastbee/framework/aspectj/ScheduledTraceAspect.java b/fastbee-framework/src/main/java/com/fastbee/framework/aspectj/ScheduledTraceAspect.java new file mode 100644 index 00000000..e4691c91 --- /dev/null +++ b/fastbee-framework/src/main/java/com/fastbee/framework/aspectj/ScheduledTraceAspect.java @@ -0,0 +1,47 @@ +package com.fastbee.framework.aspectj; + +import com.fastbee.common.trace.TraceContext; +import com.fastbee.common.trace.TraceIdGenerator; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +/** + * @Scheduled 定时任务 TraceId 切面 + * 自动为所有 Spring 定时任务方法生成 TraceId + * + * @author FastBee + */ +@Slf4j +@Aspect +@Component +@Order(1) // 高优先级,确保最先执行 +public class ScheduledTraceAspect { + + /** + * 拦截所有 @Scheduled 方法 + */ + @Around("@annotation(org.springframework.scheduling.annotation.Scheduled)") + public Object aroundScheduled(ProceedingJoinPoint joinPoint) throws Throwable { + String methodName = joinPoint.getSignature().toShortString(); + + // 生成 TraceId + String traceId = TraceIdGenerator.generate(); + TraceContext.setTraceId(traceId); + + log.debug("定时任务开始执行: {}", methodName); + + try { + return joinPoint.proceed(); + } catch (Throwable e) { + log.error("定时任务执行异常: {}", methodName, e); + throw e; + } finally { + log.debug("定时任务执行完成: {}", methodName); + TraceContext.clear(); + } + } +} \ No newline at end of file diff --git a/fastbee-framework/src/main/java/com/fastbee/framework/config/OtaThreadPoolConfig.java b/fastbee-framework/src/main/java/com/fastbee/framework/config/OtaThreadPoolConfig.java index d460de10..6a4859e5 100644 --- a/fastbee-framework/src/main/java/com/fastbee/framework/config/OtaThreadPoolConfig.java +++ b/fastbee-framework/src/main/java/com/fastbee/framework/config/OtaThreadPoolConfig.java @@ -1,6 +1,7 @@ package com.fastbee.framework.config; import com.fastbee.common.constant.FastBeeConstant; +import com.fastbee.common.trace.MdcTaskDecorator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; @@ -30,6 +31,10 @@ public class OtaThreadPoolConfig { executor.setThreadNamePrefix(threadNamePrefix); // 线程池对拒绝任务(无线程可用)的处理策略 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + + // 新增:配置 TaskDecorator,自动传播 TraceId + executor.setTaskDecorator(new MdcTaskDecorator()); + // 进行加载 executor.initialize(); return executor; diff --git a/fastbee-framework/src/main/java/com/fastbee/framework/config/ThreadPoolConfig.java b/fastbee-framework/src/main/java/com/fastbee/framework/config/ThreadPoolConfig.java index cf18f388..6cbeeb7b 100644 --- a/fastbee-framework/src/main/java/com/fastbee/framework/config/ThreadPoolConfig.java +++ b/fastbee-framework/src/main/java/com/fastbee/framework/config/ThreadPoolConfig.java @@ -1,5 +1,6 @@ package com.fastbee.framework.config; +import com.fastbee.common.trace.MdcTaskDecorator; import com.fastbee.common.utils.Threads; import org.apache.commons.lang3.concurrent.BasicThreadFactory; import org.springframework.context.annotation.Bean; @@ -38,6 +39,10 @@ public class ThreadPoolConfig { executor.setKeepAliveSeconds(keepAliveSeconds); // 线程池对拒绝任务(无线程可用)的处理策略 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + + // 新增:配置 TaskDecorator,自动传播 TraceId + executor.setTaskDecorator(new MdcTaskDecorator()); + return executor; } diff --git a/fastbee-framework/src/main/java/com/fastbee/framework/interceptor/TraceIdInterceptor.java b/fastbee-framework/src/main/java/com/fastbee/framework/interceptor/TraceIdInterceptor.java index b892f4ea..af9675c8 100644 --- a/fastbee-framework/src/main/java/com/fastbee/framework/interceptor/TraceIdInterceptor.java +++ b/fastbee-framework/src/main/java/com/fastbee/framework/interceptor/TraceIdInterceptor.java @@ -1,9 +1,9 @@ package com.fastbee.framework.interceptor; -import com.fastbee.common.utils.TraceIdUtils; +import com.fastbee.common.trace.TraceContext; +import com.fastbee.common.trace.TraceIdGenerator; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; - import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -16,24 +16,26 @@ import javax.servlet.http.HttpServletResponse; @Component public class TraceIdInterceptor implements HandlerInterceptor { + private static final String TRACE_ID_HEADER = "X-Trace-Id"; + @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 获取请求URI String requestURI = request.getRequestURI(); // 尝试从请求头中获取TraceId(支持分布式追踪) - String traceId = request.getHeader("X-Trace-Id"); + String traceId = request.getHeader(TRACE_ID_HEADER); // 如果请求头中没有TraceId,则生成一个新的 if (traceId == null || traceId.isEmpty()) { - traceId = TraceIdUtils.generateTraceIdString(); - } else { - // 如果请求头中有TraceId,设置到MDC - TraceIdUtils.setTraceId(traceId); + traceId = TraceIdGenerator.generate(); } + // 设置到 TraceContext(自动同步到 MDC) + TraceContext.setTraceId(traceId); + // 将TraceId放入响应头,方便前端获取 - response.setHeader("X-Trace-Id", traceId); + response.setHeader(TRACE_ID_HEADER, traceId); // 打印请求路径日志 org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TraceIdInterceptor.class); @@ -44,8 +46,8 @@ public class TraceIdInterceptor implements HandlerInterceptor { @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { - // 请求完成后清除MDC中的TraceId,避免内存泄漏 - TraceIdUtils.clearTraceId(); + // 请求完成后清除 TraceContext,避免内存泄漏 + TraceContext.clear(); } } diff --git a/fastbee-framework/src/main/java/com/fastbee/framework/manager/AsyncManager.java b/fastbee-framework/src/main/java/com/fastbee/framework/manager/AsyncManager.java index 7fe96dcd..5d18fff9 100644 --- a/fastbee-framework/src/main/java/com/fastbee/framework/manager/AsyncManager.java +++ b/fastbee-framework/src/main/java/com/fastbee/framework/manager/AsyncManager.java @@ -1,6 +1,7 @@ package com.fastbee.framework.manager; import com.fastbee.common.utils.Threads; +import com.fastbee.common.utils.TraceIdUtils; import com.fastbee.common.utils.spring.SpringUtils; import java.util.TimerTask; @@ -36,10 +37,17 @@ public class AsyncManager { /** * 执行任务 * + *

包装任务以透传 TraceId:{@code scheduledExecutorService} 是裸 + * {@link java.util.concurrent.ScheduledThreadPoolExecutor},不支持 + * {@code setTaskDecorator},因此必须在提交点包装任务本身。 + * {@link TraceIdUtils#wrapWithMdc} 会捕获调用方线程的 TraceContext + * 快照并在 schedule-pool 线程内恢复,使登录/登出/操作日志等 + * 异步落库链路的日志携带同一 TraceId。 + * * @param task 任务 */ public void execute(TimerTask task) { - executor.schedule(task, OPERATE_DELAY_TIME, TimeUnit.MILLISECONDS); + executor.schedule(TraceIdUtils.wrapWithMdc(task), OPERATE_DELAY_TIME, TimeUnit.MILLISECONDS); } /** diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/consumer/DevicePropFetchConsumer.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/consumer/DevicePropFetchConsumer.java index e95e3dc0..3870fb90 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/consumer/DevicePropFetchConsumer.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/consumer/DevicePropFetchConsumer.java @@ -99,7 +99,7 @@ public class DevicePropFetchConsumer { // 不进行格式校验,直接转换数据 byte[] commandData = ByteUtils.convertToHexByteArray(command); switch (serverType) { - //通过mqtt内部客户端 下发指令 + // 通过mqtt内部客户端 下发指令 case MQTT: publish(topic, commandData); log.info("=>MQTT-线程=[{}],设备=[{}],轮询指令:[{}],主题:[{}]", Thread.currentThread().getName(), serialNumber, command, topic); diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/DeviceAbstractQuartzJob.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/DeviceAbstractQuartzJob.java index c24fd805..b3b31169 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/DeviceAbstractQuartzJob.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/DeviceAbstractQuartzJob.java @@ -2,9 +2,10 @@ package com.fastbee.iot.data.job; import com.fastbee.common.constant.Constants; import com.fastbee.common.constant.ScheduleConstants; +import com.fastbee.common.trace.TraceContext; +import com.fastbee.common.trace.TraceIdGenerator; import com.fastbee.common.utils.ExceptionUtil; import com.fastbee.common.utils.StringUtils; -import com.fastbee.common.utils.TraceIdUtils; import com.fastbee.common.utils.bean.BeanUtils; import com.fastbee.common.utils.spring.SpringUtils; import com.fastbee.iot.domain.DeviceJob; @@ -29,7 +30,7 @@ public abstract class DeviceAbstractQuartzJob implements Job { /** * 线程本地变量 */ - private static ThreadLocal threadLocal = new ThreadLocal<>(); + private static final ThreadLocal threadLocal = new ThreadLocal<>(); @Override public void execute(JobExecutionContext context) throws JobExecutionException { @@ -37,9 +38,7 @@ public abstract class DeviceAbstractQuartzJob implements Job { BeanUtils.copyBeanProp(deviceJob, context.getMergedJobDataMap().get(ScheduleConstants.TASK_PROPERTIES)); try { before(context, deviceJob); - if (deviceJob != null) { - doExecute(context, deviceJob); - } + doExecute(context, deviceJob); after(context, deviceJob, null); } catch (Exception e) { log.error("任务执行异常 - :", e); @@ -56,8 +55,8 @@ public abstract class DeviceAbstractQuartzJob implements Job { protected void before(JobExecutionContext context, DeviceJob deviceJob) { threadLocal.set(new Date()); - // 为设备定时任务生成TraceId,使用统一的生成逻辑 - TraceIdUtils.generateTraceId(); + // 为设备定时任务生成 TraceId + TraceContext.setTraceId(TraceIdGenerator.generate()); log.info("设备定时任务开始执行: {} - {}", deviceJob.getJobName(), deviceJob.getJobGroup()); } @@ -91,10 +90,6 @@ public abstract class DeviceAbstractQuartzJob implements Job { startTime = new Date(); } sysJobLog.setStartTime(startTime); - Date stopTime = new Date(); - sysJobLog.setStopTime(stopTime); - long runMs = stopTime.getTime() - startTime.getTime(); - sysJobLog.setJobMessage(sysJobLog.getJobName() + " 总共耗时:" + runMs + "毫秒"); if (e != null) { sysJobLog.setStatus(Constants.FAIL); String errorMsg = StringUtils.substring(ExceptionUtil.getExceptionMessage(e), 0, 2000); @@ -103,13 +98,18 @@ public abstract class DeviceAbstractQuartzJob implements Job { sysJobLog.setStatus(Constants.SUCCESS); } + Date stopTime = new Date(); + sysJobLog.setStopTime(stopTime); + long runMs = stopTime.getTime() - startTime.getTime(); + sysJobLog.setJobMessage(sysJobLog.getJobName() + " 总共耗时:" + runMs + "毫秒"); + // 写入数据库当中 SpringUtils.getBean(ISysJobLogService.class).addJobLog(sysJobLog); - // 清除TraceId,避免内存泄漏 - TraceIdUtils.clearTraceId(); - log.info("设备定时任务执行完成: {} - {}, 耗时: {}ms", deviceJob.getJobName(), deviceJob.getJobGroup(), runMs); + + // 清除 TraceContext + TraceContext.clear(); } /** diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/DeviceDisallowConcurrentExecution.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/DeviceDisallowConcurrentExecution.java index 62c5bb74..e45a4196 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/DeviceDisallowConcurrentExecution.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/DeviceDisallowConcurrentExecution.java @@ -11,6 +11,7 @@ import org.quartz.JobExecutionContext; */ @DisallowConcurrentExecution public class DeviceDisallowConcurrentExecution extends DeviceAbstractQuartzJob { + @Override protected void doExecute(JobExecutionContext context, DeviceJob deviceJob) throws Exception { DeviceJobInvoke.invokeMethod(deviceJob); diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/DeviceJobExecution.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/DeviceJobExecution.java index 87788c0b..5f9af7c2 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/DeviceJobExecution.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/DeviceJobExecution.java @@ -9,6 +9,7 @@ import org.quartz.JobExecutionContext; * @author sumuxi, 2026-04-16 */ public class DeviceJobExecution extends DeviceAbstractQuartzJob { + @Override protected void doExecute(JobExecutionContext context, DeviceJob deviceJob) throws Exception { DeviceJobInvoke.invokeMethod(deviceJob); diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/DeviceJobInvoke.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/DeviceJobInvoke.java index 7f0f4332..cff7e810 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/DeviceJobInvoke.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/DeviceJobInvoke.java @@ -29,13 +29,13 @@ public class DeviceJobInvoke { /** * 获取消息推送接口 */ - private static IMqttMessagePublish messagePublish = SpringUtils.getBean(IMqttMessagePublish.class); + private static final IMqttMessagePublish messagePublish = SpringUtils.getBean(IMqttMessagePublish.class); - private static SceneMapper sceneMapper = SpringUtils.getBean(SceneMapper.class); + private static final SceneMapper sceneMapper = SpringUtils.getBean(SceneMapper.class); - private static FlowLogExecutor flowExecutor = SpringUtils.getBean(FlowLogExecutor.class); + private static final FlowLogExecutor flowExecutor = SpringUtils.getBean(FlowLogExecutor.class); - private static IDataHandler dataHandler = SpringUtils.getBean(IDataHandler.class); + private static final IDataHandler dataHandler = SpringUtils.getBean(IDataHandler.class); /** * 执行方法 @@ -44,45 +44,45 @@ public class DeviceJobInvoke { */ public static void invokeMethod(DeviceJob deviceJob) throws Exception { if (deviceJob.getJobType() == 1) { - log.info("------------------------1、执行设备定时任务-----------------------------"); + log.info("1、[执行设备定时任务]"); List actions = JSON.parseArray(deviceJob.getActions(), Action.class); - List propertys = new ArrayList<>(); + List properties = new ArrayList<>(); List functions = new ArrayList<>(); - for (int i = 0; i < actions.size(); i++) { + for (Action action : actions) { ThingsModelSimpleItem model = new ThingsModelSimpleItem(); - model.setId(actions.get(i).getId()); - model.setValue(actions.get(i).getValue()); + model.setId(action.getId()); + model.setValue(action.getValue()); model.setRemark("设备定时"); - if (actions.get(i).getType() == 1) { - propertys.add(model); - } else if (actions.get(i).getType() == 2) { + if (action.getType() == 1) { + properties.add(model); + } else if (action.getType() == 2) { functions.add(model); } } // 发布属性 - if (propertys.size() > 0) { - messagePublish.publishProperty(deviceJob.getProductId(), deviceJob.getSerialNumber(), propertys, 0); + if (!properties.isEmpty()) { + messagePublish.publishProperty(deviceJob.getProductId(), deviceJob.getSerialNumber(), properties, 0); } // 发布功能 - if (functions.size() > 0) { + if (!functions.isEmpty()) { messagePublish.publishFunction(deviceJob.getProductId(), deviceJob.getSerialNumber(), functions, 0); } } else if (deviceJob.getJobType() == 3) { - log.info("------------------3、[定时执行场景联动]---------------------"); + log.info("3、[定时执行场景联动]"); Scene scene = sceneMapper.selectSceneBySceneId(deviceJob.getSceneId()); // 执行场景规则,异步非阻塞 SceneContext context = new SceneContext("", 0L, 0, null); String requestId = "scene/" + scene.getChainName(); flowExecutor.execute2FutureWithRid(scene.getChainName(), null, requestId, context); } else if (4 == deviceJob.getJobType()) { - log.info("------------------4、[定时执行场景运算型变量]---------------------"); + log.info("4、[定时执行场景运算型变量]"); String s = dataHandler.calculateSceneModelTagValue(deviceJob.getDatasourceId()); if (StringUtils.isEmpty(s)) { - log.info("------------------[定时执行场景运算型变量失败:+{}]---------------------", s); + log.info("[定时执行场景运算型变量失败:+{}]", s); } } else if (5 == deviceJob.getJobType()) { - log.info("----------------5、执行modbus轮训指令----------------------"); + log.info("5、[执行Modbus轮训指令]"); ModbusPollJob job = new ModbusPollJob(); job.setType(2); job.setDeviceJob(deviceJob); diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/SyncDeviceStatusJob.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/SyncDeviceStatusJob.java index 6f113113..5b299e10 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/SyncDeviceStatusJob.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/SyncDeviceStatusJob.java @@ -38,7 +38,7 @@ public class SyncDeviceStatusJob { */ public void syncDeviceStatus() { if (enabled) { - //获取所有已激活并不是禁用的设备 + // 获取所有已激活并不是禁用的设备 List deviceStatusVOList = deviceService.selectDeviceActive(); if (!CollectionUtils.isEmpty(deviceStatusVOList)) { for (DeviceStatusVO statusVO : deviceStatusVOList) { diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceOtherListen.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceOtherListen.java index 0b84e340..8679a1af 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceOtherListen.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceOtherListen.java @@ -2,6 +2,7 @@ package com.fastbee.iot.data.listen; import com.fastbee.common.constant.FastBeeConstant; import com.fastbee.common.core.mq.DeviceReportBo; +import com.fastbee.common.trace.TraceContext; import com.fastbee.iot.data.consumer.DeviceOtherMsgConsumer; import com.fastbee.mq.queue.DeviceOtherQueue; import lombok.extern.slf4j.Slf4j; @@ -26,7 +27,15 @@ public class DeviceOtherListen { while (true) { try { DeviceReportBo reportBo = DeviceOtherQueue.take(); - otherMsgConsumer.consume(reportBo); + + // 恢复 TraceId + TraceContext.ensureTraceId(reportBo.getTraceId()); + + try { + otherMsgConsumer.consume(reportBo); + } finally { + TraceContext.clear(); + } } catch (Exception e) { log.error("=>emq数据转发异常"); } diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DevicePropFetchListen.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DevicePropFetchListen.java index 167a9b41..06388c3c 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DevicePropFetchListen.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DevicePropFetchListen.java @@ -1,6 +1,7 @@ package com.fastbee.iot.data.listen; import com.fastbee.common.constant.FastBeeConstant; +import com.fastbee.common.trace.TraceContext; import com.fastbee.iot.data.consumer.DevicePropFetchConsumer; import com.fastbee.iot.model.modbus.ModbusPollJob; import com.fastbee.mq.queue.DevicePropFetchQueue; @@ -27,9 +28,20 @@ public class DevicePropFetchListen { while (true) { try { ModbusPollJob take = DevicePropFetchQueue.take(); - devicePropFetchConsumer.consume(take); + + // 恢复 TraceId + TraceContext.ensureTraceId(take.getTraceId()); + + try { + devicePropFetchConsumer.consume(take); + } finally { + TraceContext.clear(); + } } catch (Exception e) { log.error("=>设备属性获取异常", e); + } finally { + // 确保任何情况下都清理 + TraceContext.clear(); } } } diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceReplyListen.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceReplyListen.java index fee5854f..9109c799 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceReplyListen.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceReplyListen.java @@ -2,6 +2,7 @@ package com.fastbee.iot.data.listen; import com.fastbee.common.constant.FastBeeConstant; import com.fastbee.common.core.mq.DeviceReportBo; +import com.fastbee.common.trace.TraceContext; import com.fastbee.iot.data.consumer.DeviceReplyMsgConsumer; import com.fastbee.mq.queue.DeviceReplyQueue; import lombok.extern.slf4j.Slf4j; @@ -27,10 +28,21 @@ public class DeviceReplyListen { try { /*读队列消息*/ DeviceReportBo reportBo = DeviceReplyQueue.take(); - /*处理消息*/ - deviceReplyMsgHandler.consume(reportBo); + + // 恢复 TraceId + TraceContext.ensureTraceId(reportBo.getTraceId()); + + try { + /*处理消息*/ + deviceReplyMsgHandler.consume(reportBo); + } finally { + TraceContext.clear(); + } } catch (Exception e) { log.error("=>设备回调消息监听异常", e); + } finally { + // 确保任何情况下都清理上下文 + TraceContext.clear(); } } } diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceReportListen.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceReportListen.java index caad7495..fa28bb03 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceReportListen.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceReportListen.java @@ -2,6 +2,7 @@ package com.fastbee.iot.data.listen; import com.fastbee.common.constant.FastBeeConstant; import com.fastbee.common.core.mq.DeviceReportBo; +import com.fastbee.common.trace.TraceContext; import com.fastbee.iot.data.consumer.DeviceReportMsgConsumer; import com.fastbee.mq.queue.DeviceReportQueue; import lombok.extern.slf4j.Slf4j; @@ -27,10 +28,21 @@ public class DeviceReportListen { try { /*取出数据*/ DeviceReportBo reportBo = DeviceReportQueue.take(); - /*处理数据*/ - reportMsgConsumer.consume(reportBo); + + // 恢复 TraceId + TraceContext.ensureTraceId(reportBo.getTraceId()); + + try { + /*处理数据*/ + reportMsgConsumer.consume(reportBo); + } finally { + TraceContext.clear(); + } } catch (Exception e) { log.error("=>设备上报数据监听异常", e); + } finally { + // 确保任何情况下都清理上下文 + TraceContext.clear(); } } } diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceStatusListen.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceStatusListen.java index 6252c0cb..07bdf6b9 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceStatusListen.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceStatusListen.java @@ -2,6 +2,7 @@ package com.fastbee.iot.data.listen; import com.fastbee.common.constant.FastBeeConstant; import com.fastbee.common.core.mq.DeviceStatusBo; +import com.fastbee.common.trace.TraceContext; import com.fastbee.iot.data.consumer.DeviceStatusConsumer; import com.fastbee.mq.queue.DeviceStatusQueue; import lombok.extern.slf4j.Slf4j; @@ -26,10 +27,20 @@ public class DeviceStatusListen { try { while (true) { DeviceStatusBo status = DeviceStatusQueue.take(); - deviceStatusConsumer.consume(status); + + // 恢复 TraceId + TraceContext.ensureTraceId(status.getTraceId()); + + try { + deviceStatusConsumer.consume(status); + } finally { + TraceContext.clear(); + } } } catch (Exception e) { log.error("设备状态监听错误", e); + } finally { + TraceContext.clear(); } } diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceTestListen.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceTestListen.java index 1b4de8a7..c3cd81e8 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceTestListen.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceTestListen.java @@ -2,6 +2,7 @@ package com.fastbee.iot.data.listen; import com.fastbee.common.constant.FastBeeConstant; import com.fastbee.common.core.mq.DeviceTestReportBo; +import com.fastbee.common.trace.TraceContext; import com.fastbee.iot.data.consumer.DeviceTestConsumer; import com.fastbee.mq.queue.DeviceTestQueue; import lombok.extern.slf4j.Slf4j; @@ -26,9 +27,19 @@ public class DeviceTestListen { try { DeviceTestReportBo take = DeviceTestQueue.take(); - deviceTestConsumer.consume(take); + // 恢复 TraceId + TraceContext.ensureTraceId(take.getTraceId()); + + try { + deviceTestConsumer.consume(take); + } finally { + TraceContext.clear(); + } } catch (Exception e) { log.error("=>emq数据转发异常"); + } finally { + // 确保任何情况下都清理上下文 + TraceContext.clear(); } } } diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/FunctionInvokeListen.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/FunctionInvokeListen.java index a631bd22..aa4473ae 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/FunctionInvokeListen.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/FunctionInvokeListen.java @@ -2,6 +2,7 @@ package com.fastbee.iot.data.listen; import com.fastbee.common.constant.FastBeeConstant; import com.fastbee.common.core.mq.MQSendMessageBo; +import com.fastbee.common.trace.TraceContext; import com.fastbee.iot.data.consumer.FunctionInvokeConsumer; import com.fastbee.mq.queue.FunctionInvokeQueue; import lombok.extern.slf4j.Slf4j; @@ -26,9 +27,20 @@ public class FunctionInvokeListen { while (true) { try { MQSendMessageBo sendBo = FunctionInvokeQueue.take(); - functionInvokeConsumer.handler(sendBo); + + // 恢复 TraceId + TraceContext.ensureTraceId(sendBo.getTraceId()); + + try { + functionInvokeConsumer.handler(sendBo); + } finally { + TraceContext.clear(); + } } catch (Exception e) { log.error("=>下发服务消费异常", e); + } finally { + // 确保任何情况下都清理上下文 + TraceContext.clear(); } } } diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/UpGradeListener.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/UpGradeListener.java index 9d6e0a26..aa2086c9 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/UpGradeListener.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/UpGradeListener.java @@ -4,6 +4,7 @@ package com.fastbee.iot.data.listen; import com.fastbee.common.constant.FastBeeConstant; import com.fastbee.common.core.domain.entity.SysMenu; import com.fastbee.common.core.mq.ota.OtaUpgradeDelayTask; +import com.fastbee.common.trace.TraceContext; import com.fastbee.common.utils.StringUtils; import com.fastbee.iot.data.service.IOtaUpgradeService; import com.fastbee.mq.queue.DelayUpgradeQueue; @@ -48,11 +49,21 @@ public class UpGradeListener implements ApplicationRunner { OtaUpgradeDelayTask task = DelayUpgradeQueue.task(); if (StringUtils.isNotNull(task)) { Date startTime = task.getStartTime(); - otaUpgradeService.upgrade(task); - log.info("=>开始OTA升级时间{}", startTime); + + // 恢复 TraceId + TraceContext.ensureTraceId(task.getTraceId()); + + try { + otaUpgradeService.upgrade(task); + log.info("=>开始OTA升级时间{}", startTime); + } finally { + TraceContext.clear(); + } } } catch (Exception e) { log.error("=>OTA升级监听异常", e); + } finally { + TraceContext.clear(); } } } diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/UpgradeListen.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/UpgradeListen.java index e7ea9a22..eb59af71 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/UpgradeListen.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/UpgradeListen.java @@ -2,6 +2,7 @@ package com.fastbee.iot.data.listen; import com.fastbee.common.constant.FastBeeConstant; import com.fastbee.common.core.mq.ota.OtaUpgradeBo; +import com.fastbee.common.trace.TraceContext; import com.fastbee.iot.data.service.IMqttMessagePublish; import com.fastbee.mq.queue.OtaUpgradeQueue; import lombok.extern.slf4j.Slf4j; @@ -28,10 +29,20 @@ public class UpgradeListen { try { /*获取队列中的OTA升级消息*/ OtaUpgradeBo upgradeBo = OtaUpgradeQueue.take(); - // OTA升级处理 - functionSendService.upGradeOTA(upgradeBo); + + // 恢复 TraceId + TraceContext.ensureTraceId(upgradeBo.getTraceId()); + + try { + // OTA升级处理 + functionSendService.upGradeOTA(upgradeBo); + } finally { + TraceContext.clear(); + } } catch (Exception e) { log.error("->OTA消息监听异常", e); + } finally { + TraceContext.clear(); } } } diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/service/impl/DeviceJobServiceImpl.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/service/impl/DeviceJobServiceImpl.java index 687a9dbb..fe52ccfb 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/service/impl/DeviceJobServiceImpl.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/service/impl/DeviceJobServiceImpl.java @@ -47,6 +47,10 @@ public class DeviceJobServiceImpl implements IDeviceJobService { @PostConstruct public void init() throws SchedulerException, TaskException { scheduler.clear(); + // 打印实际类型 +// log.info("Scheduler实际类型: {}", scheduler.getClass().getName()); +// log.info("Scheduler元数据: {}", scheduler.getMetaData().getSummary()); +// log.info("Scheduler class: {}", scheduler.getClass()); // 设备定时任务 List jobList = jobMapper.selectJobAll(); for (DeviceJob deviceJob : jobList) { diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/service/impl/DeviceMessageServiceImpl.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/service/impl/DeviceMessageServiceImpl.java index 009c6e8b..20f45d25 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/service/impl/DeviceMessageServiceImpl.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/service/impl/DeviceMessageServiceImpl.java @@ -222,7 +222,7 @@ public class DeviceMessageServiceImpl implements IDeviceMessageService { /** * 变量读取 * - * @param readVO + * @param readVO 前端入参 */ @Override public void readVariableValue(VariableReadVO readVO) { @@ -233,7 +233,7 @@ public class DeviceMessageServiceImpl implements IDeviceMessageService { deviceAndProtocol.getProtocolCode().equals(FastBeeConstant.PROTOCOL.ModbusTcp))) throw new ServiceException("非modbus协议请先配置主动采集协议"); Long productId = deviceAndProtocol.getProductId(); - //如果是子设备,则获取网关子设备的产品id和设备编号 + // 如果是子设备,则获取网关子设备的产品id和设备编号 Integer deviceType = deviceAndProtocol.getDeviceType(); if (deviceType == DeviceType.SUB_GATEWAY.getCode()) { serialNumber = deviceAndProtocol.getGwSerialNumber(); @@ -242,7 +242,7 @@ public class DeviceMessageServiceImpl implements IDeviceMessageService { Integer type = readVO.getType(); type = Objects.isNull(type) ? 1 : type; if (type == 1) { - //单个变量获取 + // 单个变量获取 String identifier = readVO.getIdentifier(); ThingsModelValueItem thingModels = itslCache.getSingleThingModels(deviceAndProtocol.getProductId(), identifier); ModbusConfig config = thingModels.getConfig(); @@ -275,7 +275,7 @@ public class DeviceMessageServiceImpl implements IDeviceMessageService { job.setType(1); MessageProducer.sendPropFetch(job); } else { - //读取当前设备的所有变量,这里读取所有的,判断传递的设备是网关设备、网关子设备、直连设备 + // 读取当前设备的所有变量,这里读取所有的,判断传递的设备是网关设备、网关子设备、直连设备 DeviceType code = DeviceType.transfer(deviceType); List modbusJobList = modbusJobService.selectDevicesJobByDeviceType(code, readVO.getSerialNumber()); List commandList = modbusJobList.stream().map(ModbusJob::getCommand).collect(Collectors.toList()); diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/service/impl/FirmwareTaskServiceImpl.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/service/impl/FirmwareTaskServiceImpl.java index abe92afd..e522d24a 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/service/impl/FirmwareTaskServiceImpl.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/service/impl/FirmwareTaskServiceImpl.java @@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.fastbee.common.core.mq.ota.OtaUpgradeDelayTask; import com.fastbee.common.enums.OTAUpgrade; import com.fastbee.common.exception.ServiceException; +import com.fastbee.common.trace.TraceContext; import com.fastbee.common.utils.DateUtils; import com.fastbee.common.utils.MessageUtils; import com.fastbee.common.utils.StringUtils; @@ -140,7 +141,9 @@ public class FirmwareTaskServiceImpl extends ServiceImpl + + + DELETE FROM notify_log + WHERE create_time < DATE_SUB(NOW(), INTERVAL #{days} DAY) + diff --git a/fastbee-open-api/src/main/java/com/fastbee/controller/platform/PlatformDeviceMappingController.java b/fastbee-open-api/src/main/java/com/fastbee/controller/platform/PlatformDeviceMappingController.java index ed857816..23dd647f 100644 --- a/fastbee-open-api/src/main/java/com/fastbee/controller/platform/PlatformDeviceMappingController.java +++ b/fastbee-open-api/src/main/java/com/fastbee/controller/platform/PlatformDeviceMappingController.java @@ -73,8 +73,23 @@ public class PlatformDeviceMappingController extends BaseController { return success("入网成功"); } + @PreAuthorize("@ss.hasAnyPermi('platform:deviceMapping:query,platform:deviceMapping:list')") + @GetMapping("/static-level/reference-points") + @ApiOperation("查询已配置的静力水准基准点(跨网关基准点下拉框数据源)") + public AjaxResult listStaticLevelReferencePoints(@RequestParam Long platformId, + @RequestParam(required = false) Long excludeMappingId) { + return success(mappingService.listConfiguredReferencePoints(platformId, excludeMappingId)); + } + // ==================== 子映射管理 ==================== + @PreAuthorize("@ss.hasAnyPermi('platform:deviceMapping:query,platform:deviceMapping:list')") + @GetMapping("/sub-mappings/batch") + @ApiOperation("批量查询多个设备绑定的子设备映射(列表页展开行一次拉取,避免 N 次请求)") + public AjaxResult listSubMappingsBatch(@RequestParam List parentIds) { + return success(subMappingService.selectByParentIds(parentIds)); + } + @PreAuthorize("@ss.hasAnyPermi('platform:deviceMapping:query,platform:deviceMapping:list')") @GetMapping("/{id}/sub-mappings") @ApiOperation("查询子设备映射列表") diff --git a/fastbee-open-api/src/main/java/com/fastbee/controller/system/LogCleanController.java b/fastbee-open-api/src/main/java/com/fastbee/controller/system/LogCleanController.java new file mode 100644 index 00000000..32804e54 --- /dev/null +++ b/fastbee-open-api/src/main/java/com/fastbee/controller/system/LogCleanController.java @@ -0,0 +1,102 @@ +package com.fastbee.controller.system; + +import com.fastbee.common.annotation.Log; +import com.fastbee.common.core.controller.BaseController; +import com.fastbee.common.core.domain.AjaxResult; +import com.fastbee.common.enums.BusinessType; +import com.fastbee.iot.platform.service.IPlatformPushLogService; +import com.fastbee.iot.service.IAlertLogService; +import com.fastbee.iot.service.IEventLogService; +import com.fastbee.notify.service.INotifyLogService; +import com.fastbee.quartz.service.ISysJobLogService; +import com.fastbee.system.service.ISysLogininforService; +import com.fastbee.system.service.ISysOperLogService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 日志清理Controller + * + * @author fastbee + */ +@Api(tags = "系统管理:日志清理") +@RestController +@RequestMapping("/system/log") +public class LogCleanController extends BaseController { + + @Autowired + private IEventLogService eventLogService; + + @Autowired + private ISysOperLogService operLogService; + + @Autowired + private ISysLogininforService logininforService; + + @Autowired + private IAlertLogService alertLogService; + + @Autowired + private ISysJobLogService jobLogService; + + @Autowired + private INotifyLogService notifyLogService; + + @Autowired + private IPlatformPushLogService platformPushLogService; + + /** + * 清理日志数据 + * + * @param days 保留天数,默认15天 + * @return 清理结果 + */ + @ApiOperation("清理日志数据") + @PreAuthorize("@ss.hasRole('admin')") + @Log(title = "日志清理", businessType = BusinessType.CLEAN) + @PostMapping("/clean") + public AjaxResult cleanLogs( + @ApiParam("保留天数,默认15天") + @RequestParam(value = "days", defaultValue = "15") Integer days) { + try { + logger.info("手动触发日志清理,保留最近 {} 天的记录", days); + + if (days < 1) { + return error("保留天数必须大于等于1天"); + } + + // 清理 IoT 事件日志 + eventLogService.cleanEventLog(days); + + // 清理系统操作日志 + operLogService.cleanOperLog(days); + + // 清理登录日志 + logininforService.cleanLogininfor(days); + + // 清理设备告警日志 + alertLogService.cleanAlertLog(days); + + // 清理定时任务日志 + jobLogService.cleanJobLog(days); + + // 清理通知日志 + notifyLogService.cleanNotifyLog(days); + + platformPushLogService.cleanPushLog(days); + + logger.info("日志清理完成,保留最近 {} 天的记录", days); + return success(String.format("日志清理完成,已删除 %d 天前的历史数据", days)); + } catch (Exception e) { + logger.error("日志清理失败", e); + return error("日志清理失败:" + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/fastbee-open-api/src/main/java/com/fastbee/controller/tool/ToolController.java b/fastbee-open-api/src/main/java/com/fastbee/controller/tool/ToolController.java index 5254dd68..337dd926 100644 --- a/fastbee-open-api/src/main/java/com/fastbee/controller/tool/ToolController.java +++ b/fastbee-open-api/src/main/java/com/fastbee/controller/tool/ToolController.java @@ -254,20 +254,19 @@ public class ToolController extends BaseController { } } catch (Exception ex) { - ex.printStackTrace(); log.error("发生错误:{}", ex.getMessage(), ex); } } @ApiOperation("mqtt钩子处理") @PostMapping("/mqtt/webhookv5") - public void webHookProcessv5(@RequestBody JSONObject json) { + public void webHookProcessV5(@RequestBody JSONObject json) { try { String clientid = json.getString("clientid"); String event = json.getString("event"); String peername = json.getString("peername"); // 类似 127.0.0.1:8812 String ipAddress = peername; - if (peername.indexOf(":") != -1) { + if (peername.contains(":")) { ipAddress = peername.substring(0, peername.indexOf(":")); } log.info("webhook:{},clientid:{},peername:{}", event, clientid, peername); @@ -310,18 +309,17 @@ public class ToolController extends BaseController { // 影子模式,发布属性和功能 if (device.getIsShadow() == 1) { ThingsModelShadow shadow = deviceService.getDeviceShadowThingsModel(device); - if (shadow.getProperties().size() > 0) { + if (!shadow.getProperties().isEmpty()) { messagePublish.publishProperty(device.getProductId(), device.getSerialNumber(), shadow.getProperties(), 3); } - if (shadow.getFunctions().size() > 0) { + if (!shadow.getFunctions().isEmpty()) { messagePublish.publishFunction(device.getProductId(), device.getSerialNumber(), shadow.getFunctions(), 3); } } } } catch (Exception ex) { - ex.printStackTrace(); - log.error("发生错误:" + ex.getMessage()); + log.error("发生错误:{}", ex.getMessage()); } } diff --git a/fastbee-plugs/fastbee-quartz/src/main/java/com/fastbee/quartz/controller/SysJobController.java b/fastbee-plugs/fastbee-quartz/src/main/java/com/fastbee/quartz/controller/SysJobController.java index b6f01656..bf0ddd6e 100644 --- a/fastbee-plugs/fastbee-quartz/src/main/java/com/fastbee/quartz/controller/SysJobController.java +++ b/fastbee-plugs/fastbee-quartz/src/main/java/com/fastbee/quartz/controller/SysJobController.java @@ -83,9 +83,9 @@ public class SysJobController extends BaseController { return error(StringUtils.format(MessageUtils.message("job.add.failed.cron.not.valid"), job.getJobName())); } else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_RMI)) { return error(StringUtils.format(MessageUtils.message("job.add.failed.rmi.not.valid"), job.getJobName())); - } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[]{Constants.LOOKUP_LDAP, Constants.LOOKUP_LDAPS})) { + } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_LDAP, Constants.LOOKUP_LDAPS)) { return error(StringUtils.format(MessageUtils.message("job.add.failed.ldap.not.valid"), job.getJobName())); - } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[]{Constants.HTTP, Constants.HTTPS})) { + } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), Constants.HTTP, Constants.HTTPS)) { return error(StringUtils.format(MessageUtils.message("job.add.failed.http.not.valid"), job.getJobName())); } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), Constants.JOB_ERROR_STR)) { return error(StringUtils.format(MessageUtils.message("job.add.failed.string.error"), job.getJobName())); @@ -108,9 +108,9 @@ public class SysJobController extends BaseController { return error(StringUtils.format(MessageUtils.message("job.update.failed.cron.not.valid"), job.getJobName())); } else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_RMI)) { return error(StringUtils.format(MessageUtils.message("job.update.failed.rmi.not.valid"), job.getJobName())); - } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[]{Constants.LOOKUP_LDAP, Constants.LOOKUP_LDAPS})) { + } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_LDAP, Constants.LOOKUP_LDAPS)) { return error(StringUtils.format(MessageUtils.message("job.update.failed.ldap.not.valid"), job.getJobName())); - } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[]{Constants.HTTP, Constants.HTTPS})) { + } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), Constants.HTTP, Constants.HTTPS)) { return error(StringUtils.format(MessageUtils.message("job.update.failed.http.not.valid"), job.getJobName())); } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), Constants.JOB_ERROR_STR)) { return error(StringUtils.format(MessageUtils.message("job.update.failed.string.error"), job.getJobName())); diff --git a/fastbee-plugs/fastbee-quartz/src/main/java/com/fastbee/quartz/mapper/SysJobLogMapper.java b/fastbee-plugs/fastbee-quartz/src/main/java/com/fastbee/quartz/mapper/SysJobLogMapper.java index 8b35eefe..cf40842d 100644 --- a/fastbee-plugs/fastbee-quartz/src/main/java/com/fastbee/quartz/mapper/SysJobLogMapper.java +++ b/fastbee-plugs/fastbee-quartz/src/main/java/com/fastbee/quartz/mapper/SysJobLogMapper.java @@ -61,4 +61,12 @@ public interface SysJobLogMapper { * 清空任务日志 */ public void cleanJobLog(); + + /** + * 删除指定天数前的任务日志 + * + * @param days 保留天数 + * @return 删除记录数 + */ + public int deleteJobLogBeforeDays(int days); } diff --git a/fastbee-plugs/fastbee-quartz/src/main/java/com/fastbee/quartz/service/ISysJobLogService.java b/fastbee-plugs/fastbee-quartz/src/main/java/com/fastbee/quartz/service/ISysJobLogService.java index 50893074..a0bea432 100644 --- a/fastbee-plugs/fastbee-quartz/src/main/java/com/fastbee/quartz/service/ISysJobLogService.java +++ b/fastbee-plugs/fastbee-quartz/src/main/java/com/fastbee/quartz/service/ISysJobLogService.java @@ -53,4 +53,11 @@ public interface ISysJobLogService { * 清空任务日志 */ public void cleanJobLog(); + + /** + * 清理指定天数前的任务日志 + * + * @param days 保留天数 + */ + public void cleanJobLog(int days); } diff --git a/fastbee-plugs/fastbee-quartz/src/main/java/com/fastbee/quartz/service/impl/SysJobLogServiceImpl.java b/fastbee-plugs/fastbee-quartz/src/main/java/com/fastbee/quartz/service/impl/SysJobLogServiceImpl.java index a5dfbe5e..8ba0f9dc 100644 --- a/fastbee-plugs/fastbee-quartz/src/main/java/com/fastbee/quartz/service/impl/SysJobLogServiceImpl.java +++ b/fastbee-plugs/fastbee-quartz/src/main/java/com/fastbee/quartz/service/impl/SysJobLogServiceImpl.java @@ -78,4 +78,14 @@ public class SysJobLogServiceImpl implements ISysJobLogService { public void cleanJobLog() { jobLogMapper.cleanJobLog(); } + + /** + * 清理指定天数前的任务日志 + * + * @param days 保留天数 + */ + @Override + public void cleanJobLog(int days) { + jobLogMapper.deleteJobLogBeforeDays(days); + } } diff --git a/fastbee-plugs/fastbee-quartz/src/main/java/com/fastbee/quartz/util/AbstractQuartzJob.java b/fastbee-plugs/fastbee-quartz/src/main/java/com/fastbee/quartz/util/AbstractQuartzJob.java index 6789a91b..30c6f2fe 100644 --- a/fastbee-plugs/fastbee-quartz/src/main/java/com/fastbee/quartz/util/AbstractQuartzJob.java +++ b/fastbee-plugs/fastbee-quartz/src/main/java/com/fastbee/quartz/util/AbstractQuartzJob.java @@ -2,9 +2,10 @@ package com.fastbee.quartz.util; import com.fastbee.common.constant.Constants; import com.fastbee.common.constant.ScheduleConstants; +import com.fastbee.common.trace.TraceContext; +import com.fastbee.common.trace.TraceIdGenerator; import com.fastbee.common.utils.ExceptionUtil; import com.fastbee.common.utils.StringUtils; -import com.fastbee.common.utils.TraceIdUtils; import com.fastbee.common.utils.bean.BeanUtils; import com.fastbee.common.utils.spring.SpringUtils; import com.fastbee.quartz.domain.SysJob; @@ -29,7 +30,7 @@ public abstract class AbstractQuartzJob implements Job { /** * 线程本地变量 */ - private static ThreadLocal threadLocal = new ThreadLocal<>(); + private static final ThreadLocal threadLocal = new ThreadLocal<>(); @Override public void execute(JobExecutionContext context) throws JobExecutionException { @@ -37,9 +38,7 @@ public abstract class AbstractQuartzJob implements Job { BeanUtils.copyBeanProp(sysJob, context.getMergedJobDataMap().get(ScheduleConstants.TASK_PROPERTIES)); try { before(context, sysJob); - if (sysJob != null) { - doExecute(context, sysJob); - } + doExecute(context, sysJob); after(context, sysJob, null); } catch (Exception e) { log.error("任务执行异常 - :", e); @@ -56,8 +55,8 @@ public abstract class AbstractQuartzJob implements Job { protected void before(JobExecutionContext context, SysJob sysJob) { threadLocal.set(new Date()); - // 为定时任务生成TraceId,使用统一的生成逻辑 - TraceIdUtils.generateTraceId(); + // 为定时任务生成 TraceId + TraceContext.setTraceId(TraceIdGenerator.generate()); log.info("定时任务开始执行: {} - {}", sysJob.getJobName(), sysJob.getJobGroup()); } @@ -77,9 +76,6 @@ public abstract class AbstractQuartzJob implements Job { sysJobLog.setJobGroup(sysJob.getJobGroup()); sysJobLog.setInvokeTarget(sysJob.getInvokeTarget()); sysJobLog.setStartTime(startTime); - sysJobLog.setStopTime(new Date()); - long runMs = sysJobLog.getStopTime().getTime() - sysJobLog.getStartTime().getTime(); - sysJobLog.setJobMessage(sysJobLog.getJobName() + " 总共耗时:" + runMs + "毫秒"); if (e != null) { sysJobLog.setStatus(Constants.FAIL); String errorMsg = StringUtils.substring(ExceptionUtil.getExceptionMessage(e), 0, 2000); @@ -87,14 +83,17 @@ public abstract class AbstractQuartzJob implements Job { } else { sysJobLog.setStatus(Constants.SUCCESS); } - + sysJobLog.setStopTime(new Date()); + long runMs = sysJobLog.getStopTime().getTime() - sysJobLog.getStartTime().getTime(); + sysJobLog.setJobMessage(sysJobLog.getJobName() + " 总共耗时:" + runMs + "毫秒"); // 写入数据库当中 SpringUtils.getBean(ISysJobLogService.class).addJobLog(sysJobLog); - // 清除TraceId,避免内存泄漏 - TraceIdUtils.clearTraceId(); - log.info("定时任务执行完成: {} - {}, 耗时: {}ms", sysJob.getJobName(), sysJob.getJobGroup(), runMs); + + // 清除 TraceContext + TraceContext.clear(); + } /** diff --git a/fastbee-plugs/fastbee-quartz/src/main/java/com/fastbee/quartz/util/JobInvokeUtil.java b/fastbee-plugs/fastbee-quartz/src/main/java/com/fastbee/quartz/util/JobInvokeUtil.java index 1e78616a..02a36297 100644 --- a/fastbee-plugs/fastbee-quartz/src/main/java/com/fastbee/quartz/util/JobInvokeUtil.java +++ b/fastbee-plugs/fastbee-quartz/src/main/java/com/fastbee/quartz/util/JobInvokeUtil.java @@ -99,8 +99,8 @@ public class JobInvokeUtil { } String[] methodParams = methodStr.split(",(?=([^\"']*[\"'][^\"']*[\"'])*[^\"']*$)"); List classs = new LinkedList<>(); - for (int i = 0; i < methodParams.length; i++) { - String str = StringUtils.trimToEmpty(methodParams[i]); + for (String methodParam : methodParams) { + String str = StringUtils.trimToEmpty(methodParam); // String字符串类型,以'或"开头 if (StringUtils.startsWithAny(str, "'", "\"")) { classs.add(new Object[]{StringUtils.substring(str, 1, str.length() - 1), String.class}); @@ -151,7 +151,7 @@ public class JobInvokeUtil { Object[] classs = new Object[methodParams.size()]; int index = 0; for (Object[] os : methodParams) { - classs[index] = (Object) os[0]; + classs[index] = os[0]; index++; } return classs; diff --git a/fastbee-plugs/fastbee-quartz/src/main/resources/mapper/quartz/SysJobLogMapper.xml b/fastbee-plugs/fastbee-quartz/src/main/resources/mapper/quartz/SysJobLogMapper.xml index 1a3e279e..9aafb989 100644 --- a/fastbee-plugs/fastbee-quartz/src/main/resources/mapper/quartz/SysJobLogMapper.xml +++ b/fastbee-plugs/fastbee-quartz/src/main/resources/mapper/quartz/SysJobLogMapper.xml @@ -77,6 +77,11 @@ truncate table sys_job_log + + DELETE FROM sys_job_log + WHERE create_time < DATE_SUB(NOW(), INTERVAL #{days} DAY) + + insert into sys_job_log( job_log_id, diff --git a/fastbee-protocol/fastbee-protocol-collect/src/main/java/com/fastbee/modbus/model/ModbusRtu.java b/fastbee-protocol/fastbee-protocol-collect/src/main/java/com/fastbee/modbus/model/ModbusRtu.java index 32dcb007..c87d6283 100644 --- a/fastbee-protocol/fastbee-protocol-collect/src/main/java/com/fastbee/modbus/model/ModbusRtu.java +++ b/fastbee-protocol/fastbee-protocol-collect/src/main/java/com/fastbee/modbus/model/ModbusRtu.java @@ -47,7 +47,7 @@ public class ModbusRtu extends Message { protected int start; @Column(length = 1, version = {0x80, 0x81}, desc = "标识位,实例设备是0x80") protected int mId; - @Column(length = 6, version = 0x80, desc = "MAC地址,6个字节") + @Column(length = 6, version = {0x80, 0x81}, desc = "MAC地址,6个字节") protected String mac; @Column(length = 1, version = {0x80, 0x81}, desc = "结尾,如:7E") protected int end; diff --git a/fastbee-protocol/fastbee-protocol-collect/src/main/java/com/fastbee/modbus/pak/ModbusEndPoint.java b/fastbee-protocol/fastbee-protocol-collect/src/main/java/com/fastbee/modbus/pak/ModbusEndPoint.java index aad04a54..417c0efb 100644 --- a/fastbee-protocol/fastbee-protocol-collect/src/main/java/com/fastbee/modbus/pak/ModbusEndPoint.java +++ b/fastbee-protocol/fastbee-protocol-collect/src/main/java/com/fastbee/modbus/pak/ModbusEndPoint.java @@ -39,7 +39,14 @@ public class ModbusEndPoint { @PakMapping(types = 心跳包) public void heartbeat(DeviceReport message, Session session) { - + //心跳包处理:确保设备注册并更新在线状态 + if (!session.isRegistered()) { + session.register(message); + //记录设备信息 + DeviceMsg deviceMsg = new DeviceMsg(); + deviceMsg.setClientId(message.getClientId()); + session.setAttribute(SessionKey.DeviceMsg, deviceMsg); + } } } diff --git a/fastbee-record/src/main/java/com/fastbee/record/config/ThreadPoolTaskConfig.java b/fastbee-record/src/main/java/com/fastbee/record/config/ThreadPoolTaskConfig.java index 703e36c2..6551a042 100644 --- a/fastbee-record/src/main/java/com/fastbee/record/config/ThreadPoolTaskConfig.java +++ b/fastbee-record/src/main/java/com/fastbee/record/config/ThreadPoolTaskConfig.java @@ -1,5 +1,6 @@ package com.fastbee.record.config; +import com.fastbee.common.trace.MdcTaskDecorator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableAsync; @@ -52,6 +53,10 @@ public class ThreadPoolTaskConfig { // 线程池对拒绝任务的处理策略 // CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + + // 新增:配置 TaskDecorator,自动传播 TraceId + executor.setTaskDecorator(new MdcTaskDecorator()); + // 初始化 executor.initialize(); return executor; diff --git a/fastbee-server/base-server/src/main/java/com/fastbee/base/core/hanler/ConnectionTraceHandler.java b/fastbee-server/base-server/src/main/java/com/fastbee/base/core/hanler/ConnectionTraceHandler.java new file mode 100644 index 00000000..0ec96e82 --- /dev/null +++ b/fastbee-server/base-server/src/main/java/com/fastbee/base/core/hanler/ConnectionTraceHandler.java @@ -0,0 +1,82 @@ +package com.fastbee.base.core.hanler; + +import com.fastbee.common.trace.TraceContext; +import com.fastbee.common.trace.TraceIdGenerator; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.util.AttributeKey; + +/** + * 连接级链路追踪处理器(TCP 专用) + * + *

与 {@link TraceHandler}(每报文一个 TraceId)不同,本处理器以「一次 TCP 连接」为粒度: + * 在连接建立(channelActive)时生成唯一 TraceId 并绑定到 Channel 的 attr 上, + * 该连接生命周期内的所有入站事件(读、异常、空闲等)共用同一个 TraceId, + * 便于把同一设备 TCP 会话的全部日志用一个 traceId 检索出来。 + * + *

注意: + *

    + *
  • Netty EventLoop 线程被多连接复用,因此每次入站事件处理前后必须 set/clear MDC, + * 避免 TraceId 在 EventLoop 上串线。连接级仅指「TraceId 的取值来源」, + * 不再每次重新生成,清理逻辑与 {@link TraceHandler} 一致。
  • + *
  • TraceId 存储在 Channel.attr 中,handler 自身保持无状态,可继续用 {@code @Sharable} 单例。
  • + *
  • UDP 为无连接协议,请勿复用本处理器,仍应使用 {@link TraceHandler}(每报文一个 TraceId)。
  • + *
+ * + * @author FastBee + */ +@ChannelHandler.Sharable +public class ConnectionTraceHandler extends ChannelInboundHandlerAdapter { + + /** + * 共享实例(无状态,可跨 Channel 复用) + */ + public static final ConnectionTraceHandler INSTANCE = new ConnectionTraceHandler(); + + /** + * 连接级 TraceId 在 Channel 属性中的 key + */ + private static final AttributeKey CONN_TRACE_ID = AttributeKey.valueOf("connTraceId"); + + @Override + public void channelActive(ChannelHandlerContext ctx) { + // 连接建立时生成一次连接级 TraceId,绑定到当前 Channel + ctx.channel().attr(CONN_TRACE_ID).set(TraceIdGenerator.generate()); + withTrace(ctx, ctx::fireChannelActive); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + withTrace(ctx, () -> ctx.fireChannelRead(msg)); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + withTrace(ctx, () -> ctx.fireUserEventTriggered(evt)); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + withTrace(ctx, () -> ctx.fireExceptionCaught(cause)); + } + + /** + * 包裹一次入站事件:套用连接级 TraceId 到 MDC,执行后清理, + * 防止 EventLoop 线程复用导致 TraceId 串到其它连接。 + * + * @param ctx 当前 Channel 上下文 + * @param event 需要包裹的入站事件 + */ + private void withTrace(ChannelHandlerContext ctx, Runnable event) { + String traceId = ctx.channel().attr(CONN_TRACE_ID).get(); + if (traceId != null) { + TraceContext.setTraceId(traceId); + } + try { + event.run(); + } finally { + TraceContext.clear(); + } + } +} diff --git a/fastbee-server/base-server/src/main/java/com/fastbee/base/core/hanler/TraceHandler.java b/fastbee-server/base-server/src/main/java/com/fastbee/base/core/hanler/TraceHandler.java new file mode 100644 index 00000000..557585d2 --- /dev/null +++ b/fastbee-server/base-server/src/main/java/com/fastbee/base/core/hanler/TraceHandler.java @@ -0,0 +1,38 @@ +package com.fastbee.base.core.hanler; + +import com.fastbee.common.trace.TraceContext; +import com.fastbee.common.trace.TraceIdGenerator; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; + +/** + * Netty 链路追踪处理器 + * + *

放置在协议 pipeline 的最前端(编解码器之前),为每条入站报文生成独立的 TraceId, + * 使解码、分发、业务处理全链路的日志都携带同一个 TraceId。 + * + *

注意:Netty EventLoop 线程会被多连接/多报文复用, + * 因此必须在处理结束后清理,否则会造成 TraceId 串线。 + * + * @author FastBee + */ +@ChannelHandler.Sharable +public class TraceHandler extends ChannelInboundHandlerAdapter { + + /** + * 共享实例(无状态,可跨 Channel 复用) + */ + public static final TraceHandler INSTANCE = new TraceHandler(); + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + // 每条设备报文都是独立业务事件,生成新的 TraceId + TraceContext.setTraceId(TraceIdGenerator.generate()); + try { + ctx.fireChannelRead(msg); + } finally { + TraceContext.clear(); + } + } +} \ No newline at end of file diff --git a/fastbee-server/iot-server-core/src/main/java/com/fastbee/server/TCPServer.java b/fastbee-server/iot-server-core/src/main/java/com/fastbee/server/TCPServer.java index 6a8ba7bb..709c8a00 100644 --- a/fastbee-server/iot-server-core/src/main/java/com/fastbee/server/TCPServer.java +++ b/fastbee-server/iot-server-core/src/main/java/com/fastbee/server/TCPServer.java @@ -1,5 +1,6 @@ package com.fastbee.server; +import com.fastbee.base.core.hanler.ConnectionTraceHandler; import com.fastbee.common.constant.FastBeeConstant; import com.fastbee.server.config.NettyConfig; import com.fastbee.server.handler.*; @@ -61,9 +62,12 @@ public class TCPServer extends Server { @Override protected void initChannel(NioSocketChannel channel) throws Exception { channel.pipeline() + // 第一步、链路追踪(必须位于编解码器之前,让解码日志也带 TraceId) + // 连接级:同一 TCP 连接生命周期内共用一个 TraceId,便于整段会话检索 + .addLast(FastBeeConstant.SERVER.TRACE, ConnectionTraceHandler.INSTANCE) // 设置心跳时间 .addLast(new IdleStateHandler(config.readerIdleTime, config.writerIdleTime, config.allIdleTime)) - // 第一步、添加粘包处理器 + // 第二步、添加粘包处理器 .addLast(FastBeeConstant.SERVER.FRAMEDECODER, new ProtocolSniffingDecoder(config.maxFrameLength)) // 第二步、添加消息适配器 .addLast(FastBeeConstant.SERVER.ADAPTER, adapter) diff --git a/fastbee-server/iot-server-core/src/main/java/com/fastbee/server/UDPServer.java b/fastbee-server/iot-server-core/src/main/java/com/fastbee/server/UDPServer.java index 35ffb17f..ae05b9dd 100644 --- a/fastbee-server/iot-server-core/src/main/java/com/fastbee/server/UDPServer.java +++ b/fastbee-server/iot-server-core/src/main/java/com/fastbee/server/UDPServer.java @@ -1,5 +1,6 @@ package com.fastbee.server; +import com.fastbee.base.core.hanler.TraceHandler; import com.fastbee.common.constant.FastBeeConstant; import com.fastbee.server.config.NettyConfig; import com.fastbee.server.handler.DispatcherHandler; @@ -52,6 +53,8 @@ public class UDPServer extends Server { @Override protected void initChannel(NioDatagramChannel channel) throws Exception { channel.pipeline() + // 链路追踪(位于编解码器之前,让解码日志也带 TraceId) + .addLast(FastBeeConstant.SERVER.TRACE, TraceHandler.INSTANCE) .addLast(FastBeeConstant.SERVER.ADAPTER, adapter) .addLast(FastBeeConstant.SERVER.DECODER, decoder) .addLast(FastBeeConstant.SERVER.ENCODER, encoder) diff --git a/fastbee-server/iot-server-core/src/main/java/com/fastbee/server/handler/DispatcherHandler.java b/fastbee-server/iot-server-core/src/main/java/com/fastbee/server/handler/DispatcherHandler.java index 8b8abca0..008e7564 100644 --- a/fastbee-server/iot-server-core/src/main/java/com/fastbee/server/handler/DispatcherHandler.java +++ b/fastbee-server/iot-server-core/src/main/java/com/fastbee/server/handler/DispatcherHandler.java @@ -13,6 +13,7 @@ import com.fastbee.common.core.mq.DeviceTestReportBo; import com.fastbee.common.core.protocol.Message; import com.fastbee.common.core.thingsModel.ThingsModelSimpleItem; import com.fastbee.common.enums.ServerType; +import com.fastbee.common.trace.TraceContext; import com.fastbee.common.utils.DateUtils; import com.fastbee.mq.producer.MessageProducer; import io.netty.channel.ChannelHandler; @@ -61,6 +62,7 @@ public class DispatcherHandler extends ChannelInboundHandlerAdapter { s.increment(); } + /*TraceId 由 pipeline 头部的 TraceHandler 统一生成与清理*/ Packet packet = (Packet) msg; Message request = packet.message; // 判断是否注册 @@ -127,7 +129,16 @@ public class DispatcherHandler extends ChannelInboundHandlerAdapter { } } else { if (handler.async) { - executor.execute(() -> channelRead0(ctx, packet, handler)); + /*异步执行:手动透传 TraceId(业务线程池非Spring管理,无 TaskDecorator)*/ + final String traceId = TraceContext.getTraceId(); + executor.execute(() -> { + TraceContext.ensureTraceId(traceId); + try { + channelRead0(ctx, packet, handler); + } finally { + TraceContext.clear(); + } + }); } else { channelRead0(ctx, packet, handler); } diff --git a/fastbee-server/mqtt-broker/src/main/java/com/fastbee/mqtt/handler/adapter/MqttMessageDelegate.java b/fastbee-server/mqtt-broker/src/main/java/com/fastbee/mqtt/handler/adapter/MqttMessageDelegate.java index b6d5d146..26573753 100644 --- a/fastbee-server/mqtt-broker/src/main/java/com/fastbee/mqtt/handler/adapter/MqttMessageDelegate.java +++ b/fastbee-server/mqtt-broker/src/main/java/com/fastbee/mqtt/handler/adapter/MqttMessageDelegate.java @@ -55,6 +55,8 @@ public class MqttMessageDelegate { log.error("=>客户端未连接"); throw new ServiceException("客户端未连接"); } + + /*TraceId 由 pipeline 头部的 ConnectionTraceHandler(连接级)统一生成与清理*/ Optional.of(processor.get(messageType)) .ifPresent(mqttHandler -> mqttHandler.handler(ctx, message)); } diff --git a/fastbee-server/mqtt-broker/src/main/java/com/fastbee/mqtt/server/MqttServer.java b/fastbee-server/mqtt-broker/src/main/java/com/fastbee/mqtt/server/MqttServer.java index f81f9342..afe2c88f 100644 --- a/fastbee-server/mqtt-broker/src/main/java/com/fastbee/mqtt/server/MqttServer.java +++ b/fastbee-server/mqtt-broker/src/main/java/com/fastbee/mqtt/server/MqttServer.java @@ -1,5 +1,6 @@ package com.fastbee.mqtt.server; +import com.fastbee.base.core.hanler.ConnectionTraceHandler; import com.fastbee.common.constant.FastBeeConstant; import com.fastbee.mqtt.handler.adapter.MqttMessageAdapter; import com.fastbee.server.Server; @@ -54,6 +55,8 @@ public class MqttServer extends Server { channel.pipeline() .addFirst(FastBeeConstant.SERVER.IDLE , new IdleStateHandler(config.readerIdleTime, config.writerIdleTime, config.allIdleTime, TimeUnit.SECONDS)) + // 链路追踪(位于编解码器之前,让解码日志也带 TraceId) + .addFirst(FastBeeConstant.SERVER.TRACE, ConnectionTraceHandler.INSTANCE) .addLast(FastBeeConstant.SERVER.DECODER, new MqttDecoder(1024 * 1024 * 2)) .addLast(FastBeeConstant.SERVER.ENCODER, MqttEncoder.INSTANCE) .addLast(messageAdapter); diff --git a/fastbee-server/mqtt-broker/src/main/java/com/fastbee/mqtt/server/WebSocketServer.java b/fastbee-server/mqtt-broker/src/main/java/com/fastbee/mqtt/server/WebSocketServer.java index 33ea603d..d37dffb4 100644 --- a/fastbee-server/mqtt-broker/src/main/java/com/fastbee/mqtt/server/WebSocketServer.java +++ b/fastbee-server/mqtt-broker/src/main/java/com/fastbee/mqtt/server/WebSocketServer.java @@ -1,5 +1,6 @@ package com.fastbee.mqtt.server; +import com.fastbee.base.core.hanler.ConnectionTraceHandler; import com.fastbee.common.constant.FastBeeConstant; import com.fastbee.mqtt.codec.WebSocketMqttCodec; import com.fastbee.mqtt.handler.adapter.MqttMessageAdapter; @@ -49,6 +50,8 @@ public class WebSocketServer extends Server { ch.pipeline() .addFirst(FastBeeConstant.WS.HEART_BEAT , new IdleStateHandler(0, 0, 70)) + /*链路追踪(位于编解码器之前,让解码日志也带 TraceId)*/ + .addFirst(FastBeeConstant.SERVER.TRACE, ConnectionTraceHandler.INSTANCE) /*http请求响应*/ .addLast(FastBeeConstant.WS.HTTP_SERVER_CODEC, new HttpServerCodec()) /*聚合header与body组成完整的Http请求,最大数据量为1Mb*/ diff --git a/fastbee-server/sip-server/src/main/java/com/fastbee/sip/conf/ThreadPoolTaskConfig.java b/fastbee-server/sip-server/src/main/java/com/fastbee/sip/conf/ThreadPoolTaskConfig.java index ac8d78ab..a07e51a7 100644 --- a/fastbee-server/sip-server/src/main/java/com/fastbee/sip/conf/ThreadPoolTaskConfig.java +++ b/fastbee-server/sip-server/src/main/java/com/fastbee/sip/conf/ThreadPoolTaskConfig.java @@ -1,5 +1,6 @@ package com.fastbee.sip.conf; +import com.fastbee.common.trace.MdcTaskDecorator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableAsync; @@ -26,6 +27,8 @@ public class ThreadPoolTaskConfig { executor.setKeepAliveSeconds(keepAliveTime); executor.setThreadNamePrefix(threadNamePrefix); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + // 配置 TaskDecorator,自动传播 TraceId + executor.setTaskDecorator(new MdcTaskDecorator()); executor.initialize(); return executor; } diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/Device.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/Device.java index acecb3a7..79d45989 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/Device.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/Device.java @@ -101,6 +101,13 @@ public class Device extends BaseEntity { @TableField(exist = false) private Integer deviceType; + /** + * 设备类型列表(用于多类型过滤) + */ + @ApiModelProperty("设备类型列表(用于多类型过滤,如[1,2]表示直连和网关设备)") + @TableField(exist = false) + private List deviceTypes; + /** * 设备状态(1-未激活,2-禁用,3-在线,4-离线) */ diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/Product.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/Product.java index 632da4af..040857f1 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/Product.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/Product.java @@ -130,7 +130,7 @@ public class Product extends BaseEntity { /** * 设备类型(1-直连设备、2-网关子设备、3-网关设备) */ - @ApiModelProperty(value = "设备类型", notes = "(1-直连设备、2-网关子设备、3-网关设备、4-网关子设备)") + @ApiModelProperty(value = "设备类型", notes = "(1-直连设备、2-网关设备、3-监控设备、4-网关子设备)") @Excel(name = "设备类型", readConverterExp = "1=直连设备、2=网关设备、3=监控设备、4-网关子设备") private Integer deviceType; /** diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/Script.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/Script.java index 376dfcd7..f8cf1f4c 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/Script.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/Script.java @@ -96,6 +96,11 @@ public class Script extends BaseEntity { private String productName; + /** + * 设备编号列表(逗号分隔,为空表示全部设备) + */ + private String serialNumbers; + /** * 是否生效 */ diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/mapper/AlertLogMapper.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/mapper/AlertLogMapper.java index 93ad999b..7a840871 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/mapper/AlertLogMapper.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/mapper/AlertLogMapper.java @@ -115,4 +115,12 @@ public interface AlertLogMapper { * @return com.fastbee.iot.model.AlertCountVO */ List countAlertLevel(DataCenterParam dataCenterParam); + + /** + * 删除指定天数前的告警日志 + * + * @param days 保留天数 + * @return 删除记录数 + */ + int deleteAlertLogBeforeDays(@Param("days") int days); } diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/mapper/EventLogMapper.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/mapper/EventLogMapper.java index b89ad082..6c3b5ea3 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/mapper/EventLogMapper.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/mapper/EventLogMapper.java @@ -25,4 +25,12 @@ public interface EventLogMapper extends BaseMapperX { Long selectEventLogCount(@Param("userId") Long userId); + /** + * 删除指定天数前的日志 + * + * @param days 保留天数 + * @return 删除记录数 + */ + int deleteEventLogBeforeDays(@Param("days") int days); + } diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/ScriptCondition.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/ScriptCondition.java index 626dedf2..85f5642b 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/ScriptCondition.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/ScriptCondition.java @@ -46,6 +46,11 @@ public class ScriptCondition { */ private Long productId; + /** + * 当前上报设备的编号(用于设备级过滤,为空不过滤) + */ + private String serialNumber; + /** * 场景ID */ diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/modbus/ModbusPollJob.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/modbus/ModbusPollJob.java index 237d444e..0c34e4f5 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/modbus/ModbusPollJob.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/modbus/ModbusPollJob.java @@ -11,6 +11,9 @@ import lombok.Data; @Data public class ModbusPollJob { + /** 链路追踪ID */ + private String traceId; + private ModbusPollMsg pollMsg; private DeviceJob deviceJob; diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/adapter/dcp/DcpPlatformAdapter.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/adapter/dcp/DcpPlatformAdapter.java index 45c97e4f..696c0eb7 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/adapter/dcp/DcpPlatformAdapter.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/adapter/dcp/DcpPlatformAdapter.java @@ -18,6 +18,7 @@ import com.fastbee.iot.platform.flow.FlowContext; import com.fastbee.iot.platform.model.json.*; import com.fastbee.iot.platform.flow.PlatformFlowExecutor; import com.fastbee.iot.platform.mapper.PlatformFlowMapper; +import com.fastbee.iot.platform.mapper.PlatformDeviceMappingMapper; import com.fastbee.iot.platform.mapper.PlatformInterfaceMapper; import com.fastbee.iot.platform.mapper.PlatformUserAuthMapper; import com.fastbee.iot.platform.service.IPlatformDeviceSubMappingService; @@ -35,6 +36,7 @@ import java.time.OffsetDateTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; +import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.LinkedHashMap; @@ -69,6 +71,8 @@ public class DcpPlatformAdapter { private PlatformInterfaceMapper interfaceMapper; @Autowired private PlatformUserAuthMapper platformUserAuthMapper; + @Autowired + private PlatformDeviceMappingMapper mappingMapper; @Autowired private IPlatformInstanceService instanceService; @@ -198,7 +202,10 @@ public class DcpPlatformAdapter { bufferService.saveReferenceData(platform.getId(), fieldName, reportSerialNumber, currentItem, reportIntervalSec); int replayCount = replayPendingMonitorData(platform, userAuth, mapping, fieldName, currentItem, reportIntervalSec, baseUrl, interfaceCode); - log.info("[DCP] 基准点 {} 已缓存,补推监测点 {} 条", reportSerialNumber, replayCount); + int crossCount = replaySiblingGatewayMonitors(platform, userAuth, mapping, fieldName, currentItem, + reportIntervalSec, reportSerialNumber); + log.info("[DCP] 基准点 {} 已缓存,补推监测点 {} 条(其中跨网关 {} 条)", + reportSerialNumber, replayCount + crossCount, crossCount); return; } @@ -294,8 +301,15 @@ public class DcpPlatformAdapter { throw new RuntimeException("不支持的数组组包模式 arrayMode=" + mode); } - /** 解析基准点序列号 */ + /** 解析基准点序列号:跨网关配置 > 本网关基准点行 > onboard_config > 网关序列号 */ private String resolveRefSerialNumber(PlatformDeviceSubMapping subMapping, PlatformDeviceMapping mapping) { + // 跨网关基准点:前端在下拉框中选择的、属于其他网关的基准点序列号 + ExtraMappingConfig extra = mapping.getExtraMappingParsed(); + if (extra != null && extra.getStaticLevel() != null + && StringUtils.hasText(extra.getStaticLevel().getReferenceSerialNumber())) { + return extra.getStaticLevel().getReferenceSerialNumber(); + } + try { PlatformDeviceSubMapping reference = subMappingService.findReference(mapping.getId()); if (reference != null && StringUtils.hasText(reference.getSubSerialNumber())) { @@ -315,17 +329,69 @@ public class DcpPlatformAdapter { } /** - * 基准点后到时,补推此前缓存的监测点数据。 + * 基准点后到时,补推此前缓存的监测点数据(本网关 + 引用该基准点的其他网关)。 */ private int replayPendingMonitorData(PlatformInstance platform, PlatformUserAuth userAuth,PlatformDeviceMapping mapping, String fieldName, JSONObject referenceItem, int reportIntervalSec, String baseUrl, String interfaceCode) { + return replayMonitorsOfMapping(platform, userAuth, mapping, fieldName, referenceItem, + reportIntervalSec, baseUrl, interfaceCode); + } + + /** + * 跨网关补推:基准点上报后,补推其他网关中引用该基准点、且已缓存等待的监测点。 + * + * @param refSerialNumber 本次上报的基准点序列号 + */ + private int replaySiblingGatewayMonitors(PlatformInstance platform, PlatformUserAuth userAuth, + PlatformDeviceMapping refMapping, String fieldName, + JSONObject referenceItem, int reportIntervalSec, + String refSerialNumber) { + List siblings = mappingMapper.selectList(Wrappers.lambdaQuery() + .eq(PlatformDeviceMapping::getPlatformId, platform.getId()) + .ne(PlatformDeviceMapping::getId, refMapping.getId()) + .eq(PlatformDeviceMapping::getSensorType, "static_level") + .eq(PlatformDeviceMapping::getDeviceType, 1) + .eq(PlatformDeviceMapping::getNetStatus, 1)); + if (siblings == null || siblings.isEmpty()) { + return 0; + } + + int replayCount = 0; + for (PlatformDeviceMapping sibling : siblings) { + // 仅处理配置了跨网关基准点、且指向本次上报基准点的网关 + ExtraMappingConfig extra = sibling.getExtraMappingParsed(); + if (extra == null || extra.getStaticLevel() == null + || !refSerialNumber.equals(extra.getStaticLevel().getReferenceSerialNumber())) { + continue; + } + PlatformInterface pushInterface = interfaceMapper.selectById(sibling.getPushInterfaceId()); + if (pushInterface == null) { + log.warn("[DCP] 跨网关补推跳过:网关 {} 未配置推送接口", sibling.getSerialNumber()); + continue; + } + String baseUrl = platform.getBaseUrl() + resolvePushPath(sibling, pushInterface); + String interfaceCode = resolveInterfaceCode(sibling.getSensorType(), pushInterface); + replayCount += replayMonitorsOfMapping(platform, userAuth, sibling, fieldName, referenceItem, + reportIntervalSec, baseUrl, interfaceCode); + } + return replayCount; + } + + /** + * 补推指定网关下所有已缓存的监测点:合并为一次 HTTP 请求推送, + * 全部成功才清空缓存,失败则整批保留缓存等待下次基准点上报。 + */ + private int replayMonitorsOfMapping(PlatformInstance platform, PlatformUserAuth userAuth, PlatformDeviceMapping mapping, + String fieldName, JSONObject referenceItem, + int reportIntervalSec, String baseUrl, String interfaceCode) { List subMappings = subMappingService.selectByParentId(mapping.getId()); if (subMappings == null || subMappings.isEmpty()) { return 0; } - int replayCount = 0; + JSONArray pushBodyArray = new JSONArray(); + List pushedSerials = new ArrayList<>(); for (PlatformDeviceSubMapping monitor : subMappings) { if (monitor == null || !monitor.isMonitor() || !StringUtils.hasText(monitor.getSubSerialNumber())) { continue; @@ -353,23 +419,32 @@ public class DcpPlatformAdapter { JSONArray sensorData = new JSONArray(); sensorData.add(referenceItem); sensorData.add(monitorItem); - JSONArray pushBody = buildArrayPushBody(mapping, bodyFields, fieldName, + JSONArray deviceBodyArr = buildArrayPushBody(mapping, bodyFields, fieldName, sensorData, monitor.getPlatformDeviceId()); - - PlatformPushLog logEntry = createLogEntry(platform, mapping, interfaceCode, monitor.getSubSerialNumber()); - doHttpPost(platform, userAuth, mapping, baseUrl, JSON.toJSONString(pushBody), logEntry); - pushLogService.asyncSave(logEntry); - if (logEntry.getStatus() != null && logEntry.getStatus() == 1) { - bufferService.removeMonitorData(platform.getId(), fieldName, monitor.getSubSerialNumber()); - replayCount++; - log.info("[DCP] 基准点后到,已补推监测点 {} deviceId={}", - monitor.getSubSerialNumber(), monitor.getPlatformDeviceId()); - } else { - log.warn("[DCP] 基准点后到,补推监测点 {} 失败,保留缓存等待下次基准点上报", - monitor.getSubSerialNumber()); + if (!deviceBodyArr.isEmpty()) { + pushBodyArray.add(deviceBodyArr.get(0)); + pushedSerials.add(monitor.getSubSerialNumber()); } } - return replayCount; + + if (pushBodyArray.isEmpty()) { + return 0; + } + + PlatformPushLog logEntry = createLogEntry(platform, mapping, interfaceCode, mapping.getSerialNumber()); + doHttpPost(platform, userAuth, mapping, baseUrl, JSON.toJSONString(pushBodyArray), logEntry); + pushLogService.asyncSave(logEntry); + + if (logEntry.getStatus() != null && logEntry.getStatus() == 1) { + pushedSerials.forEach(serial -> bufferService.removeMonitorData(platform.getId(), fieldName, serial)); + log.info("[DCP] 基准点后到,已合并补推监测点 {} 条 mappingId={} 序列号={}", + pushedSerials.size(), mapping.getId(), String.join(",", pushedSerials)); + return pushedSerials.size(); + } + + log.warn("[DCP] 基准点后到,合并补推 {} 条失败,整批保留缓存等待下次基准点上报 mappingId={}", + pushedSerials.size(), mapping.getId()); + return 0; } // ==================== 普通推送 ==================== diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/job/PlatformPushScheduledJob.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/job/PlatformPushScheduledJob.java index 14835bf0..4f45c1e5 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/job/PlatformPushScheduledJob.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/job/PlatformPushScheduledJob.java @@ -223,7 +223,12 @@ public class PlatformPushScheduledJob { } for (PlatformDeviceSubMapping subMapping : subMappings) { - if (subMapping == null || subMapping.getPlatformDeviceId() == null) { + if (subMapping == null) { + continue; + } + // 基准点本身不单独推送(无平台设备ID),但仍需进入推送入口以刷新基准点缓冲, + // 否则跨网关/同网关的监测点配对时读不到新鲜基准点数据 + if (subMapping.getPlatformDeviceId() == null && !subMapping.isReference()) { continue; } try { diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/mapper/PlatformDeviceSubMappingMapper.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/mapper/PlatformDeviceSubMappingMapper.java index 588134cb..0ef9ba03 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/mapper/PlatformDeviceSubMappingMapper.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/mapper/PlatformDeviceSubMappingMapper.java @@ -10,6 +10,9 @@ public interface PlatformDeviceSubMappingMapper extends BaseMapperX selectByParentId(@Param("parentMappingId") Long parentMappingId); + /** 批量查询多个父映射下的所有子映射(IN 查询,列表页展开行使用) */ + List selectByParentIds(@Param("parentIds") List parentIds); + PlatformDeviceSubMapping selectByParentAndSerial(@Param("parentMappingId") Long parentMappingId, @Param("subSerialNumber") String subSerialNumber); diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/mapper/PlatformPushLogMapper.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/mapper/PlatformPushLogMapper.java index eb8109a3..4c4156f6 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/mapper/PlatformPushLogMapper.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/mapper/PlatformPushLogMapper.java @@ -14,4 +14,5 @@ public interface PlatformPushLogMapper extends BaseMapperX { void cleanPushLog(); + void deletePushLogBeforeDays(int days); } diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/IPlatformDeviceMappingService.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/IPlatformDeviceMappingService.java index eac312ab..52671d99 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/IPlatformDeviceMappingService.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/IPlatformDeviceMappingService.java @@ -5,6 +5,7 @@ import com.fastbee.iot.platform.domain.PlatformDeviceMapping; import java.util.Date; import java.util.List; +import java.util.Map; public interface IPlatformDeviceMappingService extends IService { @@ -18,4 +19,11 @@ public interface IPlatformDeviceMappingService extends IService> listConfiguredReferencePoints(Long platformId, Long excludeMappingId); } diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/IPlatformDeviceSubMappingService.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/IPlatformDeviceSubMappingService.java index 9eeacffe..23c48331 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/IPlatformDeviceSubMappingService.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/IPlatformDeviceSubMappingService.java @@ -4,12 +4,16 @@ import com.baomidou.mybatisplus.extension.service.IService; import com.fastbee.iot.platform.domain.PlatformDeviceSubMapping; import java.util.List; +import java.util.Map; public interface IPlatformDeviceSubMappingService extends IService { /** 查询父映射下的所有子映射 */ List selectByParentId(Long parentMappingId); + /** 批量查询多个父映射下的子映射,按 parentMappingId 分组返回(列表页展开行一次拉取,避免 N 次请求) */ + Map> selectByParentIds(List parentIds); + /** 根据子设备序列号查找子映射(推送时使用) */ List selectBySubSerialNumber(String subSerialNumber); diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/IPlatformPushLogService.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/IPlatformPushLogService.java index 13a0cbaf..dca63f61 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/IPlatformPushLogService.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/IPlatformPushLogService.java @@ -23,4 +23,5 @@ public interface IPlatformPushLogService extends IService { */ void cleanPushLog(); + void cleanPushLog(int days); } diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/impl/PlatformDeviceMappingServiceImpl.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/impl/PlatformDeviceMappingServiceImpl.java index d4bb4c03..7272aa7f 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/impl/PlatformDeviceMappingServiceImpl.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/impl/PlatformDeviceMappingServiceImpl.java @@ -3,6 +3,8 @@ package com.fastbee.iot.platform.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.fastbee.iot.domain.Device; +import com.fastbee.iot.mapper.DeviceMapper; import com.fastbee.iot.platform.domain.*; import com.fastbee.iot.platform.flow.FlowContext; import com.fastbee.iot.platform.flow.PlatformFlowExecutor; @@ -10,6 +12,8 @@ import com.fastbee.iot.platform.mapper.PlatformDeviceMappingMapper; import com.fastbee.iot.platform.mapper.PlatformFlowMapper; import com.fastbee.iot.platform.mapper.PlatformInstanceMapper; import com.fastbee.iot.platform.mapper.PlatformUserAuthMapper; +import com.fastbee.iot.platform.model.json.ExtraMappingConfig; +import com.fastbee.iot.platform.model.json.OnboardConfig; import com.fastbee.iot.platform.service.IPlatformDeviceMappingService; import com.fastbee.iot.platform.service.IPlatformDeviceSubMappingService; import com.fastbee.iot.platform.service.IPlatformUserAuthService; @@ -21,6 +25,7 @@ import org.springframework.util.StringUtils; import java.util.ArrayList; import java.util.Date; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; @@ -41,6 +46,8 @@ public class PlatformDeviceMappingServiceImpl extends ServiceImpl selectList(PlatformDeviceMapping query) { @@ -58,7 +65,25 @@ public class PlatformDeviceMappingServiceImpl extends ServiceImpl rows = this.list(wrapper); + // 批量补全设备名称:device_name 为冗余字段,旧数据可能为空,按 deviceId 一次 IN 查询回填,避免前端逐个查询 + List deviceIds = rows.stream() + .filter(m -> m.getDeviceId() != null && !StringUtils.hasText(m.getDeviceName())) + .map(PlatformDeviceMapping::getDeviceId) + .distinct() + .collect(Collectors.toList()); + if (!deviceIds.isEmpty()) { + List devices = deviceMapper.selectList(Wrappers.lambdaQuery() + .in(Device::getDeviceId, deviceIds)); + Map nameMap = devices.stream() + .collect(Collectors.toMap(Device::getDeviceId, Device::getDeviceName, (a, b) -> a)); + rows.forEach(m -> { + if (m.getDeviceId() != null && !StringUtils.hasText(m.getDeviceName())) { + m.setDeviceName(nameMap.get(m.getDeviceId())); + } + }); + } + return rows; } @Override @@ -198,13 +223,30 @@ public class PlatformDeviceMappingServiceImpl extends ServiceImpl> listConfiguredReferencePoints(Long platformId, Long excludeMappingId) { + List gateways = this.list(Wrappers.lambdaQuery() + .eq(platformId != null, PlatformDeviceMapping::getPlatformId, platformId) + .ne(excludeMappingId != null, PlatformDeviceMapping::getId, excludeMappingId) + .eq(PlatformDeviceMapping::getSensorType, "static_level") + .eq(PlatformDeviceMapping::getDeviceType, 1)); + List> result = new ArrayList<>(); + for (PlatformDeviceMapping gateway : gateways) { + List subs = subMappingService.selectByParentId(gateway.getId()); + if (subs == null) { + continue; + } + for (PlatformDeviceSubMapping sub : subs) { + if (sub == null || !sub.isReference() || !StringUtils.hasText(sub.getSubSerialNumber())) { + continue; + } + Map item = new java.util.LinkedHashMap<>(); + item.put("referenceSerialNumber", sub.getSubSerialNumber()); + item.put("referenceDeviceName", sub.getSubDeviceName()); + // 供前端生成监测点入网参数 refSensorId,保持与历史数据一致 + item.put("referenceSensorId", resolveSubSensorId(sub)); + item.put("gatewaySerialNumber", gateway.getSerialNumber()); + item.put("gatewayDeviceName", StringUtils.hasText(gateway.getDeviceName()) + ? gateway.getDeviceName() : gateway.getSerialNumber()); + result.add(item); + } + } + return result; + } } diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/impl/PlatformDeviceSubMappingServiceImpl.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/impl/PlatformDeviceSubMappingServiceImpl.java index 8e331fbe..873b6e0d 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/impl/PlatformDeviceSubMappingServiceImpl.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/impl/PlatformDeviceSubMappingServiceImpl.java @@ -8,7 +8,11 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -24,6 +28,25 @@ public class PlatformDeviceSubMappingServiceImpl extends ServiceImpl> selectByParentIds(List parentIds) { + if (parentIds == null || parentIds.isEmpty()) { + return Collections.emptyMap(); + } + List all = subMappingMapper.selectByParentIds(parentIds); + Map> grouped = new HashMap<>(); + for (PlatformDeviceSubMapping sub : all) { + Long pid = sub.getParentMappingId(); + if (pid == null) continue; + grouped.computeIfAbsent(pid, k -> new ArrayList<>()).add(sub); + } + // 保证请求的每个 parentId 都有 key,即便没有子映射也返回空列表,避免前端取到 undefined + for (Long pid : parentIds) { + grouped.putIfAbsent(pid, new ArrayList<>()); + } + return grouped; + } + @Override public List selectBySubSerialNumber(String subSerialNumber) { return subMappingMapper.selectBySubSerialNumber(subSerialNumber); diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/impl/PlatformPushLogServiceImpl.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/impl/PlatformPushLogServiceImpl.java index 48890077..9814ce22 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/impl/PlatformPushLogServiceImpl.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/impl/PlatformPushLogServiceImpl.java @@ -121,4 +121,9 @@ public class PlatformPushLogServiceImpl extends ServiceImpl countAlertLevel(DataCenterParam dataCenterParam); + + /** + * 清理告警日志 + * + * @param days 保留天数 + */ + void cleanAlertLog(int days); } diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/IEventLogService.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/IEventLogService.java index 5807e9db..0f94f59d 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/IEventLogService.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/IEventLogService.java @@ -37,4 +37,11 @@ public interface IEventLogService extends IService { */ List listHistory(EventLog eventLog); + /** + * 清理事件日志 + * + * @param days 保留天数 + */ + void cleanEventLog(int days); + } diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/AlertLogServiceImpl.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/AlertLogServiceImpl.java index f309e7ca..b4d9f367 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/AlertLogServiceImpl.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/AlertLogServiceImpl.java @@ -199,4 +199,14 @@ public class AlertLogServiceImpl implements IAlertLogService { public List countAlertLevel(DataCenterParam dataCenterParam) { return alertLogMapper.countAlertLevel(dataCenterParam); } + + /** + * 清理告警日志 + * + * @param days 保留天数 + */ + @Override + public void cleanAlertLog(int days) { + alertLogMapper.deleteAlertLogBeforeDays(days); + } } diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/DataRepairServiceImpl.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/DataRepairServiceImpl.java index f7a43b12..6902bd9c 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/DataRepairServiceImpl.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/DataRepairServiceImpl.java @@ -210,7 +210,7 @@ public class DataRepairServiceImpl implements IDataRepairService { public void executeDataRepair(String serialNumber, boolean isShadow, List currentLogs) { if (CollectionUtils.isEmpty(currentLogs) || isShadow) { - log.debug("无数据或为影子模式,不执行数据修复: serialNumber={}, isShadow={}", serialNumber, isShadow); + //log.debug("无数据或为影子模式,不执行数据修复: serialNumber={}, isShadow={}", serialNumber, isShadow); return; } diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/DeviceServiceImpl.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/DeviceServiceImpl.java index 25b7c26c..a3f55973 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/DeviceServiceImpl.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/DeviceServiceImpl.java @@ -654,8 +654,7 @@ public class DeviceServiceImpl implements IDeviceService { */ @Override @DSTransactional - public List reportDeviceThingsModelValue(ThingsModelValuesInput input, int type, - boolean isShadow) { + public List reportDeviceThingsModelValue(ThingsModelValuesInput input, int type, boolean isShadow) { String serialNumber = input.getDeviceNumber(); String key = RedisKeyBuilder.buildTSLVCacheKey(input.getProductId(), serialNumber); Map maps = new HashMap<>(); @@ -681,6 +680,9 @@ public class DeviceServiceImpl implements IDeviceService { ThingsModelSimpleItem::getValue, (oldValue, newValue) -> oldValue)); } + // 查询设备信息 + Device device = this.selectDeviceBySerialNumber(serialNumber); + for (ThingsModelSimpleItem item : thingsModelValueRemarkItem) { String identity = item.getId(); if (identity.startsWith("array_")) { @@ -752,11 +754,9 @@ public class DeviceServiceImpl implements IDeviceService { valueItem.setId(identity); if (id.startsWith("array_")) { // 查询是否有缓存,如果没有先进行缓存 - if (!redisCache.containsKey(key)) { - Device device = this.selectDeviceBySerialNumber(serialNumber); - this.selectDeviceByDeviceId(device.getDeviceId()); - } - +// if (!redisCache.containsKey(key)) { +// itslValueCache.addCacheDeviceStatus(input.getProductId(), serialNumber); +// } int index = Integer.parseInt(id.substring(6, 8)); if (isShadow) { String[] shadows = cacheIdentifier.getShadow().split(","); @@ -797,7 +797,6 @@ public class DeviceServiceImpl implements IDeviceService { /* ★★★★★★★★★★★★★★★★★★★★★★ 存储数据 - 开始 ★★★★★★★★★★★★★★★★★★★★★★ */ ThingsModelType modelType = ThingsModelType.getType(thingModels.getType()); - Device device = this.selectDeviceBySerialNumber(serialNumber); switch (modelType) { case PROP: if (1 == thingModels.getIsHistory()) { diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/EventLogServiceImpl.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/EventLogServiceImpl.java index 2b8bc211..ebb756e9 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/EventLogServiceImpl.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/EventLogServiceImpl.java @@ -64,4 +64,14 @@ public class EventLogServiceImpl extends ServiceImpl i public List listHistory(EventLog eventLog) { return eventLogMapper.listHistory(eventLog); } + + /** + * 清理事件日志 + * + * @param days 保留天数 + */ + @Override + public void cleanEventLog(int days) { + eventLogMapper.deleteEventLogBeforeDays(days); + } } diff --git a/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/AlertLogMapper.xml b/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/AlertLogMapper.xml index 01d5c50b..ac0ac040 100644 --- a/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/AlertLogMapper.xml +++ b/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/AlertLogMapper.xml @@ -274,4 +274,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + DELETE FROM iot_alert_log + WHERE create_time < DATE_SUB(NOW(), INTERVAL #{days} DAY) + + diff --git a/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/DeviceMapper.xml b/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/DeviceMapper.xml index 5d756484..c3972dc4 100644 --- a/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/DeviceMapper.xml +++ b/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/DeviceMapper.xml @@ -433,6 +433,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" and d.device_name like concat('%', #{deviceName}, '%') and d.product_id = #{productId} and p.device_type = #{deviceType} + + and p.device_type in + + #{deviceTypeItem} + + and d.product_name like concat('%', #{productName}, '%') and d.serial_number = #{serialNumber} and d.gw_dev_code = #{gwDevCode} @@ -1014,6 +1020,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" and d.device_name like concat('%', #{deviceName}, '%') and d.product_id = #{productId} and p.device_type = #{deviceType} + + and p.device_type in + + #{deviceTypeItem} + + and d.product_name like concat('%', #{productName}, '%') and d.serial_number = #{serialNumber} and d.gw_dev_code = #{gwDevCode} diff --git a/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/EventLogMapper.xml b/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/EventLogMapper.xml index f5536dd3..0a1756eb 100644 --- a/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/EventLogMapper.xml +++ b/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/EventLogMapper.xml @@ -59,4 +59,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" and user_id = #{userId} + + + DELETE FROM iot_event_log + WHERE create_time < DATE_SUB(NOW(), INTERVAL #{days} DAY) + diff --git a/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/ScriptMapper.xml b/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/ScriptMapper.xml index 4bd5373c..bb833cdc 100644 --- a/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/ScriptMapper.xml +++ b/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/ScriptMapper.xml @@ -10,6 +10,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + @@ -32,16 +33,16 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - select user_id, user_name, scene_id,product_id,product_name,script_event,script_action,script_purpose,script_order,application_name, script_id, script_name, script_data, script_type, script_language,enable, del_flag, create_by, create_time, update_by, update_time, remark from iot_script + select user_id, user_name, scene_id,product_id,product_name,serial_numbers,script_event,script_action,script_purpose,script_order,application_name, script_id, script_name, script_data, script_type, script_language,enable, del_flag, create_by, create_time, update_by, update_time, remark from iot_script - select user_id, user_name, scene_id,product_id,product_name, script_event,script_action,script_purpose,script_order,application_name, script_id, script_name, script_type, script_language,enable, create_by, create_time, update_by, update_time from iot_script + select user_id, user_name, scene_id,product_id,product_name,serial_numbers, script_event,script_action,script_purpose,script_order,application_name, script_id, script_name, script_type, script_language,enable, create_by, create_time, update_by, update_time from iot_script @@ -106,7 +112,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + + diff --git a/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/platform/PlatformPushLogMapper.xml b/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/platform/PlatformPushLogMapper.xml index 1d4c128f..83751a42 100644 --- a/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/platform/PlatformPushLogMapper.xml +++ b/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/platform/PlatformPushLogMapper.xml @@ -35,4 +35,10 @@ truncate table platform_push_log + + + DELETE FROM platform_push_log + WHERE create_time < DATE_SUB(NOW(), INTERVAL #{days} DAY) + + diff --git a/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/mapper/SysLogininforMapper.java b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/mapper/SysLogininforMapper.java index c68e4946..d9397f81 100644 --- a/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/mapper/SysLogininforMapper.java +++ b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/mapper/SysLogininforMapper.java @@ -41,4 +41,12 @@ public interface SysLogininforMapper { public int cleanLogininfor(); public List getUserCount(); + + /** + * 删除指定天数前的登录日志 + * + * @param days 保留天数 + * @return 删除记录数 + */ + public int deleteLogininforBeforeDays(int days); } diff --git a/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/mapper/SysOperLogMapper.java b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/mapper/SysOperLogMapper.java index 46ac307e..a05e33b6 100644 --- a/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/mapper/SysOperLogMapper.java +++ b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/mapper/SysOperLogMapper.java @@ -45,4 +45,12 @@ public interface SysOperLogMapper { * 清空操作日志 */ public void cleanOperLog(); + + /** + * 删除指定天数前的操作日志 + * + * @param days 保留天数 + * @return 删除记录数 + */ + public int deleteOperLogBeforeDays(int days); } diff --git a/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/ISysLogininforService.java b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/ISysLogininforService.java index ef1fcb69..7c27865a 100644 --- a/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/ISysLogininforService.java +++ b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/ISysLogininforService.java @@ -42,4 +42,11 @@ public interface ISysLogininforService { * 统计一周内的登陆数据 */ public List getUserCount(); + + /** + * 清理指定天数前的登录日志 + * + * @param days 保留天数 + */ + public void cleanLogininfor(int days); } diff --git a/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/ISysOperLogService.java b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/ISysOperLogService.java index b7713433..f2b21e28 100644 --- a/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/ISysOperLogService.java +++ b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/ISysOperLogService.java @@ -45,4 +45,11 @@ public interface ISysOperLogService { * 清空操作日志 */ public void cleanOperLog(); + + /** + * 清理指定天数前的操作日志 + * + * @param days 保留天数 + */ + public void cleanOperLog(int days); } diff --git a/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/impl/SysLogininforServiceImpl.java b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/impl/SysLogininforServiceImpl.java index 3d8cd3d3..e17fdf6f 100644 --- a/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/impl/SysLogininforServiceImpl.java +++ b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/impl/SysLogininforServiceImpl.java @@ -63,4 +63,14 @@ public class SysLogininforServiceImpl implements ISysLogininforService { public List getUserCount() { return logininforMapper.getUserCount(); } + + /** + * 清理指定天数前的登录日志 + * + * @param days 保留天数 + */ + @Override + public void cleanLogininfor(int days) { + logininforMapper.deleteLogininforBeforeDays(days); + } } diff --git a/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/impl/SysOperLogServiceImpl.java b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/impl/SysOperLogServiceImpl.java index 5bfed0ee..f0ace9a1 100644 --- a/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/impl/SysOperLogServiceImpl.java +++ b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/impl/SysOperLogServiceImpl.java @@ -68,4 +68,14 @@ public class SysOperLogServiceImpl implements ISysOperLogService { public void cleanOperLog() { operLogMapper.cleanOperLog(); } + + /** + * 清理指定天数前的操作日志 + * + * @param days 保留天数 + */ + @Override + public void cleanOperLog(int days) { + operLogMapper.deleteOperLogBeforeDays(days); + } } diff --git a/fastbee-service/fastbee-system-service/src/main/resources/mapper/system/SysLogininforMapper.xml b/fastbee-service/fastbee-system-service/src/main/resources/mapper/system/SysLogininforMapper.xml index 3fe4ccd7..d5ecdf38 100644 --- a/fastbee-service/fastbee-system-service/src/main/resources/mapper/system/SysLogininforMapper.xml +++ b/fastbee-service/fastbee-system-service/src/main/resources/mapper/system/SysLogininforMapper.xml @@ -76,4 +76,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" ) b on a.login_time = b.datetime; + + DELETE FROM sys_logininfor + WHERE login_time < DATE_SUB(NOW(), INTERVAL #{days} DAY) + + diff --git a/fastbee-service/fastbee-system-service/src/main/resources/mapper/system/SysOperLogMapper.xml b/fastbee-service/fastbee-system-service/src/main/resources/mapper/system/SysOperLogMapper.xml index 38456ed0..13a93f56 100644 --- a/fastbee-service/fastbee-system-service/src/main/resources/mapper/system/SysOperLogMapper.xml +++ b/fastbee-service/fastbee-system-service/src/main/resources/mapper/system/SysOperLogMapper.xml @@ -80,4 +80,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" truncate table sys_oper_log + + DELETE FROM sys_oper_log + WHERE oper_time < DATE_SUB(NOW(), INTERVAL #{days} DAY) + + \ No newline at end of file diff --git a/sql/log_clean_task_init.sql b/sql/log_clean_task_init.sql new file mode 100644 index 00000000..f12ccdcf --- /dev/null +++ b/sql/log_clean_task_init.sql @@ -0,0 +1,96 @@ +-- ============================================ +-- 日志清理定时任务初始化 SQL +-- ============================================ + +-- 1. 插入定时任务记录到 sys_job 表 +-- 说明:每天凌晨 0 点自动清理 15 天前的日志数据 +-- 注意:LogCleanTask 类位于 fastbee-admin 模块的 com.fastbee.web.task 包下 +INSERT INTO `sys_job` ( + `job_name`, + `job_group`, + `invoke_target`, + `cron_expression`, + `misfire_policy`, + `concurrent`, + `status`, + `create_by`, + `create_time`, + `remark` +) VALUES ( + '日志清理任务', -- 任务名称 + 'SYSTEM', -- 任务组名 + 'logCleanTask.cleanLogs(30)', -- 调用目标字符串 + '0 0 0 * * ?', -- Cron 表达式:每天凌晨 00:00:00 + '3', -- 错误策略:不触发立即执行 + '1', -- 禁止并发执行 + '0', -- 状态:正常 + 'admin', -- 创建者 + NOW(), -- 创建时间 + '每天凌晨0点自动清理15天前的日志数据(iot_event_log、sys_oper_log、sys_logininfor、iot_alert_log、sys_job_log、notify_log)' +); + +-- ============================================ +-- 清理的日志表说明 +-- ============================================ +-- 1. iot_event_log - IoT 事件日志 +-- 2. sys_oper_log - 系统操作日志 +-- 3. sys_logininfor - 登录日志 +-- 4. iot_alert_log - 设备告警日志 +-- 5. sys_job_log - 定时任务日志 +-- 6. notify_log - 通知日志 +-- +-- 注意:iot_device_log 表(设备监测日志)不参与清理 +-- 该表为核心数据,目前有 214 万条记录,需要数据迁移方案 + +-- 2. 可选:添加日志清理权限菜单(根据需要执行) +-- 如果需要在管理后台显示权限按钮,可以执行以下 SQL +/* +INSERT INTO `sys_menu` ( + `menu_name`, + `parent_id`, + `order_num`, + `path`, + `component`, + `query`, + `is_frame`, + `is_cache`, + `menu_type`, + `visible`, + `status`, + `perms`, + `icon`, + `create_by`, + `create_time`, + `remark` +) VALUES ( + '日志清理', -- 菜单名称 + 2, -- 父菜单ID(根据实际情况调整,2通常是系统管理) + 99, -- 显示顺序 + '', -- 路由地址 + '', -- 组件路径 + '', -- 路由参数 + 1, -- 不是外链 + 0, -- 不缓存 + 'F', -- 菜单类型:F=按钮 + '0', -- 显示状态 + '0', -- 菜单状态 + 'system:log:clean', -- 权限标识 + '#', -- 图标 + 'admin', -- 创建者 + NOW(), -- 创建时间 + '日志清理按钮权限' +); +*/ + +-- ============================================ +-- 验证定时任务是否创建成功 +-- ============================================ +-- SELECT * FROM sys_job WHERE job_name = '日志清理任务'; + +-- ============================================ +-- 手动触发清理(可选) +-- ============================================ +-- 如果需要立即清理历史数据,可以通过以下方式: +-- 方式1:通过管理后台 -> 系统监控 -> 定时任务 -> 点击"执行一次" +-- 方式2:调用 API 接口:POST /system/log/clean?days=30 +-- 方式3:数据库执行:UPDATE sys_job SET status = '0' WHERE job_name = '日志清理任务'; \ No newline at end of file diff --git a/sql/yack-init.sql b/sql/yack-init.sql index 80418505..f2ea8097 100644 --- a/sql/yack-init.sql +++ b/sql/yack-init.sql @@ -906,6 +906,7 @@ CREATE TABLE `iot_script` ( `scene_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '关联场景ID', `product_id` bigint(20) DEFAULT NULL COMMENT '产品ID', `product_name` varchar(64) CHARACTER SET utf8 COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '产品名称', + `serial_numbers` varchar(2000) DEFAULT NULL COMMENT '设备编号列表(逗号分隔,为空表示全部设备)', `script_event` tinyint(1) NOT NULL COMMENT '脚本事件(1=设备上报,2=平台下发,3=设备上线,4=设备离线)', `script_action` tinyint(1) NOT NULL COMMENT '脚本动作(1=消息重发,2=消息通知,3=Http推送,4=Mqtt桥接,5=数据库存储)', `script_purpose` tinyint(1) NOT NULL COMMENT '脚本用途(1=数据流,2=触发器,3=执行动作)', diff --git a/yack-app/apis/modules/deviceLog.js b/yack-app/apis/modules/deviceLog.js index a17f4987..ccdcec6a 100644 --- a/yack-app/apis/modules/deviceLog.js +++ b/yack-app/apis/modules/deviceLog.js @@ -32,7 +32,7 @@ export function getDeviceHistory (params) { return http.get('/iot/deviceLog/history', { params: params }); } -//查询历史监测数据 +//查询历史监测数据(直连设备) export function getHistoryList (data) { return http.request({ url: '/data/center/deviceHistory', @@ -41,6 +41,24 @@ export function getHistoryList (data) { }) } +// 查询网关子设备对比历史数据 +export function getSubDeviceComparativeHistory (data) { + return http.request({ + url: '/data/center/subDeviceComparativeHistory', + method: 'post', + data: data, + }) +} + +// 查询网关计算物模型历史数据(查询时按公式计算) +export function getDeviceHistoryCalculated (data) { + return http.request({ + url: '/data/center/deviceHistoryCalculated', + method: 'post', + data: data, + }) +} + // 查询设备日志详细 export function getDeviceLog (logId) { return http.request({ diff --git a/yack-app/apis/modules/mqttTest.js b/yack-app/apis/modules/mqttTest.js new file mode 100644 index 00000000..65a64364 --- /dev/null +++ b/yack-app/apis/modules/mqttTest.js @@ -0,0 +1,42 @@ +const http = uni.$u.http; + +// 指令编码 +export function encode(params) { + return http.get('/iot/message/encode', { + params: params + }); +} + +// 指令解码 +export function decode(params) { + return http.get('/iot/message/decode', { + params: params + }); +} + +// 平台下发指令 +export function messagePost(data) { + return http.post('/iot/message/post', data); +} + +// 查询设备指令偏好设置列表 +export function preferencesList(params) { + return http.get('/iot/preferences/list', { + params: params + }); +} + +// 新增设备指令偏好设置 +export function addPreferences(data) { + return http.post('/iot/preferences', data); +} + +// 修改设备指令偏好设置 +export function editPreferences(data) { + return http.put('/iot/preferences', data); +} + +// 删除设备指令偏好设置 +export function delPreferences(data) { + return http.delete('/iot/preferences/' + data.id); +} diff --git a/yack-app/env.config.js b/yack-app/env.config.js index c6f8a021..1cfea6fa 100644 --- a/yack-app/env.config.js +++ b/yack-app/env.config.js @@ -1,5 +1,5 @@ // H5端开发和生产环境协议 -let protocalDev = "wss://"; +let protocalDev = "ws://"; let protocalProd = "wss://"; // 条件编译,微信端和移动端使用wxs协议 @@ -19,7 +19,7 @@ const CONFIG = { development: { officialWebUrl: 'http://localhost:8080/', baseUrl: 'http://localhost:8080/', - mqttServer: protocalDev + 'localhost/mqtt', + mqttServer: protocalDev + '47.121.180.229:8083/mqtt', decoderUrl: 'http://localhost:8080/', qqmapKey, xinzhiKey, diff --git a/yack-app/locale/en-US.json b/yack-app/locale/en-US.json index a476c7a2..a287bfd8 100644 --- a/yack-app/locale/en-US.json +++ b/yack-app/locale/en-US.json @@ -49,6 +49,8 @@ "navBar.updateDevice": "Device Update", "navBar.eventLog": "Event Log", "navBar.orderLog": "Instruction log", + "navBar.dataDebug": "Data Debug", + "navBar.commandBuilder": "Command Builder", "navBar.player": "Player", "common.add": "add", "common.delete": "Delete", @@ -240,6 +242,72 @@ "createBtn": "Create Device", "content": "This feature requires login to use. Do you want to log in?" }, + "debug": { + "title": "Data Debug", + "send": "Send", + "reveiceType": "Reply", + "reportType": "Report", + "sendType": "Send", + "hex": "HEX", + "json": "JSON", + "ascii": "ASCII", + "inputPlaceholder": "Please enter the content to send", + "typePlaceholder": "Select format", + "analysis": "Parse", + "analysised": "Parsed", + "name": "Name", + "identifier": "Identifier", + "value": "Value", + "more": "More", + "quickCommand": "Quick Commands", + "addCommand": "Add Command", + "editCommand": "Edit Command", + "slaveAddress": "Slave Address", + "issueType": "Command Type", + "functionCode": "Function Code", + "startAddress": "Start Register Address", + "registerCount": "Register Count", + "coilCount": "Coil Count", + "writeCoilValue": "Coil Value", + "writeRegisterValue": "Register Value", + "registerValue": "Register Value", + "reportValue": "Report Value", + "coilValue": "Coil Value", + "generateCommand": "Generate", + "generatedCommand": "Generated Command", + "saveAsCommand": "Save As Command", + "copy": "Copy", + "cancel": "Cancel", + "confirm": "Confirm", + "commandName": "Command Name", + "inputCommandName": "Please enter command name", + "commandContent": "Command Content", + "commandScope": "Command Scope", + "productCommand": "Product Command (shared by same product)", + "deviceCommand": "Device Command (this device only)", + "deleteCommand": "Delete Command", + "deleteConfirm": "Are you sure to delete this command?", + "reportCommand": "Device Report", + "issueCommand": "Platform Issue", + "code01": "01 Read Coils", + "code02": "02 Read Discrete Inputs", + "code03": "03 Read Holding Registers", + "code04": "04 Read Input Registers", + "code05": "05 Write Single Coil", + "code06": "06 Write Single Register", + "code15": "15 Write Multiple Coils", + "code16": "16 Write Multiple Registers", + "sendEmpty": "Please enter the content to send", + "sendFail": "Send failed", + "analysisEmpty": "No data to parse", + "analysisFail": "Parse failed", + "generateFail": "Generate failed", + "saveSuccess": "Saved successfully", + "saveFail": "Save failed", + "deleteSuccess": "Deleted successfully", + "deleteFail": "Delete failed", + "copySuccess": "Copied successfully" + }, "about": { "about": "about", "authorQQ": "Author QQ : 164770707", @@ -744,7 +812,15 @@ }, "deviceHistory": { "lastTwoHours": "last 2 hours", + "lastFourHours": "last 4 hours", + "lastEightHours": "last 8 hours", + "lastTwelveHours": "last 12 hours", "lastOneDay": "last 1 day", + "lastTwoDays": "last 2 days", + "lastThreeDays": "last 3 days", + "lastFiveDays": "last 5 days", + "lastSevenDays": "last 7 days", + "lastFifteenDays": "last 15 days", "lastThirtyDay": "last 30 day", "custom": "custom", "inputDate": "starting time", diff --git a/yack-app/locale/zh-CN.json b/yack-app/locale/zh-CN.json index 9bcf2f3c..e0ff109a 100644 --- a/yack-app/locale/zh-CN.json +++ b/yack-app/locale/zh-CN.json @@ -46,6 +46,8 @@ "navBar.updateDevice": "设备修改", "navBar.eventLog": "事件日志", "navBar.orderLog": "指令日志", + "navBar.dataDebug": "数据调试", + "navBar.commandBuilder": "指令生成", "navBar.player": "播放器", "common.fastbee": "岩安测控", "common.add": "添加", @@ -241,6 +243,72 @@ "createBtn": "创建设备", "content": "该功能需要登录才可使用,是否去登录?" }, + "debug": { + "title": "数据调试", + "send": "发送", + "reveiceType": "回复", + "reportType": "上报", + "sendType": "下发", + "hex": "HEX", + "json": "JSON", + "ascii": "ASCII", + "inputPlaceholder": "请输入要发送的内容", + "typePlaceholder": "选择格式", + "analysis": "解析", + "analysised": "已解析", + "name": "参数名", + "identifier": "标识符", + "value": "参数值", + "more": "更多", + "quickCommand": "快捷指令", + "addCommand": "新建指令", + "editCommand": "编辑指令", + "slaveAddress": "从机地址", + "issueType": "指令类型", + "functionCode": "功能码", + "startAddress": "起始寄存器地址", + "registerCount": "寄存器个数", + "coilCount": "线圈个数", + "writeCoilValue": "线圈值", + "writeRegisterValue": "寄存器值", + "registerValue": "寄存器值", + "reportValue": "上报值", + "coilValue": "线圈值", + "generateCommand": "生成指令", + "generatedCommand": "生成的指令", + "saveAsCommand": "存为指令", + "copy": "复制", + "cancel": "取消", + "confirm": "确定", + "commandName": "指令名称", + "inputCommandName": "请输入指令名称", + "commandContent": "指令内容", + "commandScope": "指令范围", + "productCommand": "产品指令(同类产品通用)", + "deviceCommand": "设备指令(仅本设备)", + "deleteCommand": "删除指令", + "deleteConfirm": "确认删除该指令吗?", + "reportCommand": "设备上报", + "issueCommand": "平台下发", + "code01": "01 读线圈", + "code02": "02 读离散输入", + "code03": "03 读保持寄存器", + "code04": "04 读输入寄存器", + "code05": "05 写单个线圈", + "code06": "06 写单个寄存器", + "code15": "15 写多个线圈", + "code16": "16 写多个寄存器", + "sendEmpty": "请输入要发送的内容", + "sendFail": "发送失败", + "analysisEmpty": "暂无可解析的数据", + "analysisFail": "解析失败", + "generateFail": "生成失败", + "saveSuccess": "保存成功", + "saveFail": "保存失败", + "deleteSuccess": "删除成功", + "deleteFail": "删除失败", + "copySuccess": "复制成功" + }, "about": { "about": "关于", "authorQQ": "作者企鹅:164770707", @@ -746,7 +814,15 @@ }, "deviceHistory": { "lastTwoHours": "最近2小时", + "lastFourHours": "最近4小时", + "lastEightHours": "最近8小时", + "lastTwelveHours": "最近12小时", "lastOneDay": "最近1天", + "lastTwoDays": "最近2天", + "lastThreeDays": "最近3天", + "lastFiveDays": "最近5天", + "lastSevenDays": "最近7天", + "lastFifteenDays": "最近15天", "lastThirtyDay": "最近30天", "custom": "自定义", "inputDate": "起始时间", diff --git a/yack-app/pages.json b/yack-app/pages.json index 9cdedb1f..c4c217e0 100644 --- a/yack-app/pages.json +++ b/yack-app/pages.json @@ -533,6 +533,26 @@ "enablePullDownRefresh": true } }, + { + "path": "home/device/debug/index", + "meta": { + "title": "%navBar.dataDebug%" + }, + "style": { + "navigationBarTitleText": "%navBar.dataDebug%", + "enablePullDownRefresh": false + } + }, + { + "path": "home/device/debug/edit", + "meta": { + "title": "%navBar.commandBuilder%" + }, + "style": { + "navigationBarTitleText": "%navBar.commandBuilder%", + "enablePullDownRefresh": false + } + }, { "path": "login/register", "style": { diff --git a/yack-app/pages/tabBar/home/index.vue b/yack-app/pages/tabBar/home/index.vue index ddc65e4f..90122b0c 100644 --- a/yack-app/pages/tabBar/home/index.vue +++ b/yack-app/pages/tabBar/home/index.vue @@ -242,7 +242,8 @@ pageSize: 10, groupId: 0, deviceName: null, - showChild: true + showChild: true, + entrance: 2 }, loadStatus: 'loadmore', // 加载更多 deviceList: [], // 设备列表 @@ -364,7 +365,8 @@ // 获取分组列表 getGroups () { getGroupList({ - userId: this.profile.userId + userId: this.profile.userId, + pageSize: 99 // 一次性获取全部 }).then(res => { if (res.code === 200) { this.groupList = [{ diff --git a/yack-app/pagesA/home/device/index.vue b/yack-app/pagesA/home/device/index.vue index 95d4ef01..334b0a60 100644 --- a/yack-app/pagesA/home/device/index.vue +++ b/yack-app/pagesA/home/device/index.vue @@ -331,12 +331,17 @@ this.deviceForm.rssid = message.rssid; } }; - //不是modbus不转发到子页面,其他设备的页面有回调方法 - if (this.isSubDev) { - /*发送设备上报到子模块*/ - if (topic.endsWith('ws/service')) { - console.log('接收到【设备数据上报】主题:', topic) - console.log('接收到【设备数据上报】主题:', message) + /*发送设备上报到数据调试页面*/ + if (topic.endsWith('ws/service')) { + console.log('接收到【设备数据上报】主题:', topic) + console.log('接收到【设备数据上报】主题:', message) + this.$bus.$emit("updateData", { + serialNumber: topics[2], + productId: this.deviceForm.productId, + data: message, + }) + //不是modbus不转发到子页面,其他设备的页面有回调方法 + if (this.isSubDev) { this.$bus.$emit("updateDeviceStatus", { serialNumber: topics[2], productId: this.deviceForm.productId, diff --git a/yack-app/pagesA/home/device/log/index.vue b/yack-app/pagesA/home/device/log/index.vue index 859b80a5..5d0f9c63 100644 --- a/yack-app/pagesA/home/device/log/index.vue +++ b/yack-app/pagesA/home/device/log/index.vue @@ -6,6 +6,8 @@ @click="handleEventLogClick"> + @@ -58,6 +60,17 @@ uni.navigateTo({ url: `/pagesB/home/device/log/order?deviceId=${this.deviceId}&serialNumber=${this.serialNumber}` }); + }, + handleDebugClick () { + const device = { + deviceId: this.deviceInfo.deviceId, + serialNumber: this.deviceInfo.serialNumber, + productId: this.deviceInfo.productId, + protocolCode: this.deviceInfo.protocolCode, + }; + uni.navigateTo({ + url: `/pagesB/home/device/debug/index?device=${encodeURIComponent(JSON.stringify(device))}` + }); } } }; diff --git a/yack-app/pagesA/home/device/statistic/history.vue b/yack-app/pagesA/home/device/statistic/history.vue index 4ae8924b..53f387df 100644 --- a/yack-app/pagesA/home/device/statistic/history.vue +++ b/yack-app/pagesA/home/device/statistic/history.vue @@ -81,7 +81,7 @@ import uniPagination from '@/pagesA/components/uni-pagination/index.vue'; import { getSubGatewayList } from '@/apis/modules/gateway.js'; import { listThingsModel } from '@/apis/modules/device.js'; - import { getHistoryList } from '@/apis/modules/deviceLog.js'; + import { getHistoryList, getSubDeviceComparativeHistory, getDeviceHistoryCalculated } from '@/apis/modules/deviceLog.js'; export default { name: 'deviceHistory', @@ -118,27 +118,59 @@ return { deviceInfo: {}, // 设备信息 deviceId: null, - // 时间快捷数据列表 + // 时间快捷数据列表(value 为小时数,-1 表示自定义) timeShortcuts: [{ - value: 0, + value: 2, text: this.$tt('deviceHistory.lastTwoHours') }, { - value: 1, + value: 4, + text: this.$tt('deviceHistory.lastFourHours') + }, + { + value: 8, + text: this.$tt('deviceHistory.lastEightHours') + }, + { + value: 12, + text: this.$tt('deviceHistory.lastTwelveHours') + }, + { + value: 24, text: this.$tt('deviceHistory.lastOneDay') }, { - value: 2, + value: 48, + text: this.$tt('deviceHistory.lastTwoDays') + }, + { + value: 72, + text: this.$tt('deviceHistory.lastThreeDays') + }, + { + value: 120, + text: this.$tt('deviceHistory.lastFiveDays') + }, + { + value: 168, + text: this.$tt('deviceHistory.lastSevenDays') + }, + { + value: 360, + text: this.$tt('deviceHistory.lastFifteenDays') + }, + { + value: 720, text: this.$tt('deviceHistory.lastThirtyDay') }, { - value: 3, + value: -1, text: this.$tt('deviceHistory.custom') }, ], // 查询参数 queryParams: { - timeShortcut: 1, + timeShortcut: 24, strTime: `${beginTime}-${endTime}`, beginTime: beginTime, endTime: endTime, @@ -201,20 +233,13 @@ }, }, methods: { - // 快捷时间选择 + // 快捷时间选择(val 为小时数,-1 表示自定义) handleTimeShortcutChange (val) { - if (val === 3) { + if (val === -1) { return; } const endTime = new Date().getTime(); - let beginTime = null; - if (val === 0) { - beginTime = endTime - 3600 * 1000 * 2; - } else if (val === 1) { - beginTime = endTime - 3600 * 1000 * 24; - } else if (val === 2) { - beginTime = endTime - 3600 * 1000 * 24 * 30; - }; + const beginTime = endTime - 3600 * 1000 * val; this.queryParams.beginTime = beginTime; this.queryParams.endTime = endTime; this.queryParams.strTime = beginTime + '-' + endTime; @@ -263,7 +288,7 @@ this.queryParams.strTime = beginTime + '-' + e.value; this.getHistory(); this.isDateTime = false; - this.queryParams.timeShortcut = 3; + this.queryParams.timeShortcut = -1; } }, // 取消日期选择 @@ -322,9 +347,9 @@ } }); }, - // 获取物模型 + // 获取物模型(网关设备按identifier去重) getIdentifierList () { - const { deviceId } = this.deviceInfo; + const { deviceId, deviceType } = this.deviceInfo; const params = { deviceId: deviceId, pageNum: 1, @@ -339,11 +364,27 @@ text: item.modelName, value: item.identifier, type: item.type, + isCalculated: item.isCalculated, // 是否计算物模型 + slaveId: item.slaveId, // 子设备从机号(网关自身物模型为空) } } - }); - this.identifierList = this.identifierList.filter(item => item !== undefined); - this.queryParams.identifiers = [this.identifierList[0].value]; + }).filter(item => item !== undefined); + + // 网关设备:按identifier去重(多个子设备可能有相同identifier) + if (deviceType === 2) { + const seen = new Set(); + this.identifierList = this.identifierList.filter(item => { + if (seen.has(item.value)) { + return false; + } + seen.add(item.value); + return true; + }); + } + + if (this.identifierList.length > 0) { + this.queryParams.identifiers = [this.identifierList[0].value]; + } } }); }, @@ -354,68 +395,215 @@ }, // 获取统计数据 getHistory () { - const { identifiers, beginTime, endTime, slaveId } = this.queryParams; - const { deviceId, serialNumber } = this.deviceInfo; + const { identifiers, beginTime, endTime } = this.queryParams; + const { deviceId, serialNumber, deviceType } = this.deviceInfo; if (identifiers.length !== 0) { - const idenList = identifiers.map((item) => { - const iden = this.identifierList.find((chil) => chil.value === item); + this.historys = []; + const beginTimeStr = moment(beginTime).format('YYYY-MM-DD HH:mm:ss'); + const endTimeStr = moment(endTime).format('YYYY-MM-DD HH:mm:ss'); + if (deviceType === 2) { + this.getGatewayHistory(identifiers, deviceId, serialNumber, beginTimeStr, endTimeStr); + } else { + // 直连设备 + const idenList = identifiers.map((item) => { + const iden = this.identifierList.find((chil) => chil.value === item); + return { + identifier: iden.value, + type: iden.type + }; + }); + const params = { + identifierList: idenList, + beginTime: beginTimeStr, + endTime: endTimeStr, + deviceId: deviceId, + serialNumber: serialNumber, + }; + getHistoryList(params).then(res => { + if (res.code === 200) { + this.historys = res.data; + this.chartData = this.getChartDatas(); + this.getTableDatas(); + } + }) + } + } + }, + // 网关设备历史查询:普通变量走多设备对比接口,计算物模型走计算接口,结果合并 + getGatewayHistory (identifiers, deviceId, serialNumber, beginTimeStr, endTimeStr) { + // 计算物模型:isCalculated=1 且属于网关自身产品(无slaveId) + const calculatedItems = identifiers.filter((id) => { + const iden = this.identifierList.find((chil) => chil.value === id); + return iden && iden.isCalculated === 1 && (iden.slaveId === null || iden.slaveId === undefined || iden.slaveId === ''); + }); + const normalIdentifiers = identifiers.filter((id) => !calculatedItems.includes(id)); + + const tasks = []; + if (normalIdentifiers.length > 0) { + // 普通变量:多设备对比接口(数据按子设备SN存储) + tasks.push(getSubDeviceComparativeHistory({ + gwDeviceId: deviceId, + identifierList: normalIdentifiers, + type: 1, + beginTime: beginTimeStr, + endTime: endTimeStr, + }).then(res => ({ + type: 'compare', + data: res.code === 200 ? (res.data || []) : [] + }))); + } + if (calculatedItems.length > 0) { + // 计算物模型:查询时按公式计算 + const idenList = calculatedItems.map((id) => { + const iden = this.identifierList.find((chil) => chil.value === id); return { identifier: iden.value, type: iden.type }; }); - const params = { + tasks.push(getDeviceHistoryCalculated({ identifierList: idenList, - beginTime: moment(beginTime).format('YYYY-MM-DD HH:mm:ss'), - endTime: moment(endTime).format('YYYY-MM-DD HH:mm:ss'), + beginTime: beginTimeStr, + endTime: endTimeStr, deviceId: deviceId, - // slaveId: slaveId, serialNumber: serialNumber, - }; - this.historys = []; - getHistoryList(params).then(res => { - if (res.code === 200) { - this.historys = res.data; - this.chartData = this.getChartDatas(); - this.getTableDatas(); + }).then(res => ({ + type: 'calculated', + data: res.code === 200 ? (res.data || []) : [] + }))); + } + if (tasks.length === 0) { + return; + } + + Promise.all(tasks).then((results) => { + // 统一转换为 [{ time, 列名: 值 }] 格式后按时间合并 + const rowMap = new Map(); + results.forEach((result) => { + if (result.type === 'compare') { + // subDeviceComparativeHistory 格式: [{ "time": "...", "子设备A(从机1)": "25.5" }] + result.data.forEach((row) => { + if (!row.time) { + return; + } + if (!rowMap.has(row.time)) { + rowMap.set(row.time, { time: row.time }); + } + const target = rowMap.get(row.time); + Object.keys(row).forEach((k) => { + if (k !== 'time') { + target[k] = row[k]; + } + }); + }); + } else { + // deviceHistoryCalculated 格式: [{ "2026-08-23 10:00:00": [{ "calcVar": "12.3" }] }] + result.data.forEach((item) => { + const time = Object.keys(item)[0]; + if (!time) { + return; + } + if (!rowMap.has(time)) { + rowMap.set(time, { time }); + } + const target = rowMap.get(time); + Object.values(item)[0].forEach((chil) => { + const id = Object.keys(chil)[0]; + const iden = this.identifierList.find((m) => m.value === id); + // 计算物模型列名使用变量名 + target[iden ? iden.text : id] = Object.values(chil)[0]; + }); + }); } - }) - } - }, - // 获取折线图数据 - getChartDatas () { - const categories = this.historys.map((item) => Object.keys(item)[0]); - const series = this.queryParams.identifiers.map((item) => { - return { - name: this.identifierList.find((chil) => chil.value === item).text, - data: this.historys.map((d) => { - const ide = Object.values(d)[0].find((f) => Object.keys(f)[0] === item); - return Number(Object.values(ide)[0]); - }), - }; - }); - return { - categories, - series - } - }, - // 获取表格数据 - getTableDatas () { - this.tableHeaders = this.queryParams.identifiers.map((item) => ({ - name: this.identifierList.find((chil) => chil.value === item).text, - value: item, - })); - this.tableComTemp = this.historys.map((item) => { - const time = Object.keys(item)[0]; - let obj = {}; - Object.values(item)[0].forEach((chil) => { - obj[Object.keys(chil)[0]] = Object.values(chil)[0]; }); - return { - time, - ...obj - }; + // 按时间升序排序 + this.historys = Array.from(rowMap.values()).sort((a, b) => (a.time < b.time ? -1 : 1)); + this.chartData = this.getChartDatas(); + this.getTableDatas(); }); + }, + // 获取折线图数据(适配两种接口响应格式) + getChartDatas () { + const { deviceType } = this.deviceInfo; + if (deviceType === 2) { + // 网关:合并后的格式 [{ "time": "...", "子设备A(从机1)": "25.5", "计算变量": "12.3" }] + if (this.historys.length === 0) { + return { categories: [], series: [] }; + } + const categories = this.historys.map((item) => item.time); + // 从所有行收集列名(避免第一行缺少某些列) + const keys = []; + this.historys.forEach((item) => Object.keys(item).forEach((k) => { + if (k !== 'time' && !keys.includes(k)) { + keys.push(k); + } + })); + const series = keys.map((key) => { + return { + name: key, + data: this.historys.map((d) => Number(d[key]) || 0), + }; + }); + return { categories, series }; + } else { + // deviceHistory 格式: [{ "2026-08-23 10:00:00": [{ "temperature": "25.5" }] }] + const categories = this.historys.map((item) => Object.keys(item)[0]); + const series = this.queryParams.identifiers.map((item) => { + return { + name: this.identifierList.find((chil) => chil.value === item).text, + data: this.historys.map((d) => { + const ide = Object.values(d)[0].find((f) => Object.keys(f)[0] === item); + return Number(Object.values(ide)[0]); + }), + }; + }); + return { categories, series }; + } + }, + // 获取表格数据(适配两种接口响应格式) + getTableDatas () { + const { deviceType } = this.deviceInfo; + if (deviceType === 2) { + // 网关:合并后的格式 [{ "time": "...", "子设备A(从机1)": "25.5", "计算变量": "12.3" }] + // 从所有行收集列名(避免第一行缺少某些列) + const keys = []; + this.historys.forEach((item) => Object.keys(item).forEach((k) => { + if (k !== 'time' && !keys.includes(k)) { + keys.push(k); + } + })); + this.tableHeaders = keys.map((key) => ({ + name: key, + value: key, + })); + this.tableComTemp = this.historys.map((item) => { + let obj = {}; + keys.forEach((key) => { + obj[key] = item[key]; + }); + return { + time: item.time, + ...obj + }; + }); + } else { + // deviceHistory 格式 + this.tableHeaders = this.queryParams.identifiers.map((item) => ({ + name: this.identifierList.find((chil) => chil.value === item).text, + value: item, + })); + this.tableComTemp = this.historys.map((item) => { + const time = Object.keys(item)[0]; + let obj = {}; + Object.values(item)[0].forEach((chil) => { + obj[Object.keys(chil)[0]] = Object.values(chil)[0]; + }); + return { + time, + ...obj + }; + }); + } this.devTotal = this.historys.length; }, // 获取设备列表数据 diff --git a/yack-app/pagesB/home/device/debug/edit.vue b/yack-app/pagesB/home/device/debug/edit.vue new file mode 100644 index 00000000..0861df5f --- /dev/null +++ b/yack-app/pagesB/home/device/debug/edit.vue @@ -0,0 +1,615 @@ + + + + + + + \ No newline at end of file diff --git a/yack-app/pagesB/home/device/debug/index.vue b/yack-app/pagesB/home/device/debug/index.vue new file mode 100644 index 00000000..321ab823 --- /dev/null +++ b/yack-app/pagesB/home/device/debug/index.vue @@ -0,0 +1,655 @@ + + + + + + + \ No newline at end of file diff --git a/yack-app/utils/common.js b/yack-app/utils/common.js index f6a27579..88f53543 100644 --- a/yack-app/utils/common.js +++ b/yack-app/utils/common.js @@ -26,7 +26,7 @@ export function tansParams (params) { /** * 参数处理,避免小程序跳转10层后无法跳转 - * @param url + * @param url */ export function navigateBackTo (url) { const backUrl = url.slice(1); @@ -35,4 +35,130 @@ export function navigateBackTo (url) { const index = pages.findIndex(item => item.route === backUrl); const backnum = num - index - 1; uni.navigateBack({ delta: backnum }); +} + +/** + * 深复制 + * @param {any} obj 需要复制的对象 + */ +export function deepClone (obj) { + if (typeof obj == 'object') { + const objClone = Array.isArray(obj) ? [] : {} + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + if (obj[key] && typeof obj[key] == 'object') { + objClone[key] = deepClone(obj[key]) + } else { + objClone[key] = obj[key] + } + } + } + return objClone + } + return obj +} + +/** + * @desc 时间戳转日期毫秒 + * @param {Date} format 时间格式 Y:年,M:月,D:天 h:小时,m:分钟,s:秒 默认:'Y/M/D h:m' + */ +export function formatDate (arg, format) { + let now + if (!arg) { + return null + } else { + now = new Date(arg) + } + format = format || 'Y.M.D h:m' + + const year = now.getFullYear() + let month = now.getMonth() + 1 + month = month >= 10 ? month : '0' + month + let date = now.getDate() + date = date >= 10 ? date : '0' + date + let hour = now.getHours() + hour = hour >= 10 ? hour : '0' + hour + let minute = now.getMinutes() + minute = minute >= 10 ? minute : '0' + minute + let second = now.getSeconds() + second = second >= 10 ? second : '0' + second + return format + .replace('Y', year) + .replace('M', month) + .replace('D', date) + .replace('h', hour) + .replace('m', minute) + .replace('s', second) +} + +/** + * 复制文本(uni-app 剪贴板) + * @param {string} copytext + */ +export function copyText (copytext) { + uni.setClipboardData({ + data: copytext, + success: () => {}, + }) + return { type: 'success', message: '复制成功' } +} + +/** + * hex转10进制 + * @param hexStr + */ +export function hex2int (hexStr) { + let twoStr = parseInt(hexStr, 16).toString(2) // 将十六转十进制,再转2进制 + let bitNum = hexStr.length * 4 // 1个字节 = 8bit ,0xff 一个 "f"就是4位 + if (twoStr.length < bitNum) { + while (twoStr.length < bitNum) { + twoStr = "0" + twoStr + } + } + if (twoStr.substring(0, 1) == "0") { + // 正数 + const two = parseInt(twoStr, 2) // 二进制转十进制 + return two + } else { + // 负数 + let twoStr_unsign = "" + let two = parseInt(twoStr, 2) - 1 // 补码:(负数)反码+1,符号位不变;相对十进制来说也是 +1,但这里是负数,+1就是绝对值数据-1 + twoStr = two.toString(2) + twoStr_unsign = twoStr.substring(1, bitNum) // 舍弃首位(符号位) + // 去除首字符,将0转为1,将1转为0 反码 + twoStr_unsign = twoStr_unsign.replace(/0/g, "z") + twoStr_unsign = twoStr_unsign.replace(/1/g, "0") + twoStr_unsign = twoStr_unsign.replace(/z/g, "1") + two = -parseInt(twoStr_unsign, 2) + return two + } +} + +/** + * 10进制转16进制 + * @param hexStr + */ +export function int2hex (hexStr) { + let hexInt = typeof hexStr == 'string' ? parseInt(hexStr, 10) : hexStr + let hex = '' + const LENGTH_04 = 4 + const LENGTH_15 = 15 + if (hexInt >= 0) { + hex = hexInt.toString(16).toLocaleLowerCase() + } else { + let twoStr = (-hexInt - 1).toString(2) + let sb = '000000000000000' + twoStr = sb.slice(0, LENGTH_15 - twoStr.length) + twoStr + // 将0转为1,将1转为0 反码 + twoStr = twoStr.replace(/0/g, "z") + twoStr = twoStr.replace(/1/g, "0") + twoStr = twoStr.replace(/z/g, "1") + hex = parseInt(('1' + twoStr), 2).toString(16).toLocaleLowerCase() + } + // 长度不等于4 补全 + if (hex.length != LENGTH_04) { + let sb = "0000" + hex = sb.slice(0, LENGTH_04 - hex.length) + hex + } + return hex.toUpperCase() } \ No newline at end of file diff --git a/yack-web/src/api/platform/deviceMapping.js b/yack-web/src/api/platform/deviceMapping.js index 380ee19f..064ec002 100644 --- a/yack-web/src/api/platform/deviceMapping.js +++ b/yack-web/src/api/platform/deviceMapping.js @@ -28,6 +28,16 @@ export function listSubMappings(parentId) { return request({ url: `/platform/device-mapping/${parentId}/sub-mappings`, method: 'get' }) } +// 批量查询多个设备绑定的子设备映射,列表页展开行一次拉取,避免 N 次请求 +export function listSubMappingsBatch(parentIds) { + return request({ url: '/platform/device-mapping/sub-mappings/batch', method: 'get', params: { parentIds: parentIds.join(',') } }) +} + export function syncSubMappings(parentId, data) { return request({ url: `/platform/device-mapping/${parentId}/sub-mappings`, method: 'post', data }) } + +// 查询已配置的静力水准基准点(跨网关基准点下拉框数据源) +export function listStaticLevelReferencePoints(params) { + return request({ url: '/platform/device-mapping/static-level/reference-points', method: 'get', params }) +} diff --git a/yack-web/src/views/iot/device/instruction-parsing.vue b/yack-web/src/views/iot/device/instruction-parsing.vue index 39590abd..77e8d9c1 100644 --- a/yack-web/src/views/iot/device/instruction-parsing.vue +++ b/yack-web/src/views/iot/device/instruction-parsing.vue @@ -5,6 +5,7 @@

{{ $t('device.instruction-parsing.830424-0') }} {{ device.serialNumber }} + 清屏 @@ -961,6 +962,10 @@ export default { }); } }, + /**清屏 */ + clearLogList() { + this.logList = []; + }, }, mounted() { this.resetCreateForm(); @@ -1031,6 +1036,17 @@ $right-btn-color: #1890ff; font-size: 12px; border-bottom: 1px solid $border-color; + .clear-btn { + margin-left: auto; + color: #909399; + padding: 0; + font-size: 12px; + + &:hover { + color: #f56c6c; + } + } + .el-dropdown-link { cursor: pointer; } @@ -1307,5 +1323,13 @@ $right-btn-color: #1890ff; } } } + + /* 如果 style 有 scoped,请用 ::v-deep */ + ::v-deep .red-btn { + color: #F56C6C !important; + } + ::v-deep .red-btn:hover { + color: #FF0000 !important; /* 悬停时更亮 */ + } } diff --git a/yack-web/src/views/iot/device/product-list.vue b/yack-web/src/views/iot/device/product-list.vue index 189f9cd0..5488304b 100644 --- a/yack-web/src/views/iot/device/product-list.vue +++ b/yack-web/src/views/iot/device/product-list.vue @@ -2,7 +2,7 @@
- + diff --git a/yack-web/src/views/iot/scene/device-list.vue b/yack-web/src/views/iot/scene/device-list.vue index 05419808..070e3b8e 100644 --- a/yack-web/src/views/iot/scene/device-list.vue +++ b/yack-web/src/views/iot/scene/device-list.vue @@ -78,6 +78,8 @@ export default { productName: null, serialNumber: null, status: null, + // 需要查询全量设备(包括子账号) + showChild: true }, }; }, @@ -132,15 +134,6 @@ export default { // 存在且取消选中 this.selectDeviceNums.splice(index, 1); } - - // 筛选产品下的设备比 - if (this.selectDeviceNums.length == 0) { - this.queryParams.productId = null; - this.getList(); - } else if (this.selectDeviceNums.length == 1) { - this.queryParams.productId = row.productId; - this.getList(); - } }, // 全选事件处理 handleSelectionAll(selection) { diff --git a/yack-web/src/views/iot/scene/product-list.vue b/yack-web/src/views/iot/scene/product-list.vue index 3902f424..211a33f0 100644 --- a/yack-web/src/views/iot/scene/product-list.vue +++ b/yack-web/src/views/iot/scene/product-list.vue @@ -3,7 +3,7 @@ :close-on-click-modal="false">
- + diff --git a/yack-web/src/views/iot/scene/script.vue b/yack-web/src/views/iot/scene/script.vue index aa9da68b..fef50256 100644 --- a/yack-web/src/views/iot/scene/script.vue +++ b/yack-web/src/views/iot/scene/script.vue @@ -118,6 +118,20 @@ {{ $t('script.349087-31') }} + + + + + + 选择设备 + + +
+ 已选 {{ selectedDeviceCount }} 个设备 +
+
+
+
@@ -164,6 +178,8 @@ + + @@ -175,6 +191,7 @@ import { listScript, getScript, getScriptLog, delScript, addScript, updateScript, validateScript} from '@/api/iot/script'; import AceEditor from '@/views/components/editor/editor.vue'; import productList from './product-list'; +import deviceList from './device-list'; import clientList from './client-list'; import bridgeList from '@/views/iot/scene/bridge-list.vue'; @@ -185,6 +202,7 @@ export default { bridgeList, AceEditor, productList, + deviceList, clientList, }, data() { @@ -208,6 +226,10 @@ export default { loading: true, // 日志遮罩层 logLoading: true, + // 设备选择显示文本 + deviceDisplayText: '', + // 已选设备数量 + selectedDeviceCount: 0, // 选中数组 scriptIds: [], // 非单个禁用 @@ -283,12 +305,15 @@ export default { reset() { this.validateMsg = ''; this.isValidate = false; + this.deviceDisplayText = ''; + this.selectedDeviceCount = 0; this.form = { id: null, applicationName: 'fastbee', // 后端配置和规则/脚本需要一致 scriptId: null, productId: null, productName: '', + serialNumbers: '', bridgeName: '', scriptName: null, scriptType: 'script', @@ -350,6 +375,12 @@ export default { const scriptId = row.scriptId || this.scriptIds; getScript(scriptId).then((response) => { this.form = response.data; + // 恢复设备选择显示 + if (this.form.serialNumbers) { + const devices = this.form.serialNumbers.split(',').filter(s => s); + this.selectedDeviceCount = devices.length; + this.deviceDisplayText = `已选 ${devices.length} 个设备`; + } this.open = true; this.title = this.$t('script.349087-26'); }); @@ -387,6 +418,32 @@ export default { getSelectProduct(data) { this.form.productId = data.productId; this.form.productName = data.productName; + // 产品变更时清空设备选择 + this.clearDeviceSelect(); + }, + /**选择设备 */ + handleSelectDevice() { + this.$refs.deviceListRef.queryParams.productId = this.form.productId; + this.$refs.deviceListRef.selectDeviceNums = this.form.serialNumbers ? this.form.serialNumbers.split(',') : []; + this.$refs.deviceListRef.openDeviceList = true; + this.$refs.deviceListRef.getList(); + }, + /**设备选择回调 */ + getSelectDevice(data) { + this.form.serialNumbers = data.deviceNums.join(','); + this.selectedDeviceCount = data.deviceNums.length; + this.deviceDisplayText = `已选 ${data.deviceNums.length} 个设备`; + // 如果选择了设备,更新产品信息(确保产品ID一致) + if (data.productId && !this.form.productId) { + this.form.productId = data.productId; + this.form.productName = data.productName; + } + }, + /**清空设备选择 */ + clearDeviceSelect() { + this.form.serialNumbers = ''; + this.selectedDeviceCount = 0; + this.deviceDisplayText = ''; }, /**选择桥接接入点 */ handleSelectBridge() { diff --git a/yack-web/src/views/platform/device-mapping/index.vue b/yack-web/src/views/platform/device-mapping/index.vue index 0b5f70e8..6f3814d6 100644 --- a/yack-web/src/views/platform/device-mapping/index.vue +++ b/yack-web/src/views/platform/device-mapping/index.vue @@ -284,6 +284,17 @@ 秒 + + + + +
+ 本网关子设备全部为监测点时使用:选择其他网关已配置的基准点后,本网关子设备无需再指定基准点。 +
+
@@ -335,7 +346,7 @@