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>
22 KiB
main_clean.py 运行机制文档
1. 概述
1.1 系统简介
离散备料计划维护数据清理工具 是一个基于 Python + Playwright 开发的自动化工具,用于清理 YonBIP(用友)ERP 系统中的备料数据。该工具通过浏览器自动化技术访问 ERP 系统,提取生产订单的备料信息,并根据数据库查询结果判断哪些物料需要清理。
1.2 技术栈
- Python 3.x: 主要开发语言
- Playwright: 浏览器自动化框架(Chromium)
- SQL Server: 数据库查询(pyodbc)
- 数据类(dataclass): 配置和数据模型
1.3 主要功能
- 自动登录 YonBIP ERP 系统
- 读取待处理的生产订单列表
- 遍历每个订单的备料计划
- 提取物料详细信息(编码、名称、数量等)
- 查询数据库判断物料是否需要删除
- 标记需要清理的物料(预留删除接口)
2. 系统架构图
graph TB
subgraph "外部系统"
ERP[YonBIP ERP 系统]
DB[(SQL Server 数据库)]
FILE[ProductionID.txt]
end
subgraph "配置层 (config/)"
CONFIG[CleanerConfig<br/>清理器配置]
UICONST[UIConstants<br/>UI 常量配置]
UICONST -.-> SELECTORS[Selectors<br/>选择器]
UICONST -.-> TIMEOUTS[Timeouts<br/>超时配置]
UICONST -.-> RETRY[RetryConfig<br/>重试配置]
end
subgraph "服务层 (services/)"
NAVIGATOR[PageNavigator<br/>页面导航服务]
EXTRACTOR[MaterialExtractor<br/>物料提取服务]
CHECKER[DatabaseDeletionChecker<br/>删除判断服务]
end
subgraph "业务逻辑层 (utils/)"
CLEANER[DiscreteMaterialCleaner<br/>离散备料清理器]
end
subgraph "数据层 (models/)"
MATERIAL[MaterialInfo<br/>物料信息模型]
end
subgraph "接口层 (interfaces/)"
I1[IPageNavigator]
I2[IMaterialExtractor]
I3[IDeletionChecker]
end
FILE --> CLEANER
CLEANER --> NAVIGATOR
CLEANER --> EXTRACTOR
CLEANER --> CHECKER
CLEANER --> CONFIG
CLEANER --> UICONST
NAVIGATOR -.实现.-> I1
EXTRACTOR -.实现.-> I2
CHECKER -.实现.-> I3
NAVIGATOR --> ERP
EXTRACTOR --> ERP
EXTRACTOR --> MATERIAL
CHECKER --> DB
CHECKER --> MATERIAL
style ERP fill:#ff9999
style DB fill:#99ccff
style FILE fill:#99ff99
style CLEANER fill:#ffcc99
3. 主执行流程图
flowchart TD
START([开始: main函数]) --> INIT_CONFIG[创建 CleanerConfig<br/>配置用户名、密码、负责人等]
INIT_CONFIG --> INIT_UI[创建 UIConstants<br/>选择器、超时、重试配置]
INIT_UI --> DEP_INJ[依赖注入<br/>创建服务实例]
DEP_INJ --> CREATE_NAV[创建 PageNavigator<br/>页面导航服务]
DEP_INJ --> CREATE_EXT[创建 MaterialExtractor<br/>物料提取服务]
DEP_INJ --> CREATE_CHK[创建 DatabaseDeletionChecker<br/>删除判断服务]
CREATE_NAV --> CREATE_CLEANER[创建 DiscreteMaterialCleaner<br/>注入所有服务]
CREATE_CHK --> CREATE_CLEANER
CREATE_EXT --> CREATE_CLEANER
CREATE_CLEANER --> READ_FILE[读取 ProductionID.txt<br/>获取生产总排号列表]
READ_FILE --> CALL_CLEAN[调用 cleaner.clean方法]
CALL_CLEAN --> PW_START[启动 Playwright]
PW_START --> LOGIN[登录 ERP 系统<br/>获取 browser, context, page]
LOGIN --> NAV_MAIN[导航到主页面<br/>获取 nested iframe]
NAV_MAIN --> SETUP_QUERY[设置查询界面<br/>打开订单号查询]
SETUP_QUERY --> READ_IDS[读取总排号]
READ_IDS --> QUERY_DB[查询数据库<br/>获取生产订单号列表]
QUERY_DB --> ORDER_LOOP{遍历订单列表}
ORDER_LOOP --> QUERY_ORDER[查询订单<br/>填充订单号并点击查询]
QUERY_ORDER --> WAIT_LOAD[等待页面加载完成]
WAIT_LOAD --> DEBUG_CHECK{调试模式?}
DEBUG_CHECK -->|是| PAUSE[暂停执行<br/>等待调试]
DEBUG_CHECK -->|否| NAV_PLAN
PAUSE --> NAV_PLAN[导航到备料计划页面]
NAV_PLAN --> EXTRACT_COUNT[提取详细信息数量]
EXTRACT_COUNT --> EXTRACT_STATUS[提取备料状态]
EXTRACT_STATUS --> SHOULD_PROC{需要处理?}
SHOULD_PROC -->|数量=0| SKIP[跳过订单]
SHOULD_PROC -->|状态≠审批通过| SKIP
SHOULD_PROC -->|是| ENTER_EDIT[进入编辑模式<br/>点击修改按钮]
ENTER_EDIT --> EXPAND[展开物料列表<br/>点击展开按钮]
EXPAND --> EXT_MATERIALS[提取所有物料信息<br/>遍历物料列表]
EXT_MATERIALS --> MAT_LOOP{遍历物料}
MAT_LOOP --> CHECK_DELETE[查询数据库<br/>判断是否需要删除]
CHECK_DELETE --> MARK[标记物料<br/>设置 should_delete]
MARK --> MAT_LOOP
MAT_LOOP -->|完成| CLOSE_PAGE[关闭当前页面]
CLOSE_PAGE --> SLEEP[等待 1 秒]
SKIP --> ORDER_LOOP
SLEEP --> ORDER_LOOP
ORDER_LOOP -->|完成| LOGOUT[注销登录]
LOGOUT --> CLOSE_ALL[关闭浏览器上下文]
CLOSE_ALL --> END([结束])
style START fill:#90EE90
style END fill:#FFB6C1
style ORDER_LOOP fill:#FFE4B5
style MAT_LOOP fill:#FFE4B5
style SHOULD_PROC fill:#FFD700
4. 组件交互时序图
sequenceDiagram
participant Main as main()
participant Cleaner as DiscreteMaterialCleaner
participant Nav as PageNavigator
participant Ext as MaterialExtractor
participant Checker as DatabaseDeletionChecker
participant ERP as YonBIP ERP
participant DB as SQL Server
Main->>Cleaner: clean(production_id_file)
activate Cleaner
Cleaner->>Cleaner: _initialize_browser()
Note over Cleaner: 启动 Playwright 并登录
Cleaner->>Nav: navigate_to_main_page(page)
activate Nav
Nav->>ERP: 点击功能菜单
Nav->>ERP: 打开"离散生产订单维护"
Nav-->>Cleaner: (page1, main_frame, inner_frame)
deactivate Nav
Cleaner->>Nav: setup_query_interface(inner_frame)
activate Nav
Nav->>ERP: 打开订单号查询界面
Nav->>ERP: 设置显示数量为 5000
deactivate Nav
Cleaner->>Cleaner: _get_order_ids(production_id_file)
Note over Cleaner: 读取文件并查询数据库
loop 遍历每个订单
Cleaner->>Cleaner: _query_order(inner_frame, order_id)
Note over Cleaner: 填充订单号并点击查询
Cleaner->>Nav: wait_for_page_loaded(inner_frame)
activate Nav
Nav->>ERP: 等待"加载中"消失
deactivate Nav
alt 调试模式
Cleaner->>ERP: pause() 调试暂停
end
Cleaner->>Nav: navigate_to_material_plan_page(page1)
activate Nav
Nav->>ERP: 点击"更多" → "备料计划"
Nav->>ERP: 等待页面加载完成
Nav-->>Cleaner: (page2, plan_frame)
deactivate Nav
Cleaner->>Ext: extract_detail_count(plan_frame)
activate Ext
Ext->>ERP: 提取详细信息数量
Ext-->>Cleaner: detail_count
deactivate Ext
Cleaner->>Ext: extract_detail_status(plan_frame)
activate Ext
Ext->>ERP: 提取备料状态
Ext-->>Cleaner: detail_status
deactivate Ext
alt 需要处理订单
Cleaner->>Cleaner: _enter_edit_mode(plan_frame)
Note over Cleaner: 点击修改、展开按钮
Cleaner->>Ext: extract_materials(plan_frame, count)
activate Ext
loop 遍历物料
Ext->>ERP: 提取物料编码
Ext->>ERP: 提取物料名称
Ext->>ERP: 提取待发数量
Ext->>ERP: 提取出库数量
end
Ext-->>Cleaner: materials (List[MaterialInfo])
deactivate Ext
loop 遍历每个物料
Cleaner->>Checker: should_delete(material)
activate Checker
Checker->>DB: 查询物料负责人
DB-->>Checker: 查询结果
Checker-->>Cleaner: True/False
deactivate Checker
Cleaner->>Cleaner: material.should_delete = result
end
Cleaner->>ERP: 关闭页面
else 不需要处理
Cleaner->>ERP: 关闭页面
end
end
Cleaner->>Cleaner: _cleanup()
Cleaner->>ERP: 注销登录
Cleaner->>Cleaner: 关闭浏览器
Cleaner-->>Main: 完成
deactivate Cleaner
5. 组件详细说明
5.1 DiscreteMaterialCleaner (utils/discrete_material_cleaner.py)
职责: 核心业务逻辑编排器,协调整个清理流程
主要方法:
clean(production_id_file)- 执行完整清理流程_initialize_browser(playwright)- 初始化浏览器并登录_get_order_ids(production_id_file)- 获取待处理订单列表_process_order(inner_frame, order_id, order_index, page1)- 处理单个订单_query_order(inner_frame, order_id, order_index)- 查询订单_should_process_order(detail_count, detail_status, order_index)- 判断是否需要处理_enter_edit_mode(frame)- 进入编辑模式_cleanup(main_frame, context, browser)- 清理资源
依赖注入:
navigator: IPageNavigator- 页面导航extractor: IMaterialExtractor- 物料提取deletion_checker: IDeletionChecker- 删除判断
5.2 PageNavigator (services/page_navigator.py)
职责: 处理所有页面导航和 iframe 操作
主要方法:
navigate_to_main_page(page)- 导航到主页面navigate_to_order_page(main_frame, page)- 导航到订单页面navigate_to_material_plan_page(page)- 导航到备料计划页面setup_query_interface(frame)- 设置查询界面wait_for_page_loaded(frame)- 等待页面加载完成
关键特性:
- 处理嵌套 iframe 结构 (#forwardFrame → #mainiframe)
- 等待加载状态("加载中"提示)
- 使用 expect_popup() 处理新窗口
5.3 MaterialExtractor (services/material_extractor.py)
职责: 从页面提取物料信息
主要方法:
extract_detail_count(frame)- 提取详细信息数量extract_detail_status(frame)- 提取备料状态extract_materials(frame, count)- 提取所有物料信息_extract_material_code(child_form)- 提取物料编码_extract_material_name(child_form)- 提取物料名称_extract_pending_quantity(child_form)- 提取待发数量_extract_shipped_quantity(child_form)- 提取出库数量_navigate_to_next_material(child_form, current_index, total_count)- 导航到下一个物料
数据返回: List[MaterialInfo]
5.4 DatabaseDeletionChecker (services/deletion_checker.py)
职责: 基于数据库查询判断物料是否需要删除
主要方法:
should_delete(material: MaterialInfo) -> bool- 判断是否需要删除
逻辑:
- 查询数据库中物料对应的负责人
- 与配置的
manager_name进行比对 - 返回是否需要删除的布尔值
5.5 CleanerConfig (config/cleaner_config.py)
职责: 管理清理器运行配置
属性:
username: str- ERP 登录用户名password: str- ERP 登录密码manager_name: str- 负责人姓名(用于删除判断)headless: bool- 是否无头模式运行浏览器verbose: bool- 是否打印详细日志debug_mode: bool- 是否启用调试模式debug_order: Optional[int]- 调试的订单索引
5.6 UIConstants (config/ui_constants.py)
职责: 集中管理所有 UI 相关常量
包含三个嵌套数据类:
-
Selectors - CSS 选择器
- 搜索相关:
SEARCH_BTN,SEARCH_ICON,QUERY_BY_ORDER - iframe:
FORWARD_FRAME,MAIN_IFRAME - 按钮:
MODIFY_BUTTON,SAVE_BUTTON,EXPAND_BUTTON
- 搜索相关:
-
Timeouts - 超时配置(毫秒)
- 页面加载:
IFRAME_VISIBLE,PLAN_CODE_VISIBLE - 加载等待:
LOADING_VISIBLE_WAIT,LOADING_HIDDEN_WAIT - 轮询:
PLAN_CODE_MAX_WAIT,PLAN_CHECK_INTERVAL
- 页面加载:
-
RetryConfig - 重试配置
DISPLAY_COUNT_MAX_RETRIES- 最大重试次数EXPECTED_DISPLAY_COUNT- 期望显示数量
5.7 MaterialInfo (models/material_info.py)
职责: 物料信息数据模型
属性:
serial_number: int- 序号code: str- 物料编码name: str- 物料名称pending_quantity: str- 累计待发数量shipped_quantity: str- 累计出库数量should_delete: bool- 是否需要删除(默认 False)
6. 配置类结构图
classDiagram
class CleanerConfig {
+str username
+str password
+str manager_name
+bool headless
+bool verbose
+bool debug_mode
+Optional~int~ debug_order
}
class UIConstants {
+Selectors selectors
+Timeouts timeouts
+RetryConfig retry
}
class Selectors {
+str SEARCH_BTN
+str SEARCH_ICON
+str QUERY_BY_ORDER
+str TAB_ALL
+str HOT_KEY_HEAD
+str MORE_BUTTON
+str MATERIAL_PLAN
+str FORWARD_FRAME
+str MAIN_IFRAME
+str MODIFY_BUTTON
+str SAVE_BUTTON
+str EXPAND_BUTTON
+str CARD_TABLE_SIDE_BOX
}
class Timeouts {
+int IFRAME_VISIBLE
+int PLAN_CODE_VISIBLE
+int BUTTON_VISIBLE
+int SERIAL_VISIBLE
+int FORM_VISIBLE
+int LOADING_VISIBLE_WAIT
+int LOADING_HIDDEN_WAIT
+int PLAN_CODE_MAX_WAIT
+float PLAN_CHECK_INTERVAL
}
class RetryConfig {
+int DISPLAY_COUNT_MAX_RETRIES
+str EXPECTED_DISPLAY_COUNT
}
class MaterialInfo {
+int serial_number
+str code
+str name
+str pending_quantity
+str shipped_quantity
+bool should_delete
}
UIConstants *-- Selectors
UIConstants *-- Timeouts
UIConstants *-- RetryConfig
CleanerConfig ..> UIConstants : 使用
DiscreteMaterialCleaner ..> CleanerConfig : 依赖
MaterialExtractor ..> MaterialInfo : 返回
DatabaseDeletionChecker ..> MaterialInfo : 处理
7. 数据流图
flowchart LR
subgraph "数据输入"
FILE[ProductionID.txt<br/>生产总排号列表]
CONFIG[CleanerConfig<br/>配置参数]
end
subgraph "数据处理"
READ[read_production_ids<br/>读取总排号]
QUERY[query_production_order_numbers<br/>查询数据库]
EXTRACT[extract_materials<br/>提取物料信息]
CHECK[should_delete<br/>删除判断]
end
subgraph "数据模型"
IDs[总排号列表]
OrderIDs[生产订单号列表]
Materials[物料信息列表]
Material[单个物料信息]
end
subgraph "外部系统"
DB[(SQL Server)]
ERP[YonBIP ERP]
end
FILE --> READ
READ --> IDs
IDs --> QUERY
QUERY --> DB
DB --> OrderIDs
OrderIDs --> ERP
ERP --> EXTRACT
EXTRACT --> Materials
Materials --> Material
Material --> CHECK
CHECK --> DB
style FILE fill:#99ff99
style DB fill:#99ccff
style ERP fill:#ff9999
style Materials fill:#ffcc99
8. 单个订单处理流程
flowchart TD
START([开始处理订单]) --> QUERY[填充订单号]
QUERY --> CLICK_SEARCH[点击查询按钮]
CLICK_SEARCH --> WAIT_LOADING[等待加载完成]
WAIT_LOADING --> NAV_PLAN[导航到备料计划页面]
NAV_PLAN --> WAIT_PLAN[等待备料计划页面加载]
WAIT_PLAN --> EXT_COUNT[提取详细信息数量]
EXT_COUNT --> EXT_STATUS[提取备料状态]
EXT_STATUS --> CHECK_COUNT{数量 > 0?}
CHECK_COUNT -->|否| SKIP_CLOSE[关闭页面]
CHECK_COUNT -->|是| CHECK_STATUS{状态 = 审批通过?}
CHECK_STATUS -->|否| SKIP_CLOSE
CHECK_STATUS -->|是| CLICK_MODIFY[点击修改按钮]
CLICK_MODIFY --> WAIT_SAVE[等待保存按钮可见]
WAIT_SAVE --> CLICK_EXPAND[点击展开按钮]
CLICK_EXPAND --> EXT_LOOP{遍历物料}
EXT_LOOP --> FIND_SERIAL[查找序号元素]
FIND_SERIAL --> EXT_CODE[提取物料编码]
EXT_CODE --> EXT_NAME[提取物料名称]
EXT_NAME --> EXT_PENDING[提取待发数量]
EXT_PENDING --> EXT_SHIPPED[提取出库数量]
EXT_SHIPPED --> CREATE_OBJ[创建 MaterialInfo 对象]
CREATE_OBJ --> CHECK_DELETE[查询数据库判断删除]
CHECK_DELETE --> MARK_DELETE[标记 should_delete]
MARK_DELETE --> NEXT_MAT{还有下一个?}
NEXT_MAT -->|是| CLICK_NEXT[点击下一个按钮]
CLICK_NEXT --> EXT_LOOP
NEXT_MAT -->|否| CLICK_LAST[点击最后一个按钮]
CLICK_LAST --> ADD_LIST[添加到物料列表]
ADD_LIST --> CLOSE_PAGE[关闭页面]
SKIP_CLOSE --> END([结束])
CLOSE_PAGE --> END
style START fill:#90EE90
style END fill:#FFB6C1
style EXT_LOOP fill:#FFE4B5
style CHECK_COUNT fill:#FFD700
style CHECK_STATUS fill:#FFD700
9. 关键设计模式
9.1 依赖注入模式
DiscreteMaterialCleaner 通过构造函数注入所有依赖服务,而不是直接创建实例:
cleaner = DiscreteMaterialCleaner(
config=config,
navigator=navigator,
extractor=extractor,
deletion_checker=deletion_checker,
)
优点:
- 降低耦合度
- 方便单元测试(可注入 Mock 对象)
- 灵活替换实现
9.2 接口隔离原则
通过抽象接口定义服务契约:
IPageNavigator- 页面导航接口IMaterialExtractor- 物料提取接口IDeletionChecker- 删除判断接口
优点:
- 职责分离清晰
- 实现可替换
- 便于扩展新功能
9.3 策略模式
IDeletionChecker 接口允许多种删除判断策略:
DatabaseDeletionChecker- 基于数据库查询- 可扩展其他策略(如基于规则、基于 API 等)
10. 嵌套 iframe 处理机制
YonBIP ERP 系统使用嵌套 iframe 结构,需要特殊处理:
page (浏览器上下文)
└── #forwardFrame (外层 iframe)
└── #mainiframe (内层 iframe,包含实际应用 UI)
处理模式:
outer_frame = page.locator("#forwardFrame").content_frame
inner_frame_locator = outer_frame.locator("#mainiframe")
inner_frame_locator.wait_for(state="visible", timeout=15000)
inner_frame = inner_frame_locator.content_frame
关键点:
- 必须等待内层 iframe 可见后再获取其 content_frame
- 操作前确保元素已加载完成
- 注意不同页面的 iframe 可能需要重新获取
11. 加载状态处理
ERP 系统使用"加载中"提示表示页面正在加载数据:
loading_locator = frame.locator("div").filter(has_text="加载中").nth(1)
try:
loading_locator.wait_for(state="visible", timeout=3000)
loading_locator.wait_for(state="hidden", timeout=0) # 无限等待
except PlaywrightTimeoutError:
# 加载很快完成或未出现
pass
处理逻辑:
- 先尝试等待"加载中"出现
- 然后无限等待直到消失
- 如果超时则认为加载已完成
12. 运行示例
# 激活虚拟环境
.venv\Scripts\activate
# 运行清理脚本
python main_clean.py
输出示例:
================================================================================
开始执行离散备料计划维护数据清理
================================================================================
使用负责人 [彭羽] 进行数据清理
从文件读取到 10 个总排号
查询到 10 个生产订单号
=== 开始处理第 1 个订单,订单号: PO20250101001 ===
第 1 个订单查询完成,等待加载结果...
第 1 个订单加载完成,开始清理数据...
等待备料计划页面加载完成...
备料计划页面加载完成,编码: BL20250101001
详细信息数量: 5
备料状态: 审批通过
处理 序号 1
材料编码: 00000001001
材料名称: 原材料A
累计待发数量: 100
累计出库数量: 0
>>> 需要清理:原材料A【00000001001】
...
开始执行账号注销...
=== 全部完成 ===
13. 扩展与维护
13.1 添加新的删除判断策略
实现 IDeletionChecker 接口:
class RuleBasedDeletionChecker(IDeletionChecker):
def should_delete(self, material: MaterialInfo) -> bool:
# 基于规则的判断逻辑
return material.name.startswith("测试")
13.2 修改配置参数
编辑 main_clean.py 中的 CleanerConfig 初始化:
config = CleanerConfig(
username="your_username",
password="your_password",
manager_name="新的负责人",
headless=True, # 无头模式
verbose=False, # 关闭详细日志
)
13.3 调试特定订单
启用调试模式:
config = CleanerConfig(
debug_mode=True,
debug_order=0, # 调试第一个订单
)
14. 常见问题
Q1: 页面加载超时
A: 检查网络连接,调整 UIConstants.Timeouts 中的超时配置
Q2: iframe 未找到
A: 确认 ERP 系统版本未变化,检查 Selectors.FORWARD_FRAME 和 Selectors.MAIN_IFRAME
Q3: 数据库查询失败
A: 检查 db/materials_to_delete.py 中的 SQL 连接配置
Q4: 物料信息提取错误
A: 使用 debug_mode=True 暂停执行,检查页面元素选择器
附录 A: 文件路径速查
| 文件 | 路径 |
|---|---|
| 主入口 | main_clean.py |
| 核心业务逻辑 | utils/discrete_material_cleaner.py |
| 页面导航服务 | services/page_navigator.py |
| 物料提取服务 | services/material_extractor.py |
| 删除判断服务 | services/deletion_checker.py |
| 清理器配置 | config/cleaner_config.py |
| UI 常量配置 | config/ui_constants.py |
| 物料信息模型 | models/material_info.py |
| 页面导航接口 | interfaces/i_page_navigator.py |
| 物料提取接口 | interfaces/i_material_extractor.py |
| 删除判断接口 | interfaces/i_deletion_checker.py |
附录 B: 接口定义摘要
IPageNavigator
def navigate_to_main_page(page: Page) -> Frame
def navigate_to_order_page(main_frame: Frame, page: Page) -> Tuple[Page, Frame]
def navigate_to_material_plan_page(page: Page) -> Tuple[Page, Frame]
def setup_query_interface(frame: Frame) -> None
def wait_for_page_loaded(frame: Frame) -> bool
IMaterialExtractor
def extract_detail_count(frame: Frame) -> int
def extract_detail_status(frame: Frame) -> str
def extract_materials(frame: Frame, count: int) -> List[MaterialInfo]
IDeletionChecker
def should_delete(material: MaterialInfo) -> bool
文档版本: 1.0 最后更新: 2026-02-10 维护者: Claude Code