目录迁移
This commit is contained in:
1 parent
2ea782ed93
commit
cc15c76e62
26 files changed
+295
-86
No files matched your search
@@ -1,27 +0,0 @@
|
||||
from utils.import_all_file import modify_file_dialog_path_and_import_all
|
||||
from utils.click_soft_button import clickFileIMportShow, clickBegingSlice
|
||||
import time
|
||||
|
||||
from utils.exe_operate import start_exe, click_confirm, close
|
||||
|
||||
|
||||
|
||||
#"C:/test/10192_small_No4/data"
|
||||
def BeginSliceing(folderPath):
|
||||
# 打开3d切片软件
|
||||
start_exe()
|
||||
time.sleep(1)
|
||||
#点击确认按钮
|
||||
click_confirm()
|
||||
#先打开导入文件的弹框
|
||||
clickFileIMportShow()
|
||||
time.sleep(1)
|
||||
#在输入路径导入obj文件
|
||||
isSuccess = modify_file_dialog_path_and_import_all(folderPath)
|
||||
if not isSuccess:
|
||||
print("导入文件失败")
|
||||
exit()
|
||||
#点击开始切片
|
||||
time.sleep(5)
|
||||
clickBegingSlice()
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
import uiautomation as auto
|
||||
|
||||
|
||||
|
||||
# 遍历所有控件 找到
|
||||
def findAndClickFileImportShow(control, depth=0,index=1):
|
||||
for child in control.GetChildren():
|
||||
# print(child)
|
||||
# print(' ' * depth + f"{child.ControlType}: {child.Name} | {child.AutomationId}")
|
||||
if child.Name == "切片及打印":
|
||||
# 点击和 child 同组的第二个元素(即 control 的子控件中的第二个)
|
||||
siblings = control.GetChildren() # 获取所有同级元素
|
||||
# print(f"\n找到目标控件: {child.Name}")
|
||||
# print(f"同级元素数量: {len(siblings)}")
|
||||
|
||||
# 打印所有同级元素信息,方便调试
|
||||
for i, sibling in enumerate(siblings):
|
||||
marker = " <-- 这是找到的目标" if sibling == child else ""
|
||||
print(f" 同级元素[{i}]: {sibling.ControlType} | {sibling.Name} | {sibling.AutomationId}{marker}")
|
||||
|
||||
# 确保有至少2个同级元素
|
||||
if len(siblings) >= 2:
|
||||
target = siblings[index] # 第二个元素(索引从0开始)
|
||||
print(f"\n准备点击同级{index+1}个元素:")
|
||||
print(f" 控件类型: {target.ControlType}")
|
||||
print(f" 控件名称: {target.Name}")
|
||||
print(f" 自动化ID: {target.AutomationId}")
|
||||
|
||||
# 尝试多种点击方式
|
||||
try:
|
||||
# 方法1: 直接点击
|
||||
target.Click()
|
||||
print("✓ 使用 Click() 方法点击成功")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"✗ Click() 失败: {e}")
|
||||
try:
|
||||
# 方法2: 使用 Invoke(如果是可调用的控件)
|
||||
target.Invoke()
|
||||
print("✓ 使用 Invoke() 方法点击成功")
|
||||
return True
|
||||
except Exception as e2:
|
||||
print(f"✗ Invoke() 失败: {e2}")
|
||||
try:
|
||||
# 方法3: 先设置焦点再点击
|
||||
target.SetFocus()
|
||||
import time
|
||||
time.sleep(0.1)
|
||||
target.Click()
|
||||
print("✓ 使用 SetFocus() + Click() 方法点击成功")
|
||||
return True
|
||||
except Exception as e3:
|
||||
print(f"✗ SetFocus() + Click() 失败: {e3}")
|
||||
# 方法4: 使用鼠标点击坐标
|
||||
try:
|
||||
rect = target.BoundingRectangle
|
||||
# BoundingRectangle 是一个矩形对象,计算中心点
|
||||
x = rect.left + ((rect.right - rect.left) / 2)
|
||||
y = rect.top + ((rect.bottom - rect.top) / 2)
|
||||
auto.Click(x, y)
|
||||
print(f"✓ 使用坐标点击成功: ({x}, {y})")
|
||||
return True
|
||||
except Exception as e4:
|
||||
print(f"✗ 坐标点击失败: {e4}")
|
||||
else:
|
||||
print(f"✗ 同级元素数量不足,只有 {len(siblings)} 个")
|
||||
return False
|
||||
else:
|
||||
findAndClickFileImportShow(child, depth + 1)
|
||||
|
||||
# 点击导入文件按钮
|
||||
def clickFileIMportShow():
|
||||
control = auto.WindowControl(searchDepth=1, Name='赛纳3D打印控制系统 V1.4.3.2')
|
||||
clickRes = findAndClickFileImportShow(control,index=1)
|
||||
return clickRes
|
||||
|
||||
# 点击开始切片按钮
|
||||
def clickBegingSlice():
|
||||
control = auto.WindowControl(searchDepth=1, Name='赛纳3D打印控制系统 V1.4.3.2')
|
||||
clickRes = findAndClickFileImportShow(control,index=2)
|
||||
return clickRes
|
||||
@@ -1,202 +0,0 @@
|
||||
#启动exe 文件
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
import uiautomation as auto
|
||||
exe_path = r"C:\Users\Administrator\Desktop\排版软件\NormalTek.ThreeDPrinter.UI.exe"
|
||||
|
||||
def start_exe():
|
||||
try:
|
||||
# 检查文件是否存在
|
||||
if not os.path.exists(exe_path):
|
||||
print(f"错误:文件不存在 - {exe_path}")
|
||||
return False
|
||||
|
||||
# 检查是否是文件(不是目录)
|
||||
if not os.path.isfile(exe_path):
|
||||
print(f"错误:路径不是文件 - {exe_path}")
|
||||
return False
|
||||
|
||||
# 获取 exe 文件所在的目录(工作目录)
|
||||
exe_dir = os.path.dirname(exe_path)
|
||||
print(f"工作目录设置为: {exe_dir}")
|
||||
|
||||
# 检查工作目录是否存在
|
||||
if not os.path.exists(exe_dir):
|
||||
print(f"错误:工作目录不存在 - {exe_dir}")
|
||||
return False
|
||||
|
||||
# 使用 subprocess 启动,并设置工作目录
|
||||
# 这样可以确保程序能找到同目录下的配置文件
|
||||
try:
|
||||
process = subprocess.Popen(
|
||||
[exe_path],
|
||||
cwd=exe_dir, # 设置工作目录为 exe 所在目录
|
||||
creationflags=subprocess.CREATE_NEW_CONSOLE # 在新控制台窗口运行
|
||||
)
|
||||
print(f"✓ 成功启动程序: {exe_path}")
|
||||
print(f" 进程ID: {process.pid}")
|
||||
print(f" 工作目录: {exe_dir}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"subprocess 启动失败: {e}")
|
||||
# 备用方案:使用 os.startfile(但无法设置工作目录)
|
||||
try:
|
||||
# 先切换到工作目录
|
||||
original_cwd = os.getcwd()
|
||||
os.chdir(exe_dir)
|
||||
os.startfile(exe_path)
|
||||
os.chdir(original_cwd) # 恢复原目录
|
||||
print(f"✓ 使用 os.startfile 成功启动程序: {exe_path}")
|
||||
return True
|
||||
except Exception as e2:
|
||||
print(f"os.startfile 启动也失败: {e2}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"启动程序时发生异常: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def click_confirm():
|
||||
"""
|
||||
触发点击 ESC 键,用于关闭弹框
|
||||
"""
|
||||
try:
|
||||
# 发送 ESC 键到当前活动窗口
|
||||
auto.SendKeys('{ESC}')
|
||||
print("✓ 已发送 ESC 键")
|
||||
time.sleep(0.3)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"发送 ESC 键失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def close():
|
||||
"""
|
||||
关闭打开的应用程序
|
||||
通过查找窗口名称并关闭窗口,使用多种方法确保成功
|
||||
"""
|
||||
try:
|
||||
import win32gui
|
||||
import win32con
|
||||
import win32api
|
||||
import win32process
|
||||
|
||||
# 应用程序的窗口名称
|
||||
window_name = "赛纳3D打印控制系统 V1.4.3.2"
|
||||
|
||||
print(f"正在查找窗口: {window_name}")
|
||||
|
||||
# 方法1: 使用 win32gui 查找窗口(更可靠)
|
||||
hwnd = win32gui.FindWindow(None, window_name)
|
||||
|
||||
if hwnd:
|
||||
print(f"✓ 找到窗口句柄: {hwnd}")
|
||||
# 激活窗口
|
||||
win32gui.SetForegroundWindow(hwnd)
|
||||
win32gui.ShowWindow(hwnd, win32con.SW_RESTORE) # 确保窗口不是最小化
|
||||
time.sleep(0.3)
|
||||
|
||||
# 方法1: 使用 SendMessage 发送 WM_CLOSE(比 PostMessage 更强制)
|
||||
try:
|
||||
win32gui.SendMessage(hwnd, win32con.WM_CLOSE, 0, 0)
|
||||
print("✓ 使用 SendMessage(WM_CLOSE) 关闭窗口")
|
||||
time.sleep(1)
|
||||
# 检查窗口是否还存在
|
||||
if not win32gui.IsWindow(hwnd):
|
||||
print("✓ 窗口已成功关闭")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"SendMessage(WM_CLOSE) 失败: {e}")
|
||||
|
||||
# 方法2: 使用 win32api 发送 Alt+F4 按键事件(更可靠)
|
||||
try:
|
||||
win32gui.SetForegroundWindow(hwnd)
|
||||
time.sleep(0.2)
|
||||
# 按下 Alt
|
||||
win32api.keybd_event(win32con.VK_MENU, 0, 0, 0)
|
||||
time.sleep(0.05)
|
||||
# 按下 F4
|
||||
win32api.keybd_event(win32con.VK_F4, 0, 0, 0)
|
||||
time.sleep(0.05)
|
||||
# 释放 F4
|
||||
win32api.keybd_event(win32con.VK_F4, 0, win32con.KEYEVENTF_KEYUP, 0)
|
||||
time.sleep(0.05)
|
||||
# 释放 Alt
|
||||
win32api.keybd_event(win32con.VK_MENU, 0, win32con.KEYEVENTF_KEYUP, 0)
|
||||
print("✓ 使用 win32api 发送 Alt+F4")
|
||||
time.sleep(1)
|
||||
# 检查窗口是否还存在
|
||||
if not win32gui.IsWindow(hwnd):
|
||||
print("✓ 窗口已成功关闭")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"win32api Alt+F4 失败: {e}")
|
||||
|
||||
# 方法3: 尝试使用 uiautomation 的 Close()
|
||||
try:
|
||||
window = auto.WindowControl(searchDepth=1, Name=window_name)
|
||||
if window.Exists(0, 0):
|
||||
window.SetFocus()
|
||||
time.sleep(0.2)
|
||||
window.Close()
|
||||
print("✓ 使用 uiautomation Close() 关闭窗口")
|
||||
time.sleep(1)
|
||||
if not win32gui.IsWindow(hwnd):
|
||||
print("✓ 窗口已成功关闭")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"uiautomation Close() 失败: {e}")
|
||||
|
||||
# 方法4: 如果以上都失败,尝试终止进程(最后手段)
|
||||
print("尝试通过进程ID终止程序...")
|
||||
try:
|
||||
import psutil
|
||||
# 通过窗口句柄获取进程ID
|
||||
_, pid = win32process.GetWindowThreadProcessId(hwnd)
|
||||
process = psutil.Process(pid)
|
||||
process.terminate()
|
||||
print(f"✓ 已终止进程 (PID: {pid})")
|
||||
time.sleep(0.5)
|
||||
return True
|
||||
except ImportError:
|
||||
print("psutil 未安装,无法使用进程终止方式")
|
||||
except Exception as e:
|
||||
print(f"终止进程失败: {e}")
|
||||
|
||||
print("✗ 所有关闭方法都失败了")
|
||||
return False
|
||||
else:
|
||||
# 如果 win32gui 找不到,尝试使用 uiautomation
|
||||
print("win32gui 未找到窗口,尝试使用 uiautomation...")
|
||||
window = auto.WindowControl(searchDepth=1, Name=window_name)
|
||||
if window.Exists(0, 0):
|
||||
print(f"✓ 使用 uiautomation 找到窗口")
|
||||
window.SetFocus()
|
||||
time.sleep(0.2)
|
||||
hwnd = window.Handle
|
||||
win32gui.SendMessage(hwnd, win32con.WM_CLOSE, 0, 0)
|
||||
print("✓ 使用 SendMessage(WM_CLOSE) 关闭窗口")
|
||||
time.sleep(1)
|
||||
return True
|
||||
else:
|
||||
print(f"✗ 未找到窗口: {window_name}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"关闭应用程序时发生异常: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
#if __name__ == "__main__":
|
||||
# start_exe()
|
||||
# #启动成功之后,有一个弹框 的窗口,点击确定
|
||||
# time.sleep(5)
|
||||
# click_confirm()
|
||||
# time.sleep(10)
|
||||
# close()
|
||||
@@ -1,179 +0,0 @@
|
||||
import win32gui, win32con, time
|
||||
import win32api
|
||||
import ctypes
|
||||
|
||||
def find_window(title=None, class_name=None):
|
||||
# 任一参数为 None 即忽略该条件
|
||||
hwnd = win32gui.FindWindow(class_name, title)
|
||||
return hwnd if hwnd else None
|
||||
|
||||
|
||||
def send_text_to_window(hwnd, text):
|
||||
"""向窗口发送文本"""
|
||||
win32gui.SetForegroundWindow(hwnd)
|
||||
time.sleep(0.2)
|
||||
|
||||
for char in text:
|
||||
# 获取虚拟键码
|
||||
vk_code = win32api.VkKeyScan(char)
|
||||
|
||||
if vk_code != -1:
|
||||
# 提取低字节(虚拟键码)和高字节(修饰键)
|
||||
vk = vk_code & 0xFF
|
||||
shift_state = (vk_code >> 8) & 0xFF
|
||||
|
||||
# 如果需要 Shift
|
||||
if shift_state & 1: # Shift 键
|
||||
win32api.keybd_event(win32con.VK_SHIFT, 0, 0, 0)
|
||||
time.sleep(0.01)
|
||||
|
||||
# 按下键
|
||||
win32api.keybd_event(vk, 0, 0, 0)
|
||||
time.sleep(0.01)
|
||||
# 释放键
|
||||
win32api.keybd_event(vk, 0, win32con.KEYEVENTF_KEYUP, 0)
|
||||
|
||||
# 释放 Shift(如果按下了)
|
||||
if shift_state & 1:
|
||||
time.sleep(0.01)
|
||||
win32api.keybd_event(win32con.VK_SHIFT, 0, win32con.KEYEVENTF_KEYUP, 0)
|
||||
|
||||
time.sleep(0.02) # 字符之间的延迟
|
||||
|
||||
#发送 Alt+D 快捷键(聚焦地址栏)
|
||||
def send_alt_d(hwnd):
|
||||
"""发送 Alt+D 快捷键(聚焦地址栏)"""
|
||||
win32gui.SetForegroundWindow(hwnd)
|
||||
time.sleep(0.2)
|
||||
win32api.keybd_event(win32con.VK_MENU, 0, 0, 0) # Alt
|
||||
time.sleep(0.05)
|
||||
win32api.keybd_event(ord('D'), 0, 0, 0)
|
||||
time.sleep(0.05)
|
||||
win32api.keybd_event(ord('D'), 0, win32con.KEYEVENTF_KEYUP, 0)
|
||||
win32api.keybd_event(win32con.VK_MENU, 0, win32con.KEYEVENTF_KEYUP, 0)
|
||||
time.sleep(0.3)
|
||||
|
||||
|
||||
#发送 Ctrl+A 快捷键(全选)
|
||||
def send_ctrl_a(hwnd):
|
||||
"""发送 Ctrl+A 快捷键(全选)"""
|
||||
win32gui.SetForegroundWindow(hwnd)
|
||||
time.sleep(0.2)
|
||||
win32api.keybd_event(win32con.VK_CONTROL, 0, 0, 0)
|
||||
time.sleep(0.05)
|
||||
win32api.keybd_event(ord('A'), 0, 0, 0)
|
||||
time.sleep(0.05)
|
||||
win32api.keybd_event(ord('A'), 0, win32con.KEYEVENTF_KEYUP, 0)
|
||||
win32api.keybd_event(win32con.VK_CONTROL, 0, win32con.KEYEVENTF_KEYUP, 0)
|
||||
time.sleep(0.3)
|
||||
|
||||
|
||||
#发送 Enter 键
|
||||
def send_enter(hwnd):
|
||||
"""发送 Enter 键"""
|
||||
win32gui.SetForegroundWindow(hwnd)
|
||||
time.sleep(0.1)
|
||||
win32api.keybd_event(win32con.VK_RETURN, 0, 0, 0)
|
||||
time.sleep(0.05)
|
||||
win32api.keybd_event(win32con.VK_RETURN, 0, win32con.KEYEVENTF_KEYUP, 0)
|
||||
time.sleep(0.2)
|
||||
|
||||
#
|
||||
def send_tab(hwnd, count=1):
|
||||
"""发送 Tab 键(用于导航焦点)"""
|
||||
win32gui.SetForegroundWindow(hwnd)
|
||||
time.sleep(0.1)
|
||||
for _ in range(count):
|
||||
win32api.keybd_event(win32con.VK_TAB, 0, 0, 0)
|
||||
time.sleep(0.05)
|
||||
win32api.keybd_event(win32con.VK_TAB, 0, win32con.KEYEVENTF_KEYUP, 0)
|
||||
time.sleep(0.1)
|
||||
time.sleep(0.2)
|
||||
|
||||
#聚焦文件列表窗口
|
||||
def focus_file_list(hwnd):
|
||||
"""将焦点移到文件列表区域"""
|
||||
win32gui.SetForegroundWindow(hwnd)
|
||||
time.sleep(0.2)
|
||||
|
||||
# 使用鼠标点击文件列表区域(最可靠的方法)
|
||||
try:
|
||||
# 获取窗口位置和大小
|
||||
left, top, right, bottom = win32gui.GetWindowRect(hwnd)
|
||||
width = right - left
|
||||
height = bottom - top
|
||||
|
||||
# 文件列表通常在窗口中间偏下的位置
|
||||
# 计算文件列表的大概位置(窗口中心偏下,避开地址栏和按钮区域)
|
||||
click_x = left + width // 2
|
||||
click_y = top + height // 2 + 30 # 稍微偏下一点,避开地址栏
|
||||
|
||||
# 点击文件列表区域
|
||||
win32api.SetCursorPos((click_x, click_y))
|
||||
time.sleep(0.1)
|
||||
win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0)
|
||||
time.sleep(0.05)
|
||||
win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0)
|
||||
time.sleep(0.2)
|
||||
except Exception as e:
|
||||
print(f"点击文件列表失败,使用Tab键方式: {e}")
|
||||
# 如果点击失败,使用Tab键导航(备用方案)
|
||||
send_tab(hwnd, 1)
|
||||
time.sleep(0.2)
|
||||
|
||||
|
||||
def modify_file_dialog_path_and_import_all(target_path=""):
|
||||
if not target_path:
|
||||
return False
|
||||
"""
|
||||
修改文件对话框的路径并导入所有文件
|
||||
|
||||
参数:
|
||||
target_path: 目标文件夹路径,默认为 C://work/batchPrint
|
||||
"""
|
||||
# 1. 找到"打开"窗口
|
||||
print("正在查找'打开'窗口...")
|
||||
hwnd = find_window(title='打开')
|
||||
if not hwnd:
|
||||
print("未找到'打开'窗口,请确保文件对话框已打开")
|
||||
return False
|
||||
|
||||
print(f"✓ 找到'打开'窗口,句柄: {hwnd}")
|
||||
time.sleep(0.5)
|
||||
|
||||
# 2. 激活窗口并聚焦地址栏(使用 Alt+D 快捷键)
|
||||
print("正在聚焦地址栏...")
|
||||
send_alt_d(hwnd)
|
||||
time.sleep(0.3)
|
||||
|
||||
# 3. 清空地址栏并输入新路径
|
||||
print(f"正在设置路径为: {target_path}")
|
||||
send_ctrl_a(hwnd) # 全选现有路径
|
||||
time.sleep(0.2)
|
||||
|
||||
# 输入新路径
|
||||
send_text_to_window(hwnd, target_path)
|
||||
time.sleep(0.3)
|
||||
|
||||
# 按 Enter 确认路径
|
||||
send_enter(hwnd)
|
||||
print("✓ 路径已设置")
|
||||
time.sleep(1.5) # 等待路径加载完成
|
||||
|
||||
# 4. 将焦点移到文件列表
|
||||
print("正在将焦点移到文件列表...")
|
||||
focus_file_list(hwnd)
|
||||
time.sleep(0.3)
|
||||
|
||||
# 5. 选择所有文件
|
||||
print("正在选择所有文件...")
|
||||
send_ctrl_a(hwnd) # Ctrl+A 选择所有文件
|
||||
time.sleep(0.5)
|
||||
print("✓ 已选择所有文件")
|
||||
|
||||
# 6. 点击"打开"按钮(使用 Enter 键)
|
||||
print("正在确认打开...")
|
||||
send_enter(hwnd)
|
||||
print("✓ 已确认打开")
|
||||
|
||||
return True
|
||||
@@ -1,4 +0,0 @@
|
||||
import time
|
||||
|
||||
def log(message):
|
||||
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} {message}')
|
||||
@@ -1,45 +0,0 @@
|
||||
import os
|
||||
from .oss_redis import ossClient
|
||||
from .logs import log
|
||||
|
||||
# 从 OSS 下载文件到本地,并做完整性校验(状态码 + 文件大小)
|
||||
def download_file_with_check(ossFilePath, localFilePath):
|
||||
try:
|
||||
log(f"开始从 OSS 下载文件: {ossFilePath} -> {localFilePath}")
|
||||
if not checkFileExists(ossFilePath):
|
||||
log(f"文件不存在: {ossFilePath}")
|
||||
return False
|
||||
# 同步阻塞下载,下载完成后才会返回
|
||||
result = ossClient().get_object_to_file(ossFilePath, localFilePath)
|
||||
|
||||
# 1. 检查 HTTP 状态码
|
||||
status = getattr(result, "status", None)
|
||||
if status != 200:
|
||||
log(f"下载失败,HTTP 状态码异常: status={status}")
|
||||
return False
|
||||
|
||||
# 2. 远端 / 本地文件大小对比,作为二次校验
|
||||
remote_meta = ossClient().head_object(ossFilePath)
|
||||
remote_size = getattr(remote_meta, "content_length", None)
|
||||
if remote_size is None:
|
||||
log("无法获取远端文件大小,放弃本次下载结果")
|
||||
return False
|
||||
|
||||
if not os.path.exists(localFilePath):
|
||||
log("本地文件不存在,下载可能失败")
|
||||
return False
|
||||
|
||||
local_size = os.path.getsize(localFilePath)
|
||||
if remote_size != local_size:
|
||||
log(f"文件大小不一致,下载可能不完整: remote={remote_size}, local={local_size}")
|
||||
return False
|
||||
|
||||
log("文件下载成功且完整性校验通过")
|
||||
return True
|
||||
except Exception as e:
|
||||
log(f"下载文件出现异常: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
def checkFileExists(ossFilePath):
|
||||
return ossClient().object_exists(ossFilePath)
|
||||
@@ -1,125 +0,0 @@
|
||||
import oss2,redis
|
||||
import time
|
||||
import logging
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 连接oss - 单例模式
|
||||
class OSSClientSingleton:
|
||||
_instance = None
|
||||
_client = None
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super(OSSClientSingleton, cls).__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def get_client(self):
|
||||
if self._client is None:
|
||||
AccessKeyId = 'LTAI5tSReWm8hz7dSYxxth8f'
|
||||
AccessKeySecret = '8ywTDF9upPAtvgXtLKALY2iMYHIxdS'
|
||||
Endpoint = 'oss-cn-shanghai.aliyuncs.com'
|
||||
Bucket = 'suwa3d-securedata'
|
||||
self._client = oss2.Bucket(oss2.Auth(AccessKeyId, AccessKeySecret), Endpoint, Bucket)
|
||||
return self._client
|
||||
|
||||
def ossClient():
|
||||
"""获取OSS客户端单例"""
|
||||
return OSSClientSingleton().get_client()
|
||||
|
||||
|
||||
#连接redis,单例模式
|
||||
class RedisClientSingleton:
|
||||
_instance = None
|
||||
_client = None
|
||||
|
||||
# Redis连接配置
|
||||
REDIS_CONFIG = {
|
||||
'host': 'mp.api.suwa3d.com',
|
||||
'password': 'kcV2000',
|
||||
'port': 6379,
|
||||
'db': 6,
|
||||
'socket_timeout': 30, # 操作超时30秒
|
||||
'socket_connect_timeout': 10, # 连接超时10秒
|
||||
'socket_keepalive': True, # 启用 TCP keepalive
|
||||
'socket_keepalive_options': {}, # keepalive 选项
|
||||
'health_check_interval': 30 # 健康检查间隔30秒
|
||||
}
|
||||
|
||||
# 重试配置
|
||||
RETRY_INTERVAL = 5 # 重试间隔(秒)
|
||||
MAX_RETRY_INTERVAL = 60 # 最大重试间隔(秒),用于指数退避
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super(RedisClientSingleton, cls).__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def _create_redis_client(self):
|
||||
"""创建Redis客户端"""
|
||||
return redis.Redis(**self.REDIS_CONFIG)
|
||||
|
||||
def _connect_with_retry(self, max_retries=None, retry_interval=None):
|
||||
"""
|
||||
带重试机制的Redis连接方法
|
||||
:param max_retries: 最大重试次数,None表示无限重试
|
||||
:param retry_interval: 重试间隔(秒),None表示使用默认值,支持指数退避
|
||||
:return: Redis客户端实例
|
||||
"""
|
||||
if retry_interval is None:
|
||||
retry_interval = self.RETRY_INTERVAL
|
||||
|
||||
retry_count = 0
|
||||
current_interval = retry_interval
|
||||
|
||||
while True:
|
||||
try:
|
||||
logger.info(f"尝试连接Redis (第 {retry_count + 1} 次)...")
|
||||
client = self._create_redis_client()
|
||||
# 测试连接
|
||||
client.ping()
|
||||
logger.info("Redis连接成功!")
|
||||
return client
|
||||
except (redis.ConnectionError, redis.TimeoutError, AttributeError, Exception) as e:
|
||||
retry_count += 1
|
||||
error_msg = str(e)
|
||||
logger.warning(f"Redis连接失败 (第 {retry_count} 次): {error_msg}")
|
||||
|
||||
# 如果设置了最大重试次数且已达到,则抛出异常
|
||||
if max_retries is not None and retry_count >= max_retries:
|
||||
logger.error(f"达到最大重试次数 {max_retries},停止重试")
|
||||
raise
|
||||
|
||||
# 指数退避:每次重试间隔逐渐增加,但不超过最大值
|
||||
logger.info(f"等待 {current_interval} 秒后重试...")
|
||||
time.sleep(current_interval)
|
||||
current_interval = min(current_interval * 1.5, self.MAX_RETRY_INTERVAL)
|
||||
|
||||
def get_client(self):
|
||||
"""
|
||||
获取Redis客户端,如果连接断开则自动重连(带重试机制)
|
||||
:return: Redis客户端实例
|
||||
"""
|
||||
if self._client is None:
|
||||
# 首次连接,使用无限重试直到成功
|
||||
logger.info("初始化Redis连接...")
|
||||
self._client = self._connect_with_retry(max_retries=None)
|
||||
else:
|
||||
# 检查连接是否有效,如果断开则重新连接(带重试)
|
||||
try:
|
||||
self._client.ping()
|
||||
except (redis.ConnectionError, redis.TimeoutError, AttributeError) as e:
|
||||
# 连接断开,重新创建连接(使用无限重试直到成功)
|
||||
logger.warning(f"Redis连接已断开: {str(e)},开始重新连接...")
|
||||
self._client = None
|
||||
self._client = self._connect_with_retry(max_retries=None)
|
||||
|
||||
return self._client
|
||||
|
||||
|
||||
|
||||
def redisClient():
|
||||
"""获取Redis客户端单例"""
|
||||
return RedisClientSingleton().get_client()
|
||||
@@ -1,59 +0,0 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
echo ========================================
|
||||
echo 开始打包 factory_sliceing 为 Windows EXE
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
REM 检查 Python 是否安装
|
||||
python --version >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo [错误] 未找到 Python,请先安装 Python
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo [1/4] 检查并安装依赖包...
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install redis>=4.0.0 oss2>=2.17.0 requests>=2.28.0 pyinstaller>=5.0.0
|
||||
|
||||
if errorlevel 1 (
|
||||
echo [错误] 依赖包安装失败
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo [2/4] 清理之前的构建文件...
|
||||
if exist build rmdir /s /q build
|
||||
if exist dist rmdir /s /q dist
|
||||
if exist __pycache__ rmdir /s /q __pycache__
|
||||
for /d /r . %%d in (__pycache__) do @if exist "%%d" rmdir /s /q "%%d"
|
||||
|
||||
echo.
|
||||
echo [3/4] 使用 PyInstaller 打包...
|
||||
pyinstaller build_exe.spec --clean --noconfirm
|
||||
|
||||
if errorlevel 1 (
|
||||
echo [错误] 打包失败
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo [4/4] 打包完成!
|
||||
echo.
|
||||
echo ========================================
|
||||
echo 打包结果:
|
||||
echo ========================================
|
||||
echo EXE 文件位置: dist\factory_sliceing.exe
|
||||
echo.
|
||||
echo 使用说明:
|
||||
echo 1. 将 dist\factory_sliceing.exe 复制到目标 Windows 机器
|
||||
echo 2. 在命令行中运行: factory_sliceing.exe --work-dir D:\work
|
||||
echo.
|
||||
echo ========================================
|
||||
pause
|
||||
|
||||
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
打包脚本 - 将 main.py 打包成 Windows EXE 文件
|
||||
使用方法: python build_exe.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import shutil
|
||||
|
||||
def check_python_version():
|
||||
"""检查 Python 版本"""
|
||||
if sys.version_info < (3, 7):
|
||||
print("[错误] 需要 Python 3.7 或更高版本")
|
||||
return False
|
||||
print(f"[信息] Python 版本: {sys.version}")
|
||||
return True
|
||||
|
||||
def install_dependencies():
|
||||
"""安装依赖包"""
|
||||
print("\n[1/4] 检查并安装依赖包...")
|
||||
dependencies = [
|
||||
'redis>=4.0.0',
|
||||
'oss2>=2.17.0',
|
||||
'requests>=2.28.0',
|
||||
'pyinstaller>=5.0.0',
|
||||
]
|
||||
|
||||
for dep in dependencies:
|
||||
print(f" 安装 {dep}...")
|
||||
result = subprocess.run(
|
||||
[sys.executable, '-m', 'pip', 'install', '--upgrade', dep],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
if result.returncode != 0:
|
||||
print(f"[错误] 安装 {dep} 失败: {result.stderr}")
|
||||
return False
|
||||
|
||||
print("[成功] 所有依赖包已安装")
|
||||
return True
|
||||
|
||||
def clean_build_files():
|
||||
"""清理之前的构建文件"""
|
||||
print("\n[2/4] 清理之前的构建文件...")
|
||||
|
||||
dirs_to_remove = ['build', 'dist']
|
||||
for dir_name in dirs_to_remove:
|
||||
if os.path.exists(dir_name):
|
||||
shutil.rmtree(dir_name)
|
||||
print(f" 已删除: {dir_name}")
|
||||
|
||||
# 清理 __pycache__ 目录
|
||||
for root, dirs, files in os.walk('.'):
|
||||
if '__pycache__' in dirs:
|
||||
pycache_path = os.path.join(root, '__pycache__')
|
||||
shutil.rmtree(pycache_path)
|
||||
print(f" 已删除: {pycache_path}")
|
||||
|
||||
print("[成功] 清理完成")
|
||||
return True
|
||||
|
||||
def build_exe():
|
||||
"""使用 PyInstaller 打包"""
|
||||
print("\n[3/4] 使用 PyInstaller 打包...")
|
||||
|
||||
spec_file = 'build_exe.spec'
|
||||
if not os.path.exists(spec_file):
|
||||
print(f"[错误] 未找到配置文件: {spec_file}")
|
||||
return False
|
||||
|
||||
result = subprocess.run(
|
||||
[sys.executable, '-m', 'PyInstaller', spec_file, '--clean', '--noconfirm'],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
print(f"[错误] 打包失败:")
|
||||
print(result.stderr)
|
||||
return False
|
||||
|
||||
print("[成功] 打包完成")
|
||||
return True
|
||||
|
||||
def show_result():
|
||||
"""显示打包结果"""
|
||||
print("\n" + "="*50)
|
||||
print("打包结果:")
|
||||
print("="*50)
|
||||
|
||||
exe_path = os.path.join('dist', 'factory_sliceing.exe')
|
||||
if os.path.exists(exe_path):
|
||||
file_size = os.path.getsize(exe_path) / (1024 * 1024) # MB
|
||||
print(f"✓ EXE 文件位置: {exe_path}")
|
||||
print(f"✓ 文件大小: {file_size:.2f} MB")
|
||||
else:
|
||||
print(f"✗ 未找到 EXE 文件: {exe_path}")
|
||||
|
||||
print("\n使用说明:")
|
||||
print(" 1. 将 dist/factory_sliceing.exe 复制到目标 Windows 机器")
|
||||
print(" 2. 在命令行中运行: factory_sliceing.exe --work-dir D:\\work")
|
||||
print("="*50)
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
print("="*50)
|
||||
print("开始打包 factory_sliceing 为 Windows EXE")
|
||||
print("="*50)
|
||||
|
||||
# 切换到脚本所在目录
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
os.chdir(script_dir)
|
||||
print(f"[信息] 工作目录: {script_dir}")
|
||||
|
||||
# 检查 Python 版本
|
||||
if not check_python_version():
|
||||
return 1
|
||||
|
||||
# 安装依赖
|
||||
if not install_dependencies():
|
||||
return 1
|
||||
|
||||
# 清理构建文件
|
||||
if not clean_build_files():
|
||||
return 1
|
||||
|
||||
# 打包
|
||||
if not build_exe():
|
||||
return 1
|
||||
|
||||
# 显示结果
|
||||
show_result()
|
||||
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
sys.exit(main())
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n[中断] 用户取消操作")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"\n[错误] 发生异常: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
block_cipher = None
|
||||
|
||||
a = Analysis(
|
||||
['main.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[
|
||||
# 将 small_machine_transform.py 作为数据文件包含,以便 Blender 可以调用
|
||||
('utils/small_machine_transform.py', 'utils'),
|
||||
],
|
||||
hiddenimports=[
|
||||
'redis',
|
||||
'oss2',
|
||||
'requests',
|
||||
'utils.oss_redis',
|
||||
'utils.funcs',
|
||||
'utils.logs',
|
||||
'utils.oss_func',
|
||||
'utils.changeFiles',
|
||||
'utils.small_machine_transform',
|
||||
],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False,
|
||||
)
|
||||
|
||||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
[],
|
||||
name='factory_sliceing',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False, # 允许信号传播,使 Ctrl+C 可以中断
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=True, # 显示控制台窗口
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
icon=None, # 可以指定图标文件路径,例如: 'icon.ico'
|
||||
)
|
||||
|
||||
Binary file not shown.
@@ -1,158 +0,0 @@
|
||||
import os,shutil
|
||||
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__)))
|
||||
|
||||
# from download_print_out import download_transform_save_by_json
|
||||
from utils.oss_redis import redisClient
|
||||
from utils.funcs import downloadJsonAndJpgFileAndMoveToCorrectDir, downloadDataByOssAndTransformSave
|
||||
# 默认使用脚本所在目录
|
||||
currentDir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
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'
|
||||
|
||||
|
||||
#判断是否上传了 JSON 文件
|
||||
def step1(versionId):
|
||||
|
||||
# 下载json 文件 和 图片
|
||||
dirName,machineInfo = downloadJsonAndJpgFileAndMoveToCorrectDir(versionId,currentDir)
|
||||
if not dirName:
|
||||
return False
|
||||
|
||||
#判断是否是小机台
|
||||
isSmallMachine = False
|
||||
if str(machineInfo["machine_type"]) == '1':
|
||||
isSmallMachine = True
|
||||
|
||||
|
||||
#下载数据,转换数据
|
||||
res = downloadDataByOssAndTransformSave(dirName,isSmallMachine)
|
||||
if not res:
|
||||
return False
|
||||
|
||||
#判断下载的obj文件数量和json里的是否一致,排除arrange文件夹
|
||||
objFilePath = os.path.join(dirName, 'data')
|
||||
objCounts = 0
|
||||
for file in os.listdir(objFilePath):
|
||||
if file == 'arrange':
|
||||
continue
|
||||
if file.endswith('.obj'):
|
||||
objCounts += 1
|
||||
|
||||
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 下载处理完成的obj文件数量: {objCounts}')
|
||||
# requestApiToUpdateSliceStatus(versionId,objCounts)
|
||||
|
||||
|
||||
|
||||
# 读取 队列中一个数据出来
|
||||
def main(work_dir=None, batch_id=None):
|
||||
|
||||
global currentDir
|
||||
# 如果指定了工作目录,使用指定的目录
|
||||
if work_dir:
|
||||
work_dir = os.path.abspath(work_dir)
|
||||
if not os.path.exists(work_dir):
|
||||
print(f'指定的工作目录不存在: {work_dir},将创建该目录')
|
||||
os.makedirs(work_dir, exist_ok=True)
|
||||
currentDir = work_dir
|
||||
print(f'使用指定的工作目录: {currentDir}')
|
||||
else:
|
||||
print(f'没有指定工作目录,退出')
|
||||
exit(0)
|
||||
|
||||
|
||||
versionId = str(batch_id)
|
||||
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 正在处理版次ID={versionId}')
|
||||
res = step1(versionId)
|
||||
if res == False:
|
||||
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} JSON文件下载数据失败,等待10秒')
|
||||
# time.sleep(10)
|
||||
# continue
|
||||
# 循环处理,直到队列为空
|
||||
# try:
|
||||
# while True:
|
||||
# try:
|
||||
# r = redisClient()
|
||||
# #检测队列是否有值
|
||||
# if r.scard('pb:sliceing') == 0:
|
||||
# print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 队列为空,等待10秒')
|
||||
# time.sleep(10)
|
||||
# continue
|
||||
# #获取队列中的值
|
||||
# data = r.spop('pb:sliceing')
|
||||
# if data is None:
|
||||
# print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 取出的数据为空,等待10秒')
|
||||
# time.sleep(10)
|
||||
# continue
|
||||
# data = data.decode('utf-8')
|
||||
# #判断是否是数字
|
||||
# if not data.isdigit():
|
||||
# print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 取出的数据不是数字,等待10秒')
|
||||
# time.sleep(10)
|
||||
# continue
|
||||
|
||||
# versionId = str(data)
|
||||
# print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 正在处理版次ID={versionId}')
|
||||
# res = step1(versionId)
|
||||
# if res == False:
|
||||
# print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} JSON文件下载数据失败,等待10秒')
|
||||
# time.sleep(10)
|
||||
# continue
|
||||
|
||||
|
||||
|
||||
|
||||
# # 在长时间操作后,确保 Redis 连接仍然有效
|
||||
# # 通过重新获取客户端来触发连接检查
|
||||
# try:
|
||||
# r = redisClient()
|
||||
# r.ping() # 测试连接
|
||||
# except Exception as e:
|
||||
# print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} Redis连接检查失败: {str(e)},将在下次循环时自动重连')
|
||||
|
||||
# #time.sleep(10)
|
||||
# except KeyboardInterrupt:
|
||||
# # 在循环内部捕获 KeyboardInterrupt,允许在 sleep 或操作中被中断
|
||||
# raise # 重新抛出,让外层捕获
|
||||
# except KeyboardInterrupt:
|
||||
# print(f'\n{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 收到中断信号,正在优雅退出...')
|
||||
# print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 程序已停止')
|
||||
# sys.exit(0)
|
||||
|
||||
|
||||
def testMain():
|
||||
global currentDir
|
||||
currentDir = "/Users/dcx/code/make2/script/factory_sliceing_v2/tempData"
|
||||
versionId = '10153' #'10153 10158'
|
||||
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 正在处理版次ID={versionId}')
|
||||
res = step1(versionId)
|
||||
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 处理完成,res={res}')
|
||||
|
||||
if __name__ == '__main__':
|
||||
#testMain() 新增参数 batchId
|
||||
parser = argparse.ArgumentParser(description='排版打印订单处理程序')
|
||||
parser.add_argument(
|
||||
'--work-dir',
|
||||
type=str,
|
||||
default=None,
|
||||
help='指定工作目录(磁盘路径),例如: D:/work 或 /Users/username/work。如果不指定,则使用脚本所在目录'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--batch—id',
|
||||
type=str,
|
||||
default=None,
|
||||
help='指定批次ID'
|
||||
)
|
||||
args = parser.parse_args()
|
||||
main(work_dir=args.work_dir, batch_id=args.batch_id)
|
||||
@@ -1,144 +0,0 @@
|
||||
import os,shutil
|
||||
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__)))
|
||||
|
||||
# from download_print_out import download_transform_save_by_json
|
||||
from utils.oss_redis import redisClient
|
||||
from utils.funcs import downloadJsonAndJpgFileAndMoveToCorrectDir, downloadDataByOssAndTransformSave
|
||||
# 默认使用脚本所在目录
|
||||
currentDir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
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'
|
||||
|
||||
|
||||
#判断是否上传了 JSON 文件
|
||||
def step1(versionId):
|
||||
|
||||
# 下载json 文件 和 图片
|
||||
dirName,machineInfo = downloadJsonAndJpgFileAndMoveToCorrectDir(versionId,currentDir)
|
||||
if not dirName:
|
||||
return False
|
||||
|
||||
#判断是否是小机台
|
||||
isSmallMachine = False
|
||||
if str(machineInfo["machine_type"]) == '1':
|
||||
isSmallMachine = True
|
||||
|
||||
|
||||
#下载数据,转换数据
|
||||
res = downloadDataByOssAndTransformSave(dirName,isSmallMachine)
|
||||
if not res:
|
||||
return False
|
||||
|
||||
#判断下载的obj文件数量和json里的是否一致,排除arrange文件夹
|
||||
objFilePath = os.path.join(dirName, 'data')
|
||||
objCounts = 0
|
||||
for file in os.listdir(objFilePath):
|
||||
if file == 'arrange':
|
||||
continue
|
||||
if file.endswith('.obj'):
|
||||
objCounts += 1
|
||||
|
||||
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 下载处理完成的obj文件数量: {objCounts}')
|
||||
requestApiToUpdateSliceStatus(versionId,objCounts)
|
||||
|
||||
|
||||
|
||||
# 读取 队列中一个数据出来
|
||||
def main(work_dir=None):
|
||||
|
||||
global currentDir
|
||||
# 如果指定了工作目录,使用指定的目录
|
||||
if work_dir:
|
||||
work_dir = os.path.abspath(work_dir)
|
||||
if not os.path.exists(work_dir):
|
||||
print(f'指定的工作目录不存在: {work_dir},将创建该目录')
|
||||
os.makedirs(work_dir, exist_ok=True)
|
||||
currentDir = work_dir
|
||||
print(f'使用指定的工作目录: {currentDir}')
|
||||
else:
|
||||
print(f'没有指定工作目录,退出')
|
||||
exit(0)
|
||||
|
||||
# 循环处理,直到队列为空
|
||||
try:
|
||||
while True:
|
||||
try:
|
||||
r = redisClient()
|
||||
#检测队列是否有值
|
||||
if r.scard('pb:sliceing') == 0:
|
||||
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 队列为空,等待10秒')
|
||||
time.sleep(10)
|
||||
continue
|
||||
#获取队列中的值
|
||||
data = r.spop('pb:sliceing')
|
||||
if data is None:
|
||||
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 取出的数据为空,等待10秒')
|
||||
time.sleep(10)
|
||||
continue
|
||||
data = data.decode('utf-8')
|
||||
#判断是否是数字
|
||||
if not data.isdigit():
|
||||
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 取出的数据不是数字,等待10秒')
|
||||
time.sleep(10)
|
||||
continue
|
||||
|
||||
versionId = str(data)
|
||||
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 正在处理版次ID={versionId}')
|
||||
res = step1(versionId)
|
||||
if res == False:
|
||||
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} JSON文件下载数据失败,等待10秒')
|
||||
time.sleep(10)
|
||||
continue
|
||||
|
||||
|
||||
|
||||
|
||||
# 在长时间操作后,确保 Redis 连接仍然有效
|
||||
# 通过重新获取客户端来触发连接检查
|
||||
try:
|
||||
r = redisClient()
|
||||
r.ping() # 测试连接
|
||||
except Exception as e:
|
||||
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} Redis连接检查失败: {str(e)},将在下次循环时自动重连')
|
||||
|
||||
#time.sleep(10)
|
||||
except KeyboardInterrupt:
|
||||
# 在循环内部捕获 KeyboardInterrupt,允许在 sleep 或操作中被中断
|
||||
raise # 重新抛出,让外层捕获
|
||||
except KeyboardInterrupt:
|
||||
print(f'\n{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 收到中断信号,正在优雅退出...')
|
||||
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 程序已停止')
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def testMain():
|
||||
global currentDir
|
||||
currentDir = "/Users/dcx/code/make2/script/factory_sliceing_v2/tempData"
|
||||
versionId = '10153' #'10153 10158'
|
||||
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 正在处理版次ID={versionId}')
|
||||
res = step1(versionId)
|
||||
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)
|
||||
@@ -1,7 +0,0 @@
|
||||
redis>=4.0.0
|
||||
oss2>=2.17.0
|
||||
requests>=2.28.0
|
||||
pyinstaller>=5.0.0
|
||||
|
||||
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
import os,time
|
||||
import re
|
||||
from .logs import log
|
||||
|
||||
|
||||
def changeMtlFile(localMtlPath,newName):
|
||||
beginTime = time.time()
|
||||
try:
|
||||
# 使用更高效的文件读取方式 [6,8](@ref)
|
||||
with open(localMtlPath, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# 使用字符串方法直接查找和替换,避免不必要的循环 [9](@ref)
|
||||
lines = content.split('\n')
|
||||
for i, line in enumerate(lines):
|
||||
stripped_line = line.strip()
|
||||
if stripped_line.startswith('map_Kd '):
|
||||
lines[i] = f"map_Kd {newName}"
|
||||
break
|
||||
# 批量写入,减少I/O操作 [6](@ref)
|
||||
with open(localMtlPath, 'w', encoding='utf-8') as f:
|
||||
f.write('\n'.join(lines))
|
||||
|
||||
except IOError as e:
|
||||
log(f"处理文件 {localMtlPath} 时出错: {e}")
|
||||
return False
|
||||
except UnicodeDecodeError as e:
|
||||
log(f"文件编码错误 {localMtlPath}: {e}")
|
||||
return False
|
||||
endTime = time.time()
|
||||
log(f"修改文件文件内容 {localMtlPath} : 耗时{endTime - beginTime}秒")
|
||||
return True
|
||||
|
||||
|
||||
|
||||
|
||||
def changeObjFile(localObjPath,newName):
|
||||
beginTime = time.time()
|
||||
try:
|
||||
# 优化策略:mtllib通常在文件前几行,先只读取前100行
|
||||
# 对于大文件(如155万行),这样可以大幅减少内存使用和处理时间
|
||||
with open(localObjPath, 'r', encoding='utf-8') as f:
|
||||
first_100_lines = []
|
||||
mtllib_line_idx = -1
|
||||
|
||||
# 读取前100行查找mtllib
|
||||
for i in range(100):
|
||||
line = f.readline()
|
||||
if not line:
|
||||
break
|
||||
first_100_lines.append(line)
|
||||
if mtllib_line_idx == -1 and line.strip().startswith('mtllib '):
|
||||
mtllib_line_idx = i
|
||||
|
||||
# 如果在前100行找到了mtllib,直接修改
|
||||
if mtllib_line_idx != -1:
|
||||
parts = first_100_lines[mtllib_line_idx].split(' ', 1)
|
||||
if len(parts) > 1:
|
||||
#old_mtl_name = parts[1].strip()
|
||||
new_mtl_name = f"{newName}"
|
||||
first_100_lines[mtllib_line_idx] = f"mtllib {new_mtl_name}\n"
|
||||
# 读取剩余内容并合并
|
||||
remaining_content = f.read()
|
||||
# 写回文件
|
||||
with open(localObjPath, 'w', encoding='utf-8') as f_out:
|
||||
f_out.writelines(first_100_lines)
|
||||
if remaining_content:
|
||||
f_out.write(remaining_content)
|
||||
|
||||
endTime = time.time()
|
||||
log(f"修改文件文件内容 {localObjPath} : 耗时{endTime - beginTime}秒")
|
||||
return True
|
||||
|
||||
log(f"浪费时间读取完整的文件,进行替换mtllib路径, localObjPath={localObjPath}, newName={newName}")
|
||||
# 如果前100行没找到,读取整个文件(很少见的情况)
|
||||
f.seek(0) # 重置文件指针
|
||||
content = f.read()
|
||||
# 使用正则表达式一次性替换(更高效)
|
||||
pattern = r'^mtllib\s+(.+)$'
|
||||
def replace_mtllib(match):
|
||||
old_mtl_name = match.group(1).strip()
|
||||
new_mtl_name = f"{newName}_{old_mtl_name}"
|
||||
return f"mtllib {new_mtl_name}"
|
||||
|
||||
new_content = re.sub(pattern, replace_mtllib, content, flags=re.MULTILINE)
|
||||
if new_content != content:
|
||||
with open(localObjPath, 'w', encoding='utf-8') as f_out:
|
||||
f_out.write(new_content)
|
||||
endTime = time.time()
|
||||
log(f"完整读取修改文件文件内容 {localObjPath} : 耗时{endTime - beginTime}秒")
|
||||
return True
|
||||
|
||||
except IOError as e:
|
||||
print(f"处理文件 {localObjPath} 时出错: {e}")
|
||||
return False
|
||||
except UnicodeDecodeError as e:
|
||||
print(f"文件编码错误 {localObjPath}: {e}")
|
||||
return False
|
||||
|
||||
return False
|
||||
@@ -1,688 +0,0 @@
|
||||
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
|
||||
@@ -1,4 +0,0 @@
|
||||
import time
|
||||
|
||||
def log(message):
|
||||
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} {message}')
|
||||
@@ -1,45 +0,0 @@
|
||||
import os
|
||||
from .oss_redis import ossClient
|
||||
from .logs import log
|
||||
|
||||
# 从 OSS 下载文件到本地,并做完整性校验(状态码 + 文件大小)
|
||||
def download_file_with_check(ossFilePath, localFilePath):
|
||||
try:
|
||||
log(f"开始从 OSS 下载文件: {ossFilePath} -> {localFilePath}")
|
||||
if not checkFileExists(ossFilePath):
|
||||
log(f"文件不存在: {ossFilePath}")
|
||||
return False
|
||||
# 同步阻塞下载,下载完成后才会返回
|
||||
result = ossClient().get_object_to_file(ossFilePath, localFilePath)
|
||||
|
||||
# 1. 检查 HTTP 状态码
|
||||
status = getattr(result, "status", None)
|
||||
if status != 200:
|
||||
log(f"下载失败,HTTP 状态码异常: status={status}")
|
||||
return False
|
||||
|
||||
# 2. 远端 / 本地文件大小对比,作为二次校验
|
||||
remote_meta = ossClient().head_object(ossFilePath)
|
||||
remote_size = getattr(remote_meta, "content_length", None)
|
||||
if remote_size is None:
|
||||
log("无法获取远端文件大小,放弃本次下载结果")
|
||||
return False
|
||||
|
||||
if not os.path.exists(localFilePath):
|
||||
log("本地文件不存在,下载可能失败")
|
||||
return False
|
||||
|
||||
local_size = os.path.getsize(localFilePath)
|
||||
if remote_size != local_size:
|
||||
log(f"文件大小不一致,下载可能不完整: remote={remote_size}, local={local_size}")
|
||||
return False
|
||||
|
||||
log("文件下载成功且完整性校验通过")
|
||||
return True
|
||||
except Exception as e:
|
||||
log(f"下载文件出现异常: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
def checkFileExists(ossFilePath):
|
||||
return ossClient().object_exists(ossFilePath)
|
||||
@@ -1,125 +0,0 @@
|
||||
import oss2,redis
|
||||
import time
|
||||
import logging
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 连接oss - 单例模式
|
||||
class OSSClientSingleton:
|
||||
_instance = None
|
||||
_client = None
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super(OSSClientSingleton, cls).__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def get_client(self):
|
||||
if self._client is None:
|
||||
AccessKeyId = 'LTAI5tSReWm8hz7dSYxxth8f'
|
||||
AccessKeySecret = '8ywTDF9upPAtvgXtLKALY2iMYHIxdS'
|
||||
Endpoint = 'oss-cn-shanghai.aliyuncs.com'
|
||||
Bucket = 'suwa3d-securedata'
|
||||
self._client = oss2.Bucket(oss2.Auth(AccessKeyId, AccessKeySecret), Endpoint, Bucket)
|
||||
return self._client
|
||||
|
||||
def ossClient():
|
||||
"""获取OSS客户端单例"""
|
||||
return OSSClientSingleton().get_client()
|
||||
|
||||
|
||||
#连接redis,单例模式
|
||||
class RedisClientSingleton:
|
||||
_instance = None
|
||||
_client = None
|
||||
|
||||
# Redis连接配置
|
||||
REDIS_CONFIG = {
|
||||
'host': 'mp.api.suwa3d.com',
|
||||
'password': 'kcV2000',
|
||||
'port': 6379,
|
||||
'db': 6,
|
||||
'socket_timeout': 30, # 操作超时30秒
|
||||
'socket_connect_timeout': 10, # 连接超时10秒
|
||||
'socket_keepalive': True, # 启用 TCP keepalive
|
||||
'socket_keepalive_options': {}, # keepalive 选项
|
||||
'health_check_interval': 30 # 健康检查间隔30秒
|
||||
}
|
||||
|
||||
# 重试配置
|
||||
RETRY_INTERVAL = 5 # 重试间隔(秒)
|
||||
MAX_RETRY_INTERVAL = 60 # 最大重试间隔(秒),用于指数退避
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super(RedisClientSingleton, cls).__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def _create_redis_client(self):
|
||||
"""创建Redis客户端"""
|
||||
return redis.Redis(**self.REDIS_CONFIG)
|
||||
|
||||
def _connect_with_retry(self, max_retries=None, retry_interval=None):
|
||||
"""
|
||||
带重试机制的Redis连接方法
|
||||
:param max_retries: 最大重试次数,None表示无限重试
|
||||
:param retry_interval: 重试间隔(秒),None表示使用默认值,支持指数退避
|
||||
:return: Redis客户端实例
|
||||
"""
|
||||
if retry_interval is None:
|
||||
retry_interval = self.RETRY_INTERVAL
|
||||
|
||||
retry_count = 0
|
||||
current_interval = retry_interval
|
||||
|
||||
while True:
|
||||
try:
|
||||
logger.info(f"尝试连接Redis (第 {retry_count + 1} 次)...")
|
||||
client = self._create_redis_client()
|
||||
# 测试连接
|
||||
client.ping()
|
||||
logger.info("Redis连接成功!")
|
||||
return client
|
||||
except (redis.ConnectionError, redis.TimeoutError, AttributeError, Exception) as e:
|
||||
retry_count += 1
|
||||
error_msg = str(e)
|
||||
logger.warning(f"Redis连接失败 (第 {retry_count} 次): {error_msg}")
|
||||
|
||||
# 如果设置了最大重试次数且已达到,则抛出异常
|
||||
if max_retries is not None and retry_count >= max_retries:
|
||||
logger.error(f"达到最大重试次数 {max_retries},停止重试")
|
||||
raise
|
||||
|
||||
# 指数退避:每次重试间隔逐渐增加,但不超过最大值
|
||||
logger.info(f"等待 {current_interval} 秒后重试...")
|
||||
time.sleep(current_interval)
|
||||
current_interval = min(current_interval * 1.5, self.MAX_RETRY_INTERVAL)
|
||||
|
||||
def get_client(self):
|
||||
"""
|
||||
获取Redis客户端,如果连接断开则自动重连(带重试机制)
|
||||
:return: Redis客户端实例
|
||||
"""
|
||||
if self._client is None:
|
||||
# 首次连接,使用无限重试直到成功
|
||||
logger.info("初始化Redis连接...")
|
||||
self._client = self._connect_with_retry(max_retries=None)
|
||||
else:
|
||||
# 检查连接是否有效,如果断开则重新连接(带重试)
|
||||
try:
|
||||
self._client.ping()
|
||||
except (redis.ConnectionError, redis.TimeoutError, AttributeError) as e:
|
||||
# 连接断开,重新创建连接(使用无限重试直到成功)
|
||||
logger.warning(f"Redis连接已断开: {str(e)},开始重新连接...")
|
||||
self._client = None
|
||||
self._client = self._connect_with_retry(max_retries=None)
|
||||
|
||||
return self._client
|
||||
|
||||
|
||||
|
||||
def redisClient():
|
||||
"""获取Redis客户端单例"""
|
||||
return RedisClientSingleton().get_client()
|
||||
@@ -1,193 +0,0 @@
|
||||
import os,time,sys,argparse
|
||||
import numpy as np
|
||||
import json
|
||||
import warnings
|
||||
|
||||
|
||||
def findBpyModule():
|
||||
# /Applications/Blender.app/Contents/Resources/4.4/python/bin
|
||||
blender_bin_path = '/Applications/Blender.app/Contents/Resources/4.4/python/bin/blender'
|
||||
return blender_bin_path
|
||||
|
||||
def custom_mesh_transform(vertices, transform_matrix):
|
||||
"""
|
||||
手动实现网格变换:对每个顶点应用齐次变换矩阵
|
||||
参数:
|
||||
vertices: 网格顶点数组 (N, 3)
|
||||
transform_matrix: 4x4 齐次变换矩阵
|
||||
返回:
|
||||
变换后的顶点数组 (N, 3)
|
||||
"""
|
||||
# 验证变换矩阵
|
||||
transform_matrix = np.array(transform_matrix, dtype=np.float64)
|
||||
|
||||
# 检查矩阵形状
|
||||
if transform_matrix.shape != (4, 4):
|
||||
raise ValueError(f"变换矩阵必须是 4x4 矩阵,当前形状: {transform_matrix.shape}")
|
||||
|
||||
# 检查矩阵中是否包含无效值(NaN 或 Inf)
|
||||
if np.any(np.isnan(transform_matrix)) or np.any(np.isinf(transform_matrix)):
|
||||
invalid_count = np.sum(np.isnan(transform_matrix)) + np.sum(np.isinf(transform_matrix))
|
||||
raise ValueError(f"变换矩阵包含无效值 (NaN/Inf): {invalid_count} 个无效元素\n矩阵:\n{transform_matrix}")
|
||||
|
||||
# 检查矩阵行列式(对于齐次变换矩阵,最后一行应该是 [0, 0, 0, 1])
|
||||
# 如果矩阵接近奇异,行列式会接近0
|
||||
det = np.linalg.det(transform_matrix[:3, :3]) # 只检查3x3旋转/缩放部分
|
||||
if abs(det) < 1e-10:
|
||||
print(f"警告: 变换矩阵的3x3部分行列式接近0 (det={det:.2e}),可能导致数值不稳定")
|
||||
|
||||
# 检查最后一行是否为 [0, 0, 0, 1](齐次变换矩阵的标准形式)
|
||||
last_row = transform_matrix[3, :]
|
||||
if not np.allclose(last_row, [0, 0, 0, 1], atol=1e-6):
|
||||
print(f"警告: 变换矩阵最后一行不是标准形式 [0,0,0,1],而是: {last_row}")
|
||||
|
||||
# 1. 顶点转齐次坐标 (N, 3) → (N, 4)
|
||||
homogeneous_vertices = np.hstack((vertices, np.ones((vertices.shape[0], 1))))
|
||||
|
||||
# 2. 应用变换矩阵:矩阵乘法 (4x4) * (4xN) → (4xN)
|
||||
# 捕获警告并检查结果
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always") # 捕获所有警告
|
||||
transformed_homogeneous = transform_matrix @ homogeneous_vertices.T
|
||||
|
||||
# 如果有警告,记录详细信息
|
||||
if w:
|
||||
warning_msgs = [str(warning.message) for warning in w]
|
||||
print(f"警告: 矩阵乘法过程中出现 {len(w)} 个警告:")
|
||||
for msg in warning_msgs:
|
||||
print(f" - {msg}")
|
||||
print(f"变换矩阵:\n{transform_matrix}")
|
||||
print(f"矩阵包含 NaN: {np.any(np.isnan(transform_matrix))}")
|
||||
print(f"矩阵包含 Inf: {np.any(np.isinf(transform_matrix))}")
|
||||
if not np.any(np.isnan(transform_matrix)) and not np.any(np.isinf(transform_matrix)):
|
||||
print(f"矩阵最大值: {np.nanmax(transform_matrix):.6e}")
|
||||
print(f"矩阵最小值: {np.nanmin(transform_matrix):.6e}")
|
||||
|
||||
# 检查结果是否包含无效值
|
||||
if np.any(np.isnan(transformed_homogeneous)) or np.any(np.isinf(transformed_homogeneous)):
|
||||
invalid_count = np.sum(np.isnan(transformed_homogeneous)) + np.sum(np.isinf(transformed_homogeneous))
|
||||
raise ValueError(f"变换后的顶点包含无效值 (NaN/Inf): {invalid_count} 个无效元素")
|
||||
|
||||
# 3. 转回非齐次坐标 (3xN) → (N, 3)
|
||||
transformed_vertices = transformed_homogeneous[:3, :].T
|
||||
return transformed_vertices
|
||||
|
||||
# {
|
||||
# "file_name": "888687_324912_P117183_13.8cm_x1.obj",
|
||||
# "transform": {
|
||||
# "homo_matrix": [
|
||||
# [-0.09437,0.995195,0.026116,77.975499],
|
||||
# [-0.58016,-0.033658,-0.813807,322.60588],
|
||||
# [-0.809017,-0.09195,0.580549,24.503686],
|
||||
# [0.0,0.0,0.0,1.0]
|
||||
# ]
|
||||
# }
|
||||
# },
|
||||
def transform_save(obj_path,homo_matrix):
|
||||
import bpy
|
||||
# 延迟导入 bpy,只在 Blender 环境中可用
|
||||
# try:
|
||||
# blender_bin_path = findBpyModule()
|
||||
# subprocess.run([blender_bin_path, "--background", "--python-expr", "import bpy"])
|
||||
# except ImportError:
|
||||
# log("错误: bpy 模块不可用。此函数只能在 Blender 环境中运行。")
|
||||
# log("请使用 Blender 的 Python 解释器运行此脚本,或通过 Blender 命令行运行。")
|
||||
# raise ImportError("bpy 模块不可用。此函数只能在 Blender 环境中运行。")
|
||||
print("开始转换数据")
|
||||
obj_name = obj_path.split("/")[-1]
|
||||
print(f"obj_name={obj_name}")
|
||||
# 清除场景
|
||||
bpy.ops.object.select_all(action='SELECT')
|
||||
bpy.ops.object.delete(use_global=False)
|
||||
|
||||
## 尺寸调整,环境设置
|
||||
bpy.ops.object.delete(use_global=False, confirm=False)
|
||||
bpy.context.scene.unit_settings.length_unit = 'CENTIMETERS'
|
||||
bpy.context.scene.unit_settings.scale_length = 0.001
|
||||
bpy.context.scene.unit_settings.mass_unit = 'GRAMS'
|
||||
|
||||
reconstructed_matrix = np.array(homo_matrix, dtype=np.float64)
|
||||
bpy.ops.wm.obj_import(filepath=obj_path)
|
||||
imported_object = bpy.context.object
|
||||
|
||||
if imported_object is None or imported_object.type != 'MESH':
|
||||
print(f"警告: 未能成功导入网格对象 {obj_name}。跳过。")
|
||||
return
|
||||
|
||||
mesh_data = imported_object.data
|
||||
mesh_data.calc_loop_triangles()
|
||||
original_vertices = np.empty(len(mesh_data.vertices) * 3, dtype=np.float64)
|
||||
mesh_data.vertices.foreach_get('co', original_vertices)
|
||||
original_vertices = original_vertices.reshape(-1, 3)
|
||||
|
||||
try:
|
||||
transformed_vertices = custom_mesh_transform(original_vertices, reconstructed_matrix)
|
||||
|
||||
# 验证转换后的顶点是否有效
|
||||
if np.any(np.isnan(transformed_vertices)) or np.any(np.isinf(transformed_vertices)):
|
||||
invalid_count = np.sum(np.isnan(transformed_vertices)) + np.sum(np.isinf(transformed_vertices))
|
||||
error_msg = f"错误: 转换后的顶点包含无效值 (NaN/Inf): {invalid_count} 个无效元素, obj_name={obj_name}"
|
||||
print(error_msg)
|
||||
print(f"变换矩阵:\n{reconstructed_matrix}")
|
||||
raise ValueError(error_msg)
|
||||
|
||||
print(f"转换成功: {len(transformed_vertices)} 个顶点")
|
||||
print(f"顶点坐标范围: X[{transformed_vertices[:, 0].min():.2f}, {transformed_vertices[:, 0].max():.2f}], "
|
||||
f"Y[{transformed_vertices[:, 1].min():.2f}, {transformed_vertices[:, 1].max():.2f}], "
|
||||
f"Z[{transformed_vertices[:, 2].min():.2f}, {transformed_vertices[:, 2].max():.2f}]")
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"转换顶点时发生错误, obj_name={obj_name}, error={str(e)}"
|
||||
print(error_msg)
|
||||
print(f"变换矩阵:\n{reconstructed_matrix}")
|
||||
raise
|
||||
|
||||
offset = np.array([-380, -345, 0])
|
||||
transformed_vertices += offset
|
||||
print(f"已对模型 {obj_name} 应用偏移: {offset}")
|
||||
|
||||
flattened_verts = transformed_vertices.reshape(-1)
|
||||
mesh_data.vertices.foreach_set('co', flattened_verts)
|
||||
|
||||
mesh_data.update()
|
||||
|
||||
# meshes.append(imported_object)
|
||||
|
||||
# 导出前确保选中当前对象
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
imported_object.select_set(True)
|
||||
bpy.context.view_layer.objects.active = imported_object
|
||||
bpy.ops.wm.obj_export(
|
||||
filepath=obj_path,
|
||||
export_selected_objects=True,
|
||||
export_materials=True,
|
||||
path_mode='AUTO'
|
||||
)
|
||||
|
||||
#os.remove(old_mtl_path)
|
||||
|
||||
if __name__ == "__main__":
|
||||
script_args = []
|
||||
|
||||
try:
|
||||
separator_index = sys.argv.index("--")
|
||||
script_args = sys.argv[separator_index + 1:]
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
parser.add_argument("--objPathName", type=str, required=True, help="obj文件路径")
|
||||
parser.add_argument("--trans", type=str, required=True, help="旋转参数(JSON格式)")
|
||||
args = parser.parse_args(script_args)
|
||||
|
||||
print(f"objPathName={args.objPathName}, trans={args.trans}")
|
||||
|
||||
# 解析 JSON 字符串为列表
|
||||
try:
|
||||
homo_matrix = json.loads(args.trans)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"错误: 无法解析 trans 参数为 JSON: {e}")
|
||||
sys.exit(1)
|
||||
print("进入转换方法")
|
||||
transform_save(args.objPathName, homo_matrix)
|
||||
+10
-51
@@ -1,54 +1,13 @@
|
||||
from pathlib import Path
|
||||
import os
|
||||
import shutil
|
||||
# 指定要遍历的文件夹路径
|
||||
folder_path = Path('G://obj_fix/print')
|
||||
import requests,json,os
|
||||
|
||||
# 定义函数来递归遍历文件夹下的所有文件
|
||||
def list_files(directory):
|
||||
for root, dirs, files in os.walk(directory):
|
||||
for file in files:
|
||||
yield os.path.join(root, file)
|
||||
objFilePath = "/Users/dcx/code/make2/script/factory_sliceing/batchPrint/45/data"
|
||||
objCounts = 0
|
||||
for file in os.listdir(objFilePath):
|
||||
print(file)
|
||||
# if file == 'arrange':
|
||||
# continue
|
||||
if file.endswith('.obj'):
|
||||
objCounts += 1
|
||||
|
||||
# 指定要遍历的文件夹路径
|
||||
# folder_path = folder_path
|
||||
|
||||
# 使用list_files函数来列出文件夹下的所有文件
|
||||
nums = 0
|
||||
for file_path in list_files(folder_path):
|
||||
|
||||
if "around" in file_path:
|
||||
continue
|
||||
|
||||
|
||||
if "_new.obj" not in file_path:
|
||||
continue
|
||||
|
||||
|
||||
|
||||
|
||||
#提取pid
|
||||
arrTemp = file_path.split('\\')
|
||||
pid = arrTemp[len(arrTemp)-2]
|
||||
|
||||
#判段是否存在 {pid}.obj 文件,存在的话就进行重命名
|
||||
initObjPath = f'G://obj_fix/print/{pid}/{pid}.obj'
|
||||
if not os.path.exists(initObjPath):
|
||||
continue
|
||||
|
||||
#存在的话就重命名文件
|
||||
old_file_path = f'G://obj_fix/print/{pid}/{pid}_old.obj'
|
||||
os.rename(initObjPath, old_file_path)
|
||||
|
||||
|
||||
newObjfilePath = f'G://obj_fix/print/{pid}/{pid}_new.obj'
|
||||
os.rename(newObjfilePath, initObjPath)
|
||||
|
||||
# # 将 new.obj 文件复制到指定的文件夹下 G:\obj_fix\print\{pid}\
|
||||
|
||||
# source_obj = file_path
|
||||
# target_obj = f'G://obj_fix/print/{pid}/{pid}_new.obj'
|
||||
# print(f"当前正在处理{pid}")
|
||||
# shutil.copy(source_obj, target_obj)
|
||||
nums += 1
|
||||
print(pid,file_path,nums)
|
||||
print(objCounts)
|
||||
+21
-30
@@ -1,33 +1,24 @@
|
||||
import os
|
||||
import shutil
|
||||
import requests
|
||||
|
||||
folder_path = 'G:\\obj_fix\print'
|
||||
#向 https://lenovodata.blob.core.windows.net/secretdata/photos/1/photo2/92_2.jpg?se=2025-10-30T10%3A01%3A13Z&sig=bv2KAPuNkYT5%2B4tNVMo53kWKIoniQaDkog27AYAXm4s%3D&sp=rcw&spr=https&sr=b&st=2025-10-30T09%3A01%3A13Z&sv=2025-05-05
|
||||
# 上传图片
|
||||
def upload_image(image_path):
|
||||
url = 'https://lenovodata.blob.core.windows.net/secretdata/photos/1/photo2/92_2.jpg?se=2025-10-30T10%3A01%3A13Z&sig=bv2KAPuNkYT5%2B4tNVMo53kWKIoniQaDkog27AYAXm4s%3D&sp=rcw&spr=https&sr=b&st=2025-10-30T09%3A01%3A13Z&sv=2025-05-05'
|
||||
with open(image_path, 'rb') as f:
|
||||
image_data = f.read()
|
||||
headers = {
|
||||
'x-ms-blob-type': 'BlockBlob',
|
||||
'Content-Type': 'image/jpeg',
|
||||
}
|
||||
response = requests.put(url, data=image_data, headers=headers)
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except requests.HTTPError as e:
|
||||
raise RuntimeError(f'上传失败 {response.status_code}: {response.text}') from e
|
||||
return {
|
||||
'status_code': response.status_code,
|
||||
'etag': response.headers.get('ETag'),
|
||||
}
|
||||
|
||||
# 遍历文件夹下的所有文件
|
||||
# 定义函数来递归遍历文件夹下的所有文件
|
||||
def list_files(directory):
|
||||
for root, dirs, files in os.walk(directory):
|
||||
for file in files:
|
||||
yield os.path.join(root, file)
|
||||
|
||||
nums = 0
|
||||
for file_path in list_files(folder_path):
|
||||
if "_preview.png" not in file_path:
|
||||
continue
|
||||
print(file_path)
|
||||
nums += 1
|
||||
# #提取pid
|
||||
# arrTemp = file_path.split('\\')
|
||||
# pid = arrTemp[len(arrTemp)-2]
|
||||
|
||||
# #判断 G:\obj_fix\print\wrong\ 文件夹里 是否存在 {pid}_preview.png 文件
|
||||
|
||||
# filePath = f'G://obj_fix\print\wrong\{pid}_preview.png'
|
||||
# #不存在跳过
|
||||
# if not os.path.exists(filePath):
|
||||
# continue
|
||||
|
||||
# #存在的话就重命名文件
|
||||
# new_file_path = f'G://obj_fix\print\wrong\{pid}_preview_already_right.png'
|
||||
# os.rename(filePath, new_file_path)
|
||||
print(nums)
|
||||
upload_image('273628Tex1.jpg')
|
||||
Reference in new issue
Block a user