From ae37f21d6d0bf6b10d9e3cb528473078de1e20ac Mon Sep 17 00:00:00 2001 From: zhugongping Date: Tue, 22 Sep 2026 09:54:51 +0800 Subject: [PATCH] =?UTF-8?q?reportEngine=EF=BC=9A=E5=AE=8C=E5=96=84?= =?UTF-8?q?=E6=8A=A5=E5=91=8A=E5=AF=BC=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker/docker-compose-txy.yaml | 4 +- docs/fastbee-device-report-engine.md | 12 +- .../config/MybatisPlusMetaObjectHandler.java | 4 +- .../controller/device/DeviceController.java | 11 ++ .../report/ThingsThresholdController.java | 22 ++-- .../report/executor/ReportAsyncExecutor.java | 31 +++++ .../service/IThingsThresholdService.java | 16 +++ .../impl/ThingsThresholdServiceImpl.java | 21 ++++ .../report/threshold/ThingsThreshold.java | 6 + .../iot/model/gateWay/SubDeviceListVO.java | 6 + .../fastbee/iot/service/IDeviceService.java | 9 ++ .../iot/service/impl/DeviceServiceImpl.java | 25 +++- .../resources/mapper/iot/SubGatewayMapper.xml | 2 + sql/fastbee-report.sql | 2 +- yack-web/src/api/iot/device.js | 9 ++ yack-web/src/api/iot/report.js | 2 +- yack-web/src/api/iot/threshold.js | 4 +- yack-web/src/views/bigScreen/home.vue | 4 +- yack-web/src/views/dataCenter/analysis.vue | 4 +- .../components/DeviceHistoryTablePanel.vue | 52 ++++++++ .../src/views/dataCenter/reportHistory.vue | 116 +++++++++++++++++- .../views/iot/components/threshold-config.vue | 14 ++- yack-web/src/views/iot/device/device-edit.vue | 6 +- yack-web/src/views/iot/device/device-sub.vue | 86 ++++++++++++- 24 files changed, 428 insertions(+), 40 deletions(-) diff --git a/docker/docker-compose-txy.yaml b/docker/docker-compose-txy.yaml index 7703f367..959b3b13 100644 --- a/docker/docker-compose-txy.yaml +++ b/docker/docker-compose-txy.yaml @@ -7,13 +7,13 @@ networks: services: minio: - image: minio/minio:latest + image: quay.io/minio/minio:latest container_name: minio restart: always # privileged: true 它几乎拥有与宿主机 root 相同的所有权限;MinIO 是纯用户空间的对象存储服务;无需用到特权模式,故而安全性与最小权限原则 networks: network: - ipv4_address: 177.7.0.6 + ipv4_address: 176.6.0.6 ports: - "9000:9000" - "9001:9001" diff --git a/docs/fastbee-device-report-engine.md b/docs/fastbee-device-report-engine.md index 4d0d579c..95dbd4d2 100644 --- a/docs/fastbee-device-report-engine.md +++ b/docs/fastbee-device-report-engine.md @@ -4,7 +4,7 @@ > > **本 Skill 目标**:基于已落库的 IoT 物模型监测数据,为单个监测设备按「设备 + 时间范围」生成专业分析报告(Word `.docx`),支持历史报表下载,**保留期 1 个月**。 > -> **前端入口(2 处)**:① 大屏分析模式;② 数据中心分析页。两处均通过「选设备 + 选时间」即可生成并导出报表;另有历史报表列表可下载。 +> **前端入口(2 处)**:① 大屏分析模式;② 数据中心分析页。两处均通过「选设备 + 选时间」即可生成并导出分析报告;另有历史报表列表可下载。 > > **存储决策**:**已确认未购买云 OSS 服务 → 统一使用自建 MinIO**(S3 协议兼容,复用现有 `fastbee-oss` 插件,不改上传链路)。 > @@ -43,7 +43,7 @@ 4. 结构化报告阈值:告警阈值藏在 LiteFlow `iot_scene` 脚本里,报告不可直接读 → 新增 `iot_things_threshold`。 5. Java 端图表(JFreeChart)与 Word 模板引擎(poi-tl)。 6. MinIO 默认存储配置。 -7. 前端两处「生成并导出报表」按钮 + 历史报表页(列表/下载/过期)。 +7. 前端两处「生成并导出分析报告」按钮 + 历史报表页(列表/下载/过期)。 ## 1.4 前端现状 @@ -262,7 +262,7 @@ UploadResult r = OssFactory.instance(redis) ## 5.1 入口一:大屏分析模式(`bigScreen/home.vue`) -- 在「分析条件」卡片“查询数据和曲线”旁新增按钮 **「生成并导出报表」**。 +- 在「分析条件」卡片“查询数据和曲线”旁新增按钮 **「生成并导出分析报告」**。 - 参数直接取已有 `analysisQuery.deviceId` + `analysisQuery.timeRange`(`[startTime, endTime]`)。 - 交互:点击 → `POST /iot/report/generate`(loading 防重复)→ 成功拿 `reportNo/fileUrl` 直接触发下载,并提示可在历史报表查看。 - 未选设备或时间时按钮禁用。 @@ -288,7 +288,7 @@ async handleGenerateReport() { ## 5.2 入口二:数据中心(`dataCenter/analysis.vue`) -- 在筛选栏“查询/重置”旁新增 **「生成并导出报表」**。 +- 在筛选栏“查询/重置”旁新增 **「生成并导出分析报告」**。 - 参数取已有 `queryParams.deviceId` + `queryParams.dayDaterange`。 - 交互与 5.1 一致,复用同一 API 封装。 @@ -402,7 +402,7 @@ CREATE TABLE report_record ( 6. **Provider**:`ReportDataProvider`(委托 `DeviceLogMapper`)、`ThingsThresholdProvider`。 7. **渲染**:`ChartGenerator`(JFreeChart)+ `WordRenderer`(poi-tl)+ 模板 `DEVICE_CRACK_METER_V1.docx`。 8. **落库与下载**:`report_record` + `expire_time`;MinIO 生命周期规则;下载接口(校验过期)。 -9. **前端**:大屏分析模式 + 数据中心加「生成并导出报表」;历史报表页 + 下载。 +9. **前端**:大屏分析模式 + 数据中心加「生成并导出分析报告」;历史报表页 + 下载。 10. **闭环验证**:真实设备 + 时间范围 → 生成 → 下载 → 列表可见 → 过期处理验证。 --- @@ -420,7 +420,7 @@ CREATE TABLE report_record ( [ ] report_record 落库,expire_time = +30 天 [ ] 列表接口只返回未过期记录 [ ] 下载接口可下载;过期返回明确提示 -[ ] 大屏分析模式与数据中心均可「生成并导出报表」 +[ ] 大屏分析模式与数据中心均可「生成并导出分析报告」 [ ] 历史报表页面可查看/下载 [ ] 设备 304259363251 完整闭环验证通过 ``` diff --git a/fastbee-framework/src/main/java/com/fastbee/framework/mybatis/config/MybatisPlusMetaObjectHandler.java b/fastbee-framework/src/main/java/com/fastbee/framework/mybatis/config/MybatisPlusMetaObjectHandler.java index 93b89d2e..07be2a24 100644 --- a/fastbee-framework/src/main/java/com/fastbee/framework/mybatis/config/MybatisPlusMetaObjectHandler.java +++ b/fastbee-framework/src/main/java/com/fastbee/framework/mybatis/config/MybatisPlusMetaObjectHandler.java @@ -47,7 +47,7 @@ public class MybatisPlusMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { - log.debug("mybatisPlus插入填充--------------"); + //log.debug("mybatisPlus插入填充--------------"); Date date = new Date(); fillValIfNullByName("createTime", date, metaObject, false); @@ -59,7 +59,7 @@ public class MybatisPlusMetaObjectHandler implements MetaObjectHandler { @Override public void updateFill(MetaObject metaObject) { - log.debug("mybatisPlus更新填充--------------"); + //log.debug("mybatisPlus更新填充--------------"); fillValIfNullByName("updateTime", new Date(), metaObject, true); fillValIfNullByName("updateBy", getUserName(), metaObject, true); } diff --git a/fastbee-open-api/src/main/java/com/fastbee/controller/device/DeviceController.java b/fastbee-open-api/src/main/java/com/fastbee/controller/device/DeviceController.java index 4f5a86c3..b05ba2b3 100644 --- a/fastbee-open-api/src/main/java/com/fastbee/controller/device/DeviceController.java +++ b/fastbee-open-api/src/main/java/com/fastbee/controller/device/DeviceController.java @@ -285,6 +285,17 @@ public class DeviceController extends BaseController { return deviceService.updateDevice(device); } + /** + * 批量更新设备基准点(仅写入 baseline_device_id) + */ + @PreAuthorize("@ss.hasPermi('iot:device:edit')") + @Log(title = "批量更新设备基准点", businessType = BusinessType.UPDATE) + @PostMapping("/baseline/batch") + @ApiOperation("批量更新设备基准点") + public AjaxResult updateBaselineBatch(@RequestBody List devices) { + return AjaxResult.success(deviceService.updateDeviceBaselineBatch(devices)); + } + /** * 重置设备状态 */ diff --git a/fastbee-open-api/src/main/java/com/fastbee/controller/report/ThingsThresholdController.java b/fastbee-open-api/src/main/java/com/fastbee/controller/report/ThingsThresholdController.java index 459c249a..86beb20f 100644 --- a/fastbee-open-api/src/main/java/com/fastbee/controller/report/ThingsThresholdController.java +++ b/fastbee-open-api/src/main/java/com/fastbee/controller/report/ThingsThresholdController.java @@ -79,25 +79,31 @@ public class ThingsThresholdController extends BaseController { } /** - * 新增阈值 + * 批量新增阈值 */ @PreAuthorize("@ss.hasPermi('iot:device:list')") @Log(title = "监测阈值配置", businessType = BusinessType.INSERT) @PostMapping - @ApiOperation("新增阈值") - public AjaxResult add(@RequestBody ThingsThreshold threshold) { - return toAjax(thingsThresholdService.insertThreshold(threshold)); + @ApiOperation("批量新增阈值") + public AjaxResult add(@RequestBody List thresholds) { + if (thresholds == null || thresholds.isEmpty()) { + return AjaxResult.error("新增列表不能为空"); + } + return toAjax(thingsThresholdService.insertThresholdBatch(thresholds)); } /** - * 修改阈值 + * 批量修改阈值 */ @PreAuthorize("@ss.hasPermi('iot:device:list')") @Log(title = "监测阈值配置", businessType = BusinessType.UPDATE) @PutMapping - @ApiOperation("修改阈值") - public AjaxResult edit(@RequestBody ThingsThreshold threshold) { - return toAjax(thingsThresholdService.updateThreshold(threshold)); + @ApiOperation("批量修改阈值") + public AjaxResult edit(@RequestBody List thresholds) { + if (thresholds == null || thresholds.isEmpty()) { + return AjaxResult.error("修改列表不能为空"); + } + return toAjax(thingsThresholdService.updateThresholdBatch(thresholds)); } /** diff --git a/fastbee-report/src/main/java/com/fastbee/report/executor/ReportAsyncExecutor.java b/fastbee-report/src/main/java/com/fastbee/report/executor/ReportAsyncExecutor.java index efa9677c..1f674f33 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/executor/ReportAsyncExecutor.java +++ b/fastbee-report/src/main/java/com/fastbee/report/executor/ReportAsyncExecutor.java @@ -19,12 +19,16 @@ import com.fastbee.report.period.ReportPeriodResolver; import com.fastbee.report.period.TimeWindow; import com.fastbee.report.record.ReportRecord; import com.fastbee.report.record.ReportRecordService; +import com.fastbee.report.data.MetricData; +import com.fastbee.report.model.Statistics; import com.fastbee.report.renderer.ReportRenderer; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.stereotype.Component; import javax.annotation.Resource; +import java.util.List; +import java.util.Map; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneOffset; @@ -111,6 +115,15 @@ public class ReportAsyncExecutor { ReportAnalyzer analyzer = analyzerFactory.get(context.getCategory()); AnalysisResult analysis = analyzer.analyze(context, request); + + // 无有效监测数据时终止生成,避免导出空白文档 + if (isNoEffectiveData(context, analysis)) { + log.warn("报告生成中止:时间范围内无有效监测数据 reportNo={} deviceId={}", + reportNo, request.getDeviceId()); + reportRecordService.markFailed(record, "所选时间范围内无有效监测数据,无法生成报告"); + return; + } + ReportModel model = reportAssembler.assemble(context, analysis); byte[] docBytes = reportRenderer.render(model); @@ -129,6 +142,24 @@ public class ReportAsyncExecutor { } } + /** + * 判断本次报告是否无任何有效监测数据,避免导出空白文档。 + * + *

