Files
ctms/backend/app/api/v1/startup.py
T
2026-01-16 13:50:08 +08:00

505 lines
17 KiB
Python

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 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_study_roles(["PM", "CRA"]))],
)
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)
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=current_user.role,
)
return StartupFeasibilityRead.model_validate(record)
@router.get(
"/feasibility",
response_model=list[StartupFeasibilityRead],
dependencies=[Depends(require_study_member())],
)
async def list_feasibilities(
study_id: uuid.UUID,
skip: int = 0,
limit: int = 100,
db: AsyncSession = Depends(get_db_session),
) -> list[StartupFeasibilityRead]:
await _ensure_study_exists(db, study_id)
items = await startup_crud.list_feasibilities(db, study_id, skip=skip, limit=limit)
return [StartupFeasibilityRead.model_validate(item) for item in items]
@router.get(
"/feasibility/{record_id}",
response_model=StartupFeasibilityRead,
dependencies=[Depends(require_study_member())],
)
async def get_feasibility(
study_id: uuid.UUID,
record_id: uuid.UUID,
db: AsyncSession = Depends(get_db_session),
) -> 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="立项记录不存在")
return StartupFeasibilityRead.model_validate(record)
@router.patch(
"/feasibility/{record_id}",
response_model=StartupFeasibilityRead,
dependencies=[Depends(require_study_roles(["PM", "CRA"]))],
)
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="立项记录不存在")
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=current_user.role,
)
return StartupFeasibilityRead.model_validate(record)
@router.delete(
"/feasibility/{record_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(require_study_roles(["PM", "CRA"]))],
)
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="立项记录不存在")
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=current_user.role,
)
@router.post(
"/ethics",
response_model=StartupEthicsRead,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(require_study_roles(["PM", "CRA"]))],
)
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)
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=current_user.role,
)
return StartupEthicsRead.model_validate(record)
@router.get(
"/ethics",
response_model=list[StartupEthicsRead],
dependencies=[Depends(require_study_member())],
)
async def list_ethics(
study_id: uuid.UUID,
skip: int = 0,
limit: int = 100,
db: AsyncSession = Depends(get_db_session),
) -> list[StartupEthicsRead]:
await _ensure_study_exists(db, study_id)
items = await startup_crud.list_ethics(db, study_id, skip=skip, limit=limit)
return [StartupEthicsRead.model_validate(item) for item in items]
@router.get(
"/ethics/{record_id}",
response_model=StartupEthicsRead,
dependencies=[Depends(require_study_member())],
)
async def get_ethics(
study_id: uuid.UUID,
record_id: uuid.UUID,
db: AsyncSession = Depends(get_db_session),
) -> 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="伦理记录不存在")
return StartupEthicsRead.model_validate(record)
@router.patch(
"/ethics/{record_id}",
response_model=StartupEthicsRead,
dependencies=[Depends(require_study_roles(["PM", "CRA"]))],
)
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="伦理记录不存在")
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=current_user.role,
)
return StartupEthicsRead.model_validate(record)
@router.delete(
"/ethics/{record_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(require_study_roles(["PM", "CRA"]))],
)
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="伦理记录不存在")
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=current_user.role,
)
@router.post(
"/kickoff",
response_model=KickoffMeetingRead,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(require_study_roles(["PM", "CRA"]))],
)
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)
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=current_user.role,
)
return KickoffMeetingRead.model_validate(meeting)
@router.get(
"/kickoff",
response_model=list[KickoffMeetingRead],
dependencies=[Depends(require_study_member())],
)
async def list_kickoffs(
study_id: uuid.UUID,
skip: int = 0,
limit: int = 100,
db: AsyncSession = Depends(get_db_session),
) -> list[KickoffMeetingRead]:
await _ensure_study_exists(db, study_id)
items = await startup_crud.list_kickoffs(db, study_id, skip=skip, limit=limit)
return [KickoffMeetingRead.model_validate(item) for item in items]
@router.get(
"/kickoff/{meeting_id}",
response_model=KickoffMeetingRead,
dependencies=[Depends(require_study_member())],
)
async def get_kickoff(
study_id: uuid.UUID,
meeting_id: uuid.UUID,
db: AsyncSession = Depends(get_db_session),
) -> 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="启动会记录不存在")
return KickoffMeetingRead.model_validate(meeting)
@router.patch(
"/kickoff/{meeting_id}",
response_model=KickoffMeetingRead,
dependencies=[Depends(require_study_roles(["PM", "CRA"]))],
)
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="启动会记录不存在")
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=current_user.role,
)
return KickoffMeetingRead.model_validate(meeting)
@router.post(
"/training-authorizations",
response_model=TrainingAuthorizationRead,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(require_study_roles(["PM", "CRA"]))],
)
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)
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=current_user.role,
)
return TrainingAuthorizationRead.model_validate(record)
@router.get(
"/training-authorizations",
response_model=list[TrainingAuthorizationRead],
dependencies=[Depends(require_study_member())],
)
async def list_training_authorizations(
study_id: uuid.UUID,
skip: int = 0,
limit: int = 200,
db: AsyncSession = Depends(get_db_session),
) -> list[TrainingAuthorizationRead]:
await _ensure_study_exists(db, study_id)
items = await startup_crud.list_training_authorizations(db, study_id, skip=skip, limit=limit)
return [TrainingAuthorizationRead.model_validate(item) for item in items]
@router.get(
"/training-authorizations/{record_id}",
response_model=TrainingAuthorizationRead,
dependencies=[Depends(require_study_member())],
)
async def get_training_authorization(
study_id: uuid.UUID,
record_id: uuid.UUID,
db: AsyncSession = Depends(get_db_session),
) -> 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="培训授权人员不存在")
return TrainingAuthorizationRead.model_validate(record)
@router.patch(
"/training-authorizations/{record_id}",
response_model=TrainingAuthorizationRead,
dependencies=[Depends(require_study_roles(["PM", "CRA"]))],
)
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="培训授权人员不存在")
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=current_user.role,
)
return TrainingAuthorizationRead.model_validate(record)
@router.delete(
"/training-authorizations/{record_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(require_study_roles(["PM", "CRA"]))],
)
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="培训授权人员不存在")
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=current_user.role,
)