From f343555a19b43e5d95b2c0429ad5d1ccdce23167 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8E=86=E5=86=B0=E9=83=81-hermes=E7=89=88?= Date: Fri, 8 May 2026 14:00:04 +0800 Subject: [PATCH] feat: record daily login tracking on auth success --- ..._LOGIN_TRACKING_AUTH_CLOSURE_2026-05-08.md | 63 +++++++++++++++++++ .../crates/api-server/src/auth_session.rs | 41 ++++++++++++ .../crates/api-server/src/password_entry.rs | 12 +++- .../api-server/src/password_management.rs | 8 +++ server-rs/crates/api-server/src/phone_auth.rs | 8 +++ .../crates/api-server/src/wechat_auth.rs | 16 +++++ 6 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 docs/technical/DAILY_LOGIN_TRACKING_AUTH_CLOSURE_2026-05-08.md diff --git a/docs/technical/DAILY_LOGIN_TRACKING_AUTH_CLOSURE_2026-05-08.md b/docs/technical/DAILY_LOGIN_TRACKING_AUTH_CLOSURE_2026-05-08.md new file mode 100644 index 00000000..ba2462e3 --- /dev/null +++ b/docs/technical/DAILY_LOGIN_TRACKING_AUTH_CLOSURE_2026-05-08.md @@ -0,0 +1,63 @@ +# 登录成功每日登录埋点闭环方案(2026-05-08) + +## 背景 + +后台“埋点数据”需要能看到真实登录触发的 `daily_login` 埋点。此前方案 A 已把“读取任务中心时顺手写每日登录埋点”拆成独立 SpacetimeDB procedure: + +- `record_daily_login_tracking_event_and_return` +- `spacetime-client` 方法:`record_daily_login_tracking_event(user_id)` + +但认证成功链路还没有调用该方法,因此当前只完成了“任务中心读取不污染登录埋点”,没有完成“用户真实登录写入每日登录埋点”。 + +## 目标 + +在用户认证成功并创建 refresh session / access token 后,异步尝试写入每日登录埋点。 + +覆盖入口: + +- 手机验证码登录:`POST /api/auth/phone/login` +- 密码入口登录:`POST /api/auth/entry` +- 重置密码后自动登录:`POST /api/auth/password/reset` +- 微信 OAuth callback 登录:`GET /api/auth/wechat/callback` +- 微信绑定手机号后激活/登录态刷新:`POST /api/auth/wechat/bind-phone` + +## 设计约束 + +1. 埋点写入不能阻断登录成功响应。 +2. 只有认证成功并已创建会话后才记录。 +3. 失败只记 warning,继续返回 token / cookie。 +4. 写入统一收口,避免多个登录 handler 各自拼 procedure 调用。 +5. 不修改 SpacetimeDB 表结构,不需要更新 `migration.rs`。 + +## 实现方案 + +新增 `api-server` 内部 helper: + +```rust +record_daily_login_tracking_event_after_auth_success( + state: &AppState, + request_context: &RequestContext, + user_id: &str, + login_method: AuthLoginMethod, +).await; +``` + +该 helper: + +- 调用 `state.spacetime_client().record_daily_login_tracking_event(user_id.to_string()).await` +- 成功时记录 info +- 失败时记录 warn,并明确“登录流程继续” + +在各登录入口 `create_auth_session` 成功后调用该 helper。 + +## 验收 + +- `cargo test -p api-server auth_session -- --nocapture` +- `cargo check -p api-server` +- `cargo check -p spacetime-client` +- `npm run check:encoding` +- `git diff --check` + +## 注意 + +`npm run dev` 是长期运行进程;如需本地 smoke,应启动后用 `/healthz` 和后台页面验证,不要等待该命令退出。 diff --git a/server-rs/crates/api-server/src/auth_session.rs b/server-rs/crates/api-server/src/auth_session.rs index e27ff631..601ca8f4 100644 --- a/server-rs/crates/api-server/src/auth_session.rs +++ b/server-rs/crates/api-server/src/auth_session.rs @@ -26,6 +26,47 @@ pub fn create_password_auth_session( create_auth_session(state, user, session_client, AuthLoginMethod::Password) } +#[cfg(not(test))] +pub async fn record_daily_login_tracking_event_after_auth_success( + state: &AppState, + request_context: &crate::request_context::RequestContext, + user_id: &str, + login_method: AuthLoginMethod, +) { + // 登录埋点是运营数据,不应反向阻断已经成功的认证会话签发。 + match state + .spacetime_client() + .record_daily_login_tracking_event(user_id.to_string()) + .await + { + Ok(()) => tracing::info!( + request_id = request_context.request_id(), + operation = request_context.operation(), + user_id = %user_id, + login_method = %login_method.as_str(), + "登录成功每日登录埋点已记录" + ), + Err(error) => tracing::warn!( + request_id = request_context.request_id(), + operation = request_context.operation(), + user_id = %user_id, + login_method = %login_method.as_str(), + error = %error, + "登录成功每日登录埋点记录失败,登录流程继续" + ), + } +} + +#[cfg(test)] +pub async fn record_daily_login_tracking_event_after_auth_success( + _state: &AppState, + _request_context: &crate::request_context::RequestContext, + _user_id: &str, + _login_method: AuthLoginMethod, +) { + // 单元测试默认不启动 SpacetimeDB;这里仅验证登录链路调用点能通过编译并保持非阻断语义。 +} + pub fn create_auth_session( state: &AppState, user: &AuthUser, diff --git a/server-rs/crates/api-server/src/password_entry.rs b/server-rs/crates/api-server/src/password_entry.rs index 308e3e87..d9d856b9 100644 --- a/server-rs/crates/api-server/src/password_entry.rs +++ b/server-rs/crates/api-server/src/password_entry.rs @@ -4,7 +4,7 @@ use axum::{ http::{HeaderMap, StatusCode}, response::IntoResponse, }; -use module_auth::{PasswordEntryError, PasswordEntryInput}; +use module_auth::{AuthLoginMethod, PasswordEntryError, PasswordEntryInput}; use serde_json::json; use shared_contracts::auth::{PasswordEntryRequest, PasswordEntryResponse}; @@ -12,7 +12,8 @@ use crate::{ api_response::json_success_body, auth_payload::map_auth_user_payload, auth_session::{ - attach_set_cookie_header, build_refresh_session_cookie_header, create_password_auth_session, + attach_set_cookie_header, build_refresh_session_cookie_header, + create_password_auth_session, record_daily_login_tracking_event_after_auth_success, }, http_error::AppError, request_context::RequestContext, @@ -49,6 +50,13 @@ pub async fn password_entry( } let session_client = resolve_session_client_context(&headers); let signed_session = create_password_auth_session(&state, &result.user, &session_client)?; + record_daily_login_tracking_event_after_auth_success( + &state, + &request_context, + &result.user.id, + AuthLoginMethod::Password, + ) + .await; state .sync_auth_store_snapshot_to_spacetime() .await diff --git a/server-rs/crates/api-server/src/password_management.rs b/server-rs/crates/api-server/src/password_management.rs index f2895b20..560e211a 100644 --- a/server-rs/crates/api-server/src/password_management.rs +++ b/server-rs/crates/api-server/src/password_management.rs @@ -16,6 +16,7 @@ use crate::{ auth_payload::map_auth_user_payload, auth_session::{ attach_set_cookie_header, build_refresh_session_cookie_header, create_auth_session, + record_daily_login_tracking_event_after_auth_success, }, http_error::AppError, phone_auth::map_phone_auth_error, @@ -79,6 +80,13 @@ pub async fn reset_password( &session_client, module_auth::AuthLoginMethod::Password, )?; + record_daily_login_tracking_event_after_auth_success( + &state, + &request_context, + &result.user.id, + module_auth::AuthLoginMethod::Password, + ) + .await; let mut headers = HeaderMap::new(); attach_set_cookie_header( diff --git a/server-rs/crates/api-server/src/phone_auth.rs b/server-rs/crates/api-server/src/phone_auth.rs index 4e49bdac..16b2ed5d 100644 --- a/server-rs/crates/api-server/src/phone_auth.rs +++ b/server-rs/crates/api-server/src/phone_auth.rs @@ -20,6 +20,7 @@ use crate::{ auth_payload::map_auth_user_payload, auth_session::{ attach_set_cookie_header, build_refresh_session_cookie_header, create_auth_session, + record_daily_login_tracking_event_after_auth_success, }, http_error::AppError, platform_errors::{attach_retry_after, map_phone_auth_platform_store_error}, @@ -176,6 +177,13 @@ pub async fn phone_login( &session_client, AuthLoginMethod::Phone, )?; + record_daily_login_tracking_event_after_auth_success( + &state, + &request_context, + &result.user.id, + AuthLoginMethod::Phone, + ) + .await; state .sync_auth_store_snapshot_to_spacetime() .await diff --git a/server-rs/crates/api-server/src/wechat_auth.rs b/server-rs/crates/api-server/src/wechat_auth.rs index 3725caae..be1b6faa 100644 --- a/server-rs/crates/api-server/src/wechat_auth.rs +++ b/server-rs/crates/api-server/src/wechat_auth.rs @@ -21,6 +21,7 @@ use crate::{ auth_payload::map_auth_user_payload, auth_session::{ attach_set_cookie_header, build_refresh_session_cookie_header, create_auth_session, + record_daily_login_tracking_event_after_auth_success, }, http_error::AppError, platform_errors::{attach_retry_after, map_wechat_provider_error}, @@ -74,6 +75,7 @@ pub async fn start_wechat_login( pub async fn handle_wechat_callback( State(state): State, + Extension(request_context): Extension, headers: HeaderMap, Query(query): Query, ) -> Result { @@ -141,6 +143,13 @@ pub async fn handle_wechat_callback( &session_client, AuthLoginMethod::Wechat, )?; + record_daily_login_tracking_event_after_auth_success( + &state, + &request_context, + &result.user.id, + AuthLoginMethod::Wechat, + ) + .await; state .sync_auth_store_snapshot_to_spacetime() .await @@ -208,6 +217,13 @@ pub async fn bind_wechat_phone( &session_client, AuthLoginMethod::Wechat, )?; + record_daily_login_tracking_event_after_auth_success( + &state, + &request_context, + &result.user.id, + AuthLoginMethod::Wechat, + ) + .await; state .sync_auth_store_snapshot_to_spacetime() .await