feat(dashboard): 新增设备最新读取记录接口

- 新增 GET /rfid/dashboard/deviceLatestRecords 接口,返回每个设备的最新读取记录
- 实现跨近7天动态分表查询,解决设备数据不连续问题
- 新增 DashboardServiceImpl#getDeviceLatestRecords 方法,封装多表联合查询逻辑
- 新增 Mapper 方法及对应 XML SQL,实现多表联合查询设备最新记录
- 定义 DeviceLatestRecordVO 数据结构,包含设备及最新读取相关字段
- 更新接口文档,详细描述请求参数与返回字段说明
- 增加异常处理及无数据返回空列表保证接口稳定性
main
zangch@mesnac.com 2 weeks ago
parent 6f1bbea019
commit aaf39d2bc4

@ -236,6 +236,79 @@ C# 服务会通过 WebSocket 实时推送设备读取记录,格式示例:
### 原有接口(聚合 / 辅助)
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可选。

@ -94,6 +94,19 @@ public class DashboardController extends BaseController {
return R.ok(dashboardService.getSuccessRateTrends(type));
}
/**
* 4
* <p>
* ID
* </p>
*
* @return
*/
@GetMapping("/deviceLatestRecords")
public R<List<DashboardVO.DeviceLatestRecordVO>> getDeviceLatestRecords() {
return R.ok(dashboardService.getDeviceLatestRecords());
}
// ==================== 以下为原有接口(保留) ====================
/**

@ -268,4 +268,58 @@ public class DashboardVO implements Serializable {
private String alarmAction;
}
/**
*
* <p>
*
* </p>
*/
@Data
public static class DeviceLatestRecordVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID
*/
private Long deviceId;
/**
*
*/
private String deviceCode;
/**
*
*/
private String deviceName;
/**
*
*/
private String latestBarcode;
/**
*
*/
private String latestRecordTime;
/**
* (1-; 0-)
*/
private String readStatus;
/**
* (0-; 1-)
*/
private String alarmFlag;
/**
*
*/
private String alarmAction;
}
}

@ -205,7 +205,7 @@ public interface RfidReadRecordMapper extends BaseMapperPlus<RfidReadRecord, Rfi
// ==================== 看板专用查询方法 ====================
/**
*
*
*
* @param tableName
* @param deviceIds ID
@ -215,6 +215,20 @@ public interface RfidReadRecordMapper extends BaseMapperPlus<RfidReadRecord, Rfi
@Param("tableName") String tableName,
@Param("deviceIds") List<Long> deviceIds);
/**
*
* <p>
* N
* </p>
*
* @param tableNames
* @param deviceIds ID
* @return
*/
List<RfidReadRecordVo> selectLatestRecordByDeviceMultiTable(
@Param("tableNames") List<String> tableNames,
@Param("deviceIds") List<Long> deviceIds);
/**
*
*

@ -78,4 +78,14 @@ public interface IDashboardService {
* @return
*/
DashboardVO getDashboardData(Long locationId);
/**
*
* <p>
* ID
* </p>
*
* @return
*/
List<DashboardVO.DeviceLatestRecordVO> getDeviceLatestRecords();
}

@ -20,7 +20,7 @@ import org.dromara.rfid.service.IDashboardService;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
@ -284,4 +284,54 @@ public class DashboardServiceImpl implements IDashboardService {
dashboard.setAlarmStats(getAlarmStats(null));
return dashboard;
}
/**
*
* <p>
* 7
*
* </p>
*/
@Override
public List<DashboardVO.DeviceLatestRecordVO> getDeviceLatestRecords() {
// 计算近7天的日期范围
LocalDate today = LocalDate.now();
LocalDate startDate = today.minusDays(6);
// 获取近7天内实际存在的分表列表使用 DateUtil 转换避免 java.sql.Date 不支持 toInstant
Date beginDate = DateUtil.date(startDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
Date endDate = DateUtil.date(today.atStartOfDay(ZoneId.systemDefault()).toInstant());
List<String> existingTables = RfidReadRecordTableHelper.getExistingTableNames(beginDate, endDate);
if (CollUtil.isEmpty(existingTables)) {
log.warn("近7天内没有可用的读取记录分表");
return Collections.emptyList();
}
log.debug("查询设备最新记录,涉及分表: {}", existingTables);
List<RfidReadRecordVo> latestRecords;
try {
// 多表查询所有设备的最新记录
latestRecords = readRecordMapper.selectLatestRecordByDeviceMultiTable(existingTables, null);
} catch (Exception e) {
log.warn("查询设备最新读取记录失败: {}", e.getMessage());
latestRecords = Collections.emptyList();
}
return latestRecords.stream().map(record -> {
DashboardVO.DeviceLatestRecordVO vo = new DashboardVO.DeviceLatestRecordVO();
vo.setDeviceId(record.getDeviceId());
vo.setDeviceCode(record.getDeviceCode());
vo.setDeviceName(record.getDeviceName());
vo.setLatestBarcode(record.getBarcode());
vo.setLatestRecordTime(record.getRecordTime() != null
? DateUtil.format(record.getRecordTime(), "yyyy-MM-dd HH:mm:ss")
: null);
vo.setReadStatus(record.getReadStatus());
vo.setAlarmFlag(record.getAlarmFlag());
vo.setAlarmAction(record.getAlarmAction());
return vo;
}).collect(Collectors.toList());
}
}

@ -379,6 +379,60 @@
order by t.device_id
</select>
<!-- 查询每个设备的最新一条读取记录(多表,跨日期范围) -->
<select id="selectLatestRecordByDeviceMultiTable" resultMap="RfidReadRecordResult">
select combined.id,
combined.device_id,
d.device_code as deviceCode,
d.device_name as deviceName,
combined.read_status,
TRIM(combined.barcode) as barcode,
combined.record_time,
combined.alarm_flag,
combined.alarm_level,
combined.alarm_type,
combined.alarm_action
from (
<!-- 合并所有分表数据,按设备分组取最新记录 -->
select t1.id, t1.device_id, t1.read_status, t1.barcode, t1.record_time,
t1.alarm_flag, t1.alarm_level, t1.alarm_type, t1.alarm_action
from (
<foreach collection="tableNames" item="tbl" separator=" UNION ALL ">
select id, device_id, read_status, barcode, record_time,
alarm_flag, alarm_level, alarm_type, alarm_action
from ${tbl}
<where>
<if test="deviceIds != null and deviceIds.size() > 0">
device_id in
<foreach collection="deviceIds" item="deviceId" open="(" separator="," close=")">
#{deviceId}
</foreach>
</if>
</where>
</foreach>
) t1
inner join (
select device_id, max(record_time) as max_time
from (
<foreach collection="tableNames" item="tbl" separator=" UNION ALL ">
select device_id, record_time from ${tbl}
<where>
<if test="deviceIds != null and deviceIds.size() > 0">
device_id in
<foreach collection="deviceIds" item="deviceId" open="(" separator="," close=")">
#{deviceId}
</foreach>
</if>
</where>
</foreach>
) tmp
group by device_id
) latest on t1.device_id = latest.device_id and t1.record_time = latest.max_time
) combined
left join rfid_device d on combined.device_id = d.id
order by combined.device_id
</select>
<!-- 按小时统计成功率 -->
<select id="selectSuccessRateByHour" resultType="java.util.Map">
select

Loading…
Cancel
Save