feat: 清理示例数据并统一空态体验

This commit is contained in:
Cheng Zhou
2026-03-31 11:20:57 +08:00
parent fc74d1e9a9
commit 4bebc64662
38 changed files with 786 additions and 729 deletions
@@ -1,4 +1,4 @@
"""seed overview demo data
"""disable overview demo data seed
Revision ID: 20260116_03
Revises: 20260116_02
@@ -6,14 +6,7 @@ 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
@@ -21,179 +14,11 @@ 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("✅ 示例数据注入完成")
# This historical revision is intentionally kept as a no-op so fresh
# databases no longer receive overview demo data during migration.
return
def downgrade() -> None:
# 删除示例数据 (可选 - 也可以留空)
pass
return
@@ -0,0 +1,397 @@
"""cleanup overview demo data
Revision ID: 20260331_01
Revises: 20260302_02
Create Date: 2026-03-31 10:45:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = "20260331_01"
down_revision: Union[str, None] = "20260302_02"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
DEMO_FEASIBILITY_CODES = ("PROJ-001", "PROJ-002", "PROJ-003", "PROJ-004", "PROJ-005")
DEMO_ETHICS_CODES = ("EC-001", "EC-002", "EC-003", "EC-004")
DEMO_FINANCE_CONTRACT_CODES = ("CT-001", "CT-002", "CT-003")
DEMO_STUDY_CODE = "DEMO-CTMS"
def upgrade() -> None:
conn = op.get_bind()
conn.execute(
sa.text(
"""
WITH candidate_studies AS (
SELECT DISTINCT study_id
FROM startup_feasibility
WHERE project_no = ANY(:feasibility_codes)
UNION
SELECT DISTINCT study_id
FROM startup_ethics
WHERE approval_no = ANY(:ethics_codes)
UNION
SELECT DISTINCT study_id
FROM finance_contracts
WHERE contract_no = ANY(:contract_codes)
AND amount = 500000.0
AND currency = 'CNY'
UNION
SELECT DISTINCT study_id
FROM kickoff_meetings
WHERE attendees::text = '["研究者", "CRA", "PM"]'
UNION
SELECT DISTINCT study_id
FROM subjects
WHERE subject_no ~ '^S0[1-3]-[0-9]{3}$'
),
candidate_sites AS (
SELECT DISTINCT site_id
FROM startup_feasibility
WHERE study_id IN (SELECT study_id FROM candidate_studies)
AND project_no = ANY(:feasibility_codes)
UNION
SELECT DISTINCT site_id
FROM startup_ethics
WHERE study_id IN (SELECT study_id FROM candidate_studies)
AND (
approval_no = ANY(:ethics_codes)
OR (
approval_no IS NULL
AND submit_date = DATE '2025-01-10'
AND accept_date = DATE '2025-01-15'
AND meeting_date IS NULL
AND approved_date IS NULL
)
OR (
approval_no IS NULL
AND submit_date = DATE '2025-01-20'
AND accept_date = DATE '2025-01-25'
AND meeting_date IS NULL
AND approved_date IS NULL
)
OR (
approval_no IS NULL
AND submit_date = DATE '2025-01-30'
AND accept_date = DATE '2025-02-04'
AND meeting_date IS NULL
AND approved_date IS NULL
)
)
UNION
SELECT DISTINCT site_id
FROM kickoff_meetings
WHERE study_id IN (SELECT study_id FROM candidate_studies)
AND attendees::text = '["研究者", "CRA", "PM"]'
UNION
SELECT DISTINCT site_id
FROM subjects
WHERE study_id IN (SELECT study_id FROM candidate_studies)
AND subject_no ~ '^S0[1-3]-[0-9]{3}$'
)
UPDATE sites
SET enrollment_target = 0
WHERE id IN (SELECT site_id FROM candidate_sites WHERE site_id IS NOT NULL)
AND enrollment_target IN (40, 50, 60, 70, 80)
"""
),
{
"feasibility_codes": list(DEMO_FEASIBILITY_CODES),
"ethics_codes": list(DEMO_ETHICS_CODES),
"contract_codes": list(DEMO_FINANCE_CONTRACT_CODES),
},
)
conn.execute(
sa.text(
"""
WITH candidate_studies AS (
SELECT DISTINCT study_id FROM startup_feasibility WHERE project_no = ANY(:feasibility_codes)
UNION
SELECT DISTINCT study_id FROM startup_ethics WHERE approval_no = ANY(:ethics_codes)
UNION
SELECT DISTINCT study_id FROM finance_contracts
WHERE contract_no = ANY(:contract_codes) AND amount = 500000.0 AND currency = 'CNY'
UNION
SELECT DISTINCT study_id FROM kickoff_meetings WHERE attendees::text = '["研究者", "CRA", "PM"]'
UNION
SELECT DISTINCT study_id FROM subjects WHERE subject_no ~ '^S0[1-3]-[0-9]{3}$'
)
DELETE FROM subjects
WHERE study_id IN (SELECT study_id FROM candidate_studies)
AND subject_no ~ '^S0[1-3]-[0-9]{3}$'
"""
),
{
"feasibility_codes": list(DEMO_FEASIBILITY_CODES),
"ethics_codes": list(DEMO_ETHICS_CODES),
"contract_codes": list(DEMO_FINANCE_CONTRACT_CODES),
},
)
conn.execute(
sa.text(
"""
WITH candidate_studies AS (
SELECT DISTINCT study_id FROM startup_feasibility WHERE project_no = ANY(:feasibility_codes)
UNION
SELECT DISTINCT study_id FROM startup_ethics WHERE approval_no = ANY(:ethics_codes)
UNION
SELECT DISTINCT study_id FROM finance_contracts
WHERE contract_no = ANY(:contract_codes) AND amount = 500000.0 AND currency = 'CNY'
UNION
SELECT DISTINCT study_id FROM kickoff_meetings WHERE attendees::text = '["研究者", "CRA", "PM"]'
UNION
SELECT DISTINCT study_id FROM subjects WHERE subject_no ~ '^S0[1-3]-[0-9]{3}$'
)
DELETE FROM kickoff_meetings
WHERE study_id IN (SELECT study_id FROM candidate_studies)
AND attendees::text = '["研究者", "CRA", "PM"]'
"""
),
{
"feasibility_codes": list(DEMO_FEASIBILITY_CODES),
"ethics_codes": list(DEMO_ETHICS_CODES),
"contract_codes": list(DEMO_FINANCE_CONTRACT_CODES),
},
)
conn.execute(
sa.text(
"""
WITH candidate_studies AS (
SELECT DISTINCT study_id FROM startup_feasibility WHERE project_no = ANY(:feasibility_codes)
UNION
SELECT DISTINCT study_id FROM startup_ethics WHERE approval_no = ANY(:ethics_codes)
UNION
SELECT DISTINCT study_id FROM finance_contracts
WHERE contract_no = ANY(:contract_codes) AND amount = 500000.0 AND currency = 'CNY'
UNION
SELECT DISTINCT study_id FROM kickoff_meetings WHERE attendees::text = '["研究者", "CRA", "PM"]'
UNION
SELECT DISTINCT study_id FROM subjects WHERE subject_no ~ '^S0[1-3]-[0-9]{3}$'
)
DELETE FROM finance_contracts
WHERE study_id IN (SELECT study_id FROM candidate_studies)
AND contract_no = ANY(:contract_codes)
AND amount = 500000.0
AND currency = 'CNY'
"""
),
{
"feasibility_codes": list(DEMO_FEASIBILITY_CODES),
"ethics_codes": list(DEMO_ETHICS_CODES),
"contract_codes": list(DEMO_FINANCE_CONTRACT_CODES),
},
)
conn.execute(
sa.text(
"""
WITH candidate_studies AS (
SELECT DISTINCT study_id FROM startup_feasibility WHERE project_no = ANY(:feasibility_codes)
UNION
SELECT DISTINCT study_id FROM startup_ethics WHERE approval_no = ANY(:ethics_codes)
UNION
SELECT DISTINCT study_id FROM finance_contracts
WHERE contract_no = ANY(:contract_codes) AND amount = 500000.0 AND currency = 'CNY'
UNION
SELECT DISTINCT study_id FROM kickoff_meetings WHERE attendees::text = '["研究者", "CRA", "PM"]'
UNION
SELECT DISTINCT study_id FROM subjects WHERE subject_no ~ '^S0[1-3]-[0-9]{3}$'
)
DELETE FROM startup_ethics
WHERE study_id IN (SELECT study_id FROM candidate_studies)
AND (
approval_no = ANY(:ethics_codes)
OR (
approval_no IS NULL
AND submit_date = DATE '2025-01-10'
AND accept_date = DATE '2025-01-15'
AND meeting_date IS NULL
AND approved_date IS NULL
)
OR (
approval_no IS NULL
AND submit_date = DATE '2025-01-20'
AND accept_date = DATE '2025-01-25'
AND meeting_date IS NULL
AND approved_date IS NULL
)
OR (
approval_no IS NULL
AND submit_date = DATE '2025-01-30'
AND accept_date = DATE '2025-02-04'
AND meeting_date IS NULL
AND approved_date IS NULL
)
)
"""
),
{
"feasibility_codes": list(DEMO_FEASIBILITY_CODES),
"ethics_codes": list(DEMO_ETHICS_CODES),
"contract_codes": list(DEMO_FINANCE_CONTRACT_CODES),
},
)
conn.execute(
sa.text(
"""
WITH candidate_studies AS (
SELECT DISTINCT study_id FROM startup_feasibility WHERE project_no = ANY(:feasibility_codes)
UNION
SELECT DISTINCT study_id FROM startup_ethics WHERE approval_no = ANY(:ethics_codes)
UNION
SELECT DISTINCT study_id FROM finance_contracts
WHERE contract_no = ANY(:contract_codes) AND amount = 500000.0 AND currency = 'CNY'
UNION
SELECT DISTINCT study_id FROM kickoff_meetings WHERE attendees::text = '["研究者", "CRA", "PM"]'
UNION
SELECT DISTINCT study_id FROM subjects WHERE subject_no ~ '^S0[1-3]-[0-9]{3}$'
)
DELETE FROM startup_feasibility
WHERE study_id IN (SELECT study_id FROM candidate_studies)
AND project_no = ANY(:feasibility_codes)
"""
),
{
"feasibility_codes": list(DEMO_FEASIBILITY_CODES),
"ethics_codes": list(DEMO_ETHICS_CODES),
"contract_codes": list(DEMO_FINANCE_CONTRACT_CODES),
},
)
conn.execute(
sa.text(
"""
WITH demo_study AS (
SELECT id
FROM studies
WHERE code = :demo_study_code
),
demo_contract_fees AS (
SELECT id
FROM contract_fees
WHERE project_id IN (SELECT id FROM demo_study)
),
demo_special_expenses AS (
SELECT id
FROM special_expenses
WHERE project_id IN (SELECT id FROM demo_study)
)
DELETE FROM fee_attachments
WHERE (
entity_type = 'contract_fee'
AND entity_id IN (SELECT id FROM demo_contract_fees)
) OR (
entity_type = 'special_expense'
AND entity_id IN (SELECT id FROM demo_special_expenses)
) OR (
url IN (
'https://example.com/contract-demo.pdf',
'https://example.com/voucher-demo.pdf',
'https://example.com/invoice-demo.pdf'
)
)
"""
),
{
"demo_study_code": DEMO_STUDY_CODE,
},
)
conn.execute(
sa.text(
"""
WITH demo_study AS (
SELECT id
FROM studies
WHERE code = :demo_study_code
),
demo_contract_fees AS (
SELECT id
FROM contract_fees
WHERE project_id IN (SELECT id FROM demo_study)
)
DELETE FROM contract_fee_payments
WHERE contract_fee_id IN (SELECT id FROM demo_contract_fees)
"""
),
{
"demo_study_code": DEMO_STUDY_CODE,
},
)
conn.execute(
sa.text(
"""
DELETE FROM contract_fees
WHERE project_id IN (
SELECT id
FROM studies
WHERE code = :demo_study_code
)
"""
),
{
"demo_study_code": DEMO_STUDY_CODE,
},
)
conn.execute(
sa.text(
"""
DELETE FROM special_expenses
WHERE project_id IN (
SELECT id
FROM studies
WHERE code = :demo_study_code
)
"""
),
{
"demo_study_code": DEMO_STUDY_CODE,
},
)
conn.execute(
sa.text(
"""
DELETE FROM sites
WHERE study_id IN (
SELECT id
FROM studies
WHERE code = :demo_study_code
)
AND name LIKE '示例中心 %'
"""
),
{
"demo_study_code": DEMO_STUDY_CODE,
},
)
conn.execute(
sa.text(
"""
DELETE FROM studies
WHERE code = :demo_study_code
"""
),
{
"demo_study_code": DEMO_STUDY_CODE,
},
)
def downgrade() -> None:
return
-175
View File
@@ -1,175 +0,0 @@
import uuid
from datetime import date, timedelta
from decimal import Decimal
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.contract_fee import ContractFee
from app.models.contract_fee_payment import ContractFeePayment
from app.models.fee_attachment import FeeAttachment
from app.models.site import Site
from app.models.special_expense import SpecialExpense
from app.models.study import Study
from app.models.user import User
async def seed_demo_fees(db: AsyncSession) -> None:
existing_contract = await db.execute(select(ContractFee.id).limit(1))
existing_special = await db.execute(select(SpecialExpense.id).limit(1))
if existing_contract.scalar_one_or_none() or existing_special.scalar_one_or_none():
return
result = await db.execute(select(Study).order_by(Study.created_at.asc()))
study = result.scalars().first()
if not study:
study = Study(
id=uuid.uuid4(),
code="DEMO-CTMS",
name="示例临床试验项目",
sponsor="示例申办方",
status="ACTIVE",
)
db.add(study)
await db.commit()
await db.refresh(study)
result = await db.execute(select(Site).where(Site.study_id == study.id))
sites = result.scalars().all()
if len(sites) < 3:
for idx in range(3 - len(sites)):
site = Site(
id=uuid.uuid4(),
study_id=study.id,
name=f"示例中心 {len(sites) + idx + 1}",
city="上海",
pi_name="张医生",
contact="010-88888888",
)
db.add(site)
await db.commit()
result = await db.execute(select(Site).where(Site.study_id == study.id))
sites = result.scalars().all()
user_result = await db.execute(select(User).order_by(User.created_at.asc()))
uploader = user_result.scalars().first()
uploader_id = uploader.id if uploader else None
today = date.today()
for idx, site in enumerate(sites[:3], start=1):
contract = ContractFee(
id=uuid.uuid4(),
project_id=study.id,
center_id=site.id,
contract_amount=Decimal("120000.00") + Decimal(idx * 10000),
contract_cases=50 + idx * 5,
actual_cases=40 + idx * 4,
settlement_amount=Decimal("80000.00") + Decimal(idx * 5000),
final_payment_amount=Decimal("20000.00") + Decimal(idx * 2000),
)
db.add(contract)
await db.commit()
await db.refresh(contract)
payments = [
ContractFeePayment(
id=uuid.uuid4(),
contract_fee_id=contract.id,
seq=1,
amount=Decimal("40000.00") + Decimal(idx * 2000),
is_paid=True,
paid_date=today - timedelta(days=30),
is_verified=True,
verified_date=today - timedelta(days=20),
remark="首付款已核销",
),
ContractFeePayment(
id=uuid.uuid4(),
contract_fee_id=contract.id,
seq=2,
amount=Decimal("30000.00") + Decimal(idx * 1000),
is_paid=True,
paid_date=today - timedelta(days=10),
is_verified=False,
remark="等待核销",
),
ContractFeePayment(
id=uuid.uuid4(),
contract_fee_id=contract.id,
seq=3,
amount=Decimal("20000.00") + Decimal(idx * 1000),
is_paid=False,
remark="未打款",
),
]
db.add_all(payments)
await db.commit()
if uploader_id:
attachments = [
FeeAttachment(
id=uuid.uuid4(),
entity_type="contract_fee",
entity_id=contract.id,
file_type="contract",
filename=f"合同-{site.name}.pdf",
mime_type="application/pdf",
size=102400,
storage_key=None,
url="https://example.com/contract-demo.pdf",
uploaded_by=uploader_id,
),
FeeAttachment(
id=uuid.uuid4(),
entity_type="contract_fee",
entity_id=contract.id,
file_type="voucher",
filename=f"凭证-{site.name}.pdf",
mime_type="application/pdf",
size=20480,
storage_key=None,
url="https://example.com/voucher-demo.pdf",
uploaded_by=uploader_id,
),
]
db.add_all(attachments)
await db.commit()
categories = ["travel", "meal", "meeting", "supplies", "other"]
for idx, category in enumerate(categories):
for seq in range(2):
expense = SpecialExpense(
id=uuid.uuid4(),
project_id=study.id,
center_id=sites[seq % len(sites)].id if sites else None,
category=category,
amount=Decimal("800.00") + Decimal(seq * 150),
happen_date=today - timedelta(days=5 * (seq + 1)),
description=f"{category} 费用示例",
is_paid=seq % 2 == 0,
paid_date=today - timedelta(days=3 * (seq + 1)) if seq % 2 == 0 else None,
is_verified=seq == 0,
verified_date=today - timedelta(days=2 * (seq + 1)) if seq == 0 else None,
created_by=uploader_id,
)
db.add(expense)
await db.commit()
result = await db.execute(select(SpecialExpense).where(SpecialExpense.project_id == study.id))
expenses = result.scalars().all()
if uploader_id:
for expense in expenses[:3]:
attachment = FeeAttachment(
id=uuid.uuid4(),
entity_type="special_expense",
entity_id=expense.id,
file_type="invoice",
filename=f"发票-{expense.id}.pdf",
mime_type="application/pdf",
size=40960,
storage_key=None,
url="https://example.com/invoice-demo.pdf",
uploaded_by=uploader_id,
)
db.add(attachment)
await db.commit()
-150
View File
@@ -1,150 +0,0 @@
"""
项目概览示例数据注入脚本
运行方式:
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())
+1 -4
View File
@@ -43,9 +43,8 @@ class _DummyEngine:
@pytest.mark.asyncio
async def test_development_startup_does_not_seed_demo_fees(monkeypatch):
async def test_development_startup_does_not_seed_business_data(monkeypatch):
ensure_admin_exists = AsyncMock()
seed_demo_fees = AsyncMock()
create_all = AsyncMock()
legacy_pk = AsyncMock()
scheduler_task = _DummyTask()
@@ -63,7 +62,6 @@ async def test_development_startup_does_not_seed_demo_fees(monkeypatch):
monkeypatch.setattr(main.settings, "ENV", "development")
monkeypatch.setattr(main, "ensure_admin_exists", ensure_admin_exists)
monkeypatch.setattr(main, "seed_demo_fees", seed_demo_fees, raising=False)
monkeypatch.setattr(main, "run_daily_lost_visit_job", fake_scheduler)
monkeypatch.setattr(main.asyncio, "create_task", fake_create_task)
monkeypatch.setattr(main, "engine", _DummyEngine())
@@ -75,4 +73,3 @@ async def test_development_startup_does_not_seed_demo_fees(monkeypatch):
pass
ensure_admin_exists.assert_awaited_once()
seed_demo_fees.assert_not_awaited()