feat(data-ana): docker 本地测试通过 + gRPC 修复 + nextstep 上下游依赖文档

This commit is contained in:
SpecialX
2026-07-13 23:57:48 +08:00
parent b745443b6d
commit 5b06bdbc52
14 changed files with 2061 additions and 42 deletions

View File

@@ -319,47 +319,53 @@
### 2.6 data-anaPython/FastAPIP4
| 场景 | 技术/规则 |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 学情诊断 | ClickHouse 宽表查询5s 内返回 |
| CDC 链路 | Debezium 监听 MySQL binlog → Kafka`edu-cdc.next_edu_cloud.<table>`)→ DataAna 消费写 ClickHouse |
| CDC 延迟监控 | Debezium 暴露 lag metrics超阈值告警 |
| 双轨读策略 | 实时查 MySQL 主库(刚提交的成绩),聚合查 CH 宽表(延迟 1-5s 可接受) |
| 幂等消费 | 所有事件消费者必须幂等(基于 event_id 去重) |
| CDC 消费者实现 | `cdc_consumer.py` 用 aiokafka AIOKafkaConsumerlifespan 启动 asyncio.create_task 后台运行 |
| ClickHouse 写入 | `clickhouse_client.upsert_student_dashboard()` 用 client.insert() 写宽表client 为 None 时降级返回 False |
| Debezium 事件解析 | `before/after/source/op/ts_ms` 五字段op=r(快照)/c(新增)/u(更新)/d(删除) |
| 多表关联缓存 | 内存 ExamCache 缓存 exam_id→class_id 映射(来自 core_edu_exams CDC 事件grades 事件触发时查缓存填充宽表 class_id |
| Consumer offset 重置 | `kafka-consumer-groups --reset-offsets --to-earliest --execute` 需先停消费者让 group 处于 Empty 状态 |
| 5 张宽表对齐 | student_dashboard_view / student_errors / mastery_snapshot / attendance_logs / ai_usage_logai-allocation §5 强制) |
| ActionState 信封对齐 | Python 服务 `{success, data, degraded}` 必须改 `{success, data, error:{code, message, details?, traceId?}}`degraded 作 details 子字段004 §11.5 P0 |
| gRPC server 启用 | P4 启用 data-ana gRPC server 端口 50055004 §4.2 矩阵HTTP 3006 保留作 Gateway 直连降级 |
| DataScope 6 级过滤 | Repository 层注入 WHERESELF/CLASS/GRADE/SCHOOL/DISTRICT/ALLiam `GetEffectiveDataScope` gRPC P4 补全004 §15.3 #5 |
| Outbox 豁免 | 派生数据事件MasteryUpdated / WarningTriggered / AIUsageRecorded豁免 Outbox允许直接 Kafka producer004 §12.2 + §15.3 #6 |
| 掌握度算法 | WEIGHTED_MOVING_AVGw_i=0.6^i+ FORGETTING_CURVE半衰期 30 天)取 max近 5 次不足降级 SIMPLE_AVG |
| 预警阈值设计 | LOW_MASTERY(<0.4) / CRITICAL_LOW(<0.2) / SCORE_DROP(下降≥20%) / ABSENT_FREQUENT(≥3次/周) / TREND_DECLINE(连续3次降) |
| 预警去重 | 同一 (target_id, warning_type) 24h 内只触发一次Redis 位图 `data_ana:warning:{target_id}:{type}:{date}` TTL 25h |
| CDC 水平扩展 | P4 单实例 + 内存 ExamCache LRU → P6 多实例 + Redis ExamCache + partition 扩容 + HPA |
| CDC 手动 commit | v2 改进:`enable_auto_commit=False` 手动 commitat-least-oncecommit 前确保 ClickHouse 写入成功 |
| ClickHouse 容量规划 | 5 宽表年增量 ~2.3GB5000 学生×20 次/月单节点可承载P6 按 class_id hash 分片 + ReplicatedMergeTree |
| ClickHouse FINAL | ReplacingMergeTree 查询必须加 `FINAL` 或用 `argMax` 聚合否则读到重复版本clickhouse_repository.py 已实现 P0 整改) |
| 缓存键命名 | `data_ana:datascope:{user_id}` 5min / `data_ana:exam:{exam_id}` 30day / `data_ana:dedup:{event_id}` 7day |
| 4 端 Dashboard | teacher/student/parent/admin 各自 Dashboard API + 权限点 ANALYTICS_*_DASHBOARDv2 新增 4 端权限) |
| SubscribeMasteryUpdate | gRPC server-streaming RPC 为 P5+ AI 个性化推荐预留实时推送通道analytics.proto v2 提案) |
| GDPR 数据删除 | ClickHouse 无 UPDATE/DELETE用 `ALTER TABLE ... DELETE WHERE student_id=?` 异步 mutationP6 实现) |
| 字段脱敏 | student_id 在日志中 hash 化structlog processorPII 不入日志 |
| 事件版本化 | 事件 payload 加 `schema_version: "v1"` 字段,破坏性变更发 v2consumer 双消费 6 个月过渡 |
| API 版本化 | HTTP `/analytics/v1/*` + gRPC `next_edu_cloud.analytics.v1`,破坏性变更新增 v2旧版保留 6 个月 |
| Projection 演进 | P4 CDC 直连 → P4 后期双消费CDC + 领域事件)→ P6 ClickHouse MaterializedView 自动聚合 |
| 测试策略 | pytest + TestcontainersClickHouse/Kafka 容器化)+ 单元 ≥80% + 集成 ≥60% + 契约 pact-python + E2E |
| Dockerfile 多阶段 | builderuv sync --frozen+ runtimepython:3.12-slim < 200MB+ HEALTHCHECK 集成 |
| 配置项完整清单 | pydantic-settings 30+ 配置项env_prefix="DATA_ANA_",含 ClickHouse/Kafka/iam gRPC/Redis/OTel/算法参数/预警阈值 |
| 降级模式 4 场景 | ClickHouse/Kafka/Redis/iam gRPC 不可用时返回骨架 + `error.details.degraded=true`4 错误码 DATA_ANA_*_UNAVAILABLE |
| Python gRPC 实现 | grpc.aio + betterproto + ServerInterceptor 透传 W3C trace context与 HTTP 共享同一 Application Service |
| structlog API | 24.x 用 `make_filtering_bound_logger(level)`,旧版 `make_filtering_logger` 已废弃 |
| ruff per-file-ignores | gRPC servicer 方法必须 PascalCaseN802 忽略)/ FastAPI Depends 在参数默认值B008 忽略pyproject.toml 配 per-file-ignores |
| Mock 数据脚本 | `scripts/data-ana-mock-data.sql` 提供 5 宽表演示数据20 学生×2 考试×3 知识点),用 `now()-INTERVAL` 避免硬编码日期 |
| Python 3.12+ 现代化 | ruff UP042 用 StrEnum 替代 `str, Enum` / UP046 用 PEP 695 类型参数 `class Foo[T]` 替代 `Generic[T]` |
| 场景 | 技术/规则 |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 学情诊断 | ClickHouse 宽表查询5s 内返回 |
| CDC 链路 | Debezium 监听 MySQL binlog → Kafka`edu-cdc.next_edu_cloud.<table>`)→ DataAna 消费写 ClickHouse |
| CDC 延迟监控 | Debezium 暴露 lag metrics超阈值告警 |
| 双轨读策略 | 实时查 MySQL 主库(刚提交的成绩),聚合查 CH 宽表(延迟 1-5s 可接受) |
| 幂等消费 | 所有事件消费者必须幂等(基于 event_id 去重) |
| CDC 消费者实现 | `cdc_consumer.py` 用 aiokafka AIOKafkaConsumerlifespan 启动 asyncio.create_task 后台运行 |
| ClickHouse 写入 | `clickhouse_client.upsert_student_dashboard()` 用 client.insert() 写宽表client 为 None 时降级返回 False |
| Debezium 事件解析 | `before/after/source/op/ts_ms` 五字段op=r(快照)/c(新增)/u(更新)/d(删除) |
| 多表关联缓存 | 内存 ExamCache 缓存 exam_id→class_id 映射(来自 core_edu_exams CDC 事件grades 事件触发时查缓存填充宽表 class_id |
| Consumer offset 重置 | `kafka-consumer-groups --reset-offsets --to-earliest --execute` 需先停消费者让 group 处于 Empty 状态 |
| 5 张宽表对齐 | student_dashboard_view / student_errors / mastery_snapshot / attendance_logs / ai_usage_logai-allocation §5 强制) |
| ActionState 信封对齐 | Python 服务 `{success, data, degraded}` 必须改 `{success, data, error:{code, message, details?, traceId?}}`degraded 作 details 子字段004 §11.5 P0 |
| gRPC server 启用 | P4 启用 data-ana gRPC server 端口 50055004 §4.2 矩阵HTTP 3006 保留作 Gateway 直连降级 |
| DataScope 6 级过滤 | Repository 层注入 WHERESELF/CLASS/GRADE/SCHOOL/DISTRICT/ALLiam `GetEffectiveDataScope` gRPC P4 补全004 §15.3 #5 |
| Outbox 豁免 | 派生数据事件MasteryUpdated / WarningTriggered / AIUsageRecorded豁免 Outbox允许直接 Kafka producer004 §12.2 + §15.3 #6 |
| 掌握度算法 | WEIGHTED_MOVING_AVGw_i=0.6^i+ FORGETTING_CURVE半衰期 30 天)取 max近 5 次不足降级 SIMPLE_AVG |
| 预警阈值设计 | LOW_MASTERY(<0.4) / CRITICAL_LOW(<0.2) / SCORE_DROP(下降≥20%) / ABSENT_FREQUENT(≥3次/周) / TREND_DECLINE(连续3次降) |
| 预警去重 | 同一 (target_id, warning_type) 24h 内只触发一次Redis 位图 `data_ana:warning:{target_id}:{type}:{date}` TTL 25h |
| CDC 水平扩展 | P4 单实例 + 内存 ExamCache LRU → P6 多实例 + Redis ExamCache + partition 扩容 + HPA |
| CDC 手动 commit | v2 改进:`enable_auto_commit=False` 手动 commitat-least-oncecommit 前确保 ClickHouse 写入成功 |
| ClickHouse 容量规划 | 5 宽表年增量 ~2.3GB5000 学生×20 次/月单节点可承载P6 按 class_id hash 分片 + ReplicatedMergeTree |
| ClickHouse FINAL | ReplacingMergeTree 查询必须加 `FINAL` 或用 `argMax` 聚合否则读到重复版本clickhouse_repository.py 已实现 P0 整改) |
| 缓存键命名 | `data_ana:datascope:{user_id}` 5min / `data_ana:exam:{exam_id}` 30day / `data_ana:dedup:{event_id}` 7day |
| 4 端 Dashboard | teacher/student/parent/admin 各自 Dashboard API + 权限点 ANALYTICS_*_DASHBOARDv2 新增 4 端权限) |
| SubscribeMasteryUpdate | gRPC server-streaming RPC 为 P5+ AI 个性化推荐预留实时推送通道analytics.proto v2 提案) |
| GDPR 数据删除 | ClickHouse 无 UPDATE/DELETE用 `ALTER TABLE ... DELETE WHERE student_id=?` 异步 mutationP6 实现) |
| 字段脱敏 | student_id 在日志中 hash 化structlog processorPII 不入日志 |
| 事件版本化 | 事件 payload 加 `schema_version: "v1"` 字段,破坏性变更发 v2consumer 双消费 6 个月过渡 |
| API 版本化 | HTTP `/analytics/v1/*` + gRPC `next_edu_cloud.analytics.v1`,破坏性变更新增 v2旧版保留 6 个月 |
| Projection 演进 | P4 CDC 直连 → P4 后期双消费CDC + 领域事件)→ P6 ClickHouse MaterializedView 自动聚合 |
| 测试策略 | pytest + TestcontainersClickHouse/Kafka 容器化)+ 单元 ≥80% + 集成 ≥60% + 契约 pact-python + E2E |
| Dockerfile 多阶段 | builderuv sync --frozen+ runtimepython:3.12-slim < 200MB+ HEALTHCHECK 集成 |
| 配置项完整清单 | pydantic-settings 30+ 配置项env_prefix="DATA_ANA_",含 ClickHouse/Kafka/iam gRPC/Redis/OTel/算法参数/预警阈值 |
| 降级模式 4 场景 | ClickHouse/Kafka/Redis/iam gRPC 不可用时返回骨架 + `error.details.degraded=true`4 错误码 DATA_ANA_*_UNAVAILABLE |
| Python gRPC 实现 | grpc.aio + betterproto + ServerInterceptor 透传 W3C trace context与 HTTP 共享同一 Application Service |
| structlog API | 24.x 用 `make_filtering_bound_logger(level)`,旧版 `make_filtering_logger` 已废弃 |
| ruff per-file-ignores | gRPC servicer 方法必须 PascalCaseN802 忽略)/ FastAPI Depends 在参数默认值B008 忽略pyproject.toml 配 per-file-ignores |
| Mock 数据脚本 | `scripts/data-ana-mock-data.sql` 提供 5 宽表演示数据20 学生×2 考试×3 知识点),用 `now()-INTERVAL` 避免硬编码日期 |
| Python 3.12+ 现代化 | ruff UP042 用 StrEnum 替代 `str, Enum` / UP046 用 PEP 695 类型参数 `class Foo[T]` 替代 `Generic[T]` |
| ClickHouse argMax 别名冲突 | `argMax(col, calculated_at)` 与同 SELECT 的 `max(calculated_at) AS calculated_at` 冲突ILLEGAL_AGGREGATION别名改为 `latest_calculated_at` |
| Python proto 生成 | `grpc_tools.protoc` 生成 `_pb2_grpc.py` 用 `import xxx_pb2`flat放入 package 需改为 `from generated_proto import xxx_pb2` |
| Dockerfile PYTHONPATH | Python 服务 `src/generated_proto/` 作为包导入时Dockerfile 需 `ENV PYTHONPATH="/app/src"`,否则 `ModuleNotFoundError: generated_proto` |
| OTel 日志噪声 | OTel collector 不可达时刷屏 `Transient error ... retrying`,设 `OTEL_TRACES_EXPORTER=none` + `OTEL_METRICS_EXPORTER=none` + `OTEL_LOGS_EXPORTER=none` 静默 |
| Docker Hub 不可达 | Dockerfile base image 用标准名 `python:3.12-slim`,本地 `docker pull docker.m.daocloud.io/library/python:3.12-slim` 后 `docker tag` 为标准名 |
| ruff per-file-ignores | 自动生成的 proto 代码(`src/generated_proto/*.py`)在 `pyproject.toml` 配 `per-file-ignores = ["ALL"]`,避免 lint-staged 传单文件时 `extend-exclude` 无效 |
### 2.7 messagingTS/NestJSP5