# 发那科采集数据结构变更 - 系统调整方案

> 创建时间：2026-05-11
> 触发原因：发那科系统采集示例文件（根目录 `发那科系统采集示例.txt`）更新，实际FANUC采集数据结构与系统原有设计存在差异
> 涉及模块：CncSimulator模拟器、数据库设计、采集服务设计、后台管理功能

---

## 一、实际FANUC数据结构（2026-05-11 采自生产环境）

### 1.1 顶层结构

```json
[
  {
    "device": "fanake-1.2_1.2",   // 设备编码，匹配 cnc_machine.device_code
    "desc": "西-1.2",              // 设备描述
    "tags": [ ... ]                // 10个Tag
  },
  ...
]
```

共32台设备，每台10个Tag。其中25台在线、7台离线。

### 1.2 Tag清单（10个）

| # | Tag ID | desc（中文） | 类型 | 示例值 | 备注 |
|---|--------|-------------|------|--------|------|
| 1 | `_io_status` | 设备状态 | 数值 | 0.00000 / 1.00000 | 0=离线, 1=在线；**始终有效** |
| 2 | `Tag5` | 执行的NC主程序名 | 字符串 | O1, O9001, 1370.NC, 7, 1027, O2, 037.NC | 可为空 |
| 3 | `Tag6` | 执行的NC主程序号 | 字符串 | N0, N1, N9, N20 | |
| 4 | `Tag7` | 当前加工程序内容 | 字符串 | G代码片段 | 可为空 |
| 5 | `Tag8` | 当前加工零件数 | 数值 | 35.00000 | 当前程序的零件计数 |
| 6 | `Tag9` | 运行状态 | 数值 | 0.00000 / 1.00000 / 3.00000 | 0=待机, 1=运行, 3=加工中 |
| 7 | `Tag11` | 操作模式 | 数值 | 1 / 4 / 5 / 10 | 1=MEM, 4=?, 5=?, 10=JOG |
| 8 | `Tag22` | 开机时间 | 数值 | 45129840.00000 | 秒，历史累计 |
| 9 | `Tag23` | 运行时间 | 数值 | 12201.00000 | 秒，历史累计 |
| 10 | `Tag1` | 加工零件总数 | 数值 | 45930.00000 | **历史终身累计，新增字段** |

### 1.3 数据有效性规则

#### 规则1：Quality字段

| quality值 | 含义 | 处理方式 |
|-----------|------|---------|
| `"0"` | 数据有效 | 正常解析 |
| `"1"` | 数据无效（采集失败/设备离线） | 忽略该tag的value |

#### 规则2：Time字段（采集失败标记）

| time值 | 含义 | 处理方式 |
|--------|------|---------|
| `1970-01-01 08:00:00` | 采集失败 | 忽略该tag的value |
| 其他正常时间 | 数据时间戳 | 正常使用 |

#### 规则3：_io_status 特殊处理

`_io_status` tag **不受规则1和规则2约束**，始终读取value判断设备在线/离线：
- 即使设备离线，`_io_status` 的 quality=0、time=真实时间、value=0

#### 规则4：Time与Value独立变化

每个tag的 `time` 和 `value` 更新频率不固定：
- time变了、value没变
- value变了、time没变
- 都变了
- 都没变

**采集服务的变更检测必须对比value值，不能依赖time变化作为变更信号。**

### 1.4 离线设备特征

以 fanake-1.10 为例：

```
_io_status: quality=0, value=0.00000, time=2026-05-11 17:06:34   ← 始终有效
Tag5:       quality=1, value="",       time=1970-01-01 08:00:00  ← 无效
Tag6:       quality=1, value="",       time=1970-01-01 08:00:00  ← 无效
Tag7:       quality=1, value="",       time=1970-01-01 08:00:00  ← 无效
Tag8:       quality=1, value=0.00000,  time=1970-01-01 08:00:00  ← 无效
Tag9:       quality=1, value=0.00000,  time=1970-01-01 08:00:00  ← 无效
Tag11:      quality=1, value=0.00000,  time=1970-01-01 08:00:00  ← 无效
Tag22:      quality=1, value=0.00000,  time=1970-01-01 08:00:00  ← 无效
Tag23:      quality=1, value=0.00000,  time=1970-01-01 08:00:00  ← 无效
Tag1:       quality=1, value=0.00000,  time=1970-01-01 08:00:00  ← 无效
```

---

## 二、与现有设计的差异对比

### 2.1 模拟器（FanucDataGenerator）差异

