2 Commits

Author SHA1 Message Date
1d9d8c2e41 feat: add analytics metric granularity query
Some checks failed
CI / verify (push) Has been cancelled
2026-05-04 16:29:48 +08:00
44d9bd55de docs: initialize shared Hermes project memory 2026-05-04 16:29:48 +08:00
29 changed files with 1635 additions and 7 deletions

53
.hermes/README.md Normal file
View File

@@ -0,0 +1,53 @@
# Genarrative 团队 Hermes 共享记忆
本目录用于在仓库内共享团队级 Hermes 上下文,供 3 名开发人员在各自本地 Hermes 中读取、更新和同步。
## 使用原则
- `.hermes/` 中只保存可以进入 Git 的团队共享内容。
- 不提交个人配置、API Key、会话转录、模型密钥、本地路径密钥等敏感内容。
- 个人 Hermes 的 `~/.hermes/config.yaml``~/.hermes/.env``~/.hermes/sessions/` 不应复制到本仓库。
- 开发前先阅读本目录下与任务相关的记忆文件;开发后如产生稳定知识,更新对应文档。
- 若本目录内容与 `docs/` 或代码事实冲突,以当前代码和最新 `docs/` 为准,并同步修正过期记忆。
## 目录结构
```text
.hermes/
├─ README.md # 本说明
├─ shared-memory/
│ ├─ project-overview.md # 项目概览与当前技术路线
│ ├─ team-conventions.md # 团队协作约定
│ ├─ development-workflow.md # 开发、测试、提交流程
│ ├─ document-map.md # README / AGENTS / docs 阅读索引
│ ├─ decision-log.md # 长期决策记录
│ ├─ pitfalls.md # 踩坑与排障记录
│ └─ handoff-template.md # 任务交接模板
├─ plans/ # 阶段性计划与实施方案
└─ skills/ # 未来可沉淀的仓库级 Hermes skills
```
## 推荐给 Hermes 的启动提示
在本仓库中开始复杂任务时,可以先对 Hermes 说:
```text
请先读取 AGENTS.md 以及 .hermes/shared-memory/ 下与本任务相关的团队共享记忆,再开始分析。若任务完成后产生稳定项目知识,请更新 .hermes/shared-memory/ 对应文件。
```
## 需要沉淀到这里的内容
- 长期有效的架构约定
- 反复会用到的本地开发/测试流程
- 已确认的接口契约或模块边界
- 重要技术决策及原因
- 踩坑、排障方式、验证命令
- 团队协作规则和任务交接规范
## 不应沉淀到这里的内容
- API Key、Token、Cookie、私有密钥
- 个人账号、个人本地绝对路径、个人隐私信息
- 大段临时聊天记录
- 尚未确认的一次性猜测
- 构建产物、日志、缓存、数据库 dump

View File

@@ -0,0 +1,65 @@
# 决策记录
> 用途:记录已经确认、会影响后续开发的长期技术/产品/协作决策。短期讨论不要写在这里。
## 记录格式
```md
## YYYY-MM-DD 决策标题
- 背景:为什么需要这个决策
- 决策:最终决定是什么
- 影响范围:涉及哪些模块/文档/流程
- 验证方式:如何确认决策仍有效
- 关联文档:相关 PRD、技术文档、提交或 Issue
```
---
## 2026-05-04 在仓库 `.hermes/` 中建立团队共享记忆
- 背景:团队有 3 名开发人员,均在各自本地安装 Hermes并需要独立拉取仓库、修改代码、本地测试团队希望形成共享的长期项目记忆。
- 决策:不共享个人 `~/.hermes`,先在 Genarrative 仓库内使用 `.hermes/` 保存可 Git 同步的团队共享记忆、计划和未来 skills。
- 影响范围:`AGENTS.md``.hermes/README.md``.hermes/shared-memory/`
- 验证方式:任一开发者拉取仓库后,在项目根目录启动 Hermes均可读取同一套 `.hermes/shared-memory/` 文件。
- 关联文档:`.hermes/README.md``.hermes/shared-memory/team-conventions.md`
## 2026-04-25 后端唯一落地口径固定为 Rust / SpacetimeDB
- 背景:项目经历过 Node/Express/PostgreSQL、Go 试验、Rust/SpacetimeDB 等多条后端路线,旧路线文档容易造成开发歧义。
- 决策新功能以后端当前基线为准HTTP 门面使用 Rust `api-server` / Axum业务真相使用 SpacetimeDB领域和契约在 `server-rs` 多 crate 分层维护。
- 影响范围:所有后端、数据真相、运行时状态、创作结果、用户系统、资产、任务、埋点、后台 API 等相关开发。
- 验证方式:开发前优先阅读 `CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md`;旧 `server-node`、Express、PostgreSQL、Go 方向只允许作为迁移参考。
- 关联文档:`docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md``AGENTS.md`
## 2026-04-28/29 server-rs DDD 分层与契约矩阵冻结
- 背景server-rs 模块多、上下文多需防止领域规则、SpacetimeDB 表、HTTP BFF、前端临时逻辑互相污染。
- 决策:按 DDD 总纲和 G1 契约/路由矩阵开发:`module-*` 承载领域,`spacetime-module` 承载表和事务,`spacetime-client` 承载 facade`api-server` 承载 HTTP/SSE/BFF`platform-*` 承载外部副作用,`shared-contracts` 承载 DTO。
- 影响范围server-rs 全部 crate、前端 API client、SpacetimeDB schema、旧接口清理。
- 验证方式:执行任务前对照 DDD 总纲、并行任务清单、G1 矩阵;提交前运行相关 DDD 边界检查和定向测试。
- 关联文档:`SERVER_RS_DDD_FULL_REFACTOR_2026-04-28.md``SERVER_RS_DDD_G1_CONTRACT_AND_ROUTE_MATRIX_2026-04-29.md``SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md`
## SpacetimeDB 表结构变更必须显式维护迁移与表目录
- 背景SpacetimeDB 的 schema 迁移模型不同于 PostgreSQL部分变更会触发冲突或拒绝自动迁移。
- 决策:凡涉及 table、reducer、procedure、row shape 或 binding 变化,必须同步 `migration.rs`、表目录和生成绑定;涉及 private 表迁移时按 JSON 导入导出和分片导入流程处理。
- 影响范围:`server-rs/crates/spacetime-module``spacetime-client` bindings、`SPACETIMEDB_TABLE_CATALOG.md`、部署/发布脚本。
- 验证方式:发布前检查 `SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md` 清单,更新 `SPACETIMEDB_TABLE_CATALOG.md`,执行生成绑定和相关测试。
- 关联文档:`SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md``SPACETIMEDB_TABLE_CATALOG.md``SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md`
## 生产部署切换到 systemd + Nginx + 自托管 SpacetimeDB
- 背景:旧一体化启动脚本和历史 Jenkinsfile 已不再是生产发布唯一入口。
- 决策:生产部署以 systemd 托管 SpacetimeDB 与 Rust `api-server`Nginx 负责站点和代理,生产 Jenkinsfile 按 web/api/stdB module/build/deploy/publish 拆分。
- 影响范围部署脚本、服务器目录、维护模式、Jenkins、Nginx、systemd 服务。
- 验证方式生产发布、服务器配置、Jenkins Job 重建或回滚时,先看 `PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md`
- 关联文档:`PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md`
## 个人任务与埋点首版边界冻结
- 背景“我的”Tab、任务、奖励、钱包和埋点涉及用户、运营、分析多条链路需要避免范围泛化。
- 决策:埋点原始事实进入 `tracking_event`,聚合投影进入 `tracking_daily_stat`;个人任务配置/进度/领奖/钱包分别进入 `profile_task_config``profile_task_progress``profile_task_reward_claim``profile_wallet_ledger`;首版个人任务 scope 仅支持 `user`
- 影响范围:用户侧任务中心、后台任务配置、运营查询、埋点查询、钱包流水。
- 验证方式:非 `user` scope 的个人任务配置应被 API 和领域构造层拒绝;任务查询与埋点查询分别放在 `docs/operations/``docs/tracking/`
- 关联文档:`PROFILE_TASK_AND_TRACKING_SYSTEM_2026-05-03.md``RUNTIME_PROFILE_TASK_SCOPE_2026-05-04.md``ANALYTICS_DATE_DIMENSION_IMPLEMENTATION_2026-05-04.md`

View File

