立项配置版本管理优化(回滚逻辑、UI、下载功能)
This commit is contained in:
+445
-12
@@ -1,6 +1,6 @@
|
||||
import uuid
|
||||
import re
|
||||
from datetime import date
|
||||
from datetime import date, datetime
|
||||
from collections import Counter
|
||||
from typing import Any
|
||||
|
||||
@@ -27,7 +27,10 @@ from app.schemas.member import StudyMemberCreate
|
||||
from app.schemas.study_setup_config import (
|
||||
ProjectPublishSnapshot,
|
||||
SetupProjectionSummary,
|
||||
StudySetupConfigCheckoutBranch,
|
||||
StudySetupConfigData,
|
||||
StudySetupConfigDraftAction,
|
||||
StudySetupConfigMergeToMain,
|
||||
StudySetupConfigPublish,
|
||||
StudySetupConfigRead,
|
||||
StudySetupConfigRollback,
|
||||
@@ -295,6 +298,8 @@ def _to_setup_config_read(
|
||||
*,
|
||||
saved_by_name: str | None = None,
|
||||
published_by_name: str | None = None,
|
||||
current_published_version_label: str | None = None,
|
||||
active_branch_base_version_label: str | None = None,
|
||||
projection_status: str | None = None,
|
||||
projection_summary: SetupProjectionSummary | None = None,
|
||||
) -> StudySetupConfigRead:
|
||||
@@ -302,6 +307,11 @@ def _to_setup_config_read(
|
||||
id=record.id,
|
||||
study_id=record.study_id,
|
||||
version=record.version,
|
||||
current_branch_name=(getattr(record, "current_branch_name", None) or "main"),
|
||||
current_published_version_id=getattr(record, "current_published_version_id", None),
|
||||
current_published_version_label=current_published_version_label,
|
||||
active_branch_base_version_id=getattr(record, "active_branch_base_version_id", None),
|
||||
active_branch_base_version_label=active_branch_base_version_label,
|
||||
data=StudySetupConfigData.model_validate(record.config or {}),
|
||||
publish_status=record.publish_status or "DRAFT",
|
||||
published_data=StudySetupConfigData.model_validate(record.published_config) if record.published_config else None,
|
||||
@@ -363,15 +373,42 @@ def _ensure_study_timeline_valid(
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="访视窗口开始偏移不能晚于结束偏移")
|
||||
|
||||
|
||||
def _to_setup_config_version_read(record, *, published_by_name: str | None = None) -> StudySetupConfigVersionRead:
|
||||
def _resolve_version_label(record: Any) -> str:
|
||||
branch_name = (getattr(record, "branch_name", None) or "main").strip() or "main"
|
||||
display_version = int(getattr(record, "display_version", 0) or 0)
|
||||
branch_seq = int(getattr(record, "branch_seq", 1) or 1)
|
||||
if branch_name == "main":
|
||||
return getattr(record, "version_label", None) or f"v{display_version}"
|
||||
return getattr(record, "version_label", None) or f"v{display_version}.{branch_seq}"
|
||||
|
||||
|
||||
def _to_setup_config_version_read(
|
||||
record,
|
||||
*,
|
||||
published_by_name: str | None = None,
|
||||
is_current_published: bool = False,
|
||||
is_active_draft_base: bool = False,
|
||||
) -> StudySetupConfigVersionRead:
|
||||
return StudySetupConfigVersionRead(
|
||||
id=record.id,
|
||||
study_id=record.study_id,
|
||||
study_setup_config_id=record.study_setup_config_id,
|
||||
version=record.version,
|
||||
display_version=record.display_version,
|
||||
branch_name=(record.branch_name or "main"),
|
||||
branch_seq=record.branch_seq or 1,
|
||||
version_label=_resolve_version_label(record),
|
||||
source_version=record.source_version,
|
||||
parent_version_id=record.parent_version_id,
|
||||
merged_from_version_id=record.merged_from_version_id,
|
||||
config=StudySetupConfigData.model_validate(record.config or {}),
|
||||
published_project_snapshot=(
|
||||
ProjectPublishSnapshot.model_validate(record.published_project_snapshot)
|
||||
if record.published_project_snapshot
|
||||
else None
|
||||
),
|
||||
is_current_published=is_current_published,
|
||||
is_active_draft_base=is_active_draft_base,
|
||||
published_by=record.published_by,
|
||||
published_by_name=published_by_name,
|
||||
published_at=record.published_at,
|
||||
@@ -379,6 +416,89 @@ def _to_setup_config_version_read(record, *, published_by_name: str | None = Non
|
||||
)
|
||||
|
||||
|
||||
def _resolve_current_published_snapshot_version(
|
||||
versions: list[Any],
|
||||
setup_record: Any | None,
|
||||
) -> int | None:
|
||||
if not setup_record:
|
||||
return None
|
||||
current_published_snapshot_id = getattr(setup_record, "current_published_version_id", None)
|
||||
if current_published_snapshot_id:
|
||||
matched = next((item for item in versions if item.id == current_published_snapshot_id), None)
|
||||
if matched:
|
||||
return matched.version
|
||||
|
||||
published_config = setup_record.published_config or {}
|
||||
if not published_config:
|
||||
return None
|
||||
|
||||
best_score = -1
|
||||
best_version: int | None = None
|
||||
for item in versions:
|
||||
if (item.config or {}) != published_config:
|
||||
continue
|
||||
score = 1
|
||||
if setup_record.published_at and item.published_at == setup_record.published_at:
|
||||
score += 2
|
||||
if setup_record.published_by and item.published_by == setup_record.published_by:
|
||||
score += 1
|
||||
if score > best_score or (score == best_score and (best_version is None or item.version > best_version)):
|
||||
best_score = score
|
||||
best_version = item.version
|
||||
return best_version
|
||||
|
||||
|
||||
def _resolve_current_published_snapshot_label(
|
||||
versions: list[Any],
|
||||
setup_record: Any | None,
|
||||
) -> str | None:
|
||||
if not setup_record:
|
||||
return None
|
||||
current_published_snapshot_id = getattr(setup_record, "current_published_version_id", None)
|
||||
if current_published_snapshot_id:
|
||||
matched = next((item for item in versions if item.id == current_published_snapshot_id), None)
|
||||
if matched:
|
||||
return _resolve_version_label(matched)
|
||||
|
||||
version = _resolve_current_published_snapshot_version(versions, setup_record)
|
||||
if version is None:
|
||||
return None
|
||||
matched = next((item for item in versions if item.version == version), None)
|
||||
if not matched:
|
||||
return None
|
||||
return _resolve_version_label(matched)
|
||||
|
||||
|
||||
def _resolve_active_branch_base_snapshot_version(
|
||||
versions: list[Any],
|
||||
setup_record: Any | None,
|
||||
) -> int | None:
|
||||
if not setup_record:
|
||||
return None
|
||||
base_snapshot_id = getattr(setup_record, "active_branch_base_version_id", None)
|
||||
if not base_snapshot_id:
|
||||
return None
|
||||
matched = next((item for item in versions if item.id == base_snapshot_id), None)
|
||||
if not matched:
|
||||
return None
|
||||
return matched.version
|
||||
|
||||
|
||||
def _resolve_active_branch_base_snapshot_label(
|
||||
versions: list[Any],
|
||||
setup_record: Any | None,
|
||||
) -> str | None:
|
||||
if not setup_record:
|
||||
return None
|
||||
base_snapshot_id = getattr(setup_record, "active_branch_base_version_id", None)
|
||||
if not base_snapshot_id:
|
||||
return None
|
||||
matched = next((item for item in versions if item.id == base_snapshot_id), None)
|
||||
if not matched:
|
||||
return None
|
||||
return _resolve_version_label(matched)
|
||||
|
||||
|
||||
_SETUP_MODULE_KEYS = (
|
||||
"projectMilestones",
|
||||
"enrollmentPlan",
|
||||
@@ -777,7 +897,14 @@ async def get_study_setup_config(
|
||||
if user:
|
||||
published_by_name = user.full_name or user.username or user.email
|
||||
|
||||
return _to_setup_config_read(record, saved_by_name=saved_by_name, published_by_name=published_by_name)
|
||||
versions = await study_setup_config_crud.list_versions(db, study_id)
|
||||
return _to_setup_config_read(
|
||||
record,
|
||||
saved_by_name=saved_by_name,
|
||||
published_by_name=published_by_name,
|
||||
current_published_version_label=_resolve_current_published_snapshot_label(versions, record),
|
||||
active_branch_base_version_label=_resolve_active_branch_base_snapshot_label(versions, record),
|
||||
)
|
||||
|
||||
|
||||
@router.put(
|
||||
@@ -843,7 +970,14 @@ async def upsert_study_setup_config(
|
||||
user = await user_crud.get_by_id(db, record.published_by)
|
||||
if user:
|
||||
published_by_name = user.full_name or user.username or user.email
|
||||
return _to_setup_config_read(record, saved_by_name=saved_by_name, published_by_name=published_by_name)
|
||||
versions = await study_setup_config_crud.list_versions(db, study_id)
|
||||
return _to_setup_config_read(
|
||||
record,
|
||||
saved_by_name=saved_by_name,
|
||||
published_by_name=published_by_name,
|
||||
current_published_version_label=_resolve_current_published_snapshot_label(versions, record),
|
||||
active_branch_base_version_label=_resolve_active_branch_base_snapshot_label(versions, record),
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -864,6 +998,10 @@ async def publish_study_setup_config(
|
||||
record = await study_setup_config_crud.get_by_study(db, study_id)
|
||||
if not record:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="请先保存立项配置草稿")
|
||||
current_project_snapshot_for_publish = _build_project_publish_snapshot(study).model_dump(mode="json")
|
||||
force_create_snapshot = bool(
|
||||
(record.published_project_snapshot or {}) != current_project_snapshot_for_publish
|
||||
)
|
||||
|
||||
sites = await site_crud.list_by_study(db, study_id, skip=0, limit=1000, include_inactive=True)
|
||||
site_lookup = {str(site.id): site.name or "" for site in sites}
|
||||
@@ -879,6 +1017,7 @@ async def publish_study_setup_config(
|
||||
study_id,
|
||||
expected_version=payload.expected_version,
|
||||
published_by=current_user.id,
|
||||
force_create_snapshot=force_create_snapshot,
|
||||
auto_commit=False,
|
||||
)
|
||||
if conflict:
|
||||
@@ -892,7 +1031,7 @@ async def publish_study_setup_config(
|
||||
projection = await apply_setup_projection_on_publish(
|
||||
db,
|
||||
study_id=study_id,
|
||||
setup_data=StudySetupConfigData.model_validate(published.config or {}),
|
||||
setup_data=StudySetupConfigData.model_validate(published.published_config or {}),
|
||||
operator_id=current_user.id,
|
||||
)
|
||||
projection_summary = SetupProjectionSummary(
|
||||
@@ -907,7 +1046,14 @@ async def publish_study_setup_config(
|
||||
)
|
||||
study_after_publish = await study_crud.get(db, study_id)
|
||||
if study_after_publish:
|
||||
published.published_project_snapshot = _build_project_publish_snapshot(study_after_publish).model_dump(mode="json")
|
||||
project_snapshot = _build_project_publish_snapshot(study_after_publish).model_dump(mode="json")
|
||||
published.published_project_snapshot = project_snapshot
|
||||
await study_setup_config_crud.set_version_project_snapshot(
|
||||
db,
|
||||
study_id,
|
||||
version=published.version,
|
||||
project_snapshot=project_snapshot,
|
||||
)
|
||||
await audit_crud.log_action(
|
||||
db,
|
||||
study_id=study_id,
|
||||
@@ -926,6 +1072,10 @@ async def publish_study_setup_config(
|
||||
operator_role=current_user.role,
|
||||
auto_commit=False,
|
||||
)
|
||||
published.config = study_setup_config_crud.empty_draft_payload()
|
||||
published.active_branch_base_version_id = None
|
||||
published.saved_by = current_user.id
|
||||
published.updated_at = datetime.utcnow()
|
||||
await db.commit()
|
||||
except Exception:
|
||||
await db.rollback()
|
||||
@@ -933,10 +1083,13 @@ async def publish_study_setup_config(
|
||||
|
||||
operator_name = current_user.full_name or current_user.username or current_user.email
|
||||
await db.refresh(published)
|
||||
versions = await study_setup_config_crud.list_versions(db, study_id)
|
||||
return _to_setup_config_read(
|
||||
published,
|
||||
saved_by_name=operator_name,
|
||||
published_by_name=operator_name,
|
||||
current_published_version_label=_resolve_current_published_snapshot_label(versions, published),
|
||||
active_branch_base_version_label=_resolve_active_branch_base_snapshot_label(versions, published),
|
||||
projection_status=projection.status,
|
||||
projection_summary=projection_summary,
|
||||
)
|
||||
@@ -956,6 +1109,9 @@ async def list_study_setup_config_versions(
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在")
|
||||
|
||||
versions = await study_setup_config_crud.list_versions(db, study_id)
|
||||
current_setup = await study_setup_config_crud.get_by_study(db, study_id)
|
||||
current_published_version = _resolve_current_published_snapshot_version(versions, current_setup)
|
||||
active_draft_base_version = _resolve_active_branch_base_snapshot_version(versions, current_setup)
|
||||
user_ids = {item.published_by for item in versions if item.published_by}
|
||||
name_map: dict[uuid.UUID, str] = {}
|
||||
for user_id in user_ids:
|
||||
@@ -963,7 +1119,12 @@ async def list_study_setup_config_versions(
|
||||
if user:
|
||||
name_map[user_id] = user.full_name or user.username or user.email
|
||||
return [
|
||||
_to_setup_config_version_read(item, published_by_name=name_map.get(item.published_by) if item.published_by else None)
|
||||
_to_setup_config_version_read(
|
||||
item,
|
||||
published_by_name=name_map.get(item.published_by) if item.published_by else None,
|
||||
is_current_published=item.version == current_published_version,
|
||||
is_active_draft_base=item.version == active_draft_base_version,
|
||||
)
|
||||
for item in versions
|
||||
]
|
||||
|
||||
@@ -982,6 +1143,9 @@ async def rollback_study_setup_config(
|
||||
study = await study_crud.get(db, study_id)
|
||||
if not study:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在")
|
||||
versions_before = await study_setup_config_crud.list_versions(db, study_id)
|
||||
target_label_map = {item.version: (item.version_label or f"v{item.display_version}") for item in versions_before}
|
||||
target_label = target_label_map.get(payload.target_version, f"v{payload.target_version}")
|
||||
|
||||
record, conflict, target_missing = await study_setup_config_crud.rollback_to_version(
|
||||
db,
|
||||
@@ -1003,7 +1167,7 @@ async def rollback_study_setup_config(
|
||||
entity_type="study_setup_config",
|
||||
entity_id=record.id,
|
||||
action="ROLLBACK_SETUP_CONFIG",
|
||||
detail=f"立项配置已回滚到发布版本 v{payload.target_version}",
|
||||
detail=f"立项配置已回滚并替换当前发布为 {target_label},草稿分支已切换到对应分支基线",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
)
|
||||
@@ -1013,7 +1177,273 @@ async def rollback_study_setup_config(
|
||||
user = await user_crud.get_by_id(db, record.published_by)
|
||||
if user:
|
||||
published_by_name = user.full_name or user.username or user.email
|
||||
return _to_setup_config_read(record, saved_by_name=operator_name, published_by_name=published_by_name)
|
||||
versions = await study_setup_config_crud.list_versions(db, study_id)
|
||||
return _to_setup_config_read(
|
||||
record,
|
||||
saved_by_name=operator_name,
|
||||
published_by_name=published_by_name,
|
||||
current_published_version_label=_resolve_current_published_snapshot_label(versions, record),
|
||||
active_branch_base_version_label=_resolve_active_branch_base_snapshot_label(versions, record),
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{study_id}/setup-config/draft/checkout-branch",
|
||||
response_model=StudySetupConfigRead,
|
||||
dependencies=[Depends(require_study_roles(["PM"])), Depends(require_study_not_locked())],
|
||||
)
|
||||
async def checkout_study_setup_config_branch_draft(
|
||||
study_id: uuid.UUID,
|
||||
payload: StudySetupConfigCheckoutBranch,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> StudySetupConfigRead:
|
||||
study = await study_crud.get(db, study_id)
|
||||
if not study:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在")
|
||||
|
||||
versions_before = await study_setup_config_crud.list_versions(db, study_id)
|
||||
target_label_map = {item.version: (item.version_label or f"v{item.display_version}") for item in versions_before}
|
||||
target_label = target_label_map.get(payload.target_version, f"v{payload.target_version}")
|
||||
|
||||
record, conflict, target_missing = await study_setup_config_crud.checkout_branch_draft_from_version(
|
||||
db,
|
||||
study_id,
|
||||
expected_version=payload.expected_version,
|
||||
target_version=payload.target_version,
|
||||
saved_by=current_user.id,
|
||||
)
|
||||
if conflict:
|
||||
_raise_conflict_error()
|
||||
if target_missing:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="目标版本不存在")
|
||||
if not record:
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="切换分支草稿失败")
|
||||
|
||||
await audit_crud.log_action(
|
||||
db,
|
||||
study_id=study_id,
|
||||
entity_type="study_setup_config",
|
||||
entity_id=record.id,
|
||||
action="CHECKOUT_SETUP_CONFIG_BRANCH_DRAFT",
|
||||
detail=f"立项配置草稿已切换到 {target_label} 对应分支",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
)
|
||||
operator_name = current_user.full_name or current_user.username or current_user.email
|
||||
published_by_name = None
|
||||
if record.published_by:
|
||||
user = await user_crud.get_by_id(db, record.published_by)
|
||||
if user:
|
||||
published_by_name = user.full_name or user.username or user.email
|
||||
versions = await study_setup_config_crud.list_versions(db, study_id)
|
||||
return _to_setup_config_read(
|
||||
record,
|
||||
saved_by_name=operator_name,
|
||||
published_by_name=published_by_name,
|
||||
current_published_version_label=_resolve_current_published_snapshot_label(versions, record),
|
||||
active_branch_base_version_label=_resolve_active_branch_base_snapshot_label(versions, record),
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{study_id}/setup-config/draft/clear",
|
||||
response_model=StudySetupConfigRead,
|
||||
dependencies=[Depends(require_study_roles(["PM"])), Depends(require_study_not_locked())],
|
||||
)
|
||||
async def clear_study_setup_config_draft(
|
||||
study_id: uuid.UUID,
|
||||
payload: StudySetupConfigDraftAction,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> StudySetupConfigRead:
|
||||
study = await study_crud.get(db, study_id)
|
||||
if not study:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在")
|
||||
|
||||
record, conflict = await study_setup_config_crud.clear_draft(
|
||||
db,
|
||||
study_id,
|
||||
expected_version=payload.expected_version,
|
||||
saved_by=current_user.id,
|
||||
)
|
||||
if conflict:
|
||||
_raise_conflict_error()
|
||||
if not record:
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="清空草稿失败")
|
||||
|
||||
await audit_crud.log_action(
|
||||
db,
|
||||
study_id=study_id,
|
||||
entity_type="study_setup_config",
|
||||
entity_id=record.id,
|
||||
action="CLEAR_SETUP_CONFIG_DRAFT",
|
||||
detail="立项配置草稿已清空",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
)
|
||||
operator_name = current_user.full_name or current_user.username or current_user.email
|
||||
published_by_name = None
|
||||
if record.published_by:
|
||||
user = await user_crud.get_by_id(db, record.published_by)
|
||||
if user:
|
||||
published_by_name = user.full_name or user.username or user.email
|
||||
versions = await study_setup_config_crud.list_versions(db, study_id)
|
||||
return _to_setup_config_read(
|
||||
record,
|
||||
saved_by_name=operator_name,
|
||||
published_by_name=published_by_name,
|
||||
current_published_version_label=_resolve_current_published_snapshot_label(versions, record),
|
||||
active_branch_base_version_label=_resolve_active_branch_base_snapshot_label(versions, record),
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{study_id}/setup-config/draft/refill",
|
||||
response_model=StudySetupConfigRead,
|
||||
dependencies=[Depends(require_study_roles(["PM"])), Depends(require_study_not_locked())],
|
||||
)
|
||||
async def refill_study_setup_config_draft(
|
||||
study_id: uuid.UUID,
|
||||
payload: StudySetupConfigDraftAction,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> StudySetupConfigRead:
|
||||
study = await study_crud.get(db, study_id)
|
||||
if not study:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在")
|
||||
|
||||
record, conflict, no_published = await study_setup_config_crud.refill_draft_from_published(
|
||||
db,
|
||||
study_id,
|
||||
expected_version=payload.expected_version,
|
||||
saved_by=current_user.id,
|
||||
)
|
||||
if conflict:
|
||||
_raise_conflict_error()
|
||||
if no_published:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="当前无已发布版本,无法回填草稿")
|
||||
if not record:
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="回填草稿失败")
|
||||
|
||||
await audit_crud.log_action(
|
||||
db,
|
||||
study_id=study_id,
|
||||
entity_type="study_setup_config",
|
||||
entity_id=record.id,
|
||||
action="REFILL_SETUP_CONFIG_DRAFT",
|
||||
detail="立项配置草稿已从当前发布版本一键回填",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
)
|
||||
operator_name = current_user.full_name or current_user.username or current_user.email
|
||||
published_by_name = None
|
||||
if record.published_by:
|
||||
user = await user_crud.get_by_id(db, record.published_by)
|
||||
if user:
|
||||
published_by_name = user.full_name or user.username or user.email
|
||||
versions = await study_setup_config_crud.list_versions(db, study_id)
|
||||
return _to_setup_config_read(
|
||||
record,
|
||||
saved_by_name=operator_name,
|
||||
published_by_name=published_by_name,
|
||||
current_published_version_label=_resolve_current_published_snapshot_label(versions, record),
|
||||
active_branch_base_version_label=_resolve_active_branch_base_snapshot_label(versions, record),
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{study_id}/setup-config/merge-main",
|
||||
response_model=StudySetupConfigRead,
|
||||
dependencies=[Depends(require_study_roles(["PM"])), Depends(require_study_not_locked())],
|
||||
)
|
||||
async def merge_study_setup_config_to_main(
|
||||
study_id: uuid.UUID,
|
||||
payload: StudySetupConfigMergeToMain,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> StudySetupConfigRead:
|
||||
study = await study_crud.get(db, study_id)
|
||||
if not study:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在")
|
||||
versions_before = await study_setup_config_crud.list_versions(db, study_id)
|
||||
source_label_map = {item.version: (item.version_label or f"v{item.display_version}") for item in versions_before}
|
||||
source_label = source_label_map.get(payload.source_version, f"v{payload.source_version}")
|
||||
|
||||
merged, conflict, source_missing = await study_setup_config_crud.merge_to_main(
|
||||
db,
|
||||
study_id,
|
||||
expected_version=payload.expected_version,
|
||||
source_version=payload.source_version,
|
||||
merged_by=current_user.id,
|
||||
auto_commit=False,
|
||||
)
|
||||
if conflict:
|
||||
await db.rollback()
|
||||
_raise_conflict_error()
|
||||
if source_missing:
|
||||
await db.rollback()
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="目标版本不存在")
|
||||
if not merged:
|
||||
await db.rollback()
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="合并主分支失败")
|
||||
|
||||
try:
|
||||
projection = await apply_setup_projection_on_publish(
|
||||
db,
|
||||
study_id=study_id,
|
||||
setup_data=StudySetupConfigData.model_validate(merged.published_config or {}),
|
||||
operator_id=current_user.id,
|
||||
)
|
||||
projection_summary = SetupProjectionSummary(
|
||||
study_updated=projection.study_updated,
|
||||
site_updated_count=projection.site_updated_count,
|
||||
site_skipped_count=projection.site_skipped_count,
|
||||
warnings=projection.warnings,
|
||||
skipped_items=[{"site_id": item.site_id, "reason": item.reason} for item in projection.skipped_items],
|
||||
)
|
||||
study_after_publish = await study_crud.get(db, study_id)
|
||||
if study_after_publish:
|
||||
project_snapshot = _build_project_publish_snapshot(study_after_publish).model_dump(mode="json")
|
||||
merged.published_project_snapshot = project_snapshot
|
||||
await study_setup_config_crud.set_version_project_snapshot(
|
||||
db,
|
||||
study_id,
|
||||
version=merged.version,
|
||||
project_snapshot=project_snapshot,
|
||||
)
|
||||
await audit_crud.log_action(
|
||||
db,
|
||||
study_id=study_id,
|
||||
entity_type="study_setup_config",
|
||||
entity_id=merged.id,
|
||||
action="MERGE_SETUP_CONFIG_TO_MAIN",
|
||||
detail=f"已将发布版本 {source_label} 合并到主分支并生成新主版本",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
auto_commit=False,
|
||||
)
|
||||
merged.config = study_setup_config_crud.empty_draft_payload()
|
||||
merged.active_branch_base_version_id = None
|
||||
merged.saved_by = current_user.id
|
||||
merged.updated_at = datetime.utcnow()
|
||||
await db.commit()
|
||||
except Exception:
|
||||
await db.rollback()
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="合并主分支失败:联动同步异常")
|
||||
|
||||
operator_name = current_user.full_name or current_user.username or current_user.email
|
||||
await db.refresh(merged)
|
||||
versions = await study_setup_config_crud.list_versions(db, study_id)
|
||||
return _to_setup_config_read(
|
||||
merged,
|
||||
saved_by_name=operator_name,
|
||||
published_by_name=operator_name,
|
||||
current_published_version_label=_resolve_current_published_snapshot_label(versions, merged),
|
||||
active_branch_base_version_label=_resolve_active_branch_base_snapshot_label(versions, merged),
|
||||
projection_status=projection.status,
|
||||
projection_summary=projection_summary,
|
||||
)
|
||||
|
||||
|
||||
@router.delete(
|
||||
@@ -1034,9 +1464,12 @@ async def delete_study_setup_config_version(
|
||||
versions = await study_setup_config_crud.list_versions(db, study_id)
|
||||
if not versions:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="目标版本不存在")
|
||||
latest_version = max(item.version for item in versions)
|
||||
if target_version == latest_version:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="最新发布版本不能删除")
|
||||
current_setup = await study_setup_config_crud.get_by_study(db, study_id)
|
||||
current_published_version = _resolve_current_published_snapshot_version(versions, current_setup)
|
||||
if current_published_version is None:
|
||||
current_published_version = max(item.version for item in versions)
|
||||
if target_version == current_published_version:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="当前发布版本不能删除")
|
||||
|
||||
deleted = await study_setup_config_crud.delete_version(
|
||||
db,
|
||||
|
||||
Reference in New Issue
Block a user