From d43a50e14dcfbd8a3d22e5a23aabbf4176e66e4f Mon Sep 17 00:00:00 2001 From: dongchangxi <458593490@qq.com> Date: Thu, 4 Dec 2025 20:41:37 +0800 Subject: [PATCH] Refactor main.py to enable API call for updating slice status and enhance argument parsing. Update funcs.py for improved code readability and error handling in Blender execution. --- script/factory_sliceing_v2/main.py | 25 +- script/factory_sliceing_v2/utils/funcs.py | 264 ++++++++++++++-------- 2 files changed, 178 insertions(+), 111 deletions(-) diff --git a/script/factory_sliceing_v2/main.py b/script/factory_sliceing_v2/main.py index c49c1e0..808d5b0 100644 --- a/script/factory_sliceing_v2/main.py +++ b/script/factory_sliceing_v2/main.py @@ -3,6 +3,7 @@ import redis import oss2,time,sys import requests import argparse,json +from utils.funcs import requestApiToUpdateSliceStatus # 将当前脚本所在目录添加到 Python 路径,以便导入 utils 模块 sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) @@ -50,14 +51,14 @@ def step1(versionId): objCounts += 1 print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 下载处理完成的obj文件数量: {objCounts}') - # requestApiToUpdateSliceStatus(versionId,objCounts) + requestApiToUpdateSliceStatus(versionId,objCounts) # 读取 队列中一个数据出来 def main(work_dir=None): - + global currentDir # 如果指定了工作目录,使用指定的目录 if work_dir: work_dir = os.path.abspath(work_dir) @@ -122,13 +123,13 @@ def testMain(): print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 处理完成,res={res}') if __name__ == '__main__': - testMain() - # parser = argparse.ArgumentParser(description='排版打印订单处理程序') - # parser.add_argument( - # '--work-dir', - # type=str, - # default=None, - # help='指定工作目录(磁盘路径),例如: D:/work 或 /Users/username/work。如果不指定,则使用脚本所在目录' - # ) - # args = parser.parse_args() - # main(work_dir=args.work_dir) + #testMain() + parser = argparse.ArgumentParser(description='排版打印订单处理程序') + parser.add_argument( + '--work-dir', + type=str, + default=None, + help='指定工作目录(磁盘路径),例如: D:/work 或 /Users/username/work。如果不指定,则使用脚本所在目录' + ) + args = parser.parse_args() + main(work_dir=args.work_dir) diff --git a/script/factory_sliceing_v2/utils/funcs.py b/script/factory_sliceing_v2/utils/funcs.py index 686b129..a3fda72 100644 --- a/script/factory_sliceing_v2/utils/funcs.py +++ b/script/factory_sliceing_v2/utils/funcs.py @@ -1,14 +1,18 @@ -import requests,time -import os,platform +import requests +import time +import os +import platform import shutil import json import shlex +import subprocess from concurrent.futures import ThreadPoolExecutor, as_completed from .oss_redis import ossClient from .logs import log from .oss_func import download_file_with_check, checkFileExists from .changeFiles import changeObjFile, changeMtlFile from .small_machine_transform import transform_save + ENV = 'prod' url = 'https://mp.api.suwa3d.com' if ENV == 'dev': @@ -17,11 +21,11 @@ elif ENV == 'prod': url = 'https://mp.api.suwa3d.com' -#根据打印ID 获取下载目录 +# 根据打印ID 获取下载目录 def getDownloadDirByPrintId(printIds): - #调用接口获取下载的路径 + # 调用接口获取下载的路径 api_url = f"{url}/api/printOrder/getInfoByPrintIds" - res = requests.post(api_url,json={"print_ids":printIds}) + res = requests.post(api_url, json={"print_ids": printIds}) res = res.json() print(f"根据打印ID 获取下载目录, res={res}") if res["code"] == 1000: @@ -29,9 +33,10 @@ def getDownloadDirByPrintId(printIds): else: return False -#根据批次id,进行切片中的状态变更 -def requestApiToUpdateSliceStatus(versionId,downloadCounts): - api_url = f"{url}/api/printTypeSettingOrder/updateBatchSliceing?batch_id={versionId}&download_counts="+str(downloadCounts) + +# 根据批次id,进行切片中的状态变更 +def requestApiToUpdateSliceStatus(versionId, downloadCounts): + api_url = f"{url}/api/printTypeSettingOrder/updateBatchSliceing?batch_id={versionId}&download_counts=" + str(downloadCounts) log(f'发起状态变更请求url={api_url}, versionId={versionId}') try: # 添加超时参数,防止请求时间过长 @@ -48,39 +53,40 @@ def requestApiToUpdateSliceStatus(versionId,downloadCounts): log(f'状态变更请求异常, error={str(e)}') return False -#判断是否上传了 JSON 文件 + +# 判断是否上传了 JSON 文件 def checkJsonFileExists(versionId): log(f"检测文件和图片是否存在, versionId={versionId}") - jsonFilePath = f'batchPrint/{versionId}/{versionId}.json' - #判断oss 上是否存在 + jsonFilePath = f'batchPrint/{versionId}/{versionId}.json' + # 判断oss 上是否存在 jpgFilePath = f'batchPrint/{versionId}/{versionId}.jpg' if not ossClient().object_exists(jsonFilePath): log(f'JSON文件不存在: {jsonFilePath}') - return False,False + return False, False - if not ossClient().object_exists(jpgFilePath): log(f'JPG文件不存在: {jpgFilePath}') - return False,False + return False, False log(f"文件和图片检测成功,存在, versionId={versionId}") - return jsonFilePath,jpgFilePath + return jsonFilePath, jpgFilePath - -#检测本地文件是否存在 +# 检测本地文件是否存在 def checkLocalFileExists(localFilePath): if not os.path.exists(localFilePath): return False return True -#读取JSON文件内容 + +# 读取JSON文件内容 def readJsonFile(localFilePath): with open(localFilePath, 'r', encoding='utf-8') as f: jsonData = json.load(f) return jsonData -#根据批次ID,获取批次信息 + +# 根据批次ID,获取批次信息 def getBatchInfo(versionId): url1 = f"{url}/api/printTypeSettingOrder/getBatchInfoAndPrintMachineInfoByBatchId?batch_id={versionId}" res = requests.get(url1) @@ -91,13 +97,14 @@ def getBatchInfo(versionId): else: return False -#下载文件,读取文件内容,并且文件迁移至正确的目录里,不再放在临时目录里 -def downloadJsonAndJpgFileAndMoveToCorrectDir(versionId,currentDir): - jsonFilePath,jpgFilePath = checkJsonFileExists(versionId) + +# 下载文件,读取文件内容,并且文件迁移至正确的目录里,不再放在临时目录里 +def downloadJsonAndJpgFileAndMoveToCorrectDir(versionId, currentDir): + jsonFilePath, jpgFilePath = checkJsonFileExists(versionId) if jsonFilePath == False or jpgFilePath == False: - return False,False - - #将文件下载到临时目录,待会要判断是否什么类型机型 + return False, False + + # 将文件下载到临时目录,待会要判断是否什么类型机型 tempDir = os.path.join(currentDir, 'batchPrint', 'temp', versionId) if not os.path.exists(tempDir): os.makedirs(tempDir) @@ -106,74 +113,73 @@ def downloadJsonAndJpgFileAndMoveToCorrectDir(versionId,currentDir): ok = download_file_with_check(jsonFilePath, localFilePath) if not ok: log(f"JSON 文件下载失败或不完整, versionId={versionId}") - return False,False - #下载JPG文件 + return False, False + # 下载JPG文件 localJpgFilePath = os.path.join(tempDir, f'{versionId}.jpg') ok = download_file_with_check(jpgFilePath, localJpgFilePath) if not ok: log(f"JPG 文件下载失败或不完整, versionId={versionId}") - return False,False - #根据批次ID,获取批次信息和打印机信息 - batchMachineInfo = getBatchInfo(versionId) + return False, False + # 根据批次ID,获取批次信息和打印机信息 + batchMachineInfo = getBatchInfo(versionId) if not batchMachineInfo: log(f"获取批次信息和打印机信息失败, versionId={versionId}") - return False,False + return False, False # "batch_info": batchInfo, - # "print_machine_info": printMachineInfo, + # "print_machine_info": printMachineInfo, machineInfo = batchMachineInfo["print_machine_info"] print(f"machineInfo={machineInfo['id']}") dirNewName = "" if str(machineInfo["machine_type"]) == '1': - dirNewName = os.path.join(currentDir, 'batchPrint', versionId + '_small_No'+str(machineInfo['id'])) + dirNewName = os.path.join(currentDir, 'batchPrint', versionId + '_small_No' + str(machineInfo['id'])) else: - dirNewName = os.path.join(currentDir, 'batchPrint', versionId + '_big_No'+str(machineInfo['id'])) + dirNewName = os.path.join(currentDir, 'batchPrint', versionId + '_big_No' + str(machineInfo['id'])) - #判断目录是否存在,存在就删除 + # 判断目录是否存在,存在就删除 if os.path.exists(dirNewName): shutil.rmtree(dirNewName) - #创建目录 + # 创建目录 os.makedirs(dirNewName) - #创建json子目录 + # 创建json子目录 jsonSubDir = os.path.join(dirNewName, 'json') if not os.path.exists(jsonSubDir): os.makedirs(jsonSubDir) - - #将数据移动过来 + # 将数据移动过来 shutil.move(localFilePath, os.path.join(jsonSubDir, '3DPrintLayout.json')) shutil.move(localJpgFilePath, os.path.join(jsonSubDir, f'{versionId}.jpg')) - #检测文件是否移动成功 + # 检测文件是否移动成功 if not os.path.exists(os.path.join(jsonSubDir, '3DPrintLayout.json')): log(f"JSON文件不存在, versionId={versionId}") - return False,False + return False, False if not os.path.exists(os.path.join(jsonSubDir, f'{versionId}.jpg')): log(f"JPG文件不存在, versionId={versionId}") - return False,False + return False, False log(f"文件移动成功, versionId={versionId}") - #返回目录路径 - return dirNewName,machineInfo + # 返回目录路径 + return dirNewName, machineInfo -#整合json文件,读取对应的数据,返回数据结构 +# 整合json文件,读取对应的数据,返回数据结构 def getJsonData(dirNewName): jsonFilePath = os.path.join(dirNewName, 'json', '3DPrintLayout.json') if not os.path.exists(jsonFilePath): log(f"JSON文件不存在, dirNewName={dirNewName}") return False - #读取JSON文件内容 + # 读取JSON文件内容 jsonData = readJsonFile(jsonFilePath) - #读取models + # 读取models models = jsonData.get('models', []) if not models: log(f"models不存在, dirNewName={dirNewName}") return False - + listData = [] - #遍历models + # 遍历models for model in models: file_name = model.get('file_name', '') - #分割数据 + # 分割数据 arrFileName = file_name.split('_') orderId = arrFileName[0] pid = arrFileName[1] @@ -181,14 +187,12 @@ def getJsonData(dirNewName): size = arrFileName[3] counts = arrFileName[4].replace("x", "").replace(".obj", "") - #检测这些数据 + # 检测这些数据 if not orderId or not pid or not printId or not size or not counts: log(f"数据不完整, orderId={orderId}, pid={pid}, printId={printId}, size={size}, counts={counts}") return False - - - #创建数据结构 + # 创建数据结构 modelInfo = { "orderId": orderId, "printId": printId, @@ -198,27 +202,27 @@ def getJsonData(dirNewName): "file_name": file_name, } listData.append(modelInfo) - - #检测数据长度 + + # 检测数据长度 if len(listData) == 0: log(f"数据长度为0, dirNewName={dirNewName}") return False - #返回数据结构 + # 返回数据结构 return listData # 读取 json 文件,获取 homo_matrix 数据,根据 file_name 获取 homo_matrix 数据 -def getHomoMatrixByFileName(dirNewName,fileName): +def getHomoMatrixByFileName(dirNewName, fileName): jsonFilePath = os.path.join(dirNewName, 'json', '3DPrintLayout.json') if not os.path.exists(jsonFilePath): log(f"JSON文件不存在, dirNewName={dirNewName}") return False - #读取JSON文件内容 + # 读取JSON文件内容 jsonData = readJsonFile(jsonFilePath) if not jsonData: log(f"读取JSON文件内容失败, dirNewName={dirNewName}") return False - + for model in jsonData["models"]: log(f"jsonData={model['file_name']} == {fileName}") if model["file_name"] == fileName: @@ -227,7 +231,7 @@ def getHomoMatrixByFileName(dirNewName,fileName): return False -#处理单个数据项:下载文件、修改关联路径、转换数据(如果是小机台) +# 处理单个数据项:下载文件、修改关联路径、转换数据(如果是小机台) def _process_single_item(v, arrDownloadPath, dirNewName, dirPath, isSmallMachine): """ 处理单个数据项的函数,用于多线程处理 @@ -247,10 +251,10 @@ def _process_single_item(v, arrDownloadPath, dirNewName, dirPath, isSmallMachine if str(tempv["print_order_id"]) == str(v["printId"]): info = tempv break - + if not info: return False, f"未找到匹配的下载路径信息, printId={v['printId']}" - + filePath = info["path"] pid = info["pid"] orderId = info["order_id"] @@ -258,8 +262,8 @@ def _process_single_item(v, arrDownloadPath, dirNewName, dirPath, isSmallMachine size = info["real_size"] counts = info["quantity"] fileName = v["file_name"] - - #判断文件是否存在 + + # 判断文件是否存在 ossJpgFilePath = f"{filePath}/printId_{printId}Tex1.jpg" if not checkFileExists(ossJpgFilePath): ossJpgFilePath = f"{filePath}/{pid}Tex1.jpg" @@ -272,12 +276,13 @@ def _process_single_item(v, arrDownloadPath, dirNewName, dirPath, isSmallMachine {"ossPath": f"{filePath}/{pid}.obj", "localPath": os.path.join(dirPath, localObjName)}, {"ossPath": f"{filePath}/{pid}.mtl", "localPath": os.path.join(dirPath, loaclMtlName)}, ] - - #遍历下载文件 + + # 遍历下载文件 beginTime = time.time() objsLocal = "" for objFiles in arrDownloadFiles: - #判断 mtl 和 jpg 文件是否存在,存在就不在下载了 + downloadBeginTime = time.time() + # 判断 mtl 和 jpg 文件是否存在,存在就不在下载了 if "mtl" in objFiles["localPath"] or "jpg" in objFiles["localPath"]: if os.path.exists(objFiles["localPath"]): continue @@ -288,18 +293,20 @@ def _process_single_item(v, arrDownloadPath, dirNewName, dirPath, isSmallMachine log(error_msg) return False, error_msg - #下载成功之后要修改文件之间的关联路径 + log(f"下载文件耗时: {time.time() - downloadBeginTime}秒 - {objFiles['localPath']}") + # 下载成功之后要修改文件之间的关联路径 if objFiles["localPath"].endswith(".obj"): + beginChangeTime = time.time() objsLocal = objFiles["localPath"] changeObjFile(objFiles["localPath"], f"{orderId}_{pid}.mtl") - + log(f"修改obj关联耗时: {time.time() - beginChangeTime}秒 - {objFiles['localPath']}") if objFiles["localPath"].endswith(".mtl"): changeMtlFile(objFiles["localPath"], f"{orderId}_{pid}Tex1.jpg") - + endTime = time.time() log(f"下载文件和修改文件之间的关联路径 : 耗时{endTime - beginTime}秒 - {fileName}") - #如果是小机台,则要转换数据 + # 如果是小机台,则要转换数据 if isSmallMachine: timeBegin = time.time() homo_matrix = getHomoMatrixByFileName(dirNewName, localObjName) @@ -308,27 +315,65 @@ def _process_single_item(v, arrDownloadPath, dirNewName, dirPath, isSmallMachine log(error_msg) return False, error_msg - #通过blender 调用执行 python 文件 + # 通过blender 调用执行 python 文件 blender_bin_path = findBpyModule() # 获取 small_machine_transform.py 的绝对路径 script_dir = os.path.dirname(os.path.abspath(__file__)) transform_script_path = os.path.join(script_dir, 'small_machine_transform.py') - # 将 homo_matrix 转换为 JSON 字符串,并对路径进行转义 + + # 将 homo_matrix 转换为 JSON 字符串 homo_matrix_json = json.dumps(homo_matrix) - # 使用 shlex.quote 来安全地转义路径和参数 - transform_script_path_quoted = shlex.quote(transform_script_path) - objsLocal_quoted = shlex.quote(objsLocal) - homo_matrix_quoted = shlex.quote(homo_matrix_json) - error = os.system(f"{blender_bin_path} -b -P {transform_script_path_quoted} -- --objPathName={objsLocal_quoted} --trans={homo_matrix_quoted}") - if error != 0: - error_msg = f"调用blender 执行 python 文件失败, error={error}, fileName={fileName}" + + # 构建命令参数列表(使用列表形式,避免 shell 转义问题) + cmd = [ + blender_bin_path, + '--background', + '--python', + transform_script_path, + '--', + f'--objPathName={objsLocal}', + f'--trans={homo_matrix_json}' + ] + + log(f"执行 Blender 命令: {' '.join(cmd[:4])} ... (参数已隐藏)") + + try: + # 使用 subprocess 执行命令,捕获输出和错误 + # 在 Windows 上,Blender 输出可能是 UTF-8,需要指定编码并处理错误 + result = subprocess.run( + cmd, + capture_output=True, + text=True, + encoding='utf-8', # 指定 UTF-8 编码 + errors='replace', # 遇到无法解码的字符时替换为占位符,而不是抛出异常 + timeout=300, # 5分钟超时 + check=False # 不自动抛出异常,手动检查返回码 + ) + + if result.returncode != 0: + error_output = result.stderr if result.stderr else result.stdout + error_msg = f"调用blender 执行 python 文件失败, error={result.returncode}, fileName={fileName}" + if error_output: + error_msg += f", 错误信息: {error_output[:500]}" # 限制错误信息长度 + log(error_msg) + return False, error_msg + + log(f"调用blender 执行 python 文件成功, fileName={fileName}") + if result.stdout: + log(f"Blender 输出: {result.stdout[:200]}") # 记录部分输出用于调试 + + except subprocess.TimeoutExpired: + error_msg = f"调用blender 执行超时(超过5分钟), fileName={fileName}" + log(error_msg) + return False, error_msg + except Exception as e: + error_msg = f"调用blender 执行时发生异常: {str(e)}, fileName={fileName}" log(error_msg) return False, error_msg - log(f"调用blender 执行 python 文件成功, error={error}, fileName={fileName}") timeEnd = time.time() log(f"转换数据时间: 耗时{timeEnd - timeBegin}秒 - {objsLocal}") - + return True, "" except Exception as e: error_msg = f"处理数据项时发生异常, fileName={v.get('file_name', 'unknown')}, error={str(e)}" @@ -336,13 +381,13 @@ def _process_single_item(v, arrDownloadPath, dirNewName, dirPath, isSmallMachine return False, error_msg -#json文件进行下载对应的数据 和转换数据,传递目录路径 -def downloadDataByOssAndTransformSave(dirNewName,isSmallMachine=False, max_workers=10): +# json文件进行下载对应的数据 和转换数据,传递目录路径 +def downloadDataByOssAndTransformSave(dirNewName, isSmallMachine=False, max_workers=1): listData = getJsonData(dirNewName) if not listData: log(f"获取数据失败, dirNewName={dirNewName}") return False - #遍历数据 + # 遍历数据 arrPrintId = [] arrPrintDataInfo = [] for modelInfo in listData: @@ -352,28 +397,28 @@ def downloadDataByOssAndTransformSave(dirNewName,isSmallMachine=False, max_worke "file_name": modelInfo.get('file_name'), }) - #调用接口获取下载的路径 + # 调用接口获取下载的路径 arrDownloadPath = getDownloadDirByPrintId(arrPrintId) if not arrDownloadPath: log(f"获取下载路径失败, arrPrintId={arrPrintId}") return False - + dirPath = os.path.join(dirNewName, 'data') if not os.path.exists(dirPath): os.makedirs(dirPath) - #使用多线程并发处理数据 + # 使用多线程并发处理数据 log(f"开始多线程处理数据, 共{len(listData)}个项目, 线程数={max_workers}") beginTime = time.time() - + # 使用线程池并发处理 with ThreadPoolExecutor(max_workers=max_workers) as executor: # 提交所有任务 future_to_item = { - executor.submit(_process_single_item, v, arrDownloadPath, dirNewName, dirPath, isSmallMachine): v + executor.submit(_process_single_item, v, arrDownloadPath, dirNewName, dirPath, isSmallMachine): v for v in listData } - + # 收集结果 success_count = 0 fail_count = 0 @@ -392,27 +437,48 @@ def downloadDataByOssAndTransformSave(dirNewName,isSmallMachine=False, max_worke fail_count += 1 error_msg = f"处理数据项时发生异常: {str(e)}" log(f"处理异常: {v.get('file_name', 'unknown')}, 错误: {error_msg} ({fail_count}/{len(listData)})") - + endTime = time.time() log(f"多线程处理完成, 总耗时{endTime - beginTime}秒, 成功:{success_count}, 失败:{fail_count}, 总计:{len(listData)}") - + # 如果有任何失败,返回 False if fail_count > 0: log(f"部分任务处理失败,共{fail_count}个失败") return False - + return True def findBpyModule(): - # 返回 Blender 可执行文件路径(macOS) + # 返回 Blender 可执行文件路径 blender_bin_path = '/Applications/Blender.app/Contents/MacOS/Blender' # 判断当前是 windows 还是 macOS if platform.system() == 'Windows': - blender_bin_path = 'C:\\Program Files\\Blender Foundation\\Blender 5.0\\blender.exe' + # Windows 上常见的 Blender 安装路径 + possible_paths = [ + 'C:\\Program Files\\Blender Foundation\\Blender 5.0\\blender.exe', + 'C:\\Program Files\\Blender Foundation\\Blender 4.4\\blender.exe', + 'C:\\Program Files\\Blender Foundation\\Blender 4.3\\blender.exe', + 'C:\\Program Files\\Blender Foundation\\Blender 4.2\\blender.exe', + 'C:\\Program Files\\Blender Foundation\\Blender 4.1\\blender.exe', + 'C:\\Program Files\\Blender Foundation\\Blender 4.0\\blender.exe', + ] + # 查找存在的路径 + for path in possible_paths: + if os.path.exists(path): + blender_bin_path = path + break + else: + # 如果都没找到,使用默认路径 + blender_bin_path = 'C:\\Program Files\\Blender Foundation\\Blender 5.0\\blender.exe' + log(f"警告: 未找到 Blender 可执行文件,使用默认路径: {blender_bin_path}") else: blender_bin_path = '/Applications/Blender.app/Contents/MacOS/Blender' + # 检查路径是否存在 + if not os.path.exists(blender_bin_path): + error_msg = f"Blender 可执行文件不存在: {blender_bin_path}" + log(error_msg) + raise FileNotFoundError(error_msg) - - return blender_bin_path \ No newline at end of file + return blender_bin_path