documents.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. """主案、子方案和共享附件只读 HTTP 接口。"""
  2. from __future__ import annotations
  3. import json
  4. import logging
  5. from flask import current_app, request
  6. from dms.api.v1.blueprint import api_v1
  7. from dms.common.errors import (
  8. BatchManifestMismatchError,
  9. DmsError,
  10. InvalidArgumentError,
  11. PayloadTooLargeError,
  12. )
  13. from dms.common.response import success_response
  14. from dms.extensions import db
  15. from dms.security.decorators import admin_required, bearer_auth_required
  16. from dms.services.attachment_query_service import (
  17. get_attachment,
  18. list_attachment_main_plans,
  19. list_attachments,
  20. list_main_plan_attachments,
  21. )
  22. from dms.services.attachment_binding_service import (
  23. bind_attachments,
  24. unbind_attachment,
  25. )
  26. from dms.services.document_query_service import (
  27. get_document,
  28. list_documents,
  29. list_main_plan_tags,
  30. list_sub_plans,
  31. parse_string_id,
  32. )
  33. from dms.services.document_mutation_service import (
  34. create_attachment,
  35. create_document,
  36. delete_attachment,
  37. delete_document,
  38. update_attachment,
  39. update_document,
  40. )
  41. from dms.services.file_read_service import download_document, preview_document
  42. from dms.services.permission_service import get_permissions, save_permissions
  43. logger = logging.getLogger(__name__)
  44. def _metadata() -> dict:
  45. if set(request.files) != {"file"} or set(request.form) != {"metadata"}:
  46. raise InvalidArgumentError("请求必须且只能包含file和metadata")
  47. try:
  48. payload = json.loads(request.form["metadata"])
  49. except (json.JSONDecodeError, TypeError) as exc:
  50. raise InvalidArgumentError("metadata必须是JSON对象字符串") from exc
  51. if not isinstance(payload, dict):
  52. raise InvalidArgumentError("metadata必须是JSON对象字符串")
  53. return payload
  54. def _json_body() -> dict:
  55. if not request.is_json:
  56. raise InvalidArgumentError("请求体必须是JSON对象")
  57. payload = request.get_json(silent=True)
  58. if not isinstance(payload, dict):
  59. raise InvalidArgumentError("请求体必须是JSON对象")
  60. return payload
  61. def _row_version() -> int:
  62. value = request.args.get("rowVersion")
  63. if value is None or not value.isdecimal():
  64. raise InvalidArgumentError("rowVersion必须是非负整数")
  65. return int(value)
  66. def _main_plan_row_version() -> int:
  67. if set(request.args) != {"mainPlanRowVersion"}:
  68. raise InvalidArgumentError(
  69. "查询参数必须且只能包含mainPlanRowVersion"
  70. )
  71. value = request.args.get("mainPlanRowVersion")
  72. if value is None or not value.isdecimal():
  73. raise InvalidArgumentError("mainPlanRowVersion必须是非负整数")
  74. return int(value)
  75. def _batch(*, attachment: bool):
  76. if set(request.files) != {"files"} or set(request.form) != {"items"}:
  77. raise InvalidArgumentError("批量请求必须且只能包含files和items")
  78. files = request.files.getlist("files")
  79. try:
  80. items = json.loads(request.form["items"])
  81. except (json.JSONDecodeError, TypeError) as exc:
  82. raise InvalidArgumentError("items必须是JSON数组字符串") from exc
  83. if not isinstance(items, list):
  84. raise InvalidArgumentError("items必须是JSON数组字符串")
  85. if len(files) != len(items):
  86. raise BatchManifestMismatchError()
  87. if not files:
  88. raise InvalidArgumentError("批量文件不能为空")
  89. if len(files) > current_app.config["DMS_BATCH_MAX_FILES"]:
  90. raise PayloadTooLargeError("批量文件数量超过允许上限")
  91. results = []
  92. success_count = 0
  93. for index, (file, payload) in enumerate(zip(files, items, strict=True)):
  94. original_name = file.filename or ""
  95. try:
  96. document = (
  97. create_attachment(file, payload, batch=True)
  98. if attachment
  99. else create_document(file, payload, batch=True)
  100. )
  101. results.append(
  102. {
  103. "index": index,
  104. "originalFileName": original_name,
  105. "success": True,
  106. "document": document,
  107. }
  108. )
  109. success_count += 1
  110. except DmsError as exc:
  111. db.session.rollback()
  112. results.append(
  113. {
  114. "index": index,
  115. "originalFileName": original_name,
  116. "success": False,
  117. "errorCode": exc.code,
  118. "errorMessage": exc.message,
  119. }
  120. )
  121. except Exception:
  122. db.session.rollback()
  123. logger.exception("批量导入单项发生未处理异常:index=%s", index)
  124. results.append(
  125. {
  126. "index": index,
  127. "originalFileName": original_name,
  128. "success": False,
  129. "errorCode": "INTERNAL_ERROR",
  130. "errorMessage": "服务器内部错误",
  131. }
  132. )
  133. return {
  134. "total": len(files),
  135. "successCount": success_count,
  136. "failureCount": len(files) - success_count,
  137. "items": results,
  138. }
  139. @api_v1.get("/documents")
  140. @bearer_auth_required
  141. def documents_list_route():
  142. return success_response(list_documents(request.args))
  143. @api_v1.get("/documents/tags")
  144. @bearer_auth_required
  145. def documents_tags_route():
  146. """聚合当前用户可见的主案标签,供前端左侧标签筛选条使用。"""
  147. return success_response(list_main_plan_tags())
  148. @api_v1.post("/documents")
  149. @bearer_auth_required
  150. @admin_required
  151. def create_document_route():
  152. payload = _metadata()
  153. return success_response(
  154. create_document(request.files["file"], payload),
  155. status=201,
  156. )
  157. @api_v1.post("/documents/batch-import")
  158. @bearer_auth_required
  159. @admin_required
  160. def batch_import_documents_route():
  161. return success_response(_batch(attachment=False))
  162. @api_v1.get("/documents/<id>")
  163. @bearer_auth_required
  164. def document_detail_route(id: str):
  165. return success_response(get_document(parse_string_id(id)))
  166. @api_v1.put("/documents/<id>")
  167. @bearer_auth_required
  168. @admin_required
  169. def update_document_route(id: str):
  170. return success_response(
  171. update_document(parse_string_id(id), _json_body())
  172. )
  173. @api_v1.delete("/documents/<id>")
  174. @bearer_auth_required
  175. @admin_required
  176. def delete_document_route(id: str):
  177. return success_response(
  178. delete_document(parse_string_id(id), _row_version())
  179. )
  180. @api_v1.get("/documents/<id>/preview")
  181. @bearer_auth_required
  182. def preview_document_route(id: str):
  183. return preview_document(parse_string_id(id))
  184. @api_v1.get("/documents/<id>/download")
  185. @bearer_auth_required
  186. def download_document_route(id: str):
  187. return download_document(parse_string_id(id))
  188. @api_v1.get("/main-plans/<id>/sub-plans")
  189. @bearer_auth_required
  190. def sub_plans_list_route(id: str):
  191. return success_response(list_sub_plans(parse_string_id(id), request.args))
  192. @api_v1.get("/attachments")
  193. @bearer_auth_required
  194. @admin_required
  195. def attachments_list_route():
  196. return success_response(list_attachments(request.args))
  197. @api_v1.post("/attachments")
  198. @bearer_auth_required
  199. @admin_required
  200. def create_attachment_route():
  201. payload = _metadata()
  202. return success_response(
  203. create_attachment(request.files["file"], payload),
  204. status=201,
  205. )
  206. @api_v1.post("/attachments/batch-import")
  207. @bearer_auth_required
  208. @admin_required
  209. def batch_import_attachments_route():
  210. return success_response(_batch(attachment=True))
  211. @api_v1.get("/attachments/<id>")
  212. @bearer_auth_required
  213. def attachment_detail_route(id: str):
  214. return success_response(get_attachment(parse_string_id(id)))
  215. @api_v1.put("/attachments/<id>")
  216. @bearer_auth_required
  217. @admin_required
  218. def update_attachment_route(id: str):
  219. return success_response(
  220. update_attachment(parse_string_id(id), _json_body())
  221. )
  222. @api_v1.delete("/attachments/<id>")
  223. @bearer_auth_required
  224. @admin_required
  225. def delete_attachment_route(id: str):
  226. return success_response(
  227. delete_attachment(parse_string_id(id), _row_version())
  228. )
  229. @api_v1.get("/attachments/<id>/main-plans")
  230. @bearer_auth_required
  231. def attachment_main_plans_route(id: str):
  232. return success_response(
  233. list_attachment_main_plans(parse_string_id(id))
  234. )
  235. @api_v1.get("/main-plans/<id>/attachments")
  236. @bearer_auth_required
  237. def main_plan_attachments_route(id: str):
  238. return success_response(
  239. list_main_plan_attachments(parse_string_id(id), request.args)
  240. )
  241. @api_v1.post("/main-plans/<id>/attachments/bind")
  242. @bearer_auth_required
  243. @admin_required
  244. def bind_main_plan_attachments_route(id: str):
  245. return success_response(
  246. bind_attachments(parse_string_id(id), _json_body())
  247. )
  248. @api_v1.delete("/main-plans/<id>/attachments/<attachmentId>")
  249. @bearer_auth_required
  250. @admin_required
  251. def unbind_main_plan_attachment_route(id: str, attachmentId: str):
  252. return success_response(
  253. unbind_attachment(
  254. parse_string_id(id),
  255. parse_string_id(attachmentId, field="attachmentId"),
  256. _main_plan_row_version(),
  257. )
  258. )
  259. @api_v1.get("/documents/<id>/permissions")
  260. @bearer_auth_required
  261. @admin_required
  262. def document_permissions_route(id: str):
  263. return success_response(get_permissions(parse_string_id(id)))
  264. @api_v1.put("/documents/<id>/permissions")
  265. @bearer_auth_required
  266. @admin_required
  267. def save_document_permissions_route(id: str):
  268. return success_response(
  269. save_permissions(parse_string_id(id), _json_body())
  270. )