Merge branch 'master' of http://82.157.175.59:3000/GenarrativeAI/Genarrative
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
@@ -32,6 +32,7 @@ mod match3d;
|
||||
mod migration;
|
||||
mod puzzle;
|
||||
mod runtime;
|
||||
mod square_hole;
|
||||
|
||||
pub use ai::*;
|
||||
pub use asset_metadata::*;
|
||||
@@ -44,3 +45,4 @@ pub use gameplay::*;
|
||||
pub use match3d::*;
|
||||
pub use migration::*;
|
||||
pub use runtime::*;
|
||||
pub use square_hole::*;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::runtime::analytics_date_dimension::analytics_date_dimension;
|
||||
use crate::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use spacetimedb_lib::sats::de::serde::DeserializeWrapper;
|
||||
@@ -12,6 +13,10 @@ use crate::puzzle::{
|
||||
puzzle_agent_message, puzzle_agent_session, puzzle_event, puzzle_leaderboard_entry,
|
||||
puzzle_runtime_run, puzzle_work_profile,
|
||||
};
|
||||
use crate::square_hole::tables::{
|
||||
square_hole_agent_message, square_hole_agent_session, square_hole_runtime_run,
|
||||
square_hole_work_profile,
|
||||
};
|
||||
|
||||
const MIGRATION_SCHEMA_VERSION: u32 = 1;
|
||||
const MIGRATION_MAX_TABLE_NAME_LEN: usize = 96;
|
||||
@@ -161,6 +166,7 @@ macro_rules! migration_tables {
|
||||
user_browse_history,
|
||||
profile_dashboard_state,
|
||||
profile_wallet_ledger,
|
||||
analytics_date_dimension,
|
||||
tracking_event,
|
||||
tracking_daily_stat,
|
||||
profile_task_config,
|
||||
@@ -206,6 +212,10 @@ macro_rules! migration_tables {
|
||||
match3d_agent_message,
|
||||
match3d_work_profile,
|
||||
match3d_runtime_run,
|
||||
square_hole_agent_session,
|
||||
square_hole_agent_message,
|
||||
square_hole_work_profile,
|
||||
square_hole_runtime_run,
|
||||
big_fish_creation_session,
|
||||
big_fish_agent_message,
|
||||
big_fish_asset_slot,
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
use crate::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct AnalyticsDateDimensionEnsureInput {
|
||||
pub date_key: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct AnalyticsDateDimensionSeedInput {
|
||||
pub start_date: String,
|
||||
pub end_date: String,
|
||||
}
|
||||
|
||||
#[spacetimedb::table(
|
||||
accessor = analytics_date_dimension,
|
||||
index(accessor = by_analytics_date_dimension_iso_week, btree(columns = [iso_week_key])),
|
||||
index(accessor = by_analytics_date_dimension_month, btree(columns = [month_key])),
|
||||
index(accessor = by_analytics_date_dimension_quarter, btree(columns = [quarter_key])),
|
||||
index(accessor = by_analytics_date_dimension_year, btree(columns = [year_key]))
|
||||
)]
|
||||
pub struct AnalyticsDateDimension {
|
||||
#[primary_key]
|
||||
pub(crate) date_key: i64,
|
||||
// 中文注释:北京时间业务日对应的公历日期,格式 YYYY-MM-DD。
|
||||
pub(crate) calendar_date: String,
|
||||
// 中文注释:ISO weekday,周一=1,周日=7,方便统计层不重复计算。
|
||||
pub(crate) weekday: u8,
|
||||
// 中文注释:ISO week 编码为 YYYYWW,跨年周按 ISO 年归属。
|
||||
pub(crate) iso_week_key: i32,
|
||||
pub(crate) week_start_date_key: i64,
|
||||
pub(crate) week_end_date_key: i64,
|
||||
pub(crate) month_key: i32,
|
||||
pub(crate) month_start_date_key: i64,
|
||||
pub(crate) month_end_date_key: i64,
|
||||
pub(crate) quarter_key: i32,
|
||||
pub(crate) quarter_start_date_key: i64,
|
||||
pub(crate) quarter_end_date_key: i64,
|
||||
pub(crate) year_key: i32,
|
||||
pub(crate) year_start_date_key: i64,
|
||||
pub(crate) year_end_date_key: i64,
|
||||
pub(crate) created_at: Timestamp,
|
||||
pub(crate) updated_at: Timestamp,
|
||||
}
|
||||
|
||||
#[spacetimedb::reducer]
|
||||
pub fn ensure_analytics_date_dimension_for_date(
|
||||
ctx: &ReducerContext,
|
||||
input: AnalyticsDateDimensionEnsureInput,
|
||||
) -> Result<(), String> {
|
||||
ensure_analytics_date_dimension_row(ctx, input.date_key)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[spacetimedb::reducer]
|
||||
pub fn seed_analytics_date_dimensions(
|
||||
ctx: &ReducerContext,
|
||||
input: AnalyticsDateDimensionSeedInput,
|
||||
) -> Result<(), String> {
|
||||
let start_date_key = parse_analytics_calendar_date_key(&input.start_date)
|
||||
.map_err(|error| format!("invalid start_date: {error}"))?;
|
||||
let end_date_key = parse_analytics_calendar_date_key(&input.end_date)
|
||||
.map_err(|error| format!("invalid end_date: {error}"))?;
|
||||
if start_date_key > end_date_key {
|
||||
return Err("start_date must be <= end_date".to_string());
|
||||
}
|
||||
let seed_days = end_date_key - start_date_key + 1;
|
||||
if seed_days > ANALYTICS_DATE_DIMENSION_MAX_SEED_DAYS {
|
||||
return Err(format!(
|
||||
"date range too large: max {} days",
|
||||
ANALYTICS_DATE_DIMENSION_MAX_SEED_DAYS
|
||||
));
|
||||
}
|
||||
|
||||
for date_key in start_date_key..=end_date_key {
|
||||
ensure_analytics_date_dimension_row(ctx, date_key)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn ensure_analytics_date_dimension_row(
|
||||
ctx: &ReducerContext,
|
||||
date_key: i64,
|
||||
) -> Result<(), String> {
|
||||
validate_analytics_date_dimension_date_key(date_key)
|
||||
.map_err(|error| format!("invalid date_key: {error}"))?;
|
||||
if ctx
|
||||
.db
|
||||
.analytics_date_dimension()
|
||||
.date_key()
|
||||
.find(&date_key)
|
||||
.is_some()
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let snapshot = build_analytics_date_dimension_from_date_key(date_key);
|
||||
ctx.db
|
||||
.analytics_date_dimension()
|
||||
.insert(build_analytics_date_dimension_row(snapshot, ctx.timestamp));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_analytics_date_dimension_row(
|
||||
snapshot: AnalyticsDateDimensionSnapshot,
|
||||
now: Timestamp,
|
||||
) -> AnalyticsDateDimension {
|
||||
AnalyticsDateDimension {
|
||||
date_key: snapshot.date_key,
|
||||
calendar_date: snapshot.calendar_date,
|
||||
weekday: snapshot.weekday,
|
||||
iso_week_key: snapshot.iso_week_key,
|
||||
week_start_date_key: snapshot.week_start_date_key,
|
||||
week_end_date_key: snapshot.week_end_date_key,
|
||||
month_key: snapshot.month_key,
|
||||
month_start_date_key: snapshot.month_start_date_key,
|
||||
month_end_date_key: snapshot.month_end_date_key,
|
||||
quarter_key: snapshot.quarter_key,
|
||||
quarter_start_date_key: snapshot.quarter_start_date_key,
|
||||
quarter_end_date_key: snapshot.quarter_end_date_key,
|
||||
year_key: snapshot.year_key,
|
||||
year_start_date_key: snapshot.year_start_date_key,
|
||||
year_end_date_key: snapshot.year_end_date_key,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
pub mod analytics_date_dimension;
|
||||
mod browse_history;
|
||||
mod profile;
|
||||
mod settings;
|
||||
mod snapshots;
|
||||
|
||||
pub use analytics_date_dimension::*;
|
||||
pub use browse_history::*;
|
||||
pub use profile::*;
|
||||
pub use settings::*;
|
||||
|
||||
@@ -6,7 +6,6 @@ const PROFILE_REFERRAL_INVITED_USERS_LIMIT: usize = 20;
|
||||
const PROFILE_NEW_USER_REGISTRATION_LEDGER_PREFIX: &str = "new-user-registration";
|
||||
const PROFILE_TASK_SYSTEM_USER_ID: &str = "system:profile-task";
|
||||
const PROFILE_TASK_LOGIN_EVENT_ID_PREFIX: &str = "daily-login";
|
||||
const PROFILE_TRACKING_SITE_SCOPE_ID: &str = "site";
|
||||
const PROFILE_TRACKING_PROFILE_MODULE_KEY: &str = "profile";
|
||||
|
||||
#[spacetimedb::table(accessor = profile_dashboard_state)]
|
||||
@@ -188,6 +187,10 @@ pub struct ProfileInviteCode {
|
||||
pub(crate) metadata_json: String,
|
||||
pub(crate) created_at: Timestamp,
|
||||
pub(crate) updated_at: Timestamp,
|
||||
#[default(None::<Timestamp>)]
|
||||
pub(crate) starts_at: Option<Timestamp>,
|
||||
#[default(None::<Timestamp>)]
|
||||
pub(crate) expires_at: Option<Timestamp>,
|
||||
}
|
||||
|
||||
#[spacetimedb::table(
|
||||
@@ -468,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(
|
||||
@@ -1902,6 +1926,7 @@ fn redeem_profile_referral_invite_code_record(
|
||||
.invite_code()
|
||||
.find(&invite_code)
|
||||
.ok_or_else(|| "邀请码不存在".to_string())?;
|
||||
validate_profile_invite_code_redeem_time(&inviter_code, validated_input.updated_at_micros)?;
|
||||
if inviter_code.user_id == invitee_user_id {
|
||||
return Err("不能填写自己的邀请码".to_string());
|
||||
}
|
||||
@@ -2124,6 +2149,8 @@ fn admin_upsert_profile_invite_code_record(
|
||||
input.admin_user_id,
|
||||
input.invite_code,
|
||||
input.metadata_json,
|
||||
input.starts_at_micros,
|
||||
input.expires_at_micros,
|
||||
input.updated_at_micros,
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
@@ -2152,6 +2179,12 @@ fn admin_upsert_profile_invite_code_record(
|
||||
metadata_json: validated_input.metadata_json,
|
||||
created_at: existing.created_at,
|
||||
updated_at,
|
||||
starts_at: validated_input
|
||||
.starts_at_micros
|
||||
.map(Timestamp::from_micros_since_unix_epoch),
|
||||
expires_at: validated_input
|
||||
.expires_at_micros
|
||||
.map(Timestamp::from_micros_since_unix_epoch),
|
||||
});
|
||||
return Ok(build_profile_invite_code_snapshot_from_row(&inserted));
|
||||
}
|
||||
@@ -2162,6 +2195,12 @@ fn admin_upsert_profile_invite_code_record(
|
||||
metadata_json: validated_input.metadata_json,
|
||||
created_at: updated_at,
|
||||
updated_at,
|
||||
starts_at: validated_input
|
||||
.starts_at_micros
|
||||
.map(Timestamp::from_micros_since_unix_epoch),
|
||||
expires_at: validated_input
|
||||
.expires_at_micros
|
||||
.map(Timestamp::from_micros_since_unix_epoch),
|
||||
});
|
||||
Ok(build_profile_invite_code_snapshot_from_row(&inserted))
|
||||
}
|
||||
@@ -2286,9 +2325,34 @@ fn ensure_profile_invite_code(ctx: &ReducerContext, user_id: &str) -> ProfileInv
|
||||
metadata_json: PROFILE_INVITE_CODE_METADATA_DEFAULT_JSON.to_string(),
|
||||
created_at: ctx.timestamp,
|
||||
updated_at: ctx.timestamp,
|
||||
starts_at: None,
|
||||
expires_at: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_profile_invite_code_redeem_time(
|
||||
invite_code: &ProfileInviteCode,
|
||||
now_micros: i64,
|
||||
) -> Result<(), String> {
|
||||
if invite_code
|
||||
.starts_at
|
||||
.map(|starts_at| now_micros < starts_at.to_micros_since_unix_epoch())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Err("邀请码未生效".to_string());
|
||||
}
|
||||
|
||||
if invite_code
|
||||
.expires_at
|
||||
.map(|expires_at| now_micros >= expires_at.to_micros_since_unix_epoch())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Err("邀请码已过期".to_string());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn count_today_profile_referral_inviter_rewards(
|
||||
ctx: &ReducerContext,
|
||||
user_id: &str,
|
||||
@@ -2397,11 +2461,7 @@ fn get_profile_task_center_snapshot(
|
||||
record_daily_login_tracking_event(ctx, &validated_input.user_id)?;
|
||||
}
|
||||
|
||||
Ok(build_profile_task_center_snapshot(
|
||||
ctx,
|
||||
&validated_input.user_id,
|
||||
ctx.timestamp,
|
||||
))
|
||||
build_profile_task_center_snapshot(ctx, &validated_input.user_id, ctx.timestamp)
|
||||
}
|
||||
|
||||
fn claim_profile_task_reward_record(
|
||||
@@ -2438,7 +2498,7 @@ fn claim_profile_task_reward_record(
|
||||
return Err(RuntimeProfileFieldError::TaskAlreadyClaimed.to_string());
|
||||
}
|
||||
|
||||
let progress_count = profile_task_progress_count(ctx, &validated_input.user_id, &config);
|
||||
let progress_count = profile_task_progress_count(ctx, &validated_input.user_id, &config)?;
|
||||
if progress_count < config.threshold {
|
||||
return Err(RuntimeProfileFieldError::TaskNotClaimable.to_string());
|
||||
}
|
||||
@@ -2469,7 +2529,7 @@ fn claim_profile_task_reward_record(
|
||||
claimed_at: ctx.timestamp,
|
||||
});
|
||||
|
||||
refresh_profile_task_progress(ctx, &validated_input.user_id, &config, day_key);
|
||||
refresh_profile_task_progress(ctx, &validated_input.user_id, &config, day_key)?;
|
||||
let ledger_entry = ctx
|
||||
.db
|
||||
.profile_wallet_ledger()
|
||||
@@ -2484,7 +2544,7 @@ fn claim_profile_task_reward_record(
|
||||
reward_points: claim.reward_points,
|
||||
wallet_balance,
|
||||
ledger_entry: build_profile_wallet_ledger_snapshot_from_row(&ledger_entry),
|
||||
center: build_profile_task_center_snapshot(ctx, &validated_input.user_id, ctx.timestamp),
|
||||
center: build_profile_task_center_snapshot(ctx, &validated_input.user_id, ctx.timestamp)?,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2640,7 +2700,7 @@ fn build_profile_task_center_snapshot(
|
||||
ctx: &ReducerContext,
|
||||
user_id: &str,
|
||||
updated_at: Timestamp,
|
||||
) -> RuntimeProfileTaskCenterSnapshot {
|
||||
) -> Result<RuntimeProfileTaskCenterSnapshot, String> {
|
||||
ensure_default_profile_task_config(ctx);
|
||||
let day_key = runtime_profile_beijing_day_key(updated_at.to_micros_since_unix_epoch());
|
||||
let mut configs = ctx.db.profile_task_config().iter().collect::<Vec<_>>();
|
||||
@@ -2649,43 +2709,81 @@ fn build_profile_task_center_snapshot(
|
||||
.cmp(&right.sort_order)
|
||||
.then_with(|| left.task_id.cmp(&right.task_id))
|
||||
});
|
||||
let tasks = configs
|
||||
.into_iter()
|
||||
.map(|config| {
|
||||
let progress_count = profile_task_progress_count(ctx, user_id, &config);
|
||||
refresh_profile_task_progress(ctx, user_id, &config, day_key);
|
||||
let claim = ctx.db.profile_task_reward_claim().claim_id().find(
|
||||
&build_runtime_profile_task_claim_id(user_id, &config.task_id, day_key),
|
||||
);
|
||||
RuntimeProfileTaskItemSnapshot {
|
||||
task_id: config.task_id,
|
||||
title: config.title,
|
||||
description: config.description,
|
||||
event_key: config.event_key,
|
||||
cycle: config.cycle,
|
||||
threshold: config.threshold,
|
||||
let mut tasks = Vec::with_capacity(configs.len());
|
||||
for config in configs {
|
||||
validate_profile_task_user_scope(&config)?;
|
||||
let progress_count = profile_task_progress_count(ctx, user_id, &config)?;
|
||||
refresh_profile_task_progress(ctx, user_id, &config, day_key)?;
|
||||
let claim = ctx.db.profile_task_reward_claim().claim_id().find(
|
||||
&build_runtime_profile_task_claim_id(user_id, &config.task_id, day_key),
|
||||
);
|
||||
tasks.push(RuntimeProfileTaskItemSnapshot {
|
||||
task_id: config.task_id,
|
||||
title: config.title,
|
||||
description: config.description,
|
||||
event_key: config.event_key,
|
||||
cycle: config.cycle,
|
||||
threshold: config.threshold,
|
||||
progress_count,
|
||||
reward_points: config.reward_points,
|
||||
status: resolve_runtime_profile_task_status(
|
||||
config.enabled,
|
||||
progress_count,
|
||||
reward_points: config.reward_points,
|
||||
status: resolve_runtime_profile_task_status(
|
||||
config.enabled,
|
||||
progress_count,
|
||||
config.threshold,
|
||||
claim.is_some(),
|
||||
),
|
||||
day_key,
|
||||
claimed_at_micros: claim.map(|row| row.claimed_at.to_micros_since_unix_epoch()),
|
||||
updated_at_micros: updated_at.to_micros_since_unix_epoch(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
config.threshold,
|
||||
claim.is_some(),
|
||||
),
|
||||
day_key,
|
||||
claimed_at_micros: claim.map(|row| row.claimed_at.to_micros_since_unix_epoch()),
|
||||
updated_at_micros: updated_at.to_micros_since_unix_epoch(),
|
||||
});
|
||||
}
|
||||
|
||||
RuntimeProfileTaskCenterSnapshot {
|
||||
Ok(RuntimeProfileTaskCenterSnapshot {
|
||||
user_id: user_id.to_string(),
|
||||
day_key,
|
||||
wallet_balance: profile_wallet_balance(ctx, user_id),
|
||||
tasks,
|
||||
updated_at_micros: updated_at.to_micros_since_unix_epoch(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
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(
|
||||
@@ -2693,7 +2791,7 @@ fn refresh_profile_task_progress(
|
||||
user_id: &str,
|
||||
config: &ProfileTaskConfig,
|
||||
day_key: i64,
|
||||
) -> ProfileTaskProgress {
|
||||
) -> Result<ProfileTaskProgress, String> {
|
||||
let progress_id = build_runtime_profile_task_progress_id(user_id, &config.task_id, day_key);
|
||||
if let Some(existing) = ctx
|
||||
.db
|
||||
@@ -2706,7 +2804,7 @@ fn refresh_profile_task_progress(
|
||||
.progress_id()
|
||||
.delete(&existing.progress_id);
|
||||
}
|
||||
let progress_count = profile_task_progress_count(ctx, user_id, config);
|
||||
let progress_count = profile_task_progress_count(ctx, user_id, config)?;
|
||||
let claimed = ctx
|
||||
.db
|
||||
.profile_task_reward_claim()
|
||||
@@ -2717,7 +2815,7 @@ fn refresh_profile_task_progress(
|
||||
day_key,
|
||||
))
|
||||
.is_some();
|
||||
ctx.db.profile_task_progress().insert(ProfileTaskProgress {
|
||||
Ok(ctx.db.profile_task_progress().insert(ProfileTaskProgress {
|
||||
progress_id,
|
||||
user_id: user_id.to_string(),
|
||||
task_id: config.task_id.clone(),
|
||||
@@ -2731,17 +2829,19 @@ fn refresh_profile_task_progress(
|
||||
claimed,
|
||||
),
|
||||
updated_at: ctx.timestamp,
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
fn profile_task_progress_count(
|
||||
ctx: &ReducerContext,
|
||||
user_id: &str,
|
||||
config: &ProfileTaskConfig,
|
||||
) -> u32 {
|
||||
) -> Result<u32, String> {
|
||||
validate_profile_task_user_scope(config)?;
|
||||
let day_key = runtime_profile_beijing_day_key(ctx.timestamp.to_micros_since_unix_epoch());
|
||||
let scope_id = profile_task_tracking_scope_id(user_id, config);
|
||||
ctx.db
|
||||
let scope_id = profile_task_tracking_scope_id(user_id, config)?;
|
||||
Ok(ctx
|
||||
.db
|
||||
.tracking_daily_stat()
|
||||
.stat_id()
|
||||
.find(&build_runtime_tracking_daily_stat_id(
|
||||
@@ -2751,15 +2851,26 @@ fn profile_task_progress_count(
|
||||
day_key,
|
||||
))
|
||||
.map(|row| row.count)
|
||||
.unwrap_or(0)
|
||||
.unwrap_or(0))
|
||||
}
|
||||
|
||||
fn profile_task_tracking_scope_id(user_id: &str, config: &ProfileTaskConfig) -> String {
|
||||
match config.scope_kind {
|
||||
RuntimeTrackingScopeKind::Site => PROFILE_TRACKING_SITE_SCOPE_ID.to_string(),
|
||||
RuntimeTrackingScopeKind::Module => PROFILE_TRACKING_PROFILE_MODULE_KEY.to_string(),
|
||||
RuntimeTrackingScopeKind::User => user_id.to_string(),
|
||||
RuntimeTrackingScopeKind::Work => user_id.to_string(),
|
||||
fn profile_task_tracking_scope_id(
|
||||
user_id: &str,
|
||||
config: &ProfileTaskConfig,
|
||||
) -> Result<String, String> {
|
||||
validate_profile_task_user_scope(config)?;
|
||||
Ok(user_id.to_string())
|
||||
}
|
||||
|
||||
fn validate_profile_task_user_scope(config: &ProfileTaskConfig) -> Result<(), String> {
|
||||
if config.scope_kind == RuntimeTrackingScopeKind::User {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!(
|
||||
"个人任务 scope_kind 首版仅支持 user,当前 task_id={} scope_kind={}",
|
||||
config.task_id,
|
||||
config.scope_kind.as_str()
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3242,6 +3353,12 @@ fn build_profile_invite_code_snapshot_from_row(
|
||||
user_id: row.user_id.clone(),
|
||||
invite_code: row.invite_code.clone(),
|
||||
metadata_json: row.metadata_json.clone(),
|
||||
starts_at_micros: row
|
||||
.starts_at
|
||||
.map(|value| value.to_micros_since_unix_epoch()),
|
||||
expires_at_micros: row
|
||||
.expires_at
|
||||
.map(|value| value.to_micros_since_unix_epoch()),
|
||||
created_at_micros: row.created_at.to_micros_since_unix_epoch(),
|
||||
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
|
||||
}
|
||||
|
||||
1399
server-rs/crates/spacetime-module/src/square_hole/mod.rs
Normal file
1399
server-rs/crates/spacetime-module/src/square_hole/mod.rs
Normal file
File diff suppressed because it is too large
Load Diff
86
server-rs/crates/spacetime-module/src/square_hole/tables.rs
Normal file
86
server-rs/crates/spacetime-module/src/square_hole/tables.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use crate::*;
|
||||
|
||||
#[spacetimedb::table(
|
||||
accessor = square_hole_agent_session,
|
||||
index(accessor = by_square_hole_agent_session_owner_user_id, btree(columns = [owner_user_id]))
|
||||
)]
|
||||
pub struct SquareHoleAgentSessionRow {
|
||||
#[primary_key]
|
||||
pub(crate) session_id: String,
|
||||
pub(crate) owner_user_id: String,
|
||||
pub(crate) seed_text: String,
|
||||
pub(crate) current_turn: u32,
|
||||
pub(crate) progress_percent: u32,
|
||||
pub(crate) stage: String,
|
||||
pub(crate) config_json: String,
|
||||
pub(crate) draft_json: String,
|
||||
pub(crate) last_assistant_reply: String,
|
||||
pub(crate) published_profile_id: String,
|
||||
pub(crate) created_at: Timestamp,
|
||||
pub(crate) updated_at: Timestamp,
|
||||
}
|
||||
|
||||
#[spacetimedb::table(
|
||||
accessor = square_hole_agent_message,
|
||||
index(accessor = by_square_hole_agent_message_session_id, btree(columns = [session_id]))
|
||||
)]
|
||||
pub struct SquareHoleAgentMessageRow {
|
||||
#[primary_key]
|
||||
pub(crate) message_id: String,
|
||||
pub(crate) session_id: String,
|
||||
pub(crate) role: String,
|
||||
pub(crate) kind: String,
|
||||
pub(crate) text: String,
|
||||
pub(crate) created_at: Timestamp,
|
||||
}
|
||||
|
||||
#[spacetimedb::table(
|
||||
accessor = square_hole_work_profile,
|
||||
index(accessor = by_square_hole_work_owner_user_id, btree(columns = [owner_user_id])),
|
||||
index(accessor = by_square_hole_work_publication_status, btree(columns = [publication_status]))
|
||||
)]
|
||||
pub struct SquareHoleWorkProfileRow {
|
||||
#[primary_key]
|
||||
pub(crate) profile_id: String,
|
||||
pub(crate) work_id: String,
|
||||
pub(crate) owner_user_id: String,
|
||||
pub(crate) source_session_id: String,
|
||||
pub(crate) author_display_name: String,
|
||||
pub(crate) game_name: String,
|
||||
pub(crate) theme_text: String,
|
||||
pub(crate) twist_rule: String,
|
||||
pub(crate) summary_text: String,
|
||||
pub(crate) tags_json: String,
|
||||
pub(crate) cover_image_src: String,
|
||||
pub(crate) shape_count: u32,
|
||||
pub(crate) difficulty: u32,
|
||||
pub(crate) config_json: String,
|
||||
pub(crate) publication_status: String,
|
||||
pub(crate) play_count: u32,
|
||||
pub(crate) updated_at: Timestamp,
|
||||
pub(crate) published_at: Option<Timestamp>,
|
||||
}
|
||||
|
||||
#[spacetimedb::table(
|
||||
accessor = square_hole_runtime_run,
|
||||
index(accessor = by_square_hole_run_owner_user_id, btree(columns = [owner_user_id])),
|
||||
index(accessor = by_square_hole_run_profile_id, btree(columns = [profile_id]))
|
||||
)]
|
||||
pub struct SquareHoleRuntimeRunRow {
|
||||
#[primary_key]
|
||||
pub(crate) run_id: String,
|
||||
pub(crate) owner_user_id: String,
|
||||
pub(crate) profile_id: String,
|
||||
pub(crate) status: String,
|
||||
pub(crate) snapshot_version: u64,
|
||||
pub(crate) started_at_ms: i64,
|
||||
pub(crate) duration_limit_ms: i64,
|
||||
pub(crate) finished_at_ms: i64,
|
||||
pub(crate) elapsed_ms: i64,
|
||||
pub(crate) total_shape_count: u32,
|
||||
pub(crate) completed_shape_count: u32,
|
||||
pub(crate) score: u32,
|
||||
pub(crate) snapshot_json: String,
|
||||
pub(crate) created_at: Timestamp,
|
||||
pub(crate) updated_at: Timestamp,
|
||||
}
|
||||
325
server-rs/crates/spacetime-module/src/square_hole/types.rs
Normal file
325
server-rs/crates/spacetime-module/src/square_hole/types.rs
Normal file
@@ -0,0 +1,325 @@
|
||||
use crate::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const SQUARE_HOLE_STAGE_COLLECTING: &str = "Collecting";
|
||||
pub const SQUARE_HOLE_STAGE_READY_TO_COMPILE: &str = "ReadyToCompile";
|
||||
pub const SQUARE_HOLE_STAGE_DRAFT_COMPILED: &str = "DraftCompiled";
|
||||
pub const SQUARE_HOLE_STAGE_PUBLISHED: &str = "Published";
|
||||
|
||||
pub const SQUARE_HOLE_ROLE_USER: &str = "user";
|
||||
pub const SQUARE_HOLE_ROLE_ASSISTANT: &str = "assistant";
|
||||
pub const SQUARE_HOLE_KIND_TEXT: &str = "text";
|
||||
|
||||
pub const SQUARE_HOLE_PUBLICATION_DRAFT: &str = "Draft";
|
||||
pub const SQUARE_HOLE_PUBLICATION_PUBLISHED: &str = "Published";
|
||||
|
||||
pub const SQUARE_HOLE_RUN_RUNNING: &str = "Running";
|
||||
pub const SQUARE_HOLE_RUN_WON: &str = "Won";
|
||||
pub const SQUARE_HOLE_RUN_FAILED: &str = "Failed";
|
||||
pub const SQUARE_HOLE_RUN_STOPPED: &str = "Stopped";
|
||||
|
||||
pub const SQUARE_HOLE_DROP_ACCEPTED: &str = "Accepted";
|
||||
pub const SQUARE_HOLE_DROP_REJECTED: &str = "Rejected";
|
||||
pub const SQUARE_HOLE_DROP_VERSION_CONFLICT: &str = "VersionConflict";
|
||||
pub const SQUARE_HOLE_DROP_RUN_FINISHED: &str = "RunFinished";
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct SquareHoleAgentSessionCreateInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub seed_text: String,
|
||||
pub welcome_message_id: String,
|
||||
pub welcome_message_text: String,
|
||||
pub config_json: Option<String>,
|
||||
pub created_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct SquareHoleAgentSessionGetInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct SquareHoleAgentMessageSubmitInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub user_message_id: String,
|
||||
pub user_message_text: String,
|
||||
pub submitted_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct SquareHoleAgentMessageFinalizeInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub assistant_message_id: Option<String>,
|
||||
pub assistant_reply_text: Option<String>,
|
||||
pub config_json: Option<String>,
|
||||
pub progress_percent: u32,
|
||||
pub stage: String,
|
||||
pub updated_at_micros: i64,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct SquareHoleDraftCompileInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub profile_id: String,
|
||||
pub author_display_name: String,
|
||||
pub game_name: Option<String>,
|
||||
pub summary_text: Option<String>,
|
||||
pub tags_json: Option<String>,
|
||||
pub cover_image_src: Option<String>,
|
||||
pub compiled_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct SquareHoleWorkUpdateInput {
|
||||
pub profile_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub game_name: String,
|
||||
pub theme_text: String,
|
||||
pub twist_rule: String,
|
||||
pub summary_text: String,
|
||||
pub tags_json: String,
|
||||
pub cover_image_src: String,
|
||||
pub shape_count: u32,
|
||||
pub difficulty: u32,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct SquareHoleWorkPublishInput {
|
||||
pub profile_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub published_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct SquareHoleWorksListInput {
|
||||
pub owner_user_id: String,
|
||||
pub published_only: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct SquareHoleWorkGetInput {
|
||||
pub profile_id: String,
|
||||
pub owner_user_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct SquareHoleWorkDeleteInput {
|
||||
pub profile_id: String,
|
||||
pub owner_user_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct SquareHoleRunStartInput {
|
||||
pub run_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub profile_id: String,
|
||||
pub started_at_ms: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct SquareHoleRunGetInput {
|
||||
pub run_id: String,
|
||||
pub owner_user_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct SquareHoleRunDropInput {
|
||||
pub run_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub hole_id: String,
|
||||
pub client_snapshot_version: u64,
|
||||
pub client_event_id: String,
|
||||
pub dropped_at_ms: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct SquareHoleRunStopInput {
|
||||
pub run_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub stopped_at_ms: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct SquareHoleRunRestartInput {
|
||||
pub source_run_id: String,
|
||||
pub next_run_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub restarted_at_ms: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct SquareHoleRunTimeUpInput {
|
||||
pub run_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub finished_at_ms: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct SquareHoleAgentSessionProcedureResult {
|
||||
pub ok: bool,
|
||||
pub session_json: Option<String>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct SquareHoleWorkProcedureResult {
|
||||
pub ok: bool,
|
||||
pub work_json: Option<String>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct SquareHoleWorksProcedureResult {
|
||||
pub ok: bool,
|
||||
pub items_json: Option<String>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct SquareHoleRunProcedureResult {
|
||||
pub ok: bool,
|
||||
pub run_json: Option<String>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct SquareHoleDropShapeProcedureResult {
|
||||
pub ok: bool,
|
||||
pub status: String,
|
||||
pub run_json: Option<String>,
|
||||
pub feedback_json: Option<String>,
|
||||
pub failure_reason: Option<String>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleCreatorConfigSnapshot {
|
||||
pub theme_text: String,
|
||||
pub twist_rule: String,
|
||||
pub shape_count: u32,
|
||||
pub difficulty: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleAgentMessageSnapshot {
|
||||
pub message_id: String,
|
||||
pub session_id: String,
|
||||
pub role: String,
|
||||
pub kind: String,
|
||||
pub text: String,
|
||||
pub created_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleDraftSnapshot {
|
||||
pub profile_id: String,
|
||||
pub game_name: String,
|
||||
pub theme_text: String,
|
||||
pub twist_rule: String,
|
||||
pub summary_text: String,
|
||||
pub tags: Vec<String>,
|
||||
pub shape_count: u32,
|
||||
pub difficulty: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleAgentSessionSnapshot {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub seed_text: String,
|
||||
pub current_turn: u32,
|
||||
pub progress_percent: u32,
|
||||
pub stage: String,
|
||||
pub config: SquareHoleCreatorConfigSnapshot,
|
||||
pub draft: Option<SquareHoleDraftSnapshot>,
|
||||
pub messages: Vec<SquareHoleAgentMessageSnapshot>,
|
||||
pub last_assistant_reply: String,
|
||||
pub published_profile_id: Option<String>,
|
||||
pub created_at_micros: i64,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleWorkSnapshot {
|
||||
pub work_id: String,
|
||||
pub profile_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub source_session_id: String,
|
||||
pub author_display_name: String,
|
||||
pub game_name: String,
|
||||
pub theme_text: String,
|
||||
pub twist_rule: String,
|
||||
pub summary_text: String,
|
||||
pub tags: Vec<String>,
|
||||
pub cover_image_src: String,
|
||||
pub shape_count: u32,
|
||||
pub difficulty: u32,
|
||||
pub config: SquareHoleCreatorConfigSnapshot,
|
||||
pub publication_status: String,
|
||||
pub publish_ready: bool,
|
||||
pub play_count: u32,
|
||||
pub updated_at_micros: i64,
|
||||
pub published_at_micros: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleShapeSnapshot {
|
||||
pub shape_id: String,
|
||||
pub shape_kind: String,
|
||||
pub label: String,
|
||||
pub color: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleHoleSnapshot {
|
||||
pub hole_id: String,
|
||||
pub hole_kind: String,
|
||||
pub label: String,
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleDropFeedbackSnapshot {
|
||||
pub accepted: bool,
|
||||
pub reject_reason: Option<String>,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleRunSnapshot {
|
||||
pub run_id: String,
|
||||
pub profile_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub status: String,
|
||||
pub snapshot_version: u64,
|
||||
pub started_at_ms: i64,
|
||||
pub duration_limit_ms: i64,
|
||||
pub server_now_ms: i64,
|
||||
pub remaining_ms: i64,
|
||||
pub total_shape_count: u32,
|
||||
pub completed_shape_count: u32,
|
||||
pub combo: u32,
|
||||
pub best_combo: u32,
|
||||
pub score: u32,
|
||||
pub rule_label: String,
|
||||
pub current_shape: Option<SquareHoleShapeSnapshot>,
|
||||
pub holes: Vec<SquareHoleHoleSnapshot>,
|
||||
pub last_feedback: Option<SquareHoleDropFeedbackSnapshot>,
|
||||
}
|
||||
Reference in New Issue
Block a user