第11章 - 开发实战案例
第11章 - 开发实战案例
11.1 案例一:GIS数据转换服务
11.1.1 需求分析
开发一个GIS数据格式转换服务,支持:
- 多种输入格式(Shapefile、GeoJSON、FileGDB)
- 多种输出格式(Shapefile、GeoJSON、PostGIS)
- 坐标系转换
- 属性过滤
11.1.2 核心实现
/**
* GIS数据转换服务
*/
public class GisConversionService {
private static final Logger log = LoggerFactory.getLogger(GisConversionService.class);
/**
* 转换请求
*/
@Data
public static class ConversionRequest {
private DataFormatType sourceFormat;
private String sourcePath;
private String sourceLayerName;
private DataFormatType targetFormat;
private String targetPath;
private String targetLayerName;
private Integer targetWkid; // 目标坐标系,null表示不转换
private String attributeFilter; // 属性过滤条件
}
/**
* 转换结果
*/
@Data
public static class ConversionResult {
private boolean success;
private String message;
private int featureCount;
private long timeMs;
}
/**
* 执行转换
*/
public ConversionResult convert(ConversionRequest request) {
long startTime = System.currentTimeMillis();
ConversionResult result = new ConversionResult();
try {
// 1. 验证请求
validateRequest(request);
// 2. 选择引擎
GisEngineType readEngine = selectEngine(request.getSourceFormat());
GisEngineType writeEngine = selectEngine(request.getTargetFormat());
log.info("开始转换: {} -> {}", request.getSourceFormat(), request.getTargetFormat());
// 3. 读取数据
OguLayer layer = OguLayerUtil.readLayer(
request.getSourceFormat(),
request.getSourcePath(),
request.getSourceLayerName(),
request.getAttributeFilter(),
null,
readEngine
);
log.info("读取完成: {} 个要素", layer.getFeatureCount());
// 4. 坐标转换
if (request.getTargetWkid() != null &&
!request.getTargetWkid().equals(layer.getWkid())) {
layer = CrsUtil.reproject(layer, request.getTargetWkid());
log.info("坐标转换完成: WKID {}", request.getTargetWkid());
}
// 5. 写入数据
OguLayerUtil.writeLayer(
request.getTargetFormat(),
layer,
request.getTargetPath(),
request.getTargetLayerName(),
null,
writeEngine
);
// 6. 返回结果
result.setSuccess(true);
result.setMessage("转换成功");
result.setFeatureCount(layer.getFeatureCount());
} catch (DataSourceException e) {
result.setSuccess(false);
result.setMessage("数据源错误: " + e.getMessage());
log.error("转换失败", e);
} catch (FormatParseException e) {
result.setSuccess(false);
result.setMessage("格式错误: " + e.getMessage());
log.error("转换失败", e);
} catch (OguException e) {
result.setSuccess(false);
result.setMessage("转换错误: " + e.getMessage());
log.error("转换失败", e);
}
result.setTimeMs(System.currentTimeMillis() - startTime);
return result;
}
/**
* 验证请求
*/
private void validateRequest(ConversionRequest request) {
if (request.getSourceFormat() == null) {
throw new IllegalArgumentException("源格式不能为空");
}
if (request.getTargetFormat() == null) {
throw new IllegalArgumentException("目标格式不能为空");
}
if (request.getSourcePath() == null || request.getSourcePath().isEmpty()) {
throw new IllegalArgumentException("源路径不能为空");
}
if (request.getTargetPath() == null || request.getTargetPath().isEmpty()) {
throw new IllegalArgumentException("目标路径不能为空");
}
}
/**
* 选择合适的引擎
*/
private GisEngineType selectEngine(DataFormatType format) {
if (format == DataFormatType.FILEGDB) {
if (!GisEngineFactory.isEngineAvailable(GisEngineType.GDAL)) {
throw new EngineNotSupportedException("FileGDB格式需要GDAL支持");
}
return GisEngineType.GDAL;
}
return GisEngineType.GEOTOOLS;
}
}
11.1.3 使用示例
public class ConversionServiceDemo {
public static void main(String[] args) {
GisConversionService service = new GisConversionService();
// Shapefile转GeoJSON
GisConversionService.ConversionRequest request =
new GisConversionService.ConversionRequest();
request.setSourceFormat(DataFormatType.SHP);
request.setSourcePath("D:/data/cities.shp");
request.setTargetFormat(DataFormatType.GEOJSON);
request.setTargetPath("D:/output/cities.geojson");
GisConversionService.ConversionResult result = service.convert(request);
if (result.isSuccess()) {
System.out.printf("转换成功: %d 个要素, 耗时 %d ms%n",
result.getFeatureCount(), result.getTimeMs());
} else {
System.out.println("转换失败: " + result.getMessage());
}
}
}
11.2 案例二:空间查询服务
11.2.1 需求分析
开发一个空间查询服务,支持:
- 点查询(查找包含指定点的要素)
- 范围查询(查找与范围相交的要素)
- 缓冲区查询(查找距离指定几何一定范围内的要素)
11.2.2 核心实现
/**
* 空间查询服务
*/
public class SpatialQueryService {
private final OguLayer layer;
private Map<String, Geometry> geometryCache;
public SpatialQueryService(OguLayer layer) {
this.layer = layer;
initCache();
}
/**
* 初始化几何缓存
*/
private void initCache() {
geometryCache = new HashMap<>();
for (OguFeature feature : layer.getFeatures()) {
if (feature.getWkt() != null) {
Geometry geom = GeometryUtil.wkt2Geometry(feature.getWkt());
geometryCache.put(feature.getFid(), geom);
}
}
}
/**
* 点查询
*/
public List<OguFeature> queryByPoint(double x, double y) {
Geometry point = GeometryUtil.wkt2Geometry(
String.format("POINT(%f %f)", x, y));
return layer.getFeatures().stream()
.filter(f -> {
Geometry geom = geometryCache.get(f.getFid());
return geom != null && GeometryUtil.contains(geom, point);
})
.collect(Collectors.toList());
}
/**
* 范围查询
*/
public List<OguFeature> queryByBounds(double minX, double minY,
double maxX, double maxY) {
String boundsWkt = String.format(
"POLYGON((%f %f, %f %f, %f %f, %f %f, %f %f))",
minX, minY, maxX, minY, maxX, maxY, minX, maxY, minX, minY);
Geometry bounds = GeometryUtil.wkt2Geometry(boundsWkt);
return layer.getFeatures().stream()
.filter(f -> {
Geometry geom = geometryCache.get(f.getFid());
return geom != null && GeometryUtil.intersects(bounds, geom);
})
.collect(Collectors.toList());
}
/**
* 缓冲区查询
*/
public List<OguFeature> queryByBuffer(String wkt, double distance) {
Geometry geom = GeometryUtil.wkt2Geometry(wkt);
Geometry buffer = GeometryUtil.buffer(geom, distance);
return layer.getFeatures().stream()
.filter(f -> {
Geometry fGeom = geometryCache.get(f.getFid());
return fGeom != null && GeometryUtil.intersects(buffer, fGeom);
})
.collect(Collectors.toList());
}
/**
* 最近邻查询
*/
public List<OguFeature> queryNearest(double x, double y, int count) {
Geometry point = GeometryUtil.wkt2Geometry(
String.format("POINT(%f %f)", x, y));
// 计算距离并排序
return layer.getFeatures().stream()
.filter(f -> geometryCache.containsKey(f.getFid()))
.sorted((f1, f2) -> {
Geometry g1 = geometryCache.get(f1.getFid());
Geometry g2 = geometryCache.get(f2.getFid());
double d1 = GeometryUtil.distance(point, g1);
double d2 = GeometryUtil.distance(point, g2);
return Double.compare(d1, d2);
})
.limit(count)
.collect(Collectors.toList());
}
/**
* 空间关系查询
*/
public List<OguFeature> queryBySpatialRelation(String wkt,
SpatialRelation relation) {
Geometry queryGeom = GeometryUtil.wkt2Geometry(wkt);
return layer.getFeatures().stream()
.filter(f -> {
Geometry geom = geometryCache.get(f.getFid());
if (geom == null) return false;
switch (relation) {
case INTERSECTS:
return GeometryUtil.intersects(queryGeom, geom);
case CONTAINS:
return GeometryUtil.contains(queryGeom, geom);
case WITHIN:
return GeometryUtil.within(geom, queryGeom);
case TOUCHES:
return GeometryUtil.touches(queryGeom, geom);
case OVERLAPS:
return GeometryUtil.overlaps(queryGeom, geom);
default:
return false;
}
})
.collect(Collectors.toList());
}
public enum SpatialRelation {
INTERSECTS, CONTAINS, WITHIN, TOUCHES, OVERLAPS
}
}
11.2.3 使用示例
public class SpatialQueryDemo {
public static void main(String[] args) {
// 加载数据
OguLayer layer = OguLayerUtil.readLayer(
DataFormatType.SHP,
"D:/data/districts.shp",
null, null, null,
GisEngineType.GEOTOOLS
);
SpatialQueryService queryService = new SpatialQueryService(layer);
// 点查询
System.out.println("=== 点查询 ===");
List<OguFeature> result1 = queryService.queryByPoint(116.4, 39.9);
for (OguFeature f : result1) {
System.out.println(" " + f.getValue("NAME"));
}
// 范围查询
System.out.println("\n=== 范围查询 ===");
List<OguFeature> result2 = queryService.queryByBounds(116, 39, 117, 40);
System.out.println(" 找到 " + result2.size() + " 个要素");
// 最近邻查询
System.out.println("\n=== 最近3个 ===");
List<OguFeature> result3 = queryService.queryNearest(116.4, 39.9, 3);
for (OguFeature f : result3) {
System.out.println(" " + f.getValue("NAME"));
}
}
}
11.3 案例三:国土数据处理
11.3.1 需求分析
处理自然资源部门的数据:
- 读取国土TXT坐标文件
- 进行几何验证和修复
- 计算面积
- 导出为Shapefile
11.3.2 核心实现
/**
* 国土数据处理服务
*/
public class LandDataProcessor {
/**
* 处理结果
*/
@Data
public static class ProcessResult {
private int totalCount;
private int validCount;
private int repairedCount;
private int failedCount;
private double totalArea;
private List<String> errors = new ArrayList<>();
}
/**
* 处理国土TXT文件
*/
public ProcessResult processTxtFile(String txtPath, String outputShpPath) {
ProcessResult result = new ProcessResult();
try {
// 1. 读取TXT
OguLayer layer = GtTxtUtil.loadTxt(txtPath, null);
result.setTotalCount(layer.getFeatureCount());
// 2. 添加面积字段
addAreaField(layer);
// 3. 验证和修复每个要素
for (OguFeature feature : layer.getFeatures()) {
try {
processFeature(feature, layer.getWkid(), result);
} catch (Exception e) {
result.setFailedCount(result.getFailedCount() + 1);
result.getErrors().add(String.format("FID %s: %s",
feature.getFid(), e.getMessage()));
}
}
// 4. 移除无效要素
layer.getFeatures().removeIf(f -> f.getWkt() == null);
// 5. 导出Shapefile
if (!layer.getFeatures().isEmpty()) {
OguLayerUtil.writeLayer(
DataFormatType.SHP,
layer,
outputShpPath,
null, null,
GisEngineType.GEOTOOLS
);
}
} catch (Exception e) {
result.getErrors().add("处理失败: " + e.getMessage());
}
return result;
}
/**
* 处理单个要素
*/
private void processFeature(OguFeature feature, Integer wkid, ProcessResult result) {
if (feature.getWkt() == null || feature.getWkt().isEmpty()) {
// 尝试从坐标点构建几何
if (feature.getCoordinates() != null && !feature.getCoordinates().isEmpty()) {
feature.setWkt(buildPolygonFromCoordinates(feature.getCoordinates()));
} else {
result.setFailedCount(result.getFailedCount() + 1);
return;
}
}
Geometry geom = GeometryUtil.wkt2Geometry(feature.getWkt());
// 验证几何
TopologyValidationResult validResult = GeometryUtil.isValid(geom);
if (!validResult.isValid()) {
// 尝试修复
try {
geom = GeometryUtil.validate(geom);
feature.setWkt(GeometryUtil.geometry2Wkt(geom));
result.setRepairedCount(result.getRepairedCount() + 1);
} catch (Exception e) {
result.setFailedCount(result.getFailedCount() + 1);
feature.setWkt(null);
return;
}
} else {
result.setValidCount(result.getValidCount() + 1);
}
// 计算面积
double area = calculateArea(geom, wkid);
feature.setValue("AREA_M2", area);
feature.setValue("AREA_MU", area / 666.67);
result.setTotalArea(result.getTotalArea() + area);
}
/**
* 从坐标点构建多边形WKT
*/
private String buildPolygonFromCoordinates(List<OguCoordinate> coordinates) {
if (coordinates.size() < 4) {
throw new TopologyException("坐标点太少,无法构建多边形");
}
StringBuilder sb = new StringBuilder("POLYGON((");
for (int i = 0; i < coordinates.size(); i++) {
OguCoordinate coord = coordinates.get(i);
if (i > 0) sb.append(", ");
sb.append(NumUtil.getPlainString(coord.getX()))
.append(" ")
.append(NumUtil.getPlainString(coord.getY()));
}
sb.append("))");
return sb.toString();
}
/**
* 计算面积(平方米)
*/
private double calculateArea(Geometry geom, Integer wkid) {
if (wkid == null || wkid == 4490) {
// 地理坐标系,需要转换
int dh = CrsUtil.getDh(geom);
Integer projWkid = CrsUtil.getProjectedWkid(dh);
geom = CrsUtil.transform(geom, wkid != null ? wkid : 4490, projWkid);
}
return GeometryUtil.area(geom);
}
/**
* 添加面积字段
*/
private void addAreaField(OguLayer layer) {
List<OguField> fields = new ArrayList<>(layer.getFields());
OguField areaM2 = new OguField();
areaM2.setName("AREA_M2");
areaM2.setAlias("面积(平方米)");
areaM2.setDataType(FieldDataType.DOUBLE);
fields.add(areaM2);
OguField areaMu = new OguField();
areaMu.setName("AREA_MU");
areaMu.setAlias("面积(亩)");
areaMu.setDataType(FieldDataType.DOUBLE);
fields.add(areaMu);
layer.setFields(fields);
}
}
11.3.3 使用示例
public class LandDataDemo {
public static void main(String[] args) {
LandDataProcessor processor = new LandDataProcessor();
LandDataProcessor.ProcessResult result = processor.processTxtFile(
"D:/data/land.txt",
"D:/output/land.shp"
);
System.out.println("=== 处理结果 ===");
System.out.println("总数量: " + result.getTotalCount());
System.out.println("有效: " + result.getValidCount());
System.out.println("已修复: " + result.getRepairedCount());
System.out.println("失败: " + result.getFailedCount());
System.out.printf("总面积: %.2f 平方米 (%.2f 亩)%n",
result.getTotalArea(), result.getTotalArea() / 666.67);
if (!result.getErrors().isEmpty()) {
System.out.println("\n=== 错误信息 ===");
for (String error : result.getErrors()) {
System.out.println(" " + error);
}
}
}
}
11.4 案例四:图层合并工具
11.4.1 需求分析
开发一个图层合并工具:
- 合并多个同结构图层
- 支持字段映射
- 去除重复要素
- 验证几何
11.4.2 核心实现
/**
* 图层合并工具
*/
public class LayerMerger {
/**
* 合并配置
*/
@Data
public static class MergeConfig {
private boolean removeDuplicates = true;
private boolean validateGeometry = true;
private Map<String, String> fieldMapping = new HashMap<>(); // 源字段 -> 目标字段
}
/**
* 合并多个图层
*/
public OguLayer merge(List<OguLayer> layers, MergeConfig config) {
if (layers == null || layers.isEmpty()) {
throw new IllegalArgumentException("图层列表不能为空");
}
// 使用第一个图层作为模板
OguLayer template = layers.get(0);
OguLayer result = new OguLayer();
result.setName("merged_layer");
result.setWkid(template.getWkid());
result.setGeometryType(template.getGeometryType());
result.setFields(new ArrayList<>(template.getFields()));
result.setFeatures(new ArrayList<>());
Set<String> wktSet = config.isRemoveDuplicates() ? new HashSet<>() : null;
int fid = 1;
for (OguLayer layer : layers) {
// 验证图层兼容性
if (layer.getGeometryType() != template.getGeometryType()) {
throw new LayerValidationException("几何类型不一致: " + layer.getName());
}
for (OguFeature sourceFeature : layer.getFeatures()) {
// 去重检查
if (wktSet != null && sourceFeature.getWkt() != null) {
if (wktSet.contains(sourceFeature.getWkt())) {
continue; // 跳过重复
}
wktSet.add(sourceFeature.getWkt());
}
// 几何验证
if (config.isValidateGeometry() && sourceFeature.getWkt() != null) {
Geometry geom = GeometryUtil.wkt2Geometry(sourceFeature.getWkt());
TopologyValidationResult valid = GeometryUtil.isValid(geom);
if (!valid.isValid()) {
try {
geom = GeometryUtil.validate(geom);
sourceFeature.setWkt(GeometryUtil.geometry2Wkt(geom));
} catch (Exception e) {
continue; // 跳过无效几何
}
}
}
// 创建新要素
OguFeature newFeature = new OguFeature();
newFeature.setFid(String.valueOf(fid++));
newFeature.setWkt(sourceFeature.getWkt());
// 复制属性(考虑字段映射)
for (OguField field : template.getFields()) {
String sourceFieldName = config.getFieldMapping()
.getOrDefault(field.getName(), field.getName());
Object value = sourceFeature.getValue(sourceFieldName);
newFeature.setValue(field.getName(), value);
}
result.getFeatures().add(newFeature);
}
}
return result;
}
/**
* 从文件列表合并
*/
public OguLayer mergeFromFiles(List<String> shpPaths, MergeConfig config) {
List<OguLayer> layers = new ArrayList<>();
for (String path : shpPaths) {
OguLayer layer = OguLayerUtil.readLayer(
DataFormatType.SHP,
path,
null, null, null,
GisEngineType.GEOTOOLS
);
layers.add(layer);
}
return merge(layers, config);
}
}
11.4.3 使用示例
public class LayerMergerDemo {
public static void main(String[] args) {
LayerMerger merger = new LayerMerger();
// 配置
LayerMerger.MergeConfig config = new LayerMerger.MergeConfig();
config.setRemoveDuplicates(true);
config.setValidateGeometry(true);
// 合并文件
List<String> files = Arrays.asList(
"D:/data/region1.shp",
"D:/data/region2.shp",
"D:/data/region3.shp"
);
OguLayer merged = merger.mergeFromFiles(files, config);
System.out.println("合并完成: " + merged.getFeatureCount() + " 个要素");
// 保存结果
OguLayerUtil.writeLayer(
DataFormatType.SHP,
merged,
"D:/output/merged.shp",
null, null,
GisEngineType.GEOTOOLS
);
}
}
11.5 案例五:数据质量检查
11.5.1 需求分析
开发一个数据质量检查工具:
- 几何有效性检查
- 属性完整性检查
- 坐标范围检查
- 生成检查报告
11.5.2 核心实现
/**
* 数据质量检查服务
*/
public class DataQualityChecker {
@Data
public static class QualityReport {
private String layerName;
private int totalFeatures;
private int validGeometries;
private int invalidGeometries;
private int emptyGeometries;
private Map<String, Integer> fieldCompleteness = new HashMap<>();
private List<QualityIssue> issues = new ArrayList<>();
}
@Data
@AllArgsConstructor
public static class QualityIssue {
private String featureId;
private String issueType;
private String description;
}
/**
* 检查图层质量
*/
public QualityReport check(OguLayer layer) {
QualityReport report = new QualityReport();
report.setLayerName(layer.getName());
report.setTotalFeatures(layer.getFeatureCount());
// 初始化字段统计
for (OguField field : layer.getFields()) {
report.getFieldCompleteness().put(field.getName(), 0);
}
// 检查每个要素
for (OguFeature feature : layer.getFeatures()) {
checkFeature(feature, layer, report);
}
return report;
}
/**
* 检查单个要素
*/
private void checkFeature(OguFeature feature, OguLayer layer, QualityReport report) {
// 1. 几何检查
checkGeometry(feature, layer.getWkid(), report);
// 2. 属性检查
checkAttributes(feature, layer.getFields(), report);
}
/**
* 几何检查
*/
private void checkGeometry(OguFeature feature, Integer wkid, QualityReport report) {
String wkt = feature.getWkt();
if (wkt == null || wkt.isEmpty()) {
report.setEmptyGeometries(report.getEmptyGeometries() + 1);
report.getIssues().add(new QualityIssue(
feature.getFid(), "EMPTY_GEOMETRY", "几何为空"));
return;
}
try {
Geometry geom = GeometryUtil.wkt2Geometry(wkt);
// 有效性检查
TopologyValidationResult validResult = GeometryUtil.isValid(geom);
if (!validResult.isValid()) {
report.setInvalidGeometries(report.getInvalidGeometries() + 1);
report.getIssues().add(new QualityIssue(
feature.getFid(),
"INVALID_GEOMETRY",
validResult.getErrorType().getDesc()));
} else {
report.setValidGeometries(report.getValidGeometries() + 1);
}
// 坐标范围检查
if (wkid != null) {
checkCoordinateRange(feature.getFid(), geom, wkid, report);
}
} catch (Exception e) {
report.setInvalidGeometries(report.getInvalidGeometries() + 1);
report.getIssues().add(new QualityIssue(
feature.getFid(), "PARSE_ERROR", "WKT解析失败: " + e.getMessage()));
}
}
/**
* 坐标范围检查
*/
private void checkCoordinateRange(String fid, Geometry geom, int wkid,
QualityReport report) {
boolean isProjected = CrsUtil.isProjectedCRS(CrsUtil.getCRS(wkid));
for (Coordinate coord : geom.getCoordinates()) {
if (isProjected) {
// 投影坐标检查
if (coord.x < 0 || coord.y < 0 || coord.x > 1e9 || coord.y > 1e9) {
report.getIssues().add(new QualityIssue(
fid, "COORDINATE_RANGE",
String.format("可疑坐标: (%.2f, %.2f)", coord.x, coord.y)));
break;
}
} else {
// 地理坐标检查
if (coord.x < -180 || coord.x > 180 || coord.y < -90 || coord.y > 90) {
report.getIssues().add(new QualityIssue(
fid, "COORDINATE_RANGE",
String.format("坐标超出范围: (%.4f, %.4f)", coord.x, coord.y)));
break;
}
}
}
}
/**
* 属性检查
*/
private void checkAttributes(OguFeature feature, List<OguField> fields,
QualityReport report) {
for (OguField field : fields) {
Object value = feature.getValue(field.getName());
if (value != null && !value.toString().isEmpty()) {
report.getFieldCompleteness().merge(field.getName(), 1, Integer::sum);
} else if (!field.getNullable()) {
report.getIssues().add(new QualityIssue(
feature.getFid(),
"MISSING_VALUE",
"必填字段为空: " + field.getName()));
}
}
}
/**
* 生成报告文本
*/
public String generateReport(QualityReport report) {
StringBuilder sb = new StringBuilder();
sb.append("=== 数据质量检查报告 ===\n\n");
sb.append(String.format("图层名称: %s%n", report.getLayerName()));
sb.append(String.format("要素总数: %d%n", report.getTotalFeatures()));
sb.append(String.format("有效几何: %d (%.1f%%)%n",
report.getValidGeometries(),
100.0 * report.getValidGeometries() / report.getTotalFeatures()));
sb.append(String.format("无效几何: %d%n", report.getInvalidGeometries()));
sb.append(String.format("空几何: %d%n", report.getEmptyGeometries()));
sb.append("\n字段完整性:\n");
for (Map.Entry<String, Integer> entry : report.getFieldCompleteness().entrySet()) {
double rate = 100.0 * entry.getValue() / report.getTotalFeatures();
sb.append(String.format(" %s: %.1f%%%n", entry.getKey(), rate));
}
if (!report.getIssues().isEmpty()) {
sb.append(String.format("%n问题列表 (%d个):%n", report.getIssues().size()));
int count = 0;
for (QualityIssue issue : report.getIssues()) {
if (++count > 20) {
sb.append(" ... 更多问题省略\n");
break;
}
sb.append(String.format(" [%s] FID=%s: %s%n",
issue.getIssueType(), issue.getFeatureId(), issue.getDescription()));
}
}
return sb.toString();
}
}
11.5.3 使用示例
public class QualityCheckDemo {
public static void main(String[] args) {
// 加载数据
OguLayer layer = OguLayerUtil.readLayer(
DataFormatType.SHP,
"D:/data/sample.shp",
null, null, null,
GisEngineType.GEOTOOLS
);
// 检查质量
DataQualityChecker checker = new DataQualityChecker();
DataQualityChecker.QualityReport report = checker.check(layer);
// 输出报告
String reportText = checker.generateReport(report);
System.out.println(reportText);
// 保存报告
try {
Files.write(Paths.get("D:/output/quality_report.txt"),
reportText.getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
e.printStackTrace();
}
}
}

浙公网安备 33010602011771号