feat: migrate rule bindings to group-based flow

This commit is contained in:
wren
2026-05-07 17:43:20 +08:00
parent 75c2111209
commit f8eb2dc817
8 changed files with 871 additions and 361 deletions
@@ -19,6 +19,7 @@ from fastapi_modules.fastapi_leaudit.domian.vo.ruleVo import (
from fastapi_modules.fastapi_leaudit.leaudit_bridge.ruleValidator import RuleValidator
from fastapi_modules.fastapi_leaudit.services import IOssService, IRuleService
from fastapi_modules.fastapi_leaudit.services.impl.ossServiceImpl import OssServiceImpl
from fastapi_modules.fastapi_leaudit.services.impl.ruleGroupSupport import sync_doc_type_bindings_from_group
class RuleServiceImpl(IRuleService):
@@ -32,6 +33,42 @@ class RuleServiceImpl(IRuleService):
self.OssService = OssService or OssServiceImpl()
self.Validator = Validator or RuleValidator()
async def _resolve_unique_child_group_id(self, Session, DocTypeId: int) -> int | None:
"""仅当文档类型唯一对应一个二级分组时,返回该分组ID。"""
row = (
await Session.execute(
text(
"""
SELECT CASE WHEN COUNT(*) = 1 THEN MIN(id) END AS group_id
FROM leaudit_evaluation_point_groups
WHERE document_type_id = :doc_type_id
AND deleted_at IS NULL
AND COALESCE(pid, 0) <> 0
"""
),
{"doc_type_id": DocTypeId},
)
).mappings().first()
return int(row["group_id"]) if row and row.get("group_id") is not None else None
async def _count_child_groups(self, Session, DocTypeId: int) -> int:
"""统计文档类型下已启用的二级分组数量,用于明确报错。"""
row = (
await Session.execute(
text(
"""
SELECT COUNT(*) AS total
FROM leaudit_evaluation_point_groups
WHERE document_type_id = :doc_type_id
AND deleted_at IS NULL
AND COALESCE(pid, 0) <> 0
"""
),
{"doc_type_id": DocTypeId},
)
).mappings().first()
return int(row["total"] or 0) if row else 0
async def ListSets(self) -> list[RuleSetVO]:
"""列出所有规则集。"""
async with GetAsyncSession() as Session:
@@ -398,119 +435,116 @@ class RuleServiceImpl(IRuleService):
)
async def ListBindings(self, RuleType: str | None = None, Region: str | None = None) -> list[RuleBindingVO]:
"""列出规则类型绑定,可按规则类型/地区过滤"""
"""列出规则类型绑定,优先读取新分组绑定,旧表仅作为兼容兜底"""
region = Region or ""
async with GetAsyncSession() as Session:
if RuleType and region:
Result = await Session.execute(
text(
"""
SELECT
b.id,
b.doc_type_id,
b.doc_type_code,
b.rule_set_id,
b.binding_mode,
b.priority,
b.is_active,
b.note,
rs.rule_type,
rs.rule_name
FROM leaudit_rule_type_bindings b
JOIN leaudit_rule_sets rs ON rs.id = b.rule_set_id
WHERE rs.rule_type = :rule_type
AND rs.deleted_at IS NULL
AND b.region = :region
ORDER BY b.priority DESC, b.id DESC
"""
),
{"rule_type": RuleType, "region": region},
params: dict[str, object] = {}
filters = ["rs.deleted_at IS NULL", "dt.deleted_at IS NULL", "child.deleted_at IS NULL", "rgb.deleted_at IS NULL"]
if RuleType:
filters.append("rs.rule_type = :rule_type")
params["rule_type"] = RuleType
where_clause = " AND ".join(filters)
result = await Session.execute(
text(
f"""
SELECT
rgb.id,
dt.id AS doc_type_id,
dt.code AS doc_type_code,
rgb.rule_set_id,
'explicit' AS binding_mode,
rgb.priority,
rgb.is_active,
rgb.note,
rs.rule_type,
rs.rule_name
FROM leaudit_rule_group_bindings rgb
JOIN leaudit_evaluation_point_groups child ON child.id = rgb.group_id
JOIN leaudit_document_types dt ON dt.id = child.document_type_id
JOIN leaudit_rule_sets rs ON rs.id = rgb.rule_set_id
WHERE {where_clause}
AND COALESCE(child.pid, 0) <> 0
ORDER BY dt.id ASC, COALESCE(child.sort_order, 0) ASC, rgb.priority DESC, rgb.id DESC
"""
),
params,
)
rows = result.mappings().all()
bindings: list[RuleBindingVO] = []
seen_doc_type_rule_pairs: set[tuple[int, int]] = set()
covered_doc_type_ids: set[int] = set()
for row in rows:
pair = (int(row["doc_type_id"]), int(row["rule_set_id"]))
if pair in seen_doc_type_rule_pairs:
continue
seen_doc_type_rule_pairs.add(pair)
covered_doc_type_ids.add(int(row["doc_type_id"]))
bindings.append(
RuleBindingVO(
id=int(row["id"]),
docTypeId=int(row["doc_type_id"]),
docTypeCode=row["doc_type_code"],
ruleSetId=int(row["rule_set_id"]),
ruleType=row["rule_type"],
ruleName=row["rule_name"],
bindingMode=row["binding_mode"],
priority=int(row["priority"]),
isActive=bool(row["is_active"]),
note=row["note"],
)
)
elif region:
Result = await Session.execute(
text(
"""
SELECT
b.id,
b.doc_type_id,
b.doc_type_code,
b.rule_set_id,
b.binding_mode,
b.priority,
b.is_active,
b.note,
rs.rule_type,
rs.rule_name
FROM leaudit_rule_type_bindings b
JOIN leaudit_rule_sets rs ON rs.id = b.rule_set_id
WHERE rs.deleted_at IS NULL
AND b.region = :region
ORDER BY rs.rule_type, b.priority DESC, b.id DESC
"""
),
{"region": region},
legacy_filters = ["rs.deleted_at IS NULL", "b.deleted_at IS NULL"]
legacy_params: dict[str, object] = {}
if RuleType:
legacy_filters.append("rs.rule_type = :rule_type")
legacy_params["rule_type"] = RuleType
if region:
legacy_filters.append("b.region = :region")
legacy_params["region"] = region
if covered_doc_type_ids:
legacy_filters.append("b.doc_type_id <> ALL(:covered_doc_type_ids)")
legacy_params["covered_doc_type_ids"] = list(covered_doc_type_ids)
legacy_where_clause = " AND ".join(legacy_filters)
legacy_result = await Session.execute(
text(
f"""
SELECT
b.id,
b.doc_type_id,
b.doc_type_code,
b.rule_set_id,
b.binding_mode,
b.priority,
b.is_active,
b.note,
rs.rule_type,
rs.rule_name
FROM leaudit_rule_type_bindings b
JOIN leaudit_rule_sets rs ON rs.id = b.rule_set_id
WHERE {legacy_where_clause}
ORDER BY rs.rule_type, b.priority DESC, b.id DESC
"""
),
legacy_params,
)
for row in legacy_result.mappings().all():
bindings.append(
RuleBindingVO(
id=int(row["id"]),
docTypeId=int(row["doc_type_id"]),
docTypeCode=row["doc_type_code"],
ruleSetId=int(row["rule_set_id"]),
ruleType=row["rule_type"],
ruleName=row["rule_name"],
bindingMode=row["binding_mode"],
priority=int(row["priority"]),
isActive=bool(row["is_active"]),
note=row["note"],
)
)
elif RuleType:
Result = await Session.execute(
text(
"""
SELECT
b.id,
b.doc_type_id,
b.doc_type_code,
b.rule_set_id,
b.binding_mode,
b.priority,
b.is_active,
b.note,
rs.rule_type,
rs.rule_name
FROM leaudit_rule_type_bindings b
JOIN leaudit_rule_sets rs ON rs.id = b.rule_set_id
WHERE rs.rule_type = :rule_type
AND rs.deleted_at IS NULL
ORDER BY b.priority DESC, b.id DESC
"""
),
{"rule_type": RuleType},
)
else:
Result = await Session.execute(
text(
"""
SELECT
b.id,
b.doc_type_id,
b.doc_type_code,
b.rule_set_id,
b.binding_mode,
b.priority,
b.is_active,
b.note,
rs.rule_type,
rs.rule_name
FROM leaudit_rule_type_bindings b
JOIN leaudit_rule_sets rs ON rs.id = b.rule_set_id
WHERE rs.deleted_at IS NULL
ORDER BY rs.rule_type, b.priority DESC, b.id DESC
"""
),
)
return [
RuleBindingVO(
id=int(Row["id"]),
docTypeId=int(Row["doc_type_id"]),
docTypeCode=Row["doc_type_code"],
ruleSetId=int(Row["rule_set_id"]),
ruleType=Row["rule_type"],
ruleName=Row["rule_name"],
bindingMode=Row["binding_mode"],
priority=int(Row["priority"]),
isActive=bool(Row["is_active"]),
note=Row["note"],
)
for Row in Result.mappings().all()
]
return bindings
async def CreateBinding(
self,
@@ -522,7 +556,7 @@ class RuleServiceImpl(IRuleService):
DocTypeCode: str | None = None,
Note: str | None = None,
) -> RuleBindingVO:
"""创建规则类型绑定。"""
"""创建规则类型绑定;若文档类型唯一对应一个二级分组,则优先写入新分组绑定"""
async with GetAsyncSession() as Session:
RuleSet = await Session.execute(
text("SELECT id, rule_type, rule_name FROM leaudit_rule_sets WHERE id = :rid AND deleted_at IS NULL LIMIT 1"),
@@ -532,68 +566,79 @@ class RuleServiceImpl(IRuleService):
if not RsRow:
raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "规则集不存在")
Existing = await Session.execute(
text(
"""
SELECT id FROM leaudit_rule_type_bindings
WHERE doc_type_id = :dtid AND rule_set_id = :rsid
LIMIT 1
"""
),
{"dtid": DocTypeId, "rsid": RuleSetId},
)
if Existing.mappings().first():
raise LeauditException(StatusCodeEnum.HTTP_409_CONFLICT, "该文档类型已绑定此规则集")
GroupId = await self._resolve_unique_child_group_id(Session, DocTypeId)
if GroupId is not None:
ExistingGroupBinding = await Session.execute(
text(
"""
SELECT id
FROM leaudit_rule_group_bindings
WHERE group_id = :group_id
AND rule_set_id = :rule_set_id
AND deleted_at IS NULL
LIMIT 1
"""
),
{"group_id": GroupId, "rule_set_id": RuleSetId},
)
if ExistingGroupBinding.mappings().first():
raise LeauditException(StatusCodeEnum.HTTP_409_CONFLICT, "该文档类型对应子组已绑定此规则集")
Result = await Session.execute(
text(
"""
INSERT INTO leaudit_rule_type_bindings (
doc_type_id,
doc_type_code,
rule_set_id,
binding_mode,
priority,
is_active,
region,
note
) VALUES (
:doc_type_id,
:doc_type_code,
:rule_set_id,
:binding_mode,
:priority,
true,
:region,
:note
)
RETURNING id, doc_type_id, doc_type_code, rule_set_id,
binding_mode, priority, is_active, region, note
"""
),
{
"doc_type_id": DocTypeId,
"doc_type_code": DocTypeCode,
"rule_set_id": RuleSetId,
"binding_mode": BindingMode,
"priority": Priority,
"region": Region,
"note": Note,
},
)
await Session.commit()
Row = Result.mappings().first()
return RuleBindingVO(
id=int(Row["id"]),
docTypeId=int(Row["doc_type_id"]),
docTypeCode=Row["doc_type_code"],
ruleSetId=int(Row["rule_set_id"]),
ruleType=RsRow["rule_type"],
ruleName=RsRow["rule_name"],
bindingMode=Row["binding_mode"],
priority=int(Row["priority"]),
isActive=bool(Row["is_active"]),
note=Row["note"],
Result = await Session.execute(
text(
"""
INSERT INTO leaudit_rule_group_bindings (
group_id,
rule_set_id,
priority,
is_active,
note,
created_at,
updated_at
) VALUES (
:group_id,
:rule_set_id,
:priority,
true,
:note,
NOW(),
NOW()
)
RETURNING id, rule_set_id, priority, is_active, note
"""
),
{
"group_id": GroupId,
"rule_set_id": RuleSetId,
"priority": Priority,
"note": Note,
},
)
await sync_doc_type_bindings_from_group(Session, GroupId)
await Session.commit()
Row = Result.mappings().first()
return RuleBindingVO(
id=int(Row["id"]),
docTypeId=DocTypeId,
docTypeCode=DocTypeCode,
ruleSetId=int(Row["rule_set_id"]),
ruleType=RsRow["rule_type"],
ruleName=RsRow["rule_name"],
bindingMode="explicit",
priority=int(Row["priority"]),
isActive=bool(Row["is_active"]),
note=Row["note"],
)
child_group_count = await self._count_child_groups(Session, DocTypeId)
if child_group_count == 0:
raise LeauditException(
StatusCodeEnum.HTTP_409_CONFLICT,
"当前文档类型尚未创建运行子类型,无法绑定规则集;请先在评查点分组管理中补齐二级分组",
)
raise LeauditException(
StatusCodeEnum.HTTP_409_CONFLICT,
"当前文档类型存在多个运行子类型,不能再直接按文档类型绑定规则集;请改为在对应二级分组下绑定规则集",
)
async def UpdateBinding(
@@ -604,8 +649,94 @@ class RuleServiceImpl(IRuleService):
BindingMode: str | None = None,
Note: str | None = None,
) -> RuleBindingVO:
"""更新规则类型绑定。"""
"""更新规则类型绑定;若绑定ID来自新分组绑定,则优先更新新表"""
async with GetAsyncSession() as Session:
GroupBinding = await Session.execute(
text(
"""
SELECT
rgb.id,
rgb.group_id,
child.document_type_id AS doc_type_id,
dt.code AS doc_type_code,
rgb.rule_set_id,
rgb.priority,
rgb.is_active,
rgb.note,
rs.rule_type,
rs.rule_name
FROM leaudit_rule_group_bindings rgb
JOIN leaudit_evaluation_point_groups child ON child.id = rgb.group_id
LEFT JOIN leaudit_document_types dt ON dt.id = child.document_type_id
JOIN leaudit_rule_sets rs ON rs.id = rgb.rule_set_id
WHERE rgb.id = :bid
AND rgb.deleted_at IS NULL
LIMIT 1
"""
),
{"bid": BindingId},
)
GroupRow = GroupBinding.mappings().first()
if GroupRow:
await Session.execute(
text(
"""
UPDATE leaudit_rule_group_bindings
SET priority = :priority,
is_active = :is_active,
note = :note,
updated_at = NOW()
WHERE id = :binding_id
"""
),
{
"binding_id": BindingId,
"priority": Priority if Priority is not None else int(GroupRow.get("priority") or 0),
"is_active": IsActive if IsActive is not None else bool(GroupRow.get("is_active", True)),
"note": Note if Note is not None else GroupRow.get("note"),
},
)
await sync_doc_type_bindings_from_group(Session, int(GroupRow["group_id"]))
await Session.commit()
refreshed = (
await Session.execute(
text(
"""
SELECT
rgb.id,
rgb.group_id,
child.document_type_id AS doc_type_id,
dt.code AS doc_type_code,
rgb.rule_set_id,
rgb.priority,
rgb.is_active,
rgb.note,
rs.rule_type,
rs.rule_name
FROM leaudit_rule_group_bindings rgb
JOIN leaudit_evaluation_point_groups child ON child.id = rgb.group_id
LEFT JOIN leaudit_document_types dt ON dt.id = child.document_type_id
JOIN leaudit_rule_sets rs ON rs.id = rgb.rule_set_id
WHERE rgb.id = :bid
LIMIT 1
"""
),
{"bid": BindingId},
)
).mappings().first()
return RuleBindingVO(
id=int(refreshed["id"]),
docTypeId=int(refreshed["doc_type_id"]),
docTypeCode=refreshed["doc_type_code"],
ruleSetId=int(refreshed["rule_set_id"]),
ruleType=refreshed["rule_type"],
ruleName=refreshed["rule_name"],
bindingMode=BindingMode or "explicit",
priority=int(refreshed["priority"]),
isActive=bool(refreshed["is_active"]),
note=refreshed["note"],
)
Existing = await Session.execute(
text(
"""
@@ -679,8 +810,30 @@ class RuleServiceImpl(IRuleService):
)
async def DeleteBinding(self, BindingId: int) -> None:
"""删除规则类型绑定。"""
"""删除规则类型绑定;若绑定ID来自新分组绑定,则优先删除新表"""
async with GetAsyncSession() as Session:
GroupBinding = await Session.execute(
text(
"""
SELECT id, group_id
FROM leaudit_rule_group_bindings
WHERE id = :bid
AND deleted_at IS NULL
LIMIT 1
"""
),
{"bid": BindingId},
)
GroupRow = GroupBinding.mappings().first()
if GroupRow:
await Session.execute(
text("UPDATE leaudit_rule_group_bindings SET deleted_at = NOW(), updated_at = NOW() WHERE id = :bid"),
{"bid": BindingId},
)
await sync_doc_type_bindings_from_group(Session, int(GroupRow["group_id"]))
await Session.commit()
return
Result = await Session.execute(
text("DELETE FROM leaudit_rule_type_bindings WHERE id = :bid"),
{"bid": BindingId},