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
@@ -0,0 +1,55 @@
from fastapi_modules.fastapi_leaudit.services.impl.crossReviewServiceImpl import CrossReviewServiceImpl
def test_cross_review_task_item_masks_progress_without_permission():
item = CrossReviewServiceImpl._build_task_item_vo(
row={
"task_id": 10,
"task_name": "交叉评查任务",
"task_type": "CITY",
"doc_type_id": 2,
"doc_type_code": "contract",
"status": "in_progress",
"total_documents": 4,
"completed_documents": 1,
"current_user_role": "principal",
"current_user_can_confirm": True,
"create_time": None,
"evaluation_tenants": [],
"evaluation_regions": [],
},
CanViewProgress=False,
)
assert item.progress is None
assert item.totalDocuments is None
assert item.completedDocuments is None
assert item.currentUserRole == "principal"
assert item.currentUserCanConfirm is True
def test_cross_review_task_item_keeps_progress_with_permission():
item = CrossReviewServiceImpl._build_task_item_vo(
row={
"task_id": 10,
"task_name": "交叉评查任务",
"task_type": "CITY",
"doc_type_id": 2,
"doc_type_code": "contract",
"status": "in_progress",
"total_documents": 4,
"completed_documents": 1,
"current_user_role": "participant",
"current_user_can_confirm": False,
"create_time": None,
"evaluation_tenants": [],
"evaluation_regions": [],
},
CanViewProgress=True,
)
assert item.progress == 25
assert item.totalDocuments == 4
assert item.completedDocuments == 1
assert item.currentUserRole == "participant"
assert item.currentUserCanConfirm is False
+128
View File
@@ -0,0 +1,128 @@
"""内部公文权限控制测试。"""
import pytest
from starlette.responses import JSONResponse
from fastapi_modules.fastapi_leaudit.controllers.govdocController import GovdocController
class _DenyPermissionService:
"""拒绝所有权限的测试权限服务。"""
async def CheckPermission(self, user_id: int, permission_key: str) -> bool:
"""检查权限。"""
return False
class _AllowPermissionService:
"""允许所有权限的测试权限服务。"""
async def CheckPermission(self, user_id: int, permission_key: str) -> bool:
"""检查权限。"""
return True
class _FakeGovdocService:
"""记录调用的测试公文服务。"""
def __init__(self) -> None:
self.list_called = False
self.upload_called = False
async def ListDocuments(self, **kwargs):
"""记录列表调用。"""
self.list_called = True
return {"items": [], "total": 0, "page": kwargs["page"], "pageSize": kwargs["pageSize"]}
async def UploadDocument(self, **kwargs):
"""记录上传调用。"""
self.upload_called = True
return {"documentId": 1}
def _find_endpoint(controller: GovdocController, path: str, method: str):
"""根据路径和方法查找路由 endpoint。"""
full_path = f"{controller.router.prefix}{path}"
for route in controller.router.routes:
if getattr(route, "path", "") == full_path and method in getattr(route, "methods", set()):
return route.endpoint
raise AssertionError(f"未找到路由 {method} {full_path}")
@pytest.mark.asyncio
async def test_govdoc_list_requires_document_read_permission():
"""公文列表无查看权限时返回 403,且不调用业务服务。"""
controller = GovdocController()
service = _FakeGovdocService()
controller.GovdocService = service
controller.PermissionService = _DenyPermissionService()
endpoint = _find_endpoint(controller, "/documents", "GET")
response = await endpoint(
page=1,
pageSize=20,
keyword=None,
fileExt=None,
region=None,
tenant_code=None,
entry_module_id=None,
type_ids=None,
document_type_id=None,
status=None,
resultStatus=None,
createdBy=None,
dateFrom=None,
dateTo=None,
payload={"user_id": 7},
)
assert isinstance(response, JSONResponse)
assert response.status_code == 403
assert service.list_called is False
@pytest.mark.asyncio
async def test_govdoc_upload_requires_document_create_permission():
"""公文上传无创建权限时返回 403,且不调用业务服务。"""
controller = GovdocController()
service = _FakeGovdocService()
controller.GovdocService = service
controller.PermissionService = _DenyPermissionService()
endpoint = _find_endpoint(controller, "/documents", "POST")
response = await endpoint(file=object(), payload={"user_id": 7})
assert isinstance(response, JSONResponse)
assert response.status_code == 403
assert service.upload_called is False
@pytest.mark.asyncio
async def test_govdoc_list_calls_service_when_permission_granted():
"""公文列表有查看权限时正常调用业务服务。"""
controller = GovdocController()
service = _FakeGovdocService()
controller.GovdocService = service
controller.PermissionService = _AllowPermissionService()
endpoint = _find_endpoint(controller, "/documents", "GET")
response = await endpoint(
page=1,
pageSize=20,
keyword=None,
fileExt=None,
region=None,
tenant_code=None,
entry_module_id=None,
type_ids=None,
document_type_id=None,
status=None,
resultStatus=None,
createdBy=None,
dateFrom=None,
dateTo=None,
payload={"user_id": 7},
)
assert response.data["total"] == 0
assert service.list_called is True
+73
View File
@@ -0,0 +1,73 @@
"""首页统计接口测试。"""
import pytest
from fastapi_modules.fastapi_leaudit.services.impl.homeServiceImpl import HomeServiceImpl
class _FakeDocument:
def __init__(self, *, audit_status: int, failed_count: int = 0, updated_at: str = "2026-05-23T10:00:00") -> None:
self.auditStatus = audit_status
self.failedCount = failed_count
self.updatedAt = updated_at
class _FakePage:
def __init__(self, *, documents, total_pages: int) -> None:
self.documents = documents
self.totalPages = total_pages
class _FakeDocumentService:
def __init__(self) -> None:
self.calls = []
async def ListDocuments(self, **kwargs):
self.calls.append(kwargs)
page = kwargs["Page"]
if page == 1:
return _FakePage(
documents=[
_FakeDocument(audit_status=0, updated_at="2026-05-23T09:00:00"),
_FakeDocument(audit_status=2, updated_at="2026-05-23T09:30:00"),
_FakeDocument(audit_status=1, failed_count=0, updated_at="2026-05-03T10:00:00"),
_FakeDocument(audit_status=1, failed_count=2, updated_at="2026-05-20T10:00:00"),
],
total_pages=2,
)
if page == 2:
return _FakePage(
documents=[
_FakeDocument(audit_status=1, failed_count=1, updated_at="2026-05-22T10:00:00"),
_FakeDocument(audit_status=1, failed_count=0, updated_at="2026-04-18T10:00:00"),
_FakeDocument(audit_status=0, updated_at="2026-05-10T10:00:00"),
],
total_pages=2,
)
return _FakePage(documents=[], total_pages=2)
@pytest.mark.asyncio
async def test_home_dashboard_statistics_uses_entry_scope_and_all_pages():
"""首页统计按入口模块与文档类型过滤,并拉取全量分页后计算。"""
document_service = _FakeDocumentService()
service = HomeServiceImpl(DocumentService=document_service)
result = await service.GetDashboardStatistics(
UserId=7,
Today="2026-05-23",
TypeIds=[10, 11],
EntryModuleId=3,
)
assert result.todayPendingFiles == 2
assert result.monthlyReviewedFiles == 3
assert result.monthlyPassRate == 33
assert result.issuesDetected == 3
assert result.monthlyReviewGrowth.value == 200
assert result.monthlyReviewGrowth.isUp is True
assert {call["EntryModuleId"] for call in document_service.calls} == {3}
assert {tuple(call["TypeIds"]) for call in document_service.calls} == {(10, 11)}
assert [call["Page"] for call in document_service.calls] == [1, 2]
assert all("DateFrom" not in call for call in document_service.calls)
assert all("DateTo" not in call for call in document_service.calls)
+66
View File
@@ -0,0 +1,66 @@
"""企查查配置与客户端测试。"""
from __future__ import annotations
import hashlib
import pytest
from fastapi_admin import config
def test_qichacha_settings_are_exported_from_app_toml():
"""企查查配置应通过 fastapi_admin.config 导出。"""
assert config.QICHACHA_BASE_URL == "https://api.qichacha.com"
assert config.QICHACHA_ENTERPRISE_PATH == "/ECIV4/GetBasicDetailsByName"
assert config.QICHACHA_DISHONESTY_PATH == "/ShixinCheck/GetList"
assert config.QICHACHA_TIMEOUT == 30
assert config.QICHACHA_CACHE_DAYS == 30
def test_qichacha_client_builds_expected_signature_headers(monkeypatch):
"""企查查客户端应按 AppKey + Timespan + SecretKey 生成大写 MD5 Token。"""
from fastapi_modules.fastapi_leaudit.services.impl.qichachaClient import QichachaClient
monkeypatch.setattr("time.time", lambda: 1779433125)
client = QichachaClient(AppKey="app-key", SecretKey="secret-key")
headers = client.BuildHeaders()
expected = hashlib.md5("app-key1779433125secret-key".encode("utf-8")).hexdigest().upper()
assert headers == {"Token": expected, "Timespan": "1779433125"}
@pytest.mark.asyncio
async def test_qichacha_client_parses_enterprise_and_dishonesty(monkeypatch):
"""企查查客户端应分别解析工商与失信接口结果。"""
from fastapi_modules.fastapi_leaudit.services.impl.qichachaClient import QichachaClient
calls: list[tuple[str, dict[str, str]]] = []
async def fake_request(self, Url: str, Params: dict[str, str]) -> dict:
calls.append((Url, Params))
if "GetBasicDetailsByName" in Url:
return {
"Status": "200",
"Message": "成功",
"Result": {"Name": "广州测试有限公司", "CreditCode": "91440000TEST"},
}
return {
"Status": "200",
"Message": "成功",
"Result": {"VerifyResult": 1, "Data": [{"Anno": "案号1"}]},
}
monkeypatch.setattr(QichachaClient, "Request", fake_request)
client = QichachaClient(AppKey="app-key", SecretKey="secret-key")
enterprise = await client.GetEnterpriseInfo("广州测试有限公司")
dishonesty = await client.GetDishonestyInfo("广州测试有限公司")
assert enterprise["Name"] == "广州测试有限公司"
assert enterprise["CreditCode"] == "91440000TEST"
assert dishonesty["VerifyResult"] == 1
assert dishonesty["Data"][0]["Anno"] == "案号1"
assert calls[0][1] == {"key": "app-key", "keyword": "广州测试有限公司"}
assert calls[1][1] == {"key": "app-key", "searchKey": "广州测试有限公司"}
+194
View File
@@ -0,0 +1,194 @@
"""企查查服务测试。"""
from __future__ import annotations
from datetime import UTC, datetime, timedelta
from types import SimpleNamespace
import pytest
class _FakeSession:
async def commit(self):
return None
async def refresh(self, obj):
return obj
async def flush(self):
return None
class _FakeSessionContext:
async def __aenter__(self):
return _FakeSession()
async def __aexit__(self, exc_type, exc, tb):
return None
async def _unexpected_upsert(cls, session, **fields):
pytest.fail("fresh cache should not upsert")
class _FakeClient:
def __init__(self):
self.calls: list[tuple[str, str]] = []
async def QueryCompany(self, Keyword: str):
self.calls.append(("company", Keyword))
return (
{"Name": "广州测试有限公司", "CreditCode": "91440000TEST"},
{"VerifyResult": 1, "Data": [{"Anno": "案号1"}, {"Anno": "案号2"}]},
"91440000TEST",
"广州测试有限公司",
)
async def GetEnterpriseInfo(self, Keyword: str):
self.calls.append(("enterprise", Keyword))
return {"Name": "广州测试有限公司", "CreditCode": "91440000TEST"}
async def GetDishonestyInfo(self, Keyword: str):
self.calls.append(("dishonesty", Keyword))
return {"VerifyResult": 0, "Data": []}
@pytest.mark.asyncio
async def test_query_company_uses_fresh_cache(monkeypatch):
"""未过期缓存应直接返回,不调用企查查 API。"""
from fastapi_modules.fastapi_leaudit.models.qichachaCompanyInfo import QichachaCompanyInfo
from fastapi_modules.fastapi_leaudit.services.impl.qichachaServiceImpl import QichachaServiceImpl
now = datetime.now(UTC)
record = SimpleNamespace(
Id=1,
searchKey="广州测试有限公司",
creditCode="91440000TEST",
companyName="广州测试有限公司",
enterprise={"Name": "广州测试有限公司", "CreditCode": "91440000TEST"},
dishonesty={"VerifyResult": 0, "Data": []},
created_at=now,
updated_at=now,
)
fake_client = _FakeClient()
async def fake_find(cls, session, Keyword):
return record
monkeypatch.setattr(
"fastapi_modules.fastapi_leaudit.services.impl.qichachaServiceImpl.GetAsyncSession",
lambda: _FakeSessionContext(),
)
monkeypatch.setattr(QichachaCompanyInfo, "FindByKeyword", classmethod(fake_find))
monkeypatch.setattr(QichachaCompanyInfo, "Upsert", classmethod(_unexpected_upsert))
service = QichachaServiceImpl(Client=fake_client, CacheDays=30)
result = await service.QueryCompany(Keyword="广州测试有限公司", ForceRefresh=False)
assert result.success is True
assert result.data is not None
assert result.data.companyName == "广州测试有限公司"
assert result.data.hasDishonesty is False
assert result.data.dishonestyCount == 0
assert fake_client.calls == []
@pytest.mark.asyncio
async def test_query_company_refreshes_when_forced(monkeypatch):
"""强制刷新时应调用企查查 API 并写入缓存。"""
from fastapi_modules.fastapi_leaudit.models.qichachaCompanyInfo import QichachaCompanyInfo
from fastapi_modules.fastapi_leaudit.services.impl.qichachaServiceImpl import QichachaServiceImpl
old_record = SimpleNamespace(
Id=1,
searchKey="旧名称",
creditCode=None,
companyName=None,
enterprise=None,
dishonesty=None,
created_at=datetime.now(UTC) - timedelta(days=1),
updated_at=datetime.now(UTC) - timedelta(days=1),
)
saved: dict[str, object] = {}
fake_client = _FakeClient()
async def fake_find(cls, session, Keyword):
return old_record
async def fake_upsert(cls, session, **fields):
saved.update(fields)
return SimpleNamespace(
Id=1,
searchKey=fields["SearchKey"],
creditCode=fields["CreditCode"],
companyName=fields["CompanyName"],
enterprise=fields["Enterprise"],
dishonesty=fields["Dishonesty"],
created_at=datetime.now(UTC),
updated_at=datetime.now(UTC),
)
monkeypatch.setattr(
"fastapi_modules.fastapi_leaudit.services.impl.qichachaServiceImpl.GetAsyncSession",
lambda: _FakeSessionContext(),
)
monkeypatch.setattr(QichachaCompanyInfo, "FindByKeyword", classmethod(fake_find))
monkeypatch.setattr(QichachaCompanyInfo, "Upsert", classmethod(fake_upsert))
service = QichachaServiceImpl(Client=fake_client, CacheDays=30)
result = await service.QueryCompany(Keyword="广州测试有限公司", ForceRefresh=True)
assert fake_client.calls == [("company", "广州测试有限公司")]
assert saved["SearchKey"] == "广州测试有限公司"
assert saved["CreditCode"] == "91440000TEST"
assert result.data is not None
assert result.data.hasDishonesty is True
assert result.data.dishonestyCount == 2
@pytest.mark.asyncio
async def test_query_company_refreshes_stale_cache(monkeypatch):
"""超过缓存天数的数据应自动刷新。"""
from fastapi_modules.fastapi_leaudit.models.qichachaCompanyInfo import QichachaCompanyInfo
from fastapi_modules.fastapi_leaudit.services.impl.qichachaServiceImpl import QichachaServiceImpl
stale_record = SimpleNamespace(
Id=1,
searchKey="广州测试有限公司",
creditCode="OLD",
companyName="旧公司",
enterprise={"Name": "旧公司"},
dishonesty={"VerifyResult": 0, "Data": []},
created_at=datetime.now(UTC) - timedelta(days=90),
updated_at=datetime.now(UTC) - timedelta(days=90),
)
fake_client = _FakeClient()
async def fake_find(cls, session, Keyword):
return stale_record
async def fake_upsert(cls, session, **fields):
return SimpleNamespace(
Id=1,
searchKey=fields["SearchKey"],
creditCode=fields["CreditCode"],
companyName=fields["CompanyName"],
enterprise=fields["Enterprise"],
dishonesty=fields["Dishonesty"],
created_at=datetime.now(UTC),
updated_at=datetime.now(UTC),
)
monkeypatch.setattr(
"fastapi_modules.fastapi_leaudit.services.impl.qichachaServiceImpl.GetAsyncSession",
lambda: _FakeSessionContext(),
)
monkeypatch.setattr(QichachaCompanyInfo, "FindByKeyword", classmethod(fake_find))
monkeypatch.setattr(QichachaCompanyInfo, "Upsert", classmethod(fake_upsert))
service = QichachaServiceImpl(Client=fake_client, CacheDays=30)
result = await service.QueryCompany(Keyword="广州测试有限公司")
assert fake_client.calls == [("company", "广州测试有限公司")]
assert result.data is not None
assert result.data.companyName == "广州测试有限公司"
@@ -0,0 +1,96 @@
from fastapi_modules.fastapi_leaudit.domian.vo.documentVo import DocumentDetailVO
from fastapi_modules.fastapi_leaudit.domian.vo.pageQualityVo import PageQualitySummaryVO
from fastapi_modules.fastapi_leaudit.services.impl.documentServiceImpl import DocumentServiceImpl
def test_review_document_payload_includes_page_quality_summary():
detail = DocumentDetailVO(
documentId=71,
internalDocumentNo=10071,
versionGroupKey="vg-71",
versionNo=1,
rootVersionId=71,
previousVersionId=None,
typeId=10,
typeCode="case",
typeName="行政许可",
groupId=None,
groupName=None,
region="梅州",
tenantCode="MEIZHOU",
tenantName="梅州",
normalizedName="图片模糊测试",
fileId=7001,
fileName="(图片模糊)第71号.pdf",
fileExt="pdf",
mimeType="application/pdf",
fileSize=1024,
ossUrl="/bucket/documents/71.pdf",
processingStatus="completed",
currentRunId=9001,
runStatus="completed",
resultStatus="warning",
latestErrorCode=None,
latestErrorMessage=None,
totalScore=88,
passedCount=2,
failedCount=1,
skippedCount=0,
documentNumber="71",
auditStatus=0,
isTestDocument=False,
pageQualityRunId=501,
pageQualityRunStatus="completed",
pageQualitySummaryStatus="review",
pageQualityIssueCount=2,
pageQualityWarningText="发现疑似模糊页",
updatedAt="2026-05-23T10:00:00",
hasHistory=False,
totalVersions=1,
historyVersions=[],
remark=None,
pageCount=10,
pageQualitySummary=PageQualitySummaryVO(
runId=501,
runStatus="completed",
summaryStatus="review",
totalPages=10,
reviewPageCount=2,
rejectPageCount=0,
warningText="发现疑似模糊页",
pages=[3, 7],
finishedAt="2026-05-23T10:01:00",
),
attachments=[],
)
payload = DocumentServiceImpl._buildReviewPageQualityPayload(
detail,
[
{"pageNum": 7, "qualityStatus": "review", "qualityScore": 0.64, "reasonText": "图片略模糊"},
{"pageNum": 3, "qualityStatus": "reject", "qualityScore": 0.25, "reasonText": "图片严重模糊"},
],
)
assert payload == {
"pageQualityRunId": 501,
"pageQualityRunStatus": "completed",
"pageQualitySummaryStatus": "review",
"pageQualityIssueCount": 2,
"pageQualityWarningText": "发现疑似模糊页",
"pageQualitySummary": {
"runId": 501,
"runStatus": "completed",
"summaryStatus": "review",
"totalPages": 10,
"reviewPageCount": 2,
"rejectPageCount": 0,
"warningText": "发现疑似模糊页",
"pages": [3, 7],
"finishedAt": "2026-05-23T10:01:00",
},
"pageQualityResults": [
{"pageNum": 3, "qualityStatus": "reject", "qualityScore": 0.25, "reasonText": "图片严重模糊"},
{"pageNum": 7, "qualityStatus": "review", "qualityScore": 0.64, "reasonText": "图片略模糊"},
],
}
+23
View File
@@ -1,5 +1,6 @@
from fastapi_common.fastapi_common_web.domain.responses import StatusCodeEnum
from fastapi_common.fastapi_common_web.exception.LeauditException import LeauditException
import pytest
from fastapi_modules.fastapi_leaudit.services.impl.rbacAdminServiceImpl import RbacAdminServiceImpl
from fastapi_modules.fastapi_leaudit.services.impl.rbacServiceImpl import RbacServiceImpl
@@ -225,3 +226,25 @@ def test_permission_cache_is_shared_and_can_invalidate_user():
PermissionServiceImpl.InvalidateUser(12345)
assert 12345 not in first._permission_cache
assert 12345 not in second._permission_cache
@pytest.mark.asyncio
async def test_rbac_admin_permission_assertion_uses_permission_service(monkeypatch):
service = RbacAdminServiceImpl()
checked_permissions: list[tuple[int, str]] = []
async def fake_context(user_id: int):
return {"can_manage": True, "is_super_admin": False}
async def fake_check_permission(self, user_id: int, permission_key: str):
checked_permissions.append((user_id, permission_key))
return permission_key != "rbac:roles:update"
monkeypatch.setattr(service, "_getCurrentUserContext", fake_context)
monkeypatch.setattr(PermissionServiceImpl, "CheckPermission", fake_check_permission)
with pytest.raises(LeauditException) as exc_info:
await service._assertPermissions(99, ["rbac:roles:update"])
assert exc_info.value.status == StatusCodeEnum.HTTP_403_FORBIDDEN
assert checked_permissions == [(99, "rbac:roles:update")]
@@ -0,0 +1,17 @@
"""系统使用统计角色权限语义测试。"""
from fastapi_modules.fastapi_leaudit.services.impl.usageStatsServiceImpl import UsageStatsServiceImpl
def test_usage_stats_service_does_not_require_admin_role_after_controller_permission_check():
"""统计服务不再用管理员角色二次拦截,权限由控制器权限点决定。"""
service = UsageStatsServiceImpl()
service._assert_stats_access(
{
"is_global": False,
"can_manage": False,
"tenant_scope_value": "梅州",
"tenant_code": "MZ",
}
)