包含 CDC consumer、analytics/mastery/warning service、grpc server、repository、ClickHouse DDL 等
387 lines
17 KiB
Markdown
387 lines
17 KiB
Markdown
# data-ana 数据分析服务
|
||
|
||
> 版本:1.0(P4 完整版)
|
||
> 阶段:D6 智能洞察领域(仲裁已对齐,v2 实现版)
|
||
> 端口:HTTP 3006(Gateway 直连降级)+ gRPC 50055(主入口,BFF/ai 调用)
|
||
> 日期:2026-07-10
|
||
|
||
## 职责
|
||
|
||
数据分析限界上下文(Python/FastAPI 微服务),消费 core-edu / content / iam / ai 的 CDC 事件,
|
||
构建 ClickHouse 学情宽表,计算知识点掌握度,评估预警,为前端提供 4 端 Dashboard 查询服务。
|
||
|
||
### 核心能力
|
||
|
||
- **5 张宽表**:student_dashboard_view / student_errors / mastery_snapshot / attendance_logs / ai_usage_log
|
||
- **CDC 链路**:Debezium → Kafka → data-ana 消费 → ClickHouse(at-least-once + 手动 commit + ReplacingMergeTree 去重)
|
||
- **掌握度算法**:WEIGHTED_MOVING_AVG(w_i=0.6^i)+ FORGETTING_CURVE(半衰期 30 天)取 max
|
||
- **预警评估**:5 类预警(LOW_MASTERY / CRITICAL_LOW / SCORE_DROP / ABSENT_FREQUENT / TREND_DECLINE)+ Redis 25h 去重
|
||
- **DataScope 过滤**:6 级(SELF/CLASS/GRADE/SCHOOL/DISTRICT/ALL),iam gRPC + 角色降级
|
||
- **gRPC 12 RPC**:AnalyticsService(含 server-streaming SubscribeMasteryUpdate)+ HealthService
|
||
- **HTTP 14 端点**:3 基础 + 11 业务(ActionState 信封 + degraded 标记)
|
||
|
||
## 架构图
|
||
|
||
```mermaid
|
||
flowchart TB
|
||
subgraph Entry["入口层"]
|
||
HTTP[FastAPI HTTP<br/>:3006 /analytics/* + /healthz + /readyz]
|
||
GRPC[grpc.aio Server<br/>:50055 AnalyticsService 12 RPC]
|
||
end
|
||
|
||
subgraph Service["应用服务层"]
|
||
S1[AnalyticsService<br/>4 Dashboard + 班级/趋势查询]
|
||
S2[MasteryService<br/>掌握度计算 + 事件发布]
|
||
S3[WarningService<br/>5 类预警 + Redis 去重]
|
||
end
|
||
|
||
subgraph Repo["数据访问层"]
|
||
R1[ClickHouseRepository<br/>FINAL/argMax + DataScope WHERE]
|
||
R2[KafkaProducer<br/>mastery.updated 派生事件]
|
||
R3[IamClient<br/>gRPC GetEffectiveDataScope]
|
||
R4[RedisClient<br/>DataScope 缓存 + 幂等]
|
||
end
|
||
|
||
subgraph Consumer["CDC 消费者(后台任务)"]
|
||
C1[CdcConsumer<br/>手动 commit + 7 类事件路由]
|
||
C2[ExamCache<br/>LRU 10000 exam_id→metadata]
|
||
end
|
||
|
||
subgraph Storage["存储/总线"]
|
||
CH[(ClickHouse<br/>5 宽表)]
|
||
KAFKA[(Kafka<br/>edu-cdc.* + edu.insight.mastery.updated)]
|
||
IAM[iam gRPC :50052]
|
||
REDIS[(Redis<br/>data_ana:* 键)]
|
||
end
|
||
|
||
HTTP --> S1
|
||
HTTP --> S3
|
||
GRPC --> S1
|
||
S1 --> R1
|
||
S2 --> R1
|
||
S2 --> R2
|
||
S3 --> R1
|
||
S3 --> R2
|
||
R1 --> CH
|
||
R2 --> KAFKA
|
||
R3 --> IAM
|
||
R3 --> R4
|
||
R4 --> REDIS
|
||
C1 --> C2
|
||
C1 --> R1
|
||
C1 --> R4
|
||
C1 --> KAFKA
|
||
```
|
||
|
||
## 技术栈
|
||
|
||
- **Python 3.12+** / **FastAPI 0.115+**(HTTP 端点)
|
||
- **grpcio 1.66+** / **grpcio-health-checking**(gRPC server + HealthService)
|
||
- **clickhouse-connect 0.7+**(ClickHouse 宽表查询,支持 FINAL/argMax)
|
||
- **aiokafka 0.11+**(CDC 消费者,手动 commit + 幂等去重)
|
||
- **redis 5.0+**(DataScope 缓存 + 事件去重 + 预警位图)
|
||
- **pydantic 2.9+** / **pydantic-settings 2.5+**(模型校验 + 配置)
|
||
- **structlog 24.4+**(结构化 JSON 日志)
|
||
- **prometheus-client 0.20+**(/metrics 指标)
|
||
- **opentelemetry-sdk 1.27+**(OTLP 链路追踪)
|
||
- **protobuf 5.28+**(buf generate 生成的 stub 运行时)
|
||
|
||
## 模块结构
|
||
|
||
```
|
||
src/data_ana/
|
||
├─ __init__.py
|
||
├─ main.py # FastAPI 入口(14 端点 + lifespan + ActionState)
|
||
├─ config.py # pydantic-settings 配置(env_prefix=DATA_ANA_)
|
||
├─ analytics_service.py # 4 Dashboard + 班级/趋势查询编排
|
||
├─ mastery_service.py # 掌握度算法(WMA + ForgettingCurve)
|
||
├─ warning_service.py # 5 类预警评估 + Redis 去重
|
||
├─ exam_cache.py # LRU exam_id→metadata 映射(max 10000)
|
||
├─ cdc_consumer.py # CDC 消费者(7 类事件 + 手动 commit)
|
||
├─ grpc_server.py # 12 RPC + HealthService + SubscribeMasteryUpdate
|
||
├─ clickhouse_client.py # ClickHouse 客户端单例(降级模式)
|
||
├─ repository/
|
||
│ ├─ __init__.py
|
||
│ ├─ clickhouse_repository.py # 宽表查询 + FINAL/argMax + DataScope WHERE
|
||
│ ├─ redis_client.py # Redis 异步客户端(cache + dedup + bitmap)
|
||
│ ├─ iam_client.py # iam gRPC GetEffectiveDataScope + 角色降级
|
||
│ └─ kafka_producer.py # mastery.updated / warning.triggered 发布
|
||
└─ shared/
|
||
├─ __init__.py
|
||
├─ action_state.py # ActionState[T] 统一信封(ok/fail + degraded)
|
||
├─ errors.py # ErrorCode 13 类 + DataAnaError
|
||
└─ permissions.py # 权限点 + DataScopeLevel + build_datascope_where
|
||
```
|
||
|
||
## 开发
|
||
|
||
```bash
|
||
# 安装依赖
|
||
uv sync
|
||
|
||
# 本地开发(HTTP + gRPC + CDC 同时启动)
|
||
uv run uvicorn src.data_ana.main:app --host 0.0.0.0 --port 3006 --reload
|
||
|
||
# ruff 校验
|
||
ruff check src/
|
||
|
||
# 类型检查(如已配置 mypy)
|
||
mypy src/
|
||
```
|
||
|
||
## 配置项
|
||
|
||
所有配置使用 `DATA_ANA_` 前缀(pydantic-settings env_prefix)。
|
||
|
||
### 基础配置
|
||
|
||
| 变量 | 默认值 | 说明 |
|
||
| ------------------------ | --------------------- | ------------------------------------ |
|
||
| `DATA_ANA_HTTP_PORT` | 3006 | HTTP 端口(Gateway 直连降级) |
|
||
| `DATA_ANA_GRPC_PORT` | 50055 | gRPC 端口(AnalyticsService 主入口) |
|
||
| `DATA_ANA_DEV_MODE` | true | 开发模式(关闭签名校验) |
|
||
| `DATA_ANA_LOG_LEVEL` | info | 日志级别(debug/info/warning/error) |
|
||
| `DATA_ANA_OTEL_ENDPOINT` | http://localhost:4318 | OpenTelemetry OTLP 端点 |
|
||
|
||
### 外部依赖(全部可选,未配置则降级模式)
|
||
|
||
| 变量 | 默认值 | 说明 |
|
||
| ------------------------------ | ------------- | ------------------------------------------ |
|
||
| `DATA_ANA_CLICKHOUSE_HOST` | "" | ClickHouse 主机(空字符串=降级模式) |
|
||
| `DATA_ANA_CLICKHOUSE_PORT` | 8123 | ClickHouse HTTP 端口 |
|
||
| `DATA_ANA_CLICKHOUSE_DATABASE` | edu_analytics | ClickHouse 数据库 |
|
||
| `DATA_ANA_CLICKHOUSE_USER` | "" | ClickHouse 用户名 |
|
||
| `DATA_ANA_CLICKHOUSE_PASSWORD` | "" | ClickHouse 密码 |
|
||
| `DATA_ANA_KAFKA_BROKERS` | "" | Kafka broker 列表(逗号分隔,空=禁用 CDC) |
|
||
| `DATA_ANA_KAFKA_GROUP_ID` | data-ana-cdc | 消费者组 ID |
|
||
| `DATA_ANA_REDIS_URL` | "" | Redis URL(空=禁用缓存/去重) |
|
||
| `DATA_ANA_IAM_GRPC_ENDPOINT` | "" | iam gRPC 端点(空=角色降级) |
|
||
|
||
### 降级模式说明
|
||
|
||
- **ClickHouse 未配置/不可达**:查询返回骨架数据(空数组 + degraded=true)
|
||
- **Kafka 未配置**:CDC 消费者不启动,`/readyz` 仍就绪
|
||
- **Redis 未配置/不可达**:缓存失效,每次查询重新计算 DataScope
|
||
- **iam gRPC 未配置/不可达**:基于 JWT 角色 fallback(teacher→CLASS,student→SELF)
|
||
- **grpcio 未安装**:gRPC server 不启动,HTTP 仍可用
|
||
|
||
## 端点清单
|
||
|
||
### 基础端点(3 个)
|
||
|
||
| 方法 | 路径 | 说明 |
|
||
| ---- | ---------- | ----------------------------------------------------------- |
|
||
| GET | `/` | 根信息(服务名/版本/端点列表) |
|
||
| GET | `/healthz` | liveness 检查(进程存活即 ok) |
|
||
| GET | `/readyz` | readiness 检查(5 依赖状态:clickhouse/cdc/redis/iam/grpc) |
|
||
| GET | `/metrics` | Prometheus 指标 |
|
||
|
||
### 业务端点(11 个,全部返回 ActionState[T])
|
||
|
||
| 方法 | 路径 | 说明 |
|
||
| ---- | -------------------------------------------------- | ------------------------------- |
|
||
| GET | `/analytics/class/{class_id}/performance` | 班级成绩分析(平均分/及格率) |
|
||
| GET | `/analytics/class/{class_id}/mastery-distribution` | 班级掌握度分布(三档) |
|
||
| GET | `/analytics/student/{student_id}/weakness` | 学生薄弱知识点(mastery < 0.6) |
|
||
| GET | `/analytics/student/{student_id}/mastery` | 学生掌握度明细 |
|
||
| GET | `/analytics/student/{student_id}/trend` | 学习趋势(历史成绩曲线) |
|
||
| GET | `/analytics/student/{student_id}/errorbook` | 学生错题本 |
|
||
| GET | `/analytics/teacher/dashboard` | 教师仪表盘 |
|
||
| GET | `/analytics/student/dashboard` | 学生仪表盘 |
|
||
| GET | `/analytics/parent/dashboard` | 家长仪表盘 |
|
||
| GET | `/analytics/admin/dashboard` | 管理员仪表盘 |
|
||
| GET | `/analytics/warnings` | 预警列表 |
|
||
| POST | `/analytics/warnings/trigger` | 手动触发预警 |
|
||
|
||
### ActionState 信封格式
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": { ... },
|
||
"details": {
|
||
"degraded": false,
|
||
"degraded_reason": ""
|
||
}
|
||
}
|
||
```
|
||
|
||
降级时 `degraded=true`,`data` 返回骨架数据(空数组/零值)。
|
||
|
||
## gRPC 契约
|
||
|
||
### AnalyticsService(12 RPC,端口 50055)
|
||
|
||
| RPC | 类型 | 说明 |
|
||
| ---------------------- | ---------------- | ----------------------------------- |
|
||
| GetClassPerformance | unary | 班级成绩分析 |
|
||
| GetStudentWeakness | unary | 学生薄弱知识点 |
|
||
| GetLearningTrend | unary | 学习趋势 |
|
||
| GetTeacherDashboard | unary | 教师仪表盘 |
|
||
| GetStudentDashboard | unary | 学生仪表盘 |
|
||
| GetParentDashboard | unary | 家长仪表盘 |
|
||
| GetAdminDashboard | unary | 管理员仪表盘 |
|
||
| GetWarnings | unary | 预警列表 |
|
||
| TriggerWarning | unary | 手动触发预警 |
|
||
| GetMasteryDistribution | unary | 班级掌握度分布 |
|
||
| GetStudentMastery | unary | 学生掌握度明细 |
|
||
| SubscribeMasteryUpdate | server-streaming | 掌握度更新订阅(P5+ AI 个性化通道) |
|
||
|
||
### HealthService
|
||
|
||
`grpc.health.v1.Health` 标准 K8s 探针。
|
||
|
||
### proto 定义
|
||
|
||
- `packages/shared-proto/proto/analytics.proto`(12 RPC + 全部消息)
|
||
- `packages/shared-proto/proto/events.proto`(MasteryEvent + AIUsageEvent)
|
||
- `packages/shared-proto/proto/iam.proto`(GetEffectiveDataScope)
|
||
|
||
## CDC 事件消费
|
||
|
||
### 消费的 7 类事件
|
||
|
||
| 事件源 | Topic | 处理 |
|
||
| ------------------------ | ------------------------------------------------- | ------------------------- |
|
||
| core-edu.exams | `edu-cdc.next_edu_cloud.core_edu_exams` | 写 exam_cache |
|
||
| core-edu.grades | `edu-cdc.next_edu_cloud.core_edu_grades` | 写 student_dashboard_view |
|
||
| core-edu.homework | `edu-cdc.next_edu_cloud.core_edu_homework` | 写 student_dashboard_view |
|
||
| core-edu.attendance | `edu-cdc.next_edu_cloud.core_edu_attendance` | 写 attendance_logs |
|
||
| content.knowledge_points | `edu-cdc.next_edu_cloud.content_knowledge_points` | 知识点元数据 |
|
||
| ai.ai_usage | `edu.insight.ai.usage` | 写 ai_usage_log |
|
||
|
||
### 幂等保证
|
||
|
||
- **Kafka producer**:`enable_idempotence=True` + `transactional_id`
|
||
- **Consumer 去重**:Redis SETNX `data_ana:dedup:{event_id}` TTL 7 天
|
||
- **ClickHouse 去重**:ReplacingMergeTree(last_updated) + FINAL/argMax 查询
|
||
- **手动 commit**:仅在 ClickHouse 写入成功后 commit(at-least-once 语义)
|
||
|
||
## 发布的派生数据事件
|
||
|
||
| 事件 | Topic | 触发条件 | Outbox 豁免 |
|
||
| ---------------- | ------------------------------------- | -------------- | ----------------------- |
|
||
| MasteryUpdated | `edu.insight.mastery.updated` | 掌握度计算完成 | ✅(004 §12.2 仲裁) |
|
||
| WarningTriggered | `edu.insight.mastery.updated`(复用) | 预警阈值触发 | ✅(004 §15.3 #6 仲裁) |
|
||
|
||
> Outbox 豁免理由:派生数据事件,无业务状态变更,重复消费幂等。
|
||
|
||
## ClickHouse 宽表
|
||
|
||
DDL 位置:`scripts/clickhouse-init.sql`
|
||
|
||
| 表名 | 引擎 | ORDER BY | 用途 |
|
||
| ---------------------- | ----------------------------------- | ----------------------------------------------- | -------------- |
|
||
| student_dashboard_view | ReplacingMergeTree(last_updated) | (student_id, exam_id, knowledge_point_id) | 学情宽表 |
|
||
| student_errors | ReplacingMergeTree(last_error_time) | (student_id, question_id) | 错题本 |
|
||
| mastery_snapshot | MergeTree | (student_id, knowledge_point_id, calculated_at) | 掌握度历史快照 |
|
||
| ai_usage_log | ReplacingMergeTree(occurred_at) | (request_id,) | AI 用量计费 |
|
||
| attendance_logs | ReplacingMergeTree(occurred_at) | (student_id, class_id, attendance_date) | 考勤记录 |
|
||
|
||
Mock 数据:`scripts/data-ana-mock-data.sql`
|
||
|
||
## 掌握度算法
|
||
|
||
### 公式
|
||
|
||
```
|
||
mastery = max(WMA, ForgettingCurve)
|
||
|
||
WMA = Σ(w_i * score_i) / Σ(w_i), w_i = 0.6^i (近 5 次,权重递减)
|
||
|
||
ForgettingCurve = last_mastery * exp(-ln(2) * days_since / 30)
|
||
```
|
||
|
||
### 分类
|
||
|
||
| 区间 | label | 说明 |
|
||
| --------- | ----------- | ------ |
|
||
| ≥ 0.8 | mastered | 已掌握 |
|
||
| 0.4 - 0.8 | progressing | 进展中 |
|
||
| < 0.4 | weak | 薄弱 |
|
||
|
||
### 降级规则
|
||
|
||
- 近 5 次成绩不足 5 次 → 降级 SIMPLE_AVG
|
||
- 无历史数据 → 返回 0.0 + label=weak
|
||
|
||
## 预警类型
|
||
|
||
| 类型 | 阈值 | severity | 说明 |
|
||
| --------------- | ------------- | -------- | -------- |
|
||
| LOW_MASTERY | mastery < 0.4 | WARN | 掌握度低 |
|
||
| CRITICAL_LOW | mastery < 0.2 | CRITICAL | 严重低 |
|
||
| SCORE_DROP | 下降 ≥ 20% | WARN | 成绩下降 |
|
||
| ABSENT_FREQUENT | ≥ 3 次/周 | WARN | 频繁缺勤 |
|
||
| TREND_DECLINE | 连续 3 次降 | INFO | 趋势下降 |
|
||
|
||
### 去重
|
||
|
||
- Redis key:`data_ana:warning:{target_id}:{type}:{date}`
|
||
- TTL:25 小时(跨日保证)
|
||
|
||
## DataScope 过滤
|
||
|
||
### 6 级
|
||
|
||
| 级别 | scope_ids 含义 |
|
||
| -------- | --------------------- |
|
||
| SELF | 学生本人 ID |
|
||
| CLASS | 可见 class_id 列表 |
|
||
| GRADE | 可见 grade_id 列表 |
|
||
| SCHOOL | 可见 school_id 列表 |
|
||
| DISTRICT | 可见 district_id 列表 |
|
||
| ALL | 无过滤 |
|
||
|
||
### 解析链
|
||
|
||
```
|
||
请求 → Redis 缓存(5min)→ iam gRPC GetEffectiveDataScope → 角色降级 fallback
|
||
```
|
||
|
||
### 角色降级(iam 不可用时)
|
||
|
||
| JWT 角色 | 降级 scope |
|
||
| -------- | --------------------- |
|
||
| teacher | CLASS(自己教的班级) |
|
||
| student | SELF(本人) |
|
||
| parent | SELF(本人 + 孩子) |
|
||
| admin | ALL |
|
||
|
||
## 部署
|
||
|
||
### Docker
|
||
|
||
```bash
|
||
# 构建(多阶段:builder + runtime)
|
||
docker build -t data-ana:1.0.0 services/data-ana/
|
||
|
||
# 运行(需 ClickHouse/Kafka/Redis 可达)
|
||
docker run -p 3006:3006 -p 50055:50055 \
|
||
-e DATA_ANA_CLICKHOUSE_HOST=clickhouse \
|
||
-e DATA_ANA_KAFKA_BROKERS=kafka:9092 \
|
||
-e DATA_ANA_REDIS_URL=redis://redis:6379 \
|
||
data-ana:1.0.0
|
||
```
|
||
|
||
### 健康检查
|
||
|
||
- HTTP `/healthz`:进程存活即 ok
|
||
- HTTP `/readyz`:检查 5 依赖(clickhouse/cdc/redis/iam/grpc)
|
||
- gRPC `grpc.health.v1.Health`:K8s 探针
|
||
|
||
## 对外契约
|
||
|
||
- **gRPC proto**:`packages/shared-proto/proto/analytics.proto`
|
||
- **HTTP OpenAPI**:启动后访问 `/docs`
|
||
- **CDC topic 命名**:`edu-cdc.next_edu_cloud.<table>`(Debezium 标准)
|
||
- **派生事件 topic**:`edu.insight.mastery.updated` / `edu.insight.ai.usage`
|
||
|
||
## 相关文档
|
||
|
||
- [02-architecture-design.md](./docs/02-architecture-design.md):模块架构设计 v2(仲裁后)
|
||
- [01-understanding.md](./docs/01-understanding.md):阶段 1 理解确认书
|
||
- [004 架构影响地图 §15.3](../../docs/architecture/004_architecture_impact_map.md):coord 仲裁结论
|
||
- [coord-cross-review.md](../../docs/architecture/coord-cross-review.md):交叉审查记录
|
||
- [known-issues §2.6](../../docs/troubleshooting/known-issues.md):data-ana 已知问题速查
|