# 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
:3006 /analytics/* + /healthz + /readyz]
GRPC[grpc.aio Server
:50055 AnalyticsService 12 RPC]
end
subgraph Service["应用服务层"]
S1[AnalyticsService
4 Dashboard + 班级/趋势查询]
S2[MasteryService
掌握度计算 + 事件发布]
S3[WarningService
5 类预警 + Redis 去重]
end
subgraph Repo["数据访问层"]
R1[ClickHouseRepository
FINAL/argMax + DataScope WHERE]
R2[KafkaProducer
mastery.updated 派生事件]
R3[IamClient
gRPC GetEffectiveDataScope]
R4[RedisClient
DataScope 缓存 + 幂等]
end
subgraph Consumer["CDC 消费者(后台任务)"]
C1[CdcConsumer
手动 commit + 7 类事件路由]
C2[ExamCache
LRU 10000 exam_id→metadata]
end
subgraph Storage["存储/总线"]
CH[(ClickHouse
5 宽表)]
KAFKA[(Kafka
edu-cdc.* + edu.insight.mastery.updated)]
IAM[iam gRPC :50052]
REDIS[(Redis
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.