chore: initialize insar management system v2

This commit is contained in:
2026-04-14 13:16:01 +08:00
commit ecc72ec9cd
361 changed files with 2142522 additions and 0 deletions
+967
View File
@@ -0,0 +1,967 @@
# InSAR 自动化管理与智能评估系统 - 部署与使用说明
本文件为本项目主中文文档,整合了产品说明、技术架构、业务流程、部署与启动、数据库与授权等内容。
补充专题文档见:`docs/TODO.md`、`docs/RADAR_PREVIEW_SCHEME_B.md`。
---
## 1. 产品概述
本系统是一套深度集成 **IDL/ENVI SARscape** 的专业级 InSAR 数据管理与智能分析平台,聚焦地质灾害监测领域,解决大规模雷达影像处理流程繁琐、数据关联困难、结果解译依赖人工等问题。
通过“Web 化管理 + 自动化引擎 + AI 智能诊断”的三位一体架构,实现从原始 SAR 影像入库到生成形变诊断报告的全生命周期自动化管理。
---
## 2. 核心优势
- **核心流程自动化**:覆盖数据监控、精轨关联、干涉配对、自动化处理引导与结果发布(当前扫描调度默认手动触发)。
- **高精度空间引擎**:凸包算法 + 仿射变换,支持亚像素级 Footprint 提取与空间关联。
- **AI 辅助决策**:集成随机森林质量评估模型与 VLM 视觉大模型,给出专家级解译建议。
- **工业级稳定性**:针对 Windows Server 环境优化,支持 GB 级超大影像处理。
---
## 3. 技术架构
- **管理层 (Web Interface)**:React + Vite + Leaflet(支持离线多级细节加载)
- **逻辑层 (Service Layer)**:FastAPI 异步框架,集成 PostgreSQL + PostGIS
- **空间引擎 (Spatial Engine)**:PostGIS 原生计算 + GeoAlchemy2 + Rasterio
- **AI 引擎 (Intelligence)**:Scikit-learn + 本地 Ollama VLM
- **计算引擎 (Compute)**:IDL 8.8 / ENVI 5.6(SARscape 5.6+)
---
## 4. 业务流程指南
### 4.1 数据资产监控
当前系统默认采用 **Manual-only(手动触发)** 扫描模式,不启用后台定时守护任务。
可通过前端“立即扫描”或接口 `POST /api/monitor/run-now` 触发扫描;扫描内部仍采用增量策略以降低系统负载。
**D-InSAR 结果扫描说明(重要):**
当前 D-InSAR 结果扫描为**全量遍历 + 仅新增入库**。
即:每次扫描都会遍历所有 `.hdr` 文件,但数据库只写入“新发现”的结果记录,以保证准确性和稳定性。
扫描任务的进度条基于**实际遍历数量与处理数量**计算,前端显示为真实进度。
数据列表已支持“影像信息”按钮,可查看影像基础元数据(卫星/日期/模式/极化/精轨等)。
影像元数据解析已扩展:优先从 XML 获取成像模式/极化/轨道方向,并补充卫星模式、接收站代号、圈号、景中心经纬度、采集时间、产品类型/级别、唯一标识等字段用于展示。
源数据预览缓存接口:`GET /api/radar-data/{data_id}/thumb`(首次请求若无缓存会自动生成)。
**源影像预览(Scheme B:后端预纠正缓存)**
系统已接入双层缓存:
- `backend/image_cache/radar_geo/`:地理纠正后的主缓存(前端优先使用)
- `backend/image_cache/radar_raw/`:原图 WebP 回退缓存
**D-InSAR 结果缓存(标准目录)**
- `backend/image_cache/dinsar/`:D-InSAR 结果可视化缓存目录
- 文件命名:`ID_{id}_{name}.webp`
- 清单文件:`backend/image_cache/dinsar/cache_manifest.json`
**一致性检测(统计面板)**
- `GET /api/statistics` 已新增一致性字段,用于校验“数据库记录 vs 实际缓存文件 vs XML 读取结果”。
- 重点包含:
- D-InSAR:`db_cached_but_file_missing_count`、`db_uncached_but_file_exists_count`、`manifest_missing_file_count`
- 源影像预览:`db_ready_but_cache_missing_count`、`preview_missing_count`
- XML:`xml_detected_but_unparsed_count`、`xml_missing_count`
- 前端「数据统计仪表盘」会直接显示异常数量并高亮提示,便于运维快速定位问题。
新增接口:
- `GET /api/radar-data/{id}/preview-status`:查询单条源影像预览缓存状态
- `POST /api/radar-data/{id}/rebuild-preview-cache`:管理员强制重建该影像预览缓存
- `GET /api/radar-data?limit=500&offset=0`:分页获取源数据列表(单次最多 2000 条)
- `POST /api/radar-data/search`:源数据分页检索(支持详情字段筛选 + 行政区/上传SHP空间筛选)
- `GET /api/radar-data/search/options`:获取源数据检索下拉选项(来自数据库去重值)
- `GET /api/dinsar-results?limit=500&offset=0`:分页获取 D-InSAR 结果列表(单次最多 2000 条)
- `GET /api/radar-data/imaging-dates`:获取所有可用成像日期(用于配对起始日期下拉框)
- `GET /api/statistics`:默认返回缓存快照;管理员可用 `GET /api/statistics?fresh=true` 强制重算
`/api/radar-data/search` 说明:
- 权限:管理员与只读账号均可调用(只读安全查询接口)。
- 筛选字段:可按卫星、成像时间范围、模式/极化/产品元数据等详情字段检索。
- 空间筛选:支持 `region_tree_id`(复用行政区)或上传 AOI 文件(`.shp`/GeoJSON)。
- 分页性能:首次 AOI 查询会返回 `aoi_token`,后续翻页可仅携带 token,无需重复上传文件。
- 前端交互:检索字段采用“全下拉可选”,避免手工输入导致的筛选误差。
分页接口响应结构(`/api/radar-data` 与 `/api/dinsar-results` 一致):
```json
{
"items": [],
"total": 12345,
"limit": 200,
"offset": 0,
"has_more": true
}
```
前端分页说明:
- 「数据列表」与「D-InSAR 结果」均支持上一页/下一页、每页条数切换(50/100/200/500)与页码跳转。
- 前端默认每页 200 条,避免一次性加载全量数据导致页面和地图卡顿。
- 「数据列表」改为“先检索后展示”:系统启动后不自动加载源数据,需点击“搜索”或“搜索全部源数据”后才显示列表与分页结果。
- 当检索条件变更并再次执行搜索时,前端会先清空旧结果再渲染新结果,避免“旧页数据残留”造成误判。
- 异常/堆栈文本默认不参与界面翻译(e.g. `NoneType object is not callable`、`Traceback`),保持原始语言以避免日志或异常信息被误替换。
新增环境变量(可选):
```env
RADAR_GEO_CACHE_WORKERS=2
RADAR_GEO_CACHE_VERSION=b1
RADAR_GEO_CACHE_QUALITY=84
RADAR_PREVIEW_BUILD_ON_DEMAND=true
```
说明:
- `thumb` 接口优先返回 `radar_geo`,失败时自动回退 `radar_raw`。
- 扫描任务会增量构建 `radar_geo`,并维护 `radar_raw` 作为兜底。
- 如纠正逻辑升级,提升 `RADAR_GEO_CACHE_VERSION` 可触发重建。
- 纠正方向判定优先使用 XML 的 `sceneCornerCoord/refRow/refColumn` 建立“像素角 ↔ 地理角”映射,可显著减少左右/上下方向错误。
### 4.2 任务分发与处理
系统生成处理清单对接 IDL 自动化脚本,用户可在 Web 端一键启动本地 IDL 工作站,实现“云端规划,本地计算”。
### 4.3 智能诊断报告
AI 模块结合影像范围内的已知灾害点生成 Markdown 报告,涵盖:活动性评估、隐患搜寻、风险评级与处置建议。
---
## 5. 核心算法与机制概览
为保持文档完整性,以下为算法专题的简要说明(不再拆分子文档):
- **智能任务规划与贪心优化**:多维约束模型、覆盖多样性惩罚与任务筛选策略。
- **空间几何与极速 Footprint**:降采样探测 + 凸包生成,结合拓扑简化提升效率。
- **AI 诊断与图像处理**:百分比截断拉伸、质量评估特征与自动裁剪策略。
---
## 6. 环境与依赖
- **OS**:Windows Server 2019+(推荐)
- **数据库**:PostgreSQL 14+ + PostGIS
- **Python**:推荐 Conda 环境(本项目 environment.yml 已固定为 Python 3.10)
- **Nginx**:用于前端静态资源与反向代理(推荐)
- **Node.js**:仅构建前端时需要
- **IDL/ENVI**:仅启用 IDL 自动化时需要
---
## 7. 离线 Python 环境(推荐:conda-pack)
### 7.1 开发机(有外网)
```powershell
conda env create -f environment.yml
conda activate InSAR
```
如需授权功能,请确保安装 cryptography:
```powershell
pip install cryptography
```
如需打包授权 GUI:
```powershell
pip install pyinstaller
```
打包环境:
```powershell
conda install -n base conda-pack
conda pack -n InSAR -o insar_env.tar.gz
```
将 `insar_env.tar.gz` 拷贝到服务器。
### 7.2 服务器(离线)
解压到目标目录(示例:`C:\envs\InSAR`):
```powershell
mkdir C:\envs\InSAR
tar -xzf insar_env.tar.gz -C C:\envs\InSAR
C:\envs\InSAR\Scripts\conda-unpack.exe
```
如果没有 `tar`,可使用 7-Zip 解压。
在 `.env` 中配置:
```
PYTHON_PATH=C:\envs\InSAR\python.exe
```
`PYTHON_PATH` 也支持命令名(如 `python`),`scripts/start_app.ps1` 会通过 `Get-Command` 解析真实可执行文件路径。
若使用 Conda,建议先激活 `InSAR` 环境后再启动,或直接填入该环境的 `python.exe` 绝对路径。
也可使用 Conda 原生模式(推荐):
```
CONDA_EXE=D:\anaconda3\Scripts\conda.exe
CONDA_ENV_NAME=InSAR
```
当 `CONDA_ENV_NAME` 非空时,`scripts/start_app.ps1` 会优先使用
`CONDA_EXE` 定位目标环境并解析其 `python.exe`,随后使用该解释器执行数据库检查、初始化、后端与 Worker 启动流程(避免并发 `conda run` 稳定性问题)。
**不建议直接复制 venv**,二进制依赖容易失效。
---
## 8. 数据库准备
### 8.1 创建数据库
```powershell
psql -U postgres -h localhost -p 5432
CREATE DATABASE insar_management;
\q
```
### 8.2 启用 PostGIS
```powershell
psql -U postgres -h localhost -p 5432 -d insar_management
CREATE EXTENSION IF NOT EXISTS postgis;
\q
```
### 8.3 配置连接并检测
在 `.env` 中设置:
```
DATABASE_URL=postgresql+asyncpg://user:pass@host:5432/insar_management
```
手动检查连接:
```powershell
D:\anaconda3\Scripts\conda.exe run -n InSAR python scripts/check_db_connection.py
```
---
## 9. 授权(离线)
启动授权管理工具:
```powershell
D:\anaconda3\Scripts\conda.exe run -n InSAR python scripts/license_manager_gui.py
```
使用流程:
1) 选择 `.env` 路径
2) 输入到期时间(UTC,格式:`YYYY-MM-DD HH:MM:SS`,留空默认 1 年)
3) 点击“生成/续期授权”
4) 自动写入 `.env` 中的 `LICENSE_SECRET` / `LICENSE_PUBLIC_KEY`
5) 生成 `backend\license\license.lic` 与 `license_private_key.txt`
说明:
- `license_private_key.txt` 请妥善保存(用于续期)。
- 服务器只需要公钥与授权文件,不要部署私钥。
- 建议仅在离线管理机保存私钥文件,避免在业务服务器落盘或备份私钥。
- 离线授权属于本地合规校验机制:可显著提高误用门槛,但不等同于“对拥有服务器管理员权限场景”的绝对防篡改。
授权相关接口:
```
/api/license/status
/api/license/upload (仅管理员可调用)
/api/license/refresh (仅管理员可调用)
```
返回约束(安全脱敏):
- `GET /api/license/status` 默认仅返回 `ok`、`reason`、`expires_at`。
- 当请求携带管理员会话时,`GET /api/license/status` 会额外返回调试字段(如 `fingerprint`、`license_path`)。
---
## 10. 关键配置(.env)
常用参数示例:
```
# 服务显示地址(用于启动日志与前端展示)
SERVER_HOST=192.168.1.100
# 后端端口(示例使用 18000,避免与常见本机服务冲突)
PORT=18000
# 前端开发模式代理目标(仅 npm run dev 使用)
VITE_BACKEND_TARGET=http://localhost:18000
# Python / Nginx 路径
PYTHON_PATH=C:\envs\InSAR\python.exe
CONDA_EXE=D:\anaconda3\Scripts\conda.exe
CONDA_ENV_NAME=InSAR
NGINX_PATH=C:\nginx-1.29.4\nginx.exe
# IDL / ENVI 路径与 worker 管理目录
IDL_EXECUTABLE=C:\Program Files\Harris\ENVI56\IDL88\bin\bin.x86_64\idl.exe
IDL_WORKBENCH_PATH=C:\Program Files\Harris\ENVI56\IDL88\bin\bin.x86_64\idlde.exe
IDL_WORKER_SCRIPT_SOURCE_DIR=Z:\Code\Insar_management_system_v2\IDL\origin
IDL_WORKER_SCRIPT_DIR=Z:\Code\Insar_management_system_v2\IDL\worker_managed
IDL_WORKER_RUNTIME_DIR=Z:\Code\Insar_management_system_v2\backend\runtime\idl_worker
IDL_WORKER_DEFAULT_TIMEOUT_SECONDS=14400
IDL_WORKER_MAX_TIMEOUT_SECONDS=43200
IDL_WORKER_TEMP_DIRECTORY=D:\Sarscape_IDL_Area
# IDL worker 默认任务参数(可被 API 请求体覆盖)
IDL_IMPORT_ROOT_DIR=
IDL_IMPORT_NUM_TO_PROCESS=0
IDL_DINSAR_ROOT_DIR=
IDL_DINSAR_DEM_BASE_FILE=
IDL_DINSAR_NUM_TO_PROCESS=0
IDL_DINSAR_TARGET_GROUND_RESOLUTION_M=10.0
IDL_DINSAR_FILTER_METHOD=GOLDSTEIN
IDL_DINSAR_UNWRAP_COH_THRESHOLD=0.05
IDL_DINSAR_GCP_COH_THRESHOLD=0.7
IDL_DINSAR_GCP_NUMBER=100
IDL_DINSAR_GEOCODING_COH_THRESHOLD=0.0
IDL_DINSAR_GEOCODING_PIXEL_SIZE_M=10.0
# 授权
LICENSE_SECRET=...
LICENSE_PUBLIC_KEY=...
LICENSE_PATH=...\backend\license\license.lic
LICENSE_STATE_PATH=...\backend\license\license_state.json
MAX_LICENSE_UPLOAD_BYTES=1048576
# 鉴权(Session + Cookie)
INIT_ADMIN_USERNAME=admin
INIT_ADMIN_PASSWORD=<REQUIRED_STRONG_PASSWORD>
INIT_ADMIN_RESET_PASSWORD=false
SESSION_TTL_HOURS=12
AUTH_SESSION_COOKIE_NAME=ims_session
AUTH_COOKIE_SAMESITE=Lax
AUTH_COOKIE_SECURE=false
AUTH_LOGIN_MAX_FAILURES=5
AUTH_LOGIN_WINDOW_SECONDS=900
AUTH_LOGIN_LOCK_SECONDS=900
AUTH_LOGIN_CLEANUP_INTERVAL_SECONDS=300
# AOI 上传限流
AOI_UPLOAD_MAX_FILES=10
AOI_UPLOAD_MAX_SINGLE_FILE_BYTES=20971520
AOI_UPLOAD_MAX_TOTAL_BYTES=104857600
# 扫描请求边界
MAX_SCAN_DIRECTORY_COUNT=64
MAX_SCAN_PATH_LENGTH=2048
# 高频列表查询边界与超时
LIST_QUERY_MAX_LIMIT=2000
LIST_QUERY_MAX_OFFSET=200000
LIST_QUERY_MAX_WINDOW=202000
LIST_QUERY_TIMEOUT_MS=20000
RADAR_SEARCH_OPTIONS_MAX_VALUES=5000
RADAR_IMAGING_DATES_MAX_VALUES=5000
# 队列入队边界(防止异常大 payload 冲击数据库)
JOB_QUEUE_MAX_PAYLOAD_BYTES=524288
JOB_QUEUE_MAX_ATTEMPTS=10
JOB_QUEUE_MAX_PRIORITY_ABS=1000
JOB_QUEUE_MAX_ID_LENGTH=128
JOB_QUEUE_MAX_TYPE_LENGTH=64
# 任务/日志查询边界(防止单次查询放大)
TASK_ACTIVE_DEFAULT_LIMIT=100
TASK_ACTIVE_MAX_LIMIT=500
TASK_LOG_DEFAULT_LIMIT=100
TASK_LOG_MAX_LIMIT=1000
TASK_QUERY_MAX_OFFSET=500000
MONITOR_LOG_DEFAULT_LIMIT=50
MONITOR_LOG_MAX_LIMIT=200
MONITOR_LOG_MAX_OFFSET=500000
# 批次与复制请求边界
TASK_BATCH_MAX_ITEMS=5000
TASK_BATCH_TEXT_MAX_LENGTH=256
TASK_BATCH_REMARK_MAX_LENGTH=2000
TASK_BATCH_LIST_DEFAULT_LIMIT=200
TASK_BATCH_LIST_MAX_LIMIT=1000
TASK_BATCH_LIST_MAX_OFFSET=500000
COPY_BATCH_TEXT_MAX_LENGTH=2048
COPY_BATCH_MAX_STATUS_COUNT=8
# 工作流请求边界
WORKFLOW_MAX_STEPS=200
WORKFLOW_MAX_DEPENDS=32
WORKFLOW_TEXT_MAX_LENGTH=128
# AI 地图分析请求边界
AI_ANALYZE_MAP_MAX_IMAGES=4
AI_ANALYZE_MAP_MAX_IMAGE_BASE64_CHARS=12582912
AI_ANALYZE_MAP_PROMPT_MAX_CHARS=8000
# 生产环境请使用白名单,不建议 *
CORS_ORIGINS=http://192.168.1.100
CORS_ALLOW_CREDENTIALS=true
CORS_STRICT_MODE=false
# 统计缓存(秒)
STATS_CACHE_TTL_SECONDS=120
# 预览缓存(可选)
RADAR_THUMBNAIL_MAX_SIZE=1600
RADAR_CACHE_WORKERS=2
# 灾害点数据源
HAZARD_POINTS_DIR=Z:\Code\Insar_management_system_v2\backend\Point
HAZARD_POINTS_FILENAME=Point.shp
```
鉴权说明:
- 系统不提供公开注册入口。
- 首次启动或重置管理员密码时,`INIT_ADMIN_PASSWORD` 为必填项(不再提供默认密码回退)。
- `scripts/init_db.py` 会检查管理员账号;若不存在则按 `INIT_ADMIN_*` 自动创建。
- 如需强制重置管理员密码:将 `INIT_ADMIN_RESET_PASSWORD=true`(仅重置当次生效,完成后建议改回 `false`)。
- 内网 HTTP 部署请保持 `AUTH_COOKIE_SECURE=false`;若启用 HTTPS,请改为 `true`。
- 登录防爆破参数:`AUTH_LOGIN_MAX_FAILURES`(窗口内失败阈值)、`AUTH_LOGIN_WINDOW_SECONDS`(失败统计窗口)、`AUTH_LOGIN_LOCK_SECONDS`(触发后锁定时长)、`AUTH_LOGIN_CLEANUP_INTERVAL_SECONDS`(限流状态表后台清理间隔,秒)。
- AOI 上传限流参数:`AOI_UPLOAD_MAX_FILES`(上传文件数量上限)、`AOI_UPLOAD_MAX_SINGLE_FILE_BYTES`(单文件字节上限)、`AOI_UPLOAD_MAX_TOTAL_BYTES`(同次请求总字节上限)。超限时接口返回 `HTTP 400`。
- 扫描请求边界参数:`MAX_SCAN_DIRECTORY_COUNT`(单次扫描请求目录数量上限)、`MAX_SCAN_PATH_LENGTH`(单路径字符串长度上限)。
- 高频列表查询边界参数:`LIST_QUERY_MAX_LIMIT`(单页最大返回条数)、`LIST_QUERY_MAX_OFFSET`(最大偏移)、`LIST_QUERY_MAX_WINDOW`(`limit+offset` 窗口上限),`LIST_QUERY_TIMEOUT_MS`(PostgreSQL 列表查询超时,毫秒);`RADAR_SEARCH_OPTIONS_MAX_VALUES` / `RADAR_IMAGING_DATES_MAX_VALUES` 用于限制雷达检索选项和成像日期去重结果规模。
- 队列入队边界参数:`JOB_QUEUE_MAX_PAYLOAD_BYTES`(单任务 payload 字节上限)、`JOB_QUEUE_MAX_ATTEMPTS`(重试次数上限)、`JOB_QUEUE_MAX_PRIORITY_ABS`(优先级绝对值上限)、`JOB_QUEUE_MAX_ID_LENGTH`(关联 ID 字符上限)、`JOB_QUEUE_MAX_TYPE_LENGTH`(任务类型字符串上限)。
- 任务/日志查询边界参数:`TASK_ACTIVE_DEFAULT_LIMIT`、`TASK_ACTIVE_MAX_LIMIT`(`GET /api/tasks/active` 分页上限),`TASK_LOG_DEFAULT_LIMIT`、`TASK_LOG_MAX_LIMIT`、`TASK_QUERY_MAX_OFFSET`(任务日志分页上限),`MONITOR_LOG_DEFAULT_LIMIT`、`MONITOR_LOG_MAX_LIMIT`、`MONITOR_LOG_MAX_OFFSET`(`GET /api/monitor/logs` 分页与偏移上限)。
- 批次与复制请求边界参数:`TASK_BATCH_MAX_ITEMS`(单批次条目数上限)、`TASK_BATCH_TEXT_MAX_LENGTH`(批次名称/方向长度上限)、`TASK_BATCH_REMARK_MAX_LENGTH`(条目备注长度上限)、`TASK_BATCH_LIST_DEFAULT_LIMIT` / `TASK_BATCH_LIST_MAX_LIMIT` / `TASK_BATCH_LIST_MAX_OFFSET`(批次列表与明细分页上限)、`COPY_BATCH_TEXT_MAX_LENGTH`(复制接口 `batch_id`/`dest_dir` 长度上限)、`COPY_BATCH_MAX_STATUS_COUNT`(复制状态列表数量上限)。
- 工作流请求边界参数:`WORKFLOW_MAX_STEPS`(单工作流步骤上限)、`WORKFLOW_MAX_DEPENDS`(单步骤依赖数上限)、`WORKFLOW_TEXT_MAX_LENGTH`(工作流文本字段长度上限)。
- AI 地图分析请求边界参数:`AI_ANALYZE_MAP_MAX_IMAGES`(单次请求图片数上限)、`AI_ANALYZE_MAP_MAX_IMAGE_BASE64_CHARS`(单图 Base64 长度上限)、`AI_ANALYZE_MAP_PROMPT_MAX_CHARS`(prompt 长度上限)。
- 授权上传默认采用“先校验后替换”的原子更新流程;无效授权不会覆盖当前在用授权文件。
- `MAX_LICENSE_UPLOAD_BYTES` 用于限制授权上传文件大小(默认 1MB)。
- `CORS_ORIGINS` 建议配置为明确白名单;生产环境不建议使用 `*`。
- 当 `CORS_STRICT_MODE=true` 时,若 `CORS_ORIGINS` 包含 `*` 且 `CORS_ALLOW_CREDENTIALS=true`,服务将拒绝启动。
- 严禁将包含真实 `DATABASE_URL`、`LICENSE_SECRET` 的 `.env` 提交到代码仓库或共享目录。
- 管理员可通过 `GET /api/auth/audit-logs?limit=200` 查看最近审计日志(仅 admin 可访问)。
- 许可文件上传与刷新接口(`POST /api/license/upload`、`POST /api/license/refresh`)仅允许管理员会话访问。
- 前端入口:左侧「运行维护」分组下新增「用户管理」「审计日志」两个管理员面板。
源影像缓存说明:
- 系统在扫描雷达源数据时,会尝试在每个场景目录中查找预览图(jpg/jpeg/png/webp/bmp/tif/tiff)并生成 WebP 缓存。
- 雷达缓存目录:`backend/image_cache/radar_geo/`(主)与 `backend/image_cache/radar_raw/`(回退)。
- D-InSAR 缓存目录:`backend/image_cache/dinsar/`(统一使用 `ID_{id}_{name}.webp`)。
- 前端「数据列表」支持源影像单条显示/隐藏与“一键显示/一键隐藏”。
- 长期稳定方案(后端预纠正缓存,B 方案)设计见:`docs/RADAR_PREVIEW_SCHEME_B.md`。
---
## 11. 启动方式
### 11.1 一键启动(推荐)
双击:
```
start_system.bat
```
或命令行:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\start_app.ps1
```
说明:
- 启动链路保持不变:`start_system.bat` -> `scripts/start_app.ps1` -> `check_db_connection.py` -> `init_db.py`。
- `scripts/start_app.ps1` 中 `PYTHON_PATH` 支持“绝对路径”或“命令名”(例如 `python`);脚本会先解析为可执行文件再调用。
- `init_db.py` 会在“建库/校验修复”后执行鉴权初始化(自动补齐管理员账号)。
- 扫描调度默认 Manual-only,不会后台定时自动触发;需通过前端或 `/api/monitor/run-now` 手动触发扫描。
- 默认不执行破坏性重建;若确需重建,请显式设置 `DB_SCHEMA_RESET_ON_MISMATCH=true`。
### 11.2 仅启动后端(无 Nginx)
```powershell
D:\anaconda3\Scripts\conda.exe run -n InSAR python run_backend.py
```
说明:后端默认绑定 `127.0.0.1`,仅本机访问。
### 11.3 后台队列 Worker(必需)
系统后台任务已改为“入队执行”,必须启动 Worker 才会真正执行扫描、AI、解包、复制等任务。
启动方式:
```powershell
D:\anaconda3\Scripts\conda.exe run -n InSAR python run_worker.py
```
说明:
- `start_system.bat` 已包含 Worker 启动。
- 若只运行 `run_backend.py`,请手动启动 `run_worker.py`。
- 数据分发/复制任务仅从“批次”读取,不再支持 Excel。
- `POST /api/monitor/run-now` 的 `target` 仅支持:`radar`、`orbit`、`dinsar`(留空表示全部)。
Worker 可选环境变量:
```
JOB_WORKER_POLL_INTERVAL=1.0 # 轮询间隔(秒)
JOB_WORKER_CONCURRENCY=1 # 并发执行数
JOB_WORKER_JOB_HEARTBEAT_INTERVAL=5 # 运行中作业心跳上报间隔(秒)
JOB_WORKER_STALE_RECOVER_INTERVAL=15 # 僵尸 RUNNING 作业回收检查间隔(秒)
JOB_WORKER_STALE_RUNNING_SECONDS=300 # 判定作业僵尸的超时阈值(秒)
IDL_JOB_MAX_ATTEMPTS=6 # IDL 队列任务最大重试次数
```
ENVI/SARscape 集成(envipyengine):
- 执行引擎:`envipyengine`(pip 包)→ `taskengine.exe` 子进程
- 工作流在独立 Python 子进程中执行,隔离 FastAPI 主进程
- D-InSAR 工作流自动检测未导入数据,先执行 Import 再处理 D-InSAR(智能串联)
- DEM 路径为系统级配置,存储在 `.env` 的 `IDL_DINSAR_DEM_BASE_FILE`
- 运行日志目录:`backend/runtime/idl_worker/`
ENVI 关键环境变量:
```
IDL_EXECUTABLE=... # IDL 可执行路径
IDL_WORKBENCH_PATH=... # IDL Workbench 路径
IDL_WORKER_RUNTIME_DIR=... # 运行日志目录
IDL_WORKER_DEFAULT_TIMEOUT_SECONDS=14400 # 单任务执行超时(秒)
IDL_WORKER_MAX_TIMEOUT_SECONDS=43200 # 单任务允许最大超时(秒)
IDL_DINSAR_DEM_BASE_FILE=... # DEM 路径(D-InSAR 必需)
```
API 端点(管理员):
```
GET /api/idl/status — 系统状态(含 DEM 路径和可用性)
POST /api/idl/launch-workbench — 启动 IDL Workbench
POST /api/idl/inspect/import — Import 预检查
POST /api/idl/inspect/dinsar — D-InSAR 预检查(含 Import 状态检测)
POST /api/idl/jobs/import — 提交 Import 任务
POST /api/idl/jobs/dinsar — 提交 D-InSAR 任务
GET /api/idl/jobs/recent — 最近运行记录
```
提交任务请求体示例:
```json
{
"root_dir": "Z:/Test_data/Test_IDL_1",
"num_to_process": 0,
"timeout_seconds": 14400
}
```
说明:
- 提交后会创建系统任务并入队,由 `run_worker.py` 消费执行。
- D-InSAR 工作流自动检测未导入数据,先执行 Import 再处理(智能串联)。
- DEM 路径从 `.env` 读取,不需要在请求中传递。
- Worker 在执行时会周期性更新作业心跳;若进程崩溃导致作业长期停留 `RUNNING`,会自动回收。
- 运行日志保存在 `IDL_WORKER_RUNTIME_DIR/` 下。
### 11.4 前端构建(仅构建时需要)
```powershell
cd frontend
npm install
npm run build
```
说明:
- 前端日期控件已统一为 `flatpickr`(原生实例接入),无需浏览器原生 `input[type=date]`。
- 若采用“本地构建后上传 `frontend/dist`”部署方式,服务器端无需额外安装前端依赖包。
### 11.5 离线切片底图切换
前端从 `frontend/public/tiles/` 读取离线切片,当前支持三套(统一 `webp`):
- `tiles/google_image/{z}/{x}/{y}.webp`
- `tiles/gaode_image/{z}/{x}/{y}.webp`
- `tiles/gaode_shp/{z}/{x}/{y}.webp`
地图右上角「底图」按钮可在三套底图间切换(默认 `gaode_shp`,便于仅下载高德矢量时直接可用)。
缩放级别已放开到 0-16;如需调整默认底图或级别,请修改 `frontend/src/App.jsx` 中的 `TILE_LAYER_DEFAULT_KEY` 与 `TILE_LAYER_OPTIONS`。
行政区边界叠加图层(地图参考边界)默认读取:
- `frontend/public/geojson/全国行政区.geojson`
建议使用 UTF-8 无 BOM 编码,且文件体积较大时优先通过 `public/` 静态加载(不要直接放入 `frontend/src` 参与打包)。
地图左上角新增「区域定位」按钮(可折叠),复用行政区二级级联(省/市):
- 可只选择到省级或市级进行定位。
- 定位时会调用后端 `/api/aoi/regions/{tree_id}/geometry` 获取边界并自动缩放到该区域。
- 定位成功后,面板会显示“当前定位”行政区名称,方便与当前视角对照。
- 面板支持「清除定位高亮」,可一键移除地图上的定位边界高亮。
- 依赖 `backend/geojson/层级映射.json` 与 `backend/geojson/全国行政区.geojson` 的行政区数据。
---
## 12. 访问与验证
- **前端**:`http://SERVER_HOST`
- **后端(内部)**:`http://127.0.0.1:8000`
- **接口文档(内部)**:`http://127.0.0.1:8000/docs`
前端语言切换:
- 顶部状态栏新增 `中文 / EN` 切换按钮。
- 切换结果会保存在浏览器本地(`localStorage`),刷新页面后保持上次语言选择。
- 站内文案翻译映射位于 `frontend/src/i18n/translations.js`,新增界面文案时需同步补充映射,避免中英文混杂。
- 第二轮已补齐高频业务模块(批次/分发、运维自检、用户管理、审计日志、IDL 自动化、统计面板)常用文案映射。
- 第三轮已支持运行日志双语展示:右侧日志栏、数据监控日志、数据分发任务日志会随语言切换进行前端翻译。
- 当前词典已覆盖常见后端运行日志模板(扫描、拷贝、解包、任务生命周期);若新增日志模板,建议同步补充词典条目。
- 收口说明:当前前端可见业务文案已基本完成双语覆盖,剩余英文主要为样式类名/技术标识(不面向客户显示)。
---
## 13. 常用脚本说明
- `start_system.bat`:一键启动入口(调用 PowerShell 启动脚本)
- `scripts/start_app.ps1`:启动后端 + Nginx,并执行数据库检查
- `run_backend.py`:仅启动后端服务
- `run_worker.py`:后台任务 Worker(队列执行)
- `scripts/check_db_connection.py`:数据库连接检查
- `scripts/init_db.py`:数据库结构检查与初始化
- `scripts/license_manager_gui.py`:授权文件生成与续期
- `scripts/get_fingerprint.py`:机器指纹获取
- `scripts/unpack_archives.py`:解包与数据整理(支持 tar.gz)
- `scripts/pack_environment.bat`:打包 Conda 环境(如使用)
- `scripts/auth_smoke_check.py`:鉴权冒烟检查(401/403/200 核心链路)
---
## 14. 常见问题
1) **Python executable not found**
检查 `.env` 中 `PYTHON_PATH` 是否正确;可使用绝对路径,或在已激活 Conda 环境后填写 `python`。
如启用 Conda 原生模式,还需确认 `CONDA_EXE` 与 `CONDA_ENV_NAME` 配置正确。
2) **数据库连接失败**
确认 PostgreSQL 服务已启动、`DATABASE_URL` 正确,并运行 `scripts/check_db_connection.py` 测试。
3) **授权无效/过期**
使用授权工具续期,检查系统时间是否回退过多。
4) **前端打不开但后端正常**
确认 Nginx 已启动、`NGINX_PATH` 正确、前端已构建。
若启动时报 `no "events" section in configuration`,请检查 `nginx/nginx.conf` 是否为空或损坏,需恢复包含 `events {}` 与 `http {}` 的完整配置。
5) **后端端口被占用(Address already in use / Access denied)**
`scripts/start_app.ps1` 启动前会检查 `PORT` 对应监听占用;若被占用会直接报错并终止。
请停止占用该端口的进程,或在 `.env` 中修改 `PORT` 后重试。
脚本会在启动时自动把 `nginx/nginx.conf` 中的 `proxy_pass` 同步到该 `PORT`,无需手动改 Nginx 反向代理端口。
6) **后端或 Worker 启动后立即退出**
`scripts/start_app.ps1` 会在拉起后端与 Worker 后做快速存活检查;若进程秒退,脚本会立即报错并退出。
请优先检查终端输出与后端日志,常见原因包括端口冲突、依赖缺失、环境变量配置错误。
最小可用 `nginx.conf` 模板(按需替换路径,`proxy_pass` 端口需与 `.env` 的 `PORT` 一致):
```nginx
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name localhost;
root "G:/Code/Insar_management_system_v2/frontend/dist";
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://127.0.0.1:18000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /image_cache/ {
alias "G:/Code/Insar_management_system_v2/backend/image_cache/";
}
}
}
```
### 14.1 鉴权与登录排查
- **`GET /api/auth/me` 返回 `401`**:未登录状态下这是正常现象,前端应显示登录页。
- **`POST /api/auth/login` 返回 `401`**:用户名或密码错误,先核对 `.env` 中 `INIT_ADMIN_USERNAME/INIT_ADMIN_PASSWORD`;若为首次启动且 `INIT_ADMIN_PASSWORD` 为空,初始化会直接报错。
- **`POST /api/auth/login` 返回 `429`**:触发登录限流/锁定,等待 `Retry-After` 秒后重试,或调整 `AUTH_LOGIN_MAX_FAILURES / AUTH_LOGIN_WINDOW_SECONDS / AUTH_LOGIN_LOCK_SECONDS`。
- **登录后接口仍持续 `401`**:检查浏览器是否携带 Cookie(请求需 `withCredentials=true`),并确认反向代理没有丢弃 `Set-Cookie` 头。
- **需要重置管理员密码**:设置 `INIT_ADMIN_RESET_PASSWORD=true`,重启一次系统完成重置后再改回 `false`。
- **Cookie 在内网 HTTP 不生效**:确认 `AUTH_COOKIE_SECURE=false`;只有 HTTPS 场景才应设置为 `true`。
- **需要核查谁触发了高风险写操作**:使用 `GET /api/auth/audit-logs?limit=200`(管理员会话)。
- **需要快速回归鉴权链路**:后端启动后运行 `D:\anaconda3\Scripts\conda.exe run -n InSAR python scripts/auth_smoke_check.py --base-url http://127.0.0.1:8000`。
5) **任务一直处于“执行中”**
确认 `run_worker.py` 已启动(或使用 `start_system.bat` 一键启动)。
- 若期望系统自动周期扫描:当前默认是手动模式(Manual-only),请使用前端“立即扫描”或调用 `POST /api/monitor/run-now`。
6) **配对结果异常或为空**
- 影像日期需为 `YYYYMMDD` 格式(系统从文件夹名解析)。
- AOI 支持 **SHP 上传** 与 **GeoJSON 传参**(`aoi_geojson`)两种模式。
- AOI Shapefile 建议使用 EPSG:4326(WGS84),否则需先进行投影转换。
- 若使用“行政区选择”,请准备标准 GeoJSON(见 17.7),并确保 `features[*].properties.treeID` 与 `backend/geojson/层级映射.json` 对应。
7) **灾害点导入为 0 条**
请确保 Shapefile 中存在唯一标识字段(优先 `TYBH`),并具备经纬度或点几何:
- 必填:`TYBH`(唯一编号)
- 推荐:`hazard_type` / `hazard_name` / `city` / `county` / `township`
- 坐标:点几何优先;若无几何,则读取经度/纬度字段
数据源路径:`.env` 中 `HAZARD_POINTS_DIR` + `HAZARD_POINTS_FILENAME`(默认 `backend/Point/Point.shp`)。
8) **Excel 功能找不到了**
Excel 导入/导出已移除,请使用“任务批次 + 数据分发”流程完成复制与人工状态管理。
9) **日志出现 “got Future attached to a different loop”**
该错误通常由**子线程内创建事件循环并复用 asyncpg 连接**引起,多发生在自建 `asyncio.run(...)` 的耗时任务里。
处理方式:
- 确保所有耗时任务由 Worker 执行(统一事件循环),避免在线程内自行 `asyncio.run`。
- 重启后端与 Worker 以释放异常连接。
- 如仍复现,请检查是否有自定义脚本在子线程内直接调用数据库。
---
## 15. 数据库结构自动对齐(重要)
系统每次启动会进行数据库结构检查:
- 首次启动:自动创建表结构。
- 后续启动:校验表/列/类型/可空性。
- 不一致(默认):不做破坏性重建,仅补齐缺失结构并输出告警。
- 不一致(显式开启):仅当 `DB_SCHEMA_RESET_ON_MISMATCH=true` 时执行 drop/recreate 重建。
推荐在生产/内网环境保持如下默认值(保留历史数据):
```
DB_SCHEMA_RESET_ON_MISMATCH=false
```
### 15.1 SQL 函数更新说明
若修改了 `backend/migrations/002_spatial_functions.sql`(例如配对函数 `find_dinsar_pairs`),需要执行一次:
- 重新启动系统(`start_system.bat` 会自动调用 `scripts/init_db.py`)
- 或手动运行 `D:\anaconda3\Scripts\conda.exe run -n InSAR python scripts/init_db.py`
### 15.2 空间函数迁移(必需)
启动时会自动执行以下迁移文件(用于空间函数、视图与日志表):
- `backend/migrations/001_st_intersection_agg.sql`
- `backend/migrations/002_spatial_functions.sql`
如需手工修改,请保证 SQL 语法正确且使用 UTF-8 编码。
---
## 16. 工作流与任务队列(新增)
后台任务已改为队列执行(DB Job Queue):
- 复制、解包、扫描、AI 训练/预测/诊断等任务都会入队。
- Worker 执行任务并更新状态,前端可查看任务进度。
- 所有耗时任务在 Worker 的事件循环中执行,避免跨事件循环导致的 asyncpg 连接异常。
工作流 API:
- `POST /api/workflow/runs` 创建工作流实例
- `GET /api/workflow/runs/{run_id}` 查询运行状态与步骤
任务查询 API(新增分页参数):
- `GET /api/tasks/active?limit=100&offset=0`
- `GET /api/tasks/{task_id}`
- `GET /api/tasks/{task_id}/logs?limit=100&offset=0`
- `GET /api/monitor/logs?limit=50&offset=0`
---
## 17. 任务批次管理(替代 Excel)
为适配 IDL 线下生产流程,D-InSAR 与 PS-InSAR 任务改为“批次”形式存储在数据库:
- 前端创建批次(不再使用 Excel 导入/导出)。
- 可按批次查看明细、手动标记完成、添加备注。
- 数据分发从批次中读取任务条目并复制到目标目录。
- Excel 导入/导出接口已移除。
相关 API:
- `POST /api/task-batches/dinsar`
- `GET /api/task-batches/dinsar?limit=200&offset=0`
- `GET /api/task-batches/dinsar/{batch_id}/items?limit=200&offset=0`
- `PATCH /api/task-batches/dinsar/{batch_id}/complete-all`
- `PATCH /api/task-batches/dinsar/items/{item_id}`
- `POST /api/task-batches/ps`
- `GET /api/task-batches/ps?limit=200&offset=0`
- `GET /api/task-batches/ps/{batch_id}/items?limit=200&offset=0`
- `PATCH /api/task-batches/ps/{batch_id}/complete-all`
- `PATCH /api/task-batches/ps/items/{item_id}`
数据分发 API:
- `POST /api/tools/copy-ps-stack` (body: `batch_id`, `dest_dir`, `copy_statuses?`)
- `POST /api/tools/copy-dinsar-pairs` (body: `batch_id`, `dest_dir`, `copy_statuses?`)
- `GET /api/tools/copy-status/{task_id}?limit=100&offset=0`
### 17.1 前端操作流程(推荐)
1) **生成候选列表**
- D-InSAR:左侧“干涉对配对”生成 pairs。
- PS-InSAR:左侧“PS 时序栈”生成 stack。
2) **保存为批次**
- 点击“保存批次”,系统将当前列表写入数据库。
- 批次会自动生成 `batch_id`,支持自定义名称。
- 保存成功后前端会自动切换到“任务批次”并定位到新建批次。
3) **批次管理与人工标注**
- 在右侧“任务批次”面板选择 D-InSAR 或 PS。
- 进入某批次后:
- 修改每条任务状态(PENDING / IN_PROGRESS / COMPLETED / FAILED)
- 接口仅接受上述四种状态值(大小写不敏感,非法值会返回 `400`)
- 为每条任务添加备注(如:文件损坏、未完成原因)
- 支持“一键全部完成”
4) **数据分发(复制)**
- 进入“数据分发”面板选择批次与目标目录后启动复制。
- 可按任务状态筛选复制(默认仅复制 `COMPLETED`)。
- 复制任务由 Worker 执行,进度可在前端日志中查看。
- 若出现部分文件复制失败,任务最终会标记为 `FAILED`(不再误报成功)。
### 17.2 批次数据结构(数据库)
系统会创建以下表:
- `dinsar_task_batches` / `dinsar_task_items`
- `ps_task_batches` / `ps_task_items`
每条 item 包含:
- `status`(人工状态)
- `remark`(备注)
- 任务路径与元数据(用于复制与后续追踪)
### 17.3 数据分发复制规则
**PS-InSAR:**
- 复制源优先使用 `{original_path}_envi_import`(若存在)
- 否则使用原始路径
**D-InSAR:**
- 为每个任务创建子目录:
- `<dest>/<task_name>/master`
- `<dest>/<task_name>/slave`
- 若源目录内存在 `envi_import` 且非空,优先复制 `envi_import`
### 17.4 Excel 迁移说明
历史流程若依赖 Excel:
- 现在请在系统中生成批次并使用“数据分发”复制。
- 复制任务仅接收 `batch_id`,不再接收 Excel 文件。
### 17.5 API 示例(简化)
创建 D-InSAR 批次:
```json
POST /api/task-batches/dinsar
{
"name": "DINSAR_20260208",
"pairs": [
{
"task_name": "T001",
"master": {"file_path": "D:/data/master1", "...": "..."},
"slave": {"file_path": "D:/data/slave1", "...": "..."},
"time_baseline_days": 12,
"spatial_baseline_meters": 1234.5
}
]
}
```
复制 D-InSAR 批次:
```json
POST /api/tools/copy-dinsar-pairs
{
"batch_id": "xxxx-xxxx-xxxx",
"dest_dir": "D:/IDL/tasks/dinsar",
"copy_statuses": ["COMPLETED"]
}
```
### 17.6 配对参数说明(新增)
`/api/find-pairs` 支持以下可选参数(表单提交):
- `require_same_imaging_mode`:是否要求主/辅影像成像模式一致(默认 `true`,空值视为不匹配)
- `require_same_polarization`:是否要求主/辅影像极化一致(默认 `true`,空值视为不匹配)
- `aoi_overlap_threshold`:AOI 覆盖比例阈值,计算方式为 `Area(image ∩ AOI) / Area(AOI)`,需主/辅影像同时满足
- `aoi_geojson`:可选,标准 GeoJSON 字符串(与 `files` 二选一或都不传)
系统会在候选配对数超过 3000 时返回提示信息。若数据库配对函数不可用,仅在影像数量 ≤1500 时允许回退计算,并返回强提醒。
前端已提供“成像模式一致 / 极化一致 / AOI 覆盖率阈值”的开关与输入框,默认关闭/0(不额外限制)。
### 17.7 行政区选择(SHP/行政区二选一)
生产规划弹窗中,AOI 来源支持:
- 上传 `SHP`(兼容旧流程)
- 行政区选择(省/市二级联动)
后端新增接口:
- `GET /api/aoi/regions/children?parent_tree_id=1`
- `GET /api/aoi/regions/{tree_id}/geometry`
相关环境变量(后端 `.env`):
```
AOI_REGION_INDEX_FILE=backend/geojson/层级映射.json
AOI_REGION_GEOJSON_FILE=backend/geojson/全国行政区.geojson
```
### 17.8 行政区 GeoJSON 数据规范(需提前准备)
建议提供一个标准 `FeatureCollection` 文件(UTF-8),示例:
```json
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"treeID": "1-23-4",
"name": "示例地市",
"level": "city"
},
"geometry": {
"type": "MultiPolygon",
"coordinates": [[[ [116.0, 39.0], [116.2, 39.0], [116.2, 39.2], [116.0, 39.2], [116.0, 39.0] ]]]
}
}
]
}
```
字段要求:
- 必填:`properties.treeID`(与 `层级映射.json` 一致)
- 可选:`properties.name`、`properties.level`
- `geometry` 建议使用 EPSG:4326(经纬度)
说明:
- 当前前端行政区选择为省/市二级,不提供区县单独选择入口。
- 若仅提供区县级数据,系统会在省/市选择时自动合并其下级边界。
- 若仅提供到市级数据,前端可直接按市级选择,后端会在必要时回退到最近可用上级边界(市→省)。
- 若 `treeID` 不匹配或缺失,行政区选择将无法返回 AOI 边界。
---
## 18. 运维自检(新增)
系统提供运维自检接口与前端面板,用于快速判断关键服务状态。
后端接口:
```
GET /api/health
```
返回约束(安全脱敏):
- 未登录或只读会话:返回最小健康摘要(`ok/timestamp` + 各子系统布尔状态与基础计数)。
- 管理员会话:返回完整诊断信息(包含错误详情、Worker 列表、外部服务状态细节)。
检查内容:
- 数据库连接 / PostGIS 扩展 / 关键表结构
- 关键表覆盖包含:任务与队列(`system_tasks/task_logs/system_jobs/scan_states`)、工作流(`workflow_defs/workflow_runs/workflow_steps/workflow_artifacts`)、鉴权(`auth_users/auth_sessions/auth_audit_logs/auth_rate_limits`)等核心表
- Worker 心跳
- IDL/ENVI 状态
- Ollama 连通性
- Nginx 连通性
- 一致性异常汇总(来自 `GET /api/statistics`):
- D-InSAR 缓存:数据库标记与实际缓存文件是否一致
- 源影像预览缓存:`READY` 状态与 `radar_geo/radar_raw` 文件是否一致
- XML 读取:是否检测到 XML 以及关键字段是否成功入库
相关环境变量:
```
JOB_WORKER_HEARTBEAT_INTERVAL=5 # Worker 心跳上报间隔(秒)
JOB_WORKER_HEALTH_TIMEOUT=60 # 运维自检判定 Worker 离线的阈值(秒)
```
前端入口:
- 左侧「系统工具」->「运维自检」
- 顶部状态栏可一键刷新自检状态
- 运维自检面板新增「一致性检测」卡片,显示异常总数、严重/一般异常,并列出具体异常项
---
## 19. 一致性统计接口(新增)
用于检查“数据库记录、缓存文件、XML读取结果”是否一致。
后端接口:
```
GET /api/statistics
```
新增返回字段(关键):
- `dinsar_cache_consistency`
- `db_cached_but_file_missing_count`:数据库标记已缓存,但缓存文件不存在
- `db_uncached_but_file_exists_count`:数据库标记未缓存,但缓存文件已存在
- `manifest_missing_file_count`:`cache_manifest.json` 记录的缓存文件缺失
- `source_preview_consistency`
- `preview_missing_count`:源影像预览缓存缺失(`radar_geo` 和 `radar_raw` 均不存在)
- `db_ready_but_cache_missing_count`:数据库 `READY`,但预览缓存实际缺失
- `source_xml_consistency`
- `xml_missing_count`:未检测到 XML
- `xml_detected_but_unparsed_count`:检测到 XML 但关键字段未成功入库
前端展示位置:
- 「数据统计仪表盘」:新增一致性图表与异常高亮
- 「运维自检」:新增一致性异常汇总卡片(总数 / 严重 / 一般)
建议运维动作:
- 出现 `db_cached_but_file_missing_count` 或 `db_ready_but_cache_missing_count`:执行一次重扫重建缓存
- 出现 `xml_detected_but_unparsed_count`:检查对应场景 XML 格式与解析字段
- 出现 `xml_missing_count`:检查场景目录是否包含 XML 元数据文件
---
*© 2026 InSAR Management System - 致力于更高效、更智能的雷达遥感监测*
+426
View File
@@ -0,0 +1,426 @@
# D-InSAR 增强 Task 文档
更新日期:2026-03-13
## 1. 文档目的
这份文档用于统一管理本轮 D-InSAR 系统重构任务,覆盖以下两大方向:
- D-InSAR 多引擎生产系统增强
- D-InSAR 结果管理系统增强
文档定位是“任务总表 + 实施边界 + 验收口径”,用于后续分阶段实施、联调和运维验收。
## 2. 背景
当前系统的 D-InSAR 生产能力主要绑定在 ENVI + SARscape 链路上,前端、后端任务模型、运行监控和结果入库都默认只有一个处理引擎。这会带来两个直接问题:
- 客户如果没有 ENVI/SARscape license,D-InSAR 核心生产能力会直接失效。
- 后续接入 ISCE2、LANDSAR 时,只能继续堆在现有 IDL/ENVI 语义上,导致架构持续恶化。
同时,当前 D-InSAR 结果管理能力偏轻,更多是“结果浏览 + 缓存管理”,还不足以支撑多引擎、多版本、多产物、多轮生产的正式管理需求。
## 3. 本轮目标
### 3.1 生产系统目标
- 将 D-InSAR 生产系统升级为“多引擎生产中心”
- 正式支持两类可执行引擎:
- `sarscape`:ENVI + SARscape
- `isce2`:WSL 内部署的 ISCE2
- 预留第三类引擎:
- `landsar`:仅保留接口和前端占位,不实现算法
- 将运维自检升级为“系统级 + D-InSAR 专项级”双层检查
### 3.2 结果管理目标
- 将 D-InSAR 结果从“单文件记录”升级为“可追踪、可治理、可比对”的结果资产
- 支持来源追踪、版本管理、多产物管理、审核状态和运维治理
- 让结果管理适配多引擎场景,而不是继续依赖单一文件命名和单一路径假设
## 4. 范围与非范围
### 4.1 本轮范围
- 多引擎 D-InSAR 后端抽象层
- 新 D-InSAR 生产中心前端架构
- ENVI/SARscape 的兼容接入
- ISCE2 的 WSL 环境校验与运行接口
- 运维自检与 D-InSAR 专项健康检查
- D-InSAR 结果管理增强
### 4.2 明确不在本轮范围
- LANDSAR 算法实现
- 对现有配对算法本身做大幅重写
- 对 AI 诊断能力做独立重构
- 一次性替换全部旧接口
## 5. 现状要点
### 5.1 ENVI 现有处理模式
当前 ENVI 侧应明确建模为 `sarscape` 引擎下的两种 profile:
- `metatask`
- `custom6`
其中 `custom6` 的 6 步链路为:
1. Interferogram Generation
2. Filtering and Coherence
3. Orbital Trend Removal
4. Phase Unwrapping
5. GCP Generation + Refinement
6. Phase to Displacement + Geocoding
### 5.2 ISCE2 的特殊约束
ISCE2 不走本机 Windows 直接执行链路,默认按 WSL 方式部署与运行,因此必须把 WSL 环境校验设计成正式能力,而不是上线前人工检查事项。
### 5.3 当前结果管理主要短板
- 结果记录字段偏少,缺少引擎来源、运行来源、版本、产物类型等核心字段
- 结果入库逻辑仍偏向单文件扫描,不适合多引擎多版本
- 前端结果面板偏重展示,缺少治理、比对、版本和来源追踪能力
- 运维自检尚未覆盖结果资产层面的异常
## 6. Task 总览
| Task ID | 主题 | 目标 |
|---|---|---|
| T1 | 多引擎生产基座 | 建立统一的 D-InSAR 引擎抽象、调度和兼容层 |
| T2 | 前端生产中心 | 将现有 IDL 自动化面板升级为多引擎生产中心 |
| T3 | SARSCAPE 兼容接入 | 将现有 ENVI 链路适配到新抽象层 |
| T4 | ISCE2 + WSL 接入 | 建立 ISCE2 的 WSL 检查、运行和日志链路 |
| T5 | 运维自检增强 | 增加多引擎专项健康检查和自愈入口 |
| T6 | 结果管理增强 | 将 D-InSAR 结果升级为正式结果资产管理体系 |
## 7. 任务拆分
### T1. 多引擎生产基座
#### 目标
- 将 D-InSAR 生产从 `IDL/ENVI 单引擎假设` 中抽离
- 为 `sarscape`、`isce2`、`landsar` 建立统一抽象
#### 关键任务
- 定义统一引擎接口
- 建立引擎注册表
- 建立统一运行记录模型
- 建立统一产物收集与入库接口
- 保留旧 `/idl/...` 路由兼容转发能力
#### 目标结构
- `dinsar_engines/base.py`
- `dinsar_engines/registry.py`
- `dinsar_engines/sarscape_engine.py`
- `dinsar_engines/isce2_engine.py`
- `dinsar_engines/landsar_engine.py`
- `dinsar_orchestrator.py`
#### 验收标准
- 新生产调度层可按 `engine_code` 分发
- `sarscape`、`isce2`、`landsar` 均可在注册表中查询到
- 旧 ENVI 生产功能不回归
### T2. 前端生产中心
#### 目标
- 用统一生产中心替代当前单引擎 `IDLAutomationPanel`
- 支持多引擎选择、运行监控、日志查看和结果入库操作
#### 前端信息架构
- 引擎状态区
- SARSCAPE
- ISCE2
- LANDSAR
- 生产提交区
- 批次选择
- 引擎选择
- profile 选择
- 输入/输出目录
- 参数模板
- 运行监控区
- 当前任务
- 步骤进度
- 运行日志
- 历史记录
- 产物处理区
- 提取
- 入库
- 重扫
- 缓存重建
#### 关键任务
- 新建 `DinsarProductionPanel`
- 废弃前端对 `/idl/...` 语义的直接依赖
- 增加引擎状态卡和 WSL 状态卡
- 运行记录中展示 `engine`、`profile`、`run_id`
#### 验收标准
- 前端可以清晰区分三类引擎状态
- `landsar` 显示为预留,不可执行
- 当 ENVI 不可用但 ISCE2 可用时,页面仍可提交 ISCE2 任务
### T3. SARSCAPE 兼容接入
#### 目标
- 将现有 ENVI 链路正式纳入新生产体系
- 明确 `metatask` 与 `custom6` 是同一引擎下的两个 profile
#### 关键任务
- 封装现有 ENVI 运行逻辑为 `sarscape_engine`
- 将 `metatask` 和 `custom6` 暴露为 profile
- 保留进度文件、日志、超时监控和历史运行能力
#### 验收标准
- 旧 ENVI 作业仍可执行
- 新系统中可选择 `sarscape/metatask` 与 `sarscape/custom6`
- 原有任务状态、日志和结果提取行为保持兼容
### T4. ISCE2 + WSL 接入
#### 目标
- 建立 ISCE2 运行的正式环境检查、任务执行和输出接入能力
#### WSL 环境校验任务
- 检查 WSL 是否安装
- 检查是否支持 WSL2
- 检查目标 distro 是否存在
- 检查目标 distro 是否可启动
- 检查 `bash -lc` 是否可执行
- 检查 Python 是否可执行
- 检查 ISCE2 是否可 import
- 检查目标入口命令是否可执行
- 检查 DEM 路径在 WSL 内是否可读
- 检查轨道目录在 WSL 内是否可读
- 检查输出目录在 WSL 内是否可写
- 检查 Windows 路径到 WSL 路径转换是否正确
- 增加可选 smoke test
#### 建议配置项
- `ISCE2_ENABLED`
- `ISCE2_WSL_DISTRO`
- `ISCE2_PYTHON`
- `ISCE2_PROFILE`
- `ISCE2_DEM_PATH`
- `ISCE2_ORBIT_DIR`
- `ISCE2_WORK_ROOT`
- `ISCE2_OUTPUT_ROOT`
- `ISCE2_SMOKE_TEST_ENABLED`
#### 关键任务
- 新建 `wsl_service.py`
- 新建 `isce2_engine.py`
- 建立 WSL 执行和日志封装
- 将 ISCE2 输出纳入统一结果收集和入库流程
#### 验收标准
- 前端可见 ISCE2 环境状态
- WSL 校验结果可解释、可定位故障点
- ISCE2 至少有一个正式 profile 可提交并跑通
### T5. 运维自检增强
#### 目标
- 将当前“系统健康检查”升级为“系统级 + D-InSAR 专项级”双层运维自检
#### 关键任务
- 保留现有 `/health` 简版接口
- 增加管理员详细健康接口
- 增加多引擎专项检查
- 增加 WSL 专项检查
- 增加 D-InSAR 队列、存储、结果一致性检查
- 增加自愈动作入口
#### 专项健康检查项
- 核心服务状态
- 数据库
- schema
- worker
- queue
- 引擎状态
- `sarscape`
- `isce2`
- `landsar`
- WSL 状态
- DEM/轨道/输出目录可用性
- D-InSAR 运行任务积压
- stale run 检测
- 输出未入库检测
- 入库但源文件缺失检测
- 缓存一致性检测
#### 自愈入口建议
- 重扫结果目录
- 重建缓存
- 重试入库
- 重置 stale run
- 标记失效结果
#### 验收标准
- 运维面板能区分系统级故障与引擎级故障
- 当 ENVI 不可用但 ISCE2 可用时,总体 D-InSAR 服务状态应为 `degraded` 或 `ok`,不能直接判全系统失败
- WSL 故障能定位到具体检查项
### T6. 结果管理增强
#### 目标
- 将 D-InSAR 结果从“扫描到的一条文件记录”升级为“正式结果资产”
#### 核心设计
- `Run`
- 一次真实生产执行
- `Artifact`
- 一次运行产出的具体文件
- `Managed Result`
- 面向前端与业务的主结果对象
#### 关键任务
- 扩充结果模型字段
- 增加结果来源追踪
- 增加版本管理
- 增加多产物管理
- 增加结果生命周期状态
- 增加批量治理能力
- 增加多引擎结果对比能力
#### 结果管理能力清单
- 来源追踪
- engine
- profile
- batch
- pair
- run
- 版本管理
- 当前版本
- 历史版本
- 最新成功版本
- 多产物管理
- disp
- coherence
- unwrapped phase
- geotiff
- browse
- log
- 生命周期管理
- `NEW`
- `INGESTED`
- `QC_PENDING`
- `QC_PASSED`
- `QC_REJECTED`
- `PUBLISHED`
- `ARCHIVED`
- 批量操作
- 发布
- 归档
- 重扫
- 重建缓存
- 重绑定来源
- 导出
- 检索增强
- 按引擎
- 按批次
- 按任务名
- 按状态
- 按 AOI
- 按版本
#### 前端结果管理架构建议
- 结果列表视图
- 结果详情视图
- 来源与版本视图
- 治理与运维视图
#### 运维治理项
- 入库记录存在但源文件缺失
- run 存在但 artifact 缺失
- 当前版本指针异常
- 同名结果冲突
- 缓存与主文件不一致
- 已发布结果未完成审核
#### 验收标准
- 同一对影像允许保留多引擎结果
- 同一任务允许保留多版本
- 结果详情页能追踪到来源 run 和主产物列表
- 运维面板可识别结果资产层异常
## 8. 实施顺序
### Phase 1
- 完成多引擎与结果管理设计冻结
- 完成接口契约
- 完成数据模型草案
### Phase 2
- 实现后端抽象层
- 完成 SARSCAPE 兼容接入
- 保持旧接口兼容
### Phase 3
- 实现新前端生产中心
- 实现引擎状态、运行监控和运维自检增强
### Phase 4
- 接入 ISCE2 + WSL
- 打通首条正式生产链
### Phase 5
- 实现结果管理增强
- 补齐版本、来源、治理与自愈
## 9. 交付顺序建议
建议按照以下顺序落地:
1. 先重构生产抽象层,不先碰最终结果治理细节
2. 先让 `sarscape` 在新体系下跑通
3. 再接入 `isce2`
4. 最后再把结果管理全面升级
这样可以避免在“生产链尚未稳定”时提前锁死结果模型细节。
## 10. 当前结论
本轮不应被理解为“给当前 ENVI 面板再加一个 ISCE2 按钮”,而应被理解为:
- D-InSAR 生产系统平台化
- D-InSAR 结果资产化
- 运维自检从系统健康升级为多引擎专项健康
后续所有开发任务、联调任务和运维验收,均以本 Task 文档为总入口。
+167
View File
@@ -0,0 +1,167 @@
# Frontend Navigation Architecture
## 1. Purpose
This document is the source of truth for the left-side navigation structure in the frontend.
It explains:
- the first-level menu groups
- the second-level working domains
- the leaf tabs bound to actual pages
- the reserved entries for future modules
- the files that must be updated when navigation changes
The goal is to keep module boundaries stable as the system expands beyond D-InSAR into PS-InSAR and broader AI analysis workflows.
## 2. Current First-Level Groups
The current first-level menu groups are:
- `data`: 数据管理
- `production`: 生产规划
- `insar_analysis`: InSAR形变分析
- `ai_analysis`: AI分析
- `water`: 水体监测
- `ops`: 运行维护
Definition file:
- `frontend/src/config/appConstants.js`
## 3. Three-Level Structure
Only some first-level groups use second-level working domains.
### 3.1 生产规划
```text
生产规划
├─ 规划编组
│ ├─ 配对规划
│ ├─ 任务规划
│ ├─ PS时序栈
│ └─ 任务批次
├─ 数据分发
│ └─ 数据分发
├─ D-InSAR
│ ├─ D-InSAR生产
│ └─ D-InSAR产物
└─ PS-InSAR
├─ PS-InSAR生产
└─ PS-InSAR产物
```
Notes:
- `PS时序栈` belongs to production planning, not analysis.
- `D-InSAR产物` is distinct from `D-InSAR结果`.
- `PS-InSAR生产` and `PS-InSAR产物` are reserved entries for future implementation.
### 3.2 InSAR形变分析
```text
InSAR形变分析
├─ D-InSAR
│ ├─ D-InSAR结果
│ └─ D-InSAR分析
└─ PS-InSAR
├─ PS-InSAR结果
└─ PS-InSAR分析
```
Notes:
- This group is for business-facing result browsing and deformation analysis.
- AI diagnosis no longer belongs here.
- `D-InSAR分析`, `PS-InSAR结果`, and `PS-InSAR分析` are currently reserved placeholders.
### 3.3 AI分析
```text
AI分析
├─ 形变智能分析
│ ├─ AI质量评估
│ └─ D-InSAR诊断
└─ 遥感视觉分析
├─ 滑坡语义分割
└─ 无人机影像分析
```
Notes:
- This group owns model-centric and intelligent-analysis capabilities.
- `D-InSAR诊断` is the renamed placement of the old AI diagnosis page.
- `滑坡语义分割` and `无人机影像分析` are reserved placeholders for future AI modules.
## 4. Navigation Design Rules
The navigation follows these rules:
- First-level groups represent stable business domains.
- Second-level domains represent workflow clusters inside a domain.
- Leaf tabs represent actual pages bound to `leftPanelTab`.
- The state source of truth remains `leftPanelTab`; group and section are derived from the tab key.
- New features should be added as leaf tabs under an existing domain whenever possible.
- A new first-level group should be introduced only when the feature becomes a long-term standalone capability cluster.
## 5. Naming Rules
To avoid future ambiguity, follow these naming constraints:
- Use `结果` for result browsing, querying, and visualization pages.
- Use `产物` for extraction, packaging, publishing, and catalog management pages.
- Use `分析` for interpretation, statistics,专题分析, and human-facing analytical workflows.
- Use `诊断` for model-assisted fault analysis or AI-driven case reasoning.
- Do not reuse `PS结果` as a generic label.
Use `PS时序栈` in production planning.
Use `PS-InSAR结果` in analysis.
## 6. Files to Update When Navigation Changes
When adding or moving a tab, update these files together:
- `frontend/src/config/appConstants.js`
Defines first-level groups, second-level sections, tab ownership, and visibility.
- `frontend/src/utils/appUiHelpers.js`
Defines display labels for leaf tabs.
- `frontend/src/App.jsx`
Renders the group tabs, section tabs, and page content.
- `frontend/src/App.css`
Styles the first-level, second-level, and leaf-tab navigation.
If the new tab is a real page instead of a placeholder, also add or update the corresponding panel component.
## 7. Reserved Leaf Tabs
The following leaf tabs are intentionally reserved for future work:
- `ps_production`
- `ps_products`
- `dinsar_analysis`
- `psinsar_results`
- `psinsar_analysis`
- `landslide_segmentation`
- `uav_image_analysis`
Reserved tabs should remain visible in the information architecture if they help stabilize the long-term module layout.
## 8. Implementation Notes
- Admin-only visibility is controlled by `ADMIN_ONLY_TABS` in `appConstants.js`.
- Section ownership is derived by `LEFT_TAB_SECTION`.
- Group ownership is derived by `LEFT_TAB_GROUP`.
- Groups without second-level sections still render as a two-level navigation.
- Groups with configured sections render as three-level navigation.
## 9. Future Extension Guidance
Recommended future additions:
- Put PS-InSAR processing forms, task submission, and runtime monitoring under `ps_production`.
- Put PS-InSAR extraction, packaging, registration, and catalog maintenance under `ps_products`.
- Put thematic deformation analysis and reporting under `dinsar_analysis` and `psinsar_analysis`.
- Put optical, UAV, or segmentation-based AI modules under `ai_analysis`, not under `insar_analysis`.
If a new feature belongs to intelligent interpretation or computer vision, prefer adding it under `AI分析`.
If a new feature belongs to result browsing or deformation business analysis, prefer adding it under `InSAR形变分析`.
+163
View File
@@ -0,0 +1,163 @@
# GAMMA + WSL2 双引擎集成方案
## Context
当前系统使用 ENVI/SARscape 做 D-InSAR 处理,速度慢(单对处理需数十分钟到数小时)。用户有 GAMMA License,希望在 Windows 服务器的 WSL2 中部署 GAMMA 作为第二处理引擎。ENVI 保留不动,用户可在前端选择用哪个引擎。
GAMMA 通过 WSL2 调用完全可行:`subprocess.run(['wsl', '-d', 'Ubuntu', 'bash', '-c', 'gamma_cmd ...'])`,与现有 ENVI 的 subprocess 模式一致。
---
## 架构对比
```
ENVI 链路(保留不动):
POST /idl/jobs/dinsar (engine=envi)
→ SystemJob(IDL_RUN_DINSAR) → envi_runner_cli 子进程
→ envi_dinsar.run_dinsar_custom_workflow() → 6步 SARscape
→ envipyengine → taskengine.exe
GAMMA 链路(新增):
POST /idl/jobs/dinsar (engine=gamma)
→ SystemJob(GAMMA_RUN_DINSAR) → gamma_runner_cli 子进程
→ gamma_dinsar.run_dinsar_workflow() → 9步 GAMMA CLI
→ subprocess.run(['wsl', ...]) → GAMMA 原生命令
```
两条链路共享:SystemTask/SystemJob 任务队列、progress.json 进度机制、job_worker 监控逻辑。
---
## 新增文件
| 文件 | 职责 |
|------|------|
| `backend/app/services/gamma_service.py` | WSL2 执行器、路径转换(win↔wsl)、配置读取、环境检查、进度写入 |
| `backend/app/services/gamma_dinsar.py` | GAMMA D-InSAR 9步工作流 |
| `backend/app/services/gamma_runner_cli.py` | 子进程 CLI 入口(对标 envi_runner_cli.py) |
| `backend/app/services/gamma_import.py` | LT-1 原始数据 → GAMMA SLC 格式导入 |
| `scripts/check_gamma_wsl.py` | GAMMA + WSL2 环境诊断脚本 |
## 修改文件
| 文件 | 修改内容 |
|------|----------|
| `.env` | 新增 `GAMMA_ENABLED`、`GAMMA_WSL_DISTRO`、`GAMMA_HOME`、`GAMMA_ENV_SCRIPT`、`GAMMA_DEM_FILE` 等配置 |
| `backend/app/config.py` | Settings 类新增 GAMMA 配置字段 |
| `backend/app/services/job_handlers.py` | 新增 `GAMMA_RUN_DINSAR` handler,注册到 `_HANDLERS` |
| `backend/app/routers/idl.py` | `DinsarJobRequest` 新增 `engine` 字段(envi/gamma),路由分发 |
| `frontend/src/IDLAutomationPanel.jsx` | Step 2 新增引擎选择器(radio:ENVI / GAMMA),GAMMA 不可用时灰显 |
| `frontend/src/api/idl.js` | `queueDinsarJob` payload 新增 `engine` |
---
## GAMMA D-InSAR 9步工作流
每步通过 `gamma_service.gamma_exec(cmd, work_dir)` → `wsl -d {distro} bash -c 'source gamma_env.sh && {cmd}'` 执行。
| 步骤 | GAMMA 命令 | 功能 | 对应 ENVI 步骤 |
|------|-----------|------|---------------|
| 1 | LT-1 导入(自定义) | 原始数据 → .slc + .slc.par | SARsImportLuTan1 |
| 2 | `gc_map` | DEM 准备 + lookup table | (ENVI 内置在 Step1) |
| 3 | `create_offset` + `offset_pwr` + `offset_fit` + `SLC_interp` | 配准 | InterferogramGeneration 内部 |
| 4 | `SLC_intf` + `multi_look` | 干涉图生成 | InterferogramGeneration |
| 5 | `adf` | 自适应滤波(Goldstein) | FilterAndCoherence |
| 6 | `cc_wave` | 相干性估计 | FilterAndCoherence |
| 7 | `mcf` | 相位解缠 | PhaseUnwrapping |
| 8 | `dispmap` | 相位→位移 | PhaseToDisplacement |
| 9 | `geocode_back` + `data2geotiff` | 地理编码 + 转 GeoTIFF | PhaseToDisplacement |
---
## 关键设计
### 路径转换
```python
def win_to_wsl(win_path: str) -> str:
# Z:\Test_data\Task_001 → /mnt/z/Test_data/Task_001
# D:\SRTM\dem.tif → /mnt/d/SRTM/dem.tif
```
### 进度跟踪
写入与 ENVI 完全相同格式的 `backend/runtime/idl_worker/job_{id}_progress.json`,job_handler 的 keepalive 无需修改。GAMMA 的 `total_steps=9`(ENVI 为 6),进度公式 `(pair_index-1+step/total)/total_pairs` 天然适配。
### 输出格式统一
GAMMA 最终输出转为 GeoTIFF(`data2geotiff`),同时生成 ENVI `.hdr`,使 `extract_disp_results()` 和 D-InSAR 结果扫描能统一识别。
### 前端引擎选择
页面加载时调 `GET /gamma/status` 检查 GAMMA 是否可用。可用时显示 radio 选择器,不可用时 GAMMA 选项灰显标注"(未配置)"。
---
## .env 配置项
```ini
# ============ GAMMA + WSL2 配置 ============
# 总开关(false 则前端不显示 GAMMA 选项)
GAMMA_ENABLED=false
# WSL2 发行版名称(wsl -l -v 查看)
GAMMA_WSL_DISTRO=Ubuntu-22.04
# GAMMA 安装路径(Linux 路径,WSL2 内部)
GAMMA_HOME=/opt/GAMMA_SOFTWARE-20230101
# GAMMA 环境初始化脚本(source 此文件后 GAMMA 命令可用)
GAMMA_ENV_SCRIPT=/opt/GAMMA_SOFTWARE-20230101/gamma_env.sh
# DEM 文件路径(Windows 路径,自动转为 WSL 路径)
GAMMA_DEM_FILE=D:\SRTM30m\srtm_dem.tif
# 处理参数
GAMMA_GEOCODE_PIXEL_SIZE_M=10.0
GAMMA_ADF_WINDOW_SIZE=32
GAMMA_MCF_COH_THRESHOLD=0.3
GAMMA_RANGE_LOOKS=4
GAMMA_AZIMUTH_LOOKS=1
```
---
## 分阶段实施
### Phase 1:基础设施 + 环境验证
- `.env` / `config.py` 配置层
- `gamma_service.py`:`win_to_wsl()`、`wsl_exec()`、`gamma_exec()`、`check_gamma_environment()`
- `scripts/check_gamma_wsl.py` 诊断脚本
- `GET /gamma/status` 端点
- 手动验证:`wsl -d Ubuntu bash -c 'source gamma_env.sh && which par_S1_SLC'`
### Phase 2:LT-1 数据导入(技术风险最高)
- `gamma_import.py`:解析 LT-1 `.meta.xml` → 构建 GAMMA `.slc.par` 参数文件
- **关键验证**:对一个 Task_*/master/ 执行导入,检查 .slc.par 参数是否正确
- 如果 GAMMA 内置 LT-1 支持,直接调用;否则需手工构建参数文件
### Phase 3:D-InSAR 核心工作流
- `gamma_dinsar.py`:9步工作流
- `gamma_runner_cli.py`:CLI 入口
- 逐步调试,每步验证输入输出
### Phase 4:任务调度 + 前端集成
- `job_handlers.py`:新增 GAMMA handler
- `idl.py`:engine 字段分发
- `IDLAutomationPanel.jsx`:引擎选择器
- 端到端测试
---
## 风险点
1. **LT-1 导入**(最大风险):GAMMA 是否内置 LT-1 读取器需要安装后确认。Plan B:用 GDAL 读 TIFF + 手工构建 .slc.par
2. **WSL2 跨文件系统 I/O**:`/mnt/z/` 比 WSL 原生文件系统慢 ~5x。初期可接受,后续可优化为先拷贝到 WSL 内部处理
3. **GAMMA License**:WSL2 中需配置 FlexLM `LM_LICENSE_FILE`,在 `gamma_env.sh` 中统一设置
---
## 验证
1. `py_compile` 所有新增/修改的 .py 文件
2. `npm run build` 前端构建通过
3. `python scripts/check_gamma_wsl.py` 环境诊断通过
4. 前端选 GAMMA 引擎 → 提交任务 → 进度正常更新 → 输出 GeoTIFF 位移图
5. 前端选 ENVI 引擎 → 行为与改造前完全一致(回归测试)
6. GAMMA 未配置时 → 前端 GAMMA 选项灰显,无法选择
+102
View File
@@ -0,0 +1,102 @@
# 当前文档总览
这是 `docs/` 目录的统一入口文档。
从现在开始,`docs/` 根目录只保留“当前仍在使用”的文档;历史过程材料、已完成实施记录、被新文档覆盖的旧方案,统一放入 `docs/archive/`。
## 1. 当前有效文档
### 核心使用
- **[README.md](../README.md)**
系统主文档。优先阅读,包含整体功能、架构、快速开始和主要配置说明。
- **[DEPLOYMENT.md](DEPLOYMENT.md)**
部署与运维细节文档。适合正式部署、数据库初始化、服务启动和环境核对时使用。
- **[PROJ_CONFIGURATION.md](PROJ_CONFIGURATION.md)**
PROJ/GDAL 相关专项配置说明。只有在坐标转换或 PROJ 冲突时才需要单独查看。
### 当前安全状态
- **[SECURITY_AUDIT_2026-03-12.md](SECURITY_AUDIT_2026-03-12.md)**
当前有效的安全审计结论。已经把“开发机阶段可接受暴露”和“上线前必须修复的问题”分开说明,后续安全判断以这份为准。
### 仍在推进的设计与任务
- **[DINSAR_ENHANCEMENT_TASKS.md](DINSAR_ENHANCEMENT_TASKS.md)**
D-InSAR 多引擎生产增强与结果管理增强的总 Task 文档。覆盖前端架构、ENVI 六步链路、ISCE2 的 WSL 校验、运维自检与结果治理。
- **[PAIRING_ENHANCEMENT_DESIGN.md](PAIRING_ENHANCEMENT_DESIGN.md)**
配对能力增强设计。属于未完全落地的专项设计文档。
- **[GAMMA_WSL2_INTEGRATION_PLAN.md](GAMMA_WSL2_INTEGRATION_PLAN.md)**
GAMMA + WSL2 双引擎方案。属于后续扩展规划,不是当前默认运行链路。
- **[WSL2_ISCE2_MINTPY_SBAS_INTEGRATION_PLAN_20260412.md](WSL2_ISCE2_MINTPY_SBAS_INTEGRATION_PLAN_20260412.md)**
基于本机 `Ubuntu-24.04` WSL2、`isce2` / `mintpy` / `isce2_mintpy_v1` 实际环境核对后的 SBAS 集成落地方案,明确推荐运行时、workflow 补全顺序和正式产品边界。
- **[ISCE2_SBAS_TIMESERIES_DESIGN.md](ISCE2_SBAS_TIMESERIES_DESIGN.md)**
ISCE2 框架下 SBAS / 时序生产设计稿,说明如何从现有 `PS stack batch` 扩展到 `workflow + psinsar catalog` 的正式生产链路。
- **[SBAS_PROGRESS_STATUS_20260406.md](SBAS_PROGRESS_STATUS_20260406.md)**
SBAS 系统改造当前落地状态。优先查看这份文档以了解“已经做到哪一步、验证做到哪一步、下一步该补什么”。
- **[SBAS_SYSTEM_INTEGRATION_AUDIT_20260406.md](SBAS_SYSTEM_INTEGRATION_AUDIT_20260406.md)**
对现有系统接入 SBAS 的审计结果,聚焦现状缺口与最小可行接入点。
- **[SBAS_SYSTEM_EMBEDDING_DESIGN_20260406.md](SBAS_SYSTEM_EMBEDDING_DESIGN_20260406.md)**
SBAS 嵌入现有系统的详细设计稿,覆盖运行记录、作业编排、结果注册与前端页面边界。
- **[SBAS_TASK_PATH_MODE_EVALUATION_20260406.md](SBAS_TASK_PATH_MODE_EVALUATION_20260406.md)**
评估是否沿用现有 D-InSAR `task 路径 -> 生产` 模式,以及 SBAS 为什么必须增加 `prepare` 阶段。
- **[SBAS_SYSTEM_CHANGE_RISK_ASSESSMENT_20260406.md](SBAS_SYSTEM_CHANGE_RISK_ASSESSMENT_20260406.md)**
SBAS 系统改造风险评估,重点约束“新增不伤旧链”。
- **[SBAS_RESULT_MANAGEMENT_AND_DISPLAY_SPEC_20260406.md](SBAS_RESULT_MANAGEMENT_AND_DISPLAY_SPEC_20260406.md)**
SBAS 结果管理、编目和展示约定,明确发布级 bundle 与系统结果目录的关系。
- **[SBAS_FRONTEND_UX_SPEC_20260406.md](SBAS_FRONTEND_UX_SPEC_20260406.md)**
SBAS 生产页、结果页与分析页的前端交互设计。
- **[SBAS_IMPLEMENTATION_TODO_20260406.md](SBAS_IMPLEMENTATION_TODO_20260406.md)**
SBAS 系统改造任务清单,适合作为后续 Phase 2 / Phase 3 推进的执行列表。
## 2. 已归档文档
以下文档已移入 `docs/archive/`,原因通常是“历史记录”“已完成实施”或“被后续结论覆盖”。
### 本轮归档
- `SECURITY_AUDIT_2026-03-04.md`
- `SECURITY_AUDIT_ANALYSIS_2026-03-04.md`
- `SECURITY_FIX_PLAN.md`
- `SECURITY_FIX_PROGRESS.md`
- `SECURITY_FIX_CHECKLIST.md`
- `SECURITY_FIX_STAGE1_SUMMARY.md`
- `SECURITY_OVERVIEW.md`
- `HARDCODE_AUDIT_2026-03-05.md`
- `LOG_MANAGEMENT_IMPLEMENTATION_2026-03-05.md`
### 既有归档
`docs/archive/` 中原有的旧设计、旧 TODO、阶段性分析和历史修复记录继续保留,仅作历史参考,不再视为当前执行依据。
## 3. 建议阅读顺序
### 新接手项目
1. 先读 [README.md](../README.md)
2. 再读 [DEPLOYMENT.md](DEPLOYMENT.md)
3. 若涉及上线前风险,再读 [SECURITY_AUDIT_2026-03-12.md](SECURITY_AUDIT_2026-03-12.md)
### 正式部署
1. [README.md](../README.md)
2. [DEPLOYMENT.md](DEPLOYMENT.md)
3. [PROJ_CONFIGURATION.md](PROJ_CONFIGURATION.md)(仅在需要时)
### 后续功能规划
1. [DINSAR_ENHANCEMENT_TASKS.md](DINSAR_ENHANCEMENT_TASKS.md)
2. [PAIRING_ENHANCEMENT_DESIGN.md](PAIRING_ENHANCEMENT_DESIGN.md)
3. [PAIRING_SYSTEM_REFACTOR_PLAN_20260413.md](PAIRING_SYSTEM_REFACTOR_PLAN_20260413.md)
4. [ISCE2_SBAS_TIMESERIES_DESIGN.md](ISCE2_SBAS_TIMESERIES_DESIGN.md)
5. [WSL2_ISCE2_MINTPY_SBAS_INTEGRATION_PLAN_20260412.md](WSL2_ISCE2_MINTPY_SBAS_INTEGRATION_PLAN_20260412.md)
6. [GAMMA_WSL2_INTEGRATION_PLAN.md](GAMMA_WSL2_INTEGRATION_PLAN.md)
## 4. 后续维护规则
- `docs/` 根目录只放当前有效文档。
- 已完成实施总结、阶段性修复过程、旧审计分析、被覆盖方案,统一放入 `docs/archive/`。
- 新增文档前,优先判断是否可以并入现有文档,避免再次堆出多份并行说明。
- 如果某份文档只记录一次任务过程,而不是长期参考资料,不应继续留在根目录。
**最后整理**:2026-03-13
+275
View File
@@ -0,0 +1,275 @@
# ISCE2 SBAS Product Specification
Updated: 2026-04-06
## 1. Goal
Define the stable product contract for the current phase-1 SBAS route:
- `ISCE2 stripmapStack -> MintPy SBAS -> geocode/export -> psinsar publish bundle`
This document answers three practical questions:
- what the final SBAS deliverables are
- which files are runtime-only vs publish-grade
- which file the future system should treat as the publish entrypoint
## 2. Product Layers
The current experiment has three artifact layers.
### 2.1 Processing-layer runtime artifacts
Generated under a MintPy work directory such as:
- `stack_work/mintpy_sbas_v5/`
These are processing outputs, not yet the final publish contract.
Key files:
- `timeseries.h5`
- radar-coordinate time-series cube
- core scientific output
- `velocity.h5`
- radar-coordinate velocity solution
- useful for quick inspection and later geocode
- `temporalCoherence.h5`
- quality indicator after inversion
- `maskTempCoh.h5`
- thresholded reliability mask
- `maskAllValid.h5`
- strict runtime mask used before inversion
- `numTriNonzeroIntAmbiguity.h5`
- unwrap/network diagnostic
- `numTriNonzeroIntAmbiguity.png`
- browseable diagnostic preview
Current runtime boundary:
- the minimum validated SBAS runtime boundary is `timeseries.h5` plus `velocity.h5`
### 2.2 Publish-layer standardized artifacts
Generated under a publish directory such as:
- `publish/mintpy_sbas_v5/`
This is the current publish-grade boundary for system embedding.
Directory contract:
- `manifest.json`
- `assets/`
- `preview/`
- `metadata/`
### 2.3 Catalog-layer registration artifacts
The future backend should publish the standardized bundle into the `psinsar` catalog.
Current rule:
- the backend should not register raw MintPy runtime paths directly
- the backend should register the publish bundle rooted by `manifest.json`
- the backend should populate `manifest.json.group_key` from task/run metadata during publish registration
## 3. Canonical Publish Bundle
### 3.1 Entrypoint
The publish entrypoint is:
- `manifest.json`
Reason:
- it binds product identity, stack dates, reference info, artifact paths, and quality summary in one place
- it allows the backend to register one bundle without hard-coding per-file conventions
- it decouples future UI/backend integration from the temporary MintPy work directory layout
### 3.2 Primary data products
#### `assets/geo_timeseries.h5`
Role:
- primary time-series product
Why it matters:
- this is the file that proves the system has true time-series capability
- every valid pixel contains multi-date displacement values rather than one summary statistic
Recommended system use:
- archive as the canonical analysis cube
- expose as the main download for advanced analysis
- use as the source for future point-query / profile / date-slice APIs
#### `assets/geo_velocity.h5`
Role:
- geocoded structured velocity product
Recommended system use:
- archive together with the time-series cube
- keep for programmatic reading when HDF5-native access is preferred
#### `assets/velocity.tif`
Role:
- publish-grade browse/export raster
Why it matters:
- easiest file for GIS browsing, thumbnail generation, map services, and first-screen display
Recommended system use:
- default map layer for result browsing
- not the only final product
- should be treated as a summary view derived from the time-series solution
### 3.3 Quality products
#### `assets/geo_temporalCoherence.h5`
Role:
- structured quality field
#### `assets/temporalCoherence.tif`
Role:
- GIS-friendly quality raster
Recommended use:
- review where the inversion is reliable
#### `assets/geo_maskTempCoh.h5`
Role:
- structured reliable-pixel mask
#### `assets/maskTempCoh.tif`
Role:
- GIS-friendly publish mask
Recommended use:
- mask browsing products
- support frontend display filtering
### 3.4 Preview and diagnostics
#### `preview/velocity_preview.png`
Role:
- lightweight preview image for cards, list pages, or quick QA
#### `preview/numTriNonzeroIntAmbiguity.png`
Role:
- diagnostic browse image
#### `metadata/source_quality_summary.json`
Role:
- compact machine-readable quality summary
#### `metadata/smallbaselineApp.cfg`
Role:
- reproducibility record of the MintPy configuration
## 4. File Priorities By Use Case
If the goal is system publish or catalog registration:
- focus on `manifest.json`
If the goal is proving time-series capability:
- focus on `assets/geo_timeseries.h5`
If the goal is first-screen map display:
- focus on `assets/velocity.tif`
If the goal is quality control:
- focus on `assets/temporalCoherence.tif`
- focus on `assets/maskTempCoh.tif`
If the goal is full reproducibility:
- keep `manifest.json`
- keep `metadata/smallbaselineApp.cfg`
- keep runtime `maskAllValid.h5` as a workflow artifact even if it is not published as a primary product
## 5. Current Product Typing
Current artifact typing implemented by the experiment bundle builder:
- `timeseries_cube`
- `assets/geo_timeseries.h5`
- `velocity_map`
- `assets/geo_velocity.h5`
- `velocity_geotiff`
- `assets/velocity.tif`
- `temporal_coherence`
- `assets/geo_temporalCoherence.h5`
- `temporal_coherence_geotiff`
- `assets/temporalCoherence.tif`
- `quality_mask`
- `assets/geo_maskTempCoh.h5`
- `quality_mask_geotiff`
- `assets/maskTempCoh.tif`
- `preview_png`
- `preview/velocity_preview.png`
- `diagnostic_png`
- `preview/numTriNonzeroIntAmbiguity.png`
## 6. Publish Rules
Current recommended rules:
- publish the geocoded bundle, not the radar-coordinate MintPy runtime directory
- treat `manifest.json` as the registration root
- treat `geo_timeseries.h5` as the canonical time-series data product
- treat `velocity.tif` as the default visualization product
- always publish at least one quality layer beside the velocity layer
- keep diagnostics and config snapshots for reproducibility even if the frontend does not display all of them
## 7. Current Open Items
Still to be formalized before production:
- final CRS contract beyond the current `EPSG:4326` experiment assumption
- whether the backend stores one product record per artifact or one bundle-root record plus child artifacts
- how the frontend will query time-series pixels from `geo_timeseries.h5`
- whether extra export formats such as COG, CSV point extraction, or JSON summaries should be added
## 8. Current Judgment
For the current phase-1 SBAS route, the final deliverable should be understood as:
- one manifest-driven publish bundle
- centered on `assets/geo_timeseries.h5`
- summarized by `assets/velocity.tif`
- constrained by `assets/temporalCoherence.tif` and `assets/maskTempCoh.tif`
That is the product contract the future system should embed first.
+609
View File
@@ -0,0 +1,609 @@
# ISCE2 SBAS Time-Series Production Design
Updated: 2026-04-06
## 1. Goal
Add stack-based time-series production under the current ISCE2-oriented architecture.
The current repository already has:
- image discovery and stack selection
- PS/time-series stack batch persistence
- single-pair D-InSAR production
The current repository does not yet have:
- a stack-level production entry
- time-series workflow orchestration
- SBAS inversion output registration
- PS-InSAR result/product pages beyond placeholders
This design focuses on the first deliverable:
- implement SBAS time-series production first
- keep true PS-InSAR or StaMPS as a later phase
Related product contract:
- `docs/ISCE2_SBAS_PRODUCT_SPEC.md`
## 2. Current State
### 2.1 What already exists
- `backend/app/routers/pairing.py`
- `/find-ps-timeseries` can search image stacks for time-series use.
- `backend/app/routers/task_batches.py`
- `/task-batches/ps` persists a selected stack into `PsTaskBatchORM` and `PsTaskItemORM`.
- `backend/app/models/orm.py`
- `ps_task_batches` and `ps_task_items` already represent the planning-layer stack snapshot.
- `backend/app/models/orm.py`
- `workflow_runs`, `workflow_steps`, and `workflow_artifacts` already exist and are suitable for multi-step orchestration.
- `backend/app/models/orm.py`
- `result_products` already supports multiple catalogs through `catalog_name`.
- `frontend/src/App.jsx`
- `ps_production`, `ps_products`, `psinsar_results`, and `psinsar_analysis` are reserved placeholders.
### 2.2 What is missing
- `backend/app/dinsar_engines/isce2_engine.py` is pair-oriented.
- It runs a custom LT-1 `stripmapApp.py` flow for one pair or a pair-root directory.
- It does not model stack/network/time-series execution.
- Current `SBAS` in pairing-related code and docs means a pairing strategy.
- It does not mean completed SBAS inversion, velocity estimation, or time-series products.
- The current PS batch is not a production run.
- It is only a stored stack selection.
### 2.3 Architectural implication
Time-series production is not a small extension of the current pair-based D-InSAR engine.
The processing object changes from:
- `pair -> one run -> one main displacement output`
to:
- `stack/network -> multi-step run -> multiple intermediate and final products`
Because of that, time-series production should not be forced into `DinsarEngine` as-is.
Still, the LT-1 input-preparation layer should be shared.
Recommended reuse point:
- keep DEM path resolution, orbit-pool resolution, and LT-1 precise-orbit XML generation in a shared helper
- current implementation anchor:
- `backend/app/isce2_pipeline/lt1_input_resolver.py`
### 2.4 Experiment Status
Current LT-1 stack experiment status already de-risks the processing side of phase 1:
- one offline LT-1 sample stack has completed the generated `run_01` to `run_07` chain under `Ubuntu-24.04`
- the same LT-1 sample stack has also completed `run_08_igram`, producing filtered and unwrapped pair products under `Igrams/`
- the same LT-1 sample stack has now also completed the first MintPy SBAS smoke test through radar-coordinate `timeseries.h5` and `velocity.h5`
- current successful MintPy work directory:
- `/mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/stack_work/mintpy_sbas_v5`
- the same LT-1 sample stack has now also completed an experiment-layer geocode plus publish-bundle export
- current successful publish-style directory:
- `/mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/publish/mintpy_sbas_v5`
- local SAR scenes, local orbit data, and a local DEM were sufficient for this stage
- Earthdata `SWBD` was not treated as a hard dependency during experiments because `run_01_reference` can recover with a synthetic all-land `waterMask`
- current runtime split decision:
- keep ISCE2 stack processing in the existing WSL `isce2` env
- validate MintPy in a separate WSL `mintpy` env to avoid mutating the working processing env on the development machine
- phase-4 unified-env update:
- a recreated WSL env `isce2_mintpy_v1` has now also completed the same LT-1 SBAS smoke test and publish export without the `isce` bridge
- current successful unified SBAS work directory:
- `/mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/stack_work/mintpy_sbas_unified_v1`
- current successful unified publish directory:
- `/mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/publish/mintpy_sbas_unified_v1`
- current judgment:
- unified env is viable at experiment layer and is now the preferred SBAS experiment runtime
- existing WSL `isce2` should remain untouched for current D-InSAR production safety
- bridge env remains the fallback baseline until a fuller comparison is written
- current MintPy bridge decision:
- keep `mintpy` isolated
- bridge only the top-level `isce` package from the `isce2` env into the `mintpy` env, because MintPy's ISCE stripmap metadata path imports `isce`
- do not bridge the full `isce2` `site-packages`, because that polluted the MintPy runtime with conflicting `h5py`
- current MintPy runtime workaround decision:
- use a strict `maskAllValid.h5` before inversion to suppress unstable partial-network pixels
- use a repo-local patched launcher for the current MintPy `1.6.2` single-pixel partial-network inversion bug
- current implementation anchors:
- `experiments/isce2_sbas_timeseries/scripts/create_mintpy_all_ifgram_mask.py`
- `experiments/isce2_sbas_timeseries/scripts/run_smallbaselineApp_patched.py`
- `experiments/isce2_sbas_timeseries/scripts/run_mintpy_sbas_smoketest_ubuntu2404.sh`
- current experiment-layer publish export decision:
- keep geocode/export as a separate post-MintPy stage
- current implementation anchors:
- `experiments/isce2_sbas_timeseries/scripts/export_mintpy_publish_products_ubuntu2404.sh`
- `experiments/isce2_sbas_timeseries/scripts/build_mintpy_publish_bundle.py`
- the shared helper refactor is already in place:
- `backend/app/isce2_pipeline/lt1_input_resolver.py`
- compatibility rule:
- the original pair-oriented D-InSAR public entry was not removed
- `backend/app/isce2_pipeline/run_lt1_dinsar_pipeline.py` still owns the existing workflow entry and now delegates shared input preparation to the helper
## 3. Recommendation
### 3.1 Deliver SBAS first
Recommended first path:
- `stack planning -> stack prep -> ISCE2 stack processing -> MintPy smallbaselineApp -> psinsar product publish -> result browsing`
Reasons:
- the repository already has stack-selection concepts
- SBAS outputs are easier to standardize into cataloged products
- MintPy is a practical downstream tool for small-baseline inversion over ISCE outputs
- the UI and backend risk are both lower than starting from full PS-InSAR
### 3.2 Do not start with StaMPS / true PS route
True PS-InSAR should be treated as phase 2 or later because it adds extra uncertainty in:
- candidate selection and amplitude stability logic
- stack export conventions
- result structure standardization
- point-level visualization and analysis UX
## 4. Recommended Technical Chain
Phase-1 recommended chain:
1. User creates or reuses a `PS stack batch`.
2. System creates a stack-level production run bound to that batch.
3. Workflow prepares a working directory and stack metadata.
4. Workflow runs ISCE2 stack processing in WSL.
5. Workflow runs MintPy SBAS inversion in a dedicated WSL MintPy runtime.
6. Workflow exports standardized products and manifest files.
7. Workflow publishes the outputs into a `psinsar` result catalog.
8. Frontend reads the catalog and workflow state for products and results.
Product interpretation rule:
- `manifest.json` is the publish entrypoint
- `assets/geo_timeseries.h5` is the canonical time-series data product
- `assets/velocity.tif` is the default browse layer, not the only final deliverable
## 5. Scope
### 5.1 In scope for phase 1
- SBAS time-series production entry
- stack-level workflow orchestration
- WSL runtime checks for stack and MintPy steps
- product extraction and catalog registration
- basic `ps_production` and `ps_products` pages
- basic `psinsar_results` browsing capability
### 5.2 Out of scope for phase 1
- full PS-InSAR / StaMPS route
- advanced point-based analysis UI
- cluster scheduling
- multi-sensor stack fusion
- automatic reference-point intelligence
## 6. Target Architecture
### 6.1 Domain split
Keep the system split into three layers:
- Planning layer
- existing `find-ps-timeseries`
- existing `ps_task_batches`
- Production layer
- new stack-level time-series run submission and monitoring
- Result layer
- `psinsar` products and business-facing browsing
This keeps planning data and production runs decoupled.
### 6.2 Orchestration model
Reuse the existing generic workflow subsystem:
- `workflow_runs`
- `workflow_steps`
- `workflow_artifacts`
Recommended rule:
- do not extend `DinsarEngine` for time-series phase 1
- use `workflow` as the orchestration primitive
- add a thin time-series metadata table that binds:
- one `ps_task_batch`
- one workflow run
- one processing mode
- one output root
### 6.3 Suggested new metadata table
Add a new business table such as `ps_timeseries_runs`.
Suggested fields:
- `run_id`
- `batch_id`
- `workflow_run_id`
- `mode`
- `sbas`
- `engine_code`
- `isce2`
- `processor_code`
- `isce2_stack_mintpy`
- `direction`
- `status`
- `work_dir`
- `output_dir`
- `publish_dir`
- `reference_date`
- `reference_strategy`
- `params_json`
- `summary_json`
- `error_message`
- `created_by`
- `created_at`
- `updated_at`
- `started_at`
- `ended_at`
Notes:
- `PsTaskBatchORM` remains the planning snapshot.
- `WorkflowRunORM` remains the step-level orchestration record.
- `ps_timeseries_runs` becomes the business-facing production record.
### 6.4 Suggested workflow
Recommended workflow name:
- `isce2_sbas_mintpy_v1`
Recommended steps:
1. `prepare_stack_input`
- validate stack size and metadata
- build stack working directory
- resolve DEM/orbit/output paths
2. `run_isce2_stack`
- run stack pre-processing in WSL
- collect stack intermediate artifacts
3. `run_mintpy_sbas`
- run repo-controlled MintPy `smallbaselineApp`
- generate strict inversion mask if required by the pinned MintPy runtime
- generate time-series and velocity products
4. `export_standard_products`
- copy or convert outputs into stable publish structure
- generate manifest and preview assets
5. `publish_psinsar_products`
- register catalog entries
- update product status and coverage
### 6.5 Suggested job types
Recommended new job types:
- `TIMESERIES_PREP_STACK`
- `ISCE2_STACK_RUN`
- `MINTPY_SBAS_RUN`
- `EXPORT_PSINSAR_PRODUCTS`
- `PUBLISH_PSINSAR_PRODUCTS`
These fit naturally under the existing job queue and worker model.
## 7. Backend Design
### 7.1 New modules
Recommended backend additions:
- `backend/app/routers/timeseries_production.py`
- `backend/app/routers/ps_products.py`
- `backend/app/services/timeseries_service.py`
- `backend/app/services/timeseries_workflow_factory.py`
- `backend/app/services/isce2_stack_service.py`
- `backend/app/services/mintpy_service.py`
- `backend/app/services/psinsar_catalog_service.py`
- `backend/app/services/timeseries_paths.py`
### 7.2 API sketch
Recommended new APIs:
- `POST /timeseries-production/runs`
- submit one SBAS run for one stored PS stack batch
- `GET /timeseries-production/runs`
- list recent time-series runs
- `GET /timeseries-production/runs/{run_id}`
- get business metadata + workflow state + artifacts
- `POST /timeseries-production/runs/{run_id}/retry-step`
- retry failed step when allowed
- `POST /timeseries-production/wsl-check`
- check stack + MintPy runtime environment
- `GET /ps-products`
- list registered `psinsar` products
- `POST /ps-products/rebuild-catalog`
- rebuild `psinsar` product catalog
### 7.3 Reuse of current tables and services
Recommended reuse:
- `ps_task_batches` / `ps_task_items`
- keep as the source stack definition
- `workflow_runs` / `workflow_steps` / `workflow_artifacts`
- keep as the run and step state model
- `result_products`
- extend with `catalog_name = psinsar`
- current job queue / worker / task log chain
- reuse for execution and monitoring
### 7.4 Product catalog strategy
Do not build a separate product table just for time-series phase 1.
Use `result_products` with:
- `catalog_name = psinsar`
- `engine_code = isce2`
- `product_type` values specific to time-series outputs
Recommended `product_type` values:
- `velocity_map`
- `timeseries_cube`
- `temporal_coherence`
- `ifgram_network`
- `point_series_csv`
- `preview_png`
- `summary_report`
Use the existing generic asset and issue models where possible.
### 7.5 Current Phase-1 Artifact Contract
The current validated LT-1 experiment supports the following publish contract:
- `timeseries_cube`
- source file:
- `assets/geo_timeseries.h5`
- `velocity_map`
- source file:
- `assets/geo_velocity.h5`
- `velocity_geotiff`
- source file:
- `assets/velocity.tif`
- `temporal_coherence`
- source file:
- `assets/geo_temporalCoherence.h5`
- `quality_mask`
- source file:
- `assets/geo_maskTempCoh.h5`
- `temporal_coherence_geotiff`
- source file:
- `assets/temporalCoherence.tif`
- `quality_mask_geotiff`
- source file:
- `assets/maskTempCoh.tif`
- `ifgram_network`
- source file:
- runtime diagnostic source:
- `numTriNonzeroIntAmbiguity.h5`
- `preview_png`
- source file:
- `preview/velocity_preview.png`
- `diagnostic_png`
- source file:
- `preview/numTriNonzeroIntAmbiguity.png`
- retained runtime-only artifacts:
- `maskAllValid.h5`
- `avgSpatialCoh.h5`
- `smallbaselineApp.cfg`
Current sample manifest draft:
- `experiments/isce2_sbas_timeseries/configs/sample_psinsar_manifest_lt1_e123p3_n46p1.json`
Current successful experiment publish bundle:
- `experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/publish/mintpy_sbas_v5/manifest.json`
## 8. Directory Conventions
Recommended Windows roots:
- `PS_TIMESERIES_WORK_ROOT`
- `PS_TIMESERIES_OUTPUT_ROOT`
- `PSINSAR_PRODUCT_DIR`
Recommended run layout:
```text
<PS_TIMESERIES_WORK_ROOT>/<batch_id>/<run_id>/
stack_input/
isce2_stack/
mintpy/
export/
logs/
```
Recommended publish layout:
```text
<PSINSAR_PRODUCT_DIR>/<product_id>/
manifest.json
preview/
assets/
metadata/
```
Rules:
- production should always have a stable publish directory
- intermediate work directory and final publish directory should stay separate
- raw imagery must not be copied into publish output unnecessarily
## 9. Frontend Design
### 9.1 `ps_production`
Phase-1 page content:
- select an existing PS stack batch
- choose processing mode
- phase 1 only exposes `SBAS`
- show runtime environment status
- WSL
- ISCE2 stack runtime
- MintPy runtime
- submit workflow run
- show step status, logs, and artifacts
### 9.2 `ps_products`
Phase-1 page content:
- list `psinsar` products
- filter by status, run, batch, direction, date
- publish and rebuild operations
- basic manifest view
### 9.3 `psinsar_results`
Phase-1 page content:
- basic product query
- map preview of velocity or deformation raster
- metadata drawer
- quick link to workflow run and source batch
### 9.4 `psinsar_analysis`
Keep this reserved in phase 1.
Later it can take:
- point time-series browsing
- rate classification
- hotspot statistics
- thematic reporting
## 10. Configuration
Recommended new or clarified settings:
- `PS_TIMESERIES_ENABLED`
- `PS_TIMESERIES_WORK_ROOT`
- `PS_TIMESERIES_OUTPUT_ROOT`
- `PSINSAR_PRODUCT_DIR`
- `MINTPY_PYTHON`
- `MINTPY_SMALLBASELINE_APP`
- `MINTPY_RUNNER`
- `MINTPY_PATCHED_SMALLBASELINE_APP`
- `MINTPY_TEMPLATE_DIR`
- `PS_TIMESERIES_AUTO_PUBLISH`
Where practical:
- reuse `ISCE2_WSL_DISTRO`
- reuse `ISCE2_PYTHON` if MintPy is installed in the same environment
- reuse current DEM and orbit path conventions
## 11. Key Risk: LT-1 Stack Compatibility
This is the main technical uncertainty.
The current repository already uses a custom LT-1 single-pair `stripmapApp.py` flow.
That does not prove official ISCE2 stack tooling will work directly for LT-1/LUTAN1.
Therefore phase 0 must verify:
- whether official ISCE2 stack tooling can ingest LT-1 metadata directly
- whether orbit formatting is already sufficient for stack mode
- whether extra stack-input conversion is required
If official stack tooling is not directly compatible:
- keep the overall architecture unchanged
- implement a custom stack builder or adapter layer before MintPy
Do not hard-code the design around official stack scripts until this experiment is confirmed.
## 12. Option Comparison
| Option | Fit for current repo | Technical risk | UI/ops complexity | Phase-1 recommendation |
|---|---|---|---|---|
| ISCE2 stack + MintPy SBAS | High | Medium | Medium | Yes |
| ISCE2 StackToStaMPS / PS route | Medium-Low | High | High | No |
Why SBAS wins first:
- closer to the current stack-planning model
- easier to standardize outputs
- easier to explain and operate
- lower uncertainty in first delivery
## 13. TODO List
### Phase 0. Validation
- [x] Verify whether LT-1/LUTAN1 can run through official ISCE2 stack tooling.
- [x] Decide whether MintPy shares the current ISCE2 environment or uses a separate env.
- [x] Finalize work/output/publish directory rules.
- [x] Decide the minimal product set for first release.
### Phase 1. Backend scaffold
- [ ] Add `ps_timeseries_runs` ORM and schema.
- [ ] Add `timeseries_production` router.
- [ ] Add `timeseries_workflow_factory` for `isce2_sbas_mintpy_v1`.
- [ ] Add new job handlers for stack prep, stack run, MintPy run, export, and publish.
- [ ] Bind workflow runs to `ps_task_batches`.
- [ ] Persist workflow artifacts for major intermediate and final outputs.
### Phase 2. Catalog and products
- [ ] Extend catalog publishing to support `catalog_name = psinsar`.
- [ ] Define manifest schema for SBAS products.
- [ ] Add coverage extraction and preview generation.
- [ ] Add rebuild and health-check operations for `psinsar` products.
### Phase 3. Frontend
- [ ] Replace the `ps_production` placeholder with a real production panel.
- [ ] Replace the `ps_products` placeholder with a product management panel.
- [ ] Add a basic `psinsar_results` result page.
- [ ] Keep `psinsar_analysis` reserved until point-series UX is clear.
### Phase 4. Validation and rollout
- [x] Run one small AOI stack end to end in the experimental workflow.
- [ ] Verify rerun, resume, and failure-recovery behavior.
- [ ] Verify published products can be queried and rendered.
- [ ] Add operator documentation and troubleshooting notes.
## 14. Experimental Environment Recommendation
Yes. A dedicated experiment folder is recommended.
Purpose:
- validate LT-1 stack compatibility without polluting production code
- collect command templates, notes, and sample manifests
- separate exploratory scripts from backend services
Recommended location:
- `experiments/isce2_sbas_timeseries/`
Rules:
- store only scripts, notes, configs, and tiny mock artifacts in git
- do not commit raw SAR scenes, DEM rasters, or large intermediate outputs
- once an experiment stabilizes, move the conclusion back into `docs/` and production code
+536
View File
@@ -0,0 +1,536 @@
# D-InSAR 配对增强设计文档
**版本**: v2.0
**日期**: 2026-03-08
**状态**: 设计阶段
---
## 一、背景与目标
### 1.1 现有系统局限
当前配对系统采用**单池全组合**模式:
- 所有影像放入同一个池子,两两组合,用时间基线范围过滤
- 无法控制主影像(master)和辅影像(slave)的时间段
- 无法指定配对策略(星型、链式、SBAS 网络等)
- 不支持手动选定参考影像
这种设计适合探索性分析,但不满足生产级 InSAR 处理需求。
### 1.2 行业标准实践
参考成熟 InSAR 软件(ISCE、LiCSBAS、MintPy、GAMMA):
| 特性 | 行业标准 | 现有系统 |
|---|---|---|
| 主/辅影像分池 | 明确的 reference/secondary 选择 | ❌ 无 |
| 配对策略 | SBAS / Sequential / Star | ❌ 仅全组合 |
| 时间范围控制 | 主辅池独立时间段 | ❌ 单一 start_date |
| 参考影像指定 | 支持手动或自动选择 | ❌ 无 |
| 多卫星支持 | 预留扩展机制 | ⚠️ 硬编码 LT-1 |
### 1.3 设计目标
1. **双池配对机制**:主影像池和辅影像池独立时间范围
2. **多策略支持**:SBAS / Sequential / Star 三种标准策略
3. **参考影像控制**:自动选择或用户指定
4. **多卫星扩展性**:预留 Sentinel-1、ALOS-2、TerraSAR-X 等卫星接入
5. **向后兼容**:现有配对逻辑作为 SBAS 策略保留
---
## 二、配对策略详解
### 2.1 SBAS(短基线子集)
**定义**:在时空基线阈值内全组合配对,形成冗余网络。
**适用场景**:
- 大范围形变监测
- 需要高时间分辨率和空间覆盖
- 对大气误差敏感的区域
**参数**:
- `time_baseline_min/max`:时间基线范围(天)
- `spatial_baseline_max_meters`:空间基线上限(米)
- `overlap_threshold`:重叠率阈值
- `coverage_diversity_penalty`:覆盖多样性惩罚因子
**配对逻辑**:
```
主池影像 × 辅池影像 → 满足时空基线约束 → 覆盖优化 → 最终配对
```
**示例**:
```
主池: 2024-01-01 ~ 2024-06-30 (10 景)
辅池: 2024-07-01 ~ 2024-12-31 (12 景)
时间基线: 30~90 天
→ 生成 45 对(经覆盖优化后约 20 对)
```
### 2.2 Sequential(顺序配对)
**定义**:按时间排序,每景影像与后续 N 景配对。
**适用场景**:
- 连续监测(如滑坡、地面沉降)
- 需要时间连续性的形变序列
- 计算资源有限时
**参数**:
- `num_connections`:每景与后续几景配对(默认 1)
- 其他时空约束同 SBAS
**配对逻辑**:
```
1. 合并主辅池,按日期排序
2. 每景与后续 num_connections 景配对
3. 应用时空基线过滤
```
**示例**:
```
影像序列: [A, B, C, D, E, F]
num_connections = 2
→ 配对: A-B, A-C, B-C, B-D, C-D, C-E, D-E, D-F, E-F
```
### 2.3 Star(星型配对)
**定义**:选择一个参考影像,所有其他影像与它配对。
**适用场景**:
- 时序形变分析(PS-InSAR 前处理)
- 需要统一参考基准
- 参考影像质量已知且优秀
**参数**:
- `reference_image_id`:指定参考影像 ID(可选)
- 自动选择策略:选择与其他影像平均重叠率最高的影像
**配对逻辑**:
```
1. 确定参考影像(用户指定 或 自动选择)
2. 参考影像 × 所有其他影像
3. 应用时空基线过滤
```
**示例**:
```
参考影像: 2024-06-15 (质量最优)
其他影像: 20 景
→ 配对: 20 对(全部以 2024-06-15 为主影像)
```
---
## 三、双池配对机制
### 3.1 时间范围控制
| 参数 | 说明 | 默认值 |
|---|---|---|
| `master_date_from` | 主影像起始日期 (YYYYMMDD) | None(不限) |
| `master_date_to` | 主影像截止日期 (YYYYMMDD) | None(不限) |
| `slave_date_from` | 辅影像起始日期 (YYYYMMDD) | None(不限) |
| `slave_date_to` | 辅影像截止日期 (YYYYMMDD) | None(不限) |
### 3.2 池子定义规则
**主影像池**:
```sql
WHERE imaging_date >= master_date_from
AND imaging_date <= master_date_to
AND (其他约束: 卫星、轨道、模式、极化、精轨...)
```
**辅影像池**:
```sql
WHERE imaging_date >= slave_date_from
AND imaging_date <= slave_date_to
AND (其他约束: 卫星、轨道、模式、极化、精轨...)
```
### 3.3 向后兼容
当四个日期参数全为 `None` 时:
- 主辅池合并为同一个池子
- 行为等效于现有系统(`m.imaging_date <= s.imaging_date` 避免重复)
---
## 四、多卫星扩展设计
### 4.1 当前硬编码问题
现有代码中卫星相关硬编码:
```python
# spatial_service.py
if master.satellite != slave.satellite:
continue
# 前端显示
satellite: "LT-1"
```
### 4.2 扩展方案
**数据库层**:
- `radar_data.satellite` 字段已支持任意字符串,无需修改
- 配对函数增加 `p_allowed_satellites` 参数(数组)
**后端层**:
```python
class PairingRequest(BaseModel):
# 新增
allowed_satellites: Optional[List[str]] = None # ["LT-1", "S1A", "S1B"]
cross_satellite_pairing: bool = False # 是否允许跨卫星配对
```
**前端层**:
- 配对 Modal 增加卫星多选框
- 从数据库动态获取可用卫星列表:`SELECT DISTINCT satellite FROM radar_data`
**配对逻辑**:
```python
# 同卫星配对(默认)
if not cross_satellite_pairing:
if master.satellite != slave.satellite:
continue
# 跨卫星配对(高级功能,需谨慎)
if allowed_satellites:
if master.satellite not in allowed_satellites:
continue
if slave.satellite not in allowed_satellites:
continue
```
### 4.3 卫星特性差异处理
不同卫星需要不同的配对参数:
| 卫星 | 典型时间基线 | 典型空间基线 | 波长 | 备注 |
|---|---|---|---|---|
| LT-1 | 30~90 天 | < 3000 m | L 波段 | 当前系统 |
| Sentinel-1 | 6~12 天 | < 150 m | C 波段 | 高重访频率 |
| ALOS-2 | 14 天 | < 500 m | L 波段 | 与 LT-1 类似 |
| TerraSAR-X | 11 天 | < 200 m | X 波段 | 高分辨率 |
**建议**:
- 前端提供"卫星预设"按钮,自动填充推荐参数
- 数据库增加 `satellite_config` 表存储默认配对参数
---
## 五、数据库函数设计
### 5.1 新函数签名
```sql
CREATE OR REPLACE FUNCTION find_dinsar_pairs_v2(
-- 时空约束
p_time_baseline_min INTEGER,
p_time_baseline_max INTEGER,
p_spatial_baseline_max_meters NUMERIC,
p_overlap_threshold NUMERIC,
p_aoi_geom GEOMETRY DEFAULT NULL,
p_require_orbit_data BOOLEAN DEFAULT TRUE,
p_require_same_imaging_mode BOOLEAN DEFAULT TRUE,
p_require_same_polarization BOOLEAN DEFAULT TRUE,
p_aoi_overlap_threshold NUMERIC DEFAULT NULL,
-- 双池日期(新增)
p_master_date_from TEXT DEFAULT NULL,
p_master_date_to TEXT DEFAULT NULL,
p_slave_date_from TEXT DEFAULT NULL,
p_slave_date_to TEXT DEFAULT NULL,
-- 多卫星支持(新增)
p_allowed_satellites TEXT[] DEFAULT NULL,
p_cross_satellite_pairing BOOLEAN DEFAULT FALSE
)
RETURNS TABLE (
master_id INTEGER,
slave_id INTEGER,
master_imaging_date TEXT,
slave_imaging_date TEXT,
time_baseline_days INTEGER,
spatial_baseline_meters NUMERIC,
overlap_ratio NUMERIC
)
```
### 5.2 核心 SQL 逻辑变化
```sql
-- 现有逻辑
WHERE m.id < s.id -- 避免重复
AND m.satellite = s.satellite
-- 新逻辑
WHERE m.id <> s.id
AND m.imaging_date <= s.imaging_date -- 主影像早于辅影像
-- 主池日期约束
AND (p_master_date_from IS NULL OR m.imaging_date >= p_master_date_from)
AND (p_master_date_to IS NULL OR m.imaging_date <= p_master_date_to)
-- 辅池日期约束
AND (p_slave_date_from IS NULL OR s.imaging_date >= p_slave_date_from)
AND (p_slave_date_to IS NULL OR s.imaging_date <= p_slave_date_to)
-- 卫星约束
AND (p_allowed_satellites IS NULL OR m.satellite = ANY(p_allowed_satellites))
AND (p_allowed_satellites IS NULL OR s.satellite = ANY(p_allowed_satellites))
AND (p_cross_satellite_pairing OR m.satellite = s.satellite)
```
### 5.3 部署策略
1. 创建 `003_pairing_enhancement.sql` 迁移文件
2. `init_db.py` 自动应用(已有机制,line 354-356)
3. 保留 `find_dinsar_pairs` 旧函数(向后兼容)
4. 后端优先调用 `find_dinsar_pairs_v2`,失败时回退
---
## 六、后端 API 设计
### 6.1 Schema 变更
```python
# models/schemas.py
class PairingRequest(BaseModel):
"""D-InSAR 配对请求参数(增强版)"""
# === 时空约束(保留) ===
time_baseline_min: int = Field(default=1, ge=0, le=3650)
time_baseline_max: int = Field(default=90, ge=1, le=3650)
overlap_threshold: float = Field(default=0.5, ge=0.0, le=1.0)
spatial_baseline_max_meters: int = Field(default=3000, ge=0, le=100000)
coverage_diversity_penalty: float = Field(default=0.3, ge=0.0, le=1.0)
require_same_imaging_mode: bool = True
require_same_polarization: bool = True
aoi_overlap_threshold: Optional[float] = Field(default=None, ge=0.0, le=1.0)
# === 双池日期(新增) ===
master_date_from: Optional[str] = Field(default=None, pattern=r'^\d{8}$')
master_date_to: Optional[str] = Field(default=None, pattern=r'^\d{8}$')
slave_date_from: Optional[str] = Field(default=None, pattern=r'^\d{8}$')
slave_date_to: Optional[str] = Field(default=None, pattern=r'^\d{8}$')
# === 配对策略(新增) ===
strategy: str = Field(default="sbas", pattern=r'^(sbas|sequential|star)$')
num_connections: int = Field(default=1, ge=1, le=10)
reference_image_id: Optional[int] = None
# === 多卫星支持(新增) ===
allowed_satellites: Optional[List[str]] = None
cross_satellite_pairing: bool = False
# === 向后兼容(保留) ===
start_date: Optional[str] = Field(default=None, pattern=r'^\d{8}$')
@field_validator('master_date_to')
def validate_master_date_range(cls, v, info):
if v and info.data.get('master_date_from'):
if v < info.data['master_date_from']:
raise ValueError('master_date_to must >= master_date_from')
return v
@field_validator('slave_date_to')
def validate_slave_date_range(cls, v, info):
if v and info.data.get('slave_date_from'):
if v < info.data['slave_date_from']:
raise ValueError('slave_date_to must >= slave_date_from')
return v
```
### 6.2 服务层变更
```python
# services/spatial_service.py
async def find_dinsar_pairs(
self,
db: AsyncSession,
params: PairingRequest,
aoi_wkt: Optional[str] = None,
require_orbit_data: bool = True
) -> Tuple[List[RadarPair], List[str], bool]:
"""
增强版配对逻辑:
1. 调用 find_dinsar_pairs_v2 SQL 函数
2. 根据 strategy 后处理结果
3. 回退路径同步更新
"""
# 1. 调用数据库函数
pairs = await self._call_db_pairing_function(db, params, aoi_wkt, require_orbit_data)
# 2. 策略后处理
if params.strategy == "sbas":
# 覆盖优化(现有逻辑)
pairs = self._optimize_coverage_diversity(pairs, params.coverage_diversity_penalty)
elif params.strategy == "sequential":
# 顺序配对
pairs = self._apply_sequential_strategy(pairs, params.num_connections)
elif params.strategy == "star":
# 星型配对
pairs = self._apply_star_strategy(pairs, params.reference_image_id)
# 3. 生成 task_name 并去重
pairs = self._generate_task_names(pairs)
return pairs, warnings, fallback_used
```
---
## 七、前端 UI 设计
### 7.1 配对 Modal 布局
```
┌─────────────────────────────────────────────────┐
│ D-InSAR 配对参数 │
├─────────────────────────────────────────────────┤
│ 配对策略: ○ SBAS ○ Sequential ○ Star │
│ │
│ [策略说明] │
│ SBAS: 短基线子集,形成冗余网络,适合大范围监测 │
│ │
├─────────────────────────────────────────────────┤
│ 主影像时间范围 │
│ 从: [日期选择器 YYYYMMDD] 至: [日期选择器] │
│ │
│ 辅影像时间范围 │
│ 从: [日期选择器 YYYYMMDD] 至: [日期选择器] │
│ │
│ ☑ 使用双池模式(不勾选则主辅池合并) │
├─────────────────────────────────────────────────┤
│ 时间基线: [1] ~ [90] 天 │
│ 空间基线上限: [3000] 米 │
│ 重叠率阈值: [0.5] │
│ 覆盖多样性惩罚: [0.3] │
│ │
│ ☑ 成像模式一致 ☑ 极化一致 ☑ 仅精轨影像 │
├─────────────────────────────────────────────────┤
│ AOI 来源: ○ 上传 SHP ○ 行政区选择 │
│ [AOI 选择区域...] │
│ │
│ AOI 覆盖率阈值: [0] (0 表示不限制) │
├─────────────────────────────────────────────────┤
│ 高级选项 [展开 ▼] │
│ 卫星选择: ☑ LT-1 ☐ Sentinel-1 ☐ ALOS-2 │
│ ☐ 允许跨卫星配对(谨慎使用) │
│ │
│ Sequential 参数: │
│ 每景配对数: [1] │
│ │
│ Star 参数: │
│ 参考影像: [自动选择 ▼] 或 手动指定 ID: [___] │
├─────────────────────────────────────────────────┤
│ [取消] [开始配对] │
└─────────────────────────────────────────────────┘
```
### 7.2 策略切换交互
- 选择 SBAS:显示"覆盖多样性惩罚"参数
- 选择 Sequential:显示"每景配对数"参数
- 选择 Star:显示"参考影像选择"下拉框(从已有影像列表动态加载)
### 7.3 卫星选择
- 从 `GET /api/radar-data/available-satellites` 动态获取
- 默认只勾选 LT-1
- 跨卫星配对显示警告提示
---
## 八、实施计划
### Phase 1: 数据库层(1 天)
- [ ] 创建 `003_pairing_enhancement.sql`
- [ ] 实现 `find_dinsar_pairs_v2` 函数
- [ ] 更新 `init_db.py` 应用迁移
- [ ] 测试 SQL 函数(手动执行验证)
### Phase 2: 后端层(2 天)
- [ ] 更新 `PairingRequest` Schema
- [ ] 实现策略后处理逻辑(Sequential / Star)
- [ ] 更新 `spatial_service.py` 调用新函数
- [ ] 更新回退路径逻辑
- [ ] 新增 `GET /api/radar-data/available-satellites` 端点
- [ ] 单元测试(pytest)
### Phase 3: 前端层(2 天)
- [ ] 更新 `pairingStore.js` 增加新字段
- [ ] 重构 `PairingModal.jsx` UI
- [ ] 实现策略切换交互
- [ ] 实现卫星多选
- [ ] 更新 `usePairingLogic.js` 传参
- [ ] 前端构建测试
### Phase 4: 集成测试(1 天)
- [ ] 端到端测试(SBAS / Sequential / Star)
- [ ] 双池配对测试
- [ ] 多卫星配对测试
- [ ] 向后兼容测试(旧参数仍可用)
- [ ] 性能测试(大数据量)
### Phase 5: 文档与部署(0.5 天)
- [ ] 更新 `CLAUDE.md` 记录改动
- [ ] 更新用户手册(如有)
- [ ] 部署到测试环境
- [ ] 用户验收测试
**总计**: 约 6.5 天
---
## 九、风险与缓解
| 风险 | 影响 | 缓解措施 |
|---|---|---|
| SQL 函数性能下降 | 配对速度变慢 | 保留旧函数,性能对比测试 |
| 前端 UI 复杂度增加 | 用户学习成本 | 提供"快速模式"和"高级模式"切换 |
| 多卫星配对参数不当 | 生成无效配对 | 前端参数校验 + 后端警告提示 |
| 向后兼容性破坏 | 旧配对失效 | 保留 `start_date` 参数,自动转换 |
---
## 十、未来扩展
1. **基线网络可视化**:时间-空间基线散点图(D3.js / ECharts)
2. **配对质量评分**:根据相干性、大气条件预估配对质量
3. **自动参数推荐**:基于历史配对结果的机器学习推荐
4. **批量配对模板**:保存常用配对参数为模板
5. **Sentinel-1 轨道框架**:支持 Sentinel-1 的 Track/Frame 概念
---
## 附录 A:术语对照表
| 中文 | 英文 | 说明 |
|---|---|---|
| 主影像 | Master / Reference | 配对中的参考影像 |
| 辅影像 | Slave / Secondary | 配对中的从属影像 |
| 时间基线 | Temporal Baseline | 两景影像的时间间隔 |
| 空间基线 | Spatial Baseline | 两景影像的空间距离 |
| 短基线子集 | SBAS (Small Baseline Subset) | 配对策略之一 |
| 星型配对 | Star Graph | 单主影像配对策略 |
| 顺序配对 | Sequential Pairing | 时间顺序配对策略 |
---
**文档结束**
@@ -0,0 +1,867 @@
# InSAR 配对系统重构方案
更新日期:2026-04-13
状态:待实施
适用范围:`Z:\Code\Insar_management_system_v2` 当前代码基线
## 1. 文档目标
这份文档不是抽象算法说明,而是基于当前系统现状给出的可落地重构方案,目标是:
- 重构 D-InSAR 配对内核,解决当前正确性、稳定性和可维护性问题
- 尽量复用现有数据库自动维护、root/cursor 自维护、manifest inventory、自检面板
- 明确开发机条件下的迁移策略
- 为后续实现提供分阶段执行顺序、表设计、接口边界和验收口径
## 2. 当前系统基础与约束
### 2.1 当前可直接复用的系统能力
当前系统已经具备以下基础设施,重构应直接接入,而不是另起一套平行机制:
- 数据库启动自维护
- `backend/app/db_maintenance.py`
- `ensure_database_ready()` 会在启动时创建表、补缺列、应用 SQL migration,并可在开发机上按环境变量执行整库 schema reset
- root 与 cursor 自维护
- `backend/app/services/root_registry_service.py`
- 启动时会根据 `.env` 自动同步 `managed_roots` 与 `scan_cursors`
- manifest inventory 自维护
- `backend/app/services/manifest_inventory_service.py`
- 会维护 `path_inventory`、`scan_cursors.last_seen_*`、fingerprint 与 root 缺失状态
- 结果目录自维护
- `backend/app/services/dinsar_scan_service.py`
- D-InSAR 统一扫描链路已经是“发布 -> manifest inventory -> catalog rebuild -> compat sync”
- 健康检查和一致性检查
- `backend/app/services/health_service.py`
- `frontend/src/HealthCheckPanel.jsx`
- 当前自检已包含:数据库 schema、worker、catalog、source roots、D-InSAR bridge、一致性统计、系统维护入口
### 2.2 当前配对系统的核心问题
现有配对系统的主要问题不是功能不够多,而是核心语义不稳定:
- SQL 路径和 Python fallback 路径不是同一算法
- 同日影像会出现反向重复配对
- `start_date` 在 SQL 主路径上已失效,但接口仍保留
- `star` 策略没有固定参考像角色
- `sequential` 对同日多景不稳定
- `all` 策略排序与前端描述不一致
- `aoi_overlap_threshold=0` 的语义前后端不一致
- 当前任务命名与配对唯一性混在一起,业务主键不稳定
### 2.3 当前系统约束
- 当前扫描模式是 manual-only,调度器默认关闭
- `backend/app/scheduler.py`
- `radar_data` 是当前源影像事实表,配对系统必须以它为唯一源
- `dinsar_task_batches` / `dinsar_task_items` 已经是生产任务落地表
- `result_products` / `dinsar_product_profiles` 已经承担结果资产追踪
- 健康检查和统计接口已上线,新增配对系统状态应并入现有面板
## 3. 重构总目标
本轮重构的目标不是继续修补 `find_dinsar_pairs_v2`,而是将配对系统改造成四层结构:
1. 影像事实层
2. 配对指标缓存层
3. 网络选择层
4. 任务物化层
核心原则:
- 候选对生成与网络选择分层
- 所有策略共享同一候选池
- 所有配对方向统一规范化
- 所有结果可追溯到策略版本、参数快照和候选边来源
- 不再保留“算法不等价的 fallback”
## 4. 目标架构
### 4.1 Layer A:影像事实层
沿用现有 `radar_data` 表作为唯一源影像事实表。
重构要求:
- `radar_data.id` 继续作为数据库主键
- 引入明确的场景业务键 `scene_uid`
- 优先复用 `radar_data.unique_id`
- 若历史数据存在 `unique_id` 缺失或不稳定,启动时执行一次回填
- 回填规则:`hash(normalized file_path)`,并写入新列或标准化字段
- 所有配对相关对象都基于 `scene_uid` 或 `radar_data.id` 建立引用
建议:
- 本轮实现阶段直接在 `radar_data` 上新增:
- `scene_uid`
- `scene_signature`
- `pairing_eligible`
- `pairing_updated_at`
- 如果希望减少变更面,也可以只新增 `scene_uid` 与 `pairing_updated_at`
### 4.2 Layer B:配对指标缓存层
这是本次重构的核心。
目标:
- 将“任意两个影像是否可配、其基础指标是什么”从请求时临时计算,改为可维护的持久化缓存
- 缓存只保存与 AOI 无关的基础指标
- AOI 相关指标在请求时动态叠加计算
建议新增表:
#### `pairing_metric_cache`
一行代表一个规范化后的影像对。
建议字段:
- `id`
- `master_scene_ref_id`
- `slave_scene_ref_id`
- `master_scene_uid`
- `slave_scene_uid`
- `pair_uid`
- `metric_version`
- `orientation_rule_version`
- `time_baseline_days`
- `spatial_baseline_meters`
- `scene_overlap_ratio`
- `orbit_direction`
- `same_satellite`
- `same_imaging_mode`
- `same_polarization`
- `master_imaging_date`
- `slave_imaging_date`
- `master_satellite`
- `slave_satellite`
- `master_imaging_mode`
- `slave_imaging_mode`
- `master_polarization`
- `slave_polarization`
- `master_file_path`
- `slave_file_path`
- `status`
- `computed_at`
唯一约束:
- `unique(master_scene_ref_id, slave_scene_ref_id, metric_version)`
关键约定:
- `master/slave` 不是请求期可变概念,而是规范化方向
- 规范化规则:
- 早时间 = master
- 同时间时,较小 `scene_uid` 或较小 `id` = master
### 4.3 Layer C:缓存状态与脏队列
为了接入现有“自动维护 + 健康检查”体系,建议新增两张状态表。
#### `pairing_cache_state`
建议设计为单例表或按 `cache_scope='global'` 管理。
字段建议:
- `id`
- `cache_scope`
- `metric_version`
- `status`
- `scene_count`
- `pair_count`
- `dirty_scene_count`
- `last_full_rebuild_at`
- `last_incremental_reconcile_at`
- `last_error`
- `updated_at`
状态建议:
- `READY`
- `DIRTY`
- `REBUILDING`
- `DEGRADED`
- `FAILED`
#### `pairing_dirty_scenes`
用于增量维护。
字段建议:
- `id`
- `scene_ref_id`
- `scene_uid`
- `reason`
- `marked_at`
- `resolved_at`
- `status`
唯一约束建议:
- `unique(scene_ref_id, status='PENDING')` 或代码层去重
### 4.4 Layer D:网络选择层
网络选择层不直接查 `radar_data`,而是查 `pairing_metric_cache`。
建议新增:
#### `pairing_network_runs`
代表一次配对请求或一次已保存的网络。
字段建议:
- `id`
- `network_run_id`
- `strategy`
- `policy_version`
- `request_hash`
- `request_params_json`
- `aoi_source`
- `aoi_hash`
- `aoi_summary_json`
- `candidate_count`
- `selected_edge_count`
- `warning_count`
- `status`
- `fallback_used`
- `created_by`
- `created_at`
#### `pairing_network_edges`
代表某个网络 run 选中的边。
字段建议:
- `id`
- `network_run_ref_id`
- `metric_cache_ref_id`
- `edge_rank`
- `selection_reason`
- `selection_score`
- `selection_meta_json`
- `is_reference_edge`
- `created_at`
唯一约束:
- `unique(network_run_ref_id, metric_cache_ref_id)`
### 4.5 Layer E:任务物化层
当前 `dinsar_task_batches` / `dinsar_task_items` 可继续保留。
但建议增加可追溯字段:
在 `dinsar_task_items` 上新增:
- `network_run_id`
- `network_edge_id`
- `policy_version`
- `selection_strategy`
- `scene_pair_uid`
在结果侧建议同步补充:
- `result_products.pair_key` 继续保留
- 在 `dinsar_product_profiles` 或 `params_json` 中补充:
- `network_run_id`
- `network_edge_id`
- `policy_version`
- `master_scene_uid`
- `slave_scene_uid`
这样最终可以把:
- 配对网络
- 任务项
- 生产结果
串成一条可追踪链路。
## 5. 配对算法设计
### 5.1 候选对生成
候选对生成只负责回答:
- 这两个场景在基础事实上是否可配
- 基础指标是什么
不负责回答:
- 用什么策略选
- 最后是否进入任务
基础硬约束:
- `master != slave`
- 方向规范化后只保留一个方向
- 同轨向
- 日期格式合法
- 几何相交
基础指标:
- `time_baseline_days`
- `spatial_baseline_meters`
- `scene_overlap_ratio`
注意:
- `same_satellite`
- `same_imaging_mode`
- `same_polarization`
只作为缓存事实,不在缓存层提前删边,以便不同请求复用同一缓存。
### 5.2 请求期过滤
请求期过滤发生在 `pairing_metric_cache` 之上:
- `time_baseline_min/max`
- `spatial_baseline_max_meters`
- `overlap_threshold`
- `allowed_satellites`
- `cross_satellite_pairing`
- `require_same_imaging_mode`
- `require_same_polarization`
- `master_date_from/to`
- `slave_date_from/to`
- `aoi_overlap_threshold`
AOI 处理原则:
- AOI 不进入持久化 pair cache 主键
- AOI 相关 overlap 在请求时动态计算
- `0` 不再表示数值阈值,而统一转成 `null`
- 前后端统一语义:
- `null` = 不启用 AOI 覆盖阈值
- `0 < x <= 1` = 启用阈值
### 5.3 四种策略的正式定义
#### `all`
- 返回全部合法候选边
- 排序统一为稳定顺序:
- `master_imaging_date`
- `slave_imaging_date`
- `selection_score desc`
- `pair_uid`
不再使用“数据库返回顺序”作为结果语义。
#### `sequential`
- 基于稳定时间序列排序构图
- 排序键:
- `acquisition_time_utc`
- 无则 `imaging_date`
- 再无则 `scene_uid`
- 每景连接后续 `N` 景
- 同日多景必须稳定,不允许受候选池原始顺序影响
#### `star`
- 参考像必须固定角色
- 本系统建议统一采用:
- 参考像始终为 `master`
- 若物理上参考像日期晚于目标像,则在选择层进行拒绝或显式反转并记录相位方向规则
本轮建议:
- 为降低复杂度,先采用“参考像必须是 master”的严格规则
- 如果用户选择的参考像晚于部分场景,则这些边不入网,并给出 warning
#### `sbas`
本轮不采用“简单按 overlap 贪心去重”的旧实现。
建议目标:
- 图连通
- 每景至少达到最小连接数
- 时间覆盖尽量连续
- 每景度数受控,避免过密
- AOI 覆盖尽量多样
可落地实现:
1. 从合法候选边中先构造基础时间邻接骨架
2. 保证每个场景至少连接前后最近邻
3. 再按综合评分补边
4. 每补一条边,对已覆盖区域施加惩罚
5. 达到目标连通性和最大边数后停止
综合评分建议:
`selection_score = w_time * time_score + w_spatial * spatial_score + w_overlap * overlap_score + w_aoi * aoi_gain - w_redundancy * redundancy_penalty`
### 5.4 删除旧 fallback 的原则
本轮不再保留“Python 语义独立 fallback”。
替代方案:
- 候选边缓存是主路径
- 如果缓存不存在或脏数据过多:
- 返回明确的 `DEGRADED` / `DIRTY` 状态
- 允许管理员触发重建
- 或在安全阈值内自动执行增量重建
也就是说:
- 可以有“降级状态”
- 不能再有“悄悄换算法”
## 6. 与现有扫描和自动维护机制的集成
### 6.1 源影像扫描后的集成点
当前源影像扫描入口:
- `backend/app/services/data_service.py::scan_radar_data`
- `backend/app/scheduler.py::scan_data_job`
建议改造:
在 `scan_radar_data()` 中,对新增、更新、删除的 `radar_data` 记录执行:
1. 标记 `pairing_dirty_scenes`
2. 更新 `pairing_cache_state.status = DIRTY`
3. 根据变更规模决定是否立即增量重建
建议阈值:
- 变更场景数 `<= SMALL_RECONCILE_LIMIT`
- 扫描结束后直接执行增量 reconcile
- 变更场景数 `> SMALL_RECONCILE_LIMIT`
- 仅标脏,不在扫描链路内同步重建
- 由管理员在自检面板手动触发
这与当前 manual-only 模式兼容,不依赖后台常驻调度器。
### 6.2 启动期自维护
当前启动期会执行:
- `ensure_database_ready()`
- `root_registry_service.sync_from_settings()`
- `manifest_inventory_service.sync_manifest_roots()`
- catalog bootstrap
- health check
建议新增启动动作:
- `pairing_service.bootstrap_pairing_cache_state()`
只做轻量检查:
- 新表是否存在
- 是否有缓存状态行
- `metric_version` 是否匹配当前版本
- dirty scene 是否堆积
不建议启动即全量重建,避免启动时间失控。
### 6.3 数据库自动维护策略
当前数据库自维护具备两个模式:
- 常规模式:建表、补列、应用 migration
- 开发机强制 reset 模式:整库 schema reset
由于本轮是配对内核重构,且当前是开发机,推荐采用:
#### 推荐落地策略
第一轮实施时直接执行一次开发机 schema reset。
原因:
- 旧配对 SQL 函数与新缓存设计语义完全不同
- 旧任务表中的配对数据没有保留价值
- 旧结果可以通过 manifest inventory 和结果 catalog 重建
- 源影像可以重新扫描
具体方式:
- 合并新 ORM 与 migration 后
- 启动前临时设置:
- `DB_SCHEMA_RESET_ON_MISMATCH=true`
- `DB_SCHEMA_RESET_CONFIRM=true`
- 启动一次后让 `ensure_database_ready()` 重建 schema
- 启动完成后恢复这两个环境变量为 `false`
### 6.4 migration 文件策略
当前 `db_maintenance.py` 中 migration 顺序为:
- `001_st_intersection_agg.sql`
- `002_spatial_functions.sql`
- `003_pairing_enhancement.sql`
建议新增:
- `004_pairing_refactor.sql`
该 migration 负责:
- 创建新 pairing 表
- 创建必要索引
- 创建候选边计算函数或 SQL helper view
- 如有需要,保留旧函数为 deprecated wrapper
建议:
- 不要在 `004` 里继续强化 `find_dinsar_pairs_v2`
- 将旧函数标记 deprecated,仅作为过渡壳
- 新逻辑直接走新 service + 新表
## 7. 自检与一致性检测升级
### 7.1 Health 接口扩展
当前健康检查聚合点:
- `backend/app/services/health_service.py::get_health_status`
建议新增检查项:
#### `pairing_system`
字段建议:
- `ok`
- `status`
- `metric_version`
- `scene_count`
- `pair_count`
- `dirty_scene_count`
- `network_run_count_last_7d`
- `last_full_rebuild_at`
- `last_incremental_reconcile_at`
- `last_error`
- `duplicate_reverse_pair_count`
- `orphan_metric_scene_ref_count`
- `task_without_network_ref_count`
- `result_without_pair_trace_count`
判定规则建议:
- `READY` 且无关键异常 -> `ok=true`
- `DIRTY` 但 dirty 数量较少 -> `ok=false`,级别 `warn`
- 存在重复反向边、孤儿引用、版本漂移 -> `ok=false`,级别 `error`
### 7.2 Health 面板扩展
当前 `HealthCheckPanel.jsx` 已能展示结构化卡片。
建议新增卡片:
- `配对系统`
展示内容:
- 缓存状态
- 指标版本
- 场景数 / 缓存对数
- dirty scene 数
- 最近全量 / 增量时间
- 关键错误数
操作按钮建议:
- `增量修复配对缓存`
- `全量重建配对缓存`
- `清理旧网络记录`
### 7.3 Statistics 接口扩展
当前 `/statistics` 已承担一致性统计角色。
建议追加:
- `pairing_consistency`
字段建议:
- `metric_cache_count`
- `duplicate_reverse_pair_count`
- `invalid_orientation_count`
- `network_edge_orphan_count`
- `task_orphan_count`
- `result_trace_missing_count`
## 8. API 重构计划
### 8.1 后端接口
当前接口:
- `POST /api/find-pairs`
建议分两阶段改造。
#### Phase 1:保持前端可用
保留:
- `POST /api/find-pairs`
但内部改为:
- 从 `pairing_metric_cache` 查询
- 选择网络
- 返回 `network_run_id + pairs`
#### Phase 2:正式 API
新增:
- `POST /api/pairing/preview`
- `POST /api/pairing/networks/{network_run_id}/materialize-batch`
- `POST /api/pairing/reconcile-dirty`
- `POST /api/pairing/rebuild-cache`
- `GET /api/pairing/health`
- `GET /api/pairing/networks/{network_run_id}`
### 8.2 请求模型
`PairingRequest` 应清理为正式语义:
- 删除 `start_date`
- `aoi_overlap_threshold` 前端空值统一传 `null`
- `strategy` 继续保留:
- `all`
- `sequential`
- `star`
- `sbas`
- `reference_image_id` 仅在 `star` 时有效
### 8.3 返回模型
建议新增返回字段:
- `network_run_id`
- `policy_version`
- `candidate_count`
- `selected_edge_count`
- `degraded`
- `warnings`
## 9. 前端改造计划
涉及文件:
- `frontend/src/components/PairingModal.jsx`
- `frontend/src/hooks/usePairingLogic.js`
- `frontend/src/store/pairingStore.js`
改造原则:
- 前端不再承载配对语义补丁
- 所有“0 表示不限”“默认排序说明”等语义统一交回后端协议
- 结果列表显示:
- `network_run_id`
- `strategy`
- `policy_version`
- warning
界面调整建议:
- `all` 策略文案改为“全部合法候选边,按稳定顺序展示”
- `star` 明确说明“参考像固定为主像”
- `sequential` 明确说明“同日多景按稳定键排序”
- AOI 覆盖阈值为空时传 `null`
## 10. 旧代码处理策略
### 10.1 可以删除的内容
在新缓存路径稳定后,可以删除:
- `spatial_service.find_dinsar_pairs()` 中当前 Python fallback
- 旧 `_apply_star_strategy()` 非固定角色实现
- 旧 `start_date` 兼容逻辑
### 10.2 需要保留过渡一段时间的内容
- 旧前端 `find-pairs` 调用入口
- 旧 `task_alias / pair_key` 命名辅助
但注意:
- 命名只能作为展示与兼容字段
- 不能继续作为算法主键
## 11. 实施阶段划分
### Phase 0:开发机切换准备
- 确认当前开发机允许 DB reset
- 记录 `.env` 中路径配置
- 保留现有 `managed_roots` / `path_inventory` / manifest publish 目录,不做文件层清理
- 准备新 migration 与 ORM
验收:
- 可以安全启动一次 schema reset
### Phase 1:数据模型落地
- 新增 pairing 相关 ORM
- 新增 `004_pairing_refactor.sql`
- 接入 `ensure_database_ready()`
- 建立 `pairing_cache_state` 初始化逻辑
验收:
- 后端启动后新表自动创建
- 健康检查不报 schema 错
### Phase 2:候选缓存引擎
- 实现 `pairing_metric_cache` 全量构建
- 实现 `pairing_dirty_scenes`
- 在 `scan_radar_data()` 中接入 dirty 标记
验收:
- 能从 `radar_data` 构建稳定 pair cache
- 同日反向重复为 0
### Phase 3:网络选择器
- 重写 `all / sequential / star / sbas`
- 删除算法不等价 fallback
- 为 `find-pairs` 接口切换到新内核
验收:
- 同一输入重复请求结果稳定
- `star` 固定参考像角色
- `sequential` 同日多景稳定
### Phase 4:任务物化与结果追踪
- `dinsar_task_items` 增加 network trace 字段
- 结果 manifest / product profile 补充配对追踪字段
验收:
- 任一结果可追溯到 network edge
### Phase 5:自维护与自检
- 健康检查新增 `pairing_system`
- Health 面板新增配对卡片与修复按钮
- Statistics 新增 pairing consistency
验收:
- 能显示 dirty scene 数、缓存状态、重复边异常
- 能从 UI 触发增量修复与全量重建
### Phase 6:清理旧逻辑
- 删除旧 fallback
- 删除 `start_date`
- 下线旧语义说明
验收:
- 配对链路只剩单一语义实现
## 12. 测试矩阵
本轮至少补以下自动化测试:
### 12.1 候选缓存层
- 同日双景只生成单方向边
- 同日多景生成顺序稳定
- 不同轨向不生成边
- 时间基线越界不入缓存
### 12.2 请求过滤层
- `allowed_satellites` 生效
- `cross_satellite_pairing=false` 生效
- `require_same_imaging_mode/polarization` 生效
- `aoi_overlap_threshold=null` 与 `0.2` 行为不同
### 12.3 策略层
- `all` 顺序稳定
- `sequential` 连接数正确
- `star` 参考像固定为 master
- `sbas` 至少满足基础连通性
### 12.4 集成层
- 扫描新增源影像后 dirty scene 正确增加
- 执行增量 reconcile 后 dirty scene 清零
- `find-pairs` 返回 `network_run_id`
- `materialize-batch` 后任务项带 network trace
### 12.5 自检层
- 重复边异常能被 health 检出
- 孤儿网络边能被 health 检出
- 任务无 network trace 能被 statistics 检出
## 13. 风险与取舍
### 13.1 本轮明确接受的取舍
- 开发机允许整库 reset
- 不迁移旧配对记录
- 不保留旧 fallback
### 13.2 需要重点控制的风险
- 首轮全量构建 pair cache 可能较慢
- `radar_data.unique_id` 可能不稳定,需要一次性规范
- 旧结果如果缺少 trace 字段,需要通过 manifest/profile 尽可能补齐
- 若前端仍传 `0`,后端必须做强制归一化,不能再依赖 UI 自觉
## 14. 推荐实施顺序
建议严格按下面顺序推进:
1. 先落表和缓存状态,不先改前端
2. 再实现候选缓存和增量脏标记
3. 再重写四种策略
4. 再接任务物化追踪
5. 最后接健康检查与自修复入口
不要反过来做:
- 不要先改 UI 文案
- 不要先修补 `find_dinsar_pairs_v2`
- 不要保留旧 fallback 到最后
## 15. 本轮结论
在当前代码基线上,最合理的重构路线不是继续维护“实时 SQL + Python fallback”模式,而是:
- 用 `radar_data` 作为唯一事实层
- 用 `pairing_metric_cache` 承载稳定候选对
- 用 `pairing_network_runs / edges` 承载策略结果
- 用 `pairing_cache_state / dirty_scenes` 承接自维护
- 用现有 `HealthCheckPanel` 和 `/statistics` 承接自检
这样做的直接收益是:
- 正确性可锁定
- 结果可追溯
- 与现有 DB 自维护、扫描链路、自检体系自然对接
- 后续再接 SBAS、跨卫星和多引擎生产时不会继续堆积配对债务
+149
View File
@@ -0,0 +1,149 @@
# PROJ 数据库配置指南
## 问题背景
InSAR 管理系统使用地理空间库(GDAL、pyproj)进行坐标转换和空间查询。这些库依赖 PROJ 数据库来执行坐标系转换。
**版本冲突问题**:
- PostgreSQL PostGIS 扩展自带 PROJ 数据库 **v2**(旧版本)
- GDAL/pyproj 期望 PROJ 数据库 **v3+**(新版本)
- 版本不匹配会导致警告,甚至**坐标转换错误**
## 配置方式
### 方式 1:自动配置(推荐)
**不需要任何配置**,系统会自动使用 pyproj 的 PROJ 数据库。
启动后端时,会看到:
```
[*] Configuring PROJ database...
[*] Auto-detected PROJ_LIB: C:\Users\Administrator\.conda\envs\InSAR\lib\site-packages\pyproj\proj_dir\share\proj
[*] PROJ version: 9.2.0
```
### 方式 2:手动配置(高级)
如果需要使用自定义的 PROJ 数据库,可以在 `.env` 文件中配置:
```bash
# PROJ 数据库路径
PROJ_LIB=C:\path\to\your\proj\data
```
**配置优先级**:
1. `.env` 中的 `PROJ_LIB`(如果设置)
2. pyproj 自动检测(默认)
## 验证配置
### 方法 1:查看启动日志
启动后端时,检查日志输出:
```bash
# 正确配置(使用 pyproj)
[*] Auto-detected PROJ_LIB: ...pyproj\proj_dir\share\proj
[*] PROJ version: 9.2.0
# 错误配置(使用 PostgreSQL)
[*] Using PROJ_LIB from .env: C:\Program Files\PostgreSQL\17\share\contrib\postgis-3.6\proj
```
### 方法 2:运行诊断脚本
```bash
# 激活 InSAR 环境
conda activate InSAR
# 运行诊断
python scripts/check_proj.py
```
诊断脚本会检查:
- 环境变量 `PROJ_LIB` 的值
- PROJ 数据库文件是否存在
- PROJ 版本信息
- 坐标转换功能是否正常
### 方法 3:测试坐标转换
```python
import pyproj
# WGS84 to Web Mercator
transformer = pyproj.Transformer.from_crs("EPSG:4326", "EPSG:3857", always_xy=True)
x, y = transformer.transform(116.4, 39.9) # 北京坐标
print(f"WGS84 (116.4, 39.9) -> Web Mercator ({x:.2f}, {y:.2f})")
# 预期输出: WGS84 (116.4, 39.9) -> Web Mercator (12957588.73, 4851421.18)
```
## 常见问题
### Q1: 为什么不能使用 PostgreSQL 的 PROJ 数据库?
**A**: PostgreSQL PostGIS 自带的 PROJ 数据库是 v2 版本,而 GDAL/pyproj 期望 v3+ 版本。使用旧版本可能导致:
- 坐标转换精度下降
- 某些坐标系不支持
- 空间查询结果错误
### Q2: 如何知道当前使用的是哪个 PROJ 数据库?
**A**: 查看后端启动日志,或运行 `python scripts/check_proj.py`。
### Q3: 删除数据库重建后,PROJ 警告又出现了怎么办?
**A**: 这是正常的。删除数据库后,PostGIS 扩展会重新安装,系统环境变量可能被重置。重启后端即可自动修复。
### Q4: 我想使用自定义的 PROJ 数据库,如何配置?
**A**: 在 `.env` 文件中设置 `PROJ_LIB` 环境变量:
```bash
PROJ_LIB=D:\custom\proj\data
```
确保该目录包含 `proj.db` 文件。
### Q5: 如何确认 PROJ 配置生效?
**A**: 执行雷达数据扫描操作,检查日志中是否还有 PROJ 警告:
```
GDAL signalled an error: err_no=1, msg='PROJ: proj_identify: ... DATABASE.LAYOUT.VERSION.MINOR = 2 ...'
```
如果没有此警告,说明配置生效。
## 影响范围
正确配置 PROJ 数据库对以下功能至关重要:
- ✅ **雷达数据扫描**:解析影像坐标系
- ✅ **空间查询**:隐患点与影像范围匹配
- ✅ **D-InSAR 处理**:地理配准和坐标转换
- ✅ **水体监测**:地理编码
- ✅ **地图显示**:坐标系转换(WGS84 ↔ Web Mercator)
## 技术细节
### PROJ 数据库版本对比
| 来源 | 版本 | 路径 | 推荐 |
|------|------|------|------|
| PostgreSQL PostGIS | v2 | `C:\Program Files\PostgreSQL\17\share\contrib\postgis-3.6\proj` | ❌ |
| pyproj (conda) | v9.2 | `C:\Users\...\envs\InSAR\lib\site-packages\pyproj\proj_dir\share\proj` | ✅ |
### 环境变量优先级
1. 程序内设置的 `os.environ["PROJ_LIB"]`(最高优先级)
2. 系统环境变量 `PROJ_LIB`
3. pyproj 默认路径
我们的配置策略是在程序启动时**强制覆盖**环境变量,确保使用正确的 PROJ 数据库。
## 参考资料
- [PROJ 官方文档](https://proj.org/)
- [pyproj 文档](https://pyproj4.github.io/pyproj/)
- [PostGIS 文档](https://postgis.net/)
+232
View File
@@ -0,0 +1,232 @@
# SBAS 前端设计说明
更新日期:2026-04-06
## 1. 设计目标
SBAS 前端必须解决四个问题:
- 让用户知道“匹配结果”和“正式生产运行”不是一回事
- 让用户能从已有 PS 批次发起正式运行
- 让用户能查看 `psinsar` 成果,而不是只看实验目录
- 让用户能逐步过渡到更复杂的时序成果浏览,而不是一次性堆满功能
## 2. 导航结构
当前建议保留现有导航层级,只把占位页替换成真实页面。
### 2.1 生产规划组
- `ps_results`
- 匹配结果浏览与批次创建
- `ps_production`
- 正式生产入口
- `ps_products`
- 成果编目与产品管理
### 2.2 InSAR 变形分析组
- `psinsar_results`
- 以成果浏览为主的只读结果页
- `psinsar_analysis`
- 后续时序分析页,当前可继续保留占位
## 3. 页面一:`ps_production`
## 3.1 页面目标
从已有 `ps_task_batches` 中选择一个 stack,提交一次正式 SBAS 运行,并查看最近运行状态。
## 3.2 页面布局
建议分成三块:
- 运行说明卡
- 新建运行表单
- 最近运行列表
## 3.3 新建运行表单字段
必填字段:
- `batch_id`
可选字段:
- `run_name`
- `reference_date`
- `water_mask_mode`
- `notes`
只读展示字段:
- `direction`
- `stack_size`
- `with_orbit_count`
- `missing_orbit_count`
## 3.4 提交后反馈
提交后需要立刻给出:
- `task_id`
- `run_id`
- 当前状态
当前 Phase 1 仅执行 prepare,因此状态建议为:
- `PENDING`
- `RUNNING`
- `PREPARED`
- `FAILED`
## 3.5 最近运行列表
每个运行卡片至少展示:
- `run_name`
- `run_id`
- `batch_id`
- `reference_date`
- `stack_size`
- `status`
- `created_at`
详情展开建议展示:
- DEM 路径
- 轨道池路径
- 生成的 stack manifest 路径
- 输入日期列表
- 质量/预检查摘要
## 4. 页面二:`ps_products`
## 4.1 页面目标
承接管理员视角的产物目录维护。
能力包括:
- 查看 `psinsar` catalog 状态
- 手动重建 catalog
- 浏览产品列表
- 查看产品详情
## 4.2 页面布局
建议分成三块:
- catalog 状态卡
- 管理操作区
- 产品列表与详情区
## 4.3 catalog 状态卡
展示字段:
- `catalog_name`
- `storage_root`
- `status`
- `needs_rebuild`
- `manifest_count`
- `db_count`
- `issue_count`
- `last_full_rebuild_at`
## 4.4 产品列表
每个产品卡片建议展示:
- `display_name`
- `product_id`
- `run_key`
- `reference_date`
- `stack_size`
- `status`
- `published_at`
优先展示缩略图:
- `preview/velocity_preview.png`
## 4.5 产品详情
详情建议包含:
- 基本信息
- 时序信息
- 质量摘要
- 资产列表
- 问题列表
资产列表首屏优先顺序:
- `velocity_geotiff`
- `timeseries_cube`
- `temporal_coherence_geotiff`
- `quality_mask_geotiff`
- `preview_png`
- 其他辅助资产
## 5. 页面三:`psinsar_results`
## 5.1 页面目标
承接业务用户视角的成果浏览,不承担目录维护操作。
## 5.2 与 `ps_products` 的区别
- `ps_products` 偏运维和管理
- `psinsar_results` 偏展示和检索
## 5.3 当前 Phase 1 能力
- 浏览产品列表
- 查看缩略图
- 查看资产清单
- 查看质量摘要
## 5.4 后续 Phase 2/3 扩展
- 地图叠加 `velocity.tif`
- 按 AOI 或时间筛选
- 像元点位时序查询
- 专题统计与热点识别
## 6. 交互原则
- 生产和结果页面必须分开
- 管理操作只出现在管理页
- 结果页优先展示 `velocity.tif` 对应预览
- 但详情页必须强调主科学产物是 `geo_timeseries.h5`
## 7. 前端阶段性实施建议
### Phase 1
- `ps_production`:真实可提交 prepare 任务
- `ps_products`:真实 catalog 管理页
- `psinsar_results`:复用 catalog 浏览能力做只读页
### Phase 2
- 地图叠加 `velocity.tif`
- 结果筛选
- 与 run 详情联动
### Phase 3
- `geo_timeseries.h5` 点查询
- 时间曲线图
- 多产品对比
## 8. 当前结论
前端第一阶段不应追求“一口气把时序分析全做完”,而应优先做三件事:
- 让 SBAS 生产有正式入口
- 让 SBAS 结果有正式目录
- 让用户在系统里能看到真正的 `psinsar` 成果
这三件事完成后,系统才算真正开始承接 SBAS 能力。
+102
View File
@@ -0,0 +1,102 @@
# SBAS 系统实施 TODO
更新日期:2026-04-06
## Phase 1:低侵入落地
目标:
- 不影响 D-InSAR
- 让系统开始承接 SBAS 生产记录与结果目录
### 后端模型
- [x] 新增 `PsTimeseriesRunORM`
- [x] 新增 `PsTimeseriesRun` schema
- [x] 为 `settings` 增加 `TIMESERIES_*` / `PSINSAR_PRODUCT_DIR` 配置
### 后端服务
- [x] 新增 `timeseries_service.py`
- [x] 新增 `psinsar_catalog_service.py`
- [x] 增加 SBAS prepare 任务处理逻辑
### 后端路由
- [x] 新增 `timeseries_production.py`
- [x] 新增 `ps_products.py`
- [x] 注册新路由
### 前端
- [x] 新增 `TimeseriesProductionPanel.jsx`
- [x] 新增 `PsinsarCatalogPanel.jsx`
- [x] 新增 `timeseriesProduction.js`
- [x] 新增 `psinsarProducts.js`
- [x] 替换 `ps_production` 占位页
- [x] 替换 `ps_products` 占位页
- [x] 替换 `psinsar_results` 占位页
### 校验
- [x] 后端语法检查
- [x] 前端 build 检查
- [ ] 验证 D-InSAR 页面未受影响
## Phase 2:串接实验处理链
目标:
- 在系统内真正触发 SBAS 处理
### 处理链
- [x] `prepare` 生成标准 stack manifest
- [x] `materialize` 生成 stack 工作目录
- [ ] 接入 ISCE2 stack 执行
- [ ] 接入 MintPy SBAS 执行
- [ ] 接入 geocode/export/publish
### 任务编排
- [x] 新增 workflow steps
- [ ] 新增失败重试策略
- [x] 增加运行日志归集
## Phase 3:结果展示增强
目标:
- 从“结果存在”升级到“结果可分析”
### 展示
- [ ] 地图叠加 `velocity.tif`
- [ ] 按 AOI/时间筛选产品
- [ ] 结果与 run 联动查看
### 分析
- [ ] `geo_timeseries.h5` 点位查询
- [ ] 时间曲线图
- [ ] 质量掩膜联动
- [ ] 热点区域分析
## 当前阶段实施顺序
推荐顺序:
1. 文档补齐
2. 运行记录模型
3. prepare 任务
4. `psinsar` catalog
5. 前端生产页
6. 前端结果页
7. 静态校验
## 当前不做
- [ ] 不重构现有 D-InSAR 结果服务为完全通用框架
- [ ] 不改现有配对算法
- [ ] 不继续保留 SBAS 使用 `*_envi_import` 的旧逻辑
- [ ] 不在当前 D-InSAR 生产环境中直接做依赖升级
+124
View File
@@ -0,0 +1,124 @@
# SBAS 系统改造当前状态
更新日期:2026-04-06
## 1. 当前结论
截至本轮,SBAS / PS-InSAR 已经完成第一阶段的“低侵入接入”改造:
- 已能在系统内创建正式 `ps_timeseries_runs`
- 已能把现有 PS 匹配结果转换为 `stack_input_manifest.json`
- 已能把实验产物 `psinsar.publish.v1` bundle 注册到系统结果目录
- 已能在前端查看 SBAS 运行记录与 `psinsar` 产品目录
- 已补齐 `psinsar` catalog 的启动期 bootstrap 与健康检查接入
这意味着系统已经不再只是“影像管理口”,而是具备了 SBAS 正式生产入口的骨架。当前已经串起实验级四步链:
- `prepare`
- `stack_prep_initial`
- `materialize`
- `stack_prep_refresh`
但仍未接入真正的 ISCE2 stack / MintPy / geocode / publish 自动执行链。
## 2. 已完成范围
### 2.1 后端
- 新增 `ps_timeseries_runs` 运行记录模型
- 新增 `timeseries_service.py`
- 新增 `psinsar_catalog_service.py`
- 新增 `timeseries_production.py`
- 新增 `ps_products.py`
- 作业系统已支持:
- `TIMESERIES_PREPARE`
- `TIMESERIES_STACK_PREP`
- `TIMESERIES_MATERIALIZE`
- `timeseries_service.py` 已补齐 workflow 驱动的:
- `create_run`
- `prepare_run`
- `build_stack_prep`
- `materialize_run`
- `list_runs`
- `get_run_detail`
- 运行详情已可返回 `workflow.steps`
- 启动阶段已同时检查 `dinsar` 与 `psinsar` 两套 catalog
- `/health` 已保留原有 `result_catalog` 兼容字段,并新增:
- `dinsar_result_catalog`
- `psinsar_result_catalog`
- `catalogs`
### 2.2 前端
- 已接入 `TimeseriesProductionPanel`
- 已接入 `PsinsarCatalogPanel`
- 已替换 `ps_production`、`ps_products`、`psinsar_results` 占位页
- `TimeseriesProductionPanel` 已补充 Phase 2 状态色和 workflow 步骤展示
### 2.3 文档
已形成以下系统设计与风险文档:
- `SBAS_SYSTEM_INTEGRATION_AUDIT_20260406.md`
- `SBAS_SYSTEM_EMBEDDING_DESIGN_20260406.md`
- `SBAS_TASK_PATH_MODE_EVALUATION_20260406.md`
- `SBAS_SYSTEM_CHANGE_RISK_ASSESSMENT_20260406.md`
- `SBAS_RESULT_MANAGEMENT_AND_DISPLAY_SPEC_20260406.md`
- `SBAS_FRONTEND_UX_SPEC_20260406.md`
- `SBAS_IMPLEMENTATION_TODO_20260406.md`
## 3. 本轮验证结果
### 3.1 已通过
- 前端 `npm run build` 已通过
- WSL `Ubuntu-24.04` 下,`isce2` 环境对本轮修改后的后端文件执行 `py_compile` 通过
- 启动日志已补充 `PSInSAR-Catalog` 状态输出
### 3.2 当前验证边界
本轮在 WSL `isce2` 环境做后端“导入级冒烟测试”时失败,失败原因不是本轮代码语法问题,而是该环境缺少系统后端依赖:
- `ModuleNotFoundError: No module named 'fastapi'`
这说明当前 `isce2` 环境可以作为 SBAS 处理链环境使用,也可以做后端源码级语法校验,但还不能直接作为系统后端完整运行环境。
## 4. 当前缺口
### 4.1 处理链缺口
系统尚未自动执行以下步骤:
- ISCE2 stack 执行
- MintPy SBAS 执行
- geocode / export / publish
- 产物自动注册收口
### 4.2 环境缺口
若目标是“统一环境”,则还需要复制一份稳定的 ISCE2 环境并补齐系统依赖,形成一个既能跑:
- ISCE2 / MintPy
- 又能支撑 FastAPI 后端依赖
的独立统一环境。当前不建议直接污染现有 D-InSAR 正在使用的生产环境。
## 5. 下一步建议
建议按以下顺序继续:
1. 保持当前 Phase 2 bridge 收口,不继续扩散系统侧改动,先把 `prepare -> stack_prep -> materialize -> refresh` 稳定住。
2. 在实验层继续推进 `isce2_stack -> mintpy -> export_publish` 分步脚本与统一环境验证。
3. 单独复制一份 WSL `isce2` 环境,做“统一环境”安装实验,不动现有 D-InSAR 生产环境。
4. 统一环境验证稳定后,再把真正的 stack / MintPy / publish 接回系统作业编排。
## 6. 当前判断
当前路线是成立的。
成立点不在于“已经跑通全系统自动 SBAS”,而在于:
- 系统边界已经切对
- D-InSAR 老链路基本保持隔离
- SBAS 已有正式运行记录、结果目录和前端入口
- 后续只需要继续往中间补处理链,而不是推倒重来
@@ -0,0 +1,236 @@
# SBAS 结果管理与展示规范
更新日期:2026-04-06
## 1. 管理目标
SBAS 结果进入系统后,必须满足三个层面的要求:
- 可登记
- 可追溯
- 可展示
当前推荐的管理边界不是 MintPy 运行目录,而是发布级 bundle。
唯一注册入口:
- `publish/.../manifest.json`
## 2. 结果层级
### 2.1 运行层
运行层记录在:
- `ps_timeseries_runs`
职责:
- 记录一次正式处理运行的业务身份
- 记录输入、参数、DEM、轨道、掩膜、目录与状态
### 2.2 产品层
产品层继续复用:
- `result_products`
- `result_assets`
- `result_issues`
约定:
- `catalog_name = psinsar`
- `product_type = psinsar_bundle`
- `run_key = ps_timeseries_runs.run_id`
### 2.3 资产层
资产层由 `result_assets` 管理。
建议角色:
- `timeseries_cube`
- `velocity_map`
- `velocity_geotiff`
- `temporal_coherence`
- `temporal_coherence_geotiff`
- `quality_mask`
- `quality_mask_geotiff`
- `preview_png`
- `diagnostic_png`
- `config`
- `quality_summary`
## 3. 发布级 bundle 约束
标准目录:
```text
<PSINSAR_PRODUCT_DIR>/<year>/<run_id>/
manifest.json
assets/
preview/
metadata/
```
最低要求:
- `manifest.json`
- `assets/geo_timeseries.h5`
- `assets/velocity.tif`
- `preview/velocity_preview.png`
建议同时保留:
- `assets/geo_velocity.h5`
- `assets/temporalCoherence.tif`
- `assets/maskTempCoh.tif`
- `preview/numTriNonzeroIntAmbiguity.png`
- `metadata/source_quality_summary.json`
## 4. 产品主文件定义
### 4.1 科学主产物
- `assets/geo_timeseries.h5`
这是系统证明“具备时序处理能力”的核心文件。
### 4.2 展示主产物
- `assets/velocity.tif`
这是结果首屏展示、地图浏览、预览生成的主文件。
### 4.3 质量主产物
- `assets/temporalCoherence.tif`
- `assets/maskTempCoh.tif`
## 5. manifest 解析要求
系统应支持:
- `schema_version = psinsar.publish.v1`
系统应从 manifest 中提取:
- 基础身份
- `catalog_name`
- `mode`
- `engine_code`
- `processor_code`
- 时序信息
- `reference_date`
- `stack_dates`
- 资产信息
- `artifacts[]`
- 质量信息
- `quality`
- 辅助摘要
- `summaries`
如 manifest 缺少显式 bbox,可从 `summaries.*.attrs` 中推导:
- `X_FIRST`
- `Y_FIRST`
- `X_STEP`
- `Y_STEP`
- `WIDTH`
- `LENGTH`
## 6. 展示规范
## 6.1 列表页
优先展示:
- 产品名称
- 运行标识
- 参考日期
- 影像数量
- 发布时间
- 缩略图
## 6.2 详情页
详情页应至少展示:
- 运行信息
- 时间序列日期列表
- 质量摘要
- 资产清单
- 问题清单
## 6.3 地图展示优先级
当前优先级:
1. `velocity.tif`
2. `temporalCoherence.tif`
3. `maskTempCoh.tif`
不建议当前阶段直接做:
- `geo_timeseries.h5` 全量地图服务化
## 7. 结果与运行的关联
必须能从结果反查运行。
推荐方式:
- `result_products.run_key = ps_timeseries_runs.run_id`
这样可以回答:
- 这个结果来自哪个 batch
- 用了哪些日期
- DEM 和轨道来自哪里
- 使用了什么水体掩膜策略
## 8. 目录与数据库职责边界
目录是事实源:
- `manifest.json`
- `assets/*`
- `preview/*`
数据库是索引层:
- 产品列表
- 检索字段
- 资产索引
- 问题索引
因此不应让数据库替代目录事实源,也不应让前端直接依赖实验运行目录。
## 9. Phase 1 / Phase 2 边界
### Phase 1
- 支持 bundle 扫描与登记
- 支持产品列表和详情
- 支持缩略图展示
### Phase 2
- 支持地图叠加 velocity
- 支持 run 与 product 联动
- 支持更多质量视图
### Phase 3
- 支持 `geo_timeseries.h5` 点位时序查询
- 支持图表和专题分析
## 10. 当前结论
SBAS 结果管理的核心不是“把很多文件存起来”,而是把下面三件事稳定下来:
- 用 `manifest.json` 定义结果边界
- 用 `result_products/result_assets` 登记 bundle
- 用 `run_key` 把结果和运行记录连起来
这三件事一旦成立,系统就具备了承接 SBAS 成果的基础能力。
@@ -0,0 +1,219 @@
# SBAS 系统改造风险评估
更新日期:2026-04-06
## 1. 目标
本次改造的目标不是把实验脚本直接搬进系统,而是在尽量不影响现有 D-InSAR 生产链的前提下,为系统增加一条独立的 SBAS/PS-InSAR 生产与结果管理能力。
约束条件:
- 现有 D-InSAR 生产逻辑必须保持可用
- 现有匹配逻辑先保持不动
- SBAS 生产只基于原始影像,不再沿用 `*_envi_import`
- 结果必须最终落到系统内可管理、可展示、可追踪的发布级 bundle
- 统一环境实验优先,但不能污染当前 D-InSAR 使用的 `isce2` 生产环境
## 2. 风险分级总览
### P0 高风险
- 误改现有 D-InSAR 生产入口、目录契约或作业类型,导致原有生产失败
- 把 SBAS 直接套进现有 `master/slave` 双景输入模型,导致设计方向错误
- 结果编目直接改写现有 `result_catalog_service.py` 的 D-InSAR 语义,造成历史产品异常
- 在当前生产 `isce2` 环境内直接安装新依赖,破坏现有运行稳定性
### P1 中风险
- SBAS 运行记录、任务记录、结果记录之间关系不清,后期无法追溯
- Windows/WSL 双路径继续散落在多个服务中,后续维护成本继续升高
- 前端直接绑定运行时目录结构,导致实验目录变化后页面失效
- 结果展示只展示 `velocity.tif`,忽略 `geo_timeseries.h5`,造成“有图无时序能力”的误判
### P2 低风险
- 字段命名不统一,后续扩展时需要补充适配
- 初期前端只做结果浏览,不做像元级时序查询
- 先不接入健康检查总览,短期不会阻断主流程
## 3. 风险清单
### 3.1 生产链串扰风险
风险:
- 新增 SBAS 能力时误复用或改写 D-InSAR 路由、作业类型、拷贝逻辑、结果目录逻辑
触发点:
- 修改 `backend/app/routers/dinsar_production.py`
- 修改 `backend/app/copier.py` 中现有 D-InSAR 行为
- 在 `result_catalog_service.py` 中直接硬改通用逻辑
影响:
- D-InSAR 生产失败
- 历史产物目录重建异常
控制策略:
- Phase 1 新增独立入口:
- `timeseries_production.py`
- `ps_products.py`
- `psinsar_catalog_service.py`
- 不修改 D-InSAR 的路由路径和任务类型
- 不复用 `copy-ps-stack` 作为 SBAS 生产输入准备
### 3.2 输入契约错误风险
风险:
- 继续沿用 D-InSAR 的 `Task_*/master + slave` 输入目录契约
影响:
- SBAS 处理逻辑被错误建模为“双景处理”
- 后续 prepare/materialize/stack workflow 无法自然落地
控制策略:
- 明确采用 `Stack_<group_key>` / `stack_input_manifest.json` 契约
- 生产入口接收 `batch_id`,由系统生成 stack 级 manifest
- 匹配层继续保留,生产层单独增加 prepare
### 3.3 环境污染风险
风险:
- 在当前 D-InSAR 使用的 `isce2` 运行环境内直接安装 MintPy 或其他依赖
影响:
- 现有 D-InSAR 生产不可复现
- 环境漂移,难以排查
控制策略:
- 当前系统 Phase 1 仅记录并使用 `TIMESERIES_*` 独立配置
- 当前实验优先对接已经验证过的统一环境或桥接环境
- 保留现有 D-InSAR `isce2` 环境不动
### 3.4 数据身份与结果归档风险
风险:
- 只有批次,没有正式“运行记录”
- 只有实验目录,没有系统级产品登记
影响:
- 无法回答“某次 SBAS 运行用的 DEM、轨道、参考日期、掩膜策略是什么”
- 无法稳定展示和管理结果
控制策略:
- 新增 `ps_timeseries_runs`
- 结果仍复用 `result_products` / `result_assets` / `result_issues`
- `run_key` 绑定 `ps_timeseries_runs.run_id`
- 发布边界固定为 `manifest.json`
### 3.5 前端误导风险
风险:
- 仅提供一个“PS 生产占位页”,但用户无法区分:
- 规划批次
- 正式生产运行
- 结果目录
- 成果展示
影响:
- 操作路径混乱
- 误以为已有生产能力
控制策略:
- 生产页和结果页分离
- 生产页展示:
- 可选批次
- 运行参数
- 最近运行
- 当前准备状态
- 结果页展示:
- 编目状态
- 产品列表
- 产品详情
- 资产列表
- 质量摘要
## 4. 本次推荐改造范围
## 4.1 必做
- 新增 `ps_timeseries_runs` 业务表
- 新增 `timeseries_production` 路由
- 新增 SBAS prepare 任务
- 新增 `psinsar_catalog_service`
- 新增 `ps_products` 路由
- 新增前端 `TimeseriesProductionPanel`
- 新增前端 `PsinsarCatalogPanel`
## 4.2 暂缓
- 不改现有 `find-ps-timeseries` 匹配算法
- 不改现有 D-InSAR 生产入口
- 不把 SBAS 直接接进 `copy-ps-stack`
- 不做像元级 `geo_timeseries.h5` 在线查询
- 不做地图服务化切片发布
## 5. 回滚策略
回滚必须满足“删新不伤旧”。
可回滚对象:
- 新增路由
- 新增 ORM 表
- 新增前端面板
- 新增 `psinsar` 编目服务
不应触碰回滚的对象:
- 现有 D-InSAR 路由
- 现有 `system_jobs` / `system_tasks` 表结构语义
- 现有 D-InSAR 结果目录
推荐回滚方式:
1. 先停用前端入口
2. 再停用新路由注册
3. 保留数据库表,不做破坏性删除
4. 保留已发布 `psinsar` bundle,必要时仅停止 catalog rebuild
## 6. 验收标准
### 6.1 低风险验收
- D-InSAR 现有页面正常打开
- D-InSAR 现有生产入口不报错
- D-InSAR 现有结果目录查询不受影响
### 6.2 SBAS Phase 1 验收
- 可以从已有 `ps_task_batch` 创建一个正式 `ps_timeseries_run`
- 系统能为该 run 生成 stack 级 manifest
- 系统能记录 work/publish 目录、参考日期、轨道统计和输入快照
- 系统可以扫描 `psinsar.publish.v1` bundle 并登记到结果目录
- 前端能查看运行记录与产品详情
## 7. 当前结论
本次改造最重要的不是“尽快把 SBAS 跑起来”,而是先把系统边界做对:
- 规划层不动
- 生产层新增
- 结果层隔离接入
- D-InSAR 旧链路不碰
按这个边界推进,风险可控,且后续可以逐步把实验能力提升为正式生产能力。
@@ -0,0 +1,680 @@
# SBAS InSAR 嵌入系统设计稿
更新日期:2026-04-06
## 1. 设计目标
目标不是把当前实验脚本“塞进系统”,而是把已经验证成功的 SBAS 路线变成系统内可提交、可跟踪、可复跑、可发布的正式能力。
设计必须同时满足四个约束:
- 不破坏现有 D-InSAR 生产
- 尽量复用现有 `task` / `job` / `workflow` / `result catalog` 骨架
- 以发布级 bundle 作为系统注册边界,而不是 MintPy 运行目录
- 保留 WSL + conda 的现实部署方式,但把路径和环境耦合收敛到专门层
## 2. 设计原则
### 2.1 规划层和生产层分离
现有:
- `find-ps-timeseries`
- `ps_task_batches`
- `ps_task_items`
这些应该继续保留,但它们只负责:
- 选片
- 栈定义
- 输入快照
它们不应该继续承担:
- 生产运行主记录
- 产物发布状态
- 重试与失败恢复
### 2.2 一个 SBAS 运行对应一个发布 bundle
不要把 SBAS 多个文件当成多个独立产品行。
推荐模型:
- 一个 `ps_timeseries_run`
- 对应一个 publish bundle
- 对应 `result_products` 中一条 `psinsar` 产品记录
- bundle 内部多个文件通过 `result_assets` 管理
这样最贴合当前系统的 `result_products -> result_assets` 结构。
### 2.3 发布边界固定为 `manifest.json`
系统不直接注册:
- `stack_work/`
- `mintpy_work/`
- `timeseries.h5`
- 任意临时中间目录
系统只注册:
- `publish/.../manifest.json`
原因:
- 这是当前实验里最稳定的对外契约
- 它天然适合作为 catalog rebuild 和产品详情入口
- 它把运行时目录与发布目录分开了
### 2.4 Phase 1 优先“隔离集成”,不优先“统一重构”
当前 `result_catalog_service` 明显偏 D-InSAR。
因此 Phase 1 不建议直接大改现有 D-InSAR catalog 服务为完全泛化版本。
更稳妥的做法是:
- 新增 `psinsar_catalog_service.py`
- 接口风格与 `result_catalog_service.py` 保持一致
- 底层继续复用 `result_products` / `result_assets` / `result_issues`
等 SBAS 生产链跑稳后,再提炼公共基类。
## 3. 目标架构
推荐分成五层。
### 3.1 规划层
复用现有:
- `POST /find-ps-timeseries`
- `POST /task-batches/ps`
职责:
- 根据 AOI 和筛选条件找到时间序列候选栈
- 生成一个可复用的 `ps_task_batch`
### 3.2 生产控制层
新增:
- `ps_timeseries_runs`
- `timeseries_production.py`
- `timeseries_service.py`
职责:
- 从一个 `ps_task_batch` 派生一次正式运行
- 固化参数、环境、输入路径、输出路径、DEM、轨道、水体掩膜策略
- 对接 `system_tasks` / `system_jobs` / `workflow_runs`
### 3.3 执行编排层
复用现有:
- `system_tasks`
- `system_jobs`
- `workflow_runs`
- `workflow_steps`
- `workflow_artifacts`
新增:
- `timeseries_workflow_factory.py`
- `mintpy_service.py`
- `isce2_stack_service.py`
职责:
- 把一次 SBAS 运行拆成多个受控步骤
- 每一步进入统一 worker
- 失败可定位到 step,而不是只看到“整条链失败”
### 3.4 发布与编目层
新增:
- `psinsar_catalog_service.py`
- `ps_products.py`
复用:
- `result_products`
- `result_assets`
- `result_issues`
- `result_catalog_states`
职责:
- 扫描和注册 `psinsar.publish.v1`
- 管理产品详情、预览、健康检查、重建
### 3.5 展示层
新增页面:
- `ps_production`
- `ps_products`
- `psinsar_results`
职责:
- 运行提交与监控
- 产品浏览与筛选
- 结果详情、预览和下载
## 4. 核心数据模型
## 4.1 新增业务表:`ps_timeseries_runs`
建议字段:
- `run_id`
- `batch_id`
- `run_name`
- `catalog_name`
- `mode`
- `sbas`
- `engine_code`
- `isce2`
- `processor_code`
- `isce2_stack_mintpy`
- `env_name`
- 例如 `isce2_mintpy_v1`
- `wsl_distro`
- 例如 `Ubuntu-24.04`
- `status`
- `PENDING` / `RUNNING` / `FAILED` / `COMPLETED` / `PUBLISHED`
- `workflow_run_id`
- `task_id`
- `direction`
- `stack_size`
- `reference_date`
- `water_mask_mode`
- `local` / `synthetic_fallback`
- `dem_path_windows`
- `dem_path_wsl`
- `orbit_pool_windows`
- `orbit_pool_wsl`
- `work_root_windows`
- `work_root_wsl`
- `publish_dir_windows`
- `publish_dir_wsl`
- `manifest_path_windows`
- `manifest_path_wsl`
- `params_json`
- `summary_json`
- `input_snapshot_json`
- `orbit_summary_json`
- `quality_summary_json`
- `error_message`
- `created_by`
- `created_at`
- `updated_at`
- `started_at`
- `ended_at`
### 4.2 不新增产品主表
继续使用:
- `result_products`
- `result_assets`
- `result_issues`
但对 `psinsar` 约定如下:
- `catalog_name = psinsar`
- `product_type = psinsar_bundle`
- `engine_code = isce2`
- `task_alias` 存运行展示名
- `run_key` 直接绑定 `ps_timeseries_runs.run_id`
原因:
- 当前系统的产品层本来就是“一个产品行 + 多资产”
- SBAS publish bundle 天然适合这种结构
### 4.3 `result_assets.asset_role` 约定
建议统一定义以下资产角色:
- `timeseries_cube`
- `velocity_h5`
- `velocity_geotiff`
- `temporal_coherence_h5`
- `temporal_coherence_geotiff`
- `quality_mask_h5`
- `quality_mask_geotiff`
- `preview_png`
- `diagnostic_png`
- `config`
- `quality_summary`
- `input_manifest`
### 4.4 精轨 provenance 建议
最小建议不是新表,而是先把运行级 summary 做扎实。
在 `ps_timeseries_runs.orbit_summary_json` 中记录:
- 每景影像日期
- 轨道 stem
- 轨道 XML 路径
- 来源
- `existing_xml` / `generated_from_txt`
- checksum
- 是否来自 ISCE2 pool
Phase 2 再考虑单独抽 `orbit_assets`。
## 5. 路径与目录设计
## 5.1 总原则
- 原始影像目录不复制
- 工作目录和发布目录必须分离
- 发布目录一旦注册,尽量不再原地改写
### 5.2 推荐目录结构
Windows 侧:
```text
<TIMESERIES_WORK_ROOT>\{run_id}\
input\
stack\
mintpy\
logs\
publish\
```
WSL 侧:
```text
/mnt/.../timeseries_work/{run_id}/
input/
stack/
mintpy/
logs/
publish/
```
正式发布根目录:
```text
<PSINSAR_PRODUCT_DIR>\{year}\{run_id}\
manifest.json
assets/
preview/
metadata/
```
### 5.3 路径服务
不要在 job handler、router、script wrapper 里反复手拼 `/mnt/z`。
新增:
- `timeseries_paths.py`
统一负责:
- Windows -> WSL 路径转换
- 运行目录分配
- 发布目录分配
- 产物入口路径解析
## 6. 工作流设计
推荐 workflow name:
- `isce2_sbas_mintpy_v1`
推荐步骤如下。
### Step 1 `prepare_stack_input`
输入:
- `batch_id`
- 运行参数
动作:
- 读取 `ps_task_items`
- 校验栈大小、方向一致性、日期连续性
- 固化 `input_snapshot_json`
- 解析 DEM、轨道池、输出路径
- 生成 stack manifest
产物:
- `stack_input_manifest.json`
- `source stack snapshot`
### Step 2 `materialize_stack_scenes`
动作:
- 复用现有实验脚本逻辑
- 为每景影像生成 `SLC/<date>/`
- 显式写入 `sensor.orbitFile`
产物:
- 物化后的 SLC
- `materialization_summary.json`
### Step 3 `run_isce2_stack`
动作:
- 运行 stripmapStack 预处理链
- 生成几何、配准、干涉网络基础产物
产物:
- stack work 目录
- run files
- log files
### Step 4 `run_mintpy_sbas`
动作:
- 运行 `smallbaselineApp`
- 生成 `timeseries.h5`、`velocity.h5`、`temporalCoherence.h5`
- 记录 MintPy 版本、参数模板、参考点信息
产物:
- MintPy work 目录
- inversion quality summary
### Step 5 `export_publish_bundle`
动作:
- 地理编码
- 导出 GeoTIFF
- 生成 preview
- 写出 `manifest.json`
产物:
- `publish/.../manifest.json`
- `assets/`
- `preview/`
- `metadata/`
### Step 6 `register_psinsar_product`
动作:
- 调用 `psinsar_catalog_service`
- 把 bundle 注册进 `result_products`
- 回填 `ps_timeseries_runs.manifest_path_*`
产物:
- `result_products` 一条记录
- `result_assets` 多条记录
### Step 7 `post_check`
动作:
- 校验关键资产是否存在
- 校验 `velocity.tif` / `geo_timeseries.h5` / `temporalCoherence.tif`
- 生成最终健康状态
## 7. 调度与任务设计
## 7.1 一个用户可见主任务
建议每次 SBAS 运行只创建一个主 `system_task`:
- `task_type = RUN_PSINSAR_PRODUCTION`
用户在 UI 上主要看这条任务。
### 7.2 一个工作流运行
创建:
- `workflow_run`
每个步骤单独入 `workflow_steps`。
worker 现有能力已经支持:
- job 完成后自动 `mark_step_completed`
- job 失败后自动 `mark_step_failed`
因此不需要再发明新的 step 状态机制。
### 7.3 建议 job types
- `TIMESERIES_PREP_STACK`
- `TIMESERIES_MATERIALIZE_STACK`
- `TIMESERIES_RUN_ISCE2_STACK`
- `TIMESERIES_RUN_MINTPY_SBAS`
- `TIMESERIES_EXPORT_PRODUCTS`
- `TIMESERIES_REGISTER_PRODUCTS`
- `TIMESERIES_POST_CHECK`
## 8. 环境设计
## 8.1 运行环境原则
不修改现有 D-InSAR 使用的 `isce2` 环境。
SBAS 生产使用独立统一环境:
- WSL distro:`Ubuntu-24.04`
- conda env:建议 `isce2_mintpy_v1`
这是当前实验已验证成功的路线。
### 8.2 配置隔离
不要直接复用所有 `ISCE2_*` 配置项来承载 SBAS。
建议新增:
- `TIMESERIES_ENABLED`
- `TIMESERIES_WSL_DISTRO`
- `TIMESERIES_PYTHON`
- `TIMESERIES_ENV_NAME`
- `TIMESERIES_WORK_ROOT`
- `TIMESERIES_PUBLISH_ROOT`
- `TIMESERIES_DEM_PATH`
- `TIMESERIES_ORBIT_POOL_ISCE2`
- `TIMESERIES_WATER_MASK_ROOT`
- `TIMESERIES_ALLOW_SYNTHETIC_WATER_MASK`
- `TIMESERIES_SMALLBASELINE_TEMPLATE`
其中可允许默认回退:
- `TIMESERIES_WSL_DISTRO -> ISCE2_WSL_DISTRO`
- `TIMESERIES_DEM_PATH -> ISCE2_DEM_PATH`
- `TIMESERIES_ORBIT_POOL_ISCE2 -> ORBIT_POOL_ISCE2`
## 9. 精轨、DEM、水体掩膜策略
## 9.1 精轨
直接复用现有:
- source orbit scan
- ENVI / ISCE2 orbit pool sync
- LT-1 orbit XML 解析/生成 helper
生产要求:
- 每景影像在运行前就解析到明确 orbit XML
- 运行记录中保存 orbit provenance
## 9.2 DEM
生产态不依赖在线下载。
要求:
- 本地 DEM 路径必须可读
- 运行记录中固定写入 DEM 路径与版本描述
## 9.3 水体掩膜
这里要区分实验和生产。
实验态:
- 允许 `synthetic waterMask fallback`
生产态建议:
- 优先本地水体数据源
- 若只能 synthetic fallback,则产品可生成,但 `health_status = WARN`
- UI 上必须显式标注“使用了 synthetic water mask”
## 10. `psinsar` 产品注册策略
## 10.1 产品行设计
`result_products` 一条记录代表一个 SBAS bundle,而不是单个文件。
推荐约定:
- `product_id = psinsar_<run_id>`
- `catalog_name = psinsar`
- `product_type = psinsar_bundle`
- `display_name = SBAS_<direction>_<reference_date>_<run_id short>`
- `source_primary_path = assets/geo_timeseries.h5`
- `primary_asset_path = assets/velocity.tif`
- `preview_path = preview/velocity_preview.png`
### 10.2 产品详情展示
产品详情页重点展示:
- 栈日期列表
- 参考日期
- 方向
- DEM
- 轨道来源
- 速度图预览
- 时间序列 cube 下载
- 质量图层下载
- 运行日志入口
## 11. API 设计
### 11.1 生产接口
- `POST /timeseries-production/runs`
- 基于 `batch_id` 提交一次 SBAS 运行
- `GET /timeseries-production/runs`
- 列表
- `GET /timeseries-production/runs/{run_id}`
- 详情
- `POST /timeseries-production/runs/{run_id}/retry-step`
- 重试单步
- `POST /timeseries-production/wsl-check`
- 环境检查
### 11.2 产品接口
- `GET /ps-products/catalog-status`
- `POST /ps-products/rebuild`
- `GET /ps-products`
- `GET /ps-products/{product_id}`
- `GET /ps-products/{product_id}/preview`
## 12. 前端页面设计
### 12.1 `ps_production`
页面模块:
- 选择已有 `ps_task_batch`
- 设定运行参数
- 提交运行
- 查看步骤进度
- 查看失败原因和重试按钮
### 12.2 `ps_products`
页面模块:
- 产品列表
- AOI / 时间 / 方向 / 状态筛选
- preview 缩略图
- 一键跳转到产品详情
### 12.3 `psinsar_results`
页面模块:
- 地图展示 `velocity.tif`
- 质量图层切换
- 时间序列主资产下载
- 后续再扩展点时序查询
## 13. 推荐实施顺序
### Phase A:最小生产主链
- 新增 `ps_timeseries_runs`
- 新增 `timeseries_production.py`
- 新增 workflow factory
- 先直接复用实验脚本作为受控 worker wrapper
目标:
- 系统能发起一次 SBAS 运行
- 能看到步骤状态
- 能落到 publish bundle
### Phase B:产品注册
- 新增 `psinsar_catalog_service.py`
- 新增 `ps_products.py`
- 注册 `psinsar.publish.v1`
目标:
- 发布 bundle 能在系统里被浏览和重建
### Phase C:稳定性与治理
- 增加 orbit provenance
- 增加 DEM / water mask 质量标记
- 增加 post-check 和 health-check
- 完善路径服务
### Phase D:UI 完善
- 补生产页
- 补产品页
- 补结果页
## 14. 最终建议
结合当前审计结果,最稳妥的路线不是“直接把 SBAS 纳入现有 D-InSAR 产品通道”,而是:
1. 继续复用 `ps_task_batches` 作为输入栈定义
2. 新增 `ps_timeseries_runs` 作为生产主记录
3. 复用现有任务、作业、工作流骨架
4. 以 publish bundle 为产品注册边界
5. 先独立实现 `psinsar_catalog_service`,避免影响现有 D-InSAR 生产
这样可以做到:
- 技术上最贴合现有系统
- 风险上最小
- 后续也最容易把实验链稳稳升级成正式生产链
@@ -0,0 +1,359 @@
# SBAS 系统集成与数据管理审计报告
更新日期:2026-04-06
## 1. 审计范围
本次审计聚焦三件事:
- 当前 ISCE2 SBAS 实验是否已经真实读取并使用精密轨道
- 现有系统的数据管理方案是否适合承接 SBAS/PS-InSAR 生产口
- 如果后续要把实验方案嵌入系统,推荐怎样分阶段落地
本报告基于当前仓库实现与已成功的实验产物,不涉及对现有 D-InSAR 生产链的破坏性修改。
## 2. 结论摘要
- 结论 1:当前 SBAS 实验已经真实读取并使用精密轨道,不只是“数据库里有轨道路径”。
- 结论 2:当前系统可以完成 PS/SBAS 选片和实验产物生成,但还不能把 `psinsar` 产物作为正式生产结果注册、编目、健康检查和发布。
- 结论 3:当前数据管理整体偏“路径驱动”,原始数据身份、存储位置、运行产物三层边界不够清晰,因此用户会感到“数据管理比较松散”。
- 结论 4:轨道管理反而是当前较完整的一块,已经具备源目录扫描、池同步、一致性检查和数据库状态统计能力。
## 3. 审计发现
### P0 / 高优先级
#### 3.1 结果编目和发布入口仍然是 D-InSAR 专用,`psinsar` 实验产物无法直接接入系统
证据:
- `backend/app/services/result_catalog_service.py:707-713`
- `_load_manifest()` 只接受 `product_type == dinsar`
- `backend/app/services/result_catalog_service.py:750-753`
- 即使读入 manifest,也把 `product_type` 固定写成 `dinsar`
- `backend/app/routers/dinsar_products.py:107-140`
- 现有编目 API 和后台任务只暴露 `/dinsar-products/*`
- `backend/app/services/health_service.py:124-170`
- 结果目录健康检查只统计 `catalog_name == dinsar`
- `experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/publish/mintpy_sbas_unified_v1/manifest.json:2-9`
- 当前实验成功产物的 manifest 是 `schema_version = psinsar.publish.v1`,`catalog_name = psinsar`
影响:
- 实验已经能产出发布级 bundle,但系统当前不会把它识别为正式产品
- 不能复用现有结果页、目录重建、健康检查、产品详情接口
- 后续如果直接“硬塞”进 D-InSAR 目录,会污染现有 D-InSAR 语义
建议:
- Phase 1 不要新建一套完全独立的产品表,继续复用 `result_products` / `result_assets`
- 但必须让 catalog 服务显式支持 `catalog_name = psinsar`
- 最小可行改造是新增 `psinsar` manifest 解析路径,或把当前 catalog service 抽象成多 catalog 分发器
#### 3.2 `ps_task_batches` / `ps_task_items` 目前只是选片快照,不是生产记录
证据:
- `backend/app/models/orm.py:516-550`
- `PsTaskBatchORM` / `PsTaskItemORM` 只保存批次、`file_path`、日期、极化、是否有轨道和状态
- `backend/app/routers/task_batches.py:281-320`
- 创建 `/task-batches/ps` 时只是把选中的影像元数据写入 `ps_task_items`
- `backend/app/routers/pairing.py:124-147`
- 现有 `find-ps-timeseries` 只负责找 stack
- `backend/app/services/spatial_service.py:444-481`
- `find_ps_timeseries_data()` 只返回按轨道方向分组的影像栈
- `backend/app/services/job_handlers.py:306-321`
- 对 `PS_STACK` 的后台处理目前只是复制选中的影像路径
影响:
- 系统里没有“某次 SBAS 生产运行”的业务主记录
- 无法稳定记录 `work_dir`、`publish_dir`、运行环境、参数版本、DEM、轨道来源、水体掩膜来源
- 无法形成真正可追溯、可重跑、可审计的生产口
建议:
- 新增业务表 `ps_timeseries_runs`
- 保留 `ps_task_batches` 作为“输入栈定义”
- 把真正的处理运行、产物发布、失败重试、步骤状态全部挂到 `ps_timeseries_runs + workflow_runs`
#### 3.3 数据身份与存储路径耦合过深,迁移和重构成本偏高
证据:
- `backend/app/models/orm.py:22-40`
- `RadarDataORM` 直接保存 `file_path`、`orbit_file_path`、`preview_cache_path`
- `backend/app/models/orm.py:82-101`
- `ResultProductORM` 直接保存 `publish_dir`、`manifest_path`、`source_primary_path`、`primary_asset_path`
- `backend/app/models/orm.py:182-195`
- `ResultAssetORM` 保存 `absolute_path`
- `backend/app/services/data_service.py:621-668`
- `unique_id` 通过 `os.path.relpath()` 生成,并按 `unique_id` 做 upsert
影响:
- 数据身份会受存储根目录变化影响
- Windows 根目录变更、WSL 路径调整、NAS 迁移时,数据库主身份和存储位置语义混在一起
- 产物迁移到正式发布目录后,历史绝对路径会变成系统耦合点
建议:
- 把“数据身份”和“存储位置”拆开
- 原始影像层至少补充稳定的 `scene_uid` 或 `product_uid`
- 结果层至少补充稳定的 `run_id` / `product_id` 业务主键,把绝对路径退化为可替换的存储定位信息
### P1 / 中优先级
#### 3.4 原始影像与轨道的关联粒度偏粗,缺少轨道版本与来源追踪
证据:
- `backend/app/services/data_service.py:611-612`
- 影像是否有轨道仅按 `(satellite, imaging_date)` 关联
- `backend/app/services/data_service.py:688-692`
- 后续补关联时也是按同一键更新
影响:
- 当同一天存在多个候选轨道版本时,数据库层无法区分来源和版本
- 无法在结果层明确回答“本次 SBAS 用的是哪份轨道文件、其 checksum 是多少、来自 source 目录还是 ISCE2 pool”
建议:
- 最小可行做法:在 `RadarDataORM` 或新表中增加
- `orbit_stem`
- `orbit_checksum`
- `orbit_source_type`
- `orbit_pool_path`
- 更完整做法:单独建 `orbit_assets` / `orbit_bindings`
#### 3.5 数据扫描服务职责过重,扫描、轨道同步、预览缓存、数据库写入耦合在一起
证据:
- `backend/app/services/data_service.py:426-463`
- 扫描原始轨道并同步 ENVI / ISCE2 池
- `backend/app/services/data_service.py:568-699`
- 同一流程里完成影像元数据读取、轨道关联、数据库 upsert
- `backend/app/services/data_service.py:709-739`
- 后面又继续做缓存重建候选收集
影响:
- 这个服务变成“总装配间”,测试和回归都更困难
- 后续要引入 SBAS 生产口时,很容易继续把更多责任塞进同一个服务
建议:
- 拆分为至少四块:
- `radar_inventory_service`
- `orbit_inventory_service`
- `radar_catalog_service`
- `preview_cache_service`
#### 3.6 Windows/WSL 双路径模型是当前现实,但需要收口到专门的路径层
证据:
- `backend/app/config.py:150-186`
- 同时维护 Windows 轨道池、DEM、IDL、WSL distro、WSL Python 等配置
- `experiments/isce2_sbas_timeseries/scripts/build_lt1_stack_prep.py:140-149`
- 实验准备阶段会解析 orbit XML
- `experiments/isce2_sbas_timeseries/scripts/build_lt1_stack_prep.py:171-193`
- 同时生成 Windows 路径和 WSL 路径
- `experiments/isce2_sbas_timeseries/scripts/materialize_lt1_stack_scenes.py:98-103`
- 物化阶段直接把 orbit XML 传给 ISCE2 sensor
影响:
- 当前方案能跑通,但如果没有统一的路径转换层,后续每个服务都可能自己拼接 `/mnt/*`
- 这会继续加重“数据管理松散”的体感
建议:
- 保留当前双路径现实,不要硬回避
- 但把路径转换统一收敛到 `timeseries_paths.py` 或类似组件
- 生产口只暴露业务字段,不让上层 UI 和业务逻辑直接感知 `/mnt/z` 与 `F:\`
### P2 / 正向发现
#### 3.7 轨道管理是当前系统里相对成熟的一块
证据:
- `backend/app/services/data_service.py:426-463`
- 已有源目录扫描与 ENVI / ISCE2 轨道池同步
- `backend/app/routers/orbit.py:27-108`
- 已有数据库统计、池一致性检查、缺口汇总
结论:
- 不需要为 SBAS 重新发明一套轨道管理
- 更合理的方向是复用现有轨道池和一致性检查能力,再补生产级 provenance
## 4. 关于“现在实验读取精密轨道了吗”
答案:是,已经读取,而且已进入实际处理链。
证据链如下:
1. 轨道解析入口:
- `backend/app/isce2_pipeline/lt1_input_resolver.py:165-201`
- `ensure_lt1_orbit_xml()` 会优先取已有 XML,找不到才从 LT-1 轨道 txt 生成 XML
2. Stack 准备阶段:
- `experiments/isce2_sbas_timeseries/scripts/build_lt1_stack_prep.py:140-149`
- 每景影像都调用 `ensure_lt1_orbit_xml()`
3. Stack 物化阶段:
- `experiments/isce2_sbas_timeseries/scripts/materialize_lt1_stack_scenes.py:98-103`
- `sensor.orbitFile = orbit_xml`
4. 实验 manifest 证据:
- `experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/stack_input_manifest.json:92-95`
- `experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/stack_input_manifest.json:123-126`
- `experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/stack_input_manifest.json:154-157`
- manifest 已明确记录 `D:\\orbit_pools\\isce2\\LT1A_GpsData_GAS_C_*.xml`
5. 运行日志证据:
- `experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/stack_work/logs/run_01_reference.log:18`
- `experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/stack_work/logs/run_03_geo2rdr_coarseResamp.log:6`
- 日志中已出现 `Orbit interpolation method: hermite`
结论补充:
- 当前 SBAS 实验使用的是 ISCE2 轨道池中的 XML 精轨
- 因此“精轨是否已读入”这件事在实验层已经成立
- 真正缺的不是“有没有轨道”,而是“系统如何把这次运行及其轨道 provenance 规范地登记为生产记录”
## 5. 集成设计建议
### 5.1 集成边界
当前最合理的系统集成边界不是 MintPy 运行目录,而是发布级 bundle:
- 入口文件:`manifest.json`
- 目录结构:`assets/`、`preview/`、`metadata/`
这与现有产品规范一致:
- `docs/ISCE2_SBAS_PRODUCT_SPEC.md:69-88`
因此:
- 不要直接注册 `stack_work/mintpy_sbas_*`
- 要注册 `publish/.../manifest.json` 所代表的一整个 bundle
### 5.2 推荐最小落地架构
#### 层 1:规划层,继续复用现有能力
- 继续使用 `find-ps-timeseries`
- 继续使用 `ps_task_batches` / `ps_task_items`
- 其职责只保留为“选片与栈定义”
#### 层 2:生产层,新增业务主记录
新增 `ps_timeseries_runs`,至少包含:
- `run_id`
- `batch_id`
- `catalog_name`
- `engine_code`
- `processor_code`
- `env_name`
- `wsl_distro`
- `work_dir`
- `publish_dir`
- `manifest_path`
- `reference_date`
- `direction`
- `params_json`
- `summary_json`
- `orbit_summary_json`
- `dem_path`
- `water_mask_source`
- `status`
- `error_message`
- `started_at`
- `ended_at`
#### 层 3:编目层,扩展现有结果目录
不要单独再造一套产品表,继续复用:
- `result_products`
- `result_assets`
- `result_issues`
但要新增 `psinsar` catalog 入口,支持:
- 解析 `psinsar.publish.v1`
- 注册 `catalog_name = psinsar`
- 单独的 catalog status / rebuild / health-check
#### 层 4:接口层
建议新增:
- `POST /timeseries-production/runs`
- `GET /timeseries-production/runs`
- `GET /timeseries-production/runs/{run_id}`
- `POST /timeseries-production/runs/{run_id}/retry-step`
- `GET /ps-products`
- `GET /ps-products/{product_id}`
- `POST /ps-products/rebuild-catalog`
### 5.3 与当前设计文档的一致性
当前仓库里的 SBAS 设计文档已经提前写出了这条路线:
- `docs/ISCE2_SBAS_TIMESERIES_DESIGN.md:227`
- 建议新增 `ps_timeseries_runs`
- `docs/ISCE2_SBAS_TIMESERIES_DESIGN.md:292-296`
- 建议新增 SBAS 相关 job types
- `docs/ISCE2_SBAS_TIMESERIES_DESIGN.md:306-313`
- 建议新增 `timeseries_production` / `ps_products` / `psinsar_catalog_service`
- `docs/ISCE2_SBAS_TIMESERIES_DESIGN.md:563-575`
- TODO 中已明确列出 ORM、路由、job handler、catalog 扩展和健康检查
所以本次审计的判断不是与现有设计冲突,而是说明:
- 实验层已经坐实
- 代码现实也证明下一步确实该进入“生产层与 catalog 层补齐”
## 6. 建议 TODO
### 6.1 立即做
- [ ] 新增 `ps_timeseries_runs` ORM / schema / migration
- [ ] 新增 `timeseries_production` router
- [ ] 新增 `PUBLISH_PSINSAR_PRODUCTS` 及相关 job handlers
- [ ] 让 catalog 服务支持 `psinsar.publish.v1`
- [ ] 把 `publish/.../manifest.json` 作为唯一注册入口
### 6.2 紧接着做
- [ ] 给 `ps_timeseries_runs` 记录 DEM、轨道、水体掩膜、运行环境版本
- [ ] 给轨道绑定补 provenance 字段或单独轨道资产表
- [ ] 把 Windows/WSL 路径转换收敛到独立路径服务
- [ ] 为 `psinsar` 增加 catalog health-check 与 rebuild
### 6.3 暂缓做
- [ ] 不要现在就改动现有 D-InSAR 生产链
- [ ] 不要把 SBAS 运行目录直接暴露为产品目录
- [ ] 不要为了 SBAS 先拆掉现有 `result_products`
## 7. 审计结论
如果现在问“SBAS 能不能开始往系统里接”,答案是:
- 能,但应该先接“发布 bundle + 生产记录 + psinsar catalog”
- 不能直接把实验目录粗暴塞进现有 D-InSAR 产品体系
如果现在问“系统的数据管理为什么显得松散”,核心原因是:
- 原始数据身份、物理存储路径、生产产物目录三层目前还没有彻底分开
如果现在问“下一步最值得做什么”,答案是:
- 先把 `ps_timeseries_runs + psinsar catalog` 这两个缺口补上
这样既不会动坏现有 D-InSAR 生产,又能把已经验证成功的 SBAS 实验稳稳接成系统能力。
@@ -0,0 +1,319 @@
# SBAS 是否可复用现有 D-InSAR Task 路径生产模式
更新日期:2026-04-06
## 1. 结论
结论分两层:
- 可以复用“以标准输入目录作为生产入口”的模式
- 不能直接复用现有 D-InSAR 的 `Task_x/master + slave` 目录协议和 `copy-ps-stack` 实现
也就是说:
- 可以借用 D-InSAR 的生产思想
- 不能照搬 D-InSAR 的输入结构
## 2. 现有 D-InSAR 生产模式是什么
当前 D-InSAR 的 ISCE2 生产模式,本质上是:
1. 先把一对影像整理成标准任务目录
2. 每个任务目录固定包含:
- `master/`
- `slave/`
- `.dinsar_pair.json`
3. 生产入口只接收一个 `root_dir`
4. 引擎自行扫描这个 `root_dir`
- 要么它本身就是一个 task 目录
- 要么它下面有多个 `Task_*` 子目录
5. 每个 task 目录独立产出一个 pair result
关键实现:
- `backend/app/dinsar_engines/isce2_engine.py:314-361`
- `validate_root_dir()` 要求:
- 单个 task 目录必须有 `master` 和 `slave`
- 或父目录下有多个合法 `Task_*`
- `backend/app/copier.py:178-218`
- D-InSAR 复制时会构建:
- `<dest>/<task_alias>/master`
- `<dest>/<task_alias>/slave`
- `.dinsar_pair.json`
- `backend/app/isce2_pipeline/run_lt1_dinsar_pipeline.py:262-298`
- pipeline 运行时明确按 `master` / `slave` 两景读取
所以这是一套“面向影像对”的输入协议。
## 3. 现有 PS/SBAS 选片算法能不能找出时序队列
可以,但当前更准确地说,它是:
- 一个“候选栈发现器”
- 还不是一个“生产级栈定义器”
现有实现:
- `backend/app/services/spatial_service.py:414-481`
当前逻辑会:
- 先选出与 AOI 初始重叠比例满足阈值的影像
- 按轨道方向分组
- 求每个方向的公共重叠区
- 再筛出对公共区覆盖比例满足阈值的影像
- 最终按日期排序返回
这对 SBAS 是有价值的,因为它已经能给出:
- 同轨方向
- 共享公共覆盖区
- 时间排序后的影像列表
但它当前还缺少若干生产级约束:
- 没有显式要求 `has_orbit_data = True`
- 没有显式要求相同 `imaging_mode`
- 没有显式要求相同 `polarization`
- 没有最小栈规模规则
- 没有最大时间间隔或参考影像策略
- 没有把结果固化为“一个稳定的 stack identity”
所以答案是:
- 用来找 SBAS 候选时序队列,可以
- 直接当生产入口,不够
## 4. 为什么 D-InSAR 的 task 路径模式不能原样套给 SBAS
### 4.1 D-InSAR 是二元输入,SBAS 是栈输入
D-InSAR 当前 pipeline 只解析两个角色:
- `master`
- `slave`
见:
- `backend/app/isce2_pipeline/run_lt1_dinsar_pipeline.py:262-298`
而 SBAS 至少需要:
- 一个有序场景集合
- 栈级别的公共参数
- 栈级别的参考日期
- 栈级别的输出目录约定
因此 SBAS 的输入目录协议必须是“stack contract”,不是“pair contract”。
### 4.2 当前 `copy-ps-stack` 是平铺复制,不会生成栈元数据
现有 PS 复制逻辑:
- `backend/app/copier.py:45-115`
它的行为是:
- 从 `PsTaskItemORM.file_path` 取路径
- 优先复制 `original_path + "_envi_import"`
- 否则复制原路径
- 直接平铺复制到 `dest_dir/<basename>`
它不会生成:
- 栈 manifest
- 日期排序
- 参考日期
- 轨道清单
- DEM / water mask / publish 约定
因此这套复制结果目前更像“把选中的影像搬过去”,不是“形成一个可投产的 SBAS 输入包”。
### 4.3 当前 PS 批次模型保存的信息不够
现有:
- `backend/app/models/orm.py:532-550`
- `backend/app/routers/task_batches.py:303-312`
`PsTaskItemORM` 只保存:
- `file_path`
- `satellite`
- `imaging_date`
- `polarization`
- `has_orbit_data`
没有保存:
- `orbit_direction`
- `imaging_mode`
- 稳定 `group_key`
- 公共覆盖区
- 参考影像策略
这说明 PS 批次目前还是“输入快照”,不是“可直接投产的 stack contract”。
## 5. 哪部分可以直接复用
可以直接复用的,不少。
### 5.1 生产入口模式
复用思路:
- 一个 `root_dir`
- 引擎自己扫描这个目录
- 支持“单个 stack 目录”或“父目录下多个 stack 目录”
这点可以直接仿照:
- `backend/app/dinsar_engines/isce2_engine.py:314-361`
只是把目录判定从:
- `master + slave`
换成:
- `stack_input_manifest.json` 或 `.ps_stack.json`
### 5.2 作业提交模式
可以直接复用 D-InSAR 当前的:
- router 提交任务
- `task_service.create_task()`
- `job_queue_service.create_job()`
- worker 执行
- 返回单次运行状态
### 5.3 目录驱动执行
这部分思想也可以复用:
- 输入目录是唯一入口
- engine 不依赖前端临时状态
- 只要目录契约稳定,就可以重复运行
这对 SBAS 是对的。
## 6. SBAS 推荐的“task 路径模式”
建议做一个 SBAS 版输入目录协议,而不是复用 D-InSAR 的 `Task_*` 结构。
推荐:
```text
Stack_<group_key>/
stack_input_manifest.json
scenes/
20250118/
source_scene.json
<raw scene or link>
20250315/
source_scene.json
<raw scene or link>
20250510/
source_scene.json
<raw scene or link>
inputs/
dem/
logs/
```
后续运行阶段再生成:
```text
Stack_<group_key>/
stack_input_manifest.json
scenes/
SLC/
stack_work/
publish/
logs/
```
## 7. 推荐的 `stack_input_manifest.json` 最小字段
至少应包含:
- `group_key`
- `batch_id`
- `mode`
- `sbas`
- `direction`
- `reference_date`
- `stack_dates`
- `scene_count`
- `dem_path_windows`
- `dem_path_wsl`
- `orbit_pool_windows`
- `orbit_pool_wsl`
- `publish_dir_windows`
- `publish_dir_wsl`
- `water_mask_mode`
- `scenes[]`
- `date`
- `file_path`
- `satellite`
- `imaging_mode`
- `polarization`
- `orbit_direction`
- `orbit_xml_path`
- `has_orbit_data`
这个文件对 SBAS 的作用,等价于 D-InSAR 里的 `.dinsar_pair.json`,但它描述的是一个栈,不是一对。
## 8. 物理复制要不要照搬 D-InSAR
不建议原样照搬。
原因:
- SBAS 常常是 5 景、10 景甚至更多
- 全量复制原始影像会放大 IO 和存储成本
- 你当前原始数据已经在 WSL 可见路径下,不一定需要再复制一遍
更合理的 Phase 1 做法:
- 保留“输入目录”概念
- 但目录里优先存:
- manifest
- sidecar metadata
- 规范化链接或原路径引用
- 仅在必要时物化最小运行所需文件
也就是说:
- 推荐“目录驱动”
- 不推荐“全量复制驱动”
## 9. 最终判断
如果你的问题是:
- “现有匹配算法能不能找出 SBAS 需要的时序影像队列?”
答案是:
- 可以作为候选栈发现器
- 但还需要补生产级校验和栈固化
如果你的问题是:
- “能不能沿用 D-InSAR 生产时的输入 task 路径模式?”
答案是:
- 可以沿用这个模式的思想
- 不能沿用现有 D-InSAR 的具体目录协议和复制实现
最推荐的落地方式是:
1. 保留“目录作为生产入口”
2. 为 SBAS 定义独立的 `Stack_*` 输入契约
3. 用 `stack_input_manifest.json` 替代 `.dinsar_pair.json`
4. 用 stack-aware 的 prepare/materialize 步骤替代当前 `copy-ps-stack` 平铺复制
这样既能继承现有系统的生产模式,又不会把 SBAS 错套成二景 D-InSAR。
+138
View File
@@ -0,0 +1,138 @@
# 安全审计补充记录(2026-03-12)
审计范围:后端权限边界、文件与归档处理链路、部署敏感资产暴露情况。
审计方式:静态只读审计,本文档仅记录结论与建议,不包含代码修复。
审计前提:当前环境为开发机,目标部署环境为私有内网;授权签发材料与初始化密码当前因封装方式不足,仍位于开发目录中。
## 1. 本次审计的判断口径
本次记录将问题分为两类:
1. 开发阶段可解释、但上线打包时必须隔离的运维/交付风险。
2. 与部署环境无关、进入内网后仍然成立的代码级安全缺陷。
这一区分的目的,是避免把“开发机上的临时暴露”与“真正需要修代码的问题”混为一谈。
## 2. 开发机阶段的可解释暴露
以下内容在当前“开发机 + 私有内网 + 尚未完成封装”的前提下,可以暂时理解为运维接受项;但它们不应进入最终部署包、客户现场机器、共享目录或备份集。
### 2.1 授权签发私钥与签发工具位于项目目录
- 证据:
- `license-issuer/issue_license.py`
- `license-issuer/private_key.b64`
- `backend/app/license_service.py`
- `license-issuer/README.md`
- 说明:
- 当前目录同时包含授权验证逻辑和签发私钥材料。
- 在开发阶段这可以理解为便于调试和现场签发。
- 但从交付角度看,签发端与验证端必须物理隔离。
- 当前结论:
- 在开发机上记录为“运维接受项”。
- 在上线交付前必须从部署物中彻底移除。
### 2.2 `.env` 中包含初始化管理员密码
- 证据:
- `.env`
- `scripts/init_db.py`
- `README.md`
- 说明:
- 初始化脚本会读取 `INIT_ADMIN_PASSWORD` 用于创建或重置管理员。
- 这在开发期、首次部署准备期可以理解为简化初始化流程。
- 但如果 `.env` 被打进部署包、同步到共享目录或留在客户机,管理员账户控制面会暴露。
- 当前结论:
- 在开发机上记录为“运维接受项”。
- 在上线交付前必须改为受控注入,不应随项目目录长期保留。
## 3. 上线前应修复的代码问题
以下问题即使部署在私有内网,仍然成立,建议在正式部署前完成修复。
### 3.1 归档解压边界仍不安全
- 风险等级:高
- 证据:
- `scripts/unpack_archives.py:123`
- `scripts/unpack_archives.py:206`
- `backend/app/services/gf3_service.py:27`
- `backend/app/services/gf3_service.py:36`
- `backend/app/services/gf3_service.py:42`
- 问题描述:
- 现有检查主要基于归档条目名中的 `..` 或绝对路径。
- 但后续仍直接使用 `extractall`。
- 这种模式对 tar 的符号链接、硬链接等条目并不稳妥。
- 影响:
- 如果输入归档来自外部来源、共享目录或低信任操作员,可能出现目录逃逸或覆盖非目标文件的风险。
- 建议:
- 禁用不安全条目类型。
- 使用逐条提取代替 `extractall`。
- 在提取前对最终落盘路径做真实路径校验。
### 3.2 导出目录白名单使用前缀匹配,存在绕过空间
- 风险等级:中
- 证据:
- `backend/app/routers/dependencies.py:179`
- `backend/app/routers/dependencies.py:219`
- `backend/app/routers/tools.py:88`
- `backend/app/routers/dinsar.py:299`
- `backend/app/routers/idl.py:246`
- 问题描述:
- `ALLOWED_EXPORT_DIRS` 白名单校验使用 `startswith`。
- 当允许目录为 `C:\exports` 时,类似 `C:\exports_tmp` 也可能被误判为合法。
- 影响:
- 导出、复制、结果提取类接口的目标目录边界不够严格。
- 建议:
- 改为基于规范化后的真实路径做“目录包含关系”判断,而不是字符串前缀判断。
### 3.3 登录限流按用户名生效,容易被用于锁死指定账号
- 风险等级:中
- 证据:
- `backend/app/routers/dependencies.py:459`
- `backend/app/routers/auth.py:72`
- `backend/app/routers/dependencies.py:92`
- `backend/app/routers/dependencies.py:94`
- 问题描述:
- 当前限流键仅基于用户名,不区分来源。
- 任意请求方只需反复提交错误密码,即可让目标账号进入锁定窗口。
- 影响:
- 会形成低成本拒绝服务,尤其对 `admin` 账号明显。
- 建议:
- 改为“用户名 + 来源”或多维限流。
- 对不存在账号和存在账号保持相近的响应节奏。
- 视部署场景决定是否叠加网关层限流。
## 4. 本轮未发现的高风险点
本轮复核中,暂未发现以下明显高危问题:
- 前端未见将会话令牌持久化到 `localStorage` 或 `sessionStorage`。
- 后端主路由默认挂载了统一的授权与登录依赖。
- 管理类写操作多数仍要求管理员身份。
这不代表系统不存在其他缺陷,只表示在本次覆盖范围内未发现更高优先级的新问题。
## 5. 建议的上线前处置顺序
### P0:正式部署前必须完成
1. 修复所有归档解压的安全边界。
2. 修复导出路径白名单判断逻辑。
3. 调整登录限流策略,避免用户名级别锁死。
4. 确保 `license-issuer/`、`.env`、任何实际授权文件不进入部署包。
### P1:交付封装优化
1. 将签发工具与后端运行目录彻底拆分。
2. 将初始化口令改为部署时注入,而不是长期存放在项目根目录。
3. 为部署物建立显式排除清单。
## 6. 说明
- 本文档用于记录 2026-03-12 的补充审计结论。
- 本次审计未进行动态利用验证。
- 本次审计未修改业务代码。
@@ -0,0 +1,393 @@
# WSL2 + ISCE2 + MintPy SBAS 集成方案
Updated: 2026-04-12
## 1. 背景
当前仓库已经具备 SBAS/PS-InSAR 的部分系统骨架:
- 已有 `ps_timeseries_runs` 业务表与基础 API
- 已有 `prepare -> stack_prep_initial -> materialize -> stack_prep_refresh` 四步 workflow
- 已有 `psinsar.publish.v1` manifest 解析与 `psinsar` catalog
- 已有 `ps_production`、`ps_products`、`psinsar_results` 前端入口
同时,实验层已经验证:
- LT-1 stack 可以在 WSL2 `Ubuntu-24.04` 下跑通到 `run_08_igram`
- MintPy SBAS 可以产出 `timeseries.h5`、`velocity.h5`
- geocode/export 可以产出 `psinsar.publish.v1` publish bundle
- 统一环境 `isce2_mintpy_v1` 可以同时导入 `isce` 与 `mintpy`
因此当前最合理的工作不是重写处理算法,而是把已经验证过的 WSL2 运行链稳定接入现有系统。
## 2. 本机环境核对
本方案基于 2026-04-12 的本机实际核对结果。
### 2.1 WSL2 发行版
- 可见发行版:
- `Ubuntu`
- `Ubuntu-24.04`
- SBAS 集成目标发行版:
- `Ubuntu-24.04`
### 2.2 已确认 conda 环境
- `isce2`
- Python:
- `/home/administrator/miniconda3/envs/isce2/bin/python`
- 可导入:
- `isce`
- `mintpy`
- Python:
- `/home/administrator/miniconda3/envs/mintpy/bin/python`
- 可导入:
- `mintpy`
- `isce2_mintpy_v1`
- Python:
- `/home/administrator/miniconda3/envs/isce2_mintpy_v1/bin/python`
- 可导入:
- `isce`
- `mintpy`
### 2.3 当前判断
- `isce2_mintpy_v1` 已满足“单环境跑 stack + MintPy”的基础条件
- 现有 `isce2` 环境仍应保留给当前 D-InSAR 生产链
- 运行时不应依赖交互式 `sudo`
- 所有系统依赖、conda 环境、脚本权限应在部署阶段一次性准备完成
## 3. 设计结论
### 3.1 主方案
SBAS 系统集成默认采用:
- WSL distro:
- `Ubuntu-24.04`
- runtime env:
- `isce2_mintpy_v1`
### 3.2 备用方案
保留实验期的双环境桥接链路作为 fallback:
- `isce2`
- 负责 ISCE2 stack
- `mintpy`
- 负责 MintPy SBAS
但 fallback 不应作为系统默认路径,只在统一环境故障时使用。
### 3.3 产品边界
系统正式接入边界固定为 publish bundle:
- `manifest.json`
- `assets/`
- `preview/`
- `metadata/`
系统不直接把以下目录视为正式产品:
- `stack_work/`
- `mintpy_sbas_*`
- `Igrams/`
- 其他中间目录
## 4. 现状与缺口
### 4.1 已完成
- 运行记录模型:
- `ps_timeseries_runs`
- 基础生产 API:
- `timeseries-production`
- 基础产品 API:
- `ps-products`
- `psinsar` catalog 解析与重建
- 前端生产页、产品页、结果页入口
### 4.2 未完成
当前系统还没有把下面四步接入正式 workflow:
1. `run_isce2_stack`
2. `run_mintpy_sbas`
3. `export_publish_bundle`
4. `register_psinsar_product`
这意味着当前系统是“桥接层已接入”,但还不是“完整自动生产闭环”。
## 5. 目标架构
```text
Windows FastAPI / Worker
-> submit run / record metadata / monitor progress
-> dispatch WSL jobs
-> register publish bundle into psinsar catalog
-> serve API to frontend
WSL2 Ubuntu-24.04
-> ISCE2 stripmapStack execution
-> MintPy SBAS inversion
-> geocode / export / preview generation
-> build manifest.json
Formal Product Boundary
-> PSINSAR_PRODUCT_DIR/<product_id>/
manifest.json
assets/
preview/
metadata/
```
设计原则:
- 系统负责编排,不负责重计算
- WSL2 负责执行,不负责业务登记
- 正式产品只认 publish bundle
- D-InSAR 与 SBAS 运行时保持隔离
## 6. 推荐 workflow
### 6.1 Phase 1 已有步骤
1. `prepare_stack_input`
2. `build_stack_prep_initial`
3. `materialize_stack_scenes`
4. `refresh_stack_ready`
### 6.2 需要新增的正式步骤
5. `run_isce2_stack`
- 在 `Ubuntu-24.04` 中执行生成的 `run_01` 至 `run_08_igram`
- 产出 `Igrams/`、`geom_reference/`、`baselines/`
6. `run_mintpy_sbas`
- 在 `isce2_mintpy_v1` 中执行 MintPy SBAS
- 产出:
- `timeseries.h5`
- `velocity.h5`
- `temporalCoherence.h5`
- `maskTempCoh.h5`
7. `export_publish_bundle`
- geocode
- GeoTIFF 导出
- 生成预览图
- 生成 `manifest.json`
8. `register_psinsar_product`
- 将 publish bundle 复制或落到正式 `PSINSAR_PRODUCT_DIR`
- 增量登记到 `psinsar` catalog
## 7. 后端设计
### 7.1 配置项
建议显式配置以下参数,不再依赖默认继承:
```dotenv
TIMESERIES_ENABLED=true
TIMESERIES_WSL_DISTRO=Ubuntu-24.04
TIMESERIES_ENV_NAME=isce2_mintpy_v1
TIMESERIES_PYTHON=/home/administrator/miniconda3/envs/isce2_mintpy_v1/bin/python
TIMESERIES_WORK_ROOT=<windows path>
TIMESERIES_DEM_PATH=<windows path>
TIMESERIES_ORBIT_POOL_ISCE2=<windows path>
TIMESERIES_EXPERIMENT_ROOT=<repo>/experiments/isce2_sbas_timeseries
TIMESERIES_STACK_PREP_SCRIPT=<repo>/experiments/isce2_sbas_timeseries/scripts/build_lt1_stack_prep.py
TIMESERIES_MATERIALIZE_SCRIPT=<repo>/experiments/isce2_sbas_timeseries/scripts/materialize_lt1_stack_scenes.py
PSINSAR_PRODUCT_DIR=<formal publish root>
```
关键要求:
- `TIMESERIES_ENV_NAME` 与 `TIMESERIES_PYTHON` 必须指向同一环境
- 不再让 `TIMESERIES_PYTHON` 默认继承 `ISCE2_PYTHON`
### 7.2 模块拆分
建议新增或补强以下模块:
- `timeseries_paths.py`
- 统一维护 Windows/WSL 路径转换
- 统一生成 work/publish/logs 路径
- `timeseries_runtime_service.py`
- 统一执行 WSL 命令
- 统一处理 timeout、stdout/stderr、返回码
- `isce2_stack_runtime_service.py`
- 封装 `run_01` 至 `run_08_igram` 执行
- `mintpy_runtime_service.py`
- 封装 MintPy SBAS 运行
- 封装统一环境 runner
- `psinsar_publish_service.py`
- 封装 export/publish bundle 生成
- 封装产品落库前的目录检查
### 7.3 建议新增 job types
- `TIMESERIES_ISCE2_STACK_RUN`
- `TIMESERIES_MINTPY_SBAS_RUN`
- `TIMESERIES_EXPORT_PUBLISH`
- `TIMESERIES_REGISTER_PRODUCT`
当前已有:
- `TIMESERIES_PREPARE`
- `TIMESERIES_STACK_PREP`
- `TIMESERIES_MATERIALIZE`
### 7.4 运行记录要求
`ps_timeseries_runs` 至少应稳定记录:
- `wsl_distro`
- `env_name`
- `python_path`
- `work_root`
- `publish_dir`
- `manifest_path`
- `reference_date`
- `stack_dates`
- `dem_path`
- `orbit_pool`
- `water_mask_mode`
- `step logs`
- `quality summary`
- `command snapshots`
目标不是“只知道跑过”,而是“知道当时用什么环境、什么 DEM、什么 orbit、怎么跑出来的”。
## 8. 结果目录与 catalog 策略
### 8.1 正式产品目录
正式产品目录统一使用:
- `PSINSAR_PRODUCT_DIR`
实验目录:
- `experiments/.../publish/...`
只作为验证来源,不应继续充当正式产品根目录。
### 8.2 推荐策略
第一阶段先支持两种接入方式:
1. `import_experiment_bundle`
- 将已有实验成功 bundle 导入正式产品目录
- 用于快速验证 catalog 和前端显示
2. `publish_from_workflow`
- 由 workflow 自动生成 bundle 并注册
- 作为正式闭环目标
### 8.3 catalog 设计要求
`psinsar_catalog_service` 建议补充:
- 单 manifest 增量注册
- 单产品刷新
- publish 目录健康检查
- run_id 到 product_id 的稳定关联
## 9. 前端设计
### 9.1 `ps_production`
第一阶段重点不是分析,而是运维可用:
- 显示 WSL/环境状态
- 显示 workflow 步骤状态
- 显示 stdout/stderr 日志位置
- 失败后支持步骤级重试
- 成功后跳转到产品详情
### 9.2 `ps_products`
第一阶段重点:
- list/filter
- 查看 manifest
- 查看 preview
- 手动 rebuild catalog
- 从 run 跳到 product
### 9.3 `psinsar_results`
第一阶段继续复用 catalog 只读视图即可。
### 9.4 `psinsar_analysis`
继续预留,不在当前阶段展开。
## 10. 稳定性约束
必须遵守以下边界:
- 不修改当前 D-InSAR 的默认 `ISCE2_PYTHON`
- 不把当前 D-InSAR 脚本改指向 `isce2_mintpy_v1`
- 不让业务层代码直接拼 `/mnt/z`
- 不把 `MintPy` 工作目录直接暴露为正式产品
- 不依赖交互式 `sudo`
## 11. 推荐实施顺序
### Phase A
先做“实验成果纳入系统”:
- 支持导入已有 experiment publish bundle
- 落到正式 `PSINSAR_PRODUCT_DIR`
- 让 catalog 和前端先可见
### Phase B
再做“完整 WSL2 自动执行闭环”:
- 接入 `run_isce2_stack`
- 接入 `run_mintpy_sbas`
- 接入 `export_publish_bundle`
- 接入 `register_psinsar_product`
### Phase C
最后做“生产级稳定化”:
- 失败重试
- 恢复执行
- 健康检查
- 环境快照
- provenance 补齐
## 12. 验收标准
达到以下条件后,才算 SBAS 正式接入完成:
1. 系统可从一个已有 `PS stack batch` 发起 SBAS 运行。
2. Worker 可在 `Ubuntu-24.04` 中自动驱动 ISCE2 stack。
3. Worker 可在 `isce2_mintpy_v1` 中自动驱动 MintPy。
4. 系统可自动生成 publish bundle。
5. bundle 可自动注册到 `psinsar` catalog。
6. 前端可查看运行状态、产品列表、preview 和 manifest。
7. 失败步骤可重试,且不会污染现有 D-InSAR 生产链。
## 13. 当前推荐
当前最稳的方向是:
- 继续以 `isce2_mintpy_v1` 作为 SBAS 主运行时
- 保留 `isce2` 与 `mintpy` 双环境桥接链路作为 fallback
- 先把 experiment publish bundle 纳入正式系统
- 再补自动执行后四步
这条路线最符合当前仓库状态,也最符合“稳定优先、不破坏现有 D-InSAR”的约束。
+49
View File
@@ -0,0 +1,49 @@
# Repository Guidelines
## Agent Behavior
- Before modifying any code or files (including config/scripts), explicitly ask the user for permission.
- Do not change the database initialization/check/generation flow used by `start_system.bat` and `scripts/start_app.ps1`; any database-related changes must preserve that startup logic.
- Keep documentation (especially `docs/DEPLOYMENT.md`) updated when introducing new changes or features.
- Any newly written or updated Chinese text in code/config/docs must be saved with UTF-8 encoding (no mojibake/garbled characters).
- For map overlay architecture decisions, prioritize long-term runtime stability and smoothness over short-term implementation speed.
- For source radar preview overlay, prefer backend pre-corrected/georeferenced cache generation (Scheme B) and keep frontend rendering lightweight; avoid heavy per-frame frontend geometric transforms as the primary approach.
## Project Structure & Module Organization
- `backend/`: FastAPI app and services. Core entrypoint is `backend/app/main.py`, router in `backend/app/api.py`, and domain logic in `backend/app/services/`.
- `backend/migrations/`: SQL migrations (including spatial functions used at startup).
- `backend/Point/` and `backend/colormaps/`: spatial inputs and colormap assets used by the backend.
- `frontend/`: React + Vite app. Source lives in `frontend/src/`, public assets in `frontend/public/`, and production build output in `frontend/dist/`.
- `image_cache/`: generated thumbnails. Filenames use `ID_{id}_{name}.webp`.
- `scripts/`, `start_system.bat`, `run_backend.py`: local helper scripts.
## Build, Test, and Development Commands
- Python runtime: use Conda env `InSAR` only. Prefer `D:\anaconda3\Scripts\conda.exe run -n InSAR <command>`; do not call system `python`.
- Backend dev server: `cd backend && D:\anaconda3\Scripts\conda.exe run -n InSAR python -m uvicorn app.main:app --reload` (runs on port 8000).
- Alternate backend start: `D:\anaconda3\Scripts\conda.exe run -n InSAR python run_backend.py` (wrapper used by docs/scripts).
- Frontend dev server: `cd frontend && npm install && npm run dev` (runs on port 5173).
- Frontend build: `cd frontend && npm run build`.
- Frontend lint: `cd frontend && npm run lint`.
## Coding Style & Naming Conventions
- Python: follow PEP 8 (4-space indentation). Keep service logic in `backend/app/services/` and API routes in `backend/app/api.py`.
- Frontend: use 2-space indentation in `.jsx` and `.css` to match existing files; React components use `PascalCase` filenames (e.g., `StatisticsDashboard.jsx`).
- Image cache naming: `ID_{id}_{name}.webp` is required for cache lookups.
- Linting: ESLint is configured in `frontend/eslint.config.js` (run via `npm run lint`).
## Testing Guidelines
- No dedicated test framework or test directory is present in this workspace. If you add tests, document the runner and add a `test` script in `frontend/package.json` or a backend test command.
- Prefer naming tests `test_*.py` (pytest-style) or `*.test.jsx` if a JS test runner is introduced.
## Commit & Pull Request Guidelines
- Git metadata is not available in this workspace, so commit message conventions cannot be inferred. Use your team standard (e.g., Conventional Commits) and keep messages concise.
- PRs should include: a short description, linked issue/ticket if applicable, screenshots for UI changes, and clear testing notes.
## Configuration & Environment Notes
- Database is PostgreSQL with PostGIS. Set `DATABASE_URL` (format: `postgresql+asyncpg://user:pass@host/dbname`).
- IDL automation requires a local IDL/ENVI install (Windows) and is managed in `backend/app/idl_service.py`.
- Frontend expects backend at `http://localhost:8000`; update CORS in the backend if ports change.
- Conda environment for this workspace: `InSAR` (Python 3.10). On this machine, Conda executable is `D:\anaconda3\Scripts\conda.exe`.
- For `scripts/start_app.ps1`, prefer Conda-native startup config in `.env`:
- `CONDA_EXE=D:\anaconda3\Scripts\conda.exe`
- `CONDA_ENV_NAME=InSAR`
- If `CONDA_ENV_NAME` is set, startup resolves the target env `python.exe` via Conda, then uses that interpreter for DB check/init/backend/worker.
+769
View File
@@ -0,0 +1,769 @@
# AI 分析模块重构设计文档
**版本**: v2.0
**日期**: 2026-03-02
**作者**: System Architect
---
## 1. 背景与目标
### 1.1 当前问题
- **架构混乱**: 同步/异步接口混用,用户体验不一致
- **Prompt 硬编码**: 无法灵活调整诊断策略
- **模型选择僵化**: 用户无法指定模型
- **结果存储不规范**: 诊断报告存在 `task.message`,无法检索和分析
- **缺少历史管理**: 无法查看、对比、导出历史诊断
### 1.2 设计目标
1. **统一异步架构**: 所有 AI 任务进队列,前端轮询状态
2. **配置化 Prompt**: 模板化管理,支持自定义
3. **灵活模型选择**: 用户可选模型,记住偏好
4. **规范化存储**: 独立表管理诊断记录
5. **完整历史管理**: 查询、筛选、导出、对比
---
## 2. 数据库设计
### 2.1 核心表:`ai_diagnosis`
存储所有 AI 诊断记录。
```sql
CREATE TABLE ai_diagnosis (
id SERIAL PRIMARY KEY,
-- 关联信息
result_id INTEGER NOT NULL REFERENCES dinsar_results(id) ON DELETE CASCADE,
task_id VARCHAR(50), -- 关联 system_tasks,用于追踪任务状态
-- 模型与配置
model_name VARCHAR(100) NOT NULL, -- 如 'qwen3-vl:8b'
prompt_template VARCHAR(50) NOT NULL, -- 'quick'/'standard'/'detailed'/'custom'
prompt_text TEXT, -- 实际使用的完整 prompt(用于审计)
-- 诊断结果
diagnosis_markdown TEXT NOT NULL, -- Markdown 格式的诊断报告
risk_level VARCHAR(20), -- 'LOW'/'MEDIUM'/'HIGH'/NULL
confidence_score FLOAT, -- 0.0-1.0,模型自评置信度(可选)
-- 上下文信息(快照,避免关联查询)
result_name VARCHAR(255),
date_range VARCHAR(100), -- 如 '20240101-20240115'
quality_score FLOAT, -- 当时的 ai_score
hazards_found INTEGER DEFAULT 0, -- 覆盖范围内的隐患点数量
hazards_snapshot JSONB, -- 隐患点详情快照 [{name, type, location}]
-- 元数据
created_at TIMESTAMP DEFAULT NOW(),
duration_seconds FLOAT, -- 诊断耗时
error_message TEXT, -- 如果失败,记录错误
-- 索引
INDEX idx_result_id (result_id),
INDEX idx_created_at (created_at DESC),
INDEX idx_risk_level (risk_level)
);
```
**设计要点**:
- `prompt_text` 存储实际 prompt,便于审计和复现
- `hazards_snapshot` 用 JSONB 存快照,避免隐患点被删除后无法回溯
- `duration_seconds` 用于性能分析
- `error_message` 支持失败记录(不删除,便于调试)
### 2.2 配置表:`ai_prompt_templates`(可选)
如果需要 UI 管理 Prompt 模板,可建此表。否则用配置文件即可。
```sql
CREATE TABLE ai_prompt_templates (
id SERIAL PRIMARY KEY,
name VARCHAR(50) UNIQUE NOT NULL, -- 'quick'/'standard'/'detailed'
display_name_zh VARCHAR(100),
display_name_en VARCHAR(100),
template_text TEXT NOT NULL,
is_system BOOLEAN DEFAULT FALSE, -- 系统内置模板不可删除
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
```
**初始数据**:
```sql
INSERT INTO ai_prompt_templates (name, display_name_zh, display_name_en, template_text, is_system) VALUES
('quick', '快速诊断', 'Quick Diagnosis', '你是 InSAR 专家,用 200 字简述...', TRUE),
('standard', '标准诊断', 'Standard Diagnosis', '你是资深专家,按 4 步分析...', TRUE),
('detailed', '详细诊断', 'Detailed Diagnosis', '深度分析,包含地形、历史对比...', TRUE);
```
### 2.3 用户偏好表:`user_ai_preferences`(可选)
存储用户的 AI 偏好设置。
```sql
CREATE TABLE user_ai_preferences (
user_id INTEGER PRIMARY KEY REFERENCES users(id),
preferred_model VARCHAR(100), -- 如 'qwen3-vl:8b'
default_template VARCHAR(50), -- 'standard'
auto_diagnose BOOLEAN DEFAULT FALSE, -- 新结果自动诊断
updated_at TIMESTAMP DEFAULT NOW()
);
```
---
## 3. 后端 API 设计
### 3.1 路由结构
新建 `backend/app/routers/ai_diagnosis.py`,遵循 RESTful 规范。
#### 3.1.1 诊断记录管理
**列表查询**
```http
GET /api/ai-diagnosis?page=1&limit=20&result_id=123&risk_level=HIGH&sort=-created_at
```
**Query 参数**:
- `page`, `limit`: 分页
- `result_id`: 筛选特定结果的诊断
- `risk_level`: 筛选风险等级(`LOW`/`MEDIUM`/`HIGH`)
- `model_name`: 筛选模型
- `date_from`, `date_to`: 时间范围
- `sort`: 排序字段(`-created_at` 表示倒序)
**响应**:
```json
{
"total": 156,
"page": 1,
"limit": 20,
"items": [
{
"id": 42,
"result_id": 123,
"result_name": "Task_001_20240101_20240115",
"model_name": "qwen3-vl:8b",
"prompt_template": "standard",
"risk_level": "HIGH",
"hazards_found": 3,
"created_at": "2026-03-02T10:30:00Z",
"duration_seconds": 45.2
}
]
}
```
**详情查询**
```http
GET /api/ai-diagnosis/{id}
```
**响��**:
```json
{
"id": 42,
"result_id": 123,
"result_name": "Task_001_20240101_20240115",
"model_name": "qwen3-vl:8b",
"prompt_template": "standard",
"prompt_text": "你是一位拥有 20 年经验...",
"diagnosis_markdown": "## 形态学分析\n\n观察到...",
"risk_level": "HIGH",
"confidence_score": 0.85,
"hazards_found": 3,
"hazards_snapshot": [
{"name": "XX滑坡", "type": "滑坡", "location": "XX县"}
],
"quality_score": 0.72,
"created_at": "2026-03-02T10:30:00Z",
"duration_seconds": 45.2
}
```
**创建诊断任务**
```http
POST /api/ai-diagnosis
Content-Type: application/json
{
"result_id": 123,
"model_name": "qwen3-vl:8b", // 可选,不传则自动选择
"prompt_template": "standard" // 'quick'/'standard'/'detailed'/'custom'
"custom_prompt": "..." // 仅当 template='custom' 时需要
}
```
**响应**:
```json
{
"diagnosis_id": 42, // 预创建的记录 ID(状态为 PENDING)
"task_id": "task_abc123",
"message": "诊断任务已进入队列"
}
```
**删除记录**
```http
DELETE /api/ai-diagnosis/{id}
```
**批量导出**
```http
POST /api/ai-diagnosis/export
Content-Type: application/json
{
"ids": [42, 43, 44],
"format": "markdown" // 'markdown'/'pdf'/'json'
}
```
**响应**:返回文件流或下载链接。
#### 3.1.2 模型管理
**获取可用模型**
```http
GET /api/ai/models
```
**响应**:
```json
{
"ollama_online": true,
"models": [
{
"name": "qwen3-vl:8b",
"size": "8.5 GB",
"modified_at": "2026-03-01T12:00:00Z",
"capabilities": ["vision", "text"],
"recommended": true
},
{
"name": "qwen2-vl:72b",
"size": "72 GB",
"capabilities": ["vision", "text"]
}
],
"default_model": "qwen3-vl:8b"
}
```
**预热模型**
```http
POST /api/ai/models/{model_name}/warmup
```
#### 3.1.3 Prompt 模板管理
**列表**
```http
GET /api/ai/prompt-templates
```
**响应**:
```json
{
"templates": [
{
"name": "quick",
"display_name": "快速诊断",
"is_system": true
},
{
"name": "custom_001",
"display_name": "我的自定义模板",
"is_system": false
}
]
}
```
**详情**
```http
GET /api/ai/prompt-templates/{name}
```
**创建/更新**(仅非系统模板)
```http
POST /api/ai/prompt-templates
PUT /api/ai/prompt-templates/{name}
{
"name": "custom_001",
"display_name": "我的模板",
"template_text": "..."
}
```
---
## 4. 任务处理流程
### 4.1 异步任务架构
**流程图**:
```
用户点击"诊断"
↓
前端 POST /api/ai-diagnosis
↓
后端创建 diagnosis 记录(状态 PENDING)+ system_task + system_job
↓
返回 diagnosis_id + task_id
↓
前端轮询 GET /api/tasks/{task_id}
↓
job_worker 执行 _handle_ai_analyze
↓
调用 Ollama VLM
↓
更新 diagnosis 记录(状态 COMPLETED,写入 markdown)
↓
更新 system_task(状态 COMPLETED)
↓
前端检测到完成,跳转到诊断详情页
```
### 4.2 Handler 改造
`backend/app/services/job_handlers.py` 中的 `_handle_ai_analyze` 改为:
```python
async def _handle_ai_analyze(job: SystemJobORM) -> None:
payload = job.payload or {}
diagnosis_id = payload.get("diagnosis_id")
if not diagnosis_id:
raise ValueError("AI_ANALYZE requires diagnosis_id")
async with AsyncSessionLocal() as db:
# 1. 查询 diagnosis 记录
diag = await db.get(AiDiagnosisORM, diagnosis_id)
if not diag:
raise ValueError(f"Diagnosis {diagnosis_id} not found")
# 2. 查询关联的 result
result = await db.get(DinsarResultORM, diag.result_id)
# 3. 查询隐患点(PostGIS)
hazards = await db.execute(
select(HazardPointORM).where(ST_Covers(result.geom, HazardPointORM.geom))
)
hazards = hazards.scalars().all()
# 4. 准备图片
img_path = data_service.get_dinsar_cache_path(result.id, result.name)
img_base64 = _load_image_as_base64(img_path)
# 5. 构建 prompt
prompt = _build_prompt(diag.prompt_template, diag.prompt_text, result, hazards)
# 6. 调用 VLM
start_time = time.time()
try:
analysis = await analyze_map_with_vlm(
images_base64=[img_base64],
prompt=prompt,
model_name=diag.model_name
)
duration = time.time() - start_time
# 7. 解析风险等级(从 Markdown 中提取)
risk_level = _extract_risk_level(analysis)
# 8. 更新 diagnosis 记录
diag.diagnosis_markdown = analysis
diag.risk_level = risk_level
diag.hazards_found = len(hazards)
diag.hazards_snapshot = [
{"name": h.hazard_name, "type": h.hazard_type, "location": f"{h.city}{h.county}"}
for h in hazards
]
diag.duration_seconds = duration
await db.commit()
# 9. 更新 task 状态
await task_service.update_task(
job.task_id,
status="COMPLETED",
message=f"诊断完成,风险等级: {risk_level}",
progress=100
)
except Exception as e:
diag.error_message = str(e)
await db.commit()
await task_service.update_task(
job.task_id,
status="FAILED",
message=f"诊断失败: {str(e)}"
)
```
---
## 5. 前端设计
### 5.1 组件结构
```
src/
├── panels/
│ └── AiAnalysisPanel.jsx # 新增:AI 分析主面板
├── components/
│ ├── AiDiagnosisTable.jsx # 诊断历史表格
│ ├── AiDiagnosisDetailModal.jsx # 诊断详情 Modal
│ ├── AiModelSelector.jsx # 模型选择器
│ ├── AiPromptTemplateEditor.jsx # Prompt 模板编辑器
│ └── AiQuickDiagnoseCard.jsx # 快速诊断卡片
├── api/
│ └── aiDiagnosis.js # API 封装
└── hooks/
└── useAiDiagnosis.js # 自定义 Hook
```
### 5.2 AiAnalysisPanel 布局
```jsx
<div className="ai-analysis-panel">
{/* 顶部状态卡片 */}
<div className="ai-status-cards">
<OllamaStatusCard />
<ModelSelectorCard />
</div>
{/* 快速诊断 */}
<AiQuickDiagnoseCard />
{/* 诊断历史 */}
<div className="ai-diagnosis-history">
<div className="history-toolbar">
<input placeholder="搜索结果名称..." />
<select>
<option>全部风险等级</option>
<option>高风险</option>
<option>中风险</option>
<option>低风险</option>
</select>
<button>导出选中</button>
</div>
<AiDiagnosisTable />
</div>
</div>
```
### 5.3 核心交互流程
**快速诊断**:
1. 用户在下拉框选择 D-InSAR 结果
2. 选择诊断模式(快速/标准/详细)
3. 点击"开始诊断"
4. 前端 POST `/api/ai-diagnosis`,获得 `task_id`
5. 显示进度条,轮询 `/api/tasks/{task_id}`
6. 完成后自动打开详情 Modal
**查看历史**:
1. 表格显示所有诊断记录(分页)
2. 点击行打开详情 Modal
3. Modal 显示完整 Markdown 报告(支持复制、导出)
**模型管理**:
1. 顶部卡片显示当前选中模型
2. 点击"切换模型"打开下拉列表
3. 选择后保存到 localStorage(或用户配置)
---
## 6. 配置管理
### 6.1 Prompt 模板文件
`backend/app/ai_prompts.py`:
```python
PROMPT_TEMPLATES = {
'quick': {
'name_zh': '快速诊断',
'name_en': 'Quick Diagnosis',
'template': """
你是 InSAR 专家。请用 200 字简述以下形变图的关键发现:
- 是否存在明显形变区?
- 与已知隐患点是否吻合?
- 风险等级(低/中/高)?
背景:{context}
""",
},
'standard': {
'name_zh': '标准诊断',
'name_en': 'Standard Diagnosis',
'template': """
你是拥有 20 年经验的 InSAR 专家。请按以下步骤分析:
1. 形态学分析
2. 时空一致性
3. 风险演化预测
4. 综合风险评级
背景:{context}
隐患点:{hazards}
""",
},
'detailed': {
'name_zh': '详细诊断',
'name_en': 'Detailed Diagnosis',
'template': """
深度分析,包含:
- 地形地貌分析
- 历史形变趋势
- 潜在触发因素
- 监测建议
背景:{context}
""",
},
}
def build_prompt(template_name: str, context: dict) -> str:
template = PROMPT_TEMPLATES.get(template_name, PROMPT_TEMPLATES['standard'])
return template['template'].format(**context)
```
### 6.2 环境变量
`.env` 新增:
```bash
# Ollama 配置
OLLAMA_API_URL=http://127.0.0.1:11434/api/generate
OLLAMA_TIMEOUT=600
OLLAMA_DEFAULT_MODEL=qwen3-vl:8b
# AI 诊断配置
AI_DIAGNOSIS_AUTO_RETRY=true
AI_DIAGNOSIS_MAX_RETRIES=2
AI_DIAGNOSIS_STREAM_OUTPUT=false # 未来支持流式
```
---
## 7. 成熟方案参考
### 7.1 Prompt 管理
参考 **LangChain PromptTemplate**:
- 支持变量插值 `{variable}`
- 支持条件渲染 `{% if condition %}`
- 支持模板继承
可引入 `Jinja2` 作为模板引擎:
```python
from jinja2 import Template
template = Template(PROMPT_TEMPLATES['standard']['template'])
prompt = template.render(context=context, hazards=hazards)
```
### 7.2 任务队列
当前使用自研的 `job_queue_service`,可考虑迁移到:
- **Celery** + Redis:成熟的分布式任务队列
- **Dramatiq**:轻量级替代方案
- **ARQ**:基于 asyncio 的现代方案
### 7.3 诊断报告导出
- **Markdown → PDF**:使用 `weasyprint` 或 `pdfkit`
- **Markdown → Word**:使用 `python-docx` + `markdown` 解析
- **模板化报告**:使用 `Jinja2` + HTML 模板
### 7.4 前端状态管理
如果 AI 功能复杂度增加,可考虑:
- **Zustand**(已使用):继续用 `useAiStore`
- **React Query**:管理服务端状态(诊断列表、模型列表)
---
## 8. 迁移计划
### 8.1 数据迁移
**Step 1**:创建新表
```bash
alembic revision -m "add_ai_diagnosis_table"
alembic upgrade head
```
**Step 2**:迁移历史数据(可选)
从 `system_tasks` 的 `message` 字段提取历史诊断,写入新表:
```python
# 迁移脚本
async def migrate_old_diagnoses():
tasks = await db.execute(
select(SystemTaskORM).where(
SystemTaskORM.task_type == 'AI_ANALYZE',
SystemTaskORM.status == 'COMPLETED'
)
)
for task in tasks.scalars():
msg = json.loads(task.message)
diagnosis = AiDiagnosisORM(
result_id=msg['result_id'],
diagnosis_markdown=msg['analysis'],
created_at=task.completed_at,
...
)
db.add(diagnosis)
await db.commit()
```
### 8.2 API 兼容性
**向后兼容**:
- 保留旧接口 `/ai/analyze-result/{id}` 3 个月
- 内部重定向到新接口
- 响应格式保持一致
**废弃通知**:
```json
{
"message": "诊断任务已进入队列",
"task_id": "...",
"diagnosis_id": 42,
"_deprecated": "此接口将在 2026-06-01 废弃,请使用 POST /api/ai-diagnosis"
}
```
---
## 9. 性能优化
### 9.1 缓存策略
- **模型列表缓存**:`GET /api/ai/models` 结果缓存 5 分钟(Redis)
- **Prompt 模板缓存**:启动时加载到内存
- **诊断列表分页**:使用游标分页(`cursor-based`)而非 offset
### 9.2 并发控制
- **Ollama 并发限制**:同时最多 2 个 VLM 任务(避免显存溢出)
- **任务优先级**:用户手动触发 > 自动诊断
### 9.3 超时与重试
```python
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=4, max=60),
retry=retry_if_exception_type(httpx.TimeoutException)
)
async def call_ollama_with_retry(...):
...
```
---
## 10. 安全考虑
### 10.1 输入验证
- **Prompt 注入防护**:限制自定义 prompt 长度(8000 字符)
- **模型名称白名单**:只允许已知模型
- **结果 ID 权限检查**:确保用户有权访问该结果
### 10.2 输出过滤
- **敏感信息脱敏**:诊断报告中的坐标、地址模糊化
- **XSS 防护**:Markdown 渲染时使用 `DOMPurify`
---
## 11. 监控与日志
### 11.1 关键指标
- **诊断成功率**:`completed / (completed + failed)`
- **平均耗时**:按模型、模板统计
- **模型使用分布**:哪个模型最受欢迎
- **风险等级分布**:高/中/低风险占比
### 11.2 日志记录
```python
logger.info(
"AI diagnosis completed",
extra={
"diagnosis_id": diag.id,
"result_id": diag.result_id,
"model": diag.model_name,
"duration": diag.duration_seconds,
"risk_level": diag.risk_level,
}
)
```
---
## 12. 未来扩展
### 12.1 多模态输入
- 支持上传多张图片(时序对比)
- 支持附加文本描述(用户观察)
### 12.2 对比分析
- 同一结果的多次诊断对比
- 不同模型的诊断结果对比
### 12.3 自动化工作流
- 新结果自动触发诊断
- 高风险自动发送邮件/钉钉通知
### 12.4 Fine-tuning
- 收集用户反馈(诊断是否准确)
- 定期 fine-tune 模型
---
## 13. 总结
### 13.1 核心改进
| 维度 | 当前 | 重构后 |
|------|------|--------|
| 架构 | 同步/异步混用 | 统一异步任务 |
| Prompt | 硬编码 | 模板化 + 可自定义 |
| 模型 | 自动选择 | 用户可选 + 偏好记忆 |
| 存储 | task.message | 独立表 + 完整字段 |
| 历史 | 无法查询 | 完整 CRUD + 导出 |
### 13.2 实施优先级
**P0(必须)**:
- 创建 `ai_diagnosis` 表
- 改造 `_handle_ai_analyze`
- 实现基础 CRUD API
- 创建 `AiAnalysisPanel` 前端面板
**P1(重要)**:
- Prompt 模板管理
- 模型选择器
- 诊断详情 Modal
- 导出功能
**P2(可选)**:
- 用户偏好保存
- 对比分析
- 自动化工作流
---
**文档结束**
@@ -0,0 +1,389 @@
# 前端锁定问题分析(2026-03-05)
## 问题描述
用户报告在执行以下操作时前端没有锁定:
1. D-InSAR 结果扫描
2. 数据分发(复制任务)
## 根本原因分析
### 问题 1:扫描任务前端未锁定
**原因**:任务执行太快(< 3 秒),前端轮询间隔(3 秒)来不及捕获。
**证据**:
```
[性能] Footprint 提取耗时: 1622.1ms (文件: Task_20250120_20250217_2_geo_disp)
```
单个文件处理只需 1.6 秒,如果只有少量文件,整个扫描任务可能在 2-3 秒内完成。
**前端轮询逻辑**:
```javascript
// useGlobalTaskControl.js
useEffect(() => {
const interval = setInterval(() => {
syncActiveTasks(); // 每 3 秒轮询一次
}, 3000);
return () => clearInterval(interval);
}, []);
```
**时间线**:
```
T=0s: 用户点击"扫描"按钮
T=0.1s: 后端创建 SystemTask (status=PENDING)
T=0.2s: job_worker 开始执行,更新为 RUNNING
T=2.0s: 扫描完成,更新为 COMPLETED
T=3.0s: 前端第一次轮询,任务已经完成
```
**结论**:前端来不及捕获 RUNNING 状态,直接看到 COMPLETED。
---
### 问题 2:数据分发任务前端未锁定
**根本原因**:`DataCopierPanel` 没有调用全局锁定机制。
**证据**:
1. **App.jsx 中的调用对比**:
```javascript
// IDLAutomationPanel - 有 onJobQueued 回调 ✅
<IDLAutomationPanel
apiEndpoint={apiClient.defaults.baseURL}
readOnly={isReadOnlyUser}
onJobQueued={(taskId) => handleTaskStart(taskId, '任务已入队,等待处理...')}
/>
// DataMonitorPanel - 有 onTaskStart 回调 ✅
<DataMonitorPanel
apiEndpoint={apiClient.defaults.baseURL}
onTaskStart={handleTaskStart}
readOnly={isReadOnlyUser}
/>
// DataCopierPanel - 没有任何回调 ❌
<DataCopierPanel
apiEndpoint={apiClient.defaults.baseURL}
readOnly={isReadOnlyUser}
/>
```
2. **DataCopierPanel 内部逻辑**:
```javascript
// DataCopierPanel.jsx:97-133
const handleStartCopy = async () => {
// ... 验证逻辑 ...
setIsUploading(true);
setLogs([]);
setStatus('RUNNING'); // 只更新组件内部状态
try {
const response = await axios.post(endpoint, {
batch_id: selectedBatchId,
dest_dir: destDir,
copy_statuses: copyStatuses,
}, { withCredentials: true });
setTaskId(response.data.task_id); // 只保存 task_id
// ❌ 没有调用 onJobQueued 或 onTaskStart
} catch (error) {
// ... 错误处理 ...
} finally {
setIsUploading(false);
}
};
```
3. **组件自己管理状态**:
`DataCopierPanel` 使用自己的状态管理(`status`、`taskId`、`logs`),通过轮询 `/tools/copy-status/${taskId}` 获取进度,**完全独立于全局任务控制系统**。
```javascript
// DataCopierPanel.jsx:82-95
const fetchLogs = async () => {
if (!taskId) return;
try {
const response = await axios.get(`${apiEndpoint}/tools/copy-status/${taskId}`, { withCredentials: true });
setLogs(response.data.logs);
const nextStatus = normalizeStatus(response.data.status);
if (nextStatus && nextStatus !== 'UNKNOWN') {
setStatus(nextStatus); // 只更新组件内部状态
}
} catch (error) {
console.error('Failed to load logs:', error);
}
};
```
**结论**:`DataCopierPanel` 是一个独立的任务管理系统,不与全局锁定机制集成。
---
## 架构对比
### 全局锁定机制(IDLAutomationPanel、DataMonitorPanel)
```
用户操作 → 组件调用 API → 后端创建 SystemTask
↓
组件调用 onJobQueued(taskId)
↓
App.jsx 调用 handleTaskStart(taskId)
↓
setPendingTaskIds([...prev, taskId])
↓
useGlobalTaskControl 轮询 /tasks/active
↓
检测到 RUNNING 任务 → setIsGlobalLocked(true)
↓
任务完成 → setIsGlobalLocked(false)
```
### 独立任务管理(DataCopierPanel)
```
用户操作 → 组件调用 API → 后端创建 SystemTask
↓
组件保存 taskId(内部状态)
↓
组件轮询 /tools/copy-status/${taskId}
↓
更新组件内部状态(status、logs)
↓
❌ 不触发全局锁定
```
---
## 影响评估
### 扫描任务未锁定
**影响等级**:🟢 低
**原因**:
- 扫描任务通常很快(< 3 秒)
- 扫描是只读操作,不会修改数据
- 即使用户在扫描时操作,也不太可能造成冲突
**是否需要修复**:可选
- 如果希望用户明确感知到扫描正在进行,可以修复
- 如果接受"快速任务不锁定"的行为,可以不修复
---
### 数据分发任务未锁定
**影响等级**:🟡 中等
**原因**:
- 数据分发任务可能耗时较长(几分钟到几十分钟)
- 用户可能在分发过程中启动其他任务
- 可能导致资源竞争(磁盘 I/O、网络带宽)
**潜在问题**:
1. 用户可能同时启动多个分发任务,导致磁盘 I/O 饱和
2. 用户可能在分发过程中启动 ENVI 工作流,导致系统卡顿
3. 用户可能不知道分发任务正在后台运行
**是否需要修复**:建议修复
- 数据分发是长时间运行的任务
- 应该与全局锁定机制集成
- 提升用户体验和系统稳定性
---
## 修复方案
### 方案 A:集成到全局锁定机制(推荐)
**修改文件**:
1. `App.jsx`
2. `DataCopierPanel.jsx`
**步骤**:
1. **在 App.jsx 中传递回调**:
```javascript
{leftPanelTab === 'copier' && (
<div className="panel-content" style={{ flex: '1 1 auto', padding: 0, overflow: 'hidden' }}>
<DataCopierPanel
apiEndpoint={apiClient.defaults.baseURL}
readOnly={isReadOnlyUser}
onJobQueued={(taskId) => handleTaskStart(taskId, '数据分发任务已入队')} // 添加此行
/>
</div>
)}
```
2. **在 DataCopierPanel.jsx 中接收并调用回调**:
```javascript
// 修改组件签名
const DataCopierPanel = ({ apiEndpoint, readOnly = false, onJobQueued }) => {
// ... 现有代码 ...
const handleStartCopy = async () => {
// ... 验证逻辑 ...
setIsUploading(true);
setLogs([]);
setStatus('RUNNING');
try {
const response = await axios.post(endpoint, {
batch_id: selectedBatchId,
dest_dir: destDir,
copy_statuses: copyStatuses,
}, { withCredentials: true });
const taskId = response.data.task_id;
setTaskId(taskId);
// 调用全局锁定回调
if (onJobQueued) {
onJobQueued(taskId);
}
} catch (error) {
// ... 错误处理 ...
} finally {
setIsUploading(false);
}
};
};
```
**优点**:
- 与现有架构一致
- 最小改动
- 复用全局锁定机制
**缺点**:
- 组件仍然保留自己的状态管理(有一定冗余)
---
### 方案 B:完全重构为全局任务管理
**修改范围**:大
**步骤**:
1. 移除 `DataCopierPanel` 的内部状态管理
2. 使用全局 `activeTasks` 和 `ActiveTasksOverlay` 显示进度
3. 移除组件内的轮询逻辑
**优点**:
- 架构统一
- 减少代码冗余
**缺点**:
- 改动较大
- 可能影响现有功能
- 需要充分测试
---
### 方案 C:保持现状,添加警告提示
**修改文件**:`DataCopierPanel.jsx`
**步骤**:
在任务运行时显示警告提示:
```javascript
{status === 'RUNNING' && (
<div className="warning-banner">
⚠️ 数据分发任务正在后台运行,请勿关闭浏览器或启动其他耗时任务
</div>
)}
```
**优点**:
- 改动最小
- 不影响现有架构
**缺点**:
- 不解决根本问题
- 用户仍可能启动冲突任务
---
## 扫描任务修复方案(可选)
### 方案 A:前端立即锁定
在提交扫描任务后立即锁定,不等待轮询:
```javascript
// DataManagementPanel.jsx
const handleScanDinsarResults = async () => {
try {
// 立即锁定前端
if (onTaskStart) {
onTaskStart(null, 'SCAN_DINSAR', '正在扫描 D-InSAR 结果...');
}
const response = await scanDinsarResults(selectedDirs);
const taskId = response.data.task_id;
// 更新任务 ID
if (onTaskStart) {
onTaskStart(taskId, 'SCAN_DINSAR', '正在扫描 D-InSAR 结果...');
}
addLog('info', `扫描任务已提交: ${taskId}`);
} catch (error) {
// 解锁前端
setIsGlobalLocked(false);
addLog('error', `扫描失败: ${error.message}`);
}
};
```
### 方案 B:后端添加最小执行时间
```python
# job_handlers.py
async def _handle_scan_dinsar(job: SystemJobORM) -> None:
start_time = time.time()
# ... 执行扫描 ...
# 确保任务至少运行 2 秒
elapsed = time.time() - start_time
if elapsed < 2.0:
await asyncio.sleep(2.0 - elapsed)
```
---
## 推荐修复顺序
1. **优先修复数据分发任务**(方案 A)
- 影响较大
- 修改简单
- 风险低
2. **可选修复扫描任务**(方案 A)
- 影响较小
- 提升用户体验
- 风险低
3. **长期考虑重构**(方案 B)
- 统一架构
- 减少冗余
- 需要充分测试
---
## 总结
- **扫描任务未锁定**:任务太快,轮询来不及捕获(影响小,可选修复)
- **数据分发任务未锁定**:组件独立管理状态,未集成全局锁定(影响中,建议修复)
- **推荐方案**:为 `DataCopierPanel` 添加 `onJobQueued` 回调,集成到全局锁定机制
+150
View File
@@ -0,0 +1,150 @@
# IDL/ENVI + 系统集成技术备忘录
> 更新日期: 2026-02-23
## 一、架构概览
```
前端 (React) → FastAPI → Job Queue (PostgreSQL) → Job Worker → 子进程 (envi_runner_cli) → envipyengine → ENVI/SARscape
```
- 前端提交任务 → 创建 Task + Job 记录 → Worker 领取 Job → 启动独立 Python 子进程执行 ENVI 工作流
- 子进程通过 progress JSON 文件向父进程报告进度
- 父进程监控子进程存活状态,同时通过 `_task_keepalive` 协程每 30 秒刷新 Task 的 `updated_at` 防止僵尸检测误杀
- 前端有两层锁: IDLAutomationPanel 内的黄色横幅 + App.jsx 的全局蓝色遮罩
## 二、已踩的坑与解决方案
### 1. subprocess.PIPE 缓冲区死锁
**现象**: 子进程挂起,不再输出
**原因**: Windows 上 `subprocess.Popen(stdout=PIPE)` 的管道缓冲区约 4KB,envipyengine 输出超过后阻塞
**方案**: 使用 `tempfile.mkstemp()` 创建临时文件接收 stdout/stderr,子进程退出后读取
### 2. Windows TemporaryFile 句柄不可继承
**现象**: `Popen` 无法写入 `tempfile.TemporaryFile` 创建的文件
**原因**: Windows 上 `TemporaryFile` 创建的句柄默认不可被子进程继承
**方案**: 改用 `tempfile.mkstemp()` + `os.close(fd)` + 手动 `os.unlink()` 清理
### 3. envipyengine 提前返回(文件未写完)
**现象**: `task.execute()` 返回后,ENVI 仍在写输出文件(特别是 step 6 的 `_rsp_disp` 文件)
**原因**: envipyengine 在 taskengine 返回后立即返回 Python,但 ENVI 后台仍在写大文件
**方案**:
- 在 step 6 后(无论成功/异常)调用 `_wait_for_disp_stable()` 等待 `*_rsp_disp` 文件出现并稳定
- 最后调用 `_wait_files_stable()` 确认所有文件写完
- 稳定判定: 连续 3 轮(每轮 15 秒)文件大小不变
### 4. envipyengine 报错但文件已生成
**现象**: `task.execute()` 抛出 "outputs not generated" 异常,但实际文件已正确生成
**原因**: envipyengine 的输出验证逻辑与 SARscape 实际行为不一致(特别是 step 3 Orbital Trend Removal)
**方案**: except 块中扫描输出目录,按文件名模式(如 `*ISARRRPF*.sml`)查找已生成的文件,构建 SARSCAPEDATA 字典继续后续步骤
### 5. envipyengine 永久挂起
**现象**: `task.execute()` 永远不返回,即使 ENVI 已完成处理
**原因**: envipyengine 内部的 taskengine 进程未正确退出
**方案**: 使用 `ThreadPoolExecutor` 包装 `task.execute()`,设置超时 `ENVI_TASK_TIMEOUT_SECONDS=14400`(4小时),超时后抛出异常,由文件扫描逻辑兜底
### 6. 子进程文件活动监控误杀
**现象**: 前端提前解锁,ENVI 进程仍在运行
**原因**: `job_handlers.py` 的监控循环检测到进度文件和输出目录超过 `ENVI_FILE_STALE_SECONDS` 无变化,kill 了子进程。某些步骤(如 Phase Unwrapping)长时间在内存中计算不写文件
**方案**: 将 `ENVI_FILE_STALE_SECONDS` 从 600 增大到 14400,与任务超时一致
### 7. Task 僵尸检测误杀(根本原因)
**现象**: 前端在 ENVI 处理过程中解锁
**原因**: `task_service.get_active_tasks()` 内置僵尸检测——每次前端轮询时检查 RUNNING 任务的 `updated_at`,超过 `TASK_TIMEOUT_MINUTES=60` 分钟未更新则标记为 FAILED。子进程运行期间 handler 没有更新 task 的 `updated_at`,而单个 step 可能超过 60 分钟
**方案**: 在 `_run_envi_workflow_job` 中添加 `_task_keepalive` 协程,每 30 秒读取进度文件并调用 `task_service.update_task()` 刷新 `updated_at`,同时更新进度百分比(step 映射到 10%-90%)和步骤信息
### 8. 强制解锁按钮被全局遮罩遮挡
**现象**: 强制解锁按钮写在 IDLAutomationPanel 的黄色横幅里,但全局蓝色遮罩(`App.jsx` 的 `global-task-overlay`)盖住了整个页面,用户无法点击
**原因**: 系统有两层锁——IDLAutomationPanel 内部的 `isLocked` 控制按钮禁用,App.jsx 的 `isGlobalLocked` 控制全屏蓝色遮罩。强制解锁只加在了内层
**方案**: 在 `App.jsx` 的全局遮罩上添加「管理员强制解锁」按钮(仅管理员可见),点击后展开密码输入框,确认后取消所有活跃任务并解锁。同时保留 IDLAutomationPanel 内的强制解锁作为备用
## 三、当前超时/保护参数
| 参数 | 值 | 位置 | 作用 |
|------|-----|------|------|
| `ENVI_TASK_TIMEOUT_SECONDS` | 14400 (4h) | `.env` | 单步 `task.execute()` 最大等待 |
| `ENVI_FILE_STALE_SECONDS` | 14400 (4h) | `.env` | 子进程无文件活动的 kill 阈值 |
| `JOB_WORKER_STALE_RUNNING_SECONDS` | 7200 (2h) | `.env` | Job 心跳超时(每 5s 更新) |
| `TASK_TIMEOUT_MINUTES` | 60 | `task_service.py` | 僵尸任务检测阈值(keepalive 每 30s 刷新) |
| `ENVI_STABILITY_CHECK_INTERVAL` | 15s | `.env` | 文件稳定检查间隔 |
| `ENVI_STABILITY_ROUNDS` | 3 | `.env` | 连续稳定轮数 |
| `ENVI_STABILITY_MAX_WAIT` | 3600 (1h) | `.env` | 文件稳定等待上限 |
| `IDL_JOB_MAX_ATTEMPTS` | 1 | `.env` | 不重试,失败即终止 |
## 四、自定义 D-InSAR 6 步流程
| 步骤 | Task 名称 | 典型耗时 | 已知问题 |
|------|-----------|----------|----------|
| 1. 干涉图生成 | `SARsInSARInterferogramGeneration` | ~900s | 无 |
| 2. 滤波+相干性 | `SARsInSARFilterAndCoherence` | ~400s | 无 |
| 3. 轨道趋势去除 | `SARsInSARRemoveResidualPhaseFrequency` | ~65s | 报 "outputs not generated" 但文件已生成 |
| 4. 相位解缠 | `SARsInSARPhaseUnwrapping` | ~1000s | 无 |
| 5a. GCP 生成 | Python (rasterio+geopandas) | <1s | 无 |
| 5b. 精化再平化 | `SARsInSARRefinementAndReflattening` | ~30s | 无 |
| 6. 位移+地理编码 | `SARsInSARPhaseToDisplacement` | ~7600s | envipyengine 可能提前返回或挂起;第二个 Task 对曾出现 "SARscape process unexpectedly terminated" |
单个 Task 对总耗时约 2.8 小时(测试数据)。两个 Task 对串行处理约 3.8 小时。
## 五、前端锁机制
### 两层锁结构
1. **全局蓝色遮罩** (`App.jsx` → `global-task-overlay`)
- `App.jsx` 每 3 秒轮询 `GET /tasks/active`
- 有任何活跃任务 → `isGlobalLocked=true` → 全屏蓝色遮罩覆盖整个 UI
- 显示所有活跃任务的名称、进度条、状态消息
- 管理员可见「管理员强制解锁」按钮 → 输入密码 → 取消所有任务 → 遮罩消失
- 任务完成后自动刷新页面数据
2. **IDLAutomationPanel 内部锁** (`IDLAutomationPanel.jsx`)
- 每 10 秒轮询 `GET /tasks/active`,查找 IDL 类型任务
- 有活跃 IDL 任务 → `isLocked=true` → 按钮禁用 + 黄色横幅
- 横幅内也有强制解锁按钮(作为备用入口)
### 强制解锁 API
- `POST /tasks/{task_id}/force-cancel`
- 需要管理员 JWT + 请求体中的密码双重验证
- 将任务标记为 CANCELLED,前端下次轮询时解锁
## 六、关键文件清单
| 文件 | 职责 |
|------|------|
| `backend/app/services/envi_service.py` | ENVI 工作流核心(6 步流程、GCP 生成、文件稳定等待) |
| `backend/app/services/envi_runner_cli.py` | 子进程入口(加载 .env、调用 run_workflow) |
| `backend/app/services/job_handlers.py` | Job handler(子进程监控、keepalive、文件稳定检查��� |
| `backend/app/services/job_worker.py` | Worker 主循环(心跳、stale 检测) |
| `backend/app/services/task_service.py` | Task 管理(僵尸检测在 get_active_tasks 中) |
| `backend/app/routers/tasks.py` | Task API(含 force-cancel 端点) |
| `backend/app/routers/idl.py` | IDL 路由(Import/D-InSAR 任务提交) |
| `frontend/src/App.jsx` | 全局遮罩锁 + 强制解锁 UI |
| `frontend/src/IDLAutomationPanel.jsx` | SARscape 面板(内部锁 + 备用强制解锁) |
| `frontend/src/api/idl.js` | 前端 API 函数 |
| `.env` | 所有超时和处理参数配置 |
## 七、待测试内容
1. **keepalive 机制验证**: 确认前端在整个处理过程中保持锁定(蓝色遮罩不消失),进度和步骤信息正确显示
2. **双 Task 对完整流程**: 两个 Task 对串行处理,全部完成后前端才解锁
3. **全局遮罩强制解锁**: 管理员在蓝色遮罩上输入密码后能正确取消任务并解锁
4. **进度显示**: 遮罩上的进度条和步骤信息随 ENVI 处理实时更新(Step 1/6 → Step 6/6, 10% → 90%)
5. **异常恢复**: envipyengine 报错时文件扫描兜底逻辑是否可靠
6. **metatask 模式**: 确认默认模式不受自定义模式改动影响
7. **多用户场景**: 管理员操作锁定时,只读用户仍可正常浏览(只读用户看不到强制解锁按钮)
8. **第二个 Task 对 step 6 失败**: 上次测试中第二个 Task 对的 step 6 报 "SARscape process unexpectedly terminated",需确认是数据问题还是系统问题
## 八、最近一次测试结果 (2026-02-23)
- Task_20250309_20250112: 全部 6 步成功,总耗时 10104.9s
- Task_20250310_20250113_9: 步骤 1-5b 成功,step 6 失败 ("SARscape process unexpectedly terminated",耗时 1853s)
- 第一个 Task 的 `_wait_for_disp_stable` 修复生效(成功路径也等待 disp 文件)
- 前端锁在第一个 Task 处理期间因僵尸检测被误杀(已通过 keepalive 修复,待验证)
@@ -0,0 +1,265 @@
# 前端锁定修复总结(2026-03-05)
## 修复概述
按照系统设计理念(管理员操作时全局锁定),修复了两个前端锁定问题:
1. ✅ **数据分发任务锁定**(P0 高优先级)
2. ✅ **扫描任务锁定**(P1 中优先级)
---
## 修复 1:数据分发任务锁定
### 问题
`DataCopierPanel` 是独立的任务管理系统,不与全局锁定机制集成。
### 修改文件
1. `frontend/src/App.jsx`
2. `frontend/src/DataCopierPanel.jsx`
### 修改内容
**App.jsx (1334 行)**:
```javascript
// 修改前
<DataCopierPanel
apiEndpoint={apiClient.defaults.baseURL}
readOnly={isReadOnlyUser}
/>
// 修改后
<DataCopierPanel
apiEndpoint={apiClient.defaults.baseURL}
readOnly={isReadOnlyUser}
onJobQueued={(taskId) => handleTaskStart(taskId, '数据分发任务已入队,正在处理...')}
/>
```
**DataCopierPanel.jsx (14 行)**:
```javascript
// 修改前
const DataCopierPanel = ({ apiEndpoint, readOnly = false }) => {
// 修改后
const DataCopierPanel = ({ apiEndpoint, readOnly = false, onJobQueued }) => {
```
**DataCopierPanel.jsx (119-131 行)**:
```javascript
// 修改前
try {
const response = await axios.post(endpoint, {
batch_id: selectedBatchId,
dest_dir: destDir,
copy_statuses: copyStatuses,
}, { withCredentials: true });
setTaskId(response.data.task_id);
} catch (error) {
// 修改后
try {
const response = await axios.post(endpoint, {
batch_id: selectedBatchId,
dest_dir: destDir,
copy_statuses: copyStatuses,
}, { withCredentials: true });
const taskId = response.data.task_id;
setTaskId(taskId);
// 触发全局锁定
if (onJobQueued) {
onJobQueued(taskId);
}
} catch (error) {
```
### 效果
- 数据分发任务启动时,前端立即锁定
- 任务完成后,前端自动解锁
- 与其他任务(IDL 自动化、数据监控)行为一致
---
## 修复 2:扫描任务锁定
### 问题
扫描任务执行太快(< 3 秒),前端轮询间隔(3 秒)来不及捕获 RUNNING 状态。
### 修改文件
`frontend/src/DataMonitorPanel.jsx`
### 修改内容
**DataMonitorPanel.jsx (168-198 行)**:
```javascript
// 修改前
const handleRunNow = async (target) => {
// ... 验证逻辑 ...
setLoading(true);
setMessage(`正在触发${targetMap[target] || '全部'}手动扫描...`);
try {
const url = target ? `${apiEndpoint}/monitor/run-now?target=${target}` : `${apiEndpoint}/monitor/run-now`;
const res = await fetch(url, {
method: 'POST',
credentials: 'include'
});
const data = await res.json();
if (res.ok) {
setMessage(data.message);
if (onTaskStart) onTaskStart(data.task_id, `已触发${targetMap[target] || '全部'}手动扫描...`);
}
// ...
}
};
// 修改后
const handleRunNow = async (target) => {
// ... 验证逻辑 ...
setLoading(true);
setMessage(`正在触发${targetMap[target] || '全部'}手动扫描...`);
// 立即触发全局锁定(在 API 调用之前)
if (onTaskStart) {
onTaskStart(null, `正在触发${targetMap[target] || '全部'}手动扫描...`);
}
try {
const url = target ? `${apiEndpoint}/monitor/run-now?target=${target}` : `${apiEndpoint}/monitor/run-now`;
const res = await fetch(url, {
method: 'POST',
credentials: 'include'
});
const data = await res.json();
if (res.ok) {
setMessage(data.message);
// 更新任务 ID
if (onTaskStart) onTaskStart(data.task_id, `已触发${targetMap[target] || '全部'}手动扫描...`);
}
// ...
}
};
```
### 关键改进
1. **在 API 调用之前立即锁定**:`onTaskStart(null, message)`
2. **API 成功后更新任务 ID**:`onTaskStart(taskId, message)`
3. **失败时自动解锁**:全局轮询检测到没有活跃任务时自动解锁
### 效果
- 用户点击"扫描"按钮后,前端立即锁定
- 即使任务执行很快(< 3 秒),用户也能看到锁定状态
- 任务完成后,前端自动解锁
---
## 测试验证
### 测试 1:数据分发任务锁定
1. 登录管理员账号
2. 进入"数据分发" Tab
3. 选择批次和目标目录
4. 点击"开始复制"
5. **预期**:前端立即锁定,显示"数据分发任务已入队,正在处理..."
6. 等待任务完成
7. **预期**:前端自动解锁
### 测试 2:扫描任务锁定
1. 登录管理员账号
2. 进入"数据管理" Tab
3. 点击"扫描雷达数据"或"扫描 D-InSAR 结果"
4. **预期**:前端立即锁定,显示"正在触发...手动扫描..."
5. 等待任务完成(可能很快)
6. **预期**:前端自动解锁
### 测试 3:多任务冲突
1. 启动数据分发任务
2. **预期**:前端锁定,无法启动其他任务
3. 尝试点击其他操作按钮
4. **预期**:按钮被禁用或操作被阻止
---
## 技术细节
### handleTaskStart 函数行为
```javascript
// hooks/useDinsarOperations.js:123-129
const handleTaskStart = (taskId, message) => {
if (taskId) {
setPendingTaskIds(prev => [...prev, taskId]);
}
setIsGlobalLocked(true); // 总是锁定
if (message) addLog('info', message);
};
```
**关键点**:
- `taskId` 可以为 `null`(立即锁定,稍后更新 ID)
- `setIsGlobalLocked(true)` 总是执行(无论 taskId 是否为 null)
- 这允许我们在 API 调用之前就锁定前端
### 解锁机制
前端通过 `useGlobalTaskControl` hook 自动解锁:
```javascript
// hooks/useGlobalTaskControl.js:56-64
if (hasRunningTasks !== isGlobalLockedRef.current) {
setIsGlobalLocked(hasRunningTasks);
if (!hasRunningTasks) {
addLog('success', '后台任务已完成,正在同步最新数据...');
setTimeout(() => {
initializeAppDataRef.current?.({ refreshRadarSearch: true });
}, 500);
}
}
```
**工作原理**:
1. 每 3 秒轮询 `/tasks/active`
2. 检测到没有活跃任务时,自动解锁
3. 不需要手动调用解锁
---
## 修改总结
### 修改文件
1. `frontend/src/App.jsx` - 1 处修改(添加 onJobQueued 回调)
2. `frontend/src/DataCopierPanel.jsx` - 2 处修改(接收回调 + 调用回调)
3. `frontend/src/DataMonitorPanel.jsx` - 1 处修改(提前锁定)
### 代码行数
- 新增:约 10 行
- 修改:约 5 行
- 删除:0 行
### 风险评估
- 🟢 **低风险**:改动小,逻辑清晰
- 🟢 **向后兼容**:不影响现有功能
- 🟢 **易于回滚**:修改集中,容易撤销
---
## 相关文档
- `ANALYSIS_FRONTEND_LOCKING_2026-03-05.md` - 问题分析
- `SECURITY_FIX_PROGRESS.md` - 修复进度跟踪
- `HOTFIX_PROJ_CONFLICT_2026-03-05.md` - PROJ 冲突修复
---
## 下一步
1. ✅ 代码修改完成
2. ⏳ 前端构建测试
3. ⏳ 功能测试验证
4. ⏳ 用户验收测试
---
**修复完成时间**:2026-03-05
**修复人**:Claude Opus 4.6
**状态**:✅ 代码修改完成,等待测试验证
+278
View File
@@ -0,0 +1,278 @@
# 硬编码审计报告(2026-03-05)
## 审计目的
确保系统可以在不同环境(开发机、生产环境、客户现场)部署,所有配置必须通过 `.env` 文件管理,不允许硬编码。
## 审计范围
- 后端代码(`backend/`)
- 启动脚本(`run_backend.py`, `scripts/`)
- 配置文件(`.env`, `config.py`)
## 发现的硬编码问题
### 🔴 高优先级(必须修复)
#### 1. Ollama API 地址硬编码
**位置**:
- `backend/app/ai_service.py:231`
- `backend/app/services/health_service.py:115`
**问题**:
```python
# 硬编码 Ollama API 地址
resp = await client.get("http://127.0.0.1:11434/api/tags")
```
**影响**:
- 如果 Ollama 部署在其他机器或端口,无法连接
- 客户环境可能使用不同的 Ollama 地址
**修复方案**:
```python
# 从环境变量读取
OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://127.0.0.1:11434")
resp = await client.get(f"{OLLAMA_BASE_URL}/api/tags")
```
**`.env` 配置**:
```bash
# Ollama 服务地址(基础 URL)
OLLAMA_BASE_URL=http://127.0.0.1:11434
```
---
#### 2. Nginx 健康检查地址硬编码
**位置**:
- `backend/app/services/health_service.py:130`
**问题**:
```python
# 硬编码 Nginx 地址
resp = await client.get("http://127.0.0.1/")
```
**影响**:
- 如果 Nginx 监听其他端口或地址,健康检查失败
- 客户环境可能使用不同的 Nginx 配置
**修复方案**:
```python
# 从环境变量读取
NGINX_HEALTH_URL = os.getenv("NGINX_HEALTH_URL", "http://127.0.0.1/")
resp = await client.get(NGINX_HEALTH_URL)
```
**`.env` 配置**:
```bash
# Nginx 健康检查地址
NGINX_HEALTH_URL=http://127.0.0.1/
```
---
#### 3. 后端绑定地址硬编码
**位置**:
- `run_backend.py:29`
**问题**:
```python
# 硬编码绑定地址
bind_host = "127.0.0.1"
```
**影响**:
- 无法从外部访问(Docker 容器、远程部署)
- 某些部署场景需要绑定 `0.0.0.0`
**修复方案**:
```python
# 从环境变量读取
bind_host = os.getenv("BACKEND_BIND_HOST", "127.0.0.1")
```
**`.env` 配置**:
```bash
# 后端绑定地址(127.0.0.1 仅本地,0.0.0.0 允许外部访问)
BACKEND_BIND_HOST=127.0.0.1
```
---
### 🟡 中优先级(建议修复)
#### 4. PROJ 数据库路径检查脚本硬编码
**位置**:
- `scripts/check_proj.py:62`
**问题**:
```python
# 硬编码 PostgreSQL PROJ 路径
pg_proj = r"C:\Program Files\PostgreSQL\17\share\contrib\postgis-3.6\proj\proj.db"
```
**影响**:
- 不同 PostgreSQL 版本路径不同
- 客户环境可能安装在其他位置
**修复方案**:
```python
# 从环境变量读取,或自动检测
pg_proj = os.getenv("POSTGRESQL_PROJ_PATH", r"C:\Program Files\PostgreSQL\17\share\contrib\postgis-3.6\proj\proj.db")
```
---
### 🟢 低优先级(可选优化)
#### 5. 默认值中的硬编码
**位置**:
- `backend/app/config.py:33-34`
**问题**:
```python
OLLAMA_API_URL: str = "http://127.0.0.1:11434/api/generate"
DEFAULT_VLM_MODEL: str = "qwen3-vl:8b"
```
**说明**:
- 这些是默认值,已经通过 `os.getenv()` 读取
- 但默认值本身是硬编码的
**修复方案**:
- 保持现状(默认值是合理的)
- 或者在文档中明确说明这些默认值
---
## 修复优先级
| 优先级 | 问题 | 影响 | 修复难度 |
|--------|------|------|----------|
| 🔴 P0 | Ollama API 地址 | 高 | 低 |
| 🔴 P0 | Nginx 健康检查地址 | 中 | 低 |
| 🔴 P0 | 后端绑定地址 | 高 | 低 |
| 🟡 P1 | PROJ 路径检查 | 低 | 低 |
| 🟢 P2 | 默认值硬编码 | 低 | 无需修复 |
---
## 修复计划
### 阶段 1:立即修复(P0)
1. **修改 `backend/app/ai_service.py`**
- 添加 `OLLAMA_BASE_URL` 环境变量
- 修改 API 调用使用动态 URL
2. **修改 `backend/app/services/health_service.py`**
- 添加 `OLLAMA_BASE_URL` 和 `NGINX_HEALTH_URL` 环境变量
- 修改健康检查使用动态 URL
3. **修改 `run_backend.py`**
- 添加 `BACKEND_BIND_HOST` 环境变量
- 支持配置绑定地址
4. **更新 `.env` 文件**
- 添加新的配置项和注释
5. **更新文档**
- 在部署文档中说明这些配置项
### 阶段 2:建议修复(P1)
1. **修改 `scripts/check_proj.py`**
- 支持从环境变量读取 PostgreSQL 路径
- 或自动检测 PostgreSQL 安装路径
---
## 验证方法
### 1. 配置文件验证
检查 `.env` 文件是否包含所有必需的配置项:
```bash
# 必需配置
DATABASE_URL=...
IDL_EXECUTABLE=...
IDL_DINSAR_DEM_BASE_FILE=...
# 新增配置
OLLAMA_BASE_URL=http://127.0.0.1:11434
NGINX_HEALTH_URL=http://127.0.0.1/
BACKEND_BIND_HOST=127.0.0.1
```
### 2. 代码审计
运行以下命令检查是否还有硬编码:
```bash
# 检查绝对路径
grep -r "C:\\\\" backend/ --include="*.py"
grep -r "D:\\\\" backend/ --include="*.py"
# 检查 IP 地址
grep -r "127\.0\.0\.1" backend/ --include="*.py"
grep -r "localhost" backend/ --include="*.py"
# 检查端口号
grep -r ":11434" backend/ --include="*.py"
grep -r ":8000" backend/ --include="*.py"
```
### 3. 部署测试
在不同环境测试:
1. **本地开发环境**:使用默认配置
2. **Docker 容器**:修改 `BACKEND_BIND_HOST=0.0.0.0`
3. **远程 Ollama**:修改 `OLLAMA_BASE_URL=http://192.168.1.100:11434`
4. **自定义端口**:修改 `PORT=18000`
---
## 部署检查清单
在部署到客户环境前,确认:
- [ ] 所有路径配置在 `.env` 中
- [ ] 所有 IP 地址/端口配置在 `.env` 中
- [ ] 没有硬编码的绝对路径
- [ ] 没有硬编码的 IP 地址
- [ ] 没有硬编码的端口号
- [ ] 所有配置项都有默认值(合理的)
- [ ] 所有配置项都有注释说明
- [ ] 部署文档已更新
---
## 相关文档
- `.env` 配置文件
- `docs/PROJ_CONFIGURATION.md` - PROJ 数据库配置
- `docs/DEPLOYMENT.md` - 部署文档(待创建)
---
## 总结
**当前状态**:
- ✅ 大部分配置已通过 `.env` 管理
- ❌ 发现 3 个高优先级硬编码问题
- ⚠️ 需要立即修复以支持生产部署
**修复后**:
- ✅ 所有配置通过 `.env` 管理
- ✅ 支持多种部署场景
- ✅ 客户环境可自定义配置
- ✅ 无需修改代码即可部署
+113
View File
@@ -0,0 +1,113 @@
# 紧急修复:Nginx UTF-8 BOM 问题(2026-03-04)
## 问题描述
在测试第一阶段修复时,发现 Nginx 启动失败:
```
nginx: [emerg] unknown directive "worker_processes" in Z:/Code/Insar_management_system_v2/nginx/nginx.conf:1
```
注意 `worker_processes` 前面有一个不可见字符 ``(UTF-8 BOM)。
## 根本原因
1. `nginx.conf` 文件包含 UTF-8 BOM(字节序标记:`ef bb bf`)
2. Nginx 无法识别 BOM,将其视为非法字符
3. PowerShell 的 `Set-Content -Encoding UTF8` 默认会添加 BOM
## 影响
- 🔴 **严重**:Nginx 无法启动,整个系统无法访问
- 这是第一阶段修复中 1.3 PowerShell 编码问题的延伸
## 解决方案
### 步骤 1:删除现有文件的 BOM
```bash
cd "Z:\Code\Insar_management_system_v2\nginx"
tail -c +4 nginx.conf > nginx.conf.tmp
mv nginx.conf.tmp nginx.conf
```
**验证**:
```bash
xxd -l 16 nginx.conf
# 应该显示:
# 00000000: 776f 726b 6572 5f70 726f 6365 7373 6573 worker_processes
# 而不是:
# 00000000: efbb bf77 6f72 6b65 725f 7072 6f63 6573 ...worker_proces
```
### 步骤 2:修改 PowerShell 脚本
**文件**:`scripts/start_app.ps1:388`
**修改前**:
```powershell
$NewConfContent | Set-Content -LiteralPath "$NginxConfPath" -NoNewline -Encoding UTF8
```
**修改后**:
```powershell
# 使用 UTF8 无 BOM 编码写入
$Utf8NoBom = New-Object System.Text.UTF8Encoding $false
[System.IO.File]::WriteAllText("$NginxConfPath", $NewConfContent, $Utf8NoBom)
```
**说明**:
- `System.Text.UTF8Encoding $false` 创建无 BOM 的 UTF-8 编码器
- `[System.IO.File]::WriteAllText()` 直接写入文件,不添加 BOM
- 这在 PowerShell 5 和 PowerShell 7+ 中都有效
## 验证
### 1. 检查文件编码
```bash
xxd -l 3 nginx/nginx.conf
# 应该不包含 ef bb bf
```
### 2. 启动系统
```powershell
.\scripts\start_app.ps1
```
### 3. 验证 Nginx 启动
```bash
curl http://localhost:8080
# 应该返回前端页面
```
## 经验教训
1. **PowerShell 编码陷阱**:
- `Set-Content -Encoding UTF8` 会添加 BOM
- 需要使用 `[System.IO.File]::WriteAllText()` 或 PowerShell 6+ 的 `utf8NoBOM`
2. **Nginx 对 BOM 敏感**:
- Nginx 配置文件必须是纯 ASCII 或无 BOM 的 UTF-8
- BOM 会导致解析失败
3. **测试的重要性**:
- 如果没有实际测试,这个问题不会被发现
- 代码审查和静态分析无法发现编码问题
## 更新的文档
- ✅ `SECURITY_FIX_PROGRESS.md` - 记录问题和解决方案
- ✅ `scripts/start_app.ps1` - 修复编码问题
- ✅ `nginx/nginx.conf` - 删除 BOM
## 状态
- ✅ 问题已解决
- ✅ 代码已修改
- ⏳ 等待重新测试
## 下一步
1. 重新运行启动脚本
2. 验证 Nginx 正常启动
3. 继续第一阶段的其他测试
@@ -0,0 +1,204 @@
# PROJ 数据库版本冲突问题修复(2026-03-05)
## 问题描述
在扫描 D-InSAR 结果时出现 PROJ 错误:
```
PROJ: proj_identify: C:\Program Files\PostgreSQL\17\share\contrib\postgis-3.6\proj\proj.db
contains DATABASE.LAYOUT.VERSION.MINOR = 2 whereas a number >= 3 is expected.
It comes from another PROJ installation.
```
## 根本原因
1. **PROJ 版本冲突**:
- PostgreSQL 17 自带的 PostGIS 3.6 包含旧版 PROJ 数据库(版本 2)
- GDAL/rasterio 期望 PROJ 数据库版本 >= 3
- 系统在坐标转换时(`Transformer.from_crs`)触发此警告
2. **环境变量优先级**:
- Windows 系统可能将 PostgreSQL 的 PROJ 路径添加到 PATH
- GDAL 默认搜索 PATH 中的 PROJ 数据库
- 找到了 PostgreSQL 的旧版本而非 GDAL 自带的新版本
## 影响
- 🟡 **中等**:不会导致程序崩溃,但会产生大量警告日志
- 坐标转换仍然可以工作(PROJ 会回退到兼容模式)
- 日志污染,影响问题排查
## 解决方案
### 方案 1:设置 PROJ_LIB 环境变量(已实施)
**文件**:`run_backend.py`
**修改内容**:
```python
def main() -> None:
# Load environment variables
load_dotenv()
# Fix PROJ database version conflict
# PostgreSQL's PROJ database is outdated, use GDAL's bundled PROJ data
# This prevents "DATABASE.LAYOUT.VERSION.MINOR = 2 whereas >= 3 is expected" error
if "PROJ_LIB" not in os.environ:
# Try to find GDAL's PROJ data directory
try:
from osgeo import gdal
gdal_data = gdal.GetConfigOption("GDAL_DATA")
if gdal_data:
proj_lib = os.path.join(os.path.dirname(gdal_data), "proj")
if os.path.exists(proj_lib):
os.environ["PROJ_LIB"] = proj_lib
print(f"[*] Set PROJ_LIB to: {proj_lib}")
except Exception as e:
print(f"[WARN] Could not auto-configure PROJ_LIB: {e}")
port = int(os.getenv("PORT", 8000))
# ... rest of the code
```
**工作原理**:
1. 在启动时检查 `PROJ_LIB` 环境变量是否已设置
2. 如果未设置,尝试从 GDAL 配置中获取 PROJ 数据目录
3. 设置 `PROJ_LIB` 指向 GDAL 自带的 PROJ 数据库
### 方案 2:手动设置环境变量(备选)
如果方案 1 不生效,可以在 `.env` 文件中手动设置:
```bash
# 找到 GDAL 的 PROJ 数据目录
python -c "from osgeo import gdal; import os; print(os.path.join(os.path.dirname(gdal.GetConfigOption('GDAL_DATA')), 'proj'))"
# 将输出路径添加到 .env
PROJ_LIB=C:\path\to\gdal\proj
```
或者在 PowerShell 启动脚本中设置:
```powershell
# scripts/start_app.ps1
$env:PROJ_LIB = "C:\path\to\gdal\proj"
```
### 方案 3:抑制 GDAL 警告(不推荐)
如果只是想隐藏警告而不解决根本问题:
```python
# 在 run_backend.py 中添加
from osgeo import gdal
gdal.SetConfigOption('CPL_LOG', 'OFF') # 关闭所有 GDAL 日志
```
**不推荐原因**:
- 会隐藏所有 GDAL 错误和警告
- 不解决根本问题
- 可能掩盖其他重要错误
## 验证
### 1. 检查 PROJ_LIB 是否生效
启动后端后,查看日志:
```
[*] Set PROJ_LIB to: C:\Users\...\site-packages\osgeo\data\proj
```
### 2. 运行扫描任务
在前端触发 D-InSAR 结果扫描,观察日志:
- ✅ 应该不再出现 PROJ 警告
- ✅ Footprint 提取正常完成
### 3. 验证坐标转换
```python
from pyproj import Transformer
transformer = Transformer.from_crs("EPSG:32650", "EPSG:4326", always_xy=True)
x, y = transformer.transform(500000, 3000000)
print(f"Transformed: {x}, {y}")
# 应该不产生警告
```
## 前端锁定问题
**问题**:扫描任务运行时前端没有锁定
**可能原因**:
1. 扫描任务执行太快(< 3 秒),前端轮询间隔(3 秒)来不及捕获
2. 任务状态更新有延迟
**解决方案**:
### 方案 A:增加最小锁定时间(推荐)
在扫描任务开始时立即锁定,结束时延迟解锁:
```python
# job_handlers.py
async def _handle_scan_dinsar(job: SystemJobORM) -> None:
if not job.task_id:
raise ValueError("SCAN_DINSAR requires task_id for progress tracking.")
await task_service.start_task(job.task_id, message="正在扫描 D-InSAR 结果...")
# 确保任务至少运行 2 秒,让前端有时间捕获
start_time = time.time()
# ... 执行扫描 ...
# 确保最小执行时间
elapsed = time.time() - start_time
if elapsed < 2.0:
await asyncio.sleep(2.0 - elapsed)
await task_service.complete_task(job.task_id, message="扫描完成")
```
### 方案 B:前端立即锁定(更好)
在提交扫描任务后立即锁定前端,不等待轮询:
```javascript
// DataManagementPanel.jsx
const handleScanDinsarResults = async () => {
try {
// 立即锁定前端
onJobQueued?.(null, 'SCAN_DINSAR');
const response = await scanDinsarResults(selectedDirs);
const taskId = response.data.task_id;
// 更新任务 ID
onJobQueued?.(taskId, 'SCAN_DINSAR');
addLog('info', `D-InSAR 结果扫描任务已提交: ${taskId}`);
} catch (error) {
// 解锁前端
setIsGlobalLocked(false);
addLog('error', `扫描失败: ${error.message}`);
}
};
```
## 状态
- ✅ PROJ 环境变量修复已实施
- ⏳ 等待测试验证
- ⏳ 前端锁定问题待确认是否需要修复
## 下一步
1. 重启后端服务
2. 运行 D-InSAR 结果扫描
3. 观察日志,确认 PROJ 警告消失
4. 如果前端仍未锁定,实施方案 B
## 相关文档
- `SECURITY_FIX_PROGRESS.md` - 安全修复进度
- `HOTFIX_NGINX_BOM_2026-03-04.md` - Nginx BOM 修复
+201
View File
@@ -0,0 +1,201 @@
# IDL/ENVI 集成状态文档
> 最后更新: 2026-02-21
## 1. 架构概述
系统的 D-InSAR 生产完全依赖 ENVI 5.6 + IDL 8.8 + SARscape 5.6+。
当前采用 **envipyengine** 作为唯一执行引擎,通过 Python subprocess 调用 `taskengine.exe`。
### 执行链路
```
前端 (IDLAutomationPanel.jsx)
→ POST /api/idl/jobs/import 或 /api/idl/jobs/dinsar
→ job_handlers.py: subprocess 启动 envi_runner_cli.py
→ envi_service.py: 调用 envipyengine → taskengine.exe
→ ENVI Task 执行 (SARsImportLuTan1 / SARsMetataskInSARDisplacementGeneration)
```
### 核心文件
| 文件 | 职责 |
|------|------|
| `backend/app/services/envi_service.py` | 核心服务:工作流、预检查、状态、历史 |
| `backend/app/services/envi_runner_cli.py` | CLI 入口,subprocess 中执行工作流 |
| `backend/app/services/job_handlers.py` | 任务分发,启动 envi_runner_cli 子进程 |
| `backend/app/routers/idl.py` | API 端点 |
| `frontend/src/IDLAutomationPanel.jsx` | 前端面板 (Step 1/2 布局) |
| `frontend/src/api/idl.js` | 前端 API 客户端 |
## 2. 环境配置
### ENVI 安装路径
```
C:\Program Files\Harris\ENVI56\
├── IDL88\bin\bin.x86_64\idl.exe
├── IDL88\bin\bin.x86_64\idlde.exe
└── IDL88\bin\bin.x86_64\taskengine.exe
```
### .env 关键配置
```ini
IDL_EXECUTABLE=C:\Program Files\Harris\ENVI56\IDL88\bin\bin.x86_64\idl.exe
IDL_WORKBENCH_PATH=C:\Program Files\Harris\ENVI56\IDL88\bin\bin.x86_64\idlde.exe
IDL_WORKER_RUNTIME_DIR=...\backend\runtime\idl_worker
IDL_WORKER_DEFAULT_TIMEOUT_SECONDS=14400
IDL_WORKER_MAX_TIMEOUT_SECONDS=43200
IDL_DINSAR_DEM_BASE_FILE=D:\SRTM30m\SRTMDEM_RSP_SARscape
```
### envipyengine 配置
```python
# 已通过 envipyengine.config.set() 配置:
engine = r'C:\Program Files\Harris\ENVI56\IDL88\bin\bin.x86_64\taskengine.exe'
```
Python 环境: `C:\Users\16216\.conda\envs\InSAR\python.exe` (envipyengine v1.0.9)
## 3. 根因分析:历史集成失败
### 3.1 症状回顾
之前尝试了三种引擎均不稳定:
- `idl.exe -e` 直连:进程挂起、管道错误、idl_opserver 崩溃 (0x0000000000000001)
- `taskengine.exe` 直调:内存访问违规 (0xC0000005 / exit code 3221225477)
- `envipyengine`:单独测试通过,集成到项目后崩溃
### 3.2 根因定位
**`.env` 中 `IDL_PATH` 变量名与 IDL 内置环境变量冲突。**
- `IDL_PATH` 是 IDL 的保留环境变量,用于指定 `.pro` 文件搜索路径
- 项目 `.env` 曾使用 `IDL_PATH` 存储 `idl.exe` 的可执行文件路径
- `load_dotenv()` 将其注入 `os.environ`,子进程继承后 IDL 读到无效的搜索路径
- `taskengine.exe` 因此崩溃,退出码 `0xC0000005`
### 3.3 验证过程
三态测试确认因果关系:
| 状态 | IDL_PATH 值 | 结果 |
|------|-------------|------|
| 干净环境 | 未设置 | ✅ 成功 |
| 设置错误值 | `C:\...\idl.exe` | ❌ 崩溃 (0xC0000005) |
| 移除后恢复 | 未设置 | ✅ 成功 |
### 3.4 修复
将 `.env` 中 `IDL_PATH` 重命名为 `IDL_EXECUTABLE`,同步更新所有引用。
## 4. 当前架构设计
### 4.1 设计决策
1. **envipyengine 为唯一执行引擎** — 删除了 idl 直连和 taskengine 直调的全部代码
2. **Import → D-InSAR 固定流水线** — D-InSAR 自动检测未导入数据,先 Import 再处理(智能串联)
3. **D-InSAR 使用 metatask** — 精细参数 (filter_method 等) metatask 不支持,已移除
4. **DEM 路径为系统级配置** — 存储在 .env,不暴露到前端
5. **subprocess 执行模式** — envipyengine 在独立子进程中运行,隔离 FastAPI 主进程
### 4.2 ENVI Tasks
| Task | 用途 | 关键参数 |
|------|------|----------|
| `SARsImportLuTan1` | 导入 LuTan-1 原始数据 | `INPUT_FILE_LIST`, `ROOT_URI_FOR_OUTPUT` |
| `SARsMetataskInSARDisplacementGeneration` | D-InSAR 位移生成 | `REFERENCE_SARSCAPEDATA`, `SECONDARY_SARSCAPEDATA`, `DEM_SARSCAPEDATA`, `OUTPUT_FOLDER` |
### 4.3 智能串联逻辑 (D-InSAR 工作流)
```
对每个 Task_* 文件夹:
1. 检查 master/slave 是否有 .sml
2. 没有 → 查找 .meta.xml → 自动执行 Import
3. Import 完成后验证 .sml 生成
4. 执行 D-InSAR metatask
```
### 4.4 API 端点
```
GET /api/idl/status — 系统状态 (含 DEM 路径和可用性)
POST /api/idl/launch-workbench — 启动 IDL Workbench
POST /api/idl/inspect/import — Import 预检查
POST /api/idl/inspect/dinsar — D-InSAR 预检查 (含 Import 状态检测)
POST /api/idl/jobs/import — 提交 Import 任务
POST /api/idl/jobs/dinsar — 提交 D-InSAR 任务
GET /api/idl/jobs/recent — 最近运行记录
```
### 4.5 目录结构要求
Import 支持两种布局:
```
# Task_* 结构 (推荐)
root_dir/
├── Task_001/
│ ├── master/ → *.meta.xml
│ └── slave/ → *.meta.xml
└── Task_002/
├── master/
└── slave/
# 平铺结构
root_dir/
├── scene_001/ → *.meta.xml
└── scene_002/ → *.meta.xml
```
D-InSAR 仅支持 Task_* 结构。
## 5. 测试记录
### 5.1 envipyengine 验证 (2026-02-20)
修复 IDL_PATH 冲突后,envipyengine Import 连续测试:
| 次数 | 耗时 | 结果 |
|------|------|------|
| 1 | 100.5s | ✅ 成功 |
| 2 | 75.8s | ✅ 成功 |
| 3 | 71.8s | ✅ 成功 |
| 4 | 73.6s | ✅ 成功 |
| 5 | 81.7s | ✅ 成功 |
测试数据: `Z:\Test_data\Test_IDL_1`
### 5.2 集成测试 — Step 1: Import (2026-02-21)
重构后通过前端 → 后端 → envipyengine 完整链路测试。
- 状态: ✅ 通过
- 链路: 前端提交 → job_handlers subprocess → envi_runner_cli → envi_service → envipyengine
### 5.3 集成测试 — Step 2: D-InSAR (待测试)
- 状态: ⏳ 待测试
- 前置条件: DEM 文件 `D:\SRTM30m\SRTMDEM_RSP_SARscape` 需存在于测试机
- 智能串联 (自动 Import) 待验证
## 6. 已删除的遗留代码
重构中删除的文件 (2026-02-21):
| 文件 | 行数 | 说明 |
|------|------|------|
| `backend/app/services/idl_worker_service.py` | 2249 | 三引擎架构的巨型服务 |
| `backend/app/services/idl_runner_cli.py` | 58 | 旧 CLI runner |
| `backend/app/services/envipyengine_runner.py` | 47 | 调试用独立 runner |
清理的 .env 配置:所有 `IDL_TASKENGINE_*`、`IDL_WORKER_ENGINE`、`IDL_JOB_RUNNER_MODE`、
`IDL_IDL_*`、`IDL_WORKER_PREFLIGHT_*`、D-InSAR 精细参数 (filter_method 等)。
## 7. 已知限制
1. **D-InSAR 精细参数不可调** — metatask 不支持 filter_method、unwrapping_coh_threshold 等参数
2. **DEM 路径固定** — 从 .env 读取,不支持前端动态指定
3. **单进程串行** — envipyengine 调用 taskengine.exe 是同步阻塞的,同一时间只能执行一个 ENVI Task
4. **仅支持 LuTan-1** — Import task 为 `SARsImportLuTan1`,其他卫星数据需要不同的 Task
@@ -0,0 +1,510 @@
# 系统日志管理分析与优化方案(2026-03-05)
## 当前日志分布情况
### 1. 根目录日志(❌ 不规范)
```
Z:\Code\Insar_management_system_v2\
├── unpacker_activity.log (34KB) - 解包活动日志
└── unpacker_log.json (8.6KB) - 解包结构化日志
```
**来源**:`scripts/unpack_archives.py`
**问题**:
- 直接保存在项目根目录,不专业
- 与代码文件混在一起
- 不便于管理和清理
- 可能被误提交到 Git
---
### 2. IDL/ENVI 工作日志
```
backend/runtime/idl_worker/
├── 20260225_022409_dinsar_custom.log
├── 20260225_051527_dinsar_custom.log
├── 20260304_091723_import.log
├── 20260304_091854_import.log
└── dinsar_custom_progress.log
```
**来源**:ENVI 工作流执行日志
**状态**:✅ 已规范化(在 `backend/runtime/` 下)
---
### 3. SARscape 工作日志
```
.idl/sarmap/sarscape-3-6_1_0-idl_8_8/
├── hwConfig.log
└── sarscape_work/
├── Process.log
├── Process_20260226165004.log
├── Process_20260226210207.log
└── Process_20260227091802.log
```
**来源**:SARscape 软件自动生成
**状态**:✅ 可接受(第三方软件日志)
---
### 4. 应用日志(缺失)
**问题**:
- ❌ 没有统一的应用日志目录
- ❌ 没有日志轮转机制
- ❌ 没有日志级别配置
- ❌ 没有结构化日志
---
## 日志用途分析
### unpacker_activity.log
**用途**:记录解包脚本的活动日志(文本格式)
**内容示例**:
```
2026-03-04 20:53:15 - INFO - 开始扫描归档文件...
2026-03-04 20:53:15 - INFO - 找到 5 个归档文件
2026-03-04 20:53:16 - INFO - 解包完成: archive1.zip
```
**问题**:
- 保存在根目录
- 无日志轮转(会无限增长)
- 无日志级别控制
---
### unpacker_log.json
**用途**:记录解包脚本的结构化日志(JSON 格式)
**内容示例**:
```json
{
"timestamp": "2026-03-04T20:53:15",
"level": "INFO",
"message": "解包完成",
"archive": "archive1.zip",
"files_extracted": 123,
"duration_seconds": 5.2
}
```
**问题**:
- 保存在根目录
- 与 activity.log 重复
- 无清理机制
---
## 系统中的其他日志
### 1. 数据库日志
- **位置**:PostgreSQL 数据目录
- **管理**:由 PostgreSQL 管理
### 2. Nginx 日志
- **位置**:`nginx/logs/` (如果配置了)
- **管理**:由 Nginx 管理
### 3. 任务日志
- **位置**:数据库 `system_task` 表的 `logs` 字段
- **管理**:通过 API 查询
### 4. 进度文件
- **位置**:`backend/runtime/idl_worker/job_{job_id}_progress.json`
- **管理**:任务完成后自动清理
---
## 日志管理问题
### 问题 1:日志分散
**现状**:
- 根目录:unpacker 日志
- backend/runtime/:ENVI 日志
- .idl/:SARscape 日志
- 数据库:任务日志
**影响**:
- 难以统一查看
- 难以统一清理
- 难以统一备份
---
### 问题 2:无日志轮转
**现状**:
- unpacker_activity.log 会无限增长
- ENVI 日志会累积(每次执行生成新文件)
**影响**:
- 磁盘空间浪费
- 日志文件过大影响性能
---
### 问题 3:无统一日志框架
**现状**:
- unpacker 使用自定义日志
- 后端使用 print() 输出
- 没有统一的日志级别
**影响**:
- 日志格式不一致
- 难以过滤和搜索
- 难以集成日志分析工具
---
## 优化方案
### 方案 A:统一日志目录结构(推荐)
```
Z:\Code\Insar_management_system_v2\
├── logs/ # 统一日志目录
│ ├── app/ # 应用日志
│ │ ├── backend.log # 后端主日志
│ │ ├── backend.log.1 # 轮转日志
│ │ ├── backend.log.2
│ │ └── ...
│ ├── tasks/ # 任务日志
│ │ ├── envi/ # ENVI 工作流日志
│ │ │ ├── 20260304_091723_import.log
│ │ │ └── ...
│ │ └── unpacker/ # 解包任务日志
│ │ ├── unpacker_20260304.log
│ │ └── ...
│ ├── access/ # 访问日志
│ │ ├── nginx_access.log
│ │ └── api_access.log
│ └── error/ # 错误日志
│ ├── nginx_error.log
│ └── api_error.log
├── backend/
└── frontend/
```
**优点**:
- 所有日志集中管理
- 便于备份和清理
- 便于配置 .gitignore
---
### 方案 B:使用 Python logging 模块
**配置文件**:`backend/app/logging_config.py`
```python
import logging
import logging.handlers
import os
from pathlib import Path
# 日志根目录
LOG_ROOT = Path(__file__).parent.parent.parent / "logs"
LOG_ROOT.mkdir(exist_ok=True)
# 应用日志目录
APP_LOG_DIR = LOG_ROOT / "app"
APP_LOG_DIR.mkdir(exist_ok=True)
# 任务日志目录
TASK_LOG_DIR = LOG_ROOT / "tasks"
TASK_LOG_DIR.mkdir(exist_ok=True)
# 日志配置
LOGGING_CONFIG = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"default": {
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S",
},
"detailed": {
"format": "%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S",
},
"json": {
"()": "pythonjsonlogger.jsonlogger.JsonFormatter",
"format": "%(asctime)s %(name)s %(levelname)s %(message)s",
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"level": "INFO",
"formatter": "default",
"stream": "ext://sys.stdout",
},
"file": {
"class": "logging.handlers.RotatingFileHandler",
"level": "DEBUG",
"formatter": "detailed",
"filename": str(APP_LOG_DIR / "backend.log"),
"maxBytes": 10 * 1024 * 1024, # 10MB
"backupCount": 5,
"encoding": "utf-8",
},
"error_file": {
"class": "logging.handlers.RotatingFileHandler",
"level": "ERROR",
"formatter": "detailed",
"filename": str(APP_LOG_DIR / "error.log"),
"maxBytes": 10 * 1024 * 1024, # 10MB
"backupCount": 5,
"encoding": "utf-8",
},
},
"loggers": {
"": { # root logger
"level": "INFO",
"handlers": ["console", "file", "error_file"],
},
"uvicorn": {
"level": "INFO",
"handlers": ["console", "file"],
"propagate": False,
},
"sqlalchemy": {
"level": "WARNING",
"handlers": ["file"],
"propagate": False,
},
},
}
def setup_logging():
"""初始化日志配置"""
import logging.config
logging.config.dictConfig(LOGGING_CONFIG)
```
**使用方式**:
```python
# backend/app/main.py
from .logging_config import setup_logging
setup_logging()
logger = logging.getLogger(__name__)
@app.on_event("startup")
async def startup_event():
logger.info("应用启动")
```
---
### 方案 C:修复 unpacker 日志位置
**修改文件**:`scripts/unpack_archives.py`
```python
# 修改前
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
LOG_FILE = os.path.join(PROJECT_ROOT, "unpacker_log.json")
ACTIVITY_LOG = os.path.join(PROJECT_ROOT, "unpacker_activity.log")
# 修改后
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
LOG_DIR = os.path.join(PROJECT_ROOT, "logs", "tasks", "unpacker")
os.makedirs(LOG_DIR, exist_ok=True)
# 使用日期命名,便于清理
from datetime import datetime
log_date = datetime.now().strftime("%Y%m%d")
LOG_FILE = os.path.join(LOG_DIR, f"unpacker_{log_date}.json")
ACTIVITY_LOG = os.path.join(LOG_DIR, f"unpacker_{log_date}.log")
```
---
### 方案 D:添加日志清理机制
**定期清理脚本**:`scripts/cleanup_logs.py`
```python
import os
import time
from pathlib import Path
PROJECT_ROOT = Path(__file__).parent.parent
LOG_ROOT = PROJECT_ROOT / "logs"
# 清理策略
CLEANUP_RULES = {
"logs/app/*.log.*": 30, # 应用日志保留 30 天
"logs/tasks/envi/*.log": 7, # ENVI 日志保留 7 天
"logs/tasks/unpacker/*.log": 7, # 解包日志保留 7 天
}
def cleanup_old_logs():
"""清理过期日志"""
now = time.time()
for pattern, days in CLEANUP_RULES.items():
max_age = days * 86400 # 转换为秒
for log_file in LOG_ROOT.glob(pattern):
if log_file.is_file():
age = now - log_file.stat().st_mtime
if age > max_age:
print(f"删除过期日志: {log_file} (已存在 {age/86400:.1f} 天)")
log_file.unlink()
if __name__ == "__main__":
cleanup_old_logs()
```
**添加到定时任务**:
```python
# backend/app/main.py
from apscheduler.schedulers.asyncio import AsyncIOScheduler
scheduler = AsyncIOScheduler()
@app.on_event("startup")
async def startup_event():
# 每天凌晨 3 点清理日志
scheduler.add_job(cleanup_old_logs, "cron", hour=3, minute=0)
scheduler.start()
```
---
## 实施步骤
### 第一步:创建日志目录结构
```bash
cd Z:\Code\Insar_management_system_v2
mkdir -p logs/app
mkdir -p logs/tasks/envi
mkdir -p logs/tasks/unpacker
mkdir -p logs/access
mkdir -p logs/error
```
### 第二步:移动现有日志
```bash
# 移动 unpacker 日志
mv unpacker_activity.log logs/tasks/unpacker/unpacker_20260304.log
mv unpacker_log.json logs/tasks/unpacker/unpacker_20260304.json
# 移动 ENVI 日志(已经在正确位置)
# backend/runtime/idl_worker/ 保持不变
```
### 第三步:修改 unpacker 脚本
修改 `scripts/unpack_archives.py`,使用新的日志路径。
### 第四步:配置 Python logging
创建 `backend/app/logging_config.py`,配置统一日志。
### 第五步:更新 .gitignore
```gitignore
# 日志文件
logs/
*.log
*.log.*
# 但保留日志目录结构
!logs/.gitkeep
!logs/app/.gitkeep
!logs/tasks/.gitkeep
```
### 第六步:添加日志清理
创建 `scripts/cleanup_logs.py`,配置定时清理。
---
## 推荐配置
### 日志级别
```python
# 开发环境
LOG_LEVEL = "DEBUG"
# 生产环境
LOG_LEVEL = "INFO"
# 错误追踪
ERROR_LOG_LEVEL = "ERROR"
```
### 日志轮转
```python
# 按大小轮转
maxBytes = 10 * 1024 * 1024 # 10MB
backupCount = 5 # 保留 5 个备份
# 按时间轮转
when = "midnight" # 每天午夜轮转
interval = 1 # 每 1 天
backupCount = 30 # 保留 30 天
```
### 日志保留
```python
# 应用日志:30 天
# 任务日志:7 天
# 错误日志:90 天
# 访问日志:30 天
```
---
## 总结
### 当前问题
1. ❌ unpacker 日志保存在根目录(不专业)
2. ❌ 日志分散在多个位置
3. ❌ 无统一日志框架
4. ❌ 无日志轮转和清理机制
### 推荐方案
1. ✅ 创建统一的 `logs/` 目录
2. ✅ 使用 Python logging 模块
3. ✅ 配置日志轮转(按大小或时间)
4. ✅ 添加定时清理机制
5. ✅ 更新 .gitignore
### 优先级
- **P0(立即)**:移动 unpacker 日志到 logs/ 目录
- **P1(本周)**:配置 Python logging 模块
- **P2(两周)**:添加日志轮转和清理
- **P3(一个月)**:集成日志分析工具(如 ELK)
---
## 下一步
需要我开始实施日志管理优化吗?我可以:
1. 创建日志目录结构
2. 修改 unpacker 脚本
3. 配置 Python logging
4. 添加日志清理脚本
还是你想先看看这个方案,再决定是否实施?
@@ -0,0 +1,332 @@
# 日志管理功能实施总结(2026-03-05)
## 实施概述
按照你的需求,实现了统一的日志管理功能:
- ✅ 统一日志目录结构
- ✅ 前端管理模块(放在运维自检)
- ✅ Modal 查看日志
- ✅ 手动删除(不自动删除)
- ✅ 仅管理员可删除
---
## 已完成的工作
### 1. 创建统一日志目录结构
```
logs/
├── app/ # 应用日志
├── tasks/ # 任务日志
│ ├── envi/ # ENVI 工作流日志
│ └── unpacker/ # 解包任务日志
└── error/ # 错误日志
```
### 2. 移动现有日志文件
- ✅ `unpacker_activity.log` → `logs/tasks/unpacker/unpacker_20260304.log`
- ✅ `unpacker_log.json` → `logs/tasks/unpacker/unpacker_20260304.json`
### 3. 修改 unpacker 脚本
**文件**:`scripts/unpack_archives.py`
**修改内容**:
- 使用统一的日志目录 `logs/tasks/unpacker/`
- 使用日期命名日志文件(`unpacker_YYYYMMDD.log`)
- 自动创建日志目录
### 4. 更新 .gitignore
**新增内容**:
```gitignore
# 日志文件
logs/
*.log
*.log.*
# 但保留日志目录结构
!logs/.gitkeep
!logs/app/.gitkeep
!logs/tasks/.gitkeep
!logs/error/.gitkeep
```
### 5. 实现后端 API
**文件**:`backend/app/routers/logs.py`
**API 端点**:
1. `GET /logs/list` - 列出所有日志文件
- 支持按类型过滤(app, task, error)
- 返回文件名、大小、修改时间
2. `GET /logs/content/{log_path}` - 读取日志内容
- 支持分页读取(offset, limit)
- 最多一次读取 10000 行
- 防止路径遍历攻击
3. `DELETE /logs/{log_path}` - 删除日志文件
- 仅管理员可删除
- 防止路径遍历攻击
**安全特性**:
- 路径安全检查(防止 `../` 攻击)
- 权限控制(删除仅管理员)
- 文件类型验证
### 6. 实现前端 API
**文件**:`frontend/src/api/logs.js`
**函数**:
- `listLogs(logType)` - 获取日志列表
- `getLogContent(logPath, offset, limit)` - 获取日志内容
- `deleteLog(logPath)` - 删除日志
### 7. 实现前端日志管理组件
**文件**:`frontend/src/LogManagementPanel.jsx`
**功能**:
- 日志列表展示(表格形式)
- 类型过滤(应用/任务/错误)
- 查看日志(Modal)
- 删除日志(仅管理员,需确认)
- 日志搜索(实时过滤)
- 分页加载(大文件支持)
**UI 特性**:
- 类型标签(彩色徽章)
- 文件大小格式化(B/KB/MB)
- 修改时间显示
- 深色代码编辑器风格
- 响应式布局
### 8. 集成到运维自检
**修改文件**:
- `frontend/src/HealthCheckPanel.jsx` - 添加日志管理区域
- `frontend/src/App.jsx` - 传递 currentUser 参数
---
## 功能特性
### 日志列表
| 列名 | 说明 |
|------|------|
| 文件名 | 日志文件名(等宽字体) |
| 类型 | 应用/任务/错误(彩色标签) |
| 大小 | 文件大小(自动格式化) |
| 修改时间 | 最后修改时间 |
| 操作 | 查看/删除按钮 |
**过滤功能**:
- 全部
- 应用日志
- 任务日志
- 错误日志
### 日志查看 Modal
**顶部信息栏**:
- 文件名(等宽字体)
- 文件大小
- 修改时间
- 总行数
**搜索栏**:
- 实时搜索(过滤日志内容)
- 显示当前行范围
- 上一页/下一页按钮
**日志内容区**:
- 深色背景(#1e1e1e)
- 等宽字体(Consolas, Monaco)
- 自动换行
- 滚动查看
### 删除功能
**权限控制**:
- 只有管理员可以看到删除按钮
- 非管理员点击会提示"只有管理员可以删除日志"
**确认对话框**:
```
确定要删除日志文件 "xxx.log" 吗?
此操作不可恢复!
```
---
## 技术实现
### 后端
**路径安全检查**:
```python
def _is_safe_path(file_path: str) -> bool:
"""检查路径是否安全(防止路径遍历攻击)"""
try:
requested_path = (LOG_ROOT / file_path).resolve()
return requested_path.is_relative_to(LOG_ROOT)
except (ValueError, RuntimeError):
return False
```
**分页读取**:
```python
with open(log_file, "r", encoding="utf-8", errors="replace") as f:
lines = f.readlines()
total_lines = len(lines)
start = offset
end = min(offset + limit, total_lines)
content_lines = lines[start:end]
```
### 前端
**分页加载**:
```javascript
const loadLogContent = async (logPath, offset = 0) => {
const data = await getLogContent(logPath, offset, 1000);
setLogContent(data.content);
setTotalLines(data.total_lines);
setCurrentOffset(offset);
};
```
**实时搜索**:
```javascript
const filteredContent = searchTerm
? logContent.split('\n')
.filter(line => line.toLowerCase().includes(searchTerm.toLowerCase()))
.join('\n')
: logContent;
```
---
## 使用说明
### 查看日志
1. 登录系统
2. 进入"运维自检" Tab
3. 滚动到"日志管理"区域
4. 点击日志文件的"查看"按钮
5. 在 Modal 中查看日志内容
6. 使用搜索框过滤内容
7. 使用上一页/下一页浏览大文件
### 删除日志
1. 以管理员身份登录
2. 进入"运维自检" Tab
3. 找到要删除的日志文件
4. 点击"删除"按钮
5. 确认删除操作
6. 日志文件被永久删除
### 过滤日志
1. 使用"类型过滤"下拉菜单
2. 选择"应用日志"、"任务日志"或"错误日志"
3. 列表自动更新
---
## 文件清单
### 后端文件
- `backend/app/routers/logs.py` - 日志管理 API(新建)
- `backend/app/routers/__init__.py` - 注册日志路由(修改)
- `scripts/unpack_archives.py` - 使用新日志路径(修改)
### 前端文件
- `frontend/src/api/logs.js` - 日志 API 封装(新建)
- `frontend/src/LogManagementPanel.jsx` - 日志管理组件(新建)
- `frontend/src/HealthCheckPanel.jsx` - 集成日志管理(修改)
- `frontend/src/App.jsx` - 传递 currentUser(修改)
### 配置文件
- `.gitignore` - 忽略日志文件(新建)
### 目录结构
- `logs/` - 统一日志目录(新建)
- `logs/app/` - 应用日志目录(新建)
- `logs/tasks/envi/` - ENVI 日志目录(新建)
- `logs/tasks/unpacker/` - 解包日志目录(新建)
- `logs/error/` - 错误日志目录(新建)
---
## 测试清单
### 后端测试
- [ ] 启动后端服务
- [ ] 访问 `/api/logs/list` 验证 API
- [ ] 访问 `/api/logs/content/{log_path}` 验证内容读取
- [ ] 测试路径遍历攻击(应该被拒绝)
- [ ] 测试删除权限(非管理员应该被拒绝)
### 前端测试
- [ ] 打包前端(`npm run build`)
- [ ] 登录系统
- [ ] 进入"运维自检" Tab
- [ ] 验证日志列表显示
- [ ] 点击"查看"按钮验证 Modal
- [ ] 测试搜索功能
- [ ] 测试分页功能
- [ ] 测试删除功能(管理员)
- [ ] 测试删除权限(非管理员应该看不到删除按钮)
### 集成测试
- [ ] 运行 unpacker 脚本,验证日志保存到新位置
- [ ] 在前端查看新生成的日志
- [ ] 删除日志后验证文件确实被删除
---
## 下一步优化(可选)
### 短期优化
1. 添加日志下载功能(如果需要)
2. 添加日志实时刷新(WebSocket)
3. 添加日志高亮(错误/警告)
### 长期优化
1. 使用 Monaco Editor 替代 Textarea
2. 添加日志统计(错误数、警告数)
3. 添加日志归档功能
4. 集成日志分析工具(ELK)
---
## 总结
✅ 已完成所有需求:
1. 统一日志管理 ✅
2. 前端管理模块(放在运维自检)✅
3. Modal 查看日志 ✅
4. 手动删除(不自动删除)✅
5. 仅管理员可删除 ✅
**代码修改**:
- 新建文件:5 个
- 修改文件:4 个
- 总代码行数:约 400 行
**风险评估**:🟢 低风险
- 不影响现有功能
- 只读操作无风险
- 删除操作有权限控制和确认
**状态**:✅ 代码完成,等待测试验证
+86
View File
@@ -0,0 +1,86 @@
# 源影像叠加稳定化方案(Scheme B)
目标:解决“覆盖面是斜四边形,源影像是矩形包围框”的错位问题,优先保障长期稳定与前端流畅。
---
## 1. 问题本质
当前地图展示中:
- 覆盖范围来自 `coverage_polygon`(真实斜四边形)
- 源图叠加常用 `imageOverlay(url, bounds)`(矩形边界)
当源图本身有旋转/倾斜时,直接矩形贴图会产生明显错位。
---
## 2. 方案选择
### A(前端实时变换)
- 在浏览器端做逐帧几何变换
- 优点:改动快
- 缺点:前端负载高,图层增多后容易卡顿
### B(后端预纠正缓存)✅
- 扫描/构建阶段在后端完成几何纠正,前端只显示结果图
- 优点:渲染轻、稳定性高、易观测
- 缺点:后端实现复杂度更高
本项目已确定采用 **B 方案**。
---
## 3. 当前实现(2026-02-10)
## 3.1 缓存分层
- `backend/image_cache/radar_geo/`:地理纠正后的主缓存
- `backend/image_cache/radar_raw/`:原图回退缓存
## 3.2 后端能力
- 默认由手动扫描任务触发增量构建(前端“立即扫描”或 `POST /api/monitor/run-now`)
- 扫描时增量构建纠正缓存(`radar_geo`)
- 同步维护原图缓存(`radar_raw`)作为兜底
- 方向判定优先使用 XML 的 `sceneCornerCoord/refRow/refColumn`,避免仅靠角点名称导致镜像/翻转误判
- `GET /api/radar-data/{id}/thumb`:
- 优先返回 `radar_geo`
- 失败自动回退 `radar_raw`
- 新增状态接口:
- `GET /api/radar-data/{id}/preview-status`
- `POST /api/radar-data/{id}/rebuild-preview-cache`(管理员)
## 3.3 前端能力
- 每条源影像显示预览状态(纠正/回退/失败/未建)
- 管理员支持“重建”按钮
- 预览请求增加缓存键,避免浏览器长期缓存旧图
---
## 4. 关键配置项
```env
RADAR_GEO_CACHE_WORKERS=2
RADAR_GEO_CACHE_VERSION=b1
RADAR_GEO_CACHE_QUALITY=84
RADAR_PREVIEW_BUILD_ON_DEMAND=true
```
建议:
- 小规模先用 `RADAR_GEO_CACHE_WORKERS=1~2`
- 算法升级时提高 `RADAR_GEO_CACHE_VERSION` 触发重建
---
## 5. 验收标准
- 同一场景下,源图与覆盖面边界明显趋于一致
- 缩放/平移时交互平滑,无明显掉帧
- 构建失败可在状态接口与日志中定位原因
- 失败场景可回退原图缓存,不阻塞业务操作
---
## 6. 后续建议
1. 采集 3 类典型数据(升轨/降轨/大倾斜)做对齐验收
2. 统计 `radar_geo` 构建失败原因并分级治理
3. 发布前执行一次全量重建,提升线上命中率
+89
View File
@@ -0,0 +1,89 @@
# 项目代码审计报告(2026-03-04)
审计范围:后端核心链路、前端鉴权调用链、启动与脚本安全边界。
审计方式:静态只读审计(未修改业务代码)。
## 1. 高危问题
### 1.1 ENVI 主流程成功后不返回结果,导致下游按 `dict` 使用时崩溃
- 证据:
- `backend/app/services/envi_service.py:1568`
- `backend/app/services/envi_service.py:1630`
- `backend/app/services/envi_runner_cli.py:44`
- `backend/app/services/envi_runner_cli.py:51`
- `backend/app/services/job_handlers.py:1020`
- `backend/app/services/job_handlers.py:1031`
- 影响:任务可能在实际执行后仍被标记失败或抛异常。
### 1.2 只读用户可通过 `GET` 触发写操作和高开销构建,绕过读写分离意图
- 证据:
- `backend/app/routers/radar.py:619`
- `backend/app/routers/radar.py:365`
- `backend/app/routers/radar.py:346`
- `backend/app/routers/dinsar.py:214`
- `backend/app/routers/dinsar.py:146`
- `backend/app/routers/dependencies.py:196`
- `backend/app/routers/dependencies.py:315`
- 影响:读账号可诱发缓存重建/数据库写入,存在资源滥用风险。
### 1.3 AOI token 内存存储无容量上限,且 token 可续期
- 证据:
- `backend/app/routers/dependencies.py:131`
- `backend/app/routers/dependencies.py:825`
- `backend/app/routers/dependencies.py:850`
- `backend/app/routers/dependencies.py:59`
- `backend/app/routers/radar.py:439`
- `backend/app/routers/radar.py:509`
- 影响:认证后低权限账号可通过频繁检索制造进程内存膨胀(DoS 面)。
## 2. 中危问题
### 2.1 `extract_disp_results` 失败计数重复累加
- 证据:
- `backend/app/services/envi_service.py:1717`
- `backend/app/services/envi_service.py:1726`
- `backend/app/services/envi_service.py:1729`
- 影响:统计报表和告警判断失真。
### 2.2 AOI 文件解析异常未统一转为 4xx,用户输入可触发 500
- 证据:
- `backend/app/routers/dependencies.py:792`
- `backend/app/routers/dependencies.py:800`
- 影响:可用性下降,错误语义不一致。
### 2.3 路径归属判断使用 `startswith`,存在前缀误匹配
- 证据:
- `backend/app/services/data_service.py:530`
- 影响:可能将不属于监控根目录的路径误判为合法。
### 2.4 解包安全校验仅检查 `member.name`,仍使用 `extractall`
- 证据:
- `scripts/unpack_archives.py:98`
- `scripts/unpack_archives.py:107`
- `scripts/unpack_archives.py:181`
- 影响:若归档文件不可信,仍可能存在目录逃逸/覆盖风险(尤其符号链接/硬链接场景)。
## 3. 低危问题
### 3.1 启动脚本写 `nginx.conf` 未显式编码
- 证据:
- `scripts/start_app.ps1:388`
- 影响:在 Windows PowerShell 5 环境可能写出 UTF-16,导致 Nginx 配置解析异常。
## 4. 已复核并排除
### 4.1 `task_service.create_task` 的 `task_id` 未定义问题为误报
- 证据:
- `backend/app/services/task_service.py:153`
- 结论:`task_id` 已正常赋值。
## 5. 假设与待确认
1. 解包问题是否上调为“高危”,取决于归档输入是否可被外部/低信任来源控制。
2. “只读账号触发缓存构建”是否视为权限缺陷,取决于产品策略是否允许此行为。
## 6. 说明
- 本报告对应一次只读审计,不包含代码修复提交。
- 可在确认后按优先级先修复:1.1 / 1.2 / 1.3。
@@ -0,0 +1,577 @@
# 安全审计分析与修复方案(2026-03-04)
## 审计概述
本文档针对 `SECURITY_AUDIT_2026-03-04.md` 中提出的安全问题进行详细分析,并提供具体的修复方案。
审计发现:
- 🔴 高危问题:3 个
- 🟡 中危问题:4 个
- 🟢 低危问题:1 个
---
## 1. 高危问题分析与修复方案
### 1.1 ENVI 主流程成功后不返回结果
**问题描述**:
- `envi_service.py:run_workflow()` 在成功执行后不返回 `record` 字典
- `envi_runner_cli.py` 期望打印 JSON 结果,但 `run_workflow()` 在成功时返回 `None`
- `job_handlers.py:_run_envi_workflow_job()` 尝试将返回值作为字典使用,导致崩溃
**根本原因**:
```python
# envi_service.py:1630
if error:
raise RuntimeError(error)
# 缺少: return record
```
**影响等级**:🔴 高危
- 所有 ENVI 工作流任务在成功执行后会被误判为失败
- 下游代码尝试访问 `run_meta.get('workflow')` 时会抛出 `AttributeError`
**修复方案**:
```python
# envi_service.py:1630 之后添加
if error:
raise RuntimeError(error)
return record # 添加此行
```
**验证方法**:
1. 运行一个 D-InSAR 工作流任务
2. 检查任务状态是否正确标记为 SUCCESS
3. 检查日志中是否包含 `workflow=dinsar_custom duration=XXXs`
---
### 1.2 只读用户可通过 GET 触发写操作
**问题描述**:
多个 GET 端点在只读操作中触发了数据库写入和高开销计算:
1. **`GET /radar-data/{data_id}/thumb`** (radar.py:622)
- 调用 `_get_cached_radar_preview()` → `_build_radar_preview_cache()`
- 触发预览图生成(GDAL 处理)并写入数据库
2. **`GET /radar-data/imaging-dates`** (radar.py:609)
- 虽然是只读查询,但在 `_build_radar_preview_cache()` 中会更新数据库
3. **`GET /dinsar-results/{result_id}/preview`** (dinsar.py:214)
- 类似问题,按需构建预览缓存
**根本原因**:
```python
# radar.py:365
if settings.RADAR_PREVIEW_BUILD_ON_DEMAND:
build_status = await _build_radar_preview_cache(record, db, force=False)
# 内部会执行 db.add(record) 和 db.commit()
```
**影响等级**:🔴 高危
- 只读账号可以触发大量 GDAL 处理任务,导致 CPU/内存耗尽(DoS)
- 违反最小权限原则,读账号不应有写权限
- 可能导致数据库锁竞争
**修复方案**:
**方案 A:权限分离(推荐)**
```python
# 在 dependencies.py 中添加权限检查
def require_write_permission(current_user: User = Depends(get_current_user)):
if current_user.role == "readonly":
raise HTTPException(
status_code=403,
detail="此操作需要写权限"
)
return current_user
# 在 radar.py 中修改端点
@router.get("/radar-data/{data_id}/thumb")
async def get_radar_data_thumb_endpoint(
data_id: int,
db: AsyncSession = Depends(get_db),
_: User = Depends(require_write_permission) # 添加权限检查
):
# 仅当用户有写权限时才允许按需构建
...
```
**方案 B:配置控制**
```python
# 在 .env 中添加
RADAR_PREVIEW_BUILD_ON_DEMAND_READONLY=false
# 在 radar.py 中修改
if settings.RADAR_PREVIEW_BUILD_ON_DEMAND:
if current_user.role == "readonly" and not settings.RADAR_PREVIEW_BUILD_ON_DEMAND_READONLY:
# 只返回已有缓存,不触发构建
raise HTTPException(status_code=404, detail="预览图未生成")
build_status = await _build_radar_preview_cache(record, db, force=False)
```
**方案 C:改为 POST 端点(最符合 RESTful 原则)**
```python
# 将按需构建改为显式 POST 操作
@router.post("/radar-data/{data_id}/build-preview")
async def build_radar_preview_endpoint(
data_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_write_permission)
):
# 显式构建预览图
...
# GET 端点只返回已有缓存
@router.get("/radar-data/{data_id}/thumb")
async def get_radar_data_thumb_endpoint(data_id: int, db: AsyncSession = Depends(get_db)):
# 不触发构建,只返回已有文件
if not os.path.exists(geo_cache_path):
raise HTTPException(status_code=404, detail="预览图未生成,请先调用构建接口")
return FileResponse(geo_cache_path, ...)
```
**推荐方案**:方案 C(最符合 RESTful 原则)
---
### 1.3 AOI token 内存存储无容量上限
**问题描述**:
- `_AOI_TOKEN_STORE` 是全局字典,无大小限制
- 每次查询 AOI 时会续期 token(`expires_at = now + AOI_TOKEN_TTL_SECONDS`)
- 恶意用户可以通过频繁查询不同 AOI 区域来填满内存
**根本原因**:
```python
# dependencies.py:131
_AOI_TOKEN_STORE: Dict[str, Dict[str, Any]] = {} # 无容量限制
# dependencies.py:850
payload["expires_at"] = now + AOI_TOKEN_TTL_SECONDS # 每次访问都续期
```
**影响等级**:🔴 高危
- 认证后的低权限账号可以制造内存泄漏
- 30 分钟 TTL + 续期机制 = 长期占用内存
- 可能导致进程 OOM 崩溃
**修复方案**:
**方案 A:添加容量上限(推荐)**
```python
# dependencies.py
AOI_TOKEN_MAX_STORE_SIZE = read_int_env(
"AOI_TOKEN_MAX_STORE_SIZE",
1000, # 最多存储 1000 个 token
minimum=100,
maximum=10000,
)
async def _store_aoi_token(aoi_wkt: str, feature_collection: Dict[str, Any]) -> str:
token = uuid.uuid4().hex
now = time.monotonic()
async with _AOI_TOKEN_LOCK:
_cleanup_expired_aoi_tokens(now)
# 检查容量上限
if len(_AOI_TOKEN_STORE) >= AOI_TOKEN_MAX_STORE_SIZE:
# 删除最旧的 token(按 expires_at 排序)
oldest_tokens = sorted(
_AOI_TOKEN_STORE.items(),
key=lambda x: x[1]["expires_at"]
)[:100] # 删除最旧的 100 个
for old_token, _ in oldest_tokens:
_AOI_TOKEN_STORE.pop(old_token, None)
_AOI_TOKEN_STORE[token] = {
"aoi_wkt": aoi_wkt,
"aoi_geojson": feature_collection,
"expires_at": now + AOI_TOKEN_TTL_SECONDS,
}
return token
```
**方案 B:限制续期次数**
```python
# 在 token payload 中添加续期计数
_AOI_TOKEN_STORE[token] = {
"aoi_wkt": aoi_wkt,
"aoi_geojson": feature_collection,
"expires_at": now + AOI_TOKEN_TTL_SECONDS,
"renew_count": 0, # 添加续期计数
"max_renews": 5, # 最多续期 5 次
}
# 在 _get_aoi_from_token 中检查
async def _get_aoi_from_token(aoi_token: Optional[str]) -> Optional[Tuple[str, Dict[str, Any]]]:
...
payload = _AOI_TOKEN_STORE.get(normalized_token)
if not payload:
return None
# 检查续期次数
if payload.get("renew_count", 0) >= payload.get("max_renews", 5):
# 达到续期上限,不再续期
return payload.get("aoi_wkt"), payload.get("aoi_geojson")
payload["expires_at"] = now + AOI_TOKEN_TTL_SECONDS
payload["renew_count"] = payload.get("renew_count", 0) + 1
return payload.get("aoi_wkt"), payload.get("aoi_geojson")
```
**方案 C:使用 Redis(生产环境推荐)**
```python
# 使用 Redis 替代内存字典,自动过期
import redis.asyncio as redis
_redis_client = redis.Redis(
host=settings.REDIS_HOST,
port=settings.REDIS_PORT,
decode_responses=False,
)
async def _store_aoi_token(aoi_wkt: str, feature_collection: Dict[str, Any]) -> str:
token = uuid.uuid4().hex
payload = {
"aoi_wkt": aoi_wkt,
"aoi_geojson": feature_collection,
}
await _redis_client.setex(
f"aoi_token:{token}",
AOI_TOKEN_TTL_SECONDS,
json.dumps(payload)
)
return token
```
**推荐方案**:方案 A(短期)+ 方案 C(长期)
---
## 2. 中危问题分析与修复方案
### 2.1 extract_disp_results 失败计数重复累加
**问题描述**:
```python
# envi_service.py:1717, 1726, 1729
except OSError as e:
task_failed += 1
failed += 1 # 第一次累加
task_status = f"error: {e}"
...
failed += task_failed # 第二次累加(1729 行)
```
**影响等级**:🟡 中危
- 统计数据不准确,影响监控和告警
- 可能导致误报(失败数翻倍)
**修复方案**:
```python
# envi_service.py:1717 和 1726 行,删除 failed += 1
except OSError as e:
task_failed += 1
# 删除: failed += 1
task_status = f"error: {e}"
# 保留 1729 行的汇总
failed += task_failed # 只在这里累加一次
```
---
### 2.2 AOI 文件解析异常未统一转为 4xx
**问题描述**:
```python
# dependencies.py:792, 800
# 解析 GeoJSON/Shapefile 时的异常直接抛出,返回 500
aoi_wkt = _parse_geojson_to_wkt(geojson_data) # 可能抛出 ValueError
```
**影响等级**:🟡 中危
- 用户输入错误导致 500 错误,应该返回 400
- 错误语义不一致,影响 API 可用性
**修复方案**:
```python
# dependencies.py 中添加统一异常处理
try:
aoi_wkt = _parse_geojson_to_wkt(geojson_data)
except (ValueError, KeyError, TypeError) as e:
raise HTTPException(
status_code=400,
detail=f"GeoJSON 格式错误: {str(e)}"
)
except Exception as e:
logger.error(f"AOI 解析失败: {e}")
raise HTTPException(
status_code=500,
detail="AOI 解析失败,请联系管理员"
)
```
---
### 2.3 路径归属判断使用 startswith
**问题描述**:
```python
# data_service.py:530
if not any(file_path_abs.startswith(root) for root in radar_roots):
continue
```
**影响等级**:🟡 中危
- 前缀误匹配:`/data/radar` 会匹配 `/data/radar_backup`
- 可能将不属于监控目录的文件误判为合法
**修复方案**:
```python
# 使用 os.path.commonpath 或规范化路径比较
def is_subpath(child: str, parent: str) -> bool:
"""检查 child 是否是 parent 的子路径"""
try:
child_abs = os.path.abspath(child)
parent_abs = os.path.abspath(parent)
common = os.path.commonpath([child_abs, parent_abs])
return common == parent_abs
except (ValueError, TypeError):
return False
# data_service.py:530
if not any(is_subpath(file_path_abs, root) for root in radar_roots):
continue
```
---
### 2.4 解包安全校验仅检查 member.name
**问题描述**:
```python
# unpack_archives.py:98-104
def _is_safe_tar_member(member_name):
norm_name = os.path.normpath(member_name)
if os.path.isabs(norm_name):
return False
if norm_name.startswith(".."):
return False
return True
# 但仍使用 extractall,未检查符号链接/硬链接
tar.extractall(path=tmp_dir) # 181 行
```
**影响等级**:🟡 中危
- 符号链接可以指向任意路径(如 `/etc/passwd`)
- 硬链接可以覆盖系统文件
- 如果归档文件来自不可信来源,存在目录逃逸风险
**修复方案**:
```python
# unpack_archives.py
def _is_safe_tar_member(member):
"""检查 tar 成员是否安全(包括符号链接检查)"""
# 检查路径
norm_name = os.path.normpath(member.name)
if os.path.isabs(norm_name):
return False
if norm_name.startswith("..") or norm_name.startswith("../") or norm_name.startswith("..\\"):
return False
# 检查符号链接
if member.issym() or member.islnk():
# 检查链接目标是否安全
link_target = member.linkname
if os.path.isabs(link_target) or link_target.startswith(".."):
return False
return True
def _validate_tar_members(tar_obj, archive_path):
for member in tar_obj.getmembers():
if not _is_safe_tar_member(member): # 传入 member 对象而非 name
raise IOError(f"unsafe tar entry detected: {member.name} in {archive_path}")
# 使用安全的逐个提取方式
def _safe_extract_tar(tar_obj, output_dir):
"""安全地提取 tar 文件"""
for member in tar_obj.getmembers():
if not _is_safe_tar_member(member):
raise IOError(f"unsafe tar entry: {member.name}")
# 逐个提取,避免 extractall
tar_obj.extract(member, path=output_dir, filter='data') # Python 3.12+
# 或者对于旧版本:
# tar_obj.extract(member, path=output_dir)
# 在 181 行替换
with tarfile.open(archive_path, "r:*") as tar:
_validate_tar_members(tar, archive_path)
_safe_extract_tar(tar, tmp_dir) # 替换 tar.extractall
```
---
## 3. 低危问题分析与修复方案
### 3.1 启动脚本写 nginx.conf 未显式编码
**问题描述**:
```powershell
# start_app.ps1:388
$NewConfContent | Set-Content -LiteralPath "$NginxConfPath" -NoNewline
# 未指定 -Encoding UTF8
```
**影响等级**:🟢 低危
- PowerShell 5 默认使用 UTF-16 LE 编码
- Nginx 期望 UTF-8 编码,可能导致配置解析失败
**修复方案**:
```powershell
# start_app.ps1:388
$NewConfContent | Set-Content -LiteralPath "$NginxConfPath" -NoNewline -Encoding UTF8
```
---
## 4. 修复优先级建议
### P0(立即修复)
1. ✅ **1.1 ENVI 主流程返回值缺失** - 影响所有工作流任务
2. ✅ **1.2 只读用户触发写操作** - 权限绕过 + DoS 风险
### P1(本周内修复)
3. ✅ **1.3 AOI token 内存泄漏** - DoS 风险
4. ✅ **2.1 失败计数重复累加** - 影响监控准确性
### P2(两周内修复)
5. ✅ **2.2 AOI 解析异常处理** - API 可用性问题
6. ✅ **2.3 路径归属判断** - 潜在安全风险
### P3(一个月内修复)
7. ✅ **2.4 解包安全校验** - 取决于归档文件来源可信度
8. ✅ **3.1 PowerShell 编码** - 低影响,但易修复
---
## 5. 修复验证清单
### 1.1 ENVI 主流程返回值
- [ ] 运行 D-InSAR 工作流任务
- [ ] 检查任务状态是否为 SUCCESS
- [ ] 检查日志是否包含 `workflow=dinsar_custom`
### 1.2 只读用户权限
- [ ] 使用只读账号访问 `/radar-data/{id}/thumb`
- [ ] 验证是否返回 403 或 404(不触发构建)
- [ ] 使用写权限账号验证按需构建仍可用
### 1.3 AOI token 容量
- [ ] 创建 1000+ 个不同 AOI token
- [ ] 验证内存占用是否稳定
- [ ] 检查日志是否有 token 清理记录
### 2.1 失败计数
- [ ] 运行 `extract_disp_results` 并故意触发失败
- [ ] 验证返回的 `failed` 计数是否准确
### 2.2 AOI 解析异常
- [ ] 上传无效 GeoJSON 文件
- [ ] 验证是否返回 400 而非 500
### 2.3 路径归属判断
- [ ] 创建 `/data/radar_backup/test.tif` 文件
- [ ] 验证是否被误判为 `/data/radar` 的子路径
### 2.4 解包安全
- [ ] 创建包含符号链接的 tar 文件
- [ ] 验证是否被拒绝解包
### 3.1 PowerShell 编码
- [ ] 在 PowerShell 5 环境运行启动脚本
- [ ] 验证 nginx.conf 是否为 UTF-8 编码
---
## 6. 长期改进建议
### 6.1 引入速率限制
```python
# 使用 slowapi 限制 API 调用频率
from slowapi import Limiter
from slowapi.util import get_remote_address
limiter = Limiter(key_func=get_remote_address)
@router.get("/radar-data/{data_id}/thumb")
@limiter.limit("10/minute") # 每分钟最多 10 次
async def get_radar_data_thumb_endpoint(...):
...
```
### 6.2 添加审计日志
```python
# 记录所有写操作和高开销操作
async def audit_log(
user_id: int,
action: str,
resource: str,
details: Dict[str, Any]
):
await db.execute(
insert(AuditLogORM).values(
user_id=user_id,
action=action,
resource=resource,
details=details,
timestamp=datetime.utcnow(),
)
)
```
### 6.3 使用 Redis 替代内存缓存
- AOI token 存储
- 预览图构建队列
- 速率限制计数器
### 6.4 添加资源配额
```python
# 限制每个用户的资源使用
USER_QUOTA = {
"readonly": {
"max_concurrent_requests": 5,
"max_aoi_tokens": 10,
"max_preview_builds_per_hour": 0,
},
"user": {
"max_concurrent_requests": 20,
"max_aoi_tokens": 50,
"max_preview_builds_per_hour": 100,
},
"admin": {
"max_concurrent_requests": 100,
"max_aoi_tokens": 500,
"max_preview_builds_per_hour": 1000,
},
}
```
---
## 7. 总结
本次审计发现了 **3 个高危问题**、**4 个中危问题**、**1 个低危问题**,主要集中在:
1. 函数返回值缺失导致任务失败
2. 权限控制不严格(读账号可触发写操作)
3. 资源无限制(内存泄漏风险)
4. 统计数据不准确
5. 异常处理不规范
建议按照优先级逐步修复,并在修复后进行充分的回归测试。长期来看,应该引入更完善的权限管理、资源配额、审计日志和速率限制机制。
+330
View File
@@ -0,0 +1,330 @@
# 安全修复快速检查清单
## 使用说明
- 每次修复前检查此清单
- 每次修复后更新状态
- 测试失败立即停止,分析原因
---
## 第一阶段检查清单
### ✅ 1.1 ENVI 主流程返回值
- [x] 代码已修改
- [ ] 本地测试通过
- [ ] 集成测试通过
- [ ] 无副作用
- [ ] 可以继续下一项
**快速测试命令**:
```bash
# 运行一个 D-InSAR 工作流
# 在前端 IDL 自动化面板中提交任务
# 观察任务状态和日志
```
---
### ✅ 1.2 失败计数重复累加
- [x] 代码已修改
- [ ] 本地测试通过
- [ ] 集成测试通过
- [ ] 无副作用
- [ ] 可以继续下一项
**快速测试命令**:
```bash
# 在前端 IDL 自动化面板中
# 使用 "Step 3: 提取 Disp 结果" 功能
# 故意触发一些失败(如修改文件权限)
# 检查返回的统计数据
```
---
### ✅ 1.3 PowerShell 编码
- [x] 代码已修改
- [ ] 本地测试通过
- [ ] 集成测试通过
- [ ] 无副作用
- [ ] 可以继续下一项
**快速测试命令**:
```powershell
# 运行启动脚本
.\scripts\start_app.ps1
# 检查 nginx.conf 编码
Get-Content .\nginx\conf\nginx.conf -Encoding UTF8
# 验证 Nginx 启动
curl http://localhost:8080
```
---
## 第二阶段检查清单
### ⏳ 2.1 AOI token 容量上限
- [ ] 代码已修改
- [ ] 环境变量已配置
- [ ] 本地测试通过
- [ ] 压力测试通过
- [ ] 内存监控正常
- [ ] 无副作用
- [ ] 可以继续下一项
**快速测试脚本**:
```python
# 创建测试脚本 test_aoi_token_limit.py
import requests
import json
base_url = "http://localhost:8000"
token = "your_auth_token"
# 创建 1500 个 AOI token
for i in range(1500):
geojson = {
"type": "FeatureCollection",
"features": [{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [100 + i * 0.001, 30 + i * 0.001]
}
}]
}
response = requests.post(
f"{base_url}/api/aoi/upload",
headers={"Authorization": f"Bearer {token}"},
json=geojson
)
if i % 100 == 0:
print(f"Created {i} tokens")
print("Test completed")
```
---
### ⏳ 2.2 AOI 解析异常处理
- [ ] 代码已修改
- [ ] 本地测试通过
- [ ] 前端兼容性测试通过
- [ ] 错误消息清晰
- [ ] 无副作用
- [ ] 可以继续下一项
**快速测试数据**:
```json
// 无效 GeoJSON 1:缺少 type
{
"features": []
}
// 无效 GeoJSON 2:错误的坐标
{
"type": "FeatureCollection",
"features": [{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [999, 999]
}
}]
}
// 无效 GeoJSON 3:格式错误
{
"type": "FeatureCollection",
"features": "not an array"
}
```
---
### ⏳ 2.3 路径归属判断
- [ ] 代码已修改
- [ ] 本地测试通过
- [ ] 边界情况测试通过
- [ ] 扫描结果准确
- [ ] 无副作用
- [ ] 可以继续下一项
**快速测试步骤**:
```bash
# 1. 创建测试目录结构
mkdir -p /data/radar_backup
mkdir -p /data/radar/subdir
# 2. 放入测试文件
touch /data/radar_backup/test.tif
touch /data/radar/subdir/test.tif
# 3. 运行数据扫描
# 在前端数据管理面板中点击"刷新"
# 4. 验证结果
# radar_backup 的文件不应该出现在列表中
# radar/subdir 的文件应该出现在列表中
```
---
## 第三阶段检查清单
### ⏳ 3.1 解包安全校验
- [ ] 代码已修改
- [ ] 本地测试通过
- [ ] 安全测试通过
- [ ] 性能测试通过
- [ ] 无副作用
- [ ] 可以继续下一项
**快速测试脚本**:
```python
# 创建测试脚本 test_tar_security.py
import tarfile
import os
# 测试 1:创建包含符号链接的 tar
with tarfile.open("test_symlink.tar", "w") as tar:
# 创建一个符号链接指向 /etc/passwd
info = tarfile.TarInfo(name="link_to_passwd")
info.type = tarfile.SYMTYPE
info.linkname = "/etc/passwd"
tar.addfile(info)
# 测试 2:创建包含 .. 路径的 tar
with tarfile.open("test_escape.tar", "w") as tar:
info = tarfile.TarInfo(name="../../../etc/passwd")
info.size = 0
tar.addfile(info)
# 测试 3:创建正常的 tar
with tarfile.open("test_normal.tar", "w") as tar:
# 创建一个正常文件
info = tarfile.TarInfo(name="normal_file.txt")
info.size = 5
tar.addfile(info, fileobj=io.BytesIO(b"hello"))
print("Test archives created")
print("test_symlink.tar - should be rejected")
print("test_escape.tar - should be rejected")
print("test_normal.tar - should be accepted")
```
---
## 紧急回滚程序
如果任何测试失败,立即执行:
```bash
# 1. 查看最近的提交
git log --oneline -5
# 2. 回滚到上一个提交
git revert HEAD
# 3. 或者硬回滚(谨慎使用)
git reset --hard HEAD~1
# 4. 重启服务
# 停止所有服务
# 重新运行 start_app.ps1
# 5. 验证系统恢复正常
curl http://localhost:8080
```
---
## 测试环境要求
### 最小测试环境
- [ ] Python 3.9+
- [ ] PostgreSQL 运行中
- [ ] Ollama 运行中(如果测试 AI 功能)
- [ ] 至少 1 个 D-InSAR 结果数据
- [ ] 至少 1 个雷达数据
### 推荐测试环境
- [ ] 完整的开发环境
- [ ] 测试数据库(非生产)
- [ ] 监控工具(内存、CPU)
- [ ] 日志查看工具
---
## 测试数据准备
### D-InSAR 测试数据
```
需要准备:
- 至少 1 个完整的 Task_* 目录
- 包含 dinsar_results/out_ISARPTD_*_rsp_disp
- 用于测试 extract_disp_results 功能
```
### 雷达数据测试
```
需要准备:
- 至少 2 个 .tif 文件
- 放在 MONITOR_RADAR_DIRS 配置的目录中
- 用于测试预览图生成和路径判断
```
### AOI 测试数据
```
需要准备:
- 有效的 GeoJSON 文件
- 无效的 GeoJSON 文件(多种错误类型)
- 用于测试异常处理
```
---
## 性能基准
记录修复前后的性能指标:
### ENVI 工作流
- 修复前成功率:_%
- 修复后成功率:_%
- 平均耗时:_秒
### AOI token
- 修复前内存占用:_MB
- 修复后内存占用:_MB
- Token 数量上限:_个
### 解包性能
- 修复前平均耗时:_秒
- 修复后平均耗时:_秒
- 性能差异:_%
---
## 签名确认
每完成一个阶段,填写以下信息:
### 第一阶段
- 修复人:_______
- 测试人:_______
- 完成日期:_______
- 签名:_______
### 第二阶段
- 修复人:_______
- 测试人:_______
- 完成日期:_______
- 签名:_______
### 第三阶段
- 修复人:_______
- 测试人:_______
- 完成日期:_______
- 签名:_______
+478
View File
@@ -0,0 +1,478 @@
# 安全问题分阶段修复计划
## 修复原则
1. **安全第一**:每个阶段修复后必须充分测试,确保不影响现有功能
2. **最小改动**:优先选择改动最小、风险最低的方案
3. **可回滚**:每个阶段独立提交,出问题可以快速回滚
4. **渐进式**:从简单到复杂,从低风险到高风险
---
## 第一阶段:明确 Bug 修复(低风险)
**目标**:修复明确的代码错误,不涉及架构变更
### 1.1 ENVI 主流程返回值缺失 ✅ 已完成
**文件**:`backend/app/services/envi_service.py:1630`
**修改**:
```python
# 修改前
if error:
raise RuntimeError(error)
# 修改后
if error:
raise RuntimeError(error)
return record # 添加此行
```
**风险评估**:🟢 极低
- 只添加一行返回语句
- 不改变任何逻辑
- 修复了明确的 bug(成功时应该返回 record)
**测试计划**:
- [ ] 运行一个完整的 D-InSAR 工作流(dinsar_custom)
- [ ] 检查任务状态是否正确标记为 SUCCESS
- [ ] 检查日志是否包含 `workflow=dinsar_custom duration=XXXs`
- [ ] 验证前端是否正确显示任务完成
---
### 1.2 失败计数重复累加
**文件**:`backend/app/services/envi_service.py:1717, 1727`
**修改**:
```python
# 修改前(1717 行)
except OSError as e:
task_failed += 1
failed += 1 # 删除此行
task_status = f"error: {e}"
# 修改后
except OSError as e:
task_failed += 1
task_status = f"error: {e}"
# 修改前(1727 行)
except OSError as e:
task_failed += 1
failed += 1 # 删除此行
task_status = f"error: {e}"
# 修改后
except OSError as e:
task_failed += 1
task_status = f"error: {e}"
# 保留 1730 行的汇总
failed += task_failed # 只在这里累加一次
```
**风险评估**:🟢 极低
- 只删除重复的累加语句
- 不改变业务逻辑
- 修复统计数据不准确的问题
**测试计划**:
- [ ] 运行 `extract_disp_results` 功能
- [ ] 故意触发一些失败(如权限问题)
- [ ] 验证返回的 `failed` 计数是否准确(不翻倍)
---
### 1.3 PowerShell 编码问题
**文件**:`scripts/start_app.ps1:388`
**修改**:
```powershell
# 修改前
$NewConfContent | Set-Content -LiteralPath "$NginxConfPath" -NoNewline
# 修改后
$NewConfContent | Set-Content -LiteralPath "$NginxConfPath" -NoNewline -Encoding UTF8
```
**风险评估**:🟢 极低
- 只添加编码参数
- 确保 nginx.conf 使用 UTF-8 编码
- 避免 PowerShell 5 默认 UTF-16 导致的问题
**测试计划**:
- [ ] 在 PowerShell 5 环境运行启动脚本
- [ ] 检查生成的 nginx.conf 文件编码(使用 `file` 命令或文本编辑器)
- [ ] 验证 Nginx 是否能正常启动和解析配置
---
**第一阶段提交**:
```
fix: 修复 ENVI 工作流返回值缺失和统计计数错误
- 修复 run_workflow 成功时不返回 record 的问题
- 修复 extract_disp_results 失败计数重复累加
- 修复 PowerShell 脚本 nginx.conf 编码问题
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
```
---
## 第二阶段:防御性增强(中风险)
**目标**:添加保护措施,不改变现有业务逻辑
### 2.1 AOI token 容量上限
**文件**:`backend/app/routers/dependencies.py`
**修改策略**:
1. 添加环境变量配置 `AOI_TOKEN_MAX_STORE_SIZE`(默认 1000)
2. 在 `_store_aoi_token` 中添加容量检查
3. 达到上限时删除最旧的 100 个 token
**实现**:
```python
# 在 dependencies.py 顶部添加
AOI_TOKEN_MAX_STORE_SIZE = read_int_env(
"AOI_TOKEN_MAX_STORE_SIZE",
1000,
minimum=100,
maximum=10000,
)
# 修改 _store_aoi_token 函数
async def _store_aoi_token(aoi_wkt: str, feature_collection: Dict[str, Any]) -> str:
token = uuid.uuid4().hex
now = time.monotonic()
async with _AOI_TOKEN_LOCK:
_cleanup_expired_aoi_tokens(now)
# 检查容量上限
if len(_AOI_TOKEN_STORE) >= AOI_TOKEN_MAX_STORE_SIZE:
# 删除最旧的 token(按 expires_at 排序)
oldest_tokens = sorted(
_AOI_TOKEN_STORE.items(),
key=lambda x: x[1]["expires_at"]
)[:100]
for old_token, _ in oldest_tokens:
_AOI_TOKEN_STORE.pop(old_token, None)
# 记录日志
print(f"[INFO] AOI token store reached limit, cleaned {len(oldest_tokens)} oldest tokens")
_AOI_TOKEN_STORE[token] = {
"aoi_wkt": aoi_wkt,
"aoi_geojson": feature_collection,
"expires_at": now + AOI_TOKEN_TTL_SECONDS,
}
return token
```
**风险评估**:🟡 中等
- 添加了新的容量限制逻辑
- 可能影响高并发场景下的 AOI 查询
- 但不改变现有 API 接口
**测试计划**:
- [ ] 创建 1000+ 个不同的 AOI token
- [ ] 验证内存占用是否稳定(不超过预期)
- [ ] 检查日志是否有 token 清理记录
- [ ] 验证被清理的 token 是否无法再使用
- [ ] 验证新创建的 token 仍然可用
---
### 2.2 AOI 解析异常处理
**文件**:`backend/app/routers/dependencies.py`
**修改策略**:
在所有 AOI 解析函数中添加统一的异常处理
**实现位置**:
- `_parse_geojson_to_wkt` 调用处
- `_parse_shapefile_to_wkt` 调用处
- 文件上传端点
**实现**:
```python
# 在相关端点中添加 try-except
try:
aoi_wkt = _parse_geojson_to_wkt(geojson_data)
except (ValueError, KeyError, TypeError) as e:
raise HTTPException(
status_code=400,
detail=f"GeoJSON 格式错误: {str(e)}"
)
except Exception as e:
logger.error(f"AOI 解析失败: {e}", exc_info=True)
raise HTTPException(
status_code=500,
detail="AOI 解析失败,请联系管理员"
)
```
**风险评估**:🟡 中等
- 改变了错误响应码(500 → 400)
- 可能影响前端错误处理逻辑
- 但提升了 API 语义正确性
**测试计划**:
- [ ] 上传无效的 GeoJSON 文件
- [ ] 验证是否返回 400 而非 500
- [ ] 验证错误消息是否清晰
- [ ] 验证前端是否能正确处理 400 错误
---
### 2.3 路径归属判断优化
**文件**:`backend/app/services/data_service.py:530`
**修改策略**:
添加辅助函数 `is_subpath`,使用 `os.path.commonpath` 进行准确判断
**实现**:
```python
# 在 data_service.py 顶部添加辅助函数
def is_subpath(child: str, parent: str) -> bool:
"""检查 child 是否是 parent 的子路径"""
try:
child_abs = os.path.abspath(child)
parent_abs = os.path.abspath(parent)
# 确保父路径以分隔符结尾,避免前缀误匹配
if not parent_abs.endswith(os.sep):
parent_abs += os.sep
if not child_abs.endswith(os.sep) and os.path.isdir(child_abs):
child_abs += os.sep
# 使用 commonpath 判断
common = os.path.commonpath([child_abs, parent_abs])
return common == parent_abs.rstrip(os.sep)
except (ValueError, TypeError):
return False
# 修改 530 行
# 修改前
if not any(file_path_abs.startswith(root) for root in radar_roots):
continue
# 修改后
if not any(is_subpath(file_path_abs, root) for root in radar_roots):
continue
```
**风险评估**:🟡 中等
- 改变了路径判断逻辑
- 可能影响文件扫描结果
- 但修复了潜在的安全问题
**测试计划**:
- [ ] 在 `MONITOR_RADAR_DIRS` 旁边创建 `MONITOR_RADAR_DIRS_backup` 目录
- [ ] 放入测试文件
- [ ] 运行数据扫描
- [ ] 验证 backup 目录的文件不会被误判为合法
---
**第二阶段提交**:
```
feat: 添加 AOI token 容量限制和异常处理增强
- 添加 AOI_TOKEN_MAX_STORE_SIZE 配置(默认 1000)
- 达到上限时自动清理最旧的 token
- 统一 AOI 解析异常处理(400 vs 500)
- 优化路径归属判断,避免前缀误匹配
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
```
---
## 第三阶段:安全加固(高风险)
**目标**:解决权限和安全问题,可能涉及架构调整
### 3.1 解包安全校验增强
**文件**:`scripts/unpack_archives.py`
**修改策略**:
1. 修改 `_is_safe_tar_member` 接收 member 对象而非字符串
2. 添加符号链接和硬链接检查
3. 使用逐个提取替代 `extractall`
**实现**:
```python
def _is_safe_tar_member(member):
"""检查 tar 成员是否安全(包括符号链接检查)"""
# 检查路径
norm_name = os.path.normpath(member.name)
if os.path.isabs(norm_name):
return False
if norm_name.startswith("..") or norm_name.startswith("../") or norm_name.startswith("..\\"):
return False
# 检查符号链接和硬链接
if member.issym() or member.islnk():
link_target = member.linkname
if os.path.isabs(link_target) or link_target.startswith(".."):
return False
return True
def _validate_tar_members(tar_obj, archive_path):
for member in tar_obj.getmembers():
if not _is_safe_tar_member(member):
raise IOError(f"unsafe tar entry detected: {member.name} in {archive_path}")
def _safe_extract_tar(tar_obj, output_dir):
"""安全地提取 tar 文件"""
for member in tar_obj.getmembers():
if not _is_safe_tar_member(member):
raise IOError(f"unsafe tar entry: {member.name}")
# 逐个提取
tar_obj.extract(member, path=output_dir)
# 在 181 行替换
with tarfile.open(archive_path, "r:*") as tar:
_validate_tar_members(tar, archive_path)
_safe_extract_tar(tar, output_dir) # 替换 tar.extractall
```
**风险评估**:🟡 中等
- 改变了解包逻辑
- 可能影响解包性能(逐个提取 vs 批量提取)
- 但提升了安全性
**测试计划**:
- [ ] 创建包含符号链接的 tar 文件
- [ ] 验证是否被拒绝解包
- [ ] 创建正常的 tar 文件
- [ ] 验证是否能正常解包
- [ ] 对比解包性能(前后差异)
---
### 3.2 只读用户权限控制(暂缓)
**状态**:⏸️ 暂缓到第四阶段
**原因**:
- 涉及权限系统重构
- 需要修改多个端点
- 可能影响前端调用逻辑
- 需要更充分的测试
**备选方案**:
- 方案 A:添加权限检查依赖
- 方案 B:配置控制(允许/禁止只读用户触发构建)
- 方案 C:改为 POST 端点(需要前端配合)
**决策点**:
- 是否有只读用户在使用系统?
- 只读用户是否需要查看预览图?
- 是否可以接受只读用户触发构建?
---
**第三阶段提交**:
```
security: 增强 tar 解包安全校验
- 添加符号链接和硬链接检查
- 使用逐个提取替代 extractall
- 防止目录逃逸和文件覆盖攻击
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
```
---
## 第四阶段:权限重构(待定)
**目标**:解决只读用户权限问题
**前置条件**:
- 第一、二、三阶段全部完成并稳定运行
- 明确只读用户的使用场景和需求
- 前端团队配合(如果需要修改 API 调用)
**待讨论问题**:
1. 系统中是否有只读用户?
2. 只读用户的典型使用场景是什么?
3. 是否可以接受只读用户无法查看预览图?
4. 是否需要前端配合修改?
---
## 修复时间表
| 阶段 | 预计时间 | 风险等级 | 依赖 |
|------|---------|---------|------|
| 第一阶段 | 1 天 | 🟢 低 | 无 |
| 第二阶段 | 2-3 天 | 🟡 中 | 第一阶段完成 |
| 第三阶段 | 2-3 天 | 🟡 中 | 第二阶段完成 |
| 第四阶段 | 待定 | 🔴 高 | 需求确认 |
---
## 回滚计划
每个阶段独立提交,出现问题时可以:
1. **快速回滚**:
```bash
git revert <commit-hash>
```
2. **部分回滚**:
如果只有某个修复有问题,可以单独回滚该文件:
```bash
git checkout <previous-commit> -- <file-path>
git commit -m "revert: 回滚 <file-path> 的修改"
```
3. **紧急回滚**:
如果影响生产环境,立即回滚到上一个稳定版本:
```bash
git reset --hard <last-stable-commit>
```
---
## 监控指标
修复后需要监控的关键指标:
### 第一阶段
- ENVI 工作流成功率(应该提升)
- 任务状态准确性
- 统计数据准确性
### 第二阶段
- AOI token 内存占用(应该稳定)
- 400 vs 500 错误比例(400 应该增加)
- 路径扫描结果准确性
### 第三阶段
- 解包成功率(应该不变)
- 解包性能(可能略微下降)
- 安全事件(应该为 0)
---
## 总结
这个分阶段修复计划:
- ✅ 从低风险到高风险渐进式修复
- ✅ 每个阶段独立可测试、可回滚
- ✅ 优先修复明确的 bug,暂缓架构性改动
- ✅ 保留了灵活性,可以根据实际情况调整
**建议**:先完成第一阶段,充分测试后再决定是否继续第二阶段。
+278
View File
@@ -0,0 +1,278 @@
# 安全问题修复进度跟踪
## 修复状态图例
- ✅ 已完成
- 🚧 进行中
- ⏸️ 暂缓
- ⏳ 待开始
- ❌ 已回滚
---
## 第一阶段:明确 Bug 修复
### 1.1 ENVI 主流程返回值缺失
- **状态**:✅ 已完成
- **修改文件**:`backend/app/services/envi_service.py:1630`
- **修改内容**:添加 `return record`
- **提交时间**:2026-03-04
- **测试状态**:⏳ 待测试
**测试清单**:
- [ ] 运行完整的 D-InSAR 工作流(dinsar_custom)
- [ ] 检查任务状态是否为 SUCCESS
- [ ] 检查日志是否包含 `workflow=dinsar_custom duration=XXXs`
- [ ] 验证前端显示是否正常
**测试结果**:
```
待填写...
```
---
### 1.2 失败计数重复累加
- **状态**:✅ 已完成
- **修改文件**:`backend/app/services/envi_service.py:1717, 1726`
- **修改内容**:删除重复的 `failed += 1`(两处)
- **提交时间**:2026-03-04
- **测试状态**:⏳ 待测试
**测试清单**:
- [ ] 运行 `extract_disp_results` 功能
- [ ] 故意触发失败(如权限问题)
- [ ] 验证 `failed` 计数是否准确
**测试结果**:
```
待填写...
```
---
### 1.3 PowerShell 编码问题
- **状态**:✅ 已完成(已增强)
- **修改文件**:`scripts/start_app.ps1:388`
- **修改内容**:使用 UTF-8 无 BOM 编码写入
- **提交时间**:2026-03-04
- **测试状态**:✅ 已验证
**额外修复**:
- 发现 `nginx.conf` 文件包含 UTF-8 BOM,导致 Nginx 启动失败
- 已删除 BOM(使用 `tail -c +4`)
- 修改脚本使用 `[System.IO.File]::WriteAllText()` 确保无 BOM
**测试清单**:
- [ ] 在 PowerShell 5 环境运行启动脚本
- [ ] 检查 nginx.conf 文件编码
- [ ] 验证 Nginx 是否正常启动
**测试结果**:
```
待填写...
```
---
**第一阶段总结**:
- 完成度:3/3 (100%) ✅
- 完成时间:2026-03-04
- 遇到的问题:无
- 回滚记录:无
- **状态**:代码修改已完成,等待测试验证
---
## 第二阶段:防御性增强
### 2.1 AOI token 容量上限
- **状态**:⏳ 待开始
- **修改文件**:`backend/app/routers/dependencies.py`
- **修改内容**:添加 `AOI_TOKEN_MAX_STORE_SIZE` 配置和容量检查
- **提交时间**:-
- **测试状态**:-
**测试清单**:
- [ ] 创建 1000+ 个 AOI token
- [ ] 验证内存占用是否稳定
- [ ] 检查日志是否有清理记录
- [ ] 验证被清理的 token 无法使用
- [ ] 验证新 token 仍可用
**测试结果**:
```
待填写...
```
---
### 2.2 AOI 解析异常处理
- **状态**:⏳ 待开始
- **修改文件**:`backend/app/routers/dependencies.py`
- **修改内容**:添加统一异常处理(400 vs 500)
- **提交时间**:-
- **测试状态**:-
**测试清单**:
- [ ] 上传无效 GeoJSON 文件
- [ ] 验证返回 400 而非 500
- [ ] 验证错误消息清晰
- [ ] 验证前端能正确处理
**测试结果**:
```
待填写...
```
---
### 2.3 路径归属判断优化
- **状态**:⏳ 待开始
- **修改文件**:`backend/app/services/data_service.py:530`
- **修改内容**:使用 `is_subpath` 替代 `startswith`
- **提交时间**:-
- **测试状态**:-
**测试清单**:
- [ ] 创建 `MONITOR_RADAR_DIRS_backup` 目录
- [ ] 放入测试文件
- [ ] 运行数据扫描
- [ ] 验证 backup 目录文件不被误判
**测试结果**:
```
待填写...
```
---
**第二阶段总结**:
- 完成度:0/3 (0%)
- 预计完成时间:-
- 遇到的问题:-
- 回滚记录:无
---
## 第三阶段:安全加固
### 3.1 解包安全校验增强
- **状态**:⏳ 待开始
- **修改文件**:`scripts/unpack_archives.py`
- **修改内容**:添加符号链接检查,使用逐个提取
- **提交时间**:-
- **测试状态**:-
**测试清单**:
- [ ] 创建包含符号链接的 tar 文件
- [ ] 验证被拒绝解包
- [ ] 创建正常 tar 文件
- [ ] 验证正常解包
- [ ] 对比解包性能
**测试结果**:
```
待填写...
```
---
**第三阶段总结**:
- 完成度:0/1 (0%)
- 预计完成时间:-
- 遇到的问题:-
- 回滚记录:无
---
## 第四阶段:权限重构
### 4.1 只读用户权限控制
- **状态**:⏸️ 暂缓
- **原因**:需要明确需求和前端配合
- **待讨论问题**:
- [ ] 系统中是否有只读用户?
- [ ] 只读用户的使用场景?
- [ ] 是否可以接受只读用户无法查看预览图?
- [ ] 是否需要前端配合?
---
## 总体进度
| 阶段 | 状态 | 完成度 | 预计时间 | 实际时间 |
|------|------|--------|---------|---------|
| 第一阶段 | ✅ 已完成 | 3/3 (100%) | 1 天 | 2026-03-04 |
| 第二阶段 | ⏳ 待开始 | 0/3 (0%) | 2-3 天 | - |
| 第三阶段 | ⏳ 待开始 | 0/1 (0%) | 2-3 天 | - |
| 第四阶段 | ⏸️ 暂缓 | - | 待定 | - |
**总完成度**:3/7 (43%)
---
## 问题记录
### 问题 #1:Nginx 启动失败 - UTF-8 BOM 问题
- **发现时间**:2026-03-04
- **问题描述**:Nginx 启动失败,错误信息 `unknown directive "worker_processes"`,发现 `nginx.conf` 文件包含 UTF-8 BOM(`ef bb bf`)
- **影响范围**:Nginx 无法启动,整个系统无法访问
- **解决方案**:
1. 使用 `tail -c +4` 删除现有文件的 BOM
2. 修改 PowerShell 脚本使用 `[System.IO.File]::WriteAllText()` 确保写入时无 BOM
- **状态**:✅ 已解决
### 问题 #2:PROJ 数据库版本冲突
- **发现时间**:2026-03-05
- **问题描述**:扫描 D-InSAR 结果时出现 PROJ 警告 `DATABASE.LAYOUT.VERSION.MINOR = 2 whereas >= 3 is expected`,PostgreSQL 自带的 PROJ 数据库版本过旧
- **影响范围**:不影响功能,但产生大量警告日志
- **解决方案**:
1. 在 `run_backend.py` 中自动检测并设置 `PROJ_LIB` 环境变量
2. 指向 GDAL 自带的新版 PROJ 数据库
- **状态**:✅ 已修复,待测试验证
### 问题 #3:扫描任务时前端未锁定
- **发现时间**:2026-03-05
- **问题描述**:D-InSAR 结果扫描任务运行时前端没有锁定
- **影响范围**:用户可能在扫描过程中进行其他操作,导致数据不一致
- **可能原因**:
1. 扫描任务执行太快(< 3 秒),前端轮询间隔(3 秒)来不及捕获
2. 任务状态更新有延迟
- **解决方案**:待确认是否需要修复(可能是正常行为)
- **状态**:⏳ 待分析
---
## 回滚记录
### 回滚 #1
- **回滚时间**:-
- **回滚内容**:-
- **回滚原因**:-
- **影响范围**:-
---
## 下一步行动
1. **立即行动**:
- [ ] 测试 1.1 ENVI 主流程返回值修复
- [ ] 如果测试通过,继续修复 1.2 和 1.3
2. **本周计划**:
- [ ] 完成第一阶段所有修复
- [ ] 充分测试第一阶段
- [ ] 提交第一阶段代码
3. **下周计划**:
- [ ] 根据第一阶段测试结果决定是否继续
- [ ] 如果继续,开始第二阶段
---
## 备注
- 每次修复后立即更新此文档
- 测试结果详细记录,包括成功和失败的情况
- 遇到问题及时记录到"问题记录"区域
- 回滚操作必须记录原因和影响范围
+271
View File
@@ -0,0 +1,271 @@
# 第一阶段修复总结(2026-03-04)
## 修复概览
**阶段**:第一阶段 - 明确 Bug 修复
**状态**:✅ 代码修改完成,等待测试验证
**完成时间**:2026-03-04
**风险等级**:🟢 低风险
---
## 修复内容
### 1.1 ENVI 主流程返回值缺失 ✅
**问题**:`run_workflow()` 函数在成功执行后不返回 `record` 字典,导致下游代码崩溃。
**修改文件**:`backend/app/services/envi_service.py:1630`
**修改内容**:
```python
# 修改前
if error:
raise RuntimeError(error)
# 函数结束,返回 None
# 修改后
if error:
raise RuntimeError(error)
return record # 添加此行
```
**影响**:
- 修复了所有 ENVI 工作流任务在成功后被误判为失败的问题
- 下游代码可以正确访问 `run_meta.get('workflow')` 等字段
**风险评估**:🟢 极低
- 只添加一行返回语句
- 不改变任何业务逻辑
- 修复明确的 bug
---
### 1.2 失败计数重复累加 ✅
**问题**:`extract_disp_results()` 函数中失败计数被重复累加,导致统计数据翻倍。
**修改文件**:`backend/app/services/envi_service.py:1717, 1726`
**修改内容**:
```python
# 修改前(1717 行)
except OSError as e:
task_failed += 1
failed += 1 # 删除此行
task_status = f"error: {e}"
# 修改后
except OSError as e:
task_failed += 1
task_status = f"error: {e}"
# 修改前(1726 行)
except OSError as e:
task_failed += 1
failed += 1 # 删除此行
task_status = f"error: {e}"
# 修改后
except OSError as e:
task_failed += 1
task_status = f"error: {e}"
# 保留 1730 行的汇总(只在这里累加一次)
failed += task_failed
```
**影响**:
- 修复了失败计数翻倍的问题
- 统计数据现在准确反映实际失败数量
**风险评估**:🟢 极低
- 只删除重复的累加语句
- 不改变业务逻辑
- 修复统计错误
---
### 1.3 PowerShell 编码问题 ✅
**问题**:启动脚本写入 `nginx.conf` 时未指定编码,PowerShell 5 默认使用 UTF-16,导致 Nginx 无法解析。
**修改文件**:`scripts/start_app.ps1:388`
**修改内容**:
```powershell
# 修改前
$NewConfContent | Set-Content -LiteralPath "$NginxConfPath" -NoNewline
# 修改后
$NewConfContent | Set-Content -LiteralPath "$NginxConfPath" -NoNewline -Encoding UTF8
```
**影响**:
- 确保 `nginx.conf` 使用 UTF-8 编码
- 避免 Nginx 配置解析失败
**风险评估**:🟢 极低
- 只添加编码参数
- 不改变文件内容
- 提升兼容性
---
## 代码验证
### Python 语法检查
```bash
cd "Z:\Code\Insar_management_system_v2\backend\app\services"
python -m py_compile envi_service.py
```
**结果**:✅ 通过
### 修改文件列表
1. `backend/app/services/envi_service.py` - 3 处修改
2. `scripts/start_app.ps1` - 1 处修改
---
## 测试计划
### 1.1 ENVI 主流程返回值测试
**测试步骤**:
1. 启动后端服务
2. 在前端 IDL 自动化面板中提交一个 D-InSAR 工作流任务
3. 等待任务完成
4. 检查任务状态
**预期结果**:
- 任务状态显示为 SUCCESS
- 日志包含 `workflow=dinsar_custom duration=XXXs`
- 前端正确显示任务完成信息
**测试命令**:
```bash
# 查看任务日志
tail -f backend/logs/app.log
# 查看 ENVI 运行日志
ls backend/runtime/*.log
```
---
### 1.2 失败计数测试
**测试步骤**:
1. 准备一个包含 `dinsar_results` 的 Task 目录
2. 故意修改某些文件权限,使复制失败
3. 在前端使用 "Step 3: 提取 Disp 结果" 功能
4. 检查返回的统计数据
**预期结果**:
- `failed` 计数等于实际失败的文件数(不翻倍)
- 统计数据准确
**测试命令**:
```bash
# 修改文件权限(Windows)
icacls "Z:\Test_data\Task_xxx\dinsar_results\out_ISARPTD_xxx_rsp_disp.dat" /deny Everyone:F
# 查看返回的统计数据
# 在前端点击"提取 Disp 结果"后查看响应
```
---
### 1.3 PowerShell 编码测试
**测试步骤**:
1. 在 PowerShell 5 环境运行启动脚本
2. 检查生成的 `nginx.conf` 文件编码
3. 验证 Nginx 是否正常启动
**预期结果**:
- `nginx.conf` 文件编码为 UTF-8
- Nginx 正常启动,无配置解析错误
**测试命令**:
```powershell
# 运行启动脚本
.\scripts\start_app.ps1
# 检查文件编码(使用 file 命令或文本编辑器)
# 在 VS Code 中打开 nginx/conf/nginx.conf,右下角显示编码
# 验证 Nginx 启动
curl http://localhost:8080
```
---
## 回滚计划
如果测试失败,可以快速回滚:
### 回滚 1.1 ENVI 返回值
```python
# 删除 envi_service.py:1631 行的 return record
# 恢复为:
if error:
raise RuntimeError(error)
# 函数结束
```
### 回滚 1.2 失败计数
```python
# 在 envi_service.py:1718 和 1727 行恢复
except OSError as e:
task_failed += 1
failed += 1 # 恢复此行
task_status = f"error: {e}"
```
### 回滚 1.3 PowerShell 编码
```powershell
# 删除 start_app.ps1:388 行的 -Encoding UTF8
$NewConfContent | Set-Content -LiteralPath "$NginxConfPath" -NoNewline
```
---
## 下一步行动
### 立即行动
1. ⏳ 执行第一阶段测试计划
2. ⏳ 记录测试结果到 `SECURITY_FIX_PROGRESS.md`
### 测试通过后
1. 决定是否继续第二阶段修复
2. 如果继续,开始第二阶段:防御性增强
### 测试失败时
1. 分析失败原因
2. 记录到 `SECURITY_FIX_PROGRESS.md` 的"问题记录"区域
3. 决定是否回滚
4. 如果回滚,记录回滚原因
---
## 文档更新
已更新的文档:
- ✅ `SECURITY_FIX_PROGRESS.md` - 更新修复状态和总体进度
- ✅ `SECURITY_FIX_CHECKLIST.md` - 标记代码修改完成
- ✅ `CLAUDE.md` - 更新当前任务状态
---
## 总结
第一阶段修复已完成所有代码修改,共修复 3 个明确的 bug:
1. ✅ ENVI 主流程返回值缺失
2. ✅ 失败计数重复累加
3. ✅ PowerShell 编码问题
所有修改都是低风险的,不涉及架构变更,只修复明确的代码错误。Python 语法检查已通过,现在等待功能测试验证。
**风险评估**:🟢 低风险
**建议**:可以安全地进行测试,不太可能影响现有功能。
+273
View File
@@ -0,0 +1,273 @@
# 安全审计与修复总览
## 📋 文档导航
本次安全审计产生了以下文档,请按顺序阅读:
1. **[SECURITY_AUDIT_2026-03-04.md](./SECURITY_AUDIT_2026-03-04.md)** - 原始审计报告
- 审计人员提供的问题清单
- 包含代码位置和影响分析
2. **[SECURITY_AUDIT_ANALYSIS_2026-03-04.md](./SECURITY_AUDIT_ANALYSIS_2026-03-04.md)** - 详细分析文档
- 每个问题的根本原因分析
- 多种修复方案对比
- 长期改进建议
3. **[SECURITY_FIX_PLAN.md](./SECURITY_FIX_PLAN.md)** - 分阶段修复计划 ⭐
- 四个阶段的详细修复计划
- 风险评估和测试方案
- 回滚计划和时间表
4. **[SECURITY_FIX_PROGRESS.md](./SECURITY_FIX_PROGRESS.md)** - 进度跟踪
- 实时更新的修复进度
- 测试结果记录
- 问题和回滚记录
5. **[SECURITY_FIX_CHECKLIST.md](./SECURITY_FIX_CHECKLIST.md)** - 快速检查清单
- 每个修复项的检查清单
- 快速测试命令和脚本
- 紧急回滚程序
---
## 🎯 问题概览
### 高危问题(3 个)
1. **ENVI 主流程返回值缺失** - 导致所有工作流任务失败
2. **只读用户可触发写操作** - 权限绕过 + DoS 风险
3. **AOI token 内存泄漏** - 可导致进程 OOM
### 中危问题(4 个)
1. **失败计数重复累加** - 统计数据不准确
2. **AOI 解析异常处理** - 用户输入错误返回 500
3. **路径归属判断错误** - 前缀误匹配风险
4. **解包安全校验不足** - 符号链接逃逸风险
### 低危问题(1 个)
1. **PowerShell 编码问题** - 可能导致 Nginx 配置解析失败
---
## 🚀 修复策略
### 分阶段修复原则
1. **安全第一** - 每个阶段充分测试后再继续
2. **最小改动** - 优先选择风险最低的方案
3. **可回滚** - 每个阶段独立提交
4. **渐进式** - 从简单到复杂
### 修复阶段
#### 第一阶段:明确 Bug 修复(1 天)
- ✅ ENVI 主流程返回值缺失
- ⏳ 失败计数重复累加
- ⏳ PowerShell 编码问题
**风险等级**:🟢 低
**状态**:🚧 进行中(1/3 完成)
#### 第二阶段:防御性增强(2-3 天)
- ⏳ AOI token 容量上限
- ⏳ AOI 解析异常处理
- ⏳ 路径归属判断优化
**风险等级**:🟡 中
**状态**:⏳ 待开始
**前置条件**:第一阶段完成并测试通过
#### 第三阶段:安全加固(2-3 天)
- ⏳ 解包安全校验增强
**风险等级**:🟡 中
**状态**:⏳ 待开始
**前置条件**:第二阶段完成并测试通过
#### 第四阶段:权限重构(待定)
- ⏸️ 只读用户权限控制
**风险等级**:🔴 高
**状态**:⏸️ 暂缓
**原因**:需要明确需求和前端配合
---
## 📊 当前状态
### 总体进度
- **已完成**:1/8 (12.5%)
- **进行中**:1/8 (12.5%)
- **待开始**:6/8 (75%)
- **已暂缓**:1/8 (12.5%)
### 已完成的修复
1. ✅ ENVI 主流程返回值缺失
- 文件:`backend/app/services/envi_service.py:1630`
- 修改:添加 `return record`
- 状态:代码已修改,待测试
### 下一步行动
1. 测试 ENVI 主流程返回值修复
2. 如果测试通过,继续修复 1.2 和 1.3
3. 完成第一阶段后提交代码
---
## 🧪 测试要求
### 第一阶段测试
- [ ] ENVI 工作流端到端测试
- [ ] 失败计数准确性测试
- [ ] PowerShell 编码测试
### 第二阶段测试
- [ ] AOI token 压力测试(1000+ tokens)
- [ ] AOI 解析异常处理测试
- [ ] 路径判断边界测试
### 第三阶段测试
- [ ] 解包安全测试(符号链接、路径逃逸)
- [ ] 解包性能测试
---
## 🔄 回滚计划
每个阶段独立提交,出现问题时可以:
### 快速回滚
```bash
git revert <commit-hash>
```
### 部分回滚
```bash
git checkout <previous-commit> -- <file-path>
git commit -m "revert: 回滚 <file-path> 的修改"
```
### 紧急回滚
```bash
git reset --hard <last-stable-commit>
```
---
## 📈 监控指标
### 关键指标
- ENVI 工作流成功率
- AOI token 内存占用
- API 错误率(400 vs 500)
- 解包性能
### 监控工具
- 应用日志
- 系统资源监控
- 数据库性能监控
---
## 🤝 协作流程
### 修复流程
1. 阅读修复计划(SECURITY_FIX_PLAN.md)
2. 执行代码修改
3. 本地测试
4. 更新进度文档(SECURITY_FIX_PROGRESS.md)
5. 提交代码
6. 集成测试
7. 部署到测试环境
### 测试流程
1. 阅读检查清单(SECURITY_FIX_CHECKLIST.md)
2. 准备测试数据
3. 执行测试用例
4. 记录测试结果
5. 更新进度文档
### 问题处理流程
1. 发现问题立即记录到进度文档
2. 分析问题原因
3. 决定是否回滚
4. 如果回滚,记录回滚原因
5. 修复问题后重新测试
---
## 📞 联系方式
### 遇到问题时
1. 查看进度文档的"问题记录"区域
2. 查看分析文档的相关章节
3. 如果需要回滚,参考回滚计划
### 需要讨论时
- 第四阶段(权限重构)需要讨论需求
- 遇到无法解决的技术问题
- 需要调整修复计划
---
## 📝 更新日志
### 2026-03-04
- ✅ 创建审计分析文档
- ✅ 创建分阶段修复计划
- ✅ 创建进度跟踪文档
- ✅ 创建快速检查清单
- ✅ 完成 1.1 ENVI 主流程返回值修复(代码已修改,待测试)
---
## 🎓 经验总结
### 审计发现的根本问题
1. **错误处理不完善** - 成功路径缺少返回值
2. **权限控制不严格** - GET 请求触发写操作
3. **资源管理不当** - 内存无限增长
4. **统计逻辑错误** - 重复累加
5. **安全校验不足** - 路径判断、解包校验
### 改进建议
1. 引入代码审查流程
2. 添加单元测试覆盖
3. 使用静态分析工具
4. 定期安全审计
5. 完善错误处理和日志
---
## 📚 参考资料
### 相关文档
- [CLAUDE.md](../CLAUDE.md) - 项目工作备忘录
- [AI_ANALYSIS_REDESIGN.md](./AI_ANALYSIS_REDESIGN.md) - AI 分析模块设计
- [RECOMMENDED_VLM_MODELS.md](./RECOMMENDED_VLM_MODELS.md) - 推荐多模态模型
### 外部资源
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
- [Python Security Best Practices](https://python.readthedocs.io/en/stable/library/security_warnings.html)
- [FastAPI Security](https://fastapi.tiangolo.com/tutorial/security/)
---
## ✅ 下一步
1. **立即行动**:
- 测试 1.1 ENVI 主流程返回值修复
- 准备测试环境和测试数据
2. **本周计划**:
- 完成第一阶段所有修复
- 充分测试第一阶段
- 提交第一阶段代码
3. **下周计划**:
- 根据第一阶段测试结果决定是否继续
- 如果继续,开始第二阶段
---
**最后更新**:2026-03-04
**文档版本**:v1.0
**维护人**:Claude Opus 4.6
+115
View File
@@ -0,0 +1,115 @@
# InSAR Management System TODO(持续维护)
最后更新:2026-02-10
维护目标:帮助开发者快速定位风险、统一改造节奏、避免“修一处坏一处”。
---
## 一、当前总体状态
- 认证授权:已切换为 `Session + Cookie`,管理员/只读权限可用。
- 用户管理:管理员可创建与管理账号,前端面板已接入。
- 审计日志:高风险写接口审计已接入,前端可查询。
- 雷达源图预览:已进入 **Scheme B(后端预纠正缓存)** 实装阶段。
- 调度模式:当前默认 `Manual-only`,通过手动触发扫描,不启用后台定时任务。
---
## 二、任务看板(按优先级)
## P0(本周优先)
### 1) SEC-01 鉴权与权限边界完善
- **状态**:DONE(首版完成,持续加固)
- **目标**:确保所有写接口只允许管理员,所有新增接口必须复用权限依赖。
- **验收**:
- 未登录访问受限接口返回 `401`
- 只读用户写操作返回 `403`
- 管理员写操作返回 `2xx`
### 2) SEC-01A 审计链路闭环
- **状态**:DONE(首版完成,持续加固)
- **目标**:高风险操作均可追踪“谁、何时、做了什么”。
- **验收**:
- 后端 `GET /api/auth/audit-logs` 可查关键操作
- 前端审计面板支持过滤与检索
### SEC-02(新增)部署安全基线收敛
- **状态**:TODO
- **目标**:降低默认配置带来的部署风险,形成可复制的最小安全基线。
- **验收**:
- 生产环境 `CORS_ORIGINS` 使用明确白名单(不使用 `*`)
- 部署文档明确 `.env` 密钥管理规范(不入库、不共享)
- HTTP/HTTPS 场景下 `AUTH_COOKIE_SECURE` 配置策略明确
### 3) GEO-01 Scheme B 纠正缓存主链路
- **状态**:DONE(首版落地)
- **范围**:
- 双缓存目录:`radar_geo` + `radar_raw`
- 后端透视纠正生成地理预览缓存
- `thumb` 接口优先 `radar_geo`,失败回退 `radar_raw`
- 新增状态与重建接口
### 4) GEO-02 纠正缓存状态可观测
- **状态**:DONE(首版落地)
- **范围**:
- 数据库记录 `preview_cache_status/version/path/error/updated_at`
- 前端列表显示状态徽标(纠正/回退/失败/未建)
- 管理员可“单条重建缓存”
---
## P1(下一阶段)
### 5) GEO-03 真实数据稳定性验证
- **状态**:TODO
- **目标**:验证升轨/降轨/大倾斜场景下叠加稳定性。
- **验收**:
- 覆盖面与源影像视觉对齐明显改善
- 地图缩放/平移无明显卡顿
- 构建失败样本可在日志与状态中定位
### 6) PERF-01 大批量扫描性能压测
- **状态**:TODO
- **目标**:评估 1000+ 场景增量扫描耗时与缓存命中率。
- **验收**:
- 二次扫描命中率 > 95%
- 失败重试不影响主扫描完成
### 7) MAINT-01 前端 `App.jsx` 模块化拆分
- **状态**:TODO
- **目标**:降低单文件复杂度,便于持续维护。
---
## 三、里程碑记录
### 2026-02-10(Session 鉴权阶段)
- 完成 Session + Cookie 鉴权首版。
- 完成管理员初始化与用户管理基础链路。
- 修复登录阶段 `MissingGreenlet` 异常。
### 2026-02-10(审计与运维阶段)
- 接入高风险写接口审计日志。
- 前端新增审计日志面板。
### 2026-02-10(Scheme B 首版实装)
- 新增 `radar_geo` 与 `radar_raw` 双缓存策略。
- 扫描流程支持增量构建纠正缓存与回退缓存。
- 新增接口:
- `GET /api/radar-data/{id}/preview-status`
- `POST /api/radar-data/{id}/rebuild-preview-cache`
- 前端源影像列表支持状态显示与管理员重建。
- 方向判定升级:优先读取 XML 的 `refRow/refColumn` 映射像素角与地理角,减少方向错位。
### 2026-02-10(文档与实现对齐)
- 明确当前扫描调度模式为 `Manual-only`(非定时守护)。
- 补充部署安全基线项(CORS 白名单、`.env` 密钥管理、Cookie 安全策略)。
---
## 四、开发注意事项(必须遵守)
- 不得破坏 `start_system.bat` 与 `scripts/start_app.ps1` 的建库/校验/修复启动链路。
- 任何数据库结构变更后,必须同步更新 `scripts/init_db.py` 的非破坏性补齐能力。
- 新增功能必须同步更新 `docs/DEPLOYMENT.md` 与本文件。
+94
View File
@@ -0,0 +1,94 @@
# 水体提取与洪涝监测 — 开发 TODO
## 架构概览
```
Phase 1: 水体提取 → WaterMaskORM(每景影像的水体掩膜)
Phase 2: 水体配对 → WaterPairORM(参考期 + 监测期)
Phase 3: 变化检测 → FloodEventORM(洪涝事件 + 告警)
```
依赖库(InSAR conda 环境已有):rasterio, shapely, numpy, scipy, Pillow
---
## 任务列表
### Step 1:ORM 建表
- [x] W01 `orm.py` 新增 WaterMaskORM、WaterPairORM、FloodEventORM
- [x] W02 `models/__init__.py` 导出新 ORM
- [x] W03 Alembic migration 0002_water_tables.py
### Step 2:核心服务
- [x] W04 `water_service.py` Phase 1 — 单景水体提取(OTSU + 形态学 + 矢量化)
- [x] W05 `water_service.py` Phase 2 — 水体配对逻辑(空间重叠 + 时间筛选)
- [x] W06 `water_service.py` Phase 3 — 变化检测(掩膜差值 + 面积统计 + 告警)
### Step 3:Job 集成
- [x] W07 `job_handlers.py` 新增 WATER_EXTRACT / WATER_DETECT job 类型
### Step 4:路由
- [x] W08 `routers/water.py` 全部端点
- [x] W09 `routers/__init__.py` 注册 water router
### Step 5:前端
- [x] W10 `api/water.js` API 层
- [x] W11 `WaterMonitorPanel.jsx` 三 Tab 面板(提取/配对/事件)
- [x] W12 `App.jsx` 注册新面板入口
---
## 端点设计
```
POST /water/extract # 批量提取(传 radar_data_ids 列表)
GET /water/masks # 查询水体掩膜列表(分页)
GET /water/masks/{id}/preview # 水体掩膜预览图
POST /water/pairs # 创建配对(reference_id + monitor_id)
GET /water/pairs # 查询配对列表
POST /water/pairs/{id}/detect # 对指定配对执行变化检测
GET /water/events # 查询洪涝事件列表
GET /water/events/{id}/preview # 变化图预览
```
---
## 关键算法备忘
### OTSU(纯 numpy 实现,不依赖 scikit-image)
```python
def _otsu_threshold(arr_db):
hist, bin_edges = np.histogram(arr_db[np.isfinite(arr_db)], bins=256)
bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
total = hist.sum()
w0 = np.cumsum(hist) / total
w1 = 1 - w0
mu0 = np.cumsum(hist * bin_centers) / (np.cumsum(hist) + 1e-10)
mu1 = (np.sum(hist * bin_centers) - np.cumsum(hist * bin_centers)) / (np.cumsum(hist[::-1])[::-1] + 1e-10)
sigma_b = w0 * w1 * (mu0 - mu1) ** 2
return bin_centers[np.argmax(sigma_b)]
```
### 水体掩膜预览图
- 原始影像灰度拉伸为背景(半透明)
- 水体区域叠加蓝色(RGBA: 0, 100, 255, 180)
- 新增水体叠加红色(洪涝变化图)
---
## 状态
**全部完成** ✓ — 等待测试
### 测试步骤
1. 重启后端(新表由 create_all 自动创建)
2. 前端强制刷新,进入"结果分析"→"水体监测"
3. Phase 1:输入已入库的雷达数据 ID,提交提取任务,等待完成后刷新列表
4. Phase 2:选参考期和监测期掩膜 ID,创建配对
5. Phase 3:对配对点击"执行变化检测",在"洪涝事件"Tab 查看结果
### 已知限制
- `_find_geocoded_path` 依赖 `preview_cache_path` 或 `file_path`,若雷达数据没有地理编码文件则提取失败
- 告警阈值默认 30%,可通过 `.env` 中 `FLOOD_ALERT_THRESHOLD=0.3` 调整
+187
View File
@@ -0,0 +1,187 @@
# 水体监测模块 v2 — 任务清单
## 背景
原 water_service.py(手动 OTSU + 形态学)方案废弃。
改用 SARscape 原生任务链,精度更高,与 D-InSAR 流程一致。
## SARscape 任务链
```
单景预处理(每景独立):
SARsBasicMultilooking → 多视处理(SLC → 强度图,降噪)
SARsBasicGeocoding → 地理编码 + 辐射定标(输出 dB 图)
洪涝检测(两景配对):
SARsBasicFeFloodingClassification → 洪涝分类图
SARsBasicFeFloodingClassificationRefinement → MRF 精化(可选)
```
## 关键 Task 参数速查
### SARsBasicMultilooking
- INPUT_SARSCAPEDATA (required)
- OUTPUT_SARSCAPEDATA (output)
- RANGE_MULTILOOK, AZIMUTH_MULTILOOK (可选,默认自动)
- GRID_SIZE_FOR_SUGGESTED_LOOKS (目标分辨率 m)
- ROOT_URI_FOR_OUTPUT
### SARsBasicGeocoding
- INPUT_SARSCAPEDATA (required)
- OUTPUT_SARSCAPEDATA, OUTPUT_DB_SARSCAPEDATA (output)
- DEM_SARSCAPEDATA (可选,有 DEM 精度更高)
- GEOCODE_GRID_SIZE_X, GEOCODE_GRID_SIZE_Y (像素大小 m)
- CALIBRATION: true(辐射定标)
- OUTPUT_TYPE: "output_type_db"(输出 dB)
- ROOT_URI_FOR_OUTPUT
### SARsBasicFeFloodingClassification
- INPUT_SARSCAPEDATA (required, 灾前)
- POST_EVENT_FILE (required, 灾后)
- OUTPUT_SARSCAPEDATA (output, 分类图)
- DEM_FILE, SLOPE_FILE (可选,提升精度)
- SWL_TH: 水体阈值 dB(默认约 -14)
- RATIO_TH: 比值阈值 dB
- HIGH_SCATT_POINT_TH: 高散射点阈值 dB
- RATIO_SARSCAPEDATA, PRE_EVENT_SARSCAPEDATA, POST_EVENT_SARSCAPEDATA (output)
- ROOT_URI_FOR_OUTPUT
### SARsBasicFeFloodingClassificationRefinement
- PRE_EVENT_FILE, POST_EVENT_FILE, CLASSIFIED_FILE, RATIO_FILE (required)
- OUTPUT_SARSCAPEDATA (output)
- DEM_FILE, SLOPE_FILE (可选)
- MRF 参数:M_STABLE_WATER, M_FLOOD, ALPHA_STABLE_WATER, ALPHA_FLOOD 等
- ROOT_URI_FOR_OUTPUT
---
## 任务列表
### W2-01 [TODO] 清理旧 water 代码
- 删除 backend/app/services/water_service.py
- 删除 backend/app/routers/water.py
- 清理 backend/app/models/orm.py 中的 WaterMaskORM / WaterPairORM / FloodEventORM
- 清理 backend/app/models/__init__.py 中的 water 导入
- 清理 backend/app/services/job_handlers.py 中的 WATER_EXTRACT / WATER_DETECT
- 删除 backend/alembic/versions/0002_water_tables.py
- 删除 frontend/src/api/water.js
- 删除 frontend/src/WaterMonitorPanel.jsx
- 清理 frontend/src/config/appConstants.js(移除 'water' tab)
- 清理 frontend/src/utils/appUiHelpers.js(移除 water case)
- 清理 frontend/src/App.jsx(移除 WaterMonitorPanel 引用)
### W2-02 [TODO] 设计新 ORM 模型
新增两张表:
**SARSceneGeoORM** (sar_scene_geo)
- id, radar_data_id (FK → radar_data.id)
- geo_path: 地理编码 dB 文件路径(无扩展名,ENVI 格式)
- pixel_size_m: 像素大小
- status: PENDING / RUNNING / DONE / FAILED
- error_msg
- created_at, updated_at
**FloodDetectionORM** (flood_detections)
- id
- pre_scene_id (FK → sar_scene_geo.id)
- post_scene_id (FK → sar_scene_geo.id)
- output_dir: 输出目录
- classified_path: 分类图路径
- flood_area_km2: 洪涝面积
- stable_water_area_km2: 稳定水体面积
- status: PENDING / RUNNING / DONE / FAILED
- error_msg
- created_at, updated_at
### W2-03 [TODO] 编写 Alembic 迁移
- 新建 backend/alembic/versions/0002_water_v2.py
- down_revision = "0001"
- 创建 sar_scene_geo 和 flood_detections 表
- 注意:旧 water_masks / water_pairs / flood_events 表如存在需 drop
### W2-04 [TODO] 实现单景预处理服务
新建 backend/app/services/water_service.py(全新)
函数:`run_geocoding_workflow_sync(radar_data_id, db_session, job_id)`
1. 查 RadarDataORM 获取 file_path
2. 找 SLC 文件(_slc 后缀,ENVI 格式)
3. 创建输出目录:`water_results/{radar_unique_id}/`
4. 调用 execute_envi_task("SARsBasicMultilooking", ...)
5. 调用 execute_envi_task("SARsBasicGeocoding", ..., OUTPUT_TYPE="output_type_db")
6. 解析输出路径,更新 SARSceneGeoORM.status = DONE
7. 写进度文件(复用 _write_progress 机制)
### W2-05 [TODO] 实现洪涝检测服务
在 water_service.py 中新增:
函数:`run_flood_detection_sync(pre_scene_id, post_scene_id, db_session, job_id, refine=False)`
1. 查两个 SARSceneGeoORM,验证 status=DONE
2. 创建输出目录:`water_results/flood_{pre_id}_{post_id}/`
3. 调用 execute_envi_task("SARsBasicFeFloodingClassification", ...)
4. 可选:调用 SARsBasicFeFloodingClassificationRefinement
5. 用 rasterio 读分类图,统计各类像素面积(km²)
6. 更新 FloodDetectionORM
### W2-06 [TODO] 更新 job_handlers.py
- 注册 JOB_TYPE_WATER_GEOCODE = "WATER_GEOCODE"
- 注册 JOB_TYPE_WATER_FLOOD = "WATER_FLOOD"
- 实现 _handle_water_geocode / _handle_water_flood
- 加入 _HANDLERS 字典
### W2-07 [TODO] 更新 envi_runner_cli.py
- choices 增加 "water_geocode" / "water_flood"
- main() 中路由到对应 water_service 函数
### W2-08 [TODO] 新建 water.py router
- POST /water/geocode (admin): 提交单景地理编码任务
- GET /water/scenes: 列出已处理场景(关联 radar_data 的 satellite/date)
- POST /water/flood-detect (admin): 提交洪涝检测任务
- GET /water/flood-events: 列出洪涝检测结果
- GET /water/flood-events/{id}/preview: 返回分类图预览
### W2-09 [TODO] 新建 WaterMonitorPanel.jsx(前端)
三个 Tab:
**Tab 1 — 单景预处理**
- 从雷达数据列表选择一景(或输入 radar_data_id)
- 显示已处理场景列表(状态、像素大小、处理时间)
- 提交按钮 → POST /water/geocode
**Tab 2 — 洪涝检测**
- 选择灾前场景 + 灾后场景(从已处理列表选)
- 可选:是否启用 MRF 精化
- 提交按钮 → POST /water/flood-detect
**Tab 3 — 洪涝事件**
- 列表:灾前日期、灾后日期、洪涝面积、稳定水体面积、状态
- 点击查看分类图预览
### W2-10 [TODO] 注册路由 + 前端 Tab
- backend/app/routers/__init__.py 引入 water router
- frontend/src/config/appConstants.js 加 'water' tab
- frontend/src/utils/appUiHelpers.js 加 case
- frontend/src/App.jsx 引入 WaterMonitorPanel
- frontend/src/api/water.js(新建 API 封装)
### W2-11 [TODO] 测试验证
- 用 Image_Pool_2025 中一景 LT1 数据测试单景预处理
- 选两景配对测试洪涝检测
- 验证分类图输出和面积统计
---
## 进度
| 任务 | 状态 |
|------|------|
| W2-01 清理旧代码 | DONE |
| W2-02 新 ORM 模型 | DONE |
| W2-03 Alembic 迁移 | DONE |
| W2-04 单景预处理服务 | DONE |
| W2-05 洪涝检测服务 | DONE |
| W2-06 job_handlers | DONE |
| W2-07 envi_runner_cli | DONE |
| W2-08 water.py router | DONE |
| W2-09 WaterMonitorPanel | DONE |
| W2-10 路由注册+前端Tab | DONE |
| W2-11 测试验证 | TODO |
+403
View File
@@ -0,0 +1,403 @@
# InSAR 管理系统项目汇报
版本日期:2026-03-13
## 一、汇报目的
本文用于项目汇报和方案评审,重点说明三件事:
1. 当前 InSAR 管理系统的建设内容和系统价值。
2. 系统在 D-InSAR 生产侧已经形成的技术架构与扩展能力。
3. 如何将 LANDSAR 系统作为第三个 D-InSAR 生产核心嵌入本系统,以及这件事对采购方案的直接影响。
本文是单独汇报材料,强调整体把握、系统边界、实施路径和采购判断,不展开到底层代码细节。
## 二、项目总体定位
本项目不是单一的数据展示平台,而是一个面向 InSAR 业务的综合管理与生产支撑平台,核心目标是把“数据入库、生产组织、结果管理、运行监控、质量分析”放到同一套系统中统一管理。
从业务定位看,系统承担了以下角色:
1. 数据管理平台:统一管理雷达源数据、轨道数据、D-InSAR 结果、水体监测结果等。
2. 生产组织平台:支持配对、批次管理、任务下发、后台处理、结果回收。
3. 运维监控平台:支持健康检查、任务状态、日志、鉴权、许可证控制。
4. 分析辅助平台:支持 AI 质量分析、AI 诊断、统计看板、地图可视化。
因此,这个项目的价值不只是“能不能跑一次处理”,而是把整个业务流程固化为可持续运行的平台能力。
## 三、当前系统建设情况
### 3.1 技术架构
当前系统采用前后端分离架构:
1. 前端:React + Vite,使用 Leaflet 负责地图展示,Chart.js 负责统计图表,Zustand 负责状态管理。
2. 后端:FastAPI 提供 REST API,SQLAlchemy 负责 ORM,Pydantic 负责数据模型校验。
3. 数据库:PostgreSQL + PostGIS,用于业务数据存储和空间能力支撑。
4. 后台任务:独立 Job Worker 进程,负责执行扫描、生产、AI、解包等异步任务。
5. 部署入口:Nginx 负责前端静态资源和反向代理。
6. 外部能力:可选接入 ENVI/SARscape、ISCE2、Ollama 等外部组件。
### 3.2 主要功能模块
系统目前已经具备较完整的业务模块:
1. 雷达数据管理
支持监控目录扫描、元数据解析、预览图生成、空间范围展示、检索和分页浏览。
2. 轨道数据管理
支持轨道文件关联和本地轨道池同步,为生产链路提供基础输入。
3. 配对与批次管理
支持 D-InSAR 配对策略、批量任务组织、批次项管理和后续复制导出。
4. D-InSAR 生产中心
已经从单引擎思路升级为多引擎生产中心,支持统一查看引擎状态、提交任务、查看运行记录。
5. D-InSAR 结果管理
支持结果扫描入库、地图展示、缓存、导出、AI 评分与人工标注。
6. AI 能力
支持质量模型训练、质量预测、影像诊断、报告生成。
7. 水体监测
已形成相对独立的处理链路和界面。
8. 运维与权限
包含健康检查、任务管理、审计日志、用户管理、登录限流、许可证控制等。
### 3.3 当前系统成熟度判断
从工程状态看,项目已经不是原型阶段,而是进入“可持续扩展的平台阶段”:
1. 业务边界已明确。
2. 前后端结构已稳定。
3. 后台任务与数据库模型已成体系。
4. 多引擎 D-InSAR 生产框架已经搭起来。
5. 系统具备继续接入第三方生产核心的基础。
这意味着后续采购不应再按“买一个孤立的软件工具”来考虑,而应按“纳入统一生产平台的外部生产核心”来考虑。
## 四、系统运行主线
从全流程看,当前系统的主线可以概括为:
1. 数据进入系统
扫描雷达目录、轨道目录、结果目录,完成入库和索引。
2. 用户在前端组织任务
选择 AOI、筛选源数据、执行配对、形成批次、下发生产任务。
3. 后端进入异步处理模式
系统创建任务记录和作业记录,由 Worker 执行具体处理。
4. 外部引擎执行生产
当前已支持 ENVI/SARscape 和 ISCE2 两类生产能力。
5. 结果回流系统
产物落地后可扫描入库、生成缓存、展示到地图和列表,并进入后续分析。
6. 运维与质量闭环
通过健康检查、日志、审计、AI 分析和人工核查,形成可运维闭环。
这一主线决定了本系统天然适合接入多个生产引擎,只要外部引擎能被标准化调用。
## 五、D-InSAR 生产体系现状
### 5.1 已形成的多引擎框架
当前项目已经不再把 D-InSAR 生产写死在某一套工具链上,而是抽象成统一的“生产引擎”框架。对每个引擎,系统要求至少具备三类能力:
1. 可用性检查:系统要知道该引擎当前能不能用。
2. Profile 列表:系统要知道该引擎支持哪些处理链路。
3. 统一运行入口:系统要能按同样的方式下发任务并接收结果。
在这个框架下,系统现在已有三类引擎标识:
1. `sarscape`
已可运行,对接现有 ENVI/SARscape 流程。
2. `isce2`
已可运行,通过 WSL 调用 ISCE2 链路。
3. `landsar`
目前仅做接口预留和前端占位,尚未真正接入生产能力。
### 5.2 当前已落地的两个生产核心
#### 1. ENVI / SARscape
这是当前系统最成熟的生产核心,主要特点:
1. 与现有历史流程兼容度高。
2. 支持 `metatask` 和 `custom6` 两种 profile。
3. 已具备运行、日志、结果回收和健康检查能力。
4. 适合作为当前生产主力链路。
#### 2. ISCE2
这是当前系统第二个生产核心,主要特点:
1. 通过 WSL 方式运行,避免强依赖本机 Windows 图形环境。
2. 已具备环境检查、路径转换、脚本调用、任务执行能力。
3. 适合承接 LT-1 等标准化链路。
4. 为系统后续接入更多异构引擎提供了样板。
### 5.3 LANDSAR 当前状态
这一点在汇报和采购中必须明确说明:
截至 2026-03-13,`LANDSAR` 在本项目中属于“架构已预留、能力未接入”的状态,具体表现为:
1. 后端已存在 `landsar` 引擎占位。
2. 前端生产中心已预留 `LANDSAR` 状态卡位置。
3. 健康检查体系已将 `LANDSAR` 视为可纳入统一监控的引擎类型。
4. 轨道池体系中已预留 `ORBIT_POOL_LANDSAR` 的配置位置。
5. 但任务提交、执行、日志、结果回收尚未真正实现。
6. 当前生产提交接口实际仍只支持已落地的引擎,`LANDSAR` 还不能像现有生产核心那样直接投入运行。
这意味着:系统架构已经准备好接纳 LANDSAR,但采购后的接入工作仍需要正式实施,不能误判为“买来即可无缝运行”。
## 六、为什么要把 LANDSAR 作为第三个生产核心
从平台演进角度,引入 LANDSAR 的意义主要有四点:
1. 降低对单一商业引擎的依赖。
2. 提升不同场景下的处理适配能力。
3. 形成多引擎并行的生产格局,增强方案弹性。
4. 使采购的软件能力真正纳入平台统一调度,而不是形成新的信息孤岛。
如果 LANDSAR 只是作为独立软件单独运行,那么它对现有系统的价值有限,最终仍然会回到“人工切换工具、人工导入结果、人工追踪过程”的旧模式。只有把它嵌入本系统,成为第三个生产核心,采购价值才能最大化。
## 七、LANDSAR 接入的目标定位
本项目对 LANDSAR 的目标定位不是“重写 LANDSAR 算法”,而是“把 LANDSAR 系统现有 D-InSAR 生产服务纳入统一调度平台”。
因此,推荐的定位是:
1. 本系统负责任务组织、权限控制、日志审计、状态展示、结果入库和运维闭环。
2. LANDSAR 系统负责实际 D-InSAR 生产处理。
3. 双方通过标准接口交互,而不是把两边代码强行揉成一体。
这也是最符合采购落地的模式,因为它对供应商最明确,对我方平台风险最可控。
## 八、LANDSAR 与本系统的推荐交互模式
### 8.1 推荐模式:服务嵌入式接入
建议将 LANDSAR 以“外部生产服务”的方式接入本系统,而不是以人工操作方式接入。
推荐架构如下:
1. 用户仍然只在本系统前端操作。
2. 用户在 D-InSAR 生产中心中选择 `LANDSAR` 作为引擎。
3. 本系统后端创建标准任务和作业记录。
4. `landsar` 适配引擎把任务参数发送给 LANDSAR 生产服务。
5. LANDSAR 返回运行编号并执行处理。
6. 本系统轮询任务状态,或者接收 LANDSAR 回调通知。
7. 处理完成后,产物输出到约定目录,或由本系统拉取到指定目录。
8. 本系统完成结果扫描、入库、展示和后续分析。
这样做的最大好处是:对用户来说,LANDSAR 不是另一套系统,而是本系统中的第三个生产核心。
### 8.2 不推荐模式:纯人工导入导出
如果采购的 LANDSAR 只能做到“人工打开软件、手工点运行、手工拷贝结果”,则它不能真正成为第三个生产核心,只能成为一个外部工具。这样会带来以下问题:
1. 无法统一任务状态。
2. 无法统一日志与审计。
3. 无法统一批次管理。
4. 无法纳入健康检查。
5. 无法形成稳定的生产闭环。
因此,采购时必须把“可集成、可调用、可监控”作为硬条件。
## 九、LANDSAR 接入后的业务流程
建议将接入流程设计为以下八步:
1. 任务组织
用户在本系统完成配对、批次选择、AOI 约束和参数选择。
2. 任务提交
本系统向 LANDSAR 服务提交标准化请求,包括输入目录、输出目录、profile、处理参数、任务编号等。
3. 运行登记
本系统保存本地任务 ID 和 LANDSAR 侧运行 ID 的映射关系。
4. 执行监控
本系统通过轮询接口或回调机制获取进度、状态和错误信息。
5. 日志采集
本系统读取 LANDSAR 返回的执行日志摘要,必要时保留完整日志文件地址。
6. 产物回收
LANDSAR 将产物输出到双方约定目录,或通过下载接口交给本系统。
7. 结果入库
本系统对产物进行扫描、索引、缓存生成和结果入库。
8. 统一展示
用户在现有结果管理界面中查看 LANDSAR 结果,与其他引擎结果统一管理。
## 十、LANDSAR 作为第三核心时的系统改造点
从本系统角度,真正需要做的改造不是推倒重来,而是在现有多引擎框架上补全 `landsar` 适配层。
### 10.1 后端改造点
1. 完成 `landsar_engine.py`
目前这个文件只是占位,需要实现真正的可用性检查、profile 获取和运行逻辑。
2. 增加 LANDSAR 任务处理器
可以新增独立 `JOB_TYPE_LANDSAR_RUN`,也可以复用统一外部引擎作业模式。
3. 增加运行状态映射
需要把 LANDSAR 的状态映射到本系统统一状态,如 `PENDING`、`RUNNING`、`COMPLETED`、`FAILED`。
4. 增加日志采集与错误归一化
要保证前端看到的日志和错误信息可理解、可追踪。
5. 增加结果回收逻辑
需要把 LANDSAR 输出目录纳入结果扫描与入库体系。
6. 补充轨道池适配
当前系统已预留 `ORBIT_POOL_LANDSAR`,采购后需明确 LANDSAR 轨道格式和同步策略。
### 10.2 前端改造点
1. 让 `LANDSAR` 状态卡从“预留”变成“可选可提交”。
2. 展示 LANDSAR 专属 profile 和参数项。
3. 在运行历史中显示 `engine=landsar`。
4. 在结果列表和详情中标识结果来源于 LANDSAR。
### 10.3 运维改造点
1. 增加 LANDSAR 服务健康检查。
2. 增加供应商服务连通性检查。
3. 增加回调鉴权或调用鉴权。
4. 增加 LANDSAR 结果目录一致性检查。
## 十一、采购时必须明确的接口要求
这一部分是采购方案的核心。若这些能力得不到保障,LANDSAR 就无法真正嵌入平台。
### 11.1 必须具备的调用方式
供应商至少应提供以下三种方式之一,优先级从高到低如下:
1. HTTP/REST 服务接口
最推荐,最适合平台接入。
2. 命令行接口(CLI)
可作为次优方案,但需要明确返回码、日志和输出目录约定。
3. Python/SDK 接口
也可接受,但后续部署和版本兼容成本通常更高。
如果三者都没有,只提供 GUI 操作,则不建议作为“第三生产核心”采购。
### 11.2 必须具备的最小接口能力
若采用服务接口,建议采购时要求至少提供以下能力:
1. 提交任务接口
输入参数包括任务编号、profile、输入目录、输出目录、处理参数。
2. 查询任务状态接口
能返回排队、运行中、完成、失败、取消等状态。
3. 查询进度接口
能返回百分比或阶段性进度。
4. 查询日志接口
至少能返回日志摘要,最好支持完整日志文件。
5. 查询产物接口
能返回主产物、辅产物、日志文件、质量文件等路径或下载地址。
6. 健康检查接口
能返回服务可用性、许可证状态、核心依赖状态。
7. 能力枚举接口
能列出当前支持的 profile、版本、约束条件。
### 11.3 必须明确的数据约定
采购时要和供应商约定清楚以下数据边界:
1. 输入目录结构由谁负责准备。
2. 输出目录结构是否固定。
3. 主结果文件命名规则是否稳定。
4. 日志文件命名规则和保存期限。
5. 失败任务是否保留中间文件。
6. 轨道文件格式和目录组织方式。
7. 坐标系、DEM、投影和元数据约定。
没有这些约定,后续集成必然出现反复沟通和返工。
## 十二、建议的采购技术条款
为了确保 LANDSAR 真正能嵌入本系统,建议采购文件中写入如下技术条款。
### 12.1 必选条款
1. 供应商须提供可编程调用接口,不得仅提供人工图形界面。
2. 供应商须提供任务提交、状态查询、日志查询、结果获取的完整接口说明。
3. 供应商须支持与第三方业务系统进行集成,并配合联调。
4. 供应商须提供稳定的版本管理和升级兼容说明。
5. 供应商须明确许可证机制对服务化调用的限制条件。
6. 供应商须配合完成至少一个标准 D-InSAR 生产 profile 的端到端联调验收。
### 12.2 强烈建议条款
1. 支持回调通知机制,减少轮询压力。
2. 支持独立健康检查接口。
3. 支持 profile 枚举和参数模板查询。
4. 支持输出标准化目录结构。
5. 支持服务部署在内网服务器环境。
6. 支持批量任务执行和失败重试。
### 12.3 验收建议
建议把验收拆成两个层次:
1. 集成验收
能在本系统中完成 LANDSAR 任务提交、状态查看、日志查看、结果入库。
2. 业务验收
能稳定完成至少一个真实生产样例,并在前端完成统一展示。
### 12.4 采购方案分级建议
为了避免采购完成后无法嵌入现有系统,建议把采购方案分成三档判断:
1. 推荐方案
采购“LANDSAR 服务接口版 + 联调实施服务 + 服务化许可”。
该方案要求供应商提供 API 或等价服务接口,并配合完成与本系统的任务、状态、日志、结果联通。这是最符合“第三生产核心”目标的方案。
2. 可接受方案
采购“LANDSAR CLI/SDK 版 + 本地部署支持 + 接口适配支持”。
该方案在没有标准服务接口时仍可落地,但后续部署、升级、兼容和运维成本会明显高于推荐方案。
3. 不建议方案
仅采购“LANDSAR 桌面 GUI 单机版”。
该方案无法稳定嵌入本系统,只能形成独立工具,难以支撑统一任务管理、日志审计和结果治理,不建议作为本项目采购目标。
## 十三、LANDSAR 接入对采购决策的影响
这一点需要在汇报中重点强调:
采购 LANDSAR,不是单纯采购一个处理软件,而是在采购本系统的第三个生产核心。
因此,采购判断应从以下维度展开:
1. 不只是看算法效果,还要看是否可集成。
2. 不只是看桌面端能不能跑,还要看服务端能不能调。
3. 不只是看一次能不能出结果,还要看能否纳入长期运维。
4. 不只是看供应商交付软件,还要看是否交付接口和联调能力。
如果采购时忽略这些条件,后续极有可能出现“软件买到了,但无法嵌入现有平台”的情况,导致采购价值被打折。
## 十四、建议的实施路径
建议分两步推进:
### 第一阶段:完成服务嵌入
目标是让 LANDSAR 先成为“可调度、可监控、可回收”的第三生产核心。
重点工作:
1. 完成接口对接。
2. 完成任务状态联通。
3. 完成日志与结果回流。
4. 完成前端选择与展示。
### 第二阶段:完成深度治理
目标是把 LANDSAR 结果纳入更完整的统一治理体系。
重点工作:
1. 结果版本管理。
2. 多引擎结果对比。
3. 质量评价与复核闭环。
4. 更细粒度的运行审计和运维检查。
这样推进的好处是先把采购价值尽快落地,再逐步做深,而不是一开始就把目标定得过重。
## 十五、结论
综合判断如下:
1. 当前 InSAR 管理系统已经具备平台化能力,适合承接第三方 D-InSAR 生产核心。
2. 系统的多引擎架构已经形成,`LANDSAR` 不是从零开始接,而是在既有框架中补齐适配层。
3. 截至目前,`LANDSAR` 在系统中仍是预留状态,尚未真正接入生产执行。
4. 因此,采购方案必须把“可编程调用、可集成、可监控、可回收”写成明确要求。
5. 最推荐的采购与实施方式,是将 LANDSAR 以外部生产服务的形式嵌入本系统,作为第三个 D-InSAR 生产核心统一调度。
一句话概括:本项目当前已经具备“接入第三生产核心”的平台基础,下一步采购是否成功,关键不在于是否购买到一个能跑的工具,而在于是否购买到一个能够被本系统稳定接入的服务化能力。