feat: add analytics date dimension bindings
Some checks failed
CI / verify (push) Has been cancelled

- lock profile task tracking scope to user

- add analytics date dimension module support and tests

- regenerate SpacetimeDB Rust bindings with private APIs
This commit is contained in:
2026-05-04 13:41:22 +08:00
committed by 历冰郁-hermes版
parent 9f3e34e81a
commit 5c7c039e52
253 changed files with 14783 additions and 1462 deletions

View File

@@ -0,0 +1,724 @@
# 埋点系统新增周、月、季、年维度映射表计划
## 目标
在 Genarrative 的埋点/统计系统中新增“周、月、季、年”维度映射表,让后续统计查询可以按不同时间粒度稳定聚合,而不是只依赖运行时临时计算日期范围。
本计划只做设计与落地步骤,不直接修改业务代码。
## 当前上下文与初步发现
1. 当前仓库根目录为 `/home/dsk/workspace/Genarrative`
2. 远端更新后已定位到当前真实埋点/任务系统:
- 原始埋点表:`tracking_event`
- 日聚合投影表:`tracking_daily_stat`
- 任务配置表:`profile_task_config`
- 任务进度表:`profile_task_progress`
- 领奖记录表:`profile_task_reward_claim`
3. 相关文件:
- `server-rs/crates/spacetime-module/src/runtime/profile.rs`
- `server-rs/crates/module-runtime/src/domain.rs`
- `server-rs/crates/module-runtime/src/application.rs`
- `server-rs/crates/api-server/src/runtime_profile.rs`
- `apps/admin-web/src/pages/AdminTaskConfigPage.tsx`
- `apps/admin-web/src/api/adminApiTypes.ts`
- `apps/admin-web/src/config/trackingEventDefinitions.ts`
- `docs/technical/PROFILE_TASK_AND_TRACKING_SYSTEM_2026-05-03.md`
- `docs/tracking/TRACKING_QUERY_PLAYBOOK_2026-05-03.md`
4. 当前 `tracking_event` 是明细表,`tracking_daily_stat` 是统一日汇总表,不按范围拆表;它通过 `event_key + scope_kind + scope_id + day_key` 区分不同聚合桶。
5. 当前任务系统是“个人任务系统”,首版任务配置均面向用户维度;后台暴露“埋点范围”选择会导致运营误配。
6. 已决定采用任务配置方案 B
- 后台任务配置不再让运营手动选择埋点范围。
- 后端个人任务配置统一限制为 `RuntimeTrackingScopeKind::User`
- 若 API 收到非 `user``scopeKind`,应拒绝或兼容忽略但最终落库必须为 `User`;推荐直接拒绝并返回清晰错误。
7. 已发现一个需要纳入本计划修复的语义问题:`profile_task_tracking_scope_id``RuntimeTrackingScopeKind::Work` 当前返回 `user_id`,这不符合 work 维度语义。即使个人任务暂不支持 work也应避免错误映射继续存在。
8. 当前日期桶使用北京时间自然日:`day_key = floor((occurred_at_micros + 8h) / 1d)`;周/月/季/年映射表应优先沿用这一业务日口径,除非产品另行确认。
9. 如果新增正式后端表,需要同步:
- 表定义
- reducer/procedure
- migration.rs
- 生成绑定
- spacetime-client facade
- shared-contracts / API DTO如有接口暴露
## 关键假设
在未定位现有埋点模块前,先按以下假设规划:
1. 当前已有某种事件明细表或统计事实表,例如:
- `telemetry_event`
- `analytics_event`
- `metric_event`
- `narrative_telemetry`
- 或类似命名
2. 新增的“映射表”用于把具体日期或事件时间映射到时间维度 bucket。
3. 映射维度包括:
-week
-month
-quarter
-year
4. 已明确选择“一张通用日期维度映射表”方案:`analytics_date_dimension`
5. 统计口径需要明确:
- 周从周一还是周日开始
- 是否使用 ISO week
- 季度是自然季度还是财务季度
- 时区使用 UTC 还是业务本地时区
## 推荐设计方向
### 方案 A单一时间维度映射表推荐
新增一张日历维度表,每一行对应一个自然日,并包含它归属的周、月、季、年。
表概念:
```text
analytics_date_dimension
```
建议字段:
```text
date_key string 例如 2026-05-04
calendar_date string 真实日期,按 YYYY-MM-DD 存储
weekday u8 1-7 或 0-6需要统一约定
iso_week_key string 例如 2026-W19
week_start_date_key string 例如 2026-05-04
week_end_date_key string 例如 2026-05-10
month_key string 例如 2026-05
month_start_date_key string
month_end_date_key string
quarter_key string 例如 2026-Q2
year_key string 例如 2026
created_at timestamp/string
updated_at timestamp/string
```
优点:
- 一张表即可支持日、周、月、季、年映射。
- 便于后续新增半月、财年、节假日、自然周等维度。
- 查询逻辑简单:事件日期 join/date_key 映射到目标粒度。
- 数据量很小,按 20 年也只有约 7300 行。
缺点:
- 需要在事件时间写入或统计查询时把 timestamp 归一为 date_key。
- 如果要支持多时区,可能需要增加 timezone 字段或多套 calendar。
### 方案 B四张独立映射表
分别新增:
```text
analytics_week_dimension
analytics_month_dimension
analytics_quarter_dimension
analytics_year_dimension
```
优点:
- 每个粒度表结构更纯粹。
- 查询时可以直接针对目标粒度表。
缺点:
- 表更多,维护复杂。
- 日期归属关系仍然需要额外处理。
- 容易出现周/月/季/年口径漂移。
### 最终选择
本计划采用方案 A单一 `analytics_date_dimension` 日期维表,而不是四张独立映射表。
如业务未来明确要求“周、月、季、年各自有独立映射表”,也应优先在日期维表基础上派生视图或物化派生表,而不是一开始拆成四张重复表。
## 后端设计建议
### 1. 明确埋点领域归属
先定位现有埋点模块。如果没有独立模块,建议新增或归入:
```text
server-rs/crates/module-analytics/
```
或如果当前项目已有 telemetry 命名,则保持已有命名,例如:
```text
server-rs/crates/module-telemetry/
```
领域层职责:
- 时间粒度定义
- date_key/week_key/month_key/quarter_key/year_key 生成规则
- 时间维度校验
- 事件聚合查询输入的纯规则
不应包含:
- SpacetimeDB 表读写
- Axum handler
- HTTP response
### 2. SpacetimeDB 表设计
`spacetime-module` 中新增时间维度表。
建议表名:
```text
analytics_date_dimension
```
建议主键:
```text
date_key
```
建议索引:
```text
iso_week_key
month_key
quarter_key
year_key
```
如果 SpacetimeDB 表定义已有统一命名规范,应按现有规范命名。
### 3. 初始化/补全 reducer
新增 reducer 或内部 procedure用于生成指定日期范围内的维度数据。
建议能力:
```text
seed_analytics_date_dimensions(start_date, end_date)
ensure_analytics_date_dimension_for_date(date_key)
ensure_analytics_date_dimensions_for_range(start_date, end_date)
```
设计原则:
- 可幂等执行。
- 已存在 date_key 时不重复插入。
- 支持一次补一段日期。
- 避免一次补太大范围导致事务过重。
- 生产环境建议按年份或月份分批。
### 4. 事件表和映射表关系
如果事件表目前只有 timestamp建议新增或计算出
```text
event_date_key
```
可选策略:
1. 写入事件时同步写 `event_date_key`
2. 查询统计时从 timestamp 临时计算 date_key。
3. 后台迁移为历史事件补 `event_date_key`
推荐:
- 新事件写入时保存 `event_date_key`
- 历史事件通过批量迁移 reducer 分批补齐。
### 5. 聚合查询设计
支持按粒度查询时API 或 facade 可以接收:
```text
granularity = day | week | month | quarter | year
start_date
end_date
metric/event_name
filters
```
内部根据粒度选择 bucket key
```text
day -> date_key
week -> iso_week_key 或 week_key
month -> month_key
quarter -> quarter_key
year -> year_key
```
返回结构建议统一:
```text
bucket_key
bucket_label
bucket_start_date
bucket_end_date
value
```
## 可能涉及的文件
由于当前尚未定位明确埋点模块,以下是预计文件范围。
### 必查文件/目录
```text
./Genarrative/server-rs/crates/
./Genarrative/server-rs/crates/spacetime-module/src/
./Genarrative/server-rs/crates/spacetime-client/src/
./Genarrative/server-rs/crates/shared-contracts/src/
./Genarrative/server-rs/crates/api-server/src/
./Genarrative/packages/shared/src/contracts/
./Genarrative/src/services/
```
### 可能新增文件
如果采用 analytics 命名:
```text
server-rs/crates/module-analytics/src/domain.rs
server-rs/crates/module-analytics/src/commands.rs
server-rs/crates/module-analytics/src/application.rs
server-rs/crates/module-analytics/src/errors.rs
server-rs/crates/module-analytics/src/events.rs
server-rs/crates/shared-contracts/src/analytics.rs
server-rs/crates/spacetime-client/src/analytics.rs
server-rs/crates/api-server/src/analytics.rs
packages/shared/src/contracts/analytics.ts
```
如果只是新增 SpacetimeDB 映射表且暂不暴露 API则可能只需
```text
server-rs/crates/spacetime-module/src/**
server-rs/crates/spacetime-module/src/migration.rs
server-rs/crates/spacetime-client/src/** # 如果查询会被 api-server 使用
```
## 详细实施步骤
### Step 1复核现有埋点系统与任务配置链路
当前已定位真实链路,实施前再做一次只读复核,确认远端最新代码没有继续变化。
已知核心表:
```text
tracking_event # 原始埋点明细
tracking_daily_stat # 日聚合投影
profile_task_config # 个人任务配置
profile_task_progress # 个人任务进度
profile_task_reward_claim # 领奖记录
```
已知核心文件:
```text
server-rs/crates/spacetime-module/src/runtime/profile.rs
server-rs/crates/module-runtime/src/domain.rs
server-rs/crates/module-runtime/src/application.rs
server-rs/crates/api-server/src/runtime_profile.rs
apps/admin-web/src/pages/AdminTaskConfigPage.tsx
apps/admin-web/src/api/adminApiTypes.ts
apps/admin-web/src/config/trackingEventDefinitions.ts
docs/technical/PROFILE_TASK_AND_TRACKING_SYSTEM_2026-05-03.md
docs/tracking/TRACKING_QUERY_PLAYBOOK_2026-05-03.md
```
重点确认:
1. `tracking_event` 是否仍包含 `event_key/scope_kind/scope_id/day_key/user_id/occurred_at`
2. `tracking_daily_stat` 是否仍按 `event_key + scope_kind + scope_id + day_key` 生成 `stat_id`
3. `profile_task_config` 是否仍包含 `scope_kind``sort_order`
4. 后台 `AdminTaskConfigPage` 是否仍暴露“埋点范围”下拉。
5. `profile_task_tracking_scope_id``Work => user_id` 的错误映射是否仍存在。
### Step 1.5:先收紧个人任务配置的埋点范围,采用方案 B
在做周/月/季/年维度映射前,先修正个人任务配置边界,避免后续在错误配置模型上继续扩展。
目标行为:
```text
个人任务配置只支持用户维度埋点。
后台页面不再展示“埋点范围”。
后端不允许 profile_task_config 被写入 site/work/module 维度。
```
建议实现:
1. 前端隐藏 `AdminTaskConfigPage` 的“埋点范围”选择。
- 文件:`apps/admin-web/src/pages/AdminTaskConfigPage.tsx`
- 移除或隐藏:`scopeKinds` 下拉 UI。
- 保存请求仍可兼容传 `scopeKind: 'user'`,避免一次性改动 API contract。
2. 后端 upsert 校验 `scopeKind` 必须为 `RuntimeTrackingScopeKind::User`
- 文件:`server-rs/crates/api-server/src/runtime_profile.rs`
- 或更底层:`server-rs/crates/module-runtime/src/domain.rs` / `server-rs/crates/spacetime-module/src/runtime/profile.rs` 的输入构造函数。
- 推荐在领域输入构造处兜底校验API 层返回清晰错误。
3. 若暂不改 API DTO则保持字段存在但限定值只能是 `user`
- 文件:`apps/admin-web/src/api/adminApiTypes.ts`
- `AdminUpsertProfileTaskConfigRequest.scopeKind` 可保留,前端固定传 `user`
4. 更新后台埋点定义注册表的语义:
- 文件:`apps/admin-web/src/config/trackingEventDefinitions.ts`
- 当前每个 event definition 包含 `scopeKind`,如果个人任务统一 `user`,可以保留为只读内部默认值;但不要让运营在页面改。
5. 更新技术文档:
- `docs/technical/PROFILE_TASK_AND_TRACKING_SYSTEM_2026-05-03.md`
- 明确“个人任务首版只支持用户维度埋点,后台不开放 scope_kind 配置”。
验收:
```text
后台任务配置页不再出现“埋点范围”选择。
保存 daily_login 后落库 scope_kind 仍为 User。
直接调用后台 upsert 接口传 site/work/module 时被拒绝,或最终不会落库为非 User推荐拒绝。
```
### Step 1.6:修复 Work 范围错误返回 user_id 的语义问题
当前函数:
```text
server-rs/crates/spacetime-module/src/runtime/profile.rs
profile_task_tracking_scope_id(user_id, config)
```
当前问题:
```rust
RuntimeTrackingScopeKind::Work => user_id.to_string()
```
这会把 work 维度错误映射为用户 ID。虽然个人任务将限制为 User但保留这个分支会误导后续扩展。
推荐修复策略:
1. 对个人任务进度计算来说,`Work` 不应进入该函数。
2.`profile_task_tracking_scope_id` 改为返回 `Result<String, String>``Option<String>`
3. 对不支持的范围返回错误,而不是伪造 scope_id
```text
Site -> "site",如果个人任务仍不允许 site则上游先拒绝
Module -> "profile",如果个人任务仍不允许 module则上游先拒绝
User -> user_id
Work -> error: personal task progress does not support work scope without work_id
```
更严格的推荐:
```text
个人任务链路只接受 User。
Work/Site/Module 在 profile_task_progress_count 前就被拒绝。
profile_task_tracking_scope_id 只保留 User 分支,或者非 User 返回错误。
```
需要同步调整调用点:
```text
profile_task_progress_count
refresh_profile_task_progress
build_profile_task_center_snapshot
claim_profile_task_reward
```
避免因为函数返回 `Result` 后调用链未处理错误。
验收:
```text
不存在 Work => user_id 的映射。
个人任务配置非 User 时不会静默算出错误进度。
相关测试覆盖User 正常Work/Site/Module 被拒绝。
```
### Step 2确定时间口径
必须先确认:
1. 周维度是否使用 ISO week。
2. 周开始日是周一还是周日。
3. 月/季/年是否按自然日历。
4. 统计时区是 UTC、服务器时区还是用户本地时区。
5. 跨年周如何命名,例如 `2025-W01` 可能开始于 2024 年末。
推荐默认:
```text
时区UTC除非产品明确要求中国时区
ISO week周一开始
月:自然月
季:自然季度
年:自然年
```
如果业务面向国内用户,建议考虑:
```text
时区Asia/Shanghai
周:周一开始
```
### Step 3设计 date dimension 表
设计字段和 key 格式,写入技术文档。
建议 key 格式:
```text
date_key: 2026-05-04
week_key: 2026-W19
month_key: 2026-05
quarter_key: 2026-Q2
year_key: 2026
```
注意:
- `week_key` 建议使用 ISO week-year不一定等于 calendar year。
- `quarter_key` 使用 calendar year。
### Step 4新增领域纯函数
在领域层或 shared-kernel 中实现纯函数:
```text
resolve_date_dimension(date, timezone) -> AnalyticsDateDimension
resolve_bucket_key(date_dimension, granularity) -> String
resolve_bucket_range(bucket_key, granularity) -> start/end date
```
要求:
- 有单元测试。
- 覆盖跨年周。
- 覆盖闰年 2 月。
- 覆盖季度边界。
### Step 5新增 SpacetimeDB 表
`spacetime-module` 中新增表。
遵守约束:
- 新增表通常安全。
- 不修改已有表字段。
- 如果必须给已有事件表加 `event_date_key`,必须加在表定义末尾并提供 default。
- 若要补历史数据,使用 reducer 分批迁移。
### Step 6新增 seed/ensure reducer
新增幂等 reducer
```text
seed_analytics_date_dimensions(start_date, end_date)
ensure_analytics_date_dimension_for_date(date_key)
```
验证点:
- 重复执行不会重复插入。
- 日期范围非法时返回稳定错误。
- 单次范围过大时拒绝或分页。
### Step 7接入事件写入链路
如果现有事件写入链路存在,新增:
```text
event_date_key
```
策略:
- 新事件写入时同步计算并保存。
- 写入前确保对应 date dimension 存在。
- 历史事件通过迁移 reducer 补齐。
如果暂不改事件表,也可以在查询阶段临时映射,但性能和一致性较差。
### Step 8接入聚合查询
如已有统计接口,扩展请求参数:
```text
granularity: day | week | month | quarter | year
```
查询逻辑改为:
```text
事件/事实表
→ event_date_key
→ analytics_date_dimension
→ 取对应 bucket key
→ group by bucket key
```
返回 bucket 时包含:
```text
bucket_key
bucket_start_date
bucket_end_date
value
```
### Step 9补 shared contracts 和前端 contracts
如果有 API 暴露,需要补:
```text
server-rs/crates/shared-contracts/src/analytics.rs
packages/shared/src/contracts/analytics.ts
```
建议 DTO
```text
AnalyticsGranularity = day | week | month | quarter | year
AnalyticsBucketMetric
AnalyticsMetricQueryRequest
AnalyticsMetricQueryResponse
```
### Step 10补测试
测试范围:
1. 领域日期映射测试
2. SpacetimeDB reducer 幂等测试
3. API 查询维度测试
4. 历史事件迁移测试,如涉及
5. 跨边界日期测试
6. 个人任务配置 scope 限制测试
7. `Work => user_id` 错误映射回归测试
重点用例:
```text
2024-02-29 闰年
2025-12-29 ISO week 可能属于 2026-W01
2026-01-01 跨年周
2026-03-31 Q1 结束
2026-04-01 Q2 开始
2026-12-31 年末
```
任务配置重点用例:
```text
admin upsert daily_login + scopeKind=user -> 成功
admin upsert daily_login + scopeKind=site -> 失败,错误信息说明个人任务仅支持 user
admin upsert daily_login + scopeKind=module -> 失败
admin upsert daily_login + scopeKind=work -> 失败
任务中心读取 daily_login -> 按 User + 当前 user_id 查询进度
代码中不存在 Work => user_id 的静默映射
```
## 测试与验证命令
具体命令需在定位模块后确认。初步建议:
```text
npm run typecheck
npm test
```
后端如涉及 Rust
```text
cargo test -p module-analytics
cargo test -p spacetime-module
cargo test -p api-server
```
涉及 API smoke
```text
npm run api-server
```
然后验证:
```text
GET /healthz
```
涉及 SpacetimeDB schema
- 需要生成绑定。
- 需要确认 migration.rs 对齐。
- 需要确认 publish 不触发不安全 schema 变更。
## 风险与权衡
### 风险 1个人任务 scope_kind 被误配置导致进度异常
当前个人任务系统本质上按用户维度计算进度。如果允许运营配置 `site/work/module`,可能导致任务进度查错 `tracking_daily_stat` 聚合桶,出现任务永远不可领取或错误可领取。
缓解:
```text
采用方案 B后台隐藏埋点范围后端限制个人任务配置只能写入 User。
```
### 风险 2Work 维度缺少 work_id上游却静默用 user_id 代替
当前 `profile_task_tracking_scope_id``Work => user_id` 是错误语义。若后续扩展作品任务,会把作品维度统计错误映射到用户维度。
缓解:
```text
移除 Work => user_id 映射;非 User 的个人任务配置应被拒绝。未来做作品任务时新增明确 work_id 来源和任务类型。
```
### 风险 3时区口径影响统计结果
周/月/季/年映射对时区敏感。当前日桶使用北京时间自然日:`floor((occurred_at_micros + 8h) / 1d)`。新增映射表应明确沿用北京时间业务日,还是切换为 UTC/用户本地时区。
### 风险 4ISO week 跨年
ISO week-year 与自然年不同。若前端展示按自然年理解,可能产生认知差异。
### 风险 5修改已有事件表可能触发 SpacetimeDB 迁移限制
如果已有事件表需要新增字段:
- 字段必须加末尾。
- 必须提供 default。
- 历史数据要分批迁移。
### 风险 5表设计过早绑定单一业务
建议用通用 date dimension而不是为某个单一埋点写死周/月/季/年表,避免后续复用困难。
## 待确认问题
1. 周维度使用 ISO week 还是自然周?周一开始还是周日开始?
2. 周/月/季/年映射是否沿用当前北京时间业务日口径?
3. 这个映射表服务的是所有埋点,还是只服务个人任务/运营后台统计?
4. 是否需要 API 暴露这些映射关系,还是只用于后端聚合?
5. 是否需要回填历史事件?历史数据规模多大?
6. 未来是否会存在非个人任务,例如整站任务、模块任务、作品任务?如果会,应另行设计任务类型和 `scope_id` 来源,不应复用当前个人任务配置页直接开放 scope。
## 建议结论
优先采用“一张通用日期维度映射表”的设计:
```text
analytics_date_dimension
```
通过字段同时提供:
```text
day / week / month / quarter / year
```
后续统计按 `granularity` 选择 bucket key 聚合。这样比直接新增四张独立映射表更稳定、更容易复用,也更容易处理跨年周、季度边界和历史回填。