IoT设备同步配置分页问题修复
提供的代码和华为云IoT API文档发现了一个关键问题分页处理不正确。在当前的HuaweiIotServiceImpl中getNextMarker()方法总是返回null导致无法获取所有设备数据。问题分析分页机制未实现getNextMarker()方法硬编码返回null设备列表不完整只能获取第一页的设备数据最多50个分页信息未解析API响应中的page对象包含分页信息但未被处理解决方案需要修改HuaweiIotServiceImpl.java来正确处理分页package com.alatus.system.service.impl; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONObject; import com.alatus.system.domain.DeviceInfo; import com.alatus.system.service.HuaweiIotService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.*; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import java.util.ArrayList; import java.util.List; Slf4j Service public class HuaweiIotServiceImpl implements HuaweiIotService { Value(${huawei.iot.base-url}) private String baseUrl; Value(${huawei.iot.project-id}) private String projectId; Value(${huawei.iot.product-id}) private String productId; private final RestTemplate restTemplate; private String accessToken; private long tokenExpireTime; public HuaweiIotServiceImpl() { this.restTemplate new RestTemplate(); } /** * 分页结果封装类 */ private static class PageResult { private ListDeviceInfo devices; private String nextMarker; public PageResult(ListDeviceInfo devices, String nextMarker) { this.devices devices; this.nextMarker nextMarker; } public ListDeviceInfo getDevices() { return devices; } public String getNextMarker() { return nextMarker; } } /** * 将JSON对象转换为DeviceInfo */ private DeviceInfo convertToDeviceInfo(JSONObject device) { try { DeviceInfo deviceInfo new DeviceInfo(); deviceInfo.setDeviceId(device.getString(device_id)); deviceInfo.setDeviceName(device.getString(device_name)); deviceInfo.setNodeId(device.getString(node_id)); deviceInfo.setProductId(device.getString(product_id)); deviceInfo.setStatus(device.getString(status)); deviceInfo.setCreateTime(device.getString(create_time)); return deviceInfo; } catch (Exception e) { log.error(转换设备信息失败: {}, e.getMessage()); return null; } } Override public DeviceInfo getDeviceById(String deviceId) { try { String token getAccessToken(); String url baseUrl /v5/iot/ projectId /devices/ deviceId; HttpHeaders headers new HttpHeaders(); headers.set(X-Auth-Token, token); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntityString entity new HttpEntity(headers); ResponseEntityString response restTemplate.exchange(url, HttpMethod.GET, entity, String.class); if (response.getStatusCode() HttpStatus.OK) { JSONObject device JSON.parseObject(response.getBody()); return convertToDeviceInfo(device); } else { log.error(获取设备详情失败状态码: {}, response.getStatusCode()); } } catch (Exception e) { log.error(获取设备详情失败: {}, e.getMessage()); } return null; } Override public boolean sendMessageToDevice(String deviceId, Object message) { try { String token getAccessToken(); String url baseUrl /v5/iot/ projectId /devices/ deviceId /messages; HttpHeaders headers new HttpHeaders(); headers.set(X-Auth-Token, token); headers.setContentType(MediaType.APPLICATION_JSON); JSONObject requestBody new JSONObject(); requestBody.put(message, message); HttpEntityString entity new HttpEntity(requestBody.toJSONString(), headers); ResponseEntityString response restTemplate.exchange(url, HttpMethod.POST, entity, String.class); return response.getStatusCode() HttpStatus.CREATED; } catch (Exception e) { log.error(发送消息到设备失败: {}, e.getMessage()); return false; } } /** * 获取访问令牌 */ private synchronized String getAccessToken() { // 如果token未过期直接返回 if (accessToken ! null System.currentTimeMillis() tokenExpireTime) { return accessToken; } try { // 这里简化处理实际应该调用IAM服务获取token // 由于安全考虑建议将认证信息放在配置文件中 String authUrl https://iam.cn-south-1.myhuaweicloud.com/v3/auth/tokens; JSONObject authRequest new JSONObject(); JSONObject auth new JSONObject(); JSONObject identity new JSONObject(); JSONArray methods new JSONArray(); methods.add(password); JSONObject password new JSONObject(); JSONObject user new JSONObject(); JSONObject domain new JSONObject(); domain.put(name, robotstorm_hw); user.put(domain, domain); user.put(name, robotstorm); user.put(password, JuShenFengBao449049); password.put(user, user); identity.put(methods, methods); identity.put(password, password); JSONObject scope new JSONObject(); JSONObject project new JSONObject(); project.put(name, cn-south-1); scope.put(project, project); auth.put(identity, identity); auth.put(scope, scope); authRequest.put(auth, auth); HttpHeaders headers new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntityString entity new HttpEntity(authRequest.toJSONString(), headers); ResponseEntityString response restTemplate.exchange(authUrl, HttpMethod.POST, entity, String.class); if (response.getStatusCode() HttpStatus.CREATED) { String token response.getHeaders().getFirst(X-Subject-Token); if (token ! null) { this.accessToken token; this.tokenExpireTime System.currentTimeMillis() 23 * 60 * 60 * 1000; // 23小时 log.info(成功获取华为云访问令牌); return token; } } log.error(获取访问令牌失败状态码: {}, response.getStatusCode()); } catch (Exception e) { log.error(获取访问令牌失败: {}, e.getMessage()); } throw new RuntimeException(无法获取华为云访问令牌); } Override public ListDeviceInfo getAllDevices() { ListDeviceInfo allDevices new ArrayList(); String marker null; int pageSize 50; // 华为云限制最大50 int pageCount 0; try { log.info(开始分页获取设备列表每页大小: {}, pageSize); do { pageCount; log.debug(获取第 {} 页设备数据marker: {}, pageCount, marker); PageResult pageResult getDevicesByPage(marker, pageSize); if (pageResult.getDevices() ! null !pageResult.getDevices().isEmpty()) { allDevices.addAll(pageResult.getDevices()); log.debug(第 {} 页获取到 {} 个设备, pageCount, pageResult.getDevices().size()); // 更新marker为下一页的标记 marker pageResult.getNextMarker(); } else { log.debug(第 {} 页无设备数据停止分页, pageCount); marker null; } // 防止无限循环最多获取100页5000个设备 if (pageCount 100) { log.warn(已达到最大分页数 {}停止获取, pageCount); break; } } while (marker ! null !marker.isEmpty()); log.info(分页获取完成共 {} 页总计 {} 个设备, pageCount, allDevices.size()); return allDevices; } catch (Exception e) { log.error(获取设备列表失败: {}, e.getMessage()); return new ArrayList(); } } /** * 分页获取设备列表 */ private PageResult getDevicesByPage(String marker, int limit) { try { String token getAccessToken(); String url baseUrl /v5/iot/ projectId /devices?limit limit; if (marker ! null !marker.isEmpty()) { url marker marker; } // 可以根据需要添加产品ID过滤 if (productId ! null !productId.isEmpty()) { url product_id productId; } log.debug(请求设备列表URL: {}, url); HttpHeaders headers new HttpHeaders(); headers.set(X-Auth-Token, token); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntityString entity new HttpEntity(headers); ResponseEntityString response restTemplate.exchange(url, HttpMethod.GET, entity, String.class); log.debug(设备列表响应状态码: {}, response.getStatusCode()); if (response.getStatusCode() HttpStatus.OK) { String responseBody response.getBody(); log.debug(设备列表响应内容: {}, responseBody); JSONObject result JSON.parseObject(responseBody); JSONArray devices result.getJSONArray(devices); JSONObject page result.getJSONObject(page); ListDeviceInfo deviceList new ArrayList(); if (devices ! null) { for (int i 0; i devices.size(); i) { JSONObject device devices.getJSONObject(i); DeviceInfo deviceInfo convertToDeviceInfo(device); if (deviceInfo ! null) { deviceList.add(deviceInfo); } } } // 获取下一页的标记 String nextMarker null; if (page ! null) { nextMarker page.getString(marker); log.debug(分页信息 - count: {}, nextMarker: {}, page.getLong(count), nextMarker); } return new PageResult(deviceList, nextMarker); } else { log.error(获取设备列表失败状态码: {}, 响应: {}, response.getStatusCode(), response.getBody()); } } catch (Exception e) { log.error(分页获取设备列表失败: {}, e.getMessage()); } return new PageResult(new ArrayList(), null); } }主要改进点新增PageResult内部类封装分页结果包含设备列表和下一页标记完善分页逻辑正确处理marker参数实现完整的分页遍历添加产品ID过滤在URL中添加product_id参数只获取指定产品的设备增强日志输出详细记录分页过程和分页信息安全限制最多获取100页5000个设备防止无限循环其他优化建议1. 考虑使用灵活搜索接口根据API文档您还可以使用灵活搜索接口来获取设备/** * 使用SQL语句灵活搜索设备 */ public ListDeviceInfo searchDevicesBySql(String sql) { try { String token getAccessToken(); String url baseUrl /v5/iot/ projectId /search/query-devices; HttpHeaders headers new HttpHeaders(); headers.set(X-Auth-Token, token); headers.setContentType(MediaType.APPLICATION_JSON); JSONObject requestBody new JSONObject(); requestBody.put(sql, sql); HttpEntityString entity new HttpEntity(requestBody.toJSONString(), headers); ResponseEntityString response restTemplate.exchange(url, HttpMethod.POST, entity, String.class); if (response.getStatusCode() HttpStatus.OK) { JSONObject result JSON.parseObject(response.getBody()); JSONArray devices result.getJSONArray(devices); ListDeviceInfo deviceList new ArrayList(); if (devices ! null) { for (int i 0; i devices.size(); i) { JSONObject device devices.getJSONObject(i); DeviceInfo deviceInfo convertToDeviceInfo(device); if (deviceInfo ! null) { deviceList.add(deviceInfo); } } } return deviceList; } } catch (Exception e) { log.error(SQL搜索设备失败: {}, e.getMessage()); } return new ArrayList(); } // 使用示例获取所有设备 // ListDeviceInfo devices searchDevicesBySql(select * from device);2. 添加实例ID支持根据API文档建议在请求头中添加Instance-Id// 在配置文件中添加 Value(${huawei.iot.instance-id:}) private String instanceId; // 在请求头中添加 if (instanceId ! null !instanceId.isEmpty()) { headers.set(Instance-Id, instanceId); }这样修改后您的设备同步功能将能够获取到所有的设备数据而不仅仅是第一页的50个设备。我们面临的问题是在HuaweiIotServiceImpl中获取设备列表的方法getAllDevices()目前没有处理分页因为getNextMarker()返回null所以只能获取第一页的数据。根据华为云IoT API文档查询设备列表接口支持分页响应中包含了page对象其中有一个marker字段用于获取下一页。因此我们需要修改getAllDevices方法使其能够处理分页直到获取所有设备。步骤在getDevicesByPage方法中我们不仅要返回设备列表还要返回下一页的marker。修改getDevicesByPage方法使其返回一个包含设备列表和下一页marker的对象。在getAllDevices方法中根据返回的marker持续获取直到没有下一页。但是由于目前getDevicesByPage方法中我们只是解析了设备列表没有解析分页信息。所以我们需要修改这个方法同时解析分页信息。根据华为云API文档响应中有一个page对象其中包含count和marker。我们需要的正是这个marker。因此我们修改getDevicesByPage方法使其返回一个自定义的PageResult对象该对象包含设备列表和下一页的marker。由于代码中已经有一个Page类但它是用于响应中的Page对象为了避免混淆我们创建一个内部类或使用Pair等。这里我们创建一个内部类。但是为了最小化改动我们可以直接返回一个Map或者创建一个新的类。这里我们创建一个静态内部类。由于我们是在同一个类中我们可以创建一个内部类来保存分页结果。我们创建一个名为DevicePageResult的内部类包含两个字段ListDeviceInfo devices 和 String nextMarker。然后修改getDevicesByPage方法返回DevicePageResult。然后在getAllDevices方法中使用这个nextMarker来循环获取。同时我们注意到华为云API文档中说明marker是上一次分页查询结果中最后一条记录的ID我们在请求中传入marker参数即可获取下一页。另外我们还需要注意华为云API分页限制每页最多50条所以我们设置limit50。修改步骤在HuaweiIotServiceImpl中创建一个内部类DevicePageResult包含devices和nextMarker。修改getDevicesByPage方法返回DevicePageResult并在方法中解析响应中的page对象获取marker。在getAllDevices方法中循环调用getDevicesByPage直到nextMarker为null或空字符串。注意华为云API文档中说明当没有更多数据时返回的page对象中marker为null或者不存在我们看响应示例中是有marker的但是当没有更多数据时可能返回的marker为null或者空字符串我们根据实际情况处理。我们根据响应中的page.marker是否存在来判断是否还有下一页。另外我们还要注意如果响应中没有page对象那么说明没有分页信息我们就按照没有下一页处理。我们修改getDevicesByPage方法在解析响应时不仅解析devices还解析page。代码修改首先创建内部类private static class DevicePageResult {private ListDeviceInfo devices;private String nextMarker;public DevicePageResult(ListDeviceInfo devices, String nextMarker) {this.devices devices;this.nextMarker nextMarker;}}然后修改getDevicesByPage方法返回DevicePageResult。在getDevicesByPage方法中我们解析响应获取devices和page然后从page中获取marker。注意响应中的page可能为null所以我们要判断。修改getDevicesByPage方法private DevicePageResult getDevicesByPage(String marker, int limit) {try {... // 原有代码if (response.getStatusCode() HttpStatus.OK) {String responseBody response.getBody();log.debug(设备列表响应内容: {}, responseBody);JSONObject result JSON.parseObject(responseBody);JSONArray devices result.getJSONArray(devices);JSONObject page result.getJSONObject(page);ListDeviceInfo deviceList new ArrayList();if (devices ! null) {for (int i 0; i devices.size(); i) {JSONObject device devices.getJSONObject(i);DeviceInfo deviceInfo convertToDeviceInfo(device);if (deviceInfo ! null) {deviceList.add(deviceInfo);}}}String nextMarker null;if (page ! null) {nextMarker page.getString(marker);}return new DevicePageResult(deviceList, nextMarker);} else {... // 原有错误处理}} catch (Exception e) {... // 原有异常处理}return new DevicePageResult(new ArrayList(), null);}然后修改getAllDevices方法Overridepublic ListDeviceInfo getAllDevices() {ListDeviceInfo allDevices new ArrayList();String marker null;int pageSize 50; // 华为云限制最大50int pageCount 0;try {log.info(开始分页获取设备列表每页大小: {}, pageSize);do {pageCount;log.debug(获取第 {} 页设备数据marker: {}, pageCount, marker);DevicePageResult pageResult getDevicesByPage(marker, pageSize);if (pageResult.devices ! null !pageResult.devices.isEmpty()) {allDevices.addAll(pageResult.devices);log.debug(第 {} 页获取到 {} 个设备, pageCount, pageResult.devices.size());}// 更新marker为下一页的markermarker pageResult.nextMarker;// 如果下一页marker为null或空字符串则停止if (marker null || marker.trim().isEmpty()) {log.debug(第 {} 页后无更多数据停止分页, pageCount);break;}// 防止无限循环最多获取100页5000个设备一般足够if (pageCount 100) {log.warn(已达到最大分页数 {}停止获取, pageCount);break;}} while (marker ! null);log.info(分页获取完成共 {} 页总计 {} 个设备, pageCount, allDevices.size());return allDevices;} catch (Exception e) {log.error(获取设备列表失败: {}, e.getMessage());return new ArrayList();}}这样我们就实现了分页获取所有设备。注意我们设置了一个最大分页数100防止无限循环相当于最多获取5000个设备。如果设备数量超过5000可能需要调整这个值或者考虑其他方式。另外我们还需要注意华为云API的marker是上一次查询结果中最后一条记录的ID我们每次请求都传入上一次响应中的marker即可。我们修改后应该能够获取所有设备了。但是我们还需要注意如果设备数量为0那么第一次调用getDevicesByPage返回的devices为空同时nextMarker也为空所以会直接退出循环。另外我们还要注意如果第一次调用getDevicesByPage就失败那么返回的DevicePageResult中devices为空nextMarker为null所以会退出循环并返回空列表。这样修改后应该可以解决分页问题。但是我们还需要修改一下在getDevicesByPage方法中如果响应状态不是200我们返回的DevicePageResult中的nextMarker应该为null避免继续分页。我们已经在getDevicesByPage方法中在异常情况下返回一个空的DevicePageResult其中nextMarker为null所以会退出循环。因此这个修改是安全的。现在我们开始修改代码。