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:
@@ -10,7 +10,13 @@ from fastapi_common.fastapi_common_sqlalchemy.database import GetAsyncSession
|
||||
from fastapi_common.fastapi_common_web.controller import BaseController
|
||||
from fastapi_common.fastapi_common_web.domain.responses import Result
|
||||
|
||||
from fastapi_modules.fastapi_leaudit.domian.vo.documentVo import DocumentListPageVO, DocumentTypeItemVO, DocumentUploadVO
|
||||
from fastapi_modules.fastapi_leaudit.domian.vo.documentVo import (
|
||||
DocumentListPageVO,
|
||||
DocumentTypeCreateDTO,
|
||||
DocumentTypeItemVO,
|
||||
DocumentTypeUpdateDTO,
|
||||
DocumentUploadVO,
|
||||
)
|
||||
from fastapi_modules.fastapi_leaudit.services import IDocumentService
|
||||
from fastapi_modules.fastapi_leaudit.services.impl.documentServiceImpl import DocumentServiceImpl
|
||||
|
||||
@@ -95,6 +101,30 @@ class DocumentController(BaseController):
|
||||
Data = await self.DocumentService.ListDocumentTypes(Ids=idList)
|
||||
return Result.success(data=Data)
|
||||
|
||||
@self.router.get("/document-types/{TypeId}", response_model=Result[DocumentTypeItemVO])
|
||||
async def GetDocumentType(TypeId: int):
|
||||
"""获取文档类型详情。"""
|
||||
Data = await self.DocumentService.GetDocumentType(Id=TypeId)
|
||||
return Result.success(data=Data)
|
||||
|
||||
@self.router.post("/document-types", response_model=Result[DocumentTypeItemVO])
|
||||
async def CreateDocumentType(Body: DocumentTypeCreateDTO):
|
||||
"""创建文档类型。"""
|
||||
Data = await self.DocumentService.CreateDocumentType(Body=Body)
|
||||
return Result.success(data=Data, message="文档类型创建成功")
|
||||
|
||||
@self.router.put("/document-types/{TypeId}", response_model=Result[DocumentTypeItemVO])
|
||||
async def UpdateDocumentType(TypeId: int, Body: DocumentTypeUpdateDTO):
|
||||
"""更新文档类型。"""
|
||||
Data = await self.DocumentService.UpdateDocumentType(Id=TypeId, Body=Body)
|
||||
return Result.success(data=Data, message="文档类型更新成功")
|
||||
|
||||
@self.router.delete("/document-types/{TypeId}", response_model=Result[None])
|
||||
async def DeleteDocumentType(TypeId: int):
|
||||
"""删除文档类型(软删除)。"""
|
||||
await self.DocumentService.DeleteDocumentType(Id=TypeId)
|
||||
return Result.success(message="文档类型已删除")
|
||||
|
||||
@self.router.get("/v2/system/queue/status", response_model=Result[QueueStatusVO])
|
||||
async def GetQueueStatus():
|
||||
"""获取文档处理队列状态。"""
|
||||
|
||||
@@ -80,6 +80,33 @@ class DocumentTypeItemVO(BaseModel):
|
||||
id: int = Field(..., description="类型ID")
|
||||
name: str = Field(..., description="类型名称")
|
||||
code: str = Field(..., description="类型编码")
|
||||
description: str | None = Field(None, description="描述")
|
||||
entryModuleId: int | None = Field(None, description="入口模块ID")
|
||||
isEnabled: bool = Field(True, description="是否启用")
|
||||
ruleSetIds: list[int] = Field(default_factory=list, description="关联的规则集ID")
|
||||
|
||||
|
||||
class DocumentTypeCreateDTO(BaseModel):
|
||||
"""文档类型创建请求。"""
|
||||
|
||||
code: str = Field(..., description="类型编码")
|
||||
name: str = Field(..., description="类型名称")
|
||||
description: str = Field("", description="描述")
|
||||
entryModuleId: int | None = Field(None, description="入口模块ID")
|
||||
isEnabled: bool = Field(True, description="是否启用")
|
||||
sortOrder: int = Field(0, description="排序")
|
||||
ruleSetIds: list[int] = Field(default_factory=list, description="关联的规则集ID")
|
||||
|
||||
|
||||
class DocumentTypeUpdateDTO(BaseModel):
|
||||
"""文档类型更新请求。"""
|
||||
|
||||
name: str | None = Field(None, description="类型名称")
|
||||
description: str | None = Field(None, description="描述")
|
||||
entryModuleId: int | None = Field(None, description="入口模块ID")
|
||||
isEnabled: bool | None = Field(None, description="是否启用")
|
||||
sortOrder: int | None = Field(None, description="排序")
|
||||
ruleSetIds: list[int] | None = Field(None, description="关联的规则集ID(传则全量替换)")
|
||||
|
||||
|
||||
class DocumentListPageVO(BaseModel):
|
||||
|
||||
@@ -2,7 +2,13 @@
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from fastapi_modules.fastapi_leaudit.domian.vo.documentVo import DocumentListPageVO, DocumentTypeItemVO, DocumentUploadVO
|
||||
from fastapi_modules.fastapi_leaudit.domian.vo.documentVo import (
|
||||
DocumentListPageVO,
|
||||
DocumentTypeCreateDTO,
|
||||
DocumentTypeItemVO,
|
||||
DocumentTypeUpdateDTO,
|
||||
DocumentUploadVO,
|
||||
)
|
||||
|
||||
|
||||
class IDocumentService(ABC):
|
||||
@@ -46,3 +52,23 @@ class IDocumentService(ABC):
|
||||
async def ListDocumentTypes(self, Ids: list[int] | None = None) -> list[DocumentTypeItemVO]:
|
||||
"""获取文档类型列表。"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def GetDocumentType(self, Id: int) -> DocumentTypeItemVO:
|
||||
"""获取文档类型详情。"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def CreateDocumentType(self, Body: DocumentTypeCreateDTO) -> DocumentTypeItemVO:
|
||||
"""创建文档类型。"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def UpdateDocumentType(self, Id: int, Body: DocumentTypeUpdateDTO) -> DocumentTypeItemVO:
|
||||
"""更新文档类型。"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def DeleteDocumentType(self, Id: int) -> None:
|
||||
"""删除文档类型(软删除)。"""
|
||||
...
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user