feat: 移除PDI和订单号字段,新增设备巡检模块

- 从物料跟踪页面移除订单号列和表单字段
- 从导航菜单移除PDI管理,添加设备巡检
- 新增InspectionLocation和InspectionRecord后端模型和API
- 新增设备巡检前端页面(左侧点位列表,右侧设备和历史记录)
This commit is contained in:
2026-05-27 16:38:40 +08:00
commit 193da0018f
86 changed files with 11379 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
from app.models.user import User
from app.models.material import Coil, MaterialTracking
from app.models.production import ProductionRecord
from app.models.plan import ProductionPlan
from app.models.downtime import DowntimeRecord, DowntimeCategory
from app.models.equipment import Equipment, EquipmentMaintenance
from app.models.message import MessageLog
from app.models.pdi import PDIRecord
from app.models.quality import QualityRecord
from app.models.energy import EnergyRecord
from app.models.inspection import InspectionLocation, InspectionRecord
__all__ = [
"User",
"Coil", "MaterialTracking",
"ProductionRecord",
"ProductionPlan",
"DowntimeRecord", "DowntimeCategory",
"Equipment", "EquipmentMaintenance",
"MessageLog",
"PDIRecord",
"QualityRecord",
"EnergyRecord",
"InspectionLocation", "InspectionRecord",
]

View File

@@ -0,0 +1,38 @@
from sqlalchemy import Column, Integer, String, Float, DateTime, Text, ForeignKey, func
from app.database import Base
class DowntimeCategory(Base):
"""停机类别"""
__tablename__ = "downtime_categories"
id = Column(Integer, primary_key=True, index=True)
code = Column(String(20), unique=True, nullable=False)
name = Column(String(50), nullable=False)
category = Column(String(20), comment="大类: equipment/process/material/other")
description = Column(Text)
is_active = Column(Integer, default=1)
class DowntimeRecord(Base):
"""停机记录"""
__tablename__ = "downtime_records"
id = Column(Integer, primary_key=True, index=True)
category_id = Column(Integer, ForeignKey("downtime_categories.id"))
category_code = Column(String(20), index=True)
category_name = Column(String(50))
shift = Column(String(10), comment="班次")
shift_date = Column(DateTime, comment="班期")
start_time = Column(DateTime, nullable=False, comment="停机开始时间")
end_time = Column(DateTime, comment="停机结束时间")
duration = Column(Float, comment="停机时长min自动计算")
equipment_code = Column(String(30), comment="停机设备编号")
fault_desc = Column(Text, comment="故障描述")
action_taken = Column(Text, comment="处理措施")
root_cause = Column(Text, comment="根本原因")
reporter = Column(String(50), comment="报告人")
handler = Column(String(50), comment="处理人")
is_planned = Column(Integer, default=0, comment="是否计划停机")
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())

View File

@@ -0,0 +1,36 @@
from sqlalchemy import Column, Integer, String, Float, DateTime, Text, func
from app.database import Base
class EnergyRecord(Base):
"""班次能耗记录"""
__tablename__ = "energy_records"
id = Column(Integer, primary_key=True, index=True)
shift = Column(String(10), nullable=True, comment="班次 甲/乙/丙/丁")
shift_date = Column(DateTime, nullable=True, comment="班期")
# 产量
production_weight_kg = Column(Float, nullable=True, comment="本班产量 kg")
# 电力
power_consumption_kwh = Column(Float, nullable=True, comment="电耗 kWh")
power_unit = Column(Float, nullable=True, comment="电耗单耗 kWh/t")
# 蒸汽
steam_consumption_kg = Column(Float, nullable=True, comment="蒸汽消耗 kg")
steam_unit = Column(Float, nullable=True, comment="蒸汽单耗 kg/t")
# 盐酸
acid_consumption_kg = Column(Float, nullable=True, comment="盐酸消耗 kg")
acid_unit = Column(Float, nullable=True, comment="酸耗单耗 kg/t")
# 冷却水
cooling_water_m3 = Column(Float, nullable=True, comment="冷却水 m³")
water_unit = Column(Float, nullable=True, comment="冷却水单耗 m³/t")
# 各槽HCl平均浓度 (JSON字符串, e.g. '[180,175,170,165,160,155]')
acid_conc_avg = Column(Text, nullable=True, comment="各槽HCl平均浓度 JSON g/L")
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())

View File

