feat: 支持充值商品配置和档位首充

This commit is contained in:
2026-05-15 06:11:57 +08:00
parent 9c33cc565c
commit c7fe793a9e
36 changed files with 2096 additions and 72 deletions

View File

@@ -8,8 +8,9 @@ use crate::{
},
runtime_profile::{
admin_disable_profile_redeem_code, admin_disable_profile_task_config,
admin_list_profile_invite_codes, admin_list_profile_redeem_codes,
admin_list_profile_task_configs, admin_upsert_profile_invite_code,
admin_list_profile_invite_codes, admin_list_profile_recharge_products,
admin_list_profile_redeem_codes, admin_list_profile_task_configs,
admin_upsert_profile_invite_code, admin_upsert_profile_recharge_product,
admin_upsert_profile_redeem_code, admin_upsert_profile_task_config,
},
state::AppState,
@@ -104,7 +105,14 @@ pub fn router(state: AppState) -> Router<AppState> {
)
.route(
"/admin/api/profile/tasks/disable",
axum::routing::post(admin_disable_profile_task_config)
axum::routing::post(admin_disable_profile_task_config).route_layer(
middleware::from_fn_with_state(state.clone(), require_admin_auth),
),
)
.route(
"/admin/api/profile/recharge-products",
get(admin_list_profile_recharge_products)
.post(admin_upsert_profile_recharge_product)
.route_layer(middleware::from_fn_with_state(state, require_admin_auth)),
)
}

View File

