Merge pull request #1 from codex/profile-redeem-code
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
profile redeem code and dev password registration
This commit was merged in pull request #1.
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 {
|
||||
@@ -851,6 +866,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(
|
||||
@@ -1357,6 +1386,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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 不能为空"),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -120,6 +120,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,
|
||||
@@ -129,8 +131,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> {
|
||||
@@ -1673,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 {
|
||||
@@ -3284,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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
// 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;
|
||||
}
|
||||
|
||||
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,53 @@
|
||||
// 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;
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -251,6 +251,9 @@ pub mod list_custom_world_works_procedure;
|
||||
pub mod list_platform_browse_history_procedure;
|
||||
pub mod list_profile_save_archives_procedure;
|
||||
pub mod list_profile_wallet_ledger_procedure;
|
||||
pub mod redeem_profile_reward_code_procedure;
|
||||
pub mod admin_upsert_profile_redeem_code_procedure;
|
||||
pub mod admin_disable_profile_redeem_code_procedure;
|
||||
pub mod list_puzzle_gallery_procedure;
|
||||
pub mod list_puzzle_works_procedure;
|
||||
pub mod npc_battle_interaction_procedure_result_type;
|
||||
@@ -415,6 +418,14 @@ pub mod runtime_profile_wallet_ledger_entry_snapshot_type;
|
||||
pub mod runtime_profile_wallet_ledger_list_input_type;
|
||||
pub mod runtime_profile_wallet_ledger_procedure_result_type;
|
||||
pub mod runtime_profile_wallet_ledger_source_type_type;
|
||||
pub mod runtime_profile_redeem_code_mode_type;
|
||||
pub mod runtime_profile_reward_code_redeem_input_type;
|
||||
pub mod runtime_profile_reward_code_redeem_snapshot_type;
|
||||
pub mod runtime_profile_reward_code_redeem_procedure_result_type;
|
||||
pub mod runtime_profile_redeem_code_admin_upsert_input_type;
|
||||
pub mod runtime_profile_redeem_code_admin_disable_input_type;
|
||||
pub mod runtime_profile_redeem_code_snapshot_type;
|
||||
pub mod runtime_profile_redeem_code_admin_procedure_result_type;
|
||||
pub mod runtime_referral_invite_center_get_input_type;
|
||||
pub mod runtime_referral_invite_center_procedure_result_type;
|
||||
pub mod runtime_referral_invite_center_snapshot_type;
|
||||
@@ -722,6 +733,9 @@ pub use list_custom_world_works_procedure::list_custom_world_works;
|
||||
pub use list_platform_browse_history_procedure::list_platform_browse_history;
|
||||
pub use list_profile_save_archives_procedure::list_profile_save_archives;
|
||||
pub use list_profile_wallet_ledger_procedure::list_profile_wallet_ledger;
|
||||
pub use redeem_profile_reward_code_procedure::redeem_profile_reward_code;
|
||||
pub use admin_upsert_profile_redeem_code_procedure::admin_upsert_profile_redeem_code;
|
||||
pub use admin_disable_profile_redeem_code_procedure::admin_disable_profile_redeem_code;
|
||||
pub use list_puzzle_gallery_procedure::list_puzzle_gallery;
|
||||
pub use list_puzzle_works_procedure::list_puzzle_works;
|
||||
pub use npc_battle_interaction_procedure_result_type::NpcBattleInteractionProcedureResult;
|
||||
@@ -886,6 +900,14 @@ pub use runtime_profile_wallet_ledger_entry_snapshot_type::RuntimeProfileWalletL
|
||||
pub use runtime_profile_wallet_ledger_list_input_type::RuntimeProfileWalletLedgerListInput;
|
||||
pub use runtime_profile_wallet_ledger_procedure_result_type::RuntimeProfileWalletLedgerProcedureResult;
|
||||
pub use runtime_profile_wallet_ledger_source_type_type::RuntimeProfileWalletLedgerSourceType;
|
||||
pub use runtime_profile_redeem_code_mode_type::RuntimeProfileRedeemCodeMode;
|
||||
pub use runtime_profile_reward_code_redeem_input_type::RuntimeProfileRewardCodeRedeemInput;
|
||||
pub use runtime_profile_reward_code_redeem_snapshot_type::RuntimeProfileRewardCodeRedeemSnapshot;
|
||||
pub use runtime_profile_reward_code_redeem_procedure_result_type::RuntimeProfileRewardCodeRedeemProcedureResult;
|
||||
pub use runtime_profile_redeem_code_admin_upsert_input_type::RuntimeProfileRedeemCodeAdminUpsertInput;
|
||||
pub use runtime_profile_redeem_code_admin_disable_input_type::RuntimeProfileRedeemCodeAdminDisableInput;
|
||||
pub use runtime_profile_redeem_code_snapshot_type::RuntimeProfileRedeemCodeSnapshot;
|
||||
pub use runtime_profile_redeem_code_admin_procedure_result_type::RuntimeProfileRedeemCodeAdminProcedureResult;
|
||||
pub use runtime_referral_invite_center_get_input_type::RuntimeReferralInviteCenterGetInput;
|
||||
pub use runtime_referral_invite_center_procedure_result_type::RuntimeReferralInviteCenterProcedureResult;
|
||||
pub use runtime_referral_invite_center_snapshot_type::RuntimeReferralInviteCenterSnapshot;
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
// 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;
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
@@ -396,6 +429,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,
|
||||
@@ -1194,6 +1285,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,
|
||||
@@ -1579,6 +1849,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