行政区划 ZIP 导入(importZip)

一、Maven 依赖

xml 复制代码
<!-- GDAL Java 绑定(解析 ESRI File Geodatabase) -->
<dependency>
    <groupId>org.gdal</groupId>
    <artifactId>gdal</artifactId>
    <version>${gdal.version}</version>
</dependency>

<!-- GeoTools(解析 Shapefile / GeoJSON / GPKG) -->
<dependency>
    <groupId>org.geotools</groupId>
    <artifactId>gt-shapefile</artifactId>
    <version>${geotools.version}</version>
</dependency>

底层通过 GDAL + GeoTools 双引擎解析地理空间数据格式(.gdb、.shp、.geojson、.gpkg 等)。


二、接口定义

文件路径: /.../xzq/controller/JcXzqController.java

java 复制代码
@ApiOperation("导入ZIP文件(包含地理空间数据GBD格式文件)")
@CommonLog("导入行政区ZIP文件")
@PostMapping("/base/xzq/importZip")
public CommonResult<Map<String, Object>> importZip(
        @RequestParam("file") MultipartFile file,
        @RequestParam(value = "overwriteMode", defaultValue = "false") Boolean overwriteMode) {
    JcXzqZipImportParam param = new JcXzqZipImportParam();
    param.setFile(file);
    param.setOverwriteMode(overwriteMode);
    return CommonResult.data(jcXzqService.importZipFile(param));
}
  • 请求方式: POST
  • 参数: file(MultipartFile) + overwriteMode(boolean, 默认 false)
  • 返回: {totalCount, successCount, errorCount, layerCounts, errorDetail}

三、服务入口方法

java 复制代码
@Transactional(rollbackFor = Exception.class)
@Override
public Map<String, Object> importZipFile(JcXzqZipImportParam jcXzqZipImportParam) {
    MultipartFile file = jcXzqZipImportParam.getFile();
    boolean overwriteMode = Boolean.TRUE.equals(jcXzqZipImportParam.getOverwriteMode());
    Map<String, Object> result = new HashMap<>();
    List<String> errorList = new ArrayList<>();
    Map<String, Integer> layerCounts = new HashMap<>();
    int totalCount = 0, successCount = 0, errorCount = 0;

    // 创建临时目录
    String tempDir = System.getProperty("java.io.tmpdir") + File.separator + "xzq_import_" + IdUtil.simpleUUID();
    Path tempPath = Paths.get(tempDir);

    try {
        Files.createDirectories(tempPath);

        // 1. 校验文件格式
        if (!file.getOriginalFilename().toLowerCase().endsWith(".zip")) {
            throw new CommonException("文件格式错误,仅支持ZIP格式文件");
        }
        // 2. 解压ZIP
        unzipFile(file, tempPath);
        // 3. 查找GDB文件
        List<File> gdbFiles = findGdbFiles(tempPath.toFile());
        if (gdbFiles.isEmpty()) {
            throw new CommonException("ZIP文件中未找到GDB格式文件");
        }
        // 4. 解析每个GDB文件
        for (File gdbFile : gdbFiles) {
            GbdFileParser.MultiLayerGdbResult gdbResult = GbdFileParser.parseMultiLayerGdbWithGdal(gdbFile);
            for (String layerName : gdbResult.getLayerNames()) {
                List<?> layerData = gdbResult.getLayerDataMap().get(layerName);
                if (layerData == null || layerData.isEmpty()) continue;
                int layerSuccess = processLayerData(layerName, layerData, overwriteMode, errorList);
                layerCounts.put(layerName, layerSuccess);
                totalCount += layerData.size();
                successCount += layerSuccess;
                errorCount += (layerData.size() - layerSuccess);
            }
        }
        result.put("totalCount", totalCount);
        result.put("successCount", successCount);
        result.put("errorCount", errorCount);
        result.put("layerCounts", layerCounts);
        result.put("errorDetail", errorList);
    } catch (Exception e) {
        throw new CommonException("GDB文件导入失败:{}", e.getMessage());
    } finally {
        FileUtil.del(tempDir);
    }
    return result;
}

四、核心方法详解

4.1 ZIP 解压

java 复制代码
private void unzipFile(MultipartFile file, Path destPath) throws IOException {
    try (ZipInputStream zipInputStream = new ZipInputStream(file.getInputStream(), StandardCharsets.UTF_8)) {
        ZipEntry entry;
        while ((entry = zipInputStream.getNextEntry()) != null) {
            if (!entry.isDirectory()) {
                Path filePath = destPath.resolve(entry.getName());
                Files.createDirectories(filePath.getParent());
                try (FileOutputStream fos = new FileOutputStream(filePath.toFile())) {
                    IoUtil.copy(zipInputStream, fos);
                }
            }
            zipInputStream.closeEntry();
        }
    }
}

逻辑: 使用 ZipInputStream + UTF-8 逐条解压 ZIP 中的文件/目录到临时目录,保留原始目录结构(.gdb 内部包含多个系统文件,必须完整保留)。

4.2 GDB 文件递归查找

