feat: add backend rule group and permission support
This commit is contained in:
@@ -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": []},
|
||||
}
|
||||
Reference in New Issue
Block a user