feat: add backend rule group and permission support
This commit is contained in:
@@ -58,6 +58,15 @@ class AuditServiceImpl(IAuditService):
|
||||
async with GetAsyncSession() as session:
|
||||
logger.info(f"触发评查: documentId={DocumentId}, ruleType={RuleType}")
|
||||
normalizedSpeed = _normalize_speed(Speed)
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
ALTER TABLE leaudit_documents
|
||||
ADD COLUMN IF NOT EXISTS group_id BIGINT NULL
|
||||
REFERENCES leaudit_evaluation_point_groups(id)
|
||||
"""
|
||||
)
|
||||
)
|
||||
document = await session.get(LeauditDocument, DocumentId)
|
||||
if not document:
|
||||
raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "评查文档不存在")
|
||||
@@ -99,6 +108,7 @@ class AuditServiceImpl(IAuditService):
|
||||
.where(
|
||||
LeauditDocumentFile.documentId == DocumentId,
|
||||
LeauditDocumentFile.isActive.is_(True),
|
||||
LeauditDocumentFile.fileRole == "primary",
|
||||
)
|
||||
.order_by(LeauditDocumentFile.Id.desc())
|
||||
.limit(1)
|
||||
@@ -115,30 +125,91 @@ class AuditServiceImpl(IAuditService):
|
||||
)
|
||||
latestRunNo = runNoResult.scalar_one_or_none() or 0
|
||||
|
||||
bindingResult = await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT
|
||||
rs.id AS rule_set_id,
|
||||
rs.current_version_id AS rule_version_id,
|
||||
rv.oss_url AS rule_source_oss_url,
|
||||
rv.file_sha256 AS rule_source_sha256,
|
||||
rv.metadata_type_id AS rule_type_id
|
||||
FROM leaudit_rule_type_bindings b
|
||||
JOIN leaudit_rule_sets rs ON rs.id = b.rule_set_id
|
||||
LEFT JOIN leaudit_rule_versions rv ON rv.id = rs.current_version_id
|
||||
WHERE b.doc_type_id = :doc_type_id
|
||||
AND b.is_active = true
|
||||
AND b.region = :region
|
||||
ORDER BY b.priority DESC, b.id DESC
|
||||
LIMIT 1
|
||||
"""
|
||||
),
|
||||
{"doc_type_id": document.typeId, "region": document.region},
|
||||
)
|
||||
binding = bindingResult.mappings().first()
|
||||
if not binding or not binding["rule_set_id"] or not binding["rule_version_id"]:
|
||||
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "当前文档类型未绑定可用规则版本")
|
||||
binding = None
|
||||
if getattr(document, "groupId", None):
|
||||
groupBindingResult = await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT
|
||||
rs.id AS rule_set_id,
|
||||
COALESCE(rs.current_version_id, fallback_rv.id) AS rule_version_id,
|
||||
COALESCE(current_rv.oss_url, fallback_rv.oss_url) AS rule_source_oss_url,
|
||||
COALESCE(current_rv.file_sha256, fallback_rv.file_sha256) AS rule_source_sha256,
|
||||
COALESCE(current_rv.metadata_type_id, fallback_rv.metadata_type_id) AS rule_type_id
|
||||
FROM leaudit_rule_group_bindings rgb
|
||||
JOIN leaudit_rule_sets rs ON rs.id = rgb.rule_set_id
|
||||
LEFT JOIN leaudit_rule_versions current_rv ON current_rv.id = rs.current_version_id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
rv.id,
|
||||
rv.oss_url,
|
||||
rv.file_sha256,
|
||||
rv.metadata_type_id
|
||||
FROM leaudit_rule_versions rv
|
||||
WHERE rv.rule_set_id = rs.id
|
||||
AND rv.status IN ('published', 'rollback')
|
||||
ORDER BY rv.version_seq DESC, rv.id DESC
|
||||
LIMIT 1
|
||||
) fallback_rv ON TRUE
|
||||
WHERE rgb.group_id = :group_id
|
||||
AND rgb.is_active = TRUE
|
||||
AND rgb.deleted_at IS NULL
|
||||
ORDER BY rgb.priority DESC, rgb.id ASC
|
||||
LIMIT 1
|
||||
"""
|
||||
),
|
||||
{"group_id": int(document.groupId)},
|
||||
)
|
||||
binding = groupBindingResult.mappings().first()
|
||||
if not binding or not binding["rule_set_id"] or not binding["rule_version_id"]:
|
||||
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "当前子类型未绑定可执行规则集,请先检查二级分组规则配置")
|
||||
|
||||
if binding is None:
|
||||
bindingResult = await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT
|
||||
rs.id AS rule_set_id,
|
||||
COALESCE(rs.current_version_id, fallback_rv.id) AS rule_version_id,
|
||||
COALESCE(current_rv.oss_url, fallback_rv.oss_url) AS rule_source_oss_url,
|
||||
COALESCE(current_rv.file_sha256, fallback_rv.file_sha256) AS rule_source_sha256,
|
||||
COALESCE(current_rv.metadata_type_id, fallback_rv.metadata_type_id) AS rule_type_id
|
||||
FROM leaudit_rule_type_bindings b
|
||||
JOIN leaudit_rule_sets rs ON rs.id = b.rule_set_id
|
||||
LEFT JOIN leaudit_rule_versions current_rv ON current_rv.id = rs.current_version_id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
rv.id,
|
||||
rv.oss_url,
|
||||
rv.file_sha256,
|
||||
rv.metadata_type_id
|
||||
FROM leaudit_rule_versions rv
|
||||
WHERE rv.rule_set_id = rs.id
|
||||
AND rv.status IN ('published', 'rollback')
|
||||
ORDER BY rv.version_seq DESC, rv.id DESC
|
||||
LIMIT 1
|
||||
) fallback_rv ON TRUE
|
||||
WHERE b.doc_type_id = :doc_type_id
|
||||
AND b.is_active = true
|
||||
AND b.deleted_at IS NULL
|
||||
AND (
|
||||
b.region = :region
|
||||
OR b.region = 'default'
|
||||
OR b.region IS NULL
|
||||
OR b.region = ''
|
||||
)
|
||||
ORDER BY
|
||||
CASE WHEN b.region = :region THEN 0 ELSE 1 END,
|
||||
b.priority DESC,
|
||||
b.id DESC
|
||||
LIMIT 1
|
||||
"""
|
||||
),
|
||||
{"doc_type_id": document.typeId, "region": document.region},
|
||||
)
|
||||
binding = bindingResult.mappings().first()
|
||||
if not binding or not binding["rule_set_id"] or not binding["rule_version_id"]:
|
||||
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "当前文档类型未绑定可用规则版本")
|
||||
|
||||
triggerSource = f"{'retry' if Force else 'upload'}:{normalizedSpeed}"
|
||||
|
||||
|
||||
@@ -0,0 +1,814 @@
|
||||
"""评查点分组服务实现(新链路:文档类型 -> 子类型 -> 规则集)。"""
|
||||
|
||||
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)
|
||||
@@ -0,0 +1,552 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
||||
|
||||
from fastapi_admin.config import DB_HOST, DB_PASSWORD, DB_PORT, DB_USER
|
||||
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.evaluationPointDto import (
|
||||
EvaluationPointCreateDTO,
|
||||
EvaluationPointUpdateDTO,
|
||||
)
|
||||
from fastapi_modules.fastapi_leaudit.domian.vo.evaluationPointVo import (
|
||||
AttributeTypeListVO,
|
||||
AttributeTypeVO,
|
||||
EvaluationPointDeleteVO,
|
||||
EvaluationPointListVO,
|
||||
EvaluationPointVO,
|
||||
)
|
||||
from fastapi_modules.fastapi_leaudit.services.evaluationPointService import IEvaluationPointService
|
||||
|
||||
_LEGACY_DB_NAME = os.getenv("LEGACY_RULE_DB_NAME", "docauditai")
|
||||
_LEGACY_DB_URL = (
|
||||
f"postgresql+asyncpg://{quote_plus(str(DB_USER))}:{quote_plus(str(DB_PASSWORD))}"
|
||||
f"@{DB_HOST}:{DB_PORT}/{quote_plus(_LEGACY_DB_NAME)}"
|
||||
)
|
||||
_LEGACY_ENGINE = create_async_engine(_LEGACY_DB_URL, pool_pre_ping=True)
|
||||
_LegacySession = async_sessionmaker(_LEGACY_ENGINE, expire_on_commit=False)
|
||||
|
||||
|
||||
class EvaluationPointServiceImpl(IEvaluationPointService):
|
||||
"""评查点服务实现。"""
|
||||
|
||||
async def ListPoints(
|
||||
self,
|
||||
Name: str | None,
|
||||
Code: str | None,
|
||||
Risk: str | None,
|
||||
IsEnabled: bool | None,
|
||||
GroupPid: int | None,
|
||||
GroupId: int | None,
|
||||
DocumentAttributeType: str | None,
|
||||
Area: str | None,
|
||||
Page: int,
|
||||
PageSize: int,
|
||||
) -> EvaluationPointListVO:
|
||||
offset = max(Page - 1, 0) * PageSize
|
||||
where_clause, params = self._build_list_filters(
|
||||
Name=Name,
|
||||
Code=Code,
|
||||
Risk=Risk,
|
||||
IsEnabled=IsEnabled,
|
||||
GroupPid=GroupPid,
|
||||
GroupId=GroupId,
|
||||
DocumentAttributeType=DocumentAttributeType,
|
||||
Area=Area,
|
||||
)
|
||||
params.update({"limit": PageSize, "offset": offset})
|
||||
|
||||
async with _LegacySession() as session:
|
||||
total = int(
|
||||
(
|
||||
await session.execute(
|
||||
text(
|
||||
f"""
|
||||
SELECT COUNT(*)
|
||||
FROM evaluation_points ep
|
||||
LEFT JOIN evaluation_point_groups child_group ON child_group.id = ep.evaluation_point_groups_id
|
||||
LEFT JOIN evaluation_point_groups parent_group ON parent_group.id = COALESCE(ep.evaluation_point_groups_pid, child_group.pid)
|
||||
WHERE {where_clause}
|
||||
"""
|
||||
),
|
||||
params,
|
||||
)
|
||||
).scalar_one()
|
||||
)
|
||||
rows = (
|
||||
await session.execute(
|
||||
text(
|
||||
f"""
|
||||
SELECT
|
||||
ep.id,
|
||||
ep.code,
|
||||
ep.name,
|
||||
ep.evaluation_point_groups_id,
|
||||
ep.evaluation_point_groups_pid,
|
||||
ep.risk,
|
||||
ep.description,
|
||||
ep.is_enabled,
|
||||
ep.document_attribute_type,
|
||||
ep.references_laws,
|
||||
ep.extraction_config,
|
||||
ep.evaluation_config,
|
||||
ep.pass_message,
|
||||
ep.fail_message,
|
||||
ep.suggestion_message,
|
||||
ep.suggestion_message_type,
|
||||
ep.post_action,
|
||||
ep.action_config,
|
||||
ep.score,
|
||||
ep.area,
|
||||
ep.created_at,
|
||||
ep.updated_at,
|
||||
child_group.name AS group_name,
|
||||
parent_group.name AS rule_type
|
||||
FROM evaluation_points ep
|
||||
LEFT JOIN evaluation_point_groups child_group ON child_group.id = ep.evaluation_point_groups_id
|
||||
LEFT JOIN evaluation_point_groups parent_group ON parent_group.id = COALESCE(ep.evaluation_point_groups_pid, child_group.pid)
|
||||
WHERE {where_clause}
|
||||
ORDER BY COALESCE(ep.sort, 0) ASC, ep.updated_at DESC, ep.id DESC
|
||||
LIMIT :limit OFFSET :offset
|
||||
"""
|
||||
),
|
||||
params,
|
||||
)
|
||||
).mappings().all()
|
||||
|
||||
return EvaluationPointListVO(
|
||||
data=[self._to_point_vo(row) for row in rows],
|
||||
total=total,
|
||||
page=Page,
|
||||
page_size=PageSize,
|
||||
)
|
||||
|
||||
async def GetPoint(self, PointId: int) -> EvaluationPointVO:
|
||||
async with _LegacySession() as session:
|
||||
row = (
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT
|
||||
ep.id,
|
||||
ep.code,
|
||||
ep.name,
|
||||
ep.evaluation_point_groups_id,
|
||||
ep.evaluation_point_groups_pid,
|
||||
ep.risk,
|
||||
ep.description,
|
||||
ep.is_enabled,
|
||||
ep.document_attribute_type,
|
||||
ep.references_laws,
|
||||
ep.extraction_config,
|
||||
ep.evaluation_config,
|
||||
ep.pass_message,
|
||||
ep.fail_message,
|
||||
ep.suggestion_message,
|
||||
ep.suggestion_message_type,
|
||||
ep.post_action,
|
||||
ep.action_config,
|
||||
ep.score,
|
||||
ep.area,
|
||||
ep.created_at,
|
||||
ep.updated_at,
|
||||
child_group.name AS group_name,
|
||||
parent_group.name AS rule_type
|
||||
FROM evaluation_points ep
|
||||
LEFT JOIN evaluation_point_groups child_group ON child_group.id = ep.evaluation_point_groups_id
|
||||
LEFT JOIN evaluation_point_groups parent_group ON parent_group.id = COALESCE(ep.evaluation_point_groups_pid, child_group.pid)
|
||||
WHERE ep.id = :point_id
|
||||
"""
|
||||
),
|
||||
{"point_id": PointId},
|
||||
)
|
||||
).mappings().first()
|
||||
|
||||
if not row:
|
||||
raise LeauditException(StatusCodeEnum.NOT_FOUND, "评查点不存在")
|
||||
return self._to_point_vo(row)
|
||||
|
||||
async def CreatePoint(self, Body: EvaluationPointCreateDTO) -> EvaluationPointVO:
|
||||
await self._validate_group_relation(Body.evaluation_point_groups_pid, Body.evaluation_point_groups_id)
|
||||
await self._ensure_code_unique(str(Body.code).strip())
|
||||
|
||||
now = datetime.utcnow()
|
||||
insert_params = self._build_write_params(Body, now)
|
||||
insert_params["created_at"] = now
|
||||
insert_params["updated_at"] = now
|
||||
|
||||
async with _LegacySession() as session:
|
||||
async with session.begin():
|
||||
new_id = await session.scalar(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO evaluation_points (
|
||||
code,
|
||||
name,
|
||||
evaluation_point_groups_id,
|
||||
evaluation_point_groups_pid,
|
||||
risk,
|
||||
description,
|
||||
is_enabled,
|
||||
document_attribute_type,
|
||||
references_laws,
|
||||
extraction_config,
|
||||
evaluation_config,
|
||||
pass_message,
|
||||
fail_message,
|
||||
suggestion_message,
|
||||
suggestion_message_type,
|
||||
post_action,
|
||||
action_config,
|
||||
score,
|
||||
area,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
:code,
|
||||
:name,
|
||||
:evaluation_point_groups_id,
|
||||
:evaluation_point_groups_pid,
|
||||
:risk,
|
||||
:description,
|
||||
:is_enabled,
|
||||
:document_attribute_type,
|
||||
CAST(:references_laws AS jsonb),
|
||||
CAST(:extraction_config AS jsonb),
|
||||
CAST(:evaluation_config AS jsonb),
|
||||
:pass_message,
|
||||
:fail_message,
|
||||
:suggestion_message,
|
||||
:suggestion_message_type,
|
||||
:post_action,
|
||||
:action_config,
|
||||
:score,
|
||||
:area,
|
||||
:created_at,
|
||||
:updated_at
|
||||
)
|
||||
RETURNING id
|
||||
"""
|
||||
),
|
||||
insert_params,
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
return await self.GetPoint(int(new_id))
|
||||
|
||||
async def UpdatePoint(self, PointId: int, Body: EvaluationPointUpdateDTO) -> EvaluationPointVO:
|
||||
await self.GetPoint(PointId)
|
||||
|
||||
payload = Body.model_dump(exclude_unset=True)
|
||||
if not payload:
|
||||
return await self.GetPoint(PointId)
|
||||
|
||||
group_pid = payload.get("evaluation_point_groups_pid")
|
||||
group_id = payload.get("evaluation_point_groups_id")
|
||||
if group_pid is not None or group_id is not None:
|
||||
current = await self.GetPoint(PointId)
|
||||
await self._validate_group_relation(
|
||||
group_pid if group_pid is not None else current.evaluation_point_groups_pid,
|
||||
group_id if group_id is not None else current.evaluation_point_groups_id,
|
||||
)
|
||||
|
||||
if "code" in payload and payload["code"]:
|
||||
await self._ensure_code_unique(str(payload["code"]).strip(), PointId)
|
||||
|
||||
updates: list[str] = []
|
||||
params: dict[str, Any] = {"point_id": PointId, "updated_at": datetime.utcnow()}
|
||||
simple_fields = [
|
||||
"code",
|
||||
"name",
|
||||
"evaluation_point_groups_id",
|
||||
"evaluation_point_groups_pid",
|
||||
"risk",
|
||||
"description",
|
||||
"is_enabled",
|
||||
"document_attribute_type",
|
||||
"pass_message",
|
||||
"fail_message",
|
||||
"suggestion_message",
|
||||
"suggestion_message_type",
|
||||
"post_action",
|
||||
"action_config",
|
||||
"score",
|
||||
"area",
|
||||
]
|
||||
json_fields = ["references_laws", "extraction_config", "evaluation_config"]
|
||||
|
||||
for field in simple_fields:
|
||||
if field not in payload:
|
||||
continue
|
||||
params[field] = self._normalize_scalar_field(field, payload[field])
|
||||
updates.append(f"{field} = :{field}")
|
||||
|
||||
for field in json_fields:
|
||||
if field not in payload:
|
||||
continue
|
||||
params[field] = json.dumps(payload[field] if payload[field] is not None else self._default_json(field), ensure_ascii=False)
|
||||
updates.append(f"{field} = CAST(:{field} AS jsonb)")
|
||||
|
||||
updates.append("updated_at = :updated_at")
|
||||
|
||||
async with _LegacySession() as session:
|
||||
async with session.begin():
|
||||
await session.execute(
|
||||
text(f"UPDATE evaluation_points SET {', '.join(updates)} WHERE id = :point_id"),
|
||||
params,
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
return await self.GetPoint(PointId)
|
||||
|
||||
async def DeletePoint(self, PointId: int) -> EvaluationPointDeleteVO:
|
||||
await self.GetPoint(PointId)
|
||||
async with _LegacySession() as session:
|
||||
async with session.begin():
|
||||
await session.execute(text("DELETE FROM evaluation_points WHERE id = :point_id"), {"point_id": PointId})
|
||||
await session.commit()
|
||||
return EvaluationPointDeleteVO(success=True, message="评查点删除成功")
|
||||
|
||||
async def GetAttributeTypes(self) -> AttributeTypeListVO:
|
||||
async with _LegacySession() as session:
|
||||
rows = (
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT DISTINCT TRIM(document_attribute_type) AS code
|
||||
FROM evaluation_points
|
||||
WHERE document_attribute_type IS NOT NULL
|
||||
AND TRIM(document_attribute_type) <> ''
|
||||
ORDER BY TRIM(document_attribute_type) ASC
|
||||
"""
|
||||
)
|
||||
)
|
||||
).scalars().all()
|
||||
|
||||
types = [AttributeTypeVO(code=str(item), label=str(item)) for item in rows if item]
|
||||
if not any(item.code == "ALL" for item in types):
|
||||
types.insert(0, AttributeTypeVO(code="ALL", label="通用"))
|
||||
return AttributeTypeListVO(types=types)
|
||||
|
||||
def _build_list_filters(
|
||||
self,
|
||||
Name: str | None,
|
||||
Code: str | None,
|
||||
Risk: str | None,
|
||||
IsEnabled: bool | None,
|
||||
GroupPid: int | None,
|
||||
GroupId: int | None,
|
||||
DocumentAttributeType: str | None,
|
||||
Area: str | None,
|
||||
) -> tuple[str, dict[str, Any]]:
|
||||
filters = ["1=1"]
|
||||
params: dict[str, Any] = {}
|
||||
|
||||
name = (Name or "").strip()
|
||||
code = (Code or "").strip()
|
||||
if name and code:
|
||||
if name == code:
|
||||
filters.append("(ep.name ILIKE :keyword OR ep.code ILIKE :keyword)")
|
||||
params["keyword"] = f"%{name}%"
|
||||
else:
|
||||
filters.append("ep.name ILIKE :name")
|
||||
filters.append("ep.code ILIKE :code")
|
||||
params["name"] = f"%{name}%"
|
||||
params["code"] = f"%{code}%"
|
||||
elif name:
|
||||
filters.append("ep.name ILIKE :name")
|
||||
params["name"] = f"%{name}%"
|
||||
elif code:
|
||||
filters.append("ep.code ILIKE :code")
|
||||
params["code"] = f"%{code}%"
|
||||
|
||||
if Risk:
|
||||
filters.append("ep.risk = :risk")
|
||||
params["risk"] = Risk
|
||||
if IsEnabled is not None:
|
||||
filters.append("ep.is_enabled = :is_enabled")
|
||||
params["is_enabled"] = IsEnabled
|
||||
if GroupPid is not None:
|
||||
filters.append("ep.evaluation_point_groups_pid = :group_pid")
|
||||
params["group_pid"] = GroupPid
|
||||
if GroupId is not None:
|
||||
filters.append("ep.evaluation_point_groups_id = :group_id")
|
||||
params["group_id"] = GroupId
|
||||
if DocumentAttributeType:
|
||||
if DocumentAttributeType == "ALL":
|
||||
filters.append("(ep.document_attribute_type = 'ALL' OR ep.document_attribute_type = '通用' OR ep.document_attribute_type IS NULL OR TRIM(ep.document_attribute_type) = '')")
|
||||
else:
|
||||
filters.append("ep.document_attribute_type = :document_attribute_type")
|
||||
params["document_attribute_type"] = DocumentAttributeType
|
||||
if Area:
|
||||
filters.append("ep.area = :area")
|
||||
params["area"] = Area
|
||||
|
||||
return " AND ".join(filters), params
|
||||
|
||||
async def _validate_group_relation(self, GroupPid: int | None, GroupId: int | None) -> None:
|
||||
if not GroupPid:
|
||||
raise LeauditException(StatusCodeEnum.BAD_REQUEST, "评查点类型不能为空")
|
||||
if not GroupId:
|
||||
raise LeauditException(StatusCodeEnum.BAD_REQUEST, "所属规则组不能为空")
|
||||
|
||||
async with _LegacySession() as session:
|
||||
parent = (
|
||||
await session.execute(
|
||||
text("SELECT id, pid, is_enabled FROM evaluation_point_groups WHERE id = :group_pid"),
|
||||
{"group_pid": GroupPid},
|
||||
)
|
||||
).mappings().first()
|
||||
child = (
|
||||
await session.execute(
|
||||
text("SELECT id, pid, is_enabled FROM evaluation_point_groups WHERE id = :group_id"),
|
||||
{"group_id": GroupId},
|
||||
)
|
||||
).mappings().first()
|
||||
|
||||
if not parent:
|
||||
raise LeauditException(StatusCodeEnum.BAD_REQUEST, "评查点类型不存在")
|
||||
if not child:
|
||||
raise LeauditException(StatusCodeEnum.BAD_REQUEST, "所属规则组不存在")
|
||||
if int(child.get("pid") or 0) != int(GroupPid):
|
||||
raise LeauditException(StatusCodeEnum.BAD_REQUEST, "所属规则组与评查点类型不匹配")
|
||||
|
||||
async def _ensure_code_unique(self, Code: str, PointId: int | None = None) -> None:
|
||||
params: dict[str, Any] = {"code": Code}
|
||||
sql = "SELECT id FROM evaluation_points WHERE LOWER(code) = LOWER(:code)"
|
||||
if PointId is not None:
|
||||
sql += " AND id <> :point_id"
|
||||
params["point_id"] = PointId
|
||||
|
||||
async with _LegacySession() as session:
|
||||
exists = (
|
||||
await session.execute(text(sql), params)
|
||||
).scalar_one_or_none()
|
||||
|
||||
if exists:
|
||||
raise LeauditException(StatusCodeEnum.BAD_REQUEST, "评查点编码已存在")
|
||||
|
||||
def _build_write_params(self, Body: EvaluationPointCreateDTO, Now: datetime) -> dict[str, Any]:
|
||||
return {
|
||||
"code": str(Body.code).strip(),
|
||||
"name": str(Body.name).strip(),
|
||||
"evaluation_point_groups_id": Body.evaluation_point_groups_id,
|
||||
"evaluation_point_groups_pid": Body.evaluation_point_groups_pid,
|
||||
"risk": Body.risk,
|
||||
"description": Body.description or "",
|
||||
"is_enabled": bool(Body.is_enabled),
|
||||
"document_attribute_type": self._normalize_document_attribute_type(Body.document_attribute_type),
|
||||
"references_laws": json.dumps(Body.references_laws or self._default_json("references_laws"), ensure_ascii=False),
|
||||
"extraction_config": json.dumps(Body.extraction_config or self._default_json("extraction_config"), ensure_ascii=False),
|
||||
"evaluation_config": json.dumps(Body.evaluation_config or self._default_json("evaluation_config"), ensure_ascii=False),
|
||||
"pass_message": Body.pass_message or "",
|
||||
"fail_message": Body.fail_message or "",
|
||||
"suggestion_message": Body.suggestion_message or "",
|
||||
"suggestion_message_type": Body.suggestion_message_type or "warning",
|
||||
"post_action": Body.post_action or "none",
|
||||
"action_config": Body.action_config or "",
|
||||
"score": float(Body.score or 0),
|
||||
"area": (Body.area or "").strip() or None,
|
||||
"created_at": Now,
|
||||
"updated_at": Now,
|
||||
}
|
||||
|
||||
def _to_point_vo(self, row: dict[str, Any]) -> EvaluationPointVO:
|
||||
group_id = row.get("evaluation_point_groups_id")
|
||||
return EvaluationPointVO(
|
||||
id=int(row["id"]),
|
||||
code=str(row.get("code") or ""),
|
||||
name=str(row.get("name") or ""),
|
||||
evaluation_point_groups_id=int(group_id) if group_id is not None else None,
|
||||
evaluation_point_groups_pid=int(row["evaluation_point_groups_pid"]) if row.get("evaluation_point_groups_pid") is not None else None,
|
||||
ruleType=str(row.get("rule_type") or ""),
|
||||
groupName=str(row.get("group_name") or ""),
|
||||
groupId=str(group_id) if group_id is not None else "",
|
||||
risk=str(row.get("risk") or ""),
|
||||
description=str(row.get("description") or ""),
|
||||
is_enabled=bool(row.get("is_enabled")),
|
||||
document_attribute_type=self._normalize_document_attribute_type(row.get("document_attribute_type")),
|
||||
references_laws=self._parse_json(row.get("references_laws"), self._default_json("references_laws")),
|
||||
extraction_config=self._parse_json(row.get("extraction_config"), self._default_json("extraction_config")),
|
||||
evaluation_config=self._parse_json(row.get("evaluation_config"), self._default_json("evaluation_config")),
|
||||
pass_message=str(row.get("pass_message") or ""),
|
||||
fail_message=str(row.get("fail_message") or ""),
|
||||
suggestion_message=str(row.get("suggestion_message") or ""),
|
||||
suggestion_message_type=str(row.get("suggestion_message_type") or "warning"),
|
||||
post_action=str(row.get("post_action") or "none"),
|
||||
action_config=str(row.get("action_config") or ""),
|
||||
score=self._normalize_score(row.get("score")),
|
||||
area=str(row.get("area") or ""),
|
||||
created_at=self._format_datetime(row.get("created_at")),
|
||||
updated_at=self._format_datetime(row.get("updated_at")),
|
||||
)
|
||||
|
||||
def _parse_json(self, value: Any, default: Any) -> Any:
|
||||
if value is None:
|
||||
return default
|
||||
if isinstance(value, (dict, list)):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
return json.loads(value)
|
||||
except json.JSONDecodeError:
|
||||
return default
|
||||
return value
|
||||
|
||||
def _format_datetime(self, value: Any) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, datetime):
|
||||
return value.isoformat()
|
||||
return str(value)
|
||||
|
||||
def _normalize_document_attribute_type(self, value: Any) -> str:
|
||||
normalized = str(value or "").strip()
|
||||
if not normalized:
|
||||
return "通用"
|
||||
if normalized == "ALL":
|
||||
return "通用"
|
||||
return normalized
|
||||
|
||||
def _normalize_score(self, value: Any) -> float:
|
||||
if value is None:
|
||||
return 0
|
||||
if isinstance(value, Decimal):
|
||||
return float(value)
|
||||
if isinstance(value, (int, float)):
|
||||
return float(value)
|
||||
try:
|
||||
return float(value)
|
||||
except (TypeError, ValueError):
|
||||
return 0
|
||||
|
||||
def _normalize_scalar_field(self, field: str, value: Any) -> Any:
|
||||
if field in {"code", "name"}:
|
||||
return str(value or "").strip()
|
||||
if field == "document_attribute_type":
|
||||
return self._normalize_document_attribute_type(value)
|
||||
if field == "area":
|
||||
area = str(value or "").strip()
|
||||
return area or None
|
||||
if field == "score":
|
||||
return self._normalize_score(value)
|
||||
return value
|
||||
|
||||
def _default_json(self, field: str) -> dict[str, Any]:
|
||||
if field == "references_laws":
|
||||
return {"name": "", "content": "", "articles": []}
|
||||
if field == "evaluation_config":
|
||||
return {"logicType": "and", "customLogic": "", "rules": []}
|
||||
return {
|
||||
"llm": {"fields": [], "prompt_setting": {"type": "system", "template": ""}},
|
||||
"vlm": {"fields": [], "prompt_setting": {"type": "system", "template": ""}},
|
||||
"regex": {"fields": []},
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
from sqlalchemy import bindparam, text
|
||||
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
||||
|
||||
from fastapi_admin.config import DB_HOST, DB_PASSWORD, DB_PORT, DB_USER
|
||||
from fastapi_common.fastapi_common_sqlalchemy.database import GetAsyncSession
|
||||
from fastapi_common.fastapi_common_web.exception.LeauditException import LeauditException
|
||||
from fastapi_common.fastapi_common_web.domain.responses import StatusCodeEnum
|
||||
from fastapi_modules.fastapi_leaudit.domian.Dto.promptTemplateDto import PromptTemplateCreateDTO, PromptTemplateUpdateDTO
|
||||
from fastapi_modules.fastapi_leaudit.domian.vo.promptTemplateVo import PromptTemplateListVO, PromptTemplateTypeListVO, PromptTemplateTypeOptionVO, PromptTemplateVO
|
||||
from fastapi_modules.fastapi_leaudit.services.promptTemplateService import IPromptTemplateService
|
||||
|
||||
_LEGACY_DB_NAME = os.getenv("LEGACY_RULE_DB_NAME", "docauditai")
|
||||
_LEGACY_DB_URL = (
|
||||
f"postgresql+asyncpg://{quote_plus(str(DB_USER))}:{quote_plus(str(DB_PASSWORD))}"
|
||||
f"@{DB_HOST}:{DB_PORT}/{quote_plus(_LEGACY_DB_NAME)}"
|
||||
)
|
||||
_LEGACY_ENGINE = create_async_engine(_LEGACY_DB_URL, pool_pre_ping=True)
|
||||
_LegacySession = async_sessionmaker(_LEGACY_ENGINE, expire_on_commit=False)
|
||||
|
||||
_ALLOWED_TEMPLATE_TYPES = {"LLM_Extraction", "VLM_Extraction", "Evaluation", "Summary", "Common"}
|
||||
_TYPE_LABELS = {
|
||||
"LLM_Extraction": "LLM抽取",
|
||||
"VLM_Extraction": "VLM抽取",
|
||||
"Evaluation": "评查",
|
||||
"Summary": "摘要",
|
||||
"Common": "通用",
|
||||
}
|
||||
|
||||
|
||||
class PromptTemplateServiceImpl(IPromptTemplateService):
|
||||
async def ListTemplates(
|
||||
self,
|
||||
Search: str | None,
|
||||
TemplateTypes: list[str] | None,
|
||||
Status: int | None,
|
||||
Page: int,
|
||||
PageSize: int,
|
||||
) -> PromptTemplateListVO:
|
||||
offset = max(Page - 1, 0) * PageSize
|
||||
filters = ["1=1"]
|
||||
params: dict[str, Any] = {"limit": PageSize, "offset": offset}
|
||||
|
||||
if Search:
|
||||
filters.append("(pt.template_name ILIKE :search OR pt.template_code ILIKE :search)")
|
||||
params["search"] = f"%{Search.strip()}%"
|
||||
normalized_types = [item for item in (TemplateTypes or []) if item in _ALLOWED_TEMPLATE_TYPES]
|
||||
if normalized_types:
|
||||
filters.append("pt.template_type IN :template_types")
|
||||
params["template_types"] = tuple(normalized_types)
|
||||
if Status is not None:
|
||||
filters.append("pt.status = :status")
|
||||
params["status"] = Status
|
||||
|
||||
where_clause = " AND ".join(filters)
|
||||
|
||||
count_sql = text(f"SELECT COUNT(*) FROM prompt_templates pt WHERE {where_clause}")
|
||||
list_sql = text(
|
||||
f"""
|
||||
SELECT
|
||||
pt.id,
|
||||
pt.template_name,
|
||||
pt.template_type,
|
||||
pt.description,
|
||||
pt.template_content,
|
||||
pt.variables,
|
||||
pt.status,
|
||||
pt.version,
|
||||
pt.created_by,
|
||||
pt.created_at,
|
||||
pt.updated_at,
|
||||
pt.template_code,
|
||||
pt.template_abbreviation
|
||||
FROM prompt_templates pt
|
||||
WHERE {where_clause}
|
||||
ORDER BY pt.updated_at DESC, pt.id DESC
|
||||
LIMIT :limit OFFSET :offset
|
||||
"""
|
||||
)
|
||||
if normalized_types:
|
||||
count_sql = count_sql.bindparams(bindparam("template_types", expanding=True))
|
||||
list_sql = list_sql.bindparams(bindparam("template_types", expanding=True))
|
||||
|
||||
async with _LegacySession() as session:
|
||||
total = int((await session.execute(count_sql, params)).scalar_one())
|
||||
rows = (
|
||||
await session.execute(
|
||||
list_sql,
|
||||
params,
|
||||
)
|
||||
).mappings().all()
|
||||
|
||||
usernames = await self._load_usernames([int(row["created_by"]) for row in rows if row.get("created_by") is not None])
|
||||
items = [self._to_vo(row, usernames.get(int(row["created_by"]))) for row in rows]
|
||||
return PromptTemplateListVO(total=total, page=Page, page_size=PageSize, items=items)
|
||||
|
||||
async def GetTemplate(self, TemplateId: int) -> PromptTemplateVO:
|
||||
async with _LegacySession() as session:
|
||||
row = (
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT
|
||||
id,
|
||||
template_name,
|
||||
template_type,
|
||||
description,
|
||||
template_content,
|
||||
variables,
|
||||
status,
|
||||
version,
|
||||
created_by,
|
||||
created_at,
|
||||
updated_at,
|
||||
template_code,
|
||||
template_abbreviation
|
||||
FROM prompt_templates
|
||||
WHERE id = :id
|
||||
"""
|
||||
),
|
||||
{"id": TemplateId},
|
||||
)
|
||||
).mappings().first()
|
||||
if not row:
|
||||
raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "提示词模板不存在")
|
||||
usernames = await self._load_usernames([int(row["created_by"])] if row.get("created_by") is not None else [])
|
||||
return self._to_vo(row, usernames.get(int(row["created_by"])) if row.get("created_by") is not None else None)
|
||||
|
||||
async def CreateTemplate(self, Body: PromptTemplateCreateDTO) -> PromptTemplateVO:
|
||||
await self._validate_template_payload(Body.template_type, Body.template_code, Body.template_abbreviation)
|
||||
await self._ensure_template_code_unique(Body.template_code)
|
||||
now = datetime.utcnow()
|
||||
payload = self._build_write_payload(Body)
|
||||
payload.update({"created_at": now, "updated_at": now})
|
||||
async with _LegacySession() as session:
|
||||
async with session.begin():
|
||||
new_id = await session.scalar(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO prompt_templates (
|
||||
template_name,
|
||||
template_type,
|
||||
description,
|
||||
template_content,
|
||||
variables,
|
||||
status,
|
||||
version,
|
||||
created_by,
|
||||
created_at,
|
||||
updated_at,
|
||||
template_code,
|
||||
template_abbreviation
|
||||
) VALUES (
|
||||
:template_name,
|
||||
:template_type,
|
||||
:description,
|
||||
:template_content,
|
||||
CAST(:variables AS jsonb),
|
||||
:status,
|
||||
:version,
|
||||
:created_by,
|
||||
:created_at,
|
||||
:updated_at,
|
||||
:template_code,
|
||||
:template_abbreviation
|
||||
) RETURNING id
|
||||
"""
|
||||
),
|
||||
payload,
|
||||
)
|
||||
await session.commit()
|
||||
return await self.GetTemplate(int(new_id))
|
||||
|
||||
async def UpdateTemplate(self, TemplateId: int, Body: PromptTemplateUpdateDTO) -> PromptTemplateVO:
|
||||
current = await self.GetTemplate(TemplateId)
|
||||
payload = Body.model_dump(exclude_unset=True)
|
||||
if not payload:
|
||||
return current
|
||||
new_type = payload.get("template_type", current.template_type)
|
||||
new_code = payload.get("template_code", current.template_code)
|
||||
new_abbr = payload.get("template_abbreviation", current.template_abbreviation)
|
||||
await self._validate_template_payload(new_type, new_code, new_abbr)
|
||||
if "template_code" in payload:
|
||||
await self._ensure_template_code_unique(payload.get("template_code"), TemplateId)
|
||||
|
||||
updates: list[str] = []
|
||||
params: dict[str, Any] = {"id": TemplateId, "updated_at": datetime.utcnow()}
|
||||
simple_fields = [
|
||||
"template_name",
|
||||
"template_type",
|
||||
"description",
|
||||
"template_content",
|
||||
"status",
|
||||
"version",
|
||||
"template_code",
|
||||
"template_abbreviation",
|
||||
]
|
||||
for field in simple_fields:
|
||||
if field in payload:
|
||||
params[field] = payload[field]
|
||||
updates.append(f"{field} = :{field}")
|
||||
if "variables" in payload:
|
||||
params["variables"] = json.dumps(payload.get("variables") or {}, ensure_ascii=False)
|
||||
updates.append("variables = CAST(:variables AS jsonb)")
|
||||
updates.append("updated_at = :updated_at")
|
||||
|
||||
async with _LegacySession() as session:
|
||||
async with session.begin():
|
||||
await session.execute(text(f"UPDATE prompt_templates SET {', '.join(updates)} WHERE id = :id"), params)
|
||||
await session.commit()
|
||||
return await self.GetTemplate(TemplateId)
|
||||
|
||||
async def DeleteTemplate(self, TemplateId: int) -> None:
|
||||
await self.GetTemplate(TemplateId)
|
||||
async with _LegacySession() as session:
|
||||
async with session.begin():
|
||||
await session.execute(text("DELETE FROM prompt_templates WHERE id = :id"), {"id": TemplateId})
|
||||
await session.commit()
|
||||
|
||||
async def GetTemplateTypes(self) -> PromptTemplateTypeListVO:
|
||||
async with _LegacySession() as session:
|
||||
rows = (
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT template_type, COUNT(*)::int AS count
|
||||
FROM prompt_templates
|
||||
WHERE template_type IS NOT NULL AND TRIM(template_type) <> ''
|
||||
GROUP BY template_type
|
||||
ORDER BY template_type ASC
|
||||
"""
|
||||
)
|
||||
)
|
||||
).mappings().all()
|
||||
items = [
|
||||
PromptTemplateTypeOptionVO(
|
||||
value=str(row["template_type"]),
|
||||
label=_TYPE_LABELS.get(str(row["template_type"]), str(row["template_type"])),
|
||||
count=int(row["count"] or 0),
|
||||
)
|
||||
for row in rows
|
||||
]
|
||||
return PromptTemplateTypeListVO(items=items)
|
||||
|
||||
async def DuplicateTemplate(self, TemplateId: int, NewCode: str | None) -> PromptTemplateVO:
|
||||
current = await self.GetTemplate(TemplateId)
|
||||
code = (NewCode or "").strip() or self._generate_copy_code(current.template_code or f"template_{TemplateId}")
|
||||
await self._ensure_template_code_unique(code)
|
||||
body = PromptTemplateCreateDTO(
|
||||
template_name=f"{current.template_name}-副本",
|
||||
template_type=current.template_type,
|
||||
description=current.description,
|
||||
template_content=current.template_content,
|
||||
variables=current.variables,
|
||||
status=current.status,
|
||||
version=current.version,
|
||||
created_by=current.created_by,
|
||||
template_code=code,
|
||||
template_abbreviation=current.template_abbreviation,
|
||||
)
|
||||
return await self.CreateTemplate(body)
|
||||
|
||||
async def _load_usernames(self, user_ids: list[int]) -> dict[int, str]:
|
||||
ids = sorted({int(item) for item in user_ids if item is not None})
|
||||
if not ids:
|
||||
return {}
|
||||
async with GetAsyncSession() as session:
|
||||
rows = (
|
||||
await session.execute(
|
||||
text("SELECT id, username FROM sso_users WHERE id IN :ids").bindparams(bindparam("ids", expanding=True)),
|
||||
{"ids": ids},
|
||||
)
|
||||
).mappings().all()
|
||||
return {int(row["id"]): str(row.get("username") or "") for row in rows}
|
||||
|
||||
async def _validate_template_payload(self, template_type: str | None, template_code: str | None, template_abbreviation: str | None) -> None:
|
||||
if template_type not in _ALLOWED_TEMPLATE_TYPES:
|
||||
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "模板类型不合法")
|
||||
if template_type == "VLM_Extraction":
|
||||
if not str(template_code or "").strip():
|
||||
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "VLM抽取模板必须填写模板code")
|
||||
if not str(template_abbreviation or "").strip():
|
||||
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "VLM抽取模板必须填写模板简称")
|
||||
|
||||
async def _ensure_template_code_unique(self, template_code: str | None, template_id: int | None = None) -> None:
|
||||
code = str(template_code or "").strip()
|
||||
if not code:
|
||||
return
|
||||
sql = "SELECT id FROM prompt_templates WHERE LOWER(template_code) = LOWER(:template_code)"
|
||||
params: dict[str, Any] = {"template_code": code}
|
||||
if template_id is not None:
|
||||
sql += " AND id <> :id"
|
||||
params["id"] = template_id
|
||||
async with _LegacySession() as session:
|
||||
exists = (await session.execute(text(sql), params)).scalar_one_or_none()
|
||||
if exists:
|
||||
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "模板code已存在")
|
||||
|
||||
def _build_write_payload(self, body: PromptTemplateCreateDTO) -> dict[str, Any]:
|
||||
return {
|
||||
"template_name": body.template_name.strip(),
|
||||
"template_type": body.template_type,
|
||||
"description": (body.description or "").strip() or None,
|
||||
"template_content": body.template_content,
|
||||
"variables": json.dumps(body.variables or {}, ensure_ascii=False),
|
||||
"status": int(body.status),
|
||||
"version": str(body.version or "v1.0").strip() or "v1.0",
|
||||
"created_by": int(body.created_by) if body.created_by is not None else None,
|
||||
"template_code": (body.template_code or "").strip() or None,
|
||||
"template_abbreviation": (body.template_abbreviation or "").strip() or None,
|
||||
}
|
||||
|
||||
def _to_vo(self, row: dict[str, Any], username: str | None) -> PromptTemplateVO:
|
||||
return PromptTemplateVO(
|
||||
id=int(row["id"]),
|
||||
template_name=str(row.get("template_name") or ""),
|
||||
template_code=row.get("template_code"),
|
||||
template_type=str(row.get("template_type") or ""),
|
||||
description=row.get("description"),
|
||||
template_content=str(row.get("template_content") or ""),
|
||||
template_abbreviation=row.get("template_abbreviation"),
|
||||
variables=self._parse_variables(row.get("variables")),
|
||||
status=int(row.get("status") or 0),
|
||||
version=str(row.get("version") or "v1.0"),
|
||||
created_by=int(row["created_by"]) if row.get("created_by") is not None else None,
|
||||
created_by_username=username,
|
||||
created_at=self._format_datetime(row.get("created_at")),
|
||||
updated_at=self._format_datetime(row.get("updated_at")),
|
||||
)
|
||||
|
||||
def _parse_variables(self, value: Any) -> dict[str, str]:
|
||||
if value is None:
|
||||
return {}
|
||||
if isinstance(value, dict):
|
||||
return {str(k): str(v) for k, v in value.items()}
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
parsed = json.loads(value)
|
||||
if isinstance(parsed, dict):
|
||||
return {str(k): str(v) for k, v in parsed.items()}
|
||||
except json.JSONDecodeError:
|
||||
return {}
|
||||
return {}
|
||||
|
||||
def _format_datetime(self, value: Any) -> str:
|
||||
if isinstance(value, datetime):
|
||||
return value.isoformat()
|
||||
return str(value or "")
|
||||
|
||||
def _generate_copy_code(self, code: str) -> str:
|
||||
return f"{code}_copy_{int(datetime.utcnow().timestamp())}"
|
||||
@@ -12,12 +12,14 @@ 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.rbacAdminDto import (
|
||||
RoleAccessSaveDTO,
|
||||
RoleCreateDTO,
|
||||
RolePermissionsBatchDTO,
|
||||
RoleRoutesUpdateDTO,
|
||||
RoleUpdateDTO,
|
||||
)
|
||||
from fastapi_modules.fastapi_leaudit.domian.vo.rbacAdminVo import (
|
||||
RoleAccessSaveVO,
|
||||
RoleListVO,
|
||||
RolePermissionsVO,
|
||||
RoleRoutesVO,
|
||||
@@ -156,6 +158,11 @@ class RbacAdminServiceImpl(IRbacAdminService):
|
||||
{"permission_key": "doc_type:create:write", "display_name": "创建文档类型", "module": "doc_type", "resource": "create", "action": "write", "api_method": "POST", "api_path": "/api/document-types", "route_path": "/document-types"},
|
||||
{"permission_key": "doc_type:update:write", "display_name": "更新文档类型", "module": "doc_type", "resource": "update", "action": "write", "api_method": "PUT", "api_path": "/api/document-types/{id}", "route_path": "/document-types"},
|
||||
{"permission_key": "doc_type:delete:delete", "display_name": "删除文档类型", "module": "doc_type", "resource": "delete", "action": "delete", "api_method": "DELETE", "api_path": "/api/document-types/{id}", "route_path": "/document-types"},
|
||||
{"permission_key": "evaluation_point:list:read", "display_name": "评查点列表", "module": "evaluation_point", "resource": "list", "action": "read", "api_method": "GET", "api_path": "/api/v3/evaluation-points", "route_path": "/rules"},
|
||||
{"permission_key": "evaluation_point:detail:read", "display_name": "评查点详情", "module": "evaluation_point", "resource": "detail", "action": "read", "api_method": "GET", "api_path": "/api/v3/evaluation-points/{id}", "route_path": "/rules"},
|
||||
{"permission_key": "evaluation_point:create:write", "display_name": "创建评查点", "module": "evaluation_point", "resource": "create", "action": "write", "api_method": "POST", "api_path": "/api/v3/evaluation-points", "route_path": "/rules"},
|
||||
{"permission_key": "evaluation_point:update:write", "display_name": "更新评查点", "module": "evaluation_point", "resource": "update", "action": "write", "api_method": "PUT", "api_path": "/api/v3/evaluation-points/{id}", "route_path": "/rules"},
|
||||
{"permission_key": "evaluation_point:delete:delete", "display_name": "删除评查点", "module": "evaluation_point", "resource": "delete", "action": "delete", "api_method": "DELETE", "api_path": "/api/v3/evaluation-points/{id}", "route_path": "/rules"},
|
||||
{"permission_key": "rbac:roles:read", "display_name": "角色列表", "module": "rbac", "resource": "roles", "action": "read", "api_method": "GET", "api_path": "/api/v3/rbac/roles", "route_path": "/role-permissions"},
|
||||
{"permission_key": "rbac:roles:create", "display_name": "创建角色", "module": "rbac", "resource": "roles", "action": "create", "api_method": "POST", "api_path": "/api/v3/rbac/roles", "route_path": "/role-permissions"},
|
||||
{"permission_key": "rbac:roles:update", "display_name": "更新角色", "module": "rbac", "resource": "roles", "action": "update", "api_method": "PUT", "api_path": "/api/v3/rbac/roles/{role_id}", "route_path": "/role-permissions"},
|
||||
@@ -495,32 +502,9 @@ class RbacAdminServiceImpl(IRbacAdminService):
|
||||
routeIds = sorted(set(Body.route_ids))
|
||||
async with GetAsyncSession() as Session:
|
||||
await self._ensureAdminSeeds(Session)
|
||||
allRouteIds = [row[0] for row in (await Session.execute(text("SELECT id FROM sys_routes WHERE deleted_at IS NULL AND route_path = ANY(:paths)").bindparams(paths=[item["route_path"] for item in self._MANAGEABLE_ROUTE_BLUEPRINTS]))).fetchall()]
|
||||
existingRows = (
|
||||
await Session.execute(text("SELECT route_id, status FROM role_route WHERE role_id = :role_id AND route_id = ANY(:route_ids)").bindparams(route_ids=allRouteIds), {"role_id": RoleId})
|
||||
).fetchall()
|
||||
existingMap = {int(routeId): int(status) for routeId, status in existingRows}
|
||||
insertedCount = 0
|
||||
for routeId in allRouteIds:
|
||||
if routeId in routeIds:
|
||||
if routeId in existingMap:
|
||||
await Session.execute(
|
||||
text("UPDATE role_route SET status = 1, permission = :permission, updated_at = NOW() WHERE role_id = :role_id AND route_id = :route_id"),
|
||||
{"role_id": RoleId, "route_id": routeId, "permission": Body.permission},
|
||||
)
|
||||
else:
|
||||
await Session.execute(
|
||||
text("INSERT INTO role_route (role_id, route_id, permission, status, created_at, updated_at) VALUES (:role_id, :route_id, :permission, 1, NOW(), NOW())"),
|
||||
{"role_id": RoleId, "route_id": routeId, "permission": Body.permission},
|
||||
)
|
||||
insertedCount += 1
|
||||
elif routeId in existingMap and existingMap[routeId] != 0:
|
||||
await Session.execute(
|
||||
text("UPDATE role_route SET status = 0, updated_at = NOW() WHERE role_id = :role_id AND route_id = :route_id"),
|
||||
{"role_id": RoleId, "route_id": routeId},
|
||||
)
|
||||
result = await self._updateRoleRoutesInSession(Session, RoleId, routeIds, Body.permission)
|
||||
await Session.commit()
|
||||
return RoleRouteUpdateResultVO(role_id=RoleId, enabled_count=len(routeIds), disabled_count=max(len(allRouteIds) - len(routeIds), 0), inserted_count=insertedCount, route_ids=routeIds)
|
||||
return result
|
||||
|
||||
async def GetRolePermissions(self, CurrentUserId: int, RoleId: int) -> RolePermissionsVO:
|
||||
"""查询角色权限授权。"""
|
||||
@@ -550,29 +534,29 @@ class RbacAdminServiceImpl(IRbacAdminService):
|
||||
await self._assertPermission(CurrentUserId, "rbac:role_permissions:write")
|
||||
async with GetAsyncSession() as Session:
|
||||
await self._ensureAdminSeeds(Session)
|
||||
permissionIds = [item.permission_id for item in Body.permissions]
|
||||
if Body.replace:
|
||||
await Session.execute(text("DELETE FROM role_permissions WHERE role_id = :role_id"), {"role_id": Body.role_id})
|
||||
for item in Body.permissions:
|
||||
await Session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO role_permissions (role_id, permission_id, grant_type, data_scope, created_at, updated_at)
|
||||
VALUES (:role_id, :permission_id, :grant_type, :data_scope, NOW(), NOW())
|
||||
ON CONFLICT (role_id, permission_id)
|
||||
DO UPDATE SET grant_type = EXCLUDED.grant_type, data_scope = EXCLUDED.data_scope, updated_at = NOW()
|
||||
"""
|
||||
),
|
||||
{
|
||||
"role_id": Body.role_id,
|
||||
"permission_id": item.permission_id,
|
||||
"grant_type": item.grant_type,
|
||||
"data_scope": item.data_scope,
|
||||
},
|
||||
)
|
||||
await self._saveRolePermissionsInSession(Session, Body.role_id, Body.permissions, Body.replace, Body.replace_scope_permission_ids)
|
||||
await Session.commit()
|
||||
return await self.GetRolePermissions(CurrentUserId, Body.role_id)
|
||||
|
||||
async def SaveRoleAccess(self, CurrentUserId: int, RoleId: int, Body: RoleAccessSaveDTO) -> RoleAccessSaveVO:
|
||||
"""原子保存角色菜单与接口权限。"""
|
||||
await self._assertManagePermission(CurrentUserId)
|
||||
await self._assertPermission(CurrentUserId, "rbac:role_routes:write")
|
||||
await self._assertPermission(CurrentUserId, "rbac:role_permissions:write")
|
||||
routeIds = sorted(set(int(routeId) for routeId in Body.route_ids))
|
||||
permissionIds = sorted(set(int(permissionId) for permissionId in Body.permission_ids))
|
||||
permissionConfigs = [
|
||||
{"permission_id": permissionId, "grant_type": "GRANT", "data_scope": "ALL"}
|
||||
for permissionId in permissionIds
|
||||
]
|
||||
async with GetAsyncSession() as Session:
|
||||
await self._ensureAdminSeeds(Session)
|
||||
routeResult = await self._updateRoleRoutesInSession(Session, RoleId, routeIds, Body.route_permission)
|
||||
await self._saveRolePermissionsInSession(Session, RoleId, permissionConfigs, True, Body.replace_scope_permission_ids)
|
||||
permissionResult = await self._getRolePermissionsInSession(Session, RoleId)
|
||||
await Session.commit()
|
||||
return RoleAccessSaveVO(role_id=RoleId, route_result=routeResult, permission_result=permissionResult)
|
||||
|
||||
async def GetRoutePermissions(self, CurrentUserId: int, RouteId: int) -> RoutePermissionsVO:
|
||||
"""查询路由关联权限定义。"""
|
||||
await self._assertManagePermission(CurrentUserId)
|
||||
@@ -592,6 +576,104 @@ class RbacAdminServiceImpl(IRbacAdminService):
|
||||
permissions=permissionMap.get(RouteId, []),
|
||||
)
|
||||
|
||||
async def _updateRoleRoutesInSession(self, Session, RoleId: int, RouteIds: list[int], Permission: str) -> RoleRouteUpdateResultVO:
|
||||
"""在当前事务中写入角色路由授权。"""
|
||||
allRouteIds = [
|
||||
row[0]
|
||||
for row in (
|
||||
await Session.execute(
|
||||
text("SELECT id FROM sys_routes WHERE deleted_at IS NULL AND route_path = ANY(:paths)").bindparams(
|
||||
paths=[item["route_path"] for item in self._MANAGEABLE_ROUTE_BLUEPRINTS]
|
||||
)
|
||||
)
|
||||
).fetchall()
|
||||
]
|
||||
existingRows = (
|
||||
await Session.execute(
|
||||
text("SELECT route_id, status FROM role_route WHERE role_id = :role_id AND route_id = ANY(:route_ids)").bindparams(route_ids=allRouteIds),
|
||||
{"role_id": RoleId},
|
||||
)
|
||||
).fetchall()
|
||||
existingMap = {int(routeId): int(status) for routeId, status in existingRows}
|
||||
insertedCount = 0
|
||||
for routeId in allRouteIds:
|
||||
if routeId in RouteIds:
|
||||
if routeId in existingMap:
|
||||
await Session.execute(
|
||||
text("UPDATE role_route SET status = 1, permission = :permission, updated_at = NOW() WHERE role_id = :role_id AND route_id = :route_id"),
|
||||
{"role_id": RoleId, "route_id": routeId, "permission": Permission},
|
||||
)
|
||||
else:
|
||||
await Session.execute(
|
||||
text("INSERT INTO role_route (role_id, route_id, permission, status, created_at, updated_at) VALUES (:role_id, :route_id, :permission, 1, NOW(), NOW())"),
|
||||
{"role_id": RoleId, "route_id": routeId, "permission": Permission},
|
||||
)
|
||||
insertedCount += 1
|
||||
elif routeId in existingMap and existingMap[routeId] != 0:
|
||||
await Session.execute(
|
||||
text("UPDATE role_route SET status = 0, updated_at = NOW() WHERE role_id = :role_id AND route_id = :route_id"),
|
||||
{"role_id": RoleId, "route_id": routeId},
|
||||
)
|
||||
return RoleRouteUpdateResultVO(
|
||||
role_id=RoleId,
|
||||
enabled_count=len(RouteIds),
|
||||
disabled_count=max(len(allRouteIds) - len(RouteIds), 0),
|
||||
inserted_count=insertedCount,
|
||||
route_ids=RouteIds,
|
||||
)
|
||||
|
||||
async def _saveRolePermissionsInSession(self, Session, RoleId: int, Permissions: list[Any], Replace: bool, ReplaceScopePermissionIds: list[int]) -> None:
|
||||
"""在当前事务中写入角色接口权限。"""
|
||||
if Replace:
|
||||
scopeIds = sorted({int(permissionId) for permissionId in ReplaceScopePermissionIds if permissionId})
|
||||
if scopeIds:
|
||||
# 仅清理当前页面负责维护的权限范围,避免局部页面保存时误删其他模块权限。
|
||||
await Session.execute(
|
||||
text("DELETE FROM role_permissions WHERE role_id = :role_id AND permission_id = ANY(:permission_ids)").bindparams(permission_ids=scopeIds),
|
||||
{"role_id": RoleId},
|
||||
)
|
||||
else:
|
||||
# 兼容旧调用方:若未传作用域,保留原有全量替换行为。
|
||||
await Session.execute(text("DELETE FROM role_permissions WHERE role_id = :role_id"), {"role_id": RoleId})
|
||||
for item in Permissions:
|
||||
permissionId = int(item.permission_id if hasattr(item, "permission_id") else item["permission_id"])
|
||||
grantType = item.grant_type if hasattr(item, "grant_type") else item.get("grant_type")
|
||||
dataScope = item.data_scope if hasattr(item, "data_scope") else item.get("data_scope")
|
||||
await Session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO role_permissions (role_id, permission_id, grant_type, data_scope, created_at, updated_at)
|
||||
VALUES (:role_id, :permission_id, :grant_type, :data_scope, NOW(), NOW())
|
||||
ON CONFLICT (role_id, permission_id)
|
||||
DO UPDATE SET grant_type = EXCLUDED.grant_type, data_scope = EXCLUDED.data_scope, updated_at = NOW()
|
||||
"""
|
||||
),
|
||||
{
|
||||
"role_id": RoleId,
|
||||
"permission_id": permissionId,
|
||||
"grant_type": grantType,
|
||||
"data_scope": dataScope,
|
||||
},
|
||||
)
|
||||
|
||||
async def _getRolePermissionsInSession(self, Session, RoleId: int) -> RolePermissionsVO:
|
||||
"""在当前事务中查询角色接口权限。"""
|
||||
rows = (
|
||||
await Session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT rp.id, rp.permission_id, p.permission_key, p.display_name, rp.grant_type, rp.data_scope
|
||||
FROM role_permissions rp
|
||||
JOIN permissions p ON p.id = rp.permission_id
|
||||
WHERE rp.role_id = :role_id
|
||||
ORDER BY p.sort_order ASC, p.id ASC
|
||||
"""
|
||||
),
|
||||
{"role_id": RoleId},
|
||||
)
|
||||
).mappings().all()
|
||||
return RolePermissionsVO(role_id=RoleId, permissions=[self._toRolePermissionVo(row) for row in rows])
|
||||
|
||||
async def _assertManagePermission(self, CurrentUserId: int) -> None:
|
||||
"""校验当前用户是否具备管理能力。"""
|
||||
context = await self._getCurrentUserContext(CurrentUserId)
|
||||
|
||||
@@ -21,8 +21,13 @@ class RbacServiceImpl(IRbacService):
|
||||
_MINIMAL_VISIBLE_ROUTE_PREFIXES: tuple[str, ...] = (
|
||||
"/home",
|
||||
"/chat-with-llm",
|
||||
"/contract-template",
|
||||
"/cross-checking",
|
||||
"/files",
|
||||
"/documents",
|
||||
"/rules",
|
||||
"/rule-groups",
|
||||
"/rules-files",
|
||||
"/settings",
|
||||
"/entry-modules",
|
||||
"/role-permissions",
|
||||
@@ -108,7 +113,7 @@ class RbacServiceImpl(IRbacService):
|
||||
"route_name": "rule-management",
|
||||
"component": "rules",
|
||||
"parent_id": None,
|
||||
"route_title": "评查规则库",
|
||||
"route_title": "规则管理",
|
||||
"icon": "ri-book-3-line",
|
||||
"sort_order": 4,
|
||||
"is_hidden": False,
|
||||
@@ -165,7 +170,7 @@ class RbacServiceImpl(IRbacService):
|
||||
"route_name": "contract-template",
|
||||
"component": "contract-template",
|
||||
"parent_id": None,
|
||||
"route_title": "合同模板",
|
||||
"route_title": "合同管理",
|
||||
"icon": "ri-file-search-line",
|
||||
"sort_order": 5,
|
||||
"is_hidden": False,
|
||||
@@ -178,7 +183,7 @@ class RbacServiceImpl(IRbacService):
|
||||
"route_name": "contract-search-ai",
|
||||
"component": "contract-template.search",
|
||||
"parent_id": 1010,
|
||||
"route_title": "智能搜索",
|
||||
"route_title": "模板搜索",
|
||||
"icon": "ri-search-line",
|
||||
"sort_order": 1,
|
||||
"is_hidden": False,
|
||||
@@ -192,7 +197,7 @@ class RbacServiceImpl(IRbacService):
|
||||
"route_name": "contract-list",
|
||||
"component": "contract-template.list",
|
||||
"parent_id": 1010,
|
||||
"route_title": "合同列表",
|
||||
"route_title": "模板列表",
|
||||
"icon": "ri-folder-line",
|
||||
"sort_order": 2,
|
||||
"is_hidden": False,
|
||||
@@ -243,6 +248,20 @@ class RbacServiceImpl(IRbacService):
|
||||
"meta": {"group": "settings"},
|
||||
"children": None,
|
||||
},
|
||||
{
|
||||
"id": 1016,
|
||||
"route_path": "/document-types",
|
||||
"route_name": "document-types",
|
||||
"component": "document-types",
|
||||
"parent_id": 1013,
|
||||
"route_title": "文档类型管理",
|
||||
"icon": "ri-file-list-3-line",
|
||||
"sort_order": 3,
|
||||
"is_hidden": False,
|
||||
"is_cache": True,
|
||||
"meta": {"group": "settings"},
|
||||
"children": None,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -353,7 +372,7 @@ class RbacServiceImpl(IRbacService):
|
||||
"route_name": "rule-management",
|
||||
"component": "rules",
|
||||
"parent_id": None,
|
||||
"route_title": "评查规则库",
|
||||
"route_title": "规则管理",
|
||||
"icon": "ri-book-3-line",
|
||||
"sort_order": 4,
|
||||
"is_hidden": False,
|
||||
@@ -410,7 +429,7 @@ class RbacServiceImpl(IRbacService):
|
||||
"route_name": "contract-template",
|
||||
"component": "contract-template",
|
||||
"parent_id": None,
|
||||
"route_title": "合同模板",
|
||||
"route_title": "合同管理",
|
||||
"icon": "ri-file-search-line",
|
||||
"sort_order": 5,
|
||||
"is_hidden": False,
|
||||
@@ -423,7 +442,7 @@ class RbacServiceImpl(IRbacService):
|
||||
"route_name": "contract-search-ai",
|
||||
"component": "contract-template.search",
|
||||
"parent_id": 2009,
|
||||
"route_title": "智能搜索",
|
||||
"route_title": "模板搜索",
|
||||
"icon": "ri-search-line",
|
||||
"sort_order": 1,
|
||||
"is_hidden": False,
|
||||
@@ -437,7 +456,7 @@ class RbacServiceImpl(IRbacService):
|
||||
"route_name": "contract-list",
|
||||
"component": "contract-template.list",
|
||||
"parent_id": 2009,
|
||||
"route_title": "合同列表",
|
||||
"route_title": "模板列表",
|
||||
"icon": "ri-folder-line",
|
||||
"sort_order": 2,
|
||||
"is_hidden": False,
|
||||
@@ -544,9 +563,9 @@ class RbacServiceImpl(IRbacService):
|
||||
"/entry-modules": ["entry_module:"],
|
||||
"/role-permissions": ["rbac:"],
|
||||
"/document-types": ["doc_type:"],
|
||||
"/rules": ["rules:"],
|
||||
"/rule-groups": ["rules:"],
|
||||
"/rules/list": ["rules:"],
|
||||
"/rules": ["rules:", "evaluation_point:"],
|
||||
"/rule-groups": ["evaluation_group:", "rules:"],
|
||||
"/rules/list": ["rules:", "evaluation_point:"],
|
||||
"/rules-files": ["rules:"],
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
"""规则配置页聚合服务实现。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
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 sqlalchemy import text
|
||||
|
||||
from fastapi_modules.fastapi_leaudit.domian.vo.ruleConfigVo import RuleConfigPackVO
|
||||
from fastapi_modules.fastapi_leaudit.services import IOssService
|
||||
from fastapi_modules.fastapi_leaudit.services.impl.ossServiceImpl import OssServiceImpl
|
||||
from fastapi_modules.fastapi_leaudit.services.impl.ruleServiceImpl import RuleServiceImpl
|
||||
from fastapi_modules.fastapi_leaudit.services.ruleConfigService import IRuleConfigService
|
||||
|
||||
|
||||
class RuleConfigServiceImpl(IRuleConfigService):
|
||||
"""规则配置页聚合服务实现。"""
|
||||
|
||||
def __init__(self, OssService: IOssService | None = None) -> None:
|
||||
self.OssService = OssService or OssServiceImpl()
|
||||
self.RuleService = RuleServiceImpl(self.OssService)
|
||||
|
||||
async def ListPacks(self) -> list[RuleConfigPackVO]:
|
||||
"""列出规则配置页所需的全部 pack。"""
|
||||
async with GetAsyncSession() as session:
|
||||
rows = (
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT
|
||||
child.id AS group_id,
|
||||
child.name AS subtype,
|
||||
COALESCE(child.document_type_id, root.document_type_id) AS document_type_id,
|
||||
dt.name AS document_type_name,
|
||||
root.id AS root_group_id,
|
||||
COALESCE(root.name, child.name) AS main_type,
|
||||
em.name AS entry_module_name
|
||||
FROM leaudit_evaluation_point_groups child
|
||||
LEFT JOIN leaudit_evaluation_point_groups root
|
||||
ON root.id = child.pid
|
||||
AND root.deleted_at IS NULL
|
||||
LEFT JOIN leaudit_document_types dt
|
||||
ON dt.id = COALESCE(child.document_type_id, root.document_type_id)
|
||||
LEFT JOIN leaudit_entry_modules em
|
||||
ON em.id = COALESCE(child.entry_module_id, root.entry_module_id, dt.entry_module_id)
|
||||
WHERE child.deleted_at IS NULL
|
||||
AND COALESCE(child.pid, 0) <> 0
|
||||
ORDER BY COALESCE(root.sort_order, 0) ASC, root.id ASC, child.sort_order ASC, child.id ASC
|
||||
"""
|
||||
)
|
||||
)
|
||||
).mappings().all()
|
||||
|
||||
rule_set_map = await self._load_rule_set_meta_map()
|
||||
return [await self._build_pack_vo(row, rule_set_map) for row in rows]
|
||||
|
||||
async def GetPack(self, PackId: int) -> RuleConfigPackVO:
|
||||
"""获取单个规则配置 pack。"""
|
||||
async with GetAsyncSession() as session:
|
||||
row = (
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT
|
||||
child.id AS group_id,
|
||||
child.name AS subtype,
|
||||
COALESCE(child.document_type_id, root.document_type_id) AS document_type_id,
|
||||
dt.name AS document_type_name,
|
||||
root.id AS root_group_id,
|
||||
COALESCE(root.name, child.name) AS main_type,
|
||||
em.name AS entry_module_name
|
||||
FROM leaudit_evaluation_point_groups child
|
||||
LEFT JOIN leaudit_evaluation_point_groups root
|
||||
ON root.id = child.pid
|
||||
AND root.deleted_at IS NULL
|
||||
LEFT JOIN leaudit_document_types dt
|
||||
ON dt.id = COALESCE(child.document_type_id, root.document_type_id)
|
||||
LEFT JOIN leaudit_entry_modules em
|
||||
ON em.id = COALESCE(child.entry_module_id, root.entry_module_id, dt.entry_module_id)
|
||||
WHERE child.id = :group_id
|
||||
AND child.deleted_at IS NULL
|
||||
AND COALESCE(child.pid, 0) <> 0
|
||||
LIMIT 1
|
||||
"""
|
||||
),
|
||||
{"group_id": PackId},
|
||||
)
|
||||
).mappings().first()
|
||||
|
||||
if not row:
|
||||
raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "规则配置 pack 不存在")
|
||||
|
||||
rule_set_map = await self._load_rule_set_meta_map()
|
||||
return await self._build_pack_vo(row, rule_set_map)
|
||||
|
||||
async def _build_pack_vo(self, row, rule_set_map: dict[int, dict[str, object]]) -> RuleConfigPackVO:
|
||||
"""构建单个 pack 聚合对象。"""
|
||||
group_id = int(row["group_id"])
|
||||
binding = await self._load_effective_binding(group_id)
|
||||
|
||||
document_type = str(row["document_type_name"] or "").strip()
|
||||
main_type = str(row["main_type"] or "").strip()
|
||||
subtype = str(row["subtype"] or "").strip() or "通用"
|
||||
module_type = str(row["entry_module_name"] or "").strip() or (f"{document_type}评查" if document_type else "规则配置")
|
||||
|
||||
source_status = "empty"
|
||||
yaml_text = ""
|
||||
rule_set_id: int | None = None
|
||||
rule_type: str | None = None
|
||||
rule_name: str | None = None
|
||||
current_version_id: int | None = None
|
||||
fallback_version_id: int | None = None
|
||||
resolved_version_id: int | None = None
|
||||
has_usable_version = False
|
||||
usable_rule_count = 0
|
||||
binding_id: int | None = None
|
||||
|
||||
if binding:
|
||||
binding_id = int(binding["id"])
|
||||
rule_set_id = int(binding["rule_set_id"])
|
||||
rule_set_meta = rule_set_map.get(rule_set_id, {})
|
||||
rule_type = str(rule_set_meta.get("rule_type") or "") or None
|
||||
rule_name = str(rule_set_meta.get("rule_name") or "") or None
|
||||
current_version_id = self._to_int(rule_set_meta.get("current_version_id"))
|
||||
fallback_version_id = self._to_int(rule_set_meta.get("fallback_version_id"))
|
||||
resolved_version_id = current_version_id or fallback_version_id
|
||||
has_usable_version = bool(rule_set_meta.get("has_usable_version"))
|
||||
usable_rule_count = int(rule_set_meta.get("usable_rule_count") or 0)
|
||||
if resolved_version_id is not None:
|
||||
yaml_text = await self._load_yaml_text_by_version_id(resolved_version_id)
|
||||
source_status = "ready" if yaml_text.strip() else "missing"
|
||||
|
||||
return RuleConfigPackVO(
|
||||
packId=group_id,
|
||||
groupId=group_id,
|
||||
rootGroupId=self._to_int(row.get("root_group_id")),
|
||||
bindingId=binding_id,
|
||||
ruleSetId=rule_set_id,
|
||||
ruleType=rule_type,
|
||||
ruleName=rule_name,
|
||||
currentVersionId=current_version_id,
|
||||
fallbackVersionId=fallback_version_id,
|
||||
resolvedVersionId=resolved_version_id,
|
||||
hasUsableVersion=has_usable_version,
|
||||
usableRuleCount=usable_rule_count,
|
||||
documentTypeId=self._to_int(row.get("document_type_id")),
|
||||
documentType=document_type,
|
||||
moduleType=module_type,
|
||||
mainType=main_type or document_type,
|
||||
subtype=subtype,
|
||||
yamlText=yaml_text,
|
||||
sourceStatus=source_status,
|
||||
)
|
||||
|
||||
async def _load_effective_binding(self, group_id: int):
|
||||
"""读取当前二级分组实际生效的规则集绑定。"""
|
||||
async with GetAsyncSession() as session:
|
||||
row = (
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT id, rule_set_id
|
||||
FROM leaudit_rule_group_bindings
|
||||
WHERE group_id = :group_id
|
||||
AND deleted_at IS NULL
|
||||
AND is_active = TRUE
|
||||
ORDER BY priority DESC, id ASC
|
||||
LIMIT 1
|
||||
"""
|
||||
),
|
||||
{"group_id": group_id},
|
||||
)
|
||||
).mappings().first()
|
||||
return row
|
||||
|
||||
async def _load_rule_set_meta_map(self) -> dict[int, dict[str, object]]:
|
||||
"""批量读取规则集元数据。"""
|
||||
items = await self.RuleService.ListSets()
|
||||
return {
|
||||
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 items
|
||||
}
|
||||
|
||||
async def _load_yaml_text_by_version_id(self, version_id: int) -> str:
|
||||
"""按版本ID读取 YAML 正文。"""
|
||||
async with GetAsyncSession() as session:
|
||||
row = (
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT oss_url
|
||||
FROM leaudit_rule_versions
|
||||
WHERE id = :version_id
|
||||
LIMIT 1
|
||||
"""
|
||||
),
|
||||
{"version_id": version_id},
|
||||
)
|
||||
).mappings().first()
|
||||
|
||||
if not row or not row["oss_url"]:
|
||||
return ""
|
||||
|
||||
try:
|
||||
return (await self.OssService.DownloadBytes(row["oss_url"])).decode("utf-8")
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def _to_int(self, value) -> int | None:
|
||||
if value is None:
|
||||
return None
|
||||
return int(value)
|
||||
@@ -0,0 +1,617 @@
|
||||
"""Rule-group schema, bootstrap, and sync helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import bindparam, text
|
||||
|
||||
|
||||
async def ensure_rule_group_schema(session) -> None:
|
||||
"""Create the new rule-group tables when they do not exist yet."""
|
||||
statements = [
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS leaudit_evaluation_point_groups (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
pid BIGINT NOT NULL DEFAULT 0,
|
||||
code VARCHAR(120) NOT NULL,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
description TEXT NULL,
|
||||
document_type_id BIGINT NULL REFERENCES leaudit_document_types(id),
|
||||
entry_module_id BIGINT NULL REFERENCES leaudit_entry_modules(id),
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
is_enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ NULL
|
||||
)
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS leaudit_rule_group_bindings (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
group_id BIGINT NOT NULL REFERENCES leaudit_evaluation_point_groups(id),
|
||||
rule_set_id BIGINT NOT NULL REFERENCES leaudit_rule_sets(id),
|
||||
rule_type_binding_id BIGINT NULL REFERENCES leaudit_rule_type_bindings(id),
|
||||
priority INTEGER NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
note TEXT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ NULL
|
||||
)
|
||||
""",
|
||||
"CREATE INDEX IF NOT EXISTS idx_leaudit_ep_groups_pid ON leaudit_evaluation_point_groups(pid)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_leaudit_ep_groups_doc_type ON leaudit_evaluation_point_groups(document_type_id)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_leaudit_ep_groups_entry_module ON leaudit_evaluation_point_groups(entry_module_id)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_leaudit_rule_group_bindings_group_id ON leaudit_rule_group_bindings(group_id)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_leaudit_rule_group_bindings_rule_set_id ON leaudit_rule_group_bindings(rule_set_id)",
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS uq_leaudit_ep_groups_code_active ON leaudit_evaluation_point_groups (LOWER(code)) WHERE deleted_at IS NULL",
|
||||
"DROP INDEX IF EXISTS uq_leaudit_ep_groups_doc_type_active",
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS uq_leaudit_rule_group_bindings_active ON leaudit_rule_group_bindings (group_id, rule_set_id) WHERE deleted_at IS NULL",
|
||||
]
|
||||
for statement in statements:
|
||||
await session.execute(text(statement))
|
||||
|
||||
|
||||
async def bootstrap_rule_groups(session) -> None:
|
||||
"""Seed doc-type roots and default child groups from current doc-type bindings."""
|
||||
await ensure_rule_group_schema(session)
|
||||
|
||||
# Once the system has entered "business root" mode, stop recreating the
|
||||
# historical "doc type as top root" structure on every request.
|
||||
business_root_exists = bool(
|
||||
(
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT 1
|
||||
FROM leaudit_evaluation_point_groups
|
||||
WHERE deleted_at IS NULL
|
||||
AND COALESCE(pid, 0) = 0
|
||||
AND document_type_id IS NULL
|
||||
LIMIT 1
|
||||
"""
|
||||
)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
)
|
||||
if business_root_exists:
|
||||
return
|
||||
|
||||
rows = (
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT
|
||||
dt.id,
|
||||
dt.code,
|
||||
dt.name,
|
||||
dt.description,
|
||||
dt.entry_module_id,
|
||||
dt.sort_order,
|
||||
dt.is_enabled,
|
||||
em.name AS entry_module_name,
|
||||
COALESCE(
|
||||
json_agg(
|
||||
json_build_object(
|
||||
'id', b.id,
|
||||
'rule_set_id', b.rule_set_id,
|
||||
'priority', b.priority,
|
||||
'is_active', b.is_active,
|
||||
'note', b.note
|
||||
)
|
||||
ORDER BY b.priority DESC, b.id ASC
|
||||
) FILTER (WHERE b.id IS NOT NULL AND b.deleted_at IS NULL),
|
||||
'[]'::json
|
||||
) AS bindings
|
||||
FROM leaudit_document_types dt
|
||||
LEFT JOIN leaudit_entry_modules em ON em.id = dt.entry_module_id
|
||||
LEFT JOIN leaudit_rule_type_bindings b ON b.doc_type_id = dt.id AND b.deleted_at IS NULL
|
||||
WHERE dt.deleted_at IS NULL
|
||||
GROUP BY dt.id, dt.code, dt.name, dt.description, dt.entry_module_id, dt.sort_order, dt.is_enabled, em.name
|
||||
ORDER BY dt.sort_order ASC, dt.id ASC
|
||||
"""
|
||||
)
|
||||
)
|
||||
).mappings().all()
|
||||
|
||||
for row in rows:
|
||||
top_group_id = await ensure_top_group(session, row)
|
||||
child_group_id = await ensure_default_child_group(session, row, top_group_id)
|
||||
existing_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": child_group_id},
|
||||
)
|
||||
).scalar_one()
|
||||
)
|
||||
bindings = list(row.get("bindings") or [])
|
||||
if existing_binding_count == 0 and bindings:
|
||||
await _replace_group_bindings(session, child_group_id, int(row["id"]), bindings)
|
||||
|
||||
|
||||
async def ensure_group_for_doc_type(session, doc_type_id: int) -> dict[str, int]:
|
||||
"""Ensure the tree nodes for one doc type exist and return their ids."""
|
||||
row = (
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT dt.id, dt.code, dt.name, dt.description, dt.entry_module_id, dt.sort_order, dt.is_enabled, em.name AS entry_module_name
|
||||
FROM leaudit_document_types dt
|
||||
LEFT JOIN leaudit_entry_modules em ON em.id = dt.entry_module_id
|
||||
WHERE dt.deleted_at IS NULL AND dt.id = :doc_type_id
|
||||
LIMIT 1
|
||||
"""
|
||||
),
|
||||
{"doc_type_id": doc_type_id},
|
||||
)
|
||||
).mappings().first()
|
||||
if not row:
|
||||
raise ValueError("文档类型不存在")
|
||||
|
||||
business_root_exists = bool(
|
||||
(
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT 1
|
||||
FROM leaudit_evaluation_point_groups
|
||||
WHERE deleted_at IS NULL
|
||||
AND COALESCE(pid, 0) = 0
|
||||
AND document_type_id IS NULL
|
||||
LIMIT 1
|
||||
"""
|
||||
)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
)
|
||||
if business_root_exists:
|
||||
target_root_code = "root.contract" if int(row.get("entry_module_id") or 0) == 1 else "root.casefile" if int(row.get("entry_module_id") or 0) == 2 else None
|
||||
if target_root_code:
|
||||
root_row = (
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT id
|
||||
FROM leaudit_evaluation_point_groups
|
||||
WHERE deleted_at IS NULL
|
||||
AND COALESCE(pid, 0) = 0
|
||||
AND code = :code
|
||||
LIMIT 1
|
||||
"""
|
||||
),
|
||||
{"code": target_root_code},
|
||||
)
|
||||
).mappings().first()
|
||||
if root_row:
|
||||
child_row = (
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT id
|
||||
FROM leaudit_evaluation_point_groups
|
||||
WHERE deleted_at IS NULL
|
||||
AND pid = :pid
|
||||
AND document_type_id = :doc_type_id
|
||||
ORDER BY sort_order ASC, id ASC
|
||||
LIMIT 1
|
||||
"""
|
||||
),
|
||||
{"pid": int(root_row["id"]), "doc_type_id": doc_type_id},
|
||||
)
|
||||
).mappings().first()
|
||||
if child_row:
|
||||
return {"top_group_id": int(root_row["id"]), "child_group_id": int(child_row["id"])}
|
||||
|
||||
top_group_id = await ensure_top_group(session, row)
|
||||
child_group_id = await ensure_default_child_group(session, row, top_group_id)
|
||||
return {"top_group_id": top_group_id, "child_group_id": child_group_id}
|
||||
|
||||
|
||||
async def sync_group_bindings_from_doc_type(session, doc_type_id: int, rule_set_ids: list[int]) -> int:
|
||||
"""Mirror doc-type bindings into the child rule-group bindings."""
|
||||
await ensure_rule_group_schema(session)
|
||||
ids = [int(item) for item in rule_set_ids if item]
|
||||
ids = list(dict.fromkeys(ids))
|
||||
group_ids = await ensure_group_for_doc_type(session, doc_type_id)
|
||||
child_group_id = group_ids["child_group_id"]
|
||||
|
||||
legacy_bindings = (
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT id, rule_set_id, priority, is_active, note
|
||||
FROM leaudit_rule_type_bindings
|
||||
WHERE doc_type_id = :doc_type_id AND deleted_at IS NULL
|
||||
ORDER BY priority DESC, id ASC
|
||||
"""
|
||||
),
|
||||
{"doc_type_id": doc_type_id},
|
||||
)
|
||||
).mappings().all()
|
||||
binding_map = {int(row["rule_set_id"]): row for row in legacy_bindings}
|
||||
payload: list[dict[str, Any]] = []
|
||||
for index, rule_set_id in enumerate(ids):
|
||||
current = binding_map.get(rule_set_id)
|
||||
payload.append(
|
||||
{
|
||||
"id": current.get("id") if current else None,
|
||||
"rule_set_id": rule_set_id,
|
||||
"priority": int(current.get("priority") if current else 100 - index),
|
||||
"is_active": bool(current.get("is_active") if current else True),
|
||||
"note": current.get("note") if current else None,
|
||||
}
|
||||
)
|
||||
await _replace_group_bindings(session, child_group_id, doc_type_id, payload)
|
||||
return child_group_id
|
||||
|
||||
|
||||
async def sync_doc_type_bindings_from_group(session, group_id: int) -> int | None:
|
||||
"""Mirror one doc type's active child-group bindings into the runtime binding table."""
|
||||
await ensure_rule_group_schema(session)
|
||||
group_row = (
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT g.id, g.document_type_id, dt.code AS document_type_code
|
||||
FROM leaudit_evaluation_point_groups g
|
||||
LEFT JOIN leaudit_document_types dt ON dt.id = g.document_type_id
|
||||
WHERE g.id = :group_id AND g.deleted_at IS NULL
|
||||
LIMIT 1
|
||||
"""
|
||||
),
|
||||
{"group_id": group_id},
|
||||
)
|
||||
).mappings().first()
|
||||
if not group_row or group_row.get("document_type_id") is None:
|
||||
return None
|
||||
|
||||
doc_type_id = int(group_row["document_type_id"])
|
||||
doc_type_code = str(group_row.get("document_type_code") or "") or None
|
||||
|
||||
binding_rows = (
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT
|
||||
rgb.id,
|
||||
rgb.group_id,
|
||||
rgb.rule_set_id,
|
||||
rgb.priority,
|
||||
rgb.is_active,
|
||||
rgb.note,
|
||||
g.sort_order AS group_sort_order
|
||||
FROM leaudit_rule_group_bindings rgb
|
||||
JOIN leaudit_evaluation_point_groups g ON g.id = rgb.group_id
|
||||
WHERE g.document_type_id = :doc_type_id
|
||||
AND g.deleted_at IS NULL
|
||||
AND COALESCE(g.pid, 0) <> 0
|
||||
AND rgb.deleted_at IS NULL
|
||||
AND rgb.is_active = TRUE
|
||||
ORDER BY
|
||||
COALESCE(g.sort_order, 0) ASC,
|
||||
COALESCE(rgb.priority, 0) DESC,
|
||||
rgb.id ASC
|
||||
"""
|
||||
),
|
||||
{"doc_type_id": doc_type_id},
|
||||
)
|
||||
).mappings().all()
|
||||
|
||||
deduped_rows: list[dict[str, Any]] = []
|
||||
seen_rule_set_ids: set[int] = set()
|
||||
for row in binding_rows:
|
||||
rule_set_id = int(row["rule_set_id"])
|
||||
if rule_set_id in seen_rule_set_ids:
|
||||
continue
|
||||
seen_rule_set_ids.add(rule_set_id)
|
||||
deduped_rows.append(dict(row))
|
||||
|
||||
await session.execute(
|
||||
text(
|
||||
"UPDATE leaudit_rule_type_bindings SET deleted_at = NOW(), updated_at = NOW() WHERE doc_type_id = :doc_type_id AND deleted_at IS NULL"
|
||||
),
|
||||
{"doc_type_id": doc_type_id},
|
||||
)
|
||||
|
||||
for index, row in enumerate(deduped_rows):
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO leaudit_rule_type_bindings (
|
||||
doc_type_id,
|
||||
doc_type_code,
|
||||
rule_set_id,
|
||||
binding_mode,
|
||||
priority,
|
||||
is_active,
|
||||
note,
|
||||
created_at,
|
||||
updated_at,
|
||||
region
|
||||
) VALUES (
|
||||
:doc_type_id,
|
||||
:doc_type_code,
|
||||
:rule_set_id,
|
||||
'explicit',
|
||||
:priority,
|
||||
TRUE,
|
||||
:note,
|
||||
NOW(),
|
||||
NOW(),
|
||||
'default'
|
||||
)
|
||||
RETURNING id
|
||||
"""
|
||||
),
|
||||
{
|
||||
"doc_type_id": doc_type_id,
|
||||
"doc_type_code": doc_type_code,
|
||||
"rule_set_id": int(row["rule_set_id"]),
|
||||
"priority": max(0, 1000 - index),
|
||||
"note": row.get("note"),
|
||||
},
|
||||
)
|
||||
return doc_type_id
|
||||
|
||||
|
||||
async def ensure_top_group(session, doc_type_row) -> int:
|
||||
"""Create or reuse the top-level root group for one doc type."""
|
||||
doc_type_id = int(doc_type_row["id"])
|
||||
top_code = str(doc_type_row["code"])
|
||||
top_name = str(doc_type_row["name"])
|
||||
top_sort = int(doc_type_row.get("sort_order") or 0)
|
||||
top = (
|
||||
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
|
||||
LIMIT 1
|
||||
"""
|
||||
),
|
||||
{"doc_type_id": doc_type_id},
|
||||
)
|
||||
).mappings().first()
|
||||
if top:
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
UPDATE leaudit_evaluation_point_groups
|
||||
SET code = :code,
|
||||
name = :name,
|
||||
description = :description,
|
||||
sort_order = :sort_order,
|
||||
is_enabled = :is_enabled,
|
||||
updated_at = NOW()
|
||||
WHERE id = :group_id
|
||||
"""
|
||||
),
|
||||
{
|
||||
"group_id": int(top["id"]),
|
||||
"code": top_code,
|
||||
"name": top_name,
|
||||
"description": doc_type_row.get("description"),
|
||||
"sort_order": top_sort,
|
||||
"is_enabled": bool(doc_type_row.get("is_enabled", True)),
|
||||
},
|
||||
)
|
||||
return int(top["id"])
|
||||
|
||||
legacy = (
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT id
|
||||
FROM leaudit_evaluation_point_groups
|
||||
WHERE deleted_at IS NULL
|
||||
AND document_type_id = :doc_type_id
|
||||
AND LOWER(code) = LOWER(:code)
|
||||
ORDER BY CASE WHEN COALESCE(pid, 0) = 0 THEN 0 ELSE 1 END, id ASC
|
||||
LIMIT 1
|
||||
"""
|
||||
),
|
||||
{"doc_type_id": doc_type_id, "code": top_code},
|
||||
)
|
||||
).mappings().first()
|
||||
if legacy:
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
UPDATE leaudit_evaluation_point_groups
|
||||
SET pid = 0,
|
||||
code = :code,
|
||||
name = :name,
|
||||
description = :description,
|
||||
document_type_id = :doc_type_id,
|
||||
sort_order = :sort_order,
|
||||
is_enabled = :is_enabled,
|
||||
updated_at = NOW()
|
||||
WHERE id = :group_id
|
||||
"""
|
||||
),
|
||||
{
|
||||
"group_id": int(legacy["id"]),
|
||||
"doc_type_id": doc_type_id,
|
||||
"code": top_code,
|
||||
"name": top_name,
|
||||
"description": doc_type_row.get("description"),
|
||||
"sort_order": top_sort,
|
||||
"is_enabled": bool(doc_type_row.get("is_enabled", True)),
|
||||
},
|
||||
)
|
||||
return int(legacy["id"])
|
||||
|
||||
inserted = (
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO leaudit_evaluation_point_groups (
|
||||
pid, code, name, description, document_type_id, sort_order, is_enabled, created_at, updated_at
|
||||
) VALUES (
|
||||
0, :code, :name, :description, :doc_type_id, :sort_order, :is_enabled, NOW(), NOW()
|
||||
)
|
||||
RETURNING id
|
||||
"""
|
||||
),
|
||||
{
|
||||
"doc_type_id": doc_type_id,
|
||||
"code": top_code,
|
||||
"name": top_name,
|
||||
"description": doc_type_row.get("description"),
|
||||
"sort_order": top_sort,
|
||||
"is_enabled": bool(doc_type_row.get("is_enabled", True)),
|
||||
},
|
||||
)
|
||||
).mappings().one()
|
||||
return int(inserted["id"])
|
||||
|
||||
|
||||
async def ensure_default_child_group(session, doc_type_row, top_group_id: int) -> int:
|
||||
"""Ensure there is at least one default second-level group under a doc-type root."""
|
||||
doc_type_id = int(doc_type_row["id"])
|
||||
child = (
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT id, pid
|
||||
FROM leaudit_evaluation_point_groups
|
||||
WHERE deleted_at IS NULL
|
||||
AND pid = :pid
|
||||
AND document_type_id = :doc_type_id
|
||||
AND LOWER(code) = LOWER(:code)
|
||||
LIMIT 1
|
||||
"""
|
||||
),
|
||||
{"pid": top_group_id, "doc_type_id": doc_type_id, "code": f"{doc_type_row['code']}.default"},
|
||||
)
|
||||
).mappings().first()
|
||||
any_children = int(
|
||||
(
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT COUNT(*)
|
||||
FROM leaudit_evaluation_point_groups
|
||||
WHERE deleted_at IS NULL
|
||||
AND pid = :pid
|
||||
"""
|
||||
),
|
||||
{"pid": top_group_id},
|
||||
)
|
||||
).scalar_one()
|
||||
)
|
||||
if child:
|
||||
return int(child["id"])
|
||||
if any_children > 0:
|
||||
adopted = (
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT id
|
||||
FROM leaudit_evaluation_point_groups
|
||||
WHERE deleted_at IS NULL
|
||||
AND pid = :pid
|
||||
AND document_type_id = :doc_type_id
|
||||
ORDER BY sort_order ASC, id ASC
|
||||
LIMIT 1
|
||||
"""
|
||||
),
|
||||
{"pid": top_group_id, "doc_type_id": doc_type_id},
|
||||
)
|
||||
).mappings().first()
|
||||
if adopted:
|
||||
return int(adopted["id"])
|
||||
|
||||
payload = {
|
||||
"pid": top_group_id,
|
||||
"code": f"{doc_type_row['code']}.default",
|
||||
"name": "通用",
|
||||
"description": f"{doc_type_row['name']}默认子类型",
|
||||
"document_type_id": doc_type_id,
|
||||
"sort_order": int(doc_type_row.get("sort_order") or 0),
|
||||
"is_enabled": bool(doc_type_row.get("is_enabled", True)),
|
||||
}
|
||||
inserted = (
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO leaudit_evaluation_point_groups (
|
||||
pid, code, name, description, document_type_id, sort_order, is_enabled, created_at, updated_at
|
||||
) VALUES (
|
||||
:pid, :code, :name, :description, :document_type_id, :sort_order, :is_enabled, NOW(), NOW()
|
||||
)
|
||||
RETURNING id
|
||||
"""
|
||||
),
|
||||
payload,
|
||||
)
|
||||
).mappings().one()
|
||||
default_child_id = int(inserted["id"])
|
||||
await _move_root_bindings_to_default_child(session, top_group_id, default_child_id)
|
||||
return int(inserted["id"])
|
||||
|
||||
|
||||
async def _move_root_bindings_to_default_child(session, root_group_id: int, child_group_id: int) -> None:
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
UPDATE leaudit_rule_group_bindings
|
||||
SET group_id = :child_group_id,
|
||||
updated_at = NOW()
|
||||
WHERE group_id = :root_group_id
|
||||
AND deleted_at IS NULL
|
||||
"""
|
||||
),
|
||||
{"root_group_id": root_group_id, "child_group_id": child_group_id},
|
||||
)
|
||||
|
||||
|
||||
async def _replace_group_bindings(session, child_group_id: int, doc_type_id: int, bindings: list[dict[str, Any]]) -> None:
|
||||
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": child_group_id},
|
||||
)
|
||||
for item in bindings:
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO leaudit_rule_group_bindings (
|
||||
group_id,
|
||||
rule_set_id,
|
||||
rule_type_binding_id,
|
||||
priority,
|
||||
is_active,
|
||||
note,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
:group_id,
|
||||
:rule_set_id,
|
||||
:rule_type_binding_id,
|
||||
:priority,
|
||||
:is_active,
|
||||
:note,
|
||||
NOW(),
|
||||
NOW()
|
||||
)
|
||||
"""
|
||||
),
|
||||
{
|
||||
"group_id": child_group_id,
|
||||
"rule_set_id": int(item["rule_set_id"]),
|
||||
"rule_type_binding_id": int(item["id"]) if item.get("id") else None,
|
||||
"priority": int(item.get("priority") or 0),
|
||||
"is_active": bool(item.get("is_active", True)),
|
||||
"note": item.get("note"),
|
||||
},
|
||||
)
|
||||
await sync_doc_type_bindings_from_group(session, child_group_id)
|
||||
@@ -39,29 +39,85 @@ class RuleServiceImpl(IRuleService):
|
||||
text(
|
||||
"""
|
||||
SELECT
|
||||
id,
|
||||
rule_type,
|
||||
rule_name,
|
||||
domain_type,
|
||||
current_version_id,
|
||||
status
|
||||
FROM leaudit_rule_sets
|
||||
WHERE deleted_at IS NULL
|
||||
ORDER BY id DESC
|
||||
rs.id,
|
||||
rs.rule_type,
|
||||
rs.rule_name,
|
||||
rs.domain_type,
|
||||
rs.current_version_id,
|
||||
current_rv.id AS usable_current_version_id,
|
||||
fallback_rv.id AS fallback_version_id,
|
||||
CASE
|
||||
WHEN current_rv.id IS NOT NULL OR fallback_rv.id IS NOT NULL THEN TRUE
|
||||
ELSE FALSE
|
||||
END AS has_usable_version,
|
||||
rs.status
|
||||
FROM leaudit_rule_sets rs
|
||||
LEFT JOIN leaudit_rule_versions current_rv
|
||||
ON current_rv.id = rs.current_version_id
|
||||
AND current_rv.status IN ('published', 'rollback')
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT rv.id
|
||||
FROM leaudit_rule_versions rv
|
||||
WHERE rv.rule_set_id = rs.id
|
||||
AND rv.status IN ('published', 'rollback')
|
||||
ORDER BY rv.version_seq DESC, rv.id DESC
|
||||
LIMIT 1
|
||||
) fallback_rv ON TRUE
|
||||
WHERE rs.deleted_at IS NULL
|
||||
ORDER BY rs.id DESC
|
||||
"""
|
||||
)
|
||||
)
|
||||
return [
|
||||
RuleSetVO(
|
||||
id=int(Row["id"]),
|
||||
ruleType=Row["rule_type"],
|
||||
ruleName=Row["rule_name"],
|
||||
domainType=Row["domain_type"],
|
||||
currentVersionId=Row["current_version_id"],
|
||||
status=Row["status"],
|
||||
)
|
||||
for Row in Result.mappings().all()
|
||||
]
|
||||
rows = Result.mappings().all()
|
||||
|
||||
usable_counts: dict[int, int] = {}
|
||||
for row in rows:
|
||||
usable_version_id = row["usable_current_version_id"] or row["fallback_version_id"]
|
||||
if usable_version_id is not None and int(usable_version_id) not in usable_counts:
|
||||
usable_counts[int(usable_version_id)] = await self._GetRuleCountByVersionId(int(usable_version_id))
|
||||
|
||||
return [
|
||||
RuleSetVO(
|
||||
id=int(Row["id"]),
|
||||
ruleType=Row["rule_type"],
|
||||
ruleName=Row["rule_name"],
|
||||
domainType=Row["domain_type"],
|
||||
currentVersionId=Row["current_version_id"],
|
||||
fallbackVersionId=Row["fallback_version_id"],
|
||||
hasUsableVersion=bool(Row["has_usable_version"]),
|
||||
usableRuleCount=usable_counts.get(int(Row["usable_current_version_id"] or Row["fallback_version_id"]), 0)
|
||||
if (Row["usable_current_version_id"] or Row["fallback_version_id"]) is not None
|
||||
else 0,
|
||||
status=Row["status"],
|
||||
)
|
||||
for Row in rows
|
||||
]
|
||||
|
||||
async def _GetRuleCountByVersionId(self, VersionId: int) -> int:
|
||||
"""读取指定可用规则版本的规则数。"""
|
||||
async with GetAsyncSession() as Session:
|
||||
Result = await Session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT oss_url
|
||||
FROM leaudit_rule_versions
|
||||
WHERE id = :version_id
|
||||
LIMIT 1
|
||||
"""
|
||||
),
|
||||
{"version_id": VersionId},
|
||||
)
|
||||
Row = Result.mappings().first()
|
||||
|
||||
if not Row or not Row["oss_url"]:
|
||||
return 0
|
||||
|
||||
try:
|
||||
yaml_text = (await self.OssService.DownloadBytes(Row["oss_url"])).decode("utf-8")
|
||||
validation = self.Validator.ValidateYaml(yaml_text)
|
||||
return int(validation.ruleCount or 0)
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
async def GetVersions(self, RuleType: str) -> list[RuleVersionVO]:
|
||||
"""获取规则集的所有版本。"""
|
||||
|
||||
Reference in New Issue
Block a user