feat: update audit platform workspace

This commit is contained in:
wren
2026-05-25 09:50:01 +08:00
parent ba8e93c0d3
commit 68d0b4c878
73 changed files with 12196 additions and 367 deletions
@@ -88,6 +88,7 @@ class DocumentServiceImpl(IDocumentService):
TypeId: int | None = None,
TypeCode: str | None = None,
GroupId: int | None = None,
EntryModuleId: int | None = None,
Region: str | None = None,
FileRole: str = "primary",
CreatedBy: int | None = None,
@@ -136,6 +137,7 @@ class DocumentServiceImpl(IDocumentService):
async with GetAsyncSession() as Session:
await self._ensureDocumentGroupColumn(Session)
normalizedEntryModuleId = int(EntryModuleId) if EntryModuleId is not None and int(EntryModuleId) > 0 else None
if TypeId is not None and TypeCode is not None:
typeResult = await Session.execute(
text(
@@ -182,7 +184,13 @@ class DocumentServiceImpl(IDocumentService):
resolvedTypeId = int(typeRow["id"])
resolvedTypeCode = str(typeRow["code"])
resolvedGroupId = await self._resolveDocumentGroupId(Session, resolvedTypeId, GroupId)
await self._assertDocumentTypeEntryModule(Session, resolvedTypeId, normalizedEntryModuleId)
resolvedGroupId = await self._resolveDocumentGroupId(
Session,
resolvedTypeId,
GroupId,
normalizedEntryModuleId,
)
resolvedRootGroupId = await self._resolveDocumentRootGroupId(Session, resolvedTypeId, resolvedGroupId)
duplicateUpload = False
previousVersionId: int | None = None
@@ -236,6 +244,21 @@ class DocumentServiceImpl(IDocumentService):
else:
rootVersionId = document.rootVersionId
if normalizedEntryModuleId is not None:
await Session.execute(
text(
"""
UPDATE leaudit_documents
SET entry_module_id = :entry_module_id
WHERE id = :document_id
"""
),
{
"entry_module_id": normalizedEntryModuleId,
"document_id": document.Id,
},
)
versionLabel = f"v{document.versionNo}"
objectKey = OssPathUtils.BuildBusinessDocKey(
Region=normalizedRegion,
@@ -324,6 +347,7 @@ class DocumentServiceImpl(IDocumentService):
typeId=resolvedTypeId,
typeCode=resolvedTypeCode,
groupId=resolvedGroupId,
entryModuleId=normalizedEntryModuleId,
region=normalizedRegion,
tenantCode=resolvedTenant.tenant_code,
tenantName=resolvedTenant.tenant_name or normalizedRegion,
@@ -463,6 +487,7 @@ class DocumentServiceImpl(IDocumentService):
optionalSelects = [
f"{resolvedGroupIdExpr} AS group_id",
"d.entry_module_id AS entry_module_id" if "entry_module_id" in documentColumns else "NULL::bigint AS entry_module_id",
"d.document_number AS document_number" if "document_number" in documentColumns else "NULL::text AS document_number",
f"{persistedOrDerivedAuditStatusExpr} AS audit_status",
"d.is_test_document AS is_test_document" if "is_test_document" in documentColumns else "FALSE AS is_test_document",
@@ -662,6 +687,7 @@ class DocumentServiceImpl(IDocumentService):
typeName=row["type_name"],
groupId=int(row["group_id"]) if row["group_id"] is not None else None,
groupName=row["group_name"],
entryModuleId=int(row["entry_module_id"]) if row["entry_module_id"] is not None else None,
region=row["region"],
tenantCode=row["tenant_code"],
tenantName=row["tenant_name"],
@@ -762,6 +788,26 @@ class DocumentServiceImpl(IDocumentService):
scoring_proposals=scoringProposals,
)
async def IsCrossReviewDocument(self, DocumentId: int) -> bool:
"""判断文档是否属于交叉评查范围。"""
async with GetAsyncSession() as Session:
row = (
await Session.execute(
text(
"""
SELECT 1
FROM leaudit_documents d
WHERE d.id = :document_id
AND d.deleted_at IS NULL
AND COALESCE(d.review_scope, 'standard') = 'cross_review'
LIMIT 1
"""
),
{"document_id": DocumentId},
)
).first()
return bool(row)
async def AuditReviewPoint(
self,
CurrentUserId: int,
@@ -2705,6 +2751,15 @@ class DocumentServiceImpl(IDocumentService):
"""
)
)
await Session.execute(
text(
"""
ALTER TABLE leaudit_documents
ADD COLUMN IF NOT EXISTS entry_module_id BIGINT NULL
REFERENCES leaudit_entry_modules(id)
"""
)
)
await Session.execute(
text(
"""
@@ -2717,23 +2772,71 @@ class DocumentServiceImpl(IDocumentService):
await Session.execute(
text("CREATE INDEX IF NOT EXISTS idx_leaudit_documents_group_id ON leaudit_documents(group_id)")
)
await Session.execute(
text("CREATE INDEX IF NOT EXISTS idx_leaudit_documents_entry_module_id ON leaudit_documents(entry_module_id)")
)
await Session.execute(
text("CREATE INDEX IF NOT EXISTS idx_leaudit_documents_tenant_code ON leaudit_documents(tenant_code)")
)
async def _resolveDocumentGroupId(self, Session, TypeId: int, GroupId: int | None) -> int | None:
"""校验上传时选择的二级分组是否属于当前文档类型。"""
if GroupId is None:
return None
async def _assertDocumentTypeEntryModule(
self,
Session,
TypeId: int,
EntryModuleId: int | None,
) -> None:
if EntryModuleId is None:
return
row = (
await Session.execute(
text(
"""
SELECT id, document_type_id, name
FROM leaudit_evaluation_point_groups
WHERE id = :group_id
SELECT id, name, entry_module_id
FROM leaudit_document_types
WHERE id = :type_id
AND deleted_at IS NULL
AND COALESCE(pid, 0) <> 0
LIMIT 1
"""
),
{"type_id": TypeId},
)
).mappings().first()
if not row:
raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "文档类型不存在或已停用")
if int(row.get("entry_module_id") or 0) != EntryModuleId:
raise LeauditException(
StatusCodeEnum.HTTP_400_BAD_REQUEST,
f"文档类型「{row['name']}」不属于当前入口模块,无法上传",
)
async def _resolveDocumentGroupId(
self,
Session,
TypeId: int,
GroupId: int | None,
EntryModuleId: int | None = None,
) -> int | None:
"""校验上传时选择的二级分组是否属于当前文档类型。"""
if GroupId is None:
return await self._resolveUniqueDocumentGroupId(Session, TypeId, EntryModuleId)
row = (
await Session.execute(
text(
"""
SELECT
child.id,
child.document_type_id,
child.name,
COALESCE(child.entry_module_id, parent.entry_module_id, dt.entry_module_id) AS entry_module_id
FROM leaudit_evaluation_point_groups child
LEFT JOIN leaudit_evaluation_point_groups parent
ON parent.id = child.pid
LEFT JOIN leaudit_document_types dt
ON dt.id = child.document_type_id
WHERE child.id = :group_id
AND child.deleted_at IS NULL
AND COALESCE(child.pid, 0) <> 0
AND child.is_enabled = true
LIMIT 1
"""
),
@@ -2748,8 +2851,63 @@ class DocumentServiceImpl(IDocumentService):
TypeId, GroupId, row["document_type_id"], row["name"],
)
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, f"当前子类型「{row['name']}」(id={GroupId}) 属于文档类型 {row['document_type_id']},与所选文档类型 {TypeId} 不匹配,无法上传")
if EntryModuleId is not None and int(row.get("entry_module_id") or 0) != EntryModuleId:
raise LeauditException(
StatusCodeEnum.HTTP_400_BAD_REQUEST,
f"当前子类型「{row['name']}」不属于当前入口模块,无法上传",
)
return int(row["id"])
async def _resolveUniqueDocumentGroupId(
self,
Session,
TypeId: int,
EntryModuleId: int | None = None,
) -> int | None:
params: dict[str, int] = {"type_id": TypeId}
entry_module_filter = ""
if EntryModuleId is not None:
entry_module_filter = """
AND COALESCE(child.entry_module_id, parent.entry_module_id, dt.entry_module_id) = :entry_module_id
"""
params["entry_module_id"] = int(EntryModuleId)
rows = (
await Session.execute(
text(
f"""
SELECT
child.id,
child.name,
COALESCE(child.entry_module_id, parent.entry_module_id, dt.entry_module_id) AS entry_module_id
FROM leaudit_evaluation_point_groups child
LEFT JOIN leaudit_evaluation_point_groups parent
ON parent.id = child.pid
LEFT JOIN leaudit_document_types dt
ON dt.id = child.document_type_id
WHERE child.document_type_id = :type_id
AND child.deleted_at IS NULL
AND COALESCE(child.pid, 0) <> 0
AND child.is_enabled = true
{entry_module_filter}
ORDER BY COALESCE(child.sort_order, 0) ASC, child.id ASC
LIMIT 2
"""
),
params,
)
).mappings().all()
if len(rows) == 1:
return int(rows[0]["id"])
if len(rows) > 1:
raise LeauditException(
StatusCodeEnum.HTTP_400_BAD_REQUEST,
"当前文档类型存在多个子类型,请选择具体子类型后再上传",
)
raise LeauditException(
StatusCodeEnum.HTTP_400_BAD_REQUEST,
"当前文档类型未配置可用子类型,请先在评查点分组管理中配置二级分组",
)
async def _resolveDocumentRootGroupId(self, Session, TypeId: int, GroupId: int | None) -> int | None:
"""解析上传命中的一级分组,用于跨二级类型做版本归档。"""
if GroupId is not None:
@@ -2942,6 +3100,7 @@ class DocumentServiceImpl(IDocumentService):
optionalSelects = [
f"{resolvedGroupIdExpr} AS group_id",
"d.entry_module_id AS entry_module_id" if "entry_module_id" in DocumentColumns else "NULL::bigint AS entry_module_id",
"d.document_number AS document_number" if "document_number" in DocumentColumns else "NULL::text AS document_number",
"d.remark AS remark" if "remark" in DocumentColumns else "NULL::text AS remark",
"d.is_test_document AS is_test_document" if "is_test_document" in DocumentColumns else "FALSE AS is_test_document",
@@ -3149,6 +3308,7 @@ class DocumentServiceImpl(IDocumentService):
typeName=detailRow["type_name"],
groupId=int(detailRow["group_id"]) if detailRow["group_id"] is not None else None,
groupName=detailRow["group_name"],
entryModuleId=int(detailRow["entry_module_id"]) if detailRow["entry_module_id"] is not None else None,
region=str(detailRow["region"] or ""),
tenantCode=detailRow["tenant_code"],
tenantName=detailRow["tenant_name"],
@@ -3562,6 +3722,9 @@ class DocumentServiceImpl(IDocumentService):
pageCount = int(metricRow["page_count"]) if metricRow and metricRow["page_count"] is not None else 0
ocrResultPayload = await self._buildReviewOcrPayload(Session, Detail.documentId, RunRow, pageCount)
pageQualityResults = []
if Detail.pageQualityRunId is not None:
pageQualityResults = await self._loadPageQualityIssueResults(Session, int(Detail.pageQualityRunId))
return {
"id": Detail.documentId,
@@ -3593,8 +3756,66 @@ class DocumentServiceImpl(IDocumentService):
"page_count": pageCount,
"ocrResult": ocrResultPayload,
"attachments": [item.model_dump() for item in Detail.attachments],
**self._buildReviewPageQualityPayload(Detail, pageQualityResults),
}
@staticmethod
def _buildReviewPageQualityPayload(
Detail: DocumentDetailVO,
PageQualityResults: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""构建评查详情页需要的图片质量摘要字段。"""
page_quality_summary = Detail.pageQualitySummary.model_dump() if Detail.pageQualitySummary else None
status_order = {"reject": 0, "review": 1}
page_quality_results = sorted(
PageQualityResults or [],
key=lambda item: (
status_order.get(str(item.get("qualityStatus") or ""), 9),
int(item.get("pageNum") or 0),
),
)
return {
"pageQualityRunId": Detail.pageQualityRunId,
"pageQualityRunStatus": Detail.pageQualityRunStatus,
"pageQualitySummaryStatus": Detail.pageQualitySummaryStatus,
"pageQualityIssueCount": Detail.pageQualityIssueCount,
"pageQualityWarningText": Detail.pageQualityWarningText,
"pageQualitySummary": page_quality_summary,
"pageQualityResults": page_quality_results,
}
async def _loadPageQualityIssueResults(self, Session, RunId: int) -> list[dict[str, Any]]:
"""加载页级图片质量问题明细。"""
if not await self._tableExists(Session, "leaudit_page_quality_results"):
return []
rows = (
await Session.execute(
text(
"""
SELECT page_num, quality_status, quality_score, reason_text
FROM leaudit_page_quality_results
WHERE run_id = :run_id
AND quality_status IN ('review', 'reject')
ORDER BY
CASE quality_status WHEN 'reject' THEN 0 WHEN 'review' THEN 1 ELSE 9 END,
page_num ASC,
id ASC
"""
),
{"run_id": RunId},
)
).mappings().all()
return [
{
"pageNum": int(row["page_num"]),
"qualityStatus": str(row["quality_status"] or "review"),
"qualityScore": float(row["quality_score"]) if row["quality_score"] is not None else None,
"reasonText": str(row["reason_text"] or "") or None,
}
for row in rows
if row["page_num"] is not None
]
async def _buildReviewOcrPayload(
self,
Session,