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