feat: add document type root management

This commit is contained in:
wren
2026-05-06 14:20:28 +08:00
parent 201e3adc18
commit c4694e11f0
8 changed files with 282 additions and 10 deletions
@@ -26,6 +26,9 @@ from fastapi_modules.fastapi_leaudit.domian.vo.documentVo import (
DocumentListItemVO,
DocumentListPageVO,
DocumentStatusItemVO,
DocumentTypeRootCreateDTO,
DocumentTypeRootItemVO,
DocumentTypeRootUpdateDTO,
DocumentUpdateDTO,
DocumentTypeCreateDTO,
DocumentTypeItemVO,
@@ -1185,6 +1188,97 @@ class DocumentServiceImpl(IDocumentService):
await Session.execute(text("UPDATE leaudit_rule_type_bindings SET deleted_at = NOW() WHERE doc_type_id = :id AND deleted_at IS NULL"), {"id": Id})
await Session.commit()
async def ListDocumentTypeRoots(self, EntryModuleId: int | None = None) -> list[DocumentTypeRootItemVO]:
"""获取一级文档类型(业务大类)列表。"""
async with GetAsyncSession() as Session:
rows = await self._queryDocumentTypeRoots(Session, EntryModuleId=EntryModuleId)
return [self._toDocumentTypeRootVo(row) for row in rows]
async def GetDocumentTypeRoot(self, Id: int) -> DocumentTypeRootItemVO:
"""获取一级文档类型(业务大类)详情。"""
async with GetAsyncSession() as Session:
rows = await self._queryDocumentTypeRoots(Session, Ids=[Id])
if not rows:
raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "一级文档类型不存在")
return self._toDocumentTypeRootVo(rows[0])
async def CreateDocumentTypeRoot(self, Body: DocumentTypeRootCreateDTO) -> DocumentTypeRootItemVO:
"""创建一级文档类型(业务大类)。"""
async with GetAsyncSession() as Session:
await self._ensureDocumentTypeRootCodeUnique(Session, Body.code.strip(), None)
root_id = (
await Session.execute(
text(
"""
INSERT INTO leaudit_evaluation_point_groups
(pid, name, code, description, document_type_id, entry_module_id, sort_order, is_enabled, created_at, updated_at)
VALUES
(0, :name, :code, :description, NULL, :entry_module_id, :sort_order, :is_enabled, NOW(), NOW())
RETURNING id
"""
),
{
"name": Body.name.strip(),
"code": Body.code.strip(),
"description": Body.description.strip() or None,
"entry_module_id": Body.entryModuleId,
"sort_order": Body.sortOrder,
"is_enabled": Body.isEnabled,
},
)
).scalar_one()
await Session.commit()
return await self.GetDocumentTypeRoot(int(root_id))
async def UpdateDocumentTypeRoot(self, Id: int, Body: DocumentTypeRootUpdateDTO) -> DocumentTypeRootItemVO:
"""更新一级文档类型(业务大类)。"""
async with GetAsyncSession() as Session:
current = (
await Session.execute(
text(
"""
SELECT 1
FROM leaudit_evaluation_point_groups
WHERE id = :id
AND deleted_at IS NULL
AND COALESCE(pid, 0) = 0
"""
),
{"id": Id},
)
).first()
if not current:
raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "一级文档类型不存在")
providedFields = set(getattr(Body, "model_fields_set", set()))
sets: list[str] = []
params: dict[str, object] = {"id": Id}
if "name" in providedFields and Body.name is not None:
sets.append("name = :name")
params["name"] = Body.name.strip()
if "description" in providedFields:
sets.append("description = :description")
params["description"] = Body.description.strip() if Body.description is not None else None
if params["description"] == "":
params["description"] = None
if "entryModuleId" in providedFields:
sets.append("entry_module_id = :entry_module_id")
params["entry_module_id"] = Body.entryModuleId
if "isEnabled" in providedFields and Body.isEnabled is not None:
sets.append("is_enabled = :is_enabled")
params["is_enabled"] = Body.isEnabled
if "sortOrder" in providedFields and Body.sortOrder is not None:
sets.append("sort_order = :sort_order")
params["sort_order"] = Body.sortOrder
if sets:
sets.append("updated_at = NOW()")
await Session.execute(
text(f"UPDATE leaudit_evaluation_point_groups SET {', '.join(sets)} WHERE id = :id"),
params,
)
await Session.commit()
return await self.GetDocumentTypeRoot(Id)
async def _queryDocumentTypes(self, Session, Ids: list[int] | None = None, EntryModuleId: int | None = None):
"""查询文档类型及其规则绑定。"""
if Ids:
@@ -1250,6 +1344,93 @@ class DocumentServiceImpl(IDocumentService):
bindingsMap.setdefault(int(b[0]), []).append(int(b[1]))
return rows, bindingsMap
async def _queryDocumentTypeRoots(self, Session, Ids: list[int] | None = None, EntryModuleId: int | None = None):
filters = ["g.deleted_at IS NULL", "COALESCE(g.pid, 0) = 0"]
params: dict[str, object] = {}
if Ids:
filters.append("g.id = ANY(:ids)")
params["ids"] = Ids
if EntryModuleId is not None:
filters.append("g.entry_module_id = :entry_module_id")
params["entry_module_id"] = EntryModuleId
where_clause = " AND ".join(filters)
rows = (
await Session.execute(
text(
f"""
SELECT
g.id,
g.name,
g.code,
g.description,
g.entry_module_id,
em.name AS entry_module_name,
g.is_enabled,
COALESCE(child_stats.child_group_count, 0) AS child_group_count,
COALESCE(rule_stats.rule_set_count, 0) AS rule_set_count,
COALESCE(rule_stats.rule_set_ids, ARRAY[]::bigint[]) AS rule_set_ids
FROM leaudit_evaluation_point_groups g
LEFT JOIN leaudit_entry_modules em ON em.id = g.entry_module_id
LEFT JOIN (
SELECT pid, COUNT(*)::int AS child_group_count
FROM leaudit_evaluation_point_groups
WHERE deleted_at IS NULL AND COALESCE(pid, 0) <> 0
GROUP BY pid
) child_stats ON child_stats.pid = g.id
LEFT JOIN (
SELECT
child.pid,
COUNT(DISTINCT rgb.rule_set_id)::int AS rule_set_count,
ARRAY_AGG(DISTINCT rgb.rule_set_id) FILTER (WHERE rgb.rule_set_id IS NOT NULL) AS rule_set_ids
FROM leaudit_evaluation_point_groups child
LEFT JOIN leaudit_rule_group_bindings rgb
ON rgb.group_id = child.id
AND rgb.deleted_at IS NULL
WHERE child.deleted_at IS NULL
AND COALESCE(child.pid, 0) <> 0
GROUP BY child.pid
) rule_stats ON rule_stats.pid = g.id
WHERE {where_clause}
ORDER BY COALESCE(g.sort_order, 0) ASC, g.id ASC
"""
),
params,
)
).mappings().all()
return rows
def _toDocumentTypeRootVo(self, row) -> DocumentTypeRootItemVO:
rule_set_ids = [int(item) for item in (row.get("rule_set_ids") or []) if item is not None]
return DocumentTypeRootItemVO(
id=int(row["id"]),
name=str(row["name"] or ""),
code=str(row["code"] or ""),
description=row.get("description"),
entryModuleId=int(row["entry_module_id"]) if row.get("entry_module_id") is not None else None,
entryModuleName=row.get("entry_module_name"),
isEnabled=bool(row.get("is_enabled", True)),
childGroupCount=int(row.get("child_group_count") or 0),
ruleSetCount=int(row.get("rule_set_count") or 0),
ruleSetIds=rule_set_ids,
)
async def _ensureDocumentTypeRootCodeUnique(self, Session, Code: str, CurrentId: int | None) -> None:
sql = """
SELECT 1
FROM leaudit_evaluation_point_groups
WHERE deleted_at IS NULL
AND COALESCE(pid, 0) = 0
AND code = :code
"""
params: dict[str, object] = {"code": Code}
if CurrentId is not None:
sql += " AND id <> :current_id"
params["current_id"] = CurrentId
exists = (await Session.execute(text(sql), params)).first()
if exists:
raise LeauditException(StatusCodeEnum.HTTP_409_CONFLICT, f"一级文档类型编码 {Code} 已存在")
async def _loadDocumentColumns(self, Session) -> set[str]:
"""读取 leaudit_documents 当前真实列,兼容不同环境的渐进式字段上线。"""
rows = (