@@ -9,13 +9,15 @@ use module_runtime::{
PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_MINI_PROGRAM, RuntimeProfileFeedbackEvidenceRecord,
RuntimeProfileFeedbackEvidenceSnapshot, RuntimeProfileFeedbackSubmissionRecord,
RuntimeProfileInviteCodeRecord, RuntimeProfileMembershipBenefitRecord,
RuntimeProfileRechargeCenterRecord, RuntimeProfileRechargeOrderRecord,
RuntimeProfileRechargeOrderStatus, RuntimeProfileRechargeProductRecord,
RuntimeProfileRedeemCodeMode, RuntimeProfileRedeemCodeRecord,
RuntimeProfileRewardCodeRedeemRecord, RuntimeProfileTaskCenterRecord,
RuntimeProfileTaskClaimRecord, RuntimeProfileTaskConfigRecord, RuntimeProfileTaskCycle,
RuntimeProfileTaskItemRecord, RuntimeProfileTaskStatus, RuntimeProfileWalletLedgerSourceType,
RuntimeReferralInviteCenterRecord, RuntimeTrackingScopeKind,
RuntimeProfileMembershipTier, RuntimeProfileRechargeCenterRecord,
RuntimeProfileRechargeOrderRecord, RuntimeProfileRechargeOrderStatus,
RuntimeProfileRechargeProductConfigRecord, RuntimeProfileRechargeProductKind,
RuntimeProfileRechargeProductRecord, RuntimeProfileRedeemCodeMode,
RuntimeProfileRedeemCodeRecord, RuntimeProfileRewardCodeRedeemRecord,
RuntimeProfileTaskCenterRecord, RuntimeProfileTaskClaimRecord, RuntimeProfileTaskConfigRecord,
RuntimeProfileTaskCycle, RuntimeProfileTaskItemRecord, RuntimeProfileTaskStatus,
RuntimeProfileWalletLedgerSourceType, RuntimeReferralInviteCenterRecord,
RuntimeTrackingScopeKind,
};
use serde::Deserialize;
use serde_json::{Value, json};
@@ -23,12 +25,16 @@ use shared_contracts::runtime::{
ANALYTICS_GRANULARITY_DAY, ANALYTICS_GRANULARITY_MONTH, ANALYTICS_GRANULARITY_QUARTER,
ANALYTICS_GRANULARITY_WEEK, ANALYTICS_GRANULARITY_YEAR, AdminDisableProfileRedeemCodeRequest,
AdminDisableProfileTaskConfigRequest, AdminUpsertProfileInviteCodeRequest,
AdminUpsertProfileRedeemCodeRequest, AdminUpsertProfileTaskConfigRequest,
AnalyticsBucketMetricResponse, AnalyticsMetricQueryResponse, ClaimProfileTaskRewardResponse,
AdminUpsertProfileRechargeProductRequest, AdminUpsertProfileRedeemCodeRequest,
AdminUpsertProfileTaskConfigRequest, AnalyticsBucketMetricResponse,
AnalyticsMetricQueryResponse, ClaimProfileTaskRewardResponse,
ConfirmWechatProfileRechargeOrderResponse, CreateProfileRechargeOrderRequest,
CreateProfileRechargeOrderResponse, PROFILE_FEEDBACK_STATUS_OPEN, PROFILE_TASK_CYCLE_DAILY,
PROFILE_TASK_STATUS_CLAIMABLE, PROFILE_TASK_STATUS_CLAIMED, PROFILE_TASK_STATUS_DISABLED,
PROFILE_TASK_STATUS_INCOMPLETE, PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_CONSUME,
CreateProfileRechargeOrderResponse, PROFILE_FEEDBACK_STATUS_OPEN,
PROFILE_MEMBERSHIP_TIER_MONTH, PROFILE_MEMBERSHIP_TIER_NORMAL, PROFILE_MEMBERSHIP_TIER_SEASON,
PROFILE_MEMBERSHIP_TIER_YEAR, PROFILE_RECHARGE_PRODUCT_KIND_MEMBERSHIP,
PROFILE_RECHARGE_PRODUCT_KIND_POINTS, PROFILE_TASK_CYCLE_DAILY, PROFILE_TASK_STATUS_CLAIMABLE,
PROFILE_TASK_STATUS_CLAIMED, PROFILE_TASK_STATUS_DISABLED, PROFILE_TASK_STATUS_INCOMPLETE,
PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_CONSUME,
PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_REFUND,
PROFILE_WALLET_LEDGER_SOURCE_TYPE_DAILY_TASK_REWARD,
PROFILE_WALLET_LEDGER_SOURCE_TYPE_INVITE_INVITEE_REWARD,
@@ -42,6 +48,7 @@ use shared_contracts::runtime::{
ProfileInviteCodeAdminListResponse, ProfileInviteCodeAdminResponse,
ProfileMembershipBenefitResponse, ProfileMembershipResponse, ProfilePlayStatsResponse,
ProfilePlayedWorkSummaryResponse, ProfileRechargeCenterResponse, ProfileRechargeOrderResponse,
ProfileRechargeProductConfigAdminListResponse, ProfileRechargeProductConfigAdminResponse,
ProfileRechargeProductResponse, ProfileRedeemCodeAdminListResponse,
ProfileRedeemCodeAdminResponse, ProfileReferralInviteCenterResponse,
ProfileReferralInvitedUserResponse, ProfileTaskCenterResponse,
@@ -669,6 +676,84 @@ pub async fn admin_disable_profile_task_config(
))
}
pub async fn admin_list_profile_recharge_products(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
Extension(admin): Extension<AuthenticatedAdmin>,
) -> Result<Json<Value>, Response> {
let entries = state
.spacetime_client()
.admin_list_profile_recharge_products(admin.session().subject.clone())
.await
.map_err(|error| {
runtime_profile_error_response(
&request_context,
map_runtime_profile_client_error(error),
)
})?;
Ok(json_success_body(
Some(&request_context),
ProfileRechargeProductConfigAdminListResponse {
entries: entries
.into_iter()
.map(build_profile_recharge_product_config_admin_response)
.collect(),
},
))
}
pub async fn admin_upsert_profile_recharge_product(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
Extension(admin): Extension<AuthenticatedAdmin>,
Json(payload): Json<AdminUpsertProfileRechargeProductRequest>,
) -> Result<Json<Value>, Response> {
let kind = parse_profile_recharge_product_kind(&payload.kind).map_err(|error| {
runtime_profile_error_response(
&request_context,
AppError::from_status(StatusCode::BAD_REQUEST).with_message(error),
)
})?;
let tier = parse_profile_membership_tier(&payload.tier).map_err(|error| {
runtime_profile_error_response(
&request_context,
AppError::from_status(StatusCode::BAD_REQUEST).with_message(error),
)
})?;
let updated_at_micros = OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000;
let record = state
.spacetime_client()
.admin_upsert_profile_recharge_product(
admin.session().subject.clone(),
payload.product_id,
payload.title,
payload.price_cents,
kind,
payload.points_amount,
payload.bonus_points,
payload.duration_days,
payload.badge_label.unwrap_or_default(),
payload.description.unwrap_or_default(),
tier,
payload.enabled,
payload.sort_order.unwrap_or(10),
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_recharge_product_config_admin_response(record),
))
}
pub async fn admin_list_profile_redeem_codes(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
@@ -1172,6 +1257,29 @@ fn build_profile_task_config_admin_response(
}
}
fn build_profile_recharge_product_config_admin_response(
record: RuntimeProfileRechargeProductConfigRecord,
) -> ProfileRechargeProductConfigAdminResponse {
ProfileRechargeProductConfigAdminResponse {
product_id: record.product_id,
title: record.title,
price_cents: record.price_cents,
kind: format_profile_recharge_product_kind(record.kind).to_string(),
points_amount: record.points_amount,
bonus_points: record.bonus_points,
duration_days: record.duration_days,
badge_label: record.badge_label,
description: record.description,
tier: format_profile_membership_tier(record.tier).to_string(),
enabled: record.enabled,
sort_order: record.sort_order,
created_by: record.created_by,
created_at: record.created_at,
updated_by: record.updated_by,
updated_at: record.updated_at,
}
}
fn normalize_admin_invite_code_metadata(metadata: Option<Value>) -> Result<String, AppError> {
let metadata = match metadata {
Some(Value::Null) | None => json!({}),
@@ -1233,6 +1341,28 @@ fn parse_profile_task_cycle(raw: &str) -> Result<RuntimeProfileTaskCycle, String
}
}
fn parse_profile_recharge_product_kind(
raw: &str,
) -> Result<RuntimeProfileRechargeProductKind, String> {
match raw.trim().to_ascii_lowercase().as_str() {
PROFILE_RECHARGE_PRODUCT_KIND_POINTS => Ok(RuntimeProfileRechargeProductKind::Points),
PROFILE_RECHARGE_PRODUCT_KIND_MEMBERSHIP => {
Ok(RuntimeProfileRechargeProductKind::Membership)
}
_ => Err("充值商品类型无效".to_string()),
}
}
fn parse_profile_membership_tier(raw: &str) -> Result<RuntimeProfileMembershipTier, String> {
match raw.trim().to_ascii_lowercase().as_str() {
PROFILE_MEMBERSHIP_TIER_NORMAL => Ok(RuntimeProfileMembershipTier::Normal),
PROFILE_MEMBERSHIP_TIER_MONTH => Ok(RuntimeProfileMembershipTier::Month),
PROFILE_MEMBERSHIP_TIER_SEASON => Ok(RuntimeProfileMembershipTier::Season),
PROFILE_MEMBERSHIP_TIER_YEAR => Ok(RuntimeProfileMembershipTier::Year),
_ => Err("会员档位无效".to_string()),
}
}
fn parse_tracking_scope_kind(raw: &str) -> Result<RuntimeTrackingScopeKind, String> {
match raw.trim().to_ascii_lowercase().as_str() {
TRACKING_SCOPE_KIND_SITE => Ok(RuntimeTrackingScopeKind::Site),
@@ -1269,6 +1399,22 @@ fn format_profile_task_status(status: RuntimeProfileTaskStatus) -> &'static str
}
}
fn format_profile_recharge_product_kind(kind: RuntimeProfileRechargeProductKind) -> &'static str {
match kind {
RuntimeProfileRechargeProductKind::Points => PROFILE_RECHARGE_PRODUCT_KIND_POINTS,
RuntimeProfileRechargeProductKind::Membership => PROFILE_RECHARGE_PRODUCT_KIND_MEMBERSHIP,
}
}
fn format_profile_membership_tier(tier: RuntimeProfileMembershipTier) -> &'static str {
match tier {
RuntimeProfileMembershipTier::Normal => PROFILE_MEMBERSHIP_TIER_NORMAL,
RuntimeProfileMembershipTier::Month => PROFILE_MEMBERSHIP_TIER_MONTH,
RuntimeProfileMembershipTier::Season => PROFILE_MEMBERSHIP_TIER_SEASON,
RuntimeProfileMembershipTier::Year => PROFILE_MEMBERSHIP_TIER_YEAR,
}
}
fn format_tracking_scope_kind(scope_kind: RuntimeTrackingScopeKind) -> &'static str {
match scope_kind {
RuntimeTrackingScopeKind::Site => TRACKING_SCOPE_KIND_SITE,
@@ -1702,6 +1848,7 @@ mod tests {
for uri in [
"/admin/api/profile/redeem-codes",
"/admin/api/profile/invite-codes",
"/admin/api/profile/recharge-products",
] {
let response = app
.clone()

View File

@@ -292,6 +292,31 @@ pub fn build_runtime_profile_recharge_product_record(
}
}
pub fn build_runtime_profile_recharge_product_config_record(
snapshot: RuntimeProfileRechargeProductConfigSnapshot,
) -> RuntimeProfileRechargeProductConfigRecord {
RuntimeProfileRechargeProductConfigRecord {
product_id: snapshot.product_id,
title: snapshot.title,
price_cents: snapshot.price_cents,
kind: snapshot.kind,
points_amount: snapshot.points_amount,
bonus_points: snapshot.bonus_points,
duration_days: snapshot.duration_days,
badge_label: snapshot.badge_label,
description: snapshot.description,
tier: snapshot.tier,
enabled: snapshot.enabled,
sort_order: snapshot.sort_order,
created_by: snapshot.created_by,
created_at: format_utc_micros(snapshot.created_at_micros),
created_at_micros: snapshot.created_at_micros,
updated_by: snapshot.updated_by,
updated_at: format_utc_micros(snapshot.updated_at_micros),
updated_at_micros: snapshot.updated_at_micros,
}
}
pub fn build_runtime_profile_membership_benefit_record(
snapshot: RuntimeProfileMembershipBenefitSnapshot,
) -> RuntimeProfileMembershipBenefitRecord {
@@ -1114,9 +1139,9 @@ fn hash_runtime_profile_recharge_order_key(
pub fn resolve_runtime_profile_points_recharge_delta(
product: &RuntimeProfileRechargeProductSnapshot,
has_points_recharged: bool,
has_product_recharged: bool,
) -> u64 {
let bonus_points = if has_points_recharged {
let bonus_points = if has_product_recharged {
0
} else {
product.bonus_points

View File

@@ -11,7 +11,7 @@ use shared_kernel::{
use crate::domain::*;
use crate::errors::*;
use crate::{format_utc_micros, runtime_profile_recharge_product_by_id};
use crate::format_utc_micros;
pub const PROFILE_USER_TAG_MAX_COUNT: usize = 8;
pub const PROFILE_USER_TAG_MAX_CHARS: usize = 16;
@@ -259,9 +259,6 @@ pub fn build_runtime_profile_recharge_order_create_input(
let user_id = normalize_runtime_profile_user_id(user_id)?;
let product_id =
normalize_required_string(product_id).ok_or(RuntimeProfileFieldError::MissingProductId)?;
if runtime_profile_recharge_product_by_id(&product_id).is_none() {
return Err(RuntimeProfileFieldError::UnknownRechargeProduct);
}
let payment_channel = normalize_required_string(payment_channel)
.unwrap_or_else(|| PROFILE_RECHARGE_PAYMENT_CHANNEL_MOCK.to_string());
@@ -273,6 +270,78 @@ pub fn build_runtime_profile_recharge_order_create_input(
})
}
pub fn build_runtime_profile_recharge_product_admin_list_input(
admin_user_id: String,
) -> Result<RuntimeProfileRechargeProductAdminListInput, RuntimeProfileFieldError> {
let admin_user_id = normalize_runtime_profile_user_id(admin_user_id)?;
Ok(RuntimeProfileRechargeProductAdminListInput { admin_user_id })
}
#[allow(clippy::too_many_arguments)]
pub fn build_runtime_profile_recharge_product_admin_upsert_input(
admin_user_id: String,
product_id: String,
title: String,
price_cents: u64,
kind: RuntimeProfileRechargeProductKind,
points_amount: u64,
bonus_points: u64,
duration_days: u32,
badge_label: String,
description: String,
tier: RuntimeProfileMembershipTier,
enabled: bool,
sort_order: i32,
updated_at_micros: i64,
) -> Result<RuntimeProfileRechargeProductAdminUpsertInput, RuntimeProfileFieldError> {
let admin_user_id = normalize_runtime_profile_user_id(admin_user_id)?;
let product_id =
normalize_required_string(product_id).ok_or(RuntimeProfileFieldError::MissingProductId)?;
let title =
normalize_required_string(title).ok_or(RuntimeProfileFieldError::MissingProductTitle)?;
if price_cents == 0 {
return Err(RuntimeProfileFieldError::InvalidRechargeProductPrice);
}
match kind {
RuntimeProfileRechargeProductKind::Points => {
if points_amount == 0 {
return Err(RuntimeProfileFieldError::InvalidRechargeProductPoints);
}
if duration_days != 0 || tier != RuntimeProfileMembershipTier::Normal {
return Err(RuntimeProfileFieldError::InvalidRechargeProductTier);
}
}
RuntimeProfileRechargeProductKind::Membership => {
if duration_days == 0 {
return Err(RuntimeProfileFieldError::InvalidRechargeProductDuration);
}
if points_amount != 0
|| bonus_points != 0
|| tier == RuntimeProfileMembershipTier::Normal
{
return Err(RuntimeProfileFieldError::InvalidRechargeProductTier);
}
}
}
Ok(RuntimeProfileRechargeProductAdminUpsertInput {
admin_user_id,
product_id,
title,
price_cents,
kind,
points_amount,
bonus_points,
duration_days,
badge_label: normalize_optional_string(Some(badge_label)).unwrap_or_default(),
description: normalize_optional_string(Some(description)).unwrap_or_default(),
tier,
enabled,
sort_order,
updated_at_micros,
})
}
pub fn build_runtime_profile_recharge_order_paid_input(
order_id: String,
paid_at_micros: i64,

View File

@@ -986,6 +986,27 @@ pub struct RuntimeProfileRechargeProductSnapshot {
pub tier: RuntimeProfileMembershipTier,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RuntimeProfileRechargeProductConfigSnapshot {
pub product_id: String,
pub title: String,
pub price_cents: u64,
pub kind: RuntimeProfileRechargeProductKind,
pub points_amount: u64,
pub bonus_points: u64,
pub duration_days: u32,
pub badge_label: String,
pub description: String,
pub tier: RuntimeProfileMembershipTier,
pub enabled: bool,
pub sort_order: i32,
pub created_by: String,
pub created_at_micros: i64,
pub updated_by: String,
pub updated_at_micros: i64,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RuntimeProfileMembershipBenefitSnapshot {
@@ -1054,6 +1075,47 @@ pub struct RuntimeProfileRechargeCenterProcedureResult {
pub error_message: Option<String>,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RuntimeProfileRechargeProductAdminListInput {
pub admin_user_id: String,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RuntimeProfileRechargeProductAdminUpsertInput {
pub admin_user_id: String,
pub product_id: String,
pub title: String,
pub price_cents: u64,
pub kind: RuntimeProfileRechargeProductKind,
pub points_amount: u64,
pub bonus_points: u64,
pub duration_days: u32,
pub badge_label: String,
pub description: String,
pub tier: RuntimeProfileMembershipTier,
pub enabled: bool,
pub sort_order: i32,
pub updated_at_micros: i64,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RuntimeProfileRechargeProductAdminListProcedureResult {
pub ok: bool,
pub entries: Vec<RuntimeProfileRechargeProductConfigSnapshot>,
pub error_message: Option<String>,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RuntimeProfileRechargeProductAdminProcedureResult {
pub ok: bool,
pub record: Option<RuntimeProfileRechargeProductConfigSnapshot>,
pub error_message: Option<String>,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RuntimeProfileRechargeCenterGetInput {
@@ -1463,6 +1525,28 @@ pub struct RuntimeProfileRechargeProductRecord {
pub tier: RuntimeProfileMembershipTier,
}
#[derive(Clone, Debug, PartialEq)]
pub struct RuntimeProfileRechargeProductConfigRecord {
pub product_id: String,
pub title: String,
pub price_cents: u64,
pub kind: RuntimeProfileRechargeProductKind,
pub points_amount: u64,
pub bonus_points: u64,
pub duration_days: u32,
pub badge_label: String,
pub description: String,
pub tier: RuntimeProfileMembershipTier,
pub enabled: bool,
pub sort_order: i32,
pub created_by: String,
pub created_at: String,
pub created_at_micros: i64,
pub updated_by: String,
pub updated_at: String,
pub updated_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq)]
pub struct RuntimeProfileMembershipBenefitRecord {
pub benefit_name: String,

View File

@@ -74,6 +74,12 @@ pub enum RuntimeProfileFieldError {
TaskAlreadyClaimed,
MissingOrderId,
MissingProductId,
MissingProductTitle,
InvalidRechargeProductPrice,
InvalidRechargeProductPoints,
InvalidRechargeProductDuration,
InvalidRechargeProductKind,
InvalidRechargeProductTier,
MissingWorldKey,
MissingBottomTab,
MissingCheckpointSessionId,
@@ -136,6 +142,14 @@ impl std::fmt::Display for RuntimeProfileFieldError {
Self::TaskAlreadyClaimed => f.write_str("任务奖励已领取"),
Self::MissingOrderId => f.write_str("recharge.order_id 不能为空"),
Self::MissingProductId => f.write_str("recharge.product_id 不能为空"),
Self::MissingProductTitle => f.write_str("recharge.product_title 不能为空"),
Self::InvalidRechargeProductPrice => f.write_str("recharge.price_cents 必须大于 0"),
Self::InvalidRechargeProductPoints => f.write_str("泥点商品 points_amount 必须大于 0"),
Self::InvalidRechargeProductDuration => {
f.write_str("会员商品 duration_days 必须大于 0")
}
Self::InvalidRechargeProductKind => f.write_str("充值商品类型无效"),
Self::InvalidRechargeProductTier => f.write_str("会员商品 tier 无效"),
Self::MissingWorldKey => f.write_str("profile.world_key 不能为空"),
Self::MissingBottomTab => f.write_str("runtime_snapshot.bottom_tab 不能为空"),
Self::MissingCheckpointSessionId => f.write_str("checkpoint.session_id 不能为空"),

View File

@@ -77,19 +77,11 @@ pub fn runtime_profile_recharge_point_products() -> Vec<RuntimeProfileRechargePr
]
}
/// 中文注释:充值中心展示当前账号本次实际可生效的首充赠送状态
/// 中文注释:保留旧展示 helper 的兼容入口;首充资格已改为按商品档位在配置表侧计算
pub fn resolve_runtime_profile_recharge_point_products(
has_points_recharged: bool,
_has_points_recharged: bool,
) -> Vec<RuntimeProfileRechargeProductSnapshot> {
let mut products = runtime_profile_recharge_point_products();
if has_points_recharged {
for product in &mut products {
product.bonus_points = 0;
product.badge_label.clear();
product.description = product.title.clone();
}
}
products
runtime_profile_recharge_point_products()
}
pub fn runtime_profile_recharge_membership_products() -> Vec<RuntimeProfileRechargeProductSnapshot>
@@ -722,32 +714,33 @@ mod tests {
}
#[test]
fn recharge_point_products_resolve_effective_first_bonus_display() {
fn recharge_point_products_do_not_hide_all_first_bonus_by_account_flag() {
let first_recharge_products = resolve_runtime_profile_recharge_point_products(false);
assert_eq!(first_recharge_products[0].bonus_points, 60);
assert_eq!(first_recharge_products[0].badge_label, "首充双倍");
assert_eq!(first_recharge_products[0].description, "首充送60泥点");
let repeated_recharge_products = resolve_runtime_profile_recharge_point_products(true);
assert_eq!(repeated_recharge_products[0].bonus_points, 0);
assert_eq!(repeated_recharge_products[0].badge_label, "");
assert_eq!(repeated_recharge_products[0].description, "60泥点");
assert_eq!(repeated_recharge_products[5].bonus_points, 0);
assert_eq!(repeated_recharge_products[5].badge_label, "");
assert_eq!(repeated_recharge_products[5].description, "3280泥点");
assert_eq!(repeated_recharge_products[0].bonus_points, 60);
assert_eq!(repeated_recharge_products[0].badge_label, "首充双倍");
assert_eq!(repeated_recharge_products[0].description, "首充送60泥点");
assert_eq!(repeated_recharge_products[5].bonus_points, 3280);
assert_eq!(repeated_recharge_products[5].badge_label, "首充双倍");
assert_eq!(repeated_recharge_products[5].description, "首充送3280泥点");
}
#[test]
fn build_recharge_order_input_rejects_unknown_product() {
let error = build_runtime_profile_recharge_order_create_input(
fn build_recharge_order_input_accepts_configured_product_id_later() {
let input = build_runtime_profile_recharge_order_create_input(
"user-1".to_string(),
"bad-product".to_string(),
"custom-points-600".to_string(),
"mock".to_string(),
1,
)
.expect_err("unknown product should fail");
.expect("product existence is validated against database config later");
assert_eq!(error, RuntimeProfileFieldError::UnknownRechargeProduct);
assert_eq!(input.product_id, "custom-points-600");
assert_eq!(input.payment_channel, "mock");
}
#[test]

View File

@@ -21,6 +21,12 @@ pub const PROFILE_TASK_STATUS_INCOMPLETE: &str = "incomplete";
pub const PROFILE_TASK_STATUS_CLAIMABLE: &str = "claimable";
pub const PROFILE_TASK_STATUS_CLAIMED: &str = "claimed";
pub const PROFILE_TASK_STATUS_DISABLED: &str = "disabled";
pub const PROFILE_RECHARGE_PRODUCT_KIND_POINTS: &str = "points";
pub const PROFILE_RECHARGE_PRODUCT_KIND_MEMBERSHIP: &str = "membership";
pub const PROFILE_MEMBERSHIP_TIER_NORMAL: &str = "normal";
pub const PROFILE_MEMBERSHIP_TIER_MONTH: &str = "month";
pub const PROFILE_MEMBERSHIP_TIER_SEASON: &str = "season";
pub const PROFILE_MEMBERSHIP_TIER_YEAR: &str = "year";
pub const PROFILE_FEEDBACK_STATUS_OPEN: &str = "open";
pub const TRACKING_SCOPE_KIND_SITE: &str = "site";
pub const TRACKING_SCOPE_KIND_WORK: &str = "work";
@@ -436,6 +442,33 @@ pub struct ProfileTaskConfigAdminListResponse {
pub entries: Vec<ProfileTaskConfigAdminResponse>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ProfileRechargeProductConfigAdminResponse {
pub product_id: String,
pub title: String,
pub price_cents: u64,
pub kind: String,
pub points_amount: u64,
pub bonus_points: u64,
pub duration_days: u32,
pub badge_label: String,
pub description: String,
pub tier: String,
pub enabled: bool,
pub sort_order: i32,
pub created_by: String,
pub created_at: String,
pub updated_by: String,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ProfileRechargeProductConfigAdminListResponse {
pub entries: Vec<ProfileRechargeProductConfigAdminResponse>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct AnalyticsMetricQueryRequest {
@@ -478,6 +511,27 @@ pub struct AdminUpsertProfileTaskConfigRequest {
pub sort_order: Option<i32>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct AdminUpsertProfileRechargeProductRequest {
pub product_id: String,
pub title: String,
pub price_cents: u64,
pub kind: String,
pub points_amount: u64,
pub bonus_points: u64,
pub duration_days: u32,
#[serde(default)]
pub badge_label: Option<String>,
#[serde(default)]
pub description: Option<String>,
pub tier: String,
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default)]
pub sort_order: Option<i32>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct AdminDisableProfileTaskConfigRequest {

View File

@@ -165,7 +165,7 @@ use module_runtime::{
RuntimePlatformTheme as DomainRuntimePlatformTheme, RuntimeProfileDashboardRecord,
RuntimeProfileFeedbackSubmissionRecord, RuntimeProfileInviteCodeRecord,
RuntimeProfilePlayStatsRecord, RuntimeProfileRechargeCenterRecord,
RuntimeProfileRechargeOrderRecord,
RuntimeProfileRechargeOrderRecord, RuntimeProfileRechargeProductConfigRecord,
RuntimeProfileRedeemCodeMode as DomainRuntimeProfileRedeemCodeMode,
RuntimeProfileRedeemCodeRecord, RuntimeProfileRewardCodeRedeemRecord,
RuntimeProfileSaveArchiveRecord, RuntimeProfileTaskCenterRecord, RuntimeProfileTaskClaimRecord,
@@ -185,6 +185,9 @@ use module_runtime::{
build_runtime_profile_recharge_center_get_input, build_runtime_profile_recharge_center_record,
build_runtime_profile_recharge_order_create_input,
build_runtime_profile_recharge_order_get_input,
build_runtime_profile_recharge_product_admin_list_input,
build_runtime_profile_recharge_product_admin_upsert_input,
build_runtime_profile_recharge_product_config_record,
build_runtime_profile_redeem_code_admin_disable_input,
build_runtime_profile_redeem_code_admin_list_input,
build_runtime_profile_redeem_code_admin_upsert_input, build_runtime_profile_redeem_code_record,

View File

@@ -309,6 +309,39 @@ impl From<module_runtime::RuntimeProfileTaskConfigAdminDisableInput>
}
}
impl From<module_runtime::RuntimeProfileRechargeProductAdminListInput>
for RuntimeProfileRechargeProductAdminListInput
{
fn from(input: module_runtime::RuntimeProfileRechargeProductAdminListInput) -> Self {
Self {
admin_user_id: input.admin_user_id,
}
}
}
impl From<module_runtime::RuntimeProfileRechargeProductAdminUpsertInput>
for RuntimeProfileRechargeProductAdminUpsertInput
{
fn from(input: module_runtime::RuntimeProfileRechargeProductAdminUpsertInput) -> Self {
Self {
admin_user_id: input.admin_user_id,
product_id: input.product_id,
title: input.title,
price_cents: input.price_cents,
kind: map_runtime_profile_recharge_product_kind(input.kind),
points_amount: input.points_amount,
bonus_points: input.bonus_points,
duration_days: input.duration_days,
badge_label: input.badge_label,
description: input.description,
tier: map_runtime_profile_membership_tier(input.tier),
enabled: input.enabled,
sort_order: input.sort_order,
updated_at_micros: input.updated_at_micros,
}
}
}
impl From<module_runtime::RuntimeProfileRedeemCodeAdminUpsertInput>
for RuntimeProfileRedeemCodeAdminUpsertInput
{
@@ -1157,6 +1190,40 @@ pub(crate) fn map_runtime_profile_task_config_admin_procedure_result(
))
}
pub(crate) fn map_runtime_profile_recharge_product_admin_list_procedure_result(
result: RuntimeProfileRechargeProductAdminListProcedureResult,
) -> Result<Vec<RuntimeProfileRechargeProductConfigRecord>, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
Ok(result
.entries
.into_iter()
.map(|snapshot| {
build_runtime_profile_recharge_product_config_record(
map_runtime_profile_recharge_product_config_snapshot(snapshot),
)
})
.collect())
}
pub(crate) fn map_runtime_profile_recharge_product_admin_procedure_result(
result: RuntimeProfileRechargeProductAdminProcedureResult,
) -> Result<RuntimeProfileRechargeProductConfigRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let snapshot = result
.record
.ok_or_else(|| SpacetimeClientError::missing_snapshot("recharge product config 快照"))?;
Ok(build_runtime_profile_recharge_product_config_record(
map_runtime_profile_recharge_product_config_snapshot(snapshot),
))
}
pub(crate) fn map_runtime_profile_redeem_code_admin_procedure_result(
result: RuntimeProfileRedeemCodeAdminProcedureResult,
) -> Result<RuntimeProfileRedeemCodeRecord, SpacetimeClientError> {
@@ -2237,6 +2304,29 @@ pub(crate) fn map_runtime_profile_recharge_product_snapshot(
}
}
pub(crate) fn map_runtime_profile_recharge_product_config_snapshot(
snapshot: RuntimeProfileRechargeProductConfigSnapshot,
) -> module_runtime::RuntimeProfileRechargeProductConfigSnapshot {
module_runtime::RuntimeProfileRechargeProductConfigSnapshot {
product_id: snapshot.product_id,
title: snapshot.title,
price_cents: snapshot.price_cents,
kind: map_runtime_profile_recharge_product_kind_back(snapshot.kind),
points_amount: snapshot.points_amount,
bonus_points: snapshot.bonus_points,
duration_days: snapshot.duration_days,
badge_label: snapshot.badge_label,
description: snapshot.description,
tier: map_runtime_profile_membership_tier_back(snapshot.tier),
enabled: snapshot.enabled,
sort_order: snapshot.sort_order,
created_by: snapshot.created_by,
created_at_micros: snapshot.created_at_micros,
updated_by: snapshot.updated_by,
updated_at_micros: snapshot.updated_at_micros,
}
}
pub(crate) fn map_runtime_profile_membership_benefit_snapshot(
snapshot: RuntimeProfileMembershipBenefitSnapshot,
) -> module_runtime::RuntimeProfileMembershipBenefitSnapshot {
@@ -5037,6 +5127,19 @@ pub(crate) fn map_runtime_profile_redeem_code_mode_back(
}
}
pub(crate) fn map_runtime_profile_recharge_product_kind(
value: module_runtime::RuntimeProfileRechargeProductKind,
) -> crate::module_bindings::RuntimeProfileRechargeProductKind {
match value {
module_runtime::RuntimeProfileRechargeProductKind::Points => {
crate::module_bindings::RuntimeProfileRechargeProductKind::Points
}
module_runtime::RuntimeProfileRechargeProductKind::Membership => {
crate::module_bindings::RuntimeProfileRechargeProductKind::Membership
}
}
}
pub(crate) fn map_runtime_profile_recharge_product_kind_back(
value: crate::module_bindings::RuntimeProfileRechargeProductKind,
) -> module_runtime::RuntimeProfileRechargeProductKind {
@@ -5050,6 +5153,25 @@ pub(crate) fn map_runtime_profile_recharge_product_kind_back(
}
}
pub(crate) fn map_runtime_profile_membership_tier(
value: module_runtime::RuntimeProfileMembershipTier,
) -> crate::module_bindings::RuntimeProfileMembershipTier {
match value {
module_runtime::RuntimeProfileMembershipTier::Normal => {
crate::module_bindings::RuntimeProfileMembershipTier::Normal
}
module_runtime::RuntimeProfileMembershipTier::Month => {
crate::module_bindings::RuntimeProfileMembershipTier::Month
}
module_runtime::RuntimeProfileMembershipTier::Season => {
crate::module_bindings::RuntimeProfileMembershipTier::Season
}
module_runtime::RuntimeProfileMembershipTier::Year => {
crate::module_bindings::RuntimeProfileMembershipTier::Year
}
}
}
pub(crate) fn map_runtime_profile_membership_status_back(
value: crate::module_bindings::RuntimeProfileMembershipStatus,
) -> module_runtime::RuntimeProfileMembershipStatus {

View File

@@ -0,0 +1,61 @@
// 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_recharge_product_admin_list_input_type::RuntimeProfileRechargeProductAdminListInput;
use super::runtime_profile_recharge_product_admin_list_procedure_result_type::RuntimeProfileRechargeProductAdminListProcedureResult;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
struct AdminListProfileRechargeProductsArgs {
pub input: RuntimeProfileRechargeProductAdminListInput,
}
impl __sdk::InModule for AdminListProfileRechargeProductsArgs {
type Module = super::RemoteModule;
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the procedure `admin_list_profile_recharge_products`.
///
/// Implemented for [`super::RemoteProcedures`].
pub trait admin_list_profile_recharge_products {
fn admin_list_profile_recharge_products(
&self,
input: RuntimeProfileRechargeProductAdminListInput,
) {
self.admin_list_profile_recharge_products_then(input, |_, _| {});
}
fn admin_list_profile_recharge_products_then(
&self,
input: RuntimeProfileRechargeProductAdminListInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileRechargeProductAdminListProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
impl admin_list_profile_recharge_products for super::RemoteProcedures {
fn admin_list_profile_recharge_products_then(
&self,
input: RuntimeProfileRechargeProductAdminListInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileRechargeProductAdminListProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp.invoke_procedure_with_callback::<_, RuntimeProfileRechargeProductAdminListProcedureResult>(
"admin_list_profile_recharge_products",
AdminListProfileRechargeProductsArgs { input, },
__callback,
);
}
}

View File

@@ -0,0 +1,62 @@
// 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_recharge_product_admin_procedure_result_type::RuntimeProfileRechargeProductAdminProcedureResult;
use super::runtime_profile_recharge_product_admin_upsert_input_type::RuntimeProfileRechargeProductAdminUpsertInput;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
struct AdminUpsertProfileRechargeProductArgs {
pub input: RuntimeProfileRechargeProductAdminUpsertInput,
}
impl __sdk::InModule for AdminUpsertProfileRechargeProductArgs {
type Module = super::RemoteModule;
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the procedure `admin_upsert_profile_recharge_product`.
///
/// Implemented for [`super::RemoteProcedures`].
pub trait admin_upsert_profile_recharge_product {
fn admin_upsert_profile_recharge_product(
&self,
input: RuntimeProfileRechargeProductAdminUpsertInput,
) {
self.admin_upsert_profile_recharge_product_then(input, |_, _| {});
}
fn admin_upsert_profile_recharge_product_then(
&self,
input: RuntimeProfileRechargeProductAdminUpsertInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileRechargeProductAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
impl admin_upsert_profile_recharge_product for super::RemoteProcedures {
fn admin_upsert_profile_recharge_product_then(
&self,
input: RuntimeProfileRechargeProductAdminUpsertInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileRechargeProductAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, RuntimeProfileRechargeProductAdminProcedureResult>(
"admin_upsert_profile_recharge_product",
AdminUpsertProfileRechargeProductArgs { input },
__callback,
);
}
}

View File

@@ -11,9 +11,11 @@ pub mod acknowledge_quest_completion_reducer;
pub mod admin_disable_profile_redeem_code_procedure;
pub mod admin_disable_profile_task_config_procedure;
pub mod admin_list_profile_invite_codes_procedure;
pub mod admin_list_profile_recharge_products_procedure;
pub mod admin_list_profile_redeem_codes_procedure;
pub mod admin_list_profile_task_configs_procedure;
pub mod admin_upsert_profile_invite_code_procedure;
pub mod admin_upsert_profile_recharge_product_procedure;
pub mod admin_upsert_profile_redeem_code_procedure;
pub mod admin_upsert_profile_task_config_procedure;
pub mod advance_puzzle_next_level_procedure;
@@ -461,6 +463,8 @@ pub mod profile_played_world_table;
pub mod profile_played_world_type;
pub mod profile_recharge_order_table;
pub mod profile_recharge_order_type;
pub mod profile_recharge_product_config_table;
pub mod profile_recharge_product_config_type;
pub mod profile_redeem_code_table;
pub mod profile_redeem_code_type;
pub mod profile_redeem_code_usage_table;
@@ -652,6 +656,11 @@ pub mod runtime_profile_recharge_order_get_input_type;
pub mod runtime_profile_recharge_order_paid_input_type;
pub mod runtime_profile_recharge_order_snapshot_type;
pub mod runtime_profile_recharge_order_status_type;
pub mod runtime_profile_recharge_product_admin_list_input_type;
pub mod runtime_profile_recharge_product_admin_list_procedure_result_type;
pub mod runtime_profile_recharge_product_admin_procedure_result_type;
pub mod runtime_profile_recharge_product_admin_upsert_input_type;
pub mod runtime_profile_recharge_product_config_snapshot_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;
@@ -857,9 +866,11 @@ 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_disable_profile_task_config_procedure::admin_disable_profile_task_config;
pub use admin_list_profile_invite_codes_procedure::admin_list_profile_invite_codes;
pub use admin_list_profile_recharge_products_procedure::admin_list_profile_recharge_products;
pub use admin_list_profile_redeem_codes_procedure::admin_list_profile_redeem_codes;
pub use admin_list_profile_task_configs_procedure::admin_list_profile_task_configs;
pub use admin_upsert_profile_invite_code_procedure::admin_upsert_profile_invite_code;
pub use admin_upsert_profile_recharge_product_procedure::admin_upsert_profile_recharge_product;
pub use admin_upsert_profile_redeem_code_procedure::admin_upsert_profile_redeem_code;
pub use admin_upsert_profile_task_config_procedure::admin_upsert_profile_task_config;
pub use advance_puzzle_next_level_procedure::advance_puzzle_next_level;
@@ -1307,6 +1318,8 @@ pub use profile_played_world_table::*;
pub use profile_played_world_type::ProfilePlayedWorld;
pub use profile_recharge_order_table::*;
pub use profile_recharge_order_type::ProfileRechargeOrder;
pub use profile_recharge_product_config_table::*;
pub use profile_recharge_product_config_type::ProfileRechargeProductConfig;
pub use profile_redeem_code_table::*;
pub use profile_redeem_code_type::ProfileRedeemCode;
pub use profile_redeem_code_usage_table::*;
@@ -1498,6 +1511,11 @@ pub use runtime_profile_recharge_order_get_input_type::RuntimeProfileRechargeOrd
pub use runtime_profile_recharge_order_paid_input_type::RuntimeProfileRechargeOrderPaidInput;
pub use runtime_profile_recharge_order_snapshot_type::RuntimeProfileRechargeOrderSnapshot;
pub use runtime_profile_recharge_order_status_type::RuntimeProfileRechargeOrderStatus;
pub use runtime_profile_recharge_product_admin_list_input_type::RuntimeProfileRechargeProductAdminListInput;
pub use runtime_profile_recharge_product_admin_list_procedure_result_type::RuntimeProfileRechargeProductAdminListProcedureResult;
pub use runtime_profile_recharge_product_admin_procedure_result_type::RuntimeProfileRechargeProductAdminProcedureResult;
pub use runtime_profile_recharge_product_admin_upsert_input_type::RuntimeProfileRechargeProductAdminUpsertInput;
pub use runtime_profile_recharge_product_config_snapshot_type::RuntimeProfileRechargeProductConfigSnapshot;
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;
@@ -2020,6 +2038,7 @@ pub struct DbUpdate {
profile_membership: __sdk::TableUpdate<ProfileMembership>,
profile_played_world: __sdk::TableUpdate<ProfilePlayedWorld>,
profile_recharge_order: __sdk::TableUpdate<ProfileRechargeOrder>,
profile_recharge_product_config: __sdk::TableUpdate<ProfileRechargeProductConfig>,
profile_redeem_code: __sdk::TableUpdate<ProfileRedeemCode>,
profile_redeem_code_usage: __sdk::TableUpdate<ProfileRedeemCodeUsage>,
profile_referral_relation: __sdk::TableUpdate<ProfileReferralRelation>,
@@ -2224,6 +2243,11 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate {
"profile_recharge_order" => db_update.profile_recharge_order.append(
profile_recharge_order_table::parse_table_update(table_update)?,
),
"profile_recharge_product_config" => {
db_update.profile_recharge_product_config.append(
profile_recharge_product_config_table::parse_table_update(table_update)?,
)
}
"profile_redeem_code" => db_update
.profile_redeem_code
.append(profile_redeem_code_table::parse_table_update(table_update)?),
@@ -2627,6 +2651,12 @@ impl __sdk::DbUpdate for DbUpdate {
&self.profile_recharge_order,
)
.with_updates_by_pk(|row| &row.order_id);
diff.profile_recharge_product_config = cache
.apply_diff_to_table::<ProfileRechargeProductConfig>(
"profile_recharge_product_config",
&self.profile_recharge_product_config,
)
.with_updates_by_pk(|row| &row.product_id);
diff.profile_redeem_code = cache
.apply_diff_to_table::<ProfileRedeemCode>(
"profile_redeem_code",
@@ -2969,6 +2999,9 @@ impl __sdk::DbUpdate for DbUpdate {
"profile_recharge_order" => db_update
.profile_recharge_order
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"profile_recharge_product_config" => db_update
.profile_recharge_product_config
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"profile_redeem_code" => db_update
.profile_redeem_code
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
@@ -3246,6 +3279,9 @@ impl __sdk::DbUpdate for DbUpdate {
"profile_recharge_order" => db_update
.profile_recharge_order
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"profile_recharge_product_config" => db_update
.profile_recharge_product_config
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"profile_redeem_code" => db_update
.profile_redeem_code
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
@@ -3427,6 +3463,7 @@ pub struct AppliedDiff<'r> {
profile_membership: __sdk::TableAppliedDiff<'r, ProfileMembership>,
profile_played_world: __sdk::TableAppliedDiff<'r, ProfilePlayedWorld>,
profile_recharge_order: __sdk::TableAppliedDiff<'r, ProfileRechargeOrder>,
profile_recharge_product_config: __sdk::TableAppliedDiff<'r, ProfileRechargeProductConfig>,
profile_redeem_code: __sdk::TableAppliedDiff<'r, ProfileRedeemCode>,
profile_redeem_code_usage: __sdk::TableAppliedDiff<'r, ProfileRedeemCodeUsage>,
profile_referral_relation: __sdk::TableAppliedDiff<'r, ProfileReferralRelation>,
@@ -3717,6 +3754,11 @@ impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> {
&self.profile_recharge_order,
event,
);
callbacks.invoke_table_row_callbacks::<ProfileRechargeProductConfig>(
"profile_recharge_product_config",
&self.profile_recharge_product_config,
event,
);
callbacks.invoke_table_row_callbacks::<ProfileRedeemCode>(
"profile_redeem_code",
&self.profile_redeem_code,
@@ -4609,6 +4651,7 @@ impl __sdk::SpacetimeModule for RemoteModule {
profile_membership_table::register_table(client_cache);
profile_played_world_table::register_table(client_cache);
profile_recharge_order_table::register_table(client_cache);
profile_recharge_product_config_table::register_table(client_cache);
profile_redeem_code_table::register_table(client_cache);
profile_redeem_code_usage_table::register_table(client_cache);
profile_referral_relation_table::register_table(client_cache);
@@ -4699,6 +4742,7 @@ impl __sdk::SpacetimeModule for RemoteModule {
"profile_membership",
"profile_played_world",
"profile_recharge_order",
"profile_recharge_product_config",
"profile_redeem_code",
"profile_redeem_code_usage",
"profile_referral_relation",

View File

@@ -0,0 +1,171 @@
// 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 super::profile_recharge_product_config_type::ProfileRechargeProductConfig;
use super::runtime_profile_membership_tier_type::RuntimeProfileMembershipTier;
use super::runtime_profile_recharge_product_kind_type::RuntimeProfileRechargeProductKind;
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
/// Table handle for the table `profile_recharge_product_config`.
///
/// Obtain a handle from the [`ProfileRechargeProductConfigTableAccess::profile_recharge_product_config`] method on [`super::RemoteTables`],
/// like `ctx.db.profile_recharge_product_config()`.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.profile_recharge_product_config().on_insert(...)`.
pub struct ProfileRechargeProductConfigTableHandle<'ctx> {
imp: __sdk::TableHandle<ProfileRechargeProductConfig>,
ctx: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the table `profile_recharge_product_config`.
///
/// Implemented for [`super::RemoteTables`].
pub trait ProfileRechargeProductConfigTableAccess {
#[allow(non_snake_case)]
/// Obtain a [`ProfileRechargeProductConfigTableHandle`], which mediates access to the table `profile_recharge_product_config`.
fn profile_recharge_product_config(&self) -> ProfileRechargeProductConfigTableHandle<'_>;
}
impl ProfileRechargeProductConfigTableAccess for super::RemoteTables {
fn profile_recharge_product_config(&self) -> ProfileRechargeProductConfigTableHandle<'_> {
ProfileRechargeProductConfigTableHandle {
imp: self
.imp
.get_table::<ProfileRechargeProductConfig>("profile_recharge_product_config"),
ctx: std::marker::PhantomData,
}
}
}
pub struct ProfileRechargeProductConfigInsertCallbackId(__sdk::CallbackId);
pub struct ProfileRechargeProductConfigDeleteCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::Table for ProfileRechargeProductConfigTableHandle<'ctx> {
type Row = ProfileRechargeProductConfig;
type EventContext = super::EventContext;
fn count(&self) -> u64 {
self.imp.count()
}
fn iter(&self) -> impl Iterator<Item = ProfileRechargeProductConfig> + '_ {
self.imp.iter()
}
type InsertCallbackId = ProfileRechargeProductConfigInsertCallbackId;
fn on_insert(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> ProfileRechargeProductConfigInsertCallbackId {
ProfileRechargeProductConfigInsertCallbackId(self.imp.on_insert(Box::new(callback)))
}
fn remove_on_insert(&self, callback: ProfileRechargeProductConfigInsertCallbackId) {
self.imp.remove_on_insert(callback.0)
}
type DeleteCallbackId = ProfileRechargeProductConfigDeleteCallbackId;
fn on_delete(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> ProfileRechargeProductConfigDeleteCallbackId {
ProfileRechargeProductConfigDeleteCallbackId(self.imp.on_delete(Box::new(callback)))
}
fn remove_on_delete(&self, callback: ProfileRechargeProductConfigDeleteCallbackId) {
self.imp.remove_on_delete(callback.0)
}
}
pub struct ProfileRechargeProductConfigUpdateCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::TableWithPrimaryKey for ProfileRechargeProductConfigTableHandle<'ctx> {
type UpdateCallbackId = ProfileRechargeProductConfigUpdateCallbackId;
fn on_update(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static,
) -> ProfileRechargeProductConfigUpdateCallbackId {
ProfileRechargeProductConfigUpdateCallbackId(self.imp.on_update(Box::new(callback)))
}
fn remove_on_update(&self, callback: ProfileRechargeProductConfigUpdateCallbackId) {
self.imp.remove_on_update(callback.0)
}
}
/// Access to the `product_id` unique index on the table `profile_recharge_product_config`,
/// which allows point queries on the field of the same name
/// via the [`ProfileRechargeProductConfigProductIdUnique::find`] method.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.profile_recharge_product_config().product_id().find(...)`.
pub struct ProfileRechargeProductConfigProductIdUnique<'ctx> {
imp: __sdk::UniqueConstraintHandle<ProfileRechargeProductConfig, String>,
phantom: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
impl<'ctx> ProfileRechargeProductConfigTableHandle<'ctx> {
/// Get a handle on the `product_id` unique index on the table `profile_recharge_product_config`.
pub fn product_id(&self) -> ProfileRechargeProductConfigProductIdUnique<'ctx> {
ProfileRechargeProductConfigProductIdUnique {
imp: self.imp.get_unique_constraint::<String>("product_id"),
phantom: std::marker::PhantomData,
}
}
}
impl<'ctx> ProfileRechargeProductConfigProductIdUnique<'ctx> {
/// Find the subscribed row whose `product_id` column value is equal to `col_val`,
/// if such a row is present in the client cache.
pub fn find(&self, col_val: &String) -> Option<ProfileRechargeProductConfig> {
self.imp.find(col_val)
}
}
#[doc(hidden)]
pub(super) fn register_table(client_cache: &mut __sdk::ClientCache<super::RemoteModule>) {
let _table = client_cache
.get_or_make_table::<ProfileRechargeProductConfig>("profile_recharge_product_config");
_table.add_unique_constraint::<String>("product_id", |row| &row.product_id);
}
#[doc(hidden)]
pub(super) fn parse_table_update(
raw_updates: __ws::v2::TableUpdate,
) -> __sdk::Result<__sdk::TableUpdate<ProfileRechargeProductConfig>> {
__sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| {
__sdk::InternalError::failed_parse(
"TableUpdate<ProfileRechargeProductConfig>",
"TableUpdate",
)
.with_cause(e)
.into()
})
}
#[allow(non_camel_case_types)]
/// Extension trait for query builder access to the table `ProfileRechargeProductConfig`.
///
/// Implemented for [`__sdk::QueryTableAccessor`].
pub trait profile_recharge_product_configQueryTableAccess {
#[allow(non_snake_case)]
/// Get a query builder for the table `ProfileRechargeProductConfig`.
fn profile_recharge_product_config(
&self,
) -> __sdk::__query_builder::Table<ProfileRechargeProductConfig>;
}
impl profile_recharge_product_configQueryTableAccess for __sdk::QueryTableAccessor {
fn profile_recharge_product_config(
&self,
) -> __sdk::__query_builder::Table<ProfileRechargeProductConfig> {
__sdk::__query_builder::Table::new("profile_recharge_product_config")
}
}

View File

@@ -0,0 +1,101 @@
// 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_membership_tier_type::RuntimeProfileMembershipTier;
use super::runtime_profile_recharge_product_kind_type::RuntimeProfileRechargeProductKind;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct ProfileRechargeProductConfig {
pub product_id: String,
pub title: String,
pub price_cents: u64,
pub kind: RuntimeProfileRechargeProductKind,
pub points_amount: u64,
pub bonus_points: u64,
pub duration_days: u32,
pub badge_label: String,
pub description: String,
pub tier: RuntimeProfileMembershipTier,
pub enabled: bool,
pub sort_order: i32,
pub created_by: String,
pub created_at: __sdk::Timestamp,
pub updated_by: String,
pub updated_at: __sdk::Timestamp,
}
impl __sdk::InModule for ProfileRechargeProductConfig {
type Module = super::RemoteModule;
}
/// Column accessor struct for the table `ProfileRechargeProductConfig`.
///
/// Provides typed access to columns for query building.
pub struct ProfileRechargeProductConfigCols {
pub product_id: __sdk::__query_builder::Col<ProfileRechargeProductConfig, String>,
pub title: __sdk::__query_builder::Col<ProfileRechargeProductConfig, String>,
pub price_cents: __sdk::__query_builder::Col<ProfileRechargeProductConfig, u64>,
pub kind: __sdk::__query_builder::Col<
ProfileRechargeProductConfig,
RuntimeProfileRechargeProductKind,
>,
pub points_amount: __sdk::__query_builder::Col<ProfileRechargeProductConfig, u64>,
pub bonus_points: __sdk::__query_builder::Col<ProfileRechargeProductConfig, u64>,
pub duration_days: __sdk::__query_builder::Col<ProfileRechargeProductConfig, u32>,
pub badge_label: __sdk::__query_builder::Col<ProfileRechargeProductConfig, String>,
pub description: __sdk::__query_builder::Col<ProfileRechargeProductConfig, String>,
pub tier:
__sdk::__query_builder::Col<ProfileRechargeProductConfig, RuntimeProfileMembershipTier>,
pub enabled: __sdk::__query_builder::Col<ProfileRechargeProductConfig, bool>,
pub sort_order: __sdk::__query_builder::Col<ProfileRechargeProductConfig, i32>,
pub created_by: __sdk::__query_builder::Col<ProfileRechargeProductConfig, String>,
pub created_at: __sdk::__query_builder::Col<ProfileRechargeProductConfig, __sdk::Timestamp>,
pub updated_by: __sdk::__query_builder::Col<ProfileRechargeProductConfig, String>,
pub updated_at: __sdk::__query_builder::Col<ProfileRechargeProductConfig, __sdk::Timestamp>,
}
impl __sdk::__query_builder::HasCols for ProfileRechargeProductConfig {
type Cols = ProfileRechargeProductConfigCols;
fn cols(table_name: &'static str) -> Self::Cols {
ProfileRechargeProductConfigCols {
product_id: __sdk::__query_builder::Col::new(table_name, "product_id"),
title: __sdk::__query_builder::Col::new(table_name, "title"),
price_cents: __sdk::__query_builder::Col::new(table_name, "price_cents"),
kind: __sdk::__query_builder::Col::new(table_name, "kind"),
points_amount: __sdk::__query_builder::Col::new(table_name, "points_amount"),
bonus_points: __sdk::__query_builder::Col::new(table_name, "bonus_points"),
duration_days: __sdk::__query_builder::Col::new(table_name, "duration_days"),
badge_label: __sdk::__query_builder::Col::new(table_name, "badge_label"),
description: __sdk::__query_builder::Col::new(table_name, "description"),
tier: __sdk::__query_builder::Col::new(table_name, "tier"),
enabled: __sdk::__query_builder::Col::new(table_name, "enabled"),
sort_order: __sdk::__query_builder::Col::new(table_name, "sort_order"),
created_by: __sdk::__query_builder::Col::new(table_name, "created_by"),
created_at: __sdk::__query_builder::Col::new(table_name, "created_at"),
updated_by: __sdk::__query_builder::Col::new(table_name, "updated_by"),
updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"),
}
}
}
/// Indexed column accessor struct for the table `ProfileRechargeProductConfig`.
///
/// Provides typed access to indexed columns for query building.
pub struct ProfileRechargeProductConfigIxCols {
pub product_id: __sdk::__query_builder::IxCol<ProfileRechargeProductConfig, String>,
}
impl __sdk::__query_builder::HasIxCols for ProfileRechargeProductConfig {
type IxCols = ProfileRechargeProductConfigIxCols;
fn ix_cols(table_name: &'static str) -> Self::IxCols {
ProfileRechargeProductConfigIxCols {
product_id: __sdk::__query_builder::IxCol::new(table_name, "product_id"),
}
}
}
impl __sdk::__query_builder::CanBeLookupTable for ProfileRechargeProductConfig {}

View File

@@ -0,0 +1,15 @@
// 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 RuntimeProfileRechargeProductAdminListInput {
pub admin_user_id: String,
}
impl __sdk::InModule for RuntimeProfileRechargeProductAdminListInput {
type Module = super::RemoteModule;
}

View File

@@ -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_recharge_product_config_snapshot_type::RuntimeProfileRechargeProductConfigSnapshot;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct RuntimeProfileRechargeProductAdminListProcedureResult {
pub ok: bool,
pub entries: Vec<RuntimeProfileRechargeProductConfigSnapshot>,
pub error_message: Option<String>,
}
impl __sdk::InModule for RuntimeProfileRechargeProductAdminListProcedureResult {
type Module = super::RemoteModule;
}

View File

@@ -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_recharge_product_config_snapshot_type::RuntimeProfileRechargeProductConfigSnapshot;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct RuntimeProfileRechargeProductAdminProcedureResult {
pub ok: bool,
pub record: Option<RuntimeProfileRechargeProductConfigSnapshot>,
pub error_message: Option<String>,
}
impl __sdk::InModule for RuntimeProfileRechargeProductAdminProcedureResult {
type Module = super::RemoteModule;
}

View File

@@ -0,0 +1,31 @@
// 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_membership_tier_type::RuntimeProfileMembershipTier;
use super::runtime_profile_recharge_product_kind_type::RuntimeProfileRechargeProductKind;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct RuntimeProfileRechargeProductAdminUpsertInput {
pub admin_user_id: String,
pub product_id: String,
pub title: String,
pub price_cents: u64,
pub kind: RuntimeProfileRechargeProductKind,
pub points_amount: u64,
pub bonus_points: u64,
pub duration_days: u32,
pub badge_label: String,
pub description: String,
pub tier: RuntimeProfileMembershipTier,
pub enabled: bool,
pub sort_order: i32,
pub updated_at_micros: i64,
}
impl __sdk::InModule for RuntimeProfileRechargeProductAdminUpsertInput {
type Module = super::RemoteModule;
}

View File

@@ -0,0 +1,33 @@
// 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_membership_tier_type::RuntimeProfileMembershipTier;
use super::runtime_profile_recharge_product_kind_type::RuntimeProfileRechargeProductKind;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct RuntimeProfileRechargeProductConfigSnapshot {
pub product_id: String,
pub title: String,
pub price_cents: u64,
pub kind: RuntimeProfileRechargeProductKind,
pub points_amount: u64,
pub bonus_points: u64,
pub duration_days: u32,
pub badge_label: String,
pub description: String,
pub tier: RuntimeProfileMembershipTier,
pub enabled: bool,
pub sort_order: i32,
pub created_by: String,
pub created_at_micros: i64,
pub updated_by: String,
pub updated_at_micros: i64,
}
impl __sdk::InModule for RuntimeProfileRechargeProductConfigSnapshot {
type Module = super::RemoteModule;
}

View File

@@ -657,6 +657,78 @@ impl SpacetimeClient {
.await
}
pub async fn admin_list_profile_recharge_products(
&self,
admin_user_id: String,
) -> Result<Vec<RuntimeProfileRechargeProductConfigRecord>, SpacetimeClientError> {
let procedure_input =
build_runtime_profile_recharge_product_admin_list_input(admin_user_id)
.map_err(SpacetimeClientError::validation_failed)?
.into();
self.call_after_connect(move |connection, sender| {
connection
.procedures()
.admin_list_profile_recharge_products_then(procedure_input, move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_runtime_profile_recharge_product_admin_list_procedure_result);
send_once(&sender, mapped);
});
})
.await
}
#[allow(clippy::too_many_arguments)]
pub async fn admin_upsert_profile_recharge_product(
&self,
admin_user_id: String,
product_id: String,
title: String,
price_cents: u64,
kind: module_runtime::RuntimeProfileRechargeProductKind,
points_amount: u64,
bonus_points: u64,
duration_days: u32,
badge_label: String,
description: String,
tier: module_runtime::RuntimeProfileMembershipTier,
enabled: bool,
sort_order: i32,
updated_at_micros: i64,
) -> Result<RuntimeProfileRechargeProductConfigRecord, SpacetimeClientError> {
let procedure_input = build_runtime_profile_recharge_product_admin_upsert_input(
admin_user_id,
product_id,
title,
price_cents,
kind,
points_amount,
bonus_points,
duration_days,
badge_label,
description,
tier,
enabled,
sort_order,
updated_at_micros,
)
.map_err(SpacetimeClientError::validation_failed)?
.into();
self.call_after_connect(move |connection, sender| {
connection
.procedures()
.admin_upsert_profile_recharge_product_then(procedure_input, move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_runtime_profile_recharge_product_admin_procedure_result);
send_once(&sender, mapped);
});
})
.await
}
pub async fn admin_upsert_profile_redeem_code(
&self,
admin_user_id: String,

View File

@@ -193,6 +193,7 @@ macro_rules! migration_tables {
public_work_play_daily_stat,
public_work_like,
profile_membership,
profile_recharge_product_config,
profile_recharge_order,
profile_feedback_submission,
profile_save_archive,

View File

@@ -6,6 +6,7 @@ const PUBLIC_WORK_RECENT_PLAY_WINDOW_DAYS: i64 = 7;
const PROFILE_REFERRAL_INVITED_USERS_LIMIT: usize = 20;
const PROFILE_NEW_USER_REGISTRATION_LEDGER_PREFIX: &str = "new-user-registration";
const PROFILE_TASK_SYSTEM_USER_ID: &str = "system:profile-task";
const PROFILE_RECHARGE_PRODUCT_SYSTEM_USER_ID: &str = "system:recharge-product";
const PROFILE_TASK_LOGIN_EVENT_ID_PREFIX: &str = "daily-login";
const PROFILE_TRACKING_PROFILE_MODULE_KEY: &str = "profile";
@@ -328,6 +329,28 @@ pub struct ProfileMembership {
pub(crate) updated_at: Timestamp,
}
#[spacetimedb::table(accessor = profile_recharge_product_config)]
#[derive(Clone)]
pub struct ProfileRechargeProductConfig {
#[primary_key]
pub(crate) product_id: String,
pub(crate) title: String,
pub(crate) price_cents: u64,
pub(crate) kind: RuntimeProfileRechargeProductKind,
pub(crate) points_amount: u64,
pub(crate) bonus_points: u64,
pub(crate) duration_days: u32,
pub(crate) badge_label: String,
pub(crate) description: String,
pub(crate) tier: RuntimeProfileMembershipTier,
pub(crate) enabled: bool,
pub(crate) sort_order: i32,
pub(crate) created_by: String,
pub(crate) created_at: Timestamp,
pub(crate) updated_by: String,
pub(crate) updated_at: Timestamp,
}
#[spacetimedb::table(
accessor = profile_recharge_order,
index(accessor = by_profile_recharge_order_user_id, btree(columns = [user_id])),
@@ -655,6 +678,44 @@ pub fn admin_disable_profile_task_config(
}
}
#[spacetimedb::procedure]
pub fn admin_list_profile_recharge_products(
ctx: &mut ProcedureContext,
input: RuntimeProfileRechargeProductAdminListInput,
) -> RuntimeProfileRechargeProductAdminListProcedureResult {
match ctx.try_with_tx(|tx| list_profile_recharge_product_config_snapshots(tx, input.clone())) {
Ok(entries) => RuntimeProfileRechargeProductAdminListProcedureResult {
ok: true,
entries,
error_message: None,
},
Err(message) => RuntimeProfileRechargeProductAdminListProcedureResult {
ok: false,
entries: Vec::new(),
error_message: Some(message),
},
}
}
#[spacetimedb::procedure]
pub fn admin_upsert_profile_recharge_product(
ctx: &mut ProcedureContext,
input: RuntimeProfileRechargeProductAdminUpsertInput,
) -> RuntimeProfileRechargeProductAdminProcedureResult {
match ctx.try_with_tx(|tx| upsert_profile_recharge_product_config_record(tx, input.clone())) {
Ok(record) => RuntimeProfileRechargeProductAdminProcedureResult {
ok: true,
record: Some(record),
error_message: None,
},
Err(message) => RuntimeProfileRechargeProductAdminProcedureResult {
ok: false,
record: None,
error_message: Some(message),
},
}
}
// 新用户注册赠送由后端注册链路调用;流水 ID 固定,保证重试不重复发放。
#[spacetimedb::procedure]
pub fn grant_new_user_registration_wallet_reward(
@@ -1454,6 +1515,22 @@ fn build_public_work_like_id(source_type: &str, profile_id: &str, user_id: &str)
mod tests {
use super::*;
#[test]
fn point_recharge_display_is_resolved_per_product() {
let products = runtime_profile_recharge_point_products();
let repeated = resolve_profile_recharge_product_display(products[0].clone(), true);
let untouched = resolve_profile_recharge_product_display(products[1].clone(), false);
assert_eq!(repeated.product_id, "points_60");
assert_eq!(repeated.bonus_points, 0);
assert_eq!(repeated.badge_label, "");
assert_eq!(repeated.description, "60泥点");
assert_eq!(untouched.product_id, "points_180");
assert_eq!(untouched.bonus_points, 180);
assert_eq!(untouched.badge_label, "首充双倍");
assert_eq!(untouched.description, "首充送180泥点");
}
#[test]
fn duplicate_tracking_event_ids_are_treated_as_idempotent_replays() {
assert!(should_skip_existing_tracking_event_id(true));
@@ -2091,8 +2168,8 @@ fn create_profile_recharge_order_record(
input.created_at_micros,
)
.map_err(|error| error.to_string())?;
let product = runtime_profile_recharge_product_by_id(&validated_input.product_id)
.ok_or_else(|| "recharge.product_id 不存在".to_string())?;
let product = enabled_profile_recharge_product_by_id(ctx, &validated_input.product_id)
.ok_or_else(|| "recharge.product_id 不存在或已下架".to_string())?;
let created_at = Timestamp::from_micros_since_unix_epoch(validated_input.created_at_micros);
let should_settle_immediately =
validated_input.payment_channel == PROFILE_RECHARGE_PAYMENT_CHANNEL_MOCK;
@@ -2201,7 +2278,7 @@ fn mark_profile_recharge_order_paid_record(
return Err("profile_recharge_order 当前状态不能确认支付".to_string());
}
let product = runtime_profile_recharge_product_by_id(&order.product_id)
let product = profile_recharge_product_by_id(ctx, &order.product_id)
.ok_or_else(|| "recharge.product_id 不存在".to_string())?;
let paid_at = Timestamp::from_micros_since_unix_epoch(validated_input.paid_at_micros);
let (points_delta, membership_expires_at) = apply_profile_recharge_purchase(
@@ -2238,7 +2315,7 @@ fn apply_profile_recharge_purchase(
) -> Result<(i64, Option<Timestamp>), String> {
match product.kind {
RuntimeProfileRechargeProductKind::Points => {
let has_recharged = has_profile_points_recharged(ctx, user_id);
let has_recharged = has_profile_product_recharged(ctx, user_id, &product.product_id);
let points_delta =
resolve_runtime_profile_points_recharge_delta(product, has_recharged);
apply_profile_wallet_delta(
@@ -2907,6 +2984,7 @@ fn build_profile_recharge_center_snapshot(
ctx: &ReducerContext,
user_id: &str,
) -> RuntimeProfileRechargeCenterSnapshot {
ensure_default_profile_recharge_product_config(ctx);
let wallet_balance = ctx
.db
.profile_dashboard_state()
@@ -2916,13 +2994,31 @@ fn build_profile_recharge_center_snapshot(
.unwrap_or(0);
let has_points_recharged = has_profile_points_recharged(ctx, user_id);
let mut point_products = Vec::new();
let mut membership_products = Vec::new();
for row in profile_recharge_product_config_rows(ctx, false) {
let product = build_profile_recharge_product_snapshot_from_config_row(&row);
match product.kind {
RuntimeProfileRechargeProductKind::Points => {
let has_product_recharged =
has_profile_product_recharged(ctx, user_id, &product.product_id);
point_products.push(resolve_profile_recharge_product_display(
product,
has_product_recharged,
));
}
RuntimeProfileRechargeProductKind::Membership => {
membership_products.push(product);
}
}
}
RuntimeProfileRechargeCenterSnapshot {
user_id: user_id.to_string(),
wallet_balance,
membership: build_profile_membership_snapshot(ctx, user_id),
point_products: resolve_runtime_profile_recharge_point_products(has_points_recharged),
membership_products: runtime_profile_recharge_membership_products(),
point_products,
membership_products,
benefits: runtime_profile_membership_benefits(),
latest_order: latest_profile_recharge_order(ctx, user_id)
.map(|row| build_profile_recharge_order_snapshot_from_row(&row)),
@@ -3052,6 +3148,21 @@ fn list_profile_task_config_snapshots(
Ok(entries)
}
fn list_profile_recharge_product_config_snapshots(
ctx: &ReducerContext,
input: RuntimeProfileRechargeProductAdminListInput,
) -> Result<Vec<RuntimeProfileRechargeProductConfigSnapshot>, String> {
let _validated_input =
build_runtime_profile_recharge_product_admin_list_input(input.admin_user_id)
.map_err(|error| error.to_string())?;
ensure_default_profile_recharge_product_config(ctx);
Ok(profile_recharge_product_config_rows(ctx, true)
.iter()
.map(build_profile_recharge_product_config_snapshot_from_row)
.collect())
}
fn admin_list_profile_redeem_code_records(
ctx: &ReducerContext,
input: RuntimeProfileRedeemCodeAdminListInput,
@@ -3097,6 +3208,74 @@ fn admin_list_profile_invite_code_records(
Ok(entries)
}
fn upsert_profile_recharge_product_config_record(
ctx: &ReducerContext,
input: RuntimeProfileRechargeProductAdminUpsertInput,
) -> Result<RuntimeProfileRechargeProductConfigSnapshot, String> {
let validated_input = build_runtime_profile_recharge_product_admin_upsert_input(
input.admin_user_id,
input.product_id,
input.title,
input.price_cents,
input.kind,
input.points_amount,
input.bonus_points,
input.duration_days,
input.badge_label,
input.description,
input.tier,
input.enabled,
input.sort_order,
input.updated_at_micros,
)
.map_err(|error| error.to_string())?;
ensure_default_profile_recharge_product_config(ctx);
let updated_at = Timestamp::from_micros_since_unix_epoch(validated_input.updated_at_micros);
let existing = ctx
.db
.profile_recharge_product_config()
.product_id()
.find(&validated_input.product_id);
if let Some(row) = existing.as_ref() {
ctx.db
.profile_recharge_product_config()
.product_id()
.delete(&row.product_id);
}
let inserted = ctx
.db
.profile_recharge_product_config()
.insert(ProfileRechargeProductConfig {
product_id: validated_input.product_id,
title: validated_input.title,
price_cents: validated_input.price_cents,
kind: validated_input.kind,
points_amount: validated_input.points_amount,
bonus_points: validated_input.bonus_points,
duration_days: validated_input.duration_days,
badge_label: validated_input.badge_label,
description: validated_input.description,
tier: validated_input.tier,
enabled: validated_input.enabled,
sort_order: validated_input.sort_order,
created_by: existing
.as_ref()
.map(|row| row.created_by.clone())
.unwrap_or_else(|| validated_input.admin_user_id.clone()),
created_at: existing
.as_ref()
.map(|row| row.created_at)
.unwrap_or(updated_at),
updated_by: validated_input.admin_user_id,
updated_at,
});
Ok(build_profile_recharge_product_config_snapshot_from_row(
&inserted,
))
}
fn upsert_profile_task_config_record(
ctx: &ReducerContext,
input: RuntimeProfileTaskConfigAdminUpsertInput,
@@ -3518,6 +3697,96 @@ fn ensure_default_profile_task_config(ctx: &ReducerContext) -> ProfileTaskConfig
})
}
fn ensure_default_profile_recharge_product_config(ctx: &ReducerContext) {
if ctx.db.profile_recharge_product_config().count() > 0 {
return;
}
let now = ctx.timestamp;
for (sort_order, product) in runtime_profile_recharge_point_products()
.into_iter()
.chain(runtime_profile_recharge_membership_products())
.enumerate()
{
ctx.db
.profile_recharge_product_config()
.insert(ProfileRechargeProductConfig {
product_id: product.product_id,
title: product.title,
price_cents: product.price_cents,
kind: product.kind,
points_amount: product.points_amount,
bonus_points: product.bonus_points,
duration_days: product.duration_days,
badge_label: product.badge_label,
description: product.description,
tier: product.tier,
enabled: true,
sort_order: sort_order as i32,
created_by: PROFILE_RECHARGE_PRODUCT_SYSTEM_USER_ID.to_string(),
created_at: now,
updated_by: PROFILE_RECHARGE_PRODUCT_SYSTEM_USER_ID.to_string(),
updated_at: now,
});
}
}
fn profile_recharge_product_config_rows(
ctx: &ReducerContext,
include_disabled: bool,
) -> Vec<ProfileRechargeProductConfig> {
ensure_default_profile_recharge_product_config(ctx);
let mut rows = ctx
.db
.profile_recharge_product_config()
.iter()
.filter(|row| include_disabled || row.enabled)
.collect::<Vec<_>>();
rows.sort_by(|left, right| {
left.sort_order
.cmp(&right.sort_order)
.then_with(|| left.product_id.cmp(&right.product_id))
});
rows
}
fn profile_recharge_product_by_id(
ctx: &ReducerContext,
product_id: &str,
) -> Option<RuntimeProfileRechargeProductSnapshot> {
ensure_default_profile_recharge_product_config(ctx);
ctx.db
.profile_recharge_product_config()
.product_id()
.find(&product_id.to_string())
.map(|row| build_profile_recharge_product_snapshot_from_config_row(&row))
}
fn enabled_profile_recharge_product_by_id(
ctx: &ReducerContext,
product_id: &str,
) -> Option<RuntimeProfileRechargeProductSnapshot> {
ensure_default_profile_recharge_product_config(ctx);
ctx.db
.profile_recharge_product_config()
.product_id()
.find(&product_id.to_string())
.filter(|row| row.enabled)
.map(|row| build_profile_recharge_product_snapshot_from_config_row(&row))
}
fn resolve_profile_recharge_product_display(
mut product: RuntimeProfileRechargeProductSnapshot,
has_product_recharged: bool,
) -> RuntimeProfileRechargeProductSnapshot {
if product.kind == RuntimeProfileRechargeProductKind::Points && has_product_recharged {
product.bonus_points = 0;
product.badge_label.clear();
product.description = product.title.clone();
}
product
}
fn build_profile_membership_snapshot(
ctx: &ReducerContext,
user_id: &str,
@@ -3754,9 +4023,19 @@ fn apply_profile_wallet_signed_delta(
}
fn has_profile_points_recharged(ctx: &ReducerContext, user_id: &str) -> bool {
ctx.db.profile_wallet_ledger().iter().any(|row| {
ctx.db.profile_recharge_order().iter().any(|row| {
row.user_id == user_id
&& row.source_type == RuntimeProfileWalletLedgerSourceType::PointsRecharge
&& row.kind == RuntimeProfileRechargeProductKind::Points
&& row.status == RuntimeProfileRechargeOrderStatus::Paid
})
}
fn has_profile_product_recharged(ctx: &ReducerContext, user_id: &str, product_id: &str) -> bool {
ctx.db.profile_recharge_order().iter().any(|row| {
row.user_id == user_id
&& row.product_id == product_id
&& row.kind == RuntimeProfileRechargeProductKind::Points
&& row.status == RuntimeProfileRechargeOrderStatus::Paid
})
}
@@ -3894,6 +4173,46 @@ fn build_profile_task_config_snapshot_from_row(
}
}
fn build_profile_recharge_product_config_snapshot_from_row(
row: &ProfileRechargeProductConfig,
) -> RuntimeProfileRechargeProductConfigSnapshot {
RuntimeProfileRechargeProductConfigSnapshot {
product_id: row.product_id.clone(),
title: row.title.clone(),
price_cents: row.price_cents,
kind: row.kind,
points_amount: row.points_amount,
bonus_points: row.bonus_points,
duration_days: row.duration_days,
badge_label: row.badge_label.clone(),
description: row.description.clone(),
tier: row.tier,
enabled: row.enabled,
sort_order: row.sort_order,
created_by: row.created_by.clone(),
created_at_micros: row.created_at.to_micros_since_unix_epoch(),
updated_by: row.updated_by.clone(),
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
}
}
fn build_profile_recharge_product_snapshot_from_config_row(
row: &ProfileRechargeProductConfig,
) -> RuntimeProfileRechargeProductSnapshot {
RuntimeProfileRechargeProductSnapshot {
product_id: row.product_id.clone(),
title: row.title.clone(),
price_cents: row.price_cents,
kind: row.kind,
points_amount: row.points_amount,
bonus_points: row.bonus_points,
duration_days: row.duration_days,
badge_label: row.badge_label.clone(),
description: row.description.clone(),
tier: row.tier,
}
}
fn build_profile_recharge_order_snapshot_from_row(
row: &ProfileRechargeOrder,
) -> RuntimeProfileRechargeOrderSnapshot {