Files
Edu/services/data-ana/README.md
SpecialX ca3780aa24 feat(data-ana): 完整实现 data-ana 数据分析服务
包含 CDC consumer、analytics/mastery/warning service、grpc server、repository、ClickHouse DDL 等
2026-07-10 19:09:27 +08:00

387 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# data-ana 数据分析服务
> 版本1.0P4 完整版)
> 阶段D6 智能洞察领域仲裁已对齐v2 实现版)
> 端口HTTP 3006Gateway 直连降级)+ 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 消费 → ClickHouseat-least-once + 手动 commit + ReplacingMergeTree 去重)
- **掌握度算法**WEIGHTED_MOVING_AVGw_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/ALLiam 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 角色 fallbackteacher→CLASSstudent→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 契约
### AnalyticsService12 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 写入成功后 commitat-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}`
- TTL25 小时(跨日保证)
## 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 已知问题速查