feat: complete review detail backend chain

This commit is contained in:
wren
2026-05-06 09:19:03 +08:00
parent 23b5445ff8
commit 2d3a0f31de
4 changed files with 2066 additions and 22 deletions
@@ -2,21 +2,30 @@
from typing import Any from typing import Any
from fastapi import File, Form, Query, UploadFile from fastapi import Depends, File, Form, Query, UploadFile
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from sqlalchemy import text from sqlalchemy import text
from fastapi_common.fastapi_common_sqlalchemy.database import GetAsyncSession 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_common.fastapi_common_security.security import verify_access_token
from fastapi_modules.fastapi_leaudit.domian.vo.documentVo import ( from fastapi_modules.fastapi_leaudit.domian.vo.documentVo import (
DocumentDetailVO,
DocumentListPageVO, DocumentListPageVO,
DocumentStatusItemVO,
DocumentUpdateDTO,
DocumentTypeCreateDTO, DocumentTypeCreateDTO,
DocumentTypeItemVO, DocumentTypeItemVO,
DocumentTypeUpdateDTO, DocumentTypeUpdateDTO,
DocumentUploadVO, DocumentUploadVO,
) )
from fastapi_modules.fastapi_leaudit.domian.vo.reviewPointVo import (
DocumentConfirmVO,
ReviewPointAuditVO,
ReviewPointsAggregateVO,
)
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
@@ -28,6 +37,11 @@ class QueueStatusVO(BaseModel):
documents: dict[str, object] = Field(default_factory=lambda: {"waiting": 0, "processing": 0, "processing_ids": []}) documents: dict[str, object] = Field(default_factory=lambda: {"waiting": 0, "processing": 0, "processing_ids": []})
class ReviewPointAuditDTO(BaseModel):
result: str = Field(..., description="true/false/review")
message: str = Field("", description="审核意见")
class DocumentController(BaseController): class DocumentController(BaseController):
"""文档控制器。""" """文档控制器。"""
@@ -38,25 +52,40 @@ class DocumentController(BaseController):
@self.router.post("/upload", response_model=Result[DocumentUploadVO]) @self.router.post("/upload", response_model=Result[DocumentUploadVO])
async def UploadDocument( async def UploadDocument(
file: UploadFile = File(..., description="上传文档"), file: UploadFile = File(..., description="上传文档"),
attachments: list[UploadFile] | None = File(None, description="合同附件列表"),
typeId: int | None = Form(None, description="文档类型ID"), typeId: int | None = Form(None, description="文档类型ID"),
typeCode: str | None = Form(None, description="文档类型编码"), typeCode: str | None = Form(None, description="文档类型编码"),
groupId: int | None = Form(None, description="二级分组ID"),
region: str = Form("default", description="所属地区"), region: str = Form("default", description="所属地区"),
fileRole: str = Form("primary", description="文件角色"), fileRole: str = Form("primary", description="文件角色"),
createdBy: int | None = Form(None, description="上传用户ID"), createdBy: int | None = Form(None, description="上传用户ID"),
autoRun: bool = Form(False, description="是否上传后自动触发评查"), autoRun: bool = Form(False, description="是否上传后自动触发评查"),
speed: str = Form("normal", description="执行速度档位:urgent/normal"), speed: str = Form("normal", description="执行速度档位:urgent/normal"),
payload: dict[str, Any] = Depends(verify_access_token),
): ):
"""上传文档并建立评查输入。""" """上传文档并建立评查输入。"""
Content = await file.read() Content = await file.read()
attachmentPayloads: list[tuple[str, bytes, str | None]] = []
for attachment in attachments or []:
attachmentContent = await attachment.read()
attachmentPayloads.append(
(
attachment.filename or "attachment.bin",
attachmentContent,
attachment.content_type,
)
)
Data = await self.DocumentService.Upload( Data = await self.DocumentService.Upload(
FileName=file.filename or "upload.bin", FileName=file.filename or "upload.bin",
FileContent=Content, FileContent=Content,
ContentType=file.content_type, ContentType=file.content_type,
TypeId=typeId, TypeId=typeId,
TypeCode=typeCode, TypeCode=typeCode,
GroupId=groupId,
Region=region, Region=region,
FileRole=fileRole, FileRole=fileRole,
CreatedBy=createdBy, CreatedBy=int(payload["user_id"]),
Attachments=attachmentPayloads,
AutoRun=autoRun, AutoRun=autoRun,
Speed=speed, Speed=speed,
) )
@@ -67,38 +96,145 @@ class DocumentController(BaseController):
page: int = 1, page: int = 1,
pageSize: int = 20, pageSize: int = 20,
keyword: str | None = None, keyword: str | None = None,
documentNumber: str | None = None,
typeCode: str | None = None, typeCode: str | None = None,
type_ids: str | None = Query(None, description="逗号分隔的文档类型ID列表"),
entry_module_id: int | None = Query(None, description="按入口模块ID过滤文档"),
region: str | None = None, region: str | None = None,
processingStatus: str | None = None, processingStatus: str | None = None,
resultStatus: str | None = None, resultStatus: str | None = None,
auditStatus: int | None = Query(None, description="按人工审核状态过滤"),
userId: int | None = Query(None, description="按用户ID过滤"), userId: int | None = Query(None, description="按用户ID过滤"),
dateFrom: str | None = Query(None, description="起始日期 (YYYY-MM-DD)"), dateFrom: str | None = Query(None, description="起始日期 (YYYY-MM-DD)"),
dateTo: str | None = Query(None, description="结束日期 (YYYY-MM-DD)"), dateTo: str | None = Query(None, description="结束日期 (YYYY-MM-DD)"),
payload: dict[str, Any] = Depends(verify_access_token),
): ):
"""获取文档列表(仅返回最新版本,附历史版本摘要)。""" """获取文档列表(带数据隔离:省级全量、地市仅本地区、普通用户仅自己)。"""
typeIdList: list[int] | None = None
if type_ids:
typeIdList = [int(x.strip()) for x in type_ids.split(",") if x.strip().isdigit()]
Data = await self.DocumentService.ListDocuments( Data = await self.DocumentService.ListDocuments(
CurrentUserId=int(payload["user_id"]),
Page=page, Page=page,
PageSize=pageSize, PageSize=pageSize,
Keyword=keyword, Keyword=keyword,
DocumentNumber=documentNumber,
TypeCode=typeCode, TypeCode=typeCode,
TypeIds=typeIdList,
EntryModuleId=entry_module_id,
Region=region, Region=region,
ProcessingStatus=processingStatus, ProcessingStatus=processingStatus,
ResultStatus=resultStatus, ResultStatus=resultStatus,
AuditStatus=auditStatus,
UserId=userId, UserId=userId,
DateFrom=dateFrom, DateFrom=dateFrom,
DateTo=dateTo, DateTo=dateTo,
) )
return Result.success(data=Data) return Result.success(data=Data)
@self.router.get("/documents/status", response_model=Result[list[DocumentStatusItemVO]])
async def GetDocumentsStatus(
ids: str = Query(..., description="逗号分隔的文档ID列表"),
payload: dict[str, Any] = Depends(verify_access_token),
):
"""批量获取文档状态(带数据隔离校验)。"""
idList = [int(x.strip()) for x in ids.split(",") if x.strip().isdigit()]
Data = await self.DocumentService.GetDocumentsStatus(CurrentUserId=int(payload["user_id"]), Ids=idList)
return Result.success(data=Data)
@self.router.get("/documents/{DocumentId}", response_model=Result[DocumentDetailVO])
async def GetDocument(
DocumentId: int,
payload: dict[str, Any] = Depends(verify_access_token),
):
"""获取单个文档详情(带数据隔离校验)。"""
Data = await self.DocumentService.GetDocument(CurrentUserId=int(payload["user_id"]), Id=DocumentId)
return Result.success(data=Data)
@self.router.get("/v3/review-points/{DocumentId}", response_model=Result[ReviewPointsAggregateVO])
async def GetReviewPoints(
DocumentId: int,
payload: dict[str, Any] = Depends(verify_access_token),
):
"""获取评查详情页聚合数据(带数据隔离校验)。"""
Data = await self.DocumentService.GetReviewPoints(CurrentUserId=int(payload["user_id"]), DocumentId=DocumentId)
return Result.success(data=Data)
@self.router.patch("/v3/review-points/{ReviewPointResultId}/audit", response_model=Result[ReviewPointAuditVO])
async def AuditReviewPoint(
ReviewPointResultId: int,
Body: ReviewPointAuditDTO,
payload: dict[str, Any] = Depends(verify_access_token),
):
"""更新单个评查点的人工审核状态。"""
Data = await self.DocumentService.AuditReviewPoint(
CurrentUserId=int(payload["user_id"]),
ReviewPointResultId=ReviewPointResultId,
Result=Body.result,
Message=Body.message,
)
return Result.success(data=Data, message="评查点审核状态已更新")
@self.router.patch("/v3/documents/{DocumentId}/confirm", response_model=Result[DocumentConfirmVO])
async def ConfirmReviewResults(
DocumentId: int,
payload: dict[str, Any] = Depends(verify_access_token),
):
"""确认文档评查结果。"""
Data = await self.DocumentService.ConfirmReviewResults(CurrentUserId=int(payload["user_id"]), DocumentId=DocumentId)
return Result.success(data=Data, message="评查结果已确认")
@self.router.post("/documents/{DocumentId}/attachments", response_model=Result[DocumentDetailVO])
async def AppendAttachments(
DocumentId: int,
files: list[UploadFile] = File(..., description="附件文件列表"),
payload: dict[str, Any] = Depends(verify_access_token),
):
"""为现有文档追加附件(带数据隔离校验)。"""
filePayloads: list[tuple[str, bytes, str | None]] = []
for file in files:
content = await file.read()
filePayloads.append((file.filename or "attachment.bin", content, file.content_type))
Data = await self.DocumentService.AppendAttachments(
CurrentUserId=int(payload["user_id"]),
Id=DocumentId,
Files=filePayloads,
)
return Result.success(data=Data, message="附件上传成功")
@self.router.put("/documents/{DocumentId}", response_model=Result[DocumentDetailVO])
async def UpdateDocument(
DocumentId: int,
Body: DocumentUpdateDTO,
payload: dict[str, Any] = Depends(verify_access_token),
):
"""更新文档元数据(带数据隔离校验)。"""
Data = await self.DocumentService.UpdateDocument(
CurrentUserId=int(payload["user_id"]),
Id=DocumentId,
Body=Body,
)
return Result.success(data=Data, message="文档更新成功")
@self.router.delete("/documents/{DocumentId}", response_model=Result[None])
async def DeleteDocument(
DocumentId: int,
payload: dict[str, Any] = Depends(verify_access_token),
):
"""软删除文档(带数据隔离校验)。"""
await self.DocumentService.DeleteDocument(CurrentUserId=int(payload["user_id"]), Id=DocumentId)
return Result.success(message="文档已删除")
@self.router.get("/document-types", response_model=Result[list[DocumentTypeItemVO]]) @self.router.get("/document-types", response_model=Result[list[DocumentTypeItemVO]])
async def ListDocumentTypes( async def ListDocumentTypes(
ids: str | None = Query(None, description="逗号分隔的ID列表,不传则返回全部"), ids: str | None = Query(None, description="逗号分隔的ID列表,不传则返回全部"),
entry_module_id: int | None = Query(None, description="按入口模块ID过滤文档类型"),
): ):
"""获取文档类型列表。""" """获取文档类型列表。"""
idList: list[int] | None = None idList: list[int] | None = None
if ids: if ids:
idList = [int(x.strip()) for x in ids.split(",") if x.strip().isdigit()] idList = [int(x.strip()) for x in ids.split(",") if x.strip().isdigit()]
Data = await self.DocumentService.ListDocumentTypes(Ids=idList) Data = await self.DocumentService.ListDocumentTypes(Ids=idList, EntryModuleId=entry_module_id)
return Result.success(data=Data) return Result.success(data=Data)
@self.router.get("/document-types/{TypeId}", response_model=Result[DocumentTypeItemVO]) @self.router.get("/document-types/{TypeId}", response_model=Result[DocumentTypeItemVO])
@@ -0,0 +1,85 @@
"""评查详情聚合 VO。"""
from __future__ import annotations
from typing import Any
from pydantic import BaseModel, Field
class ReviewPointResultVO(BaseModel):
"""前端评查详情页所需的单条评查点结果。"""
id: str | int = Field(..., description="评查结果ID")
documentId: str = Field(..., description="文档ID")
pointId: str | int = Field(..., description="评查点/规则ID")
editAuditStatusId: str | int = Field("", description="人工审核状态ID")
editAuditStatus: int = Field(0, description="人工审核状态")
editAuditStatusMessage: str = Field("", description="人工审核意见")
title: str = Field("", description="标题")
pointName: str = Field("", description="评查点名称")
pointCode: str = Field("", description="评查点编码")
groupName: str = Field("", description="所属分组名")
status: str = Field("success", description="success/warning/error/notApplicable")
content: dict[str, Any] = Field(default_factory=dict, description="抽取字段内容")
contentPage: dict[str, str] = Field(default_factory=dict, description="字段页码映射")
suggestion: str = Field("", description="建议内容")
postAction: str = Field("", description="后续动作")
actionContent: Any = Field(default_factory=dict, description="动作配置")
legalBasis: Any = Field(default_factory=dict, description="法律依据")
evaluationConfig: Any = Field(default_factory=dict, description="评查配置")
score: float = Field(0, description="分值")
finalScore: float | None = Field(None, description="最终得分")
machineScore: float | None = Field(None, description="机器得分")
result: bool | None = Field(None, description="是否通过")
failMessage: str = Field("", description="失败提示")
passMessage: str = Field("", description="通过提示")
evaluatedPointResultsLog: dict[str, Any] = Field(default_factory=dict, description="规则执行日志")
class ReviewPointStatsVO(BaseModel):
"""统计信息。"""
total: int = Field(0, description="总数")
success: int = Field(0, description="通过数")
warning: int = Field(0, description="警告数")
error: int = Field(0, description="错误数")
score: float = Field(0, description="总分")
class ReviewPointInfoVO(BaseModel):
"""评查摘要信息。"""
reviewTime: str = Field("", description="评查时间")
reviewModel: str = Field("DeepSeek", description="评查模型")
ruleGroup: str = Field("", description="规则组")
result: str = Field("success", description="总体结果")
issueCount: int = Field(0, description="问题数")
class ReviewPointsAggregateVO(BaseModel):
"""评查详情聚合响应。"""
data: list[ReviewPointResultVO] = Field(default_factory=list, description="评查点结果")
stats: ReviewPointStatsVO = Field(default_factory=ReviewPointStatsVO, description="统计信息")
reviewInfo: ReviewPointInfoVO = Field(default_factory=ReviewPointInfoVO, description="评查摘要")
document: dict[str, Any] | None = Field(default=None, description="文档信息")
comparison_document: dict[str, Any] | None = Field(default=None, description="比对文档信息")
scoring_proposals: list[dict[str, Any]] = Field(default_factory=list, description="评分提案")
class ReviewPointAuditVO(BaseModel):
"""评查点人工审核更新结果。"""
reviewPointResultId: int = Field(..., description="规则结果ID")
editAuditStatusId: int = Field(..., description="审核记录ID")
editAuditStatus: int = Field(..., description="审核状态")
overrideResult: bool | None = Field(default=None, description="人工覆盖结果")
message: str = Field("", description="审核意见")
class DocumentConfirmVO(BaseModel):
"""文档确认评查结果。"""
documentId: int = Field(..., description="文档ID")
auditStatus: int = Field(..., description="文档审核状态")
@@ -3,12 +3,20 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from fastapi_modules.fastapi_leaudit.domian.vo.documentVo import ( from fastapi_modules.fastapi_leaudit.domian.vo.documentVo import (
DocumentDetailVO,
DocumentListPageVO, DocumentListPageVO,
DocumentStatusItemVO,
DocumentUpdateDTO,
DocumentTypeCreateDTO, DocumentTypeCreateDTO,
DocumentTypeItemVO, DocumentTypeItemVO,
DocumentTypeUpdateDTO, DocumentTypeUpdateDTO,
DocumentUploadVO, DocumentUploadVO,
) )
from fastapi_modules.fastapi_leaudit.domian.vo.reviewPointVo import (
DocumentConfirmVO,
ReviewPointAuditVO,
ReviewPointsAggregateVO,
)
class IDocumentService(ABC): class IDocumentService(ABC):
@@ -22,9 +30,11 @@ class IDocumentService(ABC):
ContentType: str | None, ContentType: str | None,
TypeId: int | None = None, TypeId: int | None = None,
TypeCode: str | None = None, TypeCode: str | None = None,
GroupId: int | None = None,
Region: str = "default", Region: str = "default",
FileRole: str = "primary", FileRole: str = "primary",
CreatedBy: int | None = None, CreatedBy: int | None = None,
Attachments: list[tuple[str, bytes, str | None]] | None = None,
AutoRun: bool = False, AutoRun: bool = False,
Speed: str = "normal", Speed: str = "normal",
) -> DocumentUploadVO: ) -> DocumentUploadVO:
@@ -34,13 +44,18 @@ class IDocumentService(ABC):
@abstractmethod @abstractmethod
async def ListDocuments( async def ListDocuments(
self, self,
CurrentUserId: int,
Page: int = 1, Page: int = 1,
PageSize: int = 20, PageSize: int = 20,
Keyword: str | None = None, Keyword: str | None = None,
DocumentNumber: str | None = None,
TypeCode: str | None = None, TypeCode: str | None = None,
TypeIds: list[int] | None = None,
EntryModuleId: int | None = None,
Region: str | None = None, Region: str | None = None,
ProcessingStatus: str | None = None, ProcessingStatus: str | None = None,
ResultStatus: str | None = None, ResultStatus: str | None = None,
AuditStatus: int | None = None,
UserId: int | None = None, UserId: int | None = None,
DateFrom: str | None = None, DateFrom: str | None = None,
DateTo: str | None = None, DateTo: str | None = None,
@@ -49,7 +64,58 @@ class IDocumentService(ABC):
... ...
@abstractmethod @abstractmethod
async def ListDocumentTypes(self, Ids: list[int] | None = None) -> list[DocumentTypeItemVO]: async def GetDocument(self, CurrentUserId: int, Id: int) -> DocumentDetailVO:
"""获取单个文档详情,并执行数据隔离校验。"""
...
@abstractmethod
async def GetReviewPoints(self, CurrentUserId: int, DocumentId: int) -> ReviewPointsAggregateVO:
"""获取评查详情页所需的聚合数据。"""
...
@abstractmethod
async def AuditReviewPoint(
self,
CurrentUserId: int,
ReviewPointResultId: int,
Result: str,
Message: str,
) -> ReviewPointAuditVO:
"""更新单个评查点的人工审核状态。"""
...
@abstractmethod
async def ConfirmReviewResults(self, CurrentUserId: int, DocumentId: int) -> DocumentConfirmVO:
"""确认文档评查结果。"""
...
@abstractmethod
async def GetDocumentsStatus(self, CurrentUserId: int, Ids: list[int]) -> list[DocumentStatusItemVO]:
"""批量获取文档状态,并执行数据隔离校验。"""
...
@abstractmethod
async def UpdateDocument(self, CurrentUserId: int, Id: int, Body: DocumentUpdateDTO) -> DocumentDetailVO:
"""更新文档元数据,并执行数据隔离校验。"""
...
@abstractmethod
async def DeleteDocument(self, CurrentUserId: int, Id: int) -> None:
"""软删除文档,并执行数据隔离校验。"""
...
@abstractmethod
async def AppendAttachments(
self,
CurrentUserId: int,
Id: int,
Files: list[tuple[str, bytes, str | None]],
) -> DocumentDetailVO:
"""为现有文档追加附件,并执行数据隔离校验。"""
...
@abstractmethod
async def ListDocumentTypes(self, Ids: list[int] | None = None, EntryModuleId: int | None = None) -> list[DocumentTypeItemVO]:
"""获取文档类型列表。""" """获取文档类型列表。"""
... ...
File diff suppressed because it is too large Load Diff