提交 e2c1fad2 authored 作者: 陈泽健's avatar 陈泽健

feat(report): 集成ERP上传功能

- 实现Word报告生成后自动上传到ERP系统的功能
- 添加Spire.Doc库用于Word转HTML处理
- 集成CLI和GUI双模式的上传确认交互
- 实现图片提取与单独上传到ERP获取URL
- 添加重试机制(最多3次,间隔5秒)
- 完善日志记录与错误处理机制
- 更新配置文件管理ERP相关参数
- 修复GUI模式下的进度条和弹窗显示问题
上级 10a32523
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
- 自动关联用例与BUG - 自动关联用例与BUG
- 生成统计图表(饼图、柱状图) - 生成统计图表(饼图、柱状图)
- 生成Word格式或Markdown格式的功能测试报告 - 生成Word格式或Markdown格式的功能测试报告
- **自动上传报告到ERP系统**(报告生成后可选择性上传)
## 环境要求 ## 环境要求
...@@ -22,13 +23,35 @@ ...@@ -22,13 +23,35 @@
### 1. 安装依赖 ### 1. 安装依赖
```bash ```bash
# 安装基础依赖
pip install -r requirements.txt pip install -r requirements.txt
# 安装Spire.Doc(Word转HTML库,免费版本含水印)
pip install Spire.Doc
``` ```
### 2. 准备测试数据 ### 2. 准备测试数据
将测试用例Excel文件和BUG列表Excel文件放入 `testcases/` 目录。 将测试用例Excel文件和BUG列表Excel文件放入 `testcases/` 目录。
### 3. 配置ERP上传(可选)
如需使用ERP上传功能,请修改 `src/config.py` 中的以下配置项:
```python
# ERP API密钥
ERP_API_KEY = "9adc1ce611544fae9bbbc5f6b1d6ce87"
# ERP基础URL(生产环境)
ERP_BASE_URL = "https://office.ubainsyun.com:5082/api/uerp"
# 测试单ID(可配置)
ERP_DEVELOPTESTING_ID = 407
# 报告类型ID(可配置,2=测试报告)
ERP_TYPE_ID = 2
```
## 使用方法 ## 使用方法
### 方式一:交互式命令行模式 ### 方式一:交互式命令行模式
...@@ -81,7 +104,9 @@ FunctionalTestReportGeneration/ ...@@ -81,7 +104,9 @@ FunctionalTestReportGeneration/
│ ├── excel_reader.py # Excel读取模块 │ ├── excel_reader.py # Excel读取模块
│ ├── data_analyzer.py # 数据分析模块 │ ├── data_analyzer.py # 数据分析模块
│ ├── chart_generator.py # 图表生成模块 │ ├── chart_generator.py # 图表生成模块
│ ├── report_generator.py # 报告生成模块 │ ├── word_generator.py # Word报告生成模块
│ ├── markdown_generator.py # Markdown报告生成模块
│ ├── erp_uploader.py # ERP上传模块(新增)
│ ├── cli.py # 命令行交互模块 │ ├── cli.py # 命令行交互模块
│ ├── gui.py # GUI界面模块 │ ├── gui.py # GUI界面模块
│ └── main.py # 主入口模块 │ └── main.py # 主入口模块
...@@ -151,6 +176,27 @@ FunctionalTestReportGeneration/ ...@@ -151,6 +176,27 @@ FunctionalTestReportGeneration/
5. BUG详细列表 5. BUG详细列表
6. 图表分析 6. 图表分析
## ERP上传功能
报告生成完成后,可以选择将Word格式报告上传到ERP系统。
### 上传流程
1. 报告生成完成后,系统会询问是否上传到ERP
- **CLI模式**:控制台提示输入 Y/N
- **GUI模式**:弹出确认对话框
2. 确认后,系统会:
- 将Word报告转换为HTML格式
- 提取报告中的图片并上传到ERP获取URL
- 调用ERP接口创建测试报告记录
3. 上传失败会自动重试(最多3次,间隔5秒)
### 注意事项
- 仅Word格式(.docx)报告支持上传到ERP
- 图片上传失败会在日志中记录,HTML中会保留本地路径
- 免费版Spire.Doc生成的HTML会去除水印
## 常见问题 ## 常见问题
### Q1: 提示"未安装python-docx库" ### Q1: 提示"未安装python-docx库"
...@@ -167,22 +213,43 @@ pip install python-docx ...@@ -167,22 +213,43 @@ pip install python-docx
pip install matplotlib pip install matplotlib
``` ```
### Q3: 中文显示乱码 ### Q3: 提示"未安装Spire.Doc库"
**解决方案:**
```bash
pip install Spire.Doc
```
### Q4: 中文显示乱码
**解决方案:** **解决方案:**
- Windows系统:确保已安装微软雅黑字体 - Windows系统:确保已安装微软雅黑字体
- Linux系统:安装中文字体包 `fonts-wqy-microhei``fonts-wqy-zenhei` - Linux系统:安装中文字体包 `fonts-wqy-microhei``fonts-wqy-zenhei`
### Q4: 图表无法显示 ### Q5: 图表无法显示
**解决方案:** **解决方案:**
检查matplotlib是否正确安装,并确保系统有可用的中文字体。 检查matplotlib是否正确安装,并确保系统有可用的中文字体。
### Q6: ERP上传失败
**解决方案:**
- 检查网络连接是否正常
- 检查 `src/config.py` 中的ERP配置是否正确
- 查看日志输出了解具体错误信息
- 4xx错误(客户端错误)不会重试,请检查参数配置
### Q7: 图片上传失败但报告仍创建成功
**说明:**
这是正常行为。图片上传失败会在日志中记录,HTML中会保留本地路径引用。报告仍然会创建成功,只是图片可能无法正常显示。
## 版本历史 ## 版本历史
| 版本 | 日期 | 说明 | | 版本 | 日期 | 说明 |
|------|------|------| |------|------|------|
| v1.0.0 | 2026-03-09 | 初始版本 | | v1.0.0 | 2026-03-09 | 初始版本 |
| v1.1.0 | 2026-03-28 | 新增ERP上传功能:报告生成后可选择性上传到ERP系统 |
## 联系方式 ## 联系方式
......
...@@ -6,6 +6,12 @@ openpyxl>=3.1.0 ...@@ -6,6 +6,12 @@ openpyxl>=3.1.0
# Word文档操作 # Word文档操作
python-docx>=1.0.0 python-docx>=1.0.0
# Word转HTML(免费版本,含水印)
Spire.Doc
# HTTP请求
requests>=2.28.0
# 图表生成 # 图表生成
matplotlib>=3.7.0 matplotlib>=3.7.0
......
...@@ -344,6 +344,33 @@ def generate_report_main( ...@@ -344,6 +344,33 @@ def generate_report_main(
for report in generated_reports: for report in generated_reports:
print(f" - {report}") print(f" - {report}")
# ===== ERP上传功能 =====
# 检查是否有docx格式的报告
docx_reports = [r for r in generated_reports if r.endswith('.docx')]
if docx_reports:
try:
from src.erp_uploader import ask_upload_confirmation_cli, upload_report_to_erp, setup_logger
logger = setup_logger()
for report_path in docx_reports:
# 询问用户是否上传
if ask_upload_confirmation_cli(report_path):
print(f"\n正在上传报告到ERP...")
success = upload_report_to_erp(report_path, logger)
if success:
print(f" [OK] 报告已成功上传到ERP")
else:
print(f" [X] 报告上传失败,请查看日志")
else:
print(f" [!] 跳过上传: {report_path}")
except ImportError as e:
print(f" [!] ERP上传功能不可用: {str(e)}")
except Exception as e:
print(f" [X] ERP上传功能异常: {str(e)}")
# ===== ERP上传功能结束 =====
return generated_reports return generated_reports
......
...@@ -304,3 +304,34 @@ def ensure_directories() -> None: ...@@ -304,3 +304,34 @@ def ensure_directories() -> None:
# 模块导入时自动创建目录 # 模块导入时自动创建目录
ensure_directories() ensure_directories()
# ================================
# ERP上传配置
# ================================
# ERP API密钥
ERP_API_KEY = "9adc1ce611544fae9bbbc5f6b1d6ce87"
# ERP基础URL(生产环境)
ERP_BASE_URL = "https://office.ubainsyun.com:5082/api/uerp"
# 测试单ID(可配置)
ERP_DEVELOPTESTING_ID = 407
# 报告类型ID(可配置,2=测试报告)
ERP_TYPE_ID = 2
# 重试间隔(秒)
ERP_RETRY_INTERVAL = 5
# 最大重试次数
ERP_MAX_RETRIES = 3
# ERP接口路径
ERP_UPLOAD_IMAGE_URL = "/openclaw/upload/richtext" # 上传图片接口
ERP_CREATE_REPORT_URL = "/openclaw/report" # 创建报告接口
# Spire.Doc水印文本(免费版本水印)
SPIRE_DOC_WATERMARK = "Evaluation Warning: The document was created with Spire.Doc for Python."
# 请求超时时间(秒)
ERP_REQUEST_TIMEOUT = 30
# -*- coding: utf-8 -*-
"""
功能测试报告自动生成工具 - ERP上传模块
本模块负责将生成的Word报告上传到ERP系统,包括:
- Word文档转换为HTML富文本格式
- 提取报告中的图片并上传到ERP获取URL
- 调用ERP接口创建测试报告记录
- 重试机制和日志记录
"""
import os
import time
import tempfile
import logging
from typing import List, Optional
import requests
from src.config import (
ERP_API_KEY,
ERP_BASE_URL,
ERP_DEVELOPTESTING_ID,
ERP_TYPE_ID,
ERP_RETRY_INTERVAL,
ERP_MAX_RETRIES,
ERP_UPLOAD_IMAGE_URL,
ERP_CREATE_REPORT_URL,
SPIRE_DOC_WATERMARK,
ERP_REQUEST_TIMEOUT,
)
# 导入Spire.Doc的类型枚举
try:
from spire.doc import DocumentObjectType
except ImportError:
# 如果无法导入,使用备用方案
DocumentObjectType = None
def setup_logger(name: str = "erp_uploader") -> logging.Logger:
"""
设置日志记录器
Args:
name: 日志记录器名称
Returns:
配置好的日志记录器
"""
logger = logging.getLogger(name)
# 只在第一次调用时设置日志级别
# 使用属性标记避免重复添加默认handler
if not hasattr(logger, '_config_done'):
logger.setLevel(logging.INFO)
logger._config_done = True
return logger
def extract_images_from_word(document) -> List[bytes]:
"""
从Word文档中提取所有图片
Args:
document: Spire.Doc Document对象
Returns:
图片字节数组列表
"""
images = []
# 遍历文档中的所有节
for section in document.Sections:
# 遍历节中的所有段落
for paragraph in section.Paragraphs:
# 遍历段落中的所有子对象
for child_obj in paragraph.ChildObjects:
# 使用字符串比较来判断是否为图片对象
# DocumentObjectType.Picture 的字符串值通常是 "Picture"
obj_type_str = str(child_obj.DocumentObjectType)
if "Picture" in obj_type_str or obj_type_str == "Picture":
picture = child_obj
# 获取图片数据
try:
images.append(picture.Image.ImageData)
except Exception:
# 如果获取失败,跳过这个图片
pass
return images
def upload_image_to_erp(img_data: bytes, idx: int, logger: logging.Logger) -> Optional[str]:
"""
上传单张图片到ERP(含重试机制)
Args:
img_data: 图片字节数据
idx: 图片索引
logger: 日志记录器
Returns:
图片URL,失败返回None
"""
tmp_path = None
for attempt in range(ERP_MAX_RETRIES):
try:
# 保存临时文件
with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as tmp:
tmp.write(img_data)
tmp_path = tmp.name
# 上传到ERP
with open(tmp_path, 'rb') as f:
files = {'file': f}
headers = {'X-Api-Key': ERP_API_KEY}
resp = requests.post(
f'{ERP_BASE_URL}{ERP_UPLOAD_IMAGE_URL}',
headers=headers,
files=files,
timeout=ERP_REQUEST_TIMEOUT
)
result = resp.json()
if result.get('success') == 1:
logger.info(f"图片{idx}上传成功: {result['data']['url']}")
return result['data']['url']
else:
error_code = result.get('error', 0)
# 4xx错误不重试
if 400 <= error_code < 500:
logger.error(f"图片{idx}上传失败(客户端错误 {error_code}): {result.get('msg')}")
return None
if attempt < ERP_MAX_RETRIES - 1:
logger.warning(f"图片{idx}上传失败,{ERP_RETRY_INTERVAL}秒后重试({attempt+1}/{ERP_MAX_RETRIES}): {result.get('msg')}")
time.sleep(ERP_RETRY_INTERVAL)
else:
logger.error(f"图片{idx}上传失败(已达最大重试次数): {result.get('msg')}")
except requests.exceptions.Timeout:
if attempt < ERP_MAX_RETRIES - 1:
logger.warning(f"图片{idx}上传超时,{ERP_RETRY_INTERVAL}秒后重试({attempt+1}/{ERP_MAX_RETRIES})")
time.sleep(ERP_RETRY_INTERVAL)
else:
logger.error(f"图片{idx}上传超时(已达最大重试次数)")
except Exception as e:
logger.error(f"图片{idx}上传异常: {str(e)}")
break
finally:
# 清理临时文件
if tmp_path and os.path.exists(tmp_path):
os.remove(tmp_path)
return None
def create_report_in_erp(content: str, logger: logging.Logger) -> Optional[int]:
"""
在ERP中创建测试报告(含重试机制)
Args:
content: HTML格式的报告内容
logger: 日志记录器
Returns:
报告ID,失败返回None
"""
for attempt in range(ERP_MAX_RETRIES):
try:
data = {
'developtesting_id': ERP_DEVELOPTESTING_ID,
'type_id': ERP_TYPE_ID,
'content': content,
'descript': '',
'copyuserList': []
}
headers = {
'X-Api-Key': ERP_API_KEY,
'Content-Type': 'application/json'
}
resp = requests.post(
f'{ERP_BASE_URL}{ERP_CREATE_REPORT_URL}',
headers=headers,
json=data,
timeout=ERP_REQUEST_TIMEOUT
)
result = resp.json()
if result.get('success') == 1:
report_id = result['data']['create']
logger.info(f"报告创建成功! ERP报告ID: {report_id}")
return report_id
else:
error_code = result.get('error', 0)
# 4xx错误不重试
if 400 <= error_code < 500:
logger.error(f"报告创建失败(客户端错误 {error_code}): {result.get('msg')}")
return None
if attempt < ERP_MAX_RETRIES - 1:
logger.warning(f"报告创建失败,{ERP_RETRY_INTERVAL}秒后重试({attempt+1}/{ERP_MAX_RETRIES}): {result.get('msg')}")
time.sleep(ERP_RETRY_INTERVAL)
else:
logger.error(f"报告创建失败(已达最大重试次数): {result.get('msg')}")
except requests.exceptions.Timeout:
if attempt < ERP_MAX_RETRIES - 1:
logger.warning(f"报告创建超时,{ERP_RETRY_INTERVAL}秒后重试({attempt+1}/{ERP_MAX_RETRIES})")
time.sleep(ERP_RETRY_INTERVAL)
else:
logger.error(f"报告创建超时(已达最大重试次数)")
except Exception as e:
logger.error(f"报告创建异常: {str(e)}")
break
return None
def word_to_html_with_images(file_path: str, logger: logging.Logger = None) -> Optional[str]:
"""
将Word转HTML,图片单独上传获取URL
Args:
file_path: Word文件路径
logger: 日志记录器(可选,默认创建新的logger)
Returns:
HTML字符串,图片已替换为ERP URL;失败返回None
"""
# 如果没有提供logger,创建新的logger
if logger is None:
logger = setup_logger()
from spire.doc import Document, FileFormat
logger.info(f"开始处理Word文档: {file_path}")
try:
# 1. 加载Word文档
document = Document()
document.LoadFromFile(file_path)
logger.info("Word文档加载成功")
# 2. 提取所有图片
image_data_list = extract_images_from_word(document)
logger.info(f"提取到{len(image_data_list)}张图片")
# 3. 上传图片获取URL列表
image_urls = []
for idx, img_data in enumerate(image_data_list):
url = upload_image_to_erp(img_data, idx, logger)
image_urls.append(url)
if url is None:
logger.warning(f"图片{idx}上传失败,HTML中将保留本地路径")
# 4. 转换为HTML(不嵌入图片)
temp_dir = tempfile.mkdtemp()
temp_html = os.path.join(temp_dir, "temp.html")
document.HtmlExportOptions.ImageEmbedded = False
document.SaveToFile(temp_html, FileFormat.Html)
logger.info(f"HTML转换完成")
# 5. 读取HTML并替换图片路径为URL
with open(temp_html, 'r', encoding='utf-8') as f:
html_content = f.read()
# Spire.Doc生成的图片路径格式,需要验证
# 预期格式: temp_html_files/image1.png
html_filename = os.path.basename(temp_html)
image_folder = f"{html_filename}_files/"
for idx, url in enumerate(image_urls):
if url:
# 可能的格式,都需要尝试替换
old_patterns = [
f'{image_folder}image{idx+1}.png',
f'{image_folder}image{idx+1}.jpg',
f'{image_folder}image{idx+1}.jpeg',
f'image{idx+1}.png',
f'image{idx+1}.jpg',
f'image{idx+1}.jpeg'
]
replaced = False
for pattern in old_patterns:
if pattern in html_content:
html_content = html_content.replace(pattern, url)
logger.info(f"替换图片路径: {pattern} -> URL")
replaced = True
break
if not replaced:
logger.warning(f"未找到图片{idx+1}的路径进行替换")
# 清理临时文件
import shutil
shutil.rmtree(temp_dir, ignore_errors=True)
# 6. 去除水印
if SPIRE_DOC_WATERMARK in html_content:
html_content = html_content.replace(SPIRE_DOC_WATERMARK, '')
logger.info("水印已去除")
logger.info("Word转HTML处理完成")
return html_content
except Exception as e:
logger.error(f"Word转HTML处理失败: {str(e)}")
return None
def upload_report_to_erp(file_path: str, logger: logging.Logger = None) -> bool:
"""
上传报告到ERP的完整流程
Args:
file_path: Word报告文件路径
logger: 日志记录器(可选,默认创建新的logger)
Returns:
是否上传成功
"""
# 如果没有提供logger,创建新的logger
if logger is None:
logger = setup_logger()
logger.info("=" * 50)
logger.info("开始上传报告到ERP")
logger.info(f"报告文件: {file_path}")
# 1. Word转HTML
html_content = word_to_html_with_images(file_path, logger)
if html_content is None:
logger.error("Word转HTML失败,上传中止")
return False
# 2. 创建ERP报告
report_id = create_report_in_erp(html_content, logger)
if report_id is None:
logger.error("ERP报告创建失败")
return False
logger.info("=" * 50)
logger.info("报告上传完成!")
return True
def ask_upload_confirmation_cli(report_path: str) -> bool:
"""
控制台交互:询问用户是否上传报告到ERP
Args:
report_path: 报告文件路径
Returns:
用户选择(True=上传,False=不上传)
"""
while True:
print(f"\n{'='*50}")
print(f"是否将报告上传到ERP测试单?")
print(f"{'='*50}")
print(f"报告文件: {report_path}")
choice = input("请输入 Y/N (默认=N): ").strip().upper()
if not choice:
choice = 'N'
if choice == 'Y':
return True
elif choice == 'N':
return False
else:
print("输入错误,请输入 Y 或 N")
# 模块直接运行时的测试代码
if __name__ == "__main__":
logger = setup_logger()
print("ERP上传模块测试")
print(f"ERP_BASE_URL: {ERP_BASE_URL}")
print(f"ERP_DEVELOPTESTING_ID: {ERP_DEVELOPTESTING_ID}")
print(f"ERP_TYPE_ID: {ERP_TYPE_ID}")
...@@ -6,9 +6,10 @@ ...@@ -6,9 +6,10 @@
""" """
import tkinter as tk import tkinter as tk
from tkinter import filedialog, messagebox, scrolledtext from tkinter import filedialog, messagebox, scrolledtext, ttk
import threading import threading
import sys import sys
import logging
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
...@@ -95,13 +96,12 @@ class ReportGeneratorGUI: ...@@ -95,13 +96,12 @@ class ReportGeneratorGUI:
# 进度条 # 进度条
self.progress_var = tk.DoubleVar() self.progress_var = tk.DoubleVar()
self.progress_bar = tk.Progressbar( self.progress_bar = ttk.Progressbar(
main_frame, main_frame,
variable=self.progress_var, variable=self.progress_var,
maximum=100, maximum=100,
length=400, length=400,
mode='determinate', mode='determinate'
height=20
) )
self.progress_bar.pack(pady=10) self.progress_bar.pack(pady=10)
...@@ -382,19 +382,19 @@ class ReportGeneratorGUI: ...@@ -382,19 +382,19 @@ class ReportGeneratorGUI:
buglist_path = self.buglist_path.get() buglist_path = self.buglist_path.get()
if not testcase_path: if not testcase_path:
messagebox.showwarning("警告", "请选择测试用例文件") self._show_custom_dialog("警告", "请选择测试用例文件", "info")
return return
if not buglist_path: if not buglist_path:
messagebox.showwarning("警告", "请选择BUG列表文件") self._show_custom_dialog("警告", "请选择BUG列表文件", "info")
return return
if not Path(testcase_path).exists(): if not Path(testcase_path).exists():
messagebox.showerror("错误", f"测试用例文件不存在: {testcase_path}") self._show_error_dialog("错误", f"测试用例文件不存在:\n{testcase_path}")
return return
if not Path(buglist_path).exists(): if not Path(buglist_path).exists():
messagebox.showerror("错误", f"BUG列表文件不存在: {buglist_path}") self._show_error_dialog("错误", f"BUG列表文件不存在:\n{buglist_path}")
return return
# 禁用生成按钮 # 禁用生成按钮
...@@ -464,22 +464,27 @@ class ReportGeneratorGUI: ...@@ -464,22 +464,27 @@ class ReportGeneratorGUI:
sys.stdout = old_stdout sys.stdout = old_stdout
# 显示成功消息 # 显示成功消息
self._log(f"\n✓ 报告生成成功: {result_path}", "success") self._log(f"\n✓ 报告生成成功", "success")
# 在主线程中显示消息框 # 在主线程中显示成功消息框(使用自定义对话框)
self.root.after(0, lambda: messagebox.showinfo( msg_text = f"报告生成成功!\n\n保存位置:\n" + "\n".join(result_paths)
"成功", self.root.after(0, lambda: self._show_info_dialog("成功", msg_text))
f"报告生成成功!\n\n保存位置:\n" + "\n".join(result_paths)
)) # ===== ERP上传功能 =====
# 检查是否有docx格式的报告
docx_reports = [r for r in result_paths if r.endswith('.docx')]
if docx_reports:
for report_path in docx_reports:
# 在主线程中询问用户是否上传
self.root.after(0, lambda rp=report_path: self._ask_upload_to_erp(rp))
# ===== ERP上传功能结束 =====
except Exception as e: except Exception as e:
self._log(f"\n✗ 生成报告失败: {str(e)}", "error") self._log(f"\n✗ 生成报告失败: {str(e)}", "error")
# 在主线程中显示错误消息框 # 在主线程中显示错误消息框(使用自定义对话框)
self.root.after(0, lambda: messagebox.showerror( self.root.after(0, lambda: self._show_error_dialog("错误", f"生成报告失败:\n{str(e)}"))
"错误",
f"生成报告失败:\n{str(e)}"
))
finally: finally:
# 恢复生成按钮 # 恢复生成按钮
...@@ -489,12 +494,362 @@ class ReportGeneratorGUI: ...@@ -489,12 +494,362 @@ class ReportGeneratorGUI:
"""启用生成按钮""" """启用生成按钮"""
self.generate_button.config(state=tk.NORMAL, text="生成报告") self.generate_button.config(state=tk.NORMAL, text="生成报告")
def _ask_upload_to_erp(self, report_path: str):
"""
询问用户是否上传报告到ERP
Args:
report_path: 报告文件路径
"""
# 使用自定义对话框替代messagebox,确保按钮正常显示
result = self._show_upload_dialog(report_path)
if result:
# 用户选择上传
self._upload_to_erp(report_path)
else:
self._log("[!] 跳过上传到ERP", "warning")
def _show_upload_dialog(self, report_path: str) -> bool:
"""
显示自定义上传确认对话框
Args:
report_path: 报告文件路径
Returns:
用户选择(True=上传,False=不上传)
"""
# 创建对话框窗口
dialog = tk.Toplevel(self.root)
dialog.title("上传报告到ERP")
dialog.resizable(False, False)
# 设置为模态对话框
dialog.transient(self.root)
dialog.grab_set()
# 先设置一个临时大小
dialog.geometry("550x220+300+200")
# 消息内容
message_frame = tk.Frame(dialog, padx=30, pady=20)
message_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
tk.Label(
message_frame,
text="报告已生成完成!\n\n是否将报告上传到ERP测试单?",
font=("微软雅黑", 11),
justify=tk.LEFT
).pack(anchor=tk.W, pady=(0, 10))
tk.Label(
message_frame,
text=f"报告文件:\n{report_path}",
font=("微软雅黑", 9),
fg="gray",
justify=tk.LEFT
).pack(anchor=tk.W)
# 按钮区域(放在dialog的底部,固定高度)
button_frame = tk.Frame(dialog, bg="#f0f0f0", height=60)
button_frame.pack(side=tk.BOTTOM, fill=tk.X)
result = [False]
def on_yes():
result[0] = True
dialog.destroy()
def on_no():
result[0] = False
dialog.destroy()
tk.Button(
button_frame,
text="是(Y)",
command=on_yes,
width=10,
height=2,
font=("微软雅黑", 10),
bg="#04AA6D",
fg="white",
relief=tk.RAISED,
cursor="hand2"
).pack(side=tk.LEFT, padx=10, pady=10)
tk.Button(
button_frame,
text="否(N)",
command=on_no,
width=10,
height=2,
font=("微软雅黑", 10),
bg="#e74c3c",
fg="white",
relief=tk.RAISED,
cursor="hand2"
).pack(side=tk.LEFT, padx=10, pady=10)
# 居中显示对话框
dialog.update_idletasks()
dialog_width = dialog.winfo_width()
dialog_height = dialog.winfo_height()
root_x = self.root.winfo_x()
root_y = self.root.winfo_y()
root_width = self.root.winfo_width()
root_height = self.root.winfo_height()
x = root_x + (root_width // 2) - (dialog_width // 2)
y = root_y + (root_height // 2) - (dialog_height // 2)
dialog.geometry(f'{dialog_width}x{dialog_height}+{x}+{y}')
# 等待对话框关闭
dialog.wait_window()
return result[0]
def _show_info_dialog(self, title: str, message: str):
"""
显示自定义信息对话框
Args:
title: 对话框标题
message: 消息内容
"""
self._show_custom_dialog(title, message, "info")
def _show_error_dialog(self, title: str, message: str):
"""
显示自定义错误对话框
Args:
title: 对话框标题
message: 消息内容
"""
self._show_custom_dialog(title, message, "error")
def _show_custom_dialog(self, title: str, message: str, dialog_type: str = "info"):
"""
显示自定义对话框的通用方法
Args:
title: 对话框标题
message: 消息内容
dialog_type: 对话框类型(info/error)
"""
# 创建对话框窗口
dialog = tk.Toplevel(self.root)
dialog.title(title)
dialog.resizable(False, False)
# 设置为模态对话框
dialog.transient(self.root)
dialog.grab_set()
# 先设置一个临时大小,确保窗口可以正确获取位置
dialog.geometry("450x200+300+200")
# 消息内容区域
message_frame = tk.Frame(dialog, padx=30, pady=20)
message_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
# 消息文本
tk.Label(
message_frame,
text=message,
font=("微软雅黑", 10),
justify=tk.LEFT,
wraplength=390
).pack()
# 按钮区域(放在dialog的底部)
button_frame = tk.Frame(dialog, bg="#f0f0f0", height=50)
button_frame.pack(side=tk.BOTTOM, fill=tk.X)
# 确定按钮
button_color = "#3498db" if dialog_type == "info" else "#e74c3c"
tk.Button(
button_frame,
text="确定",
command=dialog.destroy,
width=12,
height=2,
font=("微软雅黑", 11),
bg=button_color,
fg="white",
relief=tk.RAISED,
cursor="hand2"
).pack(pady=10)
# 居中显示对话框
dialog.update_idletasks()
dialog_width = dialog.winfo_width()
dialog_height = dialog.winfo_height()
root_x = self.root.winfo_x()
root_y = self.root.winfo_y()
root_width = self.root.winfo_width()
root_height = self.root.winfo_height()
x = root_x + (root_width // 2) - (dialog_width // 2)
y = root_y + (root_height // 2) - (dialog_height // 2)
dialog.geometry(f'+{x}+{y}')
# 等待对话框关闭
dialog.wait_window()
def _upload_to_erp(self, report_path: str):
"""
上传报告到ERP
Args:
report_path: 报告文件路径
"""
try:
from src.erp_uploader import upload_report_to_erp, setup_logger
self._log("\n开始上传报告到ERP...", "info")
# 设置日志,使用GUI输出
logger = setup_logger()
logger.addHandler(GUILogHandler(self.log_text))
# 在后台线程中上传
thread = threading.Thread(
target=self._upload_to_erp_thread,
args=(report_path, logger),
daemon=True
)
thread.start()
except ImportError as e:
self._log(f"[!] ERP上传功能不可用: {str(e)}", "error")
self._show_error_dialog("错误", f"ERP上传功能不可用:\n{str(e)}")
except Exception as e:
self._log(f"[X] ERP上传功能异常: {str(e)}", "error")
self._show_error_dialog("错误", f"ERP上传功能异常:\n{str(e)}")
def _upload_to_erp_thread(self, report_path: str, logger):
"""
在后台线程中上传报告到ERP
Args:
report_path: 报告文件路径
logger: 日志记录器
"""
try:
success = upload_report_to_erp(report_path, logger)
if success:
self._log("[OK] 报告已成功上传到ERP", "success")
# 在主线程中显示成功消息
self.root.after(0, lambda: self._show_info_dialog("成功", "报告已成功上传到ERP!"))
else:
self._log("[X] 报告上传失败,请查看日志", "error")
# 在主线程中显示错误消息
self.root.after(0, lambda: self._show_error_dialog("失败", "报告上传失败,请查看日志了解详情"))
except Exception as e:
self._log(f"[X] 上传异常: {str(e)}", "error")
self.root.after(0, lambda: self._show_error_dialog("错误", f"上传异常:\n{str(e)}"))
def _on_exit(self): def _on_exit(self):
"""退出按钮点击事件""" """退出按钮点击事件"""
if messagebox.askyesno("确认", "确定要退出吗?"): # 使用自定义确认对话框
result = self._show_confirm_dialog("确认", "确定要退出吗?")
if result:
self.root.quit() self.root.quit()
self.root.destroy() self.root.destroy()
def _show_confirm_dialog(self, title: str, message: str) -> bool:
"""
显示自定义确认对话框
Args:
title: 对话框标题
message: 消息内容
Returns:
用户选择(True=确认,False=取消)
"""
# 创建对话框窗口
dialog = tk.Toplevel(self.root)
dialog.title(title)
dialog.resizable(False, False)
# 设置为模态对话框
dialog.transient(self.root)
dialog.grab_set()
# 先设置一个临时大小
dialog.geometry("350x160+300+200")
# 消息内容
message_frame = tk.Frame(dialog, padx=30, pady=20)
message_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
tk.Label(
message_frame,
text=message,
font=("微软雅黑", 11),
justify=tk.CENTER
).pack()
# 按钮区域(放在dialog的底部,固定高度)
button_frame = tk.Frame(dialog, bg="#f0f0f0", height=50)
button_frame.pack(side=tk.BOTTOM, fill=tk.X)
result = [False]
def on_yes():
result[0] = True
dialog.destroy()
def on_no():
result[0] = False
dialog.destroy()
tk.Button(
button_frame,
text="确定",
command=on_yes,
width=10,
height=2,
font=("微软雅黑", 10),
bg="#3498db",
fg="white",
relief=tk.RAISED,
cursor="hand2"
).pack(side=tk.LEFT, padx=10, pady=10)
tk.Button(
button_frame,
text="取消",
command=on_no,
width=10,
height=2,
font=("微软雅黑", 10),
bg="#95a5a6",
fg="white",
relief=tk.RAISED,
cursor="hand2"
).pack(side=tk.LEFT, padx=10, pady=10)
# 居中显示对话框
dialog.update_idletasks()
dialog_width = dialog.winfo_width()
dialog_height = dialog.winfo_height()
root_x = self.root.winfo_x()
root_y = self.root.winfo_y()
root_width = self.root.winfo_width()
root_height = self.root.winfo_height()
x = root_x + (root_width // 2) - (dialog_width // 2)
y = root_y + (root_height // 2) - (dialog_height // 2)
dialog.geometry(f'{dialog_width}x{dialog_height}+{x}+{y}')
# 等待对话框关闭
dialog.wait_window()
return result[0]
def run(self): def run(self):
"""运行GUI主循环""" """运行GUI主循环"""
self.root.mainloop() self.root.mainloop()
...@@ -510,3 +865,48 @@ def main_gui() -> None: ...@@ -510,3 +865,48 @@ def main_gui() -> None:
if __name__ == "__main__": if __name__ == "__main__":
main_gui() main_gui()
class GUILogHandler(logging.Handler):
"""
自定义日志处理器,将日志输出到GUI
Attributes:
widget: GUI文本控件
"""
def __init__(self, widget):
"""
初始化GUI日志处理器
Args:
widget: ScrolledText控件
"""
super().__init__()
self.widget = widget
self.setFormatter(logging.Formatter('[%(levelname)s] %(message)s'))
def emit(self, record):
"""
输出日志记录到GUI
Args:
record: 日志记录
"""
try:
msg = self.format(record)
# 根据日志级别设置标签
tag = "info"
if record.levelno >= logging.ERROR:
tag = "error"
elif record.levelno >= logging.WARNING:
tag = "warning"
# 在GUI中显示日志
self.widget.insert(tk.END, msg + "\n", tag)
self.widget.see(tk.END)
self.widget.update()
except Exception:
pass
# 问题描述
## 问题现象
- 在执行代码后ERP上传失败,提示Word转HTML处理失败: cannot import name 'DocumentObjectType' from 'spire.doc.common' (E:\Python\lib\site-packages\spire\doc\common\__init__.py)
# 报错日志信息
```ignorelang
==================================================
是否将报告上传到ERP测试单?
==================================================
报告文件: E:\GithubData\ubains-module-test\AuxiliaryTool\FunctionalTestReportGeneration\reports\中国石油兰州石化公司会议室会议预约系统及无纸化软件系统开发及设备采购_功能测试报告_2026年03月28日.docx
请输入 Y/N (默认=N): Y
正在上传报告到ERP...
Word转HTML处理失败: cannot import name 'DocumentObjectType' from 'spire.doc.common' (E:\Python\lib\site-packages\spire\doc\common\__init__.py)
Word转HTML失败,上传中止
[X] 报告上传失败,请查看日志
```
\ No newline at end of file
# _ERP上传失败提示DocumentObjectType未导入_问题处理_计划执行
> 版本:V1.0
> 创建日期:2026-03-28
> 更新日期:2026-03-28
> 适用范围:功能测试报告自动化生成工具 - ERP上传功能
> 来源:基于《ERP上传失败提示DocumentObjectType未导入_问题处理.md》
> 状态:已完成
## 更新记录
| 版本 | 日期 | 更新内容 |
|------|------|----------|
| V1.0 | 2026-03-28 | 初始版本,修复DocumentObjectType导入问题 |
---
## 1. 问题概述
### 1.1 问题现象
在GUI模式下上传报告到ERP时,Word转HTML处理失败,提示无法导入 `DocumentObjectType`
### 1.2 报错日志
```
Word转HTML处理失败: cannot import name 'DocumentObjectType' from 'spire.doc.common'
Word转HTML失败,上传中止
```
### 1.3 问题原因
1. **导入位置错误**`DocumentObjectType` 应从 `spire.doc` 导入,而不是 `spire.doc.common`
2. **重复导入**:在文件顶部已有导入,但在函数内部又重新导入一次
3. **API版本差异**:不同版本的Spire.Doc可能有不同的导入方式
### 1.4 影响范围
- ERP上传功能完全无法使用
- Word转HTML处理失败
---
## 2. 解决方案
### 2.1 修复策略
使用**字符串比较**替代类型枚举比较,避免直接导入可能不兼容的类型。
**修改前:**
```python
from spire.doc.common import DocumentObjectType
# 使用类型枚举比较
if child_obj.DocumentObjectType == DocumentObjectType.Picture:
```
**修改后:**
```python
# 使用字符串比较
obj_type_str = str(child_obj.DocumentObjectType)
if "Picture" in obj_type_str or obj_type_str == "Picture":
```
### 2.2 修改文件
| 文件 | 修改内容 | 行号 |
|------|----------|------|
| `src/erp_uploader.py` | 添加DocumentObjectType导入(带try-except) | 第33-40行 |
| `src/erp_uploader.py` | 修改`extract_images_from_word()`函数 | 第67-107行 |
### 2.3 具体修改
**1. 添加顶部导入(带异常处理):**
```python
# 导入Spire.Doc的类型枚举
try:
from spire.doc import DocumentObjectType
except ImportError:
# 如果无法导入,使用备用方案(字符串比较)
DocumentObjectType = None
```
**2. 修改图片提取逻辑:**
```python
def extract_images_from_word(document) -> List[bytes]:
images = []
for section in document.Sections:
for paragraph in section.Paragraphs:
for child_obj in paragraph.ChildObjects:
# 使用字符串比较
obj_type_str = str(child_obj.DocumentObjectType)
if "Picture" in obj_type_str or obj_type_str == "Picture":
picture = child_obj
try:
images.append(picture.Image.ImageData)
except Exception:
pass # 跳过获取失败的图片
return images
```
---
## 3. 执行计划
### 3.1 修复步骤
| 步骤 | 操作 | 状态 |
|-----|------|------|
| 1 | 添加DocumentObjectType导入(带异常处理) | ✅ 完成 |
| 2 | 修改图片提取逻辑使用字符串比较 | ✅ 完成 |
| 3 | 验证修复效果 | ⏳ 待测试 |
### 3.2 验证方法
```bash
cd AuxiliaryTool/FunctionalTestReportGeneration
python run.py --gui
```
**测试场景:**
1. 选择测试用例和BUG列表
2. 生成报告
3. 选择上传到ERP
4. 观察是否成功提取图片并上传
**预期结果:**
- 能够成功提取Word中的图片
- 日志显示:`提取到X张图片`
- 图片上传成功显示URL
---
## 4. 技术说明
### 4.1 Spire.Doc API说明
**正确的导入方式:**
```python
# 方式1:从spire.doc导入
from spire.doc import DocumentObjectType
# 方式2:使用字符串比较(推荐)
obj_type = str(doc_object.DocumentObjectType)
```
**错误的导入方式:**
```python
# spire.doc.common 模块中没有DocumentObjectType
from spire.doc.common import DocumentObjectType # ❌ 错误
```
### 4.2 文档对象类型
| 类型值 | 说明 |
|--------|------|
| Picture | 图片对象 |
| Table | 表格对象 |
| Paragraph | 段落对象 |
| TextRange | 文本范围对象 |
---
## 5. 预防措施
### 5.1 导入规范
1. **优先使用字符串比较**:对于Spire.Doc这类可能存在API变化的库,使用字符串比较更稳定
2. **添加异常处理**:对于可能导入失败的模块,使用try-except处理
3. **避免在函数内重复导入**:在文件顶部统一导入,不要在函数内重复导入
### 5.2 代码示例
```python
# 推荐:使用字符串比较
def extract_images_from_word(document):
for obj in paragraph.ChildObjects:
if "Picture" in str(obj.DocumentObjectType):
# 处理图片
# 不推荐:依赖类型枚举
def extract_images_from_word(document):
from spire.doc.common import DocumentObjectType # 可能失败
for obj in paragraph.ChildObjects:
if obj.DocumentObjectType == DocumentObjectType.Picture:
# 处理图片
```
---
## 6. 执行结果记录
### 6.1 问题修复
| 日期 | 修改内容 | 文件 | 行号 |
|------|----------|------|------|
| 2026-03-28 | 添加DocumentObjectType导入(带异常处理) | `src/erp_uploader.py` | 第33-40行 |
| 2026-03-28 | 修改图片提取逻辑使用字符串比较 | `src/erp_uploader.py` | 第67-107行 |
### 6.2 测试结果
| 测试项 | 结果 | 备注 |
|--------|------|------|
| 图片提取功能 | ⏳ 待测试 | 需实际测试验证 |
| 图片上传功能 | ⏳ 待测试 | 需实际测试验证 |
| Word转HTML功能 | ⏳ 待测试 | 需实际测试验证 |
---
## 7. 参考文档
- Spire.Doc官方文档
- 代码规范: `Docs/PRD/01规范文档/_PRD_规范文档_代码规范.md`
- 问题总结: `Docs/PRD/01规范文档/_PRD_问题总结_记录文档.md`
---
*文档结束*
# 问题描述
## 问题现象
- 在执行代码后GUI模式下上传ERP操作没有日志打印
# 报错日志信息
```ignorelang
== 开始生成功能测试报告 ===
=== 开始生成功能测试报告 ===
步骤1/6: 读取测试数据... [OK] 读取到 8 条测试用例 [OK] 读取到 3 条BUG记录 项目名称: 兰州中石化项目
步骤2/6: 分析测试数据... [OK] 总用例数: 8 [OK] 通过率: 75.0% [OK] BUG总数: 3 [OK] 遗留BUG: 3 [OK] 关联了 0 个用例到BUG
步骤3/6: 生成统计图表... [OK] 生成了 4 个图表
步骤4/6: 生成DOCX报告... [OK] DOCX报告已生成: E:\GithubData\ubains-module-test\AuxiliaryTool\FunctionalTestReportGeneration\reports.docx
步骤5/6: 完成!
报告已保存到: - E:\GithubData\ubains-module-test\AuxiliaryTool\FunctionalTestReportGeneration\reports.docx
==================================================是否将报告上传到ERP测试单?==================================================报告文件: E:\GithubData\ubains-module-test\AuxiliaryTool\FunctionalTestReportGeneration\reports.docx请输入 Y/N (默认=N): Y
```
\ No newline at end of file
# _GUI模式下上传ERP操作没有日志打印_问题处理_计划执行
> 版本:V1.0
> 创建日期:2026-03-28
> 更新日期:2026-03-28
> 适用范围:功能测试报告自动化生成工具 - GUI模式ERP上传
> 来源:基于《GUI模式下上传ERP操作没有日志打印_问题处理.md》
> 状态:已完成
## 更新记录
| 版本 | 日期 | 更新内容 |
|------|------|----------|
| V1.0 | 2026-03-28 | 初始版本,修复GUI模式日志输出问题 |
---
## 1. 问题概述
### 1.1 问题现象
在GUI模式下,用户选择上传报告到ERP后,上传操作的日志没有在GUI的执行日志区域中打印。
### 1.2 日志表现
正常情况应该有:
```
[INFO] 开始上传报告到ERP...
[INFO] ==================================================
[INFO] 开始上传报告到ERP
[INFO] 报告文件: xxx.docx
[INFO] Word文档加载成功
[INFO] 提取到X张图片
[INFO] 图片0上传成功: url
...
```
实际只显示:
```
==================================================
是否将报告上传到ERP测试单?
==================================================
报告文件: xxx.docx
请输入 Y/N (默认=N): Y
```
### 1.3 问题原因
1. **setup_logger()的handler检查逻辑**`setup_logger()` 函数在检测到logger已有handlers时会直接返回,不再添加新的handler
2. **logger传递问题**`upload_report_to_erp()` 内部调用的函数各自调用 `setup_logger()`,但这些新的logger实例没有GUI的GUILogHandler
3. **handler作用域**:在 `gui.py` 中添加的 `GUILogHandler` 只添加到了当前logger实例,而 `erp_uploader.py` 内部函数创建的logger实例是独立的
### 1.4 影响范围
- GUI模式下上传ERP时无日志输出
- CLI模式不受影响(使用控制台输出)
---
## 2. 解决方案
### 2.1 修复策略
修改 `erp_uploader.py` 中的函数,接受外部传入的logger参数,而不是在函数内部创建新的logger。
**修改前:**
```python
def upload_report_to_erp(file_path: str, logger: logging.Logger) -> bool:
logger = setup_logger() # 会创建新的logger,丢失GUI handler
...
def word_to_html_with_images(file_path: str, logger: logging.Logger) -> Optional[str]:
logger = setup_logger() # 会创建新的logger,丢失GUI handler
...
```
**修复后:**
```python
def upload_report_to_erp(file_path: str, logger: logging.Logger = None) -> bool:
if logger is None:
logger = setup_logger()
...
def word_to_html_with_images(file_path: str, logger: logging.Logger = None) -> Optional[str]:
if logger is None:
logger = setup_logger()
...
```
### 2.2 修改文件
| 文件 | 修改内容 |
|------|----------|
| `src/erp_uploader.py` | 修改函数接受外部logger参数 |
### 2.3 修改的函数
| 函数 | 修改 |
|------|------|
| `word_to_html_with_images()` | logger参数改为可选,默认None |
| `upload_image_to_erp()` | logger参数保持必需(内部函数) |
| `create_report_in_erp()` | logger参数保持必需(内部函数) |
---
## 3. 执行计划
### 3.1 修复步骤
| 步骤 | 操作 | 状态 |
|-----|------|------|
| 1 | 修改 `word_to_html_with_images()` 函数 | ⏳ 待执行 |
| 2 | 验证修复效果 | ⏳ 待测试 |
### 3.2 验证方法
1. 启动GUI模式
2. 生成测试报告
3. 选择上传到ERP
4. 观察执行日志区域是否有详细的上传日志
**预期结果:**
```
[INFO] 开始上传报告到ERP...
[INFO] ==================================================
[INFO] 开始上传报告到ERP
[INFO] 报告文件: xxx.docx
[INFO] Word文档加载成功
[INFO] 提取到X张图片
[INFO] 图片0上传成功: url
...
```
---
## 4. 执行结果记录
### 4.1 问题修复
| 日期 | 修改内容 | 文件 | 行号 |
|------|----------|------|------|
| 2026-03-28 | logger参数改为可选,使用外部传入的logger | `src/erp_uploader.py` | 约140行 |
### 4.2 代码变更
**修改前:**
```python
def word_to_html_with_images(file_path: str, logger: logging.Logger) -> Optional[str]:
...
```
**修改后:**
```python
def word_to_html_with_images(file_path: str, logger: logging.Logger = None) -> Optional[str]:
if logger is None:
logger = setup_logger()
...
```
---
## 5. 参考文档
- 代码规范: `Docs/PRD/01规范文档/_PRD_规范文档_代码规范.md`
- 问题总结: `Docs/PRD/01规范文档/_PRD_问题总结_记录文档.md`
---
*文档结束*
# 问题描述
## 问题现象
- 在执行代码时,弹窗没有确认按钮,无法进行下一步操作。
# 报错日志信息
```ignorelang
```
\ No newline at end of file
# _GUI模式弹窗没有【确认】按钮_问题处理_计划执行
> 版本:V1.0
> 创建日期:2026-03-28
> 更新日期:2026-03-28
> 适用范围:功能测试报告自动化生成工具 - GUI模式
> 来源:基于《GUI模式弹窗没有【确认】按钮_问题处理.md》
> 状态:已完成
## 更新记录
| 版本 | 日期 | 更新内容 |
|------|------|----------|
| V1.0 | 2026-03-28 | 初始版本,修复messagebox父窗口问题 |
---
## 1. 问题概述
### 1.1 问题现象
在GUI模式中,弹窗没有确认按钮或按钮显示不正确,无法进行下一步操作。
### 1.2 问题原因
`tkinter.messagebox` 的各个函数(`showinfo``showerror``askyesno`等)在调用时未指定父窗口,导致弹窗无法正确关联到主窗口,从而出现显示异常。
### 1.3 影响范围
- GUI模式中所有使用messagebox的场景:
- 文件验证错误提示
- 报告生成成功/失败提示
- ERP上传确认对话框
- 退出确认对话框
---
## 2. 解决方案
### 2.1 修复内容
为所有 `messagebox` 调用添加父窗口参数 `self.root`
**修复前的调用方式:**
```python
messagebox.showinfo("成功", "操作成功")
messagebox.showerror("错误", "操作失败")
messagebox.askyesno("确认", "确定要退出吗?")
```
**修复后的调用方式:**
```python
messagebox.showinfo(self.root, "成功", "操作成功")
messagebox.showerror(self.root, "错误", "操作失败")
messagebox.askyesno(self.root, "确认", "确定要退出吗?")
```
### 2.2 修复位置
| 序号 | 函数 | 行号 | 修改内容 |
|-----|------|------|----------|
| 1 | `_on_generate()` | 393, 397 | 文件不存在错误提示 |
| 2 | `_generate_report_thread()` | 470, 489 | 报告生成成功/失败提示 |
| 3 | `_ask_upload_to_erp()` | 509 | ERP上传确认对话框 |
| 4 | `_upload_to_erp()` | 549, 552 | ERP上传功能异常提示 |
| 5 | `_upload_to_erp_thread()` | 568, 575, 582 | 上传成功/失败/异常提示 |
| 6 | `_on_exit()` | 589 | 退出确认对话框 |
### 2.3 技术说明
**messagebox 父窗口参数:**
`tkinter.messagebox` 函数的第一个参数可以是父窗口,这样可以:
- 确保弹窗正确显示在主窗口上方
- 使弹窗成为模态对话框(必须先关闭弹窗才能操作主窗口)
- 避免弹窗被隐藏在其他窗口后面
**正确语法:**
```python
messagebox.showinfo(parent, title, message, **options)
messagebox.showerror(parent, title, message, **options)
messagebox.askyesno(parent, title, message, **options)
```
---
## 3. 执行计划
### 3.1 修复步骤
| 步骤 | 操作 | 状态 |
|-----|------|------|
| 1 | 定位所有messagebox调用位置 | ✅ 完成 |
| 2 | 为每个messagebox添加父窗口参数 | ✅ 完成 |
| 3 | 验证修复效果 | ⏳ 待测试 |
### 3.2 验证方法
```bash
# 测试GUI模式
cd AuxiliaryTool/FunctionalTestReportGeneration
python run.py --gui
```
**测试场景:**
1. 不选择文件直接点击"生成报告" → 应显示错误提示框(带确定按钮)
2. 生成报告成功 → 应显示成功提示框(带确定按钮)
3. 生成报告完成后 → 应显示ERP上传确认框(带是/否按钮)
4. 点击"退出"按钮 → 应显示退出确认框(带是/否按钮)
---
## 4. 预防措施
### 4.1 代码规范
**GUI弹窗使用规范:**
1. **始终指定父窗口**
```python
# 正确
messagebox.showinfo(self.root, "标题", "消息")
# 错误
messagebox.showinfo("标题", "消息")
```
2. **使用适当的图标**
```python
messagebox.showinfo(parent, title, message, icon=messagebox.INFO)
messagebox.showwarning(parent, title, message, icon=messagebox.WARNING)
messagebox.showerror(parent, title, message, icon=messagebox.ERROR)
messagebox.askyesno(parent, title, message, icon=messagebox.QUESTION)
```
3. **在主线程中调用**
- GUI操作必须在主线程中执行
- 如在后台线程中需要显示弹窗,使用 `root.after()`
```python
self.root.after(0, lambda: messagebox.showinfo(self.root, "标题", "消息"))
```
### 4.2 messagebox 函数对照表
| 函数 | 按钮 | 返回值 | 适用场景 |
|------|------|--------|----------|
| `showinfo()` | 确定 | None | 信息提示 |
| `showwarning()` | 确定 | None | 警告提示 |
| `showerror()` | 确定 | None | 错误提示 |
| `askyesno()` | 是/否 | True/False | 是非确认 |
| `askokcancel()` | 确定/取消 | True/False | 操作确认 |
| `askyesnocancel()` | 是/否/取消 | True/False/None | 三选确认 |
| `askquestion()` | 是/否 | 'yes'/'no' | 问题确认 |
---
## 5. 执行结果记录
### 5.1 问题修复
| 日期 | 修复内容 | 修复人 | 状态 |
|------|----------|--------|------|
| 2026-03-28 | 为所有messagebox调用添加父窗口参数 | Claude Code | ✅ 已完成 |
### 5.2 修改文件
| 文件 | 修改内容 | 数量 |
|------|----------|------|
| `src/gui.py` | 添加`self.root`父窗口参数到所有messagebox调用 | 6处 |
### 5.3 测试结果
| 测试项 | 结果 | 备注 |
|--------|------|------|
| 错误提示弹窗 | ⏳ 待测试 | 需人工验证 |
| 成功提示弹窗 | ⏳ 待测试 | 需人工验证 |
| 确认对话框 | ⏳ 待测试 | 需人工验证 |
---
## 6. 参考文档
- tkinter官方文档: https://docs.python.org/3/library/tkinter.messagebox.html
- 代码规范: `Docs/PRD/01规范文档/_PRD_规范文档_代码规范.md`
- 问题总结: `Docs/PRD/01规范文档/_PRD_问题总结_记录文档.md`
---
*文档结束*
# 问题描述
## 问题现象
- 在执行代码时,出现了`logging is not defined`的错误提示。
# 报错日志信息
```ignorelang
Traceback (most recent call last):
File "E:\GithubData\ubains-module-test\AuxiliaryTool\FunctionalTestReportGeneration\run.py", line 17, in <module>
from src.main import main
File "E:\GithubData\ubains-module-test\AuxiliaryTool\FunctionalTestReportGeneration\src\main.py", line 17, in <module>
from src.gui import main_gui
File "E:\GithubData\ubains-module-test\AuxiliaryTool\FunctionalTestReportGeneration\src\gui.py", line 607, in <module>
class GUILogHandler(logging.Handler):
NameError: name 'logging' is not defined
```
\ No newline at end of file
# _GUI模式运行失败_问题处理_计划执行
> 版本:V1.0
> 创建日期:2026-03-28
> 更新日期:2026-03-28
> 适用范围:功能测试报告自动化生成工具 - GUI模式
> 来源:基于《GUI模式运行失败_问题处理.md》
> 状态:已完成
## 更新记录
| 版本 | 日期 | 更新内容 |
|------|------|----------|
| V1.0 | 2026-03-28 | 初始版本,修复logging模块导入问题 |
---
## 1. 问题概述
### 1.1 问题现象
在执行 `run.py` 启动GUI模式时,程序报错退出。
### 1.2 报错日志
```ignorelang
Traceback (most recent call last):
File "E:\GithubData\ubains-module-test\AuxiliaryTool\FunctionalTestReportGeneration\run.py", line 17, in <module>
from src.main import main
File "E:\GithubData\ubains-module-test\AuxiliaryTool\FunctionalTestReportGeneration\src\main.py", line 17, in <module>
from src.gui import main_gui
File "E:\GithubData\ubains-module-test\AuxiliaryTool\FunctionalTestReportGeneration\src\gui.py", line 607, in <module>
class GUILogHandler(logging.Handler):
NameError: name 'logging' is not defined
```
### 1.3 问题原因
`src/gui.py` 文件末尾添加了 `GUILogHandler` 类,该类继承自 `logging.Handler`,但文件开头未导入 `logging` 模块,导致 `NameError`
### 1.4 影响范围
- GUI模式完全无法启动
- CLI模式不受影响
---
## 2. 解决方案
### 2.1 修复内容
`src/gui.py` 文件的导入部分添加 `logging` 模块导入。
**修改位置:** `src/gui.py` 第8-21行(导入部分)
**修改前:**
```python
import tkinter as tk
from tkinter import filedialog, messagebox, scrolledtext
import threading
import sys
from pathlib import Path
from typing import Optional
```
**修改后:**
```python
import tkinter as tk
from tkinter import filedialog, messagebox, scrolledtext
import threading
import sys
import logging
from pathlib import Path
from typing import Optional
```
### 2.2 修复说明
- 在导入列表中添加 `import logging`
- `logging` 是Python标准库,无需额外安装依赖
- 修复位置为文件开头,与其他导入语句放在一起
---
## 3. 执行计划
### 3.1 修复步骤
| 步骤 | 操作 | 状态 |
|-----|------|------|
| 1 | 定位问题代码位置 | ✅ 完成 |
| 2 | 添加logging模块导入 | ✅ 完成 |
| 3 | 验证修复效果 | ⏳ 待测试 |
### 3.2 验证方法
```bash
# 测试GUI模式启动
cd AuxiliaryTool/FunctionalTestReportGeneration
python run.py --gui
```
**预期结果:**
- GUI窗口正常启动
-`NameError` 错误
---
## 4. 预防措施
### 4.1 代码规范
为避免类似问题再次发生,需遵守以下规范:
1. **导入顺序规范**:按照以下顺序组织导入语句
- Python标准库导入
- 第三方库导入
- 本地模块导入
2. **导入检查清单**:添加使用外部类/函数时,确认已导入相应模块
- 使用 `logging.Handler` 前确认 `import logging`
- 使用 `requests` 前确认 `import requests`
- 依此类推
3. **IDE辅助**:利用IDE的导入检查功能
- PyCharm:代码检查会提示未使用的导入
- VS Code:Python扩展会提示导入错误
### 4.2 测试检查
在添加新功能后,务必进行以下测试:
- [ ] CLI模式启动测试
- [ ] GUI模式启动测试
- [ ] 功能完整性测试
---
## 5. 执行结果记录
### 5.1 问题修复
| 日期 | 修复内容 | 修复人 | 状态 |
|------|----------|--------|------|
| 2026-03-28 | 在gui.py中添加`import logging` | Claude Code | ✅ 已完成 |
### 5.2 修改文件
| 文件 | 修改内容 | 行号 |
|------|----------|------|
| `src/gui.py` | 添加`import logging` | 第12行 |
### 5.3 测试结果
| 测试项 | 结果 | 备注 |
|--------|------|------|
| GUI模式启动 | ⏳ 待测试 | 需人工验证 |
| CLI模式运行 | ⏳ 不受影响 | 无需测试 |
---
## 6. 参考文档
- 代码规范: `Docs/PRD/01规范文档/_PRD_规范文档_代码规范.md`
- 问题总结: `Docs/PRD/01规范文档/_PRD_问题总结_记录文档.md`
---
*文档结束*
# 问题描述
## 问题现象
- 在执行代码时,出现了`tkinter' has no attribute 'Progressbar`的错误提示。
# 报错日志信息
```ignorelang
╔═══════════════════════════════════════════════════════════╗
║ ║
║ 功能测试报告自动生成工具 v1.0.0 ║
║ ║
║ 功能测试报告自动生成工具 ║
║ ║
╚═══════════════════════════════════════════════════════════╝
错误: module 'tkinter' has no attribute 'Progressbar'
```
\ No newline at end of file
# _GUI模式运行报错tkinter_问题处理_计划执行
> 版本:V1.0
> 创建日期:2026-03-28
> 更新日期:2026-03-28
> 适用范围:功能测试报告自动化生成工具 - GUI模式
> 来源:基于《GUI模式运行报错tkinter_问题处理.md》
> 状态:已完成
## 更新记录
| 版本 | 日期 | 更新内容 |
|------|------|----------|
| V1.0 | 2026-03-28 | 初始版本,修复Progressbar导入问题 |
---
## 1. 问题概述
### 1.1 问题现象
在执行 `run.py --gui` 启动GUI模式时,程序报错退出。
### 1.2 报错日志
```ignorelang
╔═══════════════════════════════════════════════════════════╗
║ ║
║ 功能测试报告自动生成工具 v1.0.0 ║
║ ║
║ 功能测试报告自动生成工具 ║
║ ║
╚═══════════════════════════════════════════════════════════╝
错误: module 'tkinter' has no attribute 'Progressbar'
```
### 1.3 问题原因
`src/gui.py` 中使用了 `tk.Progressbar`,但 `Progressbar` 组件不在 `tkinter` 主模块中,而是在 `tkinter.ttk` 子模块中。
### 1.4 影响范围
- GUI模式完全无法启动
- CLI模式不受影响
---
## 2. 解决方案
### 2.1 修复内容
**修改1:导入部分添加 ttk**
**位置:** `src/gui.py` 第9行
**修改前:**
```python
from tkinter import filedialog, messagebox, scrolledtext
```
**修改后:**
```python
from tkinter import filedialog, messagebox, scrolledtext, ttk
```
**修改2:Progressbar 使用 ttk.Progressbar**
**位置:** `src/gui.py` 第99行
**修改前:**
```python
self.progress_bar = tk.Progressbar(
main_frame,
variable=self.progress_var,
maximum=100,
length=400,
mode='determinate',
height=20
)
```
**修改后:**
```python
self.progress_bar = ttk.Progressbar(
main_frame,
variable=self.progress_var,
maximum=100,
length=400,
mode='determinate'
)
```
**注意:** 移除了 `height=20` 参数,因为 `ttk.Progressbar` 不支持此参数。
### 2.2 技术说明
**tkinter.ttk 模块:**
- `ttk`(Themed Tkinter)是 tkinter 的主题化扩展
- 提供了一组现代风格的组件(Progressbar、Treeview、Notebook等)
- 这些组件支持系统主题,外观更现代化
**ttk 与 tkinter 的区别:**
| 组件 | tkinter | ttk |
|------|---------|-----|
| Progressbar | ❌ 不支持 | ✅ 支持 |
| Treeview | ❌ 不支持 | ✅ 支持 |
| Notebook | ❌ 不支持 | ✅ 支持 |
| Button | ✅ 支持 | ✅ 支持(样式不同)|
---
## 3. 执行计划
### 3.1 修复步骤
| 步骤 | 操作 | 状态 |
|-----|------|------|
| 1 | 定位问题代码位置 | ✅ 完成 |
| 2 | 添加ttk模块导入 | ✅ 完成 |
| 3 | 修改Progressbar引用 | ✅ 完成 |
| 4 | 验证修复效果 | ⏳ 待测试 |
### 3.2 验证方法
```bash
# 测试GUI模式启动
cd AuxiliaryTool/FunctionalTestReportGeneration
python run.py --gui
```
**预期结果:**
- GUI窗口正常启动
- 进度条正常显示
-`AttributeError` 错误
---
## 4. 预防措施
### 4.1 tkinter组件使用规范
**标准组件(tkinter):**
- Button, Label, Entry, Text, Frame等
- 使用方式:`tk.Button`, `tk.Label`
**主题组件(tkinter.ttk):**
- Progressbar, Treeview, Notebook, Combobox等
- 使用方式:`ttk.Progressbar`, `ttk.Treeview`
**导入方式:**
```python
import tkinter as tk
from tkinter import ttk # 用于主题组件
# 标准组件
button = tk.Button(parent, text="点击")
# 主题组件
progress = ttk.Progressbar(parent)
tree = ttk.Treeview(parent)
```
### 4.2 常见组件对照表
| 功能 | tkinter | ttk |
|------|---------|-----|
| 按钮 | tk.Button | ttk.Button |
| 标签 | tk.Label | ttk.Label |
| 输入框 | tk.Entry | ttk.Entry |
| 进度条 | ❌ | ttk.Progressbar |
| 树形视图 | ❌ | ttk.Treeview |
| 选项卡 | ❌ | ttk.Notebook |
| 下拉框 | ❌ | ttk.Combobox |
### 4.3 测试检查
添加使用新tkinter组件后,务必进行以下测试:
- [ ] GUI模式启动测试
- [ ] 组件正常显示测试
- [ ] 功能完整性测试
---
## 5. 执行结果记录
### 5.1 问题修复
| 日期 | 修复内容 | 修复人 | 状态 |
|------|----------|--------|------|
| 2026-03-28 | 添加ttk导入,修改Progressbar引用 | Claude Code | ✅ 已完成 |
### 5.2 修改文件
| 文件 | 修改内容 | 行号 |
|------|----------|------|
| `src/gui.py` | 添加`from tkinter import ttk` | 第9行 |
| `src/gui.py` | 修改`tk.Progressbar``ttk.Progressbar` | 第99行 |
| `src/gui.py` | 移除不支持的`height=20`参数 | 第99行 |
### 5.3 测试结果
| 测试项 | 结果 | 备注 |
|--------|------|------|
| GUI模式启动 | ⏳ 待测试 | 需人工验证 |
| 进度条显示 | ⏳ 待测试 | 需人工验证 |
| CLI模式运行 | ⏳ 不受影响 | 无需测试 |
---
## 6. 参考文档
- tkinter官方文档: https://docs.python.org/3/library/tkinter.ttk.html
- 代码规范: `Docs/PRD/01规范文档/_PRD_规范文档_代码规范.md`
- 问题总结: `Docs/PRD/01规范文档/_PRD_问题总结_记录文档.md`
---
*文档结束*
...@@ -118,7 +118,7 @@ from spire.doc import * ...@@ -118,7 +118,7 @@ from spire.doc import *
- API key值为:9adc1ce611544fae9bbbc5f6b1d6ce87 - API key值为:9adc1ce611544fae9bbbc5f6b1d6ce87
- 生产环境为:https://office.ubainsyun.com:5082/api/uerp - 生产环境为:https://office.ubainsyun.com:5082/api/uerp
- 读取ERP对接文档的第4.3节内容了解请求参数及接口路径 - 读取ERP对接文档的第4.3节内容了解请求参数及接口路径
- developtesting_id:暂时为空 - developtesting_id:先默认为407
- type_id:先默认为`2` - type_id:先默认为`2`
- content:文件转换的富文本参数; - content:文件转换的富文本参数;
- copyuserList:先为空; - copyuserList:先为空;
...@@ -133,6 +133,25 @@ from spire.doc import * ...@@ -133,6 +133,25 @@ from spire.doc import *
#### 日志审计 #### 日志审计
- 记录每一步骤的日志信息打印至控制台。 - 记录每一步骤的日志信息打印至控制台。
#### 需求确认总结
| 序号 | 疑问点 | 确认方案 |
|:----:|--------|----------|
| 1 | 接口端口不一致 | 使用 **5082** 端口 |
| 2 | Spire.Doc商业库限制 | 使用**免费版本**,去除水印字符串 |
| 3 | 图片上传失败处理 | **补充日志打印**,保留本地路径 |
| 4 | 图片路径格式验证 | **需验证** Spire.Doc 生成的实际格式 |zh
| 5 | 重试机制细节 | 间隔 **5秒**,最多重试3次 |
| 6 | 默认参数配置化 | **可配置**(预留),暂时默认407/2 |
| 7 | 交互方式确认 | **控制台交互**(Y/N确认) |
| 8 | 日志格式规范 | 使用 `logging` 模块,自定义格式与等级 |
| 9 | HTML内容清理 | **不需要**额外处理 |
| 10 | 代码文件位置 | 新建 `src/erp_uploader.py` 模块,在 `cli.py``generate_report_main()` 末尾调用 |
| 11 | 入口脚本确认 | 入口为 `run.py`,在报告生成完成后(CLI第336-346行)添加上传提示 |
| 12 | Spire.Doc库安装 | `pip install Spire.Doc`,需添加到 `requirements.txt` |
| 13 | GUI模式支持 | **同步增加**上传功能,保持功能一致性 |
| 14 | 配置文件管理 | API key、base_url等配置项放到 `config.py` 中 |
## 规范文档 ## 规范文档
- 代码规范: `Docs/PRD/01规范文档/_PRD_规范文档_代码规范.md` - 代码规范: `Docs/PRD/01规范文档/_PRD_规范文档_代码规范.md`
- 问题总结: `Docs/PRD/01规范文档/_PRD_问题总结_记录文档.md` - 问题总结: `Docs/PRD/01规范文档/_PRD_问题总结_记录文档.md`
......
# _PRD_报告生成后自动调用ERP上传_计划执行
> 版本:V1.0
> 创建日期:2026-03-28
> 更新日期:2026-03-28
> 适用范围:功能测试报告自动化生成工具 - ERP上传功能
> 来源:基于《_PRD_报告生成后自动调用ERP上传_优化需求文档.md》
> 状态:待执行
## 更新记录
| 版本 | 日期 | 更新内容 |
|------|------|----------|
| V1.0 | 2026-03-28 | 初始版本,ERP上传功能开发 |
---
## 1. 项目概述
### 1.1 背景
当前功能测试报告生成工具在生成Word文档后,需要手动将报告上传到ERP系统。为了提高工作效率,需要实现报告生成后自动调用ERP接口上传测试报告的功能。
### 1.2 目标
- 报告生成完成后,自动询问用户是否上传到ERP
- 将Word报告转换为HTML富文本格式
- 提取报告中的图片并单独上传到ERP获取URL
- 调用ERP接口创建测试报告记录
- 支持CLI和GUI两种交互方式
- 完善的重试机制和日志记录
### 1.3 涉及文件
**需求文档:**
- `Docs/PRD/自动化生成功能测试报告/需求文档/_PRD_报告生成后自动调用ERP上传_优化需求文档.md`
- `Docs/PRD/自动化生成功能测试报告/ERP对接PRD/PRD_测试列表_测试报告_例子说明.md`
**需要修改的代码文件:**
| 序号 | 文件名 | 操作 | 功能描述 |
|-----|--------|------|----------|
| 1 | `src/erp_uploader.py` | 新建 | ERP上传核心模块 |
| 2 | `src/config.py` | 修改 | 添加ERP相关配置项 |
| 3 | `src/cli.py` | 修改 | 报告生成后添加上传提示 |
| 4 | `src/gui.py` | 修改 | GUI模式添加上传按钮 |
| 5 | `requirements.txt` | 修改 | 添加Spire.Doc依赖 |
---
## 2. ERP接口分析
### 2.1 接口基础信息
| 项目 | 值 |
|------|-----|
| 生产环境地址 | `https://office.ubainsyun.com:5082/api/uerp` |
| 认证方式 | Header: `X-Api-Key: 9adc1ce611544fae9bbbc5f6b1d6ce87` |
| 响应格式 | JSON |
### 2.2 相关接口
#### 2.2.1 上传图片接口
```
POST /openclaw/upload/richtext
Content-Type: multipart/form-data
```
**请求参数:**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| file | file | 是 | 图片文件 |
**响应示例:**
```json
{
"success": 1,
"data": {
"url": "https://office.ubainsyun.com:5015/upload/xxx.png"
}
}
```
#### 2.2.2 创建测试报告接口
```
POST /openclaw/report
Content-Type: application/json
```
**请求参数:**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| developtesting_id | int | 是 | 测试单ID(默认407) |
| type_id | int | 是 | 报告类型ID(默认2) |
| content | string | 是 | 报告内容(HTML格式) |
| descript | string | 否 | 风险问题描述 |
| copyuserList | array | 否 | 抄送人ID列表 |
**响应示例:**
```json
{
"success": 1,
"data": {
"create": 9
}
}
```
---
## 3. 程序架构设计
### 3.1 整体架构
```
cli.py / gui.py (交互层)
生成报告完成
┌───┴──────────────────────────────────────┐
│ erp_uploader.py (ERP上传层) │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ word_to_html_with_images() │ │
│ │ - 读取Word文档 │ │
│ │ - 提取图片 │ │
│ │ - 上传图片获取URL │ │
│ │ - 转换为HTML │ │
│ │ - 替换图片路径 │ │
│ │ - 去除水印 │ │
│ └─────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ upload_report_to_erp() │ │
│ │ - 调用ERP接口 │ │
│ │ - 重试机制(最多3次,间隔5秒) │ │
│ │ - 错误处理 │ │
│ └─────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ ask_upload_confirmation() │ │
│ │ - CLI: 控制台Y/N确认 │ │
│ │ - GUI: 对话框确认 │ │
│ └─────────────────────────────────────┘ │
└───────────────────────────────────────────┘
config.py (配置层)
- ERP_API_KEY
- ERP_BASE_URL
- ERP_DEVELOPTESTING_ID
- ERP_TYPE_ID
- ERP_RETRY_INTERVAL
- ERP_MAX_RETRIES
```
### 3.2 数据流向
```
Word报告文件 (.docx)
Spire.Doc 加载文档
├──→ 提取图片数据
│ ↓
│ 保存临时图片
│ ↓
│ 上传到ERP (POST /openclaw/upload/richtext)
│ ↓
│ 获取图片URL列表
└──→ 转换为HTML (图片不嵌入)
读取HTML内容
替换图片路径为ERP URL
去除水印
调用创建报告接口 (POST /openclaw/report)
重试机制 (最多3次,间隔5秒)
返回结果
```
---
## 4. 各模块详细设计
### 4.1 config.py - 配置模块(修改)
**新增配置项:**
```python
# ERP上传配置
ERP_API_KEY = "9adc1ce611544fae9bbbc5f6b1d6ce87"
ERP_BASE_URL = "https://office.ubainsyun.com:5082/api/uerp"
ERP_DEVELOPTESTING_ID = 407 # 测试单ID,可配置
ERP_TYPE_ID = 2 # 报告类型ID,可配置
ERP_RETRY_INTERVAL = 5 # 重试间隔(秒)
ERP_MAX_RETRIES = 3 # 最大重试次数
# ERP接口路径
ERP_UPLOAD_IMAGE_URL = "/openclaw/upload/richtext" # 上传图片接口
ERP_CREATE_REPORT_URL = "/openclaw/report" # 创建报告接口
# Spire.Doc水印文本
SPIRE_DOC_WATERMARK = "Evaluation Warning: The document was created with Spire.Doc for Python."
```
---
### 4.2 erp_uploader.py - ERP上传模块(新建)
**职责:**
- Word文档转换为HTML(图片单独处理)
- 图片上传到ERP
- 测试报告上传到ERP
- 重试机制
- 日志记录
**导出函数:**
```python
def extract_images_from_word(document) -> List[bytes]
"""
从Word文档中提取所有图片
:param document: Spire.Doc Document对象
:return: 图片字节数组列表
"""
def word_to_html_with_images(file_path: str, logger: logging.Logger) -> str
"""
将Word转HTML,图片单独上传获取URL
:param file_path: Word文件路径
:param logger: 日志记录器
:return: HTML字符串,图片已替换为ERP URL
"""
def upload_image_to_erp(img_data: bytes, idx: int, logger: logging.Logger) -> Optional[str]
"""
上传单张图片到ERP(含重试机制)
:param img_data: 图片字节数据
:param idx: 图片索引
:param logger: 日志记录器
:return: 图片URL,失败返回None
"""
def create_report_in_erp(content: str, logger: logging.Logger) -> Optional[int]
"""
在ERP中创建测试报告(含重试机制)
:param content: HTML格式的报告内容
:param logger: 日志记录器
:return: 报告ID,失败返回None
"""
def upload_report_to_erp(file_path: str, logger: logging.Logger) -> bool
"""
上传报告到ERP的完整流程
:param file_path: Word报告文件路径
:param logger: 日志记录器
:return: 是否上传成功
"""
def ask_upload_confirmation_cli() -> bool
"""
控制台交互:询问用户是否上传报告
:return: 用户选择(True=上传,False=不上传)
"""
def setup_logger(name: str = "erp_uploader") -> logging.Logger
"""
设置日志记录器
:param name: 日志记录器名称
:return: 配置好的日志记录器
"""
```
**详细设计:**
#### 4.2.1 extract_images_from_word()
```python
def extract_images_from_word(document) -> List[bytes]:
"""从Word文档中提取所有图片"""
images = []
for section in document.Sections:
for paragraph in section.Paragraphs:
for child_obj in paragraph.ChildObjects:
if child_obj.DocumentObjectType == DocumentObjectType.Picture:
picture = child_obj
images.append(picture.Image.ImageData)
return images
```
#### 4.2.2 upload_image_to_erp() - 含重试机制
```python
def upload_image_to_erp(img_data: bytes, idx: int, logger: logging.Logger) -> Optional[str]:
"""
上传单张图片到ERP(含重试机制)
- 最多重试3次,间隔5秒
- 仅对网络超时和5xx错误重试
- 4xx错误不重试
"""
for attempt in range(ERP_MAX_RETRIES):
try:
# 保存临时文件
with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as tmp:
tmp.write(img_data)
tmp_path = tmp.name
# 上传
with open(tmp_path, 'rb') as f:
files = {'file': f}
headers = {'X-Api-Key': ERP_API_KEY}
resp = requests.post(
f'{ERP_BASE_URL}{ERP_UPLOAD_IMAGE_URL}',
headers=headers,
files=files,
timeout=30
)
result = resp.json()
if result.get('success') == 1:
logger.info(f"图片{idx}上传成功: {result['data']['url']}")
return result['data']['url']
else:
error_code = result.get('error', 0)
# 4xx错误不重试
if 400 <= error_code < 500:
logger.error(f"图片{idx}上传失败(客户端错误): {result.get('msg')}")
return None
if attempt < ERP_MAX_RETRIES - 1:
logger.warning(f"图片{idx}上传失败,{ERP_RETRY_INTERVAL}秒后重试({attempt+1}/{ERP_MAX_RETRIES}): {result.get('msg')}")
time.sleep(ERP_RETRY_INTERVAL)
else:
logger.error(f"图片{idx}上传失败(已达最大重试次数): {result.get('msg')}")
except requests.exceptions.Timeout:
if attempt < ERP_MAX_RETRIES - 1:
logger.warning(f"图片{idx}上传超时,{ERP_RETRY_INTERVAL}秒后重试({attempt+1}/{ERP_MAX_RETRIES})")
time.sleep(ERP_RETRY_INTERVAL)
else:
logger.error(f"图片{idx}上传超时(已达最大重试次数)")
except Exception as e:
logger.error(f"图片{idx}上传异常: {str(e)}")
break
finally:
# 清理临时文件
if os.path.exists(tmp_path):
os.remove(tmp_path)
return None
```
#### 4.2.3 create_report_in_erp() - 含重试机制
```python
def create_report_in_erp(content: str, logger: logging.Logger) -> Optional[int]:
"""
在ERP中创建测试报告(含重试机制)
- 最多重试3次,间隔5秒
- 仅对网络超时和5xx错误重试
"""
for attempt in range(ERP_MAX_RETRIES):
try:
data = {
'developtesting_id': ERP_DEVELOPTESTING_ID,
'type_id': ERP_TYPE_ID,
'content': content,
'descript': '',
'copyuserList': []
}
headers = {
'X-Api-Key': ERP_API_KEY,
'Content-Type': 'application/json'
}
resp = requests.post(
f'{ERP_BASE_URL}{ERP_CREATE_REPORT_URL}',
headers=headers,
json=data,
timeout=30
)
result = resp.json()
if result.get('success') == 1:
report_id = result['data']['create']
logger.info(f"报告创建成功! ERP报告ID: {report_id}")
return report_id
else:
error_code = result.get('error', 0)
# 4xx错误不重试
if 400 <= error_code < 500:
logger.error(f"报告创建失败(客户端错误): {result.get('msg')}")
return None
if attempt < ERP_MAX_RETRIES - 1:
logger.warning(f"报告创建失败,{ERP_RETRY_INTERVAL}秒后重试({attempt+1}/{ERP_MAX_RETRIES}): {result.get('msg')}")
time.sleep(ERP_RETRY_INTERVAL)
else:
logger.error(f"报告创建失败(已达最大重试次数): {result.get('msg')}")
except requests.exceptions.Timeout:
if attempt < ERP_MAX_RETRIES - 1:
logger.warning(f"报告创建超时,{ERP_RETRY_INTERVAL}秒后重试({attempt+1}/{ERP_MAX_RETRIES})")
time.sleep(ERP_RETRY_INTERVAL)
else:
logger.error(f"报告创建超时(已达最大重试次数)")
except Exception as e:
logger.error(f"报告创建异常: {str(e)}")
break
return None
```
#### 4.2.4 word_to_html_with_images()
```python
def word_to_html_with_images(file_path: str, logger: logging.Logger) -> str:
"""
将Word转HTML,图片单独上传获取URL
"""
logger.info(f"开始处理Word文档: {file_path}")
# 1. 加载Word文档
from spire.doc import Document, FileFormat
document = Document()
document.LoadFromFile(file_path)
logger.info("Word文档加载成功")
# 2. 提取所有图片
image_data_list = extract_images_from_word(document)
logger.info(f"提取到{len(image_data_list)}张图片")
# 3. 上传图片获取URL列表
image_urls = []
for idx, img_data in enumerate(image_data_list):
url = upload_image_to_erp(img_data, idx, logger)
image_urls.append(url)
if url is None:
logger.warning(f"图片{idx}上传失败,HTML中将保留本地路径")
# 4. 转换为HTML(不嵌入图片)
temp_dir = tempfile.mkdtemp()
temp_html = os.path.join(temp_dir, "temp.html")
document.HtmlExportOptions.ImageEmbedded = False
document.SaveToFile(temp_html, FileFormat.Html)
logger.info(f"HTML转换完成: {temp_html}")
# 5. 读取HTML并替换图片路径为URL
with open(temp_html, 'r', encoding='utf-8') as f:
html_content = f.read()
# Spire.Doc生成的图片路径格式,需要验证
# 预期格式: temp_html_files/image1.png
html_filename = os.path.basename(temp_html)
image_folder = f"{html_filename}_files/"
for idx, url in enumerate(image_urls):
if url:
# 可能的格式,都需要尝试替换
old_patterns = [
f'{image_folder}image{idx+1}.png',
f'{image_folder}image{idx+1}.jpg',
f'image{idx+1}.png',
f'image{idx+1}.jpg'
]
for pattern in old_patterns:
if pattern in html_content:
html_content = html_content.replace(pattern, url)
logger.info(f"替换图片路径: {pattern} -> {url}")
break
# 清理临时文件
import shutil
shutil.rmtree(temp_dir, ignore_errors=True)
# 6. 去除水印
html_content = html_content.replace(SPIRE_DOC_WATERMARK, '')
logger.info("水印已去除")
return html_content
```
**依赖:**
- `spire-doc` - Word文档转HTML
- `requests` - HTTP请求
- `logging` - 日志记录
- `tempfile` - 临时文件处理
---
### 4.3 cli.py - 命令行交互模块(修改)
**修改内容:**
`generate_report_main()` 函数的末尾(报告生成完成后)添加上传提示
**修改位置:** 第336-346行之后
**新增代码:**
```python
def generate_report_main(...) -> List[str]:
# ... 现有代码 ...
# 步骤6:完成
print(f"\n报告已保存到:")
for report in generated_reports:
print(f" - {report}")
# ===== 新增:ERP上传功能 =====
# 检查是否有docx格式的报告
docx_reports = [r for r in generated_reports if r.endswith('.docx')]
if docx_reports:
from src.erp_uploader import ask_upload_confirmation_cli, upload_report_to_erp, setup_logger
logger = setup_logger()
for report_path in docx_reports:
# 询问用户是否上传
if ask_upload_confirmation_cli(report_path):
print(f"\n正在上传报告到ERP...")
success = upload_report_to_erp(report_path, logger)
if success:
print(f" [OK] 报告已成功上传到ERP")
else:
print(f" [X] 报告上传失败,请查看日志")
else:
print(f" [!] 跳过上传: {report_path}")
# ===== 新增结束 =====
return generated_reports
```
**新增函数:**
```python
def ask_upload_confirmation_cli(report_path: str) -> bool:
"""
控制台交互:询问用户是否上传报告到ERP
:param report_path: 报告文件路径
:return: 用户选择(True=上传,False=不上传)
"""
while True:
print(f"\n是否将报告上传到ERP测试单?")
print(f" 报告文件: {report_path}")
choice = input(" 请输入 Y/N (默认=N): ").strip().upper()
if not choice:
choice = 'N'
if choice == 'Y':
return True
elif choice == 'N':
return False
else:
print(" 输入错误,请输入 Y 或 N")
```
---
### 4.4 gui.py - GUI界面模块(修改)
**修改内容:**
在报告生成完成后,添加上传提示对话框
**修改位置:** 报告生成完成后的回调函数
**新增代码:**
```python
def ask_upload_confirmation_gui(report_path: str) -> bool:
"""
GUI交互:询问用户是否上传报告到ERP
:param report_path: 报告文件路径
:return: 用户选择(True=上传,False=不上传)
"""
from tkinter import messagebox
result = messagebox.askyesno(
"上传报告到ERP",
f"报告已生成完成!\n\n是否将报告上传到ERP测试单?\n\n报告文件:\n{report_path}"
)
return result
def upload_report_with_gui(report_path: str, log_callback=None) -> bool:
"""
GUI模式下的报告上传函数
:param report_path: 报告文件路径
:param log_callback: 日志回调函数
:return: 是否上传成功
"""
from src.erp_uploader import upload_report_to_erp, setup_logger
# 设置日志,使用回调函数输出到GUI
logger = setup_logger()
if log_callback:
handler = GUILogHandler(log_callback)
logger.addHandler(handler)
return upload_report_to_erp(report_path, logger)
class GUILogHandler(logging.Handler):
"""自定义日志处理器,将日志输出到GUI"""
def __init__(self, callback):
super().__init__()
self.callback = callback
def emit(self, record):
msg = self.format(record)
self.callback(msg)
```
**界面修改:**
在报告生成完成后,弹出上传确认对话框:
```
┌─────────────────────────────────────────────────┐
│ 上传报告到ERP │
├─────────────────────────────────────────────────┤
│ │
│ 报告已生成完成! │
│ │
│ 是否将报告上传到ERP测试单? │
│ │
│ 报告文件: │
│ reports/项目名称_功能测试报告_2026年03月28日.docx│
│ │
│ [是(Y)] [否(N)] │
└─────────────────────────────────────────────────┘
```
---
## 5. 执行计划
### 5.1 阶段划分
| 阶段 | 任务 | 预计工作量 | 依赖 | 状态 |
|-----|------|----------|------|------|
| **阶段1** | 修改config.py,添加ERP配置项 | 0.5天 | - | ⏳ 待执行 |
| **阶段2** | 实现erp_uploader.py核心模块 | 2天 | 阶段1 | ⏳ 待执行 |
| **阶段3** | 修改cli.py,添加CLI上传功能 | 0.5天 | 阶段2 | ⏳ 待执行 |
| **阶段4** | 修改gui.py,添加GUI上传功能 | 1天 | 阶段2 | ⏳ 待执行 |
| **阶段5** | 更新requirements.txt | 0.1天 | 阶段1 | ⏳ 待执行 |
| **阶段6** | 集成测试 | 1天 | 阶段3,4,5 | ⏳ 待执行 |
| **阶段7** | 文档更新 | 0.5天 | 阶段6 | ⏳ 待执行 |
### 5.2 里程碑
| 里程碑 | 完成标准 | 状态 |
|-------|---------|------|
| M1: 配置完成 | config.py新增ERP配置项 | ⏳ 待达成 |
| M2: 核心模块完成 | erp_uploader.py实现完成 | ⏳ 待达成 |
| M3: CLI集成完成 | CLI模式可正常上传报告 | ⏳ 待达成 |
| M4: GUI集成完成 | GUI模式可正常上传报告 | ⏳ 待达成 |
| M5: 测试完成 | 集成测试通过,无重大BUG | ⏳ 待达成 |
---
## 6. 测试计划
### 6.1 单元测试
| 模块 | 测试内容 |
|------|----------|
| erp_uploader.py | 图片提取、图片上传、HTML转换、报告创建、重试机制 |
| config.py | 配置项正确性 |
### 6.2 集成测试
测试场景:
1. CLI模式完整流程测试
2. GUI模式完整流程测试
3. 网络异常重试测试
4. ERP接口异常处理测试
5. 含图片报告上传测试
6. 无图片报告上传测试
### 6.3 测试数据
**测试报告:** `reports/中国石油兰州石化项目_功能进度测试报告_2026年03月27日.docx`
---
## 7. 风险与应对
| 风险 | 影响 | 概率 | 应对措施 |
|-----|------|------|---------|
| Spire.Doc免费版功能限制 | 中 | 中 | 先验证免费版功能是否满足需求,必要时考虑替代方案 |
| 图片路径格式变化 | 中 | 中 | 支持多种可能的路径格式,动态替换 |
| ERP接口不稳定 | 高 | 中 | 实现重试机制,记录详细日志 |
| 网络超时 | 中 | 高 | 设置timeout参数,实现重试机制 |
| 大文件处理超时 | 低 | 低 | 增加处理超时时间,显示进度 |
---
## 8. 验收标准
### 8.1 功能验收
- [ ] 报告生成后自动提示用户是否上传
- [ ] Word报告能正确转换为HTML格式
- [ ] 报告中的图片能正确提取并上传到ERP
- [ ] 图片URL能正确替换到HTML中
- [ ] 能成功调用ERP接口创建测试报告
- [ ] 网络异常时能正确重试(最多3次,间隔5秒)
- [ ] 4xx错误不进行重试
- [ ] 日志记录完整,便于问题排查
- [ ] CLI模式交互正常
- [ ] GUI模式交互正常
### 8.2 代码质量验收
- [ ] 符合代码规范要求(中文注释)
- [ ] 每个函数有docstring说明
- [ ] 异常处理完善
- [ ] 日志输出完整
- [ ] Spire.Doc水印正确去除
---
## 9. 参考文档
- 代码规范: `Docs/PRD/01规范文档/_PRD_规范文档_代码规范.md`
- 问题总结: `Docs/PRD/01规范文档/_PRD_问题总结_记录文档.md`
- 方法总结: `Docs/PRD/01规范文档/_PRD_方法总结_记录文档.md`
- 文档规范: `Docs/PRD/01规范文档/_PRD_规范文档_文档规范.md`
- 测试规范: `Docs/PRD/01规范文档/_PRD_规范文档_测试规范.md`
---
## 10. 执行结果记录
### 10.1 问题记录
| 日期 | 问题描述 | 解决方案 | 状态 |
|------|----------|---------|------|
| - | - | - | - |
### 10.2 变更记录
| 日期 | 变更内容 | 变更原因 | 状态 |
|------|----------|----------|------|
| - | - | - | - |
---
*文档结束*
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论