chore: initialize insar management system v2
This commit is contained in:
@@ -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.
|
||||
@@ -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` 回调,集成到全局锁定机制
|
||||
@@ -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
|
||||
**状态**:✅ 代码修改完成,等待测试验证
|
||||
@@ -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` 管理
|
||||
- ✅ 支持多种部署场景
|
||||
- ✅ 客户环境可自定义配置
|
||||
- ✅ 无需修改代码即可部署
|
||||
@@ -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 修复
|
||||
@@ -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 行
|
||||
|
||||
**风险评估**:🟢 低风险
|
||||
- 不影响现有功能
|
||||
- 只读操作无风险
|
||||
- 删除操作有权限控制和确认
|
||||
|
||||
**状态**:✅ 代码完成,等待测试验证
|
||||
@@ -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. 发布前执行一次全量重建,提升线上命中率
|
||||
@@ -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. 异常处理不规范
|
||||
|
||||
建议按照优先级逐步修复,并在修复后进行充分的回归测试。长期来看,应该引入更完善的权限管理、资源配额、审计日志和速率限制机制。
|
||||
@@ -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 数量上限:_个
|
||||
|
||||
### 解包性能
|
||||
- 修复前平均耗时:_秒
|
||||
- 修复后平均耗时:_秒
|
||||
- 性能差异:_%
|
||||
|
||||
---
|
||||
|
||||
## 签名确认
|
||||
|
||||
每完成一个阶段,填写以下信息:
|
||||
|
||||
### 第一阶段
|
||||
- 修复人:_______
|
||||
- 测试人:_______
|
||||
- 完成日期:_______
|
||||
- 签名:_______
|
||||
|
||||
### 第二阶段
|
||||
- 修复人:_______
|
||||
- 测试人:_______
|
||||
- 完成日期:_______
|
||||
- 签名:_______
|
||||
|
||||
### 第三阶段
|
||||
- 修复人:_______
|
||||
- 测试人:_______
|
||||
- 完成日期:_______
|
||||
- 签名:_______
|
||||
@@ -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,暂缓架构性改动
|
||||
- ✅ 保留了灵活性,可以根据实际情况调整
|
||||
|
||||
**建议**:先完成第一阶段,充分测试后再决定是否继续第二阶段。
|
||||
@@ -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. **下周计划**:
|
||||
- [ ] 根据第一阶段测试结果决定是否继续
|
||||
- [ ] 如果继续,开始第二阶段
|
||||
|
||||
---
|
||||
|
||||
## 备注
|
||||
|
||||
- 每次修复后立即更新此文档
|
||||
- 测试结果详细记录,包括成功和失败的情况
|
||||
- 遇到问题及时记录到"问题记录"区域
|
||||
- 回滚操作必须记录原因和影响范围
|
||||
@@ -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 语法检查已通过,现在等待功能测试验证。
|
||||
|
||||
**风险评估**:🟢 低风险
|
||||
**建议**:可以安全地进行测试,不太可能影响现有功能。
|
||||
@@ -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
|
||||
@@ -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` 与本文件。
|
||||
@@ -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` 调整
|
||||
@@ -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 |
|
||||
Reference in New Issue
Block a user