完善后台管理与测试支撑

1、为后台用户列表增加关键词和状态筛选,后端同步支持过滤与总数统计。

2、创建用户时要求填写初始密码,并补充前端校验和自动填充隔离。

3、优化项目管理列表角色展示、项目详情抽屉脏数据保护和审计详情抽屉关闭体验。

4、清理旧费用附件关联、统一测试模型注册,并更新迁移端点和注册筛选测试。
This commit is contained in:
Cheng Zhou
2026-06-04 11:12:40 +08:00
parent 90ca061db8
commit 41968384b5
17 changed files with 337 additions and 145 deletions
-20
View File
@@ -16,7 +16,6 @@ from app.models.document import Document
from app.models.document_version import DocumentVersion
from app.models.distribution import Distribution
from app.models.drug_shipment import DrugShipment
from app.models.fee_attachment import FeeAttachment
from app.models.finance import FinanceItem
from app.models.kickoff_meeting import KickoffMeeting
from app.models.precaution import Precaution
@@ -33,7 +32,6 @@ from app.models.visit import Visit
from app.schemas.site import SiteCreate, SiteUpdate
ATTACHMENT_ROOT = Path(__file__).resolve().parent.parent / "uploads"
FEE_ATTACHMENT_ROOT = ATTACHMENT_ROOT / "fees"
DOCUMENT_ATTACHMENT_ROOT = ATTACHMENT_ROOT / "documents"
@@ -239,24 +237,7 @@ async def delete_site_and_related(db: AsyncSession, site: Site) -> None:
)
)
fee_attachment_paths: list[str] = []
if contract_fee_ids:
fee_attachment_paths.extend(
(
await db.execute(
select(FeeAttachment.storage_key).where(
FeeAttachment.entity_type == "contract_fee",
FeeAttachment.entity_id.in_(contract_fee_ids),
)
)
).scalars().all()
)
await db.execute(
delete(FeeAttachment).where(
FeeAttachment.entity_type == "contract_fee",
FeeAttachment.entity_id.in_(contract_fee_ids),
)
)
await db.execute(delete(ContractFeePayment).where(ContractFeePayment.contract_fee_id.in_(contract_fee_ids)))
if distribution_ids:
@@ -311,7 +292,6 @@ async def delete_site_and_related(db: AsyncSession, site: Site) -> None:
await db.execute(delete(Site).where(Site.id == site_id))
await _remove_files(attachment_paths, ATTACHMENT_ROOT)
await _remove_files(fee_attachment_paths, FEE_ATTACHMENT_ROOT)
await _remove_files(version_file_paths, DOCUMENT_ATTACHMENT_ROOT)
await db.commit()
+37 -3
View File
@@ -3,7 +3,7 @@ from __future__ import annotations
import uuid
from typing import Sequence
from sqlalchemy import delete, func, select, update
from sqlalchemy import delete, func, or_, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.config import (
@@ -79,11 +79,45 @@ async def update_user(db: AsyncSession, user: User, user_in: UserUpdate) -> User
return user
async def list_users(db: AsyncSession, skip: int = 0, limit: int = 100) -> Sequence[User]:
result = await db.execute(select(User).offset(skip).limit(limit))
def _apply_user_filters(query, *, keyword: str | None = None, status: UserStatus | None = None):
if keyword:
pattern = f"%{keyword.strip()}%"
query = query.where(
or_(
User.email.ilike(pattern),
User.full_name.ilike(pattern),
User.clinical_department.ilike(pattern),
)
)
if status is not None:
query = query.where(User.status == status)
return query
async def list_users(
db: AsyncSession,
skip: int = 0,
limit: int = 100,
*,
keyword: str | None = None,
status: UserStatus | None = None,
) -> Sequence[User]:
query = _apply_user_filters(select(User), keyword=keyword, status=status)
result = await db.execute(query.order_by(User.created_at.desc()).offset(skip).limit(limit))
return result.scalars().all()
async def count_users(
db: AsyncSession,
*,
keyword: str | None = None,
status: UserStatus | None = None,
) -> int:
query = _apply_user_filters(select(func.count()).select_from(User), keyword=keyword, status=status)
result = await db.execute(query)
return int(result.scalar_one() or 0)
async def list_active_member_candidates_for_study(
db: AsyncSession,
study_id: uuid.UUID,