fc / apps /cases /services /template /case_document_template_admin_service.py
jun2024's picture
Upload 2241 files
b4f2238 verified
Raw History Blame Contribute Delete
13.7 kB
"""
案件文件模板 Admin 服务
为案件编辑页的文件模板模块提供业务逻辑支持.
负责获取匹配的模板、可绑定的模板以及模板显示数据.
Requirements: 1.2, 1.3, 1.4, 1.5, 1.6, 2.3, 2.4
"""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Any
from django.utils.translation import gettext_lazy as _
from .repo import CaseTemplateBindingRepo
from .wiring import get_document_service
if TYPE_CHECKING:
from apps.core.interfaces import IDocumentService
from .case_template_binding_service import CaseTemplateBindingService
logger = logging.getLogger(__name__)
class CaseDocumentTemplateAdminService:
"""
案件文件模板 Admin 服务
为案件编辑页的文件模板 Inline 模块提供业务逻辑支持.
使用依赖注入模式,支持 DocumentService 和 CaseTemplateBindingService.
Requirements: 1.2, 1.3, 1.4, 1.5, 1.6, 2.3, 2.4
"""
def __init__(
self,
document_service: IDocumentService | None = None,
binding_service: CaseTemplateBindingService | None = None,
repo: CaseTemplateBindingRepo | None = None,
) -> None:
self._document_service = document_service
self._binding_service = binding_service
self._repo = repo or CaseTemplateBindingRepo()
@property
def document_service(self) -> IDocumentService:
"""延迟加载文档服务"""
if self._document_service is None:
self._document_service = get_document_service()
return self._document_service
@property
def binding_service(self) -> CaseTemplateBindingService:
"""延迟加载绑定服务"""
if self._binding_service is None:
from .wiring import get_case_template_binding_service
self._binding_service = get_case_template_binding_service()
return self._binding_service
@property
def repo(self) -> CaseTemplateBindingRepo:
return self._repo
def get_matched_templates_for_case(
self, case_id: int, case_type: str, case_stage: str, legal_statuses: list[str]
) -> list[dict[str, Any]]:
"""
获取案件匹配的文件模板
根据案件类型、阶段和诉讼地位筛选匹配的模板.
匹配规则:
- 模板的 case_types 包含 'all' 或包含案件的 case_type,或为空列表
- 模板的 case_stages 包含 'all' 或包含案件的 current_stage,或为空列表
- 模板的 legal_statuses 为空时匹配任意诉讼地位(Requirements 1.6)
- 模板的 legal_statuses 非空时,根据 legal_status_match_mode 进行匹配
case_id: 案件ID
case_type: 案件类型
case_stage: 案件阶段
legal_statuses: 我方当事人的诉讼地位列表
匹配的模板列表,每个元素包含:
- id: 模板ID
- name: 模板名称
- description: 模板描述
- case_sub_type: 案件文件子类型
- case_sub_type_display: 案件文件子类型显示名称
- function_code: 功能标识(可为 null)
Requirements: 1.3, 1.4, 1.5, 1.6
"""
if not case_type:
logger.info("案件 %s 未设置案件类型,返回空匹配列表", case_id)
return []
templates = self.document_service.list_case_templates_internal(is_active=True)
matched: list[dict[str, Any]] = []
case_legal_statuses_set = set(legal_statuses) if legal_statuses else set()
sub_type_display: dict[str | None, str] = {}
for template in templates:
# 1. 匹配案件类型 (Requirements 1.3)
template_case_types = getattr(template, "case_types", None) or []
if template_case_types and "all" not in template_case_types and case_type not in template_case_types:
continue
# 2. 匹配案件阶段 (Requirements 1.4)
template_case_stages = getattr(template, "case_stages", None) or []
if template_case_stages and case_stage:
# 规范化案件阶段(处理再审相关阶段)
normalized_stages = self._normalize_case_stage(case_stage)
if "all" not in template_case_stages and not any(
stage in template_case_stages for stage in normalized_stages
):
continue
# 3. 匹配诉讼地位 (Requirements 1.5, 1.6)
template_legal_statuses = getattr(template, "legal_statuses", None) or []
match_mode = getattr(template, "legal_status_match_mode", None) or "any"
if not self._match_legal_status(template_legal_statuses, case_legal_statuses_set, match_mode):
continue
# 模板匹配成功,添加到结果列表
matched.append(
{
"id": template.id,
"name": getattr(template, "name", "") or "",
"description": getattr(template, "description", "") or "",
"case_sub_type": getattr(template, "case_sub_type", None),
"case_sub_type_display": sub_type_display.get(
getattr(template, "case_sub_type", None), getattr(template, "case_sub_type", None) or ""
),
"function_code": getattr(template, "function_code", None),
}
)
logger.info(
"案件 %s 匹配到 %s 个文件模板,case_type=%s, case_stage=%s, legal_statuses=%s",
case_id,
len(matched),
case_type,
case_stage,
legal_statuses,
)
return matched
def get_available_templates_for_binding(self, case_id: int, exclude_template_ids: set[int]) -> list[dict[str, Any]]:
"""
获取可手动绑定的模板列表
排除已自动匹配和已手动绑定的模板.
case_id: 案件ID
exclude_template_ids: 需要排除的模板ID集合(已自动匹配 + 已手动绑定)
可绑定的模板列表,每个元素包含:
- template_id: 模板ID
- name: 模板名称
- description: 模板描述
- case_sub_type: 案件文件子类型
- case_sub_type_display: 案件文件子类型显示名称
- function_code: 功能标识(可为 null)
Requirements: 2.3, 2.4
"""
templates = [
t
for t in self.document_service.list_case_templates_internal(is_active=True)
if t.id not in exclude_template_ids
]
templates.sort(key=lambda x: ((getattr(x, "case_sub_type", "") or ""), (getattr(x, "name", "") or "")))
sub_type_display: dict[str | None, str] = {}
result = [
{
"template_id": t.id,
"name": getattr(t, "name", "") or "",
"description": getattr(t, "description", "") or "",
"case_sub_type": getattr(t, "case_sub_type", None),
"case_sub_type_display": sub_type_display.get(
getattr(t, "case_sub_type", None), getattr(t, "case_sub_type", None) or ""
),
"function_code": getattr(t, "function_code", None),
}
for t in templates
]
logger.info("案件 %s 可绑定模板数量: %s,排除模板数量: %s", case_id, len(result), len(exclude_template_ids))
return result
def get_templates_display_data(self, case_id: int) -> dict[str, Any]:
"""
获取模板显示数据
返回自动匹配和手动绑定的模板,分组显示.
case_id: 案件ID
包含以下字段的字典:
- auto_matched: 自动匹配的模板列表
- manual_bound: 手动绑定的模板列表
- available_for_binding: 可手动绑定的模板列表
- total_count: 总模板数量
"""
from apps.cases.models import BindingSource
from apps.core.exceptions import NotFoundError
# 获取案件信息
case = self.repo.get_case_optional(case_id)
if not case:
raise NotFoundError(
message=_("案件不存在"), code="CASE_NOT_FOUND", errors={"case_id": f"ID 为 {case_id} 的案件不存在"}
)
# 获取案件的诉讼地位列表(我方当事人)
legal_statuses = self.repo.get_our_legal_statuses(case)
# 1. 获取自动匹配的模板
auto_matched = self.get_matched_templates_for_case(
case_id=case_id,
case_type=case.case_type or "",
case_stage=case.current_stage or "",
legal_statuses=legal_statuses,
)
auto_matched_ids = {t["id"] for t in auto_matched}
# 2. 获取已手动绑定的模板
# 这里的 filter 逻辑可以保留,或者也在 repo 中添加专门方法
# 但既然我们有 get_bindings_by_case_id,可以复用并过滤
bindings = self.repo.get_bindings_by_case_id(case_id)
manual_bindings = [b for b in bindings if b.binding_source == BindingSource.MANUAL_BOUND]
sub_type_display: dict[str | None, str] = {}
manual_bound: list[dict[str, Any]] = []
manual_bound_ids: set[int] = set()
for binding in manual_bindings:
template = binding.template
manual_bound_ids.add(template.id)
manual_bound.append(
{
"binding_id": binding.id,
"id": template.id,
"name": template.name,
"description": template.description or "",
"case_sub_type": template.case_sub_type,
"case_sub_type_display": sub_type_display.get(template.case_sub_type, template.case_sub_type or ""),
"function_code": getattr(template, "function_code", None),
"created_at": binding.created_at.isoformat() if binding.created_at else None,
}
)
# 3. 获取可手动绑定的模板(排除自动匹配和已手动绑定的)
exclude_ids = auto_matched_ids | manual_bound_ids
available_for_binding = self.get_available_templates_for_binding(
case_id=case_id, exclude_template_ids=exclude_ids
)
total_count = len(auto_matched) + len(manual_bound)
logger.info(
"案件 %d 模板显示数据: auto_matched=%d, manual_bound=%d, available=%d",
case_id,
len(auto_matched),
len(manual_bound),
len(available_for_binding),
)
return {
"auto_matched": auto_matched,
"manual_bound": manual_bound,
"available_for_binding": available_for_binding,
"total_count": total_count,
}
def _match_legal_status(
self, template_legal_statuses: list[str], case_legal_statuses_set: set[str], match_mode: str
) -> bool:
"""
匹配诉讼地位
根据模板的诉讼地位配置和匹配模式,判断是否匹配案件的诉讼地位.
匹配规则:
- 模板诉讼地位为空时,匹配任意诉讼地位(Requirements 1.6)
- any 模式:案件诉讼地位与模板诉讼地位有交集,或案件诉讼地位为空
- all 模式:案件诉讼地位包含模板的所有诉讼地位
- exact 模式:案件诉讼地位与模板诉讼地位完全一致
template_legal_statuses: 模板配置的诉讼地位列表
case_legal_statuses_set: 案件的诉讼地位集合
match_mode: 匹配模式(any/all/exact)
是否匹配
Requirements: 1.5, 1.6
"""
# 模板未配置诉讼地位,匹配任意 (Requirements 1.6)
if not template_legal_statuses:
return True
template_legal_statuses_set = set(template_legal_statuses)
if match_mode == "any":
# 任意匹配:案件诉讼地位为空,或与模板诉讼地位有交集
if not case_legal_statuses_set:
return True
return bool(case_legal_statuses_set & template_legal_statuses_set)
elif match_mode == "all":
# 全部包含:案件诉讼地位包含模板的所有诉讼地位
return template_legal_statuses_set <= case_legal_statuses_set
elif match_mode == "exact":
# 完全一致:案件诉讼地位与模板诉讼地位完全相同
return case_legal_statuses_set == template_legal_statuses_set
# 未知匹配模式,默认使用 any 模式
if not case_legal_statuses_set:
return True
return bool(case_legal_statuses_set & template_legal_statuses_set)
def _normalize_case_stage(self, case_stage: str) -> list[str]:
"""
规范化案件阶段
将再审相关的多个阶段统一映射到 'retrial'.
case_stage: 原始案件阶段
规范化后的阶段列表
"""
if not case_stage:
return []
# 再审相关阶段映射
retrial_related = {
"retrial_first",
"retrial_second",
"apply_retrial",
"rehearing_first",
"rehearing_second",
"review",
"petition",
"apply_protest",
"petition_protest",
}
if case_stage in retrial_related:
return ["retrial", case_stage]
return [case_stage]