certflow.controllers.import_controller 源代码
# certflow/controllers/import_controller.py
"""导入控制器
桥接导入界面(View)与导入服务(Service):
- 写:执行销售计划导入(经 ``SalePlanService.import_from_excel_with_config``);
- 读(预览):导入范围筛选(经 ``SalePlanService.apply_range_filter``)。
架构层次(蓝图 M5「架构统一」目标 —— 补齐导入链路缺失的 Controller 层):
View (ImportDialog / ImportConfirmDialog)
→ Controller (ImportController)
→ Service (SalePlanService)
→ Handler (ExcelHandler / DataCleaner / Sorter / SaveHandler)
→ Model (SalePlan / QuarantineSalePlan)
"""
from __future__ import annotations
from pathlib import Path
from typing import Any
from certflow.controllers.base_controller import BaseController
from certflow.services.sale_plan_service import SalePlanService
from certflow.utils.id_generator import IDGenerator
from certflow.utils.logger import logger
[文档]
class ImportController(BaseController):
"""导入控制器 — 导入界面的业务调度层(薄层)"""
def __init__(self, db_session=None, db_manager=None, use_shared: bool = True):
super().__init__(db_session=db_session, db_manager=db_manager, use_shared=use_shared)
self._service = self.get_service(SalePlanService)
# ============================================================
# 写操作:执行导入
# ============================================================
[文档]
def run_import(self, config_params: dict) -> dict:
"""执行销售计划导入,返回统计 dict。
仅做「请求透传 + 调 Service + 收结果」,事务由 Service 内部持有
(与历史 ``run_import`` 行为一致,Controller 不额外 commit/rollback)。
"""
config = dict(config_params)
resolutions = config_params.get("conflict_resolutions")
if resolutions:
config["conflict_resolutions"] = resolutions
try:
return self._service.import_from_excel_with_config(config)
except Exception as e:
logger.error(f"导入销售计划失败: {e}")
raise
# ============================================================
# 读操作(预览):导入范围筛选
# ============================================================
[文档]
@staticmethod
def apply_range_filter(
records: list[dict[str, Any]],
range_filter: dict[str, str] | None,
selected_rows: list[int] | None = None,
) -> list[dict[str, Any]]:
"""导入范围筛选(预览用,不落库)。
委托 ``SalePlanService.apply_range_filter``(M5 已提升为公开静态方法)。
"""
return SalePlanService.apply_range_filter(records, range_filter, selected_rows)
# ============================================================
# 静态门面:收口 View 对 Handler / Utils / Model 的散落直连
# (即使 ImportController 未注入实例,View 仍可经类名调用,向后兼容)
# ============================================================
[文档]
@staticmethod
def read_and_clean_sheet(
file_path: str,
sheet_name: str,
header_row: int,
skip_rows: int,
column_mapping: dict[str, str] | None = None,
) -> list[dict[str, Any]]:
"""读取工作表并经清洗管线转为记录列表(与真实导入同款读取)。
委托 ``ExcelHandler.read_excel`` + ``DataCleaner.clean_sale_plan``;
pandas 在 ``header=`` 下自动对重名列加 ``.1``/``.2`` 后缀去重,避免
``df[col]`` 因重名列返回 DataFrame。
"""
df = SalePlanService.read_excel(
file_path=Path(file_path),
sheet_name=sheet_name,
header_row=header_row,
skiprows=skip_rows,
)
if column_mapping:
existing = [c for c in column_mapping if c in df.columns]
df = df[existing].rename(columns=column_mapping)
return SalePlanService.clean_sale_plan(df).to_dict("records")
[文档]
@staticmethod
def normalize_record_for_key(rec: dict[str, Any], session: Any) -> dict[str, Any]:
"""预览/扫描阶段与入库同款清洗:口径/压力规范化后返回新 dict(不污染入参)。
委托 ``IDGenerator.normalize_spec`` / ``IDGenerator.normalize_pn``。
"""
rec = dict(rec)
rec = IDGenerator.normalize_spec(rec, session)
return IDGenerator.normalize_pn(rec, session)
[文档]
@staticmethod
def generate_unique_key(rec: dict[str, Any]) -> str:
"""生成记录唯一键(委托 ``IDGenerator.generate_unique_key``)。"""
return IDGenerator.generate_unique_key(rec)
[文档]
@staticmethod
def generate_change_signature(rec: dict[str, Any]) -> str:
"""生成记录变更签名(委托 ``IDGenerator.generate_change_signature``)。"""
return IDGenerator.generate_change_signature(rec)
[文档]
@staticmethod
def find_existing_by_unique_key(session: Any, unique_key: str) -> Any:
"""按唯一键查询已存在记录(委托 ``SalePlanService.find_by_unique_key``)。"""
return SalePlanService(session).find_by_unique_key(unique_key)