@@ -0,0 +1,53 @@
from sqlalchemy import Column, Integer, String, Float, DateTime, Enum, Text, ForeignKey, func
from app.database import Base
import enum
class EquipmentStatus(str, enum.Enum):
NORMAL = "normal" # 正常
FAULT = "fault" # 故障
MAINTENANCE = "maintenance" # 检修
STANDBY = "standby" # 备用
class Equipment(Base):
"""设备台账"""
__tablename__ = "equipments"
id = Column(Integer, primary_key=True, index=True)
code = Column(String(30), unique=True, nullable=False, index=True, comment="设备编号")
name = Column(String(100), nullable=False, comment="设备名称")
category = Column(String(30), comment="设备类别")
model = Column(String(50), comment="型号规格")
manufacturer = Column(String(100), comment="制造厂商")
install_date = Column(DateTime, comment="投用日期")
location = Column(String(50), comment="安装位置")
status = Column(Enum(EquipmentStatus), default=EquipmentStatus.NORMAL)
rated_power = Column(Float, comment="额定功率kW")
remark = Column(Text)
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
class EquipmentMaintenance(Base):
"""设备维保记录"""
__tablename__ = "equipment_maintenance"
id = Column(Integer, primary_key=True, index=True)
equipment_id = Column(Integer, ForeignKey("equipments.id"), nullable=False)
equipment_code = Column(String(30), index=True)
maintenance_type = Column(String(20), comment="类型: repair/planned/inspection")
title = Column(String(200), nullable=False)
description = Column(Text, comment="维保内容")
start_time = Column(DateTime, nullable=False)
end_time = Column(DateTime)
duration = Column(Float, comment="工时h")
cost = Column(Float, comment="费用元")
spare_parts = Column(Text, comment="更换备件JSON格式")
technician = Column(String(50), comment="执行人")
approver = Column(String(50), comment="审核人")
next_maintenance = Column(DateTime, comment="下次维保时间")
result = Column(String(20), comment="结果: pass/fail/pending")
remark = Column(Text)
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())

View File

@@ -0,0 +1,28 @@
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, func
from app.database import Base
class InspectionLocation(Base):
__tablename__ = "inspection_locations"
id = Column(Integer, primary_key=True, index=True)
code = Column(String(30), unique=True, nullable=False, index=True)
name = Column(String(100), nullable=False)
description = Column(Text)
sort_order = Column(Integer, default=0)
created_at = Column(DateTime, server_default=func.now())
class InspectionRecord(Base):
__tablename__ = "inspection_records"
id = Column(Integer, primary_key=True, index=True)
location_id = Column(Integer, ForeignKey("inspection_locations.id"), nullable=False)
location_name = Column(String(100))
equipment_code = Column(String(30), index=True)
equipment_name = Column(String(100))
scan_code = Column(String(200))
inspector = Column(String(50), nullable=False)
result = Column(String(20), default="normal")
notes = Column(Text)
created_at = Column(DateTime, server_default=func.now())

View File

@@ -0,0 +1,56 @@
from sqlalchemy import Column, Integer, String, Float, DateTime, Enum, Text, ForeignKey, func
from sqlalchemy.orm import relationship
from app.database import Base
import enum
class CoilStatus(str, enum.Enum):
WAITING = "waiting" # 等待入线
ON_LINE = "on_line" # 在线处理
FINISHED = "finished" # 处理完成
ABNORMAL = "abnormal" # 异常
class Coil(Base):
"""钢卷主档"""
__tablename__ = "coils"
id = Column(Integer, primary_key=True, index=True)
coil_no = Column(String(30), unique=True, nullable=False, index=True, comment="卷号")
order_no = Column(String(30), index=True, comment="订单号")
steel_grade = Column(String(30), comment="钢种")
spec_thickness = Column(Float, comment="规格厚度mm")
spec_width = Column(Float, comment="规格宽度mm")
target_thickness = Column(Float, comment="目标厚度mm")
target_width = Column(Float, comment="目标宽度mm")
gross_weight = Column(Float, comment="毛重kg")
net_weight = Column(Float, comment="净重kg")
inner_diameter = Column(Float, comment="内径mm")
status = Column(Enum(CoilStatus), default=CoilStatus.WAITING)
plan_id = Column(Integer, ForeignKey("production_plans.id"), nullable=True)
remark = Column(Text)
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
tracking = relationship("MaterialTracking", back_populates="coil")
class MaterialTracking(Base):
"""物料跟踪记录"""
__tablename__ = "material_tracking"
id = Column(Integer, primary_key=True, index=True)
coil_id = Column(Integer, ForeignKey("coils.id"), nullable=False, index=True)
coil_no = Column(String(30), nullable=False, index=True)
position = Column(String(50), comment="当前位置/工位")
event_type = Column(String(30), comment="事件类型: entry/exit/process/inspect")
event_desc = Column(String(200), comment="事件描述")
actual_thickness = Column(Float, comment="实测厚度")
actual_width = Column(Float, comment="实测宽度")
speed = Column(Float, comment="线速度m/min")
tension = Column(Float, comment="张力N")
operator = Column(String(50))
event_time = Column(DateTime, nullable=False, index=True)
created_at = Column(DateTime, server_default=func.now())
coil = relationship("Coil", back_populates="tracking")

