From 86b85f23272548df2b86f2fa096f205ac85bfea7 Mon Sep 17 00:00:00 2001 From: zhugongping Date: Mon, 21 Sep 2026 09:29:15 +0800 Subject: [PATCH] =?UTF-8?q?reportEngine=EF=BC=9A=E5=AE=8C=E5=96=84?= =?UTF-8?q?=E6=8A=A5=E5=91=8A=E5=AF=BC=E5=87=BA=EF=BC=8C=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E9=A2=84=E8=AD=A6=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker/docker-compose-test.yml | 4 +- docker/docker-compose-txy.yaml | 27 + docs/fastbee-device-report-engine.md | 10 +- .../com/fastbee/web/task/LogCleanTask.java | 11 +- .../controller/report/ReportController.java | 5 - ...er.java => ThingsThresholdController.java} | 44 +- .../report/analysis/QualityAnalyzer.java | 21 +- .../report/analysis/TrendAnalyzer.java | 3 +- .../report/analysis/WarningAnalyzer.java | 136 +++++- .../analyzer/AbstractDeviceAnalyzer.java | 121 ++++- .../com/fastbee/report/api/ReportResult.java | 6 - .../fastbee/report/core/ReportContext.java | 12 +- .../report/core/ReportContextBuilder.java | 22 +- ...ldQuery.java => ThingsThresholdQuery.java} | 4 +- .../report/executor/ReportAsyncExecutor.java | 5 +- .../report/mapper/ReportRecordMapper.java | 10 +- ...Mapper.java => ThingsThresholdMapper.java} | 8 +- .../fastbee/report/record/ReportRecord.java | 3 - .../report/record/ReportRecordService.java | 15 +- .../record/ReportRecordServiceImpl.java | 11 +- .../fastbee/report/renderer/WordRenderer.java | 10 +- ...vice.java => IThingsThresholdService.java} | 16 +- ...l.java => ThingsThresholdServiceImpl.java} | 36 +- ...er.java => DbThingsThresholdProvider.java} | 12 +- ...ortThreshold.java => ThingsThreshold.java} | 21 +- ...ider.java => ThingsThresholdProvider.java} | 6 +- .../mapper/report/ReportRecordMapper.xml | 38 +- ...ldMapper.xml => ThingsThresholdMapper.xml} | 13 +- .../java/com/fastbee/iot/domain/Device.java | 13 + .../resources/mapper/iot/DeviceMapper.xml | 5 +- sql/fastbee-report.sql | 11 +- sql/init_report_threshold.sql | 55 --- sql/init_things_threshold.sql | 110 +++++ sql/init_threshold_with_table.sql | 77 --- sql/log_clean_task_init.sql | 4 +- sql/menu_threshold.sql | 37 -- sql/platform-dcp-init-data.sql | 460 ------------------ yack-web/src/lang/en-US/device.json | 1 + yack-web/src/lang/en-US/product.json | 1 + yack-web/src/lang/zh-CN/device.json | 1 + yack-web/src/lang/zh-CN/product.json | 3 +- .../src/views/dataCenter/reportHistory.vue | 6 +- .../views/iot/components/threshold-config.vue | 301 ++++++++++++ yack-web/src/views/iot/device/device-edit.vue | 80 ++- .../src/views/iot/product/product-edit.vue | 13 + 45 files changed, 978 insertions(+), 830 deletions(-) create mode 100644 docker/docker-compose-txy.yaml rename fastbee-open-api/src/main/java/com/fastbee/controller/report/{ReportThresholdController.java => ThingsThresholdController.java} (80%) rename fastbee-report/src/main/java/com/fastbee/report/domain/vo/{ReportThresholdQuery.java => ThingsThresholdQuery.java} (86%) rename fastbee-report/src/main/java/com/fastbee/report/mapper/{ReportThresholdMapper.java => ThingsThresholdMapper.java} (72%) rename fastbee-report/src/main/java/com/fastbee/report/service/{IReportThresholdService.java => IThingsThresholdService.java} (65%) rename fastbee-report/src/main/java/com/fastbee/report/service/impl/{ReportThresholdServiceImpl.java => ThingsThresholdServiceImpl.java} (53%) rename fastbee-report/src/main/java/com/fastbee/report/threshold/{DbReportThresholdProvider.java => DbThingsThresholdProvider.java} (50%) rename fastbee-report/src/main/java/com/fastbee/report/threshold/{ReportThreshold.java => ThingsThreshold.java} (54%) rename fastbee-report/src/main/java/com/fastbee/report/threshold/{ReportThresholdProvider.java => ThingsThresholdProvider.java} (55%) rename fastbee-report/src/main/resources/mapper/report/{ReportThresholdMapper.xml => ThingsThresholdMapper.xml} (74%) delete mode 100644 sql/init_report_threshold.sql create mode 100644 sql/init_things_threshold.sql delete mode 100644 sql/init_threshold_with_table.sql delete mode 100644 sql/menu_threshold.sql delete mode 100644 sql/platform-dcp-init-data.sql create mode 100644 yack-web/src/views/iot/components/threshold-config.vue diff --git a/docker/docker-compose-test.yml b/docker/docker-compose-test.yml index e5b97b86..417b7919 100644 --- a/docker/docker-compose-test.yml +++ b/docker/docker-compose-test.yml @@ -18,8 +18,8 @@ services: - "9000:9000" - "9001:9001" environment: - MINIO_ROOT_USER: yack-superme - MINIO_ROOT_PASSWORD: yack-minio20260816 + MINIO_ROOT_USER: supermeminio + MINIO_ROOT_PASSWORD: minio20260909 MINIO_SERVER_URL: "https://oss.ycdat.cn:9000" # ✅ 指定 API 外部访问地址 volumes: - ./minio/data:/data diff --git a/docker/docker-compose-txy.yaml b/docker/docker-compose-txy.yaml new file mode 100644 index 00000000..7703f367 --- /dev/null +++ b/docker/docker-compose-txy.yaml @@ -0,0 +1,27 @@ +networks: + network: + ipam: + driver: default + config: + - subnet: '176.6.0.0/16' + +services: + minio: + image: minio/minio:latest + container_name: minio + restart: always + # privileged: true 它几乎拥有与宿主机 root 相同的所有权限;MinIO 是纯用户空间的对象存储服务;无需用到特权模式,故而安全性与最小权限原则 + networks: + network: + ipv4_address: 177.7.0.6 + ports: + - "9000:9000" + - "9001:9001" + environment: + MINIO_ROOT_USER: supermeminio + MINIO_ROOT_PASSWORD: minio20260909 + MINIO_SERVER_URL: "https://oss.ycdat.cn:9000" # ✅ 指定 API 外部访问地址 + volumes: + - ./minio/data:/data + - ./minio/certs:/root/.minio/certs # ✅:挂载证书目录 + command: server /data --console-address ":9001" \ No newline at end of file diff --git a/docs/fastbee-device-report-engine.md b/docs/fastbee-device-report-engine.md index de2e817f..4d0d579c 100644 --- a/docs/fastbee-device-report-engine.md +++ b/docs/fastbee-device-report-engine.md @@ -40,7 +40,7 @@ 1. `fastbee-report` 模块:报告引擎、Analyzer、`report_record` 表(均不存在)。 2. 设备监测分类路由:`DeviceType` 是连接拓扑(直连/网关/摄像头),`iot_category` 是租户商务分类树,均不能作为分析路由键 → 需 `iot_product.monitor_category_code` + `MonitorCategory` 枚举。 3. 报告周期/时间窗口:`ReportPeriod` 枚举 + `ReportPeriodResolver`。 -4. 结构化报告阈值:告警阈值藏在 LiteFlow `iot_scene` 脚本里,报告不可直接读 → 新增 `iot_report_threshold`。 +4. 结构化报告阈值:告警阈值藏在 LiteFlow `iot_scene` 脚本里,报告不可直接读 → 新增 `iot_things_threshold`。 5. Java 端图表(JFreeChart)与 Word 模板引擎(poi-tl)。 6. MinIO 默认存储配置。 7. 前端两处「生成并导出报表」按钮 + 历史报表页(列表/下载/过期)。 @@ -116,7 +116,7 @@ fastbee-report/ ├── period/ ReportPeriodResolver(+Default), TimeWindow ├── analyzer/ ReportAnalyzer, ReportAnalyzerFactory(+Default), crack/CrackMeterAnalyzer ├── analysis/ StatisticsCalculator, TrendAnalyzer, RateCalculator, QualityAnalyzer, WarningAnalyzer -├── threshold/ ReportThresholdProvider(+Db 实现), ReportThreshold +├── threshold/ ThingsThresholdProvider(+Db 实现), ThingsThreshold ├── data/ ReportDataProvider, MetricData ├── model/ ReportModel, AnalysisResult, Statistics, TrendAnalysis, QualityAnalysis, WarningAnalysis, ChartData ├── chart/ ChartGenerator(JFreeChart 生成 PNG) @@ -157,7 +157,7 @@ fastbee-report/ ## 3.5 阈值(WarningAnalyzer) - 现状:告警阈值藏在 LiteFlow `iot_scene` 脚本里,报告引擎直接解析成本高、易错。 -- V1:新增 `iot_report_threshold` 表(字段:product_id、device_id、monitor_category_code、metric_identifier、warning_value、alarm_value、control_value、unit)。 +- V1:新增 `iot_things_threshold` 表(字段:product_id、device_id、monitor_category_code、metric_identifier、warning_value、alarm_value、control_value、unit)。 - 查询优先级:`device_id > product_id > 全局默认(product_id=0)`。Analyzer 只读结构化阈值,**禁止硬编码阈值**。 ## 3.6 分析能力(通用 + 专项) @@ -362,7 +362,7 @@ POST /iot/report/generate | 对象 | 说明 | |---|---| | `iot_product` 加列 | `monitor_category_code VARCHAR(64)` 可空;存量按产品名回填(如 `UPDATE iot_product SET monitor_category_code='CRACK_METER' WHERE product_name LIKE '%裂缝%'`) | -| `iot_report_threshold` | 报告阈值表(product/device 级:预警/报警/控制值) | +| `iot_things_threshold` | 物模型阈值表(product/device 级:预警/报警/控制值 + 速率双控) | | `report_template` | Word 模板表(template_code + version 唯一) | | `report_record` | 报告记录表(含 `expire_time`) | | `oss_config` 插入 | minio 默认配置记录(见 4.3) | @@ -399,7 +399,7 @@ CREATE TABLE report_record ( 3. **分类与周期**:`MonitorCategoryResolver`、`ReportPeriodResolver` + 边界单测。 4. **Analyzer 框架 + 裂缝计**:先只实现 `CrackMeterAnalyzer`。 5. **通用分析**:`StatisticsCalculator`/`TrendAnalyzer`/`RateCalculator`/`QualityAnalyzer`/`WarningAnalyzer`。 -6. **Provider**:`ReportDataProvider`(委托 `DeviceLogMapper`)、`ReportThresholdProvider`。 +6. **Provider**:`ReportDataProvider`(委托 `DeviceLogMapper`)、`ThingsThresholdProvider`。 7. **渲染**:`ChartGenerator`(JFreeChart)+ `WordRenderer`(poi-tl)+ 模板 `DEVICE_CRACK_METER_V1.docx`。 8. **落库与下载**:`report_record` + `expire_time`;MinIO 生命周期规则;下载接口(校验过期)。 9. **前端**:大屏分析模式 + 数据中心加「生成并导出报表」;历史报表页 + 下载。 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 index d0ae5e0c..475419f5 100644 --- a/fastbee-admin/src/main/java/com/fastbee/web/task/LogCleanTask.java +++ b/fastbee-admin/src/main/java/com/fastbee/web/task/LogCleanTask.java @@ -5,6 +5,7 @@ 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.report.record.ReportRecordService; import com.fastbee.system.service.ISysLogininforService; import com.fastbee.system.service.ISysOperLogService; import org.slf4j.Logger; @@ -42,6 +43,9 @@ public class LogCleanTask { @Autowired private IPlatformPushLogService platformPushLogService; + @Autowired + private ReportRecordService reportRecordService; + // 之后还有清理的表往下写 /** @@ -83,12 +87,17 @@ public class LogCleanTask { notifyLogService.cleanNotifyLog(days); //log.info("已清理 notify_log 表 {} 天前的数据", days); + // 平台对接推送日志 platformPushLogService.cleanPushLog(days); + // 清理过期报告记录(report_record 不再保留 expire_time 字段,按 create_time 倒推清理) + int reportDeleted = reportRecordService.cleanExpired(days); + log.info("已清理 report_record 表 {} 天前的数据,删除 {} 条", days, reportDeleted); + log.info("========== 日志清理任务执行完成 =========="); } catch (Exception e) { log.error("日志清理任务执行失败", e); throw e; } } -} \ No newline at end of file +} diff --git a/fastbee-open-api/src/main/java/com/fastbee/controller/report/ReportController.java b/fastbee-open-api/src/main/java/com/fastbee/controller/report/ReportController.java index 4e36e30a..e44bf8ba 100644 --- a/fastbee-open-api/src/main/java/com/fastbee/controller/report/ReportController.java +++ b/fastbee-open-api/src/main/java/com/fastbee/controller/report/ReportController.java @@ -32,7 +32,6 @@ import java.io.IOException; import java.io.InputStream; import java.net.URLEncoder; import java.util.Arrays; -import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -88,7 +87,6 @@ public class ReportController extends BaseController { .status(record.getStatus()) .fileName(record.getFileName()) .fileUrl(record.getFileUrl()) - .expireTime(record.getExpireTime()) .build(); return AjaxResult.success(result); } @@ -115,9 +113,6 @@ public class ReportController extends BaseController { if (record == null) { throw new ServiceException("报告不存在"); } - if (record.getExpireTime() != null && record.getExpireTime().before(new Date())) { - throw new ServiceException("报告已过期"); - } if (StringUtils.isEmpty(record.getFileUrl())) { throw new ServiceException("报告文件不存在"); } diff --git a/fastbee-open-api/src/main/java/com/fastbee/controller/report/ReportThresholdController.java b/fastbee-open-api/src/main/java/com/fastbee/controller/report/ThingsThresholdController.java similarity index 80% rename from fastbee-open-api/src/main/java/com/fastbee/controller/report/ReportThresholdController.java rename to fastbee-open-api/src/main/java/com/fastbee/controller/report/ThingsThresholdController.java index 8374d7b6..459c249a 100644 --- a/fastbee-open-api/src/main/java/com/fastbee/controller/report/ReportThresholdController.java +++ b/fastbee-open-api/src/main/java/com/fastbee/controller/report/ThingsThresholdController.java @@ -8,9 +8,9 @@ import com.fastbee.common.enums.BusinessType; import com.fastbee.common.utils.poi.ExcelUtil; import com.fastbee.iot.service.IDeviceService; import com.fastbee.iot.service.IProductService; -import com.fastbee.report.domain.vo.ReportThresholdQuery; -import com.fastbee.report.service.IReportThresholdService; -import com.fastbee.report.threshold.ReportThreshold; +import com.fastbee.report.domain.vo.ThingsThresholdQuery; +import com.fastbee.report.service.IThingsThresholdService; +import com.fastbee.report.threshold.ThingsThreshold; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; @@ -32,10 +32,10 @@ import java.util.Map; @Api(tags = "监测阈值配置") @RestController @RequestMapping("/iot/threshold") -public class ReportThresholdController extends BaseController { +public class ThingsThresholdController extends BaseController { @Autowired - private IReportThresholdService reportThresholdService; + private IThingsThresholdService thingsThresholdService; @Autowired private IProductService productService; @@ -46,25 +46,25 @@ public class ReportThresholdController extends BaseController { /** * 查询阈值列表 */ - @PreAuthorize("@ss.hasPermi('iot:threshold:list')") + @PreAuthorize("@ss.hasPermi('iot:device:list')") @GetMapping("/list") @ApiOperation("阈值分页列表") - public TableDataInfo list(ReportThresholdQuery query) { + public TableDataInfo list(ThingsThresholdQuery query) { startPage(); - List list = reportThresholdService.selectThresholdList(query); + List list = thingsThresholdService.selectThresholdList(query); return getDataTable(list); } /** * 导出阈值列表 */ - @PreAuthorize("@ss.hasPermi('iot:threshold:export')") + @PreAuthorize("@ss.hasPermi('iot:device:list')") @Log(title = "监测阈值配置", businessType = BusinessType.EXPORT) @PostMapping("/export") @ApiOperation("导出阈值") - public void export(HttpServletResponse response, ReportThresholdQuery query) { - List list = reportThresholdService.selectThresholdList(query); - ExcelUtil util = new ExcelUtil<>(ReportThreshold.class); + public void export(HttpServletResponse response, ThingsThresholdQuery query) { + List list = thingsThresholdService.selectThresholdList(query); + ExcelUtil util = new ExcelUtil<>(ThingsThreshold.class); util.exportExcel(response, list, "监测阈值数据"); } @@ -72,43 +72,43 @@ public class ReportThresholdController extends BaseController { * 获取阈值详细信息 */ @ApiOperation("获取阈值详情") - @PreAuthorize("@ss.hasPermi('iot:threshold:query')") + @PreAuthorize("@ss.hasPermi('iot:device:list')") @GetMapping(value = "/{id}") public AjaxResult getInfo(@PathVariable("id") Long id) { - return AjaxResult.success(reportThresholdService.selectThresholdById(id)); + return AjaxResult.success(thingsThresholdService.selectThresholdById(id)); } /** * 新增阈值 */ - @PreAuthorize("@ss.hasPermi('iot:threshold:add')") + @PreAuthorize("@ss.hasPermi('iot:device:list')") @Log(title = "监测阈值配置", businessType = BusinessType.INSERT) @PostMapping @ApiOperation("新增阈值") - public AjaxResult add(@RequestBody ReportThreshold threshold) { - return toAjax(reportThresholdService.insertThreshold(threshold)); + public AjaxResult add(@RequestBody ThingsThreshold threshold) { + return toAjax(thingsThresholdService.insertThreshold(threshold)); } /** * 修改阈值 */ - @PreAuthorize("@ss.hasPermi('iot:threshold:edit')") + @PreAuthorize("@ss.hasPermi('iot:device:list')") @Log(title = "监测阈值配置", businessType = BusinessType.UPDATE) @PutMapping @ApiOperation("修改阈值") - public AjaxResult edit(@RequestBody ReportThreshold threshold) { - return toAjax(reportThresholdService.updateThreshold(threshold)); + public AjaxResult edit(@RequestBody ThingsThreshold threshold) { + return toAjax(thingsThresholdService.updateThreshold(threshold)); } /** * 删除阈值 */ - @PreAuthorize("@ss.hasPermi('iot:threshold:remove')") + @PreAuthorize("@ss.hasPermi('iot:device:list')") @Log(title = "监测阈值配置", businessType = BusinessType.DELETE) @DeleteMapping("/{ids}") @ApiOperation("删除阈值") public AjaxResult remove(@PathVariable Long[] ids) { - return toAjax(reportThresholdService.deleteThresholdByIds(ids)); + return toAjax(thingsThresholdService.deleteThresholdByIds(ids)); } /** diff --git a/fastbee-report/src/main/java/com/fastbee/report/analysis/QualityAnalyzer.java b/fastbee-report/src/main/java/com/fastbee/report/analysis/QualityAnalyzer.java index 7e26490f..3401cce2 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/analysis/QualityAnalyzer.java +++ b/fastbee-report/src/main/java/com/fastbee/report/analysis/QualityAnalyzer.java @@ -64,22 +64,25 @@ public class QualityAnalyzer { // 完整率估算:以相邻点中位间隔为期望上报间隔 long expectedMinutes = expectedMinutes(sorted, window); - int missing; - BigDecimal completeness; - if (expectedMinutes <= 0) { - missing = 0; - completeness = BigDecimal.ONE; - } else { - int expected = (int) Math.max(expectedMinutes / Math.max(1, estimateIntervalMinutes(sorted)), 1); + long intervalMinutes = estimateIntervalMinutes(sorted); + int missing = 0; + BigDecimal completeness = null; + // 时间窗口无效或数据点不足(无法估算上报间隔)时不做缺失推断, + // 否则单点数据会按"间隔=1分钟"算出上万条缺失、完整率趋近 0 + if (expectedMinutes > 0 && intervalMinutes > 0) { + int expected = (int) Math.max(expectedMinutes / intervalMinutes, 1); missing = Math.max(0, expected - sorted.size()); completeness = BigDecimal.valueOf(Math.min(1.0, sorted.size() / (double) expected)) .setScale(4, RoundingMode.HALF_UP); } result.setMissing(missing); result.setCompletenessRate(completeness); + String rateText = completeness == null + ? "无法估算(时间窗口无效或数据点不足)" + : String.format("%.2f%%", completeness.multiply(BigDecimal.valueOf(100)).doubleValue()); result.setDescription(String.format( - "统计期内共 %d 条数据,预估缺数 %d 条,完整率 %.2f%%,重复值 %d 条%s", - sorted.size(), missing, completeness.multiply(BigDecimal.valueOf(100)).doubleValue(), + "统计期内共 %d 条数据,预估缺数 %d 条,完整率 %s,重复值 %d 条%s", + sorted.size(), missing, rateText, duplicate, outOfRange > 0 ? ",越界 " + outOfRange + " 条" : "")); return result; } diff --git a/fastbee-report/src/main/java/com/fastbee/report/analysis/TrendAnalyzer.java b/fastbee-report/src/main/java/com/fastbee/report/analysis/TrendAnalyzer.java index 10241fb0..445018b1 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/analysis/TrendAnalyzer.java +++ b/fastbee-report/src/main/java/com/fastbee/report/analysis/TrendAnalyzer.java @@ -44,7 +44,8 @@ public class TrendAnalyzer { if (points.size() < 3) { result.setDirection(TrendAnalysis.DIRECTION_STABLE); result.setSlope(BigDecimal.ZERO); - result.setRSquared(BigDecimal.ONE); + // 数据不足时 R² 无统计意义,置空而非 1.0,避免报告出现虚假的"完美拟合" + result.setRSquared(null); result.setDescription("数据点不足(" + points.size() + " 个),暂不判定趋势"); return result; } diff --git a/fastbee-report/src/main/java/com/fastbee/report/analysis/WarningAnalyzer.java b/fastbee-report/src/main/java/com/fastbee/report/analysis/WarningAnalyzer.java index 9cc472bb..b3425c84 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/analysis/WarningAnalyzer.java +++ b/fastbee-report/src/main/java/com/fastbee/report/analysis/WarningAnalyzer.java @@ -1,7 +1,7 @@ package com.fastbee.report.analysis; import com.fastbee.report.model.WarningAnalysis; -import com.fastbee.report.threshold.ReportThreshold; +import com.fastbee.report.threshold.ThingsThreshold; import org.springframework.stereotype.Component; import java.math.BigDecimal; @@ -9,13 +9,18 @@ import java.math.BigDecimal; /** * 预警分析器:只读结构化阈值(禁止硬编码阈值) * + *

