refactor: restructure main_clean.py with dependency injection and interfaces

Refactor discrete material cleaner to improve maintainability and testability:

- Introduce dependency injection pattern for service components
- Add interfaces (IPageNavigator, IMaterialExtractor, IDeletionChecker)
- Extract PageNavigator service for page navigation logic
- Extract MaterialExtractor service for data extraction
- Extract DatabaseDeletionChecker service for deletion logic
- Add MaterialInfo data model for structured data
- Centralize configuration (CleanerConfig, UIConstants)
- Implement separation of concerns across services layer

Also includes comprehensive execution mechanism documentation.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Misaka_Company
2026-02-10 14:17:37 +08:00
parent 21f5827cd3
commit 5024384c73
16 changed files with 1661 additions and 346 deletions

View File

@@ -0,0 +1,190 @@
"""
离散备料计划清理器 - 重构版
使用依赖注入、接口抽象、职责分离设计
"""
import time
from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeoutError
from interfaces.i_page_navigator import IPageNavigator
from interfaces.i_material_extractor import IMaterialExtractor
from interfaces.i_deletion_checker import IDeletionChecker
from config.cleaner_config import CleanerConfig
from config.ui_constants import UIConstants
from utils.auth import login, logout
from db.production_order_query import (
read_production_ids,
query_production_order_numbers,
)
class DiscreteMaterialCleaner:
"""离散备料计划清理器 - 重构版"""
def __init__(
self,
config: CleanerConfig,
navigator: IPageNavigator,
extractor: IMaterialExtractor,
deletion_checker: IDeletionChecker,
):
"""
初始化清理器
Args:
config: 清理器配置
navigator: 页面导航器
extractor: 物料信息提取器
deletion_checker: 删除判断器
"""
self.config = config
self.navigator = navigator
self.extractor = extractor
self.checker = deletion_checker
def _print(self, *args, **kwargs):
"""打印日志(如果 verbose=True"""
if self.config.verbose:
print(*args, **kwargs)
def clean(self, production_id_file: str) -> None:
"""
执行清理流程
Args:
production_id_file: ProductionID.txt 文件路径
"""
self._print(f"使用负责人 [{self.config.manager_name}] 进行数据清理")
with sync_playwright() as playwright:
# 1. 初始化浏览器和页面
browser, context, page, main_frame = self._initialize_browser(playwright)
# 2. 导航到目标页面
page1, main_frame, inner_frame = self.navigator.navigate_to_main_page(page)
self.navigator.setup_query_interface(inner_frame)
# 3. 获取订单列表
order_ids = self._get_order_ids(production_id_file)
# 4. 处理每个订单
for order_index, order_id in enumerate(order_ids):
self._process_order(inner_frame, order_id, order_index, page1)
# 5. 清理
self._cleanup(main_frame, context, browser)
def _initialize_browser(self, playwright):
"""初始化浏览器和登录"""
self._print("=" * 80)
self._print("开始执行离散备料计划维护数据清理")
self._print("=" * 80)
return login(
playwright=playwright,
username=self.config.username,
password=self.config.password,
headless=self.config.headless,
ignore_https_errors=True,
)
def _get_order_ids(self, production_id_file: str):
"""获取订单 ID 列表"""
# 读取总排号
production_ids = read_production_ids(production_id_file)
self._print(f"从文件读取到 {len(production_ids)} 个总排号")
# 查询数据库获取生产订单号
order_ids = query_production_order_numbers(production_ids)
self._print(f"查询到 {len(order_ids)} 个生产订单号")
return order_ids
def _process_order(self, inner_frame, order_id: str, order_index: int, page1):
"""处理单个订单"""
self._print(
f"\n=== 开始处理第 {order_index + 1} 个订单,订单号: {order_id} ==="
)
# 1. 查询订单
self._query_order(inner_frame, order_id, order_index)
# 2. 调试模式暂停
if (
self.config.debug_mode
and (self.config.debug_order is None or order_index == self.config.debug_order)
):
self._print(f"=== 调试暂停:第 {order_index + 1} 个订单 ===")
page1.pause()
# 3. 导航到备料计划页面
page2, plan_frame = self.navigator.navigate_to_material_plan_page(page1)
# 4. 提取订单信息
detail_count = self.extractor.extract_detail_count(plan_frame)
detail_status = self.extractor.extract_detail_status(plan_frame)
# 5. 检查是否需要处理
if not self._should_process_order(detail_count, detail_status, order_index):
page2.close()
return
# 6. 进入编辑模式并展开
self._enter_edit_mode(plan_frame)
# 7. 提取并处理物料
materials = self.extractor.extract_materials(plan_frame, detail_count)
for material in materials:
material.should_delete = self.checker.should_delete(material)
# TODO: 执行实际的删除操作
page2.close()
time.sleep(1)
def _query_order(self, inner_frame, order_id: str, order_index: int):
"""查询单个订单"""
# 清空文本框
textbox = inner_frame.get_by_role("textbox", name="生产订单号")
textbox.fill("")
# 填充订单号
textbox.fill(order_id)
# 点击查询
from config.ui_constants import Selectors
inner_frame.locator(Selectors.SEARCH_BTN).click()
self._print(f"{order_index + 1} 个订单查询完成,等待加载结果...")
# 等待加载完成
self.navigator.wait_for_page_loaded(inner_frame)
self._print(f"{order_index + 1} 个订单加载完成,开始清理数据...")
def _should_process_order(
self, detail_count: int, detail_status: str, order_index: int
) -> bool:
"""判断订单是否需要处理"""
if detail_count == 0:
self._print(f"{order_index + 1} 个订单无数据需要清理,跳过...")
return False
elif detail_status != "审批通过":
self._print(f"{order_index + 1} 个订单备料状态: {detail_status}")
return False
return True
def _enter_edit_mode(self, frame):
"""进入编辑模式并展开物料列表"""
from config.ui_constants import Selectors
frame.get_by_role("button", name=Selectors.MODIFY_BUTTON).click()
save_button_locator = frame.get_by_role("button", name=Selectors.SAVE_BUTTON)
save_button_locator.wait_for(state="visible", timeout=10000)
frame.get_by_text(Selectors.EXPAND_BUTTON).first.click()
def _cleanup(self, main_frame, context, browser):
"""清理资源"""
self._print("\n开始执行账号注销...")
logout(main_frame, verbose=self.config.verbose)
self._print(f"\n=== 全部完成 ===")
context.close()
browser.close()