feat: add rule draft permission flow
This commit is contained in:
@@ -7,6 +7,7 @@ from typing import Any
|
||||
|
||||
from sqlalchemy import bindparam, text
|
||||
|
||||
from fastapi_common.fastapi_common_storage.oss_path_utils import OssPathUtils
|
||||
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
|
||||
@@ -16,6 +17,7 @@ from fastapi_modules.fastapi_leaudit.domian.Dto.evaluationPointGroupDto import (
|
||||
EvaluationPointGroupBindingCreateDTO,
|
||||
EvaluationPointGroupBindingUpdateDTO,
|
||||
EvaluationPointGroupCreateDTO,
|
||||
EvaluationPointGroupRuleDraftCreateDTO,
|
||||
EvaluationPointGroupRebindDTO,
|
||||
EvaluationPointGroupUpdateDTO,
|
||||
)
|
||||
@@ -24,6 +26,9 @@ from fastapi_modules.fastapi_leaudit.domian.vo.evaluationPointGroupVo import (
|
||||
EvaluationPointGroupBatchStatusVO,
|
||||
EvaluationPointGroupDeleteVO,
|
||||
EvaluationPointGroupListVO,
|
||||
EvaluationPointGroupRuleDraftVO,
|
||||
EvaluationPointGroupRuleTemplateContextVO,
|
||||
EvaluationPointGroupRuleTemplateVO,
|
||||
EvaluationPointGroupRebindVO,
|
||||
EvaluationPointGroupVO,
|
||||
RuleGroupBindingVO,
|
||||
@@ -533,6 +538,73 @@ class EvaluationPointGroupServiceImpl(IEvaluationPointGroupService):
|
||||
await sync_doc_type_bindings_from_group(session, int(current["group_id"]))
|
||||
await session.commit()
|
||||
|
||||
async def GetRuleTemplate(self, GroupId: int) -> EvaluationPointGroupRuleTemplateVO:
|
||||
async with GetAsyncSession() as session:
|
||||
await self._ensure_ready(session)
|
||||
context = await self._load_rule_context(session, GroupId)
|
||||
template = self._build_rule_yaml_template(context)
|
||||
existing_binding_id = await self._get_group_binding_id_for_rule_type(session, GroupId, context["rule_type"])
|
||||
return EvaluationPointGroupRuleTemplateVO(
|
||||
context=EvaluationPointGroupRuleTemplateContextVO(
|
||||
group_id=context["group_id"],
|
||||
group_code=context["group_code"],
|
||||
group_name=context["group_name"],
|
||||
parent_group_id=context["parent_group_id"],
|
||||
parent_group_code=context["parent_group_code"],
|
||||
parent_group_name=context["parent_group_name"],
|
||||
document_type_id=context["document_type_id"],
|
||||
document_type_code=context["document_type_code"],
|
||||
document_type_name=context["document_type_name"],
|
||||
entry_module_id=context["entry_module_id"],
|
||||
entry_module_name=context["entry_module_name"],
|
||||
),
|
||||
ruleType=context["rule_type"],
|
||||
ruleName=context["rule_name"],
|
||||
nextVersionNo=context["next_version_no"],
|
||||
ossPreviewKey=context["oss_preview_key"],
|
||||
yamlTemplate=template,
|
||||
existingRuleSetId=context["existing_rule_set_id"],
|
||||
existingBindingId=existing_binding_id,
|
||||
)
|
||||
|
||||
async def CreateRuleDraft(self, GroupId: int, Body: EvaluationPointGroupRuleDraftCreateDTO) -> EvaluationPointGroupRuleDraftVO:
|
||||
async with GetAsyncSession() as session:
|
||||
await self._ensure_ready(session)
|
||||
context = await self._load_rule_context(session, GroupId)
|
||||
yaml_text = Body.yaml_text.strip()
|
||||
if not yaml_text:
|
||||
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "YAML 内容不能为空")
|
||||
|
||||
created_version = await self.RuleService.CreateVersion(
|
||||
RuleType=context["rule_type"],
|
||||
YamlText=yaml_text,
|
||||
ChangeNote=Body.change_note.strip() if Body.change_note else None,
|
||||
EditorUserId=Body.editor_user_id,
|
||||
)
|
||||
|
||||
async with GetAsyncSession() as session:
|
||||
await self._ensure_ready(session)
|
||||
rule_set_id = await self._get_rule_set_id_by_type(session, context["rule_type"])
|
||||
if rule_set_id is None:
|
||||
raise LeauditException(StatusCodeEnum.HTTP_500_INTERNAL_SERVER_ERROR, "规则版本已创建,但未找到对应规则集")
|
||||
binding_id, auto_bound = await self._ensure_group_binding_for_rule_set(session, GroupId, rule_set_id)
|
||||
await sync_doc_type_bindings_from_group(session, GroupId)
|
||||
await session.commit()
|
||||
binding_row = await self._get_binding_row(session, binding_id)
|
||||
binding_vo = await self._build_binding_vo(binding_row)
|
||||
|
||||
return EvaluationPointGroupRuleDraftVO(
|
||||
packId=GroupId,
|
||||
groupId=GroupId,
|
||||
ruleName=context["rule_name"],
|
||||
ruleType=context["rule_type"],
|
||||
ruleSetId=rule_set_id,
|
||||
ossKey=OssPathUtils.BuildRuleYamlKey(context["rule_type"], created_version.versionNo),
|
||||
version=created_version,
|
||||
binding=binding_vo,
|
||||
autoBound=auto_bound,
|
||||
)
|
||||
|
||||
async def _ensure_ready(self, session) -> None:
|
||||
self._rule_set_meta_cache = None
|
||||
await ensure_rule_group_schema(session)
|
||||
@@ -550,7 +622,11 @@ class EvaluationPointGroupServiceImpl(IEvaluationPointGroupService):
|
||||
g.code,
|
||||
g.description,
|
||||
g.document_type_id,
|
||||
dt.code AS document_type_code,
|
||||
dt.name AS document_type_name,
|
||||
parent.id AS root_group_id,
|
||||
parent.name AS root_group_name,
|
||||
parent.code AS root_group_code,
|
||||
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,
|
||||
@@ -763,6 +839,186 @@ class EvaluationPointGroupServiceImpl(IEvaluationPointGroupService):
|
||||
if exists is None:
|
||||
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "规则集不存在")
|
||||
|
||||
async def _load_rule_context(self, session, group_id: int) -> dict[str, Any]:
|
||||
row = await self._get_group_row(session, group_id)
|
||||
if self._normalize_pid(row["pid"]) == 0:
|
||||
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "请先选择二级分组,再创建规则 YAML")
|
||||
|
||||
rule_type = self._resolve_rule_type(row)
|
||||
if not rule_type:
|
||||
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "当前二级分组缺少可推导的规则类型编码,请先补充分组编码或文档类型编码")
|
||||
|
||||
rule_name = str(row.get("document_type_name") or row.get("name") or "").strip()
|
||||
if not rule_name:
|
||||
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "当前二级分组缺少规则名称,请先补充分组名称")
|
||||
|
||||
next_version_no = await self._get_next_rule_version_no(session, rule_type)
|
||||
return {
|
||||
"group_id": int(row["id"]),
|
||||
"group_code": str(row.get("code") or "").strip(),
|
||||
"group_name": str(row.get("name") or ""),
|
||||
"parent_group_id": int(row.get("root_group_id") or 0),
|
||||
"parent_group_code": str(row.get("root_group_code") or "").strip(),
|
||||
"parent_group_name": str(row.get("root_group_name") or row.get("entry_module_name") or ""),
|
||||
"document_type_id": int(row["document_type_id"]) if row.get("document_type_id") is not None else None,
|
||||
"document_type_code": str(row.get("document_type_code") or "").strip() or None,
|
||||
"document_type_name": str(row.get("document_type_name") or "").strip() or None,
|
||||
"entry_module_id": int(row["entry_module_id"]) if row.get("entry_module_id") is not None else None,
|
||||
"entry_module_name": str(row.get("entry_module_name") or "").strip() or None,
|
||||
"rule_type": rule_type,
|
||||
"rule_name": rule_name,
|
||||
"next_version_no": next_version_no,
|
||||
"oss_preview_key": OssPathUtils.BuildRuleYamlKey(rule_type, next_version_no),
|
||||
"existing_rule_set_id": await self._get_rule_set_id_by_type(session, rule_type),
|
||||
}
|
||||
|
||||
def _resolve_rule_type(self, row: Any) -> str:
|
||||
doc_type_code = str(row.get("document_type_code") or "").strip()
|
||||
if doc_type_code:
|
||||
return self._normalize_rule_code(doc_type_code)
|
||||
child_code = str(row.get("code") or "").strip()
|
||||
root_code = str(row.get("root_group_code") or "").strip()
|
||||
if child_code and root_code and child_code != root_code and not child_code.startswith(f"{root_code}."):
|
||||
return self._normalize_rule_code(f"{root_code}.{child_code}")
|
||||
if child_code:
|
||||
return self._normalize_rule_code(child_code)
|
||||
group_name = str(row.get("name") or "").strip()
|
||||
if root_code and group_name:
|
||||
return self._normalize_rule_code(f"{root_code}.{group_name}")
|
||||
if group_name:
|
||||
return self._normalize_rule_code(group_name)
|
||||
return ""
|
||||
|
||||
def _normalize_rule_code(self, value: str) -> str:
|
||||
normalized = ".".join(segment.strip() for segment in value.strip().split(".") if segment.strip())
|
||||
if not normalized:
|
||||
return ""
|
||||
return normalized.replace("/", "_").replace("\\", "_").replace(" ", "_")
|
||||
|
||||
async def _get_next_rule_version_no(self, session, rule_type: str) -> str:
|
||||
row = (
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT COALESCE(MAX(rv.version_seq), 0) AS max_seq
|
||||
FROM leaudit_rule_versions rv
|
||||
JOIN leaudit_rule_sets rs ON rs.id = rv.rule_set_id
|
||||
WHERE rs.rule_type = :rule_type
|
||||
AND rs.deleted_at IS NULL
|
||||
"""
|
||||
),
|
||||
{"rule_type": rule_type},
|
||||
)
|
||||
).mappings().first()
|
||||
next_seq = int((row or {}).get("max_seq") or 0) + 1
|
||||
return f"v{next_seq}"
|
||||
|
||||
def _build_rule_yaml_template(self, context: dict[str, Any]) -> str:
|
||||
keywords = [
|
||||
item
|
||||
for item in dict.fromkeys(
|
||||
[
|
||||
context.get("parent_group_name"),
|
||||
context.get("group_name"),
|
||||
context.get("document_type_name"),
|
||||
context.get("entry_module_name"),
|
||||
]
|
||||
)
|
||||
if item
|
||||
]
|
||||
keyword_lines = "\n".join(f" - {item}" for item in keywords) or " - 待补充"
|
||||
description = (
|
||||
f"{context['rule_name']} 规则模板。"
|
||||
f" 入口模块:{context.get('entry_module_name') or '未配置'};"
|
||||
f" 一级分组:{context.get('parent_group_name') or '未配置'};"
|
||||
f" 二级分组:{context.get('group_name') or '未配置'}。"
|
||||
)
|
||||
parent_type = context.get("parent_group_name") or context.get("group_name") or context["rule_type"]
|
||||
return (
|
||||
"metadata:\n"
|
||||
f" type_id: {context['rule_type']}\n"
|
||||
f" name: {context['rule_name']}\n"
|
||||
f" version: {context['next_version_no']}\n"
|
||||
f" last_updated: '{datetime.now().date().isoformat()}'\n"
|
||||
f" parent: {parent_type}\n"
|
||||
" classification_keywords:\n"
|
||||
f"{keyword_lines}\n"
|
||||
" description: >\n"
|
||||
f" {description}\n\n"
|
||||
"sub_documents: []\n"
|
||||
"rules: []\n"
|
||||
)
|
||||
|
||||
async def _get_group_binding_id_for_rule_type(self, session, group_id: int, rule_type: str) -> int | None:
|
||||
rule_set_id = await self._get_rule_set_id_by_type(session, rule_type)
|
||||
if rule_set_id is None:
|
||||
return None
|
||||
row = (
|
||||
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": group_id, "rule_set_id": rule_set_id},
|
||||
)
|
||||
).mappings().first()
|
||||
return int(row["id"]) if row else None
|
||||
|
||||
async def _get_rule_set_id_by_type(self, session, rule_type: str) -> int | None:
|
||||
row = (
|
||||
await session.execute(
|
||||
text("SELECT id FROM leaudit_rule_sets WHERE rule_type = :rule_type AND deleted_at IS NULL LIMIT 1"),
|
||||
{"rule_type": rule_type},
|
||||
)
|
||||
).mappings().first()
|
||||
return int(row["id"]) if row else None
|
||||
|
||||
async def _ensure_group_binding_for_rule_set(self, session, group_id: int, rule_set_id: int) -> tuple[int, bool]:
|
||||
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": group_id, "rule_set_id": rule_set_id},
|
||||
)
|
||||
).mappings().first()
|
||||
if existing:
|
||||
return int(existing["id"]), False
|
||||
|
||||
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, 100, TRUE, :note, NOW(), NOW()
|
||||
)
|
||||
RETURNING id
|
||||
"""
|
||||
),
|
||||
{
|
||||
"group_id": group_id,
|
||||
"rule_set_id": rule_set_id,
|
||||
"note": "由二级分组新建规则 YAML 时自动补绑",
|
||||
},
|
||||
)
|
||||
).mappings().one()
|
||||
return int(row["id"]), True
|
||||
|
||||
def _normalize_create_payload(self, body: EvaluationPointGroupCreateDTO) -> dict[str, Any]:
|
||||
name = body.name.strip()
|
||||
code = body.code.strip()
|
||||
|
||||
Reference in New Issue
Block a user