Files
leaudit-platform-backend/fastapi_modules/fastapi_leaudit/services/impl/evaluationPointGroupServiceImpl.py
T

815 lines
38 KiB
Python

"""评查点分组服务实现(新链路:文档类型 -> 子类型 -> 规则集)。"""
from __future__ import annotations
from datetime import datetime
from typing import Any
from sqlalchemy import bindparam, text
from fastapi_common.fastapi_common_sqlalchemy.database import GetAsyncSession
from fastapi_common.fastapi_common_web.domain.responses import StatusCodeEnum
from fastapi_common.fastapi_common_web.exception.LeauditException import LeauditException
from fastapi_modules.fastapi_leaudit.domian.Dto.evaluationPointGroupDto import (
EvaluationPointGroupBatchDeleteDTO,
EvaluationPointGroupBatchStatusDTO,
EvaluationPointGroupBindingCreateDTO,
EvaluationPointGroupBindingUpdateDTO,
EvaluationPointGroupCreateDTO,
EvaluationPointGroupRebindDTO,
EvaluationPointGroupUpdateDTO,
)
from fastapi_modules.fastapi_leaudit.domian.vo.evaluationPointGroupVo import (
EvaluationPointGroupBatchDeleteVO,
EvaluationPointGroupBatchStatusVO,
EvaluationPointGroupDeleteVO,
EvaluationPointGroupListVO,
EvaluationPointGroupRebindVO,
EvaluationPointGroupVO,
RuleGroupBindingVO,
)
from fastapi_modules.fastapi_leaudit.services.evaluationPointGroupService import IEvaluationPointGroupService
from fastapi_modules.fastapi_leaudit.services.impl.ruleGroupSupport import (
bootstrap_rule_groups,
ensure_rule_group_schema,
sync_doc_type_bindings_from_group,
)
from fastapi_modules.fastapi_leaudit.services.impl.ruleServiceImpl import RuleServiceImpl
class EvaluationPointGroupServiceImpl(IEvaluationPointGroupService):
"""评查点分组服务实现。"""
def __init__(self) -> None:
self.RuleService = RuleServiceImpl()
async def ListGroups(
self,
Name: str | None,
Code: str | None,
IsEnabled: bool | None,
Pid: int | None,
Page: int,
PageSize: int,
) -> EvaluationPointGroupListVO:
async with GetAsyncSession() as session:
await self._ensure_ready(session)
offset = max(Page - 1, 0) * PageSize
filters = ["g.deleted_at IS NULL", "(COALESCE(g.pid, 0) <> 0 OR g.document_type_id IS NOT NULL OR g.entry_module_id IS NOT NULL)"]
params: dict[str, Any] = {"limit": PageSize, "offset": offset}
if Name:
filters.append("g.name ILIKE :name")
params["name"] = f"%{Name.strip()}%"
if Code:
filters.append("g.code ILIKE :code")
params["code"] = f"%{Code.strip()}%"
if IsEnabled is not None:
filters.append("g.is_enabled = :is_enabled")
params["is_enabled"] = IsEnabled
if Pid is not None:
filters.append("COALESCE(g.pid, 0) = :pid")
params["pid"] = self._normalize_pid(Pid)
where_clause = " AND ".join(filters)
total = int(
(
await session.execute(
text(f"SELECT COUNT(*) FROM leaudit_evaluation_point_groups g WHERE {where_clause}"),
params,
)
).scalar_one()
)
rows = (
await session.execute(
text(
f"""
SELECT
g.id,
g.pid,
g.name,
g.code,
g.description,
g.document_type_id,
dt.name AS document_type_name,
COALESCE(g.entry_module_id, parent.entry_module_id, dt.entry_module_id) AS entry_module_id,
em.name AS entry_module_name,
g.sort_order,
g.is_enabled,
g.created_at,
g.updated_at,
COALESCE(bg.binding_count, 0) AS rule_count
FROM leaudit_evaluation_point_groups g
LEFT JOIN leaudit_document_types dt ON dt.id = g.document_type_id
LEFT JOIN leaudit_evaluation_point_groups parent ON parent.id = g.pid AND parent.deleted_at IS NULL
LEFT JOIN leaudit_entry_modules em ON em.id = COALESCE(g.entry_module_id, parent.entry_module_id, dt.entry_module_id)
LEFT JOIN (
SELECT group_id, COUNT(*)::int AS binding_count
FROM leaudit_rule_group_bindings
WHERE deleted_at IS NULL
GROUP BY group_id
) bg ON bg.group_id = g.id
WHERE {where_clause}
ORDER BY COALESCE(g.sort_order, 0) ASC, g.id ASC
LIMIT :limit OFFSET :offset
"""
),
params,
)
).mappings().all()
binding_map = await self._load_binding_map(session, [int(row["id"]) for row in rows])
return EvaluationPointGroupListVO(
data=[self._to_group_vo(row, binding_map.get(int(row["id"]), []), include_rule_count=True) for row in rows],
total=total,
page=Page,
page_size=PageSize,
)
async def ListAllGroups(self, IncludeDisabled: bool, WithRuleCount: bool) -> list[EvaluationPointGroupVO]:
async with GetAsyncSession() as session:
await self._ensure_ready(session)
filters = ["g.deleted_at IS NULL", "(COALESCE(g.pid, 0) <> 0 OR g.document_type_id IS NOT NULL OR g.entry_module_id IS NOT NULL)"]
if not IncludeDisabled:
filters.append("g.is_enabled = TRUE")
rows = (
await session.execute(
text(
f"""
SELECT
g.id,
g.pid,
g.name,
g.code,
g.description,
g.document_type_id,
dt.name AS document_type_name,
COALESCE(g.entry_module_id, parent.entry_module_id, dt.entry_module_id) AS entry_module_id,
em.name AS entry_module_name,
g.sort_order,
g.is_enabled,
g.created_at,
g.updated_at,
COALESCE(bg.binding_count, 0) AS rule_count
FROM leaudit_evaluation_point_groups g
LEFT JOIN leaudit_document_types dt ON dt.id = g.document_type_id
LEFT JOIN leaudit_evaluation_point_groups parent ON parent.id = g.pid AND parent.deleted_at IS NULL
LEFT JOIN leaudit_entry_modules em ON em.id = COALESCE(g.entry_module_id, parent.entry_module_id, dt.entry_module_id)
LEFT JOIN (
SELECT group_id, COUNT(*)::int AS binding_count
FROM leaudit_rule_group_bindings
WHERE deleted_at IS NULL
GROUP BY group_id
) bg ON bg.group_id = g.id
WHERE {' AND '.join(filters)}
ORDER BY COALESCE(g.sort_order, 0) ASC, g.id ASC
"""
)
)
).mappings().all()
binding_map = await self._load_binding_map(session, [int(row["id"]) for row in rows])
groups = [self._to_group_vo(row, binding_map.get(int(row["id"]), []), include_rule_count=WithRuleCount) for row in rows]
by_parent: dict[int, list[EvaluationPointGroupVO]] = {}
roots: list[EvaluationPointGroupVO] = []
for group in groups:
parent_id = self._normalize_pid(group.pid)
if parent_id == 0:
roots.append(group)
else:
by_parent.setdefault(parent_id, []).append(group)
for group in groups:
children = by_parent.get(group.id)
if children:
group.children = children
return roots
async def ListGroupsByDocumentTypes(
self,
DocumentTypeIds: list[int],
IncludeDisabled: bool,
WithRuleCount: bool,
) -> list[EvaluationPointGroupVO]:
normalized_ids = sorted({int(item) for item in DocumentTypeIds if item})
if not normalized_ids:
return []
roots = await self.ListAllGroups(IncludeDisabled=IncludeDisabled, WithRuleCount=WithRuleCount)
result: list[EvaluationPointGroupVO] = []
for root in roots:
if root.document_type_id in normalized_ids:
result.append(root)
continue
children = root.children or []
if any(child.document_type_id in normalized_ids for child in children):
result.append(root)
return result
async def GetGroup(self, GroupId: int, WithRuleCount: bool) -> EvaluationPointGroupVO:
async with GetAsyncSession() as session:
await self._ensure_ready(session)
row = await self._get_group_row(session, GroupId)
binding_map = await self._load_binding_map(session, [GroupId])
return self._to_group_vo(row, binding_map.get(GroupId, []), include_rule_count=WithRuleCount)
async def GetChildren(self, GroupId: int, IsEnabled: bool | None, Page: int, PageSize: int) -> EvaluationPointGroupListVO:
await self.GetGroup(GroupId, WithRuleCount=False)
return await self.ListGroups(Name=None, Code=None, IsEnabled=IsEnabled, Pid=GroupId, Page=Page, PageSize=PageSize)
async def CreateGroup(self, Body: EvaluationPointGroupCreateDTO) -> EvaluationPointGroupVO:
payload = self._normalize_create_payload(Body)
async with GetAsyncSession() as session:
await self._ensure_ready(session)
await self._ensure_code_unique(session, payload["code"], None)
parent = await self._ensure_parent_valid(session, payload["pid"])
payload["entry_module_id"] = await self._ensure_entry_module_valid(
session, payload["pid"], payload["entry_module_id"], parent
)
payload["document_type_id"] = await self._ensure_document_type_valid(
session, payload["pid"], payload["document_type_id"], payload["entry_module_id"], None, parent
)
row = (
await session.execute(
text(
"""
INSERT INTO leaudit_evaluation_point_groups (
pid, code, name, description, document_type_id, entry_module_id, sort_order, is_enabled, created_at, updated_at
) VALUES (
:pid, :code, :name, :description, :document_type_id, :entry_module_id, :sort_order, :is_enabled, NOW(), NOW()
)
RETURNING id
"""
),
payload,
)
).mappings().one()
await session.commit()
group_id = int(row["id"])
return await self.GetGroup(group_id, WithRuleCount=True)
async def UpdateGroup(self, GroupId: int, Body: EvaluationPointGroupUpdateDTO) -> EvaluationPointGroupVO:
async with GetAsyncSession() as session:
await self._ensure_ready(session)
current = await self._get_group_row(session, GroupId)
provided_fields = set(getattr(Body, "model_fields_set", set()))
next_pid = self._normalize_pid(Body.pid) if Body.pid is not None else self._normalize_pid(current["pid"])
name = (Body.name.strip() if Body.name is not None else str(current.get("name") or "")).strip()
code = (Body.code.strip() if Body.code is not None else str(current.get("code") or "")).strip()
description = Body.description.strip() if Body.description is not None and Body.description else current.get("description")
document_type_id = Body.document_type_id if "document_type_id" in provided_fields else current.get("document_type_id")
entry_module_id = Body.entry_module_id if "entry_module_id" in provided_fields else current.get("entry_module_id")
sort_order = Body.sort_order if Body.sort_order is not None else int(current.get("sort_order") or 0)
is_enabled = Body.is_enabled if Body.is_enabled is not None else bool(current.get("is_enabled", True))
if not name:
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "分组名称不能为空")
if not code:
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "分组编码不能为空")
await self._ensure_code_unique(session, code, GroupId)
parent = await self._ensure_parent_valid(session, next_pid)
entry_module_id = await self._ensure_entry_module_valid(session, next_pid, entry_module_id, parent)
document_type_id = await self._ensure_document_type_valid(
session, next_pid, document_type_id, entry_module_id, GroupId, parent
)
await session.execute(
text(
"""
UPDATE leaudit_evaluation_point_groups
SET pid = :pid,
code = :code,
name = :name,
description = :description,
document_type_id = :document_type_id,
entry_module_id = :entry_module_id,
sort_order = :sort_order,
is_enabled = :is_enabled,
updated_at = NOW()
WHERE id = :group_id
"""
),
{
"group_id": GroupId,
"pid": next_pid,
"code": code,
"name": name,
"description": description,
"document_type_id": document_type_id,
"entry_module_id": entry_module_id,
"sort_order": sort_order,
"is_enabled": is_enabled,
},
)
await sync_doc_type_bindings_from_group(session, GroupId)
await session.commit()
return await self.GetGroup(GroupId, WithRuleCount=True)
async def DeleteGroup(self, GroupId: int) -> EvaluationPointGroupDeleteVO:
async with GetAsyncSession() as session:
await self._ensure_ready(session)
current = await self._get_group_row(session, GroupId)
is_root = self._normalize_pid(current["pid"]) == 0
if is_root:
child_count = int(
(
await session.execute(
text(
"SELECT COUNT(*) FROM leaudit_evaluation_point_groups WHERE pid = :group_id AND deleted_at IS NULL"
),
{"group_id": GroupId},
)
).scalar_one()
)
if child_count > 0:
return EvaluationPointGroupDeleteVO(
success=False,
message="当前一级分组下仍存在二级分组,请先迁移或删除二级分组",
deleted_groups=0,
deleted_points=0,
)
binding_count = int(
(
await session.execute(
text(
"SELECT COUNT(*) FROM leaudit_rule_group_bindings WHERE group_id = :group_id AND deleted_at IS NULL"
),
{"group_id": GroupId},
)
).scalar_one()
)
await session.execute(
text(
"UPDATE leaudit_rule_group_bindings SET deleted_at = NOW(), updated_at = NOW() WHERE group_id = :group_id AND deleted_at IS NULL"
),
{"group_id": GroupId},
)
await sync_doc_type_bindings_from_group(session, GroupId)
result = await session.execute(
text(
"UPDATE leaudit_evaluation_point_groups SET deleted_at = NOW(), updated_at = NOW() WHERE id = :group_id AND deleted_at IS NULL"
),
{"group_id": GroupId},
)
await session.commit()
deleted_groups = int(result.rowcount or 0)
return EvaluationPointGroupDeleteVO(
success=True,
message="规则分组删除成功",
deleted_count=deleted_groups,
deleted_groups=deleted_groups,
deleted_points=binding_count,
)
async def RebindGroup(self, GroupId: int, Body: EvaluationPointGroupRebindDTO) -> EvaluationPointGroupRebindVO:
async with GetAsyncSession() as session:
await self._ensure_ready(session)
current = await self._get_group_row(session, GroupId)
target = await self._get_group_row(session, Body.new_parent_id)
if self._normalize_pid(current["pid"]) != 0 or self._normalize_pid(target["pid"]) != 0:
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "仅支持一级分组之间迁移二级分组")
if GroupId == Body.new_parent_id:
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "迁移目标不能与原分组相同")
result = await session.execute(
text(
"""
UPDATE leaudit_evaluation_point_groups
SET pid = :new_parent_id,
updated_at = NOW()
WHERE pid = :old_group_id AND deleted_at IS NULL
"""
),
{"old_group_id": GroupId, "new_parent_id": Body.new_parent_id},
)
await session.commit()
moved = int(result.rowcount or 0)
return EvaluationPointGroupRebindVO(success=True, message="二级分组迁移成功", rebind_count=moved, doc_types_updated=moved)
async def BatchUpdateStatus(self, Body: EvaluationPointGroupBatchStatusDTO) -> EvaluationPointGroupBatchStatusVO:
ids = sorted({int(item) for item in Body.ids if item})
if not ids:
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "请选择至少一个分组")
async with GetAsyncSession() as session:
await self._ensure_ready(session)
result = await session.execute(
text(
"""
UPDATE leaudit_evaluation_point_groups
SET is_enabled = :is_enabled,
updated_at = NOW()
WHERE id IN :ids AND deleted_at IS NULL
"""
).bindparams(bindparam("ids", expanding=True)),
{"ids": ids, "is_enabled": Body.is_enabled},
)
await session.commit()
updated_count = int(result.rowcount or 0)
return EvaluationPointGroupBatchStatusVO(success=True, updated_count=updated_count, message=f"成功更新 {updated_count} 个分组状态")
async def BatchDelete(self, Body: EvaluationPointGroupBatchDeleteDTO) -> EvaluationPointGroupBatchDeleteVO:
ids = sorted({int(item) for item in Body.ids if item})
if not ids:
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "请选择至少一个分组")
async with GetAsyncSession() as session:
await self._ensure_ready(session)
rows = (
await session.execute(
text(
"SELECT id, pid, document_type_id FROM leaudit_evaluation_point_groups WHERE id IN :ids AND deleted_at IS NULL"
).bindparams(bindparam("ids", expanding=True)),
{"ids": ids},
)
).mappings().all()
if len(rows) != len(ids):
raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "部分分组不存在")
if any(self._normalize_pid(row["pid"]) == 0 for row in rows):
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "批量删除仅支持二级分组")
deleted_bindings = 0
for row in rows:
deleted_bindings += int(
(
await session.execute(
text(
"UPDATE leaudit_rule_group_bindings SET deleted_at = NOW(), updated_at = NOW() WHERE group_id = :group_id AND deleted_at IS NULL"
),
{"group_id": int(row["id"])},
)
).rowcount
or 0
)
await sync_doc_type_bindings_from_group(session, int(row["id"]))
result = await session.execute(
text(
"UPDATE leaudit_evaluation_point_groups SET deleted_at = NOW(), updated_at = NOW() WHERE id IN :ids AND deleted_at IS NULL"
).bindparams(bindparam("ids", expanding=True)),
{"ids": ids},
)
await session.commit()
deleted_groups = int(result.rowcount or 0)
return EvaluationPointGroupBatchDeleteVO(success=True, deleted_groups=deleted_groups, deleted_points=deleted_bindings, message=f"成功删除 {deleted_groups} 个分组")
async def CreateBinding(self, GroupId: int, Body: EvaluationPointGroupBindingCreateDTO) -> RuleGroupBindingVO:
async with GetAsyncSession() as session:
await self._ensure_ready(session)
group = await self._get_group_row(session, GroupId)
if self._normalize_pid(group["pid"]) == 0:
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "一级分组不能直接绑定规则集,请先选择二级分组")
await self._ensure_rule_set_valid(session, Body.rule_set_id)
existing = (
await session.execute(
text(
"SELECT id FROM leaudit_rule_group_bindings WHERE group_id = :group_id AND rule_set_id = :rule_set_id AND deleted_at IS NULL LIMIT 1"
),
{"group_id": GroupId, "rule_set_id": Body.rule_set_id},
)
).mappings().first()
if existing:
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "该规则集已绑定到当前二级分组")
row = (
await session.execute(
text(
"""
INSERT INTO leaudit_rule_group_bindings (
group_id, rule_set_id, priority, is_active, note, created_at, updated_at
) VALUES (
:group_id, :rule_set_id, :priority, :is_active, :note, NOW(), NOW()
)
RETURNING id
"""
),
{
"group_id": GroupId,
"rule_set_id": Body.rule_set_id,
"priority": Body.priority,
"is_active": Body.is_active,
"note": Body.note.strip() if Body.note else None,
},
)
).mappings().one()
await sync_doc_type_bindings_from_group(session, GroupId)
await session.commit()
binding_id = int(row["id"])
binding_row = await self._get_binding_row(session, binding_id)
binding_vo = await self._build_binding_vo(binding_row)
return binding_vo
async def UpdateBinding(self, BindingId: int, Body: EvaluationPointGroupBindingUpdateDTO) -> RuleGroupBindingVO:
async with GetAsyncSession() as session:
await self._ensure_ready(session)
current = await self._get_binding_row(session, BindingId)
await session.execute(
text(
"""
UPDATE leaudit_rule_group_bindings
SET priority = :priority,
is_active = :is_active,
note = :note,
updated_at = NOW()
WHERE id = :binding_id
"""
),
{
"binding_id": BindingId,
"priority": Body.priority if Body.priority is not None else int(current.get("priority") or 0),
"is_active": Body.is_active if Body.is_active is not None else bool(current.get("is_active", True)),
"note": Body.note.strip() if Body.note else (current.get("note") if Body.note is None else None),
},
)
await sync_doc_type_bindings_from_group(session, int(current["group_id"]))
await session.commit()
binding_row = await self._get_binding_row(session, BindingId)
binding_vo = await self._build_binding_vo(binding_row)
return binding_vo
async def DeleteBinding(self, BindingId: int) -> None:
async with GetAsyncSession() as session:
await self._ensure_ready(session)
current = await self._get_binding_row(session, BindingId)
await session.execute(
text(
"UPDATE leaudit_rule_group_bindings SET deleted_at = NOW(), updated_at = NOW() WHERE id = :binding_id"
),
{"binding_id": BindingId},
)
await sync_doc_type_bindings_from_group(session, int(current["group_id"]))
await session.commit()
async def _ensure_ready(self, session) -> None:
self._rule_set_meta_cache = None
await ensure_rule_group_schema(session)
await bootstrap_rule_groups(session)
async def _get_group_row(self, session, group_id: int):
row = (
await session.execute(
text(
"""
SELECT
g.id,
g.pid,
g.name,
g.code,
g.description,
g.document_type_id,
dt.name AS document_type_name,
COALESCE(g.entry_module_id, parent.entry_module_id, dt.entry_module_id) AS entry_module_id,
em.name AS entry_module_name,
g.sort_order,
g.is_enabled,
g.created_at,
g.updated_at,
COALESCE(bg.binding_count, 0) AS rule_count
FROM leaudit_evaluation_point_groups g
LEFT JOIN leaudit_document_types dt ON dt.id = g.document_type_id
LEFT JOIN leaudit_evaluation_point_groups parent ON parent.id = g.pid AND parent.deleted_at IS NULL
LEFT JOIN leaudit_entry_modules em ON em.id = COALESCE(g.entry_module_id, parent.entry_module_id, dt.entry_module_id)
LEFT JOIN (
SELECT group_id, COUNT(*)::int AS binding_count
FROM leaudit_rule_group_bindings
WHERE deleted_at IS NULL
GROUP BY group_id
) bg ON bg.group_id = g.id
WHERE g.id = :group_id AND g.deleted_at IS NULL
LIMIT 1
"""
),
{"group_id": group_id},
)
).mappings().first()
if not row:
raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "规则分组不存在")
return row
async def _get_binding_row(self, session, binding_id: int):
row = (
await session.execute(
text(
"""
SELECT id, group_id, rule_set_id, rule_type_binding_id, priority, is_active, note, created_at, updated_at
FROM leaudit_rule_group_bindings
WHERE id = :binding_id AND deleted_at IS NULL
LIMIT 1
"""
),
{"binding_id": binding_id},
)
).mappings().first()
if not row:
raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "规则组绑定不存在")
return row
async def _load_binding_map(self, session, group_ids: list[int]) -> dict[int, list[RuleGroupBindingVO]]:
group_ids = [int(item) for item in group_ids if item]
if not group_ids:
return {}
rows = (
await session.execute(
text(
"""
SELECT id, group_id, rule_set_id, rule_type_binding_id, priority, is_active, note, created_at, updated_at
FROM leaudit_rule_group_bindings
WHERE group_id IN :group_ids AND deleted_at IS NULL
ORDER BY priority DESC, id ASC
"""
).bindparams(bindparam("group_ids", expanding=True)),
{"group_ids": group_ids},
)
).mappings().all()
result: dict[int, list[RuleGroupBindingVO]] = {}
for row in rows:
result.setdefault(int(row["group_id"]), []).append(await self._build_binding_vo(row))
return result
async def _build_binding_vo(self, row) -> RuleGroupBindingVO:
rule_set_meta = await self._get_rule_set_meta(int(row["rule_set_id"]))
return RuleGroupBindingVO(
id=int(row["id"]),
group_id=int(row["group_id"]),
rule_set_id=int(row["rule_set_id"]),
rule_type_binding_id=int(row["rule_type_binding_id"]) if row.get("rule_type_binding_id") else None,
priority=int(row.get("priority") or 0),
is_active=bool(row.get("is_active", True)),
note=row.get("note"),
rule_type=rule_set_meta.get("rule_type"),
rule_name=rule_set_meta.get("rule_name"),
current_version_id=rule_set_meta.get("current_version_id"),
fallback_version_id=rule_set_meta.get("fallback_version_id"),
has_usable_version=bool(rule_set_meta.get("has_usable_version", False)),
usable_rule_count=int(rule_set_meta.get("usable_rule_count") or 0),
)
async def _get_rule_set_meta(self, rule_set_id: int) -> dict[str, Any]:
if not getattr(self, "_rule_set_meta_cache", None):
rule_sets = await self.RuleService.ListSets()
self._rule_set_meta_cache = {
int(item.id): {
"rule_type": item.ruleType,
"rule_name": item.ruleName,
"current_version_id": item.currentVersionId,
"fallback_version_id": item.fallbackVersionId,
"has_usable_version": item.hasUsableVersion,
"usable_rule_count": item.usableRuleCount,
}
for item in rule_sets
}
return self._rule_set_meta_cache.get(rule_set_id, {})
async def _ensure_parent_valid(self, session, pid: int):
if pid == 0:
return None
parent = await self._get_group_row(session, pid)
if self._normalize_pid(parent["pid"]) != 0:
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "上级分组必须是一级分组")
return parent
async def _ensure_entry_module_valid(
self,
session,
pid: int,
entry_module_id: int | None,
parent: Any | None,
) -> int | None:
if pid != 0:
if parent is not None and parent.get("entry_module_id") is not None:
return int(parent["entry_module_id"])
return None if entry_module_id in (None, 0, "0", "") else int(entry_module_id)
if entry_module_id in (None, 0, "0", ""):
return None
exists = (
await session.execute(
text("SELECT id FROM leaudit_entry_modules WHERE id = :entry_module_id AND deleted_at IS NULL LIMIT 1"),
{"entry_module_id": int(entry_module_id)},
)
).scalar_one_or_none()
if exists is None:
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "关联入口模块不存在")
return int(entry_module_id)
async def _ensure_document_type_valid(
self,
session,
pid: int,
document_type_id: int | None,
entry_module_id: int | None,
group_id: int | None,
parent: Any | None,
) -> int | None:
if pid == 0:
if document_type_id is None and entry_module_id is None:
return None
else:
if parent is None:
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "二级分组必须挂到一级分组下")
if parent.get("document_type_id") is not None:
parent_doc_type_id = int(parent["document_type_id"])
if document_type_id is None:
document_type_id = parent_doc_type_id
elif int(document_type_id) != parent_doc_type_id:
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "二级分组的文档类型必须与所属一级分组一致")
elif document_type_id is None:
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "二级分组必须明确绑定具体文档类型")
if document_type_id is None:
return None
exists = (
await session.execute(
text(
"SELECT id FROM leaudit_document_types WHERE id = :doc_type_id AND deleted_at IS NULL LIMIT 1"
),
{"doc_type_id": int(document_type_id)},
)
).scalar_one_or_none()
if exists is None:
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "关联文档类型不存在")
if pid == 0:
duplicated_root = (
await session.execute(
text(
"""
SELECT id
FROM leaudit_evaluation_point_groups
WHERE deleted_at IS NULL
AND COALESCE(pid, 0) = 0
AND document_type_id = :doc_type_id
AND (:group_id IS NULL OR id <> :group_id)
LIMIT 1
"""
),
{"doc_type_id": int(document_type_id), "group_id": group_id},
)
).scalar_one_or_none()
if duplicated_root is not None:
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "该文档类型已存在一级分组")
return int(document_type_id)
async def _ensure_code_unique(self, session, code: str, group_id: int | None) -> None:
sql = "SELECT id FROM leaudit_evaluation_point_groups WHERE LOWER(code) = LOWER(:code) AND deleted_at IS NULL"
params: dict[str, Any] = {"code": code}
if group_id is not None:
sql += " AND id <> :group_id"
params["group_id"] = group_id
duplicated = (await session.execute(text(sql + " LIMIT 1"), params)).scalar_one_or_none()
if duplicated is not None:
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "分组编码已存在")
async def _ensure_rule_set_valid(self, session, rule_set_id: int) -> None:
exists = (
await session.execute(
text("SELECT id FROM leaudit_rule_sets WHERE id = :rule_set_id AND deleted_at IS NULL LIMIT 1"),
{"rule_set_id": rule_set_id},
)
).scalar_one_or_none()
if exists is None:
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "规则集不存在")
def _normalize_create_payload(self, body: EvaluationPointGroupCreateDTO) -> dict[str, Any]:
name = body.name.strip()
code = body.code.strip()
if not name:
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "分组名称不能为空")
if not code:
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "分组编码不能为空")
return {
"name": name,
"code": code,
"pid": self._normalize_pid(body.pid),
"description": body.description.strip() if body.description else None,
"document_type_id": body.document_type_id,
"entry_module_id": body.entry_module_id,
"sort_order": int(body.sort_order or 0),
"is_enabled": body.is_enabled,
}
def _to_group_vo(self, row, bindings: list[RuleGroupBindingVO], include_rule_count: bool) -> EvaluationPointGroupVO:
return EvaluationPointGroupVO(
id=int(row["id"]),
pid=self._normalize_pid(row.get("pid")),
name=str(row.get("name") or ""),
code=str(row.get("code") or ""),
description=row.get("description"),
document_type_id=int(row["document_type_id"]) if row.get("document_type_id") is not None else None,
document_type_name=row.get("document_type_name"),
entry_module_id=int(row["entry_module_id"]) if row.get("entry_module_id") is not None else None,
entry_module_name=row.get("entry_module_name"),
sort_order=int(row.get("sort_order") or 0),
is_enabled=bool(row.get("is_enabled", True)),
created_at=self._to_iso(row.get("created_at")),
updated_at=self._to_iso(row.get("updated_at")),
rule_count=len(bindings) if include_rule_count else None,
bindings=bindings,
children=None,
)
def _normalize_pid(self, value: Any) -> int:
if value in (None, "", "0", 0):
return 0
return int(value)
def _to_iso(self, value: Any) -> str | None:
if value is None:
return None
if isinstance(value, datetime):
return value.isoformat()
return str(value)