| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329 |
- """主案、子方案和共享附件只读 HTTP 接口。"""
- from __future__ import annotations
- import json
- import logging
- from flask import current_app, request
- from dms.api.v1.blueprint import api_v1
- from dms.common.errors import (
- BatchManifestMismatchError,
- DmsError,
- InvalidArgumentError,
- PayloadTooLargeError,
- )
- from dms.common.response import success_response
- from dms.extensions import db
- from dms.security.decorators import admin_required, bearer_auth_required
- from dms.services.attachment_query_service import (
- get_attachment,
- list_attachment_main_plans,
- list_attachments,
- list_main_plan_attachments,
- )
- from dms.services.attachment_binding_service import (
- bind_attachments,
- unbind_attachment,
- )
- from dms.services.document_query_service import (
- get_document,
- list_documents,
- list_main_plan_tags,
- list_sub_plans,
- parse_string_id,
- )
- from dms.services.document_mutation_service import (
- create_attachment,
- create_document,
- delete_attachment,
- delete_document,
- update_attachment,
- update_document,
- )
- from dms.services.file_read_service import download_document, preview_document
- from dms.services.permission_service import get_permissions, save_permissions
- logger = logging.getLogger(__name__)
- def _metadata() -> dict:
- if set(request.files) != {"file"} or set(request.form) != {"metadata"}:
- raise InvalidArgumentError("请求必须且只能包含file和metadata")
- try:
- payload = json.loads(request.form["metadata"])
- except (json.JSONDecodeError, TypeError) as exc:
- raise InvalidArgumentError("metadata必须是JSON对象字符串") from exc
- if not isinstance(payload, dict):
- raise InvalidArgumentError("metadata必须是JSON对象字符串")
- return payload
- def _json_body() -> dict:
- if not request.is_json:
- raise InvalidArgumentError("请求体必须是JSON对象")
- payload = request.get_json(silent=True)
- if not isinstance(payload, dict):
- raise InvalidArgumentError("请求体必须是JSON对象")
- return payload
- def _row_version() -> int:
- value = request.args.get("rowVersion")
- if value is None or not value.isdecimal():
- raise InvalidArgumentError("rowVersion必须是非负整数")
- return int(value)
- def _main_plan_row_version() -> int:
- if set(request.args) != {"mainPlanRowVersion"}:
- raise InvalidArgumentError(
- "查询参数必须且只能包含mainPlanRowVersion"
- )
- value = request.args.get("mainPlanRowVersion")
- if value is None or not value.isdecimal():
- raise InvalidArgumentError("mainPlanRowVersion必须是非负整数")
- return int(value)
- def _batch(*, attachment: bool):
- if set(request.files) != {"files"} or set(request.form) != {"items"}:
- raise InvalidArgumentError("批量请求必须且只能包含files和items")
- files = request.files.getlist("files")
- try:
- items = json.loads(request.form["items"])
- except (json.JSONDecodeError, TypeError) as exc:
- raise InvalidArgumentError("items必须是JSON数组字符串") from exc
- if not isinstance(items, list):
- raise InvalidArgumentError("items必须是JSON数组字符串")
- if len(files) != len(items):
- raise BatchManifestMismatchError()
- if not files:
- raise InvalidArgumentError("批量文件不能为空")
- if len(files) > current_app.config["DMS_BATCH_MAX_FILES"]:
- raise PayloadTooLargeError("批量文件数量超过允许上限")
- results = []
- success_count = 0
- for index, (file, payload) in enumerate(zip(files, items, strict=True)):
- original_name = file.filename or ""
- try:
- document = (
- create_attachment(file, payload, batch=True)
- if attachment
- else create_document(file, payload, batch=True)
- )
- results.append(
- {
- "index": index,
- "originalFileName": original_name,
- "success": True,
- "document": document,
- }
- )
- success_count += 1
- except DmsError as exc:
- db.session.rollback()
- results.append(
- {
- "index": index,
- "originalFileName": original_name,
- "success": False,
- "errorCode": exc.code,
- "errorMessage": exc.message,
- }
- )
- except Exception:
- db.session.rollback()
- logger.exception("批量导入单项发生未处理异常:index=%s", index)
- results.append(
- {
- "index": index,
- "originalFileName": original_name,
- "success": False,
- "errorCode": "INTERNAL_ERROR",
- "errorMessage": "服务器内部错误",
- }
- )
- return {
- "total": len(files),
- "successCount": success_count,
- "failureCount": len(files) - success_count,
- "items": results,
- }
- @api_v1.get("/documents")
- @bearer_auth_required
- def documents_list_route():
- return success_response(list_documents(request.args))
- @api_v1.get("/documents/tags")
- @bearer_auth_required
- def documents_tags_route():
- """聚合当前用户可见的主案标签,供前端左侧标签筛选条使用。"""
- return success_response(list_main_plan_tags())
- @api_v1.post("/documents")
- @bearer_auth_required
- @admin_required
- def create_document_route():
- payload = _metadata()
- return success_response(
- create_document(request.files["file"], payload),
- status=201,
- )
- @api_v1.post("/documents/batch-import")
- @bearer_auth_required
- @admin_required
- def batch_import_documents_route():
- return success_response(_batch(attachment=False))
- @api_v1.get("/documents/<id>")
- @bearer_auth_required
- def document_detail_route(id: str):
- return success_response(get_document(parse_string_id(id)))
- @api_v1.put("/documents/<id>")
- @bearer_auth_required
- @admin_required
- def update_document_route(id: str):
- return success_response(
- update_document(parse_string_id(id), _json_body())
- )
- @api_v1.delete("/documents/<id>")
- @bearer_auth_required
- @admin_required
- def delete_document_route(id: str):
- return success_response(
- delete_document(parse_string_id(id), _row_version())
- )
- @api_v1.get("/documents/<id>/preview")
- @bearer_auth_required
- def preview_document_route(id: str):
- return preview_document(parse_string_id(id))
- @api_v1.get("/documents/<id>/download")
- @bearer_auth_required
- def download_document_route(id: str):
- return download_document(parse_string_id(id))
- @api_v1.get("/main-plans/<id>/sub-plans")
- @bearer_auth_required
- def sub_plans_list_route(id: str):
- return success_response(list_sub_plans(parse_string_id(id), request.args))
- @api_v1.get("/attachments")
- @bearer_auth_required
- @admin_required
- def attachments_list_route():
- return success_response(list_attachments(request.args))
- @api_v1.post("/attachments")
- @bearer_auth_required
- @admin_required
- def create_attachment_route():
- payload = _metadata()
- return success_response(
- create_attachment(request.files["file"], payload),
- status=201,
- )
- @api_v1.post("/attachments/batch-import")
- @bearer_auth_required
- @admin_required
- def batch_import_attachments_route():
- return success_response(_batch(attachment=True))
- @api_v1.get("/attachments/<id>")
- @bearer_auth_required
- def attachment_detail_route(id: str):
- return success_response(get_attachment(parse_string_id(id)))
- @api_v1.put("/attachments/<id>")
- @bearer_auth_required
- @admin_required
- def update_attachment_route(id: str):
- return success_response(
- update_attachment(parse_string_id(id), _json_body())
- )
- @api_v1.delete("/attachments/<id>")
- @bearer_auth_required
- @admin_required
- def delete_attachment_route(id: str):
- return success_response(
- delete_attachment(parse_string_id(id), _row_version())
- )
- @api_v1.get("/attachments/<id>/main-plans")
- @bearer_auth_required
- def attachment_main_plans_route(id: str):
- return success_response(
- list_attachment_main_plans(parse_string_id(id))
- )
- @api_v1.get("/main-plans/<id>/attachments")
- @bearer_auth_required
- def main_plan_attachments_route(id: str):
- return success_response(
- list_main_plan_attachments(parse_string_id(id), request.args)
- )
- @api_v1.post("/main-plans/<id>/attachments/bind")
- @bearer_auth_required
- @admin_required
- def bind_main_plan_attachments_route(id: str):
- return success_response(
- bind_attachments(parse_string_id(id), _json_body())
- )
- @api_v1.delete("/main-plans/<id>/attachments/<attachmentId>")
- @bearer_auth_required
- @admin_required
- def unbind_main_plan_attachment_route(id: str, attachmentId: str):
- return success_response(
- unbind_attachment(
- parse_string_id(id),
- parse_string_id(attachmentId, field="attachmentId"),
- _main_plan_row_version(),
- )
- )
- @api_v1.get("/documents/<id>/permissions")
- @bearer_auth_required
- @admin_required
- def document_permissions_route(id: str):
- return success_response(get_permissions(parse_string_id(id)))
- @api_v1.put("/documents/<id>/permissions")
- @bearer_auth_required
- @admin_required
- def save_document_permissions_route(id: str):
- return success_response(
- save_permissions(parse_string_id(id), _json_body())
- )
|