888 lines
36 KiB
Python
888 lines
36 KiB
Python
# models/database.py
|
||
from sqlalchemy import Column, Integer, String, Text, DateTime, Date, JSON, LargeBinary, Boolean, Float, ForeignKey, UniqueConstraint, Numeric, CheckConstraint
|
||
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"<User(id={self.id}, username='{self.username}')>"
|
||
|
||
|
||
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"<Role(code='{self.code}', name='{self.name}')>"
|
||
|
||
|
||
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"<Permission(code='{self.code}', name='{self.name}')>"
|
||
|
||
|
||
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"<UserRole(user_id={self.user_id}, role_id={self.role_id})>"
|
||
|
||
|
||
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"<RolePermission(role_id={self.role_id}, permission_id={self.permission_id})>"
|
||
|
||
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)
|
||
# 关联进销存成品(P2-1:分析结果可一键创建为成品并回写)
|
||
product_id = Column(Integer, ForeignKey("products.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")
|
||
product = relationship("Product") # P2-1: 关联的进销存成品
|
||
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"<STPFile(id={self.id}, original_filename='{self.original_filename}', status='{self.status}')>"
|
||
|
||
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"<GeometryData(id={self.id}, stp_file_id={self.stp_file_id})>"
|
||
|
||
|
||
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"<MeshData(id={self.id}, stp_file_id={self.stp_file_id}, quality='{self.quality}')>"
|
||
|
||
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"<HTMLFile(id={self.id}, stp_file_id={self.stp_file_id}, object_key='{self.object_key}')>"
|
||
|
||
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"<ProcessingTask(id={self.id}, task_id='{self.task_id}', status='{self.status}')>"
|
||
|
||
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"<MoldCavityData(stp_file_id={self.stp_file_id}, mold_material='{self.mold_material}')>"
|
||
|
||
|
||
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"<FeatureDetection(id={self.id}, feature_type='{self.feature_type}', confidence={self.confidence})>"
|
||
|
||
|
||
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"<DesignRecommendation(id={self.id}, rec_type='{self.rec_type}', priority='{self.priority}')>"
|
||
|
||
|
||
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"<UserActivity(id={self.id}, user_id={self.user_id}, activity_type='{self.activity_type}')>"
|
||
|
||
|
||
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"<SystemLog(id={self.id}, level='{self.level}', module='{self.module}')>"
|
||
|
||
|
||
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"<Product(id={self.id}, sku='{self.sku}', name='{self.name}')>"
|
||
|
||
|
||
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"<ProductMaterial(finished_product_id={self.finished_product_id}, material_product_id={self.material_product_id})>"
|
||
|
||
|
||
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"<MaterialPriceHistory(product_id={self.product_id}, price={self.price}, date={self.effective_date})>"
|
||
|
||
|
||
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"<MaterialSupplier(product_id={self.product_id}, supplier_id={self.supplier_id}, primary={self.is_primary})>"
|
||
|
||
|
||
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"<Supplier(id={self.id}, name='{self.name}')>"
|
||
|
||
|
||
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"<Customer(id={self.id}, name='{self.name}')>"
|
||
|
||
|
||
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"<Warehouse(id={self.id}, name='{self.name}')>"
|
||
|
||
|
||
class Inventory(Base):
|
||
"""库存表"""
|
||
__tablename__ = "inventory"
|
||
__table_args__ = (
|
||
UniqueConstraint("product_id", "warehouse_id", name="uq_inventory_product_warehouse"),
|
||
CheckConstraint("quantity >= 0 AND locked_quantity >= 0 AND locked_quantity <= quantity", name="ck_inventory_qty_nonnegative"),
|
||
)
|
||
|
||
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"<Inventory(product_id={self.product_id}, quantity={self.quantity})>"
|
||
|
||
@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"<StockMovement(id={self.id}, type='{self.movement_type}', qty={self.quantity})>"
|
||
|
||
|
||
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"<PurchaseOrder(order_no='{self.order_no}', status='{self.status}')>"
|
||
|
||
|
||
class PurchaseOrderItem(Base):
|
||
"""采购订单明细表"""
|
||
__tablename__ = "purchase_order_items"
|
||
__table_args__ = (
|
||
CheckConstraint("quantity > 0 AND received_quantity >= 0 AND received_quantity <= quantity", name="ck_purchase_order_items_qty"),
|
||
)
|
||
|
||
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"<PurchaseOrderItem(order_id={self.order_id}, product_id={self.product_id})>"
|
||
|
||
|
||
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"<SalesOrder(order_no='{self.order_no}', status='{self.status}')>"
|
||
|
||
|
||
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"<FinanceTransaction(txn_no='{self.txn_no}', txn_type='{self.txn_type}', amount={self.amount})>"
|
||
|
||
|
||
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"<FinanceAllocation(transaction_id={self.transaction_id}, order_type='{self.order_type}', amount={self.allocated_amount})>"
|
||
|
||
|
||
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"<AnalysisMetrics(stp_file_id={self.stp_file_id}, volume_utilization={self.volume_utilization})>"
|
||
|
||
|
||
class SalesOrderItem(Base):
|
||
"""销售订单明细表"""
|
||
__tablename__ = "sales_order_items"
|
||
__table_args__ = (
|
||
CheckConstraint("quantity > 0 AND delivered_quantity >= 0 AND delivered_quantity <= quantity", name="ck_sales_order_items_qty"),
|
||
)
|
||
|
||
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"<SalesOrderItem(order_id={self.order_id}, product_id={self.product_id})>"
|
||
|