feat: 支持 CellData 文件夹上传处理
This commit is contained in:
@@ -5,13 +5,13 @@ from datetime import datetime
|
||||
from pathlib import Path
|
||||
from threading import Thread
|
||||
|
||||
from fastapi import APIRouter, Body, HTTPException
|
||||
from fastapi import APIRouter, Body, File, HTTPException, UploadFile
|
||||
|
||||
from app import state
|
||||
from app.api.routers.task_runtime import set_task_stage
|
||||
from app.config import AppConfig, CACHE_DIR
|
||||
from app.processor import ProcessLogger
|
||||
from app.services.cell_data import refresh_cell_data
|
||||
from app.services.cell_data import CellDataProcessor, refresh_cell_data
|
||||
|
||||
router = APIRouter(tags=["cell-data"])
|
||||
|
||||
@@ -57,6 +57,67 @@ async def start_cell_data_processing():
|
||||
return {"success": True, "message": "CellData 处理已启动", "task_id": task_id, "stage": current_stage}
|
||||
|
||||
|
||||
@router.post("/api/cell-data/process/upload")
|
||||
async def upload_and_start_cell_data_processing(files: list[UploadFile] = File(...)):
|
||||
if not files:
|
||||
raise HTTPException(status_code=400, detail="没有上传文件")
|
||||
if state.global_task_lock["locked"]:
|
||||
raise HTTPException(status_code=409, detail="已有任务在运行,请等待当前任务完成")
|
||||
|
||||
task_id = "cell_data_" + datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
work_dir = CACHE_DIR / task_id
|
||||
upload_dir = work_dir / "uploads"
|
||||
upload_dir.mkdir(parents=True, exist_ok=True)
|
||||
saved_count = 0
|
||||
for file in files:
|
||||
if not file.filename:
|
||||
continue
|
||||
target = upload_dir / file.filename.replace("\\", "/")
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_bytes(await file.read())
|
||||
saved_count += 1
|
||||
if saved_count == 0:
|
||||
raise HTTPException(status_code=400, detail="没有有效上传文件")
|
||||
|
||||
logs: list[str] = []
|
||||
current_stage = "parsing"
|
||||
|
||||
def log_callback(message: str) -> None:
|
||||
logs.append(message)
|
||||
set_task_stage(task_id, current_stage, logs)
|
||||
|
||||
def stage_callback(stage: str) -> None:
|
||||
nonlocal current_stage
|
||||
current_stage = stage
|
||||
set_task_stage(task_id, current_stage, logs)
|
||||
|
||||
logger = ProcessLogger(
|
||||
log_file=work_dir / "log.txt",
|
||||
callback=log_callback,
|
||||
stage_callback=stage_callback,
|
||||
)
|
||||
app_config = state.current_config()
|
||||
state.processing_tasks[task_id] = {"logs": [], "status": "processing", "stage": current_stage}
|
||||
state.global_task_lock.update(
|
||||
{
|
||||
"locked": True,
|
||||
"task_id": task_id,
|
||||
"stage": current_stage,
|
||||
"started_at": datetime.now().isoformat(),
|
||||
}
|
||||
)
|
||||
|
||||
thread = Thread(target=_run_uploaded_cell_data_processing, args=(task_id, upload_dir, work_dir, logger, app_config), daemon=True)
|
||||
thread.start()
|
||||
return {
|
||||
"success": True,
|
||||
"message": "CellData 处理已启动",
|
||||
"task_id": task_id,
|
||||
"stage": current_stage,
|
||||
"file_count": saved_count,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/api/cell-data/process/status")
|
||||
async def get_cell_data_processing_status(task_id: str = Body(..., embed=True)):
|
||||
if task_id in state.processing_tasks:
|
||||
@@ -101,3 +162,33 @@ def _run_cell_data_processing(task_id: str, work_dir: Path, logger: ProcessLogge
|
||||
}
|
||||
finally:
|
||||
state.reset_task_lock()
|
||||
|
||||
|
||||
def _run_uploaded_cell_data_processing(task_id: str, upload_dir: Path, work_dir: Path, logger: ProcessLogger, app_config: AppConfig) -> None:
|
||||
started = time.time()
|
||||
try:
|
||||
result = CellDataProcessor(app_config, work_dir, logger).run_local(upload_dir)
|
||||
elapsed = round(time.time() - started, 2)
|
||||
state.processing_tasks[task_id] = {
|
||||
"logs": logger.get_logs(),
|
||||
"status": "completed",
|
||||
"stage": "completed",
|
||||
"elapsed_time": elapsed,
|
||||
"result": {
|
||||
"selected_files": result.selected_files,
|
||||
"parsed_rows": result.parsed_rows,
|
||||
"imported_rows": result.imported_rows,
|
||||
"skipped_rows": result.skipped_rows,
|
||||
},
|
||||
}
|
||||
except Exception as exc:
|
||||
logger.error(str(exc))
|
||||
state.processing_tasks[task_id] = {
|
||||
"logs": logger.get_logs(),
|
||||
"status": "failed",
|
||||
"stage": "failed",
|
||||
"error": str(exc),
|
||||
"elapsed_time": round(time.time() - started, 2),
|
||||
}
|
||||
finally:
|
||||
state.reset_task_lock()
|
||||
|
||||
@@ -84,6 +84,24 @@ class CellDataProcessor:
|
||||
self._log(f"CellData 导入完成,共 {result.imported_rows} 行")
|
||||
return result
|
||||
|
||||
def run_local(self, upload_root: Path) -> CellDataResult:
|
||||
self._set_stage("locating")
|
||||
local_files = self._select_local_zip_files(upload_root)
|
||||
if not local_files:
|
||||
raise RuntimeError("未找到可处理的 CellData ZIP 文件")
|
||||
self._log(f"已选择 {len(local_files)} 个 CellData ZIP 文件")
|
||||
|
||||
self._set_stage("parsing")
|
||||
result = CellDataResult(selected_files=len(local_files))
|
||||
rows = self._parse_zip_files(local_files, result)
|
||||
if not rows:
|
||||
raise RuntimeError("CellData ZIP 中未解析到有效数据")
|
||||
|
||||
self._set_stage("importing")
|
||||
result.imported_rows = self._replace_cellinfo(rows)
|
||||
self._log(f"CellData 导入完成,共 {result.imported_rows} 行")
|
||||
return result
|
||||
|
||||
def _select_latest_zip_files(self) -> list[SelectedZip]:
|
||||
selected: list[SelectedZip] = []
|
||||
for template in self.config.scan_paths:
|
||||
@@ -182,6 +200,47 @@ class CellDataProcessor:
|
||||
ssh.close()
|
||||
return local_files
|
||||
|
||||
def _select_local_zip_files(self, upload_root: Path) -> list[tuple[SelectedZip, Path]]:
|
||||
grouped: dict[str, list[Path]] = {}
|
||||
for path in upload_root.rglob("*.zip"):
|
||||
if not self.file_name_re.search(path.name):
|
||||
continue
|
||||
try:
|
||||
parent = str(path.parent.relative_to(upload_root)).replace("\\", "/")
|
||||
except ValueError:
|
||||
parent = ""
|
||||
grouped.setdefault("" if parent == "." else parent, []).append(path)
|
||||
|
||||
selected: list[tuple[SelectedZip, Path]] = []
|
||||
for parent, paths in sorted(grouped.items(), key=lambda item: item[0]):
|
||||
candidates = []
|
||||
for path in paths:
|
||||
match = self.file_time_re.search(path.name)
|
||||
if match:
|
||||
candidates.append((match.group("timestamp"), path))
|
||||
if not candidates:
|
||||
continue
|
||||
timestamp, path = max(candidates, key=lambda item: item[0])
|
||||
band = Path(parent).name if parent else ""
|
||||
selected_zip = SelectedZip(
|
||||
scan_path=str(upload_root),
|
||||
band=band,
|
||||
remote_file=RemoteFileInfo(
|
||||
path=str(path),
|
||||
relative_path=str(path.relative_to(upload_root)).replace("\\", "/"),
|
||||
parent=parent,
|
||||
name=path.name,
|
||||
size=path.stat().st_size,
|
||||
),
|
||||
timestamp=timestamp,
|
||||
)
|
||||
if not band:
|
||||
self._log(f"未从目录名识别频段: {path.name}")
|
||||
else:
|
||||
self._log(f"{band}: {path.name}")
|
||||
selected.append((selected_zip, path))
|
||||
return selected
|
||||
|
||||
@staticmethod
|
||||
def _download_result():
|
||||
from app.services.remote_download import RemoteDownloadResult
|
||||
@@ -194,15 +253,22 @@ class CellDataProcessor:
|
||||
rows_by_key: dict[str, dict[str, str]] = {}
|
||||
for selected, local_path in local_files:
|
||||
with zipfile.ZipFile(local_path) as zf:
|
||||
for source in mapping["sources"]:
|
||||
if source["band"] != selected.band:
|
||||
sources = list(mapping["sources"])
|
||||
for info in zf.infolist():
|
||||
name = Path(info.filename).name
|
||||
if not name.lower().endswith(".csv"):
|
||||
continue
|
||||
for info in zf.infolist():
|
||||
name = Path(info.filename).name
|
||||
if not name.lower().endswith(".csv"):
|
||||
continue
|
||||
if not name.startswith(source["file_prefix"]):
|
||||
continue
|
||||
matching_sources = [
|
||||
source
|
||||
for source in sources
|
||||
if name.startswith(source["file_prefix"]) and (not selected.band or source["band"] == selected.band)
|
||||
]
|
||||
if not matching_sources:
|
||||
continue
|
||||
if not selected.band and len(matching_sources) > 1:
|
||||
self._log(f"跳过无法识别频段的文件: {name}")
|
||||
continue
|
||||
for source in matching_sources:
|
||||
raw = zf.read(info.filename)
|
||||
text = self._decode_csv(raw)
|
||||
reader = csv.DictReader(text.splitlines())
|
||||
|
||||
Reference in New Issue
Block a user