diff --git a/PARSE_COMTRADE_VECTOR_API.md b/PARSE_COMTRADE_VECTOR_API.md deleted file mode 100644 index aa19f6a..0000000 --- a/PARSE_COMTRADE_VECTOR_API.md +++ /dev/null @@ -1,212 +0,0 @@ -# parseComtradeVector API 文档 - -## 1. 接口概述 - -- 接口名称:解析 COMTRADE 向量与电能质量指标 -- Controller:[WaveController.java](D:/Work/SourceCode/CN_Tool/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/controller/WaveController.java) -- 方法:`parseComtradeVector` -- 请求路径:`POST /wave/parseComtradeVector` -- Content-Type:`multipart/form-data` -- 返回类型:`HttpResult` - -用途说明: - -- 上传一组 COMTRADE `cfg/dat` 文件 -- 按原始波形逐周波计算电能质量指标 -- 返回总有效值、基波相角、谐波指标、序分量与不平衡度 - -## 2. 请求参数 - -### 2.1 文件参数 - -| 参数名 | 类型 | 必填 | 说明 | -| --- | --- | --- | --- | -| `cfgFile` | file | 是 | COMTRADE 配置文件 `.cfg` | -| `datFile` | file | 是 | COMTRADE 数据文件 `.dat` | - -### 2.2 表单参数 - -参数定义来源:[WaveComtradeParseParam.java](D:/Work/SourceCode/CN_Tool/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/pojo/param/WaveComtradeParseParam.java) - -| 参数名 | 类型 | 必填 | 默认值 | 说明 | -| --- | --- | --- | --- | --- | -| `parseType` | integer | 否 | `3` | 本接口内部固定按原始波形口径计算,建议传 `3` | -| `ptType` | integer | 否 | `0` | PT 接线方式:`0` 星形,`1` 三角,`2` 开口三角 | -| `pt` | number | 否 | `1` | PT 变比,电压结果按 `pt/1000` 换算为 `kV` | -| `ct` | number | 否 | `1` | CT 变比,电流结果按 `ct` 换算为 `A` | -| `monitorName` | string | 否 | `未命名测点` | 测点名称 | - -## 3. 调试请求示例 - -### 3.1 curl - -```bash -curl -X POST "http://localhost:8080/wave/parseComtradeVector" \ - -F "cfgFile=@D:/00-B7-8D-00-E4-09/1_20260321_201458_748.CFG" \ - -F "datFile=@D:/00-B7-8D-00-E4-09/1_20260321_201458_748.DAT" \ - -F "parseType=3" \ - -F "ptType=0" \ - -F "pt=1" \ - -F "ct=1" \ - -F "monitorName=监测点1" -``` - -### 3.2 Apifox / Postman - -- Method:`POST` -- URL:`http://localhost:8080/wave/parseComtradeVector` -- Body:`form-data` - -| Key | Type | 示例值 | -| --- | --- | --- | -| `cfgFile` | File | 选择 `.cfg` 文件 | -| `datFile` | File | 选择 `.dat` 文件 | -| `parseType` | Text | `3` | -| `ptType` | Text | `0` | -| `pt` | Text | `1` | -| `ct` | Text | `1` | -| `monitorName` | Text | `监测点1` | - -## 4. 响应结构 - -### 4.1 data 字段 - -定义来源:[WaveComtradeVectorResultVO.java](D:/Work/SourceCode/CN_Tool/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/pojo/vo/WaveComtradeVectorResultVO.java) - -| 字段名 | 类型 | 说明 | -| --- | --- | --- | -| `monitorName` | string | 测点名称 | -| `time` | string | 事件发生时刻 | -| `samplePerCycle` | integer | 每周波采样点数 | -| `cycleCount` | integer | 可计算周波数 | -| `vectorGroups` | array | 各电压/电流组的逐周波电能质量结果 | - -### 4.2 vectorGroups - -定义来源:[WaveVectorGroupDTO.java](D:/Work/SourceCode/CN_Tool/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/pojo/dto/WaveVectorGroupDTO.java) - -| 字段名 | 类型 | 说明 | -| --- | --- | --- | -| `channelName` | string | 通道名称,例如 `U1`、`I1` | -| `unit` | string | 单位,电压组为 `kV`,电流组为 `A` | -| `phaseCount` | integer | 相别数量 | -| `phaseNames` | array | 相别名称列表,例如 `A相/B相/C相` | -| `vectorSeries` | array | 当前组的逐周波结果序列 | - -### 4.3 vectorSeries - -定义来源:[WaveCycleVectorDTO.java](D:/Work/SourceCode/CN_Tool/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/pojo/dto/WaveCycleVectorDTO.java) - -| 字段名 | 类型 | 说明 | -| --- | --- | --- | -| `cycleIndex` | integer | 周波序号,从 `0` 开始 | -| `time` | number | 当前周波中点时刻,单位毫秒 | -| `phaseVectors` | array | 各相结果 | -| `positiveSequence` | object | 正序分量 | -| `negativeSequence` | object | 负序分量 | -| `zeroSequence` | object | 零序分量 | -| `unbalance` | object | 负序/零序不平衡度 | - -### 4.4 phaseVectors - -定义来源:[WavePhaseVectorDTO.java](D:/Work/SourceCode/CN_Tool/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/pojo/dto/WavePhaseVectorDTO.java) - -| 字段名 | 类型 | 说明 | -| --- | --- | --- | -| `phaseName` | string | 相别名称 | -| `totalRms` | number | 电压/电流总有效值 | -| `fundamentalAmplitude` | number | 基波幅值 | -| `fundamentalRms` | number | 基波有效值 | -| `fundamentalPhaseAngle` | number | 基波相角,单位度 | -| `harmonicVoltageContentRates` | array | 仅电压组返回,2~50 次谐波电压含有率 | -| `harmonicCurrentAmplitudes` | array | 仅电流组返回,2~50 次谐波电流幅值 | -| `harmonicDistortionRate` | number | 谐波畸变率,百分比 | - -### 4.5 谐波对象 - -定义来源:[WaveHarmonicDTO.java](D:/Work/SourceCode/CN_Tool/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/pojo/dto/WaveHarmonicDTO.java) - -| 字段名 | 类型 | 说明 | -| --- | --- | --- | -| `harmonicOrder` | integer | 谐波次数,当前范围 `2~50` | -| `amplitude` | number | 谐波幅值 | -| `rms` | number | 谐波有效值 | -| `rate` | number | 谐波占基波比率,百分比,仅电压组使用 | - -### 4.6 序分量与不平衡度 - -定义来源: -- [WaveSequenceVectorDTO.java](D:/Work/SourceCode/CN_Tool/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/pojo/dto/WaveSequenceVectorDTO.java) -- [WaveSequenceUnbalanceDTO.java](D:/Work/SourceCode/CN_Tool/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/pojo/dto/WaveSequenceUnbalanceDTO.java) - -| 字段名 | 类型 | 说明 | -| --- | --- | --- | -| `sequenceName` | string | 序分量名称 | -| `amplitude` | number | 序分量幅值 | -| `rms` | number | 序分量有效值 | -| `phaseAngle` | number | 序分量相角 | -| `negativeUnbalanceRate` | number | 负序不平衡度,`负序/正序 * 100%` | -| `zeroUnbalanceRate` | number | 零序不平衡度,`零序/正序 * 100%` | - -## 5. 成功响应示例 - -```json -{ - "code": "SUCCESS", - "message": "成功", - "data": { - "monitorName": "监测点1", - "time": "2026-03-21 20:14:58.748", - "samplePerCycle": 512, - "cycleCount": 30, - "vectorGroups": [ - { - "channelName": "U1", - "unit": "kV", - "phaseCount": 3, - "phaseNames": ["A相", "B相", "C相"], - "vectorSeries": [ - { - "cycleIndex": 0, - "time": -90.0, - "phaseVectors": [ - { - "phaseName": "A相", - "totalRms": 104.9367, - "fundamentalAmplitude": 148.4032, - "fundamentalRms": 104.9367, - "fundamentalPhaseAngle": 1.3258, - "harmonicVoltageContentRates": [ - { "harmonicOrder": 2, "amplitude": 0.4213, "rms": 0.2979, "rate": 0.2839 }, - { "harmonicOrder": 3, "amplitude": 0.3187, "rms": 0.2254, "rate": 0.2147 } - ], - "harmonicDistortionRate": 1.1284 - } - ], - "positiveSequence": { "sequenceName": "正序", "amplitude": 148.1021, "rms": 104.7238, "phaseAngle": 0.9864 }, - "negativeSequence": { "sequenceName": "负序", "amplitude": 0.8632, "rms": 0.6104, "phaseAngle": -117.6241 }, - "zeroSequence": { "sequenceName": "零序", "amplitude": 0.2261, "rms": 0.1599, "phaseAngle": 86.3174 }, - "unbalance": { "negativeUnbalanceRate": 0.5828, "zeroUnbalanceRate": 0.1527 } - } - ] - } - ] - } -} -``` - -## 6. 失败场景 - -| 场景 | 说明 | -| --- | --- | -| `cfgFile` 或 `datFile` 未上传 | 返回业务异常,提示“cfg 或 dat 文件不能为空” | -| CFG 文件格式错误 | 返回 CFG 解析失败 | -| DAT 文件为空或格式错误 | 返回 DAT 解析失败 | -| 采样点不足一个周波 | 返回波形文件数据缺失或向量计算失败 | -| COMTRADE 向量计算过程中出现异常 | 返回“COMTRADE 向量计算失败” | - -## 7. 备注 - -- 当前接口固定按原始波形口径计算,不依赖 `parseComtrade` 的 RMS 或特征值开关。 -- 当前谐波范围默认计算 `2~50` 次。 -- 如果单周波采样点数过低,高次谐波指标会受分辨率限制。 diff --git a/docs/superpowers/plans/2026-04-22-disk-monitor-implementation-plan.md b/docs/superpowers/plans/2026-04-22-disk-monitor-implementation-plan.md new file mode 100644 index 0000000..db9be9d --- /dev/null +++ b/docs/superpowers/plans/2026-04-22-disk-monitor-implementation-plan.md @@ -0,0 +1,988 @@ +# Disk Monitor Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 在当前仓库内完成磁盘监控页面、前端 API 契约、数据库 SQL 交付文件和手工验证入口,为后端接入完整磁盘监控能力提供可直接联调的前端实现基础。 + +**Architecture:** 以前端单页容器 `frontend/src/views/systemMonitor/diskMonitor/index.vue` 负责编排状态、加载配置、保存配置、触发手动执行和展示历史结果;页面拆分为摘要卡片、全局策略表单、盘符编辑器、通知编辑器、任务历史与详情抽屉。后端按已确认的规格提供 `/disk-monitor/**` 接口,本仓库额外产出一份 `doc/系统磁盘监控数据库设计.sql` 作为数据库交付物。该计划不在当前仓库内实现真实磁盘扫描、定时器和通知发送逻辑,只实现页面、契约和 SQL 文件。 + +**Tech Stack:** Vue 3 ` +``` + +- [ ] **Step 3: Create the global policy form component** + +Create `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\DiskMonitorPolicyForm.vue` with: + +```vue + + + +``` + +- [ ] **Step 4: Replace the placeholder page with container state and data loading** + +Replace `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\index.vue` with a container that imports the new API and components, keeping the back navigation and adding concise Chinese comments on the main business flow: + +```vue + +``` + +Expected: 页面不再显示占位摘要和占位面板,而是渲染摘要卡片和全局策略卡片,并能在挂载时请求配置与最近任务。 + +- [ ] **Step 5: Run the first full type-check after replacing the placeholder** + +Run: + +```powershell +cd D:\Work\SourceCode\CN_Tool_client\frontend +npm run type-check +``` + +Expected: 允许失败原因为“盘符编辑组件和任务列表组件尚未创建”,不允许出现 `form.ts`、`DiskMonitorSummary.vue`、`DiskMonitorPolicyForm.vue` 的类型错误。 + +- [ ] **Step 6: Commit the page state skeleton** + +Run: + +```powershell +git add D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\utils\form.ts D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\DiskMonitorSummary.vue D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\DiskMonitorPolicyForm.vue D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\index.vue +git commit -m "feat: scaffold disk monitor page state" +``` + +## Task 3: Build The Target Editor And Notification Editors + +**Files:** +- Create: `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\NotificationPathEditor.vue` +- Create: `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\NotificationHttpEditor.vue` +- Create: `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\DiskMonitorTargetDialog.vue` +- Create: `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\DiskMonitorTargetTable.vue` +- Modify: `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\index.vue` + +- [ ] **Step 1: Create the path notification array editor** + +Create `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\NotificationPathEditor.vue` with: + +```vue + +``` + +Expected: 组件支持新增、删除、编辑路径通知目标,不自行维护状态。 + +- [ ] **Step 2: Create the HTTP notification array editor** + +Create `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\NotificationHttpEditor.vue` with: + +```vue + +``` + +- [ ] **Step 3: Create the target edit dialog** + +Create `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\DiskMonitorTargetDialog.vue` with a dialog shell that hosts the two editor components: + +```vue + +``` + +Expected: 弹窗内至少包含盘符、启用开关、预警阈值、告警阈值、路径通知开关与编辑器、HTTP 通知开关与编辑器、备注。 + +- [ ] **Step 4: Create the target table with add/edit/delete events** + +Create `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\DiskMonitorTargetTable.vue` with: + +```vue + +``` + +Expected: 表格列至少显示盘符、是否监控、预警使用率、告警使用率、当前状态、最近扫描时间、最近使用率、操作按钮。 + +- [ ] **Step 5: Wire target CRUD into the page container** + +Update `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\index.vue` to add: + +```ts +import { createEmptyTarget, validateTarget } from './utils/form' +import DiskMonitorTargetTable from './components/DiskMonitorTargetTable.vue' +import DiskMonitorTargetDialog from './components/DiskMonitorTargetDialog.vue' + +const targetDialogVisible = ref(false) +const editingTargetIndex = ref(-1) +const editingTarget = ref(createEmptyTarget()) + +const openAddTarget = () => { + editingTargetIndex.value = -1 + editingTarget.value = createEmptyTarget() + targetDialogVisible.value = true +} + +const openEditTarget = (row: DiskMonitor.TargetItem, index: number) => { + editingTargetIndex.value = index + editingTarget.value = JSON.parse(JSON.stringify(row)) + targetDialogVisible.value = true +} + +const confirmTarget = () => { + const duplicatePool = targetList.value + .filter((_, index) => index !== editingTargetIndex.value) + .map(item => item.driveLetter) + const error = validateTarget(editingTarget.value, duplicatePool) + if (error) { + ElMessage.warning(error) + return + } + + if (editingTargetIndex.value === -1) { + targetList.value = [...targetList.value, JSON.parse(JSON.stringify(editingTarget.value))] + } else { + targetList.value = targetList.value.map((item, index) => + index === editingTargetIndex.value ? JSON.parse(JSON.stringify(editingTarget.value)) : item + ) + } + + targetDialogVisible.value = false +} + +const removeTarget = (index: number) => { + targetList.value = targetList.value.filter((_, rowIndex) => rowIndex !== index) +} +``` + +Expected: 页面可以新增、编辑、删除多个盘符,并能在保存前阻止重复盘符和非法阈值。 + +- [ ] **Step 6: Run lint and type-check after target editor wiring** + +Run: + +```powershell +cd D:\Work\SourceCode\CN_Tool_client\frontend +npm run lint -- src/views/systemMonitor/diskMonitor/index.vue src/views/systemMonitor/diskMonitor/components/NotificationPathEditor.vue src/views/systemMonitor/diskMonitor/components/NotificationHttpEditor.vue src/views/systemMonitor/diskMonitor/components/DiskMonitorTargetDialog.vue src/views/systemMonitor/diskMonitor/components/DiskMonitorTargetTable.vue +npm run type-check +``` + +Expected: 两个命令退出 `0`;不允许出现盘符编辑器和通知编辑器的 props/emits 类型错误。 + +- [ ] **Step 7: Commit the target editor slice** + +Run: + +```powershell +git add D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\NotificationPathEditor.vue D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\NotificationHttpEditor.vue D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\DiskMonitorTargetDialog.vue D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\DiskMonitorTargetTable.vue D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\index.vue +git commit -m "feat: add disk monitor target editors" +``` + +## Task 4: Add Manual Run, Job History, And Job Detail Views + +**Files:** +- Create: `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\DiskMonitorJobTable.vue` +- Create: `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\DiskMonitorJobDetailDrawer.vue` +- Modify: `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\index.vue` + +- [ ] **Step 1: Create the recent job table component** + +Create `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\DiskMonitorJobTable.vue` with: + +```vue + +``` + +Expected: 表格列至少包含任务编号、来源、开始时间、结束时间、状态、预警数量、告警数量和“查看详情”按钮。 + +- [ ] **Step 2: Create the job detail drawer** + +Create `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\DiskMonitorJobDetailDrawer.vue` with: + +```vue + +``` + +Expected: 抽屉中分两块表格展示 `results` 与 `notifyLogs`,字段名与规格文档一致。 + +- [ ] **Step 3: Wire manual run and history loading into the page** + +Update `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\index.vue` with: + +```ts +import { getDiskMonitorJobDetail } from '@/api/system/diskMonitor' +import DiskMonitorJobTable from './components/DiskMonitorJobTable.vue' +import DiskMonitorJobDetailDrawer from './components/DiskMonitorJobDetailDrawer.vue' + +const jobList = ref([]) +const jobDetailVisible = ref(false) +const jobDetail = ref(null) +const detailLoading = ref(false) + +const loadJobList = async () => { + loading.jobs = true + try { + const resp = await getDiskMonitorJobList({ pageNum: 1, pageSize: 10 }) + jobList.value = resp.data.records || [] + latestJob.value = jobList.value[0] || null + } finally { + loading.jobs = false + } +} + +const openJobDetail = async (row: DiskMonitor.JobListItem) => { + detailLoading.value = true + jobDetailVisible.value = true + try { + const resp = await getDiskMonitorJobDetail(row.id) + jobDetail.value = resp.data + } finally { + detailLoading.value = false + } +} +``` + +Expected: 手动执行完任务后可以刷新最近任务列表,并且可点开详情查看每个盘符结果与通知日志。 + +- [ ] **Step 4: Keep the page refresh flow single-sourced** + +Update `loadPageData` in `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\index.vue` so it only loads config + recent任务列表 once: + +```ts +const loadPageData = async () => { + loading.init = true + try { + const policyResp = await getDiskMonitorPolicyDetail() + policyForm.value = policyResp.data.policy + targetList.value = policyResp.data.targets || [] + await loadJobList() + } finally { + loading.init = false + } +} +``` + +Expected: 保存配置、手动执行、页面初始化都复用同一套刷新入口,不出现多处重复请求逻辑。 + +- [ ] **Step 5: Run the full frontend verification commands** + +Run: + +```powershell +cd D:\Work\SourceCode\CN_Tool_client\frontend +npm run lint +npm run type-check +``` + +Expected: 两个命令都退出 `0`。 + +- [ ] **Step 6: Commit the job history UI** + +Run: + +```powershell +git add D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\DiskMonitorJobTable.vue D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\DiskMonitorJobDetailDrawer.vue D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\index.vue +git commit -m "feat: add disk monitor job views" +``` + +## Task 5: Perform Manual Verification On The Hash Route + +**Files:** +- Verify only: `D:\Work\SourceCode\CN_Tool_client\frontend\src\routers\modules\staticRouter.ts` +- Verify only: `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\index.vue` +- Verify only: `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\*.vue` +- Verify only: `D:\Work\SourceCode\CN_Tool_client\doc\系统磁盘监控数据库设计.sql` + +- [ ] **Step 1: Start the frontend dev server** + +Run: + +```powershell +cd D:\Work\SourceCode\CN_Tool_client\frontend +npm run dev +``` + +Expected: Vite 启动成功;当前开发环境使用 `hash` 路由,因此目标页面地址为 `/#/systemMonitor/diskMonitor`。 + +- [ ] **Step 2: Verify configuration load and save behavior** + +Manual checklist: + +```text +1. 访问 /#/systemMonitor/diskMonitor,能看到摘要区、全局策略区、盘符列表区、最近任务区。 +2. 修改“启用监控”“启动即监控”“每日执行时间”后点击“保存配置”,页面给出成功提示。 +3. 新增两个盘符,例如 C: 与 D:,分别配置不同的预警/告警阈值。 +4. 为其中一个盘符新增本地目录通知和 HTTP 通知目标,保存后刷新页面,配置仍正确回显。 +5. 尝试录入重复盘符或告警阈值小于预警阈值,页面必须阻止提交并给出提示。 +``` + +Expected: 五项都成立。 + +- [ ] **Step 3: Verify manual run and job detail behavior** + +Manual checklist: + +```text +1. 点击“立即执行监控”,页面提示任务已启动。 +2. 最近任务列表出现一条新的 MANUAL 任务。 +3. 打开任务详情抽屉,能看到盘符结果表和通知日志表。 +4. 若后端暂未接通,页面应以接口错误提示结束,不得卡死或出现未捕获异常。 +``` + +Expected: 四项都成立。 + +- [ ] **Step 4: Verify the SQL artifact matches the approved spec** + +Run: + +```powershell +Get-Content -Raw D:\Work\SourceCode\CN_Tool_client\doc\系统磁盘监控数据库设计.sql +Get-Content -Raw D:\Work\SourceCode\CN_Tool_client\docs\superpowers\specs\2026-04-22-disk-monitor-design.md +``` + +Expected: SQL 文件包含同样的五张表和字段命名:`disk_monitor_policy`、`disk_monitor_target`、`disk_monitor_job`、`disk_monitor_result`、`disk_monitor_notify_log`。 + +- [ ] **Step 5: Record final verification status** + +Run: + +```powershell +cd D:\Work\SourceCode\CN_Tool_client\frontend +npm run lint +npm run type-check +git status --short +``` + +Expected: `lint` 和 `type-check` 退出 `0`;`git status --short` 只显示本功能相关改动和仓库原有未处理改动,不出现意外文件。 + +## Self-Review + +- Spec coverage: 计划覆盖了静态路由兜底、前端 API 契约、数据库 SQL 文件、摘要区、全局策略区、盘符与通知编辑、手动执行、最近任务、详情抽屉和验证步骤,与已批准规格一致。 +- Placeholder scan: 没有 `TODO`、`TBD`、`后续再说` 类占位语;每个任务都给了明确文件路径、代码骨架、命令和预期结果。 +- Type consistency: 计划统一使用 `DiskMonitor.PolicyItem`、`DiskMonitor.TargetItem`、`DiskMonitor.JobListItem`、`DiskMonitor.JobDetailData`、`createDefaultPolicy`、`createEmptyTarget`、`validatePolicy`、`validateTarget` 等命名,没有前后不一致的接口名。 diff --git a/docs/superpowers/plans/2026-04-22-mmsmapping-layout-and-config-plan.md b/docs/superpowers/plans/2026-04-22-mmsmapping-layout-and-config-plan.md new file mode 100644 index 0000000..fdf7080 --- /dev/null +++ b/docs/superpowers/plans/2026-04-22-mmsmapping-layout-and-config-plan.md @@ -0,0 +1,994 @@ +# MMS Mapping Layout And Config Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Rebuild the `mmsmapping` page into a two-phase ICD parsing and mapping generation workflow with a left-side file/result layout and a right-side configuration workspace driven by `DefaultCfg.txt`. + +**Architecture:** Keep the existing `getIcdMmsJson` API contract intact, but replace the raw JSON editor flow with a typed page container, a simplified left-top request panel, a left-bottom result panel, and a new right-side configuration panel. Use two utility modules to parse `DefaultCfg.txt`, generate a draft from `indexCandidates`, validate editable rows, and convert the draft back into `request.indexSelection`; validation relies on `vue-tsc`, `eslint`, and manual browser checks because this repo does not currently ship an automated frontend test runner. + +**Tech Stack:** Vue 3 ` +``` + +```vue + +``` + +- [ ] **Step 2: Keep the result panel focused on `mappingJson` and `problems` only** + +Update the header copy in `frontend/src/views/tools/mmsmapping/components/MappingResultPanel.vue`: + +```vue +
+
+