@@ -0,0 +1,203 @@
# 开发工作流
> 用途:给本地 Hermes 和开发人员提供统一的开发、测试、提交流程。具体命令以 `package.json`、`server-rs/Cargo.toml`、`AGENTS.md` 和相关 `docs/` 最新文档为准。
## 标准任务流程
```text
同步代码 → 读取 AGENTS.md → 读取 .hermes/shared-memory → 查找/完善 docs → 制定计划 → 小步实现 → 本地验证 → 更新文档/记忆 → 提交
```
## 建议启动方式
在项目根目录启动 Hermes
```bash
cd /path/to/Genarrative
hermes
```
在本机当前常见路径为:
```bash
/home/dsk/workspace/Genarrative
```
其他开发者以自己本地实际路径为准,不要把个人绝对路径写入共享文档作为通用规则。
## 开发前检查清单
- [ ] 当前分支是否正确
- [ ] 是否已拉取最新代码
- [ ] 是否阅读 `AGENTS.md`
- [ ] 是否阅读 `.hermes/shared-memory/` 相关文件
- [ ] 是否阅读 `README.md` 中的运行和检查命令
- [ ] 是否阅读 `docs/README.md` 及任务相关分类 README
- [ ] 是否存在足够具体的 PRD / 设计 / 技术文档
- [ ] 是否明确测试、验收和文档更新方式
## 本地运行命令
安装依赖:
```bash
npm install
```
完整联调开发环境:
```bash
npm run dev
```
该命令会启动:
- SpacetimeDB standalone
- Rust `api-server`
- 主站 Vite
- 后台 Vite
单独启动前端:
```bash
npm run dev:web
```
单独启动 Rust API server
```bash
npm run api-server
```
查看本地 Rust/SpacetimeDB 日志:
```bash
npm run dev:rust:logs
```
后台管理前端:
```bash
npm run admin-web:dev
npm run admin-web:build
npm run admin-web:typecheck
```
SpacetimeDB bindings 生成:
```bash
npm run spacetime:generate
```
## 常用检查命令
编码检查:
```bash
npm run check:encoding
```
ESLint
```bash
npm run lint:eslint
```
类型检查:
```bash
npm run typecheck
```
综合 lint
```bash
npm run lint
```
测试:
```bash
npm run test
```
生产构建:
```bash
npm run build
```
内容检查:
```bash
npm run check:data
npm run check:overrides
npm run check:smoke
npm run check:content
```
全量检查:
```bash
npm run check
```
DDD 边界检查:
```bash
npm run check:server-rs-ddd
```
## 后端相关默认验证
后端修改后,按 DDD 文档中的验收命令执行。涉及 API smoke 时:
- 使用 `npm run api-server` 重新拉起后端。
- 检查 `/healthz`
- 执行对应自动测试。
- 涉及 SpacetimeDB 表、reducer、procedure、row shape 或绑定变化时,同步更新 `migration.rs`、表目录和生成绑定。
关键文档:
- `docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md`
- `docs/technical/SERVER_RS_DDD_FULL_REFACTOR_2026-04-28.md`
- `docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md`
- `docs/technical/SERVER_RS_DDD_G1_CONTRACT_AND_ROUTE_MATRIX_2026-04-29.md`
- `docs/technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md`
- `docs/technical/SPACETIMEDB_TABLE_CATALOG.md`
## 前端相关默认验证
前端修改后,应根据修改范围选择:
- `npm run check:encoding`
- `npm run lint:eslint`
- `npm run typecheck`
- `npm run test`
- 页面交互 smoke
- 移动端视口检查
前端原则:
- 移动端优先,再兼容网页端。
- 页面只展示后端返回的状态,不自行计算结论型业务状态。
- 优先复用现有面板、抽屉、弹窗,不新建独立大系统。
- 不在 UI 中默认写功能说明类文本。
- 弹出独立面板的交互不要实现成在当前面板下方追加内容。
## 文档更新规则
- 工程修改要同步更新对应文档。
- 如果没有现成文档,新文档统一放入 `docs/` 下合适分类。
- `.hermes/shared-memory/` 只记录高频、长期、团队共享的摘要和索引,不替代完整 PRD/技术文档。
- 如果 `.hermes/shared-memory/` 与代码或 `docs/` 冲突,以代码和最新 `docs/` 为准,并同步修正共享记忆。
## 提交前建议让 Hermes 执行
```text
请检查当前 git diff指出
1. 是否违反 AGENTS.md 或 .hermes/shared-memory 约定;
2. 是否需要补充 docs
3. 是否有长期知识需要写入 .hermes/shared-memory
4. 建议的测试命令和提交信息。
```

View File

@@ -0,0 +1,100 @@
# 文档地图与阅读索引
> 用途:根据 `README.md`、`AGENTS.md` 和 `docs/` 下文档索引整理团队记忆入口,帮助本地 Hermes 快速选择应该读哪些资料。
## 全局入口
| 场景 | 优先阅读 |
| --- | --- |
| 建立项目背景 | `README.md``AGENTS.md``.hermes/shared-memory/project-overview.md` |
| 找文档分类 | `docs/README.md` |
| 开发方法论 | `docs/experience/README.md` |
| 查风险与历史问题 | `docs/audits/README.md` |
| 做玩法/交互/系统设计 | `docs/design/README.md` |
| 做技术实现/后端/部署 | `docs/technical/README.md` |
| 排期与拆阶段 | `docs/planning/README.md` |
| 查脚本/Function/prompt/职责地图 | `docs/reference/README.md` |
| 查埋点 SQL | `docs/tracking/README.md` |
| 查运营/任务/钱包对账 SQL | `docs/operations/README.md` |
| 查 PRD | `docs/prd/` |
## docs 分类规则
- `experience/`:方法论、交接经验、长期有效的开发结论。
- `audits/`:现状扫描、问题定位、是否达标的审查类文档。
- `design/`:玩法机制、叙事关系、系统结构设计。
- `technical/`:技术选型、实现路线、竞品/产品形态拆解。
- `planning/`:阶段优先级与推进顺序。
- `reference/`:目录、速查、检索辅助。
- `tracking/`:埋点原始事实和聚合投影查询。
- `operations/`:后台运营核查、对账和排障查询。
- `prd/`:产品需求与阶段计划。
## 推荐阅读顺序
通用复杂任务:
1. `AGENTS.md`
2. `.hermes/shared-memory/`
3. `docs/README.md`
4. `docs/experience/README.md`
5. `docs/audits/README.md`
6. 任务对应分类下的 README 和具体文档
后端 / 数据真相 / SpacetimeDB
1. `docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md`
2. `docs/technical/SERVER_RS_DDD_FULL_REFACTOR_2026-04-28.md`
3. `docs/technical/SERVER_RS_DDD_G1_CONTRACT_AND_ROUTE_MATRIX_2026-04-29.md`
4. `docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md`
5. `docs/technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md`
6. `docs/technical/SPACETIMEDB_TABLE_CATALOG.md`
7. 具体模块方案文档
生产部署 / 服务器 / Jenkins
1. `docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md`
2. 需要迁移时再看 `SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md`
3. 历史 Jenkins / CORS / 本地远端脚本文档只作追溯,不作为当前入口
RPG 创作与运行时链路:
1. `docs/reference/RPG_CREATION_AND_RUNTIME_SCRIPT_RESPONSIBILITY_MAP_2026-04-28.md`
2. `docs/technical/CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md`
3. `docs/technical/RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md`
4. 相关工作包 progress / closure 文档
移动端 UI / 游戏 UI
1. `docs/experience/MOBILE_UI_DEV_EXPERIENCE.md`
2. `UI_CODING_STANDARD.md`
3. 相关 `docs/design/` 文档
创作 Agent / 自定义世界:
1. `docs/design/CUSTOM_WORLD_CREATOR_INPUT_AND_AI_BOUNDARY_DESIGN_2026-04-06.md`
2. `docs/design/CUSTOM_WORLD_CREATOR_MANUAL_AI_SYSTEM_BALANCE_DESIGN_2026-04-12.md`
3. `docs/design/CUSTOM_WORLD_CREATOR_PURE_AGENT_COMPARISON_AND_CONVERSION_DESIGN_2026-04-12.md`
4. `docs/technical/UNIFIED_CREATION_AGENT_CHAT_FRAMEWORK_2026-04-22.md`
5. 相关 `SPACETIMEDB_CUSTOM_WORLD_*` 技术方案
拼图 / 大鱼 / Match3D
- 拼图:优先看 `PUZZLE_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-22.md``PUZZLE_RUNTIME_FRONTEND_LOGIC_REHOME_2026-05-02.md` 和相关 Puzzle 技术文档。
- 大鱼吃小鱼:优先看 `BIG_FISH_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-22.md` 和相关 Big Fish 技术/经验文档。
- 抓大鹅 Match3D优先看 `docs/prd/AI_NATIVE_MATCH3D_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-04-30.md``MATCH3D_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-30.md` 和相关 Match3D 技术文档。
个人任务 / 埋点 / 运营查询:
1. `docs/technical/PROFILE_TASK_AND_TRACKING_SYSTEM_2026-05-03.md`
2. `docs/technical/RUNTIME_PROFILE_TASK_SCOPE_2026-05-04.md`
3. `docs/technical/ANALYTICS_DATE_DIMENSION_IMPLEMENTATION_2026-05-04.md`
4. `docs/tracking/TRACKING_QUERY_PLAYBOOK_2026-05-03.md`
5. `docs/operations/PROFILE_TASK_QUERY_PLAYBOOK_2026-05-03.md`
## 文档维护规则
- 新增工程实现时,如果已有对应文档,必须同步更新。
- 如果没有对应文档,新文档放入 `docs/` 下合适分类。
- `.hermes/shared-memory/` 只保留跨任务、跨成员、高频使用的摘要和索引。
- 如果文档与代码冲突,先确认代码事实,再更新过期文档和共享记忆。

View File

@@ -0,0 +1,53 @@
# 任务交接模板
> 用途:当一名开发者把任务交给另一名开发者,或让 Hermes 接续上下文时,复制本模板并填写。
## 基本信息
- 任务名称:
- 负责人:
- 当前分支:
- 相关需求/Issue
- 相关文档:
## 背景
简要说明为什么做这个任务,以及业务/技术目标。
## 已完成
- [ ]
- [ ]
- [ ]
## 未完成
- [ ]
- [ ]
- [ ]
## 关键文件
- `path/to/file`:说明
- `path/to/file`:说明
## 当前问题/风险
-
## 已执行验证
```bash
# 粘贴已执行命令和结果摘要
```
## 建议下一步
1.
2.
3.
## 是否需要更新团队记忆
- [ ] 不需要
- [ ] 需要,建议更新:`.hermes/shared-memory/...`

