diff --git a/server-rs/crates/api-server/src/runtime_profile.rs b/server-rs/crates/api-server/src/runtime_profile.rs index 5bb731b7..6e37e17c 100644 --- a/server-rs/crates/api-server/src/runtime_profile.rs +++ b/server-rs/crates/api-server/src/runtime_profile.rs @@ -1235,23 +1235,22 @@ fn calc_wechat_virtual_payment_signature( ) -> Result { let env = state.config.wechat_mini_program_virtual_payment_env; let app_key = if use_sandbox_key || env == 1 { - state - .config - .wechat_mini_program_virtual_payment_sandbox_app_key - .as_deref() - .or(state + required_wechat_virtual_payment_config( + state + .config + .wechat_mini_program_virtual_payment_sandbox_app_key + .as_deref(), + "微信虚拟支付沙箱 AppKey 未配置", + )? + } else { + required_wechat_virtual_payment_config( + state .config .wechat_mini_program_virtual_payment_app_key - .as_deref()) - } else { - state - .config - .wechat_mini_program_virtual_payment_app_key - .as_deref() - } - .ok_or_else(|| { - AppError::from_status(StatusCode::BAD_REQUEST).with_message("微信虚拟支付 AppKey 未配置") - })?; + .as_deref(), + "微信虚拟支付 AppKey 未配置", + )? + }; calc_wechat_virtual_payment_pay_signature_with_key(app_key, sign_data) } @@ -2382,6 +2381,57 @@ mod tests { ); } + #[tokio::test] + async fn wechat_virtual_payment_sandbox_requires_sandbox_app_key() { + 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_sandbox_app_key: None, + wechat_mini_program_virtual_payment_env: 1, + ..fast_spacetime_timeout_config() + }) + .await; + let wechat_login = state + .wechat_auth_service() + .resolve_login(ResolveWechatLoginInput { + profile: WechatIdentityProfile { + provider_uid: "openid-sandbox-1".to_string(), + provider_union_id: Some("union-sandbox-1".to_string()), + display_name: Some("资料页用户".to_string()), + avatar_url: None, + session_key: Some("session-key-sandbox-1".to_string()), + }, + }) + .await + .expect("wechat identity should seed"); + let order = RuntimeProfileRechargeOrderRecord { + order_id: "sandboxorder01".to_string(), + user_id: wechat_login.user.id, + 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 error = build_wechat_virtual_pay_params(&state, &order, "openid-sandbox-1") + .expect_err("sandbox pay params should reject missing sandbox app key"); + assert!( + error.to_string().contains("沙箱 AppKey 未配置"), + "unexpected error: {error}" + ); + } + #[tokio::test] async fn profile_feedback_requires_authentication() { let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));