feat: add rule draft permission flow
This commit is contained in:
@@ -11,6 +11,7 @@ from fastapi_modules.fastapi_leaudit.domian.Dto.evaluationPointGroupDto import (
|
||||
EvaluationPointGroupBindingCreateDTO,
|
||||
EvaluationPointGroupBindingUpdateDTO,
|
||||
EvaluationPointGroupCreateDTO,
|
||||
EvaluationPointGroupRuleDraftCreateDTO,
|
||||
EvaluationPointGroupRebindDTO,
|
||||
EvaluationPointGroupUpdateDTO,
|
||||
)
|
||||
@@ -160,6 +161,25 @@ class EvaluationPointGroupController(BaseController):
|
||||
await self.GroupService.DeleteBinding(BindingId)
|
||||
return JSONResponse(status_code=200, content={"success": True})
|
||||
|
||||
@self.router.get("/{GroupId}/rule-template")
|
||||
async def GetEvaluationPointGroupRuleTemplate(GroupId: int, payload: dict = Depends(verify_access_token)):
|
||||
if not await self._check_permission(int(payload["user_id"]), ["evaluation_group:list:read", "rules:list:read"]):
|
||||
return JSONResponse(status_code=403, content={"code": 403, "msg": "当前用户没有查看规则模板权限", "data": None})
|
||||
data = await self.GroupService.GetRuleTemplate(GroupId)
|
||||
return JSONResponse(status_code=200, content=data.model_dump())
|
||||
|
||||
@self.router.post("/{GroupId}/rule-drafts")
|
||||
async def CreateEvaluationPointGroupRuleDraft(
|
||||
GroupId: int,
|
||||
body: EvaluationPointGroupRuleDraftCreateDTO,
|
||||
payload: dict = Depends(verify_access_token),
|
||||
):
|
||||
if not await self._check_permission(int(payload["user_id"]), ["evaluation_group:update:write", "rules:create:write"]):
|
||||
return JSONResponse(status_code=403, content={"code": 403, "msg": "当前用户没有保存规则草稿权限", "data": None})
|
||||
effective_body = body.model_copy(update={"editor_user_id": body.editor_user_id or int(payload["user_id"])})
|
||||
data = await self.GroupService.CreateRuleDraft(GroupId, effective_body)
|
||||
return JSONResponse(status_code=200, content=data.model_dump())
|
||||
|
||||
async def _check_permission(self, user_id: int, permission_keys: list[str]) -> bool:
|
||||
for permission_key in permission_keys:
|
||||
if await self.PermissionService.CheckPermission(user_id, permission_key):
|
||||
|
||||
Executable → Regular
+11
-1
@@ -1,4 +1,4 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class EvaluationPointGroupCreateDTO(BaseModel):
|
||||
@@ -61,3 +61,13 @@ class EvaluationPointGroupBindingUpdateDTO(BaseModel):
|
||||
priority: int | None = Field(None, description="优先级")
|
||||
is_active: bool | None = Field(None, description="是否启用")
|
||||
note: str | None = Field(None, description="备注")
|
||||
|
||||
|
||||
class EvaluationPointGroupRuleDraftCreateDTO(BaseModel):
|
||||
"""二级分组下新建规则 YAML 草稿请求。"""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
yaml_text: str = Field(..., min_length=1, alias="yamlText", description="完整规则 YAML 正文")
|
||||
change_note: str | None = Field(None, alias="changeNote", description="版本变更说明")
|
||||
editor_user_id: int | None = Field(None, alias="editorUserId", description="编辑者用户ID")
|
||||
|
||||
Executable → Regular
+45
@@ -1,5 +1,7 @@
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from fastapi_modules.fastapi_leaudit.domian.vo.ruleVo import RuleVersionVO
|
||||
|
||||
|
||||
class RuleGroupBindingVO(BaseModel):
|
||||
"""二级分组下的规则集绑定。"""
|
||||
@@ -96,4 +98,47 @@ class EvaluationPointGroupBatchDeleteVO(BaseModel):
|
||||
message: str = Field(..., description="结果消息")
|
||||
|
||||
|
||||
class EvaluationPointGroupRuleTemplateContextVO(BaseModel):
|
||||
"""二级分组生成规则模板所需上下文。"""
|
||||
|
||||
group_id: int = Field(..., description="当前二级分组ID")
|
||||
group_code: str = Field(..., description="当前二级分组编码")
|
||||
group_name: str = Field(..., description="当前二级分组名称")
|
||||
parent_group_id: int = Field(..., description="所属一级分组ID")
|
||||
parent_group_code: str = Field(..., description="所属一级分组编码")
|
||||
parent_group_name: str = Field(..., description="所属一级分组名称")
|
||||
document_type_id: int | None = Field(None, description="关联文档类型ID")
|
||||
document_type_code: str | None = Field(None, description="关联文档类型编码")
|
||||
document_type_name: str | None = Field(None, description="关联文档类型名称")
|
||||
entry_module_id: int | None = Field(None, description="入口模块ID")
|
||||
entry_module_name: str | None = Field(None, description="入口模块名称")
|
||||
|
||||
|
||||
class EvaluationPointGroupRuleTemplateVO(BaseModel):
|
||||
"""二级分组规则模板响应。"""
|
||||
|
||||
context: EvaluationPointGroupRuleTemplateContextVO = Field(..., description="二级分组上下文")
|
||||
ruleType: str = Field(..., description="从分组上下文推导出的规则类型编码")
|
||||
ruleName: str = Field(..., description="规则集名称")
|
||||
nextVersionNo: str = Field(..., description="建议创建的下一个版本号")
|
||||
ossPreviewKey: str = Field(..., description="该版本预计写入的 OSS key")
|
||||
yamlTemplate: str = Field(..., description="可直接编辑的完整 YAML 模板")
|
||||
existingRuleSetId: int | None = Field(None, description="已存在的规则集ID")
|
||||
existingBindingId: int | None = Field(None, description="当前分组已存在的绑定ID")
|
||||
|
||||
|
||||
class EvaluationPointGroupRuleDraftVO(BaseModel):
|
||||
"""二级分组规则草稿创建响应。"""
|
||||
|
||||
packId: int = Field(..., description="规则配置 pack ID,当前等于二级分组ID")
|
||||
groupId: int = Field(..., description="二级分组ID")
|
||||
ruleName: str = Field(..., description="规则名称")
|
||||
ruleType: str = Field(..., description="规则类型编码")
|
||||
ruleSetId: int = Field(..., description="所属规则集ID")
|
||||
ossKey: str = Field(..., description="草稿写入的 OSS key")
|
||||
version: RuleVersionVO = Field(..., description="刚创建的规则版本")
|
||||
binding: RuleGroupBindingVO = Field(..., description="当前二级分组与规则集的绑定信息")
|
||||
autoBound: bool = Field(..., description="本次是否自动新增了绑定")
|
||||
|
||||
|
||||
EvaluationPointGroupVO.model_rebuild()
|
||||
|
||||
@@ -6,6 +6,7 @@ from fastapi_modules.fastapi_leaudit.domian.Dto.evaluationPointGroupDto import (
|
||||
EvaluationPointGroupBindingCreateDTO,
|
||||
EvaluationPointGroupBindingUpdateDTO,
|
||||
EvaluationPointGroupCreateDTO,
|
||||
EvaluationPointGroupRuleDraftCreateDTO,
|
||||
EvaluationPointGroupRebindDTO,
|
||||
EvaluationPointGroupUpdateDTO,
|
||||
)
|
||||
@@ -14,6 +15,8 @@ from fastapi_modules.fastapi_leaudit.domian.vo.evaluationPointGroupVo import (
|
||||
EvaluationPointGroupBatchStatusVO,
|
||||
EvaluationPointGroupDeleteVO,
|
||||
EvaluationPointGroupListVO,
|
||||
EvaluationPointGroupRuleDraftVO,
|
||||
EvaluationPointGroupRuleTemplateVO,
|
||||
EvaluationPointGroupRebindVO,
|
||||
EvaluationPointGroupVO,
|
||||
RuleGroupBindingVO,
|
||||
@@ -91,3 +94,11 @@ class IEvaluationPointGroupService(ABC):
|
||||
@abstractmethod
|
||||
async def DeleteBinding(self, BindingId: int) -> None:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def GetRuleTemplate(self, GroupId: int) -> EvaluationPointGroupRuleTemplateVO:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def CreateRuleDraft(self, GroupId: int, Body: EvaluationPointGroupRuleDraftCreateDTO) -> EvaluationPointGroupRuleDraftVO:
|
||||
...
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -158,6 +158,14 @@ 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_group:list:read", "display_name": "评查点分组列表", "module": "evaluation_group", "resource": "list", "action": "read", "api_method": "GET", "api_path": "/api/v3/evaluation-point-groups", "route_path": "/rule-groups"},
|
||||
{"permission_key": "evaluation_group:create:write", "display_name": "创建评查点分组", "module": "evaluation_group", "resource": "create", "action": "write", "api_method": "POST", "api_path": "/api/v3/evaluation-point-groups", "route_path": "/rule-groups"},
|
||||
{"permission_key": "evaluation_group:update:write", "display_name": "更新评查点分组与绑定", "module": "evaluation_group", "resource": "update", "action": "write", "api_method": "PUT", "api_path": "/api/v3/evaluation-point-groups/{id}", "route_path": "/rule-groups"},
|
||||
{"permission_key": "evaluation_group:batch:write", "display_name": "批量维护评查点分组", "module": "evaluation_group", "resource": "batch", "action": "write", "api_method": "PATCH", "api_path": "/api/v3/evaluation-point-groups/batch/status", "route_path": "/rule-groups"},
|
||||
{"permission_key": "evaluation_group:delete:delete", "display_name": "删除评查点分组", "module": "evaluation_group", "resource": "delete", "action": "delete", "api_method": "DELETE", "api_path": "/api/v3/evaluation-point-groups/{id}", "route_path": "/rule-groups"},
|
||||
{"permission_key": "rules:list:read", "display_name": "规则配置列表", "module": "rules", "resource": "list", "action": "read", "api_method": "GET", "api_path": "/api/v3/rule-config-packs", "route_path": "/rules"},
|
||||
{"permission_key": "rules:content:read", "display_name": "规则 YAML 内容", "module": "rules", "resource": "content", "action": "read", "api_method": "GET", "api_path": "/api/v3/rule-config-packs/{id}", "route_path": "/rules"},
|
||||
{"permission_key": "rules:create:write", "display_name": "创建规则草稿", "module": "rules", "resource": "create", "action": "write", "api_method": "POST", "api_path": "/api/v3/evaluation-point-groups/{id}/rule-drafts", "route_path": "/rules"},
|
||||
{"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"},
|
||||
|
||||
@@ -127,6 +127,12 @@ class RuleConfigServiceImpl(IRuleConfigService):
|
||||
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)
|
||||
latest_version_id = await self._load_latest_version_id(rule_set_id)
|
||||
latest_version_seq = await self._load_version_seq_by_id(latest_version_id)
|
||||
resolved_version_seq = await self._load_version_seq_by_id(resolved_version_id)
|
||||
if latest_version_id is not None and (resolved_version_id is None or latest_version_seq > resolved_version_seq):
|
||||
# 规则详情页优先读取最新草稿;上传运行仍以 current/fallback 的可用版本为准。
|
||||
resolved_version_id = latest_version_id
|
||||
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"
|
||||
@@ -214,6 +220,44 @@ class RuleConfigServiceImpl(IRuleConfigService):
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
async def _load_latest_version_id(self, rule_set_id: int) -> int | None:
|
||||
"""在没有可用发布版本时,退回读取最新草稿版本。"""
|
||||
async with GetAsyncSession() as session:
|
||||
row = (
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT id
|
||||
FROM leaudit_rule_versions
|
||||
WHERE rule_set_id = :rule_set_id
|
||||
ORDER BY version_seq DESC, id DESC
|
||||
LIMIT 1
|
||||
"""
|
||||
),
|
||||
{"rule_set_id": rule_set_id},
|
||||
)
|
||||
).mappings().first()
|
||||
return self._to_int(row.get("id")) if row else None
|
||||
|
||||
async def _load_version_seq_by_id(self, version_id: int | None) -> int:
|
||||
if version_id is None:
|
||||
return -1
|
||||
async with GetAsyncSession() as session:
|
||||
row = (
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT version_seq
|
||||
FROM leaudit_rule_versions
|
||||
WHERE id = :version_id
|
||||
LIMIT 1
|
||||
"""
|
||||
),
|
||||
{"version_id": version_id},
|
||||
)
|
||||
).mappings().first()
|
||||
return int(row.get("version_seq") or -1) if row else -1
|
||||
|
||||
def _to_int(self, value) -> int | None:
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user