建模程序 多个定时程序
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

688 lines
28 KiB

import requests
import time
import os
import platform
import shutil
import json
import shlex
import subprocess
import sys
import signal
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':
url = 'http://mp.api.dev.com'
elif ENV == 'prod':
url = 'https://mp.api.suwa3d.com'
def get_resource_path(relative_path):
"""
获取资源文件的绝对路径,兼容 PyInstaller 打包后的环境
参数:
relative_path: 相对于脚本目录的相对路径
返回:
资源文件的绝对路径
"""
try:
# PyInstaller 创建临时文件夹,并将路径存储在 _MEIPASS 中
base_path = sys._MEIPASS
except AttributeError:
# 如果不是打包环境,使用脚本所在目录
base_path = os.path.dirname(os.path.abspath(__file__))
return os.path.join(base_path, relative_path)
def get_transform_script_path():
"""
获取 small_machine_transform.py 脚本的路径
在打包成exe后,如果脚本不在可访问位置,则将其提取到临时目录
返回:
transform_script_path: 脚本的绝对路径
"""
# 首先尝试从资源路径获取
if getattr(sys, 'frozen', False):
# 打包后的环境
# 尝试从资源路径获取
resource_path = get_resource_path('utils/small_machine_transform.py')
# 如果资源路径存在,直接使用
if os.path.exists(resource_path):
return resource_path
# 如果不存在,尝试提取到临时目录
import tempfile
temp_dir = tempfile.gettempdir()
extract_path = os.path.join(temp_dir, 'factory_sliceing_utils', 'small_machine_transform.py')
# 确保目录存在
os.makedirs(os.path.dirname(extract_path), exist_ok=True)
# 如果提取路径不存在,尝试从资源中复制
if not os.path.exists(extract_path):
# 尝试从多个可能的资源路径查找
possible_paths = [
get_resource_path('small_machine_transform.py'),
get_resource_path('utils/small_machine_transform.py'),
]
for src_path in possible_paths:
if os.path.exists(src_path):
shutil.copy2(src_path, extract_path)
log(f"已将 small_machine_transform.py 提取到: {extract_path}")
return extract_path
# 如果提取路径存在,直接使用
if os.path.exists(extract_path):
return extract_path
# 如果都找不到,返回资源路径(即使不存在,让调用者处理错误)
return resource_path
else:
# 开发环境,直接使用相对路径
script_dir = os.path.dirname(os.path.abspath(__file__))
return os.path.join(script_dir, 'small_machine_transform.py')
# 根据打印ID 获取下载目录
def getDownloadDirByPrintId(printIds):
# 调用接口获取下载的路径
api_url = f"{url}/api/printOrder/getInfoByPrintIds"
res = requests.post(api_url, json={"print_ids": printIds})
res = res.json()
print(f"根据打印ID 获取下载目录, res={res}")
if res["code"] == 1000:
return res["data"]
else:
return False
# 根据批次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:
# 添加超时参数,防止请求时间过长
res = requests.post(api_url, timeout=60) # 60秒超时
if res.status_code != 200:
log(f'状态变更请求失败, res={res.text}')
return False
log(f'状态变更请求成功, res={res.text}')
return True
except requests.exceptions.Timeout:
log(f'状态变更请求超时, url={api_url}')
return False
except requests.exceptions.RequestException as e:
log(f'状态变更请求异常, error={str(e)}')
return False
# 判断是否上传了 JSON 文件
def checkJsonFileExists(versionId):
log(f"检测文件和图片是否存在, versionId={versionId}")
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
if not ossClient().object_exists(jpgFilePath):
log(f'JPG文件不存在: {jpgFilePath}')
return False, False
log(f"文件和图片检测成功,存在, versionId={versionId}")
return jsonFilePath, jpgFilePath
# 检测本地文件是否存在
def checkLocalFileExists(localFilePath):
if not os.path.exists(localFilePath):
return False
return True
# 读取JSON文件内容
def readJsonFile(localFilePath):
with open(localFilePath, 'r', encoding='utf-8') as f:
jsonData = json.load(f)
return jsonData
# 根据批次ID,获取批次信息
def getBatchInfo(versionId):
url1 = f"{url}/api/printTypeSettingOrder/getBatchInfoAndPrintMachineInfoByBatchId?batch_id={versionId}"
res = requests.get(url1)
res = res.json()
log(f"获取批次信息和打印机信息, url={url1}, res={res}")
if res["code"] == 1000:
return res["data"]
else:
return False
# 下载文件,读取文件内容,并且文件迁移至正确的目录里,不再放在临时目录里
def downloadJsonAndJpgFileAndMoveToCorrectDir(versionId, currentDir):
jsonFilePath, jpgFilePath = checkJsonFileExists(versionId)
if jsonFilePath == False or jpgFilePath == False:
return False, False
# 将文件下载到临时目录,待会要判断是否什么类型机型
tempDir = os.path.join(currentDir, 'batchPrint', 'temp', versionId)
if not os.path.exists(tempDir):
os.makedirs(tempDir)
localFilePath = os.path.join(tempDir, f'{versionId}.json')
# 使用带完整性校验的下载方法下载JSON文件
ok = download_file_with_check(jsonFilePath, localFilePath)
if not ok:
log(f"JSON 文件下载失败或不完整, versionId={versionId}")
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)
if not batchMachineInfo:
log(f"获取批次信息和打印机信息失败, versionId={versionId}")
return False, False
# "batch_info": batchInfo,
# "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']))
else:
dirNewName = os.path.join(currentDir, 'batchPrint', versionId + '_big_No' + str(machineInfo['id']))
# 判断目录是否存在,存在就删除
if os.path.exists(dirNewName):
shutil.rmtree(dirNewName)
# 创建目录
os.makedirs(dirNewName)
# 创建json子目录
jsonSubDir = os.path.join(dirNewName, 'json')
if not os.path.exists(jsonSubDir):
os.makedirs(jsonSubDir)
dataSubDir = os.path.join(dirNewName, 'data')
if not os.path.exists(dataSubDir):
os.makedirs(dataSubDir)
# 将数据移动过来
shutil.move(localFilePath, os.path.join(jsonSubDir, '3DPrintLayout.json'))
shutil.move(localJpgFilePath, os.path.join(jsonSubDir, f'{versionId}.jpg'))
#如果是大机台,将 json 文件复制到 data 目录下
if str(machineInfo["machine_type"]) != '1':
shutil.copy(os.path.join(jsonSubDir, '3DPrintLayout.json'), os.path.join(dataSubDir, '3DPrintLayout.json'))
if not os.path.exists(os.path.join(dataSubDir, '3DPrintLayout.json')):
log(f"JSON文件不存在, dataSubDir={dataSubDir}")
return False, False
# 检测文件是否移动成功
if not os.path.exists(os.path.join(jsonSubDir, '3DPrintLayout.json')):
log(f"JSON文件不存在, versionId={versionId}")
return False, False
if not os.path.exists(os.path.join(jsonSubDir, f'{versionId}.jpg')):
log(f"JPG文件不存在, versionId={versionId}")
return False, False
log(f"文件移动成功, versionId={versionId}")
# 返回目录路径
return dirNewName, machineInfo
# 整合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文件内容
jsonData = readJsonFile(jsonFilePath)
# 读取models
models = jsonData.get('models', [])
if not models:
log(f"models不存在, dirNewName={dirNewName}")
return False
listData = []
# 遍历models
for model in models:
file_name = model.get('file_name', '')
# 分割数据
arrFileName = file_name.split('_')
orderId = arrFileName[0]
pid = arrFileName[1]
printId = arrFileName[2].replace("P", "")
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,
"pid": pid,
"size": size,
"counts": counts,
"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):
jsonFilePath = os.path.join(dirNewName, 'json', '3DPrintLayout.json')
if not os.path.exists(jsonFilePath):
log(f"JSON文件不存在, dirNewName={dirNewName}")
return False
# 读取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:
log(f"model={model['transform']}")
return model["transform"]["homo_matrix"]
return False
# 处理单个数据项:下载文件、修改关联路径、转换数据(如果是小机台)
def _process_single_item(v, arrDownloadPath, dirNewName, dirPath, isSmallMachine):
"""
处理单个数据项的函数,用于多线程处理
参数:
v: 单个数据项
arrDownloadPath: 下载路径数组
dirNewName: 目录名称
dirPath: 数据目录路径
isSmallMachine: 是否是小机台
返回:
(success: bool, error_msg: str)
"""
try:
# 查找匹配的下载路径信息
info = {}
for tempv in arrDownloadPath:
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"]
printId = info["print_order_id"]
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"
localJpgName = os.path.join(f"{orderId}_{pid}Tex1.jpg")
loaclMtlName = os.path.join(f"{orderId}_{pid}.mtl")
localObjName = fileName
arrDownloadFiles = [
{"ossPath": ossJpgFilePath, "localPath": os.path.join(dirPath, localJpgName)},
{"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:
downloadBeginTime = time.time()
# 判断 mtl 和 jpg 文件是否存在,存在就不在下载了
if "mtl" in objFiles["localPath"] or "jpg" in objFiles["localPath"]:
if os.path.exists(objFiles["localPath"]):
continue
downloadOk = download_file_with_check(objFiles["ossPath"], objFiles["localPath"])
if not downloadOk:
error_msg = f"下载文件失败, ossPath={objFiles['ossPath']}, localPath={objFiles['localPath']}"
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)
if not homo_matrix:
error_msg = f"获取homo_matrix失败, dirNewName={dirNewName}, objsLocal={objsLocal}"
log(error_msg)
return False, error_msg
# 通过blender 调用执行 python 文件
blender_bin_path = findBpyModule()
# 获取 small_machine_transform.py 的绝对路径(兼容打包环境)
transform_script_path = get_transform_script_path()
# 将 homo_matrix 转换为 JSON 字符串
homo_matrix_json = json.dumps(homo_matrix)
# 检查必要文件是否存在
if not blender_bin_path or not os.path.exists(blender_bin_path):
error_msg = f"Blender 可执行文件不存在: {blender_bin_path}"
log(error_msg)
return False, error_msg
if not os.path.exists(transform_script_path):
error_msg = f"转换脚本文件不存在: {transform_script_path}"
log(error_msg)
return False, error_msg
if not os.path.exists(objsLocal):
error_msg = f"OBJ 文件不存在: {objsLocal}"
log(error_msg)
return False, error_msg
# 构建命令参数列表(使用列表形式,避免 shell 转义问题)
cmd = [
blender_bin_path,
'--background',
'--python',
transform_script_path,
'--',
f'--objPathName={objsLocal}',
f'--trans={homo_matrix_json}'
]
log(f"准备执行 Blender 命令")
log(f" Blender路径: {blender_bin_path}")
log(f" 脚本路径: {transform_script_path}")
log(f" OBJ文件: {objsLocal}")
log(f" 完整命令: {' '.join(cmd[:4])} ... (参数已隐藏)")
try:
log(f"开始执行 Blender 转换命令...")
# 使用 subprocess.Popen 以便更好地控制进程和信号处理
# 在 Windows 上,Blender 输出可能是 UTF-8,需要指定编码并处理错误
# 准备进程启动参数
startupinfo = None
preexec_fn = None
if platform.system() == 'Windows':
# Windows 上,不设置 CREATE_NEW_PROCESS_GROUP,以便 Ctrl+C 可以传播
import subprocess as sp
startupinfo = sp.STARTUPINFO()
startupinfo.dwFlags |= sp.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = sp.SW_HIDE
else:
# Unix 系统,设置 preexec_fn 以确保信号处理正确
preexec_fn = os.setsid
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding='utf-8', # 指定 UTF-8 编码
errors='replace', # 遇到无法解码的字符时替换为占位符,而不是抛出异常
startupinfo=startupinfo,
preexec_fn=preexec_fn
)
try:
# 等待进程完成,捕获输出
stdout, stderr = process.communicate(timeout=300) # 5分钟超时
returncode = process.returncode
except subprocess.TimeoutExpired:
# 超时,终止进程
log(f"Blender 命令执行超时,正在终止进程...")
if platform.system() == 'Windows':
process.terminate()
try:
process.wait(timeout=5)
except subprocess.TimeoutExpired:
process.kill()
else:
# Unix 系统,发送 SIGTERM 到进程组
try:
os.killpg(os.getpgid(process.pid), signal.SIGTERM)
process.wait(timeout=5)
except (subprocess.TimeoutExpired, ProcessLookupError, OSError):
try:
os.killpg(os.getpgid(process.pid), signal.SIGKILL)
except (ProcessLookupError, OSError):
pass
raise
except KeyboardInterrupt:
# 收到中断信号,终止进程
log(f"收到中断信号,正在终止 Blender 进程...")
if platform.system() == 'Windows':
process.terminate()
try:
process.wait(timeout=5)
except subprocess.TimeoutExpired:
process.kill()
else:
# Unix 系统,发送 SIGTERM 到进程组
try:
os.killpg(os.getpgid(process.pid), signal.SIGTERM)
process.wait(timeout=5)
except (subprocess.TimeoutExpired, ProcessLookupError, OSError):
try:
os.killpg(os.getpgid(process.pid), signal.SIGKILL)
except (ProcessLookupError, OSError):
pass
raise # 重新抛出,让调用者处理
# 创建结果对象(模拟 subprocess.run 的返回值)
class Result:
def __init__(self, returncode, stdout, stderr):
self.returncode = returncode
self.stdout = stdout
self.stderr = stderr
result = Result(returncode, stdout, stderr)
log(f"Blender 命令执行完成, 返回码: {result.returncode}")
# 无论成功失败都输出详细信息
if result.stdout:
log(f"Blender stdout 输出: {result.stdout}")
if result.stderr:
log(f"Blender stderr 输出: {result.stderr}")
if result.returncode != 0:
error_output = result.stderr if result.stderr else result.stdout
error_msg = f"调用blender 执行 python 文件失败, 返回码={result.returncode}, fileName={fileName}"
if error_output:
error_msg += f", 错误信息: {error_output[:10000000]}" # 增加错误信息长度限制
log(error_msg)
return False, error_msg
log(f"调用blender 执行 python 文件成功, fileName={fileName}")
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
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)}"
log(error_msg)
return False, error_msg
# 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:
arrPrintId.append(modelInfo.get('printId'))
arrPrintDataInfo.append({
"printId": modelInfo.get('printId'),
"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()
# 使用线程池并发处理
try:
with ThreadPoolExecutor(max_workers=max_workers) as executor:
# 提交所有任务
future_to_item = {
executor.submit(_process_single_item, v, arrDownloadPath, dirNewName, dirPath, isSmallMachine): v
for v in listData
}
# 收集结果
success_count = 0
fail_count = 0
try:
for future in as_completed(future_to_item):
v = future_to_item[future]
try:
success, error_msg = future.result()
if success:
success_count += 1
log(f"处理成功: {v.get('file_name', 'unknown')} ({success_count}/{len(listData)})")
else:
fail_count += 1
log(f"处理失败: {v.get('file_name', 'unknown')}, 错误: {error_msg} ({fail_count}/{len(listData)})")
# 如果任何一个任务失败,记录错误但继续处理其他任务
except KeyboardInterrupt:
# 收到中断信号,取消所有未完成的任务
log(f"收到中断信号,正在取消未完成的任务...")
for f in future_to_item:
f.cancel()
raise # 重新抛出,让外层捕获
except Exception as e:
fail_count += 1
error_msg = f"处理数据项时发生异常: {str(e)}"
log(f"处理异常: {v.get('file_name', 'unknown')}, 错误: {error_msg} ({fail_count}/{len(listData)})")
except KeyboardInterrupt:
# 收到中断信号,取消所有未完成的任务
log(f"收到中断信号,正在取消未完成的任务...")
for f in future_to_item:
f.cancel()
# 尝试等待正在执行的任务完成(非阻塞方式)
import concurrent.futures
for f in list(future_to_item.keys()):
if not f.done():
try:
# 尝试获取结果,如果任务还在执行则立即返回
f.result(timeout=0.1)
except (concurrent.futures.TimeoutError, concurrent.futures.CancelledError):
pass
except:
pass
raise # 重新抛出,让外层捕获
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
except KeyboardInterrupt:
log(f"多线程处理被中断")
raise # 重新抛出,让调用者处理
def findBpyModule():
# 返回 Blender 可执行文件路径
blender_bin_path = '/Applications/Blender.app/Contents/MacOS/Blender'
# 判断当前是 windows 还是 macOS
if platform.system() == 'Windows':
# 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