File size: 10,445 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 | """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
|