feat: add work-level play tracking
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-05-09 19:56:59 +08:00
parent 32a1530ab1
commit 3ad1075227
24 changed files with 1452 additions and 105 deletions

View File

@@ -23,8 +23,13 @@ use shared_contracts::assets::{
use spacetime_client::SpacetimeClientError;
use crate::{
api_response::json_success_body, auth::AuthenticatedAccessToken, http_error::AppError,
platform_errors::map_oss_error, request_context::RequestContext, state::AppState,
api_response::json_success_body,
auth::AuthenticatedAccessToken,
http_error::AppError,
platform_errors::map_oss_error,
request_context::RequestContext,
state::AppState,
tracking::{TrackingEventDraft, record_tracking_event_after_success},
};
// 历史素材类型需要与 SpacetimeDB 侧白名单保持同一口径,避免新增素材类型时 HTTP 门面漏同步。
@@ -41,6 +46,7 @@ const SUPPORTED_ASSET_HISTORY_KINDS: [&str; 7] = [
pub async fn create_direct_upload_ticket(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
Extension(authenticated): Extension<AuthenticatedAccessToken>,
Json(payload): Json<CreateDirectUploadTicketRequest>,
) -> Result<Json<Value>, AppError> {
let oss_client = state.oss_client().ok_or_else(|| {
@@ -75,12 +81,33 @@ pub async fn create_direct_upload_ticket(
"message": error.to_string(),
}))
})?;
let upload = DirectUploadTicketPayload::from(signed);
record_asset_tracking_event(
&state,
&request_context,
&authenticated,
"asset_upload_ticket_create",
json!({
"asset": {
"operation": "asset_upload_ticket_create",
"operationFamily": "upload_ticket",
"objectKey": upload.object_key.clone(),
"legacyPublicPath": upload.legacy_public_path.clone(),
"bucket": upload.bucket.clone(),
"contentType": upload.content_type.clone(),
"access": upload.access,
"keyPrefix": upload.key_prefix.clone(),
"maxSizeBytes": upload.max_size_bytes,
"successActionStatus": upload.success_action_status,
}
}),
)
.await;
Ok(json_success_body(
Some(&request_context),
CreateDirectUploadTicketResponse {
upload: DirectUploadTicketPayload::from(signed),
},
CreateDirectUploadTicketResponse { upload },
))
}
@@ -190,6 +217,7 @@ pub async fn create_sts_upload_credentials(
pub async fn confirm_asset_object(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
Extension(authenticated): Extension<AuthenticatedAccessToken>,
Json(payload): Json<ConfirmAssetObjectRequest>,
) -> Result<Json<Value>, AppError> {
let oss_client = state.oss_client().ok_or_else(|| {
@@ -209,33 +237,60 @@ pub async fn confirm_asset_object(
.await
.map_err(map_confirm_asset_object_error)?;
let asset_object = AssetObjectPayload {
asset_object_id: result.asset_object_id,
bucket: result.bucket,
object_key: result.object_key,
access_policy: result.access_policy.as_str().to_string(),
content_type: result.content_type,
content_length: result.content_length,
content_hash: result.content_hash,
version: result.version,
source_job_id: result.source_job_id,
owner_user_id: result.owner_user_id,
profile_id: result.profile_id,
entity_id: result.entity_id,
asset_kind: result.asset_kind,
created_at: result.created_at,
updated_at: result.updated_at,
};
record_asset_tracking_event(
&state,
&request_context,
&authenticated,
"asset_upload_confirm",
json!({
"asset": {
"operation": "asset_upload_confirm",
"operationFamily": "object_confirm",
"assetObjectId": asset_object.asset_object_id,
"assetKind": asset_object.asset_kind,
"objectKey": asset_object.object_key,
"bucket": asset_object.bucket,
"accessPolicy": asset_object.access_policy,
"contentType": asset_object.content_type,
"contentLength": asset_object.content_length,
"version": asset_object.version,
"sourceJobId": asset_object.source_job_id,
"ownerUserId": asset_object.owner_user_id,
"profileId": asset_object.profile_id,
"entityId": asset_object.entity_id,
}
}),
)
.await;
Ok(json_success_body(
Some(&request_context),
ConfirmAssetObjectResponse {
asset_object: AssetObjectPayload {
asset_object_id: result.asset_object_id,
bucket: result.bucket,
object_key: result.object_key,
access_policy: result.access_policy.as_str().to_string(),
content_type: result.content_type,
content_length: result.content_length,
content_hash: result.content_hash,
version: result.version,
source_job_id: result.source_job_id,
owner_user_id: result.owner_user_id,
profile_id: result.profile_id,
entity_id: result.entity_id,
asset_kind: result.asset_kind,
created_at: result.created_at,
updated_at: result.updated_at,
},
},
ConfirmAssetObjectResponse { asset_object },
))
}
pub async fn bind_asset_object_to_entity(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
Extension(authenticated): Extension<AuthenticatedAccessToken>,
Json(payload): Json<BindAssetObjectRequest>,
) -> Result<Json<Value>, AppError> {
let now_micros = current_utc_micros();
@@ -258,25 +313,60 @@ pub async fn bind_asset_object_to_entity(
.await
.map_err(map_confirm_asset_object_error)?;
let asset_binding = AssetBindingPayload {
binding_id: result.binding_id,
asset_object_id: result.asset_object_id,
entity_kind: result.entity_kind,
entity_id: result.entity_id,
slot: result.slot,
asset_kind: result.asset_kind,
owner_user_id: result.owner_user_id,
profile_id: result.profile_id,
created_at: result.created_at,
updated_at: result.updated_at,
};
record_asset_tracking_event(
&state,
&request_context,
&authenticated,
"asset_bind",
json!({
"asset": {
"operation": "asset_bind",
"operationFamily": "object_bind",
"bindingId": asset_binding.binding_id,
"assetObjectId": asset_binding.asset_object_id,
"assetKind": asset_binding.asset_kind,
"entityKind": asset_binding.entity_kind,
"entityId": asset_binding.entity_id,
"slot": asset_binding.slot,
"ownerUserId": asset_binding.owner_user_id,
"profileId": asset_binding.profile_id,
}
}),
)
.await;
Ok(json_success_body(
Some(&request_context),
BindAssetObjectResponse {
asset_binding: AssetBindingPayload {
binding_id: result.binding_id,
asset_object_id: result.asset_object_id,
entity_kind: result.entity_kind,
entity_id: result.entity_id,
slot: result.slot,
asset_kind: result.asset_kind,
owner_user_id: result.owner_user_id,
profile_id: result.profile_id,
created_at: result.created_at,
updated_at: result.updated_at,
},
},
BindAssetObjectResponse { asset_binding },
))
}
async fn record_asset_tracking_event(
state: &AppState,
request_context: &RequestContext,
authenticated: &AuthenticatedAccessToken,
event_key: &'static str,
metadata: Value,
) {
let user_id = authenticated.claims().user_id().to_string();
let mut draft = TrackingEventDraft::user(event_key, "asset", user_id.as_str());
draft.metadata = metadata;
record_tracking_event_after_success(state, request_context, draft).await;
}
fn resolve_object_key_from_query(query: &GetReadUrlQuery) -> Option<String> {
if let Some(object_key) = query
.object_key