# 主要展示(其余字段在前端暂时隐藏或注释掉) - 位置信息:标识、位置编号、位置别名、父级编号、是否标识、备注、创建人、创建时间、更新人、更新时间 - 设备信息:标识、设备编号、设备名称、所在位置、设备地址、设备端口、读取频率、是否标识、备注、创建人、创建时间、更新人、更新时间 - 读取记录:标识、设备编号、读取状态(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(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`,按小时(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`,再按 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` - 前端 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` - 前端 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`; - 循环 `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` - 前端 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`.