Download apps/cases/services/case/case_admin_service.py from jun2024/fc: direct link, hf CLI and curl.
- Browser
- Download file 22.4 kB
-
https://huggingface.co/spaces/jun2024/fc/resolve/main/apps/cases/services/case/case_admin_service.py
- Command line
-
hf download hf://spaces/jun2024/fc/apps/cases/services/case/case_admin_service.py
-
curl -L -o case_admin_service.py https://huggingface.co/spaces/jun2024/fc/resolve/main/apps/cases/services/case/case_admin_service.py
22.4 kB
| """Business logic services.""" | |
| from __future__ import annotations | |
| import logging | |
| from importlib import import_module | |
| from typing import TYPE_CHECKING, Protocol, TypedDict, cast | |
| from django.db import transaction | |
| from django.utils.translation import gettext_lazy as _ | |
| from apps.cases.models import Case, CaseAssignment, CaseNumber, CaseParty, SupervisingAuthority | |
| from .wiring import get_case_filing_number_service, get_document_service | |
| if TYPE_CHECKING: | |
| from django.db.models import QuerySet | |
| from apps.cases.services.case_import_service import CaseImportPayload | |
| from apps.core.interfaces import ICaseFilingNumberService, IDocumentService | |
| logger = logging.getLogger(__name__) | |
| JSONDict = dict[str, object] | |
| ImportData = dict[str, object] | |
| class MaterialViewPayload(TypedDict): | |
| party_types: list[JSONDict] | |
| non_party_types: list[JSONDict] | |
| our_parties: list[MaterialPartyPayload] | |
| opponent_parties: list[MaterialPartyPayload] | |
| authorities: list[AuthorityPayload] | |
| class SimplePartyPayload(TypedDict): | |
| id: int | |
| name: str | |
| class DetailPartyPayload(TypedDict): | |
| id: int | |
| name: str | |
| client_type: str | |
| legal_status: str | None | |
| legal_status_display: str | |
| class MaterialPartyPayload(TypedDict): | |
| id: int | |
| name: str | |
| legal_status: str | None | |
| legal_status_display: str | |
| class AuthorityPayload(TypedDict): | |
| id: int | |
| name: str | |
| authority_type: str | |
| authority_type_display: str | |
| class CaseMaterialServiceProtocol(Protocol): | |
| def get_used_type_ids(self, *, case_id: int) -> list[int]: ... | |
| def get_material_types_by_category( | |
| self, | |
| *, | |
| category: str, | |
| law_firm_id: int | None, | |
| used_type_ids: list[int], | |
| ) -> list[JSONDict]: ... | |
| class CaseImportServiceProtocol(Protocol): | |
| def import_one(self, data: CaseImportPayload) -> Case: ... | |
| class CaseAdminService: | |
| """案件 Admin 服务""" | |
| def __init__( | |
| self, | |
| document_service: IDocumentService | None = None, | |
| filing_number_service: ICaseFilingNumberService | None = None, | |
| ) -> None: | |
| """ | |
| 构造函数支持依赖注入 | |
| document_service: 文档服务实例(可选,用于依赖注入) | |
| filing_number_service: 建档编号服务实例(可选,用于依赖注入) | |
| """ | |
| self._document_service = document_service | |
| self._filing_number_service = filing_number_service | |
| def document_service(self) -> IDocumentService: | |
| """ | |
| 延迟加载文档服务 | |
| 通过 ServiceLocator 获取 IDocumentService 实例, | |
| 支持依赖注入以便于测试. | |
| IDocumentService 实例 | |
| """ | |
| if self._document_service is None: | |
| self._document_service = get_document_service() | |
| return self._document_service | |
| def filing_number_service(self) -> ICaseFilingNumberService: | |
| """ | |
| 延迟加载建档编号服务 | |
| 支持依赖注入以便于测试. | |
| FilingNumberService 实例 | |
| """ | |
| if self._filing_number_service is None: | |
| self._filing_number_service = get_case_filing_number_service() | |
| return self._filing_number_service | |
| def get_matched_folder_templates(self, case_type: str, legal_statuses: list[str] | None = None) -> str: | |
| """ | |
| 获取匹配的文件夹模板 | |
| case_type: 案件类型 | |
| legal_statuses: 案件的诉讼地位列表(我方当事人的诉讼地位) | |
| 模板名称字符串,多个模板用"、"分隔 | |
| 如果查询失败返回 "查询失败" | |
| """ | |
| try: | |
| if legal_statuses: | |
| return cast( | |
| str, | |
| self.document_service.get_matched_folder_templates_with_legal_status(case_type, legal_statuses), | |
| ) | |
| return cast(str, self.document_service.get_matched_folder_templates(case_type)) | |
| except Exception: | |
| logger.exception( | |
| "get_matched_folder_templates_failed", extra={"case_type": case_type, "legal_statuses": legal_statuses} | |
| ) | |
| return str(_("查询失败")) | |
| def get_matched_folder_templates_list( | |
| self, case_type: str, legal_statuses: list[str] | None = None | |
| ) -> list[JSONDict]: | |
| try: | |
| module = import_module("apps.documents.services.template.template_matching_service") | |
| template_matching_service_cls = module.TemplateMatchingService | |
| return cast( | |
| list[JSONDict], | |
| template_matching_service_cls().find_matching_case_folder_templates_list( | |
| case_type=case_type, | |
| legal_statuses=legal_statuses, | |
| ), | |
| ) | |
| except Exception: | |
| logger.exception("get_matched_folder_templates_list_failed", extra={"case_type": case_type}) | |
| return [] | |
| def get_matched_case_file_templates( | |
| self, | |
| case_type: str, | |
| case_stage: str, | |
| applicable_institutions: list[str] | None = None, | |
| ) -> list[JSONDict]: | |
| try: | |
| return cast( | |
| list[JSONDict], | |
| self.document_service.find_matching_case_file_templates( | |
| case_type=case_type, | |
| case_stage=case_stage, | |
| applicable_institutions=applicable_institutions, | |
| ), | |
| ) | |
| except Exception: | |
| logger.exception( | |
| "get_matched_case_file_templates_failed", | |
| extra={ | |
| "case_type": case_type, | |
| "case_stage": case_stage, | |
| "applicable_institutions": applicable_institutions, | |
| }, | |
| ) | |
| return [] | |
| def get_case_file_sub_type_choices(self) -> list[tuple[str, str]]: | |
| """获取案件文件子类型选项。""" | |
| try: | |
| choices_module = import_module("apps.documents.models.choices") | |
| return list(choices_module.DocumentCaseFileSubType.choices) | |
| except Exception: | |
| logger.exception("get_case_file_sub_type_choices_failed") | |
| return [] | |
| def get_case_file_templates_for_detail(self, case: Case) -> tuple[list[JSONDict], str]: | |
| """获取详情页案件文件模板与缺失原因。""" | |
| if not case.case_type: | |
| return [], str(_("未设置案件类型")) | |
| if not case.current_stage: | |
| return [], str(_("未设置案件阶段")) | |
| applicable_institutions = self._get_case_applicable_institutions(case) | |
| return ( | |
| self.get_matched_case_file_templates( | |
| case_type=case.case_type, | |
| case_stage=case.current_stage, | |
| applicable_institutions=applicable_institutions, | |
| ), | |
| "", | |
| ) | |
| def _get_case_applicable_institutions(self, case: Case) -> list[str]: | |
| names: list[str] = [] | |
| seen: set[str] = set() | |
| for authority in case.supervising_authorities.all(): | |
| name = str(getattr(authority, "name", "") or "").strip() | |
| if not name or name in seen: | |
| continue | |
| seen.add(name) | |
| names.append(name) | |
| return names | |
| def build_our_legal_entities(self, case: Case) -> list[SimplePartyPayload]: | |
| """构建我方主体(法人)视图数据。""" | |
| return [ | |
| {"id": party.client.id, "name": party.client.name} | |
| for party in case.parties.all() | |
| if getattr(party.client, "is_our_client", False) and getattr(party.client, "client_type", "") == "legal" | |
| ] | |
| def build_our_parties(self, case: Case) -> list[DetailPartyPayload]: | |
| """构建我方当事人视图数据。""" | |
| parties: list[DetailPartyPayload] = [] | |
| for party in case.parties.all(): | |
| client = party.client | |
| if not getattr(client, "is_our_client", False): | |
| continue | |
| parties.append( | |
| { | |
| "id": client.id, | |
| "name": client.name, | |
| "client_type": getattr(client, "client_type", "") or "", | |
| "legal_status": getattr(party, "legal_status", None), | |
| "legal_status_display": ( | |
| getattr(party, "get_legal_status_display", lambda: "")() | |
| if getattr(party, "legal_status", None) | |
| else "" | |
| ), | |
| } | |
| ) | |
| return parties | |
| def build_respondents(self, case: Case) -> list[SimplePartyPayload]: | |
| """构建对方当事人视图数据。""" | |
| return [ | |
| {"id": party.client.id, "name": party.client.name} | |
| for party in case.parties.all() | |
| if not getattr(party.client, "is_our_client", False) | |
| ] | |
| def build_material_view_parties(self, case: Case) -> tuple[list[MaterialPartyPayload], list[MaterialPartyPayload]]: | |
| """构建材料页我方/对方当事人数据。""" | |
| our_parties: list[MaterialPartyPayload] = [] | |
| opponent_parties: list[MaterialPartyPayload] = [] | |
| for party in case.parties.all(): | |
| client = party.client | |
| item: MaterialPartyPayload = { | |
| "id": party.id, | |
| "name": getattr(client, "name", "") or "", | |
| "legal_status": getattr(party, "legal_status", None), | |
| "legal_status_display": ( | |
| getattr(party, "get_legal_status_display", lambda: "")() | |
| if getattr(party, "legal_status", None) | |
| else "" | |
| ), | |
| } | |
| if getattr(client, "is_our_client", False): | |
| our_parties.append(item) | |
| else: | |
| opponent_parties.append(item) | |
| return our_parties, opponent_parties | |
| def build_material_view_authorities(self, case: Case) -> list[AuthorityPayload]: | |
| """构建材料页主管机关数据。""" | |
| return [ | |
| { | |
| "id": authority.id, | |
| "name": authority.name or "", | |
| "authority_type": authority.authority_type or "", | |
| "authority_type_display": authority.get_authority_type_display() if authority.authority_type else "", | |
| } | |
| for authority in case.supervising_authorities.all().order_by("created_at") | |
| ] | |
| def get_case_with_admin_relations(self, case_id: int) -> Case | None: | |
| """按 admin 详情页需求查询案件及关联数据。""" | |
| from django.db.models import Prefetch | |
| from apps.cases.models import CaseLog | |
| try: | |
| return cast( | |
| Case, | |
| Case.objects.select_related( | |
| "contract", | |
| "folder_binding", | |
| ) | |
| .prefetch_related( | |
| "case_numbers", | |
| "supervising_authorities", | |
| "parties__client", | |
| "assignments__lawyer", | |
| Prefetch( | |
| "logs", | |
| queryset=CaseLog.objects.select_related("actor") | |
| .prefetch_related("attachments") | |
| .order_by("-created_at"), | |
| ), | |
| "chats", | |
| ) | |
| .get(pk=case_id), | |
| ) | |
| except Case.DoesNotExist: | |
| return None | |
| def build_materials_view_payload( | |
| self, | |
| *, | |
| case: Case, | |
| material_service: CaseMaterialServiceProtocol, | |
| law_firm_id: int | None, | |
| ) -> MaterialViewPayload: | |
| """构建材料页所需数据。""" | |
| used_type_ids = material_service.get_used_type_ids(case_id=case.id) | |
| party_types = material_service.get_material_types_by_category( | |
| category="party", | |
| law_firm_id=law_firm_id, | |
| used_type_ids=used_type_ids, | |
| ) | |
| non_party_types = material_service.get_material_types_by_category( | |
| category="non_party", | |
| law_firm_id=law_firm_id, | |
| used_type_ids=used_type_ids, | |
| ) | |
| our_parties, opponent_parties = self.build_material_view_parties(case) | |
| authorities = self.build_material_view_authorities(case) | |
| return { | |
| "party_types": party_types, | |
| "non_party_types": non_party_types, | |
| "our_parties": our_parties, | |
| "opponent_parties": opponent_parties, | |
| "authorities": authorities, | |
| } | |
| def group_templates_by_sub_type( | |
| self, | |
| templates: list[JSONDict], | |
| sub_type_choices: list[tuple[str, str]], | |
| *, | |
| exclude_special_sub_types: bool = True, | |
| ) -> list[tuple[str, list[JSONDict]]]: | |
| """按案件文件子类型分组模板。""" | |
| special_sub_types = {"power_of_attorney_materials", "property_preservation_materials"} | |
| label_map = dict(sub_type_choices) | |
| groups: dict[str, list[JSONDict]] = {} | |
| for template in templates: | |
| sub_type = cast(str, template.get("case_sub_type", "other_materials")) | |
| if exclude_special_sub_types and sub_type in special_sub_types: | |
| continue | |
| groups.setdefault(sub_type, []).append(template) | |
| order = [choice[0] for choice in sub_type_choices] | |
| order_set = set(order) | |
| ordered_keys = [key for key in order if key in groups] | |
| extra_keys = sorted(key for key in groups.keys() if key not in order_set) | |
| ordered_keys.extend(extra_keys) | |
| return [(label_map.get(key, key), groups[key]) for key in ordered_keys] | |
| def detect_special_template_flags(self, unified_templates: list[JSONDict]) -> tuple[bool, bool]: | |
| """识别详情页特殊模板标记。""" | |
| has_preservation_template = any( | |
| template.get("function_code") == "preservation_application" | |
| or "财产保全申请书" in cast(str, template.get("name") or "") | |
| for template in unified_templates | |
| ) | |
| has_delay_delivery_template = any( | |
| template.get("function_code") == "delay_delivery_application" | |
| or "暂缓送达申请书" in cast(str, template.get("name") or "") | |
| for template in unified_templates | |
| ) | |
| return has_preservation_template, has_delay_delivery_template | |
| def serialize_queryset_for_export(self, queryset: QuerySet[Case]) -> list[JSONDict]: | |
| """序列化案件列表用于 Admin 导出。""" | |
| from apps.cases.services.case.case_admin_export_bridge import get_case_admin_export_prefetches | |
| from apps.cases.services.case.case_contract_export_bridge import ( | |
| get_case_admin_contract_export_prefetches, | |
| serialize_contract_for_case_export, | |
| ) | |
| from apps.cases.services.case.case_export_serializer_service import serialize_case_obj | |
| result: list[JSONDict] = [] | |
| for case in queryset.prefetch_related( | |
| *get_case_admin_export_prefetches(), | |
| *get_case_admin_contract_export_prefetches(), | |
| ): | |
| data = serialize_case_obj(case) | |
| data["contract"] = serialize_contract_for_case_export(case.contract) if case.contract else None | |
| result.append(data) | |
| return result | |
| def collect_file_paths_for_export(self, queryset: QuerySet[Case]) -> list[str]: | |
| """收集案件导出相关文件路径。""" | |
| from apps.cases.services.case.case_admin_export_bridge import ( | |
| collect_case_file_paths_for_export, | |
| get_case_admin_file_prefetches, | |
| ) | |
| from apps.cases.services.case.case_contract_export_bridge import ( | |
| collect_contract_file_paths_for_case_export, | |
| get_case_admin_contract_file_prefetches, | |
| ) | |
| seen: set[str] = set() | |
| paths: list[str] = [] | |
| def _add(path: str) -> None: | |
| if path and path not in seen: | |
| seen.add(path) | |
| paths.append(path) | |
| for case in queryset.prefetch_related( | |
| *get_case_admin_file_prefetches(), | |
| *get_case_admin_contract_file_prefetches(), | |
| ): | |
| if case.contract: | |
| collect_contract_file_paths_for_case_export(case.contract, _add) | |
| collect_case_file_paths_for_export(case, _add) | |
| return paths | |
| def import_cases_from_json_data( | |
| self, | |
| data_list: list[ImportData], | |
| *, | |
| case_import_service: CaseImportServiceProtocol, | |
| ) -> tuple[int, int, list[str]]: | |
| """执行 Admin JSON 导入并返回成功/跳过/错误统计。""" | |
| success = skipped = 0 | |
| errors: list[str] = [] | |
| for index, item in enumerate(data_list, 1): | |
| try: | |
| filing_number = item.get("filing_number") | |
| before = Case.objects.filter(filing_number=filing_number).exists() if filing_number else False | |
| case_import_service.import_one(item) | |
| if before: | |
| skipped += 1 | |
| else: | |
| success += 1 | |
| except Exception as exc: | |
| logger.exception("导入案件失败", extra={"index": index, "case_name": item.get("name", "?")}) | |
| errors.append(f"[{index}] {item.get('name', '?')} ({type(exc).__name__}): {exc}") | |
| return success, skipped, errors | |
| def duplicate_case(self, case_id: int) -> Case: | |
| """ | |
| 复制案件及其所有关联数据(不复制日志和群聊) | |
| case_id: 原案件ID | |
| 新创建的案件对象 | |
| """ | |
| # 获取原案件 | |
| original = Case.objects.get(pk=case_id) | |
| # 复制主对象 | |
| new_case = Case.objects.create( | |
| contract=original.contract, | |
| is_archived=False, # 副本默认未建档 | |
| name=f"{original.name} (副本)", | |
| status=original.status, | |
| effective_date=original.effective_date, | |
| cause_of_action=original.cause_of_action, | |
| target_amount=original.target_amount, | |
| case_type=original.case_type, | |
| current_stage=original.current_stage, | |
| ) | |
| # 批量复制当事人 | |
| parties_to_create = [ | |
| CaseParty( | |
| case=new_case, | |
| client=party.client, | |
| legal_status=party.legal_status, | |
| ) | |
| for party in original.parties.all() | |
| ] | |
| CaseParty.objects.bulk_create(parties_to_create) | |
| # 批量复制律师指派 | |
| assignments_to_create = [ | |
| CaseAssignment( | |
| case=new_case, | |
| lawyer=assignment.lawyer, | |
| ) | |
| for assignment in original.assignments.all() | |
| ] | |
| CaseAssignment.objects.bulk_create(assignments_to_create) | |
| # 批量复制主管机关 | |
| authorities_to_create = [ | |
| SupervisingAuthority( | |
| case=new_case, | |
| name=authority.name, | |
| authority_type=authority.authority_type, | |
| ) | |
| for authority in original.supervising_authorities.all() | |
| ] | |
| SupervisingAuthority.objects.bulk_create(authorities_to_create) | |
| # 批量复制案号 | |
| case_numbers_to_create = [ | |
| CaseNumber( | |
| case=new_case, | |
| number=case_number.number, | |
| remarks=case_number.remarks, | |
| ) | |
| for case_number in original.case_numbers.all() | |
| ] | |
| CaseNumber.objects.bulk_create(case_numbers_to_create) | |
| # 注意:不复制 CaseLog(日志)和 CaseChat(群聊) | |
| return new_case | |
| def handle_case_filing_change(self, case_id: int, is_archived: bool) -> str | None: | |
| """ | |
| 处理案件建档状态变化 | |
| 业务逻辑: | |
| - 如果 is_archived=True 且 filing_number 为空,调用 FilingNumberService 生成编号 | |
| - 如果 is_archived=True 且 filing_number 已存在,返回现有编号 | |
| - 如果 is_archived=False,不修改 filing_number(保留在数据库中) | |
| case_id: 案件ID | |
| is_archived: 是否已建档 | |
| str | None: 建档编号(如果已建档) | |
| NotFoundError: 案件不存在 | |
| ValidationException: 数据验证失败 | |
| Requirements: 5.1, 5.2, 6.1, 6.2, 6.3, 6.4 | |
| """ | |
| from apps.core.exceptions import NotFoundError | |
| try: | |
| case = Case.objects.get(pk=case_id) | |
| except Case.DoesNotExist: | |
| raise NotFoundError( | |
| message=_("案件不存在"), code="CASE_NOT_FOUND", errors={"case_id": f"ID为 {case_id} 的案件不存在"} | |
| ) from None | |
| # 如果取消建档,不修改 filing_number(保留在数据库中) | |
| if not is_archived: | |
| logger.info( | |
| "取消案件建档,保留建档编号", | |
| extra={"case_id": case_id, "filing_number": case.filing_number, "action": "handle_case_filing_change"}, | |
| ) | |
| return None | |
| # 如果已建档且已有编号,返回现有编号 | |
| if case.filing_number: | |
| logger.info( | |
| "案件已有建档编号,返回现有编号", | |
| extra={"case_id": case_id, "filing_number": case.filing_number, "action": "handle_case_filing_change"}, | |
| ) | |
| return cast(str, case.filing_number) | |
| # 如果已建档但没有编号,生成新编号 | |
| created_year = case.start_date.year | |
| case_type = str(case.case_type or "") | |
| filing_number = cast( | |
| str, | |
| self.filing_number_service.generate_case_filing_number_internal( | |
| case_id=case_id, | |
| case_type=case_type, | |
| created_year=created_year, | |
| ), | |
| ) | |
| # 保存编号到数据库 | |
| case.filing_number = filing_number | |
| case.save(update_fields=["filing_number"]) | |
| logger.info( | |
| "生成并保存案件建档编号", | |
| extra={"case_id": case_id, "filing_number": filing_number, "action": "handle_case_filing_change"}, | |
| ) | |
| return filing_number | |