java 复制代码
private List<File> findGdbFiles(File directory) {
    List<File> gdbFiles = new ArrayList<>();
    File[] files = directory.listFiles();
    if (files != null) {
        for (File file : files) {
            if (file.isDirectory()) {
                if (file.getName().toLowerCase().endsWith(".gdb")) {
                    gdbFiles.add(file);  // .gdb 在文件系统中是一个目录
                } else {
                    gdbFiles.addAll(findGdbFiles(file)); // 递归子目录
                }
            } else if (isGdbFile(file.getName())) {
                gdbFiles.add(file);
            }
        }
    }
    return gdbFiles;
}

逻辑: ESRI File Geodatabase 在磁盘上表现为一个目录(xxx.gdb/),内部包含 .gdbtable.freelist.gdbindexes 等文件。递归遍历解压目录,找到所有以 .gdb 结尾的目录/文件。

4.3 GDB 多图层解析(GDAL 方式)

核心类: GbdFileParser.java

路径: /.../xzq/util/GbdFileParser.java

java 复制代码
/**
     * 使用GDAL解析ESRI File Geodatabase (.gdb)文件
     *
     * 注意:此方法需要额外的依赖和配置:
     * 1. 添加GDAL Java绑定依赖
     * 2. 安装GDAL本地库
     * 3. 配置GDAL环境变量
     *
     * Maven依赖示例:
     * <dependency>
     *     <groupId>org.gdal</groupId>
     *     <artifactId>gdal</artifactId>
     *     <version>3.6.0</version>
     * </dependency>
     *
     * @param gdbFile GDB文件(实际上是文件夹)
     * @return 解析后的行政区数据列表
     * @throws IOException 解析异常
     */
    /**
     * 使用GDAL解析多图层GDB文件
     *
     * @param gdbFile GDB文件路径
     * @return 多图层解析结果
     * @throws IOException 解析异常
     */
public static MultiLayerGdbResult parseMultiLayerGdbWithGdal(File gdbFile) throws IOException {
    checkGdalEnvironment();
    MultiLayerGdbResult result = new MultiLayerGdbResult();

    // 初始化GDAL/OGR
    ogr.RegisterAll();
    gdal.AllRegister();

    // 打开GDB数据源(只读模式)
    DataSource dataSource = ogr.Open(gdbFile.getAbsolutePath(), 0);

    // 遍历所有图层
    int layerCount = dataSource.GetLayerCount();
    for (int i = 0; i < layerCount; i++) {
        Layer layer = dataSource.GetLayer(i);
        String layerName = layer.GetName();
        List<?> layerData = createLayerDataList(layerName);

        // 遍历图层中的要素
        layer.ResetReading();
        org.gdal.ogr.Feature feature;
        while ((feature = layer.GetNextFeature()) != null) {
            Object entity = convertGdalFeatureToEntity(feature, layerName);
            if (entity != null) addEntityToList(layerData, entity);
            feature.delete();  // 必须手动释放
        }
        result.addLayerData(layerName, layerData);
    }
    dataSource.delete();
    return result;
}

