fix wechat mini program virtual payment flow

This commit is contained in:
kdletters
2026-05-28 00:41:06 +08:00
parent b43c3cd823
commit 9c6fa10301
10 changed files with 335 additions and 57 deletions

View File

@@ -4,6 +4,7 @@ use axum::{
http::{HeaderMap, StatusCode},
response::Response,
};
use hmac::{Hmac, Mac};
use module_runtime::{
AnalyticsGranularity, PROFILE_RECHARGE_PAYMENT_CHANNEL_MOCK,
PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_H5,
@@ -24,7 +25,6 @@ use module_runtime::{
};
use serde::Deserialize;
use serde_json::{Value, json};
use hmac::{Hmac, Mac};
use sha2::Sha256;
use shared_contracts::runtime::{
ANALYTICS_GRANULARITY_DAY, ANALYTICS_GRANULARITY_MONTH, ANALYTICS_GRANULARITY_QUARTER,
@@ -1187,10 +1187,6 @@ fn build_wechat_virtual_pay_params(
AppError::from_status(StatusCode::BAD_REQUEST)
.with_message("当前微信登录态缺少 session_key请重新登录后再试")
})?;
let product = module_runtime::runtime_profile_recharge_product_by_id(&order.product_id)
.ok_or_else(|| {
AppError::from_status(StatusCode::BAD_REQUEST).with_message("充值商品不存在")
})?;
let offer_id = required_wechat_virtual_payment_config(
state
.config
@@ -1198,7 +1194,7 @@ fn build_wechat_virtual_pay_params(
.as_deref(),
"微信虚拟支付 OfferId 未配置",
)?;
let mode = match product.kind {
let mode = match order.kind {
RuntimeProfileRechargeProductKind::Points => "short_series_coin",
RuntimeProfileRechargeProductKind::Membership => "short_series_goods",
};
@@ -1215,9 +1211,9 @@ fn build_wechat_virtual_pay_params(
"openId": openid,
}).to_string(),
});
if product.kind == RuntimeProfileRechargeProductKind::Membership {
sign_data["productId"] = json!(product.product_id);
sign_data["goodsPrice"] = json!(product.price_cents);
if order.kind == RuntimeProfileRechargeProductKind::Membership {
sign_data["productId"] = json!(order.product_id);
sign_data["goodsPrice"] = json!(order.amount_cents);
}
let sign_data = sign_data.to_string();
let pay_sig = calc_wechat_virtual_payment_signature(state, &sign_data, false)?;
@@ -1242,7 +1238,10 @@ fn calc_wechat_virtual_payment_signature(
.config
.wechat_mini_program_virtual_payment_sandbox_app_key
.as_deref()
.or(state.config.wechat_mini_program_virtual_payment_app_key.as_deref())
.or(state
.config
.wechat_mini_program_virtual_payment_app_key
.as_deref())
} else {
state
.config
@@ -1250,8 +1249,7 @@ fn calc_wechat_virtual_payment_signature(
.as_deref()
}
.ok_or_else(|| {
AppError::from_status(StatusCode::BAD_REQUEST)
.with_message("微信虚拟支付 AppKey 未配置")
AppError::from_status(StatusCode::BAD_REQUEST).with_message("微信虚拟支付 AppKey 未配置")
})?;
calc_wechat_virtual_payment_signature_with_key(app_key, sign_data)
}
@@ -2294,6 +2292,59 @@ mod tests {
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 {
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-item01".to_string(),
provider_union_id: Some("union-user-item01".to_string()),
display_name: Some("资料页用户".to_string()),
avatar_url: None,
session_key: Some("session-key-item01".to_string()),
},
})
.await
.expect("wechat identity should seed");
let order = RuntimeProfileRechargeOrderRecord {
order_id: "item01order01".to_string(),
user_id: wechat_login.user.id,
product_id: "item01".to_string(),
product_title: "测试道具".to_string(),
kind: RuntimeProfileRechargeProductKind::Membership,
amount_cents: 100,
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-27T10:00:00Z".to_string(),
created_at_micros: 1_779_842_400_000_000,
points_delta: 0,
membership_expires_at: None,
membership_expires_at_micros: None,
};
let params = build_wechat_virtual_pay_params(&state, &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");
assert_eq!(params.mode, "short_series_goods");
assert_eq!(sign_data["productId"], "item01");
assert_eq!(sign_data["goodsPrice"], 100);
assert_eq!(sign_data["outTradeNo"], "item01order01");
}
#[tokio::test]
async fn profile_feedback_requires_authentication() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));