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

feat(erp): 集成ERP项目资料上传功能并支持创建人指定

- 新增ERP项目资料上传接口配置和上传函数实现
- 在CLI和GUI模式下集成项目资料上传交互流程
- 添加创建人ID参数支持和人员列表匹配功能
- 更新.gitignore文件以忽略新生成的构建文件
- 修复GUI模式下上传ERP的参数解包错误问题
- 实现上传成功后的项目资料归档功能
上级 45db18ec
......@@ -58,3 +58,18 @@ __pycache__/
/AuxiliaryTool/FunctionalTestReportGeneration/testcases/
/AuxiliaryTool/ScriptTool/ApiSecurityTest/requirements.txt
/AuxiliaryTool/FunctionalTestReportGeneration/reports.docx
/AuxiliaryTool/DocumentAutoOptimizationCalibration/build/文档优化工具/Analysis-00.toc
/AuxiliaryTool/DocumentAutoOptimizationCalibration/build/文档优化工具/base_library.zip
/AuxiliaryTool/DocumentAutoOptimizationCalibration/build/文档优化工具/EXE-00.toc
/AuxiliaryTool/FunctionalTestReportGeneration/temp/erp_upload_images/image_0.png
/AuxiliaryTool/FunctionalTestReportGeneration/temp/erp_upload_images/image_1.png
/AuxiliaryTool/FunctionalTestReportGeneration/temp/erp_upload_images/image_2.png
/AuxiliaryTool/FunctionalTestReportGeneration/temp/erp_upload_images/image_3.png
/AuxiliaryTool/DocumentAutoOptimizationCalibration/build/文档优化工具/PYZ-00.pyz
/AuxiliaryTool/DocumentAutoOptimizationCalibration/build/文档优化工具/PKG-00.toc
/AuxiliaryTool/DocumentAutoOptimizationCalibration/build/文档优化工具/PYZ-00.toc
/AuxiliaryTool/FunctionalTestReportGeneration/temp/erp_upload_html/report_20260617_114244.html
/AuxiliaryTool/FunctionalTestReportGeneration/temp/erp_upload_html/report_20260617_120013.html
/AuxiliaryTool/DocumentAutoOptimizationCalibration/build/文档优化工具/文档优化工具.pkg
/AuxiliaryTool/DocumentAutoOptimizationCalibration/build/文档优化工具/xref-文档优化工具.html
/AuxiliaryTool/DocumentAutoOptimizationCalibration/build/文档优化工具/warn-文档优化工具.txt
# -*- mode: python ; coding: utf-8 -*-
from PyInstaller.utils.hooks import collect_all
datas = []
binaries = []
hiddenimports = ['win32com.client', 'win32timezone', 'pythoncom', 'pywintypes', 'translators', 'translators.apis', 'docx', 'docx.oxml', 'docx.opc']
tmp_ret = collect_all('translators')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
a = Analysis(
['run.py'],
pathex=[],
binaries=binaries,
datas=datas,
hiddenimports=hiddenimports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='文档优化工具',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
......@@ -361,18 +361,32 @@ def generate_report_main(
logger = setup_logger()
for report_path in docx_reports:
# 询问用户是否上传,获取测试单ID和抄送人列表
confirmed, testing_id, copyuser_ids = ask_upload_confirmation_cli(report_path)
# 询问用户是否上传,获取测试单ID、抄送人列表和创建人ID
confirmed, testing_id, copyuser_ids, creator_id = ask_upload_confirmation_cli(report_path)
if confirmed:
print(f"\n正在上传报告到ERP(测试单ID: {testing_id})...")
success = upload_report_to_erp(
report_path,
logger,
developtesting_id=testing_id,
copyuser_list=copyuser_ids
copyuser_list=copyuser_ids,
createuser_id=creator_id
)
if success:
print(f" [OK] 报告已成功上传到ERP(测试单ID: {testing_id})")
# 询问是否上传到项目资料
from src.erp_uploader import ask_project_upload_confirmation_cli, upload_file_to_project
proj_confirmed, project_id, file_name = ask_project_upload_confirmation_cli(report_path)
if proj_confirmed:
print(f"\n正在上传报告到ERP项目资料(项目ID: {project_id})...")
file_info = upload_file_to_project(report_path, project_id, file_name, logger)
if file_info:
print(f" [OK] 报告已成功上传到ERP项目资料(文件ID: {file_info.get('id')})")
else:
print(f" [X] 项目资料上传失败,请查看日志")
else:
print(f" [!] 跳过项目资料上传")
else:
print(f" [X] 报告上传失败,请查看日志")
else:
......
......@@ -333,6 +333,7 @@ ERP_MAX_RETRIES = 3
ERP_UPLOAD_IMAGE_URL = "/openclaw/upload/richtext" # 上传图片接口路径
ERP_CREATE_REPORT_URL = "/openclaw/report" # 创建报告接口路径
ERP_STUFF_URL = "/openclaw/stuff" # 获取人员列表接口路径
ERP_UPLOAD_PROJECT_URL = "/openclaw/upload/project" # 项目资料上传接口路径
# 请求超时时间(秒)
ERP_REQUEST_TIMEOUT = 30
......
......@@ -28,6 +28,7 @@ from src.config import (
ERP_UPLOAD_IMAGE_URL,
ERP_CREATE_REPORT_URL,
ERP_STUFF_URL,
ERP_UPLOAD_PROJECT_URL,
ERP_REQUEST_TIMEOUT,
ERP_SAVE_IMAGES_TEMP,
ERP_VALIDATE_IMAGES,
......@@ -313,7 +314,7 @@ def upload_image_to_erp(img_bytes: bytes, idx: int, logger: logging.Logger) -> O
return None
def create_report_in_erp(content: str, logger: logging.Logger, developtesting_id: int = None, copyuser_list: Optional[list] = None) -> Optional[int]:
def create_report_in_erp(content: str, logger: logging.Logger, developtesting_id: int = None, copyuser_list: Optional[list] = None, createuser_id: Optional[int] = None) -> Optional[int]:
"""
在ERP中创建测试报告(含重试机制)
......@@ -322,6 +323,7 @@ def create_report_in_erp(content: str, logger: logging.Logger, developtesting_id
logger: 日志记录器
developtesting_id: 测试单ID(可选,默认使用config中的配置)
copyuser_list: 抄送人ID列表(可选,默认为空列表)
createuser_id: 创建人ID(可选,用于指定报告创建者)
Returns:
报告ID,失败返回None
......@@ -347,6 +349,10 @@ def create_report_in_erp(content: str, logger: logging.Logger, developtesting_id
'copyuserList': copyuser_list
}
# 添加创建人ID参数(如果提供)
if createuser_id is not None:
data['createuser_id'] = createuser_id
headers = {
'X-Api-Key': ERP_API_KEY,
'Content-Type': 'application/json'
......@@ -362,6 +368,8 @@ def create_report_in_erp(content: str, logger: logging.Logger, developtesting_id
logger.info(f" - content: {len(content)} 字符 (HTML)")
logger.info(f" - descript: (空)")
logger.info(f" - copyuserList: {copyuser_list}")
if createuser_id is not None:
logger.info(f" - createuser_id: {createuser_id}")
# 发送请求
resp = requests.post(
......@@ -498,7 +506,7 @@ def word_to_html_with_images(file_path: str, logger: logging.Logger) -> Optional
return None
def upload_report_to_erp(file_path: str, logger: logging.Logger = None, developtesting_id: int = None, copyuser_list: Optional[list] = None) -> bool:
def upload_report_to_erp(file_path: str, logger: logging.Logger = None, developtesting_id: int = None, copyuser_list: Optional[list] = None, createuser_id: Optional[int] = None) -> bool:
"""
上传报告到ERP的完整流程
......@@ -507,6 +515,7 @@ def upload_report_to_erp(file_path: str, logger: logging.Logger = None, developt
logger: 日志记录器(可选,默认创建新的logger)
developtesting_id: 测试单ID(可选,默认使用config中的配置)
copyuser_list: 抄送人ID列表(可选,默认为空列表)
createuser_id: 创建人ID(可选,用于指定报告创建者)
Returns:
是否上传成功
......@@ -538,7 +547,7 @@ def upload_report_to_erp(file_path: str, logger: logging.Logger = None, developt
# 2. 创建ERP报告
logger.info(f"[步骤2/2] 创建ERP测试报告")
report_id = create_report_in_erp(html_content, logger, developtesting_id=developtesting_id, copyuser_list=copyuser_list)
report_id = create_report_in_erp(html_content, logger, developtesting_id=developtesting_id, copyuser_list=copyuser_list, createuser_id=createuser_id)
if report_id is None:
logger.error("✗ ERP报告创建失败")
return False
......@@ -550,17 +559,127 @@ def upload_report_to_erp(file_path: str, logger: logging.Logger = None, developt
return True
def ask_upload_confirmation_cli(report_path: str) -> Tuple[bool, Optional[int], Optional[list]]:
def upload_file_to_project(file_path: str, project_id: int, name: str, logger: logging.Logger = None) -> Optional[dict]:
"""
控制台交互:询问用户是否上传报告到ERP,输入测试单ID和抄送人
上传文件到ERP项目资料(含重试机制)
Args:
file_path: 要上传的文件路径
project_id: 关联的项目ID
name: 文件名称
logger: 日志记录器(可选,默认创建新的logger)
Returns:
上传成功返回文件信息字典,失败返回None
文件信息格式: {'id': int, 'name': str, 'path': str, 'type': str, 'size': int, 'project_id': int}
"""
# 如果没有提供logger,创建新的logger
if logger is None:
logger = setup_logger()
url = f'{ERP_BASE_URL}{ERP_UPLOAD_PROJECT_URL}'
logger.info("=" * 50)
logger.info("项目资料上传流程开始")
logger.info(f"文件路径: {file_path}")
logger.info(f"项目ID: {project_id}")
logger.info(f"文件名称: {name}")
logger.info("=" * 50)
# 验证文件存在
if not os.path.exists(file_path):
logger.error(f"✗ 文件不存在: {file_path}")
return None
# 获取文件大小
file_size = os.path.getsize(file_path)
logger.info(f"文件大小: {file_size} bytes ({file_size / 1024:.2f} KB)")
for attempt in range(ERP_MAX_RETRIES):
try:
# 准备请求参数
data = {
'project_id': project_id,
'name': name
}
headers = {'X-Api-Key': ERP_API_KEY}
# 打印请求信息
logger.info(f"=== 项目资料上传请求 ===")
logger.info(f"请求URL: {url}")
logger.info(f"请求Headers: X-Api-Key={ERP_API_KEY[:10]}...")
logger.info(f"请求参数:")
logger.info(f" - project_id: {project_id}")
logger.info(f" - name: {name}")
logger.info(f" - file: {file_path}")
# 读取文件并上传
with open(file_path, 'rb') as f:
files = {'file': (os.path.basename(file_path), f, 'application/octet-stream')}
# 发送请求
resp = requests.post(
url,
headers=headers,
files=files,
data=data,
timeout=ERP_REQUEST_TIMEOUT
)
# 打印响应信息
logger.info(f"响应状态码: {resp.status_code}")
logger.info(f"响应内容: {resp.text}")
result = resp.json()
if result.get('success') == 1:
file_info = result['data']
logger.info(f"✓ 项目资料上传成功!")
logger.info(f" - 文件ID: {file_info.get('id')}")
logger.info(f" - 文件名称: {file_info.get('name')}")
logger.info(f" - 存储路径: {file_info.get('path')}")
logger.info(f" - 文件类型: {file_info.get('type')}")
logger.info(f" - 文件大小: {file_info.get('size')} bytes")
logger.info(f" - 项目ID: {file_info.get('project_id')}")
return file_info
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:
logger.warning(f"项目资料上传超时,{ERP_RETRY_INTERVAL}秒后重试({attempt+1}/{ERP_MAX_RETRIES})")
if attempt < ERP_MAX_RETRIES - 1:
time.sleep(ERP_RETRY_INTERVAL)
else:
logger.error(f"✗ 项目资料上传超时(已达最大重试次数)")
except Exception as e:
logger.error(f"✗ 项目资料上传异常: {str(e)}")
break
return None
def ask_upload_confirmation_cli(report_path: str) -> Tuple[bool, Optional[int], Optional[list], Optional[int]]:
"""
控制台交互:询问用户是否上传报告到ERP,输入测试单ID、抄送人和创建人
Args:
report_path: 报告文件路径
Returns:
元组 (用户选择, 测试单ID, 抄送人ID列表)
- (True, int, list) 用户确认上传
- (False, None, None) 用户选择不上传
元组 (用户选择, 测试单ID, 抄送人ID列表, 创建人ID)
- (True, int, list, int) 用户确认上传
- (False, None, None, None) 用户选择不上传
"""
while True:
print(f"\n{'='*50}")
......@@ -608,8 +727,90 @@ def ask_upload_confirmation_cli(report_path: str) -> Tuple[bool, Optional[int],
else:
print(" 获取人员列表失败,将以空列表提交")
copyuser_ids = []
else:
copyuser_ids = []
# 输入创建人姓名
creator_id = None
creator_input = input("请输入创建人姓名(直接回车跳过): ").strip()
if creator_input:
# 如果之前没有获取人员列表,现在获取
if 'stuff_list' not in locals() or stuff_list is None:
logger = setup_logger()
stuff_list = get_stuff_list(logger)
if stuff_list:
creator_ids = match_names_to_ids([creator_input], stuff_list, logger)
if creator_ids:
creator_id = creator_ids[0]
print(f" 匹配到创建人ID: {creator_id}")
else:
print(" 未匹配到创建人,将以默认提交")
else:
print(" 获取人员列表失败,将以默认提交")
return True, testing_id, copyuser_ids, creator_id
elif choice == 'N':
return False, None, None, None
else:
print("输入错误,请输入 Y 或 N")
def ask_project_upload_confirmation_cli(report_path: str) -> Tuple[bool, Optional[int], Optional[str]]:
"""
控制台交互:询问用户是否上传报告到ERP项目资料
Args:
report_path: 报告文件路径
Returns:
元组 (用户选择, 项目ID, 文件名称)
- (True, int, str) 用户确认上传
- (False, None, None) 用户选择不上传
"""
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':
# 输入项目ID
project_id = None
while True:
id_input = input("请输入项目ID: ").strip()
if not id_input:
print("项目ID不能为空,请重新输入")
continue
try:
project_id = int(id_input)
if project_id > 0:
break
else:
print("项目ID必须为正整数,请重新输入")
except ValueError:
print("输入无效,请输入数字")
# 输入文件名称
file_name = None
while True:
name_input = input("请输入文件名称(直接回车使用原文件名): ").strip()
if not name_input:
# 使用原文件名(去掉扩展名)
import os
file_name = os.path.splitext(os.path.basename(report_path))[0]
print(f"使用原文件名: {file_name}")
break
else:
file_name = name_input
break
return True, testing_id, copyuser_ids
return True, project_id, file_name
elif choice == 'N':
return False, None, None
......
......@@ -500,31 +500,39 @@ class ReportGeneratorGUI:
def _ask_upload_to_erp(self, report_path: str):
"""
询问用户是否上传报告到ERP,并获取测试单ID和抄送人
询问用户是否上传报告到ERP,并获取测试单ID、抄送人、创建人和项目资料上传信息
Args:
report_path: 报告文件路径
"""
# 使用自定义对话框替代messagebox,确保按钮正常显示
confirmed, testing_id, copyuser_ids = self._show_upload_dialog(report_path)
confirmed, testing_id, copyuser_ids, creator_id, upload_project, project_id, file_name = self._show_upload_dialog(report_path)
if confirmed:
# 用户选择上传,透传测试单ID和抄送人列表
self._upload_to_erp(report_path, developtesting_id=testing_id, copyuser_list=copyuser_ids)
# 用户选择上传,透传测试单ID、抄送人列表和创建人ID,以及项目资料上传信息
self._upload_to_erp(
report_path,
developtesting_id=testing_id,
copyuser_list=copyuser_ids,
createuser_id=creator_id,
upload_project=upload_project,
project_id=project_id,
file_name=file_name
)
else:
self._log("[!] 跳过上传到ERP", "warning")
def _show_upload_dialog(self, report_path: str) -> tuple:
"""
显示自定义上传确认对话框(含测试单ID输入框和抄送人输入框
显示自定义上传确认对话框(含测试单ID输入框、抄送人输入框、创建人输入框和项目资料上传选项
Args:
report_path: 报告文件路径
Returns:
元组 (用户选择, 测试单ID, 抄送人ID列表)
- (True, int, list) 用户确认上传
- (False, None, None) 用户选择不上传
元组 (用户选择, 测试单ID, 抄送人ID列表, 创建人ID, 是否上传项目资料, 项目ID, 文件名称)
- (True, int, list, int, bool, int, str) 用户确认上传
- (False, None, None, None, False, None, None) 用户选择不上传
"""
# 导入ERP配置
from src.config import ERP_DEVELOPTESTING_ID
......@@ -538,8 +546,8 @@ class ReportGeneratorGUI:
dialog.transient(self.root)
dialog.grab_set()
# 先设置一个临时大小
dialog.geometry("600x380+300+200")
# 先设置一个临时大小(增加高度以容纳更多选项)
dialog.geometry("650x520+300+200")
# 消息内容
message_frame = tk.Frame(dialog, padx=30, pady=15)
......@@ -603,6 +611,26 @@ class ReportGeneratorGUI:
fg="gray"
).pack(side=tk.LEFT, padx=(10, 0))
# 创建人输入区域
creator_frame = tk.Frame(message_frame)
creator_frame.pack(anchor=tk.W, pady=(10, 0))
tk.Label(
creator_frame,
text="创建人:",
font=("微软雅黑", 10),
).pack(side=tk.LEFT)
creator_entry = tk.Entry(creator_frame, width=15, font=("微软雅黑", 10))
creator_entry.pack(side=tk.LEFT, padx=(5, 0))
tk.Label(
creator_frame,
text="(可空)",
font=("微软雅黑", 8),
fg="gray"
).pack(side=tk.LEFT, padx=(10, 0))
# 获取人员列表按钮和展示区域
stuff_frame = tk.Frame(message_frame)
stuff_frame.pack(anchor=tk.W, pady=(5, 0))
......@@ -657,12 +685,71 @@ class ReportGeneratorGUI:
padx=10
).pack(side=tk.LEFT, padx=(0, 5))
# 项目资料上传区域
project_frame = tk.Frame(message_frame)
project_frame.pack(anchor=tk.W, pady=(15, 0))
# 项目资料上传复选框
project_var = tk.BooleanVar()
project_check = tk.Checkbutton(
project_frame,
text="同时上传到项目资料",
variable=project_var,
font=("微软雅黑", 10),
command=lambda: on_project_check_changed()
)
project_check.pack(anchor=tk.W)
# 项目资料输入容器(初始隐藏)
project_inputs_frame = tk.Frame(project_frame)
# project_inputs_frame.pack() # 根据复选框状态显示/隐藏
def on_project_check_changed():
"""复选框状态变化时的处理"""
if project_var.get():
project_inputs_frame.pack(anchor=tk.W, pady=(10, 0), padx=(20, 0))
else:
project_inputs_frame.pack_forget()
# 项目ID输入
project_id_frame = tk.Frame(project_inputs_frame)
project_id_frame.pack(anchor=tk.W, pady=(0, 5))
tk.Label(
project_id_frame,
text="项目ID:",
font=("微软雅黑", 9),
).pack(side=tk.LEFT)
project_id_entry = tk.Entry(project_id_frame, width=12, font=("微软雅黑", 9))
project_id_entry.pack(side=tk.LEFT, padx=(5, 0))
# 文件名称输入
file_name_frame = tk.Frame(project_inputs_frame)
file_name_frame.pack(anchor=tk.W, pady=(0, 5))
tk.Label(
file_name_frame,
text="文件名称:",
font=("微软雅黑", 9),
).pack(side=tk.LEFT)
file_name_entry = tk.Entry(file_name_frame, width=20, font=("微软雅黑", 9))
file_name_entry.pack(side=tk.LEFT, padx=(5, 0))
tk.Label(
file_name_frame,
text="(留空使用原文件名)",
font=("微软雅黑", 8),
fg="gray"
).pack(side=tk.LEFT, padx=(10, 0))
# 按钮区域(放在dialog的底部,固定高度)
button_frame = tk.Frame(dialog, bg="#f0f0f0", height=60)
button_frame.pack(side=tk.BOTTOM, fill=tk.X)
# 结果存储: [confirmed, testing_id, copyuser_ids]
result = [False, None, None]
# 结果存储: [confirmed, testing_id, copyuser_ids, creator_id, upload_project, project_id, file_name]
result = [False, None, None, None, False, None, None]
def on_yes():
"""确认上传"""
......@@ -685,6 +772,7 @@ class ReportGeneratorGUI:
# 获取抄送人输入并匹配ID
copyuser_ids = None
copyuser_input = copyuser_entry.get().strip()
stuff_list = None # 用于后续创建人匹配
if copyuser_input:
try:
from src.erp_uploader import get_stuff_list, match_names_to_ids, setup_logger
......@@ -699,10 +787,64 @@ class ReportGeneratorGUI:
copyuser_ids = []
except Exception:
copyuser_ids = []
else:
copyuser_ids = []
# 获取创建人输入并匹配ID
creator_id = None
creator_input = creator_entry.get().strip()
if creator_input:
try:
from src.erp_uploader import get_stuff_list, match_names_to_ids, setup_logger
# 如果之前没有获取人员列表,现在获取
if stuff_list is None:
logger = setup_logger()
stuff_list = get_stuff_list(logger)
if stuff_list:
creator_ids = match_names_to_ids([creator_input], stuff_list, logger)
if creator_ids:
creator_id = creator_ids[0]
except Exception:
pass
# 获取项目资料上传信息
upload_project = project_var.get()
project_id = None
file_name = None
if upload_project:
# 验证项目ID
project_id_input = project_id_entry.get().strip()
if not project_id_input:
from tkinter import messagebox
messagebox.showwarning("输入错误", "请输入项目ID", parent=dialog)
return
try:
project_id = int(project_id_input)
if project_id <= 0:
from tkinter import messagebox
messagebox.showwarning("输入错误", "项目ID必须为正整数", parent=dialog)
return
except ValueError:
from tkinter import messagebox
messagebox.showwarning("输入错误", "请输入有效的项目ID", parent=dialog)
return
# 获取文件名称
file_name_input = file_name_entry.get().strip()
if not file_name_input:
# 使用原文件名(去掉扩展名)
import os
file_name = os.path.splitext(os.path.basename(report_path))[0]
else:
file_name = file_name_input
result[0] = True
result[1] = testing_id
result[2] = copyuser_ids
result[3] = creator_id
result[4] = upload_project
result[5] = project_id
result[6] = file_name
dialog.destroy()
def on_no():
......@@ -710,6 +852,10 @@ class ReportGeneratorGUI:
result[0] = False
result[1] = None
result[2] = None
result[3] = None
result[4] = False
result[5] = None
result[6] = None
dialog.destroy()
tk.Button(
......@@ -753,7 +899,7 @@ class ReportGeneratorGUI:
# 等待对话框关闭
dialog.wait_window()
return result[0], result[1], result[2]
return result[0], result[1], result[2], result[3], result[4], result[5], result[6]
def _show_info_dialog(self, title: str, message: str):
"""
......@@ -843,7 +989,7 @@ class ReportGeneratorGUI:
# 等待对话框关闭
dialog.wait_window()
def _upload_to_erp(self, report_path: str, developtesting_id: int = None, copyuser_list: list = None):
def _upload_to_erp(self, report_path: str, developtesting_id: int = None, copyuser_list: list = None, createuser_id: int = None, upload_project: bool = False, project_id: int = None, file_name: str = None):
"""
上传报告到ERP
......@@ -851,6 +997,10 @@ class ReportGeneratorGUI:
report_path: 报告文件路径
developtesting_id: 测试单ID(可选,默认使用config中的配置)
copyuser_list: 抄送人ID列表(可选,默认为空列表)
createuser_id: 创建人ID(可选,用于指定报告创建者)
upload_project: 是否上传到项目资料(可选)
project_id: 项目ID(上传项目资料时需要)
file_name: 文件名称(上传项目资料时需要)
"""
try:
from src.erp_uploader import upload_report_to_erp, setup_logger
......@@ -864,7 +1014,7 @@ class ReportGeneratorGUI:
# 在后台线程中上传
thread = threading.Thread(
target=self._upload_to_erp_thread,
args=(report_path, logger, developtesting_id, copyuser_list),
args=(report_path, logger, developtesting_id, copyuser_list, createuser_id, upload_project, project_id, file_name),
daemon=True
)
thread.start()
......@@ -876,7 +1026,7 @@ class ReportGeneratorGUI:
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, developtesting_id: int = None, copyuser_list: list = None):
def _upload_to_erp_thread(self, report_path: str, logger, developtesting_id: int = None, copyuser_list: list = None, createuser_id: int = None, upload_project: bool = False, project_id: int = None, file_name: str = None):
"""
在后台线程中上传报告到ERP
......@@ -885,12 +1035,37 @@ class ReportGeneratorGUI:
logger: 日志记录器
developtesting_id: 测试单ID(可选,默认使用config中的配置)
copyuser_list: 抄送人ID列表(可选,默认为空列表)
createuser_id: 创建人ID(可选,用于指定报告创建者)
upload_project: 是否上传到项目资料(可选)
project_id: 项目ID(上传项目资料时需要)
file_name: 文件名称(上传项目资料时需要)
"""
try:
success = upload_report_to_erp(report_path, logger, developtesting_id=developtesting_id, copyuser_list=copyuser_list)
from src.erp_uploader import upload_report_to_erp
success = upload_report_to_erp(report_path, logger, developtesting_id=developtesting_id, copyuser_list=copyuser_list, createuser_id=createuser_id)
if success:
self._log("[OK] 报告已成功上传到ERP", "success")
# 如果需要上传到项目资料
if upload_project and project_id and file_name:
self._log(f"[INFO] 开始上传到项目资料(项目ID: {project_id})...", "info")
try:
from src.erp_uploader import upload_file_to_project
file_info = upload_file_to_project(report_path, project_id, file_name, logger)
if file_info:
self._log(f"[OK] 报告已成功上传到ERP项目资料(文件ID: {file_info.get('id')})", "success")
# 在主线程中显示成功消息(包含项目资料上传)
self.root.after(0, lambda: self._show_info_dialog("成功", "报告已成功上传到ERP和项目资料!"))
else:
self._log("[X] 项目资料上传失败,请查看日志", "error")
# 在主线程中显示部分成功消息
self.root.after(0, lambda: self._show_info_dialog("部分成功", "报告已上传到ERP,但项目资料上传失败,请查看日志"))
except Exception as proj_e:
proj_err_msg = str(proj_e)
self._log(f"[X] 项目资料上传异常: {proj_err_msg}", "error")
self.root.after(0, lambda msg=proj_err_msg: self._show_info_dialog("部分成功", f"报告已上传到ERP,但项目资料上传异常:\n{msg}"))
else:
# 在主线程中显示成功消息
self.root.after(0, lambda: self._show_info_dialog("成功", "报告已成功上传到ERP!"))
else:
......
# -*- coding: utf-8 -*-
"""
项目资料上传测试工具
独立GUI程序,用于测试ERP项目资料上传功能
"""
import tkinter as tk
from tkinter import filedialog, messagebox, scrolledtext
import threading
import sys
import logging
from pathlib import Path
import os
# 添加src目录到路径
sys.path.insert(0, str(Path(__file__).parent / "src"))
from src.erp_uploader import upload_file_to_project, setup_logger
class GUILogHandler(logging.Handler):
"""GUI日志处理器,将日志输出到GUI"""
def __init__(self, log_func):
super().__init__()
self.log_func = log_func
def emit(self, record):
"""发送日志记录"""
try:
msg = self.format(record)
level = record.levelno
# 根据日志级别设置标签
if level >= logging.ERROR:
tag = "error"
elif level >= logging.WARNING:
tag = "warning"
elif level >= logging.INFO:
tag = "info"
else:
tag = "info"
self.log_func(msg, tag)
except Exception:
pass
class ProjectUploadTestGUI:
"""项目资料上传测试GUI"""
def __init__(self, root):
self.root = root
self.root.title("项目资料上传测试工具")
self.root.geometry("700x600")
# 文件路径
self.file_path = tk.StringVar()
# 项目ID
self.project_id = tk.StringVar()
# 文件名称
self.file_name = tk.StringVar()
# 创建界面
self._create_widgets()
def _create_widgets(self):
"""创建界面组件"""
# 标题
title_frame = tk.Frame(self.root, bg="#3498db", height=60)
title_frame.pack(side=tk.TOP, fill=tk.X)
title_frame.pack_propagate(False)
tk.Label(
title_frame,
text="项目资料上传测试工具",
font=("微软雅黑", 16, "bold"),
bg="#3498db",
fg="white"
).pack(pady=15)
# 主内容区域
main_frame = tk.Frame(self.root, padx=30, pady=20)
main_frame.pack(fill=tk.BOTH, expand=True)
# 文件选择区域
file_frame = tk.LabelFrame(main_frame, text="文件选择", font=("微软雅黑", 10), padx=15, pady=10)
file_frame.pack(fill=tk.X, pady=(0, 15))
tk.Label(file_frame, text="选择文件:", font=("微软雅黑", 9)).grid(row=0, column=0, sticky=tk.W, pady=5)
file_entry = tk.Entry(file_frame, textvariable=self.file_path, width=50, font=("微软雅黑", 9))
file_entry.grid(row=0, column=1, sticky=tk.EW, padx=(10, 5), pady=5)
tk.Button(
file_frame,
text="浏览...",
command=self._browse_file,
width=10,
font=("微软雅黑", 9)
).grid(row=0, column=2, pady=5)
file_frame.grid_columnconfigure(1, weight=1)
# 参数输入区域
param_frame = tk.LabelFrame(main_frame, text="上传参数", font=("微软雅黑", 10), padx=15, pady=10)
param_frame.pack(fill=tk.X, pady=(0, 15))
# 项目ID
tk.Label(param_frame, text="项目ID:", font=("微软雅黑", 9)).grid(row=0, column=0, sticky=tk.W, pady=8)
project_id_entry = tk.Entry(param_frame, textvariable=self.project_id, width=20, font=("微软雅黑", 9))
project_id_entry.grid(row=0, column=1, sticky=tk.W, padx=(10, 0), pady=8)
tk.Label(
param_frame,
text="*必填,关联的项目ID",
font=("微软雅黑", 8),
fg="gray"
).grid(row=0, column=2, sticky=tk.W, padx=(10, 0), pady=8)
# 文件名称
tk.Label(param_frame, text="文件名称:", font=("微软雅黑", 9)).grid(row=1, column=0, sticky=tk.W, pady=8)
file_name_entry = tk.Entry(param_frame, textvariable=self.file_name, width=30, font=("微软雅黑", 9))
file_name_entry.grid(row=1, column=1, columnspan=2, sticky=tk.EW, padx=(10, 0), pady=8)
tk.Label(
param_frame,
text="*必填,留空则使用原文件名",
font=("微软雅黑", 8),
fg="gray"
).grid(row=1, column=3, sticky=tk.W, padx=(10, 0), pady=8)
# 日志区域
log_frame = tk.LabelFrame(main_frame, text="上传日志", font=("微软雅黑", 10), padx=15, pady=10)
log_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 15))
self.log_text = scrolledtext.ScrolledText(
log_frame,
font=("Consolas", 9),
height=15,
state=tk.DISABLED,
bg="#f5f5f5"
)
self.log_text.pack(fill=tk.BOTH, expand=True)
# 配置日志颜色标签
self.log_text.tag_config("info", foreground="black")
self.log_text.tag_config("success", foreground="#04AA6D")
self.log_text.tag_config("error", foreground="#e74c3c")
self.log_text.tag_config("warning", foreground="#f39c12")
# 按钮区域
button_frame = tk.Frame(main_frame)
button_frame.pack(fill=tk.X)
tk.Button(
button_frame,
text="开始上传",
command=self._start_upload,
width=15,
height=2,
font=("微软雅黑", 11, "bold"),
bg="#04AA6D",
fg="white",
cursor="hand2",
relief=tk.RAISED
).pack(side=tk.LEFT, padx=(0, 10))
tk.Button(
button_frame,
text="清空日志",
command=self._clear_log,
width=15,
height=2,
font=("微软雅黑", 10),
bg="#95a5a6",
fg="white",
cursor="hand2",
relief=tk.RAISED
).pack(side=tk.LEFT, padx=(0, 10))
tk.Button(
button_frame,
text="退出",
command=self._exit,
width=15,
height=2,
font=("微软雅黑", 10),
bg="#e74c3c",
fg="white",
cursor="hand2",
relief=tk.RAISED
).pack(side=tk.LEFT)
def _browse_file(self):
"""浏览文件"""
file_path = filedialog.askopenfilename(
title="选择要上传的文件",
filetypes=[
("Word文档", "*.docx"),
("PDF文档", "*.pdf"),
("所有文件", "*.*")
]
)
if file_path:
self.file_path.set(file_path)
# 自动设置文件名称(去掉扩展名)
if not self.file_name.get():
name = Path(file_path).stem
self.file_name.set(name)
self._log(f"已选择文件: {file_path}", "info")
def _log(self, message: str, tag: str = "info"):
"""输出日志"""
self.log_text.config(state=tk.NORMAL)
self.log_text.insert(tk.END, message + "\n", tag)
self.log_text.see(tk.END)
self.log_text.config(state=tk.DISABLED)
self.root.update_idletasks()
def _clear_log(self):
"""清空日志"""
self.log_text.config(state=tk.NORMAL)
self.log_text.delete(1.0, tk.END)
self.log_text.config(state=tk.DISABLED)
def _validate_inputs(self):
"""验证输入"""
# 验证文件路径
file_path = self.file_path.get().strip()
if not file_path:
messagebox.showwarning("输入错误", "请选择要上传的文件")
return False
if not Path(file_path).exists():
messagebox.showwarning("输入错误", f"文件不存在: {file_path}")
return False
# 验证项目ID
project_id_str = self.project_id.get().strip()
if not project_id_str:
messagebox.showwarning("输入错误", "请输入项目ID")
return False
try:
project_id = int(project_id_str)
if project_id <= 0:
messagebox.showwarning("输入错误", "项目ID必须为正整数")
return False
except ValueError:
messagebox.showwarning("输入错误", "项目ID必须是数字")
return False
# 文件名称(可为空,使用原文件名)
file_name = self.file_name.get().strip()
if not file_name:
file_name = Path(file_path).stem
self.file_name.set(file_name)
self._log(f"使用原文件名: {file_name}", "info")
return True
def _start_upload(self):
"""开始上传"""
if not self._validate_inputs():
return
# 禁用上传按钮
# TODO: 可以添加按钮禁用逻辑
# 在后台线程中上传
thread = threading.Thread(
target=self._upload_thread,
daemon=True
)
thread.start()
def _upload_thread(self):
"""上传线程"""
try:
file_path = self.file_path.get().strip()
project_id = int(self.project_id.get().strip())
file_name = self.file_name.get().strip()
self._log("=" * 50, "info")
self._log("开始上传文件到ERP项目资料", "info")
self._log(f"文件路径: {file_path}", "info")
self._log(f"项目ID: {project_id}", "info")
self._log(f"文件名称: {file_name}", "info")
self._log("=" * 50, "info")
# 创建logger
logger = setup_logger()
# 添加GUI日志处理器
gui_handler = GUILogHandler(self._log)
logger.addHandler(gui_handler)
# 上传文件
file_info = upload_file_to_project(file_path, project_id, file_name, logger)
# 显示结果
self.root.after(0, lambda: self._show_result(file_info))
except Exception as e:
err_msg = str(e)
self._log(f"上传异常: {err_msg}", "error")
self.root.after(0, lambda msg=err_msg: messagebox.showerror("错误", f"上传异常:\n{msg}"))
def _show_result(self, file_info):
"""显示上传结果"""
if file_info:
self._log("=" * 50, "success")
self._log("✓ 上传成功!", "success")
self._log(f"文件ID: {file_info.get('id')}", "success")
self._log(f"文件名称: {file_info.get('name')}", "success")
self._log(f"存储路径: {file_info.get('path')}", "success")
self._log(f"文件类型: {file_info.get('type')}", "success")
self._log(f"文件大小: {file_info.get('size')} bytes", "success")
self._log(f"项目ID: {file_info.get('project_id')}", "success")
self._log("=" * 50, "success")
messagebox.showinfo("成功", f"文件上传成功!\n文件ID: {file_info.get('id')}")
else:
self._log("=" * 50, "error")
self._log("✗ 上传失败", "error")
self._log("=" * 50, "error")
messagebox.showerror("失败", "文件上传失败,请查看日志了解详情")
def _exit(self):
"""退出程序"""
if messagebox.askyesno("退出", "确定要退出吗?"):
self.root.quit()
self.root.destroy()
def main():
"""主函数"""
root = tk.Tk()
app = ProjectUploadTestGUI(root)
root.mainloop()
if __name__ == "__main__":
main()
# OpenClaw API - 项目资料上传接口调用说明
> 文档版本:v1.0
>
> 更新时间:2026-06-16
>
> 适用对象:第三方系统对接开发人员
>
> 对应页面:`/Erp/ProjectManagement/ProjectionInfo` 项目资料页签
---
## 一、概述
本文档描述如何通过 OpenClaw API 向 ERP 项目的 **项目资料** 模块上传文件,包含以下两个功能(与页面上两个上传入口对应):
- **协作文档上传**:上传文件并创建 OnlyOffice 在线协作文档(对应页面"协作文档"上传)
- **项目资料上传**:上传项目资料文件并关联到项目(对应页面"上传文档资料")
两个接口均仅通过 **API Key** 认证,支持第三方系统(如 OpenClaw)自动化上传。
---
## 二、接口基础信息
### 2.1 请求地址
| 环境 | 地址 |
|------|------|
| 生产环境 | `https://office.ubainsyun.com:5082/api/uerp` |
| 测试环境 | `http://127.0.0.1:8002/uerp` |
### 2.2 认证方式
所有接口需要在请求头中携带 API Key:
```
X-Api-Key: your_api_key_here
```
### 2.3 请求格式
| 请求类型 | Content-Type |
|----------|--------------|
| 文件上传 | `multipart/form-data` |
### 2.4 响应格式
所有接口返回 JSON 格式:
```json
{
"success": 1, // 1=成功, 0=失败
"data": { ... }, // 成功时返回数据
"error": 40000001, // 失败时的错误码
"msg": "错误信息" // 失败时的错误描述
}
```
---
## 三、接口列表
| 序号 | 接口名称 | 方法 | 路径 | api_name | 说明 |
|------|----------|------|------|----------|------|
| 1 | 协作文档上传 | POST | `/openclaw/upload/cooperation` | `upload_cooperation` | 上传文件并创建 OnlyOffice 协作文档 |
| 2 | 项目资料上传 | POST | `/openclaw/upload/project` | `upload_project` | 上传文件并关联到项目 |
> `api_name` 用于 API Key 白名单授权,创建 Key 时需勾选对应权限。
---
## 四、接口详情
### 4.1 协作文档上传
上传文件并创建 OnlyOffice 在线协作文档,文件会同时记录到项目的协作文档列表中。
**请求**
```
POST /openclaw/upload/cooperation
Content-Type: multipart/form-data
```
**请求头**
```
X-Api-Key: your_api_key
```
**请求参数**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| file | file | 是 | 上传的文件(推荐 `.docx` `.xlsx` `.pptx` `.pdf`) |
| project_id | int | 是 | 关联的项目 ID |
| name | string | 否 | 文件名称,默认使用原文件名 |
| group_id | int | 否 | 分组 ID,可将文件归类到指定分组 |
**支持的文件类型与 fileType 映射**
| 扩展名 | fileType |
|--------|----------|
| `.docx` | word |
| `.xlsx` | excel |
| `.pptx` | ppt |
| `.pdf` | pdf |
| 其他 | 默认 word |
**请求示例 (cURL)**
```bash
curl -X POST "https://office.ubainsyun.com:5015/uerp/openclaw/upload/cooperation" \
-H "X-Api-Key: your_api_key" \
-F "file=@/path/to/项目方案.docx" \
-F "project_id=100" \
-F "name=项目协作文档"
```
**响应示例**
```json
{
"success": 1,
"data": {
"id": 61,
"file_id": 12355,
"name": "项目协作文档",
"fileKey": "7e8843c9904859da7442399f33d6eb52",
"filePath": "media/uerp/html/onlyoffice/1/7e8843c9904859da7442399f33d6eb52.docx",
"fileType": "word",
"fileSize": 104,
"sharePath": "7e8843c9904859da7442399f33d6eb52",
"project_id": 8
}
}
```
**字段说明**
| 字段 | 类型 | 说明 |
|------|------|------|
| id | int | 协作文档记录 ID(Personfile 表) |
| file_id | int | 源文件记录 ID(Producefile 表) |
| name | string | 文件名称 |
| fileKey | string | OnlyOffice 协作密钥,唯一标识协作会话 |
| filePath | string | OnlyOffice 协作文件相对路径 |
| fileType | string | 文件类型(word/excel/ppt/pdf) |
| fileSize | int | 文件大小(字节) |
| sharePath | string | 文件分享路径(与 fileKey 一致) |
| project_id | int | 关联的项目 ID |
**业务逻辑**
1. 文件上传到 `Producefile` 表(fileGroup_id=1)
2. 创建 `Personfile` 协作文档记录(userType=2 项目文件),关联项目
3. 生成 `fileKey`(MD5)并设置 OnlyOffice 协作路径
4. 可选归类到分组(group_id)
5. 返回协作文档信息,用户可在页面打开 OnlyOffice 在线协作
---
### 4.2 项目资料上传
上传项目资料文件并关联到项目,文件会出现在项目的"项目资料"列表中。
**请求**
```
POST /openclaw/upload/project
Content-Type: multipart/form-data
```
**请求头**
```
X-Api-Key: your_api_key
```
**请求参数**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| file | file | 是 | 上传的文件(任意类型) |
| project_id | int | 是 | 关联的项目 ID |
| name | string | 否 | 文件名称,默认使用原文件名 |
**请求示例 (cURL)**
```bash
curl -X POST "https://office.ubainsyun.com:5015/uerp/openclaw/upload/project" \
-H "X-Api-Key: your_api_key" \
-F "file=@/path/to/需求文档.pdf" \
-F "project_id=100" \
-F "name=项目需求文档"
```
**响应示例**
```json
{
"success": 1,
"data": {
"id": 12356,
"name": "项目需求文档",
"path": "media/uerp/productfile/20260616232417490.pdf",
"type": ".pdf",
"size": 27,
"project_id": 8
}
}
```
**字段说明**
| 字段 | 类型 | 说明 |
|------|------|------|
| id | int | 文件记录 ID(Producefile 表) |
| name | string | 文件名称 |
| path | string | 文件存储相对路径 |
| type | string | 文件扩展名(含 `.`) |
| size | int | 文件大小(字节) |
| project_id | int | 关联的项目 ID |
**业务逻辑**
1. 验证项目是否存在
2. 文件上传到 `Producefile` 表(fileGroup_id=4 项目资料)
3. 通过 `Project.projectfile` 多对多关联文件到项目
4. 返回文件信息,文件出现在项目"项目资料"列表
---
## 五、两个接口对比
| 对比项 | 协作文档上传 | 项目资料上传 |
|--------|------------|------------|
| 路径 | `/openclaw/upload/cooperation` | `/openclaw/upload/project` |
| api_name | `upload_cooperation` | `upload_project` |
| fileGroup_id | 1(协作文档) | 4(项目资料) |
| 创建协作文档记录 | 是(支持 OnlyOffice 协作) | 否 |
| 关联项目方式 | Personfile.project_id | Project.projectfile(多对多) |
| 生成 fileKey | 是(用于协作) | 否 |
| 返回 fileKey/sharePath | 是 | 否 |
> **选用建议**:
> - 文件需要 **多人在线协作编辑**(Word/Excel/PPT/PDF)→ 用 **协作文档上传**
> - 文件仅作为 **项目资料存档**(任意格式,只读查看/下载)→ 用 **项目资料上传**
---
## 六、完整调用流程
```
┌─────────────────────────────────────────────────────────────────┐
│ 项目资料上传完整流程 │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 1. 确定上传目的 │
│ - 需在线协作编辑? → 协作文档上传 │
│ - 仅存档查看? → 项目资料上传 │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 2. 调用上传接口(multipart/form-data) │
│ POST /openclaw/upload/cooperation 或 │
│ POST /openclaw/upload/project │
│ 提交:file 文件 + project_id 项目ID │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 3. 获取返回结果 │
│ 协作文档:id + fileKey(可用于后续打开协作) │
│ 项目资料:id(可用于后续下载/查看) │
└─────────────────────────────────────────────────────────────────┘
```
---
## 七、代码示例
### 7.1 Python 示例
```python
import os
import requests
from typing import Optional
class OpenclawProjectUploadAPI:
"""OpenClaw 项目资料上传 API 客户端"""
def __init__(self, base_url: str, api_key: str):
"""
初始化客户端
Args:
base_url: API 基础地址,如 https://office.ubainsyun.com:5015/uerp
api_key: API Key(需授权 upload_cooperation / upload_project)
"""
self.base_url = base_url.rstrip('/')
self.api_key = api_key
def upload_cooperation(
self,
file_path: str,
project_id: int,
name: Optional[str] = None,
group_id: Optional[int] = None
) -> dict:
"""
上传协作文档(创建 OnlyOffice 协作文档)
Args:
file_path: 本地文件路径(推荐 .docx/.xlsx/.pptx/.pdf)
project_id: 关联项目 ID
name: 文件名称(可选)
group_id: 分组 ID(可选)
Returns:
协作文档信息,含 id、fileKey、filePath 等
"""
with open(file_path, 'rb') as f:
files = {'file': f}
data = {'project_id': project_id}
if name:
data['name'] = name
if group_id:
data['group_id'] = group_id
resp = requests.post(
f'{self.base_url}/openclaw/upload/cooperation',
headers={'X-Api-Key': self.api_key},
files=files,
data=data
)
result = resp.json()
if result.get('success') == 1:
return result['data']
raise Exception(f"协作文档上传失败: {result.get('msg')}")
def upload_project(
self,
file_path: str,
project_id: int,
name: Optional[str] = None
) -> dict:
"""
上传项目资料(关联到项目)
Args:
file_path: 本地文件路径(任意类型)
project_id: 关联项目 ID
name: 文件名称(可选)
Returns:
文件信息,含 id、path、type、size 等
"""
with open(file_path, 'rb') as f:
files = {'file': f}
data = {'project_id': project_id}
if name:
data['name'] = name
resp = requests.post(
f'{self.base_url}/openclaw/upload/project',
headers={'X-Api-Key': self.api_key},
files=files,
data=data
)
result = resp.json()
if result.get('success') == 1:
return result['data']
raise Exception(f"项目资料上传失败: {result.get('msg')}")
# ==================== 使用示例 ====================
def example_upload_cooperation():
"""示例:上传协作文档"""
api = OpenclawProjectUploadAPI(
base_url='https://office.ubainsyun.com:5015/uerp',
api_key='your_api_key_here'
)
result = api.upload_cooperation(
file_path='/path/to/项目方案.docx',
project_id=100,
name='项目协作文档'
)
print(f"协作文档上传成功! ID: {result['id']}, fileKey: {result['fileKey']}")
def example_upload_project():
"""示例:上传项目资料"""
api = OpenclawProjectUploadAPI(
base_url='https://office.ubainsyun.com:5015/uerp',
api_key='your_api_key_here'
)
result = api.upload_project(
file_path='/path/to/需求文档.pdf',
project_id=100,
name='项目需求文档'
)
print(f"项目资料上传成功! ID: {result['id']}, 路径: {result['path']}")
if __name__ == '__main__':
example_upload_cooperation()
example_upload_project()
```
### 7.2 cURL 示例
```bash
#!/bin/bash
# 配置
API_KEY="your_api_key_here"
BASE_URL="https://office.ubainsyun.com:5015/uerp"
PROJECT_ID=100
echo "=== 1. 上传协作文档(创建 OnlyOffice 协作)==="
curl -s -X POST "${BASE_URL}/openclaw/upload/cooperation" \
-H "X-Api-Key: ${API_KEY}" \
-F "file=@./项目方案.docx" \
-F "project_id=${PROJECT_ID}" \
-F "name=项目协作文档" | jq .
echo -e "\n=== 2. 上传项目资料(关联到项目)==="
curl -s -X POST "${BASE_URL}/openclaw/upload/project" \
-H "X-Api-Key: ${API_KEY}" \
-F "file=@./需求文档.pdf" \
-F "project_id=${PROJECT_ID}" \
-F "name=项目需求文档" | jq .
echo -e "\n完成!"
```
---
## 八、错误码说明
| 错误码 | 说明 | 触发场景 | 解决方案 |
|--------|------|----------|----------|
| 40000001 | 参数错误或业务异常 | 缺少 file / project_id;项目不存在;上传异常 | 检查请求参数是否正确 |
| 40000001 | 缺少 API Key | 未传 X-Api-Key | 请求头添加 X-Api-Key |
| 40000014 | 无效的 API Key | Key 不存在或已禁用 | 检查 Key 是否正确、是否启用 |
| 40000014 | API Key 已过期 | Key 超过有效期 | 联系管理员延长有效期 |
| 40000015 | 无权限访问该接口 | Key 未授权 `upload_cooperation` / `upload_project` | 联系管理员添加接口权限 |
---
## 九、注意事项
1. **API Key 权限**:创建 Key 时需勾选 `upload_cooperation`(协作文档上传)和/或 `upload_project`(项目资料上传)权限,否则会返回 `40000015 无权限访问`
2. **协作文档文件类型**:协作文档上传推荐使用 OnlyOffice 支持的格式(`.docx` `.xlsx` `.pptx` `.pdf`),其他格式会默认按 word 类型处理。
3. **协作文档的 fileKey**:返回的 `fileKey` 是 OnlyOffice 协作会话的唯一标识,配合 `filePath` 可用于后续打开在线协作。
4. **创建人**:上传的文件创建人默认为 API Key 关联的用户,无需单独传创建人参数。
5. **项目资料关联**:项目资料上传通过项目的 `projectfile` 多对多关系关联,上传后即可在项目"项目资料"列表查看。
6. **文件存储**:文件统一存储到服务器 `media/uerp/productfile/` 目录(协作文档的 OnlyOffice 路径除外)。
7. **请求格式**:上传接口必须使用 `multipart/form-data`,不能使用 JSON。
---
## 十、自测验证记录
> 接口开发完成后已通过 Django 测试客户端完成自测,覆盖以下场景:
| 场景 | 接口 | 预期结果 | 实测结果 |
|------|------|----------|----------|
| 正常上传协作文档 | cooperation | success=1,返回 fileKey/filePath | 通过 |
| 缺少上传文件 | cooperation | success=0,提示"缺少上传文件" | 通过 |
| 缺少项目ID | cooperation | success=0,提示"缺少项目ID" | 通过 |
| 正常上传项目资料 | project | success=1,文件关联到项目 | 通过 |
| 项目不存在 | project | success=0,提示"项目不存在" | 通过 |
| 缺少 API Key | project | success=0,提示"缺少 API Key" | 通过 |
| 无效 API Key | project | success=0,error=40000014 | 通过 |
| 权限不足 | project | success=0,error=40000015 | 通过 |
---
## 十一、联系支持
如有问题,请联系 ERP 系统管理员获取技术支持。
# 问题描述
## 问题现象
- 在执行代码后GUI模式下上传ERP操作报错
# 报错日志信息
```ignorelang
# 问题描述
Exception in Tkinter callback
Traceback (most recent call last):
File "C:\Users\29194\AppData\Local\Programs\Python\Python310\lib\tkinter\__init__.py", line 1921, in __call__
return self.func(*args)
File "C:\Users\29194\AppData\Local\Programs\Python\Python310\lib\tkinter\__init__.py", line 839, in callit
func(*args)
File "E:\gihub\ubains-module-test\AuxiliaryTool\FunctionalTestReportGeneration\src\gui.py", line 481, in <lambda>
self.root.after(0, lambda rp=report_path: self._ask_upload_to_erp(rp))
File "E:\gihub\ubains-module-test\AuxiliaryTool\FunctionalTestReportGeneration\src\gui.py", line 509, in _ask_upload_to_erp
confirmed, testing_id, copyuser_ids, creator_id, upload_project, project_id, file_name = self._show_upload_dialog(report_path)
ValueError: not enough values to unpack (expected 7, got 3)
```
\ No newline at end of file
# GUI模式下上传ERP报错 - 问题处理执行计划
## 执行概述
### 问题背景
在GUI模式下执行ERP上传操作时,发生 `ValueError: not enough values to unpack (expected 7, got 3)` 错误。
**错误原因**
- 代码更新后,`_ask_upload_to_erp()` 函数期望 `_show_upload_dialog()` 返回7个值(包含项目资料上传参数)
-`_show_upload_dialog()` 函数仍然只返回3个值(旧版本返回值)
- 导致值解包时数量不匹配
### 执行目标
1. 更新 `_show_upload_dialog()` 函数,确保返回7个值
2. 确保所有相关的GUI上传流程函数参数一致
3. 验证修复后GUI模式能正常执行上传操作
### 涉及范围
- 代码路径:`AuxiliaryTool/FunctionalTestReportGeneration/src/gui.py`
- 影响功能:GUI模式下的ERP上传功能
---
## 任务分解与实施计划
### 任务1:分析问题根源
#### 1.1 任务详情
- **问题定位**:检查 `_show_upload_dialog()` 函数的返回值
- **状态**`[ ]`
#### 1.2 实施步骤
- [ ] 1.2.1 检查 `_show_upload_dialog()` 函数的当前返回语句
- [ ] 1.2.2 确认返回值的数量和结构
- [ ] 1.2.3 对比 `_ask_upload_to_erp()` 期望的返回值
#### 1.3 输出产物
- 问题分析报告
- 修复方案确认
---
### 任务2:修复 _show_upload_dialog() 函数
#### 2.1 任务详情
- **功能目标**:更新 `_show_upload_dialog()` 函数,返回7个值
- **期望返回值**`(confirmed, testing_id, copyuser_ids, creator_id, upload_project, project_id, file_name)`
- **状态**`[ ]`
#### 2.2 实施步骤
- [ ] 2.2.1 检查函数的 docstring 是否需要更新
- [ ] 2.2.2 检查 `result` 列表是否包含7个元素
- [ ] 2.2.3 确认 `on_yes()` 函数设置所有7个返回值
- [ ] 2.2.4 确认 `on_no()` 函数设置所有7个返回值
- [ ] 2.2.5 如果不一致,修复返回值设置
#### 2.3 输出产物
- 更新后的 `_show_upload_dialog()` 函数
---
### 任务3:验证参数一致性
#### 3.1 任务详情
- **功能目标**:确保所有相关函数的参数数量和类型一致
- **状态**`[ ]`
#### 3.2 实施步骤
- [ ] 3.2.1 验证 `_ask_upload_to_erp()` 的解包语句
- [ ] 3.2.2 验证 `_upload_to_erp()` 的参数列表
- [ ] 3.2.3 验证 `_upload_to_erp_thread()` 的参数列表
- [ ] 3.2.4 确保所有函数签名一致
#### 3.3 输出产物
- 参数一致性验证报告
---
### 任务4:测试验证
#### 4.1 任务详情
- **功能目标**:测试修复后的GUI上传功能
- **状态**`[ ]`
#### 4.2 实施步骤
- [ ] 4.2.1 启动GUI模式
- [ ] 4.2.2 生成测试报告
- [ ] 4.2.3 测试上传到ERP(不选项目资料)
- [ ] 4.2.4 测试上传到ERP(选择项目资料)
- [ ] 4.2.5 验证所有功能正常
#### 4.3 输出产物
- 测试验证报告
---
## 验收标准
### 功能验收标准
| 序号 | 验收项 | 验收标准 | 状态 |
|------|--------|----------|------|
| 1 | 函数返回值 | `_show_upload_dialog()` 返回7个值 | `[ ]` |
| 2 | 参数解包 | `_ask_upload_to_erp()` 正确解包7个值 | `[ ]` |
| 3 | 上传功能-基础 | GUI模式能正常上传报告到ERP | `[ ]` |
| 4 | 上传功能-项目资料 | GUI模式能正常上传报告到项目资料 | `[ ]` |
| 5 | 错误处理 | 各种输入错误有友好提示 | `[ ]` |
| 6 | 跳过功能 | 选择不上传能正常结束流程 | `[ ]` |
### 代码质量验收标准
| 序号 | 验收项 | 验收标准 | 状态 |
|------|--------|----------|------|
| 1 | 语法检查 | Python语法检查通过 | `[ ]` |
| 2 | 参数一致性 | 所有相关函数参数数量一致 | `[ ]` |
| 3 | 返回值一致性 | 所有返回分支返回值数量一致 | `[ ]` |
| 4 | 异常处理 | 所有异常情况都有处理 | `[ ]` |
---
## 测试计划
### 单元测试
| 测试项 | 测试内容 | 预期结果 | 状态 |
|--------|----------|----------|------|
| 返回值数量 | 检查 `_show_upload_dialog()` 返回值数量 | 返回7个值 | `[ ]` |
| 返回值类型 | 检查各返回值的类型 | 类型正确 | `[ ]` |
| on_yes分支 | 测试确认上传的返回值 | 返回7个有效值 | `[ ]` |
| on_no分支 | 测试取消上传的返回值 | 返回7个值(False和None) | `[ ]` |
### 集成测试
| 测试项 | 测试内容 | 预期结果 | 状态 |
|--------|----------|----------|------|
| GUI完整流程-不上传 | 启动GUI,生成报告,选择不上传 | 流程正常结束 | `[ ]` |
| GUI完整流程-仅上传ERP | 启动GUI,生成报告,仅上传到ERP | 上传成功 | `[ ]` |
| GUI完整流程-上传项目资料 | 启动GUI,生成报告,上传到ERP和项目资料 | 两处上传成功 | `[ ]` |
| 输入验证测试 | 测试各种无效输入 | 有友好提示 | `[ ]` |
---
## 风险评估
| 风险项 | 风险等级 | 影响范围 | 应对措施 |
|--------|----------|----------|----------|
| 返回值不完整 | 高 | 导致程序崩溃 | 检查所有返回分支 |
| 参数传递错误 | 中 | 上传功能异常 | 仔细检查参数顺序 |
| 逻辑错误 | 中 | 功能不符合预期 | 充分测试验证 |
---
## 实施记录
### 实施时间线
| 日期 | 任务 | 状态 | 备注 |
|------|------|------|------|
| - | 任务1:分析问题根源 | `[ ]` | - |
| - | 任务2:修复 _show_upload_dialog() 函数 | `[ ]` | - |
| - | 任务3:验证参数一致性 | `[ ]` | - |
| - | 任务4:测试验证 | `[ ]` | - |
### 问题记录
| 日期 | 问题描述 | 解决方案 | 状态 |
|------|----------|----------|------|
| - | - | - | `[ ]` |
---
## 根因分析
### 问题代码位置
**文件**`src/gui.py`
**行号**:481, 509
### 问题详情
```python
# 行481:调用 _ask_upload_to_erp
self.root.after(0, lambda rp=report_path: self._ask_upload_to_erp(rp))
# 行509:解包返回值
confirmed, testing_id, copyuser_ids, creator_id, upload_project, project_id, file_name = self._show_upload_dialog(report_path)
```
**期望返回**:7个值
**实际返回**:3个值
### 修复方案
确保 `_show_upload_dialog()` 函数返回7个值:
1. `confirmed` - 是否确认上传
2. `testing_id` - 测试单ID
3. `copyuser_ids` - 抄送人ID列表
4. `creator_id` - 创建人ID
5. `upload_project` - 是否上传到项目资料
6. `project_id` - 项目ID
7. `file_name` - 文件名称
---
## 后续工作
### 优化建议
- [ ] 添加更多GUI错误处理
- [ ] 添加GUI模式下的日志输出
- [ ] 考虑添加上传进度显示
### 防范措施
- [ ] 代码审查时注意函数签名一致性
- [ ] 添加单元测试覆盖关键函数
- [ ] 修改返回值时全面检查所有调用点
---
## 附录
### 参考文档
- 代码规范:`Docs/PRD/01规范文档/_PRD_规范文档_代码规范.md`
- 问题总结:`Docs/PRD/01规范文档/_PRD_问题总结_记录文档.md`
- 方法总结:`Docs/PRD/01规范文档/_PRD_方法总结_记录文档.md`
### 代码路径
- 主目录:`AuxiliaryTool/FunctionalTestReportGeneration`
- 问题文件:`src/gui.py`
### 错误日志
```
ValueError: not enough values to unpack (expected 7, got 3)
```
---
## 优化功能回填
> 此区域用于记录实施过程中的优化和改进
| 日期 | 优化内容 | 优化人 | 状态 |
|------|----------|--------|------|
| - | - | - | `[ ]` |
---
**文档版本**: 1.0
**创建日期**: 2026-06-17
**最后更新**: 2026-06-17
# 问题描述
## 问题现象
- 在执行代码后GUI模式下上传ERP操作报错提示未定义
# 报错日志信息
```ignorelang
开始上传报告到ERP(测试单ID: 445)...
[X] 上传异常: name 'upload_report_to_erp' is not defined
```
\ No newline at end of file
# GUI模式下上传ERP报错未定义 - 问题处理执行计划
## 执行概述
### 问题背景
在GUI模式下执行ERP上传操作时,发生 `NameError: name 'upload_report_to_erp' is not defined` 错误。
**错误原因**
- `_upload_to_erp_thread()` 函数中调用 `upload_report_to_erp()` 函数
- 但该函数没有在当前作用域中导入
- 导致运行时找不到该函数定义
### 执行目标
1. 确认 `upload_report_to_erp` 函数的调用位置
2. 添加正确的导入语句
3. 验证修复后GUI模式能正常执行上传操作
### 涉及范围
- 代码路径:`AuxiliaryTool/FunctionalTestReportGeneration/src/gui.py`
- 影响功能:GUI模式下的ERP上传功能
---
## 任务分解与实施计划
### 任务1:定位问题代码位置
#### 1.1 任务详情
- **问题定位**:找到调用 `upload_report_to_erp` 的代码位置
- **状态**`[ ]`
#### 1.2 实施步骤
- [ ] 1.2.1 搜索 `gui.py` 中所有调用 `upload_report_to_erp` 的位置
- [ ] 1.2.2 确认调用所在的函数
- [ ] 1.2.3 检查现有的导入语句
#### 1.3 输出产物
- 问题代码位置报告
---
### 任务2:添加导入语句
#### 2.1 任务详情
- **功能目标**:在正确的位置添加 `upload_report_to_erp` 函数的导入
- **导入模块**`from src.erp_uploader import upload_report_to_erp`
- **状态**`[ ]`
#### 2.2 实施步骤
- [ ] 2.2.1 确认导入语句应该添加的位置(函数内部或文件顶部)
- [ ] 2.2.2 添加导入语句
- [ ] 2.2.3 确保导入语句与其他ERP相关导入保持一致
#### 2.3 输出产物
- 更新后的导入语句
---
### 任务3:验证修复
#### 3.1 任务详情
- **功能目标**:验证修复后功能正常
- **状态**`[ ]`
#### 3.2 实施步骤
- [ ] 3.2.1 检查Python语法是否正确
- [ ] 3.2.2 启动GUI模式
- [ ] 3.2.3 测试ERP上传功能
- [ ] 3.2.4 验证不再出现 `NameError`
#### 3.3 输出产物
- 测试验证报告
---
## 验收标准
### 功能验收标准
| 序号 | 验收项 | 验收标准 | 状态 |
|------|--------|----------|------|
| 1 | 导入语句 | `upload_report_to_erp` 已正确导入 | `[ ]` |
| 2 | 语法检查 | Python语法检查通过 | `[ ]` |
| 3 | GUI上传功能 | GUI模式能正常上传报告到ERP | `[ ]` |
| 4 | 项目资料上传 | GUI模式能正常上传到项目资料 | `[ ]` |
| 5 | 错误处理 | 无NameError异常 | `[ ]` |
### 代码质量验收标准
| 序号 | 验收项 | 验收标准 | 状态 |
|------|--------|----------|------|
| 1 | 导入位置 | 导入语句位置合理 | `[ ]` |
| 2 | 代码风格 | 符合现有代码风格 | `[ ]` |
---
## 测试计划
### 单元测试
| 测试项 | 测试内容 | 预期结果 | 状态 |
|--------|----------|----------|------|
| 导入验证 | 检查导入语句是否正确 | 导入无错误 | `[ ]` |
| 函数调用 | 验证函数能正常调用 | 函数执行成功 | `[ ]` |
### 集成测试
| 测试项 | 测试内容 | 预期结果 | 状态 |
|--------|----------|----------|------|
| GUI完整流程 | 启动GUI,生成报告,上传到ERP | 上传成功,无报错 | `[ ]` |
---
## 风险评估
| 风险项 | 风险等级 | 影响范围 | 应对措施 |
|--------|----------|----------|----------|
| 导入位置错误 | 低 | 可能导致其他问题 | 参考现有导入语句的位置 |
| 循环导入 | 低 | 可能导致模块加载失败 | 确保导入语句在函数内部 |
---
## 实施记录
### 实施时间线
| 日期 | 任务 | 状态 | 备注 |
|------|------|------|------|
| - | 任务1:定位问题代码位置 | `[ ]` | - |
| - | 任务2:添加导入语句 | `[ ]` | - |
| - | 任务3:验证修复 | `[ ]` | - |
### 问题记录
| 日期 | 问题描述 | 解决方案 | 状态 |
|------|----------|----------|------|
| - | - | - | `[ ]` |
---
## 根因分析
### 问题代码位置
**文件**`src/gui.py`
**函数**`_upload_to_erp_thread()`
### 问题详情
```python
# 错误信息
NameError: name 'upload_report_to_erp' is not defined
```
**原因分析**
- `_upload_to_erp_thread()` 函数中直接调用 `upload_report_to_erp()`
- 但该函数没有在 `_upload_to_erp_thread()` 函数的作用域中导入
- 现有代码在 `_upload_to_erp()` 函数中有导入,但 `_upload_to_erp_thread()` 是独立执行的线程函数,无法访问该导入
### 修复方案
`_upload_to_erp_thread()` 函数开始处添加导入语句:
```python
from src.erp_uploader import upload_report_to_erp
```
---
## 后续工作
### 优化建议
- [ ] 考虑将所有ERP相关导入统一管理
- [ ] 添加更多错误处理机制
### 防范措施
- [ ] 代码审查时注意函数作用域和导入语句
- [ ] 添加单元测试覆盖关键函数
---
## 附录
### 参考文档
- 代码规范:`Docs/PRD/01规范文档/_PRD_规范文档_代码规范.md`
- 问题总结:`Docs/PRD/01规范文档/_PRD_问题总结_记录文档.md`
- 方法总结:`Docs/PRD/01规范文档/_PRD_方法总结_记录文档.md`
### 代码路径
- 主目录:`AuxiliaryTool/FunctionalTestReportGeneration`
- 问题文件:`src/gui.py`
- 相关文件:`src/erp_uploader.py`
### 错误日志
```
NameError: name 'upload_report_to_erp' is not defined
```
---
## 优化功能回填
> 此区域用于记录实施过程中的优化和改进
| 日期 | 优化内容 | 优化人 | 状态 |
|------|----------|--------|------|
| - | - | - | `[ ]` |
---
**文档版本**: 1.0
**创建日期**: 2026-06-17
**最后更新**: 2026-06-17
# 报告生成优化需求文档
## 代码路径
- 代码路径:[AuxiliaryTool/FunctionalTestReportGeneration]
## ERP对接文档
- 需求文档路径:[Docs/PRD/自动化生成功能测试报告/ERP对接PRD/PRD_项目资料_上传接口_例子说明.md]
## 功能需求
### 功能目标
**目标:** 对接ERP项目资料上传接口,实现创建完成ERP测试报告后,将测试报告文件上传至ERP项目资料中。
### 需求描述
- 上传项目资料接口:
- 根据[Docs/PRD/自动化生成功能测试报告/ERP对接PRD/PRD_项目资料_上传接口_例子说明.md]第4.2 项目资料上传章节。
- 接口调用地址:https://office.ubainsyun.com:5082/api/uerp/openclaw/upload/project
- X-Api-Key: 保持原有的key传入就行。
- 请求示例:
```bash
curl -X POST "https://office.ubainsyun.com:5082/uerp/openclaw/upload/project" \
-H "X-Api-Key: your_api_key" \
-F "file=@/path/to/需求文档.pdf" \
-F "project_id=100" \
-F "name=项目需求文档"
```
- project_id和name通过用户输入。
- file就直接采用生产的测试报告word格式即可。
## 规范文档
- 代码规范: `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`
---
\ No newline at end of file
# ERP对接优化文件归档至项目资料 - 计划执行文档
## 执行概述
### 项目背景
当前功能测试报告生成工具已实现将生成的报告上传到ERP测试报告模块。为方便项目资料管理,需要进一步对接ERP项目资料上传接口,在创建ERP测试报告后,自动将测试报告文件上传至ERP项目资料中归档。
### 执行目标
1. 实现项目资料上传接口调用(POST /openclaw/upload/project)
2. 在测试报告创建成功后,触发文件归档流程
3. 支持用户输入项目ID和文件名称
4. 集成到现有上传流程中
### 涉及范围
- 代码路径:`AuxiliaryTool/FunctionalTestReportGeneration`
- 参考文档:`Docs/PRD/自动化生成功能测试报告/ERP对接PRD/PRD_项目资料_上传接口_例子说明.md`
---
## 任务分解与实施计划
### 任务1:新增项目资料上传接口配置
#### 1.1 任务详情
- **接口地址**`POST /openclaw/upload/project`
- **接口目的**:上传测试报告文件到ERP项目资料
- **状态**`[ ]`
#### 1.2 实施步骤
- [ ] 1.2.1 在 `src/config.py` 中添加项目资料上传接口配置常量
- `ERP_UPLOAD_PROJECT_URL` = "/openclaw/upload/project"
- [ ] 1.2.2 确认现有 ERP_BASE_URL 和 ERP_API_KEY 配置可用
- [ ] 1.2.3 添加必要的配置说明注释
#### 1.3 输出产物
- config.py 中的新增配置常量
- 配置说明注释
---
### 任务2:实现项目资料上传函数
#### 2.1 任务详情
- **功能目标**:在 `src/erp_uploader.py` 中新增项目资料上传函数
- **函数签名**`upload_file_to_project(file_path: str, project_id: int, name: str, logger: logging.Logger) -> Optional[dict]`
- **状态**`[ ]`
#### 2.2 实施步骤
- [ ] 2.2.1 创建 `upload_file_to_project()` 函数
- [ ] 2.2.2 实现文件上传逻辑(multipart/form-data格式)
- [ ] 2.2.3 添加重试机制(复用现有 ERP_MAX_RETRIES 配置)
- [ ] 2.2.4 添加异常处理和日志记录
- [ ] 2.2.5 返回上传结果(包含文件ID、路径等信息)
#### 2.3 输出产物
- `upload_file_to_project()` 函数
- 完整的错误处理和日志记录
---
### 任务3:集成到现有上传流程
#### 3.1 任务详情
- **功能目标**:在报告创建成功后,询问用户是否上传到项目资料
- **集成位置**
- CLI: `src/cli.py``generate_report_main()` 函数
- GUI: `src/gui.py``_ask_upload_to_erp()` 函数
- **状态**`[ ]`
#### 3.2 实施步骤
- [ ] 3.2.1 在 CLI 交互中添加项目资料上传确认提示
- [ ] 3.2.2 获取用户输入的项目ID和文件名称
- [ ] 3.2.3 调用 `upload_file_to_project()` 执行上传
- [ ] 3.2.4 在 GUI 对话框中添加项目资料上传选项
- [ ] 3.2.5 实现GUI模式下的参数输入和上传
#### 3.3 输出产物
- CLI 模式下的项目资料上传交互
- GUI 模式下的项目资料上传对话框
---
### 任务4:优化交互体验
#### 4.1 任务详情
- **功能目标**:提供友好的用户输入提示和错误处理
- **状态**`[ ]`
#### 4.2 实施步骤
- [ ] 4.2.1 添加项目ID输入验证(确保为正整数)
- [ ] 4.2.2 添加文件名称输入验证(非空检查)
- [ ] 4.2.3 实现输入错误的友好提示
- [ ] 4.2.4 添加上传成功/失败的明确反馈
- [ ] 4.2.5 支持跳过项目资料上传的选项
#### 4.3 输出产物
- 完善的输入验证逻辑
- 友好的错误提示信息
---
## 验收标准
### 功能验收标准
| 序号 | 验收项 | 验收标准 | 状态 |
|------|--------|----------|------|
| 1 | 项目资料上传接口 | 成功调用接口,返回正确的文件信息 | `[ ]` |
| 2 | 文件上传正确性 | 上传的文件内容与原报告一致 | `[ ]` |
| 3 | CLI交互模式 | 能正常输入项目ID和文件名称 | `[ ]` |
| 4 | GUI交互模式 | 对话框能正常显示和处理输入 | `[ ]` |
| 5 | 流程集成 | 报告创建后能正确触发项目资料上传询问 | `[ ]` |
| 6 | 跳过功能 | 用户可选择跳过项目资料上传 | `[ ]` |
| 7 | 异常处理 | 网络异常、接口失败有适当提示 | `[ ]` |
### 代码质量验收标准
| 序号 | 验收项 | 验收标准 | 状态 |
|------|--------|----------|------|
| 1 | 代码规范 | 符合 `Docs/PRD/01规范文档/_PRD_规范文档_代码规范.md` | `[ ]` |
| 2 | 中文注释 | 所有新增代码有完整的中文注释 | `[ ]` |
| 3 | 异常处理 | 所有可能的异常情况都有处理逻辑 | `[ ]` |
| 4 | 日志记录 | 关键操作有日志输出,便于排查问题 | `[ ]` |
| 5 | 兼容性 | 不影响现有功能的正常运行 | `[ ]` |
| 6 | 重试机制 | 复用现有重试配置,确保上传可靠性 | `[ ]` |
---
## 测试计划
### 单元测试
| 测试项 | 测试内容 | 预期结果 | 状态 |
|--------|----------|----------|------|
| 接口调用 | 调用项目资料上传接口 | 返回正确的文件信息 | `[ ]` |
| 参数验证 | 验证project_id和name参数 | 参数正确传入接口 | `[ ]` |
| 文件读取 | 读取并上传Word文件 | 文件内容完整上传 | `[ ]` |
| 重试机制 | 模拟网络异常情况 | 能正确重试指定次数 | `[ ]` |
| 异常处理 | 模拟各种异常情况 | 有友好的错误提示 | `[ ]` |
### 集成测试
| 测试项 | 测试内容 | 预期结果 | 状态 |
|--------|----------|----------|------|
| CLI完整流程 | CLI模式下生成报告并上传项目资料 | 流程完整,上传成功 | `[ ]` |
| GUI完整流程 | GUI模式下生成报告并上传项目资料 | 对话框正常,上传成功 | `[ ]` |
| 跳过上传 | 选择跳过项目资料上传 | 流程正常结束,不影响报告创建 | `[ ]` |
| 输入验证 | 输入无效的项目ID | 提示错误并允许重新输入 | `[ ]` |
| 网络异常测试 | 模拟网络异常情况 | 有友好的错误提示,不影响主流程 | `[ ]` |
| 兼容性测试 | 执行现有报告上传功能 | 现有功能正常运行不受影响 | `[ ]` |
---
## 风险评估
| 风险项 | 风险等级 | 影响范围 | 应对措施 |
|--------|----------|----------|----------|
| 接口变更 | 中 | 项目资料上传失败 | 添加接口版本管理,做好兼容处理 |
| 网络不稳定 | 中 | 上传超时失败 | 复用现有重试机制和超时处理 |
| 用户输入错误 | 低 | 参数无效导致上传失败 | 添加输入验证和友好提示 |
| 现有功能影响 | 中 | 破坏现有上传流程 | 充分测试,保持向后兼容 |
| 文件过大 | 低 | 上传时间过长或失败 | 添加文件大小检查和提示 |
| API权限不足 | 中 | 接口返回权限错误 | 提示用户检查API Key权限配置 |
---
## 实施记录
### 实施时间线
| 日期 | 任务 | 状态 | 备注 |
|------|------|------|------|
| - | 任务1:新增项目资料上传接口配置 | `[ ]` | - |
| - | 任务2:实现项目资料上传函数 | `[ ]` | - |
| - | 任务3:集成到现有上传流程 | `[ ]` | - |
| - | 任务4:优化交互体验 | `[ ]` | - |
| - | 单元测试 | `[ ]` | - |
| - | 集成测试 | `[ ]` | - |
| - | 验收测试 | `[ ]` | - |
### 问题记录
| 日期 | 问题描述 | 解决方案 | 状态 |
|------|----------|----------|------|
| - | - | - | `[ ]` |
---
## 后续工作
### 优化建议
- [ ] 考虑支持批量上传(将历史报告上传到项目资料)
- [ ] 添加项目ID记忆功能,方便重复使用
- [ ] 支持从ERP项目列表中选择项目,而非手动输入ID
- [ ] 添加上传历史记录,便于追溯
### 待确认事项
- [ ] 确认API Key是否已授权 `upload_project` 权限
- [ ] 确认项目ID的有效范围和获取方式
- [ ] 确认文件名称的命名规范建议
- [ ] 确认是否需要支持协作文档上传(/cooperation接口)
---
## 附录
### 参考文档
- 代码规范:`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`
- ERP接口文档:`Docs/PRD/自动化生成功能测试报告/ERP对接PRD/PRD_项目资料_上传接口_例子说明.md`
### 代码路径
- 主目录:`AuxiliaryTool/FunctionalTestReportGeneration`
- 配置文件:`src/config.py`
- 上传模块:`src/erp_uploader.py`
- CLI模块:`src/cli.py`
- GUI模块:`src/gui.py`
### 接口信息
- **接口地址**`POST /openclaw/upload/project`
- **完整URL**`https://office.ubainsyun.com:5082/api/uerp/openclaw/upload/project`
- **认证方式**`X-Api-Key` 请求头
- **请求格式**`multipart/form-data`
- **请求参数**
- `file`:上传的文件(必填)
- `project_id`:关联的项目ID(必填)
- `name`:文件名称(可选,默认使用原文件名)
---
## 优化功能回填
> 此区域用于记录实施过程中的优化和改进
| 日期 | 优化内容 | 优化人 | 状态 |
|------|----------|--------|------|
| - | - | - | `[ ]` |
---
**文档版本**: 1.0
**创建日期**: 2026-06-17
**最后更新**: 2026-06-17
# 报告生成优化需求文档
## 代码路径
- 代码路径:[AuxiliaryTool/FunctionalTestReportGeneration]
## ERP对接文档
- 需求文档路径:[Docs/PRD/自动化生成功能测试报告/ERP对接PRD/PRD_测试列表_测试报告_例子说明.md]
## 功能需求
### 功能目标
**目标:** 对接获取人员列表接口以及调整上传文件接口的创建人传参。然后通过交互模式下输入的创建人姓名,最终通过接口传参实现。
### 需求描述
- 获取人员接口:
- 根据[Docs/PRD/自动化生成功能测试报告/ERP对接PRD/PRD_测试列表_测试报告_例子说明.md]第538–541实现获取人员列表。
- 上传文件接口调整:
- 新增`createuser_id`字段传参。
- 通过用户输入的创建者姓名,匹配对应的id赋值传入。
## 规范文档
- 代码规范: `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`
---
\ No newline at end of file
# ERP对接优化新增创建人传参 - 计划执行文档
## 执行概述
### 项目背景
当前功能测试报告生成工具需要对接ERP系统的人员接口,并优化上传文件接口的创建人传参方式。通过交互模式下输入创建人姓名,自动匹配对应的ID并传入接口。
### 执行目标
1. 实现获取人员列表接口调用(GET /openclaw/stuff)
2. 调整上传文件接口,新增 `createuser_id` 字段传参
3. 在交互模式下支持输入创建人姓名,自动匹配并传入对应的ID
### 涉及范围
- 代码路径:`AuxiliaryTool/FunctionalTestReportGeneration`
- 参考文档:`Docs/PRD/自动化生成功能测试报告/ERP对接PRD/PRD_测试列表_测试报告_例子说明.md`
---
## 任务分解与实施计划
### 任务1:获取人员列表接口实现
#### 1.1 任务详情
- **接口地址**`GET /openclaw/stuff`
- **接口目的**:获取人员列表,用于匹配创建人ID
- **状态**`[ ]`
#### 1.2 实施步骤
- [ ] 1.2.1 在代码中添加获取人员列表的HTTP请求方法
- [ ] 1.2.2 实现人员列表数据的解析和缓存机制
- [ ] 1.2.3 添加人员姓名到ID的映射字典/函数
- [ ] 1.2.4 添加异常处理(网络异常、接口调用失败等)
#### 1.3 输出产物
- `get_personnel_list()` 方法
- 人员数据缓存结构
- 姓名-ID映射函数 `get_user_id_by_name()`
---
### 任务2:上传文件接口优化
#### 2.1 任务详情
- **接口调整**:POST 上传文件接口
- **新增字段**`createuser_id`
- **传参逻辑**:通过创建人姓名匹配对应的ID
- **状态**`[ ]`
#### 2.2 实施步骤
- [ ] 2.2.1 定位上传文件接口的代码位置
- [ ] 2.2.2 在接口参数中新增 `createuser_id` 字段
- [ ] 2.2.3 实现姓名到ID的匹配逻辑
- [ ] 2.2.4 添加参数验证(确保ID有效)
- [ ] 2.2.5 更新接口调用处的传参代码
#### 2.3 输出产物
- 更新后的上传文件接口方法
- 姓名匹配逻辑代码
- 相关的单元测试
---
### 任务3:交互模式输入优化
#### 3.1 任务详情
- **功能目标**:在交互模式下支持输入创建人姓名
- **交互流程**:用户输入姓名 → 匹配ID → 传入接口
- **状态**`[ ]`
#### 3.2 实施步骤
- [ ] 3.2.1 在交互流程中添加创建人姓名输入提示
- [ ] 3.2.2 实现姓名输入的验证逻辑
- [ ] 3.2.3 调用人员列表接口获取最新数据
- [ ] 3.2.4 实现姓名模糊匹配或精确匹配
- [ ] 3.2.5 处理匹配失败的情况(提示用户重新输入)
#### 3.3 输出产物
- 交互式输入代码
- 姓名匹配逻辑
- 错误提示和重试机制
---
## 验收标准
### 功能验收标准
| 序号 | 验收项 | 验收标准 | 状态 |
|------|--------|----------|------|
| 1 | 获取人员列表接口 | 成功调用接口,返回完整的人员列表数据 | `[ ]` |
| 2 | 姓名-ID匹配 | 输入正确姓名,能匹配到对应的ID | `[ ]` |
| 3 | 上传接口传参 | 上传文件时正确传入 `createuser_id` 参数 | `[ ]` |
| 4 | 交互模式输入 | 能正常输入创建人姓名并完成后续流程 | `[ ]` |
| 5 | 异常处理 | 网络异常、接口失败、匹配失败有适当提示 | `[ ]` |
### 代码质量验收标准
| 序号 | 验收项 | 验收标准 | 状态 |
|------|--------|----------|------|
| 1 | 代码规范 | 符合 `Docs/PRD/01规范文档/_PRD_规范文档_代码规范.md` | `[ ]` |
| 2 | 中文注释 | 所有新增代码有完整的中文注释 | `[ ]` |
| 3 | 异常处理 | 所有可能的异常情况都有处理逻辑 | `[ ]` |
| 4 | 日志记录 | 关键操作有日志输出,便于排查问题 | `[ ]` |
| 5 | 兼容性 | 不影响现有功能的正常运行 | `[ ]` |
---
## 测试计划
### 单元测试
| 测试项 | 测试内容 | 预期结果 | 状态 |
|--------|----------|----------|------|
| 人员列表接口 | 调用获取人员接口 | 返回正确的人员列表数据 | `[ ]` |
| 姓名-ID匹配 | 输入有效姓名 | 返回正确的用户ID | `[ ]` |
| 姓名匹配-异常 | 输入不存在的姓名 | 提示错误并允许重试 | `[ ]` |
| 接口传参 | 验证上传接口参数 | `createuser_id` 正确传入 | `[ ]` |
### 集成测试
| 测试项 | 测试内容 | 预期结果 | 状态 |
|--------|----------|----------|------|
| 完整流程测试 | 交互模式下输入姓名并上传报告 | 成功上传且创建人ID正确 | `[ ]` |
| 网络异常测试 | 模拟网络异常情况 | 有友好的错误提示 | `[ ]` |
| 兼容性测试 | 执行现有功能 | 现有功能正常运行不受影响 | `[ ]` |
---
## 风险评估
| 风险项 | 风险等级 | 影响范围 | 应对措施 |
|--------|----------|----------|----------|
| ERP接口变更 | 中 | 人员列表获取失败 | 添加接口版本管理,做好兼容处理 |
| 姓名重复 | 低 | 匹配到错误的用户ID | 支持工号或唯一标识输入 |
| 网络不稳定 | 中 | 接口调用超时失败 | 添加重试机制和超时处理 |
| 现有功能影响 | 中 | 破坏现有上传流程 | 充分测试,保持向后兼容 |
---
## 实施记录
### 实施时间线
| 日期 | 任务 | 状态 | 备注 |
|------|------|------|------|
| - | 任务1:获取人员列表接口实现 | `[ ]` | - |
| - | 任务2:上传文件接口优化 | `[ ]` | - |
| - | 任务3:交互模式输入优化 | `[ ]` | - |
| - | 单元测试 | `[ ]` | - |
| - | 集成测试 | `[ ]` | - |
| - | 验收测试 | `[ ]` | - |
### 问题记录
| 日期 | 问题描述 | 解决方案 | 状态 |
|------|----------|----------|------|
| - | - | - | `[ ]` |
---
## 后续工作
### 优化建议
- [ ] 考虑添加人员列表的本地缓存,减少接口调用
- [ ] 支持创建人姓名的自动补全功能
- [ ] 添加创建人历史记录,方便快速选择
### 待确认事项
- [ ] 确认ERP接口的认证方式和token获取机制
- [ ] 确认创建人姓名的唯一性要求
- [ ] 确认上传接口的其他必填参数
---
## 附录
### 参考文档
- 代码规范:`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`
- ERP对接文档:`Docs/PRD/自动化生成功能测试报告/ERP对接PRD/PRD_测试列表_测试报告_例子说明.md`
### 代码路径
- 主目录:`AuxiliaryTool/FunctionalTestReportGeneration`
---
## 优化功能回填
> 此区域用于记录实施过程中的优化和改进
| 日期 | 优化内容 | 优化人 | 状态 |
|------|----------|--------|------|
| - | - | - | `[ ]` |
---
**文档版本**: 1.0
**创建日期**: 2026-06-17
**最后更新**: 2026-06-17
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论