feat: support cross-review supplement opinions export
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from fastapi import Depends, File, Form, Query, UploadFile
|
from fastapi import Depends, File, Form, Query, UploadFile
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse, Response
|
||||||
|
|
||||||
from fastapi_common.fastapi_common_security.security import verify_access_token
|
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.controller import BaseController
|
||||||
@@ -221,6 +221,26 @@ class CrossReviewController(BaseController):
|
|||||||
Data = await self.CrossReviewService.GetDocumentPendingVotes(CurrentUserId=int(payload["user_id"]), DocumentId=DocumentId)
|
Data = await self.CrossReviewService.GetDocumentPendingVotes(CurrentUserId=int(payload["user_id"]), DocumentId=DocumentId)
|
||||||
return Result.success(data=Data)
|
return Result.success(data=Data)
|
||||||
|
|
||||||
|
@self.router.get("/documents/{DocumentId}/proposals/export")
|
||||||
|
async def ExportDocumentProposals(
|
||||||
|
DocumentId: int,
|
||||||
|
payload: dict[str, Any] = Depends(verify_access_token),
|
||||||
|
):
|
||||||
|
"""导出文档提案列表 Excel。"""
|
||||||
|
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})
|
||||||
|
fileContent, fileName = await self.CrossReviewService.ExportDocumentProposals(
|
||||||
|
CurrentUserId=int(payload["user_id"]),
|
||||||
|
DocumentId=DocumentId,
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
content=fileContent,
|
||||||
|
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
headers={
|
||||||
|
"Content-Disposition": f"attachment; filename*=UTF-8''{fileName}",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
async def _check_permission(self, user_id: int, permission_keys: list[str]) -> bool:
|
async def _check_permission(self, user_id: int, permission_keys: list[str]) -> bool:
|
||||||
"""OR 逻辑权限校验。"""
|
"""OR 逻辑权限校验。"""
|
||||||
for permission_key in permission_keys:
|
for permission_key in permission_keys:
|
||||||
|
|||||||
@@ -39,9 +39,12 @@ class CrossReviewTaskDocumentQueryDTO(BaseModel):
|
|||||||
class CrossReviewProposalCreateDTO(BaseModel):
|
class CrossReviewProposalCreateDTO(BaseModel):
|
||||||
"""创建交叉评查提案。"""
|
"""创建交叉评查提案。"""
|
||||||
|
|
||||||
reviewPointResultId: int = Field(..., description="规则结果ID")
|
proposalType: str = Field("review_point", description="提案类型:review_point/supplement")
|
||||||
|
reviewPointResultId: int | None = Field(None, description="规则结果ID")
|
||||||
documentId: int = Field(..., description="文档ID")
|
documentId: int = Field(..., description="文档ID")
|
||||||
evaluationPointId: int | None = Field(None, description="评查点ID")
|
evaluationPointId: int | None = Field(None, description="评查点ID")
|
||||||
|
evaluationPointName: str | None = Field(None, description="补充评查点名称")
|
||||||
|
extractionResultText: str | None = Field(None, description="补充抽取结果文本")
|
||||||
auditOpinion: str = Field(..., min_length=1, description="提案理由")
|
auditOpinion: str = Field(..., min_length=1, description="提案理由")
|
||||||
deductionScore: float = Field(..., description="分值调整量")
|
deductionScore: float = Field(..., description="分值调整量")
|
||||||
|
|
||||||
|
|||||||
@@ -146,7 +146,10 @@ class CrossReviewProposalItemVO(BaseModel):
|
|||||||
"""提案列表项。"""
|
"""提案列表项。"""
|
||||||
|
|
||||||
proposalId: int = Field(..., description="提案ID")
|
proposalId: int = Field(..., description="提案ID")
|
||||||
|
proposalType: str = Field("review_point", description="提案类型")
|
||||||
|
reviewPointResultId: int | None = Field(None, description="评查点结果ID")
|
||||||
evaluationPointName: str = Field("", description="评查点名称")
|
evaluationPointName: str = Field("", description="评查点名称")
|
||||||
|
extractionResultText: str = Field("", description="抽取结果文本")
|
||||||
proposedScore: float = Field(..., description="调整分值")
|
proposedScore: float = Field(..., description="调整分值")
|
||||||
reason: str = Field("", description="提案理由")
|
reason: str = Field("", description="提案理由")
|
||||||
proposer: str = Field("", description="提案人姓名")
|
proposer: str = Field("", description="提案人姓名")
|
||||||
|
|||||||
@@ -99,6 +99,11 @@ class ICrossReviewService(ABC):
|
|||||||
"""获取文档待投票信息。"""
|
"""获取文档待投票信息。"""
|
||||||
...
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def ExportDocumentProposals(self, CurrentUserId: int, DocumentId: int) -> tuple[bytes, str]:
|
||||||
|
"""导出文档交叉评查意见 Excel。"""
|
||||||
|
...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def UploadTaskDocument(
|
async def UploadTaskDocument(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -2,8 +2,13 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import io
|
||||||
import logging
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
from math import floor
|
from math import floor
|
||||||
|
from urllib.parse import quote
|
||||||
|
from xml.sax.saxutils import escape
|
||||||
|
from zipfile import ZIP_DEFLATED, ZipFile
|
||||||
|
|
||||||
from sqlalchemy import bindparam, text
|
from sqlalchemy import bindparam, text
|
||||||
|
|
||||||
@@ -49,6 +54,37 @@ from fastapi_modules.fastapi_leaudit.services.impl.documentServiceImpl import Do
|
|||||||
class CrossReviewServiceImpl(ICrossReviewService):
|
class CrossReviewServiceImpl(ICrossReviewService):
|
||||||
"""交叉评查服务实现。"""
|
"""交叉评查服务实现。"""
|
||||||
|
|
||||||
|
_PROPOSAL_EXPORT_HEADERS: tuple[str, ...] = (
|
||||||
|
"任务名称",
|
||||||
|
"文档名称",
|
||||||
|
"意见分类",
|
||||||
|
"评查点名称",
|
||||||
|
"问题依据/命中内容",
|
||||||
|
"原问题说明",
|
||||||
|
"评查意见",
|
||||||
|
"分数调整",
|
||||||
|
"提案状态",
|
||||||
|
"发起人",
|
||||||
|
"提出时间",
|
||||||
|
"同意人数",
|
||||||
|
"同意人员",
|
||||||
|
"不同意人数",
|
||||||
|
"不同意人员",
|
||||||
|
"未投人数",
|
||||||
|
"未投人员",
|
||||||
|
)
|
||||||
|
|
||||||
|
_VOTE_EXPORT_HEADERS: tuple[str, ...] = (
|
||||||
|
"任务名称",
|
||||||
|
"文档名称",
|
||||||
|
"意见分类",
|
||||||
|
"评查点名称",
|
||||||
|
"发起人",
|
||||||
|
"投票人",
|
||||||
|
"投票状态",
|
||||||
|
"投票时间",
|
||||||
|
)
|
||||||
|
|
||||||
_SCHEMA_BOOTSTRAP_STATEMENTS: tuple[str, ...] = (
|
_SCHEMA_BOOTSTRAP_STATEMENTS: tuple[str, ...] = (
|
||||||
"""
|
"""
|
||||||
CREATE TABLE IF NOT EXISTS leaudit_cross_review_tasks (
|
CREATE TABLE IF NOT EXISTS leaudit_cross_review_tasks (
|
||||||
@@ -110,8 +146,11 @@ class CrossReviewServiceImpl(ICrossReviewService):
|
|||||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
task_id BIGINT NOT NULL,
|
task_id BIGINT NOT NULL,
|
||||||
document_id BIGINT NOT NULL,
|
document_id BIGINT NOT NULL,
|
||||||
rule_result_id BIGINT NOT NULL,
|
proposal_type VARCHAR(32) NOT NULL DEFAULT 'review_point',
|
||||||
|
rule_result_id BIGINT,
|
||||||
proposer_id BIGINT NOT NULL,
|
proposer_id BIGINT NOT NULL,
|
||||||
|
evaluation_point_name VARCHAR(255),
|
||||||
|
extraction_result_text TEXT,
|
||||||
proposed_score_delta NUMERIC(10, 2) NOT NULL,
|
proposed_score_delta NUMERIC(10, 2) NOT NULL,
|
||||||
reason TEXT NOT NULL,
|
reason TEXT NOT NULL,
|
||||||
status VARCHAR(32) NOT NULL DEFAULT 'pending',
|
status VARCHAR(32) NOT NULL DEFAULT 'pending',
|
||||||
@@ -564,6 +603,13 @@ class CrossReviewServiceImpl(ICrossReviewService):
|
|||||||
WHERE rr.document_id = d.id
|
WHERE rr.document_id = d.id
|
||||||
AND rr.run_id = d.current_run_id
|
AND rr.run_id = d.current_run_id
|
||||||
) es ON TRUE
|
) 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 {whereSql}
|
WHERE {whereSql}
|
||||||
ORDER BY d.created_at DESC, d.id DESC
|
ORDER BY d.created_at DESC, d.id DESC
|
||||||
LIMIT :limit OFFSET :offset
|
LIMIT :limit OFFSET :offset
|
||||||
@@ -601,10 +647,16 @@ class CrossReviewServiceImpl(ICrossReviewService):
|
|||||||
errorMessages=self._parse_text_array(row.get("error_messages")),
|
errorMessages=self._parse_text_array(row.get("error_messages")),
|
||||||
issueMessages=self._parse_text_array(row.get("issue_messages")),
|
issueMessages=self._parse_text_array(row.get("issue_messages")),
|
||||||
manualMessages=self._parse_text_array(row.get("manual_messages")),
|
manualMessages=self._parse_text_array(row.get("manual_messages")),
|
||||||
finalScore=float(row.get("final_score") or 0),
|
finalScore=float(row.get("final_score") or 0) + float(row.get("approved_delta") or 0),
|
||||||
fullScore=float(row.get("full_score") or 0),
|
fullScore=float(row.get("full_score") or 0),
|
||||||
scoreSummary=str(row.get("score_summary") or ""),
|
scoreSummary=self._build_score_summary(
|
||||||
scorePercent=float(row.get("score_percent") or 0),
|
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),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
for row in rows
|
for row in rows
|
||||||
]
|
]
|
||||||
@@ -738,13 +790,33 @@ class CrossReviewServiceImpl(ICrossReviewService):
|
|||||||
"""创建交叉评查提案。"""
|
"""创建交叉评查提案。"""
|
||||||
async with GetAsyncSession() as session:
|
async with GetAsyncSession() as session:
|
||||||
await self._ensure_tables_ready(session)
|
await self._ensure_tables_ready(session)
|
||||||
|
proposalType = (Body.proposalType or "review_point").strip().lower()
|
||||||
|
if proposalType not in {"review_point", "supplement"}:
|
||||||
|
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "proposalType 仅支持 review_point、supplement")
|
||||||
if Body.deductionScore == 0:
|
if Body.deductionScore == 0:
|
||||||
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "不允许创建 0 分提案")
|
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "不允许创建 0 分提案")
|
||||||
|
|
||||||
taskId = await self._resolve_task_id_for_document(session, Body.documentId, CurrentUserId)
|
taskId = await self._resolve_task_id_for_document(session, Body.documentId, CurrentUserId)
|
||||||
await self._ensure_task_member(session, taskId, CurrentUserId)
|
await self._ensure_task_member(session, taskId, CurrentUserId)
|
||||||
await self._ensure_task_document(session, taskId, Body.documentId)
|
await self._ensure_task_document(session, taskId, Body.documentId)
|
||||||
ruleResult = await self._load_rule_result(session, Body.reviewPointResultId, Body.documentId)
|
|
||||||
|
ruleResult = None
|
||||||
|
reviewPointResultId: int | None = None
|
||||||
|
evaluationPointName = (Body.evaluationPointName or "").strip()
|
||||||
|
extractionResultText = (Body.extractionResultText or "").strip()
|
||||||
|
|
||||||
|
if proposalType == "review_point":
|
||||||
|
if Body.reviewPointResultId is None:
|
||||||
|
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "reviewPointResultId 不能为空")
|
||||||
|
reviewPointResultId = int(Body.reviewPointResultId)
|
||||||
|
ruleResult = await self._load_rule_result(session, reviewPointResultId, Body.documentId)
|
||||||
|
if not evaluationPointName:
|
||||||
|
evaluationPointName = str(ruleResult.get("rule_name") or ruleResult.get("rule_id") or "")
|
||||||
|
else:
|
||||||
|
if not evaluationPointName:
|
||||||
|
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "补充意见必须填写评查点名称")
|
||||||
|
if not extractionResultText:
|
||||||
|
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "补充意见必须填写抽取结果")
|
||||||
|
|
||||||
duplicateExists = bool(
|
duplicateExists = bool(
|
||||||
await session.scalar(
|
await session.scalar(
|
||||||
@@ -754,25 +826,37 @@ class CrossReviewServiceImpl(ICrossReviewService):
|
|||||||
FROM leaudit_cross_review_proposals
|
FROM leaudit_cross_review_proposals
|
||||||
WHERE task_id = :task_id
|
WHERE task_id = :task_id
|
||||||
AND document_id = :document_id
|
AND document_id = :document_id
|
||||||
AND rule_result_id = :rule_result_id
|
AND proposal_type = :proposal_type
|
||||||
AND proposer_id = :proposer_id
|
AND proposer_id = :proposer_id
|
||||||
AND status IN ('pending', 'approved')
|
AND status IN ('pending', 'approved')
|
||||||
AND delete_time IS NULL
|
AND delete_time IS NULL
|
||||||
|
AND (
|
||||||
|
(:proposal_type = 'review_point' AND rule_result_id = :rule_result_id)
|
||||||
|
OR (
|
||||||
|
:proposal_type = 'supplement'
|
||||||
|
AND COALESCE(evaluation_point_name, '') = :evaluation_point_name
|
||||||
|
AND COALESCE(extraction_result_text, '') = :extraction_result_text
|
||||||
|
)
|
||||||
|
)
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
"""
|
"""
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
"task_id": taskId,
|
"task_id": taskId,
|
||||||
"document_id": Body.documentId,
|
"document_id": Body.documentId,
|
||||||
"rule_result_id": Body.reviewPointResultId,
|
"proposal_type": proposalType,
|
||||||
|
"rule_result_id": reviewPointResultId,
|
||||||
|
"evaluation_point_name": evaluationPointName,
|
||||||
|
"extraction_result_text": extractionResultText,
|
||||||
"proposer_id": CurrentUserId,
|
"proposer_id": CurrentUserId,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if duplicateExists:
|
if duplicateExists:
|
||||||
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "当前评查点已存在您的有效提案")
|
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "当前内容已存在您的有效提案")
|
||||||
|
|
||||||
currentScore, fullScore = await self._calculate_current_score(session, Body.reviewPointResultId, Body.documentId)
|
if proposalType == "review_point":
|
||||||
|
currentScore, fullScore = await self._calculate_current_score(session, reviewPointResultId, Body.documentId)
|
||||||
if Body.deductionScore < 0 and currentScore <= 0:
|
if Body.deductionScore < 0 and currentScore <= 0:
|
||||||
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "当前分值已为 0,不能继续扣分")
|
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "当前分值已为 0,不能继续扣分")
|
||||||
if Body.deductionScore > 0 and currentScore >= fullScore:
|
if Body.deductionScore > 0 and currentScore >= fullScore:
|
||||||
@@ -785,17 +869,42 @@ class CrossReviewServiceImpl(ICrossReviewService):
|
|||||||
text(
|
text(
|
||||||
"""
|
"""
|
||||||
INSERT INTO leaudit_cross_review_proposals
|
INSERT INTO leaudit_cross_review_proposals
|
||||||
(task_id, document_id, rule_result_id, proposer_id, proposed_score_delta, reason, status)
|
(
|
||||||
|
task_id,
|
||||||
|
document_id,
|
||||||
|
proposal_type,
|
||||||
|
rule_result_id,
|
||||||
|
proposer_id,
|
||||||
|
evaluation_point_name,
|
||||||
|
extraction_result_text,
|
||||||
|
proposed_score_delta,
|
||||||
|
reason,
|
||||||
|
status
|
||||||
|
)
|
||||||
VALUES
|
VALUES
|
||||||
(:task_id, :document_id, :rule_result_id, :proposer_id, :proposed_score_delta, :reason, 'pending')
|
(
|
||||||
|
:task_id,
|
||||||
|
:document_id,
|
||||||
|
:proposal_type,
|
||||||
|
:rule_result_id,
|
||||||
|
:proposer_id,
|
||||||
|
:evaluation_point_name,
|
||||||
|
:extraction_result_text,
|
||||||
|
:proposed_score_delta,
|
||||||
|
:reason,
|
||||||
|
'pending'
|
||||||
|
)
|
||||||
RETURNING id, create_time
|
RETURNING id, create_time
|
||||||
"""
|
"""
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
"task_id": taskId,
|
"task_id": taskId,
|
||||||
"document_id": Body.documentId,
|
"document_id": Body.documentId,
|
||||||
"rule_result_id": Body.reviewPointResultId,
|
"proposal_type": proposalType,
|
||||||
|
"rule_result_id": reviewPointResultId,
|
||||||
"proposer_id": CurrentUserId,
|
"proposer_id": CurrentUserId,
|
||||||
|
"evaluation_point_name": evaluationPointName or None,
|
||||||
|
"extraction_result_text": extractionResultText or None,
|
||||||
"proposed_score_delta": Body.deductionScore,
|
"proposed_score_delta": Body.deductionScore,
|
||||||
"reason": Body.auditOpinion.strip(),
|
"reason": Body.auditOpinion.strip(),
|
||||||
},
|
},
|
||||||
@@ -965,6 +1074,74 @@ class CrossReviewServiceImpl(ICrossReviewService):
|
|||||||
pendingProposals=pendingProposals,
|
pendingProposals=pendingProposals,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def ExportDocumentProposals(self, CurrentUserId: int, DocumentId: int) -> tuple[bytes, str]:
|
||||||
|
"""导出文档交叉评查意见 Excel。"""
|
||||||
|
async with GetAsyncSession() as session:
|
||||||
|
await self._ensure_tables_ready(session)
|
||||||
|
taskId = await self._resolve_task_id_for_document(session, DocumentId, CurrentUserId)
|
||||||
|
await self._ensure_task_member(session, taskId, CurrentUserId)
|
||||||
|
|
||||||
|
documentMeta = await self._load_document_export_meta(session, taskId, DocumentId)
|
||||||
|
proposalPage = await self._build_document_proposals_page(session, CurrentUserId, taskId, DocumentId, 1, 1000)
|
||||||
|
|
||||||
|
defaultRows: list[dict[str, object]] = []
|
||||||
|
supplementRows: list[dict[str, object]] = []
|
||||||
|
voteRows: list[dict[str, object]] = []
|
||||||
|
taskMembers = await self._load_task_member_names(session, taskId)
|
||||||
|
|
||||||
|
for item in proposalPage.items:
|
||||||
|
row = self._build_export_summary_row(
|
||||||
|
taskId=taskId,
|
||||||
|
taskName=documentMeta["task_name"],
|
||||||
|
documentId=DocumentId,
|
||||||
|
documentName=documentMeta["document_name"],
|
||||||
|
item=item,
|
||||||
|
)
|
||||||
|
if item.proposalType == "supplement":
|
||||||
|
supplementRows.append(row)
|
||||||
|
else:
|
||||||
|
defaultRows.append(row)
|
||||||
|
|
||||||
|
votedNames = {vote.voter for vote in item.votes}
|
||||||
|
for vote in item.votes:
|
||||||
|
voteRows.append(
|
||||||
|
{
|
||||||
|
"任务名称": documentMeta["task_name"],
|
||||||
|
"文档名称": documentMeta["document_name"],
|
||||||
|
"意见分类": "补充意见" if item.proposalType == "supplement" else "默认规则意见",
|
||||||
|
"评查点名称": item.evaluationPointName,
|
||||||
|
"发起人": item.proposer,
|
||||||
|
"投票人": vote.voter,
|
||||||
|
"投票结果": self._translate_vote_type(vote.voteType),
|
||||||
|
"投票时间": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
for _, memberName in taskMembers.items():
|
||||||
|
if memberName in votedNames:
|
||||||
|
continue
|
||||||
|
voteRows.append(
|
||||||
|
{
|
||||||
|
"任务名称": documentMeta["task_name"],
|
||||||
|
"文档名称": documentMeta["document_name"],
|
||||||
|
"意见分类": "补充意见" if item.proposalType == "supplement" else "默认规则意见",
|
||||||
|
"评查点名称": item.evaluationPointName,
|
||||||
|
"发起人": item.proposer,
|
||||||
|
"投票人": memberName,
|
||||||
|
"投票结果": "未投",
|
||||||
|
"投票时间": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
workbook = self._build_simple_xlsx(
|
||||||
|
sheets=[
|
||||||
|
("默认规则意见", list(self._PROPOSAL_EXPORT_HEADERS), defaultRows),
|
||||||
|
("补充意见", list(self._PROPOSAL_EXPORT_HEADERS), supplementRows),
|
||||||
|
("投票明细", list(self._VOTE_EXPORT_HEADERS), voteRows),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
safeName = quote(f"交叉评查意见_{documentMeta['document_name']}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx")
|
||||||
|
return workbook, safeName
|
||||||
|
|
||||||
async def UploadTaskDocument(
|
async def UploadTaskDocument(
|
||||||
self,
|
self,
|
||||||
CurrentUserId: int,
|
CurrentUserId: int,
|
||||||
@@ -1105,8 +1282,11 @@ class CrossReviewServiceImpl(ICrossReviewService):
|
|||||||
p.id,
|
p.id,
|
||||||
p.task_id,
|
p.task_id,
|
||||||
p.document_id,
|
p.document_id,
|
||||||
|
p.proposal_type,
|
||||||
p.rule_result_id,
|
p.rule_result_id,
|
||||||
p.proposer_id,
|
p.proposer_id,
|
||||||
|
p.evaluation_point_name,
|
||||||
|
p.extraction_result_text,
|
||||||
p.proposed_score_delta,
|
p.proposed_score_delta,
|
||||||
p.reason,
|
p.reason,
|
||||||
p.status,
|
p.status,
|
||||||
@@ -1145,7 +1325,10 @@ class CrossReviewServiceImpl(ICrossReviewService):
|
|||||||
items.append(
|
items.append(
|
||||||
CrossReviewProposalItemVO(
|
CrossReviewProposalItemVO(
|
||||||
proposalId=proposalId,
|
proposalId=proposalId,
|
||||||
evaluationPointName=str(row.get("rule_name") or ""),
|
proposalType=str(row.get("proposal_type") or "review_point"),
|
||||||
|
reviewPointResultId=self._to_int(row.get("rule_result_id")),
|
||||||
|
evaluationPointName=str(row.get("evaluation_point_name") or row.get("rule_name") or ""),
|
||||||
|
extractionResultText=str(row.get("extraction_result_text") or ""),
|
||||||
proposedScore=float(row["proposed_score_delta"] or 0),
|
proposedScore=float(row["proposed_score_delta"] or 0),
|
||||||
reason=str(row.get("reason") or ""),
|
reason=str(row.get("reason") or ""),
|
||||||
proposer=str(row.get("proposer_name") or ""),
|
proposer=str(row.get("proposer_name") or ""),
|
||||||
@@ -1160,7 +1343,7 @@ class CrossReviewServiceImpl(ICrossReviewService):
|
|||||||
disagreeVoters=disagreeVoters,
|
disagreeVoters=disagreeVoters,
|
||||||
pendingVoters=pendingVoters,
|
pendingVoters=pendingVoters,
|
||||||
canVote=canVote,
|
canVote=canVote,
|
||||||
problemMessage=str(row.get("fail_message") or row.get("rule_name") or ""),
|
problemMessage=str(row.get("fail_message") or row.get("extraction_result_text") or row.get("rule_name") or ""),
|
||||||
proposerId=int(row["proposer_id"]),
|
proposerId=int(row["proposer_id"]),
|
||||||
createdAt=row.get("create_time"),
|
createdAt=row.get("create_time"),
|
||||||
status=str(row.get("status") or "pending"),
|
status=str(row.get("status") or "pending"),
|
||||||
@@ -1288,6 +1471,38 @@ class CrossReviewServiceImpl(ICrossReviewService):
|
|||||||
raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "交叉评查提案不存在")
|
raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "交叉评查提案不存在")
|
||||||
return row
|
return row
|
||||||
|
|
||||||
|
async def _load_document_export_meta(self, session, taskId: int, documentId: int) -> dict[str, str]:
|
||||||
|
row = (
|
||||||
|
await session.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
t.task_name,
|
||||||
|
COALESCE(f.file_name, d.normalized_name, CAST(d.id AS TEXT)) AS document_name
|
||||||
|
FROM leaudit_cross_review_tasks t
|
||||||
|
JOIN leaudit_documents d
|
||||||
|
ON d.id = :document_id
|
||||||
|
LEFT JOIN leaudit_document_files f
|
||||||
|
ON f.document_id = d.id
|
||||||
|
AND f.is_active = true
|
||||||
|
AND f.deleted_at IS NULL
|
||||||
|
AND f.file_role IN ('original', 'primary')
|
||||||
|
WHERE t.id = :task_id
|
||||||
|
AND t.delete_time IS NULL
|
||||||
|
AND d.deleted_at IS NULL
|
||||||
|
LIMIT 1
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{"task_id": taskId, "document_id": documentId},
|
||||||
|
)
|
||||||
|
).mappings().first()
|
||||||
|
if not row:
|
||||||
|
raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "导出目标不存在")
|
||||||
|
return {
|
||||||
|
"task_name": str(row.get("task_name") or f"任务{taskId}"),
|
||||||
|
"document_name": str(row.get("document_name") or f"文档{documentId}"),
|
||||||
|
}
|
||||||
|
|
||||||
async def _load_task_member_names(self, session, taskId: int) -> dict[int, str]:
|
async def _load_task_member_names(self, session, taskId: int) -> dict[int, str]:
|
||||||
rows = (
|
rows = (
|
||||||
await session.execute(
|
await session.execute(
|
||||||
@@ -1469,6 +1684,41 @@ class CrossReviewServiceImpl(ICrossReviewService):
|
|||||||
if not exists:
|
if not exists:
|
||||||
raise LeauditException(StatusCodeEnum.HTTP_500_INTERNAL_SERVER_ERROR, f"交叉评查表未初始化: {tableName}")
|
raise LeauditException(StatusCodeEnum.HTTP_500_INTERNAL_SERVER_ERROR, f"交叉评查表未初始化: {tableName}")
|
||||||
|
|
||||||
|
proposalColumns = await self._load_table_columns(session, "leaudit_cross_review_proposals")
|
||||||
|
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'")
|
||||||
|
)
|
||||||
|
if "evaluation_point_name" not in proposalColumns:
|
||||||
|
await session.execute(
|
||||||
|
text("ALTER TABLE leaudit_cross_review_proposals ADD COLUMN evaluation_point_name VARCHAR(255)")
|
||||||
|
)
|
||||||
|
if "extraction_result_text" not in proposalColumns:
|
||||||
|
await session.execute(
|
||||||
|
text("ALTER TABLE leaudit_cross_review_proposals ADD COLUMN extraction_result_text TEXT")
|
||||||
|
)
|
||||||
|
if proposalColumns:
|
||||||
|
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")
|
||||||
|
)
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
async def _ensure_task_member(self, session, taskId: int, userId: int) -> None:
|
async def _ensure_task_member(self, session, taskId: int, userId: int) -> None:
|
||||||
"""校验当前用户是否是任务成员。"""
|
"""校验当前用户是否是任务成员。"""
|
||||||
exists = bool(
|
exists = bool(
|
||||||
@@ -1522,3 +1772,283 @@ class CrossReviewServiceImpl(ICrossReviewService):
|
|||||||
if isinstance(value, list):
|
if isinstance(value, list):
|
||||||
return [str(v) for v in value]
|
return [str(v) for v in value]
|
||||||
return [str(value)]
|
return [str(value)]
|
||||||
|
|
||||||
|
def _build_score_summary(self, finalScore: float, fullScore: float) -> str:
|
||||||
|
if fullScore <= 0:
|
||||||
|
return "0/0"
|
||||||
|
return f"{round(finalScore, 1)}/{round(fullScore, 1)}"
|
||||||
|
|
||||||
|
def _build_score_percent(self, finalScore: float, fullScore: float) -> float:
|
||||||
|
if fullScore <= 0:
|
||||||
|
return 0.0
|
||||||
|
return round(finalScore / fullScore * 100, 1)
|
||||||
|
|
||||||
|
def _build_export_summary_row(
|
||||||
|
self,
|
||||||
|
taskId: int,
|
||||||
|
taskName: str,
|
||||||
|
documentId: int,
|
||||||
|
documentName: str,
|
||||||
|
item: CrossReviewProposalItemVO,
|
||||||
|
) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"任务名称": taskName,
|
||||||
|
"文档名称": documentName,
|
||||||
|
"意见分类": "补充意见" if item.proposalType == "supplement" else "默认规则意见",
|
||||||
|
"评查点名称": item.evaluationPointName,
|
||||||
|
"问题依据/命中内容": item.extractionResultText or item.problemMessage or "",
|
||||||
|
"原问题说明": item.problemMessage or "",
|
||||||
|
"评查意见": item.reason,
|
||||||
|
"分数调整": item.proposedScore,
|
||||||
|
"提案状态": self._translate_proposal_status(item.status),
|
||||||
|
"发起人": item.proposer,
|
||||||
|
"提出时间": self._format_datetime(item.createdAt),
|
||||||
|
"同意人数": len(item.agreeVoters),
|
||||||
|
"同意人员": "、".join(item.agreeVoters),
|
||||||
|
"不同意人数": len(item.disagreeVoters),
|
||||||
|
"不同意人员": "、".join(item.disagreeVoters),
|
||||||
|
"未投人数": len(item.pendingVoters),
|
||||||
|
"未投人员": "、".join(item.pendingVoters),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _translate_vote_type(self, voteType: str) -> str:
|
||||||
|
return {
|
||||||
|
"agree": "同意",
|
||||||
|
"disagree": "不同意",
|
||||||
|
"cancel": "撤销",
|
||||||
|
}.get(voteType, voteType or "")
|
||||||
|
|
||||||
|
def _translate_proposal_status(self, status: str) -> str:
|
||||||
|
return {
|
||||||
|
"pending": "待定",
|
||||||
|
"approved": "已通过",
|
||||||
|
"rejected": "已驳回",
|
||||||
|
"cancelled": "已撤销",
|
||||||
|
}.get(status, status or "")
|
||||||
|
|
||||||
|
def _format_datetime(self, value) -> str:
|
||||||
|
if value is None:
|
||||||
|
return ""
|
||||||
|
if isinstance(value, datetime):
|
||||||
|
return value.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
def _build_simple_xlsx(self, sheets: list[tuple[str, list[str], list[dict[str, object]]]]) -> bytes:
|
||||||
|
normalized_sheets: list[tuple[str, list[str], list[list[str]]]] = []
|
||||||
|
for sheetName, headers, rows in sheets:
|
||||||
|
tableRows = [[self._excel_cell_text(row.get(header)) for header in headers] for row in rows]
|
||||||
|
normalized_sheets.append((sheetName, headers, tableRows))
|
||||||
|
|
||||||
|
sharedStrings: list[str] = []
|
||||||
|
stringIndex: dict[str, int] = {}
|
||||||
|
|
||||||
|
def intern(value: str) -> int:
|
||||||
|
if value in stringIndex:
|
||||||
|
return stringIndex[value]
|
||||||
|
idx = len(sharedStrings)
|
||||||
|
stringIndex[value] = idx
|
||||||
|
sharedStrings.append(value)
|
||||||
|
return idx
|
||||||
|
|
||||||
|
for _, headers, rows in normalized_sheets:
|
||||||
|
for header in headers:
|
||||||
|
intern(header)
|
||||||
|
for row in rows:
|
||||||
|
for cell in row:
|
||||||
|
intern(cell)
|
||||||
|
|
||||||
|
buffer = io.BytesIO()
|
||||||
|
with ZipFile(buffer, "w", ZIP_DEFLATED) as zf:
|
||||||
|
zf.writestr("[Content_Types].xml", self._xlsx_content_types(len(normalized_sheets)))
|
||||||
|
zf.writestr("_rels/.rels", self._xlsx_root_rels())
|
||||||
|
zf.writestr("xl/workbook.xml", self._xlsx_workbook([sheet[0] for sheet in normalized_sheets]))
|
||||||
|
zf.writestr("xl/_rels/workbook.xml.rels", self._xlsx_workbook_rels(len(normalized_sheets)))
|
||||||
|
zf.writestr("xl/styles.xml", self._xlsx_styles())
|
||||||
|
zf.writestr("xl/sharedStrings.xml", self._xlsx_shared_strings(sharedStrings))
|
||||||
|
for index, (_, headers, rows) in enumerate(normalized_sheets, start=1):
|
||||||
|
zf.writestr(f"xl/worksheets/sheet{index}.xml", self._xlsx_sheet(headers, rows, intern))
|
||||||
|
zf.writestr("docProps/core.xml", self._xlsx_core_props())
|
||||||
|
zf.writestr("docProps/app.xml", self._xlsx_app_props([sheet[0] for sheet in normalized_sheets]))
|
||||||
|
return buffer.getvalue()
|
||||||
|
|
||||||
|
def _excel_cell_text(self, value: object) -> str:
|
||||||
|
if value is None:
|
||||||
|
return ""
|
||||||
|
if isinstance(value, float):
|
||||||
|
return str(round(value, 2))
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
def _xlsx_content_types(self, sheetCount: int) -> str:
|
||||||
|
overrides = [
|
||||||
|
'<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>',
|
||||||
|
'<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>',
|
||||||
|
'<Override PartName="/xl/sharedStrings.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"/>',
|
||||||
|
'<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>',
|
||||||
|
'<Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>',
|
||||||
|
]
|
||||||
|
overrides.extend(
|
||||||
|
f'<Override PartName="/xl/worksheets/sheet{index}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>'
|
||||||
|
for index in range(1, sheetCount + 1)
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
||||||
|
'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">'
|
||||||
|
'<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>'
|
||||||
|
'<Default Extension="xml" ContentType="application/xml"/>'
|
||||||
|
+ "".join(overrides)
|
||||||
|
+ "</Types>"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _xlsx_root_rels(self) -> str:
|
||||||
|
return (
|
||||||
|
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
||||||
|
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
|
||||||
|
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>'
|
||||||
|
'<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>'
|
||||||
|
'<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>'
|
||||||
|
"</Relationships>"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _xlsx_workbook(self, sheetNames: list[str]) -> str:
|
||||||
|
sheets = "".join(
|
||||||
|
f'<sheet name="{escape(name)}" sheetId="{index}" r:id="rId{index}"/>'
|
||||||
|
for index, name in enumerate(sheetNames, start=1)
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
||||||
|
'<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" '
|
||||||
|
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">'
|
||||||
|
f"<sheets>{sheets}</sheets>"
|
||||||
|
"</workbook>"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _xlsx_workbook_rels(self, sheetCount: int) -> str:
|
||||||
|
relationships = []
|
||||||
|
for index in range(1, sheetCount + 1):
|
||||||
|
relationships.append(
|
||||||
|
f'<Relationship Id="rId{index}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet{index}.xml"/>'
|
||||||
|
)
|
||||||
|
relationships.append(
|
||||||
|
f'<Relationship Id="rId{sheetCount + 1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>'
|
||||||
|
)
|
||||||
|
relationships.append(
|
||||||
|
f'<Relationship Id="rId{sheetCount + 2}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" Target="sharedStrings.xml"/>'
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
||||||
|
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
|
||||||
|
+ "".join(relationships)
|
||||||
|
+ "</Relationships>"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _xlsx_styles(self) -> str:
|
||||||
|
return (
|
||||||
|
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
||||||
|
'<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">'
|
||||||
|
'<fonts count="2">'
|
||||||
|
'<font><sz val="11"/><name val="Calibri"/></font>'
|
||||||
|
'<font><b/><sz val="11"/><name val="Calibri"/></font>'
|
||||||
|
'</fonts>'
|
||||||
|
'<fills count="3">'
|
||||||
|
'<fill><patternFill patternType="none"/></fill>'
|
||||||
|
'<fill><patternFill patternType="gray125"/></fill>'
|
||||||
|
'<fill><patternFill patternType="solid"><fgColor rgb="FFD1F41F"/><bgColor indexed="64"/></patternFill></fill>'
|
||||||
|
'</fills>'
|
||||||
|
'<borders count="2">'
|
||||||
|
'<border><left/><right/><top/><bottom/><diagonal/></border>'
|
||||||
|
'<border>'
|
||||||
|
'<left style="thin"><color auto="1"/></left>'
|
||||||
|
'<right style="thin"><color auto="1"/></right>'
|
||||||
|
'<top style="thin"><color auto="1"/></top>'
|
||||||
|
'<bottom style="thin"><color auto="1"/></bottom>'
|
||||||
|
'<diagonal/>'
|
||||||
|
'</border>'
|
||||||
|
'</borders>'
|
||||||
|
'<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>'
|
||||||
|
'<cellXfs count="2">'
|
||||||
|
'<xf numFmtId="0" fontId="0" fillId="0" borderId="1" xfId="0" applyBorder="1"/>'
|
||||||
|
'<xf numFmtId="0" fontId="1" fillId="2" borderId="1" xfId="0" applyFont="1" applyFill="1" applyBorder="1" applyAlignment="1">'
|
||||||
|
'<alignment vertical="center"/>'
|
||||||
|
'</xf>'
|
||||||
|
'</cellXfs>'
|
||||||
|
'<cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles>'
|
||||||
|
'</styleSheet>'
|
||||||
|
)
|
||||||
|
|
||||||
|
def _xlsx_shared_strings(self, values: list[str]) -> str:
|
||||||
|
items = "".join(f"<si><t>{escape(value)}</t></si>" for value in values)
|
||||||
|
return (
|
||||||
|
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
||||||
|
f'<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="{len(values)}" uniqueCount="{len(values)}">{items}</sst>'
|
||||||
|
)
|
||||||
|
|
||||||
|
def _xlsx_sheet(self, headers: list[str], rows: list[list[str]], intern) -> str:
|
||||||
|
all_rows: list[list[str]] = []
|
||||||
|
if headers:
|
||||||
|
all_rows.append(headers)
|
||||||
|
all_rows.extend(rows)
|
||||||
|
sheet_rows = []
|
||||||
|
for rowIndex, row in enumerate(all_rows, start=1):
|
||||||
|
cells = []
|
||||||
|
styleIndex = "1" if headers and rowIndex == 1 else "0"
|
||||||
|
for colIndex, value in enumerate(row, start=1):
|
||||||
|
colRef = self._xlsx_column_name(colIndex)
|
||||||
|
cells.append(f'<c r="{colRef}{rowIndex}" t="s" s="{styleIndex}"><v>{intern(value)}</v></c>')
|
||||||
|
sheet_rows.append(f'<row r="{rowIndex}" ht="26" customHeight="1">{"".join(cells)}</row>')
|
||||||
|
return (
|
||||||
|
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
||||||
|
'<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">'
|
||||||
|
f'<sheetData>{"".join(sheet_rows)}</sheetData>'
|
||||||
|
'</worksheet>'
|
||||||
|
)
|
||||||
|
|
||||||
|
def _xlsx_column_name(self, columnIndex: int) -> str:
|
||||||
|
result = ""
|
||||||
|
current = columnIndex
|
||||||
|
while current > 0:
|
||||||
|
current, remainder = divmod(current - 1, 26)
|
||||||
|
result = chr(65 + remainder) + result
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _xlsx_core_props(self) -> str:
|
||||||
|
now = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
return (
|
||||||
|
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
||||||
|
'<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" '
|
||||||
|
'xmlns:dc="http://purl.org/dc/elements/1.1/" '
|
||||||
|
'xmlns:dcterms="http://purl.org/dc/terms/" '
|
||||||
|
'xmlns:dcmitype="http://purl.org/dc/dcmitype/" '
|
||||||
|
'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">'
|
||||||
|
'<dc:creator>LeAudit</dc:creator>'
|
||||||
|
'<cp:lastModifiedBy>LeAudit</cp:lastModifiedBy>'
|
||||||
|
f'<dcterms:created xsi:type="dcterms:W3CDTF">{now}</dcterms:created>'
|
||||||
|
f'<dcterms:modified xsi:type="dcterms:W3CDTF">{now}</dcterms:modified>'
|
||||||
|
'</cp:coreProperties>'
|
||||||
|
)
|
||||||
|
|
||||||
|
def _xlsx_app_props(self, sheetNames: list[str]) -> str:
|
||||||
|
parts = "".join(f"<vt:lpstr>{escape(name)}</vt:lpstr>" for name in sheetNames)
|
||||||
|
return (
|
||||||
|
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
||||||
|
'<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" '
|
||||||
|
'xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">'
|
||||||
|
'<Application>LeAudit</Application>'
|
||||||
|
f'<TitlesOfParts><vt:vector size="{len(sheetNames)}" baseType="lpstr">{parts}</vt:vector></TitlesOfParts>'
|
||||||
|
'</Properties>'
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _load_table_columns(self, session, tableName: str) -> set[str]:
|
||||||
|
rows = (
|
||||||
|
await session.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
SELECT column_name
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = current_schema()
|
||||||
|
AND table_name = :table_name
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{"table_name": tableName},
|
||||||
|
)
|
||||||
|
).mappings().all()
|
||||||
|
return {str(row["column_name"]) for row in rows if row.get("column_name")}
|
||||||
|
|||||||
Reference in New Issue
Block a user