import uuid from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession from app.core.deps import get_operator_role_label, get_cra_site_scope, get_current_user, get_db_session, require_api_permission from app.crud import audit as audit_crud from app.crud import site as site_crud from app.crud import startup as startup_crud from app.crud import study as study_crud from app.schemas.startup import ( KickoffMeetingCreate, KickoffMeetingRead, KickoffMeetingUpdate, StartupEthicsCreate, StartupEthicsRead, StartupEthicsUpdate, StartupFeasibilityCreate, StartupFeasibilityRead, StartupFeasibilityUpdate, TrainingAuthorizationCreate, TrainingAuthorizationRead, TrainingAuthorizationUpdate, ) 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_site_name_active(db: AsyncSession, study_id: uuid.UUID, site_name: str | None): if not site_name: return active_names = await site_crud.list_active_names(db, study_id) if site_name not in active_names: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="中心已停用") async def _ensure_site_active(db: AsyncSession, site_id: uuid.UUID | None): if not site_id: return site = await site_crud.get_site(db, site_id) if not site or not site.is_active: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="中心已停用") @router.post( "/feasibility", response_model=StartupFeasibilityRead, status_code=status.HTTP_201_CREATED, dependencies=[Depends(require_api_permission("startup_initiation:create"))], ) async def create_feasibility( study_id: uuid.UUID, record_in: StartupFeasibilityCreate, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ) -> StartupFeasibilityRead: await _ensure_study_exists(db, study_id) cra_scope = await get_cra_site_scope(db, study_id, current_user) if cra_scope and record_in.site_id not in cra_scope[0]: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足") await _ensure_site_active(db, record_in.site_id) record = await startup_crud.create_feasibility(db, study_id, record_in, created_by=current_user.id) await audit_crud.log_action( db, study_id=study_id, entity_type="startup_feasibility", entity_id=record.id, action="CREATE_STARTUP_FEASIBILITY", detail="立项记录已创建", operator_id=current_user.id, operator_role=await get_operator_role_label(db, study_id, current_user), ) return StartupFeasibilityRead.model_validate(record) @router.get( "/feasibility", response_model=list[StartupFeasibilityRead], dependencies=[Depends(require_api_permission("startup_initiation:read"))], ) async def list_feasibilities( study_id: uuid.UUID, skip: int = 0, limit: int = 100, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ) -> list[StartupFeasibilityRead]: 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 items = await startup_crud.list_feasibilities(db, study_id, skip=skip, limit=limit, site_ids=site_ids) return [StartupFeasibilityRead.model_validate(item) for item in items] @router.get( "/feasibility/{record_id}", response_model=StartupFeasibilityRead, dependencies=[Depends(require_api_permission("startup_initiation:read"))], ) async def get_feasibility( study_id: uuid.UUID, record_id: uuid.UUID, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ) -> StartupFeasibilityRead: await _ensure_study_exists(db, study_id) record = await startup_crud.get_feasibility(db, record_id) if not record or record.study_id != study_id: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="立项记录不存在") cra_scope = await get_cra_site_scope(db, study_id, current_user) if cra_scope and record.site_id not in cra_scope[0]: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足") return StartupFeasibilityRead.model_validate(record) @router.patch( "/feasibility/{record_id}", response_model=StartupFeasibilityRead, dependencies=[Depends(require_api_permission("startup_initiation:update"))], ) async def update_feasibility( study_id: uuid.UUID, record_id: uuid.UUID, record_in: StartupFeasibilityUpdate, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ) -> StartupFeasibilityRead: await _ensure_study_exists(db, study_id) record = await startup_crud.get_feasibility(db, record_id) if not record or record.study_id != study_id: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="立项记录不存在") cra_scope = await get_cra_site_scope(db, study_id, current_user) if cra_scope and record.site_id not in cra_scope[0]: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足") await _ensure_site_active(db, record.site_id) record = await startup_crud.update_feasibility(db, record, record_in) await audit_crud.log_action( db, study_id=study_id, entity_type="startup_feasibility", entity_id=record_id, action="UPDATE_STARTUP_FEASIBILITY", detail="立项记录已更新", operator_id=current_user.id, operator_role=await get_operator_role_label(db, study_id, current_user), ) return StartupFeasibilityRead.model_validate(record) @router.delete( "/feasibility/{record_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(require_api_permission("startup_initiation:delete"))], ) async def delete_feasibility( study_id: uuid.UUID, record_id: uuid.UUID, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ) -> None: await _ensure_study_exists(db, study_id) record = await startup_crud.get_feasibility(db, record_id) if not record or record.study_id != study_id: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="立项记录不存在") cra_scope = await get_cra_site_scope(db, study_id, current_user) if cra_scope and record.site_id not in cra_scope[0]: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足") await _ensure_site_active(db, record.site_id) await startup_crud.delete_feasibility(db, record) await audit_crud.log_action( db, study_id=study_id, entity_type="startup_feasibility", entity_id=record_id, action="DELETE_STARTUP_FEASIBILITY", detail="立项记录已删除", operator_id=current_user.id, operator_role=await get_operator_role_label(db, study_id, current_user), ) @router.post( "/ethics", response_model=StartupEthicsRead, status_code=status.HTTP_201_CREATED, dependencies=[Depends(require_api_permission("startup_ethics:create"))], ) async def create_ethics( study_id: uuid.UUID, record_in: StartupEthicsCreate, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ) -> StartupEthicsRead: await _ensure_study_exists(db, study_id) cra_scope = await get_cra_site_scope(db, study_id, current_user) if cra_scope and record_in.site_id not in cra_scope[0]: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足") await _ensure_site_active(db, record_in.site_id) record = await startup_crud.create_ethics(db, study_id, record_in, created_by=current_user.id) await audit_crud.log_action( db, study_id=study_id, entity_type="startup_ethics", entity_id=record.id, action="CREATE_STARTUP_ETHICS", detail="伦理记录已创建", operator_id=current_user.id, operator_role=await get_operator_role_label(db, study_id, current_user), ) return StartupEthicsRead.model_validate(record) @router.get( "/ethics", response_model=list[StartupEthicsRead], dependencies=[Depends(require_api_permission("startup_ethics:read"))], ) async def list_ethics( study_id: uuid.UUID, skip: int = 0, limit: int = 100, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ) -> list[StartupEthicsRead]: 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 items = await startup_crud.list_ethics(db, study_id, skip=skip, limit=limit, site_ids=site_ids) return [StartupEthicsRead.model_validate(item) for item in items] @router.get( "/ethics/{record_id}", response_model=StartupEthicsRead, dependencies=[Depends(require_api_permission("startup_ethics:read"))], ) async def get_ethics( study_id: uuid.UUID, record_id: uuid.UUID, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ) -> StartupEthicsRead: await _ensure_study_exists(db, study_id) record = await startup_crud.get_ethics(db, record_id) if not record or record.study_id != study_id: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="伦理记录不存在") cra_scope = await get_cra_site_scope(db, study_id, current_user) if cra_scope and record.site_id not in cra_scope[0]: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足") return StartupEthicsRead.model_validate(record) @router.patch( "/ethics/{record_id}", response_model=StartupEthicsRead, dependencies=[Depends(require_api_permission("startup_ethics:update"))], ) async def update_ethics( study_id: uuid.UUID, record_id: uuid.UUID, record_in: StartupEthicsUpdate, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ) -> StartupEthicsRead: await _ensure_study_exists(db, study_id) record = await startup_crud.get_ethics(db, record_id) if not record or record.study_id != study_id: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="伦理记录不存在") cra_scope = await get_cra_site_scope(db, study_id, current_user) if cra_scope and record.site_id not in cra_scope[0]: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足") await _ensure_site_active(db, record.site_id) record = await startup_crud.update_ethics(db, record, record_in) await audit_crud.log_action( db, study_id=study_id, entity_type="startup_ethics", entity_id=record_id, action="UPDATE_STARTUP_ETHICS", detail="伦理记录已更新", operator_id=current_user.id, operator_role=await get_operator_role_label(db, study_id, current_user), ) return StartupEthicsRead.model_validate(record) @router.delete( "/ethics/{record_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(require_api_permission("startup_ethics:delete"))], ) async def delete_ethics( study_id: uuid.UUID, record_id: uuid.UUID, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ) -> None: await _ensure_study_exists(db, study_id) record = await startup_crud.get_ethics(db, record_id) if not record or record.study_id != study_id: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="伦理记录不存在") cra_scope = await get_cra_site_scope(db, study_id, current_user) if cra_scope and record.site_id not in cra_scope[0]: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足") await _ensure_site_active(db, record.site_id) await startup_crud.delete_ethics(db, record) await audit_crud.log_action( db, study_id=study_id, entity_type="startup_ethics", entity_id=record_id, action="DELETE_STARTUP_ETHICS", detail="伦理记录已删除", operator_id=current_user.id, operator_role=await get_operator_role_label(db, study_id, current_user), ) @router.post( "/kickoff", response_model=KickoffMeetingRead, status_code=status.HTTP_201_CREATED, dependencies=[Depends(require_api_permission("startup_auth:create"))], ) async def create_kickoff( study_id: uuid.UUID, meeting_in: KickoffMeetingCreate, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ) -> KickoffMeetingRead: await _ensure_study_exists(db, study_id) cra_scope = await get_cra_site_scope(db, study_id, current_user) if cra_scope and meeting_in.site_id not in cra_scope[0]: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足") await _ensure_site_active(db, meeting_in.site_id) meeting = await startup_crud.create_kickoff(db, study_id, meeting_in, created_by=current_user.id) await audit_crud.log_action( db, study_id=study_id, entity_type="startup_kickoff", entity_id=meeting.id, action="CREATE_KICKOFF_MEETING", detail="启动会记录已创建", operator_id=current_user.id, operator_role=await get_operator_role_label(db, study_id, current_user), ) return KickoffMeetingRead.model_validate(meeting) @router.get( "/kickoff", response_model=list[KickoffMeetingRead], dependencies=[Depends(require_api_permission("startup_auth:read"))], ) async def list_kickoffs( study_id: uuid.UUID, skip: int = 0, limit: int = 100, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ) -> list[KickoffMeetingRead]: 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 items = await startup_crud.list_kickoffs(db, study_id, skip=skip, limit=limit, site_ids=site_ids) return [KickoffMeetingRead.model_validate(item) for item in items] @router.get( "/kickoff/{meeting_id}", response_model=KickoffMeetingRead, dependencies=[Depends(require_api_permission("startup_auth:read"))], ) async def get_kickoff( study_id: uuid.UUID, meeting_id: uuid.UUID, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ) -> KickoffMeetingRead: await _ensure_study_exists(db, study_id) meeting = await startup_crud.get_kickoff(db, meeting_id) if not meeting or meeting.study_id != study_id: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="启动会记录不存在") cra_scope = await get_cra_site_scope(db, study_id, current_user) if cra_scope and meeting.site_id not in cra_scope[0]: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足") return KickoffMeetingRead.model_validate(meeting) @router.patch( "/kickoff/{meeting_id}", response_model=KickoffMeetingRead, dependencies=[Depends(require_api_permission("startup_auth:update"))], ) async def update_kickoff( study_id: uuid.UUID, meeting_id: uuid.UUID, meeting_in: KickoffMeetingUpdate, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ) -> KickoffMeetingRead: await _ensure_study_exists(db, study_id) meeting = await startup_crud.get_kickoff(db, meeting_id) if not meeting or meeting.study_id != study_id: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="启动会记录不存在") cra_scope = await get_cra_site_scope(db, study_id, current_user) if cra_scope and meeting.site_id not in cra_scope[0]: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足") await _ensure_site_active(db, meeting.site_id) meeting = await startup_crud.update_kickoff(db, meeting, meeting_in) await audit_crud.log_action( db, study_id=study_id, entity_type="startup_kickoff", entity_id=meeting_id, action="UPDATE_KICKOFF_MEETING", detail="启动会记录已更新", operator_id=current_user.id, operator_role=await get_operator_role_label(db, study_id, current_user), ) return KickoffMeetingRead.model_validate(meeting) @router.post( "/training-authorizations", response_model=TrainingAuthorizationRead, status_code=status.HTTP_201_CREATED, dependencies=[Depends(require_api_permission("startup_auth:create"))], ) async def create_training_authorization( study_id: uuid.UUID, record_in: TrainingAuthorizationCreate, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ) -> TrainingAuthorizationRead: await _ensure_study_exists(db, study_id) cra_scope = await get_cra_site_scope(db, study_id, current_user) if cra_scope and record_in.site_name not in cra_scope[1]: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足") await _ensure_site_name_active(db, study_id, record_in.site_name) record = await startup_crud.create_training_authorization(db, study_id, record_in, created_by=current_user.id) await audit_crud.log_action( db, study_id=study_id, entity_type="training_authorization", entity_id=record.id, action="CREATE_TRAINING_AUTH", detail="培训授权人员已创建", operator_id=current_user.id, operator_role=await get_operator_role_label(db, study_id, current_user), ) return TrainingAuthorizationRead.model_validate(record) @router.get( "/training-authorizations", response_model=list[TrainingAuthorizationRead], dependencies=[Depends(require_api_permission("startup_auth:read"))], ) async def list_training_authorizations( study_id: uuid.UUID, skip: int = 0, limit: int = 200, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ) -> list[TrainingAuthorizationRead]: await _ensure_study_exists(db, study_id) cra_scope = await get_cra_site_scope(db, study_id, current_user) site_names = cra_scope[1] if cra_scope else None items = await startup_crud.list_training_authorizations(db, study_id, skip=skip, limit=limit, site_names=site_names) return [TrainingAuthorizationRead.model_validate(item) for item in items] @router.get( "/training-authorizations/{record_id}", response_model=TrainingAuthorizationRead, dependencies=[Depends(require_api_permission("startup_auth:read"))], ) async def get_training_authorization( study_id: uuid.UUID, record_id: uuid.UUID, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ) -> TrainingAuthorizationRead: await _ensure_study_exists(db, study_id) record = await startup_crud.get_training_authorization(db, record_id) if not record or record.study_id != study_id: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="培训授权人员不存在") cra_scope = await get_cra_site_scope(db, study_id, current_user) if cra_scope and record.site_name not in cra_scope[1]: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足") return TrainingAuthorizationRead.model_validate(record) @router.patch( "/training-authorizations/{record_id}", response_model=TrainingAuthorizationRead, dependencies=[Depends(require_api_permission("startup_auth:update"))], ) async def update_training_authorization( study_id: uuid.UUID, record_id: uuid.UUID, record_in: TrainingAuthorizationUpdate, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ) -> TrainingAuthorizationRead: await _ensure_study_exists(db, study_id) record = await startup_crud.get_training_authorization(db, record_id) if not record or record.study_id != study_id: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="培训授权人员不存在") cra_scope = await get_cra_site_scope(db, study_id, current_user) if cra_scope and record.site_name not in cra_scope[1]: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足") await _ensure_site_name_active(db, study_id, record.site_name) record = await startup_crud.update_training_authorization(db, record, record_in) await audit_crud.log_action( db, study_id=study_id, entity_type="training_authorization", entity_id=record_id, action="UPDATE_TRAINING_AUTH", detail="培训授权人员已更新", operator_id=current_user.id, operator_role=await get_operator_role_label(db, study_id, current_user), ) return TrainingAuthorizationRead.model_validate(record) @router.delete( "/training-authorizations/{record_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(require_api_permission("startup_auth:delete"))], ) async def delete_training_authorization( study_id: uuid.UUID, record_id: uuid.UUID, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ) -> None: await _ensure_study_exists(db, study_id) record = await startup_crud.get_training_authorization(db, record_id) if not record or record.study_id != study_id: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="培训授权人员不存在") cra_scope = await get_cra_site_scope(db, study_id, current_user) if cra_scope and record.site_name not in cra_scope[1]: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足") await _ensure_site_name_active(db, study_id, record.site_name) await startup_crud.delete_training_authorization(db, record) await audit_crud.log_action( db, study_id=study_id, entity_type="training_authorization", entity_id=record_id, action="DELETE_TRAINING_AUTH", detail="培训授权人员已删除", operator_id=current_user.id, operator_role=await get_operator_role_label(db, study_id, current_user), )