feat(data-ana): v2 P6 硬化完成 + 6 新 RPC + Prometheus 监控

P6 硬化(5 项全部完成):

- CDC 多实例水平扩展: _INSTANCE_ID + get_lag() 真实 lag 计算

- ExamCache Redis 化: key data_ana:exam:{exam_id}, TTL 30 天 + 内存 LRU fallback

- ClickHouse TTL 归档: 5 表均加 TTL(1-3 年),分区级删除

- Prometheus 监控: 18 个指标(CDC/CH/ExamCache/DataScope/gRPC/业务)

- readyz 深度硬化: 4 依赖超时检查(CH 1s/Redis 200ms/iam 2s/CDC lag<1000)

v2 新增 6 个 RPC(analytics.proto 扩展为 18 RPC):

- GetStudentGrowth / GetAssignmentAnalysis / GetMasterySummary

- ListDiagnosticReports(占位,待 ai 服务)/ ListErrorBookItems / GetErrorBookStats

监控与可观测性: lifespan 预热 + gRPC ServerInterceptor + CDC 消费者指标

Docker 本地测试 19 项全部通过(healthz/readyz/metrics + 11 HTTP + 10 gRPC + ruff)

nextstep-v2.md: 上游需求对齐 + 下游要求(iam/core-edu/content/ai/SRE)
This commit is contained in:
SpecialX
2026-07-14 18:07:17 +08:00
parent 78e406b317
commit 9db7fd917e
13 changed files with 2118 additions and 147 deletions

View File