| 差异项 | 现有模拟器（18个Tag） | 实际FANUC（10个Tag） | 影响 |
|--------|----------------------|---------------------|------|
| Tag2 (当前轴数) | ✅ 有 | ❌ 不存在 | 模拟器多余 |
| Tag14 (主轴倍率) | ✅ 有 | ❌ 不存在 | 模拟器多余 |
| Tag17 (主轴设定速度) | ✅ 有 | ❌ 不存在 | 模拟器多余 |
| Tag18 (进给设定速度) | ✅ 有 | ❌ 不存在 | 模拟器多余 |
| Tag19 (主轴实际速度) | ✅ 有 | ❌ 不存在 | 模拟器多余 |
| Tag20 (进给实际转速) | ✅ 有 | ❌ 不存在 | 模拟器多余 |
| Tag21 (主轴负载) | ✅ 有 | ❌ 不存在 | 模拟器多余 |
| Tag24 (切削时间) | ✅ 有 | ❌ 不存在 | 模拟器多余 |
| Tag25 (循环时间) | ✅ 有 | ❌ 不存在 | 模拟器多余 |
| Tag26 (加工状态) | ✅ 有 | ❌ 不存在 | 模拟器多余 |
| **Tag1 (加工零件总数)** | ❌ 缺少 | ✅ 有 | **模拟器缺少，需新增** |
| Tag6 desc | "执行的NC主程序号" | "执行的NC主程序号" | 一致 |

### 2.2 数据库设计差异

| 差异项 | 现有设计 | 实际需求 |
|--------|---------|---------|
| 标准字段 total_part_count | ❌ 无 | ✅ 需要新增，对应Tag1 |
| cnc_collect_record.total_part_count | ❌ 无 | ✅ 需要新增列 |
| cnc_machine.last_total_part_count | ❌ 无 | ✅ 需要新增列 |

### 2.3 采集服务设计差异

| 差异项 | 现有设计假设 | 实际行为 |
|--------|-------------|---------|
| 数据有效性判断 | 未明确quality处理 | 需按quality+time双规则过滤 |
| _io_status处理 | 未明确特殊性 | 始终有效，不受quality/time约束 |
| 变更检测 | 未明确time/value独立性 | 必须对比value，不看time |
| Tag1(总零件数) | 无此字段 | 需解析并存储 |

### 2.4 后台管理差异

| 差异项 | 现有设计 | 实际需求 |
|--------|---------|---------|
| 速度/倍率/负载字段 | 设计了展示 | FANUC设备这些字段为NULL，需处理空值 |
| 加工零件总数 | 未设计展示 | 可选展示 |
| 操作模式枚举 | 仅定义1=MEM, 10=JOG | 实际还有4、5值，需补充定义 |

---

## 三、调整方案

### 3.1 CncSimulator 模拟器调整（优先级：高）

> 目标：让模拟器生成的数据结构与实际FANUC完全一致

#### 3.1.1 DeviceState.cs 修改

```diff
  public class DeviceState
  {
      // ===== 固定信息（来自配置） =====
      public string DeviceCode { get; set; }
      public string Desc { get; set; }

      // ===== 动态状态 =====
      public string CurrentScenario { get; set; } = "idle";
      public bool IsOnline { get; set; } = true;
      public string ProgramName { get; set; } = "O0001";
      public int PartCount { get; set; } = 0;
+     /// <summary>历史终身累计零件总数（对应Tag1）</summary>
+     public decimal TotalPartCount { get; set; } = 0;
      public int DeviceStatus { get; set; } = 1;
      public int RunStatus { get; set; } = 0;
      public int OperateMode { get; set; } = 1;
-     public decimal SpindleSpeedSet { get; set; } = 450;
-     public decimal FeedSpeedSet { get; set; } = 60;
-     public decimal SpindleSpeedActual { get; set; } = 0;
-     public decimal FeedSpeedActual { get; set; } = 0;
-     public decimal SpindleLoad { get; set; } = 0;
-     public decimal SpindleOverride { get; set; } = 100;
      public decimal PowerOnTime { get; set; } = 0;
      public decimal RunTime { get; set; } = 0;
-     public decimal CuttingTime { get; set; } = 0;
-     public decimal CycleTime { get; set; } = 0;
-     public string MachiningStatus { get; set; } = "";
      public string ProgramContent { get; set; } = "";
      // ... 其余字段保留不变
  }
```

