# 埋点系统新增周、月、季、年维度映射表计划 ## 目标 在 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` 或 `Option`。 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。 ``` ### 风险 2:Work 维度缺少 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/用户本地时区。 ### 风险 4:ISO 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 聚合。这样比直接新增四张独立映射表更稳定、更容易复用,也更容易处理跨年周、季度边界和历史回填。