attachment_query_service.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. """共享附件及其有效挂载关系只读查询。"""
  2. from __future__ import annotations
  3. from typing import Mapping
  4. from sqlalchemy import String, cast, func, or_, select
  5. from dms.common.enums import AttachmentType, DocumentType
  6. from dms.common.errors import ResourceNotFoundError
  7. from dms.common.pagination import page_result
  8. from dms.common.response import serialize_id
  9. from dms.common.time_range import parse_updated_range
  10. from dms.extensions import db
  11. from dms.models import AttachmentBinding, Document
  12. from dms.security.auth_context import AuthContext, get_auth_context
  13. from dms.services.authorization_service import (
  14. attachment_allowed_actions,
  15. evaluate_plan_access,
  16. )
  17. from dms.services.document_query_service import (
  18. DOCUMENT_SORTS,
  19. _enum,
  20. _iso,
  21. _page,
  22. _record_view,
  23. _sort,
  24. parse_string_id,
  25. )
  26. def mounted_plan_count(attachment_id: int) -> int:
  27. return (
  28. db.session.scalar(
  29. select(func.count(AttachmentBinding.id)).where(
  30. AttachmentBinding.attachment_document_id == attachment_id,
  31. AttachmentBinding.is_deleted.is_(False),
  32. )
  33. )
  34. or 0
  35. )
  36. def attachment_summary(
  37. attachment: Document,
  38. context: AuthContext,
  39. ) -> dict[str, object]:
  40. return {
  41. "id": serialize_id(attachment.id),
  42. "documentName": attachment.document_name,
  43. "summary": attachment.summary,
  44. "documentType": attachment.document_type,
  45. "status": attachment.document_status,
  46. "securityLevel": attachment.security_level,
  47. "visibilityType": attachment.visibility_type,
  48. "visibilitySummary": "全部已登录用户",
  49. "attachmentType": attachment.attachment_type,
  50. "categoryId": None,
  51. "categoryName": None,
  52. "categoryPath": None,
  53. "parentDocumentId": None,
  54. "rootDocumentId": None,
  55. "tags": attachment.tags or [],
  56. "fileExtension": attachment.file_extension,
  57. "childCount": attachment.child_count,
  58. "attachmentCount": attachment.attachment_count,
  59. "viewCount": attachment.view_count,
  60. "downloadCount": attachment.download_count,
  61. "createdByName": attachment.created_by_name,
  62. "createdAt": _iso(attachment.created_at),
  63. "updatedAt": _iso(attachment.updated_at),
  64. "rowVersion": attachment.row_version,
  65. "allowedActions": attachment_allowed_actions(context),
  66. "mountedPlanCount": mounted_plan_count(attachment.id),
  67. }
  68. def attachment_detail(
  69. attachment: Document,
  70. context: AuthContext,
  71. ) -> dict[str, object]:
  72. result = attachment_summary(attachment, context)
  73. result.update(
  74. {
  75. "originalFileName": attachment.original_file_name,
  76. "mimeType": attachment.mime_type,
  77. "fileSize": attachment.file_size,
  78. "fileHash": attachment.file_hash,
  79. "permissionSummary": {
  80. "organizationCount": 0,
  81. "userCount": 0,
  82. "inheritedFromMainPlan": False,
  83. },
  84. }
  85. )
  86. return result
  87. def _attachment(attachment_id: int) -> Document:
  88. attachment = db.session.scalar(
  89. select(Document).where(
  90. Document.id == attachment_id,
  91. Document.document_type == DocumentType.ATTACHMENT.value,
  92. Document.is_deleted.is_(False),
  93. )
  94. )
  95. if attachment is None:
  96. raise ResourceNotFoundError("共享附件不存在")
  97. return attachment
  98. def _filtered_attachments(params: Mapping[str, str]):
  99. statement = select(Document).where(
  100. Document.document_type == DocumentType.ATTACHMENT.value,
  101. Document.is_deleted.is_(False),
  102. )
  103. keyword = params.get("keyword")
  104. if keyword and keyword.strip():
  105. pattern = f"%{keyword.strip()}%"
  106. statement = statement.where(
  107. or_(
  108. Document.document_name.like(pattern),
  109. Document.summary.like(pattern),
  110. Document.search_text.like(pattern),
  111. cast(Document.tags, String).like(pattern),
  112. )
  113. )
  114. attachment_type = _enum(
  115. params.get("attachmentType"), AttachmentType, "attachmentType"
  116. )
  117. if attachment_type:
  118. statement = statement.where(Document.attachment_type == attachment_type)
  119. extension = params.get("fileExtension")
  120. if extension:
  121. statement = statement.where(
  122. Document.file_extension == extension.strip().lower().lstrip(".")
  123. )
  124. updated_from, updated_to = parse_updated_range(params)
  125. if updated_from:
  126. statement = statement.where(Document.updated_at >= updated_from)
  127. if updated_to:
  128. statement = statement.where(Document.updated_at <= updated_to)
  129. order, _ = _sort(params, allowed=DOCUMENT_SORTS)
  130. return statement.order_by(order, Document.id.asc())
  131. def list_attachments(params: Mapping[str, str]) -> dict[str, object]:
  132. context = get_auth_context()
  133. page = _page(params)
  134. attachments = db.session.scalars(_filtered_attachments(params)).all()
  135. total = len(attachments)
  136. selected = attachments[page.offset : page.offset + page.page_size]
  137. return page_result(
  138. [attachment_summary(item, context) for item in selected],
  139. page=page.page,
  140. page_size=page.page_size,
  141. total=total,
  142. )
  143. def get_attachment(attachment_id: int) -> dict[str, object]:
  144. context = get_auth_context()
  145. attachment = _attachment(attachment_id)
  146. result = attachment_detail(attachment, context)
  147. result["viewCount"] = _record_view(attachment)
  148. return result
  149. def list_attachment_main_plans(attachment_id: int) -> dict[str, object]:
  150. context = get_auth_context()
  151. _attachment(attachment_id)
  152. rows = db.session.execute(
  153. select(AttachmentBinding, Document)
  154. .join(Document, Document.id == AttachmentBinding.main_document_id)
  155. .where(
  156. AttachmentBinding.attachment_document_id == attachment_id,
  157. AttachmentBinding.is_deleted.is_(False),
  158. Document.document_type == DocumentType.MAIN.value,
  159. Document.is_deleted.is_(False),
  160. )
  161. .order_by(Document.document_name.asc(), Document.id.asc())
  162. ).all()
  163. items: list[dict[str, object]] = []
  164. for _, main in rows:
  165. if not evaluate_plan_access(main, context).allowed:
  166. continue
  167. items.append(
  168. {
  169. "id": serialize_id(main.id),
  170. "documentName": main.document_name,
  171. "categoryName": main.category_name,
  172. "securityLevel": main.security_level,
  173. }
  174. )
  175. return {"items": items, "total": len(items)}
  176. def list_main_plan_attachments(
  177. main_document_id: int,
  178. params: Mapping[str, str],
  179. ) -> dict[str, object]:
  180. from dms.common.errors import MainPlanNotFoundError
  181. from dms.services.document_query_service import _require_plan_access
  182. context = get_auth_context()
  183. main = db.session.scalar(
  184. select(Document).where(
  185. Document.id == main_document_id,
  186. Document.document_type == DocumentType.MAIN.value,
  187. Document.is_deleted.is_(False),
  188. )
  189. )
  190. if main is None:
  191. raise MainPlanNotFoundError()
  192. _require_plan_access(main, context)
  193. page = _page(params)
  194. statement = (
  195. select(AttachmentBinding, Document)
  196. .join(Document, Document.id == AttachmentBinding.attachment_document_id)
  197. .where(
  198. AttachmentBinding.main_document_id == main.id,
  199. AttachmentBinding.is_deleted.is_(False),
  200. Document.document_type == DocumentType.ATTACHMENT.value,
  201. Document.is_deleted.is_(False),
  202. )
  203. )
  204. keyword = params.get("keyword")
  205. if keyword and keyword.strip():
  206. pattern = f"%{keyword.strip()}%"
  207. statement = statement.where(
  208. or_(
  209. Document.document_name.like(pattern),
  210. Document.summary.like(pattern),
  211. Document.search_text.like(pattern),
  212. )
  213. )
  214. attachment_type = _enum(
  215. params.get("attachmentType"), AttachmentType, "attachmentType"
  216. )
  217. if attachment_type:
  218. statement = statement.where(Document.attachment_type == attachment_type)
  219. updated_from, updated_to = parse_updated_range(params)
  220. if updated_from:
  221. statement = statement.where(Document.updated_at >= updated_from)
  222. if updated_to:
  223. statement = statement.where(Document.updated_at <= updated_to)
  224. rows = db.session.execute(
  225. statement.order_by(
  226. AttachmentBinding.sort_no.asc(),
  227. AttachmentBinding.id.asc(),
  228. )
  229. ).all()
  230. total = len(rows)
  231. selected = rows[page.offset : page.offset + page.page_size]
  232. items: list[dict[str, object]] = []
  233. for binding, attachment in selected:
  234. item = attachment_summary(attachment, context)
  235. item.update(
  236. {
  237. "bindingId": serialize_id(binding.id),
  238. "bindingSortNo": binding.sort_no,
  239. }
  240. )
  241. items.append(item)
  242. return page_result(
  243. items,
  244. page=page.page,
  245. page_size=page.page_size,
  246. total=total,
  247. )
  248. __all__ = [
  249. "get_attachment",
  250. "list_attachment_main_plans",
  251. "list_attachments",
  252. "list_main_plan_attachments",
  253. "parse_string_id",
  254. ]