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:
2026-06-29 14:10:16 +08:00
parent 9fb3dcb785
commit 03709da1ca
8 changed files with 233 additions and 68 deletions

View File

@@ -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/min2000m ≈ 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