attachment_query_service.py 9.8 KB

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