JhHardwareWRS_BackPoint/app/services/mold_locks.py
2026-06-24 15:19:14 +08:00

288 lines
10 KiB
Python

from contextlib import contextmanager
import hashlib
from fastapi import HTTPException
from sqlalchemy import select, text
from sqlalchemy.orm import Session, selectinload
from app.models import (
MoldLockFeedback,
Personnel,
Product,
SessionStatus,
WorkSession,
WorkSessionDevice,
)
from app.schemas import MoldLockFeedbackOut, MonitorLockOut
from app.services.cleaning import is_cleaning_product
from app.services.common import round2
from app.services.display_names import point_mold_display_name
from app.services.metrics import minutes_between
from app.services.misc_work import is_misc_product
from app.services.multi_person import is_multi_person_product
from app.timezone import now
FEEDBACK_STATUS_PENDING = "pending"
FEEDBACK_STATUS_HANDLED = "handled"
MOLD_LOCK_PREFIX = "jh_mold_lock"
def mold_display_name(
point_name: str,
mold_name: str,
process_name: str | None,
stamping_method: str | None = None,
) -> str:
return point_mold_display_name(point_name, mold_name, process_name, stamping_method)
def is_lockable_product(product: Product | None) -> bool:
return (
bool(product)
and not is_cleaning_product(product)
and not is_misc_product(product)
and not is_multi_person_product(product)
)
def _mutex_key(point_name: str, mold_name: str, process_name: str) -> str:
raw = f"{point_name}\0{mold_name}\0{process_name}"
digest = hashlib.sha1(raw.encode("utf-8")).hexdigest()
return f"{MOLD_LOCK_PREFIX}:{digest}"
@contextmanager
def mold_lock_mutex(
db: Session,
point_name: str,
mold_name: str,
process_name: str,
*,
timeout_seconds: int = 5,
):
bind = db.get_bind()
if bind.dialect.name != "mysql":
yield
return
key = _mutex_key(point_name, mold_name, process_name)
acquired = db.scalar(
text("select get_lock(:lock_key, :timeout_seconds)"),
{"lock_key": key, "timeout_seconds": timeout_seconds},
)
if acquired != 1:
raise HTTPException(status_code=409, detail="模具占用检查繁忙,请稍后重试")
try:
yield
finally:
db.execute(text("select release_lock(:lock_key)"), {"lock_key": key})
def _product_for_mold(db: Session, point_name: str, mold_name: str, process_name: str) -> Product | None:
return db.scalar(
select(Product)
.where(
Product.attendance_point_name == point_name,
Product.product_name == mold_name,
Product.process_name == process_name,
Product.device_no == "",
)
.order_by(Product.project_no.asc())
)
def _ordered_devices(session: WorkSession) -> list[WorkSessionDevice]:
return sorted(
session.devices,
key=lambda device: (int(device.sort_order or 0), device.scanned_at, int(device.id or 0)),
)
def current_unreleased_device(session: WorkSession) -> WorkSessionDevice | None:
devices = [device for device in _ordered_devices(session) if device.released_at is None]
return devices[-1] if devices else None
def _is_session_current_device(session: WorkSession, device: WorkSessionDevice) -> bool:
current = current_unreleased_device(session)
return bool(current and current.id == device.id)
def active_lock_for_mold(
db: Session,
point_name: str,
mold_name: str,
process_name: str,
) -> WorkSessionDevice | None:
rows = db.scalars(
select(WorkSessionDevice)
.options(
selectinload(WorkSessionDevice.session).selectinload(WorkSession.devices),
selectinload(WorkSessionDevice.session).selectinload(WorkSession.employee),
)
.join(WorkSession, WorkSession.id == WorkSessionDevice.session_id)
.where(
WorkSession.status.in_([SessionStatus.active, SessionStatus.reporting]),
WorkSessionDevice.released_at.is_(None),
WorkSessionDevice.attendance_point_name == point_name,
WorkSessionDevice.device_no == mold_name,
WorkSessionDevice.process_name == process_name,
)
.order_by(WorkSessionDevice.scanned_at.desc(), WorkSessionDevice.id.desc())
).all()
for device in rows:
if _is_session_current_device(device.session, device):
return device
return None
def active_locks_for_points(db: Session, point_names: list[str]) -> list[WorkSessionDevice]:
rows = db.scalars(
select(WorkSessionDevice)
.options(
selectinload(WorkSessionDevice.session).selectinload(WorkSession.devices),
selectinload(WorkSessionDevice.session).selectinload(WorkSession.employee),
)
.join(WorkSession, WorkSession.id == WorkSessionDevice.session_id)
.where(
WorkSession.status.in_([SessionStatus.active, SessionStatus.reporting]),
WorkSessionDevice.released_at.is_(None),
WorkSessionDevice.attendance_point_name.in_(point_names),
)
.order_by(WorkSessionDevice.scanned_at.desc(), WorkSessionDevice.id.desc())
).all()
locks: list[WorkSessionDevice] = []
seen_sessions: set[int] = set()
for device in rows:
session_id = int(device.session_id)
if session_id in seen_sessions or not _is_session_current_device(device.session, device):
continue
product = _product_for_mold(db, device.attendance_point_name, device.device_no, device.process_name)
if not is_lockable_product(product):
continue
locks.append(device)
seen_sessions.add(session_id)
return locks
def release_session_locks(
session: WorkSession,
released_by: str | None,
reason: str,
released_at=None,
) -> None:
timestamp = released_at or now()
for device in session.devices:
if device.released_at is None:
device.released_at = timestamp
device.released_by = released_by
device.release_reason = reason
def ensure_mold_available(
db: Session,
user: Personnel,
point_name: str,
mold_name: str,
process_name: str,
product: Product | None,
) -> None:
if not is_lockable_product(product):
return
lock_process_name = str(process_name or (product.process_name if product else "") or "").strip()
lock = active_lock_for_mold(db, point_name, mold_name, lock_process_name)
if lock is None or lock.session.employee_phone == user.phone:
return
occupied_name = lock.session.employee.name if lock.session and lock.session.employee else ""
message = f"已经被{occupied_name or lock.session.employee_phone}占用,请联系管理员解除占用"
raise HTTPException(
status_code=423,
detail={
"code": "mold_occupied",
"message": message,
"lock_id": lock.id,
"attendance_point_name": point_name,
"mold_name": mold_name,
"process_name": process_name,
"mold_display_name": mold_display_name(point_name, mold_name, process_name, product.stamping_method if product else None),
"occupied_phone": lock.session.employee_phone,
"occupied_name": occupied_name,
},
)
def release_lock(
db: Session,
lock_id: int,
released_by: str,
reason: str = "管理员强制解除占用",
) -> WorkSessionDevice:
lock = db.scalar(
select(WorkSessionDevice)
.options(selectinload(WorkSessionDevice.session).selectinload(WorkSession.devices))
.where(WorkSessionDevice.id == lock_id)
)
if lock is None:
raise ValueError("占用记录不存在")
if lock.released_at is None:
lock.released_at = now()
lock.released_by = released_by
lock.release_reason = reason
mark_feedbacks_handled(db, lock.id, released_by)
return lock
def mark_feedbacks_handled(db: Session, lock_id: int, handled_by: str) -> None:
timestamp = now()
feedbacks = db.scalars(
select(MoldLockFeedback).where(
MoldLockFeedback.session_device_id == lock_id,
MoldLockFeedback.status == FEEDBACK_STATUS_PENDING,
)
).all()
for feedback in feedbacks:
feedback.status = FEEDBACK_STATUS_HANDLED
feedback.handled_at = timestamp
feedback.handled_by = handled_by
def lock_out(db: Session, lock: WorkSessionDevice) -> MonitorLockOut:
occupied_name = lock.session.employee.name if lock.session and lock.session.employee else ""
product = _product_for_mold(db, lock.attendance_point_name, lock.device_no, lock.process_name)
return MonitorLockOut(
id=lock.id,
attendance_point_name=lock.attendance_point_name,
mold_name=lock.device_no,
process_name=lock.process_name,
stamping_method=product.stamping_method if product else None,
mold_display_name=mold_display_name(lock.attendance_point_name, lock.device_no, lock.process_name, product.stamping_method if product else None),
occupied_phone=lock.session.employee_phone,
occupied_name=occupied_name,
occupied_start_at=lock.scanned_at,
occupied_minutes=round2(minutes_between(lock.scanned_at, now())),
)
def feedback_out(db: Session, feedback: MoldLockFeedback) -> MoldLockFeedbackOut:
product = _product_for_mold(db, feedback.attendance_point_name, feedback.mold_name, feedback.process_name)
return MoldLockFeedbackOut(
id=feedback.id,
attendance_point_name=feedback.attendance_point_name,
mold_name=feedback.mold_name,
process_name=feedback.process_name,
stamping_method=product.stamping_method if product else None,
mold_display_name=mold_display_name(feedback.attendance_point_name, feedback.mold_name, feedback.process_name, product.stamping_method if product else None),
lock_id=feedback.session_device_id,
reporter_phone=feedback.reporter_phone,
reporter_name=feedback.reporter.name if feedback.reporter else "",
occupied_phone=feedback.occupied_phone,
occupied_name=feedback.occupied_person.name if feedback.occupied_person else "",
feedback_at=feedback.created_at,
read_at=feedback.read_at,
handled_at=feedback.handled_at,
status=feedback.status,
status_name="已处理" if feedback.status == FEEDBACK_STATUS_HANDLED else "未处理",
can_release=feedback.status != FEEDBACK_STATUS_HANDLED,
)