use module_runtime::RuntimeTrackingScopeKind; use serde_json::{Value, json}; use crate::{ auth::AuthenticatedAccessToken, request_context::RequestContext, state::AppState, tracking::{TrackingEventDraft, record_tracking_event_after_success}, }; pub(crate) const WORK_PLAY_START_EVENT_KEY: &str = "work_play_start"; pub(crate) struct WorkPlayTrackingDraft { pub play_type: &'static str, pub work_id: String, pub user_id: String, pub owner_user_id: Option, pub profile_id: Option, pub run_id: Option, pub source_route: &'static str, pub extra: Value, } impl WorkPlayTrackingDraft { pub(crate) fn new( play_type: &'static str, work_id: impl Into, authenticated: &AuthenticatedAccessToken, source_route: &'static str, ) -> Self { let user_id = authenticated.claims().user_id().to_string(); Self { play_type, work_id: work_id.into(), user_id, owner_user_id: None, profile_id: None, run_id: None, source_route, extra: json!({}), } } pub(crate) fn owner_user_id(mut self, owner_user_id: impl Into) -> Self { self.owner_user_id = Some(owner_user_id.into()); self } pub(crate) fn profile_id(mut self, profile_id: impl Into) -> Self { self.profile_id = Some(profile_id.into()); self } pub(crate) fn run_id(mut self, run_id: impl Into) -> Self { self.run_id = Some(run_id.into()); self } pub(crate) fn extra(mut self, extra: Value) -> Self { self.extra = extra; self } } /// 作品级正式游玩埋点:scope 固定为 work,scope_id 固定为稳定作品 ID。 /// 中文注释:该埋点用于“某作品被多少用户玩过”等分析,写入失败不阻断 runtime 主流程。 pub(crate) async fn record_work_play_start_after_success( state: &AppState, request_context: &RequestContext, draft: WorkPlayTrackingDraft, ) { let mut metadata = json!({ "operation": WORK_PLAY_START_EVENT_KEY, "playType": draft.play_type, "workId": draft.work_id, "sourceRoute": draft.source_route, }); metadata["userId"] = json!(draft.user_id); if let Some(owner_user_id) = draft.owner_user_id.as_deref() { metadata["ownerUserId"] = json!(owner_user_id); } if let Some(profile_id) = draft.profile_id.as_deref() { metadata["profileId"] = json!(profile_id); } if let Some(run_id) = draft.run_id.as_deref() { metadata["runId"] = json!(run_id); } if !draft.extra.is_null() { metadata["extra"] = draft.extra; } let mut tracking = TrackingEventDraft::new(WORK_PLAY_START_EVENT_KEY, draft.play_type); tracking.scope_kind = RuntimeTrackingScopeKind::Work; tracking.scope_id = draft.work_id; tracking.user_id = Some(draft.user_id); tracking.owner_user_id = draft.owner_user_id; tracking.profile_id = draft.profile_id; tracking.metadata = metadata; record_tracking_event_after_success(state, request_context, tracking).await; } #[cfg(test)] mod tests { use super::*; #[test] fn work_play_event_key_is_stable() { assert_eq!(WORK_PLAY_START_EVENT_KEY, "work_play_start"); } }