**说明**：移除实际FANUC不存在的字段（SpindleSpeedSet、FeedSpeedSet、SpindleSpeedActual、FeedSpeedActual、SpindleLoad、SpindleOverride、CuttingTime、CycleTime、MachiningStatus），新增TotalPartCount。

#### 3.1.2 FanucDataGenerator.cs 修改

```diff
  private JArray GenerateTags(DeviceState state)
  {
      var tags = new JArray();
      DateTime baseTime = DateTime.Now;

      // 每个tag的时间基准上随机偏移 -5~0 秒
      DateTime TagTime()
      {
          return baseTime.AddSeconds(-_rng.Next(0, 6));
      }

      void AddNumericTag(string id, string desc, decimal value) { /* 不变 */ }
      void AddStringTag(string id, string desc, string value) { /* 不变 */ }

-     // 18个tag → 10个tag，与实际FANUC完全一致
      AddNumericTag("_io_status", "设备状态", state.DeviceStatus);
-     AddNumericTag("Tag2", "当前轴数", 4);
+     AddNumericTag("Tag1", "加工零件总数", state.TotalPartCount);
      AddStringTag("Tag5", "执行的NC主程序名", state.ProgramName);
      AddStringTag("Tag6", "执行的NC主程序号", "N0");
      AddStringTag("Tag7", "当前加工程序内容", /* ... */);
      AddNumericTag("Tag8", "当前加工零件数", state.PartCount);
      AddNumericTag("Tag9", "运行状态", state.RunStatus);
      AddNumericTag("Tag11", "操作模式", state.OperateMode);
-     AddNumericTag("Tag14", "当前主轴倍率", state.SpindleOverride);
-     AddNumericTag("Tag17", "主轴设定速度", state.SpindleSpeedSet);
-     AddNumericTag("Tag18", "进给设定速度", state.FeedSpeedSet);
-     AddNumericTag("Tag19", "主轴实际速度", state.SpindleSpeedActual);
-     AddNumericTag("Tag20", "进给实际转速", state.FeedSpeedActual);
-     AddNumericTag("Tag21", "主轴负载", state.SpindleLoad);
      AddNumericTag("Tag22", "开机时间", state.PowerOnTime);
      AddNumericTag("Tag23", "运行时间", state.RunTime);
-     AddNumericTag("Tag24", "切削时间", state.CuttingTime);
-     AddNumericTag("Tag25", "循环时间", state.CycleTime);
-     AddStringTag("Tag26", "加工状态", state.MachiningStatus);

      return tags;
  }
```

#### 3.1.3 DeviceSimulator.cs 修改

- 构造函数：新增 `TotalPartCount` 初始值配置（从simulator.json读取）
- `machining` 场景：`TotalPartCount++`
- `ApplyProgramChange()`：TotalPartCount 不清零（终身累计）
- `ApplyManualReset()`：TotalPartCount 不清零
- `ApplyPowerOn()`：TotalPartCount 不清零
- 移除所有已删除字段（SpindleSpeed*、SpindleLoad、SpindleOverride、CuttingTime、CycleTime、MachiningStatus）的模拟逻辑

#### 3.1.4 simulator.json 修改

devices配置中新增 `initialTotalPartCount` 字段：

```json
{
  "deviceCode": "CNC-A001",
  "desc": "西栋1号",
  "initialProgram": "O0001",
  "initialPartCount": 50,
  "initialTotalPartCount": 45000
}
```

#### 3.1.5 模拟器离线行为

当设备断电（power_off）时，需要生成与实际一致的离线数据：
- `_io_status`：quality=0, value=0, time=当前时间
- 其他所有tag：quality=1, value="" 或 "0.00000", time="1970-01-01 08:00:00"

---

### 3.2 数据库设计调整（优先级：高）

#### 3.2.1 cnc_brand_field_mapping 标准字段约定

在 `docs/01-数据库设计.md` 的标准字段约定表中新增：

| standard_field | 含义 | data_type |
|----------------|------|-----------|
| **total_part_count** | **历史累计加工零件总数** | **number** |

#### 3.2.2 cnc_collect_record 表

新增列：

```sql
ALTER TABLE cnc_collect_record
  ADD COLUMN total_part_count DECIMAL(15,5) NULL COMMENT '历史累计加工零件总数'
  AFTER part_count;
```

完整DDL变更位置：在 `part_count` 列之后增加 `total_part_count` 列。

#### 3.2.3 cnc_machine 表

新增列：

```sql
ALTER TABLE cnc_machine
  ADD COLUMN last_total_part_count DECIMAL(15,5) NULL COMMENT '最新历史累计加工零件总数'
  AFTER last_part_count;
```