View File

@@ -0,0 +1,102 @@
# 踩坑与排障记录
> 用途:记录已验证、未来很可能再次遇到的问题。每条都应包含现象、原因、处理方式和验证方式。
## 记录格式
```md
## 问题标题
- 现象:看到什么错误或异常行为
- 原因:确认后的根因
- 处理:具体修复步骤
- 验证:如何确认修复有效
- 关联:相关文件、文档、提交或 Issue
```
---
## 中文乱码与编码风险
- 现象:中文文案、注释、剧情或文档显示为乱码,或被改写成英文。
- 原因Windows/PowerShell/终端编码不一致,或整文件重写导致编码变化。
- 处理:
- 不要直接沿用乱码文本。
- 不要用英文替换中文,除非用户明确要求翻译。
- 在 PowerShell 5.1 中显式使用 UTF-8。
- 优先用 Python/Node 或 `Get-Content -Encoding UTF8` 核对原文。
- 修改中文文件时优先局部补丁,避免无关内容重写。
- 验证:运行仓库已有编码检查;人工抽查修改文件中的中文内容。
- 关联:`AGENTS.md``npm run check:encoding`
## `.hermes` 只放共享内容,不放个人 Hermes 配置
- 现象:团队成员误把个人 Hermes 配置、会话或密钥复制进仓库。
- 原因:仓库 `.hermes/` 与个人 `~/.hermes/` 名称相似。
- 处理:仓库 `.hermes/` 只放 Markdown 共享记忆、计划和可公开 skills不提交 `.env``config.yaml``sessions/``auth.json`
- 验证:提交前检查 `git diff -- .hermes`,确认没有密钥、会话记录或个人路径敏感信息。
- 关联:`.hermes/README.md`
## 旧后端路线文档造成判断漂移
- 现象:开发时参考到 Express、Node、PostgreSQL 或 Go 方向旧文档,导致接口、数据真相或部署路径与当前主线不一致。
- 原因:项目历史文档较多,部分旧方案仍保留作迁移参考。
- 处理涉及服务端、数据真相、SpacetimeDB、运行时状态时先看 `CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md`,再看 DDD 总纲和具体技术方案。
- 验证:代码改动应落在 `server-rs + Axum + SpacetimeDB` 主线;旧路线只作为迁移参考,不作为兼容目标。
- 关联:`docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md``AGENTS.md`
## SpacetimeDB 表结构变更不能按 PostgreSQL 迁移直觉处理
- 现象:发布时 schema 冲突、自动迁移拒绝、旧客户端调用 reducer 失败、private 表数据迁移遗漏。
- 原因SpacetimeDB 对字段删除、类型变化、索引/主键/RLS/reducer 变化有不同自动迁移边界。
- 处理:变更前阅读 `SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md`;涉及表变化时同步 `migration.rs``SPACETIMEDB_TABLE_CATALOG.md` 和 bindings必要时走 JSON 导入导出与分片导入迁移流程。
- 验证:发布前完成 schema 检查、bindings 生成、表目录更新和相关 smoke。
- 关联:`docs/technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md``docs/technical/SPACETIMEDB_TABLE_CATALOG.md`
## 本地 SpacetimeDB replica identity 不匹配
- 现象:本地 standalone 启动时报 `mismatched database identity`
- 原因root-dir / replica 数据残留与当前数据库身份不一致。
- 处理:按本地 replica identity mismatch 文档进行备份、重建和脚本诊断。
- 验证:本地 SpacetimeDB 可正常启动并 publish / 访问。
- 关联:`docs/technical/SPACETIMEDB_LOCAL_REPLICA_IDENTITY_MISMATCH_FIX_2026-04-30.md`
## Vite SPA fallback 吞掉 API 请求
- 现象:本地请求 `/api/profile/*` 等接口时返回 HTML被前端当 JSON 解析报错。
- 原因Vite 代理缺少对应 `/api/*` 前缀API 请求落到 SPA fallback。
- 处理:补齐 Vite 代理,让 API 请求转发到 Rust `api-server`
- 验证:请求返回 JSON相关页面不再出现 HTML parse 错误。
- 关联:`docs/technical/PROFILE_MAIN_ROUTE_VITE_PROXY_FIX_2026-05-02.md`
## Rust 冷编译导致 api-server 健康检查误超时
- 现象:`npm run dev:rust` 在 Windows 冷编译/链接阶段误判 `/healthz` 等待超时并杀掉 `cargo run`
- 原因:脚本把 SpacetimeDB 与 api-server 等待窗口混在一起,未考虑 Rust 冷编译耗时。
- 处理:按冷编译超时修复文档拆分等待窗口。
- 验证:冷启动时不再误杀仍在编译的 api-server。
- 关联:`docs/technical/API_SERVER_DEV_STACK_COLD_BUILD_TIMEOUT_FIX_2026-04-25.md`
## server-rs 默认 cargo build 不能等同于构建 SpacetimeDB 模块
- 现象:在 `server-rs` 下无参数 `cargo build` 期望同时构建 `spacetime-module`,导致链接或构建范围误判。
- 原因workspace default-members 当前只包含 `crates/api-server`SpacetimeDB module 有独立构建/发布方式。
- 处理:默认 Rust 构建只覆盖原生 `api-server`;模块产物继续走 `spacetime build` / publish / bindings 生成流程。
- 验证:查看 `server-rs/Cargo.toml` default-members并按相关 SpacetimeDB 文档执行模块构建。
- 关联:`server-rs/Cargo.toml``docs/technical/RUST_WORKSPACE_DEFAULT_BUILD_SCOPE_FIX_2026-04-25.md`
## 生产发布入口不要沿用旧 Jenkinsfile / 一体化脚本
- 现象:部署、回滚或 Jenkins Job 重建时参考旧发布文档,导致 systemd、Nginx、SpacetimeDB 自托管和生产包拆分不一致。
- 原因:旧 Jenkins / 旧本地远端部署脚本文档仍作为历史经验保留。
- 处理:生产相关操作先看 `PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md`,再按需追溯旧文档。
- 验证:发布链路使用当前 `deploy/systemd``deploy/nginx``scripts/deploy``jenkins/Jenkinsfile.production-*`
- 关联:`docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md`
## 个人任务 scope 不得扩成 work/site/module
- 现象:个人任务配置为 `work` / `site` / `module` 后进度串桶或静默按 0 处理。
- 原因:首版个人任务只支持用户维度,非 user scope 会造成任务进度读取语义错误。
- 处理Admin 任务配置页不展示范围选择,保存时固定 `scopeKind: 'user'`API 和领域构造层拒绝非 `User`
- 验证:非 `user` scope 返回错误;相关测试覆盖 `Site` / `Module` / `Work` 被拒绝。
- 关联:`docs/technical/RUNTIME_PROFILE_TASK_SCOPE_2026-05-04.md``docs/technical/ANALYTICS_DATE_DIMENSION_IMPLEMENTATION_2026-05-04.md`

View File

