Files
pickling-mes/backend/app/api/plan.py

234 lines
9.6 KiB
Python
Raw Normal View History

from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, desc
from typing import Optional
from datetime import datetime
from app.database import get_db
from app.models.plan import ProductionPlan
from app.schemas.plan import PlanCreate, PlanUpdate, PlanOut, PlanTemplate
from app.schemas.common import Response, PageResponse
from app.services.auth_service import get_current_user
from app.services import line_service
router = APIRouter()
def _parse_dt(s):
if not s:
return None
try:
return datetime.fromisoformat(s.replace('Z', ''))
except Exception:
return None
TEMPLATE_FIELDS = (
"steel_grade", "incoming_thickness", "product_thickness",
"deviation_upper", "deviation_lower",
"incoming_width", "product_width",
"packaging_req", "trim_req", "rolling_mode",
"coil_diameter", "split_count", "next_process",
"incoming_weight", "incoming_od", "split_weights",
)
@router.get("/", response_model=Response[PageResponse[PlanOut]])
async def list_plans(
page: int = 1,
page_size: int = 20,
status: Optional[str] = None,
start_date: Optional[str] = Query(None),
end_date: Optional[str] = Query(None),
db: AsyncSession = Depends(get_db),
_ = Depends(get_current_user),
):
# 拉取前保证队首自动上线(鞍座推进/停机检测由 /saddle 与后台引擎负责)
await line_service.ensure_online(db)
query = select(ProductionPlan).order_by(desc(ProductionPlan.plan_date))
if status:
query = query.where(ProductionPlan.status == status)
_sd = _parse_dt(start_date)
if _sd:
query = query.where(ProductionPlan.plan_date >= _sd)
_ed = _parse_dt(end_date)
if _ed:
query = query.where(ProductionPlan.plan_date <= _ed)
total = (await db.execute(select(func.count()).select_from(query.subquery()))).scalar()
result = await db.execute(query.offset((page - 1) * page_size).limit(page_size))
items = [PlanOut.model_validate(p) for p in result.scalars()]
return Response.ok(PageResponse(total=total, page=page, page_size=page_size, items=items))
@router.get("/last-template", response_model=Response[PlanTemplate])
async def get_last_template(db: AsyncSession = Depends(get_db), _ = Depends(get_current_user)):
"""最近一条计划的工艺字段(不含计划号/卷号/时间),用于新增时回填。"""
result = await db.execute(select(ProductionPlan).order_by(desc(ProductionPlan.created_at)).limit(1))
p = result.scalar_one_or_none()
if not p:
return Response.ok(PlanTemplate())
return Response.ok(PlanTemplate(**{k: getattr(p, k, None) for k in TEMPLATE_FIELDS}))
@router.post("/", response_model=Response[PlanOut])
async def create_plan(
body: PlanCreate,
db: AsyncSession = Depends(get_db),
current_user = Depends(get_current_user),
):
existing = await db.execute(select(ProductionPlan).where(ProductionPlan.plan_no == body.plan_no))
if existing.scalar_one_or_none():
raise HTTPException(status_code=400, detail="计划号已存在")
plan = ProductionPlan(**body.model_dump(), created_by=current_user.username)
db.add(plan)
await db.flush()
# 录入即准备好;若当前无在线计划,则队首自动上线
await line_service.ensure_online(db)
await db.refresh(plan)
return Response.ok(PlanOut.model_validate(plan))
@router.get("/{plan_id}", response_model=Response[PlanOut])
async def get_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="计划不存在")
return Response.ok(PlanOut.model_validate(plan))
@router.put("/{plan_id}", response_model=Response[PlanOut])
async def update_plan(
plan_id: int,
body: PlanUpdate,
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="计划不存在")
for k, v in body.model_dump(exclude_none=True).items():
setattr(plan, k, v)
await db.flush()
return Response.ok(PlanOut.model_validate(plan))
@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))
plan = result.scalar_one_or_none()
if not plan:
raise HTTPException(status_code=404, detail="计划不存在")
plan.status = "online"
await db.flush()
return Response.ok(PlanOut.model_validate(plan))
@router.patch("/{plan_id}/start", response_model=Response[PlanOut])
async def move_to_saddle(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="计划不存在")
try:
await line_service.move_to_saddle(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))
@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)):
"""投入生产:把鞍座上的计划置为生产中(兜底未实时变化的数据)。"""
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="计划不存在")
await line_service.commit_plan(db, plan)
await db.flush()
await db.refresh(plan)
return Response.ok(PlanOut.model_validate(plan))
@router.get("/saddle/current", response_model=Response[Optional[PlanOut]])
async def get_saddle(db: AsyncSession = Depends(get_db), _ = Depends(get_current_user)):
"""上卷鞍座当前计划(含实时速度/已生产长度),并推进联动。"""
await line_service.tick(db)
res = await db.execute(select(ProductionPlan).where(ProductionPlan.on_saddle == 1))
plan = res.scalars().first()
return Response.ok(PlanOut.model_validate(plan) if plan else None)
@router.post("/seed", response_model=Response[dict])
async def seed_plans(count: int = 50, db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user)):
"""批量插入准备好的计划(轧制力模式),用于演示联动。"""
import random
from datetime import timedelta
res = await db.execute(select(func.count()).select_from(ProductionPlan))
base = (res.scalar() or 0)
grades = ["QStE340TM", "SPHC", "SAPH440", "B510L", "QTGLG-2019"]
now = datetime.now()
created = 0
for i in range(count):
seq = base + i + 1
it = round(random.uniform(2.0, 5.0), 2)
pt = round(it - random.uniform(0.0, 0.1), 2)
iw = random.choice([1000, 1050, 1100, 1150, 1200, 1250])
wt = round(random.uniform(15.0, 26.0), 3)
plan = ProductionPlan(
plan_no=f"PL{now:%Y%m%d}{seq:04d}",
plan_date=now + timedelta(minutes=i),
status="ready",
cold_coil_no=f"C{now:%y%m%d}{seq:04d}",
hot_coil_no=f"H{now:%y%m%d}{seq:04d}",
steel_grade=random.choice(grades),
incoming_thickness=it,
product_thickness=pt,
deviation_upper=0.05,
deviation_lower=-0.05,
incoming_width=iw,
product_width=iw - random.choice([0, 4, 6]),
packaging_req=random.choice(["裸包", "筒包"]),
trim_req=random.choice(["切边", "不切边"]),
rolling_mode="轧制力模式",
coil_diameter=random.choice([1450, 1500, 1550]),
split_count=1,
next_process="冷轧",
incoming_weight=wt,
incoming_od=random.choice([1400, 1450, 1500]),
split_weights=[wt],
created_by=current_user.username,
)
db.add(plan)
created += 1
await db.flush()
await line_service.ensure_online(db)
return Response.ok({"created": created})