最近新增的工作

This commit is contained in:
2026-04-23 11:06:04 +08:00
parent c9461f8b8a
commit 03df9d3a10
140 changed files with 5591 additions and 2767 deletions

View File

@@ -14,10 +14,15 @@ CN_Tool 是一个基于 Spring Boot 的多模块后端聚合工程,当前仓
- `entrance`
- `system`
- `systemmonitor`
- `user`
- `detection`
- `tools`
其中 `systemmonitor` 当前包含:
- `disk-monitor`
其中 `tools` 当前包含:
- `activate-tool`
@@ -30,7 +35,7 @@ CN_Tool 是一个基于 Spring Boot 的多模块后端聚合工程,当前仓
- `entrance/src/main/java/com/njcn/gather/EntranceApplication.java`
`entrance` 模块聚合了 `system``user``detection``activate-tool``wave-tool``mms-mapping`,是当前运行时主入口。
`entrance` 模块聚合了 `system``disk-monitor``user``detection``activate-tool``wave-tool``mms-mapping`,是当前运行时主入口。
## 技术基线
@@ -71,6 +76,8 @@ P0 已补齐基线文档,建议按以下顺序阅读:
- 负责认证、用户、角色、菜单资源相关能力
- `system`
- 负责字典、日志、系统配置、注册资源相关能力
- `systemmonitor/disk-monitor`
- 负责磁盘监控相关能力的独立扩展实现
- `detection`
- 当前以通信基础设施为主,包含 WebSocket / Netty 相关组件
- `tools/activate-tool`

View File

@@ -16,6 +16,11 @@
<artifactId>system</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>disk-monitor</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>detection</artifactId>

View File

@@ -12,6 +12,7 @@
<modules>
<module>entrance</module>
<module>system</module>
<module>systemmonitor</module>
<module>user</module>
<module>detection</module>
<module>tools</module>

29
systemmonitor/README.md Normal file
View File

@@ -0,0 +1,29 @@
# System Monitor 模块说明
## 当前状态
`systemmonitor` 当前作为系统监控能力聚合模块使用。
当前真实保留的子模块有:
- `disk-monitor`
## 当前结构
```text
systemmonitor/
└── disk-monitor/
```
## disk-monitor 的职责
`disk-monitor` 预留用于承载磁盘监控相关能力,后续可在该模块内继续补充:
- 磁盘监控配置管理
- 磁盘巡检任务
- 磁盘容量计算与阈值判定
- 磁盘预警与告警留痕
## 模块定位
当前将磁盘监控拆分到 `systemmonitor/disk-monitor`,目的是避免将系统监控扩展逻辑直接混入 `system` 公共基础模块,便于后续继续扩展 CPU、内存或进程级监控能力。

View File

@@ -0,0 +1,11 @@
# disk-monitor
`disk-monitor` 模块用于承载磁盘监控相关代码。
当前这一版只完成模块骨架接入,后续可以在该模块内继续补充:
- Controller
- Service
- 定时巡检任务
- 磁盘容量采集与阈值判断
- 日志留痕与 SQL 脚本

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.njcn.gather</groupId>
<artifactId>systemmonitor</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>disk-monitor</artifactId>
<packaging>jar</packaging>
<name>disk-monitor</name>
<dependencies>
<dependency>
<groupId>com.njcn</groupId>
<artifactId>njcn-common</artifactId>
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>com.njcn</groupId>
<artifactId>mybatis-plus</artifactId>
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>com.njcn</groupId>
<artifactId>spingboot2.3.12</artifactId>
<version>2.3.12</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,250 @@
package com.njcn.gather.systemmonitor.disk.component;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.njcn.gather.systemmonitor.disk.constant.DiskMonitorConstant;
import com.njcn.gather.systemmonitor.disk.mapper.DiskMonitorNotifyLogMapper;
import com.njcn.gather.systemmonitor.disk.pojo.dto.DiskMonitorNotifyHttpItem;
import com.njcn.gather.systemmonitor.disk.pojo.dto.DiskMonitorNotifyPathItem;
import com.njcn.gather.systemmonitor.disk.pojo.po.DiskMonitorJob;
import com.njcn.gather.systemmonitor.disk.pojo.po.DiskMonitorNotifyLog;
import com.njcn.gather.systemmonitor.disk.pojo.po.DiskMonitorResult;
import com.njcn.gather.systemmonitor.disk.pojo.po.DiskMonitorTarget;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/**
* 磁盘监控通知发送组件。
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class DiskMonitorNotificationComponent {
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final TypeReference<List<DiskMonitorNotifyPathItem>> PATH_LIST_TYPE = new TypeReference<List<DiskMonitorNotifyPathItem>>() {
};
private static final TypeReference<List<DiskMonitorNotifyHttpItem>> HTTP_LIST_TYPE = new TypeReference<List<DiskMonitorNotifyHttpItem>>() {
};
private final DiskMonitorNotifyLogMapper notifyLogMapper;
private final ObjectMapper objectMapper;
public void sendNotifications(DiskMonitorJob job, DiskMonitorTarget target, DiskMonitorResult result,
BigDecimal usedPercent, String currentStatus, String notifyReason,
String notifyLevel, LocalDateTime scanTime, String message) {
List<DiskMonitorNotifyPathItem> pathItems;
List<DiskMonitorNotifyHttpItem> httpItems;
try {
pathItems = parsePathItems(target.getNotifyPathListJson());
httpItems = parseHttpItems(target.getNotifyHttpListJson());
} catch (Exception exception) {
insertFailedNotifyLog(job, result, target, notifyLevel, "N/A", "通知配置解析失败: " + exception.getMessage());
return;
}
boolean pathConfigured = isEnabled(target.getNotifyPathEnabled()) && pathItems.stream().anyMatch(item -> Boolean.TRUE.equals(item.getEnabled()));
boolean httpConfigured = isEnabled(target.getNotifyHttpEnabled()) && httpItems.stream().anyMatch(item -> Boolean.TRUE.equals(item.getEnabled()));
if (!pathConfigured && !httpConfigured) {
insertFailedNotifyLog(job, result, target, notifyLevel, "N/A", "当前盘符未配置可用通知通道");
return;
}
String notifyTitle = buildNotifyTitle(target.getDriveLetter(), notifyLevel, usedPercent);
String notifyContent = buildNotifyContent(job, target, usedPercent, currentStatus, notifyLevel, scanTime, message);
if (pathConfigured) {
for (DiskMonitorNotifyPathItem item : pathItems) {
if (!Boolean.TRUE.equals(item.getEnabled())) {
continue;
}
sendPathNotification(job, target, result, notifyReason, notifyLevel, notifyTitle, notifyContent, item,
usedPercent, currentStatus, scanTime, message);
}
}
if (httpConfigured) {
for (DiskMonitorNotifyHttpItem item : httpItems) {
if (!Boolean.TRUE.equals(item.getEnabled())) {
continue;
}
sendHttpNotification(job, target, result, notifyReason, notifyLevel, notifyTitle, notifyContent, item,
usedPercent, currentStatus, scanTime, message);
}
}
}
private void sendPathNotification(DiskMonitorJob job, DiskMonitorTarget target, DiskMonitorResult result,
String notifyReason, String notifyLevel, String notifyTitle, String notifyContent,
DiskMonitorNotifyPathItem item, BigDecimal usedPercent, String currentStatus,
LocalDateTime scanTime, String message) {
DiskMonitorNotifyLog notifyLog = createNotifyLog(job, result, target, notifyLevel, notifyTitle, notifyContent,
DiskMonitorConstant.CHANNEL_TYPE_PATH, item.getPath());
try {
Path directoryPath = Paths.get(item.getPath());
Files.createDirectories(directoryPath);
String fileName = String.format("disk-monitor-%s-%s-%s.json",
job.getJobNo(), target.getDriveLetter().replace(":", ""),
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")));
Path filePath = directoryPath.resolve(fileName);
Map<String, Object> payload = buildNotifyPayload(job, target, usedPercent, currentStatus, notifyReason, notifyLevel, scanTime, message);
Files.write(filePath, objectMapper.writerWithDefaultPrettyPrinter().writeValueAsBytes(payload));
notifyLog.setSendStatus(DiskMonitorConstant.SEND_STATUS_SUCCESS);
notifyLog.setResponseMessage("写入成功:" + filePath.toString());
} catch (Exception exception) {
log.error("磁盘监控路径通知发送失败driveLetter={}, path={}", target.getDriveLetter(), item.getPath(), exception);
notifyLog.setSendStatus(DiskMonitorConstant.SEND_STATUS_FAILED);
notifyLog.setResponseMessage(exception.getMessage());
}
notifyLogMapper.insert(notifyLog);
}
private void sendHttpNotification(DiskMonitorJob job, DiskMonitorTarget target, DiskMonitorResult result,
String notifyReason, String notifyLevel, String notifyTitle, String notifyContent,
DiskMonitorNotifyHttpItem item, BigDecimal usedPercent, String currentStatus,
LocalDateTime scanTime, String message) {
DiskMonitorNotifyLog notifyLog = createNotifyLog(job, result, target, notifyLevel, notifyTitle, notifyContent,
DiskMonitorConstant.CHANNEL_TYPE_HTTP, item.getUrl());
HttpURLConnection connection = null;
try {
Map<String, Object> payload = buildNotifyPayload(job, target, usedPercent, currentStatus, notifyReason, notifyLevel, scanTime, message);
byte[] body = objectMapper.writeValueAsBytes(payload);
URL url = new URL(item.getUrl());
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod(StrUtil.blankToDefault(item.getMethod(), DiskMonitorConstant.HTTP_METHOD_POST).trim().toUpperCase(Locale.ROOT));
connection.setConnectTimeout(resolveTimeout(item.getTimeoutMs()));
connection.setReadTimeout(resolveTimeout(item.getTimeoutMs()));
connection.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
connection.setDoInput(true);
if (!"GET".equalsIgnoreCase(connection.getRequestMethod())) {
connection.setDoOutput(true);
try (OutputStream outputStream = connection.getOutputStream()) {
outputStream.write(body);
}
}
int statusCode = connection.getResponseCode();
notifyLog.setSendStatus(statusCode >= 200 && statusCode < 300
? DiskMonitorConstant.SEND_STATUS_SUCCESS
: DiskMonitorConstant.SEND_STATUS_FAILED);
notifyLog.setResponseMessage(statusCode + " " + connection.getResponseMessage());
} catch (Exception exception) {
log.error("磁盘监控HTTP通知发送失败driveLetter={}, url={}", target.getDriveLetter(), item.getUrl(), exception);
notifyLog.setSendStatus(DiskMonitorConstant.SEND_STATUS_FAILED);
notifyLog.setResponseMessage(exception.getMessage());
} finally {
if (connection != null) {
connection.disconnect();
}
}
notifyLogMapper.insert(notifyLog);
}
private Map<String, Object> buildNotifyPayload(DiskMonitorJob job, DiskMonitorTarget target, BigDecimal usedPercent,
String currentStatus, String notifyReason, String notifyLevel,
LocalDateTime scanTime, String message) {
Map<String, Object> payload = new LinkedHashMap<>();
payload.put("jobNo", job.getJobNo());
payload.put("driveLetter", target.getDriveLetter());
payload.put("currentStatus", currentStatus);
payload.put("notifyReason", notifyReason);
payload.put("notifyLevel", notifyLevel);
payload.put("usedPercent", usedPercent);
payload.put("warningUsagePercent", target.getWarningUsagePercent());
payload.put("alarmUsagePercent", target.getAlarmUsagePercent());
payload.put("scanTime", formatDateTime(scanTime));
payload.put("message", message);
return payload;
}
private String buildNotifyTitle(String driveLetter, String notifyLevel, BigDecimal usedPercent) {
return String.format("磁盘监控通知[%s] %s 使用率%s%%",
notifyLevel, driveLetter, usedPercent == null ? "0.00" : usedPercent.toPlainString());
}
private String buildNotifyContent(DiskMonitorJob job, DiskMonitorTarget target, BigDecimal usedPercent,
String currentStatus, String notifyLevel, LocalDateTime scanTime, String message) {
StringBuilder builder = new StringBuilder();
builder.append("任务编号:").append(job.getJobNo())
.append(";盘符:").append(target.getDriveLetter())
.append(";当前状态:").append(currentStatus)
.append(";通知级别:").append(notifyLevel)
.append(";使用率:").append(usedPercent == null ? "0.00" : usedPercent.toPlainString()).append("%")
.append(";预警阈值:").append(target.getWarningUsagePercent()).append("%")
.append(";告警阈值:").append(target.getAlarmUsagePercent()).append("%")
.append(";扫描时间:").append(formatDateTime(scanTime));
if (StrUtil.isNotBlank(message)) {
builder.append(";说明:").append(message);
}
return builder.toString();
}
private DiskMonitorNotifyLog createNotifyLog(DiskMonitorJob job, DiskMonitorResult result, DiskMonitorTarget target,
String notifyLevel, String notifyTitle, String notifyContent,
String channelType, String channelTarget) {
DiskMonitorNotifyLog notifyLog = new DiskMonitorNotifyLog();
notifyLog.setJobId(job.getId());
notifyLog.setResultId(result.getId());
notifyLog.setTargetId(target.getId());
notifyLog.setDriveLetter(target.getDriveLetter());
notifyLog.setNotifyLevel(notifyLevel);
notifyLog.setChannelType(channelType);
notifyLog.setChannelTarget(channelTarget);
notifyLog.setNotifyTitle(notifyTitle);
notifyLog.setNotifyContent(notifyContent);
notifyLog.setSentAt(LocalDateTime.now());
return notifyLog;
}
private void insertFailedNotifyLog(DiskMonitorJob job, DiskMonitorResult result, DiskMonitorTarget target,
String notifyLevel, String channelTarget, String responseMessage) {
DiskMonitorNotifyLog notifyLog = createNotifyLog(job, result, target, notifyLevel,
"磁盘监控通知失败", responseMessage, DiskMonitorConstant.CHANNEL_TYPE_HTTP, channelTarget);
notifyLog.setSendStatus(DiskMonitorConstant.SEND_STATUS_FAILED);
notifyLog.setResponseMessage(responseMessage);
notifyLogMapper.insert(notifyLog);
}
private List<DiskMonitorNotifyPathItem> parsePathItems(String json) throws Exception {
return readJsonList(json, PATH_LIST_TYPE);
}
private List<DiskMonitorNotifyHttpItem> parseHttpItems(String json) throws Exception {
return readJsonList(json, HTTP_LIST_TYPE);
}
private <T> List<T> readJsonList(String json, TypeReference<List<T>> typeReference) throws Exception {
if (StrUtil.isBlank(json)) {
return new ArrayList<>();
}
List<T> result = objectMapper.readValue(json, typeReference);
return result == null ? new ArrayList<>() : result;
}
private boolean isEnabled(Integer value) {
return value != null && value == 1;
}
private int resolveTimeout(Integer timeoutMs) {
return timeoutMs == null || timeoutMs <= 0 ? DiskMonitorConstant.DEFAULT_HTTP_TIMEOUT_MS : timeoutMs;
}
private String formatDateTime(LocalDateTime dateTime) {
return dateTime == null ? null : dateTime.format(DATE_TIME_FORMATTER);
}
}

View File

@@ -0,0 +1,10 @@
package com.njcn.gather.systemmonitor.disk.config;
import org.springframework.context.annotation.Configuration;
/**
* 磁盘监控模块基础配置入口,后续磁盘监控相关 Bean 统一放在该模块下扩展。
*/
@Configuration
public class DiskMonitorModuleConfig {
}

View File

@@ -0,0 +1,43 @@
package com.njcn.gather.systemmonitor.disk.constant;
/**
* 磁盘监控模块常量。
*/
public interface DiskMonitorConstant {
String DEFAULT_POLICY_NAME = "默认磁盘监控策略";
String DEFAULT_DAILY_RUN_TIME = "08:30:00";
String WARNING_NOTIFY_MODE_STATUS_CHANGE = "STATUS_CHANGE";
String ALARM_NOTIFY_MODE_EVERY_TIME = "EVERY_TIME";
String STATUS_UNKNOWN = "UNKNOWN";
String STATUS_NORMAL = "NORMAL";
String STATUS_WARNING = "WARNING";
String STATUS_ALARM = "ALARM";
String JOB_SOURCE_APP_START = "APP_START";
String JOB_SOURCE_DAILY_SCHEDULE = "DAILY_SCHEDULE";
String JOB_SOURCE_MANUAL = "MANUAL";
String JOB_STATUS_RUNNING = "RUNNING";
String JOB_STATUS_SUCCESS = "SUCCESS";
String JOB_STATUS_PARTIAL_SUCCESS = "PARTIAL_SUCCESS";
String JOB_STATUS_FAILED = "FAILED";
String NOTIFY_REASON_ALARM_EVERY_TIME = "ALARM_EVERY_TIME";
String NOTIFY_REASON_STATUS_CHANGED = "STATUS_CHANGED";
String NOTIFY_REASON_NO_NOTIFY = "NO_NOTIFY";
String NOTIFY_LEVEL_WARNING = "WARNING";
String NOTIFY_LEVEL_ALARM = "ALARM";
String NOTIFY_LEVEL_RECOVER = "RECOVER";
String CHANNEL_TYPE_PATH = "PATH";
String CHANNEL_TYPE_HTTP = "HTTP";
String SEND_STATUS_SUCCESS = "SUCCESS";
String SEND_STATUS_FAILED = "FAILED";
String HTTP_METHOD_POST = "POST";
int DEFAULT_HTTP_TIMEOUT_MS = 5000;
}

View File

@@ -0,0 +1,19 @@
package com.njcn.gather.systemmonitor.disk.constant;
/**
* 磁盘监控参数校验提示。
*/
public interface DiskMonitorValidMessage {
String POLICY_NOT_NULL = "policy不能为空请检查policy参数";
String TARGETS_NOT_NULL = "targets不能为空请检查targets参数";
String DRIVE_LETTER_NOT_BLANK = "盘符不能为空请检查driveLetter参数";
String DAILY_RUN_TIME_NOT_BLANK = "每日统一执行时间不能为空请检查dailyRunTime参数";
String DAILY_RUN_TIME_FORMAT_ERROR = "每日统一执行时间格式错误请使用HH:mm:ss";
String WARNING_USAGE_PERCENT_NOT_NULL = "预警使用率不能为空请检查warningUsagePercent参数";
String ALARM_USAGE_PERCENT_NOT_NULL = "告警使用率不能为空请检查alarmUsagePercent参数";
String USAGE_PERCENT_FORMAT_ERROR = "阈值范围必须在1-100之间请检查阈值参数";
String JOB_SOURCE_NOT_BLANK = "任务来源不能为空请检查jobSource参数";
String JOB_ID_NOT_NULL = "jobId不能为空请检查jobId参数";
String POLICY_NAME_NOT_BLANK = "策略名称不能为空请检查policyName参数";
}

View File

@@ -0,0 +1,72 @@
package com.njcn.gather.systemmonitor.disk.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.common.pojo.annotation.OperateInfo;
import com.njcn.common.pojo.constant.OperateType;
import com.njcn.common.pojo.enums.common.LogEnum;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.response.HttpResult;
import com.njcn.common.utils.LogUtil;
import com.njcn.gather.systemmonitor.disk.pojo.param.DiskMonitorParam;
import com.njcn.gather.systemmonitor.disk.pojo.vo.DiskMonitorVO;
import com.njcn.gather.systemmonitor.disk.service.IDiskMonitorJobService;
import com.njcn.web.controller.BaseController;
import com.njcn.web.utils.HttpResultUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
/**
* 磁盘监控任务接口。
*/
@Validated
@Slf4j
@Api(tags = "磁盘监控任务")
@RestController
@RequestMapping("/disk-monitor")
@RequiredArgsConstructor
public class DiskMonitorJobController extends BaseController {
private final IDiskMonitorJobService diskMonitorJobService;
@OperateInfo(info = LogEnum.SYSTEM_COMMON, operateType = OperateType.UPDATE)
@PostMapping("/job/run")
@ApiOperation("手动执行磁盘监控")
@ApiImplicitParam(name = "param", value = "手动执行参数", required = true)
public HttpResult<DiskMonitorVO.JobRunVO> run(@RequestBody @Valid DiskMonitorParam.JobRunParam param) {
String methodDescribe = getMethodDescribe("run");
LogUtil.njcnDebug(log, "{}执行磁盘监控任务source={}", methodDescribe, param.getJobSource());
return HttpResultUtil.assembleResult(CommonResponseEnum.SUCCESS.getCode(), diskMonitorJobService.runJob(param), "任务已启动");
}
@OperateInfo(info = LogEnum.SYSTEM_COMMON)
@PostMapping("/job/list")
@ApiOperation("分页查询磁盘监控任务列表")
@ApiImplicitParam(name = "param", value = "任务分页查询参数", required = true)
public HttpResult<Page<DiskMonitorVO.JobListVO>> list(@RequestBody @Valid DiskMonitorParam.JobListParam param) {
String methodDescribe = getMethodDescribe("list");
LogUtil.njcnDebug(log, "{},分页查询磁盘监控任务列表", methodDescribe);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, diskMonitorJobService.listJobs(param), methodDescribe);
}
@OperateInfo(info = LogEnum.SYSTEM_COMMON)
@GetMapping("/job/{jobId}/detail")
@ApiOperation("查询磁盘监控任务详情")
@ApiImplicitParam(name = "jobId", value = "任务ID", required = true)
public HttpResult<DiskMonitorVO.JobDetailVO> jobDetail(@PathVariable("jobId") Long jobId) {
String methodDescribe = getMethodDescribe("jobDetail");
LogUtil.njcnDebug(log, "{}查询磁盘监控任务详情jobId={}", methodDescribe, jobId);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, diskMonitorJobService.getJobDetail(jobId), methodDescribe);
}
}

View File

@@ -0,0 +1,49 @@
package com.njcn.gather.systemmonitor.disk.controller;
import com.njcn.common.pojo.annotation.OperateInfo;
import com.njcn.common.pojo.constant.OperateType;
import com.njcn.common.pojo.enums.common.LogEnum;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.response.HttpResult;
import com.njcn.common.utils.LogUtil;
import com.njcn.gather.systemmonitor.disk.pojo.param.DiskMonitorParam;
import com.njcn.gather.systemmonitor.disk.service.IDiskMonitorNotifyService;
import com.njcn.web.controller.BaseController;
import com.njcn.web.utils.HttpResultUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
/**
* 磁盘监控通知接口。
*/
@Validated
@Slf4j
@Api(tags = "磁盘监控通知")
@RestController
@RequestMapping("/disk-monitor")
@RequiredArgsConstructor
public class DiskMonitorNotifyController extends BaseController {
private final IDiskMonitorNotifyService diskMonitorNotifyService;
@OperateInfo(info = LogEnum.SYSTEM_COMMON, operateType = OperateType.UPDATE)
@PostMapping("/notify/test")
@ApiOperation("测试磁盘监控通知")
@ApiImplicitParam(name = "param", value = "通知测试参数", required = true)
public HttpResult<Boolean> testNotify(@RequestBody @Valid DiskMonitorParam.NotifyTestParam param) {
String methodDescribe = getMethodDescribe("testNotify");
LogUtil.njcnDebug(log, "{}测试磁盘监控通知driveLetter={}", methodDescribe, param.getDriveLetter());
boolean result = diskMonitorNotifyService.testNotify(param);
return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL, result, methodDescribe);
}
}

View File

@@ -0,0 +1,60 @@
package com.njcn.gather.systemmonitor.disk.controller;
import com.njcn.common.pojo.annotation.OperateInfo;
import com.njcn.common.pojo.constant.OperateType;
import com.njcn.common.pojo.enums.common.LogEnum;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.response.HttpResult;
import com.njcn.common.utils.LogUtil;
import com.njcn.gather.systemmonitor.disk.pojo.param.DiskMonitorParam;
import com.njcn.gather.systemmonitor.disk.pojo.vo.DiskMonitorVO;
import com.njcn.gather.systemmonitor.disk.service.IDiskMonitorPolicyService;
import com.njcn.web.controller.BaseController;
import com.njcn.web.utils.HttpResultUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
/**
* 磁盘监控配置接口。
*/
@Validated
@Slf4j
@Api(tags = "磁盘监控配置")
@RestController
@RequestMapping("/disk-monitor")
@RequiredArgsConstructor
public class DiskMonitorPolicyController extends BaseController {
private final IDiskMonitorPolicyService diskMonitorPolicyService;
@OperateInfo(info = LogEnum.SYSTEM_COMMON)
@GetMapping("/policy/detail")
@ApiOperation("查询磁盘监控配置详情")
public HttpResult<DiskMonitorVO.PolicyDetailVO> detail() {
String methodDescribe = getMethodDescribe("detail");
LogUtil.njcnDebug(log, "{},查询磁盘监控配置详情", methodDescribe);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, diskMonitorPolicyService.getPolicyDetail(), methodDescribe);
}
@OperateInfo(info = LogEnum.SYSTEM_COMMON, operateType = OperateType.UPDATE)
@PostMapping("/policy/save")
@ApiOperation("保存磁盘监控配置")
@ApiImplicitParam(name = "param", value = "磁盘监控配置", required = true)
public HttpResult<Boolean> save(@RequestBody @Valid DiskMonitorParam.PolicySaveParam param) {
String methodDescribe = getMethodDescribe("save");
LogUtil.njcnDebug(log, "{},保存磁盘监控配置", methodDescribe);
boolean result = diskMonitorPolicyService.savePolicy(param);
return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL, result, methodDescribe);
}
}

View File

@@ -0,0 +1,37 @@
package com.njcn.gather.systemmonitor.disk.enums;
import lombok.Getter;
/**
* 磁盘监控模块业务响应码。
*/
@Getter
public enum DiskMonitorResponseEnum {
POLICY_NOT_FOUND("A01060", "磁盘监控策略不存在"),
TARGET_NOT_FOUND("A01061", "磁盘监控盘符配置不存在"),
DRIVE_LETTER_REPEAT("A01062", "盘符重复请检查driveLetter参数"),
DRIVE_LETTER_FORMAT_ERROR("A01063", "盘符格式错误,请使用类似 C: 的格式"),
DAILY_RUN_TIME_FORMAT_ERROR("A01064", "每日统一执行时间格式错误请使用HH:mm:ss"),
USAGE_PERCENT_INVALID("A01065", "告警使用率必须大于等于预警使用率"),
NOTIFY_PATH_EMPTY("A01066", "路径通知目标不能为空"),
NOTIFY_HTTP_EMPTY("A01067", "HTTP通知目标不能为空"),
NOTIFY_PATH_VALUE_EMPTY("A01068", "通知路径不能为空"),
NOTIFY_HTTP_URL_INVALID("A01069", "HTTP通知目标格式错误请检查url参数"),
NO_ENABLED_TARGET("A01070", "暂无启用的磁盘监控盘符配置"),
JOB_NOT_FOUND("A01071", "监控任务不存在"),
POLICY_SAVE_FAILED("A01072", "磁盘监控配置保存失败"),
DRIVE_SCAN_FAILED("A01073", "磁盘扫描失败"),
NOTIFY_TARGET_NOT_FOUND("A01074", "通知测试盘符不存在"),
NOTIFY_CHANNEL_MISSING("A01075", "当前盘符未配置可用通知通道"),
POLICY_MODE_INVALID("A01076", "通知模式配置非法"),
JOB_SOURCE_INVALID("A01077", "任务来源非法");
private final String code;
private final String message;
DiskMonitorResponseEnum(String code, String message) {
this.code = code;
this.message = message;
}
}

View File

@@ -0,0 +1,7 @@
package com.njcn.gather.systemmonitor.disk.event;
/**
* 磁盘监控策略变更事件。
*/
public class DiskMonitorPolicyChangedEvent {
}

View File

@@ -0,0 +1,10 @@
package com.njcn.gather.systemmonitor.disk.mapper;
import com.github.yulichang.base.MPJBaseMapper;
import com.njcn.gather.systemmonitor.disk.pojo.po.DiskMonitorJob;
/**
* 磁盘监控任务 Mapper。
*/
public interface DiskMonitorJobMapper extends MPJBaseMapper<DiskMonitorJob> {
}

View File

@@ -0,0 +1,10 @@
package com.njcn.gather.systemmonitor.disk.mapper;
import com.github.yulichang.base.MPJBaseMapper;
import com.njcn.gather.systemmonitor.disk.pojo.po.DiskMonitorNotifyLog;
/**
* 磁盘监控通知日志 Mapper。
*/
public interface DiskMonitorNotifyLogMapper extends MPJBaseMapper<DiskMonitorNotifyLog> {
}

View File

@@ -0,0 +1,10 @@
package com.njcn.gather.systemmonitor.disk.mapper;
import com.github.yulichang.base.MPJBaseMapper;
import com.njcn.gather.systemmonitor.disk.pojo.po.DiskMonitorPolicy;
/**
* 磁盘监控策略 Mapper。
*/
public interface DiskMonitorPolicyMapper extends MPJBaseMapper<DiskMonitorPolicy> {
}

View File

@@ -0,0 +1,10 @@
package com.njcn.gather.systemmonitor.disk.mapper;
import com.github.yulichang.base.MPJBaseMapper;
import com.njcn.gather.systemmonitor.disk.pojo.po.DiskMonitorResult;
/**
* 磁盘监控结果 Mapper。
*/
public interface DiskMonitorResultMapper extends MPJBaseMapper<DiskMonitorResult> {
}

View File

