import json import uuid from datetime import date from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession from app.core.deps import get_cra_site_scope, get_current_user, get_db_session, require_study_not_locked, require_api_permission from app.crud import ae as ae_crud from app.crud import audit as audit_crud from app.crud import member as member_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.schemas.ae import AECreate, AERead, AEUpdate router = APIRouter() async def _ensure_study_exists(db: AsyncSession, study_id: uuid.UUID): study = await study_crud.get(db, study_id) if not study: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在") return study async def _ensure_ae_visible(db: AsyncSession, study_id: uuid.UUID, ae: AERead | None): if not ae: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="AE 不存在") if ae.site_id: site = await site_crud.get_site(db, ae.site_id) if not site or site.study_id != study_id or not site.is_active: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="AE 不存在") if ae.subject_id: subject = await subject_crud.get_subject(db, ae.subject_id) if not subject or subject.study_id != study_id: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="AE 不存在") site = await site_crud.get_site(db, subject.site_id) if not site or not site.is_active: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="AE 不存在") def _is_overdue(ae: AERead) -> bool: return bool(ae.report_due_date and date.today() > ae.report_due_date and ae.status != "CLOSED") @router.post( "/", response_model=AERead, status_code=status.HTTP_201_CREATED, dependencies=[Depends(require_api_permission("POST:/risk-issues")), Depends(require_study_not_locked())], ) async def create_ae( study_id: uuid.UUID, ae_in: AECreate, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ) -> AERead: await _ensure_study_exists(db, study_id) cra_scope = await get_cra_site_scope(db, study_id, current_user) if cra_scope: if ae_in.site_id and ae_in.site_id not in cra_scope[0]: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足") if ae_in.subject_id: subj = await subject_crud.get_subject(db, ae_in.subject_id) if subj and subj.site_id not in cra_scope[0]: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足") if ae_in.onset_date and ae_in.onset_date > date.today(): raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="发生日期不能晚于今天") try: ae = await ae_crud.create_ae(db, study_id, ae_in, created_by=current_user.id) except ValueError as exc: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc await audit_crud.log_action( db, study_id=study_id, entity_type="ae", entity_id=ae.id, action="CREATE_AE", detail=f"AE {ae.id} 已创建", operator_id=current_user.id, operator_role=current_user.role, ) data = AERead.model_validate(ae) data.is_overdue = _is_overdue(data) return data @router.get( "/", response_model=list[AERead], dependencies=[Depends(require_api_permission("GET:/risk-issues"))], ) async def list_ae( study_id: uuid.UUID, status_filter: str | None = None, seriousness: str | None = None, site_id: uuid.UUID | None = None, subject_id: uuid.UUID | None = None, overdue: bool | None = None, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ) -> list[AERead]: await _ensure_study_exists(db, study_id) cra_scope = await get_cra_site_scope(db, study_id, current_user) site_ids = cra_scope[0] if cra_scope else None if site_id and site_ids is not None and site_id not in site_ids: return [] aes = await ae_crud.list_ae( db, study_id, status=status_filter, seriousness=seriousness, site_id=site_id, subject_id=subject_id, overdue=overdue, site_ids=site_ids, ) result: list[AERead] = [] for item in aes: obj = AERead.model_validate(item) obj.is_overdue = _is_overdue(obj) result.append(obj) return result @router.get( "/{ae_id}", response_model=AERead, dependencies=[Depends(require_api_permission("GET:/risk-issues/{id}"))], ) async def get_ae( study_id: uuid.UUID, ae_id: uuid.UUID, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ) -> AERead: await _ensure_study_exists(db, study_id) ae = await ae_crud.get_ae(db, ae_id) if not ae or ae.study_id != study_id: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="AE 不存在") data = AERead.model_validate(ae) data.is_overdue = _is_overdue(data) cra_scope = await get_cra_site_scope(db, study_id, current_user) if cra_scope: if data.site_id and data.site_id not in cra_scope[0]: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足") if data.subject_id: subj = await subject_crud.get_subject(db, data.subject_id) if subj and subj.site_id not in cra_scope[0]: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足") return data @router.patch( "/{ae_id}", response_model=AERead, dependencies=[Depends(require_api_permission("PATCH:/risk-issues/{id}")), Depends(require_study_not_locked())], ) async def update_ae( study_id: uuid.UUID, ae_id: uuid.UUID, ae_in: AEUpdate, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ) -> AERead: await _ensure_study_exists(db, study_id) if ae_in.onset_date and ae_in.onset_date > date.today(): raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="发生日期不能晚于今天") ae = await ae_crud.get_ae(db, ae_id) if not ae or ae.study_id != study_id: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="AE 不存在") await _ensure_ae_visible(db, study_id, AERead.model_validate(ae)) old_status = ae.status detail_before = { "event": ae.term, "onset_date": ae.onset_date, "seriousness": ae.seriousness, "status": ae.status, "description": ae.description, } updated = await ae_crud.update_ae(db, study_id, ae, ae_in) detail_after = { "event": updated.term, "onset_date": updated.onset_date, "seriousness": updated.seriousness, "status": updated.status, "description": updated.description, } action = "UPDATE_AE" if ae_in.status and ae_in.status != old_status: action = "AE_STATUS_CHANGE" await audit_crud.log_action( db, study_id=study_id, entity_type="ae", entity_id=ae_id, action=action, detail=json.dumps({"before": detail_before, "after": detail_after}, ensure_ascii=False, default=str), operator_id=current_user.id, operator_role=current_user.role, ) data = AERead.model_validate(updated) data.is_overdue = _is_overdue(data) return data @router.delete( "/{ae_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(require_api_permission("DELETE:/risk-issues/{id}")), Depends(require_study_not_locked())], ) async def delete_ae( study_id: uuid.UUID, ae_id: uuid.UUID, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ) -> None: await _ensure_study_exists(db, study_id) ae = await ae_crud.get_ae(db, ae_id) if not ae or ae.study_id != study_id: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="AE 不存在") await _ensure_ae_visible(db, study_id, AERead.model_validate(ae)) await ae_crud.delete_ae(db, ae) await audit_crud.log_action( db, study_id=study_id, entity_type="ae", entity_id=ae_id, action="DELETE_AE", detail=f"AE {ae_id} 已删除", operator_id=current_user.id, operator_role=current_user.role, )