fc / apps /cases /services /material /case_material_binding_workflow.py
jun2024's picture
Upload 2241 files
b4f2238 verified
Raw History Blame Contribute Delete
10.4 kB
"""Business workflow orchestration."""
from __future__ import annotations
from collections.abc import Sequence
from typing import Any
from django.db import models, transaction
from django.utils.translation import gettext_lazy as _
from apps.cases.models import (
CaseLogAttachment,
CaseMaterial,
CaseMaterialCategory,
CaseMaterialGroupOrder,
CaseMaterialSide,
CaseMaterialType,
CaseParty,
SupervisingAuthority,
)
from apps.core.exceptions import NotFoundError, ValidationException
class CaseMaterialBindingWorkflow:
def __init__(self, case_service: Any | None = None) -> None:
self._case_service = case_service
@property
def case_service(self) -> Any:
if self._case_service is None:
raise RuntimeError("CaseMaterialBindingWorkflow.case_service 未注入")
return self._case_service
def bind_materials(
self,
case_id: int,
items: Sequence[dict[str, Any]],
user: Any | None = None,
org_access: dict[str, Any] | None = None,
perm_open_access: bool = False,
) -> list[CaseMaterial]:
self.case_service.get_case(case_id, user=user, org_access=org_access, perm_open_access=perm_open_access)
attachment_ids = [int(x["attachment_id"]) for x in items if x.get("attachment_id") is not None]
attachments = {
a.id: a
for a in CaseLogAttachment.objects.filter(log__case_id=case_id, id__in=attachment_ids).select_related("log")
}
missing = [aid for aid in attachment_ids if aid not in attachments]
if missing:
raise NotFoundError(_("部分附件不存在或不属于该案件"))
parties_by_id = {p.id: p for p in CaseParty.objects.filter(case_id=case_id).select_related("client").all()}
authorities_by_id = {a.id: a for a in SupervisingAuthority.objects.filter(case_id=case_id).all()}
law_firm_id = getattr(user, "law_firm_id", None) if user else None
type_cache: dict[str, CaseMaterialType] = {}
saved: list[CaseMaterial] = []
with transaction.atomic():
for payload in items:
att_id_raw = payload.get("attachment_id")
att_id = int(att_id_raw) if att_id_raw is not None else 0
category = (payload.get("category") or "").strip()
type_id = payload.get("type_id")
type_name = (payload.get("type_name") or "").strip()
side = payload.get("side")
party_ids = payload.get("party_ids") or []
supervising_authority_id = payload.get("supervising_authority_id")
if category not in {CaseMaterialCategory.PARTY, CaseMaterialCategory.NON_PARTY}:
raise ValidationException(message=_("材料大类不合法"), errors={"category": category})
if not type_name:
raise ValidationException(message=_("类型名称不能为空"), errors={"type_name": "required"})
if category == CaseMaterialCategory.PARTY:
if side not in {CaseMaterialSide.OUR, CaseMaterialSide.OPPONENT}:
raise ValidationException(message=_("当事人方向不合法"), errors={"side": side})
supervising_authority_id = None
else:
side = None
if not supervising_authority_id:
raise ValidationException(
message=_("必须选择主管机关"), errors={"supervising_authority_id": "required"}
)
supervising_authority_id = int(supervising_authority_id)
if supervising_authority_id not in authorities_by_id:
raise ValidationException(
message=_("主管机关不属于该案件"),
errors={"supervising_authority_id": supervising_authority_id},
)
resolved_type = self._resolve_type_cached(
cache=type_cache, category=category, type_id=type_id, type_name=type_name, law_firm_id=law_firm_id
)
material, _created = CaseMaterial.objects.update_or_create(
source_attachment=attachments[att_id],
defaults={
"case_id": case_id,
"category": category,
"type": resolved_type,
"type_name": resolved_type.name,
"side": side,
"supervising_authority_id": supervising_authority_id,
},
)
if category == CaseMaterialCategory.PARTY:
validated_party_ids = self._validate_party_ids(party_ids, parties_by_id, side)
material.parties.set(validated_party_ids)
else:
material.parties.clear()
saved.append(material)
# 根据绑定顺序自动创建/更新分组排序,保留用户文件夹顺序
if saved:
self._ensure_group_orders(case_id, saved)
return saved
def _ensure_group_orders(self, case_id: int, saved: list[CaseMaterial]) -> None:
"""根据绑定顺序自动创建/更新分组排序记录,保留用户文件夹顺序。
遍历 saved 列表(按前端传入顺序),收集每个 (category, side, supervising_authority_id)
下的 type 首次出现顺序,然后用 update_or_create 写入 CaseMaterialGroupOrder。
已存在的分组排序记录(之前通过拖拽设置的)会被保留,只补充新出现的分组。
"""
# 收集每个 (category, side, authority_id) 下的 type 出现顺序
group_order_map: dict[tuple[str, str | None, int | None], list[int]] = {}
for material in saved:
if not material.type_id:
continue
key = (material.category, material.side, material.supervising_authority_id)
if material.type_id not in group_order_map.get(key, []):
group_order_map.setdefault(key, []).append(material.type_id)
# 读取已有的分组排序,确定已有分组的 sort_index
existing_orders = CaseMaterialGroupOrder.objects.filter(case_id=case_id)
existing_max_index: dict[tuple[str, str | None, int | None], int] = {}
for order in existing_orders:
key = (order.category, order.side, order.supervising_authority_id)
existing_max_index[key] = max(existing_max_index.get(key, -1), order.sort_index)
# 对每个分组维度,为未设置排序的新分组追加 sort_index
for (category, side, authority_id), type_ids in group_order_map.items():
existing_type_ids = set(
CaseMaterialGroupOrder.objects.filter(
case_id=case_id,
category=category,
side=side,
supervising_authority_id=authority_id,
).values_list("type_id", flat=True)
)
next_index = existing_max_index.get((category, side, authority_id), -1) + 1
for type_id in type_ids:
if type_id in existing_type_ids:
continue
CaseMaterialGroupOrder.objects.update_or_create(
case_id=case_id,
category=category,
side=side,
supervising_authority_id=authority_id,
type_id=type_id,
defaults={"sort_index": next_index},
)
next_index += 1
def _resolve_type_cached(
self,
cache: dict[str, CaseMaterialType],
*,
category: str,
type_id: int | None,
type_name: str,
law_firm_id: int | None,
) -> CaseMaterialType:
cache_key = f"id:{type_id}" if type_id else f"name:{category}:{type_name}:{law_firm_id}"
cached = cache.get(cache_key)
if cached:
return cached
resolved = self._resolve_type(category=category, type_id=type_id, type_name=type_name, law_firm_id=law_firm_id)
cache[cache_key] = resolved
return resolved
def _resolve_type(
self, *, category: str, type_id: int | None, type_name: str, law_firm_id: int | None
) -> CaseMaterialType:
if type_id:
try:
t = CaseMaterialType.objects.get(id=int(type_id), category=category, is_active=True)
except CaseMaterialType.DoesNotExist:
raise ValidationException(message=_("材料类型不存在"), errors={"type_id": type_id}) from None
return t
qs = CaseMaterialType.objects.filter(category=category, name=type_name, is_active=True).order_by(
models.Case(
models.When(law_firm_id=law_firm_id, then=0),
models.When(law_firm_id=None, then=1),
default=2,
output_field=models.IntegerField(),
)
)
t: CaseMaterialType | None = qs.first() # type: ignore
if t is not None:
return t
return CaseMaterialType.objects.create(
category=category, name=type_name, law_firm_id=law_firm_id, is_active=True
)
def _validate_party_ids(
self, party_ids: Sequence[Any], parties_by_id: dict[int, CaseParty], side: str
) -> list[int]:
validated: list[int] = []
for pid in party_ids:
try:
pid_int = int(pid)
except (TypeError, ValueError):
continue
party = parties_by_id.get(pid_int)
if not party:
raise ValidationException(message=_("包含无效当事人"), errors={"party_ids": pid_int})
is_our = bool(getattr(getattr(party, "client", None), "is_our_client", False))
if side == CaseMaterialSide.OUR and (not is_our):
raise ValidationException(message=_("当事人不属于我方"), errors={"party_ids": pid_int})
if side == CaseMaterialSide.OPPONENT and is_our:
raise ValidationException(message=_("当事人不属于对方"), errors={"party_ids": pid_int})
validated.append(pid_int)
return validated