feat: 接入微信小程序支付
This commit is contained in:
@@ -1151,6 +1151,14 @@ fn normalize_migration_row(table_name: &str, value: &serde_json::Value) -> serde
|
||||
.or_insert_with(|| serde_json::Value::String("{}".to_string()));
|
||||
}
|
||||
}
|
||||
if table_name == "profile_recharge_order" {
|
||||
if let Some(object) = next_value.as_object_mut() {
|
||||
// 中文注释:真实微信支付接入后才有平台交易号,旧迁移包按未回填处理。
|
||||
object
|
||||
.entry("provider_transaction_id".to_string())
|
||||
.or_insert(serde_json::Value::Null);
|
||||
}
|
||||
}
|
||||
if table_name == "big_fish_creation_session" {
|
||||
if let Some(object) = next_value.as_object_mut() {
|
||||
// 中文注释:旧迁移包没有公开游玩次数字段,导入时按新建作品默认 0 兼容。
|
||||
|
||||
@@ -336,6 +336,7 @@ pub struct ProfileMembership {
|
||||
btree(columns = [user_id, created_at])
|
||||
)
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
pub struct ProfileRechargeOrder {
|
||||
#[primary_key]
|
||||
pub(crate) order_id: String,
|
||||
@@ -346,7 +347,10 @@ pub struct ProfileRechargeOrder {
|
||||
pub(crate) amount_cents: u64,
|
||||
pub(crate) status: RuntimeProfileRechargeOrderStatus,
|
||||
pub(crate) payment_channel: String,
|
||||
pub(crate) paid_at: Timestamp,
|
||||
#[default(None::<Timestamp>)]
|
||||
pub(crate) paid_at: Option<Timestamp>,
|
||||
#[default(None::<String>)]
|
||||
pub(crate) provider_transaction_id: Option<String>,
|
||||
pub(crate) created_at: Timestamp,
|
||||
pub(crate) points_delta: i64,
|
||||
pub(crate) membership_expires_at: Option<Timestamp>,
|
||||
@@ -767,7 +771,6 @@ pub fn get_profile_recharge_center(
|
||||
}
|
||||
}
|
||||
|
||||
// 当前阶段没有真实支付网关,下单后在服务端模拟支付成功并立即写入权益。
|
||||
#[spacetimedb::procedure]
|
||||
pub fn create_profile_recharge_order_and_return(
|
||||
ctx: &mut ProcedureContext,
|
||||
@@ -789,6 +792,27 @@ pub fn create_profile_recharge_order_and_return(
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn mark_profile_recharge_order_paid_and_return(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: RuntimeProfileRechargeOrderPaidInput,
|
||||
) -> RuntimeProfileRechargeCenterProcedureResult {
|
||||
match ctx.try_with_tx(|tx| mark_profile_recharge_order_paid_record(tx, input.clone())) {
|
||||
Ok((record, order)) => RuntimeProfileRechargeCenterProcedureResult {
|
||||
ok: true,
|
||||
record: Some(record),
|
||||
order: Some(order),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => RuntimeProfileRechargeCenterProcedureResult {
|
||||
ok: false,
|
||||
record: None,
|
||||
order: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn submit_profile_feedback_and_return(
|
||||
ctx: &mut ProcedureContext,
|
||||
@@ -2049,36 +2073,24 @@ fn create_profile_recharge_order_record(
|
||||
let product = runtime_profile_recharge_product_by_id(&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 (points_delta, membership_expires_at) = match product.kind {
|
||||
RuntimeProfileRechargeProductKind::Points => {
|
||||
let has_recharged = has_profile_points_recharged(ctx, &validated_input.user_id);
|
||||
let points_delta =
|
||||
resolve_runtime_profile_points_recharge_delta(&product, has_recharged);
|
||||
apply_profile_wallet_delta(
|
||||
ctx,
|
||||
&validated_input.user_id,
|
||||
points_delta,
|
||||
RuntimeProfileWalletLedgerSourceType::PointsRecharge,
|
||||
&build_runtime_profile_recharge_wallet_ledger_id(
|
||||
&validated_input.user_id,
|
||||
validated_input.created_at_micros,
|
||||
&product.product_id,
|
||||
),
|
||||
created_at,
|
||||
)?;
|
||||
(points_delta as i64, None)
|
||||
}
|
||||
RuntimeProfileRechargeProductKind::Membership => {
|
||||
let expires_at = apply_profile_membership_purchase(
|
||||
ctx,
|
||||
&validated_input.user_id,
|
||||
product.tier,
|
||||
product.duration_days,
|
||||
created_at,
|
||||
);
|
||||
(0, Some(expires_at))
|
||||
}
|
||||
let should_settle_immediately =
|
||||
validated_input.payment_channel == PROFILE_RECHARGE_PAYMENT_CHANNEL_MOCK;
|
||||
let (status, paid_at, points_delta, membership_expires_at) = if should_settle_immediately {
|
||||
let (points_delta, membership_expires_at) = apply_profile_recharge_purchase(
|
||||
ctx,
|
||||
&validated_input.user_id,
|
||||
&product,
|
||||
validated_input.created_at_micros,
|
||||
created_at,
|
||||
)?;
|
||||
(
|
||||
RuntimeProfileRechargeOrderStatus::Paid,
|
||||
Some(created_at),
|
||||
points_delta,
|
||||
membership_expires_at,
|
||||
)
|
||||
} else {
|
||||
(RuntimeProfileRechargeOrderStatus::Pending, None, 0, None)
|
||||
};
|
||||
|
||||
let order = ProfileRechargeOrder {
|
||||
@@ -2092,9 +2104,10 @@ fn create_profile_recharge_order_record(
|
||||
product_title: product.title.clone(),
|
||||
kind: product.kind,
|
||||
amount_cents: product.price_cents,
|
||||
status: RuntimeProfileRechargeOrderStatus::Paid,
|
||||
status,
|
||||
payment_channel: validated_input.payment_channel,
|
||||
paid_at: created_at,
|
||||
paid_at,
|
||||
provider_transaction_id: None,
|
||||
created_at,
|
||||
points_delta,
|
||||
membership_expires_at,
|
||||
@@ -2109,6 +2122,106 @@ fn create_profile_recharge_order_record(
|
||||
))
|
||||
}
|
||||
|
||||
fn mark_profile_recharge_order_paid_record(
|
||||
ctx: &ReducerContext,
|
||||
input: RuntimeProfileRechargeOrderPaidInput,
|
||||
) -> Result<
|
||||
(
|
||||
RuntimeProfileRechargeCenterSnapshot,
|
||||
RuntimeProfileRechargeOrderSnapshot,
|
||||
),
|
||||
String,
|
||||
> {
|
||||
let validated_input = build_runtime_profile_recharge_order_paid_input(
|
||||
input.order_id,
|
||||
input.paid_at_micros,
|
||||
input.provider_transaction_id,
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let mut order = ctx
|
||||
.db
|
||||
.profile_recharge_order()
|
||||
.order_id()
|
||||
.find(&validated_input.order_id)
|
||||
.ok_or_else(|| "profile_recharge_order 不存在".to_string())?;
|
||||
|
||||
if order.status == RuntimeProfileRechargeOrderStatus::Paid {
|
||||
return Ok((
|
||||
build_profile_recharge_center_snapshot(ctx, &order.user_id),
|
||||
build_profile_recharge_order_snapshot_from_row(&order),
|
||||
));
|
||||
}
|
||||
if order.status != RuntimeProfileRechargeOrderStatus::Pending {
|
||||
return Err("profile_recharge_order 当前状态不能确认支付".to_string());
|
||||
}
|
||||
|
||||
let product = runtime_profile_recharge_product_by_id(&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(
|
||||
ctx,
|
||||
&order.user_id,
|
||||
&product,
|
||||
order.created_at.to_micros_since_unix_epoch(),
|
||||
paid_at,
|
||||
)?;
|
||||
|
||||
ctx.db
|
||||
.profile_recharge_order()
|
||||
.order_id()
|
||||
.delete(&order.order_id);
|
||||
order.status = RuntimeProfileRechargeOrderStatus::Paid;
|
||||
order.paid_at = Some(paid_at);
|
||||
order.provider_transaction_id = validated_input.provider_transaction_id;
|
||||
order.points_delta = points_delta;
|
||||
order.membership_expires_at = membership_expires_at;
|
||||
ctx.db.profile_recharge_order().insert(order.clone());
|
||||
|
||||
Ok((
|
||||
build_profile_recharge_center_snapshot(ctx, &order.user_id),
|
||||
build_profile_recharge_order_snapshot_from_row(&order),
|
||||
))
|
||||
}
|
||||
|
||||
fn apply_profile_recharge_purchase(
|
||||
ctx: &ReducerContext,
|
||||
user_id: &str,
|
||||
product: &RuntimeProfileRechargeProductSnapshot,
|
||||
order_created_at_micros: i64,
|
||||
paid_at: Timestamp,
|
||||
) -> Result<(i64, Option<Timestamp>), String> {
|
||||
match product.kind {
|
||||
RuntimeProfileRechargeProductKind::Points => {
|
||||
let has_recharged = has_profile_points_recharged(ctx, user_id);
|
||||
let points_delta =
|
||||
resolve_runtime_profile_points_recharge_delta(product, has_recharged);
|
||||
apply_profile_wallet_delta(
|
||||
ctx,
|
||||
user_id,
|
||||
points_delta,
|
||||
RuntimeProfileWalletLedgerSourceType::PointsRecharge,
|
||||
&build_runtime_profile_recharge_wallet_ledger_id(
|
||||
user_id,
|
||||
order_created_at_micros,
|
||||
&product.product_id,
|
||||
),
|
||||
paid_at,
|
||||
)?;
|
||||
Ok((points_delta as i64, None))
|
||||
}
|
||||
RuntimeProfileRechargeProductKind::Membership => {
|
||||
let expires_at = apply_profile_membership_purchase(
|
||||
ctx,
|
||||
user_id,
|
||||
product.tier,
|
||||
product.duration_days,
|
||||
paid_at,
|
||||
);
|
||||
Ok((0, Some(expires_at)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn submit_profile_feedback_record(
|
||||
ctx: &ReducerContext,
|
||||
input: RuntimeProfileFeedbackSubmissionInput,
|
||||
@@ -3745,7 +3858,8 @@ fn build_profile_recharge_order_snapshot_from_row(
|
||||
amount_cents: row.amount_cents,
|
||||
status: row.status,
|
||||
payment_channel: row.payment_channel.clone(),
|
||||
paid_at_micros: row.paid_at.to_micros_since_unix_epoch(),
|
||||
paid_at_micros: row.paid_at.map(|value| value.to_micros_since_unix_epoch()),
|
||||
provider_transaction_id: row.provider_transaction_id.clone(),
|
||||
created_at_micros: row.created_at.to_micros_since_unix_epoch(),
|
||||
points_delta: row.points_delta,
|
||||
membership_expires_at_micros: row
|
||||
|
||||
Reference in New Issue
Block a user