diff --git a/backend/app/api/plan.py b/backend/app/api/plan.py index 2aa5a02..f9b8907 100644 --- a/backend/app/api/plan.py +++ b/backend/app/api/plan.py @@ -115,6 +115,18 @@ async def update_plan( return Response.ok(PlanOut.model_validate(plan)) +@router.delete("/{plan_id}", response_model=Response[dict]) +async def delete_plan(plan_id: int, 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="计划不存在") + if plan.status == "producing": + raise HTTPException(status_code=400, detail="生产中的计划不可删除") + await db.delete(plan) + return Response.ok({"deleted": plan_id}) + + @router.patch("/{plan_id}/confirm", response_model=Response[PlanOut]) async def confirm_plan(plan_id: int, db: AsyncSession = Depends(get_db), _ = Depends(get_current_user)): result = await db.execute(select(ProductionPlan).where(ProductionPlan.id == plan_id)) @@ -171,7 +183,10 @@ async def commit_producing(plan_id: int, db: AsyncSession = Depends(get_db), _ = plan = result.scalar_one_or_none() if not plan: raise HTTPException(status_code=404, detail="计划不存在") - await line_service.commit_plan(db, plan) + try: + await line_service.commit_plan(db, plan) + 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)) diff --git a/backend/app/services/line_service.py b/backend/app/services/line_service.py index e9ad8e0..ac4e739 100644 --- a/backend/app/services/line_service.py +++ b/backend/app/services/line_service.py @@ -51,9 +51,9 @@ async def place_at_position(db: AsyncSession, plan: ProductionPlan, position: st plan.run_speed = 0 plan.run_length_m = 0 plan.position = position - if plan.status == "producing": + # 移动到入口端即变「在线」(人工触发,非自动) + if plan.status != "produced": plan.status = "online" - await ensure_online(db) def _shift_of(dt: datetime) -> str: @@ -66,70 +66,40 @@ async def _saddle_plan(db: AsyncSession): async def ensure_online(db: AsyncSession): - """单卷在产:有卷在上卷鞍座/生产中时不保留在线队首;否则把最早 ready 置为唯一在线。""" - res = await db.execute( - select(ProductionPlan).where(ProductionPlan.status == "online", ProductionPlan.on_saddle != 1) - ) - online = list(res.scalars()) - - # 是否已有在产/在鞍座的卷 - res2 = await db.execute( - select(ProductionPlan).where( - (ProductionPlan.status == "producing") | (ProductionPlan.on_saddle == 1) - ) - ) - active = res2.scalars().first() is not None - - if active: - # 正在生产时,不再保留「在线」队首,全部回退 ready,等当前卷完成 - for p in online: - p.status = "ready" - return - - if len(online) > 1: - online.sort(key=lambda p: (p.plan_date or datetime.max, p.id)) - for p in online[1:]: - p.status = "ready" - online = online[:1] - if not online: - res = await db.execute( - select(ProductionPlan) - .where(ProductionPlan.status == "ready") - .order_by(asc(ProductionPlan.plan_date), asc(ProductionPlan.id)) - .limit(1) - ) - head = res.scalar_one_or_none() - if head: - head.status = "online" + """在线为人工触发:用户手动「移动到入口」才变在线,引擎不再自动上线。""" + return async def move_to_saddle(db: AsyncSession, plan: ProductionPlan): - """把计划移动到上卷鞍座:上卷即获得速度 → 生产中。""" + """把计划移动到上卷鞍座(预备生产位,尚未生产)。""" occupied = await _saddle_plan(db) if occupied and occupied.id != plan.id: - raise ValueError("上卷鞍座已被占用,请等待当前钢卷生产完成") - now = datetime.now() + raise ValueError("上卷鞍座已有预备卷,请先投入生产或移走") plan.on_saddle = 1 plan.position = SADDLE_NAME - plan.saddle_at = now + plan.saddle_at = datetime.now() + plan.run_started_at = None + plan.run_speed = 0 + plan.run_length_m = 0 if plan.status != "produced": - plan.run_started_at = now - plan.run_speed = SIM_SPEED_M_MIN - plan.run_length_m = 0 - plan.status = "producing" + plan.status = "online" # 在鞍座预备(待投入生产) await ensure_online(db) async def commit_plan(db: AsyncSession, plan: ProductionPlan): - """投入生产(兜底):鞍座计划置为生产中。""" - if plan.on_saddle != 1: - await move_to_saddle(db, plan) - return - if plan.run_started_at is None: - plan.run_started_at = datetime.now() + """投入生产:鞍座预备卷进入生产中,离开鞍座进入产线(物料跟踪)。""" + res = await db.execute(select(ProductionPlan).where(ProductionPlan.status == "producing")) + if res.scalars().first() is not None: + raise ValueError("产线已有钢卷在生产,请等待当前卷完成") + now = datetime.now() + plan.on_saddle = 0 # 离开上卷鞍座 + plan.position = None + plan.saddle_at = None + plan.run_started_at = now plan.run_speed = SIM_SPEED_M_MIN + plan.run_length_m = 0 plan.status = "producing" - await ensure_online(db) + await ensure_online(db) # 鞍座空出 → 队首自动上线 async def _produce(db: AsyncSession, plan: ProductionPlan): @@ -183,17 +153,17 @@ async def _produce(db: AsyncSession, plan: ProductionPlan): logger.info(f"生产完成并产生实绩: {plan.cold_coil_no or plan.plan_no}") -async def advance_saddle(db: AsyncSession): - """推进鞍座计划:累计带头长度到 2000m → 生产完成。""" - plan = await _saddle_plan(db) - if not plan: +async def advance_production(db: AsyncSession): + """推进产线在产卷(已离开鞍座):累计带头长度到 2000m → 生产完成。""" + res = await db.execute(select(ProductionPlan).where(ProductionPlan.status == "producing")) + plan = res.scalars().first() + if not plan or not plan.run_started_at: return now = datetime.now() - if plan.status == "producing" and plan.run_started_at: - elapsed = (now - plan.run_started_at).total_seconds() - plan.run_length_m = min(TARGET_LENGTH_M, (plan.run_speed or 0) / 60.0 * elapsed) - if plan.run_length_m >= TARGET_LENGTH_M: - await _produce(db, plan) + elapsed = (now - plan.run_started_at).total_seconds() + plan.run_length_m = min(TARGET_LENGTH_M, (plan.run_speed or 0) / 60.0 * elapsed) + if plan.run_length_m >= TARGET_LENGTH_M: + await _produce(db, plan) async def _get_line_state(db: AsyncSession) -> LineState: @@ -243,9 +213,8 @@ async def detect_downtime(db: AsyncSession): async def tick(db: AsyncSession): - """引擎单步:上线 + 推进鞍座 + 停机检测。""" - await ensure_online(db) - await advance_saddle(db) + """引擎单步:推进在产卷 + 停机检测(在线为人工触发,不自动上线)。""" + await advance_production(db) await detect_downtime(db) diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index a9cc5e1..287d8e7 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -19,6 +19,7 @@ export const updateProductionRecord = (id, data) => request.put(`/production/${i export const getPlans = params => request.get('/plan/', { params }) export const createPlan = data => request.post('/plan/', data) export const updatePlan = (id, data) => request.put(`/plan/${id}`, data) +export const deletePlan = id => request.delete(`/plan/${id}`) export const confirmPlan = id => request.patch(`/plan/${id}/confirm`) export const startProducing = id => request.patch(`/plan/${id}/start`) // 移动到上卷鞍座(兼容) export const moveToSaddle = id => request.patch(`/plan/${id}/start`) diff --git a/frontend/src/views/EntryTracking.vue b/frontend/src/views/EntryTracking.vue index f4cc164..5a6dea0 100644 --- a/frontend/src/views/EntryTracking.vue +++ b/frontend/src/views/EntryTracking.vue @@ -5,9 +5,9 @@
| 冷卷号 | 钢种 | 厚度 | 宽度 | 分卷 | 下达时间 | 操作 | {{ statusLabel(row.status) }} | 编辑 - 移动 + 删除 |
|---|---|---|---|---|---|---|