diff --git a/git_src.txt b/git_src.txt new file mode 100644 index 0000000..2fa1d9d --- /dev/null +++ b/git_src.txt @@ -0,0 +1,85 @@ +src/api/__init__.py +src/api/aluminum_price_routes.py +src/api/auth_routes.py +src/api/inventory/__init__.py +src/api/inventory/customer_routes.py +src/api/inventory/dashboard_routes.py +src/api/inventory/finance_routes.py +src/api/inventory/inventory_routes.py +src/api/inventory/material_routes.py +src/api/inventory/product_routes.py +src/api/inventory/purchase_order_routes.py +src/api/inventory/sales_order_routes.py +src/api/inventory/schemas/__init__.py +src/api/inventory/schemas/common_schemas.py +src/api/inventory/schemas/customer_schemas.py +src/api/inventory/schemas/finance_schemas.py +src/api/inventory/schemas/inventory_schemas.py +src/api/inventory/schemas/material_schemas.py +src/api/inventory/schemas/product_schemas.py +src/api/inventory/schemas/purchase_order_schemas.py +src/api/inventory/schemas/sales_order_schemas.py +src/api/inventory/schemas/stock_movement_schemas.py +src/api/inventory/schemas/supplier_schemas.py +src/api/inventory/schemas/warehouse_schemas.py +src/api/inventory/stock_movement_routes.py +src/api/inventory/supplier_routes.py +src/api/inventory/utils.py +src/api/inventory/warehouse_routes.py +src/api/routes.py +src/api/v1/__init__.py +src/api/v1/advanced_router.py +src/api/v1/cam_router.py +src/api/v1/debug_router.py +src/api/v1/health_router.py +src/api/v1/history_router.py +src/api/v1/task_router.py +src/api/v1/upload_router.py +src/core/__init__.py +src/core/ai_mold_assistant.py +src/core/ai_parting_detector.py +src/core/aluminum_foam_mold.py +src/core/base_mold_generator.py +src/core/cad_exporter.py +src/core/cavity_layout_optimizer.py +src/core/geometry_analyzer.py +src/core/mesh_generator.py +src/core/mold_cam.py +src/core/mold_generator.py +src/core/mold_machining.py +src/core/mold_quality_inspector.py +src/core/mold_system_designer.py +src/core/multi_scheme_planner.py +src/core/parting_candidate_generator.py +src/core/parting_scheme_scorer.py +src/core/side_action_designer.py +src/core/stp_parser.py +src/database/__init__.py +src/database/database.py +src/database/init_db.py +src/database/migrate_db.py +src/main.py +src/models/database.py +src/models/schemas.py +src/scripts/create_admin.py +src/services/__init__.py +src/services/aluminum_price_service.py +src/services/auth_service.py +src/services/calculation_service.py +src/services/cam_bundle_service.py +src/services/material_service.py +src/services/processing_service.py +src/services/redis_task_manager.py +src/services/storage_integration.py +src/services/storage_integration_rustfs.py +src/services/storage_service.py +src/services/task_query_service.py +src/services/verification_service.py +src/storage/__init__.py +src/storage/init_storage.py +src/storage/object_storage.py +src/storage/rustfs_storage.py +src/utils/__init__.py +src/utils/file_handler.py +src/utils/html_generator.py +src/utils/logger.py diff --git a/local_src.txt b/local_src.txt new file mode 100644 index 0000000..af48d1b --- /dev/null +++ b/local_src.txt @@ -0,0 +1,110 @@ +src/main.py +src/api/aluminum_price_routes.py +src/api/auth_routes.py +src/api/routes.py +src/api/__init__.py +src/api/inventory/customer_routes.py +src/api/inventory/dashboard_routes.py +src/api/inventory/finance_routes.py +src/api/inventory/inventory_routes.py +src/api/inventory/material_routes.py +src/api/inventory/product_routes.py +src/api/inventory/purchase_order_routes.py +src/api/inventory/sales_order_routes.py +src/api/inventory/stock_movement_routes.py +src/api/inventory/supplier_routes.py +src/api/inventory/utils.py +src/api/inventory/warehouse_routes.py +src/api/inventory/__init__.py +src/api/inventory/schemas/common_schemas.py +src/api/inventory/schemas/customer_schemas.py +src/api/inventory/schemas/finance_schemas.py +src/api/inventory/schemas/inventory_schemas.py +src/api/inventory/schemas/material_schemas.py +src/api/inventory/schemas/product_schemas.py +src/api/inventory/schemas/purchase_order_schemas.py +src/api/inventory/schemas/sales_order_schemas.py +src/api/inventory/schemas/stock_movement_schemas.py +src/api/inventory/schemas/supplier_schemas.py +src/api/inventory/schemas/warehouse_schemas.py +src/api/inventory/schemas/__init__.py +src/api/inventory/__pycache__/__init__.cpython-312.pyc +src/api/v1/advanced_router.py +src/api/v1/cam_router.py +src/api/v1/debug_router.py +src/api/v1/health_router.py +src/api/v1/history_router.py +src/api/v1/task_router.py +src/api/v1/upload_router.py +src/api/v1/__init__.py +src/api/v1/__pycache__/advanced_router.cpython-312.pyc +src/api/v1/__pycache__/upload_router.cpython-312.pyc +src/api/v1/__pycache__/__init__.cpython-312.pyc +src/api/__pycache__/aluminum_price_routes.cpython-312.pyc +src/api/__pycache__/routes.cpython-312.pyc +src/api/__pycache__/__init__.cpython-312.pyc +src/core/ai_mold_assistant.py +src/core/ai_parting_detector.py +src/core/aluminum_foam_mold.py +src/core/cad_exporter.py +src/core/cavity_layout_optimizer.py +src/core/geometry_analyzer.py +src/core/mesh_generator.py +src/core/mold_cam.py +src/core/mold_generator.py +src/core/mold_machining.py +src/core/mold_quality_inspector.py +src/core/mold_system_designer.py +src/core/multi_scheme_planner.py +src/core/parting_candidate_generator.py +src/core/parting_scheme_scorer.py +src/core/side_action_designer.py +src/core/stp_parser.py +src/core/__init__.py +src/core/__pycache__/ai_parting_detector.cpython-312.pyc +src/core/__pycache__/aluminum_foam_mold.cpython-312.pyc +src/core/__pycache__/base_mold_generator.cpython-312.pyc +src/core/__pycache__/cad_exporter.cpython-312.pyc +src/core/__pycache__/mold_generator.cpython-312.pyc +src/core/__pycache__/multi_scheme_planner.cpython-312.pyc +src/core/__pycache__/side_action_designer.cpython-312.pyc +src/core/__pycache__/stp_parser.cpython-312.pyc +src/core/__pycache__/__init__.cpython-312.pyc +src/database/database.py +src/database/migrate_db.py +src/database/__init__.py +src/database/__pycache__/database.cpython-312.pyc +src/database/__pycache__/__init__.cpython-312.pyc +src/models/database.py +src/models/schemas.py +src/models/__pycache__/schemas.cpython-312.pyc +src/scripts/create_admin.py +src/services/aluminum_price_service.py +src/services/auth_service.py +src/services/calculation_service.py +src/services/cam_bundle_service.py +src/services/llm_service.py +src/services/material_service.py +src/services/processing_service.py +src/services/redis_task_manager.py +src/services/storage_integration.py +src/services/storage_integration_rustfs.py +src/services/storage_service.py +src/services/task_query_service.py +src/services/verification_service.py +src/services/__init__.py +src/services/__pycache__/aluminum_price_service.cpython-312.pyc +src/services/__pycache__/processing_service.cpython-312.pyc +src/services/__pycache__/verification_service.cpython-312.pyc +src/services/__pycache__/__init__.cpython-312.pyc +src/storage/init_storage.py +src/storage/object_storage.py +src/storage/rustfs_storage.py +src/storage/__init__.py +src/utils/file_handler.py +src/utils/logger.py +src/utils/__init__.py +src/utils/__pycache__/html_generator.cpython-312.pyc +src/utils/__pycache__/logger.cpython-312.pyc +src/utils/__pycache__/__init__.cpython-312.pyc +src/__pycache__/main.cpython-312.pyc diff --git a/src/api/routes.py b/src/api/routes.py index d9e014b..4781629 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -1116,20 +1116,10 @@ async def process_file_core( logger.info("FreeCAD验证已禁用(设置 ENABLE_FREECAD_VERIFICATION=true 启用)") verification_result = {"status": "disabled", "reason": "FreeCAD验证已禁用"} - # 9.8 LLM 增强分析(可选,不影响主流程) + # 9.8 LLM 增强分析 llm_report = None - llm_parting = None if analysis_result: - llm_report = await llm_service.generate_design_report( - analysis_result, detailed_cavity_json - ) - # 分型推荐:当前无候选方案时跳过 - candidate_schemes = detailed_cavity_json.get("candidate_schemes", []) - if candidate_schemes: - llm_parting = await llm_service.recommend_parting_direction( - geometry_data, candidate_schemes, selected_material, - cavity_count=cavity_count, - ) + llm_report = await llm_service.generate_design_report(analysis_result, detailed_cavity_json) # 10. 完成处理 await storage_service.update_stp_file_status(db_session, stp_file_id, "completed") @@ -1146,7 +1136,6 @@ async def process_file_core( tasks[task_id]["html_file"] = f"/html/{Path(html_file_path).name}" # 只使用文件名 tasks[task_id]["verification"] = verification_result # 添加验证结果 tasks[task_id]["llm_report"] = llm_report - tasks[task_id]["llm_parting_recommendation"] = llm_parting tasks[task_id]["status"] = ProcessingStatus.COMPLETED tasks[task_id]["completed_at"] = str(datetime.now()) diff --git a/src/core/base_mold_generator.py b/src/core/base_mold_generator.py new file mode 100644 index 0000000..8cdea28 --- /dev/null +++ b/src/core/base_mold_generator.py @@ -0,0 +1,905 @@ +from typing import Dict, List, Any, Tuple, Optional +import math +import numpy as np +from OCC.Core.BRepOffsetAPI import BRepOffsetAPI_DraftAngle +from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut, BRepAlgoAPI_Section, BRepAlgoAPI_Common +from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeFace, BRepBuilderAPI_Transform +from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox, BRepPrimAPI_MakeHalfSpace +from OCC.Core.gp import gp_Pln, gp_Dir, gp_Pnt, gp_Trsf, gp_Ax2 +from OCC.Core.TopoDS import TopoDS_Face, topods +from OCC.Core.BRep import BRep_Tool +from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh +from OCC.Core.GProp import GProp_GProps +from OCC.Core.BRepGProp import brepgprop +from OCC.Core.TopExp import TopExp_Explorer +from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_EDGE +from OCC.Core.BRepAdaptor import BRepAdaptor_Surface, BRepAdaptor_Curve +from OCC.Core.Bnd import Bnd_Box +from OCC.Core.BRepBndLib import brepbndlib +from OCC.Core.TopLoc import TopLoc_Location + +from models.schemas import create_mold_cavity_data, create_mold_key_info +from utils.logger import get_logger + +logger = get_logger(__name__) + + +class BaseMoldGenerator: + """模具生成器基类 - 提供共用方法""" + + def __init__(self, shrinkage_rate: float = 0.005, draft_angle: float = 2.0, + material_density: float = 1.05): + self.shrinkage_rate = shrinkage_rate + self.draft_angle = draft_angle + self.material_density = material_density + + self.ai_parting_detector: Optional[Any] = None + self.ai_draft_analyzer: Optional[Any] = None + + def set_ai_model(self, parting_detector: Any = None, draft_analyzer: Any = None): + self.ai_parting_detector = parting_detector + self.ai_draft_analyzer = draft_analyzer + logger.info("AI 模型接口已设置") + + def _apply_shrinkage_compensation(self, shape: Any) -> Any: + scale_factor = 1.0 + self.shrinkage_rate + trsf = gp_Trsf() + trsf.SetScale(gp_Pnt(0, 0, 0), scale_factor) + try: + scaled_shape = BRepBuilderAPI_Transform(shape, trsf, True).Shape() + logger.info(f"收缩率补偿: {self.shrinkage_rate*100:.2f}%, 缩放因子: {scale_factor:.4f}") + return scaled_shape + except Exception as e: + logger.warning(f"收缩率补偿失败: {e}") + return shape + + def _apply_draft_angles(self, shape: Any, parting_surface: Any) -> Any: + try: + draft_direction = self._get_draft_direction(parting_surface) + if draft_direction is None: + logger.warning("无法确定拔模方向,跳过拔模处理") + return shape + + draft_angle_rad = math.radians(self.draft_angle) + draftable_faces = self._find_draftable_faces(shape, draft_direction) + + if not draftable_faces: + logger.info("未找到需要拔模的面,跳过拔模处理") + return shape + + logger.info(f"应用拔模角: {self.draft_angle}°, {len(draftable_faces)} 个面") + + drafted_shape = self._execute_draft(shape, draftable_faces, draft_direction, draft_angle_rad) + return drafted_shape + + except Exception as e: + logger.warning(f"拔模角处理失败,返回原始形状: {e}") + return shape + + def _get_draft_direction(self, parting_surface: Any) -> Optional[gp_Dir]: + try: + surface = BRepAdaptor_Surface(parting_surface) + if surface.GetType() == 0: + return surface.Plane().Position().Direction() + return gp_Dir(0, 0, 1) + except Exception: + return gp_Dir(0, 0, 1) + + def _find_draftable_faces(self, shape: Any, draft_direction: gp_Dir) -> List[Any]: + draftable = [] + explorer = TopExp_Explorer(shape, TopAbs_FACE) + + while explorer.More(): + face = topods.Face(explorer.Current()) + normal = self._get_face_normal(face) + + if normal is not None: + dot = abs(normal.Dot(draft_direction)) + angle = math.degrees(math.acos(min(dot, 1.0))) + if 5.0 < angle < 85.0: + draftable.append(face) + + explorer.Next() + + return draftable + + def _get_face_normal(self, face: Any) -> Optional[gp_Dir]: + try: + surface = BRepAdaptor_Surface(face) + u = (surface.FirstUParameter() + surface.LastUParameter()) / 2 + v = (surface.FirstVParameter() + surface.LastVParameter()) / 2 + + if surface.GetType() == 0: + return surface.Plane().Position().Direction() + + from OCC.Core.BRepLProp import BRepLProp_SLProps + props = BRepLProp_SLProps(surface, 1, 0.001) + props.SetParameters(u, v) + if props.IsNormalDefined(): + return props.Normal() + + return None + except Exception: + return None + + def _execute_draft(self, shape: Any, faces: List[Any], + draft_direction: gp_Dir, draft_angle_rad: float) -> Any: + try: + draft = BRepOffsetAPI_DraftAngle(shape) + + for face in faces: + try: + normal = self._get_face_normal(face) + if normal is None: + continue + + dot = normal.Dot(draft_direction) + if dot > 0: + face_dir = draft_direction + else: + face_dir = gp_Dir(-draft_direction.X(), -draft_direction.Y(), -draft_direction.Z()) + + draft.Add(face, face_dir, draft_angle_rad, True) + except Exception: + continue + + draft.Build() + + if draft.IsDone(): + logger.info(f"拔模角应用成功: {len(faces)} 个面, {self.draft_angle}°") + return draft.Shape() + else: + logger.warning("BRepOffsetAPI_DraftAngle 构建失败,尝试逐面拔模") + return self._draft_faces_sequentially(shape, faces, draft_direction, draft_angle_rad) + + except Exception as e: + logger.warning(f"拔模执行失败: {e}") + return shape + + def _draft_faces_sequentially(self, shape: Any, faces: List[Any], + draft_direction: gp_Dir, draft_angle_rad: float) -> Any: + current_shape = shape + success_count = 0 + + for face in faces: + try: + draft = BRepOffsetAPI_DraftAngle(current_shape) + normal = self._get_face_normal(face) + if normal is None: + continue + + dot = normal.Dot(draft_direction) + if dot > 0: + face_dir = draft_direction + else: + face_dir = gp_Dir(-draft_direction.X(), -draft_direction.Y(), -draft_direction.Z()) + + draft.Add(face, face_dir, draft_angle_rad, True) + draft.Build() + + if draft.IsDone(): + current_shape = draft.Shape() + success_count += 1 + except Exception: + continue + + if success_count > 0: + logger.info(f"逐面拔模完成: {success_count}/{len(faces)} 个面成功") + else: + logger.warning("逐面拔模全部失败,返回原始形状") + + return current_shape + + def _analyze_product_geometry(self, shape: Any) -> Dict[str, Any]: + try: + props = GProp_GProps() + brepgprop.VolumeProperties(shape, props) + volume = props.Mass() + + surface_props = GProp_GProps() + brepgprop.SurfaceProperties(shape, surface_props) + surface_area = surface_props.Mass() + + center = props.CentreOfMass() + + bbox = Bnd_Box() + brepbndlib.Add(shape, bbox) + xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get() + + inertia = props.MatrixOfInertia() + + return { + "volume": volume, + "surface_area": surface_area, + "center_of_mass": [float(center.X()), float(center.Y()), float(center.Z())], + "bounding_box": { + "min": [float(xmin), float(ymin), float(zmin)], + "max": [float(xmax), float(ymax), float(zmax)], + "center": [float((xmin+xmax)/2), float((ymin+ymax)/2), float((zmin+zmax)/2)], + "dimensions": [float(xmax-xmin), float(ymax-ymin), float(zmax-zmin)] + }, + "inertia_matrix": self._get_inertia_matrix(props) + } + except Exception as e: + logger.error(f"产品几何分析失败: {e}") + raise + + def _split_cavity_core(self, shape: Any, parting_surface: Any, margin: int = 20) -> Tuple[Any, Any]: + """ + 用分型面将模具块切分为A板(上模/型腔)和B板(下模/型芯), + 然后从每个板中减去产品形状的对应部分。 + + 流程: + 1. 创建完整模具块(产品包围盒 + 全方向余量) + 2. 用分型面将模具块切分为 A板 和 B板 + 3. A板 - 产品 = 型腔(凹模) + 4. B板 - 产品 = 型芯(凸模) + """ + try: + bbox = Bnd_Box() + brepbndlib.Add(shape, bbox) + xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get() + + mold_xmin = xmin - margin + mold_ymin = ymin - margin + mold_zmin = zmin - margin + mold_xmax = xmax + margin + mold_ymax = ymax + margin + mold_zmax = zmax + margin + + mold_block = BRepPrimAPI_MakeBox( + gp_Pnt(mold_xmin, mold_ymin, mold_zmin), + gp_Pnt(mold_xmax, mold_ymax, mold_zmax) + ).Shape() + + parting_plane = self._get_parting_plane(parting_surface, shape) + if parting_plane is None: + logger.warning("无法提取分型面平面,使用回退方案") + center_z = (zmin + zmax) / 2 + parting_plane = gp_Pln(gp_Pnt(0, 0, center_z), gp_Dir(0, 0, 1)) + + a_plate, b_plate = self._split_mold_block_by_plane(mold_block, parting_plane) + + if a_plate is not None and b_plate is not None: + cavity = self._subtract_product_from_plate(a_plate, shape, "型腔(A板)") + core = self._subtract_product_from_plate(b_plate, shape, "型芯(B板)") + + if cavity is not None and core is not None: + logger.info("型腔/型芯分离完成(分型面切分+布尔减)") + return cavity, core + elif cavity is not None: + logger.warning("型芯生成失败,使用产品形状") + return cavity, shape + elif core is not None: + logger.warning("型腔生成失败,使用模具块") + return mold_block, core + + logger.warning("A/B板切分不完全,回退到原方案") + return self._split_cavity_core_fallback(shape, mold_block) + + except Exception as e: + logger.error(f"型腔分离失败: {e}") + return self._split_cavity_core_fallback(shape, None) + + def _get_parting_plane(self, parting_surface: Any, shape: Any) -> Optional[gp_Pln]: + """从分型面提取平面方程""" + try: + surface = BRepAdaptor_Surface(parting_surface) + if surface.GetType() == 0: + return surface.Plane() + + bbox = Bnd_Box() + brepbndlib.Add(shape, bbox) + xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get() + center_z = (zmin + zmax) / 2 + return gp_Pln(gp_Pnt(0, 0, center_z), gp_Dir(0, 0, 1)) + + except Exception as e: + logger.warning(f"分型面平面提取失败: {e}") + return None + + def _split_mold_block_by_plane(self, mold_block: Any, + parting_plane: gp_Pln) -> Tuple[Any, Any]: + """ + 用分型面将模具块切分为A板(上模)和B板(下模) + + 方法:使用半空间体与模具块的布尔交集运算 + - A板 = 模具块 ∩ 分型面上方半空间 + - B板 = 模具块 ∩ 分型面下方半空间 + """ + try: + plane_origin = parting_plane.Location() + plane_normal = parting_plane.Axis().Direction() + + ref_point_above = gp_Pnt( + plane_origin.X() + plane_normal.X() * 10, + plane_origin.Y() + plane_normal.Y() * 10, + plane_origin.Z() + plane_normal.Z() * 10 + ) + ref_point_below = gp_Pnt( + plane_origin.X() - plane_normal.X() * 10, + plane_origin.Y() - plane_normal.Y() * 10, + plane_origin.Z() - plane_normal.Z() * 10 + ) + + half_space_above = BRepPrimAPI_MakeHalfSpace( + BRepBuilderAPI_MakeFace(parting_plane).Face(), + ref_point_above + ).Shape() + + half_space_below = BRepPrimAPI_MakeHalfSpace( + BRepBuilderAPI_MakeFace(parting_plane).Face(), + ref_point_below + ).Shape() + + a_plate_op = BRepAlgoAPI_Common(mold_block, half_space_above) + a_plate = None + if a_plate_op.IsDone(): + a_plate = a_plate_op.Shape() + logger.info("A板(上模)切分成功") + else: + logger.warning("A板切分失败") + + b_plate_op = BRepAlgoAPI_Common(mold_block, half_space_below) + b_plate = None + if b_plate_op.IsDone(): + b_plate = b_plate_op.Shape() + logger.info("B板(下模)切分成功") + else: + logger.warning("B板切分失败") + + return a_plate, b_plate + + except Exception as e: + logger.error(f"A/B板分离失败: {e}") + return None, None + + def _subtract_product_from_plate(self, plate: Any, product: Any, + plate_name: str) -> Any: + """从模板中减去产品形状,生成型腔或型芯""" + try: + cut_op = BRepAlgoAPI_Cut(plate, product) + if cut_op.IsDone(): + result = cut_op.Shape() + logger.info(f"{plate_name}减去产品成功") + return result + else: + logger.warning(f"{plate_name}布尔减运算失败") + return plate + except Exception as e: + logger.warning(f"{plate_name}减产品失败: {e}") + return plate + + def _split_cavity_core_fallback(self, shape: Any, + mold_block: Optional[Any] = None) -> Tuple[Any, Any]: + """ + 分模回退方案:用边界框中心面作为分型面切分模具块。 + """ + logger.warning("使用分模回退方案") + + try: + bbox = Bnd_Box() + brepbndlib.Add(shape, bbox) + xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get() + + margin = 20 + if mold_block is None: + mold_block = BRepPrimAPI_MakeBox( + gp_Pnt(xmin - margin, ymin - margin, zmin - margin), + gp_Pnt(xmax + margin, ymax + margin, zmax + margin) + ).Shape() + + center_z = (zmin + zmax) / 2 + parting_plane = gp_Pln(gp_Pnt(0, 0, center_z), gp_Dir(0, 0, 1)) + a_plate, b_plate = self._split_mold_block_by_plane(mold_block, parting_plane) + + if a_plate is not None and b_plate is not None: + cavity = self._subtract_product_from_plate(a_plate, shape, "型腔(回退)") + core = self._subtract_product_from_plate(b_plate, shape, "型芯(回退)") + if cavity is not None and core is not None: + logger.info("回退方案型腔/型芯分离完成") + return cavity, core + + cavity = self._subtract_product_from_plate(mold_block, shape, "型腔(兜底)") + return cavity or mold_block, shape + + except Exception as e: + logger.error(f"分模回退方案失败: {e}") + try: + bbox = Bnd_Box() + brepbndlib.Add(shape, bbox) + xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get() + margin = 20 + cavity_block = BRepPrimAPI_MakeBox( + gp_Pnt(xmin - margin, ymin - margin, zmin - margin), + gp_Pnt(xmax + margin, ymax + margin, zmax + margin) + ).Shape() + cavity = self._subtract_product_from_plate(cavity_block, shape, "型腔(兜底)") + return cavity or cavity_block, shape + except Exception: + return shape, shape + + def detect_insert_regions(self, shape: Any, analysis: Dict, + depth_threshold: float = 30.0, + aspect_threshold: float = 3.0) -> List[Dict[str, Any]]: + """ + 检测需要独立镶件的区域 + + 镶件判定条件: + 1. 深腔区域(深度超过阈值) + 2. 细长特征(长径比超过阈值) + 3. 易磨损区域(尖锐角落、薄壁) + 4. 精密特征(高精度要求的局部区域) + + Args: + shape: 产品形状 + analysis: 几何分析结果 + depth_threshold: 深腔深度阈值 mm + aspect_threshold: 长径比阈值 + + Returns: + 镶件区域列表 + """ + inserts = [] + + try: + bbox = analysis.get("bounding_box", {}) + dims = bbox.get("dimensions", [0, 0, 0]) + center = bbox.get("center", [0, 0, 0]) + + if dims[2] > depth_threshold: + inserts.append({ + "type": "deep_cavity_insert", + "location": center, + "depth": dims[2], + "reason": f"型腔深度 {dims[2]:.1f}mm 超过阈值 {depth_threshold}mm", + "insert_type": "core_pin", + "priority": "high" + }) + + explorer = TopExp_Explorer(shape, TopAbs_FACE) + face_idx = 0 + + while explorer.More(): + face = topods.Face(explorer.Current()) + face_idx += 1 + + try: + surface = BRepAdaptor_Surface(face) + + face_props = GProp_GProps() + brepgprop.SurfaceProperties(face, face_props) + area = face_props.Mass() + + if area < 1.0 and area > 0.001: + bbox_face = Bnd_Box() + brepbndlib.Add(face, bbox_face) + try: + fxmin, fymin, fzmin, fxmax, fymax, fzmax = bbox_face.Get() + f_dims = [fxmax - fxmin, fymax - fymin, fzmax - fzmin] + max_dim = max(f_dims) + min_dim = min(f_dims) + + if min_dim > 0.01 and max_dim / min_dim > aspect_threshold: + face_center = [ + float((fxmin + fxmax) / 2), + float((fymin + fymax) / 2), + float((fzmin + fzmax) / 2) + ] + + inserts.append({ + "type": "slender_feature_insert", + "location": face_center, + "aspect_ratio": max_dim / min_dim, + "reason": f"细长特征,长径比 {max_dim/min_dim:.1f}", + "insert_type": "core_pin", + "priority": "medium", + "face_index": face_idx + }) + except Exception: + pass + + if surface.GetType() == 1: + radius = surface.Cylinder().Radius() + if radius < 3.0 and radius > 0.1: + cyl_axis = surface.Cylinder().Position().Axis() + cyl_loc = cyl_axis.Location() + + inserts.append({ + "type": "small_hole_insert", + "location": [float(cyl_loc.X()), float(cyl_loc.Y()), float(cyl_loc.Z())], + "radius": float(radius), + "reason": f"小孔特征,半径 {radius:.2f}mm", + "insert_type": "core_pin", + "priority": "high", + "face_index": face_idx + }) + + except Exception: + pass + + explorer.Next() + + if not inserts: + logger.info("未检测到需要镶件的区域") + else: + logger.info(f"检测到 {len(inserts)} 个镶件区域") + + except Exception as e: + logger.warning(f"镶件检测失败: {e}") + + return inserts + + def _extract_shape_geometry(self, shape: Any, shape_type: str) -> Dict[str, Any]: + try: + mesh = BRepMesh_IncrementalMesh(shape, 0.1) + mesh.Perform() + + vertices = [] + faces = [] + + explorer = TopExp_Explorer(shape, TopAbs_FACE) + vertex_index = 0 + + while explorer.More(): + face = explorer.Current() + location = TopLoc_Location() + triangulation = BRep_Tool.Triangulation(face, location) + + if triangulation: + nb_nodes = triangulation.NbNodes() + for i in range(1, nb_nodes + 1): + node = triangulation.Node(i) + transformed = node.Transformed(location.Transformation()) + vertices.extend([ + float(transformed.X()), + float(transformed.Y()), + float(transformed.Z()) + ]) + + nb_triangles = triangulation.NbTriangles() + for i in range(1, nb_triangles + 1): + triangle = triangulation.Triangle(i) + idx1 = triangle.Value(1) + vertex_index - 1 + idx2 = triangle.Value(2) + vertex_index - 1 + idx3 = triangle.Value(3) + vertex_index - 1 + faces.extend([int(idx1), int(idx2), int(idx3)]) + + vertex_index += nb_nodes + + explorer.Next() + + vertex_count = len(vertices) // 3 + face_count = len(faces) // 3 + + return { + "type": shape_type, + "vertices": vertices, + "faces": faces, + "vertex_count": vertex_count, + "face_count": face_count, + } + + except Exception as e: + logger.error(f"{shape_type}几何提取失败: {e}") + return { + "type": shape_type, + "vertices": [], + "faces": [], + "vertex_count": 0, + "face_count": 0, + } + + def _extract_plane_metadata(self, surface: Any) -> Dict[str, Any]: + """从分型面提取平面元数据(法向量、原点、边界)""" + metadata = { + "normal": [0.0, 0.0, 1.0], + "origin": [0.0, 0.0, 0.0], + "bounds": {"min": [0.0, 0.0, 0.0], "max": [0.0, 0.0, 0.0]}, + } + try: + surface_adaptor = BRepAdaptor_Surface(surface) + if surface_adaptor.GetType() == 0: + plane = surface_adaptor.Plane() + axis = plane.Axis() + normal = axis.Direction() + origin = plane.Location() + metadata["normal"] = [float(normal.X()), float(normal.Y()), float(normal.Z())] + metadata["origin"] = [float(origin.X()), float(origin.Y()), float(origin.Z())] + + bbox = Bnd_Box() + brepbndlib.Add(surface, bbox) + xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get() + metadata["bounds"] = { + "min": [float(xmin), float(ymin), float(zmin)], + "max": [float(xmax), float(ymax), float(zmax)], + } + except Exception as e: + logger.warning(f"提取平面元数据失败: {e}") + return metadata + + def _calculate_product_weight(self, analysis: Dict) -> str: + volume_cm3 = analysis.get("volume", 0) / 1000 + weight_g = volume_cm3 * self.material_density + return f"{weight_g:.2f} g" + + def _assess_warpage_risk(self, analysis: Dict) -> str: + bbox = analysis.get("bounding_box", {}).get("dimensions", [1, 1, 1]) + aspect_ratio = max(bbox) / min(bbox) if min(bbox) > 0 else 1 + + if aspect_ratio > 5: + return "高 - 建议增加加强筋" + elif aspect_ratio > 3: + return "中 - 需优化冷却" + else: + return "低" + + def _get_inertia_matrix(self, props: GProp_GProps) -> List[List[float]]: + inertia = props.MatrixOfInertia() + return [ + [inertia.Value(1, 1), inertia.Value(1, 2), inertia.Value(1, 3)], + [inertia.Value(2, 1), inertia.Value(2, 2), inertia.Value(2, 3)], + [inertia.Value(3, 1), inertia.Value(3, 2), inertia.Value(3, 3)] + ] + + def _calculate_parting_line_length(self, parting_line: List) -> float: + if not parting_line or len(parting_line) < 2: + return 0.0 + + total_length = 0.0 + for i in range(1, len(parting_line)): + p1 = np.array(parting_line[i-1]) + p2 = np.array(parting_line[i]) + segment_length = np.linalg.norm(p2 - p1) + total_length += segment_length + + return total_length + + def _calculate_parting_line(self, shape: Any, parting_surface: Any) -> List[List[float]]: + try: + section = BRepAlgoAPI_Section(shape, parting_surface) + section.Build() + + if not section.IsDone(): + logger.warning("截面运算未完成,使用简化分型线") + return self._simple_parting_line(shape) + + edges = [] + explorer = TopExp_Explorer(section.Shape(), TopAbs_EDGE) + + while explorer.More(): + edge = explorer.Current() + + curve = BRepAdaptor_Curve(edge) + first_param = curve.FirstParameter() + last_param = curve.LastParameter() + + num_points = max(10, int((last_param - first_param) / 0.5)) + step = (last_param - first_param) / num_points + + for i in range(num_points + 1): + param = first_param + i * step + point = curve.Value(param) + edges.append([point.X(), point.Y(), point.Z()]) + + explorer.Next() + + if not edges: + logger.warning("未找到交线,使用简化分型线") + return self._simple_parting_line(shape) + + logger.info(f"计算得到 {len(edges)} 个分型线点") + return edges + + except Exception as e: + logger.error(f"分型线计算失败: {e}") + return self._simple_parting_line(shape) + + def _simple_parting_line(self, shape: Any) -> List[List[float]]: + try: + bbox = Bnd_Box() + brepbndlib.Add(shape, bbox) + xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get() + center_z = (zmin + zmax) / 2 + + return [ + [xmin, ymin, center_z], + [xmax, ymin, center_z], + [xmax, ymax, center_z], + [xmin, ymax, center_z], + [xmin, ymin, center_z] + ] + except Exception: + return [[-50, -50, 0], [50, -50, 0], [50, 50, 0], [-50, 50, 0], [-50, -50, 0]] + + def extend_parting_surface(self, parting_surface: Any, shape: Any, + extension: float = 30.0) -> Any: + """ + 将分型面延伸到模具块边界 + + 分型面通常只覆盖产品轮廓,需要延伸到模具块边缘 + 才能正确分离A板和B板 + + Args: + parting_surface: 原始分型面 + shape: 产品形状 + extension: 延伸距离 mm + + Returns: + 延伸后的分型面 + """ + try: + bbox = Bnd_Box() + brepbndlib.Add(shape, bbox) + xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get() + + surface = BRepAdaptor_Surface(parting_surface) + if surface.GetType() != 0: + logger.info("分型面非平面,延伸操作跳过") + return parting_surface + + plane = surface.Plane() + origin = plane.Location() + normal = plane.Axis().Direction() + + extended_xmin = xmin - extension + extended_ymin = ymin - extension + extended_xmax = xmax + extension + extended_ymax = ymax + extension + + extended_plane = gp_Pln(origin, normal) + extended_surface = BRepBuilderAPI_MakeFace( + extended_plane, + extended_xmin, extended_xmax, + extended_ymin, extended_ymax + ).Face() + + logger.info(f"分型面延伸完成: 延伸距离={extension}mm") + return extended_surface + + except Exception as e: + logger.warning(f"分型面延伸失败: {e}") + return parting_surface + + def optimize_parting_line(self, parting_line: List[List[float]], + smooth_window: int = 5, + min_segment_length: float = 0.5, + angle_threshold: float = 150.0) -> List[List[float]]: + """ + 优化分型线 + + 优化内容: + 1. 平滑处理 - 消除噪声点 + 2. 去除短线段 - 合并过短的线段 + 3. 尖角处理 - 在尖角处添加过渡圆弧 + 4. 点密度均匀化 - 重采样使点间距均匀 + + Args: + parting_line: 原始分型线点列表 + smooth_window: 平滑窗口大小 + min_segment_length: 最小线段长度 + angle_threshold: 尖角判定角度(度) + + Returns: + 优化后的分型线 + """ + if len(parting_line) < 3: + return parting_line + + try: + smoothed = self._smooth_parting_line(parting_line, smooth_window) + + filtered = self._filter_short_segments(smoothed, min_segment_length) + + optimized = self._round_sharp_corners(filtered, angle_threshold) + + resampled = self._resample_parting_line(optimized, target_spacing=2.0) + + logger.info(f"分型线优化: {len(parting_line)} → {len(resampled)} 点") + return resampled + + except Exception as e: + logger.warning(f"分型线优化失败: {e}") + return parting_line + + def _smooth_parting_line(self, points: List[List[float]], + window: int = 5) -> List[List[float]]: + """移动平均平滑""" + if len(points) < window: + return points + + arr = np.array(points, dtype=np.float64) + smoothed = [] + + for i in range(len(arr)): + start = max(0, i - window // 2) + end = min(len(arr), i + window // 2 + 1) + avg = np.mean(arr[start:end], axis=0) + smoothed.append(avg.tolist()) + + return smoothed + + def _filter_short_segments(self, points: List[List[float]], + min_length: float) -> List[List[float]]: + """去除过短线段""" + if not points: + return points + + filtered = [points[0]] + for i in range(1, len(points)): + dist = np.linalg.norm(np.array(points[i]) - np.array(filtered[-1])) + if dist >= min_length: + filtered.append(points[i]) + + return filtered + + def _round_sharp_corners(self, points: List[List[float]], + angle_threshold: float) -> List[List[float]]: + """在尖角处添加过渡点""" + if len(points) < 3: + return points + + result = [points[0]] + + for i in range(1, len(points) - 1): + v1 = np.array(points[i]) - np.array(points[i - 1]) + v2 = np.array(points[i + 1]) - np.array(points[i]) + + len1 = np.linalg.norm(v1) + len2 = np.linalg.norm(v2) + + if len1 > 0.001 and len2 > 0.001: + cos_angle = np.clip(np.dot(v1, v2) / (len1 * len2), -1, 1) + angle = math.degrees(math.acos(cos_angle)) + + if angle < angle_threshold: + mid1 = (np.array(points[i - 1]) + np.array(points[i])) / 2 + mid2 = (np.array(points[i]) + np.array(points[i + 1])) / 2 + result.append(mid1.tolist()) + result.append(mid2.tolist()) + else: + result.append(points[i]) + else: + result.append(points[i]) + + result.append(points[-1]) + return result + + def _resample_parting_line(self, points: List[List[float]], + target_spacing: float) -> List[List[float]]: + """重采样使点间距均匀""" + if len(points) < 2: + return points + + arr = np.array(points, dtype=np.float64) + + cumulative_dist = [0.0] + for i in range(1, len(arr)): + dist = np.linalg.norm(arr[i] - arr[i - 1]) + cumulative_dist.append(cumulative_dist[-1] + dist) + + total_length = cumulative_dist[-1] + if total_length < target_spacing: + return points + + num_points = max(3, int(total_length / target_spacing)) + new_distances = np.linspace(0, total_length, num_points) + + resampled = [] + for d in new_distances: + idx = np.searchsorted(cumulative_dist, d) - 1 + idx = max(0, min(idx, len(arr) - 2)) + + seg_start = cumulative_dist[idx] + seg_end = cumulative_dist[idx + 1] + seg_length = seg_end - seg_start + + if seg_length > 0: + t = (d - seg_start) / seg_length + else: + t = 0 + + point = arr[idx] + t * (arr[idx + 1] - arr[idx]) + resampled.append(point.tolist()) + + return resampled diff --git a/src/database/init_db.py b/src/database/init_db.py new file mode 100644 index 0000000..ceff8b4 --- /dev/null +++ b/src/database/init_db.py @@ -0,0 +1,256 @@ +import asyncio +import sys +from pathlib import Path +from sqlalchemy import text + +project_root = Path(__file__).parent.parent.parent +src_root = Path(__file__).parent.parent +sys.path.insert(0, str(project_root)) +sys.path.insert(0, str(src_root)) + +from sqlalchemy import select +from database.database import db_manager +from models.database import User, Role, Permission, UserRole, RolePermission +from services.auth_service import get_password_hash +from config.settings import settings +from utils.logger import get_logger + +logger = get_logger(__name__) + +DEFAULT_PERMISSIONS = [ + {"code": "view_dashboard", "name": "查看仪表盘", "module": "dashboard"}, + {"code": "view_moldinsight", "name": "使用模具分析", "module": "moldinsight"}, + {"code": "upload_file", "name": "上传文件", "module": "moldinsight"}, + {"code": "view_history", "name": "查看历史记录", "module": "moldinsight"}, + {"code": "view_inventory", "name": "查看库存", "module": "inventory"}, + {"code": "manage_inventory", "name": "管理库存", "module": "inventory"}, + {"code": "view_products", "name": "查看产品", "module": "inventory"}, + {"code": "manage_products", "name": "管理产品", "module": "inventory"}, + {"code": "view_suppliers", "name": "查看供应商", "module": "inventory"}, + {"code": "manage_suppliers", "name": "管理供应商", "module": "inventory"}, + {"code": "view_customers", "name": "查看客户", "module": "inventory"}, + {"code": "manage_customers", "name": "管理客户", "module": "inventory"}, + {"code": "view_finance", "name": "查看财务", "module": "finance"}, + {"code": "manage_receipts", "name": "管理收款", "module": "finance"}, + {"code": "manage_payments", "name": "管理付款", "module": "finance"}, + {"code": "void_finance_transaction", "name": "作废财务单据", "module": "finance"}, + {"code": "view_users", "name": "查看用户", "module": "admin"}, + {"code": "manage_users", "name": "管理用户", "module": "admin"}, + {"code": "manage_roles", "name": "管理角色", "module": "admin"}, +] + +DEFAULT_ROLES = [ + {"code": "admin", "name": "管理员", "description": "系统管理员,拥有所有权限", "is_system": True, "permissions": ["view_dashboard", "view_moldinsight", "upload_file", "view_history", "view_inventory", "manage_inventory", "view_products", "manage_products", "view_suppliers", "manage_suppliers", "view_customers", "manage_customers", "view_finance", "manage_receipts", "manage_payments", "void_finance_transaction", "view_users", "manage_users", "manage_roles"]}, + {"code": "user", "name": "普通用户", "description": "普通用户,可使用模具分析和查看库存", "is_system": False, "permissions": ["view_dashboard", "view_moldinsight", "upload_file", "view_history", "view_inventory", "view_products", "view_suppliers", "view_customers", "view_finance", "manage_receipts", "manage_payments"]}, + {"code": "viewer", "name": "只读用户", "description": "只读用户,只能查看数据", "is_system": False, "permissions": ["view_dashboard", "view_moldinsight", "view_history", "view_inventory", "view_products", "view_suppliers", "view_customers", "view_finance"]}, +] + + +async def init_permissions(session): + """初始化权限""" + result = await session.execute(select(Permission)) + existing_perms = result.scalars().all() + + if existing_perms: + logger.info("权限已初始化") + return + + perm_map = {} + for perm_data in DEFAULT_PERMISSIONS: + perm = Permission(**perm_data) + session.add(perm) + await session.flush() + perm_map[perm.code] = perm.id + + logger.info(f"创建了 {len(DEFAULT_PERMISSIONS)} 个权限") + return perm_map + + +async def init_roles(session, perm_map): + """初始化角色""" + result = await session.execute(select(Role)) + existing_roles = result.scalars().all() + + if existing_roles: + logger.info("角色已初始化") + return + + for role_data in DEFAULT_ROLES: + perm_ids = [perm_map[code] for code in role_data.pop("permissions")] + role = Role(**role_data) + session.add(role) + await session.flush() + + for perm_id in perm_ids: + rp = RolePermission(role_id=role.id, permission_id=perm_id) + session.add(rp) + + logger.info(f"创建了 {len(DEFAULT_ROLES)} 个角色") + + +async def create_admin_user(session): + """创建默认管理员""" + result = await session.execute(select(User).where(User.username == settings.ADMIN_USERNAME)) + existing_admin = result.scalar_one_or_none() + + if existing_admin: + logger.info("管理员账户已存在") + return + + admin = User( + username=settings.ADMIN_USERNAME, + email=settings.ADMIN_EMAIL, + hashed_password=get_password_hash(settings.ADMIN_PASSWORD), + full_name=settings.ADMIN_FULL_NAME, + is_active=True + ) + session.add(admin) + await session.flush() + + result = await session.execute(select(Role).where(Role.code == "admin")) + admin_role = result.scalar_one_or_none() + + if admin_role: + user_role = UserRole(user_id=admin.id, role_id=admin_role.id) + session.add(user_role) + + await session.commit() + logger.info(f"创建了管理员账户: {settings.ADMIN_USERNAME}") + + +async def init_database(keep_connected: bool = True): + """初始化数据库""" + try: + await db_manager.connect() + await db_manager.create_tables() + await ensure_schema_updates() + + async with db_manager.session() as session: + perm_map = await init_permissions(session) + if perm_map is None: + # Permissions already existed, fetch them from database + result = await session.execute(select(Permission)) + perms = result.scalars().all() + perm_map = {perm.code: perm.id for perm in perms} + await init_roles(session, perm_map) + await create_admin_user(session) + + + logger.info("数据库初始化完成") + print("=" * 60) + print("数据库初始化成功!") + print("=" * 60) + print(f"管理员用户名: {settings.ADMIN_USERNAME}") + print(f"管理员邮箱: {settings.ADMIN_EMAIL}") + print("=" * 60) + print("可以在 .env 文件中修改管理员配置:") + print(" ADMIN_USERNAME") + print(" ADMIN_EMAIL") + print(" ADMIN_FULL_NAME") + print("=" * 60) + + return True + + except Exception as e: + logger.error(f"数据库初始化失败: {e}") + print(f"数据库初始化失败: {e}") + return False + finally: + if not keep_connected: + await db_manager.disconnect() + + +async def ensure_schema_updates(): + async with db_manager.engine.begin() as conn: + await conn.execute(text("ALTER TABLE products ADD COLUMN IF NOT EXISTS item_type VARCHAR(20) DEFAULT 'finished'")) + await conn.execute(text("UPDATE products SET item_type = 'finished' WHERE item_type IS NULL")) + await conn.execute(text("ALTER TABLE sales_orders ADD COLUMN IF NOT EXISTS production_status VARCHAR(20) DEFAULT 'not_started'")) + await conn.execute(text("ALTER TABLE sales_orders ADD COLUMN IF NOT EXISTS production_no VARCHAR(50)")) + await conn.execute(text("ALTER TABLE sales_orders ADD COLUMN IF NOT EXISTS planned_material_cost DOUBLE PRECISION DEFAULT 0")) + await conn.execute(text("ALTER TABLE sales_orders ADD COLUMN IF NOT EXISTS actual_material_cost DOUBLE PRECISION DEFAULT 0")) + await conn.execute(text("ALTER TABLE purchase_orders ADD COLUMN IF NOT EXISTS received_date TIMESTAMP WITHOUT TIME ZONE")) + await conn.execute(text("ALTER TABLE purchase_orders ADD COLUMN IF NOT EXISTS paid_date TIMESTAMP WITHOUT TIME ZONE")) + await conn.execute(text(""" + CREATE TABLE IF NOT EXISTS product_materials ( + id SERIAL PRIMARY KEY, + finished_product_id INTEGER NOT NULL REFERENCES products(id), + material_product_id INTEGER NOT NULL REFERENCES products(id), + quantity DOUBLE PRECISION NOT NULL, + loss_rate DOUBLE PRECISION DEFAULT 0, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() + ) + """)) + await conn.execute(text(""" + CREATE UNIQUE INDEX IF NOT EXISTS uq_product_material_unique + ON product_materials (finished_product_id, material_product_id) + """)) + await conn.execute(text("ALTER TABLE mold_cavity_data ADD COLUMN IF NOT EXISTS best_scheme_id VARCHAR(64)")) + await conn.execute(text("ALTER TABLE mold_cavity_data ADD COLUMN IF NOT EXISTS confidence_score DOUBLE PRECISION")) + await conn.execute(text("ALTER TABLE mold_cavity_data ADD COLUMN IF NOT EXISTS is_fallback BOOLEAN")) + await conn.execute(text("ALTER TABLE mold_cavity_data ADD COLUMN IF NOT EXISTS fallback_reason TEXT")) + await conn.execute(text(""" + CREATE INDEX IF NOT EXISTS idx_mold_cavity_best_scheme_id + ON mold_cavity_data (best_scheme_id) + """)) + await conn.execute(text(""" + CREATE INDEX IF NOT EXISTS idx_mold_cavity_is_fallback + ON mold_cavity_data (is_fallback) + """)) + await conn.execute(text(""" + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint + WHERE conname = 'uq_inventory_product_warehouse' + ) THEN + ALTER TABLE inventory + ADD CONSTRAINT uq_inventory_product_warehouse UNIQUE (product_id, warehouse_id); + END IF; + END $$; + """)) + await conn.execute(text(""" + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint + WHERE conname = 'ck_inventory_qty_nonnegative' + ) THEN + ALTER TABLE inventory + ADD CONSTRAINT ck_inventory_qty_nonnegative + CHECK (quantity >= 0 AND locked_quantity >= 0 AND locked_quantity <= quantity); + END IF; + END $$; + """)) + await conn.execute(text(""" + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint + WHERE conname = 'ck_purchase_order_items_qty' + ) THEN + ALTER TABLE purchase_order_items + ADD CONSTRAINT ck_purchase_order_items_qty + CHECK (quantity > 0 AND received_quantity >= 0 AND received_quantity <= quantity); + END IF; + END $$; + """)) + await conn.execute(text(""" + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint + WHERE conname = 'ck_sales_order_items_qty' + ) THEN + ALTER TABLE sales_order_items + ADD CONSTRAINT ck_sales_order_items_qty + CHECK (quantity > 0 AND delivered_quantity >= 0 AND delivered_quantity <= quantity); + END IF; + END $$; + """)) + await conn.execute(text("ALTER TABLE sales_orders ADD COLUMN IF NOT EXISTS manufacturing_date TIMESTAMP WITHOUT TIME ZONE")) + await conn.execute(text("ALTER TABLE sales_orders ALTER COLUMN manufacturing_date TYPE TIMESTAMP WITHOUT TIME ZONE")) + + +if __name__ == "__main__": + asyncio.run(init_database(keep_connected=False)) diff --git a/src/services/llm_service.py b/src/services/llm_service.py index 872aa17..862d972 100644 --- a/src/services/llm_service.py +++ b/src/services/llm_service.py @@ -74,7 +74,7 @@ JSON 格式: "confidence": 0.85, "reasoning": "详细的中文推理过程...", "risk_notes": ["风险1", "风险2"], - "rankings": [{"axis":"Z","rank":1,"score":92,"note":"..."},{"axis":"X","rank":2,"score":78,"note":"..."}] + "rankings": [{"axis":"Z","rank":1,"score":92,"note":"..."}] }""" _PARTING_USER = """请评估以下候选分模方向并推荐最优方案: @@ -126,11 +126,7 @@ class LLMService: try: prompt = self._build_design_report_prompt(analysis_result, detailed_cavity_json) - response = await self._chat( - system=_DESIGN_REPORT_SYSTEM, - user=prompt, - max_tokens=self._max_tokens, - ) + response = await self._chat(_DESIGN_REPORT_SYSTEM, prompt, self._max_tokens) if response: logger.info("LLM 设计报告生成成功 (%d 字符)", len(response)) return response @@ -150,128 +146,74 @@ class LLMService: return None try: - prompt = self._build_parting_prompt( - geometry_data, candidate_schemes, material, cavity_count, - ) - response = await self._chat( - system=_PARTING_SYSTEM, - user=prompt, - max_tokens=min(self._max_tokens, 1200), - expect_json=True, - ) + prompt = self._build_parting_prompt(geometry_data, candidate_schemes, material, cavity_count) + response = await self._chat(_PARTING_SYSTEM, prompt, min(self._max_tokens, 1200), expect_json=True) if response: result = self._parse_json_response(response) if result: - logger.info( - "LLM 分型推荐: %s (置信度 %.2f)", - result.get("recommended_axis", "?"), - result.get("confidence", 0), - ) + logger.info("LLM 分型推荐: %s (%.2f)", result.get("recommended_axis", "?"), result.get("confidence", 0)) return result return None except Exception as e: logger.warning("LLM 分型推荐失败(不影响主流程): %s", e) return None - def _build_design_report_prompt( - self, - analysis_result: Dict[str, Any], - detailed_cavity_json: Optional[Dict[str, Any]], - ) -> str: - detected_features = analysis_result.get("detected_features", []) - quality_metrics = analysis_result.get("quality_metrics", {}) - recommendations = analysis_result.get("design_recommendations", []) - - feature_text = json.dumps(detected_features, ensure_ascii=False, indent=2) - if len(feature_text) > 4000: - feature_text = feature_text[:4000] + "\n... (已截断)" + def _build_design_report_prompt(self, analysis_result, detailed_cavity_json) -> str: + features = json.dumps(analysis_result.get("detected_features", []), ensure_ascii=False, indent=2) + if len(features) > 4000: + features = features[:4000] + "\n... (已截断)" schemes_text = "" if detailed_cavity_json: schemes = detailed_cavity_json.get("candidate_schemes", []) if schemes: - schemes_text = json.dumps( - [ - { - "scheme_id": s.get("scheme_id"), - "rank": s.get("rank"), - "title": s.get("title"), - "score": s.get("score"), - "confidence_score": s.get("confidence_score"), - "summary": s.get("summary"), - "parting_axis": s.get("parting", {}).get("axis"), - "mold_structure_type": s.get("mold_structure_type"), - "dfm_violations": s.get("dfm_violations", []), - } - for s in schemes - ], - ensure_ascii=False, - indent=2, - ) + schemes_text = json.dumps([{ + "scheme_id": s.get("scheme_id"), "rank": s.get("rank"), "title": s.get("title"), + "score": s.get("score"), "summary": s.get("summary"), + "parting_axis": s.get("parting", {}).get("axis"), + "mold_structure_type": s.get("mold_structure_type"), + "dfm_violations": s.get("dfm_violations", []), + } for s in schemes], ensure_ascii=False, indent=2) - best_scheme = ( - detailed_cavity_json.get("candidate_schemes", [{}])[0] - if detailed_cavity_json - else {} - ) - cavity_data = best_scheme.get("cavity_data", {}) if isinstance(best_scheme, dict) else {} - mfg_info = cavity_data.get("manufacturing_info", {}) - metadata = cavity_data.get("metadata", {}) + best = detailed_cavity_json.get("candidate_schemes", [{}])[0] if detailed_cavity_json else {} + cd = best.get("cavity_data", {}) if isinstance(best, dict) else {} + mfg = cd.get("manufacturing_info", {}) + meta = cd.get("metadata", {}) return _DESIGN_REPORT_USER.format( - filename=metadata.get("file_name", "unknown.stp"), - material=metadata.get("selected_material", "ABS"), + filename=meta.get("file_name", "unknown.stp"), + material=meta.get("selected_material", "ABS"), volume=f"{analysis_result.get('geometry_data', {}).get('volume', 0):.1f} mm³", surface_area=f"{analysis_result.get('geometry_data', {}).get('surface_area', 0):.1f} mm²", bbox=json.dumps(analysis_result.get("geometry_data", {}).get("bounding_box", {}), ensure_ascii=False), - features=feature_text or "无特征检测数据", - quality_metrics=json.dumps(quality_metrics, ensure_ascii=False, indent=2), + features=features or "无特征检测数据", + quality_metrics=json.dumps(analysis_result.get("quality_metrics", {}), ensure_ascii=False, indent=2), schemes=schemes_text or "无分模方案数据", - mold_material=mfg_info.get("mold_material", "自动选择"), - mold_hardness=mfg_info.get("mold_hardness", "自动选择"), - clamping_force=mfg_info.get("estimated_clamping_force", "自动计算"), - mold_size=json.dumps(mfg_info.get("estimated_mold_size", {}), ensure_ascii=False), - cycle_time=mfg_info.get("estimated_cycle_time", "自动计算"), - draft_angle=f"{metadata.get('draft_angle', 2.0)}°", - shrinkage_rate=f"{metadata.get('shrinkage_rate', 0.0) * 100:.2f}%" - if isinstance(metadata.get("shrinkage_rate"), (int, float)) - else "自动计算", - recommendations=json.dumps(recommendations, ensure_ascii=False, indent=2) if recommendations else "无", + mold_material=mfg.get("mold_material", "自动选择"), + mold_hardness=mfg.get("mold_hardness", "自动选择"), + clamping_force=mfg.get("estimated_clamping_force", "自动计算"), + mold_size=json.dumps(mfg.get("estimated_mold_size", {}), ensure_ascii=False), + cycle_time=mfg.get("estimated_cycle_time", "自动计算"), + draft_angle=f"{meta.get('draft_angle', 2.0)}°", + shrinkage_rate="自动计算", + recommendations=json.dumps(analysis_result.get("design_recommendations", []), ensure_ascii=False, indent=2) or "无", ) - def _build_parting_prompt( - self, - geometry_data: Dict[str, Any], - candidate_schemes: List[Dict[str, Any]], - material: Dict[str, Any], - cavity_count: int, - ) -> str: + def _build_parting_prompt(self, geometry_data, candidate_schemes, material, cavity_count) -> str: bbox = geometry_data.get("bounding_box", {}) axis_normal_stats = geometry_data.get("axis_normal_stats", {}) inertia = geometry_data.get("inertia_matrix", []) - inertia_diag = [ - inertia[i][i] if i < len(inertia) and i < len(inertia[i]) else 0.0 - for i in range(3) - ] + inertia_diag = [inertia[i][i] if i < len(inertia) and i < len(inertia[i]) else 0.0 for i in range(3)] - schemes_text = json.dumps( - [ - { - "axis": s.get("parting", {}).get("axis") or s.get("axis"), - "score": s.get("score"), - "confidence_score": s.get("confidence_score"), - "summary": s.get("summary"), - "mold_structure_type": s.get("mold_structure_type"), - "core_required": s.get("core_required"), - "dfm_violations": s.get("dfm_violations", []), - "undercut_regions_count": len(s.get("undercut_regions", [])), - "score_breakdown": s.get("score_breakdown", {}), - } - for s in candidate_schemes - ], - ensure_ascii=False, - indent=2, - ) + schemes_text = json.dumps([{ + "axis": s.get("parting", {}).get("axis") or s.get("axis"), + "score": s.get("score"), "summary": s.get("summary"), + "mold_structure_type": s.get("mold_structure_type"), + "core_required": s.get("core_required"), + "dfm_violations": s.get("dfm_violations", []), + "undercut_regions_count": len(s.get("undercut_regions", [])), + "score_breakdown": s.get("score_breakdown", {}), + } for s in candidate_schemes], ensure_ascii=False, indent=2) return _PARTING_USER.format( bbox=json.dumps(bbox, ensure_ascii=False), @@ -284,47 +226,31 @@ class LLMService: schemes=schemes_text, ) - async def _chat( - self, - system: str, - user: str, - max_tokens: int = 2000, - expect_json: bool = False, - temperature: float = 0.3, - ) -> Optional[str]: + async def _chat(self, system, user, max_tokens=2000, expect_json=False, temperature=0.3): url = f"{self._api_url}/chat/completions" - headers = { - "Authorization": f"Bearer {self._api_key}", - "Content-Type": "application/json", - } + headers = {"Authorization": f"Bearer {self._api_key}", "Content-Type": "application/json"} payload = { "model": self._model, - "messages": [ - {"role": "system", "content": system}, - {"role": "user", "content": user}, - ], - "max_tokens": max_tokens, - "temperature": temperature, + "messages": [{"role": "system", "content": system}, {"role": "user", "content": user}], + "max_tokens": max_tokens, "temperature": temperature, } if expect_json: payload["response_format"] = {"type": "json_object"} - async with httpx.AsyncClient(timeout=self._timeout) as client: resp = await client.post(url, json=payload, headers=headers) resp.raise_for_status() - data = resp.json() - content = data["choices"][0]["message"]["content"] + content = resp.json()["choices"][0]["message"]["content"] return content.strip() if content else None @staticmethod - def _parse_json_response(raw: str) -> Optional[Dict[str, Any]]: + def _parse_json_response(raw): try: return json.loads(raw) except json.JSONDecodeError: - match = re.search(r"\{[\s\S]*\}", raw) - if match: + m = re.search(r"\{[\s\S]*\}", raw) + if m: try: - return json.loads(match.group()) + return json.loads(m.group()) except json.JSONDecodeError: pass logger.warning("LLM JSON 解析失败: %s...", raw[:200]) diff --git a/src/services/processing_service.py b/src/services/processing_service.py index 7c0927b..d3973d6 100644 --- a/src/services/processing_service.py +++ b/src/services/processing_service.py @@ -259,24 +259,10 @@ class ProcessingService: file_path, db_session, task_id, stp_file_id, analysis_result ) - # 9.8 LLM 增强分析(可选,不影响主流程) + # 9.8 LLM 增强分析 llm_report = None - llm_parting = None if analysis_result: - llm_report = await llm_service.generate_design_report( - analysis_result, detailed_cavity_json - ) - candidate_schemes = detailed_cavity_json.get("candidate_schemes", []) - if candidate_schemes: - cavity_count = detailed_cavity_json.get("mold_cavities", {}).get("cavity_count", 1) - if isinstance(cavity_count, (int, float)): - cavity_count = int(cavity_count) - else: - cavity_count = 1 - llm_parting = await llm_service.recommend_parting_direction( - geometry_data, candidate_schemes, selected_material, - cavity_count=cavity_count, - ) + llm_report = await llm_service.generate_design_report(analysis_result, detailed_cavity_json) # 10. 完成处理 await self.storage_service.update_stp_file_status(db_session, stp_file_id, "completed") @@ -296,7 +282,6 @@ class ProcessingService: "html_file": best_scheme.get("html_file", f"/html/{Path(html_file_path).name}") if best_scheme else f"/html/{Path(html_file_path).name}", "verification": verification_result, "llm_report": llm_report, - "llm_parting_recommendation": llm_parting, "status": ProcessingStatus.COMPLETED, "completed_at": str(datetime.now()), }) diff --git a/src/utils/html_generator.py b/src/utils/html_generator.py new file mode 100644 index 0000000..f200607 --- /dev/null +++ b/src/utils/html_generator.py @@ -0,0 +1,831 @@ +# utils/html_generator.py +from pathlib import Path +from typing import Dict, Any, Optional +from datetime import datetime +from utils.logger import get_logger + +logger = get_logger(__name__) + +try: + import orjson + + def _json_dumps(obj: Any) -> bytes: + return orjson.dumps(obj, option=orjson.OPT_SERIALIZE_NUMPY | orjson.OPT_NON_STR_KEYS) + + def _json_dumps_str(obj: Any) -> str: + return orjson.dumps(obj, option=orjson.OPT_SERIALIZE_NUMPY | orjson.OPT_NON_STR_KEYS).decode("utf-8") + + _JSON_FAST = True +except ImportError: + import json + + def _json_dumps(obj: Any) -> bytes: + return json.dumps(obj, ensure_ascii=False).encode("utf-8") + + def _json_dumps_str(obj: Any) -> str: + return json.dumps(obj, ensure_ascii=False) + + _JSON_FAST = False + + +class HTMLGenerator: + """HTML文件生成器 — 数据分离架构,Three.js 0.170 + PBR渲染""" + + def __init__(self, output_dir: str = "./html_output"): + self.output_dir = Path(output_dir) + self.output_dir.mkdir(exist_ok=True) + + def generate_3d_viewer_html(self, stp_filename: str, data_filename: str) -> str: + """生成3D可视化HTML页面 — 通过fetch异步加载companion JSON数据""" + + cavity_html = self._build_cavity_info_panel_template() + + html_content = f""" + +
+ + +