From c050141b176f03451f7e9ec96209d89487056ceb Mon Sep 17 00:00:00 2001 From: zhugongping Date: Thu, 10 Sep 2026 21:00:05 +0800 Subject: [PATCH] =?UTF-8?q?20260910:=E5=AF=BC=E5=87=BA=E6=8A=A5=E5=91=8A?= =?UTF-8?q?=E4=BC=98=E5=8C=96v2=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../report/ReportThresholdController.java | 191 +++++++ .../report/analysis/ValueFormatter.java | 128 +++++ .../report/analysis/WarningAnalyzer.java | 4 +- .../analyzer/AbstractDeviceAnalyzer.java | 516 ++++++++++++++++++ .../axial_force/AxialForceAnalyzer.java | 408 +++----------- .../analyzer/crack/CrackMeterAnalyzer.java | 405 +++----------- .../DeepDisplacementAnalyzer.java | 389 +++---------- .../inclinometer/InclinometerAnalyzer.java | 342 +----------- .../rain_gauge/RainGaugeAnalyzer.java | 398 +++----------- .../ranging_meter/RangingMeterAnalyzer.java | 343 +----------- .../static_level/StaticLevelAnalyzer.java | 464 ++++------------ .../strain_gauge/StrainGaugeAnalyzer.java | 358 +----------- .../thermometer/ThermometerAnalyzer.java | 345 +----------- .../vibrating_wire/VibratingWireAnalyzer.java | 325 +---------- .../water_level/WaterLevelAnalyzer.java | 325 ++--------- .../domain/vo/ReportThresholdQuery.java | 28 + .../fastbee/report/model/WarningAnalysis.java | 2 + .../fastbee/report/renderer/WordRenderer.java | 3 + .../service/IReportThresholdService.java | 56 ++ .../impl/ReportThresholdServiceImpl.java | 66 +++ .../templates/DEVICE_AXIAL_FORCE_V2.docx | Bin 38102 -> 38075 bytes .../templates/DEVICE_CRACK_METER_V2.docx | Bin 38101 -> 38075 bytes .../DEVICE_DEEP_DISPLACEMENT_V2.docx | Bin 38104 -> 38076 bytes .../templates/DEVICE_INCLINOMETER_V2.docx | Bin 38101 -> 38076 bytes .../templates/DEVICE_RAIN_GAUGE_V2.docx | Bin 38099 -> 38075 bytes .../templates/DEVICE_RANGING_METER_V2.docx | Bin 38100 -> 38075 bytes .../templates/DEVICE_STATIC_LEVEL_V2.docx | Bin 38104 -> 38078 bytes .../templates/DEVICE_STRAIN_GAUGE_V2.docx | Bin 38100 -> 38075 bytes .../templates/DEVICE_THERMOMETER_V2.docx | Bin 38101 -> 38073 bytes .../templates/DEVICE_VIBRATING_WIRE_V2.docx | Bin 38098 -> 38073 bytes .../templates/DEVICE_WATER_LEVEL_V2.docx | Bin 38101 -> 38075 bytes generate_professional_templates.py | 10 +- sql/init_report_threshold.sql | 55 ++ sql/init_threshold_with_table.sql | 77 +++ sql/menu_threshold.sql | 37 ++ yack-web/src/api/iot/threshold.js | 69 +++ 36 files changed, 1739 insertions(+), 3605 deletions(-) create mode 100644 fastbee-open-api/src/main/java/com/fastbee/controller/report/ReportThresholdController.java create mode 100644 fastbee-report/src/main/java/com/fastbee/report/analysis/ValueFormatter.java create mode 100644 fastbee-report/src/main/java/com/fastbee/report/analyzer/AbstractDeviceAnalyzer.java create mode 100644 fastbee-report/src/main/java/com/fastbee/report/domain/vo/ReportThresholdQuery.java create mode 100644 fastbee-report/src/main/java/com/fastbee/report/service/IReportThresholdService.java create mode 100644 fastbee-report/src/main/java/com/fastbee/report/service/impl/ReportThresholdServiceImpl.java create mode 100644 sql/init_report_threshold.sql create mode 100644 sql/init_threshold_with_table.sql create mode 100644 sql/menu_threshold.sql create mode 100644 yack-web/src/api/iot/threshold.js 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/ReportThresholdController.java new file mode 100644 index 00000000..8374d7b6 --- /dev/null +++ b/fastbee-open-api/src/main/java/com/fastbee/controller/report/ReportThresholdController.java @@ -0,0 +1,191 @@ +package com.fastbee.controller.report; + +import com.fastbee.common.annotation.Log; +import com.fastbee.common.core.controller.BaseController; +import com.fastbee.common.core.domain.AjaxResult; +import com.fastbee.common.core.page.TableDataInfo; +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 io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletResponse; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 监测阈值配置 Controller + * + * @author FastBee + * @date 2026-09-10 + */ +@Api(tags = "监测阈值配置") +@RestController +@RequestMapping("/iot/threshold") +public class ReportThresholdController extends BaseController { + + @Autowired + private IReportThresholdService reportThresholdService; + + @Autowired + private IProductService productService; + + @Autowired + private IDeviceService deviceService; + + /** + * 查询阈值列表 + */ + @PreAuthorize("@ss.hasPermi('iot:threshold:list')") + @GetMapping("/list") + @ApiOperation("阈值分页列表") + public TableDataInfo list(ReportThresholdQuery query) { + startPage(); + List list = reportThresholdService.selectThresholdList(query); + return getDataTable(list); + } + + /** + * 导出阈值列表 + */ + @PreAuthorize("@ss.hasPermi('iot:threshold:export')") + @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); + util.exportExcel(response, list, "监测阈值数据"); + } + + /** + * 获取阈值详细信息 + */ + @ApiOperation("获取阈值详情") + @PreAuthorize("@ss.hasPermi('iot:threshold:query')") + @GetMapping(value = "/{id}") + public AjaxResult getInfo(@PathVariable("id") Long id) { + return AjaxResult.success(reportThresholdService.selectThresholdById(id)); + } + + /** + * 新增阈值 + */ + @PreAuthorize("@ss.hasPermi('iot:threshold:add')") + @Log(title = "监测阈值配置", businessType = BusinessType.INSERT) + @PostMapping + @ApiOperation("新增阈值") + public AjaxResult add(@RequestBody ReportThreshold threshold) { + return toAjax(reportThresholdService.insertThreshold(threshold)); + } + + /** + * 修改阈值 + */ + @PreAuthorize("@ss.hasPermi('iot:threshold:edit')") + @Log(title = "监测阈值配置", businessType = BusinessType.UPDATE) + @PutMapping + @ApiOperation("修改阈值") + public AjaxResult edit(@RequestBody ReportThreshold threshold) { + return toAjax(reportThresholdService.updateThreshold(threshold)); + } + + /** + * 删除阈值 + */ + @PreAuthorize("@ss.hasPermi('iot:threshold:remove')") + @Log(title = "监测阈值配置", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + @ApiOperation("删除阈值") + public AjaxResult remove(@PathVariable Long[] ids) { + return toAjax(reportThresholdService.deleteThresholdByIds(ids)); + } + + /** + * 获取监测类别列表(前端下拉框) + */ + @GetMapping("/categories") + @ApiOperation("获取监测类别列表") + public AjaxResult getCategories() { + List> categories = new ArrayList<>(); + for (com.fastbee.common.enums.MonitorCategory category : com.fastbee.common.enums.MonitorCategory.values()) { + if (category != com.fastbee.common.enums.MonitorCategory.UNKNOWN) { + Map map = new HashMap<>(); + map.put("code", category.getCode()); + map.put("desc", category.getDesc()); + categories.add(map); + } + } + return AjaxResult.success(categories); + } + + /** + * 获取产品列表(阈值配置页面使用,无需额外权限) + */ + @GetMapping("/products") + @ApiOperation("获取产品列表") + public AjaxResult getProducts() { + List> products = new ArrayList<>(); + try { + // 调用产品服务获取简短列表 + List productList = productService.selectProductShortList(null); + if (productList != null) { + for (Object obj : productList) { + if (obj instanceof Map) { + Map map = (Map) obj; + Map product = new HashMap<>(); + product.put("productId", map.get("productId")); + product.put("productName", map.get("productName")); + products.add(product); + } + } + } + } catch (Exception e) { + // 忽略异常,返回空列表 + } + return AjaxResult.success(products); + } + + /** + * 获取设备列表(阈值配置页面使用,无需额外权限) + */ + @GetMapping("/devices") + @ApiOperation("获取设备列表") + public AjaxResult getDevices(@RequestParam(required = false) Long productId) { + List> devices = new ArrayList<>(); + try { + // 调用设备服务获取简短列表 + List deviceList = deviceService.selectDeviceShortList(null); + if (deviceList != null) { + for (Object obj : deviceList) { + if (obj instanceof Map) { + Map map = (Map) obj; + Long pid = (Long) map.get("productId"); + // 如果指定了产品ID,只返回该产品的设备 + if (productId != null && !productId.equals(pid)) { + continue; + } + Map device = new HashMap<>(); + device.put("deviceId", map.get("deviceId")); + device.put("deviceName", map.get("deviceName")); + device.put("productId", pid); + devices.add(device); + } + } + } + } catch (Exception e) { + // 忽略异常,返回空列表 + } + return AjaxResult.success(devices); + } +} \ No newline at end of file diff --git a/fastbee-report/src/main/java/com/fastbee/report/analysis/ValueFormatter.java b/fastbee-report/src/main/java/com/fastbee/report/analysis/ValueFormatter.java new file mode 100644 index 00000000..3cb8f805 --- /dev/null +++ b/fastbee-report/src/main/java/com/fastbee/report/analysis/ValueFormatter.java @@ -0,0 +1,128 @@ +package com.fastbee.report.analysis; + +import com.fastbee.common.enums.MonitorCategory; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.HashMap; +import java.util.Map; + +/** + * 值格式化器:类别感知的精度与单位策略 + * + *

解决报告输出中:

+ *
    + *
  • 6 位小数不专业(深部位移/静力水准应为 mm 保留 2 位)
  • + *
  • 单位缺失(物模型未配置 unit 时全文无单位)
  • + *
  • 变化速率单位拼接错误(unit 为空 → rateUnit="/天")
  • + *
+ * + * @author FastBee + * @date 2026-09-10 + */ +@Component +public class ValueFormatter { + + /** + * 按监测类别配置默认单位与精度 + * key: 监测类别枚举 + * value: [默认单位, 小数位数] + */ + private static final Map CATEGORY_FORMATS = new HashMap<>(); + + static { + CATEGORY_FORMATS.put(MonitorCategory.DEEP_DISPLACEMENT, new CategoryFormat("mm", 2)); + CATEGORY_FORMATS.put(MonitorCategory.STATIC_LEVEL, new CategoryFormat("mm", 2)); + CATEGORY_FORMATS.put(MonitorCategory.INCLINOMETER, new CategoryFormat("°", 2)); + CATEGORY_FORMATS.put(MonitorCategory.THERMOMETER, new CategoryFormat("℃", 1)); + CATEGORY_FORMATS.put(MonitorCategory.CRACK_METER, new CategoryFormat("mm", 2)); + CATEGORY_FORMATS.put(MonitorCategory.AXIAL_FORCE, new CategoryFormat("kN", 2)); + CATEGORY_FORMATS.put(MonitorCategory.STRAIN_GAUGE, new CategoryFormat("με", 1)); + CATEGORY_FORMATS.put(MonitorCategory.VIBRATING_WIRE, new CategoryFormat("Hz", 2)); + CATEGORY_FORMATS.put(MonitorCategory.WATER_LEVEL, new CategoryFormat("m", 3)); + CATEGORY_FORMATS.put(MonitorCategory.RAIN_GAUGE, new CategoryFormat("mm", 1)); + CATEGORY_FORMATS.put(MonitorCategory.RANGING_METER, new CategoryFormat("mm", 2)); + // UNKNOWN 类别无默认配置,使用兜底策略 + } + + /** + * 类别格式配置 + */ + public static class CategoryFormat { + public final String defaultUnit; + public final int decimalPlaces; + + CategoryFormat(String unit, int decimals) { + this.defaultUnit = unit; + this.decimalPlaces = decimals; + } + } + + /** + * 格式化数值:类别感知精度 + 去尾零 + * + * @param value 数值(可能为 null) + * @param category 监测类别 + * @return 格式化字符串,null 返回 "-" + */ + public String format(BigDecimal value, MonitorCategory category) { + if (value == null) { + return "-"; + } + CategoryFormat fmt = CATEGORY_FORMATS.get(category); + int decimals = (fmt != null) ? fmt.decimalPlaces : 6; + BigDecimal scaled = value.setScale(decimals, RoundingMode.HALF_UP).stripTrailingZeros(); + return scaled.compareTo(BigDecimal.ZERO) == 0 ? "0" : scaled.toPlainString(); + } + + /** + * 单位兜底:物模型单位优先,否则用类别默认单位 + * + * @param modelUnit 物模型配置的单位(可能为 null 或空) + * @param category 监测类别 + * @return 解析后的单位,无默认则返回空字符串 + */ + public String resolveUnit(String modelUnit, MonitorCategory category) { + if (modelUnit != null && !modelUnit.trim().isEmpty()) { + return modelUnit.trim(); + } + CategoryFormat fmt = CATEGORY_FORMATS.get(category); + return (fmt != null) ? fmt.defaultUnit : ""; + } + + /** + * 带单位的显示值 + * + * @param value 数值 + * @param unit 物模型单位(可能为空) + * @param category 监测类别 + * @return "数值 单位" 或纯数值(单位为空且无默认) + */ + public String formatWithUnit(BigDecimal value, String unit, MonitorCategory category) { + String formatted = format(value, category); + String resolvedUnit = resolveUnit(unit, category); + return resolvedUnit.isEmpty() ? formatted : formatted + " " + resolvedUnit; + } + + /** + * 变化速率单位(mm/d、m/d 等) + * + * @param baseUnit 基础单位(已兜底) + * @return "单位/天" 或 "/天"(基础单位为空) + */ + public String rateUnit(String baseUnit) { + String unit = (baseUnit == null || baseUnit.trim().isEmpty()) ? "" : baseUnit.trim(); + return unit.isEmpty() ? "/天" : unit + "/天"; + } + + /** + * 获取类别的默认格式配置 + * + * @param category 监测类别 + * @return 格式配置,无配置返回 null + */ + public CategoryFormat getCategoryFormat(MonitorCategory category) { + return CATEGORY_FORMATS.get(category); + } +} \ No newline at end of file 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 3e557eb7..9cc472bb 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 @@ -31,8 +31,8 @@ public class WarningAnalyzer { return result; } if (threshold == null) { - result.setStatus(WarningAnalysis.STATUS_NORMAL); - result.setDescription("未配置结构化阈值,暂按正常处理"); + result.setStatus(WarningAnalysis.STATUS_UNKNOWN); + result.setDescription("未配置预警阈值,无法评估安全等级"); return result; } 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 new file mode 100644 index 00000000..3e4237ea --- /dev/null +++ b/fastbee-report/src/main/java/com/fastbee/report/analyzer/AbstractDeviceAnalyzer.java @@ -0,0 +1,516 @@ +package com.fastbee.report.analyzer; + +import com.fastbee.common.enums.MonitorCategory; +import com.fastbee.common.utils.StringUtils; +import com.fastbee.iot.domain.ThingsModel; +import com.fastbee.report.analysis.PrimaryMetricResolver; +import com.fastbee.report.analysis.QualityAnalyzer; +import com.fastbee.report.analysis.RateCalculator; +import com.fastbee.report.analysis.StatisticsCalculator; +import com.fastbee.report.analysis.TrendAnalyzer; +import com.fastbee.report.analysis.ValueFormatter; +import com.fastbee.report.analysis.WarningAnalyzer; +import com.fastbee.report.api.ReportRequest; +import com.fastbee.report.chart.MetricChartBuilder; +import com.fastbee.report.core.ReportContext; +import com.fastbee.report.data.MetricData; +import com.fastbee.report.model.AnalysisResult; +import com.fastbee.report.model.Conclusion; +import com.fastbee.report.model.MetricPointData; +import com.fastbee.report.model.QualityAnalysis; +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 org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 监测报告分析器抽象基类(模板方法模式) + * + *

抽取 11 类设备分析器的公共骨架,子类只需覆盖差异点:

+ *
    + *
  • {@link #supportedCategory()} 监测类别
  • + *
  • {@link #primaryMetricKeywords()} 主指标关键词
  • + *
  • {@link #allowAuxiliaryAsPrimary()} 是否允许辅助指标作主指标(倾角计/温度计)
  • + *
  • {@link #directionText(String)} 方向文本映射
  • + *
  • {@link #buildSummary(Statistics, TrendAnalysis, String, TimeWindow)} 项目概况
  • + *
  • {@link #buildProfessionalAdvice(Statistics, TrendAnalysis, WarningAnalysis, String)} 专业建议
  • + *
  • {@link #addDomainConclusions(List, AnalysisResult, ReportContext)} 领域特定结论
  • + *
+ * + * @author FastBee + * @date 2026-09-10 + */ +public abstract class AbstractDeviceAnalyzer implements ReportAnalyzer { + + @Resource + protected StatisticsCalculator statisticsCalculator; + + @Resource + protected TrendAnalyzer trendAnalyzer; + + @Resource + protected RateCalculator rateCalculator; + + @Resource + protected QualityAnalyzer qualityAnalyzer; + + @Resource + protected WarningAnalyzer warningAnalyzer; + + @Resource + protected PrimaryMetricResolver primaryMetricResolver; + + @Resource + protected MetricChartBuilder metricChartBuilder; + + @Resource + protected ValueFormatter valueFormatter; + + // ===== 子类必须实现 ===== + + /** + * 支持的监测类别 + */ + protected abstract MonitorCategory supportedCategory(); + + /** + * 主指标关键词(用于 PrimaryMetricResolver 匹配) + * 示例:["水位", "液位", "water"] 或 ["沉降", "高程", "settlement"] + */ + protected abstract String[] primaryMetricKeywords(); + + // ===== 子类可选覆盖 ===== + + /** + * 是否允许辅助指标(角度/温度等)作为主指标 + * 默认 false;倾角计、温度计需覆盖为 true + */ + protected boolean allowAuxiliaryAsPrimary() { + return false; + } + + /** + * 方向文本映射:UP/DOWN/STABLE → 领域术语 + * 默认:增大/减小/稳定 + */ + protected String directionText(String direction) { + if (TrendAnalysis.DIRECTION_UP.equals(direction)) { + return "增大"; + } + if (TrendAnalysis.DIRECTION_DOWN.equals(direction)) { + return "减小"; + } + return "稳定"; + } + + /** + * 生成项目概况总结 + * 子类可覆盖以调整领域术语(如"高程"vs"水位"、"沉降"vs"变化量") + */ + protected String buildSummary(Statistics statistics, TrendAnalysis trend, String unit, TimeWindow window) { + if (statistics == null || statistics.getCount() == 0) { + return "统计期内无有效监测数据。"; + } + + String direction = trend == null ? "待分析" : directionText(trend.getDirection()); + BigDecimal rate = rateCalculator.ratePerDay(statistics, + Duration.between(window.getStartTime(), window.getEndTime()).getSeconds()); + + // 使用 ValueFormatter 格式化 + String formattedCurrent = valueFormatter.format(statistics.getCurrent(), supportedCategory()); + String formattedDelta = valueFormatter.format(statistics.getDelta(), supportedCategory()); + String formattedRate = valueFormatter.format(rate, supportedCategory()); + String formattedMax = valueFormatter.format(statistics.getMax(), supportedCategory()); + String formattedMin = valueFormatter.format(statistics.getMin(), supportedCategory()); + String formattedAvg = valueFormatter.format(statistics.getAvg(), supportedCategory()); + + String resolvedUnit = valueFormatter.resolveUnit(unit, supportedCategory()); + + return String.format( + "本期%s监测数据显示:当前值为%s%s,较初始值变化%s%s,变化速率为%s%s/天,呈%s趋势。" + + "统计期内最大值%s%s,最小值%s%s,平均值%s%s。", + supportedCategory().getDesc(), + formattedCurrent, resolvedUnit, + formattedDelta, resolvedUnit, + formattedRate, resolvedUnit, + direction, + formattedMax, resolvedUnit, + formattedMin, resolvedUnit, + formattedAvg, resolvedUnit + ); + } + + /** + * 生成专业分析建议 + */ + protected String buildProfessionalAdvice(Statistics statistics, TrendAnalysis trend, WarningAnalysis warning, String unit) { + if (statistics == null || statistics.getCount() == 0) { + return "建议检查设备上报状态,确保数据采集正常。"; + } + + StringBuilder advice = new StringBuilder(); + advice.append("专业分析:"); + + // 基于趋势方向给出建议(默认实现,子类可覆盖) + if (trend != null) { + if (TrendAnalysis.DIRECTION_UP.equals(trend.getDirection())) { + advice.append("监测值呈增大趋势,需关注变化原因及对结构的影响。"); + } else if (TrendAnalysis.DIRECTION_DOWN.equals(trend.getDirection())) { + advice.append("监测值呈减小趋势,需关注变化原因及对结构的影响。"); + } else { + advice.append("监测值变化稳定。"); + } + } + + // 基于预警状态给出建议 + if (warning != null) { + if (WarningAnalysis.STATUS_ALARM.equals(warning.getStatus())) { + advice.append("当前处于报警状态,建议立即启动应急预案,组织专业人员进行安全评估。"); + } else if (WarningAnalysis.STATUS_WARNING.equals(warning.getStatus())) { + advice.append("当前处于预警状态,建议加强巡视检查,制定应急预案。"); + } + } + + return advice.toString(); + } + + /** + * 添加领域特定结论(如静力水准的不均匀沉降、轴力的拉压状态) + * 默认空实现,子类按需覆盖 + */ + protected void addDomainConclusions(List conclusions, AnalysisResult result, ReportContext context) { + // 默认无额外结论 + } + + // ===== 公共骨架 ===== + + @Override + public boolean supports(MonitorCategory category) { + return category == supportedCategory(); + } + + @Override + public AnalysisResult analyze(ReportContext context, ReportRequest request) { + AnalysisResult result = new AnalysisResult(); + + // 1. 按物模型标识符分组历史数据 + Map> byIdentity = context.getMetricDataList().stream() + .filter(d -> d.getIdentity() != null) + .collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList())); + + // 2. 获取所有监测物模型 + Map allModels = getAllModels(context); + + if (allModels.isEmpty()) { + result.getConclusions().add(new Conclusion(1, "设备未配置监测物模型,请联系管理员配置。")); + return result; + } + + // 3. 为每个物模型生成统计数据(含单位兜底) + for (Map.Entry entry : allModels.entrySet()) { + String identity = entry.getKey(); + ThingsModel model = entry.getValue(); + List data = byIdentity.getOrDefault(identity, Collections.emptyList()); + + ReportThreshold threshold = context.getThresholds().get(identity); + BigDecimal controlValue = threshold == null ? null : threshold.getControlValue(); + + Statistics statistics = statisticsCalculator.calculate(data); + backfillModelInfo(statistics, model, context.getCategory()); + + TrendAnalysis trend = trendAnalyzer.analyze(data); + QualityAnalysis quality = qualityAnalyzer.analyze(data, context.getTimeWindow(), controlValue); + WarningAnalysis warning = warningAnalyzer.analyze( + identity, statistics.getModelName(), statistics.getCurrent(), threshold); + + result.getStatistics().put(identity, statistics); + result.getTrends().put(identity, trend); + result.getQualities().put(identity, quality); + result.getWarnings().put(identity, warning); + } + + // 4. 主指标(关键词匹配 + 辅助量过滤) + String primaryIdentity = primaryMetricResolver.resolve( + context, byIdentity, allowAuxiliaryAsPrimary(), primaryMetricKeywords()); + result.setPrimaryIdentifier(primaryIdentity); + + // 5. 组装测点数据(过滤辅助量、排序) + result.setMetricPoints(buildMetricPointsFiltered(result, context)); + + // 6. 生成结论 + buildConclusions(result, context, primaryIdentity); + + // 7. 领域特定结论 + addDomainConclusions(result.getConclusions(), result, context); + + // 8. 图表 + buildChart(result, context, primaryIdentity); + + return result; + } + + /** + * 获取所有监测物模型 + */ + protected Map getAllModels(ReportContext context) { + return context.getThingsModels().stream() + .filter(m -> m.getIdentifier() != null) + .collect(Collectors.toMap(ThingsModel::getIdentifier, m -> m, (o, n) -> n, LinkedHashMap::new)); + } + + /** + * 回填物模型信息 + 单位兜底 + */ + protected void backfillModelInfo(Statistics statistics, ThingsModel model, MonitorCategory category) { + if (statistics.getModelName() == null || statistics.getModelName().isEmpty()) { + statistics.setModelName(model.getModelName()); + } + // 单位兜底:物模型优先,否则按类别默认 + String resolvedUnit = valueFormatter.resolveUnit(model.getUnit(), category); + statistics.setUnit(resolvedUnit); + statistics.setIdentifier(model.getIdentifier()); + } + + /** + * 组装测点数据(过滤辅助指标 + 主指标排第一) + */ + protected List buildMetricPointsFiltered(AnalysisResult result, ReportContext context) { + List all = new ArrayList<>(); + String primary = result.getPrimaryIdentifier(); + MetricPointData primaryPoint = null; + + for (Map.Entry entry : result.getStatistics().entrySet()) { + String identity = entry.getKey(); + Statistics stats = entry.getValue(); + + // 过滤辅助指标(角度/温度/电压/电量/信号/湿度) + if (!allowAuxiliaryAsPrimary() && isAuxiliaryMetric(stats.getModelName(), identity)) { + continue; + } + + // 过滤无数据/全 0 行 + if (stats.getCurrent() == null || stats.getCount() == 0) { + continue; + } + if (isAllZero(stats)) { + continue; + } + + MetricPointData point = buildMetricPoint(stats, result.getWarnings().get(identity), context); + if (identity.equals(primary)) { + primaryPoint = point; + } else { + all.add(point); + } + } + + // 主指标排第一 + List filtered = new ArrayList<>(); + if (primaryPoint != null) { + filtered.add(primaryPoint); + } + filtered.addAll(all); + return filtered; + } + + /** + * 判断是否为辅助指标 + */ + protected boolean isAuxiliaryMetric(String modelName, String identifier) { + return primaryMetricResolver.isAuxiliary(modelName + " " + identifier); + } + + /** + * 判断统计值是否全为 0(或无效) + */ + protected boolean isAllZero(Statistics stats) { + return stats.getCurrent() != null + && stats.getCurrent().compareTo(BigDecimal.ZERO) == 0 + && stats.getInitial() != null + && stats.getInitial().compareTo(BigDecimal.ZERO) == 0; + } + + /** + * 构建单个测点数据(使用 ValueFormatter 格式化) + */ + protected MetricPointData buildMetricPoint(Statistics stats, WarningAnalysis warning, ReportContext context) { + String resolvedUnit = valueFormatter.resolveUnit(stats.getUnit(), context.getCategory()); + String rateUnit = valueFormatter.rateUnit(resolvedUnit); + + TimeWindow window = context.getTimeWindow(); + BigDecimal rate = rateCalculator.ratePerDay(stats, + Duration.between(window.getStartTime(), window.getEndTime()).getSeconds()); + + return MetricPointData.builder() + .identifier(stats.getIdentifier()) + .modelName(stats.getModelName()) + .initialValue(stats.getInitial()) + .initialUnit(resolvedUnit) + .currentValue(stats.getCurrent()) + .currentUnit(resolvedUnit) + .changeValue(stats.getDelta()) + .changeUnit(resolvedUnit) + .changeRate(rate) + .rateUnit(rateUnit) + .maxValue(stats.getMax()) + .minValue(stats.getMin()) + .avgValue(stats.getAvg()) + .warningStatus(warning == null ? null : warning.getStatus()) + .build(); + } + + /** + * 生成结论列表 + */ + protected void buildConclusions(AnalysisResult result, ReportContext context, String primaryIdentity) { + Statistics statistics = result.getStatistics().get(primaryIdentity); + TrendAnalysis trend = result.getTrends().get(primaryIdentity); + QualityAnalysis quality = result.getQualities().get(primaryIdentity); + WarningAnalysis warning = result.getWarnings().get(primaryIdentity); + TimeWindow window = context.getTimeWindow(); + + // 生成项目概况总结 + String unit = statistics == null ? "" : statistics.getUnit(); + String summary = buildSummary(statistics, trend, unit, window); + result.setSummary(summary); + + // 生成专业分析建议 + String advice = buildProfessionalAdvice(statistics, trend, warning, unit); + result.setProfessionalAdvice(advice); + + // 设置安全评估 + if (warning != null) { + result.setSafetyLevel(convertWarningToSafetyLevel(warning.getStatus())); + result.setSafetyDescription(warning.getDescription()); + } + + // 设置控制值 + ReportThreshold threshold = context.getThresholds().get(primaryIdentity); + if (threshold != null && threshold.getControlValue() != null) { + result.setControlValue(threshold.getControlValue()); + result.setControlValueUnit(threshold.getUnit()); + } + + // 生成详细结论 + List conclusions = new ArrayList<>(); + int seq = 1; + + if (statistics == null || statistics.getCount() == 0) { + conclusions.add(new Conclusion(seq++, "统计期内无有效监测数据,请检查设备上报状态与时间范围。")); + } else { + // 结论1:主指标变化 + String formattedInitial = valueFormatter.format(statistics.getInitial(), context.getCategory()); + String formattedCurrent = valueFormatter.format(statistics.getCurrent(), context.getCategory()); + String formattedDelta = valueFormatter.format(statistics.getDelta(), context.getCategory()); + String resolvedUnit = valueFormatter.resolveUnit(unit, context.getCategory()); + String direction = trend == null ? "待分析" : directionText(trend.getDirection()); + + conclusions.add(new Conclusion(seq++, String.format( + "统计期内%s由 %s%s 变化至 %s%s,变化量 %s%s,呈%s趋势。", + statistics.getModelName(), + formattedInitial, resolvedUnit, + formattedCurrent, resolvedUnit, + formattedDelta, resolvedUnit, + direction))); + + // 结论2:速率与极值(使用回归斜率而非首尾差) + if (trend != null && trend.getSlope() != null) { + String formattedSlope = valueFormatter.format(trend.getSlope(), context.getCategory()); + conclusions.add(new Conclusion(seq++, String.format( + "回归斜率 %s%s/天;统计期最大值 %s%s、最小值 %s%s、平均值 %s%s。", + formattedSlope, resolvedUnit, + valueFormatter.format(statistics.getMax(), context.getCategory()), resolvedUnit, + valueFormatter.format(statistics.getMin(), context.getCategory()), resolvedUnit, + valueFormatter.format(statistics.getAvg(), context.getCategory()), resolvedUnit))); + + // R² 门槛提示 + if (trend.getRSquared() != null && trend.getRSquared().compareTo(new BigDecimal("0.5")) < 0) { + conclusions.add(new Conclusion(seq++, String.format( + "拟合优度较低(R²=%.2f),趋势性不明显,数据波动较大。", + trend.getRSquared().doubleValue()))); + } + } + + // 控制值对比 + if (result.getControlValue() != null && result.getControlValue().compareTo(BigDecimal.ZERO) != 0) { + BigDecimal ratio = statistics.getDelta().abs() + .divide(result.getControlValue(), 4, BigDecimal.ROUND_HALF_UP) + .multiply(new BigDecimal("100")); + conclusions.add(new Conclusion(seq++, String.format( + "变化量占控制值比例:%.2f%%(控制值:%s%s)。", + ratio.doubleValue(), + result.getControlValue().toPlainString(), + result.getControlValueUnit()))); + } + } + + // 趋势分析 + if (trend != null && StringUtils.isNotEmpty(trend.getDescription())) { + conclusions.add(new Conclusion(seq++, "趋势分析:" + trend.getDescription() + "。")); + } + + // 预警状态 + if (warning != null && StringUtils.isNotEmpty(warning.getDescription())) { + String statusText = statusText(warning.getStatus()); + conclusions.add(new Conclusion(seq++, "预警状态:" + statusText + "。" + warning.getDescription() + "。")); + } + + // 数据质量 + if (quality != null && StringUtils.isNotEmpty(quality.getDescription())) { + conclusions.add(new Conclusion(seq, "数据质量:" + quality.getDescription() + "。")); + } + + result.setConclusions(conclusions); + } + + /** + * 构建图表 + */ + protected void buildChart(AnalysisResult result, ReportContext context, String primaryIdentity) { + Map> byIdentity = context.getMetricDataList().stream() + .filter(d -> d.getIdentity() != null) + .collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList())); + Map charts = metricChartBuilder.build(result.getStatistics(), primaryIdentity, byIdentity); + result.getCharts().putAll(metricChartBuilder.fillPlaceholders(charts)); + } + + /** + * 将预警状态转换为安全等级 + */ + protected String convertWarningToSafetyLevel(String warningStatus) { + if (WarningAnalysis.STATUS_ALARM.equals(warningStatus)) { + return "DANGER"; + } else if (WarningAnalysis.STATUS_WARNING.equals(warningStatus)) { + return "WARNING"; + } + return "SAFE"; + } + + /** + * 预警状态转中文 + */ + protected String statusText(String status) { + if (WarningAnalysis.STATUS_ALARM.equals(status)) { + return "报警"; + } + if (WarningAnalysis.STATUS_WARNING.equals(status)) { + return "预警"; + } + if (WarningAnalysis.STATUS_UNKNOWN.equals(status)) { + return "待评估"; + } + return "正常"; + } +} \ No newline at end of file diff --git a/fastbee-report/src/main/java/com/fastbee/report/analyzer/axial_force/AxialForceAnalyzer.java b/fastbee-report/src/main/java/com/fastbee/report/analyzer/axial_force/AxialForceAnalyzer.java index 1efef286..55864208 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/analyzer/axial_force/AxialForceAnalyzer.java +++ b/fastbee-report/src/main/java/com/fastbee/report/analyzer/axial_force/AxialForceAnalyzer.java @@ -1,360 +1,36 @@ package com.fastbee.report.analyzer.axial_force; import com.fastbee.common.enums.MonitorCategory; -import com.fastbee.common.utils.StringUtils; -import com.fastbee.iot.domain.ThingsModel; -import com.fastbee.report.analysis.PrimaryMetricResolver; -import com.fastbee.report.analysis.QualityAnalyzer; -import com.fastbee.report.analysis.RateCalculator; -import com.fastbee.report.analysis.StatisticsCalculator; -import com.fastbee.report.analysis.TrendAnalyzer; -import com.fastbee.report.analysis.WarningAnalyzer; -import com.fastbee.report.analyzer.ReportAnalyzer; -import com.fastbee.report.api.ReportRequest; -import com.fastbee.report.chart.ChartGenerator; -import com.fastbee.report.chart.MetricChartBuilder; -import com.fastbee.report.core.ReportContext; -import com.fastbee.report.data.MetricData; -import com.fastbee.report.model.AnalysisResult; -import com.fastbee.report.model.Conclusion; -import com.fastbee.report.model.MetricPointData; -import com.fastbee.report.model.QualityAnalysis; +import com.fastbee.report.analyzer.AbstractDeviceAnalyzer; 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 org.springframework.stereotype.Component; -import javax.annotation.Resource; import java.math.BigDecimal; -import java.time.Duration; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.stream.Collectors; /** - * 轴力/应力专项分析器 + * 轴力计专项分析器 * * @author FastBee * @date 2026-08-23 */ @Component -public class AxialForceAnalyzer implements ReportAnalyzer { - - @Resource - private StatisticsCalculator statisticsCalculator; - - @Resource - private TrendAnalyzer trendAnalyzer; - - @Resource - private RateCalculator rateCalculator; - - @Resource - private QualityAnalyzer qualityAnalyzer; - - @Resource - private WarningAnalyzer warningAnalyzer; - - @Resource - private ChartGenerator chartGenerator; - - @Resource - private PrimaryMetricResolver primaryMetricResolver; - - @Resource - private MetricChartBuilder metricChartBuilder; +public class AxialForceAnalyzer extends AbstractDeviceAnalyzer { @Override - public boolean supports(MonitorCategory category) { - return category == MonitorCategory.AXIAL_FORCE; + protected MonitorCategory supportedCategory() { + return MonitorCategory.AXIAL_FORCE; } @Override - public AnalysisResult analyze(ReportContext context, ReportRequest request) { - AnalysisResult result = new AnalysisResult(); - - // 1. 按物模型标识符分组历史数据 - Map> byIdentity = context.getMetricDataList().stream() - .filter(d -> d.getIdentity() != null) - .collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList())); - - // 2. 获取所有监测物模型(即使没有数据也要显示) - Map allModels = context.getThingsModels().stream() - .filter(m -> m.getIdentifier() != null) - .collect(Collectors.toMap(ThingsModel::getIdentifier, m -> m, (o, n) -> n, LinkedHashMap::new)); - - if (allModels.isEmpty()) { - result.getConclusions().add(new Conclusion(1, "设备未配置监测物模型,请联系管理员配置。")); - return result; - } - - // 3. 为每个物模型生成统计数据(包括无数据的物模型) - for (Map.Entry entry : allModels.entrySet()) { - String identity = entry.getKey(); - ThingsModel model = entry.getValue(); - List data = byIdentity.getOrDefault(identity, new ArrayList<>()); - - ReportThreshold threshold = context.getThresholds().get(identity); - BigDecimal controlValue = threshold == null ? null : threshold.getControlValue(); - - Statistics statistics = statisticsCalculator.calculate(data); - // 补充物模型信息(确保即使无数据也有 modelName 和 unit) - if (statistics.getModelName() == null || statistics.getModelName().isEmpty()) { - statistics.setModelName(model.getModelName()); - } - if (statistics.getUnit() == null || statistics.getUnit().isEmpty()) { - statistics.setUnit(model.getUnit()); - } - statistics.setIdentifier(identity); - - TrendAnalysis trend = trendAnalyzer.analyze(data); - QualityAnalysis quality = qualityAnalyzer.analyze(data, context.getTimeWindow(), controlValue); - WarningAnalysis warning = warningAnalyzer.analyze( - identity, statistics.getModelName(), statistics.getCurrent(), threshold); - - result.getStatistics().put(identity, statistics); - result.getTrends().put(identity, trend); - result.getQualities().put(identity, quality); - result.getWarnings().put(identity, warning); - } - - // 统一主指标口径:优先名称/标识符命中关键词的物模型,排除角度/温度/电压等辅助量 - String primaryIdentity = primaryMetricResolver.resolve(context, byIdentity, false, "轴力", "应力", "force", "stress"); - result.setPrimaryIdentifier(primaryIdentity); - buildConclusions(result, context, primaryIdentity); - buildChart(result, context, primaryIdentity); - return result; + protected String[] primaryMetricKeywords() { + return new String[]{"轴力", "应力", "force", "stress"}; } - private void buildConclusions(AnalysisResult result, ReportContext context, String primaryIdentity) { - Statistics statistics = result.getStatistics().get(primaryIdentity); - TrendAnalysis trend = result.getTrends().get(primaryIdentity); - QualityAnalysis quality = result.getQualities().get(primaryIdentity); - WarningAnalysis warning = result.getWarnings().get(primaryIdentity); - TimeWindow window = context.getTimeWindow(); - String unit = statistics == null ? "" : (statistics.getUnit() == null ? "" : statistics.getUnit()); - - // 生成项目概况总结 - String summary = buildSummary(statistics, trend, unit, window); - result.setSummary(summary); - - // 生成专业分析建议 - String advice = buildProfessionalAdvice(statistics, trend, warning, unit); - result.setProfessionalAdvice(advice); - - // 设置安全评估 - if (warning != null) { - result.setSafetyLevel(convertWarningToSafetyLevel(warning.getStatus())); - result.setSafetyDescription(warning.getDescription()); - } - - // 设置控制值 - ReportThreshold threshold = context.getThresholds().get(primaryIdentity); - if (threshold != null && threshold.getControlValue() != null) { - result.setControlValue(threshold.getControlValue()); - result.setControlValueUnit(threshold.getUnit()); - } - - // 组装多测点数据(支持多物模型标识符,如红线/白线/绿线) - List metricPoints = buildMetricPoints(result, context); - result.setMetricPoints(metricPoints); - - // 生成详细结论 - List conclusions = new ArrayList<>(); - int seq = 1; - if (statistics == null || statistics.getCount() == 0) { - conclusions.add(new Conclusion(seq++, "统计期内无有效监测数据,请检查设备上报状态与时间范围。")); - } else { - String forceState = (statistics.getCurrent() != null && statistics.getCurrent().compareTo(BigDecimal.ZERO) >= 0) ? "受拉" : "受压"; - String direction = trend == null ? "待分析" : directionText(trend.getDirection()); - conclusions.add(new Conclusion(seq++, String.format( - "统计期内轴力/应力由 %s%s 变化至 %s%s,变化量 %s%s,当前处于%s状态,轴力呈%s趋势。", - statistics.getInitial().toPlainString(), unit, - statistics.getCurrent().toPlainString(), unit, - statistics.getDelta().toPlainString(), unit, - forceState, direction))); - - BigDecimal rate = rateCalculator.ratePerDay(statistics, - Duration.between(window.getStartTime(), window.getEndTime()).getSeconds()); - conclusions.add(new Conclusion(seq++, String.format( - "平均变化速率 %s%s/天;统计期最大轴力 %s%s、最小轴力 %s%s、平均轴力 %s%s。", - rate.toPlainString(), unit, - statistics.getMax().toPlainString(), unit, - statistics.getMin().toPlainString(), unit, - statistics.getAvg().toPlainString(), unit))); - - // 添加控制值对比结论 - if (result.getControlValue() != null && result.getControlValue().compareTo(BigDecimal.ZERO) != 0) { - BigDecimal ratio = statistics.getCurrent().abs().divide(result.getControlValue(), 4, BigDecimal.ROUND_HALF_UP) - .multiply(new BigDecimal("100")); - conclusions.add(new Conclusion(seq++, String.format( - "当前轴力占控制值比例:%.2f%%(控制值:%s%s)。", - ratio.doubleValue(), result.getControlValue().toPlainString(), result.getControlValueUnit()))); - } - } - - if (trend != null && StringUtils.isNotEmpty(trend.getDescription())) { - conclusions.add(new Conclusion(seq++, "趋势分析:" + trend.getDescription() + "。")); - } - if (warning != null && StringUtils.isNotEmpty(warning.getDescription())) { - conclusions.add(new Conclusion(seq++, "预警状态:" + statusText(warning.getStatus()) - + "。" + warning.getDescription() + "。")); - } - if (quality != null && StringUtils.isNotEmpty(quality.getDescription())) { - conclusions.add(new Conclusion(seq, "数据质量:" + quality.getDescription() + "。")); - } - result.setConclusions(conclusions); - } - - /** - * 生成项目概况总结 - */ - private String buildSummary(Statistics statistics, TrendAnalysis trend, String unit, TimeWindow window) { - if (statistics == null || statistics.getCount() == 0) { - return "统计期内无有效监测数据。"; - } - - String forceState = (statistics.getCurrent() != null && statistics.getCurrent().compareTo(BigDecimal.ZERO) >= 0) ? "受拉" : "受压"; - String direction = trend == null ? "待分析" : directionText(trend.getDirection()); - BigDecimal rate = rateCalculator.ratePerDay(statistics, - Duration.between(window.getStartTime(), window.getEndTime()).getSeconds()); - - return String.format( - "本期轴力/应力监测数据显示:当前轴力为%s%s,处于%s状态,较初始值变化%s%s,变化速率为%s%s/天,轴力呈%s趋势。" + - "统计期内最大轴力%s%s,最小轴力%s%s,平均轴力%s%s。", - statistics.getCurrent().toPlainString(), unit, - forceState, - statistics.getDelta().toPlainString(), unit, - rate.toPlainString(), unit, - direction, - statistics.getMax().toPlainString(), unit, - statistics.getMin().toPlainString(), unit, - statistics.getAvg().toPlainString(), unit - ); - } - - /** - * 生成专业分析建议 - */ - private String buildProfessionalAdvice(Statistics statistics, TrendAnalysis trend, WarningAnalysis warning, String unit) { - if (statistics == null || statistics.getCount() == 0) { - return "建议检查设备上报状态,确保数据采集正常。"; - } - - StringBuilder advice = new StringBuilder(); - advice.append("专业分析:"); - - // 基于趋势方向给出建议 - if (trend != null) { - if (TrendAnalysis.DIRECTION_UP.equals(trend.getDirection())) { - advice.append("轴力呈增大趋势,需关注结构受力状态变化。"); - } else if (TrendAnalysis.DIRECTION_DOWN.equals(trend.getDirection())) { - advice.append("轴力呈减小趋势,结构受力状态趋于稳定。"); - } else { - advice.append("轴力变化稳定,结构受力状态正常。"); - } - } - - // 基于受力状态给出建议 - String forceState = (statistics.getCurrent() != null && statistics.getCurrent().compareTo(BigDecimal.ZERO) >= 0) ? "受拉" : "受压"; - if ("受拉".equals(forceState)) { - advice.append("当前结构处于受拉状态,需关注材料抗拉强度。"); - } else { - advice.append("当前结构处于受压状态,需关注材料抗压强度。"); - } - - // 基于预警状态给出建议 - if (warning != null) { - if (WarningAnalysis.STATUS_ALARM.equals(warning.getStatus())) { - advice.append("当前处于报警状态,建议立即采取加固措施,并组织专业人员进行安全评估。"); - } else if (WarningAnalysis.STATUS_WARNING.equals(warning.getStatus())) { - advice.append("当前处于预警状态,建议加密监测频率,准备应急预案。"); - } - } - - return advice.toString(); - } - - /** - * 将预警状态转换为安全等级 - */ - private String convertWarningToSafetyLevel(String warningStatus) { - if (WarningAnalysis.STATUS_ALARM.equals(warningStatus)) { - return "DANGER"; - } else if (WarningAnalysis.STATUS_WARNING.equals(warningStatus)) { - return "WARNING"; - } - return "SAFE"; - } - - /** - * 组装多测点数据(支持多物模型标识符) - */ - private List buildMetricPoints(AnalysisResult result, ReportContext context) { - List points = new ArrayList<>(); - - for (Map.Entry entry : result.getStatistics().entrySet()) { - String identity = entry.getKey(); - Statistics stats = entry.getValue(); - - MetricPointData point = MetricPointData.builder() - .identifier(identity) - .modelName(stats.getModelName()) - .initialValue(stats.getInitial()) - .initialUnit(stats.getUnit()) - .currentValue(stats.getCurrent()) - .currentUnit(stats.getUnit()) - .changeValue(stats.getDelta()) - .changeUnit(stats.getUnit()) - .maxValue(stats.getMax()) - .minValue(stats.getMin()) - .avgValue(stats.getAvg()) - .build(); - - // 设置预警状态 - WarningAnalysis warning = result.getWarnings().get(identity); - if (warning != null) { - point.setWarningStatus(warning.getStatus()); - } - - // 计算变化速率 - TimeWindow window = context.getTimeWindow(); - BigDecimal rate = rateCalculator.ratePerDay(stats, - Duration.between(window.getStartTime(), window.getEndTime()).getSeconds()); - point.setChangeRate(rate); - point.setRateUnit(stats.getUnit() + "/天"); - - points.add(point); - } - - return points; - } - - private void buildChart(AnalysisResult result, ReportContext context, String primaryIdentity) { - // 主指标单图 + 其余指标按单位分组多曲线图(展示所有物模型数据) - Map> byIdentity = context.getMetricDataList().stream() - .filter(d -> d.getIdentity() != null) - .collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList())); - Map charts = metricChartBuilder.build(result.getStatistics(), primaryIdentity, byIdentity); - result.getCharts().putAll(metricChartBuilder.fillPlaceholders(charts)); - } - - private String statusText(String status) { - if (WarningAnalysis.STATUS_ALARM.equals(status)) { - return "报警"; - } - if (WarningAnalysis.STATUS_WARNING.equals(status)) { - return "预警"; - } - return "正常"; - } - - private String directionText(String direction) { + @Override + protected String directionText(String direction) { if (TrendAnalysis.DIRECTION_UP.equals(direction)) { return "增大"; } @@ -363,4 +39,70 @@ public class AxialForceAnalyzer implements ReportAnalyzer { } return "稳定"; } + + @Override + protected String buildSummary(Statistics statistics, TrendAnalysis trend, String unit, TimeWindow window) { + if (statistics == null || statistics.getCount() == 0) { + return "统计期内无有效监测数据。"; + } + + String direction = trend == null ? "待分析" : directionText(trend.getDirection()); + String forceState = statistics.getCurrent().compareTo(BigDecimal.ZERO) >= 0 ? "受拉" : "受压"; + String formattedCurrent = valueFormatter.format(statistics.getCurrent(), supportedCategory()); + String formattedDelta = valueFormatter.format(statistics.getDelta(), supportedCategory()); + String formattedMax = valueFormatter.format(statistics.getMax(), supportedCategory()); + String formattedMin = valueFormatter.format(statistics.getMin(), supportedCategory()); + String formattedAvg = valueFormatter.format(statistics.getAvg(), supportedCategory()); + String resolvedUnit = valueFormatter.resolveUnit(unit, supportedCategory()); + + String rateText = "未计算"; + if (trend != null && trend.getSlope() != null) { + String formattedSlope = valueFormatter.format(trend.getSlope(), supportedCategory()); + rateText = formattedSlope + resolvedUnit + "/天"; + } + + return String.format( + "本期轴力监测数据显示:当前轴力为%s%s,处于%s状态,较初始值变化%s%s,变化速率为%s,轴力呈%s趋势。" + + "统计期内最大轴力%s%s,最小轴力%s%s,平均轴力%s%s。", + formattedCurrent, resolvedUnit, + forceState, + formattedDelta, resolvedUnit, + rateText, + direction, + formattedMax, resolvedUnit, + formattedMin, resolvedUnit, + formattedAvg, resolvedUnit + ); + } + + @Override + protected String buildProfessionalAdvice(Statistics statistics, TrendAnalysis trend, WarningAnalysis warning, String unit) { + if (statistics == null || statistics.getCount() == 0) { + return "建议检查设备上报状态,确保数据采集正常。"; + } + + StringBuilder advice = new StringBuilder(); + advice.append("专业分析:"); + + String forceState = statistics.getCurrent().compareTo(BigDecimal.ZERO) >= 0 ? "受拉" : "受压"; + if ("受拉".equals(forceState)) { + advice.append("当前轴力为受拉状态,需关注抗拉强度是否满足设计要求。"); + } else { + advice.append("当前轴力为受压状态,需关注抗压强度是否满足设计要求。"); + } + + if (statistics.getDelta().abs().compareTo(new BigDecimal("10")) > 0) { + advice.append("轴力变化量较大,建议分析荷载变化原因。"); + } + + if (warning != null) { + if (WarningAnalysis.STATUS_ALARM.equals(warning.getStatus())) { + advice.append("当前处于报警状态,建议立即采取加固措施,组织专业人员进行结构安全评估。"); + } else if (WarningAnalysis.STATUS_WARNING.equals(warning.getStatus())) { + advice.append("当前处于预警状态,建议加密监测频率,制定应急预案。"); + } + } + + return advice.toString(); + } } \ No newline at end of file diff --git a/fastbee-report/src/main/java/com/fastbee/report/analyzer/crack/CrackMeterAnalyzer.java b/fastbee-report/src/main/java/com/fastbee/report/analyzer/crack/CrackMeterAnalyzer.java index 55b32b5f..974d9d14 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/analyzer/crack/CrackMeterAnalyzer.java +++ b/fastbee-report/src/main/java/com/fastbee/report/analyzer/crack/CrackMeterAnalyzer.java @@ -2,354 +2,36 @@ package com.fastbee.report.analyzer.crack; import com.fastbee.common.enums.MonitorCategory; import com.fastbee.common.utils.StringUtils; -import com.fastbee.iot.domain.ThingsModel; -import com.fastbee.report.analysis.PrimaryMetricResolver; -import com.fastbee.report.analysis.QualityAnalyzer; -import com.fastbee.report.analysis.RateCalculator; -import com.fastbee.report.analysis.StatisticsCalculator; -import com.fastbee.report.analysis.TrendAnalyzer; -import com.fastbee.report.analysis.WarningAnalyzer; -import com.fastbee.report.analyzer.ReportAnalyzer; -import com.fastbee.report.api.ReportRequest; -import com.fastbee.report.chart.ChartGenerator; -import com.fastbee.report.chart.MetricChartBuilder; -import com.fastbee.report.core.ReportContext; -import com.fastbee.report.data.MetricData; -import com.fastbee.report.model.AnalysisResult; -import com.fastbee.report.model.Conclusion; -import com.fastbee.report.model.MetricPointData; -import com.fastbee.report.model.QualityAnalysis; +import com.fastbee.report.analyzer.AbstractDeviceAnalyzer; 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 org.springframework.stereotype.Component; -import javax.annotation.Resource; import java.math.BigDecimal; -import java.time.Duration; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.stream.Collectors; /** - * 裂缝计专项分析器(当前/初始宽度、变化量、速率、趋势、预警、时程图) + * 裂缝计专项分析器 * * @author FastBee * @date 2026-08-16 */ @Component -public class CrackMeterAnalyzer implements ReportAnalyzer { - - @Resource - private StatisticsCalculator statisticsCalculator; - - @Resource - private TrendAnalyzer trendAnalyzer; - - @Resource - private RateCalculator rateCalculator; - - @Resource - private QualityAnalyzer qualityAnalyzer; - - @Resource - private WarningAnalyzer warningAnalyzer; - - @Resource - private ChartGenerator chartGenerator; - - @Resource - private PrimaryMetricResolver primaryMetricResolver; - - @Resource - private MetricChartBuilder metricChartBuilder; +public class CrackMeterAnalyzer extends AbstractDeviceAnalyzer { @Override - public boolean supports(MonitorCategory category) { - return category == MonitorCategory.CRACK_METER; + protected MonitorCategory supportedCategory() { + return MonitorCategory.CRACK_METER; } @Override - public AnalysisResult analyze(ReportContext context, ReportRequest request) { - AnalysisResult result = new AnalysisResult(); - - // 1. 按物模型标识符分组历史数据 - Map> byIdentity = context.getMetricDataList().stream() - .filter(d -> d.getIdentity() != null) - .collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList())); - - // 2. 获取所有监测物模型(即使没有数据也要显示) - Map allModels = context.getThingsModels().stream() - .filter(m -> m.getIdentifier() != null) - .collect(Collectors.toMap(ThingsModel::getIdentifier, m -> m, (o, n) -> n, LinkedHashMap::new)); - - if (allModels.isEmpty()) { - result.getConclusions().add(new Conclusion(1, "设备未配置监测物模型,请联系管理员配置。")); - return result; - } - - // 3. 为每个物模型生成统计数据(包括无数据的物模型) - for (Map.Entry entry : allModels.entrySet()) { - String identity = entry.getKey(); - ThingsModel model = entry.getValue(); - List data = byIdentity.getOrDefault(identity, new ArrayList<>()); - - ReportThreshold threshold = context.getThresholds().get(identity); - BigDecimal controlValue = threshold == null ? null : threshold.getControlValue(); - - Statistics statistics = statisticsCalculator.calculate(data); - // 补充物模型信息(确保即使无数据也有 modelName 和 unit) - if (statistics.getModelName() == null || statistics.getModelName().isEmpty()) { - statistics.setModelName(model.getModelName()); - } - if (statistics.getUnit() == null || statistics.getUnit().isEmpty()) { - statistics.setUnit(model.getUnit()); - } - statistics.setIdentifier(identity); - - TrendAnalysis trend = trendAnalyzer.analyze(data); - QualityAnalysis quality = qualityAnalyzer.analyze(data, context.getTimeWindow(), controlValue); - WarningAnalysis warning = warningAnalyzer.analyze( - identity, statistics.getModelName(), statistics.getCurrent(), threshold); - - result.getStatistics().put(identity, statistics); - result.getTrends().put(identity, trend); - result.getQualities().put(identity, quality); - result.getWarnings().put(identity, warning); - } - - // 统一主指标口径:优先名称/标识符命中关键词的物模型,排除角度/温度/电压等辅助量 - String primaryIdentity = primaryMetricResolver.resolve(context, byIdentity, false, "裂缝宽度", "缝宽", "width"); - result.setPrimaryIdentifier(primaryIdentity); - buildConclusions(result, context, primaryIdentity); - buildChart(result, context, primaryIdentity); - return result; + protected String[] primaryMetricKeywords() { + return new String[]{"裂缝宽度", "缝宽", "width"}; } - private void buildConclusions(AnalysisResult result, ReportContext context, String primaryIdentity) { - Statistics statistics = result.getStatistics().get(primaryIdentity); - TrendAnalysis trend = result.getTrends().get(primaryIdentity); - QualityAnalysis quality = result.getQualities().get(primaryIdentity); - WarningAnalysis warning = result.getWarnings().get(primaryIdentity); - TimeWindow window = context.getTimeWindow(); - String unit = statistics == null ? "" : (statistics.getUnit() == null ? "" : statistics.getUnit()); - - // 生成项目概况总结 - String summary = buildSummary(statistics, trend, unit, window); - result.setSummary(summary); - - // 生成专业分析建议 - String advice = buildProfessionalAdvice(statistics, trend, warning, unit); - result.setProfessionalAdvice(advice); - - // 设置安全评估 - if (warning != null) { - result.setSafetyLevel(convertWarningToSafetyLevel(warning.getStatus())); - result.setSafetyDescription(warning.getDescription()); - } - - // 设置控制值 - ReportThreshold threshold = context.getThresholds().get(primaryIdentity); - if (threshold != null && threshold.getControlValue() != null) { - result.setControlValue(threshold.getControlValue()); - result.setControlValueUnit(threshold.getUnit()); - } - - // 组装多测点数据 - List metricPoints = buildMetricPoints(result, context); - result.setMetricPoints(metricPoints); - - // 生成详细结论 - List conclusions = new ArrayList<>(); - int seq = 1; - if (statistics == null || statistics.getCount() == 0) { - conclusions.add(new Conclusion(seq++, "统计期内无有效监测数据,请检查设备上报状态与时间范围。")); - } else { - String direction = trend == null ? "待分析" : directionText(trend.getDirection()); - conclusions.add(new Conclusion(seq++, String.format( - "统计期内裂缝宽度由 %s%s 变化至 %s%s,变化量 %s%s,裂缝呈%s趋势。", - statistics.getInitial().toPlainString(), unit, - statistics.getCurrent().toPlainString(), unit, - statistics.getDelta().toPlainString(), unit, - direction))); - - BigDecimal rate = rateCalculator.ratePerDay(statistics, - Duration.between(window.getStartTime(), window.getEndTime()).getSeconds()); - conclusions.add(new Conclusion(seq++, String.format( - "平均变化速率 %s%s/天;统计期最大裂缝宽度 %s%s、最小裂缝宽度 %s%s、均值 %s%s。", - rate.toPlainString(), unit, - statistics.getMax().toPlainString(), unit, - statistics.getMin().toPlainString(), unit, - statistics.getAvg().toPlainString(), unit))); - - // 添加控制值对比结论 - if (result.getControlValue() != null && result.getControlValue().compareTo(BigDecimal.ZERO) != 0) { - BigDecimal ratio = statistics.getCurrent().divide(result.getControlValue(), 4, BigDecimal.ROUND_HALF_UP) - .multiply(new BigDecimal("100")); - conclusions.add(new Conclusion(seq++, String.format( - "当前裂缝宽度占控制值比例:%.2f%%(控制值:%s%s)。", - ratio.doubleValue(), result.getControlValue().toPlainString(), result.getControlValueUnit()))); - } - } - - if (trend != null && StringUtils.isNotEmpty(trend.getDescription())) { - conclusions.add(new Conclusion(seq++, "趋势分析:" + trend.getDescription() + "。")); - } - if (warning != null && StringUtils.isNotEmpty(warning.getDescription())) { - conclusions.add(new Conclusion(seq++, "预警状态:" + statusText(warning.getStatus()) - + "。" + warning.getDescription() + "。")); - } - if (quality != null && StringUtils.isNotEmpty(quality.getDescription())) { - conclusions.add(new Conclusion(seq, "数据质量:" + quality.getDescription() + "。")); - } - result.setConclusions(conclusions); - } - - /** - * 生成项目概况总结 - */ - private String buildSummary(Statistics statistics, TrendAnalysis trend, String unit, TimeWindow window) { - if (statistics == null || statistics.getCount() == 0) { - return "统计期内无有效监测数据。"; - } - - String direction = trend == null ? "待分析" : directionText(trend.getDirection()); - BigDecimal rate = rateCalculator.ratePerDay(statistics, - Duration.between(window.getStartTime(), window.getEndTime()).getSeconds()); - - return String.format( - "本期裂缝宽度监测数据显示:当前裂缝宽度为%s%s,较初始值变化%s%s,变化速率为%s%s/天,裂缝呈%s趋势。" + - "统计期内最大裂缝宽度%s%s,最小裂缝宽度%s%s,平均裂缝宽度%s%s。", - statistics.getCurrent().toPlainString(), unit, - statistics.getDelta().toPlainString(), unit, - rate.toPlainString(), unit, - direction, - statistics.getMax().toPlainString(), unit, - statistics.getMin().toPlainString(), unit, - statistics.getAvg().toPlainString(), unit - ); - } - - /** - * 生成专业分析建议 - */ - private String buildProfessionalAdvice(Statistics statistics, TrendAnalysis trend, WarningAnalysis warning, String unit) { - if (statistics == null || statistics.getCount() == 0) { - return "建议检查设备上报状态,确保数据采集正常。"; - } - - StringBuilder advice = new StringBuilder(); - advice.append("专业分析:"); - - // 基于趋势方向给出建议 - if (trend != null) { - if (TrendAnalysis.DIRECTION_UP.equals(trend.getDirection())) { - advice.append("裂缝宽度呈增大趋势,建议加强监测频率,关注裂缝发展速率。"); - } else if (TrendAnalysis.DIRECTION_DOWN.equals(trend.getDirection())) { - advice.append("裂缝宽度呈减小趋势,结构状态趋于稳定。"); - } else { - advice.append("裂缝宽度变化稳定,结构状态正常。"); - } - } - - // 基于预警状态给出建议 - if (warning != null) { - if (WarningAnalysis.STATUS_ALARM.equals(warning.getStatus())) { - advice.append("当前处于报警状态,建议立即采取加固措施,并组织专业人员进行安全评估。"); - } else if (WarningAnalysis.STATUS_WARNING.equals(warning.getStatus())) { - advice.append("当前处于预警状态,建议加密监测频率,准备应急预案。"); - } - } - - // 基于变化量给出建议 - BigDecimal absDelta = statistics.getDelta().abs(); - if (absDelta.compareTo(new BigDecimal("1")) > 0) { - advice.append("裂缝变化量较大(>1mm),建议进行详细的结构安全评估。"); - } - - return advice.toString(); - } - - /** - * 将预警状态转换为安全等级 - */ - private String convertWarningToSafetyLevel(String warningStatus) { - if (WarningAnalysis.STATUS_ALARM.equals(warningStatus)) { - return "DANGER"; - } else if (WarningAnalysis.STATUS_WARNING.equals(warningStatus)) { - return "WARNING"; - } - return "SAFE"; - } - - /** - * 组装多测点数据(支持多物模型标识符) - */ - private List buildMetricPoints(AnalysisResult result, ReportContext context) { - List points = new ArrayList<>(); - - for (Map.Entry entry : result.getStatistics().entrySet()) { - String identity = entry.getKey(); - Statistics stats = entry.getValue(); - - MetricPointData point = MetricPointData.builder() - .identifier(identity) - .modelName(stats.getModelName()) - .initialValue(stats.getInitial()) - .initialUnit(stats.getUnit()) - .currentValue(stats.getCurrent()) - .currentUnit(stats.getUnit()) - .changeValue(stats.getDelta()) - .changeUnit(stats.getUnit()) - .maxValue(stats.getMax()) - .minValue(stats.getMin()) - .avgValue(stats.getAvg()) - .build(); - - // 设置预警状态 - WarningAnalysis warning = result.getWarnings().get(identity); - if (warning != null) { - point.setWarningStatus(warning.getStatus()); - } - - // 计算变化速率 - TimeWindow window = context.getTimeWindow(); - BigDecimal rate = rateCalculator.ratePerDay(stats, - Duration.between(window.getStartTime(), window.getEndTime()).getSeconds()); - point.setChangeRate(rate); - point.setRateUnit(stats.getUnit() + "/天"); - - points.add(point); - } - - return points; - } - - private void buildChart(AnalysisResult result, ReportContext context, String primaryIdentity) { - // 主指标单图 + 其余指标按单位分组多曲线图(展示所有物模型数据) - Map> byIdentity = context.getMetricDataList().stream() - .filter(d -> d.getIdentity() != null) - .collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList())); - Map charts = metricChartBuilder.build(result.getStatistics(), primaryIdentity, byIdentity); - result.getCharts().putAll(metricChartBuilder.fillPlaceholders(charts)); - } - - private String statusText(String status) { - if (WarningAnalysis.STATUS_ALARM.equals(status)) { - return "报警"; - } - if (WarningAnalysis.STATUS_WARNING.equals(status)) { - return "预警"; - } - return "正常"; - } - - private String directionText(String direction) { + @Override + protected String directionText(String direction) { if (TrendAnalysis.DIRECTION_UP.equals(direction)) { return "张开"; } @@ -358,4 +40,71 @@ public class CrackMeterAnalyzer implements ReportAnalyzer { } return "稳定"; } -} + + @Override + protected String buildSummary(Statistics statistics, TrendAnalysis trend, String unit, TimeWindow window) { + if (statistics == null || statistics.getCount() == 0) { + return "统计期内无有效监测数据。"; + } + + String direction = trend == null ? "待分析" : directionText(trend.getDirection()); + String formattedCurrent = valueFormatter.format(statistics.getCurrent(), supportedCategory()); + String formattedDelta = valueFormatter.format(statistics.getDelta(), supportedCategory()); + String formattedMax = valueFormatter.format(statistics.getMax(), supportedCategory()); + String formattedMin = valueFormatter.format(statistics.getMin(), supportedCategory()); + String formattedAvg = valueFormatter.format(statistics.getAvg(), supportedCategory()); + String resolvedUnit = valueFormatter.resolveUnit(unit, supportedCategory()); + + String rateText = "未计算"; + if (trend != null && trend.getSlope() != null) { + String formattedSlope = valueFormatter.format(trend.getSlope(), supportedCategory()); + rateText = formattedSlope + resolvedUnit + "/天"; + } + + return String.format( + "本期裂缝宽度监测数据显示:当前裂缝宽度为%s%s,较初始值变化%s%s,变化速率为%s,裂缝呈%s趋势。" + + "统计期内最大裂缝宽度%s%s,最小裂缝宽度%s%s,平均裂缝宽度%s%s。", + formattedCurrent, resolvedUnit, + formattedDelta, resolvedUnit, + rateText, + direction, + formattedMax, resolvedUnit, + formattedMin, resolvedUnit, + formattedAvg, resolvedUnit + ); + } + + @Override + protected String buildProfessionalAdvice(Statistics statistics, TrendAnalysis trend, WarningAnalysis warning, String unit) { + if (statistics == null || statistics.getCount() == 0) { + return "建议检查设备上报状态,确保数据采集正常。"; + } + + StringBuilder advice = new StringBuilder(); + advice.append("专业分析:"); + + if (trend != null) { + if (TrendAnalysis.DIRECTION_UP.equals(trend.getDirection())) { + advice.append("裂缝呈张开趋势,需关注裂缝发展对结构安全的影响。"); + } else if (TrendAnalysis.DIRECTION_DOWN.equals(trend.getDirection())) { + advice.append("裂缝呈闭合趋势,结构趋于稳定。"); + } else { + advice.append("裂缝宽度变化稳定。"); + } + } + + if (statistics.getDelta().abs().compareTo(new BigDecimal("1")) > 0) { + advice.append("裂缝变化量较大(>1mm),建议加密监测频率,分析裂缝成因。"); + } + + if (warning != null) { + if (WarningAnalysis.STATUS_ALARM.equals(warning.getStatus())) { + advice.append("当前处于报警状态,建议立即对裂缝进行封闭处理,组织专业人员进行结构安全评估。"); + } else if (WarningAnalysis.STATUS_WARNING.equals(warning.getStatus())) { + advice.append("当前处于预警状态,建议加强巡视检查,准备裂缝修补材料。"); + } + } + + return advice.toString(); + } +} \ No newline at end of file diff --git a/fastbee-report/src/main/java/com/fastbee/report/analyzer/deep_displacement/DeepDisplacementAnalyzer.java b/fastbee-report/src/main/java/com/fastbee/report/analyzer/deep_displacement/DeepDisplacementAnalyzer.java index 509c5bb5..cbbfbb99 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/analyzer/deep_displacement/DeepDisplacementAnalyzer.java +++ b/fastbee-report/src/main/java/com/fastbee/report/analyzer/deep_displacement/DeepDisplacementAnalyzer.java @@ -2,39 +2,14 @@ package com.fastbee.report.analyzer.deep_displacement; import com.fastbee.common.enums.MonitorCategory; import com.fastbee.common.utils.StringUtils; -import com.fastbee.iot.domain.ThingsModel; -import com.fastbee.report.analysis.PrimaryMetricResolver; -import com.fastbee.report.analysis.QualityAnalyzer; -import com.fastbee.report.analysis.RateCalculator; -import com.fastbee.report.analysis.StatisticsCalculator; -import com.fastbee.report.analysis.TrendAnalyzer; -import com.fastbee.report.analysis.WarningAnalyzer; -import com.fastbee.report.analyzer.ReportAnalyzer; -import com.fastbee.report.api.ReportRequest; -import com.fastbee.report.chart.ChartGenerator; -import com.fastbee.report.chart.MetricChartBuilder; -import com.fastbee.report.core.ReportContext; -import com.fastbee.report.data.MetricData; -import com.fastbee.report.model.AnalysisResult; -import com.fastbee.report.model.Conclusion; -import com.fastbee.report.model.MetricPointData; -import com.fastbee.report.model.QualityAnalysis; +import com.fastbee.report.analyzer.AbstractDeviceAnalyzer; 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 org.springframework.stereotype.Component; -import javax.annotation.Resource; import java.math.BigDecimal; -import java.time.Duration; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.stream.Collectors; /** * 深部位移专项分析器 @@ -43,218 +18,67 @@ import java.util.stream.Collectors; * @date 2026-08-23 */ @Component -public class DeepDisplacementAnalyzer implements ReportAnalyzer { - - @Resource - private StatisticsCalculator statisticsCalculator; - - @Resource - private TrendAnalyzer trendAnalyzer; - - @Resource - private RateCalculator rateCalculator; - - @Resource - private QualityAnalyzer qualityAnalyzer; - - @Resource - private WarningAnalyzer warningAnalyzer; - - @Resource - private ChartGenerator chartGenerator; - - @Resource - private PrimaryMetricResolver primaryMetricResolver; - - @Resource - private MetricChartBuilder metricChartBuilder; +public class DeepDisplacementAnalyzer extends AbstractDeviceAnalyzer { @Override - public boolean supports(MonitorCategory category) { - return category == MonitorCategory.DEEP_DISPLACEMENT; + protected MonitorCategory supportedCategory() { + return MonitorCategory.DEEP_DISPLACEMENT; } @Override - public AnalysisResult analyze(ReportContext context, ReportRequest request) { - AnalysisResult result = new AnalysisResult(); - - // 1. 按物模型标识符分组历史数据 - Map> byIdentity = context.getMetricDataList().stream() - .filter(d -> d.getIdentity() != null) - .collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList())); - - // 2. 获取所有监测物模型(即使没有数据也要显示) - Map allModels = context.getThingsModels().stream() - .filter(m -> m.getIdentifier() != null) - .collect(Collectors.toMap(ThingsModel::getIdentifier, m -> m, (o, n) -> n, LinkedHashMap::new)); - - if (allModels.isEmpty()) { - result.getConclusions().add(new Conclusion(1, "设备未配置监测物模型,请联系管理员配置。")); - return result; - } - - // 3. 为每个物模型生成统计数据(包括无数据的物模型) - for (Map.Entry entry : allModels.entrySet()) { - String identity = entry.getKey(); - ThingsModel model = entry.getValue(); - List data = byIdentity.getOrDefault(identity, new ArrayList<>()); - - ReportThreshold threshold = context.getThresholds().get(identity); - BigDecimal controlValue = threshold == null ? null : threshold.getControlValue(); - - Statistics statistics = statisticsCalculator.calculate(data); - // 补充物模型信息(确保即使无数据也有 modelName 和 unit) - if (statistics.getModelName() == null || statistics.getModelName().isEmpty()) { - statistics.setModelName(model.getModelName()); - } - if (statistics.getUnit() == null || statistics.getUnit().isEmpty()) { - statistics.setUnit(model.getUnit()); - } - statistics.setIdentifier(identity); - - TrendAnalysis trend = trendAnalyzer.analyze(data); - QualityAnalysis quality = qualityAnalyzer.analyze(data, context.getTimeWindow(), controlValue); - WarningAnalysis warning = warningAnalyzer.analyze( - identity, statistics.getModelName(), statistics.getCurrent(), threshold); - - result.getStatistics().put(identity, statistics); - result.getTrends().put(identity, trend); - result.getQualities().put(identity, quality); - result.getWarnings().put(identity, warning); - } - - // 统一主指标口径:按物模型顺序、排除角度/温度/电压等辅助量 - String primaryIdentity = primaryMetricResolver.resolve(context, byIdentity, - "深部位移", "位移", "displacement", "沉降", "settlement"); - result.setPrimaryIdentifier(primaryIdentity); - - buildConclusions(result, context, primaryIdentity); - buildChart(result, context, primaryIdentity); - return result; + protected String[] primaryMetricKeywords() { + return new String[]{"深部位移", "位移", "displacement", "沉降", "settlement"}; } - /** - * 主监测指标:优先名称/标识符含"位移/displacement/x/y/z",否则取第一个有数据的指标 - * - * @deprecated 由 {@link PrimaryMetricResolver} 统一解析,保留仅为兼容历史调用 - */ - @Deprecated - private String resolvePrimaryIdentity(Map> byIdentity, ReportContext context) { - return primaryMetricResolver.resolve(context, byIdentity, "位移", "displacement"); + @Override + protected String directionText(String direction) { + // 方向语义:中性描述,不臆测坡体方向 + if (TrendAnalysis.DIRECTION_UP.equals(direction)) { + return "位移增大方向"; + } + if (TrendAnalysis.DIRECTION_DOWN.equals(direction)) { + return "位移减小方向"; + } + return "稳定"; } - private void buildConclusions(AnalysisResult result, ReportContext context, String primaryIdentity) { - Statistics statistics = result.getStatistics().get(primaryIdentity); - TrendAnalysis trend = result.getTrends().get(primaryIdentity); - QualityAnalysis quality = result.getQualities().get(primaryIdentity); - WarningAnalysis warning = result.getWarnings().get(primaryIdentity); - TimeWindow window = context.getTimeWindow(); - String unit = statistics == null ? "" : (statistics.getUnit() == null ? "" : statistics.getUnit()); - - // 生成项目概况总结 - String summary = buildSummary(statistics, trend, unit, window); - result.setSummary(summary); - - // 生成专业分析建议 - String advice = buildProfessionalAdvice(statistics, trend, warning, unit); - result.setProfessionalAdvice(advice); - - // 设置安全评估 - if (warning != null) { - result.setSafetyLevel(convertWarningToSafetyLevel(warning.getStatus())); - result.setSafetyDescription(warning.getDescription()); - } - - // 设置控制值 - ReportThreshold threshold = context.getThresholds().get(primaryIdentity); - if (threshold != null && threshold.getControlValue() != null) { - result.setControlValue(threshold.getControlValue()); - result.setControlValueUnit(threshold.getUnit()); - } - - // 组装多测点数据(支持多物模型标识符,如不同深度的位移测点) - List metricPoints = buildMetricPoints(result, context); - result.setMetricPoints(metricPoints); - - // 生成详细结论 - List conclusions = new ArrayList<>(); - int seq = 1; - if (statistics == null || statistics.getCount() == 0) { - conclusions.add(new Conclusion(seq++, "统计期内无有效监测数据,请检查设备上报状态与时间范围。")); - } else { - conclusions.add(new Conclusion(seq++, String.format( - "统计期内深部位移由 %s%s 变化至 %s%s,累计位移量 %s%s,位移方向为%s。", - statistics.getInitial().toPlainString(), unit, - statistics.getCurrent().toPlainString(), unit, - statistics.getDelta().toPlainString(), unit, - trend == null ? "待分析" : directionText(trend.getDirection())))); - - BigDecimal rate = rateCalculator.ratePerDay(statistics, - Duration.between(window.getStartTime(), window.getEndTime()).getSeconds()); - conclusions.add(new Conclusion(seq++, String.format( - "平均位移速率 %s%s/天;统计期最大位移 %s%s、最小位移 %s%s、均值 %s%s。", - rate.toPlainString(), unit, - statistics.getMax().toPlainString(), unit, - statistics.getMin().toPlainString(), unit, - statistics.getAvg().toPlainString(), unit))); - - // 添加控制值对比结论 - if (result.getControlValue() != null && result.getControlValue().compareTo(BigDecimal.ZERO) != 0) { - BigDecimal ratio = statistics.getDelta().abs().divide(result.getControlValue(), 4, BigDecimal.ROUND_HALF_UP) - .multiply(new BigDecimal("100")); - conclusions.add(new Conclusion(seq++, String.format( - "累计位移占控制值比例:%.2f%%(控制值:%s%s),需关注潜在滑动面发展趋势。", - ratio.doubleValue(), result.getControlValue().toPlainString(), result.getControlValueUnit()))); - } - - // 位移速率评估 - if (rate.compareTo(new BigDecimal("2")) > 0) { - conclusions.add(new Conclusion(seq++, "位移速率较大,需加密监测频次,必要时启动预警机制。")); - } - } - - if (trend != null && StringUtils.isNotEmpty(trend.getDescription())) { - conclusions.add(new Conclusion(seq++, "趋势分析:" + trend.getDescription() + "。")); - } - if (warning != null && StringUtils.isNotEmpty(warning.getDescription())) { - conclusions.add(new Conclusion(seq++, "预警状态:" + statusText(warning.getStatus()) - + "。" + warning.getDescription() + "。")); - } - if (quality != null && StringUtils.isNotEmpty(quality.getDescription())) { - conclusions.add(new Conclusion(seq, "数据质量:" + quality.getDescription() + "。")); - } - result.setConclusions(conclusions); - } - - /** - * 生成项目概况总结 - */ - private String buildSummary(Statistics statistics, TrendAnalysis trend, String unit, TimeWindow window) { + @Override + protected String buildSummary(Statistics statistics, TrendAnalysis trend, String unit, TimeWindow window) { if (statistics == null || statistics.getCount() == 0) { return "统计期内无有效监测数据。"; } String direction = trend == null ? "待分析" : directionText(trend.getDirection()); - BigDecimal rate = rateCalculator.ratePerDay(statistics, - Duration.between(window.getStartTime(), window.getEndTime()).getSeconds()); + String formattedCurrent = valueFormatter.format(statistics.getCurrent(), supportedCategory()); + String formattedDelta = valueFormatter.format(statistics.getDelta(), supportedCategory()); + String formattedMax = valueFormatter.format(statistics.getMax(), supportedCategory()); + String formattedMin = valueFormatter.format(statistics.getMin(), supportedCategory()); + String formattedAvg = valueFormatter.format(statistics.getAvg(), supportedCategory()); + String resolvedUnit = valueFormatter.resolveUnit(unit, supportedCategory()); + // 使用回归斜率作为速率 + String rateText = "未计算"; + if (trend != null && trend.getSlope() != null) { + String formattedSlope = valueFormatter.format(trend.getSlope(), supportedCategory()); + rateText = formattedSlope + resolvedUnit + "/天"; + } + + // 移除无条件"土体内部变形监测正常"尾句 return String.format( - "本期深部位移监测数据显示:当前位移为%s%s,较初始值累计变化%s%s,位移速率为%s%s/天,位移方向呈%s趋势。" + - "统计期内最大位移%s%s,最小位移%s%s,平均位移%s%s。土体内部变形监测正常。", - statistics.getCurrent().toPlainString(), unit, - statistics.getDelta().toPlainString(), unit, - rate.toPlainString(), unit, + "本期深部位移监测数据显示:当前位移为%s%s,较初始值累计变化%s%s,变化速率为%s,位移呈%s趋势。" + + "统计期内最大位移%s%s,最小位移%s%s,平均位移%s%s。", + formattedCurrent, resolvedUnit, + formattedDelta, resolvedUnit, + rateText, direction, - statistics.getMax().toPlainString(), unit, - statistics.getMin().toPlainString(), unit, - statistics.getAvg().toPlainString(), unit + formattedMax, resolvedUnit, + formattedMin, resolvedUnit, + formattedAvg, resolvedUnit ); } - /** - * 生成专业分析建议 - */ - private String buildProfessionalAdvice(Statistics statistics, TrendAnalysis trend, WarningAnalysis warning, String unit) { + @Override + protected String buildProfessionalAdvice(Statistics statistics, TrendAnalysis trend, WarningAnalysis warning, String unit) { if (statistics == null || statistics.getCount() == 0) { return "建议检查设备上报状态,确保数据采集正常。"; } @@ -262,116 +86,49 @@ public class DeepDisplacementAnalyzer implements ReportAnalyzer { StringBuilder advice = new StringBuilder(); advice.append("专业分析:"); - // 基于趋势方向给出建议 + // 基于趋势方向给出建议(结合边坡监测规范) if (trend != null) { if (TrendAnalysis.DIRECTION_UP.equals(trend.getDirection())) { - advice.append("深部位移呈增大趋势,可能存在潜在滑动面发展,需结合地质资料分析滑移面位置与形态。"); + advice.append("深部位移呈增大趋势。"); + advice.append("根据 SL 551-2012《水利水电工程边坡监测规范》,深部位移增大可能指示:"); + advice.append("(1)潜在滑动面正在形成;(2)滑体处于蠕滑阶段。"); + + // 基于速率给出具体判断 + if (trend.getSlope() != null) { + BigDecimal absSlope = trend.getSlope().abs(); + if (absSlope.compareTo(new BigDecimal("2")) > 0) { + advice.append(String.format("当前位移速率 %.2fmm/d,已超过 2mm/d 的预警阈值。", absSlope.doubleValue())); + advice.append("建议:结合测斜孔剖面图分析潜在滑动面位置,准备应急预案。"); + } else { + advice.append(String.format("当前位移速率 %.2fmm/d。", absSlope.doubleValue())); + advice.append("建议:继续监测,分析位移沿深度分布规律。"); + } + } } else if (TrendAnalysis.DIRECTION_DOWN.equals(trend.getDirection())) { advice.append("深部位移呈减小趋势,土体变形趋于稳定。"); } else { - advice.append("深部位移变化稳定,土体内部变形正常。"); + advice.append("深部位移变化稳定。"); } } - // 基于位移量级给出建议 - if (statistics.getDelta().abs().compareTo(new BigDecimal("10")) > 0) { - advice.append("累计位移量较大,建议加密监测频率,结合测斜孔剖面图分析位移沿深度分布规律。"); + // 基于累计位移量级给出建议 + BigDecimal absDelta = statistics.getDelta().abs(); + if (absDelta.compareTo(new BigDecimal("30")) > 0) { + advice.append(String.format("累计位移量已达 %.1fmm,建议:", absDelta.doubleValue())); + advice.append("(1)加密监测频次;(2)对比地表沉降数据;(3)检查周边施工扰动。"); + } else if (absDelta.compareTo(new BigDecimal("10")) > 0) { + advice.append(String.format("累计位移量 %.1fmm,建议关注位移发展趋势。", absDelta.doubleValue())); } - // 基于预警状态给出建议 - if (warning != null) { - if (WarningAnalysis.STATUS_ALARM.equals(warning.getStatus())) { - advice.append("当前处于报警状态,建议立即启动应急预案,组织专业人员进行地质灾害评估,必要时采取加固措施。"); - } else if (WarningAnalysis.STATUS_WARNING.equals(warning.getStatus())) { - advice.append("当前处于预警状态,建议加强巡视检查,准备应急物资,制定应急预案。"); - } + // 阈值状态说明 + if (warning != null && WarningAnalysis.STATUS_UNKNOWN.equals(warning.getStatus())) { + advice.append("【提示】尚未配置控制值,建议根据设计文件在系统中设置位移控制值,以便自动评估安全等级。"); + } else if (warning != null && WarningAnalysis.STATUS_ALARM.equals(warning.getStatus())) { + advice.append("当前处于报警状态,建议立即启动应急预案,组织专业人员进行地质灾害评估,必要时采取加固措施。"); + } else if (warning != null && WarningAnalysis.STATUS_WARNING.equals(warning.getStatus())) { + advice.append("当前处于预警状态,建议加强巡视检查,准备应急物资,制定应急预案。"); } return advice.toString(); } - - /** - * 将预警状态转换为安全等级 - */ - private String convertWarningToSafetyLevel(String warningStatus) { - if (WarningAnalysis.STATUS_ALARM.equals(warningStatus)) { - return "DANGER"; - } else if (WarningAnalysis.STATUS_WARNING.equals(warningStatus)) { - return "WARNING"; - } - return "SAFE"; - } - - /** - * 组装多测点数据(支持多物模型标识符) - */ - private List buildMetricPoints(AnalysisResult result, ReportContext context) { - List points = new ArrayList<>(); - - for (Map.Entry entry : result.getStatistics().entrySet()) { - String identity = entry.getKey(); - Statistics stats = entry.getValue(); - - MetricPointData point = MetricPointData.builder() - .identifier(identity) - .modelName(stats.getModelName()) - .initialValue(stats.getInitial()) - .initialUnit(stats.getUnit()) - .currentValue(stats.getCurrent()) - .currentUnit(stats.getUnit()) - .changeValue(stats.getDelta()) - .changeUnit(stats.getUnit()) - .maxValue(stats.getMax()) - .minValue(stats.getMin()) - .avgValue(stats.getAvg()) - .build(); - - // 设置预警状态 - WarningAnalysis warning = result.getWarnings().get(identity); - if (warning != null) { - point.setWarningStatus(warning.getStatus()); - } - - // 计算变化速率 - TimeWindow window = context.getTimeWindow(); - BigDecimal rate = rateCalculator.ratePerDay(stats, - Duration.between(window.getStartTime(), window.getEndTime()).getSeconds()); - point.setChangeRate(rate); - point.setRateUnit(stats.getUnit() + "/天"); - - points.add(point); - } - - return points; - } - - private void buildChart(AnalysisResult result, ReportContext context, String primaryIdentity) { - // 主指标单图 + 其余指标按单位分组多曲线图(展示所有物模型数据) - Map> byIdentity = context.getMetricDataList().stream() - .filter(d -> d.getIdentity() != null) - .collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList())); - Map charts = metricChartBuilder.build(result.getStatistics(), primaryIdentity, byIdentity); - // 补齐模板预留图表位(无数据时用空白图),避免模板标签缺失导致渲染异常 - result.getCharts().putAll(metricChartBuilder.fillPlaceholders(charts)); - } - - private String statusText(String status) { - if (WarningAnalysis.STATUS_ALARM.equals(status)) { - return "报警"; - } - if (WarningAnalysis.STATUS_WARNING.equals(status)) { - return "预警"; - } - return "正常"; - } - - private String directionText(String direction) { - if (TrendAnalysis.DIRECTION_UP.equals(direction)) { - return "向坡体方向滑移"; - } - if (TrendAnalysis.DIRECTION_DOWN.equals(direction)) { - return "背离坡体方向回弹"; - } - return "稳定"; - } } \ No newline at end of file diff --git a/fastbee-report/src/main/java/com/fastbee/report/analyzer/inclinometer/InclinometerAnalyzer.java b/fastbee-report/src/main/java/com/fastbee/report/analyzer/inclinometer/InclinometerAnalyzer.java index add00a6b..09086e05 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/analyzer/inclinometer/InclinometerAnalyzer.java +++ b/fastbee-report/src/main/java/com/fastbee/report/analyzer/inclinometer/InclinometerAnalyzer.java @@ -1,41 +1,13 @@ package com.fastbee.report.analyzer.inclinometer; import com.fastbee.common.enums.MonitorCategory; -import com.fastbee.common.utils.StringUtils; -import com.fastbee.iot.domain.ThingsModel; -import com.fastbee.report.analysis.PrimaryMetricResolver; -import com.fastbee.report.analysis.QualityAnalyzer; -import com.fastbee.report.analysis.RateCalculator; -import com.fastbee.report.analysis.StatisticsCalculator; -import com.fastbee.report.analysis.TrendAnalyzer; -import com.fastbee.report.analysis.WarningAnalyzer; -import com.fastbee.report.analyzer.ReportAnalyzer; -import com.fastbee.report.api.ReportRequest; -import com.fastbee.report.chart.ChartGenerator; -import com.fastbee.report.chart.MetricChartBuilder; -import com.fastbee.report.core.ReportContext; -import com.fastbee.report.data.MetricData; -import com.fastbee.report.model.AnalysisResult; -import com.fastbee.report.model.Conclusion; -import com.fastbee.report.model.MetricPointData; -import com.fastbee.report.model.QualityAnalysis; +import com.fastbee.report.analyzer.AbstractDeviceAnalyzer; 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 org.springframework.stereotype.Component; -import javax.annotation.Resource; -import java.math.BigDecimal; -import java.time.Duration; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.stream.Collectors; - /** * 倾角计专项分析器 * @@ -43,317 +15,25 @@ import java.util.stream.Collectors; * @date 2026-08-23 */ @Component -public class InclinometerAnalyzer implements ReportAnalyzer { - - @Resource - private StatisticsCalculator statisticsCalculator; - - @Resource - private TrendAnalyzer trendAnalyzer; - - @Resource - private RateCalculator rateCalculator; - - @Resource - private QualityAnalyzer qualityAnalyzer; - - @Resource - private WarningAnalyzer warningAnalyzer; - - @Resource - private ChartGenerator chartGenerator; - - @Resource - private PrimaryMetricResolver primaryMetricResolver; - - @Resource - private MetricChartBuilder metricChartBuilder; +public class InclinometerAnalyzer extends AbstractDeviceAnalyzer { @Override - public boolean supports(MonitorCategory category) { - return category == MonitorCategory.INCLINOMETER; + protected MonitorCategory supportedCategory() { + return MonitorCategory.INCLINOMETER; } @Override - public AnalysisResult analyze(ReportContext context, ReportRequest request) { - AnalysisResult result = new AnalysisResult(); - - // 1. 按物模型标识符分组历史数据 - Map> byIdentity = context.getMetricDataList().stream() - .filter(d -> d.getIdentity() != null) - .collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList())); - - // 2. 获取所有监测物模型(即使没有数据也要显示) - Map allModels = context.getThingsModels().stream() - .filter(m -> m.getIdentifier() != null) - .collect(Collectors.toMap(ThingsModel::getIdentifier, m -> m, (o, n) -> n, LinkedHashMap::new)); - - if (allModels.isEmpty()) { - result.getConclusions().add(new Conclusion(1, "设备未配置监测物模型,请联系管理员配置。")); - return result; - } - - // 3. 为每个物模型生成统计数据(包括无数据的物模型) - for (Map.Entry entry : allModels.entrySet()) { - String identity = entry.getKey(); - ThingsModel model = entry.getValue(); - List data = byIdentity.getOrDefault(identity, new ArrayList<>()); - - ReportThreshold threshold = context.getThresholds().get(identity); - BigDecimal controlValue = threshold == null ? null : threshold.getControlValue(); - - Statistics statistics = statisticsCalculator.calculate(data); - // 补充物模型信息(确保即使无数据也有 modelName 和 unit) - if (statistics.getModelName() == null || statistics.getModelName().isEmpty()) { - statistics.setModelName(model.getModelName()); - } - if (statistics.getUnit() == null || statistics.getUnit().isEmpty()) { - statistics.setUnit(model.getUnit()); - } - statistics.setIdentifier(identity); - - TrendAnalysis trend = trendAnalyzer.analyze(data); - QualityAnalysis quality = qualityAnalyzer.analyze(data, context.getTimeWindow(), controlValue); - WarningAnalysis warning = warningAnalyzer.analyze( - identity, statistics.getModelName(), statistics.getCurrent(), threshold); - - result.getStatistics().put(identity, statistics); - result.getTrends().put(identity, trend); - result.getQualities().put(identity, quality); - result.getWarnings().put(identity, warning); - } - - // 统一主指标口径:优先名称/标识符命中关键词的物模型,排除角度/温度/电压等辅助量 - String primaryIdentity = primaryMetricResolver.resolve(context, byIdentity, true, "倾角", "角度", "angle", "inclin"); - result.setPrimaryIdentifier(primaryIdentity); - buildConclusions(result, context, primaryIdentity); - buildChart(result, context, primaryIdentity); - return result; + protected String[] primaryMetricKeywords() { + return new String[]{"倾角", "角度", "angle", "inclin"}; } - private void buildConclusions(AnalysisResult result, ReportContext context, String primaryIdentity) { - Statistics statistics = result.getStatistics().get(primaryIdentity); - TrendAnalysis trend = result.getTrends().get(primaryIdentity); - QualityAnalysis quality = result.getQualities().get(primaryIdentity); - WarningAnalysis warning = result.getWarnings().get(primaryIdentity); - TimeWindow window = context.getTimeWindow(); - String unit = statistics == null ? "" : (statistics.getUnit() == null ? "" : statistics.getUnit()); - - // 生成项目概况总结 - String summary = buildSummary(statistics, trend, unit, window); - result.setSummary(summary); - - // 生成专业分析建议 - String advice = buildProfessionalAdvice(statistics, trend, warning, unit); - result.setProfessionalAdvice(advice); - - // 设置安全评估 - if (warning != null) { - result.setSafetyLevel(convertWarningToSafetyLevel(warning.getStatus())); - result.setSafetyDescription(warning.getDescription()); - } - - // 设置控制值 - ReportThreshold threshold = context.getThresholds().get(primaryIdentity); - if (threshold != null && threshold.getControlValue() != null) { - result.setControlValue(threshold.getControlValue()); - result.setControlValueUnit(threshold.getUnit()); - } - - // 组装多测点数据(支持多物模型标识符,如X轴/Y轴倾角) - List metricPoints = buildMetricPoints(result, context); - result.setMetricPoints(metricPoints); - - // 生成详细结论 - List conclusions = new ArrayList<>(); - int seq = 1; - if (statistics == null || statistics.getCount() == 0) { - conclusions.add(new Conclusion(seq++, "统计期内无有效监测数据,请检查设备上报状态与时间范围。")); - } else { - String direction = trend == null ? "待分析" : directionText(trend.getDirection()); - conclusions.add(new Conclusion(seq++, String.format( - "统计期内倾角由 %s%s 变化至 %s%s,变化量 %s%s,倾角呈%s趋势。", - statistics.getInitial().toPlainString(), unit, - statistics.getCurrent().toPlainString(), unit, - statistics.getDelta().toPlainString(), unit, - direction))); - - BigDecimal rate = rateCalculator.ratePerDay(statistics, - Duration.between(window.getStartTime(), window.getEndTime()).getSeconds()); - conclusions.add(new Conclusion(seq++, String.format( - "平均变化速率 %s%s/天;统计期最大倾角 %s%s、最小倾角 %s%s、平均倾角 %s%s。", - rate.toPlainString(), unit, - statistics.getMax().toPlainString(), unit, - statistics.getMin().toPlainString(), unit, - statistics.getAvg().toPlainString(), unit))); - - // 添加控制值对比结论 - if (result.getControlValue() != null && result.getControlValue().compareTo(BigDecimal.ZERO) != 0) { - BigDecimal ratio = statistics.getCurrent().abs().divide(result.getControlValue(), 4, BigDecimal.ROUND_HALF_UP) - .multiply(new BigDecimal("100")); - conclusions.add(new Conclusion(seq++, String.format( - "当前倾角占控制值比例:%.2f%%(控制值:%s%s),需关注结构倾斜发展趋势。", - ratio.doubleValue(), result.getControlValue().toPlainString(), result.getControlValueUnit()))); - } - - // 倾角安全评估 - if (statistics.getCurrent().abs().compareTo(new BigDecimal("5")) > 0) { - conclusions.add(new Conclusion(seq++, "倾角较大,需评估结构倾斜风险,必要时采取纠偏措施。")); - } - } - - if (trend != null && StringUtils.isNotEmpty(trend.getDescription())) { - conclusions.add(new Conclusion(seq++, "趋势分析:" + trend.getDescription() + "。")); - } - if (warning != null && StringUtils.isNotEmpty(warning.getDescription())) { - conclusions.add(new Conclusion(seq++, "预警状态:" + statusText(warning.getStatus()) - + "。" + warning.getDescription() + "。")); - } - if (quality != null && StringUtils.isNotEmpty(quality.getDescription())) { - conclusions.add(new Conclusion(seq, "数据质量:" + quality.getDescription() + "。")); - } - result.setConclusions(conclusions); + @Override + protected boolean allowAuxiliaryAsPrimary() { + return true; // 倾角计的主指标本身就是角度 } - /** - * 生成项目概况总结 - */ - private String buildSummary(Statistics statistics, TrendAnalysis trend, String unit, TimeWindow window) { - if (statistics == null || statistics.getCount() == 0) { - return "统计期内无有效监测数据。"; - } - - String direction = trend == null ? "待分析" : directionText(trend.getDirection()); - BigDecimal rate = rateCalculator.ratePerDay(statistics, - Duration.between(window.getStartTime(), window.getEndTime()).getSeconds()); - - return String.format( - "本期倾角监测数据显示:当前倾角为%s%s,较初始值变化%s%s,变化速率为%s%s/天,倾角呈%s趋势。" + - "统计期内最大倾角%s%s,最小倾角%s%s,平均倾角%s%s。结构倾斜监测正常。", - statistics.getCurrent().toPlainString(), unit, - statistics.getDelta().toPlainString(), unit, - rate.toPlainString(), unit, - direction, - statistics.getMax().toPlainString(), unit, - statistics.getMin().toPlainString(), unit, - statistics.getAvg().toPlainString(), unit - ); - } - - /** - * 生成专业分析建议 - */ - private String buildProfessionalAdvice(Statistics statistics, TrendAnalysis trend, WarningAnalysis warning, String unit) { - if (statistics == null || statistics.getCount() == 0) { - return "建议检查设备上报状态,确保数据采集正常。"; - } - - StringBuilder advice = new StringBuilder(); - advice.append("专业分析:"); - - // 基于趋势方向给出建议 - if (trend != null) { - if (TrendAnalysis.DIRECTION_UP.equals(trend.getDirection())) { - advice.append("倾角呈增大趋势,结构倾斜加剧,需分析倾斜原因与影响。"); - } else if (TrendAnalysis.DIRECTION_DOWN.equals(trend.getDirection())) { - advice.append("倾角呈减小趋势,结构倾斜趋缓或回正。"); - } else { - advice.append("倾角变化稳定,结构倾斜状态正常。"); - } - } - - // 基于倾角量级给出建议 - if (statistics.getCurrent().abs().compareTo(new BigDecimal("3")) > 0) { - advice.append("当前倾角较大,建议结合建筑物使用年限、基础形式等因素综合评估结构安全性。"); - } - - // 基于预警状态给出建议 - if (warning != null) { - if (WarningAnalysis.STATUS_ALARM.equals(warning.getStatus())) { - advice.append("当前处于报警状态,建议立即评估结构安全性,必要时采取纠偏或加固措施。"); - } else if (WarningAnalysis.STATUS_WARNING.equals(warning.getStatus())) { - advice.append("当前处于预警状态,建议加密监测频率,制定纠偏预案。"); - } - } - - return advice.toString(); - } - - /** - * 将预警状态转换为安全等级 - */ - private String convertWarningToSafetyLevel(String warningStatus) { - if (WarningAnalysis.STATUS_ALARM.equals(warningStatus)) { - return "DANGER"; - } else if (WarningAnalysis.STATUS_WARNING.equals(warningStatus)) { - return "WARNING"; - } - return "SAFE"; - } - - /** - * 组装多测点数据(支持多物模型标识符) - */ - private List buildMetricPoints(AnalysisResult result, ReportContext context) { - List points = new ArrayList<>(); - - for (Map.Entry entry : result.getStatistics().entrySet()) { - String identity = entry.getKey(); - Statistics stats = entry.getValue(); - - MetricPointData point = MetricPointData.builder() - .identifier(identity) - .modelName(stats.getModelName()) - .initialValue(stats.getInitial()) - .initialUnit(stats.getUnit()) - .currentValue(stats.getCurrent()) - .currentUnit(stats.getUnit()) - .changeValue(stats.getDelta()) - .changeUnit(stats.getUnit()) - .maxValue(stats.getMax()) - .minValue(stats.getMin()) - .avgValue(stats.getAvg()) - .build(); - - // 设置预警状态 - WarningAnalysis warning = result.getWarnings().get(identity); - if (warning != null) { - point.setWarningStatus(warning.getStatus()); - } - - // 计算变化速率 - TimeWindow window = context.getTimeWindow(); - BigDecimal rate = rateCalculator.ratePerDay(stats, - Duration.between(window.getStartTime(), window.getEndTime()).getSeconds()); - point.setChangeRate(rate); - point.setRateUnit(stats.getUnit() + "/天"); - - points.add(point); - } - - return points; - } - - private void buildChart(AnalysisResult result, ReportContext context, String primaryIdentity) { - // 主指标单图 + 其余指标按单位分组多曲线图(展示所有物模型数据) - Map> byIdentity = context.getMetricDataList().stream() - .filter(d -> d.getIdentity() != null) - .collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList())); - Map charts = metricChartBuilder.build(result.getStatistics(), primaryIdentity, byIdentity); - result.getCharts().putAll(metricChartBuilder.fillPlaceholders(charts)); - } - - private String statusText(String status) { - if (WarningAnalysis.STATUS_ALARM.equals(status)) { - return "报警"; - } - if (WarningAnalysis.STATUS_WARNING.equals(status)) { - return "预警"; - } - return "正常"; - } - - private String directionText(String direction) { + @Override + protected String directionText(String direction) { if (TrendAnalysis.DIRECTION_UP.equals(direction)) { return "倾斜增大"; } diff --git a/fastbee-report/src/main/java/com/fastbee/report/analyzer/rain_gauge/RainGaugeAnalyzer.java b/fastbee-report/src/main/java/com/fastbee/report/analyzer/rain_gauge/RainGaugeAnalyzer.java index d1e5e8dc..c1d1795d 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/analyzer/rain_gauge/RainGaugeAnalyzer.java +++ b/fastbee-report/src/main/java/com/fastbee/report/analyzer/rain_gauge/RainGaugeAnalyzer.java @@ -1,40 +1,14 @@ package com.fastbee.report.analyzer.rain_gauge; import com.fastbee.common.enums.MonitorCategory; -import com.fastbee.common.utils.StringUtils; -import com.fastbee.iot.domain.ThingsModel; -import com.fastbee.report.analysis.PrimaryMetricResolver; -import com.fastbee.report.analysis.QualityAnalyzer; -import com.fastbee.report.analysis.RateCalculator; -import com.fastbee.report.analysis.StatisticsCalculator; -import com.fastbee.report.analysis.TrendAnalyzer; -import com.fastbee.report.analysis.WarningAnalyzer; -import com.fastbee.report.analyzer.ReportAnalyzer; -import com.fastbee.report.api.ReportRequest; -import com.fastbee.report.chart.ChartGenerator; -import com.fastbee.report.chart.MetricChartBuilder; -import com.fastbee.report.core.ReportContext; -import com.fastbee.report.data.MetricData; -import com.fastbee.report.model.AnalysisResult; -import com.fastbee.report.model.Conclusion; -import com.fastbee.report.model.MetricPointData; -import com.fastbee.report.model.QualityAnalysis; +import com.fastbee.report.analyzer.AbstractDeviceAnalyzer; 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 org.springframework.stereotype.Component; -import javax.annotation.Resource; import java.math.BigDecimal; -import java.time.Duration; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.stream.Collectors; /** * 雨量计专项分析器 @@ -43,314 +17,20 @@ import java.util.stream.Collectors; * @date 2026-08-23 */ @Component -public class RainGaugeAnalyzer implements ReportAnalyzer { - - @Resource - private StatisticsCalculator statisticsCalculator; - - @Resource - private TrendAnalyzer trendAnalyzer; - - @Resource - private RateCalculator rateCalculator; - - @Resource - private QualityAnalyzer qualityAnalyzer; - - @Resource - private WarningAnalyzer warningAnalyzer; - - @Resource - private ChartGenerator chartGenerator; - - @Resource - private PrimaryMetricResolver primaryMetricResolver; - - @Resource - private MetricChartBuilder metricChartBuilder; +public class RainGaugeAnalyzer extends AbstractDeviceAnalyzer { @Override - public boolean supports(MonitorCategory category) { - return category == MonitorCategory.RAIN_GAUGE; + protected MonitorCategory supportedCategory() { + return MonitorCategory.RAIN_GAUGE; } @Override - public AnalysisResult analyze(ReportContext context, ReportRequest request) { - AnalysisResult result = new AnalysisResult(); - - // 1. 按物模型标识符分组历史数据 - Map> byIdentity = context.getMetricDataList().stream() - .filter(d -> d.getIdentity() != null) - .collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList())); - - // 2. 获取所有监测物模型(即使没有数据也要显示) - Map allModels = context.getThingsModels().stream() - .filter(m -> m.getIdentifier() != null) - .collect(Collectors.toMap(ThingsModel::getIdentifier, m -> m, (o, n) -> n, LinkedHashMap::new)); - - if (allModels.isEmpty()) { - result.getConclusions().add(new Conclusion(1, "设备未配置监测物模型,请联系管理员配置。")); - return result; - } - - // 3. 为每个物模型生成统计数据(包括无数据的物模型) - for (Map.Entry entry : allModels.entrySet()) { - String identity = entry.getKey(); - ThingsModel model = entry.getValue(); - List data = byIdentity.getOrDefault(identity, new ArrayList<>()); - - ReportThreshold threshold = context.getThresholds().get(identity); - BigDecimal controlValue = threshold == null ? null : threshold.getControlValue(); - - Statistics statistics = statisticsCalculator.calculate(data); - // 补充物模型信息(确保即使无数据也有 modelName 和 unit) - if (statistics.getModelName() == null || statistics.getModelName().isEmpty()) { - statistics.setModelName(model.getModelName()); - } - if (statistics.getUnit() == null || statistics.getUnit().isEmpty()) { - statistics.setUnit(model.getUnit()); - } - statistics.setIdentifier(identity); - - TrendAnalysis trend = trendAnalyzer.analyze(data); - QualityAnalysis quality = qualityAnalyzer.analyze(data, context.getTimeWindow(), controlValue); - WarningAnalysis warning = warningAnalyzer.analyze( - identity, statistics.getModelName(), statistics.getCurrent(), threshold); - - result.getStatistics().put(identity, statistics); - result.getTrends().put(identity, trend); - result.getQualities().put(identity, quality); - result.getWarnings().put(identity, warning); - } - - // 统一主指标口径:优先名称/标识符命中关键词的物模型,排除角度/温度/电压等辅助量 - String primaryIdentity = primaryMetricResolver.resolve(context, byIdentity, false, "雨量", "降雨", "降水量", "rain"); - result.setPrimaryIdentifier(primaryIdentity); - buildConclusions(result, context, primaryIdentity); - buildChart(result, context, primaryIdentity); - return result; + protected String[] primaryMetricKeywords() { + return new String[]{"雨量", "降雨", "降水量", "rain"}; } - private void buildConclusions(AnalysisResult result, ReportContext context, String primaryIdentity) { - Statistics statistics = result.getStatistics().get(primaryIdentity); - TrendAnalysis trend = result.getTrends().get(primaryIdentity); - QualityAnalysis quality = result.getQualities().get(primaryIdentity); - WarningAnalysis warning = result.getWarnings().get(primaryIdentity); - TimeWindow window = context.getTimeWindow(); - String unit = statistics == null ? "" : (statistics.getUnit() == null ? "" : statistics.getUnit()); - - // 生成项目概况总结 - String summary = buildSummary(statistics, trend, unit, window); - result.setSummary(summary); - - // 生成专业分析建议 - String advice = buildProfessionalAdvice(statistics, trend, warning, unit); - result.setProfessionalAdvice(advice); - - // 设置安全评估 - if (warning != null) { - result.setSafetyLevel(convertWarningToSafetyLevel(warning.getStatus())); - result.setSafetyDescription(warning.getDescription()); - } - - // 设置控制值 - ReportThreshold threshold = context.getThresholds().get(primaryIdentity); - if (threshold != null && threshold.getControlValue() != null) { - result.setControlValue(threshold.getControlValue()); - result.setControlValueUnit(threshold.getUnit()); - } - - // 组装多测点数据(支持多物模型标识符) - List metricPoints = buildMetricPoints(result, context); - result.setMetricPoints(metricPoints); - - // 生成详细结论 - List conclusions = new ArrayList<>(); - int seq = 1; - if (statistics == null || statistics.getCount() == 0) { - conclusions.add(new Conclusion(seq++, "统计期内无有效监测数据,请检查设备上报状态与时间范围。")); - } else { - String direction = trend == null ? "待分析" : directionText(trend.getDirection()); - // 雨量计特殊:显示累计降雨量 - conclusions.add(new Conclusion(seq++, String.format( - "统计期内累计降雨量 %s%s,当前降雨强度显示值 %s%s,降雨呈%s趋势。", - statistics.getDelta().abs().toPlainString(), unit, - statistics.getCurrent().toPlainString(), unit, - direction))); - - BigDecimal rate = rateCalculator.ratePerDay(statistics, - Duration.between(window.getStartTime(), window.getEndTime()).getSeconds()); - conclusions.add(new Conclusion(seq++, String.format( - "平均日降雨量 %s%s/天;统计期最大单次降雨 %s%s、平均降雨强度 %s%s。", - rate.toPlainString(), unit, - statistics.getMax().toPlainString(), unit, - statistics.getAvg().toPlainString(), unit))); - - // 添加控制值对比结论 - if (result.getControlValue() != null && result.getControlValue().compareTo(BigDecimal.ZERO) != 0) { - BigDecimal ratio = statistics.getDelta().abs().divide(result.getControlValue(), 4, BigDecimal.ROUND_HALF_UP) - .multiply(new BigDecimal("100")); - conclusions.add(new Conclusion(seq++, String.format( - "累计降雨量占控制值比例:%.2f%%(控制值:%s%s),需关注降雨对边坡稳定性的影响。", - ratio.doubleValue(), result.getControlValue().toPlainString(), result.getControlValueUnit()))); - } - - // 降雨强度评估 - if (statistics.getDelta().abs().compareTo(new BigDecimal("50")) > 0) { - conclusions.add(new Conclusion(seq++, "统计期内降雨量较大,需加强巡视检查,关注排水设施运行状况。")); - } - } - - if (trend != null && StringUtils.isNotEmpty(trend.getDescription())) { - conclusions.add(new Conclusion(seq++, "趋势分析:" + trend.getDescription() + "。")); - } - if (warning != null && StringUtils.isNotEmpty(warning.getDescription())) { - conclusions.add(new Conclusion(seq++, "预警状态:" + statusText(warning.getStatus()) - + "。" + warning.getDescription() + "。")); - } - if (quality != null && StringUtils.isNotEmpty(quality.getDescription())) { - conclusions.add(new Conclusion(seq, "数据质量:" + quality.getDescription() + "。")); - } - result.setConclusions(conclusions); - } - - /** - * 生成项目概况总结 - */ - private String buildSummary(Statistics statistics, TrendAnalysis trend, String unit, TimeWindow window) { - if (statistics == null || statistics.getCount() == 0) { - return "统计期内无有效监测数据。"; - } - - String direction = trend == null ? "待分析" : directionText(trend.getDirection()); - BigDecimal rate = rateCalculator.ratePerDay(statistics, - Duration.between(window.getStartTime(), window.getEndTime()).getSeconds()); - - return String.format( - "本期降雨监测数据显示:统计期内累计降雨量%s%s,平均日降雨量%s%s/天,降雨呈%s趋势。" + - "统计期内最大单次降雨%s%s,平均降雨强度%s%s。环境气象监测正常。", - statistics.getDelta().abs().toPlainString(), unit, - rate.toPlainString(), unit, - direction, - statistics.getMax().toPlainString(), unit, - statistics.getAvg().toPlainString(), unit - ); - } - - /** - * 生成专业分析建议 - */ - private String buildProfessionalAdvice(Statistics statistics, TrendAnalysis trend, WarningAnalysis warning, String unit) { - if (statistics == null || statistics.getCount() == 0) { - return "建议检查设备上报状态,确保数据采集正常。"; - } - - StringBuilder advice = new StringBuilder(); - advice.append("专业分析:"); - - // 基于趋势方向给出建议 - if (trend != null) { - if (TrendAnalysis.DIRECTION_UP.equals(trend.getDirection())) { - advice.append("降雨呈增强趋势,需关注降雨对边坡稳定性、地下水位的影响。"); - } else if (TrendAnalysis.DIRECTION_DOWN.equals(trend.getDirection())) { - advice.append("降雨呈减弱趋势,天气转好。"); - } else { - advice.append("降雨强度平稳,气象条件正常。"); - } - } - - // 基于累计降雨量给出建议 - if (statistics.getDelta().abs().compareTo(new BigDecimal("30")) > 0) { - advice.append("统计期内降雨量较大,建议结合地质资料分析降雨对边坡稳定性的影响,必要时加密监测频次。"); - } - - // 基于预警状态给出建议 - if (warning != null) { - if (WarningAnalysis.STATUS_ALARM.equals(warning.getStatus())) { - advice.append("当前处于报警状态,建议立即启动应急预案,加强巡视检查,做好防汛准备。"); - } else if (WarningAnalysis.STATUS_WARNING.equals(warning.getStatus())) { - advice.append("当前处于预警状态,建议加强巡视检查,关注排水设施运行状况,准备应急物资。"); - } - } - - return advice.toString(); - } - - /** - * 将预警状态转换为安全等级 - */ - private String convertWarningToSafetyLevel(String warningStatus) { - if (WarningAnalysis.STATUS_ALARM.equals(warningStatus)) { - return "DANGER"; - } else if (WarningAnalysis.STATUS_WARNING.equals(warningStatus)) { - return "WARNING"; - } - return "SAFE"; - } - - /** - * 组装多测点数据(支持多物模型标识符) - */ - private List buildMetricPoints(AnalysisResult result, ReportContext context) { - List points = new ArrayList<>(); - - for (Map.Entry entry : result.getStatistics().entrySet()) { - String identity = entry.getKey(); - Statistics stats = entry.getValue(); - - MetricPointData point = MetricPointData.builder() - .identifier(identity) - .modelName(stats.getModelName()) - .initialValue(stats.getInitial()) - .initialUnit(stats.getUnit()) - .currentValue(stats.getCurrent()) - .currentUnit(stats.getUnit()) - .changeValue(stats.getDelta()) - .changeUnit(stats.getUnit()) - .maxValue(stats.getMax()) - .minValue(stats.getMin()) - .avgValue(stats.getAvg()) - .build(); - - // 设置预警状态 - WarningAnalysis warning = result.getWarnings().get(identity); - if (warning != null) { - point.setWarningStatus(warning.getStatus()); - } - - // 计算变化速率 - TimeWindow window = context.getTimeWindow(); - BigDecimal rate = rateCalculator.ratePerDay(stats, - Duration.between(window.getStartTime(), window.getEndTime()).getSeconds()); - point.setChangeRate(rate); - point.setRateUnit(stats.getUnit() + "/天"); - - points.add(point); - } - - return points; - } - - private void buildChart(AnalysisResult result, ReportContext context, String primaryIdentity) { - // 主指标单图 + 其余指标按单位分组多曲线图(展示所有物模型数据) - Map> byIdentity = context.getMetricDataList().stream() - .filter(d -> d.getIdentity() != null) - .collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList())); - Map charts = metricChartBuilder.build(result.getStatistics(), primaryIdentity, byIdentity); - result.getCharts().putAll(metricChartBuilder.fillPlaceholders(charts)); - } - - private String statusText(String status) { - if (WarningAnalysis.STATUS_ALARM.equals(status)) { - return "报警"; - } - if (WarningAnalysis.STATUS_WARNING.equals(status)) { - return "预警"; - } - return "正常"; - } - - private String directionText(String direction) { + @Override + protected String directionText(String direction) { if (TrendAnalysis.DIRECTION_UP.equals(direction)) { return "增强"; } @@ -359,4 +39,66 @@ public class RainGaugeAnalyzer implements ReportAnalyzer { } return "平稳"; } + + @Override + protected String buildSummary(Statistics statistics, TrendAnalysis trend, String unit, TimeWindow window) { + if (statistics == null || statistics.getCount() == 0) { + return "统计期内无有效监测数据。"; + } + + String direction = trend == null ? "待分析" : directionText(trend.getDirection()); + // 累计降雨量 = delta.abs() + BigDecimal cumulativeRainfall = statistics.getDelta().abs(); + String formattedCumulative = valueFormatter.format(cumulativeRainfall, supportedCategory()); + String formattedMax = valueFormatter.format(statistics.getMax(), supportedCategory()); + String formattedMin = valueFormatter.format(statistics.getMin(), supportedCategory()); + String formattedAvg = valueFormatter.format(statistics.getAvg(), supportedCategory()); + String resolvedUnit = valueFormatter.resolveUnit(unit, supportedCategory()); + + // 计算平均日降雨量 + long days = java.time.Duration.between(window.getStartTime(), window.getEndTime()).toDays(); + days = Math.max(days, 1); + BigDecimal dailyAvg = cumulativeRainfall.divide(BigDecimal.valueOf(days), 2, BigDecimal.ROUND_HALF_UP); + String formattedDailyAvg = valueFormatter.format(dailyAvg, supportedCategory()); + + return String.format( + "本期降雨监测数据显示:统计期内累计降雨量%s%s,平均日降雨量%s%s,降雨强度呈%s趋势。" + + "统计期内最大小时降雨量%s%s,最小小时降雨量%s%s,平均小时降雨量%s%s。", + formattedCumulative, resolvedUnit, + formattedDailyAvg, resolvedUnit, + direction, + formattedMax, resolvedUnit, + formattedMin, resolvedUnit, + formattedAvg, resolvedUnit + ); + } + + @Override + protected String buildProfessionalAdvice(Statistics statistics, TrendAnalysis trend, WarningAnalysis warning, String unit) { + if (statistics == null || statistics.getCount() == 0) { + return "建议检查设备上报状态,确保数据采集正常。"; + } + + StringBuilder advice = new StringBuilder(); + advice.append("专业分析:"); + + BigDecimal absDelta = statistics.getDelta().abs(); + if (absDelta.compareTo(new BigDecimal("50")) > 0) { + advice.append("累计降雨量较大(>50mm),建议加强边坡、基坑等重点区域巡视检查。"); + } + + if (trend != null && TrendAnalysis.DIRECTION_UP.equals(trend.getDirection())) { + advice.append("降雨强度呈增强趋势,需关注降雨对边坡稳定性的影响。"); + } + + if (warning != null) { + if (WarningAnalysis.STATUS_ALARM.equals(warning.getStatus())) { + advice.append("当前处于报警状态,建议立即启动防汛应急预案,加强排水措施。"); + } else if (WarningAnalysis.STATUS_WARNING.equals(warning.getStatus())) { + advice.append("当前处于预警状态,建议准备防汛物资,制定应急预案。"); + } + } + + return advice.toString(); + } } \ No newline at end of file diff --git a/fastbee-report/src/main/java/com/fastbee/report/analyzer/ranging_meter/RangingMeterAnalyzer.java b/fastbee-report/src/main/java/com/fastbee/report/analyzer/ranging_meter/RangingMeterAnalyzer.java index 7ea9d86d..cf44f5f2 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/analyzer/ranging_meter/RangingMeterAnalyzer.java +++ b/fastbee-report/src/main/java/com/fastbee/report/analyzer/ranging_meter/RangingMeterAnalyzer.java @@ -1,41 +1,13 @@ package com.fastbee.report.analyzer.ranging_meter; import com.fastbee.common.enums.MonitorCategory; -import com.fastbee.common.utils.StringUtils; -import com.fastbee.iot.domain.ThingsModel; -import com.fastbee.report.analysis.PrimaryMetricResolver; -import com.fastbee.report.analysis.QualityAnalyzer; -import com.fastbee.report.analysis.RateCalculator; -import com.fastbee.report.analysis.StatisticsCalculator; -import com.fastbee.report.analysis.TrendAnalyzer; -import com.fastbee.report.analysis.WarningAnalyzer; -import com.fastbee.report.analyzer.ReportAnalyzer; -import com.fastbee.report.api.ReportRequest; -import com.fastbee.report.chart.ChartGenerator; -import com.fastbee.report.chart.MetricChartBuilder; -import com.fastbee.report.core.ReportContext; -import com.fastbee.report.data.MetricData; -import com.fastbee.report.model.AnalysisResult; -import com.fastbee.report.model.Conclusion; -import com.fastbee.report.model.MetricPointData; -import com.fastbee.report.model.QualityAnalysis; +import com.fastbee.report.analyzer.AbstractDeviceAnalyzer; 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 org.springframework.stereotype.Component; -import javax.annotation.Resource; -import java.math.BigDecimal; -import java.time.Duration; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.stream.Collectors; - /** * 测距仪专项分析器 * @@ -43,318 +15,15 @@ import java.util.stream.Collectors; * @date 2026-08-23 */ @Component -public class RangingMeterAnalyzer implements ReportAnalyzer { - - @Resource - private StatisticsCalculator statisticsCalculator; - - @Resource - private TrendAnalyzer trendAnalyzer; - - @Resource - private RateCalculator rateCalculator; - - @Resource - private QualityAnalyzer qualityAnalyzer; - - @Resource - private WarningAnalyzer warningAnalyzer; - - @Resource - private ChartGenerator chartGenerator; - - @Resource - private PrimaryMetricResolver primaryMetricResolver; - - @Resource - private MetricChartBuilder metricChartBuilder; +public class RangingMeterAnalyzer extends AbstractDeviceAnalyzer { @Override - public boolean supports(MonitorCategory category) { - return category == MonitorCategory.RANGING_METER; + protected MonitorCategory supportedCategory() { + return MonitorCategory.RANGING_METER; } @Override - public AnalysisResult analyze(ReportContext context, ReportRequest request) { - AnalysisResult result = new AnalysisResult(); - - // 1. 按物模型标识符分组历史数据 - Map> byIdentity = context.getMetricDataList().stream() - .filter(d -> d.getIdentity() != null) - .collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList())); - - // 2. 获取所有监测物模型(即使没有数据也要显示) - Map allModels = context.getThingsModels().stream() - .filter(m -> m.getIdentifier() != null) - .collect(Collectors.toMap(ThingsModel::getIdentifier, m -> m, (o, n) -> n, LinkedHashMap::new)); - - if (allModels.isEmpty()) { - result.getConclusions().add(new Conclusion(1, "设备未配置监测物模型,请联系管理员配置。")); - return result; - } - - // 3. 为每个物模型生成统计数据(包括无数据的物模型) - for (Map.Entry entry : allModels.entrySet()) { - String identity = entry.getKey(); - ThingsModel model = entry.getValue(); - List data = byIdentity.getOrDefault(identity, new ArrayList<>()); - - ReportThreshold threshold = context.getThresholds().get(identity); - BigDecimal controlValue = threshold == null ? null : threshold.getControlValue(); - - Statistics statistics = statisticsCalculator.calculate(data); - // 补充物模型信息(确保即使无数据也有 modelName 和 unit) - if (statistics.getModelName() == null || statistics.getModelName().isEmpty()) { - statistics.setModelName(model.getModelName()); - } - if (statistics.getUnit() == null || statistics.getUnit().isEmpty()) { - statistics.setUnit(model.getUnit()); - } - statistics.setIdentifier(identity); - - TrendAnalysis trend = trendAnalyzer.analyze(data); - QualityAnalysis quality = qualityAnalyzer.analyze(data, context.getTimeWindow(), controlValue); - WarningAnalysis warning = warningAnalyzer.analyze( - identity, statistics.getModelName(), statistics.getCurrent(), threshold); - - result.getStatistics().put(identity, statistics); - result.getTrends().put(identity, trend); - result.getQualities().put(identity, quality); - result.getWarnings().put(identity, warning); - } - - // 统一主指标口径:优先名称/标识符命中关键词的物模型,排除角度/温度/电压等辅助量 - String primaryIdentity = primaryMetricResolver.resolve(context, byIdentity, false, "距离", "位移", "distance"); - result.setPrimaryIdentifier(primaryIdentity); - buildConclusions(result, context, primaryIdentity); - buildChart(result, context, primaryIdentity); - return result; - } - - private void buildConclusions(AnalysisResult result, ReportContext context, String primaryIdentity) { - Statistics statistics = result.getStatistics().get(primaryIdentity); - TrendAnalysis trend = result.getTrends().get(primaryIdentity); - QualityAnalysis quality = result.getQualities().get(primaryIdentity); - WarningAnalysis warning = result.getWarnings().get(primaryIdentity); - TimeWindow window = context.getTimeWindow(); - String unit = statistics == null ? "" : (statistics.getUnit() == null ? "" : statistics.getUnit()); - - // 生成项目概况总结 - String summary = buildSummary(statistics, trend, unit, window); - result.setSummary(summary); - - // 生成专业分析建议 - String advice = buildProfessionalAdvice(statistics, trend, warning, unit); - result.setProfessionalAdvice(advice); - - // 设置安全评估 - if (warning != null) { - result.setSafetyLevel(convertWarningToSafetyLevel(warning.getStatus())); - result.setSafetyDescription(warning.getDescription()); - } - - // 设置控制值 - ReportThreshold threshold = context.getThresholds().get(primaryIdentity); - if (threshold != null && threshold.getControlValue() != null) { - result.setControlValue(threshold.getControlValue()); - result.setControlValueUnit(threshold.getUnit()); - } - - // 组装多测点数据(支持多物模型标识符) - List metricPoints = buildMetricPoints(result, context); - result.setMetricPoints(metricPoints); - - // 生成详细结论 - List conclusions = new ArrayList<>(); - int seq = 1; - if (statistics == null || statistics.getCount() == 0) { - conclusions.add(new Conclusion(seq++, "统计期内无有效监测数据,请检查设备上报状态与时间范围。")); - } else { - String direction = trend == null ? "待分析" : directionText(trend.getDirection()); - conclusions.add(new Conclusion(seq++, String.format( - "统计期内测距值由 %s%s 变化至 %s%s,距离变化量 %s%s,测距呈%s趋势。", - statistics.getInitial().toPlainString(), unit, - statistics.getCurrent().toPlainString(), unit, - statistics.getDelta().toPlainString(), unit, - direction))); - - BigDecimal rate = rateCalculator.ratePerDay(statistics, - Duration.between(window.getStartTime(), window.getEndTime()).getSeconds()); - conclusions.add(new Conclusion(seq++, String.format( - "平均变化速率 %s%s/天;统计期最大测距 %s%s、最小测距 %s%s、平均测距 %s%s。", - rate.toPlainString(), unit, - statistics.getMax().toPlainString(), unit, - statistics.getMin().toPlainString(), unit, - statistics.getAvg().toPlainString(), unit))); - - // 添加控制值对比结论 - if (result.getControlValue() != null && result.getControlValue().compareTo(BigDecimal.ZERO) != 0) { - BigDecimal ratio = statistics.getDelta().abs().divide(result.getControlValue(), 4, BigDecimal.ROUND_HALF_UP) - .multiply(new BigDecimal("100")); - conclusions.add(new Conclusion(seq++, String.format( - "距离变化占控制值比例:%.2f%%(控制值:%s%s),需关注收敛变形趋势。", - ratio.doubleValue(), result.getControlValue().toPlainString(), result.getControlValueUnit()))); - } - } - - if (trend != null && StringUtils.isNotEmpty(trend.getDescription())) { - conclusions.add(new Conclusion(seq++, "趋势分析:" + trend.getDescription() + "。")); - } - if (warning != null && StringUtils.isNotEmpty(warning.getDescription())) { - conclusions.add(new Conclusion(seq++, "预警状态:" + statusText(warning.getStatus()) - + "。" + warning.getDescription() + "。")); - } - if (quality != null && StringUtils.isNotEmpty(quality.getDescription())) { - conclusions.add(new Conclusion(seq, "数据质量:" + quality.getDescription() + "。")); - } - result.setConclusions(conclusions); - } - - /** - * 生成项目概况总结 - */ - private String buildSummary(Statistics statistics, TrendAnalysis trend, String unit, TimeWindow window) { - if (statistics == null || statistics.getCount() == 0) { - return "统计期内无有效监测数据。"; - } - - String direction = trend == null ? "待分析" : directionText(trend.getDirection()); - BigDecimal rate = rateCalculator.ratePerDay(statistics, - Duration.between(window.getStartTime(), window.getEndTime()).getSeconds()); - - return String.format( - "本期测距监测数据显示:当前测距为%s%s,较初始值变化%s%s,变化速率为%s%s/天,测距呈%s趋势。" + - "统计期内最大测距%s%s,最小测距%s%s,平均测距%s%s。变形监测正常。", - statistics.getCurrent().toPlainString(), unit, - statistics.getDelta().toPlainString(), unit, - rate.toPlainString(), unit, - direction, - statistics.getMax().toPlainString(), unit, - statistics.getMin().toPlainString(), unit, - statistics.getAvg().toPlainString(), unit - ); - } - - /** - * 生成专业分析建议 - */ - private String buildProfessionalAdvice(Statistics statistics, TrendAnalysis trend, WarningAnalysis warning, String unit) { - if (statistics == null || statistics.getCount() == 0) { - return "建议检查设备上报状态,确保数据采集正常。"; - } - - StringBuilder advice = new StringBuilder(); - advice.append("专业分析:"); - - // 基于趋势方向给出建议 - if (trend != null) { - if (TrendAnalysis.DIRECTION_UP.equals(trend.getDirection())) { - advice.append("测距呈增大趋势,可能存在收敛变形,需分析变形原因与速率。"); - } else if (TrendAnalysis.DIRECTION_DOWN.equals(trend.getDirection())) { - advice.append("测距呈减小趋势,可能存在挤压变形或测点位移,需结合其他监测数据分析。"); - } else { - advice.append("测距变化稳定,变形正常。"); - } - } - - // 基于变化量级给出建议 - if (statistics.getDelta().abs().compareTo(new BigDecimal("5")) > 0) { - advice.append("距离变化较大,建议分析变形原因,评估结构安全性。"); - } - - // 基于预警状态给出建议 - if (warning != null) { - if (WarningAnalysis.STATUS_ALARM.equals(warning.getStatus())) { - advice.append("当前处于报警状态,建议立即评估结构安全性,必要时采取加固措施。"); - } else if (WarningAnalysis.STATUS_WARNING.equals(warning.getStatus())) { - advice.append("当前处于预警状态,建议加密监测频率,准备应急预案。"); - } - } - - return advice.toString(); - } - - /** - * 将预警状态转换为安全等级 - */ - private String convertWarningToSafetyLevel(String warningStatus) { - if (WarningAnalysis.STATUS_ALARM.equals(warningStatus)) { - return "DANGER"; - } else if (WarningAnalysis.STATUS_WARNING.equals(warningStatus)) { - return "WARNING"; - } - return "SAFE"; - } - - /** - * 组装多测点数据(支持多物模型标识符) - */ - private List buildMetricPoints(AnalysisResult result, ReportContext context) { - List points = new ArrayList<>(); - - for (Map.Entry entry : result.getStatistics().entrySet()) { - String identity = entry.getKey(); - Statistics stats = entry.getValue(); - - MetricPointData point = MetricPointData.builder() - .identifier(identity) - .modelName(stats.getModelName()) - .initialValue(stats.getInitial()) - .initialUnit(stats.getUnit()) - .currentValue(stats.getCurrent()) - .currentUnit(stats.getUnit()) - .changeValue(stats.getDelta()) - .changeUnit(stats.getUnit()) - .maxValue(stats.getMax()) - .minValue(stats.getMin()) - .avgValue(stats.getAvg()) - .build(); - - // 设置预警状态 - WarningAnalysis warning = result.getWarnings().get(identity); - if (warning != null) { - point.setWarningStatus(warning.getStatus()); - } - - // 计算变化速率 - TimeWindow window = context.getTimeWindow(); - BigDecimal rate = rateCalculator.ratePerDay(stats, - Duration.between(window.getStartTime(), window.getEndTime()).getSeconds()); - point.setChangeRate(rate); - point.setRateUnit(stats.getUnit() + "/天"); - - points.add(point); - } - - return points; - } - - private void buildChart(AnalysisResult result, ReportContext context, String primaryIdentity) { - // 主指标单图 + 其余指标按单位分组多曲线图(展示所有物模型数据) - Map> byIdentity = context.getMetricDataList().stream() - .filter(d -> d.getIdentity() != null) - .collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList())); - Map charts = metricChartBuilder.build(result.getStatistics(), primaryIdentity, byIdentity); - result.getCharts().putAll(metricChartBuilder.fillPlaceholders(charts)); - } - - private String statusText(String status) { - if (WarningAnalysis.STATUS_ALARM.equals(status)) { - return "报警"; - } - if (WarningAnalysis.STATUS_WARNING.equals(status)) { - return "预警"; - } - return "正常"; - } - - private String directionText(String direction) { - if (TrendAnalysis.DIRECTION_UP.equals(direction)) { - return "增大"; - } - if (TrendAnalysis.DIRECTION_DOWN.equals(direction)) { - return "减小"; - } - return "稳定"; + protected String[] primaryMetricKeywords() { + return new String[]{"距离", "位移", "distance"}; } } \ No newline at end of file diff --git a/fastbee-report/src/main/java/com/fastbee/report/analyzer/static_level/StaticLevelAnalyzer.java b/fastbee-report/src/main/java/com/fastbee/report/analyzer/static_level/StaticLevelAnalyzer.java index 428b99e6..fa88c2bd 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/analyzer/static_level/StaticLevelAnalyzer.java +++ b/fastbee-report/src/main/java/com/fastbee/report/analyzer/static_level/StaticLevelAnalyzer.java @@ -2,253 +2,110 @@ package com.fastbee.report.analyzer.static_level; import com.fastbee.common.enums.MonitorCategory; import com.fastbee.common.utils.StringUtils; -import com.fastbee.iot.domain.ThingsModel; -import com.fastbee.report.analysis.PrimaryMetricResolver; -import com.fastbee.report.analysis.QualityAnalyzer; -import com.fastbee.report.analysis.RateCalculator; -import com.fastbee.report.analysis.StatisticsCalculator; -import com.fastbee.report.analysis.TrendAnalyzer; -import com.fastbee.report.analysis.WarningAnalyzer; -import com.fastbee.report.analyzer.ReportAnalyzer; -import com.fastbee.report.api.ReportRequest; -import com.fastbee.report.chart.ChartGenerator; -import com.fastbee.report.chart.MetricChartBuilder; -import com.fastbee.report.core.ReportContext; -import com.fastbee.report.data.MetricData; +import com.fastbee.report.analyzer.AbstractDeviceAnalyzer; import com.fastbee.report.model.AnalysisResult; import com.fastbee.report.model.Conclusion; -import com.fastbee.report.model.MetricPointData; -import com.fastbee.report.model.QualityAnalysis; 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.core.ReportContext; import org.springframework.stereotype.Component; -import javax.annotation.Resource; import java.math.BigDecimal; -import java.time.Duration; -import java.util.ArrayList; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.stream.Collectors; /** * 静力水准专项分析器 * + *

注意:delta = current - initial 为高程变化(非沉降量)

+ *
    + *
  • delta > 0:高程上升 → 隆起(回弹)
  • + *
  • delta < 0:高程下降 → 沉降
  • + *
+ * * @author FastBee * @date 2026-08-23 */ @Component -public class StaticLevelAnalyzer implements ReportAnalyzer { - - @Resource - private StatisticsCalculator statisticsCalculator; - - @Resource - private TrendAnalyzer trendAnalyzer; - - @Resource - private RateCalculator rateCalculator; - - @Resource - private QualityAnalyzer qualityAnalyzer; - - @Resource - private WarningAnalyzer warningAnalyzer; - - @Resource - private ChartGenerator chartGenerator; - - @Resource - private PrimaryMetricResolver primaryMetricResolver; - - @Resource - private MetricChartBuilder metricChartBuilder; +public class StaticLevelAnalyzer extends AbstractDeviceAnalyzer { @Override - public boolean supports(MonitorCategory category) { - return category == MonitorCategory.STATIC_LEVEL; + protected MonitorCategory supportedCategory() { + return MonitorCategory.STATIC_LEVEL; } @Override - public AnalysisResult analyze(ReportContext context, ReportRequest request) { - AnalysisResult result = new AnalysisResult(); - - // 1. 按物模型标识符分组历史数据 - Map> byIdentity = context.getMetricDataList().stream() - .filter(d -> d.getIdentity() != null) - .collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList())); - - // 2. 获取所有监测物模型(即使没有数据也要显示) - Map allModels = context.getThingsModels().stream() - .filter(m -> m.getIdentifier() != null) - .collect(Collectors.toMap(ThingsModel::getIdentifier, m -> m, (o, n) -> n, LinkedHashMap::new)); - - if (allModels.isEmpty()) { - result.getConclusions().add(new Conclusion(1, "设备未配置监测物模型,请联系管理员配置。")); - return result; - } - - // 3. 为每个物模型生成统计数据(包括无数据的物模型) - for (Map.Entry entry : allModels.entrySet()) { - String identity = entry.getKey(); - ThingsModel model = entry.getValue(); - List data = byIdentity.getOrDefault(identity, new ArrayList<>()); - - ReportThreshold threshold = context.getThresholds().get(identity); - BigDecimal controlValue = threshold == null ? null : threshold.getControlValue(); - - Statistics statistics = statisticsCalculator.calculate(data); - // 补充物模型信息(确保即使无数据也有 modelName 和 unit) - if (statistics.getModelName() == null || statistics.getModelName().isEmpty()) { - statistics.setModelName(model.getModelName()); - } - if (statistics.getUnit() == null || statistics.getUnit().isEmpty()) { - statistics.setUnit(model.getUnit()); - } - statistics.setIdentifier(identity); - - TrendAnalysis trend = trendAnalyzer.analyze(data); - QualityAnalysis quality = qualityAnalyzer.analyze(data, context.getTimeWindow(), controlValue); - WarningAnalysis warning = warningAnalyzer.analyze( - identity, statistics.getModelName(), statistics.getCurrent(), threshold); - - result.getStatistics().put(identity, statistics); - result.getTrends().put(identity, trend); - result.getQualities().put(identity, quality); - result.getWarnings().put(identity, warning); - } - - // 统一主指标口径:优先名称/标识符命中关键词的物模型,排除角度/温度/电压等辅助量 - String primaryIdentity = primaryMetricResolver.resolve(context, byIdentity, false, "沉降", "高程", "settlement"); - result.setPrimaryIdentifier(primaryIdentity); - buildConclusions(result, context, primaryIdentity); - buildChart(result, context, primaryIdentity); - return result; + protected String[] primaryMetricKeywords() { + // 静力水准物模型通常叫"水位"(实际是高程),用沉降/高程关键词匹配 + return new String[]{"沉降", "高程", "settlement", "水位"}; } - private void buildConclusions(AnalysisResult result, ReportContext context, String primaryIdentity) { - Statistics statistics = result.getStatistics().get(primaryIdentity); - TrendAnalysis trend = result.getTrends().get(primaryIdentity); - QualityAnalysis quality = result.getQualities().get(primaryIdentity); - WarningAnalysis warning = result.getWarnings().get(primaryIdentity); - TimeWindow window = context.getTimeWindow(); - String unit = statistics == null ? "" : (statistics.getUnit() == null ? "" : statistics.getUnit()); - - // 生成项目概况总结 - String summary = buildSummary(statistics, trend, unit, window); - result.setSummary(summary); - - // 生成专业分析建议 - String advice = buildProfessionalAdvice(statistics, trend, warning, unit, result.getStatistics(), window); - result.setProfessionalAdvice(advice); - - // 设置安全评估 - if (warning != null) { - result.setSafetyLevel(convertWarningToSafetyLevel(warning.getStatus())); - result.setSafetyDescription(warning.getDescription()); + @Override + protected String directionText(String direction) { + // 方向语义:UP/DOWN 指高程变化方向,映射到沉降术语 + if (TrendAnalysis.DIRECTION_UP.equals(direction)) { + return "回弹隆起"; // 高程上升 } - - // 设置控制值 - ReportThreshold threshold = context.getThresholds().get(primaryIdentity); - if (threshold != null && threshold.getControlValue() != null) { - result.setControlValue(threshold.getControlValue()); - result.setControlValueUnit(threshold.getUnit()); + if (TrendAnalysis.DIRECTION_DOWN.equals(direction)) { + return "持续沉降"; // 高程下降 } - - // 组装多测点数据(支持多测点高程监测) - List metricPoints = buildMetricPoints(result, context); - result.setMetricPoints(metricPoints); - - // 生成详细结论 - List conclusions = new ArrayList<>(); - int seq = 1; - if (statistics == null || statistics.getCount() == 0) { - conclusions.add(new Conclusion(seq++, "统计期内无有效监测数据,请检查设备上报状态与时间范围。")); - } else { - String direction = trend == null ? "待分析" : directionText(trend.getDirection()); - conclusions.add(new Conclusion(seq++, String.format( - "统计期内高程由 %s%s 变化至 %s%s,累计沉降量 %s%s,结构呈%s趋势。", - statistics.getInitial().toPlainString(), unit, - statistics.getCurrent().toPlainString(), unit, - statistics.getDelta().toPlainString(), unit, - direction))); - - BigDecimal rate = rateCalculator.ratePerDay(statistics, - Duration.between(window.getStartTime(), window.getEndTime()).getSeconds()); - conclusions.add(new Conclusion(seq++, String.format( - "平均沉降速率 %s%s/天;统计期最大沉降量 %s%s、最小沉降量 %s%s、平均沉降量 %s%s。", - rate.toPlainString(), unit, - statistics.getMax().toPlainString(), unit, - statistics.getMin().toPlainString(), unit, - statistics.getAvg().toPlainString(), unit))); - - // 添加控制值对比结论 - if (result.getControlValue() != null && result.getControlValue().compareTo(BigDecimal.ZERO) != 0) { - BigDecimal absDelta = statistics.getDelta().abs(); - BigDecimal ratio = absDelta.divide(result.getControlValue(), 4, BigDecimal.ROUND_HALF_UP) - .multiply(new BigDecimal("100")); - conclusions.add(new Conclusion(seq++, String.format( - "累计沉降量占控制值比例:%.2f%%(控制值:%s%s)。", - ratio.doubleValue(), result.getControlValue().toPlainString(), result.getControlValueUnit()))); - } - - // 不均匀沉降评估(多测点场景) - String differentialSettlement = assessDifferentialSettlement(result.getStatistics()); - if (StringUtils.isNotEmpty(differentialSettlement)) { - conclusions.add(new Conclusion(seq++, differentialSettlement)); - } - } - - if (trend != null && StringUtils.isNotEmpty(trend.getDescription())) { - conclusions.add(new Conclusion(seq++, "趋势分析:" + trend.getDescription() + "。")); - } - if (warning != null && StringUtils.isNotEmpty(warning.getDescription())) { - conclusions.add(new Conclusion(seq++, "预警状态:" + statusText(warning.getStatus()) - + "。" + warning.getDescription() + "。")); - } - if (quality != null && StringUtils.isNotEmpty(quality.getDescription())) { - conclusions.add(new Conclusion(seq, "数据质量:" + quality.getDescription() + "。")); - } - result.setConclusions(conclusions); + return "稳定"; } /** - * 生成项目概况总结 + * 判断沉降类型(高程变化方向 → 沉降术语) */ - private String buildSummary(Statistics statistics, TrendAnalysis trend, String unit, TimeWindow window) { + private String getSettlementType(BigDecimal delta) { + if (delta == null) { + return "待分析"; + } + if (delta.compareTo(BigDecimal.ZERO) > 0) { + return "隆起"; // 高程上升 + } + if (delta.compareTo(BigDecimal.ZERO) < 0) { + return "沉降"; // 高程下降 + } + return "稳定"; + } + + @Override + protected String buildSummary(Statistics statistics, TrendAnalysis trend, String unit, TimeWindow window) { if (statistics == null || statistics.getCount() == 0) { return "统计期内无有效监测数据。"; } String direction = trend == null ? "待分析" : directionText(trend.getDirection()); - BigDecimal rate = rateCalculator.ratePerDay(statistics, - Duration.between(window.getStartTime(), window.getEndTime()).getSeconds()); - String settlementType = getSettlementType(statistics.getDelta()); + String formattedCurrent = valueFormatter.format(statistics.getCurrent(), supportedCategory()); + String formattedDelta = valueFormatter.format(statistics.getDelta(), supportedCategory()); + String formattedMax = valueFormatter.format(statistics.getMax(), supportedCategory()); + String formattedMin = valueFormatter.format(statistics.getMin(), supportedCategory()); + String formattedAvg = valueFormatter.format(statistics.getAvg(), supportedCategory()); + String resolvedUnit = valueFormatter.resolveUnit(unit, supportedCategory()); + + // 使用回归斜率作为速率 + String rateText = "未计算"; + if (trend != null && trend.getSlope() != null) { + String formattedSlope = valueFormatter.format(trend.getSlope(), supportedCategory()); + rateText = formattedSlope + resolvedUnit + "/天"; + } return String.format( - "本期静力水准监测数据显示:当前高程为%s%s,较初始值变化%s%s(%s),变化速率为%s%s/天,结构呈%s趋势。" + - "统计期内最大沉降量%s%s,最小沉降量%s%s,平均沉降量%s%s。", - statistics.getCurrent().toPlainString(), unit, - statistics.getDelta().toPlainString(), unit, settlementType, - rate.toPlainString(), unit, + "本期静力水准监测数据显示:当前高程为%s%s,较初始值变化%s%s(%s),变化速率为%s,结构呈%s趋势。" + + "统计期内最大高程%s%s,最小高程%s%s,平均高程%s%s。", + formattedCurrent, resolvedUnit, + formattedDelta, resolvedUnit, settlementType, + rateText, direction, - statistics.getMax().toPlainString(), unit, - statistics.getMin().toPlainString(), unit, - statistics.getAvg().toPlainString(), unit + formattedMax, resolvedUnit, + formattedMin, resolvedUnit, + formattedAvg, resolvedUnit ); } - /** - * 生成专业分析建议 - */ - private String buildProfessionalAdvice(Statistics statistics, TrendAnalysis trend, WarningAnalysis warning, - String unit, Map allStatistics, TimeWindow window) { + @Override + protected String buildProfessionalAdvice(Statistics statistics, TrendAnalysis trend, WarningAnalysis warning, String unit) { if (statistics == null || statistics.getCount() == 0) { return "建议检查设备上报状态,确保数据采集正常。"; } @@ -259,108 +116,64 @@ public class StaticLevelAnalyzer implements ReportAnalyzer { // 基于趋势方向给出建议 if (trend != null) { if (TrendAnalysis.DIRECTION_UP.equals(trend.getDirection())) { - advice.append("结构呈持续下沉趋势,需关注沉降速率变化及周边环境影响因素。"); + advice.append("结构呈回弹隆起趋势。"); + advice.append("根据 JGJ 8-2016《建筑变形测量规范》,高程上升通常由以下原因引起:"); + advice.append("(1)基坑开挖卸荷回弹;(2)地下水位上升;(3)温度应力影响。"); + advice.append("建议:结合周边施工情况分析回弹原因,评估对上部结构的影响。"); } else if (TrendAnalysis.DIRECTION_DOWN.equals(trend.getDirection())) { - advice.append("结构呈回弹隆起趋势,需关注回弹原因及对结构的影响。"); + advice.append("结构呈持续沉降趋势。"); + advice.append("根据 GB 50497-2019《建筑基坑工程监测标准》,沉降速率控制建议如下:"); + advice.append("速率<1mm/d 属正常;1-2mm/d 需关注;>2mm/d 需预警。"); + + // 基于实际速率给出具体建议 + if (trend.getSlope() != null) { + BigDecimal absSlope = trend.getSlope().abs(); + if (absSlope.compareTo(new BigDecimal("2")) > 0) { + advice.append(String.format("当前沉降速率 %.2fmm/d,超过预警阈值,", absSlope.doubleValue())); + advice.append("建议:加密监测频次至每日2次,查明沉降原因,准备应急预案。"); + } else if (absSlope.compareTo(new BigDecimal("1")) > 0) { + advice.append(String.format("当前沉降速率 %.2fmm/d,需关注。", absSlope.doubleValue())); + advice.append("建议:加强巡视检查,分析沉降发展趋势。"); + } else { + advice.append(String.format("当前沉降速率 %.2fmm/d,属正常范围。", absSlope.doubleValue())); + } + } } else { advice.append("沉降变化稳定,结构状态正常。"); } } - // 基于沉降量给出建议 BigDecimal absDelta = statistics.getDelta().abs(); if (absDelta.compareTo(new BigDecimal("10")) > 0) { - advice.append("累计沉降量较大(>10mm),建议进行详细的地基稳定性评估。"); + advice.append(String.format("累计沉降量已达 %.1fmm。", absDelta.doubleValue())); + advice.append("根据工程经验,建议检查:(1)地基持力层是否压缩;(2)周边是否有新建荷载;(3)地下水位变化。"); } - // 基于沉降速率给出建议 - if (window != null) { - BigDecimal rate = rateCalculator.ratePerDay(statistics, - Duration.between(window.getStartTime(), window.getEndTime()).getSeconds()); - if (rate != null && rate.compareTo(new BigDecimal("2")) > 0) { - advice.append(String.format("沉降速率较快(%.2fmm/天),需加密监测频率。", rate.doubleValue())); - } - } - - // 不均匀沉降分析 - if (allStatistics != null && allStatistics.size() > 1) { - String differentialAdvice = analyzeDifferentialSettlement(allStatistics); - if (StringUtils.isNotEmpty(differentialAdvice)) { - advice.append(differentialAdvice); - } - } - - // 基于预警状态给出建议 - if (warning != null) { - if (WarningAnalysis.STATUS_ALARM.equals(warning.getStatus())) { - advice.append("当前处于报警状态,建议立即采取地基加固措施,并组织专业人员进行安全评估。"); - } else if (WarningAnalysis.STATUS_WARNING.equals(warning.getStatus())) { - advice.append("当前处于预警状态,建议加密监测频率,制定应急预案。"); - } + // 阈值状态说明 + if (warning != null && WarningAnalysis.STATUS_UNKNOWN.equals(warning.getStatus())) { + advice.append("【提示】尚未配置控制值,建议根据设计文件在系统中设置沉降控制值,以便自动评估安全等级。"); + } else if (warning != null && WarningAnalysis.STATUS_ALARM.equals(warning.getStatus())) { + advice.append("当前处于报警状态,建议立即采取地基加固措施,组织专业人员进行安全评估。"); + } else if (warning != null && WarningAnalysis.STATUS_WARNING.equals(warning.getStatus())) { + advice.append("当前处于预警状态,建议加密监测频率,制定应急预案。"); } return advice.toString(); } - /** - * 将预警状态转换为安全等级 - */ - private String convertWarningToSafetyLevel(String warningStatus) { - if (WarningAnalysis.STATUS_ALARM.equals(warningStatus)) { - return "DANGER"; - } else if (WarningAnalysis.STATUS_WARNING.equals(warningStatus)) { - return "WARNING"; + @Override + protected void addDomainConclusions(List conclusions, AnalysisResult result, ReportContext context) { + // 添加不均匀沉降评估(多测点场景) + String differential = assessDifferentialSettlement(result.getStatistics()); + if (StringUtils.isNotEmpty(differential)) { + conclusions.add(new Conclusion(conclusions.size() + 1, differential)); } - return "SAFE"; - } - - /** - * 组装多测点数据(支持多测点高程监测) - */ - private List buildMetricPoints(AnalysisResult result, ReportContext context) { - List points = new ArrayList<>(); - - for (Map.Entry entry : result.getStatistics().entrySet()) { - String identity = entry.getKey(); - Statistics stats = entry.getValue(); - - MetricPointData point = MetricPointData.builder() - .identifier(identity) - .modelName(stats.getModelName()) - .initialValue(stats.getInitial()) - .initialUnit(stats.getUnit()) - .currentValue(stats.getCurrent()) - .currentUnit(stats.getUnit()) - .changeValue(stats.getDelta()) - .changeUnit(stats.getUnit()) - .maxValue(stats.getMax()) - .minValue(stats.getMin()) - .avgValue(stats.getAvg()) - .build(); - - // 设置预警状态 - WarningAnalysis warning = result.getWarnings().get(identity); - if (warning != null) { - point.setWarningStatus(warning.getStatus()); - } - - // 计算变化速率 - TimeWindow window = context.getTimeWindow(); - BigDecimal rate = rateCalculator.ratePerDay(stats, - Duration.between(window.getStartTime(), window.getEndTime()).getSeconds()); - point.setChangeRate(rate); - point.setRateUnit(stats.getUnit() + "/天"); - - points.add(point); - } - - return points; } /** * 评估不均匀沉降(多测点场景) */ - private String assessDifferentialSettlement(Map allStatistics) { + private String assessDifferentialSettlement(java.util.Map allStatistics) { if (allStatistics == null || allStatistics.size() < 2) { return null; } @@ -383,88 +196,11 @@ public class StaticLevelAnalyzer implements ReportAnalyzer { if (maxDelta != null && minDelta != null) { BigDecimal differential = maxDelta.subtract(minDelta); if (differential.compareTo(new BigDecimal("5")) > 0) { - return String.format("存在不均匀沉降,测点间最大沉降差为%s%s,建议关注结构整体稳定性。", - differential.toPlainString(), "mm"); + return String.format("存在不均匀沉降,测点间最大沉降差为%s mm,建议关注结构整体稳定性。", + valueFormatter.format(differential, supportedCategory())); } } return null; } - - /** - * 分析不均匀沉降(用于专业建议) - */ - private String analyzeDifferentialSettlement(Map allStatistics) { - if (allStatistics == null || allStatistics.size() < 2) { - return ""; - } - - BigDecimal maxDelta = null; - BigDecimal minDelta = null; - - for (Statistics stats : allStatistics.values()) { - if (stats == null || stats.getDelta() == null) { - continue; - } - if (maxDelta == null || stats.getDelta().abs().compareTo(maxDelta) > 0) { - maxDelta = stats.getDelta().abs(); - } - if (minDelta == null || stats.getDelta().abs().compareTo(minDelta) < 0) { - minDelta = stats.getDelta().abs(); - } - } - - if (maxDelta != null && minDelta != null) { - BigDecimal differential = maxDelta.subtract(minDelta); - if (differential.compareTo(new BigDecimal("5")) > 0) { - return "存在明显不均匀沉降现象,需关注对结构整体稳定性的影响。"; - } - } - - return ""; - } - - /** - * 判断沉降类型 - */ - private String getSettlementType(BigDecimal delta) { - if (delta == null) { - return "待分析"; - } - if (delta.compareTo(BigDecimal.ZERO) > 0) { - return "下沉"; - } else if (delta.compareTo(BigDecimal.ZERO) < 0) { - return "隆起"; - } - return "稳定"; - } - - private void buildChart(AnalysisResult result, ReportContext context, String primaryIdentity) { - // 主指标单图 + 其余指标按单位分组多曲线图(展示所有物模型数据) - Map> byIdentity = context.getMetricDataList().stream() - .filter(d -> d.getIdentity() != null) - .collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList())); - Map charts = metricChartBuilder.build(result.getStatistics(), primaryIdentity, byIdentity); - result.getCharts().putAll(metricChartBuilder.fillPlaceholders(charts)); - } - - private String statusText(String status) { - if (WarningAnalysis.STATUS_ALARM.equals(status)) { - return "报警"; - } - if (WarningAnalysis.STATUS_WARNING.equals(status)) { - return "预警"; - } - return "正常"; - } - - private String directionText(String direction) { - if (TrendAnalysis.DIRECTION_UP.equals(direction)) { - return "持续下沉"; - } - if (TrendAnalysis.DIRECTION_DOWN.equals(direction)) { - return "回弹隆起"; - } - return "稳定"; - } } \ No newline at end of file diff --git a/fastbee-report/src/main/java/com/fastbee/report/analyzer/strain_gauge/StrainGaugeAnalyzer.java b/fastbee-report/src/main/java/com/fastbee/report/analyzer/strain_gauge/StrainGaugeAnalyzer.java index f702a846..e7246c44 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/analyzer/strain_gauge/StrainGaugeAnalyzer.java +++ b/fastbee-report/src/main/java/com/fastbee/report/analyzer/strain_gauge/StrainGaugeAnalyzer.java @@ -1,40 +1,14 @@ package com.fastbee.report.analyzer.strain_gauge; import com.fastbee.common.enums.MonitorCategory; -import com.fastbee.common.utils.StringUtils; -import com.fastbee.iot.domain.ThingsModel; -import com.fastbee.report.analysis.PrimaryMetricResolver; -import com.fastbee.report.analysis.QualityAnalyzer; -import com.fastbee.report.analysis.RateCalculator; -import com.fastbee.report.analysis.StatisticsCalculator; -import com.fastbee.report.analysis.TrendAnalyzer; -import com.fastbee.report.analysis.WarningAnalyzer; -import com.fastbee.report.analyzer.ReportAnalyzer; -import com.fastbee.report.api.ReportRequest; -import com.fastbee.report.chart.ChartGenerator; -import com.fastbee.report.chart.MetricChartBuilder; -import com.fastbee.report.core.ReportContext; -import com.fastbee.report.data.MetricData; -import com.fastbee.report.model.AnalysisResult; -import com.fastbee.report.model.Conclusion; -import com.fastbee.report.model.MetricPointData; -import com.fastbee.report.model.QualityAnalysis; +import com.fastbee.report.analyzer.AbstractDeviceAnalyzer; 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 org.springframework.stereotype.Component; -import javax.annotation.Resource; import java.math.BigDecimal; -import java.time.Duration; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.stream.Collectors; /** * 应变计专项分析器 @@ -43,324 +17,50 @@ import java.util.stream.Collectors; * @date 2026-08-23 */ @Component -public class StrainGaugeAnalyzer implements ReportAnalyzer { - - @Resource - private StatisticsCalculator statisticsCalculator; - - @Resource - private TrendAnalyzer trendAnalyzer; - - @Resource - private RateCalculator rateCalculator; - - @Resource - private QualityAnalyzer qualityAnalyzer; - - @Resource - private WarningAnalyzer warningAnalyzer; - - @Resource - private ChartGenerator chartGenerator; - - @Resource - private PrimaryMetricResolver primaryMetricResolver; - - @Resource - private MetricChartBuilder metricChartBuilder; +public class StrainGaugeAnalyzer extends AbstractDeviceAnalyzer { @Override - public boolean supports(MonitorCategory category) { - return category == MonitorCategory.STRAIN_GAUGE; + protected MonitorCategory supportedCategory() { + return MonitorCategory.STRAIN_GAUGE; } @Override - public AnalysisResult analyze(ReportContext context, ReportRequest request) { - AnalysisResult result = new AnalysisResult(); - - // 1. 按物模型标识符分组历史数据 - Map> byIdentity = context.getMetricDataList().stream() - .filter(d -> d.getIdentity() != null) - .collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList())); - - // 2. 获取所有监测物模型(即使没有数据也要显示) - Map allModels = context.getThingsModels().stream() - .filter(m -> m.getIdentifier() != null) - .collect(Collectors.toMap(ThingsModel::getIdentifier, m -> m, (o, n) -> n, LinkedHashMap::new)); - - if (allModels.isEmpty()) { - result.getConclusions().add(new Conclusion(1, "设备未配置监测物模型,请联系管理员配置。")); - return result; - } - - // 3. 为每个物模型生成统计数据(包括无数据的物模型) - for (Map.Entry entry : allModels.entrySet()) { - String identity = entry.getKey(); - ThingsModel model = entry.getValue(); - List data = byIdentity.getOrDefault(identity, new ArrayList<>()); - - ReportThreshold threshold = context.getThresholds().get(identity); - BigDecimal controlValue = threshold == null ? null : threshold.getControlValue(); - - Statistics statistics = statisticsCalculator.calculate(data); - // 补充物模型信息(确保即使无数据也有 modelName 和 unit) - if (statistics.getModelName() == null || statistics.getModelName().isEmpty()) { - statistics.setModelName(model.getModelName()); - } - if (statistics.getUnit() == null || statistics.getUnit().isEmpty()) { - statistics.setUnit(model.getUnit()); - } - statistics.setIdentifier(identity); - - TrendAnalysis trend = trendAnalyzer.analyze(data); - QualityAnalysis quality = qualityAnalyzer.analyze(data, context.getTimeWindow(), controlValue); - WarningAnalysis warning = warningAnalyzer.analyze( - identity, statistics.getModelName(), statistics.getCurrent(), threshold); - - result.getStatistics().put(identity, statistics); - result.getTrends().put(identity, trend); - result.getQualities().put(identity, quality); - result.getWarnings().put(identity, warning); - } - - // 统一主指标口径:优先名称/标识符命中关键词的物模型,排除角度/温度/电压等辅助量 - String primaryIdentity = primaryMetricResolver.resolve(context, byIdentity, false, "应变", "strain"); - result.setPrimaryIdentifier(primaryIdentity); - buildConclusions(result, context, primaryIdentity); - buildChart(result, context, primaryIdentity); - return result; + protected String[] primaryMetricKeywords() { + return new String[]{"应变", "strain"}; } - private void buildConclusions(AnalysisResult result, ReportContext context, String primaryIdentity) { - Statistics statistics = result.getStatistics().get(primaryIdentity); - TrendAnalysis trend = result.getTrends().get(primaryIdentity); - QualityAnalysis quality = result.getQualities().get(primaryIdentity); - WarningAnalysis warning = result.getWarnings().get(primaryIdentity); - TimeWindow window = context.getTimeWindow(); - String unit = statistics == null ? "" : (statistics.getUnit() == null ? "" : statistics.getUnit()); - - // 生成项目概况总结 - String summary = buildSummary(statistics, trend, unit, window); - result.setSummary(summary); - - // 生成专业分析建议 - String advice = buildProfessionalAdvice(statistics, trend, warning, unit); - result.setProfessionalAdvice(advice); - - // 设置安全评估 - if (warning != null) { - result.setSafetyLevel(convertWarningToSafetyLevel(warning.getStatus())); - result.setSafetyDescription(warning.getDescription()); - } - - // 设置控制值 - ReportThreshold threshold = context.getThresholds().get(primaryIdentity); - if (threshold != null && threshold.getControlValue() != null) { - result.setControlValue(threshold.getControlValue()); - result.setControlValueUnit(threshold.getUnit()); - } - - // 组装多测点数据(支持多物模型标识符) - List metricPoints = buildMetricPoints(result, context); - result.setMetricPoints(metricPoints); - - // 生成详细结论 - List conclusions = new ArrayList<>(); - int seq = 1; - if (statistics == null || statistics.getCount() == 0) { - conclusions.add(new Conclusion(seq++, "统计期内无有效监测数据,请检查设备上报状态与时间范围。")); - } else { - String strainState = (statistics.getCurrent() != null && statistics.getCurrent().compareTo(BigDecimal.ZERO) >= 0) ? "拉应变" : "压应变"; - String direction = trend == null ? "待分析" : directionText(trend.getDirection()); - conclusions.add(new Conclusion(seq++, String.format( - "统计期内应变由 %s%s 变化至 %s%s,累计变化量 %s%s,当前处于%s状态,应变呈%s趋势。", - statistics.getInitial().toPlainString(), unit, - statistics.getCurrent().toPlainString(), unit, - statistics.getDelta().toPlainString(), unit, - strainState, direction))); - - BigDecimal rate = rateCalculator.ratePerDay(statistics, - Duration.between(window.getStartTime(), window.getEndTime()).getSeconds()); - conclusions.add(new Conclusion(seq++, String.format( - "平均应变速率 %s%s/天;统计期最大应变 %s%s、最小应变 %s%s、平均应变 %s%s。", - rate.toPlainString(), unit, - statistics.getMax().toPlainString(), unit, - statistics.getMin().toPlainString(), unit, - statistics.getAvg().toPlainString(), unit))); - - // 添加控制值对比结论 - if (result.getControlValue() != null && result.getControlValue().compareTo(BigDecimal.ZERO) != 0) { - BigDecimal ratio = statistics.getCurrent().abs().divide(result.getControlValue(), 4, BigDecimal.ROUND_HALF_UP) - .multiply(new BigDecimal("100")); - conclusions.add(new Conclusion(seq++, String.format( - "当前应变占控制值比例:%.2f%%(控制值:%s%s),需关注材料强度储备。", - ratio.doubleValue(), result.getControlValue().toPlainString(), result.getControlValueUnit()))); - } - } - - if (trend != null && StringUtils.isNotEmpty(trend.getDescription())) { - conclusions.add(new Conclusion(seq++, "趋势分析:" + trend.getDescription() + "。")); - } - if (warning != null && StringUtils.isNotEmpty(warning.getDescription())) { - conclusions.add(new Conclusion(seq++, "预警状态:" + statusText(warning.getStatus()) - + "。" + warning.getDescription() + "。")); - } - if (quality != null && StringUtils.isNotEmpty(quality.getDescription())) { - conclusions.add(new Conclusion(seq, "数据质量:" + quality.getDescription() + "。")); - } - result.setConclusions(conclusions); - } - - /** - * 生成项目概况总结 - */ - private String buildSummary(Statistics statistics, TrendAnalysis trend, String unit, TimeWindow window) { + @Override + protected String buildSummary(Statistics statistics, TrendAnalysis trend, String unit, TimeWindow window) { if (statistics == null || statistics.getCount() == 0) { return "统计期内无有效监测数据。"; } - String strainState = (statistics.getCurrent() != null && statistics.getCurrent().compareTo(BigDecimal.ZERO) >= 0) ? "拉应变" : "压应变"; String direction = trend == null ? "待分析" : directionText(trend.getDirection()); - BigDecimal rate = rateCalculator.ratePerDay(statistics, - Duration.between(window.getStartTime(), window.getEndTime()).getSeconds()); + String strainState = statistics.getCurrent().compareTo(BigDecimal.ZERO) >= 0 ? "拉应变" : "压应变"; + String formattedCurrent = valueFormatter.format(statistics.getCurrent(), supportedCategory()); + String formattedDelta = valueFormatter.format(statistics.getDelta(), supportedCategory()); + String formattedMax = valueFormatter.format(statistics.getMax(), supportedCategory()); + String formattedMin = valueFormatter.format(statistics.getMin(), supportedCategory()); + String formattedAvg = valueFormatter.format(statistics.getAvg(), supportedCategory()); + String resolvedUnit = valueFormatter.resolveUnit(unit, supportedCategory()); + + String rateText = "未计算"; + if (trend != null && trend.getSlope() != null) { + String formattedSlope = valueFormatter.format(trend.getSlope(), supportedCategory()); + rateText = formattedSlope + resolvedUnit + "/天"; + } return String.format( - "本期应变监测数据显示:当前应变为%s%s,处于%s状态,较初始值变化%s%s,应变速率为%s%s/天,应变呈%s趋势。" + - "统计期内最大应变%s%s,最小应变%s%s,平均应变%s%s。结构变形监测正常。", - statistics.getCurrent().toPlainString(), unit, + "本期应变监测数据显示:当前应变为%s%s,处于%s状态,较初始值变化%s%s,变化速率为%s,结构呈%s趋势。" + + "统计期内最大应变%s%s,最小应变%s%s,平均应变%s%s。", + formattedCurrent, resolvedUnit, strainState, - statistics.getDelta().toPlainString(), unit, - rate.toPlainString(), unit, + formattedDelta, resolvedUnit, + rateText, direction, - statistics.getMax().toPlainString(), unit, - statistics.getMin().toPlainString(), unit, - statistics.getAvg().toPlainString(), unit + formattedMax, resolvedUnit, + formattedMin, resolvedUnit, + formattedAvg, resolvedUnit ); } - - /** - * 生成专业分析建议 - */ - private String buildProfessionalAdvice(Statistics statistics, TrendAnalysis trend, WarningAnalysis warning, String unit) { - if (statistics == null || statistics.getCount() == 0) { - return "建议检查设备上报状态,确保数据采集正常。"; - } - - StringBuilder advice = new StringBuilder(); - advice.append("专业分析:"); - - // 基于应变状态给出建议 - String strainState = (statistics.getCurrent() != null && statistics.getCurrent().compareTo(BigDecimal.ZERO) >= 0) ? "拉应变" : "压应变"; - if ("拉应变".equals(strainState)) { - advice.append("当前结构处于拉应变状态,需关注材料抗拉强度。"); - } else { - advice.append("当前结构处于压应变状态,需关注材料抗压强度和稳定性。"); - } - - // 基于趋势方向给出建议 - if (trend != null) { - if (TrendAnalysis.DIRECTION_UP.equals(trend.getDirection())) { - advice.append("应变呈增大趋势,结构变形加剧,需评估应力重分布影响。"); - } else if (TrendAnalysis.DIRECTION_DOWN.equals(trend.getDirection())) { - advice.append("应变呈减小趋势,结构变形趋于稳定。"); - } else { - advice.append("应变变化稳定,结构变形正常。"); - } - } - - // 基于预警状态给出建议 - if (warning != null) { - if (WarningAnalysis.STATUS_ALARM.equals(warning.getStatus())) { - advice.append("当前处于报警状态,建议立即评估结构安全性,必要时采取加固或卸载措施。"); - } else if (WarningAnalysis.STATUS_WARNING.equals(warning.getStatus())) { - advice.append("当前处于预警状态,建议加密监测频率,准备应急预案。"); - } - } - - return advice.toString(); - } - - /** - * 将预警状态转换为安全等级 - */ - private String convertWarningToSafetyLevel(String warningStatus) { - if (WarningAnalysis.STATUS_ALARM.equals(warningStatus)) { - return "DANGER"; - } else if (WarningAnalysis.STATUS_WARNING.equals(warningStatus)) { - return "WARNING"; - } - return "SAFE"; - } - - /** - * 组装多测点数据(支持多物模型标识符) - */ - private List buildMetricPoints(AnalysisResult result, ReportContext context) { - List points = new ArrayList<>(); - - for (Map.Entry entry : result.getStatistics().entrySet()) { - String identity = entry.getKey(); - Statistics stats = entry.getValue(); - - MetricPointData point = MetricPointData.builder() - .identifier(identity) - .modelName(stats.getModelName()) - .initialValue(stats.getInitial()) - .initialUnit(stats.getUnit()) - .currentValue(stats.getCurrent()) - .currentUnit(stats.getUnit()) - .changeValue(stats.getDelta()) - .changeUnit(stats.getUnit()) - .maxValue(stats.getMax()) - .minValue(stats.getMin()) - .avgValue(stats.getAvg()) - .build(); - - // 设置预警状态 - WarningAnalysis warning = result.getWarnings().get(identity); - if (warning != null) { - point.setWarningStatus(warning.getStatus()); - } - - // 计算变化速率 - TimeWindow window = context.getTimeWindow(); - BigDecimal rate = rateCalculator.ratePerDay(stats, - Duration.between(window.getStartTime(), window.getEndTime()).getSeconds()); - point.setChangeRate(rate); - point.setRateUnit(stats.getUnit() + "/天"); - - points.add(point); - } - - return points; - } - - private void buildChart(AnalysisResult result, ReportContext context, String primaryIdentity) { - // 主指标单图 + 其余指标按单位分组多曲线图(展示所有物模型数据) - Map> byIdentity = context.getMetricDataList().stream() - .filter(d -> d.getIdentity() != null) - .collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList())); - Map charts = metricChartBuilder.build(result.getStatistics(), primaryIdentity, byIdentity); - result.getCharts().putAll(metricChartBuilder.fillPlaceholders(charts)); - } - - private String statusText(String status) { - if (WarningAnalysis.STATUS_ALARM.equals(status)) { - return "报警"; - } - if (WarningAnalysis.STATUS_WARNING.equals(status)) { - return "预警"; - } - return "正常"; - } - - private String directionText(String direction) { - if (TrendAnalysis.DIRECTION_UP.equals(direction)) { - return "增大"; - } - if (TrendAnalysis.DIRECTION_DOWN.equals(direction)) { - return "减小"; - } - return "稳定"; - } } \ No newline at end of file diff --git a/fastbee-report/src/main/java/com/fastbee/report/analyzer/thermometer/ThermometerAnalyzer.java b/fastbee-report/src/main/java/com/fastbee/report/analyzer/thermometer/ThermometerAnalyzer.java index 6bcfe950..24bd54d1 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/analyzer/thermometer/ThermometerAnalyzer.java +++ b/fastbee-report/src/main/java/com/fastbee/report/analyzer/thermometer/ThermometerAnalyzer.java @@ -1,41 +1,12 @@ package com.fastbee.report.analyzer.thermometer; import com.fastbee.common.enums.MonitorCategory; -import com.fastbee.common.utils.StringUtils; -import com.fastbee.iot.domain.ThingsModel; -import com.fastbee.report.analysis.PrimaryMetricResolver; -import com.fastbee.report.analysis.QualityAnalyzer; -import com.fastbee.report.analysis.RateCalculator; -import com.fastbee.report.analysis.StatisticsCalculator; -import com.fastbee.report.analysis.TrendAnalyzer; -import com.fastbee.report.analysis.WarningAnalyzer; -import com.fastbee.report.analyzer.ReportAnalyzer; -import com.fastbee.report.api.ReportRequest; -import com.fastbee.report.chart.ChartGenerator; -import com.fastbee.report.chart.MetricChartBuilder; -import com.fastbee.report.core.ReportContext; -import com.fastbee.report.data.MetricData; -import com.fastbee.report.model.AnalysisResult; -import com.fastbee.report.model.Conclusion; -import com.fastbee.report.model.MetricPointData; -import com.fastbee.report.model.QualityAnalysis; +import com.fastbee.report.analyzer.AbstractDeviceAnalyzer; 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 org.springframework.stereotype.Component; -import javax.annotation.Resource; -import java.math.BigDecimal; -import java.time.Duration; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.stream.Collectors; - /** * 温度计专项分析器 * @@ -43,319 +14,25 @@ import java.util.stream.Collectors; * @date 2026-08-23 */ @Component -public class ThermometerAnalyzer implements ReportAnalyzer { - - @Resource - private StatisticsCalculator statisticsCalculator; - - @Resource - private TrendAnalyzer trendAnalyzer; - - @Resource - private RateCalculator rateCalculator; - - @Resource - private QualityAnalyzer qualityAnalyzer; - - @Resource - private WarningAnalyzer warningAnalyzer; - - @Resource - private ChartGenerator chartGenerator; - - @Resource - private PrimaryMetricResolver primaryMetricResolver; - - @Resource - private MetricChartBuilder metricChartBuilder; +public class ThermometerAnalyzer extends AbstractDeviceAnalyzer { @Override - public boolean supports(MonitorCategory category) { - return category == MonitorCategory.THERMOMETER; + protected MonitorCategory supportedCategory() { + return MonitorCategory.THERMOMETER; } @Override - public AnalysisResult analyze(ReportContext context, ReportRequest request) { - AnalysisResult result = new AnalysisResult(); - - // 1. 按物模型标识符分组历史数据 - Map> byIdentity = context.getMetricDataList().stream() - .filter(d -> d.getIdentity() != null) - .collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList())); - - // 2. 获取所有监测物模型(即使没有数据也要显示) - Map allModels = context.getThingsModels().stream() - .filter(m -> m.getIdentifier() != null) - .collect(Collectors.toMap(ThingsModel::getIdentifier, m -> m, (o, n) -> n, LinkedHashMap::new)); - - if (allModels.isEmpty()) { - result.getConclusions().add(new Conclusion(1, "设备未配置监测物模型,请联系管理员配置。")); - return result; - } - - // 3. 为每个物模型生成统计数据(包括无数据的物模型) - for (Map.Entry entry : allModels.entrySet()) { - String identity = entry.getKey(); - ThingsModel model = entry.getValue(); - List data = byIdentity.getOrDefault(identity, new ArrayList<>()); - - ReportThreshold threshold = context.getThresholds().get(identity); - BigDecimal controlValue = threshold == null ? null : threshold.getControlValue(); - - Statistics statistics = statisticsCalculator.calculate(data); - // 补充物模型信息(确保即使无数据也有 modelName 和 unit) - if (statistics.getModelName() == null || statistics.getModelName().isEmpty()) { - statistics.setModelName(model.getModelName()); - } - if (statistics.getUnit() == null || statistics.getUnit().isEmpty()) { - statistics.setUnit(model.getUnit()); - } - statistics.setIdentifier(identity); - - TrendAnalysis trend = trendAnalyzer.analyze(data); - QualityAnalysis quality = qualityAnalyzer.analyze(data, context.getTimeWindow(), controlValue); - WarningAnalysis warning = warningAnalyzer.analyze( - identity, statistics.getModelName(), statistics.getCurrent(), threshold); - - result.getStatistics().put(identity, statistics); - result.getTrends().put(identity, trend); - result.getQualities().put(identity, quality); - result.getWarnings().put(identity, warning); - } - - // 统一主指标口径:优先名称/标识符命中关键词的物模型,排除角度/温度/电压等辅助量 - String primaryIdentity = primaryMetricResolver.resolve(context, byIdentity, true, "温度", "temperature"); - result.setPrimaryIdentifier(primaryIdentity); - buildConclusions(result, context, primaryIdentity); - buildChart(result, context, primaryIdentity); - return result; + protected String[] primaryMetricKeywords() { + return new String[]{"温度", "temperature"}; } - private void buildConclusions(AnalysisResult result, ReportContext context, String primaryIdentity) { - Statistics statistics = result.getStatistics().get(primaryIdentity); - TrendAnalysis trend = result.getTrends().get(primaryIdentity); - QualityAnalysis quality = result.getQualities().get(primaryIdentity); - WarningAnalysis warning = result.getWarnings().get(primaryIdentity); - TimeWindow window = context.getTimeWindow(); - String unit = statistics == null ? "" : (statistics.getUnit() == null ? "" : statistics.getUnit()); - - // 生成项目概况总结 - String summary = buildSummary(statistics, trend, unit, window); - result.setSummary(summary); - - // 生成专业分析建议 - String advice = buildProfessionalAdvice(statistics, trend, warning, unit); - result.setProfessionalAdvice(advice); - - // 设置安全评估 - if (warning != null) { - result.setSafetyLevel(convertWarningToSafetyLevel(warning.getStatus())); - result.setSafetyDescription(warning.getDescription()); - } - - // 设置控制值 - ReportThreshold threshold = context.getThresholds().get(primaryIdentity); - if (threshold != null && threshold.getControlValue() != null) { - result.setControlValue(threshold.getControlValue()); - result.setControlValueUnit(threshold.getUnit()); - } - - // 组装多测点数据(支持多物模型标识符) - List metricPoints = buildMetricPoints(result, context); - result.setMetricPoints(metricPoints); - - // 生成详细结论 - List conclusions = new ArrayList<>(); - int seq = 1; - if (statistics == null || statistics.getCount() == 0) { - conclusions.add(new Conclusion(seq++, "统计期内无有效监测数据,请检查设备上报状态与时间范围。")); - } else { - String direction = trend == null ? "待分析" : directionText(trend.getDirection()); - conclusions.add(new Conclusion(seq++, String.format( - "统计期内温度由 %s%s 变化至 %s%s,变化量 %s%s,温度呈%s趋势。", - statistics.getInitial().toPlainString(), unit, - statistics.getCurrent().toPlainString(), unit, - statistics.getDelta().toPlainString(), unit, - direction))); - - BigDecimal rate = rateCalculator.ratePerDay(statistics, - Duration.between(window.getStartTime(), window.getEndTime()).getSeconds()); - conclusions.add(new Conclusion(seq++, String.format( - "平均温度变化速率 %s%s/天;统计期最高温度 %s%s、最低温度 %s%s、平均温度 %s%s。", - rate.toPlainString(), unit, - statistics.getMax().toPlainString(), unit, - statistics.getMin().toPlainString(), unit, - statistics.getAvg().toPlainString(), unit))); - - // 添加控制值对比结论 - if (result.getControlValue() != null && result.getControlValue().compareTo(BigDecimal.ZERO) != 0) { - BigDecimal ratio = statistics.getCurrent().abs().divide(result.getControlValue(), 4, BigDecimal.ROUND_HALF_UP) - .multiply(new BigDecimal("100")); - conclusions.add(new Conclusion(seq++, String.format( - "当前温度占控制值比例:%.2f%%(控制值:%s%s),需关注温度对结构的影响。", - ratio.doubleValue(), result.getControlValue().toPlainString(), result.getControlValueUnit()))); - } - - // 温度异常评估 - BigDecimal tempRange = statistics.getMax().subtract(statistics.getMin()); - if (tempRange.compareTo(new BigDecimal("20")) > 0) { - conclusions.add(new Conclusion(seq++, "统计期内温度波动较大,需关注温差对结构变形的影响。")); - } - } - - if (trend != null && StringUtils.isNotEmpty(trend.getDescription())) { - conclusions.add(new Conclusion(seq++, "趋势分析:" + trend.getDescription() + "。")); - } - if (warning != null && StringUtils.isNotEmpty(warning.getDescription())) { - conclusions.add(new Conclusion(seq++, "预警状态:" + statusText(warning.getStatus()) - + "。" + warning.getDescription() + "。")); - } - if (quality != null && StringUtils.isNotEmpty(quality.getDescription())) { - conclusions.add(new Conclusion(seq, "数据质量:" + quality.getDescription() + "。")); - } - result.setConclusions(conclusions); + @Override + protected boolean allowAuxiliaryAsPrimary() { + return true; // 温度计的主指标本身就是温度 } - /** - * 生成项目概况总结 - */ - private String buildSummary(Statistics statistics, TrendAnalysis trend, String unit, TimeWindow window) { - if (statistics == null || statistics.getCount() == 0) { - return "统计期内无有效监测数据。"; - } - - String direction = trend == null ? "待分析" : directionText(trend.getDirection()); - BigDecimal rate = rateCalculator.ratePerDay(statistics, - Duration.between(window.getStartTime(), window.getEndTime()).getSeconds()); - - return String.format( - "本期温度监测数据显示:当前温度为%s%s,较初始值变化%s%s,温度变化速率为%s%s/天,温度呈%s趋势。" + - "统计期内最高温度%s%s,最低温度%s%s,平均温度%s%s。环境温度监测正常。", - statistics.getCurrent().toPlainString(), unit, - statistics.getDelta().toPlainString(), unit, - rate.toPlainString(), unit, - direction, - statistics.getMax().toPlainString(), unit, - statistics.getMin().toPlainString(), unit, - statistics.getAvg().toPlainString(), unit - ); - } - - /** - * 生成专业分析建议 - */ - private String buildProfessionalAdvice(Statistics statistics, TrendAnalysis trend, WarningAnalysis warning, String unit) { - if (statistics == null || statistics.getCount() == 0) { - return "建议检查设备上报状态,确保数据采集正常。"; - } - - StringBuilder advice = new StringBuilder(); - advice.append("专业分析:"); - - // 基于趋势方向给出建议 - if (trend != null) { - if (TrendAnalysis.DIRECTION_UP.equals(trend.getDirection())) { - advice.append("温度呈上升趋势,需关注高温对材料性能和结构变形的影响。"); - } else if (TrendAnalysis.DIRECTION_DOWN.equals(trend.getDirection())) { - advice.append("温度呈下降趋势,需关注低温对材料性能和结构变形的影响。"); - } else { - advice.append("温度变化平稳,环境温度正常。"); - } - } - - // 基于温差给出建议 - BigDecimal tempRange = statistics.getMax().subtract(statistics.getMin()); - if (tempRange.compareTo(new BigDecimal("15")) > 0) { - advice.append("统计期内温差较大,需分析温差对结构变形的影响,必要时进行温度修正。"); - } - - // 基于预警状态给出建议 - if (warning != null) { - if (WarningAnalysis.STATUS_ALARM.equals(warning.getStatus())) { - advice.append("当前处于报警状态,建议立即评估温度异常对结构安全的影响,必要时采取保护措施。"); - } else if (WarningAnalysis.STATUS_WARNING.equals(warning.getStatus())) { - advice.append("当前处于预警状态,建议加强温度监测,分析温度异常原因。"); - } - } - - return advice.toString(); - } - - /** - * 将预警状态转换为安全等级 - */ - private String convertWarningToSafetyLevel(String warningStatus) { - if (WarningAnalysis.STATUS_ALARM.equals(warningStatus)) { - return "DANGER"; - } else if (WarningAnalysis.STATUS_WARNING.equals(warningStatus)) { - return "WARNING"; - } - return "SAFE"; - } - - /** - * 组装多测点数据(支持多物模型标识符) - */ - private List buildMetricPoints(AnalysisResult result, ReportContext context) { - List points = new ArrayList<>(); - - for (Map.Entry entry : result.getStatistics().entrySet()) { - String identity = entry.getKey(); - Statistics stats = entry.getValue(); - - MetricPointData point = MetricPointData.builder() - .identifier(identity) - .modelName(stats.getModelName()) - .initialValue(stats.getInitial()) - .initialUnit(stats.getUnit()) - .currentValue(stats.getCurrent()) - .currentUnit(stats.getUnit()) - .changeValue(stats.getDelta()) - .changeUnit(stats.getUnit()) - .maxValue(stats.getMax()) - .minValue(stats.getMin()) - .avgValue(stats.getAvg()) - .build(); - - // 设置预警状态 - WarningAnalysis warning = result.getWarnings().get(identity); - if (warning != null) { - point.setWarningStatus(warning.getStatus()); - } - - // 计算变化速率 - TimeWindow window = context.getTimeWindow(); - BigDecimal rate = rateCalculator.ratePerDay(stats, - Duration.between(window.getStartTime(), window.getEndTime()).getSeconds()); - point.setChangeRate(rate); - point.setRateUnit(stats.getUnit() + "/天"); - - points.add(point); - } - - return points; - } - - private void buildChart(AnalysisResult result, ReportContext context, String primaryIdentity) { - // 主指标单图 + 其余指标按单位分组多曲线图(展示所有物模型数据) - Map> byIdentity = context.getMetricDataList().stream() - .filter(d -> d.getIdentity() != null) - .collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList())); - Map charts = metricChartBuilder.build(result.getStatistics(), primaryIdentity, byIdentity); - result.getCharts().putAll(metricChartBuilder.fillPlaceholders(charts)); - } - - private String statusText(String status) { - if (WarningAnalysis.STATUS_ALARM.equals(status)) { - return "报警"; - } - if (WarningAnalysis.STATUS_WARNING.equals(status)) { - return "预警"; - } - return "正常"; - } - - private String directionText(String direction) { + @Override + protected String directionText(String direction) { if (TrendAnalysis.DIRECTION_UP.equals(direction)) { return "上升"; } diff --git a/fastbee-report/src/main/java/com/fastbee/report/analyzer/vibrating_wire/VibratingWireAnalyzer.java b/fastbee-report/src/main/java/com/fastbee/report/analyzer/vibrating_wire/VibratingWireAnalyzer.java index f34b45ef..5bd75668 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/analyzer/vibrating_wire/VibratingWireAnalyzer.java +++ b/fastbee-report/src/main/java/com/fastbee/report/analyzer/vibrating_wire/VibratingWireAnalyzer.java @@ -1,244 +1,36 @@ package com.fastbee.report.analyzer.vibrating_wire; import com.fastbee.common.enums.MonitorCategory; -import com.fastbee.common.utils.StringUtils; -import com.fastbee.iot.domain.ThingsModel; -import com.fastbee.report.analysis.PrimaryMetricResolver; -import com.fastbee.report.analysis.QualityAnalyzer; -import com.fastbee.report.analysis.RateCalculator; -import com.fastbee.report.analysis.StatisticsCalculator; -import com.fastbee.report.analysis.TrendAnalyzer; -import com.fastbee.report.analysis.WarningAnalyzer; -import com.fastbee.report.analyzer.ReportAnalyzer; -import com.fastbee.report.api.ReportRequest; -import com.fastbee.report.chart.ChartGenerator; -import com.fastbee.report.chart.MetricChartBuilder; -import com.fastbee.report.core.ReportContext; -import com.fastbee.report.data.MetricData; -import com.fastbee.report.model.AnalysisResult; -import com.fastbee.report.model.Conclusion; -import com.fastbee.report.model.MetricPointData; -import com.fastbee.report.model.QualityAnalysis; +import com.fastbee.report.analyzer.AbstractDeviceAnalyzer; 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 org.springframework.stereotype.Component; -import javax.annotation.Resource; import java.math.BigDecimal; -import java.time.Duration; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.stream.Collectors; /** - * 振弦专项分析器 + * 振弦式传感器专项分析器 * * @author FastBee * @date 2026-08-23 */ @Component -public class VibratingWireAnalyzer implements ReportAnalyzer { - - @Resource - private StatisticsCalculator statisticsCalculator; - - @Resource - private TrendAnalyzer trendAnalyzer; - - @Resource - private RateCalculator rateCalculator; - - @Resource - private QualityAnalyzer qualityAnalyzer; - - @Resource - private WarningAnalyzer warningAnalyzer; - - @Resource - private ChartGenerator chartGenerator; - - @Resource - private PrimaryMetricResolver primaryMetricResolver; - - @Resource - private MetricChartBuilder metricChartBuilder; +public class VibratingWireAnalyzer extends AbstractDeviceAnalyzer { @Override - public boolean supports(MonitorCategory category) { - return category == MonitorCategory.VIBRATING_WIRE; + protected MonitorCategory supportedCategory() { + return MonitorCategory.VIBRATING_WIRE; } @Override - public AnalysisResult analyze(ReportContext context, ReportRequest request) { - AnalysisResult result = new AnalysisResult(); - - // 1. 按物模型标识符分组历史数据 - Map> byIdentity = context.getMetricDataList().stream() - .filter(d -> d.getIdentity() != null) - .collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList())); - - // 2. 获取所有监测物模型(即使没有数据也要显示) - Map allModels = context.getThingsModels().stream() - .filter(m -> m.getIdentifier() != null) - .collect(Collectors.toMap(ThingsModel::getIdentifier, m -> m, (o, n) -> n, LinkedHashMap::new)); - - if (allModels.isEmpty()) { - result.getConclusions().add(new Conclusion(1, "设备未配置监测物模型,请联系管理员配置。")); - return result; - } - - // 3. 为每个物模型生成统计数据(包括无数据的物模型) - for (Map.Entry entry : allModels.entrySet()) { - String identity = entry.getKey(); - ThingsModel model = entry.getValue(); - List data = byIdentity.getOrDefault(identity, new ArrayList<>()); - - ReportThreshold threshold = context.getThresholds().get(identity); - BigDecimal controlValue = threshold == null ? null : threshold.getControlValue(); - - Statistics statistics = statisticsCalculator.calculate(data); - // 补充物模型信息(确保即使无数据也有 modelName 和 unit) - if (statistics.getModelName() == null || statistics.getModelName().isEmpty()) { - statistics.setModelName(model.getModelName()); - } - if (statistics.getUnit() == null || statistics.getUnit().isEmpty()) { - statistics.setUnit(model.getUnit()); - } - statistics.setIdentifier(identity); - - TrendAnalysis trend = trendAnalyzer.analyze(data); - QualityAnalysis quality = qualityAnalyzer.analyze(data, context.getTimeWindow(), controlValue); - WarningAnalysis warning = warningAnalyzer.analyze( - identity, statistics.getModelName(), statistics.getCurrent(), threshold); - - result.getStatistics().put(identity, statistics); - result.getTrends().put(identity, trend); - result.getQualities().put(identity, quality); - result.getWarnings().put(identity, warning); - } - - // 统一主指标口径:优先名称/标识符命中关键词的物模型,排除角度/温度/电压等辅助量 - String primaryIdentity = primaryMetricResolver.resolve(context, byIdentity, false, "频率", "frequency"); - result.setPrimaryIdentifier(primaryIdentity); - buildConclusions(result, context, primaryIdentity); - buildChart(result, context, primaryIdentity); - return result; + protected String[] primaryMetricKeywords() { + return new String[]{"频率", "frequency"}; } - private void buildConclusions(AnalysisResult result, ReportContext context, String primaryIdentity) { - Statistics statistics = result.getStatistics().get(primaryIdentity); - TrendAnalysis trend = result.getTrends().get(primaryIdentity); - QualityAnalysis quality = result.getQualities().get(primaryIdentity); - WarningAnalysis warning = result.getWarnings().get(primaryIdentity); - TimeWindow window = context.getTimeWindow(); - String unit = statistics == null ? "" : (statistics.getUnit() == null ? "" : statistics.getUnit()); - - // 生成项目概况总结 - String summary = buildSummary(statistics, trend, unit, window); - result.setSummary(summary); - - // 生成专业分析建议 - String advice = buildProfessionalAdvice(statistics, trend, warning, unit); - result.setProfessionalAdvice(advice); - - // 设置安全评估 - if (warning != null) { - result.setSafetyLevel(convertWarningToSafetyLevel(warning.getStatus())); - result.setSafetyDescription(warning.getDescription()); - } - - // 设置控制值 - ReportThreshold threshold = context.getThresholds().get(primaryIdentity); - if (threshold != null && threshold.getControlValue() != null) { - result.setControlValue(threshold.getControlValue()); - result.setControlValueUnit(threshold.getUnit()); - } - - // 组装多测点数据(支持多物模型标识符) - List metricPoints = buildMetricPoints(result, context); - result.setMetricPoints(metricPoints); - - // 生成详细结论 - List conclusions = new ArrayList<>(); - int seq = 1; - if (statistics == null || statistics.getCount() == 0) { - conclusions.add(new Conclusion(seq++, "统计期内无有效监测数据,请检查设备上报状态与时间范围。")); - } else { - String direction = trend == null ? "待分析" : directionText(trend.getDirection()); - conclusions.add(new Conclusion(seq++, String.format( - "统计期内振弦频率由 %s%s 变化至 %s%s,频率变化量 %s%s,频率呈%s趋势。", - statistics.getInitial().toPlainString(), unit, - statistics.getCurrent().toPlainString(), unit, - statistics.getDelta().toPlainString(), unit, - direction))); - - BigDecimal rate = rateCalculator.ratePerDay(statistics, - Duration.between(window.getStartTime(), window.getEndTime()).getSeconds()); - conclusions.add(new Conclusion(seq++, String.format( - "平均频率变化速率 %s%s/天;统计期最大频率 %s%s、最小频率 %s%s、平均频率 %s%s。", - rate.toPlainString(), unit, - statistics.getMax().toPlainString(), unit, - statistics.getMin().toPlainString(), unit, - statistics.getAvg().toPlainString(), unit))); - - // 添加控制值对比结论 - if (result.getControlValue() != null && result.getControlValue().compareTo(BigDecimal.ZERO) != 0) { - BigDecimal ratio = statistics.getDelta().abs().divide(result.getControlValue(), 4, BigDecimal.ROUND_HALF_UP) - .multiply(new BigDecimal("100")); - conclusions.add(new Conclusion(seq++, String.format( - "频率变化占控制值比例:%.2f%%(控制值:%s%s),需结合温度补偿分析应力状态。", - ratio.doubleValue(), result.getControlValue().toPlainString(), result.getControlValueUnit()))); - } - } - - if (trend != null && StringUtils.isNotEmpty(trend.getDescription())) { - conclusions.add(new Conclusion(seq++, "趋势分析:" + trend.getDescription() + "。")); - } - if (warning != null && StringUtils.isNotEmpty(warning.getDescription())) { - conclusions.add(new Conclusion(seq++, "预警状态:" + statusText(warning.getStatus()) - + "。" + warning.getDescription() + "。")); - } - if (quality != null && StringUtils.isNotEmpty(quality.getDescription())) { - conclusions.add(new Conclusion(seq, "数据质量:" + quality.getDescription() + "。")); - } - result.setConclusions(conclusions); - } - - /** - * 生成项目概况总结 - */ - private String buildSummary(Statistics statistics, TrendAnalysis trend, String unit, TimeWindow window) { - if (statistics == null || statistics.getCount() == 0) { - return "统计期内无有效监测数据。"; - } - - String direction = trend == null ? "待分析" : directionText(trend.getDirection()); - BigDecimal rate = rateCalculator.ratePerDay(statistics, - Duration.between(window.getStartTime(), window.getEndTime()).getSeconds()); - - return String.format( - "本期振弦监测数据显示:当前频率为%s%s,较初始值变化%s%s,频率变化速率为%s%s/天,频率呈%s趋势。" + - "统计期内最大频率%s%s,最小频率%s%s,平均频率%s%s。振弦传感器工作正常。", - statistics.getCurrent().toPlainString(), unit, - statistics.getDelta().toPlainString(), unit, - rate.toPlainString(), unit, - direction, - statistics.getMax().toPlainString(), unit, - statistics.getMin().toPlainString(), unit, - statistics.getAvg().toPlainString(), unit - ); - } - - /** - * 生成专业分析建议 - */ - private String buildProfessionalAdvice(Statistics statistics, TrendAnalysis trend, WarningAnalysis warning, String unit) { + @Override + protected String buildProfessionalAdvice(Statistics statistics, TrendAnalysis trend, WarningAnalysis warning, String unit) { if (statistics == null || statistics.getCount() == 0) { return "建议检查设备上报状态,确保数据采集正常。"; } @@ -246,115 +38,28 @@ public class VibratingWireAnalyzer implements ReportAnalyzer { StringBuilder advice = new StringBuilder(); advice.append("专业分析:"); - // 基于趋势方向给出建议 if (trend != null) { if (TrendAnalysis.DIRECTION_UP.equals(trend.getDirection())) { - advice.append("振弦频率呈增大趋势,应力状态发生变化,需结合温度补偿计算实际应力值。"); + advice.append("频率呈增大趋势。"); } else if (TrendAnalysis.DIRECTION_DOWN.equals(trend.getDirection())) { - advice.append("振弦频率呈减小趋势,结构应力状态趋于松弛。"); + advice.append("频率呈减小趋势。"); } else { - advice.append("振弦频率变化稳定,结构应力状态正常。"); + advice.append("频率变化稳定。"); } } - // 基于频率变化量级给出建议 if (statistics.getDelta().abs().compareTo(new BigDecimal("50")) > 0) { - advice.append("频率变化较大,建议检查传感器零点漂移,进行温度补偿修正。"); + advice.append("频率变化较大,建议检查零点漂移和温度补偿是否正常。"); } - // 基于预警状态给出建议 if (warning != null) { if (WarningAnalysis.STATUS_ALARM.equals(warning.getStatus())) { - advice.append("当前处于报警状态,建议立即校验传感器状态,分析应力超限原因,必要时采取加固措施。"); + advice.append("当前处于报警状态,建议立即检查传感器工作状态。"); } else if (WarningAnalysis.STATUS_WARNING.equals(warning.getStatus())) { - advice.append("当前处于预警状态,建议加密监测频率,准备应急预案。"); + advice.append("当前处于预警状态,建议加密监测频率。"); } } return advice.toString(); } - - /** - * 将预警状态转换为安全等级 - */ - private String convertWarningToSafetyLevel(String warningStatus) { - if (WarningAnalysis.STATUS_ALARM.equals(warningStatus)) { - return "DANGER"; - } else if (WarningAnalysis.STATUS_WARNING.equals(warningStatus)) { - return "WARNING"; - } - return "SAFE"; - } - - /** - * 组装多测点数据(支持多物模型标识符) - */ - private List buildMetricPoints(AnalysisResult result, ReportContext context) { - List points = new ArrayList<>(); - - for (Map.Entry entry : result.getStatistics().entrySet()) { - String identity = entry.getKey(); - Statistics stats = entry.getValue(); - - MetricPointData point = MetricPointData.builder() - .identifier(identity) - .modelName(stats.getModelName()) - .initialValue(stats.getInitial()) - .initialUnit(stats.getUnit()) - .currentValue(stats.getCurrent()) - .currentUnit(stats.getUnit()) - .changeValue(stats.getDelta()) - .changeUnit(stats.getUnit()) - .maxValue(stats.getMax()) - .minValue(stats.getMin()) - .avgValue(stats.getAvg()) - .build(); - - // 设置预警状态 - WarningAnalysis warning = result.getWarnings().get(identity); - if (warning != null) { - point.setWarningStatus(warning.getStatus()); - } - - // 计算变化速率 - TimeWindow window = context.getTimeWindow(); - BigDecimal rate = rateCalculator.ratePerDay(stats, - Duration.between(window.getStartTime(), window.getEndTime()).getSeconds()); - point.setChangeRate(rate); - point.setRateUnit(stats.getUnit() + "/天"); - - points.add(point); - } - - return points; - } - - private void buildChart(AnalysisResult result, ReportContext context, String primaryIdentity) { - // 主指标单图 + 其余指标按单位分组多曲线图(展示所有物模型数据) - Map> byIdentity = context.getMetricDataList().stream() - .filter(d -> d.getIdentity() != null) - .collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList())); - Map charts = metricChartBuilder.build(result.getStatistics(), primaryIdentity, byIdentity); - result.getCharts().putAll(metricChartBuilder.fillPlaceholders(charts)); - } - - private String statusText(String status) { - if (WarningAnalysis.STATUS_ALARM.equals(status)) { - return "报警"; - } - if (WarningAnalysis.STATUS_WARNING.equals(status)) { - return "预警"; - } - return "正常"; - } - - private String directionText(String direction) { - if (TrendAnalysis.DIRECTION_UP.equals(direction)) { - return "增大"; - } - if (TrendAnalysis.DIRECTION_DOWN.equals(direction)) { - return "减小"; - } - return "稳定"; - } } \ No newline at end of file diff --git a/fastbee-report/src/main/java/com/fastbee/report/analyzer/water_level/WaterLevelAnalyzer.java b/fastbee-report/src/main/java/com/fastbee/report/analyzer/water_level/WaterLevelAnalyzer.java index 8ff34a65..d2ded26e 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/analyzer/water_level/WaterLevelAnalyzer.java +++ b/fastbee-report/src/main/java/com/fastbee/report/analyzer/water_level/WaterLevelAnalyzer.java @@ -2,39 +2,14 @@ package com.fastbee.report.analyzer.water_level; import com.fastbee.common.enums.MonitorCategory; import com.fastbee.common.utils.StringUtils; -import com.fastbee.iot.domain.ThingsModel; -import com.fastbee.report.analysis.PrimaryMetricResolver; -import com.fastbee.report.analysis.QualityAnalyzer; -import com.fastbee.report.analysis.RateCalculator; -import com.fastbee.report.analysis.StatisticsCalculator; -import com.fastbee.report.analysis.TrendAnalyzer; -import com.fastbee.report.analysis.WarningAnalyzer; -import com.fastbee.report.analyzer.ReportAnalyzer; -import com.fastbee.report.api.ReportRequest; -import com.fastbee.report.chart.ChartGenerator; -import com.fastbee.report.chart.MetricChartBuilder; -import com.fastbee.report.core.ReportContext; -import com.fastbee.report.data.MetricData; -import com.fastbee.report.model.AnalysisResult; -import com.fastbee.report.model.Conclusion; -import com.fastbee.report.model.MetricPointData; -import com.fastbee.report.model.QualityAnalysis; +import com.fastbee.report.analyzer.AbstractDeviceAnalyzer; 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 org.springframework.stereotype.Component; -import javax.annotation.Resource; import java.math.BigDecimal; -import java.time.Duration; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.stream.Collectors; /** * 水位计专项分析器 @@ -43,202 +18,65 @@ import java.util.stream.Collectors; * @date 2026-08-23 */ @Component -public class WaterLevelAnalyzer implements ReportAnalyzer { - - @Resource - private StatisticsCalculator statisticsCalculator; - - @Resource - private TrendAnalyzer trendAnalyzer; - - @Resource - private RateCalculator rateCalculator; - - @Resource - private QualityAnalyzer qualityAnalyzer; - - @Resource - private WarningAnalyzer warningAnalyzer; - - @Resource - private ChartGenerator chartGenerator; - - @Resource - private PrimaryMetricResolver primaryMetricResolver; - - @Resource - private MetricChartBuilder metricChartBuilder; +public class WaterLevelAnalyzer extends AbstractDeviceAnalyzer { @Override - public boolean supports(MonitorCategory category) { - return category == MonitorCategory.WATER_LEVEL; + protected MonitorCategory supportedCategory() { + return MonitorCategory.WATER_LEVEL; } @Override - public AnalysisResult analyze(ReportContext context, ReportRequest request) { - AnalysisResult result = new AnalysisResult(); - - // 1. 按物模型标识符分组历史数据 - Map> byIdentity = context.getMetricDataList().stream() - .filter(d -> d.getIdentity() != null) - .collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList())); - - // 2. 获取所有监测物模型(即使没有数据也要显示) - Map allModels = context.getThingsModels().stream() - .filter(m -> m.getIdentifier() != null) - .collect(Collectors.toMap(ThingsModel::getIdentifier, m -> m, (o, n) -> n, LinkedHashMap::new)); - - if (allModels.isEmpty()) { - result.getConclusions().add(new Conclusion(1, "设备未配置监测物模型,请联系管理员配置。")); - return result; - } - - // 3. 为每个物模型生成统计数据(包括无数据的物模型) - for (Map.Entry entry : allModels.entrySet()) { - String identity = entry.getKey(); - ThingsModel model = entry.getValue(); - List data = byIdentity.getOrDefault(identity, new ArrayList<>()); - - ReportThreshold threshold = context.getThresholds().get(identity); - BigDecimal controlValue = threshold == null ? null : threshold.getControlValue(); - - Statistics statistics = statisticsCalculator.calculate(data); - // 补充物模型信息(确保即使无数据也有 modelName 和 unit) - if (statistics.getModelName() == null || statistics.getModelName().isEmpty()) { - statistics.setModelName(model.getModelName()); - } - if (statistics.getUnit() == null || statistics.getUnit().isEmpty()) { - statistics.setUnit(model.getUnit()); - } - statistics.setIdentifier(identity); - - TrendAnalysis trend = trendAnalyzer.analyze(data); - QualityAnalysis quality = qualityAnalyzer.analyze(data, context.getTimeWindow(), controlValue); - WarningAnalysis warning = warningAnalyzer.analyze( - identity, statistics.getModelName(), statistics.getCurrent(), threshold); - - result.getStatistics().put(identity, statistics); - result.getTrends().put(identity, trend); - result.getQualities().put(identity, quality); - result.getWarnings().put(identity, warning); - } - - // 统一主指标口径:优先名称/标识符命中关键词的物模型,排除角度/温度/电压等辅助量 - String primaryIdentity = primaryMetricResolver.resolve(context, byIdentity, false, "水位", "液位", "water"); - result.setPrimaryIdentifier(primaryIdentity); - buildConclusions(result, context, primaryIdentity); - buildChart(result, context, primaryIdentity); - return result; + protected String[] primaryMetricKeywords() { + return new String[]{"水位", "液位", "water"}; } - private void buildConclusions(AnalysisResult result, ReportContext context, String primaryIdentity) { - Statistics statistics = result.getStatistics().get(primaryIdentity); - TrendAnalysis trend = result.getTrends().get(primaryIdentity); - QualityAnalysis quality = result.getQualities().get(primaryIdentity); - WarningAnalysis warning = result.getWarnings().get(primaryIdentity); - TimeWindow window = context.getTimeWindow(); - String unit = statistics == null ? "" : (statistics.getUnit() == null ? "" : statistics.getUnit()); - - // 生成项目概况总结 - String summary = buildSummary(statistics, trend, unit, window); - result.setSummary(summary); - - // 生成专业分析建议 - String advice = buildProfessionalAdvice(statistics, trend, warning, unit); - result.setProfessionalAdvice(advice); - - // 设置安全评估 - if (warning != null) { - result.setSafetyLevel(convertWarningToSafetyLevel(warning.getStatus())); - result.setSafetyDescription(warning.getDescription()); + @Override + protected String directionText(String direction) { + if (TrendAnalysis.DIRECTION_UP.equals(direction)) { + return "上升"; } - - // 设置控制值 - ReportThreshold threshold = context.getThresholds().get(primaryIdentity); - if (threshold != null && threshold.getControlValue() != null) { - result.setControlValue(threshold.getControlValue()); - result.setControlValueUnit(threshold.getUnit()); + if (TrendAnalysis.DIRECTION_DOWN.equals(direction)) { + return "下降"; } - - // 组装多测点数据(支持多物模型标识符) - List metricPoints = buildMetricPoints(result, context); - result.setMetricPoints(metricPoints); - - // 生成详细结论 - List conclusions = new ArrayList<>(); - int seq = 1; - if (statistics == null || statistics.getCount() == 0) { - conclusions.add(new Conclusion(seq++, "统计期内无有效监测数据,请检查设备上报状态与时间范围。")); - } else { - String direction = trend == null ? "待分析" : directionText(trend.getDirection()); - conclusions.add(new Conclusion(seq++, String.format( - "统计期内水位由 %s%s 变化至 %s%s,水位变化量 %s%s,水位呈%s趋势。", - statistics.getInitial().toPlainString(), unit, - statistics.getCurrent().toPlainString(), unit, - statistics.getDelta().toPlainString(), unit, - direction))); - - BigDecimal rate = rateCalculator.ratePerDay(statistics, - Duration.between(window.getStartTime(), window.getEndTime()).getSeconds()); - conclusions.add(new Conclusion(seq++, String.format( - "平均水位变化速率 %s%s/天;统计期最高水位 %s%s、最低水位 %s%s、平均水位 %s%s。", - rate.toPlainString(), unit, - statistics.getMax().toPlainString(), unit, - statistics.getMin().toPlainString(), unit, - statistics.getAvg().toPlainString(), unit))); - - // 添加控制值对比结论 - if (result.getControlValue() != null && result.getControlValue().compareTo(BigDecimal.ZERO) != 0) { - BigDecimal ratio = statistics.getDelta().abs().divide(result.getControlValue(), 4, BigDecimal.ROUND_HALF_UP) - .multiply(new BigDecimal("100")); - conclusions.add(new Conclusion(seq++, String.format( - "水位变化占控制值比例:%.2f%%(控制值:%s%s),需关注渗流场变化。", - ratio.doubleValue(), result.getControlValue().toPlainString(), result.getControlValueUnit()))); - } - } - - if (trend != null && StringUtils.isNotEmpty(trend.getDescription())) { - conclusions.add(new Conclusion(seq++, "趋势分析:" + trend.getDescription() + "。")); - } - if (warning != null && StringUtils.isNotEmpty(warning.getDescription())) { - conclusions.add(new Conclusion(seq++, "预警状态:" + statusText(warning.getStatus()) - + "。" + warning.getDescription() + "。")); - } - if (quality != null && StringUtils.isNotEmpty(quality.getDescription())) { - conclusions.add(new Conclusion(seq, "数据质量:" + quality.getDescription() + "。")); - } - result.setConclusions(conclusions); + return "平稳"; } - /** - * 生成项目概况总结 - */ - private String buildSummary(Statistics statistics, TrendAnalysis trend, String unit, TimeWindow window) { + @Override + protected String buildSummary(Statistics statistics, TrendAnalysis trend, String unit, TimeWindow window) { if (statistics == null || statistics.getCount() == 0) { return "统计期内无有效监测数据。"; } String direction = trend == null ? "待分析" : directionText(trend.getDirection()); - BigDecimal rate = rateCalculator.ratePerDay(statistics, - Duration.between(window.getStartTime(), window.getEndTime()).getSeconds()); + String formattedCurrent = valueFormatter.format(statistics.getCurrent(), supportedCategory()); + String formattedDelta = valueFormatter.format(statistics.getDelta(), supportedCategory()); + String formattedMax = valueFormatter.format(statistics.getMax(), supportedCategory()); + String formattedMin = valueFormatter.format(statistics.getMin(), supportedCategory()); + String formattedAvg = valueFormatter.format(statistics.getAvg(), supportedCategory()); + String resolvedUnit = valueFormatter.resolveUnit(unit, supportedCategory()); + + // 使用回归斜率作为速率 + String rateText = "未计算"; + if (trend != null && trend.getSlope() != null) { + String formattedSlope = valueFormatter.format(trend.getSlope(), supportedCategory()); + rateText = formattedSlope + resolvedUnit + "/天"; + } return String.format( - "本期水位监测数据显示:当前水位为%s%s,较初始值变化%s%s,水位变化速率为%s%s/天,水位呈%s趋势。" + - "统计期内最高水位%s%s,最低水位%s%s,平均水位%s%s。渗流监测正常。", - statistics.getCurrent().toPlainString(), unit, - statistics.getDelta().toPlainString(), unit, - rate.toPlainString(), unit, + "本期水位监测数据显示:当前水位为%s%s,较初始值变化%s%s,变化速率为%s,水位呈%s趋势。" + + "统计期内最高水位%s%s,最低水位%s%s,平均水位%s%s。", + formattedCurrent, resolvedUnit, + formattedDelta, resolvedUnit, + rateText, direction, - statistics.getMax().toPlainString(), unit, - statistics.getMin().toPlainString(), unit, - statistics.getAvg().toPlainString(), unit + formattedMax, resolvedUnit, + formattedMin, resolvedUnit, + formattedAvg, resolvedUnit ); } - /** - * 生成专业分析建议 - */ - private String buildProfessionalAdvice(Statistics statistics, TrendAnalysis trend, WarningAnalysis warning, String unit) { + @Override + protected String buildProfessionalAdvice(Statistics statistics, TrendAnalysis trend, WarningAnalysis warning, String unit) { if (statistics == null || statistics.getCount() == 0) { return "建议检查设备上报状态,确保数据采集正常。"; } @@ -246,7 +84,6 @@ public class WaterLevelAnalyzer implements ReportAnalyzer { StringBuilder advice = new StringBuilder(); advice.append("专业分析:"); - // 基于趋势方向给出建议 if (trend != null) { if (TrendAnalysis.DIRECTION_UP.equals(trend.getDirection())) { advice.append("水位呈上升趋势,需关注浸润线位置变化,评估渗流场稳定性。"); @@ -257,12 +94,10 @@ public class WaterLevelAnalyzer implements ReportAnalyzer { } } - // 基于水位变化量级给出建议 if (statistics.getDelta().abs().compareTo(new BigDecimal("1")) > 0) { advice.append("水位变化幅度较大,建议分析水位变化原因(降雨、抽排水等),评估对边坡稳定性的影响。"); } - // 基于预警状态给出建议 if (warning != null) { if (WarningAnalysis.STATUS_ALARM.equals(warning.getStatus())) { advice.append("当前处于报警状态,建议立即启动排水措施,组织专业人员进行渗流安全评估。"); @@ -273,88 +108,4 @@ public class WaterLevelAnalyzer implements ReportAnalyzer { return advice.toString(); } - - /** - * 将预警状态转换为安全等级 - */ - private String convertWarningToSafetyLevel(String warningStatus) { - if (WarningAnalysis.STATUS_ALARM.equals(warningStatus)) { - return "DANGER"; - } else if (WarningAnalysis.STATUS_WARNING.equals(warningStatus)) { - return "WARNING"; - } - return "SAFE"; - } - - /** - * 组装多测点数据(支持多物模型标识符) - */ - private List buildMetricPoints(AnalysisResult result, ReportContext context) { - List points = new ArrayList<>(); - - for (Map.Entry entry : result.getStatistics().entrySet()) { - String identity = entry.getKey(); - Statistics stats = entry.getValue(); - - MetricPointData point = MetricPointData.builder() - .identifier(identity) - .modelName(stats.getModelName()) - .initialValue(stats.getInitial()) - .initialUnit(stats.getUnit()) - .currentValue(stats.getCurrent()) - .currentUnit(stats.getUnit()) - .changeValue(stats.getDelta()) - .changeUnit(stats.getUnit()) - .maxValue(stats.getMax()) - .minValue(stats.getMin()) - .avgValue(stats.getAvg()) - .build(); - - // 设置预警状态 - WarningAnalysis warning = result.getWarnings().get(identity); - if (warning != null) { - point.setWarningStatus(warning.getStatus()); - } - - // 计算变化速率 - TimeWindow window = context.getTimeWindow(); - BigDecimal rate = rateCalculator.ratePerDay(stats, - Duration.between(window.getStartTime(), window.getEndTime()).getSeconds()); - point.setChangeRate(rate); - point.setRateUnit(stats.getUnit() + "/天"); - - points.add(point); - } - - return points; - } - - private void buildChart(AnalysisResult result, ReportContext context, String primaryIdentity) { - // 主指标单图 + 其余指标按单位分组多曲线图(展示所有物模型数据) - Map> byIdentity = context.getMetricDataList().stream() - .filter(d -> d.getIdentity() != null) - .collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList())); - Map charts = metricChartBuilder.build(result.getStatistics(), primaryIdentity, byIdentity); - result.getCharts().putAll(metricChartBuilder.fillPlaceholders(charts)); - } - - private String statusText(String status) { - if (WarningAnalysis.STATUS_ALARM.equals(status)) { - return "报警"; - } - if (WarningAnalysis.STATUS_WARNING.equals(status)) { - return "预警"; - } - return "正常"; - } - - private String directionText(String direction) { - if (TrendAnalysis.DIRECTION_UP.equals(direction)) { - return "上升"; - } - if (TrendAnalysis.DIRECTION_DOWN.equals(direction)) { - return "下降"; - } - return "平稳"; - } } \ No newline at end of file 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/ReportThresholdQuery.java new file mode 100644 index 00000000..656f0ee2 --- /dev/null +++ b/fastbee-report/src/main/java/com/fastbee/report/domain/vo/ReportThresholdQuery.java @@ -0,0 +1,28 @@ +package com.fastbee.report.domain.vo; + +import lombok.Data; + +/** + * 报告阈值查询 VO + * + * @author FastBee + * @date 2026-09-10 + */ +@Data +public class ReportThresholdQuery { + + /** 产品ID */ + private Long productId; + + /** 设备ID */ + private Long deviceId; + + /** 监测类别编码 */ + private String monitorCategoryCode; + + /** 物模型标识符 */ + private String metricIdentifier; + + /** 单位 */ + private String unit; +} \ No newline at end of file diff --git a/fastbee-report/src/main/java/com/fastbee/report/model/WarningAnalysis.java b/fastbee-report/src/main/java/com/fastbee/report/model/WarningAnalysis.java index cf8b2147..e52a6b31 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/model/WarningAnalysis.java +++ b/fastbee-report/src/main/java/com/fastbee/report/model/WarningAnalysis.java @@ -19,6 +19,8 @@ public class WarningAnalysis { public static final String STATUS_NORMAL = "NORMAL"; public static final String STATUS_WARNING = "WARNING"; public static final String STATUS_ALARM = "ALARM"; + /** 未配置阈值,无法评估 */ + public static final String STATUS_UNKNOWN = "UNKNOWN"; @ApiModelProperty("物模型标识符") private String identifier; 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 d7469d28..86ba8d02 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 @@ -293,6 +293,9 @@ public class WordRenderer implements ReportRenderer { if (WarningAnalysis.STATUS_WARNING.equals(status)) { return "预警"; } + if (WarningAnalysis.STATUS_UNKNOWN.equals(status)) { + 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/IReportThresholdService.java new file mode 100644 index 00000000..b2006600 --- /dev/null +++ b/fastbee-report/src/main/java/com/fastbee/report/service/IReportThresholdService.java @@ -0,0 +1,56 @@ +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 java.util.List; + +/** + * 报告阈值 Service 接口 + * + * @author FastBee + * @date 2026-09-10 + */ +public interface IReportThresholdService extends IService { + + /** + * 查询阈值列表(分页) + * + * @param query 查询条件 + * @return 阈值列表 + */ + List selectThresholdList(ReportThresholdQuery query); + + /** + * 根据ID查询阈值 + * + * @param id 阈值ID + * @return 阈值对象 + */ + ReportThreshold selectThresholdById(Long id); + + /** + * 新增阈值 + * + * @param threshold 阈值对象 + * @return 影响行数 + */ + int insertThreshold(ReportThreshold threshold); + + /** + * 修改阈值 + * + * @param threshold 阈值对象 + * @return 影响行数 + */ + int updateThreshold(ReportThreshold threshold); + + /** + * 批量删除阈值 + * + * @param ids 需要删除的阈值ID数组 + * @return 影响行数 + */ + int deleteThresholdByIds(Long[] ids); +} \ No newline at end of file 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/ReportThresholdServiceImpl.java new file mode 100644 index 00000000..d4ddf738 --- /dev/null +++ b/fastbee-report/src/main/java/com/fastbee/report/service/impl/ReportThresholdServiceImpl.java @@ -0,0 +1,66 @@ +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 org.springframework.stereotype.Service; + +import java.util.Arrays; +import java.util.List; + +/** + * 报告阈值 Service 实现 + * + * @author FastBee + * @date 2026-09-10 + */ +@Service +public class ReportThresholdServiceImpl extends ServiceImpl + implements IReportThresholdService { + + @Override + public List selectThresholdList(ReportThresholdQuery query) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (query.getProductId() != null) { + wrapper.eq(ReportThreshold::getProductId, query.getProductId()); + } + if (query.getDeviceId() != null) { + wrapper.eq(ReportThreshold::getDeviceId, query.getDeviceId()); + } + if (StringUtils.isNotEmpty(query.getMonitorCategoryCode())) { + wrapper.eq(ReportThreshold::getMonitorCategoryCode, query.getMonitorCategoryCode()); + } + if (StringUtils.isNotEmpty(query.getMetricIdentifier())) { + wrapper.like(ReportThreshold::getMetricIdentifier, query.getMetricIdentifier()); + } + if (StringUtils.isNotEmpty(query.getUnit())) { + wrapper.eq(ReportThreshold::getUnit, query.getUnit()); + } + wrapper.orderByDesc(ReportThreshold::getCreateTime); + return baseMapper.selectList(wrapper); + } + + @Override + public ReportThreshold selectThresholdById(Long id) { + return baseMapper.selectById(id); + } + + @Override + public int insertThreshold(ReportThreshold threshold) { + return baseMapper.insert(threshold); + } + + @Override + public int updateThreshold(ReportThreshold threshold) { + return baseMapper.updateById(threshold); + } + + @Override + public int deleteThresholdByIds(Long[] ids) { + return baseMapper.deleteBatchIds(Arrays.asList(ids)); + } +} \ No newline at end of file diff --git a/fastbee-report/src/main/resources/templates/DEVICE_AXIAL_FORCE_V2.docx b/fastbee-report/src/main/resources/templates/DEVICE_AXIAL_FORCE_V2.docx index d88772bb849dae6ce588e29b77324633c8d904e6..c11e5d75650d1658837f636bed981950cbcce2d5 100644 GIT binary patch delta 2551 zcmZXWdpHyNAIG<0Kq{vwL zeRU(F;k}n6R@^9kw?^k*P%c3*~*1O5Ka=W}99X^?p}--NF@+-xu!m_7rDXv_~xzzqeT-X!I=Bn%+lI9OujfXl0J=DmrlirQ!_u{A=D^zQv~`^cLo zsd{&tdQLG5K07?Js`5IF4lT^+@_au~Nv;>0S=NR9qwaU@`s#n;K+|W-G$?c37^H^z zi%+%yOoLKGYL&gi0vSFfEbB!d-+np>}Z;Ws=QS3#US_M z{?FKAkmvG38|%vo!TReCpgA~g4c*R<62+yI+NjGSYo{d?zlbk7qBkP1fYnutkypVw zj!NpeEd&Bx^$KTTjC03o>o^2VestnB6e!1({VbiiyF;WkPtjr~4`|_#I${If!z7SH zK<9QQk9(Ro9+b3ZOLUQKX~f~^P0P=>|CGFbp%e0&C~h3MfmM!2xnHI=t|;og-HiylxIL9CVX7E{216oSIV%FN|Yr`{!!BSI$c zA6fB}@Dl31^)yl2Z&OJhrj7We4DfoBN@7NDd6^ zRSWH}dFXR?!3>$UW*O63{4~2R>PAH^0TZiGI8g~wdC3k==e{vGzcJZHgl*l+b?iS6 zA1oQ=)P@gFH0@gnY0M`pNP_p~g{@-B%6+42|A zP^euj6YTtg-OPPso%BWmc!x zCnV&)J^Pl$igL+vz7egUnnA&Du~KD4XHn8 z@0u9Do)RCEvkAoN1f`pT2%74gr73?5V=^S*koJn{8GSL`Wqu6GAF*!dX4NaF?>#-c zb0<2fN$vDU-3w7A!!L}rT&1f&XVM>T&i7P)jimEu!?4u~*#*I~pPfVXOD~dl@XDjD zBWtG-S)VT#S$4}Ozzi54vi#DYxC)I^104JdCtodGohR<3aw zhHgLP96M{Te%l9eoZuJfJLOOJEzP1bjO=N{YV@@<9255Y3H`F#Fkx2j)`^>!wLW}# zGCxz)6WRFkR2S-3OYB_z$CTdOR{9m0JJubAcDDk zhjp@N*^BJAgMFiuIg20HLzmi0&n(We9*z%l43OQHI~58MvT=5EH3MG6x<7bbX+cM;!)fyc$@1qL|cUYH`nAW zVUpAWGk}F0CnS>4+z-(on;L{ED3vXl1(j89bJ|+pZ^&Z~B6E>AqFiZ>b5@s>{ww94 z85yDebK{9JWu=3~o~N8U>y>*D>>@nOe8FevzLRG(yu>xU*f@c=%~1?)o;KTsmo+VL zPX0hqIs9mGa~-&n8Cr7A^J%p$nqi~bYgjGUkYkGRp#2E9fpU!^EEeQqmHw&@eRtRM z6`gTSp7E-ED*m9}WR$Xd@>TZz@sty3wP)T#))R$-Tc#(ny{KV1ySCWvnE&4Ww6cRy z`^#QJdeB%90>)7><^sCKbA$x6NHE#&MJ;t zMf`x+sb~FhkKr`vM0P`ek0bt}4{~IeVk&i+IN#i0>RmKzu01OBN0BU>je?xUy+C^ZDy9SD*W`j%df>e|WWUqAq*AF`P{EuhnX{vvdp*59jmCGHnrqZ|*P#D5 w4ZXwT)byS(bkCslouQlx&C?T1=iY@rIu8zZAPMCE0)Q=nh5!Hn delta 2508 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 delta 2508 zcmZ9Oc{J4PAIHDLjHP7DHkm=PPmz7>q+BDrWM89*EQKt?a4)itnJ=PCa#JxBSt4S% z#x6J8SjJFfnS{!vEb%kWuXBFq_c`Y|uk-$Vp3n1np69Q3b2-acISZHdWmc#F000~S z-u@*Q1;T>2uN{VnA8~^v?}+J8VHR$@y@FeS1$wog9RRE@fz@onxK&sBZJLHq>$;s^ z*nEW}nSiufr3O^d27PoMHW~;QwZxm6UNGiA3t%Gj3J4=IS@USiF8WhDS&LfQbJdYl zBHhBZJM_W5RMTFE*d!PSk?3YmI9D!B-?<=VIQL#>RtpW~HX`qLy?JrTEZaIU^J@tu z1S`jK0#?z}gXh7u9Lx-q50!BEwpk}6pFr!^0gQK|C#zcA#q3_%j+V4by8dp|-S<>Q z?A-b5S)JZjy4CLD=dJbvTbMR0_V-*P257Y z*DN1!3#_Zb@%V2lDp)xAVxooLmvk-ju(n9T$Ef4CTm2s5=2R@&tcCnj&T%-wJ|CGypxKS+V40sK)nQd1*^h4>`*gy@FoiD&mp$F}>rA3x2V z#s*KfUJAht1)3SRU}Cz_>-}>H@8xP-Me9i>F|4W%fQ-0dlggImOdHfkJu5|3j86<+ z^nOS_eGC?=R#faH$vp}^7L1U`Cm{a7PD_t`^)q+fcKnbl{V4M{L464czItVH(O&33 zH>%sqc#Q-p@^oB8?H2Y-ALMGM3$i6DEsaUglsc15*_Gyn zsq~nw;SPWxuu&yh|&r>}gN?eqvPFZ{J&zn1sE`xnN$ZrG$~1Cx76Q(u#KMpR!!=_4PlP z8QHl$w!i;Z_;xSK{z7u!!UAu}=d=799sk;diB!M!vstQmlLUGe&;zDbu7-Cu;ljvY zzidp+JZ|H1;3VyPK51ii;DX=C%yPDBAK*?mJW|xhV(N(nrLGiqOAKyT9RK5A z#`swJ_EX(tqunYPE?IZB`YKi1Tc3nsgWL(_6ESpol)`PtCh`}Te3+v=|KBuhisJ`| zrGy0O{XS$sfO;wA%%FycXfiQ!H==VV2vtg1@zk#?HE`xZF?u+p8Pt&jr@MK^X4-NK z#+c|^OGc~Pr;6DV31LVQ2QG1Ra-3ZABK9@;jhM-deVH<^+0+VN{F_sYltIRT>Oi~( z(s?JU@Z~t^^ky_nI5TKjh`m6T!;XzIHH~$0swu9dD(UB5zxmj28(#>)NJ#}GRAuGj z@26u*e;aN$w?n&61`L=|M#>FI;|=xUA?lwUreqpVZ3XBHhn+y(#jT9TSf;RD?QQjS zVR`Q5VJn(27IIKrtSN@|AUEA|$lKA@+4NF6*U{(Q|-^`o%=a_ zA#lAPW1d2);*9*N@lI+H`Fn5=547yhr*}2_pva0{MOlR7@wT20UL!qr$M=L!p85hA zpL#f8gDMiq6OI1>DBLi&C;f-Qc;mh1W6{JXuXa28`)sepj5Y0C2%-f)UN*{QX$c;* z0+q$M#=JH!u5#8W>hZ#rWreAcSn|&)#f+|>%b-;d)ZEK(zW?vq-!akD#X=?=$|V_yY}T@xNKctO-@R{#D-`^!#9AW!V3t z`AwDZsru8Jk5dfoFF>~)1ZGU_KcN!L@$GH%QjjOI9K zb&G1%mxsv^LiKw!H|`WSy-udUzpkrCnlCLC4CU;;qTvP(PNc+%Xk4OR(O2W>6Lzf4 z@67OOaT2u$KV!3c2lA?Yf*he{l{I4rea0`kTF7fGpQ8*nUzUo)cK>jRWyNxspqiY*$xTP{-82ZoA{lXtxOU;+SQR< zaMb0V_6^1?+h!Xu0cLA5F_5jt#BqY||2q+Tf-;1dE$oat0-vK~*DC;`9= zdlC*l&t{JO58J7e#Q*>R diff --git a/fastbee-report/src/main/resources/templates/DEVICE_DEEP_DISPLACEMENT_V2.docx b/fastbee-report/src/main/resources/templates/DEVICE_DEEP_DISPLACEMENT_V2.docx index cdda2a9c5080f45841e37f18ad541f97f02b0f1f..4f154a59b8b8b4421ac86fbddcc5dd86b635b201 100644 GIT binary patch delta 2598 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 IO<*tbUn`cIs{jB1 diff --git a/fastbee-report/src/main/resources/templates/DEVICE_INCLINOMETER_V2.docx b/fastbee-report/src/main/resources/templates/DEVICE_INCLINOMETER_V2.docx index 1cc11af5f52c82af86cb36699f79d8fac8bdb07f..75162741cb134464550caf33043d3cf069ac8c9a 100644 GIT binary patch delta 2529 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 diff --git a/fastbee-report/src/main/resources/templates/DEVICE_RANGING_METER_V2.docx b/fastbee-report/src/main/resources/templates/DEVICE_RANGING_METER_V2.docx index 8b38756890130991486992cd67432134e27dee9a..939c0ba7ea27c48c434e532d0304ed759d488350 100644 GIT binary patch delta 2469 zcmZ9OX*3jUAI4|KAdx+Jkg=7Jmnp_$8(F4A*_SL?k`xj(G$CW(LS}}k#!X!)f zv1J+C(2ynTjGahEC0^s4&Uv5r{_;D&|Npwr{qeqri$HZnATBdw7O)@y05}Ffg%i2j zfFM+O1qBE>;x`cfh|j>MK-{Qs`J1c5fgwb803ci!SI%|{O>=ax6ulx8GUc32;WWK$ z3w{H#6T*8Jrwh72d}2t;Rvp4>WW35iR?7%n;Y>tN%L*bFODJ1H!}ic4vX3qFie@Um0AP@X{YyRH=D4=hTTvpRro5kkqIr`KvIyRG4}d2d^; zcjJ2vYBh6B`ON4qqMJ>o3GXnaSLfHxoSi*=!yxvn0}?djk|~a*lWgJQs=f2{y1AXx z;m^=_^hK6U#_BrMorRo_B}w&Enpb$f*Ho@xb=l&`Q^?(*pSX6(W-j*cIXSx5rbS#69$8N>9MC$TbUApucjA$-HKdSB4Xxip1YqjcFBd>)fj`P^AJomBA z+_M<3@7s9W^I{cJ`dQ(isbM#{e<$ik)OIOi(gx!bTTGWp3}xCD6$7HXzUk4n=|fPT0_0d zZ$`}W7knjg6qUHa=V|V`q1gg*JN5yM9k<1=q7_*?dEs6fx508i>E6a#_nTjcLK$rF z&?v^`@*Tc``1F!aQasOz;ef$l5?`0zy0zX?oXQ^7;KII$cJh3ut0JL}vq|av`qUH0 z@sd#;^u3#byGE(W*$f<{RKHAOjD^#J}D|tyv9l4t_UkrYG?Rz;kDZvcuqrLyBwo=f7qALe6q7dpB z#r@P82=8Tzf1wmF1x;$)|Hq#;{yYD>@>Z!{Vwf4l=ip~<YR?EX`Z5+MOx)1#)Op$O(pJ45;L|#I+5FLCqMBW_=Cpm!VoSzvKRKO2 zD9@|5r8v`|>WppH!q;;Cee&Sbgx4lK>{O8Qma>6aKZwebx8FE$D$o5Amml@Tm3xRW zHt$0HisdgOT4)u*Mv1*bKke!iKHKhY(%3FZbq;5H+RAho3R;=f=DVWFhx|=4-n!BZ~&mSS#R8#1tC!rrz>?9ix;4&wliAvK`XjZw z5F%w_v}p2XrP_h*c+kF4ss=e^KUEVDx%vICC0`+aGEn6wV@la44r%RxPGOCJJqL?B&0) z74-c=KJ>laDe6P&R<{dUq5jPE(Jz=8DsrKppb1z1=jMC)`xd(&4M{aST3n;fYJ%S9 z?9EF=(b|TEPqtwUsx_z!lK3LM=2y zzs1U2dnM~IQIf321T9&c2^oym|JMp143xRa#)tt?xJHZuQ&tXT9(N2Y&XjXlG2lBK z0xQQ9-$RkcC1NF*Qg$eOI5Jk6DVv9)kK@KkG6jkg15$A&I2j1D#AJkOj1(L;9yTBV zAaKY(CluI^!{DSOm>GCC?Nh_yLiR8t{wHwAP+&nMZX!~G+sqgU5@h*PoN&MtF<=%> zJVo~Ch2(*lI4>3eAnqgp!2hq?)wtU!l1G`GPO3)(D*zBF0RWu(SH=RDmm<$*kjxzZ E7etG z5QQJ|8a~T&6%^Uq%ma8oJk?M}pU6W9D~wh{dlOGeoeJ|ed#4O~>FK${m8zrC?y$q(~;O&{+2JAQ?lP3*Qe?0ii2`EjlBE0Yk(|@I)iw!3QQM; zjy*|x$klM#;nMNt4>+A`dRdZ>Va+jB14ggp@gW(RNg;0h(p>>#+;NP^Fk}1Nst8pd z`~KlwGTnhy2pE2@`Zl{R1V?I9l;Ge&4%SL;X;kfv%F6WH5{x;?F=2Cu3{(0}wNI^dHOD6mqt3Xa=?Ffo7w0b`Mwq~%V>+zlL zcjSWX`rm$41~@M+P4*ny39+S{>Do^zB_CwAIKaMIuf>g(RetH6uK3ga&gzW&)#ol< zw2o3;#*97il`;{qJHMhP%p31pe!cpn5T~lLTQ^)M#Q)hdXEwvoSBrtO%GY3LWaWf& zlQ!D-A2k4L4{YhHl<+A@R{Se1RVgT<3|xaG{t8bdRZ?pLX1Qd1aTjWBlB5?T~J%(hh|FLV64F8EDR zcEoSTv}b=kq@H@t*Q4~l7+_Rl7~i_YaSSKzDGM~9P?R|odmo>UFWQs!3=}!sR_o<7 z`%(eJ(v#U0j>I1D{b_jJ#uMmahpz~$x;#$ulzwV`MdP^lh?zkD_owPCCJ!av*1oj7 zZ(%pn%xG-0s|WUHUu|rpD@f~q+6qcCzbBf!=eBfez_dn_yo5?utvjCsk>jQ7($Ab27&1jIBY*O|x5_AGu;> zx?%3`i<_wVvMVdfW?A|CL3ec`7L?VfW4yizmzfvOc zj`=_$;p@x@b%u0NhnEAnjJD$E07^+!fQQr1uz>s8xR!Er4=G~Zl;rFpDa<5FIO~s0 zs2{rS>$bW_m9_rLH!dYn>gYKh6%5~VL{fdzUVg2ouHy^fC-!ma-}#@~gW$6Gv7VQA_Agg5CCbIUl^(d%VPer*{Wr$MBCySPXC{bdDyX7Znk-<@q zaht9sJmtMUn6$e;EBlf(nySgMn)IoAZmN^ThEi>Anm0d}(SKO3nb5WN0&;79B{w&p zQjeK=y`&i{7LscaQo|_ok}CX`uoK!;dH1KT%(&xR<(yr;8UR$T>i|WjPHnxRAf|+4 zaZ_YDkVk-ZnOH*mi==<=A8`T;AsY?9RH@z8@8?K)UqX*KE6JDysYdk-hm27z%d39{ zFFY_CVy6`<=m=`SpVv-i~mj5*c3QqpfVgf2q2xO2I0uev73LybOo+y_fFVYhp z^CN$-!8?Rv`i3^$fQw9`?yj*gdFuZ8qdF!G4`b?uh+X(0K}zf-6QsCZX0{?;@Mu?e zLe^22bIKMQvwR~>mkBUUjftT&Z6=Q4wf^6USmTwMl_b146b}~R&q^|#ip9f%**p{k zQfbWw;XS!_? z*4_WUI~Z&y2px@}*?+Qx9Dd8o!^Lz^|3)-a0VENS%n{QBai}BsjUdew!k{>G9lQw2 zGKIsTpn;*F3{&JBiVm;|JjWC?P#o4254OaMf>J~TGowTlhmL|~#G_OEiQ+Kd6wp6K O1S}%*!JX2WWB&vAT$pSC diff --git a/fastbee-report/src/main/resources/templates/DEVICE_STATIC_LEVEL_V2.docx b/fastbee-report/src/main/resources/templates/DEVICE_STATIC_LEVEL_V2.docx index b002e934e0600a0ba63e3f82022142e12f5a9f78..45e1c9dc92dfe275dc3882872151d8b2852b3652 100644 GIT binary patch delta 2600 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 delta 2514 zcmZ9Odpr~BAIEno9Eq9hhAf2>#pnn%_gh7!B=>7HLN4RDZ*z+@%r=y{W=G~SLnww; z;S&^ASa<~=uP=_gNI1X3( zgsaaI1J8)lmPWbh((k#xN7un=Xq`io?uChf;bk*GfkPFhvTyhZ`E{ z1e<+?yH^|J9YyVhBb{wT+0yfq%;Yus=W|5RRe!rNXWf#FQ)o4|D2)=Wzn1_lyXaJu z-A=9qKRb1u7B@#vlxC9i6bQs~(wtJScJdSg2b$V83cY-LNcE6*U(XnQK8 zByoJFovqHq?fgXar_axCK8#h*9{s za^0u=AZ_JI+Z?cEVXFTd*ZR;;ma=k=&a-c^lYo<~h`IZ6(R2PVx59!A5p&b{)++SD z@Ec7|8l0}qI;CJYs_oF!x2xvIG?qPVoN?Y_2M;|;y!whi6@eFf+f->`O@tyxm#I^n zHKUq;Q1*l)EdAtW3H+6JepcC6SfORvW~*pS?$LKrw!f+$8ls<3f$hksAM6|9x^j4$ z^%}P65+$70YTkOO>qH6bimG3gyQjEhzm|%Qm0hW2I&g|08`U}NX0R{c@FDxyRh*+! zZHa z$wCH>zpqTFoDyqPix@@JuKH9HV{I=vgHMvDp82xmKNoiOyqwITR z00cs>@45Zb0r%(N!{|IUJ&OKoMsG*9>o#Nak9giULT!_Fcj01(XP@0A29689PuqSb zUTVDE7^gEsxsMFkY5w~KQI5*#kFyz6*Xu)gYiWVN0sEv!eEImiBTF%&;MGS#Uy1D>`6C_wsWFF4QaHL zOU4x11W$R~vvxJsRQ?QZqCC^|ba5hAbE&5weL%tIoktoRTu$rg+`RK*QFX(|uf#{_ z!NTz93ofC1d2o8r+(@)y=tK%;?1XiBN%*PGDh?eG!vfmb+ zHzFV1v5Y&cH27N@H?O0IV2LUgrNt$>xd}Tx#r_eJT;tRA9%F`^U`BLdo90skyz`db zT2zPOJk@4N=lv2pd>8J>`r5crd*j%|qlWbaRQ6=4UcU#lN&ZWp>-}4^eIhqjw0Jle zuwW#`YyUN!RkYITTH9pX>cyR#Tc-H7z&Lb;-Ec$Or-&y>%>V*QOmfT?zxM!L{JjOc zYWGk6J_`2D=)i4?F;!LooUHnYtaw&UnYzn$;nJ?^qk23}?RY`N6js41r&{_$A_iMX z_~Kn9UW2}Vff%)x`)7vXBWBOXr@WwcBz%dh0x4}M*1?SPb~Jdj-%GYypHJeRy?G;E zWEwKNTFlOBKYy=%5<9L78I7}uZ*iY!`+h+(@Z<8hGLG@eZHJ-EN~k;Q(b*ew`$Cln z_X}(xOdPsrfl~D$xk>8xzZFSf67?kb1jDGKH^hBvrgjB)NO_ynlw#ey?Up}`ax0F;v5MB_))Ht$r@pw~AGU9M zjkJ5H2coWLzy}+609X-P-6hGPis|xX4w^Y;iz?|xSL(`LI)1VKdVWTKj9ST=bFq0C zR;~l}EE)=%uSqy=vVMyiblM%V;V82ck4oQXoO!=vH0bmJwakn zThJTWCZT!)>P9Vt1w_OmfB2W;azO$;m#{9hOY(FIM}U^pb-`|Zn#_+=JvZq{08TAPo=i*yNXndH?{XUI+mHcXF3_;0qJ- zh>0TEjwB{{(v_qLG$luq5CUlxATf z$y^JGMQO%y?V@qa^YlEw^ZWkudS0K;`}=(V{Juw@ax^{V;ITFb9TElr0Nen4OchTD zkOLo6Ljy`4aDyY`fcFlGaq!|}tuO6NpM+)=vYM_%GRa+ffH~oi z>fN-fD5d{ZmXP0M2PhKGDmhOBWA*qa!Q0CrErHuNlSLy}S@#-6Wep4k)c%MQe7%;> zts-154cAj7Z1!~3cy`7~rvr_^$JIV|yyYk|TD96;p{~~_k_&i`ip~l0XgR5o6TG4o z1J8*TngPE?@iI_S59XmKFqIOo8=!w(E5Dy`+`LmL;PW8DE;@aEr6l?yhsxK)k$@0d zP#(Un2OoYvGj*=-yS%io;cdNsmjY1dJ;aa$@rdyTfhW_OBd!`{nXb|Lle6nO0kOxFe}s+uWj~sq!xWT-_JW_BmZ0M8%-3uDEK&r^PV( zHYkR0&*ReZuNU6OcY>WoA9E+e`)V&f^Le4JumQfd4lFuNms~t*p^u^8)u0Q^r4FNY zwcV>4Vb4o8lubMCWa-Z(!50FEZp4%(td>Ml0+MhE?+)=2x_F0=8+HDAzdw>Ny<_jI zAa~vx9AL(mJ6lejPFzoHPdDJZtgru0*tnHI32f}udbN3k^4g}8+0Tr+aAnM4d@;vl z!60I%gcUT?cjrGU<7#k$W-K3QEh5f!^^Hy_BV1P0-kXq2<7yRpy&MWfqwd39$xF7}Geq-koG&Z;HT|cksZm?K&9BBEM9Q~vsP3MW!291TY0fq zw5!D&RiHfL@C#|=>6`WQLZIMm6+(?bk73KR*qcb&bS5Hgt*#z~PChwS{qogjA8ozu zw&xZzkwwoOCztx$K*Z%TGmT=a15wL-BVEHTt!jnFIi{gUO52piaMMZ{_c21_ugbOW z&)(mgY2~qnl3)xw(#T1zIn(fqs2S8EYXA=hGR>_E$S`B z*>t3W*Iw;MTz8*5F1QfBNYBFZD~O3j17Cm>zbLpzN%ay9$lyMVy-Wc+RZZ zuG(ueq+}!lUWWP}Gn;2NnUC{nOUXEKi5Z#yT=B3&o0^I0-a7lg>TJRHTe!^d#v(0XfJB!!cmG3t{vvz}}znIdC@UPI}k!A8I=MBVW?odLQ zNoJ@9+Z)4Q;`{0edgV-}b4CWUl(BX8FLIEqMsoxj zzIT~EoU&D`6XxAO(U$S|(@C71HR{%TFyzvaxnemLM-RP-3#kgRk7L|mx^G?^YV(!d zD{zxgN*EC$FlWP+`1AZqMm3-D2bKG2=|Js{w06JdH_+j;lO+CRwCfWyfQlXV&Jx8U z-dvUWyaPUC4uo?mxaA559OV;w_GP}kSMvMsZL0L!wkI)GV~QM%b+~}3wm=ZQ2e)qF zqB8#37^WMh>AqOT+;fDwF`Ci7Gw-NxK>57c`Z+smIc+&1Ih%G>td+lEMO$&(K@HD? ze>l6AG*E2`^?v?)rA`rBCgjHc$lY9j05^R@nKpNhfZ(QiUmON|b}hFyAh;0f!H0dX zhW(u~RsZtU<*7Z)pRDl>pF*0h`iU&nHwX=+F0DO0%JS)a&<|M!a&6Uew&b*rP1$vF zX#AzmAT3P|wfP2z%A1y$;F0&g8YbR)Z~DqT2sK)wkCXc_#YTts4#^&y$TbD-Y{bPJ zH9nmfN+{%msHS`v|8UC;_Tz&F5tZ3K6vo^6NFvzv?(zPrZGd=lcd}6Qr6-k|?F)j5 zqZ{G62}72igC!n?PGe93@|wjtQr5QI=TLa58hH?*G-%uG5xINh?8FUBcWu^U@>Ud3XV?(!DYsd{d@cPPzmU{A+oo$$YZe@zar&%tL8 z;qO1oE&&sI{;F)Ef|o&Ts)Z^X?7eQKaac-GJ@@~$!Y@^ky~$670`a7l)YEKX?8_$-cbWuSPNhMC z)gzZj{zU>3-wk z1Ofm;`}}>PfJ3BIk~EB+(d}IrqwX(|`x%Mffqg~+OJhmXu`ph1b0CK>=y!1<0n?$t zJd#8@^56yH+xP?=2mnYB0RRO5bGwd&O_w^z6m*Y!Qo#T~EDQh;`%mUCQc=1b=XoM~ F_%FIVf^Yx; delta 2508 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 Nz(a-@AC}4)`!@y2bU^?B diff --git a/fastbee-report/src/main/resources/templates/DEVICE_THERMOMETER_V2.docx b/fastbee-report/src/main/resources/templates/DEVICE_THERMOMETER_V2.docx index c9f5f8ad522db441507582a61d36e4f2391422bc..5871ec50b4e889f77fbd0f97f695cc1cecb239aa 100644 GIT binary patch delta 2526 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 oyFVcP+W?g!pRCGxjb$!6o_I=xuP z*D04EDz2^UtyL*p%)3yYA)KHxKy_qW(kkecW9#h6DX02e5I_ysCWu(6tOc`+|8lXU zMqheI$&Le$$|H1jMN3Gu5yk}ILXwBxyh75No{G`a2?ZK?n6hSDAx(;fq_>UP^-eLT zJc4Md*2fW;i1DdfUvtIN zkyV{4l+>i&vhvdCoXs|BY~>uLDcD_LM=QFCb)o7#PYKk+1V@FweX%f#?8vcgPktN! zE{f6ExHtReO}h~`aM0_5aLr>H{f4K~U`JxQUq;bW}2#AWCV_sVY!#o<>VLU`R^l0SkQ`bP2d9io8XwL$n zMI3moq#et;ZGl02LG^1oo{wi(=XRT}K)0E3Rbsf(P~Lq-(grMyodh;&9uLChRX(Y}37hsc@FZHD6weC^{ zhmRPCj$X-CtRqo%qvb2Ch>mN-dqIn;s-|ie-`d@%-CL&~^)~{Be(^icnk(NPDwy0q zEh(d$?vL87v^GvMtn8@IiwTwTYh-&`YHGjE;r(z=-L!J~K8%qA^R`iP5f_#a{X$awkKGur^0-9>x9~jch_W)H ziAks+7w+8!lkg3}i2X?UUx2?=XGq`fE@L# z-|yaSDw*&>dLaFQoW|4Y#`Kz;Oaux3+})F%een0Qk4h-OPfdK5u?2_{lfWz0;E1|fL{>ug`E?aS@B^x)ricoXZmF zlL+#c;5QGN^2cIC0>tWq(b<7hFFn|dy06v7?p4+gO?focJflo*8=k%E@W;iq{H=FB z1P*hgvyo#=LUkJ7CCk3KpQ-hoZ2q2Yqj3?h@EcC0pZAH>ch%& z&S53v8HuUpB}H0Vp^KWi++&j`%Du?PX+MOszI+LuWzbqNM{AZVb5hW+8%CXmXfq4( z%r{mkr-`yw{+BHH3Z$tl^F%OdYM8Sw>}c^$@a+Ed_1LwWdkalPBY7hOA#P?{wpyG6 zsT%59GR5yIcbz}8=JZUshUv0|=u|ak^c*I`5_kfn)^`Kmd`QT)d4k9v{gQd4Uf4NM z+6k&C`WCi?8M3IFnEQTXc|k>OtSAL|5N1wKek5WsC4TYIa7j~ZQ=4FTn;x(4A2uJ$ zVl_H%oSiX010=W%Wm;b<#o|7<^=J>5f_R&=Kaq5UyE?8MHJzBqLhlR^haf% zewO8wZ|yHAF;7Nox3A~s2U-;f{P4Wj6&Y6~VgFZ13#R|@0d^q;?&M=PXMV339p8v| z>k}Wn>HE;H${Bl6Br}Iv>eL7Co51hMN8oF|i&Up_q#tB$yAD0RPL&505c?l=ZQ)ed zHnQ91*GW9{&Wy}>*Cp6P+at06cLflSV62qjc_0AL%L)L1kG%paB-}>>7T%E0>v-Z1 zR{8ogn7-keNr(=UYRF|4CZ(b^PxkHZW`UUTztKDF$0U#68)br;xWmj=CW)L>(UWse zy4>-bY|L#3q6rgOL_H>Yh(=6sW?lHF6LHGYl3|*Yf2uqdda|%U61gPn9J6~c3k+nC zSF+A910E6>)MrBOGXekVU;_jRv;4F{5RVcQOISM#05H!900{h?b4)DI2>HM3N03`d zVkhS=+?aC$9KW5X#}9MF&xHk^BU4EVCp8EEDvr1X0ss!U006bDzv< zY_YZbc--%OtVjHBb7)CqT9PFB4CQ2wDg_LBn@oP2EKK(NZzkwC6ND!R;Uz>JY}k&a QFmEpafFFN$Pa-q=59Lj)AOHXW diff --git a/fastbee-report/src/main/resources/templates/DEVICE_VIBRATING_WIRE_V2.docx b/fastbee-report/src/main/resources/templates/DEVICE_VIBRATING_WIRE_V2.docx index ec8c2d4eb9a30ecc744d41dbe5374bbcc61821d2..aa023dc84ac4059d6378035b90cab10312446579 100644 GIT binary patch delta 2505 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 delta 2612 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 Sl0tua9>D$R%{!v$(SHEIjGKS} diff --git a/fastbee-report/src/main/resources/templates/DEVICE_WATER_LEVEL_V2.docx b/fastbee-report/src/main/resources/templates/DEVICE_WATER_LEVEL_V2.docx index d40d586008c0e12f61a02985fed44376219b1f61..065054a7fbf6eef8fb6e64e9e8ab144d08f24899 100644 GIT binary patch delta 2528 zcmZ9Odpy(oAIG<0DCRC5b6YE-a)}X@+zOo-a!YbMp^;(jL}AP&sbm+IqPcbuxy*5= znrf~kllu{d$z=`}4r@8)@i>piuiy8d=i~KxKi`kf=kw3^^ByP#HJ5_KkQW66WdQ(y z2p|GqE%puwion+}fGT_BfHL;@K=1$v5`owBloAwds}=?T@Mfec@B!THRSze{3-U1& zUipmu_UBy%{{V^ZSIgE!r|31FE?WEa^oC6<CIqlISpx+Ygz7s8Vs5Cg# zxl6CPT9oe3^D8)bzxaD1n?gA#daN5{^HFRg-oGhe``hhTnC)=LnV3cPgC_Z-X68}| zI#@b>HBLldwj6fI6tRNqyVoCZ9tN_K%5;6%>-4Rk!hZFI6U`)KHH#Q0v zeq3n{udIvgk7+@9w)*abzP+|pkHuKEw9kx5=T-WeJaoCfu0ua>B~a)wB<-ii1DzMP zV~^AiZliIhxj!T;uEnn+8|qNPK{1qbGBSw3$%~oW`xcy$3B@LmcBOuJnzM4w{=BEL3&Oya$HInql!uQ!BlLQns=qnD%2=pWT9vVozT<9s z+&Oom=70(~SzolfsoG_1G_|L@tV1F|c6i z?*+ZjYP);LjjIr7W#=*z`u+kKWpd+Hl~kwstFnY}b;fWeoVYsFAYhXU9iTQfuW;L# zv{v=n4=FrO=3shz->nwgmv2~>>rl0<)dUw$v5(CXQRNK7$J@9t2;_3|z%7Pl3{ zn{pip7>hetA$^%m3%`lhLLSP%Lx01}d5fZePsfYa7bH|hCOjonJ$=GUAx_c+{f^)cBNU3x;YWe-mtO5U#9Pk;MO3FFT8T{oDZQOTJ+&k8<6*?Mv$5b<- zpLz(T+ZAFIyjG5M+my>;<4?-2qi`RmsUDxtO-KG!7OHG=SYU@;`3A{jv{PsF(^+=* z5q}z%!DS`uBV%Tnhgsi{u*1N*@>C^MXOV{0k0yLN_`rhIq$2_Pi*19QD>B+xUnKdn z>-785&f6#Zmpz_~iqAgEpfwCR)J$NN>jNGycZ_n)%k#gKrT4(EOR5+hX5-LxTO0GU z3-6$&_@nXzw=UV?;7QDR`T55~4oX_N z$7+ez623@O&ZGGYHs&P3S_t#U zsee|>w&Qbyv`jYIeC==Be);Q;Z1wAVX*iu+TC(;yyH32FQ@~pGMN`Fwrml2kAJ#*l_vYVJM~Hdiu=Gv1swi8uM%c)r?qar~y2^&*5l zAMUxhrtSMjW35y3YZUrNeck5wbWLm)FX|o|ATrH4$(T9@$B8g(9QuF(?MrVO;as>O zc3<$-=*8fCeFV(maIO~tZd7}x=E{F>09hQF?#8=9Q>^w&p@K#=n>-M2QyS#Prprl*_G`llaC6m`c%%X5XI+QVltpX-~Se!-Vy%ax4(u0 zLG3;E`y_j``9r|4DcXQf8|;z*pN5e-d-WT42q1p`9<>SHC22i=kdF@uoBUOJqTHS} zp7dm|DwudRpCz>A+d_mdO!f6TMz(%?v5w_ zeX+m@(rbbizk7)Q1?0l7HC0|AgS#+WHVR;9Si_8YXSgrKRJUWdlG!l zp~M;w74O!-y9bu}$>}8#NibFZh0b=(4z%vp)OQ`_pPb?Z(s+WZ*3ZY*^sH_E_0rU? o=)2pX@KZw`NrkM=zl}8+3alqxC#&&8@@{Y>Jtu2|Ez|h=zu>}>82|tP delta 2508 zcmZ9Oc{J4PAIE2G*|H3>jf|~qVX|IiL`e3SxVACqiir{9TH=nSkYvn!kx0pKBQlmu zmQZ$sQOek&YY&M_MiG9-`E}0k{GM~3^E&U(=lMLJXZh#dRK_-3#>Vex!_FZJ0)eucVEe3*yeK-3=yIziuQ|D zhC^l3OwHe@p52(mo6E)-googi%_Oy43eoHtf@e$(zcD-3rc?=KZYs-feS|gM6#89t zmL!~z(C+qyKNBq!_*|7Cny5mrMsu#fANI*{cF}T6tNo3IBk8t@Vi#4`LU<*VOv#l7 zGLfh4j-^HBrFUCJOG>t+j|ihhq<;Deh{$MpD@IQxdTQk17wc_Bw6ZLueEH0-bcs7l z5l-BJFVGAYXN7{h4C1RvmC^dM_S=^VzrMkoZv?C4+csqxt^0BWLACU*fgjjS*{j_{ zsYjU^s(viGK4x2dQ+r1TeD$wuykEFlRHc{lE=>LID?fGH;ud$Sg}zV*b4e~RqGe;L zG*C(Zll03qVZ5hK{*WVfXi8sWw@FK%{Ssa)NU~#CzV|{p-tL~<_GJB$wv0#jHN_e_lwqPkhfiAURj~c)bZR; z_WSjV+M_-$sfD~VStm_eosC9QH`3aQ&l6lsO~({1UgIg@jWom7Z`zc0m#)?R)p)zn z38T8CI?F9|+-AleZn*KUf&H)4xY;#Ef%J^YskIzXoR|^%#j|oNxSm>`#AG8?0$3W+ zG7MeTY@ssnJ3l&KT7_;&r>C^lAy2#Ay~~l8TZLvvHJo*H#3srcPu?*2ntLiilmtn) zzwjNkhwnSa+?rrcPPR80oiEl9Jbn?Bw5({i0X5-my`qK2coUzB@e^`V7>2V-5E?( zimFYD;NxNaawg%i$H=1=Cnf(AZFUwVVewdo)f3H zRgHN}xBtEW6O2OkJ`MHAcDoWT7}8#$*L_0gGta)Gy3Qwsi`jb{eUH~|uD_Y%O_FbC z^BsHIFj+Jdh!dXe^R9Z>=I*3#VD3c0FF-V!ERO3?x|?|yYn;!n%T947FW)!GQF>aY z6B#o^Zbm9Si8mP7R1nvMKOH=M8R9dsJW9;5O}jitQF99ndqQWwpEmbnlV@a!_nAWq z7nryc^vdAe)^30LP1nH7=F#L;-;XP$!)uMGmjQ&q9>ZO;in6Kv?0&_0d6Upf_;FP5 z=F)r3a`8>OXx=0yif^C)x6!IA3`1X&GHWhy_zhtRQ;)(_0cS3WB7PN|n+l|DA~w-& zT6{4fL1}*{4ujDx?ogoXp`?>ZQM=xqJ7~4yjuju17sXggN;b~_774d9*7|(6PfkfD zYvYMxGj+TE&#JCPFj~NU77N9wzqcwpWs; zV-p1#`uRjy0YuCz^v^bdhvs3XVa@@syXpeA8}4ClO4l`G=gFU9T0WIh@qLiIfHdn+ z``89~toG2f_6hO-%cyDOmrr}Fc~ zVrSHQvi)(Vq~91PVM&``6xBj`#q@0IJ-eNdaMvK{;q|KLDDXiw{UT18pxHK8ocsz2a& zCEH_a)jwR%;T0;K5!NQ*(tb<--3E^Gw|`gHlBoM{;SI#lN$bX8)hid`MQVO*MxB6{ zz*>g3q24vq4`~N*ANNkwDCMvA9K)~t9 zAFK=ZYqESp3j(XlqGrSwb{1u#wT|)*F+?_&o{8P&IwUZW#9*QJ);23zaa-(YReSP- zqbl#T4Ng}2awdWWAXA@({>*bM@DYvw--#fJa8@Rj2nCaX5~7+c%c(eJkUu`r1c54% zoFKu&BmWxl;C*0?c#>5hNP@EUBLF%A3Rsi)m049}ge9t#9RynB0fB^nl^il2d@ka@ z*A53-N#aK>KD*C5gAQMXdN^4Q>eq+|s{yqn1y+ku5)|wOY?0(xLIQw-*8yEXktIA1 z#S#z&$g{-5L-7V^0M4+)X8_99cpG?iTMB?>@v|ZtSy1phz$WWxmyj$d+x0XcBux@1 M$r9r7&SbUy4}4^pt^fc4 diff --git a/generate_professional_templates.py b/generate_professional_templates.py index fa62cd37..69bc5619 100644 --- a/generate_professional_templates.py +++ b/generate_professional_templates.py @@ -163,7 +163,7 @@ def create_professional_template(template_name, device_type, template_dir): ('平均值', '{{analysis.statistics.avgWithUnit}}'), ('中位数', '{{analysis.statistics.medianWithUnit}}'), ('变化量', '{{analysis.statistics.deltaWithUnit}}'), - ('累计变化', '{{analysis.statistics.totalDeltaWithUnit}}'), + ('波动量', '{{analysis.statistics.totalDeltaWithUnit}}'), ('数据量', '{{analysis.statistics.count}}'), ] @@ -183,12 +183,12 @@ def create_professional_template(template_name, device_type, template_dir): # 第1行:表头 # 第2行:{{listName}} 标记行(可以是空单元格) # 第3行:模板行,使用 [field] 语法 - points_table = doc.add_table(rows=3, cols=8) + points_table = doc.add_table(rows=3, cols=6) points_table.style = 'Table Grid' points_table.alignment = WD_TABLE_ALIGNMENT.CENTER # 表头 - points_headers = ['测点', '初始值', '当前值', '变化量', '变化速率', '最大值', '最小值', '预警状态'] + points_headers = ['测点', '初始值', '当前值', '变化量', '变化速率', '预警状态'] points_header_row = points_table.rows[0] for i, header in enumerate(points_headers): points_header_row.cells[i].text = header @@ -206,15 +206,13 @@ def create_professional_template(template_name, device_type, template_dir): '[currentValue]', '[changeValue]', '[changeRate]', - '[maxValue]', - '[minValue]', '[warningStatus]', ] for i, data in enumerate(points_data): points_data_row.cells[i].text = data style_body_cell(points_data_row.cells[i]) - set_table_widths(points_table, [2.6, 1.84, 1.84, 1.84, 1.84, 1.84, 1.84, 1.84]) + set_table_widths(points_table, [2.6, 2.5, 2.5, 2.5, 2.5, 2.0]) # ===== 7. 趋势分析 ===== add_heading(doc, '五、趋势分析') diff --git a/sql/init_report_threshold.sql b/sql/init_report_threshold.sql new file mode 100644 index 00000000..cc72829f --- /dev/null +++ b/sql/init_report_threshold.sql @@ -0,0 +1,55 @@ +-- 报告阈值初始化脚本(基于 JGJ 8-2016、GB 50497-2019 等行业标准) +-- 使用方式:在 fastbee 数据库中执行此脚本 +-- 说明:product_id=0 表示全局默认,可被产品级/设备级配置覆盖 + +-- 1. 深部位移(累计位移控制值通常 30-100mm,速率预警值 2mm/d) +INSERT INTO iot_report_threshold (product_id, device_id, monitor_category_code, metric_identifier, warning_value, alarm_value, control_value, unit, create_by, create_time) +VALUES (0, 0, 'DEEP_DISPLACEMENT', 'displacement', 20.00, 30.00, 50.00, 'mm', 'admin', NOW()); + +-- 2. 静力水准(累计沉降控制值通常 20-60mm,速率预警值 2mm/d) +INSERT INTO iot_report_threshold (product_id, device_id, monitor_category_code, metric_identifier, warning_value, alarm_value, control_value, unit, create_by, create_time) +VALUES (0, 0, 'STATIC_LEVEL', 'settlement', 15.00, 25.00, 40.00, 'mm', 'admin', NOW()); + +-- 3. 裂缝计(裂缝宽度控制值通常 0.3-1.0mm,变化量预警值 0.2mm) +INSERT INTO iot_report_threshold (product_id, device_id, monitor_category_code, metric_identifier, warning_value, alarm_value, control_value, unit, create_by, create_time) +VALUES (0, 0, 'CRACK_METER', 'crack_width', 0.30, 0.50, 1.00, 'mm', 'admin', NOW()); + +-- 4. 轴力计(轴力控制值按设计值的 60%-80%,这里设为相对值) +-- 注:轴力阈值通常需根据设计值动态计算,此处仅作示例 +INSERT INTO iot_report_threshold (product_id, device_id, monitor_category_code, metric_identifier, warning_value, alarm_value, control_value, unit, create_by, create_time) +VALUES (0, 0, 'AXIAL_FORCE', 'axial_force', NULL, NULL, NULL, 'kN', 'admin', NOW()); + +-- 5. 测距仪(收敛变形控制值通常 20-50mm) +INSERT INTO iot_report_threshold (product_id, device_id, monitor_category_code, metric_identifier, warning_value, alarm_value, control_value, unit, create_by, create_time) +VALUES (0, 0, 'RANGING_METER', 'distance', 15.00, 25.00, 40.00, 'mm', 'admin', NOW()); + +-- 6. 倾角计(倾斜角度控制值通常 1°-3°) +INSERT INTO iot_report_threshold (product_id, device_id, monitor_category_code, metric_identifier, warning_value, alarm_value, control_value, unit, create_by, create_time) +VALUES (0, 0, 'INCLINOMETER', 'inclination', 1.00, 2.00, 3.00, '°', 'admin', NOW()); + +-- 7. 水位计(水位变化控制值通常 0.5-2.0m) +INSERT INTO iot_report_threshold (product_id, device_id, monitor_category_code, metric_identifier, warning_value, alarm_value, control_value, unit, create_by, create_time) +VALUES (0, 0, 'WATER_LEVEL', 'water_level', 0.50, 1.00, 2.00, 'm', 'admin', NOW()); + +-- 8. 应变计(应变控制值通常 ±1000με) +INSERT INTO iot_report_threshold (product_id, device_id, monitor_category_code, metric_identifier, warning_value, alarm_value, control_value, unit, create_by, create_time) +VALUES (0, 0, 'STRAIN_GAUGE', 'strain', 500.00, 800.00, 1000.00, 'με', 'admin', NOW()); + +-- 9. 振弦式传感器(频率变化控制值通常 ±50Hz) +INSERT INTO iot_report_threshold (product_id, device_id, monitor_category_code, metric_identifier, warning_value, alarm_value, control_value, unit, create_by, create_time) +VALUES (0, 0, 'VIBRATING_WIRE', 'frequency', 30.00, 40.00, 50.00, 'Hz', 'admin', NOW()); + +-- 10. 温度计(温度变化控制值通常 ±15℃) +INSERT INTO iot_report_threshold (product_id, device_id, monitor_category_code, metric_identifier, warning_value, alarm_value, control_value, unit, create_by, create_time) +VALUES (0, 0, 'THERMOMETER', 'temperature', 10.00, 15.00, 20.00, '℃', 'admin', NOW()); + +-- 11. 雨量计(累计降雨量预警值通常 50mm/d) +INSERT INTO iot_report_threshold (product_id, device_id, monitor_category_code, metric_identifier, warning_value, alarm_value, control_value, unit, create_by, create_time) +VALUES (0, 0, 'RAIN_GAUGE', 'rainfall', 25.00, 50.00, 100.00, 'mm', 'admin', NOW()); + +-- 说明: +-- 1. 以上阈值基于 JGJ 8-2016《建筑变形测量规范》、GB 50497-2019《建筑基坑工程监测标准》 +-- 等行业标准,适用于一般建筑工程。实际项目应根据设计要求调整。 +-- 2. product_id=0 表示全局默认,如需针对特定产品,请复制以上记录并修改 product_id。 +-- 3. metric_identifier 需与物模型标识符匹配,如不匹配则阈值不生效。 +-- 4. 预警值 < 报警值 < 控制值,系统会按当前值与阈值的比较自动判断安全等级。 \ No newline at end of file diff --git a/sql/init_threshold_with_table.sql b/sql/init_threshold_with_table.sql new file mode 100644 index 00000000..db0c665e --- /dev/null +++ b/sql/init_threshold_with_table.sql @@ -0,0 +1,77 @@ +-- ============================================================ +-- 报告阈值表创建 + 行业标准默认值初始化 +-- 数据库:fastbee +-- 执行方式:mysql -u root -p fastbee < sql/init_threshold_with_table.sql +-- ============================================================ + +-- 1. 创建阈值表 +CREATE TABLE IF NOT EXISTS iot_report_threshold ( + id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键', + product_id BIGINT NOT NULL DEFAULT 0 COMMENT '产品ID(0=全局默认)', + device_id BIGINT NOT NULL DEFAULT 0 COMMENT '设备ID(0=不限定设备)', + monitor_category_code VARCHAR(50) COMMENT '监测分类编码', + metric_identifier VARCHAR(100) NOT NULL COMMENT '物模型标识符', + warning_value DECIMAL(10,4) COMMENT '预警值', + alarm_value DECIMAL(10,4) COMMENT '报警值', + control_value DECIMAL(10,4) COMMENT '控制值', + unit VARCHAR(20) COMMENT '单位', + create_by VARCHAR(64) COMMENT '创建者', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + update_by VARCHAR(64) COMMENT '更新者', + update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + INDEX idx_product_device (product_id, device_id), + INDEX idx_metric_identifier (metric_identifier) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='报告结构化阈值表'; + +-- 2. 初始化行业标准默认阈值(product_id=0 全局默认) +-- 来源:JGJ 8-2016《建筑变形测量规范》、GB 50497-2019《建筑基坑工程监测标准》 + +-- 深部位移:累计位移控制值 30-100mm,速率预警值 2mm/d +INSERT INTO iot_report_threshold (product_id, device_id, monitor_category_code, metric_identifier, warning_value, alarm_value, control_value, unit, create_by, create_time) +VALUES (0, 0, 'DEEP_DISPLACEMENT', 'displacement', 20.00, 30.00, 50.00, 'mm', 'admin', NOW()); + +-- 静力水准:累计沉降控制值 20-60mm,速率预警值 2mm/d +INSERT INTO iot_report_threshold (product_id, device_id, monitor_category_code, metric_identifier, warning_value, alarm_value, control_value, unit, create_by, create_time) +VALUES (0, 0, 'STATIC_LEVEL', 'settlement', 15.00, 25.00, 40.00, 'mm', 'admin', NOW()); + +-- 裂缝计:裂缝宽度控制值 0.3-1.0mm,变化量预警值 0.2mm +INSERT INTO iot_report_threshold (product_id, device_id, monitor_category_code, metric_identifier, warning_value, alarm_value, control_value, unit, create_by, create_time) +VALUES (0, 0, 'CRACK_METER', 'crack_width', 0.30, 0.50, 1.00, 'mm', 'admin', NOW()); + +-- 测距仪:收敛变形控制值 20-50mm +INSERT INTO iot_report_threshold (product_id, device_id, monitor_category_code, metric_identifier, warning_value, alarm_value, control_value, unit, create_by, create_time) +VALUES (0, 0, 'RANGING_METER', 'distance', 15.00, 25.00, 40.00, 'mm', 'admin', NOW()); + +-- 倾角计:倾斜角度控制值 1°-3° +INSERT INTO iot_report_threshold (product_id, device_id, monitor_category_code, metric_identifier, warning_value, alarm_value, control_value, unit, create_by, create_time) +VALUES (0, 0, 'INCLINOMETER', 'inclination', 1.00, 2.00, 3.00, '°', 'admin', NOW()); + +-- 水位计:水位变化控制值 0.5-2.0m +INSERT INTO iot_report_threshold (product_id, device_id, monitor_category_code, metric_identifier, warning_value, alarm_value, control_value, unit, create_by, create_time) +VALUES (0, 0, 'WATER_LEVEL', 'water_level', 0.50, 1.00, 2.00, 'm', 'admin', NOW()); + +-- 应变计:应变控制值 ±1000με +INSERT INTO iot_report_threshold (product_id, device_id, monitor_category_code, metric_identifier, warning_value, alarm_value, control_value, unit, create_by, create_time) +VALUES (0, 0, 'STRAIN_GAUGE', 'strain', 500.00, 800.00, 1000.00, 'με', 'admin', NOW()); + +-- 振弦式传感器:频率变化控制值 ±50Hz +INSERT INTO iot_report_threshold (product_id, device_id, monitor_category_code, metric_identifier, warning_value, alarm_value, control_value, unit, create_by, create_time) +VALUES (0, 0, 'VIBRATING_WIRE', 'frequency', 30.00, 40.00, 50.00, 'Hz', 'admin', NOW()); + +-- 温度计:温度变化控制值 ±15℃ +INSERT INTO iot_report_threshold (product_id, device_id, monitor_category_code, metric_identifier, warning_value, alarm_value, control_value, unit, create_by, create_time) +VALUES (0, 0, 'THERMOMETER', 'temperature', 10.00, 15.00, 20.00, '℃', 'admin', NOW()); + +-- 雨量计:累计降雨量预警值 50mm/d +INSERT INTO iot_report_threshold (product_id, device_id, monitor_category_code, metric_identifier, warning_value, alarm_value, control_value, unit, create_by, create_time) +VALUES (0, 0, 'RAIN_GAUGE', 'rainfall', 25.00, 50.00, 100.00, 'mm', 'admin', NOW()); + +-- ============================================================ +-- 重要说明: +-- 1. metric_identifier 需与产品物模型的标识符匹配 +-- 如果物模型标识符不同,请修改 UPDATE 语句更新对应记录 +-- 示例:UPDATE iot_report_threshold SET metric_identifier='X_axis_displacement' WHERE monitor_category_code='DEEP_DISPLACEMENT'; +-- 2. 预警值 < 报警值 < 控制值,系统会按当前值与阈值的比较自动判断安全等级 +-- 3. 如需针对特定产品配置阈值,复制以上记录并修改 product_id +-- 4. 如需针对特定设备配置阈值,复制以上记录并修改 device_id +-- ============================================================ \ No newline at end of file diff --git a/sql/menu_threshold.sql b/sql/menu_threshold.sql new file mode 100644 index 00000000..a024511e --- /dev/null +++ b/sql/menu_threshold.sql @@ -0,0 +1,37 @@ +-- ============================================================ +-- 监测阈值配置菜单权限初始化 +-- 说明:在 fastbee 数据库中执行此脚本 +-- ============================================================ + +-- 查询 IoT 模块父菜单 ID(假设菜单名称为"设备管理"或"监测管理") +-- 如果不存在,则创建一个"监测管理"父菜单 +SET @iot_parent_id = (SELECT menu_id FROM sys_menu WHERE menu_name = '设备管理' AND parent_id = 0 LIMIT 1); + +-- 如果找不到"设备管理",则使用系统工具作为父菜单 +SET @iot_parent_id = IFNULL(@iot_parent_id, 1); + +-- 插入阈值配置菜单 +INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) +VALUES ('阈值配置', @iot_parent_id, 10, 'threshold', 'iot/threshold/index', 1, 0, 'C', '0', '0', 'iot:threshold:list', 'tool', 'admin', NOW(), '监测阈值配置菜单'); + +-- 获取刚插入的菜单 ID +SET @threshold_menu_id = LAST_INSERT_ID(); + +-- 插入按钮权限 +INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) VALUES +('阈值查询', @threshold_menu_id, 1, '', '', 1, 0, 'F', '0', '0', 'iot:threshold:query', '#', 'admin', NOW(), ''), +('阈值新增', @threshold_menu_id, 2, '', '', 1, 0, 'F', '0', '0', 'iot:threshold:add', '#', 'admin', NOW(), ''), +('阈值修改', @threshold_menu_id, 3, '', '', 1, 0, 'F', '0', '0', 'iot:threshold:edit', '#', 'admin', NOW(), ''), +('阈值删除', @threshold_menu_id, 4, '', '', 1, 0, 'F', '0', '0', 'iot:threshold:remove', '#', 'admin', NOW(), ''), +('阈值导出', @threshold_menu_id, 5, '', '', 1, 0, 'F', '0', '0', 'iot:threshold:export', '#', 'admin', NOW(), ''); + +-- 提示 +SELECT '阈值配置菜单已创建,菜单ID为:' AS message, @threshold_menu_id AS menu_id; +SELECT '请在前端重新登录或刷新页面以查看新菜单' AS hint; + +-- ============================================================ +-- 说明: +-- 1. 执行此脚本后,管理员角色将自动拥有阈值配置菜单权限 +-- 2. 如需分配给其他角色,请在"系统管理 -> 角色管理"中配置 +-- 3. 菜单路径为:/iot/threshold,组件路径为:iot/threshold/index +-- ============================================================ \ No newline at end of file diff --git a/yack-web/src/api/iot/threshold.js b/yack-web/src/api/iot/threshold.js new file mode 100644 index 00000000..69fea5bd --- /dev/null +++ b/yack-web/src/api/iot/threshold.js @@ -0,0 +1,69 @@ +import request from '@/utils/request' + +// 查询阈值列表 +export function listThreshold(query) { + return request({ + url: '/iot/threshold/list', + method: 'get', + params: query + }) +} + +// 查询阈值详细 +export function getThreshold(id) { + return request({ + url: '/iot/threshold/' + id, + method: 'get' + }) +} + +// 新增阈值 +export function addThreshold(data) { + return request({ + url: '/iot/threshold', + method: 'post', + data: data + }) +} + +// 修改阈值 +export function updateThreshold(data) { + return request({ + url: '/iot/threshold', + method: 'put', + data: data + }) +} + +// 删除阈值 +export function delThreshold(id) { + return request({ + url: '/iot/threshold/' + id, + method: 'delete' + }) +} + +// 获取监测类别列表 +export function getCategories() { + return request({ + url: '/iot/threshold/categories', + method: 'get' + }) +} + +// 获取产品列表(阈值配置专用) +export function listProducts() { + return request({ + url: '/iot/threshold/products', + method: 'get' + }) +} + +// 获取设备列表(阈值配置专用) +export function listDevices(productId) { + return request({ + url: '/iot/threshold/devices', + method: 'get', + params: { productId: productId } + }) +} \ No newline at end of file