"""运营支撑 API(洁净室、进出记录、仓储设施、储位、展厅预约)""" from datetime import datetime from typing import Any, Optional from fastapi import APIRouter, Depends, Query from pydantic import BaseModel, ConfigDict, Field from sqlalchemy import or_ from sqlalchemy.orm import Session from app.core.data_scope import apply_data_scope from app.core.database import get_db from app.core.menu_guard import require_admin_or_super_admin from app.core.response import error_response, success_response from app.crud import clean_room as clean_room_svc from app.crud import exhibition_appointment as exhibition_svc from app.crud import storage_facility as storage_facility_svc from app.crud import storage_occupancy as occupancy_svc from app.core.utils import generate_uuid from app.models.models import BizCleanRoom, BizCleanRoomAccessLog, BizCleanRoomUvDisinfectLog, BizExhibitionAppointment, BizStorageFacility, BizStorageFacilitySlot, SysUser router = APIRouter(prefix="/web/admin", tags=["运营支撑"]) def _get_tenant_no(db: Session, user: dict) -> str: user_id = user.get("user_id") if not user_id: raise PermissionError("用户身份信息缺失,无法确定租户") sys_user = db.query(SysUser).filter(SysUser.id == user_id).first() if not sys_user or not sys_user.tenant_no: raise PermissionError("用户租户信息缺失") return sys_user.tenant_no # --------------------------------------------------------------------------- # 洁净室 # --------------------------------------------------------------------------- class CleanRoomCreateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) room_code: str = Field(..., alias="roomCode") room_name: str = Field(..., alias="roomName") room_grade: Optional[str] = Field(None, alias="roomGrade") room_status: Optional[str] = Field("zc", alias="roomStatus") area_sqm: Optional[float] = Field(None, alias="areaSqm") room_image: Optional[str] = Field(None, alias="roomImage") order_num: Optional[int] = Field(0, alias="orderNum") remark: Optional[str] = None class CleanRoomUpdateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) room_code: Optional[str] = Field(None, alias="roomCode") room_name: Optional[str] = Field(None, alias="roomName") room_grade: Optional[str] = Field(None, alias="roomGrade") room_status: Optional[str] = Field(None, alias="roomStatus") area_sqm: Optional[float] = Field(None, alias="areaSqm") room_image: Optional[str] = Field(None, alias="roomImage") order_num: Optional[int] = Field(None, alias="orderNum") remark: Optional[str] = None @router.get("/clean-rooms") async def list_clean_rooms( page: int = Query(1, ge=1), pageSize: int = Query(10, ge=1, le=100, alias="pageSize"), keyword: Optional[str] = None, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) query = db.query(BizCleanRoom).filter( BizCleanRoom.del_flag == 0, BizCleanRoom.tenant_no == tenant_no, ) kw = (keyword or "").strip() if kw: like = f"%{kw}%" query = query.filter(or_(BizCleanRoom.room_code.like(like), BizCleanRoom.room_name.like(like))) query = apply_data_scope(query, BizCleanRoom, user, db, tenant_no) total = query.count() items = query.order_by(BizCleanRoom.create_time.desc()).offset((page - 1) * pageSize).limit(pageSize).all() result_list = [clean_room_svc._row_to_dict(r) for r in items] tp = clean_room_svc.total_pages(total, pageSize) return success_response({"list": result_list, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp}) @router.post("/clean-rooms") async def create_clean_room( body: CleanRoomCreateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) obj = clean_room_svc.create_clean_room( db, room_code=body.room_code, room_name=body.room_name, room_grade=body.room_grade, room_status=body.room_status or "zc", area_sqm=body.area_sqm, room_image=body.room_image, tenant_no=tenant_no, order_num=body.order_num, remark=body.remark, user_id=user.get("user_id"), ) return success_response(clean_room_svc._row_to_dict(obj), message="创建成功") except ValueError as e: return error_response(400, str(e)) @router.put("/clean-rooms/{room_id}") async def update_clean_room( room_id: str, body: CleanRoomUpdateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False) obj = clean_room_svc.update_clean_room( db, room_id, room_code=payload.get("room_code"), room_name=payload.get("room_name"), room_grade=payload.get("room_grade"), room_status=payload.get("room_status"), area_sqm=payload.get("area_sqm"), room_image=payload.get("room_image"), order_num=payload.get("order_num"), remark=payload.get("remark"), user_id=user.get("user_id"), tenant_no=tenant_no, ) return success_response(clean_room_svc._row_to_dict(obj), message="更新成功") except ValueError as e: return error_response(400, str(e)) @router.delete("/clean-rooms/{room_id}") async def delete_clean_room( room_id: str, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) clean_room_svc.soft_delete_clean_room(db, room_id, tenant_no=tenant_no) return success_response(None, message="删除成功") except ValueError as e: return error_response(400, str(e)) # --------------------------------------------------------------------------- # 进出记录 # --------------------------------------------------------------------------- class AccessLogCreateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) clean_room_id: str = Field(..., alias="cleanRoomId") person_name: Optional[str] = Field(None, alias="personName") dept_name: Optional[str] = Field(None, alias="deptName") enter_time: Optional[str] = Field(None, alias="enterTime") purpose: Optional[str] = None leave_time: Optional[str] = Field(None, alias="leaveTime") body_temp: Optional[str] = Field(None, alias="bodyTemp") order_num: Optional[int] = Field(0, alias="orderNum") remark: Optional[str] = None class AccessLogUpdateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) person_name: Optional[str] = Field(None, alias="personName") dept_name: Optional[str] = Field(None, alias="deptName") enter_time: Optional[str] = Field(None, alias="enterTime") purpose: Optional[str] = None leave_time: Optional[str] = Field(None, alias="leaveTime") body_temp: Optional[str] = Field(None, alias="bodyTemp") order_num: Optional[int] = Field(None, alias="orderNum") remark: Optional[str] = None def _parse_optional_dt(value: Optional[str]) -> Optional[datetime]: if not value: return None return datetime.fromisoformat(value) @router.get("/clean-room-access-logs") async def list_access_logs( page: int = Query(1, ge=1), pageSize: int = Query(10, ge=1, le=100, alias="pageSize"), cleanRoomId: str = Query(..., alias="cleanRoomId"), keyword: Optional[str] = None, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) query = db.query(BizCleanRoomAccessLog).filter( BizCleanRoomAccessLog.del_flag == 0, BizCleanRoomAccessLog.tenant_no == tenant_no, BizCleanRoomAccessLog.clean_room_id == cleanRoomId, ) if keyword: kw = f"%{keyword}%" query = query.filter( BizCleanRoomAccessLog.person_name.like(kw) | BizCleanRoomAccessLog.dept_name.like(kw) ) query = apply_data_scope(query, BizCleanRoomAccessLog, user, db, tenant_no) total = query.count() items = query.order_by(BizCleanRoomAccessLog.create_time.desc()).offset((page - 1) * pageSize).limit(pageSize).all() result_list = [clean_room_svc._access_log_to_dict(r) for r in items] tp = clean_room_svc.total_pages(total, pageSize) return success_response({"list": result_list, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp}) @router.post("/clean-room-access-logs") async def create_access_log( body: AccessLogCreateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) obj = clean_room_svc.create_access_log( db, clean_room_id=body.clean_room_id, person_name=body.person_name, dept_name=body.dept_name, enter_time=_parse_optional_dt(body.enter_time), purpose=body.purpose, leave_time=_parse_optional_dt(body.leave_time), body_temp=body.body_temp, tenant_no=tenant_no, order_num=body.order_num, remark=body.remark, user_id_param=user.get("user_id"), ) return success_response(clean_room_svc._access_log_to_dict(obj), message="创建成功") except ValueError as e: return error_response(400, str(e)) @router.put("/clean-room-access-logs/{log_id}") async def update_access_log( log_id: str, body: AccessLogUpdateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False) obj = clean_room_svc.update_access_log( db, log_id, person_name=payload.get("person_name"), dept_name=payload.get("dept_name"), enter_time=_parse_optional_dt(payload.get("enter_time")) if "enter_time" in payload else None, purpose=payload.get("purpose"), leave_time=_parse_optional_dt(payload.get("leave_time")) if "leave_time" in payload else None, body_temp=payload.get("body_temp"), order_num=payload.get("order_num"), remark=payload.get("remark"), user_id_param=user.get("user_id"), tenant_no=tenant_no, ) return success_response(clean_room_svc._access_log_to_dict(obj), message="更新成功") except ValueError as e: return error_response(400, str(e)) @router.delete("/clean-room-access-logs/{log_id}") async def delete_access_log( log_id: str, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) clean_room_svc.soft_delete_access_log(db, log_id, tenant_no=tenant_no) return success_response(None, message="删除成功") except ValueError as e: return error_response(400, str(e)) # --------------------------------------------------------------------------- # 紫外线消毒记录 # --------------------------------------------------------------------------- class UvDisinfectLogCreateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) clean_room_id: str = Field(..., alias="cleanRoomId") operator_name: Optional[str] = Field(None, alias="operatorName") start_time: Optional[str] = Field(None, alias="startTime") end_time: Optional[str] = Field(None, alias="endTime") duration_minutes: Optional[int] = Field(None, alias="durationMinutes") order_num: Optional[int] = Field(0, alias="orderNum") remark: Optional[str] = None class UvDisinfectLogUpdateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) operator_name: Optional[str] = Field(None, alias="operatorName") start_time: Optional[str] = Field(None, alias="startTime") end_time: Optional[str] = Field(None, alias="endTime") duration_minutes: Optional[int] = Field(None, alias="durationMinutes") order_num: Optional[int] = Field(None, alias="orderNum") remark: Optional[str] = None @router.get("/clean-room-uv-disinfect-logs") async def list_uv_disinfect_logs( page: int = Query(1, ge=1), pageSize: int = Query(10, ge=1, le=100, alias="pageSize"), cleanRoomId: str = Query(..., alias="cleanRoomId"), keyword: Optional[str] = None, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) query = db.query(BizCleanRoomUvDisinfectLog).filter( BizCleanRoomUvDisinfectLog.del_flag == 0, BizCleanRoomUvDisinfectLog.tenant_no == tenant_no, BizCleanRoomUvDisinfectLog.clean_room_id == cleanRoomId, ) if keyword: kw = f"%{keyword}%" query = query.filter(BizCleanRoomUvDisinfectLog.operator_name.like(kw)) query = apply_data_scope(query, BizCleanRoomUvDisinfectLog, user, db, tenant_no) total = query.count() items = ( query.order_by(BizCleanRoomUvDisinfectLog.create_time.desc()) .offset((page - 1) * pageSize) .limit(pageSize) .all() ) result_list = [clean_room_svc._uv_disinfect_log_to_dict(r) for r in items] tp = clean_room_svc.total_pages(total, pageSize) return success_response({"list": result_list, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp}) @router.post("/clean-room-uv-disinfect-logs") async def create_uv_disinfect_log( body: UvDisinfectLogCreateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) obj = clean_room_svc.create_uv_disinfect_log( db, clean_room_id=body.clean_room_id, operator_name=body.operator_name, start_time=_parse_optional_dt(body.start_time), end_time=_parse_optional_dt(body.end_time), duration_minutes=body.duration_minutes, tenant_no=tenant_no, order_num=body.order_num, remark=body.remark, user_id_param=user.get("user_id"), ) return success_response(clean_room_svc._uv_disinfect_log_to_dict(obj), message="创建成功") except ValueError as e: return error_response(400, str(e)) @router.put("/clean-room-uv-disinfect-logs/{log_id}") async def update_uv_disinfect_log( log_id: str, body: UvDisinfectLogUpdateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False) obj = clean_room_svc.update_uv_disinfect_log( db, log_id, operator_name=payload.get("operator_name"), start_time=_parse_optional_dt(payload.get("start_time")) if "start_time" in payload else None, end_time=_parse_optional_dt(payload.get("end_time")) if "end_time" in payload else None, duration_minutes=payload.get("duration_minutes"), order_num=payload.get("order_num"), remark=payload.get("remark"), user_id_param=user.get("user_id"), tenant_no=tenant_no, clear_end_time="end_time" in payload and payload.get("end_time") is None, ) return success_response(clean_room_svc._uv_disinfect_log_to_dict(obj), message="更新成功") except ValueError as e: return error_response(400, str(e)) @router.delete("/clean-room-uv-disinfect-logs/{log_id}") async def delete_uv_disinfect_log( log_id: str, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) clean_room_svc.soft_delete_uv_disinfect_log(db, log_id, tenant_no=tenant_no) return success_response(None, message="删除成功") except ValueError as e: return error_response(400, str(e)) # --------------------------------------------------------------------------- # 仓储设施 # --------------------------------------------------------------------------- class StorageFacilityCreateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) parent_id: str = Field("0", alias="parentId") node_name: str = Field(..., alias="nodeName") node_code: str = Field("", alias="nodeCode", max_length=32) node_type: Optional[str] = Field(None, alias="nodeType") is_leaf: Optional[int] = Field(0, alias="isLeaf") order_num: Optional[int] = Field(0, alias="orderNum") remark: Optional[str] = None class StorageFacilityUpdateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) node_name: Optional[str] = Field(None, alias="nodeName") node_code: Optional[str] = Field(None, alias="nodeCode", max_length=32) node_type: Optional[str] = Field(None, alias="nodeType") is_leaf: Optional[int] = Field(None, alias="isLeaf") order_num: Optional[int] = Field(None, alias="orderNum") remark: Optional[str] = None class StorageFacilityBatchCreateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) parent_id: str = Field("0", alias="parentId") name_prefix: str = Field("", alias="namePrefix") name_suffix: str = Field("", alias="nameSuffix") start_num: int = Field(..., alias="startNum", ge=0) end_num: int = Field(..., alias="endNum", ge=0) pad_width: int = Field(0, alias="padWidth", ge=0, le=8) code_prefix: str = Field("", alias="codePrefix") code_suffix: str = Field("", alias="codeSuffix") node_type: Optional[str] = Field(None, alias="nodeType") is_leaf: Optional[int] = Field(0, alias="isLeaf") remark: Optional[str] = None @router.get("/storage-facilities") async def list_storage_facilities( keyword: Optional[str] = None, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) query = db.query(BizStorageFacility).filter( BizStorageFacility.del_flag == 0, BizStorageFacility.tenant_no == tenant_no, ) if keyword: like = f"%{keyword}%" query = query.filter( or_( BizStorageFacility.node_name.like(like), BizStorageFacility.node_code.like(like), ) ) query = apply_data_scope(query, BizStorageFacility, user, db, tenant_no) items = query.order_by( BizStorageFacility.order_num.asc(), BizStorageFacility.create_time.asc(), ).all() result_list = [storage_facility_svc._row_to_dict(r) for r in items] return success_response(result_list) @router.post("/storage-facilities") async def create_storage_facility( body: StorageFacilityCreateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) obj = storage_facility_svc.create_facility( db, parent_id=body.parent_id, node_name=body.node_name, node_code=body.node_code, node_type=body.node_type, is_leaf=body.is_leaf, tenant_no=tenant_no, order_num=body.order_num, remark=body.remark, user_id=user.get("user_id"), ) return success_response(storage_facility_svc._row_to_dict(obj), message="创建成功") except ValueError as e: return error_response(400, str(e)) @router.post("/storage-facilities/batch") async def batch_create_storage_facilities( body: StorageFacilityBatchCreateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) items = storage_facility_svc.batch_create_facilities( db, parent_id=body.parent_id, name_prefix=body.name_prefix, name_suffix=body.name_suffix, start_num=body.start_num, end_num=body.end_num, pad_width=body.pad_width, code_prefix=body.code_prefix, code_suffix=body.code_suffix, node_type=body.node_type, is_leaf=body.is_leaf or 0, tenant_no=tenant_no, remark=body.remark, user_id=user.get("user_id"), ) return success_response(items, message=f"批量创建 {len(items)} 个子节点成功") except ValueError as e: return error_response(400, str(e)) @router.put("/storage-facilities/{node_id}") async def update_storage_facility( node_id: str, body: StorageFacilityUpdateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False) obj = storage_facility_svc.update_facility( db, node_id, node_name=payload.get("node_name"), node_code=payload.get("node_code"), node_type=payload.get("node_type"), is_leaf=payload.get("is_leaf"), order_num=payload.get("order_num"), remark=payload.get("remark"), user_id=user.get("user_id"), tenant_no=tenant_no, ) return success_response(storage_facility_svc._row_to_dict(obj), message="更新成功") except ValueError as e: return error_response(400, str(e)) @router.delete("/storage-facilities/{node_id}") async def delete_storage_facility( node_id: str, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) storage_facility_svc.soft_delete_facility(db, node_id, tenant_no=tenant_no) return success_response(None, message="删除成功") except ValueError as e: return error_response(400, str(e)) # --------------------------------------------------------------------------- # 储位 # --------------------------------------------------------------------------- class StorageSlotCreateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) facility_node_id: str = Field(..., alias="facilityNodeId") slot_code: Optional[str] = Field(None, alias="slotCode") slot_status: Optional[str] = Field(None, alias="slotStatus") order_num: Optional[int] = Field(0, alias="orderNum") at_row_num: Optional[int] = Field(None, alias="atRowNum", ge=1) remark: Optional[str] = None class StorageSlotBatchCreateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) facility_node_id: str = Field(..., alias="facilityNodeId") rows: int = Field(..., ge=1, le=50) cols: int = Field(..., ge=1, le=50) class StorageSlotUpdateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) slot_code: Optional[str] = Field(None, alias="slotCode") slot_status: Optional[str] = Field(None, alias="slotStatus") order_num: Optional[int] = Field(None, alias="orderNum") remark: Optional[str] = None class StorageSlotClearBody(BaseModel): model_config = ConfigDict(populate_by_name=True) facility_node_id: str = Field(..., alias="facilityNodeId") class StorageSlotInsertRowBody(BaseModel): model_config = ConfigDict(populate_by_name=True) facility_node_id: str = Field(..., alias="facilityNodeId") after_row_num: int = Field(..., ge=0, alias="afterRowNum") cols: int = Field(..., ge=1) @router.get("/storage-facility-slots") async def list_storage_slots( facilityNodeId: str = Query(..., alias="facilityNodeId"), availableOnly: Optional[bool] = Query(None, alias="availableOnly"), user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) query = db.query(BizStorageFacilitySlot).filter( BizStorageFacilitySlot.del_flag == 0, BizStorageFacilitySlot.tenant_no == tenant_no, BizStorageFacilitySlot.facility_node_id == facilityNodeId, ) query = apply_data_scope(query, BizStorageFacilitySlot, user, db, tenant_no) items = query.order_by( BizStorageFacilitySlot.order_num.asc(), BizStorageFacilitySlot.create_time.asc(), ).all() facility_by_id = storage_facility_svc.load_facility_ancestor_map( db, tenant_no, facilityNodeId ) result_list = [storage_facility_svc._slot_to_dict(r, facility_by_id) for r in items] if availableOnly: # 仅空格场景无需 enrich 在库产品 result_list = [ row for row in result_list if (row.get("slotStatus") or "") == "空" or int(row.get("occupiedCount") or 0) == 0 ] else: # 占用格附带名称/类型(轻量批量,供库位管理货架展示) result_list = occupancy_svc.enrich_slots_with_stored_cell_products( db, tenant_no, result_list ) return success_response(result_list) @router.get("/storage-facility-slots/stats") async def list_storage_slot_stats( user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): """各叶子节点格子占用/总数统计(单次聚合,替代逐节点拉全量格子列表)。""" tenant_no = _get_tenant_no(db, user) query = db.query(BizStorageFacilitySlot).filter( BizStorageFacilitySlot.del_flag == 0, BizStorageFacilitySlot.tenant_no == tenant_no, ) query = apply_data_scope(query, BizStorageFacilitySlot, user, db, tenant_no) items = storage_facility_svc.aggregate_slot_stats_by_facility(query=query) return success_response(items) @router.get("/storage-facility-slots/select-options") async def list_storage_slot_select_options( keyword: Optional[str] = None, availableOnly: Optional[bool] = Query(None, alias="availableOnly"), includeSlotIds: Optional[str] = Query(None, alias="includeSlotIds"), page: int = Query(1, ge=1), pageSize: int = Query(20, ge=1, le=50, alias="pageSize"), user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) query = db.query(BizStorageFacilitySlot).filter( BizStorageFacilitySlot.del_flag == 0, BizStorageFacilitySlot.tenant_no == tenant_no, ) query = apply_data_scope(query, BizStorageFacilitySlot, user, db, tenant_no) include_ids = [ x.strip() for x in (includeSlotIds or "").split(",") if x.strip() ] items, total = storage_facility_svc.list_slot_select_options( db, query=query, tenant_no=tenant_no, keyword=keyword, available_only=bool(availableOnly), include_slot_ids=include_ids or None, page=page, page_size=pageSize, ) return success_response( { "list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": (total + pageSize - 1) // pageSize if pageSize else 0, } ) @router.post("/storage-facility-slots/batch") async def batch_create_storage_slots( body: StorageSlotBatchCreateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) items = storage_facility_svc.batch_create_slots( db, facility_node_id=body.facility_node_id, rows=body.rows, cols=body.cols, tenant_no=tenant_no, user_id=user.get("user_id"), ) return success_response(items, message=f"批量创建 {len(items)} 个格子成功") except ValueError as e: return error_response(400, str(e)) @router.post("/storage-facility-slots") async def create_storage_slot( body: StorageSlotCreateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) obj = storage_facility_svc.create_slot( db, facility_node_id=body.facility_node_id, slot_code=body.slot_code, slot_status=body.slot_status, tenant_no=tenant_no, order_num=body.order_num, remark=body.remark, user_id=user.get("user_id"), at_row_num=body.at_row_num, ) return success_response(storage_facility_svc._slot_to_dict_for_row(db, obj), message="创建成功") except ValueError as e: return error_response(400, str(e)) @router.put("/storage-facility-slots/{slot_id}") async def update_storage_slot( slot_id: str, body: StorageSlotUpdateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False) obj = storage_facility_svc.update_slot( db, slot_id, slot_code=payload.get("slot_code"), slot_status=payload.get("slot_status"), order_num=payload.get("order_num"), remark=payload.get("remark"), user_id=user.get("user_id"), tenant_no=tenant_no, ) return success_response(storage_facility_svc._slot_to_dict_for_row(db, obj), message="更新成功") except ValueError as e: return error_response(400, str(e)) @router.post("/storage-facility-slots/insert-row") async def insert_storage_slot_row( body: StorageSlotInsertRowBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) result = storage_facility_svc.insert_row_and_shift_slots( db, facility_node_id=body.facility_node_id, after_row_num=body.after_row_num, cols=body.cols, tenant_no=tenant_no, user_id=user.get("user_id"), ) return success_response(result, message="新增行成功") except ValueError as e: return error_response(400, str(e)) @router.post("/storage-facility-slots/clear") async def clear_storage_slots( body: StorageSlotClearBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) cleared_count = storage_facility_svc.clear_slots_by_facility_node( db, facility_node_id=body.facility_node_id, tenant_no=tenant_no, user_id=user.get("user_id"), ) return success_response( {"clearedCount": cleared_count}, message=f"已清空 {cleared_count} 个格子", ) except ValueError as e: return error_response(400, str(e)) @router.delete("/storage-facility-slots/{slot_id}") async def delete_storage_slot( slot_id: str, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) storage_facility_svc.soft_delete_slot( db, slot_id, tenant_no=tenant_no, user_id=user.get("user_id") ) return success_response(None, message="删除成功") except ValueError as e: return error_response(400, str(e)) # --------------------------------------------------------------------------- # 库位占用记录 # --------------------------------------------------------------------------- class OccupancyCreateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) slot_id: str = Field(..., alias="slotId") item_type: str = Field("other", alias="itemType") item_code: str = Field(..., alias="itemCode") item_name: str = Field(..., alias="itemName") item_source: str = Field("manual", alias="itemSource") remark: Optional[str] = None @router.get("/storage-slot-occupancies") async def list_occupancies( slotId: str = Query(..., alias="slotId"), occupancyStatus: Optional[str] = Query("stored", alias="occupancyStatus"), itemType: Optional[str] = Query(None, alias="itemType"), page: int = Query(1), pageSize: int = Query(20, alias="pageSize"), user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) result = occupancy_svc.list_occupancies( db, slot_id=slotId, occupancy_status=occupancyStatus, item_type=itemType, tenant_no=tenant_no, page=page, page_size=pageSize, ) return success_response(result) @router.get("/storage-slot-occupancies/history") async def list_occupancy_history( slotId: Optional[str] = Query(None, alias="slotId"), facilityNodeId: Optional[str] = Query(None, alias="facilityNodeId"), itemType: Optional[str] = Query(None, alias="itemType"), startDate: Optional[str] = Query(None, alias="startDate"), endDate: Optional[str] = Query(None, alias="endDate"), page: int = Query(1), pageSize: int = Query(20, alias="pageSize"), user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) result = occupancy_svc.list_occupancy_history( db, slot_id=slotId, facility_node_id=facilityNodeId, item_type=itemType, start_date=startDate, end_date=endDate, tenant_no=tenant_no, page=page, page_size=pageSize, ) return success_response(result) @router.post("/storage-slot-occupancies") async def create_occupancy( body: OccupancyCreateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) item_id = generate_uuid() obj = occupancy_svc.create_occupancy( db, slot_id=body.slot_id, item_type=body.item_type, item_id=item_id, item_code=body.item_code, item_name=body.item_name, item_source=body.item_source, tenant_no=tenant_no, user_id=user.get("user_id"), remark=body.remark, ) return success_response(occupancy_svc._row_to_dict(obj), message="入库成功") except ValueError as e: return error_response(400, str(e)) @router.put("/storage-slot-occupancies/{occupancy_id}/remove") async def remove_occupancy( occupancy_id: str, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: obj = occupancy_svc.remove_occupancy( db, occupancy_id, user_id=user.get("user_id"), ) return success_response(occupancy_svc._row_to_dict(obj), message="出库成功") except ValueError as e: return error_response(400, str(e)) # --------------------------------------------------------------------------- # 展厅预约 # --------------------------------------------------------------------------- class ExhibitionAppointmentCreateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) appointment_no: Optional[str] = Field(None, alias="appointmentNo") visit_org: Optional[str] = Field(None, alias="visitOrg") contact_name: Optional[str] = Field(None, alias="contactName") visit_date: Optional[str] = Field(None, alias="visitDate") visitor_count: Optional[int] = Field(None, alias="visitorCount") reception_user_id: Optional[str] = Field(None, alias="receptionUserId") order_num: Optional[int] = Field(0, alias="orderNum") remark: Optional[str] = None class ExhibitionAppointmentUpdateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) visit_org: Optional[str] = Field(None, alias="visitOrg") contact_name: Optional[str] = Field(None, alias="contactName") visit_date: Optional[str] = Field(None, alias="visitDate") visitor_count: Optional[int] = Field(None, alias="visitorCount") reception_user_id: Optional[str] = Field(None, alias="receptionUserId") order_num: Optional[int] = Field(None, alias="orderNum") remark: Optional[str] = None def _normalize_visit_date_bound(value: str, *, end_of_day: bool = False) -> str: val = (value or "").strip() if len(val) == 10: return f"{val} 23:59:59" if end_of_day else f"{val} 00:00:00" return val @router.get("/exhibition-appointments") async def list_exhibition_appointments( page: int = Query(1, ge=1), pageSize: int = Query(10, ge=1, le=100, alias="pageSize"), keyword: Optional[str] = None, visitDateStart: Optional[str] = Query(None, alias="visitDateStart"), visitDateEnd: Optional[str] = Query(None, alias="visitDateEnd"), user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) query = db.query(BizExhibitionAppointment).filter( BizExhibitionAppointment.del_flag == 0, BizExhibitionAppointment.tenant_no == tenant_no, ) kw = (keyword or "").strip() if kw: like = f"%{kw}%" query = query.filter( or_( BizExhibitionAppointment.appointment_no.like(like), BizExhibitionAppointment.contact_name.like(like), BizExhibitionAppointment.visit_org.like(like), ) ) if visitDateStart: query = query.filter( BizExhibitionAppointment.visit_date >= _normalize_visit_date_bound(visitDateStart) ) if visitDateEnd: query = query.filter( BizExhibitionAppointment.visit_date <= _normalize_visit_date_bound(visitDateEnd, end_of_day=True) ) query = apply_data_scope(query, BizExhibitionAppointment, user, db, tenant_no) total = query.count() items = query.order_by(BizExhibitionAppointment.create_time.desc()).offset((page - 1) * pageSize).limit(pageSize).all() result_list = [exhibition_svc._row_to_dict(r) for r in items] tp = exhibition_svc.total_pages(total, pageSize) return success_response({"list": result_list, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp}) @router.post("/exhibition-appointments") async def create_exhibition_appointment( body: ExhibitionAppointmentCreateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) obj = exhibition_svc.create_exhibition_appointment( db, appointment_no=body.appointment_no, visit_org=body.visit_org, contact_name=body.contact_name, visit_date=body.visit_date, visitor_count=body.visitor_count, reception_user_id=body.reception_user_id, tenant_no=tenant_no, order_num=body.order_num, remark=body.remark, user_id=user.get("user_id"), ) return success_response(exhibition_svc._row_to_dict(obj), message="创建成功") except ValueError as e: return error_response(400, str(e)) @router.put("/exhibition-appointments/{appointment_id}") async def update_exhibition_appointment( appointment_id: str, body: ExhibitionAppointmentUpdateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False) obj = exhibition_svc.update_exhibition_appointment( db, appointment_id, visit_org=payload.get("visit_org"), contact_name=payload.get("contact_name"), visit_date=payload.get("visit_date"), visitor_count=payload.get("visitor_count"), reception_user_id=payload.get("reception_user_id"), order_num=payload.get("order_num"), remark=payload.get("remark"), user_id=user.get("user_id"), ) return success_response(exhibition_svc._row_to_dict(obj), message="更新成功") except ValueError as e: return error_response(400, str(e)) @router.delete("/exhibition-appointments/{appointment_id}") async def delete_exhibition_appointment( appointment_id: str, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: exhibition_svc.soft_delete_exhibition_appointment(db, appointment_id) return success_response(None, message="删除成功") except ValueError as e: return error_response(400, str(e))