reportEngine:完善报告导出,设置模型预警值

This commit is contained in:
zhugongping 2026-09-21 09:29:15 +08:00
parent a27e99b031
commit 86b85f2327
45 changed files with 978 additions and 830 deletions

View File

@ -18,8 +18,8 @@ services:
- "9000:9000"
- "9001:9001"
environment:
MINIO_ROOT_USER: yack-superme
MINIO_ROOT_PASSWORD: yack-minio20260816
MINIO_ROOT_USER: supermeminio
MINIO_ROOT_PASSWORD: minio20260909
MINIO_SERVER_URL: "https://oss.ycdat.cn:9000" # ✅ 指定 API 外部访问地址
volumes:
- ./minio/data:/data

View File

@ -0,0 +1,27 @@
networks:
network:
ipam:
driver: default
config:
- subnet: '176.6.0.0/16'
services:
minio:
image: minio/minio:latest
container_name: minio
restart: always
# privileged: true 它几乎拥有与宿主机 root 相同的所有权限;MinIO 是纯用户空间的对象存储服务;无需用到特权模式,故而安全性与最小权限原则
networks:
network:
ipv4_address: 177.7.0.6
ports:
- "9000:9000"
- "9001:9001"
environment:
MINIO_ROOT_USER: supermeminio
MINIO_ROOT_PASSWORD: minio20260909
MINIO_SERVER_URL: "https://oss.ycdat.cn:9000" # ✅ 指定 API 外部访问地址
volumes:
- ./minio/data:/data
- ./minio/certs:/root/.minio/certs # ✅:挂载证书目录
command: server /data --console-address ":9001"

View File

