# CNC机床数据采集系统 - 采集服务设计文档 > 最后更新:2026-04-30 > 状态:设计中 --- ## 一、概述 采集服务(CncCollector)是一个独立 Windows Service,负责定时从采集地址HTTP拉取JSON数据,通过品牌字段映射解析为结构化数据,实时更新机床状态,追踪产量分段,并执行日终汇总。 ### 1.1 核心职责 | 职责 | 说明 | |------|------| | HTTP数据拉取 | 按采集地址配置的间隔,定时GET拉取JSON | | 字段映射解析 | 用品牌模板的映射规则,从JSON中提取标准字段 | | 实时状态更新 | 更新机床表的在线状态、采集状态、程序名、零件数等 | | 产量分段追踪 | 检测NC程序名切换、零件数手动清零,自动结账/开段 | | 日终汇总 | 按自然天汇总产量到机床+程序级别、工人级别 | | 原始数据存档 | 存原始JSON到日志库,支持事后重解析 | | 心跳上报 | 定时写心跳记录,供管理后台判断服务是否在线 | | 告警生成 | 采集失败、未知设备、字段缺失等异常生成告警记录 | ### 1.2 技术栈 | 项 | 选型 | 说明 | |----|------|------| | 框架 | .NET Framework 4.7.2 Windows Service | 与Web API同框架 | | ORM | Dapper | 复用现有CncRepository层 | | HTTP客户端 | System.Net.Http.HttpClient | 拉取采集地址JSON | | JSON解析 | Newtonsoft.Json 12.0.3 | 与Web API版本一致 | | 日志 | log4net | 文件日志 + DB日志双写 | | 数据库 | MariaDB | 业务库 + 日志库双库 | | 管理API | HttpListener(轻量自托管) | 5800端口,独立于IIS | ### 1.3 与现有系统的关系 ``` ┌─────────────────────────────────────────────────┐ │ Windows Server │ │ │ │ ┌──────────────┐ ┌───────────────────────┐ │ │ │ IIS │ │ CncCollector │ │ │ │ CncWebApi │ │ (Windows Service) │ │ │ │ :80 │ │ 管理API :5800 │ │ │ │ 管理后台/大屏 │ │ 采集循环+Ping+汇总 │ │ │ └──────┬───────┘ └───────────┬───────────┘ │ │ │ │ │ │ └──────────┬─────────────┘ │ │ ▼ │ │ ┌────────────────────┐ │ │ │ MariaDB │ │ │ │ cnc_business │ │ │ │ cnc_log │ │ │ └────────────────────┘ │ └─────────────────────────────────────────────────┘ ``` - CncCollector 复用现有的 `CncModels`、`CncRepository` 层(直接引用项目) - 不复用 `CncService` 层(采集服务有独立的业务逻辑,不依赖Web API的Service) - 通过数据库共享数据,不直接调用Web API --- ## 二、项目结构 ### 2.1 新建项目 在解决方案中新增 `CncCollector` 项目: ``` src/CncCollector/ ├── CncCollector.csproj ← .NET Framework 4.7.2 类库 ├── ServiceEntry.cs ← Windows Service 入口(ServiceBase子类) ├── ServiceInstaller.cs ← 安装器(Installer子类,installutil用) ├── Program.cs ← 主入口(Main) │ ├── Engine/ │ ├── CollectorEngine.cs ← 引擎主控(启停所有任务、生命周期管理) │ ├── CollectorTask.cs ← 单个采集地址的采集循环 │ └── PingTask.cs ← 单个采集地址的Ping循环 │ ├── Parser/ │ ├── FieldMapper.cs ← 字段映射解析器(纯函数,输入JSON+映射规则→标准字段字典) │ └── ValueConverter.cs ← 数值转换(去.00000尾缀,string/number类型转换) │ ├── Tracker/ │ └── ProductionTracker.cs ← 产量分段追踪(检测程序切换/手动清零→结账/开段) │ ├── Jobs/ │ └── DailySummaryJob.cs ← 日终汇总任务(关闭活跃段→合并产量→工人汇总→日状态) │ ├── Config/ │ ├── ConfigProvider.cs ← 配置热更新(DB轮询+HTTP通知双保险) │ └── CollectorConfig.cs ← 配置快照数据结构 │ ├── Heartbeat/ │ └── HeartbeatReporter.cs ← 心跳上报(定时写log_collector_heartbeat) │ ├── Api/ │ ├── ManagementServer.cs ← 轻量HTTP管理API(HttpListener自托管) │ └── ManagementController.cs ← API端点实现(状态/重载/启停/汇总) │ ├── Logging/ │ └── DualLogger.cs ← 双写日志(文件log4net + DB log_system) │ └── App.config ← 连接串、端口、API Key、log4net配置 ``` ### 2.2 项目引用 ``` CncCollector → CncRepository → CncModels ↘ (不引用 CncService、CncWebApi) ``` CncCollector 直接使用 Repository 层操作数据库,不经过 Service 层。原因: - 采集服务有大量独立的业务逻辑(字段映射、产量分段、汇总计算),与Web API的CRUD Service职责不同 - 避免与Web API产生耦合(Service层可能被修改影响采集服务) --- ## 三、核心流程设计 ### 3.1 采集地址的数据模型 ``` 采集地址(cnc_collect_address) ├── URL:HTTP端点地址(如 http://192.168.1.101/) ├── 品牌:决定字段映射规则 ├── 采集间隔:本地址独立配置 ├── 关联机床:该地址下要采的机床列表 └── 启停状态:is_enabled 一次HTTP GET 返回: [ ← JSON数组 { device:"fanake_1.8", desc:"西-1.8", tags:[{id:"Tag5",value:"1566.NC"},...] }, { device:"fanake_1.9", desc:"西-1.9", tags:[{id:"Tag5",value:"O1"},...] }, ... ] device值 匹配 cnc_machine.device_code → 定位具体机床 tags数组 通过品牌字段映射规则 → 提取标准字段 ``` ### 3.2 双循环模型(Ping + 采集) 每个启用的采集地址启动两个独立的 Task: #### 3.2.1 Ping循环 ``` PingTask(address): while(服务运行中 && address启用): try: success = Ping(address.Url) // ICMP Ping采集地址 catch: success = false 更新该地址下所有机床: is_online = success ? 1 : 0 last_ping_time = NOW() sleep(ping_interval) // 全局配置,默认60秒 ``` **关键点**: - Ping结果仅用于管理后台/大屏显示机床在线状态 - Ping不通**不影响**采集逻辑——采集循环照常HTTP拉取 - 机床表的 `ip_address` 字段不参与Ping逻辑,仅信息记录 - Ping是针对采集地址URL(HTTP端点),不是单台机床 #### 3.2.2 采集循环 ``` CollectTask(address): while(服务运行中 && address启用): roundStart = NOW() // 检查重载/停止请求(本轮开始前) if(停止请求): 安全退出 if(重载请求): 执行重载后继续 try: // 1. HTTP拉取 responseJson, duration = HTTP GET address.Url // 2. 原始JSON存档(日志库,先于解析,防止崩溃丢数据) 写入 log_collect_raw(is_success=1, raw_json=responseJson) // 3. 按device逐台解析 devices = 解析JSON数组 for each device in devices: 处理单个设备(device, address) // 4. 更新采集地址状态 address.last_collect_time = NOW() address.last_collect_status = "success" address.fail_count = 0 catch(异常 ex): 处理采集失败(address, ex) // 本轮完成,检查挂起的停止/重载请求 if(停止请求): 安全退出 if(重载请求): 执行重载 // 计算本轮耗时,sleep剩余间隔 elapsed = NOW() - roundStart sleep(max(0, collect_interval - elapsed)) ``` **关键点**: - 原始JSON先写入日志库再解析,防止解析过程中服务崩溃导致数据丢失 - 每轮采集结束才检查停止/重载请求,保证不会中断中间状态 - 采集间隔扣除本轮耗时,保证频率稳定 ### 3.3 处理单个设备 ``` 处理单个设备(deviceObj, address): // 1. 匹配机床 deviceCode = deviceObj[brand.deviceField] // 默认取 device 字段 machine = 按 device_code 查机床表 if(machine == null): // 未知设备 → 告警(带完整JSON) 创建告警(alert_type="unknown_device", detail=deviceObj完整JSON) return if(machine.is_enabled == 0): return // 已停用的机床跳过 // 2. 字段映射解析 fields = FieldMapper.Map(deviceObj, address.字段映射规则) // fields = { program_name:"1566.NC", part_count:1219, device_status:1, ... } // 3. 处理缺失字段 missingRequired = 检查必填字段缺失情况 if(missingRequired.Count > 0): 创建告警(alert_type="collect_fail", machine_id=machine.id, detail="缺失字段: " + string.Join(", ", missingRequired)) // 缺失字段写null,不跳过整条记录 // 4. 写结构化记录(业务库 cnc_collect_record) 写入 CollectRecord(machine_id, collect_time=NOW(), fields) // 5. 更新机床实时状态 更新 cnc_machine: last_collect_time = NOW() last_program_name = fields.program_name last_part_count = fields.part_count last_device_status = fields.device_status last_run_status = fields.run_status last_operate_mode = fields.operate_mode last_machining_status = fields.machining_status // 6. 产量分段追踪 ProductionTracker.Track(machine.id, fields.program_name, fields.part_count) ``` ### 3.4 字段映射解析器 #### 输入 ``` deviceObj = { "device": "fanake_1.8", "desc": "西-1.8", "tags": [...] } mappings = [ { standard_field: "program_name", field_name: "Tag5", match_by: "id", data_type: "string" }, { standard_field: "part_count", field_name: "Tag8", match_by: "id", data_type: "number" }, { standard_field: "device_status",field_name: "_io_status", match_by: "id", data_type: "number" }, ... ] ``` #### 解析逻辑 ``` FieldMapper.Map(deviceObj, mappings) → Dictionary 对每条映射规则: 1. 在 deviceObj.tags 数组中查找匹配: match_by == "id" → 找 tag.id == field_name match_by == "desc" → 找 tag.desc == field_name 2. 找到则提取 tag.value: data_type == "number" → 去除".00000"尾缀,转decimal data_type == "string" → 直接取字符串 3. 未找到: 结果为 null if(is_required == 1) → 加入缺失列表 ``` #### 数值转换规则 ``` ValueConverter.Convert(rawValue, dataType): if(dataType == "number"): // "1219.00000" → 1219 // "0.00000" → 0 // "1566.NC" → null(非数字,不抛异常) str = rawValue.Trim() if(decimal.TryParse(str, out result)): return result else: return null if(dataType == "string"): return rawValue ``` ### 3.5 产量分段追踪 #### 核心状态机 ``` 每台机床的产量追踪状态 = cnc_production_segment 表中的一条活跃记录(is_settled=0, end_time=NULL) 触发结账的事件: 1. NC程序名变化 → close_reason = "program_change" 2. 同程序下part_count下降(手动清零) → close_reason = "manual_reset" 3. 日终汇总时仍在运行 → close_reason = "end_of_day" 4. 服务停止/崩溃恢复 → close_reason = "service_stop" / "service_crash" ``` #### Track逻辑伪代码 ``` ProductionTracker.Track(machineId, currentProgramName, currentPartCount): activeSegment = repository.GetActiveSegment(machineId) if(activeSegment == null): // 无活跃段 → 开新段 CreateSegment(machineId, currentProgramName, currentPartCount) return if(activeSegment.program_name != currentProgramName): // NC程序切换 → 结账旧段,开新段 quantity = activeSegment.start_part_count 不变(上一段的part_count快照) // 实际产量 = currentPartCount - activeSegment.start_part_count // 但注意:旧的activeSegment.start_part_count是旧程序开始时的值 CloseSegment(activeSegment.id, endPartCount = currentPartCount, quantity = currentPartCount - activeSegment.start_part_count, closeReason = "program_change") CreateSegment(machineId, currentProgramName, currentPartCount) return if(currentPartCount < activeSegment.start_part_count): // 同程序下part_count下降 → 手动清零 // 结账旧段(产量 = 清零前的值 - 段起始值) CloseSegment(activeSegment.id, endPartCount = 上一次采集的part_count(需要内存缓存), quantity = 上一次采集的part_count - activeSegment.start_part_count, closeReason = "manual_reset") // 开新段,从当前值(清零后的值)开始 CreateSegment(machineId, currentProgramName, currentPartCount) return // 正常运行 → 无需操作,活跃段继续 // 可选:更新内存中"上一次采集的part_count"缓存 ``` #### part_count缓存 为了检测手动清零,需要知道**上一次采集的part_count**。在内存中维护: ``` Dictionary _lastPartCountByMachine // machineId → 上次part_count 每次采集后更新: _lastPartCountByMachine[machineId] = currentPartCount 手动清零判断条件: currentPartCount < activeSegment.start_part_count (零件数比段起始值还小,说明被手动清零了) ``` #### A-B-C-A-B场景 ``` 时间线: 10:00 program=A, part=0 → 开段(A, start=0) 10:30 program=A, part=10 → 正常 11:00 program=B, part=0 → 结段(A, qty=10), 开段(B, start=0) 11:30 program=C, part=0 → 结段(B, qty=0), 开段(C, start=0) 12:00 program=A, part=0 → 结段(C, qty=0), 开段(A, start=0) ← 新段,不合并 12:30 program=A, part=5 → 正常 日汇总时: 按 (machine_id, production_date, program_name) 合并 program=A 的总产量 = 10 + 5 = 15 program=B 的总产量 = 0 program=C 的总产量 = 0 ``` --- ## 四、采集失败与重试 ### 4.1 重试机制 ``` 一轮采集循环中的重试: 第1次采集 → 失败 sleep(retry_interval) // 默认30秒,后台可配 第2次采集 → 失败 sleep(retry_interval) 第3次采集 → 失败(达到 retry_count 上限,默认3次,后台可配) 重试总耗时 = retry_count × retry_interval if(重试总耗时 < collect_interval): // 正常情况:重试耗时不影响下一轮 fail_count++ sleep(collect_interval - 重试总耗时) // 等待剩余间隔 else: // 异常情况:重试耗时超过了采集间隔 // 重置本轮错误状态(不算一次有效失败) // 直接进入下一轮采集 不增加 fail_count 立即开始下一轮 if(fail_count >= collect_fail_alert_threshold): // 默认5次 创建告警(alert_type="collect_fail") fail_count = 0 // 重置,避免重复告警 ``` ### 4.2 配置约束 | 配置项 | 默认值 | 说明 | |--------|--------|------| | `collect_retry_count` | 3 | 每轮最大重试次数 | | `collect_retry_interval` | 30 | 重试间隔(秒) | | `collect_fail_alert_threshold` | 5 | 连续失败N轮触发告警 | **约束**:正常配置下 `retry_count × retry_interval < collect_interval`。如果不满足,重试超时不算失败。 ### 4.3 fail_count 连续性 - 采集成功一次 → `fail_count` 立即清零 - 只有**连续**失败才累积 - 达到阈值触发告警后清零,避免同一问题重复告警 --- ## 五、配置热更新 ### 5.1 双保险机制 ``` 管理后台修改 → 写DB → HTTP通知采集服务 → 采集服务验证生效 ┌─── 即时通知(HTTP POST /api/reload)───┐ │ │ 管理后台写DB ──┤ ├──→ 采集服务重载配置 │ │ └─── 兜底轮询(每30秒查DB)─────────────────┘ ``` ### 5.2 重载流程 ``` 1. 管理后台修改采集地址/品牌模板 → 写入数据库 2. 管理后台调用采集服务API: POST http://localhost:5800/api/reload Headers: X-Api-Key: {collector_api_key} 3. 采集服务收到重载请求: a. 标记"重载请求挂起",不立即中断 b. 等待所有地址当前轮采集完成 c. 从DB重新加载配置: - cnc_collect_address(启用的地址列表) - cnc_brand_field_mapping(字段映射规则) - cnc_sys_config(系统配置) d. 对比新旧配置,生成变更摘要 e. 调整运行中的任务: - 新增的地址 → 启动新的Ping+采集Task - 停用的地址 → 停止对应Task - 变更的地址 → 停止旧Task,启动新Task f. 返回变更结果 4. 管理后台验证: GET http://localhost:5800/api/config/hash 对比本地DB配置的hash → 一致则确认生效 ``` ### 5.3 配置快照 内存中维护的配置数据结构: ```csharp class CollectorConfig { string ConfigHash; // MD5指纹,快速判断是否变更 List Addresses; // 启用的采集地址列表 Dictionary> BrandMappings; // brandId → 字段映射 Dictionary SysConfig; // 系统配置键值对 DateTime LoadedAt; // 加载时间 } ``` ### 5.4 管理后台交互 管理后台修改采集地址/品牌模板后: - 保存按钮旁显示"已通知采集服务重载 ✓"或"通知失败,将在30秒后自动同步 ⚠" - 通过Web API后端转发HTTP请求到采集服务(前端不直连5800端口) --- ## 六、日终汇总 ### 6.1 触发机制(启动时补检 + 内置定时器) ``` DailySummaryJob: // 启动时自检 OnStart(): yesterday = DateTime.Today.AddDays(-1) if(昨天尚未汇总): 立即执行昨天的汇总 // 定时检查(每分钟) Timer(60秒): now = DateTime.Now targetDate = DateTime.Today.AddDays(-1) // 汇总昨天 if(now >= 配置的daily_summary_time): // 默认01:00 if(今天尚未执行过汇总): 执行汇总(targetDate) 记录"今天已汇总" // HTTP API 手动触发 OnManualTrigger(date): 执行汇总(date) // 幂等:先删旧数据再重算 ``` **优势**: - 服务01:00挂了,02:00恢复后自动补上 - 手动触发支持重算任意日期 - 幂等设计:同一天汇总多次不会重复 ### 6.2 汇总步骤 ``` DailySummaryJob.Execute(targetDate): for each 启用的机床: // Step 1: 关闭该机床的所有活跃段 activeSegments = 查询(machine_id, is_settled=0, end_time=NULL) for each segment in activeSegments: CloseSegment(segment.id, endPartCount = lastPartCount缓存或段内最大part_count, quantity = endPartCount - segment.start_part_count, closeReason = "end_of_day") // Step 2: 标记结算 SettleByDate(targetDate) // Step 3: 按程序合并 → cnc_daily_production segments = 查询该机床targetDate的所有段 grouped = segments.GroupBy(s => s.program_name) for each group: UpsertDailyProduction( machine_id, targetDate, program_name, total_quantity = Sum(group.quantity), segment_count = group.Count() ) // Step 4: 写机床日状态 UpsertMachineDailyStatus( machine_id, targetDate, data_status = 根据当天采集情况判断: "normal" → 至少成功采集过一次 "offline" → 当天从未Ping通 "data_missing" → Ping通但采集始终失败 ) // Step 5: 工人汇总 → cnc_worker_daily_summary for each worker: machines = 该工人绑定的机床列表 workerProductions = 查这些机床在targetDate的daily_production UpsertWorkerDailySummary( worker_id, targetDate, total_quantity = Sum(workerProductions.total_quantity), machine_count = 去重机床数, program_count = 去重程序名数 ) // Step 6: 未开机机床插0产量记录 allEnabledMachines = 所有启用的机床 machinesWithData = 当天有daily_production记录的机床 for each machine in (allEnabledMachines - machinesWithData): InsertDailyProduction(machine.id, targetDate, quantity=0) UpsertMachineDailyStatus(machine.id, targetDate, "offline") ``` --- ## 七、心跳上报 ### 7.1 心跳机制 ``` HeartbeatReporter: 每 heartbeat_interval 秒(默认10秒,可配): 写入 log_collector_heartbeat: service_id = "CncCollector" status = "running" collect_address_id = null(全局心跳) last_collect_time = 最近一次成功采集时间 success_count = 本轮成功采集次数 fail_count = 本轮失败采集次数 uptime_seconds = (NOW() - 服务启动时间).TotalSeconds detail = JSON.stringify({ activeTasks: 活跃采集任务数, activePings: 活跃Ping任务数, configHash: 当前配置指纹 }) ``` ### 7.2 Web API判断服务状态 ```sql -- 管理后台/大屏查询采集服务是否在线 SELECT * FROM log_collector_heartbeat WHERE service_id = 'CncCollector' AND created_at > NOW() - INTERVAL 30 SECOND ORDER BY created_at DESC LIMIT 1 -- 有记录 → 运行中;无记录 → 已停止 ``` --- ## 八、日志系统 ### 8.1 双写策略 | 事件类型 | 文件日志 | DB日志(log_system) | |----------|---------|-------------------| | 每次采集成功 | ✅ INFO | ❌ 不写(量太大) | | 采集失败/重试 | ✅ ERROR | ✅ ERROR | | 字段缺失告警 | ✅ WARN | ❌(写入cnc_alert) | | 未知设备告警 | ✅ WARN | ❌(写入cnc_alert) | | 配置变更/重载 | ✅ INFO | ✅ INFO | | 服务启停 | ✅ INFO | ✅ INFO | | 日终汇总 | ✅ INFO | ✅ INFO | | 异常崩溃 | ✅ FATAL | ✅ ERROR(如果还能写的话) | | Ping状态变化 | ✅ DEBUG | ❌ | ### 8.2 文件日志格式 ``` logs/collector-2026-04-30.log [2026-04-30 01:00:02 INFO ] 日终汇总开始,目标日期:2026-04-29 [2026-04-30 01:00:05 INFO ] 日终汇总完成,处理160台机床,耗时3.2秒 [2026-04-30 01:00:30 INFO ] 地址[FANUC-1号] 采集成功,获取24台设备数据,耗时120ms [2026-04-30 01:01:00 WARN ] 机床[西-1.8](id=5) 必填字段缺失: spindle_speed_set [2026-04-30 01:02:00 ERROR] 地址[FANUC-2号] 采集失败(第3次重试): 连接超时 [2026-04-30 01:02:05 INFO ] 配置重载完成:新增地址1个,停用地址1个,更新映射3条 [2026-04-30 01:02:30 WARN ] 未知设备 device=fanake_1.15,采集地址[FANUC-1号] ``` ### 8.3 log4net配置要点 ```xml ``` --- ## 九、优雅停止与异常恢复 ### 9.1 优雅停止 ``` 核心原则:收到停止请求后,等待所有地址当前轮采集完成再退出 CollectorEngine.Stop(): 1. 标记 _stopRequested = true 2. 日志记录"收到停止请求,等待当前轮采集完成" 3. 等待所有CollectorTask和PingTask完成(带超时) Task.WaitAll(allTasks, timeout: 30秒) 4. 关闭所有活跃的产量分段: for each 活跃段: CloseSegment(reason="service_stop") 5. 写最终心跳(status="stopped") 6. 日志记录"服务已停止" 7. 释放资源 ``` ### 9.2 配置重载时的保护 ``` CollectorEngine.Reload(): 1. 标记 _reloadRequested = true 2. 日志记录"收到重载请求,等待当前轮采集完成" 3. 不中断任何正在执行的采集任务 4. 等待所有任务到达本轮结束点(循环顶部检查标记) 5. 从DB重新加载配置 6. 对比新旧配置,生成变更摘要 7. 停掉不再需要的任务 8. 启动新任务 9. 清除 _reloadRequested 标记 10. 返回变更摘要 ``` ### 9.3 异常停止(崩溃/强杀)的数据防护 #### 产量分段——天然事务边界 ``` 采集服务对产量分段的操作只有两种原子动作: 1. 开新段:INSERT → end_time=NULL 2. 结旧段:UPDATE → 设置end_time 最坏情况:结旧段成功但开新段失败 后果:机床没有活跃段 恢复:下次服务启动时自检恢复(见下) 数据丢失:最多丢失一次采集的产量,不会出现脏数据 ``` #### 服务启动时自检恢复 ``` CollectorEngine.OnStart(): 1. 加载配置 2. 自检恢复: activeSegments = 查询所有 is_settled=0 AND end_time=NULL 的段 for each segment: if(segment.start_time 距今 > 2小时): // 很可能是崩溃遗留的孤儿段 CloseSegment(segment.id, endPartCount = segment.start_part_count, quantity = 0, closeReason = "service_crash", endTime = segment.start_time.AddMinutes(5)) 3. 启动Ping+采集任务 4. 自检日终汇总 ``` #### 原始JSON先写后解析 ``` 采集流程中的写入顺序: 第1步:写入 log_collect_raw(原始JSON + is_success=1) ← 先保存 第2步:逐台解析 + 写 cnc_collect_record ← 后处理 如果第2步中途崩溃: - 原始JSON已安全保存 - 可通过管理API手动触发"重解析":按log_collect_raw的记录重新跑字段映射 ``` #### 采集记录——天然原子性 ``` 每台设备的结构化数据是一条独立的 INSERT 即使中途崩溃,已写入的记录是完整的 未写入的记录只是缺失,下次采集会补上 ``` --- ## 十、管理API ### 10.1 端点列表 | 方法 | URL | 说明 | 认证 | |------|-----|------|------| | GET | /api/status | 服务状态(运行时长、任务数、各地址状态) | X-Api-Key | | POST | /api/reload | 立即重载配置(等本轮完成后执行) | X-Api-Key | | POST | /api/stop | 停止所有采集任务 | X-Api-Key | | POST | /api/start | 启动所有采集任务 | X-Api-Key | | GET | /api/tasks | 当前运行的任务列表 | X-Api-Key | | GET | /api/config/snapshot | 当前生效的配置快照 | X-Api-Key | | GET | /api/config/hash | 配置指纹(MD5) | X-Api-Key | | POST | /api/daily-summary | 手动触发日终汇总,参数: date=2026-04-29 | X-Api-Key | | POST | /api/reparse | 重解析指定原始记录,参数: rawId=12345 | X-Api-Key | ### 10.2 认证 所有管理API请求需携带 Header `X-Api-Key`,值与 `cnc_sys_config` 表中的 `collector_api_key` 一致。 ### 10.3 实现方式 使用 `System.Net.HttpListener` 自托管,独立于IIS: ```csharp // ManagementServer.cs HttpListener listener = new HttpListener(); listener.Prefixes.Add("http://+:{port}/api/"); listener.Start(); // 请求处理循环 while(running) { var context = listener.GetContext(); // 路由到 ManagementController 对应方法 } ``` ### 10.4 响应格式 ```json { "code": 0, "message": "success", "data": { ... } } ``` 与Web API统一格式。 --- ## 十一、告警类型汇总 | alert_type | 触发条件 | detail内容 | |------------|---------|------------| | `collect_fail` | 连续N轮采集失败 | 失败原因摘要 | | `collect_fail` | 必填字段缺失 | 缺失字段列表 | | `unknown_device` | device匹配不到机床表 | 完整device JSON对象 | | `device_offline` | (预留,当前Ping状态直接更新机床表) | - | | `service_error` | 采集服务内部异常 | 异常消息+堆栈 | ### 告警频率控制 - 同一台机床同一个问题,5分钟内不重复告警 - 未知设备告警:同一device编码,1小时内不重复 - 告警表不记录采集成功的正常事件 --- ## 十二、App.config 配置项 ```xml ``` --- ## 十三、数据库交互清单 ### 13.1 采集服务读取的表 | 表 | 库 | 用途 | 频率 | |----|-----|------|------| | cnc_collect_address | 业务 | 加载启用的采集地址列表 | 每30秒/重载时 | | cnc_brand_field_mapping | 业务 | 加载品牌字段映射规则 | 每30秒/重载时 | | cnc_brand | 业务 | 获取品牌device_field/tags_path | 每30秒/重载时 | | cnc_machine | 业务 | 按device_code匹配机床 | 每次采集 | | cnc_production_segment | 业务 | 查活跃段 | 每次采集每台机床 | | cnc_worker_machine | 业务 | 查机床绑定的工人(日汇总用) | 日汇总时 | | cnc_sys_config | 业务 | 读取系统配置 | 每30秒/重载时 | | log_collect_raw | 日志 | 读取原始记录(重解析用) | 按需 | ### 13.2 采集服务写入的表 | 表 | 库 | 用途 | 频率 | |----|-----|------|------| | log_collect_raw | 日志 | 存原始JSON | 每次采集 | | log_collector_heartbeat | 日志 | 心跳 | 每10秒 | | log_system | 日志 | 关键事件日志 | 关键事件时 | | cnc_collect_record | 业务 | 结构化采集记录 | 每次采集每台机床 | | cnc_production_segment | 业务 | 开段/结段 | 程序切换/清零时 | | cnc_daily_production | 业务 | 日汇总产量 | 日汇总时 | | cnc_worker_daily_summary | 业务 | 工人日汇总 | 日汇总时 | | cnc_machine_daily_status | 业务 | 机床日状态 | 日汇总时 | | cnc_machine | 业务 | 更新实时状态字段 | 每次采集每台机床 | | cnc_collect_address | 业务 | 更新last_collect_time/fail_count | 每次采集 | | cnc_alert | 业务 | 告警记录 | 异常时 | ### 13.3 需要新增的Repository方法 以下方法在现有接口中不存在,需要新增: ```csharp // ICollectRecordRepository(新建) long Create(CollectRecord entity); // IMachineRepository(补充) void UpdateRealtimeStatus(int machineId, Machine realtimeFields); // IDailyProductionRepository(补充) int Upsert(DailyProduction entity); // INSERT ON DUPLICATE KEY UPDATE void DeleteByMachineAndDate(int machineId, DateTime date); // IWorkerDailySummaryRepository(补充) int Upsert(WorkerDailySummary entity); ``` --- ## 十四、部署与运维 ### 14.1 安装为Windows Service ```bash # 安装 installutil CncCollector.exe # 卸载 installutil /u CncCollector.exe # 或使用 sc 命令 sc create CncCollector binPath= "E:\opencode\haoliang\src\CncCollector\bin\Debug\CncCollector.exe" sc start CncCollector sc stop CncCollector ``` ### 14.2 文件部署位置 ``` E:\opencode\haoliang\src\CncCollector\bin\Debug\ ├── CncCollector.exe ├── CncCollector.exe.config ← App.config 编译输出 ├── CncModels.dll ← 引用的模型层 ├── CncRepository.dll ← 引用的仓储层 ├── Dapper.dll ├── MySqlConnector.dll ├── Newtonsoft.Json.dll ├── log4net.dll └── logs\ ← 日志目录(自动创建) ``` ### 14.3 与Web API共享数据库 - 采集服务和Web API同时读写同一个MariaDB - 采集服务只写自己的表(采集记录、产量段、日汇总等),不会覆盖Web API管理的配置数据 - 配置变更通过DB传递,双保险保证一致 ### 14.4 防火墙 - 开放5800端口供管理API通信(仅本机/局域网访问) - 采集服务需要出站HTTP访问采集地址URL - Ping需要ICMP出站权限 --- ## 十五、开发顺序建议 | 步骤 | 内容 | 依赖 | |------|------|------| | 1 | 新建CncCollector项目 + 引用CncModels/CncRepository + App.config | 无 | | 2 | FieldMapper + ValueConverter(纯函数,可独立测试) | 步骤1 | | 3 | CollectorTask 单地址采集循环(HTTP拉取+解析+写库) | 步骤2 | | 4 | ProductionTracker 产量分段追踪 | 步骤1 | | 5 | PingTask Ping循环 | 步骤1 | | 6 | CollectorEngine 引擎(启停所有Task) | 步骤3+4+5 | | 7 | HeartbeatReporter 心跳 | 步骤1 | | 8 | ConfigProvider 配置热更新 | 步骤6 | | 9 | DailySummaryJob 日终汇总 | 步骤4 | | 10 | ManagementServer + ManagementController 管理API | 步骤6+8+9 | | 11 | DualLogger 双写日志 | 步骤1 | | 12 | ServiceEntry + ServiceInstaller Windows Service包装 | 步骤6+10 | | 13 | 集成测试(完整链路) | 全部 | 建议先跑通 **步骤1→2→3** 这条最小链路(一个地址→拉取→解析→写库),再逐步叠加其他功能。