补齐微信虚拟支付查单与补偿

接入微信官方虚拟支付查单并缓存稳定接口令牌
让用户确认与订单过期补偿统一核对支付类型、状态、金额和单号
增加单笔历史订单的 dry-run 与确认指纹补单工具
隔离运行时身份与敏感 openid 文件并补充配置契约和回归测试
This commit is contained in:
2026-07-13 13:12:44 +08:00
parent 06aa99e3eb
commit 2f69f2fda2
16 changed files with 1343 additions and 33 deletions
@@ -11,6 +11,9 @@ use platform_wechat::pay::{
parse_virtual_payment_notify, parse_wechat_mini_program_message_push_payload,
resolve_wechat_message_push_verify_response, verify_wechat_message_push_signature,
};
use platform_wechat::{
WechatError, WechatVirtualPaymentOrder, WechatVirtualPaymentQueryOrderRequest,
};
use serde::Serialize;
use serde_json::json;
use shared_kernel::offset_datetime_to_unix_micros;
@@ -248,6 +251,80 @@ pub fn build_wechat_pay_config(config: &AppConfig) -> WechatPayConfig {
}
}
pub fn build_wechat_virtual_payment_query_order_request(
config: &AppConfig,
openid: String,
order_id: String,
) -> Result<WechatVirtualPaymentQueryOrderRequest, WechatError> {
let app_key = match config.wechat_mini_program_virtual_payment_env {
0 => config
.wechat_mini_program_virtual_payment_app_key
.as_deref(),
1 => config
.wechat_mini_program_virtual_payment_sandbox_app_key
.as_deref(),
env => {
return Err(WechatError::InvalidConfig(format!(
"微信虚拟支付查单 env 只允许 0 或 1,当前为 {env}"
)));
}
}
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| {
WechatError::InvalidConfig(match config.wechat_mini_program_virtual_payment_env {
1 => "微信虚拟支付沙箱 AppKey 未配置".to_string(),
_ => "微信虚拟支付 AppKey 未配置".to_string(),
})
})?;
Ok(WechatVirtualPaymentQueryOrderRequest {
openid,
order_id,
env: config.wechat_mini_program_virtual_payment_env,
app_key: app_key.to_string(),
})
}
pub fn validate_wechat_virtual_payment_order(
expected_order_id: &str,
expected_amount_cents: u64,
order: &WechatVirtualPaymentOrder,
) -> Result<(), WechatError> {
if order.order_id != expected_order_id {
return Err(WechatError::Upstream(
"微信虚拟支付查单返回的订单号与本地订单不一致".to_string(),
));
}
if !matches!(order.order_type, 0 | 7) {
return Err(WechatError::Upstream(
"微信虚拟支付查单返回的不是可入账支付单".to_string(),
));
}
if order.order_fee != expected_amount_cents {
return Err(WechatError::Upstream(
"微信虚拟支付查单返回的金额与本地订单不一致".to_string(),
));
}
if !(0..=10).contains(&order.status) {
return Err(WechatError::Upstream(
"微信虚拟支付查单返回了未知订单状态".to_string(),
));
}
Ok(())
}
pub fn is_wechat_virtual_payment_order_paid(status: i64) -> bool {
matches!(status, 2..=4)
}
pub fn paid_at_micros_from_wechat_virtual_payment_order(order: &WechatVirtualPaymentOrder) -> i64 {
order
.paid_time
.and_then(|seconds| seconds.checked_mul(1_000_000))
.unwrap_or_else(current_unix_micros)
}
pub fn map_wechat_pay_error(error: WechatPayError) -> AppError {
match error {
WechatPayError::Disabled => AppError::from_status(StatusCode::BAD_REQUEST)
@@ -345,6 +422,82 @@ fn build_wechat_message_push_verify_error_response(error: WechatPayError) -> Res
(StatusCode::BAD_REQUEST, message).into_response()
}
#[cfg(test)]
mod tests {
use super::{
build_wechat_virtual_payment_query_order_request, is_wechat_virtual_payment_order_paid,
validate_wechat_virtual_payment_order,
};
use crate::config::AppConfig;
use platform_wechat::WechatVirtualPaymentOrder;
#[test]
fn virtual_payment_query_uses_the_key_for_the_selected_environment() {
let config = AppConfig {
wechat_mini_program_virtual_payment_app_key: Some("production-key".to_string()),
wechat_mini_program_virtual_payment_sandbox_app_key: Some("sandbox-key".to_string()),
wechat_mini_program_virtual_payment_env: 1,
..AppConfig::default()
};
let request = build_wechat_virtual_payment_query_order_request(
&config,
"openid-001".to_string(),
"order-001".to_string(),
)
.expect("sandbox query request should build");
assert_eq!(request.app_key, "sandbox-key");
assert_eq!(request.env, 1);
}
#[test]
fn virtual_payment_query_only_treats_paid_and_delivery_states_as_paid() {
assert!(!is_wechat_virtual_payment_order_paid(0));
assert!(!is_wechat_virtual_payment_order_paid(1));
assert!(is_wechat_virtual_payment_order_paid(2));
assert!(is_wechat_virtual_payment_order_paid(3));
assert!(is_wechat_virtual_payment_order_paid(4));
assert!(!is_wechat_virtual_payment_order_paid(5));
assert!(!is_wechat_virtual_payment_order_paid(6));
}
#[test]
fn virtual_payment_query_rejects_a_mismatched_amount_before_crediting() {
let order = WechatVirtualPaymentOrder {
order_id: "order-001".to_string(),
status: 2,
order_fee: 601,
order_type: 0,
paid_time: Some(1_777_111_300),
wx_order_id: Some("wx-order-001".to_string()),
wxpay_order_id: Some("wxpay-order-001".to_string()),
};
assert!(validate_wechat_virtual_payment_order("order-001", 600, &order).is_err());
}
#[test]
fn virtual_payment_query_accepts_ios_payment_and_rejects_refunds() {
let mut order = WechatVirtualPaymentOrder {
order_id: "order-001".to_string(),
status: 2,
order_fee: 600,
order_type: 7,
paid_time: Some(1_777_111_300),
wx_order_id: Some("wx-order-001".to_string()),
wxpay_order_id: None,
};
validate_wechat_virtual_payment_order("order-001", 600, &order)
.expect("iOS payment order should be eligible for confirmation");
for refund_type in [1, 8] {
order.order_type = refund_type;
assert!(validate_wechat_virtual_payment_order("order-001", 600, &order).is_err());
}
}
}
fn build_virtual_payment_notify_error_response(
error: WechatPayError,
response_format: VirtualPaymentNotifyResponseFormat,