diff --git a/README.md b/README.md index 78d6454..08a7f53 100644 --- a/README.md +++ b/README.md @@ -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` diff --git a/entrance/pom.xml b/entrance/pom.xml index c5cdff1..24d6a1d 100644 --- a/entrance/pom.xml +++ b/entrance/pom.xml @@ -16,6 +16,11 @@ system 1.0.0 + + com.njcn.gather + disk-monitor + 1.0.0 + com.njcn.gather detection diff --git a/pom.xml b/pom.xml index ce81522..0953afe 100644 --- a/pom.xml +++ b/pom.xml @@ -12,6 +12,7 @@ entrance system + systemmonitor user detection tools diff --git a/systemmonitor/README.md b/systemmonitor/README.md new file mode 100644 index 0000000..36e09ac --- /dev/null +++ b/systemmonitor/README.md @@ -0,0 +1,29 @@ +# System Monitor 模块说明 + +## 当前状态 + +`systemmonitor` 当前作为系统监控能力聚合模块使用。 + +当前真实保留的子模块有: + +- `disk-monitor` + +## 当前结构 + +```text +systemmonitor/ +└── disk-monitor/ +``` + +## disk-monitor 的职责 + +`disk-monitor` 预留用于承载磁盘监控相关能力,后续可在该模块内继续补充: + +- 磁盘监控配置管理 +- 磁盘巡检任务 +- 磁盘容量计算与阈值判定 +- 磁盘预警与告警留痕 + +## 模块定位 + +当前将磁盘监控拆分到 `systemmonitor/disk-monitor`,目的是避免将系统监控扩展逻辑直接混入 `system` 公共基础模块,便于后续继续扩展 CPU、内存或进程级监控能力。 diff --git a/systemmonitor/disk-monitor/README.md b/systemmonitor/disk-monitor/README.md new file mode 100644 index 0000000..ab9683c --- /dev/null +++ b/systemmonitor/disk-monitor/README.md @@ -0,0 +1,11 @@ +# disk-monitor + +`disk-monitor` 模块用于承载磁盘监控相关代码。 + +当前这一版只完成模块骨架接入,后续可以在该模块内继续补充: + +- Controller +- Service +- 定时巡检任务 +- 磁盘容量采集与阈值判断 +- 日志留痕与 SQL 脚本 diff --git a/systemmonitor/disk-monitor/pom.xml b/systemmonitor/disk-monitor/pom.xml new file mode 100644 index 0000000..a1333d8 --- /dev/null +++ b/systemmonitor/disk-monitor/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + + + com.njcn.gather + systemmonitor + 1.0.0 + + + disk-monitor + jar + disk-monitor + + + + com.njcn + njcn-common + 0.0.1 + + + + com.njcn + mybatis-plus + 0.0.1 + + + + com.njcn + spingboot2.3.12 + 2.3.12 + + + + diff --git a/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/component/DiskMonitorNotificationComponent.java b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/component/DiskMonitorNotificationComponent.java new file mode 100644 index 0000000..b8d2ae7 --- /dev/null +++ b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/component/DiskMonitorNotificationComponent.java @@ -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> PATH_LIST_TYPE = new TypeReference>() { + }; + private static final TypeReference> HTTP_LIST_TYPE = new TypeReference>() { + }; + + 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 pathItems; + List 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 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 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 buildNotifyPayload(DiskMonitorJob job, DiskMonitorTarget target, BigDecimal usedPercent, + String currentStatus, String notifyReason, String notifyLevel, + LocalDateTime scanTime, String message) { + Map 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 parsePathItems(String json) throws Exception { + return readJsonList(json, PATH_LIST_TYPE); + } + + private List parseHttpItems(String json) throws Exception { + return readJsonList(json, HTTP_LIST_TYPE); + } + + private List readJsonList(String json, TypeReference> typeReference) throws Exception { + if (StrUtil.isBlank(json)) { + return new ArrayList<>(); + } + List 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); + } +} diff --git a/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/config/DiskMonitorModuleConfig.java b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/config/DiskMonitorModuleConfig.java new file mode 100644 index 0000000..d2a8b38 --- /dev/null +++ b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/config/DiskMonitorModuleConfig.java @@ -0,0 +1,10 @@ +package com.njcn.gather.systemmonitor.disk.config; + +import org.springframework.context.annotation.Configuration; + +/** + * 磁盘监控模块基础配置入口,后续磁盘监控相关 Bean 统一放在该模块下扩展。 + */ +@Configuration +public class DiskMonitorModuleConfig { +} diff --git a/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/constant/DiskMonitorConstant.java b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/constant/DiskMonitorConstant.java new file mode 100644 index 0000000..02973a9 --- /dev/null +++ b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/constant/DiskMonitorConstant.java @@ -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; +} diff --git a/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/constant/DiskMonitorValidMessage.java b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/constant/DiskMonitorValidMessage.java new file mode 100644 index 0000000..cd792c7 --- /dev/null +++ b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/constant/DiskMonitorValidMessage.java @@ -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参数"; +} diff --git a/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/controller/DiskMonitorJobController.java b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/controller/DiskMonitorJobController.java new file mode 100644 index 0000000..5c45ace --- /dev/null +++ b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/controller/DiskMonitorJobController.java @@ -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 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> 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 jobDetail(@PathVariable("jobId") Long jobId) { + String methodDescribe = getMethodDescribe("jobDetail"); + LogUtil.njcnDebug(log, "{},查询磁盘监控任务详情,jobId={}", methodDescribe, jobId); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, diskMonitorJobService.getJobDetail(jobId), methodDescribe); + } +} diff --git a/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/controller/DiskMonitorNotifyController.java b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/controller/DiskMonitorNotifyController.java new file mode 100644 index 0000000..7cba386 --- /dev/null +++ b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/controller/DiskMonitorNotifyController.java @@ -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 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); + } +} diff --git a/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/controller/DiskMonitorPolicyController.java b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/controller/DiskMonitorPolicyController.java new file mode 100644 index 0000000..8487ae1 --- /dev/null +++ b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/controller/DiskMonitorPolicyController.java @@ -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 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 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); + } +} diff --git a/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/enums/DiskMonitorResponseEnum.java b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/enums/DiskMonitorResponseEnum.java new file mode 100644 index 0000000..74c4001 --- /dev/null +++ b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/enums/DiskMonitorResponseEnum.java @@ -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; + } +} diff --git a/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/event/DiskMonitorPolicyChangedEvent.java b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/event/DiskMonitorPolicyChangedEvent.java new file mode 100644 index 0000000..633d2a3 --- /dev/null +++ b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/event/DiskMonitorPolicyChangedEvent.java @@ -0,0 +1,7 @@ +package com.njcn.gather.systemmonitor.disk.event; + +/** + * 磁盘监控策略变更事件。 + */ +public class DiskMonitorPolicyChangedEvent { +} diff --git a/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/mapper/DiskMonitorJobMapper.java b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/mapper/DiskMonitorJobMapper.java new file mode 100644 index 0000000..b713ad9 --- /dev/null +++ b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/mapper/DiskMonitorJobMapper.java @@ -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 { +} diff --git a/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/mapper/DiskMonitorNotifyLogMapper.java b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/mapper/DiskMonitorNotifyLogMapper.java new file mode 100644 index 0000000..8c7970c --- /dev/null +++ b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/mapper/DiskMonitorNotifyLogMapper.java @@ -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 { +} diff --git a/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/mapper/DiskMonitorPolicyMapper.java b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/mapper/DiskMonitorPolicyMapper.java new file mode 100644 index 0000000..2e01dc6 --- /dev/null +++ b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/mapper/DiskMonitorPolicyMapper.java @@ -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 { +} diff --git a/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/mapper/DiskMonitorResultMapper.java b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/mapper/DiskMonitorResultMapper.java new file mode 100644 index 0000000..980560c --- /dev/null +++ b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/mapper/DiskMonitorResultMapper.java @@ -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 { +} diff --git a/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/mapper/DiskMonitorTargetMapper.java b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/mapper/DiskMonitorTargetMapper.java new file mode 100644 index 0000000..b3b1c02 --- /dev/null +++ b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/mapper/DiskMonitorTargetMapper.java @@ -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 { +} diff --git a/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/mapper/mapping/DiskMonitorJobMapper.xml b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/mapper/mapping/DiskMonitorJobMapper.xml new file mode 100644 index 0000000..c1620f1 --- /dev/null +++ b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/mapper/mapping/DiskMonitorJobMapper.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/mapper/mapping/DiskMonitorNotifyLogMapper.xml b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/mapper/mapping/DiskMonitorNotifyLogMapper.xml new file mode 100644 index 0000000..409e8b1 --- /dev/null +++ b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/mapper/mapping/DiskMonitorNotifyLogMapper.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/mapper/mapping/DiskMonitorPolicyMapper.xml b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/mapper/mapping/DiskMonitorPolicyMapper.xml new file mode 100644 index 0000000..7b964d0 --- /dev/null +++ b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/mapper/mapping/DiskMonitorPolicyMapper.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/mapper/mapping/DiskMonitorResultMapper.xml b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/mapper/mapping/DiskMonitorResultMapper.xml new file mode 100644 index 0000000..ce0d629 --- /dev/null +++ b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/mapper/mapping/DiskMonitorResultMapper.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/mapper/mapping/DiskMonitorTargetMapper.xml b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/mapper/mapping/DiskMonitorTargetMapper.xml new file mode 100644 index 0000000..228061a --- /dev/null +++ b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/mapper/mapping/DiskMonitorTargetMapper.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/pojo/dto/DiskMonitorNotifyHttpItem.java b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/pojo/dto/DiskMonitorNotifyHttpItem.java new file mode 100644 index 0000000..c4717bb --- /dev/null +++ b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/pojo/dto/DiskMonitorNotifyHttpItem.java @@ -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; +} diff --git a/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/pojo/dto/DiskMonitorNotifyPathItem.java b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/pojo/dto/DiskMonitorNotifyPathItem.java new file mode 100644 index 0000000..4fb6db4 --- /dev/null +++ b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/pojo/dto/DiskMonitorNotifyPathItem.java @@ -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; +} diff --git a/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/pojo/param/DiskMonitorParam.java b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/pojo/param/DiskMonitorParam.java new file mode 100644 index 0000000..36ed66c --- /dev/null +++ b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/pojo/param/DiskMonitorParam.java @@ -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 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 notifyPathList = new ArrayList<>(); + + @ApiModelProperty("是否启用HTTP通知") + private Boolean notifyHttpEnabled; + + @ApiModelProperty("HTTP通知列表") + @Valid + private List 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; + } +} diff --git a/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/pojo/po/DiskMonitorJob.java b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/pojo/po/DiskMonitorJob.java new file mode 100644 index 0000000..6f52f43 --- /dev/null +++ b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/pojo/po/DiskMonitorJob.java @@ -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; +} diff --git a/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/pojo/po/DiskMonitorNotifyLog.java b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/pojo/po/DiskMonitorNotifyLog.java new file mode 100644 index 0000000..ae8a920 --- /dev/null +++ b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/pojo/po/DiskMonitorNotifyLog.java @@ -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; +} diff --git a/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/pojo/po/DiskMonitorPolicy.java b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/pojo/po/DiskMonitorPolicy.java new file mode 100644 index 0000000..f362aec --- /dev/null +++ b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/pojo/po/DiskMonitorPolicy.java @@ -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; +} diff --git a/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/pojo/po/DiskMonitorResult.java b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/pojo/po/DiskMonitorResult.java new file mode 100644 index 0000000..530fdf0 --- /dev/null +++ b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/pojo/po/DiskMonitorResult.java @@ -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; +} diff --git a/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/pojo/po/DiskMonitorTarget.java b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/pojo/po/DiskMonitorTarget.java new file mode 100644 index 0000000..23f24ad --- /dev/null +++ b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/pojo/po/DiskMonitorTarget.java @@ -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; +} diff --git a/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/pojo/vo/DiskMonitorVO.java b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/pojo/vo/DiskMonitorVO.java new file mode 100644 index 0000000..5ae233f --- /dev/null +++ b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/pojo/vo/DiskMonitorVO.java @@ -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 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 notifyPathList = new ArrayList<>(); + private Boolean notifyHttpEnabled; + private List 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 results = new ArrayList<>(); + private List 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; + } +} diff --git a/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/schedule/DiskMonitorScheduleManager.java b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/schedule/DiskMonitorScheduleManager.java new file mode 100644 index 0000000..f48005f --- /dev/null +++ b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/schedule/DiskMonitorScheduleManager.java @@ -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(); + } +} diff --git a/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/service/IDiskMonitorJobService.java b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/service/IDiskMonitorJobService.java new file mode 100644 index 0000000..6671ecc --- /dev/null +++ b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/service/IDiskMonitorJobService.java @@ -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 listJobs(DiskMonitorParam.JobListParam param); + + DiskMonitorVO.JobDetailVO getJobDetail(Long jobId); + + void executeAppStartMonitor(); + + void executeDailyScheduleMonitor(); + + LocalDateTime getNextDailyRunTime(); + + void executeNotifyTest(String driveLetter); +} diff --git a/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/service/IDiskMonitorNotifyService.java b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/service/IDiskMonitorNotifyService.java new file mode 100644 index 0000000..e740501 --- /dev/null +++ b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/service/IDiskMonitorNotifyService.java @@ -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); +} diff --git a/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/service/IDiskMonitorPolicyService.java b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/service/IDiskMonitorPolicyService.java new file mode 100644 index 0000000..c99350e --- /dev/null +++ b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/service/IDiskMonitorPolicyService.java @@ -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 listEnabledTargets(); + + DiskMonitorTarget getTargetByDriveLetter(String driveLetter); +} diff --git a/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/service/impl/DiskMonitorJobServiceImpl.java b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/service/impl/DiskMonitorJobServiceImpl.java new file mode 100644 index 0000000..65d51b0 --- /dev/null +++ b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/service/impl/DiskMonitorJobServiceImpl.java @@ -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 listJobs(DiskMonitorParam.JobListParam param) { + Page page = jobMapper.selectPage( + new Page<>(PageFactory.getPageNum(param), PageFactory.getPageSize(param)), + new QueryWrapper().orderByDesc("started_at", "id") + ); + Page 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 results = resultMapper.selectList(new QueryWrapper() + .eq("job_id", jobId) + .orderByAsc("scan_time", "id")); + detailVO.setResults(results.stream().map(this::toResultVO).collect(Collectors.toList())); + List notifyLogs = notifyLogMapper.selectList(new QueryWrapper() + .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 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 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 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 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; + } + } +} diff --git a/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/service/impl/DiskMonitorNotifyServiceImpl.java b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/service/impl/DiskMonitorNotifyServiceImpl.java new file mode 100644 index 0000000..77bef26 --- /dev/null +++ b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/service/impl/DiskMonitorNotifyServiceImpl.java @@ -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; + } +} diff --git a/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/service/impl/DiskMonitorPolicyServiceImpl.java b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/service/impl/DiskMonitorPolicyServiceImpl.java new file mode 100644 index 0000000..489067e --- /dev/null +++ b/systemmonitor/disk-monitor/src/main/java/com/njcn/gather/systemmonitor/disk/service/impl/DiskMonitorPolicyServiceImpl.java @@ -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> PATH_LIST_TYPE = new TypeReference>() { + }; + private static final TypeReference> HTTP_LIST_TYPE = new TypeReference>() { + }; + + 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 existingTargets = listTargetsByPolicyId(currentPolicy.getId()); + Map existingById = existingTargets.stream() + .collect(Collectors.toMap(DiskMonitorTarget::getId, item -> item, (left, right) -> left, LinkedHashMap::new)); + Map existingByDrive = existingTargets.stream() + .collect(Collectors.toMap(item -> normalizeDriveLetter(item.getDriveLetter()), item -> item, (left, right) -> left, LinkedHashMap::new)); + + List 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 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().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 listEnabledTargets() { + return targetMapper.selectList(new QueryWrapper() + .eq("monitor_enabled", 1) + .orderByAsc("drive_letter", "id")); + } + + @Override + public DiskMonitorTarget getTargetByDriveLetter(String driveLetter) { + return targetMapper.selectOne(new QueryWrapper() + .eq("drive_letter", normalizeDriveLetter(driveLetter)) + .last("LIMIT 1")); + } + + private List listTargetsByPolicyId(Long policyId) { + return targetMapper.selectList(new QueryWrapper() + .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 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 parsePathItems(String json) { + return readJsonList(json, PATH_LIST_TYPE); + } + + private List parseHttpItems(String json) { + return readJsonList(json, HTTP_LIST_TYPE); + } + + private List readJsonList(String json, TypeReference> typeReference) { + if (StrUtil.isBlank(json)) { + return new ArrayList<>(); + } + try { + List 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); + } +} diff --git a/systemmonitor/disk-monitor/src/main/resources/sql/disk-monitor/disk-monitor-init.sql b/systemmonitor/disk-monitor/src/main/resources/sql/disk-monitor/disk-monitor-init.sql new file mode 100644 index 0000000..13c0f55 --- /dev/null +++ b/systemmonitor/disk-monitor/src/main/resources/sql/disk-monitor/disk-monitor-init.sql @@ -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='磁盘监控通知日志表'; diff --git a/systemmonitor/pom.xml b/systemmonitor/pom.xml new file mode 100644 index 0000000..6cf262a --- /dev/null +++ b/systemmonitor/pom.xml @@ -0,0 +1,22 @@ + + + 4.0.0 + + + com.njcn.gather + CN_Tool + 1.0.0 + + + systemmonitor + pom + systemmonitor + System monitor capability aggregator. + + + disk-monitor + + + diff --git a/tools/mms-mapping/API-getIcdMmsJson.md b/tools/mms-mapping/API-getIcdMmsJson.md new file mode 100644 index 0000000..682a0f0 --- /dev/null +++ b/tools/mms-mapping/API-getIcdMmsJson.md @@ -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`,而不是 `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` 响应,具体字段结构以全局公共响应定义为准,本文不展开其完整协议,只强调: + +- 不能把这类错误等同理解为 `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` 为结构化示意,实际字段数量与内容以运行结果为准 diff --git a/tools/mms-mapping/README.md b/tools/mms-mapping/README.md new file mode 100644 index 0000000..eb4db9c --- /dev/null +++ b/tools/mms-mapping/README.md @@ -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` 为结构化示意,实际字段数量以运行结果为准。 diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/application/MappingTaskAppService.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/application/MappingTaskAppService.java deleted file mode 100644 index 41ed340..0000000 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/application/MappingTaskAppService.java +++ /dev/null @@ -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"); - } -} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/application/command/GenerateFromIcdCommand.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/application/command/GenerateFromIcdCommand.java deleted file mode 100644 index 23d7479..0000000 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/application/command/GenerateFromIcdCommand.java +++ /dev/null @@ -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 indexSelection = new ArrayList(); - - 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 getIndexSelection() { - return indexSelection; - } - - public void setIndexSelection(List indexSelection) { - this.indexSelection = indexSelection; - } -} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/application/command/IndexBindingCommand.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/application/command/IndexBindingCommand.java deleted file mode 100644 index 44a065b..0000000 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/application/command/IndexBindingCommand.java +++ /dev/null @@ -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; - } -} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/application/command/IndexSelectionGroupCommand.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/application/command/IndexSelectionGroupCommand.java deleted file mode 100644 index ad7e239..0000000 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/application/command/IndexSelectionGroupCommand.java +++ /dev/null @@ -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 bindings = new ArrayList(); - - 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 getBindings() { - return bindings; - } - - public void setBindings(List bindings) { - this.bindings = bindings; - } -} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/application/result/GenerateMappingResult.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/application/result/GenerateMappingResult.java deleted file mode 100644 index 1330121..0000000 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/application/result/GenerateMappingResult.java +++ /dev/null @@ -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 problems = new ArrayList(); - - 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 getProblems() { return problems; } - public void setProblems(List problems) { this.problems = problems; } -} \ No newline at end of file diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/service/DefaultTemplateLoader.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/DefaultTemplateLoader.java similarity index 85% rename from tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/service/DefaultTemplateLoader.java rename to tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/DefaultTemplateLoader.java index 852ea08..2aea155 100644 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/service/DefaultTemplateLoader.java +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/DefaultTemplateLoader.java @@ -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()) { diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/infrastructure/storage/FileStorageService.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/FileStorageService.java similarity index 80% rename from tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/infrastructure/storage/FileStorageService.java rename to tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/FileStorageService.java index d76b74a..c6f41e7 100644 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/infrastructure/storage/FileStorageService.java +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/FileStorageService.java @@ -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()); diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/service/IcdParserService.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/IcdParserService.java similarity index 67% rename from tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/service/IcdParserService.java rename to tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/IcdParserService.java index 729fd63..d0f3041 100644 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/service/IcdParserService.java +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/IcdParserService.java @@ -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); } diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/service/IndexAnalysisService.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/IndexAnalysisService.java similarity index 78% rename from tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/service/IndexAnalysisService.java rename to tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/IndexAnalysisService.java index 3365211..03f73fa 100644 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/service/IndexAnalysisService.java +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/IndexAnalysisService.java @@ -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 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 collectMatchedReports(IcdDocument icdDocument, DefaultTemplate.ReportCfgItem reportCfg) { List result = new ArrayList(); 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 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("_+", "_") diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/service/IndexValidationService.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/IndexValidationService.java similarity index 81% rename from tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/service/IndexValidationService.java rename to tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/IndexValidationService.java index eb10544..1284afe 100644 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/service/IndexValidationService.java +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/IndexValidationService.java @@ -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 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; diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/infrastructure/serializer/MappingDocumentSerializer.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/MappingDocumentSerializer.java similarity index 78% rename from tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/infrastructure/serializer/MappingDocumentSerializer.java rename to tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/MappingDocumentSerializer.java index 7ce88bc..a58877d 100644 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/infrastructure/serializer/MappingDocumentSerializer.java +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/MappingDocumentSerializer.java @@ -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); diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/service/MappingGenerationService.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/MappingGenerationService.java similarity index 91% rename from tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/service/MappingGenerationService.java rename to tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/MappingGenerationService.java index 0451a79..4f58967 100644 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/service/MappingGenerationService.java +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/MappingGenerationService.java @@ -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 dataSetGroupMap = new LinkedHashMap(); 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 findDataObjectCfgItems(DefaultTemplate template, String label) { List result = new ArrayList(); if (template == null || template.getDataObjectsList() == null) { @@ -639,6 +657,11 @@ public class MappingGenerationService { } } + /** + * 解析 DOI 的倍率系数。 + * + * 找不到模板倍率配置时默认返回 1.0。 + */ private float resolveCoefficient(DoiNode doiNode, DefaultTemplate template) { Set 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 collectAllLeafValues(List nodes) { Set result = new LinkedHashSet(); 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; } diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/MappingRequestConverter.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/MappingRequestConverter.java new file mode 100644 index 0000000..f2fb8ab --- /dev/null +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/MappingRequestConverter.java @@ -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 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(); + } +} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/MappingResponseConverter.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/MappingResponseConverter.java new file mode 100644 index 0000000..0b63e05 --- /dev/null +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/MappingResponseConverter.java @@ -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); + } + } +} \ No newline at end of file diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/config/MappingModuleConfig.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/config/MappingModuleConfig.java index 3021ca7..23d56d6 100644 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/config/MappingModuleConfig.java +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/config/MappingModuleConfig.java @@ -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; - } -} \ No newline at end of file +} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/controller/MappingController.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/controller/MappingController.java index 292057d..7e21674 100644 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/controller/MappingController.java +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/controller/MappingController.java @@ -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. icdFile:ICD 文件 - * 2. request:JSON 请求体 + * 根据前端确认后的索引绑定关系生成 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 JSON,indexSelectionCount={}", + 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 JSON,fileName={}, 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); } } diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/controller/request/GenerateMappingFromIcdRequest.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/controller/request/GenerateMappingFromIcdRequest.java deleted file mode 100644 index e9f86de..0000000 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/controller/request/GenerateMappingFromIcdRequest.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.njcn.gather.icd.mapping.controller.request; - -import java.util.ArrayList; -import java.util.List; - -/** - * 生成映射接口请求体。 - * - * 说明: - * 1. 旧版结构中,indexSelection 是 Map,只能表达“一个报告对应一个值”, - * 无法表达“一个业务分组下有多个报告、一个报告下又有多条标签绑定”的真实场景。 - * 2. 新版结构改成 List,用来完整承载用户在前端完成的绑定结果。 - * 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 indexSelection = new ArrayList(); - - 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 getIndexSelection() { - return indexSelection; - } - - public void setIndexSelection(List indexSelection) { - this.indexSelection = indexSelection; - } -} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/controller/request/IndexBindingRequest.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/controller/request/IndexBindingRequest.java deleted file mode 100644 index 7f64576..0000000 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/controller/request/IndexBindingRequest.java +++ /dev/null @@ -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; - } -} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/controller/request/IndexSelectionGroupRequest.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/controller/request/IndexSelectionGroupRequest.java deleted file mode 100644 index 5d2b77e..0000000 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/controller/request/IndexSelectionGroupRequest.java +++ /dev/null @@ -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 bindings = new ArrayList(); - - 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 getBindings() { - return bindings; - } - - public void setBindings(List bindings) { - this.bindings = bindings; - } -} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/controller/response/IndexCandidateReportItemResponse.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/controller/response/IndexCandidateReportItemResponse.java deleted file mode 100644 index 687b7ff..0000000 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/controller/response/IndexCandidateReportItemResponse.java +++ /dev/null @@ -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 availableLnInstValues = new ArrayList(); - - 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 getAvailableLnInstValues() { - return availableLnInstValues; - } - - public void setAvailableLnInstValues(List availableLnInstValues) { - this.availableLnInstValues = availableLnInstValues; - } -} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/controller/response/IndexCandidateResponse.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/controller/response/IndexCandidateResponse.java deleted file mode 100644 index d6c9f74..0000000 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/controller/response/IndexCandidateResponse.java +++ /dev/null @@ -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 templateLabels = new ArrayList(); - - /** 当前分组下的报告候选列表。 */ - private List reports = new ArrayList(); - - 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 getTemplateLabels() { - return templateLabels; - } - - public void setTemplateLabels(List templateLabels) { - this.templateLabels = templateLabels; - } - - public List getReports() { - return reports; - } - - public void setReports(List reports) { - this.reports = reports; - } -} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/controller/response/MappingDocumentResponse.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/controller/response/MappingDocumentResponse.java deleted file mode 100644 index 31fcfd9..0000000 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/controller/response/MappingDocumentResponse.java +++ /dev/null @@ -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; } -} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/controller/response/MappingTaskResponse.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/controller/response/MappingTaskResponse.java deleted file mode 100644 index 1b10c01..0000000 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/controller/response/MappingTaskResponse.java +++ /dev/null @@ -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 indexCandidates = new ArrayList(); - private List problems = new ArrayList(); - 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 getIndexCandidates() { return indexCandidates; } - public void setIndexCandidates(List indexCandidates) { this.indexCandidates = indexCandidates; } - public List getProblems() { return problems; } - public void setProblems(List problems) { this.problems = problems; } -} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/converter/MappingRequestConverter.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/converter/MappingRequestConverter.java deleted file mode 100644 index d57d816..0000000 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/converter/MappingRequestConverter.java +++ /dev/null @@ -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(); - } -} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/converter/MappingResponseConverter.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/converter/MappingResponseConverter.java deleted file mode 100644 index ff6844a..0000000 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/converter/MappingResponseConverter.java +++ /dev/null @@ -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; - } -} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/analysis/IndexCandidate.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/analysis/IndexCandidate.java deleted file mode 100644 index 957ca83..0000000 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/analysis/IndexCandidate.java +++ /dev/null @@ -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 templateLabels = new ArrayList(); - - /** 当前分组下匹配到的报告列表。 */ - private List reports = new ArrayList(); - - /** - * 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 getTemplateLabels() { - return templateLabels; - } - - public void setTemplateLabels(List templateLabels) { - this.templateLabels = templateLabels; - } - - public List getReports() { - return reports; - } - - public void setReports(List 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; - } -} \ No newline at end of file diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/analysis/IndexCandidateReportItem.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/analysis/IndexCandidateReportItem.java deleted file mode 100644 index 3620ae7..0000000 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/analysis/IndexCandidateReportItem.java +++ /dev/null @@ -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 availableLnInstValues = new ArrayList(); - - 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 getAvailableLnInstValues() { - return availableLnInstValues; - } - - public void setAvailableLnInstValues(List availableLnInstValues) { - this.availableLnInstValues = availableLnInstValues; - } -} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/analysis/ValidationResult.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/analysis/ValidationResult.java deleted file mode 100644 index 1ed5c89..0000000 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/analysis/ValidationResult.java +++ /dev/null @@ -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 problems = new ArrayList(); - - public boolean isValid() { return valid; } - public void setValid(boolean valid) { this.valid = valid; } - public List getProblems() { return problems; } - public void setProblems(List problems) { this.problems = problems; } -} \ No newline at end of file diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/icd/DataSetNode.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/icd/DataSetNode.java deleted file mode 100644 index 04e7a31..0000000 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/icd/DataSetNode.java +++ /dev/null @@ -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 fcdas = new ArrayList(); - public String getName() { return name; } - public void setName(String name) { this.name = name; } - public List getFcdas() { return fcdas; } - public void setFcdas(List fcdas) { this.fcdas = fcdas; } -} \ No newline at end of file diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/icd/DoiElementNode.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/icd/DoiElementNode.java deleted file mode 100644 index 23cf301..0000000 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/icd/DoiElementNode.java +++ /dev/null @@ -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 values = new ArrayList(); - private List children = new ArrayList(); - - 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 getValues() { return values; } - public void setValues(List values) { this.values = values; } - public List getChildren() { return children; } - public void setChildren(List children) { this.children = children; } -} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/icd/DoiNode.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/icd/DoiNode.java deleted file mode 100644 index 51f7a37..0000000 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/icd/DoiNode.java +++ /dev/null @@ -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 children = new ArrayList(); - - 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 getChildren() { return children; } - public void setChildren(List children) { this.children = children; } -} \ No newline at end of file diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/icd/FcdaNode.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/icd/FcdaNode.java deleted file mode 100644 index db985bf..0000000 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/icd/FcdaNode.java +++ /dev/null @@ -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; } -} \ No newline at end of file diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/icd/IcdDocument.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/icd/IcdDocument.java deleted file mode 100644 index d3aed97..0000000 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/icd/IcdDocument.java +++ /dev/null @@ -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 logicalNodes = new ArrayList(); - private List reportControls = new ArrayList(); - private Map dataSets = new LinkedHashMap(); - - 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 getLogicalNodes() { return logicalNodes; } - public void setLogicalNodes(List logicalNodes) { this.logicalNodes = logicalNodes; } - public List getReportControls() { return reportControls; } - public void setReportControls(List reportControls) { this.reportControls = reportControls; } - public Map getDataSets() { return dataSets; } - public void setDataSets(Map dataSets) { this.dataSets = dataSets; } -} \ No newline at end of file diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/icd/IedNode.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/icd/IedNode.java deleted file mode 100644 index efc1a35..0000000 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/icd/IedNode.java +++ /dev/null @@ -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 accessPointNames = new ArrayList(); - private List lDeviceInstList = new ArrayList(); - public String getName() { return name; } - public void setName(String name) { this.name = name; } - public List getAccessPointNames() { return accessPointNames; } - public void setAccessPointNames(List accessPointNames) { this.accessPointNames = accessPointNames; } - public List getLDeviceInstList() { return lDeviceInstList; } - public void setLDeviceInstList(List lDeviceInstList) { this.lDeviceInstList = lDeviceInstList; } -} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/icd/LnNode.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/icd/LnNode.java deleted file mode 100644 index f106aa4..0000000 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/icd/LnNode.java +++ /dev/null @@ -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 doiList = new ArrayList(); - - 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 getDoiList() { return doiList; } - public void setDoiList(List doiList) { this.doiList = doiList; } -} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/icd/LogicalDeviceNode.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/icd/LogicalDeviceNode.java deleted file mode 100644 index 07a47c2..0000000 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/icd/LogicalDeviceNode.java +++ /dev/null @@ -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; } -} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/icd/ReportControlNode.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/icd/ReportControlNode.java deleted file mode 100644 index 476295d..0000000 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/icd/ReportControlNode.java +++ /dev/null @@ -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; } -} \ No newline at end of file diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/intermediate/DataSetSelectionState.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/intermediate/DataSetSelectionState.java deleted file mode 100644 index 14d8f84..0000000 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/intermediate/DataSetSelectionState.java +++ /dev/null @@ -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 lnNodes = new ArrayList(); - - 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 getLnNodes() { - return lnNodes; - } - - public void setLnNodes(List lnNodes) { - this.lnNodes = lnNodes; - } -} \ No newline at end of file diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/intermediate/ReportAndDataSetState.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/intermediate/ReportAndDataSetState.java deleted file mode 100644 index d4b7950..0000000 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/intermediate/ReportAndDataSetState.java +++ /dev/null @@ -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 reportGroups = new ArrayList(); - - /** 数据集选择状态列表。 */ - private List dataSetSelections = new ArrayList(); - - 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 getReportGroups() { - return reportGroups; - } - - public void setReportGroups(List reportGroups) { - this.reportGroups = reportGroups; - } - - public List getDataSetSelections() { - return dataSetSelections; - } - - public void setDataSetSelections(List dataSetSelections) { - this.dataSetSelections = dataSetSelections; - } -} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/intermediate/ReportBindingState.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/intermediate/ReportBindingState.java deleted file mode 100644 index e433073..0000000 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/intermediate/ReportBindingState.java +++ /dev/null @@ -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; - } -} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/intermediate/ReportGroupState.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/intermediate/ReportGroupState.java deleted file mode 100644 index 385c303..0000000 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/intermediate/ReportGroupState.java +++ /dev/null @@ -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 reportItems = new ArrayList(); - - /** 用户最终确认的绑定关系。 */ - private List bindings = new ArrayList(); - - 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 getReportItems() { - return reportItems; - } - - public void setReportItems(List reportItems) { - this.reportItems = reportItems; - } - - public List getBindings() { - return bindings; - } - - public void setBindings(List bindings) { - this.bindings = bindings; - } -} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/mapping/DataSetGroupItem.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/mapping/DataSetGroupItem.java deleted file mode 100644 index e6cf35f..0000000 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/mapping/DataSetGroupItem.java +++ /dev/null @@ -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 instList = new ArrayList(); - - 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 getInstList() { - return instList; - } - - public void setInstList(List instList) { - this.instList = instList; - } -} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/mapping/DoiItem.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/mapping/DoiItem.java deleted file mode 100644 index 1aa5f35..0000000 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/mapping/DoiItem.java +++ /dev/null @@ -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 sdiList = new ArrayList(); - - 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 getSdiList() { - return sdiList; - } - - public void setSdiList(List sdiList) { - this.sdiList = sdiList; - } -} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/mapping/InstItem.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/mapping/InstItem.java deleted file mode 100644 index 5e30e02..0000000 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/mapping/InstItem.java +++ /dev/null @@ -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 doiList = new ArrayList(); - - 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 getDoiList() { - return doiList; - } - - public void setDoiList(List doiList) { - this.doiList = doiList; - } -} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/mapping/MappingDocument.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/mapping/MappingDocument.java deleted file mode 100644 index a029389..0000000 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/mapping/MappingDocument.java +++ /dev/null @@ -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 reportMap = new ArrayList(); - - @JsonProperty("DataSetList") - private List dataSetList = new ArrayList(); - - 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 getReportMap() { - return reportMap; - } - - public void setReportMap(List reportMap) { - this.reportMap = reportMap; - } - - public List getDataSetList() { - return dataSetList; - } - - public void setDataSetList(List dataSetList) { - this.dataSetList = dataSetList; - } -} \ No newline at end of file diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/mapping/ReportMapItem.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/mapping/ReportMapItem.java deleted file mode 100644 index 0063d7b..0000000 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/mapping/ReportMapItem.java +++ /dev/null @@ -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; - } -} \ No newline at end of file diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/mapping/SdiItem.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/mapping/SdiItem.java deleted file mode 100644 index 71c5aae..0000000 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/mapping/SdiItem.java +++ /dev/null @@ -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 typeList = new ArrayList(); - - 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 getTypeList() { - return typeList; - } - - public void setTypeList(List typeList) { - this.typeList = typeList; - } -} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/mapping/TypeItem.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/mapping/TypeItem.java deleted file mode 100644 index 81c133f..0000000 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/mapping/TypeItem.java +++ /dev/null @@ -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; - } -} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/template/DefaultTemplate.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/template/DefaultTemplate.java deleted file mode 100644 index b369b60..0000000 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/template/DefaultTemplate.java +++ /dev/null @@ -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 reportList = new ArrayList(); - - @JsonProperty("LnClassList") - private List lnClassList = new ArrayList(); - - @JsonProperty("PhaseList") - private List phaseList = new ArrayList(); - - @JsonProperty("MultiplierList") - private List multiplierList = new ArrayList(); - - @JsonProperty("UnitList") - private List unitList = new ArrayList(); - - @JsonProperty("TypeList") - private List typeList = new ArrayList(); - - @JsonProperty("DataObjectsList") - private List dataObjectsList = new ArrayList(); - - /** - * 基础校验。 - * - * 返回问题列表;为空表示通过。 - */ - public List verify() { - List problems = new ArrayList(); - 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 extractNames(List list) { - List names = new ArrayList(); - 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 names, List problems) { - Set set = new HashSet(); - for (String name : names) { - if (name == null) { - continue; - } - String key = name.trim(); - if (!set.add(key)) { - problems.add(section + " 中存在重复配置项:" + key); - } - } - } - - private void ensureDuplicateObjectNames(List problems) { - if (dataObjectsList == null) { - return; - } - for (DataObjectCfgItem dataObject : dataObjectsList) { - List names = new ArrayList(); - 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 getReportList() { - return reportList; - } - - public void setReportList(List reportList) { - this.reportList = reportList; - } - - public List getLnClassList() { - return lnClassList; - } - - public void setLnClassList(List lnClassList) { - this.lnClassList = lnClassList; - } - - public List getPhaseList() { - return phaseList; - } - - public void setPhaseList(List phaseList) { - this.phaseList = phaseList; - } - - public List getMultiplierList() { - return multiplierList; - } - - public void setMultiplierList(List multiplierList) { - this.multiplierList = multiplierList; - } - - public List getUnitList() { - return unitList; - } - - public void setUnitList(List unitList) { - this.unitList = unitList; - } - - public List getTypeList() { - return typeList; - } - - public void setTypeList(List typeList) { - this.typeList = typeList; - } - - public List getDataObjectsList() { - return dataObjectsList; - } - - public void setDataObjectsList(List dataObjectsList) { - this.dataObjectsList = dataObjectsList; - } - - /** 统一抽象:凡是有 nameList 的配置项都实现它。 */ - public interface NameListSupport { - List 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 dataSetList = new ArrayList(); - - /** - * 该分组可配置的标签模板列表 - */ - @JsonProperty("LnInstList") - private List lnInstList = new ArrayList(); - - 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 getDataSetList() { - return dataSetList; - } - - public void setDataSetList(List dataSetList) { - this.dataSetList = dataSetList; - } - - public List getLnInstList() { - return lnInstList; - } - - public void setLnInstList(List lnInstList) { - this.lnInstList = lnInstList; - } - } - - @JsonIgnoreProperties(ignoreUnknown = true) - public static class LnClassCfgItem implements NameListSupport { - private String desc; - private List nameList = new ArrayList(); - public String getDesc() { return desc; } - public void setDesc(String desc) { this.desc = desc; } - public List getNameList() { return nameList; } - public void setNameList(List nameList) { this.nameList = nameList; } - } - - @JsonIgnoreProperties(ignoreUnknown = true) - public static class PhaseCfgItem implements NameListSupport { - private String desc; - private List nameList = new ArrayList(); - public String getDesc() { return desc; } - public void setDesc(String desc) { this.desc = desc; } - public List getNameList() { return nameList; } - public void setNameList(List nameList) { this.nameList = nameList; } - } - - @JsonIgnoreProperties(ignoreUnknown = true) - public static class MultiplierCfgItem implements NameListSupport { - private int multiplier; - private List nameList = new ArrayList(); - public int getMultiplier() { return multiplier; } - public void setMultiplier(int multiplier) { this.multiplier = multiplier; } - public List getNameList() { return nameList; } - public void setNameList(List nameList) { this.nameList = nameList; } - } - - @JsonIgnoreProperties(ignoreUnknown = true) - public static class UnitCfgItem implements NameListSupport { - private String desc; - private List nameList = new ArrayList(); - public String getDesc() { return desc; } - public void setDesc(String desc) { this.desc = desc; } - public List getNameList() { return nameList; } - public void setNameList(List nameList) { this.nameList = nameList; } - } - - @JsonIgnoreProperties(ignoreUnknown = true) - public static class TypeCfgItem implements NameListSupport { - private String desc; - private List nameList = new ArrayList(); - public String getDesc() { return desc; } - public void setDesc(String desc) { this.desc = desc; } - public List getNameList() { return nameList; } - public void setNameList(List nameList) { this.nameList = nameList; } - } - - @JsonIgnoreProperties(ignoreUnknown = true) - public static class DataObjectCfgItem { - private String desc; - @JsonProperty("LnInstList") - private List lnInstList = new ArrayList(); - @JsonProperty("ObjectList") - private List objectList = new ArrayList(); - public String getDesc() { return desc; } - public void setDesc(String desc) { this.desc = desc; } - public List getLnInstList() { return lnInstList; } - public void setLnInstList(List lnInstList) { this.lnInstList = lnInstList; } - public List getObjectList() { return objectList; } - public void setObjectList(List 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 nameList = new ArrayList(); - private List queueList = new ArrayList(); - 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 getNameList() { return nameList; } - public void setNameList(List nameList) { this.nameList = nameList; } - public List getQueueList() { return queueList; } - public void setQueueList(List queueList) { this.queueList = queueList; } - } -} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/infrastructure/parser/SclGeneratedModelReader.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/infrastructure/parser/SclGeneratedModelReader.java index 0bb5b08..b6bff12 100644 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/infrastructure/parser/SclGeneratedModelReader.java +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/infrastructure/parser/SclGeneratedModelReader.java @@ -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 allFcdas = new ArrayList(); 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 - * - TLN0.getLnClass() 返回 List + * 当前生成模型中,`TLN` 和 `TLN0` 都返回 `List`,这里只取首个值。 */ 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 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 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 buildTemplateSequenceCountMap(SCL scl) { Map result = new LinkedHashMap(); @@ -367,7 +370,7 @@ public class SclGeneratedModelReader { return result; } - // 1. 先建 DOType.id -> count + // 1. 先构建 `DOType.id -> count`。 Map doTypeCountMap = new LinkedHashMap(); 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, - * 所以这里要做一次安全转换。 + * 当前 JAXB 生成类把该字段定义为 `List`,这里统一做安全转换。 */ 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/FCDA 的 sequenceCount 反查。 + * 从 `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) { diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/infrastructure/parser/SclParserAdapter.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/infrastructure/parser/SclParserAdapter.java index 5ba1801..48658b3 100644 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/infrastructure/parser/SclParserAdapter.java +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/infrastructure/parser/SclParserAdapter.java @@ -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 两种根对象形式。 + */ private SCL resolveSclRoot(Object raw) { if (raw instanceof SCL) { return (SCL) raw; diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/infrastructure/parser/SclTraversalSupport.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/infrastructure/parser/SclTraversalSupport.java index 856d1be..1e74114 100644 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/infrastructure/parser/SclTraversalSupport.java +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/infrastructure/parser/SclTraversalSupport.java @@ -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 allFcdas, FcdaNode current) { int count = 0; @@ -30,6 +32,9 @@ public final class SclTraversalSupport { return count <= 1 ? 0 : count; } + /** + * 展开 DOI 树下所有 DAI 节点,便于后续读取单位、倍率等叶子值。 + */ public static List flattenLeafDai(DoiNode doi) { List result = new ArrayList(); if (doi == null || doi.getChildren() == null) { @@ -41,6 +46,9 @@ public final class SclTraversalSupport { return result; } + /** + * 递归收集 DAI 叶子节点。 + */ private static void collectLeafDai(DoiElementNode node, List 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); } diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/GenerateMappingResult.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/GenerateMappingResult.java new file mode 100644 index 0000000..9e93c02 --- /dev/null +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/GenerateMappingResult.java @@ -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 problems = new ArrayList(); +} \ No newline at end of file diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/analysis/IndexAnalysis.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/analysis/IndexAnalysis.java similarity index 50% rename from tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/analysis/IndexAnalysis.java rename to tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/analysis/IndexAnalysis.java index dd3f714..a97fedd 100644 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/domain/model/analysis/IndexAnalysis.java +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/analysis/IndexAnalysis.java @@ -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 candidates = new ArrayList(); - private List problems = new ArrayList(); - public List getCandidates() { return candidates; } - public void setCandidates(List candidates) { this.candidates = candidates; } - public List getProblems() { return problems; } - public void setProblems(List problems) { this.problems = problems; } + /** 分析过程中发现但不一定阻断流程的问题。 */ + private List problems = new ArrayList(); } \ No newline at end of file diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/analysis/IndexCandidate.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/analysis/IndexCandidate.java new file mode 100644 index 0000000..514c693 --- /dev/null +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/analysis/IndexCandidate.java @@ -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 templateLabels = new ArrayList(); + + /** 当前分组下匹配到的报告列表。 */ + private List reports = new ArrayList(); + + /** + * 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; +} \ No newline at end of file diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/analysis/IndexCandidateReportItem.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/analysis/IndexCandidateReportItem.java new file mode 100644 index 0000000..562f674 --- /dev/null +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/analysis/IndexCandidateReportItem.java @@ -0,0 +1,26 @@ +package com.njcn.gather.icd.mapping.pojo.bo.analysis; + +import lombok.Data; +import java.util.ArrayList; +import java.util.List; + +/** + * 候选分组下的单个报告项。 + * + * 这个对象专门给前端展示“这个分组下面有哪些报告,以及每个报告对应哪些可用 lnInst 数字”。 + */ +@Data +public class IndexCandidateReportItem { + + /** 报告名称。 */ + private String reportName; + + /** 数据集名称。 */ + private String dataSetName; + + /** 报告中文描述。一般和分组描述相同,保留它是为了前端渲染更灵活。 */ + private String reportDesc; + + /** 当前报告在 ICD 中解析出来的所有可选 lnInst 数字。 */ + private List availableLnInstValues = new ArrayList(); +} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/analysis/ValidationResult.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/analysis/ValidationResult.java new file mode 100644 index 0000000..a4b451e --- /dev/null +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/analysis/ValidationResult.java @@ -0,0 +1,18 @@ +package com.njcn.gather.icd.mapping.pojo.bo.analysis; + +import lombok.Data; +import java.util.ArrayList; +import java.util.List; + +/** + * 校验结果。 + * 索引校验结果模型。保存是否通过以及问题列表。 + */ +@Data +public class ValidationResult { + /** 是否通过索引绑定校验。 */ + private boolean valid; + + /** 未通过校验时的具体问题列表。 */ + private List problems = new ArrayList(); +} \ No newline at end of file diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/icd/DataSetNode.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/icd/DataSetNode.java new file mode 100644 index 0000000..6039250 --- /dev/null +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/icd/DataSetNode.java @@ -0,0 +1,18 @@ +package com.njcn.gather.icd.mapping.pojo.bo.icd; + +import lombok.Data; +import java.util.ArrayList; +import java.util.List; + +/** + * DataSet 节点。 + * 数据集模型。用于承接 DataSet 下的 FCDA 列表。 + */ +@Data +public class DataSetNode { + /** DataSet 名称。 */ + private String name; + + /** DataSet 下的 FCDA 引用列表。 */ + private List fcdas = new ArrayList(); +} \ No newline at end of file diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/icd/DoiElementNode.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/icd/DoiElementNode.java new file mode 100644 index 0000000..edabc77 --- /dev/null +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/icd/DoiElementNode.java @@ -0,0 +1,31 @@ +package com.njcn.gather.icd.mapping.pojo.bo.icd; + +import lombok.Data; +import java.util.ArrayList; +import java.util.List; + +/** + * DOI/SDI/DAI 统一节点。 + * DOI/SDI/DAI 细项模型。用于递归承接 DOI 树明细。 + * + * kind 常见值: + * - SDI + * - DAI + */ +@Data +public class DoiElementNode { + /** 节点类型,当前主要为 SDI 或 DAI。 */ + private String kind; + + /** SDI/DAI 名称。 */ + private String name; + + /** SDI/DAI 自带的 ix 序号。 */ + private Long ix; + + /** DAI 节点下的 Val 文本值。 */ + private List values = new ArrayList(); + + /** SDI 节点下继续嵌套的子节点。 */ + private List children = new ArrayList(); +} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/icd/DoiNode.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/icd/DoiNode.java new file mode 100644 index 0000000..6bdff12 --- /dev/null +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/icd/DoiNode.java @@ -0,0 +1,30 @@ +package com.njcn.gather.icd.mapping.pojo.bo.icd; + +import lombok.Data; +import java.util.ArrayList; +import java.util.List; + +/** + * DOI 节点。 + * DOI 模型。表示逻辑节点下的一个数据对象节点。 + */ +@Data +public class DoiNode { + /** DOI 名称,例如 PhV、Hz。 */ + private String name; + + /** DOI 自带的 ix 序号。 */ + private Long ix; + + /** DOI 所属 LN 的 lnClass。 */ + private String lnClass; + + /** DOI 所属 LN 的 lnInst。 */ + private String lnInst; + + /** 从模板或 FCDA 反查得到的实际序列数量。 */ + private int sequenceCount; + + /** DOI 下的 SDI/DAI 子节点树。 */ + private List children = new ArrayList(); +} \ No newline at end of file diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/icd/FcdaNode.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/icd/FcdaNode.java new file mode 100644 index 0000000..bd95910 --- /dev/null +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/icd/FcdaNode.java @@ -0,0 +1,36 @@ +package com.njcn.gather.icd.mapping.pojo.bo.icd; + +import lombok.Data; +/** + * FCDA 节点。 + * FCDA 模型。保存 lnClass、lnInst、doName、daName、fc、ix 等信息。 + */ +@Data +public class FcdaNode { + /** FCDA.ldInst。 */ + private String ldInst; + + /** FCDA.prefix。 */ + private String prefix; + + /** FCDA.lnClass。 */ + private String lnClass; + + /** FCDA.lnInst。 */ + private String lnInst; + + /** FCDA.doName,对应 DOI 名称。 */ + private String doName; + + /** FCDA.daName,对应 DAI 名称。 */ + private String daName; + + /** FCDA.fc 功能约束。 */ + private String fc; + + /** FCDA.ix 序号。 */ + private Long ix; + + /** 同一数据对象在数据集中的序列数量。 */ + private int sequenceCount; +} \ No newline at end of file diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/icd/IcdDocument.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/icd/IcdDocument.java new file mode 100644 index 0000000..7c5b921 --- /dev/null +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/icd/IcdDocument.java @@ -0,0 +1,46 @@ +package com.njcn.gather.icd.mapping.pojo.bo.icd; + +import lombok.Data; +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. 业务层全部依赖这个标准化模型,便于后续替换解析实现。 + */ +@Data +public class IcdDocument { + /** 原始 ICD 文件名。 */ + private String fileName; + + /** 当前选中的 IED 名称。 */ + private String iedName; + + /** 当前选中的逻辑设备实例名。 */ + private String ldInst; + + /** LD 输出前缀,通常去掉实例名尾部数字。 */ + private String ldPrefix; + + /** 当前解析到的主 IED 节点。 */ + private IedNode primaryIed; + + /** 当前参与映射生成的逻辑设备。 */ + private LogicalDeviceNode logicalDevice; + + /** 当前逻辑设备下的 LN0 和 LN 列表。 */ + private List logicalNodes = new ArrayList(); + + /** 当前逻辑设备下的报告控制块列表。 */ + private List reportControls = new ArrayList(); + + /** 当前逻辑设备下的数据集,key 为 DataSet 名称。 */ + private Map dataSets = new LinkedHashMap(); +} \ No newline at end of file diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/icd/IedNode.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/icd/IedNode.java new file mode 100644 index 0000000..4953bfb --- /dev/null +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/icd/IedNode.java @@ -0,0 +1,21 @@ +package com.njcn.gather.icd.mapping.pojo.bo.icd; + +import lombok.Data; +import java.util.ArrayList; +import java.util.List; + +/** + * IED 节点。 + * IED 节点模型。保存 IED 名称以及其下逻辑设备引用。 + */ +@Data +public class IedNode { + /** IED 名称。 */ + private String name; + + /** IED 下的 AccessPoint 名称列表。 */ + private List accessPointNames = new ArrayList(); + + /** IED 下的逻辑设备实例名列表。 */ + private List lDeviceInstList = new ArrayList(); +} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/icd/LnNode.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/icd/LnNode.java new file mode 100644 index 0000000..5903cd9 --- /dev/null +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/icd/LnNode.java @@ -0,0 +1,32 @@ +package com.njcn.gather.icd.mapping.pojo.bo.icd; + +import lombok.Data; +import java.util.ArrayList; +import java.util.List; + +/** + * 逻辑节点。 + * 逻辑节点模型。保存 lnClass、lnInst、prefix、DOI 等信息。 + * + * LN0 和 LN 最终都统一抽象为这个模型。 + */ +@Data +public class LnNode { + /** 是否来自 LN0 节点。 */ + private boolean ln0; + + /** LN 前缀。 */ + private String prefix; + + /** LN 类型类别,例如 MMXU、MHAI。 */ + private String lnClass; + + /** LN 实例号,用于和前端选择的数字索引绑定。 */ + private String lnInst; + + /** LN 类型模板 ID,用于反查 DataTypeTemplates。 */ + private String lnType; + + /** LN 下解析出的 DOI 列表。 */ + private List doiList = new ArrayList(); +} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/icd/LogicalDeviceNode.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/icd/LogicalDeviceNode.java new file mode 100644 index 0000000..82c36d6 --- /dev/null +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/icd/LogicalDeviceNode.java @@ -0,0 +1,15 @@ +package com.njcn.gather.icd.mapping.pojo.bo.icd; + +import lombok.Data; +/** + * 逻辑设备节点。 + * 逻辑设备模型。保存 inst、prefix 以及其下 LN/LN0 列表。 + */ +@Data +public class LogicalDeviceNode { + /** LDevice.inst。 */ + private String inst; + + /** LDevice.ldName。 */ + private String ldName; +} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/icd/ReportControlNode.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/icd/ReportControlNode.java new file mode 100644 index 0000000..1c71846 --- /dev/null +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/icd/ReportControlNode.java @@ -0,0 +1,33 @@ +package com.njcn.gather.icd.mapping.pojo.bo.icd; + +import lombok.Data; + +/** + * ReportControl 节点。 + * 报告控制块模型。用于保存报告名称、关联 DataSet、缓冲属性等。 + */ +@Data +public class ReportControlNode { + /** ReportControl.name。 */ + private String name; + + /** ReportControl.rptID。 */ + private String rptId; + + /** 是否为缓存报告,后续会转换为 BR/RP。 */ + private boolean buffered; + + /** 当前报告关联的 DataSet 名称。 */ + private String dataSetName; + + /** 解析后的触发选项编码。 */ + private String trgOps; + + /** ReportControl.confRev。 */ + private String confRev; + + /** 兼容现有调用链,继续保留 Boolean 形式的 buffered 访问器。 */ + public Boolean getBuffered() { return buffered; } + + public void setBuffered(Boolean buffered) { this.buffered = buffered; } +} \ No newline at end of file diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/mapping/DataSetGroupItem.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/mapping/DataSetGroupItem.java new file mode 100644 index 0000000..d731ad5 --- /dev/null +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/mapping/DataSetGroupItem.java @@ -0,0 +1,21 @@ +package com.njcn.gather.icd.mapping.pojo.bo.mapping; + +import lombok.Data; +import java.util.ArrayList; +import java.util.List; + +/** + * 最终映射中的 DataSetList 单项。 + */ +@Data +public class DataSetGroupItem { + + /** 分组描述。一般来自 LnClassList.desc。 */ + private String desc; + + /** lnClass。 */ + private String lnClass; + + /** 该 lnClass 下的 inst 列表。 */ + private List instList = new ArrayList(); +} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/mapping/DoiItem.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/mapping/DoiItem.java new file mode 100644 index 0000000..46d87e0 --- /dev/null +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/mapping/DoiItem.java @@ -0,0 +1,42 @@ +package com.njcn.gather.icd.mapping.pojo.bo.mapping; + +import lombok.Data; +import java.util.ArrayList; +import java.util.List; + +/** + * 最终映射中的 doiList 单项。 + */ +@Data +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 sdiList = new ArrayList(); +} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/mapping/InstItem.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/mapping/InstItem.java new file mode 100644 index 0000000..b67e67b --- /dev/null +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/mapping/InstItem.java @@ -0,0 +1,21 @@ +package com.njcn.gather.icd.mapping.pojo.bo.mapping; + +import lombok.Data; +import java.util.ArrayList; +import java.util.List; + +/** + * 最终映射中的 instList 单项。 + */ +@Data +public class InstItem { + + /** lnInst。 */ + private String inst; + + /** 该 inst 的描述。通常使用当前绑定的 label。 */ + private String desc; + + /** DOI 列表。 */ + private List doiList = new ArrayList(); +} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/mapping/MappingDocument.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/mapping/MappingDocument.java new file mode 100644 index 0000000..42eb696 --- /dev/null +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/mapping/MappingDocument.java @@ -0,0 +1,63 @@ +package com.njcn.gather.icd.mapping.pojo.bo.mapping; + +import lombok.Data; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.ArrayList; +import java.util.List; + +/** + * 最终映射文档。 + * + * 关键说明: + * 1. Java 字段统一使用 lowerCamelCase,避免 Jackson 同时输出 ied/IED 这类重复字段。 + * 2. JSON 输出名通过 @JsonProperty 显式指定,确保与原 C# 输出格式一致。 + */ +@Data +public class MappingDocument { + + /** 映射文档版本。 */ + @JsonProperty("version") + private String version; + + /** 映射文档作者。 */ + @JsonProperty("author") + private String author; + + /** ICD 中的 IED 名称,输出字段固定为 IED。 */ + @JsonProperty("IED") + private String ied; + + /** LD 前缀,输出字段固定为 LD。 */ + @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 reportMap = new ArrayList(); + + /** 数据集映射列表,按 lnClass 和 inst 聚合。 */ + @JsonProperty("DataSetList") + private List dataSetList = new ArrayList(); +} \ No newline at end of file diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/mapping/ReportMapItem.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/mapping/ReportMapItem.java new file mode 100644 index 0000000..b5c0bc8 --- /dev/null +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/mapping/ReportMapItem.java @@ -0,0 +1,60 @@ +package com.njcn.gather.icd.mapping.pojo.bo.mapping; + +import lombok.Data; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * 最终映射中的 ReportMap 单项。 + * + * 关键说明: + * 1. 原 C# 不是把同组报告合并成一条,而是组内每个报告各自输出一条。 + * 2. 但 reportCount 仍然写该分组总数。 + * 3. buffered 不是 boolean,而是: + * - true -> BR + * - false -> RP + */ +@Data +public class ReportMapItem { + + /** 报告分组描述。 */ + @JsonProperty("desc") + private String desc; + + /** 同一分组下的报告数量,单报告按原规则写 0。 */ + @JsonProperty("reportCount") + private int reportCount; + + /** ICD ReportControl.rptID。 */ + @JsonProperty("rptID") + private String rptId; + + /** ICD ReportControl.name。 */ + @JsonProperty("name") + private String name; + + /** 报告缓存类型编码,取值 BR 或 RP。 */ + @JsonProperty("buffered") + private String buffered; + + /** 模板 ReportList.inst,限定 01~04。 */ + @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; +} \ No newline at end of file diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/mapping/SdiItem.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/mapping/SdiItem.java new file mode 100644 index 0000000..dedc26d --- /dev/null +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/mapping/SdiItem.java @@ -0,0 +1,21 @@ +package com.njcn.gather.icd.mapping.pojo.bo.mapping; + +import lombok.Data; +import java.util.ArrayList; +import java.util.List; + +/** + * 最终映射中的 sdiList 单项。 + */ +@Data +public class SdiItem { + + /** SDI 名称。 */ + private String name; + + /** SDI 描述。 */ + private String desc; + + /** 类型列表。 */ + private List typeList = new ArrayList(); +} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/mapping/TypeItem.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/mapping/TypeItem.java new file mode 100644 index 0000000..ce18407 --- /dev/null +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/mapping/TypeItem.java @@ -0,0 +1,15 @@ +package com.njcn.gather.icd.mapping.pojo.bo.mapping; + +import lombok.Data; +/** + * 最终映射中的 typeList 单项。 + */ +@Data +public class TypeItem { + + /** 类型名称。 */ + private String name; + + /** 类型描述。 */ + private String desc; +} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/state/DataSetSelectionState.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/state/DataSetSelectionState.java new file mode 100644 index 0000000..8353b14 --- /dev/null +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/state/DataSetSelectionState.java @@ -0,0 +1,49 @@ +package com.njcn.gather.icd.mapping.pojo.bo.state; + +import lombok.Data; +import com.njcn.gather.icd.mapping.pojo.bo.icd.LnNode; + +import java.util.ArrayList; +import java.util.List; + +/** + * 最终参与生成 DataSetList 的选择状态。 + * + * 关键修正: + * 旧版本一个绑定只保存一个 LnNode,导致: + * - MSQI 整组丢失 + * - MHAI 只生成一半 + * + * 新版本改成: + * 一个绑定可以关联多个 LnNode,后续生成阶段再逐个展开。 + */ +@Data +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 lnNodes = new ArrayList(); +} \ No newline at end of file diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/state/ReportAndDataSetState.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/state/ReportAndDataSetState.java new file mode 100644 index 0000000..78ca60f --- /dev/null +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/state/ReportAndDataSetState.java @@ -0,0 +1,29 @@ +package com.njcn.gather.icd.mapping.pojo.bo.state; + +import lombok.Data; +import java.util.ArrayList; +import java.util.List; + +/** + * 中间态总对象。 + * + * 作用: + * 1. 对应原 C# 里“先形成中间态,再做最终 JSON 组装”的思路; + * 2. 把业务分组、用户绑定、最终 DataSet 选择结果集中保存; + * 3. 避免在 MappingGenerationService 里直接边遍历边拼 JSON,便于后续扩展和排查。 + */ +@Data +public class ReportAndDataSetState { + + /** IED 名称。 */ + private String iedName; + + /** LD 实例名。 */ + private String ldInst; + + /** 分组状态列表。 */ + private List reportGroups = new ArrayList(); + + /** 数据集选择状态列表。 */ + private List dataSetSelections = new ArrayList(); +} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/state/ReportBindingState.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/state/ReportBindingState.java new file mode 100644 index 0000000..4719e4c --- /dev/null +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/state/ReportBindingState.java @@ -0,0 +1,27 @@ +package com.njcn.gather.icd.mapping.pojo.bo.state; + +import lombok.Data; +/** + * 单条最终有效绑定关系的中间态。 + */ +@Data +public class ReportBindingState { + + /** 分组 key。 */ + private String groupKey; + + /** 分组描述。 */ + private String groupDesc; + + /** 报告名。 */ + private String reportName; + + /** 数据集名。 */ + private String dataSetName; + + /** 标签。 */ + private String label; + + /** 绑定的 lnInst 数字。 */ + private String lnInst; +} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/state/ReportGroupState.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/state/ReportGroupState.java new file mode 100644 index 0000000..2f2e8a0 --- /dev/null +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/state/ReportGroupState.java @@ -0,0 +1,38 @@ +package com.njcn.gather.icd.mapping.pojo.bo.state; + +import lombok.Data; +import com.njcn.gather.icd.mapping.pojo.bo.analysis.IndexCandidateReportItem; + +import java.util.ArrayList; +import java.util.List; + +/** + * 单个业务分组的中间态。 + */ +@Data +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 reportItems = new ArrayList(); + + /** 用户最终确认的绑定关系。 */ + private List bindings = new ArrayList(); +} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/template/DefaultTemplate.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/template/DefaultTemplate.java new file mode 100644 index 0000000..d95d1dc --- /dev/null +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/template/DefaultTemplate.java @@ -0,0 +1,255 @@ +package com.njcn.gather.icd.mapping.pojo.bo.template; + +import lombok.Data; +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; + +/** + * 默认模板模型。 + * + * 用于承接 `DefaultCfg.txt` 解析后的配置内容,当前仅保留映射接口实际使用的字段。 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@Data +public class DefaultTemplate { + + /** 报告分组配置,决定 ReportMap 和前端候选分组。 */ + @JsonProperty("ReportList") + private List reportList = new ArrayList(); + + /** LN 类别配置,决定 DataSetList 的业务分组描述。 */ + @JsonProperty("LnClassList") + private List lnClassList = new ArrayList(); + + /** 相别配置,用于生成 sdiList 的相别描述。 */ + @JsonProperty("PhaseList") + private List phaseList = new ArrayList(); + + /** 倍率配置,用于从 DOI/DAI 值解析 coefficient。 */ + @JsonProperty("MultiplierList") + private List multiplierList = new ArrayList(); + + /** 单位配置,用于从 DOI/DAI 值解析 unit。 */ + @JsonProperty("UnitList") + private List unitList = new ArrayList(); + + /** 类型配置,用于识别 mag/ang 等 typeList 节点。 */ + @JsonProperty("TypeList") + private List typeList = new ArrayList(); + + /** 数据对象配置,决定每个绑定标签应生成哪些 DOI。 */ + @JsonProperty("DataObjectsList") + private List dataObjectsList = new ArrayList(); + + /** + * 执行模板基础校验。 + * + * 返回问题列表;为空表示校验通过。 + */ + public List verify() { + List problems = new ArrayList(); + 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; + } + + /** + * 汇总配置项中的所有 nameList 名称,用于重复项校验。 + */ + private List extractNames(List list) { + List names = new ArrayList(); + 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 names, List problems) { + Set set = new HashSet(); + for (String name : names) { + if (name == null) { + continue; + } + String key = name.trim(); + if (!set.add(key)) { + problems.add(section + " 中存在重复配置项:" + key); + } + } + } + + /** + * 校验每个 DataObjectsList 分组内的对象名称是否重复。 + */ + private void ensureDuplicateObjectNames(List problems) { + if (dataObjectsList == null) { + return; + } + for (DataObjectCfgItem dataObject : dataObjectsList) { + List names = new ArrayList(); + if (dataObject.getObjectList() != null) { + for (ObjectCfgItem object : dataObject.getObjectList()) { + if (object.getNameList() != null) { + names.addAll(object.getNameList()); + } + } + } + ensureDuplicateNames("DataObjectsList[" + dataObject.getDesc() + "]", names, problems); + } + } + /** 统一抽象:所有包含 `nameList` 的配置项都实现该接口。 */ + public interface NameListSupport { + List getNameList(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + @Data + 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 dataSetList = new ArrayList(); + + /** + * 当前分组允许配置的标签模板列表。 + */ + @JsonProperty("LnInstList") + private List lnInstList = new ArrayList(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + @Data + public static class LnClassCfgItem implements NameListSupport { + /** LN 类别描述。 */ + private String desc; + + /** 当前描述覆盖的 lnClass 名称列表。 */ + private List nameList = new ArrayList(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + @Data + public static class PhaseCfgItem implements NameListSupport { + /** 相别中文描述。 */ + private String desc; + + /** 当前相别描述覆盖的 SDI 名称列表。 */ + private List nameList = new ArrayList(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + @Data + public static class MultiplierCfgItem implements NameListSupport { + /** 倍率数值。 */ + private int multiplier; + + /** 能映射到该倍率的 DAI 值列表。 */ + private List nameList = new ArrayList(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + @Data + public static class UnitCfgItem implements NameListSupport { + /** 单位描述。 */ + private String desc; + + /** 能映射到该单位的 DAI 值列表。 */ + private List nameList = new ArrayList(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + @Data + public static class TypeCfgItem implements NameListSupport { + /** 类型中文描述。 */ + private String desc; + + /** 能映射到该类型的 SDI 名称列表。 */ + private List nameList = new ArrayList(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + @Data + public static class DataObjectCfgItem { + /** 数据对象业务描述。 */ + private String desc; + + /** 当前数据对象配置适用的绑定标签列表。 */ + @JsonProperty("LnInstList") + private List lnInstList = new ArrayList(); + + /** 当前标签下需要输出的 DOI 对象配置。 */ + @JsonProperty("ObjectList") + private List objectList = new ArrayList(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + @Data + public static class ObjectCfgItem implements NameListSupport { + /** DOI 输出描述。 */ + private String desc; + + /** 基波标志,直接写入最终 doiList.baseflag。 */ + private int baseflag; + + /** 普通 DOI 分支下的基波数量。 */ + private int basecount; + + /** queueList 分支下的队列数量。 */ + private int queuecount; + + /** 普通匹配优先使用的 DOI 名称列表。 */ + private List nameList = new ArrayList(); + + /** nameList 未命中时才使用的 DOI 兜底名称列表。 */ + private List queueList = new ArrayList(); + } +} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/dto/GenerateFromIcdCommand.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/dto/GenerateFromIcdCommand.java new file mode 100644 index 0000000..ea25152 --- /dev/null +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/dto/GenerateFromIcdCommand.java @@ -0,0 +1,43 @@ +package com.njcn.gather.icd.mapping.pojo.dto; + +import lombok.Data; +import com.njcn.gather.icd.mapping.pojo.bo.icd.IcdDocument; + +import java.util.ArrayList; +import java.util.List; + +/** + * ICD 映射生成命令。 + * + * 用于隔离接口层参数,便于应用层统一编排生成流程。 + */ +@Data +public class GenerateFromIcdCommand { + + /** 原始文件名。 */ + private String fileName; + + /** ICD 文件字节数组。 */ + private byte[] fileBytes; + + /** 前端确认或修改后的 ICD 解析结果。 */ + private IcdDocument icdDocument; + + /** 输出版本号。 */ + private String version; + + /** 作者。 */ + private String author; + + /** 是否保存到磁盘。 */ + private boolean saveToDisk; + + /** 是否输出美化 JSON。 */ + private boolean prettyJson; + + /** 输出目录。 */ + private String outputDir; + + /** 用户提交的索引选择结果。 */ + private List indexSelection = new ArrayList(); +} \ No newline at end of file diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/dto/IndexBindingCommand.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/dto/IndexBindingCommand.java new file mode 100644 index 0000000..536fbf5 --- /dev/null +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/dto/IndexBindingCommand.java @@ -0,0 +1,21 @@ +package com.njcn.gather.icd.mapping.pojo.dto; + +import lombok.Data; +/** + * 应用层单条绑定命令。 + */ +@Data +public class IndexBindingCommand { + + /** 报告名。 */ + private String reportName; + + /** 数据集名。 */ + private String dataSetName; + + /** 标签。 */ + private String label; + + /** 绑定到的 lnInst 数字。 */ + private String lnInst; +} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/dto/IndexSelectionGroupCommand.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/dto/IndexSelectionGroupCommand.java new file mode 100644 index 0000000..8b22722 --- /dev/null +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/dto/IndexSelectionGroupCommand.java @@ -0,0 +1,21 @@ +package com.njcn.gather.icd.mapping.pojo.dto; + +import lombok.Data; +import java.util.ArrayList; +import java.util.List; + +/** + * 应用层业务分组选择命令。 + */ +@Data +public class IndexSelectionGroupCommand { + + /** 分组唯一键。 */ + private String groupKey; + + /** 分组中文描述。 */ + private String groupDesc; + + /** 当前分组下的多条绑定关系。 */ + private List bindings = new ArrayList(); +} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/enums/GenerateStatus.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/enums/GenerateStatus.java similarity index 52% rename from tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/enums/GenerateStatus.java rename to tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/enums/GenerateStatus.java index 8138b64..de3f55f 100644 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/enums/GenerateStatus.java +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/enums/GenerateStatus.java @@ -1,12 +1,12 @@ -package com.njcn.gather.icd.mapping.enums; +package com.njcn.gather.icd.mapping.pojo.enums; /** - * 接口生成状态。 + * 映射生成状态。 */ public enum GenerateStatus { /** 生成成功。 */ SUCCESS, - /** 需要前端重新选择索引。 */ + /** 需要前端补充或重新选择索引。 */ NEED_INDEX_SELECTION, /** 生成失败。 */ FAILED diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/param/GenerateMappingFromIcdRequest.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/param/GenerateMappingFromIcdRequest.java new file mode 100644 index 0000000..9194dc6 --- /dev/null +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/param/GenerateMappingFromIcdRequest.java @@ -0,0 +1,38 @@ +package com.njcn.gather.icd.mapping.pojo.param; + +import lombok.Data; +import java.util.ArrayList; +import java.util.List; + +/** + * 候选接口请求体。 + * + * 当前主要承载输出版本、作者、落盘选项等基础参数。 + * 为兼容旧调用,仍保留 indexSelection 字段,但候选接口本身不依赖该字段。 + */ +@Data +public class GenerateMappingFromIcdRequest { + + /** 输出版本号。为空时后端默认补当天日期。 */ + private String version; + + /** 作者。为空时使用模块默认作者。 */ + private String author; + + /** 是否保存到磁盘。 */ + private boolean saveToDisk; + + /** 是否返回格式化 JSON。 */ + private boolean prettyJson; + + /** 输出目录。saveToDisk=true 时才会用到。 */ + private String outputDir; + + /** + * 兼容保留的索引选择结果。 + * + * 当前候选接口会直接返回 indexCandidates 和 icdDocument; + * 正式提交时请改用 get-mms-json 接口。 + */ + private List indexSelection = new ArrayList(); +} \ No newline at end of file diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/param/IndexBindingRequest.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/param/IndexBindingRequest.java new file mode 100644 index 0000000..1b288b6 --- /dev/null +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/param/IndexBindingRequest.java @@ -0,0 +1,24 @@ +package com.njcn.gather.icd.mapping.pojo.param; + +import lombok.Data; +/** + * 单条索引绑定请求。 + * + * 一条绑定只表达一个最小关系: + * 某个报告 reportName 下,使用某个标签 label 与某个 lnInst 数字做绑定。 + */ +@Data +public class IndexBindingRequest { + + /** 绑定发生在哪个报告上,例如 brcbStHarm。 */ + private String reportName; + + /** 绑定发生在哪个数据集上,例如 dsStHarm。 */ + private String dataSetName; + + /** 业务标签,例如最大值、最小值、实时数据。 */ + private String label; + + /** 当前标签最终绑定到的 lnInst 数字,例如 1、2、3。 */ + private String lnInst; +} \ No newline at end of file diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/param/IndexSelectionGroupRequest.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/param/IndexSelectionGroupRequest.java new file mode 100644 index 0000000..2020bbe --- /dev/null +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/param/IndexSelectionGroupRequest.java @@ -0,0 +1,28 @@ +package com.njcn.gather.icd.mapping.pojo.param; + +import lombok.Data; +import java.util.ArrayList; +import java.util.List; + +/** + * 单个业务分组的索引选择请求。 + * + * 用于回传某个业务分组下,前端最终确认的多条绑定关系。 + */ +@Data +public class IndexSelectionGroupRequest { + + /** + * 分组唯一键。 + * + * 该值由后端在 NEED_INDEX_SELECTION 场景返回,前端应原样回传, + * 避免仅依赖中文描述做匹配。 + */ + private String groupKey; + + /** 分组中文描述,例如“实时数据”“统计数据”。 */ + private String groupDesc; + + /** 当前业务分组下,用户最终确认的绑定关系。 */ + private List bindings = new ArrayList(); +} \ No newline at end of file diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/param/SubmitIndexSelectionRequest.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/param/SubmitIndexSelectionRequest.java new file mode 100644 index 0000000..208fcfd --- /dev/null +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/param/SubmitIndexSelectionRequest.java @@ -0,0 +1,37 @@ +package com.njcn.gather.icd.mapping.pojo.param; + +import lombok.Data; +import com.njcn.gather.icd.mapping.pojo.bo.icd.IcdDocument; + +import java.util.ArrayList; +import java.util.List; + +/** + * 提交索引绑定并生成映射的请求体。 + * + * 第二个接口不再重复上传 ICD 文件,而是直接接收前端确认或修改后的 ICD 解析结果。 + */ +@Data +public class SubmitIndexSelectionRequest { + + /** 前端基于候选接口返回值确认或修改后的 ICD 解析结果。 */ + private IcdDocument icdDocument; + + /** 输出版本号。为空时后端默认补当天日期。 */ + private String version; + + /** 作者。为空时使用模块默认作者。 */ + private String author; + + /** 是否保存到磁盘。 */ + private boolean saveToDisk; + + /** 是否返回格式化 JSON。 */ + private boolean prettyJson; + + /** 输出目录。saveToDisk=true 时才会用到。 */ + private String outputDir; + + /** 用户最终确认的索引绑定关系。 */ + private List indexSelection = new ArrayList(); +} \ No newline at end of file diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/vo/IndexCandidateReportItemResponse.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/vo/IndexCandidateReportItemResponse.java new file mode 100644 index 0000000..62c202c --- /dev/null +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/vo/IndexCandidateReportItemResponse.java @@ -0,0 +1,27 @@ +package com.njcn.gather.icd.mapping.pojo.vo; + +import lombok.Data; +import com.fasterxml.jackson.annotation.JsonInclude; + +import java.util.ArrayList; +import java.util.List; + +/** + * 业务分组下的单个报告候选响应。 + */ +@JsonInclude(JsonInclude.Include.NON_EMPTY) +@Data +public class IndexCandidateReportItemResponse { + + /** 报告名称。 */ + private String reportName; + + /** 数据集名称。 */ + private String dataSetName; + + /** 报告描述。 */ + private String reportDesc; + + /** 当前报告可选的 `lnInst` 数值。 */ + private List availableLnInstValues = new ArrayList(); +} \ No newline at end of file diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/vo/IndexCandidateResponse.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/vo/IndexCandidateResponse.java new file mode 100644 index 0000000..cba1d87 --- /dev/null +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/vo/IndexCandidateResponse.java @@ -0,0 +1,33 @@ +package com.njcn.gather.icd.mapping.pojo.vo; + +import lombok.Data; +import com.fasterxml.jackson.annotation.JsonInclude; + +import java.util.ArrayList; +import java.util.List; + +/** + * 索引候选响应对象。 + * + * 一个候选对应一个业务分组,分组下可包含多个报告, + * 前端据此完成模板标签与 `lnInst` 的人工绑定。 + */ +@JsonInclude(JsonInclude.Include.NON_EMPTY) +@Data +public class IndexCandidateResponse { + + /** 分组唯一键。 */ + private String groupKey; + + /** 分组中文描述。 */ + private String groupDesc; + + /** 当前分组包含的报告数。 */ + private int reportCount; + + /** 模板里配置的可选标签。 */ + private List templateLabels = new ArrayList(); + + /** 当前分组下的报告候选列表。 */ + private List reports = new ArrayList(); +} \ No newline at end of file diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/vo/MappingDocumentResponse.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/vo/MappingDocumentResponse.java new file mode 100644 index 0000000..e486b3e --- /dev/null +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/vo/MappingDocumentResponse.java @@ -0,0 +1,28 @@ +package com.njcn.gather.icd.mapping.pojo.vo; + +import lombok.Data; +/** + * 映射文档摘要响应。 + * + * 用于返回最终映射结果中的关键信息摘要。 + */ +@Data +public class MappingDocumentResponse { + /** 映射文档版本。 */ + private String version; + + /** 映射文档作者。 */ + private String author; + + /** 输出 JSON 中的 IED。 */ + private String ied; + + /** 输出 JSON 中的 LD。 */ + private String ld; + + /** ReportMap 条目数量。 */ + private int reportCount; + + /** DataSetList 分组数量。 */ + private int dataSetCount; +} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/vo/MappingTaskResponse.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/vo/MappingTaskResponse.java new file mode 100644 index 0000000..2dd238b --- /dev/null +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/vo/MappingTaskResponse.java @@ -0,0 +1,39 @@ +package com.njcn.gather.icd.mapping.pojo.vo; + +import lombok.Data; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.njcn.gather.icd.mapping.pojo.bo.icd.IcdDocument; +import com.njcn.gather.icd.mapping.pojo.enums.GenerateStatus; + +import java.util.ArrayList; +import java.util.List; + +/** + * ICD 映射接口统一响应。 + * + * 按接口阶段仅返回当前场景必需字段;空字段和空集合不参与序列化。 + */ +@JsonInclude(JsonInclude.Include.NON_EMPTY) +@Data +public class MappingTaskResponse { + /** 本次接口处理状态。 */ + private GenerateStatus status; + + /** 状态说明或错误提示。 */ + private String message; + + /** 候选接口或需要重新选择索引时返回的 ICD 解析结果。 */ + private IcdDocument icdDocument; + + /** 正式生成成功后的完整映射 JSON。 */ + private String mappingJson; + + /** 生成文件落盘后的绝对路径。 */ + private String savedPath; + + /** 待绑定状态下返回的索引候选分组。 */ + private List indexCandidates = new ArrayList(); + + /** 模板校验、候选分析或绑定校验问题。 */ + private List problems = new ArrayList(); +} \ No newline at end of file diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/service/MappingTaskService.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/service/MappingTaskService.java new file mode 100644 index 0000000..1353a25 --- /dev/null +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/service/MappingTaskService.java @@ -0,0 +1,36 @@ +package com.njcn.gather.icd.mapping.service; + +import com.njcn.gather.icd.mapping.pojo.bo.GenerateMappingResult; +import com.njcn.gather.icd.mapping.pojo.dto.GenerateFromIcdCommand; + +/** + * ICD 映射任务编排服务。 + * + * 统一封装 ICD 解析、索引候选分析、索引绑定校验和正式映射生成流程。 + */ +public interface MappingTaskService { + + /** + * 解析 ICD 并返回索引候选结果。 + * + * @param command ICD 文件与生成参数 + * @return 候选分析结果 + */ + GenerateMappingResult getICD(GenerateFromIcdCommand command); + + /** + * 串联 getICD 与索引提交流程,直接返回正式提交阶段的处理结果。 + * + * @param command ICD 文件与前端提交参数 + * @return 正式提交阶段结果 + */ + GenerateMappingResult getIcdMmsJson(GenerateFromIcdCommand command); + + /** + * 根据前端确认后的索引绑定关系生成正式映射。 + * + * @param command 已确认的 ICD 解析结果与索引绑定关系 + * @return 映射生成结果 + */ + GenerateMappingResult getMmsJson(GenerateFromIcdCommand command); +} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/service/impl/MappingTaskServiceImpl.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/service/impl/MappingTaskServiceImpl.java new file mode 100644 index 0000000..bbae010 --- /dev/null +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/service/impl/MappingTaskServiceImpl.java @@ -0,0 +1,271 @@ +package com.njcn.gather.icd.mapping.service.impl; + +import com.njcn.gather.icd.mapping.component.DefaultTemplateLoader; +import com.njcn.gather.icd.mapping.component.FileStorageService; +import com.njcn.gather.icd.mapping.component.IcdParserService; +import com.njcn.gather.icd.mapping.component.IndexAnalysisService; +import com.njcn.gather.icd.mapping.component.IndexValidationService; +import com.njcn.gather.icd.mapping.component.MappingDocumentSerializer; +import com.njcn.gather.icd.mapping.component.MappingGenerationService; +import com.njcn.gather.icd.mapping.pojo.bo.GenerateMappingResult; +import com.njcn.gather.icd.mapping.pojo.bo.analysis.IndexAnalysis; +import com.njcn.gather.icd.mapping.pojo.bo.analysis.ValidationResult; +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.bo.template.DefaultTemplate; +import com.njcn.gather.icd.mapping.pojo.dto.GenerateFromIcdCommand; +import com.njcn.gather.icd.mapping.pojo.dto.IndexSelectionGroupCommand; +import com.njcn.gather.icd.mapping.pojo.enums.GenerateStatus; +import com.njcn.gather.icd.mapping.service.MappingTaskService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * ICD 映射任务编排服务实现。 + * + * 按固定链路组织 ICD 解析、索引分析、校验和映射生成,避免 Controller 关注业务编排细节。 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class MappingTaskServiceImpl implements MappingTaskService { + + /** ICD 解析阶段名称。 */ + private static final String ICD_PARSE_TASK_NAME = "ICD 解析"; + /** 正式映射生成阶段名称。 */ + private static final String MAPPING_GENERATE_TASK_NAME = "映射生成"; + /** 候选分析成功提示。 */ + private static final String INDEX_SELECTION_SUCCESS_MESSAGE = "ICD 解析成功,请确认解析结果并完成索引绑定后提交"; + /** 索引绑定缺失提示。 */ + private static final String INDEX_SELECTION_MISSING_MESSAGE = "索引配置缺失,请根据候选信息完成标签与数字索引的绑定后重新提交"; + /** 索引绑定非法提示。 */ + private static final String INDEX_SELECTION_INVALID_MESSAGE = "索引配置不合法,请根据候选信息完成标签与数字索引的绑定后重新提交"; + /** 映射生成成功提示。 */ + private static final String MAPPING_GENERATE_SUCCESS_MESSAGE = "映射生成成功"; + + /** ICD/SCL 文件解析服务。 */ + private final IcdParserService icdParserService; + /** DefaultCfg.txt 默认模板加载器。 */ + private final DefaultTemplateLoader defaultTemplateLoader; + /** 根据 ICD 和模板生成前端可选索引候选。 */ + private final IndexAnalysisService indexAnalysisService; + /** 校验前端回传的标签与 lnInst 绑定关系。 */ + private final IndexValidationService indexValidationService; + /** 根据有效绑定关系生成最终 MappingDocument。 */ + private final MappingGenerationService mappingGenerationService; + /** 将 MappingDocument 序列化为 JSON。 */ + private final MappingDocumentSerializer mappingDocumentSerializer; + /** 按需把生成结果保存到本地磁盘。 */ + private final FileStorageService fileStorageService; + + @Override + public GenerateMappingResult getICD(GenerateFromIcdCommand command) { + return executeTask(ICD_PARSE_TASK_NAME, result -> { + parseTaskContext(command, result); + result.setStatus(GenerateStatus.NEED_INDEX_SELECTION); + result.setMessage(INDEX_SELECTION_SUCCESS_MESSAGE); + }); + } + + @Override + public GenerateMappingResult getIcdMmsJson(GenerateFromIcdCommand command) { + GenerateMappingResult icdResult = getICD(command); + if (icdResult.getStatus() == GenerateStatus.FAILED || icdResult.getIcdDocument() == null) { + return icdResult; + } + + command.setIcdDocument(icdResult.getIcdDocument()); + return getMmsJson(command); + } + + @Override + public GenerateMappingResult getMmsJson(GenerateFromIcdCommand command) { + return executeTask(MAPPING_GENERATE_TASK_NAME, result -> { + MappingTaskContext context = buildTaskContext(requireIcdDocument(command), result); + if (isIndexSelectionEmpty(command.getIndexSelection())) { + markNeedIndexSelection(result, INDEX_SELECTION_MISSING_MESSAGE); + return; + } + + ValidationResult validationResult = indexValidationService.validate(context.indexAnalysis, command.getIndexSelection()); + if (!validationResult.isValid()) { + markNeedIndexSelection(result, INDEX_SELECTION_INVALID_MESSAGE); + result.getProblems().addAll(validationResult.getProblems()); + return; + } + + MappingDocument mappingDocument = mappingGenerationService.generate( + context.icdDocument, + context.template, + context.indexAnalysis, + command.getIndexSelection(), + command.getVersion(), + command.getAuthor() + ); + result.setMappingDocument(mappingDocument); + + String mappingJson = serializeMappingDocument(mappingDocument, command.isPrettyJson()); + result.setMappingJson(mappingJson); + + if (command.isSaveToDisk()) { + String fileName = buildOutputFileName(context.icdDocument, command.isPrettyJson()); + String savedPath = fileStorageService.save(fileName, mappingJson, command.getOutputDir()); + result.setSavedPath(savedPath); + } + + result.setStatus(GenerateStatus.SUCCESS); + result.setMessage(MAPPING_GENERATE_SUCCESS_MESSAGE); + }); + } + + /** + * 统一包装任务执行结果,避免每个入口重复编写异常兜底逻辑。 + */ + private GenerateMappingResult executeTask(String taskName, MappingTaskAction action) { + GenerateMappingResult result = new GenerateMappingResult(); + try { + action.execute(result); + } catch (Exception ex) { + handleTaskFailure(result, taskName, ex); + } + return result; + } + + /** + * 解析上传的 ICD 文件,并补齐候选分析上下文。 + */ + private MappingTaskContext parseTaskContext(GenerateFromIcdCommand command, GenerateMappingResult result) { + IcdDocument icdDocument = icdParserService.parse(command.getFileBytes(), command.getFileName()); + return buildTaskContext(icdDocument, result); + } + + /** + * 基于 ICD 解析结果补齐模板和候选分析信息,供后续提交流程复用。 + */ + private MappingTaskContext buildTaskContext(IcdDocument icdDocument, GenerateMappingResult result) { + fillIcdSummary(result, icdDocument); + + DefaultTemplate template = loadTemplate(result); + IndexAnalysis indexAnalysis = analyzeIndexCandidates(icdDocument, template, result); + result.setIndexAnalysis(indexAnalysis); + return new MappingTaskContext(icdDocument, template, indexAnalysis); + } + + /** + * 提交流程必须带上前端确认后的 ICD 解析结果。 + */ + private IcdDocument requireIcdDocument(GenerateFromIcdCommand command) { + if (command.getIcdDocument() == null) { + throw new IllegalArgumentException("ICD 解析结果不能为空"); + } + return command.getIcdDocument(); + } + + /** + * 加载并校验默认模板。 + */ + private DefaultTemplate loadTemplate(GenerateMappingResult result) { + DefaultTemplate template = defaultTemplateLoader.load(); + result.getProblems().addAll(template.verify()); + return template; + } + + /** + * 分析 ICD 对应的索引候选。 + */ + private IndexAnalysis analyzeIndexCandidates(IcdDocument icdDocument, + DefaultTemplate template, + GenerateMappingResult result) { + IndexAnalysis indexAnalysis = indexAnalysisService.analyze(icdDocument, template); + result.getProblems().addAll(indexAnalysis.getProblems()); + return indexAnalysis; + } + + /** + * 回填响应公共信息。 + */ + private void fillIcdSummary(GenerateMappingResult result, IcdDocument icdDocument) { + result.setIedName(icdDocument.getIedName()); + result.setLdInst(icdDocument.getLdInst()); + result.setIcdDocument(icdDocument); + } + + /** + * 标记当前仍需用户重新确认索引绑定。 + */ + private void markNeedIndexSelection(GenerateMappingResult result, String message) { + result.setStatus(GenerateStatus.NEED_INDEX_SELECTION); + result.setMessage(message); + } + + /** + * 根据输出模式选择紧凑或美化 JSON。 + */ + private String serializeMappingDocument(MappingDocument mappingDocument, boolean prettyJson) { + return prettyJson + ? mappingDocumentSerializer.toPrettyJson(mappingDocument) + : mappingDocumentSerializer.toCompactJson(mappingDocument); + } + + /** + * 构造落盘文件名。 + * + * 文件名优先使用 IED 名称;如 IED 名称为空或清理后为空,则回退使用 mapping。 + */ + private String buildOutputFileName(IcdDocument icdDocument, boolean prettyJson) { + String baseName = icdDocument.getIedName() == null ? "mapping" : icdDocument.getIedName(); + String safeBaseName = baseName.replaceAll("[\\\\/:*?\"<>|]+", "_").trim(); + if (safeBaseName.isEmpty()) { + safeBaseName = "mapping"; + } + return safeBaseName + (prettyJson ? "-mapping-pretty.json" : "-mapping.json"); + } + + /** + * 统一处理任务异常,避免控制层再补失败分支。 + */ + private void handleTaskFailure(GenerateMappingResult result, String taskName, Exception ex) { + log.error("{}失败", taskName, ex); + String errorMessage = resolveErrorMessage(ex); + result.setStatus(GenerateStatus.FAILED); + result.setMessage(taskName + "失败:" + errorMessage); + result.getProblems().add(errorMessage); + } + + private boolean isIndexSelectionEmpty(List indexSelection) { + return indexSelection == null || indexSelection.isEmpty(); + } + + private String resolveErrorMessage(Exception ex) { + if (ex.getMessage() == null || ex.getMessage().trim().isEmpty()) { + return ex.getClass().getSimpleName(); + } + return ex.getMessage(); + } + + @FunctionalInterface + private interface MappingTaskAction { + void execute(GenerateMappingResult result) throws Exception; + } + + /** + * 供多个任务阶段复用的编排上下文。 + */ + private static class MappingTaskContext { + + private final IcdDocument icdDocument; + + private final DefaultTemplate template; + + private final IndexAnalysis indexAnalysis; + + private MappingTaskContext(IcdDocument icdDocument, DefaultTemplate template, IndexAnalysis indexAnalysis) { + this.icdDocument = icdDocument; + this.template = template; + this.indexAnalysis = indexAnalysis; + } + } +} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/utils/DateUtils.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/utils/DateUtils.java index 4603533..960dc62 100644 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/utils/DateUtils.java +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/utils/DateUtils.java @@ -1,17 +1,37 @@ package com.njcn.gather.icd.mapping.utils; import java.text.SimpleDateFormat; +import java.util.Collection; import java.util.Date; +import org.springframework.web.multipart.MultipartFile; + /** - * 日期工具。 + * 日期格式化工具。 */ public final class DateUtils { private DateUtils() { } + /** + * 返回当前时间文本,格式为 yyyy-MM-dd HH:mm:ss。 + */ public static String nowText() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); } -} \ No newline at end of file + + /** + * 统一提取上传文件名,避免控制器重复判空。 + */ + public static String resolveFileName(MultipartFile file) { + return file == null ? null : file.getOriginalFilename(); + } + + /** + * 统一统计选择项数量,空集合按 0 处理。 + */ + public static int resolveSelectionCount(Collection selections) { + return selections == null ? 0 : selections.size(); + } +} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/utils/JsonUtils.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/utils/JsonUtils.java index becb695..6c81284 100644 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/utils/JsonUtils.java +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/utils/JsonUtils.java @@ -1,22 +1,23 @@ package com.njcn.gather.icd.mapping.utils; /** - * JSON 文本处理工具。 + * JSON 文本清洗工具。 * - * 作用: - * 1. 兼容老配置文件中常见的尾逗号问题。 - * 2. 降低 DefaultCfg.txt 因历史书写习惯造成的解析失败概率。 + * 当前主要用于兼容历史模板中常见的尾逗号写法,减少 `DefaultCfg.txt` 解析失败。 */ public final class JsonUtils { private JsonUtils() { } + /** + * 清理历史 JSON 模板中对象或数组结束前的尾逗号。 + */ public static String sanitizeJson(String text) { if (text == null) { return null; } - // 去掉对象或数组结束前的尾逗号。 + // 删除对象或数组结束前的尾逗号,兼容历史配置写法。 return text.replaceAll(",\\s*([}\\]])", "$1"); } } \ No newline at end of file diff --git a/tools/mms-mapping/src/test/java/com/njcn/gather/icd/mapping/debug/GetIcdMmsJsonDebugRunner.java b/tools/mms-mapping/src/test/java/com/njcn/gather/icd/mapping/debug/GetIcdMmsJsonDebugRunner.java new file mode 100644 index 0000000..f8b67bf --- /dev/null +++ b/tools/mms-mapping/src/test/java/com/njcn/gather/icd/mapping/debug/GetIcdMmsJsonDebugRunner.java @@ -0,0 +1,214 @@ +package com.njcn.gather.icd.mapping.debug; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.njcn.gather.icd.mapping.component.MappingResponseConverter; +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.vo.IndexCandidateReportItemResponse; +import com.njcn.gather.icd.mapping.pojo.vo.IndexCandidateResponse; +import com.njcn.gather.icd.mapping.pojo.vo.MappingTaskResponse; +import com.njcn.gather.icd.mapping.service.MappingTaskService; +import org.springframework.boot.Banner; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.context.ConfigurableApplicationContext; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +/** + * `getIcdMmsJson` 本地调试入口。 + * + * 使用方式: + * 1. 先修改 ICD_FILE_PATH 指向本地真实 ICD/SCD 文件。 + * 2. 直接运行 main,先观察第一次调试返回的 indexCandidates。 + * 3. 按第一次返回结果补齐 buildSecondStepSelection() 中的绑定关系。 + * 4. 把 RUN_SECOND_STEP 改成 true,再次运行 main 获取最终 mappingJson。 + */ +public class GetIcdMmsJsonDebugRunner { + + /** 本地 ICD/SCD 文件路径,运行前请改成真实文件。 */ + private static final String ICD_FILE_PATH = "D:/temp/demo.icd"; + + /** 调试时可固定版本号,便于对比输出。 */ + private static final String VERSION = "20260421"; + + /** 调试作者标识。 */ + private static final String AUTHOR = "debug-user"; + + /** 是否输出格式化 JSON,便于人工查看。 */ + private static final boolean PRETTY_JSON = true; + + /** 是否把生成结果写入磁盘。 */ + private static final boolean SAVE_TO_DISK = false; + + /** saveToDisk=true 时使用的输出目录。 */ + private static final String OUTPUT_DIR = "D:/temp/mms-output"; + + /** + * 第二次正式生成开关。 + * 默认先只跑第一次,确认 groupKey/reportName/dataSetName/lnInst 候选值后再打开。 + */ + private static final boolean RUN_SECOND_STEP = false; + + public static void main(String[] args) { + try (ConfigurableApplicationContext context = new SpringApplicationBuilder(DebugApplication.class) + .web(WebApplicationType.NONE) + .bannerMode(Banner.Mode.OFF) + .logStartupInfo(false) + .run(args)) { + MappingTaskService mappingTaskService = context.getBean(MappingTaskService.class); + MappingResponseConverter responseConverter = context.getBean(MappingResponseConverter.class); + ObjectMapper objectMapper = createObjectMapper(); + + MappingTaskResponse firstResponse = debugNeedIndexSelection(mappingTaskService, responseConverter); + printResponse("第一次调试:获取索引候选", firstResponse, objectMapper); + printIndexCandidateSummary(firstResponse); + + if (!RUN_SECOND_STEP) { + System.out.println("第二次调试未开启。请先根据第一次返回结果补齐 buildSecondStepSelection(),再把 RUN_SECOND_STEP 改成 true。"); + return; + } + + MappingTaskResponse secondResponse = debugGenerateMapping(mappingTaskService, responseConverter); + printResponse("第二次调试:生成 MMS JSON", secondResponse, objectMapper); + } catch (Exception ex) { + throw new IllegalStateException("getIcdMmsJson 调试失败:" + ex.getMessage(), ex); + } + } + + /** + * 第一次调试:不传 indexSelection,只获取 icdDocument 和 indexCandidates。 + */ + private static MappingTaskResponse debugNeedIndexSelection(MappingTaskService mappingTaskService, + MappingResponseConverter responseConverter) { + GenerateFromIcdCommand command = buildBaseCommand(); + return responseConverter.fromSubmitResult(mappingTaskService.getIcdMmsJson(command)); + } + + /** + * 第二次调试:补齐 indexSelection 后直接生成最终 mappingJson。 + */ + private static MappingTaskResponse debugGenerateMapping(MappingTaskService mappingTaskService, + MappingResponseConverter responseConverter) { + List selectionGroups = buildSecondStepSelection(); + if (selectionGroups.isEmpty()) { + throw new IllegalArgumentException("第二次调试缺少 indexSelection,请先补齐 buildSecondStepSelection()"); + } + + GenerateFromIcdCommand command = buildBaseCommand(); + command.getIndexSelection().addAll(selectionGroups); + return responseConverter.fromSubmitResult(mappingTaskService.getIcdMmsJson(command)); + } + + /** + * 组装调试用基础命令,等价于接口中的 request 基础参数。 + */ + private static GenerateFromIcdCommand buildBaseCommand() { + Path icdPath = Paths.get(ICD_FILE_PATH); + if (!Files.exists(icdPath)) { + throw new IllegalArgumentException("ICD 文件不存在:" + icdPath.toAbsolutePath()); + } + + try { + GenerateFromIcdCommand command = new GenerateFromIcdCommand(); + command.setFileName(icdPath.getFileName().toString()); + command.setFileBytes(Files.readAllBytes(icdPath)); + command.setVersion(VERSION); + command.setAuthor(AUTHOR); + command.setPrettyJson(PRETTY_JSON); + command.setSaveToDisk(SAVE_TO_DISK); + command.setOutputDir(OUTPUT_DIR); + return command; + } catch (Exception ex) { + throw new IllegalArgumentException("读取 ICD 文件失败:" + ex.getMessage(), ex); + } + } + + /** + * 第二次调试的索引绑定示例。 + * + * 注意: + * 1. groupKey/groupDesc/reportName/dataSetName/lnInst 必须使用第一次返回的真实值。 + * 2. 下面示例仅作占位,默认不参与运行。 + */ + private static List buildSecondStepSelection() { + List groups = new ArrayList(); + + // 示例: + // IndexSelectionGroupCommand group = createSelectionGroup("HARM__DSSTHARM", "谐波数据"); + // group.getBindings().add(createBinding("brcbStHarm", "dsStHarm", "A相", "1")); + // group.getBindings().add(createBinding("brcbStHarm", "dsStHarm", "B相", "2")); + // group.getBindings().add(createBinding("brcbStHarm", "dsStHarm", "C相", "3")); + // groups.add(group); + + return groups; + } + + private static IndexSelectionGroupCommand createSelectionGroup(String groupKey, String groupDesc) { + IndexSelectionGroupCommand group = new IndexSelectionGroupCommand(); + group.setGroupKey(groupKey); + group.setGroupDesc(groupDesc); + return group; + } + + private static IndexBindingCommand createBinding(String reportName, + String dataSetName, + String label, + String lnInst) { + IndexBindingCommand binding = new IndexBindingCommand(); + binding.setReportName(reportName); + binding.setDataSetName(dataSetName); + binding.setLabel(label); + binding.setLnInst(lnInst); + return binding; + } + + /** + * 控制台输出候选摘要,方便把第一次返回值填回第二次调试配置。 + */ + private static void printIndexCandidateSummary(MappingTaskResponse response) { + if (response == null || response.getIndexCandidates() == null || response.getIndexCandidates().isEmpty()) { + return; + } + + System.out.println("===== 索引候选摘要 ====="); + for (IndexCandidateResponse candidate : response.getIndexCandidates()) { + System.out.println("groupKey=" + candidate.getGroupKey() + ", groupDesc=" + candidate.getGroupDesc()); + if (candidate.getReports() == null) { + continue; + } + for (IndexCandidateReportItemResponse report : candidate.getReports()) { + System.out.println(" reportName=" + report.getReportName() + + ", dataSetName=" + report.getDataSetName() + + ", availableLnInstValues=" + report.getAvailableLnInstValues()); + } + } + } + + private static void printResponse(String title, MappingTaskResponse response, ObjectMapper objectMapper) { + try { + System.out.println(); + System.out.println("===== " + title + " ====="); + System.out.println(objectMapper.writeValueAsString(response)); + } catch (Exception ex) { + throw new IllegalArgumentException("打印调试响应失败:" + ex.getMessage(), ex); + } + } + + private static ObjectMapper createObjectMapper() { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.enable(SerializationFeature.INDENT_OUTPUT); + return objectMapper; + } + + @SpringBootApplication(scanBasePackages = "com.njcn.gather.icd.mapping") + public static class DebugApplication { + } +}