from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile, status from fastapi.responses import Response from sqlalchemy import func, or_, select from sqlalchemy.orm import Session, selectinload from app.database import get_db from app.deps import require_roles from app.models import PersonAttendancePoint, PersonRole, Personnel, Role from app.schemas import ImportResult, PageResponse, PersonnelCreate, PersonnelOut from app.services.common import ROLE_NAMES from app.services.attendance_points import accessible_point_names, set_person_attendance_points from app.services.excel_export import export_people from app.services.excel_import import ProductImportValidationError, import_people from app.services.serializers import personnel_out, personnel_summary_out router = APIRouter(prefix="/api/people", tags=["people"]) @router.get("", response_model=PageResponse) def list_people( keyword: str = "", page: int = Query(1, ge=1), page_size: int = Query(10, ge=1, le=100), user: Personnel = Depends(require_roles(Role.admin, Role.manager)), db: Session = Depends(get_db), ) -> PageResponse: point_names = accessible_point_names(db, user) query = ( select(Personnel) .options(selectinload(Personnel.roles), selectinload(Personnel.attendance_points).selectinload(PersonAttendancePoint.attendance_point)) .where(Personnel.roles.any()) .order_by(Personnel.created_at.desc()) ) if user.role == Role.admin: query = query.where( Personnel.roles.any(PersonRole.role == Role.worker), Personnel.attendance_points.any(PersonAttendancePoint.attendance_point_name.in_(point_names)), ) if keyword: like = f"%{keyword}%" matched_roles = [role for role, name in ROLE_NAMES.items() if keyword in name or keyword in role] query = query.where( or_( Personnel.phone.like(like), Personnel.name.like(like), Personnel.roles.any(PersonRole.role.in_(matched_roles)), ) ) safe_page = max(1, page) safe_page_size = min(100, max(1, page_size)) total = db.scalar(select(func.count()).select_from(query.order_by(None).subquery())) or 0 rows = db.scalars(query.limit(safe_page_size).offset((safe_page - 1) * safe_page_size)).all() return PageResponse( page=safe_page, page_size=safe_page_size, total=total, total_pages=max(1, (total + safe_page_size - 1) // safe_page_size), rows=[personnel_summary_out(row) for row in rows], ) @router.post("", response_model=PersonnelOut) def save_person( payload: PersonnelCreate, user: Personnel = Depends(require_roles(Role.admin, Role.manager)), db: Session = Depends(get_db), ) -> PersonnelOut: if user.role == Role.admin and payload.role != Role.worker: raise HTTPException(status_code=403, detail="管理员只能新增或导入冲压工人") point_names = payload.attendance_point_names or accessible_point_names(db, user) if user.role == Role.admin: allowed = set(accessible_point_names(db, user)) denied = [name for name in point_names if name not in allowed] if denied: raise HTTPException(status_code=403, detail=f"无该考勤点人员维护权限:{'、'.join(denied)}") phone = str(payload.phone or "").strip() original_phone = str(payload.original_phone or "").strip() if original_phone and original_phone != phone: raise HTTPException(status_code=400, detail="编辑人员不能修改手机号") person = db.get(Personnel, phone) editing_existing = bool(original_phone) active_role_count = db.scalar(select(func.count()).select_from(PersonRole).where(PersonRole.phone == phone)) or 0 if person is not None and not editing_existing and active_role_count > 0: raise HTTPException(status_code=409, detail="该手机号已存在,不能重复添加") if person is None: person = Personnel(phone=phone, name=payload.name) db.add(person) db.flush() else: person.name = payload.name person.is_temporary = False person.temporary_expires_at = None role = db.get(PersonRole, {"phone": phone, "role": payload.role}) if role is None: role = PersonRole(phone=phone, role=payload.role) db.add(role) try: set_person_attendance_points(db, phone, point_names) except ValueError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc db.commit() person = db.scalar( select(Personnel) .options(selectinload(Personnel.roles), selectinload(Personnel.attendance_points).selectinload(PersonAttendancePoint.attendance_point)) .where(Personnel.phone == phone) ) if person is None: raise HTTPException(status_code=500, detail="人员保存失败") return personnel_out(person, payload.role) @router.delete("/{phone}", status_code=204) def delete_person( phone: str, role: Role | None = None, user: Personnel = Depends(require_roles(Role.admin, Role.manager)), db: Session = Depends(get_db), ) -> None: person = db.get(Personnel, phone) if person is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="人员不存在") if user.role == Role.admin: point_names = accessible_point_names(db, user) if not any(item.attendance_point_name in point_names for item in person.attendance_points): raise HTTPException(status_code=403, detail="无该考勤点人员维护权限") if role is not None and role != Role.worker: raise HTTPException(status_code=403, detail="管理员只能删除冲压工人") if role is None: db.query(PersonRole).filter(PersonRole.phone == phone).delete() else: person_role = db.get(PersonRole, {"phone": phone, "role": role}) if person_role is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="角色不存在") db.delete(person_role) db.commit() @router.post("/import", response_model=ImportResult) async def import_people_excel( file: UploadFile = File(...), user: Personnel = Depends(require_roles(Role.admin, Role.manager)), db: Session = Depends(get_db), ) -> ImportResult: content = await file.read() try: result = import_people(db, content, user=user) except ProductImportValidationError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc return ImportResult(**result.as_dict()) @router.get("/export") def export_people_excel( user: Personnel = Depends(require_roles(Role.admin, Role.manager)), db: Session = Depends(get_db), ) -> Response: content = export_people(db, user=user) return Response( content=content, media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", headers={"Content-Disposition": 'attachment; filename="people.xlsx"'}, )