@@ -0,0 +1,138 @@
# Genarrative 项目共享概览
> 用途:给团队成员本地 Hermes 快速建立项目背景。内容应保持高层、稳定、可验证;细节以代码、`README.md`、`AGENTS.md` 和 `docs/` 最新文档为准。
## 一句话定位
Genarrative / AI Native Visual RPG 是一个以 **AI 叙事 + 本地规则 + 像素演出** 为核心的视觉 RPG 与 AI 原生游戏创作平台原型。
项目当前不只是单一 RPG demo而是在同一平台内同时承载
- RPG / 自定义世界创作与运行时
- 拼图玩法创作与运行时
- 大鱼吃小鱼玩法链路
- 抓大鹅 Match3D 玩法链路
- 用户账号、存档、钱包、任务、埋点、后台管理与生产部署链路
## 已具备的主要能力
来自根目录 `README.md` 的当前主能力:
- 世界与角色选择
- AI 剧情推进与流式对话
- 战斗演出、NPC 战斗、切磋
- NPC 交易、送礼、求助、招募
- 宝藏交互
- 同伴跟随与战斗
- 游戏主流程内嵌的角色资产工坊、自定义世界实体编辑与角色形象编辑
- 自动存档与继续游戏
## 当前前端与平台入口
- 主站默认地址:`http://127.0.0.1:3000`
- 后台可从 `http://127.0.0.1:3000/admin/` 进入,也可直连 `http://127.0.0.1:3102`
- 主站、后台和 Rust 后端联调默认走 `npm run dev`
- 只启动前端页面可用 `npm run dev:web`,默认代理到本地 Rust `api-server`
- 后台管理独立前端工程为 `apps/admin-web`,管理端只做表现,数据和写操作走 `server-rs``/admin/api/*`
## 当前后端唯一落地口径
后端主线已经切到:
```text
server-rs + Axum + SpacetimeDB
```
当前唯一有效后端方向:
- HTTP 门面Rust `api-server` / Axum
- 实时状态与业务真相:`server-rs/crates/spacetime-module` / SpacetimeDB
- 共享领域与契约:`server-rs` 多 crate 分层维护
- 前端职责:表现、输入采集、临时 UI 状态、服务端结果渲染
明确不再作为正式兼容目标:
- `server-node` / Express / PostgreSQL 正式后端路线
- Go 服务端试验路线
- 浏览器侧承担正式运行时逻辑、正式生成编排或正式数据真相的路线
## server-rs DDD 分层边界
DDD 分层边界以 `docs/technical/SERVER_RS_DDD_FULL_REFACTOR_2026-04-28.md``SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md``AGENTS.md` 为准:
- `module-*`:领域模型、命令、应用编排结果、领域事件、领域错误
- `spacetime-module`SpacetimeDB 表、reducer、procedure、事务 adapter、mapper
- `spacetime-client`:后端访问 SpacetimeDB 的 typed facade
- `api-server`HTTP / SSE / BFF adapter 与外部平台服务编排
- `platform-*`LLM、OSS、SMS、微信等外部副作用
- `shared-contracts`:前后端 DTO 与公开协议
- `shared-kernel`:跨纯领域 crate 复用的基础字符串、ID、时间和归一化能力
- `tests-support``server-rs` workspace 共享测试支撑
## 当前 Rust workspace 主要 crate
`server-rs/Cargo.toml` 为准,当前主要成员包括:
- 业务领域:`module-ai``module-assets``module-auth``module-big-fish``module-combat``module-inventory``module-custom-world``module-match3d``module-npc``module-puzzle``module-progression``module-quest``module-runtime``module-runtime-story``module-runtime-item``module-story`
- 平台副作用:`platform-oss``platform-auth``platform-llm`
- 共享层:`shared-contracts``shared-kernel``shared-logging`
- SpacetimeDB 接入:`spacetime-client``spacetime-module`
- HTTP 服务与测试:`api-server``tests-support`
注意:`server-rs` 的默认 `cargo build` 只构建 `crates/api-server`SpacetimeDB 模块产物继续走 `spacetime build` / 发布链路。
## SpacetimeDB 表域总览
`docs/technical/SPACETIMEDB_TABLE_CATALOG.md` 为持续维护入口。当前表域包括:
- 运维迁移:`database_migration_operator``database_migration_import_chunk`
- 认证:`auth_store_snapshot``user_account``auth_identity``refresh_session`
- 运行时档案:`runtime_setting``runtime_snapshot``user_browse_history``profile_dashboard_state``profile_wallet_ledger``analytics_date_dimension``tracking_event``tracking_daily_stat``profile_task_config``profile_task_progress``profile_task_reward_claim`
- RPG 运行时:`story_session``story_event``npc_state``inventory_slot``battle_state``treasure_record``quest_record``quest_log``player_progression``chapter_progression`
- 世界创作:`custom_world_profile``custom_world_session``custom_world_agent_session``custom_world_agent_message``custom_world_agent_operation``custom_world_draft_card``custom_world_gallery_entry`
- 拼图:`puzzle_agent_session``puzzle_agent_message``puzzle_work_profile``puzzle_event``puzzle_runtime_run``puzzle_leaderboard_entry`
- 抓大鹅 Match3D`match3d_agent_session``match3d_agent_message``match3d_work_profile``match3d_runtime_run`
- 大鱼吃小鱼:`big_fish_creation_session``big_fish_agent_message``big_fish_asset_slot``big_fish_event``big_fish_runtime_run`
- 资产:`asset_object``asset_entity_binding``asset_event`
- AI 任务:`ai_task``ai_task_stage``ai_text_chunk``ai_result_reference``ai_task_event`
## 产品命名与运营口径
`docs/technical/PRODUCT_NAMING_BAIMENG_RENAME_2026-05-01.md` 为准:
- 产品展示名:百梦
- 消费单位:光点
- 公开账号标识:百梦号
- 创作侧称谓:百梦主
个人任务与埋点系统首版边界:
- 埋点原始事实写入 `tracking_event`
- 聚合投影写入 `tracking_daily_stat`
- 任务配置写入 `profile_task_config`
- 任务进度写入 `profile_task_progress`
- 领奖记录写入 `profile_task_reward_claim`
- 钱包流水写入 `profile_wallet_ledger`
- “星光”奖励复用现有“光点”钱包,不新增第二种货币
- 个人任务 scope 首版仅支持 `user`
## 关键文档入口
- 根项目说明:`README.md`
- 项目总约束:`AGENTS.md`
- 文档总入口:`docs/README.md`
- 经验沉淀:`docs/experience/README.md`
- 审计与复盘:`docs/audits/README.md`
- 系统设计:`docs/design/README.md`
- 技术方案:`docs/technical/README.md`
- 规划与优先级:`docs/planning/README.md`
- 参考目录:`docs/reference/README.md`
- 埋点查询:`docs/tracking/README.md`
- 运营查询:`docs/operations/README.md`
- 后端当前基线:`docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md`
- 后端 DDD 总纲:`docs/technical/SERVER_RS_DDD_FULL_REFACTOR_2026-04-28.md`
- 后端并行任务清单:`docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md`
- 契约与路由矩阵:`docs/technical/SERVER_RS_DDD_G1_CONTRACT_AND_ROUTE_MATRIX_2026-04-29.md`
- SpacetimeDB 表结构变更约束:`docs/technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md`
- SpacetimeDB 表目录:`docs/technical/SPACETIMEDB_TABLE_CATALOG.md`
- 生产部署计划:`docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md`

View File

@@ -0,0 +1,95 @@
# 团队协作约定
> 用途:约定 3 名开发人员在各自本地 Hermes 中协作开发、共享项目记忆的方式。
## 基本模式
- 每位开发人员在自己的电脑上使用本地 Hermes。
- 每位开发人员本地拉取同一个项目仓库,独立修改代码、运行测试、提交分支。
- 团队共享内容优先放在本仓库 `.hermes/``docs/` 中,通过 Git 同步。
- 不共享个人 `~/.hermes` 目录。
## 共享与禁止共享
推荐共享:
- `.hermes/shared-memory/` 团队级长期记忆
- `.hermes/plans/` 阶段性实施计划
- `.hermes/skills/` 未来可复用仓库级 skills
- `docs/` 中 PRD、设计、技术、经验、审计、查询手册
- `AGENTS.md` 项目级 Agent 约束
禁止提交:
- 个人 `~/.hermes/config.yaml`
- 个人 `~/.hermes/.env`
- 个人 `~/.hermes/sessions/`
- API Key、Token、Cookie、认证文件
- 个人本地私密路径和个人隐私信息
- 构建产物、日志、缓存、数据库 dump
## 开发前
1. 拉取最新代码。
2. 阅读 `AGENTS.md`
3. 阅读 `.hermes/shared-memory/` 中与任务相关的文件。
4. 阅读 `docs/README.md` 和任务相关分类 README。
5. 阅读对应 PRD、设计、技术、经验或审计文档。
6. 如果文档不足以指导编码,先补充或修正文档。
## 开发中
- 保持修改范围聚焦,不做无关重构。
- 复用、修改、扩展现有系统优先,避免新建重复系统或页面。
- 涉及中文文本时注意 UTF-8 编码和乱码排查。
- 涉及后端时遵循 DDD 分层,不把业务真相下沉到前端或临时兼容层。
- 涉及 SpacetimeDB 表结构、发布或迁移时,先看 `SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md``SPACETIMEDB_TABLE_CATALOG.md`
- 涉及生产发布、服务器配置、Jenkins Job 重建或回滚时,先看 `PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md`
## 开发后
1. 运行与修改范围匹配的测试或验证命令。
2. 更新相关 `docs/` 文档。
3. 若产生长期有效知识,更新 `.hermes/shared-memory/`
4. 若形成可复用流程,考虑沉淀到 `.hermes/skills/`
5. 在提交信息中区分代码变更与文档/记忆变更。
## 文档阅读顺序
通用任务建议:
1. `README.md`
2. `AGENTS.md`
3. `.hermes/shared-memory/`
4. `docs/README.md`
5. `docs/experience/README.md`
6. `docs/audits/README.md`
7. 任务所属分类:`docs/design/``docs/technical/``docs/planning/``docs/prd/``docs/reference/``docs/tracking/``docs/operations/`
后端任务建议:
1. `docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md`
2. `docs/technical/SERVER_RS_DDD_FULL_REFACTOR_2026-04-28.md`
3. `docs/technical/SERVER_RS_DDD_G1_CONTRACT_AND_ROUTE_MATRIX_2026-04-29.md`
4. `docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md`
5. `docs/technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md`
6. `docs/technical/SPACETIMEDB_TABLE_CATALOG.md`
## 共享记忆更新准则
适合更新:
- 新增稳定架构约定
- 新增长期开发流程
- 已验证的踩坑和排障步骤
- 重要接口契约变化
- 团队协作规范变化
- 文档索引或阅读顺序变化
不适合更新:
- 一次性临时计划
- 未验证猜测
- 个人偏好和个人路径
- 敏感信息
- 大段聊天记录

27
.hermes/skills/README.md Normal file
View File

@@ -0,0 +1,27 @@
# 仓库级 Hermes Skills
本目录预留给未来可共享的仓库级 Hermes skills。
## 什么时候沉淀为 Skill
当某个流程满足以下条件之一时,可以考虑从普通 Markdown 升级为 skill
- 需要反复执行,且步骤稳定。
- 涉及多个目录、命令或验证步骤。
- 曾经踩过坑,需要明确规避步骤。
- 新成员容易做错。
- Hermes 在执行时需要强制加载专门知识。
## 建议结构
```text
.hermes/skills/
└─ skill-name/
└─ SKILL.md
```
## 注意
- 不要把 API Key、Token、账号密码写入 skill。
- 如果 skill 与 `AGENTS.md``docs/` 冲突,先更新冲突来源再使用。
- Skill 应包含触发条件、步骤、坑点和验证方式。

