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.controller import BaseController
|
||||||
from fastapi_common.fastapi_common_web.domain.responses import Result
|
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 import IDocumentService
|
||||||
from fastapi_modules.fastapi_leaudit.services.impl.documentServiceImpl import DocumentServiceImpl
|
from fastapi_modules.fastapi_leaudit.services.impl.documentServiceImpl import DocumentServiceImpl
|
||||||
|
|
||||||
@@ -95,6 +101,30 @@ class DocumentController(BaseController):
|
|||||||
Data = await self.DocumentService.ListDocumentTypes(Ids=idList)
|
Data = await self.DocumentService.ListDocumentTypes(Ids=idList)
|
||||||
return Result.success(data=Data)
|
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])
|
@self.router.get("/v2/system/queue/status", response_model=Result[QueueStatusVO])
|
||||||
async def GetQueueStatus():
|
async def GetQueueStatus():
|
||||||
"""获取文档处理队列状态。"""
|
"""获取文档处理队列状态。"""
|
||||||
|
|||||||
@@ -80,6 +80,33 @@ class DocumentTypeItemVO(BaseModel):
|
|||||||
id: int = Field(..., description="类型ID")
|
id: int = Field(..., description="类型ID")
|
||||||
name: str = Field(..., description="类型名称")
|
name: str = Field(..., description="类型名称")
|
||||||
code: 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):
|
class DocumentListPageVO(BaseModel):
|
||||||
|
|||||||
@@ -2,7 +2,13 @@
|
|||||||
|
|
||||||
from abc import ABC, abstractmethod
|
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):
|
class IDocumentService(ABC):
|
||||||
@@ -46,3 +52,23 @@ class IDocumentService(ABC):
|
|||||||
async def ListDocumentTypes(self, Ids: list[int] | None = None) -> list[DocumentTypeItemVO]:
|
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,
|
DocumentHistoryVersionVO,
|
||||||
DocumentListItemVO,
|
DocumentListItemVO,
|
||||||
DocumentListPageVO,
|
DocumentListPageVO,
|
||||||
|
DocumentTypeCreateDTO,
|
||||||
DocumentTypeItemVO,
|
DocumentTypeItemVO,
|
||||||
|
DocumentTypeUpdateDTO,
|
||||||
DocumentUploadVO,
|
DocumentUploadVO,
|
||||||
)
|
)
|
||||||
from fastapi_modules.fastapi_leaudit.models import LeauditDocument, LeauditDocumentFile
|
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 def ListDocumentTypes(self, Ids: list[int] | None = None) -> list[DocumentTypeItemVO]:
|
||||||
"""获取文档类型列表。"""
|
"""获取文档类型列表。"""
|
||||||
async with GetAsyncSession() as Session:
|
async with GetAsyncSession() as Session:
|
||||||
if Ids:
|
rows, bindingsMap = await self._queryDocumentTypes(Session, 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()
|
|
||||||
return [
|
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
|
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(
|
async def _find_latest_version_candidate(
|
||||||
session,
|
session,
|
||||||
|
|||||||
Reference in New Issue
Block a user