调试输出

+

左下只展示最近一次接口返回的 `mappingJson` 和 `problems`。

+
+ {{ responseStatusText }} +
+``` + +Keep the existing tab body structure, but do not reintroduce `icdDocument` or request JSON rendering. + +- [ ] **Step 3: Trim panel styles so the left-top card no longer reserves textarea space** + +Remove the obsolete request textarea blocks from `frontend/src/views/tools/mmsmapping/components/MappingRequestPanel.vue` and keep only these shared styles: + +```scss +.panel-content { + display: flex; + flex: 1; + flex-direction: column; + gap: 16px; + min-height: 0; +} + +.file-action-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 12px; + padding: 16px; +} + +.file-select-row { + display: flex; + gap: 12px; + align-items: center; + min-width: 0; + flex: 1; +} +``` + +- [ ] **Step 4: Run lint on the two touched panel components** + +Run: + +```powershell +cd frontend +npm run lint -- src/views/tools/mmsmapping/components/MappingRequestPanel.vue src/views/tools/mmsmapping/components/MappingResultPanel.vue +``` + +Expected: command exits with code `0` and no ESLint diagnostics for those files. + +- [ ] **Step 5: Commit the left-side panel refactor** + +Run: + +```powershell +git add frontend/src/views/tools/mmsmapping/components/MappingRequestPanel.vue frontend/src/views/tools/mmsmapping/components/MappingResultPanel.vue +git commit -m "refactor: simplify mmsmapping side panels" +``` + +### Task 3: Build The Right-Side Mapping Configuration Panel + +**Files:** +- Create: `frontend/src/views/tools/mmsmapping/components/MappingConfigPanel.vue` + +- [ ] **Step 1: Create the component shell with typed props, emits, and immutable patch helpers** + +Create `frontend/src/views/tools/mmsmapping/components/MappingConfigPanel.vue` with this script scaffold: + +```vue + +``` + +- [ ] **Step 2: Add the right-top base form, template error banner, and generate action** + +Use this top section template in `MappingConfigPanel.vue`: + +```vue + +``` + +Then add the minimum styles needed to keep the panel scrollable: + +```scss +.config-panel { + min-height: 0; +} + +.candidate-report-list { + display: grid; + gap: 8px; + margin-bottom: 16px; +} + +.candidate-report-item { + padding: 12px; + border: 1px solid #dbe3f0; + border-radius: 10px; + background: #ffffff; +} + +.draft-table { + width: 100%; +} +``` + +- [ ] **Step 4: Run type-check to validate the new configuration component** + +Run: + +```powershell +cd frontend +npm run type-check +``` + +Expected: command exits with code `0`; if it fails only because `index.vue` is not wired yet, proceed directly to Task 4 before re-running. + +- [ ] **Step 5: Commit the new configuration panel** + +Run: + +```powershell +git add frontend/src/views/tools/mmsmapping/components/MappingConfigPanel.vue +git commit -m "feat: add mmsmapping config panel" +``` + +### Task 4: Rebuild `index.vue` Around Parse-And-Generate Flow + +**Files:** +- Modify: `frontend/src/views/tools/mmsmapping/index.vue` + +- [ ] **Step 1: Replace raw JSON state with typed form, candidate, draft, and template-error state** + +In `frontend/src/views/tools/mmsmapping/index.vue`, replace `requestJsonText`, `defaultRequestPayload`, and the old JSON parsing helpers with this state block. Keep the existing `unwrapApiPayload`, `getErrorMessage`, `handleIcdFileChange`, `mappingJsonPreview`, `problemList`, and status-tag computed blocks, but rewire them to the new request flow: + +```ts +import { computed, ref } from 'vue' +import { ElMessage } from 'element-plus' +import type { ResultData } from '@/api/interface' +import { getIcdMmsJsonApi } from '@/api/tools/mmsmapping' +import type { MmsMapping } from '@/api/tools/mmsmapping/interface' +import MappingRequestPanel from './components/MappingRequestPanel.vue' +import MappingResultPanel from './components/MappingResultPanel.vue' +import MappingConfigPanel from './components/MappingConfigPanel.vue' +import { parseDefaultCfgTemplate } from './utils/defaultCfg' +import { + buildDraftGroups, + buildIndexSelectionPayload, + createBaseRequestPayload, + validateDraftGroups +} from './utils/mappingDraft' + +const selectedIcdFile = ref(null) +const responsePayload = ref(null) +const activeResultTab = ref<'mapping' | 'problem'>('mapping') +const requestForm = ref({ + version: '1.0', + author: 'system' +}) +const parsedCandidates = ref([]) +const configDraft = ref([]) +const templateError = ref('') +const defaultCfgTemplate = ref({ reportList: [] }) +const isParsing = ref(false) +const isGenerating = ref(false) +const icdFileAccept = '.icd,.cid,.scd,.xml' + +try { + defaultCfgTemplate.value = parseDefaultCfgTemplate() +} catch { + templateError.value = 'DefaultCfg.txt 解析失败,请检查模板内容' +} + +const isSubmitting = computed(() => isParsing.value || isGenerating.value) +const canGenerate = computed(() => Boolean(selectedIcdFile.value && configDraft.value.length && !templateError.value)) +const selectedIcdFileName = computed(() => selectedIcdFile.value?.name || '') +``` + +- [ ] **Step 2: Add separate parse and generate handlers** + +Use these handlers in `index.vue`: + +```ts +const handleParseIcd = async () => { + if (!selectedIcdFile.value) { + ElMessage.warning('请先选择 ICD 文件') + return + } + + if (templateError.value) { + ElMessage.error(templateError.value) + return + } + + isParsing.value = true + responsePayload.value = null + + try { + const response = await getIcdMmsJsonApi({ + icdFile: selectedIcdFile.value, + request: { + ...createBaseRequestPayload(requestForm.value), + indexSelection: [] + } + }) + + const payload = unwrapApiPayload(response) + responsePayload.value = payload + parsedCandidates.value = payload.indexCandidates || [] + configDraft.value = buildDraftGroups(defaultCfgTemplate.value, parsedCandidates.value) + activeResultTab.value = payload.mappingJson ? 'mapping' : 'problem' + ElMessage.success(payload.message || 'ICD 解析完成') + } catch (error) { + responsePayload.value = null + parsedCandidates.value = [] + configDraft.value = [] + ElMessage.error(getErrorMessage(error)) + } finally { + isParsing.value = false + } +} + +const handleGenerateMapping = async () => { + if (!selectedIcdFile.value) { + ElMessage.warning('请先选择 ICD 文件') + return + } + + const draftProblems = validateDraftGroups(configDraft.value) + if (draftProblems.length) { + ElMessage.warning(draftProblems[0]) + responsePayload.value = { + status: 'NEED_INDEX_SELECTION', + message: '当前配置不完整,请继续修正', + problems: draftProblems + } + activeResultTab.value = 'problem' + return + } + + isGenerating.value = true + responsePayload.value = null + + try { + const response = await getIcdMmsJsonApi({ + icdFile: selectedIcdFile.value, + request: { + ...createBaseRequestPayload(requestForm.value), + indexSelection: buildIndexSelectionPayload(configDraft.value) + } + }) + + const payload = unwrapApiPayload(response) + responsePayload.value = payload + activeResultTab.value = payload.mappingJson ? 'mapping' : 'problem' + + if (payload.status === 'FAILED') { + ElMessage.error(payload.message || '映射生成失败') + return + } + + ElMessage.success(payload.message || '映射生成完成') + } catch (error) { + responsePayload.value = null + ElMessage.error(getErrorMessage(error)) + } finally { + isGenerating.value = false + } +} + +const resetPage = () => { + selectedIcdFile.value = null + responsePayload.value = null + parsedCandidates.value = [] + configDraft.value = [] + activeResultTab.value = 'mapping' + requestForm.value = { + version: '1.0', + author: 'system' + } +} +``` + +- [ ] **Step 3: Rebuild the template and layout to left-stack the request/result panels and mount the new configuration panel** + +Replace the page template and layout styles in `index.vue` with: + +```vue + +``` + +```scss +.mms-mapping-layout { + display: grid; + grid-template-columns: minmax(320px, 0.9fr) minmax(0, 1.1fr); + gap: 16px; + width: 100%; + height: 100%; + overflow: hidden; +} + +.left-panel-stack { + display: grid; + grid-template-rows: auto minmax(0, 1fr); + gap: 16px; + min-height: 0; +} + +@media (max-width: 1280px) { + .mms-mapping-layout { + grid-template-columns: 1fr; + } +} +``` + +- [ ] **Step 4: Run lint and type-check on the full frontend after container integration** + +Run: + +```powershell +cd frontend +npm run lint +npm run type-check +``` + +Expected: both commands exit with code `0`; the lint step may rewrite formatting, so inspect the diff before committing. + +- [ ] **Step 5: Commit the new page flow** + +Run: + +```powershell +git add frontend/src/views/tools/mmsmapping/index.vue frontend/src/views/tools/mmsmapping/components/MappingRequestPanel.vue frontend/src/views/tools/mmsmapping/components/MappingResultPanel.vue frontend/src/views/tools/mmsmapping/components/MappingConfigPanel.vue frontend/src/views/tools/mmsmapping/utils/defaultCfg.ts frontend/src/views/tools/mmsmapping/utils/mappingDraft.ts frontend/src/api/tools/mmsmapping/interface/index.ts +git commit -m "feat: rebuild mmsmapping page workflow" +``` + +### Task 5: Verify The Two-Phase Workflow Manually + +**Files:** +- Verify only: `frontend/src/views/tools/mmsmapping/index.vue` +- Verify only: `frontend/src/views/tools/mmsmapping/components/MappingRequestPanel.vue` +- Verify only: `frontend/src/views/tools/mmsmapping/components/MappingResultPanel.vue` +- Verify only: `frontend/src/views/tools/mmsmapping/components/MappingConfigPanel.vue` +- Verify only: `frontend/src/views/tools/mmsmapping/utils/defaultCfg.ts` +- Verify only: `frontend/src/views/tools/mmsmapping/utils/mappingDraft.ts` + +- [ ] **Step 1: Run the full static verification suite one more time** + +Run: + +```powershell +cd frontend +npm run lint +npm run type-check +``` + +Expected: both commands exit with code `0`. + +- [ ] **Step 2: Start the frontend and open the MMS mapping route** + +Run: + +```powershell +cd frontend +npm run dev +``` + +Expected: Vite starts successfully and prints a local URL. Open the page route that resolves to `/tools/mmsMapping`. + +- [ ] **Step 3: Verify the parse flow** + +Manual checklist: + +```text +1. 进入页面后,左上仅看到文件选择、解析按钮和状态标签。 +2. 选择一个合法 ICD 文件后,左上状态变为“待提交”或同类准备状态。 +3. 点击“解析 ICD”后,右侧出现 version/author 表单、默认模板分组、候选辅助信息。 +4. 左下不出现 icdDocument 树;只显示 mappingJson/problems 页签。 +5. 若后端返回 NEED_INDEX_SELECTION,左下默认切到 problems。 +``` + +Expected: all five observations are true. + +- [ ] **Step 4: Verify repeated mapping generation without re-parsing** + +Manual checklist: + +```text +1. 在右侧选择一个模板分组,补齐 reportName、dataSetName、lnInst。 +2. 点击“生成映射”,确认左下显示新的 mappingJson 或新的 problems。 +3. 不重新点击“解析 ICD”,直接修改右侧任意一行的 lnInst。 +4. 再次点击“生成映射”,确认左下结果刷新为第二次生成结果。 +5. 若第二次返回 NEED_INDEX_SELECTION 或 FAILED,右侧已编辑内容仍然保留。 +``` + +Expected: repeated generation works on the same parsed candidate set. + +- [ ] **Step 5: Verify reset and file replacement behavior** + +Manual checklist: + +```text +1. 点击“清空”,确认左下结果、右侧草稿、当前候选缓存全部清空。 +2. 重新选择另一个 ICD 文件,确认旧的候选和草稿不会继续显示。 +3. 重新点击“解析 ICD”后,右侧根据新文件重新生成默认模板。 +``` + +Expected: reset and file replacement force a fresh parse cycle. + +## Self-Review + +- Spec coverage: the tasks cover left-top simplification, left-bottom result-only output, right-side auto-generated template editing, hidden request defaults, repeated generation, candidate matching, template-parse failure handling, and lint/type-check/manual validation. +- Placeholder scan: no `TODO`/`TBD`/“later” markers remain; every task includes exact file paths, code blocks, commands, and expected results. +- Type consistency: the plan uses the same names throughout: `BaseRequestForm`, `DefaultCfgTemplate`, `MappingDraftGroup`, `parseDefaultCfgTemplate`, `buildDraftGroups`, `validateDraftGroups`, `buildIndexSelectionPayload`, and `createBaseRequestPayload`. diff --git a/frontend/src/api/system/diskMonitor/interface/index.ts b/frontend/src/api/system/diskMonitor/interface/index.ts index 0d27871..892ec89 100644 --- a/frontend/src/api/system/diskMonitor/interface/index.ts +++ b/frontend/src/api/system/diskMonitor/interface/index.ts @@ -77,7 +77,8 @@ export namespace DiskMonitor { } export interface JobListItem { - id: number + id?: number + jobId?: number jobNo: string jobSource: JobSource startedAt: string diff --git a/frontend/src/api/tools/mmsmapping/index.ts b/frontend/src/api/tools/mmsmapping/index.ts new file mode 100644 index 0000000..2dc3341 --- /dev/null +++ b/frontend/src/api/tools/mmsmapping/index.ts @@ -0,0 +1,31 @@ +import http from '@/api' +import type { MmsMapping } from './interface' + +const buildIcdFormData = (icdFile: File) => { + const formData = new FormData() + + formData.append('icdFile', icdFile) + + return formData +} + +export const getIcdApi = (params: MmsMapping.GetIcdParams) => { + const formData = buildIcdFormData(params.icdFile) + + // 关键业务节点:解析 ICD 按钮改走独立 get-icd 接口,只上传当前选择的 ICD 文件。 + return http.post('/api/mms-mapping/get-icd', formData, { + headers: { 'Content-Type': 'multipart/form-data' } + }) +} + +export const getIcdMmsJsonApi = (params: MmsMapping.GetIcdMmsJsonParams) => { + const formData = buildIcdFormData(params.icdFile) + + // 接口文档要求 request 以 application/json 分段提交,避免后端按普通字符串丢失 JSON 结构。 + formData.append('request', new Blob([JSON.stringify(params.request)], { type: 'application/json' })) + + // 关键业务节点:生成映射仍走 get-icd-mms-json,提交时保持 icdFile + request 的 multipart 结构。 + return http.post('/api/mms-mapping/get-icd-mms-json', formData, { + headers: { 'Content-Type': 'multipart/form-data' } + }) +} diff --git a/frontend/src/api/tools/mmsmapping/interface/index.ts b/frontend/src/api/tools/mmsmapping/interface/index.ts new file mode 100644 index 0000000..bd9a195 --- /dev/null +++ b/frontend/src/api/tools/mmsmapping/interface/index.ts @@ -0,0 +1,68 @@ +export namespace MmsMapping { + export interface GetIcdParams { + icdFile: File + } + + export interface IndexSelectionBinding { + reportName: string + dataSetName: string + label: string + lnInst: string + } + + export interface IndexSelectionGroup { + groupKey: string + groupDesc?: string + bindings: IndexSelectionBinding[] + } + + export interface GetIcdMmsJsonRequestPayload { + version: string + author: string + saveToDisk: boolean + prettyJson: boolean + outputDir: string + indexSelection: IndexSelectionGroup[] + } + + export interface GetIcdMmsJsonParams { + icdFile: File + request: GetIcdMmsJsonRequestPayload + } + + export interface IcdDocument { + [key: string]: unknown + } + + export interface IndexCandidateReport { + reportName?: string + dataSetName?: string + reportDesc?: string + availableLnInstValues?: string[] + } + + export interface IndexCandidateGroup { + groupKey?: string + groupDesc?: string + reportCount?: number + templateLabels?: string[] + reports?: IndexCandidateReport[] + } + + export type MappingTaskStatus = 'SUCCESS' | 'NEED_INDEX_SELECTION' | 'FAILED' | (string & {}) + + export interface MappingTaskResponse { + status?: MappingTaskStatus + message?: string + icdDocument?: IcdDocument + mappingJson?: string + savedPath?: string + indexCandidates?: IndexCandidateGroup[] + problems?: string[] + } + + export interface BaseRequestForm { + version: string + author: string + } +} diff --git a/frontend/src/routers/modules/staticRouter.ts b/frontend/src/routers/modules/staticRouter.ts index c7481fb..e72598d 100644 --- a/frontend/src/routers/modules/staticRouter.ts +++ b/frontend/src/routers/modules/staticRouter.ts @@ -54,7 +54,7 @@ export const staticRouter: RouteRecordRaw[] = [ path: '/tools/mmsMapping', name: 'toolMmsMapping', alias: ['/tools/mmsmapping', '/tools/mms-mapping'], - component: () => import('@/views/tools/mmsMapping/index.vue'), + component: () => import('@/views/tools/mmsmapping/index.vue'), meta: { title: 'MMS 映射' } diff --git a/frontend/src/views/systemMonitor/2026-04-22-disk-monitor-design.md b/frontend/src/views/systemMonitor/2026-04-22-disk-monitor-design.md new file mode 100644 index 0000000..493cc00 --- /dev/null +++ b/frontend/src/views/systemMonitor/2026-04-22-disk-monitor-design.md @@ -0,0 +1,1034 @@ +# 系统磁盘监控功能设计 + +## 1. 背景 + +当前仓库已经存在系统监控入口页与磁盘监控占位页: + +- `frontend/src/views/systemMonitor/index.vue` +- `frontend/src/views/systemMonitor/diskMonitor/index.vue` + +本次目标是在现有“系统监控 > 磁盘监控”入口下,补齐一套完整的磁盘监控方案设计,覆盖: + +- 多盘符监控配置 +- 盘符级预警/告警阈值配置 +- 配置保存与回显 +- 应用启动后自动执行一次监控 +- 每日统一时间执行定时监控 +- 本地目录/网络路径通知 +- HTTP 回调通知 +- 监控结果与通知日志留痕 +- MySQL 表结构与建表 SQL + +## 2. 已确认需求结论 + +### 2.1 业务范围 + +- 磁盘监控配置由页面完成,配置后持久化保存 +- 页面支持配置磁盘预警和告警阈值,配置后持久化保存 +- 支持多个盘符监控,每个盘符独立配置预警和告警阈值 +- 应用启动时立即执行一次监控 +- 支持录入通知路径,录入完成后按规则通知 +- 支持每天定时执行监控 + +### 2.2 已确认的约束 + +- 监控阈值采用“已使用率”而不是“剩余容量” +- “系统启动时即可进行监控”指“应用启动后立即执行一次”,不包含操作系统开机自启动 +- 每日定时监控采用“全局统一时间”,而不是每盘符独立时间 +- 通知方式同时支持: + - 本地目录路径 + - 网络共享路径 + - HTTP 回调地址 +- 通知规则固定为: + - 告警状态下每次监控都通知 + - 其他状态按状态变化通知 + +### 2.3 当前仓库边界 + +- 当前仓库包含前端和 Electron 壳层 +- 当前仓库中未提供可直接修改的业务后端源码 +- 因此本设计覆盖完整业务方案,但在本仓库实际落地时,前端页面和接口契约可直接实现,真正的扫描、调度、通知执行需由后端配合实现 + +## 3. 推荐方案 + +采用“分层业务方案”,但按最终确认结果收敛为 5 张核心表: + +1. `disk_monitor_policy` +2. `disk_monitor_target` +3. `disk_monitor_job` +4. `disk_monitor_result` +5. `disk_monitor_notify_log` + +其中: + +- `disk_monitor_policy` 负责全局监控策略 +- `disk_monitor_target` 单独存盘符配置,并合并该盘符的通知目标配置 +- `disk_monitor_job` 记录每次监控批次 +- `disk_monitor_result` 记录批次内每个盘符的扫描结果 +- `disk_monitor_notify_log` 记录通知发送结果 + +该方案兼顾当前需求与后续可维护性,避免把状态判断、任务记录、通知日志混在单表里。 + +## 4. 页面设计 + +## 4.1 页面入口 + +页面入口保留在系统监控模块下: + +- 入口页:`frontend/src/views/systemMonitor/index.vue` +- 目标页:`frontend/src/views/systemMonitor/diskMonitor/index.vue` + +磁盘监控页作为独立配置页,不再只是占位内容。 + +## 4.2 页面布局 + +页面分为 4 个区域。 + +### 4.2.1 顶部摘要区 + +展示当前系统的全局监控状态,字段建议包括: + +- 监控总开关 +- 应用启动即监控 +- 每日统一执行时间 +- 最近执行时间 +- 最近执行状态 +- 监控盘符数量 +- 当前告警盘符数量 + +### 4.2.2 全局策略区 + +用于维护全局监控策略,字段包括: + +- 是否启用磁盘监控 +- 应用启动后立即执行一次 +- 每日统一监控时间 +- 通知规则说明 + +通知规则在页面上只展示,不开放复杂配置: + +- 预警:状态变化通知 +- 告警:每次命中都通知 + +### 4.2.3 通知配置区 + +通知配置跟盘符走,每个盘符都带一组通知目标。页面上建议以“盘符展开卡片”或“盘符表格 + 弹窗”方式编辑通知配置。 + +每个盘符支持两类通知目标: + +- 路径通知: + - 本地目录,如 `D:\disk-monitor` + - 网络共享目录,如 `\\server\share\disk-monitor` +- HTTP 回调通知: + - 如 `http://127.0.0.1:8080/api/disk/notify` + +每条通知目标包含: + +- 名称 +- 地址 +- 是否启用 +- 备注 + +HTTP 目标额外包含: + +- 请求方法,默认 `POST` +- 超时时间,默认 `5000ms` + +### 4.2.4 盘符配置区 + +盘符配置区以表格展示,支持新增、编辑、删除盘符配置。字段包括: + +- 盘符 +- 是否监控 +- 预警使用率 +- 告警使用率 +- 当前状态 +- 最近扫描时间 +- 最近使用率 +- 操作 + +## 4.3 页面交互规则 + +### 4.3.1 配置保存 + +页面保存时一次性提交整页配置,包含: + +- 全局策略 +- 所有盘符配置 +- 每个盘符下的路径通知配置 +- 每个盘符下的 HTTP 通知配置 + +### 4.3.2 配置回显 + +页面加载时一次性读取当前生效配置,并回显: + +- 全局策略 +- 盘符列表 +- 各盘符的通知配置 +- 最近执行摘要 + +### 4.3.3 手动执行 + +页面建议提供“立即执行监控”按钮,用于: + +- 联调验证 +- 保存配置后立刻测试 +- 验证通知逻辑 + +### 4.3.4 最近执行结果 + +页面下半区建议展示最近若干次执行记录,支持查看: + +- 任务来源 +- 开始/结束时间 +- 执行状态 +- 预警数量 +- 告警数量 +- 每盘符结果 +- 通知发送记录 + +## 4.4 页面校验规则 + +- 盘符不能为空 +- 盘符不能重复 +- 预警使用率必须在 `1-100` 之间 +- 告警使用率必须在 `1-100` 之间 +- 告警使用率必须大于等于预警使用率 +- 每日统一执行时间不能为空,格式固定 `HH:mm:ss` +- 路径通知目标不能为空 +- HTTP 通知目标必须是合法 URL + +## 5. 数据库设计 + +## 5.1 设计原则 + +- 使用 MySQL 8.0 +- 使用 `InnoDB` +- 字符集使用 `utf8mb4` +- 配置表与日志表分离 +- 为减少配置调整对历史记录的影响,表之间以“索引型关联”为主,不强依赖数据库外键 +- 通知目标与盘符配置合并到 `disk_monitor_target`,使用 JSON 字段存多条通知目标 + +## 5.2 表结构概览 + +### 5.2.1 `disk_monitor_policy` + +用途:存全局策略,仅保留一套当前生效配置。 + +关键字段: + +- `id` +- `policy_name` +- `monitor_enabled` +- `run_on_app_start` +- `daily_run_time` +- `warning_notify_mode` +- `alarm_notify_mode` +- `last_job_id` +- `remark` +- `created_by` +- `created_at` +- `updated_by` +- `updated_at` + +### 5.2.2 `disk_monitor_target` + +用途:存盘符配置,并合并该盘符的通知配置。 + +关键字段: + +- `id` +- `policy_id` +- `drive_letter` +- `monitor_enabled` +- `warning_usage_percent` +- `alarm_usage_percent` +- `notify_path_enabled` +- `notify_path_list_json` +- `notify_http_enabled` +- `notify_http_list_json` +- `last_status` +- `last_scan_time` +- `last_used_percent` +- `remark` +- `created_by` +- `created_at` +- `updated_by` +- `updated_at` + +### 5.2.3 `disk_monitor_job` + +用途:记录每次监控任务批次。 + +关键字段: + +- `id` +- `job_no` +- `job_source` +- `planned_time` +- `started_at` +- `finished_at` +- `job_status` +- `target_count` +- `success_count` +- `warning_count` +- `alarm_count` +- `message` +- `created_at` + +### 5.2.4 `disk_monitor_result` + +用途:记录一次任务下每个盘符的扫描结果。 + +关键字段: + +- `id` +- `job_id` +- `target_id` +- `drive_letter` +- `total_bytes` +- `used_bytes` +- `free_bytes` +- `used_percent` +- `current_status` +- `previous_status` +- `status_changed` +- `should_notify` +- `notify_reason` +- `scan_time` +- `message` + +### 5.2.5 `disk_monitor_notify_log` + +用途:记录每次通知发送日志。 + +关键字段: + +- `id` +- `job_id` +- `result_id` +- `target_id` +- `drive_letter` +- `notify_level` +- `channel_type` +- `channel_target` +- `notify_title` +- `notify_content` +- `send_status` +- `response_message` +- `sent_at` + +## 5.3 JSON 字段结构 + +### 5.3.1 `notify_path_list_json` + +```json +[ + { + "path": "D:\\disk-monitor", + "name": "本地通知目录", + "enabled": true + }, + { + "path": "\\\\server\\share\\disk-monitor", + "name": "共享目录通知", + "enabled": true + } +] +``` + +### 5.3.2 `notify_http_list_json` + +```json +[ + { + "url": "http://127.0.0.1:8080/api/disk/notify", + "name": "运维平台回调", + "method": "POST", + "timeoutMs": 5000, + "enabled": true + } +] +``` + +## 5.4 状态值约定 + +### 5.4.1 盘符状态 + +- `UNKNOWN` +- `NORMAL` +- `WARNING` +- `ALARM` + +### 5.4.2 任务来源 + +- `APP_START` +- `DAILY_SCHEDULE` +- `MANUAL` + +### 5.4.3 任务状态 + +- `RUNNING` +- `SUCCESS` +- `PARTIAL_SUCCESS` +- `FAILED` + +### 5.4.4 通知原因 + +- `ALARM_EVERY_TIME` +- `STATUS_CHANGED` +- `NO_NOTIFY` + +### 5.4.5 通知级别 + +- `WARNING` +- `ALARM` +- `RECOVER` + +### 5.4.6 通知通道类型 + +- `PATH` +- `HTTP` + +### 5.4.7 通知发送状态 + +- `SUCCESS` +- `FAILED` + +## 5.5 索引设计 + +### 5.5.1 `disk_monitor_policy` + +- 主键:`id` + +### 5.5.2 `disk_monitor_target` + +- 主键:`id` +- 唯一索引:`uk_drive_letter(drive_letter)` +- 普通索引:`idx_policy_id(policy_id)` + +### 5.5.3 `disk_monitor_job` + +- 主键:`id` +- 唯一索引:`uk_job_no(job_no)` +- 普通索引:`idx_job_source(job_source)` +- 普通索引:`idx_started_at(started_at)` + +### 5.5.4 `disk_monitor_result` + +- 主键:`id` +- 普通索引:`idx_job_id(job_id)` +- 普通索引:`idx_target_id(target_id)` +- 普通索引:`idx_drive_letter(drive_letter)` +- 普通索引:`idx_scan_time(scan_time)` + +### 5.5.5 `disk_monitor_notify_log` + +- 主键:`id` +- 普通索引:`idx_job_id(job_id)` +- 普通索引:`idx_result_id(result_id)` +- 普通索引:`idx_target_id(target_id)` +- 普通索引:`idx_sent_at(sent_at)` + +## 5.6 MySQL 建表 SQL + +```sql +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='磁盘监控通知日志表'; +``` + +## 6. 接口设计 + +## 6.1 配置查询 + +### 接口 + +`GET /disk-monitor/policy/detail` + +### 用途 + +页面初始化时一次性返回: + +- 当前生效的全局策略 +- 所有盘符配置 + +### 返回结构 + +```json +{ + "code": "A0000", + "message": "success", + "data": { + "policy": { + "id": 1, + "policyName": "默认磁盘监控策略", + "monitorEnabled": true, + "runOnAppStart": true, + "dailyRunTime": "08:30:00", + "warningNotifyMode": "STATUS_CHANGE", + "alarmNotifyMode": "EVERY_TIME", + "lastJobId": 1001, + "remark": "" + }, + "targets": [ + { + "id": 11, + "driveLetter": "C:", + "monitorEnabled": true, + "warningUsagePercent": 80, + "alarmUsagePercent": 90, + "notifyPathEnabled": true, + "notifyPathList": [ + { + "path": "D:\\disk-monitor", + "name": "本地目录", + "enabled": true + } + ], + "notifyHttpEnabled": true, + "notifyHttpList": [ + { + "url": "http://127.0.0.1:8080/api/disk/notify", + "name": "运维平台", + "method": "POST", + "timeoutMs": 5000, + "enabled": true + } + ], + "lastStatus": "NORMAL", + "lastScanTime": "2026-04-22 08:30:00", + "lastUsedPercent": 76.42, + "remark": "" + } + ] + } +} +``` + +## 6.2 配置保存 + +### 接口 + +`POST /disk-monitor/policy/save` + +### 用途 + +页面点击保存时,整页提交一次: + +- 全局策略 +- 所有盘符配置 +- 所有通知配置 + +### 请求结构 + +```json +{ + "policy": { + "id": 1, + "policyName": "默认磁盘监控策略", + "monitorEnabled": true, + "runOnAppStart": true, + "dailyRunTime": "08:30:00", + "warningNotifyMode": "STATUS_CHANGE", + "alarmNotifyMode": "EVERY_TIME", + "remark": "" + }, + "targets": [ + { + "id": 11, + "driveLetter": "C:", + "monitorEnabled": true, + "warningUsagePercent": 80, + "alarmUsagePercent": 90, + "notifyPathEnabled": true, + "notifyPathList": [ + { + "path": "D:\\disk-monitor", + "name": "本地目录", + "enabled": true + } + ], + "notifyHttpEnabled": true, + "notifyHttpList": [ + { + "url": "http://127.0.0.1:8080/api/disk/notify", + "name": "运维平台", + "method": "POST", + "timeoutMs": 5000, + "enabled": true + } + ], + "remark": "" + } + ] +} +``` + +### 保存规则 + +- 已存在盘符则更新 +- 新增盘符则插入 +- 请求中已移除的盘符可按业务选择: + - 直接删除 + - 逻辑删除 +- 必须做完整参数校验 + +## 6.3 手动执行监控 + +### 接口 + +`POST /disk-monitor/job/run` + +### 用途 + +页面“立即执行监控”按钮触发一次人工监控。 + +### 请求结构 + +```json +{ + "jobSource": "MANUAL" +} +``` + +### 返回结构 + +```json +{ + "code": "A0000", + "message": "任务已启动", + "data": { + "jobId": 1002, + "jobNo": "DM202604220001" + } +} +``` + +## 6.4 最近任务列表 + +### 接口 + +`POST /disk-monitor/job/list` + +### 用途 + +查询最近执行记录,用于页面结果区展示。 + +### 请求结构 + +```json +{ + "pageNum": 1, + "pageSize": 10 +} +``` + +### 列表字段 + +- `jobId` +- `jobNo` +- `jobSource` +- `startedAt` +- `finishedAt` +- `jobStatus` +- `targetCount` +- `warningCount` +- `alarmCount` +- `message` + +## 6.5 任务详情 + +### 接口 + +`GET /disk-monitor/job/{jobId}/detail` + +### 用途 + +查看一次任务的盘符结果和通知发送情况。 + +### 返回结构 + +```json +{ + "code": "A0000", + "message": "success", + "data": { + "job": { + "id": 1002, + "jobNo": "DM202604220001", + "jobSource": "MANUAL", + "jobStatus": "SUCCESS" + }, + "results": [ + { + "resultId": 2001, + "targetId": 11, + "driveLetter": "C:", + "totalBytes": 512000000000, + "usedBytes": 430000000000, + "freeBytes": 82000000000, + "usedPercent": 83.98, + "currentStatus": "WARNING", + "previousStatus": "NORMAL", + "statusChanged": true, + "shouldNotify": true, + "notifyReason": "STATUS_CHANGED", + "scanTime": "2026-04-22 15:10:00", + "message": "" + } + ], + "notifyLogs": [ + { + "id": 3001, + "resultId": 2001, + "driveLetter": "C:", + "notifyLevel": "WARNING", + "channelType": "HTTP", + "channelTarget": "http://127.0.0.1:8080/api/disk/notify", + "sendStatus": "SUCCESS", + "responseMessage": "200 OK", + "sentAt": "2026-04-22 15:10:02" + } + ] + } +} +``` + +## 6.6 通知测试接口 + +### 接口 + +`POST /disk-monitor/notify/test` + +### 用途 + +用于页面保存配置后,快速验证通知目标是否可用。 + +### 请求结构 + +```json +{ + "driveLetter": "C:" +} +``` + +该接口非必须,但建议保留,便于联调。 + +## 7. 执行流设计 + +## 7.1 应用启动监控 + +执行条件: + +- `disk_monitor_policy.monitor_enabled = 1` +- `disk_monitor_policy.run_on_app_start = 1` + +执行流程: + +1. 应用启动完成 +2. 后端读取当前全局策略 +3. 创建一条 `disk_monitor_job` +4. `job_source = APP_START` +5. 扫描所有启用监控的盘符 +6. 为每个盘符生成一条 `disk_monitor_result` +7. 若本次需通知,则生成 `disk_monitor_notify_log` +8. 更新 `disk_monitor_target.last_status / last_scan_time / last_used_percent` + +## 7.2 每日定时监控 + +执行条件: + +- `disk_monitor_policy.monitor_enabled = 1` +- 已配置 `daily_run_time` + +执行流程: + +1. 后端启动时注册统一调度器 +2. 每天到 `daily_run_time` 执行一次 +3. 创建一条 `disk_monitor_job` +4. `job_source = DAILY_SCHEDULE` +5. 扫描所有启用监控的盘符 +6. 生成结果与通知日志 + +## 7.3 手动执行监控 + +执行流程: + +1. 页面点击“立即执行监控” +2. 调用 `POST /disk-monitor/job/run` +3. 创建一条 `disk_monitor_job` +4. `job_source = MANUAL` +5. 扫描所有启用盘符 +6. 返回任务编号 + +## 8. 状态判定与通知规则 + +## 8.1 盘符状态判定 + +设: + +- `usedPercent` 为当前使用率 +- `warningUsagePercent` 为预警阈值 +- `alarmUsagePercent` 为告警阈值 + +判定规则: + +- `usedPercent >= alarmUsagePercent` -> `ALARM` +- `usedPercent >= warningUsagePercent 且 usedPercent < alarmUsagePercent` -> `WARNING` +- 其他 -> `NORMAL` + +## 8.2 通知判定规则 + +本次需求固定为: + +- 告警后需要每次都通知 +- 其余状态按状态变化通知 + +执行逻辑: + +```text +if currentStatus == ALARM: + shouldNotify = true + notifyReason = ALARM_EVERY_TIME + +else if currentStatus == WARNING and previousStatus != WARNING: + shouldNotify = true + notifyReason = STATUS_CHANGED + +else if currentStatus == NORMAL and previousStatus in (WARNING, ALARM): + shouldNotify = true + notifyReason = STATUS_CHANGED + +else: + shouldNotify = false + notifyReason = NO_NOTIFY +``` + +## 8.3 恢复通知 + +当盘符由: + +- `WARNING -> NORMAL` +- `ALARM -> NORMAL` + +时,视为状态变化,发送恢复通知,通知级别建议记为 `RECOVER`。 + +## 9. 通知执行设计 + +## 9.1 路径通知 + +执行方式: + +- 遍历 `notify_path_list_json` +- 对每个启用路径生成一份通知文件 + +建议文件内容为 JSON 或文本,至少包含: + +- 任务编号 +- 盘符 +- 当前状态 +- 当前使用率 +- 预警阈值 +- 告警阈值 +- 扫描时间 + +每次路径写入都要记录到 `disk_monitor_notify_log`。 + +## 9.2 HTTP 回调通知 + +执行方式: + +- 遍历 `notify_http_list_json` +- 按配置的 `url / method / timeoutMs` 发请求 + +建议请求体: + +```json +{ + "jobNo": "DM202604220001", + "driveLetter": "C:", + "currentStatus": "ALARM", + "usedPercent": 93.12, + "warningUsagePercent": 80, + "alarmUsagePercent": 90, + "scanTime": "2026-04-22 15:10:00" +} +``` + +HTTP 执行结果需要记录: + +- 发送成功或失败 +- 状态码 +- 异常信息 + +并统一写入 `disk_monitor_notify_log`。 + +## 10. 前端改动范围建议 + +若后续进入本仓库实现,预计改动集中在: + +- `frontend/src/views/systemMonitor/diskMonitor/index.vue` +- `frontend/src/views/systemMonitor/diskMonitor/components/` +- `frontend/src/api/systemMonitor/` +- 必要的前端类型定义文件 + +页面入口继续复用: + +- `frontend/src/views/systemMonitor/index.vue` + +## 11. 验证方案 + +## 11.1 配置验证 + +- 页面保存后重新进入 +- 全局策略正确回显 +- 盘符配置正确回显 +- 通知路径与 HTTP 回调正确回显 + +## 11.2 启动验证 + +- 应用启动后自动生成一条 `APP_START` 任务记录 +- 启用盘符均生成结果记录 + +## 11.3 定时验证 + +- 到设定时间后自动生成一条 `DAILY_SCHEDULE` 任务记录 +- 所有启用盘符都参与扫描 + +## 11.4 通知规则验证 + +- `NORMAL -> WARNING` 时通知一次 +- 连续 `WARNING -> WARNING` 不重复通知 +- 连续 `ALARM -> ALARM` 每次都通知 +- `WARNING -> NORMAL` 时发恢复通知 +- `ALARM -> NORMAL` 时发恢复通知 + +## 11.5 通知通道验证 + +- 路径通知能成功写入本地目录 +- 路径通知能成功写入网络共享目录 +- HTTP 回调能成功返回状态码 +- 失败情况能正确写入通知日志 + +## 12. 风险与注意事项 + +- 当前仓库中未提供业务后端源码,设计中的扫描、调度、通知逻辑需由后端配合实现 +- 通知目标合并进 `disk_monitor_target` 后,当前需求下表结构更简洁,但后续若要做通知目标复用统计,不如独立通知表灵活 +- 路径通知涉及网络共享目录时,需要额外关注运行账户权限 +- HTTP 回调需要处理超时、重复通知、目标不可达等异常情况 +- 若后续允许删除盘符配置,建议保留结果表中的 `drive_letter` 冗余字段,避免历史查询受影响 + +## 13. 结论 + +本设计采用“全局策略 + 盘符配置 + 任务批次 + 扫描结果 + 通知日志”的五表方案,在满足当前需求的前提下,保证了: + +- 多盘符独立配置 +- 启动监控 +- 每日统一定时监控 +- 路径与 HTTP 双通知通道 +- 告警每次通知、其他状态变化通知 +- 配置、结果、通知日志完整留痕 + +该设计可直接作为后续接口开发、数据库建表和前端页面实现的依据。 diff --git a/frontend/src/views/systemMonitor/diskMonitor/components/DiskMonitorJobDetailDrawer.vue b/frontend/src/views/systemMonitor/diskMonitor/components/DiskMonitorJobDetailDrawer.vue index b5c1c27..f555723 100644 --- a/frontend/src/views/systemMonitor/diskMonitor/components/DiskMonitorJobDetailDrawer.vue +++ b/frontend/src/views/systemMonitor/diskMonitor/components/DiskMonitorJobDetailDrawer.vue @@ -13,9 +13,21 @@ 任务编号 {{ props.detail.job.jobNo }} +
+ 任务来源 + {{ getSourceLabel(props.detail.job.jobSource) }} +
任务状态 - {{ props.detail.job.jobStatus }} + {{ getJobStatusLabel(props.detail.job.jobStatus) }} +
+
+ 预警数量 + {{ props.detail.job.warningCount }} +
+
+ 告警数量 + {{ props.detail.job.alarmCount }}
开始时间 @@ -28,38 +40,71 @@
-

results

+

盘符结果

- - - - + + - - - - - - + + + + + + + + + + + + + + + + - +
-

notifyLogs

+

通知日志

- - - - - - - - - + + + + + + + + + + + + + @@ -88,6 +133,70 @@ const emit = defineEmits<{ 'update:visible': [value: boolean] }>() +const getSourceLabel = (source: DiskMonitor.JobSource) => { + if (source === 'APP_START') return '应用启动' + if (source === 'DAILY_SCHEDULE') return '定时任务' + return '手动触发' +} + +const getJobStatusLabel = (status: DiskMonitor.JobStatus) => { + if (status === 'SUCCESS') return '成功' + if (status === 'PARTIAL_SUCCESS') return '部分成功' + if (status === 'FAILED') return '失败' + return '运行中' +} + +const getMonitorStatusType = (status: DiskMonitor.MonitorStatus) => { + if (status === 'NORMAL') return 'success' + if (status === 'WARNING') return 'warning' + if (status === 'ALARM') return 'danger' + return 'info' +} + +const getMonitorStatusLabel = (status: DiskMonitor.MonitorStatus) => { + if (status === 'NORMAL') return '正常' + if (status === 'WARNING') return '预警' + if (status === 'ALARM') return '告警' + return '未知' +} + +const getNotifyReasonLabel = (reason: DiskMonitor.ResultItem['notifyReason']) => { + if (reason === 'ALARM_EVERY_TIME') return '告警每次通知' + if (reason === 'STATUS_CHANGED') return '状态变化通知' + return '本次不通知' +} + +const getNotifyLevelType = (level: DiskMonitor.NotifyLevel) => { + if (level === 'WARNING') return 'warning' + if (level === 'ALARM') return 'danger' + return 'success' +} + +const getNotifyLevelLabel = (level: DiskMonitor.NotifyLevel) => { + if (level === 'WARNING') return '预警' + if (level === 'ALARM') return '告警' + return '恢复' +} + +const getChannelTypeLabel = (type: DiskMonitor.NotifyChannelType) => { + if (type === 'PATH') return '路径通知' + return 'HTTP 回调' +} + +const getSendStatusType = (status: DiskMonitor.NotifySendStatus) => { + if (status === 'SUCCESS') return 'success' + return 'danger' +} + +const getSendStatusLabel = (status: DiskMonitor.NotifySendStatus) => { + if (status === 'SUCCESS') return '成功' + return '失败' +} + +const formatBoolean = (value: boolean) => { + return value ? '是' : '否' +} + const formatTime = (value?: string | null) => { if (!value) return '--' return dayjs(value).format('YYYY-MM-DD HH:mm:ss') diff --git a/frontend/src/views/systemMonitor/diskMonitor/components/DiskMonitorSummary.vue b/frontend/src/views/systemMonitor/diskMonitor/components/DiskMonitorSummary.vue index 9c956f3..19347a2 100644 --- a/frontend/src/views/systemMonitor/diskMonitor/components/DiskMonitorSummary.vue +++ b/frontend/src/views/systemMonitor/diskMonitor/components/DiskMonitorSummary.vue @@ -14,20 +14,25 @@
监控盘符数量
-
{{ targets.length }}
+
{{ monitorTargetCount }}
当前告警盘符
{{ alarmCount }}
+
+
最近执行时间
+
{{ latestRunTime }}
+
最近执行状态
-
{{ latestJob?.jobStatus || '--' }}
+
{{ latestJobStatus }}
diff --git a/frontend/src/views/tools/mmsmapping/API-debug-generate-and-submit-index-selection.md b/frontend/src/views/tools/mmsmapping/API-debug-generate-and-submit-index-selection.md new file mode 100644 index 0000000..a7073dc --- /dev/null +++ b/frontend/src/views/tools/mmsmapping/API-debug-generate-and-submit-index-selection.md @@ -0,0 +1,333 @@ +# generateAndSubmitIndexSelection 标准 API 调试文档 + +## 1. 接口信息 + +- 接口名称:上传 ICD 后直接生成并提交映射 +- Controller 方法:`generateAndSubmitIndexSelection` +- 请求方式:`POST` +- 请求路径:`/api/mms-mapping/generate-and-submit-index-selection` +- Content-Type:`multipart/form-data` +- 适用场景:前端或调试人员已经能直接给出 `indexSelection`,希望一次请求完成 ICD 解析、候选分析、索引校验和正式映射生成。 + +源码参考: +- `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/service/MappingTaskService.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` + +## 2. 处理链路 + +该接口内部按以下顺序执行: +1. 读取上传的 `icdFile`。 +2. 解析 ICD,生成 `icdDocument`。 +3. 加载默认模板并分析候选索引。 +4. 读取同一次请求中的 `indexSelection`。 +5. 校验分组、标签、报告、`lnInst` 是否有效。 +6. 校验通过则生成正式映射 JSON;校验不通过则返回 `NEED_INDEX_SELECTION`。 + +因此,这个接口有两类主要调试目标: +- 调试一次成功生成映射。 +- 调试为什么需要重新选择索引。 + +## 3. 请求定义 + +### 3.1 表单字段 + +| 字段 | 类型 | 是否必填 | 说明 | +| --- | --- | --- | --- | +| `icdFile` | file | 是 | ICD 文件 | +| `request` | json part | 是 | JSON 请求体,必须作为独立 part 传入 | + +### 3.2 request JSON 字段 + +`request` 结构复用 `GenerateMappingFromIcdRequest`。 + +| 字段 | 类型 | 是否必填 | 说明 | +| --- | --- | --- | --- | +| `version` | string | 否 | 输出版本号,空值时按服务端默认逻辑处理 | +| `author` | string | 否 | 作者,空值时回退到模块默认作者 | +| `saveToDisk` | boolean | 否 | 是否落盘保存生成结果 | +| `prettyJson` | boolean | 否 | 是否返回格式化 JSON | +| `outputDir` | string | 否 | 落盘目录,仅 `saveToDisk=true` 时有意义 | +| `indexSelection` | array | 否 | 直接提交的索引绑定关系;为空时会返回 `NEED_INDEX_SELECTION` | + +### 3.3 indexSelection 字段 + +| 字段 | 类型 | 是否必填 | 说明 | +| --- | --- | --- | --- | +| `groupKey` | string | 建议必填 | 候选分组唯一键,建议直接使用候选接口返回值 | +| `groupDesc` | string | 否 | 分组中文描述,用于兼容匹配 | +| `bindings` | array | 是 | 当前分组下的绑定关系列表 | + +### 3.4 bindings 子项字段 + +| 字段 | 类型 | 是否必填 | 说明 | +| --- | --- | --- | --- | +| `reportName` | string | 是 | 报告名称 | +| `dataSetName` | string | 是 | 数据集名称 | +| `label` | string | 是 | 模板业务标签 | +| `lnInst` | string | 是 | 最终绑定的 `lnInst` 数字 | + +## 4. Postman 调试方式 + +### 4.1 请求设置 + +在 Postman 中选择: +- Method:`POST` +- Body:`form-data` + +新增两个表单项: +1. `icdFile` + - 类型:`File` + - 值:选择本地 ICD 文件 +2. `request` + - 类型:`Text` + - 值:填写 JSON 字符串 + +建议把 `request` 这个 part 的 Content-Type 显式设为 `application/json`。 + +### 4.2 request 示例 + +```json +{ + "version": "1.0", + "author": "system", + "saveToDisk": false, + "prettyJson": true, + "outputDir": "", + "indexSelection": [ + { + "groupKey": "实时数据__DSSTHARM", + "groupDesc": "实时数据", + "bindings": [ + { + "reportName": "brcbStHarm", + "dataSetName": "dsStHarm", + "label": "A相", + "lnInst": "1" + }, + { + "reportName": "brcbStHarm", + "dataSetName": "dsStHarm", + "label": "B相", + "lnInst": "2" + } + ] + } + ] +} +``` + +## 5. cURL 调试示例 + +### 5.1 成功生成示例 + +```bash +curl -X POST "http://localhost:8080/api/mms-mapping/generate-and-submit-index-selection" \ + -H "Accept: application/json" \ + -F "icdFile=@D:/data/demo.icd" \ + -F 'request={ + "version":"1.0", + "author":"system", + "saveToDisk":false, + "prettyJson":true, + "outputDir":"", + "indexSelection":[ + { + "groupKey":"实时数据__DSSTHARM", + "groupDesc":"实时数据", + "bindings":[ + { + "reportName":"brcbStHarm", + "dataSetName":"dsStHarm", + "label":"A相", + "lnInst":"1" + } + ] + } + ] + };type=application/json' +``` + +### 5.2 触发 NEED_INDEX_SELECTION 示例 + +将 `indexSelection` 置空,或故意传入非法 `label` / `lnInst`: + +```bash +curl -X POST "http://localhost:8080/api/mms-mapping/generate-and-submit-index-selection" \ + -H "Accept: application/json" \ + -F "icdFile=@D:/data/demo.icd" \ + -F 'request={ + "version":"1.0", + "author":"system", + "saveToDisk":false, + "prettyJson":true, + "outputDir":"", + "indexSelection":[] + };type=application/json' +``` + +## 6. 最小响应定义 + +该接口已经按场景裁剪返回值。 + +### 6.1 SUCCESS + +只返回: +- `status` +- `message` +- `mappingJson` +- `savedPath`,仅 `saveToDisk=true` 且保存成功时返回 +- `problems`,仅存在问题时返回 + +### 6.2 NEED_INDEX_SELECTION + +只返回: +- `status` +- `message` +- `icdDocument` +- `indexCandidates` +- `problems` + +### 6.3 FAILED + +只返回: +- `status` +- `message` +- `problems`,仅存在问题时返回 + +## 7. 响应示例 + +### 7.1 成功响应 + +```json +{ + "status": "SUCCESS", + "message": "映射生成成功", + "mappingJson": "{\"Version\":\"1.0\",\"Author\":\"system\"}" +} +``` + +### 7.2 成功并落盘响应 + +```json +{ + "status": "SUCCESS", + "message": "映射生成成功", + "mappingJson": "{\"Version\":\"1.0\",\"Author\":\"system\"}", + "savedPath": "D:/output/IED1-mapping-pretty.json" +} +``` + +### 7.3 需要重新选择索引响应 + +```json +{ + "status": "NEED_INDEX_SELECTION", + "message": "索引配置不合法,请根据候选信息完成标签与数字索引的绑定后重新提交", + "icdDocument": { + "fileName": "demo.icd", + "iedName": "IED1", + "ldInst": "LD0" + }, + "indexCandidates": [ + { + "groupKey": "实时数据__DSSTHARM", + "groupDesc": "实时数据", + "reportCount": 1, + "templateLabels": ["A相", "B相", "C相"], + "reports": [ + { + "reportName": "brcbStHarm", + "dataSetName": "dsStHarm", + "reportDesc": "实时数据", + "availableLnInstValues": ["1", "2", "3"] + } + ] + } + ], + "problems": [ + "分组【实时数据】中 label【A相】不在模板候选中" + ] +} +``` + +### 7.4 失败响应 + +```json +{ + "status": "FAILED", + "message": "ICD 解析失败:ICD 文件内容不能为空", + "problems": [ + "ICD 文件内容不能为空" + ] +} +``` + +## 8. 常见调试问题 + +### 8.1 `request` 没有按 JSON 传入 + +现象: +- Spring 无法正确绑定 `@RequestPart("request")` +- 返回 400 或参数解析异常 + +处理: +- 确保 `request` 是单独的 multipart part +- 建议显式设置 `type=application/json` + +### 8.2 `indexSelection` 为空 + +现象: +- 返回 `NEED_INDEX_SELECTION` +- `message` 提示索引配置缺失 + +处理: +- 先调用 `generateFromIcdCandidates` +- 使用其返回的 `groupKey`、`templateLabels`、`availableLnInstValues` 重新组装绑定 + +### 8.3 `groupKey` 不匹配 + +现象: +- `problems` 中出现“未找到分组配置” + +处理: +- 不要自己拼 `groupKey` +- 直接使用候选接口返回值原样回传 + +### 8.4 `label` 不合法 + +现象: +- `problems` 中出现“label 不在模板候选中” + +处理: +- `label` 必须取自当前分组的 `templateLabels` + +### 8.5 `lnInst` 不合法 + +现象: +- `problems` 中出现“lnInst 不在可选数字中” + +处理: +- `lnInst` 必须取自当前报告的 `availableLnInstValues` + +### 8.6 `saveToDisk=true` 但没有 `savedPath` + +可能原因: +- 保存过程抛异常,接口直接返回 `FAILED` +- 输出目录无权限或路径非法 + +处理: +- 优先先用 `saveToDisk=false` 验证生成链路 +- 再单独验证落盘目录权限 + +## 9. 联调建议 + +- 首次联调建议先走两步: + 1. 先调用 `generateFromIcdCandidates` + 2. 根据候选结果确认 `groupKey`、`label`、`lnInst` 后,再调当前接口 +- 如果只是验证“提交链路”是否通,不建议一开始就打开 `saveToDisk` +- 如果需要排查返回值最小化是否生效,重点看: + - `SUCCESS` 不应再返回 `icdDocument`、`indexCandidates` + - `NEED_INDEX_SELECTION` 不应返回 `mappingJson` + - `FAILED` 不应返回候选或映射结果字段 \ No newline at end of file diff --git a/frontend/src/views/tools/mmsmapping/API-getIcdMmsJson.md b/frontend/src/views/tools/mmsmapping/API-getIcdMmsJson.md new file mode 100644 index 0000000..682a0f0 --- /dev/null +++ b/frontend/src/views/tools/mmsmapping/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/frontend/src/views/tools/mmsmapping/DefaultCfg.txt b/frontend/src/views/tools/mmsmapping/DefaultCfg.txt new file mode 100644 index 0000000..ced05d1 --- /dev/null +++ b/frontend/src/views/tools/mmsmapping/DefaultCfg.txt @@ -0,0 +1,756 @@ +{ + "ReportList": [ + { + "desc": "统计数据", + "inst": "01", + "TrgOps": "96", + "Select": "DataStatFileMap", + "DataSetList": [ + "dsStatisticData", + "dsStHarm", + "dsStIHarm", + "dsStMMXU", + "dsStMSQI" + ], + "LnInstList": [ + "最大值", + "最小值", + "平均值", + "95值", + "方均根值", + "间谐波最大值", + "间谐波最小值", + "间谐波平均值", + "间谐波95值", + "间谐波方均根值" + ] + }, + { + "desc": "波动闪变", + "inst": "01", + "TrgOps": "96", + "Select": "FlickerFileMap", + "DataSetList": [ + "dsFlickerData", + "dsPST" + ], + "LnInstList": [ + "波动闪变值" + ] + }, + { + "desc": "实时数据", + "inst": "01", + "TrgOps": "40", + "Select": "DataRealFileMap", + "DataSetList": [ + "dsRealTimeData", + "dsRtHarm", + "dsRtIHarm", + "dsRtMMXU", + "dsRtMSQI", + "dsRtFre" + ], + "LnInstList": [ + "实时数据", + "间谐波实时数据" + ] + }, + { + "desc": "暂态事件", + "inst": "01", + "TrgOps": "96", + "Select": "QVVR", + "DataSetList": [ + "dsEveQVVR" + ], + "LnInstList": [ + "电压变动A", + "电压变动B", + "电压变动C" + ] + }, + { + "desc": "录波状态", + "inst": "01", + "TrgOps": "96", + "Select": "RDRE", + "DataSetList": [ + "dsEveRDRE" + ], + "LnInstList": [ + "录波文件" + ] + } + ], + "LnClassList": [ + { + "desc": "基本数据", + "nameList": [ + "MMXU" + ] + }, + { + "desc": "序分量值", + "nameList": [ + "MSQI" + ] + }, + { + "desc": "谐波/间谐波数据", + "nameList": [ + "MHAI" + ] + }, + { + "desc": "波动闪变", + "nameList": [ + "MFLK" + ] + }, + { + "desc": "电压变动", + "nameList": [ + "QVVR" + ] + } + ], + "PhaseList": [ + { + "desc": "无相别", + "nameList": [ + "null" + ] + }, + { + "desc": "正序", + "nameList": [ + "c1" + ] + }, + { + "desc": "负序", + "nameList": [ + "c2" + ] + }, + { + "desc": "零序", + "nameList": [ + "c3" + ] + }, + { + "desc": "A相", + "nameList": [ + "phsA", + "phsAHar" + ] + }, + { + "desc": "B相", + "nameList": [ + "phsB", + "phsBHar" + ] + }, + { + "desc": "C相", + "nameList": [ + "phsC", + "phsCHar" + ] + }, + { + "desc": "AB线", + "nameList": [ + "phsAB", + "phsABHar" + ] + }, + { + "desc": "BC线", + "nameList": [ + "phsBC", + "phsBCHar" + ] + }, + { + "desc": "CA线", + "nameList": [ + "phsCA", + "phsCAHar" + ] + } + ], + "MultiplierList": [ + { + "multiplier": 1, + "nameList": [ + "null" + ] + }, + { + "multiplier": 1000, + "nameList": [ + "k" + ] + } + ], + "UnitList": [ + { + "desc": "other", + "nameList": [ + "null" + ] + }, + { + "desc": "v", + "nameList": [ + "V" + ] + }, + { + "desc": "a", + "nameList": [ + "A" + ] + }, + { + "desc": "w", + "nameList": [ + "W", + "VAr", + "VA" + ] + } + ], + "TypeList": [ + { + "desc": "值", + "nameList": [ + "mag" + ] + }, + { + "desc": "角度", + "nameList": [ + "ang" + ] + } + ], + "DataObjectsList": [ + { + "desc": "非间谐波数据", + "LnInstList": [ + "最大值", + "最小值", + "平均值", + "95值", + "实时数据" + ], + "ObjectList": [ + { + "desc": "频率", + "nameList": [ + "Hz" + ] + }, + { + "desc": "线电压总有效值", + "nameList": [ + "PPV" + ] + }, + { + "desc": "相电压总有效值", + "nameList": [ + "PhV" + ] + }, + { + "desc": "电流总有效值", + "nameList": [ + "A" + ] + }, + { + "desc": "有功功率", + "nameList": [ + "W" + ] + }, + { + "desc": "无功功率", + "nameList": [ + "VAr" + ] + }, + { + "desc": "视在功率", + "nameList": [ + "VA" + ] + }, + { + "desc": "功率因数", + "nameList": [ + "PF" + ] + }, + { + "desc": "位移功率因数", + "nameList": [ + "DF" + ] + }, + { + "desc": "三相总有功功率", + "nameList": [ + "TotW" + ] + }, + { + "desc": "三相总无功功率", + "nameList": [ + "TotVAr" + ] + }, + { + "desc": "三相总视在功率", + "nameList": [ + "TotVA" + ] + }, + { + "desc": "三相功率因数", + "nameList": [ + "TotPF" + ] + }, + { + "desc": "三相位移功率因数", + "nameList": [ + "TotDF" + ] + }, + { + "desc": "频率偏差", + "nameList": [ + "HzDev" + ] + }, + { + "desc": "相电压偏差", + "nameList": [ + "PhVDev" + ] + }, + { + "desc": "线电压偏差", + "nameList": [ + "PPVDev" + ] + }, + { + "desc": "正序负序和零序电压", + "nameList": [ + "SeqV" + ] + }, + { + "desc": "正序负序和零序电流", + "nameList": [ + "SeqA" + ] + }, + { + "desc": "电压负序不平衡度", + "nameList": [ + "ImbNgV" + ] + }, + { + "desc": "电流负序不平衡度", + "nameList": [ + "ImbNgA" + ] + }, + { + "desc": "电压零序不平衡度", + "nameList": [ + "ImbZroV" + ] + }, + { + "desc": "电流零序不平衡度", + "nameList": [ + "ImbZroA" + ] + }, + { + "desc": "相电压谐波总畸变率", + "nameList": [ + "ThdPhV" + ] + }, + { + "desc": "相电压总偶次谐波畸变率", + "nameList": [ + "ThdEvnPhV" + ] + }, + { + "desc": "相电压总奇次谐波畸变率", + "nameList": [ + "ThdOddPhV" + ] + }, + { + "desc": "线电压谐波总畸变率", + "nameList": [ + "ThdPPV" + ] + }, + { + "desc": "线电压总偶次谐波畸变率", + "nameList": [ + "ThdEvnPPV" + ] + }, + { + "desc": "线电压总奇次谐波畸变率", + "nameList": [ + "ThdOddPPV" + ] + }, + { + "desc": "相电压谐波含有率序列", + "baseflag": 1, + "basecount": 49, + "nameList": [ + "HRPhV", + "HPhVMag" + ] + }, + { + "desc": "线电压谐波含有率序列", + "baseflag": 1, + "basecount": 49, + "nameList": [ + "HRPPV" + ] + }, + { + "desc": "电流总谐波畸变率", + "nameList": [ + "ThdA" + ] + }, + { + "desc": "电流总偶次谐波畸变率", + "nameList": [ + "ThdEvnA" + ] + }, + { + "desc": "电流总奇次谐波畸变率", + "nameList": [ + "ThdOddA" + ] + }, + { + "desc": "谐波电流有效值序列", + "baseflag": 1, + "basecount": 49, + "nameList": [ + "HA", + "HAMag" + ] + }, + { + "desc": "谐波电压有效值序列", + "baseflag": 1, + "basecount": 49, + "nameList": [ + "HPhV" + ] + }, + { + "desc": "2~50次谐波有功功率序列", + "baseflag": 1, + "basecount": 49, + "nameList": [ + "HW" + ] + }, + { + "desc": "2~50次谐波无功功率序列", + "baseflag": 1, + "basecount": 49, + "nameList": [ + "HVAr" + ] + }, + { + "desc": "2~50次谐波视在功率序列", + "baseflag": 1, + "basecount": 49, + "nameList": [ + "HVA" + ] + }, + { + "desc": "三相总谐波视在功率", + "nameList": [ + "TotHVA" + ] + }, + { + "desc": "三相总谐波无功功率", + "nameList": [ + "TotHVAr" + ] + }, + { + "desc": "三相总谐波有功功率", + "nameList": [ + "TotHW" + ] + }, + { + "desc": "相电压基波有效值", + "baseflag": 2, + "queuecount": 49, + "nameList": [ + "HFundPhV", + "FundPhV" + ], + "queueList":[ + "HPhV" + ] + }, + { + "desc": "线电压基波有效值", + "baseflag": 2, + "queuecount": 49, + "nameList": [ + "HFundPPV" + ], + "queueList":[ + "HPPV" + ] + }, + { + "desc": "电流基波有效值", + "baseflag": 2, + "queuecount": 49, + "nameList": [ + + ], + "queueList":[ + "HA" + ] + }, + { + "desc": "基波有功功率", + "baseflag": 2, + "queuecount": 49, + "nameList": [ + + ], + "queueList":[ + "HW" + ] + }, + { + "desc": "基波无功功率", + "baseflag": 2, + "queuecount": 49, + "nameList": [ + + ], + "queueList":[ + "HVAr" + ] + }, + { + "desc": "基波视在功率", + "baseflag": 2, + "queuecount": 49, + "nameList": [ + + ], + "queueList":[ + "HVA" + ] + } + ] + }, + { + "desc": "间谐波数据", + "LnInstList": [ + "间谐波最大值", + "间谐波最小值", + "间谐波平均值", + "间谐波95值", + "间谐波实时数据" + ], + "ObjectList": [ + { + "desc": "相电压间谐波含有率序列", + "baseflag": 1, + "basecount": 50, + "nameList": [ + "HPhV" + ] + }, + { + "desc": "线电压间谐波含有率序列", + "baseflag": 1, + "basecount": 50, + "nameList": [ + "HRPPV" + ] + }, + { + "desc": "间谐波电流有效值序列", + "baseflag": 1, + "basecount": 50, + "nameList": [ + "HA" + ] + }, + { + "desc": "间谐波电压有效值序列", + "baseflag": 1, + "basecount": 50, + "nameList": [ + "HRPhV" + ] + } + ] + }, + { + "desc": "电压变动", + "LnInstList": [ + "电压变动A", + "电压变动B", + "电压变动C" + ], + "ObjectList": [ + { + "desc": "电压扰动事件启动", + "nameList": [ + "VarStr" + ] + }, + { + "desc": "电压暂降事件启动", + "nameList": [ + "DipStr" + ] + }, + { + "desc": "电压暂升事件启动", + "nameList": [ + "SwlStr" + ] + }, + { + "desc": "电压中断事件启动", + "nameList": [ + "IntrStr" + ] + }, + { + "desc": "电压扰动事件特征幅值", + "nameList": [ + "VVa" + ] + }, + { + "desc": "电压扰动事件持续时间", + "nameList": [ + "VVaTm" + ] + }, + { + "desc": "电压暂降启动定值", + "nameList": [ + "DipStrVal" + ] + }, + { + "desc": "电压暂升启动定值", + "nameList": [ + "SwlStrVal" + ] + }, + { + "desc": "电压中断启动定值", + "nameList": [ + "IntrStrVal" + ] + } + ] + }, + { + "desc": "其余数据", + "LnInstList": [ + "波动闪变值", + "录波文件" + ], + "ObjectList": [ + { + "desc": "线电压短时闪变值", + "nameList": [ + "PPPst" + ] + }, + { + "desc": "相电压短时闪变值", + "nameList": [ + "PhPst" + ] + }, + { + "desc": "线电压长时闪变值", + "nameList": [ + "PPPlt" + ] + }, + { + "desc": "相电压长时闪变值", + "nameList": [ + "PhPlt" + ] + }, + { + "desc": "线电压电压变动幅值", + "nameList": [ + "PPFluc" + ] + }, + { + "desc": "相电压电压变动幅值", + "nameList": [ + "PhFluc" + ] + }, + { + "desc": "线电压电压变动频度", + "nameList": [ + "PPFlucf" + ] + }, + { + "desc": "相电压电压变动频度", + "nameList": [ + "PhFlucf" + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/frontend/src/views/tools/mmsmapping/components/IcdDocumentTree.vue b/frontend/src/views/tools/mmsmapping/components/IcdDocumentTree.vue new file mode 100644 index 0000000..162c0b0 --- /dev/null +++ b/frontend/src/views/tools/mmsmapping/components/IcdDocumentTree.vue @@ -0,0 +1,160 @@ + + + + + diff --git a/frontend/src/views/tools/mmsmapping/components/MappingConfigPanel.vue b/frontend/src/views/tools/mmsmapping/components/MappingConfigPanel.vue new file mode 100644 index 0000000..e640e12 --- /dev/null +++ b/frontend/src/views/tools/mmsmapping/components/MappingConfigPanel.vue @@ -0,0 +1,160 @@ + + + + + diff --git a/frontend/src/views/tools/mmsmapping/components/MappingRequestPanel.vue b/frontend/src/views/tools/mmsmapping/components/MappingRequestPanel.vue new file mode 100644 index 0000000..7130519 --- /dev/null +++ b/frontend/src/views/tools/mmsmapping/components/MappingRequestPanel.vue @@ -0,0 +1,173 @@ + + + + + diff --git a/frontend/src/views/tools/mmsmapping/components/MappingResultPanel.vue b/frontend/src/views/tools/mmsmapping/components/MappingResultPanel.vue new file mode 100644 index 0000000..b4a27fb --- /dev/null +++ b/frontend/src/views/tools/mmsmapping/components/MappingResultPanel.vue @@ -0,0 +1,282 @@ + + + + + diff --git a/frontend/src/views/tools/mmsmapping/utils/indexSelection.ts b/frontend/src/views/tools/mmsmapping/utils/indexSelection.ts new file mode 100644 index 0000000..dc58adf --- /dev/null +++ b/frontend/src/views/tools/mmsmapping/utils/indexSelection.ts @@ -0,0 +1,101 @@ +import type { MmsMapping } from '@/api/tools/mmsmapping/interface' + +const isRecord = (value: unknown): value is Record => + typeof value === 'object' && value !== null && !Array.isArray(value) + +const normalizeRequiredString = (value: unknown, fieldPath: string) => { + if (typeof value !== 'string' || !value.trim()) { + throw new Error(`${fieldPath} 必须是非空字符串`) + } + + return value.trim() +} + +const normalizeOptionalString = (value: unknown) => (typeof value === 'string' ? value.trim() : '') + +const normalizeBindings = (value: unknown, groupIndex: number): MmsMapping.IndexSelectionBinding[] => { + if (!Array.isArray(value) || !value.length) { + throw new Error(`request.indexSelection[${groupIndex}].bindings 必须是非空数组`) + } + + return value.map((binding, bindingIndex) => { + if (!isRecord(binding)) { + throw new Error(`request.indexSelection[${groupIndex}].bindings[${bindingIndex}] 必须是对象`) + } + + return { + reportName: normalizeRequiredString( + binding.reportName, + `request.indexSelection[${groupIndex}].bindings[${bindingIndex}].reportName` + ), + dataSetName: normalizeRequiredString( + binding.dataSetName, + `request.indexSelection[${groupIndex}].bindings[${bindingIndex}].dataSetName` + ), + label: normalizeRequiredString( + binding.label, + `request.indexSelection[${groupIndex}].bindings[${bindingIndex}].label` + ), + lnInst: normalizeRequiredString( + binding.lnInst, + `request.indexSelection[${groupIndex}].bindings[${bindingIndex}].lnInst` + ) + } + }) +} + +export const buildDefaultIndexSelection = ( + candidateGroups: MmsMapping.IndexCandidateGroup[] +): MmsMapping.IndexSelectionGroup[] => + candidateGroups + .filter(candidate => candidate.groupKey?.trim()) + .map(candidate => { + const defaultReport = (candidate.reports || []).find( + report => report.reportName?.trim() && report.dataSetName?.trim() + ) + const defaultLnInst = (defaultReport?.availableLnInstValues || []).find(item => item?.trim())?.trim() || '' + + return { + groupKey: candidate.groupKey!.trim(), + groupDesc: candidate.groupDesc?.trim() || '', + bindings: (candidate.templateLabels || []) + .map(label => label?.trim() || '') + .filter(Boolean) + .map(label => ({ + reportName: defaultReport?.reportName?.trim() || '', + dataSetName: defaultReport?.dataSetName?.trim() || '', + label, + lnInst: defaultLnInst + })) + } + }) + +export const formatIndexSelectionJson = (value: MmsMapping.IndexSelectionGroup[]) => JSON.stringify(value, null, 4) + +export const parseIndexSelectionJson = (source: string): MmsMapping.IndexSelectionGroup[] => { + let parsed: unknown + + try { + parsed = JSON.parse(source) + } catch { + throw new Error('request.indexSelection 不是合法 JSON') + } + + if (!Array.isArray(parsed)) { + throw new Error('request.indexSelection 必须是数组') + } + + return parsed.map((group, groupIndex) => { + if (!isRecord(group)) { + throw new Error(`request.indexSelection[${groupIndex}] 必须是对象`) + } + + const groupDesc = normalizeOptionalString(group.groupDesc) + + return { + groupKey: normalizeRequiredString(group.groupKey, `request.indexSelection[${groupIndex}].groupKey`), + groupDesc, + bindings: normalizeBindings(group.bindings, groupIndex) + } + }) +} diff --git a/frontend/src/views/tools/mmsmapping/utils/requestPayload.ts b/frontend/src/views/tools/mmsmapping/utils/requestPayload.ts new file mode 100644 index 0000000..be3e452 --- /dev/null +++ b/frontend/src/views/tools/mmsmapping/utils/requestPayload.ts @@ -0,0 +1,15 @@ +import type { MmsMapping } from '@/api/tools/mmsmapping/interface' + +export const DEFAULT_REQUEST_OPTIONS = { + saveToDisk: false, + prettyJson: true, + outputDir: '' +} satisfies Pick + +export const createBaseRequestPayload = ( + form: MmsMapping.BaseRequestForm +): Omit => ({ + version: form.version.trim() || '1.0', + author: form.author.trim() || 'system', + ...DEFAULT_REQUEST_OPTIONS +}) diff --git a/frontend/src/views/tools/waveform/PARSE_COMTRADE_API.md b/frontend/src/views/tools/waveform/PARSE_COMTRADE_API.md deleted file mode 100644 index 920b321..0000000 --- a/frontend/src/views/tools/waveform/PARSE_COMTRADE_API.md +++ /dev/null @@ -1,266 +0,0 @@ -# parseComtrade API 文档 - -## 1. 接口概述 - -- 接口名称:解析 COMTRADE 波形文件 -- Controller:[WaveController.java](D:/Work/SourceCode/CN_Tool/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/controller/WaveController.java) -- 方法:`parseComtrade` -- 请求路径:`POST /wave/parseComtrade` -- Content-Type:`multipart/form-data` -- 返回类型:`HttpResult` - -用途说明: - -- 上传一组 COMTRADE `cfg/dat` 文件 -- 解析原始波形数据 -- 按请求决定是否补充 RMS 数据、前端查看明细和特征值结果 - -## 2. 请求参数 - -### 2.1 文件参数 - -| 参数名 | 类型 | 必填 | 说明 | -| --- | --- | --- | --- | -| `cfgFile` | file | 是 | COMTRADE 配置文件 `.cfg` | -| `datFile` | file | 是 | COMTRADE 数据文件 `.dat` | - -### 2.2 表单参数 - -参数定义来源:[WaveComtradeParseParam.java](D:/Work/SourceCode/CN_Tool/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/pojo/param/WaveComtradeParseParam.java) - -| 参数名 | 类型 | 必填 | 默认值 | 说明 | -| --- | --- | --- | --- | --- | -| `parseType` | integer | 否 | `1` | 解析类型:`0` 高级算法采样率 32-128,`1` 普通展示,`2` App 抽点,`3` 原始波形 | -| `ptType` | integer | 否 | `0` | PT 接线方式:`0` 星形,`1` 三角,`2` 开口三角 | -| `pt` | number | 否 | `1` | PT 变比 | -| `ct` | number | 否 | `1` | CT 变比 | -| `monitorName` | string | 否 | `未命名测点` | 测点名称 | -| `calculateRms` | boolean | 否 | `true` | 是否计算 RMS | -| `buildDetails` | boolean | 否 | `true` | 是否构建前端查看明细 | -| `calculateEigenvalue` | boolean | 否 | `false` | 是否计算特征值 | -| `dynamicThreshold` | boolean | 否 | `true` | 特征值是否使用浮动门槛 | - -## 3. 请求示例 - -```bash -curl -X POST "http://localhost:8080/wave/parseComtrade" \ - -F "cfgFile=@D:/00-B7-8D-00-E4-09/1_20260321_201458_748.CFG" \ - -F "datFile=@D:/00-B7-8D-00-E4-09/1_20260321_201458_748.DAT" \ - -F "parseType=1" \ - -F "ptType=0" \ - -F "pt=1" \ - -F "ct=1" \ - -F "monitorName=监测点1" \ - -F "calculateRms=true" \ - -F "buildDetails=true" \ - -F "calculateEigenvalue=true" \ - -F "dynamicThreshold=true" -``` - -## 4. 响应结构 - -### 4.1 外层响应 - -Controller 返回的是 `HttpResult`。当前仓库内未展开 `HttpResult` 类型源码,本接口文档只对业务 `data` 部分做精确定义。 - -业务数据类型来源:[WaveComtradeResultVO.java](D:/Work/SourceCode/CN_Tool/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/pojo/vo/WaveComtradeResultVO.java) - -### 4.2 data 字段定义 - -| 字段名 | 类型 | 说明 | -| --- | --- | --- | -| `waveData` | object | 波形基础数据 | -| `waveDataDetails` | array | 前端查看明细,`buildDetails=true` 时返回 | -| `eigenvalues` | array | 特征值结果,`calculateEigenvalue=true` 时返回 | - -## 5. 业务对象说明 - -### 5.1 waveData - -定义来源:[WaveDataDTO.java](D:/Work/SourceCode/CN_Tool/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/pojo/dto/WaveDataDTO.java) - -| 字段名 | 类型 | 说明 | -| --- | --- | --- | -| `comtradeCfgDTO` | object | CFG 解析结果 | -| `waveTitle` | array | 波形标题,例如 `["Time","UA相","UB相"]` | -| `channelNames` | array | 通道名称列表 | -| `listWaveData` | array> | 原始波形数据,首列为时间,后续列为相电压/电流值 | -| `listRmsData` | array> | RMS 波形数据,`calculateRms=true` 时可用 | -| `listRmsMinData` | array> | RMS 最小值摘要 | -| `iPhasic` | integer | 相别数量 | -| `ptType` | integer | PT 接线方式 | -| `pt` | number | PT 变比 | -| `ct` | number | CT 变比 | -| `time` | string | 事件发生时刻 | -| `monitorName` | string | 测点名称 | - -### 5.2 comtradeCfgDTO - -定义来源:[ComtradeCfgDTO.java](D:/Work/SourceCode/CN_Tool/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/pojo/dto/ComtradeCfgDTO.java) - -| 字段名 | 类型 | 说明 | -| --- | --- | --- | -| `nChannelNum` | integer | 通道总数 | -| `nPhasic` | integer | 相别数量 | -| `nAnalogNum` | integer | 模拟量通道数 | -| `nDigitalNum` | integer | 开关量通道数 | -| `timeStart` | string/date | 录波开始时间 | -| `timeTrige` | string/date | 触发时间 | -| `lstAnalogDTO` | array | 模拟量通道配置 | -| `lstDigitalDTO` | array | 开关量通道配置 | -| `nRates` | integer | 采样率分段数 | -| `lstRate` | array | 采样率分段配置 | -| `firstTime` | string/date | 首个触发时间对象 | -| `firstMs` | integer | 首个触发毫秒值 | -| `nPush` | integer | 触发前推点数 | -| `finalSampleRate` | integer | 最终采样率 | -| `nAllWaveNum` | integer | 总周波数 | -| `strBinType` | string | 文件编码类型,例如 `BINARY` | - -### 5.3 waveDataDetails - -定义来源:[WaveDataDetail.java](D:/Work/SourceCode/CN_Tool/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/pojo/bo/WaveDataDetail.java) - -| 字段名 | 类型 | 说明 | -| --- | --- | --- | -| `instantData` | object | 瞬时波形数据 | -| `rmsData` | object | RMS 波形数据 | -| `a` | string | A 相名称 | -| `b` | string | B 相名称 | -| `c` | string | C 相名称 | -| `channelName` | string | 通道名称 | -| `unit` | string | 单位 | -| `isOpen` | boolean | 是否开口三角模式 | -| `title` | string | 当前图标题 | -| `colors` | array | 曲线颜色 | - -其中 `instantData` 和 `rmsData` 结构一致,定义分别来自: - -- [InstantData.java](D:/Work/SourceCode/CN_Tool/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/pojo/bo/InstantData.java) -- [RmsData.java](D:/Work/SourceCode/CN_Tool/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/pojo/bo/RmsData.java) - -公共字段: - -| 字段名 | 类型 | 说明 | -| --- | --- | --- | -| `max` | number | 当前曲线最大值 | -| `min` | number | 当前曲线最小值 | -| `aValue` | array> | A 相点位 | -| `bValue` | array> | B 相点位 | -| `cValue` | array> | C 相点位 | - -### 5.4 eigenvalues - -定义来源:[EigenvalueDTO.java](D:/Work/SourceCode/CN_Tool/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/pojo/dto/EigenvalueDTO.java) - -| 字段名 | 类型 | 说明 | -| --- | --- | --- | -| `amplitude` | number | 特征幅值百分比 | -| `residualVoltage` | number | 残余电压 | -| `ratedVoltage` | number | 额定电压 | -| `durationTime` | number | 持续时间 | - -## 6. 成功响应示例 - -以下示例基于真实样本文件联测结果整理,长数组做了截断展示。 - -```json -{ - "code": "SUCCESS", - "message": "成功", - "data": { - "waveData": { - "comtradeCfgDTO": { - "nChannelNum": 6, - "nPhasic": 3, - "nAnalogNum": 6, - "nDigitalNum": 0, - "timeStart": "2026-03-21 20:14:58.648", - "timeTrige": "2026-03-21 20:14:58.748", - "nRates": 1, - "firstMs": 748, - "nPush": 100, - "finalSampleRate": 512, - "nAllWaveNum": 30, - "strBinType": "BINARY" - }, - "waveTitle": ["Time", "UA相", "UB相", "UC相", "IA相", "IB相", "IC相"], - "channelNames": ["/", "U1", "U2", "U3", "I1", "I2", "I3"], - "listWaveData": { - "count": 15616, - "first": [-100.0, -146.56, -76.9, -76.9, -0.13, 0.01, -0.2], - "last": [509.96, 148.02, 69.73, 69.75, 0.16, 0.01, 0.15] - }, - "listRmsData": { - "count": 15616, - "first": [-100.0, 104.94, 104.22, 104.23, 0.27, 0.01, 0.28], - "last": [509.96, 105.6, 105.1, 105.12, 0.24, 0.01, 0.24] - }, - "listRmsMinData": [ - [40.74, 41.2], - [362.19, 0.01] - ], - "iPhasic": 3, - "ptType": 0, - "pt": 1.0, - "ct": 1.0, - "time": "2026-03-21 20:14:58.748", - "monitorName": "监测点1" - }, - "waveDataDetails": [ - { - "channelName": "U1", - "unit": "kV", - "a": "A相", - "b": "B相", - "c": "C相", - "isOpen": false - }, - { - "channelName": "I1", - "unit": "A", - "a": "A相", - "b": "B相", - "c": "C相", - "isOpen": false - } - ], - "eigenvalues": [ - { - "amplitude": 0.3926178, - "residualVoltage": 41.200005, - "ratedVoltage": 104.936676, - "durationTime": 48.632812 - }, - { - "amplitude": 0.4067544, - "residualVoltage": 42.390152, - "ratedVoltage": 104.21559, - "durationTime": 54.492188 - }, - { - "amplitude": 0.40674016, - "residualVoltage": 42.396355, - "ratedVoltage": 104.2345, - "durationTime": 54.492188 - } - ] - } -} -``` - -## 7. 失败场景 - -基于当前代码,常见失败场景包括: - -| 场景 | 说明 | -| --- | --- | -| `cfgFile` 或 `datFile` 未上传 | 返回业务异常,提示“cfg 或 dat 文件不能为空” | -| CFG 文件格式错误 | 返回 CFG 解析失败 | -| DAT 文件为空或格式错误 | 返回 DAT 解析失败 | -| COMTRADE 解析过程中出现异常 | 返回“COMTRADE 波形解析失败” | - -## 8. 备注 - -- 当前接口已经移除图片生成相关参数,不再支持 `generateInstantImage`、`generateRmsImage` 等旧字段。 -- 当前接口文档只覆盖 `parseComtrade`,其他波形文本解析接口请单独编写。 diff --git a/frontend/src/views/tools/waveform/components/WaveformInfoPanel.vue b/frontend/src/views/tools/waveform/components/WaveformInfoPanel.vue index 6906b6f..d6cba51 100644 --- a/frontend/src/views/tools/waveform/components/WaveformInfoPanel.vue +++ b/frontend/src/views/tools/waveform/components/WaveformInfoPanel.vue @@ -14,6 +14,7 @@
{{ item.value }}
+ @@ -23,27 +24,16 @@ :last-vector-parse-error-message="lastVectorParseErrorMessage" :active-vector-channel-name="activeVectorChannelName" /> + - -
特征值
-
-
-
{{ item.title }}
-
- {{ row.label }} - {{ row.value }} -
-
-
-
当前文件未返回特征值结果。
暂无解析信息
-
接口联调完成后,右侧会展示波形信息、向量信息和特征值。
+
接口联调完成后,右侧会展示波形信息和向量信息。
最近一次解析失败:{{ lastParseErrorMessage }}
@@ -53,13 +43,12 @@ + diff --git a/frontend/src/views/tools/waveform/index.vue b/frontend/src/views/tools/waveform/index.vue index 6558e4d..b9c7d6a 100644 --- a/frontend/src/views/tools/waveform/index.vue +++ b/frontend/src/views/tools/waveform/index.vue @@ -33,7 +33,6 @@ { }) const activeWaveData = computed(() => waveformParseResult.value?.waveData) -const eigenvalueList = computed(() => waveformParseResult.value?.eigenvalues || []) const normalizeRatio = (value?: number) => { const ratio = Number(value) @@ -251,18 +248,6 @@ const formatNumber = (value: unknown, fractionDigits = 3) => { return `${Number(numberValue.toFixed(fractionDigits))}` } -const formatPercentage = (value?: number) => { - const numberValue = Number(value) - if (!Number.isFinite(numberValue)) return '--' - return `${Number((numberValue * 100).toFixed(2))}%` -} - -const formatDuration = (value?: number) => { - const numberValue = Number(value) - if (!Number.isFinite(numberValue)) return '--' - return `${Number(numberValue.toFixed(3))} ms` -} - const formatWaveformTime = (value?: string) => { if (!value) return '--' @@ -546,18 +531,6 @@ const summaryItems = computed(() => { ] }) -const featureCards = computed(() => { - return eigenvalueList.value.map((item, index) => ({ - title: `特征值 ${index + 1}`, - rows: [ - { label: '幅值占比', value: formatPercentage(item.amplitude) }, - { label: '残余电压', value: formatNumber(item.residualVoltage) }, - { label: '额定电压', value: formatNumber(item.ratedVoltage) }, - { label: '持续时间', value: formatDuration(item.durationTime) } - ] - })) -}) - const getFileBaseName = (fileName: string) => { return fileName.replace(/\.[^.]+$/, '').toLowerCase() }