288 lines
10 KiB
Python
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,
|
|
)
|