fix wechat virtual payment coin flow

This commit is contained in:
kdletters
2026-05-30 16:42:25 +08:00
parent e941ac4539
commit aaaba77c3a
8 changed files with 496 additions and 13 deletions

View File

@@ -256,7 +256,7 @@ pub async fn create_profile_recharge_order(
.await
.map_err(|error| runtime_profile_error_response(&request_context, error))?;
Some(
build_wechat_virtual_pay_params(&state, &order, &openid)
build_wechat_virtual_pay_params(&state, &center, &order, &openid)
.map(WechatMiniProgramPaymentParamsResponse::Virtual)
.map_err(|error| runtime_profile_error_response(&request_context, error))?,
)
@@ -1169,9 +1169,28 @@ async fn resolve_wechat_identity_for_payment(
fn build_wechat_virtual_pay_params(
state: &AppState,
center: &RuntimeProfileRechargeCenterRecord,
order: &RuntimeProfileRechargeOrderRecord,
openid: &str,
) -> Result<WechatMiniProgramVirtualPayParamsResponse, AppError> {
let product = match order.kind {
RuntimeProfileRechargeProductKind::Points => center
.point_products
.iter()
.find(|item| item.product_id == order.product_id)
.ok_or_else(|| {
AppError::from_status(StatusCode::BAD_REQUEST)
.with_message("当前充值商品不存在,请刷新后再试")
})?,
RuntimeProfileRechargeProductKind::Membership => center
.membership_products
.iter()
.find(|item| item.product_id == order.product_id)
.ok_or_else(|| {
AppError::from_status(StatusCode::BAD_REQUEST)
.with_message("当前充值商品不存在,请刷新后再试")
})?,
};
let identity = state
.wechat_auth_service()
.get_identity_by_user_id(&order.user_id)
@@ -1198,9 +1217,13 @@ fn build_wechat_virtual_pay_params(
RuntimeProfileRechargeProductKind::Points => "short_series_coin",
RuntimeProfileRechargeProductKind::Membership => "short_series_goods",
};
let buy_quantity = match product.kind {
RuntimeProfileRechargeProductKind::Points => product.points_amount,
RuntimeProfileRechargeProductKind::Membership => 1,
};
let mut sign_data = serde_json::json!({
"offerId": offer_id,
"buyQuantity": 1,
"buyQuantity": buy_quantity,
"env": state.config.wechat_mini_program_virtual_payment_env,
"currencyType": "CNY",
"outTradeNo": order.order_id,
@@ -1772,8 +1795,11 @@ mod tests {
use module_auth::{ResolveWechatLoginInput, WechatIdentityProfile};
use module_runtime::{
PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_MINI_PROGRAM_VIRTUAL,
RuntimeProfileMembershipRecord, RuntimeProfileMembershipStatus,
RuntimeProfileMembershipTier, RuntimeProfileRechargeCenterRecord,
RuntimeProfileRechargeOrderRecord, RuntimeProfileRechargeOrderStatus,
RuntimeProfileRechargeProductKind, RuntimeProfileWalletLedgerSourceType,
RuntimeProfileRechargeProductKind, RuntimeProfileRechargeProductRecord,
RuntimeProfileWalletLedgerSourceType,
};
use super::{
@@ -2284,7 +2310,39 @@ mod tests {
membership_expires_at_micros: None,
};
let params = build_wechat_virtual_pay_params(&state, &order, "openid-user-00000001")
let center = RuntimeProfileRechargeCenterRecord {
user_id: user_id.clone(),
wallet_balance: 0,
membership: RuntimeProfileMembershipRecord {
user_id: user_id.clone(),
status: RuntimeProfileMembershipStatus::Normal,
tier: RuntimeProfileMembershipTier::Normal,
started_at: None,
started_at_micros: None,
expires_at: None,
expires_at_micros: None,
updated_at: None,
updated_at_micros: None,
},
point_products: vec![],
membership_products: vec![RuntimeProfileRechargeProductRecord {
product_id: "member_month".to_string(),
title: "月卡".to_string(),
price_cents: 2800,
kind: RuntimeProfileRechargeProductKind::Membership,
points_amount: 0,
bonus_points: 0,
duration_days: 30,
badge_label: String::new(),
description: "30天会员".to_string(),
tier: RuntimeProfileMembershipTier::Month,
}],
benefits: vec![],
latest_order: None,
has_points_recharged: false,
};
let params = build_wechat_virtual_pay_params(&state, &center, &order, "openid-user-00000001")
.expect("membership virtual pay params should build");
let sign_data: Value =
serde_json::from_str(&params.sign_data).expect("sign data should be valid json");
@@ -2296,6 +2354,7 @@ mod tests {
.expect("attach should decode");
assert_eq!(params.mode, "short_series_goods");
assert_eq!(sign_data["buyQuantity"], 1);
assert_eq!(sign_data["offerId"], "offer-1");
assert_eq!(sign_data["productId"], "member_month");
assert_eq!(sign_data["goodsPrice"], 2800);
@@ -2305,6 +2364,106 @@ mod tests {
assert!(!params.signature.is_empty());
}
#[tokio::test]
async fn wechat_virtual_pay_params_use_coin_quantity_for_points_products() {
let state = seed_authenticated_state_with_config(AppConfig {
wechat_mini_program_virtual_payment_offer_id: Some("offer-1".to_string()),
wechat_mini_program_virtual_payment_app_key: Some("app-key-1".to_string()),
wechat_mini_program_virtual_payment_env: 0,
..fast_spacetime_timeout_config()
})
.await;
let wechat_login = state
.wechat_auth_service()
.resolve_login(ResolveWechatLoginInput {
profile: WechatIdentityProfile {
provider_uid: "openid-user-points-60".to_string(),
provider_union_id: Some("union-user-points-60".to_string()),
display_name: Some("资料页用户".to_string()),
avatar_url: None,
session_key: Some("session-key-points-60".to_string()),
},
})
.await
.expect("wechat identity should seed");
let user_id = wechat_login.user.id.clone();
let order = RuntimeProfileRechargeOrderRecord {
order_id: "pointsorder60".to_string(),
user_id: user_id.clone(),
product_id: "points_60".to_string(),
product_title: "60泥点".to_string(),
kind: RuntimeProfileRechargeProductKind::Points,
amount_cents: 600,
status: RuntimeProfileRechargeOrderStatus::Pending,
payment_channel: PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_MINI_PROGRAM_VIRTUAL
.to_string(),
paid_at: None,
paid_at_micros: None,
provider_transaction_id: None,
created_at: "2026-05-30T10:00:00Z".to_string(),
created_at_micros: 1_780_000_000_000_000,
points_delta: 0,
membership_expires_at: None,
membership_expires_at_micros: None,
};
let center = RuntimeProfileRechargeCenterRecord {
user_id: user_id.clone(),
wallet_balance: 0,
membership: RuntimeProfileMembershipRecord {
user_id: user_id.clone(),
status: RuntimeProfileMembershipStatus::Normal,
tier: RuntimeProfileMembershipTier::Normal,
started_at: None,
started_at_micros: None,
expires_at: None,
expires_at_micros: None,
updated_at: None,
updated_at_micros: None,
},
point_products: vec![RuntimeProfileRechargeProductRecord {
product_id: "points_60".to_string(),
title: "60泥点".to_string(),
price_cents: 600,
kind: RuntimeProfileRechargeProductKind::Points,
points_amount: 60,
bonus_points: 60,
duration_days: 0,
badge_label: "首充双倍".to_string(),
description: "60+60泥点".to_string(),
tier: RuntimeProfileMembershipTier::Normal,
}],
membership_products: vec![],
benefits: vec![],
latest_order: None,
has_points_recharged: true,
};
let params = build_wechat_virtual_pay_params(
&state,
&center,
&order,
"openid-user-points-60",
)
.expect("points virtual pay params should build");
let sign_data: Value =
serde_json::from_str(&params.sign_data).expect("sign data should be valid json");
let attach: Value = serde_json::from_str(
sign_data["attach"]
.as_str()
.expect("attach should be string json"),
)
.expect("attach should decode");
assert_eq!(params.mode, "short_series_coin");
assert_eq!(sign_data["buyQuantity"], 60);
assert_eq!(sign_data["offerId"], "offer-1");
assert_eq!(sign_data["outTradeNo"], "pointsorder60");
assert_eq!(attach["paymentChannel"], "wechat_mp_virtual");
assert!(!params.pay_sig.is_empty());
assert!(!params.signature.is_empty());
}
#[tokio::test]
async fn wechat_virtual_pay_params_accept_admin_membership_product_ids() {
let state = seed_authenticated_state_with_config(AppConfig {
@@ -2327,9 +2486,10 @@ mod tests {
})
.await
.expect("wechat identity should seed");
let user_id = wechat_login.user.id.clone();
let order = RuntimeProfileRechargeOrderRecord {
order_id: "item01order01".to_string(),
user_id: wechat_login.user.id,
user_id: user_id.clone(),
product_id: "item01".to_string(),
product_title: "测试道具".to_string(),
kind: RuntimeProfileRechargeProductKind::Membership,
@@ -2347,7 +2507,39 @@ mod tests {
membership_expires_at_micros: None,
};
let params = build_wechat_virtual_pay_params(&state, &order, "openid-user-item01")
let center = RuntimeProfileRechargeCenterRecord {
user_id: user_id.clone(),
wallet_balance: 0,
membership: RuntimeProfileMembershipRecord {
user_id: user_id.clone(),
status: RuntimeProfileMembershipStatus::Normal,
tier: RuntimeProfileMembershipTier::Normal,
started_at: None,
started_at_micros: None,
expires_at: None,
expires_at_micros: None,
updated_at: None,
updated_at_micros: None,
},
point_products: vec![],
membership_products: vec![RuntimeProfileRechargeProductRecord {
product_id: "item01".to_string(),
title: "测试道具".to_string(),
price_cents: 100,
kind: RuntimeProfileRechargeProductKind::Membership,
points_amount: 0,
bonus_points: 0,
duration_days: 30,
badge_label: String::new(),
description: "30天会员".to_string(),
tier: RuntimeProfileMembershipTier::Month,
}],
benefits: vec![],
latest_order: None,
has_points_recharged: false,
};
let params = build_wechat_virtual_pay_params(&state, &center, &order, "openid-user-item01")
.expect("custom membership virtual pay params should build");
let sign_data: Value =
serde_json::from_str(&params.sign_data).expect("sign data should be valid json");
@@ -2404,9 +2596,10 @@ mod tests {
})
.await
.expect("wechat identity should seed");
let user_id = wechat_login.user.id.clone();
let order = RuntimeProfileRechargeOrderRecord {
order_id: "sandboxorder01".to_string(),
user_id: wechat_login.user.id,
user_id: user_id.clone(),
product_id: "points_60".to_string(),
product_title: "60泥点".to_string(),
kind: RuntimeProfileRechargeProductKind::Points,
@@ -2424,7 +2617,39 @@ mod tests {
membership_expires_at_micros: None,
};
let error = build_wechat_virtual_pay_params(&state, &order, "openid-sandbox-1")
let center = RuntimeProfileRechargeCenterRecord {
user_id: user_id.clone(),
wallet_balance: 0,
membership: RuntimeProfileMembershipRecord {
user_id: user_id.clone(),
status: RuntimeProfileMembershipStatus::Normal,
tier: RuntimeProfileMembershipTier::Normal,
started_at: None,
started_at_micros: None,
expires_at: None,
expires_at_micros: None,
updated_at: None,
updated_at_micros: None,
},
point_products: vec![RuntimeProfileRechargeProductRecord {
product_id: "points_60".to_string(),
title: "60泥点".to_string(),
price_cents: 600,
kind: RuntimeProfileRechargeProductKind::Points,
points_amount: 60,
bonus_points: 60,
duration_days: 0,
badge_label: "首充双倍".to_string(),
description: "60+60泥点".to_string(),
tier: RuntimeProfileMembershipTier::Normal,
}],
membership_products: vec![],
benefits: vec![],
latest_order: None,
has_points_recharged: false,
};
let error = build_wechat_virtual_pay_params(&state, &center, &order, "openid-sandbox-1")
.expect_err("sandbox pay params should reject missing sandbox app key");
assert!(
error.to_string().contains("沙箱 AppKey 未配置"),