feat: migrate rule bindings to group-based flow
This commit is contained in:
@@ -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},
|
||||
|
||||
Reference in New Issue
Block a user