documents.py 9.1 KB

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