From 0f03281f4240a6dbfda338c5182ebcf1de7f1ebf4da80c9d5ca9acfb9a418076 Mon Sep 17 00:00:00 2001 From: PC <2413103649@qq.com> Date: Tue, 3 Mar 2026 17:36:58 +0800 Subject: [PATCH] =?UTF-8?q?=E8=BF=90=E8=90=A5=E5=8C=BA=E8=BE=B9=E7=BC=98?= =?UTF-8?q?=E8=AE=A1=E7=AE=97=EF=BC=9A=E5=9F=BA=E7=A1=80=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feign/model/dto/FeignEbikeRegionDto.java | 2 - .../service/impl/EbikeRegionServiceImpl.java | 8 +- .../com/cdzy/report/config/JacksonConfig.java | 10 + .../report/handler/PointDeserializer.java | 246 ++++++++++ .../cdzy/report/handler/PointSerializer.java | 108 +++++ .../cdzy/report/handler/PointTypeHandler.java | 143 ++++++ .../report/handler/PolygonDeserializer.java | 243 ++++++++++ .../report/handler/PolygonSerializer.java | 170 +++++++ .../report/handler/PolygonTypeHandler.java | 429 ++++++++++++++++++ .../java/com/cdzy/report/utils/RedisUtil.java | 9 + 10 files changed, 1364 insertions(+), 4 deletions(-) create mode 100644 ebike-report/src/main/java/com/cdzy/report/handler/PointDeserializer.java create mode 100644 ebike-report/src/main/java/com/cdzy/report/handler/PointSerializer.java create mode 100644 ebike-report/src/main/java/com/cdzy/report/handler/PointTypeHandler.java create mode 100644 ebike-report/src/main/java/com/cdzy/report/handler/PolygonDeserializer.java create mode 100644 ebike-report/src/main/java/com/cdzy/report/handler/PolygonSerializer.java create mode 100644 ebike-report/src/main/java/com/cdzy/report/handler/PolygonTypeHandler.java diff --git a/ebike-feign/src/main/java/com/ebike/feign/model/dto/FeignEbikeRegionDto.java b/ebike-feign/src/main/java/com/ebike/feign/model/dto/FeignEbikeRegionDto.java index d5daa53..9a10ce2 100644 --- a/ebike-feign/src/main/java/com/ebike/feign/model/dto/FeignEbikeRegionDto.java +++ b/ebike-feign/src/main/java/com/ebike/feign/model/dto/FeignEbikeRegionDto.java @@ -1,7 +1,6 @@ package com.ebike.feign.model.dto; -import com.mybatisflex.annotation.Id; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -30,7 +29,6 @@ public class FeignEbikeRegionDto implements Serializable { /** * 运营区ID */ - @Id private Long regionId; private Long operatorId; diff --git a/ebike-operations/src/main/java/com/cdzy/operations/service/impl/EbikeRegionServiceImpl.java b/ebike-operations/src/main/java/com/cdzy/operations/service/impl/EbikeRegionServiceImpl.java index 982f403..e48e99f 100644 --- a/ebike-operations/src/main/java/com/cdzy/operations/service/impl/EbikeRegionServiceImpl.java +++ b/ebike-operations/src/main/java/com/cdzy/operations/service/impl/EbikeRegionServiceImpl.java @@ -26,6 +26,7 @@ import org.springframework.transaction.annotation.Transactional; import java.util.Objects; +import static com.cdzy.operations.model.entity.table.EbikeBikeInfoTableDef.EBIKE_BIKE_INFO; import static com.cdzy.operations.model.entity.table.EbikeOperationConfigTableDef.EBIKE_OPERATION_CONFIG; import static com.cdzy.operations.model.entity.table.EbikeOperationLockConfigTableDef.EBIKE_OPERATION_LOCK_CONFIG; import static com.cdzy.operations.model.entity.table.EbikeOperationReturnConfigTableDef.EBIKE_OPERATION_RETURN_CONFIG; @@ -168,8 +169,11 @@ public class EbikeRegionServiceImpl extends ServiceImpl { + + private final GeometryFactory geometryFactory = new GeometryFactory(); + + @Override + public Point deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonNode node = p.getCodec().readTree(p); + + if (node.isNull()) { + return null; + } + + try { + // 格式1: 三字段格式 {"type": "point", "longitude": 116.3974, "latitude": 39.9093} + if (node.isObject() && node.has("type") && + (node.has("longitude") || node.has("latitude"))) { + return parseFromThreeFields(node); + } + + // 格式2: 自定义格式 {"type": "point", "coordinates": [lng, lat]} + if (node.isObject() && node.has("type")) { + String type = node.get("type").asText().toLowerCase(Locale.ROOT); + if ("point".equals(type)) { + return parseFromCustomFormat(node); + } + } + // 格式3: GeoJSON 格式 {"type": "Point", "coordinates": [lng, lat]} + else if (node.isObject() && node.has("type")) { + String type = node.get("type").asText(); + if ("Point".equals(type)) { + return parseFromGeoJSON(node); + } + } + // 格式4: 坐标数组格式 [lng, lat] + else if (node.isArray()) { + return parseFromArray(node); + } + // 格式5: 简化的三字段格式(没有type) + else if (node.isObject() && (node.has("longitude") || node.has("latitude"))) { + return parseFromLongitudeLatitude(node); + } + // 格式6: WKT 字符串格式 "POINT(lng lat)" + else if (node.isTextual()) { + return parseFromWKT(node.asText()); + } + + throw new IOException(""" + 不支持的 Point JSON 格式,支持的格式包括: + 1. 三字段格式: {"type":"point","longitude":116.3974,"latitude":39.9093} + 2. 自定义格式: {"type":"point","coordinates":[lng,lat]} + 3. GeoJSON格式: {"type":"Point","coordinates":[lng,lat]} + 4. 坐标数组: [lng,lat] + 5. 简化格式: {"longitude":116.3974,"latitude":39.9093} + 6. WKT字符串: "POINT(lng lat)" + 7. PostgreSQL点格式: "(lng,lat)\""""); + + } catch (Exception e) { + log.error("!!! PointDeserializer 反序列化失败 !!!"); + log.error("错误信息: {}", e.getMessage()); + log.error("输入 JSON: {}", node); + throw new IOException("Point 反序列化失败: " + e.getMessage(), e); + } + } + + @Override + public Class handledType() { + return Point.class; + } + + /** + * 解析三字段格式:{"type":"point","longitude":116.3974,"latitude":39.9093} + */ + private Point parseFromThreeFields(JsonNode node) throws IOException { + String type = node.get("type").asText(); + if (!"point".equalsIgnoreCase(type)) { + throw new IOException("type 字段必须是 'point',当前是: " + type); + } + + JsonNode lngNode = node.get("longitude"); + JsonNode latNode = node.get("latitude"); + + if (lngNode == null || latNode == null) { + throw new IOException("longitude 或 latitude 字段缺失"); + } + + try { + double longitude = lngNode.asDouble(); + double latitude = latNode.asDouble(); + return geometryFactory.createPoint(new Coordinate(longitude, latitude)); + } catch (NumberFormatException e) { + throw new IOException("longitude/latitude 格式错误", e); + } + } + + /** + * 解析自定义格式:{"type":"point","coordinates":[lng,lat]} + */ + private Point parseFromCustomFormat(JsonNode node) throws IOException { + JsonNode coordinatesNode = node.get("coordinates"); + if (coordinatesNode == null) { + throw new IOException("coordinates 字段缺失"); + } + + return parseCoordinates(coordinatesNode); + } + + /** + * 解析 GeoJSON 格式:{"type":"Point","coordinates":[lng,lat]} + */ + private Point parseFromGeoJSON(JsonNode node) throws IOException { + JsonNode coordinatesNode = node.get("coordinates"); + if (coordinatesNode == null) { + throw new IOException("coordinates 字段缺失"); + } + + return parseCoordinates(coordinatesNode); + } + + /** + * 解析坐标数组格式:[lng, lat] + */ + private Point parseFromArray(JsonNode node) throws IOException { + return parseCoordinates(node); + } + + /** + * 解析简化格式:{"longitude":116.3974,"latitude":39.9093} + */ + private Point parseFromLongitudeLatitude(JsonNode node) throws IOException { + JsonNode lngNode = node.get("longitude"); + JsonNode latNode = node.get("latitude"); + + if (lngNode == null || latNode == null) { + throw new IOException("longitude 或 latitude 字段缺失"); + } + + try { + double longitude = lngNode.asDouble(); + double latitude = latNode.asDouble(); + return geometryFactory.createPoint(new Coordinate(longitude, latitude)); + } catch (NumberFormatException e) { + throw new IOException("longitude/latitude 格式错误", e); + } + } + + /** + * 解析 WKT 字符串格式:POINT(lng lat) + */ + private Point parseFromWKT(String wktString) throws IOException { + if (wktString == null || wktString.trim().isEmpty()) { + throw new IOException("WKT 字符串为空"); + } + + try { + wktString = wktString.trim(); + + // 支持 PostgreSQL 点格式转换:(x,y) -> POINT(x y) + if (wktString.startsWith("(") && wktString.endsWith(")")) { + wktString = convertPostgresPointToWKT(wktString); + } + + // 确保是有效的 WKT + if (!wktString.toUpperCase().startsWith("POINT")) { + wktString = "POINT(" + wktString + ")"; + } + + WKTReader reader = new WKTReader(geometryFactory); + Geometry geometry = reader.read(wktString); + + if (!(geometry instanceof Point)) { + throw new IOException("WKT 字符串不是有效的 Point: " + wktString); + } + + return (Point) geometry; + } catch (Exception e) { + throw new IOException("WKT 格式解析失败: " + wktString, e); + } + } + + /** + * 解析坐标节点 + */ + private Point parseCoordinates(JsonNode coordinatesNode) throws IOException { + if (coordinatesNode == null) { + throw new IOException("坐标节点为 null"); + } + + if (!coordinatesNode.isArray()) { + throw new IOException("coordinates 字段必须是数组类型"); + } + + if (coordinatesNode.size() < 2) { + throw new IOException("坐标数组至少需要2个值(经度和纬度)"); + } + + try { + double lng = coordinatesNode.get(0).asDouble(); + double lat = coordinatesNode.get(1).asDouble(); + + return geometryFactory.createPoint(new Coordinate(lng, lat)); + } catch (NumberFormatException e) { + throw new IOException("坐标格式错误: " + coordinatesNode, e); + } + } + + /** + * 转换 PostgreSQL 点格式到 WKT 格式 + */ + private String convertPostgresPointToWKT(String pgPointString) { + // PostgreSQL 格式: (x,y) 或 (x, y) + // WKT 格式: POINT(x y) + + String cleaned = pgPointString + .replaceAll("\\s+", "") // 移除所有空格 + .replace("(", "") + .replace(")", ""); + + String[] coords = cleaned.split(","); + if (coords.length >= 2) { + return "POINT(" + coords[0] + " " + coords[1] + ")"; + } + + return pgPointString; // 无法转换,返回原值 + } +} \ No newline at end of file diff --git a/ebike-report/src/main/java/com/cdzy/report/handler/PointSerializer.java b/ebike-report/src/main/java/com/cdzy/report/handler/PointSerializer.java new file mode 100644 index 0000000..8427f10 --- /dev/null +++ b/ebike-report/src/main/java/com/cdzy/report/handler/PointSerializer.java @@ -0,0 +1,108 @@ +package com.cdzy.report.handler; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import lombok.extern.slf4j.Slf4j; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.io.WKTWriter; + +import java.io.IOException; + +/** + * JTS Point 序列化器 - 将 Point 序列化为 JSON + * 输出 type、longitude、latitude 三个字段 + */ +@Slf4j +public class PointSerializer extends JsonSerializer { + + private final WKTWriter wktWriter = new WKTWriter(); + + @Override + public void serialize(Point point, JsonGenerator gen, SerializerProvider serializers) + throws IOException { + if (point == null) { + gen.writeNull(); + return; + } + + try { + // 获取坐标 + Coordinate coordinate = point.getCoordinate(); + + if (coordinate == null || Double.isNaN(coordinate.x) || Double.isNaN(coordinate.y)) { + log.warn("警告: 点坐标无效: {}", coordinate); + gen.writeNull(); + return; + } + + // 序列化为三字段格式:type、longitude、latitude + gen.writeStartObject(); + gen.writeStringField("type", "point"); + gen.writeNumberField("longitude", formatCoordinate(coordinate.x)); + gen.writeNumberField("latitude", formatCoordinate(coordinate.y)); + gen.writeEndObject(); + + } catch (Exception e) { + log.error("!!! PointSerializer 序列化失败 !!!"); + log.error("错误信息: {}", e.getMessage()); + // 尝试备用序列化方式 + try { + serializeAsAlternativeFormat(point, gen); + } catch (Exception e2) { + throw new IOException("Point 序列化失败: " + e.getMessage(), e); + } + } + } + + @Override + public Class handledType() { + return Point.class; + } + + /** + * 格式化坐标值 + */ + private double formatCoordinate(double value) { + // 返回原始值,Jackson 会处理格式化 + return value; + } + + /** + * 备用序列化方式:序列化为 coordinates 数组格式 + */ + private void serializeAsAlternativeFormat(Point point, JsonGenerator gen) throws IOException { + if (point == null || point.isEmpty()) { + gen.writeNull(); + return; + } + + try { + Coordinate coordinate = point.getCoordinate(); + + // 方式1:coordinates 数组格式 + gen.writeStartObject(); + gen.writeStringField("type", "point"); + + gen.writeArrayFieldStart("coordinates"); + gen.writeNumber(coordinate.x); + gen.writeNumber(coordinate.y); + gen.writeEndArray(); + + gen.writeEndObject(); + + } catch (Exception e) { + log.error("备用序列化失败: {}", e.getMessage()); + + // 最终备用:序列化为 WKT 字符串 + try { + String wkt = wktWriter.write(point); + gen.writeString(wkt); + } catch (Exception e2) { + throw new IOException("Point 序列化完全失败", e2); + } + } + } + +} \ No newline at end of file diff --git a/ebike-report/src/main/java/com/cdzy/report/handler/PointTypeHandler.java b/ebike-report/src/main/java/com/cdzy/report/handler/PointTypeHandler.java new file mode 100644 index 0000000..0e30a6b --- /dev/null +++ b/ebike-report/src/main/java/com/cdzy/report/handler/PointTypeHandler.java @@ -0,0 +1,143 @@ +package com.cdzy.report.handler; + +import com.cdzy.common.utils.CoordinateUtil; +import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.TypeHandler; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.io.WKTReader; +import org.postgresql.util.PGobject; + +import java.sql.*; + +public class PointTypeHandler implements TypeHandler { + + private static final int WGS84_SRID = 4326; + private final GeometryFactory geometryFactory = new GeometryFactory(); + + @Override + public void setParameter(PreparedStatement ps, int i, Point parameter, JdbcType jdbcType) + throws SQLException { + if (parameter == null) { + ps.setNull(i, Types.OTHER); + } else { + try { + // 写入时:GCJ-02 → WGS-84 + Point wgs84Point = gcj02ToWgs84(parameter); + + PGobject pgObject = new PGobject(); + pgObject.setType("geometry"); + + String wkt = "SRID=" + WGS84_SRID + ";" + wgs84Point.toText(); + pgObject.setValue(wkt); + + ps.setObject(i, pgObject); + } catch (Exception e) { + System.err.println("Point 坐标转换失败: " + e.getMessage()); + ps.setNull(i, Types.OTHER); + } + } + } + + @Override + public Point getResult(ResultSet rs, String columnName) throws SQLException { + Object object = rs.getObject(columnName); + Point wgs84Point = convertToPoint(object); + return wgs84ToGcj02(wgs84Point); + } + + @Override + public Point getResult(ResultSet rs, int columnIndex) throws SQLException { + Object object = rs.getObject(columnIndex); + Point wgs84Point = convertToPoint(object); + return wgs84ToGcj02(wgs84Point); + } + + @Override + public Point getResult(CallableStatement cs, int columnIndex) throws SQLException { + Object object = cs.getObject(columnIndex); + Point wgs84Point = convertToPoint(object); + return wgs84ToGcj02(wgs84Point); + } + + private Point convertToPoint(Object object) { + if (object == null) return null; + + try { + if (object instanceof PGobject pgObject) { + String value = pgObject.getValue(); + if (value == null) return null; + + // 解析 WKT 格式:SRID=4326;POINT(lng lat) + if (value.contains("POINT")) { + String wkt = value; + if (value.startsWith("SRID=")) { + wkt = value.substring(value.indexOf(';') + 1); + } + + WKTReader reader = new WKTReader(geometryFactory); + Geometry geometry = reader.read(wkt); + if (geometry instanceof Point point) { + point.setSRID(WGS84_SRID); + return point; + } + } + } else if (object instanceof String str) { + WKTReader reader = new WKTReader(geometryFactory); + Geometry geometry = reader.read(str); + if (geometry instanceof Point point) { + point.setSRID(WGS84_SRID); + return point; + } + } + } catch (Exception e) { + System.err.println("转换 Point 失败: " + e.getMessage()); + } + + return null; + } + + private Point gcj02ToWgs84(Point gcj02Point) { + if (gcj02Point == null || gcj02Point.isEmpty()) { + return gcj02Point; + } + + try { + Coordinate coord = gcj02Point.getCoordinate(); + double[] wgs84 = CoordinateUtil.GCJ02ToWGS84(coord.x, coord.y); + + Point wgs84Point = geometryFactory.createPoint( + new Coordinate(wgs84[0], wgs84[1]) + ); + wgs84Point.setSRID(WGS84_SRID); + + return wgs84Point; + } catch (Exception e) { + System.err.println("GCJ-02 → WGS-84 点坐标转换失败: " + e.getMessage()); + gcj02Point.setSRID(WGS84_SRID); + return gcj02Point; + } + } + + private Point wgs84ToGcj02(Point wgs84Point) { + if (wgs84Point == null || wgs84Point.isEmpty()) { + return wgs84Point; + } + + try { + Coordinate coord = wgs84Point.getCoordinate(); + double[] gcj02 = CoordinateUtil.WGS84ToGCJ02(coord.x, coord.y); + + Point gcj02Point = geometryFactory.createPoint( + new Coordinate(gcj02[0], gcj02[1]) + ); + gcj02Point.setSRID(WGS84_SRID); + + return gcj02Point; + } catch (Exception e) { + throw new RuntimeException("WGS-84 → GCJ-02 点坐标转换失败: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/ebike-report/src/main/java/com/cdzy/report/handler/PolygonDeserializer.java b/ebike-report/src/main/java/com/cdzy/report/handler/PolygonDeserializer.java new file mode 100644 index 0000000..e3964ad --- /dev/null +++ b/ebike-report/src/main/java/com/cdzy/report/handler/PolygonDeserializer.java @@ -0,0 +1,243 @@ +package com.cdzy.report.handler; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.extern.slf4j.Slf4j; +import org.locationtech.jts.geom.*; +import org.locationtech.jts.io.WKTReader; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * JTS Polygon 反序列化器 - 将 JSON 反序列化为 Polygon + * 支持与 PGpolygonDeserializer 相同的 JSON 格式 + */ +@Slf4j +public class PolygonDeserializer extends JsonDeserializer { + + private final GeometryFactory geometryFactory = new GeometryFactory(); + + @Override + public Polygon deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonNode node = p.getCodec().readTree(p); + + if (node.isNull()) { + return null; + } + + try { + Polygon result = null; + + // 格式1: 自定义格式 {"type": "polygon", "coordinates": [[x1,y1], [x2,y2], ...]} + if (node.isObject() && node.has("type")) { + String type = node.get("type").asText().toLowerCase(); + if ("polygon".equals(type)) { + result = parseFromCustomFormat(node); + } + } + // 格式2: 直接坐标数组 [[x1,y1], [x2,y2], ...] + else if (node.isArray()) { + result = parseFromArray(node); + } + // 格式3: WKT 字符串格式 "POLYGON((x1 y1, x2 y2, ...))" + else if (node.isTextual()) { + result = parseFromWKT(node.asText()); + } + // 格式4: 对象格式但没有 type 字段,尝试解析 coordinates + else if (node.isObject() && node.has("coordinates")) { + result = parseFromCustomFormat(node); + } + + if (result == null) { + throw new IOException(""" + 不支持的 Polygon JSON 格式,支持的格式包括: + 1. 自定义格式: {"type":"polygon","coordinates":[[x1,y1],[x2,y2],...]} + 2. GeoJSON格式: {"type":"Polygon","coordinates":[[[x1,y1],[x2,y2],...]]} + 3. 坐标数组: [[x1,y1],[x2,y2],...] + 4. WKT字符串: "POLYGON((x1 y1, x2 y2, ...))" + 5. PGpolygon格式: "((x1,y1),(x2,y2),...)\""""); + } + + return result; + + } catch (Exception e) { + log.error("!!! PolygonDeserializer 反序列化失败 !!!"); + log.error("错误信息: {}", e.getMessage()); + log.error("输入 JSON: {}", node); + throw new IOException("Polygon 反序列化失败: " + e.getMessage(), e); + } + } + + @Override + public Class handledType() { + return Polygon.class; + } + + /** + * 解析自定义格式 + */ + private Polygon parseFromCustomFormat(JsonNode node) throws IOException { + JsonNode coordinatesNode = node.get("coordinates"); + if (coordinatesNode == null) { + throw new IOException("coordinates 字段缺失"); + } + + if (!coordinatesNode.isArray()) { + throw new IOException("coordinates 字段必须是数组类型"); + } + + List coordinates = new ArrayList<>(); + for (int i = 0; i < coordinatesNode.size(); i++) { + JsonNode coordNode = coordinatesNode.get(i); + + if (!coordNode.isArray()) { + throw new IOException("坐标 " + i + " 必须是数组格式"); + } + + if (coordNode.size() < 2) { + throw new IOException("坐标 " + i + " 至少需要2个值(经度和纬度)"); + } + + try { + double x = coordNode.get(0).asDouble(); + double y = coordNode.get(1).asDouble(); + coordinates.add(new Coordinate(x, y)); + } catch (NumberFormatException e) { + throw new IOException("坐标 " + i + " 格式错误: " + coordNode, e); + } + } + + return createPolygonFromCoordinates(coordinates); + } + + /** + * 解析坐标数组格式 + */ + private Polygon parseFromArray(JsonNode node) throws IOException { + List coordinates = new ArrayList<>(); + for (int i = 0; i < node.size(); i++) { + JsonNode coordNode = node.get(i); + + if (!coordNode.isArray()) { + throw new IOException("坐标 " + i + " 必须是数组格式"); + } + + if (coordNode.size() < 2) { + throw new IOException("坐标 " + i + " 至少需要2个值(经度和纬度)"); + } + + try { + double x = coordNode.get(0).asDouble(); + double y = coordNode.get(1).asDouble(); + coordinates.add(new Coordinate(x, y)); + } catch (NumberFormatException e) { + throw new IOException("坐标 " + i + " 格式错误: " + coordNode, e); + } + } + + return createPolygonFromCoordinates(coordinates); + } + + /** + * 解析 WKT 字符串格式 + */ + private Polygon parseFromWKT(String wktString) throws IOException { + if (wktString == null || wktString.trim().isEmpty()) { + throw new IOException("WKT 字符串为空"); + } + + try { + // 清理字符串 + wktString = wktString.trim(); + + // 支持 PGpolygon 格式转换:((x,y),(x,y),...) -> POLYGON((x y, x y, ...)) + if (wktString.startsWith("((") && wktString.endsWith("))")) { + wktString = convertPGpolygonToWKT(wktString); + } + + // 确保是有效的 WKT + if (!wktString.toUpperCase().startsWith("POLYGON")) { + // 尝试包装 + wktString = "POLYGON(" + wktString + ")"; + } + + WKTReader reader = new WKTReader(geometryFactory); + Geometry geometry = reader.read(wktString); + + if (!(geometry instanceof Polygon)) { + throw new IOException("WKT 字符串不是有效的 Polygon: " + wktString); + } + + return (Polygon) geometry; + } catch (Exception e) { + throw new IOException("WKT 格式解析失败: " + wktString, e); + } + } + + /** + * 从坐标列表创建 Polygon + */ + private Polygon createPolygonFromCoordinates(List coordinates) throws IOException { + if (coordinates == null || coordinates.size() < 3) { + throw new IOException("多边形至少需要3个点,当前只有 " + + (coordinates == null ? 0 : coordinates.size()) + " 个点"); + } + + // 确保多边形闭合(首尾点相同) + Coordinate first = coordinates.get(0); + Coordinate last = coordinates.get(coordinates.size() - 1); + + if (!first.equals2D(last)) { + log.debug("多边形未闭合,添加首点以闭合"); + coordinates.add(new Coordinate(first.x, first.y)); + } + + try { + // 转换为数组 + Coordinate[] coordArray = coordinates.toArray(new Coordinate[0]); + + // 创建线性环 + LinearRing shell = geometryFactory.createLinearRing(coordArray); + + // 创建多边形(无孔洞) + return geometryFactory.createPolygon(shell); + } catch (Exception e) { + throw new IOException("创建 Polygon 失败: " + e.getMessage(), e); + } + } + + /** + * 转换 PGpolygon 格式到 WKT 格式 + */ + private String convertPGpolygonToWKT(String pgPolygonString) { + // PGpolygon 格式: ((x,y),(x,y),...) + // WKT 格式: POLYGON((x y, x y, ...)) + + String cleaned = pgPolygonString + .replaceAll("\\s+", "") // 移除所有空格 + .replace("((", "(") + .replace("))", ")"); + + // 替换逗号为空格,括号内逗号为空格 + String[] points = cleaned.substring(1, cleaned.length() - 1).split("\\),\\("); + StringBuilder wktBuilder = new StringBuilder("POLYGON(("); + + for (int i = 0; i < points.length; i++) { + String point = points[i]; + String[] coords = point.split(","); + if (coords.length >= 2) { + wktBuilder.append(coords[0]).append(" ").append(coords[1]); + if (i < points.length - 1) { + wktBuilder.append(", "); + } + } + } + + wktBuilder.append("))"); + return wktBuilder.toString(); + } +} \ No newline at end of file diff --git a/ebike-report/src/main/java/com/cdzy/report/handler/PolygonSerializer.java b/ebike-report/src/main/java/com/cdzy/report/handler/PolygonSerializer.java new file mode 100644 index 0000000..f207053 --- /dev/null +++ b/ebike-report/src/main/java/com/cdzy/report/handler/PolygonSerializer.java @@ -0,0 +1,170 @@ +package com.cdzy.report.handler; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import lombok.extern.slf4j.Slf4j; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LinearRing; +import org.locationtech.jts.geom.Polygon; +import org.locationtech.jts.io.WKTWriter; + +import java.io.IOException; + +/** + * JTS Polygon 序列化器 - 将 Polygon 序列化为 JSON + * 生成与 PGpolygonSerializer 相同的 JSON 格式 + */ +@Slf4j +public class PolygonSerializer extends JsonSerializer { + + private final WKTWriter wktWriter = new WKTWriter(); + + @Override + public void serialize(Polygon polygon, JsonGenerator gen, SerializerProvider serializers) + throws IOException { + if (polygon == null) { + gen.writeNull(); + return; + } + + try { + // 获取多边形的外环坐标 + Coordinate[] coordinates = getPolygonCoordinates(polygon); + + if (coordinates.length == 0) { + log.warn("警告: 多边形没有有效坐标"); + gen.writeNull(); + return; + } + + // 检查是否有无效坐标 + for (int i = 0; i < coordinates.length; i++) { + Coordinate coord = coordinates[i]; + if (coord == null || Double.isNaN(coord.x) || Double.isNaN(coord.y)) { + log.error("错误: 第 {} 个坐标无效: {}", i, coord); + } + } + + // 序列化为自定义格式(与 PGpolygonSerializer 格式一致) + gen.writeStartObject(); + gen.writeStringField("type", "polygon"); + + gen.writeArrayFieldStart("coordinates"); + for (int i = 0; i < coordinates.length; i++) { + Coordinate coord = coordinates[i]; + if (coord == null || Double.isNaN(coord.x) || Double.isNaN(coord.y)) { + continue; + } + + try { + gen.writeStartArray(); + // 使用格式化器避免科学计数法 + gen.writeNumber(formatCoordinate(coord.x)); + gen.writeNumber(formatCoordinate(coord.y)); + gen.writeEndArray(); + } catch (Exception e) { + throw new IOException("序列化坐标 " + i + " 失败", e); + } + } + gen.writeEndArray(); + gen.writeEndObject(); + + } catch (Exception e) { + log.error("!!! PolygonSerializer 序列化失败 !!!"); + log.error("错误信息: {}", e.getMessage()); + // 尝试备用序列化方式 + try { + serializeAsWKT(polygon, gen); + } catch (Exception e2) { + throw new IOException("Polygon 序列化失败: " + e.getMessage(), e); + } + } + } + + @Override + public Class handledType() { + return Polygon.class; + } + + /** + * 获取多边形的坐标(仅外环,忽略孔洞) + */ + private Coordinate[] getPolygonCoordinates(Polygon polygon) throws IOException { + if (polygon == null || polygon.isEmpty()) { + return new Coordinate[0]; + } + + try { + // 获取外环 + LinearRing exteriorRing = polygon.getExteriorRing(); + if (exteriorRing == null) { + throw new IOException("多边形外环为空"); + } + + return exteriorRing.getCoordinates(); + + } catch (Exception e) { + throw new IOException("获取多边形坐标失败: " + e.getMessage(), e); + } + } + + /** + * 格式化坐标值 + */ + private double formatCoordinate(double value) { + // 返回原始值,Jackson 会处理格式化 + return value; + } + + /** + * 备用序列化方式:序列化为 WKT 字符串 + */ + private void serializeAsWKT(Polygon polygon, JsonGenerator gen) throws IOException { + if (polygon == null) { + gen.writeNull(); + return; + } + + try { + String wkt = wktWriter.write(polygon); + + // 转换为与 PGpolygon 相似的格式 + if (wkt.startsWith("POLYGON")) { + // 提取坐标部分 + String coordsPart = wkt.substring(wkt.indexOf("((") + 2, wkt.lastIndexOf("))")); + String[] points = coordsPart.split(", "); + + gen.writeStartObject(); + gen.writeStringField("type", "polygon"); + + gen.writeArrayFieldStart("coordinates"); + for (String point : points) { + String[] xy = point.split(" "); + if (xy.length >= 2) { + try { + double x = Double.parseDouble(xy[0]); + double y = Double.parseDouble(xy[1]); + + gen.writeStartArray(); + gen.writeNumber(x); + gen.writeNumber(y); + gen.writeEndArray(); + } catch (NumberFormatException e) { + log.warn("坐标解析失败: {}", point); + } + } + } + gen.writeEndArray(); + gen.writeEndObject(); + } else { + // 直接写 WKT 字符串 + gen.writeString(wkt); + } + + } catch (Exception e) { + log.error("WKT 序列化失败: {}", e.getMessage()); + throw new IOException("Polygon 序列化失败", e); + } + } +} \ No newline at end of file diff --git a/ebike-report/src/main/java/com/cdzy/report/handler/PolygonTypeHandler.java b/ebike-report/src/main/java/com/cdzy/report/handler/PolygonTypeHandler.java new file mode 100644 index 0000000..3be57a7 --- /dev/null +++ b/ebike-report/src/main/java/com/cdzy/report/handler/PolygonTypeHandler.java @@ -0,0 +1,429 @@ +package com.cdzy.report.handler; + +import com.cdzy.common.ex.EbikeException; +import com.cdzy.common.utils.CoordinateUtil; +import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.TypeHandler; +import org.locationtech.jts.geom.*; +import org.locationtech.jts.io.ParseException; +import org.locationtech.jts.io.WKBReader; +import org.locationtech.jts.io.WKTReader; +import org.postgresql.util.PGobject; + +import java.sql.*; +import java.util.ArrayList; +import java.util.List; + +/** + * JTS Polygon 类型处理器 - 集成 CoordinateUtil 实现坐标转换 + * 写入时:GCJ-02 → WGS-84 + * 读取时:WGS-84 → GCJ-02 + * 支持 PostgreSQL + PostGIS 的 geometry(Polygon, 4326) 类型 + */ +public class PolygonTypeHandler implements TypeHandler { + + private static final int WGS84_SRID = 4326; + private final GeometryFactory geometryFactory = new GeometryFactory(); + private final WKTReader wktReader = new WKTReader(geometryFactory); + private final WKBReader wkbReader = new WKBReader(geometryFactory); + + @Override + public void setParameter(PreparedStatement ps, int i, Polygon parameter, JdbcType jdbcType) + throws SQLException { + if (parameter == null) { + ps.setNull(i, Types.OTHER); + } else { + try { + // 写入数据库时:GCJ-02 → WGS-84 + Polygon wgs84Polygon = gcj02ToWgs84(parameter); + if (wgs84Polygon != null) { + // 创建 PGobject 并设置 PostGIS 类型 + PGobject pgObject = new PGobject(); + pgObject.setType("geometry"); + + // 使用 WKT 格式,设置正确的 SRID + String wkt = wgs84Polygon.toText(); + String wktWithSRID = "SRID=" + WGS84_SRID + ";" + wkt; + pgObject.setValue(wktWithSRID); + + ps.setObject(i, pgObject); + } else { + ps.setNull(i, Types.OTHER); + } + } catch (Exception e) { + // 如果转换失败,直接使用原始值(不带坐标转换) + System.err.println("坐标转换失败,使用原始值: " + e.getMessage()); + setPolygonWithoutConversion(ps, i, parameter); + } + } + } + + @Override + public Polygon getResult(ResultSet rs, String columnName) throws SQLException { + Object object = rs.getObject(columnName); + Polygon wgs84Polygon = convertToPolygon(object); + // 读取时:WGS-84 → GCJ-02 + return wgs84ToGcj02(wgs84Polygon); + } + + @Override + public Polygon getResult(ResultSet rs, int columnIndex) throws SQLException { + Object object = rs.getObject(columnIndex); + Polygon wgs84Polygon = convertToPolygon(object); + // 读取时:WGS-84 → GCJ-02 + return wgs84ToGcj02(wgs84Polygon); + } + + @Override + public Polygon getResult(CallableStatement cs, int columnIndex) throws SQLException { + Object object = cs.getObject(columnIndex); + Polygon wgs84Polygon = convertToPolygon(object); + // 读取时:WGS-84 → GCJ-02 + return wgs84ToGcj02(wgs84Polygon); + } + + /** + * 将数据库对象转换为 Polygon + */ + private Polygon convertToPolygon(Object object) { + if (object == null) { + return null; + } + + try { + if (object instanceof PGobject) { + return parseFromPGobject((PGobject) object); + } else if (object instanceof String) { + return parseFromString((String) object); + } else if (object instanceof byte[]) { + return parseFromWKB((byte[]) object); + } else if (object instanceof Polygon) { + return (Polygon) object; + } + } catch (Exception e) { + System.err.println("转换 Polygon 失败: " + object.getClass().getName() + + ", 错误: " + e.getMessage()); + } + + return null; + } + + /** + * 从 PGobject 解析 Polygon + */ + private Polygon parseFromPGobject(PGobject pgObject) throws ParseException { + if (pgObject == null || pgObject.getValue() == null) { + return null; + } + + String value = pgObject.getValue().trim(); + + // 判断是否是 WKT 格式 + if (value.startsWith("SRID=") || + value.startsWith("POLYGON") || + value.startsWith("0103")) { // WKB 的 polygon 类型标识 + try { + // 尝试解析为 WKT + if (value.contains("POLYGON")) { + // 去除 SRID 前缀 + if (value.startsWith("SRID=")) { + int semicolonIndex = value.indexOf(';'); + if (semicolonIndex > 0) { + value = value.substring(semicolonIndex + 1); + } + } + return (Polygon) wktReader.read(value); + } + // 尝试解析为 WKB + else if (value.startsWith("0103") || value.startsWith("\\x")) { + byte[] wkbBytes; + if (value.startsWith("\\x")) { + // PostgreSQL 十六进制格式 + String hex = value.substring(2); + wkbBytes = hexStringToByteArray(hex); + } else { + // 已经是十六进制字符串 + wkbBytes = value.getBytes(); + } + return (Polygon) wkbReader.read(wkbBytes); + } + } catch (Exception e) { + throw new ParseException("解析 PGobject 失败: " + value, e); + } + } + + return null; + } + + /** + * 从字符串解析 Polygon + */ + private Polygon parseFromString(String str) throws ParseException { + if (str == null || str.trim().isEmpty()) { + return null; + } + + try { + // 尝试 WKT 格式 + if (str.startsWith("POLYGON") || str.startsWith("SRID=")) { + return (Polygon) wktReader.read(str); + } + // 尝试 PGpolygon 格式:((x y, x y, ...)) + else if (str.startsWith("((")) { + return parseFromPGpolygonFormat(str); + } + } catch (Exception e) { + throw new ParseException("解析字符串失败: " + str, e); + } + + return null; + } + + /** + * 从 WKB 字节数组解析 Polygon + */ + private Polygon parseFromWKB(byte[] wkbBytes) throws ParseException { + if (wkbBytes == null || wkbBytes.length == 0) { + return null; + } + + try { + return (Polygon) wkbReader.read(wkbBytes); + } catch (Exception e) { + throw new ParseException("解析 WKB 失败", e); + } + } + + /** + * GCJ-02 转 WGS-84(写入数据库时调用) + */ + private Polygon gcj02ToWgs84(Polygon gcj02Polygon) { + if (gcj02Polygon == null || gcj02Polygon.isEmpty()) { + return gcj02Polygon; + } + + try { + // 获取多边形外环坐标 + Coordinate[] coordinates = gcj02Polygon.getExteriorRing().getCoordinates(); + if (coordinates == null || coordinates.length == 0) { + return gcj02Polygon; + } + + List wgs84Coordinates = new ArrayList<>(); + + // 对每个坐标点进行转换 + for (Coordinate gcj02Coord : coordinates) { + double[] wgs84 = CoordinateUtil.GCJ02ToWGS84(gcj02Coord.x, gcj02Coord.y); + wgs84Coordinates.add(new Coordinate(wgs84[0], wgs84[1])); + } + + // 确保多边形闭合 + Coordinate first = wgs84Coordinates.get(0); + Coordinate last = wgs84Coordinates.get(wgs84Coordinates.size() - 1); + if (!first.equals2D(last)) { + wgs84Coordinates.add(new Coordinate(first.x, first.y)); + } + + // 创建 WGS-84 多边形 + Coordinate[] wgs84Array = wgs84Coordinates.toArray(new Coordinate[0]); + LinearRing shell = geometryFactory.createLinearRing(wgs84Array); + + // 如果有孔洞,也需要转换 + int numHoles = gcj02Polygon.getNumInteriorRing(); + LinearRing[] holes = new LinearRing[numHoles]; + + for (int i = 0; i < numHoles; i++) { + LineString holeRing = gcj02Polygon.getInteriorRingN(i); + Coordinate[] holeCoords = holeRing.getCoordinates(); + List wgs84HoleCoords = new ArrayList<>(); + + for (Coordinate holeCoord : holeCoords) { + double[] wgs84Hole = CoordinateUtil.GCJ02ToWGS84(holeCoord.x, holeCoord.y); + wgs84HoleCoords.add(new Coordinate(wgs84Hole[0], wgs84Hole[1])); + } + + // 确保孔洞闭合 + Coordinate holeFirst = wgs84HoleCoords.get(0); + Coordinate holeLast = wgs84HoleCoords.get(wgs84HoleCoords.size() - 1); + if (!holeFirst.equals2D(holeLast)) { + wgs84HoleCoords.add(new Coordinate(holeFirst.x, holeFirst.y)); + } + + holes[i] = geometryFactory.createLinearRing( + wgs84HoleCoords.toArray(new Coordinate[0]) + ); + } + + Polygon wgs84Polygon = geometryFactory.createPolygon(shell, holes); + wgs84Polygon.setSRID(WGS84_SRID); + + System.out.println("GCJ-02 → WGS-84 转换成功,点数: " + wgs84Coordinates.size()); + return wgs84Polygon; + + } catch (Exception e) { + System.err.println("GCJ-02 → WGS-84 坐标系转换错误: " + e.getMessage()); + // 转换失败时,返回原始多边形但设置正确的 SRID + gcj02Polygon.setSRID(WGS84_SRID); + return gcj02Polygon; + } + } + + /** + * WGS-84 转 GCJ-02(从数据库读取时调用) + */ + private Polygon wgs84ToGcj02(Polygon wgs84Polygon) { + if (wgs84Polygon == null || wgs84Polygon.isEmpty()) { + return wgs84Polygon; + } + + try { + // 获取多边形外环坐标 + Coordinate[] coordinates = wgs84Polygon.getExteriorRing().getCoordinates(); + if (coordinates == null || coordinates.length == 0) { + return wgs84Polygon; + } + + List gcj02Coordinates = new ArrayList<>(); + + // 对每个坐标点进行转换 + for (Coordinate wgs84Coord : coordinates) { + double[] gcj02 = CoordinateUtil.WGS84ToGCJ02(wgs84Coord.x, wgs84Coord.y); + gcj02Coordinates.add(new Coordinate(gcj02[0], gcj02[1])); + } + + // 确保多边形闭合 + Coordinate first = gcj02Coordinates.get(0); + Coordinate last = gcj02Coordinates.get(gcj02Coordinates.size() - 1); + if (!first.equals2D(last)) { + gcj02Coordinates.add(new Coordinate(first.x, first.y)); + } + + // 创建 GCJ-02 多边形 + Coordinate[] gcj02Array = gcj02Coordinates.toArray(new Coordinate[0]); + LinearRing shell = geometryFactory.createLinearRing(gcj02Array); + + // 如果有孔洞,也需要转换 + int numHoles = wgs84Polygon.getNumInteriorRing(); + LinearRing[] holes = new LinearRing[numHoles]; + + for (int i = 0; i < numHoles; i++) { + LineString holeRing = wgs84Polygon.getInteriorRingN(i); + Coordinate[] holeCoords = holeRing.getCoordinates(); + List gcj02HoleCoords = new ArrayList<>(); + + for (Coordinate holeCoord : holeCoords) { + double[] gcj02Hole = CoordinateUtil.WGS84ToGCJ02(holeCoord.x, holeCoord.y); + gcj02HoleCoords.add(new Coordinate(gcj02Hole[0], gcj02Hole[1])); + } + + // 确保孔洞闭合 + Coordinate holeFirst = gcj02HoleCoords.get(0); + Coordinate holeLast = gcj02HoleCoords.get(gcj02HoleCoords.size() - 1); + if (!holeFirst.equals2D(holeLast)) { + gcj02HoleCoords.add(new Coordinate(holeFirst.x, holeFirst.y)); + } + + holes[i] = geometryFactory.createLinearRing( + gcj02HoleCoords.toArray(new Coordinate[0]) + ); + } + + Polygon gcj02Polygon = geometryFactory.createPolygon(shell, holes); + gcj02Polygon.setSRID(WGS84_SRID); // 保持 SRID 不变 + + return gcj02Polygon; + + } catch (Exception e) { + throw new EbikeException("WGS-84 → GCJ-02 坐标系转换错误: " + e.getMessage()); + } + } + + /** + * 设置多边形到 PreparedStatement(不进行坐标转换) + */ + private void setPolygonWithoutConversion(PreparedStatement ps, int i, Polygon polygon) + throws SQLException { + if (polygon == null) { + ps.setNull(i, Types.OTHER); + return; + } + + try { + PGobject pgObject = new PGobject(); + pgObject.setType("geometry"); + + // 使用 WKT 格式,设置正确的 SRID + polygon.setSRID(WGS84_SRID); + String wkt = polygon.toText(); + String wktWithSRID = "SRID=" + WGS84_SRID + ";" + wkt; + pgObject.setValue(wktWithSRID); + + ps.setObject(i, pgObject); + } catch (Exception e) { + System.err.println("设置 Polygon 失败: " + e.getMessage()); + ps.setNull(i, Types.OTHER); + } + } + + /** + * 解析 PGpolygon 格式的字符串:((x1 y1, x2 y2, x3 y3)) + */ + private Polygon parseFromPGpolygonFormat(String pgPolygonStr) throws ParseException { + try { + // 清理字符串 + String cleaned = pgPolygonStr.trim(); + + // 移除外层括号 + if (cleaned.startsWith("((") && cleaned.endsWith("))")) { + cleaned = cleaned.substring(2, cleaned.length() - 2); + } + + // 分割坐标点 + String[] pointStrings = cleaned.split(","); + List coordinates = new ArrayList<>(); + + for (String pointStr : pointStrings) { + pointStr = pointStr.trim(); + String[] xy = pointStr.split("\\s+"); + if (xy.length >= 2) { + double x = Double.parseDouble(xy[0]); + double y = Double.parseDouble(xy[1]); + coordinates.add(new Coordinate(x, y)); + } + } + + // 确保多边形闭合 + if (coordinates.size() >= 3) { + Coordinate first = coordinates.get(0); + Coordinate last = coordinates.get(coordinates.size() - 1); + if (!first.equals2D(last)) { + coordinates.add(new Coordinate(first.x, first.y)); + } + } + + // 创建多边形 + Coordinate[] coordArray = coordinates.toArray(new Coordinate[0]); + LinearRing shell = geometryFactory.createLinearRing(coordArray); + Polygon polygon = geometryFactory.createPolygon(shell); + polygon.setSRID(WGS84_SRID); + + return polygon; + + } catch (Exception e) { + throw new ParseException("解析 PGpolygon 格式失败: " + pgPolygonStr, e); + } + } + + /** + * 十六进制字符串转字节数组 + */ + private byte[] hexStringToByteArray(String hex) { + int len = hex.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4) + + Character.digit(hex.charAt(i + 1), 16)); + } + return data; + } +} \ No newline at end of file diff --git a/ebike-report/src/main/java/com/cdzy/report/utils/RedisUtil.java b/ebike-report/src/main/java/com/cdzy/report/utils/RedisUtil.java index c65f9e5..626d171 100644 --- a/ebike-report/src/main/java/com/cdzy/report/utils/RedisUtil.java +++ b/ebike-report/src/main/java/com/cdzy/report/utils/RedisUtil.java @@ -269,4 +269,13 @@ public class RedisUtil { public Object getEcu(String ecuSn) { return get( BIKE_ECU_PREFIX + ecuSn); } + + /** + * 获取 key 的类型 + * @param key 键 + * @return 类型字符串(如 "string", "hash", "none" 等) + */ + public String type(String key) { + return redisTemplate.type(key).code(); + } }