diff --git a/stwzhj-modules/stwzhj-data2es/src/main/java/org/dromara/data2es/domain/entity/GpsInfoEntity.java b/stwzhj-modules/stwzhj-data2es/src/main/java/org/dromara/data2es/domain/entity/GpsInfoEntity.java index 959c0aa9..0f835e22 100644 --- a/stwzhj-modules/stwzhj-data2es/src/main/java/org/dromara/data2es/domain/entity/GpsInfoEntity.java +++ b/stwzhj-modules/stwzhj-data2es/src/main/java/org/dromara/data2es/domain/entity/GpsInfoEntity.java @@ -27,7 +27,17 @@ public class GpsInfoEntity implements Serializable { */ private String deviceType; + private String zzjgdm; + private String zzjgmc; + + private String policeNo; + + private String policeName; + + private String phoneNum; + + private String carNum; private Double[] location; @@ -45,5 +55,7 @@ public class GpsInfoEntity implements Serializable { //地市代码 3401,3402 private String infoSource; + private Integer online; + } diff --git a/stwzhj-modules/stwzhj-data2es/src/main/java/org/dromara/data2es/service/impl/GpsServiceImpl.java b/stwzhj-modules/stwzhj-data2es/src/main/java/org/dromara/data2es/service/impl/GpsServiceImpl.java index 087c518a..0afab091 100644 --- a/stwzhj-modules/stwzhj-data2es/src/main/java/org/dromara/data2es/service/impl/GpsServiceImpl.java +++ b/stwzhj-modules/stwzhj-data2es/src/main/java/org/dromara/data2es/service/impl/GpsServiceImpl.java @@ -136,6 +136,7 @@ public class GpsServiceImpl implements IGpsService { @Override public R updateDataStatus(List esGpsInfoVO2s) { Map onlineUserDataMap = new HashMap<>(); + BulkRequest bulkRequest = new BulkRequest(); for (EsGpsInfoVO2 info : esGpsInfoVO2s) { String zzjgdm = info.getZzjgdm(); String deviceCode = info.getDeviceCode(); @@ -147,7 +148,10 @@ public class GpsServiceImpl implements IGpsService { ":" + deviceCode; onlineUserDataMap.put(onlineUsersKey, jsonValue); requestHandler.sendToKafka(info); + IndexRequest indexRequest = buildEsIndexRequest(info); + bulkRequest.add(indexRequest); } + requestHandler.esRealBulkSave(bulkRequest); requestHandler.redisOnlineUserBatch(onlineUserDataMap, 864000); //存放10天 return R.ok(); } @@ -216,7 +220,7 @@ public class GpsServiceImpl implements IGpsService { return esGpsInfoVO2; } - private IndexRequest buildEsIndexRequest(EsGpsInfo esGpsInfo) { + private IndexRequest buildEsIndexRequest(EsGpsInfoVO2 esGpsInfo) { IndexRequest request = getIndexRequest(esGpsInfo); return request; } @@ -300,7 +304,7 @@ public class GpsServiceImpl implements IGpsService { * @param esGpsInfo * @return */ - public IndexRequest getIndexRequest(EsGpsInfo esGpsInfo){ + public IndexRequest getIndexRequest(EsGpsInfoVO2 esGpsInfo){ GpsInfoEntity gpsInfoEntity = new GpsInfoEntity(); BeanUtil.copyProperties(esGpsInfo,gpsInfoEntity); double lng ; diff --git a/stwzhj-modules/stwzhj-location/src/main/java/org/dromara/location/controller/LocationController.java b/stwzhj-modules/stwzhj-location/src/main/java/org/dromara/location/controller/LocationController.java index 41ba29ad..b9241474 100644 --- a/stwzhj-modules/stwzhj-location/src/main/java/org/dromara/location/controller/LocationController.java +++ b/stwzhj-modules/stwzhj-location/src/main/java/org/dromara/location/controller/LocationController.java @@ -8,6 +8,7 @@ import com.alibaba.fastjson.JSONArray; import org.apache.dubbo.config.annotation.DubboReference; import org.dromara.common.core.domain.R; import org.dromara.common.redis.utils.RedisUtils; +import org.dromara.location.service.ISearchService; import org.dromara.system.api.RemoteDeviceService; import org.dromara.system.api.domain.bo.RemoteDeviceBo; import org.dromara.system.api.domain.vo.RemoteDeviceVo; @@ -16,12 +17,17 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.util.CollectionUtils; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import redis.clients.jedis.resps.GeoRadiusResponse; -import javax.annotation.Resource; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; import java.util.*; @Configuration @@ -32,6 +38,9 @@ public class LocationController { @DubboReference private RemoteDeviceService deviceService; + @Resource + private ISearchService searchService; + Logger logger = LoggerFactory.getLogger(LocationController.class); @@ -149,21 +158,6 @@ public class LocationController { String lat = params.get("lat").toString(); String lng = params.get("lng").toString(); String dist = params.get("distance").toString(); - /* List geoRadiusResponses = redisUtil.nearByXYReadonly(RedisConstants.ONLINE_USERS_GEO, - Double.parseDouble(lng), Double.parseDouble(lat), Double.parseDouble(dist)); - List list = new ArrayList<>(); - for (GeoRadiusResponse geoRadiusRespons : geoRadiusResponses) { - String memberByString = geoRadiusRespons.getMemberByString(); - logger.info("member:"+memberByString); - String[] strs = memberByString.split("#"); - logger.info("key值:"+keys+":"+strs[0]+":"+strs[1]+":"+strs[2]); - Object object = redisUtil.get(keys+":"+strs[0]+":"+strs[1]+":"+strs[2]); - if (null != object){ - Device device = FastJSONUtil.parsePojo(object.toString(), Device.class); - //device = rebuildDevice(device); - list.add(device); - } - }*/ return R.ok(); } @@ -190,11 +184,6 @@ public class LocationController { device.setValid(1); device.setDeviceType(type); device.setPoliceName(name); - /*if("01".equals(type) || "02".equals(type) || "06".equals(type) ||"07".equals(type) ||"08".equals(type) || "09".equals(type)){ - device.setCarNum(name); - }else{ - device.setPoliceNo(name); - }*/ List devices = new ArrayList<>(); if (!"".equals(zzjgdms)){ //前端选择机构时 String[] zzjgdm = zzjgdms.split(","); @@ -230,6 +219,290 @@ public class LocationController { return zzjgdm; } + /** + * 从Elasticsearch获取所有设备最新位置数据 + * 支持条件:type(设备类型,单个或多个逗号分隔,不传则查询所有)、deptId(部门ID,单个)、zzjgdm(组织机构代码,多个用逗号分隔,与deptId二选一)、online(在线状态:1-在线,0-离线) + * 可视范围查询:minLng(最小经度)、minLat(最小纬度)、maxLng(最大经度)、maxLat(最大纬度) + * deviceCode(设备编码,单个或多个逗号分隔,不传则查询所有) + */ + @PostMapping("/getAllLocationFromEs") + public R getAllLocationFromEs(@RequestBody Map params){ + logger.info("从ES获取设备最新位置,参数: {}", params); + String type = params.get("type") != null ? params.get("type").toString() : null; + String deptId = params.get("deptId") != null ? params.get("deptId").toString() : null; + String zzjgdm = params.get("zzjgdm") != null ? params.get("zzjgdm").toString() : null; + Integer online = params.get("online") != null ? Convert.toInt(params.get("online")) : null; + Double minLng = params.get("minLng") != null ? Convert.toDouble(params.get("minLng")) : null; + Double minLat = params.get("minLat") != null ? Convert.toDouble(params.get("minLat")) : null; + Double maxLng = params.get("maxLng") != null ? Convert.toDouble(params.get("maxLng")) : null; + Double maxLat = params.get("maxLat") != null ? Convert.toDouble(params.get("maxLat")) : null; + String deviceCode = params.get("deviceCode") != null ? params.get("deviceCode").toString() : null; + List> list = searchService.getAllLatestLocationFromEs(type, deptId, zzjgdm, online, minLng, minLat, maxLng, maxLat, deviceCode); + list.removeAll(Collections.singleton(null)); + logger.info("从ES查询到设备数量: {}", list.size()); + return R.ok(list); + } + + /** + * ASCII协议实时定位接口 - GET方式 + * 请求和响应消息以ASCII形式给出,适用于定时请求获取实时定位数据 + * + * 请求参数格式(ASCII): + * type: 设备类型(可选,多个用逗号分隔) + * deptId: 部门ID(可选,单个,与zzjgdm二选一) + * zzjgdm: 组织机构代码(可选,多个用逗号分隔,与deptId二选一) + * online: 在线状态(可选,1-在线,0-离线) + * minLng: 最小经度(可选) + * minLat: 最小纬度(可选) + * maxLng: 最大经度(可选) + * maxLat: 最大纬度(可选) + * deviceCode: 设备编码(可选,多个用逗号分隔) + * + * 响应格式(ASCII): + * STATUS=OK|COUNT=10 + * DEVICE:deviceCode=001,deviceName=测试设备,lng=116.397,lat=39.908,online=1,time=2024-01-01 12:00:00 + * DEVICE:deviceCode=002,deviceName=测试设备2,lng=116.398,lat=39.909,online=1,time=2024-01-01 12:00:01 + * ... + * END + * + * 错误响应格式: + * STATUS=ERROR|MESSAGE=错误描述 + */ + @GetMapping("/getAllLocationAscii") + public void getAllLocationAscii( + @RequestParam(required = false) String type, + @RequestParam(required = false) String deptId, + @RequestParam(required = false) String zzjgdm, + @RequestParam(required = false) Integer online, + @RequestParam(required = false) Double minLng, + @RequestParam(required = false) Double minLat, + @RequestParam(required = false) Double maxLng, + @RequestParam(required = false) Double maxLat, + @RequestParam(required = false) String deviceCode, + HttpServletResponse response) { + response.setContentType("text/plain;charset=US-ASCII"); + response.setCharacterEncoding("US-ASCII"); + + try (PrintWriter writer = response.getWriter()) { + logger.info("ASCII协议获取设备位置 - type={}, deptId={}, zzjgdm={}, online={}, deviceCode={}, bounds=[{},{},{},{}]", + type, deptId, zzjgdm, online, deviceCode, minLng, minLat, maxLng, maxLat); + + List> list = searchService.getAllLatestLocationFromEs(type, deptId, zzjgdm, online, minLng, minLat, maxLng, maxLat, deviceCode); + list.removeAll(Collections.singleton(null)); + + // ASCII格式响应头 + writer.print("STATUS=OK|COUNT="); + writer.println(list.size()); + + // 每条设备数据 + for (Map item : list) { + StringBuilder sb = new StringBuilder(); + sb.append("DEVICE:"); + + boolean first = true; + for (Map.Entry entry : item.entrySet()) { + if (!first) { + sb.append(","); + } + first = false; + sb.append(escapeAscii(entry.getKey())); + sb.append("="); + Object value = entry.getValue(); + sb.append(value != null ? escapeAscii(value.toString()) : ""); + } + + writer.println(sb.toString()); + } + + // 结束标记 + writer.println("END"); + writer.flush(); + + logger.info("ASCII协议返回设备数量: {}", list.size()); + + } catch (Exception e) { + logger.error("ASCII协议接口异常", e); + try (PrintWriter writer = response.getWriter()) { + writer.print("STATUS=ERROR|MESSAGE="); + writer.println(escapeAscii(e.getMessage() != null ? e.getMessage() : "Unknown error")); + writer.flush(); + } catch (IOException ioEx) { + logger.error("写入错误响应失败", ioEx); + } + } + } + + /** + * ASCII协议实时定位接口 - POST方式 + * 支持POST请求,参数以JSON格式放在请求体中,JSON中包含ASCII字段 + * + * 请求体格式(JSON): + * { + * "ASCII": "type=01,02\nonline=1\nminLng=116.0\nminLat=39.0\nmaxLng=117.0\nmaxLat=40.0" + * } + * + * 响应格式同GET方式 + */ + @PostMapping(value = "/getAllLocationAscii") + public R>> getAllLocationAsciiPost(@RequestBody Map params) { + // 解析JSON中的ASCII字段,使用数组包装以支持lambda赋值 + String[] typeHolder = new String[1]; + String[] deptIdHolder = new String[1]; + String[] zzjgdmHolder = new String[1]; + Integer[] onlineHolder = new Integer[1]; + Double[] minLngHolder = new Double[1]; + Double[] minLatHolder = new Double[1]; + Double[] maxLngHolder = new Double[1]; + Double[] maxLatHolder = new Double[1]; + String[] deviceCodeHolder = new String[1]; + + logger.info("接收到的原始参数: {}", params); + + // 支持 ASCII 和 ascii 两种key + Object asciiObj = params.get("ASCII"); + if (asciiObj == null) { + asciiObj = params.get("ascii"); + } + String asciiBody = asciiObj != null ? asciiObj.toString() : null; + + logger.info("解析到的ASCII内容: [{}]", asciiBody); + + if (asciiBody != null && !asciiBody.isEmpty()) { + // 逐个解析已知的key + parseAndSet(asciiBody, "type", v -> typeHolder[0] = v); + parseAndSet(asciiBody, "deptId", v -> deptIdHolder[0] = v); + parseAndSet(asciiBody, "zzjgdm", v -> zzjgdmHolder[0] = v); + parseAndSetInt(asciiBody, "online", v -> onlineHolder[0] = v); + parseAndSetDouble(asciiBody, "minLng", v -> minLngHolder[0] = v); + parseAndSetDouble(asciiBody, "minLat", v -> minLatHolder[0] = v); + parseAndSetDouble(asciiBody, "maxLng", v -> maxLngHolder[0] = v); + parseAndSetDouble(asciiBody, "maxLat", v -> maxLatHolder[0] = v); + parseAndSet(asciiBody, "deviceCode", v -> deviceCodeHolder[0] = v); + } + + String type = typeHolder[0]; + String deptId = deptIdHolder[0]; + String zzjgdm = zzjgdmHolder[0]; + Integer online = onlineHolder[0]; + Double minLng = minLngHolder[0]; + Double minLat = minLatHolder[0]; + Double maxLng = maxLngHolder[0]; + Double maxLat = maxLatHolder[0]; + String deviceCode = deviceCodeHolder[0]; + + logger.info("ASCII协议POST获取设备位置 - type={}, deptId={}, zzjgdm={}, online={}, deviceCode={}", type, deptId, zzjgdm, online, deviceCode); + + List> list = searchService.getAllLatestLocationFromEs(type, deptId, zzjgdm, online, minLng, minLat, maxLng, maxLat, deviceCode); + list.removeAll(Collections.singleton(null)); + + // 为每个对象添加ASCII字段 + for (Map item : list) { + StringBuilder sb = new StringBuilder(); + sb.append("DEVICE:"); + boolean first = true; + for (Map.Entry entry : item.entrySet()) { + if (!first) sb.append(","); + first = false; + sb.append(escapeAscii(entry.getKey())); + sb.append("="); + Object val = entry.getValue(); + sb.append(val != null ? escapeAscii(val.toString()) : ""); + } + item.put("ASCII", sb.toString()); + } + + logger.info("ASCII协议POST返回设备数量: {}", list.size()); + + return R.ok(list); + } + + /** + * 转义字符串为安全的ASCII格式 + * 将非ASCII字符和特殊字符进行转义 + */ + private String escapeAscii(String input) { + if (input == null) { + return ""; + } + StringBuilder sb = new StringBuilder(); + for (char c : input.toCharArray()) { + if (c >= 32 && c <= 126 && c != '=' && c != ',' && c != '|' && c != ':') { + sb.append(c); + } else { + // 转义特殊字符: = , | : 以及非ASCII字符 + sb.append(String.format("%%%02X", (int) c)); + } + } + return sb.toString(); + } + + /** + * 从ASCII字符串中解析指定key的值并设置 + */ + private void parseAndSet(String content, String key, java.util.function.Consumer setter) { + int idx = content.indexOf(key + "="); + if (idx >= 0) { + int start = idx + key.length() + 1; + // 找到value的结束位置:换行、分号、空格或字符串结尾 + int end = content.length(); + for (int i = start; i < content.length(); i++) { + char c = content.charAt(i); + if (c == '\n' || c == '\r' || c == ';' || c == ' ') { + end = i; + break; + } + } + String value = content.substring(start, end).trim(); + logger.info("解析到key=[{}], value=[{}]", key, value); + setter.accept(value); + } + } + + /** + * 解析Integer类型参数 + */ + private void parseAndSetInt(String content, String key, java.util.function.Consumer setter) { + int idx = content.indexOf(key + "="); + if (idx >= 0) { + int start = idx + key.length() + 1; + int end = content.length(); + for (int i = start; i < content.length(); i++) { + char c = content.charAt(i); + if (c == '\n' || c == '\r' || c == ';' || c == ' ') { + end = i; + break; + } + } + String value = content.substring(start, end).trim(); + logger.info("解析到key=[{}], value=[{}]", key, value); + if (!value.isEmpty()) { + setter.accept(Convert.toInt(value)); + } + } + } + + /** + * 解析Double类型参数 + */ + private void parseAndSetDouble(String content, String key, java.util.function.Consumer setter) { + int idx = content.indexOf(key + "="); + if (idx >= 0) { + int start = idx + key.length() + 1; + int end = content.length(); + for (int i = start; i < content.length(); i++) { + char c = content.charAt(i); + if (c == '\n' || c == '\r' || c == ';' || c == ' ') { + end = i; + break; + } + } + String value = content.substring(start, end).trim(); + logger.info("解析到key=[{}], value=[{}]", key, value); + if (!value.isEmpty()) { + setter.accept(Convert.toDouble(value)); + } + } + } } diff --git a/stwzhj-modules/stwzhj-location/src/main/java/org/dromara/location/service/ISearchService.java b/stwzhj-modules/stwzhj-location/src/main/java/org/dromara/location/service/ISearchService.java index b57e17b1..ff384242 100644 --- a/stwzhj-modules/stwzhj-location/src/main/java/org/dromara/location/service/ISearchService.java +++ b/stwzhj-modules/stwzhj-location/src/main/java/org/dromara/location/service/ISearchService.java @@ -13,5 +13,19 @@ public interface ISearchService { List queryDistinctDevicesNearPoint(QueryBuilder spatialQuery, String indexName) throws IOException; + /** + * 从Elasticsearch获取所有设备的最新位置数据 + * @param type 设备类型,单个或多个用逗号分隔,不传则查询所有类型 + * @param deptId 部门ID(单个,与zzjgdm二选一) + * @param zzjgdm 组织机构代码(多个用逗号分隔,与deptId二选一) + * @param online 在线状态:1-在线,0-离线,null-全部 + * @param minLng 边界框最小经度 + * @param minLat 边界框最小纬度 + * @param maxLng 边界框最大经度 + * @param maxLat 边界框最大纬度 + * @param deviceCode 设备编码,单个或多个用逗号分隔,不传则查询所有设备 + * @return 设备最新位置列表 + */ + List> getAllLatestLocationFromEs(String type, String deptId, String zzjgdm, Integer online, Double minLng, Double minLat, Double maxLng, Double maxLat, String deviceCode); } diff --git a/stwzhj-modules/stwzhj-location/src/main/java/org/dromara/location/service/impl/SearchServiceImpl.java b/stwzhj-modules/stwzhj-location/src/main/java/org/dromara/location/service/impl/SearchServiceImpl.java index 37df8370..a8d414b8 100644 --- a/stwzhj-modules/stwzhj-location/src/main/java/org/dromara/location/service/impl/SearchServiceImpl.java +++ b/stwzhj-modules/stwzhj-location/src/main/java/org/dromara/location/service/impl/SearchServiceImpl.java @@ -227,4 +227,180 @@ public class SearchServiceImpl implements ISearchService { }; } + @Override + public List> getAllLatestLocationFromEs(String type, String deptId, String zzjgdm, Integer online, Double minLng, Double minLat, Double maxLng, Double maxLat, String deviceCode) { + List> resultList = new ArrayList<>(); + List indexList = getRecentIndexList(); + log.info("查询ES索引列表: {}", indexList); + + if (indexList.isEmpty()) { + return resultList; + } + + try { + // 构建查询条件 + BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); + + // 设备类型条件 - 支持单个、多个逗号分隔或不传 + if (type != null && !type.isEmpty()) { + String[] types = type.split(","); + BoolQueryBuilder typeQuery = QueryBuilders.boolQuery(); + typeQuery.must(QueryBuilders.existsQuery("deviceType")); + if (types.length == 1) { + typeQuery.must(QueryBuilders.termQuery("deviceType", types[0])); + } else { + typeQuery.must(QueryBuilders.termsQuery("deviceType", types)); + } + boolQuery.must(typeQuery); + } + + // 设备编码条件 - 支持单个、多个逗号分隔或不传 + if (deviceCode != null && !deviceCode.isEmpty()) { + String[] deviceCodes = deviceCode.split(","); + BoolQueryBuilder deviceCodeQuery = QueryBuilders.boolQuery(); + deviceCodeQuery.must(QueryBuilders.existsQuery("deviceCode")); + if (deviceCodes.length == 1) { + deviceCodeQuery.must(QueryBuilders.termQuery("deviceCode", deviceCodes[0])); + } else { + deviceCodeQuery.must(QueryBuilders.termsQuery("deviceCode", deviceCodes)); + } + boolQuery.must(deviceCodeQuery); + } + + // 部门ID条件(单个)- 使用zzjgdm字段 + if (deptId != null && !deptId.isEmpty()) { + String deptPattern = deptIdSub(deptId); + BoolQueryBuilder deptQuery = QueryBuilders.boolQuery(); + deptQuery.must(QueryBuilders.existsQuery("zzjgdm")); + if (deptPattern.endsWith("*")) { + deptPattern = deptPattern.substring(0, deptPattern.length() - 1); + deptQuery.must(QueryBuilders.prefixQuery("zzjgdm", deptPattern)); + } else { + deptQuery.must(QueryBuilders.termQuery("zzjgdm", deptId)); + } + boolQuery.must(deptQuery); + } + + // 组织机构代码条件(多个用逗号分隔)- 使用zzjgdm字段 + if (zzjgdm != null && !zzjgdm.isEmpty()) { + String[] zzjgdms = zzjgdm.split(","); + BoolQueryBuilder zzjgdmBoolQuery = QueryBuilders.boolQuery(); + zzjgdmBoolQuery.must(QueryBuilders.existsQuery("zzjgdm")); + if (zzjgdms.length == 1) { + String deptPattern = deptIdSub(zzjgdms[0]); + if (deptPattern.endsWith("*")) { + deptPattern = deptPattern.substring(0, deptPattern.length() - 1); + zzjgdmBoolQuery.must(QueryBuilders.prefixQuery("zzjgdm", deptPattern)); + } else { + zzjgdmBoolQuery.must(QueryBuilders.termQuery("zzjgdm", zzjgdms[0])); + } + } else { + BoolQueryBuilder shouldQuery = QueryBuilders.boolQuery(); + for (String zzjg : zzjgdms) { + String deptPattern = deptIdSub(zzjg); + if (deptPattern.endsWith("*")) { + deptPattern = deptPattern.substring(0, deptPattern.length() - 1); + shouldQuery.should(QueryBuilders.prefixQuery("zzjgdm", deptPattern)); + } else { + shouldQuery.should(QueryBuilders.termQuery("zzjgdm", zzjg)); + } + } + shouldQuery.minimumShouldMatch(1); + zzjgdmBoolQuery.must(shouldQuery); + } + boolQuery.must(zzjgdmBoolQuery); + } + + // 在线状态条件 + if (online != null) { + BoolQueryBuilder onlineQuery = QueryBuilders.boolQuery(); + onlineQuery.must(QueryBuilders.existsQuery("online")); + onlineQuery.must(QueryBuilders.termQuery("online", online)); + boolQuery.must(onlineQuery); + } + + // 可视范围(边界框)查询 - 当四个边界参数都不为空时添加 + if (minLng != null && minLat != null && maxLng != null && maxLat != null) { + BoolQueryBuilder locationQuery = QueryBuilders.boolQuery(); + locationQuery.must(QueryBuilders.existsQuery("location")); + locationQuery.must(QueryBuilders.geoBoundingBoxQuery("location") + .setCorners(maxLat, minLng, minLat, maxLng)); + boolQuery.must(locationQuery); + } + + // 使用聚合查询获取每个设备的最新记录 + TermsAggregationBuilder aggregation = AggregationBuilders.terms("distinct_devices") + .field("deviceCode") + .size(10000) + .shardSize(20000) + .subAggregation( + AggregationBuilders.topHits("latest_record") + .size(1) + .sort("gpsTime", SortOrder.DESC) + ); + + SearchSourceBuilder sourceBuilder = new SearchSourceBuilder() + .query(boolQuery) + .aggregation(aggregation) + .size(0); + + SearchRequest request = new SearchRequest(indexList.toArray(new String[0])) + .source(sourceBuilder) + .indicesOptions(IndicesOptions.lenientExpandOpen()); + + log.info("ES查询DSL: {}", sourceBuilder.toString()); + + // 执行查询 + SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT); + + // 解析聚合结果 + Terms terms = response.getAggregations().get("distinct_devices"); + log.info("查询到设备数量: {}", terms.getBuckets().size()); + + for (Terms.Bucket bucket : terms.getBuckets()) { + TopHits topHits = bucket.getAggregations().get("latest_record"); + SearchHit[] hits = topHits.getHits().getHits(); + if (hits.length > 0) { + Map source = hits[0].getSourceAsMap(); + resultList.add(source); + } + } + + } catch (Exception e) { + log.error("从ES查询设备最新位置异常", e); + } + + return resultList; + } + + /** + * 获取最近7天的索引列表 + */ + private List getRecentIndexList() { + List indexList = new ArrayList<>(); + DateTime now = DateUtil.date(); + for (int i = 0; i < 1; i++) { + DateTime dateTime = DateUtil.offsetDay(now, -i); + String dateStr = dateTime.toString("yyyyMMdd"); + indexList.add("rs_gpsinfo" + dateStr); + } + return indexList; + } + + /** + * 部门ID处理,与controller中的deptIdSub保持一致 + */ + private String deptIdSub(String zzjgdm) { + if (zzjgdm.endsWith("0000000000")) { // 省厅 即全部 + zzjgdm = zzjgdm.substring(0, 2) + "*"; + } else if (zzjgdm.endsWith("00000000")) { //地市 + zzjgdm = zzjgdm.substring(0, 4) + "*"; + } else if (zzjgdm.endsWith("000000")) { // 分局 + zzjgdm = zzjgdm.substring(0, 6) + "*"; + } else { // 支队 + zzjgdm = zzjgdm.substring(0, 8) + "*"; + } + return zzjgdm; + } + }