判定口径:① 时序数据列表为空(设备完全未上报);② 所有物模型统计 count 均为 0 + * (上报数据均无效或未落在统计窗口内);③ 未配置任何监测物模型(无统计数据)。

+ */ + private boolean isNoEffectiveData(ReportContext context, AnalysisResult analysis) { + List metricDataList = context.getMetricDataList(); + if (metricDataList == null || metricDataList.isEmpty()) { + return true; + } + Map statisticsMap = analysis.getStatistics(); + if (statisticsMap == null || statisticsMap.isEmpty()) { + return true; + } + return statisticsMap.values().stream().noneMatch(s -> s.getCount() > 0); + } + private void validate(ReportRequest request) { if (request == null || request.getDeviceId() == null) { throw new ReportException("请选择设备"); diff --git a/fastbee-report/src/main/java/com/fastbee/report/service/IThingsThresholdService.java b/fastbee-report/src/main/java/com/fastbee/report/service/IThingsThresholdService.java index b4e31d73..d6477507 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/service/IThingsThresholdService.java +++ b/fastbee-report/src/main/java/com/fastbee/report/service/IThingsThresholdService.java @@ -53,4 +53,20 @@ public interface IThingsThresholdService extends IService { * @return 影响行数 */ int deleteThresholdByIds(Long[] ids); + + /** + * 批量新增阈值 + * + * @param thresholds 阈值列表 + * @return 影响行数 + */ + int insertThresholdBatch(List thresholds); + + /** + * 批量修改阈值 + * + * @param thresholds 阈值列表 + * @return 影响行数 + */ + int updateThresholdBatch(List thresholds); } \ No newline at end of file diff --git a/fastbee-report/src/main/java/com/fastbee/report/service/impl/ThingsThresholdServiceImpl.java b/fastbee-report/src/main/java/com/fastbee/report/service/impl/ThingsThresholdServiceImpl.java index 19615aec..7d2ae40f 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/service/impl/ThingsThresholdServiceImpl.java +++ b/fastbee-report/src/main/java/com/fastbee/report/service/impl/ThingsThresholdServiceImpl.java @@ -8,6 +8,7 @@ import com.fastbee.report.mapper.ThingsThresholdMapper; import com.fastbee.report.service.IThingsThresholdService; import com.fastbee.report.threshold.ThingsThreshold; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.Arrays; import java.util.List; @@ -63,4 +64,24 @@ public class ThingsThresholdServiceImpl extends ServiceImpl thresholds) { + if (thresholds == null || thresholds.isEmpty()) { + return 0; + } + boolean ok = saveBatch(thresholds); + return ok ? thresholds.size() : 0; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public int updateThresholdBatch(List thresholds) { + if (thresholds == null || thresholds.isEmpty()) { + return 0; + } + boolean ok = updateBatchById(thresholds); + return ok ? thresholds.size() : 0; + } } \ No newline at end of file diff --git a/fastbee-report/src/main/java/com/fastbee/report/threshold/ThingsThreshold.java b/fastbee-report/src/main/java/com/fastbee/report/threshold/ThingsThreshold.java index 24f5540c..b5dfdcec 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/threshold/ThingsThreshold.java +++ b/fastbee-report/src/main/java/com/fastbee/report/threshold/ThingsThreshold.java @@ -1,5 +1,7 @@ package com.fastbee.report.threshold; +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Data; @@ -48,13 +50,17 @@ public class ThingsThreshold { private String unit; + @TableField(fill = FieldFill.INSERT) private String createBy; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @TableField(fill = FieldFill.INSERT) private Date createTime; + @TableField(fill = FieldFill.INSERT_UPDATE) private String updateBy; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @TableField(fill = FieldFill.INSERT_UPDATE) private Date updateTime; } diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/gateWay/SubDeviceListVO.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/gateWay/SubDeviceListVO.java index 91da6c42..c308459c 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/gateWay/SubDeviceListVO.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/gateWay/SubDeviceListVO.java @@ -57,4 +57,10 @@ public class SubDeviceListVO { @ApiModelProperty("在线状态") private Integer status; + + /** + * 基准点设备id(iot_device.baseline_device_id):同一子产品下作为相对测量基准的设备;0/null 表示绝对量测量 + */ + @ApiModelProperty("基准点设备id") + private Long baselineDeviceId; } diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/IDeviceService.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/IDeviceService.java index c103324f..7fd3cf03 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/IDeviceService.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/IDeviceService.java @@ -202,6 +202,15 @@ public interface IDeviceService { */ public AjaxResult updateDevice(Device device); + /** + * 批量更新设备基准点(仅写入 baseline_device_id) + *

同一子产品下通过单选确定一个基准点设备,组内其余设备指向该基准点;基准点设备自身写 0。

+ * + * @param devices 仅包含 deviceId 与 baselineDeviceId 的设备列表 + * @return 影响行数 + */ + public int updateDeviceBaselineBatch(List devices); + /** * 更新设备状态和定位 * diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/DeviceServiceImpl.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/DeviceServiceImpl.java index a3f55973..d9ec3eda 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/DeviceServiceImpl.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/DeviceServiceImpl.java @@ -325,6 +325,26 @@ public class DeviceServiceImpl implements IDeviceService { return AjaxResult.success("修改成功", 1); } + /** + * 批量更新设备基准点(仅写入 baseline_device_id) + *

组内其余设备指向基准点设备,基准点设备自身写 0。循环复用 updateDevice 的 baselineDeviceId 分支。

+ */ + @Override + public int updateDeviceBaselineBatch(List devices) { + if (devices == null || devices.isEmpty()) { + return 0; + } + int rows = 0; + for (Device device : devices) { + if (device.getDeviceId() == null) { + continue; + } + device.setUpdateTime(DateUtils.getNowDate()); + rows += deviceMapper.updateDevice(device); + } + return rows; + } + @Caching(evict = { @CacheEvict(cacheNames = "Device", key = "'selectDeviceBySerialNumber:' + #device.getSerialNumber()"), // @CacheEvict(cacheNames = "Device", key = "'selectDeviceByDeviceId:' + @@ -1024,7 +1044,10 @@ public class DeviceServiceImpl implements IDeviceService { } } fillShortListLatestReportTime(list, device.getEntrance()); - log.warn("当前selectDeviceShortList查询接口耗时: {}", System.currentTimeMillis() - startTime); + long took = System.currentTimeMillis() - startTime; + if (took > 1000L) { + log.warn("selectDeviceShortList took: {}", took); + } return list; } diff --git a/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/SubGatewayMapper.xml b/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/SubGatewayMapper.xml index 63bb7ab5..5fae3073 100644 --- a/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/SubGatewayMapper.xml +++ b/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/SubGatewayMapper.xml @@ -32,6 +32,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + @@ -51,6 +52,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"