View File

@@ -0,0 +1,20 @@
from sqlalchemy import Column, Integer, String, DateTime, Text, Float, func
from app.database import Base
class MessageLog(Base):
"""L1报文日志"""
__tablename__ = "message_logs"
id = Column(Integer, primary_key=True, index=True)
msg_id = Column(String(50), index=True, comment="报文ID")
msg_type = Column(String(30), index=True, comment="报文类型")
direction = Column(String(10), comment="方向: recv/send")
source = Column(String(50), comment="来源系统")
raw_data = Column(Text, comment="原始报文")
parsed_data = Column(Text, comment="解析结果JSON")
status = Column(String(20), default="success", comment="处理状态: success/error")
error_msg = Column(Text, comment="错误信息")
process_time = Column(Float, comment="处理耗时ms")
received_at = Column(DateTime, nullable=False, index=True)
created_at = Column(DateTime, server_default=func.now())

59
backend/app/models/pdi.py Normal file
View File

@@ -0,0 +1,59 @@
import enum
from sqlalchemy import Column, Integer, String, Float, DateTime, Text, Enum, func
from app.database import Base
class L3Status(str, enum.Enum):
pending = "pending"
sent = "sent"
confirmed = "confirmed"
cancelled = "cancelled"
class L2Status(str, enum.Enum):
pending = "pending"
processing = "processing"
done = "done"
class PDIRecord(Base):
"""PDI (Process Data Input) per-coil process order from L3."""
__tablename__ = "pdi_records"
id = Column(Integer, primary_key=True, index=True)
coil_no = Column(String(30), nullable=False, index=True, comment="卷号")
order_no = Column(String(30), nullable=True, comment="订单号")
customer = Column(String(100), nullable=True, comment="客户名称")
steel_grade = Column(String(30), nullable=True, comment="钢种")
# 几何规格
thickness = Column(Float, nullable=True, comment="来料厚度 mm")
width = Column(Float, nullable=True, comment="来料宽度 mm")
target_thickness = Column(Float, nullable=True, comment="目标厚度 mm")
target_width = Column(Float, nullable=True, comment="目标宽度 mm")
# 力学性能
yield_strength = Column(Float, nullable=True, comment="屈服强度 MPa")
tensile_strength = Column(Float, nullable=True, comment="抗拉强度 MPa")
elongation = Column(Float, nullable=True, comment="延伸率 %")
# 卷重/尺寸
coil_weight = Column(Float, nullable=True, comment="卷重 kg")
inner_diameter = Column(Float, nullable=True, comment="内径 mm")
outer_diameter = Column(Float, nullable=True, comment="外径 mm")
# 工艺
process_route = Column(String(100), nullable=True, comment="工艺路径 e.g. P1+P2+P3+P4+P5+P6")
priority = Column(Integer, default=3, comment="优先级 1-5")
# 状态
l3_status = Column(Enum(L3Status), default=L3Status.pending, comment="L3状态")
l2_status = Column(Enum(L2Status), default=L2Status.pending, comment="L2状态")
# 时间戳
send_time = Column(DateTime, nullable=True, comment="下发时间")
confirm_time = Column(DateTime, nullable=True, comment="确认时间")
remark = Column(Text, nullable=True, comment="备注")
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())

View File

