docs: add shared admin backoffice skill
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
# 本次后台表查询接入的可复用经验
|
||||
|
||||
## 需求落点
|
||||
- 后台“总览”页的表统计仍保留,只把每张表的表名改成可点击跳转到 `#tables?table=<name>`。
|
||||
- 新增独立 `#tables` 页承载表选择、关键词搜索、JSON filters、limit、行详情弹窗。
|
||||
|
||||
## 后端实现要点
|
||||
- 新增只读接口:
|
||||
- `GET /admin/api/database/tables`
|
||||
- `GET /admin/api/database/tables/{table_name}/rows`
|
||||
- 表名必须来自 schema 白名单;再加一层 identifier 校验,避免任意 SQL 表名注入。
|
||||
- `limit` 必须 clamp;本次实现使用默认 100、最大 500。
|
||||
- `search` / `filters` 不进入 SQL 字符串:
|
||||
- SQL 只负责 `SELECT * FROM {table_name} LIMIT {limit}`
|
||||
- 返回后在 api-server 内存中过滤
|
||||
- `filters` 仅接受 JSON object,按列名匹配;非 object 直接 400
|
||||
- SpacetimeDB HTTP SQL 返回可能是 statement array + rows,解析时要兼容这一层结构。
|
||||
|
||||
## 前端实现要点
|
||||
- `adminRoutes` 必须新增 `tables`,`AdminShell.routeIcons` 也要同步覆盖。
|
||||
- `AdminApp` 需要显式渲染 `AdminDatabaseTablesPage`。
|
||||
- worktree 下可能没有本地 `node_modules/typescript/bin/tsc`,而根目录有依赖;在验证前可以临时把根目录 `node_modules` 软链到 worktree 再执行 `npm run admin-web:typecheck`,验证后删除软链,避免污染 git 状态。
|
||||
|
||||
## 验证结果
|
||||
- `cargo test -p api-server admin_database -- --nocapture` 通过。
|
||||
- `cargo fmt --manifest-path Cargo.toml -p api-server -p shared-contracts --check` 通过。
|
||||
- `npm run admin-web:typecheck` 通过。
|
||||
- `npm run admin-web:build` 通过。
|
||||
- `npm run check:encoding` 通过。
|
||||
@@ -0,0 +1,53 @@
|
||||
# 后台埋点数据页与本地启动验证记录(2026-05-07)
|
||||
|
||||
## 背景
|
||||
|
||||
本次在 Genarrative/百梦后台新增“埋点数据”页:
|
||||
|
||||
- 后端新增 `GET /admin/api/tracking/events`。
|
||||
- shared-contracts 新增 admin tracking query/list/entry DTO。
|
||||
- 前端新增 `#tracking` 路由、导航、表格、详情面板与 `.xls` 导出。
|
||||
- 导出使用浏览器端 HTML table + Excel MIME,不引入 `xlsx` 依赖。
|
||||
|
||||
## 关键实现点
|
||||
|
||||
- 后台只读接口仍必须套 `require_admin_auth`。
|
||||
- SpacetimeDB 明细读取使用 HTTP SQL,不新增 reducer、不改 schema。
|
||||
- SQL 固定白名单列,不用 `SELECT *`。
|
||||
- Query 只允许 `eventKey/userId/scopeKind/scopeId/limit`。
|
||||
- `scopeKind` 只允许 `site/work/module/user`。
|
||||
- limit 默认 200,最大 1000。
|
||||
- SpacetimeDB HTTP SQL 响应要兼容 statement array + `rows`,Option 可能表现为 `{ "some": value }`。
|
||||
- 前端导出 `.xls` 时给单元格加 `mso-number-format:'\\@';`,防止 Excel 把 ID 转科学计数法。
|
||||
|
||||
## 验证命令
|
||||
|
||||
```bash
|
||||
cd <repo-root>/.worktrees/hermes-996d586b
|
||||
npm install # 若 node_modules 缺失
|
||||
npm run admin-web:typecheck
|
||||
npm run admin-web:build
|
||||
npm run check:encoding
|
||||
|
||||
cd server-rs
|
||||
cargo fmt -p api-server -p shared-contracts --check
|
||||
cargo test -p api-server admin_tracking -- --nocapture
|
||||
```
|
||||
|
||||
## 本地启动观察
|
||||
|
||||
启动命令:
|
||||
|
||||
```bash
|
||||
cd <repo-root>/.worktrees/hermes-996d586b
|
||||
npm run api-server
|
||||
npm run admin-web:dev -- --host 127.0.0.1
|
||||
```
|
||||
|
||||
实际验证:
|
||||
|
||||
- api-server 监听 `127.0.0.1:3100`,健康检查为 `http://127.0.0.1:3100/healthz`。
|
||||
- admin-web 监听 `127.0.0.1:5173`,后台地址为 `http://127.0.0.1:5173/admin/`。
|
||||
- 请求 `http://127.0.0.1:5173/` 会 302 到 `/admin/`。
|
||||
- 不能默认用 3200 检查 api-server;本地脚本通过 `GENARRATIVE_API_PORT=3100` 启动。
|
||||
- 如果启动日志出现 SpacetimeDB `127.0.0.1:3101` connection refused,api-server 仍可能已正常监听;这是依赖的本地 SpacetimeDB 未启动,埋点页读真实数据会受影响。
|
||||
@@ -0,0 +1,55 @@
|
||||
# 真实登录成功链路接入每日登录埋点(2026-05-08)
|
||||
|
||||
## 背景
|
||||
|
||||
后台“埋点数据”页要能看到真实登录产生的 `daily_login`。此前已完成方案 A:把“读取任务中心时顺手写每日登录埋点”拆成独立 SpacetimeDB procedure/client 方法,避免后台查看或刷新任务中心污染登录数据。
|
||||
|
||||
闭环时不要再把写入点放回任务中心读取流程;应在认证成功且会话签发后显式调用每日登录埋点入口。
|
||||
|
||||
## 推荐接入点
|
||||
|
||||
在 `api-server` 认证成功路径中,先创建/签发会话,再非阻断记录埋点,再同步认证快照并返回:
|
||||
|
||||
1. `create_auth_session` / `create_password_auth_session` 成功。
|
||||
2. 调用统一 helper:`record_daily_login_tracking_event_after_auth_success(...)`。
|
||||
3. helper 调用 `state.spacetime_client().record_daily_login_tracking_event(user_id.to_string()).await`。
|
||||
4. 成功写 `info`,失败写 `warn`,不能把埋点失败返回给用户。
|
||||
|
||||
已验证的真实登录链路包括:
|
||||
|
||||
- 手机验证码登录:`server-rs/crates/api-server/src/phone_auth.rs` 的 `phone_login`。
|
||||
- 密码登录入口:`server-rs/crates/api-server/src/password_entry.rs` 的 `password_entry`。
|
||||
- 重置密码后自动登录:`server-rs/crates/api-server/src/password_management.rs` 的 `reset_password`。
|
||||
- 微信 OAuth 回调登录:`server-rs/crates/api-server/src/wechat_auth.rs` 的 `handle_wechat_callback`。
|
||||
- 微信绑定手机号后自动登录:`server-rs/crates/api-server/src/wechat_auth.rs` 的 `bind_wechat_phone`。
|
||||
- refresh cookie 续期:`server-rs/crates/api-server/src/refresh_session.rs` 的 `refresh_session`。在 `rotate_session` 成功并签发新 access token 后记录,`login_method` 应使用 `rotated.session.issued_by_provider.clone()`,不要固定写成 Password。
|
||||
|
||||
## 关键实现约束
|
||||
|
||||
- 埋点是运营数据,必须保持非阻断:SpacetimeDB 调用失败只记录日志,不影响登录成功返回。
|
||||
- helper 建议放在 `auth_session.rs`,避免各登录 handler 重复错误处理。
|
||||
- refresh cookie 续期也被产品视为一次每日登录触发;接入 `refresh_session.rs` 时必须放在 token rotate 和 access token 签发成功之后,且保持非阻断,避免刷新失败或缺 cookie 时误写埋点。
|
||||
- `handle_wechat_callback` 如果要记录 `request_id/operation`,需要在 handler 参数中补 `Extension<RequestContext>`;确认路由层已注入 RequestContext。
|
||||
- 单元测试默认不启动 SpacetimeDB。若直接调用真实 `spacetime_client` 会让既有认证测试依赖外部服务;可在 `#[cfg(test)]` 下让 helper no-op,仅用编译和现有登录成功测试覆盖调用点不破坏返回。
|
||||
- 后续如需要严格断言“helper 被调用”,应优先为 Spacetime client 引入可注入 trait/mock,而不是让 API 单测连接真实 SpacetimeDB。
|
||||
|
||||
## 验证命令
|
||||
|
||||
```bash
|
||||
cd server-rs
|
||||
cargo fmt -p api-server --check
|
||||
cargo check -p api-server
|
||||
cargo check -p spacetime-client
|
||||
cargo test -p api-server auth_session -- --nocapture
|
||||
cargo test -p api-server refresh_session_rotates_cookie_and_returns_new_access_token -- --nocapture
|
||||
cargo test -p api-server password_entry_logs_in_existing_phone_user_and_sets_refresh_cookie -- --nocapture
|
||||
cargo test -p api-server phone_login_creates_user_and_sets_refresh_cookie -- --nocapture
|
||||
cd ..
|
||||
npm run check:encoding
|
||||
git diff --check
|
||||
```
|
||||
|
||||
## 提交注意
|
||||
|
||||
- 不要提交 `.env.local`、`.env.secrets.local` 或任何 token/密码/连接串。
|
||||
- 若工作区里有本地敏感文件,只提交明确改动的 Rust 文件和 `docs/technical/*` 文档。
|
||||
@@ -0,0 +1,97 @@
|
||||
# Genarrative daily_login 埋点触发点排查记录
|
||||
|
||||
## 背景
|
||||
|
||||
用户在后台“埋点数据”页看到 `daily_login` 事件后询问:为什么每日登录埋点看起来只有在用户领取每日登录任务奖励后才记录,而不是登录时记录。
|
||||
|
||||
## 结论
|
||||
|
||||
当前代码口径里,`daily_login` 不是认证登录成功瞬间写入的事件。它挂在个人任务链路:
|
||||
|
||||
- `GET /api/profile/tasks`:读取任务中心时会记录当日 `daily_login`,并刷新任务进度。
|
||||
- `POST /api/profile/tasks/{task_id}/claim`:领取任务奖励时,如果任务配置是 daily_login,会兜底记录当日 `daily_login`。
|
||||
|
||||
因为 `record_daily_login_tracking_event` 用 `daily-login:<user_id>:<day_key>` 作为 event id,并先查重,所以同一用户同一北京自然日最多写一条。
|
||||
|
||||
## 关键文件
|
||||
|
||||
- `docs/technical/PROFILE_TASK_AND_TRACKING_SYSTEM_2026-05-03.md`
|
||||
- 第 47 行左右写明:用户打开任务中心时后端幂等记录当日 `daily_login`;点击领取时校验进度和领奖记录。
|
||||
- 接口说明中写明 `GET /api/profile/tasks` 会读取任务中心并记录当日登录埋点。
|
||||
- `server-rs/crates/api-server/src/runtime_profile.rs`
|
||||
- `get_profile_task_center` 调用 `state.spacetime_client().get_profile_task_center(user_id)`。
|
||||
- `claim_profile_task_reward` 调用 `state.spacetime_client().claim_profile_task_reward(user_id, task_id)`。
|
||||
- `server-rs/crates/spacetime-module/src/runtime/profile.rs`
|
||||
- `get_profile_task_center_snapshot(..., record_login_event: bool)` 在 `record_login_event` 为 true 时调用 `record_daily_login_tracking_event`。
|
||||
- `claim_profile_task_reward_record` 对 daily_login 任务调用 `record_daily_login_tracking_event` 作为兜底。
|
||||
- `record_daily_login_tracking_event` 负责生成 event id、查重、写入 `tracking_event` 和更新 `tracking_daily_stat`。
|
||||
- `server-rs/crates/api-server/src/phone_auth.rs`
|
||||
- 手机号登录成功后做验证码校验、新用户奖励、邀请码绑定、session 签发、认证快照同步;当前没有写入 `daily_login`,也没有调用任务中心接口。
|
||||
|
||||
## 排查方法
|
||||
|
||||
1. 不要只看后台埋点页。先搜事件 key 和任务接口:
|
||||
|
||||
```bash
|
||||
git grep -n "daily_login\|tracking_event\|get_profile_task_center\|claim_profile_task_reward" -- server-rs apps docs
|
||||
```
|
||||
|
||||
2. 对照设计文档中的事件口径:
|
||||
|
||||
```bash
|
||||
sed -n '35,58p' docs/technical/PROFILE_TASK_AND_TRACKING_SYSTEM_2026-05-03.md
|
||||
```
|
||||
|
||||
3. 追 API handler 到 SpacetimeDB reducer:
|
||||
|
||||
- `api-server/src/runtime_profile.rs`
|
||||
- `spacetime-client` 对应 procedure wrapper
|
||||
- `spacetime-module/src/runtime/profile.rs`
|
||||
|
||||
4. 再看真实登录接口是否写入同一事件。手机号登录入口是:
|
||||
|
||||
- `server-rs/crates/api-server/src/phone_auth.rs::phone_login`
|
||||
|
||||
## 常见误判
|
||||
|
||||
- 后台只是在展示 `tracking_event`,不是事件产生点。
|
||||
- “每日登录”这个中文名容易让人以为它必然在 auth 登录成功时写入;当前实现不是这样。
|
||||
- 如果用户登录后没有打开“我的/任务中心”,只在领奖时触发 claim 接口,就会表现为“领奖时才出现埋点”。
|
||||
- 领取接口里的写入是兜底,避免用户直接点击领取时因为未先打开任务中心而无法完成每日任务。
|
||||
|
||||
## 后续方案A落地记录
|
||||
|
||||
在后续修复中,采用“方案A”:把“读取任务中心时顺手记录每日登录埋点”拆成独立 SpacetimeDB procedure,使任务中心读取只负责读取/刷新进度,避免后台查看或刷新任务中心时污染埋点数据。
|
||||
|
||||
关键变化:
|
||||
|
||||
- `server-rs/crates/module-runtime/src/domain.rs`
|
||||
- 新增 `RuntimeTrackingEventProcedureResult { ok, error_message }`,用于返回纯事件写入结果。
|
||||
- `server-rs/crates/spacetime-module/src/runtime/profile.rs`
|
||||
- 新增 `record_daily_login_tracking_event_and_return(ctx, input)` procedure。
|
||||
- `get_profile_task_center` 注释和行为调整为只读取/刷新任务进度,不再作为每日登录埋点产生点。
|
||||
- `server-rs/crates/spacetime-client/src/runtime.rs`
|
||||
- 新增 `record_daily_login_tracking_event(user_id)` client 方法,调用新 procedure。
|
||||
- `server-rs/crates/spacetime-client/src/mapper.rs`
|
||||
- 新增 `map_runtime_tracking_event_procedure_result`,把 `ok=false` 映射为 `procedure_failed`。
|
||||
|
||||
落地注意:
|
||||
|
||||
- 这一步只拆出后端写入入口,不等于所有登录方式已经接入;接入手机号/微信/密码等认证成功链路前,需确认产品口径:统计登录成功是否应覆盖所有登录方式,以及事件失败是否阻断登录。
|
||||
- 修改 `spacetime-module` procedure 后,通常需要重新生成/同步 SpacetimeDB 绑定;若直接手补 `spacetime-client/src/module_bindings`,要非常谨慎,因为该目录声明为自动生成。
|
||||
- patch 工具可能对 Rust 单文件使用 2015 edition lint,看到 `async fn is not permitted in Rust 2015` 时不要立即按该误报改代码,应以 `cd server-rs && cargo test/check ...` 为准。
|
||||
|
||||
验证记录:
|
||||
|
||||
```bash
|
||||
cd server-rs
|
||||
cargo test -p module-runtime runtime_profile_task_status_matches_progress_and_claim -- --nocapture
|
||||
```
|
||||
|
||||
该测试通过可验证任务中心领域进度/领取逻辑未被破坏;完整接入认证链路后还应补 api-server 层登录成功埋点测试。
|
||||
|
||||
## 后续设计建议
|
||||
|
||||
如果产品口径要求“登录成功就算每日登录”,应把 `daily_login` 写入点前移到统一 auth 登录成功链路,并覆盖手机号/微信/密码等登录方式;任务中心只读取进度或最多保留幂等兜底。
|
||||
|
||||
如果需要同时分析真实登录和任务完成,建议新增独立事件,例如 `auth_login_success` / `user_login_success`,让 `daily_login` 继续表示每日任务完成条件。
|
||||
@@ -0,0 +1,37 @@
|
||||
# 本地 private table SQL 权限修复
|
||||
|
||||
场景:
|
||||
- 后台或 api-server 通过 SpacetimeDB HTTP SQL 读取 `tracking_event` 这类 private table。
|
||||
- 本地清库、重建 standalone 或重新发布模块后,原 CLI token 失效,SQL 可能报 `no such table ... If the table exists, it may be marked private`。
|
||||
|
||||
操作步骤:
|
||||
|
||||
1. 清空本地 SpacetimeDB 数据目录
|
||||
- 使用:`spacetime --root-dir=<root> server clear --yes`
|
||||
- 只清本地开发环境,不要误伤远端或其他 worktree。
|
||||
|
||||
2. 启动本地 standalone
|
||||
- 用项目约定的 `scripts/dev-rust-stack.sh` 或等价命令启动 `spacetime`。
|
||||
- 确认 `/v1/ping` 可访问后再取 identity。
|
||||
|
||||
3. 通过 `/v1/identity` 获取新 token 和 identity
|
||||
- 使用 `POST http://127.0.0.1:3101/v1/identity`
|
||||
- 只记录 identity,不要在日志中打印 token 明文。
|
||||
|
||||
4. 用新 token 登录 CLI
|
||||
- 运行:`spacetime --root-dir=<root> login --token <token>`
|
||||
- 这会把 token 写到本地 CLI 配置,后续 HTTP SQL 可读 private table。
|
||||
|
||||
5. 重新验证 SQL
|
||||
- 使用带 token 的 `POST /v1/database/<db>/sql`
|
||||
- 先尝试 `SELECT ... FROM tracking_event LIMIT 1`
|
||||
- 若成功,再让 api-server 走同样 token。
|
||||
|
||||
6. 如果 api-server 需要复用 token
|
||||
- 优先读取项目内本地 CLI 配置中的 token,而不是硬编码或回填到 `.env`。
|
||||
- 输出日志时统一 `[REDACTED]`。
|
||||
|
||||
排查要点:
|
||||
- `ORDER BY` 和 private table 是两个独立问题,先分开修。
|
||||
- 清库后旧 token 很可能不再能看见 private table,不代表表不存在。
|
||||
- 若 `/v1/identity` 返回的 token 没权限,再检查当前 standalone 是否就是刚启动的本地实例、database 名是否一致、模块是否已重新发布。
|
||||
@@ -0,0 +1,43 @@
|
||||
# SpacetimeDB HTTP SQL SATS 值后台展示处理
|
||||
|
||||
本参考用于 Genarrative 后台通过 SpacetimeDB HTTP SQL 读取表明细并展示/导出时,处理 SQL rows 中的 SATS 原始 JSON 值。
|
||||
|
||||
## 典型现象
|
||||
|
||||
读取 private table(例如 `tracking_event`)后,HTTP SQL 可能返回如下原始形态:
|
||||
|
||||
- enum:`RuntimeTrackingScopeKind::User` 返回 `[3, []]`
|
||||
- `Option<String>::Some("user_00000001")` 返回 `[0, "user_00000001"]`
|
||||
- `Option<String>::None` 返回 `[1, []]`
|
||||
- `Timestamp` 返回 `[1778207451731746]`
|
||||
|
||||
如果直接 `value.to_string()` 展示,后台会出现 `[3,[]]`、`[0,"..."]`、`[1,[]]`、`[1778207451731746]`,运营不可读。
|
||||
|
||||
## 推荐处理
|
||||
|
||||
1. 后端解析层优先标准化:
|
||||
- Option:`[0, value] -> value`,`[1, []] -> None`
|
||||
- enum:按生成 binding 的 variant 顺序映射,例如 `RuntimeTrackingScopeKind` 为 `site/work/module/user`,索引 `0/1/2/3` 分别对应这些字符串
|
||||
- Timestamp:单元素数组 `[micros] -> "micros"`
|
||||
2. 前端展示层再格式化时间:
|
||||
- 纯数字时间戳按微秒处理:`Date(Math.floor(micros / 1000))`
|
||||
- ISO 字符串用 `new Date(value)`
|
||||
- 展示为 `YYYY-MM-DD HH:mm:ss`
|
||||
3. 列表、详情弹窗、Excel 导出必须使用同一套格式化结果,避免导出仍残留 SATS 原始值。
|
||||
4. 增加单测覆盖 SATS 原始 rows,至少断言:
|
||||
- `[3, []] -> user`
|
||||
- `[0, "user"] -> Some("user")`
|
||||
- `[1, []] -> None`
|
||||
- `[1778207451731746] -> "1778207451731746"`
|
||||
|
||||
## 验收建议
|
||||
|
||||
- `cargo test -p api-server admin_tracking -- --nocapture`
|
||||
- `npm run admin-web:typecheck`
|
||||
- `npm run admin-web:build`
|
||||
- `npm run check:encoding`
|
||||
- `git diff --check`
|
||||
|
||||
## 注意
|
||||
|
||||
不同 enum 的 variant 顺序必须以生成 binding 或 module 源码为准,不能复用其他 enum 的索引映射。
|
||||
Reference in New Issue
Block a user