feat: migrate cross review to v3 leaudit flow

This commit is contained in:
wren
2026-05-07 18:18:59 +08:00
parent 35e0c45c42
commit 1c84209f38
23 changed files with 5172 additions and 39 deletions
@@ -0,0 +1,225 @@
"""交叉评查控制器(第一阶段骨架)。"""
from typing import Any
from fastapi import Depends, File, Query, UploadFile
from fastapi.responses import JSONResponse
from fastapi_common.fastapi_common_security.security import verify_access_token
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.Dto.crossReviewDto import (
CrossReviewProposalCreateDTO,
CrossReviewProposalVoteDTO,
CrossReviewTaskCreateDTO,
CrossReviewTaskDocumentQueryDTO,
CrossReviewTaskQueryDTO,
)
from fastapi_modules.fastapi_leaudit.domian.vo.crossReviewVo import (
CrossReviewPendingVotesVO,
CrossReviewPermissionVO,
CrossReviewProposalCancelVO,
CrossReviewProposalCreateVO,
CrossReviewProposalPageVO,
CrossReviewProposalVoteVO,
CrossReviewTaskCompleteVO,
CrossReviewTaskCreateVO,
CrossReviewTaskDocumentPageVO,
CrossReviewTaskDocumentUploadVO,
CrossReviewTaskPageVO,
CrossReviewTaskProgressVO,
)
from fastapi_modules.fastapi_leaudit.services.crossReviewService import ICrossReviewService
from fastapi_modules.fastapi_leaudit.services.impl.crossReviewServiceImpl import CrossReviewServiceImpl
from fastapi_modules.fastapi_leaudit.services.impl.permissionServiceImpl import PermissionServiceImpl
from fastapi_modules.fastapi_leaudit.services.permissionService import IPermissionService
class CrossReviewController(BaseController):
"""交叉评查控制器。"""
_PERMISSIONS = {
"task_create": "cross_review:task:create",
"task_read": "cross_review:task:read",
"progress_view": "cross_review:progress:view",
"document_read": "cross_review:document:read",
"document_complete": "cross_review:document:complete",
"proposal_create": "cross_review:proposal:create",
"proposal_read": "cross_review:proposal:read",
"proposal_delete": "cross_review:proposal:delete",
"proposal_vote": "cross_review:proposal:vote",
}
def __init__(self):
super().__init__(prefix="/v3/cross-review", tags=["交叉评查"])
self.CrossReviewService: ICrossReviewService = CrossReviewServiceImpl()
self.PermissionService: IPermissionService = PermissionServiceImpl()
@self.router.post("/tasks", response_model=Result[CrossReviewTaskCreateVO])
async def CreateTask(
Body: CrossReviewTaskCreateDTO,
payload: dict[str, Any] = Depends(verify_access_token),
):
"""创建交叉评查任务。"""
if not await self._check_permission(int(payload["user_id"]), [self._PERMISSIONS["task_create"]]):
return JSONResponse(status_code=403, content={"code": 403, "msg": "当前用户没有创建交叉评查任务权限", "data": None})
Data = await self.CrossReviewService.CreateTask(CurrentUserId=int(payload["user_id"]), Body=Body)
return Result.success(data=Data, message="交叉评查任务创建成功")
@self.router.post("/tasks/query", response_model=Result[CrossReviewTaskPageVO])
async def GetUserTasks(
Body: CrossReviewTaskQueryDTO,
payload: dict[str, Any] = Depends(verify_access_token),
):
"""查询当前用户参与的交叉评查任务。"""
if not await self._check_permission(int(payload["user_id"]), [self._PERMISSIONS["task_read"]]):
return JSONResponse(status_code=403, content={"code": 403, "msg": "当前用户没有查看交叉评查任务权限", "data": None})
Data = await self.CrossReviewService.GetUserTasks(CurrentUserId=int(payload["user_id"]), Body=Body)
return Result.success(data=Data)
@self.router.get("/tasks/{TaskId}/progress", response_model=Result[CrossReviewTaskProgressVO])
async def GetTaskProgress(
TaskId: int,
payload: dict[str, Any] = Depends(verify_access_token),
):
"""查询任务进度。"""
if not await self._check_permission(int(payload["user_id"]), [self._PERMISSIONS["progress_view"]]):
return JSONResponse(status_code=403, content={"code": 403, "msg": "当前用户没有查看交叉评查任务进度权限", "data": None})
Data = await self.CrossReviewService.GetTaskProgress(CurrentUserId=int(payload["user_id"]), TaskId=TaskId)
return Result.success(data=Data)
@self.router.get("/tasks/{TaskId}/documents", response_model=Result[CrossReviewTaskDocumentPageVO])
async def GetTaskDocuments(
TaskId: int,
page: int = Query(1, ge=1, description="页码"),
pageSize: int = Query(20, ge=1, le=100, description="每页大小"),
keyword: str | None = Query(None, description="关键字"),
payload: dict[str, Any] = Depends(verify_access_token),
):
"""查询任务文档列表。"""
if not await self._check_permission(int(payload["user_id"]), [self._PERMISSIONS["task_read"], self._PERMISSIONS["document_read"]]):
return JSONResponse(status_code=403, content={"code": 403, "msg": "当前用户没有查看交叉评查任务文档权限", "data": None})
Body = CrossReviewTaskDocumentQueryDTO(page=page, pageSize=pageSize, keyword=keyword)
Data = await self.CrossReviewService.GetTaskDocuments(
CurrentUserId=int(payload["user_id"]),
TaskId=TaskId,
Body=Body,
)
return Result.success(data=Data)
@self.router.get("/tasks/{TaskId}/can-confirm", response_model=Result[CrossReviewPermissionVO])
async def CanConfirmTaskDocument(
TaskId: int,
payload: dict[str, Any] = Depends(verify_access_token),
):
"""查询当前用户是否可确认完成。"""
if not await self._check_permission(int(payload["user_id"]), [self._PERMISSIONS["document_complete"]]):
return JSONResponse(status_code=403, content={"code": 403, "msg": "当前用户没有确认交叉评查文档完成权限", "data": None})
Data = await self.CrossReviewService.CanConfirmTaskDocument(CurrentUserId=int(payload["user_id"]), TaskId=TaskId)
return Result.success(data=Data)
@self.router.post("/tasks/{TaskId}/documents/{DocumentId}/complete", response_model=Result[CrossReviewTaskCompleteVO])
async def CompleteTaskDocument(
TaskId: int,
DocumentId: int,
payload: dict[str, Any] = Depends(verify_access_token),
):
"""确认任务文档完成。"""
if not await self._check_permission(int(payload["user_id"]), [self._PERMISSIONS["document_complete"]]):
return JSONResponse(status_code=403, content={"code": 403, "msg": "当前用户没有确认交叉评查文档完成权限", "data": None})
Data = await self.CrossReviewService.CompleteTaskDocument(
CurrentUserId=int(payload["user_id"]),
TaskId=TaskId,
DocumentId=DocumentId,
)
return Result.success(data=Data, message="交叉评查文档已确认完成")
@self.router.post("/tasks/{TaskId}/documents/upload", response_model=Result[CrossReviewTaskDocumentUploadVO])
async def UploadTaskDocument(
TaskId: int,
file: UploadFile = File(..., description="上传文档"),
payload: dict[str, Any] = Depends(verify_access_token),
):
"""向交叉评查任务补传文档。"""
if not await self._check_permission(int(payload["user_id"]), [self._PERMISSIONS["document_complete"]]):
return JSONResponse(status_code=403, content={"code": 403, "msg": "当前用户没有交叉评查任务补传文档权限", "data": None})
content = await file.read()
Data = await self.CrossReviewService.UploadTaskDocument(
CurrentUserId=int(payload["user_id"]),
TaskId=TaskId,
FileName=file.filename or "upload.bin",
FileContent=content,
ContentType=file.content_type,
)
return Result.success(data=Data, message="交叉评查任务文档上传成功")
@self.router.post("/proposals", response_model=Result[CrossReviewProposalCreateVO])
async def CreateProposal(
Body: CrossReviewProposalCreateDTO,
payload: dict[str, Any] = Depends(verify_access_token),
):
"""创建交叉评查提案。"""
if not await self._check_permission(int(payload["user_id"]), [self._PERMISSIONS["proposal_create"]]):
return JSONResponse(status_code=403, content={"code": 403, "msg": "当前用户没有创建交叉评查提案权限", "data": None})
Data = await self.CrossReviewService.CreateProposal(CurrentUserId=int(payload["user_id"]), Body=Body)
return Result.success(data=Data, message="交叉评查提案创建成功")
@self.router.post("/proposals/{ProposalId}/votes", response_model=Result[CrossReviewProposalVoteVO])
async def VoteProposal(
ProposalId: int,
Body: CrossReviewProposalVoteDTO,
payload: dict[str, Any] = Depends(verify_access_token),
):
"""对交叉评查提案投票。"""
if not await self._check_permission(int(payload["user_id"]), [self._PERMISSIONS["proposal_vote"]]):
return JSONResponse(status_code=403, content={"code": 403, "msg": "当前用户没有交叉评查提案投票权限", "data": None})
Data = await self.CrossReviewService.VoteProposal(CurrentUserId=int(payload["user_id"]), ProposalId=ProposalId, Body=Body)
return Result.success(data=Data, message="交叉评查提案投票成功")
@self.router.delete("/proposals/{ProposalId}", response_model=Result[CrossReviewProposalCancelVO])
async def CancelProposal(
ProposalId: int,
payload: dict[str, Any] = Depends(verify_access_token),
):
"""撤销交叉评查提案。"""
if not await self._check_permission(int(payload["user_id"]), [self._PERMISSIONS["proposal_delete"]]):
return JSONResponse(status_code=403, content={"code": 403, "msg": "当前用户没有撤销交叉评查提案权限", "data": None})
Data = await self.CrossReviewService.CancelProposal(CurrentUserId=int(payload["user_id"]), ProposalId=ProposalId)
return Result.success(data=Data, message="交叉评查提案已撤销")
@self.router.get("/documents/{DocumentId}/proposals", response_model=Result[CrossReviewProposalPageVO])
async def GetDocumentProposals(
DocumentId: int,
page: int = Query(1, ge=1, description="页码"),
pageSize: int = Query(20, ge=1, le=100, description="每页大小"),
payload: dict[str, Any] = Depends(verify_access_token),
):
"""获取文档提案列表。"""
if not await self._check_permission(int(payload["user_id"]), [self._PERMISSIONS["proposal_read"]]):
return JSONResponse(status_code=403, content={"code": 403, "msg": "当前用户没有查看交叉评查提案权限", "data": None})
Data = await self.CrossReviewService.GetDocumentProposals(
CurrentUserId=int(payload["user_id"]),
DocumentId=DocumentId,
Page=page,
PageSize=pageSize,
)
return Result.success(data=Data)
@self.router.get("/documents/{DocumentId}/pending-votes", response_model=Result[CrossReviewPendingVotesVO])
async def GetDocumentPendingVotes(
DocumentId: int,
payload: dict[str, Any] = Depends(verify_access_token),
):
"""获取文档待投票摘要。"""
if not await self._check_permission(int(payload["user_id"]), [self._PERMISSIONS["proposal_read"], self._PERMISSIONS["document_complete"]]):
return JSONResponse(status_code=403, content={"code": 403, "msg": "当前用户没有查看待投票信息权限", "data": None})
Data = await self.CrossReviewService.GetDocumentPendingVotes(CurrentUserId=int(payload["user_id"]), DocumentId=DocumentId)
return Result.success(data=Data)
async def _check_permission(self, user_id: int, permission_keys: list[str]) -> bool:
"""OR 逻辑权限校验。"""
for permission_key in permission_keys:
if await self.PermissionService.CheckPermission(user_id, permission_key):
return True
return False
@@ -0,0 +1,52 @@
"""交叉评查 DTO。"""
from __future__ import annotations
from pydantic import BaseModel, Field
class CrossReviewTaskCreateDTO(BaseModel):
"""创建交叉评查任务。"""
taskName: str = Field(..., min_length=1, description="任务名称")
taskType: str = Field("CITY", description="任务类型")
docTypeId: int | None = Field(None, description="文档类型ID")
docTypeCode: str | None = Field(None, description="文档类型编码")
memberUserIds: list[int] = Field(default_factory=list, description="参与成员用户ID")
principalUserIds: list[int] = Field(default_factory=list, description="负责人用户ID")
documentIds: list[int] = Field(default_factory=list, description="挂载文档ID")
class CrossReviewTaskQueryDTO(BaseModel):
"""查询当前用户交叉评查任务。"""
page: int = Field(1, ge=1, description="页码")
pageSize: int = Field(20, ge=1, le=100, description="每页大小")
keyword: str | None = Field(None, description="关键字")
status: str | None = Field(None, description="任务状态")
taskType: str | None = Field(None, description="任务类型")
docTypeCode: str | None = Field(None, description="文档类型编码")
class CrossReviewTaskDocumentQueryDTO(BaseModel):
"""查询任务文档。"""
page: int = Field(1, ge=1, description="页码")
pageSize: int = Field(20, ge=1, le=100, description="每页大小")
keyword: str | None = Field(None, description="关键字")
class CrossReviewProposalCreateDTO(BaseModel):
"""创建交叉评查提案。"""
reviewPointResultId: int = Field(..., description="规则结果ID")
documentId: int = Field(..., description="文档ID")
evaluationPointId: int | None = Field(None, description="评查点ID")
auditOpinion: str = Field(..., min_length=1, description="提案理由")
deductionScore: float = Field(..., description="分值调整量")
class CrossReviewProposalVoteDTO(BaseModel):
"""提案投票。"""
voteType: str = Field(..., description="agree/disagree/cancel")
@@ -0,0 +1,171 @@
"""交叉评查 VO。"""
from __future__ import annotations
from datetime import datetime
from pydantic import BaseModel, Field
class CrossReviewTaskItemVO(BaseModel):
"""任务列表项。"""
taskId: int = Field(..., description="任务ID")
taskName: str = Field(..., description="任务名称")
taskType: str = Field(..., description="任务类型")
docTypeId: int | None = Field(None, description="文档类型ID")
docTypeCode: str | None = Field(None, description="文档类型编码")
status: str = Field(..., description="任务状态")
progress: float = Field(0, description="进度百分比")
totalDocuments: int = Field(0, description="文档总数")
completedDocuments: int = Field(0, description="已完成文档数")
createdAt: datetime | None = Field(None, description="创建时间")
class CrossReviewTaskPageVO(BaseModel):
"""任务分页响应。"""
total: int = Field(0, description="总数")
page: int = Field(1, description="页码")
pageSize: int = Field(20, description="每页大小")
items: list[CrossReviewTaskItemVO] = Field(default_factory=list, description="任务列表")
class CrossReviewTaskProgressVO(BaseModel):
"""任务进度。"""
taskId: int = Field(..., description="任务ID")
totalDocuments: int = Field(0, description="文档总数")
completedDocuments: int = Field(0, description="已完成文档数")
progress: float = Field(0, description="进度百分比")
class CrossReviewTaskDocumentVO(BaseModel):
"""任务文档列表项。"""
documentId: int = Field(..., description="文档ID")
name: str = Field("", description="文档名称")
documentNumber: str | None = Field(None, description="文号")
typeId: int | None = Field(None, description="文档类型ID")
processingStatus: str | None = Field(None, description="处理状态")
versionNo: int = Field(1, description="版本号")
isLatestVersion: bool = Field(True, description="是否最新版本")
auditStatus: int = Field(0, description="任务内完成状态")
createdAt: datetime | None = Field(None, description="创建时间")
class CrossReviewTaskDocumentPageVO(BaseModel):
"""任务文档分页响应。"""
taskId: int = Field(..., description="任务ID")
total: int = Field(0, description="总数")
page: int = Field(1, description="页码")
pageSize: int = Field(20, description="每页大小")
items: list[CrossReviewTaskDocumentVO] = Field(default_factory=list, description="文档列表")
class CrossReviewPermissionVO(BaseModel):
"""是否有权确认完成。"""
canConfirm: bool = Field(False, description="是否可以确认完成")
reason: str = Field("", description="原因")
class CrossReviewTaskCreateVO(BaseModel):
"""创建任务结果。"""
taskId: int = Field(..., description="任务ID")
taskName: str = Field(..., description="任务名称")
memberCount: int = Field(0, description="成员数")
documentCount: int = Field(0, description="挂载文档数")
class CrossReviewTaskCompleteVO(BaseModel):
"""确认任务文档完成结果。"""
taskId: int = Field(..., description="任务ID")
documentId: int = Field(..., description="文档ID")
auditStatus: int = Field(..., description="任务内完成状态")
taskStatus: str = Field(..., description="任务状态")
taskCompleted: bool = Field(False, description="任务是否已全部完成")
class CrossReviewProposalCreateVO(BaseModel):
"""创建提案结果。"""
proposalId: int = Field(..., description="提案ID")
createdAt: datetime | None = Field(None, description="创建时间")
class CrossReviewProposalVoteVO(BaseModel):
"""提案投票结果。"""
proposalId: int = Field(..., description="提案ID")
voterId: int = Field(..., description="投票人ID")
voteType: str = Field(..., description="投票类型")
proposalStatus: str = Field(..., description="提案状态")
class CrossReviewProposalCancelVO(BaseModel):
"""撤销提案结果。"""
proposalId: int = Field(..., description="提案ID")
status: str = Field(..., description="提案状态")
class CrossReviewProposalVoteItemVO(BaseModel):
"""提案已投票明细。"""
voter: str = Field("", description="投票人姓名")
voteType: str = Field(..., description="投票类型")
class CrossReviewProposalItemVO(BaseModel):
"""提案列表项。"""
proposalId: int = Field(..., description="提案ID")
evaluationPointName: str = Field("", description="评查点名称")
proposedScore: float = Field(..., description="调整分值")
reason: str = Field("", description="提案理由")
proposer: str = Field("", description="提案人姓名")
votes: list[CrossReviewProposalVoteItemVO] = Field(default_factory=list, description="已投票明细")
agreeVoters: list[str] = Field(default_factory=list, description="同意人")
disagreeVoters: list[str] = Field(default_factory=list, description="反对人")
pendingVoters: list[str] = Field(default_factory=list, description="待投票人")
canVote: bool = Field(False, description="当前用户是否可投票")
problemMessage: str = Field("", description="问题描述")
proposerId: int = Field(..., description="提案人ID")
createdAt: datetime | None = Field(None, description="创建时间")
status: str = Field("pending", description="提案状态")
class CrossReviewProposalPageVO(BaseModel):
"""提案分页结果。"""
total: int = Field(0, description="总数")
page: int = Field(1, description="页码")
pageSize: int = Field(20, description="每页数量")
items: list[CrossReviewProposalItemVO] = Field(default_factory=list, description="提案列表")
class CrossReviewPendingProposalVO(BaseModel):
"""待投票提案摘要。"""
evaluationPointName: str = Field("", description="评查点名称")
pendingVotersNum: int = Field(0, description="待投票人数")
class CrossReviewPendingVotesVO(BaseModel):
"""文档待投票检查结果。"""
hasPendingVotes: bool = Field(False, description="是否存在待投票")
pendingProposals: list[CrossReviewPendingProposalVO] = Field(default_factory=list, description="待投票提案摘要")
class CrossReviewTaskDocumentUploadVO(BaseModel):
"""交叉评查任务补传文档结果。"""
taskId: int = Field(..., description="任务ID")
documentId: int = Field(..., description="文档ID")
auditStatus: int = Field(0, description="任务内评查状态")
processingStatus: str | None = Field(None, description="文档处理状态")
@@ -3,9 +3,19 @@
from fastapi_modules.fastapi_leaudit.models.leauditDocument import LeauditDocument
from fastapi_modules.fastapi_leaudit.models.leauditDocumentFile import LeauditDocumentFile
from fastapi_modules.fastapi_leaudit.models.leauditAuditRun import LeauditAuditRun
from fastapi_modules.fastapi_leaudit.models.leauditCrossReviewProposal import LeauditCrossReviewProposal
from fastapi_modules.fastapi_leaudit.models.leauditCrossReviewTask import LeauditCrossReviewTask
from fastapi_modules.fastapi_leaudit.models.leauditCrossReviewTaskDocument import LeauditCrossReviewTaskDocument
from fastapi_modules.fastapi_leaudit.models.leauditCrossReviewTaskMember import LeauditCrossReviewTaskMember
from fastapi_modules.fastapi_leaudit.models.leauditCrossReviewVote import LeauditCrossReviewVote
__all__ = [
"LeauditDocument",
"LeauditDocumentFile",
"LeauditAuditRun",
"LeauditCrossReviewTask",
"LeauditCrossReviewTaskMember",
"LeauditCrossReviewTaskDocument",
"LeauditCrossReviewProposal",
"LeauditCrossReviewVote",
]
@@ -0,0 +1,23 @@
"""LeAudit 交叉评查提案模型。"""
from __future__ import annotations
from sqlalchemy import BigInteger, Numeric, String, Text
from sqlalchemy.orm import Mapped, mapped_column
from fastapi_common.fastapi_common_web.models import BaseModel
class LeauditCrossReviewProposal(BaseModel):
"""交叉评查提案表。"""
__tablename__ = "leaudit_cross_review_proposals"
Id: Mapped[int] = mapped_column("id", BigInteger, primary_key=True, autoincrement=True)
taskId: Mapped[int] = mapped_column("task_id", BigInteger, comment="任务ID")
documentId: Mapped[int] = mapped_column("document_id", BigInteger, comment="文档ID")
ruleResultId: Mapped[int] = mapped_column("rule_result_id", BigInteger, comment="结果项ID")
proposerId: Mapped[int] = mapped_column("proposer_id", BigInteger, comment="提案人ID")
proposedScoreDelta: Mapped[float] = mapped_column("proposed_score_delta", Numeric(10, 2), comment="分数变化量")
reason: Mapped[str] = mapped_column(Text, comment="提案理由")
status: Mapped[str] = mapped_column(String(32), default="pending", comment="pending/approved/rejected")
@@ -0,0 +1,22 @@
"""LeAudit 交叉评查任务模型。"""
from __future__ import annotations
from sqlalchemy import BigInteger, String
from sqlalchemy.orm import Mapped, mapped_column
from fastapi_common.fastapi_common_web.models import BaseModel
class LeauditCrossReviewTask(BaseModel):
"""交叉评查任务主表。"""
__tablename__ = "leaudit_cross_review_tasks"
Id: Mapped[int] = mapped_column("id", BigInteger, primary_key=True, autoincrement=True)
taskName: Mapped[str] = mapped_column("task_name", String(255), comment="任务名称")
taskType: Mapped[str] = mapped_column("task_type", String(32), comment="任务类型")
docTypeId: Mapped[int | None] = mapped_column("doc_type_id", BigInteger, comment="文档类型ID")
docTypeCode: Mapped[str | None] = mapped_column("doc_type_code", String(64), comment="文档类型编码")
assignerId: Mapped[int] = mapped_column("assigner_id", BigInteger, comment="创建者ID")
status: Mapped[str] = mapped_column("status", String(32), default="in_progress", comment="任务状态")
@@ -0,0 +1,19 @@
"""LeAudit 交叉评查任务文档模型。"""
from __future__ import annotations
from sqlalchemy import BigInteger, Integer
from sqlalchemy.orm import Mapped, mapped_column
from fastapi_common.fastapi_common_web.models import BaseModel
class LeauditCrossReviewTaskDocument(BaseModel):
"""交叉评查任务文档挂载表。"""
__tablename__ = "leaudit_cross_review_task_documents"
Id: Mapped[int] = mapped_column("id", BigInteger, primary_key=True, autoincrement=True)
taskId: Mapped[int] = mapped_column("task_id", BigInteger, comment="任务ID")
documentId: Mapped[int] = mapped_column("document_id", BigInteger, comment="文档ID")
auditStatus: Mapped[int] = mapped_column("audit_status", Integer, default=0, comment="0=未完成,1=已完成")
@@ -0,0 +1,19 @@
"""LeAudit 交叉评查任务成员模型。"""
from __future__ import annotations
from sqlalchemy import BigInteger, String
from sqlalchemy.orm import Mapped, mapped_column
from fastapi_common.fastapi_common_web.models import BaseModel
class LeauditCrossReviewTaskMember(BaseModel):
"""交叉评查任务成员表。"""
__tablename__ = "leaudit_cross_review_task_members"
Id: Mapped[int] = mapped_column("id", BigInteger, primary_key=True, autoincrement=True)
taskId: Mapped[int] = mapped_column("task_id", BigInteger, comment="任务ID")
userId: Mapped[int] = mapped_column("user_id", BigInteger, comment="用户ID")
memberRole: Mapped[str] = mapped_column("member_role", String(32), default="participant", comment="participant/principal")
@@ -0,0 +1,19 @@
"""LeAudit 交叉评查投票模型。"""
from __future__ import annotations
from sqlalchemy import BigInteger, String
from sqlalchemy.orm import Mapped, mapped_column
from fastapi_common.fastapi_common_web.models import BaseModel
class LeauditCrossReviewVote(BaseModel):
"""交叉评查投票表。"""
__tablename__ = "leaudit_cross_review_votes"
Id: Mapped[int] = mapped_column("id", BigInteger, primary_key=True, autoincrement=True)
proposalId: Mapped[int] = mapped_column("proposal_id", BigInteger, comment="提案ID")
voterId: Mapped[int] = mapped_column("voter_id", BigInteger, comment="投票用户ID")
voteType: Mapped[str] = mapped_column("vote_type", String(16), comment="agree/disagree/cancel")
@@ -1,6 +1,7 @@
"""LeAudit 服务层导出。"""
from fastapi_modules.fastapi_leaudit.services.auditService import IAuditService
from fastapi_modules.fastapi_leaudit.services.crossReviewService import ICrossReviewService
from fastapi_modules.fastapi_leaudit.services.documentService import IDocumentService
from fastapi_modules.fastapi_leaudit.services.evaluationPointService import IEvaluationPointService
from fastapi_modules.fastapi_leaudit.services.entryModuleAdminService import IEntryModuleAdminService
@@ -16,6 +17,7 @@ from fastapi_modules.fastapi_leaudit.services.ruleService import IRuleService
__all__ = [
"IAuditService",
"ICrossReviewService",
"IDocumentService",
"IEvaluationPointService",
"IEntryModuleAdminService",
@@ -0,0 +1,112 @@
"""交叉评查服务接口。"""
from abc import ABC, abstractmethod
from fastapi_modules.fastapi_leaudit.domian.Dto.crossReviewDto import (
CrossReviewProposalCreateDTO,
CrossReviewProposalVoteDTO,
CrossReviewTaskCreateDTO,
CrossReviewTaskDocumentQueryDTO,
CrossReviewTaskQueryDTO,
)
from fastapi_modules.fastapi_leaudit.domian.vo.crossReviewVo import (
CrossReviewPendingVotesVO,
CrossReviewPermissionVO,
CrossReviewProposalCancelVO,
CrossReviewProposalCreateVO,
CrossReviewProposalPageVO,
CrossReviewProposalVoteVO,
CrossReviewTaskCompleteVO,
CrossReviewTaskCreateVO,
CrossReviewTaskDocumentPageVO,
CrossReviewTaskDocumentUploadVO,
CrossReviewTaskPageVO,
CrossReviewTaskProgressVO,
)
class ICrossReviewService(ABC):
"""交叉评查服务接口。"""
@abstractmethod
async def CreateTask(self, CurrentUserId: int, Body: CrossReviewTaskCreateDTO) -> CrossReviewTaskCreateVO:
"""创建交叉评查任务。"""
...
@abstractmethod
async def GetUserTasks(self, CurrentUserId: int, Body: CrossReviewTaskQueryDTO) -> CrossReviewTaskPageVO:
"""查询当前用户参与的交叉评查任务。"""
...
@abstractmethod
async def GetTaskProgress(self, CurrentUserId: int, TaskId: int) -> CrossReviewTaskProgressVO:
"""查询任务进度。"""
...
@abstractmethod
async def GetTaskDocuments(
self,
CurrentUserId: int,
TaskId: int,
Body: CrossReviewTaskDocumentQueryDTO,
) -> CrossReviewTaskDocumentPageVO:
"""查询任务文档列表。"""
...
@abstractmethod
async def CanConfirmTaskDocument(self, CurrentUserId: int, TaskId: int) -> CrossReviewPermissionVO:
"""判断当前用户是否有权确认任务文档完成。"""
...
@abstractmethod
async def CompleteTaskDocument(self, CurrentUserId: int, TaskId: int, DocumentId: int) -> CrossReviewTaskCompleteVO:
"""确认任务文档完成。"""
...
@abstractmethod
async def CreateProposal(self, CurrentUserId: int, Body: CrossReviewProposalCreateDTO) -> CrossReviewProposalCreateVO:
"""创建交叉评查提案。"""
...
@abstractmethod
async def VoteProposal(
self,
CurrentUserId: int,
ProposalId: int,
Body: CrossReviewProposalVoteDTO,
) -> CrossReviewProposalVoteVO:
"""对交叉评查提案投票。"""
...
@abstractmethod
async def CancelProposal(self, CurrentUserId: int, ProposalId: int) -> CrossReviewProposalCancelVO:
"""撤销交叉评查提案。"""
...
@abstractmethod
async def GetDocumentProposals(
self,
CurrentUserId: int,
DocumentId: int,
Page: int,
PageSize: int,
) -> CrossReviewProposalPageVO:
"""获取文档提案列表。"""
...
@abstractmethod
async def GetDocumentPendingVotes(self, CurrentUserId: int, DocumentId: int) -> CrossReviewPendingVotesVO:
"""获取文档待投票信息。"""
...
@abstractmethod
async def UploadTaskDocument(
self,
CurrentUserId: int,
TaskId: int,
FileName: str,
FileContent: bytes,
ContentType: str | None,
) -> CrossReviewTaskDocumentUploadVO:
"""向交叉评查任务补传文档。"""
...
File diff suppressed because it is too large Load Diff
@@ -1128,6 +1128,7 @@ class DocumentServiceImpl(IDocumentService):
)
).scalar_one()
await self._syncRuleBindings(Session, int(row), Body.ruleSetIds, "default")
await sync_group_bindings_from_doc_type(Session, int(row), Body.ruleSetIds)
await Session.commit()
return await self.GetDocumentType(int(row))
@@ -1170,6 +1171,7 @@ class DocumentServiceImpl(IDocumentService):
if "ruleSetIds" in providedFields and Body.ruleSetIds is not None:
await self._syncRuleBindings(Session, Id, Body.ruleSetIds, "default")
await sync_group_bindings_from_doc_type(Session, Id, Body.ruleSetIds)
await Session.commit()
return await self.GetDocumentType(Id)
@@ -1186,6 +1188,7 @@ class DocumentServiceImpl(IDocumentService):
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 ListDocumentTypeRoots(self, EntryModuleId: int | None = None) -> list[DocumentTypeRootItemVO]:
@@ -1331,36 +1334,17 @@ class DocumentServiceImpl(IDocumentService):
await Session.execute(
text(
"""
SELECT
child.document_type_id AS doc_type_id,
rgb.rule_set_id,
child.sort_order AS child_sort_order,
rgb.priority,
rgb.id
FROM leaudit_evaluation_point_groups child
JOIN leaudit_rule_group_bindings rgb
ON rgb.group_id = child.id
AND rgb.deleted_at IS NULL
AND rgb.is_active = TRUE
WHERE child.document_type_id = ANY(:ids)
AND child.deleted_at IS NULL
AND COALESCE(child.pid, 0) <> 0
ORDER BY
child.document_type_id ASC,
COALESCE(child.sort_order, 0) ASC,
COALESCE(rgb.priority, 0) DESC,
rgb.id ASC
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 docTypeId, ruleSetId, *_ in bindingRows:
current = bindingsMap.setdefault(int(docTypeId), [])
normalizedRuleSetId = int(ruleSetId)
if normalizedRuleSetId not in current:
current.append(normalizedRuleSetId)
for b in bindingRows:
bindingsMap.setdefault(int(b[0]), []).append(int(b[1]))
return rows, bindingsMap
async def _queryDocumentTypeRoots(self, Session, Ids: list[int] | None = None, EntryModuleId: int | None = None):
@@ -2198,6 +2182,33 @@ class DocumentServiceImpl(IDocumentService):
async def _loadScoringProposals(self, Session, DocumentId: int) -> list[dict[str, Any]]:
"""读取交叉评分提案;缺表时降级为空。"""
if await self._tableExists(Session, "leaudit_cross_review_proposals"):
rows = (
await Session.execute(
text(
"""
SELECT
id,
rule_result_id AS evaluation_result_id,
proposer_id,
proposed_score_delta AS proposed_score,
reason,
status,
create_time AS created_at,
update_time AS updated_at,
document_id
FROM leaudit_cross_review_proposals
WHERE document_id = :document_id
AND delete_time IS NULL
ORDER BY id DESC
"""
),
{"document_id": DocumentId},
)
).mappings().all()
if rows:
return [dict(row) for row in rows]
if not await self._tableExists(Session, "cross_scoring_proposals"):
return []
@@ -2580,8 +2591,21 @@ class DocumentServiceImpl(IDocumentService):
return str(Row.get("rule_name") or Row.get("rule_id") or "")
async def _syncRuleBindings(self, Session, DocTypeId: int, RuleSetIds: list[int], Region: str = "default") -> None:
"""全量替换规则绑定,仅写入新分组绑定"""
await sync_group_bindings_from_doc_type(Session, DocTypeId, RuleSetIds)
"""全量替换规则绑定。"""
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(
@@ -97,6 +97,41 @@ class RbacAdminServiceImpl(IRbacAdminService):
"is_cache": True,
"meta": {"group": "documents"},
},
{
"route_path": "/cross-checking",
"route_name": "cross-checking",
"component": "cross-checking",
"route_title": "交叉评查",
"icon": "ri-color-filter-line",
"sort_order": 60,
"is_hidden": False,
"is_cache": True,
"meta": {"group": "cross-review"},
},
{
"route_path": "/cross-checking/upload",
"route_name": "cross-checking-upload",
"component": "cross-checking.upload",
"route_title": "创建任务",
"icon": "ri-upload-cloud-line",
"sort_order": 1,
"parent_path": "/cross-checking",
"is_hidden": False,
"is_cache": True,
"meta": {"group": "cross-review"},
},
{
"route_path": "/cross-checking/result",
"route_name": "cross-checking-result",
"component": "cross-checking.result",
"route_title": "评查结果",
"icon": "ri-file-list-3-line",
"sort_order": 2,
"parent_path": "/cross-checking",
"is_hidden": False,
"is_cache": True,
"meta": {"group": "cross-review"},
},
{
"route_path": "/settings",
"route_name": "system-settings",
@@ -171,6 +206,15 @@ class RbacAdminServiceImpl(IRbacAdminService):
{"permission_key": "evaluation_point:create:write", "display_name": "创建评查点", "module": "evaluation_point", "resource": "create", "action": "write", "api_method": "POST", "api_path": "/api/v3/evaluation-points", "route_path": "/rules"},
{"permission_key": "evaluation_point:update:write", "display_name": "更新评查点", "module": "evaluation_point", "resource": "update", "action": "write", "api_method": "PUT", "api_path": "/api/v3/evaluation-points/{id}", "route_path": "/rules"},
{"permission_key": "evaluation_point:delete:delete", "display_name": "删除评查点", "module": "evaluation_point", "resource": "delete", "action": "delete", "api_method": "DELETE", "api_path": "/api/v3/evaluation-points/{id}", "route_path": "/rules"},
{"permission_key": "cross_review:task:create", "display_name": "创建交叉评查任务", "module": "cross_review", "resource": "task", "action": "create", "api_method": "POST", "api_path": "/api/v3/cross-review/tasks", "route_path": "/cross-checking/upload"},
{"permission_key": "cross_review:task:read", "display_name": "查看交叉评查任务", "module": "cross_review", "resource": "task", "action": "read", "api_method": "POST", "api_path": "/api/v3/cross-review/tasks/query", "route_path": "/cross-checking"},
{"permission_key": "cross_review:progress:view", "display_name": "查看交叉评查任务进度", "module": "cross_review", "resource": "progress", "action": "view", "api_method": "GET", "api_path": "/api/v3/cross-review/tasks/{task_id}/progress", "route_path": "/cross-checking"},
{"permission_key": "cross_review:document:read", "display_name": "查看交叉评查任务文档", "module": "cross_review", "resource": "document", "action": "read", "api_method": "GET", "api_path": "/api/v3/cross-review/tasks/{task_id}/documents", "route_path": "/cross-checking/result"},
{"permission_key": "cross_review:document:complete", "display_name": "确认交叉评查文档完成", "module": "cross_review", "resource": "document", "action": "complete", "api_method": "GET", "api_path": "/api/v3/cross-review/tasks/{task_id}/can-confirm", "route_path": "/cross-checking/result"},
{"permission_key": "cross_review:proposal:create", "display_name": "创建交叉评查提案", "module": "cross_review", "resource": "proposal", "action": "create", "api_method": "POST", "api_path": "/api/v3/cross-review/proposals", "route_path": "/cross-checking/result"},
{"permission_key": "cross_review:proposal:read", "display_name": "查看交叉评查提案", "module": "cross_review", "resource": "proposal", "action": "read", "api_method": "GET", "api_path": "/api/v3/cross-review/documents/{document_id}/proposals", "route_path": "/cross-checking/result"},
{"permission_key": "cross_review:proposal:delete", "display_name": "撤销交叉评查提案", "module": "cross_review", "resource": "proposal", "action": "delete", "api_method": "DELETE", "api_path": "/api/v3/cross-review/proposals/{proposal_id}", "route_path": "/cross-checking/result"},
{"permission_key": "cross_review:proposal:vote", "display_name": "交叉评查提案投票", "module": "cross_review", "resource": "proposal", "action": "vote", "api_method": "POST", "api_path": "/api/v3/cross-review/proposals/{proposal_id}/votes", "route_path": "/cross-checking/result"},
{"permission_key": "rbac:roles:read", "display_name": "角色列表", "module": "rbac", "resource": "roles", "action": "read", "api_method": "GET", "api_path": "/api/v3/rbac/roles", "route_path": "/role-permissions"},
{"permission_key": "rbac:roles:create", "display_name": "创建角色", "module": "rbac", "resource": "roles", "action": "create", "api_method": "POST", "api_path": "/api/v3/rbac/roles", "route_path": "/role-permissions"},
{"permission_key": "rbac:roles:update", "display_name": "更新角色", "module": "rbac", "resource": "roles", "action": "update", "api_method": "PUT", "api_path": "/api/v3/rbac/roles/{role_id}", "route_path": "/role-permissions"},