feat: 完善模板对比持久化与附件版本处理
This commit is contained in:
@@ -26,6 +26,8 @@ from fastapi_modules.fastapi_leaudit.domian.Dto.crossReviewDto import (
|
||||
CrossReviewTaskQueryDTO,
|
||||
)
|
||||
from fastapi_modules.fastapi_leaudit.domian.vo.crossReviewVo import (
|
||||
CrossReviewTaskDocumentAppendVO,
|
||||
CrossReviewTaskHistoryVersionVO,
|
||||
CrossReviewPendingProposalVO,
|
||||
CrossReviewPendingVotesVO,
|
||||
CrossReviewPermissionVO,
|
||||
@@ -463,14 +465,16 @@ class CrossReviewServiceImpl(ICrossReviewService):
|
||||
"limit": Body.pageSize,
|
||||
"offset": (Body.page - 1) * Body.pageSize,
|
||||
}
|
||||
whereClauses = [
|
||||
baseWhereClauses = [
|
||||
"td.task_id = :task_id",
|
||||
"td.delete_time IS NULL",
|
||||
"d.deleted_at IS NULL",
|
||||
]
|
||||
if Body.keyword:
|
||||
whereClauses.append("(d.normalized_name ILIKE :keyword OR CAST(d.biz_document_id AS TEXT) ILIKE :keyword)")
|
||||
baseWhereClauses.append("(d.normalized_name ILIKE :keyword OR CAST(d.biz_document_id AS TEXT) ILIKE :keyword)")
|
||||
params["keyword"] = f"%{Body.keyword.strip()}%"
|
||||
whereSql = " AND ".join(whereClauses)
|
||||
baseWhereSql = " AND ".join(baseWhereClauses)
|
||||
latestWhereSql = f"{baseWhereSql} AND COALESCE(d.is_latest_version, false) = true"
|
||||
|
||||
total = int(
|
||||
(
|
||||
@@ -481,7 +485,7 @@ class CrossReviewServiceImpl(ICrossReviewService):
|
||||
FROM leaudit_cross_review_task_documents td
|
||||
JOIN leaudit_documents d
|
||||
ON d.id = td.document_id
|
||||
WHERE {whereSql}
|
||||
WHERE {latestWhereSql}
|
||||
"""
|
||||
),
|
||||
params,
|
||||
@@ -510,7 +514,7 @@ class CrossReviewServiceImpl(ICrossReviewService):
|
||||
) AS processing_status,
|
||||
d.version_no,
|
||||
d.is_latest_version,
|
||||
COALESCE(d.version_group_key, '') AS version_group_key,
|
||||
COALESCE(NULLIF(d.version_group_key, ''), CONCAT('root:', COALESCE(d.root_version_id, d.id)::text)) AS version_group_key,
|
||||
COALESCE(vc.total_versions, 1)::int AS total_versions,
|
||||
d.created_at,
|
||||
td.audit_status,
|
||||
@@ -549,7 +553,9 @@ class CrossReviewServiceImpl(ICrossReviewService):
|
||||
LIMIT 1
|
||||
) df ON TRUE
|
||||
LEFT JOIN (
|
||||
SELECT d2.version_group_key, COUNT(*) AS total_versions
|
||||
SELECT
|
||||
COALESCE(NULLIF(d2.version_group_key, ''), CONCAT('root:', COALESCE(d2.root_version_id, d2.id)::text)) AS version_group_key,
|
||||
COUNT(*) AS total_versions
|
||||
FROM leaudit_documents d2
|
||||
JOIN leaudit_cross_review_task_documents td2
|
||||
ON td2.document_id = d2.id
|
||||
@@ -557,7 +563,8 @@ class CrossReviewServiceImpl(ICrossReviewService):
|
||||
AND td2.task_id = :task_id
|
||||
WHERE d2.deleted_at IS NULL
|
||||
GROUP BY d2.version_group_key
|
||||
) vc ON vc.version_group_key = d.version_group_key
|
||||
, COALESCE(d2.root_version_id, d2.id)
|
||||
) vc ON vc.version_group_key = COALESCE(NULLIF(d.version_group_key, ''), CONCAT('root:', COALESCE(d.root_version_id, d.id)::text))
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
COUNT(*)::int AS total_evaluation_points,
|
||||
@@ -610,7 +617,7 @@ class CrossReviewServiceImpl(ICrossReviewService):
|
||||
AND p.status = 'approved'
|
||||
AND p.delete_time IS NULL
|
||||
) pd ON TRUE
|
||||
WHERE {whereSql}
|
||||
WHERE {latestWhereSql}
|
||||
ORDER BY d.created_at DESC, d.id DESC
|
||||
LIMIT :limit OFFSET :offset
|
||||
"""
|
||||
@@ -619,47 +626,208 @@ class CrossReviewServiceImpl(ICrossReviewService):
|
||||
)
|
||||
).mappings().all()
|
||||
|
||||
items = [
|
||||
CrossReviewTaskDocumentVO(
|
||||
documentId=int(row["document_id"]),
|
||||
name=str(row["name"] or ""),
|
||||
documentNumber=row.get("document_number"),
|
||||
typeId=self._to_int(row.get("type_id")),
|
||||
typeName=row.get("type_name"),
|
||||
processingStatus=row.get("processing_status"),
|
||||
versionNo=int(row.get("version_no") or 1),
|
||||
isLatestVersion=bool(row.get("is_latest_version")),
|
||||
versionGroupKey=str(row.get("version_group_key") or ""),
|
||||
totalVersions=int(row.get("total_versions") or 1),
|
||||
auditStatus=int(row.get("audit_status") or 0),
|
||||
createdAt=row.get("created_at"),
|
||||
fileSize=int(row.get("file_size") or 0),
|
||||
path=str(row.get("path") or ""),
|
||||
uploadTime=row.get("upload_time"),
|
||||
fileExt=str(row.get("file_ext") or "") or None,
|
||||
totalEvaluationPoints=int(row.get("total_evaluation_points") or 0),
|
||||
passCount=int(row.get("pass_count") or 0),
|
||||
warningCount=int(row.get("warning_count") or 0),
|
||||
errorCount=int(row.get("error_count") or 0),
|
||||
manualCount=int(row.get("manual_count") or 0),
|
||||
issueCount=int(row.get("issue_count") or 0),
|
||||
warningMessages=self._parse_text_array(row.get("warning_messages")),
|
||||
errorMessages=self._parse_text_array(row.get("error_messages")),
|
||||
issueMessages=self._parse_text_array(row.get("issue_messages")),
|
||||
manualMessages=self._parse_text_array(row.get("manual_messages")),
|
||||
finalScore=float(row.get("final_score") or 0) + float(row.get("approved_delta") or 0),
|
||||
fullScore=float(row.get("full_score") or 0),
|
||||
scoreSummary=self._build_score_summary(
|
||||
float(row.get("final_score") or 0) + float(row.get("approved_delta") or 0),
|
||||
float(row.get("full_score") or 0),
|
||||
),
|
||||
scorePercent=self._build_score_percent(
|
||||
float(row.get("final_score") or 0) + float(row.get("approved_delta") or 0),
|
||||
float(row.get("full_score") or 0),
|
||||
),
|
||||
latestDocumentIds = [int(row["document_id"]) for row in rows]
|
||||
historyByGroup: dict[str, list[CrossReviewTaskHistoryVersionVO]] = {}
|
||||
if latestDocumentIds:
|
||||
historyRows = (
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
WITH target_groups AS (
|
||||
SELECT DISTINCT COALESCE(NULLIF(d.version_group_key, ''), CONCAT('root:', COALESCE(d.root_version_id, d.id)::text)) AS version_group_key
|
||||
FROM leaudit_cross_review_task_documents td
|
||||
JOIN leaudit_documents d
|
||||
ON d.id = td.document_id
|
||||
WHERE td.task_id = :task_id
|
||||
AND td.delete_time IS NULL
|
||||
AND d.id = ANY(:document_ids)
|
||||
)
|
||||
SELECT
|
||||
d.id AS document_id,
|
||||
COALESCE(d.normalized_name, '') AS name,
|
||||
CAST(d.biz_document_id AS TEXT) AS document_number,
|
||||
d.type_id,
|
||||
COALESCE(
|
||||
CASE
|
||||
WHEN LOWER(COALESCE(ar.status, '')) IN ('pending', 'queued', 'running', 'retrying')
|
||||
THEN ar.status
|
||||
ELSE d.processing_status
|
||||
END,
|
||||
d.processing_status,
|
||||
'waiting'
|
||||
) AS processing_status,
|
||||
d.version_no,
|
||||
d.is_latest_version,
|
||||
COALESCE(NULLIF(d.version_group_key, ''), CONCAT('root:', COALESCE(d.root_version_id, d.id)::text)) AS version_group_key,
|
||||
td.audit_status,
|
||||
d.created_at,
|
||||
COALESCE(dt.name, '') AS type_name,
|
||||
COALESCE(df.file_size, 0) AS file_size,
|
||||
COALESCE(df.file_path, '') AS path,
|
||||
df.file_upload_time AS upload_time,
|
||||
COALESCE(df.file_ext, '') AS file_ext,
|
||||
COALESCE(es.total_evaluation_points, 0) AS total_evaluation_points,
|
||||
COALESCE(es.pass_count, 0) AS pass_count,
|
||||
COALESCE(es.warning_count, 0) AS warning_count,
|
||||
COALESCE(es.error_count, 0) AS error_count,
|
||||
COALESCE(es.manual_count, 0) AS manual_count,
|
||||
COALESCE(es.issue_count, 0) AS issue_count,
|
||||
COALESCE(es.warning_messages, ARRAY[]::text[]) AS warning_messages,
|
||||
COALESCE(es.error_messages, ARRAY[]::text[]) AS error_messages,
|
||||
COALESCE(es.issue_messages, ARRAY[]::text[]) AS issue_messages,
|
||||
COALESCE(es.manual_messages, ARRAY[]::text[]) AS manual_messages,
|
||||
COALESCE(es.final_score, 0) AS final_score,
|
||||
COALESCE(es.full_score, 0) AS full_score,
|
||||
COALESCE(pd.approved_delta, 0) AS approved_delta
|
||||
FROM leaudit_cross_review_task_documents td
|
||||
JOIN leaudit_documents d
|
||||
ON d.id = td.document_id
|
||||
JOIN target_groups tg
|
||||
ON tg.version_group_key = COALESCE(NULLIF(d.version_group_key, ''), CONCAT('root:', COALESCE(d.root_version_id, d.id)::text))
|
||||
LEFT JOIN leaudit_audit_runs ar
|
||||
ON ar.id = d.current_run_id
|
||||
LEFT JOIN leaudit_document_types dt
|
||||
ON dt.id = d.type_id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT file_size, local_path AS file_path, created_at AS file_upload_time,
|
||||
COALESCE(file_ext, '') AS file_ext
|
||||
FROM leaudit_document_files
|
||||
WHERE document_id = d.id
|
||||
ORDER BY id ASC
|
||||
LIMIT 1
|
||||
) df ON TRUE
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
COUNT(*)::int AS total_evaluation_points,
|
||||
COUNT(*) FILTER (WHERE rr.passed IS TRUE)::int AS pass_count,
|
||||
COUNT(*) FILTER (WHERE rr.passed IS FALSE AND rr.risk = 'high')::int AS error_count,
|
||||
COUNT(*) FILTER (WHERE rr.passed IS FALSE AND rr.risk IN ('low', 'medium'))::int AS warning_count,
|
||||
0::int AS manual_count,
|
||||
COUNT(*) FILTER (WHERE rr.passed IS FALSE)::int AS issue_count,
|
||||
ARRAY_AGG(rr.fail_message ORDER BY rr.id) FILTER (
|
||||
WHERE rr.passed IS FALSE AND rr.risk = 'high' AND rr.fail_message IS NOT NULL AND rr.fail_message != ''
|
||||
) AS error_messages,
|
||||
ARRAY_AGG(rr.fail_message ORDER BY rr.id) FILTER (
|
||||
WHERE rr.passed IS FALSE AND rr.risk IN ('low', 'medium') AND rr.fail_message IS NOT NULL AND rr.fail_message != ''
|
||||
) AS warning_messages,
|
||||
ARRAY_AGG(rr.fail_message ORDER BY rr.id) FILTER (
|
||||
WHERE rr.passed IS FALSE AND rr.fail_message IS NOT NULL AND rr.fail_message != ''
|
||||
) AS issue_messages,
|
||||
ARRAY[]::text[] AS manual_messages,
|
||||
COALESCE(SUM(rr.score) FILTER (WHERE rr.passed IS TRUE), 0) AS final_score,
|
||||
COALESCE(SUM(rr.score), 0) AS full_score
|
||||
FROM leaudit_rule_results rr
|
||||
WHERE rr.document_id = d.id
|
||||
AND rr.run_id = d.current_run_id
|
||||
) es ON TRUE
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT COALESCE(SUM(proposed_score_delta), 0) AS approved_delta
|
||||
FROM leaudit_cross_review_proposals p
|
||||
WHERE p.document_id = d.id
|
||||
AND p.status = 'approved'
|
||||
AND p.delete_time IS NULL
|
||||
) pd ON TRUE
|
||||
WHERE td.task_id = :task_id
|
||||
AND td.delete_time IS NULL
|
||||
AND d.deleted_at IS NULL
|
||||
ORDER BY COALESCE(NULLIF(d.version_group_key, ''), CONCAT('root:', COALESCE(d.root_version_id, d.id)::text)), d.version_no DESC, d.id DESC
|
||||
"""
|
||||
).bindparams(bindparam("document_ids", expanding=False)),
|
||||
{"task_id": TaskId, "document_ids": latestDocumentIds},
|
||||
)
|
||||
).mappings().all()
|
||||
|
||||
groupedRows: dict[str, list[dict]] = {}
|
||||
for row in historyRows:
|
||||
groupedRows.setdefault(str(row.get("version_group_key") or ""), []).append(dict(row))
|
||||
|
||||
for groupKey, groupRows in groupedRows.items():
|
||||
orderedRows = sorted(
|
||||
groupRows,
|
||||
key=lambda item: (int(item.get("version_no") or 1), int(item.get("document_id") or 0)),
|
||||
)
|
||||
localVersionMap = {
|
||||
int(item["document_id"]): index + 1 for index, item in enumerate(orderedRows)
|
||||
}
|
||||
historyItems: list[CrossReviewTaskHistoryVersionVO] = []
|
||||
for item in reversed(orderedRows[:-1]):
|
||||
finalScore = float(item.get("final_score") or 0) + float(item.get("approved_delta") or 0)
|
||||
fullScore = float(item.get("full_score") or 0)
|
||||
historyItems.append(
|
||||
CrossReviewTaskHistoryVersionVO(
|
||||
documentId=int(item["document_id"]),
|
||||
name=str(item.get("name") or ""),
|
||||
documentNumber=item.get("document_number"),
|
||||
typeId=self._to_int(item.get("type_id")),
|
||||
typeName=item.get("type_name"),
|
||||
processingStatus=item.get("processing_status"),
|
||||
versionNo=int(localVersionMap.get(int(item["document_id"]), 1)),
|
||||
auditStatus=int(item.get("audit_status") or 0),
|
||||
createdAt=item.get("created_at"),
|
||||
fileSize=int(item.get("file_size") or 0),
|
||||
path=str(item.get("path") or ""),
|
||||
uploadTime=item.get("upload_time"),
|
||||
fileExt=str(item.get("file_ext") or "") or None,
|
||||
totalEvaluationPoints=int(item.get("total_evaluation_points") or 0),
|
||||
passCount=int(item.get("pass_count") or 0),
|
||||
warningCount=int(item.get("warning_count") or 0),
|
||||
errorCount=int(item.get("error_count") or 0),
|
||||
manualCount=int(item.get("manual_count") or 0),
|
||||
issueCount=int(item.get("issue_count") or 0),
|
||||
warningMessages=self._parse_text_array(item.get("warning_messages")),
|
||||
errorMessages=self._parse_text_array(item.get("error_messages")),
|
||||
issueMessages=self._parse_text_array(item.get("issue_messages")),
|
||||
manualMessages=self._parse_text_array(item.get("manual_messages")),
|
||||
finalScore=finalScore,
|
||||
fullScore=fullScore,
|
||||
scoreSummary=self._build_score_summary(finalScore, fullScore),
|
||||
scorePercent=self._build_score_percent(finalScore, fullScore),
|
||||
)
|
||||
)
|
||||
historyByGroup[groupKey] = historyItems
|
||||
|
||||
items: list[CrossReviewTaskDocumentVO] = []
|
||||
for row in rows:
|
||||
groupKey = str(row.get("version_group_key") or "")
|
||||
historyVersions = historyByGroup.get(groupKey, [])
|
||||
finalScore = float(row.get("final_score") or 0) + float(row.get("approved_delta") or 0)
|
||||
fullScore = float(row.get("full_score") or 0)
|
||||
versionNo = int(row.get("total_versions") or 1)
|
||||
items.append(
|
||||
CrossReviewTaskDocumentVO(
|
||||
documentId=int(row["document_id"]),
|
||||
name=str(row["name"] or ""),
|
||||
documentNumber=row.get("document_number"),
|
||||
typeId=self._to_int(row.get("type_id")),
|
||||
typeName=row.get("type_name"),
|
||||
processingStatus=row.get("processing_status"),
|
||||
versionNo=versionNo,
|
||||
isLatestVersion=True,
|
||||
versionGroupKey=groupKey,
|
||||
totalVersions=int(row.get("total_versions") or 1),
|
||||
auditStatus=int(row.get("audit_status") or 0),
|
||||
createdAt=row.get("created_at"),
|
||||
fileSize=int(row.get("file_size") or 0),
|
||||
path=str(row.get("path") or ""),
|
||||
uploadTime=row.get("upload_time"),
|
||||
fileExt=str(row.get("file_ext") or "") or None,
|
||||
totalEvaluationPoints=int(row.get("total_evaluation_points") or 0),
|
||||
passCount=int(row.get("pass_count") or 0),
|
||||
warningCount=int(row.get("warning_count") or 0),
|
||||
errorCount=int(row.get("error_count") or 0),
|
||||
manualCount=int(row.get("manual_count") or 0),
|
||||
issueCount=int(row.get("issue_count") or 0),
|
||||
warningMessages=self._parse_text_array(row.get("warning_messages")),
|
||||
errorMessages=self._parse_text_array(row.get("error_messages")),
|
||||
issueMessages=self._parse_text_array(row.get("issue_messages")),
|
||||
manualMessages=self._parse_text_array(row.get("manual_messages")),
|
||||
finalScore=finalScore,
|
||||
fullScore=fullScore,
|
||||
scoreSummary=self._build_score_summary(finalScore, fullScore),
|
||||
scorePercent=self._build_score_percent(finalScore, fullScore),
|
||||
historyVersions=historyVersions,
|
||||
)
|
||||
)
|
||||
for row in rows
|
||||
]
|
||||
return CrossReviewTaskDocumentPageVO(
|
||||
taskId=TaskId,
|
||||
total=total,
|
||||
@@ -1242,6 +1410,84 @@ class CrossReviewServiceImpl(ICrossReviewService):
|
||||
processingStatus=uploadResult.processingStatus,
|
||||
)
|
||||
|
||||
async def AppendTaskDocumentAttachments(
|
||||
self,
|
||||
CurrentUserId: int,
|
||||
TaskId: int,
|
||||
DocumentId: int,
|
||||
Files: list[tuple[str, bytes, str | None]],
|
||||
Remark: str | None = None,
|
||||
) -> CrossReviewTaskDocumentAppendVO:
|
||||
"""为交叉评查任务文档追加附件,并生成同版本链新版本。"""
|
||||
async with GetAsyncSession() as session:
|
||||
await self._ensure_tables_ready(session)
|
||||
permission = await self.CanConfirmTaskDocument(CurrentUserId, TaskId)
|
||||
if not permission.canConfirm:
|
||||
raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, permission.reason)
|
||||
await self._ensure_task_document(session, TaskId, DocumentId)
|
||||
|
||||
appendResult = await self.DocumentService.AppendAttachments(
|
||||
CurrentUserId=CurrentUserId,
|
||||
Id=DocumentId,
|
||||
Files=Files,
|
||||
MergeMode="new",
|
||||
Remark=Remark,
|
||||
)
|
||||
|
||||
async with GetAsyncSession() as session:
|
||||
await self._ensure_tables_ready(session)
|
||||
await self._reset_transaction_for_write(session)
|
||||
async with session.begin():
|
||||
exists = bool(
|
||||
await session.scalar(
|
||||
text(
|
||||
"""
|
||||
SELECT 1
|
||||
FROM leaudit_cross_review_task_documents
|
||||
WHERE task_id = :task_id
|
||||
AND document_id = :document_id
|
||||
AND delete_time IS NULL
|
||||
LIMIT 1
|
||||
"""
|
||||
),
|
||||
{"task_id": TaskId, "document_id": appendResult.documentId},
|
||||
)
|
||||
)
|
||||
if not exists:
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO leaudit_cross_review_task_documents
|
||||
(task_id, document_id, audit_status)
|
||||
VALUES
|
||||
(:task_id, :document_id, 0)
|
||||
"""
|
||||
),
|
||||
{"task_id": TaskId, "document_id": appendResult.documentId},
|
||||
)
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
UPDATE leaudit_cross_review_tasks
|
||||
SET status = 'in_progress',
|
||||
update_time = NOW()
|
||||
WHERE id = :task_id
|
||||
AND delete_time IS NULL
|
||||
"""
|
||||
),
|
||||
{"task_id": TaskId},
|
||||
)
|
||||
|
||||
return CrossReviewTaskDocumentAppendVO(
|
||||
taskId=TaskId,
|
||||
originalDocumentId=DocumentId,
|
||||
documentId=appendResult.documentId,
|
||||
versionNo=appendResult.versionNo,
|
||||
versionGroupKey=appendResult.versionGroupKey,
|
||||
auditStatus=0,
|
||||
processingStatus=appendResult.processingStatus,
|
||||
)
|
||||
|
||||
async def _build_document_proposals_page(
|
||||
self,
|
||||
session,
|
||||
|
||||
Reference in New Issue
Block a user