立项配置数据入库、审计日志数据入库、编辑页UI界面美化、设备管理内容补充、审计日志显示优化

This commit is contained in:
Cheng Zhou
2026-02-27 16:16:26 +08:00
parent fd7e3fc948
commit db2d38edbc
48 changed files with 2936 additions and 909 deletions
+71
View File
@@ -0,0 +1,71 @@
import uuid
from typing import Sequence
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.material_equipment import MaterialEquipment
from app.schemas.material_equipment import MaterialEquipmentCreate, MaterialEquipmentUpdate
async def create_equipment(
db: AsyncSession,
study_id: uuid.UUID,
equipment_in: MaterialEquipmentCreate,
created_by: uuid.UUID | None,
) -> MaterialEquipment:
item = MaterialEquipment(
study_id=study_id,
name=equipment_in.name,
spec_model=equipment_in.spec_model,
unit=equipment_in.unit,
brand=equipment_in.brand,
origin=equipment_in.origin,
production_permit_file_name=equipment_in.production_permit_file_name,
tech_index_file_name=equipment_in.tech_index_file_name,
need_calibration=equipment_in.need_calibration,
calibration_cycle_days=equipment_in.calibration_cycle_days,
created_by=created_by,
)
db.add(item)
await db.commit()
await db.refresh(item)
return item
async def get_equipment(db: AsyncSession, equipment_id: uuid.UUID) -> MaterialEquipment | None:
result = await db.execute(select(MaterialEquipment).where(MaterialEquipment.id == equipment_id))
return result.scalar_one_or_none()
async def list_equipments(
db: AsyncSession,
study_id: uuid.UUID,
name: str | None = None,
skip: int = 0,
limit: int = 100,
) -> Sequence[MaterialEquipment]:
stmt = select(MaterialEquipment).where(MaterialEquipment.study_id == study_id)
if name:
stmt = stmt.where(MaterialEquipment.name.ilike(f"%{name}%"))
stmt = stmt.order_by(MaterialEquipment.created_at.desc()).offset(skip).limit(limit)
result = await db.execute(stmt)
return result.scalars().all()
async def update_equipment(
db: AsyncSession,
equipment: MaterialEquipment,
equipment_in: MaterialEquipmentUpdate,
) -> MaterialEquipment:
update_data = equipment_in.model_dump(exclude_unset=True)
for key, value in update_data.items():
setattr(equipment, key, value)
await db.commit()
await db.refresh(equipment)
return equipment
async def delete_equipment(db: AsyncSession, equipment: MaterialEquipment) -> None:
await db.delete(equipment)
await db.commit()
+54
View File
@@ -0,0 +1,54 @@
import uuid
from typing import Sequence
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.milestone import Milestone
from app.schemas.project_milestone import ProjectMilestoneUpdate
PROJECT_MILESTONE_TYPE = "SETUP_PROJECT_MILESTONE"
async def list_project_milestones(db: AsyncSession, study_id: uuid.UUID) -> Sequence[Milestone]:
stmt = (
select(Milestone)
.where(
Milestone.study_id == study_id,
Milestone.type == PROJECT_MILESTONE_TYPE,
)
.order_by(Milestone.planned_date.asc().nulls_last(), Milestone.created_at.asc())
)
result = await db.execute(stmt)
return result.scalars().all()
async def get_project_milestone(db: AsyncSession, study_id: uuid.UUID, milestone_id: uuid.UUID) -> Milestone | None:
result = await db.execute(
select(Milestone).where(
Milestone.id == milestone_id,
Milestone.study_id == study_id,
Milestone.type == PROJECT_MILESTONE_TYPE,
)
)
return result.scalar_one_or_none()
async def update_project_milestone(db: AsyncSession, milestone: Milestone, data: ProjectMilestoneUpdate) -> Milestone:
milestone.adjusted_start_date = data.adjusted_start_date
milestone.adjusted_end_date = data.adjusted_end_date
milestone.actual_start_date = data.actual_start_date
milestone.actual_end_date = data.actual_end_date
milestone.actual_date = data.actual_end_date
milestone.notes = data.notes
if milestone.actual_end_date:
milestone.status = "DONE"
elif milestone.actual_start_date:
milestone.status = "IN_PROGRESS"
else:
milestone.status = "NOT_STARTED"
await db.commit()
await db.refresh(milestone)
return milestone
+2
View File
@@ -112,6 +112,7 @@ async def delete(db: AsyncSession, study_id: uuid.UUID) -> None:
from app.models.document import Document
from app.models.attachment import Attachment
from app.models.drug_shipment import DrugShipment
from app.models.material_equipment import MaterialEquipment
from app.models.training_authorization import TrainingAuthorization
from app.models.startup_feasibility import StartupFeasibility
from app.models.startup_ethics import StartupEthics
@@ -145,6 +146,7 @@ async def delete(db: AsyncSession, study_id: uuid.UUID) -> None:
# 6. 删除药物配送
await db.execute(sa_delete(DrugShipment).where(DrugShipment.study_id == study_id))
await db.execute(sa_delete(MaterialEquipment).where(MaterialEquipment.study_id == study_id))
# 7. 删除附件和文档
await db.execute(sa_delete(Attachment).where(Attachment.study_id == study_id))
+8 -1
View File
@@ -28,6 +28,7 @@ async def upsert(
expected_version: int | None,
data: StudySetupConfigData | dict,
saved_by: uuid.UUID | None,
force_draft: bool = False,
) -> tuple[StudySetupConfig | None, bool]:
existing = await get_by_study(db, study_id)
payload = _to_storage(data)
@@ -37,7 +38,13 @@ async def upsert(
existing.version = existing.version + 1
existing.config = payload
existing.saved_by = saved_by
existing.publish_status = "PUBLISHED" if existing.published_config == payload else "DRAFT"
if force_draft:
existing.publish_status = "DRAFT"
elif existing.publish_status == "DRAFT":
# Keep draft state sticky until explicit publish API is called.
existing.publish_status = "DRAFT"
else:
existing.publish_status = "PUBLISHED" if existing.published_config == payload else "DRAFT"
existing.updated_at = datetime.utcnow()
await db.commit()
await db.refresh(existing)