同步全局协作台账与工作台界面优化
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled
Storage Persistence Guard / storage-persistence-audit (push) Has been cancelled
Client Quality Gates / Shared client and Web (pull_request) Has been cancelled
Client Quality Gates / macOS Desktop (pull_request) Has been cancelled
Storage Persistence Guard / storage-persistence-audit (pull_request) Has been cancelled

工作台新增与项目平级的全局协作台账,复用共享库在线协作能力。两本台账使用合同台账明细表、临床运营项目编号的空白模板,支持独立账号授权、在线编辑和历史版本保留,禁止删除整个台账及历史版本。

台账管理拆分为信息维护、访问与权限、历史版本三个独立入口;后两者与项目共享组件。统一项目和台账的卡片高度、数量徽标与标题布局,缩小工作台顶部留白,优化信息维护弹窗,并修复空用途说明和版本命名的默认提示。同步现有飞线图白色残影修复,以及在线文档的字体构建支持。

已验证:前端五百五十四项测试、后端台账与在线文档七十四项测试、类型检查、界面约束、运行时边界、桌面发布静态检查、网页构建和本地桌面应用构建均通过。数据库表结构升级已完成离线脚本生成检查,存储持久化检查通过。桌面凭据测试显式隔离构建环境,并覆盖使用默认服务器地址读取凭据的场景;在注入默认服务器地址的环境中完成全部前端测试。

