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>
802 lines
22 KiB
Markdown
802 lines
22 KiB
Markdown
# main_clean.py 运行机制文档
|
||
|
||
## 1. 概述
|
||
|
||
### 1.1 系统简介
|
||
|
||
**离散备料计划维护数据清理工具** 是一个基于 Python + Playwright 开发的自动化工具,用于清理 YonBIP(用友)ERP 系统中的备料数据。该工具通过浏览器自动化技术访问 ERP 系统,提取生产订单的备料信息,并根据数据库查询结果判断哪些物料需要清理。
|
||
|
||
### 1.2 技术栈
|
||
|
||
- **Python 3.x**: 主要开发语言
|
||
- **Playwright**: 浏览器自动化框架(Chromium)
|
||
- **SQL Server**: 数据库查询(pyodbc)
|
||
- **数据类(dataclass)**: 配置和数据模型
|
||
|
||
### 1.3 主要功能
|
||
|
||
1. 自动登录 YonBIP ERP 系统
|
||
2. 读取待处理的生产订单列表
|
||
3. 遍历每个订单的备料计划
|
||
4. 提取物料详细信息(编码、名称、数量等)
|
||
5. 查询数据库判断物料是否需要删除
|
||
6. 标记需要清理的物料(预留删除接口)
|
||
|
||
---
|
||
|
||
## 2. 系统架构图
|
||
|
||
```mermaid
|
||
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. 主执行流程图
|
||
|
||
```mermaid
|
||
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. 组件交互时序图
|
||
|
||
```mermaid
|
||
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 相关常量
|
||
|
||
**包含三个嵌套数据类**:
|
||
|
||
1. **Selectors** - CSS 选择器
|
||
- 搜索相关: `SEARCH_BTN`, `SEARCH_ICON`, `QUERY_BY_ORDER`
|
||
- iframe: `FORWARD_FRAME`, `MAIN_IFRAME`
|
||
- 按钮: `MODIFY_BUTTON`, `SAVE_BUTTON`, `EXPAND_BUTTON`
|
||
|
||
2. **Timeouts** - 超时配置(毫秒)
|
||
- 页面加载: `IFRAME_VISIBLE`, `PLAN_CODE_VISIBLE`
|
||
- 加载等待: `LOADING_VISIBLE_WAIT`, `LOADING_HIDDEN_WAIT`
|
||
- 轮询: `PLAN_CODE_MAX_WAIT`, `PLAN_CHECK_INTERVAL`
|
||
|
||
3. **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. 配置类结构图
|
||
|
||
```mermaid
|
||
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. 数据流图
|
||
|
||
```mermaid
|
||
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. 单个订单处理流程
|
||
|
||
```mermaid
|
||
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` 通过构造函数注入所有依赖服务,而不是直接创建实例:
|
||
|
||
```python
|
||
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)
|
||
```
|
||
|
||
**处理模式**:
|
||
```python
|
||
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 系统使用"加载中"提示表示页面正在加载数据:
|
||
|
||
```python
|
||
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
|
||
```
|
||
|
||
**处理逻辑**:
|
||
1. 先尝试等待"加载中"出现
|
||
2. 然后无限等待直到消失
|
||
3. 如果超时则认为加载已完成
|
||
|
||
---
|
||
|
||
## 12. 运行示例
|
||
|
||
```bash
|
||
# 激活虚拟环境
|
||
.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` 接口:
|
||
|
||
```python
|
||
class RuleBasedDeletionChecker(IDeletionChecker):
|
||
def should_delete(self, material: MaterialInfo) -> bool:
|
||
# 基于规则的判断逻辑
|
||
return material.name.startswith("测试")
|
||
```
|
||
|
||
### 13.2 修改配置参数
|
||
|
||
编辑 `main_clean.py` 中的 `CleanerConfig` 初始化:
|
||
|
||
```python
|
||
config = CleanerConfig(
|
||
username="your_username",
|
||
password="your_password",
|
||
manager_name="新的负责人",
|
||
headless=True, # 无头模式
|
||
verbose=False, # 关闭详细日志
|
||
)
|
||
```
|
||
|
||
### 13.3 调试特定订单
|
||
|
||
启用调试模式:
|
||
|
||
```python
|
||
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
|
||
```python
|
||
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
|
||
```python
|
||
def extract_detail_count(frame: Frame) -> int
|
||
def extract_detail_status(frame: Frame) -> str
|
||
def extract_materials(frame: Frame, count: int) -> List[MaterialInfo]
|
||
```
|
||
|
||
### IDeletionChecker
|
||
```python
|
||
def should_delete(material: MaterialInfo) -> bool
|
||
```
|
||
|
||
---
|
||
|
||
**文档版本**: 1.0
|
||
**最后更新**: 2026-02-10
|
||
**维护者**: Claude Code
|