#### 3.2.4 FANUC品牌预置映射数据

在 `database/` 预置数据脚本中，FANUC品牌的 field_mapping 新增一行：

| brand_id | standard_field | field_name | match_by | data_type | is_required |
|----------|---------------|------------|----------|-----------|-------------|
| (FANUC) | total_part_count | Tag1 | id | number | 0 |

同时，以下标准字段在实际FANUC中不存在，映射记录应标记 `is_required=0` 或不创建映射（由采集服务按实际数据决定）：
- spindle_speed_set → 无对应Tag
- feed_speed_set → 无对应Tag
- spindle_speed_actual → 无对应Tag
- feed_speed_actual → 无对应Tag
- spindle_load → 无对应Tag
- spindle_override → 无对应Tag
- cutting_time → 无对应Tag
- cycle_time → 无对应Tag
- machining_status → 无对应Tag

---

### 3.3 采集服务设计调整（优先级：高）

#### 3.3.1 采集循环核心逻辑（伪代码）

```
每次采集循环（按采集地址配置的间隔）：

1. Ping目标地址
   - Ping失败 → 更新 cnc_collect_address.fail_count++
   - 连续5次失败 → 写告警
   - 跳过本次采集

2. HTTP GET 拉取JSON数组

3. 原始JSON存日志库（cnc_raw_log，每次都存，不管有没有变化）

4. 逐设备处理JSON数组中的每个元素：

   4.1 用 device 值匹配 cnc_machine.device_code
       - 未匹配到 → 记录告警"发现未知设备"，跳过

   4.2 解析 _io_status（特殊处理）：
       → 直接读取 value，不管 quality 和 time
       → value=1 → 设备在线
       → value=0 → 设备离线

   4.3 解析其他tag（标准处理）：
       for each tag in tags (排除 _io_status):
         if tag.quality != "0"               → 跳过（数据无效）
         if tag.time 以 "1970-01-01" 开头     → 跳过（采集失败）
         否则 → 按 brand_field_mapping 映射为标准字段，解析 value

   4.4 写入结构化采集记录：
       → INSERT INTO cnc_collect_record (每台设备每次采集一条)
       → total_part_count 列写入 Tag1 的值（如果有效）
       → 速度/倍率/负载等字段写 NULL（FANUC无此数据）

   4.5 业务变更检测（只对比 value，不看 time）：

       a. NC程序名变更检测：
          if 新Tag5.value != 旧 cnc_machine.last_program_name:
              → 结账当前产量段 (close_reason='program_change')
              → 开新产量段

       b. 零件数下降检测（手动清零）：
          if 新Tag8.value < 旧 cnc_machine.last_part_count
             AND 新Tag5.value == 旧 cnc_machine.last_program_name:
              → 结账当前产量段 (close_reason='manual_reset')
              → 开新产量段，part_count 从新值开始

       c. 运行状态变更：
          if 新Tag9.value != 旧 cnc_machine.last_run_status:
              → 仅更新实时状态，不触发产量段逻辑

       d. 总零件数：仅记录，不触发任何业务逻辑

   4.6 更新机床实时状态（cnc_machine 的 last_* 字段）：
       → last_program_name = Tag5.value
       → last_part_count = Tag8.value
       → last_total_part_count = Tag1.value
       → last_run_status = Tag9.value
       → last_operate_mode = Tag11.value
       → last_device_status = _io_status.value
       → is_online = (_io_status.value == 1)
       → last_collect_time = 当前服务器时间

5. 更新采集地址状态：
   → last_collect_time = 当前时间
   → last_collect_status = 'success'
   → fail_count = 0
```

#### 3.3.2 数据有效性判断流程图

```
收到一个tag
    │
    ├── tag.id == "_io_status" ?
    │       ├── YES → 始终有效，直接读 value
    │       └── NO  → 进入标准判断 ↓
    │
    ├── tag.quality != "0" ?
    │       ├── YES → 无效，跳过
    │       └── NO  → 继续判断 ↓
    │
    └── tag.time 以 "1970-01-01" 开头 ?
            ├── YES → 无效，跳过
            └── NO  → 有效，解析 tag.value
```

---

### 3.4 后台管理功能调整（优先级：中）

#### 3.4.1 设备状态展示

`cnc_machine` 表已有 `last_device_status`、`last_run_status` 等字段，展示逻辑不变。新增：
- 设备详情中可选展示"累计加工总数"（来自 `last_total_part_count`）

#### 3.4.2 大屏看板空值处理

FANUC设备的以下字段恒为NULL：
- 主轴设定速度、进给设定速度
- 主轴实际速度、进给实际速度
- 主轴负载、主轴倍率
- 切削时间、循环时间
- 加工状态

大屏如果展示这些指标：
- 数值型 → 显示 `--` 或 `N/A`
- 图表型 → 该FANUC设备的线不画

#### 3.4.3 品牌字段映射管理

映射配置页面需支持新增的 `total_part_count` 标准字段。FANUC品牌预置映射中需包含 `Tag1 → total_part_count`。

#### 3.4.4 前端Mock数据同步

前端Mock数据中与FANUC相关的设备状态Mock，应同步为10个Tag的字段集（移除速度/负载等，新增total_part_count）。

---

### 3.5 设计文档更新（优先级：中）

| 文档 | 更新内容 |
|------|---------|
| `docs/01-数据库设计.md` | 标准字段约定表增加 total_part_count；cnc_collect_record 和 cnc_machine 的DDL增加新列 |
| `docs/00-需求与设计文档.md` | 4.2数据格式章节补充quality/1970-time/Tag1说明 |
| `docs/03-API接口设计.md` | 如果有涉及采集记录字段的API响应格式，需同步增加 total_part_count |
| `database/` | ALTER TABLE脚本 + FANUC预置映射数据新增 |

---

## 四、不需要调整的部分

| 项目 | 原因 |
|------|------|
| 顶层JSON结构 `[{device, desc, tags}]` | 实际数据与设计一致 |
| device字段匹配机床逻辑 | 设计正确，device值匹配device_code |
| 零件分段计数核心逻辑（基于Tag8） | 不受影响，仍基于NC程序名切换+清零 |
| 产量分段记录表 cnc_production_segment | 结构不变，total_part_count不影响分段计算 |
| 采集地址/品牌管理配置架构 | 不变 |
| 双库分离设计（原始JSON存日志库） | 不变 |
| 采集间隔配置（地址级统一间隔） | 不变 |
| 日终汇总逻辑 | 不变 |

---

## 五、执行顺序

```
阶段1：模拟器适配（让测试数据与实际一致）
  ├── 1.1 DeviceState.cs 新增 TotalPartCount，移除多余字段
  ├── 1.2 FanucDataGenerator.cs 改为10个Tag
  ├── 1.3 DeviceSimulator.cs 调整场景逻辑
  ├── 1.4 simulator.json 新增 initialTotalPartCount
  └── 1.5 编译验证

阶段2：数据库变更（数据基础）
  ├── 2.1 cnc_collect_record 增加 total_part_count 列
  ├── 2.2 cnc_machine 增加 last_total_part_count 列
  ├── 2.3 标准字段约定表增加 total_part_count
  ├── 2.4 FANUC预置映射数据增加 Tag1 映射
  └── 2.5 设计文档同步更新

阶段3：采集服务设计定稿（核心逻辑）
  ├── 3.1 编写采集服务数据解析逻辑（含quality/time过滤）
  ├── 3.2 编写变更检测逻辑（只看value不看time）
  └── 3.3 编写Tag1存储逻辑（total_part_count仅记录不触发业务）

阶段4：后台管理适配（展示层）
  ├── 4.1 大屏空值处理（FANUC设备速度/负载为NULL时显示N/A）
  ├── 4.2 前端Mock数据同步为10个Tag结构
  └── 4.3 品牌映射管理支持total_part_count字段
```

---

## 六、补充说明

### 6.1 操作模式枚举值

实际数据中出现了以下操作模式值，具体含义需与现场确认：

| 值 | 已知含义 | 出现设备 |
|----|---------|---------|
| 1 | MEM（内存运行） | 加工中的设备 |
| 4 | 待确认 | fanake-1.19（运行状态=1） |
| 5 | 待确认 | fanake-1.24（运行状态=0） |
| 10 | JOG（手动） | 待机中的设备 |

### 6.2 device字段格式

实际数据格式为 `fanake-1.2_1.2`（品牌-编号_编号），机床表的 `device_code` 需按此格式录入。

### 6.3 数值精度

所有数值型Tag的value都带5位小数（如 `35.00000`），采集服务解析时需 `Convert.ToDecimal` 后存储到 `DECIMAL(15,5)` 列。

### 6.4 time字段格式

统一格式 `yyyy-MM-dd HH:mm:ss`，需要按此格式解析。设备离线时为 `1970-01-01 08:00:00`（UTC+8时区下的epoch时间）。
