chore: stop tracking frontend node_modules

This commit is contained in:
Cheng Zhou
2026-01-20 08:52:54 +08:00
parent 7fdcfdaadd
commit 8e258d21a7
17405 changed files with 1643 additions and 2086724 deletions
@@ -0,0 +1,28 @@
"""add enrollment_target to sites
Revision ID: 20260116_02
Revises: 20260116_01
Create Date: 2026-01-16 16:10:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '20260116_02'
down_revision: Union[str, None] = '20260116_01'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# 添加 enrollment_target 字段到 sites 表
op.add_column('sites', sa.Column('enrollment_target', sa.Integer(), nullable=True))
def downgrade() -> None:
# 删除 enrollment_target 字段
op.drop_column('sites', 'enrollment_target')
@@ -0,0 +1,199 @@
"""seed overview demo data
Revision ID: 20260116_03
Revises: 20260116_02
Create Date: 2026-01-16 16:30:00.000000
"""
from typing import Sequence, Union
from datetime import date, timedelta
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import UUID
# revision identifiers, used by Alembic.
revision: str = '20260116_03'
down_revision: Union[str, None] = '20260116_02'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# 获取connection
conn = op.get_bind()
# 首先获取第一个项目ID
result = conn.execute(sa.text("SELECT id FROM studies LIMIT 1"))
study_row = result.first()
if not study_row:
print("没有项目,跳过示例数据注入")
return
study_id = study_row[0]
# 获取该项目的中心
result = conn.execute(sa.text("SELECT id, name FROM sites WHERE study_id = :study_id ORDER BY created_at LIMIT 5"), {"study_id": study_id})
sites = result.fetchall()
if not sites:
print("没有中心,跳过示例数据注入")
return
print(f"为 {len(sites)} 个中心注入示例数据...")
# 清理可能存在的演示数据(使用ON CONFLICT处理或先删除)
# 收集site_ids
site_ids = [site[0] for site in sites]
# 删除这些中心的existing数据 (如果是演示数据)
# 由于我们无法判断哪些是演示数据,使用ON CONFLICT DO NOTHING策略
# 更新入组目标
targets = [80, 60, 70, 50, 40]
for idx, site in enumerate(sites):
site_id = site[0]
target = targets[idx] if idx < len(targets) else 50
conn.execute(
sa.text("UPDATE sites SET enrollment_target = :target WHERE id = :site_id"),
{"target": target, "site_id": site_id}
)
# 基准日期
base_date = date(2024, 12, 1)
# 为每个中心添加启动流程数据
for idx, site in enumerate(sites):
site_id = site[0]
# 机构立项 - 使用 ON CONFLICT DO NOTHING (假设有唯一约束)
submit_date = base_date + timedelta(days=idx * 10)
accept_date = base_date + timedelta(days=idx * 10 + 5)
approved_date = base_date + timedelta(days=idx * 10 + 15) if idx < 4 else None
# 检查是否已存在
existing = conn.execute(sa.text(
"SELECT 1 FROM startup_feasibility WHERE study_id = :study_id AND site_id = :site_id LIMIT 1"
), {"study_id": study_id, "site_id": site_id}).first()
if not existing:
conn.execute(sa.text("""
INSERT INTO startup_feasibility (id, study_id, site_id, submit_date, accept_date, approved_date, project_no, created_at)
VALUES (gen_random_uuid(), :study_id, :site_id, :submit_date, :accept_date, :approved_date, :project_no, NOW())
"""), {
"study_id": study_id,
"site_id": site_id,
"submit_date": submit_date,
"accept_date": accept_date,
"approved_date": approved_date,
"project_no": f"PROJ-{idx+1:03d}"
})
# 伦理审批
ethics_submit = base_date + timedelta(days=idx * 10 + 20)
ethics_accept = base_date + timedelta(days=idx * 10 + 25)
meeting_date = base_date + timedelta(days=idx * 10 + 35) if idx < 4 else None
ethics_approved = base_date + timedelta(days=idx * 10 + 40) if idx < 4 else None
existing = conn.execute(sa.text(
"SELECT 1 FROM startup_ethics WHERE study_id = :study_id AND site_id = :site_id LIMIT 1"
), {"study_id": study_id, "site_id": site_id}).first()
if not existing:
conn.execute(sa.text("""
INSERT INTO startup_ethics (id, study_id, site_id, submit_date, accept_date, meeting_date, approved_date, approval_no, created_at)
VALUES (gen_random_uuid(), :study_id, :site_id, :submit_date, :accept_date, :meeting_date, :approved_date, :approval_no, NOW())
"""), {
"study_id": study_id,
"site_id": site_id,
"submit_date": ethics_submit,
"accept_date": ethics_accept,
"meeting_date": meeting_date,
"approved_date": ethics_approved,
"approval_no": f"EC-{idx+1:03d}" if idx < 4 else None
})
# 合同签署 (前3个中心)
if idx < 3:
site_name = site[1]
signed_date = base_date + timedelta(days=idx * 10 + 50)
# 检查合同是否已存在
existing = conn.execute(sa.text(
"SELECT 1 FROM finance_contracts WHERE study_id = :study_id AND site_name = :site_name LIMIT 1"
), {"study_id": study_id, "site_name": site_name}).first()
if not existing:
conn.execute(sa.text("""
INSERT INTO finance_contracts (id, study_id, site_name, contract_no, signed_date, amount, currency, created_at)
VALUES (gen_random_uuid(), :study_id, :site_name, :contract_no, :signed_date, 500000.0, 'CNY', NOW())
"""), {
"study_id": study_id,
"site_name": site_name,
"contract_no": f"CT-{idx+1:03d}",
"signed_date": signed_date
})
# 启动会 (前3个中心)
if idx < 3:
kickoff_date = base_date + timedelta(days=idx * 10 + 60)
# 检查启动会是否已存在
existing = conn.execute(sa.text(
"SELECT 1 FROM kickoff_meetings WHERE study_id = :study_id AND site_id = :site_id LIMIT 1"
), {"study_id": study_id, "site_id": site_id}).first()
if not existing:
conn.execute(sa.text("""
INSERT INTO kickoff_meetings (id, study_id, site_id, kickoff_date, attendees, created_at)
VALUES (gen_random_uuid(), :study_id, :site_id, :kickoff_date, '["研究者", "CRA", "PM"]'::jsonb, NOW())
"""), {
"study_id": study_id,
"site_id": site_id,
"kickoff_date": kickoff_date
})
# 为前3个中心添加受试者
enrollment_counts = [52, 18, 70]
start_enrollment = date(2025, 1, 1)
for idx in range(min(3, len(sites))):
site_id = sites[idx][0]
count = enrollment_counts[idx] if idx < len(enrollment_counts) else 0
# 检查该中心是否已有受试者数据
existing_count = conn.execute(sa.text(
"SELECT COUNT(*) FROM subjects WHERE study_id = :study_id AND site_id = :site_id"
), {"study_id": study_id, "site_id": site_id}).scalar()
# 如果已有数据,跳过该中心
if existing_count > 0:
print(f" 中心 {idx+1} 已有 {existing_count} 名受试者,跳过")
continue
for i in range(count):
enrollment_date = start_enrollment + timedelta(days=i * 4)
screening_date = enrollment_date - timedelta(days=7)
consent_date = enrollment_date - timedelta(days=3)
conn.execute(sa.text("""
INSERT INTO subjects (id, study_id, site_id, subject_no, status, screening_date, consent_date, enrollment_date, created_at)
VALUES (gen_random_uuid(), :study_id, :site_id, :subject_no, 'ENROLLED', :screening_date, :consent_date, :enrollment_date, NOW())
"""), {
"study_id": study_id,
"site_id": site_id,
"subject_no": f"S{idx+1:02d}-{i+1:03d}",
"screening_date": screening_date,
"consent_date": consent_date,
"enrollment_date": enrollment_date
})
print("✅ 示例数据注入完成")
def downgrade() -> None:
# 删除示例数据 (可选 - 也可以留空)
pass
+35
View File
@@ -0,0 +1,35 @@
"""项目概览API路由"""
import uuid
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.deps import get_db_session, require_study_member
from app.crud import overview as overview_crud
from app.schemas.overview import ProjectOverviewResponse
router = APIRouter()
@router.get(
"/overview",
response_model=ProjectOverviewResponse,
dependencies=[Depends(require_study_member())],
)
async def get_project_overview(
study_id: uuid.UUID,
db: AsyncSession = Depends(get_db_session),
) -> ProjectOverviewResponse:
"""
获取项目概览数据
返回项目各中心的进度情况,包括:
- 机构立项状态
- 伦理审批状态
- 合同签署状态
- 启动会状态
- 入组进度
- 月度入组统计
"""
data = await overview_crud.get_project_overview(db, study_id)
return ProjectOverviewResponse(**data)
+3 -1
View File
@@ -1,12 +1,14 @@
from fastapi import APIRouter
from app.api.v1 import auth, users, admin_users, studies, sites, members, attachments, audit_logs, dashboard, subjects, visits, aes, finance_dashboard, finance_contracts, finance_specials, fees_contracts, fees_specials, fees_attachments, drug_shipments, startup, knowledge_notes, subject_histories, faq_categories, faqs, documents
from app.api.v1 import auth, users, admin_users, studies, sites, members, attachments, audit_logs, dashboard, subjects, visits, aes, finance_dashboard, finance_contracts, finance_specials, fees_contracts, fees_specials, fees_attachments, drug_shipments, startup, knowledge_notes, subject_histories, faq_categories, faqs, documents, overview
api_router = APIRouter()
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
api_router.include_router(admin_users.router, prefix="/admin", tags=["admin"])
api_router.include_router(users.router, prefix="/users", tags=["users"])
api_router.include_router(studies.router, prefix="/studies", tags=["studies"])
api_router.include_router(overview.router, prefix="/studies/{study_id}", tags=["overview"])
api_router.include_router(sites.router, prefix="/studies/{study_id}/sites", tags=["sites"])
api_router.include_router(members.router, prefix="/studies/{study_id}/members", tags=["study-members"])
api_router.include_router(attachments.router, prefix="/studies/{study_id}/{entity_type}/{entity_id}/attachments", tags=["attachments"])
+299
View File
@@ -0,0 +1,299 @@
"""概览数据聚合CRUD操作"""
import uuid
from datetime import datetime
from typing import Dict, List, Tuple
from sqlalchemy import func, select, and_
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.site import Site
from app.models.startup_feasibility import StartupFeasibility
from app.models.startup_ethics import StartupEthics
from app.models.finance_contract import FinanceContract
from app.models.kickoff_meeting import KickoffMeeting
from app.models.subject import Subject
async def get_project_overview(db: AsyncSession, study_id: uuid.UUID) -> Dict:
"""
获取项目概览数据,聚合各中心的进度信息
返回格式:
{
"study_id": "...",
"updated_at": "2026-01-16T16:10:00",
"centers": [...],
"enrollment_by_month": [...]
}
"""
# 获取所有中心
stmt = select(Site).where(Site.study_id == study_id).order_by(Site.created_at)
result = await db.execute(stmt)
sites = result.scalars().all()
if not sites:
return {
"study_id": str(study_id),
"updated_at": datetime.now().isoformat(),
"centers": [],
"enrollment_by_month": []
}
site_ids = [site.id for site in sites]
# 并行获取各类数据
feasibility_data = await _get_feasibility_data(db, study_id, site_ids)
ethics_data = await _get_ethics_data(db, study_id, site_ids)
contract_data = await _get_contract_data(db, study_id, site_ids)
kickoff_data = await _get_kickoff_data(db, study_id, site_ids)
enrollment_data = await _get_enrollment_data(db, study_id, site_ids)
monthly_enrollment = await _get_monthly_enrollment(db, study_id)
# 组装中心数据
centers = []
for site in sites:
site_id_str = str(site.id)
# 机构立项状态
feas = feasibility_data.get(site_id_str, {})
institution_status = "COMPLETED" if feas.get("approved_date") else "IN_PROGRESS" if feas.get("submit_date") else "NOT_STARTED"
institution_completed = feas.get("approved_date")
# 伦理审批状态 - 依赖机构立项完成
eth = ethics_data.get(site_id_str, {})
if institution_status == "COMPLETED":
ethics_status = "COMPLETED" if eth.get("approved_date") else "IN_PROGRESS" if eth.get("submit_date") else "NOT_STARTED"
else:
# 机构立项未完成,伦理强制为未开始
ethics_status = "NOT_STARTED"
ethics_completed = eth.get("approved_date") if institution_status == "COMPLETED" else None
# 合同签署状态 - 依赖伦理审批完成
contract = contract_data.get(site_id_str, {})
if ethics_status == "COMPLETED":
contract_status = "COMPLETED" if contract.get("signed_date") else "NOT_STARTED"
else:
# 伦理未完成,合同强制为未开始
contract_status = "NOT_STARTED"
contract_completed = contract.get("signed_date") if ethics_status == "COMPLETED" else None
# 启动会状态 - 依赖合同签署完成
kickoff = kickoff_data.get(site_id_str, {})
if contract_status == "COMPLETED":
startup_status = "COMPLETED" if kickoff.get("kickoff_date") else "NOT_STARTED"
else:
# 合同未完成,启动会强制为未开始
startup_status = "NOT_STARTED"
startup_completed = kickoff.get("kickoff_date") if contract_status == "COMPLETED" else None
# 入组状态 - 依赖启动会完成
enrollment = enrollment_data.get(site_id_str, {"actual": 0})
enrollment_actual = enrollment["actual"]
enrollment_target = site.enrollment_target or 0
if startup_status == "COMPLETED":
if enrollment_actual >= enrollment_target and enrollment_target > 0:
enrollment_status = "COMPLETED"
elif enrollment_actual > 0:
enrollment_status = "IN_PROGRESS"
else:
enrollment_status = "NOT_STARTED"
else:
# 启动会未完成,入组强制为未开始
enrollment_status = "NOT_STARTED"
# 检查状态 - 依赖入组完成
# 暂未实现数据源,固定为NOT_STARTED
if enrollment_status == "COMPLETED":
inspection_status = "NOT_STARTED" # 未来从inspection表获取
else:
inspection_status = "NOT_STARTED"
# 关中心状态 - 依赖检查完成
# 暂未实现数据源,固定为NOT_STARTED
if inspection_status == "COMPLETED":
closeout_status = "NOT_STARTED" # 未来从closeout表获取
else:
closeout_status = "NOT_STARTED"
centers.append({
"center_id": str(site.id),
"center_name": site.name,
"institution_initiation_status": institution_status,
"ethics_status": ethics_status,
"contract_sign_status": contract_status,
"startup_status": startup_status,
"enrollment_status": enrollment_status,
"inspection_status": inspection_status,
"closeout_status": closeout_status,
"institution_initiation_completed_at": institution_completed,
"ethics_completed_at": ethics_completed,
"contract_sign_completed_at": contract_completed,
"startup_completed_at": startup_completed,
"enrollment_completed_at": None, # 暂未实现
"inspection_completed_at": None,
"closeout_completed_at": None,
"enrollment_target": enrollment_target,
"enrollment_actual": enrollment_actual,
})
return {
"study_id": str(study_id),
"updated_at": datetime.now().isoformat(),
"centers": centers,
"enrollment_by_month": monthly_enrollment
}
async def _get_feasibility_data(db: AsyncSession, study_id: uuid.UUID, site_ids: List[uuid.UUID]) -> Dict:
"""获取各中心的立项数据"""
stmt = select(
StartupFeasibility.site_id,
StartupFeasibility.submit_date,
StartupFeasibility.approved_date
).where(
and_(
StartupFeasibility.study_id == study_id,
StartupFeasibility.site_id.in_(site_ids)
)
).order_by(StartupFeasibility.created_at.desc())
result = await db.execute(stmt)
rows = result.all()
data = {}
for row in rows:
site_id_str = str(row.site_id)
if site_id_str not in data:
data[site_id_str] = {
"submit_date": row.submit_date.isoformat() if row.submit_date else None,
"approved_date": row.approved_date.isoformat() if row.approved_date else None,
}
return data
async def _get_ethics_data(db: AsyncSession, study_id: uuid.UUID, site_ids: List[uuid.UUID]) -> Dict:
"""获取各中心的伦理数据"""
stmt = select(
StartupEthics.site_id,
StartupEthics.submit_date,
StartupEthics.approved_date
).where(
and_(
StartupEthics.study_id == study_id,
StartupEthics.site_id.in_(site_ids)
)
).order_by(StartupEthics.created_at.desc())
result = await db.execute(stmt)
rows = result.all()
data = {}
for row in rows:
site_id_str = str(row.site_id)
if site_id_str not in data:
data[site_id_str] = {
"submit_date": row.submit_date.isoformat() if row.submit_date else None,
"approved_date": row.approved_date.isoformat() if row.approved_date else None,
}
return data
async def _get_contract_data(db: AsyncSession, study_id: uuid.UUID, site_ids: List[uuid.UUID]) -> Dict:
"""获取各中心的合同签署数据"""
# FinanceContract 通过 site_name 关联,需要先获取site名称映射
stmt_sites = select(Site.id, Site.name).where(Site.id.in_(site_ids))
result = await db.execute(stmt_sites)
site_name_map = {row.name: str(row.id) for row in result.all()}
stmt = select(
FinanceContract.site_name,
func.min(FinanceContract.signed_date).label("signed_date")
).where(
FinanceContract.study_id == study_id
).group_by(FinanceContract.site_name)
result = await db.execute(stmt)
rows = result.all()
data = {}
for row in rows:
site_id_str = site_name_map.get(row.site_name)
if site_id_str:
data[site_id_str] = {
"signed_date": row.signed_date.isoformat() if row.signed_date else None,
}
return data
async def _get_kickoff_data(db: AsyncSession, study_id: uuid.UUID, site_ids: List[uuid.UUID]) -> Dict:
"""获取各中心的启动会数据"""
stmt = select(
KickoffMeeting.site_id,
KickoffMeeting.kickoff_date
).where(
and_(
KickoffMeeting.study_id == study_id,
KickoffMeeting.site_id.in_(site_ids)
)
).order_by(KickoffMeeting.created_at.desc())
result = await db.execute(stmt)
rows = result.all()
data = {}
for row in rows:
site_id_str = str(row.site_id)
if site_id_str not in data:
data[site_id_str] = {
"kickoff_date": row.kickoff_date.isoformat() if row.kickoff_date else None,
}
return data
async def _get_enrollment_data(db: AsyncSession, study_id: uuid.UUID, site_ids: List[uuid.UUID]) -> Dict:
"""获取各中心的入组数据"""
stmt = select(
Subject.site_id,
func.count(Subject.id).label("count")
).where(
and_(
Subject.study_id == study_id,
Subject.site_id.in_(site_ids),
Subject.enrollment_date.isnot(None)
)
).group_by(Subject.site_id)
result = await db.execute(stmt)
rows = result.all()
data = {}
for row in rows:
data[str(row.site_id)] = {"actual": row.count}
return data
async def _get_monthly_enrollment(db: AsyncSession, study_id: uuid.UUID) -> List[Dict]:
"""获取月度入组统计"""
month_col = func.to_char(Subject.enrollment_date, 'YYYY-MM').label("month")
stmt = select(
month_col,
func.count(Subject.id).label("count")
).where(
and_(
Subject.study_id == study_id,
Subject.enrollment_date.isnot(None)
)
).group_by(month_col).order_by(month_col)
result = await db.execute(stmt)
rows = result.all()
return [{"month": row.month, "count": row.count} for row in rows]
+3 -1
View File
@@ -1,7 +1,7 @@
import uuid
from datetime import datetime
from sqlalchemy import Boolean, DateTime, ForeignKey, String, func
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, func
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column
@@ -18,4 +18,6 @@ class Site(Base):
pi_name: Mapped[str | None] = mapped_column(String(100), nullable=True)
contact: Mapped[str | None] = mapped_column(String(100), nullable=True)
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default="true")
enrollment_target: Mapped[int | None] = mapped_column(Integer, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
-2
View File
@@ -47,12 +47,10 @@ class DocumentSummary(BaseModel):
id: uuid.UUID
trial_id: uuid.UUID
site_id: Optional[uuid.UUID] = None
doc_no: str
doc_type: str
title: str
scope_type: DocumentScopeType
owner_id: Optional[uuid.UUID]
status: DocumentStatus
current_effective_version_id: Optional[uuid.UUID]
current_effective_version: Optional[DocumentVersionSummary] = None
created_at: datetime
-1
View File
@@ -20,7 +20,6 @@ class DocumentVersionSummary(BaseModel):
id: uuid.UUID
version_no: str
status: DocumentVersionStatus
effective_at: Optional[datetime] = None
model_config = ConfigDict(from_attributes=True)
+48
View File
@@ -0,0 +1,48 @@
"""Overview Schema定义"""
import uuid
from datetime import date
from typing import List, Optional
from pydantic import BaseModel
class CenterOverview(BaseModel):
"""单个中心的概览数据"""
center_id: str
center_name: str
# 各阶段状态
institution_initiation_status: str = "NOT_STARTED"
ethics_status: str = "NOT_STARTED"
contract_sign_status: str = "NOT_STARTED"
startup_status: str = "NOT_STARTED"
enrollment_status: str = "NOT_STARTED"
inspection_status: str = "NOT_STARTED"
closeout_status: str = "NOT_STARTED"
# 各阶段完成日期
institution_initiation_completed_at: Optional[str] = None
ethics_completed_at: Optional[str] = None
contract_sign_completed_at: Optional[str] = None
startup_completed_at: Optional[str] = None
enrollment_completed_at: Optional[str] = None
inspection_completed_at: Optional[str] = None
closeout_completed_at: Optional[str] = None
# 入组数据
enrollment_target: int = 0
enrollment_actual: int = 0
class EnrollmentByMonth(BaseModel):
"""月度入组统计"""
month: str
count: int
class ProjectOverviewResponse(BaseModel):
"""项目概览响应"""
study_id: str
updated_at: str
centers: List[CenterOverview]
enrollment_by_month: List[EnrollmentByMonth]
+3
View File
@@ -11,6 +11,7 @@ class SiteCreate(BaseModel):
pi_name: Optional[str] = None
contact: Optional[str] = None
is_active: bool = True
enrollment_target: Optional[int] = None
class SiteUpdate(BaseModel):
@@ -19,6 +20,7 @@ class SiteUpdate(BaseModel):
pi_name: Optional[str] = None
contact: Optional[str] = None
is_active: Optional[bool] = None
enrollment_target: Optional[int] = None
class SiteRead(BaseModel):
@@ -29,6 +31,7 @@ class SiteRead(BaseModel):
pi_name: Optional[str]
contact: Optional[str]
is_active: bool
enrollment_target: Optional[int]
created_at: datetime
model_config = ConfigDict(from_attributes=True)
-4
View File
@@ -163,12 +163,10 @@ async def list_documents(
id=doc.id,
trial_id=doc.trial_id,
site_id=doc.site_id,
doc_no=doc.doc_no,
doc_type=doc.doc_type,
title=doc.title,
scope_type=doc.scope_type,
owner_id=doc.owner_id,
status=doc.status,
current_effective_version_id=doc.current_effective_version_id,
current_effective_version=current_version,
created_at=doc.created_at,
@@ -205,12 +203,10 @@ async def get_document_detail(
id=doc.id,
trial_id=doc.trial_id,
site_id=doc.site_id,
doc_no=doc.doc_no,
doc_type=doc.doc_type,
title=doc.title,
scope_type=doc.scope_type,
owner_id=doc.owner_id,
status=doc.status,
current_effective_version_id=doc.current_effective_version_id,
current_effective_version=current_version,
description=doc.description,
+150
View File
@@ -0,0 +1,150 @@
"""
项目概览示例数据注入脚本
运行方式:
docker compose run --rm backend python scripts/seed_overview_data.py
"""
import asyncio
import sys
import uuid
from datetime import date, timedelta
from pathlib import Path
# 添加app目录到Python路径
sys.path.insert(0, str(Path(__file__).parent.parent))
from sqlalchemy import select
from app.db.session import SessionLocal
from app.models.site import Site
from app.models.startup_feasibility import StartupFeasibility
from app.models.startup_ethics import StartupEthics
from app.models.finance_contract import FinanceContract
from app.models.kickoff_meeting import KickoffMeeting
from app.models.subject import Subject
async def seed_data():
"""注入示例数据"""
async with SessionLocal() as db:
# 获取第一个项目用于测试
stmt = select(Site.study_id).limit(1)
result = await db.execute(stmt)
study_id_row = result.first()
if not study_id_row:
print("❌ 数据库中没有项目,请先创建项目和中心")
return
study_id = study_id_row[0]
print(f"✓ 使用项目 ID: {study_id}")
# 获取该项目的所有中心
stmt = select(Site).where(Site.study_id == study_id)
result = await db.execute(stmt)
sites = result.scalars().all()
if not sites:
print("❌ 项目下没有中心,请先创建中心")
return
print(f"✓ 找到 {len(sites)} 个中心")
# 为每个中心设置入组目标
targets = [80, 60, 70, 50, 40]
for idx, site in enumerate(sites[:5]):
site.enrollment_target = targets[idx] if idx < len(targets) else 50
print(f" • {site.name}: 目标入组 {site.enrollment_target} 人")
await db.commit()
print("✓ 入组目标已更新")
# 为中心添加启动流程数据
base_date = date(2024, 12, 1)
for idx, site in enumerate(sites[:5]):
# 机构立项
feasibility = StartupFeasibility(
id=uuid.uuid4(),
study_id=study_id,
site_id=site.id,
submit_date=base_date + timedelta(days=idx * 10),
accept_date=base_date + timedelta(days=idx * 10 + 5),
approved_date=base_date + timedelta(days=idx * 10 + 15) if idx < 4 else None,
project_no=f"PROJ-{idx+1:03d}"
)
db.add(feasibility)
# 伦理审批
ethics = StartupEthics(
id=uuid.uuid4(),
study_id=study_id,
site_id=site.id,
submit_date=base_date + timedelta(days=idx * 10 + 20),
accept_date=base_date + timedelta(days=idx * 10 + 25),
meeting_date=base_date + timedelta(days=idx * 10 + 35) if idx < 4 else None,
approved_date=base_date + timedelta(days=idx * 10 + 40) if idx < 4 else None,
approval_no=f"EC-{idx+1:03d}" if idx < 4 else None
)
db.add(ethics)
# 合同签署 (前3个中心)
if idx < 3:
contract = FinanceContract(
id=uuid.uuid4(),
study_id=study_id,
site_name=site.name,
contract_no=f"CT-{idx+1:03d}",
signed_date=base_date + timedelta(days=idx * 10 + 50),
amount=500000.0,
currency="CNY"
)
db.add(contract)
# 启动会 (前3个中心)
if idx < 3:
kickoff = KickoffMeeting(
id=uuid.uuid4(),
study_id=study_id,
site_id=site.id,
kickoff_date=base_date + timedelta(days=idx * 10 + 60),
attendees=["研究者", "CRA", "PM"]
)
db.add(kickoff)
await db.commit()
print("✓ 启动流程数据已添加")
# 为前3个中心添加入组受试者
enrollment_counts = [52, 18, 70]
start_enrollment_date = date(2025, 1, 1)
for idx, site in enumerate(sites[:3]):
count = enrollment_counts[idx] if idx < len(enrollment_counts) else 0
for i in range(count):
# 分散在6个月内入组
enrollment_date = start_enrollment_date + timedelta(days=i * 4)
subject = Subject(
id=uuid.uuid4(),
study_id=study_id,
site_id=site.id,
subject_no=f"S{idx+1:02d}-{i+1:03d}",
status="ENROLLED",
screening_date=enrollment_date - timedelta(days=7),
consent_date=enrollment_date - timedelta(days=3),
enrollment_date=enrollment_date
)
db.add(subject)
print(f" • {site.name}: 已入组 {count} 人")
await db.commit()
print("✓ 受试者入组数据已添加")
print("\n✅ 示例数据注入完成!")
print("请刷新项目概览页面查看效果")
if __name__ == "__main__":
asyncio.run(seed_data())