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:
@@ -1 +1,6 @@
|
|||||||
# Config package
|
"""配置模块 - 集中管理应用配置"""
|
||||||
|
|
||||||
|
from config.ui_constants import UIConstants
|
||||||
|
from config.cleaner_config import CleanerConfig
|
||||||
|
|
||||||
|
__all__ = ["UIConstants", "CleanerConfig"]
|
||||||
|
|||||||
20
config/cleaner_config.py
Normal file
20
config/cleaner_config.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
"""
|
||||||
|
清理器配置
|
||||||
|
管理离散备料计划清理器的运行配置
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CleanerConfig:
|
||||||
|
"""清理器配置"""
|
||||||
|
|
||||||
|
username: str
|
||||||
|
password: str
|
||||||
|
manager_name: str
|
||||||
|
headless: bool = False
|
||||||
|
verbose: bool = True
|
||||||
|
debug_mode: bool = False
|
||||||
|
debug_order: Optional[int] = None
|
||||||
72
config/ui_constants.py
Normal file
72
config/ui_constants.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
"""
|
||||||
|
UI 常量配置
|
||||||
|
集中管理所有 UI 相关的选择器、超时、重试配置
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Selectors:
|
||||||
|
"""CSS 选择器常量"""
|
||||||
|
|
||||||
|
# 搜索相关
|
||||||
|
SEARCH_BTN: str = ".search-component-searchBtn"
|
||||||
|
SEARCH_ICON: str = ".search-name-wrapper > .iconfont"
|
||||||
|
QUERY_BY_ORDER: str = "订单号查询"
|
||||||
|
TAB_ALL: str = "全部"
|
||||||
|
DISPLAY_COUNT_INPUT: str = "#rc_select_0"
|
||||||
|
|
||||||
|
# 订单相关
|
||||||
|
HOT_KEY_HEAD: str = "#hot-key-head_list"
|
||||||
|
MORE_BUTTON: str = "更多"
|
||||||
|
MATERIAL_PLAN: str = "备料计划"
|
||||||
|
|
||||||
|
# iframe
|
||||||
|
FORWARD_FRAME: str = "#forwardFrame"
|
||||||
|
MAIN_IFRAME: str = "#mainiframe"
|
||||||
|
|
||||||
|
# 按钮
|
||||||
|
MODIFY_BUTTON: str = "修改"
|
||||||
|
SAVE_BUTTON: str = "保存"
|
||||||
|
EXPAND_BUTTON: str = "展开"
|
||||||
|
|
||||||
|
# 容器
|
||||||
|
CARD_TABLE_SIDE_BOX: str = ".card-table-side-box"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Timeouts:
|
||||||
|
"""超时配置常量(单位:毫秒)"""
|
||||||
|
|
||||||
|
# 页面加载
|
||||||
|
IFRAME_VISIBLE: int = 15000
|
||||||
|
PLAN_CODE_VISIBLE: int = 30000
|
||||||
|
BUTTON_VISIBLE: int = 10000
|
||||||
|
SERIAL_VISIBLE: int = 10000
|
||||||
|
FORM_VISIBLE: int = 5000
|
||||||
|
|
||||||
|
# 加载等待
|
||||||
|
LOADING_VISIBLE_WAIT: int = 3000
|
||||||
|
LOADING_HIDDEN_WAIT: int = 0 # 无限等待
|
||||||
|
|
||||||
|
# 轮询等待(秒)
|
||||||
|
PLAN_CODE_MAX_WAIT: int = 30 # 最多等待30秒
|
||||||
|
PLAN_CHECK_INTERVAL: float = 0.5 # 每0.5秒检查一次
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RetryConfig:
|
||||||
|
"""重试配置"""
|
||||||
|
|
||||||
|
DISPLAY_COUNT_MAX_RETRIES: int = 3
|
||||||
|
EXPECTED_DISPLAY_COUNT: str = "5000"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class UIConstants:
|
||||||
|
"""UI 配置总入口"""
|
||||||
|
|
||||||
|
selectors: Selectors = Selectors()
|
||||||
|
timeouts: Timeouts = Timeouts()
|
||||||
|
retry: RetryConfig = RetryConfig()
|
||||||
801
docs/MAIN_CLEAN_EXECUTION.md
Normal file
801
docs/MAIN_CLEAN_EXECUTION.md
Normal file
@@ -0,0 +1,801 @@
|
|||||||
|
# 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
|
||||||
@@ -1,345 +0,0 @@
|
|||||||
# main_clean.py 流程说明
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
|
|
||||||
`main_clean.py` 是离散备料计划维护数据清理工具的主程序入口。该工具通过 Playwright 自动化登录用友 BIP 系统,读取生产订单号,然后逐个清理订单中不需要的物料数据。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 系统架构图
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TB
|
|
||||||
Start([开始]) --> Init[初始化 DiscreteMaterialPlanCleaner]
|
|
||||||
|
|
||||||
Init --> QueryDB[(查询数据库<br/>获取待删除物料列表)]
|
|
||||||
|
|
||||||
QueryDB --> Login[登录用友BIP系统]
|
|
||||||
|
|
||||||
Login --> OpenMenu[点击功能菜单]
|
|
||||||
OpenMenu --> OpenPage[打开离散生产订单维护页面]
|
|
||||||
|
|
||||||
OpenPage --> SetupQuery[设置查询界面]
|
|
||||||
SetupQuery --> ReadFile[(读取ProductionID.txt)]
|
|
||||||
ReadFile --> GetOrders[(查询数据库<br/>获取生产订单号)]
|
|
||||||
|
|
||||||
GetOrders --> LoopStart{遍历订单}
|
|
||||||
|
|
||||||
LoopStart --> ProcessOrder[处理单个订单]
|
|
||||||
ProcessOrder --> QueryOrder[查询订单详情]
|
|
||||||
QueryOrder --> OpenPlanPage[打开备料计划页面]
|
|
||||||
OpenPlanPage --> WaitLoad[等待页面加载完成]
|
|
||||||
|
|
||||||
WaitLoad --> CheckData{检查数据状态}
|
|
||||||
|
|
||||||
CheckData -->|detail_count = 0| SkipOrder[跳过该订单]
|
|
||||||
CheckData -->|status != 审批通过| SkipOrder
|
|
||||||
CheckData -->|detail_count > 0<br/>status = 审批通过| ClickModify[点击修改按钮]
|
|
||||||
|
|
||||||
ClickModify --> ExpandForm[展开详细信息表单]
|
|
||||||
ExpandForm --> LoopMaterials{遍历物料}
|
|
||||||
|
|
||||||
LoopMaterials --> GetMaterial[获取物料信息]
|
|
||||||
GetMaterial --> CheckDelete{需要删除?}
|
|
||||||
|
|
||||||
CheckDelete -->|是| MarkDelete[标记待删除]
|
|
||||||
CheckDelete -->|否| KeepMaterial[保留物料]
|
|
||||||
|
|
||||||
MarkDelete --> NextMaterial{下一个物料?}
|
|
||||||
KeepMaterial --> NextMaterial
|
|
||||||
|
|
||||||
NextMaterial -->|是| LoopMaterials
|
|
||||||
NextMaterial -->|否| ClosePlanPage[关闭备料计划页面]
|
|
||||||
|
|
||||||
SkipOrder --> ClosePlanPage
|
|
||||||
ClosePlanPage --> NextOrder{下一个订单?}
|
|
||||||
|
|
||||||
NextOrder -->|是| LoopStart
|
|
||||||
NextOrder -->|否| Logout[注销账号]
|
|
||||||
|
|
||||||
Logout --> CloseBrowser[关闭浏览器]
|
|
||||||
CloseBrowser --> End([结束])
|
|
||||||
|
|
||||||
style Init fill:#e1f5ff
|
|
||||||
style QueryDB fill:#fff4e1
|
|
||||||
style Login fill:#e1f5ff
|
|
||||||
style End fill:#ffe1e1
|
|
||||||
style CheckDelete fill:#fff4e1
|
|
||||||
style CheckData fill:#fff4e1
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 详细流程说明
|
|
||||||
|
|
||||||
### 1. 初始化阶段
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart LR
|
|
||||||
A[main函数] --> B[创建 DiscreteMaterialPlanCleaner 实例]
|
|
||||||
B --> C[配置参数:<br/>username, password,<br/>manager_name, headless, verbose]
|
|
||||||
|
|
||||||
style A fill:#e1f5ff
|
|
||||||
style B fill:#e1f5ff
|
|
||||||
style C fill:#e1f5ff
|
|
||||||
```
|
|
||||||
|
|
||||||
**代码位置**: `main_clean.py:9-24`
|
|
||||||
|
|
||||||
```python
|
|
||||||
cleaner = DiscreteMaterialPlanCleaner(
|
|
||||||
username="BLDpengqiangqiang",
|
|
||||||
password="Cqbld123456.",
|
|
||||||
manager_name="彭羽",
|
|
||||||
headless=False,
|
|
||||||
verbose=True,
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. 数据准备阶段
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TB
|
|
||||||
A[clean.clean方法] --> B[查询数据库<br/>MaterialsToBeDeleted表]
|
|
||||||
B --> C[获取指定负责人的<br/>待删除物料编码列表]
|
|
||||||
|
|
||||||
style A fill:#fff4e1
|
|
||||||
style B fill:#fff4e1
|
|
||||||
style C fill:#fff4e1
|
|
||||||
```
|
|
||||||
|
|
||||||
**代码位置**: `utils/离散备料计划维护数据清理.py:292-304`
|
|
||||||
|
|
||||||
| 模块 | 功能 | 文件 |
|
|
||||||
|------|------|------|
|
|
||||||
| `get_materials_to_delete()` | 查询待删除物料编码 | `db/materials_to_delete.py` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. 登录与导航
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant Main as main_clean.py
|
|
||||||
participant Auth as auth.py
|
|
||||||
participant Browser as Chromium Browser
|
|
||||||
participant ERP as 用友BIP系统
|
|
||||||
|
|
||||||
Main->>Auth: login(playwright, username, password)
|
|
||||||
Auth->>Browser: launch(headless=False)
|
|
||||||
Auth->>Browser: new_context()
|
|
||||||
Auth->>Browser: new_page()
|
|
||||||
Auth->>ERP: goto(登录页面URL)
|
|
||||||
Auth->>ERP: 填写用户名和密码
|
|
||||||
Auth->>ERP: 点击登录按钮
|
|
||||||
alt 有强制登录弹窗
|
|
||||||
ERP-->>Auth: 显示弹窗
|
|
||||||
Auth->>ERP: 点击确定
|
|
||||||
end
|
|
||||||
Auth-->>Main: 返回 (browser, context, page, main_frame)
|
|
||||||
```
|
|
||||||
|
|
||||||
**代码位置**: `utils/auth.py:8-72`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4. 设置查询界面
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TB
|
|
||||||
A[点击图标按钮] --> B[选择"订单号查询"]
|
|
||||||
B --> C[点击"全部"标签]
|
|
||||||
C --> D{填充显示条数}
|
|
||||||
D --> E{成功?}
|
|
||||||
E -->|否| F[重试<br/>最多3次]
|
|
||||||
F --> D
|
|
||||||
E -->|是| G[继续]
|
|
||||||
|
|
||||||
style D fill:#fff4e1
|
|
||||||
style E fill:#fff4e1
|
|
||||||
style F fill:#ffe1e1
|
|
||||||
```
|
|
||||||
|
|
||||||
**代码位置**: `utils/离散备料计划维护数据清理.py:263-290`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 5. 获取生产订单号
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart LR
|
|
||||||
A[read_production_ids] --> B[读取ProductionID.txt]
|
|
||||||
B --> C[解析总排号列表]
|
|
||||||
C --> D[query_production_order_numbers]
|
|
||||||
D --> E[查询数据库<br/>获取生产订单号]
|
|
||||||
|
|
||||||
style A fill:#fff4e1
|
|
||||||
style D fill:#fff4e1
|
|
||||||
```
|
|
||||||
|
|
||||||
**代码位置**: `utils/离散备料计划维护数据清理.py:41-59`
|
|
||||||
|
|
||||||
| 函数 | 功能 | 数据库表 |
|
|
||||||
|------|------|----------|
|
|
||||||
| `read_production_ids()` | 读取总排号文件 | 文件输入 |
|
|
||||||
| `query_production_order_numbers()` | 查询生产订单号 | ProductionOrder |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 6. 订单处理流程
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TB
|
|
||||||
Start([开始处理订单]) --> FillInput[填充生产订单号]
|
|
||||||
FillInput --> ClickQuery[点击查询按钮]
|
|
||||||
ClickQuery --> WaitLoad[等待加载完成]
|
|
||||||
|
|
||||||
WaitLoad --> ClickMore[点击"更多"按钮]
|
|
||||||
ClickMore --> OpenPlan[打开"备料计划"页面]
|
|
||||||
|
|
||||||
OpenPlan --> GetIframe[获取嵌套iframe]
|
|
||||||
GetIframe --> WaitPlanLoad[等待备料计划页面加载]
|
|
||||||
|
|
||||||
WaitPlanLoad --> ExtractInfo[提取信息:<br/>- 编码<br/>- 详细信息数量<br/>- 备料状态]
|
|
||||||
|
|
||||||
ExtractInfo --> CheckStatus{状态检查}
|
|
||||||
|
|
||||||
style Start fill:#e1f5ff
|
|
||||||
style CheckStatus fill:#fff4e1
|
|
||||||
```
|
|
||||||
|
|
||||||
**代码位置**: `utils/离散备料计划维护数据清理.py:61-167`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 7. 物料清理流程
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TB
|
|
||||||
Check{detail_count > 0<br/>&<br/>status = 审批通过} -->|否| Skip[跳过处理]
|
|
||||||
Check -->|是| ClickModify[点击修改按钮]
|
|
||||||
|
|
||||||
ClickModify --> WaitSave[等待保存按钮可见]
|
|
||||||
WaitSave --> Expand[点击展开按钮]
|
|
||||||
Expand --> GetForm[获取表单容器]
|
|
||||||
|
|
||||||
GetForm --> LoopStart{遍历物料}
|
|
||||||
|
|
||||||
LoopStart --> GetSerial[定位序号]
|
|
||||||
GetSerial --> GetCode[获取材料编码]
|
|
||||||
GetCode --> GetName[获取材料名称]
|
|
||||||
GetName --> GetQty1[获取累计待发数量]
|
|
||||||
GetQty1 --> GetQty2[获取累计出库数量]
|
|
||||||
|
|
||||||
GetQty2 --> Match{物料编码<br/>在待删除列表中?}
|
|
||||||
|
|
||||||
Match -->|精确匹配| Delete[标记待删除]
|
|
||||||
Match -->|未匹配| Keep[保留]
|
|
||||||
|
|
||||||
Delete --> Next{下一个?}
|
|
||||||
Keep --> Next
|
|
||||||
|
|
||||||
Next -->|是| LoopStart
|
|
||||||
Next -->|否| Done([完成])
|
|
||||||
|
|
||||||
style Check fill:#fff4e1
|
|
||||||
style Match fill:#fff4e1
|
|
||||||
style Done fill:#e1f5ff
|
|
||||||
```
|
|
||||||
|
|
||||||
**代码位置**: `utils/离散备料计划维护数据清理.py:169-261`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 关键数据结构
|
|
||||||
|
|
||||||
### DiscreteMaterialPlanCleaner 类
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
classDiagram
|
|
||||||
class DiscreteMaterialPlanCleaner {
|
|
||||||
+str username
|
|
||||||
+str password
|
|
||||||
+str manager_name
|
|
||||||
+bool headless
|
|
||||||
+bool verbose
|
|
||||||
+_print(args)
|
|
||||||
+get_production_order_numbers(file)
|
|
||||||
+process_order(frame, order_id, ...)
|
|
||||||
+setup_query_interface(frame)
|
|
||||||
+clean(file, debug_mode, ...)
|
|
||||||
}
|
|
||||||
|
|
||||||
class auth {
|
|
||||||
+login(playwright, username, password)
|
|
||||||
+logout(main_frame, verbose)
|
|
||||||
+close_session(browser, context)
|
|
||||||
}
|
|
||||||
|
|
||||||
class materials_to_delete {
|
|
||||||
+get_materials_to_delete(manager_name)
|
|
||||||
+get_all_materials_to_delete()
|
|
||||||
}
|
|
||||||
|
|
||||||
class production_order_query {
|
|
||||||
+read_production_ids(file)
|
|
||||||
+query_production_order_numbers(ids)
|
|
||||||
}
|
|
||||||
|
|
||||||
DiscreteMaterialPlanCleaner --> auth
|
|
||||||
DiscreteMaterialPlanCleaner --> materials_to_delete
|
|
||||||
DiscreteMaterialPlanCleaner --> production_order_query
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 配置参数说明
|
|
||||||
|
|
||||||
| 参数 | 类型 | 默认值 | 说明 |
|
|
||||||
|------|------|--------|------|
|
|
||||||
| `username` | str | - | 登录用户名 |
|
|
||||||
| `password` | str | - | 登录密码 |
|
|
||||||
| `manager_name` | str | - | 负责人姓名(用于查询待删除物料) |
|
|
||||||
| `headless` | bool | False | 是否无头模式运行浏览器 |
|
|
||||||
| `verbose` | bool | True | 是否打印详细日志 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 文件依赖关系
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TD
|
|
||||||
A[main_clean.py] --> B[utils/离散备料计划维护数据清理.py]
|
|
||||||
B --> C[utils/auth.py]
|
|
||||||
B --> D[db/production_order_query.py]
|
|
||||||
B --> E[db/materials_to_delete.py]
|
|
||||||
B --> F[db/connection.py]
|
|
||||||
|
|
||||||
style A fill:#e1f5ff
|
|
||||||
style B fill:#e1f5ff
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 执行入口
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 直接运行
|
|
||||||
python main_clean.py
|
|
||||||
|
|
||||||
# 或在虚拟环境中运行
|
|
||||||
.venv\Scripts\activate
|
|
||||||
python main_clean.py
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
1. **浏览器模式**: 默认使用非无头模式 (`headless=False`),便于调试观察
|
|
||||||
2. **日志输出**: 启用详细日志 (`verbose=True`),打印每一步操作
|
|
||||||
3. **数据库连接**: 使用上下文管理器自动管理数据库连接
|
|
||||||
4. **异常处理**: 加载超时会自动重试或跳过
|
|
||||||
5. **调试模式**: 支持在指定订单处暂停调试
|
|
||||||
7
interfaces/__init__.py
Normal file
7
interfaces/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
"""接口模块 - 定义服务接口"""
|
||||||
|
|
||||||
|
from interfaces.i_page_navigator import IPageNavigator
|
||||||
|
from interfaces.i_material_extractor import IMaterialExtractor
|
||||||
|
from interfaces.i_deletion_checker import IDeletionChecker
|
||||||
|
|
||||||
|
__all__ = ["IPageNavigator", "IMaterialExtractor", "IDeletionChecker"]
|
||||||
24
interfaces/i_deletion_checker.py
Normal file
24
interfaces/i_deletion_checker.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"""
|
||||||
|
删除判断接口
|
||||||
|
定义判断物料是否需要删除的抽象接口
|
||||||
|
"""
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from models.material_info import MaterialInfo
|
||||||
|
|
||||||
|
|
||||||
|
class IDeletionChecker(ABC):
|
||||||
|
"""删除判断接口"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def should_delete(self, material: MaterialInfo) -> bool:
|
||||||
|
"""
|
||||||
|
判断物料是否需要删除
|
||||||
|
|
||||||
|
Args:
|
||||||
|
material: 物料信息对象
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
是否需要删除
|
||||||
|
"""
|
||||||
|
pass
|
||||||
53
interfaces/i_material_extractor.py
Normal file
53
interfaces/i_material_extractor.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
"""
|
||||||
|
物料信息提取接口
|
||||||
|
定义从页面提取物料信息的抽象接口
|
||||||
|
"""
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from playwright.sync_api import Frame
|
||||||
|
from typing import List
|
||||||
|
from models.material_info import MaterialInfo
|
||||||
|
|
||||||
|
|
||||||
|
class IMaterialExtractor(ABC):
|
||||||
|
"""物料信息提取接口"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def extract_detail_count(self, frame: Frame) -> int:
|
||||||
|
"""
|
||||||
|
提取详细信息数量
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: 目标 iframe
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
详细信息数量
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def extract_detail_status(self, frame: Frame) -> str:
|
||||||
|
"""
|
||||||
|
提取备料状态
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: 目标 iframe
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
备料状态文本
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def extract_materials(self, frame: Frame, count: int) -> List[MaterialInfo]:
|
||||||
|
"""
|
||||||
|
提取所有物料信息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: 目标 iframe
|
||||||
|
count: 物料数量
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
物料信息列表
|
||||||
|
"""
|
||||||
|
pass
|
||||||
75
interfaces/i_page_navigator.py
Normal file
75
interfaces/i_page_navigator.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
"""
|
||||||
|
页面导航接口
|
||||||
|
定义页面导航和 iframe 操作的抽象接口
|
||||||
|
"""
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from playwright.sync_api import Page, Frame
|
||||||
|
from typing import Tuple, Optional
|
||||||
|
|
||||||
|
|
||||||
|
class IPageNavigator(ABC):
|
||||||
|
"""页面导航接口 - 处理 iframe 导航、页面切换"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def navigate_to_main_page(self, page: Page) -> Frame:
|
||||||
|
"""
|
||||||
|
导航到主页面 iframe
|
||||||
|
|
||||||
|
Args:
|
||||||
|
page: 浏览器页面对象
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
主页面 iframe 的 Frame 对象
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def navigate_to_order_page(self, main_frame: Frame, page: Page) -> Tuple[Page, Frame]:
|
||||||
|
"""
|
||||||
|
导航到订单页面
|
||||||
|
|
||||||
|
Args:
|
||||||
|
main_frame: 主 iframe
|
||||||
|
page: 当前页面对象
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(新页面, 内部 iframe)
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def navigate_to_material_plan_page(self, page: Page) -> Tuple[Page, Frame]:
|
||||||
|
"""
|
||||||
|
导航到备料计划页面
|
||||||
|
|
||||||
|
Args:
|
||||||
|
page: 订单页面对象
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(新页面, 内部 iframe)
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def setup_query_interface(self, frame: Frame) -> None:
|
||||||
|
"""
|
||||||
|
设置查询界面
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: 目标 iframe
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def wait_for_page_loaded(self, frame: Frame) -> bool:
|
||||||
|
"""
|
||||||
|
等待页面加载完成
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: 目标 iframe
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
是否加载成功
|
||||||
|
"""
|
||||||
|
pass
|
||||||
5
models/__init__.py
Normal file
5
models/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
"""数据模型模块 - 定义数据结构"""
|
||||||
|
|
||||||
|
from models.material_info import MaterialInfo
|
||||||
|
|
||||||
|
__all__ = ["MaterialInfo"]
|
||||||
19
models/material_info.py
Normal file
19
models/material_info.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
"""
|
||||||
|
物料信息数据模型
|
||||||
|
定义物料信息的数据结构
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MaterialInfo:
|
||||||
|
"""物料信息"""
|
||||||
|
|
||||||
|
serial_number: int # 序号
|
||||||
|
code: str # 物料编码
|
||||||
|
name: str # 物料名称
|
||||||
|
pending_quantity: str # 累计待发数量
|
||||||
|
shipped_quantity: str # 累计出库数量
|
||||||
|
should_delete: bool = False # 是否需要删除
|
||||||
7
services/__init__.py
Normal file
7
services/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
"""服务模块 - 实现业务逻辑"""
|
||||||
|
|
||||||
|
from services.page_navigator import PageNavigator
|
||||||
|
from services.material_extractor import MaterialExtractor
|
||||||
|
from services.deletion_checker import DatabaseDeletionChecker
|
||||||
|
|
||||||
|
__all__ = ["PageNavigator", "MaterialExtractor", "DatabaseDeletionChecker"]
|
||||||
45
services/deletion_checker.py
Normal file
45
services/deletion_checker.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
"""
|
||||||
|
删除判断服务实现
|
||||||
|
基于数据库查询判断物料是否需要删除
|
||||||
|
"""
|
||||||
|
|
||||||
|
from interfaces.i_deletion_checker import IDeletionChecker
|
||||||
|
from models.material_info import MaterialInfo
|
||||||
|
from db.materials_to_delete import should_delete_material
|
||||||
|
|
||||||
|
|
||||||
|
class DatabaseDeletionChecker(IDeletionChecker):
|
||||||
|
"""基于数据库的删除判断实现"""
|
||||||
|
|
||||||
|
def __init__(self, manager_name: str, verbose: bool = True):
|
||||||
|
"""
|
||||||
|
初始化删除检查器
|
||||||
|
|
||||||
|
Args:
|
||||||
|
manager_name: 负责人姓名
|
||||||
|
verbose: 是否打印详细日志
|
||||||
|
"""
|
||||||
|
self.manager_name = manager_name
|
||||||
|
self.verbose = verbose
|
||||||
|
|
||||||
|
def _print(self, *args, **kwargs):
|
||||||
|
"""打印日志(如果 verbose=True)"""
|
||||||
|
if self.verbose:
|
||||||
|
print(*args, **kwargs)
|
||||||
|
|
||||||
|
def should_delete(self, material: MaterialInfo) -> bool:
|
||||||
|
"""
|
||||||
|
通过查询数据库判断是否需要删除
|
||||||
|
|
||||||
|
Args:
|
||||||
|
material: 物料信息对象
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
是否需要删除
|
||||||
|
"""
|
||||||
|
result = should_delete_material(self.manager_name, material.code)
|
||||||
|
if self.verbose and result:
|
||||||
|
self._print(f">>> 需要清理:{material.name}【{material.code}】")
|
||||||
|
elif self.verbose and not result:
|
||||||
|
self._print(f"保留:{material.name}【{material.code}】无需清理")
|
||||||
|
return result
|
||||||
166
services/material_extractor.py
Normal file
166
services/material_extractor.py
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
"""
|
||||||
|
物料信息提取服务实现
|
||||||
|
实现从页面提取物料信息的具体逻辑
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
from playwright.sync_api import TimeoutError as PlaywrightTimeoutError
|
||||||
|
from interfaces.i_material_extractor import IMaterialExtractor
|
||||||
|
from config.ui_constants import UIConstants
|
||||||
|
from models.material_info import MaterialInfo
|
||||||
|
|
||||||
|
|
||||||
|
class MaterialExtractor(IMaterialExtractor):
|
||||||
|
"""物料信息提取实现"""
|
||||||
|
|
||||||
|
def __init__(self, config: UIConstants, verbose: bool = True):
|
||||||
|
"""
|
||||||
|
初始化物料提取器
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: UI 配置
|
||||||
|
verbose: 是否打印详细日志
|
||||||
|
"""
|
||||||
|
self.config = config
|
||||||
|
self.verbose = verbose
|
||||||
|
|
||||||
|
def _print(self, *args, **kwargs):
|
||||||
|
"""打印日志(如果 verbose=True)"""
|
||||||
|
if self.verbose:
|
||||||
|
print(*args, **kwargs)
|
||||||
|
|
||||||
|
def extract_detail_count(self, frame) -> int:
|
||||||
|
"""
|
||||||
|
提取详细信息数量
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: 目标 iframe
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
详细信息数量
|
||||||
|
"""
|
||||||
|
detail_element = frame.get_by_text(re.compile(r"^详细信息 \(\d+\)$"))
|
||||||
|
detail_text = detail_element.inner_text()
|
||||||
|
match = re.search(r"详细信息 \((\d+)\)", detail_text)
|
||||||
|
if match:
|
||||||
|
detail_count = int(match.group(1))
|
||||||
|
self._print(f"详细信息数量: {detail_count}")
|
||||||
|
return detail_count
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def extract_detail_status(self, frame) -> str:
|
||||||
|
"""
|
||||||
|
提取备料状态
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: 目标 iframe
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
备料状态文本
|
||||||
|
"""
|
||||||
|
detail_element = frame.get_by_text(re.compile(r"^备料状态:.+$"))
|
||||||
|
detail_text = detail_element.inner_text().replace("\n", "")
|
||||||
|
match = re.search(r"^备料状态:(.+)$", detail_text)
|
||||||
|
if match:
|
||||||
|
detail_status = match.group(1)
|
||||||
|
self._print(f"备料状态: {detail_status}")
|
||||||
|
return detail_status
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def extract_materials(self, frame, count: int):
|
||||||
|
"""
|
||||||
|
提取所有物料信息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: 目标 iframe
|
||||||
|
count: 物料数量
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
物料信息列表
|
||||||
|
"""
|
||||||
|
materials = []
|
||||||
|
|
||||||
|
# 获取展开后的父容器
|
||||||
|
child_form = frame.locator(self.config.selectors.CARD_TABLE_SIDE_BOX)
|
||||||
|
child_form.wait_for(state="visible", timeout=self.config.timeouts.FORM_VISIBLE)
|
||||||
|
self._print(f"父容器 {self.config.selectors.CARD_TABLE_SIDE_BOX} 已找到")
|
||||||
|
|
||||||
|
for i in range(count):
|
||||||
|
serial_number = i + 1
|
||||||
|
id_label_locator = child_form.get_by_text("序号 " + str(serial_number))
|
||||||
|
id_label_locator.wait_for(
|
||||||
|
state="visible", timeout=self.config.timeouts.SERIAL_VISIBLE
|
||||||
|
)
|
||||||
|
self._print(f"处理 {id_label_locator.inner_text()}")
|
||||||
|
|
||||||
|
# 提取物料信息
|
||||||
|
code = self._extract_material_code(child_form)
|
||||||
|
name = self._extract_material_name(child_form)
|
||||||
|
pending_quantity = self._extract_pending_quantity(child_form)
|
||||||
|
shipped_quantity = self._extract_shipped_quantity(child_form)
|
||||||
|
|
||||||
|
material = MaterialInfo(
|
||||||
|
serial_number=serial_number,
|
||||||
|
code=code,
|
||||||
|
name=name,
|
||||||
|
pending_quantity=pending_quantity,
|
||||||
|
shipped_quantity=shipped_quantity,
|
||||||
|
)
|
||||||
|
materials.append(material)
|
||||||
|
|
||||||
|
# 导航到下一个物料
|
||||||
|
self._navigate_to_next_material(child_form, i, count)
|
||||||
|
|
||||||
|
return materials
|
||||||
|
|
||||||
|
def _extract_material_code(self, child_form) -> str:
|
||||||
|
"""提取物料编码"""
|
||||||
|
input_box = (
|
||||||
|
child_form.locator("div")
|
||||||
|
.filter(has_text=re.compile(r"^材料编码\d{11}$", re.MULTILINE))
|
||||||
|
.locator("input")
|
||||||
|
.first
|
||||||
|
)
|
||||||
|
code = input_box.input_value()
|
||||||
|
self._print(f"材料编码: {code}")
|
||||||
|
return code
|
||||||
|
|
||||||
|
def _extract_material_name(self, child_form) -> str:
|
||||||
|
"""提取物料名称"""
|
||||||
|
input_box = (
|
||||||
|
child_form.locator("div")
|
||||||
|
.filter(has_text=re.compile(r"^材料名称$"))
|
||||||
|
.locator("input[type='text']")
|
||||||
|
)
|
||||||
|
name = input_box.input_value()
|
||||||
|
self._print(f"材料名称: {name}")
|
||||||
|
return name
|
||||||
|
|
||||||
|
def _extract_pending_quantity(self, child_form) -> str:
|
||||||
|
"""提取累计待发数量"""
|
||||||
|
input_box = (
|
||||||
|
child_form.locator("div")
|
||||||
|
.filter(has_text=re.compile(r"^累计待发数量$"))
|
||||||
|
.locator("input[type='text']")
|
||||||
|
)
|
||||||
|
quantity = input_box.input_value()
|
||||||
|
self._print(f"累计待发数量: {quantity}")
|
||||||
|
return quantity
|
||||||
|
|
||||||
|
def _extract_shipped_quantity(self, child_form) -> str:
|
||||||
|
"""提取累计出库数量"""
|
||||||
|
input_box = (
|
||||||
|
child_form.locator("div")
|
||||||
|
.filter(has_text=re.compile(r"^累计出库数量$"))
|
||||||
|
.locator("input[type='text']")
|
||||||
|
)
|
||||||
|
quantity = input_box.input_value()
|
||||||
|
self._print(f"累计出库数量: {quantity}")
|
||||||
|
return quantity
|
||||||
|
|
||||||
|
def _navigate_to_next_material(self, child_form, current_index: int, total_count: int):
|
||||||
|
"""导航到下一个物料"""
|
||||||
|
if current_index != total_count - 1:
|
||||||
|
child_form.get_by_role("button").filter(has_text=re.compile(r"^$")).nth(2).click()
|
||||||
|
else:
|
||||||
|
child_form.get_by_role("button").filter(has_text=re.compile(r"^$")).nth(4).click()
|
||||||
171
services/page_navigator.py
Normal file
171
services/page_navigator.py
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
"""
|
||||||
|
页面导航服务实现
|
||||||
|
实现页面导航、iframe 操作等具体逻辑
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
from playwright.sync_api import TimeoutError as PlaywrightTimeoutError
|
||||||
|
from interfaces.i_page_navigator import IPageNavigator
|
||||||
|
from config.ui_constants import UIConstants
|
||||||
|
|
||||||
|
|
||||||
|
class PageNavigator(IPageNavigator):
|
||||||
|
"""页面导航实现"""
|
||||||
|
|
||||||
|
def __init__(self, config: UIConstants, verbose: bool = True):
|
||||||
|
"""
|
||||||
|
初始化页面导航器
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: UI 配置
|
||||||
|
verbose: 是否打印详细日志
|
||||||
|
"""
|
||||||
|
self.config = config
|
||||||
|
self.verbose = verbose
|
||||||
|
|
||||||
|
def _print(self, *args, **kwargs):
|
||||||
|
"""打印日志(如果 verbose=True)"""
|
||||||
|
if self.verbose:
|
||||||
|
print(*args, **kwargs)
|
||||||
|
|
||||||
|
def navigate_to_main_page(self, page) -> tuple:
|
||||||
|
"""
|
||||||
|
导航到主页面 iframe
|
||||||
|
|
||||||
|
实际返回 (page, main_frame, inner_frame) 的元组
|
||||||
|
"""
|
||||||
|
# 点击打开"功能菜单"
|
||||||
|
main_frame = page.locator(self.config.selectors.FORWARD_FRAME).content_frame
|
||||||
|
main_frame.locator("i").first.click()
|
||||||
|
|
||||||
|
# 点击打开"离散生产订单维护"
|
||||||
|
with page.expect_popup() as page1_info:
|
||||||
|
main_frame.get_by_title("离散生产订单维护", exact=True).first.click()
|
||||||
|
page1 = page1_info.value
|
||||||
|
|
||||||
|
# 获取 nested iframe
|
||||||
|
main_frame = page1.locator(self.config.selectors.FORWARD_FRAME).content_frame
|
||||||
|
inner_frame_locator = main_frame.locator(self.config.selectors.MAIN_IFRAME)
|
||||||
|
inner_frame_locator.wait_for(
|
||||||
|
state="visible", timeout=self.config.timeouts.IFRAME_VISIBLE
|
||||||
|
)
|
||||||
|
inner_frame = inner_frame_locator.content_frame
|
||||||
|
|
||||||
|
return page1, main_frame, inner_frame
|
||||||
|
|
||||||
|
def navigate_to_order_page(self, main_frame, page):
|
||||||
|
"""导航到订单页面"""
|
||||||
|
# 这个方法在当前实现中与 navigate_to_main_page 类似
|
||||||
|
# 返回新的页面对象和内部 iframe
|
||||||
|
return self.navigate_to_main_page(page)
|
||||||
|
|
||||||
|
def navigate_to_material_plan_page(self, page):
|
||||||
|
"""
|
||||||
|
导航到备料计划页面
|
||||||
|
|
||||||
|
Args:
|
||||||
|
page: 订单页面对象 (page1)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(page2, inner_frame) - 新页面和内部 iframe
|
||||||
|
"""
|
||||||
|
# 获取主 iframe
|
||||||
|
main_frame = page.locator(self.config.selectors.FORWARD_FRAME).content_frame
|
||||||
|
inner_frame = main_frame.locator(self.config.selectors.MAIN_IFRAME).content_frame
|
||||||
|
|
||||||
|
# 点击"更多"
|
||||||
|
inner_frame.locator(self.config.selectors.HOT_KEY_HEAD).get_by_text(
|
||||||
|
self.config.selectors.MORE_BUTTON
|
||||||
|
).click()
|
||||||
|
|
||||||
|
# 点击"备料计划"并等待新页面
|
||||||
|
with page.expect_popup() as page2_info:
|
||||||
|
inner_frame.get_by_text(self.config.selectors.MATERIAL_PLAN).click()
|
||||||
|
page2 = page2_info.value
|
||||||
|
|
||||||
|
# 获取 nested iframe
|
||||||
|
main_frame = page2.locator(self.config.selectors.FORWARD_FRAME).content_frame
|
||||||
|
inner_frame_locator = main_frame.locator(self.config.selectors.MAIN_IFRAME)
|
||||||
|
inner_frame_locator.wait_for(
|
||||||
|
state="visible", timeout=self.config.timeouts.IFRAME_VISIBLE
|
||||||
|
)
|
||||||
|
inner_frame = inner_frame_locator.content_frame
|
||||||
|
|
||||||
|
# 等待备料计划页面加载完成
|
||||||
|
self._wait_for_plan_page_loaded(inner_frame)
|
||||||
|
|
||||||
|
return page2, inner_frame
|
||||||
|
|
||||||
|
def setup_query_interface(self, frame):
|
||||||
|
"""设置查询界面"""
|
||||||
|
# 点击图标按钮打开查询界面
|
||||||
|
frame.locator(self.config.selectors.SEARCH_ICON).click()
|
||||||
|
frame.get_by_text(self.config.selectors.QUERY_BY_ORDER).click()
|
||||||
|
frame.get_by_role("tab", name=self.config.selectors.TAB_ALL).click()
|
||||||
|
|
||||||
|
# 填充并验证显示数量,如果失败则重试
|
||||||
|
max_retries = self.config.retry.DISPLAY_COUNT_MAX_RETRIES
|
||||||
|
expected_value = self.config.retry.EXPECTED_DISPLAY_COUNT
|
||||||
|
|
||||||
|
for attempt in range(max_retries):
|
||||||
|
frame.locator(self.config.selectors.DISPLAY_COUNT_INPUT).fill(expected_value)
|
||||||
|
frame.locator(self.config.selectors.DISPLAY_COUNT_INPUT).press("Enter")
|
||||||
|
# 检查填充是否成功
|
||||||
|
actual_value = frame.locator(self.config.selectors.DISPLAY_COUNT_INPUT).input_value()
|
||||||
|
if actual_value == expected_value:
|
||||||
|
self._print(f"文本框填充成功: {expected_value}")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
self._print(
|
||||||
|
f"第 {attempt + 1} 次填充失败,实际值: {actual_value},重试..."
|
||||||
|
)
|
||||||
|
if attempt == max_retries - 1:
|
||||||
|
self._print(
|
||||||
|
f"警告: {max_retries} 次尝试后仍未成功填充,继续执行..."
|
||||||
|
)
|
||||||
|
|
||||||
|
def wait_for_page_loaded(self, frame):
|
||||||
|
"""等待页面加载完成"""
|
||||||
|
loading_locator = frame.locator("div").filter(has_text="加载中").nth(1)
|
||||||
|
try:
|
||||||
|
loading_locator.wait_for(
|
||||||
|
state="visible", timeout=self.config.timeouts.LOADING_VISIBLE_WAIT
|
||||||
|
)
|
||||||
|
loading_locator.wait_for(
|
||||||
|
state="hidden", timeout=self.config.timeouts.LOADING_HIDDEN_WAIT
|
||||||
|
)
|
||||||
|
except PlaywrightTimeoutError:
|
||||||
|
# 加载很快完成,或者没有出现加载提示
|
||||||
|
pass
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _wait_for_plan_page_loaded(self, inner_frame):
|
||||||
|
"""等待备料计划页面加载完成(内部方法)"""
|
||||||
|
self._print("等待备料计划页面加载完成...")
|
||||||
|
plan_code_locator = inner_frame.get_by_text(
|
||||||
|
re.compile(r"^离散备料计划维护:")
|
||||||
|
)
|
||||||
|
plan_code_locator.wait_for(
|
||||||
|
state="visible", timeout=self.config.timeouts.PLAN_CODE_VISIBLE
|
||||||
|
)
|
||||||
|
|
||||||
|
# 循环检查编码是否已加载
|
||||||
|
max_wait = self.config.timeouts.PLAN_CODE_MAX_WAIT
|
||||||
|
wait_interval = self.config.timeouts.PLAN_CHECK_INTERVAL
|
||||||
|
waited = 0
|
||||||
|
plan_code = None
|
||||||
|
|
||||||
|
while waited < max_wait:
|
||||||
|
plan_text = plan_code_locator.inner_text()
|
||||||
|
match = re.search(r"离散备料计划维护:(.+)", plan_text)
|
||||||
|
if match and match.group(1).strip():
|
||||||
|
plan_code = match.group(1).strip()
|
||||||
|
break
|
||||||
|
time.sleep(wait_interval)
|
||||||
|
waited += wait_interval
|
||||||
|
|
||||||
|
if plan_code:
|
||||||
|
self._print(f"备料计划页面加载完成,编码: {plan_code}")
|
||||||
|
else:
|
||||||
|
self._print(f"警告: 备料计划页面加载超时")
|
||||||
190
utils/discrete_material_cleaner.py
Normal file
190
utils/discrete_material_cleaner.py
Normal 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()
|
||||||
Reference in New Issue
Block a user