@@ -0,0 +1,33 @@
from sqlalchemy import Column, Integer, String, Float, DateTime, Enum, Text, func
from app.database import Base
import enum
class PlanStatus(str, enum.Enum):
DRAFT = "draft" # 草稿
CONFIRMED = "confirmed" # 已确认
IN_PROGRESS = "in_progress" # 执行中
COMPLETED = "completed" # 完成
CANCELLED = "cancelled" # 取消
class ProductionPlan(Base):
"""生产计划"""
__tablename__ = "production_plans"
id = Column(Integer, primary_key=True, index=True)
plan_no = Column(String(30), unique=True, nullable=False, index=True, comment="计划号")
plan_date = Column(DateTime, nullable=False, comment="计划日期")
shift = Column(String(10), comment="班次")
plan_quantity = Column(Integer, default=0, comment="计划数量(卷)")
plan_weight = Column(Float, default=0, comment="计划重量kg")
actual_quantity = Column(Integer, default=0, comment="实际数量(卷)")
actual_weight = Column(Float, default=0, comment="实际重量kg")
status = Column(Enum(PlanStatus), default=PlanStatus.DRAFT)
steel_grade = Column(String(30), comment="主要钢种")
spec_range = Column(String(50), comment="规格范围")
priority = Column(Integer, default=5, comment="优先级1-10")
remark = Column(Text)
created_by = Column(String(50))
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())

View File

@@ -0,0 +1,28 @@
from sqlalchemy import Column, Integer, String, Float, DateTime, Text, ForeignKey, func
from app.database import Base
class ProductionRecord(Base):
"""生产实绩"""
__tablename__ = "production_records"
id = Column(Integer, primary_key=True, index=True)
coil_no = Column(String(30), nullable=False, index=True)
plan_id = Column(Integer, ForeignKey("production_plans.id"), nullable=True)
shift = Column(String(10), comment="班次: 甲/乙/丙/丁")
shift_date = Column(DateTime, comment="班期")
start_time = Column(DateTime, comment="开始时间")
end_time = Column(DateTime, comment="结束时间")
process_length = Column(Float, comment="处理长度m")
process_weight = Column(Float, comment="处理重量kg")
avg_speed = Column(Float, comment="平均速度m/min")
max_speed = Column(Float, comment="最大速度m/min")
acid_consumption = Column(Float, comment="酸耗量L")
inlet_thickness = Column(Float, comment="入口厚度mm")
outlet_thickness = Column(Float, comment="出口厚度mm")
inlet_width = Column(Float, comment="入口宽度mm")
quality_grade = Column(String(10), comment="质量等级")
operator = Column(String(50))
remark = Column(Text)
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())

View File

@@ -0,0 +1,38 @@
from sqlalchemy import Column, Integer, String, Float, DateTime, Text, Boolean, ForeignKey, func
from app.database import Base
class QualityRecord(Base):
"""质量检验记录"""
__tablename__ = "quality_records"
id = Column(Integer, primary_key=True, index=True)
coil_no = Column(String(30), nullable=False, index=True, comment="卷号")
production_record_id = Column(Integer, ForeignKey("production_records.id"), nullable=True, comment="关联生产实绩")
# 实测规格
thickness_actual = Column(Float, nullable=True, comment="实测厚度 mm")
width_actual = Column(Float, nullable=True, comment="实测宽度 mm")
flatness = Column(Float, nullable=True, comment="平直度 IU")
crown = Column(Float, nullable=True, comment="凸度 μm")
# 表面缺陷
surface_defect_type = Column(String(50), nullable=True, comment="表面缺陷类型")
defect_length_m = Column(Float, nullable=True, comment="缺陷长度 m")
defect_position = Column(String(50), nullable=True, comment="缺陷位置")
# 质量模型评分
pi_score = Column(Float, nullable=True, comment="酸洗指数评分 0-100")
surface_score = Column(Float, nullable=True, comment="表面质量评分 0-100")
overall_grade = Column(String(5), nullable=True, comment="综合等级 A1/A2/B1/B2/C")
# 残酸 / 粗糙度
acid_residual = Column(Float, nullable=True, comment="残酸量 g/m²")
roughness_ra = Column(Float, nullable=True, comment="粗糙度 Ra μm")
# 检验信息
inspector = Column(String(50), nullable=True, comment="检验员")
inspect_time = Column(DateTime, nullable=True, comment="检验时间")
is_passed = Column(Boolean, default=True, comment="是否合格")
created_at = Column(DateTime, server_default=func.now())

View File

@@ -0,0 +1,15 @@
from sqlalchemy import Column, Integer, String, Boolean, DateTime, func
from app.database import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String(50), unique=True, nullable=False, index=True)
full_name = Column(String(100))
hashed_password = Column(String(255), nullable=False)
role = Column(String(20), default="operator") # admin/engineer/operator/viewer
is_active = Column(Boolean, default=True)
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())