20260910:导出报告优化v2版本

This commit is contained in:
zhugongping 2026-09-10 21:00:05 +08:00
parent 0fdcf8dad7
commit c050141b17
36 changed files with 1739 additions and 3605 deletions

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -31,8 +31,8 @@ public class WarningAnalyzer {
return result; return result;
} }
if (threshold == null) { if (threshold == null) {
result.setStatus(WarningAnalysis.STATUS_NORMAL); result.setStatus(WarningAnalysis.STATUS_UNKNOWN);
result.setDescription("未配置结构化阈值,暂按正常处理"); result.setDescription("未配置预警阈值,无法评估安全等级");
return result; return result;
} }

View File

@ -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 "正常";
}
}

View File

@ -1,360 +1,36 @@
package com.fastbee.report.analyzer.axial_force; package com.fastbee.report.analyzer.axial_force;
import com.fastbee.common.enums.MonitorCategory; import com.fastbee.common.enums.MonitorCategory;
import com.fastbee.common.utils.StringUtils; import com.fastbee.report.analyzer.AbstractDeviceAnalyzer;
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.model.Statistics; import com.fastbee.report.model.Statistics;
import com.fastbee.report.model.TrendAnalysis; import com.fastbee.report.model.TrendAnalysis;
import com.fastbee.report.model.WarningAnalysis; import com.fastbee.report.model.WarningAnalysis;
import com.fastbee.report.period.TimeWindow; import com.fastbee.report.period.TimeWindow;
import com.fastbee.report.threshold.ReportThreshold;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.math.BigDecimal; 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 * @author FastBee
* @date 2026-08-23 * @date 2026-08-23
*/ */
@Component @Component
public class AxialForceAnalyzer implements ReportAnalyzer { public class AxialForceAnalyzer extends AbstractDeviceAnalyzer {
@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;
@Override @Override
public boolean supports(MonitorCategory category) { protected MonitorCategory supportedCategory() {
return category == MonitorCategory.AXIAL_FORCE; return MonitorCategory.AXIAL_FORCE;
} }
@Override @Override
public AnalysisResult analyze(ReportContext context, ReportRequest request) { protected String[] primaryMetricKeywords() {
AnalysisResult result = new AnalysisResult(); return new String[]{"轴力", "应力", "force", "stress"};
// 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. 为每个物模型生成统计数据(包括无数据的物模型) @Override
for (Map.Entry<String, ThingsModel> entry : allModels.entrySet()) { protected String directionText(String direction) {
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) {
if (TrendAnalysis.DIRECTION_UP.equals(direction)) { if (TrendAnalysis.DIRECTION_UP.equals(direction)) {
return "增大"; return "增大";
} }
@ -363,4 +39,70 @@ public class AxialForceAnalyzer implements ReportAnalyzer {
} }
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());
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();
}
} }

View File

@ -2,354 +2,36 @@ package com.fastbee.report.analyzer.crack;
import com.fastbee.common.enums.MonitorCategory; import com.fastbee.common.enums.MonitorCategory;
import com.fastbee.common.utils.StringUtils; import com.fastbee.common.utils.StringUtils;
import com.fastbee.iot.domain.ThingsModel; import com.fastbee.report.analyzer.AbstractDeviceAnalyzer;
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.model.Statistics; import com.fastbee.report.model.Statistics;
import com.fastbee.report.model.TrendAnalysis; import com.fastbee.report.model.TrendAnalysis;
import com.fastbee.report.model.WarningAnalysis; import com.fastbee.report.model.WarningAnalysis;
import com.fastbee.report.period.TimeWindow; import com.fastbee.report.period.TimeWindow;
import com.fastbee.report.threshold.ReportThreshold;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.math.BigDecimal; 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 * @author FastBee
* @date 2026-08-16 * @date 2026-08-16
*/ */
@Component @Component
public class CrackMeterAnalyzer implements ReportAnalyzer { public class CrackMeterAnalyzer extends AbstractDeviceAnalyzer {
@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;
@Override @Override
public boolean supports(MonitorCategory category) { protected MonitorCategory supportedCategory() {
return category == MonitorCategory.CRACK_METER; return MonitorCategory.CRACK_METER;
} }
@Override @Override
public AnalysisResult analyze(ReportContext context, ReportRequest request) { protected String[] primaryMetricKeywords() {
AnalysisResult result = new AnalysisResult(); return new String[]{"裂缝宽度", "缝宽", "width"};
// 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. 为每个物模型生成统计数据(包括无数据的物模型) @Override
for (Map.Entry<String, ThingsModel> entry : allModels.entrySet()) { protected String directionText(String direction) {
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) {
if (TrendAnalysis.DIRECTION_UP.equals(direction)) { if (TrendAnalysis.DIRECTION_UP.equals(direction)) {
return "张开"; return "张开";
} }
@ -358,4 +40,71 @@ public class CrackMeterAnalyzer implements ReportAnalyzer {
} }
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());
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();
}
} }

View File

@ -2,39 +2,14 @@ package com.fastbee.report.analyzer.deep_displacement;
import com.fastbee.common.enums.MonitorCategory; import com.fastbee.common.enums.MonitorCategory;
import com.fastbee.common.utils.StringUtils; import com.fastbee.common.utils.StringUtils;
import com.fastbee.iot.domain.ThingsModel; import com.fastbee.report.analyzer.AbstractDeviceAnalyzer;
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.model.Statistics; import com.fastbee.report.model.Statistics;
import com.fastbee.report.model.TrendAnalysis; import com.fastbee.report.model.TrendAnalysis;
import com.fastbee.report.model.WarningAnalysis; import com.fastbee.report.model.WarningAnalysis;
import com.fastbee.report.period.TimeWindow; import com.fastbee.report.period.TimeWindow;
import com.fastbee.report.threshold.ReportThreshold;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.math.BigDecimal; 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 * @date 2026-08-23
*/ */
@Component @Component
public class DeepDisplacementAnalyzer implements ReportAnalyzer { public class DeepDisplacementAnalyzer extends AbstractDeviceAnalyzer {
@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;
@Override @Override
public boolean supports(MonitorCategory category) { protected MonitorCategory supportedCategory() {
return category == MonitorCategory.DEEP_DISPLACEMENT; return MonitorCategory.DEEP_DISPLACEMENT;
} }
@Override @Override
public AnalysisResult analyze(ReportContext context, ReportRequest request) { protected String[] primaryMetricKeywords() {
AnalysisResult result = new AnalysisResult(); return new String[]{"深部位移", "位移", "displacement", "沉降", "settlement"};
// 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. 为每个物模型生成统计数据(包括无数据的物模型) @Override
for (Map.Entry<String, ThingsModel> entry : allModels.entrySet()) { protected String directionText(String direction) {
String identity = entry.getKey(); // 方向语义:中性描述,不臆测坡体方向
ThingsModel model = entry.getValue(); if (TrendAnalysis.DIRECTION_UP.equals(direction)) {
List<MetricData> data = byIdentity.getOrDefault(identity, new ArrayList<>()); return "位移增大方向";
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()) { if (TrendAnalysis.DIRECTION_DOWN.equals(direction)) {
statistics.setUnit(model.getUnit()); return "位移减小方向";
} }
statistics.setIdentifier(identity); return "稳定";
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
String primaryIdentity = primaryMetricResolver.resolve(context, byIdentity, protected String buildSummary(Statistics statistics, TrendAnalysis trend, String unit, TimeWindow window) {
"深部位移", "位移", "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) {
if (statistics == null || statistics.getCount() == 0) { if (statistics == null || statistics.getCount() == 0) {
return "统计期内无有效监测数据。"; return "统计期内无有效监测数据。";
} }
String direction = trend == null ? "待分析" : directionText(trend.getDirection()); String direction = trend == null ? "待分析" : directionText(trend.getDirection());
BigDecimal rate = rateCalculator.ratePerDay(statistics, String formattedCurrent = valueFormatter.format(statistics.getCurrent(), supportedCategory());
Duration.between(window.getStartTime(), window.getEndTime()).getSeconds()); 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( return String.format(
"本期深部位移监测数据显示:当前位移为%s%s,较初始值累计变化%s%s,位移速率为%s%s/天,位移方向呈%s趋势。" + "本期深部位移监测数据显示:当前位移为%s%s,较初始值累计变化%s%s,变化速率为%s,位移呈%s趋势。" +
"统计期内最大位移%s%s,最小位移%s%s,平均位移%s%s。土体内部变形监测正常。", "统计期内最大位移%s%s,最小位移%s%s,平均位移%s%s。",
statistics.getCurrent().toPlainString(), unit, formattedCurrent, resolvedUnit,
statistics.getDelta().toPlainString(), unit, formattedDelta, resolvedUnit,
rate.toPlainString(), unit, rateText,
direction, direction,
statistics.getMax().toPlainString(), unit, formattedMax, resolvedUnit,
statistics.getMin().toPlainString(), unit, formattedMin, resolvedUnit,
statistics.getAvg().toPlainString(), unit formattedAvg, resolvedUnit
); );
} }
/** @Override
* 生成专业分析建议 protected String buildProfessionalAdvice(Statistics statistics, TrendAnalysis trend, WarningAnalysis warning, String unit) {
*/
private String buildProfessionalAdvice(Statistics statistics, TrendAnalysis trend, WarningAnalysis warning, String unit) {
if (statistics == null || statistics.getCount() == 0) { if (statistics == null || statistics.getCount() == 0) {
return "建议检查设备上报状态,确保数据采集正常。"; return "建议检查设备上报状态,确保数据采集正常。";
} }
@ -262,116 +86,49 @@ public class DeepDisplacementAnalyzer implements ReportAnalyzer {
StringBuilder advice = new StringBuilder(); StringBuilder advice = new StringBuilder();
advice.append("专业分析:"); advice.append("专业分析:");
// 基于趋势方向给出建议 // 基于趋势方向给出建议(结合边坡监测规范)
if (trend != null) { if (trend != null) {
if (TrendAnalysis.DIRECTION_UP.equals(trend.getDirection())) { 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())) { } else if (TrendAnalysis.DIRECTION_DOWN.equals(trend.getDirection())) {
advice.append("深部位移呈减小趋势,土体变形趋于稳定。"); advice.append("深部位移呈减小趋势,土体变形趋于稳定。");
} else { } else {
advice.append("深部位移变化稳定,土体内部变形正常。"); advice.append("深部位移变化稳定。");
} }
} }
// 基于位移量级给出建议 // 基于累计位移量级给出建议
if (statistics.getDelta().abs().compareTo(new BigDecimal("10")) > 0) { BigDecimal absDelta = statistics.getDelta().abs();
advice.append("累计位移量较大,建议加密监测频率,结合测斜孔剖面图分析位移沿深度分布规律。"); 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 (warning != null && WarningAnalysis.STATUS_UNKNOWN.equals(warning.getStatus())) {
if (WarningAnalysis.STATUS_ALARM.equals(warning.getStatus())) { advice.append("【提示】尚未配置控制值,建议根据设计文件在系统中设置位移控制值,以便自动评估安全等级。");
} else if (warning != null && WarningAnalysis.STATUS_ALARM.equals(warning.getStatus())) {
advice.append("当前处于报警状态,建议立即启动应急预案,组织专业人员进行地质灾害评估,必要时采取加固措施。"); advice.append("当前处于报警状态,建议立即启动应急预案,组织专业人员进行地质灾害评估,必要时采取加固措施。");
} else if (WarningAnalysis.STATUS_WARNING.equals(warning.getStatus())) { } else if (warning != null && WarningAnalysis.STATUS_WARNING.equals(warning.getStatus())) {
advice.append("当前处于预警状态,建议加强巡视检查,准备应急物资,制定应急预案。"); advice.append("当前处于预警状态,建议加强巡视检查,准备应急物资,制定应急预案。");
} }
}
return advice.toString(); 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 "稳定";
}
} }

View File

@ -1,41 +1,13 @@
package com.fastbee.report.analyzer.inclinometer; package com.fastbee.report.analyzer.inclinometer;
import com.fastbee.common.enums.MonitorCategory; import com.fastbee.common.enums.MonitorCategory;
import com.fastbee.common.utils.StringUtils; import com.fastbee.report.analyzer.AbstractDeviceAnalyzer;
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.model.Statistics; import com.fastbee.report.model.Statistics;
import com.fastbee.report.model.TrendAnalysis; import com.fastbee.report.model.TrendAnalysis;
import com.fastbee.report.model.WarningAnalysis; import com.fastbee.report.model.WarningAnalysis;
import com.fastbee.report.period.TimeWindow; import com.fastbee.report.period.TimeWindow;
import com.fastbee.report.threshold.ReportThreshold;
import org.springframework.stereotype.Component; 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 * @date 2026-08-23
*/ */
@Component @Component
public class InclinometerAnalyzer implements ReportAnalyzer { public class InclinometerAnalyzer extends AbstractDeviceAnalyzer {
@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;
@Override @Override
public boolean supports(MonitorCategory category) { protected MonitorCategory supportedCategory() {
return category == MonitorCategory.INCLINOMETER; return MonitorCategory.INCLINOMETER;
} }
@Override @Override
public AnalysisResult analyze(ReportContext context, ReportRequest request) { protected String[] primaryMetricKeywords() {
AnalysisResult result = new AnalysisResult(); return new String[]{"倾角", "角度", "angle", "inclin"};
// 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. 为每个物模型生成统计数据(包括无数据的物模型) @Override
for (Map.Entry<String, ThingsModel> entry : allModels.entrySet()) { protected boolean allowAuxiliaryAsPrimary() {
String identity = entry.getKey(); return true; // 倾角计的主指标本身就是角度
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
String primaryIdentity = primaryMetricResolver.resolve(context, byIdentity, true, "倾角", "角度", "angle", "inclin"); protected String directionText(String direction) {
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) {
if (TrendAnalysis.DIRECTION_UP.equals(direction)) { if (TrendAnalysis.DIRECTION_UP.equals(direction)) {
return "倾斜增大"; return "倾斜增大";
} }

View File

@ -1,40 +1,14 @@
package com.fastbee.report.analyzer.rain_gauge; package com.fastbee.report.analyzer.rain_gauge;
import com.fastbee.common.enums.MonitorCategory; import com.fastbee.common.enums.MonitorCategory;
import com.fastbee.common.utils.StringUtils; import com.fastbee.report.analyzer.AbstractDeviceAnalyzer;
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.model.Statistics; import com.fastbee.report.model.Statistics;
import com.fastbee.report.model.TrendAnalysis; import com.fastbee.report.model.TrendAnalysis;
import com.fastbee.report.model.WarningAnalysis; import com.fastbee.report.model.WarningAnalysis;
import com.fastbee.report.period.TimeWindow; import com.fastbee.report.period.TimeWindow;
import com.fastbee.report.threshold.ReportThreshold;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.math.BigDecimal; 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 * @date 2026-08-23
*/ */
@Component @Component
public class RainGaugeAnalyzer implements ReportAnalyzer { public class RainGaugeAnalyzer extends AbstractDeviceAnalyzer {
@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;
@Override @Override
public boolean supports(MonitorCategory category) { protected MonitorCategory supportedCategory() {
return category == MonitorCategory.RAIN_GAUGE; return MonitorCategory.RAIN_GAUGE;
} }
@Override @Override
public AnalysisResult analyze(ReportContext context, ReportRequest request) { protected String[] primaryMetricKeywords() {
AnalysisResult result = new AnalysisResult(); return new String[]{"雨量", "降雨", "降水量", "rain"};
// 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. 为每个物模型生成统计数据(包括无数据的物模型) @Override
for (Map.Entry<String, ThingsModel> entry : allModels.entrySet()) { protected String directionText(String direction) {
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) {
if (TrendAnalysis.DIRECTION_UP.equals(direction)) { if (TrendAnalysis.DIRECTION_UP.equals(direction)) {
return "增强"; return "增强";
} }
@ -359,4 +39,66 @@ public class RainGaugeAnalyzer implements ReportAnalyzer {
} }
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());
// 累计降雨量 = 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();
}
} }

View File

@ -1,41 +1,13 @@
package com.fastbee.report.analyzer.ranging_meter; package com.fastbee.report.analyzer.ranging_meter;
import com.fastbee.common.enums.MonitorCategory; import com.fastbee.common.enums.MonitorCategory;
import com.fastbee.common.utils.StringUtils; import com.fastbee.report.analyzer.AbstractDeviceAnalyzer;
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.model.Statistics; import com.fastbee.report.model.Statistics;
import com.fastbee.report.model.TrendAnalysis; import com.fastbee.report.model.TrendAnalysis;
import com.fastbee.report.model.WarningAnalysis; import com.fastbee.report.model.WarningAnalysis;
import com.fastbee.report.period.TimeWindow; import com.fastbee.report.period.TimeWindow;
import com.fastbee.report.threshold.ReportThreshold;
import org.springframework.stereotype.Component; 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 * @date 2026-08-23
*/ */
@Component @Component
public class RangingMeterAnalyzer implements ReportAnalyzer { public class RangingMeterAnalyzer extends AbstractDeviceAnalyzer {
@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;
@Override @Override
public boolean supports(MonitorCategory category) { protected MonitorCategory supportedCategory() {
return category == MonitorCategory.RANGING_METER; return MonitorCategory.RANGING_METER;
} }
@Override @Override
public AnalysisResult analyze(ReportContext context, ReportRequest request) { protected String[] primaryMetricKeywords() {
AnalysisResult result = new AnalysisResult(); return new String[]{"距离", "位移", "distance"};
// 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 "稳定";
} }
} }

View File

@ -2,253 +2,110 @@ package com.fastbee.report.analyzer.static_level;
import com.fastbee.common.enums.MonitorCategory; import com.fastbee.common.enums.MonitorCategory;
import com.fastbee.common.utils.StringUtils; import com.fastbee.common.utils.StringUtils;
import com.fastbee.iot.domain.ThingsModel; import com.fastbee.report.analyzer.AbstractDeviceAnalyzer;
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.AnalysisResult;
import com.fastbee.report.model.Conclusion; 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.Statistics;
import com.fastbee.report.model.TrendAnalysis; import com.fastbee.report.model.TrendAnalysis;
import com.fastbee.report.model.WarningAnalysis; import com.fastbee.report.model.WarningAnalysis;
import com.fastbee.report.period.TimeWindow; import com.fastbee.report.period.TimeWindow;
import com.fastbee.report.threshold.ReportThreshold; import com.fastbee.report.core.ReportContext;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.Duration;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List; 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 * @author FastBee
* @date 2026-08-23 * @date 2026-08-23
*/ */
@Component @Component
public class StaticLevelAnalyzer implements ReportAnalyzer { public class StaticLevelAnalyzer extends AbstractDeviceAnalyzer {
@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;
@Override @Override
public boolean supports(MonitorCategory category) { protected MonitorCategory supportedCategory() {
return category == MonitorCategory.STATIC_LEVEL; return MonitorCategory.STATIC_LEVEL;
} }
@Override @Override
public AnalysisResult analyze(ReportContext context, ReportRequest request) { protected String[] primaryMetricKeywords() {
AnalysisResult result = new AnalysisResult(); // 静力水准物模型通常叫"水位"(实际是高程),用沉降/高程关键词匹配
return new String[]{"沉降", "高程", "settlement", "水位"};
// 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. 为每个物模型生成统计数据(包括无数据的物模型) @Override
for (Map.Entry<String, ThingsModel> entry : allModels.entrySet()) { protected String directionText(String direction) {
String identity = entry.getKey(); // 方向语义:UP/DOWN 指高程变化方向,映射到沉降术语
ThingsModel model = entry.getValue(); if (TrendAnalysis.DIRECTION_UP.equals(direction)) {
List<MetricData> data = byIdentity.getOrDefault(identity, new ArrayList<>()); return "回弹隆起"; // 高程上升
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()) { if (TrendAnalysis.DIRECTION_DOWN.equals(direction)) {
statistics.setUnit(model.getUnit()); return "持续沉降"; // 高程下降
} }
statistics.setIdentifier(identity); return "稳定";
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);
} }
/** /**
* 生成项目概况总结 * 判断沉降类型(高程变化方向 → 沉降术语)
*/ */
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) { if (statistics == null || statistics.getCount() == 0) {
return "统计期内无有效监测数据。"; return "统计期内无有效监测数据。";
} }
String direction = trend == null ? "待分析" : directionText(trend.getDirection()); 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 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( return String.format(
"本期静力水准监测数据显示:当前高程为%s%s,较初始值变化%s%s(%s),变化速率为%s%s/天,结构呈%s趋势。" + "本期静力水准监测数据显示:当前高程为%s%s,较初始值变化%s%s(%s),变化速率为%s,结构呈%s趋势。" +
"统计期内最大沉降量%s%s,最小沉降量%s%s,平均沉降量%s%s。", "统计期内最大高程%s%s,最小高程%s%s,平均高程%s%s。",
statistics.getCurrent().toPlainString(), unit, formattedCurrent, resolvedUnit,
statistics.getDelta().toPlainString(), unit, settlementType, formattedDelta, resolvedUnit, settlementType,
rate.toPlainString(), unit, rateText,
direction, direction,
statistics.getMax().toPlainString(), unit, formattedMax, resolvedUnit,
statistics.getMin().toPlainString(), unit, formattedMin, resolvedUnit,
statistics.getAvg().toPlainString(), unit formattedAvg, resolvedUnit
); );
} }
/** @Override
* 生成专业分析建议 protected String buildProfessionalAdvice(Statistics statistics, TrendAnalysis trend, WarningAnalysis warning, String unit) {
*/
private String buildProfessionalAdvice(Statistics statistics, TrendAnalysis trend, WarningAnalysis warning,
String unit, Map<String, Statistics> allStatistics, TimeWindow window) {
if (statistics == null || statistics.getCount() == 0) { if (statistics == null || statistics.getCount() == 0) {
return "建议检查设备上报状态,确保数据采集正常。"; return "建议检查设备上报状态,确保数据采集正常。";
} }
@ -259,108 +116,64 @@ public class StaticLevelAnalyzer implements ReportAnalyzer {
// 基于趋势方向给出建议 // 基于趋势方向给出建议
if (trend != null) { if (trend != null) {
if (TrendAnalysis.DIRECTION_UP.equals(trend.getDirection())) { 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())) { } 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 { } else {
advice.append("沉降变化稳定,结构状态正常。"); advice.append("沉降变化稳定,结构状态正常。");
} }
} }
// 基于沉降量给出建议
BigDecimal absDelta = statistics.getDelta().abs(); BigDecimal absDelta = statistics.getDelta().abs();
if (absDelta.compareTo(new BigDecimal("10")) > 0) { 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) { if (warning != null && WarningAnalysis.STATUS_UNKNOWN.equals(warning.getStatus())) {
BigDecimal rate = rateCalculator.ratePerDay(statistics, advice.append("【提示】尚未配置控制值,建议根据设计文件在系统中设置沉降控制值,以便自动评估安全等级。");
Duration.between(window.getStartTime(), window.getEndTime()).getSeconds()); } else if (warning != null && WarningAnalysis.STATUS_ALARM.equals(warning.getStatus())) {
if (rate != null && rate.compareTo(new BigDecimal("2")) > 0) { advice.append("当前处于报警状态,建议立即采取地基加固措施,组织专业人员进行安全评估。");
advice.append(String.format("沉降速率较快(%.2fmm/天),需加密监测频率。", rate.doubleValue())); } else if (warning != null && WarningAnalysis.STATUS_WARNING.equals(warning.getStatus())) {
}
}
// 不均匀沉降分析
if (allStatistics != null && allStatistics.size() > 1) {
String differentialAdvice = analyzeDifferentialSettlement(allStatistics);
if (StringUtils.isNotEmpty(differentialAdvice)) {
advice.append(differentialAdvice);
}
}
// 基于预警状态给出建议
if (warning != null) {
if (WarningAnalysis.STATUS_ALARM.equals(warning.getStatus())) {
advice.append("当前处于报警状态,建议立即采取地基加固措施,并组织专业人员进行安全评估。");
} else if (WarningAnalysis.STATUS_WARNING.equals(warning.getStatus())) {
advice.append("当前处于预警状态,建议加密监测频率,制定应急预案。"); advice.append("当前处于预警状态,建议加密监测频率,制定应急预案。");
} }
}
return advice.toString(); return advice.toString();
} }
/** @Override
* 将预警状态转换为安全等级 protected void addDomainConclusions(List<Conclusion> conclusions, AnalysisResult result, ReportContext context) {
*/ // 添加不均匀沉降评估(多测点场景)
private String convertWarningToSafetyLevel(String warningStatus) { String differential = assessDifferentialSettlement(result.getStatistics());
if (WarningAnalysis.STATUS_ALARM.equals(warningStatus)) { if (StringUtils.isNotEmpty(differential)) {
return "DANGER"; conclusions.add(new Conclusion(conclusions.size() + 1, differential));
} 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 String assessDifferentialSettlement(Map<String, Statistics> allStatistics) { private String assessDifferentialSettlement(java.util.Map<String, Statistics> allStatistics) {
if (allStatistics == null || allStatistics.size() < 2) { if (allStatistics == null || allStatistics.size() < 2) {
return null; return null;
} }
@ -383,88 +196,11 @@ public class StaticLevelAnalyzer implements ReportAnalyzer {
if (maxDelta != null && minDelta != null) { if (maxDelta != null && minDelta != null) {
BigDecimal differential = maxDelta.subtract(minDelta); BigDecimal differential = maxDelta.subtract(minDelta);
if (differential.compareTo(new BigDecimal("5")) > 0) { if (differential.compareTo(new BigDecimal("5")) > 0) {
return String.format("存在不均匀沉降,测点间最大沉降差为%s%s,建议关注结构整体稳定性。", return String.format("存在不均匀沉降,测点间最大沉降差为%s mm,建议关注结构整体稳定性。",
differential.toPlainString(), "mm"); valueFormatter.format(differential, supportedCategory()));
} }
} }
return null; 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 "稳定";
}
} }

View File

@ -1,40 +1,14 @@
package com.fastbee.report.analyzer.strain_gauge; package com.fastbee.report.analyzer.strain_gauge;
import com.fastbee.common.enums.MonitorCategory; import com.fastbee.common.enums.MonitorCategory;
import com.fastbee.common.utils.StringUtils; import com.fastbee.report.analyzer.AbstractDeviceAnalyzer;
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.model.Statistics; import com.fastbee.report.model.Statistics;
import com.fastbee.report.model.TrendAnalysis; import com.fastbee.report.model.TrendAnalysis;
import com.fastbee.report.model.WarningAnalysis; import com.fastbee.report.model.WarningAnalysis;
import com.fastbee.report.period.TimeWindow; import com.fastbee.report.period.TimeWindow;
import com.fastbee.report.threshold.ReportThreshold;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.math.BigDecimal; 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 * @date 2026-08-23
*/ */
@Component @Component
public class StrainGaugeAnalyzer implements ReportAnalyzer { public class StrainGaugeAnalyzer extends AbstractDeviceAnalyzer {
@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;
@Override @Override
public boolean supports(MonitorCategory category) { protected MonitorCategory supportedCategory() {
return category == MonitorCategory.STRAIN_GAUGE; return MonitorCategory.STRAIN_GAUGE;
} }
@Override @Override
public AnalysisResult analyze(ReportContext context, ReportRequest request) { protected String[] primaryMetricKeywords() {
AnalysisResult result = new AnalysisResult(); return new String[]{"应变", "strain"};
// 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. 为每个物模型生成统计数据(包括无数据的物模型) @Override
for (Map.Entry<String, ThingsModel> entry : allModels.entrySet()) { protected String buildSummary(Statistics statistics, TrendAnalysis trend, String unit, TimeWindow window) {
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) {
if (statistics == null || statistics.getCount() == 0) { if (statistics == null || statistics.getCount() == 0) {
return "统计期内无有效监测数据。"; return "统计期内无有效监测数据。";
} }
String strainState = (statistics.getCurrent() != null && statistics.getCurrent().compareTo(BigDecimal.ZERO) >= 0) ? "拉应变" : "压应变";
String direction = trend == null ? "待分析" : directionText(trend.getDirection()); String direction = trend == null ? "待分析" : directionText(trend.getDirection());
BigDecimal rate = rateCalculator.ratePerDay(statistics, String strainState = statistics.getCurrent().compareTo(BigDecimal.ZERO) >= 0 ? "拉应变" : "压应变";
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( return String.format(
"本期应变监测数据显示:当前应变为%s%s,处于%s状态,较初始值变化%s%s,应变速率为%s%s/天,应变呈%s趋势。" + "本期应变监测数据显示:当前应变为%s%s,处于%s状态,较初始值变化%s%s,变化速率为%s,结构呈%s趋势。" +
"统计期内最大应变%s%s,最小应变%s%s,平均应变%s%s。结构变形监测正常。", "统计期内最大应变%s%s,最小应变%s%s,平均应变%s%s。",
statistics.getCurrent().toPlainString(), unit, formattedCurrent, resolvedUnit,
strainState, strainState,
statistics.getDelta().toPlainString(), unit, formattedDelta, resolvedUnit,
rate.toPlainString(), unit, rateText,
direction, direction,
statistics.getMax().toPlainString(), unit, formattedMax, resolvedUnit,
statistics.getMin().toPlainString(), unit, formattedMin, resolvedUnit,
statistics.getAvg().toPlainString(), unit 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 "稳定";
}
} }

View File

@ -1,41 +1,12 @@
package com.fastbee.report.analyzer.thermometer; package com.fastbee.report.analyzer.thermometer;
import com.fastbee.common.enums.MonitorCategory; import com.fastbee.common.enums.MonitorCategory;
import com.fastbee.common.utils.StringUtils; import com.fastbee.report.analyzer.AbstractDeviceAnalyzer;
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.model.Statistics; import com.fastbee.report.model.Statistics;
import com.fastbee.report.model.TrendAnalysis; import com.fastbee.report.model.TrendAnalysis;
import com.fastbee.report.model.WarningAnalysis;
import com.fastbee.report.period.TimeWindow; import com.fastbee.report.period.TimeWindow;
import com.fastbee.report.threshold.ReportThreshold;
import org.springframework.stereotype.Component; 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 * @date 2026-08-23
*/ */
@Component @Component
public class ThermometerAnalyzer implements ReportAnalyzer { public class ThermometerAnalyzer extends AbstractDeviceAnalyzer {
@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;
@Override @Override
public boolean supports(MonitorCategory category) { protected MonitorCategory supportedCategory() {
return category == MonitorCategory.THERMOMETER; return MonitorCategory.THERMOMETER;
} }
@Override @Override
public AnalysisResult analyze(ReportContext context, ReportRequest request) { protected String[] primaryMetricKeywords() {
AnalysisResult result = new AnalysisResult(); return new String[]{"温度", "temperature"};
// 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. 为每个物模型生成统计数据(包括无数据的物模型) @Override
for (Map.Entry<String, ThingsModel> entry : allModels.entrySet()) { protected boolean allowAuxiliaryAsPrimary() {
String identity = entry.getKey(); return true; // 温度计的主指标本身就是温度
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
String primaryIdentity = primaryMetricResolver.resolve(context, byIdentity, true, "温度", "temperature"); protected String directionText(String direction) {
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) {
if (TrendAnalysis.DIRECTION_UP.equals(direction)) { if (TrendAnalysis.DIRECTION_UP.equals(direction)) {
return "上升"; return "上升";
} }

View File

@ -1,244 +1,36 @@
package com.fastbee.report.analyzer.vibrating_wire; package com.fastbee.report.analyzer.vibrating_wire;
import com.fastbee.common.enums.MonitorCategory; import com.fastbee.common.enums.MonitorCategory;
import com.fastbee.common.utils.StringUtils; import com.fastbee.report.analyzer.AbstractDeviceAnalyzer;
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.model.Statistics; import com.fastbee.report.model.Statistics;
import com.fastbee.report.model.TrendAnalysis; import com.fastbee.report.model.TrendAnalysis;
import com.fastbee.report.model.WarningAnalysis; import com.fastbee.report.model.WarningAnalysis;
import com.fastbee.report.period.TimeWindow; import com.fastbee.report.period.TimeWindow;
import com.fastbee.report.threshold.ReportThreshold;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.math.BigDecimal; 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 * @author FastBee
* @date 2026-08-23 * @date 2026-08-23
*/ */
@Component @Component
public class VibratingWireAnalyzer implements ReportAnalyzer { public class VibratingWireAnalyzer extends AbstractDeviceAnalyzer {
@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;
@Override @Override
public boolean supports(MonitorCategory category) { protected MonitorCategory supportedCategory() {
return category == MonitorCategory.VIBRATING_WIRE; return MonitorCategory.VIBRATING_WIRE;
} }
@Override @Override
public AnalysisResult analyze(ReportContext context, ReportRequest request) { protected String[] primaryMetricKeywords() {
AnalysisResult result = new AnalysisResult(); return new String[]{"频率", "frequency"};
// 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. 为每个物模型生成统计数据(包括无数据的物模型) @Override
for (Map.Entry<String, ThingsModel> entry : allModels.entrySet()) { protected String buildProfessionalAdvice(Statistics statistics, TrendAnalysis trend, WarningAnalysis warning, String unit) {
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) {
if (statistics == null || statistics.getCount() == 0) { if (statistics == null || statistics.getCount() == 0) {
return "建议检查设备上报状态,确保数据采集正常。"; return "建议检查设备上报状态,确保数据采集正常。";
} }
@ -246,115 +38,28 @@ public class VibratingWireAnalyzer implements ReportAnalyzer {
StringBuilder advice = new StringBuilder(); StringBuilder advice = new StringBuilder();
advice.append("专业分析:"); advice.append("专业分析:");
// 基于趋势方向给出建议
if (trend != null) { if (trend != null) {
if (TrendAnalysis.DIRECTION_UP.equals(trend.getDirection())) { if (TrendAnalysis.DIRECTION_UP.equals(trend.getDirection())) {
advice.append("振弦频率呈增大趋势,应力状态发生变化,需结合温度补偿计算实际应力值。"); advice.append("频率呈增大趋势。");
} else if (TrendAnalysis.DIRECTION_DOWN.equals(trend.getDirection())) { } else if (TrendAnalysis.DIRECTION_DOWN.equals(trend.getDirection())) {
advice.append("振弦频率呈减小趋势,结构应力状态趋于松弛。"); advice.append("频率呈减小趋势。");
} else { } else {
advice.append("振弦频率变化稳定,结构应力状态正常。"); advice.append("频率变化稳定。");
} }
} }
// 基于频率变化量级给出建议
if (statistics.getDelta().abs().compareTo(new BigDecimal("50")) > 0) { if (statistics.getDelta().abs().compareTo(new BigDecimal("50")) > 0) {
advice.append("频率变化较大,建议检查传感器零点漂移,进行温度补偿修正。"); advice.append("频率变化较大,建议检查零点漂移和温度补偿是否正常。");
} }
// 基于预警状态给出建议
if (warning != null) { if (warning != null) {
if (WarningAnalysis.STATUS_ALARM.equals(warning.getStatus())) { if (WarningAnalysis.STATUS_ALARM.equals(warning.getStatus())) {
advice.append("当前处于报警状态,建议立即校验传感器状态,分析应力超限原因,必要时采取加固措施。"); advice.append("当前处于报警状态,建议立即检查传感器工作状态。");
} else if (WarningAnalysis.STATUS_WARNING.equals(warning.getStatus())) { } else if (WarningAnalysis.STATUS_WARNING.equals(warning.getStatus())) {
advice.append("当前处于预警状态,建议加密监测频率,准备应急预案。"); advice.append("当前处于预警状态,建议加密监测频率。");
} }
} }
return advice.toString(); 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 "稳定";
}
} }

View File

@ -2,39 +2,14 @@ package com.fastbee.report.analyzer.water_level;
import com.fastbee.common.enums.MonitorCategory; import com.fastbee.common.enums.MonitorCategory;
import com.fastbee.common.utils.StringUtils; import com.fastbee.common.utils.StringUtils;
import com.fastbee.iot.domain.ThingsModel; import com.fastbee.report.analyzer.AbstractDeviceAnalyzer;
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.model.Statistics; import com.fastbee.report.model.Statistics;
import com.fastbee.report.model.TrendAnalysis; import com.fastbee.report.model.TrendAnalysis;
import com.fastbee.report.model.WarningAnalysis; import com.fastbee.report.model.WarningAnalysis;
import com.fastbee.report.period.TimeWindow; import com.fastbee.report.period.TimeWindow;
import com.fastbee.report.threshold.ReportThreshold;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.math.BigDecimal; 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 * @date 2026-08-23
*/ */
@Component @Component
public class WaterLevelAnalyzer implements ReportAnalyzer { public class WaterLevelAnalyzer extends AbstractDeviceAnalyzer {
@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;
@Override @Override
public boolean supports(MonitorCategory category) { protected MonitorCategory supportedCategory() {
return category == MonitorCategory.WATER_LEVEL; return MonitorCategory.WATER_LEVEL;
} }
@Override @Override
public AnalysisResult analyze(ReportContext context, ReportRequest request) { protected String[] primaryMetricKeywords() {
AnalysisResult result = new AnalysisResult(); return new String[]{"水位", "液位", "water"};
// 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. 为每个物模型生成统计数据(包括无数据的物模型) @Override
for (Map.Entry<String, ThingsModel> entry : allModels.entrySet()) { protected String directionText(String direction) {
String identity = entry.getKey(); if (TrendAnalysis.DIRECTION_UP.equals(direction)) {
ThingsModel model = entry.getValue(); return "上升";
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()) { if (TrendAnalysis.DIRECTION_DOWN.equals(direction)) {
statistics.setUnit(model.getUnit()); return "下降";
} }
statistics.setIdentifier(identity); return "平稳";
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
String primaryIdentity = primaryMetricResolver.resolve(context, byIdentity, false, "水位", "液位", "water"); protected String buildSummary(Statistics statistics, TrendAnalysis trend, String unit, TimeWindow window) {
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) { if (statistics == null || statistics.getCount() == 0) {
return "统计期内无有效监测数据。"; return "统计期内无有效监测数据。";
} }
String direction = trend == null ? "待分析" : directionText(trend.getDirection()); String direction = trend == null ? "待分析" : directionText(trend.getDirection());
BigDecimal rate = rateCalculator.ratePerDay(statistics, String formattedCurrent = valueFormatter.format(statistics.getCurrent(), supportedCategory());
Duration.between(window.getStartTime(), window.getEndTime()).getSeconds()); 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( return String.format(
"本期水位监测数据显示:当前水位为%s%s,较初始值变化%s%s,水位变化速率为%s%s/天,水位呈%s趋势。" + "本期水位监测数据显示:当前水位为%s%s,较初始值变化%s%s,变化速率为%s,水位呈%s趋势。" +
"统计期内最高水位%s%s,最低水位%s%s,平均水位%s%s。渗流监测正常。", "统计期内最高水位%s%s,最低水位%s%s,平均水位%s%s。",
statistics.getCurrent().toPlainString(), unit, formattedCurrent, resolvedUnit,
statistics.getDelta().toPlainString(), unit, formattedDelta, resolvedUnit,
rate.toPlainString(), unit, rateText,
direction, direction,
statistics.getMax().toPlainString(), unit, formattedMax, resolvedUnit,
statistics.getMin().toPlainString(), unit, formattedMin, resolvedUnit,
statistics.getAvg().toPlainString(), unit formattedAvg, resolvedUnit
); );
} }
/** @Override
* 生成专业分析建议 protected String buildProfessionalAdvice(Statistics statistics, TrendAnalysis trend, WarningAnalysis warning, String unit) {
*/
private String buildProfessionalAdvice(Statistics statistics, TrendAnalysis trend, WarningAnalysis warning, String unit) {
if (statistics == null || statistics.getCount() == 0) { if (statistics == null || statistics.getCount() == 0) {
return "建议检查设备上报状态,确保数据采集正常。"; return "建议检查设备上报状态,确保数据采集正常。";
} }
@ -246,7 +84,6 @@ public class WaterLevelAnalyzer implements ReportAnalyzer {
StringBuilder advice = new StringBuilder(); StringBuilder advice = new StringBuilder();
advice.append("专业分析:"); advice.append("专业分析:");
// 基于趋势方向给出建议
if (trend != null) { if (trend != null) {
if (TrendAnalysis.DIRECTION_UP.equals(trend.getDirection())) { if (TrendAnalysis.DIRECTION_UP.equals(trend.getDirection())) {
advice.append("水位呈上升趋势,需关注浸润线位置变化,评估渗流场稳定性。"); advice.append("水位呈上升趋势,需关注浸润线位置变化,评估渗流场稳定性。");
@ -257,12 +94,10 @@ public class WaterLevelAnalyzer implements ReportAnalyzer {
} }
} }
// 基于水位变化量级给出建议
if (statistics.getDelta().abs().compareTo(new BigDecimal("1")) > 0) { if (statistics.getDelta().abs().compareTo(new BigDecimal("1")) > 0) {
advice.append("水位变化幅度较大,建议分析水位变化原因(降雨、抽排水等),评估对边坡稳定性的影响。"); advice.append("水位变化幅度较大,建议分析水位变化原因(降雨、抽排水等),评估对边坡稳定性的影响。");
} }
// 基于预警状态给出建议
if (warning != null) { if (warning != null) {
if (WarningAnalysis.STATUS_ALARM.equals(warning.getStatus())) { if (WarningAnalysis.STATUS_ALARM.equals(warning.getStatus())) {
advice.append("当前处于报警状态,建议立即启动排水措施,组织专业人员进行渗流安全评估。"); advice.append("当前处于报警状态,建议立即启动排水措施,组织专业人员进行渗流安全评估。");
@ -273,88 +108,4 @@ public class WaterLevelAnalyzer implements ReportAnalyzer {
return advice.toString(); 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 "平稳";
}
} }

View File

@ -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;
}

View File

@ -19,6 +19,8 @@ public class WarningAnalysis {
public static final String STATUS_NORMAL = "NORMAL"; public static final String STATUS_NORMAL = "NORMAL";
public static final String STATUS_WARNING = "WARNING"; public static final String STATUS_WARNING = "WARNING";
public static final String STATUS_ALARM = "ALARM"; public static final String STATUS_ALARM = "ALARM";
/** 未配置阈值,无法评估 */
public static final String STATUS_UNKNOWN = "UNKNOWN";
@ApiModelProperty("物模型标识符") @ApiModelProperty("物模型标识符")
private String identifier; private String identifier;

View File

@ -293,6 +293,9 @@ public class WordRenderer implements ReportRenderer {
if (WarningAnalysis.STATUS_WARNING.equals(status)) { if (WarningAnalysis.STATUS_WARNING.equals(status)) {
return "预警"; return "预警";
} }
if (WarningAnalysis.STATUS_UNKNOWN.equals(status)) {
return "待评估";
}
return "正常"; return "正常";
} }

View File

@ -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);
}

View File

@ -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));
}
}

View File

@ -163,7 +163,7 @@ def create_professional_template(template_name, device_type, template_dir):
('平均值', '{{analysis.statistics.avgWithUnit}}'), ('平均值', '{{analysis.statistics.avgWithUnit}}'),
('中位数', '{{analysis.statistics.medianWithUnit}}'), ('中位数', '{{analysis.statistics.medianWithUnit}}'),
('变化量', '{{analysis.statistics.deltaWithUnit}}'), ('变化量', '{{analysis.statistics.deltaWithUnit}}'),
('累计变化', '{{analysis.statistics.totalDeltaWithUnit}}'), ('波动量', '{{analysis.statistics.totalDeltaWithUnit}}'),
('数据量', '{{analysis.statistics.count}}'), ('数据量', '{{analysis.statistics.count}}'),
] ]
@ -183,12 +183,12 @@ def create_professional_template(template_name, device_type, template_dir):
# 第1行:表头 # 第1行:表头
# 第2行:{{listName}} 标记行(可以是空单元格) # 第2行:{{listName}} 标记行(可以是空单元格)
# 第3行:模板行,使用 [field] 语法 # 第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.style = 'Table Grid'
points_table.alignment = WD_TABLE_ALIGNMENT.CENTER points_table.alignment = WD_TABLE_ALIGNMENT.CENTER
# 表头 # 表头
points_headers = ['测点', '初始值', '当前值', '变化量', '变化速率', '最大值', '最小值', '预警状态'] points_headers = ['测点', '初始值', '当前值', '变化量', '变化速率', '预警状态']
points_header_row = points_table.rows[0] points_header_row = points_table.rows[0]
for i, header in enumerate(points_headers): for i, header in enumerate(points_headers):
points_header_row.cells[i].text = header points_header_row.cells[i].text = header
@ -206,15 +206,13 @@ def create_professional_template(template_name, device_type, template_dir):
'[currentValue]', '[currentValue]',
'[changeValue]', '[changeValue]',
'[changeRate]', '[changeRate]',
'[maxValue]',
'[minValue]',
'[warningStatus]', '[warningStatus]',
] ]
for i, data in enumerate(points_data): for i, data in enumerate(points_data):
points_data_row.cells[i].text = data points_data_row.cells[i].text = data
style_body_cell(points_data_row.cells[i]) 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. 趋势分析 ===== # ===== 7. 趋势分析 =====
add_heading(doc, '五、趋势分析') add_heading(doc, '五、趋势分析')

View 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. 预警值 < 报警值 < 控制值,系统会按当前值与阈值的比较自动判断安全等级。

View 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
View 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
-- ============================================================

View 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 }
})
}