"""共享附件及其有效挂载关系只读查询。""" from __future__ import annotations from typing import Mapping from sqlalchemy import func, select from dms.common.enums import AttachmentType, DocumentType, RoleCode from dms.common.errors import ResourceNotFoundError from dms.common.pagination import page_result from dms.common.response import serialize_id from dms.common.time_range import parse_updated_range from dms.extensions import db from dms.models import AttachmentBinding, Document from dms.security.auth_context import AuthContext, get_auth_context from dms.services.authorization_service import ( attachment_allowed_actions, evaluate_plan_access, require_attachment_access, ) from dms.services.document_query_service import ( DOCUMENT_SORTS, _advanced_filters, _content_snippet, _enum, _escape_like, _iso, _page, _record_view, _sort, parse_string_id, ) def mounted_plan_count(attachment_id: int) -> int: return ( db.session.scalar( select(func.count(AttachmentBinding.id)).where( AttachmentBinding.attachment_document_id == attachment_id, AttachmentBinding.is_deleted.is_(False), ) ) or 0 ) def attachment_summary( attachment: Document, context: AuthContext, *, plan_access=None, keyword: str | None = None, ) -> dict[str, object]: result: dict[str, object] = { "id": serialize_id(attachment.id), "documentName": attachment.document_name, "summary": attachment.summary, "description": attachment.description, "documentType": attachment.document_type, "status": attachment.document_status, "securityLevel": attachment.security_level, "visibilityType": attachment.visibility_type, "visibilitySummary": "全部已登录用户", "attachmentType": attachment.attachment_type, "categoryId": None, "categoryName": None, "categoryPath": None, "organizationId": serialize_id(attachment.organization_id), "organizationName": attachment.organization_name, "parentDocumentId": None, "rootDocumentId": None, "tags": attachment.tags or [], "fileExtension": attachment.file_extension, "childCount": attachment.child_count, "attachmentCount": attachment.attachment_count, "viewCount": attachment.view_count, "downloadCount": attachment.download_count, "createdByName": attachment.created_by_name, "createdAt": _iso(attachment.created_at), "updatedAt": _iso(attachment.updated_at), "rowVersion": attachment.row_version, "allowedActions": attachment_allowed_actions(context, plan_access), "mountedPlanCount": mounted_plan_count(attachment.id), } snippet = _content_snippet(attachment.content_text, keyword) if snippet is not None: result["contentSnippet"] = snippet return result def attachment_detail( attachment: Document, context: AuthContext, ) -> dict[str, object]: result = attachment_summary(attachment, context) result.update( { "originalFileName": attachment.original_file_name, "mimeType": attachment.mime_type, "fileSize": attachment.file_size, "fileHash": attachment.file_hash, "permissionSummary": { "organizationCount": 0, "userCount": 0, "inheritedFromMainPlan": False, }, "contentText": attachment.content_text, } ) # 提取状态/失败原因仅供管理员排障("编辑"界面提示与重新解析) if context.role_code == RoleCode.ADMIN: result.update( { "contentExtractStatus": attachment.content_extract_status, "contentExtractError": attachment.content_extract_error, "contentExtractedAt": ( _iso(attachment.content_extracted_at) if attachment.content_extracted_at else None ), } ) return result def _attachment(attachment_id: int) -> Document: attachment = db.session.scalar( select(Document).where( Document.id == attachment_id, Document.document_type == DocumentType.ATTACHMENT.value, Document.is_deleted.is_(False), ) ) if attachment is None: raise ResourceNotFoundError("共享附件不存在") return attachment def _filtered_attachments(params: Mapping[str, str]): statement = select(Document).where( Document.document_type == DocumentType.ATTACHMENT.value, Document.is_deleted.is_(False), ) keyword = params.get("keyword") if keyword and keyword.strip(): pattern = f"%{_escape_like(keyword)}%" statement = statement.where( Document.document_name.like(pattern, escape="\\") ) statement = _advanced_filters(statement, params) attachment_type = _enum( params.get("attachmentType"), AttachmentType, "attachmentType" ) if attachment_type: statement = statement.where(Document.attachment_type == attachment_type) extension = params.get("fileExtension") if extension: statement = statement.where( Document.file_extension == extension.strip().lower().lstrip(".") ) updated_from, updated_to = parse_updated_range(params) if updated_from: statement = statement.where(Document.updated_at >= updated_from) if updated_to: statement = statement.where(Document.updated_at <= updated_to) order, _ = _sort(params, allowed=DOCUMENT_SORTS) return statement.order_by(order, Document.id.asc()) def list_attachments(params: Mapping[str, str]) -> dict[str, object]: context = get_auth_context() page = _page(params) attachments = db.session.scalars(_filtered_attachments(params)).all() total = len(attachments) selected = attachments[page.offset : page.offset + page.page_size] # 列表全量可见(全局检索需要):普通用户同样返回全部附件标题, # 但 allowedActions 为空集,且不返回详细说明与正文片段; # 内容访问由详情/预览/下载接口按挂载主案权限独立拦截。 is_admin = context.role_code == RoleCode.ADMIN items: list[dict[str, object]] = [] for item in selected: summary = attachment_summary( item, context, keyword=params.get("contentKeyword") if is_admin else None, ) if not is_admin: summary["description"] = None items.append(summary) return page_result( items, page=page.page, page_size=page.page_size, total=total, ) def get_attachment(attachment_id: int) -> dict[str, object]: context = get_auth_context() attachment = _attachment(attachment_id) can_view, can_download = require_attachment_access( attachment, context, download=False ) result = attachment_detail(attachment, context) if context.role_code.value == "USER": result["allowedActions"] = ["VIEW"] + ( ["DOWNLOAD"] if can_download else [] ) result["viewCount"] = _record_view(attachment) return result def list_attachment_main_plans(attachment_id: int) -> dict[str, object]: context = get_auth_context() _attachment(attachment_id) rows = db.session.execute( select(AttachmentBinding, Document) .join(Document, Document.id == AttachmentBinding.main_document_id) .where( AttachmentBinding.attachment_document_id == attachment_id, AttachmentBinding.is_deleted.is_(False), Document.document_type == DocumentType.MAIN.value, Document.is_deleted.is_(False), ) .order_by(Document.document_name.asc(), Document.id.asc()) ).all() items: list[dict[str, object]] = [] for _, main in rows: if not evaluate_plan_access(main, context).allowed: continue items.append( { "id": serialize_id(main.id), "documentName": main.document_name, "categoryName": main.category_name, "securityLevel": main.security_level, } ) return {"items": items, "total": len(items)} def list_main_plan_attachments( main_document_id: int, params: Mapping[str, str], ) -> dict[str, object]: from dms.common.errors import MainPlanNotFoundError context = get_auth_context() main = db.session.scalar( select(Document).where( Document.id == main_document_id, Document.document_type == DocumentType.MAIN.value, Document.is_deleted.is_(False), ) ) if main is None: raise MainPlanNotFoundError() # 主案无权限时也返回挂载列表(列表全量可见);allowedActions 为空集, # 附件正文片段同步抑制,预览/下载接口仍独立拦截。 main_access = evaluate_plan_access(main, context) page = _page(params) statement = ( select(AttachmentBinding, Document) .join(Document, Document.id == AttachmentBinding.attachment_document_id) .where( AttachmentBinding.main_document_id == main.id, AttachmentBinding.is_deleted.is_(False), Document.document_type == DocumentType.ATTACHMENT.value, Document.is_deleted.is_(False), ) ) keyword = params.get("keyword") if keyword and keyword.strip(): pattern = f"%{_escape_like(keyword)}%" statement = statement.where( Document.document_name.like(pattern, escape="\\") ) statement = _advanced_filters(statement, params) attachment_type = _enum( params.get("attachmentType"), AttachmentType, "attachmentType" ) if attachment_type: statement = statement.where(Document.attachment_type == attachment_type) updated_from, updated_to = parse_updated_range(params) if updated_from: statement = statement.where(Document.updated_at >= updated_from) if updated_to: statement = statement.where(Document.updated_at <= updated_to) rows = db.session.execute( statement.order_by( AttachmentBinding.sort_no.asc(), AttachmentBinding.id.asc(), ) ).all() total = len(rows) selected = rows[page.offset : page.offset + page.page_size] items: list[dict[str, object]] = [] for binding, attachment in selected: item = attachment_summary( attachment, context, plan_access=main_access, keyword=params.get("contentKeyword") if main_access.allowed else None, ) item.update( { "bindingId": serialize_id(binding.id), "bindingSortNo": binding.sort_no, } ) items.append(item) return page_result( items, page=page.page, page_size=page.page_size, total=total, ) __all__ = [ "get_attachment", "list_attachment_main_plans", "list_attachments", "list_main_plan_attachments", "parse_string_id", ]