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_current_user, get_db_session, require_study_member 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 study as study_crud from app.schemas.ae import AECreate, AERead, AEUpdate router = APIRouter() ALLOWED_CREATE_ROLES = {"PM", "CRA", "PV"} ALLOWED_UPDATE_ROLES = {"PM", "PV", "CRA"} 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 _get_member_role(db: AsyncSession, study_id: uuid.UUID, user_id: uuid.UUID) -> str | None: member = await member_crud.get_member(db, study_id, user_id) return member.role_in_study if member else None 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_study_member())], ) 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) if ae_in.onset_date and ae_in.onset_date > date.today(): raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="发生日期不能晚于今天") member_role = await _get_member_role(db, study_id, current_user.id) if current_user.role != "ADMIN" and member_role not in ALLOWED_CREATE_ROLES: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, 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_study_member())], ) 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), ) -> list[AERead]: await _ensure_study_exists(db, study_id) aes = await ae_crud.list_ae(db, study_id, status=status_filter, seriousness=seriousness, site_id=site_id, subject_id=subject_id, overdue=overdue) 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_study_member())], ) async def get_ae( study_id: uuid.UUID, ae_id: uuid.UUID, db: AsyncSession = Depends(get_db_session), ) -> 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) return data @router.patch( "/{ae_id}", response_model=AERead, dependencies=[Depends(require_study_member())], ) 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 不存在") member_role = await _get_member_role(db, study_id, current_user.id) if current_user.role == "ADMIN": pass elif member_role in ALLOWED_UPDATE_ROLES: pass elif member_role == "CRA": if ae.created_by != current_user.id: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="仅创建人可更新 AE") if ae_in.status == "CLOSED": raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="CRA 无法关闭 AE") else: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足") old_status = ae.status detail_before = { "event": ae.term, "onset_date": ae.onset_date, "severity": ae.severity, "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, "severity": updated.severity, "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_study_member())], ) 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 不存在") member_role = await _get_member_role(db, study_id, current_user.id) if current_user.role != "ADMIN" and member_role not in {"PM", "PV"}: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足") 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, )