From e3f37d5433a0de67ac53ee19797d560865dbc271 Mon Sep 17 00:00:00 2001
From: haoliang <821644@qq.com>
Date: Tue, 5 May 2026 17:03:38 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E9=87=87=E9=9B=86?=
=?UTF-8?q?=E5=88=86=E6=9E=90=E5=BC=95=E6=93=8E=EF=BC=88AnalysisEngine?=
=?UTF-8?q?=EF=BC=89+=20=E5=90=8E=E5=8F=B0=E7=AE=A1=E7=90=86API=20+=20?=
=?UTF-8?q?=E5=89=8D=E7=AB=AF=E8=AE=BE=E8=AE=A1=E6=96=87=E6=A1=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增 log_collect_analysis + log_collect_cycle 两张按月分区表DDL
- 完整实现 AnalysisEngine:9种分析类型检测、DB写入、异常告警联动
- 修改 CollectRecordWriter.WriteBatch 返回 rawLogId
- 集成 AnalysisEngine 到 CollectWorker 采集主流程
- 新增 CollectLogController 5个API端点(分析查询/详情/周期/原始日志)
- 新增 Entity/Enum/DTO/Repository/Service 全链路代码
- 修复子代理创建的文件:DTO命名空间、Repository方法名、SQL列映射、using引用
- 新增13-采集日志前端设计文档(索引+规范+页面)
- 全部5个主项目编译通过,0错误
---
database/sqls/03-collect-analysis-tables.sql | 93 ++++
docs/01-数据库设计.md | 116 ++++-
.../管理后台/13-采集日志/00-采集日志-索引.md | 26 ++
.../管理后台/13-采集日志/01-采集日志-规范.md | 131 ++++++
.../管理后台/13-采集日志/13-01-采集日志页面.md | 117 +++++
src/CncCollector/Core/AnalysisEngine.cs | 401 ++++++++++++++++++
src/CncCollector/Core/CollectRecordWriter.cs | 12 +-
src/CncCollector/Core/CollectWorker.cs | 12 +-
src/CncCollector/Core/CollectorEngine.cs | 20 +-
.../Dto/CollectLog/CollectAnalysisDetail.cs | 15 +
.../Dto/CollectLog/CollectAnalysisListItem.cs | 19 +
.../Dto/CollectLog/CollectAnalysisQuery.cs | 17 +
.../Dto/CollectLog/CollectCycleListItem.cs | 19 +
.../Dto/CollectLog/CollectCycleQuery.cs | 15 +
src/CncModels/Entity/CollectAnalysis.cs | 58 +++
src/CncModels/Entity/CollectCycle.cs | 49 +++
src/CncModels/Enum/AnalysisType.cs | 18 +
.../Impl/Log/CollectAnalysisRepository.cs | 191 +++++++++
.../Impl/Log/CollectCycleRepository.cs | 123 ++++++
.../Interface/ICollectAnalysisRepository.cs | 20 +
.../Interface/ICollectCycleRepository.cs | 18 +
src/CncService/Impl/CollectLogService.cs | 4 +-
.../Interface/ICollectLogService.cs | 8 +-
.../Controllers/CollectLogController.cs | 24 +-
.../Infrastructure/ServiceResolver.cs | 11 +
.../CncCollector.Tests/CollectWorkerTests.cs | 4 +-
26 files changed, 1508 insertions(+), 33 deletions(-)
create mode 100644 database/sqls/03-collect-analysis-tables.sql
create mode 100644 docs/02-功能清单/管理后台/13-采集日志/00-采集日志-索引.md
create mode 100644 docs/02-功能清单/管理后台/13-采集日志/01-采集日志-规范.md
create mode 100644 docs/02-功能清单/管理后台/13-采集日志/13-01-采集日志页面.md
create mode 100644 src/CncCollector/Core/AnalysisEngine.cs
create mode 100644 src/CncModels/Dto/CollectLog/CollectAnalysisDetail.cs
create mode 100644 src/CncModels/Dto/CollectLog/CollectAnalysisListItem.cs
create mode 100644 src/CncModels/Dto/CollectLog/CollectAnalysisQuery.cs
create mode 100644 src/CncModels/Dto/CollectLog/CollectCycleListItem.cs
create mode 100644 src/CncModels/Dto/CollectLog/CollectCycleQuery.cs
create mode 100644 src/CncModels/Entity/CollectAnalysis.cs
create mode 100644 src/CncModels/Entity/CollectCycle.cs
create mode 100644 src/CncModels/Enum/AnalysisType.cs
create mode 100644 src/CncRepository/Impl/Log/CollectAnalysisRepository.cs
create mode 100644 src/CncRepository/Impl/Log/CollectCycleRepository.cs
create mode 100644 src/CncRepository/Interface/ICollectAnalysisRepository.cs
create mode 100644 src/CncRepository/Interface/ICollectCycleRepository.cs
diff --git a/database/sqls/03-collect-analysis-tables.sql b/database/sqls/03-collect-analysis-tables.sql
new file mode 100644
index 0000000..430652e
--- /dev/null
+++ b/database/sqls/03-collect-analysis-tables.sql
@@ -0,0 +1,93 @@
+-- ============================================================
+-- 采集分析日志表 + 采集周期汇总表(幂等迁移脚本)
+-- 创建时间:2026-05-05
+-- 说明:在 cnc_log 库中新增两张按月分区表
+-- log_collect_analysis: 每次采集、每台机床的分析记录
+-- log_collect_cycle: 每次采集周期的汇总信息
+-- 执行前提:USE cnc_log; 已执行 01-init-schema.sql
+-- ============================================================
+
+USE cnc_log;
+
+-- -----------------------------------------------------------
+-- 1. 采集分析日志表 log_collect_analysis(按月分区)
+-- 记录每次采集后对每台机床的数据变化分析
+-- -----------------------------------------------------------
+DROP TABLE IF EXISTS log_collect_analysis;
+CREATE TABLE log_collect_analysis (
+ id BIGINT AUTO_INCREMENT,
+ analysis_time DATETIME NOT NULL COMMENT '分析时间(分区键)',
+ raw_log_id BIGINT NOT NULL COMMENT '关联原始日志ID(log_collect_raw.id)',
+ collect_address_id INT NOT NULL COMMENT '采集地址ID(关联cnc_collect_address)',
+ machine_id INT NOT NULL COMMENT '机床ID(关联cnc_machine)',
+ analysis_type VARCHAR(30) NOT NULL COMMENT '分析类型:NORMAL_UNCHANGED/PART_COUNT_INCREASE/PROGRAM_SWITCH/MANUAL_RESET/DEVICE_ONLINE/DEVICE_OFFLINE/NEW_DEVICE_FOUND/DATA_ANOMALY/COLLECTION_FAILED',
+ previous_program VARCHAR(200) NULL COMMENT '上一次NC程序名',
+ current_program VARCHAR(200) NULL COMMENT '本次NC程序名',
+ previous_part_count DECIMAL(15,5) NULL COMMENT '上一次零件计数',
+ current_part_count DECIMAL(15,5) NULL COMMENT '本次零件计数',
+ part_count_delta DECIMAL(15,5) NULL COMMENT '零件计数变化量(正=增加,负=减少)',
+ previous_status VARCHAR(20) NULL COMMENT '上一次设备状态',
+ current_status VARCHAR(20) NULL COMMENT '本次设备状态',
+ analysis_summary VARCHAR(500) NOT NULL COMMENT '人类可读的分析摘要',
+ analysis_detail JSON NULL COMMENT '完整的字段级对比数据(JSON)',
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (id, analysis_time),
+ INDEX idx_address_time (collect_address_id, analysis_time),
+ INDEX idx_machine_time (machine_id, analysis_time),
+ INDEX idx_type_time (analysis_type, analysis_time),
+ INDEX idx_raw_log (raw_log_id),
+ INDEX idx_program_time (current_program, analysis_time)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
+ COMMENT='采集分析日志表(按月分区,记录每次采集对每台机床的数据变化分析)'
+ PARTITION BY RANGE (TO_DAYS(analysis_time)) (
+ PARTITION p202605 VALUES LESS THAN (TO_DAYS('2026-06-01')),
+ PARTITION p202606 VALUES LESS THAN (TO_DAYS('2026-07-01')),
+ PARTITION p202607 VALUES LESS THAN (TO_DAYS('2026-08-01')),
+ PARTITION p_future VALUES LESS THAN MAXVALUE
+ );
+
+-- -----------------------------------------------------------
+-- 2. 采集周期汇总表 log_collect_cycle(按月分区)
+-- 记录每次采集周期(一个地址的一次完整采集)的汇总信息
+-- -----------------------------------------------------------
+DROP TABLE IF EXISTS log_collect_cycle;
+CREATE TABLE log_collect_cycle (
+ id BIGINT AUTO_INCREMENT,
+ cycle_time DATETIME NOT NULL COMMENT '周期开始时间(分区键)',
+ collect_address_id INT NOT NULL COMMENT '采集地址ID(关联cnc_collect_address)',
+ raw_log_id BIGINT NOT NULL COMMENT '关联原始日志ID(log_collect_raw.id)',
+ end_time DATETIME NULL COMMENT '周期结束时间',
+ duration_ms INT NULL COMMENT '本次采集总耗时(毫秒)',
+ total_machines INT NOT NULL DEFAULT 0 COMMENT '本周期采集的机床总数',
+ success_count INT NOT NULL DEFAULT 0 COMMENT '成功采集的机床数',
+ fail_count INT NOT NULL DEFAULT 0 COMMENT '失败采集的机床数',
+ change_distribution JSON NULL COMMENT '变化类型分布(如 {"PROGRAM_SWITCH":2,"PART_COUNT_INCREASE":5,"NORMAL_UNCHANGED":3})',
+ has_anomaly TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否存在异常(1=有异常:DATA_ANOMALY/COLLECTION_FAILED/DEVICE_OFFLINE)',
+ cycle_summary VARCHAR(500) NULL COMMENT '人类可读的周期汇总',
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (id, cycle_time),
+ INDEX idx_address_time (collect_address_id, cycle_time),
+ INDEX idx_time (cycle_time),
+ INDEX idx_anomaly_time (has_anomaly, cycle_time)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
+ COMMENT='采集周期汇总表(按月分区,每次采集周期的汇总信息)'
+ PARTITION BY RANGE (TO_DAYS(cycle_time)) (
+ PARTITION p202605 VALUES LESS THAN (TO_DAYS('2026-06-01')),
+ PARTITION p202606 VALUES LESS THAN (TO_DAYS('2026-07-01')),
+ PARTITION p202607 VALUES LESS THAN (TO_DAYS('2026-08-01')),
+ PARTITION p_future VALUES LESS THAN MAXVALUE
+ );
+
+-- -----------------------------------------------------------
+-- 3. 为现有 log_collect_raw 表增加补充索引
+-- 支持按采集成功/失败筛选,以及按响应时长分析
+-- -----------------------------------------------------------
+-- 检查索引是否已存在,若不存在则添加(幂等)
+SET @exist := (SELECT COUNT(*) FROM information_schema.STATISTICS
+ WHERE TABLE_SCHEMA = 'cnc_log' AND TABLE_NAME = 'log_collect_raw' AND INDEX_NAME = 'idx_success_time');
+SET @sqlstmt := IF(@exist = 0,
+ 'ALTER TABLE cnc_log.log_collect_raw ADD INDEX idx_success_time (is_success, request_time)',
+ 'SELECT ''索引 idx_success_time 已存在,跳过''');
+PREPARE stmt FROM @sqlstmt;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
diff --git a/docs/01-数据库设计.md b/docs/01-数据库设计.md
index 01fe219..d15a363 100644
--- a/docs/01-数据库设计.md
+++ b/docs/01-数据库设计.md
@@ -511,7 +511,7 @@ CREATE TABLE cnc_screen_filter (
---
-## 三、日志库 cnc_log(3张表)
+## 三、日志库 cnc_log(5张表)
### 3.1 原始采集JSON表 log_collect_raw(按月分区)
@@ -594,6 +594,99 @@ CREATE TABLE log_collector_heartbeat (
---
+### 3.4 采集分析日志表 log_collect_analysis(按月分区)
+
+记录每次采集后对每台机床的数据变化分析。每次采集周期中,每台机床产生一条分析记录,包含与上一次采集数据的对比结果。
+
+``sql
+CREATE TABLE log_collect_analysis (
+ id BIGINT AUTO_INCREMENT,
+ analysis_time DATETIME NOT NULL COMMENT '分析时间(分区键)',
+ raw_log_id BIGINT NOT NULL COMMENT '关联原始日志ID(log_collect_raw.id)',
+ collect_address_id INT NOT NULL COMMENT '采集地址ID(关联cnc_collect_address)',
+ machine_id INT NOT NULL COMMENT '机床ID(关联cnc_machine)',
+ analysis_type VARCHAR(30) NOT NULL COMMENT '分析类型枚举',
+ previous_program VARCHAR(200) NULL COMMENT '上一次NC程序名',
+ current_program VARCHAR(200) NULL COMMENT '本次NC程序名',
+ previous_part_count DECIMAL(15,5) NULL COMMENT '上一次零件计数',
+ current_part_count DECIMAL(15,5) NULL COMMENT '本次零件计数',
+ part_count_delta DECIMAL(15,5) NULL COMMENT '零件计数变化量(正=增加,负=减少)',
+ previous_status VARCHAR(20) NULL COMMENT '上一次设备状态',
+ current_status VARCHAR(20) NULL COMMENT '本次设备状态',
+ analysis_summary VARCHAR(500) NOT NULL COMMENT '人类可读的分析摘要',
+ analysis_detail JSON NULL COMMENT '完整的字段级对比数据(JSON)',
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (id, analysis_time),
+ INDEX idx_address_time (collect_address_id, analysis_time),
+ INDEX idx_machine_time (machine_id, analysis_time),
+ INDEX idx_type_time (analysis_type, analysis_time),
+ INDEX idx_raw_log (raw_log_id),
+ INDEX idx_program_time (current_program, analysis_time)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
+ COMMENT='采集分析日志表(按月分区,记录每次采集对每台机床的数据变化分析)'
+ PARTITION BY RANGE (TO_DAYS(analysis_time)) (
+ PARTITION p202605 VALUES LESS THAN (TO_DAYS('2026-06-01')),
+ PARTITION p202606 VALUES LESS THAN (TO_DAYS('2026-07-01')),
+ PARTITION p202607 VALUES LESS THAN (TO_DAYS('2026-08-01')),
+ PARTITION p_future VALUES LESS THAN MAXVALUE
+ );
+``
+
+**analysis_type 分析类型枚举:**
+
+| 值 | 含义 | 说明 |
+|----|------|------|
+| NORMAL_UNCHANGED | 正常无变化 | 数据与上次一致,正常加工中 |
+| PART_COUNT_INCREASE | 零件数增加 | 零件计数增长,正常加工中 |
+| PROGRAM_SWITCH | NC程序切换 | 程序名变更,触发上一段结账 |
+| MANUAL_RESET | 手动清零 | 同程序下零件计数下降 |
+| DEVICE_ONLINE | 设备上线 | 设备从离线恢复在线 |
+| DEVICE_OFFLINE | 设备离线 | 设备变为离线状态 |
+| NEW_DEVICE_FOUND | 发现新设备 | 采集到未注册的device |
+| DATA_ANOMALY | 数据异常 | 字段缺失/格式错误/值异常 |
+| COLLECTION_FAILED | 采集失败 | 本次采集请求失败 |
+
+**数据量估算**:每次采集周期×每台机床1条。假设每30秒1次采集×5-10个地址×每地址2-5台机床 = 600-2500条/分钟 = 约86-360万条/天。按月分区便于查询和清理。
+
+---
+
+### 3.5 采集周期汇总表 log_collect_cycle(按月分区)
+
+记录每次采集周期(一个地址的一次完整HTTP采集)的汇总信息。一个周期对应 log_collect_raw 中的一条记录和 log_collect_analysis 中的多条记录。
+
+``sql
+CREATE TABLE log_collect_cycle (
+ id BIGINT AUTO_INCREMENT,
+ cycle_time DATETIME NOT NULL COMMENT '周期开始时间(分区键)',
+ collect_address_id INT NOT NULL COMMENT '采集地址ID(关联cnc_collect_address)',
+ raw_log_id BIGINT NOT NULL COMMENT '关联原始日志ID(log_collect_raw.id)',
+ end_time DATETIME NULL COMMENT '周期结束时间',
+ duration_ms INT NULL COMMENT '本次采集总耗时(毫秒)',
+ total_machines INT NOT NULL DEFAULT 0 COMMENT '本周期采集的机床总数',
+ success_count INT NOT NULL DEFAULT 0 COMMENT '成功采集的机床数',
+ fail_count INT NOT NULL DEFAULT 0 COMMENT '失败采集的机床数',
+ change_distribution JSON NULL COMMENT '变化类型分布(如 {"PROGRAM_SWITCH":2,"PART_COUNT_INCREASE":5})',
+ has_anomaly TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否存在异常(1=有异常)',
+ cycle_summary VARCHAR(500) NULL COMMENT '人类可读的周期汇总',
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (id, cycle_time),
+ INDEX idx_address_time (collect_address_id, cycle_time),
+ INDEX idx_time (cycle_time),
+ INDEX idx_anomaly_time (has_anomaly, cycle_time)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
+ COMMENT='采集周期汇总表(按月分区,每次采集周期的汇总信息)'
+ PARTITION BY RANGE (TO_DAYS(cycle_time)) (
+ PARTITION p202605 VALUES LESS THAN (TO_DAYS('2026-06-01')),
+ PARTITION p202606 VALUES LESS THAN (TO_DAYS('2026-07-01')),
+ PARTITION p202607 VALUES LESS THAN (TO_DAYS('2026-08-01')),
+ PARTITION p_future VALUES LESS THAN MAXVALUE
+ );
+``
+
+**数据量估算**:每次采集1条。每30秒×5-10个地址 = 10-20条/分钟 = 约1.4-2.9万条/天。远小于分析表,查询负担轻。
+
+---
+
## 四、ER关系总览
`
@@ -614,6 +707,10 @@ cnc_machine 1-----N cnc_alert
--- 日志库 ---
cnc_collect_address 1--N log_collect_raw (弱关联,跨库)
+log_collect_raw 1--N log_collect_analysis (原始日志→分析记录)
+log_collect_raw 1---1 log_collect_cycle (原始日志→周期汇总)
+cnc_machine 1--N log_collect_analysis (弱关联,跨库)
+cnc_collect_address 1--N log_collect_cycle (弱关联,跨库)
log_collector_heartbeat (独立,弱关联collect_address_id)
log_system (独立)
`
@@ -624,9 +721,9 @@ log_system (独立)
| 任务 | 频率 | 操作 |
|------|------|------|
-| 创建新分区 | 每月1日 | 为cnc_collect_record、log_collect_raw、log_system预创建下下月分区 |
-| 删除过期分区 | 每月1日 | DROP超过保留期的分区 |
-| 清理心跳表 | 每天 | DELETE log_collector_heartbeat超过7天的记录 |
+| 创建新分区 | 每月1日 | 为cnc_collect_record、log_collect_raw、log_system、log_collect_analysis、log_collect_cycle预创建下下月分区 |
+| 删除过期分区 | 每月1日 | DROP超过保留期的分区(保留天数=0时不删除) |
+| 清理心跳表 | 每天 | DELETE log_collector_heartbeat超过7天的记录(保留天数=0时不删除) |
| 清理告警表 | 每天 | DELETE cnc_alert已处理且超过180天的记录 |
---
@@ -666,7 +763,16 @@ log_system (独立)
| log_collect_raw | idx_request_time | INDEX | 时间范围清理 |
| log_system | idx_level_time | INDEX | 按级别查错误日志 |
| log_system | idx_source_time | INDEX | 按来源查日志 |
+| log_collect_raw | idx_success_time | INDEX | 按成功/失败筛选 |
+| log_collect_analysis | idx_address_time | INDEX | 按采集地址+时间查分析 |
+| log_collect_analysis | idx_machine_time | INDEX | 按机床+时间查分析 |
+| log_collect_analysis | idx_type_time | INDEX | 按分析类型+时间查 |
+| log_collect_analysis | idx_raw_log | INDEX | 按原始日志ID关联查 |
+| log_collect_analysis | idx_program_time | INDEX | 按NC程序名+时间查 |
+| log_collect_cycle | idx_address_time | INDEX | 按采集地址+时间查周期 |
+| log_collect_cycle | idx_time | INDEX | 时间范围查询 |
+| log_collect_cycle | idx_anomaly_time | INDEX | 查异常周期 |
| log_collector_heartbeat | idx_service_time | INDEX | 服务最新状态 |
| log_collector_heartbeat | idx_address_time | INDEX | 地址心跳历史 |
-共计:20张表,34个索引(含唯一索引)
+共计:22张表,43个索引(含唯一索引)
diff --git a/docs/02-功能清单/管理后台/13-采集日志/00-采集日志-索引.md b/docs/02-功能清单/管理后台/13-采集日志/00-采集日志-索引.md
new file mode 100644
index 0000000..3f99510
--- /dev/null
+++ b/docs/02-功能清单/管理后台/13-采集日志/00-采集日志-索引.md
@@ -0,0 +1,26 @@
+# 采集日志 索引
+
+> 版本:v1.0
+> 最后更新:2026-04-25
+
+---
+
+## 模块概述
+
+展示每次采集的分析日志、采集周期汇总和原始采集数据
+
+## 页面清单
+
+| 页面编号 | 页面名称 | 路由 | 功能概述 |
+|---------|---------|------|---------|
+| 13-01 | 采集日志页面 | /collect-log | 3个Tab(采集周期+分析日志+原始数据) |
+
+## 页面功能详情
+
+### 13-01 采集日志页面
+**路由**:`/collect-log`
+**功能概述**:在同一页面以三个标签页展示采集周期、分析日志、原始数据,支持弹窗查看对比信息和JSON原始数据。
+
+**交互关系说明**:查看分析详情 → 弹窗展示字段对比;查看原始数据 → 弹窗展示JSON
+
+---
diff --git a/docs/02-功能清单/管理后台/13-采集日志/01-采集日志-规范.md b/docs/02-功能清单/管理后台/13-采集日志/01-采集日志-规范.md
new file mode 100644
index 0000000..764fa4b
--- /dev/null
+++ b/docs/02-功能清单/管理后台/13-采集日志/01-采集日志-规范.md
@@ -0,0 +1,131 @@
+# 采集日志-规范
+
+本规范用于管理后台「采集日志」模块的前端实现,覆盖组件选用、数据表格定义、查询筛选、分页、时间选择、标签颜色映射及操作按钮等方面的设计约定,确保UI风格统一、交互清晰、数据结构对齐后端接口。以下规范与现有模块风格保持一致,参考文档:界面变更执行规范、前端全局规范等。
+
+## 1. 组件规范
+- 数据展示:使用 Element Plus 的 el-table 及 el-table-column。
+- 查询区域:使用 el-form、el-form-item、el-input、el-select、el-date-picker、el-time-picker(如需)等。
+- 选项卡:el-tabs、el-tab-pane,分别承载分析记录、采集周期与原始数据三大区域。
+- 弹窗与详情:el-dialog 展示分析详情及原始日志原文等。
+- 按钮与标签:el-button、el-tag、el-tooltip 提供操作入口及信息提示。
+- 提示与对齐:使用 el-message、el-notification 提供反馈,表格列对齐统一采用左对齐。
+- 加载与空态:使用 el-skeleton、empty 组件作为加载与无数据态的占位显示。
+
+## 2. 数据表格列定义
+### Tab1:分析记录 表格列
+| 字段名 | 展示含义 | 注意事项 |
+|---|---|---|
+| time | 日志分析时间 | 日期时间格式统一为 yyyy-MM-dd HH:mm:ss |
+| address | 采集地址 | 全局唯一识别码或名称 |
+| machine | 机床 | 机器编号/名称 |
+| type | 分析类型 | 参考下方标签颜色映射 |
+| previousProgram | 前程序 | 准确的程序名 |
+| currentProgram | 当前程序 | 当前正在执行的程序名 |
+| yieldDelta | 产量变化 | 数值变化量,单位需一致 |
+| summary | 摘要 | 简短描述分析结果 |
+| actions | 操作 | 查看详情按钮等 |
+
+### Tab2:采集周期 表格列
+| 字段名 | 展示含义 | 备注 |
+|---|---|---|
+| time | 周期开始时间 | 统一时间格式 |
+| address | 采集地址 | |
+| totalMachines | 总机床数 | 统计口径一致 |
+| success | 成功次数 | |
+| failure | 失败次数 | |
+| anomaly | 异常次数 | |
+| distribution | 数据分布摘要 | 摘要字段,方便快速浏览 |
+| summary | 摘要 | 简要描述周期信息 |
+
+### Tab3:原始数据 表格列
+| 字段名 | 展示含义 | 备注 |
+|---|---|---|
+| rawId | 日志原始ID | |
+| logTime | 日志时间 | 解析时间戳 |
+| contentPreview | 内容预览 | 仅显示摘要片段 |
+| sourceAddress | 数据来源地址 | |
+
+> 注:表格列定义仅为前端展现的约束,实际字段名称以后端接口返回字段为准。
+
+## 3. 查询筛选条件
+- 时间范围筛选:使用 el-date-picker 的 date 范围选择,格式 yyyy-MM-dd HH:mm:ss,范围值作为请求的 startTime/endTime。
+- 采集地址:下拉或输入框筛选,支持模糊匹配地址名称。
+- 机床:下拉选择框,按可选机床列出。
+- 分析类型:多选筛选,UI 采用 el-tag 形式展示筛选条件。
+- 程序名:文本输入,用于匹配前程序或当前程序。
+- 分页参数:page、pageSize,pageSize 默认为 20,支持切换 [20, 50, 100]。
+- 备注:筛选条件应可组合使用,且具备清空按钮重置。
+
+## 4. 分页规范
+- pageSize 选项:20、50、100
+- 分页控件样式:el-pagination,显示总条数、每页条数、页码跳转
+- 数据加载时,应显示加载中状态,切换分页时防重复请求,避免并发冲突。
+
+## 5. API 响应格式
+- 前端对接后端 API 的统一响应格式为:
+```
+{ code: 0, message: "success", data: ... }
+```
+- 当 code 非 0 时,展示错误信息 message,必要时提供可复现的错误提示。
+
+## 6. 时间选择器规范
+- 使用 el-date-picker,类型为 daterange,时间格式统一为 yyyy-MM-dd HH:mm:ss,value-format 亦为 yyyy-MM-dd HH:mm:ss。
+- 时间筛选优先级高于别的筛选条件;在无时区信息情况下以服务器时区为准。
+- Tab1 的时间范围应以分析时间为准,Tab2 的时间范围以周期起止时间为准。
+
+## 7. 分析类型标签颜色映射
+- NORMAL_UNCHANGED -> info
+- PART_COUNT_INCREASE -> success
+- PROGRAM_SWITCH -> warning
+- MANUAL_RESET -> warning
+- DEVICE_ONLINE -> success
+- DEVICE_OFFLINE -> danger
+- NEW_DEVICE_FOUND -> danger
+- DATA_ANOMALY -> danger
+- COLLECTION_FAILED -> danger
+
+## 8. 操作按钮规范
+- 行级操作:查看详情按钮,点击后弹出详情弹窗,展示分析详情或原始数据片段。
+- 关联跳转:若需要跳转到关联页面,提供跳转按钮,并在按钮上标注目标路径或模块名称。
+- 只在需要的场景启用导出、复制等辅助按钮,避免界面拥挤。
+- 按钮颜色使用 Element Plus 默认颜色方案,确保与全局主题一致。
+
+## 9. 路由与权限(简要)
+- 路由路径:/collect-log(遵循现有路由命名约定)
+- 页面权限:遵循统一的路由权限策略,必要时标注只读/编辑权限。
+
+## 10. 错误处理与空态
+- 网络异常、接口返回错误应给出清晰的错误信息提示。
+- 数据为空时展示空态组件,辅以引导文本。
+
+## 11. 视觉与可访问性
+- 保持列宽一致,避免列数据溢出,必要时显示省略号并悬浮显示完整内容。
+- 表格行高、文本颜色、对比度符合无障碍要求,确保在常用屏幕下可读。
+
+## 12. 性能与缓存
+- 大数据分页时采用服务端分页,前端仅请求当前页数据。
+- 尽量使用简化字段,减少表格渲染开销。
+
+## 13. 安全与数据脱敏
+- 脱敏处理涉及敏感字段的展示,必要时对字段进行脱敏或隐藏。
+- 请求应携带适用的鉴权信息,后端返回的数据不可直接暴露敏感字段。
+
+## 14. 版本与兼容性
+- 文档版本随代码同步更新,保持与后端接口版本一致。
+- 如后端接口变更,及时在前端更新字段映射及展示逻辑。
+
+## 15. 兼容性与国际化
+- 支持简体中文显示,未来如扩展到多语言需提供翻译资源。
+- UI 组件需要兼容主流浏览器,与公司统一浏览器兼容性要求一致。
+
+## 16. 维护与扩展点
+- 行为和字段若增加,必须更新对应的索引与页面文档。
+- 新增字段应通过后端接口文档对齐,并同步 Mock 数据结构。
+
+## 17. 附加说明
+- 本规范仅定义前端展示层的通用原则,具体字段名称以后端接口返回字段为准。
+- 如遇特殊场景,需与后端对接团队共同确认后再实现。
+
+---
+
+备注:如需对照其他模块的设计风格,请参考文档:`docs/02-功能清单/07-告警管理/`等的规范表述。
diff --git a/docs/02-功能清单/管理后台/13-采集日志/13-01-采集日志页面.md b/docs/02-功能清单/管理后台/13-采集日志/13-01-采集日志页面.md
new file mode 100644
index 0000000..6644600
--- /dev/null
+++ b/docs/02-功能清单/管理后台/13-采集日志/13-01-采集日志页面.md
@@ -0,0 +1,117 @@
+# 13-01 采集日志页面
+
+_
+本文档按照20项模板撰写,用于前端实现“采集日志页面”的设计与交互规范。页面在管理后台中放置于 /collect-log 路由下,包含三个标签页:分析记录、采集周期、原始数据。_
+
+## 1. 页面基本信息
+- 模块:管理后台 -> 采集日志
+- 页面名称:采集日志页面
+- 路由:/collect-log
+- 版本:v1.0
+- 作者:设计/前端团队
+- 依赖:Element Plus、Vue 3、TypeScript
+
+## 2. 布局结构
+- 顶部区域:查询条件区域(时间范围、地址、机床、分析类型、程序名等)
+- 中部区域:Tabs 切换,包含三个 Tab:分析记录、采集周期、原始数据
+- 各 Tab 之下为各自的表格 + 分页控件
+- 底部/弹窗区域:查看详情弹窗、关联跳转入口
+
+## 3. 数据表格列定义
+- 分析记录(Tab1)列:时间、地址、机床、分析类型、前程序、当前程序、产量变化、摘要、操作
+- 采集周期(Tab2)列:时间、地址、总机床、成功、失败、异常、分布、摘要
+- 原始数据(Tab3)列:原始日志ID、时间、内容摘要、来源地址
+
+## 4. 查询条件字段
+- 时间范围:日期时间范围选择器,格式 yyyy-MM-dd HH:mm:ss
+- 采集地址:下拉或文本输入,支持模糊匹配
+- 机床:下拉选择
+- 分析类型:多选过滤标签
+- 程序名:文本输入
+- 提交触发:查询按钮,重置按钮
+- 每页显示条数:分页组件控制
+
+## 5. API端点定义
+- GET /api/admin/collect-log/analysis
+- GET /api/admin/collect-log/analysis/{id}
+- GET /api/admin/collect-log/analysis/by-raw/{rawLogId}
+- GET /api/admin/collect-log/cycle
+- GET /api/admin/collect-log/raw
+
+## 6. Mock数据结构
+- 分析记录列表 Mock:数组对象包含 time、address、machine、type、previousProgram、currentProgram、yieldDelta、summary
+- 分析详情 Mock:含 detail 字段、difference 和对比数据
+- 周期数据 Mock:time、address、totalMachines、success、failure、anomaly、distribution、summary
+- 原始数据 Mock:rawId、logTime、contentPreview、sourceAddress
+
+## 7. 交互行为
+- 标签页切换:切换时重新加载对应表格数据
+- 分页:点击页码、切换每页条数时加载对应页数据
+- 查看详情弹窗:选中行后弹出,展示分析详情或对比信息
+- 关联跳转:点击相关行的跳转按钮,跳转至对应的关联页面
+- 原始数据查看:点击原始数据行,弹出 JSON 原始数据预览
+
+## 8. 组件树
+- 组件树示例:CollectLogPage -> (QueryForm, ElTabs -> (AnalysisTab, CycleTab, RawTab) -> (ElTable, ElPagination)) -> DetailDialog
+- 表单控件(QueryForm):el-form、el-form-item、el-date-picker、el-input、el-select、el-tag
+- 表格与分页:el-table、el-table-column、el-pagination
+- 弹窗:el-dialog
+- 细化的子组件:AnalysisTable、CycleTable、RawTable、DetailDialog(可复用)
+
+## 9. 路由配置
+- 路由路径:/collect-log
+- 路由组件:CollectLogPage
+- 路由守卫:同其他管理后台页面的权限控制
+- 嵌套路由(若有需要):/collect-log/analysis、/collect-log/cycle、/collect-log/raw
+
+## 10. 数据校验与错误处理
+- 搜索条件必填项的格式校验:时间范围格式、文本字段长度等
+- API 请求失败时,展示友好错误信息并保留上一次有效数据展示
+- 弹窗中的对比信息若数据为空,显示空态提示
+
+## 11. 性能与优化点
+- 分页按需加载,避免一次性加载所有数据
+- 表格列尽量避免使用复杂自定义渲染,必要时使用虚拟滚动
+- 原始数据区域对大文本使用内容摘要展示,点击展开查看全文
+
+## 12. 国际化与无障碍
+- 暂定中文显示,未来支持多语言资源
+- 组件具备基础无障碍特性,表格可读性良好
+
+## 13. 数据引用与结构(接口关联)
+- 参考后端 API 端点,数据结构需与后端返回字段严格对齐
+- 需在页面中以常量形式存放端点引用及字段映射,便于维护
+
+## 14. 组件样式与风格
+- 遵循全局主题,表格列宽可自适应,必要时固定宽度以确保对齐
+- 按钮、标签、弹窗风格与全局规范保持一致
+
+## 15. 版本与变更记录
+- 每次变更需记录版本号和变更摘要,便于回溯
+- 与模块索引、总览文档保持同步
+
+## 16. 技术实现概要
+- 主要使用 Vue 3 + TypeScript + Element Plus,按项目的前端全局规范实现
+- 数据获取走统一的 API 调用封装,错误统一处理
+- UI 组件具有可复用性,便于其他模块复用
+
+## 17. 数据流与状态管理
+- 页面局部状态通过 Vue 的响应式系统管理
+- 表格数据、筛选条件和分页状态保持在组件状态中,必要时通过 Store/Pinia 共享
+
+## 18. 测试用例设计
+- 基本渲染测试:页面渲染、表格列正确显示
+- 筛选与分页功能测试
+- 弹窗查看详情测试
+- 跳转与联动测试
+
+## 19. 部署与运行
+- 本地调试:确保 /collect-log 路由可访问,接口 Mock/测试环境数据正常
+- 与后端联调时保持端点一致,返回字段映射不变
+
+## 20. 变更记录
+- 记录本页面文档的变更时间、版本和修改内容,便于团队追踪
+
+---
+
+备注:本文档遵循文档结构规范,确保与其他模块文档风格一致,如需对齐请参考 `docs/02-功能清单/02-文件夹创建规范.md` 与 `docs/02-界面变更执行规范.md`。
diff --git a/src/CncCollector/Core/AnalysisEngine.cs b/src/CncCollector/Core/AnalysisEngine.cs
new file mode 100644
index 0000000..95f4914
--- /dev/null
+++ b/src/CncCollector/Core/AnalysisEngine.cs
@@ -0,0 +1,401 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using Dapper;
+using MySqlConnector;
+using Newtonsoft.Json;
+using CncModels.Entity;
+using log4net;
+
+namespace CncCollector.Core
+{
+ ///
+ /// 采集分析引擎。
+ /// 在每次采集周期后,对比每台机床的当前数据与上一次采集数据,
+ /// 生成分析记录(log_collect_analysis)和周期汇总(log_collect_cycle)。
+ /// 异常类分析自动写入告警表(cnc_alert)。
+ ///
+ public class AnalysisEngine
+ {
+ private static readonly ILog _log = LogManager.GetLogger(typeof(AnalysisEngine));
+
+ /// 业务库连接字符串(写告警用)
+ private readonly string _businessConnStr;
+
+ /// 日志库连接字符串(写分析/周期表用)
+ private readonly string _logConnStr;
+
+ /// 内存缓存:machineId → 上一次采集状态快照
+ private readonly ConcurrentDictionary _lastSnapshot = new ConcurrentDictionary();
+
+ ///
+ /// 采集缓存快照(每台机床的上一次状态)
+ ///
+ public class MachineSnapshot
+ {
+ public string ProgramName { get; set; }
+ public decimal? PartCount { get; set; }
+ public string DeviceStatus { get; set; }
+ public DateTime CollectTime { get; set; }
+ }
+
+ ///
+ /// 初始化分析引擎
+ ///
+ /// 业务库连接字符串
+ /// 日志库连接字符串
+ public AnalysisEngine(string businessConnStr, string logConnStr)
+ {
+ _businessConnStr = businessConnStr ?? throw new ArgumentNullException(nameof(businessConnStr));
+ _logConnStr = logConnStr ?? throw new ArgumentNullException(nameof(logConnStr));
+ }
+
+ ///
+ /// 分析一次采集周期的所有设备数据,写入分析记录和周期汇总。
+ ///
+ /// 本次原始日志ID(log_collect_raw.id)
+ /// 采集地址ID
+ /// 采集地址名称
+ /// 本次采集的结构化记录列表
+ /// device_code → Machine 的查找字典
+ /// 周期开始时间
+ /// 本次采集耗时(毫秒)
+ public void AnalyzeAndRecord(long rawLogId, int collectAddressId, string addressName,
+ List records, Dictionary machineDict,
+ DateTime cycleStartTime, long durationMs)
+ {
+ if (records == null || records.Count == 0) return;
+
+ try
+ {
+ var analysisTime = DateTime.Now;
+ var hasAnomaly = false;
+ var changeDistribution = new Dictionary();
+ int successCount = 0;
+
+ // 构建 machineId → Machine 查找字典
+ var machineById = new Dictionary();
+ foreach (var m in machineDict.Values)
+ {
+ machineById[m.Id] = m;
+ }
+
+ // 逐条分析
+ foreach (var rec in records)
+ {
+ try
+ {
+ // 获取机床信息
+ Machine machine = null;
+ machineById.TryGetValue(rec.MachineId, out machine);
+ string machineName = machine?.Name ?? ("机床" + rec.MachineId);
+
+ // 当前值
+ string currentProgram = rec.ProgramName;
+ decimal? currentPartCount = rec.PartCount;
+ string currentStatus = rec.DeviceStatus;
+
+ // 获取上次快照
+ MachineSnapshot prev;
+ _lastSnapshot.TryGetValue(rec.MachineId, out prev);
+
+ // 计算分析类型和摘要
+ string analysisType;
+ string summary;
+ bool needAlert = false;
+ string alertType = null;
+
+ DetermineAnalysis(prev, currentProgram, currentPartCount, currentStatus,
+ machineName, out analysisType, out summary, out needAlert, out alertType);
+
+ // 计算变化量
+ decimal? partCountDelta = null;
+ if (currentPartCount.HasValue && prev != null && prev.PartCount.HasValue)
+ {
+ partCountDelta = currentPartCount.Value - prev.PartCount.Value;
+ }
+
+ // 构建分析明细JSON
+ var detail = new
+ {
+ previous = prev != null ? new
+ {
+ program = prev.ProgramName,
+ partCount = prev.PartCount,
+ status = prev.DeviceStatus
+ } : null,
+ current = new
+ {
+ program = currentProgram,
+ partCount = currentPartCount,
+ status = currentStatus
+ },
+ delta = new { partCount = partCountDelta },
+ collectTime = rec.CollectTime.ToString("yyyy-MM-dd HH:mm:ss")
+ };
+ string detailJson = JsonConvert.SerializeObject(detail);
+
+ // 写入分析记录
+ WriteAnalysis(new CncModels.Entity.CollectAnalysis
+ {
+ AnalysisTime = analysisTime,
+ RawLogId = rawLogId,
+ CollectAddressId = collectAddressId,
+ MachineId = rec.MachineId,
+ AnalysisType = analysisType,
+ PreviousProgram = prev?.ProgramName,
+ CurrentProgram = currentProgram,
+ PreviousPartCount = prev?.PartCount,
+ CurrentPartCount = currentPartCount,
+ PartCountDelta = partCountDelta,
+ PreviousStatus = prev?.DeviceStatus,
+ CurrentStatus = currentStatus,
+ AnalysisSummary = summary,
+ AnalysisDetail = detailJson
+ });
+
+ // 更新快照
+ _lastSnapshot[rec.MachineId] = new MachineSnapshot
+ {
+ ProgramName = currentProgram,
+ PartCount = currentPartCount,
+ DeviceStatus = currentStatus,
+ CollectTime = rec.CollectTime
+ };
+
+ // 统计分布
+ if (changeDistribution.ContainsKey(analysisType))
+ changeDistribution[analysisType]++;
+ else
+ changeDistribution[analysisType] = 1;
+
+ // 异常告警
+ if (needAlert)
+ {
+ hasAnomaly = true;
+ WriteAlert(alertType, rec.MachineId, collectAddressId, summary, detailJson);
+ }
+
+ // 统计成功数(非异常即为成功)
+ if (analysisType != "COLLECTION_FAILED" && analysisType != "DATA_ANOMALY")
+ successCount++;
+ }
+ catch (Exception ex)
+ {
+ _log.Error($"分析单条记录失败(machineId={rec.MachineId})", ex);
+ }
+ }
+
+ // 写入周期汇总
+ WriteCycleSummary(new CncModels.Entity.CollectCycle
+ {
+ CycleTime = cycleStartTime,
+ CollectAddressId = collectAddressId,
+ RawLogId = rawLogId,
+ EndTime = analysisTime,
+ DurationMs = (int)durationMs,
+ TotalMachines = records.Count,
+ SuccessCount = successCount,
+ FailCount = records.Count - successCount,
+ ChangeDistribution = JsonConvert.SerializeObject(changeDistribution),
+ HasAnomaly = hasAnomaly ? 1 : 0,
+ CycleSummary = $"共{records.Count}台机床完成分析" + (hasAnomaly ? ",存在异常" : "")
+ });
+ }
+ catch (Exception ex)
+ {
+ _log.Error($"采集分析失败(地址={addressName}, rawLogId={rawLogId})", ex);
+ }
+ }
+
+ ///
+ /// 根据前后状态对比,确定分析类型
+ ///
+ private void DetermineAnalysis(MachineSnapshot prev, string currentProgram, decimal? currentPartCount,
+ string currentStatus, string machineName, out string analysisType, out string summary,
+ out bool needAlert, out string alertType)
+ {
+ needAlert = false;
+ alertType = null;
+
+ // 无历史快照 → 首次上线
+ if (prev == null)
+ {
+ analysisType = "DEVICE_ONLINE";
+ summary = $"机床{machineName}首次上线,程序={currentProgram ?? "未知"}";
+ needAlert = true;
+ alertType = "unknown_device";
+ return;
+ }
+
+ string prevProgram = prev.ProgramName;
+ decimal? prevPartCount = prev.PartCount;
+ string prevStatus = prev.DeviceStatus;
+
+ // 检测程序切换
+ if (!string.IsNullOrEmpty(currentProgram) && !string.IsNullOrEmpty(prevProgram) &&
+ !string.Equals(prevProgram, currentProgram, StringComparison.OrdinalIgnoreCase))
+ {
+ analysisType = "PROGRAM_SWITCH";
+ summary = $"机床{machineName}程序切换: {prevProgram} → {currentProgram}";
+ return;
+ }
+
+ // 检测手动清零(同程序下零件数下降)
+ if (currentPartCount.HasValue && prevPartCount.HasValue &&
+ currentPartCount.Value < prevPartCount.Value)
+ {
+ analysisType = "MANUAL_RESET";
+ summary = $"机床{machineName}零件计数手动清零: {prevPartCount} → {currentPartCount}";
+ return;
+ }
+
+ // 检测零件数增加
+ if (currentPartCount.HasValue && prevPartCount.HasValue &&
+ currentPartCount.Value > prevPartCount.Value)
+ {
+ decimal delta = currentPartCount.Value - prevPartCount.Value;
+ analysisType = "PART_COUNT_INCREASE";
+ summary = $"机床{machineName}新增{delta}个零件({prevPartCount} → {currentPartCount})";
+ return;
+ }
+
+ // 检测设备离线/告警
+ if (!string.IsNullOrEmpty(currentStatus) &&
+ (currentStatus.Equals("OFFLINE", StringComparison.OrdinalIgnoreCase) ||
+ currentStatus.Equals("ALARM", StringComparison.OrdinalIgnoreCase) ||
+ currentStatus.Equals("EMERGENCY", StringComparison.OrdinalIgnoreCase)))
+ {
+ analysisType = "DEVICE_OFFLINE";
+ summary = $"机床{machineName}设备离线/告警: {currentStatus}";
+ needAlert = true;
+ alertType = "device_offline";
+ return;
+ }
+
+ // 检测数据异常(关键字段缺失但设备应该在线)
+ if (string.IsNullOrEmpty(currentProgram) && !string.IsNullOrEmpty(currentStatus) &&
+ !currentStatus.Equals("OFFLINE", StringComparison.OrdinalIgnoreCase))
+ {
+ analysisType = "DATA_ANOMALY";
+ summary = $"机床{machineName}数据异常: 缺少程序名字段";
+ needAlert = true;
+ alertType = "data_anomaly";
+ return;
+ }
+
+ // 无重大变化
+ analysisType = "NORMAL_UNCHANGED";
+ summary = $"机床{machineName}数据无重大变化";
+ }
+
+ ///
+ /// 写入单条分析记录到 log_collect_analysis
+ ///
+ private void WriteAnalysis(CncModels.Entity.CollectAnalysis entity)
+ {
+ try
+ {
+ using (var conn = new MySqlConnection(_logConnStr))
+ {
+ conn.Execute(@"INSERT INTO log_collect_analysis
+ (analysis_time, raw_log_id, collect_address_id, machine_id, analysis_type,
+ previous_program, current_program, previous_part_count, current_part_count,
+ part_count_delta, previous_status, current_status, analysis_summary,
+ analysis_detail, created_at)
+ VALUES (@AnalysisTime, @RawLogId, @CollectAddressId, @MachineId, @AnalysisType,
+ @PreviousProgram, @CurrentProgram, @PreviousPartCount, @CurrentPartCount,
+ @PartCountDelta, @PreviousStatus, @CurrentStatus, @AnalysisSummary,
+ @AnalysisDetail, NOW())",
+ new
+ {
+ entity.AnalysisTime,
+ entity.RawLogId,
+ entity.CollectAddressId,
+ entity.MachineId,
+ entity.AnalysisType,
+ entity.PreviousProgram,
+ entity.CurrentProgram,
+ entity.PreviousPartCount,
+ entity.CurrentPartCount,
+ entity.PartCountDelta,
+ entity.PreviousStatus,
+ entity.CurrentStatus,
+ entity.AnalysisSummary,
+ entity.AnalysisDetail
+ });
+ }
+ }
+ catch (Exception ex)
+ {
+ _log.Error($"写入分析记录失败(machineId={entity.MachineId})", ex);
+ }
+ }
+
+ ///
+ /// 写入周期汇总到 log_collect_cycle
+ ///
+ private void WriteCycleSummary(CncModels.Entity.CollectCycle entity)
+ {
+ try
+ {
+ using (var conn = new MySqlConnection(_logConnStr))
+ {
+ conn.Execute(@"INSERT INTO log_collect_cycle
+ (cycle_time, collect_address_id, raw_log_id, end_time, duration_ms,
+ total_machines, success_count, fail_count, change_distribution,
+ has_anomaly, cycle_summary, created_at)
+ VALUES (@CycleTime, @CollectAddressId, @RawLogId, @EndTime, @DurationMs,
+ @TotalMachines, @SuccessCount, @FailCount, @ChangeDistribution,
+ @HasAnomaly, @CycleSummary, NOW())",
+ new
+ {
+ entity.CycleTime,
+ entity.CollectAddressId,
+ entity.RawLogId,
+ entity.EndTime,
+ entity.DurationMs,
+ entity.TotalMachines,
+ entity.SuccessCount,
+ entity.FailCount,
+ entity.ChangeDistribution,
+ entity.HasAnomaly,
+ entity.CycleSummary
+ });
+ }
+ }
+ catch (Exception ex)
+ {
+ _log.Error("写入周期汇总失败", ex);
+ }
+ }
+
+ ///
+ /// 写入告警到 cnc_alert(业务库)
+ ///
+ private void WriteAlert(string alertType, int machineId, int collectAddressId, string title, string detail)
+ {
+ try
+ {
+ using (var conn = new MySqlConnection(_businessConnStr))
+ {
+ conn.Execute(@"INSERT INTO cnc_alert (alert_type, machine_id, collect_address_id, title, detail, is_resolved, created_at)
+ VALUES (@AlertType, @MachineId, @AddressId, @Title, @Detail, 0, NOW())",
+ new
+ {
+ AlertType = alertType,
+ MachineId = machineId,
+ AddressId = collectAddressId,
+ Title = title,
+ Detail = detail
+ });
+ }
+ }
+ catch (Exception ex)
+ {
+ _log.Error($"写入告警失败(alertType={alertType}, machineId={machineId})", ex);
+ }
+ }
+ }
+}
diff --git a/src/CncCollector/Core/CollectRecordWriter.cs b/src/CncCollector/Core/CollectRecordWriter.cs
index 2423575..a8daa23 100644
--- a/src/CncCollector/Core/CollectRecordWriter.cs
+++ b/src/CncCollector/Core/CollectRecordWriter.cs
@@ -28,11 +28,12 @@ namespace CncCollector.Core
/// 响应耗时(毫秒)
/// 是否采集成功
/// 错误信息(失败时)
- public static void WriteBatch(string businessConnStr, string logConnStr,
+ public static long WriteBatch(string businessConnStr, string logConnStr,
List records, string rawJson, int collectAddressId,
DateTime requestTime, long? responseDurationMs, bool isSuccess, string errorMessage, int? statusCode = null)
{
var now = DateTime.Now;
+ long lastRawLogId = 0;
// 1. 写入原始JSON到日志库
try
@@ -54,6 +55,12 @@ namespace CncCollector.Core
ErrorMessage = errorMessage ?? (string)null,
CreatedAt = now
});
+ // 记录刚插入的 raw_log 的自增ID
+ try
+ {
+ lastRawLogId = conn.ExecuteScalar("SELECT LAST_INSERT_ID();");
+ }
+ catch { lastRawLogId = 0; }
}
}
catch (Exception ex)
@@ -61,7 +68,7 @@ namespace CncCollector.Core
_log.Error($"写入原始JSON日志失败(地址ID={collectAddressId})", ex);
}
- if (!isSuccess || records == null || records.Count == 0) return;
+ if (!isSuccess || records == null || records.Count == 0) return lastRawLogId;
// 2. 批量写入采集结构化记录到业务库
try
@@ -162,6 +169,7 @@ namespace CncCollector.Core
{
_log.Error($"批量写入采集记录失败(地址ID={collectAddressId})", ex);
}
+ return lastRawLogId;
}
///
diff --git a/src/CncCollector/Core/CollectWorker.cs b/src/CncCollector/Core/CollectWorker.cs
index 7311ff0..7d6d408 100644
--- a/src/CncCollector/Core/CollectWorker.cs
+++ b/src/CncCollector/Core/CollectWorker.cs
@@ -29,6 +29,7 @@ namespace CncCollector.Core
private readonly CollectAddress _address;
private readonly CollectorConfig _config;
private readonly ProductionTracker _tracker;
+ private readonly AnalysisEngine _analysisEngine;
private readonly string _businessConnStr;
private readonly string _logConnStr;
private Thread _thread;
@@ -65,11 +66,12 @@ namespace CncCollector.Core
/// 业务库连接字符串
/// 日志库连接字符串
public CollectWorker(CollectAddress address, CollectorConfig config, ProductionTracker tracker,
- string businessConnStr, string logConnStr)
+ AnalysisEngine analysisEngine, string businessConnStr, string logConnStr)
{
_address = address;
_config = config;
_tracker = tracker;
+ _analysisEngine = analysisEngine;
_businessConnStr = businessConnStr;
_logConnStr = logConnStr;
}
@@ -400,9 +402,15 @@ namespace CncCollector.Core
}
// 4. 批量写入
- CollectRecordWriter.WriteBatch(_businessConnStr, _logConnStr, records, rawJson,
+ long rawLogId = CollectRecordWriter.WriteBatch(_businessConnStr, _logConnStr, records, rawJson,
_address.Id, requestTime, durationMs, true, null, statusCode);
+ // 采集分析:将分析任务委托给 AnalysisEngine
+ if (rawLogId > 0 && records != null && records.Count > 0 && _analysisEngine != null)
+ {
+ _analysisEngine.AnalyzeAndRecord(rawLogId, _address.Id, _address.Name, records, machineDict, requestTime, durationMs);
+ }
+
_log.Info($"采集完成: {_address.Name} → {records.Count}台设备, {durationMs}ms");
}
diff --git a/src/CncCollector/Core/CollectorEngine.cs b/src/CncCollector/Core/CollectorEngine.cs
index b3d6424..48e9602 100644
--- a/src/CncCollector/Core/CollectorEngine.cs
+++ b/src/CncCollector/Core/CollectorEngine.cs
@@ -21,6 +21,8 @@ namespace CncCollector.Core
private readonly CollectorConfig _config;
private readonly ConcurrentDictionary _workers = new ConcurrentDictionary();
private readonly ProductionTracker _tracker;
+ // 复用的分析引擎实例(简单实现:按地址注入,避免跨线程问题)
+ private readonly AnalysisEngine _analysisEngine;
private readonly DailySummaryJob _dailySummary;
private Timer _heartbeatTimer;
private Timer _configPollTimer;
@@ -52,6 +54,8 @@ namespace CncCollector.Core
_config = config;
_tracker = new ProductionTracker(config.BusinessConnection);
_dailySummary = new DailySummaryJob(config.BusinessConnection);
+ // 初始化分析引擎(与业务库和日志库同源,后续按需调整)
+ _analysisEngine = new AnalysisEngine(config.BusinessConnection, config.LogConnection);
}
///
@@ -203,17 +207,17 @@ namespace CncCollector.Core
}
// 启动新增的地址
- foreach (var addr in addresses)
- {
- if (!_workers.ContainsKey(addr.Id))
+ foreach (var addr in addresses)
{
- var worker = new CollectWorker(addr, _config, _tracker,
- _config.BusinessConnection, _config.LogConnection);
- worker.Start();
- _workers[addr.Id] = worker;
+ if (!_workers.ContainsKey(addr.Id))
+ {
+ var worker = new CollectWorker(addr, _config, _tracker,
+ _analysisEngine, _config.BusinessConnection, _config.LogConnection);
+ worker.Start();
+ _workers[addr.Id] = worker;
_log.Info($"已启动采集地址: {addr.Name}(URL={addr.Url}, 间隔={addr.CollectInterval}秒)");
+ }
}
- }
}
catch (Exception ex)
{
diff --git a/src/CncModels/Dto/CollectLog/CollectAnalysisDetail.cs b/src/CncModels/Dto/CollectLog/CollectAnalysisDetail.cs
new file mode 100644
index 0000000..e9459bb
--- /dev/null
+++ b/src/CncModels/Dto/CollectLog/CollectAnalysisDetail.cs
@@ -0,0 +1,15 @@
+namespace CncModels.Dto.CollectLog
+{
+ ///
+ /// 采集分析详情(基于 CollectAnalysisListItem 的扩展字段)
+ ///
+ public class CollectAnalysisDetail : CollectAnalysisListItem
+ {
+ public decimal? PreviousPartCount { get; set; }
+ public decimal? CurrentPartCount { get; set; }
+ public string PreviousStatus { get; set; }
+ public string CurrentStatus { get; set; }
+ public string AnalysisDetail { get; set; }
+ public long RawLogId { get; set; }
+ }
+}
diff --git a/src/CncModels/Dto/CollectLog/CollectAnalysisListItem.cs b/src/CncModels/Dto/CollectLog/CollectAnalysisListItem.cs
new file mode 100644
index 0000000..1a47198
--- /dev/null
+++ b/src/CncModels/Dto/CollectLog/CollectAnalysisListItem.cs
@@ -0,0 +1,19 @@
+namespace CncModels.Dto.CollectLog
+{
+ ///
+ /// 采集分析列表项
+ ///
+ public class CollectAnalysisListItem
+ {
+ public long Id { get; set; }
+ public string AnalysisTime { get; set; }
+ public int CollectAddressId { get; set; }
+ public int MachineId { get; set; }
+ public string MachineName { get; set; }
+ public string AnalysisType { get; set; }
+ public string PreviousProgram { get; set; }
+ public string CurrentProgram { get; set; }
+ public decimal? PartCountDelta { get; set; }
+ public string AnalysisSummary { get; set; }
+ }
+}
diff --git a/src/CncModels/Dto/CollectLog/CollectAnalysisQuery.cs b/src/CncModels/Dto/CollectLog/CollectAnalysisQuery.cs
new file mode 100644
index 0000000..dc14b37
--- /dev/null
+++ b/src/CncModels/Dto/CollectLog/CollectAnalysisQuery.cs
@@ -0,0 +1,17 @@
+using System;
+
+namespace CncModels.Dto.CollectLog
+{
+ ///
+ /// 采集分析列表查询条件(分页)
+ ///
+ public class CollectAnalysisQuery : PagedQuery
+ {
+ public DateTime? StartDate { get; set; }
+ public DateTime? EndDate { get; set; }
+ public int? CollectAddressId { get; set; }
+ public int? MachineId { get; set; }
+ public string AnalysisType { get; set; }
+ public string ProgramName { get; set; }
+ }
+}
diff --git a/src/CncModels/Dto/CollectLog/CollectCycleListItem.cs b/src/CncModels/Dto/CollectLog/CollectCycleListItem.cs
new file mode 100644
index 0000000..65638a6
--- /dev/null
+++ b/src/CncModels/Dto/CollectLog/CollectCycleListItem.cs
@@ -0,0 +1,19 @@
+namespace CncModels.Dto.CollectLog
+{
+ ///
+ /// 采集周期列表项
+ ///
+ public class CollectCycleListItem
+ {
+ public long Id { get; set; }
+ public string CycleTime { get; set; }
+ public int CollectAddressId { get; set; }
+ public string AddressName { get; set; }
+ public int TotalMachines { get; set; }
+ public int SuccessCount { get; set; }
+ public int FailCount { get; set; }
+ public int HasAnomaly { get; set; }
+ public string ChangeDistribution { get; set; }
+ public string CycleSummary { get; set; }
+ }
+}
diff --git a/src/CncModels/Dto/CollectLog/CollectCycleQuery.cs b/src/CncModels/Dto/CollectLog/CollectCycleQuery.cs
new file mode 100644
index 0000000..b398b96
--- /dev/null
+++ b/src/CncModels/Dto/CollectLog/CollectCycleQuery.cs
@@ -0,0 +1,15 @@
+using System;
+
+namespace CncModels.Dto.CollectLog
+{
+ ///
+ /// 采集周期查询条件(分页)
+ ///
+ public class CollectCycleQuery : PagedQuery
+ {
+ public DateTime? StartDate { get; set; }
+ public DateTime? EndDate { get; set; }
+ public int? CollectAddressId { get; set; }
+ public int? HasAnomaly { get; set; }
+ }
+}
diff --git a/src/CncModels/Entity/CollectAnalysis.cs b/src/CncModels/Entity/CollectAnalysis.cs
new file mode 100644
index 0000000..ff4a454
--- /dev/null
+++ b/src/CncModels/Entity/CollectAnalysis.cs
@@ -0,0 +1,58 @@
+using System;
+
+namespace CncModels.Entity
+{
+ ///
+ /// 采集分析记录实体(日志库 log_collect_analysis)
+ ///
+ public class CollectAnalysis
+ {
+ /// 自增ID
+ public long Id { get; set; }
+
+ /// 分析时间
+ public DateTime AnalysisTime { get; set; }
+
+ /// 原始日志ID
+ public long RawLogId { get; set; }
+
+ /// 采集地址ID
+ public int CollectAddressId { get; set; }
+
+ /// 机器ID
+ public int MachineId { get; set; }
+
+ /// 分析类型
+ public string AnalysisType { get; set; }
+
+ /// 前一程序名
+ public string PreviousProgram { get; set; }
+
+ /// 当前程序名
+ public string CurrentProgram { get; set; }
+
+ /// 前一阶段产出数量
+ public decimal? PreviousPartCount { get; set; }
+
+ /// 当前阶段产出数量
+ public decimal? CurrentPartCount { get; set; }
+
+ /// 产出变化量
+ public decimal? PartCountDelta { get; set; }
+
+ /// 前一状态
+ public string PreviousStatus { get; set; }
+
+ /// 当前状态
+ public string CurrentStatus { get; set; }
+
+ /// 分析概要
+ public string AnalysisSummary { get; set; }
+
+ /// 分析细节JSON字符串
+ public string AnalysisDetail { get; set; }
+
+ /// 创建时间
+ public DateTime CreatedAt { get; set; }
+ }
+}
diff --git a/src/CncModels/Entity/CollectCycle.cs b/src/CncModels/Entity/CollectCycle.cs
new file mode 100644
index 0000000..bef175b
--- /dev/null
+++ b/src/CncModels/Entity/CollectCycle.cs
@@ -0,0 +1,49 @@
+using System;
+
+namespace CncModels.Entity
+{
+ ///
+ /// 采集分析周期实体(日志库 log_collect_cycle)
+ ///
+ public class CollectCycle
+ {
+ /// 自增ID
+ public long Id { get; set; }
+
+ /// 周期时间
+ public DateTime CycleTime { get; set; }
+
+ /// 采集地址ID
+ public int CollectAddressId { get; set; }
+
+ /// 原始日志ID
+ public long RawLogId { get; set; }
+
+ /// 结束时间
+ public DateTime? EndTime { get; set; }
+
+ /// 周期持续时长(毫秒)
+ public int? DurationMs { get; set; }
+
+ /// 总机器数
+ public int TotalMachines { get; set; }
+
+ /// 成功计数
+ public int SuccessCount { get; set; }
+
+ /// 失败计数
+ public int FailCount { get; set; }
+
+ /// 分布变化JSON
+ public string ChangeDistribution { get; set; }
+
+ /// 是否存在异常(0/1)
+ public int HasAnomaly { get; set; }
+
+ /// 周期概要
+ public string CycleSummary { get; set; }
+
+ /// 创建时间
+ public DateTime CreatedAt { get; set; }
+ }
+}
diff --git a/src/CncModels/Enum/AnalysisType.cs b/src/CncModels/Enum/AnalysisType.cs
new file mode 100644
index 0000000..e81209f
--- /dev/null
+++ b/src/CncModels/Enum/AnalysisType.cs
@@ -0,0 +1,18 @@
+namespace CncModels.Enum
+{
+ ///
+ /// 采集分析的分析类型枚举(以字符串常量形式提供)
+ ///
+ public static class AnalysisType
+ {
+ public const string NORMAL_UNCHANGED = "NORMAL_UNCHANGED";
+ public const string PART_COUNT_INCREASE = "PART_COUNT_INCREASE";
+ public const string PROGRAM_SWITCH = "PROGRAM_SWITCH";
+ public const string MANUAL_RESET = "MANUAL_RESET";
+ public const string DEVICE_ONLINE = "DEVICE_ONLINE";
+ public const string DEVICE_OFFLINE = "DEVICE_OFFLINE";
+ public const string NEW_DEVICE_FOUND = "NEW_DEVICE_FOUND";
+ public const string DATA_ANOMALY = "DATA_ANOMALY";
+ public const string COLLECTION_FAILED = "COLLECTION_FAILED";
+ }
+}
diff --git a/src/CncRepository/Impl/Log/CollectAnalysisRepository.cs b/src/CncRepository/Impl/Log/CollectAnalysisRepository.cs
new file mode 100644
index 0000000..6a79e73
--- /dev/null
+++ b/src/CncRepository/Impl/Log/CollectAnalysisRepository.cs
@@ -0,0 +1,191 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Dapper;
+using MySqlConnector;
+using CncModels.Dto;
+using CncModels.Dto.CollectLog;
+using CncModels.Entity;
+using CncRepository.Base;
+using CncRepository.Interface;
+
+namespace CncRepository.Impl.Log
+{
+ ///
+ /// 采集分析仓储实现(日志库 - log_collect_analysis)
+ ///
+ public class CollectAnalysisRepository : LogRepository, ICollectAnalysisRepository
+ {
+ public CollectAnalysisRepository(string connectionString) : base(connectionString) { }
+
+ public PagedResult GetAnalysisList(CollectAnalysisQuery query)
+ {
+ using (var conn = CreateConnection())
+ {
+ var whereParts = new List { "1=1" };
+ var p = new DynamicParameters();
+
+ if (query.StartDate.HasValue)
+ {
+ whereParts.Add("a.analysis_time >= @StartDate");
+ p.Add("StartDate", query.StartDate);
+ }
+ if (query.EndDate.HasValue)
+ {
+ whereParts.Add("a.analysis_time <= @EndDate");
+ p.Add("EndDate", query.EndDate);
+ }
+ if (query.CollectAddressId.HasValue)
+ {
+ whereParts.Add("a.collect_address_id = @CollectAddressId");
+ p.Add("CollectAddressId", query.CollectAddressId);
+ }
+ if (query.MachineId.HasValue)
+ {
+ whereParts.Add("a.machine_id = @MachineId");
+ p.Add("MachineId", query.MachineId);
+ }
+ if (!string.IsNullOrEmpty(query.AnalysisType))
+ {
+ whereParts.Add("a.analysis_type = @AnalysisType");
+ p.Add("AnalysisType", query.AnalysisType);
+ }
+ if (!string.IsNullOrEmpty(query.ProgramName))
+ {
+ whereParts.Add("a.current_program LIKE CONCAT('%', @ProgramName, '%')");
+ p.Add("ProgramName", query.ProgramName);
+ }
+
+ var whereSql = string.Join(" AND ", whereParts);
+
+ // 统计总条数
+ var total = conn.ExecuteScalar(
+ $"SELECT COUNT(1) FROM log_collect_analysis a WHERE {whereSql}", p);
+
+ // 分页查询(左连机床表获取名称)
+ var dataSql = $@"SELECT
+ a.id AS Id,
+ DATE_FORMAT(a.analysis_time, '%Y-%m-%d %H:%i:%s') AS AnalysisTime,
+ a.collect_address_id AS CollectAddressId,
+ a.machine_id AS MachineId,
+ m.name AS MachineName,
+ a.analysis_type AS AnalysisType,
+ a.previous_program AS PreviousProgram,
+ a.current_program AS CurrentProgram,
+ a.part_count_delta AS PartCountDelta,
+ a.analysis_summary AS AnalysisSummary
+ FROM log_collect_analysis a
+ LEFT JOIN cnc_business.cnc_machine m ON a.machine_id = m.id
+ WHERE {whereSql}
+ ORDER BY a.analysis_time DESC
+ LIMIT @PageSize OFFSET @Offset";
+
+ var items = conn.Query(dataSql,
+ new { PageSize = query.PageSize, Offset = query.Offset }).AsList();
+
+ return new PagedResult
+ {
+ Items = items,
+ Total = total,
+ Page = query.Page,
+ PageSize = query.PageSize
+ };
+ }
+ }
+
+ public CollectAnalysisDetail GetAnalysisDetail(long id)
+ {
+ using (var conn = CreateConnection())
+ {
+ var sql = @"SELECT
+ a.id AS Id,
+ DATE_FORMAT(a.analysis_time, '%Y-%m-%d %H:%i:%s') AS AnalysisTime,
+ a.collect_address_id AS CollectAddressId,
+ a.machine_id AS MachineId,
+ m.name AS MachineName,
+ a.analysis_type AS AnalysisType,
+ a.previous_program AS PreviousProgram,
+ a.current_program AS CurrentProgram,
+ a.part_count_delta AS PartCountDelta,
+ a.previous_part_count AS PreviousPartCount,
+ a.current_part_count AS CurrentPartCount,
+ a.previous_status AS PreviousStatus,
+ a.current_status AS CurrentStatus,
+ a.analysis_summary AS AnalysisSummary,
+ a.analysis_detail AS AnalysisDetail,
+ a.raw_log_id AS RawLogId
+ FROM log_collect_analysis a
+ LEFT JOIN cnc_business.cnc_machine m ON a.machine_id = m.id
+ WHERE a.id = @Id";
+ return conn.QueryFirstOrDefault(sql, new { Id = id });
+ }
+ }
+
+ public List GetAnalysisByRawLogId(long rawLogId)
+ {
+ using (var conn = CreateConnection())
+ {
+ var sql = @"SELECT
+ a.id AS Id,
+ DATE_FORMAT(a.analysis_time, '%Y-%m-%d %H:%i:%s') AS AnalysisTime,
+ a.collect_address_id AS CollectAddressId,
+ a.machine_id AS MachineId,
+ m.name AS MachineName,
+ a.analysis_type AS AnalysisType,
+ a.previous_program AS PreviousProgram,
+ a.current_program AS CurrentProgram,
+ a.part_count_delta AS PartCountDelta,
+ a.analysis_summary AS AnalysisSummary
+ FROM log_collect_analysis a
+ LEFT JOIN cnc_business.cnc_machine m ON a.machine_id = m.id
+ WHERE a.raw_log_id = @RawLogId
+ ORDER BY a.analysis_time DESC";
+ return conn.Query(sql, new { RawLogId = rawLogId }).AsList();
+ }
+ }
+
+ public long Create(CollectAnalysis entity)
+ {
+ using (var conn = CreateConnection())
+ {
+ var sql = @"INSERT INTO log_collect_analysis
+ (analysis_time, raw_log_id, collect_address_id, machine_id, analysis_type,
+ previous_program, current_program, previous_part_count, current_part_count,
+ part_count_delta, previous_status, current_status, analysis_summary,
+ analysis_detail, created_at)
+ VALUES (@AnalysisTime, @RawLogId, @CollectAddressId, @MachineId, @AnalysisType,
+ @PreviousProgram, @CurrentProgram, @PreviousPartCount, @CurrentPartCount,
+ @PartCountDelta, @PreviousStatus, @CurrentStatus, @AnalysisSummary,
+ @AnalysisDetail, NOW());
+ SELECT LAST_INSERT_ID();";
+ return conn.ExecuteScalar(sql, new
+ {
+ entity.AnalysisTime,
+ entity.RawLogId,
+ entity.CollectAddressId,
+ entity.MachineId,
+ entity.AnalysisType,
+ entity.PreviousProgram,
+ entity.CurrentProgram,
+ entity.PreviousPartCount,
+ entity.CurrentPartCount,
+ entity.PartCountDelta,
+ entity.PreviousStatus,
+ entity.CurrentStatus,
+ entity.AnalysisSummary,
+ entity.AnalysisDetail
+ });
+ }
+ }
+
+ public int DeleteBeforeDate(DateTime date)
+ {
+ using (var conn = CreateConnection())
+ {
+ return conn.Execute(
+ "DELETE FROM log_collect_analysis WHERE analysis_time < @Date",
+ new { Date = date });
+ }
+ }
+ }
+}
diff --git a/src/CncRepository/Impl/Log/CollectCycleRepository.cs b/src/CncRepository/Impl/Log/CollectCycleRepository.cs
new file mode 100644
index 0000000..7bb70e3
--- /dev/null
+++ b/src/CncRepository/Impl/Log/CollectCycleRepository.cs
@@ -0,0 +1,123 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Dapper;
+using MySqlConnector;
+using CncModels.Dto;
+using CncModels.Dto.CollectLog;
+using CncModels.Entity;
+using CncRepository.Base;
+using CncRepository.Interface;
+
+namespace CncRepository.Impl.Log
+{
+ ///
+ /// 采集周期仓储实现(日志库 - log_collect_cycle)
+ ///
+ public class CollectCycleRepository : LogRepository, ICollectCycleRepository
+ {
+ public CollectCycleRepository(string connectionString) : base(connectionString) { }
+
+ public PagedResult GetCycleList(CollectCycleQuery query)
+ {
+ using (var conn = CreateConnection())
+ {
+ var whereParts = new List { "1=1" };
+ var p = new DynamicParameters();
+
+ if (query.StartDate.HasValue)
+ {
+ whereParts.Add("c.cycle_time >= @StartDate");
+ p.Add("StartDate", query.StartDate);
+ }
+ if (query.EndDate.HasValue)
+ {
+ whereParts.Add("c.cycle_time <= @EndDate");
+ p.Add("EndDate", query.EndDate);
+ }
+ if (query.CollectAddressId.HasValue)
+ {
+ whereParts.Add("c.collect_address_id = @CollectAddressId");
+ p.Add("CollectAddressId", query.CollectAddressId);
+ }
+ if (query.HasAnomaly.HasValue)
+ {
+ whereParts.Add("c.has_anomaly = @HasAnomaly");
+ p.Add("HasAnomaly", query.HasAnomaly);
+ }
+
+ var whereSql = string.Join(" AND ", whereParts);
+
+ var total = conn.ExecuteScalar(
+ $"SELECT COUNT(1) FROM log_collect_cycle c WHERE {whereSql}", p);
+
+ var dataSql = $@"SELECT
+ c.id AS Id,
+ DATE_FORMAT(c.cycle_time, '%Y-%m-%d %H:%i:%s') AS CycleTime,
+ c.collect_address_id AS CollectAddressId,
+ ca.address_name AS AddressName,
+ c.total_machines AS TotalMachines,
+ c.success_count AS SuccessCount,
+ c.fail_count AS FailCount,
+ c.has_anomaly AS HasAnomaly,
+ c.change_distribution AS ChangeDistribution,
+ c.cycle_summary AS CycleSummary
+ FROM log_collect_cycle c
+ LEFT JOIN cnc_business.cnc_collect_address ca ON c.collect_address_id = ca.id
+ WHERE {whereSql}
+ ORDER BY c.cycle_time DESC
+ LIMIT @PageSize OFFSET @Offset";
+
+ var items = conn.Query(dataSql,
+ new { PageSize = query.PageSize, Offset = query.Offset }).AsList();
+
+ return new PagedResult
+ {
+ Items = items,
+ Total = total,
+ Page = query.Page,
+ PageSize = query.PageSize
+ };
+ }
+ }
+
+ public long Create(CollectCycle entity)
+ {
+ using (var conn = CreateConnection())
+ {
+ var sql = @"INSERT INTO log_collect_cycle
+ (cycle_time, collect_address_id, raw_log_id, end_time, duration_ms,
+ total_machines, success_count, fail_count, change_distribution,
+ has_anomaly, cycle_summary, created_at)
+ VALUES (@CycleTime, @CollectAddressId, @RawLogId, @EndTime, @DurationMs,
+ @TotalMachines, @SuccessCount, @FailCount, @ChangeDistribution,
+ @HasAnomaly, @CycleSummary, NOW());
+ SELECT LAST_INSERT_ID();";
+ return conn.ExecuteScalar(sql, new
+ {
+ entity.CycleTime,
+ entity.CollectAddressId,
+ entity.RawLogId,
+ entity.EndTime,
+ entity.DurationMs,
+ entity.TotalMachines,
+ entity.SuccessCount,
+ entity.FailCount,
+ entity.ChangeDistribution,
+ entity.HasAnomaly,
+ entity.CycleSummary
+ });
+ }
+ }
+
+ public int DeleteBeforeDate(DateTime date)
+ {
+ using (var conn = CreateConnection())
+ {
+ return conn.Execute(
+ "DELETE FROM log_collect_cycle WHERE cycle_time < @Date",
+ new { Date = date });
+ }
+ }
+ }
+}
diff --git a/src/CncRepository/Interface/ICollectAnalysisRepository.cs b/src/CncRepository/Interface/ICollectAnalysisRepository.cs
new file mode 100644
index 0000000..42b6086
--- /dev/null
+++ b/src/CncRepository/Interface/ICollectAnalysisRepository.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Collections.Generic;
+using CncModels.Dto;
+using CncModels.Dto.CollectLog;
+using CncModels.Entity;
+
+namespace CncRepository.Interface
+{
+ ///
+ /// 采集分析仓储接口
+ ///
+ public interface ICollectAnalysisRepository
+ {
+ PagedResult GetAnalysisList(CollectAnalysisQuery query);
+ CollectAnalysisDetail GetAnalysisDetail(long id);
+ List GetAnalysisByRawLogId(long rawLogId);
+ long Create(CollectAnalysis entity);
+ int DeleteBeforeDate(DateTime date);
+ }
+}
diff --git a/src/CncRepository/Interface/ICollectCycleRepository.cs b/src/CncRepository/Interface/ICollectCycleRepository.cs
new file mode 100644
index 0000000..8c5509f
--- /dev/null
+++ b/src/CncRepository/Interface/ICollectCycleRepository.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Collections.Generic;
+using CncModels.Dto;
+using CncModels.Dto.CollectLog;
+using CncModels.Entity;
+
+namespace CncRepository.Interface
+{
+ ///
+ /// 采集周期仓储接口
+ ///
+ public interface ICollectCycleRepository
+ {
+ PagedResult GetCycleList(CollectCycleQuery query);
+ long Create(CollectCycle entity);
+ int DeleteBeforeDate(DateTime date);
+ }
+}
diff --git a/src/CncService/Impl/CollectLogService.cs b/src/CncService/Impl/CollectLogService.cs
index 60817fd..33a9f91 100644
--- a/src/CncService/Impl/CollectLogService.cs
+++ b/src/CncService/Impl/CollectLogService.cs
@@ -8,7 +8,9 @@ using CncRepository.Interface;
namespace CncService.Impl
{
- // 采集日志相关的业务实现
+ ///
+ /// 采集日志相关的业务实现
+ ///
public class CollectLogService : ICollectLogService
{
private readonly ICollectAnalysisRepository _analysisRepository;
diff --git a/src/CncService/Interface/ICollectLogService.cs b/src/CncService/Interface/ICollectLogService.cs
index a83a50a..3de1c6a 100644
--- a/src/CncService/Interface/ICollectLogService.cs
+++ b/src/CncService/Interface/ICollectLogService.cs
@@ -6,16 +6,16 @@ namespace CncService.Interface
{
public interface ICollectLogService
{
- // 分页查询采集分析日志
+ /// 分页查询采集分析日志
PagedResult GetAnalysisList(CollectAnalysisQuery query);
- // 获取单条采集分析日志的详情
+ /// 获取单条采集分析日志的详情
CollectAnalysisDetail GetAnalysisDetail(long id);
- // 根据原始日志ID查找相关联的分析记录
+ /// 根据原始日志ID查找相关联的分析记录
List GetAnalysisByRawLogId(long rawLogId);
- // 分页查询采集周期信息
+ /// 分页查询采集周期信息
PagedResult GetCycleList(CollectCycleQuery query);
}
}
diff --git a/src/CncWebApi/Controllers/CollectLogController.cs b/src/CncWebApi/Controllers/CollectLogController.cs
index 0f4d401..7ab02e0 100644
--- a/src/CncWebApi/Controllers/CollectLogController.cs
+++ b/src/CncWebApi/Controllers/CollectLogController.cs
@@ -3,12 +3,17 @@ using System.Collections.Generic;
using System.Web.Http;
using CncModels.Dto;
using CncModels.Dto.CollectLog;
+using CncModels.Entity;
using CncService.Interface;
using CncRepository.Interface;
+using CncWebApi.Infrastructure;
using System.Web.Http.Description;
namespace CncWebApi.Controllers
{
+ ///
+ /// 采集日志管理控制器
+ ///
[RoutePrefix("api/admin/collect-log")]
[JwtAuthFilter]
public class CollectLogController : ApiController
@@ -22,19 +27,20 @@ namespace CncWebApi.Controllers
_rawRepository = rawRepository ?? throw new ArgumentNullException(nameof(rawRepository));
}
- // GET api/admin/collect-log/analysis
+ /// 分页查询采集分析记录
[HttpGet]
[Route("analysis")]
[ResponseType(typeof(ApiResponse>))]
public IHttpActionResult GetAnalysisList([FromUri] CollectAnalysisQuery query)
{
+ if (query == null) query = new CollectAnalysisQuery();
var result = _collectLogService.GetAnalysisList(query);
return Ok(ApiResponse>.Success(result));
}
- // GET api/admin/collect-log/analysis/{id}
+ /// 获取采集分析详情
[HttpGet]
- [Route("analysis/{id}")]
+ [Route("analysis/{id:long}")]
[ResponseType(typeof(ApiResponse))]
public IHttpActionResult GetAnalysisDetail(long id)
{
@@ -42,9 +48,9 @@ namespace CncWebApi.Controllers
return Ok(ApiResponse.Success(detail));
}
- // GET api/admin/collect-log/analysis/by-raw/{rawLogId}
+ /// 根据原始日志ID查询关联的分析记录
[HttpGet]
- [Route("analysis/by-raw/{rawLogId}")]
+ [Route("analysis/by-raw/{rawLogId:long}")]
[ResponseType(typeof(ApiResponse>))]
public IHttpActionResult GetAnalysisByRawLogId(long rawLogId)
{
@@ -52,23 +58,23 @@ namespace CncWebApi.Controllers
return Ok(ApiResponse>.Success(list));
}
- // GET api/admin/collect-log/cycle
+ /// 分页查询采集周期
[HttpGet]
[Route("cycle")]
[ResponseType(typeof(ApiResponse>))]
public IHttpActionResult GetCycleList([FromUri] CollectCycleQuery query)
{
+ if (query == null) query = new CollectCycleQuery();
var result = _collectLogService.GetCycleList(query);
return Ok(ApiResponse>.Success(result));
}
- // GET api/admin/collect-log/raw
+ /// 查询原始采集日志
[HttpGet]
[Route("raw")]
[ResponseType(typeof(ApiResponse>))]
- public IHttpActionResult GetRaw([FromUri] int? collectAddressId, [FromUri] int page = 1, [FromUri] int pageSize = 20, [FromUri] string startDate = null, [FromUri] string endDate = null, [FromUri] bool? isSuccess = null)
+ public IHttpActionResult GetRawList([FromUri] int? collectAddressId, [FromUri] int page = 1, [FromUri] int pageSize = 20)
{
- // 通过 ICollectRawRepository 进行分页查询,具体筛选条件以仓储实现为准
var result = _rawRepository.GetByAddressId(collectAddressId ?? 0, page, pageSize);
return Ok(ApiResponse>.Success(result));
}
diff --git a/src/CncWebApi/Infrastructure/ServiceResolver.cs b/src/CncWebApi/Infrastructure/ServiceResolver.cs
index 25f4551..52cc095 100644
--- a/src/CncWebApi/Infrastructure/ServiceResolver.cs
+++ b/src/CncWebApi/Infrastructure/ServiceResolver.cs
@@ -78,6 +78,10 @@ namespace CncWebApi.Infrastructure
ResolveCollectAddressService());
if (serviceType == typeof(Controllers.HealthController))
return new Controllers.HealthController();
+ if (serviceType == typeof(Controllers.CollectLogController))
+ return new Controllers.CollectLogController(
+ ResolveCollectLogService(),
+ new CncRepository.Impl.Log.CollectRawRepository(_logConn));
return null;
}
@@ -184,6 +188,13 @@ namespace CncWebApi.Infrastructure
return new CncRepository.Impl.ProductionAdjustmentRepository(_businessConn);
}
+ private ICollectLogService ResolveCollectLogService()
+ {
+ return new CncService.Impl.CollectLogService(
+ new CncRepository.Impl.Log.CollectAnalysisRepository(_logConn),
+ new CncRepository.Impl.Log.CollectCycleRepository(_logConn));
+ }
+
#endregion
}
}
diff --git a/tests/CncCollector.Tests/CollectWorkerTests.cs b/tests/CncCollector.Tests/CollectWorkerTests.cs
index be0a12c..b88b546 100644
--- a/tests/CncCollector.Tests/CollectWorkerTests.cs
+++ b/tests/CncCollector.Tests/CollectWorkerTests.cs
@@ -39,8 +39,8 @@ namespace CncCollector.Tests
string businessConn = "Server=.;Database=NonExistingDb;User Id=invalid;Password=invalid;";
string logConn = "Server=.;Database=NonExistingLogDb;User Id=invalid;Password=invalid;";
- // 允许 tracker 为 null,因为在无效 URL 的测试路径下通常不会进入需要 Tracker 的分支
- _worker = new CollectWorker(_address, _config, null, businessConn, logConn);
+ // 允许 tracker 和 analysisEngine 为 null,因为在无效 URL 的测试路径下通常不会进入需要它们的分支
+ _worker = new CollectWorker(_address, _config, null, null, businessConn, logConn);
}
public void Dispose()