File size: 7,227 Bytes
b4f2238 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 | """合同文件夹绑定 API。"""
from __future__ import annotations
import logging
from typing import Any
from django.http import HttpRequest
from django.utils.translation import gettext_lazy as _
from ninja import Router
from apps.contracts.schemas import (
FolderBindingCreateSchema,
FolderBindingResponseSchema,
FolderBrowseEntrySchema,
FolderBrowseResponseSchema,
)
from apps.core.exceptions import PermissionDenied
from apps.core.security import get_request_access_context
logger = logging.getLogger("apps.contracts.api")
router = Router()
def _get_folder_binding_service() -> Any:
"""
工厂函数:创建 FolderBindingService 实例
Returns:
FolderBindingService 实例
"""
from apps.contracts.services import FolderBindingService
from apps.core.dependencies.documents import build_document_template_binding_service
return FolderBindingService(document_template_binding_service=build_document_template_binding_service())
def _require_contract_access(request: HttpRequest, contract_id: int) -> None:
from apps.contracts.services.contract import wiring
ctx = get_request_access_context(request)
wiring.get_contract_query_facade().get_contract_ctx(contract_id=contract_id, ctx=ctx)
def _require_admin(request: HttpRequest) -> None:
user = getattr(request, "user", None)
if not user or not getattr(user, "is_authenticated", False):
raise PermissionDenied(_("需要登录"))
@router.post("/{contract_id}/folder-binding", response=FolderBindingResponseSchema)
def create_folder_binding(request: HttpRequest, contract_id: int, data: FolderBindingCreateSchema) -> Any:
"""
创建或更新文件夹绑定
Args:
contract_id: 合同ID
data: 绑定数据,包含文件夹路径
Returns:
FolderBindingResponseSchema: 绑定信息
Raises:
ValidationException: 路径无效或合同不存在
Requirements: 9.1, 9.4
"""
_require_admin(request)
_require_contract_access(request, contract_id)
service = _get_folder_binding_service()
ctx = get_request_access_context(request)
binding = service.create_binding(owner_id=contract_id, folder_path=data.folder_path)
display_path = service.format_path_for_display(binding.folder_path)
is_accessible = service.check_folder_accessible(binding.folder_path)
logger.info(
"contract_folder_binding_upsert",
extra={
"action": "contract_folder_binding_upsert",
"contract_id": contract_id,
"folder_path": str(getattr(binding, "folder_path", "") or ""),
"display_path": str(display_path or ""),
"is_accessible": bool(is_accessible),
"user_id": getattr(getattr(ctx, "user", None), "id", None),
},
)
return FolderBindingResponseSchema.from_binding(
binding,
is_accessible=is_accessible,
display_path=display_path,
)
@router.get("/{contract_id}/folder-binding", response=FolderBindingResponseSchema | None)
def get_folder_binding(request: HttpRequest, contract_id: int) -> Any:
"""
获取文件夹绑定信息
Args:
contract_id: 合同ID
Returns:
Optional[FolderBindingResponseSchema]: 绑定信息,如果未绑定则返回 None
Requirements: 9.2, 9.4
"""
_require_contract_access(request, contract_id)
service = _get_folder_binding_service()
binding = service.get_binding(owner_id=contract_id)
if not binding:
return None
# 格式化显示路径
display_path = service.format_path_for_display(binding.folder_path)
# 检查文件夹是否可访问(只检查,不创建)
is_accessible = service.check_folder_accessible(binding.folder_path)
return FolderBindingResponseSchema.from_binding(binding, is_accessible=is_accessible, display_path=display_path)
@router.delete("/{contract_id}/folder-binding")
def delete_folder_binding(request: HttpRequest, contract_id: int) -> Any:
"""
删除文件夹绑定
Args:
contract_id: 合同ID
Returns:
dict: 删除结果
Requirements: 9.3, 9.5
"""
_require_admin(request)
_require_contract_access(request, contract_id)
service = _get_folder_binding_service()
ctx = get_request_access_context(request)
success = service.delete_binding(owner_id=contract_id)
logger.info(
"contract_folder_binding_delete",
extra={
"action": "contract_folder_binding_delete",
"contract_id": contract_id,
"success": bool(success),
"user_id": getattr(getattr(ctx, "user", None), "id", None),
},
)
return {"success": success, "message": _("绑定已删除") if success else _("绑定不存在")}
@router.get("/folder-browse", response=FolderBrowseResponseSchema)
def browse_folders(request: HttpRequest, path: str | None = None, include_hidden: bool = False) -> Any:
_require_admin(request)
service = _get_folder_binding_service()
ctx = get_request_access_context(request)
if not path or not str(path).strip():
# 默认进入用户下载文件夹
default_path = service.get_default_browse_path()
if default_path:
path = str(default_path)
else:
# 降级:显示根目录列表
roots = service.get_browse_roots()
entries = [FolderBrowseEntrySchema(name=(p.name or str(p)), path=str(p)) for p in roots]
logger.info(
"contract_folder_browse_roots",
extra={
"action": "contract_folder_browse_roots",
"include_hidden": bool(include_hidden),
"roots_count": len(roots),
"user_id": getattr(getattr(ctx, "user", None), "id", None),
},
)
return FolderBrowseResponseSchema(
browsable=True,
message=None,
path=None,
parent_path=None,
entries=entries,
)
browsable, browse_message = service.is_browsable_path(str(path))
if not browsable:
return FolderBrowseResponseSchema(
browsable=False,
message=browse_message,
path=str(path).strip(),
parent_path=None,
entries=[],
)
resolved = service.resolve_under_allowed_roots(str(path))
entries = [
FolderBrowseEntrySchema(**item) for item in service.list_subdirs(str(path), include_hidden=include_hidden)
]
parent_path: str | None = service.compute_parent_path(resolved)
logger.info(
"contract_folder_browse",
extra={
"action": "contract_folder_browse",
"path": str(path).strip(),
"resolved_path": str(resolved),
"include_hidden": bool(include_hidden),
"entries_count": len(entries),
"user_id": getattr(getattr(ctx, "user", None), "id", None),
},
)
return FolderBrowseResponseSchema(
browsable=True,
message=None,
path=str(resolved),
parent_path=parent_path,
entries=entries,
)
|