逻辑:

  1. 检测 GDAL 环境变量(GDAL_DATAPROJ_LIBjava.library.path
  2. ogr.RegisterAll() + gdal.AllRegister() 注册所有驱动
  3. ogr.Open() 打开 GDB 数据源(只读模式 0)
  4. dataSource.GetLayerCount() + GetLayer(i) 遍历所有数据图层
  5. 每图层中 GetNextFeature() 遍历要素,转为业务实体
  6. feature.delete()dataSource.delete() 必须显式调用释放 GDAL 内存

4.4 GDAL Feature → 业务实体转换

java 复制代码
private static Object convertGdalFeatureToEntity(org.gdal.ogr.Feature gdalFeature, String layerName) {
    FeatureDefn featureDefn = gdalFeature.GetDefnRef();
    int fieldCount = featureDefn.GetFieldCount();

    // 获取几何数据(WKT格式)
    String geomWkt = null;
    org.gdal.ogr.Geometry geometry = gdalFeature.GetGeometryRef();
    if (geometry != null) {
        geomWkt = geometry.ExportToWkt();
    }

    // 按图层名称分流转换
    switch (layerName.toLowerCase()) {
        case "xzq":         return convertGdalFeatureToXzq(gdalFeature, featureDefn, fieldCount, geomWkt);
        case "dxssyjjbxx":  return convertGdalFeatureToDxssyjjbxx(gdalFeature, featureDefn, fieldCount, geomWkt);
        case "dxszy_1_5jfq":return convertGdalFeatureToDxszy15jfq(gdalFeature, featureDefn, fieldCount, geomWkt);
        case "gczjd":       return convertGdalFeatureToGczjd(gdalFeature, featureDefn, fieldCount, geomWkt);
        case "pdt":         return convertGdalFeatureToPdt(gdalFeature, featureDefn, fieldCount, geomWkt);
        case "szy_1_3jfq":  return convertGdalFeatureToSzy13jfq(gdalFeature, featureDefn, fieldCount, geomWkt);
        default:            return convertGdalFeatureToXzq(gdalFeature, featureDefn, fieldCount, geomWkt);
    }
}

字段映射示例(以 Xzq 为例):

java 复制代码
private static JcXzq convertGdalFeatureToXzq(org.gdal.ogr.Feature gdalFeature,
                                              FeatureDefn featureDefn,
                                              int fieldCount, String geomWkt) {
    JcXzq xzq = new JcXzq();
    for (int i = 0; i < fieldCount; i++) {
        FieldDefn fieldDefn = featureDefn.GetFieldDefn(i);
        String fieldName = fieldDefn.GetName().toLowerCase();
        if (gdalFeature.IsFieldSet(i)) {
            String value = gdalFeature.GetFieldAsString(i);
            if (value != null && !value.trim().isEmpty()) {
                mapAttributeToXzq(xzq, fieldName, value);
            }
        }
    }
    xzq.setGeom(geomWkt);
    return isValidXzq(xzq) ? xzq : null;
}

逻辑: 遍历 Feature 的字段定义,根据字段名(支持中文/英文模糊匹配如 xzqdm / 行政区代码 / adcode)映射到实体属性;几何数据通过 ExportToWkt() 转为 WKT 字符串存入 geom 字段。

4.5 GeoTools 方式解析(非 GDB 格式)

对于 Shapefile、GeoPackage、GeoJSON 等格式,使用 GeoTools 库解析(无需 GDAL):

java 复制代码
// Shapefile 解析
Map<String, Object> map = new HashMap<>();
map.put("url", shpFile.toURI().toURL());
map.put("charset", "UTF-8");  // 失败时自动降级 GBK
DataStore dataStore = DataStoreFinder.getDataStore(map);
SimpleFeatureSource featureSource = dataStore.getFeatureSource(typeName);
SimpleFeatureCollection features = featureSource.getFeatures();

try (SimpleFeatureIterator iterator = features.features()) {
    while (iterator.hasNext()) {
        SimpleFeature feature = iterator.next();
        JcXzq xzq = convertFeatureToXzq(feature);

        // 坐标系自动转换到 WGS84
        CoordinateReferenceSystem sourceCRS = feature.getFeatureType()
            .getCoordinateReferenceSystem();
        CoordinateReferenceSystem targetCRS = CRS.decode("EPSG:4326", true);
        MathTransform transform = CRS.findMathTransform(sourceCRS, targetCRS, true);
        Geometry geometry = JTS.transform((Geometry) feature.getDefaultGeometry(), transform);
        xzq.setGeom(new WKTWriter().write(geometry));
    }
}

逻辑: GeoTools 通过 DataStoreFinder 自动识别格式,读取 Feature 后完成字段映射 + 坐标系转换(自动转为 WGS84)。


五、注意事项

  1. GDAL 环境 --- 解析 .gdb 格式必须安装 GDAL 本地库并配置 PATHGDAL_DATAPROJ_LIB 环境变量及 java.library.path JVM 参数。如环境配置困难可使用 ogr2ogr 命令行将 GDB 转 Shapefile 后通过 GeoTools 解析。
  2. GDB 目录结构 --- ESRI File Geodatabase 在文件系统上是一个目录,解压时必须保留完整目录结构和所有内部文件,否则 GDAL 无法打开。
  3. 编码问题 --- 中国 Spatial Data 常用 GBK 编码,Shapefile 的 .dbf 属性文件可能是 GBK。GeoTools 自动降级:UTF-8 失败后自动切 GBK。
  4. 坐标转换 --- 中国常用坐标系(Xian80 / Beijing54 / CGCS2000)需配置 EPSG 参数才能正确转为 WGS84。CRS.AxisOrder.NORTH_EAST(如 EPSG:4326)需设置 true 参数启用强制 XY 顺序。
  5. GDAL 内存释放 --- GDAL Java 绑定的 Feature 和 DataSource 对象必须显式调用 delete() 释放,否则会导致内存泄漏。
  6. 大文件事务 --- @Transactional 标注在整个方法上,GDB 数据量大时可能导致事务超时,建议在 processLayerData 内部分段批量提交。
相关推荐
何中应1 小时前
Nexus如何设置端口号
java·服务器·maven·nexus
思麟呀1 小时前
C++11并发编程:条件变量
java·linux·jvm·c++·windows
Full Stack Developme1 小时前
Hutool CollUtil 教程
java·开发语言·windows·python
我是一颗柠檬1 小时前
【Java项目技术亮点】Kafka异步写+写聚合:吞吐量提升10倍的消息队列优化秘籍
java·分布式·kafka·linq
摇滚侠2 小时前
git ignore 忽略 .idea 目录 全新项目(尚未提交过 .idea).idea 已经被 Git 跟踪(已提交过)
java·git·intellij-idea
linge_sun2 小时前
SpringAI SQL 智能助手实战:用自然语言查询数据库
java·人工智能·ai编程
熟悉的新风景2 小时前
maven常用依赖
java·maven
light blue bird2 小时前
3C 数码电子BOM 协同工作台组件
java·开发语言·jvm·windows·.net·桌面端
我是一颗柠檬2 小时前
【Redis】Redis分布式锁Day13(2026年)
java·redis·分布式·缓存