feat(linkage): 移动改为任意入口位置选择,仅上卷鞍座触发生产
- 计划新增 position 字段;新增 /plan/{id}/move?position=… 与 /plan/positions/all
- line_service.place_at_position:放到任意位置(位置唯一占用),上卷鞍座单独触发生产联动
- 入口跟踪:新增入口位置图(单一鞍座)显示占位;移动按钮弹出位置选择框
- 计划管理:移动按钮同样弹出位置选择框
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -142,6 +142,28 @@ async def move_to_saddle(plan_id: int, db: AsyncSession = Depends(get_db), _ = D
|
||||
return Response.ok(PlanOut.model_validate(plan))
|
||||
|
||||
|
||||
@router.get("/positions/all", response_model=Response[list[str]])
|
||||
async def list_positions(_ = Depends(get_current_user)):
|
||||
"""可移动到的入口位置列表(含唯一上卷鞍座)。"""
|
||||
return Response.ok(line_service.ENTRY_POSITIONS)
|
||||
|
||||
|
||||
@router.patch("/{plan_id}/move", response_model=Response[PlanOut])
|
||||
async def move_plan(plan_id: int, position: str = Query(...), db: AsyncSession = Depends(get_db), _ = Depends(get_current_user)):
|
||||
"""移动:把计划放到所选入口位置;放到上卷鞍座才触发生产联动。"""
|
||||
result = await db.execute(select(ProductionPlan).where(ProductionPlan.id == plan_id))
|
||||
plan = result.scalar_one_or_none()
|
||||
if not plan:
|
||||
raise HTTPException(status_code=404, detail="计划不存在")
|
||||
try:
|
||||
await line_service.place_at_position(db, plan, position)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
await db.flush()
|
||||
await db.refresh(plan)
|
||||
return Response.ok(PlanOut.model_validate(plan))
|
||||
|
||||
|
||||
@router.patch("/{plan_id}/commit", response_model=Response[PlanOut])
|
||||
async def commit_producing(plan_id: int, db: AsyncSession = Depends(get_db), _ = Depends(get_current_user)):
|
||||
"""投入生产:把鞍座上的计划置为生产中(兜底未实时变化的数据)。"""
|
||||
|
||||
@@ -59,7 +59,8 @@ async def _run_migrations(conn):
|
||||
"ALTER TABLE production_plans ADD COLUMN IF NOT EXISTS incoming_weight DOUBLE PRECISION",
|
||||
"ALTER TABLE production_plans ADD COLUMN IF NOT EXISTS incoming_od DOUBLE PRECISION",
|
||||
"ALTER TABLE production_plans ADD COLUMN IF NOT EXISTS split_weights JSONB",
|
||||
# 上卷鞍座 / 生产联动字段
|
||||
# 入口位置 / 上卷鞍座 / 生产联动字段
|
||||
"ALTER TABLE production_plans ADD COLUMN IF NOT EXISTS position VARCHAR(30)",
|
||||
"ALTER TABLE production_plans ADD COLUMN IF NOT EXISTS on_saddle INTEGER DEFAULT 0",
|
||||
"ALTER TABLE production_plans ADD COLUMN IF NOT EXISTS saddle_at TIMESTAMP",
|
||||
"ALTER TABLE production_plans ADD COLUMN IF NOT EXISTS run_started_at TIMESTAMP",
|
||||
|
||||
@@ -35,7 +35,8 @@ class ProductionPlan(Base):
|
||||
incoming_od = Column(Float, comment="来料外径 mm")
|
||||
split_weights = Column(JSON, comment="分卷重量 [t,...]")
|
||||
|
||||
# 上卷鞍座 / 生产联动
|
||||
# 入口位置 / 上卷鞍座 / 生产联动
|
||||
position = Column(String(30), comment="当前入口位置(上卷小车/称重位/地辊/倒卷小车/上卷鞍座)")
|
||||
on_saddle = Column(Integer, default=0, comment="是否在上卷鞍座 0/1")
|
||||
saddle_at = Column(DateTime, comment="移动到鞍座时间")
|
||||
run_started_at = Column(DateTime, comment="投入生产(有速度)时间")
|
||||
|
||||
@@ -78,7 +78,8 @@ class PlanOut(BaseModel):
|
||||
remark: Optional[str] = None
|
||||
created_by: Optional[str] = None
|
||||
created_at: datetime
|
||||
# 上卷鞍座 / 生产联动
|
||||
# 入口位置 / 上卷鞍座 / 生产联动
|
||||
position: Optional[str] = None
|
||||
on_saddle: Optional[int] = 0
|
||||
saddle_at: Optional[datetime] = None
|
||||
run_started_at: Optional[datetime] = None
|
||||
|
||||
@@ -17,12 +17,45 @@ from app.models.production import ProductionRecord
|
||||
from app.models.downtime import DowntimeRecord
|
||||
from app.models.line_state import LineState
|
||||
|
||||
# ── 入口位置 ──
|
||||
SADDLE_NAME = "上卷鞍座" # 唯一会触发生产联动的位置
|
||||
ENTRY_POSITIONS = [
|
||||
"1#上卷小车", "2#上卷小车",
|
||||
"1#称重位", "2#称重位",
|
||||
"1#地辊", "2#地辊",
|
||||
"1#倒卷小车", "2#倒卷小车",
|
||||
SADDLE_NAME,
|
||||
]
|
||||
|
||||
# ── 仿真参数 ──
|
||||
TARGET_LENGTH_M = 2000.0 # 带头目标长度(生产完成阈值)
|
||||
SIM_SPEED_M_MIN = 600.0 # 仿真线速度 m/min(2000m ≈ 200s)
|
||||
DOWNTIME_THRESHOLD_S = 600 # 速度为0持续超过该秒数判定停机(10min)
|
||||
|
||||
|
||||
async def place_at_position(db: AsyncSession, plan: ProductionPlan, position: str):
|
||||
"""把计划放到任意入口位置;位置唯一占用。上卷鞍座单独触发生产联动。"""
|
||||
if position not in ENTRY_POSITIONS:
|
||||
raise ValueError("未知入口位置")
|
||||
if position == SADDLE_NAME:
|
||||
await move_to_saddle(db, plan)
|
||||
return
|
||||
# 普通位置:清除同位置上的其它计划,移出鞍座(若在)
|
||||
res = await db.execute(
|
||||
select(ProductionPlan).where(ProductionPlan.position == position, ProductionPlan.id != plan.id)
|
||||
)
|
||||
for o in res.scalars():
|
||||
o.position = None
|
||||
plan.on_saddle = 0
|
||||
plan.run_started_at = None
|
||||
plan.run_speed = 0
|
||||
plan.run_length_m = 0
|
||||
plan.position = position
|
||||
if plan.status == "producing":
|
||||
plan.status = "online"
|
||||
await ensure_online(db)
|
||||
|
||||
|
||||
def _shift_of(dt: datetime) -> str:
|
||||
return "甲" if 8 <= dt.hour < 20 else "乙"
|
||||
|
||||
@@ -60,6 +93,7 @@ async def move_to_saddle(db: AsyncSession, plan: ProductionPlan):
|
||||
if occupied and occupied.id != plan.id:
|
||||
raise ValueError("上卷鞍座已被占用,请等待当前钢卷生产完成")
|
||||
plan.on_saddle = 1
|
||||
plan.position = SADDLE_NAME
|
||||
plan.saddle_at = datetime.now()
|
||||
plan.run_started_at = None
|
||||
plan.run_speed = 0
|
||||
@@ -86,6 +120,7 @@ async def _produce(db: AsyncSession, plan: ProductionPlan):
|
||||
plan.status = "produced"
|
||||
plan.produced_at = now
|
||||
plan.on_saddle = 0
|
||||
plan.position = None
|
||||
plan.run_speed = 0
|
||||
plan.run_length_m = TARGET_LENGTH_M
|
||||
|
||||
|
||||
Reference in New Issue
Block a user