diff --git a/src/backend/bisheng/common/constants/telemetry.py b/src/backend/bisheng/common/constants/telemetry.py new file mode 100644 index 000000000..1ed512eca --- /dev/null +++ b/src/backend/bisheng/common/constants/telemetry.py @@ -0,0 +1,8 @@ +KNOWLEDGE_SPACE_CONTENT_STAT_INDEX = "mid_knowledge_space_content_stat" + +KNOWLEDGE_SPACE_DASHBOARD_FILE_LEVELS = ( + "public", + "department", + "team", + "team_ks", +) diff --git a/src/backend/bisheng/common/telemetry/portal_event_service.py b/src/backend/bisheng/common/telemetry/portal_event_service.py index 7c054e02e..b38dce579 100644 --- a/src/backend/bisheng/common/telemetry/portal_event_service.py +++ b/src/backend/bisheng/common/telemetry/portal_event_service.py @@ -2,6 +2,10 @@ import logging from typing import Any from bisheng.common.constants.enums.telemetry import BaseTelemetryTypeEnum +from bisheng.common.constants.telemetry import ( + KNOWLEDGE_SPACE_CONTENT_STAT_INDEX, + KNOWLEDGE_SPACE_DASHBOARD_FILE_LEVELS, +) from bisheng.common.schemas.telemetry.event_data_schema import ( PortalDocumentDownloadEventData, PortalDocumentReadEventData, @@ -223,6 +227,40 @@ class PortalTelemetryEventService: "qa_count": counts[BaseTelemetryTypeEnum.PORTAL_QA.value], } + @staticmethod + async def count_dashboard_files() -> int: + """Count file snapshots with the global dashboard metric definition.""" + es_client = await get_statistics_es_connection() + response = await es_client.search( + index=KNOWLEDGE_SPACE_CONTENT_STAT_INDEX, + body={ + "size": 0, + "query": { + "bool": { + "filter": [ + {"term": {"record_type": "file"}}, + {"term": {"file_type": 1}}, + { + "terms": { + "space_level": list(KNOWLEDGE_SPACE_DASHBOARD_FILE_LEVELS), + } + }, + ] + } + }, + "aggs": { + "total_file_count": { + "value_count": { + "field": "file_id", + } + } + }, + }, + filter_path="aggregations.total_file_count.value", + ) + value = response["aggregations"]["total_file_count"]["value"] + return int(value or 0) + def is_portal_bff_proxy_source(source: str | None) -> bool: return (source or "").strip() == PORTAL_BFF_TELEMETRY_SOURCE diff --git a/src/backend/bisheng/knowledge/api/endpoints/shougang_portal.py b/src/backend/bisheng/knowledge/api/endpoints/shougang_portal.py index e21a8b148..5222ec76a 100644 --- a/src/backend/bisheng/knowledge/api/endpoints/shougang_portal.py +++ b/src/backend/bisheng/knowledge/api/endpoints/shougang_portal.py @@ -22,7 +22,6 @@ from bisheng.knowledge.api.dependencies import ( get_portal_pdf_download_service, ) from bisheng.knowledge.api.portal_pdf_download_response import prepare_portal_pdf_download_response -from bisheng.knowledge.domain.models.knowledge_file import KnowledgeFileDao from bisheng.knowledge.domain.schemas.knowledge_space_schema import ( ChatReq, KnowledgeSpaceFolderStatsReq, @@ -357,7 +356,7 @@ async def get_shougang_portal_home_stats( _ = login_user result, total_files = await asyncio.gather( PortalTelemetryEventService.count_home_events(), - KnowledgeFileDao.async_count_all_success_files(), + PortalTelemetryEventService.count_dashboard_files(), ) return resp_200(ShougangPortalHomeStatsResp(**result, total_files=total_files).model_dump(mode="json")) diff --git a/src/backend/bisheng/telemetry/domain/mid_table/knowledge_space_content.py b/src/backend/bisheng/telemetry/domain/mid_table/knowledge_space_content.py index c96c45e89..0e730b868 100644 --- a/src/backend/bisheng/telemetry/domain/mid_table/knowledge_space_content.py +++ b/src/backend/bisheng/telemetry/domain/mid_table/knowledge_space_content.py @@ -11,6 +11,7 @@ from elasticsearch import helpers from loguru import logger from pydantic import BaseModel, Field +from bisheng.common.constants.telemetry import KNOWLEDGE_SPACE_CONTENT_STAT_INDEX from bisheng.common.schemas.telemetry.base_telemetry_schema import UserDepartmentInfo from bisheng.core.cache.redis_manager import get_redis_client, get_redis_client_sync from bisheng.knowledge.domain.constants import ( @@ -81,8 +82,8 @@ class ProjectionWorkItem: class KnowledgeSpaceContentStat(BaseMidTable): - INDEX_NAME: ClassVar[str] = "mid_knowledge_space_content_stat" - _index_name: str = "mid_knowledge_space_content_stat" + INDEX_NAME: ClassVar[str] = KNOWLEDGE_SPACE_CONTENT_STAT_INDEX + _index_name: str = KNOWLEDGE_SPACE_CONTENT_STAT_INDEX _update_mappings_on_existing: bool = True _include_common_mappings: bool = False _refresh_settings_applied: ClassVar[set[str]] = set() diff --git a/src/backend/bisheng/telemetry_search/domain/init_dataset.py b/src/backend/bisheng/telemetry_search/domain/init_dataset.py index fcdd5d886..6d4e6c865 100644 --- a/src/backend/bisheng/telemetry_search/domain/init_dataset.py +++ b/src/backend/bisheng/telemetry_search/domain/init_dataset.py @@ -1,3 +1,7 @@ +from bisheng.common.constants.telemetry import ( + KNOWLEDGE_SPACE_CONTENT_STAT_INDEX, + KNOWLEDGE_SPACE_DASHBOARD_FILE_LEVELS, +) from bisheng.core.database import get_async_db_session, get_database_connection from bisheng.database.models.group import DefaultGroup from bisheng.database.models.group_resource import GroupResourceDao, GroupResource, ResourceTypeEnum @@ -719,8 +723,8 @@ DASHBOARD_DATASET = [ ), DashboardDataset( dataset_name="知识空间内容统计", - dataset_code="mid_knowledge_space_content_stat", - es_index_name="mid_knowledge_space_content_stat", + dataset_code=KNOWLEDGE_SPACE_CONTENT_STAT_INDEX, + es_index_name=KNOWLEDGE_SPACE_CONTENT_STAT_INDEX, description="知识空间成功文件与文件预览统计数据表", is_commercial_only=False, schema_config=SchemaConfig( @@ -734,7 +738,7 @@ DASHBOARD_DATASET = [ TermOp(field="file_type", value=1), TermsOp( field="space_level", - value=["public", "department", "team", "team_ks"], + value=list(KNOWLEDGE_SPACE_DASHBOARD_FILE_LEVELS), ), ]), aggregations=[ @@ -762,7 +766,7 @@ DASHBOARD_DATASET = [ TermOp(field="file_type", value=1), TermsOp( field="space_level", - value=["public", "department", "team", "team_ks"], + value=list(KNOWLEDGE_SPACE_DASHBOARD_FILE_LEVELS), ), ]), aggregations=[ @@ -781,7 +785,7 @@ DASHBOARD_DATASET = [ TermOp(field="record_type", value="file"), TermsOp( field="space_level", - value=["public", "department", "team", "team_ks"], + value=list(KNOWLEDGE_SPACE_DASHBOARD_FILE_LEVELS), ), ]), aggregations=[ diff --git a/src/backend/test/knowledge/test_portal_home_file_count.py b/src/backend/test/knowledge/test_portal_home_file_count.py new file mode 100644 index 000000000..e1349b31f --- /dev/null +++ b/src/backend/test/knowledge/test_portal_home_file_count.py @@ -0,0 +1,138 @@ +from types import SimpleNamespace + +import pytest + +from bisheng.common.telemetry import portal_event_service +from bisheng.knowledge.api.endpoints import shougang_portal + + +class _FakeSearchClient: + def __init__(self, *, total: int = 0) -> None: + self.total = total + self.search_calls: list[dict] = [] + + async def search(self, **kwargs): + self.search_calls.append(kwargs) + return { + "aggregations": { + "total_file_count": { + "value": self.total, + } + } + } + + +@pytest.mark.asyncio +async def test_portal_file_count_uses_dashboard_es_metric(monkeypatch: pytest.MonkeyPatch): + fake_client = _FakeSearchClient(total=41) + + async def fake_get_es_connection(): + return fake_client + + monkeypatch.setattr( + portal_event_service, + "get_statistics_es_connection", + fake_get_es_connection, + ) + + result = await portal_event_service.PortalTelemetryEventService.count_dashboard_files() + + assert result == 41 + assert fake_client.search_calls == [ + { + "index": "mid_knowledge_space_content_stat", + "body": { + "size": 0, + "query": { + "bool": { + "filter": [ + {"term": {"record_type": "file"}}, + {"term": {"file_type": 1}}, + { + "terms": { + "space_level": [ + "public", + "department", + "team", + "team_ks", + ] + } + }, + ] + } + }, + "aggs": { + "total_file_count": { + "value_count": { + "field": "file_id", + } + } + }, + }, + "filter_path": "aggregations.total_file_count.value", + } + ] + + +@pytest.mark.asyncio +async def test_portal_home_stats_uses_es_file_count(monkeypatch: pytest.MonkeyPatch): + async def fake_count_home_events(): + return { + "read_count": 12, + "favorite_count": 3, + "qa_count": 7, + } + + async def fake_count_dashboard_files(): + return 41 + + monkeypatch.setattr( + shougang_portal.PortalTelemetryEventService, + "count_home_events", + fake_count_home_events, + ) + monkeypatch.setattr( + shougang_portal.PortalTelemetryEventService, + "count_dashboard_files", + fake_count_dashboard_files, + ) + + response = await shougang_portal.get_shougang_portal_home_stats( + login_user=SimpleNamespace(user_id=1), + ) + + assert response.data == { + "read_count": 12, + "favorite_count": 3, + "qa_count": 7, + "total_files": 41, + } + + +@pytest.mark.asyncio +async def test_portal_home_stats_propagates_es_file_count_failure(monkeypatch: pytest.MonkeyPatch): + async def fake_count_home_events(): + return { + "read_count": 12, + "favorite_count": 3, + "qa_count": 7, + } + + async def fail_count_dashboard_files(): + raise RuntimeError("statistics es unavailable") + + monkeypatch.setattr( + shougang_portal.PortalTelemetryEventService, + "count_home_events", + fake_count_home_events, + ) + monkeypatch.setattr( + shougang_portal.PortalTelemetryEventService, + "count_dashboard_files", + fail_count_dashboard_files, + ) + + with pytest.raises(RuntimeError, match="statistics es unavailable"): + await shougang_portal.get_shougang_portal_home_stats( + login_user=SimpleNamespace(user_id=1), + )