@@ -0,0 +1,10 @@
package com.njcn.gather.systemmonitor.disk.mapper;
import com.github.yulichang.base.MPJBaseMapper;
import com.njcn.gather.systemmonitor.disk.pojo.po.DiskMonitorTarget;
/**
* 磁盘监控盘符配置 Mapper。
*/
public interface DiskMonitorTargetMapper extends MPJBaseMapper<DiskMonitorTarget> {
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.njcn.gather.systemmonitor.disk.mapper.DiskMonitorJobMapper">
</mapper>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.njcn.gather.systemmonitor.disk.mapper.DiskMonitorNotifyLogMapper">
</mapper>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.njcn.gather.systemmonitor.disk.mapper.DiskMonitorPolicyMapper">
</mapper>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.njcn.gather.systemmonitor.disk.mapper.DiskMonitorResultMapper">
</mapper>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.njcn.gather.systemmonitor.disk.mapper.DiskMonitorTargetMapper">
</mapper>

View File

@@ -0,0 +1,31 @@
package com.njcn.gather.systemmonitor.disk.pojo.dto;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
/**
* HTTP 通知项。
*/
@Data
public class DiskMonitorNotifyHttpItem {
@ApiModelProperty("回调地址")
@NotBlank(message = "HTTP通知目标不能为空请检查url参数")
private String url;
@ApiModelProperty("名称")
private String name;
@ApiModelProperty("请求方法")
private String method;
@ApiModelProperty("超时时间")
@Min(value = 1, message = "timeoutMs必须大于0")
private Integer timeoutMs;
@ApiModelProperty("是否启用")
private Boolean enabled;
}

View File

@@ -0,0 +1,23 @@
package com.njcn.gather.systemmonitor.disk.pojo.dto;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotBlank;
/**
* 路径通知项。
*/
@Data
public class DiskMonitorNotifyPathItem {
@ApiModelProperty("路径")
@NotBlank(message = "通知路径不能为空请检查path参数")
private String path;
@ApiModelProperty("名称")
private String name;
@ApiModelProperty("是否启用")
private Boolean enabled;
}

View File

@@ -0,0 +1,131 @@
package com.njcn.gather.systemmonitor.disk.pojo.param;
import com.njcn.gather.systemmonitor.disk.constant.DiskMonitorValidMessage;
import com.njcn.gather.systemmonitor.disk.pojo.dto.DiskMonitorNotifyHttpItem;
import com.njcn.gather.systemmonitor.disk.pojo.dto.DiskMonitorNotifyPathItem;
import com.njcn.web.pojo.param.BaseParam;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.Valid;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.ArrayList;
import java.util.List;
/**
* 磁盘监控接口参数。
*/
public class DiskMonitorParam {
@Data
public static class PolicySaveParam {
@ApiModelProperty("全局策略")
@NotNull(message = DiskMonitorValidMessage.POLICY_NOT_NULL)
@Valid
private PolicyParam policy;
@ApiModelProperty("盘符配置")
@NotNull(message = DiskMonitorValidMessage.TARGETS_NOT_NULL)
@Valid
private List<TargetParam> targets = new ArrayList<>();
}
@Data
public static class PolicyParam {
@ApiModelProperty("策略ID")
private Long id;
@ApiModelProperty("策略名称")
@NotBlank(message = DiskMonitorValidMessage.POLICY_NAME_NOT_BLANK)
private String policyName;
@ApiModelProperty("是否启用监控")
private Boolean monitorEnabled;
@ApiModelProperty("应用启动后立即执行一次")
private Boolean runOnAppStart;
@ApiModelProperty("每日统一执行时间")
@NotBlank(message = DiskMonitorValidMessage.DAILY_RUN_TIME_NOT_BLANK)
private String dailyRunTime;
@ApiModelProperty("预警通知模式")
private String warningNotifyMode;
@ApiModelProperty("告警通知模式")
private String alarmNotifyMode;
@ApiModelProperty("备注")
private String remark;
}
@Data
public static class TargetParam {
@ApiModelProperty("盘符配置ID")
private Long id;
@ApiModelProperty("盘符")
@NotBlank(message = DiskMonitorValidMessage.DRIVE_LETTER_NOT_BLANK)
private String driveLetter;
@ApiModelProperty("是否监控")
private Boolean monitorEnabled;
@ApiModelProperty("预警使用率")
@NotNull(message = DiskMonitorValidMessage.WARNING_USAGE_PERCENT_NOT_NULL)
@Min(value = 1, message = DiskMonitorValidMessage.USAGE_PERCENT_FORMAT_ERROR)
@Max(value = 100, message = DiskMonitorValidMessage.USAGE_PERCENT_FORMAT_ERROR)
private Integer warningUsagePercent;
@ApiModelProperty("告警使用率")
@NotNull(message = DiskMonitorValidMessage.ALARM_USAGE_PERCENT_NOT_NULL)
@Min(value = 1, message = DiskMonitorValidMessage.USAGE_PERCENT_FORMAT_ERROR)
@Max(value = 100, message = DiskMonitorValidMessage.USAGE_PERCENT_FORMAT_ERROR)
private Integer alarmUsagePercent;
@ApiModelProperty("是否启用路径通知")
private Boolean notifyPathEnabled;
@ApiModelProperty("路径通知列表")
@Valid
private List<DiskMonitorNotifyPathItem> notifyPathList = new ArrayList<>();
@ApiModelProperty("是否启用HTTP通知")
private Boolean notifyHttpEnabled;
@ApiModelProperty("HTTP通知列表")
@Valid
private List<DiskMonitorNotifyHttpItem> notifyHttpList = new ArrayList<>();
@ApiModelProperty("备注")
private String remark;
}
@Data
public static class JobRunParam {
@ApiModelProperty("任务来源")
@NotBlank(message = DiskMonitorValidMessage.JOB_SOURCE_NOT_BLANK)
private String jobSource;
}
@Data
@EqualsAndHashCode(callSuper = true)
public static class JobListParam extends BaseParam {
}
@Data
public static class NotifyTestParam {
@ApiModelProperty("盘符")
@NotBlank(message = DiskMonitorValidMessage.DRIVE_LETTER_NOT_BLANK)
private String driveLetter;
}
}

View File

@@ -0,0 +1,58 @@
package com.njcn.gather.systemmonitor.disk.pojo.po;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 磁盘监控任务批次表。
*/
@Data
@TableName("disk_monitor_job")
public class DiskMonitorJob implements Serializable {
private static final long serialVersionUID = 2956749403195165256L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@TableField("job_no")
private String jobNo;
@TableField("job_source")
private String jobSource;
@TableField("planned_time")
private LocalDateTime plannedTime;
@TableField("started_at")
private LocalDateTime startedAt;
@TableField("finished_at")
private LocalDateTime finishedAt;
@TableField("job_status")
private String jobStatus;
@TableField("target_count")
private Integer targetCount;
@TableField("success_count")
private Integer successCount;
@TableField("warning_count")
private Integer warningCount;
@TableField("alarm_count")
private Integer alarmCount;
@TableField("message")
private String message;
@TableField("created_at")
private LocalDateTime createdAt;
}

View File

@@ -0,0 +1,58 @@
package com.njcn.gather.systemmonitor.disk.pojo.po;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 磁盘监控通知日志表。
*/
@Data
@TableName("disk_monitor_notify_log")
public class DiskMonitorNotifyLog implements Serializable {
private static final long serialVersionUID = -8873612319853902451L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@TableField("job_id")
private Long jobId;
@TableField("result_id")
private Long resultId;
@TableField("target_id")
private Long targetId;
@TableField("drive_letter")
private String driveLetter;
@TableField("notify_level")
private String notifyLevel;
@TableField("channel_type")
private String channelType;
@TableField("channel_target")
private String channelTarget;
@TableField("notify_title")
private String notifyTitle;
@TableField("notify_content")
private String notifyContent;
@TableField("send_status")
private String sendStatus;
@TableField("response_message")
private String responseMessage;
@TableField("sent_at")
private LocalDateTime sentAt;
}

View File

@@ -0,0 +1,59 @@
package com.njcn.gather.systemmonitor.disk.pojo.po;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.time.LocalTime;
/**
* 磁盘监控全局策略表。
*/
@Data
@TableName("disk_monitor_policy")
public class DiskMonitorPolicy implements Serializable {
private static final long serialVersionUID = -2789228200584651740L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@TableField("policy_name")
private String policyName;
@TableField("monitor_enabled")
private Integer monitorEnabled;
@TableField("run_on_app_start")
private Integer runOnAppStart;
@TableField("daily_run_time")
private LocalTime dailyRunTime;
@TableField("warning_notify_mode")
private String warningNotifyMode;
@TableField("alarm_notify_mode")
private String alarmNotifyMode;
@TableField("last_job_id")
private Long lastJobId;
@TableField("remark")
private String remark;
@TableField("created_by")
private String createdBy;
@TableField("created_at")
private LocalDateTime createdAt;
@TableField("updated_by")
private String updatedBy;
@TableField("updated_at")
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,65 @@
package com.njcn.gather.systemmonitor.disk.pojo.po;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 磁盘监控结果表。
*/
@Data
@TableName("disk_monitor_result")
public class DiskMonitorResult implements Serializable {
private static final long serialVersionUID = 5557836396879348480L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@TableField("job_id")
private Long jobId;
@TableField("target_id")
private Long targetId;
@TableField("drive_letter")
private String driveLetter;
@TableField("total_bytes")
private Long totalBytes;
@TableField("used_bytes")
private Long usedBytes;
@TableField("free_bytes")
private Long freeBytes;
@TableField("used_percent")
private BigDecimal usedPercent;
@TableField("current_status")
private String currentStatus;
@TableField("previous_status")
private String previousStatus;
@TableField("status_changed")
private Integer statusChanged;
@TableField("should_notify")
private Integer shouldNotify;
@TableField("notify_reason")
private String notifyReason;
@TableField("scan_time")
private LocalDateTime scanTime;
@TableField("message")
private String message;
}

View File

@@ -0,0 +1,74 @@
package com.njcn.gather.systemmonitor.disk.pojo.po;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 磁盘监控盘符配置表。
*/
@Data
@TableName("disk_monitor_target")
public class DiskMonitorTarget implements Serializable {
private static final long serialVersionUID = -3567440027709476481L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@TableField("policy_id")
private Long policyId;
@TableField("drive_letter")
private String driveLetter;
@TableField("monitor_enabled")
private Integer monitorEnabled;
@TableField("warning_usage_percent")
private Integer warningUsagePercent;
@TableField("alarm_usage_percent")
private Integer alarmUsagePercent;
@TableField("notify_path_enabled")
private Integer notifyPathEnabled;
@TableField("notify_path_list_json")
private String notifyPathListJson;
@TableField("notify_http_enabled")
private Integer notifyHttpEnabled;
@TableField("notify_http_list_json")
private String notifyHttpListJson;
@TableField("last_status")
private String lastStatus;
@TableField("last_scan_time")
private LocalDateTime lastScanTime;
@TableField("last_used_percent")
private BigDecimal lastUsedPercent;
@TableField("remark")
private String remark;
@TableField("created_by")
private String createdBy;
@TableField("created_at")
private LocalDateTime createdAt;
@TableField("updated_by")
private String updatedBy;
@TableField("updated_at")
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,124 @@
package com.njcn.gather.systemmonitor.disk.pojo.vo;
import com.njcn.gather.systemmonitor.disk.pojo.dto.DiskMonitorNotifyHttpItem;
import com.njcn.gather.systemmonitor.disk.pojo.dto.DiskMonitorNotifyPathItem;
import lombok.Data;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
/**
* 磁盘监控接口返回对象。
*/
public class DiskMonitorVO {
@Data
public static class PolicyDetailVO {
private PolicyVO policy;
private List<TargetVO> targets = new ArrayList<>();
}
@Data
public static class PolicyVO {
private Long id;
private String policyName;
private Boolean monitorEnabled;
private Boolean runOnAppStart;
private String dailyRunTime;
private String warningNotifyMode;
private String alarmNotifyMode;
private Long lastJobId;
private String remark;
}
@Data
public static class TargetVO {
private Long id;
private String driveLetter;
private Boolean monitorEnabled;
private Integer warningUsagePercent;
private Integer alarmUsagePercent;
private Boolean notifyPathEnabled;
private List<DiskMonitorNotifyPathItem> notifyPathList = new ArrayList<>();
private Boolean notifyHttpEnabled;
private List<DiskMonitorNotifyHttpItem> notifyHttpList = new ArrayList<>();
private String lastStatus;
private String lastScanTime;
private BigDecimal lastUsedPercent;
private String remark;
}
@Data
public static class JobRunVO {
private Long jobId;
private String jobNo;
}
@Data
public static class JobListVO {
private Long jobId;
private String jobNo;
private String jobSource;
private String startedAt;
private String finishedAt;
private String jobStatus;
private Integer targetCount;
private Integer warningCount;
private Integer alarmCount;
private String message;
}
@Data
public static class JobDetailVO {
private JobInfoVO job;
private List<ResultVO> results = new ArrayList<>();
private List<NotifyLogVO> notifyLogs = new ArrayList<>();
}
@Data
public static class JobInfoVO {
private Long id;
private String jobNo;
private String jobSource;
private String startedAt;
private String finishedAt;
private String jobStatus;
private Integer targetCount;
private Integer successCount;
private Integer warningCount;
private Integer alarmCount;
private String message;
}
@Data
public static class ResultVO {
private Long resultId;
private Long targetId;
private String driveLetter;
private Long totalBytes;
private Long usedBytes;
private Long freeBytes;
private BigDecimal usedPercent;
private String currentStatus;
private String previousStatus;
private Boolean statusChanged;
private Boolean shouldNotify;
private String notifyReason;
private String scanTime;
private String message;
}
@Data
public static class NotifyLogVO {
private Long id;
private Long resultId;
private String driveLetter;
private String notifyLevel;
private String channelType;
private String channelTarget;
private String sendStatus;
private String responseMessage;
private String sentAt;
}
}

View File

@@ -0,0 +1,94 @@
package com.njcn.gather.systemmonitor.disk.schedule;
import com.njcn.gather.systemmonitor.disk.event.DiskMonitorPolicyChangedEvent;
import com.njcn.gather.systemmonitor.disk.service.IDiskMonitorJobService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import javax.annotation.PreDestroy;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 磁盘监控启动与定时调度管理。
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class DiskMonitorScheduleManager implements ApplicationRunner {
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private final IDiskMonitorJobService diskMonitorJobService;
private final AtomicInteger threadIndex = new AtomicInteger(1);
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(createThreadFactory());
private ScheduledFuture<?> scheduledFuture;
@Override
public void run(ApplicationArguments args) {
try {
diskMonitorJobService.executeAppStartMonitor();
} catch (Exception exception) {
log.error("应用启动后执行磁盘监控失败", exception);
}
refreshSchedule();
}
@EventListener(DiskMonitorPolicyChangedEvent.class)
public void onPolicyChanged() {
refreshSchedule();
}
public synchronized void refreshSchedule() {
if (scheduledFuture != null) {
scheduledFuture.cancel(false);
scheduledFuture = null;
}
LocalDateTime nextRunTime = diskMonitorJobService.getNextDailyRunTime();
if (nextRunTime == null) {
log.info("磁盘监控每日调度未启用或未配置执行时间,跳过注册");
return;
}
long delayMs = Math.max(Duration.between(LocalDateTime.now(), nextRunTime).toMillis(), 0L);
scheduledFuture = scheduler.schedule(this::executeDailyTask, delayMs, TimeUnit.MILLISECONDS);
log.info("磁盘监控每日调度已注册,下次执行时间:{}", nextRunTime.format(DATE_TIME_FORMATTER));
}
private void executeDailyTask() {
try {
diskMonitorJobService.executeDailyScheduleMonitor();
} catch (Exception exception) {
log.error("每日磁盘监控执行失败", exception);
} finally {
refreshSchedule();
}
}
private ThreadFactory createThreadFactory() {
return runnable -> {
Thread thread = new Thread(runnable);
thread.setName("disk-monitor-schedule-" + threadIndex.getAndIncrement());
thread.setDaemon(true);
return thread;
};
}
@PreDestroy
public synchronized void destroy() {
if (scheduledFuture != null) {
scheduledFuture.cancel(false);
}
scheduler.shutdown();
}
}

View File

@@ -0,0 +1,27 @@
package com.njcn.gather.systemmonitor.disk.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.gather.systemmonitor.disk.pojo.param.DiskMonitorParam;
import com.njcn.gather.systemmonitor.disk.pojo.vo.DiskMonitorVO;
import java.time.LocalDateTime;
/**
* 磁盘监控任务服务。
*/
public interface IDiskMonitorJobService {
DiskMonitorVO.JobRunVO runJob(DiskMonitorParam.JobRunParam param);
Page<DiskMonitorVO.JobListVO> listJobs(DiskMonitorParam.JobListParam param);
DiskMonitorVO.JobDetailVO getJobDetail(Long jobId);
void executeAppStartMonitor();
void executeDailyScheduleMonitor();
LocalDateTime getNextDailyRunTime();
void executeNotifyTest(String driveLetter);
}

View File

@@ -0,0 +1,11 @@
package com.njcn.gather.systemmonitor.disk.service;
import com.njcn.gather.systemmonitor.disk.pojo.param.DiskMonitorParam;
/**
* 磁盘监控通知服务。
*/
public interface IDiskMonitorNotifyService {
boolean testNotify(DiskMonitorParam.NotifyTestParam param);
}

View File

@@ -0,0 +1,26 @@
package com.njcn.gather.systemmonitor.disk.service;
import com.njcn.gather.systemmonitor.disk.pojo.param.DiskMonitorParam;
import com.njcn.gather.systemmonitor.disk.pojo.po.DiskMonitorPolicy;
import com.njcn.gather.systemmonitor.disk.pojo.po.DiskMonitorTarget;
import com.njcn.gather.systemmonitor.disk.pojo.vo.DiskMonitorVO;
import java.util.List;
/**
* 磁盘监控配置服务。
*/
public interface IDiskMonitorPolicyService {
DiskMonitorVO.PolicyDetailVO getPolicyDetail();
boolean savePolicy(DiskMonitorParam.PolicySaveParam param);
DiskMonitorPolicy getCurrentPolicy();
DiskMonitorPolicy getOrCreatePolicy();
List<DiskMonitorTarget> listEnabledTargets();
DiskMonitorTarget getTargetByDriveLetter(String driveLetter);
}

View File

@@ -0,0 +1,517 @@
package com.njcn.gather.systemmonitor.disk.service.impl;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.systemmonitor.disk.component.DiskMonitorNotificationComponent;
import com.njcn.gather.systemmonitor.disk.constant.DiskMonitorConstant;
import com.njcn.gather.systemmonitor.disk.enums.DiskMonitorResponseEnum;
import com.njcn.gather.systemmonitor.disk.mapper.DiskMonitorJobMapper;
import com.njcn.gather.systemmonitor.disk.mapper.DiskMonitorNotifyLogMapper;
import com.njcn.gather.systemmonitor.disk.mapper.DiskMonitorPolicyMapper;
import com.njcn.gather.systemmonitor.disk.mapper.DiskMonitorResultMapper;
import com.njcn.gather.systemmonitor.disk.mapper.DiskMonitorTargetMapper;
import com.njcn.gather.systemmonitor.disk.pojo.param.DiskMonitorParam;
import com.njcn.gather.systemmonitor.disk.pojo.po.DiskMonitorJob;
import com.njcn.gather.systemmonitor.disk.pojo.po.DiskMonitorNotifyLog;
import com.njcn.gather.systemmonitor.disk.pojo.po.DiskMonitorPolicy;
import com.njcn.gather.systemmonitor.disk.pojo.po.DiskMonitorResult;
import com.njcn.gather.systemmonitor.disk.pojo.po.DiskMonitorTarget;
import com.njcn.gather.systemmonitor.disk.pojo.vo.DiskMonitorVO;
import com.njcn.gather.systemmonitor.disk.service.IDiskMonitorJobService;
import com.njcn.gather.systemmonitor.disk.service.IDiskMonitorPolicyService;
import com.njcn.web.factory.PageFactory;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.File;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* 磁盘监控任务服务实现。
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class DiskMonitorJobServiceImpl implements IDiskMonitorJobService {
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private final IDiskMonitorPolicyService diskMonitorPolicyService;
private final DiskMonitorNotificationComponent diskMonitorNotificationComponent;
private final DiskMonitorJobMapper jobMapper;
private final DiskMonitorResultMapper resultMapper;
private final DiskMonitorNotifyLogMapper notifyLogMapper;
private final DiskMonitorTargetMapper targetMapper;
private final DiskMonitorPolicyMapper policyMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public DiskMonitorVO.JobRunVO runJob(DiskMonitorParam.JobRunParam param) {
String jobSource = StrUtil.blankToDefault(param.getJobSource(), DiskMonitorConstant.JOB_SOURCE_MANUAL).trim().toUpperCase();
if (!DiskMonitorConstant.JOB_SOURCE_MANUAL.equals(jobSource)) {
throw new BusinessException(DiskMonitorResponseEnum.JOB_SOURCE_INVALID);
}
return executeMonitorJob(jobSource, null, false, true);
}
@Override
public Page<DiskMonitorVO.JobListVO> listJobs(DiskMonitorParam.JobListParam param) {
Page<DiskMonitorJob> page = jobMapper.selectPage(
new Page<>(PageFactory.getPageNum(param), PageFactory.getPageSize(param)),
new QueryWrapper<DiskMonitorJob>().orderByDesc("started_at", "id")
);
Page<DiskMonitorVO.JobListVO> resultPage = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
resultPage.setRecords(page.getRecords().stream().map(this::toJobListVO).collect(Collectors.toList()));
return resultPage;
}
@Override
public DiskMonitorVO.JobDetailVO getJobDetail(Long jobId) {
DiskMonitorJob job = jobMapper.selectById(jobId);
if (job == null) {
throw new BusinessException(DiskMonitorResponseEnum.JOB_NOT_FOUND);
}
DiskMonitorVO.JobDetailVO detailVO = new DiskMonitorVO.JobDetailVO();
detailVO.setJob(toJobInfoVO(job));
List<DiskMonitorResult> results = resultMapper.selectList(new QueryWrapper<DiskMonitorResult>()
.eq("job_id", jobId)
.orderByAsc("scan_time", "id"));
detailVO.setResults(results.stream().map(this::toResultVO).collect(Collectors.toList()));
List<DiskMonitorNotifyLog> notifyLogs = notifyLogMapper.selectList(new QueryWrapper<DiskMonitorNotifyLog>()
.eq("job_id", jobId)
.orderByAsc("sent_at", "id"));
detailVO.setNotifyLogs(notifyLogs.stream().map(this::toNotifyLogVO).collect(Collectors.toList()));
return detailVO;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void executeAppStartMonitor() {
DiskMonitorPolicy policy = diskMonitorPolicyService.getOrCreatePolicy();
if (!isEnabled(policy.getMonitorEnabled()) || !isEnabled(policy.getRunOnAppStart())) {
return;
}
executeMonitorJob(DiskMonitorConstant.JOB_SOURCE_APP_START, null, false, false);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void executeDailyScheduleMonitor() {
DiskMonitorPolicy policy = diskMonitorPolicyService.getOrCreatePolicy();
if (!isEnabled(policy.getMonitorEnabled()) || policy.getDailyRunTime() == null) {
return;
}
executeMonitorJob(DiskMonitorConstant.JOB_SOURCE_DAILY_SCHEDULE, null, false, false);
}
@Override
public LocalDateTime getNextDailyRunTime() {
DiskMonitorPolicy policy = diskMonitorPolicyService.getOrCreatePolicy();
if (!isEnabled(policy.getMonitorEnabled()) || policy.getDailyRunTime() == null) {
return null;
}
LocalDateTime now = LocalDateTime.now();
LocalDateTime candidate = LocalDate.now().atTime(policy.getDailyRunTime());
if (!candidate.isAfter(now)) {
candidate = candidate.plusDays(1);
}
return candidate;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void executeNotifyTest(String driveLetter) {
executeMonitorJob(DiskMonitorConstant.JOB_SOURCE_MANUAL, driveLetter, true, true);
}
private DiskMonitorVO.JobRunVO executeMonitorJob(String jobSource, String driveLetter, boolean forceNotify, boolean failWhenNoTarget) {
validateJobSource(jobSource);
List<DiskMonitorTarget> targets = resolveExecutionTargets(driveLetter);
if (targets.isEmpty()) {
logNoExecutionTargets(jobSource, driveLetter);
if (failWhenNoTarget) {
throw new BusinessException(DiskMonitorResponseEnum.NO_ENABLED_TARGET);
}
return null;
}
LocalDateTime now = LocalDateTime.now();
DiskMonitorJob job = new DiskMonitorJob();
job.setJobNo(generateJobNo());
job.setJobSource(jobSource);
job.setPlannedTime(now);
job.setStartedAt(now);
job.setJobStatus(DiskMonitorConstant.JOB_STATUS_RUNNING);
job.setTargetCount(targets.size());
job.setSuccessCount(0);
job.setWarningCount(0);
job.setAlarmCount(0);
job.setMessage(forceNotify ? "通知测试任务" : null);
jobMapper.insert(job);
int successCount = 0;
int warningCount = 0;
int alarmCount = 0;
List<String> errorMessages = new ArrayList<>();
DiskMonitorPolicy policy = diskMonitorPolicyService.getOrCreatePolicy();
for (DiskMonitorTarget target : targets) {
try {
ScanSnapshot snapshot = resolveSnapshotStatus(target, scanDrive(target.getDriveLetter()));
if (snapshot.isSuccess()) {
successCount++;
}
if (DiskMonitorConstant.STATUS_WARNING.equals(snapshot.getCurrentStatus())) {
warningCount++;
}
if (DiskMonitorConstant.STATUS_ALARM.equals(snapshot.getCurrentStatus())) {
alarmCount++;
}
NotifyDecision notifyDecision = resolveNotifyDecision(target, snapshot.getCurrentStatus(), forceNotify);
DiskMonitorResult result = buildResult(job, target, snapshot, notifyDecision);
resultMapper.insert(result);
if (notifyDecision.isShouldNotify()) {
diskMonitorNotificationComponent.sendNotifications(job, target, result,
snapshot.getUsedPercent(), snapshot.getCurrentStatus(),
notifyDecision.getNotifyReason(), notifyDecision.getNotifyLevel(),
snapshot.getScanTime(), snapshot.getMessage());
}
updateTargetLastState(target, snapshot);
} catch (Exception exception) {
log.error("磁盘监控执行异常driveLetter={}", target.getDriveLetter(), exception);
errorMessages.add(target.getDriveLetter() + ":" + exception.getMessage());
ScanSnapshot failedSnapshot = ScanSnapshot.failed(exception.getMessage());
DiskMonitorResult result = buildResult(job, target, failedSnapshot,
new NotifyDecision(false, DiskMonitorConstant.NOTIFY_REASON_NO_NOTIFY, resolveNotifyLevel(failedSnapshot.getCurrentStatus())));
resultMapper.insert(result);
updateTargetLastState(target, failedSnapshot);
}
}
finishJob(job, policy, successCount, warningCount, alarmCount, errorMessages);
DiskMonitorVO.JobRunVO runVO = new DiskMonitorVO.JobRunVO();
runVO.setJobId(job.getId());
runVO.setJobNo(job.getJobNo());
return runVO;
}
private void finishJob(DiskMonitorJob job, DiskMonitorPolicy policy, int successCount, int warningCount,
int alarmCount, List<String> errorMessages) {
job.setSuccessCount(successCount);
job.setWarningCount(warningCount);
job.setAlarmCount(alarmCount);
job.setFinishedAt(LocalDateTime.now());
if (successCount == job.getTargetCount()) {
job.setJobStatus(DiskMonitorConstant.JOB_STATUS_SUCCESS);
} else if (successCount == 0) {
job.setJobStatus(DiskMonitorConstant.JOB_STATUS_FAILED);
} else {
job.setJobStatus(DiskMonitorConstant.JOB_STATUS_PARTIAL_SUCCESS);
}
if (!errorMessages.isEmpty()) {
job.setMessage(String.join("", errorMessages));
}
jobMapper.updateById(job);
policy.setLastJobId(job.getId());
policyMapper.updateById(policy);
}
private DiskMonitorResult buildResult(DiskMonitorJob job, DiskMonitorTarget target, ScanSnapshot snapshot, NotifyDecision notifyDecision) {
DiskMonitorResult result = new DiskMonitorResult();
result.setJobId(job.getId());
result.setTargetId(target.getId());
result.setDriveLetter(target.getDriveLetter());
result.setTotalBytes(snapshot.getTotalBytes());
result.setUsedBytes(snapshot.getUsedBytes());
result.setFreeBytes(snapshot.getFreeBytes());
result.setUsedPercent(snapshot.getUsedPercent());
result.setCurrentStatus(snapshot.getCurrentStatus());
result.setPreviousStatus(resolvePreviousStatus(target.getLastStatus()));
result.setStatusChanged(snapshot.getCurrentStatus().equals(resolvePreviousStatus(target.getLastStatus())) ? 0 : 1);
result.setShouldNotify(notifyDecision.isShouldNotify() ? 1 : 0);
result.setNotifyReason(notifyDecision.getNotifyReason());
result.setScanTime(snapshot.getScanTime());
result.setMessage(snapshot.getMessage());
return result;
}
private void updateTargetLastState(DiskMonitorTarget target, ScanSnapshot snapshot) {
target.setLastStatus(snapshot.getCurrentStatus());
target.setLastScanTime(snapshot.getScanTime());
target.setLastUsedPercent(snapshot.getUsedPercent());
targetMapper.updateById(target);
}
private NotifyDecision resolveNotifyDecision(DiskMonitorTarget target, String currentStatus, boolean forceNotify) {
String previousStatus = resolvePreviousStatus(target.getLastStatus());
if (forceNotify) {
return new NotifyDecision(true, DiskMonitorConstant.NOTIFY_REASON_STATUS_CHANGED, resolveNotifyLevel(currentStatus));
}
if (DiskMonitorConstant.STATUS_ALARM.equals(currentStatus)) {
return new NotifyDecision(true, DiskMonitorConstant.NOTIFY_REASON_ALARM_EVERY_TIME, DiskMonitorConstant.NOTIFY_LEVEL_ALARM);
}
if (DiskMonitorConstant.STATUS_WARNING.equals(currentStatus) && !DiskMonitorConstant.STATUS_WARNING.equals(previousStatus)) {
return new NotifyDecision(true, DiskMonitorConstant.NOTIFY_REASON_STATUS_CHANGED, DiskMonitorConstant.NOTIFY_LEVEL_WARNING);
}
if (DiskMonitorConstant.STATUS_NORMAL.equals(currentStatus)
&& (DiskMonitorConstant.STATUS_WARNING.equals(previousStatus) || DiskMonitorConstant.STATUS_ALARM.equals(previousStatus))) {
return new NotifyDecision(true, DiskMonitorConstant.NOTIFY_REASON_STATUS_CHANGED, DiskMonitorConstant.NOTIFY_LEVEL_RECOVER);
}
return new NotifyDecision(false, DiskMonitorConstant.NOTIFY_REASON_NO_NOTIFY, resolveNotifyLevel(currentStatus));
}
private String resolveNotifyLevel(String currentStatus) {
if (DiskMonitorConstant.STATUS_ALARM.equals(currentStatus)) {
return DiskMonitorConstant.NOTIFY_LEVEL_ALARM;
}
if (DiskMonitorConstant.STATUS_WARNING.equals(currentStatus)) {
return DiskMonitorConstant.NOTIFY_LEVEL_WARNING;
}
return DiskMonitorConstant.NOTIFY_LEVEL_RECOVER;
}
private ScanSnapshot scanDrive(String driveLetter) {
File driveRoot = new File(driveLetter + File.separator);
if (!driveRoot.exists()) {
log.warn("磁盘监控扫描跳过监控盘符不存在或当前运行账号无访问权限driveLetter={}", driveLetter);
return ScanSnapshot.failed("盘符不存在或当前运行账户无访问权限");
}
long totalBytes = driveRoot.getTotalSpace();
if (totalBytes <= 0) {
log.warn("磁盘监控扫描失败未读取到有效磁盘容量driveLetter={}", driveLetter);
return ScanSnapshot.failed("未读取到有效磁盘容量");
}
long freeBytes = driveRoot.getUsableSpace();
long usedBytes = totalBytes - freeBytes;
BigDecimal usedPercent = BigDecimal.valueOf(usedBytes)
.multiply(BigDecimal.valueOf(100))
.divide(BigDecimal.valueOf(totalBytes), 2, RoundingMode.HALF_UP);
return new ScanSnapshot(true, totalBytes, usedBytes, freeBytes, usedPercent, LocalDateTime.now(),
DiskMonitorConstant.STATUS_UNKNOWN, null);
}
private ScanSnapshot resolveSnapshotStatus(DiskMonitorTarget target, ScanSnapshot snapshot) {
if (!snapshot.isSuccess()) {
return snapshot;
}
String currentStatus;
if (snapshot.getUsedPercent().compareTo(BigDecimal.valueOf(target.getAlarmUsagePercent())) >= 0) {
currentStatus = DiskMonitorConstant.STATUS_ALARM;
} else if (snapshot.getUsedPercent().compareTo(BigDecimal.valueOf(target.getWarningUsagePercent())) >= 0) {
currentStatus = DiskMonitorConstant.STATUS_WARNING;
} else {
currentStatus = DiskMonitorConstant.STATUS_NORMAL;
}
return new ScanSnapshot(true, snapshot.getTotalBytes(), snapshot.getUsedBytes(), snapshot.getFreeBytes(),
snapshot.getUsedPercent(), snapshot.getScanTime(), currentStatus, snapshot.getMessage());
}
private List<DiskMonitorTarget> resolveExecutionTargets(String driveLetter) {
if (StrUtil.isNotBlank(driveLetter)) {
DiskMonitorTarget target = diskMonitorPolicyService.getTargetByDriveLetter(driveLetter);
return target == null ? Collections.emptyList() : Collections.singletonList(target);
}
return diskMonitorPolicyService.listEnabledTargets();
}
private void logNoExecutionTargets(String jobSource, String driveLetter) {
if (StrUtil.isNotBlank(driveLetter)) {
log.warn("磁盘监控执行跳过未找到可执行的盘符配置jobSource={}, driveLetter={}", jobSource, driveLetter);
return;
}
log.warn("磁盘监控执行跳过暂无启用的磁盘监控盘符配置jobSource={}", jobSource);
}
private void validateJobSource(String jobSource) {
if (!DiskMonitorConstant.JOB_SOURCE_APP_START.equals(jobSource)
&& !DiskMonitorConstant.JOB_SOURCE_DAILY_SCHEDULE.equals(jobSource)
&& !DiskMonitorConstant.JOB_SOURCE_MANUAL.equals(jobSource)) {
throw new BusinessException(DiskMonitorResponseEnum.JOB_SOURCE_INVALID);
}
}
private boolean isEnabled(Integer value) {
return value != null && value == 1;
}
private String resolvePreviousStatus(String previousStatus) {
return StrUtil.isBlank(previousStatus) ? DiskMonitorConstant.STATUS_UNKNOWN : previousStatus;
}
private String formatDateTime(LocalDateTime dateTime) {
return dateTime == null ? null : dateTime.format(DATE_TIME_FORMATTER);
}
private String generateJobNo() {
return "DM" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"));
}
private DiskMonitorVO.JobListVO toJobListVO(DiskMonitorJob job) {
DiskMonitorVO.JobListVO jobListVO = new DiskMonitorVO.JobListVO();
jobListVO.setJobId(job.getId());
jobListVO.setJobNo(job.getJobNo());
jobListVO.setJobSource(job.getJobSource());
jobListVO.setStartedAt(formatDateTime(job.getStartedAt()));
jobListVO.setFinishedAt(formatDateTime(job.getFinishedAt()));
jobListVO.setJobStatus(job.getJobStatus());
jobListVO.setTargetCount(job.getTargetCount());
jobListVO.setWarningCount(job.getWarningCount());
jobListVO.setAlarmCount(job.getAlarmCount());
jobListVO.setMessage(job.getMessage());
return jobListVO;
}
private DiskMonitorVO.JobInfoVO toJobInfoVO(DiskMonitorJob job) {
DiskMonitorVO.JobInfoVO jobInfoVO = new DiskMonitorVO.JobInfoVO();
jobInfoVO.setId(job.getId());
jobInfoVO.setJobNo(job.getJobNo());
jobInfoVO.setJobSource(job.getJobSource());
jobInfoVO.setStartedAt(formatDateTime(job.getStartedAt()));
jobInfoVO.setFinishedAt(formatDateTime(job.getFinishedAt()));
jobInfoVO.setJobStatus(job.getJobStatus());
jobInfoVO.setTargetCount(job.getTargetCount());
jobInfoVO.setSuccessCount(job.getSuccessCount());
jobInfoVO.setWarningCount(job.getWarningCount());
jobInfoVO.setAlarmCount(job.getAlarmCount());
jobInfoVO.setMessage(job.getMessage());
return jobInfoVO;
}
private DiskMonitorVO.ResultVO toResultVO(DiskMonitorResult result) {
DiskMonitorVO.ResultVO resultVO = new DiskMonitorVO.ResultVO();
resultVO.setResultId(result.getId());
resultVO.setTargetId(result.getTargetId());
resultVO.setDriveLetter(result.getDriveLetter());
resultVO.setTotalBytes(result.getTotalBytes());
resultVO.setUsedBytes(result.getUsedBytes());
resultVO.setFreeBytes(result.getFreeBytes());
resultVO.setUsedPercent(result.getUsedPercent());
resultVO.setCurrentStatus(result.getCurrentStatus());
resultVO.setPreviousStatus(result.getPreviousStatus());
resultVO.setStatusChanged(Objects.equals(result.getStatusChanged(), 1));
resultVO.setShouldNotify(Objects.equals(result.getShouldNotify(), 1));
resultVO.setNotifyReason(result.getNotifyReason());
resultVO.setScanTime(formatDateTime(result.getScanTime()));
resultVO.setMessage(result.getMessage());
return resultVO;
}
private DiskMonitorVO.NotifyLogVO toNotifyLogVO(DiskMonitorNotifyLog notifyLog) {
DiskMonitorVO.NotifyLogVO notifyLogVO = new DiskMonitorVO.NotifyLogVO();
notifyLogVO.setId(notifyLog.getId());
notifyLogVO.setResultId(notifyLog.getResultId());
notifyLogVO.setDriveLetter(notifyLog.getDriveLetter());
notifyLogVO.setNotifyLevel(notifyLog.getNotifyLevel());
notifyLogVO.setChannelType(notifyLog.getChannelType());
notifyLogVO.setChannelTarget(notifyLog.getChannelTarget());
notifyLogVO.setSendStatus(notifyLog.getSendStatus());
notifyLogVO.setResponseMessage(notifyLog.getResponseMessage());
notifyLogVO.setSentAt(formatDateTime(notifyLog.getSentAt()));
return notifyLogVO;
}
private static class NotifyDecision {
private final boolean shouldNotify;
private final String notifyReason;
private final String notifyLevel;
private NotifyDecision(boolean shouldNotify, String notifyReason, String notifyLevel) {
this.shouldNotify = shouldNotify;
this.notifyReason = notifyReason;
this.notifyLevel = notifyLevel;
}
public boolean isShouldNotify() {
return shouldNotify;
}
public String getNotifyReason() {
return notifyReason;
}
public String getNotifyLevel() {
return notifyLevel;
}
}
private static class ScanSnapshot {
private final boolean success;
private final Long totalBytes;
private final Long usedBytes;
private final Long freeBytes;
private final BigDecimal usedPercent;
private final LocalDateTime scanTime;
private final String currentStatus;
private final String message;
private ScanSnapshot(boolean success, Long totalBytes, Long usedBytes, Long freeBytes,
BigDecimal usedPercent, LocalDateTime scanTime, String currentStatus, String message) {
this.success = success;
this.totalBytes = totalBytes;
this.usedBytes = usedBytes;
this.freeBytes = freeBytes;
this.usedPercent = usedPercent == null ? BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP) : usedPercent;
this.scanTime = scanTime == null ? LocalDateTime.now() : scanTime;
this.currentStatus = StrUtil.blankToDefault(currentStatus, DiskMonitorConstant.STATUS_UNKNOWN);
this.message = message;
}
public static ScanSnapshot failed(String message) {
return new ScanSnapshot(false, 0L, 0L, 0L, BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP),
LocalDateTime.now(), DiskMonitorConstant.STATUS_UNKNOWN, message);
}
public boolean isSuccess() {
return success;
}
public Long getTotalBytes() {
return totalBytes;
}
public Long getUsedBytes() {
return usedBytes;
}
public Long getFreeBytes() {
return freeBytes;
}
public BigDecimal getUsedPercent() {
return usedPercent;
}
public LocalDateTime getScanTime() {
return scanTime;
}
public String getCurrentStatus() {
if (!success || totalBytes == null || totalBytes <= 0) {
return DiskMonitorConstant.STATUS_UNKNOWN;
}
return currentStatus == null ? DiskMonitorConstant.STATUS_NORMAL : currentStatus;
}
public String getMessage() {
return message;
}
}
}

View File

@@ -0,0 +1,34 @@
package com.njcn.gather.systemmonitor.disk.service.impl;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.systemmonitor.disk.enums.DiskMonitorResponseEnum;
import com.njcn.gather.systemmonitor.disk.pojo.param.DiskMonitorParam;
import com.njcn.gather.systemmonitor.disk.pojo.po.DiskMonitorTarget;
import com.njcn.gather.systemmonitor.disk.service.IDiskMonitorJobService;
import com.njcn.gather.systemmonitor.disk.service.IDiskMonitorNotifyService;
import com.njcn.gather.systemmonitor.disk.service.IDiskMonitorPolicyService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 磁盘监控通知服务实现。
*/
@Service
@RequiredArgsConstructor
public class DiskMonitorNotifyServiceImpl implements IDiskMonitorNotifyService {
private final IDiskMonitorPolicyService diskMonitorPolicyService;
private final IDiskMonitorJobService diskMonitorJobService;
@Override
@Transactional(rollbackFor = Exception.class)
public boolean testNotify(DiskMonitorParam.NotifyTestParam param) {
DiskMonitorTarget target = diskMonitorPolicyService.getTargetByDriveLetter(param.getDriveLetter());
if (target == null) {
throw new BusinessException(DiskMonitorResponseEnum.NOTIFY_TARGET_NOT_FOUND);
}
diskMonitorJobService.executeNotifyTest(target.getDriveLetter());
return true;
}
}

View File

@@ -0,0 +1,341 @@
package com.njcn.gather.systemmonitor.disk.service.impl;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.systemmonitor.disk.constant.DiskMonitorConstant;
import com.njcn.gather.systemmonitor.disk.enums.DiskMonitorResponseEnum;
import com.njcn.gather.systemmonitor.disk.event.DiskMonitorPolicyChangedEvent;
import com.njcn.gather.systemmonitor.disk.mapper.DiskMonitorPolicyMapper;
import com.njcn.gather.systemmonitor.disk.mapper.DiskMonitorTargetMapper;
import com.njcn.gather.systemmonitor.disk.pojo.dto.DiskMonitorNotifyHttpItem;
import com.njcn.gather.systemmonitor.disk.pojo.dto.DiskMonitorNotifyPathItem;
import com.njcn.gather.systemmonitor.disk.pojo.param.DiskMonitorParam;
import com.njcn.gather.systemmonitor.disk.pojo.po.DiskMonitorPolicy;
import com.njcn.gather.systemmonitor.disk.pojo.po.DiskMonitorTarget;
import com.njcn.gather.systemmonitor.disk.pojo.vo.DiskMonitorVO;
import com.njcn.gather.systemmonitor.disk.service.IDiskMonitorPolicyService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.net.URL;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 磁盘监控配置服务实现。
*/
@Service
@RequiredArgsConstructor
public class DiskMonitorPolicyServiceImpl implements IDiskMonitorPolicyService {
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss");
private static final TypeReference<List<DiskMonitorNotifyPathItem>> PATH_LIST_TYPE = new TypeReference<List<DiskMonitorNotifyPathItem>>() {
};
private static final TypeReference<List<DiskMonitorNotifyHttpItem>> HTTP_LIST_TYPE = new TypeReference<List<DiskMonitorNotifyHttpItem>>() {
};
private final DiskMonitorPolicyMapper policyMapper;
private final DiskMonitorTargetMapper targetMapper;
private final ObjectMapper objectMapper;
private final ApplicationEventPublisher applicationEventPublisher;
@Override
@Transactional(rollbackFor = Exception.class)
public DiskMonitorVO.PolicyDetailVO getPolicyDetail() {
DiskMonitorPolicy policy = getOrCreatePolicy();
DiskMonitorVO.PolicyDetailVO detailVO = new DiskMonitorVO.PolicyDetailVO();
detailVO.setPolicy(toPolicyVO(policy));
detailVO.setTargets(listTargetsByPolicyId(policy.getId()).stream().map(this::toTargetVO).collect(Collectors.toList()));
return detailVO;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean savePolicy(DiskMonitorParam.PolicySaveParam param) {
validateSaveParam(param);
DiskMonitorPolicy currentPolicy = getOrCreatePolicy();
DiskMonitorParam.PolicyParam policyParam = param.getPolicy();
currentPolicy.setPolicyName(policyParam.getPolicyName().trim());
currentPolicy.setMonitorEnabled(boolToInt(policyParam.getMonitorEnabled(), true));
currentPolicy.setRunOnAppStart(boolToInt(policyParam.getRunOnAppStart(), true));
currentPolicy.setDailyRunTime(parseTime(policyParam.getDailyRunTime()));
currentPolicy.setWarningNotifyMode(resolveWarningNotifyMode(policyParam.getWarningNotifyMode()));
currentPolicy.setAlarmNotifyMode(resolveAlarmNotifyMode(policyParam.getAlarmNotifyMode()));
currentPolicy.setRemark(policyParam.getRemark());
if (policyMapper.updateById(currentPolicy) <= 0) {
throw new BusinessException(DiskMonitorResponseEnum.POLICY_SAVE_FAILED);
}
List<DiskMonitorTarget> existingTargets = listTargetsByPolicyId(currentPolicy.getId());
Map<Long, DiskMonitorTarget> existingById = existingTargets.stream()
.collect(Collectors.toMap(DiskMonitorTarget::getId, item -> item, (left, right) -> left, LinkedHashMap::new));
Map<String, DiskMonitorTarget> existingByDrive = existingTargets.stream()
.collect(Collectors.toMap(item -> normalizeDriveLetter(item.getDriveLetter()), item -> item, (left, right) -> left, LinkedHashMap::new));
List<Long> keepIds = new ArrayList<>();
for (DiskMonitorParam.TargetParam targetParam : param.getTargets()) {
String driveLetter = normalizeDriveLetter(targetParam.getDriveLetter());
DiskMonitorTarget target = targetParam.getId() == null ? null : existingById.get(targetParam.getId());
if (target == null) {
target = existingByDrive.get(driveLetter);
}
if (target == null) {
target = new DiskMonitorTarget();
target.setPolicyId(currentPolicy.getId());
target.setDriveLetter(driveLetter);
target.setLastStatus(DiskMonitorConstant.STATUS_UNKNOWN);
}
fillTarget(target, targetParam, driveLetter);
if (target.getId() == null) {
targetMapper.insert(target);
} else {
targetMapper.updateById(target);
}
keepIds.add(target.getId());
}
List<Long> deleteIds = existingTargets.stream()
.map(DiskMonitorTarget::getId)
.filter(id -> !keepIds.contains(id))
.collect(Collectors.toList());
if (!deleteIds.isEmpty()) {
targetMapper.deleteBatchIds(deleteIds);
}
applicationEventPublisher.publishEvent(new DiskMonitorPolicyChangedEvent());
return true;
}
@Override
public DiskMonitorPolicy getCurrentPolicy() {
return policyMapper.selectOne(new QueryWrapper<DiskMonitorPolicy>().orderByAsc("id").last("LIMIT 1"));
}
@Override
@Transactional(rollbackFor = Exception.class)
public DiskMonitorPolicy getOrCreatePolicy() {
DiskMonitorPolicy policy = getCurrentPolicy();
if (policy != null) {
return policy;
}
policy = new DiskMonitorPolicy();
policy.setPolicyName(DiskMonitorConstant.DEFAULT_POLICY_NAME);
policy.setMonitorEnabled(1);
policy.setRunOnAppStart(1);
policy.setDailyRunTime(parseTime(DiskMonitorConstant.DEFAULT_DAILY_RUN_TIME));
policy.setWarningNotifyMode(DiskMonitorConstant.WARNING_NOTIFY_MODE_STATUS_CHANGE);
policy.setAlarmNotifyMode(DiskMonitorConstant.ALARM_NOTIFY_MODE_EVERY_TIME);
policyMapper.insert(policy);
return policy;
}
@Override
public List<DiskMonitorTarget> listEnabledTargets() {
return targetMapper.selectList(new QueryWrapper<DiskMonitorTarget>()
.eq("monitor_enabled", 1)
.orderByAsc("drive_letter", "id"));
}
@Override
public DiskMonitorTarget getTargetByDriveLetter(String driveLetter) {
return targetMapper.selectOne(new QueryWrapper<DiskMonitorTarget>()
.eq("drive_letter", normalizeDriveLetter(driveLetter))
.last("LIMIT 1"));
}
private List<DiskMonitorTarget> listTargetsByPolicyId(Long policyId) {
return targetMapper.selectList(new QueryWrapper<DiskMonitorTarget>()
.eq("policy_id", policyId)
.orderByAsc("drive_letter", "id"));
}
private void fillTarget(DiskMonitorTarget target, DiskMonitorParam.TargetParam targetParam, String driveLetter) {
target.setDriveLetter(driveLetter);
target.setMonitorEnabled(boolToInt(targetParam.getMonitorEnabled(), true));
target.setWarningUsagePercent(targetParam.getWarningUsagePercent());
target.setAlarmUsagePercent(targetParam.getAlarmUsagePercent());
target.setNotifyPathEnabled(boolToInt(targetParam.getNotifyPathEnabled(), false));
target.setNotifyPathListJson(writeValueAsJson(targetParam.getNotifyPathList()));
target.setNotifyHttpEnabled(boolToInt(targetParam.getNotifyHttpEnabled(), false));
target.setNotifyHttpListJson(writeValueAsJson(targetParam.getNotifyHttpList()));
target.setRemark(targetParam.getRemark());
}
private void validateSaveParam(DiskMonitorParam.PolicySaveParam param) {
DiskMonitorParam.PolicyParam policyParam = param.getPolicy();
parseTime(policyParam.getDailyRunTime());
resolveWarningNotifyMode(policyParam.getWarningNotifyMode());
resolveAlarmNotifyMode(policyParam.getAlarmNotifyMode());
Map<String, Boolean> driveMap = new LinkedHashMap<>();
for (DiskMonitorParam.TargetParam targetParam : param.getTargets()) {
String driveLetter = normalizeDriveLetter(targetParam.getDriveLetter());
if (driveMap.put(driveLetter, Boolean.TRUE) != null) {
throw new BusinessException(DiskMonitorResponseEnum.DRIVE_LETTER_REPEAT);
}
if (targetParam.getAlarmUsagePercent() < targetParam.getWarningUsagePercent()) {
throw new BusinessException(DiskMonitorResponseEnum.USAGE_PERCENT_INVALID);
}
if (Boolean.TRUE.equals(targetParam.getNotifyPathEnabled())
&& (targetParam.getNotifyPathList() == null || targetParam.getNotifyPathList().isEmpty())) {
throw new BusinessException(DiskMonitorResponseEnum.NOTIFY_PATH_EMPTY);
}
if (Boolean.TRUE.equals(targetParam.getNotifyHttpEnabled())
&& (targetParam.getNotifyHttpList() == null || targetParam.getNotifyHttpList().isEmpty())) {
throw new BusinessException(DiskMonitorResponseEnum.NOTIFY_HTTP_EMPTY);
}
if (targetParam.getNotifyPathList() != null) {
for (DiskMonitorNotifyPathItem item : targetParam.getNotifyPathList()) {
if (StrUtil.isBlank(item.getPath())) {
throw new BusinessException(DiskMonitorResponseEnum.NOTIFY_PATH_VALUE_EMPTY);
}
}
}
if (targetParam.getNotifyHttpList() != null) {
for (DiskMonitorNotifyHttpItem item : targetParam.getNotifyHttpList()) {
validateHttpUrl(item.getUrl());
}
}
}
}
private String resolveWarningNotifyMode(String mode) {
String value = StrUtil.blankToDefault(mode, DiskMonitorConstant.WARNING_NOTIFY_MODE_STATUS_CHANGE).trim().toUpperCase(Locale.ROOT);
if (!DiskMonitorConstant.WARNING_NOTIFY_MODE_STATUS_CHANGE.equals(value)) {
throw new BusinessException(DiskMonitorResponseEnum.POLICY_MODE_INVALID);
}
return value;
}
private String resolveAlarmNotifyMode(String mode) {
String value = StrUtil.blankToDefault(mode, DiskMonitorConstant.ALARM_NOTIFY_MODE_EVERY_TIME).trim().toUpperCase(Locale.ROOT);
if (!DiskMonitorConstant.ALARM_NOTIFY_MODE_EVERY_TIME.equals(value)) {
throw new BusinessException(DiskMonitorResponseEnum.POLICY_MODE_INVALID);
}
return value;
}
private void validateHttpUrl(String url) {
try {
URL httpUrl = new URL(url);
httpUrl.toURI();
String protocol = httpUrl.getProtocol();
if (!"http".equalsIgnoreCase(protocol) && !"https".equalsIgnoreCase(protocol)) {
throw new BusinessException(DiskMonitorResponseEnum.NOTIFY_HTTP_URL_INVALID);
}
} catch (Exception exception) {
throw new BusinessException(DiskMonitorResponseEnum.NOTIFY_HTTP_URL_INVALID);
}
}
private String normalizeDriveLetter(String driveLetter) {
if (StrUtil.isBlank(driveLetter)) {
throw new BusinessException(DiskMonitorResponseEnum.DRIVE_LETTER_FORMAT_ERROR);
}
String normalized = driveLetter.trim().toUpperCase(Locale.ROOT);
if (!normalized.matches("^[A-Z]:$")) {
throw new BusinessException(DiskMonitorResponseEnum.DRIVE_LETTER_FORMAT_ERROR);
}
return normalized;
}
private LocalTime parseTime(String time) {
try {
return LocalTime.parse(time, TIME_FORMATTER);
} catch (DateTimeParseException exception) {
throw new BusinessException(DiskMonitorResponseEnum.DAILY_RUN_TIME_FORMAT_ERROR);
}
}
private int boolToInt(Boolean value, boolean defaultValue) {
return Boolean.TRUE.equals(value == null ? defaultValue : value) ? 1 : 0;
}
private boolean isEnabled(Integer value) {
return value != null && value == 1;
}
private String writeValueAsJson(Object value) {
if (value == null) {
return null;
}
try {
return objectMapper.writeValueAsString(value);
} catch (JsonProcessingException exception) {
throw new BusinessException(CommonResponseEnum.JSON_CONVERT_EXCEPTION, exception.getMessage());
}
}
private List<DiskMonitorNotifyPathItem> parsePathItems(String json) {
return readJsonList(json, PATH_LIST_TYPE);
}
private List<DiskMonitorNotifyHttpItem> parseHttpItems(String json) {
return readJsonList(json, HTTP_LIST_TYPE);
}
private <T> List<T> readJsonList(String json, TypeReference<List<T>> typeReference) {
if (StrUtil.isBlank(json)) {
return new ArrayList<>();
}
try {
List<T> result = objectMapper.readValue(json, typeReference);
return result == null ? new ArrayList<>() : result;
} catch (Exception exception) {
throw new BusinessException(CommonResponseEnum.JSON_CONVERT_EXCEPTION, exception.getMessage());
}
}
private DiskMonitorVO.PolicyVO toPolicyVO(DiskMonitorPolicy policy) {
DiskMonitorVO.PolicyVO policyVO = new DiskMonitorVO.PolicyVO();
policyVO.setId(policy.getId());
policyVO.setPolicyName(policy.getPolicyName());
policyVO.setMonitorEnabled(isEnabled(policy.getMonitorEnabled()));
policyVO.setRunOnAppStart(isEnabled(policy.getRunOnAppStart()));
policyVO.setDailyRunTime(policy.getDailyRunTime() == null ? null : policy.getDailyRunTime().format(TIME_FORMATTER));
policyVO.setWarningNotifyMode(policy.getWarningNotifyMode());
policyVO.setAlarmNotifyMode(policy.getAlarmNotifyMode());
policyVO.setLastJobId(policy.getLastJobId());
policyVO.setRemark(policy.getRemark());
return policyVO;
}
private DiskMonitorVO.TargetVO toTargetVO(DiskMonitorTarget target) {
DiskMonitorVO.TargetVO targetVO = new DiskMonitorVO.TargetVO();
targetVO.setId(target.getId());
targetVO.setDriveLetter(target.getDriveLetter());
targetVO.setMonitorEnabled(isEnabled(target.getMonitorEnabled()));
targetVO.setWarningUsagePercent(target.getWarningUsagePercent());
targetVO.setAlarmUsagePercent(target.getAlarmUsagePercent());
targetVO.setNotifyPathEnabled(isEnabled(target.getNotifyPathEnabled()));
targetVO.setNotifyPathList(parsePathItems(target.getNotifyPathListJson()));
targetVO.setNotifyHttpEnabled(isEnabled(target.getNotifyHttpEnabled()));
targetVO.setNotifyHttpList(parseHttpItems(target.getNotifyHttpListJson()));
targetVO.setLastStatus(target.getLastStatus());
targetVO.setLastScanTime(formatDateTime(target.getLastScanTime()));
targetVO.setLastUsedPercent(target.getLastUsedPercent());
targetVO.setRemark(target.getRemark());
return targetVO;
}
private String formatDateTime(LocalDateTime dateTime) {
return dateTime == null ? null : dateTime.format(DATE_TIME_FORMATTER);
}
}

View File

@@ -0,0 +1,104 @@
CREATE TABLE IF NOT EXISTS `disk_monitor_policy` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`policy_name` VARCHAR(100) NOT NULL DEFAULT '默认磁盘监控策略' COMMENT '策略名称',
`monitor_enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用监控0否 1是',
`run_on_app_start` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '应用启动后是否执行一次0否 1是',
`daily_run_time` TIME NOT NULL COMMENT '每日统一执行时间',
`warning_notify_mode` VARCHAR(32) NOT NULL DEFAULT 'STATUS_CHANGE' COMMENT '预警通知模式',
`alarm_notify_mode` VARCHAR(32) NOT NULL DEFAULT 'EVERY_TIME' COMMENT '告警通知模式',
`last_job_id` BIGINT NULL COMMENT '最近一次任务ID',
`remark` VARCHAR(500) NULL COMMENT '备注',
`created_by` VARCHAR(64) NULL COMMENT '创建人',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_by` VARCHAR(64) NULL COMMENT '更新人',
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='磁盘监控全局策略表';
CREATE TABLE IF NOT EXISTS `disk_monitor_target` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`policy_id` BIGINT NOT NULL COMMENT '策略ID',
`drive_letter` VARCHAR(10) NOT NULL COMMENT '盘符,例如 C:',
`monitor_enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用监控0否 1是',
`warning_usage_percent` TINYINT UNSIGNED NOT NULL COMMENT '预警使用率阈值',
`alarm_usage_percent` TINYINT UNSIGNED NOT NULL COMMENT '告警使用率阈值',
`notify_path_enabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否启用路径通知0否 1是',
`notify_path_list_json` JSON NULL COMMENT '路径通知目标列表JSON',
`notify_http_enabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否启用HTTP通知0否 1是',
`notify_http_list_json` JSON NULL COMMENT 'HTTP通知目标列表JSON',
`last_status` VARCHAR(32) NOT NULL DEFAULT 'UNKNOWN' COMMENT '最近一次状态',
`last_scan_time` DATETIME NULL COMMENT '最近扫描时间',
`last_used_percent` DECIMAL(5,2) NULL COMMENT '最近一次使用率',
`remark` VARCHAR(500) NULL COMMENT '备注',
`created_by` VARCHAR(64) NULL COMMENT '创建人',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_by` VARCHAR(64) NULL COMMENT '更新人',
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_drive_letter` (`drive_letter`),
KEY `idx_policy_id` (`policy_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='磁盘监控盘符配置表';
CREATE TABLE IF NOT EXISTS `disk_monitor_job` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`job_no` VARCHAR(64) NOT NULL COMMENT '任务编号',
`job_source` VARCHAR(32) NOT NULL COMMENT '任务来源',
`planned_time` DATETIME NULL COMMENT '计划执行时间',
`started_at` DATETIME NOT NULL COMMENT '开始时间',
`finished_at` DATETIME NULL COMMENT '结束时间',
`job_status` VARCHAR(32) NOT NULL COMMENT '任务状态',
`target_count` INT NOT NULL DEFAULT 0 COMMENT '计划扫描盘符数量',
`success_count` INT NOT NULL DEFAULT 0 COMMENT '成功扫描数量',
`warning_count` INT NOT NULL DEFAULT 0 COMMENT '预警数量',
`alarm_count` INT NOT NULL DEFAULT 0 COMMENT '告警数量',
`message` VARCHAR(1000) NULL COMMENT '结果说明',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_job_no` (`job_no`),
KEY `idx_job_source` (`job_source`),
KEY `idx_started_at` (`started_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='磁盘监控任务批次表';
CREATE TABLE IF NOT EXISTS `disk_monitor_result` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`job_id` BIGINT NOT NULL COMMENT '任务ID',
`target_id` BIGINT NOT NULL COMMENT '盘符配置ID',
`drive_letter` VARCHAR(10) NOT NULL COMMENT '盘符',
`total_bytes` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '总容量字节数',
`used_bytes` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '已使用字节数',
`free_bytes` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '剩余字节数',
`used_percent` DECIMAL(5,2) NOT NULL DEFAULT 0.00 COMMENT '使用率',
`current_status` VARCHAR(32) NOT NULL COMMENT '当前状态',
`previous_status` VARCHAR(32) NOT NULL DEFAULT 'UNKNOWN' COMMENT '上一次状态',
`status_changed` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '状态是否变化0否 1是',
`should_notify` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '本次是否通知0否 1是',
`notify_reason` VARCHAR(32) NOT NULL DEFAULT 'NO_NOTIFY' COMMENT '通知原因',
`scan_time` DATETIME NOT NULL COMMENT '扫描时间',
`message` VARCHAR(1000) NULL COMMENT '扫描说明',
PRIMARY KEY (`id`),
KEY `idx_job_id` (`job_id`),
KEY `idx_target_id` (`target_id`),
KEY `idx_drive_letter` (`drive_letter`),
KEY `idx_scan_time` (`scan_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='磁盘监控结果表';
CREATE TABLE IF NOT EXISTS `disk_monitor_notify_log` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`job_id` BIGINT NOT NULL COMMENT '任务ID',
`result_id` BIGINT NOT NULL COMMENT '结果ID',
`target_id` BIGINT NOT NULL COMMENT '盘符配置ID',
`drive_letter` VARCHAR(10) NOT NULL COMMENT '盘符',
`notify_level` VARCHAR(32) NOT NULL COMMENT '通知级别',
`channel_type` VARCHAR(32) NOT NULL COMMENT '通知通道类型',
`channel_target` VARCHAR(1000) NOT NULL COMMENT '通知目标',
`notify_title` VARCHAR(255) NOT NULL COMMENT '通知标题',
`notify_content` TEXT NOT NULL COMMENT '通知内容',
`send_status` VARCHAR(32) NOT NULL COMMENT '发送状态',
`response_message` VARCHAR(2000) NULL COMMENT '响应结果或异常信息',
`sent_at` DATETIME NOT NULL COMMENT '发送时间',
PRIMARY KEY (`id`),
KEY `idx_job_id` (`job_id`),
KEY `idx_result_id` (`result_id`),
KEY `idx_target_id` (`target_id`),
KEY `idx_sent_at` (`sent_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='磁盘监控通知日志表';

22
systemmonitor/pom.xml Normal file
View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.njcn.gather</groupId>
<artifactId>CN_Tool</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>systemmonitor</artifactId>
<packaging>pom</packaging>
<name>systemmonitor</name>
<description>System monitor capability aggregator.</description>
<modules>
<module>disk-monitor</module>
</modules>
</project>

View File

@@ -0,0 +1,371 @@
# getIcdMmsJson 标准 API 调试文档
## 1. 文档范围
本文档用于说明 `mms-mapping` 模块统一调试接口 `getIcdMmsJson` 的标准调用方式、请求结构、响应规则和联调注意事项。
本文档内容以当前源码为准,主要对照以下实现:
- `tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/controller/MappingController.java`
- `tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/MappingRequestConverter.java`
- `tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/MappingResponseConverter.java`
- `tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/service/impl/MappingTaskServiceImpl.java`
- `tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/MappingGenerationService.java`
- `tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/param/GenerateMappingFromIcdRequest.java`
- `tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/vo/MappingTaskResponse.java`
说明:
- 本文档仅描述接口契约和调试方式,不改动业务代码。
- 本次未执行 `mvn` 编译、打包或真实接口联调。
- 如文档与运行结果冲突,以源码和实际部署配置为准。
## 2. 接口基本信息
| 项 | 说明 |
| --- | --- |
| 接口名称 | `getIcdMmsJson` |
| 请求方法 | `POST` |
| 请求路径 | `/api/mms-mapping/get-icd-mms-json` |
| Content-Type | `multipart/form-data` |
| 控制器入口 | `MappingController#getIcdMmsJson` |
| 请求组成 | `icdFile` 文件 Part + `request` JSON Part |
| 正常业务响应体 | `MappingTaskResponse` |
## 3. 接口职责
该接口是 `mms-mapping` 模块的统一调试入口,串联以下两个阶段:
1. 上传 ICD 文件并完成解析,生成 `icdDocument``indexCandidates`
2. 根据 `request.indexSelection` 判断是否继续生成正式 `mappingJson`
接口行为分为三种典型结果:
1. `request.indexSelection` 未传或为空
返回 `NEED_INDEX_SELECTION`,用于引导前端或调试人员先确认标签与 `lnInst` 的绑定关系。
2. `request.indexSelection` 已传但校验不通过
返回 `NEED_INDEX_SELECTION`,同时通过 `problems` 给出不合法原因,要求重新选择。
3. `request.indexSelection` 校验通过
返回 `SUCCESS`,输出正式 `mappingJson`,必要时同时落盘并返回 `savedPath`
补充说明:
- 该接口每次都会重新解析上传的 ICD 文件,因此第二次调试仍然必须重新上传 ICD 文件。
- 该接口正常进入业务编排后,返回体类型为 `MappingTaskResponse`
- 如果异常发生在控制器参数绑定或请求转换阶段例如文件为空、Part 缺失、JSON Part 解析失败,则由全局异常处理器统一包装为 `HttpResult<String>`,而不是 `MappingTaskResponse`
## 4. 请求规范
### 4.1 multipart/form-data Part 说明
| Part 名称 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `icdFile` | File | 是 | ICD 文件,不能为空 |
| `request` | JSON Part | 是 | 生成参数,必须按 `application/json` 发送 |
说明:
- `request` Part 不能省略。即使第一次只想拿候选结果,也必须传一个最小 JSON。
- `request.indexSelection` 可以省略或传空数组,此时接口只返回候选结果,不生成正式映射。
### 4.2 request JSON 结构
```json
{
"version": "2026-04-22",
"author": "debug-user",
"saveToDisk": false,
"prettyJson": true,
"outputDir": "D:/temp/mms-output",
"indexSelection": [
{
"groupKey": "harm",
"groupDesc": "谐波数据",
"bindings": [
{
"reportName": "brcbStHarm",
"dataSetName": "dsStHarm",
"label": "A相",
"lnInst": "1"
}
]
}
]
}
```
### 4.3 request 字段说明
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `version` | String | 否 | 输出版本号。未传或空白时,后端按当天日期补齐,格式为 `yyyy-MM-dd` |
| `author` | String | 否 | 作者。未传或空白时,回退到配置项 `icd.mapping.default-author`,默认值为 `system` |
| `saveToDisk` | boolean | 否 | 是否将生成结果写入磁盘 |
| `prettyJson` | boolean | 否 | 是否输出格式化 JSON。`true` 为美化 JSON`false` 为紧凑 JSON |
| `outputDir` | String | 否 | 输出目录。未传或空白时,先回退到配置项 `icd.mapping.default-output-dir`;如果配置也为空,最终落到当前工作目录 |
| `indexSelection` | Array | 否 | 标签与 `lnInst` 的最终绑定关系。未传或为空时,只返回候选结果 |
### 4.4 indexSelection 字段说明
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `groupKey` | String | 是 | 分组唯一键,必须使用第一次响应里返回的原值 |
| `groupDesc` | String | 否 | 分组中文描述,便于调试查看 |
| `bindings` | Array | 是 | 当前业务分组下最终确认的绑定关系列表 |
### 4.5 bindings 字段说明
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `reportName` | String | 是 | 绑定发生在哪个报告上,例如 `brcbStHarm` |
| `dataSetName` | String | 是 | 绑定发生在哪个数据集上,例如 `dsStHarm` |
| `label` | String | 是 | 业务标签,例如 `A相``最大值``实时数据` |
| `lnInst` | String | 是 | 标签最终绑定到的逻辑节点实例值,例如 `1``2``3` |
## 5. 标准调试流程
### 5.1 第一次调试:只获取候选结果
用途:
- 上传 ICD 文件
- 获取 `icdDocument`
- 获取 `indexCandidates`
- 确认每个业务分组下可选的 `reportName``dataSetName``availableLnInstValues`
调用要求:
- `request` Part 仍然必须传
- `request.indexSelection` 可以不传,或传空数组
预期结果:
- `status = NEED_INDEX_SELECTION`
- 响应中返回 `icdDocument`
- 响应中返回 `indexCandidates`
### 5.2 第二次调试:带索引绑定生成正式结果
用途:
- 根据第一次返回的 `indexCandidates` 组装 `request.indexSelection`
- 再次上传同一个 ICD 文件
- 生成正式 `mappingJson`
调用要求:
- 必须继续上传 `icdFile`
- `groupKey` 必须沿用第一次返回值
- `reportName``dataSetName``lnInst` 必须与第一次返回的候选结果匹配
预期结果:
- `status = SUCCESS`
- 响应中返回 `mappingJson`
-`saveToDisk = true` 时,响应中额外返回 `savedPath`
### 5.3 第二次调试但绑定不合法
适用场景:
- `groupKey` 与候选结果不匹配
- `reportName``dataSetName` 不在候选集中
- `lnInst` 不在 `availableLnInstValues`
- 绑定关系缺失、不完整或结构错误
预期结果:
- `status = NEED_INDEX_SELECTION`
- 响应中仍然返回 `icdDocument``indexCandidates`
- `problems` 返回具体问题列表,要求重新确认绑定关系
## 6. 响应规范
### 6.1 正常业务响应体
接口正常进入业务编排后,统一返回 `MappingTaskResponse`。该对象使用了 `@JsonInclude(JsonInclude.Include.NON_EMPTY)`,空字段和空集合不会参与序列化。
基础字段说明:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `status` | Enum | 本次处理状态,可能为 `SUCCESS``NEED_INDEX_SELECTION``FAILED` |
| `message` | String | 状态说明或错误提示 |
| `icdDocument` | Object | 需要重新选择索引时返回的 ICD 解析结果 |
| `mappingJson` | String | 正式生成成功后的映射 JSON 文本 |
| `savedPath` | String | 结果已落盘时返回的绝对路径 |
| `indexCandidates` | Array | 待绑定状态下返回的索引候选分组 |
| `problems` | Array | 模板校验、候选分析或绑定校验问题 |
字段出现规则:
| 状态 | 必有字段 | 可能出现字段 |
| --- | --- | --- |
| `SUCCESS` | `status``message``mappingJson` | `savedPath``problems` |
| `NEED_INDEX_SELECTION` | `status``message``icdDocument``indexCandidates` | `problems` |
| `FAILED` | `status``message` | `problems` |
### 6.2 NEED_INDEX_SELECTION 响应示例
```json
{
"status": "NEED_INDEX_SELECTION",
"message": "索引配置缺失,请根据候选信息完成标签与数字索引的绑定后重新提交",
"icdDocument": {
"fileName": "demo.icd",
"iedName": "IED1",
"ldInst": "LD0",
"ldPrefix": "LD",
"logicalNodes": [
{
"lnInst": "1"
}
]
},
"indexCandidates": [
{
"groupKey": "harm",
"groupDesc": "谐波数据",
"reportCount": 1,
"templateLabels": [
"A相",
"B相",
"C相"
],
"reports": [
{
"reportName": "brcbStHarm",
"dataSetName": "dsStHarm",
"reportDesc": "谐波报告",
"availableLnInstValues": [
"1",
"2",
"3"
]
}
]
}
]
}
```
说明:
- `icdDocument` 实际字段可能比示例更多。
- 如果本次是“索引配置不合法”而不是“索引配置缺失”,通常还会返回 `problems`
### 6.3 SUCCESS 响应示例
```json
{
"status": "SUCCESS",
"message": "映射生成成功",
"mappingJson": "{\n \"version\": \"2026-04-22\",\n \"author\": \"debug-user\",\n \"ied\": \"IED1\",\n \"ld\": \"LD\",\n \"instList\": []\n}"
}
```
说明:
- `mappingJson` 是字符串字段,字段值本身是一段 JSON 文本。
-`saveToDisk = true` 时,响应中还会额外返回 `savedPath`
### 6.4 FAILED 响应示例
```json
{
"status": "FAILED",
"message": "映射生成失败:加载 DefaultCfg.txt 失败默认模板文件不存在template/DefaultCfg.txt",
"problems": [
"加载 DefaultCfg.txt 失败默认模板文件不存在template/DefaultCfg.txt"
]
}
```
说明:
- `FAILED` 主要对应服务编排阶段捕获到的运行异常,例如 ICD 解析、模板加载、映射生成、序列化或落盘失败。
- 并非所有错误都会进入 `FAILED`。如果异常发生在控制器参数绑定或请求转换阶段,会走全局异常处理器,而不是这里的业务响应结构。
## 7. 全局异常响应说明
以下场景通常不会返回 `MappingTaskResponse`,而是由 `GlobalBusinessExceptionHandler` 统一包装:
- `icdFile` 缺失或为空
- `request` Part 缺失
- `request` Part 的 `Content-Type` 不是 `application/json`
- `multipart/form-data` 结构不合法
- JSON 反序列化失败或框架参数绑定失败
这类异常最终会包装为统一的 `HttpResult<String>` 响应,具体字段结构以全局公共响应定义为准,本文不展开其完整协议,只强调:
- 不能把这类错误等同理解为 `MappingTaskResponse.status = FAILED`
- 联调时应先区分“业务响应体”与“全局异常包装”
## 8. 调试示例
### 8.1 curl 示例:第一次调用,只获取候选结果
```powershell
curl.exe -X POST "http://localhost:8080/api/mms-mapping/get-icd-mms-json" `
-H "Accept: application/json" `
-F 'icdFile=@D:/data/demo.icd' `
-F 'request={"prettyJson":true,"saveToDisk":false};type=application/json'
```
### 8.2 curl 示例:第二次调用,带索引绑定直接生成 MMS JSON
```powershell
curl.exe -X POST "http://localhost:8080/api/mms-mapping/get-icd-mms-json" `
-H "Accept: application/json" `
-F 'icdFile=@D:/data/demo.icd' `
-F 'request={"version":"2026-04-22","author":"debug-user","prettyJson":true,"saveToDisk":false,"indexSelection":[{"groupKey":"harm","groupDesc":"谐波数据","bindings":[{"reportName":"brcbStHarm","dataSetName":"dsStHarm","label":"A相","lnInst":"1"},{"reportName":"brcbStHarm","dataSetName":"dsStHarm","label":"B相","lnInst":"2"},{"reportName":"brcbStHarm","dataSetName":"dsStHarm","label":"C相","lnInst":"3"}]}]};type=application/json'
```
## 9. Postman 调试要点
1. `Body` 选择 `form-data`
2. `icdFile` 类型选择 `File`
3. `request` 保持文本输入,但该 Part 的 `Content-Type` 必须显式设置为 `application/json`
4. 第一次调试不要省略 `request` Part只是不传 `indexSelection`
5. 第二次调试时必须继续上传 ICD 文件,并严格按第一次返回的候选结果组装绑定关系
## 10. 常见问题
### 10.1 为什么第一次调试也必须传 `request`
因为控制器方法签名使用的是 `@RequestPart("request") GenerateMappingFromIcdRequest request`,该 Part 本身就是必填参数。第一次调试可以只传最小 JSON但不能完全省略。
### 10.2 为什么没有传 `indexSelection`,却没有返回 `FAILED`
这是接口设计的正常行为。`indexSelection` 缺失或为空时,业务语义不是“接口执行失败”,而是“还需要前端继续确认索引绑定”,因此返回的是 `NEED_INDEX_SELECTION`
### 10.3 `saveToDisk=true` 但没有传 `outputDir`,结果会保存到哪里
处理顺序如下:
1. 先读取请求中的 `outputDir`
2. 如果请求空白,则回退到配置项 `icd.mapping.default-output-dir`
3. 如果配置项也为空,则最终落到当前工作目录
### 10.4 `version` 不传时会变成什么
后端在正式生成映射文档时,会把空白 `version` 自动补成当天日期,格式为 `yyyy-MM-dd`
### 10.5 `mappingJson` 为什么是字符串,不是嵌套对象
因为当前响应结构中 `mappingJson` 定义为 `String`,接口返回的是一段已经序列化好的 JSON 文本,而不是再次展开后的对象结构。
### 10.6 什么情况下会返回 `problems`
`problems` 主要用于承载以下问题:
- 默认模板校验问题
- 索引候选分析问题
- `indexSelection` 绑定校验问题
- 服务编排阶段捕获到的异常原因
## 11. 当前边界
- 当前文档仅覆盖 `getIcdMmsJson` 接口,不覆盖 `get-icd``get-mms-json` 的独立接口文档
- 当前文档重点描述业务返回体与调试方式,不展开全局 `HttpResult` 的完整协议
- 示例中的 `icdDocument``indexCandidates``mappingJson` 为结构化示意,实际字段数量与内容以运行结果为准

221
tools/mms-mapping/README.md Normal file
View File

@@ -0,0 +1,221 @@
# mms-mapping
`mms-mapping` 模块负责解析 ICD 文件并生成 MMS 映射数据。当前统一调试入口为 `getIcdMmsJson`,该接口同时覆盖“先解析 ICD 获取索引候选”和“确认索引后生成正式 MMS JSON”两类场景。
## 1. 接口信息
- 路径:`POST /api/mms-mapping/get-icd-mms-json`
- Content-Type`multipart/form-data`
- 控制器:`MappingController#getIcdMmsJson`
- 说明:上传 ICD 文件后,后端先解析 ICD再根据 `request.indexSelection` 决定返回候选结果还是正式映射结果
说明:
- `request.indexSelection` 为空时,接口返回 `NEED_INDEX_SELECTION`,用于指导前端或调试人员完成标签与 `lnInst` 绑定。
- `request.indexSelection` 有效时,接口返回 `SUCCESS`,直接给出 `mappingJson`,必要时同时落盘。
- 该接口每次都会重新解析上传的 ICD 文件,因此二次调试时仍需要重新上传同一个 ICD 文件。
## 2. 调试流程
### 2.1 第一次调试
用途:只上传 ICD先拿到 `icdDocument``indexCandidates`
预期结果:
- `status = NEED_INDEX_SELECTION`
- 返回 `icdDocument`
- 返回 `indexCandidates`
### 2.2 第二次调试
用途:根据第一次返回的 `indexCandidates` 组装 `request.indexSelection`,再次调用同一个接口生成正式 MMS JSON。
预期结果:
- `status = SUCCESS`
- 返回 `mappingJson`
-`saveToDisk=true` 时返回 `savedPath`
## 3. 请求参数
### 3.1 form-data 参数
| 参数名 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `icdFile` | File | 是 | ICD 文件 |
| `request` | JSON Part | 是 | 生成参数,必须按 `application/json` 发送 |
### 3.2 request JSON 结构
```json
{
"version": "20260421",
"author": "debug-user",
"saveToDisk": false,
"prettyJson": true,
"outputDir": "D:/temp/mms-output",
"indexSelection": [
{
"groupKey": "harm",
"groupDesc": "谐波数据",
"bindings": [
{
"reportName": "brcbStHarm",
"dataSetName": "dsStHarm",
"label": "A相",
"lnInst": "1"
}
]
}
]
}
```
### 3.3 request 字段说明
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `version` | String | 否 | 映射版本号;为空时由后端按当前日期补齐 |
| `author` | String | 否 | 作者;为空时使用模块默认作者 |
| `saveToDisk` | boolean | 否 | 是否将生成结果写入磁盘 |
| `prettyJson` | boolean | 否 | 是否生成格式化 JSON |
| `outputDir` | String | 否 | 输出目录;仅 `saveToDisk=true` 时生效 |
| `indexSelection` | Array | 否 | 标签与 `lnInst` 的最终绑定关系;为空时只返回候选结果 |
### 3.4 indexSelection 字段说明
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `groupKey` | String | 是 | 分组唯一键,必须使用第一次响应里返回的原值 |
| `groupDesc` | String | 否 | 分组描述,便于调试查看 |
| `bindings` | Array | 是 | 当前分组下最终确认的绑定列表 |
### 3.5 bindings 字段说明
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `reportName` | String | 是 | 报告名称 |
| `dataSetName` | String | 是 | 数据集名称 |
| `label` | String | 是 | 模板标签 |
| `lnInst` | String | 是 | 实际绑定的逻辑节点实例值 |
## 4. 调试示例
### 4.1 第一次调用,只获取候选结果
```powershell
curl.exe -X POST "http://localhost:8080/api/mms-mapping/get-icd-mms-json" `
-H "Accept: application/json" `
-F 'icdFile=@D:/data/demo.icd' `
-F 'request={"version":"20260421","author":"debug-user","prettyJson":true,"saveToDisk":false};type=application/json'
```
### 4.2 第二次调用,带索引绑定直接生成 MMS JSON
```powershell
curl.exe -X POST "http://localhost:8080/api/mms-mapping/get-icd-mms-json" `
-H "Accept: application/json" `
-F 'icdFile=@D:/data/demo.icd' `
-F 'request={"version":"20260421","author":"debug-user","prettyJson":true,"saveToDisk":false,"indexSelection":[{"groupKey":"harm","groupDesc":"谐波数据","bindings":[{"reportName":"brcbStHarm","dataSetName":"dsStHarm","label":"A相","lnInst":"1"},{"reportName":"brcbStHarm","dataSetName":"dsStHarm","label":"B相","lnInst":"2"},{"reportName":"brcbStHarm","dataSetName":"dsStHarm","label":"C相","lnInst":"3"}]}]};type=application/json'
```
## 5. 响应说明
接口统一返回 `MappingTaskResponse`。由于响应对象使用了 `@JsonInclude(JsonInclude.Include.NON_EMPTY)`,空字段和空集合默认不会出现在最终 JSON 中。
### 5.1 NEED_INDEX_SELECTION
适用场景:未传 `indexSelection`,或绑定关系不完整、不合法。
```json
{
"status": "NEED_INDEX_SELECTION",
"message": "索引配置缺失,请根据候选信息完成标签与数字索引的绑定后重新提交",
"icdDocument": {
"fileName": "demo.icd",
"iedName": "IED1",
"ldInst": "LD0",
"ldPrefix": "LD",
"logicalNodes": [
{
"lnInst": "1"
}
]
},
"indexCandidates": [
{
"groupKey": "harm",
"groupDesc": "谐波数据",
"reportCount": 1,
"templateLabels": [
"A相",
"B相",
"C相"
],
"reports": [
{
"reportName": "brcbStHarm",
"dataSetName": "dsStHarm",
"reportDesc": "谐波报告",
"availableLnInstValues": [
"1",
"2",
"3"
]
}
]
}
]
}
```
说明:
- `icdDocument` 实际返回内容会比示例更大,这里只保留关键字段用于说明结构。
- 如果绑定值非法,还会额外返回 `problems`,提示缺失或不匹配的绑定项。
### 5.2 SUCCESS
适用场景:`indexSelection` 校验通过,映射成功生成。
```json
{
"status": "SUCCESS",
"message": "映射生成成功",
"mappingJson": "{\n \"ied\": \"IED1\",\n \"ld\": \"LD\",\n \"instList\": []\n}"
}
```
说明:
- `mappingJson` 是字符串字段,字段值本身也是一段 JSON 文本。
-`saveToDisk=true` 时,响应中还会返回 `savedPath`
### 5.3 FAILED
适用场景ICD 解析失败、模板校验失败、文件读取失败或其他运行异常。
```json
{
"status": "FAILED",
"message": "映射生成失败ICD 文件不能为空",
"problems": [
"ICD 文件不能为空"
]
}
```
## 6. Postman 调试注意事项
- `Body` 选择 `form-data`
- `icdFile` 类型选择 `File`
- `request` 类型保持文本,但该 Part 的 `Content-Type` 需要设置为 `application/json`
- 第一次调试建议不要传 `indexSelection`,先观察 `indexCandidates``availableLnInstValues`
- 第二次调试时必须继续上传 ICD 文件,并按第一次返回的 `groupKey``reportName``dataSetName``availableLnInstValues` 组装绑定关系。
## 7. 当前限制
- 当前仅补充调试文档,未改动 `mms-mapping` 业务代码。
- 当前未执行 `mvn` 编译、打包或接口联调验证。
- 示例响应中的 `icdDocument``mappingJson` 为结构化示意,实际字段数量以运行结果为准。

View File

@@ -1,135 +0,0 @@
package com.njcn.gather.icd.mapping.application;
import com.njcn.gather.icd.mapping.application.command.GenerateFromIcdCommand;
import com.njcn.gather.icd.mapping.application.result.GenerateMappingResult;
import com.njcn.gather.icd.mapping.domain.model.analysis.IndexAnalysis;
import com.njcn.gather.icd.mapping.domain.model.analysis.ValidationResult;
import com.njcn.gather.icd.mapping.domain.model.icd.IcdDocument;
import com.njcn.gather.icd.mapping.domain.model.mapping.MappingDocument;
import com.njcn.gather.icd.mapping.domain.model.template.DefaultTemplate;
import com.njcn.gather.icd.mapping.domain.service.DefaultTemplateLoader;
import com.njcn.gather.icd.mapping.domain.service.IcdParserService;
import com.njcn.gather.icd.mapping.domain.service.IndexAnalysisService;
import com.njcn.gather.icd.mapping.domain.service.IndexValidationService;
import com.njcn.gather.icd.mapping.domain.service.MappingGenerationService;
import com.njcn.gather.icd.mapping.enums.GenerateStatus;
import com.njcn.gather.icd.mapping.infrastructure.serializer.MappingDocumentSerializer;
import com.njcn.gather.icd.mapping.infrastructure.storage.FileStorageService;
import org.springframework.stereotype.Service;
/**
* 生成任务应用服务。
*
* 完整流程:
* 1. 解析 ICD
* 2. 读取 DefaultCfg.txt
* 3. 按业务分组生成候选项;
* 4. 如果用户未提交绑定关系,返回 NEED_INDEX_SELECTION
* 5. 如果提交了绑定关系,先做合法性校验;
* 6. 校验通过后生成正式 MappingDocument
* 7. 序列化并按需落盘。
*/
@Service
public class MappingTaskAppService {
private final IcdParserService icdParserService;
private final DefaultTemplateLoader defaultTemplateLoader;
private final IndexAnalysisService indexAnalysisService;
private final IndexValidationService indexValidationService;
private final MappingGenerationService mappingGenerationService;
private final MappingDocumentSerializer mappingDocumentSerializer;
private final FileStorageService fileStorageService;
public MappingTaskAppService(IcdParserService icdParserService,
DefaultTemplateLoader defaultTemplateLoader,
IndexAnalysisService indexAnalysisService,
IndexValidationService indexValidationService,
MappingGenerationService mappingGenerationService,
MappingDocumentSerializer mappingDocumentSerializer,
FileStorageService fileStorageService) {
this.icdParserService = icdParserService;
this.defaultTemplateLoader = defaultTemplateLoader;
this.indexAnalysisService = indexAnalysisService;
this.indexValidationService = indexValidationService;
this.mappingGenerationService = mappingGenerationService;
this.mappingDocumentSerializer = mappingDocumentSerializer;
this.fileStorageService = fileStorageService;
}
public GenerateMappingResult generateFromIcd(GenerateFromIcdCommand command) {
GenerateMappingResult result = new GenerateMappingResult();
try {
// 1. 解析 ICD
IcdDocument icdDocument = icdParserService.parse(command.getFileBytes(), command.getFileName());
result.setIedName(icdDocument.getIedName());
result.setLdInst(icdDocument.getLdInst());
// 2. 加载 DefaultCfg.txt
DefaultTemplate template = defaultTemplateLoader.load();
result.getProblems().addAll(template.verify());
// 3. 分析索引候选
IndexAnalysis indexAnalysis = indexAnalysisService.analyze(icdDocument, template);
result.setIndexAnalysis(indexAnalysis);
result.getProblems().addAll(indexAnalysis.getProblems());
// 4. 如果没有提交任何绑定关系,则直接返回待匹配项
if (command.getIndexSelection() == null || command.getIndexSelection().isEmpty()) {
result.setStatus(GenerateStatus.NEED_INDEX_SELECTION);
result.setMessage("索引配置缺失或不合法,请根据候选信息完成标签与数字索引的绑定后重新提交");
return result;
}
// 5. 校验用户提交的绑定关系
ValidationResult validationResult = indexValidationService.validate(indexAnalysis, command.getIndexSelection());
if (!validationResult.isValid()) {
result.setStatus(GenerateStatus.NEED_INDEX_SELECTION);
result.setMessage("索引配置缺失或不合法,请根据候选信息完成标签与数字索引的绑定后重新提交");
result.getProblems().addAll(validationResult.getProblems());
return result;
}
// 6. 生成正式映射结构
MappingDocument mappingDocument = mappingGenerationService.generate(
icdDocument,
template,
indexAnalysis,
command.getIndexSelection(),
command.getVersion(),
command.getAuthor()
);
result.setMappingDocument(mappingDocument);
// 7. 序列化输出
String mappingJson = command.isPrettyJson()
? mappingDocumentSerializer.toPrettyJson(mappingDocument)
: mappingDocumentSerializer.toCompactJson(mappingDocument);
result.setMappingJson(mappingJson);
if (command.isSaveToDisk()) {
String fileName = buildOutputFileName(icdDocument, command.isPrettyJson());
String savedPath = fileStorageService.save(fileName, mappingJson, command.getOutputDir());
result.setSavedPath(savedPath);
}
result.setStatus(GenerateStatus.SUCCESS);
result.setMessage("映射生成成功");
return result;
} catch (Exception ex) {
result.setStatus(GenerateStatus.FAILED);
result.setMessage("映射生成失败:" + ex.getMessage());
result.getProblems().add(ex.getMessage());
return result;
}
}
private String buildOutputFileName(IcdDocument icdDocument, boolean prettyJson) {
String baseName = icdDocument.getIedName() == null ? "mapping" : icdDocument.getIedName();
// 落盘文件名只保留安全字符,避免 IED 名称携带路径分隔符导致越界写入。
String safeBaseName = baseName.replaceAll("[\\\\/:*?\"<>|]+", "_").trim();
if (safeBaseName.isEmpty()) {
safeBaseName = "mapping";
}
return safeBaseName + (prettyJson ? "-mapping-pretty.json" : "-mapping.json");
}
}

View File

@@ -1,102 +0,0 @@
package com.njcn.gather.icd.mapping.application.command;
import java.util.ArrayList;
import java.util.List;
/**
* 生成命令对象。
*
* 说明:
* controller 层不要把 MultipartFile 和 request 直接传进领域服务;
* 统一转成 command便于应用层做流程编排。
*/
public class GenerateFromIcdCommand {
/** 原始文件名。 */
private String fileName;
/** ICD 文件字节数组。 */
private byte[] fileBytes;
/** 输出版本号。 */
private String version;
/** 作者。 */
private String author;
/** 是否保存到磁盘。 */
private boolean saveToDisk;
/** 是否输出美化 JSON。 */
private boolean prettyJson;
/** 输出目录。 */
private String outputDir;
/** 用户上送的索引选择结果。 */
private List<IndexSelectionGroupCommand> indexSelection = new ArrayList<IndexSelectionGroupCommand>();
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public byte[] getFileBytes() {
return fileBytes;
}
public void setFileBytes(byte[] fileBytes) {
this.fileBytes = fileBytes;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public boolean isSaveToDisk() {
return saveToDisk;
}
public void setSaveToDisk(boolean saveToDisk) {
this.saveToDisk = saveToDisk;
}
public boolean isPrettyJson() {
return prettyJson;
}
public void setPrettyJson(boolean prettyJson) {
this.prettyJson = prettyJson;
}
public String getOutputDir() {
return outputDir;
}
public void setOutputDir(String outputDir) {
this.outputDir = outputDir;
}
public List<IndexSelectionGroupCommand> getIndexSelection() {
return indexSelection;
}
public void setIndexSelection(List<IndexSelectionGroupCommand> indexSelection) {
this.indexSelection = indexSelection;
}
}

View File

@@ -1,51 +0,0 @@
package com.njcn.gather.icd.mapping.application.command;
/**
* 应用层单条绑定命令。
*/
public class IndexBindingCommand {
/** 报告名。 */
private String reportName;
/** 数据集名。 */
private String dataSetName;
/** 标签。 */
private String label;
/** 绑定到的 lnInst 数字。 */
private String lnInst;
public String getReportName() {
return reportName;
}
public void setReportName(String reportName) {
this.reportName = reportName;
}
public String getDataSetName() {
return dataSetName;
}
public void setDataSetName(String dataSetName) {
this.dataSetName = dataSetName;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
public String getLnInst() {
return lnInst;
}
public void setLnInst(String lnInst) {
this.lnInst = lnInst;
}
}

View File

@@ -1,43 +0,0 @@
package com.njcn.gather.icd.mapping.application.command;
import java.util.ArrayList;
import java.util.List;
/**
* 应用层分组选择命令。
*/
public class IndexSelectionGroupCommand {
/** 分组唯一键。 */
private String groupKey;
/** 分组中文描述。 */
private String groupDesc;
/** 当前分组下的多条绑定关系。 */
private List<IndexBindingCommand> bindings = new ArrayList<IndexBindingCommand>();
public String getGroupKey() {
return groupKey;
}
public void setGroupKey(String groupKey) {
this.groupKey = groupKey;
}
public String getGroupDesc() {
return groupDesc;
}
public void setGroupDesc(String groupDesc) {
this.groupDesc = groupDesc;
}
public List<IndexBindingCommand> getBindings() {
return bindings;
}
public void setBindings(List<IndexBindingCommand> bindings) {
this.bindings = bindings;
}
}

View File

@@ -1,42 +0,0 @@
package com.njcn.gather.icd.mapping.application.result;
import com.njcn.gather.icd.mapping.domain.model.analysis.IndexAnalysis;
import com.njcn.gather.icd.mapping.domain.model.mapping.MappingDocument;
import com.njcn.gather.icd.mapping.enums.GenerateStatus;
import java.util.ArrayList;
import java.util.List;
/**
* 应用层返回对象。统一封装成功、需要选择索引、失败三类结果。
*/
public class GenerateMappingResult {
private GenerateStatus status;
private String message;
private String iedName;
private String ldInst;
private IndexAnalysis indexAnalysis;
private MappingDocument mappingDocument;
private String mappingJson;
private String savedPath;
private List<String> problems = new ArrayList<String>();
public GenerateStatus getStatus() { return status; }
public void setStatus(GenerateStatus status) { this.status = status; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public String getIedName() { return iedName; }
public void setIedName(String iedName) { this.iedName = iedName; }
public String getLdInst() { return ldInst; }
public void setLdInst(String ldInst) { this.ldInst = ldInst; }
public IndexAnalysis getIndexAnalysis() { return indexAnalysis; }
public void setIndexAnalysis(IndexAnalysis indexAnalysis) { this.indexAnalysis = indexAnalysis; }
public MappingDocument getMappingDocument() { return mappingDocument; }
public void setMappingDocument(MappingDocument mappingDocument) { this.mappingDocument = mappingDocument; }
public String getMappingJson() { return mappingJson; }
public void setMappingJson(String mappingJson) { this.mappingJson = mappingJson; }
public String getSavedPath() { return savedPath; }
public void setSavedPath(String savedPath) { this.savedPath = savedPath; }
public List<String> getProblems() { return problems; }
public void setProblems(List<String> problems) { this.problems = problems; }
}

View File

@@ -1,9 +1,9 @@
package com.njcn.gather.icd.mapping.domain.service;
package com.njcn.gather.icd.mapping.component;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.njcn.gather.icd.mapping.config.MappingModuleConfig;
import com.njcn.gather.icd.mapping.domain.model.template.DefaultTemplate;
import com.njcn.gather.icd.mapping.pojo.bo.template.DefaultTemplate;
import com.njcn.gather.icd.mapping.utils.JsonUtils;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
@@ -23,7 +23,10 @@ import java.util.List;
@Service
public class DefaultTemplateLoader {
/** 模块配置,提供默认模板路径等运行参数。 */
private final MappingModuleConfig moduleConfig;
/** 模板反序列化使用的 Jackson 实例。 */
private final ObjectMapper objectMapper;
public DefaultTemplateLoader(MappingModuleConfig moduleConfig) {
@@ -32,6 +35,11 @@ public class DefaultTemplateLoader {
this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
/**
* 加载并校验默认映射模板
*
* 返回值会直接参与索引候选分析和最终 MappingDocument 生成
*/
public DefaultTemplate load() {
try {
ClassPathResource resource = new ClassPathResource(moduleConfig.getDefaultTemplatePath());
@@ -55,6 +63,9 @@ public class DefaultTemplateLoader {
}
}
/**
* 读取 classpath 资源的完整字节内容
*/
private byte[] readAllBytes(ClassPathResource resource) throws Exception {
try (InputStream inputStream = resource.getInputStream();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {

View File

@@ -1,4 +1,4 @@
package com.njcn.gather.icd.mapping.infrastructure.storage;
package com.njcn.gather.icd.mapping.component;
import org.springframework.stereotype.Component;
@@ -11,8 +11,14 @@ import java.nio.charset.StandardCharsets;
*/
@Component
public class FileStorageService {
/**
* 将映射 JSON 写入目标目录
*
* outputDir 为空时使用当前工作目录返回最终文件绝对路径
*/
public String save(String fileName, String content, String outputDir) {
try {
// 输出目录允许由请求覆盖未传时落到当前工作目录
File dir = outputDir == null || outputDir.trim().isEmpty() ? new File(".") : new File(outputDir);
if (!dir.exists() && !dir.mkdirs()) {
throw new IllegalStateException("输出目录创建失败:" + dir.getAbsolutePath());

View File

@@ -1,6 +1,6 @@
package com.njcn.gather.icd.mapping.domain.service;
package com.njcn.gather.icd.mapping.component;
import com.njcn.gather.icd.mapping.domain.model.icd.IcdDocument;
import com.njcn.gather.icd.mapping.pojo.bo.icd.IcdDocument;
import com.njcn.gather.icd.mapping.infrastructure.parser.SclParserAdapter;
import org.springframework.stereotype.Service;
@@ -11,12 +11,16 @@ import org.springframework.stereotype.Service;
@Service
public class IcdParserService {
/** SCL 底层解析适配器,封装 JAXB generated 模型处理。 */
private final SclParserAdapter parserAdapter;
public IcdParserService(SclParserAdapter parserAdapter) {
this.parserAdapter = parserAdapter;
}
/**
* 解析 ICD 文件内容为模块内部统一领域模型
*/
public IcdDocument parse(byte[] bytes, String fileName) {
return parserAdapter.parse(bytes, fileName);
}

View File

@@ -1,13 +1,13 @@
package com.njcn.gather.icd.mapping.domain.service;
package com.njcn.gather.icd.mapping.component;
import com.njcn.gather.icd.mapping.domain.model.analysis.IndexAnalysis;
import com.njcn.gather.icd.mapping.domain.model.analysis.IndexCandidate;
import com.njcn.gather.icd.mapping.domain.model.analysis.IndexCandidateReportItem;
import com.njcn.gather.icd.mapping.domain.model.icd.DataSetNode;
import com.njcn.gather.icd.mapping.domain.model.icd.FcdaNode;
import com.njcn.gather.icd.mapping.domain.model.icd.IcdDocument;
import com.njcn.gather.icd.mapping.domain.model.icd.ReportControlNode;
import com.njcn.gather.icd.mapping.domain.model.template.DefaultTemplate;
import com.njcn.gather.icd.mapping.pojo.bo.analysis.IndexAnalysis;
import com.njcn.gather.icd.mapping.pojo.bo.analysis.IndexCandidate;
import com.njcn.gather.icd.mapping.pojo.bo.analysis.IndexCandidateReportItem;
import com.njcn.gather.icd.mapping.pojo.bo.icd.DataSetNode;
import com.njcn.gather.icd.mapping.pojo.bo.icd.FcdaNode;
import com.njcn.gather.icd.mapping.pojo.bo.icd.IcdDocument;
import com.njcn.gather.icd.mapping.pojo.bo.icd.ReportControlNode;
import com.njcn.gather.icd.mapping.pojo.bo.template.DefaultTemplate;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
@@ -21,17 +21,15 @@ import java.util.Set;
/**
* 索引候选分析服务
*
* 说明
* 1. 新版不再按单个报告平铺返回而是按 DefaultCfg.ReportList 的业务配置项聚合
* 2. 一个业务配置项下可能包含多个报告因此这里会计算 reportCount并返回 reports 子列表
* 3. templateLabels 只是模板参考不要求与 ICD 解析到的 lnInst 数量完全一一对应
* 4. 关键修正
* 在这里就把 DefaultCfg.ReportList inst / Select / TrgOps 一并带入 IndexCandidate
* 后续正式生成阶段直接使用不再重新查模板
* `DefaultCfg.ReportList` 的业务分组聚合 ICD 中的报告信息
* 同时把后续正式生成阶段需要的模板字段一并带入候选结果
*/
@Service
public class IndexAnalysisService {
/**
* 根据 ICD 报告控制块和 DefaultCfg.ReportList 生成索引候选分组
*/
public IndexAnalysis analyze(IcdDocument icdDocument, DefaultTemplate template) {
IndexAnalysis analysis = new IndexAnalysis();
if (icdDocument == null) {
@@ -43,7 +41,7 @@ public class IndexAnalysisService {
return analysis;
}
// 先按模板分组聚合
// 先按模板分组聚合候选项而不是按单个报告平铺返回
for (DefaultTemplate.ReportCfgItem reportCfg : template.getReportList()) {
List<ReportControlNode> matchedReports = collectMatchedReports(icdDocument, reportCfg);
if (matchedReports.isEmpty()) {
@@ -55,7 +53,7 @@ public class IndexAnalysisService {
candidate.setGroupDesc(reportCfg.getDesc());
candidate.setReportCount(matchedReports.size());
// 关键 DefaultCfg.ReportList 配置直接带入候选对象
// 把模板中的关键配置直接带入候选对象避免正式生成阶段再次回查模板
candidate.setReportInst(reportCfg.getInst());
candidate.setSelect(reportCfg.getSelect());
candidate.setTrgOps(reportCfg.getTrgOps());
@@ -76,7 +74,7 @@ public class IndexAnalysisService {
analysis.getCandidates().add(candidate);
}
// 检查是否有 ICD 报告没有被模板覆盖
// 检查 ICD 中是否存在未被模板覆盖的报告配置
if (icdDocument.getReportControls() != null) {
for (ReportControlNode reportControl : icdDocument.getReportControls()) {
if (!isCoveredByTemplate(reportControl, template)) {
@@ -91,6 +89,9 @@ public class IndexAnalysisService {
return analysis;
}
/**
* 收集命中当前模板报告分组 DataSetList 的报告控制块
*/
private List<ReportControlNode> collectMatchedReports(IcdDocument icdDocument, DefaultTemplate.ReportCfgItem reportCfg) {
List<ReportControlNode> result = new ArrayList<ReportControlNode>();
if (icdDocument.getReportControls() == null || reportCfg.getDataSetList() == null) {
@@ -104,6 +105,9 @@ public class IndexAnalysisService {
return result;
}
/**
* 判断 ICD 中的报告是否已经被模板 ReportList 覆盖
*/
private boolean isCoveredByTemplate(ReportControlNode reportControl, DefaultTemplate template) {
if (template == null || template.getReportList() == null) {
return false;
@@ -116,6 +120,9 @@ public class IndexAnalysisService {
return false;
}
/**
* 从指定 DataSet FCDA 中提取可选 lnInst 数字并按数字优先排序
*/
private List<String> collectLnInstValues(IcdDocument icdDocument, String dataSetName) {
if (icdDocument.getDataSets() == null) {
return Collections.emptyList();
@@ -151,6 +158,9 @@ public class IndexAnalysisService {
return result;
}
/**
* 构造前端回传使用的稳定分组 key
*/
private String buildGroupKey(DefaultTemplate.ReportCfgItem reportCfg) {
String desc = reportCfg.getDesc() == null ? "GROUP" : reportCfg.getDesc();
String firstDataSet = (reportCfg.getDataSetList() == null || reportCfg.getDataSetList().isEmpty())
@@ -158,6 +168,9 @@ public class IndexAnalysisService {
return normalize(desc) + "__" + normalize(firstDataSet);
}
/**
* 将中文描述和 DataSet 名转换为可比较的 key 片段
*/
private String normalize(String value) {
return value.replaceAll("[^0-9A-Za-z\\u4e00-\\u9fa5]+", "_")
.replaceAll("_+", "_")

View File

@@ -1,11 +1,11 @@
package com.njcn.gather.icd.mapping.domain.service;
package com.njcn.gather.icd.mapping.component;
import com.njcn.gather.icd.mapping.application.command.IndexBindingCommand;
import com.njcn.gather.icd.mapping.application.command.IndexSelectionGroupCommand;
import com.njcn.gather.icd.mapping.domain.model.analysis.IndexAnalysis;
import com.njcn.gather.icd.mapping.domain.model.analysis.IndexCandidate;
import com.njcn.gather.icd.mapping.domain.model.analysis.IndexCandidateReportItem;
import com.njcn.gather.icd.mapping.domain.model.analysis.ValidationResult;
import com.njcn.gather.icd.mapping.pojo.bo.analysis.IndexAnalysis;
import com.njcn.gather.icd.mapping.pojo.bo.analysis.IndexCandidate;
import com.njcn.gather.icd.mapping.pojo.bo.analysis.IndexCandidateReportItem;
import com.njcn.gather.icd.mapping.pojo.bo.analysis.ValidationResult;
import com.njcn.gather.icd.mapping.pojo.dto.IndexBindingCommand;
import com.njcn.gather.icd.mapping.pojo.dto.IndexSelectionGroupCommand;
import org.springframework.stereotype.Service;
import java.util.List;
@@ -15,13 +15,16 @@ import java.util.List;
*
* 校验原则
* 1. 只校验用户明确提交的绑定项
* 2. 要求模板的所有标签都必须配置
* 2. 强制模板的所有标签都必须配置
* 3. label 必须属于当前业务分组的 templateLabels
* 4. lnInst 必须属于当前报告对应的 ICD 候选数字
*/
@Service
public class IndexValidationService {
/**
* 校验前端提交的分组绑定是否能落到当前 ICD 候选范围内
*/
public ValidationResult validate(IndexAnalysis analysis, List<IndexSelectionGroupCommand> selections) {
ValidationResult result = new ValidationResult();
@@ -33,7 +36,7 @@ public class IndexValidationService {
if (selections == null || selections.isEmpty()) {
for (IndexCandidate candidate : analysis.getCandidates()) {
result.getProblems().add(
"报告组【" + candidate.getGroupDesc() + "】未提交绑定关系,请根据 templateLabels 与 reports[*].availableLnInstValues 完成配置"
"组【" + candidate.getGroupDesc() + "】未提交绑定关系,请根据 templateLabels 与 reports[*].availableLnInstValues 完成配置"
);
}
return result;
@@ -62,6 +65,9 @@ public class IndexValidationService {
return result;
}
/**
* 校验单条 label + report + lnInst 绑定
*/
private void validateBinding(IndexCandidate candidate, IndexBindingCommand binding, ValidationResult result) {
if (binding == null) {
result.getProblems().add("存在空的绑定项");
@@ -93,6 +99,9 @@ public class IndexValidationService {
}
}
/**
* 优先按 groupKey 匹配候选分组兼容只传 groupDesc 的旧调用
*/
private IndexCandidate findCandidate(IndexAnalysis analysis, String groupKey, String groupDesc) {
for (IndexCandidate candidate : analysis.getCandidates()) {
if (same(candidate.getGroupKey(), groupKey)) {
@@ -105,6 +114,9 @@ public class IndexValidationService {
return null;
}
/**
* 在候选分组内定位具体报告dataSetName 为空时只按报告名匹配
*/
private IndexCandidateReportItem findReport(IndexCandidate candidate, String reportName, String dataSetName) {
if (candidate.getReports() == null) {
return null;

View File

@@ -1,9 +1,9 @@
package com.njcn.gather.icd.mapping.infrastructure.serializer;
package com.njcn.gather.icd.mapping.component;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.njcn.gather.icd.mapping.domain.model.mapping.MappingDocument;
import com.njcn.gather.icd.mapping.pojo.bo.mapping.MappingDocument;
import org.springframework.stereotype.Component;
/**
@@ -12,7 +12,10 @@ import org.springframework.stereotype.Component;
@Component
public class MappingDocumentSerializer {
/** 紧凑 JSON 输出器,用于减少响应体体积。 */
private final ObjectMapper compactMapper;
/** 格式化 JSON 输出器,用于人工查看和落盘审阅。 */
private final ObjectMapper prettyMapper;
public MappingDocumentSerializer() {
@@ -24,6 +27,9 @@ public class MappingDocumentSerializer {
prettyMapper.enable(SerializationFeature.INDENT_OUTPUT);
}
/**
* 序列化为紧凑 JSON
*/
public String toCompactJson(MappingDocument document) {
try {
return compactMapper.writeValueAsString(document);
@@ -32,6 +38,9 @@ public class MappingDocumentSerializer {
}
}
/**
* 序列化为格式化 JSON
*/
public String toPrettyJson(MappingDocument document) {
try {
return prettyMapper.writeValueAsString(document);

View File

@@ -1,29 +1,29 @@
package com.njcn.gather.icd.mapping.domain.service;
package com.njcn.gather.icd.mapping.component;
import com.njcn.gather.icd.mapping.application.command.IndexBindingCommand;
import com.njcn.gather.icd.mapping.application.command.IndexSelectionGroupCommand;
import com.njcn.gather.icd.mapping.domain.model.analysis.IndexAnalysis;
import com.njcn.gather.icd.mapping.domain.model.analysis.IndexCandidate;
import com.njcn.gather.icd.mapping.domain.model.analysis.IndexCandidateReportItem;
import com.njcn.gather.icd.mapping.domain.model.icd.DataSetNode;
import com.njcn.gather.icd.mapping.domain.model.icd.DoiElementNode;
import com.njcn.gather.icd.mapping.domain.model.icd.DoiNode;
import com.njcn.gather.icd.mapping.domain.model.icd.FcdaNode;
import com.njcn.gather.icd.mapping.domain.model.icd.IcdDocument;
import com.njcn.gather.icd.mapping.domain.model.icd.LnNode;
import com.njcn.gather.icd.mapping.domain.model.icd.ReportControlNode;
import com.njcn.gather.icd.mapping.domain.model.intermediate.DataSetSelectionState;
import com.njcn.gather.icd.mapping.domain.model.intermediate.ReportAndDataSetState;
import com.njcn.gather.icd.mapping.domain.model.intermediate.ReportBindingState;
import com.njcn.gather.icd.mapping.domain.model.intermediate.ReportGroupState;
import com.njcn.gather.icd.mapping.domain.model.mapping.DataSetGroupItem;
import com.njcn.gather.icd.mapping.domain.model.mapping.DoiItem;
import com.njcn.gather.icd.mapping.domain.model.mapping.InstItem;
import com.njcn.gather.icd.mapping.domain.model.mapping.MappingDocument;
import com.njcn.gather.icd.mapping.domain.model.mapping.ReportMapItem;
import com.njcn.gather.icd.mapping.domain.model.mapping.SdiItem;
import com.njcn.gather.icd.mapping.domain.model.mapping.TypeItem;
import com.njcn.gather.icd.mapping.domain.model.template.DefaultTemplate;
import com.njcn.gather.icd.mapping.pojo.dto.IndexBindingCommand;
import com.njcn.gather.icd.mapping.pojo.dto.IndexSelectionGroupCommand;
import com.njcn.gather.icd.mapping.pojo.bo.analysis.IndexAnalysis;
import com.njcn.gather.icd.mapping.pojo.bo.analysis.IndexCandidate;
import com.njcn.gather.icd.mapping.pojo.bo.analysis.IndexCandidateReportItem;
import com.njcn.gather.icd.mapping.pojo.bo.icd.DataSetNode;
import com.njcn.gather.icd.mapping.pojo.bo.icd.DoiElementNode;
import com.njcn.gather.icd.mapping.pojo.bo.icd.DoiNode;
import com.njcn.gather.icd.mapping.pojo.bo.icd.FcdaNode;
import com.njcn.gather.icd.mapping.pojo.bo.icd.IcdDocument;
import com.njcn.gather.icd.mapping.pojo.bo.icd.LnNode;
import com.njcn.gather.icd.mapping.pojo.bo.icd.ReportControlNode;
import com.njcn.gather.icd.mapping.pojo.bo.state.DataSetSelectionState;
import com.njcn.gather.icd.mapping.pojo.bo.state.ReportAndDataSetState;
import com.njcn.gather.icd.mapping.pojo.bo.state.ReportBindingState;
import com.njcn.gather.icd.mapping.pojo.bo.state.ReportGroupState;
import com.njcn.gather.icd.mapping.pojo.bo.mapping.DataSetGroupItem;
import com.njcn.gather.icd.mapping.pojo.bo.mapping.DoiItem;
import com.njcn.gather.icd.mapping.pojo.bo.mapping.InstItem;
import com.njcn.gather.icd.mapping.pojo.bo.mapping.MappingDocument;
import com.njcn.gather.icd.mapping.pojo.bo.mapping.ReportMapItem;
import com.njcn.gather.icd.mapping.pojo.bo.mapping.SdiItem;
import com.njcn.gather.icd.mapping.pojo.bo.mapping.TypeItem;
import com.njcn.gather.icd.mapping.pojo.bo.template.DefaultTemplate;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
@@ -46,6 +46,9 @@ import java.time.format.DateTimeFormatter;
@Service
public class MappingGenerationService {
/**
* 根据 ICD默认模板和用户索引绑定生成最终映射文档
*/
public MappingDocument generate(IcdDocument icdDocument,
DefaultTemplate template,
IndexAnalysis analysis,
@@ -103,6 +106,7 @@ public class MappingGenerationService {
}
// 2. 生成 DataSetList
// DataSetList desc + lnClass 聚合保持输出顺序稳定
Map<String, DataSetGroupItem> dataSetGroupMap = new LinkedHashMap<String, DataSetGroupItem>();
for (DataSetSelectionState selectionState : state.getDataSetSelections()) {
if (selectionState.getLnNodes() == null || selectionState.getLnNodes().isEmpty()) {
@@ -140,6 +144,11 @@ public class MappingGenerationService {
return document;
}
/**
* 构建正式生成前的中间态
*
* 中间态把报告分组用户绑定和命中的 LN 节点拆开保存避免生成 JSON 时重复查找
*/
private ReportAndDataSetState buildState(IcdDocument icdDocument,
DefaultTemplate template,
IndexAnalysis analysis,
@@ -263,6 +272,9 @@ public class MappingGenerationService {
return result;
}
/**
* lnClass 分组下查找或创建指定 lnInst 的输出节点
*/
private InstItem findOrCreateInst(DataSetGroupItem groupItem, String inst, String desc) {
for (InstItem item : groupItem.getInstList()) {
if (equalsTrim(item.getInst(), inst)) {
@@ -372,6 +384,9 @@ public class MappingGenerationService {
instItem.getDoiList().add(doiItem);
}
/**
* 判断 queueList 命中的对象是否应按原 C# 规则跳过
*/
private boolean shouldSkipQueueItem(boolean queueMode, int icdCount, DefaultTemplate.ObjectCfgItem objectCfg) {
if (!queueMode || objectCfg == null) {
return false;
@@ -418,6 +433,9 @@ public class MappingGenerationService {
return false;
}
/**
* 根据用户绑定标签查找对应的 DataObjectsList 配置项
*/
private List<DefaultTemplate.DataObjectCfgItem> findDataObjectCfgItems(DefaultTemplate template, String label) {
List<DefaultTemplate.DataObjectCfgItem> result = new ArrayList<DefaultTemplate.DataObjectCfgItem>();
if (template == null || template.getDataObjectsList() == null) {
@@ -639,6 +657,11 @@ public class MappingGenerationService {
}
}
/**
* 解析 DOI 的倍率系数
*
* 找不到模板倍率配置时默认返回 1.0
*/
private float resolveCoefficient(DoiNode doiNode, DefaultTemplate template) {
Set<String> values = collectAllLeafValues(doiNode.getChildren());
for (String value : values) {
@@ -837,6 +860,9 @@ public class MappingGenerationService {
return false;
}
/**
* 从模板 LnClassList 中查找当前 LN 的业务分组配置
*/
private DefaultTemplate.LnClassCfgItem findLnClassCfg(DefaultTemplate template, String lnClass) {
if (template == null || template.getLnClassList() == null) {
return null;
@@ -850,6 +876,9 @@ public class MappingGenerationService {
}
/**
* 在候选分析结果中定位当前前端回传的业务分组
*/
private IndexCandidate findCandidate(IndexAnalysis analysis, String groupKey, String groupDesc) {
if (analysis == null || analysis.getCandidates() == null) {
return null;
@@ -862,6 +891,9 @@ public class MappingGenerationService {
return null;
}
/**
* 递归收集 DOI 子树下所有 Val 文本用于匹配倍率等模板配置
*/
private Set<String> collectAllLeafValues(List<DoiElementNode> nodes) {
Set<String> result = new LinkedHashSet<String>();
if (nodes == null) {
@@ -880,6 +912,9 @@ public class MappingGenerationService {
return result;
}
/**
* 从模板 UnitList 中查找指定 DAI 值对应的单位配置
*/
private DefaultTemplate.UnitCfgItem findUnitCfg(DefaultTemplate template, String value) {
if (template == null || template.getUnitList() == null) {
return null;
@@ -892,6 +927,9 @@ public class MappingGenerationService {
return null;
}
/**
* 从模板 MultiplierList 中查找指定 DAI 值对应的倍率配置
*/
private DefaultTemplate.MultiplierCfgItem findMultiplierCfg(DefaultTemplate template, String value) {
if (template == null || template.getMultiplierList() == null) {
return null;
@@ -904,6 +942,9 @@ public class MappingGenerationService {
return null;
}
/**
* 将相别节点名称转换为模板中的中文描述
*/
private String resolvePhaseDesc(DefaultTemplate template, String value) {
if (template == null || template.getPhaseList() == null) {
return value;
@@ -916,6 +957,9 @@ public class MappingGenerationService {
return value;
}
/**
* 将类型节点名称转换为模板中的中文描述
*/
private String resolveTypeDesc(DefaultTemplate template, String value) {
if (template == null || template.getTypeList() == null) {
return value;
@@ -929,6 +973,9 @@ public class MappingGenerationService {
}
/**
* 空安全去首尾空白的字符串比较
*/
private boolean equalsTrim(String left, String right) {
if (left == null && right == null) {
return true;
@@ -1002,6 +1049,9 @@ public class MappingGenerationService {
return "96";
}
/**
* 解析输出版本号请求未传时使用当天日期
*/
private String resolveVersion(String version) {
if (version != null && !version.trim().isEmpty()) {
return version.trim();
@@ -1009,6 +1059,9 @@ public class MappingGenerationService {
return LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE);
}
/**
* 按原 C# 规则转换 reportCount单报告写 0多报告写实际数量
*/
private int resolveReportCount(int size) {
return size <= 1 ? 0 : size;
}

View File

@@ -0,0 +1,132 @@
package com.njcn.gather.icd.mapping.component;
import com.njcn.gather.icd.mapping.config.MappingModuleConfig;
import com.njcn.gather.icd.mapping.pojo.dto.GenerateFromIcdCommand;
import com.njcn.gather.icd.mapping.pojo.dto.IndexBindingCommand;
import com.njcn.gather.icd.mapping.pojo.dto.IndexSelectionGroupCommand;
import com.njcn.gather.icd.mapping.pojo.param.GenerateMappingFromIcdRequest;
import com.njcn.gather.icd.mapping.pojo.param.IndexBindingRequest;
import com.njcn.gather.icd.mapping.pojo.param.IndexSelectionGroupRequest;
import com.njcn.gather.icd.mapping.pojo.param.SubmitIndexSelectionRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
/**
* 请求转换器。
*
* 作用:
* 1. 将接口层 request 转成应用层 command
* 2. 统一处理 null 和空集合,避免后续业务层到处判空。
*/
@Component
public class MappingRequestConverter {
/** 模块默认配置,用于补齐作者、输出目录等缺省值。 */
private final MappingModuleConfig moduleConfig;
public MappingRequestConverter(MappingModuleConfig moduleConfig) {
this.moduleConfig = moduleConfig;
}
/**
* 将候选接口上传文件转换为应用层命令,其他参数走模块默认值。
*/
public GenerateFromIcdCommand toCommand(MultipartFile icdFile) {
return toCommand(icdFile, null);
}
/**
* 将上传文件和请求体转换为应用层命令。
*/
public GenerateFromIcdCommand toCommand(MultipartFile icdFile, GenerateMappingFromIcdRequest request) {
try {
if (icdFile == null || icdFile.isEmpty()) {
throw new IllegalArgumentException("ICD 文件不能为空");
}
GenerateFromIcdCommand command = new GenerateFromIcdCommand();
command.setFileName(icdFile.getOriginalFilename());
command.setFileBytes(icdFile.getBytes());
command.setVersion(request == null ? null : request.getVersion());
command.setAuthor(resolveText(request == null ? null : request.getAuthor(), moduleConfig.getDefaultAuthor()));
command.setSaveToDisk(request != null && request.isSaveToDisk());
command.setPrettyJson(request != null && request.isPrettyJson());
command.setOutputDir(resolveText(request == null ? null : request.getOutputDir(), moduleConfig.getDefaultOutputDir()));
fillIndexSelection(command, request == null ? null : request.getIndexSelection());
return command;
} catch (Exception ex) {
throw new IllegalArgumentException("请求转换失败:" + ex.getMessage(), ex);
}
}
/**
* 将提交绑定接口请求体转换为应用层命令。
*/
public GenerateFromIcdCommand toCommand(SubmitIndexSelectionRequest request) {
try {
if (request == null) {
throw new IllegalArgumentException("请求体不能为空");
}
if (request.getIcdDocument() == null) {
throw new IllegalArgumentException("ICD 解析结果不能为空");
}
GenerateFromIcdCommand command = new GenerateFromIcdCommand();
command.setIcdDocument(request.getIcdDocument());
command.setFileName(request.getIcdDocument().getFileName());
command.setVersion(request.getVersion());
command.setAuthor(resolveText(request.getAuthor(), moduleConfig.getDefaultAuthor()));
command.setSaveToDisk(request.isSaveToDisk());
command.setPrettyJson(request.isPrettyJson());
command.setOutputDir(resolveText(request.getOutputDir(), moduleConfig.getDefaultOutputDir()));
fillIndexSelection(command, request.getIndexSelection());
return command;
} catch (Exception ex) {
throw new IllegalArgumentException("请求转换失败:" + ex.getMessage(), ex);
}
}
/**
* 复制前端提交的索引绑定结构。
*/
private void fillIndexSelection(GenerateFromIcdCommand command, List<IndexSelectionGroupRequest> indexSelection) {
if (indexSelection == null) {
return;
}
for (IndexSelectionGroupRequest groupRequest : indexSelection) {
if (groupRequest == null) {
continue;
}
IndexSelectionGroupCommand groupCommand = new IndexSelectionGroupCommand();
groupCommand.setGroupKey(groupRequest.getGroupKey());
groupCommand.setGroupDesc(groupRequest.getGroupDesc());
if (groupRequest.getBindings() != null) {
for (IndexBindingRequest bindingRequest : groupRequest.getBindings()) {
if (bindingRequest == null) {
continue;
}
IndexBindingCommand bindingCommand = new IndexBindingCommand();
bindingCommand.setReportName(bindingRequest.getReportName());
bindingCommand.setDataSetName(bindingRequest.getDataSetName());
bindingCommand.setLabel(bindingRequest.getLabel());
bindingCommand.setLnInst(bindingRequest.getLnInst());
groupCommand.getBindings().add(bindingCommand);
}
}
command.getIndexSelection().add(groupCommand);
}
}
/**
* 统一处理文本参数的 trim 和默认值回退。
*/
private String resolveText(String value, String defaultValue) {
if (value != null && !value.trim().isEmpty()) {
return value.trim();
}
return defaultValue == null ? null : defaultValue.trim();
}
}

View File

@@ -0,0 +1,85 @@
package com.njcn.gather.icd.mapping.component;
import com.njcn.gather.icd.mapping.pojo.bo.GenerateMappingResult;
import com.njcn.gather.icd.mapping.pojo.bo.analysis.IndexCandidate;
import com.njcn.gather.icd.mapping.pojo.bo.analysis.IndexCandidateReportItem;
import com.njcn.gather.icd.mapping.pojo.enums.GenerateStatus;
import com.njcn.gather.icd.mapping.pojo.vo.IndexCandidateReportItemResponse;
import com.njcn.gather.icd.mapping.pojo.vo.IndexCandidateResponse;
import com.njcn.gather.icd.mapping.pojo.vo.MappingTaskResponse;
import org.springframework.stereotype.Component;
/**
* 接口响应转换器。
*
* 按接口阶段仅组装当前场景必需字段,避免返回冗余信息。
*/
@Component
public class MappingResponseConverter {
/**
* 候选接口响应。
*/
public MappingTaskResponse fromCandidateResult(GenerateMappingResult result) {
MappingTaskResponse response = initBaseResponse(result);
response.setIcdDocument(result.getIcdDocument());
fillIndexCandidates(response, result);
return response;
}
/**
* 正式提交类接口响应。
*
* SUCCESS仅返回最终结果
* NEED_INDEX_SELECTION返回重新选择所需的候选信息
* FAILED仅返回错误信息。
*/
public MappingTaskResponse fromSubmitResult(GenerateMappingResult result) {
MappingTaskResponse response = initBaseResponse(result);
if (result.getStatus() == GenerateStatus.SUCCESS) {
response.setMappingJson(result.getMappingJson());
response.setSavedPath(result.getSavedPath());
return response;
}
if (result.getStatus() == GenerateStatus.NEED_INDEX_SELECTION) {
response.setIcdDocument(result.getIcdDocument());
fillIndexCandidates(response, result);
}
return response;
}
private MappingTaskResponse initBaseResponse(GenerateMappingResult result) {
MappingTaskResponse response = new MappingTaskResponse();
response.setStatus(result.getStatus());
response.setMessage(result.getMessage());
response.getProblems().addAll(result.getProblems());
return response;
}
private void fillIndexCandidates(MappingTaskResponse response, GenerateMappingResult result) {
if (result.getIndexAnalysis() == null || result.getIndexAnalysis().getCandidates() == null) {
return;
}
for (IndexCandidate candidate : result.getIndexAnalysis().getCandidates()) {
IndexCandidateResponse candidateResponse = new IndexCandidateResponse();
candidateResponse.setGroupKey(candidate.getGroupKey());
candidateResponse.setGroupDesc(candidate.getGroupDesc());
candidateResponse.setReportCount(candidate.getReportCount());
candidateResponse.getTemplateLabels().addAll(candidate.getTemplateLabels());
if (candidate.getReports() != null) {
for (IndexCandidateReportItem item : candidate.getReports()) {
IndexCandidateReportItemResponse itemResponse = new IndexCandidateReportItemResponse();
itemResponse.setReportName(item.getReportName());
itemResponse.setDataSetName(item.getDataSetName());
itemResponse.setReportDesc(item.getReportDesc());
itemResponse.getAvailableLnInstValues().addAll(item.getAvailableLnInstValues());
candidateResponse.getReports().add(itemResponse);
}
}
response.getIndexCandidates().add(candidateResponse);
}
}
}

View File

@@ -1,17 +1,18 @@
package com.njcn.gather.icd.mapping.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* 模块配置。
* 模块配置类。集中管理默认作者、默认模板路径等可配置项。
* `mms-mapping` 模块配置。
*
* 说明:
* 1. 这里把模板路径、输出目录、默认作者等集中管理。
* 2. 当前先用 @Value + 默认值,后续你也可以改成 @ConfigurationProperties。
* 统一管理默认模板路径、默认作者和默认输出目录等配置项。
*/
@Component
@Getter
@Setter
public class MappingModuleConfig {
/** 默认模板资源路径。 */
@@ -25,28 +26,4 @@ public class MappingModuleConfig {
/** 默认输出目录。 */
@Value("${icd.mapping.default-output-dir:}")
private String defaultOutputDir;
public String getDefaultTemplatePath() {
return defaultTemplatePath;
}
public void setDefaultTemplatePath(String defaultTemplatePath) {
this.defaultTemplatePath = defaultTemplatePath;
}
public String getDefaultAuthor() {
return defaultAuthor;
}
public void setDefaultAuthor(String defaultAuthor) {
this.defaultAuthor = defaultAuthor;
}
public String getDefaultOutputDir() {
return defaultOutputDir;
}
public void setDefaultOutputDir(String defaultOutputDir) {
this.defaultOutputDir = defaultOutputDir;
}
}

View File

@@ -1,50 +1,92 @@
package com.njcn.gather.icd.mapping.controller;
import com.njcn.gather.icd.mapping.application.MappingTaskAppService;
import com.njcn.gather.icd.mapping.application.command.GenerateFromIcdCommand;
import com.njcn.gather.icd.mapping.application.result.GenerateMappingResult;
import com.njcn.gather.icd.mapping.controller.request.GenerateMappingFromIcdRequest;
import com.njcn.gather.icd.mapping.controller.response.MappingTaskResponse;
import com.njcn.gather.icd.mapping.converter.MappingRequestConverter;
import com.njcn.gather.icd.mapping.converter.MappingResponseConverter;
import com.njcn.common.pojo.annotation.OperateInfo;
import com.njcn.common.pojo.enums.common.LogEnum;
import com.njcn.common.utils.LogUtil;
import com.njcn.gather.icd.mapping.component.MappingRequestConverter;
import com.njcn.gather.icd.mapping.component.MappingResponseConverter;
import com.njcn.gather.icd.mapping.pojo.bo.GenerateMappingResult;
import com.njcn.gather.icd.mapping.pojo.dto.GenerateFromIcdCommand;
import com.njcn.gather.icd.mapping.pojo.param.GenerateMappingFromIcdRequest;
import com.njcn.gather.icd.mapping.pojo.param.SubmitIndexSelectionRequest;
import com.njcn.gather.icd.mapping.pojo.vo.MappingTaskResponse;
import com.njcn.gather.icd.mapping.service.MappingTaskService;
import com.njcn.gather.icd.mapping.utils.DateUtils;
import com.njcn.web.controller.BaseController;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
/**
* ICD 映射接口。
* ICD 映射接口入口
*/
@Slf4j
@Api(tags = "ICD 映射")
@RestController
@RequestMapping("/api/mms-mapping")
public class MappingController {
@RequiredArgsConstructor
public class MappingController extends BaseController {
private final MappingTaskAppService mappingTaskAppService;
/** 映射任务编排服务,负责从 ICD 解析到映射生成的完整链路。 */
private final MappingTaskService mappingTaskService;
/** 请求参数转换器,将接口入参转换为应用层命令。 */
private final MappingRequestConverter requestConverter;
/** 响应转换器,按接口阶段裁剪最小返回字段。 */
private final MappingResponseConverter responseConverter;
public MappingController(MappingTaskAppService mappingTaskAppService,
MappingRequestConverter requestConverter,
MappingResponseConverter responseConverter) {
this.mappingTaskAppService = mappingTaskAppService;
this.requestConverter = requestConverter;
this.responseConverter = responseConverter;
/**
* 上传 ICD 文件,返回候选结果和可编辑的 ICD 解析结果。
*/
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("上传 ICD 文件并生成索引候选")
@PostMapping(value = "/get-icd", consumes = {"multipart/form-data"})
public MappingTaskResponse getICD(@RequestPart("icdFile") MultipartFile icdFile) {
String methodDescribe = getMethodDescribe("getICD");
LogUtil.njcnDebug(log, "{},开始解析 ICD 文件并生成索引候选fileName={}", methodDescribe, DateUtils.resolveFileName(icdFile));
GenerateFromIcdCommand command = requestConverter.toCommand(icdFile);
GenerateMappingResult result = mappingTaskService.getICD(command);
return responseConverter.fromCandidateResult(result);
}
/**
* 上传 ICD 并生成映射
*
* 表单参数:
* 1. icdFileICD 文件
* 2. requestJSON 请求体
* 根据前端确认后的索引绑定关系生成 MMS JSON
*/
@PostMapping(value = "/generate-from-icd", consumes = {"multipart/form-data"})
public MappingTaskResponse generateFromIcd(@RequestPart("icdFile") MultipartFile icdFile,
@Validated @RequestPart("request") GenerateMappingFromIcdRequest request) {
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("获取 MMS JSON")
@PostMapping("/get-mms-json")
public MappingTaskResponse getMmsJson(@Validated @RequestBody SubmitIndexSelectionRequest request) {
String methodDescribe = getMethodDescribe("getMmsJson");
LogUtil.njcnDebug(log, "{},开始生成 MMS JSONindexSelectionCount={}",
methodDescribe, DateUtils.resolveSelectionCount(request == null ? null : request.getIndexSelection()));
GenerateFromIcdCommand command = requestConverter.toCommand(request);
GenerateMappingResult result = mappingTaskService.getMmsJson(command);
return responseConverter.fromSubmitResult(result);
}
/**
* 上传 ICD 后直接串联候选生成和索引提交,统一返回正式提交阶段结果。
*/
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("上传 ICD 后直接获取 MMS JSON")
@PostMapping(value = "/get-icd-mms-json", consumes = {"multipart/form-data"})
public MappingTaskResponse getIcdMmsJson(@RequestPart("icdFile") MultipartFile icdFile,
@Validated @RequestPart("request") GenerateMappingFromIcdRequest request) {
String methodDescribe = getMethodDescribe("getIcdMmsJson");
LogUtil.njcnDebug(log, "{},开始获取 ICD MMS JSONfileName={}, indexSelectionCount={}",
methodDescribe, DateUtils.resolveFileName(icdFile),
DateUtils.resolveSelectionCount(request == null ? null : request.getIndexSelection()));
GenerateFromIcdCommand command = requestConverter.toCommand(icdFile, request);
GenerateMappingResult result = mappingTaskAppService.generateFromIcd(command);
return responseConverter.fromResult(result);
GenerateMappingResult result = mappingTaskService.getIcdMmsJson(command);
return responseConverter.fromSubmitResult(result);
}
}

View File

@@ -1,89 +0,0 @@
package com.njcn.gather.icd.mapping.controller.request;
import java.util.ArrayList;
import java.util.List;
/**
* 生成映射接口请求体。
*
* 说明:
* 1. 旧版结构中indexSelection 是 Map<String, String>,只能表达“一个报告对应一个值”,
* 无法表达“一个业务分组下有多个报告、一个报告下又有多条标签绑定”的真实场景。
* 2. 新版结构改成 List<IndexSelectionGroupRequest>,用来完整承载用户在前端完成的绑定结果。
* 3. 第一次只上传 ICD 时,这个字段可以为空;第二次用户确认绑定后再把完整结构上送即可。
*/
public class GenerateMappingFromIcdRequest {
/** 输出版本号。为空时后端默认补 1.0。 */
private String version;
/** 作者。为空时默认空字符串。 */
private String author;
/** 是否保存到磁盘。 */
private boolean saveToDisk;
/** 是否返回美化 JSON。 */
private boolean prettyJson;
/** 输出目录。saveToDisk=true 时才会用到。 */
private String outputDir;
/**
* 索引选择结果。
*
* 说明:
* 1. 每一个元素代表一个“业务分组”,例如:实时数据、统计数据、波动闪变。
* 2. 每个业务分组下面又包含多条绑定关系。
* 3. 允许为空;为空时后端返回 NEED_INDEX_SELECTION给前端候选参考项。
*/
private List<IndexSelectionGroupRequest> indexSelection = new ArrayList<IndexSelectionGroupRequest>();
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public boolean isSaveToDisk() {
return saveToDisk;
}
public void setSaveToDisk(boolean saveToDisk) {
this.saveToDisk = saveToDisk;
}
public boolean isPrettyJson() {
return prettyJson;
}
public void setPrettyJson(boolean prettyJson) {
this.prettyJson = prettyJson;
}
public String getOutputDir() {
return outputDir;
}
public void setOutputDir(String outputDir) {
this.outputDir = outputDir;
}
public List<IndexSelectionGroupRequest> getIndexSelection() {
return indexSelection;
}
public void setIndexSelection(List<IndexSelectionGroupRequest> indexSelection) {
this.indexSelection = indexSelection;
}
}

View File

@@ -1,59 +0,0 @@
package com.njcn.gather.icd.mapping.controller.request;
/**
* 单条索引绑定请求。
*
* 一条绑定只表达一个最小关系:
* 某个报告(reportName)下,使用某个标签(label)与某个 lnInst 数字做绑定。
*
* 这样做的好处:
* 1. 一个报告可以出现多次,对应多个标签。
* 2. 一个业务分组下也可以有多个报告。
* 3. 后端校验、生成映射时都更容易逐条处理。
*/
public class IndexBindingRequest {
/** 绑定发生在哪个报告上例如brcbStHarm。 */
private String reportName;
/** 绑定发生在哪个数据集上例如dsStHarm。 */
private String dataSetName;
/** 业务标签,例如:最大值、最小值、实时数据。 */
private String label;
/** 当前标签最终绑定到的 lnInst 数字例如1、2、8。 */
private String lnInst;
public String getReportName() {
return reportName;
}
public void setReportName(String reportName) {
this.reportName = reportName;
}
public String getDataSetName() {
return dataSetName;
}
public void setDataSetName(String dataSetName) {
this.dataSetName = dataSetName;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
public String getLnInst() {
return lnInst;
}
public void setLnInst(String lnInst) {
this.lnInst = lnInst;
}
}

View File

@@ -1,53 +0,0 @@
package com.njcn.gather.icd.mapping.controller.request;
import java.util.ArrayList;
import java.util.List;
/**
* 单个业务分组的索引选择请求。
*
* 例如:
* - groupKey = REALTIME_DATA
* - groupDesc = 实时数据
* - bindings = 多条“报告 + 标签 + lnInst”的绑定关系
*/
public class IndexSelectionGroupRequest {
/**
* 分组唯一键。
*
* 说明:
* 这个值由后端在 NEED_INDEX_SELECTION 时返回,前端原样带回即可,避免仅靠中文描述做匹配。
*/
private String groupKey;
/** 分组中文描述,例如:实时数据、统计数据。 */
private String groupDesc;
/** 当前业务分组下,用户最终确认的绑定关系。 */
private List<IndexBindingRequest> bindings = new ArrayList<IndexBindingRequest>();
public String getGroupKey() {
return groupKey;
}
public void setGroupKey(String groupKey) {
this.groupKey = groupKey;
}
public String getGroupDesc() {
return groupDesc;
}
public void setGroupDesc(String groupDesc) {
this.groupDesc = groupDesc;
}
public List<IndexBindingRequest> getBindings() {
return bindings;
}
public void setBindings(List<IndexBindingRequest> bindings) {
this.bindings = bindings;
}
}

View File

@@ -1,54 +0,0 @@
package com.njcn.gather.icd.mapping.controller.response;
import java.util.ArrayList;
import java.util.List;
/**
* 索引候选分组下的单个报告响应。
*/
public class IndexCandidateReportItemResponse {
/** 报告名称。 */
private String reportName;
/** 数据集名称。 */
private String dataSetName;
/** 报告描述。 */
private String reportDesc;
/** 当前报告可选的 lnInst 数字。 */
private List<String> availableLnInstValues = new ArrayList<String>();
public String getReportName() {
return reportName;
}
public void setReportName(String reportName) {
this.reportName = reportName;
}
public String getDataSetName() {
return dataSetName;
}
public void setDataSetName(String dataSetName) {
this.dataSetName = dataSetName;
}
public String getReportDesc() {
return reportDesc;
}
public void setReportDesc(String reportDesc) {
this.reportDesc = reportDesc;
}
public List<String> getAvailableLnInstValues() {
return availableLnInstValues;
}
public void setAvailableLnInstValues(List<String> availableLnInstValues) {
this.availableLnInstValues = availableLnInstValues;
}
}

View File

@@ -1,71 +0,0 @@
package com.njcn.gather.icd.mapping.controller.response;
import java.util.ArrayList;
import java.util.List;
/**
* 索引候选返回对象。
*
* 说明:
* 这是给前端“待匹配界面”使用的正式结构:
* - 一个候选就是一个业务分组;
* - 分组下面再挂多个报告;
* - 前端根据 templateLabels 与 reports[*].availableLnInstValues 做人工绑定。
*/
public class IndexCandidateResponse {
/** 分组唯一键。 */
private String groupKey;
/** 分组中文描述。 */
private String groupDesc;
/** 当前分组包含的报告数。 */
private int reportCount;
/** 模板里配置的可选标签。 */
private List<String> templateLabels = new ArrayList<String>();
/** 当前分组下的报告候选列表。 */
private List<IndexCandidateReportItemResponse> reports = new ArrayList<IndexCandidateReportItemResponse>();
public String getGroupKey() {
return groupKey;
}
public void setGroupKey(String groupKey) {
this.groupKey = groupKey;
}
public String getGroupDesc() {
return groupDesc;
}
public void setGroupDesc(String groupDesc) {
this.groupDesc = groupDesc;
}
public int getReportCount() {
return reportCount;
}
public void setReportCount(int reportCount) {
this.reportCount = reportCount;
}
public List<String> getTemplateLabels() {
return templateLabels;
}
public void setTemplateLabels(List<String> templateLabels) {
this.templateLabels = templateLabels;
}
public List<IndexCandidateReportItemResponse> getReports() {
return reports;
}
public void setReports(List<IndexCandidateReportItemResponse> reports) {
this.reports = reports;
}
}

View File

@@ -1,26 +0,0 @@
package com.njcn.gather.icd.mapping.controller.response;
/**
* 映射摘要响应。
* 映射摘要响应 DTO。用于把最终映射的关键信息单独封装返回。
*/
public class MappingDocumentResponse {
private String version;
private String author;
private String ied;
private String ld;
private int reportCount;
private int dataSetCount;
public String getVersion() { return version; }
public void setVersion(String version) { this.version = version; }
public String getAuthor() { return author; }
public void setAuthor(String author) { this.author = author; }
public String getIed() { return ied; }
public void setIed(String ied) { this.ied = ied; }
public String getLd() { return ld; }
public void setLd(String ld) { this.ld = ld; }
public int getReportCount() { return reportCount; }
public void setReportCount(int reportCount) { this.reportCount = reportCount; }
public int getDataSetCount() { return dataSetCount; }
public void setDataSetCount(int dataSetCount) { this.dataSetCount = dataSetCount; }
}

View File

@@ -1,40 +0,0 @@
package com.njcn.gather.icd.mapping.controller.response;
import com.njcn.gather.icd.mapping.enums.GenerateStatus;
import java.util.ArrayList;
import java.util.List;
/**
* 第一个接口统一响应。
* 接口统一响应 DTO。返回生成状态、映射内容、候选索引、问题列表等。
*/
public class MappingTaskResponse {
private GenerateStatus status;
private String message;
private String iedName;
private String ldInst;
private String mappingJson;
private String savedPath;
private MappingDocumentResponse mappingDocument;
private List<IndexCandidateResponse> indexCandidates = new ArrayList<IndexCandidateResponse>();
private List<String> problems = new ArrayList<String>();
public GenerateStatus getStatus() { return status; }
public void setStatus(GenerateStatus status) { this.status = status; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public String getIedName() { return iedName; }
public void setIedName(String iedName) { this.iedName = iedName; }
public String getLdInst() { return ldInst; }
public void setLdInst(String ldInst) { this.ldInst = ldInst; }
public String getMappingJson() { return mappingJson; }
public void setMappingJson(String mappingJson) { this.mappingJson = mappingJson; }
public String getSavedPath() { return savedPath; }
public void setSavedPath(String savedPath) { this.savedPath = savedPath; }
public MappingDocumentResponse getMappingDocument() { return mappingDocument; }
public void setMappingDocument(MappingDocumentResponse mappingDocument) { this.mappingDocument = mappingDocument; }
public List<IndexCandidateResponse> getIndexCandidates() { return indexCandidates; }
public void setIndexCandidates(List<IndexCandidateResponse> indexCandidates) { this.indexCandidates = indexCandidates; }
public List<String> getProblems() { return problems; }
public void setProblems(List<String> problems) { this.problems = problems; }
}

View File

@@ -1,82 +0,0 @@
package com.njcn.gather.icd.mapping.converter;
import com.njcn.gather.icd.mapping.application.command.GenerateFromIcdCommand;
import com.njcn.gather.icd.mapping.application.command.IndexBindingCommand;
import com.njcn.gather.icd.mapping.application.command.IndexSelectionGroupCommand;
import com.njcn.gather.icd.mapping.config.MappingModuleConfig;
import com.njcn.gather.icd.mapping.controller.request.GenerateMappingFromIcdRequest;
import com.njcn.gather.icd.mapping.controller.request.IndexBindingRequest;
import com.njcn.gather.icd.mapping.controller.request.IndexSelectionGroupRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
/**
* 请求转换器。
*
* 作用:
* 1. 把接口层 request 转成应用层 command。
* 2. 统一处理 null 和空集合,避免后面业务层到处判空。
*/
@Component
public class MappingRequestConverter {
private final MappingModuleConfig moduleConfig;
public MappingRequestConverter(MappingModuleConfig moduleConfig) {
this.moduleConfig = moduleConfig;
}
public GenerateFromIcdCommand toCommand(MultipartFile icdFile, GenerateMappingFromIcdRequest request) {
try {
if (icdFile == null || icdFile.isEmpty()) {
throw new IllegalArgumentException("ICD 文件不能为空");
}
GenerateFromIcdCommand command = new GenerateFromIcdCommand();
command.setFileName(icdFile.getOriginalFilename());
command.setFileBytes(icdFile.getBytes());
command.setVersion(request == null ? null : request.getVersion());
command.setAuthor(resolveText(request == null ? null : request.getAuthor(), moduleConfig.getDefaultAuthor()));
command.setSaveToDisk(request != null && request.isSaveToDisk());
command.setPrettyJson(request == null || request.isPrettyJson());
command.setOutputDir(resolveText(request == null ? null : request.getOutputDir(), moduleConfig.getDefaultOutputDir()));
if (request != null && request.getIndexSelection() != null) {
for (IndexSelectionGroupRequest groupRequest : request.getIndexSelection()) {
if (groupRequest == null) {
continue;
}
IndexSelectionGroupCommand groupCommand = new IndexSelectionGroupCommand();
groupCommand.setGroupKey(groupRequest.getGroupKey());
groupCommand.setGroupDesc(groupRequest.getGroupDesc());
if (groupRequest.getBindings() != null) {
for (IndexBindingRequest bindingRequest : groupRequest.getBindings()) {
if (bindingRequest == null) {
continue;
}
IndexBindingCommand bindingCommand = new IndexBindingCommand();
bindingCommand.setReportName(bindingRequest.getReportName());
bindingCommand.setDataSetName(bindingRequest.getDataSetName());
bindingCommand.setLabel(bindingRequest.getLabel());
bindingCommand.setLnInst(bindingRequest.getLnInst());
groupCommand.getBindings().add(bindingCommand);
}
}
command.getIndexSelection().add(groupCommand);
}
}
return command;
} catch (Exception ex) {
throw new IllegalArgumentException("请求转换失败:" + ex.getMessage(), ex);
}
}
private String resolveText(String value, String defaultValue) {
if (value != null && !value.trim().isEmpty()) {
return value.trim();
}
return defaultValue == null ? null : defaultValue.trim();
}
}

View File

@@ -1,70 +0,0 @@
package com.njcn.gather.icd.mapping.converter;
import com.njcn.gather.icd.mapping.application.result.GenerateMappingResult;
import com.njcn.gather.icd.mapping.controller.response.IndexCandidateReportItemResponse;
import com.njcn.gather.icd.mapping.controller.response.IndexCandidateResponse;
import com.njcn.gather.icd.mapping.controller.response.MappingDocumentResponse;
import com.njcn.gather.icd.mapping.controller.response.MappingTaskResponse;
import com.njcn.gather.icd.mapping.domain.model.analysis.IndexCandidate;
import com.njcn.gather.icd.mapping.domain.model.analysis.IndexCandidateReportItem;
import org.springframework.stereotype.Component;
/**
* 响应转换器。
*
* 作用:
* 1. 把应用层结果转换成接口层响应对象;
* 2. 对待匹配索引场景,输出新的“按业务分组返回”的结构。
*/
@Component
public class MappingResponseConverter {
public MappingTaskResponse fromResult(GenerateMappingResult result) {
MappingTaskResponse response = new MappingTaskResponse();
response.setStatus(result.getStatus());
response.setMessage(result.getMessage());
response.setIedName(result.getIedName());
response.setLdInst(result.getLdInst());
response.setMappingJson(result.getMappingJson());
response.setSavedPath(result.getSavedPath());
response.getProblems().addAll(result.getProblems());
if (result.getIndexAnalysis() != null && result.getIndexAnalysis().getCandidates() != null) {
for (IndexCandidate candidate : result.getIndexAnalysis().getCandidates()) {
IndexCandidateResponse candidateResponse = new IndexCandidateResponse();
candidateResponse.setGroupKey(candidate.getGroupKey());
candidateResponse.setGroupDesc(candidate.getGroupDesc());
candidateResponse.setReportCount(candidate.getReportCount());
candidateResponse.getTemplateLabels().addAll(candidate.getTemplateLabels());
if (candidate.getReports() != null) {
for (IndexCandidateReportItem item : candidate.getReports()) {
IndexCandidateReportItemResponse itemResponse = new IndexCandidateReportItemResponse();
itemResponse.setReportName(item.getReportName());
itemResponse.setDataSetName(item.getDataSetName());
itemResponse.setReportDesc(item.getReportDesc());
itemResponse.getAvailableLnInstValues().addAll(item.getAvailableLnInstValues());
candidateResponse.getReports().add(itemResponse);
}
}
response.getIndexCandidates().add(candidateResponse);
}
}
if (result.getMappingDocument() != null) {
MappingDocumentResponse doc = new MappingDocumentResponse();
doc.setVersion(result.getMappingDocument().getVersion());
doc.setAuthor(result.getMappingDocument().getAuthor());
doc.setIed(result.getMappingDocument().getIed());
doc.setLd(result.getMappingDocument().getLd());
doc.setReportCount(result.getMappingDocument().getReportMap() == null
? 0 : result.getMappingDocument().getReportMap().size());
doc.setDataSetCount(result.getMappingDocument().getDataSetList() == null
? 0 : result.getMappingDocument().getDataSetList().size());
response.setMappingDocument(doc);
}
return response;
}
}

View File

@@ -1,113 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.analysis;
import java.util.ArrayList;
import java.util.List;
/**
* 索引候选分组。
*
* 说明:
* 1. 一条候选对应一个业务分组,例如:统计数据、实时数据;
* 2. 一个业务分组下可以包含多个报告;
* 3. 这里不仅保存返回给前端的候选项,也保存从 DefaultCfg.ReportList 带下来的配置项,
* 供后续 MappingGenerationService 直接使用,避免“二次查模板”失败。
*/
public class IndexCandidate {
/** 分组唯一键。 */
private String groupKey;
/** 分组描述。 */
private String groupDesc;
/** 该分组下实际匹配到的报告数量。 */
private int reportCount;
/** DefaultCfg.txt 中该分组可用的标签模板。 */
private List<String> templateLabels = new ArrayList<String>();
/** 当前分组下匹配到的报告列表。 */
private List<IndexCandidateReportItem> reports = new ArrayList<IndexCandidateReportItem>();
/**
* DefaultCfg.ReportList.inst
* 例如01 / 02 / 03 / 04
*/
private String reportInst;
/**
* DefaultCfg.ReportList.Select
* 例如DataStatFileMap / DataRealFileMap / FlickerFileMap
*/
private String select;
/**
* DefaultCfg.ReportList.TrgOps
* 例如40 / 96
*/
private String trgOps;
public String getGroupKey() {
return groupKey;
}
public void setGroupKey(String groupKey) {
this.groupKey = groupKey;
}
public String getGroupDesc() {
return groupDesc;
}
public void setGroupDesc(String groupDesc) {
this.groupDesc = groupDesc;
}
public int getReportCount() {
return reportCount;
}
public void setReportCount(int reportCount) {
this.reportCount = reportCount;
}
public List<String> getTemplateLabels() {
return templateLabels;
}
public void setTemplateLabels(List<String> templateLabels) {
this.templateLabels = templateLabels;
}
public List<IndexCandidateReportItem> getReports() {
return reports;
}
public void setReports(List<IndexCandidateReportItem> reports) {
this.reports = reports;
}
public String getReportInst() {
return reportInst;
}
public void setReportInst(String reportInst) {
this.reportInst = reportInst;
}
public String getSelect() {
return select;
}
public void setSelect(String select) {
this.select = select;
}
public String getTrgOps() {
return trgOps;
}
public void setTrgOps(String trgOps) {
this.trgOps = trgOps;
}
}

View File

@@ -1,56 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.analysis;
import java.util.ArrayList;
import java.util.List;
/**
* 候选分组下的单个报告项。
*
* 这个对象专门给前端展示“这个分组下面有哪些报告,以及每个报告对应哪些可用 lnInst 数字”。
*/
public class IndexCandidateReportItem {
/** 报告名称。 */
private String reportName;
/** 数据集名称。 */
private String dataSetName;
/** 报告中文描述。一般和分组描述相同,保留它是为了前端渲染更灵活。 */
private String reportDesc;
/** 当前报告在 ICD 中解析出来的所有可选 lnInst 数字。 */
private List<String> availableLnInstValues = new ArrayList<String>();
public String getReportName() {
return reportName;
}
public void setReportName(String reportName) {
this.reportName = reportName;
}
public String getDataSetName() {
return dataSetName;
}
public void setDataSetName(String dataSetName) {
this.dataSetName = dataSetName;
}
public String getReportDesc() {
return reportDesc;
}
public void setReportDesc(String reportDesc) {
this.reportDesc = reportDesc;
}
public List<String> getAvailableLnInstValues() {
return availableLnInstValues;
}
public void setAvailableLnInstValues(List<String> availableLnInstValues) {
this.availableLnInstValues = availableLnInstValues;
}
}

View File

@@ -1,18 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.analysis;
import java.util.ArrayList;
import java.util.List;
/**
* 校验结果。
* 索引校验结果模型。保存是否通过以及问题列表。
*/
public class ValidationResult {
private boolean valid;
private List<String> problems = new ArrayList<String>();
public boolean isValid() { return valid; }
public void setValid(boolean valid) { this.valid = valid; }
public List<String> getProblems() { return problems; }
public void setProblems(List<String> problems) { this.problems = problems; }
}

View File

@@ -1,17 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.icd;
import java.util.ArrayList;
import java.util.List;
/**
* DataSet 节点。
* 数据集模型。用于承接 DataSet 下的 FCDA 列表。
*/
public class DataSetNode {
private String name;
private List<FcdaNode> fcdas = new ArrayList<FcdaNode>();
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public List<FcdaNode> getFcdas() { return fcdas; }
public void setFcdas(List<FcdaNode> fcdas) { this.fcdas = fcdas; }
}

View File

@@ -1,31 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.icd;
import java.util.ArrayList;
import java.util.List;
/**
* DOI/SDI/DAI 统一节点。
* DOI/SDI/DAI 细项模型。用于递归承接 DOI 树明细。
*
* kind 常见值:
* - SDI
* - DAI
*/
public class DoiElementNode {
private String kind;
private String name;
private Long ix;
private List<String> values = new ArrayList<String>();
private List<DoiElementNode> children = new ArrayList<DoiElementNode>();
public String getKind() { return kind; }
public void setKind(String kind) { this.kind = kind; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Long getIx() { return ix; }
public void setIx(Long ix) { this.ix = ix; }
public List<String> getValues() { return values; }
public void setValues(List<String> values) { this.values = values; }
public List<DoiElementNode> getChildren() { return children; }
public void setChildren(List<DoiElementNode> children) { this.children = children; }
}

View File

@@ -1,30 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.icd;
import java.util.ArrayList;
import java.util.List;
/**
* DOI 节点。
* DOI 模型。表示逻辑节点下的一个数据对象节点。
*/
public class DoiNode {
private String name;
private Long ix;
private String lnClass;
private String lnInst;
private int sequenceCount;
private List<DoiElementNode> children = new ArrayList<DoiElementNode>();
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Long getIx() { return ix; }
public void setIx(Long ix) { this.ix = ix; }
public String getLnClass() { return lnClass; }
public void setLnClass(String lnClass) { this.lnClass = lnClass; }
public String getLnInst() { return lnInst; }
public void setLnInst(String lnInst) { this.lnInst = lnInst; }
public int getSequenceCount() { return sequenceCount; }
public void setSequenceCount(int sequenceCount) { this.sequenceCount = sequenceCount; }
public List<DoiElementNode> getChildren() { return children; }
public void setChildren(List<DoiElementNode> children) { this.children = children; }
}

View File

@@ -1,36 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.icd;
/**
* FCDA 节点。
* FCDA 模型。保存 lnClass、lnInst、doName、daName、fc、ix 等信息。
*/
public class FcdaNode {
private String ldInst;
private String prefix;
private String lnClass;
private String lnInst;
private String doName;
private String daName;
private String fc;
private Long ix;
private int sequenceCount;
public String getLdInst() { return ldInst; }
public void setLdInst(String ldInst) { this.ldInst = ldInst; }
public String getPrefix() { return prefix; }
public void setPrefix(String prefix) { this.prefix = prefix; }
public String getLnClass() { return lnClass; }
public void setLnClass(String lnClass) { this.lnClass = lnClass; }
public String getLnInst() { return lnInst; }
public void setLnInst(String lnInst) { this.lnInst = lnInst; }
public String getDoName() { return doName; }
public void setDoName(String doName) { this.doName = doName; }
public String getDaName() { return daName; }
public void setDaName(String daName) { this.daName = daName; }
public String getFc() { return fc; }
public void setFc(String fc) { this.fc = fc; }
public Long getIx() { return ix; }
public void setIx(Long ix) { this.ix = ix; }
public int getSequenceCount() { return sequenceCount; }
public void setSequenceCount(int sequenceCount) { this.sequenceCount = sequenceCount; }
}

View File

@@ -1,46 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.icd;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* ICD 文档聚合根。
* ICD 统一领域模型的聚合根。承接 IED、LDevice、ReportControl、DataSet、LN 等解析结果。
*
* 说明:
* 1. 这是系统内部统一的 ICD 模型。
* 2. 外部 JAXB generated 类只在 parser 层使用。
* 3. 业务层全部依赖这个标准化模型,便于后续替换解析实现。
*/
public class IcdDocument {
private String fileName;
private String iedName;
private String ldInst;
private String ldPrefix;
private IedNode primaryIed;
private LogicalDeviceNode logicalDevice;
private List<LnNode> logicalNodes = new ArrayList<LnNode>();
private List<ReportControlNode> reportControls = new ArrayList<ReportControlNode>();
private Map<String, DataSetNode> dataSets = new LinkedHashMap<String, DataSetNode>();
public String getFileName() { return fileName; }
public void setFileName(String fileName) { this.fileName = fileName; }
public String getIedName() { return iedName; }
public void setIedName(String iedName) { this.iedName = iedName; }
public String getLdInst() { return ldInst; }
public void setLdInst(String ldInst) { this.ldInst = ldInst; }
public String getLdPrefix() { return ldPrefix; }
public void setLdPrefix(String ldPrefix) { this.ldPrefix = ldPrefix; }
public IedNode getPrimaryIed() { return primaryIed; }
public void setPrimaryIed(IedNode primaryIed) { this.primaryIed = primaryIed; }
public LogicalDeviceNode getLogicalDevice() { return logicalDevice; }
public void setLogicalDevice(LogicalDeviceNode logicalDevice) { this.logicalDevice = logicalDevice; }
public List<LnNode> getLogicalNodes() { return logicalNodes; }
public void setLogicalNodes(List<LnNode> logicalNodes) { this.logicalNodes = logicalNodes; }
public List<ReportControlNode> getReportControls() { return reportControls; }
public void setReportControls(List<ReportControlNode> reportControls) { this.reportControls = reportControls; }
public Map<String, DataSetNode> getDataSets() { return dataSets; }
public void setDataSets(Map<String, DataSetNode> dataSets) { this.dataSets = dataSets; }
}

View File

@@ -1,20 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.icd;
import java.util.ArrayList;
import java.util.List;
/**
* IED 节点。
* IED 节点模型。保存 IED 名称以及其下逻辑设备引用。
*/
public class IedNode {
private String name;
private List<String> accessPointNames = new ArrayList<String>();
private List<String> lDeviceInstList = new ArrayList<String>();
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public List<String> getAccessPointNames() { return accessPointNames; }
public void setAccessPointNames(List<String> accessPointNames) { this.accessPointNames = accessPointNames; }
public List<String> getLDeviceInstList() { return lDeviceInstList; }
public void setLDeviceInstList(List<String> lDeviceInstList) { this.lDeviceInstList = lDeviceInstList; }
}

View File

@@ -1,32 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.icd;
import java.util.ArrayList;
import java.util.List;
/**
* 逻辑节点。
* 逻辑节点模型。保存 lnClass、lnInst、prefix、DOI 等信息。
*
* LN0 和 LN 最终都统一抽象为这个模型。
*/
public class LnNode {
private boolean ln0;
private String prefix;
private String lnClass;
private String lnInst;
private String lnType;
private List<DoiNode> doiList = new ArrayList<DoiNode>();
public boolean isLn0() { return ln0; }
public void setLn0(boolean ln0) { this.ln0 = ln0; }
public String getPrefix() { return prefix; }
public void setPrefix(String prefix) { this.prefix = prefix; }
public String getLnClass() { return lnClass; }
public void setLnClass(String lnClass) { this.lnClass = lnClass; }
public String getLnInst() { return lnInst; }
public void setLnInst(String lnInst) { this.lnInst = lnInst; }
public String getLnType() { return lnType; }
public void setLnType(String lnType) { this.lnType = lnType; }
public List<DoiNode> getDoiList() { return doiList; }
public void setDoiList(List<DoiNode> doiList) { this.doiList = doiList; }
}

View File

@@ -1,14 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.icd;
/**
* 逻辑设备节点。
* 逻辑设备模型。保存 inst、prefix 以及其下 LN/LN0 列表。
*/
public class LogicalDeviceNode {
private String inst;
private String ldName;
public String getInst() { return inst; }
public void setInst(String inst) { this.inst = inst; }
public String getLdName() { return ldName; }
public void setLdName(String ldName) { this.ldName = ldName; }
}

View File

@@ -1,29 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.icd;
/**
* ReportControl 节点。
* 报告控制块模型。用于保存报告名称、关联 DataSet、缓冲属性等。
*/
public class ReportControlNode {
private String name;
private String rptId;
private boolean buffered;
private String dataSetName;
private String trgOps;
private String confRev;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getRptId() { return rptId; }
public void setRptId(String rptId) { this.rptId = rptId; }
public boolean isBuffered() { return buffered; }
public void setBuffered(boolean buffered) { this.buffered = buffered; }
public String getDataSetName() { return dataSetName; }
public void setDataSetName(String dataSetName) { this.dataSetName = dataSetName; }
public String getTrgOps() { return trgOps; }
public void setTrgOps(String trgOps) { this.trgOps = trgOps; }
public String getConfRev() { return confRev; }
public void setConfRev(String confRev) { this.confRev = confRev; }
public Boolean getBuffered() { return buffered; }
public void setBuffered(Boolean buffered) { this.buffered = buffered; }
}

View File

@@ -1,103 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.intermediate;
import com.njcn.gather.icd.mapping.domain.model.icd.LnNode;
import java.util.ArrayList;
import java.util.List;
/**
* 最终参与生成 DataSetList 的选择状态。
*
* 关键修正:
* 旧版本一个绑定只保存一个 LnNode导致
* - MSQI 整组丢失
* - MHAI 只生成一半
*
* 新版本改成:
* 一个绑定可以关联多个 LnNode后续生成阶段再逐个展开。
*/
public class DataSetSelectionState {
/** 所属分组 key。 */
private String groupKey;
/** 所属分组 desc。 */
private String groupDesc;
/** 报告名。 */
private String reportName;
/** 数据集名。 */
private String dataSetName;
/** 绑定标签。 */
private String label;
/** 绑定的 lnInst。 */
private String lnInst;
/**
* 当前绑定最终命中的 LN 节点列表。
*
* 说明:
* 同一个 lnInst 在不同 lnClass 下可能都需要展开,
* 例如MMXU / MSQI / MHAI。
*/
private List<LnNode> lnNodes = new ArrayList<LnNode>();
public String getGroupKey() {
return groupKey;
}
public void setGroupKey(String groupKey) {
this.groupKey = groupKey;
}
public String getGroupDesc() {
return groupDesc;
}
public void setGroupDesc(String groupDesc) {
this.groupDesc = groupDesc;
}
public String getReportName() {
return reportName;
}
public void setReportName(String reportName) {
this.reportName = reportName;
}
public String getDataSetName() {
return dataSetName;
}
public void setDataSetName(String dataSetName) {
this.dataSetName = dataSetName;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
public String getLnInst() {
return lnInst;
}
public void setLnInst(String lnInst) {
this.lnInst = lnInst;
}
public List<LnNode> getLnNodes() {
return lnNodes;
}
public void setLnNodes(List<LnNode> lnNodes) {
this.lnNodes = lnNodes;
}
}

View File

@@ -1,59 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.intermediate;
import java.util.ArrayList;
import java.util.List;
/**
* 中间态总对象。
*
* 作用:
* 1. 对应原 C# 里“先形成中间态,再做最终 JSON 组装”的思路;
* 2. 把业务分组、用户绑定、最终 DataSet 选择结果集中保存;
* 3. 避免在 MappingGenerationService 里直接边遍历边拼 JSON便于后续扩展和排查。
*/
public class ReportAndDataSetState {
/** IED 名称。 */
private String iedName;
/** LD 实例名。 */
private String ldInst;
/** 分组状态列表。 */
private List<ReportGroupState> reportGroups = new ArrayList<ReportGroupState>();
/** 数据集选择状态列表。 */
private List<DataSetSelectionState> dataSetSelections = new ArrayList<DataSetSelectionState>();
public String getIedName() {
return iedName;
}
public void setIedName(String iedName) {
this.iedName = iedName;
}
public String getLdInst() {
return ldInst;
}
public void setLdInst(String ldInst) {
this.ldInst = ldInst;
}
public List<ReportGroupState> getReportGroups() {
return reportGroups;
}
public void setReportGroups(List<ReportGroupState> reportGroups) {
this.reportGroups = reportGroups;
}
public List<DataSetSelectionState> getDataSetSelections() {
return dataSetSelections;
}
public void setDataSetSelections(List<DataSetSelectionState> dataSetSelections) {
this.dataSetSelections = dataSetSelections;
}
}

View File

@@ -1,73 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.intermediate;
/**
* 单条最终有效绑定关系的中间态。
*/
public class ReportBindingState {
/** 分组 key。 */
private String groupKey;
/** 分组描述。 */
private String groupDesc;
/** 报告名。 */
private String reportName;
/** 数据集名。 */
private String dataSetName;
/** 标签。 */
private String label;
/** 绑定的 lnInst 数字。 */
private String lnInst;
public String getGroupKey() {
return groupKey;
}
public void setGroupKey(String groupKey) {
this.groupKey = groupKey;
}
public String getGroupDesc() {
return groupDesc;
}
public void setGroupDesc(String groupDesc) {
this.groupDesc = groupDesc;
}
public String getReportName() {
return reportName;
}
public void setReportName(String reportName) {
this.reportName = reportName;
}
public String getDataSetName() {
return dataSetName;
}
public void setDataSetName(String dataSetName) {
this.dataSetName = dataSetName;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
public String getLnInst() {
return lnInst;
}
public void setLnInst(String lnInst) {
this.lnInst = lnInst;
}
}

View File

@@ -1,100 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.intermediate;
import com.njcn.gather.icd.mapping.domain.model.analysis.IndexCandidateReportItem;
import java.util.ArrayList;
import java.util.List;
/**
* 单个业务分组的中间态。
*/
public class ReportGroupState {
/** 分组唯一键。 */
private String groupKey;
/** 分组描述。 */
private String groupDesc;
/** 当前分组匹配到的报告数量。 */
private int reportCount;
/** 报表 inst来自 DefaultCfg.ReportList.inst。 */
private String reportInst;
/** Select来自 DefaultCfg.ReportList.Select。 */
private String select;
/** TrgOps来自 DefaultCfg.ReportList.TrgOps。 */
private String trgOps;
/** 当前分组包含的报告列表。 */
private List<IndexCandidateReportItem> reportItems = new ArrayList<IndexCandidateReportItem>();
/** 用户最终确认的绑定关系。 */
private List<ReportBindingState> bindings = new ArrayList<ReportBindingState>();
public String getGroupKey() {
return groupKey;
}
public void setGroupKey(String groupKey) {
this.groupKey = groupKey;
}
public String getGroupDesc() {
return groupDesc;
}
public void setGroupDesc(String groupDesc) {
this.groupDesc = groupDesc;
}
public int getReportCount() {
return reportCount;
}
public void setReportCount(int reportCount) {
this.reportCount = reportCount;
}
public String getReportInst() {
return reportInst;
}
public void setReportInst(String reportInst) {
this.reportInst = reportInst;
}
public String getSelect() {
return select;
}
public void setSelect(String select) {
this.select = select;
}
public String getTrgOps() {
return trgOps;
}
public void setTrgOps(String trgOps) {
this.trgOps = trgOps;
}
public List<IndexCandidateReportItem> getReportItems() {
return reportItems;
}
public void setReportItems(List<IndexCandidateReportItem> reportItems) {
this.reportItems = reportItems;
}
public List<ReportBindingState> getBindings() {
return bindings;
}
public void setBindings(List<ReportBindingState> bindings) {
this.bindings = bindings;
}
}

View File

@@ -1,43 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.mapping;
import java.util.ArrayList;
import java.util.List;
/**
* 最终映射中的 DataSetList 单项。
*/
public class DataSetGroupItem {
/** 分组描述。一般来自 LnClassList.desc。 */
private String desc;
/** lnClass。 */
private String lnClass;
/** 该 lnClass 下的 inst 列表。 */
private List<InstItem> instList = new ArrayList<InstItem>();
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
public String getLnClass() {
return lnClass;
}
public void setLnClass(String lnClass) {
this.lnClass = lnClass;
}
public List<InstItem> getInstList() {
return instList;
}
public void setInstList(List<InstItem> instList) {
this.instList = instList;
}
}

View File

@@ -1,120 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.mapping;
import java.util.ArrayList;
import java.util.List;
/**
* 最终映射中的 doiList 单项。
*/
public class DoiItem {
/** DOI 名称。 */
private String name;
/** DOI 描述。 */
private String desc;
/** 起始序号。 */
private int start;
/** 结束序号。 */
private int end;
/** 单位。 */
private String unit;
/** 系数。 */
private float coefficient;
/** 基波标志。 */
private int baseflag;
/** 基波数量。 */
private int basecount;
/** ICD 实际序列数。 */
private int icdcout;
/** SDI 列表。 */
private List<SdiItem> sdiList = new ArrayList<SdiItem>();
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
public int getStart() {
return start;
}
public void setStart(int start) {
this.start = start;
}
public int getEnd() {
return end;
}
public void setEnd(int end) {
this.end = end;
}
public String getUnit() {
return unit;
}
public void setUnit(String unit) {
this.unit = unit;
}
public float getCoefficient() {
return coefficient;
}
public void setCoefficient(float coefficient) {
this.coefficient = coefficient;
}
public int getBaseflag() {
return baseflag;
}
public void setBaseflag(int baseflag) {
this.baseflag = baseflag;
}
public int getBasecount() {
return basecount;
}
public void setBasecount(int basecount) {
this.basecount = basecount;
}
public int getIcdcout() {
return icdcout;
}
public void setIcdcout(int icdcout) {
this.icdcout = icdcout;
}
public List<SdiItem> getSdiList() {
return sdiList;
}
public void setSdiList(List<SdiItem> sdiList) {
this.sdiList = sdiList;
}
}

View File

@@ -1,43 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.mapping;
import java.util.ArrayList;
import java.util.List;
/**
* 最终映射中的 instList 单项。
*/
public class InstItem {
/** lnInst。 */
private String inst;
/** 该 inst 的描述。通常使用当前绑定的 label。 */
private String desc;
/** DOI 列表。 */
private List<DoiItem> doiList = new ArrayList<DoiItem>();
public String getInst() {
return inst;
}
public void setInst(String inst) {
this.inst = inst;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
public List<DoiItem> getDoiList() {
return doiList;
}
public void setDoiList(List<DoiItem> doiList) {
this.doiList = doiList;
}
}

View File

@@ -1,127 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.mapping;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.ArrayList;
import java.util.List;
/**
* 最终映射文档。
*
* 关键说明:
* 1. Java 字段统一使用 lowerCamelCase避免 Jackson 同时输出 ied/IED 这类重复字段。
* 2. JSON 输出名通过 @JsonProperty 显式指定,确保与原 C# 输出格式一致。
*/
public class MappingDocument {
@JsonProperty("version")
private String version;
@JsonProperty("author")
private String author;
@JsonProperty("IED")
private String ied;
@JsonProperty("LD")
private String ld;
/**
* 原 C# mainFrom.txt 固定调用:
* Set_WaveTimeFlag("BeiJing")
*/
@JsonProperty("WaveTimeFlag")
private String waveTimeFlag;
/**
* 原 C# mainFrom.txt 固定调用:
* Set_DataType("1")
*/
@JsonProperty("DataType")
private String dataType;
/**
* 原 C# mainFrom.txt 固定调用:
* Set_unit("1")
*/
@JsonProperty("unit")
private String unit;
@JsonProperty("ReportMap")
private List<ReportMapItem> reportMap = new ArrayList<ReportMapItem>();
@JsonProperty("DataSetList")
private List<DataSetGroupItem> dataSetList = new ArrayList<DataSetGroupItem>();
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public String getIed() {
return ied;
}
public void setIed(String ied) {
this.ied = ied;
}
public String getLd() {
return ld;
}
public void setLd(String ld) {
this.ld = ld;
}
public String getWaveTimeFlag() {
return waveTimeFlag;
}
public void setWaveTimeFlag(String waveTimeFlag) {
this.waveTimeFlag = waveTimeFlag;
}
public String getDataType() {
return dataType;
}
public void setDataType(String dataType) {
this.dataType = dataType;
}
public String getUnit() {
return unit;
}
public void setUnit(String unit) {
this.unit = unit;
}
public List<ReportMapItem> getReportMap() {
return reportMap;
}
public void setReportMap(List<ReportMapItem> reportMap) {
this.reportMap = reportMap;
}
public List<DataSetGroupItem> getDataSetList() {
return dataSetList;
}
public void setDataSetList(List<DataSetGroupItem> dataSetList) {
this.dataSetList = dataSetList;
}
}

View File

@@ -1,124 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.mapping;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* 最终映射中的 ReportMap 单项。
*
* 关键说明:
* 1. 原 C# 不是把同组报告合并成一条,而是组内每个报告各自输出一条。
* 2. 但 reportCount 仍然写该分组总数。
* 3. buffered 不是 boolean而是
* - true -> BR
* - false -> RP
*/
public class ReportMapItem {
@JsonProperty("desc")
private String desc;
@JsonProperty("reportCount")
private int reportCount;
@JsonProperty("rptID")
private String rptId;
@JsonProperty("name")
private String name;
@JsonProperty("buffered")
private String buffered;
@JsonProperty("inst")
private String inst;
/**
* 原 C# Set_FlickerFlag() 当前固定写 "0"
*/
@JsonProperty("FlickerFlag")
private String flickerFlag;
/**
* 原 C# 来自 DefaultCfg.ReportList.Select
*/
@JsonProperty("Select")
private String select;
/**
* 原 C# 来自 DefaultCfg.ReportList.TrgOps
*/
@JsonProperty("TrgOps")
private String trgOps;
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
public int getReportCount() {
return reportCount;
}
public void setReportCount(int reportCount) {
this.reportCount = reportCount;
}
public String getRptId() {
return rptId;
}
public void setRptId(String rptId) {
this.rptId = rptId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getBuffered() {
return buffered;
}
public void setBuffered(String buffered) {
this.buffered = buffered;
}
public String getInst() {
return inst;
}
public void setInst(String inst) {
this.inst = inst;
}
public String getFlickerFlag() {
return flickerFlag;
}
public void setFlickerFlag(String flickerFlag) {
this.flickerFlag = flickerFlag;
}
public String getSelect() {
return select;
}
public void setSelect(String select) {
this.select = select;
}
public String getTrgOps() {
return trgOps;
}
public void setTrgOps(String trgOps) {
this.trgOps = trgOps;
}
}

View File

@@ -1,43 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.mapping;
import java.util.ArrayList;
import java.util.List;
/**
* 最终映射中的 sdiList 单项。
*/
public class SdiItem {
/** SDI 名称。 */
private String name;
/** SDI 描述。 */
private String desc;
/** 类型列表。 */
private List<TypeItem> typeList = new ArrayList<TypeItem>();
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
public List<TypeItem> getTypeList() {
return typeList;
}
public void setTypeList(List<TypeItem> typeList) {
this.typeList = typeList;
}
}

View File

@@ -1,29 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.mapping;
/**
* 最终映射中的 typeList 单项。
*/
public class TypeItem {
/** 类型名称。 */
private String name;
/** 类型描述。 */
private String desc;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
}

View File

@@ -1,345 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.template;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* 默认模板。
* 默认模板模型。把 default-template.json 解析成可直接使用的对象。
*
* 当前只保留第一个接口真正会用到的部分。
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public class DefaultTemplate {
@JsonProperty("ReportList")
private List<ReportCfgItem> reportList = new ArrayList<ReportCfgItem>();
@JsonProperty("LnClassList")
private List<LnClassCfgItem> lnClassList = new ArrayList<LnClassCfgItem>();
@JsonProperty("PhaseList")
private List<PhaseCfgItem> phaseList = new ArrayList<PhaseCfgItem>();
@JsonProperty("MultiplierList")
private List<MultiplierCfgItem> multiplierList = new ArrayList<MultiplierCfgItem>();
@JsonProperty("UnitList")
private List<UnitCfgItem> unitList = new ArrayList<UnitCfgItem>();
@JsonProperty("TypeList")
private List<TypeCfgItem> typeList = new ArrayList<TypeCfgItem>();
@JsonProperty("DataObjectsList")
private List<DataObjectCfgItem> dataObjectsList = new ArrayList<DataObjectCfgItem>();
/**
* 基础校验。
*
* 返回问题列表;为空表示通过。
*/
public List<String> verify() {
List<String> problems = new ArrayList<String>();
ensureDuplicateNames("LnClassList", extractNames(lnClassList), problems);
ensureDuplicateNames("PhaseList", extractNames(phaseList), problems);
ensureDuplicateNames("MultiplierList", extractNames(multiplierList), problems);
ensureDuplicateNames("UnitList", extractNames(unitList), problems);
ensureDuplicateNames("TypeList", extractNames(typeList), problems);
ensureDuplicateObjectNames(problems);
if (reportList == null || reportList.isEmpty()) {
problems.add("DefaultCfg.ReportList 为空");
}
return problems;
}
private List<String> extractNames(List<? extends NameListSupport> list) {
List<String> names = new ArrayList<String>();
if (list == null) {
return names;
}
for (NameListSupport item : list) {
if (item.getNameList() != null) {
names.addAll(item.getNameList());
}
}
return names;
}
private void ensureDuplicateNames(String section, List<String> names, List<String> problems) {
Set<String> set = new HashSet<String>();
for (String name : names) {
if (name == null) {
continue;
}
String key = name.trim();
if (!set.add(key)) {
problems.add(section + " 中存在重复配置项:" + key);
}
}
}
private void ensureDuplicateObjectNames(List<String> problems) {
if (dataObjectsList == null) {
return;
}
for (DataObjectCfgItem dataObject : dataObjectsList) {
List<String> names = new ArrayList<String>();
if (dataObject.getObjectList() != null) {
for (ObjectCfgItem object : dataObject.getObjectList()) {
if (object.getNameList() != null) {
names.addAll(object.getNameList());
}
}
}
ensureDuplicateNames("DataObjectsList[" + dataObject.getDesc() + "]", names, problems);
}
}
public List<ReportCfgItem> getReportList() {
return reportList;
}
public void setReportList(List<ReportCfgItem> reportList) {
this.reportList = reportList;
}
public List<LnClassCfgItem> getLnClassList() {
return lnClassList;
}
public void setLnClassList(List<LnClassCfgItem> lnClassList) {
this.lnClassList = lnClassList;
}
public List<PhaseCfgItem> getPhaseList() {
return phaseList;
}
public void setPhaseList(List<PhaseCfgItem> phaseList) {
this.phaseList = phaseList;
}
public List<MultiplierCfgItem> getMultiplierList() {
return multiplierList;
}
public void setMultiplierList(List<MultiplierCfgItem> multiplierList) {
this.multiplierList = multiplierList;
}
public List<UnitCfgItem> getUnitList() {
return unitList;
}
public void setUnitList(List<UnitCfgItem> unitList) {
this.unitList = unitList;
}
public List<TypeCfgItem> getTypeList() {
return typeList;
}
public void setTypeList(List<TypeCfgItem> typeList) {
this.typeList = typeList;
}
public List<DataObjectCfgItem> getDataObjectsList() {
return dataObjectsList;
}
public void setDataObjectsList(List<DataObjectCfgItem> dataObjectsList) {
this.dataObjectsList = dataObjectsList;
}
/** 统一抽象:凡是有 nameList 的配置项都实现它。 */
public interface NameListSupport {
List<String> getNameList();
}
@JsonIgnoreProperties(ignoreUnknown = true)
public static class ReportCfgItem {
/**
* 报告分组描述,例如:统计数据、实时数据、波动闪变
*/
@JsonProperty("desc")
private String desc;
/**
* 报告 inst例如01
*/
@JsonProperty("inst")
private String inst;
/**
* 原始配置中的 TrgOps。
* 例如40 / 96
*
* 这里必须显式加 @JsonProperty("TrgOps")
* 否则在当前项目里很容易反序列化后为 null。
*/
@JsonProperty("TrgOps")
private String trgOps;
/**
* 原始配置中的 Select。
* 例如DataStatFileMap / DataRealFileMap / FlickerFileMap
*
* 这里必须显式加 @JsonProperty("Select")
* 否则在当前项目里很容易反序列化后为 null。
*/
@JsonProperty("Select")
private String select;
/**
* 该分组覆盖的数据集名称列表
*/
@JsonProperty("DataSetList")
private List<String> dataSetList = new ArrayList<String>();
/**
* 该分组可配置的标签模板列表
*/
@JsonProperty("LnInstList")
private List<String> lnInstList = new ArrayList<String>();
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
public String getInst() {
return inst;
}
public void setInst(String inst) {
this.inst = inst;
}
public String getTrgOps() {
return trgOps;
}
public void setTrgOps(String trgOps) {
this.trgOps = trgOps;
}
public String getSelect() {
return select;
}
public void setSelect(String select) {
this.select = select;
}
public List<String> getDataSetList() {
return dataSetList;
}
public void setDataSetList(List<String> dataSetList) {
this.dataSetList = dataSetList;
}
public List<String> getLnInstList() {
return lnInstList;
}
public void setLnInstList(List<String> lnInstList) {
this.lnInstList = lnInstList;
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
public static class LnClassCfgItem implements NameListSupport {
private String desc;
private List<String> nameList = new ArrayList<String>();
public String getDesc() { return desc; }
public void setDesc(String desc) { this.desc = desc; }
public List<String> getNameList() { return nameList; }
public void setNameList(List<String> nameList) { this.nameList = nameList; }
}
@JsonIgnoreProperties(ignoreUnknown = true)
public static class PhaseCfgItem implements NameListSupport {
private String desc;
private List<String> nameList = new ArrayList<String>();
public String getDesc() { return desc; }
public void setDesc(String desc) { this.desc = desc; }
public List<String> getNameList() { return nameList; }
public void setNameList(List<String> nameList) { this.nameList = nameList; }
}
@JsonIgnoreProperties(ignoreUnknown = true)
public static class MultiplierCfgItem implements NameListSupport {
private int multiplier;
private List<String> nameList = new ArrayList<String>();
public int getMultiplier() { return multiplier; }
public void setMultiplier(int multiplier) { this.multiplier = multiplier; }
public List<String> getNameList() { return nameList; }
public void setNameList(List<String> nameList) { this.nameList = nameList; }
}
@JsonIgnoreProperties(ignoreUnknown = true)
public static class UnitCfgItem implements NameListSupport {
private String desc;
private List<String> nameList = new ArrayList<String>();
public String getDesc() { return desc; }
public void setDesc(String desc) { this.desc = desc; }
public List<String> getNameList() { return nameList; }
public void setNameList(List<String> nameList) { this.nameList = nameList; }
}
@JsonIgnoreProperties(ignoreUnknown = true)
public static class TypeCfgItem implements NameListSupport {
private String desc;
private List<String> nameList = new ArrayList<String>();
public String getDesc() { return desc; }
public void setDesc(String desc) { this.desc = desc; }
public List<String> getNameList() { return nameList; }
public void setNameList(List<String> nameList) { this.nameList = nameList; }
}
@JsonIgnoreProperties(ignoreUnknown = true)
public static class DataObjectCfgItem {
private String desc;
@JsonProperty("LnInstList")
private List<String> lnInstList = new ArrayList<String>();
@JsonProperty("ObjectList")
private List<ObjectCfgItem> objectList = new ArrayList<ObjectCfgItem>();
public String getDesc() { return desc; }
public void setDesc(String desc) { this.desc = desc; }
public List<String> getLnInstList() { return lnInstList; }
public void setLnInstList(List<String> lnInstList) { this.lnInstList = lnInstList; }
public List<ObjectCfgItem> getObjectList() { return objectList; }
public void setObjectList(List<ObjectCfgItem> objectList) { this.objectList = objectList; }
}
@JsonIgnoreProperties(ignoreUnknown = true)
public static class ObjectCfgItem implements NameListSupport {
private String desc;
private int baseflag;
private int basecount;
private int queuecount;
private List<String> nameList = new ArrayList<String>();
private List<String> queueList = new ArrayList<String>();
public String getDesc() { return desc; }
public void setDesc(String desc) { this.desc = desc; }
public int getBaseflag() { return baseflag; }
public void setBaseflag(int baseflag) { this.baseflag = baseflag; }
public int getBasecount() { return basecount; }
public void setBasecount(int basecount) { this.basecount = basecount; }
public int getQueuecount() { return queuecount; }
public void setQueuecount(int queuecount) { this.queuecount = queuecount; }
public List<String> getNameList() { return nameList; }
public void setNameList(List<String> nameList) { this.nameList = nameList; }
public List<String> getQueueList() { return queueList; }
public void setQueueList(List<String> queueList) { this.queueList = queueList; }
}
}

View File

@@ -1,26 +1,29 @@
package com.njcn.gather.icd.mapping.infrastructure.parser;
import com.njcn.gather.icd.mapping.domain.model.icd.*;
import com.njcn.gather.icd.mapping.pojo.bo.icd.*;
import com.njcn.gather.icd.mapping.infrastructure.parser.generated.*;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* JAXB generated 模型读取器。
* generated 模型读取器。遍历 SCL/IED/LDevice/LN0/LN/ReportControl/DataSet/DOI 树并转换。
* JAXB 生成模型读取器。
*
* 说明:
* 1. 这里只负责“把 generated 模型读取出来”
* 2. 不直接做接口编排,也不做 JSON 序列化。
* 负责遍历 `SCL/IED/LDevice/LN0/LN/ReportControl/DataSet/DOI` 结构,
* 并转换为模块内部使用的 ICD 领域模型
*/
public class SclGeneratedModelReader {
/**
* 从 JAXB SCL 根对象中读取当前模块需要的 ICD 结构。
*/
public IcdDocument read(SCL scl, String fileName) {
IcdDocument document = new IcdDocument();
document.setFileName(fileName);
// 当前业务只处理第一个可用的非 LD0/PQLD0 逻辑设备。
TIED targetIed = null;
TLDevice targetDevice = null;
@@ -82,15 +85,16 @@ public class SclGeneratedModelReader {
document.getLogicalNodes().add(readLogicalNode(ln, false));
}
}
// 关键修正:
// C# 原版的 icdcout 优先来自 DataTypeTemplates -> LNodeType -> DO -> DOType -> DA.count
// 不是靠 DataSet/FCDA 重复数来反推。
// 这里改成:先按模板 count 回填 DOI.sequenceCount找不到再退回 FCDA。
// `icdcout` 优先取自模板链路 `DataTypeTemplates -> LNodeType -> DO -> DOType -> DA.count`
// 只有模板未提供时才退回到 `DataSet/FCDA` 反查,保持与原 C# 行为一致。
syncDoiSequenceCount(scl, document);
return document;
}
/**
* 从 LN0 或 LN 中读取 DataSet 与 ReportControl 信息。
*/
private void readReportAndDataSetFromAnyLn(TAnyLN anyLn, IcdDocument document) {
List<FcdaNode> allFcdas = new ArrayList<FcdaNode>();
if (anyLn.getDataSet() != null) {
@@ -127,6 +131,9 @@ public class SclGeneratedModelReader {
}
}
/**
* 将 ReportControl.TrgOps 转成后续模板使用的触发编码。
*/
private String readTrgOps(TReportControl reportControl) {
if (reportControl.getTrgOps() == null) {
return null;
@@ -146,24 +153,22 @@ public class SclGeneratedModelReader {
/**
* 读取逻辑节点。
*
* 注意:
* 当前这套 JAXB 生成类中TAnyLN 只提供了通用能力,
* prefix / lnClass / inst 这些属性实际定义在子类 TLN / TLN0 上,
* 所以这里不能直接对 TAnyLN 调用 getPrefix()/getLnClass()/getInst()。
* `TAnyLN` 只暴露通用能力,`prefix / lnClass / inst` 实际定义在 `TLN / TLN0` 子类上,
* 因此这里需要按具体类型分别读取。
*/
private LnNode readLogicalNode(TAnyLN anyLn, boolean isLn0) {
LnNode node = new LnNode();
node.setLn0(isLn0);
// lnType 在 TAnyLN 上是存在的,可直接读取
// `lnType` 定义`TAnyLN` 上,可直接读取
node.setLnType(anyLn.getLnType());
// prefix / lnClass / inst 需要根据具体子类读取
// `prefix / lnClass / inst` 需要根据具体子类读取
node.setPrefix(resolveLnPrefix(anyLn));
node.setLnClass(resolveLnClass(anyLn));
node.setLnInst(resolveLnInst(anyLn));
// DOI 定义在 TAnyLN 上,可直接读取
// `DOI` 定义在 `TAnyLN` 上,可直接读取
if (anyLn.getDOI() != null) {
for (TDOI doi : anyLn.getDOI()) {
DoiNode doiNode = new DoiNode();
@@ -188,10 +193,9 @@ public class SclGeneratedModelReader {
return node;
}
/**
* 解析逻辑节点 prefix。
* 解析逻辑节点的 `prefix`
*
* TLN 有 prefix
* TLN0 按这套生成类没有单独 prefix 字段,返回空字符串即可。
* `TLN``prefix` 字段,`TLN0` 在当前生成模型中没有单独字段,统一返回空字符串。
*/
private String resolveLnPrefix(TAnyLN anyLn) {
if (anyLn instanceof TLN) {
@@ -201,11 +205,9 @@ public class SclGeneratedModelReader {
}
/**
* 解析逻辑节点 lnClass。
* 解析逻辑节点的 `lnClass`
*
* 当前生成类里:
* - TLN.getLnClass() 返回 List<String>
* - TLN0.getLnClass() 返回 List<String>
* 当前生成模型中,`TLN` 和 `TLN0` 都返回 `List<String>`,这里只取首个值。
*/
private String resolveLnClass(TAnyLN anyLn) {
if (anyLn instanceof TLN) {
@@ -218,11 +220,9 @@ public class SclGeneratedModelReader {
}
/**
* 解析逻辑节点 inst。
* 解析逻辑节点的 `inst`
*
* 当前生成类里:
* - TLN.getInst() 存在
* - TLN0.getInst() 也存在
* 当前生成模型中,`TLN` 和 `TLN0` 都提供了该字段。
*/
private String resolveLnInst(TAnyLN anyLn) {
if (anyLn instanceof TLN) {
@@ -234,6 +234,9 @@ public class SclGeneratedModelReader {
return null;
}
/**
* 递归读取 DOI 下的 SDI/DAI 混合子节点。
*/
private DoiElementNode readUnNamingNode(TUnNaming source) {
if (source == null) {
return null;
@@ -272,6 +275,9 @@ public class SclGeneratedModelReader {
return null;
}
/**
* 将 JAXB FCDA 节点转换为内部 FCDA 模型。
*/
private FcdaNode toFcdaNode(TFCDA fcda) {
FcdaNode node = new FcdaNode();
node.setLdInst(fcda.getLdInst());
@@ -285,10 +291,16 @@ public class SclGeneratedModelReader {
return node;
}
/**
* 读取 JAXB 列表型字段的首个值。
*/
private String first(List<String> values) {
return values == null || values.isEmpty() ? null : values.get(0);
}
/**
* 从 LD 实例名中提取非数字前缀,例如 PQM1 -> PQM。
*/
private String extractLdPrefix(String ldInst) {
if (ldInst == null) {
return null;
@@ -305,19 +317,16 @@ public class SclGeneratedModelReader {
}
/**
* DOI 的 sequenceCount 同步出来
* 回填 DOI 的 `sequenceCount`
*
* 规则严格贴近原 C#
* 1. 优先从 DataTypeTemplates -> LNodeType -> DO -> DOType -> DA.count 取值;
* 2. 如果模板里没有 count再退回 DataSet/FCDA 反查;
* 3. 这样后续 MappingGenerationService 里的 icdcout 才会和 C# 一致。
* 优先按模板链路取值,模板缺失时再退回 `DataSet/FCDA` 反查,确保后续生成结果与原 C# 逻辑一致。
*/
private void syncDoiSequenceCount(SCL scl, IcdDocument document) {
if (document == null || document.getLogicalNodes() == null) {
return;
}
// 先把 “lnType + doName -> count” 建好缓存
// 先构建 `lnType + doName -> count` 缓存
Map<String, Integer> templateSequenceCountMap = buildTemplateSequenceCountMap(scl);
for (LnNode lnNode : document.getLogicalNodes()) {
@@ -338,23 +347,17 @@ public class SclGeneratedModelReader {
int fcdaCount = findDoiSequenceCountFromFcda(document, doiNode);
// 关键:优先使用模板 count保持与 C# 原版一致
// 优先使用模板中的 `count`,保持与 C# 行为一致
doiNode.setSequenceCount(templateCount > 0 ? templateCount : fcdaCount);
}
}
}
/**
* 构建 lnType + doName -> 序列数量 缓存。
* 构建 `lnType + doName -> 序列数量` 缓存。
*
* 来源:
* DataTypeTemplates
* -> LNodeType(id = lnType)
* -> DO(name/type)
* -> DOType(id = type)
* -> DA.count
*
* C# 原版本质上就是沿这条链把 doi.tNUM 算出来。
* 数据来源为 `DataTypeTemplates -> LNodeType -> DO -> DOType -> DA.count`
* 原 C# 版本也是沿着这条链路计算 `doi.tNUM`。
*/
private Map<String, Integer> buildTemplateSequenceCountMap(SCL scl) {
Map<String, Integer> result = new LinkedHashMap<String, Integer>();
@@ -367,7 +370,7 @@ public class SclGeneratedModelReader {
return result;
}
// 1. 先建 DOType.id -> count
// 1. 先`DOType.id -> count`。
Map<String, Integer> doTypeCountMap = new LinkedHashMap<String, Integer>();
for (TDOType doType : templates.getDOType()) {
if (doType == null || doType.getId() == null) {
@@ -378,7 +381,7 @@ public class SclGeneratedModelReader {
doTypeCountMap.put(doType.getId(), count);
}
// 2. 再建 lnType + doName -> count
// 2. 再`lnType + doName -> count`。
for (TLNodeType lNodeType : templates.getLNodeType()) {
if (lNodeType == null || lNodeType.getId() == null || lNodeType.getDO() == null) {
continue;
@@ -402,10 +405,9 @@ public class SclGeneratedModelReader {
}
/**
* 从个 DOType 中提取序列数量。
* 从`DOType` 中提取序列数量。
*
* C# 原版用的是 DOType 下 DA.count
* 这里取所有顶层 DA 里最大的正整数 count。
* 这里取顶层 `DA.count` 中最大的正整数,贴近原 C# 的处理方式
*/
private int extractDoTypeSequenceCount(TDOType doType) {
int max = 0;
@@ -429,10 +431,9 @@ public class SclGeneratedModelReader {
}
/**
* 解析 DA.count。
* 解析 `DA.count`
*
* JAXB 这套生成类把 count 生成为 List<String>
* 所以这里要做一次安全转换。
* 当前 JAXB 生成类把该字段定义`List<String>`,这里统一做安全转换。
*/
private int parseDaCount(TAbstractDataAttribute dataAttribute) {
if (dataAttribute == null || dataAttribute.getCount() == null || dataAttribute.getCount().isEmpty()) {
@@ -456,16 +457,16 @@ public class SclGeneratedModelReader {
max = value;
}
} catch (NumberFormatException ignore) {
// 非法 count 直接忽略,保持容错
// 非法 `count` 直接忽略,保持容错
}
}
return max;
}
/**
* 退回到 DataSet/FCDAsequenceCount 反查
* 从 `DataSet/FCDA` 反查 `sequenceCount`
*
* 这里只作为模板 count 找不到时的兜底逻辑。
* 这里只在模板未提供 `count` 时作为兜底逻辑使用
*/
private int findDoiSequenceCountFromFcda(IcdDocument document, DoiNode doiNode) {
int max = 0;
@@ -501,6 +502,9 @@ public class SclGeneratedModelReader {
return max;
}
/**
* 构造模板序列数量缓存使用的复合 key。
*/
private String buildLnTypeDoKey(String lnType, String doName) {
String left = lnType == null ? "" : lnType.trim();
String right = doName == null ? "" : doName.trim();
@@ -508,7 +512,7 @@ public class SclGeneratedModelReader {
}
/**
* 空安全字符串比较。
* 空安全字符串比较。
*/
private boolean equalsTrim(String left, String right) {
if (left == null && right == null) {

View File

@@ -1,6 +1,6 @@
package com.njcn.gather.icd.mapping.infrastructure.parser;
import com.njcn.gather.icd.mapping.domain.model.icd.IcdDocument;
import com.njcn.gather.icd.mapping.pojo.bo.icd.IcdDocument;
import com.njcn.gather.icd.mapping.infrastructure.parser.generated.SCL;
import org.springframework.stereotype.Component;
@@ -11,18 +11,18 @@ import java.io.ByteArrayInputStream;
/**
* SCL 解析适配器。
* JAXB 解析适配器。负责把 ICD XML 反序列化为 SCL 根对象,再转成内部模型。
*
* 说明:
* 1. 这是真正会用到的 JAXB 版解析入口。
* 2. 这里只负责把 ICD XML 反序列化成 SCL 根对象,再交给 reader 转成内部模型。
* 3. 业务层不会直接依赖 JAXB generated 类。
* 负责把 ICD XML 反序列化为 SCL 根对象,再交给读取器转换为内部模型。
*/
@Component
public class SclParserAdapter {
/** JAXB generated 模型读取器,负责转成内部 ICD 领域模型。 */
private final SclGeneratedModelReader modelReader = new SclGeneratedModelReader();
/**
* 解析 ICD/SCL XML 字节内容。
*/
public IcdDocument parse(byte[] content, String fileName) {
if (content == null || content.length == 0) {
throw new IllegalArgumentException("ICD 文件内容不能为空");
@@ -38,6 +38,9 @@ public class SclParserAdapter {
}
}
/**
* 兼容 JAXB 直接返回 SCL 或 JAXBElement<SCL> 两种根对象形式。
*/
private SCL resolveSclRoot(Object raw) {
if (raw instanceof SCL) {
return (SCL) raw;

View File

@@ -1,14 +1,16 @@
package com.njcn.gather.icd.mapping.infrastructure.parser;
import com.njcn.gather.icd.mapping.domain.model.icd.DoiElementNode;
import com.njcn.gather.icd.mapping.domain.model.icd.DoiNode;
import com.njcn.gather.icd.mapping.domain.model.icd.FcdaNode;
import com.njcn.gather.icd.mapping.pojo.bo.icd.DoiElementNode;
import com.njcn.gather.icd.mapping.pojo.bo.icd.DoiNode;
import com.njcn.gather.icd.mapping.pojo.bo.icd.FcdaNode;
import java.util.ArrayList;
import java.util.List;
/**
* SCL 遍历辅助工具。
*
* 提供序列数量统计和 DOI 叶子节点展开等公共能力。
*/
public final class SclTraversalSupport {
@@ -16,7 +18,7 @@ public final class SclTraversalSupport {
}
/**
* 统计同一数据对象在数据集出现次数,作为 ICD 实际序列数参考。
* 统计同一数据对象在数据集中的出现次数,作为 ICD 实际序列数量的参考
*/
public static int calculateSequenceCount(List<FcdaNode> allFcdas, FcdaNode current) {
int count = 0;
@@ -30,6 +32,9 @@ public final class SclTraversalSupport {
return count <= 1 ? 0 : count;
}
/**
* 展开 DOI 树下所有 DAI 节点,便于后续读取单位、倍率等叶子值。
*/
public static List<DoiElementNode> flattenLeafDai(DoiNode doi) {
List<DoiElementNode> result = new ArrayList<DoiElementNode>();
if (doi == null || doi.getChildren() == null) {
@@ -41,6 +46,9 @@ public final class SclTraversalSupport {
return result;
}
/**
* 递归收集 DAI 叶子节点。
*/
private static void collectLeafDai(DoiElementNode node, List<DoiElementNode> result) {
if (node == null) {
return;
@@ -55,6 +63,9 @@ public final class SclTraversalSupport {
}
}
/**
* 空安全字符串比较,供解析阶段统计序列数量使用。
*/
public static boolean safeEquals(String a, String b) {
return a == null ? b == null : a.equals(b);
}

View File

@@ -0,0 +1,48 @@
package com.njcn.gather.icd.mapping.pojo.bo;
import lombok.Data;
import com.njcn.gather.icd.mapping.pojo.bo.analysis.IndexAnalysis;
import com.njcn.gather.icd.mapping.pojo.bo.icd.IcdDocument;
import com.njcn.gather.icd.mapping.pojo.bo.mapping.MappingDocument;
import com.njcn.gather.icd.mapping.pojo.enums.GenerateStatus;
import java.util.ArrayList;
import java.util.List;
/**
* 应用层返回对象。
*
* 统一封装成功、需选择索引、失败三类结果。
*/
@Data
public class GenerateMappingResult {
/** 本次生成流程状态。 */
private GenerateStatus status;
/** 给前端或调用方展示的处理结果说明。 */
private String message;
/** ICD 中解析到的 IED 名称。 */
private String iedName;
/** ICD 中解析到的 LD 实例名。 */
private String ldInst;
/** 前端可编辑的 ICD 解析结果。 */
private IcdDocument icdDocument;
/** 需要人工绑定索引时返回的候选分析结果。 */
private IndexAnalysis indexAnalysis;
/** 生成成功后的结构化映射文档。 */
private MappingDocument mappingDocument;
/** 生成成功后的 JSON 字符串。 */
private String mappingJson;
/** saveToDisk=true 时的文件保存路径。 */
private String savedPath;
/** 解析、校验或生成过程中收集到的问题。 */
private List<String> problems = new ArrayList<String>();
}

View File

@@ -1,5 +1,6 @@
package com.njcn.gather.icd.mapping.domain.model.analysis;
package com.njcn.gather.icd.mapping.pojo.bo.analysis;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@@ -9,12 +10,11 @@ import java.util.List;
*
* key = reportName
*/
@Data
public class IndexAnalysis {
/** 按 DefaultCfg.ReportList 分组后的索引候选。 */
private List<IndexCandidate> candidates = new ArrayList<IndexCandidate>();
private List<String> problems = new ArrayList<String>();
public List<IndexCandidate> getCandidates() { return candidates; }
public void setCandidates(List<IndexCandidate> candidates) { this.candidates = candidates; }
public List<String> getProblems() { return problems; }
public void setProblems(List<String> problems) { this.problems = problems; }
/** 分析过程中发现但不一定阻断流程的问题。 */
private List<String> problems = new ArrayList<String>();
}

View File

@@ -0,0 +1,51 @@
package com.njcn.gather.icd.mapping.pojo.bo.analysis;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 索引候选分组。
*
* 说明:
* 1. 一条候选对应一个业务分组,例如:统计数据、实时数据;
* 2. 一个业务分组下可以包含多个报告;
* 3. 这里不仅保存返回给前端的候选项,也保存从 DefaultCfg.ReportList 带下来的配置项,
* 供后续 MappingGenerationService 直接使用,避免“二次查模板”失败。
*/
@Data
public class IndexCandidate {
/** 分组唯一键。 */
private String groupKey;
/** 分组描述。 */
private String groupDesc;
/** 该分组下实际匹配到的报告数量。 */
private int reportCount;
/** DefaultCfg.txt 中该分组可用的标签模板。 */
private List<String> templateLabels = new ArrayList<String>();
/** 当前分组下匹配到的报告列表。 */
private List<IndexCandidateReportItem> reports = new ArrayList<IndexCandidateReportItem>();
/**
* DefaultCfg.ReportList.inst
* 例如01 / 02 / 03 / 04
*/
private String reportInst;
/**
* DefaultCfg.ReportList.Select
* 例如DataStatFileMap / DataRealFileMap / FlickerFileMap
*/
private String select;
/**
* DefaultCfg.ReportList.TrgOps
* 例如40 / 96
*/
private String trgOps;
}

Some files were not shown because too many files have changed in this diff Show More