提交包含必需的表结构升级和只有表头的初始模板;不包含本地台账业务记录、数据库导出、账号授权运行数据或上传目录中的历史文件。字体文件沿用部署方单独提供的方式。
This commit is contained in:
chengchengzhou7
2026-09-04 08:42:21 +08:00
committed by GitHub
parent 9c533b8c37
commit 6c45cef4b7
44 changed files with 4182 additions and 1819 deletions
+201
View File
@@ -0,0 +1,201 @@
import uuid
from fastapi import APIRouter, Depends, File, Form, Query, Response, UploadFile
from fastapi.responses import FileResponse
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.deps import get_current_user, get_db_session, require_roles
from app.schemas.collaboration import (
CollaborationEditorConfigRead, CollaborationExportRecord, CollaborationRevisionRead,
CollaborationEditRequestRead, CollaborationEditRequestResolve, CollaborationOwnershipTransferRequest,
CollaborationShareLinkRead, CollaborationShareLinkUpdate, CollaborationRevisionUpdate,
)
from app.schemas.ledger import (
LedgerAccessSettings, LedgerCandidateRead, LedgerMemberRead, LedgerMemberUpsert, LedgerRead, LedgerRestore, LedgerSettings,
)
from app.schemas.onlyoffice import OnlyOfficePreviewConfigRead
from app.services import collaboration_service, collaboration_share_service, ledger_service, onlyoffice_collaboration_service, onlyoffice_service
def no_store(response: Response):
response.headers["Cache-Control"] = "no-store"
router = APIRouter(dependencies=[Depends(no_store)])
@router.get("", response_model=list[LedgerRead])
async def list_ledgers(db: AsyncSession = Depends(get_db_session), user=Depends(get_current_user)):
return await ledger_service.list_ledgers(db, user)
@router.post("/initialize", response_model=list[LedgerRead])
async def initialize(db: AsyncSession = Depends(get_db_session), user=Depends(require_roles(["ADMIN"]))):
return await ledger_service.initialize_ledgers(db, user)
@router.get("/{ledger_id}", response_model=LedgerRead)
async def read(ledger_id: uuid.UUID, db: AsyncSession = Depends(get_db_session), user=Depends(get_current_user)):
item = await ledger_service.get_ledger(db, ledger_id, user)
return await ledger_service.read_ledger(db, item, user)
@router.patch("/{ledger_id}", response_model=LedgerRead)
async def update(ledger_id: uuid.UUID, payload: LedgerSettings, db: AsyncSession = Depends(get_db_session), user=Depends(get_current_user)):
item = await ledger_service.get_ledger(db, ledger_id, user, manage=True, lock=True)
return await ledger_service.update_settings(db, item, payload, user)
@router.post("/{ledger_id}/import", response_model=LedgerRead)
async def import_template(
ledger_id: uuid.UUID, file: UploadFile = File(...), generation: int = Form(..., ge=1),
db: AsyncSession = Depends(get_db_session), user=Depends(get_current_user),
):
item = await ledger_service.get_ledger(db, ledger_id, user, manage=True, lock=True)
return await ledger_service.import_content(db, item, file, generation, user)
@router.get("/{ledger_id}/members", response_model=list[LedgerMemberRead])
async def members(ledger_id: uuid.UUID, db: AsyncSession = Depends(get_db_session), user=Depends(get_current_user)):
item = await ledger_service.get_ledger(db, ledger_id, user, manage=True)
return await ledger_service.list_members(db, item)
@router.get("/{ledger_id}/candidates", response_model=list[LedgerCandidateRead])
async def candidates(
ledger_id: uuid.UUID, keyword: str = Query(default="", max_length=100),
db: AsyncSession = Depends(get_db_session), user=Depends(get_current_user),
):
await ledger_service.get_ledger(db, ledger_id, user, manage=True)
return await ledger_service.candidates(db, keyword)
@router.put("/{ledger_id}/members", status_code=204)
async def grant(
ledger_id: uuid.UUID, payload: LedgerMemberUpsert,
db: AsyncSession = Depends(get_db_session), user=Depends(get_current_user),
):
item = await ledger_service.get_ledger(db, ledger_id, user, manage=True, lock=True)
await ledger_service.set_member(db, item, payload, user)
@router.delete("/{ledger_id}/members/{user_id}", status_code=204)
async def revoke(
ledger_id: uuid.UUID, user_id: uuid.UUID,
db: AsyncSession = Depends(get_db_session), user=Depends(get_current_user),
):
item = await ledger_service.get_ledger(db, ledger_id, user, manage=True, lock=True)
await ledger_service.remove_member(db, item, user_id, user)
@router.get("/{ledger_id}/editor-config", response_model=CollaborationEditorConfigRead)
async def editor_config(ledger_id: uuid.UUID, db: AsyncSession = Depends(get_db_session), user=Depends(get_current_user)):
item = await ledger_service.get_ledger(db, ledger_id, user, lock=True)
return await onlyoffice_collaboration_service.build_editor_config(db, item, user)
@router.patch("/{ledger_id}/access-settings", response_model=LedgerRead)
async def access_settings(ledger_id: uuid.UUID, payload: LedgerAccessSettings,
db: AsyncSession = Depends(get_db_session), user=Depends(get_current_user)):
item = await ledger_service.get_ledger(db, ledger_id, user, manage=True, lock=True)
return await ledger_service.update_access_settings(db, item, payload, user)
@router.get("/{ledger_id}/share-link", response_model=CollaborationShareLinkRead)
async def share_link(ledger_id: uuid.UUID, db: AsyncSession = Depends(get_db_session), user=Depends(get_current_user)):
item = await ledger_service.get_ledger(db, ledger_id, user, manage=True, lock=True)
return await collaboration_share_service.get_share_link(db, item, user)
@router.put("/{ledger_id}/share-link", response_model=CollaborationShareLinkRead)
async def update_share_link(ledger_id: uuid.UUID, payload: CollaborationShareLinkUpdate,
db: AsyncSession = Depends(get_db_session), user=Depends(get_current_user)):
item = await ledger_service.get_ledger(db, ledger_id, user, manage=True, lock=True)
return await collaboration_share_service.update_share_link(db, item, payload, user)
@router.post("/{ledger_id}/edit-requests", response_model=CollaborationEditRequestRead)
async def request_edit(ledger_id: uuid.UUID, db: AsyncSession = Depends(get_db_session), user=Depends(get_current_user)):
item = await ledger_service.get_ledger(db, ledger_id, user, lock=True)
return await collaboration_service.create_edit_request(db, item, user)
@router.get("/{ledger_id}/edit-requests", response_model=list[CollaborationEditRequestRead])
async def edit_requests(ledger_id: uuid.UUID, db: AsyncSession = Depends(get_db_session), user=Depends(get_current_user)):
item = await ledger_service.get_ledger(db, ledger_id, user, manage=True)
return await collaboration_service.list_edit_requests(db, item, user)
@router.post("/{ledger_id}/edit-requests/{request_id}/resolve", response_model=CollaborationEditRequestRead)
async def resolve_request(ledger_id: uuid.UUID, request_id: uuid.UUID, payload: CollaborationEditRequestResolve,
db: AsyncSession = Depends(get_db_session), user=Depends(get_current_user)):
item = await ledger_service.get_ledger(db, ledger_id, user, manage=True, lock=True)
return await collaboration_service.resolve_edit_request(db, item, request_id, payload, user)
@router.post("/{ledger_id}/transfer-ownership", response_model=LedgerRead)
async def transfer_owner(ledger_id: uuid.UUID, payload: CollaborationOwnershipTransferRequest,
db: AsyncSession = Depends(get_db_session), user=Depends(get_current_user)):
item = await ledger_service.get_ledger(db, ledger_id, user, manage=True, lock=True)
item = await collaboration_service.transfer_ownership(db, item, payload, user)
return await ledger_service.read_ledger(db, item, user)
@router.post("/{ledger_id}/downloads", status_code=204)
async def download_check(
ledger_id: uuid.UUID, payload: CollaborationExportRecord,
db: AsyncSession = Depends(get_db_session), user=Depends(get_current_user),
):
item = await ledger_service.get_ledger(db, ledger_id, user)
await collaboration_service.record_download(db, item, user, payload.file_type)
@router.get("/{ledger_id}/revisions", response_model=list[CollaborationRevisionRead])
async def revisions(ledger_id: uuid.UUID, db: AsyncSession = Depends(get_db_session), user=Depends(get_current_user)):
item = await ledger_service.get_ledger(db, ledger_id, user)
return await collaboration_service.list_revisions(db, item)
@router.post("/{ledger_id}/revisions/{revision_id}/restore", response_model=CollaborationRevisionRead)
async def restore(
ledger_id: uuid.UUID, revision_id: uuid.UUID, payload: LedgerRestore,
db: AsyncSession = Depends(get_db_session), user=Depends(get_current_user),
):
item = await ledger_service.get_ledger(db, ledger_id, user, manage=True, lock=True)
return await ledger_service.restore_revision(db, item, revision_id, payload.generation, user)
@router.patch("/{ledger_id}/revisions/{revision_id}", response_model=CollaborationRevisionRead)
async def name_revision(
ledger_id: uuid.UUID, revision_id: uuid.UUID, payload: CollaborationRevisionUpdate,
db: AsyncSession = Depends(get_db_session), user=Depends(get_current_user),
):
item = await ledger_service.get_ledger(db, ledger_id, user, lock=True)
return await collaboration_service.update_revision(db, item, revision_id, payload, user)
@router.get("/{ledger_id}/revisions/{revision_id}/download")
async def download_revision(
ledger_id: uuid.UUID, revision_id: uuid.UUID,
db: AsyncSession = Depends(get_db_session), user=Depends(get_current_user),
):
item = await ledger_service.get_ledger(db, ledger_id, user, lock=True)
await collaboration_service.require_file_exporter(db, item, user)
revision = await collaboration_service.prepare_revision_preview(db, item, revision_id, user)
await collaboration_service.record_download(db, item, user, "xlsx")
return FileResponse(revision.file_uri, filename=item.title, media_type=revision.mime_type,
headers={"Cache-Control": "no-store"})
@router.get("/{ledger_id}/revisions/{revision_id}/preview-config", response_model=OnlyOfficePreviewConfigRead)
async def preview(
ledger_id: uuid.UUID, revision_id: uuid.UUID,
db: AsyncSession = Depends(get_db_session), user=Depends(get_current_user),
):
item = await ledger_service.get_ledger(db, ledger_id, user)
revision = await collaboration_service.prepare_revision_preview(db, item, revision_id, user)
await onlyoffice_service.ensure_onlyoffice_available()
return onlyoffice_service.build_preview_config(
resource_type="collaboration_revision", resource_id=revision.id, file_name=item.title,
file_hash=revision.file_hash, user_id=user.id, user_name=user.full_name,
)
+2
View File
@@ -1,4 +1,5 @@
from fastapi import APIRouter
from app.api.v1 import ledgers
from app.api.v1 import auth, users, admin_email_settings, studies, sites, members, attachments, audit_logs, dashboard, subjects, visits, aes, finance_dashboard, fees_contracts, drug_shipments, material_equipments, project_milestones, startup, precautions, subject_histories, subject_pds, study_subject_pds, faq_categories, faqs, documents, etmf, overview, notifications, desktop_notifications, monitoring_visit_issues, api_permissions, permission_monitoring, permission_templates, system_permissions, study_active_roles, onlyoffice, collaboration
@@ -32,6 +33,7 @@ api_router.include_router(startup.router, prefix="/studies/{study_id}/startup",
api_router.include_router(precautions.router, prefix="/studies/{study_id}/shared-library", tags=["precautions"])
api_router.include_router(collaboration.router, prefix="/studies/{study_id}/collaboration", tags=["collaboration"])
api_router.include_router(collaboration.public_router, prefix="/collaboration/shares", tags=["collaboration-shares"])
api_router.include_router(ledgers.router, prefix="/ledgers", tags=["ledgers"])
api_router.include_router(monitoring_visit_issues.router, prefix="/studies/{study_id}/monitoring", tags=["monitoring-visit-issues"])
api_router.include_router(subject_histories.router, prefix="/studies/{study_id}/subjects/{subject_id}", tags=["subject-histories"])
api_router.include_router(subject_pds.router, prefix="/studies/{study_id}/subjects/{subject_id}", tags=["subject-pds"])