From a2f9285d29391335493cd6673efa0ca76fcd9cba Mon Sep 17 00:00:00 2001 From: zhugongping Date: Tue, 6 Oct 2026 10:02:50 +0800 Subject: [PATCH] =?UTF-8?q?reportEngine=EF=BC=9Adcp=E6=8A=A5=E5=91=8A?= =?UTF-8?q?=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../system/SysUserSettingController.java | 26 ++++- .../src/main/resources/application.yml | 9 -- .../common/constant/UserSettingConstants.java | 13 +++ .../fastbee/common/enums/ReportTemplate.java | 92 ++++++++++++++++++ .../controller/report/ReportController.java | 18 ++++ fastbee-report/pom.xml | 6 ++ .../report/config/ReportProperties.java | 73 -------------- .../fastbee/report/core/ReportAssembler.java | 53 ++++++---- .../fastbee/report/core/ReportContext.java | 3 + .../report/core/ReportContextBuilder.java | 3 + .../fastbee/report/model/AnalysisResult.java | 2 +- .../fastbee/report/renderer/WordRenderer.java | 29 ++++++ .../main/resources/templates/DCPReport.docx | Bin 37200 -> 26238 bytes .../WordRendererDcpReportTemplateTest.java | 78 +++++++++++---- .../fastbee/system/domain/SysUserSetting.java | 20 ++++ .../system/mapper/SysUserSettingMapper.java | 15 +-- .../impl/SysUserSettingServiceImpl.java | 20 ++-- .../mapper/system/SysUserSettingMapper.xml | 7 +- sql/sys_user_setting.sql | 14 ++- yack-web/src/api/iot/report.js | 8 ++ .../setting/components/UserSettingForm.vue | 39 +++++++- 21 files changed, 387 insertions(+), 141 deletions(-) create mode 100644 fastbee-common/src/main/java/com/fastbee/common/enums/ReportTemplate.java delete mode 100644 fastbee-report/src/main/java/com/fastbee/report/config/ReportProperties.java diff --git a/fastbee-admin/src/main/java/com/fastbee/web/controller/system/SysUserSettingController.java b/fastbee-admin/src/main/java/com/fastbee/web/controller/system/SysUserSettingController.java index e4f4b7eb..66c41b2e 100644 --- a/fastbee-admin/src/main/java/com/fastbee/web/controller/system/SysUserSettingController.java +++ b/fastbee-admin/src/main/java/com/fastbee/web/controller/system/SysUserSettingController.java @@ -4,6 +4,7 @@ import com.fastbee.common.annotation.Log; import com.fastbee.common.core.controller.BaseController; import com.fastbee.common.core.domain.AjaxResult; import com.fastbee.common.enums.BusinessType; +import com.fastbee.common.enums.ReportTemplate; import com.fastbee.common.utils.SecurityUtils; import com.fastbee.system.domain.LoginRedirectOption; import com.fastbee.system.domain.SysUserSetting; @@ -92,7 +93,8 @@ public class SysUserSettingController extends BaseController { public AjaxResult update(@RequestBody SysUserSetting setting) { if (setting.getLoginRedirect() == null && setting.getQueryGatewayChild() == null - && setting.getShowChild() == null) { + && setting.getShowChild() == null + && setting.getReportTemplate() == null) { return AjaxResult.error("没有需要保存的配置项"); } // 只有在本次确实要改「登录后显示页面」时才做白名单校验; @@ -100,11 +102,33 @@ public class SysUserSettingController extends BaseController { if (setting.getLoginRedirect() != null && !isValidLoginRedirect(setting.getLoginRedirect())) { return AjaxResult.error("登录后跳转页面不合法,请从系统菜单中选择"); } + // 报告模板版式只接受已注册的枚举编码,挡住脏数据/未发布的模板 + if (setting.getReportTemplate() != null && !isValidReportTemplate(setting.getReportTemplate())) { + return AjaxResult.error("报告导出模板不合法,请从可选项中选择"); + } // 审计字段由服务端填充,不接受前端传入,避免越权写入他人操作名 userSettingService.updatePartial(SecurityUtils.getUserId(), setting, SecurityUtils.getUsername()); return AjaxResult.success(); } + /** + * 校验报告模板编码为已注册的模板版式 + * + *

取值即 {@code ReportTemplate.code},未注册的一律拒绝(而非静默回退默认版式), + * 避免用户选到未发布的模板后导出行为与预期不符。

+ */ + private boolean isValidReportTemplate(String code) { + if (code == null || code.trim().isEmpty()) { + return false; + } + for (ReportTemplate template : ReportTemplate.values()) { + if (template.getCode().equalsIgnoreCase(code.trim())) { + return true; + } + } + return false; + } + /** * 校验跳转目标为系统内已注册的一级页签 * diff --git a/fastbee-admin/src/main/resources/application.yml b/fastbee-admin/src/main/resources/application.yml index 4ea2e8cf..569f8a68 100644 --- a/fastbee-admin/src/main/resources/application.yml +++ b/fastbee-admin/src/main/resources/application.yml @@ -233,12 +233,3 @@ map: gaode_secret: d0561ad20142e74f8c59edc5deafdbe4 tianditu_ak: b920e6076a06aa279c974f5f76cf746e -# 设备监测报告导出配置(全局一刀切,test/prod 环境共用) -report: - # 模板编码(取值即 templates/ 下的模板名,V2 为按分类取默认的版本标记): - # 留空 或 V2 → 完整版,按监测分类走 DEVICE_{CATEGORY}_V2(共 10 节) - # DCPReport → DCP 平台专用三节版式(项目概况 / 测点数据 / 时程曲线), - # 测点表只输出 ThingsThreshold.is_primary=1 的主监测指标 - # 默认保持完整版(V2):切换版式是交付格式变更,需现场确认后由运维显式改为 DCPReport - template-code: V2 - diff --git a/fastbee-common/src/main/java/com/fastbee/common/constant/UserSettingConstants.java b/fastbee-common/src/main/java/com/fastbee/common/constant/UserSettingConstants.java index 5bc2c663..f1e74650 100644 --- a/fastbee-common/src/main/java/com/fastbee/common/constant/UserSettingConstants.java +++ b/fastbee-common/src/main/java/com/fastbee/common/constant/UserSettingConstants.java @@ -1,5 +1,7 @@ package com.fastbee.common.constant; +import com.fastbee.common.enums.ReportTemplate; + import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -80,6 +82,17 @@ public class UserSettingConstants { */ public static final boolean DEFAULT_SHOW_CHILD = false; + /** + * 报告导出模板版式的默认值 + * + *

对应 {@code sys_user_setting.report_template},取值即 {@code ReportTemplate.code}: + * 默认 {@code V2}(完整版,按监测分类取 {@code DEVICE_{CATEGORY}_V2})= 与引入本开关前完全一致。 + * 用户可在「系统设置」页切换为 {@code DCPReport}(DCP 平台三节版式)等其它版式。 + *

维度归属:报告导出由登录用户触发(异步线程已显式透传 SecurityContext,能拿到 userId), + * 故为 per-user 设置;无登录态不允许导出,不做全局兜底。 + */ + public static final String DEFAULT_REPORT_TEMPLATE = ReportTemplate.FULL_VERSION.getCode(); + private UserSettingConstants() { } diff --git a/fastbee-common/src/main/java/com/fastbee/common/enums/ReportTemplate.java b/fastbee-common/src/main/java/com/fastbee/common/enums/ReportTemplate.java new file mode 100644 index 00000000..23164d8b --- /dev/null +++ b/fastbee-common/src/main/java/com/fastbee/common/enums/ReportTemplate.java @@ -0,0 +1,92 @@ +package com.fastbee.common.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 设备监测报告导出模板版式 + * + *

版式是现场交付物,每种版式对应一套固定的渲染策略(是否只出主指标、是否按分类拆分模板文件), + * 因此用枚举把「模板编码 ↔ 渲染行为」内聚在一起,新增版式只需新增枚举项 + 放置模板文件, + * 渲染层无需再散落字符串比较。

+ * + *

为什么是枚举而不是自由字符串:早期实现把「是否只出主指标」硬绑在 + * {@code templateCode == "DEVICE_SUMMARY"} 上,一旦模板改名判定会静默失效(不报错, + * 辅助量列又冒出来)。枚举把编码与行为绑定,改名只动枚举,判定入口唯一。

+ * + *

模板编码落库语义:{@link #getCode()} 即 {@code sys_user_setting.report_template} + * 的存储值,同时是 {@code templates/} 下模板文件名的决定依据 —— + * {@link #isCategoryScoped()} 为 false 时直接用 code 拼文件名,为 true 时拼分类前缀。

+ * + * @author FastBee + * @date 2026-10-06 + */ +@Getter +@AllArgsConstructor +public enum ReportTemplate { + + /** + * 完整版(V2):按监测分类取 {@code DEVICE_{CATEGORY}_V2},共 10 节 + * + *

默认版式。测点表沿用「主指标完整四列 + 辅助量合并列」原口径, + * 辅助物理量(水温、电压、角度…)保留为「本次值(变化量)」一列用于分析影响量。

+ */ + FULL_VERSION("V2", "完整版", false, true), + + /** + * DCP 平台专用三节版式(项目概况 / 测点数据 / 时程曲线) + * + *

全分类共用一份 {@code templates/DCPReport.docx}(首行标题用 + * {@code {{device.categoryDesc}}} 占位,故不按分类拆分)。测点表只输出 + * {@code iot_things_threshold.is_primary=1} 的主监测指标,非主监测指标整列不出现。

+ * + *

命名对齐 DCP 平台(平台实时数据推送,见 + * {@code com.fastbee.iot.platform.adapter.dcp})要求的交付格式;注意与推送侧的 + * DCP 适配器区分:本枚举属报告导出,彼处属数据推送。

+ */ + DCP_REPORT("DCPReport", "DCP 三节版式", true, false); + + /** + * 模板编码(存库值;非分类拆分时即 templates/ 下模板文件名) + */ + private final String code; + + /** + * 模板版式中文说明(前端下拉展示用) + */ + private final String desc; + + /** + * 测点表是否只输出主监测指标(is_primary=1) + */ + private final boolean primaryOnly; + + /** + * 是否按监测分类拆分模板文件 + * + *

true:模板文件名为 {@code DEVICE_{CATEGORY}_V2}(每分类一份); + * false:全分类共用一份 {@code {code}.docx}。

+ */ + private final boolean categoryScoped; + + /** + * 按编码解析模板版式 + * + *

未知编码回退 {@link #FULL_VERSION}(完整版)而非抛异常:报告生成不能因为 + * 一个脏配置就整体失败,回退到默认版式至少能产出报告,同时由调用方记录告警。

+ * + * @param code 模板编码(可为 null / 空串) + * @return 匹配的模板版式;空值或未识别时返回 FULL_VERSION + */ + public static ReportTemplate fromCode(String code) { + if (code == null || code.trim().isEmpty()) { + return FULL_VERSION; + } + for (ReportTemplate template : values()) { + if (template.code.equalsIgnoreCase(code.trim())) { + return template; + } + } + return FULL_VERSION; + } +} diff --git a/fastbee-open-api/src/main/java/com/fastbee/controller/report/ReportController.java b/fastbee-open-api/src/main/java/com/fastbee/controller/report/ReportController.java index a5e0e64f..48a9e1bc 100644 --- a/fastbee-open-api/src/main/java/com/fastbee/controller/report/ReportController.java +++ b/fastbee-open-api/src/main/java/com/fastbee/controller/report/ReportController.java @@ -6,6 +6,7 @@ import com.fastbee.common.core.domain.AjaxResult; import com.fastbee.common.core.page.TableDataInfo; import com.fastbee.common.core.redis.RedisCache; import com.fastbee.common.enums.MonitorCategory; +import com.fastbee.common.enums.ReportTemplate; import com.fastbee.common.exception.ServiceException; import com.fastbee.common.utils.StringUtils; import com.fastbee.oss.service.OssClient; @@ -160,4 +161,21 @@ public class ReportController extends BaseController { .collect(Collectors.toList()); return AjaxResult.success(list); } + + /** + * 报告导出模板版式列表(枚举驱动,供系统设置页下拉选择 per-user 模板) + */ + @ApiOperation("报告导出模板版式列表") + @GetMapping("/templates") + public AjaxResult templates() { + List> list = Arrays.stream(ReportTemplate.values()) + .map(t -> { + Map m = new HashMap<>(); + m.put("code", t.getCode()); + m.put("desc", t.getDesc()); + return m; + }) + .collect(Collectors.toList()); + return AjaxResult.success(list); + } } diff --git a/fastbee-report/pom.xml b/fastbee-report/pom.xml index 9a94506d..ad2eb926 100644 --- a/fastbee-report/pom.xml +++ b/fastbee-report/pom.xml @@ -38,6 +38,12 @@ fastbee-iot-data + + + com.fastbee + fastbee-system-service + + com.deepoove diff --git a/fastbee-report/src/main/java/com/fastbee/report/config/ReportProperties.java b/fastbee-report/src/main/java/com/fastbee/report/config/ReportProperties.java deleted file mode 100644 index 3d4d7413..00000000 --- a/fastbee-report/src/main/java/com/fastbee/report/config/ReportProperties.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.fastbee.report.config; - -import lombok.Data; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -/** - * 报告导出配置 - * - *

只控制「用哪一套模板」,全局一刀切。

- * - *

模板版式(2026-10-04 起只有两种,不要按字面值硬判断):

- *
    - *
  • V2:完整版,按监测分类取 {@code DEVICE_{CATEGORY}_V2},共 10 节;
  • - *
  • DCPReport:DCP 平台专用的三节版式(项目概况 / 测点数据 / 时程曲线), - * 测点表只输出 {@code iot_things_threshold.is_primary=1} 的主监测指标。 - * 全分类共用一份 {@code templates/DCPReport.docx},首行标题用 - * {@code {{device.categoryDesc}}} 占位,因此不再按分类拆分。
  • - *
- * - *

为什么叫 DCPReport:这套三节版式是 DCP 平台(平台实时数据推送,见 - * {@code com.fastbee.iot.platform.adapter.dcp})要求交付的报告格式,命名与用途对齐, - * 便于日后回溯「为什么报告有两套版式」。注意与推送侧的 DCP 适配器区分:本常量属 - * 报告导出,彼处属数据推送,两者是不同业务线。

- * - *

为什么不按分类/产品白名单:报告模板是现场交付物,格式要求由业主一次性约定, - * 不会一部分产品要三节版、一部分要完整版。全局开关让「改配置即全量生效」, - * 也避免白名单长期无人维护、导致部分设备悄悄导出错版本。

- * - * @author FastBee - * @date 2026-10-04 - */ -@Data -@Component -@ConfigurationProperties(prefix = "report") -public class ReportProperties { - - /** 完整版模板的版本标记:配置值等于它(或为空)时按监测分类取默认模板 */ - public static final String FULL_VERSION_CODE = "V2"; - - /** - * DCP 平台三节版式的模板编码,与 {@code templates/DCPReport.docx} 文件名严格一致 - */ - public static final String DCP_REPORT_TEMPLATE_CODE = "DCPReport"; - - /** - * 报告模板编码 - * - *

取值:空 / {@code V2} → 完整版;{@code DCPReport} → DCP 三节版式。

- */ - private String templateCode; - - /** - * 是否启用 DCP 三节版式(只出项目概况、isPrimary 测点表、时程曲线) - * - *

唯一判定入口。装配器与渲染层都只依赖这个方法,不各自比对模板编码字符串 —— - * 早期版本把「是否只出主指标」硬绑在 {@code templateCode == "DEVICE_SUMMARY"} 上, - * 一旦模板改名,判定会静默失效(不报错,辅助量列又冒出来),是必须避免的耦合。

- */ - public boolean isDcpReportMode() { - return DCP_REPORT_TEMPLATE_CODE.equalsIgnoreCase(trimmed()); - } - - /** - * 实际生效的模板编码 - * - *

用于 {@code ReportModel.templateCode} 落值与日志;非 DCP 版式返回 {@code V2}, - * 由调用方拼分类前缀得到具体文件名。

- */ - public String trimmed() { - return templateCode == null ? "" : templateCode.trim(); - } -} diff --git a/fastbee-report/src/main/java/com/fastbee/report/core/ReportAssembler.java b/fastbee-report/src/main/java/com/fastbee/report/core/ReportAssembler.java index 2dfde331..80881d24 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/core/ReportAssembler.java +++ b/fastbee-report/src/main/java/com/fastbee/report/core/ReportAssembler.java @@ -1,12 +1,14 @@ package com.fastbee.report.core; import com.fastbee.common.enums.ReportPeriod; +import com.fastbee.common.enums.ReportTemplate; import com.fastbee.common.utils.DateUtils; import com.fastbee.common.utils.StringUtils; -import com.fastbee.report.config.ReportProperties; +import com.fastbee.report.exception.ReportException; import com.fastbee.report.model.AnalysisResult; import com.fastbee.report.model.ReportModel; import com.fastbee.report.model.Statistics; +import com.fastbee.system.service.ISysUserSettingService; import org.springframework.stereotype.Component; import javax.annotation.Resource; @@ -23,7 +25,7 @@ import java.util.UUID; public class ReportAssembler { @Resource - private ReportProperties reportProperties; + private ISysUserSettingService userSettingService; public ReportModel assemble(ReportContext context, AnalysisResult analysis) { return assemble(context, analysis, 1); @@ -33,8 +35,8 @@ public class ReportAssembler { * @param pointCount 参与分析的测点数(>1 时为多测点报告,设备信息展示测点规模) */ public ReportModel assemble(ReportContext context, AnalysisResult analysis, int pointCount) { - // 模板选择:DCP 版式为全局配置(report.template-code),不拼分类后缀,全分类共用一份模板 - String templateCode = resolveTemplateCode(context); + // 模板选择:per-user(sys_user_setting.report_template),必须能拿到 userId + ReportTemplate template = resolveTemplate(context); ReportPeriod period = context.getTimeWindow().getPeriod(); String serialNumber = context.getDevice().getSerialNumber(); @@ -68,7 +70,7 @@ public class ReportAssembler { return ReportModel.builder() .reportNo(context.getReportNo()) - .templateCode(templateCode) + .templateCode(resolveTemplateFileName(template, context)) .fileName(fileName) .periodDesc(context.getTimeWindow().getDescription()) .period(period) @@ -82,32 +84,45 @@ public class ReportAssembler { .metricName(primary == null ? null : primary.getModelName()) .metricNames(metricNames) .metricUnit(primary == null ? null : primary.getUnit()) - // DCP 版式:测点表只出 is_primary=1 的主监测指标(未勾选时由渲染层输出提示行) - .primaryOnly(ReportProperties.DCP_REPORT_TEMPLATE_CODE.equals(templateCode)) + // 精简版(DCP 三节版式):测点表只出 is_primary=1 的主监测指标 + .primaryOnly(template.isPrimaryOnly()) .analysis(analysis) .build(); } /** - * 解析报告模板编码 + * 解析报告模板版式(per-user) * - *

取值优先级:

- *
    - *
  1. 配置 {@code report.template-code=DCPReport} → DCP 平台三节版式, - * 全分类共用一份 {@code templates/DCPReport.docx},不拼分类后缀;
  2. - *
  3. 配空或配成 {@code V2} → 按监测分类取完整版 {@code DEVICE_{CATEGORY}_V2}。
  4. - *
+ *

报告导出由登录用户触发(异步线程已显式透传 SecurityContext,userId 落在 + * {@code ReportContext.userId}),因此模板版式是 per-user 偏好,从 + * {@code sys_user_setting.report_template} 读取。无登录态(userId 为 null)时直接失败: + * 报告导出本就不允许无登录态触发,不做全局兜底,避免后台任务用错版式。

* *

为什么不读 {@code request.templateCode}:那是历史字段,从未被真正使用过 * (此前本行是硬编码),前端也从未传值。放开它等于给外部一个可任意指定 classpath * 模板文件名的入口,而报告模板与渲染策略强耦合(动态表列、图表位、poi-tl 策略绑定), - * 传错编码会直接渲染失败或产出错版式。格式是现场交付物,由运维改配置决定即可。

+ * 传错编码会直接渲染失败或产出错版式。版式由用户在系统设置页选定即可。

*/ - private String resolveTemplateCode(ReportContext context) { - if (reportProperties != null && reportProperties.isDcpReportMode()) { - return ReportProperties.DCP_REPORT_TEMPLATE_CODE; + private ReportTemplate resolveTemplate(ReportContext context) { + Long userId = context == null ? null : context.getUserId(); + if (userId == null) { + throw new ReportException("无法确定报告导出人,请登录后重试"); } - return "DEVICE_" + context.getCategory().name() + "_" + ReportProperties.FULL_VERSION_CODE; + String code = userSettingService.selectByUserId(userId).getReportTemplate(); + return ReportTemplate.fromCode(code); + } + + /** + * 由版式与分类拼出模板文件名(解析器按它从 classpath 定位 {@code templates/{name}.docx}) + * + *

按分类拆分的版式(完整版 V2)拼 {@code DEVICE_{CATEGORY}_V2}; + * 全分类共用一份的版式(DCP 三节)直接用 {@code code} 拼 {@code DCPReport}。

+ */ + private String resolveTemplateFileName(ReportTemplate template, ReportContext context) { + if (template.isCategoryScoped()) { + return "DEVICE_" + context.getCategory().name() + "_" + template.getCode(); + } + return template.getCode(); } /** diff --git a/fastbee-report/src/main/java/com/fastbee/report/core/ReportContext.java b/fastbee-report/src/main/java/com/fastbee/report/core/ReportContext.java index d45a0dbe..e9c959ec 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/core/ReportContext.java +++ b/fastbee-report/src/main/java/com/fastbee/report/core/ReportContext.java @@ -70,6 +70,9 @@ public class ReportContext { /** 租户ID(从主线程传递) */ private Long tenantId; + /** 创建人用户ID(从主线程传递,报告模板版式 per-user 判定依赖它) */ + private Long userId; + /** * 该物模型是否被用户显式指定为主监测指标 */ diff --git a/fastbee-report/src/main/java/com/fastbee/report/core/ReportContextBuilder.java b/fastbee-report/src/main/java/com/fastbee/report/core/ReportContextBuilder.java index 64f346dc..540c3853 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/core/ReportContextBuilder.java +++ b/fastbee-report/src/main/java/com/fastbee/report/core/ReportContextBuilder.java @@ -148,10 +148,12 @@ public class ReportContextBuilder { // 从主线程获取用户信息,传递到异步线程 String username = null; Long tenantId = null; + Long userId = null; try { LoginUser loginUser = SecurityUtils.getLoginUser(); if (loginUser != null) { username = loginUser.getUsername(); + userId = loginUser.getUserId(); if (loginUser.getUser() != null && loginUser.getUser().getDept() != null) { tenantId = loginUser.getUser().getDept().getDeptUserId(); } @@ -161,6 +163,7 @@ public class ReportContextBuilder { } context.setCreateBy(username); context.setTenantId(tenantId); + context.setUserId(userId); return context; } diff --git a/fastbee-report/src/main/java/com/fastbee/report/model/AnalysisResult.java b/fastbee-report/src/main/java/com/fastbee/report/model/AnalysisResult.java index 8d635097..4e48a8ab 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/model/AnalysisResult.java +++ b/fastbee-report/src/main/java/com/fastbee/report/model/AnalysisResult.java @@ -48,7 +48,7 @@ public class AnalysisResult { * {@code is_primary=1} 的物模型,没配就是空,不做任何推断。 * * - *

精简版报告({@code report.template-code=DCPReport})的测点表只输出本集合中的物模型, + *

精简版报告({@code ReportTemplate.DCP_REPORT},三节版式)的测点表只输出本集合中的物模型, * 因此必须与用户配置严格一致 —— 用关键词推断的结果冒充实配会输出用户没勾的指标。

* *

多测点归并时取代表测点(最不利测点)的集合,与 {@link #primaryIdentifiers} 同源同口径。

diff --git a/fastbee-report/src/main/java/com/fastbee/report/renderer/WordRenderer.java b/fastbee-report/src/main/java/com/fastbee/report/renderer/WordRenderer.java index e11ebcdf..a90f9df9 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/renderer/WordRenderer.java +++ b/fastbee-report/src/main/java/com/fastbee/report/renderer/WordRenderer.java @@ -300,6 +300,12 @@ public class WordRenderer implements ReportRenderer { analysisMap.put("quality", qualityMap); analysisMap.put("warning", warningMap); + // 报告最上方「是否达到预警值」结论(DCP 三节版式头部用 {{warningStatus}}): + // 与正文预警章节同一份 warning 数据,但措辞直接回答「有无达到」,不落状态词 + data.put("warningStatus", warning == null + ? "未配置预警阈值,无法判定是否达到预警值" + : warningReachedText(warning.getStatus())); + // 添加项目概况总结(始终 put,空值兜底) data.put("summary", StringUtils.isNotEmpty(analysis.getSummary()) ? analysis.getSummary() : "统计期内无有效监测数据。"); @@ -797,6 +803,29 @@ public class WordRenderer implements ReportRenderer { return "正常"; } + /** + * 预警状态转「是否达到预警值」结论(报告头部 {{warningStatus}}) + * + *

与 {@link #warningLevelText} 的区别:后者输出状态词(正常/黄色预警…), + * 前者直接回答「有无达到预警值」,并点明达到的是哪一档(预警值/报警值/控制值), + * 措辞与 {@code WarningAnalyzer} 的 description 口径一致(GB 50497-2019 双控)。

+ */ + private String warningReachedText(String status) { + if (WarningAnalysis.STATUS_CONTROL.equals(status)) { + return "已达到红色预警(控制值)"; + } + if (WarningAnalysis.STATUS_ALARM.equals(status)) { + return "已达到橙色预警(报警值)"; + } + if (WarningAnalysis.STATUS_WARNING.equals(status)) { + return "已达到黄色预警(预警值)"; + } + if (WarningAnalysis.STATUS_UNKNOWN.equals(status)) { + return "未配置预警阈值,无法判定是否达到预警值"; + } + return "未达到预警值"; + } + /** 安全等级转中文 */ private String safetyLevelText(String level) { if ("DANGER".equals(level)) { diff --git a/fastbee-report/src/main/resources/templates/DCPReport.docx b/fastbee-report/src/main/resources/templates/DCPReport.docx index d2f13ad2d3075efef6f4f52dad53a05805e9045b..dba5c0a33b2ae527b9bff1fec7394a858ae46c2e 100644 GIT binary patch literal 26238 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 literal 37200 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 Wz?&6Vlz=j#AVVUsOt{$t;sF51a=o?y diff --git a/fastbee-report/src/test/java/com/fastbee/report/renderer/WordRendererDcpReportTemplateTest.java b/fastbee-report/src/test/java/com/fastbee/report/renderer/WordRendererDcpReportTemplateTest.java index 316c532a..735b1404 100644 --- a/fastbee-report/src/test/java/com/fastbee/report/renderer/WordRendererDcpReportTemplateTest.java +++ b/fastbee-report/src/test/java/com/fastbee/report/renderer/WordRendererDcpReportTemplateTest.java @@ -2,8 +2,8 @@ package com.fastbee.report.renderer; import com.fastbee.common.enums.MonitorCategory; import com.fastbee.common.enums.ReportPeriod; +import com.fastbee.common.enums.ReportTemplate; import com.fastbee.report.chart.ChartGenerator; -import com.fastbee.report.config.ReportProperties; import com.fastbee.report.core.ReportAssembler; import com.fastbee.report.data.MetricData; import com.fastbee.report.model.AnalysisResult; @@ -13,6 +13,8 @@ import com.fastbee.report.model.ReportModel; import com.fastbee.report.model.Statistics; import com.fastbee.report.model.TrendAnalysis; import com.fastbee.report.model.WarningAnalysis; +import com.fastbee.system.domain.SysUserSetting; +import com.fastbee.system.service.ISysUserSettingService; import org.junit.jupiter.api.Test; import org.springframework.test.util.ReflectionTestUtils; @@ -60,7 +62,7 @@ public class WordRendererDcpReportTemplateTest { } @Test - public void testRenderDcpReportTemplate() { + public void testRenderDcpReportTemplate() throws Exception { ReportModel model = buildModel(Arrays.asList("crackWidth")); byte[] docx = newRenderer().render(model); assertNotNull(docx); @@ -68,6 +70,11 @@ public class WordRendererDcpReportTemplateTest { String magic = new String(new byte[]{docx[0], docx[1]}, StandardCharsets.ISO_8859_1); assertTrue("PK".equals(magic), "渲染结果应为有效 docx(zip)"); + // 头部「是否达到预警值」结论:主指标为正常状态 → 应输出「未达到预警值」 + String xml = readDocumentXml(docx); + assertTrue(xml.contains("预警状态"), "DCP 头部应包含「预警状态」标签"); + assertTrue(xml.contains("未达到预警值"), "正常状态下应输出「未达到预警值」"); + try { java.io.File out = new java.io.File("target/test-output-dcp-report.docx"); java.nio.file.Files.write(out.toPath(), docx); @@ -80,11 +87,11 @@ public class WordRendererDcpReportTemplateTest { /** DCP 模板只需一份,服务全部监测分类(首行标题用 {{device.categoryDesc}} 占位) */ @Test public void testDcpReportTemplateExists() { - // 常量必须与 templates/ 下的文件名严格一致:解析器按 templateCode 拼文件名找资源, - // 常量与文件任一侧改名都会在运行时抛「报告模板不存在」,这里提前挡住 + // 枚举编码必须与 templates/ 下的文件名严格一致:解析器按 templateCode 拼文件名找资源, + // 编码与文件任一侧改名都会在运行时抛「报告模板不存在」,这里提前挡住 assertEquals("DCPReport.docx", - ReportProperties.DCP_REPORT_TEMPLATE_CODE + ".docx", - "常量与模板文件名不一致,改名时需同步"); + ReportTemplate.DCP_REPORT.getCode() + ".docx", + "枚举编码与模板文件名不一致,改名时需同步"); try (InputStream in = getClass().getClassLoader() .getResourceAsStream("templates/DCPReport.docx")) { assertNotNull(in, "DCP 版式模板缺失: DCPReport.docx"); @@ -93,13 +100,15 @@ public class WordRendererDcpReportTemplateTest { } } - /** 配置切到 DCPReport 时,装配出的模板编码与 primaryOnly 标记都正确 */ + /** 用户设置选 DCPReport 时,装配出的模板编码与 primaryOnly 标记都正确 */ @Test public void testAssemblerResolvesDcpReportTemplate() { ReportAssembler assembler = new ReportAssembler(); - ReflectionTestUtils.setField(assembler, "reportProperties", dcpReportProperties()); + ReflectionTestUtils.setField(assembler, "userSettingService", + mockUserSettingService(ReportTemplate.DCP_REPORT.getCode())); com.fastbee.report.core.ReportContext context = new com.fastbee.report.core.ReportContext(); + context.setUserId(1L); context.setCategory(MonitorCategory.CRACK_METER); com.fastbee.iot.domain.Device device = new com.fastbee.iot.domain.Device(); device.setDeviceId(1L); @@ -116,19 +125,19 @@ public class WordRendererDcpReportTemplateTest { context.setReportNo("REPORT-TEST-0001"); ReportModel model = assembler.assemble(context, buildAnalysis(Arrays.asList("crackWidth")), 1); - assertEquals(ReportProperties.DCP_REPORT_TEMPLATE_CODE, model.getTemplateCode()); + assertEquals(ReportTemplate.DCP_REPORT.getCode(), model.getTemplateCode()); assertTrue(model.isPrimaryOnly(), "DCP 版式必须置 primaryOnly,测点表才只出主监测指标"); } - /** 配置切回 V2 时,仍按监测分类取完整版模板,且 primaryOnly=false(原行为不变) */ + /** 用户设置默认 V2 时,仍按监测分类取完整版模板,且 primaryOnly=false(原行为不变) */ @Test public void testAssemblerFallsBackToFullTemplate() { ReportAssembler assembler = new ReportAssembler(); - ReportProperties properties = new ReportProperties(); - properties.setTemplateCode(ReportProperties.FULL_VERSION_CODE); - ReflectionTestUtils.setField(assembler, "reportProperties", properties); + ReflectionTestUtils.setField(assembler, "userSettingService", + mockUserSettingService(ReportTemplate.FULL_VERSION.getCode())); com.fastbee.report.core.ReportContext context = new com.fastbee.report.core.ReportContext(); + context.setUserId(1L); context.setCategory(MonitorCategory.CRACK_METER); com.fastbee.iot.domain.Device device = new com.fastbee.iot.domain.Device(); device.setDeviceId(1L); @@ -152,7 +161,7 @@ public class WordRendererDcpReportTemplateTest { @Test public void testDcpReportWithoutExplicitPrimaryShowsHint() { ReportModel model = buildModel(new ArrayList<>()); - model.setTemplateCode(ReportProperties.DCP_REPORT_TEMPLATE_CODE); + model.setTemplateCode(ReportTemplate.DCP_REPORT.getCode()); byte[] docx = newRenderer().render(model); assertTrue(docx.length > 0, "未配置主监测指标时也应产出可读文档"); } @@ -207,10 +216,41 @@ public class WordRendererDcpReportTemplateTest { throw new IllegalStateException("docx 中未找到 word/document.xml"); } - private ReportProperties dcpReportProperties() { - ReportProperties properties = new ReportProperties(); - properties.setTemplateCode(ReportProperties.DCP_REPORT_TEMPLATE_CODE); - return properties; + /** + * 构造 mock 的 {@link ISysUserSettingService}:按入参模板编码返回对应设置对象 + */ + private ISysUserSettingService mockUserSettingService(String reportTemplateCode) { + return new ISysUserSettingService() { + @Override + public SysUserSetting selectByUserId(Long userId) { + SysUserSetting setting = SysUserSetting.defaultOf(userId); + setting.setReportTemplate(reportTemplateCode); + return setting; + } + + @Override + public int updatePartial(Long userId, SysUserSetting patch, String operator) { + return 1; + } + + @Override + public List listLoginRedirectOptions() { + return new ArrayList<>(); + } + + @Override + public void addPinnedTag(Long userId, String path, String operator) { + } + + @Override + public void removePinnedTag(Long userId, String path, String operator) { + } + + @Override + public List getPinnedTags(Long userId) { + return new ArrayList<>(); + } + }; } /** @@ -221,7 +261,7 @@ public class WordRendererDcpReportTemplateTest { AnalysisResult analysis = buildAnalysis(explicitPrimary); return ReportModel.builder() .reportNo("REPORT-20261004120000-0001") - .templateCode(ReportProperties.DCP_REPORT_TEMPLATE_CODE) + .templateCode(ReportTemplate.DCP_REPORT.getCode()) .fileName("裂缝计-监测分析报告-abc123.docx") .periodDesc("2026-10-01 00:00:00 ~ 2026-10-04 00:00:00") .period(ReportPeriod.CUSTOM) diff --git a/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/domain/SysUserSetting.java b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/domain/SysUserSetting.java index 0bad61af..807c64b0 100644 --- a/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/domain/SysUserSetting.java +++ b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/domain/SysUserSetting.java @@ -71,6 +71,16 @@ public class SysUserSetting extends BaseEntity { @ApiModelProperty(value = "设备管理列表是否包含下级机构数据") private Boolean showChild; + /** + * 报告导出模板版式编码 + * + *

取值即 {@link com.fastbee.common.enums.ReportTemplate#getCode()}: + * {@code V2} 完整版 / {@code DCPReport} DCP 三节版式,可扩展其它版式。 + * 报告导出由登录用户触发,模板版式是 per-user 偏好,故落本表而非全局 {@code sys_config}。

+ */ + @ApiModelProperty(value = "报告导出模板版式编码(V2完整版/DCPReport三节版式)") + private String reportTemplate; + public SysUserSetting() { } @@ -87,6 +97,7 @@ public class SysUserSetting extends BaseEntity { setting.setPinnedTags(UserSettingConstants.EMPTY_PINNED_TAGS); setting.setQueryGatewayChild(UserSettingConstants.DEFAULT_QUERY_GATEWAY_CHILD); setting.setShowChild(UserSettingConstants.DEFAULT_SHOW_CHILD); + setting.setReportTemplate(UserSettingConstants.DEFAULT_REPORT_TEMPLATE); return setting; } @@ -138,6 +149,14 @@ public class SysUserSetting extends BaseEntity { this.showChild = showChild; } + public String getReportTemplate() { + return reportTemplate; + } + + public void setReportTemplate(String reportTemplate) { + this.reportTemplate = reportTemplate; + } + @Override public String toString() { return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE) @@ -147,6 +166,7 @@ public class SysUserSetting extends BaseEntity { .append("pinnedTags", getPinnedTags()) .append("queryGatewayChild", getQueryGatewayChild()) .append("showChild", getShowChild()) + .append("reportTemplate", getReportTemplate()) .append("createBy", getCreateBy()) .append("createTime", getCreateTime()) .append("updateBy", getUpdateBy()) diff --git a/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/mapper/SysUserSettingMapper.java b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/mapper/SysUserSettingMapper.java index 6af3d64c..eb7bd818 100644 --- a/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/mapper/SysUserSettingMapper.java +++ b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/mapper/SysUserSettingMapper.java @@ -51,20 +51,23 @@ public interface SysUserSettingMapper { * *

固定标签的增量写入要在一行上做「锁定读 → 改 → 写」,行必须先存在。 * 刻意不用 ON DUPLICATE 写其它列:那会覆盖 - * login_redirect / query_gateway_child / show_child,把用户已保存的设置冲回默认值。 + * login_redirect / query_gateway_child / show_child / report_template, + * 把用户已保存的设置冲回默认值。 * - * @param userId 用户ID - * @param loginRedirect 新行使用的默认登录跳转 + * @param userId 用户ID + * @param loginRedirect 新行使用的默认登录跳转 * @param queryGatewayChild 新行使用的默认开关值 - * @param showChild 新行使用的默认「包含下级机构数据」值 - * @param operator 操作人用户名 - * @param now 当前时间 + * @param showChild 新行使用的默认「包含下级机构数据」值 + * @param reportTemplate 新行使用的默认报告模板编码 + * @param operator 操作人用户名 + * @param now 当前时间 * @return 影响行数(插入 1 / 已存在 0) */ int insertDefaultIfAbsent(@Param("userId") Long userId, @Param("loginRedirect") String loginRedirect, @Param("queryGatewayChild") Boolean queryGatewayChild, @Param("showChild") Boolean showChild, + @Param("reportTemplate") String reportTemplate, @Param("operator") String operator, @Param("now") Date now); diff --git a/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/impl/SysUserSettingServiceImpl.java b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/impl/SysUserSettingServiceImpl.java index c11f370a..0ce88aa8 100644 --- a/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/impl/SysUserSettingServiceImpl.java +++ b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/impl/SysUserSettingServiceImpl.java @@ -37,8 +37,9 @@ public class SysUserSettingServiceImpl implements ISysUserSettingService { * *

用 \n 而不是常见的逗号/竖线:loginRedirect 是路由 path 不含换行, * pinnedTags 是 JSON 数组(元素为带引号的字符串)也不含裸换行, - * showChild 与 queryGatewayChild 是布尔字面量,四段互不冲突。 - *

字段顺序固定为:loginRedirect | pinnedTags | showChild | queryGatewayChild, + * showChild 与 queryGatewayChild 是布尔字面量,reportTemplate 是模板编码, + * 五段互不冲突。 + *

字段顺序固定为:loginRedirect | pinnedTags | showChild | queryGatewayChild | reportTemplate, * 与 {@link #parseCached} 一一对应,增删字段必须同步改这两处。 */ private static final String CACHE_SEPARATOR = "\n"; @@ -83,15 +84,17 @@ public class SysUserSettingServiceImpl implements ISysUserSettingService { setting.getLoginRedirect() + CACHE_SEPARATOR + setting.getPinnedTags() + CACHE_SEPARATOR + setting.getShowChild() + CACHE_SEPARATOR - + setting.getQueryGatewayChild()); + + setting.getQueryGatewayChild() + CACHE_SEPARATOR + + setting.getReportTemplate()); } /** * 解析缓存值,格式非法时返回 null 走查库 * - *

段数必须严格等于 4:历史版本只缓存了 2 段(loginRedirect + pinnedTags)、 - * 后又扩到 3 段(追加 queryGatewayChild)。若按缺省值补齐,会把旧格式里 - * 那个位置的字段当成 showChild 读出来,故段数不等一律判为未命中,由查库回填新格式。 + *

段数必须严格等于 5:历史版本只缓存了 2 段(loginRedirect + pinnedTags)、 + * 后又扩到 3 段(追加 queryGatewayChild)、4 段(追加 showChild)。 + * 若按缺省值补齐,会把旧格式里那个位置的字段当成别的字段读出来, + * 故段数不等一律判为未命中,由查库回填新格式。 * 缓存无 TTL,但每个用户最多回填一次。 * * @param cached 缓存字符串 @@ -100,7 +103,7 @@ public class SysUserSettingServiceImpl implements ISysUserSettingService { */ private SysUserSetting parseCached(String cached, Long userId) { String[] parts = cached.split(CACHE_SEPARATOR, -1); - if (parts.length != 4) { + if (parts.length != 5) { return null; } SysUserSetting setting = SysUserSetting.defaultOf(userId); @@ -108,6 +111,7 @@ public class SysUserSettingServiceImpl implements ISysUserSettingService { setting.setPinnedTags(parts[1]); setting.setShowChild(Boolean.parseBoolean(parts[2])); setting.setQueryGatewayChild(Boolean.parseBoolean(parts[3])); + setting.setReportTemplate(parts[4]); return setting; } @@ -119,6 +123,7 @@ public class SysUserSettingServiceImpl implements ISysUserSettingService { // 行必须先存在,否则后面的部分更新会 UPDATE 0 行、静默丢掉本次改动 userSettingMapper.insertDefaultIfAbsent(userId, UserSettingConstants.DEFAULT_LOGIN_REDIRECT, UserSettingConstants.DEFAULT_QUERY_GATEWAY_CHILD, UserSettingConstants.DEFAULT_SHOW_CHILD, + UserSettingConstants.DEFAULT_REPORT_TEMPLATE, operator, now); patch.setUserId(userId); patch.setUpdateBy(operator); @@ -141,6 +146,7 @@ public class SysUserSettingServiceImpl implements ISysUserSettingService { // 行必须先存在,后面的锁定读才有东西可锁 userSettingMapper.insertDefaultIfAbsent(userId, UserSettingConstants.DEFAULT_LOGIN_REDIRECT, UserSettingConstants.DEFAULT_QUERY_GATEWAY_CHILD, UserSettingConstants.DEFAULT_SHOW_CHILD, + UserSettingConstants.DEFAULT_REPORT_TEMPLATE, operator, now); List paths = parsePinnedTags(userSettingMapper.selectPinnedTagsForUpdate(userId)); if (paths.contains(path)) { diff --git a/fastbee-service/fastbee-system-service/src/main/resources/mapper/system/SysUserSettingMapper.xml b/fastbee-service/fastbee-system-service/src/main/resources/mapper/system/SysUserSettingMapper.xml index 24d04ec4..5a4ff477 100644 --- a/fastbee-service/fastbee-system-service/src/main/resources/mapper/system/SysUserSettingMapper.xml +++ b/fastbee-service/fastbee-system-service/src/main/resources/mapper/system/SysUserSettingMapper.xml @@ -11,6 +11,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + @@ -25,6 +26,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" select setting_id, user_id, login_redirect, pinned_tags, query_gateway_child, show_child, + report_template, create_by, create_time, update_by, update_time, remark from sys_user_setting @@ -49,6 +51,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" login_redirect = #{loginRedirect}, query_gateway_child = #{queryGatewayChild}, show_child = #{showChild}, + report_template = #{reportTemplate}, update_by = #{updateBy}, update_time = #{updateTime} @@ -64,9 +67,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" --> insert into sys_user_setting - (user_id, login_redirect, pinned_tags, query_gateway_child, show_child, create_by, create_time, update_by, update_time) + (user_id, login_redirect, pinned_tags, query_gateway_child, show_child, report_template, create_by, create_time, update_by, update_time) values - (#{userId}, #{loginRedirect}, '[]', #{queryGatewayChild}, #{showChild}, #{operator}, #{now}, #{operator}, #{now}) + (#{userId}, #{loginRedirect}, '[]', #{queryGatewayChild}, #{showChild}, #{reportTemplate}, #{operator}, #{now}, #{operator}, #{now}) on duplicate key update user_id = user_id diff --git a/sql/sys_user_setting.sql b/sql/sys_user_setting.sql index 4918599f..3af1cbb1 100644 --- a/sql/sys_user_setting.sql +++ b/sql/sys_user_setting.sql @@ -40,6 +40,10 @@ -- ADD COLUMN `show_child` tinyint(1) NOT NULL DEFAULT 0 -- COMMENT '设备管理列表是否包含下级机构数据(0否1是)' -- AFTER `query_gateway_child`; +-- ALTER TABLE `sys_user_setting` +-- ADD COLUMN `report_template` varchar(32) NOT NULL DEFAULT 'V2' +-- COMMENT '报告导出模板编码(V2完整版/DCPReport三节版式)' +-- AFTER `show_child`; -- -- query_gateway_child:开启后 /iot/device/shortList **按设备编号查询时**(serialNumber 非空): -- ① 命中的若是网关子设备编号,返回其所属的网关设备(子设备 gw_dev_code 存的就是网关 serial_number, @@ -60,6 +64,13 @@ -- 设备管理页进入时读一次该设置并作为查询参数下发,故本列是「前端读取、后端不感知」的偏好, -- DeviceController 在未收到 showChild 时依然按 false 处理。 -- 默认 0 = 与引入本开关前完全一致。 +-- +-- report_template:报告导出模板版式编码,取值即 ReportTemplate.code(V2 完整版 / +-- DCPReport 三节版式,可扩展其它版式)。 +-- 版式是 per-user 偏好:报告导出由登录用户触发(异步线程已显式透传 SecurityContext, +-- 能拿到 userId),故落本表而非全局 sys_config;无登录态不允许导出,不做全局兜底。 +-- 由「系统设置」页维护(下拉选择),新增版式 = 枚举新增项 + 放 templates/ 下模板文件。 +-- 默认 'V2' = 与引入本开关前完全一致(完整版,按监测分类取 DEVICE_{CATEGORY}_V2)。 -- ------------------------------------------------------------ CREATE TABLE IF NOT EXISTS `sys_user_setting` ( `setting_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '设置主键', @@ -68,6 +79,7 @@ CREATE TABLE IF NOT EXISTS `sys_user_setting` ( `pinned_tags` text NOT NULL DEFAULT ('[]') COMMENT '任务栏固定标签JSON数组,按序存 path;空数组表示未固定', `query_gateway_child` tinyint(1) NOT NULL DEFAULT 0 COMMENT '按编号查询时是否查网关子设备(0否1是)', `show_child` tinyint(1) NOT NULL DEFAULT 0 COMMENT '设备管理列表是否包含下级机构数据(0否1是)', + `report_template` varchar(32) NOT NULL DEFAULT 'V2' COMMENT '报告导出模板编码(V2完整版/DCPReport三节版式)', `create_by` varchar(64) DEFAULT NULL COMMENT '创建者', `create_time` datetime DEFAULT NULL COMMENT '创建时间', `update_by` varchar(64) DEFAULT NULL COMMENT '更新者', @@ -87,7 +99,7 @@ CREATE TABLE IF NOT EXISTS `sys_user_setting` ( -- ------------------------------------------------------------ INSERT INTO `sys_config` (`config_name`, `config_key`, `config_value`, `config_type`, `create_by`, `create_time`, `remark`) -SELECT '是否下发读取设备IMEI/ICCID指令', 'iot.power_on.read_imei_iccid', 'true', 'Y', 'system', NOW(), +SELECT '是否自动下发读取设备IMEI/ICCID指令', 'iot.power_on.read_imei_iccid', 'true', 'Y', 'system', NOW(), '设备上电后是否下发 AT+IMEI/AT+ICCID/AT+LBS 指令读取;读该值的线程无登录上下文,故为全局配置' WHERE NOT EXISTS ( SELECT 1 FROM `sys_config` WHERE `config_key` = 'iot.power_on.read_imei_iccid' diff --git a/yack-web/src/api/iot/report.js b/yack-web/src/api/iot/report.js index 28aaf6e1..d09790b0 100644 --- a/yack-web/src/api/iot/report.js +++ b/yack-web/src/api/iot/report.js @@ -43,3 +43,11 @@ export function listMonitorCategories() { method: 'get', }); } + +// 报告导出模板版式列表(枚举驱动,供系统设置页下拉选择 per-user 模板) +export function listReportTemplates() { + return request({ + url: '/iot/report/templates', + method: 'get', + }); +} diff --git a/yack-web/src/views/system/setting/components/UserSettingForm.vue b/yack-web/src/views/system/setting/components/UserSettingForm.vue index e4125ebf..e7377167 100644 --- a/yack-web/src/views/system/setting/components/UserSettingForm.vue +++ b/yack-web/src/views/system/setting/components/UserSettingForm.vue @@ -25,6 +25,22 @@ + + + + + +

保存 仅提交改动过的配置项 @@ -38,14 +54,17 @@ import { updateUserSetting, listLoginRedirectOptions } from '@/api/system/userSetting'; +import { listReportTemplates } from '@/api/iot/report'; import SettingRow from './SettingRow'; // 默认值与后端 UserSettingConstants 保持一致: -// 未指定的账号保持原有的数据大屏落地行为;子设备开关与「查下级机构数据」默认关闭 +// 未指定的账号保持原有的数据大屏落地行为;子设备开关与「查下级机构数据」默认关闭; +// 报告模板默认完整版(V2) const DEFAULT_FORM = { loginRedirect: '/bigscreen', queryGatewayChild: false, - showChild: false + showChild: false, + reportTemplate: 'V2' }; export default { @@ -55,7 +74,9 @@ export default { return { saving: false, optionsLoading: false, + templatesLoading: false, options: [], + reportTemplates: [], form: { ...DEFAULT_FORM }, // 服务端当前值的快照:保存时只提交与它不同的字段,未改动的不进 payload original: { ...DEFAULT_FORM } @@ -63,6 +84,7 @@ export default { }, created() { this.getOptions(); + this.getTemplates(); this.getSetting(); }, methods: { @@ -76,6 +98,16 @@ export default { this.optionsLoading = false; }); }, + getTemplates() { + this.templatesLoading = true; + listReportTemplates() + .then((res) => { + this.reportTemplates = res.data || []; + }) + .finally(() => { + this.templatesLoading = false; + }); + }, getSetting() { getUserSetting().then((res) => { const data = res.data || {}; @@ -88,7 +120,8 @@ export default { : data.queryGatewayChild, showChild: data.showChild === null || data.showChild === undefined ? DEFAULT_FORM.showChild - : data.showChild + : data.showChild, + reportTemplate: data.reportTemplate || DEFAULT_FORM.reportTemplate }; this.form = { ...form }; // 快照必须与回显值同源(含兜底后的值),否则未改动也会被当成脏数据提交