You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

325 lines
16 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 主要展示(其余字段在前端暂时隐藏或注释掉)
- 位置信息:标识、位置编号、位置别名、父级编号、是否标识、备注、创建人、创建时间、更新人、更新时间
- 设备信息:标识、设备编号、设备名称、所在位置、设备地址、设备端口、读取频率、是否标识、备注、创建人、创建时间、更新人、更新时间
- 读取记录:标识、设备编号、读取状态(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数据库的总结文档
# 业务逻辑
# 位置树与设备
- 位置树分为车间/机台/工位,共三级
- 设备绑定在工位上
- IP地址 deviceAddress 唯一校验
- 端口号devicePort应该都是20108创建的时候先默认这个如果有需要再修改
## 设备记录
### 分页查询
- 分页查询逻辑参考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`按小时023 点)返回折线图数据,用于前端折线图展示.
- 统计维度:后端自动以当前系统日期作为“今日”,**同时统计昨日同一小时的成功率**,前端无需传任何查询参数.
- 分表与时间范围:分别通过 `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>`,再按 023 小时顺序构造 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. **补全 023 小时完整时间轴**
- 将 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`.
---
### 接口4设备最新读取记录
> 请求方式:`GET /rfid/dashboard/deviceLatestRecords`
> 前端方法:`getDeviceLatestRecords()`
> 后端链路:`DashboardController#getDeviceLatestRecords` → `IDashboardService#getDeviceLatestRecords`
**请求参数Query**
- 无参数,直接调用即返回所有设备的最新读取记录。
**返回实体与字段**
- 后端返回类型:`List<DashboardVO.DeviceLatestRecordVO>`
- 前端 TS 类型:`DeviceLatestRecordVO[]`
字段说明:
1. `deviceId: number`:设备 ID。
2. `deviceCode: string`:设备编号。
3. `deviceName: string`:设备名称。
4. `latestBarcode: string | null`:最新条码信息,可能为 `null`(无记录时)。
5. `latestRecordTime: string | null`:最新记录时间(`yyyy-MM-dd HH:mm:ss` 格式),可能为 `null`
6. `readStatus: string | null`:读取状态(`1-成功0-失败`),可能为 `null`
7. `alarmFlag: string | null`:是否告警(`0-否1-是`),可能为 `null`
8. `alarmAction: string | null`:告警行为,可能为 `null`
**后端处理要点**
1. **近7天多表查询**由于设备可能当天或最近几天没有数据接口会查询近7天内包含今天所有实际存在的分表。
2. **分表检测**:通过 `RfidReadRecordTableHelper.getExistingTableNames()` 获取近7天内**实际存在**的分表列表。
- 先生成近7天的所有可能表名`rfid_read_record_20251128` ~ `rfid_read_record_20251204`
- 通过 `checkTableExists()` 查询 `information_schema` 过滤掉数据库中不存在的表;
- **分表不一定连续**:如某几天无数据则无对应分表,最终可能只返回 2-3 张表。
3. **多表联合查询**:使用 `selectLatestRecordByDeviceMultiTable` 方法,通过 `UNION ALL` 合并多表数据,按 `device_id` 分组取每个设备 `record_time` 最大的记录。
4. **异常处理**当近7天内无可用分表或查询出错时返回空列表不影响接口可用性。
**SQL 逻辑说明**
```sql
-- 合并近7天分表数据
SELECT ... FROM (
SELECT ... FROM rfid_read_record_20251128
UNION ALL
SELECT ... FROM rfid_read_record_20251129
...
) t1
INNER JOIN (
-- 按设备分组取最新时间
SELECT device_id, MAX(record_time) as max_time FROM (...) GROUP BY device_id
) latest ON t1.device_id = latest.device_id AND t1.record_time = latest.max_time
```
---
### 原有接口(聚合 / 辅助)
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`.