Merge master into user play stats branch
Some checks failed
CI / verify (pull_request) Has been cancelled
Some checks failed
CI / verify (pull_request) Has been cancelled
This commit is contained in:
@@ -94,9 +94,10 @@ use crate::{
|
||||
runtime_chat::stream_runtime_npc_chat_turn,
|
||||
runtime_inventory::get_runtime_inventory_state,
|
||||
runtime_profile::{
|
||||
admin_disable_profile_redeem_code, admin_upsert_profile_redeem_code,
|
||||
create_profile_recharge_order, get_profile_dashboard, get_profile_play_stats,
|
||||
get_profile_recharge_center, get_profile_referral_invite_center, get_profile_wallet_ledger,
|
||||
redeem_profile_referral_invite_code,
|
||||
redeem_profile_referral_invite_code, redeem_profile_reward_code,
|
||||
},
|
||||
runtime_save::{
|
||||
delete_runtime_snapshot, get_runtime_snapshot, list_profile_save_archives,
|
||||
@@ -143,6 +144,20 @@ pub fn build_router(state: AppState) -> Router {
|
||||
require_admin_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/admin/api/profile/redeem-codes",
|
||||
post(admin_upsert_profile_redeem_code).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_admin_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/admin/api/profile/redeem-codes/disable",
|
||||
post(admin_disable_profile_redeem_code).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_admin_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/healthz",
|
||||
get(|Extension(request_context): Extension<_>| async move {
|
||||
@@ -581,6 +596,13 @@ pub fn build_router(state: AppState) -> Router {
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/big-fish/works/{session_id}/play",
|
||||
post(record_big_fish_play).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/puzzle/agent/sessions",
|
||||
post(create_puzzle_agent_session).route_layer(middleware::from_fn_with_state(
|
||||
@@ -854,6 +876,20 @@ pub fn build_router(state: AppState) -> Router {
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/profile/redeem-codes/redeem",
|
||||
post(redeem_profile_reward_code).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/profile/redeem-codes/redeem",
|
||||
post(redeem_profile_reward_code).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/profile/play-stats",
|
||||
get(get_profile_play_stats).route_layer(middleware::from_fn_with_state(
|
||||
@@ -1360,6 +1396,36 @@ mod tests {
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn password_entry_dev_auto_register_creates_unknown_phone_when_enabled() {
|
||||
let config = AppConfig {
|
||||
dev_password_entry_auto_register_enabled: true,
|
||||
..AppConfig::default()
|
||||
};
|
||||
let app = build_router(AppState::new(config).expect("state should build"));
|
||||
|
||||
let first_response =
|
||||
password_login_request(app.clone(), "13800138023", TEST_PASSWORD).await;
|
||||
let first_status = first_response.status();
|
||||
let first_body = first_response
|
||||
.into_body()
|
||||
.collect()
|
||||
.await
|
||||
.expect("first response body should collect")
|
||||
.to_bytes();
|
||||
let first_payload: Value =
|
||||
serde_json::from_slice(&first_body).expect("first response body should be valid json");
|
||||
let second_response = password_login_request(app, "13800138023", TEST_PASSWORD).await;
|
||||
|
||||
assert_eq!(first_status, StatusCode::OK);
|
||||
assert!(first_payload["token"].as_str().is_some());
|
||||
assert_eq!(
|
||||
first_payload["user"]["loginMethod"],
|
||||
Value::String("password".to_string())
|
||||
);
|
||||
assert_eq!(second_response.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn password_entry_logs_in_existing_phone_user_and_sets_refresh_cookie() {
|
||||
let state = AppState::new(AppConfig::default()).expect("state should build");
|
||||
|
||||
@@ -211,7 +211,7 @@ pub async fn record_big_fish_play(
|
||||
})?;
|
||||
ensure_non_empty(&request_context, &session_id, "sessionId")?;
|
||||
|
||||
let session = state
|
||||
let items = state
|
||||
.spacetime_client()
|
||||
.record_big_fish_play(BigFishPlayReportRecordInput {
|
||||
session_id,
|
||||
@@ -226,8 +226,11 @@ pub async fn record_big_fish_play(
|
||||
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
BigFishSessionResponse {
|
||||
session: map_big_fish_session_response(session),
|
||||
BigFishWorksResponse {
|
||||
items: items
|
||||
.into_iter()
|
||||
.map(map_big_fish_work_summary_response)
|
||||
.collect(),
|
||||
},
|
||||
))
|
||||
}
|
||||
@@ -965,6 +968,7 @@ fn map_big_fish_work_summary_response(
|
||||
level_main_image_ready_count: item.level_main_image_ready_count,
|
||||
level_motion_ready_count: item.level_motion_ready_count,
|
||||
background_ready: item.background_ready,
|
||||
play_count: item.play_count,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ pub struct AppConfig {
|
||||
pub refresh_cookie_same_site: String,
|
||||
pub refresh_session_ttl_days: u32,
|
||||
pub auth_store_path: PathBuf,
|
||||
pub dev_password_entry_auto_register_enabled: bool,
|
||||
pub sms_auth_enabled: bool,
|
||||
pub sms_auth_provider: String,
|
||||
pub sms_endpoint: String,
|
||||
@@ -118,6 +119,7 @@ impl Default for AppConfig {
|
||||
refresh_cookie_same_site: "Lax".to_string(),
|
||||
refresh_session_ttl_days: 30,
|
||||
auth_store_path: PathBuf::from(DEFAULT_AUTH_STORE_PATH),
|
||||
dev_password_entry_auto_register_enabled: false,
|
||||
sms_auth_enabled: false,
|
||||
sms_auth_provider: "mock".to_string(),
|
||||
sms_endpoint: "dypnsapi.aliyuncs.com".to_string(),
|
||||
@@ -273,6 +275,11 @@ impl AppConfig {
|
||||
if let Some(auth_store_path) = read_first_non_empty_env(&["GENARRATIVE_AUTH_STORE_PATH"]) {
|
||||
config.auth_store_path = PathBuf::from(auth_store_path);
|
||||
}
|
||||
if let Some(enabled) =
|
||||
read_first_bool_env(&["GENARRATIVE_DEV_PASSWORD_ENTRY_AUTO_REGISTER_ENABLED"])
|
||||
{
|
||||
config.dev_password_entry_auto_register_enabled = enabled;
|
||||
}
|
||||
|
||||
if let Some(sms_auth_enabled) = read_first_bool_env(&["SMS_AUTH_ENABLED"]) {
|
||||
config.sms_auth_enabled = sms_auth_enabled;
|
||||
|
||||
@@ -94,19 +94,19 @@
|
||||
},
|
||||
{
|
||||
"templateId": "big_fish",
|
||||
"displayName": "大鱼吃小鱼共创",
|
||||
"displayName": "大鱼吃小鱼",
|
||||
"creationGoal": "收束成可直接编译为竖屏大鱼吃小鱼玩法草稿的成长、生态、节奏方案。",
|
||||
"anchorQuestions": [
|
||||
{
|
||||
"key": "gameplayPromise",
|
||||
"label": "玩法承诺",
|
||||
"question": "这版大鱼吃小鱼最核心的吞噬成长爽点是什么?",
|
||||
"label": "玩法爽点",
|
||||
"question": "核心的吞噬成长爽点是什么?",
|
||||
"requiredEffect": "明确玩家为什么要持续吞噬、升级和冒险。"
|
||||
},
|
||||
{
|
||||
"key": "ecologyVisualTheme",
|
||||
"label": "生态视觉主题",
|
||||
"question": "鱼群、场景和敌我生态的视觉主题是什么?",
|
||||
"label": "视觉主题",
|
||||
"question": "场景和形象的视觉主题是什么?",
|
||||
"requiredEffect": "提供后续角色图、动作图和背景图的一致视觉方向。"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -26,14 +26,19 @@ pub async fn password_entry(
|
||||
headers: HeaderMap,
|
||||
Json(payload): Json<PasswordEntryRequest>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let result = state
|
||||
.password_entry_service()
|
||||
.execute(PasswordEntryInput {
|
||||
phone_number: payload.phone,
|
||||
password: payload.password,
|
||||
})
|
||||
.await
|
||||
.map_err(map_password_entry_error)?;
|
||||
let input = PasswordEntryInput {
|
||||
phone_number: payload.phone,
|
||||
password: payload.password,
|
||||
};
|
||||
let result = if state.config.dev_password_entry_auto_register_enabled {
|
||||
state
|
||||
.password_entry_service()
|
||||
.execute_with_dev_registration(input)
|
||||
.await
|
||||
} else {
|
||||
state.password_entry_service().execute(input).await
|
||||
}
|
||||
.map_err(map_password_entry_error)?;
|
||||
let session_client = resolve_session_client_context(&headers);
|
||||
let signed_session = create_password_auth_session(&state, &result.user, &session_client)?;
|
||||
state
|
||||
|
||||
@@ -7,30 +7,36 @@ use axum::{
|
||||
use module_runtime::{
|
||||
PROFILE_RECHARGE_PAYMENT_CHANNEL_MOCK, RuntimeProfileMembershipBenefitRecord,
|
||||
RuntimeProfileRechargeCenterRecord, RuntimeProfileRechargeOrderRecord,
|
||||
RuntimeProfileRechargeProductRecord, RuntimeProfileWalletLedgerSourceType,
|
||||
RuntimeReferralInviteCenterRecord, RuntimeReferralRedeemRecord,
|
||||
RuntimeProfileRechargeProductRecord, RuntimeProfileRedeemCodeMode,
|
||||
RuntimeProfileRedeemCodeRecord, RuntimeProfileRewardCodeRedeemRecord,
|
||||
RuntimeProfileWalletLedgerSourceType, RuntimeReferralInviteCenterRecord,
|
||||
RuntimeReferralRedeemRecord,
|
||||
};
|
||||
use serde_json::{Value, json};
|
||||
use shared_contracts::runtime::{
|
||||
AdminDisableProfileRedeemCodeRequest, AdminUpsertProfileRedeemCodeRequest,
|
||||
CreateProfileRechargeOrderRequest, CreateProfileRechargeOrderResponse,
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_GENERATION_CONSUME,
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_GENERATION_REFUND,
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_INVITE_INVITEE_REWARD,
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_INVITE_INVITER_REWARD,
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_POINTS_RECHARGE,
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_REDEEM_CODE_REWARD,
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_SNAPSHOT_SYNC, ProfileDashboardSummaryResponse,
|
||||
ProfileMembershipBenefitResponse, ProfileMembershipResponse, ProfilePlayStatsResponse,
|
||||
ProfilePlayedWorkSummaryResponse, ProfileRechargeCenterResponse, ProfileRechargeOrderResponse,
|
||||
ProfileRechargeProductResponse, ProfileReferralInviteCenterResponse,
|
||||
ProfileWalletLedgerEntryResponse, ProfileWalletLedgerResponse,
|
||||
RedeemProfileReferralInviteCodeRequest, RedeemProfileReferralInviteCodeResponse,
|
||||
ProfileRechargeProductResponse, ProfileRedeemCodeAdminResponse,
|
||||
ProfileReferralInviteCenterResponse, ProfileWalletLedgerEntryResponse,
|
||||
ProfileWalletLedgerResponse, RedeemProfileReferralInviteCodeRequest,
|
||||
RedeemProfileReferralInviteCodeResponse, RedeemProfileRewardCodeRequest,
|
||||
RedeemProfileRewardCodeResponse,
|
||||
};
|
||||
use spacetime_client::SpacetimeClientError;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use crate::{
|
||||
api_response::json_success_body, auth::AuthenticatedAccessToken, http_error::AppError,
|
||||
request_context::RequestContext, state::AppState,
|
||||
admin::AuthenticatedAdmin, api_response::json_success_body, auth::AuthenticatedAccessToken,
|
||||
http_error::AppError, request_context::RequestContext, state::AppState,
|
||||
};
|
||||
|
||||
pub async fn get_profile_dashboard(
|
||||
@@ -118,6 +124,9 @@ fn format_profile_wallet_ledger_source_type(
|
||||
RuntimeProfileWalletLedgerSourceType::AssetGenerationRefund => {
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_GENERATION_REFUND
|
||||
}
|
||||
RuntimeProfileWalletLedgerSourceType::RedeemCodeReward => {
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_REDEEM_CODE_REWARD
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,6 +237,99 @@ pub async fn redeem_profile_referral_invite_code(
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn redeem_profile_reward_code(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Json(payload): Json<RedeemProfileRewardCodeRequest>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
let user_id = authenticated.claims().user_id().to_string();
|
||||
let redeemed_at_micros = OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000;
|
||||
let record = state
|
||||
.spacetime_client()
|
||||
.redeem_profile_reward_code(user_id, payload.code, redeemed_at_micros as i64)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
runtime_profile_error_response(
|
||||
&request_context,
|
||||
map_runtime_profile_client_error(error),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
build_redeem_profile_reward_code_response(record),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn admin_upsert_profile_redeem_code(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(admin): Extension<AuthenticatedAdmin>,
|
||||
Json(payload): Json<AdminUpsertProfileRedeemCodeRequest>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
let updated_at_micros = OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000;
|
||||
let mode = parse_profile_redeem_code_mode(&payload.mode).map_err(|error| {
|
||||
runtime_profile_error_response(
|
||||
&request_context,
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_message(error),
|
||||
)
|
||||
})?;
|
||||
let record = state
|
||||
.spacetime_client()
|
||||
.admin_upsert_profile_redeem_code(
|
||||
admin.session().subject.clone(),
|
||||
payload.code,
|
||||
mode,
|
||||
payload.reward_points,
|
||||
payload.max_uses,
|
||||
payload.enabled,
|
||||
payload.allowed_user_ids,
|
||||
payload.allowed_public_user_codes,
|
||||
updated_at_micros as i64,
|
||||
)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
runtime_profile_error_response(
|
||||
&request_context,
|
||||
map_runtime_profile_client_error(error),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
build_profile_redeem_code_admin_response(record),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn admin_disable_profile_redeem_code(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(admin): Extension<AuthenticatedAdmin>,
|
||||
Json(payload): Json<AdminDisableProfileRedeemCodeRequest>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
let updated_at_micros = OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000;
|
||||
let record = state
|
||||
.spacetime_client()
|
||||
.admin_disable_profile_redeem_code(
|
||||
admin.session().subject.clone(),
|
||||
payload.code,
|
||||
updated_at_micros as i64,
|
||||
)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
runtime_profile_error_response(
|
||||
&request_context,
|
||||
map_runtime_profile_client_error(error),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
build_profile_redeem_code_admin_response(record),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn get_profile_play_stats(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
@@ -396,6 +498,49 @@ fn build_redeem_profile_referral_invite_code_response(
|
||||
}
|
||||
}
|
||||
|
||||
fn build_redeem_profile_reward_code_response(
|
||||
record: RuntimeProfileRewardCodeRedeemRecord,
|
||||
) -> RedeemProfileRewardCodeResponse {
|
||||
RedeemProfileRewardCodeResponse {
|
||||
wallet_balance: record.wallet_balance,
|
||||
amount_granted: record.amount_granted,
|
||||
ledger_entry: ProfileWalletLedgerEntryResponse {
|
||||
id: record.ledger_entry.wallet_ledger_id,
|
||||
amount_delta: record.ledger_entry.amount_delta,
|
||||
balance_after: record.ledger_entry.balance_after,
|
||||
source_type: format_profile_wallet_ledger_source_type(record.ledger_entry.source_type)
|
||||
.to_string(),
|
||||
created_at: record.ledger_entry.created_at,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_profile_redeem_code_mode(raw: &str) -> Result<RuntimeProfileRedeemCodeMode, String> {
|
||||
match raw.trim().to_ascii_lowercase().as_str() {
|
||||
"public" => Ok(RuntimeProfileRedeemCodeMode::Public),
|
||||
"unique" => Ok(RuntimeProfileRedeemCodeMode::Unique),
|
||||
"private" => Ok(RuntimeProfileRedeemCodeMode::Private),
|
||||
_ => Err("兑换码类型无效".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_profile_redeem_code_admin_response(
|
||||
record: RuntimeProfileRedeemCodeRecord,
|
||||
) -> ProfileRedeemCodeAdminResponse {
|
||||
ProfileRedeemCodeAdminResponse {
|
||||
code: record.code,
|
||||
mode: record.mode.as_str().to_string(),
|
||||
reward_points: record.reward_points,
|
||||
max_uses: record.max_uses,
|
||||
global_used_count: record.global_used_count,
|
||||
enabled: record.enabled,
|
||||
allowed_user_ids: record.allowed_user_ids,
|
||||
created_by: record.created_by,
|
||||
created_at: record.created_at,
|
||||
updated_at: record.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use module_runtime::RuntimeProfileWalletLedgerSourceType;
|
||||
|
||||
@@ -486,6 +486,38 @@ impl PasswordEntryService {
|
||||
verify_stored_password_user(existing_user, &input.password).await
|
||||
}
|
||||
|
||||
pub async fn execute_with_dev_registration(
|
||||
&self,
|
||||
input: PasswordEntryInput,
|
||||
) -> Result<PasswordEntryResult, PasswordEntryError> {
|
||||
validate_password(&input.password)?;
|
||||
let normalized_phone = normalize_mainland_china_phone_number(&input.phone_number)
|
||||
.map_err(|_| PasswordEntryError::InvalidPhoneNumber)?;
|
||||
if let Some(existing_user) = self
|
||||
.store
|
||||
.find_by_phone_number_for_password(&normalized_phone.e164)?
|
||||
{
|
||||
return verify_stored_password_user(existing_user, &input.password).await;
|
||||
}
|
||||
|
||||
let password_hash = hash_password(&input.password)
|
||||
.await
|
||||
.map_err(|error| PasswordEntryError::PasswordHash(error.to_string()))?;
|
||||
let user = self.store.create_dev_password_phone_user(
|
||||
normalized_phone.clone(),
|
||||
normalized_phone.masked_national_number,
|
||||
password_hash,
|
||||
)?;
|
||||
|
||||
Ok(PasswordEntryResult {
|
||||
user: AuthUser {
|
||||
login_method: AuthLoginMethod::Password,
|
||||
..user
|
||||
},
|
||||
created: true,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_user_by_id(
|
||||
&self,
|
||||
user_id: &str,
|
||||
@@ -1336,6 +1368,53 @@ impl InMemoryAuthStore {
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
fn create_dev_password_phone_user(
|
||||
&self,
|
||||
phone_number: PhoneNumberSnapshot,
|
||||
display_name: String,
|
||||
password_hash: String,
|
||||
) -> Result<AuthUser, PasswordEntryError> {
|
||||
let mut state = self
|
||||
.inner
|
||||
.lock()
|
||||
.map_err(|_| PasswordEntryError::Store("用户仓储锁已中毒".to_string()))?;
|
||||
if state.phone_to_user_id.contains_key(&phone_number.e164) {
|
||||
return Err(PasswordEntryError::InvalidCredentials);
|
||||
}
|
||||
|
||||
let sequence = state.next_user_id;
|
||||
let user_id = format!("user_{sequence:08}");
|
||||
let public_user_code = build_public_user_code(sequence);
|
||||
state.next_user_id += 1;
|
||||
let username = build_system_username("phone", state.next_user_id);
|
||||
let user = AuthUser {
|
||||
id: user_id.clone(),
|
||||
public_user_code,
|
||||
username: username.clone(),
|
||||
display_name,
|
||||
phone_number_masked: Some(phone_number.masked_national_number.clone()),
|
||||
login_method: AuthLoginMethod::Password,
|
||||
binding_status: AuthBindingStatus::Active,
|
||||
wechat_bound: false,
|
||||
token_version: 1,
|
||||
};
|
||||
state
|
||||
.phone_to_user_id
|
||||
.insert(phone_number.e164.clone(), user_id);
|
||||
state.users_by_username.insert(
|
||||
username,
|
||||
StoredPasswordUser {
|
||||
user: user.clone(),
|
||||
password_hash,
|
||||
password_login_enabled: true,
|
||||
phone_number: Some(phone_number.e164),
|
||||
},
|
||||
);
|
||||
self.persist_password_state(&state)?;
|
||||
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
fn create_pending_wechat_user(
|
||||
&self,
|
||||
profile: WechatIdentityProfile,
|
||||
@@ -2474,6 +2553,39 @@ mod tests {
|
||||
assert_eq!(error, PasswordEntryError::InvalidCredentials);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn password_entry_dev_registration_creates_unknown_phone_user() {
|
||||
let service = build_password_service(build_store());
|
||||
|
||||
let created = service
|
||||
.execute_with_dev_registration(PasswordEntryInput {
|
||||
phone_number: "13800138009".to_string(),
|
||||
password: "secret123".to_string(),
|
||||
})
|
||||
.await
|
||||
.expect("dev registration should create user");
|
||||
let reused = service
|
||||
.execute_with_dev_registration(PasswordEntryInput {
|
||||
phone_number: "13800138009".to_string(),
|
||||
password: "secret123".to_string(),
|
||||
})
|
||||
.await
|
||||
.expect("same password should reuse created user");
|
||||
let wrong_password = service
|
||||
.execute_with_dev_registration(PasswordEntryInput {
|
||||
phone_number: "13800138009".to_string(),
|
||||
password: "secret999".to_string(),
|
||||
})
|
||||
.await
|
||||
.expect_err("existing user still requires the right password");
|
||||
|
||||
assert!(created.created);
|
||||
assert_eq!(created.user.login_method, AuthLoginMethod::Password);
|
||||
assert!(!reused.created);
|
||||
assert_eq!(created.user.id, reused.user.id);
|
||||
assert_eq!(wrong_password, PasswordEntryError::InvalidCredentials);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn phone_user_can_set_password_then_login() {
|
||||
let store = build_store();
|
||||
|
||||
@@ -221,6 +221,7 @@ pub struct BigFishWorkSummarySnapshot {
|
||||
pub level_main_image_ready_count: u32,
|
||||
pub level_motion_ready_count: u32,
|
||||
pub background_ready: bool,
|
||||
pub play_count: u32,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
@@ -318,11 +319,11 @@ pub struct BigFishPublishInput {
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishPlayReportInput {
|
||||
pub struct BigFishPlayRecordInput {
|
||||
pub session_id: String,
|
||||
pub user_id: String,
|
||||
pub elapsed_ms: u64,
|
||||
pub reported_at_micros: i64,
|
||||
pub played_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@@ -663,7 +664,7 @@ pub fn validate_publish_input(input: &BigFishPublishInput) -> Result<(), BigFish
|
||||
validate_session_owner(&input.session_id, &input.owner_user_id)
|
||||
}
|
||||
|
||||
pub fn validate_play_report_input(input: &BigFishPlayReportInput) -> Result<(), BigFishFieldError> {
|
||||
pub fn validate_play_record_input(input: &BigFishPlayRecordInput) -> Result<(), BigFishFieldError> {
|
||||
if normalize_required_string(&input.session_id).is_none() {
|
||||
return Err(BigFishFieldError::MissingSessionId);
|
||||
}
|
||||
|
||||
@@ -1964,14 +1964,18 @@ fn with_next_board(run: &PuzzleRunSnapshot, next_board: PuzzleBoardSnapshot) ->
|
||||
if current_level.status != PuzzleRuntimeLevelStatus::Cleared && is_cleared {
|
||||
let cleared_at_ms = current_unix_ms();
|
||||
current_level.cleared_at_ms = Some(cleared_at_ms);
|
||||
current_level.elapsed_ms =
|
||||
Some(cleared_at_ms.saturating_sub(current_level.started_at_ms).max(1_000));
|
||||
current_level.elapsed_ms = Some(
|
||||
cleared_at_ms
|
||||
.saturating_sub(current_level.started_at_ms)
|
||||
.max(1_000),
|
||||
);
|
||||
}
|
||||
current_level.status = next_level_status;
|
||||
}
|
||||
|
||||
if is_cleared && run.current_level.as_ref().map(|level| level.status)
|
||||
!= Some(PuzzleRuntimeLevelStatus::Cleared)
|
||||
if is_cleared
|
||||
&& run.current_level.as_ref().map(|level| level.status)
|
||||
!= Some(PuzzleRuntimeLevelStatus::Cleared)
|
||||
{
|
||||
next_run.cleared_level_count += 1;
|
||||
}
|
||||
|
||||
@@ -261,6 +261,15 @@ pub enum RuntimeProfileWalletLedgerSourceType {
|
||||
PointsRecharge,
|
||||
AssetGenerationConsume,
|
||||
AssetGenerationRefund,
|
||||
RedeemCodeReward,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum RuntimeProfileRedeemCodeMode {
|
||||
Public,
|
||||
Unique,
|
||||
Private,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
@@ -424,6 +433,75 @@ pub struct RuntimeProfileWalletAdjustmentInput {
|
||||
pub created_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RuntimeProfileRewardCodeRedeemInput {
|
||||
pub user_id: String,
|
||||
pub code: String,
|
||||
pub redeemed_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RuntimeProfileRewardCodeRedeemSnapshot {
|
||||
pub wallet_balance: u64,
|
||||
pub amount_granted: u64,
|
||||
pub ledger_entry: RuntimeProfileWalletLedgerEntrySnapshot,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RuntimeProfileRewardCodeRedeemProcedureResult {
|
||||
pub ok: bool,
|
||||
pub record: Option<RuntimeProfileRewardCodeRedeemSnapshot>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RuntimeProfileRedeemCodeAdminUpsertInput {
|
||||
pub admin_user_id: String,
|
||||
pub code: String,
|
||||
pub mode: RuntimeProfileRedeemCodeMode,
|
||||
pub reward_points: u64,
|
||||
pub max_uses: u32,
|
||||
pub enabled: bool,
|
||||
pub allowed_user_ids: Vec<String>,
|
||||
pub allowed_public_user_codes: Vec<String>,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RuntimeProfileRedeemCodeAdminDisableInput {
|
||||
pub admin_user_id: String,
|
||||
pub code: String,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RuntimeProfileRedeemCodeSnapshot {
|
||||
pub code: String,
|
||||
pub mode: RuntimeProfileRedeemCodeMode,
|
||||
pub reward_points: u64,
|
||||
pub max_uses: u32,
|
||||
pub global_used_count: u32,
|
||||
pub enabled: bool,
|
||||
pub allowed_user_ids: Vec<String>,
|
||||
pub created_by: String,
|
||||
pub created_at_micros: i64,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RuntimeProfileRedeemCodeAdminProcedureResult {
|
||||
pub ok: bool,
|
||||
pub record: Option<RuntimeProfileRedeemCodeSnapshot>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RuntimeReferralInviteCenterSnapshot {
|
||||
@@ -537,6 +615,9 @@ pub enum RuntimeProfileFieldError {
|
||||
MissingLedgerId,
|
||||
InvalidWalletAmount,
|
||||
MissingInviteCode,
|
||||
MissingRedeemCode,
|
||||
InvalidRedeemCodeReward,
|
||||
InvalidRedeemCodeMaxUses,
|
||||
MissingProductId,
|
||||
MissingWorldKey,
|
||||
MissingBottomTab,
|
||||
@@ -812,6 +893,29 @@ pub struct RuntimeProfileRechargeCenterRecord {
|
||||
pub has_points_recharged: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct RuntimeProfileRewardCodeRedeemRecord {
|
||||
pub wallet_balance: u64,
|
||||
pub amount_granted: u64,
|
||||
pub ledger_entry: RuntimeProfileWalletLedgerEntryRecord,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct RuntimeProfileRedeemCodeRecord {
|
||||
pub code: String,
|
||||
pub mode: RuntimeProfileRedeemCodeMode,
|
||||
pub reward_points: u64,
|
||||
pub max_uses: u32,
|
||||
pub global_used_count: u32,
|
||||
pub enabled: bool,
|
||||
pub allowed_user_ids: Vec<String>,
|
||||
pub created_by: String,
|
||||
pub created_at: String,
|
||||
pub created_at_micros: i64,
|
||||
pub updated_at: String,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct RuntimeReferralInviteCenterRecord {
|
||||
pub user_id: String,
|
||||
@@ -970,6 +1074,73 @@ pub fn build_runtime_referral_redeem_input(
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build_runtime_profile_reward_code_redeem_input(
|
||||
user_id: String,
|
||||
code: String,
|
||||
redeemed_at_micros: i64,
|
||||
) -> Result<RuntimeProfileRewardCodeRedeemInput, RuntimeProfileFieldError> {
|
||||
let user_id = normalize_runtime_profile_user_id(user_id)?;
|
||||
let code = normalize_redeem_code(code).ok_or(RuntimeProfileFieldError::MissingRedeemCode)?;
|
||||
Ok(RuntimeProfileRewardCodeRedeemInput {
|
||||
user_id,
|
||||
code,
|
||||
redeemed_at_micros,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build_runtime_profile_redeem_code_admin_upsert_input(
|
||||
admin_user_id: String,
|
||||
code: String,
|
||||
mode: RuntimeProfileRedeemCodeMode,
|
||||
reward_points: u64,
|
||||
max_uses: u32,
|
||||
enabled: bool,
|
||||
allowed_user_ids: Vec<String>,
|
||||
allowed_public_user_codes: Vec<String>,
|
||||
updated_at_micros: i64,
|
||||
) -> Result<RuntimeProfileRedeemCodeAdminUpsertInput, RuntimeProfileFieldError> {
|
||||
let admin_user_id = normalize_runtime_profile_user_id(admin_user_id)?;
|
||||
let code = normalize_redeem_code(code).ok_or(RuntimeProfileFieldError::MissingRedeemCode)?;
|
||||
if reward_points == 0 {
|
||||
return Err(RuntimeProfileFieldError::InvalidRedeemCodeReward);
|
||||
}
|
||||
if max_uses == 0 {
|
||||
return Err(RuntimeProfileFieldError::InvalidRedeemCodeMaxUses);
|
||||
}
|
||||
|
||||
Ok(RuntimeProfileRedeemCodeAdminUpsertInput {
|
||||
admin_user_id,
|
||||
code,
|
||||
mode,
|
||||
reward_points,
|
||||
max_uses,
|
||||
enabled,
|
||||
allowed_user_ids: allowed_user_ids
|
||||
.into_iter()
|
||||
.filter_map(|value| normalize_optional_string(Some(value)))
|
||||
.collect(),
|
||||
allowed_public_user_codes: allowed_public_user_codes
|
||||
.into_iter()
|
||||
.filter_map(|value| normalize_optional_string(Some(value)))
|
||||
.collect(),
|
||||
updated_at_micros,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build_runtime_profile_redeem_code_admin_disable_input(
|
||||
admin_user_id: String,
|
||||
code: String,
|
||||
updated_at_micros: i64,
|
||||
) -> Result<RuntimeProfileRedeemCodeAdminDisableInput, RuntimeProfileFieldError> {
|
||||
let admin_user_id = normalize_runtime_profile_user_id(admin_user_id)?;
|
||||
let code = normalize_redeem_code(code).ok_or(RuntimeProfileFieldError::MissingRedeemCode)?;
|
||||
Ok(RuntimeProfileRedeemCodeAdminDisableInput {
|
||||
admin_user_id,
|
||||
code,
|
||||
updated_at_micros,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build_runtime_profile_play_stats_get_input(
|
||||
user_id: String,
|
||||
) -> Result<RuntimeProfilePlayStatsGetInput, RuntimeProfileFieldError> {
|
||||
@@ -1323,6 +1494,35 @@ pub fn build_runtime_referral_redeem_record(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_runtime_profile_reward_code_redeem_record(
|
||||
snapshot: RuntimeProfileRewardCodeRedeemSnapshot,
|
||||
) -> RuntimeProfileRewardCodeRedeemRecord {
|
||||
RuntimeProfileRewardCodeRedeemRecord {
|
||||
wallet_balance: snapshot.wallet_balance,
|
||||
amount_granted: snapshot.amount_granted,
|
||||
ledger_entry: build_runtime_profile_wallet_ledger_entry_record(snapshot.ledger_entry),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_runtime_profile_redeem_code_record(
|
||||
snapshot: RuntimeProfileRedeemCodeSnapshot,
|
||||
) -> RuntimeProfileRedeemCodeRecord {
|
||||
RuntimeProfileRedeemCodeRecord {
|
||||
code: snapshot.code,
|
||||
mode: snapshot.mode,
|
||||
reward_points: snapshot.reward_points,
|
||||
max_uses: snapshot.max_uses,
|
||||
global_used_count: snapshot.global_used_count,
|
||||
enabled: snapshot.enabled,
|
||||
allowed_user_ids: snapshot.allowed_user_ids,
|
||||
created_by: snapshot.created_by,
|
||||
created_at: format_utc_micros(snapshot.created_at_micros),
|
||||
created_at_micros: snapshot.created_at_micros,
|
||||
updated_at: format_utc_micros(snapshot.updated_at_micros),
|
||||
updated_at_micros: snapshot.updated_at_micros,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_runtime_profile_played_world_record(
|
||||
snapshot: RuntimeProfilePlayedWorldSnapshot,
|
||||
) -> RuntimeProfilePlayedWorldRecord {
|
||||
@@ -1508,6 +1708,17 @@ impl RuntimeProfileWalletLedgerSourceType {
|
||||
Self::PointsRecharge => "points_recharge",
|
||||
Self::AssetGenerationConsume => "asset_generation_consume",
|
||||
Self::AssetGenerationRefund => "asset_generation_refund",
|
||||
Self::RedeemCodeReward => "redeem_code_reward",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RuntimeProfileRedeemCodeMode {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Public => "public",
|
||||
Self::Unique => "unique",
|
||||
Self::Private => "private",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1736,6 +1947,10 @@ pub fn normalize_invite_code(value: String) -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn normalize_redeem_code(value: String) -> Option<String> {
|
||||
normalize_invite_code(value)
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RuntimeProfileFieldError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
@@ -1743,6 +1958,9 @@ impl std::fmt::Display for RuntimeProfileFieldError {
|
||||
Self::MissingLedgerId => f.write_str("profile.wallet_ledger_id 不能为空"),
|
||||
Self::InvalidWalletAmount => f.write_str("profile.wallet_amount 必须大于 0"),
|
||||
Self::MissingInviteCode => f.write_str("referral.invite_code 不能为空"),
|
||||
Self::MissingRedeemCode => f.write_str("兑换码不能为空"),
|
||||
Self::InvalidRedeemCodeReward => f.write_str("兑换码奖励无效"),
|
||||
Self::InvalidRedeemCodeMaxUses => f.write_str("兑换次数必须大于 0"),
|
||||
Self::MissingProductId => f.write_str("recharge.product_id 不能为空"),
|
||||
Self::MissingWorldKey => f.write_str("profile.world_key 不能为空"),
|
||||
Self::MissingBottomTab => f.write_str("runtime_snapshot.bottom_tab 不能为空"),
|
||||
|
||||
@@ -18,6 +18,8 @@ pub struct BigFishWorkSummaryResponse {
|
||||
pub level_main_image_ready_count: u32,
|
||||
pub level_motion_ready_count: u32,
|
||||
pub background_ready: bool,
|
||||
#[serde(default)]
|
||||
pub play_count: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
|
||||
@@ -11,6 +11,7 @@ pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_GENERATION_CONSUME: &str =
|
||||
"asset_generation_consume";
|
||||
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_GENERATION_REFUND: &str =
|
||||
"asset_generation_refund";
|
||||
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_REDEEM_CODE_REWARD: &str = "redeem_code_reward";
|
||||
pub const BROWSE_HISTORY_THEME_MODE_MARTIAL: &str = "martial";
|
||||
pub const BROWSE_HISTORY_THEME_MODE_ARCANE: &str = "arcane";
|
||||
pub const BROWSE_HISTORY_THEME_MODE_MACHINA: &str = "machina";
|
||||
@@ -258,6 +259,60 @@ pub struct RedeemProfileReferralInviteCodeResponse {
|
||||
pub inviter_balance_after: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RedeemProfileRewardCodeRequest {
|
||||
pub code: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RedeemProfileRewardCodeResponse {
|
||||
pub wallet_balance: u64,
|
||||
pub amount_granted: u64,
|
||||
pub ledger_entry: ProfileWalletLedgerEntryResponse,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminUpsertProfileRedeemCodeRequest {
|
||||
pub code: String,
|
||||
pub mode: String,
|
||||
pub reward_points: u64,
|
||||
pub max_uses: u32,
|
||||
#[serde(default = "default_true")]
|
||||
pub enabled: bool,
|
||||
#[serde(default)]
|
||||
pub allowed_user_ids: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub allowed_public_user_codes: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminDisableProfileRedeemCodeRequest {
|
||||
pub code: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProfileRedeemCodeAdminResponse {
|
||||
pub code: String,
|
||||
pub mode: String,
|
||||
pub reward_points: u64,
|
||||
pub max_uses: u32,
|
||||
pub global_used_count: u32,
|
||||
pub enabled: bool,
|
||||
pub allowed_user_ids: Vec<String>,
|
||||
pub created_by: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProfilePlayedWorkSummaryResponse {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use super::*;
|
||||
use crate::mapper::*;
|
||||
use crate::module_bindings::delete_big_fish_work_procedure::delete_big_fish_work;
|
||||
use crate::module_bindings::record_big_fish_play_procedure::record_big_fish_play;
|
||||
|
||||
impl SpacetimeClient {
|
||||
pub async fn create_big_fish_session(
|
||||
@@ -80,12 +81,22 @@ impl SpacetimeClient {
|
||||
procedure_input: BigFishWorksListInput,
|
||||
) -> Result<Vec<BigFishWorkSummaryRecord>, SpacetimeClientError> {
|
||||
self.call_after_connect(move |connection, sender| {
|
||||
let fallback_owner_user_id = if procedure_input.published_only {
|
||||
None
|
||||
} else {
|
||||
Some(procedure_input.owner_user_id.clone())
|
||||
};
|
||||
connection
|
||||
.procedures()
|
||||
.list_big_fish_works_then(procedure_input, move |_, result| {
|
||||
let mapped = result
|
||||
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||
.and_then(map_big_fish_works_procedure_result);
|
||||
.and_then(|result| {
|
||||
map_big_fish_works_procedure_result(
|
||||
result,
|
||||
fallback_owner_user_id.as_deref(),
|
||||
)
|
||||
});
|
||||
send_once(&sender, mapped);
|
||||
});
|
||||
})
|
||||
@@ -103,12 +114,18 @@ impl SpacetimeClient {
|
||||
};
|
||||
|
||||
self.call_after_connect(move |connection, sender| {
|
||||
let fallback_owner_user_id = Some(procedure_input.owner_user_id.clone());
|
||||
connection
|
||||
.procedures()
|
||||
.delete_big_fish_work_then(procedure_input, move |_, result| {
|
||||
let mapped = result
|
||||
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||
.and_then(map_big_fish_works_procedure_result);
|
||||
.and_then(|result| {
|
||||
map_big_fish_works_procedure_result(
|
||||
result,
|
||||
fallback_owner_user_id.as_deref(),
|
||||
)
|
||||
});
|
||||
send_once(&sender, mapped);
|
||||
});
|
||||
})
|
||||
@@ -254,12 +271,12 @@ impl SpacetimeClient {
|
||||
pub async fn record_big_fish_play(
|
||||
&self,
|
||||
input: BigFishPlayReportRecordInput,
|
||||
) -> Result<BigFishSessionRecord, SpacetimeClientError> {
|
||||
let procedure_input = BigFishPlayReportInput {
|
||||
) -> Result<Vec<BigFishWorkSummaryRecord>, SpacetimeClientError> {
|
||||
let procedure_input = BigFishPlayRecordInput {
|
||||
session_id: input.session_id,
|
||||
user_id: input.user_id,
|
||||
elapsed_ms: input.elapsed_ms,
|
||||
reported_at_micros: input.reported_at_micros,
|
||||
played_at_micros: input.reported_at_micros,
|
||||
};
|
||||
|
||||
self.call_after_connect(move |connection, sender| {
|
||||
@@ -268,7 +285,7 @@ impl SpacetimeClient {
|
||||
.record_big_fish_play_then(procedure_input, move |_, result| {
|
||||
let mapped = result
|
||||
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||
.and_then(map_big_fish_session_procedure_result);
|
||||
.and_then(|result| map_big_fish_works_procedure_result(result, None));
|
||||
send_once(&sender, mapped);
|
||||
});
|
||||
})
|
||||
|
||||
@@ -121,6 +121,8 @@ use module_runtime::{
|
||||
RuntimeBrowseHistoryRecord, RuntimePlatformTheme as DomainRuntimePlatformTheme,
|
||||
RuntimeProfileDashboardRecord, RuntimeProfilePlayStatsRecord,
|
||||
RuntimeProfileRechargeCenterRecord, RuntimeProfileRechargeOrderRecord,
|
||||
RuntimeProfileRedeemCodeMode as DomainRuntimeProfileRedeemCodeMode,
|
||||
RuntimeProfileRedeemCodeRecord, RuntimeProfileRewardCodeRedeemRecord,
|
||||
RuntimeProfileSaveArchiveRecord, RuntimeProfileWalletLedgerEntryRecord,
|
||||
RuntimeReferralInviteCenterRecord, RuntimeReferralRedeemRecord, RuntimeSettingsRecord,
|
||||
RuntimeSnapshotRecord, build_runtime_browse_history_clear_input,
|
||||
@@ -130,8 +132,12 @@ use module_runtime::{
|
||||
build_runtime_profile_play_stats_record, build_runtime_profile_recharge_center_get_input,
|
||||
build_runtime_profile_recharge_center_record,
|
||||
build_runtime_profile_recharge_order_create_input,
|
||||
build_runtime_profile_save_archive_list_input, build_runtime_profile_save_archive_record,
|
||||
build_runtime_profile_save_archive_resume_input, build_runtime_profile_wallet_adjustment_input,
|
||||
build_runtime_profile_redeem_code_admin_disable_input,
|
||||
build_runtime_profile_redeem_code_admin_upsert_input, build_runtime_profile_redeem_code_record,
|
||||
build_runtime_profile_reward_code_redeem_input,
|
||||
build_runtime_profile_reward_code_redeem_record, build_runtime_profile_save_archive_list_input,
|
||||
build_runtime_profile_save_archive_record, build_runtime_profile_save_archive_resume_input,
|
||||
build_runtime_profile_wallet_adjustment_input,
|
||||
build_runtime_profile_wallet_ledger_entry_record,
|
||||
build_runtime_profile_wallet_ledger_list_input, build_runtime_referral_invite_center_get_input,
|
||||
build_runtime_referral_invite_center_record, build_runtime_referral_redeem_input,
|
||||
|
||||
@@ -161,6 +161,48 @@ impl From<module_runtime::RuntimeProfileRechargeOrderCreateInput>
|
||||
}
|
||||
}
|
||||
|
||||
impl From<module_runtime::RuntimeProfileRewardCodeRedeemInput>
|
||||
for RuntimeProfileRewardCodeRedeemInput
|
||||
{
|
||||
fn from(input: module_runtime::RuntimeProfileRewardCodeRedeemInput) -> Self {
|
||||
Self {
|
||||
user_id: input.user_id,
|
||||
code: input.code,
|
||||
redeemed_at_micros: input.redeemed_at_micros,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<module_runtime::RuntimeProfileRedeemCodeAdminUpsertInput>
|
||||
for RuntimeProfileRedeemCodeAdminUpsertInput
|
||||
{
|
||||
fn from(input: module_runtime::RuntimeProfileRedeemCodeAdminUpsertInput) -> Self {
|
||||
Self {
|
||||
admin_user_id: input.admin_user_id,
|
||||
code: input.code,
|
||||
mode: map_runtime_profile_redeem_code_mode(input.mode),
|
||||
reward_points: input.reward_points,
|
||||
max_uses: input.max_uses,
|
||||
enabled: input.enabled,
|
||||
allowed_user_ids: input.allowed_user_ids,
|
||||
allowed_public_user_codes: input.allowed_public_user_codes,
|
||||
updated_at_micros: input.updated_at_micros,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<module_runtime::RuntimeProfileRedeemCodeAdminDisableInput>
|
||||
for RuntimeProfileRedeemCodeAdminDisableInput
|
||||
{
|
||||
fn from(input: module_runtime::RuntimeProfileRedeemCodeAdminDisableInput) -> Self {
|
||||
Self {
|
||||
admin_user_id: input.admin_user_id,
|
||||
code: input.code,
|
||||
updated_at_micros: input.updated_at_micros,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<module_runtime::RuntimeReferralInviteCenterGetInput>
|
||||
for RuntimeReferralInviteCenterGetInput
|
||||
{
|
||||
@@ -802,6 +844,48 @@ pub(crate) fn map_runtime_referral_redeem_procedure_result(
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn map_runtime_profile_reward_code_redeem_procedure_result(
|
||||
result: RuntimeProfileRewardCodeRedeemProcedureResult,
|
||||
) -> Result<RuntimeProfileRewardCodeRedeemRecord, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::Procedure(
|
||||
result
|
||||
.error_message
|
||||
.unwrap_or_else(|| "SpacetimeDB procedure 返回未知错误".to_string()),
|
||||
));
|
||||
}
|
||||
|
||||
let snapshot = result.record.ok_or_else(|| {
|
||||
SpacetimeClientError::Procedure(
|
||||
"SpacetimeDB procedure 未返回 reward redeem 快照".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(build_runtime_profile_reward_code_redeem_record(
|
||||
map_runtime_profile_reward_code_redeem_snapshot(snapshot),
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn map_runtime_profile_redeem_code_admin_procedure_result(
|
||||
result: RuntimeProfileRedeemCodeAdminProcedureResult,
|
||||
) -> Result<RuntimeProfileRedeemCodeRecord, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::Procedure(
|
||||
result
|
||||
.error_message
|
||||
.unwrap_or_else(|| "SpacetimeDB procedure 返回未知错误".to_string()),
|
||||
));
|
||||
}
|
||||
|
||||
let snapshot = result.record.ok_or_else(|| {
|
||||
SpacetimeClientError::Procedure("SpacetimeDB procedure 未返回 redeem code 快照".to_string())
|
||||
})?;
|
||||
|
||||
Ok(build_runtime_profile_redeem_code_record(
|
||||
map_runtime_profile_redeem_code_snapshot(snapshot),
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn map_runtime_profile_play_stats_procedure_result(
|
||||
result: RuntimeProfilePlayStatsProcedureResult,
|
||||
) -> Result<RuntimeProfilePlayStatsRecord, SpacetimeClientError> {
|
||||
@@ -1278,6 +1362,7 @@ pub(crate) fn map_big_fish_session_procedure_result(
|
||||
|
||||
pub(crate) fn map_big_fish_works_procedure_result(
|
||||
result: BigFishWorksProcedureResult,
|
||||
fallback_owner_user_id: Option<&str>,
|
||||
) -> Result<Vec<BigFishWorkSummaryRecord>, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::Procedure(
|
||||
@@ -1292,9 +1377,15 @@ pub(crate) fn map_big_fish_works_procedure_result(
|
||||
"SpacetimeDB procedure 未返回 big fish works 快照".to_string(),
|
||||
)
|
||||
})?;
|
||||
serde_json::from_str::<Vec<BigFishWorkSummaryRecord>>(&items_json).map_err(|error| {
|
||||
SpacetimeClientError::Runtime(format!("big fish works items_json 非法: {error}"))
|
||||
})
|
||||
let items = serde_json::from_str::<Vec<CompatibleBigFishWorkSummaryRecord>>(&items_json)
|
||||
.map_err(|error| {
|
||||
SpacetimeClientError::Runtime(format!("big fish works items_json 非法: {error}"))
|
||||
})?;
|
||||
|
||||
Ok(items
|
||||
.into_iter()
|
||||
.map(|item| item.into_record(fallback_owner_user_id))
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub(crate) fn map_story_session_procedure_result(
|
||||
@@ -1666,6 +1757,33 @@ pub(crate) fn map_runtime_referral_redeem_snapshot(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_runtime_profile_reward_code_redeem_snapshot(
|
||||
snapshot: RuntimeProfileRewardCodeRedeemSnapshot,
|
||||
) -> module_runtime::RuntimeProfileRewardCodeRedeemSnapshot {
|
||||
module_runtime::RuntimeProfileRewardCodeRedeemSnapshot {
|
||||
wallet_balance: snapshot.wallet_balance,
|
||||
amount_granted: snapshot.amount_granted,
|
||||
ledger_entry: map_runtime_profile_wallet_ledger_entry_snapshot(snapshot.ledger_entry),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_runtime_profile_redeem_code_snapshot(
|
||||
snapshot: RuntimeProfileRedeemCodeSnapshot,
|
||||
) -> module_runtime::RuntimeProfileRedeemCodeSnapshot {
|
||||
module_runtime::RuntimeProfileRedeemCodeSnapshot {
|
||||
code: snapshot.code,
|
||||
mode: map_runtime_profile_redeem_code_mode_back(snapshot.mode),
|
||||
reward_points: snapshot.reward_points,
|
||||
max_uses: snapshot.max_uses,
|
||||
global_used_count: snapshot.global_used_count,
|
||||
enabled: snapshot.enabled,
|
||||
allowed_user_ids: snapshot.allowed_user_ids,
|
||||
created_by: snapshot.created_by,
|
||||
created_at_micros: snapshot.created_at_micros,
|
||||
updated_at_micros: snapshot.updated_at_micros,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_runtime_profile_played_world_snapshot(
|
||||
snapshot: RuntimeProfilePlayedWorldSnapshot,
|
||||
) -> module_runtime::RuntimeProfilePlayedWorldSnapshot {
|
||||
@@ -3277,6 +3395,41 @@ pub(crate) fn map_runtime_profile_wallet_ledger_source_type_back(
|
||||
crate::module_bindings::RuntimeProfileWalletLedgerSourceType::AssetGenerationRefund => {
|
||||
module_runtime::RuntimeProfileWalletLedgerSourceType::AssetGenerationRefund
|
||||
}
|
||||
crate::module_bindings::RuntimeProfileWalletLedgerSourceType::RedeemCodeReward => {
|
||||
module_runtime::RuntimeProfileWalletLedgerSourceType::RedeemCodeReward
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_runtime_profile_redeem_code_mode(
|
||||
value: module_runtime::RuntimeProfileRedeemCodeMode,
|
||||
) -> crate::module_bindings::RuntimeProfileRedeemCodeMode {
|
||||
match value {
|
||||
module_runtime::RuntimeProfileRedeemCodeMode::Public => {
|
||||
crate::module_bindings::RuntimeProfileRedeemCodeMode::Public
|
||||
}
|
||||
module_runtime::RuntimeProfileRedeemCodeMode::Unique => {
|
||||
crate::module_bindings::RuntimeProfileRedeemCodeMode::Unique
|
||||
}
|
||||
module_runtime::RuntimeProfileRedeemCodeMode::Private => {
|
||||
crate::module_bindings::RuntimeProfileRedeemCodeMode::Private
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_runtime_profile_redeem_code_mode_back(
|
||||
value: crate::module_bindings::RuntimeProfileRedeemCodeMode,
|
||||
) -> module_runtime::RuntimeProfileRedeemCodeMode {
|
||||
match value {
|
||||
crate::module_bindings::RuntimeProfileRedeemCodeMode::Public => {
|
||||
module_runtime::RuntimeProfileRedeemCodeMode::Public
|
||||
}
|
||||
crate::module_bindings::RuntimeProfileRedeemCodeMode::Unique => {
|
||||
module_runtime::RuntimeProfileRedeemCodeMode::Unique
|
||||
}
|
||||
crate::module_bindings::RuntimeProfileRedeemCodeMode::Private => {
|
||||
module_runtime::RuntimeProfileRedeemCodeMode::Private
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4607,6 +4760,124 @@ pub struct BigFishWorkSummaryRecord {
|
||||
pub level_main_image_ready_count: u32,
|
||||
pub level_motion_ready_count: u32,
|
||||
pub background_ready: bool,
|
||||
pub play_count: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
|
||||
struct CompatibleBigFishWorkSummaryRecord {
|
||||
work_id: String,
|
||||
source_session_id: String,
|
||||
#[serde(default)]
|
||||
owner_user_id: Option<String>,
|
||||
title: String,
|
||||
subtitle: String,
|
||||
summary: String,
|
||||
cover_image_src: Option<String>,
|
||||
status: String,
|
||||
updated_at_micros: i64,
|
||||
publish_ready: bool,
|
||||
level_count: u32,
|
||||
level_main_image_ready_count: u32,
|
||||
level_motion_ready_count: u32,
|
||||
background_ready: bool,
|
||||
#[serde(default)]
|
||||
play_count: u32,
|
||||
}
|
||||
|
||||
impl CompatibleBigFishWorkSummaryRecord {
|
||||
fn into_record(self, fallback_owner_user_id: Option<&str>) -> BigFishWorkSummaryRecord {
|
||||
BigFishWorkSummaryRecord {
|
||||
work_id: self.work_id,
|
||||
source_session_id: self.source_session_id,
|
||||
// 中文注释:兼容旧 works JSON 没有 owner_user_id 的历史数据,避免一次字段升级把整个作品列表打崩。
|
||||
owner_user_id: self.owner_user_id.unwrap_or_else(|| {
|
||||
fallback_owner_user_id
|
||||
.map(str::to_string)
|
||||
.unwrap_or_default()
|
||||
}),
|
||||
title: self.title,
|
||||
subtitle: self.subtitle,
|
||||
summary: self.summary,
|
||||
cover_image_src: self.cover_image_src,
|
||||
status: self.status,
|
||||
updated_at_micros: self.updated_at_micros,
|
||||
publish_ready: self.publish_ready,
|
||||
level_count: self.level_count,
|
||||
level_main_image_ready_count: self.level_main_image_ready_count,
|
||||
level_motion_ready_count: self.level_motion_ready_count,
|
||||
background_ready: self.background_ready,
|
||||
play_count: self.play_count,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn big_fish_works_mapper_backfills_missing_owner_user_id_for_private_lists() {
|
||||
let result = BigFishWorksProcedureResult {
|
||||
ok: true,
|
||||
items_json: Some(
|
||||
r#"[{
|
||||
"work_id":"big-fish-work-session-1",
|
||||
"source_session_id":"session-1",
|
||||
"title":"深海草稿",
|
||||
"subtitle":"副标题",
|
||||
"summary":"摘要",
|
||||
"cover_image_src":null,
|
||||
"status":"draft",
|
||||
"updated_at_micros":123,
|
||||
"publish_ready":false,
|
||||
"level_count":8,
|
||||
"level_main_image_ready_count":0,
|
||||
"level_motion_ready_count":0,
|
||||
"background_ready":false
|
||||
}]"#
|
||||
.to_string(),
|
||||
),
|
||||
error_message: None,
|
||||
};
|
||||
|
||||
let items = map_big_fish_works_procedure_result(result, Some("user-1"))
|
||||
.expect("旧 works JSON 应能被兼容解析");
|
||||
|
||||
assert_eq!(items.len(), 1);
|
||||
assert_eq!(items[0].owner_user_id, "user-1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn big_fish_works_mapper_keeps_empty_owner_when_gallery_legacy_json_lacks_field() {
|
||||
let result = BigFishWorksProcedureResult {
|
||||
ok: true,
|
||||
items_json: Some(
|
||||
r#"[{
|
||||
"work_id":"big-fish-work-session-2",
|
||||
"source_session_id":"session-2",
|
||||
"title":"公开作品",
|
||||
"subtitle":"副标题",
|
||||
"summary":"摘要",
|
||||
"cover_image_src":null,
|
||||
"status":"published",
|
||||
"updated_at_micros":456,
|
||||
"publish_ready":true,
|
||||
"level_count":8,
|
||||
"level_main_image_ready_count":8,
|
||||
"level_motion_ready_count":16,
|
||||
"background_ready":true
|
||||
}]"#
|
||||
.to_string(),
|
||||
),
|
||||
error_message: None,
|
||||
};
|
||||
|
||||
let items = map_big_fish_works_procedure_result(result, None)
|
||||
.expect("公开 works 旧 JSON 也不应因缺字段报错");
|
||||
|
||||
assert_eq!(items.len(), 1);
|
||||
assert!(items[0].owner_user_id.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
use super::runtime_profile_redeem_code_admin_disable_input_type::RuntimeProfileRedeemCodeAdminDisableInput;
|
||||
use super::runtime_profile_redeem_code_admin_procedure_result_type::RuntimeProfileRedeemCodeAdminProcedureResult;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
struct AdminDisableProfileRedeemCodeArgs {
|
||||
pub input: RuntimeProfileRedeemCodeAdminDisableInput,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for AdminDisableProfileRedeemCodeArgs {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
/// Extension trait for access to the procedure `admin_disable_profile_redeem_code`.
|
||||
///
|
||||
/// Implemented for [`super::RemoteProcedures`].
|
||||
pub trait admin_disable_profile_redeem_code {
|
||||
fn admin_disable_profile_redeem_code(&self, input: RuntimeProfileRedeemCodeAdminDisableInput) {
|
||||
self.admin_disable_profile_redeem_code_then(input, |_, _| {});
|
||||
}
|
||||
|
||||
fn admin_disable_profile_redeem_code_then(
|
||||
&self,
|
||||
input: RuntimeProfileRedeemCodeAdminDisableInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<RuntimeProfileRedeemCodeAdminProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
);
|
||||
}
|
||||
|
||||
impl admin_disable_profile_redeem_code for super::RemoteProcedures {
|
||||
fn admin_disable_profile_redeem_code_then(
|
||||
&self,
|
||||
input: RuntimeProfileRedeemCodeAdminDisableInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<RuntimeProfileRedeemCodeAdminProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
) {
|
||||
self.imp
|
||||
.invoke_procedure_with_callback::<_, RuntimeProfileRedeemCodeAdminProcedureResult>(
|
||||
"admin_disable_profile_redeem_code",
|
||||
AdminDisableProfileRedeemCodeArgs { input },
|
||||
__callback,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
use super::runtime_profile_redeem_code_admin_procedure_result_type::RuntimeProfileRedeemCodeAdminProcedureResult;
|
||||
use super::runtime_profile_redeem_code_admin_upsert_input_type::RuntimeProfileRedeemCodeAdminUpsertInput;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
struct AdminUpsertProfileRedeemCodeArgs {
|
||||
pub input: RuntimeProfileRedeemCodeAdminUpsertInput,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for AdminUpsertProfileRedeemCodeArgs {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
/// Extension trait for access to the procedure `admin_upsert_profile_redeem_code`.
|
||||
///
|
||||
/// Implemented for [`super::RemoteProcedures`].
|
||||
pub trait admin_upsert_profile_redeem_code {
|
||||
fn admin_upsert_profile_redeem_code(&self, input: RuntimeProfileRedeemCodeAdminUpsertInput) {
|
||||
self.admin_upsert_profile_redeem_code_then(input, |_, _| {});
|
||||
}
|
||||
|
||||
fn admin_upsert_profile_redeem_code_then(
|
||||
&self,
|
||||
input: RuntimeProfileRedeemCodeAdminUpsertInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<RuntimeProfileRedeemCodeAdminProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
);
|
||||
}
|
||||
|
||||
impl admin_upsert_profile_redeem_code for super::RemoteProcedures {
|
||||
fn admin_upsert_profile_redeem_code_then(
|
||||
&self,
|
||||
input: RuntimeProfileRedeemCodeAdminUpsertInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<RuntimeProfileRedeemCodeAdminProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
) {
|
||||
self.imp
|
||||
.invoke_procedure_with_callback::<_, RuntimeProfileRedeemCodeAdminProcedureResult>(
|
||||
"admin_upsert_profile_redeem_code",
|
||||
AdminUpsertProfileRedeemCodeArgs { input },
|
||||
__callback,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ pub struct BigFishCreationSession {
|
||||
pub asset_coverage_json: String,
|
||||
pub last_assistant_reply: Option<String>,
|
||||
pub publish_ready: bool,
|
||||
pub play_count: u32,
|
||||
pub created_at: __sdk::Timestamp,
|
||||
pub updated_at: __sdk::Timestamp,
|
||||
}
|
||||
@@ -43,6 +44,7 @@ pub struct BigFishCreationSessionCols {
|
||||
pub asset_coverage_json: __sdk::__query_builder::Col<BigFishCreationSession, String>,
|
||||
pub last_assistant_reply: __sdk::__query_builder::Col<BigFishCreationSession, Option<String>>,
|
||||
pub publish_ready: __sdk::__query_builder::Col<BigFishCreationSession, bool>,
|
||||
pub play_count: __sdk::__query_builder::Col<BigFishCreationSession, u32>,
|
||||
pub created_at: __sdk::__query_builder::Col<BigFishCreationSession, __sdk::Timestamp>,
|
||||
pub updated_at: __sdk::__query_builder::Col<BigFishCreationSession, __sdk::Timestamp>,
|
||||
}
|
||||
@@ -68,6 +70,7 @@ impl __sdk::__query_builder::HasCols for BigFishCreationSession {
|
||||
"last_assistant_reply",
|
||||
),
|
||||
publish_ready: __sdk::__query_builder::Col::new(table_name, "publish_ready"),
|
||||
play_count: __sdk::__query_builder::Col::new(table_name, "play_count"),
|
||||
created_at: __sdk::__query_builder::Col::new(table_name, "created_at"),
|
||||
updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"),
|
||||
}
|
||||
|
||||
@@ -6,13 +6,13 @@ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
pub struct BigFishPlayReportInput {
|
||||
pub struct BigFishPlayRecordInput {
|
||||
pub session_id: String,
|
||||
pub user_id: String,
|
||||
pub elapsed_ms: u64,
|
||||
pub reported_at_micros: i64,
|
||||
pub played_at_micros: i64,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for BigFishPlayReportInput {
|
||||
impl __sdk::InModule for BigFishPlayRecordInput {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
@@ -8,6 +8,8 @@ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
pub mod accept_quest_reducer;
|
||||
pub mod acknowledge_quest_completion_reducer;
|
||||
pub mod admin_disable_profile_redeem_code_procedure;
|
||||
pub mod admin_upsert_profile_redeem_code_procedure;
|
||||
pub mod advance_puzzle_next_level_procedure;
|
||||
pub mod ai_result_reference_input_type;
|
||||
pub mod ai_result_reference_kind_type;
|
||||
@@ -89,7 +91,7 @@ pub mod big_fish_game_draft_type;
|
||||
pub mod big_fish_level_blueprint_type;
|
||||
pub mod big_fish_message_finalize_input_type;
|
||||
pub mod big_fish_message_submit_input_type;
|
||||
pub mod big_fish_play_report_input_type;
|
||||
pub mod big_fish_play_record_input_type;
|
||||
pub mod big_fish_publish_input_type;
|
||||
pub mod big_fish_runtime_params_type;
|
||||
pub mod big_fish_session_create_input_type;
|
||||
@@ -278,6 +280,8 @@ pub mod profile_invite_code_type;
|
||||
pub mod profile_membership_type;
|
||||
pub mod profile_played_world_type;
|
||||
pub mod profile_recharge_order_type;
|
||||
pub mod profile_redeem_code_type;
|
||||
pub mod profile_redeem_code_usage_type;
|
||||
pub mod profile_referral_relation_type;
|
||||
pub mod profile_save_archive_type;
|
||||
pub mod profile_wallet_ledger_type;
|
||||
@@ -346,6 +350,7 @@ pub mod quest_treasure_inspected_signal_type;
|
||||
pub mod quest_turn_in_input_type;
|
||||
pub mod record_big_fish_play_procedure;
|
||||
pub mod redeem_profile_referral_invite_code_procedure;
|
||||
pub mod redeem_profile_reward_code_procedure;
|
||||
pub mod refresh_session_type;
|
||||
pub mod refund_profile_wallet_points_and_return_procedure;
|
||||
pub mod resolve_combat_action_and_return_procedure;
|
||||
@@ -405,6 +410,14 @@ pub mod runtime_profile_recharge_order_snapshot_type;
|
||||
pub mod runtime_profile_recharge_order_status_type;
|
||||
pub mod runtime_profile_recharge_product_kind_type;
|
||||
pub mod runtime_profile_recharge_product_snapshot_type;
|
||||
pub mod runtime_profile_redeem_code_admin_disable_input_type;
|
||||
pub mod runtime_profile_redeem_code_admin_procedure_result_type;
|
||||
pub mod runtime_profile_redeem_code_admin_upsert_input_type;
|
||||
pub mod runtime_profile_redeem_code_mode_type;
|
||||
pub mod runtime_profile_redeem_code_snapshot_type;
|
||||
pub mod runtime_profile_reward_code_redeem_input_type;
|
||||
pub mod runtime_profile_reward_code_redeem_procedure_result_type;
|
||||
pub mod runtime_profile_reward_code_redeem_snapshot_type;
|
||||
pub mod runtime_profile_save_archive_list_input_type;
|
||||
pub mod runtime_profile_save_archive_procedure_result_type;
|
||||
pub mod runtime_profile_save_archive_resume_input_type;
|
||||
@@ -479,6 +492,8 @@ pub mod user_browse_history_type;
|
||||
|
||||
pub use accept_quest_reducer::accept_quest;
|
||||
pub use acknowledge_quest_completion_reducer::acknowledge_quest_completion;
|
||||
pub use admin_disable_profile_redeem_code_procedure::admin_disable_profile_redeem_code;
|
||||
pub use admin_upsert_profile_redeem_code_procedure::admin_upsert_profile_redeem_code;
|
||||
pub use advance_puzzle_next_level_procedure::advance_puzzle_next_level;
|
||||
pub use ai_result_reference_input_type::AiResultReferenceInput;
|
||||
pub use ai_result_reference_kind_type::AiResultReferenceKind;
|
||||
@@ -560,7 +575,7 @@ pub use big_fish_game_draft_type::BigFishGameDraft;
|
||||
pub use big_fish_level_blueprint_type::BigFishLevelBlueprint;
|
||||
pub use big_fish_message_finalize_input_type::BigFishMessageFinalizeInput;
|
||||
pub use big_fish_message_submit_input_type::BigFishMessageSubmitInput;
|
||||
pub use big_fish_play_report_input_type::BigFishPlayReportInput;
|
||||
pub use big_fish_play_record_input_type::BigFishPlayRecordInput;
|
||||
pub use big_fish_publish_input_type::BigFishPublishInput;
|
||||
pub use big_fish_runtime_params_type::BigFishRuntimeParams;
|
||||
pub use big_fish_session_create_input_type::BigFishSessionCreateInput;
|
||||
@@ -749,6 +764,8 @@ pub use profile_invite_code_type::ProfileInviteCode;
|
||||
pub use profile_membership_type::ProfileMembership;
|
||||
pub use profile_played_world_type::ProfilePlayedWorld;
|
||||
pub use profile_recharge_order_type::ProfileRechargeOrder;
|
||||
pub use profile_redeem_code_type::ProfileRedeemCode;
|
||||
pub use profile_redeem_code_usage_type::ProfileRedeemCodeUsage;
|
||||
pub use profile_referral_relation_type::ProfileReferralRelation;
|
||||
pub use profile_save_archive_type::ProfileSaveArchive;
|
||||
pub use profile_wallet_ledger_type::ProfileWalletLedger;
|
||||
@@ -817,6 +834,7 @@ pub use quest_treasure_inspected_signal_type::QuestTreasureInspectedSignal;
|
||||
pub use quest_turn_in_input_type::QuestTurnInInput;
|
||||
pub use record_big_fish_play_procedure::record_big_fish_play;
|
||||
pub use redeem_profile_referral_invite_code_procedure::redeem_profile_referral_invite_code;
|
||||
pub use redeem_profile_reward_code_procedure::redeem_profile_reward_code;
|
||||
pub use refresh_session_type::RefreshSession;
|
||||
pub use refund_profile_wallet_points_and_return_procedure::refund_profile_wallet_points_and_return;
|
||||
pub use resolve_combat_action_and_return_procedure::resolve_combat_action_and_return;
|
||||
@@ -876,6 +894,14 @@ pub use runtime_profile_recharge_order_snapshot_type::RuntimeProfileRechargeOrde
|
||||
pub use runtime_profile_recharge_order_status_type::RuntimeProfileRechargeOrderStatus;
|
||||
pub use runtime_profile_recharge_product_kind_type::RuntimeProfileRechargeProductKind;
|
||||
pub use runtime_profile_recharge_product_snapshot_type::RuntimeProfileRechargeProductSnapshot;
|
||||
pub use runtime_profile_redeem_code_admin_disable_input_type::RuntimeProfileRedeemCodeAdminDisableInput;
|
||||
pub use runtime_profile_redeem_code_admin_procedure_result_type::RuntimeProfileRedeemCodeAdminProcedureResult;
|
||||
pub use runtime_profile_redeem_code_admin_upsert_input_type::RuntimeProfileRedeemCodeAdminUpsertInput;
|
||||
pub use runtime_profile_redeem_code_mode_type::RuntimeProfileRedeemCodeMode;
|
||||
pub use runtime_profile_redeem_code_snapshot_type::RuntimeProfileRedeemCodeSnapshot;
|
||||
pub use runtime_profile_reward_code_redeem_input_type::RuntimeProfileRewardCodeRedeemInput;
|
||||
pub use runtime_profile_reward_code_redeem_procedure_result_type::RuntimeProfileRewardCodeRedeemProcedureResult;
|
||||
pub use runtime_profile_reward_code_redeem_snapshot_type::RuntimeProfileRewardCodeRedeemSnapshot;
|
||||
pub use runtime_profile_save_archive_list_input_type::RuntimeProfileSaveArchiveListInput;
|
||||
pub use runtime_profile_save_archive_procedure_result_type::RuntimeProfileSaveArchiveProcedureResult;
|
||||
pub use runtime_profile_save_archive_resume_input_type::RuntimeProfileSaveArchiveResumeInput;
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
use super::runtime_profile_redeem_code_mode_type::RuntimeProfileRedeemCodeMode;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
pub struct ProfileRedeemCode {
|
||||
pub code: String,
|
||||
pub mode: RuntimeProfileRedeemCodeMode,
|
||||
pub reward_points: u64,
|
||||
pub max_uses: u32,
|
||||
pub global_used_count: u32,
|
||||
pub enabled: bool,
|
||||
pub allowed_user_ids: Vec<String>,
|
||||
pub created_by: String,
|
||||
pub created_at: __sdk::Timestamp,
|
||||
pub updated_at: __sdk::Timestamp,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for ProfileRedeemCode {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
|
||||
/// Column accessor struct for the table `ProfileRedeemCode`.
|
||||
///
|
||||
/// Provides typed access to columns for query building.
|
||||
pub struct ProfileRedeemCodeCols {
|
||||
pub code: __sdk::__query_builder::Col<ProfileRedeemCode, String>,
|
||||
pub mode: __sdk::__query_builder::Col<ProfileRedeemCode, RuntimeProfileRedeemCodeMode>,
|
||||
pub reward_points: __sdk::__query_builder::Col<ProfileRedeemCode, u64>,
|
||||
pub max_uses: __sdk::__query_builder::Col<ProfileRedeemCode, u32>,
|
||||
pub global_used_count: __sdk::__query_builder::Col<ProfileRedeemCode, u32>,
|
||||
pub enabled: __sdk::__query_builder::Col<ProfileRedeemCode, bool>,
|
||||
pub allowed_user_ids: __sdk::__query_builder::Col<ProfileRedeemCode, Vec<String>>,
|
||||
pub created_by: __sdk::__query_builder::Col<ProfileRedeemCode, String>,
|
||||
pub created_at: __sdk::__query_builder::Col<ProfileRedeemCode, __sdk::Timestamp>,
|
||||
pub updated_at: __sdk::__query_builder::Col<ProfileRedeemCode, __sdk::Timestamp>,
|
||||
}
|
||||
|
||||
impl __sdk::__query_builder::HasCols for ProfileRedeemCode {
|
||||
type Cols = ProfileRedeemCodeCols;
|
||||
fn cols(table_name: &'static str) -> Self::Cols {
|
||||
ProfileRedeemCodeCols {
|
||||
code: __sdk::__query_builder::Col::new(table_name, "code"),
|
||||
mode: __sdk::__query_builder::Col::new(table_name, "mode"),
|
||||
reward_points: __sdk::__query_builder::Col::new(table_name, "reward_points"),
|
||||
max_uses: __sdk::__query_builder::Col::new(table_name, "max_uses"),
|
||||
global_used_count: __sdk::__query_builder::Col::new(table_name, "global_used_count"),
|
||||
enabled: __sdk::__query_builder::Col::new(table_name, "enabled"),
|
||||
allowed_user_ids: __sdk::__query_builder::Col::new(table_name, "allowed_user_ids"),
|
||||
created_by: __sdk::__query_builder::Col::new(table_name, "created_by"),
|
||||
created_at: __sdk::__query_builder::Col::new(table_name, "created_at"),
|
||||
updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Indexed column accessor struct for the table `ProfileRedeemCode`.
|
||||
///
|
||||
/// Provides typed access to indexed columns for query building.
|
||||
pub struct ProfileRedeemCodeIxCols {
|
||||
pub code: __sdk::__query_builder::IxCol<ProfileRedeemCode, String>,
|
||||
}
|
||||
|
||||
impl __sdk::__query_builder::HasIxCols for ProfileRedeemCode {
|
||||
type IxCols = ProfileRedeemCodeIxCols;
|
||||
fn ix_cols(table_name: &'static str) -> Self::IxCols {
|
||||
ProfileRedeemCodeIxCols {
|
||||
code: __sdk::__query_builder::IxCol::new(table_name, "code"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl __sdk::__query_builder::CanBeLookupTable for ProfileRedeemCode {}
|
||||
@@ -0,0 +1,65 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
pub struct ProfileRedeemCodeUsage {
|
||||
pub usage_id: String,
|
||||
pub code: String,
|
||||
pub user_id: String,
|
||||
pub amount_granted: u64,
|
||||
pub created_at: __sdk::Timestamp,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for ProfileRedeemCodeUsage {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
|
||||
/// Column accessor struct for the table `ProfileRedeemCodeUsage`.
|
||||
///
|
||||
/// Provides typed access to columns for query building.
|
||||
pub struct ProfileRedeemCodeUsageCols {
|
||||
pub usage_id: __sdk::__query_builder::Col<ProfileRedeemCodeUsage, String>,
|
||||
pub code: __sdk::__query_builder::Col<ProfileRedeemCodeUsage, String>,
|
||||
pub user_id: __sdk::__query_builder::Col<ProfileRedeemCodeUsage, String>,
|
||||
pub amount_granted: __sdk::__query_builder::Col<ProfileRedeemCodeUsage, u64>,
|
||||
pub created_at: __sdk::__query_builder::Col<ProfileRedeemCodeUsage, __sdk::Timestamp>,
|
||||
}
|
||||
|
||||
impl __sdk::__query_builder::HasCols for ProfileRedeemCodeUsage {
|
||||
type Cols = ProfileRedeemCodeUsageCols;
|
||||
fn cols(table_name: &'static str) -> Self::Cols {
|
||||
ProfileRedeemCodeUsageCols {
|
||||
usage_id: __sdk::__query_builder::Col::new(table_name, "usage_id"),
|
||||
code: __sdk::__query_builder::Col::new(table_name, "code"),
|
||||
user_id: __sdk::__query_builder::Col::new(table_name, "user_id"),
|
||||
amount_granted: __sdk::__query_builder::Col::new(table_name, "amount_granted"),
|
||||
created_at: __sdk::__query_builder::Col::new(table_name, "created_at"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Indexed column accessor struct for the table `ProfileRedeemCodeUsage`.
|
||||
///
|
||||
/// Provides typed access to indexed columns for query building.
|
||||
pub struct ProfileRedeemCodeUsageIxCols {
|
||||
pub code: __sdk::__query_builder::IxCol<ProfileRedeemCodeUsage, String>,
|
||||
pub usage_id: __sdk::__query_builder::IxCol<ProfileRedeemCodeUsage, String>,
|
||||
pub user_id: __sdk::__query_builder::IxCol<ProfileRedeemCodeUsage, String>,
|
||||
}
|
||||
|
||||
impl __sdk::__query_builder::HasIxCols for ProfileRedeemCodeUsage {
|
||||
type IxCols = ProfileRedeemCodeUsageIxCols;
|
||||
fn ix_cols(table_name: &'static str) -> Self::IxCols {
|
||||
ProfileRedeemCodeUsageIxCols {
|
||||
code: __sdk::__query_builder::IxCol::new(table_name, "code"),
|
||||
usage_id: __sdk::__query_builder::IxCol::new(table_name, "usage_id"),
|
||||
user_id: __sdk::__query_builder::IxCol::new(table_name, "user_id"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl __sdk::__query_builder::CanBeLookupTable for ProfileRedeemCodeUsage {}
|
||||
@@ -4,13 +4,13 @@
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
use super::big_fish_play_report_input_type::BigFishPlayReportInput;
|
||||
use super::big_fish_session_procedure_result_type::BigFishSessionProcedureResult;
|
||||
use super::big_fish_play_record_input_type::BigFishPlayRecordInput;
|
||||
use super::big_fish_works_procedure_result_type::BigFishWorksProcedureResult;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
struct RecordBigFishPlayArgs {
|
||||
pub input: BigFishPlayReportInput,
|
||||
pub input: BigFishPlayRecordInput,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for RecordBigFishPlayArgs {
|
||||
@@ -22,17 +22,17 @@ impl __sdk::InModule for RecordBigFishPlayArgs {
|
||||
///
|
||||
/// Implemented for [`super::RemoteProcedures`].
|
||||
pub trait record_big_fish_play {
|
||||
fn record_big_fish_play(&self, input: BigFishPlayReportInput) {
|
||||
fn record_big_fish_play(&self, input: BigFishPlayRecordInput) {
|
||||
self.record_big_fish_play_then(input, |_, _| {});
|
||||
}
|
||||
|
||||
fn record_big_fish_play_then(
|
||||
&self,
|
||||
input: BigFishPlayReportInput,
|
||||
input: BigFishPlayRecordInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<BigFishSessionProcedureResult, __sdk::InternalError>,
|
||||
Result<BigFishWorksProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
);
|
||||
@@ -41,16 +41,16 @@ pub trait record_big_fish_play {
|
||||
impl record_big_fish_play for super::RemoteProcedures {
|
||||
fn record_big_fish_play_then(
|
||||
&self,
|
||||
input: BigFishPlayReportInput,
|
||||
input: BigFishPlayRecordInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<BigFishSessionProcedureResult, __sdk::InternalError>,
|
||||
Result<BigFishWorksProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
) {
|
||||
self.imp
|
||||
.invoke_procedure_with_callback::<_, BigFishSessionProcedureResult>(
|
||||
.invoke_procedure_with_callback::<_, BigFishWorksProcedureResult>(
|
||||
"record_big_fish_play",
|
||||
RecordBigFishPlayArgs { input },
|
||||
__callback,
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
use super::runtime_profile_reward_code_redeem_input_type::RuntimeProfileRewardCodeRedeemInput;
|
||||
use super::runtime_profile_reward_code_redeem_procedure_result_type::RuntimeProfileRewardCodeRedeemProcedureResult;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
struct RedeemProfileRewardCodeArgs {
|
||||
pub input: RuntimeProfileRewardCodeRedeemInput,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for RedeemProfileRewardCodeArgs {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
/// Extension trait for access to the procedure `redeem_profile_reward_code`.
|
||||
///
|
||||
/// Implemented for [`super::RemoteProcedures`].
|
||||
pub trait redeem_profile_reward_code {
|
||||
fn redeem_profile_reward_code(&self, input: RuntimeProfileRewardCodeRedeemInput) {
|
||||
self.redeem_profile_reward_code_then(input, |_, _| {});
|
||||
}
|
||||
|
||||
fn redeem_profile_reward_code_then(
|
||||
&self,
|
||||
input: RuntimeProfileRewardCodeRedeemInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<RuntimeProfileRewardCodeRedeemProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
);
|
||||
}
|
||||
|
||||
impl redeem_profile_reward_code for super::RemoteProcedures {
|
||||
fn redeem_profile_reward_code_then(
|
||||
&self,
|
||||
input: RuntimeProfileRewardCodeRedeemInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<RuntimeProfileRewardCodeRedeemProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
) {
|
||||
self.imp
|
||||
.invoke_procedure_with_callback::<_, RuntimeProfileRewardCodeRedeemProcedureResult>(
|
||||
"redeem_profile_reward_code",
|
||||
RedeemProfileRewardCodeArgs { input },
|
||||
__callback,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
pub struct RuntimeProfileRedeemCodeAdminDisableInput {
|
||||
pub admin_user_id: String,
|
||||
pub code: String,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for RuntimeProfileRedeemCodeAdminDisableInput {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
use super::runtime_profile_redeem_code_snapshot_type::RuntimeProfileRedeemCodeSnapshot;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
pub struct RuntimeProfileRedeemCodeAdminProcedureResult {
|
||||
pub ok: bool,
|
||||
pub record: Option<RuntimeProfileRedeemCodeSnapshot>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for RuntimeProfileRedeemCodeAdminProcedureResult {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
use super::runtime_profile_redeem_code_mode_type::RuntimeProfileRedeemCodeMode;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
pub struct RuntimeProfileRedeemCodeAdminUpsertInput {
|
||||
pub admin_user_id: String,
|
||||
pub code: String,
|
||||
pub mode: RuntimeProfileRedeemCodeMode,
|
||||
pub reward_points: u64,
|
||||
pub max_uses: u32,
|
||||
pub enabled: bool,
|
||||
pub allowed_user_ids: Vec<String>,
|
||||
pub allowed_public_user_codes: Vec<String>,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for RuntimeProfileRedeemCodeAdminUpsertInput {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
#[derive(Copy, Eq, Hash)]
|
||||
pub enum RuntimeProfileRedeemCodeMode {
|
||||
Public,
|
||||
|
||||
Unique,
|
||||
|
||||
Private,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for RuntimeProfileRedeemCodeMode {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
use super::runtime_profile_redeem_code_mode_type::RuntimeProfileRedeemCodeMode;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
pub struct RuntimeProfileRedeemCodeSnapshot {
|
||||
pub code: String,
|
||||
pub mode: RuntimeProfileRedeemCodeMode,
|
||||
pub reward_points: u64,
|
||||
pub max_uses: u32,
|
||||
pub global_used_count: u32,
|
||||
pub enabled: bool,
|
||||
pub allowed_user_ids: Vec<String>,
|
||||
pub created_by: String,
|
||||
pub created_at_micros: i64,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for RuntimeProfileRedeemCodeSnapshot {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
pub struct RuntimeProfileRewardCodeRedeemInput {
|
||||
pub user_id: String,
|
||||
pub code: String,
|
||||
pub redeemed_at_micros: i64,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for RuntimeProfileRewardCodeRedeemInput {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
use super::runtime_profile_reward_code_redeem_snapshot_type::RuntimeProfileRewardCodeRedeemSnapshot;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
pub struct RuntimeProfileRewardCodeRedeemProcedureResult {
|
||||
pub ok: bool,
|
||||
pub record: Option<RuntimeProfileRewardCodeRedeemSnapshot>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for RuntimeProfileRewardCodeRedeemProcedureResult {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
use super::runtime_profile_wallet_ledger_entry_snapshot_type::RuntimeProfileWalletLedgerEntrySnapshot;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
pub struct RuntimeProfileRewardCodeRedeemSnapshot {
|
||||
pub wallet_balance: u64,
|
||||
pub amount_granted: u64,
|
||||
pub ledger_entry: RuntimeProfileWalletLedgerEntrySnapshot,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for RuntimeProfileRewardCodeRedeemSnapshot {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
@@ -19,6 +19,8 @@ pub enum RuntimeProfileWalletLedgerSourceType {
|
||||
AssetGenerationConsume,
|
||||
|
||||
AssetGenerationRefund,
|
||||
|
||||
RedeemCodeReward,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for RuntimeProfileWalletLedgerSourceType {
|
||||
|
||||
@@ -478,15 +478,14 @@ impl SpacetimeClient {
|
||||
};
|
||||
|
||||
self.call_after_connect(move |connection, sender| {
|
||||
connection.procedures().submit_puzzle_leaderboard_entry_then(
|
||||
procedure_input,
|
||||
move |_, result| {
|
||||
connection
|
||||
.procedures()
|
||||
.submit_puzzle_leaderboard_entry_then(procedure_input, move |_, result| {
|
||||
let mapped = result
|
||||
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||
.and_then(map_puzzle_run_procedure_result);
|
||||
send_once(&sender, mapped);
|
||||
},
|
||||
);
|
||||
});
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -255,6 +255,97 @@ impl SpacetimeClient {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn redeem_profile_reward_code(
|
||||
&self,
|
||||
user_id: String,
|
||||
code: String,
|
||||
redeemed_at_micros: i64,
|
||||
) -> Result<RuntimeProfileRewardCodeRedeemRecord, SpacetimeClientError> {
|
||||
let procedure_input =
|
||||
build_runtime_profile_reward_code_redeem_input(user_id, code, redeemed_at_micros)
|
||||
.map_err(|error| SpacetimeClientError::Runtime(error.to_string()))?
|
||||
.into();
|
||||
|
||||
self.call_after_connect(move |connection, sender| {
|
||||
connection.procedures().redeem_profile_reward_code_then(
|
||||
procedure_input,
|
||||
move |_, result| {
|
||||
let mapped = result
|
||||
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||
.and_then(map_runtime_profile_reward_code_redeem_procedure_result);
|
||||
send_once(&sender, mapped);
|
||||
},
|
||||
);
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn admin_upsert_profile_redeem_code(
|
||||
&self,
|
||||
admin_user_id: String,
|
||||
code: String,
|
||||
mode: DomainRuntimeProfileRedeemCodeMode,
|
||||
reward_points: u64,
|
||||
max_uses: u32,
|
||||
enabled: bool,
|
||||
allowed_user_ids: Vec<String>,
|
||||
allowed_public_user_codes: Vec<String>,
|
||||
updated_at_micros: i64,
|
||||
) -> Result<RuntimeProfileRedeemCodeRecord, SpacetimeClientError> {
|
||||
let procedure_input = build_runtime_profile_redeem_code_admin_upsert_input(
|
||||
admin_user_id,
|
||||
code,
|
||||
mode,
|
||||
reward_points,
|
||||
max_uses,
|
||||
enabled,
|
||||
allowed_user_ids,
|
||||
allowed_public_user_codes,
|
||||
updated_at_micros,
|
||||
)
|
||||
.map_err(|error| SpacetimeClientError::Runtime(error.to_string()))?
|
||||
.into();
|
||||
|
||||
self.call_after_connect(move |connection, sender| {
|
||||
connection
|
||||
.procedures()
|
||||
.admin_upsert_profile_redeem_code_then(procedure_input, move |_, result| {
|
||||
let mapped = result
|
||||
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||
.and_then(map_runtime_profile_redeem_code_admin_procedure_result);
|
||||
send_once(&sender, mapped);
|
||||
});
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn admin_disable_profile_redeem_code(
|
||||
&self,
|
||||
admin_user_id: String,
|
||||
code: String,
|
||||
updated_at_micros: i64,
|
||||
) -> Result<RuntimeProfileRedeemCodeRecord, SpacetimeClientError> {
|
||||
let procedure_input = build_runtime_profile_redeem_code_admin_disable_input(
|
||||
admin_user_id,
|
||||
code,
|
||||
updated_at_micros,
|
||||
)
|
||||
.map_err(|error| SpacetimeClientError::Runtime(error.to_string()))?
|
||||
.into();
|
||||
|
||||
self.call_after_connect(move |connection, sender| {
|
||||
connection
|
||||
.procedures()
|
||||
.admin_disable_profile_redeem_code_then(procedure_input, move |_, result| {
|
||||
let mapped = result
|
||||
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||
.and_then(map_runtime_profile_redeem_code_admin_procedure_result);
|
||||
send_once(&sender, mapped);
|
||||
});
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_profile_play_stats(
|
||||
&self,
|
||||
user_id: String,
|
||||
|
||||
@@ -108,6 +108,7 @@ pub(crate) fn generate_big_fish_asset_tx(
|
||||
.map_err(|error| error.to_string())?,
|
||||
last_assistant_reply: Some(reply.clone()),
|
||||
publish_ready: coverage.publish_ready,
|
||||
play_count: session.play_count,
|
||||
created_at: session.created_at,
|
||||
updated_at,
|
||||
};
|
||||
@@ -164,6 +165,7 @@ pub(crate) fn publish_big_fish_game_tx(
|
||||
.map_err(|error| error.to_string())?,
|
||||
last_assistant_reply: Some("玩法已发布,可以进入测试运行态。".to_string()),
|
||||
publish_ready: true,
|
||||
play_count: session.play_count,
|
||||
created_at: session.created_at,
|
||||
updated_at: published_at,
|
||||
};
|
||||
|
||||
@@ -96,6 +96,32 @@ pub fn delete_big_fish_work(
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn record_big_fish_play(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: BigFishPlayRecordInput,
|
||||
) -> BigFishWorksProcedureResult {
|
||||
match ctx.try_with_tx(|tx| record_big_fish_play_tx(tx, input.clone())) {
|
||||
Ok(items) => match serde_json::to_string(&items) {
|
||||
Ok(items_json) => BigFishWorksProcedureResult {
|
||||
ok: true,
|
||||
items_json: Some(items_json),
|
||||
error_message: None,
|
||||
},
|
||||
Err(error) => BigFishWorksProcedureResult {
|
||||
ok: false,
|
||||
items_json: None,
|
||||
error_message: Some(error.to_string()),
|
||||
},
|
||||
},
|
||||
Err(message) => BigFishWorksProcedureResult {
|
||||
ok: false,
|
||||
items_json: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn submit_big_fish_message(
|
||||
ctx: &mut ProcedureContext,
|
||||
@@ -153,25 +179,6 @@ pub fn compile_big_fish_draft(
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn record_big_fish_play(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: BigFishPlayReportInput,
|
||||
) -> BigFishSessionProcedureResult {
|
||||
match ctx.try_with_tx(|tx| record_big_fish_play_tx(tx, input.clone())) {
|
||||
Ok(session) => BigFishSessionProcedureResult {
|
||||
ok: true,
|
||||
session: Some(session),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => BigFishSessionProcedureResult {
|
||||
ok: false,
|
||||
session: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn create_big_fish_session_tx(
|
||||
ctx: &ReducerContext,
|
||||
input: BigFishSessionCreateInput,
|
||||
@@ -216,6 +223,7 @@ pub(crate) fn create_big_fish_session_tx(
|
||||
.map_err(|error| error.to_string())?,
|
||||
last_assistant_reply: Some(input.welcome_message_text.clone()),
|
||||
publish_ready: false,
|
||||
play_count: 0,
|
||||
created_at,
|
||||
updated_at: created_at,
|
||||
});
|
||||
@@ -405,6 +413,7 @@ pub(crate) fn submit_big_fish_message_tx(
|
||||
asset_coverage_json: session.asset_coverage_json.clone(),
|
||||
last_assistant_reply: session.last_assistant_reply.clone(),
|
||||
publish_ready: session.publish_ready,
|
||||
play_count: session.play_count,
|
||||
created_at: session.created_at,
|
||||
updated_at: submitted_at,
|
||||
};
|
||||
@@ -451,6 +460,7 @@ pub(crate) fn finalize_big_fish_agent_message_turn_tx(
|
||||
asset_coverage_json: session.asset_coverage_json.clone(),
|
||||
last_assistant_reply: session.last_assistant_reply.clone(),
|
||||
publish_ready: session.publish_ready,
|
||||
play_count: session.play_count,
|
||||
created_at: session.created_at,
|
||||
updated_at,
|
||||
};
|
||||
@@ -505,6 +515,7 @@ pub(crate) fn finalize_big_fish_agent_message_turn_tx(
|
||||
asset_coverage_json: session.asset_coverage_json.clone(),
|
||||
last_assistant_reply: Some(assistant_reply_text),
|
||||
publish_ready: session.publish_ready,
|
||||
play_count: session.play_count,
|
||||
created_at: session.created_at,
|
||||
updated_at,
|
||||
};
|
||||
@@ -552,6 +563,7 @@ pub(crate) fn compile_big_fish_draft_tx(
|
||||
.map_err(|error| error.to_string())?,
|
||||
last_assistant_reply: Some(reply.clone()),
|
||||
publish_ready: coverage.publish_ready,
|
||||
play_count: session.play_count,
|
||||
created_at: session.created_at,
|
||||
updated_at: compiled_at,
|
||||
};
|
||||
@@ -568,16 +580,17 @@ pub(crate) fn compile_big_fish_draft_tx(
|
||||
|
||||
pub(crate) fn record_big_fish_play_tx(
|
||||
ctx: &ReducerContext,
|
||||
input: BigFishPlayReportInput,
|
||||
) -> Result<BigFishSessionSnapshot, String> {
|
||||
validate_play_report_input(&input).map_err(|error| error.to_string())?;
|
||||
input: BigFishPlayRecordInput,
|
||||
) -> Result<Vec<BigFishWorkSummarySnapshot>, String> {
|
||||
validate_play_record_input(&input).map_err(|error| error.to_string())?;
|
||||
let session = ctx
|
||||
.db
|
||||
.big_fish_creation_session()
|
||||
.session_id()
|
||||
.find(&input.session_id)
|
||||
.filter(|row| row.stage == BigFishCreationStage::Published)
|
||||
.ok_or_else(|| "big_fish_creation_session 不存在或尚未发布".to_string())?;
|
||||
.ok_or_else(|| "big_fish 已发布作品不存在".to_string())?;
|
||||
let played_at = Timestamp::from_micros_since_unix_epoch(input.played_at_micros);
|
||||
let draft = session
|
||||
.draft_json
|
||||
.as_deref()
|
||||
@@ -613,7 +626,7 @@ pub(crate) fn record_big_fish_play_tx(
|
||||
world_type: Some("BIG_FISH".to_string()),
|
||||
world_title: title,
|
||||
world_subtitle: subtitle,
|
||||
played_at_micros: input.reported_at_micros,
|
||||
played_at_micros: input.played_at_micros,
|
||||
},
|
||||
)?;
|
||||
add_profile_observed_play_time(
|
||||
@@ -621,10 +634,34 @@ pub(crate) fn record_big_fish_play_tx(
|
||||
&input.user_id,
|
||||
&world_key,
|
||||
input.elapsed_ms,
|
||||
input.reported_at_micros,
|
||||
input.played_at_micros,
|
||||
)?;
|
||||
let next_session = BigFishCreationSession {
|
||||
session_id: session.session_id.clone(),
|
||||
owner_user_id: session.owner_user_id.clone(),
|
||||
seed_text: session.seed_text.clone(),
|
||||
current_turn: session.current_turn,
|
||||
progress_percent: session.progress_percent,
|
||||
stage: session.stage,
|
||||
anchor_pack_json: session.anchor_pack_json.clone(),
|
||||
draft_json: session.draft_json.clone(),
|
||||
asset_coverage_json: session.asset_coverage_json.clone(),
|
||||
last_assistant_reply: session.last_assistant_reply.clone(),
|
||||
publish_ready: session.publish_ready,
|
||||
// 中文注释:正式进入已发布作品时同时累加作品播放数,用户侧去重由 profile_played_world 保证。
|
||||
play_count: session.play_count.saturating_add(1),
|
||||
created_at: session.created_at,
|
||||
updated_at: played_at,
|
||||
};
|
||||
replace_big_fish_session(ctx, &session, next_session);
|
||||
|
||||
build_big_fish_session_snapshot(ctx, &session)
|
||||
list_big_fish_works_tx(
|
||||
ctx,
|
||||
BigFishWorksListInput {
|
||||
owner_user_id: String::new(),
|
||||
published_only: true,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn build_big_fish_session_snapshot(
|
||||
@@ -740,6 +777,7 @@ pub(crate) fn build_big_fish_work_summary(
|
||||
level_main_image_ready_count: coverage.level_main_image_ready_count,
|
||||
level_motion_ready_count: coverage.level_motion_ready_count,
|
||||
background_ready: coverage.background_ready,
|
||||
play_count: row.play_count,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -776,6 +814,7 @@ mod tests {
|
||||
asset_coverage_json: "{}".to_string(),
|
||||
last_assistant_reply: Some("欢迎来到大鱼吃小鱼共创。".to_string()),
|
||||
publish_ready: false,
|
||||
play_count: 0,
|
||||
created_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||
updated_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ pub struct BigFishCreationSession {
|
||||
pub(crate) asset_coverage_json: String,
|
||||
pub(crate) last_assistant_reply: Option<String>,
|
||||
pub(crate) publish_ready: bool,
|
||||
pub(crate) play_count: u32,
|
||||
pub(crate) created_at: Timestamp,
|
||||
pub(crate) updated_at: Timestamp,
|
||||
}
|
||||
|
||||
@@ -109,6 +109,8 @@ macro_rules! migration_tables {
|
||||
user_browse_history,
|
||||
profile_dashboard_state,
|
||||
profile_wallet_ledger,
|
||||
profile_redeem_code,
|
||||
profile_redeem_code_usage,
|
||||
profile_invite_code,
|
||||
profile_referral_relation,
|
||||
profile_played_world,
|
||||
@@ -659,6 +661,19 @@ where
|
||||
Ok(wrapped.0)
|
||||
}
|
||||
|
||||
fn normalize_migration_row(table_name: &str, value: &serde_json::Value) -> serde_json::Value {
|
||||
let mut next_value = value.clone();
|
||||
if table_name == "big_fish_creation_session" {
|
||||
if let Some(object) = next_value.as_object_mut() {
|
||||
// 中文注释:旧迁移包没有公开游玩次数字段,导入时按新建作品默认 0 兼容。
|
||||
object
|
||||
.entry("play_count".to_string())
|
||||
.or_insert_with(|| serde_json::Value::from(0));
|
||||
}
|
||||
}
|
||||
next_value
|
||||
}
|
||||
|
||||
fn insert_migration_table_rows(
|
||||
ctx: &ReducerContext,
|
||||
table: &MigrationTable,
|
||||
@@ -672,7 +687,8 @@ fn insert_migration_table_rows(
|
||||
let mut imported = 0u64;
|
||||
let mut skipped = 0u64;
|
||||
for value in &table.rows {
|
||||
let row = row_from_json(value)
|
||||
let normalized_value = normalize_migration_row(stringify!($table), value);
|
||||
let row = row_from_json(&normalized_value)
|
||||
.map_err(|error| format!("{}: {error}", stringify!($table)))?;
|
||||
let insert_result = ctx.db
|
||||
.$table()
|
||||
|
||||
@@ -28,6 +28,39 @@ pub struct ProfileWalletLedger {
|
||||
pub(crate) created_at: Timestamp,
|
||||
}
|
||||
|
||||
#[spacetimedb::table(accessor = profile_redeem_code)]
|
||||
pub struct ProfileRedeemCode {
|
||||
#[primary_key]
|
||||
pub(crate) code: String,
|
||||
pub(crate) mode: RuntimeProfileRedeemCodeMode,
|
||||
pub(crate) reward_points: u64,
|
||||
pub(crate) max_uses: u32,
|
||||
pub(crate) global_used_count: u32,
|
||||
pub(crate) enabled: bool,
|
||||
pub(crate) allowed_user_ids: Vec<String>,
|
||||
pub(crate) created_by: String,
|
||||
pub(crate) created_at: Timestamp,
|
||||
pub(crate) updated_at: Timestamp,
|
||||
}
|
||||
|
||||
#[spacetimedb::table(
|
||||
accessor = profile_redeem_code_usage,
|
||||
index(accessor = by_profile_redeem_code_usage_code, btree(columns = [code])),
|
||||
index(accessor = by_profile_redeem_code_usage_user_id, btree(columns = [user_id])),
|
||||
index(
|
||||
accessor = by_profile_redeem_code_usage_code_user_id,
|
||||
btree(columns = [code, user_id])
|
||||
)
|
||||
)]
|
||||
pub struct ProfileRedeemCodeUsage {
|
||||
#[primary_key]
|
||||
pub(crate) usage_id: String,
|
||||
pub(crate) code: String,
|
||||
pub(crate) user_id: String,
|
||||
pub(crate) amount_granted: u64,
|
||||
pub(crate) created_at: Timestamp,
|
||||
}
|
||||
|
||||
#[spacetimedb::table(accessor = profile_invite_code)]
|
||||
pub struct ProfileInviteCode {
|
||||
#[primary_key]
|
||||
@@ -407,6 +440,64 @@ pub fn redeem_profile_referral_invite_code(
|
||||
}
|
||||
}
|
||||
|
||||
// 兑换码奖励、usage 与钱包流水必须在同一事务内落库,避免到账和计次分离。
|
||||
#[spacetimedb::procedure]
|
||||
pub fn redeem_profile_reward_code(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: RuntimeProfileRewardCodeRedeemInput,
|
||||
) -> RuntimeProfileRewardCodeRedeemProcedureResult {
|
||||
match ctx.try_with_tx(|tx| redeem_profile_reward_code_record(tx, input.clone())) {
|
||||
Ok(record) => RuntimeProfileRewardCodeRedeemProcedureResult {
|
||||
ok: true,
|
||||
record: Some(record),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => RuntimeProfileRewardCodeRedeemProcedureResult {
|
||||
ok: false,
|
||||
record: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn admin_upsert_profile_redeem_code(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: RuntimeProfileRedeemCodeAdminUpsertInput,
|
||||
) -> RuntimeProfileRedeemCodeAdminProcedureResult {
|
||||
match ctx.try_with_tx(|tx| admin_upsert_profile_redeem_code_record(tx, input.clone())) {
|
||||
Ok(record) => RuntimeProfileRedeemCodeAdminProcedureResult {
|
||||
ok: true,
|
||||
record: Some(record),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => RuntimeProfileRedeemCodeAdminProcedureResult {
|
||||
ok: false,
|
||||
record: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn admin_disable_profile_redeem_code(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: RuntimeProfileRedeemCodeAdminDisableInput,
|
||||
) -> RuntimeProfileRedeemCodeAdminProcedureResult {
|
||||
match ctx.try_with_tx(|tx| admin_disable_profile_redeem_code_record(tx, input.clone())) {
|
||||
Ok(record) => RuntimeProfileRedeemCodeAdminProcedureResult {
|
||||
ok: true,
|
||||
record: Some(record),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => RuntimeProfileRedeemCodeAdminProcedureResult {
|
||||
ok: false,
|
||||
record: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn list_profile_save_archive_rows(
|
||||
ctx: &ReducerContext,
|
||||
input: RuntimeProfileSaveArchiveListInput,
|
||||
@@ -1371,6 +1462,185 @@ fn redeem_profile_referral_invite_code_record(
|
||||
})
|
||||
}
|
||||
|
||||
fn redeem_profile_reward_code_record(
|
||||
ctx: &ReducerContext,
|
||||
input: RuntimeProfileRewardCodeRedeemInput,
|
||||
) -> Result<RuntimeProfileRewardCodeRedeemSnapshot, String> {
|
||||
let validated_input = build_runtime_profile_reward_code_redeem_input(
|
||||
input.user_id,
|
||||
input.code,
|
||||
input.redeemed_at_micros,
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let redeemed_at = Timestamp::from_micros_since_unix_epoch(validated_input.redeemed_at_micros);
|
||||
let user_id = validated_input.user_id;
|
||||
let code = validated_input.code;
|
||||
let redeem_code = ctx
|
||||
.db
|
||||
.profile_redeem_code()
|
||||
.code()
|
||||
.find(&code)
|
||||
.ok_or_else(|| "兑换码不存在".to_string())?;
|
||||
|
||||
if !redeem_code.enabled {
|
||||
return Err("兑换码已停用".to_string());
|
||||
}
|
||||
if redeem_code.reward_points == 0 {
|
||||
return Err("兑换码奖励无效".to_string());
|
||||
}
|
||||
|
||||
let user_used_count = count_profile_redeem_code_user_usage(ctx, &code, &user_id);
|
||||
match redeem_code.mode {
|
||||
RuntimeProfileRedeemCodeMode::Public if user_used_count >= redeem_code.max_uses => {
|
||||
return Err("兑换次数已用完".to_string());
|
||||
}
|
||||
RuntimeProfileRedeemCodeMode::Unique
|
||||
if redeem_code.global_used_count >= redeem_code.max_uses =>
|
||||
{
|
||||
return Err("兑换次数已用完".to_string());
|
||||
}
|
||||
RuntimeProfileRedeemCodeMode::Private => {
|
||||
if !redeem_code
|
||||
.allowed_user_ids
|
||||
.iter()
|
||||
.any(|item| item == &user_id)
|
||||
{
|
||||
return Err("该兑换码不适用于当前账号".to_string());
|
||||
}
|
||||
if redeem_code.global_used_count >= redeem_code.max_uses {
|
||||
return Err("兑换次数已用完".to_string());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let usage_id = build_profile_redeem_code_usage_id(
|
||||
ctx,
|
||||
&code,
|
||||
&user_id,
|
||||
validated_input.redeemed_at_micros,
|
||||
);
|
||||
let wallet_ledger_id = format!("{}:ledger", usage_id);
|
||||
let wallet_balance = apply_profile_wallet_delta(
|
||||
ctx,
|
||||
&user_id,
|
||||
redeem_code.reward_points,
|
||||
RuntimeProfileWalletLedgerSourceType::RedeemCodeReward,
|
||||
&wallet_ledger_id,
|
||||
redeemed_at,
|
||||
)?;
|
||||
|
||||
ctx.db
|
||||
.profile_redeem_code_usage()
|
||||
.insert(ProfileRedeemCodeUsage {
|
||||
usage_id,
|
||||
code: code.clone(),
|
||||
user_id,
|
||||
amount_granted: redeem_code.reward_points,
|
||||
created_at: redeemed_at,
|
||||
});
|
||||
|
||||
let next_code = ProfileRedeemCode {
|
||||
global_used_count: redeem_code.global_used_count.saturating_add(1),
|
||||
updated_at: redeemed_at,
|
||||
..redeem_code
|
||||
};
|
||||
ctx.db.profile_redeem_code().code().delete(&code);
|
||||
ctx.db.profile_redeem_code().insert(next_code);
|
||||
|
||||
let ledger_entry = ctx
|
||||
.db
|
||||
.profile_wallet_ledger()
|
||||
.wallet_ledger_id()
|
||||
.find(&wallet_ledger_id)
|
||||
.ok_or_else(|| "兑换码钱包流水写入失败".to_string())?;
|
||||
|
||||
Ok(RuntimeProfileRewardCodeRedeemSnapshot {
|
||||
wallet_balance,
|
||||
amount_granted: ledger_entry.amount_delta.max(0) as u64,
|
||||
ledger_entry: build_profile_wallet_ledger_snapshot_from_row(&ledger_entry),
|
||||
})
|
||||
}
|
||||
|
||||
fn admin_upsert_profile_redeem_code_record(
|
||||
ctx: &ReducerContext,
|
||||
input: RuntimeProfileRedeemCodeAdminUpsertInput,
|
||||
) -> Result<RuntimeProfileRedeemCodeSnapshot, String> {
|
||||
let validated_input = build_runtime_profile_redeem_code_admin_upsert_input(
|
||||
input.admin_user_id,
|
||||
input.code,
|
||||
input.mode,
|
||||
input.reward_points,
|
||||
input.max_uses,
|
||||
input.enabled,
|
||||
input.allowed_user_ids,
|
||||
input.allowed_public_user_codes,
|
||||
input.updated_at_micros,
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let updated_at = Timestamp::from_micros_since_unix_epoch(validated_input.updated_at_micros);
|
||||
let allowed_user_ids = resolve_profile_redeem_code_allowed_user_ids(ctx, &validated_input)?;
|
||||
let existing = ctx
|
||||
.db
|
||||
.profile_redeem_code()
|
||||
.code()
|
||||
.find(&validated_input.code);
|
||||
let created_at = existing
|
||||
.as_ref()
|
||||
.map(|row| row.created_at)
|
||||
.unwrap_or(updated_at);
|
||||
let global_used_count = existing
|
||||
.as_ref()
|
||||
.map(|row| row.global_used_count)
|
||||
.unwrap_or(0);
|
||||
|
||||
if let Some(existing) = existing {
|
||||
ctx.db.profile_redeem_code().code().delete(&existing.code);
|
||||
}
|
||||
|
||||
let row = ProfileRedeemCode {
|
||||
code: validated_input.code,
|
||||
mode: validated_input.mode,
|
||||
reward_points: validated_input.reward_points,
|
||||
max_uses: validated_input.max_uses,
|
||||
global_used_count,
|
||||
enabled: validated_input.enabled,
|
||||
allowed_user_ids,
|
||||
created_by: validated_input.admin_user_id,
|
||||
created_at,
|
||||
updated_at,
|
||||
};
|
||||
let inserted = ctx.db.profile_redeem_code().insert(row);
|
||||
Ok(build_profile_redeem_code_snapshot_from_row(&inserted))
|
||||
}
|
||||
|
||||
fn admin_disable_profile_redeem_code_record(
|
||||
ctx: &ReducerContext,
|
||||
input: RuntimeProfileRedeemCodeAdminDisableInput,
|
||||
) -> Result<RuntimeProfileRedeemCodeSnapshot, String> {
|
||||
let validated_input = build_runtime_profile_redeem_code_admin_disable_input(
|
||||
input.admin_user_id,
|
||||
input.code,
|
||||
input.updated_at_micros,
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let updated_at = Timestamp::from_micros_since_unix_epoch(validated_input.updated_at_micros);
|
||||
let existing = ctx
|
||||
.db
|
||||
.profile_redeem_code()
|
||||
.code()
|
||||
.find(&validated_input.code)
|
||||
.ok_or_else(|| "兑换码不存在".to_string())?;
|
||||
|
||||
ctx.db.profile_redeem_code().code().delete(&existing.code);
|
||||
let inserted = ctx.db.profile_redeem_code().insert(ProfileRedeemCode {
|
||||
enabled: false,
|
||||
updated_at,
|
||||
..existing
|
||||
});
|
||||
Ok(build_profile_redeem_code_snapshot_from_row(&inserted))
|
||||
}
|
||||
|
||||
fn build_profile_referral_invite_center_snapshot(
|
||||
ctx: &ReducerContext,
|
||||
user_id: &str,
|
||||
@@ -1756,6 +2026,74 @@ fn latest_profile_recharge_order(
|
||||
orders.into_iter().next()
|
||||
}
|
||||
|
||||
fn count_profile_redeem_code_user_usage(ctx: &ReducerContext, code: &str, user_id: &str) -> u32 {
|
||||
ctx.db
|
||||
.profile_redeem_code_usage()
|
||||
.by_profile_redeem_code_usage_code_user_id()
|
||||
.filter((code, user_id))
|
||||
.count() as u32
|
||||
}
|
||||
|
||||
fn build_profile_redeem_code_usage_id(
|
||||
ctx: &ReducerContext,
|
||||
code: &str,
|
||||
user_id: &str,
|
||||
redeemed_at_micros: i64,
|
||||
) -> String {
|
||||
let sequence = count_profile_redeem_code_user_usage(ctx, code, user_id);
|
||||
format!(
|
||||
"redeem:{}:{}:{}:{}",
|
||||
code, user_id, redeemed_at_micros, sequence
|
||||
)
|
||||
}
|
||||
|
||||
fn resolve_profile_redeem_code_allowed_user_ids(
|
||||
ctx: &ReducerContext,
|
||||
input: &RuntimeProfileRedeemCodeAdminUpsertInput,
|
||||
) -> Result<Vec<String>, String> {
|
||||
if input.mode != RuntimeProfileRedeemCodeMode::Private {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mut allowed_user_ids = input.allowed_user_ids.clone();
|
||||
for public_user_code in &input.allowed_public_user_codes {
|
||||
if let Some(account) = ctx
|
||||
.db
|
||||
.user_account()
|
||||
.by_user_account_public_code()
|
||||
.filter(public_user_code)
|
||||
.next()
|
||||
{
|
||||
allowed_user_ids.push(account.user_id);
|
||||
}
|
||||
}
|
||||
allowed_user_ids.sort();
|
||||
allowed_user_ids.dedup();
|
||||
|
||||
if allowed_user_ids.is_empty() {
|
||||
return Err("私有兑换码必须指定可兑换用户".to_string());
|
||||
}
|
||||
|
||||
Ok(allowed_user_ids)
|
||||
}
|
||||
|
||||
fn build_profile_redeem_code_snapshot_from_row(
|
||||
row: &ProfileRedeemCode,
|
||||
) -> RuntimeProfileRedeemCodeSnapshot {
|
||||
RuntimeProfileRedeemCodeSnapshot {
|
||||
code: row.code.clone(),
|
||||
mode: row.mode,
|
||||
reward_points: row.reward_points,
|
||||
max_uses: row.max_uses,
|
||||
global_used_count: row.global_used_count,
|
||||
enabled: row.enabled,
|
||||
allowed_user_ids: row.allowed_user_ids.clone(),
|
||||
created_by: row.created_by.clone(),
|
||||
created_at_micros: row.created_at.to_micros_since_unix_epoch(),
|
||||
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_profile_wallet_ledger_snapshot_from_row(
|
||||
row: &ProfileWalletLedger,
|
||||
) -> RuntimeProfileWalletLedgerEntrySnapshot {
|
||||
|
||||
Reference in New Issue
Block a user