新增全局协作台账并统一工作台管理界面
新增与项目平级的协作台账,提供空白模板、独立账号授权和在线编辑。 复用共享库的访问权限与历史版本界面,拆分信息维护入口并统一工作台卡片样式。 补充在线文档字体构建支持、数据库表结构升级和回归测试。 不包含本地台账内容、账号授权记录或历史文件迁移数据。
This commit is contained in:
@@ -46,6 +46,7 @@ from app.schemas.collaboration import (
|
||||
CollaborationRevisionUpdate,
|
||||
)
|
||||
from app.services import notification_service
|
||||
from app.services import ledger_access
|
||||
|
||||
COLLABORATION_ROOT = Path(__file__).resolve().parent.parent / "uploads" / "collaboration"
|
||||
FILE_TYPE_EXTENSION = {"word": "docx", "cell": "xlsx", "slide": "pptx"}
|
||||
@@ -332,6 +333,8 @@ async def _require_role_assignable(
|
||||
|
||||
|
||||
async def can_edit_file(db: AsyncSession, item: CollaborationFile, user) -> bool:
|
||||
if ledger_access.is_ledger(item):
|
||||
return await ledger_access.can_edit(db, item, user)
|
||||
if is_system_admin(user):
|
||||
return True
|
||||
membership = await member_crud.get_member(db, item.study_id, user.id)
|
||||
@@ -342,6 +345,8 @@ async def can_edit_file(db: AsyncSession, item: CollaborationFile, user) -> bool
|
||||
|
||||
|
||||
async def can_manage_file(db: AsyncSession, item: CollaborationFile, user) -> bool:
|
||||
if ledger_access.is_ledger(item):
|
||||
return await ledger_access.can_manage(db, item, user)
|
||||
if is_system_admin(user):
|
||||
return True
|
||||
membership = await member_crud.get_member(db, item.study_id, user.id)
|
||||
@@ -351,6 +356,8 @@ async def can_manage_file(db: AsyncSession, item: CollaborationFile, user) -> bo
|
||||
|
||||
|
||||
async def can_export_file(db: AsyncSession, item: CollaborationFile, user) -> bool:
|
||||
if ledger_access.is_ledger(item):
|
||||
return await ledger_access.can_export(db, item, user)
|
||||
if is_system_admin(user):
|
||||
return True
|
||||
membership = await member_crud.get_member(db, item.study_id, user.id)
|
||||
@@ -362,6 +369,8 @@ async def can_export_file(db: AsyncSession, item: CollaborationFile, user) -> bo
|
||||
|
||||
|
||||
async def can_create_file(db: AsyncSession, item: CollaborationFile, user) -> bool:
|
||||
if ledger_access.is_ledger(item):
|
||||
return False
|
||||
if is_system_admin(user):
|
||||
return True
|
||||
membership = await member_crud.get_member(db, item.study_id, user.id)
|
||||
@@ -387,6 +396,12 @@ async def edit_request_status(
|
||||
|
||||
|
||||
async def can_request_edit_file(db: AsyncSession, item: CollaborationFile, user) -> bool:
|
||||
if ledger_access.is_ledger(item):
|
||||
return (
|
||||
item.status == "ACTIVE" and item.allow_edit_request
|
||||
and await ledger_access.role_for(db, item, user) == "VIEWER"
|
||||
and await edit_request_status(db, item, user.id) != "PENDING"
|
||||
)
|
||||
if not item.allow_edit_request or await can_edit_file(db, item, user):
|
||||
return False
|
||||
membership = await member_crud.get_member(db, item.study_id, user.id)
|
||||
@@ -398,6 +413,8 @@ async def can_request_edit_file(db: AsyncSession, item: CollaborationFile, user)
|
||||
|
||||
|
||||
async def can_transfer_ownership(db: AsyncSession, item: CollaborationFile, user) -> bool:
|
||||
if ledger_access.is_ledger(item):
|
||||
return bool(user and user.is_active and (is_system_admin(user) or item.owner_id == user.id))
|
||||
if is_system_admin(user):
|
||||
return True
|
||||
membership = await member_crud.get_member(db, item.study_id, user.id)
|
||||
@@ -535,7 +552,7 @@ async def _persist_revision_bytes(
|
||||
select(func.max(CollaborationRevision.revision_no)).where(CollaborationRevision.file_id == locked.id)
|
||||
)
|
||||
revision_no = int(last_no or 0) + 1
|
||||
directory = COLLABORATION_ROOT / str(locked.study_id) / str(locked.id)
|
||||
directory = COLLABORATION_ROOT / ("ledgers" if ledger_access.is_ledger(locked) else str(locked.study_id)) / str(locked.id)
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
destination = directory / f"{uuid.uuid4()}.{locked.extension}"
|
||||
async with aiofiles.open(destination, "wb") as stream:
|
||||
@@ -841,6 +858,8 @@ async def prepare_download(db: AsyncSession, item: CollaborationFile, user) -> C
|
||||
|
||||
|
||||
async def move_to_trash(db: AsyncSession, item: CollaborationFile, user) -> None:
|
||||
if ledger_access.is_ledger(item):
|
||||
raise HTTPException(403, "台账禁止删除,请使用归档")
|
||||
await require_file_manager(db, item, user)
|
||||
item.status = "DELETED"
|
||||
item.deleted_at = datetime.now(timezone.utc)
|
||||
@@ -899,6 +918,8 @@ async def delete_revision(
|
||||
revision_id: uuid.UUID,
|
||||
user,
|
||||
) -> None:
|
||||
if ledger_access.is_ledger(item):
|
||||
raise HTTPException(403, "台账历史版本禁止删除")
|
||||
await require_file_manager(db, item, user)
|
||||
revision = await get_revision_or_404(db, item, revision_id)
|
||||
if item.current_revision_id == revision.id:
|
||||
@@ -965,10 +986,19 @@ async def copy_revision(
|
||||
async def restore_revision(
|
||||
db: AsyncSession, item: CollaborationFile, revision_id: uuid.UUID, user, change_summary: str | None
|
||||
) -> CollaborationRevision:
|
||||
if ledger_access.is_ledger(item):
|
||||
await ledger_access.require_access(db, item, user, manage=True)
|
||||
await require_file_editor(db, item, user)
|
||||
revision = await get_revision_or_404(db, item, revision_id)
|
||||
async with aiofiles.open(revision.file_uri, "rb") as stream:
|
||||
content = await stream.read()
|
||||
if ledger_access.is_ledger(item):
|
||||
content, _ = apply_workbook_structure_policy(
|
||||
content, file_id=item.id, allow_sheet_structure_edit=item.allow_sheet_structure_edit,
|
||||
# A historical revision may still carry CTMS protection after the
|
||||
# current ledger was unlocked. Apply today's policy on restoration.
|
||||
protection_backup=item.sheet_structure_protection_backup or "",
|
||||
)
|
||||
restored, created = await append_revision(
|
||||
db,
|
||||
item,
|
||||
@@ -1092,9 +1122,14 @@ async def create_edit_request(
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="您已拥有此文件的编辑权限")
|
||||
if not item.allow_edit_request:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="此文件未开放编辑权限申请")
|
||||
membership = await member_crud.get_member(db, item.study_id, user.id)
|
||||
if not membership or not membership.is_active:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="只有当前项目成员可以申请编辑权限")
|
||||
if ledger_access.is_ledger(item):
|
||||
await ledger_access.require_access(db, item, user)
|
||||
if item.status != "ACTIVE":
|
||||
raise HTTPException(409, "已归档台账不能申请编辑权限")
|
||||
else:
|
||||
membership = await member_crud.get_member(db, item.study_id, user.id)
|
||||
if not membership or not membership.is_active:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="只有当前项目成员可以申请编辑权限")
|
||||
pending = await db.scalar(
|
||||
select(CollaborationEditRequest).where(
|
||||
CollaborationEditRequest.file_id == item.id,
|
||||
@@ -1107,40 +1142,41 @@ async def create_edit_request(
|
||||
request = CollaborationEditRequest(file_id=item.id, requester_id=user.id)
|
||||
db.add(request)
|
||||
await db.flush()
|
||||
manager_ids = set((await db.scalars(
|
||||
select(StudyMember.user_id)
|
||||
.outerjoin(
|
||||
CollaborationMember,
|
||||
and_(
|
||||
CollaborationMember.file_id == item.id,
|
||||
CollaborationMember.user_id == StudyMember.user_id,
|
||||
),
|
||||
if not ledger_access.is_ledger(item):
|
||||
manager_ids = set((await db.scalars(
|
||||
select(StudyMember.user_id)
|
||||
.outerjoin(
|
||||
CollaborationMember,
|
||||
and_(
|
||||
CollaborationMember.file_id == item.id,
|
||||
CollaborationMember.user_id == StudyMember.user_id,
|
||||
),
|
||||
)
|
||||
.where(
|
||||
StudyMember.study_id == item.study_id,
|
||||
StudyMember.is_active.is_(True),
|
||||
or_(
|
||||
StudyMember.user_id == item.owner_id,
|
||||
CollaborationMember.role == "MANAGER",
|
||||
),
|
||||
)
|
||||
)).all())
|
||||
manager_ids.add(item.owner_id)
|
||||
requester_name = str(user.full_name or user.email or "项目成员")
|
||||
await notification_service.create_recipient_notifications(
|
||||
db,
|
||||
study_id=item.study_id,
|
||||
recipient_ids=manager_ids,
|
||||
category="COLLABORATION_EDIT_REQUEST",
|
||||
priority="NORMAL",
|
||||
title="新的编辑权限申请",
|
||||
message=f"{requester_name} 申请编辑“{item.title}”",
|
||||
action_path=f"/knowledge/collaboration?editRequestFile={item.id}",
|
||||
source_type="COLLABORATION_EDIT_REQUEST",
|
||||
source_id=str(request.id),
|
||||
dedupe_key=f"collaboration-edit-request:{request.id}",
|
||||
source_version="PENDING",
|
||||
)
|
||||
.where(
|
||||
StudyMember.study_id == item.study_id,
|
||||
StudyMember.is_active.is_(True),
|
||||
or_(
|
||||
StudyMember.user_id == item.owner_id,
|
||||
CollaborationMember.role == "MANAGER",
|
||||
),
|
||||
)
|
||||
)).all())
|
||||
manager_ids.add(item.owner_id)
|
||||
requester_name = str(user.full_name or user.email or "项目成员")
|
||||
await notification_service.create_recipient_notifications(
|
||||
db,
|
||||
study_id=item.study_id,
|
||||
recipient_ids=manager_ids,
|
||||
category="COLLABORATION_EDIT_REQUEST",
|
||||
priority="NORMAL",
|
||||
title="新的编辑权限申请",
|
||||
message=f"{requester_name} 申请编辑“{item.title}”",
|
||||
action_path=f"/knowledge/collaboration?editRequestFile={item.id}",
|
||||
source_type="COLLABORATION_EDIT_REQUEST",
|
||||
source_id=str(request.id),
|
||||
dedupe_key=f"collaboration-edit-request:{request.id}",
|
||||
source_version="PENDING",
|
||||
)
|
||||
await db.commit()
|
||||
await db.refresh(request)
|
||||
return _edit_request_read(request, user)
|
||||
@@ -1179,11 +1215,18 @@ async def resolve_edit_request(
|
||||
if request.status != "PENDING":
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="编辑权限申请已处理")
|
||||
requester = await db.get(User, request.requester_id)
|
||||
membership = await member_crud.get_member(db, item.study_id, request.requester_id)
|
||||
if not requester or not membership or not membership.is_active:
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="申请人已不是当前项目的有效成员")
|
||||
if ledger_access.is_ledger(item):
|
||||
if item.status != "ACTIVE" or not await ledger_access.role_for(db, item, requester):
|
||||
raise HTTPException(422, "申请人已无台账访问权限或台账已归档")
|
||||
else:
|
||||
membership = await member_crud.get_member(db, item.study_id, request.requester_id)
|
||||
if not requester or not membership or not membership.is_active:
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="申请人已不是当前项目的有效成员")
|
||||
if payload.status == "APPROVED":
|
||||
await _require_role_assignable(db, item.study_id, requester, membership, "EDITOR")
|
||||
if ledger_access.is_ledger(item):
|
||||
item.generation += 1
|
||||
else:
|
||||
await _require_role_assignable(db, item.study_id, requester, membership, "EDITOR")
|
||||
member = await db.scalar(select(CollaborationMember).where(
|
||||
CollaborationMember.file_id == item.id,
|
||||
CollaborationMember.user_id == request.requester_id,
|
||||
@@ -1201,30 +1244,31 @@ async def resolve_edit_request(
|
||||
request.status = payload.status
|
||||
request.resolved_by = user.id
|
||||
request.resolved_at = datetime.now(timezone.utc)
|
||||
approved = payload.status == "APPROVED"
|
||||
await notification_service.create_recipient_notifications(
|
||||
db,
|
||||
study_id=item.study_id,
|
||||
recipient_ids=[request.requester_id],
|
||||
category="COLLABORATION_EDIT_REQUEST_RESULT",
|
||||
priority="NORMAL",
|
||||
title="编辑权限申请已通过" if approved else "编辑权限申请未通过",
|
||||
message=f"您对“{item.title}”的编辑权限申请已{'通过' if approved else '被拒绝'}",
|
||||
action_path=(
|
||||
f"/knowledge/collaboration/{item.id}"
|
||||
if approved
|
||||
else "/knowledge/collaboration"
|
||||
),
|
||||
source_type="COLLABORATION_EDIT_REQUEST_RESULT",
|
||||
source_id=str(request.id),
|
||||
dedupe_key=f"collaboration-edit-request-result:{request.id}",
|
||||
requires_action=False,
|
||||
)
|
||||
await notification_service.resolve_source_notifications(
|
||||
db,
|
||||
source_type="COLLABORATION_EDIT_REQUEST",
|
||||
source_id=str(request.id),
|
||||
)
|
||||
if not ledger_access.is_ledger(item):
|
||||
approved = payload.status == "APPROVED"
|
||||
await notification_service.create_recipient_notifications(
|
||||
db,
|
||||
study_id=item.study_id,
|
||||
recipient_ids=[request.requester_id],
|
||||
category="COLLABORATION_EDIT_REQUEST_RESULT",
|
||||
priority="NORMAL",
|
||||
title="编辑权限申请已通过" if approved else "编辑权限申请未通过",
|
||||
message=f"您对“{item.title}”的编辑权限申请已{'通过' if approved else '被拒绝'}",
|
||||
action_path=(
|
||||
f"/knowledge/collaboration/{item.id}"
|
||||
if approved
|
||||
else "/knowledge/collaboration"
|
||||
),
|
||||
source_type="COLLABORATION_EDIT_REQUEST_RESULT",
|
||||
source_id=str(request.id),
|
||||
dedupe_key=f"collaboration-edit-request-result:{request.id}",
|
||||
requires_action=False,
|
||||
)
|
||||
await notification_service.resolve_source_notifications(
|
||||
db,
|
||||
source_type="COLLABORATION_EDIT_REQUEST",
|
||||
source_id=str(request.id),
|
||||
)
|
||||
await db.commit()
|
||||
await db.refresh(request)
|
||||
return _edit_request_read(request, requester)
|
||||
@@ -1242,11 +1286,16 @@ async def transfer_ownership(
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="协作文件不存在")
|
||||
if payload.new_owner_id == locked.owner_id:
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="所选联系人已经是文档所有者")
|
||||
membership = await member_crud.get_member(db, locked.study_id, payload.new_owner_id)
|
||||
target = await db.get(User, payload.new_owner_id)
|
||||
if not target or not membership or not membership.is_active:
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="只能转让给当前项目的有效成员")
|
||||
await _require_role_assignable(db, locked.study_id, target, membership, "MANAGER")
|
||||
if ledger_access.is_ledger(locked):
|
||||
if not target or not target.is_active:
|
||||
raise HTTPException(422, "只能转让给有效的系统账号")
|
||||
locked.generation += 1
|
||||
else:
|
||||
membership = await member_crud.get_member(db, locked.study_id, payload.new_owner_id)
|
||||
if not target or not membership or not membership.is_active:
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="只能转让给当前项目的有效成员")
|
||||
await _require_role_assignable(db, locked.study_id, target, membership, "MANAGER")
|
||||
previous_owner_id = locked.owner_id
|
||||
member = await db.scalar(select(CollaborationMember).where(
|
||||
CollaborationMember.file_id == locked.id,
|
||||
@@ -1268,6 +1317,9 @@ async def transfer_ownership(
|
||||
))
|
||||
if previous_owner_member:
|
||||
previous_owner_member.role = "MANAGER"
|
||||
elif ledger_access.is_ledger(locked):
|
||||
db.add(CollaborationMember(file_id=locked.id, user_id=previous_owner_id,
|
||||
role="MANAGER", invited_by=user.id))
|
||||
locked.owner_id = payload.new_owner_id
|
||||
locked.updated_at = datetime.now(timezone.utc)
|
||||
await db.commit()
|
||||
|
||||
Reference in New Issue
Block a user