diff --git a/src/models/database.py b/src/models/database.py new file mode 100644 index 0000000..dc3721c --- /dev/null +++ b/src/models/database.py @@ -0,0 +1,874 @@ +# models/database.py +from sqlalchemy import Column, Integer, String, Text, DateTime, Date, JSON, LargeBinary, Boolean, Float, ForeignKey, UniqueConstraint, Numeric +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.sql import func +from sqlalchemy.orm import relationship +from datetime import datetime, date + +Base = declarative_base() + +class User(Base): + """用户表""" + __tablename__ = "users" + __excluded_fields__ = {'hashed_password'} + + id = Column(Integer, primary_key=True, index=True) + username = Column(String(50), unique=True, index=True, nullable=False) + email = Column(String(255), unique=True, index=True, nullable=False) + hashed_password = Column(String(255), nullable=False) + full_name = Column(String(100)) + is_active = Column(Boolean, default=True) + created_at = Column(DateTime, default=func.now()) + last_login = Column(DateTime, nullable=True) + + stp_files = relationship("STPFile", back_populates="user") + user_roles = relationship("UserRole", back_populates="user", cascade="all, delete-orphan") + + @property + def roles(self): + return [ur.role for ur in self.user_roles] + + @property + def is_superuser(self): + return any(r.code == 'admin' for r in self.roles) + + def has_permission(self, permission_code: str) -> bool: + if self.is_superuser: + return True + for role in self.roles: + for perm in role.permissions: + if perm.code == permission_code: + return True + return False + + def safe_dict(self): + return {k: v for k, v in self.__dict__.items() + if not k.startswith('_') and k not in self.__excluded_fields__} + + def __repr__(self): + return f"" + + +class Role(Base): + """角色表""" + __tablename__ = "roles" + + id = Column(Integer, primary_key=True, index=True) + code = Column(String(50), unique=True, index=True, nullable=False) + name = Column(String(100), nullable=False) + description = Column(Text, nullable=True) + is_system = Column(Boolean, default=False) + created_at = Column(DateTime, default=func.now()) + + user_roles = relationship("UserRole", back_populates="role", cascade="all, delete-orphan") + role_permissions = relationship("RolePermission", back_populates="role", cascade="all, delete-orphan") + + @property + def permissions(self): + return [rp.permission for rp in self.role_permissions] + + def __repr__(self): + return f"" + + +class Permission(Base): + """权限表""" + __tablename__ = "permissions" + + id = Column(Integer, primary_key=True, index=True) + code = Column(String(100), unique=True, index=True, nullable=False) + name = Column(String(100), nullable=False) + module = Column(String(50), nullable=True) + description = Column(Text, nullable=True) + created_at = Column(DateTime, default=func.now()) + + role_permissions = relationship("RolePermission", back_populates="permission", cascade="all, delete-orphan") + + def __repr__(self): + return f"" + + +class UserRole(Base): + """用户角色关联表""" + __tablename__ = "user_roles" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True) + role_id = Column(Integer, ForeignKey("roles.id"), nullable=False, index=True) + created_at = Column(DateTime, default=func.now()) + + user = relationship("User", back_populates="user_roles") + role = relationship("Role", back_populates="user_roles") + + def __repr__(self): + return f"" + + +class RolePermission(Base): + """角色权限关联表""" + __tablename__ = "role_permissions" + + id = Column(Integer, primary_key=True, index=True) + role_id = Column(Integer, ForeignKey("roles.id"), nullable=False, index=True) + permission_id = Column(Integer, ForeignKey("permissions.id"), nullable=False, index=True) + created_at = Column(DateTime, default=func.now()) + + role = relationship("Role", back_populates="role_permissions") + permission = relationship("Permission", back_populates="role_permissions") + + def __repr__(self): + return f"" + +class STPFile(Base): + """STP源文件元数据表 - 支持同一文件多次上传""" + __tablename__ = "stp_files" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=True, index=True) + + # 对象存储信息 + object_key = Column(String(500), nullable=False, index=True) # MinIO对象键 + storage_bucket = Column(String(100), nullable=False) # 存储桶名称 + object_url = Column(String(1000), nullable=True) # 预签名URL(可选) + + # 文件信息 + original_filename = Column(String(255), nullable=False, index=True) # 添加索引支持按文件名查询 + file_size = Column(Integer, nullable=False) + file_hash = Column(String(64), index=True) # 移除unique约束,允许同一文件多次上传 + mime_type = Column(String(50), default="application/octet-stream") + + # 上传批次标识 - 用于区分同一文件的多次上传 + upload_batch = Column(String(36), index=True) # UUID批次号 + + # 时间戳 + upload_time = Column(DateTime, default=func.now()) + processed_time = Column(DateTime, nullable=True) + + # 状态 + status = Column(String(20), default="pending", index=True) # pending, processing, completed, failed + error_message = Column(Text, nullable=True) + + # 分析摘要 - 快速查询字段 + volume = Column(Float, nullable=True) # 体积 mm³ + surface_area = Column(Float, nullable=True) # 表面积 mm² + product_weight = Column(Float, nullable=True) # 产品重量 g + + # 保留旧字段以兼容 + file_path = Column(String(500), nullable=True) # 本地路径(已弃用) + file_content = Column(LargeBinary, nullable=True) # 本地存储(已弃用) + filename = Column(String(255), nullable=True) # 已弃用 + + # 关联关系 + user = relationship("User", back_populates="stp_files") + geometry_data = relationship("GeometryData", back_populates="stp_file", uselist=False) + mesh_data = relationship("MeshData", back_populates="stp_file", uselist=False) + mold_cavity_data = relationship("MoldCavityData", back_populates="stp_file", uselist=False) + html_file = relationship("HTMLFile", back_populates="stp_file", uselist=False) + analysis_metrics = relationship("AnalysisMetrics", back_populates="stp_file", uselist=False) + feature_detections = relationship("FeatureDetection", back_populates="stp_file") + design_recommendations = relationship("DesignRecommendation", back_populates="stp_file") + processing_tasks = relationship("ProcessingTask", back_populates="stp_file") + + def __repr__(self): + return f"" + +class GeometryData(Base): + """几何数据JSON元数据表""" + __tablename__ = "geometry_data" + + id = Column(Integer, primary_key=True, index=True) + stp_file_id = Column(Integer, ForeignKey("stp_files.id"), nullable=False, index=True) + + # 对象存储信息 + object_key = Column(String(500), nullable=False) + storage_bucket = Column(String(100), nullable=False) + object_url = Column(String(1000), nullable=True) + + # 分析方法 + analysis_method = Column(String(50), default="pythonocc") # pythonocc, simulated + + # 时间戳 + created_time = Column(DateTime, default=func.now()) + + # 几何属性摘要(便于快速查询) + volume = Column(Float, nullable=True) + surface_area = Column(Float, nullable=True) + bounding_box_min = Column(JSON, nullable=True) + bounding_box_max = Column(JSON, nullable=True) + center_of_mass = Column(JSON, nullable=True) + + # 拓扑信息 + topology_faces = Column(Integer, nullable=True) + topology_edges = Column(Integer, nullable=True) + topology_vertices = Column(Integer, nullable=True) + + # 关联关系 + stp_file = relationship("STPFile", back_populates="geometry_data") + + def __repr__(self): + return f"" + + +class MeshData(Base): + """网格数据JSON元数据表(详细网格存 RustFS,PostgreSQL 存摘要)""" + __tablename__ = "mesh_data" + + id = Column(Integer, primary_key=True, index=True) + stp_file_id = Column(Integer, ForeignKey("stp_files.id"), nullable=False, index=True) + + # 对象存储信息 + object_key = Column(String(500), nullable=False) + storage_bucket = Column(String(100), nullable=False) + object_url = Column(String(1000), nullable=True) + + # 生成设置 + quality = Column(String(20), default="medium") # low / medium / high + + # 网格规模信息 + vertex_count = Column(Integer, nullable=True) + face_count = Column(Integer, nullable=True) + point_count = Column(Integer, nullable=True) # 采样点云数量 + + # 网格边界框(便于快速查询) + bounding_box_min = Column(JSON, nullable=True) + bounding_box_max = Column(JSON, nullable=True) + + # 时间戳 + created_time = Column(DateTime, default=func.now()) + + # 关联关系 + stp_file = relationship("STPFile", back_populates="mesh_data") + + def __repr__(self): + return f"" + +class HTMLFile(Base): + """网页文件元数据表""" + __tablename__ = "html_files" + + id = Column(Integer, primary_key=True, index=True) + stp_file_id = Column(Integer, ForeignKey("stp_files.id"), nullable=False, index=True) + + # 对象存储信息 + object_key = Column(String(500), nullable=False) + storage_bucket = Column(String(100), nullable=False) + object_url = Column(String(1000), nullable=True) + + # 文件信息 + filename = Column(String(255), nullable=False) + generated_time = Column(DateTime, default=func.now()) + + # 可视化相关元数据 + visualization_type = Column(String(50), default="3d_viewer") + has_interactive_elements = Column(Boolean, default=True) + + # 保留旧字段以兼容 + file_path = Column(String(500), nullable=True) + html_content = Column(Text, nullable=True) + + # 关联关系 + stp_file = relationship("STPFile", back_populates="html_file") + + def __repr__(self): + return f"" + +class ProcessingTask(Base): + """处理任务记录表""" + __tablename__ = "processing_tasks" + + id = Column(Integer, primary_key=True, index=True) + task_id = Column(String(36), unique=True, index=True, nullable=False) + stp_file_id = Column(Integer, ForeignKey("stp_files.id"), nullable=False, index=True) + + # 任务类型和状态 + task_type = Column(String(50), default="stp_parsing") # stp_parsing, geometry_analysis, mold_generation + status = Column(String(20), default="pending") # pending, processing, completed, failed + + # 时间戳 + created_time = Column(DateTime, default=func.now()) + started_time = Column(DateTime, nullable=True) + completed_time = Column(DateTime, nullable=True) + + # 处理进度 + progress = Column(Integer, default=0) # 0-100 + current_step = Column(String(100), nullable=True) + + # 错误信息 + error_message = Column(Text, nullable=True) + error_stack = Column(Text, nullable=True) + + # 处理参数 + parameters = Column(JSON, nullable=True) # 任务参数 + + # 关联关系 + stp_file = relationship("STPFile", back_populates="processing_tasks") + + def __repr__(self): + return f"" + +class MoldCavityData(Base): + """模具型腔数据元数据表""" + __tablename__ = "mold_cavity_data" + + id = Column(Integer, primary_key=True, index=True) + stp_file_id = Column(Integer, ForeignKey("stp_files.id"), nullable=False, index=True) + + # 对象存储信息 + detailed_object_key = Column(String(500), nullable=False) # 完整三维数据 + storage_bucket = Column(String(100), nullable=False) + + # 模具类型和材料 + mold_material = Column(String(100), default="Aluminum Alloy 7075") + mold_type = Column(String(50), default="single_cavity") # single_cavity, multi_cavity + + # 工艺参数 + shrinkage_rate = Column(Float, nullable=False) + draft_angle = Column(Float, nullable=False) + parting_line_length = Column(Float, nullable=True) + + # 生成时间 + generated_time = Column(DateTime, default=func.now()) + + # 关键信息摘要(快速查询字段) + cavity_key_info = Column(JSON, nullable=True) # 完整关键信息 + + # 提取的字段(便于查询和排序) + mold_size_length = Column(Float, nullable=True) + mold_size_width = Column(Float, nullable=True) + mold_size_height = Column(Float, nullable=True) + estimated_clamping_force = Column(String(50), nullable=True) + product_weight = Column(String(50), nullable=True) + product_volume = Column(Float, nullable=True) + wall_thickness_range = Column(String(50), nullable=True) + complexity_score = Column(Float, nullable=True) + + # 质量评估 + weld_line_risk = Column(String(50), nullable=True) # 熔接痕风险 + sink_mark_risk = Column(String(50), nullable=True) # 缩痕风险 + warpage_risk = Column(String(50), nullable=True) # 翘曲风险 + + # 多方案可信化摘要(第1周阶段1) + best_scheme_id = Column(String(64), nullable=True, index=True) + confidence_score = Column(Float, nullable=True) + is_fallback = Column(Boolean, nullable=True, index=True) + fallback_reason = Column(Text, nullable=True) + + # 关联关系 + stp_file = relationship("STPFile", back_populates="mold_cavity_data") + + def __repr__(self): + return f"" + + +class FeatureDetection(Base): + """特征检测结果表""" + __tablename__ = "feature_detections" + + id = Column(Integer, primary_key=True, index=True) + stp_file_id = Column(Integer, ForeignKey("stp_files.id"), nullable=False, index=True) + + # 特征信息 + feature_type = Column(String(50), nullable=False, index=True) # thin_wall, thick_wall, wall_non_uniform, rib, boss, draft_angle, high_curvature, fillet + confidence = Column(Float, nullable=False) # 0.0 - 1.0 + + # 位置和尺寸 + location = Column(JSON, nullable=True) # [x, y, z] + dimensions = Column(JSON, nullable=True) # [length, width, height] + + # 特征参数 + parameters = Column(JSON, nullable=True) # 自定义参数 + + # 检测时间 + detected_at = Column(DateTime, default=func.now()) + + # 关联的几何数据 + geometry_data_id = Column(Integer, ForeignKey("geometry_data.id"), nullable=True) + + # 关联关系 + stp_file = relationship("STPFile", back_populates="feature_detections") + + def __repr__(self): + return f"" + + +class DesignRecommendation(Base): + """设计建议表""" + __tablename__ = "design_recommendations" + + id = Column(Integer, primary_key=True, index=True) + stp_file_id = Column(Integer, ForeignKey("stp_files.id"), nullable=False, index=True) + + # 建议信息 + rec_type = Column(String(50), nullable=False) # wall_thickness, draft_angle, etc. + priority = Column(String(20), nullable=False) # high, medium, low + description = Column(String(500), nullable=False) + reason = Column(Text, nullable=True) + + # 建议参数 + parameters = Column(JSON, nullable=True) + + # 状态 + status = Column(String(20), default="pending") # pending, accepted, rejected + user_notes = Column(Text, nullable=True) + + # 时间戳 + created_at = Column(DateTime, default=func.now()) + updated_at = Column(DateTime, nullable=True) + + # 关联关系 + stp_file = relationship("STPFile", back_populates="design_recommendations") + + def __repr__(self): + return f"" + + +class UserActivity(Base): + """用户活动日志表""" + __tablename__ = "user_activities" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True) + + # 活动信息 + activity_type = Column(String(50), nullable=False, index=True) # upload, view, download, delete, export + resource_type = Column(String(50), nullable=True) # stp_file, geometry_data, mold_cavity + resource_id = Column(Integer, nullable=True) + + # 活动详情 + description = Column(Text, nullable=True) + meta_data = Column(JSON, nullable=True) + + # 时间戳 + created_at = Column(DateTime, default=func.now(), index=True) + + # IP和设备信息 + ip_address = Column(String(45), nullable=True) + user_agent = Column(String(500), nullable=True) + + def __repr__(self): + return f"" + + +class SystemLog(Base): + """系统日志表(重要操作和错误)""" + __tablename__ = "system_logs" + + id = Column(Integer, primary_key=True, index=True) + + # 日志级别 + level = Column(String(20), nullable=False, index=True) # INFO, WARNING, ERROR, CRITICAL + + # 日志信息 + message = Column(Text, nullable=False) + module = Column(String(100), nullable=True) # 模块名 + function_name = Column(String(100), nullable=True) + + # 时间戳 + created_at = Column(DateTime, default=func.now(), index=True) + + # 用户信息(如果有关联用户) + user_id = Column(Integer, ForeignKey("users.id"), nullable=True) + + # 额外信息 + request_id = Column(String(100), nullable=True) # 关联的请求ID + execution_time_ms = Column(Integer, nullable=True) # 执行时间 + + # 关联数据 + resource_type = Column(String(50), nullable=True) + resource_id = Column(Integer, nullable=True) + + def __repr__(self): + return f"" + + +class Product(Base): + """产品表""" + __tablename__ = "products" + + id = Column(Integer, primary_key=True, index=True) + sku = Column(String(50), unique=True, index=True, nullable=False) + name = Column(String(200), nullable=False) + description = Column(Text, nullable=True) + category = Column(String(100), nullable=True) + unit = Column(String(20), default="件") + item_type = Column(String(20), default="finished", index=True) + cost_price = Column(Numeric(12, 2), default=0) + sale_price = Column(Numeric(12, 2), default=0) + min_stock = Column(Integer, default=0) + max_stock = Column(Integer, default=1000) + is_active = Column(Boolean, default=True) + created_at = Column(DateTime, default=func.now()) + updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + + inventory = relationship("Inventory", back_populates="product", uselist=False) + stock_movements = relationship("StockMovement", back_populates="product") + bom_materials = relationship( + "ProductMaterial", + foreign_keys="ProductMaterial.finished_product_id", + back_populates="finished_product", + cascade="all, delete-orphan" + ) + used_in_products = relationship( + "ProductMaterial", + foreign_keys="ProductMaterial.material_product_id", + back_populates="material_product" + ) + + def __repr__(self): + return f"" + + +class ProductMaterial(Base): + __tablename__ = "product_materials" + __table_args__ = ( + UniqueConstraint("finished_product_id", "material_product_id", name="uq_product_material_unique"), + ) + + id = Column(Integer, primary_key=True, index=True) + finished_product_id = Column(Integer, ForeignKey("products.id"), nullable=False, index=True) + material_product_id = Column(Integer, ForeignKey("products.id"), nullable=False, index=True) + quantity = Column(Numeric(12, 4), nullable=False) + loss_rate = Column(Numeric(5, 4), default=0) + created_at = Column(DateTime, default=func.now()) + updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + + finished_product = relationship( + "Product", + foreign_keys=[finished_product_id], + back_populates="bom_materials" + ) + material_product = relationship( + "Product", + foreign_keys=[material_product_id], + back_populates="used_in_products" + ) + + def __repr__(self): + return f"" + + +class MaterialPriceHistory(Base): + """物料价格历史表""" + __tablename__ = "material_price_history" + + id = Column(Integer, primary_key=True, index=True) + product_id = Column(Integer, ForeignKey("products.id"), nullable=False, index=True) + price = Column(Numeric(12, 2), nullable=False) + effective_date = Column(DateTime, default=func.now(), index=True) + supplier_id = Column(Integer, ForeignKey("suppliers.id"), nullable=True, index=True) + remark = Column(Text, nullable=True) + created_at = Column(DateTime, default=func.now()) + + product = relationship("Product", backref="price_history") + supplier = relationship("Supplier", backref="price_history") + + def __repr__(self): + return f"" + + +class MaterialSupplier(Base): + """物料供应商关联表""" + __tablename__ = "material_suppliers" + + id = Column(Integer, primary_key=True, index=True) + product_id = Column(Integer, ForeignKey("products.id"), nullable=False, index=True) + supplier_id = Column(Integer, ForeignKey("suppliers.id"), nullable=False, index=True) + is_primary = Column(Boolean, default=False) + contact_person = Column(String(100), nullable=True) + contact_phone = Column(String(50), nullable=True) + lead_time = Column(Integer, nullable=True) # 交货周期(天) + min_order_quantity = Column(Integer, nullable=True) + created_at = Column(DateTime, default=func.now()) + updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + + product = relationship("Product", backref="suppliers") + supplier = relationship("Supplier", backref="materials") + + def __repr__(self): + return f"" + + +class Supplier(Base): + """供应商表""" + __tablename__ = "suppliers" + + id = Column(Integer, primary_key=True, index=True) + code = Column(String(50), unique=True, index=True) + name = Column(String(200), nullable=False) + contact_person = Column(String(100), nullable=True) + phone = Column(String(50), nullable=True) + email = Column(String(100), nullable=True) + address = Column(Text, nullable=True) + bank_name = Column(String(100), nullable=True) + bank_account = Column(String(50), nullable=True) + tax_number = Column(String(50), nullable=True) + is_active = Column(Boolean, default=True) + created_at = Column(DateTime, default=func.now()) + updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + + purchase_orders = relationship("PurchaseOrder", back_populates="supplier") + + def __repr__(self): + return f"" + + +class Customer(Base): + """客户表""" + __tablename__ = "customers" + + id = Column(Integer, primary_key=True, index=True) + code = Column(String(50), unique=True, index=True) + name = Column(String(200), nullable=False) + contact_person = Column(String(100), nullable=True) + phone = Column(String(50), nullable=True) + email = Column(String(100), nullable=True) + address = Column(Text, nullable=True) + bank_name = Column(String(100), nullable=True) + bank_account = Column(String(50), nullable=True) + tax_number = Column(String(50), nullable=True) + credit_limit = Column(Numeric(12, 2), default=0) + is_active = Column(Boolean, default=True) + created_at = Column(DateTime, default=func.now()) + updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + + sales_orders = relationship("SalesOrder", back_populates="customer") + + def __repr__(self): + return f"" + + +class Warehouse(Base): + """仓库表""" + __tablename__ = "warehouses" + + id = Column(Integer, primary_key=True, index=True) + code = Column(String(50), unique=True, index=True) + name = Column(String(200), nullable=False) + address = Column(Text, nullable=True) + manager = Column(String(100), nullable=True) + phone = Column(String(50), nullable=True) + is_active = Column(Boolean, default=True) + is_default = Column(Boolean, default=False) + created_at = Column(DateTime, default=func.now()) + + inventories = relationship("Inventory", back_populates="warehouse") + + def __repr__(self): + return f"" + + +class Inventory(Base): + """库存表""" + __tablename__ = "inventory" + + id = Column(Integer, primary_key=True, index=True) + product_id = Column(Integer, ForeignKey("products.id"), nullable=False, index=True) + warehouse_id = Column(Integer, ForeignKey("warehouses.id"), nullable=False, index=True) + quantity = Column(Numeric(12, 4), default=0) + locked_quantity = Column(Numeric(12, 4), default=0) + batch_number = Column(String(50), nullable=True) + location = Column(String(100), nullable=True) + updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + + product = relationship("Product", back_populates="inventory") + warehouse = relationship("Warehouse", back_populates="inventories") + + def __repr__(self): + return f"" + + @property + def available_quantity(self): + return self.quantity - self.locked_quantity + + +class StockMovement(Base): + """库存变动记录表""" + __tablename__ = "stock_movements" + + id = Column(Integer, primary_key=True, index=True) + product_id = Column(Integer, ForeignKey("products.id"), nullable=False, index=True) + warehouse_id = Column(Integer, ForeignKey("warehouses.id"), nullable=False) + movement_type = Column(String(20), nullable=False) + quantity = Column(Numeric(12, 4), nullable=False) + before_quantity = Column(Numeric(12, 4), default=0) + after_quantity = Column(Numeric(12, 4), default=0) + reference_type = Column(String(50), nullable=True) + reference_id = Column(Integer, nullable=True) + reference_no = Column(String(50), nullable=True) + unit_price = Column(Numeric(12, 2), nullable=True) + total_amount = Column(Numeric(12, 2), nullable=True) + remark = Column(Text, nullable=True) + operator_id = Column(Integer, ForeignKey("users.id"), nullable=True) + created_at = Column(DateTime, default=func.now(), index=True) + + product = relationship("Product", back_populates="stock_movements") + + def __repr__(self): + return f"" + + +class PurchaseOrder(Base): + """采购订单表""" + __tablename__ = "purchase_orders" + + id = Column(Integer, primary_key=True, index=True) + order_no = Column(String(50), unique=True, index=True, nullable=False) + supplier_id = Column(Integer, ForeignKey("suppliers.id"), nullable=False, index=True) + order_date = Column(DateTime, default=func.now()) + expected_date = Column(Date, nullable=True) + status = Column(String(20), default="draft") + total_amount = Column(Numeric(12, 2), default=0) + paid_amount = Column(Numeric(12, 2), default=0) + remark = Column(Text, nullable=True) + operator_id = Column(Integer, ForeignKey("users.id"), nullable=True) + created_at = Column(DateTime, default=func.now()) + updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + # 状态变更时间 + received_date = Column(DateTime, nullable=True) # 已收货时间 + paid_date = Column(DateTime, nullable=True) # 已付款时间 + + supplier = relationship("Supplier", back_populates="purchase_orders") + items = relationship("PurchaseOrderItem", back_populates="order", cascade="all, delete-orphan") + + def __repr__(self): + return f"" + + +class PurchaseOrderItem(Base): + """采购订单明细表""" + __tablename__ = "purchase_order_items" + + id = Column(Integer, primary_key=True, index=True) + order_id = Column(Integer, ForeignKey("purchase_orders.id"), nullable=False) + product_id = Column(Integer, ForeignKey("products.id"), nullable=False) + quantity = Column(Integer, nullable=False) + received_quantity = Column(Integer, default=0) + unit_price = Column(Numeric(12, 2), nullable=False) + amount = Column(Numeric(12, 2), nullable=False) + remark = Column(Text, nullable=True) + + order = relationship("PurchaseOrder", back_populates="items") + + def __repr__(self): + return f"" + + +class SalesOrder(Base): + """销售订单表""" + __tablename__ = "sales_orders" + + id = Column(Integer, primary_key=True, index=True) + order_no = Column(String(50), unique=True, index=True, nullable=False) + customer_id = Column(Integer, ForeignKey("customers.id"), nullable=False, index=True) + order_date = Column(DateTime, default=func.now()) + delivery_date = Column(Date, nullable=True) + manufacturing_date = Column(DateTime, nullable=True) + actual_delivery_date = Column(DateTime, nullable=True) + actual_payment_date = Column(DateTime, nullable=True) + status = Column(String(20), default="draft") + production_status = Column(String(20), default="not_started", index=True) + production_no = Column(String(50), nullable=True, index=True) + planned_material_cost = Column(Numeric(12, 2), default=0) + actual_material_cost = Column(Numeric(12, 2), default=0) + total_amount = Column(Numeric(12, 2), default=0) + received_amount = Column(Numeric(12, 2), default=0) + remark = Column(Text, nullable=True) + operator_id = Column(Integer, ForeignKey("users.id"), nullable=True) + created_at = Column(DateTime, default=func.now()) + updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + + customer = relationship("Customer", back_populates="sales_orders") + items = relationship("SalesOrderItem", back_populates="order", cascade="all, delete-orphan") + + def __repr__(self): + return f"" + + +class FinanceTransaction(Base): + __tablename__ = "finance_transactions" + + id = Column(Integer, primary_key=True, index=True) + txn_no = Column(String(50), unique=True, index=True, nullable=False) + txn_type = Column(String(20), nullable=False, index=True) + partner_type = Column(String(20), nullable=False, index=True) + partner_id = Column(Integer, nullable=False, index=True) + amount = Column(Numeric(12, 2), nullable=False) + txn_date = Column(DateTime, default=func.now(), index=True) + method = Column(String(30), default="bank") + account_name = Column(String(100), nullable=True) + status = Column(String(20), default="confirmed", index=True) + remark = Column(Text, nullable=True) + operator_id = Column(Integer, ForeignKey("users.id"), nullable=True) + created_at = Column(DateTime, default=func.now(), index=True) + + allocations = relationship("FinanceAllocation", back_populates="transaction", cascade="all, delete-orphan") + + def __repr__(self): + return f"" + + +class FinanceAllocation(Base): + __tablename__ = "finance_allocations" + + id = Column(Integer, primary_key=True, index=True) + transaction_id = Column(Integer, ForeignKey("finance_transactions.id"), nullable=False, index=True) + order_type = Column(String(20), nullable=False, index=True) + order_id = Column(Integer, nullable=False, index=True) + allocated_amount = Column(Numeric(12, 2), nullable=False) + created_at = Column(DateTime, default=func.now(), index=True) + + transaction = relationship("FinanceTransaction", back_populates="allocations") + + def __repr__(self): + return f"" + + +class AnalysisMetrics(Base): + """分析指标表""" + __tablename__ = "analysis_metrics" + + id = Column(Integer, primary_key=True, index=True) + stp_file_id = Column(Integer, ForeignKey("stp_files.id"), nullable=False, index=True) + + # 质量指标 + volume_utilization = Column(Float, default=0) # 体积利用率 + topology_complexity = Column(Float, default=0) # 拓扑复杂度 + wall_uniformity = Column(Float, default=0) # 壁厚均匀性 + + # 分析摘要 + analysis_summary = Column(Text, nullable=True) + + # FreeCAD 验证结果 + verification_status = Column(String(20), nullable=True) # passed, failed, pending, error + verification_volume_diff = Column(Float, nullable=True) # 体积差异百分比 + verification_area_diff = Column(Float, nullable=True) # 表面积差异百分比 + verification_details = Column(JSON, nullable=True) # 完整验证结果 + + # 时间戳 + created_at = Column(DateTime, default=func.now()) + + # 关联关系 + stp_file = relationship("STPFile", back_populates="analysis_metrics") + + def __repr__(self): + return f"" + + +class SalesOrderItem(Base): + """销售订单明细表""" + __tablename__ = "sales_order_items" + + id = Column(Integer, primary_key=True, index=True) + order_id = Column(Integer, ForeignKey("sales_orders.id"), nullable=False) + product_id = Column(Integer, ForeignKey("products.id"), nullable=False) + quantity = Column(Integer, nullable=False) + delivered_quantity = Column(Integer, default=0) + unit_price = Column(Numeric(12, 2), nullable=False) + amount = Column(Numeric(12, 2), nullable=False) + remark = Column(Text, nullable=True) + + order = relationship("SalesOrder", back_populates="items") + + def __repr__(self): + return f"" + diff --git a/src/services/processing_service.py b/src/services/processing_service.py new file mode 100644 index 0000000..7c0927b --- /dev/null +++ b/src/services/processing_service.py @@ -0,0 +1,527 @@ +# services/processing_service.py +"""STP 文件处理流程编排器 — 协调解析、网格生成、型腔生成、保存、验证""" + +import asyncio +import traceback +from datetime import datetime +from pathlib import Path +from typing import Optional, Dict, Any + +from sqlalchemy.ext.asyncio import AsyncSession + +from core.stp_parser import STPParser +from core.geometry_analyzer import GeometryAnalyzer +from core.mold_generator import MoldCavityGenerator +from core.aluminum_foam_mold import AluminumFoamMoldGenerator +from core.mold_quality_inspector import AluminumFoamMoldQualityInspector +from core.mesh_generator import MeshGenerator +from core.multi_scheme_planner import MultiSchemeMoldPlanner +from services.storage_integration_rustfs import StorageIntegrationService +from services.redis_task_manager import redis_task_manager +from services.material_service import MaterialService +from services.calculation_service import CalculationService +from services.llm_service import llm_service +from models.schemas import ProcessingStatus +from database.database import db_manager +from utils.html_generator import HTMLGenerator +from utils.logger import get_logger + +logger = get_logger(__name__) + + +class ProcessingService: + """核心处理流程编排 — 协调 STP 解析、网格、型腔、计算、保存、验证""" + + def __init__(self): + self.stp_parser = STPParser() + self.geometry_analyzer = GeometryAnalyzer() + self.mold_generator = MoldCavityGenerator(shrinkage_rate=0.005) + self.aluminum_foam_generator = AluminumFoamMoldGenerator(shrinkage_rate=0.015, draft_angle=3.0) + self.mold_quality_inspector = AluminumFoamMoldQualityInspector() + self.mesh_generator = MeshGenerator(quality="medium") + self.html_generator = HTMLGenerator() + self.storage_service = StorageIntegrationService() + self.multi_scheme_planner = MultiSchemeMoldPlanner() + + # ─── 对外入口 ─── + + async def process_file_with_storage( + self, + task_id: str, + file_path: str, + stp_file_id: int, + material: str = "ABS", + ): + """处理文件的后台任务 — 使用独立数据库会话""" + + # 创建独立的数据库会话,避免请求范围会话关闭 + async with db_manager.session() as db_session: + try: + logger.info(f"开始处理文件并生成模具型腔: {file_path}") + + # 设置处理超时(5分钟) + timeout_seconds = 300 + + try: + await asyncio.wait_for( + self.process_file_core( + task_id, file_path, stp_file_id, db_session, material + ), + timeout_seconds, + ) + except asyncio.TimeoutError: + logger.error(f"处理超时: {task_id}") + raise Exception(f"处理超时,超过{timeout_seconds}秒未完成") + + except Exception as e: + logger.error(f"模具型腔生成失败: {e}") + + await self.storage_service.update_stp_file_status(db_session, stp_file_id, "failed") + await self.storage_service.update_task_status( + db_session, task_id, "failed", error_message=str(e) + ) + + # 安全更新 Redis 任务状态 + task = await redis_task_manager.get_task(task_id) + if task: + await redis_task_manager.update_task(task_id, { + "status": ProcessingStatus.FAILED, + "error": str(e), + "completed_at": str(datetime.now()), + }) + + async def process_file_core( + self, + task_id: str, + file_path: str, + stp_file_id: int, + db_session: AsyncSession, + material: str = "ABS", + ): + """核心处理逻辑""" + + try: + logger.info(f"开始处理文件并生成模具型腔: {file_path}") + + # 1. 解析STP文件 + await self.storage_service.update_task_status( + db_session, task_id, "processing", 20, "解析STP文件" + ) + + shape = self.stp_parser.load_step_file(Path(file_path)) + geometry_data = self.stp_parser.analyze_geometry(shape) + + # 2. 生成网格数据并持久化 + await self.storage_service.update_task_status( + db_session, task_id, "processing", 30, "生成网格数据" + ) + + mesh_result = await self._step_generate_mesh( + shape, geometry_data, file_path, db_session, stp_file_id, task_id + ) + + # 3. 生成模具型腔 + await self.storage_service.update_task_status( + db_session, task_id, "processing", 40, "生成模具型腔" + ) + + # 材料属性 — 通过 MaterialService 集中管理 + requested_material = MaterialService.resolve_material(material) + selected_material = MaterialService.get_material(requested_material) + is_foam_material = MaterialService.is_foam_material(requested_material) + + plan_result = await self._step_generate_cavity( + shape, selected_material, is_foam_material + ) + + # 4. 生成详细JSON数据 — 委托 CalculationService + await self.storage_service.update_task_status( + db_session, task_id, "processing", 60, "生成型腔详细数据" + ) + + detailed_cavity_json = CalculationService.build_plan_result( + geometry_data=geometry_data, + material=selected_material, + file_path=str(file_path), + plan_result=plan_result, + ) + + best_scheme = CalculationService.get_best_scheme(detailed_cavity_json) + best_cavity_data = best_scheme.get("cavity_data", {}) if best_scheme else {} + best_key_info = best_scheme.get("key_info", {}) if best_scheme else {} + + if best_cavity_data.get("mold_cavities"): + cavity_geometry = best_cavity_data["mold_cavities"].get("cavity", {}) + logger.info( + f"推荐方案型腔数据已合并: cavity {cavity_geometry.get('vertex_count', 0)} 顶点" + ) + + # 5. 生成关键信息 + cavity_key_info = best_key_info + + # 6. 保存几何数据到数据库 + await self.storage_service.update_task_status( + db_session, task_id, "processing", 70, "保存几何数据" + ) + + await self.storage_service.save_geometry_data( + db_session, + stp_file_id, + geometry_data, + geometry_data.get("analysis_method", "mold_cavity"), + ) + + # 7. 生成HTML可视化 + await self.storage_service.update_task_status( + db_session, task_id, "processing", 85, "生成可视化报告" + ) + + pointcloud_data = None + lod_data = None + if mesh_result: + pointcloud_data = { + "points": mesh_result.get("points", []), + "normals": mesh_result.get("normals", []), + "vertices": mesh_result.get("vertices", []), + "faces": mesh_result.get("faces", []), + "point_count": mesh_result.get("point_count", 0), + "vertex_count": mesh_result.get("vertex_count", 0), + "face_count": mesh_result.get("face_count", 0), + } + + # 生成多级LOD数据(用于前端按距离切换精度) + try: + lod_result = self.mesh_generator.generate_multi_lod_mesh(shape) + if lod_result and lod_result.get("lods"): + lod_data = lod_result + logger.info(f"LOD数据生成成功: {len(lod_result['lods'])} 级 (面数: {[lod_result['lods'][k]['face_count'] for k in sorted(lod_result['lods'].keys())]})") + except Exception as lod_err: + logger.warning(f"LOD数据生成失败,使用单级精度: {lod_err}") + + detailed_cavity_json = await self._attach_scheme_previews( + detailed_cavity_json=detailed_cavity_json, + geometry_data=geometry_data, + stp_filename=Path(file_path).name, + pointcloud_data=pointcloud_data, + lod_data=lod_data, + ) + + best_scheme = CalculationService.get_best_scheme(detailed_cavity_json) + best_cavity_data = best_scheme.get("cavity_data", {}) if best_scheme else best_cavity_data + best_key_info = best_scheme.get("key_info", {}) if best_scheme else best_key_info + + # 8. 保存模具型腔数据(包含方案级预览链接) + await self.storage_service.save_mold_cavity_data( + db_session, stp_file_id, detailed_cavity_json + ) + + html_file_path = self.html_generator.generate_and_save_visualization( + geometry_data, + Path(file_path).name, + cavity_data=best_cavity_data, + pointcloud_data=pointcloud_data, + lod_data=lod_data, + ) + + await self.storage_service.save_html_file( + db_session, + stp_file_id, + Path(html_file_path).name, + html_file_path, + ) + + # 9. 分析模具设计 + analysis_result = self.geometry_analyzer.analyze_mold_design(geometry_data) + + if analysis_result: + await self.storage_service.save_features_and_recommendations( + db_session, + stp_file_id, + analysis_result.get("detected_features", []), + analysis_result.get("design_recommendations", []), + ) + + await self._save_analysis_metrics(db_session, stp_file_id, analysis_result) + + # 9.6 更新STP文件的分析摘要字段 + await self.storage_service.update_stp_file_analysis_summary( + db_session, + stp_file_id, + volume=geometry_data.get("volume", 0), + surface_area=geometry_data.get("surface_area", 0), + product_weight=CalculationService.calculate_product_weight( + geometry_data.get("volume", 0), selected_material["density"] + ), + ) + + # 9.7 FreeCAD 几何验证 + verification_result = await self._step_verify( + file_path, db_session, task_id, stp_file_id, analysis_result + ) + + # 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, + ) + + # 10. 完成处理 + await self.storage_service.update_stp_file_status(db_session, stp_file_id, "completed") + await self.storage_service.update_task_status( + db_session, task_id, "completed", 100, "模具型腔生成完成" + ) + + # 更新任务缓存状态 + await redis_task_manager.update_task(task_id, { + "geometry_data": geometry_data, + "analysis_result": analysis_result, + "plan_result": detailed_cavity_json, + "candidate_schemes": detailed_cavity_json.get("candidate_schemes", []), + "best_scheme_id": detailed_cavity_json.get("best_scheme_id"), + "cavity_data": best_cavity_data, + "key_info": best_key_info, + "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()), + }) + + logger.info(f"模具型腔生成完成: {task_id}") + logger.info(f"key_info metadata: {detailed_cavity_json.get('metadata', {})}") + logger.info(f"key_info manufacturing_info: {detailed_cavity_json.get('manufacturing_info', {})}") + logger.info(f"key_info geometric_characteristics: {detailed_cavity_json.get('mold_cavities', {}).get('cavity_key_info', {}).get('geometric_characteristics', {})}") + + except Exception as e: + logger.error(f"模具型腔生成失败: {e}") + + await self.storage_service.update_stp_file_status(db_session, stp_file_id, "failed") + await self.storage_service.update_task_status( + db_session, task_id, "failed", error_message=str(e) + ) + + task = await redis_task_manager.get_task(task_id) + if task: + await redis_task_manager.update_task(task_id, { + "status": ProcessingStatus.FAILED, + "error": str(e), + "completed_at": str(datetime.now()), + }) + + # ─── 内部步骤 ─── + + async def _step_generate_mesh( + self, shape, geometry_data: dict, file_path: str, + db_session: AsyncSession, stp_file_id: int, task_id: str, + ) -> Optional[Dict[str, Any]]: + """生成网格数据并持久化,失败不影响主流程""" + mesh_result = None + try: + mesh_result = self.mesh_generator.generate_mesh_from_shape(shape) + + vertices = mesh_result.get("vertices", []) + faces = mesh_result.get("faces", []) + points = mesh_result.get("points", []) + normals = mesh_result.get("normals", []) + point_count = mesh_result.get("point_count", 0) + vertex_count = mesh_result.get("vertex_count", 0) + face_count = mesh_result.get("face_count", 0) + + if vertices and faces: + bbox = geometry_data.get("bounding_box", {}) + + mesh_json = { + "metadata": { + "file_name": Path(file_path).name, + "generated_at": datetime.now().isoformat(), + "quality": "medium", + "vertex_count": vertex_count, + "face_count": face_count, + "point_count": point_count, + }, + "mesh": { + "vertices": vertices, + "faces": faces, + }, + "pointcloud": { + "points": points, + "normals": normals, + "count": point_count, + }, + "bounding_box": bbox, + } + + await self.storage_service.save_mesh_data( + db_session, + stp_file_id=stp_file_id, + mesh_json=mesh_json, + quality="medium", + ) + await redis_task_manager.update_task(task_id, { + "mesh_summary": { + "vertex_count": vertex_count, + "face_count": face_count, + "point_count": point_count, + "quality": "medium", + } + }) + except Exception as mesh_err: + logger.warning(f"网格生成或保存失败,不影响主流程: {mesh_err}") + + return mesh_result + + async def _step_generate_cavity( + self, shape, selected_material: dict, is_foam_material: bool, + ) -> Optional[Dict[str, Any]]: + """生成多方案分模结果""" + plan_result = None + try: + if shape: + plan_result = self.multi_scheme_planner.generate_plan( + shape=shape, + material=selected_material, + is_foam_material=is_foam_material, + ) + logger.info( + f"多方案分模完成: 生成 {len(plan_result.get('candidate_schemes', []))} 套方案" + ) + except Exception as cavity_err: + logger.warning(f"多方案分模失败,使用简化数据: {cavity_err}") + traceback.print_exc() + plan_result = None + + return plan_result + + async def _step_verify( + self, file_path: str, db_session: AsyncSession, + task_id: str, stp_file_id: int, analysis_result: Optional[dict], + ) -> Optional[Dict[str, Any]]: + """FreeCAD 几何验证(可通过配置禁用)""" + from config.settings import settings + + if not settings.ENABLE_FREECAD_VERIFICATION: + logger.info("FreeCAD验证已禁用(设置 ENABLE_FREECAD_VERIFICATION=true 启用)") + return {"status": "disabled", "reason": "FreeCAD验证已禁用"} + + await self.storage_service.update_task_status( + db_session, task_id, "processing", 90, "FreeCAD几何验证" + ) + + try: + from services.verification_service import GeometryVerificationService + verification_svc = GeometryVerificationService(timeout=settings.FREECAD_VERIFICATION_TIMEOUT) + verification_result = await verification_svc.verify_stp_file(file_path) + + if verification_result and analysis_result: + await self._save_verification_metrics(db_session, stp_file_id, verification_result) + + logger.info(f"FreeCAD验证完成: {verification_result.get('status', 'unknown') if verification_result else 'failed'}") + return verification_result + except Exception as ve: + logger.warning(f"FreeCAD验证失败(不影响主流程): {ve}") + return {"status": "error", "error": str(ve)} + + async def _attach_scheme_previews( + self, + detailed_cavity_json: Dict[str, Any], + geometry_data: Dict[str, Any], + stp_filename: str, + pointcloud_data: Optional[Dict[str, Any]] = None, + lod_data: Optional[Dict[str, Any]] = None, + ) -> Dict[str, Any]: + """为每个候选分模方案生成独立HTML预览链接。""" + candidate_schemes = detailed_cavity_json.get("candidate_schemes", []) + if not candidate_schemes: + return detailed_cavity_json + + for scheme in candidate_schemes: + cavity_data = scheme.get("cavity_data") + if not cavity_data: + continue + suffix = scheme.get("scheme_id") + html_path = self.html_generator.generate_and_save_visualization( + geometry_data, + stp_filename, + cavity_data=cavity_data, + pointcloud_data=pointcloud_data, + suffix=suffix, + lod_data=lod_data, + ) + scheme["html_file"] = f"/html/{Path(html_path).name}" + + best_scheme = CalculationService.get_best_scheme(detailed_cavity_json) + if best_scheme: + detailed_cavity_json["html_file"] = best_scheme.get("html_file") + + return detailed_cavity_json + + # ─── 指标持久化 ─── + + async def _save_analysis_metrics(self, session: AsyncSession, stp_file_id: int, analysis_result: dict): + """保存分析指标到数据库""" + from models.database import AnalysisMetrics + + quality_metrics = analysis_result.get("quality_metrics", {}) + analysis_summary = analysis_result.get("analysis_summary", "") + + metrics = AnalysisMetrics( + stp_file_id=stp_file_id, + volume_utilization=quality_metrics.get("volume_utilization", 0), + topology_complexity=quality_metrics.get("topology_complexity", 0), + wall_uniformity=quality_metrics.get("wall_uniformity", 0), + analysis_summary=analysis_summary, + ) + + session.add(metrics) + await session.commit() + logger.info(f"分析指标保存成功: {metrics.id}") + + async def _save_verification_metrics(self, session: AsyncSession, stp_file_id: int, verification_result: dict): + """保存验证指标到数据库""" + from models.database import AnalysisMetrics + from sqlalchemy import select + + result = await session.execute( + select(AnalysisMetrics).where(AnalysisMetrics.stp_file_id == stp_file_id) + ) + metrics = result.scalar_one_or_none() + + comparison = verification_result.get("comparison", {}) + volume_comparison = comparison.get("volume", {}) + area_comparison = comparison.get("surface_area", {}) + + if metrics: + metrics.verification_status = verification_result.get("status", "unknown") + metrics.verification_volume_diff = volume_comparison.get("difference_percent", 0) + metrics.verification_area_diff = area_comparison.get("difference_percent", 0) + metrics.verification_details = verification_result + else: + metrics = AnalysisMetrics( + stp_file_id=stp_file_id, + verification_status=verification_result.get("status", "unknown"), + verification_volume_diff=volume_comparison.get("difference_percent", 0), + verification_area_diff=area_comparison.get("difference_percent", 0), + verification_details=verification_result, + ) + session.add(metrics) + + await session.commit() + logger.info(f"验证指标保存成功: stp_file_id={stp_file_id}") + + +# 模块级单例,供路由层直接使用 +processing_service = ProcessingService()