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 d88772bb..c11e5d75 100644 Binary files a/fastbee-report/src/main/resources/templates/DEVICE_AXIAL_FORCE_V2.docx and b/fastbee-report/src/main/resources/templates/DEVICE_AXIAL_FORCE_V2.docx differ diff --git a/fastbee-report/src/main/resources/templates/DEVICE_CRACK_METER_V2.docx b/fastbee-report/src/main/resources/templates/DEVICE_CRACK_METER_V2.docx index 2f4ad757..afab2c14 100644 Binary files a/fastbee-report/src/main/resources/templates/DEVICE_CRACK_METER_V2.docx and b/fastbee-report/src/main/resources/templates/DEVICE_CRACK_METER_V2.docx differ 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 cdda2a9c..4f154a59 100644 Binary files a/fastbee-report/src/main/resources/templates/DEVICE_DEEP_DISPLACEMENT_V2.docx and b/fastbee-report/src/main/resources/templates/DEVICE_DEEP_DISPLACEMENT_V2.docx differ 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 1cc11af5..75162741 100644 Binary files a/fastbee-report/src/main/resources/templates/DEVICE_INCLINOMETER_V2.docx and b/fastbee-report/src/main/resources/templates/DEVICE_INCLINOMETER_V2.docx differ diff --git a/fastbee-report/src/main/resources/templates/DEVICE_RAIN_GAUGE_V2.docx b/fastbee-report/src/main/resources/templates/DEVICE_RAIN_GAUGE_V2.docx index 55d8d314..128f8272 100644 Binary files a/fastbee-report/src/main/resources/templates/DEVICE_RAIN_GAUGE_V2.docx and b/fastbee-report/src/main/resources/templates/DEVICE_RAIN_GAUGE_V2.docx differ 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 8b387568..939c0ba7 100644 Binary files a/fastbee-report/src/main/resources/templates/DEVICE_RANGING_METER_V2.docx and b/fastbee-report/src/main/resources/templates/DEVICE_RANGING_METER_V2.docx differ 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 b002e934..45e1c9dc 100644 Binary files a/fastbee-report/src/main/resources/templates/DEVICE_STATIC_LEVEL_V2.docx and b/fastbee-report/src/main/resources/templates/DEVICE_STATIC_LEVEL_V2.docx differ diff --git a/fastbee-report/src/main/resources/templates/DEVICE_STRAIN_GAUGE_V2.docx b/fastbee-report/src/main/resources/templates/DEVICE_STRAIN_GAUGE_V2.docx index 48171537..e40c94ba 100644 Binary files a/fastbee-report/src/main/resources/templates/DEVICE_STRAIN_GAUGE_V2.docx and b/fastbee-report/src/main/resources/templates/DEVICE_STRAIN_GAUGE_V2.docx differ 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 c9f5f8ad..5871ec50 100644 Binary files a/fastbee-report/src/main/resources/templates/DEVICE_THERMOMETER_V2.docx and b/fastbee-report/src/main/resources/templates/DEVICE_THERMOMETER_V2.docx differ 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 ec8c2d4e..aa023dc8 100644 Binary files a/fastbee-report/src/main/resources/templates/DEVICE_VIBRATING_WIRE_V2.docx and b/fastbee-report/src/main/resources/templates/DEVICE_VIBRATING_WIRE_V2.docx differ 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 d40d5860..065054a7 100644 Binary files a/fastbee-report/src/main/resources/templates/DEVICE_WATER_LEVEL_V2.docx and b/fastbee-report/src/main/resources/templates/DEVICE_WATER_LEVEL_V2.docx differ 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