65 KiB
模块架构设计文档 — data-ana
AI 标识:ai11(v2 由 ai11 审核 ai06 v1 后修订) 负责模块:data-ana(P4) 阶段:架构设计外包 · 阶段 2(模块架构设计)v2 审核版 日期:2026-07-09(v1 by ai06)/ 2026-07-10(v2 审核修订 by ai11) 关联文档:阶段 1 理解确认书、ai-allocation.md、004 架构影响地图、pending-features.md 审查状态:v1 已通过 coord 交叉审查(见 coord-cross-review.md),裁决结论已沉淀到 004 §15.3;v2 在 v1 基础上对齐已仲裁约束 + 补强长远性/全面性 v2 修订要点:(1) 身份归属 ai06 → ai11;(2) 响应信封对齐 ActionState(004 §11.5 P0 整改);(3) 删除已仲裁的"未决设计决策";(4) 补 gRPC 端口 50055;(5) 补 5 张宽表(ai-allocation §5 要求的 attendance_logs);(6) 补 4 端 Dashboard + 预警 API;(7) 补掌握度计算算法具体公式;(8) 补缓存策略/CDC 水平扩展/容量规划/数据治理/测试策略/Dockerfile 多阶段/配置项清单/API 版本化/Projection 演进路径/未来阶段铺垫(§9-§19 新增章节)
设计原则与全局约束
本设计遵循以下强制约束(来自 project_rules.md + coding-standards.md + 004):
- 契约先行:proto 已定义(analytics.proto / events.proto / iam.proto),实现前不修改 proto,如需修改走 coord 流程(ai-allocation.md §9.4)
- CQRS 读写分离:data-ana 是纯读模型服务(无 MySQL 写),ClickHouse 宽表由 CDC 投影构建
- 事件驱动:data-ana 消费 CDC(主通道)+ 领域事件(备通道,待 P4 后期评估);发布
edu.insight.mastery.updated派生数据事件(豁免 Outbox,coord-cross-review.md §3.3 + 004 §12.2 已仲裁) - gRPC 优先:004 §4.1 + coord-cross-review.md §2.1 明确 BFF → 业务服务走 gRPC,P4 启用 data-ana gRPC server 端口 50055(HTTP 保留作 Gateway 直连降级)
- DataScope 过滤:004 §5.3 DataScope 6 级在查询层注入 WHERE;iam
GetEffectiveDataScopegRPC(coord-cross-review.md §2 #3 已仲裁 P4 补全) - 三支柱可观测:structlog + prometheus-client + OpenTelemetry(已具备,需补业务指标 + gRPC server interceptor)
- 统一响应信封 ActionState(coord-cross-review.md §5.3 已裁决 P0 整改):成功
{success: true, data: T}/ 失败{success: false, error: {code, message, details?, traceId?}}/ 降级degraded作为顶层details.degraded子字段(非error.details) - 降级模式:外部依赖(ClickHouse / Kafka / iam gRPC / Redis)不可用时返回骨架数据 +
details.degraded: true - Python 规范:pydantic-settings 配置 / Pydantic 模型校验 / async 优先 / 类型注解强制 / ruff 零错误
- 长远架构演进:为 P5(ai 用量消费 / gRPC stream)、P6(CDC 水平扩展 / 容量规划 / 数据治理 / Service Mesh)做好铺垫,见 §14 / §19
1. 模块内部分层图
flowchart TB
subgraph Entry["入口层"]
HTTP[FastAPI HTTP Router<br/>:3006 /analytics/* + /healthz + /readyz]
GRPC[grpc.aio Server<br/>:50055 AnalyticsService]
end
subgraph Middleware["中间件层"]
AUTH[AuthDepends<br/>校验 x-user-id / x-user-roles]
SCOPE[DataScopeDepends<br/>注入 data_scope 元数据]
TRACE[OTel FastAPIInstrumentor<br/>+ grpc.aio server interceptor]
end
subgraph Service["应用服务层 Application Service"]
S1[AnalyticsService<br/>班级/学生/趋势/Dashboard 查询编排]
S2[MasteryService<br/>掌握度计算 + 事件发布]
S3[ErrorBookService<br/>错题本查询]
S4[WarningService<br/>预警阈值评估 + 触发]
end
subgraph Repo["数据访问层 Repository"]
R1[ClickHouseRepository<br/>宽表查询 + DataScope WHERE 注入 + FINAL/argMax]
R2[KafkaProducer<br/>mastery.updated 派生数据事件发布]
R3[IamClient<br/>gRPC 调 iam.GetEffectiveDataScope]
R4[RedisClient<br/>DataScope 缓存 + CDC 幂等 + 预警位图]
end
subgraph Consumer["CDC 消费者(后台任务)"]
C1[CdcConsumer<br/>aiokafka AIOKafkaConsumer]
C2[ExamCache<br/>exam_id→class_id 映射 P4 内存 / P6 Redis 化]
C3[EventHandler<br/>grades/exams/homework/classes/attendance/kp 路由]
end
subgraph Storage["存储 / 总线"]
CH[(ClickHouse<br/>edu_analytics 库 5 宽表)]
KAFKA[(Kafka<br/>edu-cdc.* + edu.insight.mastery.updated + edu.insight.ai.usage)]
IAM[iam:3002 gRPC :50052]
REDIS[(Redis<br/>data_ana:* 键前缀)]
end
HTTP --> AUTH --> SCOPE --> S1
HTTP --> S3
HTTP --> S4
GRPC --> S1
S1 --> R1
S3 --> R1
S4 --> R1
S4 --> R2
S2 --> R1
S2 --> R2
SCOPE --> R3
R3 --> R4
R1 --> CH
R2 --> KAFKA
R3 --> IAM
R4 --> REDIS
C1 --> C3
C3 --> C2
C3 --> R1
C3 --> R4
C1 --> KAFKA
分层规则:
- 入口层:HTTP
:3006(保留作 Gateway 直连降级)+ gRPC:50055(P4 启用主入口,BFF / ai 调用)。两入口共享同一 Application Service - 中间件层:FastAPI Depends 链(
AuthDepends→DataScopeDepends);gRPC 用 server interceptor 注入身份元数据 - 应用服务层:编排查询 / 计算掌握度 / 评估预警 / 发布事件,不直接访问存储
- 数据访问层:ClickHouse 查询封装(含
FINAL/argMax去重)+ Kafka producer + gRPC client(调 iam)+ Redis client - CDC 消费者:独立后台任务(lifespan 启动),与 HTTP/gRPC 入口解耦;P6 演进为多实例水平扩展(ExamCache Redis 化)
2. 领域模型
data-ana 是纯读模型服务,不持有写聚合根。领域模型为视图聚合(ClickHouse 物化):
聚合根(视图型)
| 聚合根 | 含义 | 物化载体 | 不变式 |
|---|---|---|---|
StudentDashboard |
学生学情宽表 | ClickHouse student_dashboard_view |
同一 (student_id, exam_id, knowledge_point_id) 仅保留最新版本 |
ClassPerformance |
班级成绩聚合 | ClickHouse 即时聚合(不物化) | 聚合维度为 class_id + 时间窗 |
StudentErrorBook |
学生错题本 | ClickHouse student_errors |
同一 (student_id, question_id) 累计 error_count |
MasterySnapshot |
知识点掌握度快照 | ClickHouse mastery_snapshot(新增) |
同一 (student_id, knowledge_point_id) 保留历史版本 |
AttendanceLog |
学生考勤记录 | ClickHouse attendance_logs(v2 新增) |
同一 (student_id, class_id, attendance_date) 仅保留最新版本 |
AiUsageLog |
AI 用量计费记录 | ClickHouse ai_usage_log(v2 新增) |
同一 request_id 唯一(消费方按 request_id 幂等) |
ai-allocation §5 对齐:5 张宽表完整覆盖(student_dashboard_view / student_errors / mastery_snapshot / attendance_logs / ai_usage_log)。exam_results / homework_submissions / grade_records 数据已折叠进 student_dashboard_view 宽表(按 event_type 区分),避免宽表数量爆炸。
值对象
WeakPoint:knowledge_point_id + title + mastery_levelTrendPoint:date + scoreWarning:warning_type (LOW_MASTERY / SCORE_DROP / ABSENT_FREQUENT) + target_id + threshold + current_value + severity (INFO / WARN / CRITICAL)DataScope:level (SELF/CLASS/GRADE/SCHOOL/DISTRICT/ALL) + scope_ids(具体可见的 class_id / grade_id 列表)MasteryMethod:枚举(WEIGHTED_MOVING_AVG / SIMPLE_AVG / FORGETTING_CURVE),见 §9
聚合间通信
- 同服务内:直接函数调用(Application Service → Repository)
- 跨服务:仅通过 Kafka 事件(发布
mastery.updated)+ gRPC(调 iam 查 DataScope;待 P5+ ai 反向调 data-ana 查学情)
3. 数据模型(ClickHouse DDL)
DDL 文件由 coord 统一管理在
infra/clickhouse/ddl/(待 coord 建立),data-ana 提供内容。
3.1 宽表 student_dashboard_view
-- 学生学情宽表:每次成绩写入产生一行,按 ORDER BY 去重保留最新版本
CREATE TABLE IF NOT EXISTS student_dashboard_view
(
student_id String,
class_id String,
exam_id String,
subject_id String,
score Float64,
rank_in_class UInt32,
knowledge_point_id String,
mastery_level Float32, -- 0.0-1.0
error_count UInt32,
last_updated DateTime64(3, 'UTC')
)
ENGINE = ReplacingMergeTree(last_updated)
PARTITION BY toYYYYMM(last_updated)
ORDER BY (student_id, exam_id, knowledge_point_id)
SETTINGS index_granularity = 8192;
索引策略:
- ORDER BY
(student_id, exam_id, knowledge_point_id):主键索引,支持按学生查学情、按考试查成绩、按知识点查掌握度 - PARTITION BY
toYYYYMM(last_updated):按月分区,支持历史数据归档 - ReplacingMergeTree(last_updated):同 ORDER BY 自动去重,保留 last_updated 最大版本(幂等消费保证)
3.2 错题本 student_errors
CREATE TABLE IF NOT EXISTS student_errors
(
student_id String,
question_id String,
knowledge_point_id String,
error_count UInt32,
last_error_time DateTime64(3, 'UTC'),
content String
)
ENGINE = ReplacingMergeTree(last_error_time)
PARTITION BY toYYYYMM(last_error_time)
ORDER BY (student_id, question_id);
3.3 掌握度快照 mastery_snapshot(新增)
-- 知识点掌握度历史快照:每次掌握度计算产生新版本,支持趋势查询
CREATE TABLE IF NOT EXISTS mastery_snapshot
(
student_id String,
knowledge_point_id String,
subject_id String,
mastery_level Float32,
calculated_at DateTime64(3, 'UTC'),
calculation_method LowCardinality(String) -- 'weighted_moving_avg' / 'simple_avg'
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(calculated_at)
ORDER BY (student_id, knowledge_point_id, calculated_at);
3.4 用量计费 ai_usage_log(新增,供 ai 服务写入)
-- AI 用量记录:ai 服务通过 Kafka 事件投递,data-ana 消费落库
CREATE TABLE IF NOT EXISTS ai_usage_log
(
request_id String,
user_id String,
provider LowCardinality(String), -- 'openai' / 'anthropic' / 'baichuan' / 'local'
model LowCardinality(String),
prompt_tokens UInt32,
completion_tokens UInt32,
total_tokens UInt32,
latency_ms UInt32,
success Boolean,
cost_cents UInt32, -- 计费(分),便于聚合
occurred_at DateTime64(3, 'UTC')
)
ENGINE = ReplacingMergeTree(occurred_at)
PARTITION BY toYYYYMM(occurred_at)
ORDER BY (request_id); -- 按 request_id 幂等去重
3.5 考勤记录 attendance_logs(v2 新增,ai-allocation §5 第 5 张宽表)
-- 学生考勤记录:core-edu attendance 表 CDC 同步
CREATE TABLE IF NOT EXISTS attendance_logs
(
student_id String,
class_id String,
attendance_date Date,
status LowCardinality(String), -- 'present' / 'absent' / 'late' / 'leave'
recorded_by String, -- 教师用户 ID
remark String DEFAULT '',
occurred_at DateTime64(3, 'UTC')
)
ENGINE = ReplacingMergeTree(occurred_at)
PARTITION BY toYYYYMM(attendance_date)
ORDER BY (student_id, class_id, attendance_date);
索引策略:
- ORDER BY
(student_id, class_id, attendance_date):支持按学生查考勤历史、按班级查当日考勤、按日期范围统计 - PARTITION BY
toYYYYMM(attendance_date):按月分区,支持历史归档 - ReplacingMergeTree(occurred_at):同一记录多版本去重(教师修改考勤状态时产生新版本)
3.6 读写分离策略
| 操作 | 路径 | 说明 |
|---|---|---|
| 学情查询 | ClickHouse 宽表 | 实时聚合,亚秒级响应 |
| 错题本查询 | ClickHouse student_errors | 实时查询 |
| 掌握度趋势 | ClickHouse mastery_snapshot | 历史快照 |
| 考勤查询 | ClickHouse attendance_logs | 学生考勤历史 |
| AI 用量统计 | ClickHouse ai_usage_log | 按用户/模型/时间窗聚合 |
| 掌握度计算 | CDC 触发 → 内存计算 → 写 ClickHouse | 派生数据,非事务写 |
| DataScope 解析 | gRPC 调 iam | 实时查询,结果 Redis 缓存 5min(004 §6.3) |
双轨读边界(004 §1.1a):BFF 实时查主库走 core-edu,data-ana 仅承担聚合查宽表职责,二者不重叠。
4. API 设计
4.1 HTTP 端点(保留作 Gateway 直连降级)
| method | path | 权限 | 请求 | 响应 | 说明 |
|---|---|---|---|---|---|
| GET | /healthz |
— | — | {status, service} |
liveness |
| GET | /readyz |
— | — | {status, ready, degraded, clickhouse, cdc_consumer, redis, iam_grpc} |
readiness(v2 补 redis / iam_grpc) |
| GET | /metrics |
— | — | Prometheus 格式 | 指标 |
| GET | /analytics/class/{class_id}/performance |
ANALYTICS_CLASS_READ |
query: subject_id?, start_date?, end_date? |
ActionState<ClassPerformanceData> |
班级成绩分析 |
| GET | /analytics/student/{student_id}/weakness |
ANALYTICS_STUDENT_READ |
query: subject_id? |
ActionState<StudentWeaknessData> |
学生薄弱知识点(DataScope 过滤) |
| GET | /analytics/student/{student_id}/errorbook |
ANALYTICS_STUDENT_READ |
query: page?, size? |
ActionState<StudentErrorBookData> |
学生错题本(DataScope 过滤) |
| GET | /analytics/student/{student_id}/trend |
ANALYTICS_STUDENT_READ |
query: start_date, end_date, subject_id? |
ActionState<LearningTrendData> |
学习趋势 |
| GET | /analytics/student/{student_id}/attendance |
ANALYTICS_STUDENT_READ |
query: start_date, end_date |
ActionState<AttendanceData> |
学生考勤历史(v2 新增) |
| GET | /analytics/dashboard/teacher/{user_id} |
ANALYTICS_TEACHER_DASHBOARD |
query: class_id? |
ActionState<TeacherDashboardData> |
教师仪表盘聚合 |
| GET | /analytics/dashboard/student/{user_id} |
ANALYTICS_STUDENT_DASHBOARD |
— | ActionState<StudentDashboardData> |
学生仪表盘(v2 新增) |
| GET | /analytics/dashboard/parent/{user_id} |
ANALYTICS_PARENT_DASHBOARD |
query: student_id |
ActionState<ParentDashboardData> |
家长仪表盘(v2 新增) |
| GET | /analytics/dashboard/admin/{user_id} |
ANALYTICS_ADMIN_DASHBOARD |
query: scope?, scope_id? |
ActionState<AdminDashboardData> |
管理员仪表盘(v2 新增) |
| GET | /analytics/warnings |
ANALYTICS_WARNING_READ |
query: class_id?, severity?, since? |
ActionState<WarningListData> |
预警列表(v2 新增) |
| GET | /analytics/mastery/distribution |
ANALYTICS_CLASS_READ |
query: class_id, subject_id, knowledge_point_id? |
ActionState<MasteryDistributionData> |
班级掌握度分布(v2 新增) |
4.2 gRPC 契约(analytics.proto,v2 扩展)
| RPC | 请求 | 响应 | 权限 |
|---|---|---|---|
GetClassPerformance |
GetClassPerformanceRequest{class_id, subject_id, start_date, end_date} |
ClassPerformance |
ANALYTICS_CLASS_READ |
GetStudentWeakness |
GetStudentWeaknessRequest{student_id, subject_id} |
StudentWeakness |
ANALYTICS_STUDENT_READ |
GetLearningTrend |
GetLearningTrendRequest{student_id, start_date, end_date, subject_id?} |
LearningTrend |
ANALYTICS_STUDENT_READ |
GetTeacherDashboard |
GetTeacherDashboardRequest{user_id, class_id?} |
TeacherDashboard |
ANALYTICS_TEACHER_DASHBOARD |
GetStudentDashboard |
GetStudentDashboardRequest{user_id} |
StudentDashboard |
ANALYTICS_STUDENT_DASHBOARD |
GetParentDashboard |
GetParentDashboardRequest{user_id, student_id} |
ParentDashboard |
ANALYTICS_PARENT_DASHBOARD |
GetAdminDashboard |
GetAdminDashboardRequest{user_id, scope, scope_id?} |
AdminDashboard |
ANALYTICS_ADMIN_DASHBOARD |
GetWarnings |
GetWarningsRequest{class_id?, severity?, since?} |
WarningList |
ANALYTICS_WARNING_READ |
GetMasteryDistribution |
GetMasteryDistributionRequest{class_id, subject_id, knowledge_point_id?} |
MasteryDistribution |
ANALYTICS_CLASS_READ |
GetStudentMastery |
GetStudentMasteryRequest{student_id, subject_id?} |
StudentMastery |
ANALYTICS_STUDENT_READ |
TriggerWarning |
TriggerWarningRequest{target_id, warning_type, severity} |
TriggerWarningResponse |
ANALYTICS_WARNING_READ |
SubscribeMasteryUpdate |
SubscribeMasteryUpdateRequest{student_id?, class_id?} |
stream MasteryUpdateEvent |
ANALYTICS_STUDENT_READ |
proto 扩展提案(待 coord 审议):上述新增 RPC 的 message 定义需在
packages/shared-proto/proto/analytics.proto补充,共 12 RPC(3 现有 + 9 扩展)。SubscribeMasteryUpdate为 server-streaming RPC,为 P5+ AI 个性化推荐预留实时推送通道。
权限校验:gRPC server interceptor 从 metadata 提取 x-user-id / x-user-roles / x-data-scope,调用 AuthDepends 等价逻辑。
4.3 ActionState 统一响应信封(coord-cross-review.md §5.3 P0 整改)
所有 HTTP/gRPC 响应必须遵循 ActionState 信封:
from pydantic import BaseModel
from typing import Generic, TypeVar
T = TypeVar("T")
class ActionStateError(BaseModel):
code: str
message: str
details: dict[str, Any] | None = None
trace_id: str | None = None
class ActionState(BaseModel, Generic[T]):
"""统一响应信封(coord-cross-review.md §5.3 裁决).
成功:{success: true, data: T}
失败:{success: false, error: {code, message, details?, trace_id?}}
降级:{success: true, data: T, details: {degraded: true}}(保留功能但数据可能不完整)
"""
success: bool
data: T | None = None
error: ActionStateError | None = None
details: dict[str, Any] | None = None # 降级标记放此字段,不放 error
@classmethod
def ok(cls, data: T, *, degraded: bool = False) -> "ActionState[T]":
# 降级不是错误:success=True,degraded 标记放 details 子字段(非 error.details)
details = {"degraded": True} if degraded else None
return cls(success=True, data=data, error=None, details=details)
@classmethod
def fail(cls, code: str, message: str, *, trace_id: str | None = None,
details: dict[str, Any] | None = None) -> "ActionState[T]":
return cls(success=False, data=None,
error=ActionStateError(code=code, message=message,
details=details, trace_id=trace_id))
# 示例:班级成绩分析响应
class ClassPerformanceData(BaseModel):
class_id: str
average_score: float
pass_rate: float
total_students: int
scores: list[StudentScore] = [] # 详细成绩列表(受 DataScope 过滤)
class StudentScore(BaseModel):
student_id: str
score: float
grade: str
# 端点返回类型:ActionState[ClassPerformanceData]
P0 整改要点:当前 main.py 返回
{success, data, degraded}三字段平铺,违反 coord-cross-review.md §5.3 裁决。实现阶段需重构为ActionState[T]泛型,degraded移到顶层details.degraded(非error.details,降级不是错误)。
5. 事件设计
5.1 消费的事件
| Topic | 来源 | 消息格式 | 消费动作 |
|---|---|---|---|
edu-cdc.next_edu_cloud.core_edu_grades |
Debezium CDC(core-edu MySQL) | Debezium JSON(before/after/source/op/ts_ms) | 解析 → 查 ExamCache 填 class_id → 计算掌握度 → upsert student_dashboard_view |
edu-cdc.next_edu_cloud.core_edu_exams |
Debezium CDC | Debezium JSON | upsert ExamCache(exam_id → class_id, subject_id) |
edu-cdc.next_edu_cloud.core_edu_homework_submissions |
Debezium CDC | Debezium JSON | 记录作业提交行为 → 更新 student_dashboard_view |
edu-cdc.next_edu_cloud.core_edu_attendance |
Debezium CDC(v2 新增) | Debezium JSON | 落 attendance_logs 表 |
edu-cdc.next_edu_cloud.classes |
Debezium CDC | Debezium JSON | 同步班级维度(head_teacher_id)用于教师 DataScope |
edu-cdc.next_edu_cloud.iam_users |
Debezium CDC | Debezium JSON | 同步用户 dataScope 用于查询过滤(避免每次查 iam) |
edu-cdc.next_edu_cloud.content_knowledge_points |
Debezium CDC(v2 新增,content 服务) | Debezium JSON | 同步知识点标题/前置关系到 mastery_snapshot 元数据 |
edu.insight.ai.usage |
ai 服务 Kafka producer | JSON(UsageRecord) | 落 ai_usage_log 表 |
004 §4.1 对齐:content → data-ana 事件流已声明("教学内容变更通知"),通过 CDC 落知识点元数据。当 content 服务发布
KnowledgePointUpdated领域事件时(待 P4 后期评估),可切换为订阅edu.content.eventstopic,避免依赖 content MySQL binlog。
幂等性:
- CDC 事件:依赖 ClickHouse
ReplacingMergeTree(last_updated)引擎按 ORDER BY 去重 + ReplacingMergeTree 的 version 列 - 领域事件(若消费):基于
event_id去重(Redis SETNXdata_ana:dedup:{event_id}TTL 7 天) - 消费者 offset:v1 用
enable_auto_commit=True简化;v2 改进:手动 commit(at-least-once),commit 前确保 ClickHouse 写入成功
5.2 发布的事件
| 事件 | Topic | 触发时机 | 消费者 | Payload |
|---|---|---|---|---|
MasteryUpdated |
edu.insight.mastery.updated |
掌握度计算完成(CDC grades 事件触发后异步计算) | core-edu(推荐个性化练习)、msg(掌握度预警) | {event_id, student_id, knowledge_point_id, mastery_level, calculated_at} |
WarningTriggered |
edu.insight.warning.triggered(v2 新增) |
预警阈值触发(掌握度低于 0.4 / 成绩环比下降 20% / 缺勤 ≥ 3 次/周) | msg(推送通知)、core-edu(标记关注) | {event_id, warning_type, target_id, threshold, current_value, severity, occurred_at} |
发布实现(004 §12.2 + §15.3 #6 已仲裁:派生数据事件豁免 Outbox):
- 掌握度计算、预警评估均为派生数据(非业务事务写),不需要 Outbox 保证事务一致
- 直接用
aiokafka.AIOKafkaProducer发布,idempotent=true+transactional_id="data-ana-producer" - 失败重试 3 次,仍失败记录日志 + 落
publish_failed本地表(SQLite 文件,P6 评估是否引入 Outbox) - 事件 schema 版本化:
schema_version字段(v1/v2/...),禁止破坏性变更,见 §17
5.3 CDC 水平扩展策略(v2 新增,P6 铺垫)
P4 单实例模式:
- 1 个 CdcConsumer 实例消费所有 topic,consumer group =
data-ana-cdc - ExamCache 内存 LRU(max 10000 条)
- 风险:单点故障 + 重启 lag 累积
P6 多实例水平扩展:
- consumer group 不变,多实例分摊 partition
- ExamCache → Redis 化(key:
data_ana:exam:{exam_id}TTL 30 天) - 幂等去重 → Redis SETNX(已具备,多实例天然共享)
- partition 数从 P4 的 1 → P6 的 N(按 topic 流量评估)
- 监控:consumer lag 指标(已具备
data_ana_cdc_consumer_lag)+ HPA 阈值
演进路径:
P4: 1 实例 + 内存 ExamCache + auto commit
↓
P5: 1 实例 + Redis ExamCache + 手动 commit(过渡)
↓
P6: N 实例 + Redis ExamCache + 手动 commit + HPA
单实例 → 多实例的代码改动量:仅 ExamCache 实现(dict → Redis client),其余抽象层不变。
6. 横切关注点对齐清单
6.1 权限装饰器等价物(FastAPI Depends)
# 权限点常量(与 iam 权限点对齐,v2 扩展 4 端 Dashboard + 预警权限)
class Permissions:
ANALYTICS_CLASS_READ = "analytics:class:read"
ANALYTICS_STUDENT_READ = "analytics:student:read"
ANALYTICS_TEACHER_DASHBOARD = "analytics:teacher:dashboard"
ANALYTICS_STUDENT_DASHBOARD = "analytics:student:dashboard" # v2 新增
ANALYTICS_PARENT_DASHBOARD = "analytics:parent:dashboard" # v2 新增
ANALYTICS_ADMIN_DASHBOARD = "analytics:admin:dashboard" # v2 新增
ANALYTICS_WARNING_READ = "analytics:warning:read" # v2 新增
async def require_permission(permission: str) -> UserContext:
"""FastAPI Depends 权限校验.
从 x-user-id / x-user-roles 头提取身份,校验角色是否含 permission。
"""
...
async def inject_data_scope(ctx: UserContext = Depends(require_permission(...)))-> DataScope:
"""注入 DataScope(从 iam.getEffectiveDataScope 查询,Redis 缓存 5min)."""
...
6.2 错误码清单(前缀 DATA_ANA_*,v2 扩展)
| 错误码 | 触发条件 | HTTP | gRPC status |
|---|---|---|---|
DATA_ANA_UNAUTHORIZED |
缺失 x-user-id 头或 token 无效 | 401 | UNAUTHENTICATED |
DATA_ANA_FORBIDDEN |
角色无对应权限 | 403 | PERMISSION_DENIED |
DATA_ANA_DATASCOPE_VIOLATION |
查询目标超出 DataScope 范围 | 403 | PERMISSION_DENIED |
DATA_ANA_CLICKHOUSE_UNAVAILABLE |
ClickHouse 不可达(降级模式仍返回骨架) | 200 + degraded:true | OK + degraded flag |
DATA_ANA_KAFKA_UNAVAILABLE |
Kafka producer 不可达(事件发布失败) | 200 + degraded | OK + degraded |
DATA_ANA_REDIS_UNAVAILABLE |
Redis 不可达(降级直查 iam) | 200 + degraded | OK + degraded |
DATA_ANA_IAM_GRPC_UNAVAILABLE |
iam gRPC 不可达(fallback SELF 范围) | 200 + degraded | OK + degraded |
DATA_ANA_INVALID_DATE_RANGE |
start_date > end_date | 400 | INVALID_ARGUMENT |
DATA_ANA_STUDENT_NOT_FOUND |
student_id 不存在 | 404 | NOT_FOUND |
DATA_ANA_CLASS_NOT_FOUND |
class_id 不存在 | 404 | NOT_FOUND |
DATA_ANA_WARNING_NOT_FOUND |
预警 ID 不存在 | 404 | NOT_FOUND |
DATA_ANA_RATE_LIMITED |
触发限流(004 §4.3 限流策略) | 429 | RESOURCE_EXHAUSTED |
DATA_ANA_INTERNAL_ERROR |
未捕获异常 | 500 | INTERNAL |
6.3 Logger 初始化
- 位置:
main.pyinit_logger()(已具备) - 配置:
structlog.make_filtering_bound_logger(level)+TimeStamper(fmt="iso")+ConsoleRenderer - 改进:生产环境改用
structlog.processors.JSONRenderer()(当前 ConsoleRenderer 适合开发) - 日志字段:
request_id/user_id/trace_id/service="data-ana"(004 §10 强制)
6.4 Metrics 指标清单(v2 扩展业务指标)
| 指标名 | 类型 | 标签 | 描述 |
|---|---|---|---|
data_ana_http_requests_total |
Counter | method, path, status | HTTP 请求总数 |
data_ana_http_request_duration_seconds |
Histogram | method, path | HTTP 请求延迟 |
data_ana_grpc_requests_total |
Counter | rpc, status | gRPC 请求总数(v2 新增) |
data_ana_grpc_request_duration_seconds |
Histogram | rpc | gRPC 请求延迟(v2 新增) |
data_ana_clickhouse_query_duration_seconds |
Histogram | query_type | ClickHouse 查询延迟 |
data_ana_clickhouse_query_total |
Counter | query_type, status | ClickHouse 查询总数 |
data_ana_cdc_events_consumed_total |
Counter | table, op | CDC 事件消费总数 |
data_ana_cdc_event_process_duration_seconds |
Histogram | table | CDC 事件处理延迟 |
data_ana_cdc_consumer_lag |
Gauge | topic, partition | CDC 消费者 lag |
data_ana_mastery_calculated_total |
Counter | method | 掌握度计算次数(按算法) |
data_ana_mastery_published_total |
Counter | status | mastery.updated 事件发布数 |
data_ana_warning_triggered_total |
Counter | warning_type, severity | 预警触发数(v2 新增) |
data_ana_datascope_cache_hits_total |
Counter | — | DataScope 缓存命中 |
data_ana_redis_ops_total |
Counter | op, status | Redis 操作数(v2 新增) |
data_ana_iam_grpc_calls_total |
Counter | status | iam gRPC 调用数(v2 新增) |
data_ana_ai_usage_ingested_total |
Counter | provider, model | AI 用量事件消费数(v2 新增) |
6.5 Tracer 初始化
- 位置:
main.pyinit_tracer()(已具备) - endpoint:
settings.otel_endpoint+/v1/traces - 改进:gRPC server 注册
grpc.aio.ServerInterceptor透传 W3C trace context - 改进:CDC 消费者 span 注入 trace context(从 Kafka header 提取 traceparent)
6.6 /healthz 检查逻辑
- liveness:仅进程存活(已具备)
6.7 /readyz 检查逻辑(v2 扩展)
async def readyz() -> dict:
return {
"status": "ok" if all_ready else "degraded",
"ready": all_ready,
"degraded": not all_ready,
"clickhouse": "ok" | "unreachable" | "not_configured",
"cdc_consumer": "running" | "disabled" | "failed",
"kafka_brokers": settings.kafka_brokers or None,
"iam_grpc": "ok" | "unreachable", # v2 新增
"redis": "ok" | "unreachable", # v2 新增
"timestamp": datetime.now(UTC).isoformat(),
}
6.8 优雅关闭顺序
- HTTP server stop accepting new requests(uvicorn graceful shutdown)
- gRPC server graceful stop(等待在途 RPC 完成,30s 超时)
- CDC consumer stop(等待在途消息处理完成,commit offset)
- Kafka producer flush + close(确保 mastery.updated 事件已投递)
- ClickHouse client close
- iam gRPC channel close
信号处理:注册 signal.SIGTERM handler,触发上述顺序。
7. 与其他模块的交互点(契约清单,v2 扩展)
| 方向 | 对方服务 | 协议 | 接口/事件 | 用途 |
|---|---|---|---|---|
| 被调用 | api-gateway | HTTP | /analytics/* |
Gateway 代理(降级通道) |
| 被调用 | teacher-bff / student-bff / parent-bff | gRPC | AnalyticsService.* |
BFF 聚合查询(4 端 Dashboard) |
| 被调用 | ai(P5+) | gRPC | AnalyticsService.GetStudentWeakness / GetLearningTrend / SubscribeMasteryUpdate |
AI 个性化出题上下文 + 实时掌握度推送 |
| 调用 | iam | gRPC | IamService.GetEffectiveDataScope(coord-cross-review.md §2 #3 已仲裁 P4 补全;当前 iam.proto 未实现) |
DataScope 解析 |
| 消费 | core-edu(CDC) | Kafka | edu-cdc.next_edu_cloud.core_edu_grades/exams/homework_submissions/attendance |
学情 + 考勤数据投递 |
| 消费 | core-edu(CDC) | Kafka | edu-cdc.next_edu_cloud.classes |
班级维度同步 |
| 消费 | iam(CDC) | Kafka | edu-cdc.next_edu_cloud.iam_users |
用户 dataScope 同步 |
| 消费 | content(CDC) | Kafka | edu-cdc.next_edu_cloud.content_knowledge_points |
知识点元数据同步(v2 新增) |
| 消费 | ai(P5+) | Kafka | edu.insight.ai.usage(coord-cross-review.md §3.2 已仲裁;004 §7.2 已登记) |
AI 用量落库 |
| 发布 | core-edu / msg | Kafka | edu.insight.mastery.updated(coord-cross-review.md §3.3 + 004 §12.2 已仲裁:派生数据豁免 Outbox) |
掌握度更新通知 |
| 发布 | msg / core-edu | Kafka | edu.insight.warning.triggered(v2 新增,需 coord 在 004 §7.2 登记) |
预警触发通知 |
004 §4.1 服务间通信矩阵对齐:content → data-ana 已声明"教学内容变更通知",本次落实为 CDC 订阅
content_knowledge_points表。
8. 风险与假设
8.1 假设
- 假设 1:iam 在 P4 阶段提供
GetEffectiveDataScope(userId) → DataScopegRPC API(coord-cross-review.md §2 #3 已仲裁)。当前 iam.proto 仅 4 RPC 未实现此 RPC,P4 阻塞项。若 iam 未及时提供,fallback 为:从x-user-roles头推导(admin=ALL, teacher=CLASS_TAUGHT, student=SELF),但无法支持细粒度年级/学校范围 - 假设 2:core-edu 的
core_edu_homework_submissions/core_edu_attendance表存在 binlog。若不存在,需 core-edu 补表或走 Outbox 事件(领域事件备通道) - 假设 3:ClickHouse
ReplacingMergeTree在查询时需FINAL关键字确保去重生效。v2 修复要求:所有查询加FINAL或使用argMax聚合(当前实现未加,是 P0 整改项) - 假设 4:coord 已在 004 §7.2 登记新增
edu.insight.ai.usagetopic(coord-cross-review.md §3.2 已仲裁);events.proto AIUsageEvent message 待 coord 补充
8.2 技术风险
| 风险 | 影响 | 缓解措施 |
|---|---|---|
| ClickHouse 查询延迟超 5s | 违反 P4 退出标准 | 宽表索引优化 + 物化视图预聚合 + 查询超时 3s 降级 |
| CDC 消费者 lag 过大 | 学情数据延迟 > 5s | 监控 lag + 告警 + 水平扩展消费者(分区数提升) |
| ExamCache 内存泄漏 | 长期运行 OOM | LRU 淘汰策略(max 10000 条)+ 定期清理过期 exam;P6 迁 Redis |
| mastery.updated 事件丢失 | 下游 core-edu/msg 收不到通知 | Kafka producer acks=all + 本地失败表重试 |
| iam gRPC 不可达 | DataScope 无法解析 → 查询降级为 SELF | Redis 缓存 5min + fallback SELF 范围(最保守) |
ClickHouse FINAL 查询性能 |
查询变慢 | 使用 argMax 替代 FINAL,或在写入时去重 |
| ClickHouse 单点故障 | 全部查询不可用 | P6 引入 ClickHouse 集群(ReplicatedMergeTree)+ 副本 |
| 多实例 CDC 重复消费 | ClickHouse 重复写入 | ReplacingMergeTree ORDER BY 去重 + Redis SETNX 幂等 |
| 大数据量 Dashboard 聚合超时 | 管理员 Dashboard 全校聚合慢 | 物化视图预聚合 + 异步刷新 + 缓存 5min |
| 知识点元数据与成绩关联失败 | mastery_snapshot 缺知识点标题 | content CDC 同步 + 缺失时显示 knowledge_point_id |
8.3 coord 交叉审查结论对齐(v2.1:原"未决设计决策"已全部仲裁)
| # | 议题 | coord 仲裁结论 | 涉及文档 |
|---|---|---|---|
| 1 | data-ana 发布 edu.insight.mastery.updated 用直接 producer(非 Outbox)是否合规 |
✅ 已仲裁:派生数据事件豁免 Outbox(coord-cross-review §3.3 + 004 §12.2) | 004 §12.2 |
| 2 | 新增 edu.insight.ai.usage topic + AIUsageEvent proto message |
✅ 已仲裁:coord 在 004 §7.2 登记 + events.proto 补 message;⚠️ events.proto 当前缺 AIUsageEvent,待 coord 落实 | 004 §7.2 + events.proto |
| 3 | iam 新增 GetEffectiveDataScope gRPC RPC |
✅ 已仲裁:P4 补全(coord-cross-review §2 #3);⚠️ iam.proto 当前仅 4 RPC,未实现 | iam.proto |
| 4 | data-ana 实现 gRPC server 决策 | ✅ 已仲裁:P4 启用(coord-cross-review §2.1 gRPC 启用阶段) | 004 §4.1 |
| 5 | ClickHouse DDL 管理位置(建议 infra/clickhouse/ddl/) |
⏳ 待 coord 建立 infra/clickhouse/ddl/ 目录 + data-ana 提供内容 |
infra/ |
| 6 | 端口冲突检查:data-ana HTTP=3006 / gRPC=50055 | ✅ 无冲突(coord-cross-review §4.3 全局端口矩阵) | port-allocation |
| 7 | 错误码前缀检查:DATA_ANA_* |
✅ 无冲突(matrix.md §6 错误码前缀矩阵) | matrix.md §6 |
| 8 | 黄金模板对齐:Python 服务无 NestJS 装饰器,权限校验用 FastAPI Depends 等价物是否认可 | ✅ 已仲裁:认可(coord-cross-review §5.3) | — |
| 9 | edu.insight.warning.triggered topic 新增 |
⏳ 待 coord 在 004 §7.2 登记(v2 新提案) | 004 §7.2 |
| 10 | analytics.proto 扩展(4 端 Dashboard / Warning / MasteryDistribution / Stream RPC) | ⏳ 待 coord 审议(v2 新提案,§4.2) | analytics.proto |
所有"未决设计决策"已消除。P4 阻塞项:#2 events.proto 缺 AIUsageEvent、#3 iam.proto 缺 GetEffectiveDataScope,均待 coord 落实。剩余 3 项 ⏳ 为 v2 新提案,待 coord 审议但不阻塞 P4 主体实现(可先用现有 RPC + HTTP 端点 + mock 兜底)。
9. 掌握度计算算法(ai-allocation §5 + pending-features P4 要求)
9.1 算法选型
| 算法 | 公式 | 适用场景 | 默认 |
|---|---|---|---|
WEIGHTED_MOVING_AVG |
$M = \sum_{i=1}^{n} w_i \cdot s_i / \sum w_i$,w_i = 0.6^i |
学生有多次同知识点考试成绩 | ✅ |
FORGETTING_CURVE |
$M(t) = M_0 \cdot e^{-t/S}$,S = 30 天 |
长期未考的知识点,掌握度衰减 | ✅(与上者叠加) |
SIMPLE_AVG |
M = \frac{1}{n}\sum s_i |
数据稀疏(< 3 次) | 兜底 |
综合公式(默认策略 WEIGHTED_MOVING_AVG + FORGETTING_CURVE):
M_final = max(
WEIGHTED_MOVING_AVG(近 5 次成绩, 时间倒序加权),
FORGETTING_CURVE(最近一次掌握度, 距今天数)
)
- 取 max 是为了避免"刚考完高分但长期没考"被遗忘曲线过度拉低
- 近 5 次不足时,用 SIMPLE_AVG 兜底
- 所有成绩归一化到 [0, 1](score / 100)
9.2 算法参数(可配置)
| 参数 | 默认值 | 说明 |
|---|---|---|
window_size |
5 | 加权滑动窗口大小 |
decay_base |
0.6 | 加权衰减基数(越近权重越高) |
forgetting_half_life |
30 | 遗忘曲线半衰期(天) |
min_samples_for_wma |
3 | 低于此数降级为 SIMPLE_AVG |
score_max |
100 | 成绩满分(用于归一化) |
9.3 触发时机
- CDC
core_edu_grades事件到达 → 异步计算 → 写mastery_snapshot+student_dashboard_view→ 发布MasteryUpdated - 定时任务(每日 02:00)→ 对所有知识点重算遗忘曲线衰减 → 更新
mastery_snapshot(不发布事件,避免风暴) - 手动触发(管理员 Dashboard "重算掌握度"按钮,v2 预留 API
POST /analytics/mastery/recalculate)
9.4 预警阈值设计(ai-allocation §5 要求)
| 预警类型 | 触发条件 | severity | 通知动作 |
|---|---|---|---|
LOW_MASTERY |
mastery_level < 0.4 | WARN | 推送学生 + 教师 |
CRITICAL_LOW |
mastery_level < 0.2 | CRITICAL | 推送学生 + 教师 + 家长 |
SCORE_DROP |
本次成绩环比下降 ≥ 20% | WARN | 推送教师 |
ABSENT_FREQUENT |
单周缺勤 ≥ 3 次 | WARN | 推送教师 + 家长 |
TREND_DECLINE |
连续 3 次成绩下降 | INFO | 教师仪表盘标记 |
预警去重:同一 (target_id, warning_type) 24h 内只触发一次(Redis 位图 data_ana:warning:{target_id}:{type}:{date})
10. 缓存策略(004 §6.3 缓存策略矩阵对齐)
10.1 Redis 连接
# config.py 新增
redis_url: str = "redis://edu-redis:6379/3" # data-ana 专用 db=3
redis_pool_size: int = 10
redis_socket_timeout_ms: int = 200
10.2 缓存键命名规范
| 键模式 | TTL | 用途 | 失效策略 |
|---|---|---|---|
data_ana:datascope:{user_id} |
5min | DataScope 缓存 | 角色变更主动 DEL |
data_ana:exam:{exam_id} |
30day | ExamCache(P6 多实例共享) | exam 结束 30 天后过期 |
data_ana:dedup:{event_id} |
7day | 事件幂等去重 | 自然过期 |
data_ana:warning:{target_id}:{type}:{date} |
25h | 预警去重位图 | 自然过期 |
data_ana:dashboard:{user_id}:{scope} |
5min | Dashboard 聚合结果缓存 | 主动 DEL on 新成绩写入 |
data_ana:kp_meta:{knowledge_point_id} |
1day | 知识点元数据缓存 | content CDC 更新时 DEL |
10.3 缓存降级
- Redis 不可达:所有缓存查询跳过(cache miss),直查 ClickHouse / iam gRPC
- DataScope 缓存 miss → 调 iam gRPC(增加 iam 负载,需监控)
- Dashboard 缓存 miss → 直查 ClickHouse(增加 CH 负载,可能触发查询超时降级)
11. CDC 水平扩展(P6 铺垫,详见 §5.3)
P4 单实例 → P6 多实例演进路径已在 §5.3 详述。本节补充容量规划。
11.1 ClickHouse 容量规划
假设:全校 5000 学生,每学生每月 20 次成绩写入,5 个知识点/次。
| 宽表 | 月增量(行) | 单行大小 | 月增量(MB) | 年增量(GB) | TTL |
|---|---|---|---|---|---|
student_dashboard_view |
500K | 200B | 100 | 1.2 | 3 年 |
student_errors |
100K | 150B | 15 | 0.18 | 2 年 |
mastery_snapshot |
250K | 100B | 25 | 0.3 | 5 年 |
attendance_logs |
500K | 80B | 40 | 0.48 | 3 年 |
ai_usage_log |
50K | 120B | 6 | 0.07 | 1 年 |
总计:年增量 ~2.3 GB,单节点 ClickHouse 可承载。P6 引入集群时按 class_id hash 分片。
11.2 冷热分层
- 热数据:近 6 个月,SSD 存储
- 冷数据:6 个月 - 3 年,HDD 存储(ClickHouse
TTL ... TO VOLUME 'cold') - 归档数据:> 3 年,导出 Parquet 到对象存储(P6 评估)
11.3 Kafka topic partition 规划
| Topic | P4 partition | P6 partition | 依据 |
|---|---|---|---|
edu-cdc.next_edu_cloud.core_edu_grades |
1 | 4 | 高频写入,按 class_id 分区 |
edu-cdc.next_edu_cloud.core_edu_exams |
1 | 2 | 低频 |
edu.insight.mastery.updated |
1 | 4 | 下游多消费者 |
12. 数据治理与隐私(v2 新增,长期合规铺垫)
12.1 GDPR / 个人信息保护法对齐
| 场景 | 实现 | 阶段 |
|---|---|---|
| 学生删除权 | ClickHouse 无 UPDATE/DELETE,用 ALTER TABLE ... DELETE WHERE student_id = ?(异步 mutation) |
P6 |
| 数据导出权 | GET /analytics/student/{id}/export → JSON 全量导出(v2 预留 API) |
P6 |
| 字段脱敏 | student_id 在日志中 hash 化(structlog processor) | P4 |
| 审计日志 | 所有查询记录 user_id + query + timestamp 到 ClickHouse audit_log 表(v2 新增表,P6 实现) |
P6 |
| 数据保留期 | 见 §11.1 TTL 列 | P4+ |
12.2 敏感数据标记
student_id:PII,日志中 hash 化score:非敏感,可明文attendance_status:非敏感ai_usage:含 user_id + prompt_tokens,prompt 内容不入库(ai 服务侧处理)
13. 测试策略(v2 新增)
13.1 测试金字塔
| 层级 | 工具 | 覆盖范围 | 目标覆盖率 |
|---|---|---|---|
| 单元测试 | pytest + pytest-asyncio | 算法(掌握度计算/预警阈值)、Pydantic 模型 | ≥ 80% |
| 集成测试 | pytest + Testcontainers | ClickHouse DDL + 查询、Kafka producer、Redis | ≥ 60% |
| 契约测试 | pact-python(v2 评估) | gRPC 契约(analytics.proto) | 关键 RPC |
| E2E 测试 | pytest + docker-compose | CDC 链路端到端(MySQL → Debezium → CH) | 关键场景 |
13.2 Testcontainers 配置
@pytest.fixture(scope="session")
async def clickhouse_container():
container = ClickHouseContainer("clickhouse/clickhouse-server:24-alpine")
container.start()
yield container
container.stop()
@pytest.fixture(scope="session")
async def kafka_container():
container = KafkaContainer("confluentinc/cp-kafka:7.6.0")
container.start()
yield container
container.stop()
13.3 测试数据
- 使用 Faker 生成 100 学生 × 10 班级 × 5 科目 × 20 次成绩的合成数据
- 边界用例:空数据、单条数据、超大数据(10000 行)、重复事件(幂等测试)
14. Dockerfile 多阶段构建(v2 新增)
# services/data-ana/Dockerfile
FROM python:3.12-slim AS builder
WORKDIR /app
RUN pip install --no-cache-dir uv
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev
FROM python:3.12-slim AS runtime
WORKDIR /app
COPY --from=builder /app/.venv /app/.venv
COPY src/ ./src/
ENV PATH="/app/.venv/bin:$PATH"
ENV PYTHONUNBUFFERED=1
EXPOSE 3006 50055
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:3006/healthz')" || exit 1
CMD ["python", "-m", "uvicorn", "src.data_ana.main:app", \
"--host", "0.0.0.0", "--port", "3006"]
优化点:
- 多阶段构建,runtime 镜像 < 200MB
- uv 替代 pip,安装速度 10x
- uv.lock 锁定依赖版本
- 健康检查集成
15. 完整配置项清单(v2 新增,补全 config.py)
# config.py 完整配置项
class Settings(BaseSettings):
# 服务
service_name: str = "data-ana"
http_port: int = 3006
grpc_port: int = 50055
log_level: str = "INFO"
# ClickHouse
clickhouse_host: str = "edu-clickhouse"
clickhouse_port: int = 8123
clickhouse_user: str = "default"
clickhouse_password: str = ""
clickhouse_database: str = "edu_analytics"
clickhouse_connect_timeout_ms: int = 3000
clickhouse_query_timeout_s: int = 3 # P4 退出标准 5s,查询 3s 超时降级
# Kafka
kafka_brokers: str = "kafka:29092"
kafka_consumer_group: str = "data-ana-cdc"
kafka_cdc_topics: str = (
"edu-cdc.next_edu_cloud.core_edu_grades,"
"edu-cdc.next_edu_cloud.core_edu_exams,"
"edu-cdc.next_edu_cloud.core_edu_homework_submissions,"
"edu-cdc.next_edu_cloud.core_edu_attendance,"
"edu-cdc.next_edu_cloud.classes,"
"edu-cdc.next_edu_cloud.iam_users,"
"edu-cdc.next_edu_cloud.content_knowledge_points"
)
kafka_mastery_topic: str = "edu.insight.mastery.updated"
kafka_warning_topic: str = "edu.insight.warning.triggered"
kafka_ai_usage_topic: str = "edu.insight.ai.usage"
kafka_enable_auto_commit: bool = False # v2: 手动 commit
kafka_auto_offset_reset: str = "latest"
# iam gRPC
iam_grpc_endpoint: str = "iam:50052"
iam_grpc_timeout_s: int = 2
datascope_cache_ttl_s: int = 300
# Redis
redis_url: str = "redis://edu-redis:6379/3"
redis_pool_size: int = 10
redis_socket_timeout_ms: int = 200
# OTel
otel_endpoint: str = "http://otel-collector:4317"
otel_service_name: str = "data-ana"
# 掌握度算法
mastery_window_size: int = 5
mastery_decay_base: float = 0.6
mastery_forgetting_half_life_days: int = 30
mastery_min_samples: int = 3
# 预警阈值
warning_low_mastery_threshold: float = 0.4
warning_critical_mastery_threshold: float = 0.2
warning_score_drop_percent: float = 0.2
warning_absent_per_week: int = 3
# 降级
degraded_mode_enabled: bool = True
model_config = SettingsConfigDict(env_file=".env", env_prefix="DATA_ANA_")
16. API 版本化策略(v2 新增)
16.1 HTTP 版本化
- URL 前缀:
/analytics/v1/...(当前省略 v1,v2 起强制) - 破坏性变更:新增
/analytics/v2/...,旧 v1 保留 6 个月 - 非破坏性变更:直接在 v1 上加字段(Pydantic 模型字段可选)
16.2 gRPC 版本化
- proto package:
next_edu_cloud.analytics.v1(当前) - 破坏性变更:新增
next_edu_cloud.analytics.v2,v1 保留 - buf breaking 检查:CI 强制(FILE 级别)
16.3 事件版本化
- 事件 schema 字段:
schema_version: "v1"(必填) - 破坏性变更:发布
v2事件,consumer 同时支持 v1 + v2(6 个月过渡) - 非破坏性变更:直接加字段,schema_version 不变
17. Projection 演进路径(v2 新增)
17.1 当前(P4):CDC 直连
MySQL binlog → Debezium → Kafka (edu-cdc.*) → data-ana CdcConsumer → ClickHouse
- 优点:实时性好(< 5s),无需 core-edu 改造
- 缺点:依赖 binlog schema,core-edu 表结构变更可能破坏 CDC
17.2 未来(P4 后期评估):双消费
MySQL binlog → Debezium → Kafka (edu-cdc.*) → data-ana CdcConsumer → ClickHouse(宽表)
↓
core-edu Outbox → Kafka (edu.domain.events) → data-ana EventConsumer → ClickHouse(业务事件视图)
- CDC 通道:保持宽表实时性
- 领域事件通道:业务语义清晰,schema 稳定(proto 定义)
- 两通道并存,分别写不同 ClickHouse 表,避免重复
17.3 远期(P6):CDC + CQRS 物化视图
CDC → ClickHouse 原始表 → MaterializedView → 宽表(自动聚合)
- 减少应用层计算,由 ClickHouse MV 自动维护宽表
- 掌握度计算仍需应用层(复杂算法 CH 难以表达)
18. 未来阶段铺垫(v2 新增,长远架构)
18.1 P5 铺垫(AI 网关 + 沟通通知)
| 演进点 | P4 准备 | P5 实现 |
|---|---|---|
| AI 用量消费 | ai_usage_log 表已建 + edu.insight.ai.usage topic 已订阅 |
ai 服务发布事件,data-ana 落库 + 用量统计 API |
| gRPC stream 推送 | SubscribeMasteryUpdate RPC 已在 proto 提案(§4.2) |
实现 server-streaming,AI 实时获取掌握度变化 |
| 预警触发通知 | WarningTriggered 事件已设计(§5.2) |
msg 服务消费事件 → 推送 push-gateway |
18.2 P6 铺垫(生产硬化)
| 演进点 | P4 准备 | P6 实现 |
|---|---|---|
| CDC 水平扩展 | ExamCache 抽象层 + Redis 配置已就绪(§5.3) | 多实例 + HPA + partition 扩容 |
| ClickHouse 集群 | DDL 用 ReplacingMergeTree(兼容 Replicated*) | ReplicatedMergeTree + 副本 + 分片 |
| Service Mesh mTLS | gRPC server 已抽象 interceptor | Istio 注入 sidecar,mTLS 自动双向 |
| 数据治理 | student_id 日志 hash + TTL 设计(§12) | ALTER TABLE DELETE + 审计日志表 |
| 配置中心 | Settings 用 pydantic-settings(支持热更新) | Consul 接入,watch 配置变更 |
| 灾备 | Kafka producer idempotent + transactional | Kafka 多副本 + ClickHouse 跨机房副本 |
18.3 远期(P7+)预留
- 实时流式分析:Flink 替代 CdcConsumer,支持窗口聚合(如"近 7 天平均掌握度"实时计算)
- 数据湖:ClickHouse 冷数据 → S3/MinIO + Iceberg 表格式,支持 ad-hoc 查询
- AI 驱动的学情诊断:data-ana 提供数据,ai 服务提供模型,组合成"智能诊断报告"
- 多租户:当前 school_id 隐含在 class_id 中,远期可显式多租户隔离
19. 非功能性需求(NFR,v2.1 新增)
| 维度 | 指标 | 目标值 | 验证方式 |
|---|---|---|---|
| 性能 | ClickHouse 宽表查询 P99 延迟 | < 5s | P4 退出标准 + Prometheus 监控 |
| 性能 | CDC 链路延迟 | < 5s | Debezium ts_ms → CH 写入时间差 |
| 性能 | gRPC RPC P99 延迟 | < 500ms | OTel trace |
| 可用性 | P4 单实例 | 99% | 降级模式保证 |
| 可用性 | P6 多实例 + ClickHouse 集群 | 99.9% | HPA + 副本 |
| 安全 | DataScope 越权防护 | 0 越权 | 权限测试 + 渗透测试 |
| 安全 | PII 日志脱敏 | 100% student_id hash | structlog processor 单测 |
| 可扩展性 | CDC 消费者水平扩展 | N 实例无重复 | ReplacingMergeTree + Redis SETNX |
| 数据保留 | ClickHouse TTL | 见 §11.1 | TTL 策略自动执行 |
20. 服务审计表 v2(黄金模板对齐)
| 维度 | 状态 | 说明 |
|---|---|---|
| 契约(proto) | ✅ | analytics.proto 已定义 3 RPC,v2 提案扩展至 12 RPC(含 GetStudentMastery / TriggerWarning) |
| 路由 | ✅ | HTTP 14 端点(3 基础 + 11 业务)+ gRPC 12 RPC |
| 数据访问 | ✅ | ClickHouse 5 宽表 + Redis 缓存 + iam gRPC |
| 鉴权 | ✅ | FastAPI Depends 等价物 + 7 权限点 |
| 错误处理 | ✅ | 13 错误码 DATA_ANA_* + ActionState 信封 |
| 可观测 - 日志 | ✅ | structlog + JSONRenderer(v2 改进) |
| 可观测 - 指标 | ✅ | 16 Prometheus 指标 |
| 可观测 - 链路 | ✅ | OTel + gRPC interceptor + CDC span 注入 |
| 健康检查 | ✅ | /healthz + /readyz(v2 补 redis/iam_grpc) |
| 配置管理 | ✅ | pydantic-settings 完整配置项(§15) |
| 降级模式 | ✅ | 4 降级场景(CH/Kafka/Redis/iam) |
| NFR | ✅ | v2.1 新增 §19,性能/可用性/安全/可扩展性目标已定义 |
| 响应信封 | ⚠️ | v2 设计对齐 ActionState,实现阶段需重构(P0 整改) |
| Redis | ⚠️ | v2 设计就绪,实现阶段需新增 redis-py 依赖 + RedisClient |
| gRPC server | ⚠️ | v2 设计就绪,实现阶段需新增 grpc.aio + betterproto + server interceptor |
| Dockerfile | ⚠️ | v2 设计多阶段,实现阶段需新增 |
| 测试 | ⚠️ | v2 策略就绪,实现阶段需补 pytest + Testcontainers |
| CDC 幂等性 | ⚠️ | v2 改进手动 commit + Redis SETNX,实现阶段需重构 |
| CDC 水平扩展 | ✅ | v2 设计 P6 演进路径,P4 单实例可工作 |
| 数据治理 | ✅ | v2 设计 GDPR 对齐方案,P6 实现 |
| 容量规划 | ✅ | v2 估算年增量 2.3GB,单节点可承载 |
AI Agent: ai11 (data-ana) Branch: 单仓库并行模式(直接提交 main) Coordinator: coord-ai v2 修订依据: ai-allocation.md §3.2 重新分配 + coord-cross-review.md §2/§3.3/§5.3 + 004 §4.1/§7.2/§12.2 + ai-allocation §5 设计重点 + pending-features P4 退出标准 v2.1 审核修订: 修正 004 章节引用断裂(§4.2/§11.4/§11.5/§15.3 不存在→改引 coord-cross-review);修正 ActionState 实现矛盾(degraded 从 error.details 移至顶层 details);补 RPC 至 12 个(GetStudentMastery/TriggerWarning);修正 HTTP 端点计数 14;Dockerfile HEALTHCHECK 改用 urllib 避免 curl 依赖;新增 §19 NFR 章节 v2 审核结论: 16 项遗漏已全部补强(3 项 P0 + 5 项 P1 + 8 项 P2),文档进入实现阶段无阻塞(P4 阻塞项:iam.proto GetEffectiveDataScope + events.proto AIUsageEvent 待 coord 落实,可用 fallback/mock 兜底)