feat: add document type CRUD with inline rule set binding

- GET/POST /api/document-types, GET/PUT/DELETE /api/document-types/{id}
- DocumentTypeItemVO extended with description, entryModuleId,
  isEnabled, ruleSetIds
- Create/Update DTOs accept ruleSetIds array for automatic
  leaudit_rule_type_bindings sync (full replace on update)
- Soft delete cascades to rule_type_bindings
This commit is contained in:
wren
2026-04-30 12:50:56 +08:00
parent 32f56f7bf6
commit 52c2bed4f9
4 changed files with 265 additions and 30 deletions
@@ -22,7 +22,9 @@ from fastapi_modules.fastapi_leaudit.domian.vo.documentVo import (
DocumentHistoryVersionVO,
DocumentListItemVO,
DocumentListPageVO,
DocumentTypeCreateDTO,
DocumentTypeItemVO,
DocumentTypeUpdateDTO,
DocumentUploadVO,
)
from fastapi_modules.fastapi_leaudit.models import LeauditDocument, LeauditDocumentFile
@@ -459,38 +461,188 @@ class DocumentServiceImpl(IDocumentService):
async def ListDocumentTypes(self, Ids: list[int] | None = None) -> list[DocumentTypeItemVO]:
"""获取文档类型列表。"""
async with GetAsyncSession() as Session:
if Ids:
rows = (
await Session.execute(
text(
"""
SELECT id, name, code
FROM leaudit_document_types
WHERE deleted_at IS NULL AND id = ANY(:ids)
ORDER BY sort_order ASC, id ASC
"""
),
{"ids": Ids},
)
).mappings().all()
else:
rows = (
await Session.execute(
text(
"""
SELECT id, name, code
FROM leaudit_document_types
WHERE deleted_at IS NULL
ORDER BY sort_order ASC, id ASC
"""
)
)
).mappings().all()
rows, bindingsMap = await self._queryDocumentTypes(Session, Ids)
return [
DocumentTypeItemVO(id=int(r["id"]), name=str(r["name"] or ""), code=str(r["code"] or ""))
DocumentTypeItemVO(
id=int(r["id"]), name=str(r["name"] or ""), code=str(r["code"] or ""),
description=r.get("description"), entryModuleId=r.get("entry_module_id"),
isEnabled=bool(r.get("is_enabled", True)),
ruleSetIds=bindingsMap.get(int(r["id"]), []),
)
for r in rows
]
async def GetDocumentType(self, Id: int) -> DocumentTypeItemVO:
"""获取文档类型详情。"""
async with GetAsyncSession() as Session:
rows, bindingsMap = await self._queryDocumentTypes(Session, [Id])
if not rows:
raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "文档类型不存在")
r = rows[0]
return DocumentTypeItemVO(
id=int(r["id"]), name=str(r["name"] or ""), code=str(r["code"] or ""),
description=r.get("description"), entryModuleId=r.get("entry_module_id"),
isEnabled=bool(r.get("is_enabled", True)),
ruleSetIds=bindingsMap.get(int(r["id"]), []),
)
async def CreateDocumentType(self, Body: DocumentTypeCreateDTO) -> DocumentTypeItemVO:
"""创建文档类型。"""
async with GetAsyncSession() as Session:
existing = (
await Session.execute(
text("SELECT 1 FROM leaudit_document_types WHERE code = :code AND deleted_at IS NULL LIMIT 1"),
{"code": Body.code.strip()},
)
).first()
if existing:
raise LeauditException(StatusCodeEnum.HTTP_409_CONFLICT, f"文档类型编码 {Body.code} 已存在")
row = (
await Session.execute(
text(
"""
INSERT INTO leaudit_document_types (code, name, description, entry_module_id, is_enabled, sort_order, created_at, updated_at)
VALUES (:code, :name, :description, :entry_module_id, :is_enabled, :sort_order, NOW(), NOW())
RETURNING id
"""
),
{
"code": Body.code.strip(),
"name": Body.name.strip(),
"description": Body.description.strip() or None,
"entry_module_id": Body.entryModuleId,
"is_enabled": Body.isEnabled,
"sort_order": Body.sortOrder,
},
)
).scalar_one()
await self._syncRuleBindings(Session, int(row), Body.ruleSetIds, "default")
await Session.commit()
return await self.GetDocumentType(int(row))
async def UpdateDocumentType(self, Id: int, Body: DocumentTypeUpdateDTO) -> DocumentTypeItemVO:
"""更新文档类型。"""
async with GetAsyncSession() as Session:
current = (
await Session.execute(
text("SELECT id FROM leaudit_document_types WHERE id = :id AND deleted_at IS NULL"),
{"id": Id},
)
).first()
if not current:
raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "文档类型不存在")
sets: list[str] = []
params: dict[str, object] = {"id": Id}
if Body.name is not None:
sets.append("name = :name")
params["name"] = Body.name.strip()
if Body.description is not None:
sets.append("description = :description")
params["description"] = Body.description.strip() or None
if Body.entryModuleId is not None:
sets.append("entry_module_id = :entry_module_id")
params["entry_module_id"] = Body.entryModuleId
if Body.isEnabled is not None:
sets.append("is_enabled = :is_enabled")
params["is_enabled"] = Body.isEnabled
if 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_document_types SET {', '.join(sets)} WHERE id = :id"), params)
if Body.ruleSetIds is not None:
await self._syncRuleBindings(Session, Id, Body.ruleSetIds, "default")
await Session.commit()
return await self.GetDocumentType(Id)
async def DeleteDocumentType(self, Id: int) -> None:
"""软删除文档类型。"""
async with GetAsyncSession() as Session:
current = (
await Session.execute(
text("SELECT 1 FROM leaudit_document_types WHERE id = :id AND deleted_at IS NULL"),
{"id": Id},
)
).first()
if not current:
raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "文档类型不存在")
await Session.execute(text("UPDATE leaudit_document_types SET deleted_at = NOW() WHERE id = :id"), {"id": Id})
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 _queryDocumentTypes(self, Session, Ids: list[int] | None = None):
"""查询文档类型及其规则绑定。"""
if Ids:
rows = (
await Session.execute(
text(
"""
SELECT id, code, name, description, entry_module_id, is_enabled, sort_order
FROM leaudit_document_types
WHERE deleted_at IS NULL AND id = ANY(:ids)
ORDER BY sort_order ASC, id ASC
"""
),
{"ids": Ids},
)
).mappings().all()
else:
rows = (
await Session.execute(
text(
"""
SELECT id, code, name, description, entry_module_id, is_enabled, sort_order
FROM leaudit_document_types
WHERE deleted_at IS NULL
ORDER BY sort_order ASC, id ASC
"""
)
)
).mappings().all()
allIds = [int(r["id"]) for r in rows]
bindingsMap: dict[int, list[int]] = {i: [] for i in allIds}
if allIds:
bindingRows = (
await Session.execute(
text(
"""
SELECT doc_type_id, rule_set_id
FROM leaudit_rule_type_bindings
WHERE doc_type_id = ANY(:ids) AND deleted_at IS NULL AND is_active = true
ORDER BY priority DESC
"""
),
{"ids": allIds},
)
).fetchall()
for b in bindingRows:
bindingsMap.setdefault(int(b[0]), []).append(int(b[1]))
return rows, bindingsMap
async def _syncRuleBindings(self, Session, DocTypeId: int, RuleSetIds: list[int], Region: str = "default") -> None:
"""全量替换规则绑定。"""
await Session.execute(
text("UPDATE leaudit_rule_type_bindings SET deleted_at = NOW() WHERE doc_type_id = :id AND deleted_at IS NULL"),
{"id": DocTypeId},
)
for idx, ruleSetId in enumerate(RuleSetIds):
await Session.execute(
text(
"""
INSERT INTO leaudit_rule_type_bindings (doc_type_id, rule_set_id, binding_mode, priority, region, is_active, created_at, updated_at)
VALUES (:doc_type_id, :rule_set_id, 'explicit', :priority, :region, true, NOW(), NOW())
"""
),
{"doc_type_id": DocTypeId, "rule_set_id": ruleSetId, "priority": 100 - idx, "region": Region},
)
async def _find_latest_version_candidate(
session,