fc / apps /client /services /client_mutation_service.py
jun2024's picture
Upload 2241 files
b4f2238 verified
Raw History Blame Contribute Delete
8.27 kB
"""当事人写操作服务。"""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Any, cast
from django.db import transaction
from django.utils.translation import gettext_lazy as _
from apps.client.models import Client
from apps.core.exceptions import ForbiddenError, ValidationException
if TYPE_CHECKING:
from ninja.files import UploadedFile
from apps.client.workflows import ClientDeletionWorkflow
from .client_access_policy import ClientAccessPolicy
from .client_identity_doc_service import ClientIdentityDocService
from .client_query_service import ClientQueryService
logger = logging.getLogger("apps.client")
class ClientMutationService:
_VALID_CLIENT_TYPES: list[str] = [Client.NATURAL, Client.LEGAL, Client.NON_LEGAL_ORG]
_UPDATABLE_FIELDS: set[str] = {
"name",
"phone",
"address",
"client_type",
"id_number",
"legal_representative",
"legal_representative_id_number",
"is_our_client",
}
def __init__(
self,
access_policy: ClientAccessPolicy | None = None,
query_service: ClientQueryService | None = None,
deletion_workflow: ClientDeletionWorkflow | None = None,
identity_doc_service: ClientIdentityDocService | None = None,
) -> None:
self._access_policy = access_policy
self._query_service = query_service
self._deletion_workflow = deletion_workflow
self._identity_doc_service = identity_doc_service
@property
def access_policy(self) -> ClientAccessPolicy:
if self._access_policy is None:
from .client_access_policy import ClientAccessPolicy
self._access_policy = ClientAccessPolicy()
return self._access_policy
@property
def query_service(self) -> ClientQueryService:
if self._query_service is None:
from .client_query_service import ClientQueryService
self._query_service = ClientQueryService()
return self._query_service
@property
def identity_doc_service(self) -> ClientIdentityDocService:
if self._identity_doc_service is None:
from .client_identity_doc_service import ClientIdentityDocService
self._identity_doc_service = ClientIdentityDocService()
return self._identity_doc_service
@property
def deletion_workflow(self) -> ClientDeletionWorkflow:
if self._deletion_workflow is None:
from apps.client.workflows import ClientDeletionWorkflow
self._deletion_workflow = ClientDeletionWorkflow()
return self._deletion_workflow
@transaction.atomic
def create_client(self, *, data: dict[str, Any], user: Any | None = None) -> Client:
try:
self.access_policy.ensure_can_create_client(user)
except ForbiddenError:
logger.warning(
"用户尝试创建客户但权限不足",
extra={"user_id": getattr(user, "id", None), "action": "create_client"},
)
raise
self._validate_create_data(data)
client = Client.objects.create(**data)
# prefetch identity_docs 避免 schema 序列化时 N+1
client = Client.objects.prefetch_related("identity_docs").get(pk=client.pk)
logger.info(
"客户创建成功",
extra={"client_id": cast(int, client.pk), "user_id": getattr(user, "id", None), "action": "create_client"},
)
return client
@transaction.atomic
def update_client(self, *, client_id: int, data: dict[str, Any], user: Any | None = None) -> Client:
try:
self.access_policy.ensure_can_update_client(user)
except ForbiddenError:
logger.warning(
"用户尝试更新客户但权限不足",
extra={"user_id": getattr(user, "id", None), "client_id": client_id, "action": "update_client"},
)
raise
client = self.query_service.get_client(client_id=client_id, user=user)
self._validate_update_data(client, data)
updated_fields = []
for key, value in data.items():
if key in self._UPDATABLE_FIELDS:
setattr(client, key, value)
updated_fields.append(key)
if updated_fields:
client.save(update_fields=updated_fields)
logger.info(
"客户更新成功",
extra={"client_id": cast(int, client.pk), "user_id": getattr(user, "id", None), "action": "update_client"},
)
return client
@transaction.atomic
def delete_client(self, *, client_id: int, user: Any | None = None) -> None:
try:
self.access_policy.ensure_can_delete_client(user)
except ForbiddenError:
logger.warning(
"用户尝试删除客户但权限不足",
extra={"user_id": getattr(user, "id", None), "client_id": client_id, "action": "delete_client"},
)
raise
client = self.query_service.get_client(client_id=client_id, user=user)
file_paths = self.deletion_workflow.collect_client_file_paths(client_id=cast(int, client.pk))
client.delete()
self.deletion_workflow.cleanup_files_on_commit(file_paths=file_paths)
logger.info(
"客户删除成功",
extra={"client_id": client_id, "user_id": getattr(user, "id", None), "action": "delete_client"},
)
@transaction.atomic
def create_client_with_docs(
self,
*,
data: dict[str, Any],
doc_types: list[str],
files: list[UploadedFile],
user: Any | None = None,
) -> Client:
"""创建客户并上传证件文档(事务内完成)。"""
client = self.create_client(data=data, user=user)
for doc_type, file in zip(doc_types, files, strict=True):
self.identity_doc_service.add_identity_doc_from_upload(
client_id=client.id,
doc_type=doc_type,
uploaded_file=file,
user=user,
)
return Client.objects.prefetch_related("identity_docs").get(pk=client.pk)
def _validate_create_data(self, data: dict[str, Any]) -> None:
if not data.get("name"):
raise ValidationException(
message=_("客户名称不能为空"), code="INVALID_NAME", errors={"name": _("客户名称不能为空")}
)
if data.get("client_type") not in self._VALID_CLIENT_TYPES:
raise ValidationException(
message=_("无效的客户类型"),
code="INVALID_CLIENT_TYPE",
errors={"client_type": _("无效的客户类型")},
)
if data.get("client_type") == Client.LEGAL and not data.get("legal_representative"):
raise ValidationException(
message=_("法人客户必须填写法定代表人"),
code="MISSING_LEGAL_REPRESENTATIVE",
errors={"legal_representative": _("法人客户必须填写法定代表人")},
)
def _validate_update_data(self, client: Client, data: dict[str, Any]) -> None:
if "name" in data and not data["name"]:
raise ValidationException(
message=_("客户名称不能为空"), code="INVALID_NAME", errors={"name": _("客户名称不能为空")}
)
if "client_type" in data and data["client_type"] not in self._VALID_CLIENT_TYPES:
raise ValidationException(
message=_("无效的客户类型"),
code="INVALID_CLIENT_TYPE",
errors={"client_type": _("无效的客户类型")},
)
client_type = data.get("client_type", client.client_type)
legal_rep = data.get("legal_representative", client.legal_representative)
if client_type == Client.LEGAL and not legal_rep:
raise ValidationException(
message=_("法人客户必须填写法定代表人"),
code="MISSING_LEGAL_REPRESENTATIVE",
errors={"legal_representative": _("法人客户必须填写法定代表人")},
)