reportEngine:完善报告导出,设置模型预警值
This commit is contained in:
parent
a27e99b031
commit
86b85f2327
@ -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
|
||||
|
||||
27
docker/docker-compose-txy.yaml
Normal file
27
docker/docker-compose-txy.yaml
Normal 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"
|
||||
@ -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. **前端**:大屏分析模式 + 数据中心加「生成并导出报表」;历史报表页 + 下载。
|
||||
|
||||
@ -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,12 +87,17 @@ 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);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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("报告文件不存在");
|
||||
}
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
result.setStatus(WarningAnalysis.STATUS_UNKNOWN);
|
||||
|
||||
if (threshold == null) {
|
||||
result.setStatus(WarningAnalysis.STATUS_UNKNOWN);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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;
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 "安全";
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
/**
|
||||
* 批量删除阈值
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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) > 产品级(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;
|
||||
@ -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);
|
||||
}
|
||||
@ -7,25 +7,31 @@ 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 >= now()
|
||||
<if test="deviceId != null">
|
||||
and device_id = #{deviceId}
|
||||
</if>
|
||||
<if test="period != null and period != ''">
|
||||
and period = #{period}
|
||||
</if>
|
||||
<if test="status != null and status != ''">
|
||||
and status = #{status}
|
||||
</if>
|
||||
<if test="keyword != null and keyword != ''">
|
||||
and (report_no like concat('%', #{keyword}, '%')
|
||||
or device_name like concat('%', #{keyword}, '%'))
|
||||
</if>
|
||||
<where>
|
||||
<if test="deviceId != null">
|
||||
and device_id = #{deviceId}
|
||||
</if>
|
||||
<if test="period != null and period != ''">
|
||||
and period = #{period}
|
||||
</if>
|
||||
<if test="status != null and status != ''">
|
||||
and status = #{status}
|
||||
</if>
|
||||
<if test="keyword != null and keyword != ''">
|
||||
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 < date_sub(now(), interval #{days} day)
|
||||
</delete>
|
||||
|
||||
</mapper>
|
||||
|
||||
@ -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
|
||||
@ -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;
|
||||
|
||||
/**
|
||||
* 固件版本
|
||||
*/
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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='报告生成记录';
|
||||
|
||||
|
||||
@ -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. 预警值 < 报警值 < 控制值,系统会按当前值与阈值的比较自动判断安全等级。
|
||||
110
sql/init_things_threshold.sql
Normal file
110
sql/init_things_threshold.sql
Normal 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%。
|
||||
-- 注:雨量计不是安全指标,规范中是工况触发因素(雨季/暴雨后须加测),
|
||||
-- 不应参与预警判定。
|
||||
-- ============================================================
|
||||
@ -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
|
||||
-- ============================================================
|
||||
@ -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 万条记录,需要数据迁移方案
|
||||
|
||||
@ -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
|
||||
-- ============================================================
|
||||
@ -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');
|
||||
@ -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:$",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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": "从机地址:$",
|
||||
|
||||
@ -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": "固件名称",
|
||||
@ -551,4 +552,4 @@
|
||||
"product.product-things-model.142341-201": "引用产品物模型标识",
|
||||
"product.product-things-model.142341-202": "暂无可用标识",
|
||||
"product.product-things-model.142341-203": "新增计算模型"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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: '裂缝计',
|
||||
|
||||
301
yack-web/src/views/iot/components/threshold-config.vue
Normal file
301
yack-web/src/views/iot/components/threshold-config.vue
Normal 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>
|
||||
@ -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) {
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user