import uuid from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession from app.core.deps import get_current_user, get_db_session, require_study_member, require_study_roles, require_study_not_locked from app.crud import audit as audit_crud from app.crud import site as site_crud from app.crud import subject as subject_crud from app.crud import study as study_crud from app.crud import visit as visit_crud from app.schemas.visit import VisitCreate, VisitRead, VisitUpdate router = APIRouter() async def _ensure_subject(db: AsyncSession, study_id: uuid.UUID, subject_id: uuid.UUID): subject = await subject_crud.get_subject(db, subject_id) if not subject or subject.study_id != study_id: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="参与者不存在") return subject async def _ensure_subject_active(db: AsyncSession, subject) -> None: if not subject or not subject.site_id: return site = await site_crud.get_site(db, subject.site_id) if not site or not site.is_active: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="中心已停用") @router.get( "/", response_model=list[VisitRead], dependencies=[Depends(require_study_member())], ) async def list_visits( study_id: uuid.UUID, subject_id: uuid.UUID, db: AsyncSession = Depends(get_db_session), ) -> list[VisitRead]: await _ensure_subject(db, study_id, subject_id) visits = await visit_crud.list_visits(db, subject_id) return list(visits) @router.post( "/", response_model=VisitRead, status_code=status.HTTP_201_CREATED, dependencies=[Depends(require_study_roles(["PM", "CRA"])), Depends(require_study_not_locked())], ) async def create_visit( study_id: uuid.UUID, subject_id: uuid.UUID, visit_in: VisitCreate, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ) -> VisitRead: subject = await _ensure_subject(db, study_id, subject_id) await _ensure_subject_active(db, subject) if visit_in.subject_id != subject_id: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="参与者不匹配") visit = await visit_crud.create_visit( db, study_id=study_id, visit_in=visit_in, subject=subject, visit_code=visit_in.visit_code, planned_date=visit_in.planned_date, ) if visit_in.visit_code == "V1" and visit_in.planned_date: study = await study_crud.get(db, study_id) if study: await visit_crud.create_followup_visits( db, study_id=study_id, subject=subject, base_date=visit_in.planned_date, visit_total=study.visit_total, visit_interval_days=study.visit_interval_days, window_start_offset=study.visit_window_start_offset, window_end_offset=study.visit_window_end_offset, ) await audit_crud.log_action( db, study_id=study_id, entity_type="visit", entity_id=visit.id, action="CREATE_VISIT", detail=f"访视 {visit.visit_code} 已创建", operator_id=current_user.id, operator_role=current_user.role, ) return visit @router.patch( "/{visit_id}", response_model=VisitRead, dependencies=[Depends(require_study_roles(["PM", "CRA"])), Depends(require_study_not_locked())], ) async def update_visit( study_id: uuid.UUID, subject_id: uuid.UUID, visit_id: uuid.UUID, visit_in: VisitUpdate, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ) -> VisitRead: subject = await _ensure_subject(db, study_id, subject_id) await _ensure_subject_active(db, subject) visit = await visit_crud.get_visit(db, visit_id) if not visit or visit.subject_id != subject_id or visit.study_id != study_id: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="访视不存在") if visit_in.status == "DONE" and not visit_in.actual_date: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="状态为已完成时必须填写实际日期") updated = await visit_crud.update_visit(db, visit, visit_in) detail = None if visit_in.status: detail = f"访视 {visit.visit_code} {visit.status} -> {visit_in.status}" await audit_crud.log_action( db, study_id=study_id, entity_type="visit", entity_id=visit_id, action="VISIT_STATUS_CHANGE" if detail else "UPDATE_VISIT", detail=detail or "访视已更新", operator_id=current_user.id, operator_role=current_user.role, ) return updated @router.delete( "/{visit_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(require_study_roles(["PM", "CRA"])), Depends(require_study_not_locked())], ) async def delete_visit( study_id: uuid.UUID, subject_id: uuid.UUID, visit_id: uuid.UUID, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ) -> None: subject = await _ensure_subject(db, study_id, subject_id) await _ensure_subject_active(db, subject) visit = await visit_crud.get_visit(db, visit_id) if not visit or visit.subject_id != subject_id or visit.study_id != study_id: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="访视不存在") await visit_crud.delete_visit(db, visit) await audit_crud.log_action( db, study_id=study_id, entity_type="visit", entity_id=visit_id, action="DELETE_VISIT", detail=f"访视 {visit_id} 已删除", operator_id=current_user.id, operator_role=current_user.role, )