From f9de903accd9fd312470cf0fc78901776fda33b5 Mon Sep 17 00:00:00 2001 From: wren <“porlong@qq.com”> Date: Wed, 6 May 2026 20:06:41 +0800 Subject: [PATCH] feat: add rule draft permission flow --- .../evaluationPointGroupController.py | 20 ++ .../domian/Dto/evaluationPointGroupDto.py | 12 +- .../domian/vo/evaluationPointGroupVo.py | 45 +++ .../services/evaluationPointGroupService.py | 11 + .../impl/evaluationPointGroupServiceImpl.py | 256 ++++++++++++++++++ .../services/impl/rbacAdminServiceImpl.py | 8 + .../services/impl/ruleConfigServiceImpl.py | 44 +++ scripts/user_rbac_seed.sql | 30 +- 8 files changed, 412 insertions(+), 14 deletions(-) mode change 100755 => 100644 fastapi_modules/fastapi_leaudit/domian/Dto/evaluationPointGroupDto.py mode change 100755 => 100644 fastapi_modules/fastapi_leaudit/domian/vo/evaluationPointGroupVo.py diff --git a/fastapi_modules/fastapi_leaudit/controllers/evaluationPointGroupController.py b/fastapi_modules/fastapi_leaudit/controllers/evaluationPointGroupController.py index 220f91b..2521d41 100644 --- a/fastapi_modules/fastapi_leaudit/controllers/evaluationPointGroupController.py +++ b/fastapi_modules/fastapi_leaudit/controllers/evaluationPointGroupController.py @@ -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): diff --git a/fastapi_modules/fastapi_leaudit/domian/Dto/evaluationPointGroupDto.py b/fastapi_modules/fastapi_leaudit/domian/Dto/evaluationPointGroupDto.py old mode 100755 new mode 100644 index 3e24ba4..1808b7f --- a/fastapi_modules/fastapi_leaudit/domian/Dto/evaluationPointGroupDto.py +++ b/fastapi_modules/fastapi_leaudit/domian/Dto/evaluationPointGroupDto.py @@ -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") diff --git a/fastapi_modules/fastapi_leaudit/domian/vo/evaluationPointGroupVo.py b/fastapi_modules/fastapi_leaudit/domian/vo/evaluationPointGroupVo.py old mode 100755 new mode 100644 index e8fd719..39c9ceb --- a/fastapi_modules/fastapi_leaudit/domian/vo/evaluationPointGroupVo.py +++ b/fastapi_modules/fastapi_leaudit/domian/vo/evaluationPointGroupVo.py @@ -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() diff --git a/fastapi_modules/fastapi_leaudit/services/evaluationPointGroupService.py b/fastapi_modules/fastapi_leaudit/services/evaluationPointGroupService.py index db5ca07..c6839a7 100755 --- a/fastapi_modules/fastapi_leaudit/services/evaluationPointGroupService.py +++ b/fastapi_modules/fastapi_leaudit/services/evaluationPointGroupService.py @@ -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: + ... diff --git a/fastapi_modules/fastapi_leaudit/services/impl/evaluationPointGroupServiceImpl.py b/fastapi_modules/fastapi_leaudit/services/impl/evaluationPointGroupServiceImpl.py index 3ee03c8..7904e2c 100644 --- a/fastapi_modules/fastapi_leaudit/services/impl/evaluationPointGroupServiceImpl.py +++ b/fastapi_modules/fastapi_leaudit/services/impl/evaluationPointGroupServiceImpl.py @@ -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() diff --git a/fastapi_modules/fastapi_leaudit/services/impl/rbacAdminServiceImpl.py b/fastapi_modules/fastapi_leaudit/services/impl/rbacAdminServiceImpl.py index a9b2df4..99546da 100644 --- a/fastapi_modules/fastapi_leaudit/services/impl/rbacAdminServiceImpl.py +++ b/fastapi_modules/fastapi_leaudit/services/impl/rbacAdminServiceImpl.py @@ -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"}, diff --git a/fastapi_modules/fastapi_leaudit/services/impl/ruleConfigServiceImpl.py b/fastapi_modules/fastapi_leaudit/services/impl/ruleConfigServiceImpl.py index fdd9b29..5d0c104 100644 --- a/fastapi_modules/fastapi_leaudit/services/impl/ruleConfigServiceImpl.py +++ b/fastapi_modules/fastapi_leaudit/services/impl/ruleConfigServiceImpl.py @@ -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 diff --git a/scripts/user_rbac_seed.sql b/scripts/user_rbac_seed.sql index 7ff4006..381b6e7 100644 --- a/scripts/user_rbac_seed.sql +++ b/scripts/user_rbac_seed.sql @@ -82,19 +82,20 @@ VALUES ('rules:list:read', 'rules', 'list', 'read', '查看规则集列表', '规则集列表', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 40, NULL, '/api/rule-sets', 'GET', NULL), ('rules:version_list:read', 'rules', 'version_list', 'read', '查看规则版本列表', '规则版本列表', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 41, NULL, '/api/rule-sets/{rule_type}/versions', 'GET', NULL), ('rules:content:read', 'rules', 'content', 'read', '查看规则正文', '规则正文', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 42, NULL, '/api/rule-sets/versions/{version_id}/content', 'GET', NULL), - ('rules:validate:execute', 'rules', 'validate', 'execute', '校验规则 YAML', '规则校验', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 43, NULL, '/api/rule-sets/{rule_type}/validate', 'POST', NULL), - ('rules:version_create:write', 'rules', 'version_create', 'write', '创建规则版本', '创建规则版本', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 44, NULL, '/api/rule-sets/{rule_type}/versions', 'POST', NULL), - ('rules:publish:write', 'rules', 'publish', 'write', '发布规则版本', '发布规则', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 45, NULL, '/api/rule-sets/{rule_type}/publish', 'POST', NULL), - ('rules:rollback:write', 'rules', 'rollback', 'write', '回滚规则版本', '回滚规则', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 46, NULL, '/api/rule-sets/{rule_type}/rollback', 'POST', NULL), - ('rules:binding_list:read', 'rules', 'binding_list', 'read', '查看规则绑定列表', '规则绑定列表', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 47, NULL, '/api/rule-sets/bindings', 'GET', NULL), - ('rules:binding_create:write', 'rules', 'binding_create', 'write', '创建规则绑定', '创建规则绑定', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 48, NULL, '/api/rule-sets/{rule_type}/bindings', 'POST', NULL), - ('rules:binding_update:write', 'rules', 'binding_update', 'write', '更新规则绑定', '更新规则绑定', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 49, NULL, '/api/rule-sets/bindings/{binding_id}', 'PUT', NULL), - ('rules:binding_delete:delete', 'rules', 'binding_delete', 'delete', '删除规则绑定', '删除规则绑定', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 50, NULL, '/api/rule-sets/bindings/{binding_id}', 'DELETE', NULL), - ('evaluation_point:list:read', 'evaluation_point', 'list', 'read', '查看评查点列表', '评查点列表', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 51, NULL, '/api/v3/evaluation-points', 'GET', NULL), - ('evaluation_point:detail:read', 'evaluation_point', 'detail', 'read', '查看评查点详情', '评查点详情', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 52, NULL, '/api/v3/evaluation-points/{id}', 'GET', NULL), - ('evaluation_point:create:write', 'evaluation_point', 'create', 'write', '创建评查点', '创建评查点', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 53, NULL, '/api/v3/evaluation-points', 'POST', NULL), - ('evaluation_point:update:write', 'evaluation_point', 'update', 'write', '更新评查点', '更新评查点', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 54, NULL, '/api/v3/evaluation-points/{id}', 'PUT', NULL), - ('evaluation_point:delete:delete', 'evaluation_point', 'delete', 'delete', '删除评查点', '删除评查点', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 55, NULL, '/api/v3/evaluation-points/{id}', 'DELETE', NULL), + ('rules:create:write', 'rules', 'create', 'write', '创建规则草稿', '创建规则草稿', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 43, NULL, '/api/v3/evaluation-point-groups/{id}/rule-drafts', 'POST', NULL), + ('rules:validate:execute', 'rules', 'validate', 'execute', '校验规则 YAML', '规则校验', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 44, NULL, '/api/rule-sets/{rule_type}/validate', 'POST', NULL), + ('rules:version_create:write', 'rules', 'version_create', 'write', '创建规则版本', '创建规则版本', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 45, NULL, '/api/rule-sets/{rule_type}/versions', 'POST', NULL), + ('rules:publish:write', 'rules', 'publish', 'write', '发布规则版本', '发布规则', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 46, NULL, '/api/rule-sets/{rule_type}/publish', 'POST', NULL), + ('rules:rollback:write', 'rules', 'rollback', 'write', '回滚规则版本', '回滚规则', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 47, NULL, '/api/rule-sets/{rule_type}/rollback', 'POST', NULL), + ('rules:binding_list:read', 'rules', 'binding_list', 'read', '查看规则绑定列表', '规则绑定列表', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 48, NULL, '/api/rule-sets/bindings', 'GET', NULL), + ('rules:binding_create:write', 'rules', 'binding_create', 'write', '创建规则绑定', '创建规则绑定', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 49, NULL, '/api/rule-sets/{rule_type}/bindings', 'POST', NULL), + ('rules:binding_update:write', 'rules', 'binding_update', 'write', '更新规则绑定', '更新规则绑定', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 50, NULL, '/api/rule-sets/bindings/{binding_id}', 'PUT', NULL), + ('rules:binding_delete:delete', 'rules', 'binding_delete', 'delete', '删除规则绑定', '删除规则绑定', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 51, NULL, '/api/rule-sets/bindings/{binding_id}', 'DELETE', NULL), + ('evaluation_point:list:read', 'evaluation_point', 'list', 'read', '查看评查点列表', '评查点列表', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 52, NULL, '/api/v3/evaluation-points', 'GET', NULL), + ('evaluation_point:detail:read', 'evaluation_point', 'detail', 'read', '查看评查点详情', '评查点详情', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 53, NULL, '/api/v3/evaluation-points/{id}', 'GET', NULL), + ('evaluation_point:create:write', 'evaluation_point', 'create', 'write', '创建评查点', '创建评查点', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 54, NULL, '/api/v3/evaluation-points', 'POST', NULL), + ('evaluation_point:update:write', 'evaluation_point', 'update', 'write', '更新评查点', '更新评查点', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 55, NULL, '/api/v3/evaluation-points/{id}', 'PUT', NULL), + ('evaluation_point:delete:delete', 'evaluation_point', 'delete', 'delete', '删除评查点', '删除评查点', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 56, NULL, '/api/v3/evaluation-points/{id}', 'DELETE', NULL), ('users:list:read', 'users', 'list', 'read', '查看用户列表', '用户列表', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 60, NULL, '/api/users/list', 'GET', NULL), ('users:create:write', 'users', 'create', 'write', '创建用户', '创建用户', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 61, NULL, '/api/users', 'POST', NULL), @@ -214,6 +215,7 @@ seed(role_key, permission_key, grant_type, data_scope) AS ( ('super_admin', 'audit:status:read', 'GRANT', 'ALL'), ('super_admin', 'audit:result:read', 'GRANT', 'ALL'), ('super_admin', 'rules:list:read', 'GRANT', 'ALL'), + ('super_admin', 'rules:create:write', 'GRANT', 'ALL'), ('super_admin', 'rules:version_list:read', 'GRANT', 'ALL'), ('super_admin', 'rules:content:read', 'GRANT', 'ALL'), ('super_admin', 'rules:validate:execute', 'GRANT', 'ALL'), @@ -250,6 +252,7 @@ seed(role_key, permission_key, grant_type, data_scope) AS ( ('provincial_admin', 'audit:status:read', 'GRANT', 'ALL'), ('provincial_admin', 'audit:result:read', 'GRANT', 'ALL'), ('provincial_admin', 'rules:list:read', 'GRANT', 'ALL'), + ('provincial_admin', 'rules:create:write', 'GRANT', 'ALL'), ('provincial_admin', 'rules:version_list:read', 'GRANT', 'ALL'), ('provincial_admin', 'rules:content:read', 'GRANT', 'ALL'), ('provincial_admin', 'rules:validate:execute', 'GRANT', 'ALL'), @@ -286,6 +289,7 @@ seed(role_key, permission_key, grant_type, data_scope) AS ( ('admin', 'audit:status:read', 'GRANT', 'DEPT'), ('admin', 'audit:result:read', 'GRANT', 'DEPT'), ('admin', 'rules:list:read', 'GRANT', 'DEPT'), + ('admin', 'rules:create:write', 'GRANT', 'DEPT'), ('admin', 'rules:version_list:read', 'GRANT', 'DEPT'), ('admin', 'rules:content:read', 'GRANT', 'DEPT'), ('admin', 'rules:validate:execute', 'GRANT', 'DEPT'),