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, )