feat: 清理示例数据并统一空态体验
This commit is contained in:
@@ -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
|
||||
@@ -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()
|
||||
@@ -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())
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user