feat: 支持合同模板上传与对比记录持久化
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
"""文档控制器。"""
|
"""文档控制器。"""
|
||||||
|
|
||||||
|
import json
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from fastapi import Depends, File, Form, Query, UploadFile
|
from fastapi import Depends, File, Form, Query, UploadFile
|
||||||
@@ -8,7 +9,8 @@ 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, StatusCodeEnum
|
||||||
|
from fastapi_common.fastapi_common_web.exception.LeauditException import LeauditException
|
||||||
from fastapi_common.fastapi_common_security.security import verify_access_token
|
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 (
|
||||||
@@ -21,6 +23,7 @@ from fastapi_modules.fastapi_leaudit.domian.vo.documentVo import (
|
|||||||
DocumentTypeRootCreateDTO,
|
DocumentTypeRootCreateDTO,
|
||||||
DocumentTypeRootItemVO,
|
DocumentTypeRootItemVO,
|
||||||
DocumentTypeRootUpdateDTO,
|
DocumentTypeRootUpdateDTO,
|
||||||
|
ContractTemplateUploadVO,
|
||||||
DocumentTypeUpdateDTO,
|
DocumentTypeUpdateDTO,
|
||||||
DocumentUploadVO,
|
DocumentUploadVO,
|
||||||
)
|
)
|
||||||
@@ -94,6 +97,33 @@ class DocumentController(BaseController):
|
|||||||
)
|
)
|
||||||
return Result.success(data=Data)
|
return Result.success(data=Data)
|
||||||
|
|
||||||
|
@self.router.post("/upload/upload_contract_template", response_model=Result[ContractTemplateUploadVO])
|
||||||
|
async def UploadContractTemplate(
|
||||||
|
file: UploadFile = File(..., description="合同模板文件"),
|
||||||
|
upload_info: str = Form(..., description="模板上传信息 JSON,包含 document_id/comparison_id"),
|
||||||
|
payload: dict[str, Any] = Depends(verify_access_token),
|
||||||
|
):
|
||||||
|
"""兼容旧前端的合同模板上传接口。"""
|
||||||
|
try:
|
||||||
|
uploadInfo = json.loads(upload_info or "{}")
|
||||||
|
except json.JSONDecodeError as error:
|
||||||
|
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "upload_info 不是合法 JSON") from error
|
||||||
|
|
||||||
|
documentId = int(uploadInfo.get("document_id") or 0)
|
||||||
|
comparisonIdRaw = uploadInfo.get("comparison_id")
|
||||||
|
comparisonId = int(comparisonIdRaw) if comparisonIdRaw not in (None, "") else None
|
||||||
|
content = await file.read()
|
||||||
|
|
||||||
|
data = await self.DocumentService.UploadContractTemplate(
|
||||||
|
CurrentUserId=int(payload["user_id"]),
|
||||||
|
DocumentId=documentId,
|
||||||
|
FileName=file.filename or "template.bin",
|
||||||
|
FileContent=content,
|
||||||
|
ContentType=file.content_type,
|
||||||
|
ComparisonId=comparisonId,
|
||||||
|
)
|
||||||
|
return Result.success(data=data, message="合同模板上传成功")
|
||||||
|
|
||||||
@self.router.get("/documents/list", response_model=Result[DocumentListPageVO])
|
@self.router.get("/documents/list", response_model=Result[DocumentListPageVO])
|
||||||
async def ListDocuments(
|
async def ListDocuments(
|
||||||
page: int = 1,
|
page: int = 1,
|
||||||
|
|||||||
@@ -28,6 +28,17 @@ class DocumentUploadVO(BaseModel):
|
|||||||
run: AuditRunVO | None = Field(None, description="自动触发后的运行信息")
|
run: AuditRunVO | None = Field(None, description="自动触发后的运行信息")
|
||||||
|
|
||||||
|
|
||||||
|
class ContractTemplateUploadVO(BaseModel):
|
||||||
|
"""合同模板上传响应。"""
|
||||||
|
|
||||||
|
documentId: int = Field(..., description="目标文档ID")
|
||||||
|
comparisonId: int = Field(..., description="合同结构对比记录ID")
|
||||||
|
templateName: str = Field(..., description="模板文件名")
|
||||||
|
templateContractPath: str = Field(..., description="模板文件 OSS 路径")
|
||||||
|
fileSize: int = Field(..., description="模板文件大小")
|
||||||
|
status: str = Field("uploaded", description="上传状态")
|
||||||
|
|
||||||
|
|
||||||
class DocumentStatusItemVO(BaseModel):
|
class DocumentStatusItemVO(BaseModel):
|
||||||
"""文档状态项。"""
|
"""文档状态项。"""
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
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 (
|
||||||
|
ContractTemplateUploadVO,
|
||||||
DocumentDetailVO,
|
DocumentDetailVO,
|
||||||
DocumentListPageVO,
|
DocumentListPageVO,
|
||||||
DocumentStatusItemVO,
|
DocumentStatusItemVO,
|
||||||
@@ -118,6 +119,19 @@ class IDocumentService(ABC):
|
|||||||
"""为现有文档追加附件,并执行数据隔离校验。"""
|
"""为现有文档追加附件,并执行数据隔离校验。"""
|
||||||
...
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def UploadContractTemplate(
|
||||||
|
self,
|
||||||
|
CurrentUserId: int,
|
||||||
|
DocumentId: int,
|
||||||
|
FileName: str,
|
||||||
|
FileContent: bytes,
|
||||||
|
ContentType: str | None,
|
||||||
|
ComparisonId: int | None = None,
|
||||||
|
) -> ContractTemplateUploadVO:
|
||||||
|
"""为现有合同文档上传结构对比模板,并持久化记录。"""
|
||||||
|
...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def ListDocumentTypes(self, Ids: list[int] | None = None, EntryModuleId: int | None = None) -> list[DocumentTypeItemVO]:
|
async def ListDocumentTypes(self, Ids: list[int] | None = None, EntryModuleId: int | None = None) -> list[DocumentTypeItemVO]:
|
||||||
"""获取文档类型列表。"""
|
"""获取文档类型列表。"""
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ from fastapi_common.fastapi_common_web.exception.LeauditException import Leaudit
|
|||||||
from fastapi_common.fastapi_common_storage.oss_path_utils import OssPathUtils
|
from fastapi_common.fastapi_common_storage.oss_path_utils import OssPathUtils
|
||||||
|
|
||||||
from fastapi_modules.fastapi_leaudit.domian.vo.documentVo import (
|
from fastapi_modules.fastapi_leaudit.domian.vo.documentVo import (
|
||||||
|
ContractTemplateUploadVO,
|
||||||
DocumentAttachmentVO,
|
DocumentAttachmentVO,
|
||||||
DocumentDetailVO,
|
DocumentDetailVO,
|
||||||
DocumentHistoryVersionVO,
|
DocumentHistoryVersionVO,
|
||||||
@@ -632,6 +633,7 @@ class DocumentServiceImpl(IDocumentService):
|
|||||||
async with GetAsyncSession() as Session:
|
async with GetAsyncSession() as Session:
|
||||||
await self._ensureDocumentGroupColumn(Session)
|
await self._ensureDocumentGroupColumn(Session)
|
||||||
await self._ensureReviewPointAuditTable(Session)
|
await self._ensureReviewPointAuditTable(Session)
|
||||||
|
await self._ensureCrossReviewProposalSchema(Session)
|
||||||
currentUser = await self._getCurrentUserContext(CurrentUserId)
|
currentUser = await self._getCurrentUserContext(CurrentUserId)
|
||||||
documentColumns = await self._loadDocumentColumns(Session)
|
documentColumns = await self._loadDocumentColumns(Session)
|
||||||
detail = await self._getDocumentDetail(Session, DocumentId, CurrentUserId, currentUser, documentColumns)
|
detail = await self._getDocumentDetail(Session, DocumentId, CurrentUserId, currentUser, documentColumns)
|
||||||
@@ -655,6 +657,10 @@ class DocumentServiceImpl(IDocumentService):
|
|||||||
reviewPoints = await self._loadReviewPointResults(Session, detail, int(runRow["id"]))
|
reviewPoints = await self._loadReviewPointResults(Session, detail, int(runRow["id"]))
|
||||||
stats = self._buildReviewPointStats(reviewPoints)
|
stats = self._buildReviewPointStats(reviewPoints)
|
||||||
|
|
||||||
|
approvedSupplementDelta = await self._loadApprovedSupplementScoreDelta(Session, detail.documentId)
|
||||||
|
if approvedSupplementDelta:
|
||||||
|
stats.score += approvedSupplementDelta
|
||||||
|
|
||||||
documentPayload = await self._buildReviewDocumentPayload(Session, detail, runRow)
|
documentPayload = await self._buildReviewDocumentPayload(Session, detail, runRow)
|
||||||
reviewInfo = self._buildReviewInfo(runRow, reviewPoints, stats)
|
reviewInfo = self._buildReviewInfo(runRow, reviewPoints, stats)
|
||||||
comparisonDocument = await self._loadComparisonDocument(Session, detail.documentId)
|
comparisonDocument = await self._loadComparisonDocument(Session, detail.documentId)
|
||||||
@@ -866,6 +872,85 @@ class DocumentServiceImpl(IDocumentService):
|
|||||||
for row in rows
|
for row in rows
|
||||||
]
|
]
|
||||||
|
|
||||||
|
async def UploadContractTemplate(
|
||||||
|
self,
|
||||||
|
CurrentUserId: int,
|
||||||
|
DocumentId: int,
|
||||||
|
FileName: str,
|
||||||
|
FileContent: bytes,
|
||||||
|
ContentType: str | None,
|
||||||
|
ComparisonId: int | None = None,
|
||||||
|
) -> ContractTemplateUploadVO:
|
||||||
|
"""为现有合同文档上传结构对比模板,并写入 contract_structure_comparison。"""
|
||||||
|
if DocumentId <= 0:
|
||||||
|
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "文档ID不能为空")
|
||||||
|
if not FileName:
|
||||||
|
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "模板文件名不能为空")
|
||||||
|
if not FileContent:
|
||||||
|
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "模板文件内容不能为空")
|
||||||
|
|
||||||
|
fileExt = Path(FileName).suffix.lstrip(".").lower()
|
||||||
|
if fileExt not in {"pdf", "doc", "docx"}:
|
||||||
|
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "模板文件仅支持 pdf/doc/docx")
|
||||||
|
|
||||||
|
mimeType = ContentType or mimetypes.guess_type(FileName)[0] or "application/octet-stream"
|
||||||
|
fileSize = len(FileContent)
|
||||||
|
uploadedAt = datetime.now()
|
||||||
|
|
||||||
|
async with GetAsyncSession() as Session:
|
||||||
|
await self._ensureDocumentGroupColumn(Session)
|
||||||
|
await self._ensureContractStructureComparisonTable(Session)
|
||||||
|
currentUser = await self._getCurrentUserContext(CurrentUserId)
|
||||||
|
documentColumns = await self._loadDocumentColumns(Session)
|
||||||
|
detail = await self._getDocumentDetail(Session, DocumentId, CurrentUserId, currentUser, documentColumns)
|
||||||
|
if not detail and await self._hasCrossReviewDocumentAccess(Session, DocumentId, CurrentUserId):
|
||||||
|
detail = await self._getDocumentDetail(
|
||||||
|
Session,
|
||||||
|
DocumentId,
|
||||||
|
CurrentUserId,
|
||||||
|
currentUser,
|
||||||
|
documentColumns,
|
||||||
|
BypassScopeCheck=True,
|
||||||
|
)
|
||||||
|
if not detail:
|
||||||
|
raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "目标文档不存在或无权限访问")
|
||||||
|
|
||||||
|
versionLabel = f"v{int(detail.versionNo or 1)}"
|
||||||
|
objectKey = OssPathUtils.BuildBusinessDocKey(
|
||||||
|
Region=detail.region or "default",
|
||||||
|
TypeCode=detail.typeCode or "contract",
|
||||||
|
DocumentId=DocumentId,
|
||||||
|
Version=versionLabel,
|
||||||
|
FileRole="template",
|
||||||
|
FileName=FileName,
|
||||||
|
Year=uploadedAt.year,
|
||||||
|
Month=uploadedAt.month,
|
||||||
|
)
|
||||||
|
ossUrl = await self.OssService.UploadBytes(
|
||||||
|
ObjectKey=objectKey,
|
||||||
|
Content=FileContent,
|
||||||
|
ContentType=mimeType,
|
||||||
|
)
|
||||||
|
|
||||||
|
comparisonId = await self._upsertContractStructureComparison(
|
||||||
|
Session=Session,
|
||||||
|
DocumentId=DocumentId,
|
||||||
|
ComparisonId=ComparisonId,
|
||||||
|
TemplateName=FileName,
|
||||||
|
TemplatePath=ossUrl,
|
||||||
|
FileSize=fileSize,
|
||||||
|
)
|
||||||
|
await Session.commit()
|
||||||
|
|
||||||
|
return ContractTemplateUploadVO(
|
||||||
|
documentId=DocumentId,
|
||||||
|
comparisonId=comparisonId,
|
||||||
|
templateName=FileName,
|
||||||
|
templateContractPath=ossUrl,
|
||||||
|
fileSize=fileSize,
|
||||||
|
status="uploaded",
|
||||||
|
)
|
||||||
|
|
||||||
async def UpdateDocument(self, CurrentUserId: int, Id: int, Body: DocumentUpdateDTO) -> DocumentDetailVO:
|
async def UpdateDocument(self, CurrentUserId: int, Id: int, Body: DocumentUpdateDTO) -> DocumentDetailVO:
|
||||||
"""更新文档元数据,并执行数据隔离校验。"""
|
"""更新文档元数据,并执行数据隔离校验。"""
|
||||||
async with GetAsyncSession() as Session:
|
async with GetAsyncSession() as Session:
|
||||||
@@ -1521,6 +1606,214 @@ class DocumentServiceImpl(IDocumentService):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def _ensureCrossReviewProposalSchema(self, Session) -> None:
|
||||||
|
"""补齐交叉评查提案表的补充意见字段,兼容旧环境。"""
|
||||||
|
if not await self._tableExists(Session, "leaudit_cross_review_proposals"):
|
||||||
|
return
|
||||||
|
|
||||||
|
proposalColumns = await self._loadTableColumns(Session, "leaudit_cross_review_proposals")
|
||||||
|
touched = False
|
||||||
|
|
||||||
|
if "proposal_type" not in proposalColumns:
|
||||||
|
await Session.execute(
|
||||||
|
text(
|
||||||
|
"ALTER TABLE leaudit_cross_review_proposals ADD COLUMN proposal_type VARCHAR(32) NOT NULL DEFAULT 'review_point'"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
touched = True
|
||||||
|
if "evaluation_point_name" not in proposalColumns:
|
||||||
|
await Session.execute(
|
||||||
|
text(
|
||||||
|
"ALTER TABLE leaudit_cross_review_proposals ADD COLUMN evaluation_point_name VARCHAR(255)"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
touched = True
|
||||||
|
if "extraction_result_text" not in proposalColumns:
|
||||||
|
await Session.execute(
|
||||||
|
text(
|
||||||
|
"ALTER TABLE leaudit_cross_review_proposals ADD COLUMN extraction_result_text TEXT"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
touched = True
|
||||||
|
|
||||||
|
isRuleResultNotNull = bool(
|
||||||
|
await Session.scalar(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = current_schema()
|
||||||
|
AND table_name = 'leaudit_cross_review_proposals'
|
||||||
|
AND column_name = 'rule_result_id'
|
||||||
|
AND is_nullable = 'NO'
|
||||||
|
LIMIT 1
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if isRuleResultNotNull:
|
||||||
|
await Session.execute(
|
||||||
|
text(
|
||||||
|
"ALTER TABLE leaudit_cross_review_proposals ALTER COLUMN rule_result_id DROP NOT NULL"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
touched = True
|
||||||
|
|
||||||
|
if touched:
|
||||||
|
await Session.commit()
|
||||||
|
|
||||||
|
async def _ensureContractStructureComparisonTable(self, Session) -> None:
|
||||||
|
"""补齐合同结构比对表,兼容旧前端模板上传与详情页读取。"""
|
||||||
|
await Session.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS contract_structure_comparison (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
document_id BIGINT NOT NULL,
|
||||||
|
comparison_id BIGINT NULL,
|
||||||
|
template_contract_name VARCHAR(512) NULL,
|
||||||
|
template_contract_path TEXT NOT NULL DEFAULT '',
|
||||||
|
file_size BIGINT NOT NULL DEFAULT 0,
|
||||||
|
comparison_results JSONB NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await Session.execute(
|
||||||
|
text(
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_contract_structure_comparison_document_id ON contract_structure_comparison(document_id)"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
existingColumns = await self._loadTableColumns(Session, "contract_structure_comparison")
|
||||||
|
if "comparison_id" not in existingColumns:
|
||||||
|
await Session.execute(text("ALTER TABLE contract_structure_comparison ADD COLUMN comparison_id BIGINT NULL"))
|
||||||
|
if "template_contract_name" not in existingColumns:
|
||||||
|
await Session.execute(text("ALTER TABLE contract_structure_comparison ADD COLUMN template_contract_name VARCHAR(512) NULL"))
|
||||||
|
if "template_contract_path" not in existingColumns:
|
||||||
|
await Session.execute(text("ALTER TABLE contract_structure_comparison ADD COLUMN template_contract_path TEXT NOT NULL DEFAULT ''"))
|
||||||
|
if "file_size" not in existingColumns:
|
||||||
|
await Session.execute(text("ALTER TABLE contract_structure_comparison ADD COLUMN file_size BIGINT NOT NULL DEFAULT 0"))
|
||||||
|
if "comparison_results" not in existingColumns:
|
||||||
|
await Session.execute(text("ALTER TABLE contract_structure_comparison ADD COLUMN comparison_results JSONB NULL"))
|
||||||
|
|
||||||
|
async def _upsertContractStructureComparison(
|
||||||
|
self,
|
||||||
|
Session,
|
||||||
|
DocumentId: int,
|
||||||
|
ComparisonId: int | None,
|
||||||
|
TemplateName: str,
|
||||||
|
TemplatePath: str,
|
||||||
|
FileSize: int,
|
||||||
|
) -> int:
|
||||||
|
"""新增或更新合同结构比对模板记录。"""
|
||||||
|
targetRow = None
|
||||||
|
if ComparisonId is not None:
|
||||||
|
targetRow = (
|
||||||
|
await Session.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
SELECT id
|
||||||
|
FROM contract_structure_comparison
|
||||||
|
WHERE id = :comparison_id
|
||||||
|
AND document_id = :document_id
|
||||||
|
LIMIT 1
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{"comparison_id": ComparisonId, "document_id": DocumentId},
|
||||||
|
)
|
||||||
|
).mappings().first()
|
||||||
|
|
||||||
|
if targetRow is None:
|
||||||
|
targetRow = (
|
||||||
|
await Session.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
SELECT id
|
||||||
|
FROM contract_structure_comparison
|
||||||
|
WHERE document_id = :document_id
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT 1
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{"document_id": DocumentId},
|
||||||
|
)
|
||||||
|
).mappings().first()
|
||||||
|
|
||||||
|
if targetRow:
|
||||||
|
comparisonId = int(targetRow["id"])
|
||||||
|
await Session.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
UPDATE contract_structure_comparison
|
||||||
|
SET comparison_id = COALESCE(comparison_id, :comparison_id),
|
||||||
|
template_contract_name = :template_name,
|
||||||
|
template_contract_path = :template_path,
|
||||||
|
file_size = :file_size,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = :comparison_id
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{
|
||||||
|
"comparison_id": comparisonId,
|
||||||
|
"template_name": TemplateName,
|
||||||
|
"template_path": TemplatePath,
|
||||||
|
"file_size": FileSize,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return comparisonId
|
||||||
|
|
||||||
|
inserted = (
|
||||||
|
await Session.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
INSERT INTO contract_structure_comparison (
|
||||||
|
document_id,
|
||||||
|
comparison_id,
|
||||||
|
template_contract_name,
|
||||||
|
template_contract_path,
|
||||||
|
file_size,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
) VALUES (
|
||||||
|
:document_id,
|
||||||
|
NULL,
|
||||||
|
:template_name,
|
||||||
|
:template_path,
|
||||||
|
:file_size,
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
)
|
||||||
|
RETURNING id
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{
|
||||||
|
"document_id": DocumentId,
|
||||||
|
"template_name": TemplateName,
|
||||||
|
"template_path": TemplatePath,
|
||||||
|
"file_size": FileSize,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
).mappings().first()
|
||||||
|
if not inserted:
|
||||||
|
raise LeauditException(StatusCodeEnum.HTTP_500_INTERNAL_SERVER_ERROR, "合同模板记录写入失败")
|
||||||
|
|
||||||
|
comparisonId = int(inserted["id"])
|
||||||
|
await Session.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
UPDATE contract_structure_comparison
|
||||||
|
SET comparison_id = COALESCE(comparison_id, :comparison_id),
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = :comparison_id
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{"comparison_id": comparisonId},
|
||||||
|
)
|
||||||
|
return comparisonId
|
||||||
|
|
||||||
async def _tableExists(self, Session, TableName: str) -> bool:
|
async def _tableExists(self, Session, TableName: str) -> bool:
|
||||||
"""检查表是否存在。"""
|
"""检查表是否存在。"""
|
||||||
row = (
|
row = (
|
||||||
@@ -2268,20 +2561,33 @@ class DocumentServiceImpl(IDocumentService):
|
|||||||
async def _loadScoringProposals(self, Session, DocumentId: int) -> list[dict[str, Any]]:
|
async def _loadScoringProposals(self, Session, DocumentId: int) -> list[dict[str, Any]]:
|
||||||
"""读取交叉评分提案;缺表时降级为空。"""
|
"""读取交叉评分提案;缺表时降级为空。"""
|
||||||
if await self._tableExists(Session, "leaudit_cross_review_proposals"):
|
if await self._tableExists(Session, "leaudit_cross_review_proposals"):
|
||||||
|
columns = await self._loadTableColumns(Session, "leaudit_cross_review_proposals")
|
||||||
|
selectColumns = [
|
||||||
|
"id",
|
||||||
|
"'review_point' AS proposal_type",
|
||||||
|
"rule_result_id AS evaluation_result_id",
|
||||||
|
"proposer_id",
|
||||||
|
"NULL::varchar AS evaluation_point_name",
|
||||||
|
"NULL::text AS extraction_result_text",
|
||||||
|
"proposed_score_delta AS proposed_score",
|
||||||
|
"reason",
|
||||||
|
"status",
|
||||||
|
"create_time AS created_at",
|
||||||
|
"update_time AS updated_at",
|
||||||
|
"document_id",
|
||||||
|
]
|
||||||
|
if "proposal_type" in columns:
|
||||||
|
selectColumns[1] = "proposal_type"
|
||||||
|
if "evaluation_point_name" in columns:
|
||||||
|
selectColumns[4] = "evaluation_point_name"
|
||||||
|
if "extraction_result_text" in columns:
|
||||||
|
selectColumns[5] = "extraction_result_text"
|
||||||
rows = (
|
rows = (
|
||||||
await Session.execute(
|
await Session.execute(
|
||||||
text(
|
text(
|
||||||
"""
|
f"""
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
{', '.join(selectColumns)}
|
||||||
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
|
FROM leaudit_cross_review_proposals
|
||||||
WHERE document_id = :document_id
|
WHERE document_id = :document_id
|
||||||
AND delete_time IS NULL
|
AND delete_time IS NULL
|
||||||
@@ -2336,6 +2642,29 @@ class DocumentServiceImpl(IDocumentService):
|
|||||||
).mappings().all()
|
).mappings().all()
|
||||||
return [dict(row) for row in rows]
|
return [dict(row) for row in rows]
|
||||||
|
|
||||||
|
async def _loadApprovedSupplementScoreDelta(self, Session, DocumentId: int) -> float:
|
||||||
|
"""读取已通过的补充意见分值调整,计入详情页文档总分。"""
|
||||||
|
if not await self._tableExists(Session, "leaudit_cross_review_proposals"):
|
||||||
|
return 0.0
|
||||||
|
columns = await self._loadTableColumns(Session, "leaudit_cross_review_proposals")
|
||||||
|
if "proposal_type" not in columns:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
delta = await Session.scalar(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
SELECT COALESCE(SUM(proposed_score_delta), 0)
|
||||||
|
FROM leaudit_cross_review_proposals
|
||||||
|
WHERE document_id = :document_id
|
||||||
|
AND COALESCE(proposal_type, 'review_point') = 'supplement'
|
||||||
|
AND status = 'approved'
|
||||||
|
AND delete_time IS NULL
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{"document_id": DocumentId},
|
||||||
|
)
|
||||||
|
return float(delta or 0.0)
|
||||||
|
|
||||||
async def _loadReviewPointResults(
|
async def _loadReviewPointResults(
|
||||||
self,
|
self,
|
||||||
Session,
|
Session,
|
||||||
@@ -2377,6 +2706,7 @@ class DocumentServiceImpl(IDocumentService):
|
|||||||
FROM leaudit_cross_review_proposals
|
FROM leaudit_cross_review_proposals
|
||||||
WHERE rule_result_id = rr.id
|
WHERE rule_result_id = rr.id
|
||||||
AND document_id = :document_id
|
AND document_id = :document_id
|
||||||
|
AND COALESCE(proposal_type, 'review_point') = 'review_point'
|
||||||
AND status = 'approved'
|
AND status = 'approved'
|
||||||
AND delete_time IS NULL
|
AND delete_time IS NULL
|
||||||
) ad ON TRUE
|
) ad ON TRUE
|
||||||
@@ -2652,7 +2982,7 @@ class DocumentServiceImpl(IDocumentService):
|
|||||||
stats.warning += 1
|
stats.warning += 1
|
||||||
elif item.status == "error":
|
elif item.status == "error":
|
||||||
stats.error += 1
|
stats.error += 1
|
||||||
stats.score += float(item.score or 0)
|
stats.score += float(item.currentScore if item.currentScore is not None else (item.score or 0))
|
||||||
return stats
|
return stats
|
||||||
|
|
||||||
def _buildReviewInfo(
|
def _buildReviewInfo(
|
||||||
|
|||||||
+1
-1
Submodule legal-platform-frontend updated: c41ddc844c...dc8159837b
Reference in New Issue
Block a user