@ -40,7 +40,7 @@
1. `fastbee-report` 模块:报告引擎、Analyzer、`report_record` 表(均不存在)。
2. 设备监测分类路由:`DeviceType` 是连接拓扑(直连/网关/摄像头),`iot_category` 是租户商务分类树,均不能作为分析路由键 → 需 `iot_product.monitor_category_code` + `MonitorCategory` 枚举。
3. 报告周期/时间窗口:`ReportPeriod` 枚举 + `ReportPeriodResolver`。
4. 结构化报告阈值:告警阈值藏在 LiteFlow `iot_scene` 脚本里,报告不可直接读 → 新增 `iot_report_threshold`。
4. 结构化报告阈值:告警阈值藏在 LiteFlow `iot_scene` 脚本里,报告不可直接读 → 新增 `iot_things_threshold`。
5. Java 端图表(JFreeChart)与 Word 模板引擎(poi-tl)。
6. MinIO 默认存储配置。
7. 前端两处「生成并导出报表」按钮 + 历史报表页(列表/下载/过期)。
@ -116,7 +116,7 @@ fastbee-report/
├── period/ ReportPeriodResolver(+Default), TimeWindow
├── analyzer/ ReportAnalyzer, ReportAnalyzerFactory(+Default), crack/CrackMeterAnalyzer
├── analysis/ StatisticsCalculator, TrendAnalyzer, RateCalculator, QualityAnalyzer, WarningAnalyzer
├── threshold/ ReportThresholdProvider(+Db 实现), ReportThreshold
├── threshold/ ThingsThresholdProvider(+Db 实现), ThingsThreshold
├── data/ ReportDataProvider, MetricData
├── model/ ReportModel, AnalysisResult, Statistics, TrendAnalysis, QualityAnalysis, WarningAnalysis, ChartData
├── chart/ ChartGenerator(JFreeChart 生成 PNG)
@ -157,7 +157,7 @@ fastbee-report/
## 3.5 阈值(WarningAnalyzer)
- 现状:告警阈值藏在 LiteFlow `iot_scene` 脚本里,报告引擎直接解析成本高、易错。
- V1:新增 `iot_report_threshold` 表(字段:product_id、device_id、monitor_category_code、metric_identifier、warning_value、alarm_value、control_value、unit)。
- V1:新增 `iot_things_threshold` 表(字段:product_id、device_id、monitor_category_code、metric_identifier、warning_value、alarm_value、control_value、unit)。
- 查询优先级:`device_id > product_id > 全局默认(product_id=0)`。Analyzer 只读结构化阈值,**禁止硬编码阈值**。
## 3.6 分析能力(通用 + 专项)
@ -362,7 +362,7 @@ POST /iot/report/generate
| 对象 | 说明 |
|---|---|
| `iot_product` 加列 | `monitor_category_code VARCHAR(64)` 可空;存量按产品名回填(如 `UPDATE iot_product SET monitor_category_code='CRACK_METER' WHERE product_name LIKE '%裂缝%'`) |
| `iot_report_threshold` | 报告阈值表(product/device 级:预警/报警/控制值) |
| `iot_things_threshold` | 物模型阈值表(product/device 级:预警/报警/控制值 + 速率双控) |
| `report_template` | Word 模板表(template_code + version 唯一) |
| `report_record` | 报告记录表(含 `expire_time`) |
| `oss_config` 插入 | minio 默认配置记录(见 4.3) |
@ -399,7 +399,7 @@ CREATE TABLE report_record (
3. **分类与周期**:`MonitorCategoryResolver`、`ReportPeriodResolver` + 边界单测。
4. **Analyzer 框架 + 裂缝计**:先只实现 `CrackMeterAnalyzer`。
5. **通用分析**:`StatisticsCalculator`/`TrendAnalyzer`/`RateCalculator`/`QualityAnalyzer`/`WarningAnalyzer`。
6. **Provider**:`ReportDataProvider`(委托 `DeviceLogMapper`)、`ReportThresholdProvider`。
6. **Provider**:`ReportDataProvider`(委托 `DeviceLogMapper`)、`ThingsThresholdProvider`。
7. **渲染**:`ChartGenerator`(JFreeChart)+ `WordRenderer`(poi-tl)+ 模板 `DEVICE_CRACK_METER_V1.docx`。
8. **落库与下载**:`report_record` + `expire_time`;MinIO 生命周期规则;下载接口(校验过期)。
9. **前端**:大屏分析模式 + 数据中心加「生成并导出报表」;历史报表页 + 下载。

View File

@ -5,6 +5,7 @@ import com.fastbee.iot.service.IAlertLogService;
import com.fastbee.iot.service.IEventLogService;
import com.fastbee.notify.service.INotifyLogService;
import com.fastbee.quartz.service.ISysJobLogService;
import com.fastbee.report.record.ReportRecordService;
import com.fastbee.system.service.ISysLogininforService;
import com.fastbee.system.service.ISysOperLogService;
import org.slf4j.Logger;
@ -42,6 +43,9 @@ public class LogCleanTask {
@Autowired
private IPlatformPushLogService platformPushLogService;
@Autowired
private ReportRecordService reportRecordService;
// 之后还有清理的表往下写
/**
@ -83,8 +87,13 @@ public class LogCleanTask {
notifyLogService.cleanNotifyLog(days);
//log.info("已清理 notify_log 表 {} 天前的数据", days);
// 平台对接推送日志
platformPushLogService.cleanPushLog(days);
// 清理过期报告记录(report_record 不再保留 expire_time 字段,按 create_time 倒推清理)
int reportDeleted = reportRecordService.cleanExpired(days);
log.info("已清理 report_record 表 {} 天前的数据,删除 {} 条", days, reportDeleted);
log.info("========== 日志清理任务执行完成 ==========");
} catch (Exception e) {
log.error("日志清理任务执行失败", e);

View File

@ -32,7 +32,6 @@ import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -88,7 +87,6 @@ public class ReportController extends BaseController {
.status(record.getStatus())
.fileName(record.getFileName())
.fileUrl(record.getFileUrl())
.expireTime(record.getExpireTime())
.build();
return AjaxResult.success(result);
}
@ -115,9 +113,6 @@ public class ReportController extends BaseController {
if (record == null) {
throw new ServiceException("报告不存在");
}
if (record.getExpireTime() != null && record.getExpireTime().before(new Date())) {
throw new ServiceException("报告已过期");
}
if (StringUtils.isEmpty(record.getFileUrl())) {
throw new ServiceException("报告文件不存在");
}

View File

@ -8,9 +8,9 @@ import com.fastbee.common.enums.BusinessType;
import com.fastbee.common.utils.poi.ExcelUtil;
import com.fastbee.iot.service.IDeviceService;
import com.fastbee.iot.service.IProductService;
import com.fastbee.report.domain.vo.ReportThresholdQuery;
import com.fastbee.report.service.IReportThresholdService;
import com.fastbee.report.threshold.ReportThreshold;
import com.fastbee.report.domain.vo.ThingsThresholdQuery;
import com.fastbee.report.service.IThingsThresholdService;
import com.fastbee.report.threshold.ThingsThreshold;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
@ -32,10 +32,10 @@ import java.util.Map;
@Api(tags = "监测阈值配置")
@RestController
@RequestMapping("/iot/threshold")
public class ReportThresholdController extends BaseController {
public class ThingsThresholdController extends BaseController {
@Autowired
private IReportThresholdService reportThresholdService;
private IThingsThresholdService thingsThresholdService;
@Autowired
private IProductService productService;
@ -46,25 +46,25 @@ public class ReportThresholdController extends BaseController {
/**
* 查询阈值列表
*/
@PreAuthorize("@ss.hasPermi('iot:threshold:list')")
@PreAuthorize("@ss.hasPermi('iot:device:list')")
@GetMapping("/list")
@ApiOperation("阈值分页列表")
public TableDataInfo list(ReportThresholdQuery query) {
public TableDataInfo list(ThingsThresholdQuery query) {
startPage();
List<ReportThreshold> list = reportThresholdService.selectThresholdList(query);
List<ThingsThreshold> list = thingsThresholdService.selectThresholdList(query);
return getDataTable(list);
}
/**
* 导出阈值列表
*/
@PreAuthorize("@ss.hasPermi('iot:threshold:export')")
@PreAuthorize("@ss.hasPermi('iot:device:list')")
@Log(title = "监测阈值配置", businessType = BusinessType.EXPORT)
@PostMapping("/export")
@ApiOperation("导出阈值")
public void export(HttpServletResponse response, ReportThresholdQuery query) {
List<ReportThreshold> list = reportThresholdService.selectThresholdList(query);
ExcelUtil<ReportThreshold> util = new ExcelUtil<>(ReportThreshold.class);
public void export(HttpServletResponse response, ThingsThresholdQuery query) {
List<ThingsThreshold> list = thingsThresholdService.selectThresholdList(query);
ExcelUtil<ThingsThreshold> util = new ExcelUtil<>(ThingsThreshold.class);
util.exportExcel(response, list, "监测阈值数据");
}
@ -72,43 +72,43 @@ public class ReportThresholdController extends BaseController {
* 获取阈值详细信息
*/
@ApiOperation("获取阈值详情")
@PreAuthorize("@ss.hasPermi('iot:threshold:query')")
@PreAuthorize("@ss.hasPermi('iot:device:list')")
@GetMapping(value = "/{id}")
public AjaxResult getInfo(@PathVariable("id") Long id) {
return AjaxResult.success(reportThresholdService.selectThresholdById(id));
return AjaxResult.success(thingsThresholdService.selectThresholdById(id));
}
/**
* 新增阈值
*/
@PreAuthorize("@ss.hasPermi('iot:threshold:add')")
@PreAuthorize("@ss.hasPermi('iot:device:list')")
@Log(title = "监测阈值配置", businessType = BusinessType.INSERT)
@PostMapping
@ApiOperation("新增阈值")
public AjaxResult add(@RequestBody ReportThreshold threshold) {
return toAjax(reportThresholdService.insertThreshold(threshold));
public AjaxResult add(@RequestBody ThingsThreshold threshold) {
return toAjax(thingsThresholdService.insertThreshold(threshold));
}
/**
* 修改阈值
*/
@PreAuthorize("@ss.hasPermi('iot:threshold:edit')")
@PreAuthorize("@ss.hasPermi('iot:device:list')")
@Log(title = "监测阈值配置", businessType = BusinessType.UPDATE)
@PutMapping
@ApiOperation("修改阈值")
public AjaxResult edit(@RequestBody ReportThreshold threshold) {
return toAjax(reportThresholdService.updateThreshold(threshold));
public AjaxResult edit(@RequestBody ThingsThreshold threshold) {
return toAjax(thingsThresholdService.updateThreshold(threshold));
}
/**
* 删除阈值
*/
@PreAuthorize("@ss.hasPermi('iot:threshold:remove')")
@PreAuthorize("@ss.hasPermi('iot:device:list')")
@Log(title = "监测阈值配置", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
@ApiOperation("删除阈值")
public AjaxResult remove(@PathVariable Long[] ids) {
return toAjax(reportThresholdService.deleteThresholdByIds(ids));
return toAjax(thingsThresholdService.deleteThresholdByIds(ids));
}
/**

View File

@ -64,22 +64,25 @@ public class QualityAnalyzer {
// 完整率估算:以相邻点中位间隔为期望上报间隔
long expectedMinutes = expectedMinutes(sorted, window);
int missing;
BigDecimal completeness;
if (expectedMinutes <= 0) {
missing = 0;
completeness = BigDecimal.ONE;
} else {
int expected = (int) Math.max(expectedMinutes / Math.max(1, estimateIntervalMinutes(sorted)), 1);
long intervalMinutes = estimateIntervalMinutes(sorted);
int missing = 0;
BigDecimal completeness = null;
// 时间窗口无效或数据点不足(无法估算上报间隔)时不做缺失推断,
// 否则单点数据会按"间隔=1分钟"算出上万条缺失、完整率趋近 0
if (expectedMinutes > 0 && intervalMinutes > 0) {
int expected = (int) Math.max(expectedMinutes / intervalMinutes, 1);
missing = Math.max(0, expected - sorted.size());
completeness = BigDecimal.valueOf(Math.min(1.0, sorted.size() / (double) expected))
.setScale(4, RoundingMode.HALF_UP);
}
result.setMissing(missing);
result.setCompletenessRate(completeness);
String rateText = completeness == null
? "无法估算(时间窗口无效或数据点不足)"
: String.format("%.2f%%", completeness.multiply(BigDecimal.valueOf(100)).doubleValue());
result.setDescription(String.format(
"统计期内共 %d 条数据,预估缺数 %d 条,完整率 %.2f%%,重复值 %d 条%s",
sorted.size(), missing, completeness.multiply(BigDecimal.valueOf(100)).doubleValue(),
"统计期内共 %d 条数据,预估缺数 %d 条,完整率 %s,重复值 %d 条%s",
sorted.size(), missing, rateText,
duplicate, outOfRange > 0 ? ",越界 " + outOfRange + " 条" : ""));
return result;
}

View File

@ -44,7 +44,8 @@ public class TrendAnalyzer {
if (points.size() < 3) {
result.setDirection(TrendAnalysis.DIRECTION_STABLE);
result.setSlope(BigDecimal.ZERO);
result.setRSquared(BigDecimal.ONE);
// 数据不足时 R² 无统计意义,置空而非 1.0,避免报告出现虚假的"完美拟合"
result.setRSquared(null);
result.setDescription("数据点不足(" + points.size() + " 个),暂不判定趋势");
return result;
}

View File

@ -1,7 +1,7 @@
package com.fastbee.report.analysis;
import com.fastbee.report.model.WarningAnalysis;
import com.fastbee.report.threshold.ReportThreshold;
import com.fastbee.report.threshold.ThingsThreshold;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
@ -9,13 +9,18 @@ import java.math.BigDecimal;
/**
* 预警分析器:只读结构化阈值(禁止硬编码阈值)
*
* <p>按 GB 50497-2019 第 8.0.4 条「双控」口径:累计变化量与变化速率分别判定,
* 取较严重等级。某一维度未配置阈值时该维度不参与判定;两个维度都不可判定时结果为
* {@link WarningAnalysis#STATUS_UNKNOWN},不得回退为「正常」。</p>
*
* @author FastBee
* @date 2026-08-16
*/
@Component
public class WarningAnalyzer {
public WarningAnalysis analyze(String identifier, String modelName, BigDecimal currentValue, ReportThreshold threshold) {
public WarningAnalysis analyze(String identifier, String modelName, BigDecimal currentValue,
BigDecimal changeRate, ThingsThreshold threshold) {
WarningAnalysis result = new WarningAnalysis();
result.setIdentifier(identifier);
result.setModelName(modelName);
@ -25,34 +30,117 @@ public class WarningAnalyzer {
result.setAlarmValue(threshold.getAlarmValue());
result.setControlValue(threshold.getControlValue());
}
if (currentValue == null) {
result.setStatus(WarningAnalysis.STATUS_NORMAL);
result.setDescription("统计期内无有效当前值");
return result;
}
if (threshold == null) {
result.setStatus(WarningAnalysis.STATUS_UNKNOWN);
if (threshold == null) {
result.setDescription("未配置预警阈值,无法评估安全等级");
return result;
}
BigDecimal abs = currentValue.abs();
if (threshold.getControlValue() != null && abs.compareTo(threshold.getControlValue()) >= 0) {
result.setStatus(WarningAnalysis.STATUS_ALARM);
result.setDescription(String.format("当前值 %s 达到/超过控制值 %s,处于报警状态",
currentValue.toPlainString(), threshold.getControlValue().toPlainString()));
} else if (threshold.getAlarmValue() != null && abs.compareTo(threshold.getAlarmValue()) >= 0) {
result.setStatus(WarningAnalysis.STATUS_ALARM);
result.setDescription(String.format("当前值 %s 达到/超过报警值 %s,处于报警状态",
currentValue.toPlainString(), threshold.getAlarmValue().toPlainString()));
} else if (threshold.getWarningValue() != null && abs.compareTo(threshold.getWarningValue()) >= 0) {
result.setStatus(WarningAnalysis.STATUS_WARNING);
result.setDescription(String.format("当前值 %s 达到/超过预警值 %s,处于预警状态",
currentValue.toPlainString(), threshold.getWarningValue().toPlainString()));
} else {
result.setStatus(WarningAnalysis.STATUS_NORMAL);
result.setDescription("当前值处于正常范围");
}
boolean noCumulative = isAllNull(threshold.getControlValue(), threshold.getAlarmValue(), threshold.getWarningValue());
boolean noRate = isAllNull(threshold.getControlRate(), threshold.getAlarmRate(), threshold.getWarningRate());
if (noCumulative && noRate) {
result.setDescription("阈值记录未配置累计值与速率阈值,无法评估安全等级");
return result;
}
// 维度一:累计变化量(相对量设备传相对基准点的累计变化,绝对量设备传当前值)
String cumulativeStatus = null;
String cumulativeDesc = null;
if (!noCumulative && currentValue != null) {
BigDecimal abs = currentValue.abs();
if (threshold.getControlValue() != null && abs.compareTo(threshold.getControlValue()) >= 0) {
cumulativeStatus = WarningAnalysis.STATUS_ALARM;
cumulativeDesc = String.format("当前值 %s 达到/超过控制值 %s,处于报警状态",
currentValue.toPlainString(), threshold.getControlValue().toPlainString());
} else if (threshold.getAlarmValue() != null && abs.compareTo(threshold.getAlarmValue()) >= 0) {
cumulativeStatus = WarningAnalysis.STATUS_ALARM;
cumulativeDesc = String.format("当前值 %s 达到/超过报警值 %s,处于报警状态",
currentValue.toPlainString(), threshold.getAlarmValue().toPlainString());
} else if (threshold.getWarningValue() != null && abs.compareTo(threshold.getWarningValue()) >= 0) {
cumulativeStatus = WarningAnalysis.STATUS_WARNING;
cumulativeDesc = String.format("当前值 %s 达到/超过预警值 %s,处于预警状态",
currentValue.toPlainString(), threshold.getWarningValue().toPlainString());
} else {
cumulativeStatus = WarningAnalysis.STATUS_NORMAL;
cumulativeDesc = "累计值处于正常范围";
}
}
// 维度二:变化速率(单位/天)
String rateStatus = null;
String rateDesc = null;
if (!noRate && changeRate != null) {
BigDecimal absRate = changeRate.abs();
if (threshold.getControlRate() != null && absRate.compareTo(threshold.getControlRate()) >= 0) {
rateStatus = WarningAnalysis.STATUS_ALARM;
rateDesc = String.format("变化速率 %s 达到/超过速率控制值 %s,处于报警状态",
absRate.toPlainString(), threshold.getControlRate().toPlainString());
} else if (threshold.getAlarmRate() != null && absRate.compareTo(threshold.getAlarmRate()) >= 0) {
rateStatus = WarningAnalysis.STATUS_ALARM;
rateDesc = String.format("变化速率 %s 达到/超过速率报警值 %s,处于报警状态",
absRate.toPlainString(), threshold.getAlarmRate().toPlainString());
} else if (threshold.getWarningRate() != null && absRate.compareTo(threshold.getWarningRate()) >= 0) {
rateStatus = WarningAnalysis.STATUS_WARNING;
rateDesc = String.format("变化速率 %s 达到/超过速率预警值 %s,处于预警状态",
absRate.toPlainString(), threshold.getWarningRate().toPlainString());
} else {
rateStatus = WarningAnalysis.STATUS_NORMAL;
rateDesc = "变化速率处于正常范围";
}
}
String finalStatus = worse(cumulativeStatus, rateStatus);
if (finalStatus == null) {
result.setDescription("缺少可判定的累计变化量或变化速率,无法评估安全等级");
return result;
}
result.setStatus(finalStatus);
StringBuilder desc = new StringBuilder();
if (cumulativeDesc != null) {
desc.append(cumulativeDesc);
}
if (rateDesc != null) {
if (desc.length() > 0) {
desc.append(";");
}
desc.append(rateDesc);
}
// 维度缺数据时明确说明,避免"只判了一半"被当成已完整评估
if (!noCumulative && currentValue == null) {
desc.append(";累计值缺失,累计维度未参与判定");
}
if (!noRate && changeRate == null) {
desc.append(";变化速率缺失,速率维度未参与判定");
}
result.setDescription(desc.toString());
return result;
}
/** 取更严重等级:ALARM > WARNING > NORMAL;null 表示该维度未参与判定 */
private String worse(String a, String b) {
if (a == null) {
return b;
}
if (b == null) {
return a;
}
if (WarningAnalysis.STATUS_ALARM.equals(a) || WarningAnalysis.STATUS_ALARM.equals(b)) {
return WarningAnalysis.STATUS_ALARM;
}
if (WarningAnalysis.STATUS_WARNING.equals(a) || WarningAnalysis.STATUS_WARNING.equals(b)) {
return WarningAnalysis.STATUS_WARNING;
}
return WarningAnalysis.STATUS_NORMAL;
}
private boolean isAllNull(BigDecimal... values) {
for (BigDecimal v : values) {
if (v != null) {
return false;
}
}
return true;
}
}

View File

@ -22,7 +22,7 @@ import com.fastbee.report.model.Statistics;
import com.fastbee.report.model.TrendAnalysis;
import com.fastbee.report.model.WarningAnalysis;
import com.fastbee.report.period.TimeWindow;
import com.fastbee.report.threshold.ReportThreshold;
import com.fastbee.report.threshold.ThingsThreshold;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
@ -30,6 +30,7 @@ import java.math.BigDecimal;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@ -54,6 +55,15 @@ import java.util.stream.Collectors;
*/
public abstract class AbstractDeviceAnalyzer implements ReportAnalyzer {
/** 安全等级:安全 */
public static final String SAFETY_SAFE = "SAFE";
/** 安全等级:预警 */
public static final String SAFETY_WARNING = "WARNING";
/** 安全等级:危险 */
public static final String SAFETY_DANGER = "DANGER";
/** 安全等级:未配置阈值,无法评估(区别于 SAFE,避免"未评估"被当成"安全") */
public static final String SAFETY_NOT_ASSESSED = "NOT_ASSESSED";
@Resource
protected StatisticsCalculator statisticsCalculator;
@ -210,6 +220,15 @@ public abstract class AbstractDeviceAnalyzer implements ReportAnalyzer {
.filter(d -> d.getIdentity() != null)
.collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList()));
// 1.1 基准点数据分组(相对测量换算用)
Map<String, List<MetricData>> baselineByIdentity = Collections.emptyMap();
List<MetricData> baselineList = context.getBaselineMetricDataList();
if (baselineList != null && !baselineList.isEmpty()) {
baselineByIdentity = baselineList.stream()
.filter(d -> d.getIdentity() != null)
.collect(Collectors.groupingBy(MetricData::getIdentity, LinkedHashMap::new, Collectors.toList()));
}
// 2. 获取所有监测物模型
Map<String, ThingsModel> allModels = getAllModels(context);
@ -223,8 +242,12 @@ public abstract class AbstractDeviceAnalyzer implements ReportAnalyzer {
String identity = entry.getKey();
ThingsModel model = entry.getValue();
List<MetricData> data = byIdentity.getOrDefault(identity, Collections.emptyList());
// 相对测量换算:监测点读数 − 基准点读数(按时间就近对齐)
if (!baselineByIdentity.isEmpty()) {
data = toRelativeSeries(data, baselineByIdentity.get(identity));
}
ReportThreshold threshold = context.getThresholds().get(identity);
ThingsThreshold threshold = context.getThresholds().get(identity);
BigDecimal controlValue = threshold == null ? null : threshold.getControlValue();
Statistics statistics = statisticsCalculator.calculate(data);
@ -232,8 +255,15 @@ public abstract class AbstractDeviceAnalyzer implements ReportAnalyzer {
TrendAnalysis trend = trendAnalyzer.analyze(data);
QualityAnalysis quality = qualityAnalyzer.analyze(data, context.getTimeWindow(), controlValue);
// 双控:变化速率(单位/天)与累计量一并参与安全判定
TimeWindow window = context.getTimeWindow();
long durationSeconds = (window != null && window.getStartTime() != null && window.getEndTime() != null)
? Duration.between(window.getStartTime(), window.getEndTime()).getSeconds()
: 0L;
BigDecimal changeRate = rateCalculator.ratePerDay(statistics, durationSeconds);
WarningAnalysis warning = warningAnalyzer.analyze(
identity, statistics.getModelName(), statistics.getCurrent(), threshold);
identity, statistics.getModelName(), statistics.getCurrent(), changeRate, threshold);
result.getStatistics().put(identity, statistics);
result.getTrends().put(identity, trend);
@ -261,6 +291,76 @@ public abstract class AbstractDeviceAnalyzer implements ReportAnalyzer {
return result;
}
/**
* 相对测量换算:监测点读数 − 基准点读数(按时间就近对齐)
*
* <p>静力水准等设备的原始读数是相对基准点的,累计变化量应按
* 「(当前读数 − 基准点当前读数) − (初始读数 − 基准点初始读数)」计算,
* 直接拿原始读数的首尾差会混入基准点自身的漂移。</p>
*
* <p>对齐不上的点直接剔除而非补零,避免制造虚假数据点。</p>
*/
protected List<MetricData> toRelativeSeries(List<MetricData> monitor, List<MetricData> baseline) {
if (monitor == null || monitor.isEmpty() || baseline == null || baseline.isEmpty()) {
return monitor;
}
List<MetricData> sortedBase = baseline.stream()
.filter(d -> d.getTime() != null && d.getNumericValue() != null)
.sorted(Comparator.comparing(MetricData::getTime))
.collect(Collectors.toList());
if (sortedBase.isEmpty()) {
return monitor;
}
long toleranceMs = alignmentToleranceMs(sortedBase);
List<MetricData> sortedMonitor = monitor.stream()
.filter(d -> d.getTime() != null && d.getNumericValue() != null)
.sorted(Comparator.comparing(MetricData::getTime))
.collect(Collectors.toList());
List<MetricData> result = new ArrayList<>();
int j = 0;
for (MetricData m : sortedMonitor) {
long target = m.getTime().getTime();
while (j + 1 < sortedBase.size()
&& Math.abs(sortedBase.get(j + 1).getTime().getTime() - target)
<= Math.abs(sortedBase.get(j).getTime().getTime() - target)) {
j++;
}
MetricData base = sortedBase.get(j);
if (Math.abs(base.getTime().getTime() - target) > toleranceMs) {
continue;
}
MetricData rel = new MetricData();
rel.setIdentity(m.getIdentity());
rel.setModelName(m.getModelName());
rel.setUnit(m.getUnit());
rel.setTime(m.getTime());
rel.setNumericValue(m.getNumericValue().subtract(base.getNumericValue()));
rel.setValue(rel.getNumericValue().toPlainString());
result.add(rel);
}
return result;
}
/** 对齐容差:基准点上报间隔中位数的 2 倍,至少 1 分钟 */
private long alignmentToleranceMs(List<MetricData> sortedBase) {
List<Long> gaps = new ArrayList<>();
for (int i = 1; i < sortedBase.size(); i++) {
long gap = sortedBase.get(i).getTime().getTime() - sortedBase.get(i - 1).getTime().getTime();
if (gap > 0) {
gaps.add(gap);
}
}
long median;
if (gaps.isEmpty()) {
median = 30 * 60 * 1000L;
} else {
Collections.sort(gaps);
median = gaps.get(gaps.size() / 2);
}
return Math.max(median * 2, 60 * 1000L);
}
/**
* 获取所有监测物模型
*/
@ -397,7 +497,7 @@ public abstract class AbstractDeviceAnalyzer implements ReportAnalyzer {
}
// 设置控制值
ReportThreshold threshold = context.getThresholds().get(primaryIdentity);
ThingsThreshold threshold = context.getThresholds().get(primaryIdentity);
if (threshold != null && threshold.getControlValue() != null) {
result.setControlValue(threshold.getControlValue());
result.setControlValueUnit(threshold.getUnit());
@ -491,11 +591,16 @@ public abstract class AbstractDeviceAnalyzer implements ReportAnalyzer {
*/
protected String convertWarningToSafetyLevel(String warningStatus) {
if (WarningAnalysis.STATUS_ALARM.equals(warningStatus)) {
return "DANGER";
} else if (WarningAnalysis.STATUS_WARNING.equals(warningStatus)) {
return "WARNING";
return SAFETY_DANGER;
}
return "SAFE";
if (WarningAnalysis.STATUS_WARNING.equals(warningStatus)) {
return SAFETY_WARNING;
}
// 未配置阈值/阈值不可用:不可等同于安全,否则报告会给出虚假的安全结论
if (WarningAnalysis.STATUS_UNKNOWN.equals(warningStatus)) {
return SAFETY_NOT_ASSESSED;
}
return SAFETY_SAFE;
}
/**

View File

@ -1,13 +1,11 @@
package com.fastbee.report.api;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Builder;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 报告生成结果
@ -33,8 +31,4 @@ public class ReportResult implements Serializable {
@ApiModelProperty("文件地址(MinIO)")
private String fileUrl;
@ApiModelProperty("过期时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date expireTime;
}

View File

@ -7,7 +7,7 @@ import com.fastbee.iot.domain.ThingsModel;
import com.fastbee.report.api.ReportRequest;
import com.fastbee.report.data.MetricData;
import com.fastbee.report.period.TimeWindow;
import com.fastbee.report.threshold.ReportThreshold;
import com.fastbee.report.threshold.ThingsThreshold;
import lombok.Data;
import java.util.List;
@ -38,8 +38,16 @@ public class ReportContext {
/** 时序数据 */
private List<MetricData> metricDataList;
/**
* 基准点设备的时序数据(device.baselineDeviceId 非空时加载)
*
* <p>相对测量类设备(静力水准等)的读数是相对基准点的,需先按时间对齐做差值
* 换算为相对序列,再统计累计变化量。为 null 表示该设备为绝对量测量。</p>
*/
private List<MetricData> baselineMetricDataList;
/** 阈值(key = 物模型标识符) */
private Map<String, ReportThreshold> thresholds;
private Map<String, ThingsThreshold> thresholds;
private String reportNo;

View File

@ -15,8 +15,8 @@ import com.fastbee.report.data.MetricData;
import com.fastbee.report.data.ReportDataProvider;
import com.fastbee.report.exception.ReportException;
import com.fastbee.report.period.TimeWindow;
import com.fastbee.report.threshold.ReportThreshold;
import com.fastbee.report.threshold.ReportThresholdProvider;
import com.fastbee.report.threshold.ThingsThreshold;
import com.fastbee.report.threshold.ThingsThresholdProvider;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
@ -53,7 +53,7 @@ public class ReportContextBuilder {
private ReportDataProvider dataProvider;
@Resource
private ReportThresholdProvider thresholdProvider;
private ThingsThresholdProvider thingsThresholdProvider;
@Resource
private SubGatewayMapper subGatewayMapper;
@ -90,12 +90,23 @@ public class ReportContextBuilder {
List<MetricData> metricDataList = dataProvider.loadHistory(device, models, window);
Map<String, ReportThreshold> thresholds = new LinkedHashMap<>();
// 基准点设备数据:相对测量类设备(静力水准)需按时间对齐做差值换算
// deviceId 全局唯一,因此基准点可以属于另一个网关(跨网关参考点)
List<MetricData> baselineMetricDataList = null;
Long baselineDeviceId = device.getBaselineDeviceId();
if (baselineDeviceId != null && baselineDeviceId > 0 && !baselineDeviceId.equals(device.getDeviceId())) {
Device baselineDevice = deviceService.selectDeviceByDeviceId(baselineDeviceId);
if (baselineDevice != null) {
baselineMetricDataList = dataProvider.loadHistory(baselineDevice, models, window);
}
}
Map<String, ThingsThreshold> thresholds = new LinkedHashMap<>();
for (ThingsModel model : models) {
if (model.getIdentifier() == null) {
continue;
}
ReportThreshold threshold = thresholdProvider.find(
ThingsThreshold threshold = thingsThresholdProvider.find(
device.getDeviceId(), product.getProductId(), category.getCode(), model.getIdentifier());
if (threshold != null) {
thresholds.put(model.getIdentifier(), threshold);
@ -110,6 +121,7 @@ public class ReportContextBuilder {
context.setProduct(product);
context.setThingsModels(models);
context.setMetricDataList(metricDataList);
context.setBaselineMetricDataList(baselineMetricDataList);
context.setThresholds(thresholds);
context.setReportNo(reportNo);

View File

@ -3,13 +3,13 @@ package com.fastbee.report.domain.vo;
import lombok.Data;
/**
* 报告阈值查询 VO
* 物模型阈值查询 VO
*
* @author FastBee
* @date 2026-09-10
*/
@Data
public class ReportThresholdQuery {
public class ThingsThresholdQuery {
/** 产品ID */
private Long productId;

View File

@ -29,7 +29,6 @@ import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.concurrent.ThreadLocalRandom;
/**
@ -44,7 +43,6 @@ public class ReportAsyncExecutor {
private static final String WORD_CONTENT_TYPE =
"application/vnd.openxmlformats-officedocument.wordprocessingml.document";
private static final int RETENTION_DAYS = 30;
private static final DateTimeFormatter REPORT_NO_TIME = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
private static final DateTimeFormatter PATH_DATE = DateTimeFormatter.ofPattern("yyyy/MM/dd");
@ -119,9 +117,8 @@ public class ReportAsyncExecutor {
String path = buildObjectPath(request.getDeviceId(), reportNo);
UploadResult upload = OssFactory.instance(redisCache)
.upload(docBytes, path, WORD_CONTENT_TYPE);
Date expireTime = new Date(System.currentTimeMillis() + RETENTION_DAYS * 24L * 3600 * 1000);
reportRecordService.markSuccess(record, model.getFileName(), upload.getUrl(), expireTime);
reportRecordService.markSuccess(record, model.getFileName(), upload.getUrl());
log.info("报告生成成功 reportNo={} fileUrl={}", reportNo, upload.getUrl());
} catch (Exception e) {

View File

@ -15,7 +15,15 @@ import java.util.List;
public interface ReportRecordMapper extends BaseMapper<ReportRecord> {
/**
* 查询未过期报告记录(expire_time >= now)
* 查询报告记录(过期清理由 LogCleanTask#cleanLogs 统一负责)
*/
List<ReportRecord> selectUnexpiredList(ReportRecordQuery query);
/**
* 清理过期报告记录:按 create_time 倒推,保留最近 days 天
*
* @param days 保留天数
* @return 删除记录数
*/
int cleanExpired(int days);
}

View File

@ -1,21 +1,21 @@
package com.fastbee.report.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.fastbee.report.threshold.ReportThreshold;
import com.fastbee.report.threshold.ThingsThreshold;
import org.apache.ibatis.annotations.Param;
/**
* 报告阈值 Mapper
* 物模型阈值 Mapper
*
* @author FastBee
* @date 2026-08-16
*/
public interface ReportThresholdMapper extends BaseMapper<ReportThreshold> {
public interface ThingsThresholdMapper extends BaseMapper<ThingsThreshold> {
/**
* 按优先级查询阈值:device_id > product_id > 全局默认(product_id=0)
*/
ReportThreshold selectByPriority(@Param("deviceId") Long deviceId,
ThingsThreshold selectByPriority(@Param("deviceId") Long deviceId,
@Param("productId") Long productId,
@Param("categoryCode") String categoryCode,
@Param("metricIdentifier") String metricIdentifier);

View File

@ -49,9 +49,6 @@ public class ReportRecord extends BaseEntity {
private String fileUrl;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date expireTime;
private String errorMessage;
private Long tenantId;

View File

@ -2,7 +2,6 @@ package com.fastbee.report.record;
import com.fastbee.report.core.ReportContext;
import java.util.Date;
import java.util.List;
/**
@ -14,14 +13,14 @@ import java.util.List;
public interface ReportRecordService {
/**
* 生成前创建 CREATING 记录(expire_time = create_time + 30 天)
* 生成前创建 CREATING 记录
*/
ReportRecord createCreating(ReportContext context);
/**
* 生成成功:更新 SUCCESS + 文件信息
*/
void markSuccess(ReportRecord record, String fileName, String fileUrl, Date expireTime);
void markSuccess(ReportRecord record, String fileName, String fileUrl);
/**
* 生成失败:更新 FAILED + 失败原因
@ -29,7 +28,7 @@ public interface ReportRecordService {
void markFailed(ReportRecord record, String errorMessage);
/**
* 分页查询未过期记录
* 分页查询报告记录
*/
List<ReportRecord> listUnexpired(ReportRecordQuery query);
@ -37,4 +36,12 @@ public interface ReportRecordService {
* 按报告编号查询
*/
ReportRecord findByReportNo(String reportNo);
/**
* 清理过期报告记录:保留最近 days 天的记录
*
* @param days 保留天数
* @return 删除记录数
*/
int cleanExpired(int days);
}

View File

@ -19,8 +19,6 @@ import java.time.ZoneOffset;
@Service
public class ReportRecordServiceImpl implements ReportRecordService {
private static final int RETENTION_DAYS = 30;
@Resource
private ReportRecordMapper reportRecordMapper;
@ -39,7 +37,6 @@ public class ReportRecordServiceImpl implements ReportRecordService {
record.setTenantId(context.getTenantId());
record.setCreateBy(context.getCreateBy());
record.setCreateTime(now);
record.setExpireTime(new Date(now.getTime() + RETENTION_DAYS * 24L * 3600 * 1000));
reportRecordMapper.insert(record);
return record;
}
@ -49,13 +46,12 @@ public class ReportRecordServiceImpl implements ReportRecordService {
}
@Override
public void markSuccess(ReportRecord record, String fileName, String fileUrl, Date expireTime) {
public void markSuccess(ReportRecord record, String fileName, String fileUrl) {
ReportRecord update = new ReportRecord();
update.setId(record.getId());
update.setStatus(ReportRecord.STATUS_SUCCESS);
update.setFileName(fileName);
update.setFileUrl(fileUrl);
update.setExpireTime(expireTime);
// 异步线程中无法访问 SecurityContext,使用创建时的用户名
update.setUpdateBy(record.getCreateBy());
update.setUpdateTime(DateUtils.getNowDate());
@ -86,4 +82,9 @@ public class ReportRecordServiceImpl implements ReportRecordService {
.eq(ReportRecord::getReportNo, reportNo)
.last("limit 1"));
}
@Override
public int cleanExpired(int days) {
return reportRecordMapper.cleanExpired(days);
}
}

View File

@ -178,9 +178,9 @@ public class WordRenderer implements ReportRenderer {
data.put("professionalAdvice", StringUtils.isNotEmpty(analysis.getProfessionalAdvice())
? analysis.getProfessionalAdvice() : "暂无专业分析建议。");
// 添加安全评估(始终 put,空值兜底)
data.put("safetyLevel", safetyLevelText(StringUtils.isNotEmpty(analysis.getSafetyLevel())
? analysis.getSafetyLevel() : "待评估"));
// 添加安全评估(始终 put,空值兜底直接给"待评估",不可再经 safetyLevelText 回退成"安全")
data.put("safetyLevel", StringUtils.isNotEmpty(analysis.getSafetyLevel())
? safetyLevelText(analysis.getSafetyLevel()) : "待评估");
data.put("safetyDescription", StringUtils.isNotEmpty(analysis.getSafetyDescription())
? analysis.getSafetyDescription() : "暂无评估说明。");
@ -310,6 +310,10 @@ public class WordRenderer implements ReportRenderer {
if ("CRITICAL".equals(level)) {
return "临界";
}
// 未配置阈值,无法评估——不得回退为"安全"
if ("NOT_ASSESSED".equals(level) || "UNKNOWN".equals(level)) {
return "待评估";
}
return "安全";
}
}

View File

@ -1,18 +1,18 @@
package com.fastbee.report.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.fastbee.report.domain.vo.ReportThresholdQuery;
import com.fastbee.report.threshold.ReportThreshold;
import com.fastbee.report.domain.vo.ThingsThresholdQuery;
import com.fastbee.report.threshold.ThingsThreshold;
import java.util.List;
/**
* 报告阈值 Service 接口
* 物模型阈值 Service 接口
*
* @author FastBee
* @date 2026-09-10
*/
public interface IReportThresholdService extends IService<ReportThreshold> {
public interface IThingsThresholdService extends IService<ThingsThreshold> {
/**
* 查询阈值列表(分页)
@ -20,7 +20,7 @@ public interface IReportThresholdService extends IService<ReportThreshold> {
* @param query 查询条件
* @return 阈值列表
*/
List<ReportThreshold> selectThresholdList(ReportThresholdQuery query);
List<ThingsThreshold> selectThresholdList(ThingsThresholdQuery query);
/**
* 根据ID查询阈值
@ -28,7 +28,7 @@ public interface IReportThresholdService extends IService<ReportThreshold> {
* @param id 阈值ID
* @return 阈值对象
*/
ReportThreshold selectThresholdById(Long id);
ThingsThreshold selectThresholdById(Long id);
/**
* 新增阈值
@ -36,7 +36,7 @@ public interface IReportThresholdService extends IService<ReportThreshold> {
* @param threshold 阈值对象
* @return 影响行数
*/
int insertThreshold(ReportThreshold threshold);
int insertThreshold(ThingsThreshold threshold);
/**
* 修改阈值
@ -44,7 +44,7 @@ public interface IReportThresholdService extends IService<ReportThreshold> {
* @param threshold 阈值对象
* @return 影响行数
*/
int updateThreshold(ReportThreshold threshold);
int updateThreshold(ThingsThreshold threshold);
/**
* 批量删除阈值

View File

@ -3,59 +3,59 @@ package com.fastbee.report.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fastbee.common.utils.StringUtils;
import com.fastbee.report.domain.vo.ReportThresholdQuery;
import com.fastbee.report.mapper.ReportThresholdMapper;
import com.fastbee.report.service.IReportThresholdService;
import com.fastbee.report.threshold.ReportThreshold;
import com.fastbee.report.domain.vo.ThingsThresholdQuery;
import com.fastbee.report.mapper.ThingsThresholdMapper;
import com.fastbee.report.service.IThingsThresholdService;
import com.fastbee.report.threshold.ThingsThreshold;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.List;
/**
* 报告阈值 Service 实现
* 物模型阈值 Service 实现
*
* @author FastBee
* @date 2026-09-10
*/
@Service
public class ReportThresholdServiceImpl extends ServiceImpl<ReportThresholdMapper, ReportThreshold>
implements IReportThresholdService {
public class ThingsThresholdServiceImpl extends ServiceImpl<ThingsThresholdMapper, ThingsThreshold>
implements IThingsThresholdService {
@Override
public List<ReportThreshold> selectThresholdList(ReportThresholdQuery query) {
LambdaQueryWrapper<ReportThreshold> wrapper = new LambdaQueryWrapper<>();
public List<ThingsThreshold> selectThresholdList(ThingsThresholdQuery query) {
LambdaQueryWrapper<ThingsThreshold> wrapper = new LambdaQueryWrapper<>();
if (query.getProductId() != null) {
wrapper.eq(ReportThreshold::getProductId, query.getProductId());
wrapper.eq(ThingsThreshold::getProductId, query.getProductId());
}
if (query.getDeviceId() != null) {
wrapper.eq(ReportThreshold::getDeviceId, query.getDeviceId());
wrapper.eq(ThingsThreshold::getDeviceId, query.getDeviceId());
}
if (StringUtils.isNotEmpty(query.getMonitorCategoryCode())) {
wrapper.eq(ReportThreshold::getMonitorCategoryCode, query.getMonitorCategoryCode());
wrapper.eq(ThingsThreshold::getMonitorCategoryCode, query.getMonitorCategoryCode());
}
if (StringUtils.isNotEmpty(query.getMetricIdentifier())) {
wrapper.like(ReportThreshold::getMetricIdentifier, query.getMetricIdentifier());
wrapper.like(ThingsThreshold::getMetricIdentifier, query.getMetricIdentifier());
}
if (StringUtils.isNotEmpty(query.getUnit())) {
wrapper.eq(ReportThreshold::getUnit, query.getUnit());
wrapper.eq(ThingsThreshold::getUnit, query.getUnit());
}
wrapper.orderByDesc(ReportThreshold::getCreateTime);
wrapper.orderByDesc(ThingsThreshold::getCreateTime);
return baseMapper.selectList(wrapper);
}
@Override
public ReportThreshold selectThresholdById(Long id) {
public ThingsThreshold selectThresholdById(Long id) {
return baseMapper.selectById(id);
}
@Override
public int insertThreshold(ReportThreshold threshold) {
public int insertThreshold(ThingsThreshold threshold) {
return baseMapper.insert(threshold);
}
@Override
public int updateThreshold(ReportThreshold threshold) {
public int updateThreshold(ThingsThreshold threshold) {
return baseMapper.updateById(threshold);
}

View File

@ -1,25 +1,25 @@
package com.fastbee.report.threshold;
import com.fastbee.common.utils.StringUtils;
import com.fastbee.report.mapper.ReportThresholdMapper;
import com.fastbee.report.mapper.ThingsThresholdMapper;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* 基于 iot_report_threshold 表的阈值 Provider
* 基于 iot_things_threshold 表的阈值 Provider
*
* @author FastBee
* @date 2026-08-16
*/
@Service
public class DbReportThresholdProvider implements ReportThresholdProvider {
public class DbThingsThresholdProvider implements ThingsThresholdProvider {
@Resource
private ReportThresholdMapper reportThresholdMapper;
private ThingsThresholdMapper thingsThresholdMapper;
@Override
public ReportThreshold find(Long deviceId, Long productId, String categoryCode, String metricIdentifier) {
return reportThresholdMapper.selectByPriority(deviceId, productId, categoryCode, metricIdentifier);
public ThingsThreshold find(Long deviceId, Long productId, String categoryCode, String metricIdentifier) {
return thingsThresholdMapper.selectByPriority(deviceId, productId, categoryCode, metricIdentifier);
}
}

View File

@ -8,23 +8,23 @@ import java.math.BigDecimal;
import java.util.Date;
/**
* 报告结构化阈值(iot_report_threshold)
* 物模型监测阈值(iot_things_threshold)
*
* <p>查询优先级:device_id > product_id > 全局默认(product_id=0)。</p>
* <p>查询优先级:设备级(device_id) &gt; 产品级(device_id=0)。设备阈值覆盖产品阈值,无全局默认层。</p>
*
* @author FastBee
* @date 2026-08-16
*/
@Data
@TableName("iot_report_threshold")
public class ReportThreshold {
@TableName("iot_things_threshold")
public class ThingsThreshold {
private Long id;
/** 产品ID(0=全局默认) */
/** 产品ID */
private Long productId;
/** 设备ID(0=不限定设备) */
/** 设备ID(0=产品级阈值,作用于该产品下所有设备;非0=设备级阈值,覆盖产品级) */
private Long deviceId;
private String monitorCategoryCode;
@ -37,6 +37,15 @@ public class ReportThreshold {
private BigDecimal controlValue;
/** 变化速率预警值(单位/天),GB 50497-2019 双控口径 */
private BigDecimal warningRate;
/** 变化速率报警值(单位/天) */
private BigDecimal alarmRate;
/** 变化速率控制值(单位/天) */
private BigDecimal controlRate;
private String unit;
private String createBy;

View File

@ -1,15 +1,15 @@
package com.fastbee.report.threshold;
/**
* 报告阈值 Provider(Analyzer 只读结构化阈值,禁止硬编码阈值)
* 物模型阈值 Provider(Analyzer 只读结构化阈值,禁止硬编码阈值)
*
* @author FastBee
* @date 2026-08-16
*/
public interface ReportThresholdProvider {
public interface ThingsThresholdProvider {
/**
* 查询指定设备 + 物模型标识符的结构化阈值(device > product > 全局)
*/
ReportThreshold find(Long deviceId, Long productId, String categoryCode, String metricIdentifier);
ThingsThreshold find(Long deviceId, Long productId, String categoryCode, String metricIdentifier);
}

View File

@ -7,11 +7,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<select id="selectUnexpiredList" parameterType="com.fastbee.report.record.ReportRecordQuery"
resultType="com.fastbee.report.record.ReportRecord">
select id, report_no, device_id, device_name, monitor_category_code, period,
start_time, end_time, status, file_name, file_url, expire_time, error_message,
start_time, end_time, status, file_name, file_url, error_message,
tenant_id, create_by, create_time, update_by, update_time
from report_record
where expire_time is not null
and expire_time &gt;= now()
<where>
<if test="deviceId != null">
and device_id = #{deviceId}
</if>
@ -25,7 +24,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
and (report_no like concat('%', #{keyword}, '%')
or device_name like concat('%', #{keyword}, '%'))
</if>
</where>
order by create_time desc
</select>
<!-- 清理过期报告记录:按 create_time 倒推,保留最近 N 天 -->
<delete id="cleanExpired" parameterType="int">
delete from report_record
where create_time &lt; date_sub(now(), interval #{days} day)
</delete>
</mapper>

View File

@ -2,13 +2,15 @@
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.fastbee.report.mapper.ReportThresholdMapper">
<mapper namespace="com.fastbee.report.mapper.ThingsThresholdMapper">
<select id="selectByPriority" resultType="com.fastbee.report.threshold.ReportThreshold">
<!-- 阈值优先级:设备级(device_id) > 产品级(product_id)。无全局默认层。 -->
<select id="selectByPriority" resultType="com.fastbee.report.threshold.ThingsThreshold">
select id, product_id, device_id, monitor_category_code, metric_identifier,
warning_value, alarm_value, control_value, unit,
warning_value, alarm_value, control_value,
warning_rate, alarm_rate, control_rate, unit,
create_by, create_time, update_by, update_time
from iot_report_threshold
from iot_things_threshold
<where>
metric_identifier = #{metricIdentifier}
<if test="categoryCode != null and categoryCode != ''">
@ -17,8 +19,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
or monitor_category_code = '')
</if>
and (device_id = #{deviceId}
or (device_id = 0 and product_id = #{productId})
or (device_id = 0 and product_id = 0))
or (device_id = 0 and product_id = #{productId}))
</where>
order by
case when device_id = #{deviceId} then 0

View File

@ -79,6 +79,19 @@ public class Device extends BaseEntity {
@Excel(name = "设备编号")
private String serialNumber;
/**
* 基准点设备ID(结构安全监测用)
*
* <p>相对测量类设备(如静力水准)的读数是相对基准点的,累计变化量需按
* 「当前相对值 − 初始相对值」计算,其中相对值 = 本设备读数 − 基准点设备读数。</p>
*
* <p>deviceId 全局唯一,因此天然支持跨网关引用(如 GW2 的监测点引用 GW1 下的参考点)。
* 留空表示该设备为绝对量测量,不参与基准点换算。</p>
*/
@ApiModelProperty("基准点设备ID(留空表示无需基准点换算)")
@Excel(name = "基准点设备ID")
private Long baselineDeviceId;
/**
* 固件版本
*/

View File

@ -40,6 +40,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="iccid" column="iccid" />
<result property="projectId" column="project_id" />
<result property="projectName" column="project_name" />
<result property="baselineDeviceId" column="baseline_device_id" />
</resultMap>
<!--
@ -543,7 +544,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
select d.device_id, d.device_name, d.product_id, p.product_name,p.device_type, d.tenant_id, d.tenant_name,
d.serial_number, d.firmware_version,d.wireless_version, d.status, d.rssi,d.is_shadow,d.is_simulate ,d.location_way,d.things_model_value,
d.network_address, d.network_ip, d.longitude, d.latitude, d.active_time, d.create_time, d.update_time,
d.img_url,d.summary,d.imei,d.iccid,d.remark,d.project_id,d.project_name,p.guid, p.transport, p.protocol_code from iot_device d
d.img_url,d.summary,d.imei,d.iccid,d.remark,d.project_id,d.project_name,p.guid, p.transport, p.protocol_code,
d.baseline_device_id from iot_device d
left join iot_product p on p.product_id=d.product_id
where device_id = #{deviceId}
</select>
@ -784,6 +786,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="imei != null">imei = #{imei},</if>
<if test="iccid != null">iccid = #{iccid},</if>
<if test="slaveId != null">slave_id = #{slaveId},</if>
<if test="baselineDeviceId != null">baseline_device_id = #{baselineDeviceId},</if>
<if test="projectId != null">project_id = #{projectId},</if>
</trim>
where device_id = #{deviceId}

View File

@ -3,7 +3,7 @@
-- 创建日期: 2026-08-16
-- 说明:
-- 1. iot_product 增加监测分类编码列
-- 2. 新增 iot_report_threshold / report_template / report_record 三张表
-- 2. 新增 iot_things_threshold / report_template / report_record 三张表
-- 3. oss_config 插入 MinIO 默认配置(存储决策:未购买云 OSS,统一使用自建 MinIO)
-- 4. 历史报表菜单与按钮权限
-- 注意: 存量库执行前请先备份;增量脚本可重复执行(使用 / INSERT IGNORE)
@ -62,7 +62,7 @@ WHERE (`monitor_category_code` IS NULL OR `monitor_category_code` = '')
-- ------------------------------------------------------------
-- 2.1 报告阈值表(查询优先级:device_id > product_id > 全局默认 product_id=0)
-- ------------------------------------------------------------
CREATE TABLE `iot_report_threshold` (
CREATE TABLE `iot_things_threshold` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`product_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '产品ID(0=全局默认)',
`device_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '设备ID(0=不限定设备)',
@ -71,6 +71,9 @@ CREATE TABLE `iot_report_threshold` (
`warning_value` decimal(20,6) DEFAULT NULL COMMENT '预警值',
`alarm_value` decimal(20,6) DEFAULT NULL COMMENT '报警值',
`control_value` decimal(20,6) DEFAULT NULL COMMENT '控制值',
`warning_rate` DECIMAL(18, 6) DEFAULT NULL COMMENT '变化速率预警值(单位/天)',
`alarm_rate` DECIMAL(18, 6) DEFAULT NULL COMMENT '变化速率报警值(单位/天)' ,
`control_rate` DECIMAL(18, 6) DEFAULT NULL COMMENT '变化速率控制值(单位/天)',
`unit` varchar(32) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '单位',
`create_by` varchar(64) COLLATE utf8_unicode_ci DEFAULT '' COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
@ -117,7 +120,7 @@ VALUES
('DEVICE_RAIN_GAUGE_V1', '雨量计监测分析报告V1', 'RAIN_GAUGE', 1, 'templates/DEVICE_RAIN_GAUGE_V1.docx', '0', '0', 'admin', NOW());
-- ------------------------------------------------------------
-- 2.3 报告生成记录表(保留期 1 个月:expire_time = create_time + 30 天)
-- 2.3 报告生成记录表(保留期由 LogCleanTask#cleanLogs(days) 统一管理,按 create_time 倒推清理)
-- ------------------------------------------------------------
CREATE TABLE `report_record` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
@ -131,7 +134,6 @@ CREATE TABLE `report_record` (
`status` varchar(16) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '状态(CREATING/SUCCESS/FAILED)',
`file_name` varchar(256) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '文件名称',
`file_url` varchar(512) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '文件地址',
`expire_time` datetime DEFAULT NULL COMMENT '过期时间(= create_time + 30 天)',
`error_message` varchar(1024) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '失败原因',
`tenant_id` bigint(20) DEFAULT NULL COMMENT '租户ID',
`create_by` varchar(64) COLLATE utf8_unicode_ci DEFAULT '' COMMENT '创建者',
@ -140,7 +142,6 @@ CREATE TABLE `report_record` (
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_device_time` (`device_id`, `create_time`) USING BTREE,
KEY `idx_expire` (`expire_time`) USING BTREE,
UNIQUE KEY `uk_report_no` (`report_no`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='报告生成记录';

View File

@ -1,55 +0,0 @@
-- 报告阈值初始化脚本(基于 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,110 @@
-- 报告阈值初始化脚本(基于 JGJ 8-2016、GB 50497-2019 等行业标准)
-- 使用方式:在 fastbee 数据库中执行此脚本
-- 说明:product_id=0 表示全局默认,可被产品级/设备级配置覆盖
-- 1. 深部位移(累计位移控制值通常 30-100mm,速率预警值 2mm/d)
INSERT INTO iot_things_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_things_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_things_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_things_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_things_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_things_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_things_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_things_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_things_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_things_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_things_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. 预警值 < 报警值 < 控制值,系统会按当前值与阈值的比较自动判断安全等级。
-- 结构安全监测改造:设备级基准点 + 阈值速率维度(双控)
-- 执行前请确认已备份,且字段尚未存在(MySQL 不支持 ADD COLUMN IF NOT EXISTS,重复执行会报错)
-- ============================================================
-- 1. 设备级基准点(相对测量用)
-- 静力水准等设备的读数是相对基准点的,累计变化量需按
-- 「(当前读数 − 基准点当前读数) − (初始读数 − 基准点初始读数)」计算。
-- deviceId 全局唯一,因此天然支持跨网关引用
-- (如 GW2 的监测点引用 GW1 下的参考点)。
-- 留空表示该设备为绝对量测量,不参与基准点换算。
-- ============================================================
ALTER TABLE iot_device
ADD COLUMN baseline_device_id BIGINT NULL COMMENT '基准点设备ID(相对测量用,留空=绝对量)' AFTER serial_number;
-- ============================================================
-- 2. 阈值表:变化速率维度
-- 依据 GB 50497-2019 第 8.0.4 条(强制性条文):
-- 监测报警按「累计变化量」与「变化速率」双控,任一超限即取较严重等级。
-- 单位统一为「单位/天」,例如 mm/d、kN/d。
-- ============================================================
ALTER TABLE iot_things_threshold
ADD COLUMN warning_rate DECIMAL(18, 6) NULL COMMENT '变化速率预警值(单位/天)' AFTER control_value,
ADD COLUMN alarm_rate DECIMAL(18, 6) NULL COMMENT '变化速率报警值(单位/天)' AFTER warning_rate,
ADD COLUMN control_rate DECIMAL(18, 6) NULL COMMENT '变化速率控制值(单位/天)' AFTER alarm_rate;
-- ============================================================
-- 附:各行业规范推荐取值(供配置产品阈值时参考,不写入全局默认值)
-- 未配置阈值的产品将输出「待评估」,不会伪造为「安全」。
--
-- 监测项目 累计控制值 速率预警值 依据
-- 深部位移(测斜) 40~50mm (0.4%~0.5%H) 3~5 mm/d GB 50497-2019
-- 支护结构顶部位移 25~30mm (0.2%~0.3%H) 3~5 mm/d GB 50497-2019
-- 周边地表沉降 25~35mm 3~5 mm/d GB 50497-2019
-- 管片结构沉降 硬土 10~20 / 软土 20~30mm 2~3 mm/d GB 50911-2013
-- 管片结构隆起 10mm 1 mm/d GB 50911-2013
-- 净空收敛(测距仪) 0.2%D 或 10mm 3 mm/d GB 50911-2013
-- 差异沉降 0.04%La 3 mm/d GB 50911-2013
-- 地下水位变化 1000~2000mm(常年变幅外) 500 mm/d GB 50497-2019 表8.0.5
-- 裂缝(结构性/既有) 1.5~3mm 持续发展 GB 50497-2019 表8.0.5
-- 裂缝(地表/新增) 1~3mm 持续发展 GB 50497-2019 表8.0.5
-- 锚索/支撑轴力 构件承载力设计值的 80% ≥80% 且持续增长 GB 50497-2019 8.0.6
-- 管线沉降(刚性/压力) 10~20mm 2 mm/d GB 50497-2019 表8.0.5
-- 构筑物及无压管线 20~25mm 2 mm/d GB 50157-2013
-- 建筑物倾斜 — 2°/d GB 50157-2013
--
-- 三级取值比例(设计允许值为基准):
-- 预警值 ≈ 60%~70% 报警值 ≈ 80% 控制值 = 100%
-- 速率细则:
-- 达到规定值、或连续 3 次超过该值的 70%,即应预警;
-- 底板浇筑完成后,速率不宜超过速率预警值的 70%。
-- 注:雨量计不是安全指标,规范中是工况触发因素(雨季/暴雨后须加测),
-- 不应参与预警判定。
-- ============================================================

View File

@ -1,77 +0,0 @@
-- ============================================================
-- 报告阈值表创建 + 行业标准默认值初始化
-- 数据库: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
-- ============================================================

View File

@ -26,7 +26,7 @@ INSERT INTO `sys_job` (
'0', -- 状态:正常
'admin', -- 创建者
NOW(), -- 创建时间
'每天凌晨0点自动清理15天前的日志数据(iot_event_log、sys_oper_log、sys_logininfor、iot_alert_log、sys_job_log、notify_log)'
'每天凌晨0点自动清理30天前的日志与报告数据(iot_event_log、sys_oper_log、sys_logininfor、iot_alert_log、sys_job_log、notify_log、iot_platform_push_log、report_record)'
);
-- ============================================
@ -38,6 +38,8 @@ INSERT INTO `sys_job` (
-- 4. iot_alert_log - 设备告警日志
-- 5. sys_job_log - 定时任务日志
-- 6. notify_log - 通知日志
-- 7. iot_platform_push_log - 平台对接推送日志
-- 8. report_record - 报告生成记录(按 create_time 倒推清理,不再使用 expire_time 字段)
--
-- 注意:iot_device_log 表(设备监测日志)不参与清理
-- 该表为核心数据,目前有 214 万条记录,需要数据迁移方案

View File

@ -1,37 +0,0 @@
-- ============================================================
-- 监测阈值配置菜单权限初始化
-- 说明:在 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

@ -1,460 +0,0 @@
-- =============================================
-- DCP 平台内置接口和流程初始化数据
-- 使用方法:
-- 1. 先在平台管理页面新增 DCP 平台实例(code='dcp')
-- 2. 执行本脚本前,将下方 @dcp_platform_id 替换为实际的平台 ID
-- 或直接执行:SET @dcp_platform_id = (SELECT id FROM platform_instance WHERE code='dcp' LIMIT 1);
-- =============================================
SET @dcp_platform_id = (SELECT id FROM platform_instance WHERE code = 'dcp' LIMIT 1);
-- 重新执行时先清理本脚本维护的流程和接口,避免旧配置被 LIMIT 1 命中。
DELETE s FROM platform_flow_step s
INNER JOIN platform_flow f ON f.id = s.flow_id
WHERE f.platform_id = @dcp_platform_id
AND f.code IN (
'dcp_init', 'dcp_token_refresh', 'dcp_onboard_inclino',
'dcp_onboard_stress', 'dcp_onboard_hyeto',
'dcp_onboard_static_level', 'dcp_onboard_deep_inclino'
);
DELETE FROM platform_flow
WHERE platform_id = @dcp_platform_id
AND code IN (
'dcp_init', 'dcp_token_refresh', 'dcp_onboard_inclino',
'dcp_onboard_stress', 'dcp_onboard_hyeto',
'dcp_onboard_static_level', 'dcp_onboard_deep_inclino'
);
DELETE FROM platform_interface
WHERE platform_id = @dcp_platform_id
AND code IN (
'dcp_auth', 'dcp_get_user', 'dcp_get_projects', 'dcp_create_point',
'dcp_bind_inclino', 'dcp_bind_stress', 'dcp_bind_hyeto',
'dcp_bind_static_level', 'dcp_bind_deep_inclino',
'dcp_push_inclino', 'dcp_push_stress', 'dcp_push_hyeto',
'dcp_push_static_level'
);
-- =============================================
-- 一、接口定义(13个)
-- =============================================
-- 1. 认证接口
INSERT INTO `platform_interface` (`platform_id`,`name`,`code`,`method`,`path`,`content_type`,`request_schema`,`response_schema`,`sort_order`,`remark`,`create_by`,`create_time`)
VALUES (@dcp_platform_id, '获取Token', 'dcp_auth', 'POST', '/connect/token', 'application/x-www-form-urlencoded',
'[
{"name":"grant_type","type":"string","source":"fixed","value":"password","required":true,"desc":"授权类型"},
{"name":"scope", "type":"string","source":"auth", "field":"scope", "required":true,"desc":"scope"},
{"name":"client_id", "type":"string","source":"auth", "field":"clientId", "required":true,"desc":"客户端ID"},
{"name":"userName", "type":"string","source":"auth", "field":"userName", "required":true,"desc":"用户名"},
{"name":"password", "type":"string","source":"auth", "field":"password", "required":true,"desc":"密码"}
]',
'[
{"name":"access_token","type":"string","path":"access_token","desc":"访问Token"},
{"name":"expires_in", "type":"number","path":"expires_in", "desc":"过期秒数"},
{"name":"token_type", "type":"string","path":"token_type", "desc":"Token类型"}
]',
1, 'DCP平台认证,获取access_token', 'system', NOW());
-- 2. 获取用户ID
INSERT INTO `platform_interface` (`platform_id`,`name`,`code`,`method`,`path`,`content_type`,`request_schema`,`response_schema`,`sort_order`,`remark`,`create_by`,`create_time`)
VALUES (@dcp_platform_id, '获取用户ID', 'dcp_get_user', 'GET', '/api/app/user/user-id', 'application/json',
'[]',
'[
{"name":"id", "type":"string","path":"id", "desc":"用户ID"},
{"name":"userName", "type":"string","path":"userName", "desc":"用户名"},
{"name":"phoneNumber", "type":"string","path":"phoneNumber", "desc":"手机号"}
]',
2, '获取当前登录用户信息', 'system', NOW());
-- 3. 获取项目列表
INSERT INTO `platform_interface` (`platform_id`,`name`,`code`,`method`,`path`,`content_type`,`request_schema`,`response_schema`,`sort_order`,`remark`,`create_by`,`create_time`)
VALUES (@dcp_platform_id, '获取项目列表', 'dcp_get_projects', 'GET', '/api/app/project/by-user-id/{userId}', 'application/json',
'[
{"name":"userId","type":"string","source":"context","contextKey":"step2.id","required":true,"desc":"用户ID,来自上一步"}
]',
'[
{"name":"id", "type":"string","path":"id", "desc":"项目ID"},
{"name":"name", "type":"string","path":"name", "desc":"项目名称"}
]',
3, '获取用户下的完整项目列表', 'system', NOW());
-- 4. 创建测点
INSERT INTO `platform_interface` (`platform_id`,`name`,`code`,`method`,`path`,`content_type`,`request_schema`,`response_schema`,`sort_order`,`remark`,`create_by`,`create_time`)
VALUES (@dcp_platform_id, '创建测点', 'dcp_create_point', 'POST', '/api/app/point', 'application/json',
'[
{"name":"projectId","type":"string","source":"device","field":"platform_project_id","required":true,"desc":"项目ID"},
{"name":"name", "type":"string","source":"device","field":"serial_number","required":true,"desc":"测点名称,用设备序列号"},
{"name":"type", "type":"number","source":"fixed","value":3,"required":true,"desc":"测点类型,固定为3"}
]',
'[
{"name":"id","type":"string","path":"id","desc":"测点ID(pointId)"}
]',
4, '在DCP平台创建监测测点', 'system', NOW());
-- 5. 绑定倾角传感器
INSERT INTO `platform_interface` (`platform_id`,`name`,`code`,`method`,`path`,`content_type`,`request_schema`,`response_schema`,`sort_order`,`remark`,`create_by`,`create_time`)
VALUES (@dcp_platform_id, '绑定倾角传感器', 'dcp_bind_inclino', 'POST', '/api/app/inclino-sensor', 'application/json',
'[
{"name":"projectId", "type":"string","source":"device", "field":"platform_project_id", "required":true,"desc":"项目ID"},
{"name":"pointId", "type":"string","source":"context","contextKey":"step1.id","required":true,"desc":"测点ID,来自创建测点步骤"},
{"name":"deviceType", "type":"number","source":"fixed", "value":5, "required":true,"desc":"设备类型,倾角=5"},
{"name":"name", "type":"string","source":"device", "field":"serial_number","required":true,"desc":"传感器名称"},
{"name":"moduleId", "type":"string","source":"uuid", "required":true,"desc":"模块ID,系统自动生成UUID"},
{"name":"virtualId", "type":"string","source":"uuid", "required":true,"desc":"虚拟ID,系统自动生成UUID"},
{"name":"dH", "type":"number","source":"fixed", "value":0, "required":false,"desc":"高程"}
]',
'[
{"name":"id","type":"string","path":"id","desc":"DCP设备ID(deviceId)"}
]',
5, '将倾角传感器绑定到测点', 'system', NOW());
-- 6. 绑定轴力计
INSERT INTO `platform_interface` (`platform_id`,`name`,`code`,`method`,`path`,`content_type`,`request_schema`,`response_schema`,`sort_order`,`remark`,`create_by`,`create_time`)
VALUES (@dcp_platform_id, '绑定轴力计', 'dcp_bind_stress', 'POST', '/api/app/stress-sensor', 'application/json',
'[
{"name":"projectId", "type":"string","source":"device", "field":"platform_project_id", "required":true,"desc":"项目ID"},
{"name":"pointId", "type":"string","source":"context","contextKey":"step1.id","required":true,"desc":"测点ID"},
{"name":"deviceType", "type":"number","source":"fixed", "value":10, "required":true,"desc":"设备类型,轴力计=10"},
{"name":"name", "type":"string","source":"device", "field":"serial_number","required":true,"desc":"传感器名称"},
{"name":"factor", "type":"number","source":"fixed", "value":1, "required":false,"desc":"系数"},
{"name":"moduleId", "type":"string","source":"uuid", "required":true},
{"name":"virtualId", "type":"string","source":"uuid", "required":true}
]',
'[{"name":"id","type":"string","path":"id","desc":"DCP设备ID"}]',
6, '将轴力计绑定到测点', 'system', NOW());
-- 7. 绑定雨量计
INSERT INTO `platform_interface` (`platform_id`,`name`,`code`,`method`,`path`,`content_type`,`request_schema`,`response_schema`,`sort_order`,`remark`,`create_by`,`create_time`)
VALUES (@dcp_platform_id, '绑定雨量计', 'dcp_bind_hyeto', 'POST', '/api/app/hyeto-sensor', 'application/json',
'[
{"name":"projectId", "type":"string","source":"device", "field":"platform_project_id", "required":true},
{"name":"pointId", "type":"string","source":"context","contextKey":"step1.id","required":true},
{"name":"deviceType", "type":"number","source":"fixed", "value":3, "required":true,"desc":"雨量计=3"},
{"name":"name", "type":"string","source":"device", "field":"serial_number","required":true},
{"name":"moduleId", "type":"string","source":"uuid", "required":true},
{"name":"virtualId", "type":"string","source":"uuid", "required":true}
]',
'[{"name":"id","type":"string","path":"id","desc":"DCP设备ID"}]',
7, '将雨量计绑定到测点', 'system', NOW());
-- 8. 绑定静力水准
INSERT INTO `platform_interface` (`platform_id`,`name`,`code`,`method`,`path`,`content_type`,`request_schema`,`response_schema`,`sort_order`,`remark`,`create_by`,`create_time`)
VALUES (@dcp_platform_id, '绑定静力水准', 'dcp_bind_static_level', 'POST', '/api/app/static-level-device', 'application/json',
'[
{"name":"projectId", "type":"string","source":"device", "field":"platform_project_id", "required":true},
{"name":"pointId", "type":"string","source":"context","contextKey":"step1.id","required":true},
{"name":"deviceType", "type":"number","source":"fixed", "value":1002, "required":true,"desc":"静力水准=1002"},
{"name":"name", "type":"string","source":"device", "field":"serial_number","required":true},
{"name":"moduleId", "type":"string","source":"uuid", "required":true},
{"name":"virtualId", "type":"string","source":"uuid", "required":true},
{"name":"sensorIds", "type":"array", "source":"referenceSubDeviceSensorIdPairs","required":true,"desc":"基准点与每个监测点成对绑定"}
]',
'[{"name":"id","type":"string","path":"id","desc":"DCP设备ID"}]',
8, '将静力水准传感器绑定到测点', 'system', NOW());
-- 9. 绑定深部位移
INSERT INTO `platform_interface` (`platform_id`,`name`,`code`,`method`,`path`,`content_type`,`request_schema`,`response_schema`,`sort_order`,`remark`,`create_by`,`create_time`)
VALUES (@dcp_platform_id, '绑定深部位移', 'dcp_bind_deep_inclino', 'POST', '/api/app/deep-inclino-device', 'application/json',
'[
{"name":"projectId", "type":"string","source":"device", "field":"platform_project_id","required":true},
{"name":"pointId", "type":"string","source":"context", "contextKey":"step1.id", "required":true},
{"name":"deviceType", "type":"number","source":"fixed", "value":1001, "required":true},
{"name":"name", "type":"string","source":"device", "field":"serial_number", "required":true},
{"name":"moduleId", "type":"string","source":"uuid", "required":true},
{"name":"virtualId", "type":"string","source":"uuid", "required":true},
{"name":"sensorIds", "type":"array", "source":"cascadedSubDeviceSensorIds", "required":true},
{"name":"sensorDepths", "type":"array", "source":"cascadedSubDeviceDepths", "required":true},
{"name":"holeDepth", "type":"number","source":"onboard_config", "field":"holeDepth", "required":true},
{"name":"holeElevation","type":"number","source":"onboard_config", "field":"holeElevation", "required":true}
]',
'[{"name":"id","type":"string","path":"id","desc":"DCP设备ID"}]',
9, '将深部位移设备绑定到测点', 'system', NOW());
-- =============================================
-- 1.5 推送数据接口(4个,设备绑定时选择推送接口用)
-- =============================================
-- 9. 推送倾角数据
INSERT INTO `platform_interface` (`platform_id`,`name`,`code`,`method`,`path`,`content_type`,`request_schema`,`response_schema`,`sort_order`,`remark`,`create_by`,`create_time`)
VALUES (@dcp_platform_id, '推送倾角数据', 'dcp_push_inclino', 'POST', '/api/app/inclino-data/many', 'application/json',
'[
{"name":"deviceId", "type":"string","source":"device","field":"platform_device_id","required":true,"desc":"DCP设备ID"},
{"name":"epoch", "type":"string","source":"ts","required":true,"desc":"采集时间,ISO8601格式"},
{"name":"xDeg", "type":"number","source":"thingsModel","required":true,"desc":"X轴倾角(度)"},
{"name":"yDeg", "type":"number","source":"thingsModel","required":true,"desc":"Y轴倾角(度)"},
{"name":"temperature","type":"number","source":"thingsModel","required":false,"desc":"温度"}
]',
'[]',
9, '推送倾角传感器实时数据到DCP', 'system', NOW());
-- 10. 推送轴力计数据
INSERT INTO `platform_interface` (`platform_id`,`name`,`code`,`method`,`path`,`content_type`,`request_schema`,`response_schema`,`sort_order`,`remark`,`create_by`,`create_time`)
VALUES (@dcp_platform_id, '推送轴力计数据', 'dcp_push_stress', 'POST', '/api/app/stress-data/many', 'application/json',
'[
{"name":"deviceId","type":"string","source":"device","field":"platform_device_id","required":true,"desc":"DCP设备ID"},
{"name":"epoch", "type":"string","source":"ts","required":true,"desc":"采集时间,ISO8601格式"},
{"name":"e", "type":"number","source":"thingsModel","required":true,"desc":"累计值(必须递增)"}
]',
'[]',
10, '推送轴力计实时数据到DCP', 'system', NOW());
-- 11. 推送雨量计数据
INSERT INTO `platform_interface` (`platform_id`,`name`,`code`,`method`,`path`,`content_type`,`request_schema`,`response_schema`,`sort_order`,`remark`,`create_by`,`create_time`)
VALUES (@dcp_platform_id, '推送雨量计数据', 'dcp_push_hyeto', 'POST', '/api/app/hyeto-data/many', 'application/json',
'[
{"name":"deviceId","type":"string","source":"device","field":"platform_device_id","required":true,"desc":"DCP设备ID"},
{"name":"epoch", "type":"string","source":"ts","required":true,"desc":"采集时间,ISO8601格式"},
{"name":"value", "type":"number","source":"thingsModel","required":true,"desc":"累计雨量(必须递增)"}
]',
'[]',
11, '推送雨量计实时数据到DCP', 'system', NOW());
-- 12. 推送静力水准数据
INSERT INTO `platform_interface` (`platform_id`,`name`,`code`,`method`,`path`,`content_type`,`request_schema`,`response_schema`,`sort_order`,`remark`,`create_by`,`create_time`)
VALUES (@dcp_platform_id, '推送静力水准数据', 'dcp_push_static_level', 'POST', '/api/app/static-level-data/many', 'application/json',
'[
{"name":"deviceId", "type":"string","source":"device","field":"platform_device_id","required":true,"desc":"DCP设备ID"},
{"name":"sensorData","type":"array","source":"context","contextKey":"sensorData","required":true,"desc":"静力水准数组","itemSchema":[
{"name":"epoch","type":"string","source":"ts","desc":"采集时间"},
{"name":"height","type":"number","source":"thingsModel","desc":"高程"},
{"name":"temperature","type":"number","source":"thingsModel","desc":"温度"},
{"name":"sensorId","type":"string","source":"subDeviceId","desc":"DCP侧sensorId"}
]},
{"name":"ct","type":"number","source":"fixed","value":1,"required":true,"desc":"采集类型"}
]',
'[]',
12, '推送静力水准实时数据到DCP', 'system', NOW());
-- =============================================
-- 二、流程定义(7个)
-- =============================================
-- 流程1:平台初始化(flow_type=1)
INSERT INTO `platform_flow` (`platform_id`,`name`,`code`,`flow_type`,`sensor_type`,`enable`,`remark`,`create_by`,`create_time`)
VALUES (@dcp_platform_id, 'DCP平台初始化', 'dcp_init', 1, NULL, '1', '获取Token→userId,一次性执行', 'system', NOW());
SET @flow_init_id = LAST_INSERT_ID();
-- 初始化流程步骤
INSERT INTO `platform_flow_step` (`flow_id`,`step_order`,`interface_id`,`result_key`,`result_save`,`remark`)
VALUES (@flow_init_id, 1,
(SELECT id FROM platform_interface WHERE platform_id=@dcp_platform_id AND code='dcp_auth' LIMIT 1),
'step1',
'[{"field":"access_token","saveTo":"platform_instance.access_token"},{"field":"expires_in","saveTo":"platform_instance.token_expires_at"}]',
'获取access_token并持久化');
INSERT INTO `platform_flow_step` (`flow_id`,`step_order`,`interface_id`,`result_key`,`result_save`,`remark`)
VALUES (@flow_init_id, 2,
(SELECT id FROM platform_interface WHERE platform_id=@dcp_platform_id AND code='dcp_get_user' LIMIT 1),
'step2',
'[{"field":"id","saveTo":"extra_config.userId"}]',
'获取userId并保存');
-- 流程2:Token刷新(flow_type=3)
INSERT INTO `platform_flow` (`platform_id`,`name`,`code`,`flow_type`,`sensor_type`,`enable`,`remark`,`create_by`,`create_time`)
VALUES (@dcp_platform_id, 'DCP Token刷新', 'dcp_token_refresh', 3, NULL, '1', 'Token过期时自动触发', 'system', NOW());
SET @flow_token_id = LAST_INSERT_ID();
INSERT INTO `platform_flow_step` (`flow_id`,`step_order`,`interface_id`,`result_key`,`result_save`,`remark`)
VALUES (@flow_token_id, 1,
(SELECT id FROM platform_interface WHERE platform_id=@dcp_platform_id AND code='dcp_auth' LIMIT 1),
'tokenStep',
'[{"field":"access_token","saveTo":"platform_instance.access_token"},{"field":"expires_in","saveTo":"platform_instance.token_expires_at"}]',
'获取新Token并持久化');
-- 流程3:倾角设备入网(flow_type=2)
INSERT INTO `platform_flow` (`platform_id`,`name`,`code`,`flow_type`,`sensor_type`,`enable`,`remark`,`create_by`,`create_time`)
VALUES (@dcp_platform_id, 'DCP倾角入网', 'dcp_onboard_inclino', 2, 'inclino', '1', '创建测点→绑定倾角传感器', 'system', NOW());
SET @flow_inclino_id = LAST_INSERT_ID();
INSERT INTO `platform_flow_step` (`flow_id`,`step_order`,`interface_id`,`result_key`,`result_save`,`remark`)
VALUES (@flow_inclino_id, 1,
(SELECT id FROM platform_interface WHERE platform_id=@dcp_platform_id AND code='dcp_create_point' LIMIT 1),
'step1',
'[{"field":"id","saveTo":"device_mapping.platform_point_id"}]',
'创建测点,获取pointId并回填');
INSERT INTO `platform_flow_step` (`flow_id`,`step_order`,`interface_id`,`result_key`,`result_save`,`remark`)
VALUES (@flow_inclino_id, 2,
(SELECT id FROM platform_interface WHERE platform_id=@dcp_platform_id AND code='dcp_bind_inclino' LIMIT 1),
'step2',
'[{"field":"id","saveTo":"device_mapping.platform_device_id"}]',
'绑定传感器,获取DCP deviceId');
-- 流程4:轴力计设备入网(flow_type=2)
INSERT INTO `platform_flow` (`platform_id`,`name`,`code`,`flow_type`,`sensor_type`,`enable`,`remark`,`create_by`,`create_time`)
VALUES (@dcp_platform_id, 'DCP轴力计入网', 'dcp_onboard_stress', 2, 'stress', '1', '创建测点→绑定轴力计', 'system', NOW());
SET @flow_stress_id = LAST_INSERT_ID();
INSERT INTO `platform_flow_step` (`flow_id`,`step_order`,`interface_id`,`result_key`,`result_save`,`remark`)
VALUES (@flow_stress_id, 1,
(SELECT id FROM platform_interface WHERE platform_id=@dcp_platform_id AND code='dcp_create_point' LIMIT 1),
'step1',
'[{"field":"id","saveTo":"device_mapping.platform_point_id"}]',
'创建测点');
INSERT INTO `platform_flow_step` (`flow_id`,`step_order`,`interface_id`,`result_key`,`result_save`,`remark`)
VALUES (@flow_stress_id, 2,
(SELECT id FROM platform_interface WHERE platform_id=@dcp_platform_id AND code='dcp_bind_stress' LIMIT 1),
'step2',
'[{"field":"id","saveTo":"device_mapping.platform_device_id"}]',
'绑定轴力计');
-- 流程5:雨量计设备入网(flow_type=2)
INSERT INTO `platform_flow` (`platform_id`,`name`,`code`,`flow_type`,`sensor_type`,`enable`,`remark`,`create_by`,`create_time`)
VALUES (@dcp_platform_id, 'DCP雨量计入网', 'dcp_onboard_hyeto', 2, 'hyeto', '1', '创建测点→绑定雨量计', 'system', NOW());
SET @flow_hyeto_id = LAST_INSERT_ID();
INSERT INTO `platform_flow_step` (`flow_id`,`step_order`,`interface_id`,`result_key`,`result_save`,`remark`)
VALUES (@flow_hyeto_id, 1,
(SELECT id FROM platform_interface WHERE platform_id=@dcp_platform_id AND code='dcp_create_point' LIMIT 1),
'step1',
'[{"field":"id","saveTo":"device_mapping.platform_point_id"}]',
'创建测点');
INSERT INTO `platform_flow_step` (`flow_id`,`step_order`,`interface_id`,`result_key`,`result_save`,`remark`)
VALUES (@flow_hyeto_id, 2,
(SELECT id FROM platform_interface WHERE platform_id=@dcp_platform_id AND code='dcp_bind_hyeto' LIMIT 1),
'step2',
'[{"field":"id","saveTo":"device_mapping.platform_device_id"}]',
'绑定雨量计');
-- 流程6:静力水准设备入网(flow_type=2)
INSERT INTO `platform_flow` (`platform_id`,`name`,`code`,`flow_type`,`sensor_type`,`enable`,`remark`,`create_by`,`create_time`)
VALUES (@dcp_platform_id, 'DCP静力水准入网', 'dcp_onboard_static_level', 2, 'static_level', '1', '创建测点→绑定静力水准', 'system', NOW());
SET @flow_static_level_id = LAST_INSERT_ID();
INSERT INTO `platform_flow_step` (`flow_id`,`step_order`,`interface_id`,`result_key`,`result_save`,`remark`)
VALUES (@flow_static_level_id, 1,
(SELECT id FROM platform_interface WHERE platform_id=@dcp_platform_id AND code='dcp_create_point' LIMIT 1),
'step1',
'[{"field":"id","saveTo":"device_mapping.platform_point_id"}]',
'创建测点');
INSERT INTO `platform_flow_step` (`flow_id`,`step_order`,`interface_id`,`result_key`,`result_save`,`remark`)
VALUES (@flow_static_level_id, 2,
(SELECT id FROM platform_interface WHERE platform_id=@dcp_platform_id AND code='dcp_bind_static_level' LIMIT 1),
'step2',
'[{"field":"id","saveTo":"device_mapping.platform_device_id"}]',
'绑定静力水准');
-- 流程7:深部位移设备入网(flow_type=2)
INSERT INTO `platform_flow` (`platform_id`,`name`,`code`,`flow_type`,`sensor_type`,`enable`,`remark`,`create_by`,`create_time`)
VALUES (@dcp_platform_id, 'DCP深部位移入网', 'dcp_onboard_deep_inclino', 2, 'deep_inclino', '1', '创建测点→绑定深部位移', 'system', NOW());
SET @flow_deep_inclino_id = LAST_INSERT_ID();
INSERT INTO `platform_flow_step` (`flow_id`,`step_order`,`interface_id`,`result_key`,`result_save`,`remark`)
VALUES (@flow_deep_inclino_id, 1,
(SELECT id FROM platform_interface WHERE platform_id=@dcp_platform_id AND code='dcp_create_point' LIMIT 1),
'step1',
'[{"field":"id","saveTo":"device_mapping.platform_point_id"}]',
'创建测点');
INSERT INTO `platform_flow_step` (`flow_id`,`step_order`,`interface_id`,`result_key`,`result_save`,`remark`)
VALUES (@flow_deep_inclino_id, 2,
(SELECT id FROM platform_interface WHERE platform_id=@dcp_platform_id AND code='dcp_bind_deep_inclino' LIMIT 1),
'step2',
'[{"field":"id","saveTo":"device_mapping.platform_device_id"}]',
'绑定深部位移');
-- 更新已落库的平台接口配置,去掉 fixed:auto 和静力水准扁平 sensorIds 配置。
-- 执行前确认平台实例 code,如不是 dcp,请手动替换 @platform_id。
SET @platform_id = (SELECT id FROM platform_instance WHERE code = 'dcp' LIMIT 1);
UPDATE platform_interface
SET request_schema = '[
{"name":"projectId","type":"string","source":"device","field":"platform_project_id","required":true,"desc":"项目ID"},
{"name":"name", "type":"string","source":"device","field":"serial_number","required":true,"desc":"测点名称,用设备序列号"},
{"name":"type", "type":"number","source":"fixed","value":3,"required":true,"desc":"测点类型,固定为3"}
]', update_by = 'system', update_time = NOW()
WHERE platform_id = @platform_id AND code = 'dcp_create_point';
DELETE s FROM platform_flow_step s
INNER JOIN platform_flow f ON f.id = s.flow_id
WHERE f.platform_id = @platform_id AND f.code = 'dcp_init' AND s.step_order > 2;
UPDATE platform_flow
SET remark = '获取Token→userId,一次性执行', update_by = 'system', update_time = NOW()
WHERE platform_id = @platform_id AND code = 'dcp_init';
UPDATE platform_interface
SET request_schema = '[
{"name":"projectId", "type":"string","source":"device", "field":"platform_project_id", "required":true,"desc":"项目ID"},
{"name":"pointId", "type":"string","source":"context","contextKey":"step1.id","required":true,"desc":"测点ID,来自创建测点步骤"},
{"name":"deviceType", "type":"number","source":"fixed", "value":5, "required":true,"desc":"设备类型,倾角=5"},
{"name":"name", "type":"string","source":"device", "field":"serial_number","required":true,"desc":"传感器名称"},
{"name":"moduleId", "type":"string","source":"uuid", "required":true,"desc":"模块ID,系统自动生成UUID"},
{"name":"virtualId", "type":"string","source":"uuid", "required":true,"desc":"虚拟ID,系统自动生成UUID"},
{"name":"dH", "type":"number","source":"fixed", "value":0, "required":false,"desc":"高程"}
]',
update_by = 'system',
update_time = NOW()
WHERE platform_id = @platform_id
AND code = 'dcp_bind_inclino';
UPDATE platform_interface
SET request_schema = '[
{"name":"projectId", "type":"string","source":"device", "field":"platform_project_id", "required":true,"desc":"项目ID"},
{"name":"pointId", "type":"string","source":"context","contextKey":"step1.id","required":true,"desc":"测点ID"},
{"name":"deviceType", "type":"number","source":"fixed", "value":10, "required":true,"desc":"设备类型,轴力计=10"},
{"name":"name", "type":"string","source":"device", "field":"serial_number","required":true,"desc":"传感器名称"},
{"name":"factor", "type":"number","source":"fixed", "value":1, "required":false,"desc":"系数"},
{"name":"moduleId", "type":"string","source":"uuid", "required":true},
{"name":"virtualId", "type":"string","source":"uuid", "required":true}
]',
update_by = 'system',
update_time = NOW()
WHERE platform_id = @platform_id
AND code = 'dcp_bind_stress';
UPDATE platform_interface
SET request_schema = '[
{"name":"projectId", "type":"string","source":"device", "field":"platform_project_id", "required":true},
{"name":"pointId", "type":"string","source":"context","contextKey":"step1.id","required":true},
{"name":"deviceType", "type":"number","source":"fixed", "value":3, "required":true,"desc":"雨量计=3"},
{"name":"name", "type":"string","source":"device", "field":"serial_number","required":true},
{"name":"moduleId", "type":"string","source":"uuid", "required":true},
{"name":"virtualId", "type":"string","source":"uuid", "required":true}
]',
update_by = 'system',
update_time = NOW()
WHERE platform_id = @platform_id
AND code = 'dcp_bind_hyeto';
UPDATE platform_interface
SET request_schema = '[
{"name":"projectId", "type":"string","source":"device", "field":"platform_project_id", "required":true},
{"name":"pointId", "type":"string","source":"context","contextKey":"step1.id","required":true},
{"name":"deviceType", "type":"number","source":"fixed", "value":1002, "required":true,"desc":"静力水准=1002"},
{"name":"name", "type":"string","source":"device", "field":"serial_number","required":true},
{"name":"moduleId", "type":"string","source":"uuid", "required":true},
{"name":"virtualId", "type":"string","source":"uuid", "required":true},
{"name":"sensorIds", "type":"array", "source":"referenceSubDeviceSensorIdPairs","required":true,"desc":"基准点与每个监测点成对绑定"}
]',
update_by = 'system',
update_time = NOW()
WHERE platform_id = @platform_id
AND code = 'dcp_bind_static_level';
SELECT code, request_schema
FROM platform_interface
WHERE platform_id = @platform_id
AND code IN ('dcp_bind_inclino', 'dcp_bind_stress', 'dcp_bind_hyeto', 'dcp_bind_static_level');

View File

@ -113,6 +113,7 @@
"device.device-edit.148398-82": "Voice intercom",
"device.device-edit.148398-83": "Calculation Formula",
"device.device-edit.148398-84": "History Records",
"device.device-edit.148398-85": "Device Threshold",
"device.device-functionlog.399522-0": "Please Select A Device Slave:",
"device.device-functionlog.399522-1": "Please Select A Device Slave",
"device.device-functionlog.399522-2": "Slave Address:$",

View File

@ -200,6 +200,7 @@
"product.product-edit.473153-82": "Please select the protocol",
"product.product-edit.473153-83": "Sub products",
"product.product-edit.473153-84": "Polling task",
"product.product-edit.473153-85": "Product Threshold",
"product.product-firmware.420545-0": "Add",
"product.product-firmware.420545-1": "Refresh",
"product.product-firmware.420545-2": "Firmware Name",

View File

@ -113,6 +113,7 @@
"device.device-edit.148398-82": "语音对讲",
"device.device-edit.148398-83": "计算公式",
"device.device-edit.148398-84": "历史记录",
"device.device-edit.148398-85": "设备阈值",
"device.device-functionlog.399522-0": "请选择设备从机:",
"device.device-functionlog.399522-1": "请选择设备从机",
"device.device-functionlog.399522-2": "从机地址:$",

View File

@ -200,6 +200,7 @@
"product.product-edit.473153-82": "请选择协议",
"product.product-edit.473153-83": "子产品",
"product.product-edit.473153-84": "轮询任务",
"product.product-edit.473153-85": "产品阈值",
"product.product-firmware.420545-0": "新增",
"product.product-firmware.420545-1": "刷新",
"product.product-firmware.420545-2": "固件名称",

View File

@ -42,7 +42,6 @@
<template slot-scope="scope">{{ scope.row.startTime }} ~ {{ scope.row.endTime }}</template>
</el-table-column>
<el-table-column label="生成时间" prop="createTime" width="160" align="center" />
<el-table-column label="过期时间" prop="expireTime" width="160" align="center" />
<el-table-column label="状态" min-width="90" align="center">
<template slot-scope="scope">
<el-tag :type="statusType(scope.row.status)" size="mini">{{ statusText(scope.row.status) }}</el-tag>
@ -54,7 +53,7 @@
type="success"
icon="el-icon-download"
size="mini"
:disabled="scope.row.status !== 'SUCCESS' || isExpired(scope.row)"
:disabled="scope.row.status !== 'SUCCESS'"
@click="handleDownload(scope.row)"
>下载</el-button>
</template>
@ -149,9 +148,6 @@ export default {
this.$message.error('下载失败,请稍后重试');
});
},
isExpired(row) {
return !!row.expireTime && new Date(row.expireTime.replace(/-/g, '/')).getTime() < Date.now();
},
categoryText(code) {
const map = {
CRACK_METER: '裂缝计',

View File

@ -0,0 +1,301 @@
<template>
<div style="padding-left: 10px">
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-check" size="mini" :loading="saving" @click="save">
保存阈值
</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="el-icon-refresh" size="mini" @click="load">刷新</el-button>
</el-col>
<el-col :span="14">
<el-link type="info" :underline="false" style="padding-top: 5px">
{{ tipText }}
</el-link>
</el-col>
</el-row>
<el-table v-loading="loading" :data="rows" size="mini" border>
<el-table-column label="物模型名称" align="center" prop="modelName" min-width="140" />
<el-table-column label="标识符" align="center" prop="identifier" min-width="120" />
<el-table-column label="数据类型" align="center" width="90">
<template slot-scope="scope">
<dict-tag :options="dict.type.iot_data_type" :value="scope.row.datatype" />
</template>
</el-table-column>
<el-table-column label="单位" align="center" width="80" prop="unit" />
<el-table-column label="预警值" align="center" width="140">
<template slot-scope="scope">
<el-input v-model="scope.row.warningValue" size="mini" type="number" placeholder="未设置" />
</template>
</el-table-column>
<el-table-column label="报警值" align="center" width="140">
<template slot-scope="scope">
<el-input v-model="scope.row.alarmValue" size="mini" type="number" placeholder="未设置" />
</template>
</el-table-column>
<el-table-column label="控制值" align="center" width="140">
<template slot-scope="scope">
<el-input v-model="scope.row.controlValue" size="mini" type="number" placeholder="未设置" />
</template>
</el-table-column>
<el-table-column label="速率预警值" align="center" width="140">
<template slot-scope="scope">
<el-input v-model="scope.row.warningRate" size="mini" type="number" placeholder="单位/天" />
</template>
</el-table-column>
<el-table-column label="速率报警值" align="center" width="140">
<template slot-scope="scope">
<el-input v-model="scope.row.alarmRate" size="mini" type="number" placeholder="单位/天" />
</template>
</el-table-column>
<el-table-column label="速率控制值" align="center" width="140">
<template slot-scope="scope">
<el-input v-model="scope.row.controlRate" size="mini" type="number" placeholder="单位/天" />
</template>
</el-table-column>
<el-table-column v-if="scopeType === 'device'" label="产品阈值(继承)" align="center" min-width="160">
<template slot-scope="scope">
<span v-if="parentMap[scope.row.identifier]">{{ formatThreshold(parentMap[scope.row.identifier]) }}</span>
<span v-else style="color: #c0c4cc">未配置</span>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
import { listModel } from '@/api/iot/model';
import { getProduct } from '@/api/iot/product';
import { listThreshold, addThreshold, updateThreshold, delThreshold } from '@/api/iot/threshold';
/** 仅数值型物模型可配置阈值 */
const NUMERIC_TYPES = ['integer', 'decimal'];
export default {
name: 'ThresholdConfig',
dicts: ['iot_data_type'],
props: {
/** 产品ID */
productId: {
type: [Number, String],
required: true,
},
/** 设备ID,产品级传 0 */
deviceId: {
type: [Number, String],
default: 0,
},
/** 作用域:product=产品级,device=设备级 */
scopeType: {
type: String,
default: 'product',
validator: (v) => ['product', 'device'].indexOf(v) !== -1,
},
},
data() {
return {
loading: false,
saving: false,
rows: [],
/** 上级(产品级)阈值,仅设备级展示 */
parentMap: {},
monitorCategoryCode: '',
};
},
computed: {
tipText() {
return this.scopeType === 'device'
? '设备阈值优先级高于产品阈值;留空则继承所属产品的阈值'
: '产品阈值对该产品下所有设备生效,可被单个设备的设备阈值覆盖';
},
/** 保存阈值时落库的 deviceId:产品级为 0 */
targetDeviceId() {
return this.scopeType === 'device' ? this.toNum(this.deviceId) : 0;
},
},
watch: {
productId() {
this.load();
},
deviceId() {
this.load();
},
},
created() {
this.load();
},
methods: {
/** 入参可能为字符串(路由/表单来源),统一转数字 */
toNum(v) {
const n = Number(v);
return Number.isNaN(n) ? 0 : n;
},
/** 解析 specs 中的单位 */
parseUnit(specs) {
if (!specs) return '';
try {
const obj = typeof specs === 'string' ? JSON.parse(specs) : specs;
return obj && obj.unit ? obj.unit : '';
} catch (e) {
return '';
}
},
/** 加载:产品监测类别码 -> 物模型 -> 本级阈值(设备级额外加载产品级阈值) */
load() {
if (this.toNum(this.productId) <= 0) {
this.rows = [];
return;
}
this.loading = true;
this.loadCategoryCode()
.then(() => this.loadModels())
.then(() => this.loadThresholds())
.finally(() => {
this.loading = false;
});
},
/** 监测类别码从产品继承 */
loadCategoryCode() {
return getProduct(this.productId)
.then((res) => {
this.monitorCategoryCode = (res.data && res.data.monitorCategoryCode) || '';
})
.catch(() => {
this.monitorCategoryCode = '';
});
},
/** 物模型:产品级取产品物模型;设备级额外并入设备级物模型 */
loadModels() {
const base = { productId: this.productId, deviceId: 0, pageNum: 1, pageSize: 500 };
const tasks = [listModel(base)];
if (this.scopeType === 'device' && this.toNum(this.deviceId) > 0) {
tasks.push(listModel({ deviceId: this.deviceId, pageNum: 1, pageSize: 500 }));
}
return Promise.all(tasks).then((responses) => {
const merged = new Map();
responses.forEach((res) => {
const list = (res && res.rows) || [];
list.forEach((m) => {
if (!m || !m.identifier) return;
if (NUMERIC_TYPES.indexOf(m.datatype) === -1) return;
if (!merged.has(m.identifier)) {
merged.set(m.identifier, {
modelName: m.modelName,
identifier: m.identifier,
datatype: m.datatype,
unit: this.parseUnit(m.specs),
id: undefined,
warningValue: undefined,
alarmValue: undefined,
controlValue: undefined,
warningRate: undefined,
alarmRate: undefined,
controlRate: undefined,
});
}
});
});
this.rows = Array.from(merged.values());
});
},
/** 阈值回填 */
loadThresholds() {
const tasks = [listThreshold({ productId: this.productId, deviceId: this.targetDeviceId, pageSize: 500 })];
if (this.scopeType === 'device') {
tasks.push(listThreshold({ productId: this.productId, deviceId: 0, pageSize: 500 }));
}
return Promise.all(tasks).then((responses) => {
const own = (responses[0] && (responses[0].rows || responses[0].data)) || [];
const ownMap = {};
own.forEach((t) => {
if (t && t.metricIdentifier) ownMap[t.metricIdentifier] = t;
});
this.rows.forEach((row) => {
const t = ownMap[row.identifier];
row.id = t ? t.id : undefined;
row.warningValue = t && t.warningValue !== null ? t.warningValue : undefined;
row.alarmValue = t && t.alarmValue !== null ? t.alarmValue : undefined;
row.controlValue = t && t.controlValue !== null ? t.controlValue : undefined;
row.warningRate = t && t.warningRate !== null ? t.warningRate : undefined;
row.alarmRate = t && t.alarmRate !== null ? t.alarmRate : undefined;
row.controlRate = t && t.controlRate !== null ? t.controlRate : undefined;
});
if (this.scopeType === 'device') {
const parent = (responses[1] && (responses[1].rows || responses[1].data)) || [];
const map = {};
parent.forEach((t) => {
if (t && t.metricIdentifier) map[t.metricIdentifier] = t;
});
this.parentMap = map;
}
});
},
formatThreshold(t) {
if (!t) return '';
const parts = [];
if (t.warningValue !== null && t.warningValue !== undefined) parts.push('预警 ' + t.warningValue);
if (t.alarmValue !== null && t.alarmValue !== undefined) parts.push('报警 ' + t.alarmValue);
if (t.controlValue !== null && t.controlValue !== undefined) parts.push('控制 ' + t.controlValue);
return parts.length ? parts.join(' / ') : '未配置';
},
/** 是否有任一阈值值 */
hasAnyValue(row) {
return [
row.warningValue,
row.alarmValue,
row.controlValue,
row.warningRate,
row.alarmRate,
row.controlRate,
].some((v) => v !== undefined && v !== null && v !== '');
},
/** 批量保存:有值新增/更新,清空则删除已有记录 */
save() {
if (this.toNum(this.productId) <= 0) {
this.$modal.msgError('请先保存产品信息');
return;
}
const tasks = [];
this.rows.forEach((row) => {
const payload = {
productId: this.toNum(this.productId),
deviceId: this.targetDeviceId,
monitorCategoryCode: this.monitorCategoryCode,
metricIdentifier: row.identifier,
unit: row.unit,
warningValue: row.warningValue === '' ? undefined : row.warningValue,
alarmValue: row.alarmValue === '' ? undefined : row.alarmValue,
controlValue: row.controlValue === '' ? undefined : row.controlValue,
warningRate: row.warningRate === '' ? undefined : row.warningRate,
alarmRate: row.alarmRate === '' ? undefined : row.alarmRate,
controlRate: row.controlRate === '' ? undefined : row.controlRate,
};
if (this.hasAnyValue(row)) {
if (row.id) {
tasks.push(updateThreshold(Object.assign({ id: row.id }, payload)));
} else {
tasks.push(addThreshold(payload));
}
} else if (row.id) {
tasks.push(delThreshold(row.id));
}
});
if (!tasks.length) {
this.$modal.msgWarning('没有需要保存的阈值');
return;
}
this.saving = true;
Promise.all(tasks)
.then(() => {
this.$modal.msgSuccess('阈值保存成功');
this.load();
})
.finally(() => {
this.saving = false;
});
},
},
};
</script>

View File

@ -54,6 +54,25 @@
</el-button>
</el-input>
</el-form-item>
<!-- 基准点设备:相对测量类设备(静力水准等)需指定基准点,留空表示绝对量测量 -->
<el-form-item v-if="form.deviceType != 2" prop="baselineDeviceId">
<template slot="label">
<el-tooltip placement="top">
<div slot="content">
相对测量类设备(如静力水准)需指定基准点。<br/>
累计变化量按「(当前读数−基准点读数)−(初始读数−基准点初始读数)」换算。<br/>
留空表示绝对量测量,不参与基准点换算。<br/>
deviceId 全局唯一,支持跨网关引用(如 GW2 监测点引用 GW1 参考点)。
</div>
<span>基准点设备 <i class="el-icon-question"></i></span>
</el-tooltip>
</template>
<el-select v-model="form.baselineDeviceId" placeholder="留空=绝对量测量,不参与基准点换算" clearable
filterable size="small" style="width: 100%">
<el-option v-for="item in baselineDeviceOptions" :key="item.deviceId"
:label="item.deviceName + '(' + item.serialNumber + ')'" :value="Number(item.deviceId)"/>
</el-select>
</el-form-item>
<el-form-item v-if="openServerTip">
<template>
<el-alert type="info" show-icon :description="$t('device.device-edit.148398-10')"></el-alert>
@ -234,6 +253,17 @@
<device-log ref="deviceLog" :device="form"/>
</el-tab-pane>
<!-- 设备阈值(优先级高于产品阈值,留空则继承产品阈值) -->
<el-tab-pane name="deviceThreshold" :disabled="form.deviceId == 0" v-if="form.deviceType !== 3" lazy>
<span slot="label">{{ $t('device.device-edit.148398-85') }}</span>
<threshold-config
ref="deviceThreshold"
:product-id="form.productId"
:device-id="form.deviceId"
scope-type="device"
/>
</el-tab-pane>
<el-tab-pane name="alertUser" :disabled="form.deviceId == 0" v-if="form.deviceType !== 3">
<span slot="label" v-hasPermi="['iot:device:alert:user:list']">{{ $t('device.device-edit.148398-80') }}</span>
<alert-user ref="alertUser" :device="form"/>
@ -243,20 +273,20 @@
<device-alert ref="deviceAlert" :device="form"/>
</el-tab-pane>
<el-tab-pane name="deviceFuncLog" :disabled="form.deviceId == 0" v-if="form.deviceType !== 3" lazy>
<!-- <el-tab-pane name="deviceFuncLog" :disabled="form.deviceId == 0" v-if="form.deviceType !== 3" lazy>
<span slot="label">{{ $t('device.device-edit.148398-50') }}</span>
<device-func ref="deviceFuncLog" :device="form"/>
</el-tab-pane>
</el-tab-pane> -->
<el-tab-pane name="deviceMonitor" :disabled="form.deviceId == 0" v-if="form.deviceType !== 3">
<!-- <el-tab-pane name="deviceMonitor" :disabled="form.deviceId == 0" v-if="form.deviceType !== 3">
<span slot="label">{{ $t('device.device-edit.148398-51') }}</span>
<device-monitor ref="deviceMonitor" :device="form"/>
</el-tab-pane>
</el-tab-pane> -->
<el-tab-pane name="deviceStastic" :disabled="form.deviceId == 0" v-if="form.deviceType !== 3">
<!-- <el-tab-pane name="deviceStastic" :disabled="form.deviceId == 0" v-if="form.deviceType !== 3">
<span slot="label">{{ $t('device.device-edit.148398-52') }}</span>
<device-statistic ref="deviceStatistic" :device="form"/>
</el-tab-pane>
</el-tab-pane> -->
<!-- 轮询任务 -->
<el-tab-pane name="deviceModbusTask" :disabled="form.deviceId == 0" v-if="form.canConfigPoll === true" lazy>
@ -279,10 +309,10 @@
<device-variable ref="deviceVariable" :device="form"/>
</el-tab-pane>
<el-tab-pane name="inlineVideo" :disabled="form.deviceId == 0" v-if="form.deviceType !== 3" lazy>
<!-- <el-tab-pane name="inlineVideo" :disabled="form.deviceId == 0" v-if="form.deviceType !== 3" lazy>
<span slot="label">{{ $t('device.device-edit.148398-75') }}</span>
<device-inline-video ref="deviceInlineVideo" :sipRelationList="form.sipRelationList"/>
</el-tab-pane>
</el-tab-pane> -->
<!-- 用于设置间距 -->
<el-tab-pane disabled>
@ -436,7 +466,8 @@ import {
updateDevice,
generatorDeviceNum,
getMqttConnect,
assignDeviceUser
assignDeviceUser,
listDeviceShort
} from '@/api/iot/device';
import {getDeviceRunningStatus} from '@/api/iot/device';
import {listUser} from '@/api/system/user';
@ -446,6 +477,7 @@ import DeviceFunc from '@/views/iot/device/device-functionlog';
import RealTimeStatus from '@/views/iot/device/realTime-status';
import {clientOut} from '@/api/iot/netty';
import defaultSettings from '@/settings';
import thresholdConfig from '@/views/iot/components/threshold-config';
export default {
name: 'DeviceEdit',
@ -480,6 +512,7 @@ export default {
deviceInlineVideo,
deviceThingsModel,
DeviceHistoryTablePanel,
thresholdConfig,
},
watch: {
activeName(val) {
@ -489,6 +522,12 @@ export default {
});
}
},
// 产品变更(含编辑态回显、新增态选产品)时刷新基准点候选设备
'form.productId': {
handler(val) {
this.loadBaselineDeviceOptions();
},
},
},
computed: {
deviceStatus: {
@ -542,6 +581,8 @@ export default {
phonenumber: undefined,
status: 0,
},
// 基准点候选设备列表(同产品下的其他设备,支持跨网关)
baselineDeviceOptions: [],
// 设备开始状态
oldDeviceStatus: null,
deviceId: '',
@ -555,6 +596,8 @@ export default {
serialNumber: '',
deviceType: 1,
isSimulate: 0,
// 基准点设备ID,null/0 表示绝对量测量,不参与基准点换算
baselineDeviceId: null,
},
//mqtt参数查看
listQuery: {
@ -627,6 +670,22 @@ export default {
this.mqttUnSubscribe(this.form);
},
methods: {
/** 加载基准点候选设备:同产品下的其他设备(排除自身,支持跨网关引用) **/
loadBaselineDeviceOptions() {
if (!this.form.productId) {
this.baselineDeviceOptions = [];
return;
}
listDeviceShort({productId: this.form.productId, pageSize: 1000}).then((response) => {
const rows = (response && response.rows) || [];
// 排除自身:基准点不能指向自己
this.baselineDeviceOptions = rows.filter(
(item) => Number(item.deviceId) !== Number(this.form.deviceId)
);
}).catch(() => {
this.baselineDeviceOptions = [];
});
},
openAssignUserDialog() {
this.assignUserOpen = true;
this.selectedAssignUser = null;
@ -1088,6 +1147,9 @@ export default {
this.$refs['form'].validate((valid) => {
if (valid) {
// 基准点未选择时归一化为 0:后端 <if test="baselineDeviceId != null"> 才能落库覆盖旧值
// 0 与 null 在 ReportContextBuilder 中均表示「绝对量测量,不参与基准点换算」
this.form.baselineDeviceId = this.form.baselineDeviceId || 0;
if (this.form.deviceId != 0) {
updateDevice(this.form).then((response) => {
if (response.data == 0) {

View File

@ -233,6 +233,17 @@
<product-modbus ref="productModbus" :product="form" @sendPollType="sendPollType" />
</el-tab-pane>
<!-- 产品阈值(对该产品下所有设备生效,可被单个设备的设备阈值覆盖) -->
<el-tab-pane name="productThreshold" :disabled="form.productId == 0" v-if="form.deviceType !== 3" lazy>
<span slot="label">{{ $t('product.product-edit.473153-85') }}</span>
<threshold-config
ref="productThreshold"
:product-id="form.productId"
:device-id="0"
scope-type="product"
/>
</el-tab-pane>
<!-- 轮询任务(已隐藏) -->
<!-- <el-tab-pane
name="productModbusTask"
@ -309,6 +320,7 @@ import { getAllPoints } from '@/api/iot/template';
import { listMonitorCategories } from '@/api/iot/report';
import ProductModbus from '@/views/iot/product/product-modbus';
import defaultSettings from '@/settings';
import thresholdConfig from '@/views/iot/components/threshold-config';
export default {
name: 'ProductEdit',
@ -324,6 +336,7 @@ export default {
productScada,
productSub,
productModbusTask,
thresholdConfig,
},
data() {
return {