commit 967fa0f59cc6efbf1df35a1f0ebd0496d8269da5 Author: SuiteForge Date: Tue Jul 21 00:10:50 2026 +0800 feat: 添加 Linux/Windows 双平台测试套件(云南省界叠加分析) - test-linux: platform=linux, docker + python3 - test-windows: platform=windows, arcpy + python3 - 附示例 GeoJSON: 云南/昆明/大理/百色 Ref: SuiteHub/agent-profiles#22 diff --git a/suites/test-linux/data/baise.geojson b/suites/test-linux/data/baise.geojson new file mode 100644 index 0000000..b3ca548 --- /dev/null +++ b/suites/test-linux/data/baise.geojson @@ -0,0 +1,17 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { "name": "百色市", "code": "451000", "population": 3600000 }, + "geometry": { + "type": "Polygon", + "coordinates": [[ + [105.5, 24.5], [105.8, 24.3], [106.2, 24.0], [106.8, 23.8], + [107.3, 23.5], [107.5, 23.2], [107.3, 23.0], [107.0, 23.2], + [106.5, 23.5], [106.0, 23.8], [105.7, 24.0], [105.5, 24.5] + ]] + } + } + ] +} diff --git a/suites/test-linux/data/dali.geojson b/suites/test-linux/data/dali.geojson new file mode 100644 index 0000000..771b453 --- /dev/null +++ b/suites/test-linux/data/dali.geojson @@ -0,0 +1,17 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { "name": "大理州", "code": "532900", "population": 3300000 }, + "geometry": { + "type": "Polygon", + "coordinates": [[ + [99.5, 26.5], [99.8, 26.3], [100.0, 26.2], [100.3, 26.0], + [100.5, 25.8], [100.4, 25.5], [100.3, 25.3], [100.1, 25.1], + [99.9, 25.0], [99.7, 25.1], [99.5, 25.3], [99.5, 26.5] + ]] + } + } + ] +} diff --git a/suites/test-linux/data/kunming.geojson b/suites/test-linux/data/kunming.geojson new file mode 100644 index 0000000..87dbc59 --- /dev/null +++ b/suites/test-linux/data/kunming.geojson @@ -0,0 +1,18 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { "name": "昆明市", "code": "530100", "population": 8500000 }, + "geometry": { + "type": "Polygon", + "coordinates": [[ + [102.4, 25.7], [102.6, 25.6], [102.9, 25.5], [103.1, 25.4], + [103.4, 25.2], [103.3, 24.9], [103.2, 24.7], [103.0, 24.5], + [102.8, 24.4], [102.6, 24.5], [102.5, 24.7], [102.4, 24.9], + [102.4, 25.7] + ]] + } + } + ] +} diff --git a/suites/test-linux/data/yunnan.geojson b/suites/test-linux/data/yunnan.geojson new file mode 100644 index 0000000..07fee87 --- /dev/null +++ b/suites/test-linux/data/yunnan.geojson @@ -0,0 +1,19 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { "name": "云南省", "code": "530000" }, + "geometry": { + "type": "Polygon", + "coordinates": [[ + [97.5, 27.5], [98.5, 28.8], [100.0, 29.3], [101.5, 28.5], + [103.0, 28.0], [104.5, 27.5], [105.5, 26.5], [106.2, 25.5], + [106.0, 24.0], [105.5, 23.0], [104.5, 22.2], [103.5, 22.0], + [102.5, 21.5], [101.5, 21.1], [100.5, 21.8], [99.5, 22.5], + [98.5, 23.5], [97.8, 24.5], [97.5, 25.5], [97.5, 27.5] + ]] + } + } + ] +} diff --git a/suites/test-linux/scripts/convert.py b/suites/test-linux/scripts/convert.py new file mode 100644 index 0000000..23f90ab --- /dev/null +++ b/suites/test-linux/scripts/convert.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +""" +Step 1: GeoJSON → SHP +将省界和城市 GeoJSON 文件转换为 Shapefile +""" +import sys +import json +import os +import geopandas as gpd + + +def main(): + province_path = os.environ.get("PARAM_PROVINCE", "") + cities_param = os.environ.get("PARAM_CITIES", "") + output_dir = os.environ.get("PARAM_OUTPUT_DIR", "/tmp/output/shp") + + if not province_path: + print(json.dumps({"error": "缺少 province 参数", "success": False})) + sys.exit(1) + if not cities_param: + print(json.dumps({"error": "缺少 cities 参数", "success": False})) + sys.exit(1) + + os.makedirs(output_dir, exist_ok=True) + + # 读取并转换省界 + print(json.dumps({"msg": f"读取省界: {province_path}"})) + province_gdf = gpd.read_file(province_path) + province_shp = os.path.join(output_dir, "province.shp") + province_gdf.to_file(province_shp, driver="ESRI Shapefile") + print(json.dumps({"msg": f"省界已转为 SHP: {province_shp}", "count": len(province_gdf)})) + + # 读取并转换每个城市 GeoJSON + city_files = [c.strip() for c in cities_param.split(",") if c.strip()] + converted = [] + for cf in city_files: + if not os.path.isfile(cf): + print(json.dumps({"warn": f"文件不存在,跳过: {cf}"})) + continue + print(json.dumps({"msg": f"读取城市: {cf}"})) + gdf = gpd.read_file(cf) + base = os.path.splitext(os.path.basename(cf))[0] + shp_path = os.path.join(output_dir, f"{base}.shp") + gdf.to_file(shp_path, driver="ESRI Shapefile") + converted.append(base) + print(json.dumps({"msg": f"已转换: {cf} → {shp_path}", "count": len(gdf)})) + + result = { + "success": True, + "province_shp": province_shp, + "city_count": len(converted), + "cities": converted + } + print(json.dumps(result)) + + +if __name__ == "__main__": + main() diff --git a/suites/test-linux/scripts/excel.py b/suites/test-linux/scripts/excel.py new file mode 100644 index 0000000..7940c75 --- /dev/null +++ b/suites/test-linux/scripts/excel.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +""" +Step 3: 输出 Excel +将叠加分析结果写入 Excel 文件 +""" +import sys +import json +import os +from openpyxl import Workbook +from openpyxl.styles import Font, Alignment, PatternFill + + +def main(): + result_dir = os.environ.get("PARAM_RESULT_DIR", "/tmp/output/result") + output_path = os.environ.get("PARAM_OUTPUT_PATH", "") + + if not output_path: + print(json.dumps({"error": "缺少 output_path 参数", "success": False})) + sys.exit(1) + + # 读取分析结果 + result_json = os.path.join(result_dir, "overlay_results.json") + if not os.path.isfile(result_json): + print(json.dumps({"error": f"结果文件不存在: {result_json}", "success": False})) + sys.exit(1) + + with open(result_json, "r", encoding="utf-8") as f: + results = json.load(f) + + # 创建工作簿 + wb = Workbook() + ws = wb.active + ws.title = "叠加分析结果" + + # 标题样式 + header_font = Font(bold=True, size=12, color="FFFFFF") + header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid") + header_align = Alignment(horizontal="center", vertical="center") + + # 表头 + headers = ["城市", "城市代码", "城市名称", "人口", "在省内", "相交", "状态"] + for col, h in enumerate(headers, 1): + cell = ws.cell(row=1, column=col, value=h) + cell.font = header_font + cell.fill = header_fill + cell.alignment = header_align + + # 数据行 + pass_fill = PatternFill(start_color="E2EFDA", end_color="E2EFDA", fill_type="solid") + fail_fill = PatternFill(start_color="FCE4EC", end_color="FCE4EC", fill_type="solid") + + for row_idx, r in enumerate(results, 2): + ws.cell(row=row_idx, column=1, value=r.get("city", "")) + ws.cell(row=row_idx, column=2, value=r.get("city_code", "")) + ws.cell(row=row_idx, column=3, value=r.get("city_name", "")) + ws.cell(row=row_idx, column=4, value=r.get("population", 0)) + ws.cell(row=row_idx, column=5, value="是" if r.get("in_province") else "否") + ws.cell(row=row_idx, column=6, value="是" if r.get("intersects") else "否") + ws.cell(row=row_idx, column=7, value=r.get("status", "")) + + # 条件高亮 + fill = pass_fill if r.get("in_province") else fail_fill + for col in range(1, 8): + ws.cell(row=row_idx, column=col).fill = fill + + # 调整列宽 + ws.column_dimensions["A"].width = 15 + ws.column_dimensions["B"].width = 12 + ws.column_dimensions["C"].width = 15 + ws.column_dimensions["D"].width = 12 + ws.column_dimensions["E"].width = 10 + ws.column_dimensions["F"].width = 10 + ws.column_dimensions["G"].width = 15 + + # 确保输出目录存在 + out_dir = os.path.dirname(output_path) + if out_dir: + os.makedirs(out_dir, exist_ok=True) + + wb.save(output_path) + print(json.dumps({ + "success": True, + "output_path": output_path, + "total": len(results), + "msg": f"Excel 已输出到: {output_path}" + })) + + +if __name__ == "__main__": + main() diff --git a/suites/test-linux/scripts/overlay.py b/suites/test-linux/scripts/overlay.py new file mode 100644 index 0000000..fc4e6c9 --- /dev/null +++ b/suites/test-linux/scripts/overlay.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +""" +Step 2: 叠加分析 +判断各城市是否在省界内(spatial join),输出分析结果 +""" +import sys +import json +import os +import glob +import geopandas as gpd + + +def main(): + province_shp = os.environ.get("PARAM_PROVINCE_SHP", "") + cities_dir = os.environ.get("PARAM_CITIES_DIR", "/tmp/output/shp") + output_dir = os.environ.get("PARAM_OUTPUT_DIR", "/tmp/output/result") + + if not province_shp or not os.path.isfile(province_shp): + print(json.dumps({"error": f"省界 SHP 文件不存在: {province_shp}", "success": False})) + sys.exit(1) + + os.makedirs(output_dir, exist_ok=True) + + # 读取省界 + print(json.dumps({"msg": f"读取省界 SHP: {province_shp}"})) + province = gpd.read_file(province_shp) + + if province.empty: + print(json.dumps({"error": "省界数据为空", "success": False})) + sys.exit(1) + + # 确保坐标系一致 + target_crs = province.crs or "EPSG:4326" + province = province.to_crs(target_crs) + + # 查找所有城市 SHP + city_shp_files = glob.glob(os.path.join(cities_dir, "*.shp")) + city_shp_files = [f for f in city_shp_files + if os.path.basename(f) != "province.shp"] + + if not city_shp_files: + print(json.dumps({"error": "未找到城市 SHP 文件", "success": False})) + sys.exit(1) + + results = [] + + for shp in city_shp_files: + city_name = os.path.splitext(os.path.basename(shp))[0] + print(json.dumps({"msg": f"分析城市: {city_name}"})) + city = gpd.read_file(shp) + + if city.empty: + results.append({"city": city_name, "in_province": False, + "reason": "数据为空", "status": "跳过"}) + continue + + city = city.to_crs(target_crs) + + # 空间判断:城市中心点是否在省界内 + city_centroid = city.geometry.centroid.iloc[0] + in_province = province.geometry.contains(city_centroid).any() + + # 也检查 intersect 作为辅助判断 + intersects = province.geometry.intersects(city.geometry).any() + + status = "通过 ✅" if in_province else "不通过 ❌" + + city_name_prop = city.iloc[0].get("name", city_name) + city_code = city.iloc[0].get("code", "") + city_pop = city.iloc[0].get("population", 0) + + results.append({ + "city": city_name, + "city_code": str(city_code), + "city_name": str(city_name_prop), + "population": int(city_pop) if city_pop else 0, + "in_province": bool(in_province), + "intersects": bool(intersects), + "status": status + }) + print(json.dumps({"msg": f"{city_name}: {status}"})) + + # 输出分析结果 JSON + result_path = os.path.join(output_dir, "overlay_results.json") + with open(result_path, "w", encoding="utf-8") as f: + json.dump(results, f, ensure_ascii=False, indent=2) + + summary = { + "success": True, + "total_cities": len(results), + "in_province": sum(1 for r in results if r["in_province"]), + "not_in_province": sum(1 for r in results if not r["in_province"]), + "results": results, + "result_json": result_path + } + print(json.dumps(summary)) + + +if __name__ == "__main__": + main() diff --git a/suites/test-linux/workflow.yaml b/suites/test-linux/workflow.yaml new file mode 100644 index 0000000..426d17f --- /dev/null +++ b/suites/test-linux/workflow.yaml @@ -0,0 +1,53 @@ +name: 云南省界叠加分析(Linux 测试) +description: 验证 GIS Actions 4.0 Linux 双平台执行链路的测试套件。将示例 GeoJSON 转为 SHP → 叠加分析(判断城市是否在省内)→ 输出 Excel +version: 1.0.0 +author: SuiteForge +platform: linux +slug: test-yunnan-overlay-linux +tags: [测试, 叠加分析, Linux, 云南省] +category: 测试验证 + +params: + province: + type: string + required: true + desc: 省界 GeoJSON 文件路径 + cities: + type: string + required: true + desc: 待分析城市 GeoJSON 文件路径,多个文件用逗号分隔 + output_path: + type: string + required: true + desc: 输出 Excel 文件路径(.xlsx) + +base_image: gis-base:latest + +steps: + - id: geojson_to_shp + name: 转为 SHP + runtime: docker + script_id: convert + params: + province: $params.province + cities: $params.cities + output_dir: /tmp/output/shp + + - id: overlay + name: 叠加分析 + runtime: docker + script_id: overlay + depends_on: [geojson_to_shp] + params: + province_shp: /tmp/output/shp/province.shp + cities_dir: /tmp/output/shp + output_dir: /tmp/output/result + + - id: export_excel + name: 输出 Excel + runtime: python3 + script_id: excel + depends_on: [overlay] + params: + result_dir: /tmp/output/result + output_path: $params.output_path diff --git a/suites/test-windows/data/baise.geojson b/suites/test-windows/data/baise.geojson new file mode 100644 index 0000000..b3ca548 --- /dev/null +++ b/suites/test-windows/data/baise.geojson @@ -0,0 +1,17 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { "name": "百色市", "code": "451000", "population": 3600000 }, + "geometry": { + "type": "Polygon", + "coordinates": [[ + [105.5, 24.5], [105.8, 24.3], [106.2, 24.0], [106.8, 23.8], + [107.3, 23.5], [107.5, 23.2], [107.3, 23.0], [107.0, 23.2], + [106.5, 23.5], [106.0, 23.8], [105.7, 24.0], [105.5, 24.5] + ]] + } + } + ] +} diff --git a/suites/test-windows/data/dali.geojson b/suites/test-windows/data/dali.geojson new file mode 100644 index 0000000..771b453 --- /dev/null +++ b/suites/test-windows/data/dali.geojson @@ -0,0 +1,17 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { "name": "大理州", "code": "532900", "population": 3300000 }, + "geometry": { + "type": "Polygon", + "coordinates": [[ + [99.5, 26.5], [99.8, 26.3], [100.0, 26.2], [100.3, 26.0], + [100.5, 25.8], [100.4, 25.5], [100.3, 25.3], [100.1, 25.1], + [99.9, 25.0], [99.7, 25.1], [99.5, 25.3], [99.5, 26.5] + ]] + } + } + ] +} diff --git a/suites/test-windows/data/kunming.geojson b/suites/test-windows/data/kunming.geojson new file mode 100644 index 0000000..87dbc59 --- /dev/null +++ b/suites/test-windows/data/kunming.geojson @@ -0,0 +1,18 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { "name": "昆明市", "code": "530100", "population": 8500000 }, + "geometry": { + "type": "Polygon", + "coordinates": [[ + [102.4, 25.7], [102.6, 25.6], [102.9, 25.5], [103.1, 25.4], + [103.4, 25.2], [103.3, 24.9], [103.2, 24.7], [103.0, 24.5], + [102.8, 24.4], [102.6, 24.5], [102.5, 24.7], [102.4, 24.9], + [102.4, 25.7] + ]] + } + } + ] +} diff --git a/suites/test-windows/data/yunnan.geojson b/suites/test-windows/data/yunnan.geojson new file mode 100644 index 0000000..07fee87 --- /dev/null +++ b/suites/test-windows/data/yunnan.geojson @@ -0,0 +1,19 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { "name": "云南省", "code": "530000" }, + "geometry": { + "type": "Polygon", + "coordinates": [[ + [97.5, 27.5], [98.5, 28.8], [100.0, 29.3], [101.5, 28.5], + [103.0, 28.0], [104.5, 27.5], [105.5, 26.5], [106.2, 25.5], + [106.0, 24.0], [105.5, 23.0], [104.5, 22.2], [103.5, 22.0], + [102.5, 21.5], [101.5, 21.1], [100.5, 21.8], [99.5, 22.5], + [98.5, 23.5], [97.8, 24.5], [97.5, 25.5], [97.5, 27.5] + ]] + } + } + ] +} diff --git a/suites/test-windows/scripts/arcpy_convert.py b/suites/test-windows/scripts/arcpy_convert.py new file mode 100644 index 0000000..7cd0267 --- /dev/null +++ b/suites/test-windows/scripts/arcpy_convert.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python +""" +Step 1: GeoJSON → SHP (arcpy) +将省界和城市 GeoJSON 文件转换为 Shapefile +使用 arcpy 的 JSON To Feature Class 工具 +""" +import sys +import json +import os +import arcpy + +# 允许覆写输出 +arcpy.env.overwriteOutput = True + + +def main(): + province_path = os.environ.get("PARAM_PROVINCE", "") + cities_param = os.environ.get("PARAM_CITIES", "") + output_dir = os.environ.get("PARAM_OUTPUT_DIR", r"C:\temp\output\shp") + + if not province_path: + print json.dumps({"error": "缺少 province 参数", "success": False}) + sys.exit(1) + if not cities_param: + print json.dumps({"error": "缺少 cities 参数", "success": False}) + sys.exit(1) + + if not os.path.isdir(output_dir): + os.makedirs(output_dir) + + # 转换省界 + print json.dumps({"msg": "转换省界: " + province_path}) + province_name = os.path.splitext(os.path.basename(province_path))[0] + province_shp = os.path.join(output_dir, "province.shp") + # GeoJSON → Feature Class → Shapefile + arcpy.JSONToFeatures_conversion(province_path, province_shp) + print json.dumps({"msg": "省界已转为 SHP: " + province_shp}) + + # 转换每个城市 + city_files = [c.strip() for c in cities_param.split(",") if c.strip()] + converted = [] + for cf in city_files: + if not os.path.isfile(cf): + print json.dumps({"warn": "文件不存在,跳过: " + cf}) + continue + print json.dumps({"msg": "转换城市: " + cf}) + base = os.path.splitext(os.path.basename(cf))[0] + shp_path = os.path.join(output_dir, base + ".shp") + arcpy.JSONToFeatures_conversion(cf, shp_path) + converted.append(base) + print json.dumps({"msg": "已转换: " + cf + " -> " + shp_path}) + + result = { + "success": True, + "province_shp": province_shp, + "city_count": len(converted), + "cities": converted + } + print json.dumps(result) + + +if __name__ == "__main__": + main() diff --git a/suites/test-windows/scripts/arcpy_overlay.py b/suites/test-windows/scripts/arcpy_overlay.py new file mode 100644 index 0000000..19caa93 --- /dev/null +++ b/suites/test-windows/scripts/arcpy_overlay.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python +""" +Step 2: 叠加分析 (arcpy) +判断各城市是否在省界内,输出分析结果 JSON +""" +import sys +import json +import os +import glob +import arcpy + +arcpy.env.overwriteOutput = True + + +def main(): + province_shp = os.environ.get("PARAM_PROVINCE_SHP", "") + cities_dir = os.environ.get("PARAM_CITIES_DIR", r"C:\temp\output\shp") + output_dir = os.environ.get("PARAM_OUTPUT_DIR", r"C:\temp\output\result") + + if not province_shp or not os.path.isfile(province_shp): + print json.dumps({"error": "省界 SHP 文件不存在: " + str(province_shp), "success": False}) + sys.exit(1) + + if not os.path.isdir(output_dir): + os.makedirs(output_dir) + + # 查找所有城市 SHP + city_shp_files = glob.glob(os.path.join(cities_dir, "*.shp")) + city_shp_files = [f for f in city_shp_files + if os.path.basename(f) != "province.shp"] + + if not city_shp_files: + print json.dumps({"error": "未找到城市 SHP 文件", "success": False}) + sys.exit(1) + + # 创建省界图层 + province_lyr = "province_layer" + arcpy.MakeFeatureLayer_management(province_shp, province_lyr) + + results = [] + desc = arcpy.Describe(province_shp) + sr = desc.spatialReference + + for shp in city_shp_files: + city_name = os.path.splitext(os.path.basename(shp))[0] + print json.dumps({"msg": "分析城市: " + city_name}) + + # 创建城市图层 + city_lyr = "city_layer_" + city_name + arcpy.MakeFeatureLayer_management(shp, city_lyr) + + # 空间选择:城市中心点在省界内 + # 先获取城市几何中心 + city_centroid_shp = os.path.join(output_dir, city_name + "_centroid.shp") + arcpy.FeatureToPoint_management(shp, city_centroid_shp, "INSIDE") + + centroid_lyr = "centroid_layer_" + city_name + arcpy.MakeFeatureLayer_management(city_centroid_shp, centroid_lyr) + + # Select by location: 中心点在省界内 + arcpy.SelectLayerByLocation_management(centroid_lyr, "WITHIN", province_lyr) + count = int(arcpy.GetCount_management(centroid_lyr).getOutput(0)) + in_province = count > 0 + + # 获取属性 + city_name_prop = city_name + city_code = "" + population = 0 + try: + with arcpy.da.SearchCursor(shp, ["name", "code", "population"]) as cursor: + for row in cursor: + if row[0]: + city_name_prop = str(row[0]) + if row[1]: + city_code = str(row[1]) + if row[2]: + population = int(row[2]) + break + except Exception: + pass + + status = u"通过 \u2705" if in_province else u"不通过 \u274c" + + results.append({ + "city": city_name, + "city_code": city_code, + "city_name": city_name_prop, + "population": population, + "in_province": in_province, + "intersects": in_province, + "status": status + }) + print json.dumps({"msg": city_name + ": " + status}) + + # 清理 + arcpy.Delete_management(city_lyr) + arcpy.Delete_management(centroid_lyr) + arcpy.Delete_management(city_centroid_shp) + + arcpy.Delete_management(province_lyr) + + # 输出 JSON + result_json = os.path.join(output_dir, "overlay_results.json") + with open(result_json, "w") as f: + json.dump(results, f, indent=2) + + summary = { + "success": True, + "total_cities": len(results), + "in_province": sum(1 for r in results if r["in_province"]), + "not_in_province": sum(1 for r in results if not r["in_province"]), + "results": results, + "result_json": result_json + } + print json.dumps(summary) + + +if __name__ == "__main__": + main() diff --git a/suites/test-windows/scripts/excel.py b/suites/test-windows/scripts/excel.py new file mode 100644 index 0000000..7940c75 --- /dev/null +++ b/suites/test-windows/scripts/excel.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +""" +Step 3: 输出 Excel +将叠加分析结果写入 Excel 文件 +""" +import sys +import json +import os +from openpyxl import Workbook +from openpyxl.styles import Font, Alignment, PatternFill + + +def main(): + result_dir = os.environ.get("PARAM_RESULT_DIR", "/tmp/output/result") + output_path = os.environ.get("PARAM_OUTPUT_PATH", "") + + if not output_path: + print(json.dumps({"error": "缺少 output_path 参数", "success": False})) + sys.exit(1) + + # 读取分析结果 + result_json = os.path.join(result_dir, "overlay_results.json") + if not os.path.isfile(result_json): + print(json.dumps({"error": f"结果文件不存在: {result_json}", "success": False})) + sys.exit(1) + + with open(result_json, "r", encoding="utf-8") as f: + results = json.load(f) + + # 创建工作簿 + wb = Workbook() + ws = wb.active + ws.title = "叠加分析结果" + + # 标题样式 + header_font = Font(bold=True, size=12, color="FFFFFF") + header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid") + header_align = Alignment(horizontal="center", vertical="center") + + # 表头 + headers = ["城市", "城市代码", "城市名称", "人口", "在省内", "相交", "状态"] + for col, h in enumerate(headers, 1): + cell = ws.cell(row=1, column=col, value=h) + cell.font = header_font + cell.fill = header_fill + cell.alignment = header_align + + # 数据行 + pass_fill = PatternFill(start_color="E2EFDA", end_color="E2EFDA", fill_type="solid") + fail_fill = PatternFill(start_color="FCE4EC", end_color="FCE4EC", fill_type="solid") + + for row_idx, r in enumerate(results, 2): + ws.cell(row=row_idx, column=1, value=r.get("city", "")) + ws.cell(row=row_idx, column=2, value=r.get("city_code", "")) + ws.cell(row=row_idx, column=3, value=r.get("city_name", "")) + ws.cell(row=row_idx, column=4, value=r.get("population", 0)) + ws.cell(row=row_idx, column=5, value="是" if r.get("in_province") else "否") + ws.cell(row=row_idx, column=6, value="是" if r.get("intersects") else "否") + ws.cell(row=row_idx, column=7, value=r.get("status", "")) + + # 条件高亮 + fill = pass_fill if r.get("in_province") else fail_fill + for col in range(1, 8): + ws.cell(row=row_idx, column=col).fill = fill + + # 调整列宽 + ws.column_dimensions["A"].width = 15 + ws.column_dimensions["B"].width = 12 + ws.column_dimensions["C"].width = 15 + ws.column_dimensions["D"].width = 12 + ws.column_dimensions["E"].width = 10 + ws.column_dimensions["F"].width = 10 + ws.column_dimensions["G"].width = 15 + + # 确保输出目录存在 + out_dir = os.path.dirname(output_path) + if out_dir: + os.makedirs(out_dir, exist_ok=True) + + wb.save(output_path) + print(json.dumps({ + "success": True, + "output_path": output_path, + "total": len(results), + "msg": f"Excel 已输出到: {output_path}" + })) + + +if __name__ == "__main__": + main() diff --git a/suites/test-windows/workflow.yaml b/suites/test-windows/workflow.yaml new file mode 100644 index 0000000..bde4778 --- /dev/null +++ b/suites/test-windows/workflow.yaml @@ -0,0 +1,51 @@ +name: 云南省界叠加分析(Windows 测试) +description: 验证 GIS Actions 4.0 Windows 执行链路的测试套件。将示例 GeoJSON 转为 SHP → 叠加分析(arcpy)→ 输出 Excel +version: 1.0.0 +author: SuiteForge +platform: windows +slug: test-yunnan-overlay-windows +tags: [测试, 叠加分析, Windows, 云南省] +category: 测试验证 + +params: + province: + type: string + required: true + desc: 省界 GeoJSON 文件路径 + cities: + type: string + required: true + desc: 待分析城市 GeoJSON 文件路径,多个文件用逗号分隔 + output_path: + type: string + required: true + desc: 输出 Excel 文件路径(.xlsx) + +steps: + - id: geojson_to_shp + name: 转为 SHP + runtime: arcpy + script_id: arcpy_convert + params: + province: $params.province + cities: $params.cities + output_dir: /tmp/output/shp + + - id: overlay + name: 叠加分析 + runtime: arcpy + script_id: arcpy_overlay + depends_on: [geojson_to_shp] + params: + province_shp: /tmp/output/shp/province.shp + cities_dir: /tmp/output/shp + output_dir: /tmp/output/result + + - id: export_excel + name: 输出 Excel + runtime: python3 + script_id: excel + depends_on: [overlay] + params: + result_dir: /tmp/output/result + output_path: $params.output_path