@@ -111,6 +111,42 @@ class AnalyticsServiceStub:
response_deserializer=analytics__pb2.MasteryUpdateEvent.FromString,
_registered_method=True,
)
self.GetStudentGrowth = channel.unary_unary(
"/next_edu_cloud.analytics.v1.AnalyticsService/GetStudentGrowth",
request_serializer=analytics__pb2.GetStudentGrowthRequest.SerializeToString,
response_deserializer=analytics__pb2.StudentGrowth.FromString,
_registered_method=True,
)
self.GetAssignmentAnalysis = channel.unary_unary(
"/next_edu_cloud.analytics.v1.AnalyticsService/GetAssignmentAnalysis",
request_serializer=analytics__pb2.GetAssignmentAnalysisRequest.SerializeToString,
response_deserializer=analytics__pb2.AssignmentAnalysis.FromString,
_registered_method=True,
)
self.GetMasterySummary = channel.unary_unary(
"/next_edu_cloud.analytics.v1.AnalyticsService/GetMasterySummary",
request_serializer=analytics__pb2.GetMasterySummaryRequest.SerializeToString,
response_deserializer=analytics__pb2.MasterySummary.FromString,
_registered_method=True,
)
self.ListDiagnosticReports = channel.unary_unary(
"/next_edu_cloud.analytics.v1.AnalyticsService/ListDiagnosticReports",
request_serializer=analytics__pb2.ListDiagnosticReportsRequest.SerializeToString,
response_deserializer=analytics__pb2.DiagnosticReportList.FromString,
_registered_method=True,
)
self.ListErrorBookItems = channel.unary_unary(
"/next_edu_cloud.analytics.v1.AnalyticsService/ListErrorBookItems",
request_serializer=analytics__pb2.ListErrorBookItemsRequest.SerializeToString,
response_deserializer=analytics__pb2.ErrorBookList.FromString,
_registered_method=True,
)
self.GetErrorBookStats = channel.unary_unary(
"/next_edu_cloud.analytics.v1.AnalyticsService/GetErrorBookStats",
request_serializer=analytics__pb2.GetErrorBookStatsRequest.SerializeToString,
response_deserializer=analytics__pb2.ErrorBookStats.FromString,
_registered_method=True,
)
class AnalyticsServiceServicer:
@@ -191,6 +227,44 @@ class AnalyticsServiceServicer:
context.set_details("Method not implemented!")
raise NotImplementedError("Method not implemented!")
def GetStudentGrowth(self, request, context):
"""===== P6+ v2 扩展 RPC响应上游 parent-bff / student-bff v2 请求) =====
学生成长档案(综合成绩趋势 + 掌握度变化 + 考勤统计,供 parent-bff GrowthArchiveService.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details("Method not implemented!")
raise NotImplementedError("Method not implemented!")
def GetAssignmentAnalysis(self, request, context):
"""作业/考试分析(单次作业/考试维度统计,供 student-bff / parent-bff."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details("Method not implemented!")
raise NotImplementedError("Method not implemented!")
def GetMasterySummary(self, request, context):
"""学生掌握度汇总(轻量级,仅返回总体掌握度 + 三档分布,供 parent-bff MasteryService.GetSummary."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details("Method not implemented!")
raise NotImplementedError("Method not implemented!")
def ListDiagnosticReports(self, request, context):
"""诊断报告列表(占位实现,真实数据需 ai 服务集成,供 parent-bff / student-bff."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details("Method not implemented!")
raise NotImplementedError("Method not implemented!")
def ListErrorBookItems(self, request, context):
"""错题本列表gRPC 版本,供 parent-bff ErrorBookService.List."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details("Method not implemented!")
raise NotImplementedError("Method not implemented!")
def GetErrorBookStats(self, request, context):
"""错题本统计(按知识点聚合,供 parent-bff ErrorBookService.GetStats."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details("Method not implemented!")
raise NotImplementedError("Method not implemented!")
def add_AnalyticsServiceServicer_to_server(servicer, server):
rpc_method_handlers = {
@@ -254,6 +328,36 @@ def add_AnalyticsServiceServicer_to_server(servicer, server):
request_deserializer=analytics__pb2.SubscribeMasteryUpdateRequest.FromString,
response_serializer=analytics__pb2.MasteryUpdateEvent.SerializeToString,
),
"GetStudentGrowth": grpc.unary_unary_rpc_method_handler(
servicer.GetStudentGrowth,
request_deserializer=analytics__pb2.GetStudentGrowthRequest.FromString,
response_serializer=analytics__pb2.StudentGrowth.SerializeToString,
),
"GetAssignmentAnalysis": grpc.unary_unary_rpc_method_handler(
servicer.GetAssignmentAnalysis,
request_deserializer=analytics__pb2.GetAssignmentAnalysisRequest.FromString,
response_serializer=analytics__pb2.AssignmentAnalysis.SerializeToString,
),
"GetMasterySummary": grpc.unary_unary_rpc_method_handler(
servicer.GetMasterySummary,
request_deserializer=analytics__pb2.GetMasterySummaryRequest.FromString,
response_serializer=analytics__pb2.MasterySummary.SerializeToString,
),
"ListDiagnosticReports": grpc.unary_unary_rpc_method_handler(
servicer.ListDiagnosticReports,
request_deserializer=analytics__pb2.ListDiagnosticReportsRequest.FromString,
response_serializer=analytics__pb2.DiagnosticReportList.SerializeToString,
),
"ListErrorBookItems": grpc.unary_unary_rpc_method_handler(
servicer.ListErrorBookItems,
request_deserializer=analytics__pb2.ListErrorBookItemsRequest.FromString,
response_serializer=analytics__pb2.ErrorBookList.SerializeToString,
),
"GetErrorBookStats": grpc.unary_unary_rpc_method_handler(
servicer.GetErrorBookStats,
request_deserializer=analytics__pb2.GetErrorBookStatsRequest.FromString,
response_serializer=analytics__pb2.ErrorBookStats.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
"next_edu_cloud.analytics.v1.AnalyticsService", rpc_method_handlers
@@ -630,3 +734,183 @@ class AnalyticsService:
metadata,
_registered_method=True,
)
@staticmethod
def GetStudentGrowth(
request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None,
):
return grpc.experimental.unary_unary(
request,
target,
"/next_edu_cloud.analytics.v1.AnalyticsService/GetStudentGrowth",
analytics__pb2.GetStudentGrowthRequest.SerializeToString,
analytics__pb2.StudentGrowth.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True,
)
@staticmethod
def GetAssignmentAnalysis(
request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None,
):
return grpc.experimental.unary_unary(
request,
target,
"/next_edu_cloud.analytics.v1.AnalyticsService/GetAssignmentAnalysis",
analytics__pb2.GetAssignmentAnalysisRequest.SerializeToString,
analytics__pb2.AssignmentAnalysis.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True,
)
@staticmethod
def GetMasterySummary(
request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None,
):
return grpc.experimental.unary_unary(
request,
target,
"/next_edu_cloud.analytics.v1.AnalyticsService/GetMasterySummary",
analytics__pb2.GetMasterySummaryRequest.SerializeToString,
analytics__pb2.MasterySummary.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True,
)
@staticmethod
def ListDiagnosticReports(
request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None,
):
return grpc.experimental.unary_unary(
request,
target,
"/next_edu_cloud.analytics.v1.AnalyticsService/ListDiagnosticReports",
analytics__pb2.ListDiagnosticReportsRequest.SerializeToString,
analytics__pb2.DiagnosticReportList.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True,
)
@staticmethod
def ListErrorBookItems(
request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None,
):
return grpc.experimental.unary_unary(
request,
target,
"/next_edu_cloud.analytics.v1.AnalyticsService/ListErrorBookItems",
analytics__pb2.ListErrorBookItemsRequest.SerializeToString,
analytics__pb2.ErrorBookList.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True,
)
@staticmethod
def GetErrorBookStats(
request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None,
):
return grpc.experimental.unary_unary(
request,
target,
"/next_edu_cloud.analytics.v1.AnalyticsService/GetErrorBookStats",
analytics__pb2.GetErrorBookStatsRequest.SerializeToString,
analytics__pb2.ErrorBookStats.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True,
)