import uuid from datetime import date from typing import Sequence from sqlalchemy import delete as sa_delete, select, update as sa_update from sqlalchemy.ext.asyncio import AsyncSession from app.crud import visit as visit_crud from app.models.ae import AdverseEvent from app.models.finance import FinanceItem from app.models.study import Study from app.models.subject_history import SubjectHistory from app.models.visit import Visit from app.models.site import Site from app.models.subject import Subject from app.schemas.subject import SubjectCreate, SubjectUpdate def _validate_subject_date_chain( *, screening_date: date | None, consent_date: date | None, enrollment_date: date | None, completion_date: date | None, ) -> None: if screening_date and consent_date and consent_date < screening_date: raise ValueError("知情同意日期不能早于筛选日期") if screening_date and enrollment_date and enrollment_date < screening_date: raise ValueError("入组日期不能早于筛选日期") if consent_date and enrollment_date and enrollment_date < consent_date: raise ValueError("入组日期不能早于知情同意日期") if screening_date and completion_date and completion_date < screening_date: raise ValueError("完成日期不能早于筛选日期") if consent_date and completion_date and completion_date < consent_date: raise ValueError("完成日期不能早于知情同意日期") if enrollment_date and completion_date and completion_date < enrollment_date: raise ValueError("完成日期不能早于入组日期") def _should_sync_visits(previous_baseline_date: date | None, next_baseline_date: date | None) -> bool: return next_baseline_date is not None and previous_baseline_date != next_baseline_date async def _validate_site(db: AsyncSession, study_id: uuid.UUID, site_id: uuid.UUID) -> None: result = await db.execute(select(Site).where(Site.id == site_id)) site = result.scalar_one_or_none() if not site or site.study_id != study_id: raise ValueError("分中心不属于当前项目") if not site.is_active: raise ValueError("中心已停用") async def create_subject(db: AsyncSession, study_id: uuid.UUID, subject_in: SubjectCreate) -> Subject: await _validate_site(db, study_id, subject_in.site_id) _validate_subject_date_chain( screening_date=subject_in.screening_date, consent_date=subject_in.consent_date, enrollment_date=subject_in.enrollment_date, completion_date=None, ) subject = Subject( study_id=study_id, site_id=subject_in.site_id, subject_no=subject_in.subject_no, status="SCREENING", screening_date=subject_in.screening_date, consent_date=subject_in.consent_date, enrollment_date=subject_in.enrollment_date, baseline_date=subject_in.baseline_date, completion_date=None, drop_reason=None, ) db.add(subject) await db.commit() await db.refresh(subject) await sync_visits_from_baseline(db, subject) return subject async def get_subject(db: AsyncSession, subject_id: uuid.UUID) -> Subject | None: result = await db.execute(select(Subject).where(Subject.id == subject_id)) return result.scalar_one_or_none() async def list_subjects( db: AsyncSession, study_id: uuid.UUID, site_id: uuid.UUID | None = None, site_ids: set[uuid.UUID] | None = None, ) -> Sequence[Subject]: stmt = select(Subject).where(Subject.study_id == study_id) if site_ids is not None: if not site_ids: return [] stmt = stmt.where(Subject.site_id.in_(site_ids)) if site_id: stmt = stmt.where(Subject.site_id == site_id) result = await db.execute(stmt) return result.scalars().all() async def sync_visits_from_baseline(db: AsyncSession, subject: Subject) -> None: # 基线/治疗日期是访视计划的唯一推算基准,不能用入组日期替代。 result = await db.execute(select(Study).where(Study.id == subject.study_id)) study = result.scalar_one_or_none() if not study: return await visit_crud.create_scheduled_visits( db, study_id=subject.study_id, subject=subject, base_date=subject.baseline_date, visit_schedule=study.visit_schedule, ) async def generate_default_visits(db: AsyncSession, subject: Subject) -> None: await sync_visits_from_baseline(db, subject) async def update_subject(db: AsyncSession, subject: Subject, subject_in: SubjectUpdate) -> Subject: update_data = subject_in.model_dump(exclude_unset=True) next_screening_date = subject.screening_date next_consent_date = update_data.get("consent_date", subject.consent_date) next_enrollment_date = update_data.get("enrollment_date", subject.enrollment_date) next_completion_date = update_data.get("completion_date", subject.completion_date) _validate_subject_date_chain( screening_date=next_screening_date, consent_date=next_consent_date, enrollment_date=next_enrollment_date, completion_date=next_completion_date, ) if update_data: await db.execute( sa_update(Subject) .where(Subject.id == subject.id) .values(**update_data) ) await db.commit() await db.refresh(subject) return subject def should_generate_visits_after_subject_update( *, previous_baseline_date: date | None, next_baseline_date: date | None, ) -> bool: return _should_sync_visits(previous_baseline_date, next_baseline_date) async def delete_subject(db: AsyncSession, subject: Subject) -> None: subject_id = subject.id await db.execute(sa_delete(Visit).where(Visit.subject_id == subject_id)) await db.execute(sa_delete(SubjectHistory).where(SubjectHistory.subject_id == subject_id)) await db.execute(sa_delete(AdverseEvent).where(AdverseEvent.subject_id == subject_id)) await db.execute(sa_delete(FinanceItem).where(FinanceItem.subject_id == subject_id)) await db.delete(subject) await db.commit()