attachment_query_service.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  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. }
  88. )
  89. return result
  90. def _attachment(attachment_id: int) -> Document:
  91. attachment = db.session.scalar(
  92. select(Document).where(
  93. Document.id == attachment_id,
  94. Document.document_type == DocumentType.ATTACHMENT.value,
  95. Document.is_deleted.is_(False),
  96. )
  97. )
  98. if attachment is None:
  99. raise ResourceNotFoundError("共享附件不存在")
  100. return attachment
  101. def _filtered_attachments(params: Mapping[str, str]):
  102. statement = select(Document).where(
  103. Document.document_type == DocumentType.ATTACHMENT.value,
  104. Document.is_deleted.is_(False),
  105. )
  106. keyword = params.get("keyword")
  107. if keyword and keyword.strip():
  108. escaped = keyword.strip().replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
  109. pattern = f"%{escaped}%"
  110. statement = statement.where(
  111. or_(
  112. Document.document_name.like(pattern, escape="\\"),
  113. Document.summary.like(pattern, escape="\\"),
  114. Document.search_text.like(pattern, escape="\\"),
  115. cast(Document.tags, String).like(pattern, escape="\\"),
  116. )
  117. )
  118. attachment_type = _enum(
  119. params.get("attachmentType"), AttachmentType, "attachmentType"
  120. )
  121. if attachment_type:
  122. statement = statement.where(Document.attachment_type == attachment_type)
  123. extension = params.get("fileExtension")
  124. if extension:
  125. statement = statement.where(
  126. Document.file_extension == extension.strip().lower().lstrip(".")
  127. )
  128. updated_from, updated_to = parse_updated_range(params)
  129. if updated_from:
  130. statement = statement.where(Document.updated_at >= updated_from)
  131. if updated_to:
  132. statement = statement.where(Document.updated_at <= updated_to)
  133. order, _ = _sort(params, allowed=DOCUMENT_SORTS)
  134. return statement.order_by(order, Document.id.asc())
  135. def list_attachments(params: Mapping[str, str]) -> dict[str, object]:
  136. context = get_auth_context()
  137. page = _page(params)
  138. attachments = db.session.scalars(_filtered_attachments(params)).all()
  139. total = len(attachments)
  140. selected = attachments[page.offset : page.offset + page.page_size]
  141. return page_result(
  142. [attachment_summary(item, context) for item in selected],
  143. page=page.page,
  144. page_size=page.page_size,
  145. total=total,
  146. )
  147. def get_attachment(attachment_id: int) -> dict[str, object]:
  148. context = get_auth_context()
  149. attachment = _attachment(attachment_id)
  150. can_view, can_download = require_attachment_access(
  151. attachment, context, download=False
  152. )
  153. result = attachment_detail(attachment, context)
  154. if context.role_code.value == "USER":
  155. result["allowedActions"] = ["VIEW"] + (
  156. ["DOWNLOAD"] if can_download else []
  157. )
  158. result["viewCount"] = _record_view(attachment)
  159. return result
  160. def list_attachment_main_plans(attachment_id: int) -> dict[str, object]:
  161. context = get_auth_context()
  162. _attachment(attachment_id)
  163. rows = db.session.execute(
  164. select(AttachmentBinding, Document)
  165. .join(Document, Document.id == AttachmentBinding.main_document_id)
  166. .where(
  167. AttachmentBinding.attachment_document_id == attachment_id,
  168. AttachmentBinding.is_deleted.is_(False),
  169. Document.document_type == DocumentType.MAIN.value,
  170. Document.is_deleted.is_(False),
  171. )
  172. .order_by(Document.document_name.asc(), Document.id.asc())
  173. ).all()
  174. items: list[dict[str, object]] = []
  175. for _, main in rows:
  176. if not evaluate_plan_access(main, context).allowed:
  177. continue
  178. items.append(
  179. {
  180. "id": serialize_id(main.id),
  181. "documentName": main.document_name,
  182. "categoryName": main.category_name,
  183. "securityLevel": main.security_level,
  184. }
  185. )
  186. return {"items": items, "total": len(items)}
  187. def list_main_plan_attachments(
  188. main_document_id: int,
  189. params: Mapping[str, str],
  190. ) -> dict[str, object]:
  191. from dms.common.errors import MainPlanNotFoundError
  192. from dms.services.document_query_service import _require_plan_access
  193. context = get_auth_context()
  194. main = db.session.scalar(
  195. select(Document).where(
  196. Document.id == main_document_id,
  197. Document.document_type == DocumentType.MAIN.value,
  198. Document.is_deleted.is_(False),
  199. )
  200. )
  201. if main is None:
  202. raise MainPlanNotFoundError()
  203. main_access = _require_plan_access(main, context)
  204. page = _page(params)
  205. statement = (
  206. select(AttachmentBinding, Document)
  207. .join(Document, Document.id == AttachmentBinding.attachment_document_id)
  208. .where(
  209. AttachmentBinding.main_document_id == main.id,
  210. AttachmentBinding.is_deleted.is_(False),
  211. Document.document_type == DocumentType.ATTACHMENT.value,
  212. Document.is_deleted.is_(False),
  213. )
  214. )
  215. keyword = params.get("keyword")
  216. if keyword and keyword.strip():
  217. escaped = keyword.strip().replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
  218. pattern = f"%{escaped}%"
  219. statement = statement.where(
  220. or_(
  221. Document.document_name.like(pattern, escape="\\"),
  222. Document.summary.like(pattern, escape="\\"),
  223. Document.search_text.like(pattern, escape="\\"),
  224. )
  225. )
  226. attachment_type = _enum(
  227. params.get("attachmentType"), AttachmentType, "attachmentType"
  228. )
  229. if attachment_type:
  230. statement = statement.where(Document.attachment_type == attachment_type)
  231. updated_from, updated_to = parse_updated_range(params)
  232. if updated_from:
  233. statement = statement.where(Document.updated_at >= updated_from)
  234. if updated_to:
  235. statement = statement.where(Document.updated_at <= updated_to)
  236. rows = db.session.execute(
  237. statement.order_by(
  238. AttachmentBinding.sort_no.asc(),
  239. AttachmentBinding.id.asc(),
  240. )
  241. ).all()
  242. total = len(rows)
  243. selected = rows[page.offset : page.offset + page.page_size]
  244. items: list[dict[str, object]] = []
  245. for binding, attachment in selected:
  246. item = attachment_summary(
  247. attachment, context, plan_access=main_access
  248. )
  249. item.update(
  250. {
  251. "bindingId": serialize_id(binding.id),
  252. "bindingSortNo": binding.sort_no,
  253. }
  254. )
  255. items.append(item)
  256. return page_result(
  257. items,
  258. page=page.page,
  259. page_size=page.page_size,
  260. total=total,
  261. )
  262. __all__ = [
  263. "get_attachment",
  264. "list_attachment_main_plans",
  265. "list_attachments",
  266. "list_main_plan_attachments",
  267. "parse_string_id",
  268. ]