diff --git a/fastapi_modules/fastapi_leaudit/controllers/documentController.py b/fastapi_modules/fastapi_leaudit/controllers/documentController.py index a42ad32..2450d01 100644 --- a/fastapi_modules/fastapi_leaudit/controllers/documentController.py +++ b/fastapi_modules/fastapi_leaudit/controllers/documentController.py @@ -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(): """获取文档处理队列状态。""" diff --git a/fastapi_modules/fastapi_leaudit/domian/vo/documentVo.py b/fastapi_modules/fastapi_leaudit/domian/vo/documentVo.py index f1846d6..8f98319 100644 --- a/fastapi_modules/fastapi_leaudit/domian/vo/documentVo.py +++ b/fastapi_modules/fastapi_leaudit/domian/vo/documentVo.py @@ -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): diff --git a/fastapi_modules/fastapi_leaudit/services/documentService.py b/fastapi_modules/fastapi_leaudit/services/documentService.py index 61b0d86..6388bfe 100644 --- a/fastapi_modules/fastapi_leaudit/services/documentService.py +++ b/fastapi_modules/fastapi_leaudit/services/documentService.py @@ -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: + """删除文档类型(软删除)。""" + ... diff --git a/fastapi_modules/fastapi_leaudit/services/impl/documentServiceImpl.py b/fastapi_modules/fastapi_leaudit/services/impl/documentServiceImpl.py index 8620bc9..fa56eaa 100644 --- a/fastapi_modules/fastapi_leaudit/services/impl/documentServiceImpl.py +++ b/fastapi_modules/fastapi_leaudit/services/impl/documentServiceImpl.py @@ -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,