按 GB 50497-2019 第 8.0.4 条「双控」口径:累计变化量与变化速率分别判定, + * 取较严重等级。某一维度未配置阈值时该维度不参与判定;两个维度都不可判定时结果为 + * {@link WarningAnalysis#STATUS_UNKNOWN},不得回退为「正常」。

+ * * @author FastBee * @date 2026-08-16 */ @Component public class WarningAnalyzer { - public WarningAnalysis analyze(String identifier, String modelName, BigDecimal currentValue, ReportThreshold threshold) { + public WarningAnalysis analyze(String identifier, String modelName, BigDecimal currentValue, + BigDecimal changeRate, ThingsThreshold threshold) { WarningAnalysis result = new WarningAnalysis(); result.setIdentifier(identifier); result.setModelName(modelName); @@ -25,34 +30,117 @@ public class WarningAnalyzer { result.setAlarmValue(threshold.getAlarmValue()); result.setControlValue(threshold.getControlValue()); } - if (currentValue == null) { - result.setStatus(WarningAnalysis.STATUS_NORMAL); - result.setDescription("统计期内无有效当前值"); - return result; - } + result.setStatus(WarningAnalysis.STATUS_UNKNOWN); + if (threshold == null) { - result.setStatus(WarningAnalysis.STATUS_UNKNOWN); result.setDescription("未配置预警阈值,无法评估安全等级"); return result; } - BigDecimal abs = currentValue.abs(); - if (threshold.getControlValue() != null && abs.compareTo(threshold.getControlValue()) >= 0) { - result.setStatus(WarningAnalysis.STATUS_ALARM); - result.setDescription(String.format("当前值 %s 达到/超过控制值 %s,处于报警状态", - currentValue.toPlainString(), threshold.getControlValue().toPlainString())); - } else if (threshold.getAlarmValue() != null && abs.compareTo(threshold.getAlarmValue()) >= 0) { - result.setStatus(WarningAnalysis.STATUS_ALARM); - result.setDescription(String.format("当前值 %s 达到/超过报警值 %s,处于报警状态", - currentValue.toPlainString(), threshold.getAlarmValue().toPlainString())); - } else if (threshold.getWarningValue() != null && abs.compareTo(threshold.getWarningValue()) >= 0) { - result.setStatus(WarningAnalysis.STATUS_WARNING); - result.setDescription(String.format("当前值 %s 达到/超过预警值 %s,处于预警状态", - currentValue.toPlainString(), threshold.getWarningValue().toPlainString())); - } else { - result.setStatus(WarningAnalysis.STATUS_NORMAL); - result.setDescription("当前值处于正常范围"); + boolean noCumulative = isAllNull(threshold.getControlValue(), threshold.getAlarmValue(), threshold.getWarningValue()); + boolean noRate = isAllNull(threshold.getControlRate(), threshold.getAlarmRate(), threshold.getWarningRate()); + if (noCumulative && noRate) { + result.setDescription("阈值记录未配置累计值与速率阈值,无法评估安全等级"); + return result; } + + // 维度一:累计变化量(相对量设备传相对基准点的累计变化,绝对量设备传当前值) + String cumulativeStatus = null; + String cumulativeDesc = null; + if (!noCumulative && currentValue != null) { + BigDecimal abs = currentValue.abs(); + if (threshold.getControlValue() != null && abs.compareTo(threshold.getControlValue()) >= 0) { + cumulativeStatus = WarningAnalysis.STATUS_ALARM; + cumulativeDesc = String.format("当前值 %s 达到/超过控制值 %s,处于报警状态", + currentValue.toPlainString(), threshold.getControlValue().toPlainString()); + } else if (threshold.getAlarmValue() != null && abs.compareTo(threshold.getAlarmValue()) >= 0) { + cumulativeStatus = WarningAnalysis.STATUS_ALARM; + cumulativeDesc = String.format("当前值 %s 达到/超过报警值 %s,处于报警状态", + currentValue.toPlainString(), threshold.getAlarmValue().toPlainString()); + } else if (threshold.getWarningValue() != null && abs.compareTo(threshold.getWarningValue()) >= 0) { + cumulativeStatus = WarningAnalysis.STATUS_WARNING; + cumulativeDesc = String.format("当前值 %s 达到/超过预警值 %s,处于预警状态", + currentValue.toPlainString(), threshold.getWarningValue().toPlainString()); + } else { + cumulativeStatus = WarningAnalysis.STATUS_NORMAL; + cumulativeDesc = "累计值处于正常范围"; + } + } + + // 维度二:变化速率(单位/天) + String rateStatus = null; + String rateDesc = null; + if (!noRate && changeRate != null) { + BigDecimal absRate = changeRate.abs(); + if (threshold.getControlRate() != null && absRate.compareTo(threshold.getControlRate()) >= 0) { + rateStatus = WarningAnalysis.STATUS_ALARM; + rateDesc = String.format("变化速率 %s 达到/超过速率控制值 %s,处于报警状态", + absRate.toPlainString(), threshold.getControlRate().toPlainString()); + } else if (threshold.getAlarmRate() != null && absRate.compareTo(threshold.getAlarmRate()) >= 0) { + rateStatus = WarningAnalysis.STATUS_ALARM; + rateDesc = String.format("变化速率 %s 达到/超过速率报警值 %s,处于报警状态", + absRate.toPlainString(), threshold.getAlarmRate().toPlainString()); + } else if (threshold.getWarningRate() != null && absRate.compareTo(threshold.getWarningRate()) >= 0) { + rateStatus = WarningAnalysis.STATUS_WARNING; + rateDesc = String.format("变化速率 %s 达到/超过速率预警值 %s,处于预警状态", + absRate.toPlainString(), threshold.getWarningRate().toPlainString()); + } else { + rateStatus = WarningAnalysis.STATUS_NORMAL; + rateDesc = "变化速率处于正常范围"; + } + } + + String finalStatus = worse(cumulativeStatus, rateStatus); + if (finalStatus == null) { + result.setDescription("缺少可判定的累计变化量或变化速率,无法评估安全等级"); + return result; + } + result.setStatus(finalStatus); + + StringBuilder desc = new StringBuilder(); + if (cumulativeDesc != null) { + desc.append(cumulativeDesc); + } + if (rateDesc != null) { + if (desc.length() > 0) { + desc.append(";"); + } + desc.append(rateDesc); + } + // 维度缺数据时明确说明,避免"只判了一半"被当成已完整评估 + if (!noCumulative && currentValue == null) { + desc.append(";累计值缺失,累计维度未参与判定"); + } + if (!noRate && changeRate == null) { + desc.append(";变化速率缺失,速率维度未参与判定"); + } + result.setDescription(desc.toString()); return result; } + + /** 取更严重等级:ALARM > WARNING > NORMAL;null 表示该维度未参与判定 */ + private String worse(String a, String b) { + if (a == null) { + return b; + } + if (b == null) { + return a; + } + if (WarningAnalysis.STATUS_ALARM.equals(a) || WarningAnalysis.STATUS_ALARM.equals(b)) { + return WarningAnalysis.STATUS_ALARM; + } + if (WarningAnalysis.STATUS_WARNING.equals(a) || WarningAnalysis.STATUS_WARNING.equals(b)) { + return WarningAnalysis.STATUS_WARNING; + } + return WarningAnalysis.STATUS_NORMAL; + } + + private boolean isAllNull(BigDecimal... values) { + for (BigDecimal v : values) { + if (v != null) { + return false; + } + } + return true; + } } diff --git a/fastbee-report/src/main/java/com/fastbee/report/analyzer/AbstractDeviceAnalyzer.java b/fastbee-report/src/main/java/com/fastbee/report/analyzer/AbstractDeviceAnalyzer.java index 3e4237ea..ab780d37 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/analyzer/AbstractDeviceAnalyzer.java +++ b/fastbee-report/src/main/java/com/fastbee/report/analyzer/AbstractDeviceAnalyzer.java @@ -22,7 +22,7 @@ import com.fastbee.report.model.Statistics; import com.fastbee.report.model.TrendAnalysis; import com.fastbee.report.model.WarningAnalysis; import com.fastbee.report.period.TimeWindow; -import com.fastbee.report.threshold.ReportThreshold; +import com.fastbee.report.threshold.ThingsThreshold; import org.springframework.stereotype.Component; import javax.annotation.Resource; @@ -30,6 +30,7 @@ import java.math.BigDecimal; import java.time.Duration; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -54,6 +55,15 @@ import java.util.stream.Collectors; */ public abstract class AbstractDeviceAnalyzer implements ReportAnalyzer { + /** 安全等级:安全 */ + public static final String SAFETY_SAFE = "SAFE"; + /** 安全等级:预警 */ + public static final String SAFETY_WARNING = "WARNING"; + /** 安全等级:危险 */ + public static final String SAFETY_DANGER = "DANGER"; + /** 安全等级:未配置阈值,无法评估(区别于 SAFE,避免"未评估"被当成"安全") */ + public static final String SAFETY_NOT_ASSESSED = "NOT_ASSESSED"; + @Resource protected StatisticsCalculator statisticsCalculator; @@ -210,6 +220,15 @@ public abstract class AbstractDeviceAnalyzer implements ReportAnalyzer { .filter(d -> d.getIdentity() != null) .collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList())); + // 1.1 基准点数据分组(相对测量换算用) + Map> baselineByIdentity = Collections.emptyMap(); + List baselineList = context.getBaselineMetricDataList(); + if (baselineList != null && !baselineList.isEmpty()) { + baselineByIdentity = baselineList.stream() + .filter(d -> d.getIdentity() != null) + .collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList())); + } + // 2. 获取所有监测物模型 Map allModels = getAllModels(context); @@ -223,8 +242,12 @@ public abstract class AbstractDeviceAnalyzer implements ReportAnalyzer { String identity = entry.getKey(); ThingsModel model = entry.getValue(); List data = byIdentity.getOrDefault(identity, Collections.emptyList()); + // 相对测量换算:监测点读数 − 基准点读数(按时间就近对齐) + if (!baselineByIdentity.isEmpty()) { + data = toRelativeSeries(data, baselineByIdentity.get(identity)); + } - ReportThreshold threshold = context.getThresholds().get(identity); + ThingsThreshold threshold = context.getThresholds().get(identity); BigDecimal controlValue = threshold == null ? null : threshold.getControlValue(); Statistics statistics = statisticsCalculator.calculate(data); @@ -232,8 +255,15 @@ public abstract class AbstractDeviceAnalyzer implements ReportAnalyzer { TrendAnalysis trend = trendAnalyzer.analyze(data); QualityAnalysis quality = qualityAnalyzer.analyze(data, context.getTimeWindow(), controlValue); + + // 双控:变化速率(单位/天)与累计量一并参与安全判定 + TimeWindow window = context.getTimeWindow(); + long durationSeconds = (window != null && window.getStartTime() != null && window.getEndTime() != null) + ? Duration.between(window.getStartTime(), window.getEndTime()).getSeconds() + : 0L; + BigDecimal changeRate = rateCalculator.ratePerDay(statistics, durationSeconds); WarningAnalysis warning = warningAnalyzer.analyze( - identity, statistics.getModelName(), statistics.getCurrent(), threshold); + identity, statistics.getModelName(), statistics.getCurrent(), changeRate, threshold); result.getStatistics().put(identity, statistics); result.getTrends().put(identity, trend); @@ -261,6 +291,76 @@ public abstract class AbstractDeviceAnalyzer implements ReportAnalyzer { return result; } + /** + * 相对测量换算:监测点读数 − 基准点读数(按时间就近对齐) + * + *

静力水准等设备的原始读数是相对基准点的,累计变化量应按 + * 「(当前读数 − 基准点当前读数) − (初始读数 − 基准点初始读数)」计算, + * 直接拿原始读数的首尾差会混入基准点自身的漂移。

+ * + *

对齐不上的点直接剔除而非补零,避免制造虚假数据点。

+ */ + protected List toRelativeSeries(List monitor, List baseline) { + if (monitor == null || monitor.isEmpty() || baseline == null || baseline.isEmpty()) { + return monitor; + } + List sortedBase = baseline.stream() + .filter(d -> d.getTime() != null && d.getNumericValue() != null) + .sorted(Comparator.comparing(MetricData::getTime)) + .collect(Collectors.toList()); + if (sortedBase.isEmpty()) { + return monitor; + } + long toleranceMs = alignmentToleranceMs(sortedBase); + List sortedMonitor = monitor.stream() + .filter(d -> d.getTime() != null && d.getNumericValue() != null) + .sorted(Comparator.comparing(MetricData::getTime)) + .collect(Collectors.toList()); + + List result = new ArrayList<>(); + int j = 0; + for (MetricData m : sortedMonitor) { + long target = m.getTime().getTime(); + while (j + 1 < sortedBase.size() + && Math.abs(sortedBase.get(j + 1).getTime().getTime() - target) + <= Math.abs(sortedBase.get(j).getTime().getTime() - target)) { + j++; + } + MetricData base = sortedBase.get(j); + if (Math.abs(base.getTime().getTime() - target) > toleranceMs) { + continue; + } + MetricData rel = new MetricData(); + rel.setIdentity(m.getIdentity()); + rel.setModelName(m.getModelName()); + rel.setUnit(m.getUnit()); + rel.setTime(m.getTime()); + rel.setNumericValue(m.getNumericValue().subtract(base.getNumericValue())); + rel.setValue(rel.getNumericValue().toPlainString()); + result.add(rel); + } + return result; + } + + /** 对齐容差:基准点上报间隔中位数的 2 倍,至少 1 分钟 */ + private long alignmentToleranceMs(List sortedBase) { + List gaps = new ArrayList<>(); + for (int i = 1; i < sortedBase.size(); i++) { + long gap = sortedBase.get(i).getTime().getTime() - sortedBase.get(i - 1).getTime().getTime(); + if (gap > 0) { + gaps.add(gap); + } + } + long median; + if (gaps.isEmpty()) { + median = 30 * 60 * 1000L; + } else { + Collections.sort(gaps); + median = gaps.get(gaps.size() / 2); + } + return Math.max(median * 2, 60 * 1000L); + } + /** * 获取所有监测物模型 */ @@ -397,7 +497,7 @@ public abstract class AbstractDeviceAnalyzer implements ReportAnalyzer { } // 设置控制值 - ReportThreshold threshold = context.getThresholds().get(primaryIdentity); + ThingsThreshold threshold = context.getThresholds().get(primaryIdentity); if (threshold != null && threshold.getControlValue() != null) { result.setControlValue(threshold.getControlValue()); result.setControlValueUnit(threshold.getUnit()); @@ -491,11 +591,16 @@ public abstract class AbstractDeviceAnalyzer implements ReportAnalyzer { */ protected String convertWarningToSafetyLevel(String warningStatus) { if (WarningAnalysis.STATUS_ALARM.equals(warningStatus)) { - return "DANGER"; - } else if (WarningAnalysis.STATUS_WARNING.equals(warningStatus)) { - return "WARNING"; + return SAFETY_DANGER; } - return "SAFE"; + if (WarningAnalysis.STATUS_WARNING.equals(warningStatus)) { + return SAFETY_WARNING; + } + // 未配置阈值/阈值不可用:不可等同于安全,否则报告会给出虚假的安全结论 + if (WarningAnalysis.STATUS_UNKNOWN.equals(warningStatus)) { + return SAFETY_NOT_ASSESSED; + } + return SAFETY_SAFE; } /** diff --git a/fastbee-report/src/main/java/com/fastbee/report/api/ReportResult.java b/fastbee-report/src/main/java/com/fastbee/report/api/ReportResult.java index 77d0ce7c..4c202bf6 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/api/ReportResult.java +++ b/fastbee-report/src/main/java/com/fastbee/report/api/ReportResult.java @@ -1,13 +1,11 @@ package com.fastbee.report.api; -import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Builder; import lombok.Data; import java.io.Serializable; -import java.util.Date; /** * 报告生成结果 @@ -33,8 +31,4 @@ public class ReportResult implements Serializable { @ApiModelProperty("文件地址(MinIO)") private String fileUrl; - - @ApiModelProperty("过期时间") - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") - private Date expireTime; } diff --git a/fastbee-report/src/main/java/com/fastbee/report/core/ReportContext.java b/fastbee-report/src/main/java/com/fastbee/report/core/ReportContext.java index c2b4974e..b6f3d607 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/core/ReportContext.java +++ b/fastbee-report/src/main/java/com/fastbee/report/core/ReportContext.java @@ -7,7 +7,7 @@ import com.fastbee.iot.domain.ThingsModel; import com.fastbee.report.api.ReportRequest; import com.fastbee.report.data.MetricData; import com.fastbee.report.period.TimeWindow; -import com.fastbee.report.threshold.ReportThreshold; +import com.fastbee.report.threshold.ThingsThreshold; import lombok.Data; import java.util.List; @@ -38,8 +38,16 @@ public class ReportContext { /** 时序数据 */ private List metricDataList; + /** + * 基准点设备的时序数据(device.baselineDeviceId 非空时加载) + * + *

相对测量类设备(静力水准等)的读数是相对基准点的,需先按时间对齐做差值 + * 换算为相对序列,再统计累计变化量。为 null 表示该设备为绝对量测量。

+ */ + private List baselineMetricDataList; + /** 阈值(key = 物模型标识符) */ - private Map thresholds; + private Map thresholds; private String reportNo; diff --git a/fastbee-report/src/main/java/com/fastbee/report/core/ReportContextBuilder.java b/fastbee-report/src/main/java/com/fastbee/report/core/ReportContextBuilder.java index 1dfd5176..dd6dac9f 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/core/ReportContextBuilder.java +++ b/fastbee-report/src/main/java/com/fastbee/report/core/ReportContextBuilder.java @@ -15,8 +15,8 @@ import com.fastbee.report.data.MetricData; import com.fastbee.report.data.ReportDataProvider; import com.fastbee.report.exception.ReportException; import com.fastbee.report.period.TimeWindow; -import com.fastbee.report.threshold.ReportThreshold; -import com.fastbee.report.threshold.ReportThresholdProvider; +import com.fastbee.report.threshold.ThingsThreshold; +import com.fastbee.report.threshold.ThingsThresholdProvider; import org.springframework.stereotype.Component; import javax.annotation.Resource; @@ -53,7 +53,7 @@ public class ReportContextBuilder { private ReportDataProvider dataProvider; @Resource - private ReportThresholdProvider thresholdProvider; + private ThingsThresholdProvider thingsThresholdProvider; @Resource private SubGatewayMapper subGatewayMapper; @@ -90,12 +90,23 @@ public class ReportContextBuilder { List metricDataList = dataProvider.loadHistory(device, models, window); - Map thresholds = new LinkedHashMap<>(); + // 基准点设备数据:相对测量类设备(静力水准)需按时间对齐做差值换算 + // deviceId 全局唯一,因此基准点可以属于另一个网关(跨网关参考点) + List baselineMetricDataList = null; + Long baselineDeviceId = device.getBaselineDeviceId(); + if (baselineDeviceId != null && baselineDeviceId > 0 && !baselineDeviceId.equals(device.getDeviceId())) { + Device baselineDevice = deviceService.selectDeviceByDeviceId(baselineDeviceId); + if (baselineDevice != null) { + baselineMetricDataList = dataProvider.loadHistory(baselineDevice, models, window); + } + } + + Map thresholds = new LinkedHashMap<>(); for (ThingsModel model : models) { if (model.getIdentifier() == null) { continue; } - ReportThreshold threshold = thresholdProvider.find( + ThingsThreshold threshold = thingsThresholdProvider.find( device.getDeviceId(), product.getProductId(), category.getCode(), model.getIdentifier()); if (threshold != null) { thresholds.put(model.getIdentifier(), threshold); @@ -110,6 +121,7 @@ public class ReportContextBuilder { context.setProduct(product); context.setThingsModels(models); context.setMetricDataList(metricDataList); + context.setBaselineMetricDataList(baselineMetricDataList); context.setThresholds(thresholds); context.setReportNo(reportNo); diff --git a/fastbee-report/src/main/java/com/fastbee/report/domain/vo/ReportThresholdQuery.java b/fastbee-report/src/main/java/com/fastbee/report/domain/vo/ThingsThresholdQuery.java similarity index 86% rename from fastbee-report/src/main/java/com/fastbee/report/domain/vo/ReportThresholdQuery.java rename to fastbee-report/src/main/java/com/fastbee/report/domain/vo/ThingsThresholdQuery.java index 656f0ee2..6fb84778 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/domain/vo/ReportThresholdQuery.java +++ b/fastbee-report/src/main/java/com/fastbee/report/domain/vo/ThingsThresholdQuery.java @@ -3,13 +3,13 @@ package com.fastbee.report.domain.vo; import lombok.Data; /** - * 报告阈值查询 VO + * 物模型阈值查询 VO * * @author FastBee * @date 2026-09-10 */ @Data -public class ReportThresholdQuery { +public class ThingsThresholdQuery { /** 产品ID */ private Long productId; diff --git a/fastbee-report/src/main/java/com/fastbee/report/executor/ReportAsyncExecutor.java b/fastbee-report/src/main/java/com/fastbee/report/executor/ReportAsyncExecutor.java index a51c6f81..efa9677c 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/executor/ReportAsyncExecutor.java +++ b/fastbee-report/src/main/java/com/fastbee/report/executor/ReportAsyncExecutor.java @@ -29,7 +29,6 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; -import java.util.Date; import java.util.concurrent.ThreadLocalRandom; /** @@ -44,7 +43,6 @@ public class ReportAsyncExecutor { private static final String WORD_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; - private static final int RETENTION_DAYS = 30; private static final DateTimeFormatter REPORT_NO_TIME = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); private static final DateTimeFormatter PATH_DATE = DateTimeFormatter.ofPattern("yyyy/MM/dd"); @@ -119,9 +117,8 @@ public class ReportAsyncExecutor { String path = buildObjectPath(request.getDeviceId(), reportNo); UploadResult upload = OssFactory.instance(redisCache) .upload(docBytes, path, WORD_CONTENT_TYPE); - Date expireTime = new Date(System.currentTimeMillis() + RETENTION_DAYS * 24L * 3600 * 1000); - reportRecordService.markSuccess(record, model.getFileName(), upload.getUrl(), expireTime); + reportRecordService.markSuccess(record, model.getFileName(), upload.getUrl()); log.info("报告生成成功 reportNo={} fileUrl={}", reportNo, upload.getUrl()); } catch (Exception e) { diff --git a/fastbee-report/src/main/java/com/fastbee/report/mapper/ReportRecordMapper.java b/fastbee-report/src/main/java/com/fastbee/report/mapper/ReportRecordMapper.java index 1c67b0c6..66792d43 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/mapper/ReportRecordMapper.java +++ b/fastbee-report/src/main/java/com/fastbee/report/mapper/ReportRecordMapper.java @@ -15,7 +15,15 @@ import java.util.List; public interface ReportRecordMapper extends BaseMapper { /** - * 查询未过期报告记录(expire_time >= now) + * 查询报告记录(过期清理由 LogCleanTask#cleanLogs 统一负责) */ List selectUnexpiredList(ReportRecordQuery query); + + /** + * 清理过期报告记录:按 create_time 倒推,保留最近 days 天 + * + * @param days 保留天数 + * @return 删除记录数 + */ + int cleanExpired(int days); } diff --git a/fastbee-report/src/main/java/com/fastbee/report/mapper/ReportThresholdMapper.java b/fastbee-report/src/main/java/com/fastbee/report/mapper/ThingsThresholdMapper.java similarity index 72% rename from fastbee-report/src/main/java/com/fastbee/report/mapper/ReportThresholdMapper.java rename to fastbee-report/src/main/java/com/fastbee/report/mapper/ThingsThresholdMapper.java index f2136d85..33c73bca 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/mapper/ReportThresholdMapper.java +++ b/fastbee-report/src/main/java/com/fastbee/report/mapper/ThingsThresholdMapper.java @@ -1,21 +1,21 @@ package com.fastbee.report.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.fastbee.report.threshold.ReportThreshold; +import com.fastbee.report.threshold.ThingsThreshold; import org.apache.ibatis.annotations.Param; /** - * 报告阈值 Mapper + * 物模型阈值 Mapper * * @author FastBee * @date 2026-08-16 */ -public interface ReportThresholdMapper extends BaseMapper { +public interface ThingsThresholdMapper extends BaseMapper { /** * 按优先级查询阈值:device_id > product_id > 全局默认(product_id=0) */ - ReportThreshold selectByPriority(@Param("deviceId") Long deviceId, + ThingsThreshold selectByPriority(@Param("deviceId") Long deviceId, @Param("productId") Long productId, @Param("categoryCode") String categoryCode, @Param("metricIdentifier") String metricIdentifier); diff --git a/fastbee-report/src/main/java/com/fastbee/report/record/ReportRecord.java b/fastbee-report/src/main/java/com/fastbee/report/record/ReportRecord.java index 124c9300..5445a44a 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/record/ReportRecord.java +++ b/fastbee-report/src/main/java/com/fastbee/report/record/ReportRecord.java @@ -49,9 +49,6 @@ public class ReportRecord extends BaseEntity { private String fileUrl; - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") - private Date expireTime; - private String errorMessage; private Long tenantId; diff --git a/fastbee-report/src/main/java/com/fastbee/report/record/ReportRecordService.java b/fastbee-report/src/main/java/com/fastbee/report/record/ReportRecordService.java index 7c718f7c..1a321c4a 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/record/ReportRecordService.java +++ b/fastbee-report/src/main/java/com/fastbee/report/record/ReportRecordService.java @@ -2,7 +2,6 @@ package com.fastbee.report.record; import com.fastbee.report.core.ReportContext; -import java.util.Date; import java.util.List; /** @@ -14,14 +13,14 @@ import java.util.List; public interface ReportRecordService { /** - * 生成前创建 CREATING 记录(expire_time = create_time + 30 天) + * 生成前创建 CREATING 记录 */ ReportRecord createCreating(ReportContext context); /** * 生成成功:更新 SUCCESS + 文件信息 */ - void markSuccess(ReportRecord record, String fileName, String fileUrl, Date expireTime); + void markSuccess(ReportRecord record, String fileName, String fileUrl); /** * 生成失败:更新 FAILED + 失败原因 @@ -29,7 +28,7 @@ public interface ReportRecordService { void markFailed(ReportRecord record, String errorMessage); /** - * 分页查询未过期记录 + * 分页查询报告记录 */ List listUnexpired(ReportRecordQuery query); @@ -37,4 +36,12 @@ public interface ReportRecordService { * 按报告编号查询 */ ReportRecord findByReportNo(String reportNo); + + /** + * 清理过期报告记录:保留最近 days 天的记录 + * + * @param days 保留天数 + * @return 删除记录数 + */ + int cleanExpired(int days); } diff --git a/fastbee-report/src/main/java/com/fastbee/report/record/ReportRecordServiceImpl.java b/fastbee-report/src/main/java/com/fastbee/report/record/ReportRecordServiceImpl.java index 6530ebff..96dc3348 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/record/ReportRecordServiceImpl.java +++ b/fastbee-report/src/main/java/com/fastbee/report/record/ReportRecordServiceImpl.java @@ -19,8 +19,6 @@ import java.time.ZoneOffset; @Service public class ReportRecordServiceImpl implements ReportRecordService { - private static final int RETENTION_DAYS = 30; - @Resource private ReportRecordMapper reportRecordMapper; @@ -39,7 +37,6 @@ public class ReportRecordServiceImpl implements ReportRecordService { record.setTenantId(context.getTenantId()); record.setCreateBy(context.getCreateBy()); record.setCreateTime(now); - record.setExpireTime(new Date(now.getTime() + RETENTION_DAYS * 24L * 3600 * 1000)); reportRecordMapper.insert(record); return record; } @@ -49,13 +46,12 @@ public class ReportRecordServiceImpl implements ReportRecordService { } @Override - public void markSuccess(ReportRecord record, String fileName, String fileUrl, Date expireTime) { + public void markSuccess(ReportRecord record, String fileName, String fileUrl) { ReportRecord update = new ReportRecord(); update.setId(record.getId()); update.setStatus(ReportRecord.STATUS_SUCCESS); update.setFileName(fileName); update.setFileUrl(fileUrl); - update.setExpireTime(expireTime); // 异步线程中无法访问 SecurityContext,使用创建时的用户名 update.setUpdateBy(record.getCreateBy()); update.setUpdateTime(DateUtils.getNowDate()); @@ -86,4 +82,9 @@ public class ReportRecordServiceImpl implements ReportRecordService { .eq(ReportRecord::getReportNo, reportNo) .last("limit 1")); } + + @Override + public int cleanExpired(int days) { + return reportRecordMapper.cleanExpired(days); + } } diff --git a/fastbee-report/src/main/java/com/fastbee/report/renderer/WordRenderer.java b/fastbee-report/src/main/java/com/fastbee/report/renderer/WordRenderer.java index 86ba8d02..dc4069f2 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/renderer/WordRenderer.java +++ b/fastbee-report/src/main/java/com/fastbee/report/renderer/WordRenderer.java @@ -178,9 +178,9 @@ public class WordRenderer implements ReportRenderer { data.put("professionalAdvice", StringUtils.isNotEmpty(analysis.getProfessionalAdvice()) ? analysis.getProfessionalAdvice() : "暂无专业分析建议。"); - // 添加安全评估(始终 put,空值兜底) - data.put("safetyLevel", safetyLevelText(StringUtils.isNotEmpty(analysis.getSafetyLevel()) - ? analysis.getSafetyLevel() : "待评估")); + // 添加安全评估(始终 put,空值兜底直接给"待评估",不可再经 safetyLevelText 回退成"安全") + data.put("safetyLevel", StringUtils.isNotEmpty(analysis.getSafetyLevel()) + ? safetyLevelText(analysis.getSafetyLevel()) : "待评估"); data.put("safetyDescription", StringUtils.isNotEmpty(analysis.getSafetyDescription()) ? analysis.getSafetyDescription() : "暂无评估说明。"); @@ -310,6 +310,10 @@ public class WordRenderer implements ReportRenderer { if ("CRITICAL".equals(level)) { return "临界"; } + // 未配置阈值,无法评估——不得回退为"安全" + if ("NOT_ASSESSED".equals(level) || "UNKNOWN".equals(level)) { + return "待评估"; + } return "安全"; } } diff --git a/fastbee-report/src/main/java/com/fastbee/report/service/IReportThresholdService.java b/fastbee-report/src/main/java/com/fastbee/report/service/IThingsThresholdService.java similarity index 65% rename from fastbee-report/src/main/java/com/fastbee/report/service/IReportThresholdService.java rename to fastbee-report/src/main/java/com/fastbee/report/service/IThingsThresholdService.java index b2006600..b4e31d73 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/service/IReportThresholdService.java +++ b/fastbee-report/src/main/java/com/fastbee/report/service/IThingsThresholdService.java @@ -1,18 +1,18 @@ package com.fastbee.report.service; import com.baomidou.mybatisplus.extension.service.IService; -import com.fastbee.report.domain.vo.ReportThresholdQuery; -import com.fastbee.report.threshold.ReportThreshold; +import com.fastbee.report.domain.vo.ThingsThresholdQuery; +import com.fastbee.report.threshold.ThingsThreshold; import java.util.List; /** - * 报告阈值 Service 接口 + * 物模型阈值 Service 接口 * * @author FastBee * @date 2026-09-10 */ -public interface IReportThresholdService extends IService { +public interface IThingsThresholdService extends IService { /** * 查询阈值列表(分页) @@ -20,7 +20,7 @@ public interface IReportThresholdService extends IService { * @param query 查询条件 * @return 阈值列表 */ - List selectThresholdList(ReportThresholdQuery query); + List selectThresholdList(ThingsThresholdQuery query); /** * 根据ID查询阈值 @@ -28,7 +28,7 @@ public interface IReportThresholdService extends IService { * @param id 阈值ID * @return 阈值对象 */ - ReportThreshold selectThresholdById(Long id); + ThingsThreshold selectThresholdById(Long id); /** * 新增阈值 @@ -36,7 +36,7 @@ public interface IReportThresholdService extends IService { * @param threshold 阈值对象 * @return 影响行数 */ - int insertThreshold(ReportThreshold threshold); + int insertThreshold(ThingsThreshold threshold); /** * 修改阈值 @@ -44,7 +44,7 @@ public interface IReportThresholdService extends IService { * @param threshold 阈值对象 * @return 影响行数 */ - int updateThreshold(ReportThreshold threshold); + int updateThreshold(ThingsThreshold threshold); /** * 批量删除阈值 diff --git a/fastbee-report/src/main/java/com/fastbee/report/service/impl/ReportThresholdServiceImpl.java b/fastbee-report/src/main/java/com/fastbee/report/service/impl/ThingsThresholdServiceImpl.java similarity index 53% rename from fastbee-report/src/main/java/com/fastbee/report/service/impl/ReportThresholdServiceImpl.java rename to fastbee-report/src/main/java/com/fastbee/report/service/impl/ThingsThresholdServiceImpl.java index d4ddf738..19615aec 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/service/impl/ReportThresholdServiceImpl.java +++ b/fastbee-report/src/main/java/com/fastbee/report/service/impl/ThingsThresholdServiceImpl.java @@ -3,59 +3,59 @@ package com.fastbee.report.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.fastbee.common.utils.StringUtils; -import com.fastbee.report.domain.vo.ReportThresholdQuery; -import com.fastbee.report.mapper.ReportThresholdMapper; -import com.fastbee.report.service.IReportThresholdService; -import com.fastbee.report.threshold.ReportThreshold; +import com.fastbee.report.domain.vo.ThingsThresholdQuery; +import com.fastbee.report.mapper.ThingsThresholdMapper; +import com.fastbee.report.service.IThingsThresholdService; +import com.fastbee.report.threshold.ThingsThreshold; import org.springframework.stereotype.Service; import java.util.Arrays; import java.util.List; /** - * 报告阈值 Service 实现 + * 物模型阈值 Service 实现 * * @author FastBee * @date 2026-09-10 */ @Service -public class ReportThresholdServiceImpl extends ServiceImpl - implements IReportThresholdService { +public class ThingsThresholdServiceImpl extends ServiceImpl + implements IThingsThresholdService { @Override - public List selectThresholdList(ReportThresholdQuery query) { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + public List selectThresholdList(ThingsThresholdQuery query) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); if (query.getProductId() != null) { - wrapper.eq(ReportThreshold::getProductId, query.getProductId()); + wrapper.eq(ThingsThreshold::getProductId, query.getProductId()); } if (query.getDeviceId() != null) { - wrapper.eq(ReportThreshold::getDeviceId, query.getDeviceId()); + wrapper.eq(ThingsThreshold::getDeviceId, query.getDeviceId()); } if (StringUtils.isNotEmpty(query.getMonitorCategoryCode())) { - wrapper.eq(ReportThreshold::getMonitorCategoryCode, query.getMonitorCategoryCode()); + wrapper.eq(ThingsThreshold::getMonitorCategoryCode, query.getMonitorCategoryCode()); } if (StringUtils.isNotEmpty(query.getMetricIdentifier())) { - wrapper.like(ReportThreshold::getMetricIdentifier, query.getMetricIdentifier()); + wrapper.like(ThingsThreshold::getMetricIdentifier, query.getMetricIdentifier()); } if (StringUtils.isNotEmpty(query.getUnit())) { - wrapper.eq(ReportThreshold::getUnit, query.getUnit()); + wrapper.eq(ThingsThreshold::getUnit, query.getUnit()); } - wrapper.orderByDesc(ReportThreshold::getCreateTime); + wrapper.orderByDesc(ThingsThreshold::getCreateTime); return baseMapper.selectList(wrapper); } @Override - public ReportThreshold selectThresholdById(Long id) { + public ThingsThreshold selectThresholdById(Long id) { return baseMapper.selectById(id); } @Override - public int insertThreshold(ReportThreshold threshold) { + public int insertThreshold(ThingsThreshold threshold) { return baseMapper.insert(threshold); } @Override - public int updateThreshold(ReportThreshold threshold) { + public int updateThreshold(ThingsThreshold threshold) { return baseMapper.updateById(threshold); } diff --git a/fastbee-report/src/main/java/com/fastbee/report/threshold/DbReportThresholdProvider.java b/fastbee-report/src/main/java/com/fastbee/report/threshold/DbThingsThresholdProvider.java similarity index 50% rename from fastbee-report/src/main/java/com/fastbee/report/threshold/DbReportThresholdProvider.java rename to fastbee-report/src/main/java/com/fastbee/report/threshold/DbThingsThresholdProvider.java index 97b10a30..5ab9cab1 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/threshold/DbReportThresholdProvider.java +++ b/fastbee-report/src/main/java/com/fastbee/report/threshold/DbThingsThresholdProvider.java @@ -1,25 +1,25 @@ package com.fastbee.report.threshold; import com.fastbee.common.utils.StringUtils; -import com.fastbee.report.mapper.ReportThresholdMapper; +import com.fastbee.report.mapper.ThingsThresholdMapper; import org.springframework.stereotype.Service; import javax.annotation.Resource; /** - * 基于 iot_report_threshold 表的阈值 Provider + * 基于 iot_things_threshold 表的阈值 Provider * * @author FastBee * @date 2026-08-16 */ @Service -public class DbReportThresholdProvider implements ReportThresholdProvider { +public class DbThingsThresholdProvider implements ThingsThresholdProvider { @Resource - private ReportThresholdMapper reportThresholdMapper; + private ThingsThresholdMapper thingsThresholdMapper; @Override - public ReportThreshold find(Long deviceId, Long productId, String categoryCode, String metricIdentifier) { - return reportThresholdMapper.selectByPriority(deviceId, productId, categoryCode, metricIdentifier); + public ThingsThreshold find(Long deviceId, Long productId, String categoryCode, String metricIdentifier) { + return thingsThresholdMapper.selectByPriority(deviceId, productId, categoryCode, metricIdentifier); } } diff --git a/fastbee-report/src/main/java/com/fastbee/report/threshold/ReportThreshold.java b/fastbee-report/src/main/java/com/fastbee/report/threshold/ThingsThreshold.java similarity index 54% rename from fastbee-report/src/main/java/com/fastbee/report/threshold/ReportThreshold.java rename to fastbee-report/src/main/java/com/fastbee/report/threshold/ThingsThreshold.java index bbc1eb77..24f5540c 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/threshold/ReportThreshold.java +++ b/fastbee-report/src/main/java/com/fastbee/report/threshold/ThingsThreshold.java @@ -8,23 +8,23 @@ import java.math.BigDecimal; import java.util.Date; /** - * 报告结构化阈值(iot_report_threshold) + * 物模型监测阈值(iot_things_threshold) * - *

查询优先级:device_id > product_id > 全局默认(product_id=0)。

+ *

查询优先级:设备级(device_id) > 产品级(device_id=0)。设备阈值覆盖产品阈值,无全局默认层。

* * @author FastBee * @date 2026-08-16 */ @Data -@TableName("iot_report_threshold") -public class ReportThreshold { +@TableName("iot_things_threshold") +public class ThingsThreshold { private Long id; - /** 产品ID(0=全局默认) */ + /** 产品ID */ private Long productId; - /** 设备ID(0=不限定设备) */ + /** 设备ID(0=产品级阈值,作用于该产品下所有设备;非0=设备级阈值,覆盖产品级) */ private Long deviceId; private String monitorCategoryCode; @@ -37,6 +37,15 @@ public class ReportThreshold { private BigDecimal controlValue; + /** 变化速率预警值(单位/天),GB 50497-2019 双控口径 */ + private BigDecimal warningRate; + + /** 变化速率报警值(单位/天) */ + private BigDecimal alarmRate; + + /** 变化速率控制值(单位/天) */ + private BigDecimal controlRate; + private String unit; private String createBy; diff --git a/fastbee-report/src/main/java/com/fastbee/report/threshold/ReportThresholdProvider.java b/fastbee-report/src/main/java/com/fastbee/report/threshold/ThingsThresholdProvider.java similarity index 55% rename from fastbee-report/src/main/java/com/fastbee/report/threshold/ReportThresholdProvider.java rename to fastbee-report/src/main/java/com/fastbee/report/threshold/ThingsThresholdProvider.java index 411f0784..bbb4d61c 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/threshold/ReportThresholdProvider.java +++ b/fastbee-report/src/main/java/com/fastbee/report/threshold/ThingsThresholdProvider.java @@ -1,15 +1,15 @@ package com.fastbee.report.threshold; /** - * 报告阈值 Provider(Analyzer 只读结构化阈值,禁止硬编码阈值) + * 物模型阈值 Provider(Analyzer 只读结构化阈值,禁止硬编码阈值) * * @author FastBee * @date 2026-08-16 */ -public interface ReportThresholdProvider { +public interface ThingsThresholdProvider { /** * 查询指定设备 + 物模型标识符的结构化阈值(device > product > 全局) */ - ReportThreshold find(Long deviceId, Long productId, String categoryCode, String metricIdentifier); + ThingsThreshold find(Long deviceId, Long productId, String categoryCode, String metricIdentifier); } diff --git a/fastbee-report/src/main/resources/mapper/report/ReportRecordMapper.xml b/fastbee-report/src/main/resources/mapper/report/ReportRecordMapper.xml index f0b5e9c0..db9bd12e 100644 --- a/fastbee-report/src/main/resources/mapper/report/ReportRecordMapper.xml +++ b/fastbee-report/src/main/resources/mapper/report/ReportRecordMapper.xml @@ -7,25 +7,31 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + + delete from report_record + where create_time < date_sub(now(), interval #{days} day) + + diff --git a/fastbee-report/src/main/resources/mapper/report/ReportThresholdMapper.xml b/fastbee-report/src/main/resources/mapper/report/ThingsThresholdMapper.xml similarity index 74% rename from fastbee-report/src/main/resources/mapper/report/ReportThresholdMapper.xml rename to fastbee-report/src/main/resources/mapper/report/ThingsThresholdMapper.xml index 6b91b3d1..e9141f05 100644 --- a/fastbee-report/src/main/resources/mapper/report/ReportThresholdMapper.xml +++ b/fastbee-report/src/main/resources/mapper/report/ThingsThresholdMapper.xml @@ -2,13 +2,15 @@ - + - select id, product_id, device_id, monitor_category_code, metric_identifier, - warning_value, alarm_value, control_value, unit, + warning_value, alarm_value, control_value, + warning_rate, alarm_rate, control_rate, unit, create_by, create_time, update_by, update_time - from iot_report_threshold + from iot_things_threshold metric_identifier = #{metricIdentifier} @@ -17,8 +19,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" or monitor_category_code = '') and (device_id = #{deviceId} - or (device_id = 0 and product_id = #{productId}) - or (device_id = 0 and product_id = 0)) + or (device_id = 0 and product_id = #{productId})) order by case when device_id = #{deviceId} then 0 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 79d45989..89eec527 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 @@ -79,6 +79,19 @@ public class Device extends BaseEntity { @Excel(name = "设备编号") private String serialNumber; + /** + * 基准点设备ID(结构安全监测用) + * + *

相对测量类设备(如静力水准)的读数是相对基准点的,累计变化量需按 + * 「当前相对值 − 初始相对值」计算,其中相对值 = 本设备读数 − 基准点设备读数。

+ * + *

deviceId 全局唯一,因此天然支持跨网关引用(如 GW2 的监测点引用 GW1 下的参考点)。 + * 留空表示该设备为绝对量测量,不参与基准点换算。

+ */ + @ApiModelProperty("基准点设备ID(留空表示无需基准点换算)") + @Excel(name = "基准点设备ID") + private Long baselineDeviceId; + /** * 固件版本 */ 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 c3972dc4..ce77b044 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 @@ -40,6 +40,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + + + + + +