# 主要展示（其余字段在前端暂时隐藏或注释掉）
- 位置信息:标识、位置编号、位置别名、父级编号、是否标识、备注、创建人、创建时间、更新人、更新时间
- 设备信息:标识、设备编号、设备名称、所在位置、设备地址、设备端口、读取频率、是否标识、备注、创建人、创建时间、更新人、更新时间
- 读取记录:标识、设备编号、读取状态(1-成功;0-失败)、条码信息、记录时间


- **代码注意事项**
  1. ** 连表查询规范**  
     使用 `mybatis-plus`做连表查询时：
     - `MybatisPlus`自带方法不支持连表查询，需要在mapper.xml中编写sql语句.
     - 在实体类和 `VO` 中添加连表字段，实体类字段需加 `@TableField(exist = false)` 注解。
  2. **types 同步维护**  
     连表查询得到的字段，需要同步补充到前端 `types.ts` 中，保证类型定义完整。
  3. **接口前缀规范**  
     参考同模块下已有接口前缀，统一后台 `@RequestMapping` 前缀。
  4. **前端列表 / 表单字段规范**
     - 只展示业务字段，技术字段仅用于内部关联（连表查询得到名称字段），若无关联则隐藏技术字段（前端在 `columns` 中设为 `false`）
     - 业务字段不显示多余的说明文字，例如字典字段的括号以及括号的字典数据
     - 列表不直接展示主键，使用“序号”列（`index`）替代。
     - 注释掉不合理的搜索条件，对话框中注释掉不合理的输入字段。
     - 数字字段使用数字输入框；
     - 新增对话框时，前端激活标识 `isMarked` 默认为 `1`。
     - 需要绑定其他表字段时，通过下拉框选择，表单字段使用 `get${ClassName}List` 接口获取不分页的全部数据。
  5. **主子表 List 字段**  
     主子表结构时，需要在主表实体中将子表作为 `List` 类型字段，便于提交新增/修改、返回数据和删除时统一处理，保证事务一致性。
  6. **字典使用规范**
     - 后端只返回字典键值。
     - 前端列表通过 `dict-tag` 组件展示字典文本。
     - 前端需要显式声明字典，例如：  
       `const { project_category, change_type, project_phases } = toRefs<any>(proxy?.useDict('project_category', 'change_type', 'project_phases'));`
  7. **实体字段类型规范**
     - 数据库 `int` → Java `Integer`
     - `decimal` → `BigDecimal`
     - `bigint` → `Long`
     - `double` → `Double`
     - `float` → `Float`
 11. **主子表删除规范**
     - 主子表删除时，必须在同一事务中同时删除主表与子表记录，保证数据一致性。
 12. **接口合并与聚合**
     - 前端调用接口应尽量合并：同一模块或同一功能点优先通过单个接口组装返回所需数据，减少多次请求。
 13. **前端 columns 配置规则**
     - `columns` 的 `key` 必须从 `0` 开始，不间断完整自增。
     - 对比实际表格列与 `columns` 配置，第 `0` 列使用“序号”（`index` 列）而不是主键。
     - 将“项目ID”等技术列从 `columns` 中完全移除（而不是隐藏），去掉租户编号等无业务意义的字段，其他字段根据实际情况调整，确保 `columns.key` 连续从 `0` 自增且与表格实际列一一对应。
 14. **名称获取规范**
     - 所有创建人名称、部门名称必须通过后端连表查询得到，而不是前端拼接或本地缓存获取。
 15. **业务编号规范**
     - 业务编号应该唯一。
 16. **树形结构规范**
     - 树形结构的parent_id和ancestors字段的递归逻辑需要完善，若parent_id和ancestors为0则为顶级节点，0是必须有的。
 17. **更新子节点的ancestors**
     - 更新子节点的ancestors时，需要更新所有子节点的ancestors，而不是只更新当前节点的ancestors。
 18. **前端树形结构的新增或修改**
     - 前端对话框第一行必须是先选择父节点！
 19. **特别注意：rfid_read_record分表**
     - rfid_read_record按照日期分表，例如rfid_read_record_20251126，可参考rfid_read_record_20251126.sql文件，数据库类型是MySQL
     - 前后端代码可以参考ShardingQuery.md文件（其他项目TiDb数据库的总结文档）
     
 
# 业务逻辑
## 设备记录
### 分页查询
- 分页查询逻辑参考rfid-middleware\ruoyi-modules\hw-rfid\ShardingQuery.md文档
### 条码信息显示空格符号的空格处理
- 202020202020205357303034 设备返回的这个 20 转成 ascii 是个空格
- 条码信息原始值可能包含空格(0x20) 和 NULL 字符(0x00) 等不可见字符，例如设备返回 `202020202020205357303034`，其中 `20` 转成 ASCII 是空格。
- Mapper 层在 `RfidReadRecordMapper.xml` 中对 `barcode` 使用 `TRIM(t.barcode) as barcode` 去掉前后空格，但对 NULL/控制字符无效（SQL 的 `TRIM()` 只能去除标准空格 ASCII 32）。
- Service 层在 `RfidReadRecordServiceImpl` 中通过 `cleanBarcodeList / cleanBarcode`，使用正则 `Pattern.compile("[\\s\\x00-\\x1F\\x7F]+")` 清理所有不可见字符，只保留业务字符，最终前端看到的条码为无空格的纯业务值（例如 `QNC006`）。
### 设备记录成功率逻辑
- 成功率统计接口：`GET /rfid/dashboard/successRate`，按小时（0–23 点）返回折线图数据，用于前端折线图展示.
- 统计维度：后端自动以当前系统日期作为“今日”，**同时统计昨日同一小时的成功率**，前端无需传任何查询参数.
- 分表与时间范围：分别通过 `RfidReadRecordTableHelper.getTableName(今日)` 与 `RfidReadRecordTableHelper.getTableName(昨日)` 生成当日与昨日分表（如 `rfid_read_record_20251126`），各自只统计 `00:00:00` 至 `23:59:59` 之间的读取记录.
- 成功率聚合 SQL：在分表上按 `DATE_FORMAT(record_time, '%H:00')` 分组，统计每小时的总记录数 `totalCount` 与成功记录数 `successCount`（`read_status = '1'`）。
- 成功率计算公式：`successRate = ROUND(successCount * 100.0 / totalCount, 2)`，即“该小时成功条数 ÷ 总条数 × 100”，保留两位小数.
- Mapper 至少返回 `timePoint`、`successRate` 字段，Service 层分别将“今日”与“昨日”的统计结果转为 `Map<timePoint, successRate>`，再按 0–23 小时顺序构造 24 条数据：
  - `successRate`：今日该小时成功率；
  - `yesterdaySuccessRate`：昨日同一小时成功率；
  - 对于没有统计结果的小时，对应字段返回 `null`，保证时间轴完整.
- 异常与容错：当某日分表不存在或查询出错时，Service 捕获异常记录告警日志并返回空列表或空 Map，不影响整个看板接口的可用性.

## 看板模块

### 接口1：实时统计（顶部概览 + 告警列表）

> 请求方式：`GET /rfid/dashboard/realtime`  
> 前端方法：`getRealtimeStats(alarmLimit?: number)`  
> 后端链路：`DashboardController#getRealtimeStats` → `IDashboardService#getRealtimeStats`

**请求参数（Query）**

- `alarmLimit?: number`  
  告警列表限制数量，可选。  
  - **不传：返回全部告警记录**，供前端滚动显示。

**返回实体与字段**

- 后端返回类型：`DashboardVO.RealtimeStats`
- 前端 TS 类型：`RealtimeStats`

字段说明：

1. `overview: StatisticsOverview` —— 顶部统计概览  
   - `deviceTotal: number`：设备总数（仅统计 `is_marked = 1` 的设备）。  
   - `onlineCount: number`：在线设备数量（`online_status = 1`）。  
   - `offlineCount: number`：离线设备数量（`online_status = 0`）。  
   - `alarmCount: number`：告警设备数量（`alarm_status = 1`）。  

2. `alarmStats: AlarmStatVO[]` —— 告警统计列表  
   - `alarmTime: string`：告警时间（`MM-dd HH:mm` 格式）。  
   - `deviceName: string`：设备名称。  
   - `location: string`：所在位置名称。  
   - `alarmLevel: string`：告警级别。  
   - `alarmAction: string`：告警行为/建议动作。  

---

### 接口2：位置树（含设备信息）

> 请求方式：`GET /rfid/dashboard/deviceStatus`  
> 前端方法：`getLocationTree()`  
> 后端链路：`DashboardController#getLocationTree` → `IDashboardService#getLocationTree`

**请求参数（Query）**

- 无参数，直接调用即返回完整位置树。

**返回实体与字段**

- 后端返回类型：`List<DashboardVO.LocationTreeNode>`
- 前端 TS 类型：`LocationTreeNode[]`

字段说明：

1. 位置基础信息  
   - `id: number`：位置 ID。  
   - `locationCode: string`：位置编号。  
   - `locationAlias: string`：位置别名。  
   - `locationType: string`：位置类型（`1-车间；2-工序；3-工位/设备`）。  
   - `parentId: number`：父级位置 ID（`0` 或 `null` 表示顶级）。  

2. 设备信息（仅当 `locationType = 3` 时有值）  
   - `deviceId: number`：设备 ID，**前端用于匹配 WebSocket 数据**。  
   - `deviceCode: string`：设备编号。  
   - `deviceName: string`：设备名称。  
   - `onlineStatus: string`：在线状态（`1-在线；0-离线`）。  
   - `alarmStatus: string`：告警状态（`0-正常；1-告警`）。  

3. 子节点  
   - `children: LocationTreeNode[]`：子位置/设备列表。  

**WebSocket 数据匹配说明**

C# 服务会通过 WebSocket 实时推送设备读取记录，格式示例：
```json
{
    "objid": 1993656942031147008,
    "deviceId": 1,
    "readStatus": "1",
    "epcStr": "SW004",
    "alarmFlag": "0",
    "alarmLevel": "",
    "alarmType": "",
    "alarmAction": "",
    "recordTime": "2025-11-26T20:23:49.695356+08:00"
}
```
前端通过 `deviceId` 字段匹配位置树中的设备节点，实现实时数据展示。  

---

### 接口3：设备记录的成功率（按小时统计）

> 请求方式：`GET /rfid/dashboard/successRate`  
> 前端方法：`getSuccessRateTrends(type?: string)`  
> 后端链路：`DashboardController#getSuccessRateTrends` → `DashboardServiceImpl#getSuccessRateTrends`

**请求参数（Query）**

- `type?: string`  
  - 前端不传参，后端直接默认当天，返回

**返回实体与字段**

- 后端返回类型：`List<DashboardVO.SuccessRateTrend>`
- 前端 TS 类型：`SuccessRateTrend[]`

字段说明：

1. `timePoint: string`  
   - 时间点，格式为 `"HH:00"`，例如 `"09:00"`，从 `"00:00"` 到 `"23:00"` 共 24 个。  

2. `successRate: number | null`  
   - 该小时的读取成功率（百分比，如 `98.5`）。  
   - 如果某个小时没有任何读取记录，则为 `null`。  

**后端处理要点（概要版）**

1. **查询维度**  
   - 入参 `type`：`today`（默认）、`yesterday`。  
   - 根据 `type` 计算目标日期 `targetDate`：  
     - `today` 或其它值 → `LocalDate.now()`（今日）；  
     - `yesterday` → `LocalDate.now().minusDays(1)`（昨日）。  

2. **分表与时间范围**  
   - 使用 `RfidReadRecordTableHelper.getTableName(targetDate)` 生成当日分表名，例如 `rfid_read_record_20251126`。  
   - 计算当日时间范围字符串：  
     - 起始时间 `startTime = targetDate.atStartOfDay()`（`yyyy-MM-dd 00:00:00`）；  
     - 结束时间 `endTime = targetDate.atTime(23, 59, 59)`（`yyyy-MM-dd 23:59:59`）。  

3. **按小时统计成功率（Mapper 层）**  
   - 调用 `readRecordMapper.selectSuccessRateByHour(tableName, startTime, endTime)` 进行聚合统计。  
   - SQL 约定返回字段：  
     - `timePoint`：小时点，格式为 `"HH:00"`，如 `"08:00"`；  
     - `successRate`：该小时的读取成功率（类型可能为 `Double` / `BigDecimal` 等）。  
   - 成功率的具体计算逻辑由 Mapper SQL 维护（如成功条数 / 总条数），Service 层只消费结果，不参与公式计算。  

4. **补全 0–23 小时完整时间轴**  
   - 将 Mapper 返回结果转换为 `Map<String, Double>`；  
   - 循环 `hour = 0..23` 构建 24 个时间点；  
   - 对没有统计结果的小时，`successRate` 返回为 `null`。  

5. **前端展示约定**  
   - 折线图 X 轴：直接使用 `timePoint`（`"HH:00"`）。  
   - 折线图 Y 轴：使用 `successRate` 数值。  
   - 当前端拿到 `successRate = null` 的点时，可根据实际需求选择：  
     - 展示为空值（不连线 / 断点）；或  
     - 按 0 处理（展示为 0%）。  
   - 由于后端已保证 24 个小时点完整返回，前端无需再做补全或排序逻辑。  

---

### 原有接口（聚合 / 辅助）

1. `GET /rfid/dashboard/data`  
   - 前端方法：`getDashboardData(locationId?: number)` / `getDashboardStats(locationId?: number)`（兼容老名称）。  
   - 参数：`locationId?: number`，位置 ID，可选。  
   - 返回：`DashboardVO`（统计概览 + 设备状态列表 + 成功率趋势 + 告警统计）。  

2. `GET /rfid/dashboard/overview`  
   - 前端方法：`getOverview()`。  
   - 参数：无。  
   - 返回：`StatisticsOverview`，字段含义同上。  

3. `GET /rfid/dashboard/alarmStats`  
   - 前端方法：`getAlarmStats(limit?: number)`。  
   - 参数：`limit?: number`，限制返回条数，默认 10。  
   - 返回：`AlarmStatVO[]`，字段含义同实时统计中的 `alarmStats`.