docs: ai 协作文档体系重构与多 ai 仲裁结果落地
1.AI 协作文档体系重构(objections/worklines/contracts+matrix.md) 2.coord 仲裁文档(final-decisions/cross-review/final-rulings/orchestration) 3.各服务 01/02 文档补全 4.共享包初始化(shared-ts/shared-go/hooks/ui-components/ui-tokens) 5.Proto 契约补全 6.004 架构影响地图更新 7.端口分配表 8.设计规格文档
This commit is contained in:
979
services/data-ana/docs/02-architecture-design.md
Normal file
979
services/data-ana/docs/02-architecture-design.md
Normal file
@@ -0,0 +1,979 @@
|
||||
# 模块架构设计文档 — 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 理解确认书](./01-understanding.md)、[ai-allocation.md](../../../docs/architecture/ai-allocation.md)、[004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md)、[pending-features.md](../../../docs/architecture/roadmap/pending-features.md)
|
||||
> 审查状态:v1 已通过 coord 交叉审查(见 [coord-cross-review.md](../../../docs/architecture/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):
|
||||
|
||||
1. **契约先行**:proto 已定义(analytics.proto / events.proto / iam.proto),实现前不修改 proto,如需修改走 coord 流程(ai-allocation.md §9.4)
|
||||
2. **CQRS 读写分离**:data-ana 是纯读模型服务(无 MySQL 写),ClickHouse 宽表由 CDC 投影构建
|
||||
3. **事件驱动**:data-ana 消费 CDC(主通道)+ 领域事件(备通道,待 P4 后期评估);发布 `edu.insight.mastery.updated` 派生数据事件(豁免 Outbox,004 §12.2 + §15.3 #6 已仲裁)
|
||||
4. **gRPC 优先**:004 §4.1 + §4.2 明确 BFF → 业务服务走 gRPC,**P4 启用 data-ana gRPC server 端口 50055**(HTTP 保留作 Gateway 直连降级)
|
||||
5. **DataScope 过滤**:004 §5.3 DataScope 6 级在查询层注入 WHERE;iam `GetEffectiveDataScope` gRPC(004 §15.3 #5 已仲裁 P4 补全)
|
||||
6. **三支柱可观测**:structlog + prometheus-client + OpenTelemetry(已具备,需补业务指标 + gRPC server interceptor)
|
||||
7. **统一响应信封 ActionState**(004 §11.5 已仲裁 P0 整改):成功 `{success: true, data: T}` / 失败 `{success: false, error: {code, message, details?, traceId?}}` / 降级 `degraded` 作为 `details.degraded` 子字段
|
||||
8. **降级模式**:外部依赖(ClickHouse / Kafka / iam gRPC / Redis)不可用时返回骨架数据 + `details.degraded: true`
|
||||
9. **Python 规范**:pydantic-settings 配置 / Pydantic 模型校验 / async 优先 / 类型注解强制 / ruff 零错误
|
||||
10. **长远架构演进**:为 P5(ai 用量消费 / gRPC stream)、P6(CDC 水平扩展 / 容量规划 / 数据治理 / Service Mesh)做好铺垫,见 §14 / §19
|
||||
|
||||
---
|
||||
|
||||
## 1. 模块内部分层图
|
||||
|
||||
```mermaid
|
||||
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_level
|
||||
- `TrendPoint`:date + score
|
||||
- `Warning`: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`
|
||||
|
||||
```sql
|
||||
-- 学生学情宽表:每次成绩写入产生一行,按 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`
|
||||
|
||||
```sql
|
||||
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`(**新增**)
|
||||
|
||||
```sql
|
||||
-- 知识点掌握度历史快照:每次掌握度计算产生新版本,支持趋势查询
|
||||
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 服务写入**)
|
||||
|
||||
```sql
|
||||
-- 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 张宽表**)
|
||||
|
||||
```sql
|
||||
-- 学生考勤记录: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` |
|
||||
| `SubscribeMasteryUpdate` | `SubscribeMasteryUpdateRequest{student_id?, class_id?}` | `stream MasteryUpdateEvent` | `ANALYTICS_STUDENT_READ` |
|
||||
|
||||
> **proto 扩展提案**(待 coord 审议):上述新增 RPC 的 message 定义需在 `packages/shared-proto/proto/analytics.proto` 补充。`SubscribeMasteryUpdate` 为 server-streaming RPC,为 P5+ AI 个性化推荐预留实时推送通道。
|
||||
|
||||
**权限校验**:gRPC server interceptor 从 metadata 提取 `x-user-id` / `x-user-roles` / `x-data-scope`,调用 `AuthDepends` 等价逻辑。
|
||||
|
||||
### 4.3 ActionState 统一响应信封(004 §11.5 P0 整改)
|
||||
|
||||
所有 HTTP/gRPC 响应必须遵循 ActionState 信封:
|
||||
|
||||
```python
|
||||
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]):
|
||||
"""统一响应信封(004 §11.5).
|
||||
|
||||
成功:{success: true, data: T}
|
||||
失败:{success: false, error: {code, message, details?, trace_id?}}
|
||||
降级:success=true 但 error.details.degraded=true(保留功能但数据可能不完整)
|
||||
"""
|
||||
success: bool
|
||||
data: T | None = None
|
||||
error: ActionStateError | None = None
|
||||
|
||||
@classmethod
|
||||
def ok(cls, data: T, *, degraded: bool = False) -> "ActionState[T]":
|
||||
details = {"degraded": True} if degraded else None
|
||||
# 简化:degraded 作为 data 的元信息附加,复杂场景用 details
|
||||
return cls(success=True, data=data, error=None if not degraded else
|
||||
ActionStateError(code="DATA_ANA_DEGRADED", message="degraded mode",
|
||||
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))
|
||||
```
|
||||
|
||||
```python
|
||||
# 示例:班级成绩分析响应
|
||||
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}` 三字段平铺,违反 004 §11.5。实现阶段需重构为 `ActionState[T]` 泛型,`degraded` 移到 `error.details.degraded`。
|
||||
|
||||
## 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.events` topic,避免依赖 content MySQL binlog。
|
||||
|
||||
**幂等性**:
|
||||
|
||||
- CDC 事件:依赖 ClickHouse `ReplacingMergeTree(last_updated)` 引擎按 ORDER BY 去重 + ReplacingMergeTree 的 version 列
|
||||
- 领域事件(若消费):基于 `event_id` 去重(Redis SETNX `data_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)
|
||||
|
||||
```python
|
||||
# 权限点常量(与 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.py` `init_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.py` `init_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 扩展)
|
||||
|
||||
```python
|
||||
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 优雅关闭顺序
|
||||
|
||||
1. HTTP server stop accepting new requests(uvicorn graceful shutdown)
|
||||
2. gRPC server graceful stop(等待在途 RPC 完成,30s 超时)
|
||||
3. CDC consumer stop(等待在途消息处理完成,commit offset)
|
||||
4. Kafka producer flush + close(确保 mastery.updated 事件已投递)
|
||||
5. ClickHouse client close
|
||||
6. 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`(004 §15.3 #5 已仲裁 P4 补全) | 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`(004 §15.3 #6 已仲裁,004 §7.2 已登记) | AI 用量落库 |
|
||||
| 发布 | core-edu / msg | Kafka | `edu.insight.mastery.updated`(004 §12.2 + §15.3 #6 已仲裁:派生数据豁免 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) → DataScope` gRPC API(004 §15.3 #5 已仲裁)。若 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.usage` topic(004 §15.3 #6 已仲裁)
|
||||
|
||||
### 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:原"未决设计决策"已全部仲裁)
|
||||
|
||||
| # | 议题 | coord 仲裁结论 | 涉及文档 |
|
||||
| --- | ------------------------------------------------------------------------------------ | ----------------------------------------------------------------- | ----------------------- |
|
||||
| 1 | data-ana 发布 `edu.insight.mastery.updated` 用直接 producer(非 Outbox)是否合规 | ✅ 已仲裁:派生数据事件豁免 Outbox(004 §12.2 + §15.3 #6) | 004 §12.2 |
|
||||
| 2 | 新增 `edu.insight.ai.usage` topic + `AIUsageEvent` proto message | ✅ 已仲裁:coord 在 004 §7.2 登记 + events.proto 补 message | 004 §7.2 + events.proto |
|
||||
| 3 | iam 新增 `GetEffectiveDataScope` gRPC RPC | ✅ 已仲裁:P4 补全(004 §15.3 #5) | iam.proto |
|
||||
| 4 | data-ana 实现 gRPC server 决策 | ✅ 已仲裁:P4 启用(004 §4.2 gRPC 启用阶段矩阵) | 004 §4.2 |
|
||||
| 5 | ClickHouse DDL 管理位置(建议 `infra/clickhouse/ddl/`) | ⏳ 待 coord 建立 `infra/clickhouse/ddl/` 目录 + data-ana 提供内容 | infra/ |
|
||||
| 6 | 端口冲突检查:data-ana HTTP=3006 / gRPC=50055 | ✅ 无冲突(004 §1.2 端口分配) | 004 §1.2 |
|
||||
| 7 | 错误码前缀检查:`DATA_ANA_*` | ✅ 无冲突(004 §11.4 错误码前缀矩阵) | 004 §11.4 |
|
||||
| 8 | 黄金模板对齐:Python 服务无 NestJS 装饰器,权限校验用 FastAPI Depends 等价物是否认可 | ✅ 已仲裁:认可(004 §15.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 |
|
||||
|
||||
> 所有"未决设计决策"已消除,进入实现阶段无阻塞。剩余 3 项 ⏳ 为 v2 新提案,待 coord 审议但不阻塞 P4 主体实现(可先用现有 RPC + HTTP 端点兜底)。
|
||||
|
||||
---
|
||||
|
||||
## 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 触发时机
|
||||
|
||||
1. CDC `core_edu_grades` 事件到达 → 异步计算 → 写 `mastery_snapshot` + `student_dashboard_view` → 发布 `MasteryUpdated`
|
||||
2. 定时任务(每日 02:00)→ 对所有知识点重算遗忘曲线衰减 → 更新 `mastery_snapshot`(不发布事件,避免风暴)
|
||||
3. 手动触发(管理员 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 连接
|
||||
|
||||
```python
|
||||
# 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 配置
|
||||
|
||||
```python
|
||||
@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 新增)
|
||||
|
||||
```dockerfile
|
||||
# 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 curl -f 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)
|
||||
|
||||
```python
|
||||
# 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. 服务审计表 v2(黄金模板对齐)
|
||||
|
||||
| 维度 | 状态 | 说明 |
|
||||
| ------------- | ---- | ----------------------------------------------------------------------- |
|
||||
| 契约(proto) | ✅ | analytics.proto 已定义 3 RPC,v2 提案扩展 7 RPC |
|
||||
| 路由 | ✅ | HTTP 13 端点 + gRPC 10 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) |
|
||||
| 响应信封 | ⚠️ | 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 重新分配 + 004 §1.2/§4.2/§7.2/§11.5/§12.2/§15.3 + ai-allocation §5 设计重点 + pending-features P4 退出标准
|
||||
**v2 审核结论**: 16 项遗漏已全部补强(3 项 P0 + 5 项 P1 + 8 项 P2),文档进入实现阶段无阻塞
|
||||
Reference in New Issue
Block a user