20260910:导出报告优化v2版本
This commit is contained in:
parent
0fdcf8dad7
commit
c050141b17
@ -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<ReportThreshold> 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<ReportThreshold> list = reportThresholdService.selectThresholdList(query);
|
||||
ExcelUtil<ReportThreshold> 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<Map<String, String>> 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<String, String> 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<Map<String, Object>> 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<String, Object> 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<Map<String, Object>> 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<String, Object> 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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
/**
|
||||
* 值格式化器:类别感知的精度与单位策略
|
||||
*
|
||||
* <p>解决报告输出中:</p>
|
||||
* <ul>
|
||||
* <li>6 位小数不专业(深部位移/静力水准应为 mm 保留 2 位)</li>
|
||||
* <li>单位缺失(物模型未配置 unit 时全文无单位)</li>
|
||||
* <li>变化速率单位拼接错误(unit 为空 → rateUnit="/天")</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author FastBee
|
||||
* @date 2026-09-10
|
||||
*/
|
||||
@Component
|
||||
public class ValueFormatter {
|
||||
|
||||
/**
|
||||
* 按监测类别配置默认单位与精度
|
||||
* key: 监测类别枚举
|
||||
* value: [默认单位, 小数位数]
|
||||
*/
|
||||
private static final Map<MonitorCategory, CategoryFormat> 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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
/**
|
||||
* 监测报告分析器抽象基类(模板方法模式)
|
||||
*
|
||||
* <p>抽取 11 类设备分析器的公共骨架,子类只需覆盖差异点:</p>
|
||||
* <ul>
|
||||
* <li>{@link #supportedCategory()} 监测类别</li>
|
||||
* <li>{@link #primaryMetricKeywords()} 主指标关键词</li>
|
||||
* <li>{@link #allowAuxiliaryAsPrimary()} 是否允许辅助指标作主指标(倾角计/温度计)</li>
|
||||
* <li>{@link #directionText(String)} 方向文本映射</li>
|
||||
* <li>{@link #buildSummary(Statistics, TrendAnalysis, String, TimeWindow)} 项目概况</li>
|
||||
* <li>{@link #buildProfessionalAdvice(Statistics, TrendAnalysis, WarningAnalysis, String)} 专业建议</li>
|
||||
* <li>{@link #addDomainConclusions(List, AnalysisResult, ReportContext)} 领域特定结论</li>
|
||||
* </ul>
|
||||
*
|
||||
* @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<Conclusion> 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<String, List<MetricData>> byIdentity = context.getMetricDataList().stream()
|
||||
.filter(d -> d.getIdentity() != null)
|
||||
.collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList()));
|
||||
|
||||
// 2. 获取所有监测物模型
|
||||
Map<String, ThingsModel> allModels = getAllModels(context);
|
||||
|
||||
if (allModels.isEmpty()) {
|
||||
result.getConclusions().add(new Conclusion(1, "设备未配置监测物模型,请联系管理员配置。"));
|
||||
return result;
|
||||
}
|
||||
|
||||
// 3. 为每个物模型生成统计数据(含单位兜底)
|
||||
for (Map.Entry<String, ThingsModel> entry : allModels.entrySet()) {
|
||||
String identity = entry.getKey();
|
||||
ThingsModel model = entry.getValue();
|
||||
List<MetricData> 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<String, ThingsModel> 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<MetricPointData> buildMetricPointsFiltered(AnalysisResult result, ReportContext context) {
|
||||
List<MetricPointData> all = new ArrayList<>();
|
||||
String primary = result.getPrimaryIdentifier();
|
||||
MetricPointData primaryPoint = null;
|
||||
|
||||
for (Map.Entry<String, Statistics> 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<MetricPointData> 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<Conclusion> 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<String, List<MetricData>> byIdentity = context.getMetricDataList().stream()
|
||||
.filter(d -> d.getIdentity() != null)
|
||||
.collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList()));
|
||||
Map<String, byte[]> 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 "正常";
|
||||
}
|
||||
}
|
||||
@ -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<String, List<MetricData>> byIdentity = context.getMetricDataList().stream()
|
||||
.filter(d -> d.getIdentity() != null)
|
||||
.collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList()));
|
||||
|
||||
// 2. 获取所有监测物模型(即使没有数据也要显示)
|
||||
Map<String, ThingsModel> 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;
|
||||
protected String[] primaryMetricKeywords() {
|
||||
return new String[]{"轴力", "应力", "force", "stress"};
|
||||
}
|
||||
|
||||
// 3. 为每个物模型生成统计数据(包括无数据的物模型)
|
||||
for (Map.Entry<String, ThingsModel> entry : allModels.entrySet()) {
|
||||
String identity = entry.getKey();
|
||||
ThingsModel model = entry.getValue();
|
||||
List<MetricData> 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;
|
||||
}
|
||||
|
||||
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<MetricPointData> metricPoints = buildMetricPoints(result, context);
|
||||
result.setMetricPoints(metricPoints);
|
||||
|
||||
// 生成详细结论
|
||||
List<Conclusion> 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<MetricPointData> buildMetricPoints(AnalysisResult result, ReportContext context) {
|
||||
List<MetricPointData> points = new ArrayList<>();
|
||||
|
||||
for (Map.Entry<String, Statistics> 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<String, List<MetricData>> byIdentity = context.getMetricDataList().stream()
|
||||
.filter(d -> d.getIdentity() != null)
|
||||
.collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList()));
|
||||
Map<String, byte[]> 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();
|
||||
}
|
||||
}
|
||||
@ -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<String, List<MetricData>> byIdentity = context.getMetricDataList().stream()
|
||||
.filter(d -> d.getIdentity() != null)
|
||||
.collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList()));
|
||||
|
||||
// 2. 获取所有监测物模型(即使没有数据也要显示)
|
||||
Map<String, ThingsModel> 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;
|
||||
protected String[] primaryMetricKeywords() {
|
||||
return new String[]{"裂缝宽度", "缝宽", "width"};
|
||||
}
|
||||
|
||||
// 3. 为每个物模型生成统计数据(包括无数据的物模型)
|
||||
for (Map.Entry<String, ThingsModel> entry : allModels.entrySet()) {
|
||||
String identity = entry.getKey();
|
||||
ThingsModel model = entry.getValue();
|
||||
List<MetricData> 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;
|
||||
}
|
||||
|
||||
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<MetricPointData> metricPoints = buildMetricPoints(result, context);
|
||||
result.setMetricPoints(metricPoints);
|
||||
|
||||
// 生成详细结论
|
||||
List<Conclusion> 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<MetricPointData> buildMetricPoints(AnalysisResult result, ReportContext context) {
|
||||
List<MetricPointData> points = new ArrayList<>();
|
||||
|
||||
for (Map.Entry<String, Statistics> 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<String, List<MetricData>> byIdentity = context.getMetricDataList().stream()
|
||||
.filter(d -> d.getIdentity() != null)
|
||||
.collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList()));
|
||||
Map<String, byte[]> 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();
|
||||
}
|
||||
}
|
||||
@ -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<String, List<MetricData>> byIdentity = context.getMetricDataList().stream()
|
||||
.filter(d -> d.getIdentity() != null)
|
||||
.collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList()));
|
||||
|
||||
// 2. 获取所有监测物模型(即使没有数据也要显示)
|
||||
Map<String, ThingsModel> 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;
|
||||
protected String[] primaryMetricKeywords() {
|
||||
return new String[]{"深部位移", "位移", "displacement", "沉降", "settlement"};
|
||||
}
|
||||
|
||||
// 3. 为每个物模型生成统计数据(包括无数据的物模型)
|
||||
for (Map.Entry<String, ThingsModel> entry : allModels.entrySet()) {
|
||||
String identity = entry.getKey();
|
||||
ThingsModel model = entry.getValue();
|
||||
List<MetricData> 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());
|
||||
@Override
|
||||
protected String directionText(String direction) {
|
||||
// 方向语义:中性描述,不臆测坡体方向
|
||||
if (TrendAnalysis.DIRECTION_UP.equals(direction)) {
|
||||
return "位移增大方向";
|
||||
}
|
||||
if (statistics.getUnit() == null || statistics.getUnit().isEmpty()) {
|
||||
statistics.setUnit(model.getUnit());
|
||||
if (TrendAnalysis.DIRECTION_DOWN.equals(direction)) {
|
||||
return "位移减小方向";
|
||||
}
|
||||
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);
|
||||
return "稳定";
|
||||
}
|
||||
|
||||
// 统一主指标口径:按物模型顺序、排除角度/温度/电压等辅助量
|
||||
String primaryIdentity = primaryMetricResolver.resolve(context, byIdentity,
|
||||
"深部位移", "位移", "displacement", "沉降", "settlement");
|
||||
result.setPrimaryIdentifier(primaryIdentity);
|
||||
|
||||
buildConclusions(result, context, primaryIdentity);
|
||||
buildChart(result, context, primaryIdentity);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 主监测指标:优先名称/标识符含"位移/displacement/x/y/z",否则取第一个有数据的指标
|
||||
*
|
||||
* @deprecated 由 {@link PrimaryMetricResolver} 统一解析,保留仅为兼容历史调用
|
||||
*/
|
||||
@Deprecated
|
||||
private String resolvePrimaryIdentity(Map<String, List<MetricData>> byIdentity, ReportContext context) {
|
||||
return primaryMetricResolver.resolve(context, byIdentity, "位移", "displacement");
|
||||
}
|
||||
|
||||
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<MetricPointData> metricPoints = buildMetricPoints(result, context);
|
||||
result.setMetricPoints(metricPoints);
|
||||
|
||||
// 生成详细结论
|
||||
List<Conclusion> 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())) {
|
||||
// 阈值状态说明
|
||||
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 (WarningAnalysis.STATUS_WARNING.equals(warning.getStatus())) {
|
||||
} 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<MetricPointData> buildMetricPoints(AnalysisResult result, ReportContext context) {
|
||||
List<MetricPointData> points = new ArrayList<>();
|
||||
|
||||
for (Map.Entry<String, Statistics> 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<String, List<MetricData>> byIdentity = context.getMetricDataList().stream()
|
||||
.filter(d -> d.getIdentity() != null)
|
||||
.collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList()));
|
||||
Map<String, byte[]> 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 "稳定";
|
||||
}
|
||||
}
|
||||
@ -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<String, List<MetricData>> byIdentity = context.getMetricDataList().stream()
|
||||
.filter(d -> d.getIdentity() != null)
|
||||
.collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList()));
|
||||
|
||||
// 2. 获取所有监测物模型(即使没有数据也要显示)
|
||||
Map<String, ThingsModel> 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;
|
||||
protected String[] primaryMetricKeywords() {
|
||||
return new String[]{"倾角", "角度", "angle", "inclin"};
|
||||
}
|
||||
|
||||
// 3. 为每个物模型生成统计数据(包括无数据的物模型)
|
||||
for (Map.Entry<String, ThingsModel> entry : allModels.entrySet()) {
|
||||
String identity = entry.getKey();
|
||||
ThingsModel model = entry.getValue();
|
||||
List<MetricData> 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);
|
||||
@Override
|
||||
protected boolean allowAuxiliaryAsPrimary() {
|
||||
return true; // 倾角计的主指标本身就是角度
|
||||
}
|
||||
|
||||
// 统一主指标口径:优先名称/标识符命中关键词的物模型,排除角度/温度/电压等辅助量
|
||||
String primaryIdentity = primaryMetricResolver.resolve(context, byIdentity, true, "倾角", "角度", "angle", "inclin");
|
||||
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());
|
||||
}
|
||||
|
||||
// 组装多测点数据(支持多物模型标识符,如X轴/Y轴倾角)
|
||||
List<MetricPointData> metricPoints = buildMetricPoints(result, context);
|
||||
result.setMetricPoints(metricPoints);
|
||||
|
||||
// 生成详细结论
|
||||
List<Conclusion> 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成项目概况总结
|
||||
*/
|
||||
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<MetricPointData> buildMetricPoints(AnalysisResult result, ReportContext context) {
|
||||
List<MetricPointData> points = new ArrayList<>();
|
||||
|
||||
for (Map.Entry<String, Statistics> 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<String, List<MetricData>> byIdentity = context.getMetricDataList().stream()
|
||||
.filter(d -> d.getIdentity() != null)
|
||||
.collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList()));
|
||||
Map<String, byte[]> 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 "倾斜增大";
|
||||
}
|
||||
|
||||
@ -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<String, List<MetricData>> byIdentity = context.getMetricDataList().stream()
|
||||
.filter(d -> d.getIdentity() != null)
|
||||
.collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList()));
|
||||
|
||||
// 2. 获取所有监测物模型(即使没有数据也要显示)
|
||||
Map<String, ThingsModel> 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;
|
||||
protected String[] primaryMetricKeywords() {
|
||||
return new String[]{"雨量", "降雨", "降水量", "rain"};
|
||||
}
|
||||
|
||||
// 3. 为每个物模型生成统计数据(包括无数据的物模型)
|
||||
for (Map.Entry<String, ThingsModel> entry : allModels.entrySet()) {
|
||||
String identity = entry.getKey();
|
||||
ThingsModel model = entry.getValue();
|
||||
List<MetricData> 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;
|
||||
}
|
||||
|
||||
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<MetricPointData> metricPoints = buildMetricPoints(result, context);
|
||||
result.setMetricPoints(metricPoints);
|
||||
|
||||
// 生成详细结论
|
||||
List<Conclusion> 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<MetricPointData> buildMetricPoints(AnalysisResult result, ReportContext context) {
|
||||
List<MetricPointData> points = new ArrayList<>();
|
||||
|
||||
for (Map.Entry<String, Statistics> 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<String, List<MetricData>> byIdentity = context.getMetricDataList().stream()
|
||||
.filter(d -> d.getIdentity() != null)
|
||||
.collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList()));
|
||||
Map<String, byte[]> 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();
|
||||
}
|
||||
}
|
||||
@ -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<String, List<MetricData>> byIdentity = context.getMetricDataList().stream()
|
||||
.filter(d -> d.getIdentity() != null)
|
||||
.collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList()));
|
||||
|
||||
// 2. 获取所有监测物模型(即使没有数据也要显示)
|
||||
Map<String, ThingsModel> 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<String, ThingsModel> entry : allModels.entrySet()) {
|
||||
String identity = entry.getKey();
|
||||
ThingsModel model = entry.getValue();
|
||||
List<MetricData> 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<MetricPointData> metricPoints = buildMetricPoints(result, context);
|
||||
result.setMetricPoints(metricPoints);
|
||||
|
||||
// 生成详细结论
|
||||
List<Conclusion> 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<MetricPointData> buildMetricPoints(AnalysisResult result, ReportContext context) {
|
||||
List<MetricPointData> points = new ArrayList<>();
|
||||
|
||||
for (Map.Entry<String, Statistics> 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<String, List<MetricData>> byIdentity = context.getMetricDataList().stream()
|
||||
.filter(d -> d.getIdentity() != null)
|
||||
.collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList()));
|
||||
Map<String, byte[]> 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"};
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
/**
|
||||
* 静力水准专项分析器
|
||||
*
|
||||
* <p>注意:delta = current - initial 为高程变化(非沉降量)</p>
|
||||
* <ul>
|
||||
* <li>delta > 0:高程上升 → 隆起(回弹)</li>
|
||||
* <li>delta < 0:高程下降 → 沉降</li>
|
||||
* </ul>
|
||||
*
|
||||
* @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<String, List<MetricData>> byIdentity = context.getMetricDataList().stream()
|
||||
.filter(d -> d.getIdentity() != null)
|
||||
.collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList()));
|
||||
|
||||
// 2. 获取所有监测物模型(即使没有数据也要显示)
|
||||
Map<String, ThingsModel> 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;
|
||||
protected String[] primaryMetricKeywords() {
|
||||
// 静力水准物模型通常叫"水位"(实际是高程),用沉降/高程关键词匹配
|
||||
return new String[]{"沉降", "高程", "settlement", "水位"};
|
||||
}
|
||||
|
||||
// 3. 为每个物模型生成统计数据(包括无数据的物模型)
|
||||
for (Map.Entry<String, ThingsModel> entry : allModels.entrySet()) {
|
||||
String identity = entry.getKey();
|
||||
ThingsModel model = entry.getValue();
|
||||
List<MetricData> 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());
|
||||
@Override
|
||||
protected String directionText(String direction) {
|
||||
// 方向语义:UP/DOWN 指高程变化方向,映射到沉降术语
|
||||
if (TrendAnalysis.DIRECTION_UP.equals(direction)) {
|
||||
return "回弹隆起"; // 高程上升
|
||||
}
|
||||
if (statistics.getUnit() == null || statistics.getUnit().isEmpty()) {
|
||||
statistics.setUnit(model.getUnit());
|
||||
if (TrendAnalysis.DIRECTION_DOWN.equals(direction)) {
|
||||
return "持续沉降"; // 高程下降
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
// 设置控制值
|
||||
ReportThreshold threshold = context.getThresholds().get(primaryIdentity);
|
||||
if (threshold != null && threshold.getControlValue() != null) {
|
||||
result.setControlValue(threshold.getControlValue());
|
||||
result.setControlValueUnit(threshold.getUnit());
|
||||
}
|
||||
|
||||
// 组装多测点数据(支持多测点高程监测)
|
||||
List<MetricPointData> metricPoints = buildMetricPoints(result, context);
|
||||
result.setMetricPoints(metricPoints);
|
||||
|
||||
// 生成详细结论
|
||||
List<Conclusion> 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<String, Statistics> 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())) {
|
||||
// 阈值状态说明
|
||||
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<Conclusion> 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<MetricPointData> buildMetricPoints(AnalysisResult result, ReportContext context) {
|
||||
List<MetricPointData> points = new ArrayList<>();
|
||||
|
||||
for (Map.Entry<String, Statistics> 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<String, Statistics> allStatistics) {
|
||||
private String assessDifferentialSettlement(java.util.Map<String, Statistics> 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<String, Statistics> 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<String, List<MetricData>> byIdentity = context.getMetricDataList().stream()
|
||||
.filter(d -> d.getIdentity() != null)
|
||||
.collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList()));
|
||||
Map<String, byte[]> 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 "稳定";
|
||||
}
|
||||
}
|
||||
@ -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<String, List<MetricData>> byIdentity = context.getMetricDataList().stream()
|
||||
.filter(d -> d.getIdentity() != null)
|
||||
.collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList()));
|
||||
|
||||
// 2. 获取所有监测物模型(即使没有数据也要显示)
|
||||
Map<String, ThingsModel> 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;
|
||||
protected String[] primaryMetricKeywords() {
|
||||
return new String[]{"应变", "strain"};
|
||||
}
|
||||
|
||||
// 3. 为每个物模型生成统计数据(包括无数据的物模型)
|
||||
for (Map.Entry<String, ThingsModel> entry : allModels.entrySet()) {
|
||||
String identity = entry.getKey();
|
||||
ThingsModel model = entry.getValue();
|
||||
List<MetricData> 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;
|
||||
}
|
||||
|
||||
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<MetricPointData> metricPoints = buildMetricPoints(result, context);
|
||||
result.setMetricPoints(metricPoints);
|
||||
|
||||
// 生成详细结论
|
||||
List<Conclusion> 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<MetricPointData> buildMetricPoints(AnalysisResult result, ReportContext context) {
|
||||
List<MetricPointData> points = new ArrayList<>();
|
||||
|
||||
for (Map.Entry<String, Statistics> 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<String, List<MetricData>> byIdentity = context.getMetricDataList().stream()
|
||||
.filter(d -> d.getIdentity() != null)
|
||||
.collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList()));
|
||||
Map<String, byte[]> 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 "稳定";
|
||||
}
|
||||
}
|
||||
@ -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<String, List<MetricData>> byIdentity = context.getMetricDataList().stream()
|
||||
.filter(d -> d.getIdentity() != null)
|
||||
.collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList()));
|
||||
|
||||
// 2. 获取所有监测物模型(即使没有数据也要显示)
|
||||
Map<String, ThingsModel> 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;
|
||||
protected String[] primaryMetricKeywords() {
|
||||
return new String[]{"温度", "temperature"};
|
||||
}
|
||||
|
||||
// 3. 为每个物模型生成统计数据(包括无数据的物模型)
|
||||
for (Map.Entry<String, ThingsModel> entry : allModels.entrySet()) {
|
||||
String identity = entry.getKey();
|
||||
ThingsModel model = entry.getValue();
|
||||
List<MetricData> 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);
|
||||
@Override
|
||||
protected boolean allowAuxiliaryAsPrimary() {
|
||||
return true; // 温度计的主指标本身就是温度
|
||||
}
|
||||
|
||||
// 统一主指标口径:优先名称/标识符命中关键词的物模型,排除角度/温度/电压等辅助量
|
||||
String primaryIdentity = primaryMetricResolver.resolve(context, byIdentity, true, "温度", "temperature");
|
||||
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<MetricPointData> metricPoints = buildMetricPoints(result, context);
|
||||
result.setMetricPoints(metricPoints);
|
||||
|
||||
// 生成详细结论
|
||||
List<Conclusion> 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成项目概况总结
|
||||
*/
|
||||
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<MetricPointData> buildMetricPoints(AnalysisResult result, ReportContext context) {
|
||||
List<MetricPointData> points = new ArrayList<>();
|
||||
|
||||
for (Map.Entry<String, Statistics> 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<String, List<MetricData>> byIdentity = context.getMetricDataList().stream()
|
||||
.filter(d -> d.getIdentity() != null)
|
||||
.collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList()));
|
||||
Map<String, byte[]> 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 "上升";
|
||||
}
|
||||
|
||||
@ -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<String, List<MetricData>> byIdentity = context.getMetricDataList().stream()
|
||||
.filter(d -> d.getIdentity() != null)
|
||||
.collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList()));
|
||||
|
||||
// 2. 获取所有监测物模型(即使没有数据也要显示)
|
||||
Map<String, ThingsModel> 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;
|
||||
protected String[] primaryMetricKeywords() {
|
||||
return new String[]{"频率", "frequency"};
|
||||
}
|
||||
|
||||
// 3. 为每个物模型生成统计数据(包括无数据的物模型)
|
||||
for (Map.Entry<String, ThingsModel> entry : allModels.entrySet()) {
|
||||
String identity = entry.getKey();
|
||||
ThingsModel model = entry.getValue();
|
||||
List<MetricData> 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;
|
||||
}
|
||||
|
||||
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<MetricPointData> metricPoints = buildMetricPoints(result, context);
|
||||
result.setMetricPoints(metricPoints);
|
||||
|
||||
// 生成详细结论
|
||||
List<Conclusion> 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<MetricPointData> buildMetricPoints(AnalysisResult result, ReportContext context) {
|
||||
List<MetricPointData> points = new ArrayList<>();
|
||||
|
||||
for (Map.Entry<String, Statistics> 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<String, List<MetricData>> byIdentity = context.getMetricDataList().stream()
|
||||
.filter(d -> d.getIdentity() != null)
|
||||
.collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList()));
|
||||
Map<String, byte[]> 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 "稳定";
|
||||
}
|
||||
}
|
||||
@ -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<String, List<MetricData>> byIdentity = context.getMetricDataList().stream()
|
||||
.filter(d -> d.getIdentity() != null)
|
||||
.collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList()));
|
||||
|
||||
// 2. 获取所有监测物模型(即使没有数据也要显示)
|
||||
Map<String, ThingsModel> 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;
|
||||
protected String[] primaryMetricKeywords() {
|
||||
return new String[]{"水位", "液位", "water"};
|
||||
}
|
||||
|
||||
// 3. 为每个物模型生成统计数据(包括无数据的物模型)
|
||||
for (Map.Entry<String, ThingsModel> entry : allModels.entrySet()) {
|
||||
String identity = entry.getKey();
|
||||
ThingsModel model = entry.getValue();
|
||||
List<MetricData> 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());
|
||||
@Override
|
||||
protected String directionText(String direction) {
|
||||
if (TrendAnalysis.DIRECTION_UP.equals(direction)) {
|
||||
return "上升";
|
||||
}
|
||||
if (statistics.getUnit() == null || statistics.getUnit().isEmpty()) {
|
||||
statistics.setUnit(model.getUnit());
|
||||
if (TrendAnalysis.DIRECTION_DOWN.equals(direction)) {
|
||||
return "下降";
|
||||
}
|
||||
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);
|
||||
return "平稳";
|
||||
}
|
||||
|
||||
// 统一主指标口径:优先名称/标识符命中关键词的物模型,排除角度/温度/电压等辅助量
|
||||
String primaryIdentity = primaryMetricResolver.resolve(context, byIdentity, false, "水位", "液位", "water");
|
||||
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<MetricPointData> metricPoints = buildMetricPoints(result, context);
|
||||
result.setMetricPoints(metricPoints);
|
||||
|
||||
// 生成详细结论
|
||||
List<Conclusion> 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) {
|
||||
@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<MetricPointData> buildMetricPoints(AnalysisResult result, ReportContext context) {
|
||||
List<MetricPointData> points = new ArrayList<>();
|
||||
|
||||
for (Map.Entry<String, Statistics> 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<String, List<MetricData>> byIdentity = context.getMetricDataList().stream()
|
||||
.filter(d -> d.getIdentity() != null)
|
||||
.collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList()));
|
||||
Map<String, byte[]> 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 "平稳";
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -293,6 +293,9 @@ public class WordRenderer implements ReportRenderer {
|
||||
if (WarningAnalysis.STATUS_WARNING.equals(status)) {
|
||||
return "预警";
|
||||
}
|
||||
if (WarningAnalysis.STATUS_UNKNOWN.equals(status)) {
|
||||
return "待评估";
|
||||
}
|
||||
return "正常";
|
||||
}
|
||||
|
||||
|
||||
@ -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<ReportThreshold> {
|
||||
|
||||
/**
|
||||
* 查询阈值列表(分页)
|
||||
*
|
||||
* @param query 查询条件
|
||||
* @return 阈值列表
|
||||
*/
|
||||
List<ReportThreshold> 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);
|
||||
}
|
||||
@ -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<ReportThresholdMapper, ReportThreshold>
|
||||
implements IReportThresholdService {
|
||||
|
||||
@Override
|
||||
public List<ReportThreshold> selectThresholdList(ReportThresholdQuery query) {
|
||||
LambdaQueryWrapper<ReportThreshold> 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));
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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, '五、趋势分析')
|
||||
|
||||
55
sql/init_report_threshold.sql
Normal file
55
sql/init_report_threshold.sql
Normal file
@ -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. 预警值 < 报警值 < 控制值,系统会按当前值与阈值的比较自动判断安全等级。
|
||||
77
sql/init_threshold_with_table.sql
Normal file
77
sql/init_threshold_with_table.sql
Normal file
@ -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
|
||||
-- ============================================================
|
||||
37
sql/menu_threshold.sql
Normal file
37
sql/menu_threshold.sql
Normal file
@ -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
|
||||
-- ============================================================
|
||||
69
yack-web/src/api/iot/threshold.js
Normal file
69
yack-web/src/api/iot/threshold.js
Normal file
@ -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 }
|
||||
})
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user