View File

@@ -1,5 +1,17 @@
# AGENTS.md
## 团队 Hermes 共享记忆
- 本仓库的团队级 Hermes 共享内容位于 [`.hermes/`](.hermes/),用于在 3 名开发人员各自本地 Hermes 之间同步长期项目记忆。
- 开始复杂开发任务前,除阅读本文件外,还应优先读取:
- [`.hermes/README.md`](.hermes/README.md)
- [`.hermes/shared-memory/project-overview.md`](.hermes/shared-memory/project-overview.md)
- [`.hermes/shared-memory/team-conventions.md`](.hermes/shared-memory/team-conventions.md)
- [`.hermes/shared-memory/development-workflow.md`](.hermes/shared-memory/development-workflow.md)
- 与任务相关的 [`.hermes/shared-memory/decision-log.md`](.hermes/shared-memory/decision-log.md) 和 [`.hermes/shared-memory/pitfalls.md`](.hermes/shared-memory/pitfalls.md)
- 如果本次任务产生长期有效的架构约定、接口变化、排障经验、开发流程或协作规则,应同步更新 `.hermes/shared-memory/` 中对应文件。
- 仓库 `.hermes/` 只保存可进入 Git 的团队共享内容;禁止提交个人 `~/.hermes` 配置、`.env`、API Key、Token、会话记录、认证文件和本地私密路径。
-`.hermes/shared-memory/` 与当前代码或 `docs/` 最新文档冲突,以代码和最新 `docs/` 为准,并同步修正过期共享记忆。
## 项目约束
- 代码需要有完善的中文注释
- 在落地工程修改前检查是否有详细指导本次落地的文档,若没有文档或文档的完善程度仍有落地过程中编码级别的歧义优先优化文档后落地工程迭代。

View File

@@ -281,6 +281,85 @@ SpacetimeClient::seed_analytics_date_dimensions
3. 如需要由 `api-server` 触发 seed/ensure再补 `spacetime-client` facade。
4. 进入 Step 7/8/9事件写入链路、聚合查询 API、前端 contracts。
## Step 7/8/9 后续接入记录2026-05-04
本次继续推进此前暂缓的 Step 7/8/9 中“按日期维度聚合查询 API / contracts / client facade”部分。
### 已新增能力
1. `module-runtime` 新增 analytics metric 聚合领域类型与纯函数:
- `AnalyticsGranularity = day | week | month | quarter | year`
- `AnalyticsMetricQueryInput`
- `AnalyticsBucketMetric`
- `AnalyticsMetricQueryResponse`
- `aggregate_runtime_tracking_daily_stats(...)`
2. `spacetime-module` 新增 `query_analytics_metric` procedure直接聚合 tracking daily stat输出按 bucket 排序的统计结果。
3. `spacetime-client` 新增 facade
```rust
SpacetimeClient::query_analytics_metric(event_key, scope_kind, scope_id, granularity)
```
4. `api-server` 新增登录态接口:
```http
GET /api/profile/analytics/metric?eventKey=...&scopeKind=user&scopeId=...&granularity=day
```
请求参数:
| 参数 | 说明 |
| --- | --- |
| `eventKey` | 埋点事件 key必填 |
| `scopeKind` | `site | work | module | user` |
| `scopeId` | 对应范围 ID必填 |
| `granularity` | `day | week | month | quarter | year` |
响应 data
```ts
type AnalyticsMetricQueryResponse = {
buckets: Array<{
bucketKey: string;
bucketStartDateKey: number;
bucketEndDateKey: number;
value: number;
}>;
};
```
5. shared contracts / 前端 shared contracts 已新增 analytics query 类型:
- `AnalyticsMetricQueryRequest`
- `AnalyticsMetricQueryResponse`
- `AnalyticsBucketMetricResponse` / `AnalyticsBucketMetric`
- `AnalyticsGranularity`
### 本次验证
`server-rs/` 执行通过:
```bash
cargo test -p module-runtime --test analytics_granularity
cargo check -p spacetime-module
cargo check -p spacetime-client
cargo check -p api-server
```
验证结果:
- `analytics_granularity` 测试通过3 passed。
- `spacetime-module` 编译通过,仅存在既有 dead_code warnings。
- `spacetime-client` 编译通过。
- `api-server` 编译通过,仅存在既有 prompt dead_code warnings。
### 注意事项
当前环境未检测到 `spacetime` / `spacetimedb` CLI因此 analytics metric 相关 `module_bindings` 是按现有生成物结构手动补齐的临时生成物。后续有 CLI 的开发机应优先通过项目脚本重新生成 bindings并复核手写生成物是否可被正式生成输出覆盖。
---
## 阶段结论
当前阶段已经完成“个人任务埋点范围收紧”和“日期维表 module 侧能力”的核心落地,并已生成 SpacetimeDB Rust client bindings。

View File

@@ -189,6 +189,7 @@ export type RedeemProfileRewardCodeResponse = {
export type ProfileTaskCycle = 'daily';
export type TrackingScopeKind = 'site' | 'work' | 'module' | 'user';
export type AnalyticsGranularity = 'day' | 'week' | 'month' | 'quarter' | 'year';
export type ProfileTaskStatus =
| 'incomplete'
| 'claimable'
@@ -247,6 +248,24 @@ export type ProfileTaskConfigAdminListResponse = {
entries: ProfileTaskConfigAdminResponse[];
};
export type AnalyticsMetricQueryRequest = {
eventKey: string;
scopeKind: TrackingScopeKind;
scopeId: string;
granularity: AnalyticsGranularity;
};
export type AnalyticsBucketMetric = {
bucketKey: string;
bucketStartDateKey: number;
bucketEndDateKey: number;
value: number;
};
export type AnalyticsMetricQueryResponse = {
buckets: AnalyticsBucketMetric[];
};
export type AdminUpsertProfileTaskConfigRequest = {
taskId: string;
title: string;

View File

@@ -109,10 +109,10 @@ use crate::{
admin_list_profile_invite_codes, admin_list_profile_redeem_codes,
admin_list_profile_task_configs, admin_upsert_profile_invite_code,
admin_upsert_profile_redeem_code, admin_upsert_profile_task_config,
claim_profile_task_reward, create_profile_recharge_order, get_profile_dashboard,
get_profile_play_stats, get_profile_recharge_center, get_profile_referral_invite_center,
get_profile_task_center, get_profile_wallet_ledger, redeem_profile_referral_invite_code,
redeem_profile_reward_code,
claim_profile_task_reward, create_profile_recharge_order, get_profile_analytics_metric,
get_profile_dashboard, get_profile_play_stats, get_profile_recharge_center,
get_profile_referral_invite_center, get_profile_task_center, get_profile_wallet_ledger,
redeem_profile_referral_invite_code, redeem_profile_reward_code,
},
runtime_save::{
delete_runtime_snapshot, get_runtime_snapshot, list_profile_save_archives,
@@ -1074,6 +1074,13 @@ pub fn build_router(state: AppState) -> Router {
require_bearer_auth,
)),
)
.route(
"/api/profile/analytics/metric",
get(get_profile_analytics_metric).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/profile/tasks",
get(get_profile_task_center).route_layer(middleware::from_fn_with_state(

View File

@@ -1,11 +1,11 @@
use axum::{
Json,
extract::{Extension, Path, State},
extract::{Extension, Path, Query, State},
http::StatusCode,
response::Response,
};
use module_runtime::{
PROFILE_RECHARGE_PAYMENT_CHANNEL_MOCK, RuntimeProfileInviteCodeRecord,
AnalyticsGranularity, PROFILE_RECHARGE_PAYMENT_CHANNEL_MOCK, RuntimeProfileInviteCodeRecord,
RuntimeProfileMembershipBenefitRecord, RuntimeProfileRechargeCenterRecord,
RuntimeProfileRechargeOrderRecord, RuntimeProfileRechargeProductRecord,
RuntimeProfileRedeemCodeMode, RuntimeProfileRedeemCodeRecord,
@@ -15,10 +15,12 @@ use module_runtime::{
RuntimeReferralInviteCenterRecord, RuntimeTrackingScopeKind,
};
use serde_json::{Value, json};
use serde::Deserialize;
use shared_contracts::runtime::{
AdminDisableProfileRedeemCodeRequest, AdminDisableProfileTaskConfigRequest,
AdminUpsertProfileInviteCodeRequest, AdminUpsertProfileRedeemCodeRequest,
AdminUpsertProfileTaskConfigRequest, ClaimProfileTaskRewardResponse,
AdminUpsertProfileTaskConfigRequest, AnalyticsBucketMetricResponse,
AnalyticsMetricQueryResponse, ClaimProfileTaskRewardResponse,
CreateProfileRechargeOrderRequest, CreateProfileRechargeOrderResponse,
PROFILE_TASK_CYCLE_DAILY, PROFILE_TASK_STATUS_CLAIMABLE, PROFILE_TASK_STATUS_CLAIMED,
PROFILE_TASK_STATUS_DISABLED, PROFILE_TASK_STATUS_INCOMPLETE,
@@ -31,6 +33,8 @@ use shared_contracts::runtime::{
PROFILE_WALLET_LEDGER_SOURCE_TYPE_POINTS_RECHARGE,
PROFILE_WALLET_LEDGER_SOURCE_TYPE_PUZZLE_AUTHOR_INCENTIVE_CLAIM,
PROFILE_WALLET_LEDGER_SOURCE_TYPE_REDEEM_CODE_REWARD,
ANALYTICS_GRANULARITY_DAY, ANALYTICS_GRANULARITY_MONTH, ANALYTICS_GRANULARITY_QUARTER,
ANALYTICS_GRANULARITY_WEEK, ANALYTICS_GRANULARITY_YEAR,
PROFILE_WALLET_LEDGER_SOURCE_TYPE_SNAPSHOT_SYNC, ProfileDashboardSummaryResponse,
ProfileInviteCodeAdminListResponse, ProfileInviteCodeAdminResponse,
ProfileMembershipBenefitResponse, ProfileMembershipResponse, ProfilePlayStatsResponse,
@@ -278,6 +282,51 @@ pub async fn redeem_profile_reward_code(
))
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AnalyticsMetricQueryParams {
pub event_key: String,
pub scope_kind: String,
pub scope_id: String,
pub granularity: String,
}
pub async fn get_profile_analytics_metric(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
Extension(_authenticated): Extension<AuthenticatedAccessToken>,
Query(query): Query<AnalyticsMetricQueryParams>,
) -> Result<Json<Value>, Response> {
let scope_kind = parse_tracking_scope_kind(&query.scope_kind).map_err(|error| {
runtime_profile_error_response(
&request_context,
AppError::from_status(StatusCode::BAD_REQUEST).with_message(error),
)
})?;
let granularity = parse_analytics_granularity(&query.granularity).map_err(|error| {
runtime_profile_error_response(
&request_context,
AppError::from_status(StatusCode::BAD_REQUEST).with_message(error),
)
})?;
let record = state
.spacetime_client()
.query_analytics_metric(query.event_key, scope_kind, query.scope_id, granularity)
.await
.map_err(|error| {
runtime_profile_error_response(
&request_context,
map_runtime_profile_client_error(error),
)
})?;
Ok(json_success_body(
Some(&request_context),
build_analytics_metric_query_response(record),
))
}
pub async fn get_profile_task_center(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
@@ -811,6 +860,23 @@ fn build_profile_task_center_response(
}
}
fn build_analytics_metric_query_response(
record: module_runtime::AnalyticsMetricQueryResponse,
) -> AnalyticsMetricQueryResponse {
AnalyticsMetricQueryResponse {
buckets: record
.buckets
.into_iter()
.map(|bucket| AnalyticsBucketMetricResponse {
bucket_key: bucket.bucket_key,
bucket_start_date_key: bucket.bucket_start_date_key,
bucket_end_date_key: bucket.bucket_end_date_key,
value: bucket.value,
})
.collect(),
}
}
fn build_profile_task_item_response(
record: RuntimeProfileTaskItemRecord,
) -> ProfileTaskItemResponse {
@@ -935,6 +1001,17 @@ fn parse_tracking_scope_kind(raw: &str) -> Result<RuntimeTrackingScopeKind, Stri
}
}
fn parse_analytics_granularity(raw: &str) -> Result<AnalyticsGranularity, String> {
match raw.trim().to_ascii_lowercase().as_str() {
ANALYTICS_GRANULARITY_DAY => Ok(AnalyticsGranularity::Day),
ANALYTICS_GRANULARITY_WEEK => Ok(AnalyticsGranularity::Week),
ANALYTICS_GRANULARITY_MONTH => Ok(AnalyticsGranularity::Month),
ANALYTICS_GRANULARITY_QUARTER => Ok(AnalyticsGranularity::Quarter),
ANALYTICS_GRANULARITY_YEAR => Ok(AnalyticsGranularity::Year),
_ => Err("统计粒度无效".to_string()),
}
}
fn format_profile_task_cycle(cycle: RuntimeProfileTaskCycle) -> &'static str {
match cycle {
RuntimeProfileTaskCycle::Daily => PROFILE_TASK_CYCLE_DAILY,

View File

@@ -4,6 +4,7 @@
use serde_json::Value;
use shared_kernel::{offset_datetime_to_unix_micros, parse_rfc3339};
use std::collections::BTreeMap;
use crate::domain::*;
use crate::errors::RuntimeProfileFieldError;
@@ -502,6 +503,81 @@ pub fn build_runtime_tracking_daily_stat_id(
)
}
pub fn aggregate_runtime_tracking_daily_stats(
stats: Vec<RuntimeAnalyticsDailyStatSnapshot>,
event_key: &str,
scope_kind: RuntimeTrackingScopeKind,
scope_id: &str,
granularity: AnalyticsGranularity,
) -> Vec<AnalyticsBucketMetric> {
let mut buckets: BTreeMap<(String, i64, i64), u64> = BTreeMap::new();
let event_key = event_key.trim();
let scope_id = scope_id.trim();
for stat in stats {
if stat.event_key.trim() != event_key
|| stat.scope_kind != scope_kind
|| stat.scope_id.trim() != scope_id
{
continue;
}
let dimension = build_analytics_date_dimension_from_date_key(stat.day_key);
let (bucket_key, bucket_start_date_key, bucket_end_date_key) =
analytics_bucket_for_dimension(&dimension, granularity);
*buckets
.entry((bucket_key, bucket_start_date_key, bucket_end_date_key))
.or_insert(0) += u64::from(stat.count);
}
buckets
.into_iter()
.map(
|((bucket_key, bucket_start_date_key, bucket_end_date_key), value)| {
AnalyticsBucketMetric {
bucket_key,
bucket_start_date_key,
bucket_end_date_key,
value,
}
},
)
.collect()
}
fn analytics_bucket_for_dimension(
dimension: &AnalyticsDateDimensionSnapshot,
granularity: AnalyticsGranularity,
) -> (String, i64, i64) {
match granularity {
AnalyticsGranularity::Day => (
dimension.calendar_date.clone(),
dimension.date_key,
dimension.date_key,
),
AnalyticsGranularity::Week => (
dimension.iso_week_key.to_string(),
dimension.week_start_date_key,
dimension.week_end_date_key,
),
AnalyticsGranularity::Month => (
dimension.month_key.to_string(),
dimension.month_start_date_key,
dimension.month_end_date_key,
),
AnalyticsGranularity::Quarter => (
dimension.quarter_key.to_string(),
dimension.quarter_start_date_key,
dimension.quarter_end_date_key,
),
AnalyticsGranularity::Year => (
dimension.year_key.to_string(),
dimension.year_start_date_key,
dimension.year_end_date_key,
),
}
}
pub fn build_runtime_profile_task_config_record(
snapshot: RuntimeProfileTaskConfigSnapshot,
) -> RuntimeProfileTaskConfigRecord {

View File

@@ -116,6 +116,24 @@ pub fn build_runtime_profile_task_center_get_input(
Ok(RuntimeProfileTaskCenterGetInput { user_id })
}
pub fn build_analytics_metric_query_input(
event_key: String,
scope_kind: RuntimeTrackingScopeKind,
scope_id: String,
granularity: AnalyticsGranularity,
) -> Result<AnalyticsMetricQueryInput, RuntimeProfileFieldError> {
let event_key = normalize_required_string(event_key)
.ok_or(RuntimeProfileFieldError::MissingTaskEventKey)?;
let scope_id = normalize_required_string(scope_id)
.ok_or(RuntimeProfileFieldError::MissingTrackingScopeId)?;
Ok(AnalyticsMetricQueryInput {
event_key,
scope_kind,
scope_id,
granularity,
})
}
pub fn build_runtime_profile_task_claim_input(
user_id: String,
task_id: String,

View File

@@ -58,6 +58,78 @@ pub struct AnalyticsDateDimensionSnapshot {
pub year_end_date_key: i64,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum AnalyticsGranularity {
Day,
Week,
Month,
Quarter,
Year,
}
impl AnalyticsGranularity {
pub fn as_str(&self) -> &'static str {
match self {
Self::Day => "day",
Self::Week => "week",
Self::Month => "month",
Self::Quarter => "quarter",
Self::Year => "year",
}
}
pub fn from_client_str(value: &str) -> Option<Self> {
match value.trim().to_ascii_lowercase().as_str() {
"day" => Some(Self::Day),
"week" => Some(Self::Week),
"month" => Some(Self::Month),
"quarter" => Some(Self::Quarter),
"year" => Some(Self::Year),
_ => None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RuntimeAnalyticsDailyStatSnapshot {
pub event_key: String,
pub scope_kind: RuntimeTrackingScopeKind,
pub scope_id: String,
pub day_key: i64,
pub count: u32,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AnalyticsBucketMetric {
pub bucket_key: String,
pub bucket_start_date_key: i64,
pub bucket_end_date_key: i64,
pub value: u64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AnalyticsMetricQueryRequest {
pub event_key: String,
pub scope_kind: RuntimeTrackingScopeKind,
pub scope_id: String,
pub granularity: AnalyticsGranularity,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AnalyticsMetricQueryResponse {
pub buckets: Vec<AnalyticsBucketMetric>,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AnalyticsMetricQueryProcedureResult {
pub ok: bool,
pub buckets: Vec<AnalyticsBucketMetric>,
pub error_message: Option<String>,
}
/// 运行时平台主题。
///
/// 当前只冻结 light/dark 两种主题,避免各层散落字符串字面量。
@@ -552,6 +624,15 @@ pub struct RuntimeProfileTaskCenterGetInput {
pub user_id: String,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct AnalyticsMetricQueryInput {
pub event_key: String,
pub scope_kind: RuntimeTrackingScopeKind,
pub scope_id: String,
pub granularity: AnalyticsGranularity,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RuntimeProfileTaskClaimInput {

View File

@@ -0,0 +1,105 @@
use module_runtime::{
AnalyticsGranularity, RuntimeAnalyticsDailyStatSnapshot, RuntimeTrackingScopeKind,
aggregate_runtime_tracking_daily_stats,
};
fn stat(event_key: &str, scope_id: &str, day_key: i64, count: u32) -> RuntimeAnalyticsDailyStatSnapshot {
RuntimeAnalyticsDailyStatSnapshot {
event_key: event_key.to_string(),
scope_kind: RuntimeTrackingScopeKind::User,
scope_id: scope_id.to_string(),
day_key,
count,
}
}
#[test]
fn aggregates_daily_stats_by_iso_week_bucket() {
let buckets = aggregate_runtime_tracking_daily_stats(
vec![
stat("daily_login", "user-1", 20_517, 1), // 2026-03-05
stat("daily_login", "user-1", 20_518, 2), // 2026-03-06
stat("daily_login", "user-1", 20_524, 3), // 2026-03-12
stat("daily_login", "user-2", 20_517, 9),
],
"daily_login",
RuntimeTrackingScopeKind::User,
"user-1",
AnalyticsGranularity::Week,
);
assert_eq!(buckets.len(), 2);
assert_eq!(buckets[0].bucket_key, "202610");
assert_eq!(buckets[0].bucket_start_date_key, 20_514); // 2026-03-02
assert_eq!(buckets[0].bucket_end_date_key, 20_520); // 2026-03-08
assert_eq!(buckets[0].value, 3);
assert_eq!(buckets[1].bucket_key, "202611");
assert_eq!(buckets[1].bucket_start_date_key, 20_521); // 2026-03-09
assert_eq!(buckets[1].bucket_end_date_key, 20_527); // 2026-03-15
assert_eq!(buckets[1].value, 3);
}
#[test]
fn aggregates_daily_stats_by_month_quarter_and_year_bucket() {
let stats = vec![
stat("play", "user-1", 20_545, 2), // 2026-04-02
stat("play", "user-1", 20_573, 3), // 2026-04-30
stat("play", "user-1", 20_574, 5), // 2026-05-01
stat("play", "user-1", 20_818, 7), // 2026-12-31
];
let month = aggregate_runtime_tracking_daily_stats(
stats.clone(),
"play",
RuntimeTrackingScopeKind::User,
"user-1",
AnalyticsGranularity::Month,
);
assert_eq!(month.iter().map(|bucket| (&bucket.bucket_key, bucket.value)).collect::<Vec<_>>(), vec![(&"202604".to_string(), 5), (&"202605".to_string(), 5), (&"202612".to_string(), 7)]);
assert_eq!(month[0].bucket_start_date_key, 20_544);
assert_eq!(month[0].bucket_end_date_key, 20_573);
let quarter = aggregate_runtime_tracking_daily_stats(
stats.clone(),
"play",
RuntimeTrackingScopeKind::User,
"user-1",
AnalyticsGranularity::Quarter,
);
assert_eq!(quarter.iter().map(|bucket| (&bucket.bucket_key, bucket.value)).collect::<Vec<_>>(), vec![(&"20262".to_string(), 10), (&"20264".to_string(), 7)]);
let year = aggregate_runtime_tracking_daily_stats(
stats,
"play",
RuntimeTrackingScopeKind::User,
"user-1",
AnalyticsGranularity::Year,
);
assert_eq!(year.len(), 1);
assert_eq!(year[0].bucket_key, "2026");
assert_eq!(year[0].value, 17);
}
#[test]
fn day_granularity_keeps_each_day_bucket_and_filters_scope() {
let buckets = aggregate_runtime_tracking_daily_stats(
vec![
stat("daily_login", "user-1", 20_517, 1),
stat("daily_login", "user-1", 20_518, 2),
stat("daily_login", "user-2", 20_517, 9),
stat("other", "user-1", 20_517, 9),
],
"daily_login",
RuntimeTrackingScopeKind::User,
"user-1",
AnalyticsGranularity::Day,
);
assert_eq!(buckets.len(), 2);
assert_eq!(buckets[0].bucket_key, "2026-03-05");
assert_eq!(buckets[0].bucket_start_date_key, 20_517);
assert_eq!(buckets[0].bucket_end_date_key, 20_517);
assert_eq!(buckets[0].value, 1);
assert_eq!(buckets[1].bucket_key, "2026-03-06");
assert_eq!(buckets[1].value, 2);
}

View File

@@ -25,6 +25,11 @@ pub const TRACKING_SCOPE_KIND_SITE: &str = "site";
pub const TRACKING_SCOPE_KIND_WORK: &str = "work";
pub const TRACKING_SCOPE_KIND_MODULE: &str = "module";
pub const TRACKING_SCOPE_KIND_USER: &str = "user";
pub const ANALYTICS_GRANULARITY_DAY: &str = "day";
pub const ANALYTICS_GRANULARITY_WEEK: &str = "week";
pub const ANALYTICS_GRANULARITY_MONTH: &str = "month";
pub const ANALYTICS_GRANULARITY_QUARTER: &str = "quarter";
pub const ANALYTICS_GRANULARITY_YEAR: &str = "year";
pub const BROWSE_HISTORY_THEME_MODE_MARTIAL: &str = "martial";
pub const BROWSE_HISTORY_THEME_MODE_ARCANE: &str = "arcane";
pub const BROWSE_HISTORY_THEME_MODE_MACHINA: &str = "machina";
@@ -367,6 +372,30 @@ pub struct ProfileTaskConfigAdminListResponse {
pub entries: Vec<ProfileTaskConfigAdminResponse>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct AnalyticsMetricQueryRequest {
pub event_key: String,
pub scope_kind: String,
pub scope_id: String,
pub granularity: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct AnalyticsBucketMetricResponse {
pub bucket_key: String,
pub bucket_start_date_key: i64,
pub bucket_end_date_key: i64,
pub value: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct AnalyticsMetricQueryResponse {
pub buckets: Vec<AnalyticsBucketMetricResponse>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct AdminUpsertProfileTaskConfigRequest {

View File

@@ -136,6 +136,7 @@ use module_puzzle::{
use module_runtime::{
RuntimeBrowseHistoryRecord, RuntimePlatformTheme as DomainRuntimePlatformTheme,
RuntimeProfileDashboardRecord, RuntimeProfileInviteCodeRecord, RuntimeProfilePlayStatsRecord,
AnalyticsMetricQueryResponse as DomainAnalyticsMetricQueryResponse,
RuntimeProfileRechargeCenterRecord, RuntimeProfileRechargeOrderRecord,
RuntimeProfileRedeemCodeMode as DomainRuntimeProfileRedeemCodeMode,
RuntimeProfileRedeemCodeRecord, RuntimeProfileRewardCodeRedeemRecord,
@@ -167,6 +168,7 @@ use module_runtime::{
build_runtime_profile_wallet_adjustment_input,
build_runtime_profile_wallet_ledger_entry_record,
build_runtime_profile_wallet_ledger_list_input, build_runtime_referral_invite_center_get_input,
build_analytics_metric_query_input,
build_runtime_referral_invite_center_record, build_runtime_referral_redeem_input,
build_runtime_referral_redeem_record, build_runtime_setting_get_input,
build_runtime_setting_record, build_runtime_setting_upsert_input,

View File

@@ -181,6 +181,17 @@ impl From<module_runtime::RuntimeProfileTaskCenterGetInput> for RuntimeProfileTa
}
}
impl From<module_runtime::AnalyticsMetricQueryInput> for AnalyticsMetricQueryInput {
fn from(input: module_runtime::AnalyticsMetricQueryInput) -> Self {
Self {
event_key: input.event_key,
scope_kind: map_runtime_tracking_scope_kind(input.scope_kind),
scope_id: input.scope_id,
granularity: map_analytics_granularity(input.granularity),
}
}
}
impl From<module_runtime::RuntimeProfileTaskClaimInput> for RuntimeProfileTaskClaimInput {
fn from(input: module_runtime::RuntimeProfileTaskClaimInput) -> Self {
Self {
@@ -899,6 +910,22 @@ pub(crate) fn map_runtime_profile_task_center_procedure_result(
))
}
pub(crate) fn map_analytics_metric_query_procedure_result(
result: AnalyticsMetricQueryProcedureResult,
) -> Result<DomainAnalyticsMetricQueryResponse, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
Ok(DomainAnalyticsMetricQueryResponse {
buckets: result
.buckets
.into_iter()
.map(map_analytics_bucket_metric)
.collect(),
})
}
pub(crate) fn map_runtime_profile_task_claim_procedure_result(
result: RuntimeProfileTaskClaimProcedureResult,
) -> Result<RuntimeProfileTaskClaimRecord, SpacetimeClientError> {
@@ -1751,6 +1778,17 @@ pub(crate) fn map_runtime_profile_dashboard_snapshot(
}
}
pub(crate) fn map_analytics_bucket_metric(
bucket: AnalyticsBucketMetric,
) -> module_runtime::AnalyticsBucketMetric {
module_runtime::AnalyticsBucketMetric {
bucket_key: bucket.bucket_key,
bucket_start_date_key: bucket.bucket_start_date_key,
bucket_end_date_key: bucket.bucket_end_date_key,
value: bucket.value,
}
}
pub(crate) fn map_runtime_profile_wallet_ledger_entry_snapshot(
snapshot: RuntimeProfileWalletLedgerEntrySnapshot,
) -> module_runtime::RuntimeProfileWalletLedgerEntrySnapshot {
@@ -4012,6 +4050,18 @@ pub(crate) fn map_runtime_profile_wallet_ledger_source_type_back(
}
}
pub(crate) fn map_analytics_granularity(
granularity: module_runtime::AnalyticsGranularity,
) -> AnalyticsGranularity {
match granularity {
module_runtime::AnalyticsGranularity::Day => AnalyticsGranularity::Day,
module_runtime::AnalyticsGranularity::Week => AnalyticsGranularity::Week,
module_runtime::AnalyticsGranularity::Month => AnalyticsGranularity::Month,
module_runtime::AnalyticsGranularity::Quarter => AnalyticsGranularity::Quarter,
module_runtime::AnalyticsGranularity::Year => AnalyticsGranularity::Year,
}
}
pub(crate) fn map_runtime_tracking_scope_kind(
value: DomainRuntimeTrackingScopeKind,
) -> crate::module_bindings::RuntimeTrackingScopeKind {

View File

@@ -0,0 +1,18 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct AnalyticsBucketMetric {
pub bucket_key: String,
pub bucket_start_date_key: i64,
pub bucket_end_date_key: i64,
pub value: u64,
}
impl __sdk::InModule for AnalyticsBucketMetric {
type Module = super::RemoteModule;
}

View File

@@ -0,0 +1,24 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
#[derive(Copy, Eq, Hash)]
pub enum AnalyticsGranularity {
Day,
Week,
Month,
Quarter,
Year,
}
impl __sdk::InModule for AnalyticsGranularity {
type Module = super::RemoteModule;
}

View File

@@ -0,0 +1,21 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
use super::analytics_granularity_type::AnalyticsGranularity;
use super::runtime_tracking_scope_kind_type::RuntimeTrackingScopeKind;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct AnalyticsMetricQueryInput {
pub event_key: String,
pub scope_kind: RuntimeTrackingScopeKind,
pub scope_id: String,
pub granularity: AnalyticsGranularity,
}
impl __sdk::InModule for AnalyticsMetricQueryInput {
type Module = super::RemoteModule;
}

View File

@@ -0,0 +1,19 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
use super::analytics_bucket_metric_type::AnalyticsBucketMetric;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct AnalyticsMetricQueryProcedureResult {
pub ok: bool,
pub buckets: Vec<AnalyticsBucketMetric>,
pub error_message: Option<String>,
}
impl __sdk::InModule for AnalyticsMetricQueryProcedureResult {
type Module = super::RemoteModule;
}

View File

@@ -690,6 +690,12 @@ pub mod user_account_type;
pub mod user_browse_history_table;
pub mod user_browse_history_type;
pub mod analytics_bucket_metric_type;
pub mod analytics_granularity_type;
pub mod analytics_metric_query_input_type;
pub mod analytics_metric_query_procedure_result_type;
pub mod query_analytics_metric_procedure;
pub use accept_quest_reducer::accept_quest;
pub use acknowledge_quest_completion_reducer::acknowledge_quest_completion;
pub use admin_disable_profile_redeem_code_procedure::admin_disable_profile_redeem_code;
@@ -736,6 +742,10 @@ pub use analytics_date_dimension_ensure_input_type::AnalyticsDateDimensionEnsure
pub use analytics_date_dimension_seed_input_type::AnalyticsDateDimensionSeedInput;
pub use analytics_date_dimension_table::*;
pub use analytics_date_dimension_type::AnalyticsDateDimension;
pub use analytics_bucket_metric_type::AnalyticsBucketMetric;
pub use analytics_granularity_type::AnalyticsGranularity;
pub use analytics_metric_query_input_type::AnalyticsMetricQueryInput;
pub use analytics_metric_query_procedure_result_type::AnalyticsMetricQueryProcedureResult;
pub use append_ai_text_chunk_and_return_procedure::append_ai_text_chunk_and_return;
pub use apply_chapter_progression_ledger_entry_and_return_procedure::apply_chapter_progression_ledger_entry_and_return;
pub use apply_chapter_progression_ledger_entry_reducer::apply_chapter_progression_ledger_entry;
@@ -978,6 +988,7 @@ pub use get_profile_play_stats_procedure::get_profile_play_stats;
pub use get_profile_recharge_center_procedure::get_profile_recharge_center;
pub use get_profile_referral_invite_center_procedure::get_profile_referral_invite_center;
pub use get_profile_task_center_procedure::get_profile_task_center;
pub use query_analytics_metric_procedure::query_analytics_metric;
pub use get_puzzle_agent_session_procedure::get_puzzle_agent_session;
pub use get_puzzle_gallery_detail_procedure::get_puzzle_gallery_detail;
pub use get_puzzle_run_procedure::get_puzzle_run;

View File

@@ -0,0 +1,59 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
use super::analytics_metric_query_input_type::AnalyticsMetricQueryInput;
use super::analytics_metric_query_procedure_result_type::AnalyticsMetricQueryProcedureResult;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
struct QueryAnalyticsMetricArgs {
pub input: AnalyticsMetricQueryInput,
}
impl __sdk::InModule for QueryAnalyticsMetricArgs {
type Module = super::RemoteModule;
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the procedure `query_analytics_metric`.
///
/// Implemented for [`super::RemoteProcedures`].
pub trait query_analytics_metric {
fn query_analytics_metric(&self, input: AnalyticsMetricQueryInput) {
self.query_analytics_metric_then(input, |_, _| {});
}
fn query_analytics_metric_then(
&self,
input: AnalyticsMetricQueryInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<AnalyticsMetricQueryProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
impl query_analytics_metric for super::RemoteProcedures {
fn query_analytics_metric_then(
&self,
input: AnalyticsMetricQueryInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<AnalyticsMetricQueryProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, AnalyticsMetricQueryProcedureResult>(
"query_analytics_metric",
QueryAnalyticsMetricArgs { input },
__callback,
);
}
}

View File

@@ -348,6 +348,31 @@ impl SpacetimeClient {
.await
}
pub async fn query_analytics_metric(
&self,
event_key: String,
scope_kind: DomainRuntimeTrackingScopeKind,
scope_id: String,
granularity: module_runtime::AnalyticsGranularity,
) -> Result<DomainAnalyticsMetricQueryResponse, SpacetimeClientError> {
let procedure_input =
build_analytics_metric_query_input(event_key, scope_kind, scope_id, granularity)
.map_err(SpacetimeClientError::validation_failed)?
.into();
self.call_after_connect(move |connection, sender| {
connection
.procedures()
.query_analytics_metric_then(procedure_input, move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_analytics_metric_query_procedure_result);
send_once(&sender, mapped);
});
})
.await
}
pub async fn admin_list_profile_task_configs(
&self,
admin_user_id: String,

View File

@@ -471,6 +471,27 @@ pub fn list_profile_wallet_ledger(
}
}
// analytics metric 查询直接聚合 tracking_daily_stat避免 API 层订阅全量表后自行汇总。
#[spacetimedb::procedure]
pub fn query_analytics_metric(
ctx: &mut ProcedureContext,
input: AnalyticsMetricQueryInput,
) -> AnalyticsMetricQueryProcedureResult {
match ctx.try_with_tx(|tx| query_analytics_metric_buckets(tx, input.clone())) {
Ok(buckets) => AnalyticsMetricQueryProcedureResult {
ok: true,
buckets,
error_message: None,
},
Err(message) => AnalyticsMetricQueryProcedureResult {
ok: false,
buckets: Vec::new(),
error_message: Some(message),
},
}
}
// 任务中心读取会顺手记录当日登录埋点,确保“每日登录”只依赖后端事实。
#[spacetimedb::procedure]
pub fn get_profile_task_center(
@@ -2726,6 +2747,45 @@ fn build_profile_task_center_snapshot(
})
}
fn query_analytics_metric_buckets(
ctx: &ReducerContext,
input: AnalyticsMetricQueryInput,
) -> Result<Vec<AnalyticsBucketMetric>, String> {
let validated_input = build_analytics_metric_query_input(
input.event_key,
input.scope_kind,
input.scope_id,
input.granularity,
)
.map_err(|error| error.to_string())?;
let stats = ctx
.db
.tracking_daily_stat()
.iter()
.filter(|row| {
row.event_key.trim() == validated_input.event_key
&& row.scope_kind == validated_input.scope_kind
&& row.scope_id.trim() == validated_input.scope_id
})
.map(|row| RuntimeAnalyticsDailyStatSnapshot {
event_key: row.event_key,
scope_kind: row.scope_kind,
scope_id: row.scope_id,
day_key: row.day_key,
count: row.count,
})
.collect::<Vec<_>>();
Ok(aggregate_runtime_tracking_daily_stats(
stats,
&validated_input.event_key,
validated_input.scope_kind,
&validated_input.scope_id,
validated_input.granularity,
))
}
fn refresh_profile_task_progress(
ctx: &ReducerContext,
user_id: &str,