Files
Genarrative/server-rs/crates/api-server/src/wechat/pay.rs
T
kdletters 8862887baa 新增后台充值退款管理与微信V3退款闭环
新增充值订单查询、通用用户详情、钱包冻结与部分退款操作
接入微信V3退款申请、回调、主动查单、交易账单对账与安全诊断
新增退款占用、权益追回、异常欠账和消费限制事务
补齐Native支付SSE自动收口及退款编号与部分退款校验
同步SpacetimeDB schema、生成绑定、测试与项目文档
2026-07-14 12:16:01 +08:00

1483 lines
56 KiB
Rust

use std::time::{Duration, Instant};
use axum::{
Json,
extract::{Query, State},
http::{HeaderMap, HeaderValue, StatusCode, header::CONTENT_TYPE},
response::{IntoResponse, Response},
};
use bytes::Bytes;
use module_runtime::{
PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_H5, PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_JSAPI,
PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_MINI_PROGRAM,
PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_MINI_PROGRAM_VIRTUAL,
PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_NATIVE, RuntimeProfileRechargeOrderStatus,
RuntimeProfileRechargeRefundObservationInput, RuntimeProfileRechargeRefundObservationSource,
RuntimeProfileRechargeRefundStatus, build_runtime_profile_recharge_refund_observation_input,
};
use platform_wechat::pay::{
WechatMiniProgramMessagePushQuery, WechatMiniProgramOrderRequest, WechatPayConfig,
WechatPayError, WechatPayRefund, WechatPayRefundNotification,
WechatVirtualPaymentNotifyDebugSummary, WechatVirtualPaymentNotifyOrder, WechatWebOrderRequest,
build_virtual_payment_notify_debug_summary, decrypt_wechat_message_push_ciphertext,
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, WechatVirtualPaymentNotifyProvideGoodsRequest, WechatVirtualPaymentOrder,
WechatVirtualPaymentQueryOrderRequest,
};
use serde::Serialize;
use serde_json::json;
use sha2::{Digest, Sha256};
use shared_kernel::offset_datetime_to_unix_micros;
use spacetime_client::SpacetimeClientError;
use time::OffsetDateTime;
use tracing::{debug, info, warn};
use crate::{config::AppConfig, http_error::AppError, state::AppState};
#[derive(Clone, Copy)]
enum VirtualPaymentNotifyResponseFormat {
Json,
Xml,
}
impl VirtualPaymentNotifyResponseFormat {
fn as_str(self) -> &'static str {
match self {
Self::Json => "json",
Self::Xml => "xml",
}
}
}
#[derive(Serialize)]
struct ApiWechatVirtualPaymentNotifyResponse {
#[serde(rename = "ErrCode")]
err_code: i32,
#[serde(rename = "ErrMsg")]
err_msg: String,
}
const WECHAT_VIRTUAL_PAYMENT_IOS_REFUND_QUERY_EVENT: &str =
"xpay_subscribe_ios_refund_query_notify";
const WECHAT_VIRTUAL_PAYMENT_NOTIFY_CONFIRM_TIMEOUT: Duration = Duration::from_millis(2_500);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum VirtualPaymentNotifyHandling {
CreditOrder,
DebugAcknowledge,
DebugRetry,
}
enum VirtualPaymentNotifyCreditOutcome {
IgnoredNoRechargeOrder,
AlreadyPaid { order_id: String },
Credited { order_id: String },
}
impl VirtualPaymentNotifyHandling {
fn as_str(self) -> &'static str {
match self {
Self::CreditOrder => "credit_order",
Self::DebugAcknowledge => "debug_acknowledge",
Self::DebugRetry => "debug_retry",
}
}
}
struct VirtualPaymentNotifyDebugContext {
request_ref: String,
response_format: VirtualPaymentNotifyResponseFormat,
started_at: Instant,
}
impl VirtualPaymentNotifyDebugContext {
fn new(response_format: VirtualPaymentNotifyResponseFormat, body: &[u8]) -> Self {
let digest = Sha256::digest(body);
Self {
request_ref: format!("sha256:{}", hex::encode(&digest[..16])),
response_format,
started_at: Instant::now(),
}
}
fn log_received(&self, body_bytes: usize) {
debug!(
request_ref = self.request_ref.as_str(),
response_format = self.response_format.as_str(),
encrypted_request_bytes = body_bytes,
"收到微信虚拟支付通知"
);
}
fn log_signature_verified(&self, ciphertext_bytes: usize) {
debug!(
request_ref = self.request_ref.as_str(),
signature_verified = true,
ciphertext_bytes,
"微信虚拟支付通知验签成功"
);
}
fn log_summary(&self, summary: &WechatVirtualPaymentNotifyDebugSummary) {
debug!(
request_ref = self.request_ref.as_str(),
notification_fingerprint = summary.payload_fingerprint.as_str(),
event = summary.event.as_str(),
event_ref = summary.event_ref.as_deref().unwrap_or("not_applicable"),
known_event = summary.known_event,
response_format = self.response_format.as_str(),
payload_bytes = summary.payload_bytes,
payload_schema_keys = ?summary.schema_fields,
identifier_refs = ?summary.identifier_refs,
safe_fields = ?summary.safe_fields,
sensitive_text_fingerprints = ?summary.sensitive_text_fields,
apple_subscription_info = summary.apple_subscription_info,
subscription_info = summary.subscription_info,
signature_verified = true,
decrypt_succeeded = true,
"微信虚拟支付通知诊断摘要"
);
}
fn success(
&self,
summary: &WechatVirtualPaymentNotifyDebugSummary,
handling: VirtualPaymentNotifyHandling,
) -> Response {
debug!(
request_ref = self.request_ref.as_str(),
notification_fingerprint = summary.payload_fingerprint.as_str(),
event = summary.event.as_str(),
handling = handling.as_str(),
response_err_code = 0,
handler_latency_ms = self.started_at.elapsed().as_millis() as u64,
"微信虚拟支付通知处理完成"
);
build_virtual_payment_notify_success_response(self.response_format)
}
fn error(
&self,
error: WechatPayError,
stage: &'static str,
summary: Option<&WechatVirtualPaymentNotifyDebugSummary>,
) -> Response {
let event = summary
.map(|value| value.event.as_str())
.unwrap_or("unparsed");
let notification_fingerprint = summary
.map(|value| value.payload_fingerprint.as_str())
.unwrap_or("unavailable");
warn!(
request_ref = self.request_ref.as_str(),
notification_fingerprint,
event,
stage,
error = %error,
response_err_code = 1,
handler_latency_ms = self.started_at.elapsed().as_millis() as u64,
"微信虚拟支付通知处理失败"
);
build_virtual_payment_notify_error_response(error, self.response_format)
}
}
fn classify_virtual_payment_notify(
summary: &WechatVirtualPaymentNotifyDebugSummary,
) -> VirtualPaymentNotifyHandling {
match summary.raw_event() {
WECHAT_VIRTUAL_PAYMENT_IOS_REFUND_QUERY_EVENT => VirtualPaymentNotifyHandling::DebugRetry,
"xpay_goods_deliver_notify" | "xpay_coin_pay_notify" if summary.subscription_info => {
VirtualPaymentNotifyHandling::DebugAcknowledge
}
"xpay_goods_deliver_notify" | "xpay_coin_pay_notify" => {
VirtualPaymentNotifyHandling::CreditOrder
}
_ if summary.known_event => VirtualPaymentNotifyHandling::DebugAcknowledge,
_ => VirtualPaymentNotifyHandling::DebugRetry,
}
}
async fn confirm_virtual_payment_recharge_order(
state: &AppState,
notify: &WechatVirtualPaymentNotifyOrder,
) -> Result<VirtualPaymentNotifyCreditOutcome, WechatPayError> {
let (_, order) = match state
.spacetime_client()
.get_profile_recharge_order(notify.out_trade_no.clone())
.await
{
Ok(result) => result,
Err(error) if is_profile_recharge_order_not_found(&error) => {
return Ok(VirtualPaymentNotifyCreditOutcome::IgnoredNoRechargeOrder);
}
Err(error) => {
return Err(WechatPayError::Upstream(format!(
"读取本地充值订单失败:{error}"
)));
}
};
if order.payment_channel != PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_MINI_PROGRAM_VIRTUAL {
return Err(WechatPayError::InvalidRequest(
"微信虚拟支付通知对应的本地订单渠道不匹配".to_string(),
));
}
if order.status == RuntimeProfileRechargeOrderStatus::Paid {
return Ok(VirtualPaymentNotifyCreditOutcome::AlreadyPaid {
order_id: order.order_id,
});
}
if !matches!(
order.status,
RuntimeProfileRechargeOrderStatus::Pending | RuntimeProfileRechargeOrderStatus::Expired
) {
return Err(WechatPayError::InvalidRequest(
"微信虚拟支付通知对应的本地订单状态不允许确认支付".to_string(),
));
}
let identity = state
.wechat_auth_service()
.get_identity_by_user_id(&order.user_id)
.map_err(|error| WechatPayError::Upstream(format!("读取微信身份失败:{error}")))?
.ok_or_else(|| {
WechatPayError::InvalidRequest("微信虚拟支付通知对应的本地订单缺少微信身份".to_string())
})?;
let query_request = build_wechat_virtual_payment_query_order_request(
&state.config,
identity.provider_uid,
order.order_id.clone(),
)
.map_err(|error| {
WechatPayError::InvalidConfig(format!("构造微信虚拟支付查单请求失败:{error}"))
})?;
let wechat_order = state
.wechat_client()
.query_virtual_payment_order(query_request)
.await
.map_err(|error| WechatPayError::Upstream(format!("微信虚拟支付查单失败:{error}")))?;
validate_wechat_virtual_payment_order(&order.order_id, order.amount_cents, &wechat_order)
.map_err(|error| {
WechatPayError::Upstream(format!("微信虚拟支付查单契约校验失败:{error}"))
})?;
if !is_wechat_virtual_payment_order_paid(wechat_order.status) {
return Err(WechatPayError::Upstream(format!(
"微信虚拟支付查单尚未确认支付,status={}",
wechat_order.status
)));
}
let paid_at_micros =
paid_at_micros_from_wechat_virtual_payment_order(&wechat_order).map_err(|error| {
WechatPayError::Upstream(format!("微信虚拟支付查单缺少权威支付时间:{error}"))
})?;
let order_id = order.order_id;
state
.spacetime_client()
.mark_profile_recharge_order_paid(
order_id.clone(),
paid_at_micros,
wechat_order.wxpay_order_id.or(wechat_order.wx_order_id),
)
.await
.map_err(|error| WechatPayError::Upstream(format!("确认微信虚拟支付订单失败:{error}")))?;
Ok(VirtualPaymentNotifyCreditOutcome::Credited { order_id })
}
fn is_profile_recharge_order_not_found(error: &SpacetimeClientError) -> bool {
matches!(
error,
SpacetimeClientError::Procedure(message)
if matches!(
message.trim(),
"profile_recharge_order missing" | "profile_recharge_order 不存在"
)
)
}
pub async fn handle_wechat_pay_notify(
State(state): State<AppState>,
headers: HeaderMap,
body: Bytes,
) -> Result<StatusCode, AppError> {
let notify = state
.wechat_pay_client()
.parse_notify(&headers, &body)
.map_err(map_wechat_pay_notify_error)?;
if notify.trade_state != "SUCCESS" {
info!(
order_id = notify.out_trade_no.as_str(),
trade_state = notify.trade_state.as_str(),
"收到非成功微信支付通知"
);
return Ok(StatusCode::NO_CONTENT);
}
let paid_at_micros = notify
.success_time
.as_deref()
.ok_or_else(|| {
AppError::from_status(StatusCode::BAD_REQUEST)
.with_message("微信支付成功通知缺少 success_time")
})
.and_then(|value| {
shared_kernel::parse_rfc3339(value).map_err(|error| {
AppError::from_status(StatusCode::BAD_REQUEST)
.with_message(format!("微信支付成功通知 success_time 无效:{error}"))
})
})
.map(offset_datetime_to_unix_micros)?;
let transaction_id = notify.transaction_id.clone().ok_or_else(|| {
AppError::from_status(StatusCode::BAD_REQUEST)
.with_message("微信支付成功通知缺少 transaction_id")
})?;
let amount_total_cents = notify.amount_total_cents.ok_or_else(|| {
AppError::from_status(StatusCode::BAD_REQUEST).with_message("微信支付成功通知缺少金额")
})?;
let (_, order) = state
.spacetime_client()
.get_profile_recharge_order(notify.out_trade_no.clone())
.await
.map_err(|error| {
AppError::from_status(StatusCode::BAD_GATEWAY)
.with_message(format!("读取微信支付本地订单失败:{error}"))
})?;
validate_wechat_pay_notify_order(&order, amount_total_cents, &transaction_id)?;
state
.spacetime_client()
.mark_profile_recharge_order_paid(
notify.out_trade_no.clone(),
paid_at_micros,
Some(transaction_id),
)
.await
.map_err(|error| {
AppError::from_status(StatusCode::BAD_GATEWAY)
.with_message(format!("确认微信支付订单失败:{error}"))
})?;
state.publish_profile_recharge_order_update(notify.out_trade_no.clone());
info!(
order_id = notify.out_trade_no.as_str(),
"微信支付通知已确认订单入账"
);
Ok(StatusCode::NO_CONTENT)
}
fn validate_wechat_pay_notify_order(
order: &module_runtime::RuntimeProfileRechargeOrderRecord,
amount_total_cents: u64,
transaction_id: &str,
) -> Result<(), AppError> {
if !matches!(
order.payment_channel.as_str(),
PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_MINI_PROGRAM
| PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_JSAPI
| PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_H5
| PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_NATIVE
) {
return Err(AppError::from_status(StatusCode::BAD_REQUEST)
.with_message("微信支付成功通知对应订单不是普通 V3 渠道"));
}
if order.amount_cents != amount_total_cents {
return Err(AppError::from_status(StatusCode::BAD_REQUEST)
.with_message("微信支付成功通知金额与本地订单不一致"));
}
if let Some(existing) = order.provider_transaction_id.as_deref()
&& existing != transaction_id
{
return Err(AppError::from_status(StatusCode::BAD_REQUEST)
.with_message("微信支付成功通知 transaction_id 与已结算订单不一致"));
}
Ok(())
}
pub async fn handle_wechat_pay_refund_notify(
State(state): State<AppState>,
headers: HeaderMap,
body: Bytes,
) -> Response {
let request_ref = state
.wechat_pay_client()
.refund_notify_request_ref(&body)
.unwrap_or_else(|| "unavailable".to_string());
debug!(
request_ref = request_ref.as_str(),
payload_bytes = body.len(),
"收到微信支付 V3 退款结果通知,开始验签解密"
);
let notification = match state
.wechat_pay_client()
.parse_refund_notify(&headers, &body)
{
Ok(notification) => notification,
Err(error) => {
warn!(
request_ref = request_ref.as_str(),
stage = wechat_pay_refund_notify_error_stage(&error),
error_code = error.diagnostic_code(),
failure_reason = wechat_pay_refund_notify_failure_reason(&error),
payload_bytes = body.len(),
"微信支付 V3 退款结果通知处理失败"
);
return build_wechat_pay_refund_notify_error_response(error);
}
};
let summary = &notification.debug;
let received_account_bytes = summary
.user_received_account
.as_ref()
.map(|value| value.bytes);
let received_account_ref = summary
.user_received_account
.as_ref()
.map(|value| value.hmac_ref.as_str());
debug!(
request_ref = request_ref.as_str(),
event_type = summary.event_type.as_str(),
known_event = summary.known_event,
resource_type = summary.resource_type.as_str(),
original_type = summary.original_type.as_str(),
algorithm = summary.algorithm.as_str(),
create_time = summary.create_time.as_str(),
refund_status = summary.refund_status.as_str(),
success_time = ?summary.success_time.as_deref(),
amount_total_cents = summary.amount_total_cents,
amount_refund_cents = summary.amount_refund_cents,
amount_payer_total_cents = summary.amount_payer_total_cents,
amount_payer_refund_cents = summary.amount_payer_refund_cents,
payload_bytes = summary.payload_bytes,
payload_fingerprint = summary.payload_fingerprint.as_str(),
notification_ref = summary.notification_ref.as_str(),
merchant_ref = summary.merchant_ref.as_str(),
transaction_ref = summary.transaction_ref.as_str(),
order_ref = summary.order_ref.as_str(),
refund_ref = summary.refund_ref.as_str(),
merchant_refund_ref = summary.merchant_refund_ref.as_str(),
user_received_account_bytes = ?received_account_bytes,
user_received_account_ref = ?received_account_ref,
signature_verified = true,
decrypt_succeeded = true,
business_mutation = true,
"微信支付 V3 退款结果通知诊断摘要"
);
let observation = match build_wechat_pay_refund_notification_observation(&notification) {
Ok(observation) => observation,
Err(error) => {
warn!(
request_ref = request_ref.as_str(),
stage = "observation_validation",
error_code = error.diagnostic_code(),
failure_reason = wechat_pay_refund_notify_failure_reason(&error),
"微信支付 V3 退款结果通知无法构造持久化事实"
);
return build_wechat_pay_refund_notify_error_response(error);
}
};
let (record, settlement, duplicate, resolution_code) = match state
.spacetime_client()
.record_profile_recharge_refund_observation(observation)
.await
{
Ok(result) => result,
Err(error) => {
warn!(
request_ref = request_ref.as_str(),
stage = "refund_persistence",
error = %error,
"微信支付 V3 退款结果通知持久化失败"
);
return build_wechat_pay_refund_notify_error_response(WechatPayError::Upstream(
format!("持久化微信支付退款事实失败:{error}"),
));
}
};
state.publish_profile_recharge_order_update(record.order_id.clone());
let wallet_frozen = settlement
.as_ref()
.map(|value| value.wallet_frozen)
.unwrap_or(false);
if matches!(
resolution_code.as_str(),
"immutable_conflict" | "provider_refund_id_conflict" | "status_conflict"
) {
warn!(
request_ref = request_ref.as_str(),
notification_fingerprint = summary.payload_fingerprint.as_str(),
duplicate,
resolution_code = resolution_code.as_str(),
wallet_frozen,
"微信支付 V3 退款结果通知已持久化为冲突观察"
);
} else {
info!(
request_ref = request_ref.as_str(),
notification_fingerprint = summary.payload_fingerprint.as_str(),
duplicate,
resolution_code = resolution_code.as_str(),
wallet_frozen,
"微信支付 V3 退款结果通知已持久化"
);
}
StatusCode::NO_CONTENT.into_response()
}
fn build_wechat_pay_refund_notification_observation(
notification: &WechatPayRefundNotification,
) -> Result<RuntimeProfileRechargeRefundObservationInput, WechatPayError> {
let observed_at_micros = parse_wechat_pay_refund_time(
Some(notification.create_time.as_str()),
"微信支付退款通知 create_time",
)?
.ok_or_else(|| {
WechatPayError::InvalidRequest("微信支付退款通知缺少 create_time".to_string())
})?;
build_wechat_pay_refund_observation(
notification.notification_id.clone(),
RuntimeProfileRechargeRefundObservationSource::Callback,
Some(notification.debug.notification_ref.clone()),
notification.debug.payload_fingerprint.clone(),
&notification.refund,
observed_at_micros,
)
}
pub(crate) fn build_wechat_pay_refund_observation(
observation_id: String,
source: RuntimeProfileRechargeRefundObservationSource,
notification_ref: Option<String>,
payload_fingerprint: String,
refund: &WechatPayRefund,
observed_at_micros: i64,
) -> Result<RuntimeProfileRechargeRefundObservationInput, WechatPayError> {
let provider_status = match refund.status.trim().to_ascii_uppercase().as_str() {
"PROCESSING" => RuntimeProfileRechargeRefundStatus::Processing,
"SUCCESS" => RuntimeProfileRechargeRefundStatus::Success,
"ABNORMAL" => RuntimeProfileRechargeRefundStatus::Abnormal,
"CLOSED" => RuntimeProfileRechargeRefundStatus::Closed,
_ => {
return Err(WechatPayError::InvalidRequest(
"微信支付退款包含未知状态".to_string(),
));
}
};
let success_at_micros =
parse_wechat_pay_refund_time(refund.success_time.as_deref(), "微信支付退款 success_time")?;
build_runtime_profile_recharge_refund_observation_input(
observation_id,
source,
notification_ref,
payload_fingerprint,
refund.out_refund_no.clone(),
refund.refund_id.clone(),
refund.out_trade_no.clone(),
refund.transaction_id.clone(),
provider_status,
refund.amount_total_cents,
refund.amount_refund_cents,
refund.amount_payer_total_cents,
refund.amount_payer_refund_cents,
success_at_micros,
observed_at_micros,
)
.map_err(WechatPayError::InvalidRequest)
}
fn parse_wechat_pay_refund_time(
value: Option<&str>,
field_name: &str,
) -> Result<Option<i64>, WechatPayError> {
value
.map(|value| {
shared_kernel::parse_rfc3339(value)
.map(offset_datetime_to_unix_micros)
.map_err(|error| {
WechatPayError::InvalidRequest(format!("{field_name} 格式无效:{error}"))
})
})
.transpose()
}
fn wechat_pay_refund_notify_error_stage(error: &WechatPayError) -> &'static str {
match error {
WechatPayError::InvalidSignature(_) => "signature_verification",
WechatPayError::Crypto(_) => "payload_decryption",
WechatPayError::Deserialize(_) => "payload_parse",
WechatPayError::InvalidRequest(_) => "contract_validation",
WechatPayError::Disabled | WechatPayError::InvalidConfig(_) => "provider_config",
WechatPayError::OrderNotExist(_)
| WechatPayError::RequestFailed(_)
| WechatPayError::Upstream(_) => "unexpected_upstream",
}
}
fn wechat_pay_refund_notify_failure_reason(error: &WechatPayError) -> &'static str {
match error {
WechatPayError::InvalidSignature(message) if message.contains("Wechatpay-Timestamp") => {
"missing_timestamp"
}
WechatPayError::InvalidSignature(message) if message.contains("Wechatpay-Nonce") => {
"missing_nonce"
}
WechatPayError::InvalidSignature(message) if message.contains("Wechatpay-Signature") => {
"missing_signature"
}
WechatPayError::InvalidSignature(message) if message.contains("Wechatpay-Serial") => {
"missing_platform_serial"
}
WechatPayError::InvalidSignature(message) if message.contains("时间戳格式") => {
"invalid_timestamp"
}
WechatPayError::InvalidSignature(message) if message.contains("时间戳超出") => {
"timestamp_out_of_window"
}
WechatPayError::InvalidSignature(message) if message.contains("序列号不匹配") => {
"platform_serial_mismatch"
}
WechatPayError::InvalidSignature(message) if message.contains("签名探测") => {
"signature_probe"
}
WechatPayError::InvalidSignature(message) if message.contains("base64") => {
"signature_encoding_invalid"
}
WechatPayError::InvalidSignature(_) => "signature_invalid",
WechatPayError::Crypto(message) if message.contains("base64") => {
"ciphertext_encoding_invalid"
}
WechatPayError::Crypto(message) if message.contains("nonce") => "resource_nonce_invalid",
WechatPayError::Crypto(_) => "ciphertext_authentication_failed",
WechatPayError::Deserialize(message) if message.contains("解密资源") => {
"decrypted_resource_schema_invalid"
}
WechatPayError::Deserialize(_) => "notification_envelope_schema_invalid",
WechatPayError::InvalidRequest(message) if message.contains("event_type") => {
"event_type_invalid"
}
WechatPayError::InvalidRequest(message) if message.contains("退款状态不一致") => {
"event_status_mismatch"
}
WechatPayError::InvalidRequest(message) if message.contains("商户号不匹配") => {
"merchant_mismatch"
}
WechatPayError::InvalidRequest(message) if message.contains("resource_type") => {
"resource_type_invalid"
}
WechatPayError::InvalidRequest(message) if message.contains("algorithm") => {
"resource_algorithm_invalid"
}
WechatPayError::InvalidRequest(message) if message.contains("original_type") => {
"resource_original_type_invalid"
}
WechatPayError::InvalidRequest(_) => "contract_invalid",
WechatPayError::Disabled => "provider_disabled",
WechatPayError::InvalidConfig(_) => "provider_config_invalid",
WechatPayError::OrderNotExist(_) => "unexpected_order_lookup",
WechatPayError::RequestFailed(_) => "unexpected_request_failure",
WechatPayError::Upstream(_) => "unexpected_upstream_failure",
}
}
fn build_wechat_pay_refund_notify_error_response(error: WechatPayError) -> Response {
let status = match error {
WechatPayError::InvalidSignature(_)
| WechatPayError::InvalidRequest(_)
| WechatPayError::Deserialize(_)
| WechatPayError::Crypto(_) => StatusCode::BAD_REQUEST,
WechatPayError::Disabled | WechatPayError::InvalidConfig(_) => {
StatusCode::SERVICE_UNAVAILABLE
}
WechatPayError::OrderNotExist(_)
| WechatPayError::RequestFailed(_)
| WechatPayError::Upstream(_) => StatusCode::BAD_GATEWAY,
};
(
status,
Json(json!({
"code": "FAIL",
"message": "失败"
})),
)
.into_response()
}
pub async fn handle_wechat_virtual_payment_message_push_verify(
State(state): State<AppState>,
Query(query): Query<WechatMiniProgramMessagePushQuery>,
) -> Response {
let token = match read_wechat_message_push_config(
state.config.wechat_mini_program_message_token.as_deref(),
"WECHAT_MINIPROGRAM_MESSAGE_TOKEN",
) {
Ok(token) => token,
Err(error) => return build_wechat_message_push_verify_error_response(error),
};
let aes_key = match read_wechat_message_push_config(
state
.config
.wechat_mini_program_message_encoding_aes_key
.as_deref(),
"WECHAT_MINIPROGRAM_MESSAGE_ENCODING_AES_KEY",
) {
Ok(value) => value,
Err(error) => return build_wechat_message_push_verify_error_response(error),
};
match resolve_wechat_message_push_verify_response(
token,
aes_key,
state
.config
.wechat_mini_program_app_id
.as_deref()
.or(state.config.wechat_app_id.as_deref()),
&query,
) {
Ok(plaintext) => (StatusCode::OK, plaintext).into_response(),
Err(error) => build_wechat_message_push_verify_error_response(error),
}
}
pub async fn handle_wechat_virtual_payment_notify(
State(state): State<AppState>,
headers: HeaderMap,
Query(query): Query<WechatMiniProgramMessagePushQuery>,
body: Bytes,
) -> Response {
let response_format = detect_virtual_payment_notify_response_format(&headers, &body);
let debug_context = VirtualPaymentNotifyDebugContext::new(response_format, &body);
debug_context.log_received(body.len());
let encrypted_payload = match parse_wechat_mini_program_message_push_payload(&body) {
Ok(payload) => payload,
Err(error) => return debug_context.error(error, "encrypted_envelope_parse", None),
};
let token = match read_wechat_message_push_config(
state.config.wechat_mini_program_message_token.as_deref(),
"WECHAT_MINIPROGRAM_MESSAGE_TOKEN",
) {
Ok(token) => token,
Err(error) => return debug_context.error(error, "message_token_config", None),
};
let aes_key = match read_wechat_message_push_config(
state
.config
.wechat_mini_program_message_encoding_aes_key
.as_deref(),
"WECHAT_MINIPROGRAM_MESSAGE_ENCODING_AES_KEY",
) {
Ok(value) => value,
Err(error) => return debug_context.error(error, "message_aes_key_config", None),
};
let signature = query
.msg_signature
.as_deref()
.or(query.signature.as_deref())
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or("");
let timestamp = query.timestamp.as_deref().map(str::trim).unwrap_or("");
let nonce = query.nonce.as_deref().map(str::trim).unwrap_or("");
if signature.is_empty() || timestamp.is_empty() || nonce.is_empty() {
return debug_context.error(
WechatPayError::InvalidRequest("微信消息推送加密参数不完整".to_string()),
"signature_parameters",
None,
);
}
if !verify_wechat_message_push_signature(
token,
timestamp,
nonce,
encrypted_payload.encrypt.as_str(),
signature,
) {
return debug_context.error(
WechatPayError::InvalidSignature("微信消息推送 msg_signature 无效".to_string()),
"signature_verification",
None,
);
}
debug_context.log_signature_verified(encrypted_payload.encrypt.len());
let notify_body = match decrypt_wechat_message_push_ciphertext(
aes_key,
encrypted_payload.encrypt.as_str(),
state
.config
.wechat_mini_program_app_id
.as_deref()
.or(state.config.wechat_app_id.as_deref()),
) {
Ok(body) => body,
Err(error) => return debug_context.error(error, "payload_decryption", None),
};
let summary = match build_virtual_payment_notify_debug_summary(
notify_body.as_bytes(),
token.as_bytes(),
) {
Ok(summary) => summary,
Err(error) => return debug_context.error(error, "payload_summary", None),
};
debug_context.log_summary(&summary);
let handling = classify_virtual_payment_notify(&summary);
match handling {
VirtualPaymentNotifyHandling::DebugAcknowledge => {
info!(
event = summary.event.as_str(),
notification_fingerprint = summary.payload_fingerprint.as_str(),
apple_subscription_info = summary.apple_subscription_info,
subscription_info = summary.subscription_info,
"微信虚拟支付通知已进入诊断处理,不变更订单或用户权益"
);
return debug_context.success(&summary, handling);
}
VirtualPaymentNotifyHandling::DebugRetry => {
let (stage, message) =
if summary.raw_event() == WECHAT_VIRTUAL_PAYMENT_IOS_REFUND_QUERY_EVENT {
(
"ios_refund_decision_unconfigured",
"iOS退款问询尚未配置基于真实履约数据的自动决策",
)
} else {
("unknown_event", "微信虚拟支付通知事件尚未配置处理策略")
};
return debug_context.error(
WechatPayError::InvalidRequest(message.to_string()),
stage,
Some(&summary),
);
}
VirtualPaymentNotifyHandling::CreditOrder => {}
}
let notify = match parse_virtual_payment_notify(notify_body.as_bytes()) {
Ok(notify) => notify,
Err(error) => return debug_context.error(error, "order_payload_parse", Some(&summary)),
};
let outcome = match tokio::time::timeout(
WECHAT_VIRTUAL_PAYMENT_NOTIFY_CONFIRM_TIMEOUT,
confirm_virtual_payment_recharge_order(&state, &notify),
)
.await
{
Ok(Ok(outcome)) => outcome,
Ok(Err(error)) => {
return debug_context.error(error, "authoritative_order_confirmation", Some(&summary));
}
Err(_) => {
return debug_context.error(
WechatPayError::Upstream("微信虚拟支付通知权威查单确认超出 2.5 秒预算".to_string()),
"authoritative_order_confirmation_timeout",
Some(&summary),
);
}
};
match outcome {
VirtualPaymentNotifyCreditOutcome::IgnoredNoRechargeOrder => {
info!(
event = notify.event.as_str(),
order_ref = summary
.primary_identifier_ref("order_ref")
.unwrap_or("unavailable"),
"微信虚拟支付通知没有对应的本地充值订单,已按非充值通知记录"
);
debug_context.success(&summary, VirtualPaymentNotifyHandling::DebugAcknowledge)
}
VirtualPaymentNotifyCreditOutcome::AlreadyPaid { order_id } => {
state.publish_profile_recharge_order_update(order_id);
info!(
event = notify.event.as_str(),
order_ref = summary
.primary_identifier_ref("order_ref")
.unwrap_or("unavailable"),
"微信虚拟支付通知对应的本地充值订单已入账"
);
debug_context.success(&summary, handling)
}
VirtualPaymentNotifyCreditOutcome::Credited { order_id } => {
state.publish_profile_recharge_order_update(order_id);
info!(
event = notify.event.as_str(),
order_ref = summary
.primary_identifier_ref("order_ref")
.unwrap_or("unavailable"),
"微信虚拟支付推送已通过官方查单确认订单入账"
);
debug_context.success(&summary, handling)
}
}
}
pub fn build_wechat_pay_config(config: &AppConfig) -> WechatPayConfig {
WechatPayConfig {
enabled: config.wechat_pay_enabled,
provider: config.wechat_pay_provider.clone(),
app_id: config
.wechat_mini_program_app_id
.clone()
.or_else(|| config.wechat_app_id.clone()),
mch_id: config.wechat_pay_mch_id.clone(),
merchant_serial_no: config.wechat_pay_merchant_serial_no.clone(),
private_key_pem: config.wechat_pay_private_key_pem.clone(),
private_key_path: config.wechat_pay_private_key_path.clone(),
platform_public_key_pem: config.wechat_pay_platform_public_key_pem.clone(),
platform_public_key_path: config.wechat_pay_platform_public_key_path.clone(),
platform_serial_no: config.wechat_pay_platform_serial_no.clone(),
api_v3_key: config.wechat_pay_api_v3_key.clone(),
notify_url: config.wechat_pay_notify_url.clone(),
jsapi_endpoint: config.wechat_pay_jsapi_endpoint.clone(),
}
}
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 build_wechat_virtual_payment_notify_provide_goods_request(
config: &AppConfig,
order_id: String,
) -> Result<WechatVirtualPaymentNotifyProvideGoodsRequest, WechatError> {
if config.wechat_mini_program_virtual_payment_env > 1 {
return Err(WechatError::InvalidConfig(format!(
"微信虚拟支付发货确认 env 只允许 0 或 1,当前为 {}",
config.wechat_mini_program_virtual_payment_env
)));
}
Ok(WechatVirtualPaymentNotifyProvideGoodsRequest {
order_id,
env: config.wechat_mini_program_virtual_payment_env,
})
}
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,
) -> Result<i64, WechatError> {
let paid_time = order
.paid_time
.filter(|value| *value > 0)
.ok_or_else(|| WechatError::Upstream("微信已支付虚拟订单缺少合法 paid_time".to_string()))?;
paid_time.checked_mul(1_000_000).ok_or_else(|| {
WechatError::Upstream("微信已支付虚拟订单 paid_time 超出安全范围".to_string())
})
}
pub fn map_wechat_pay_error(error: WechatPayError) -> AppError {
match error {
WechatPayError::Disabled => AppError::from_status(StatusCode::BAD_REQUEST)
.with_message("微信支付暂未启用")
.with_details(json!({ "provider": "wechat_pay" })),
WechatPayError::InvalidConfig(message) => {
AppError::from_status(StatusCode::SERVICE_UNAVAILABLE)
.with_message(message)
.with_details(json!({ "provider": "wechat_pay" }))
}
WechatPayError::InvalidRequest(message) => AppError::from_status(StatusCode::BAD_REQUEST)
.with_message(message)
.with_details(json!({ "provider": "wechat_pay" })),
WechatPayError::OrderNotExist(message) => AppError::from_status(StatusCode::NOT_FOUND)
.with_message(message)
.with_details(json!({ "provider": "wechat_pay", "code": "ORDER_NOT_EXIST" })),
WechatPayError::RequestFailed(message)
| WechatPayError::Upstream(message)
| WechatPayError::Deserialize(message)
| WechatPayError::Crypto(message) => AppError::from_status(StatusCode::BAD_GATEWAY)
.with_message(message)
.with_details(json!({ "provider": "wechat_pay" })),
WechatPayError::InvalidSignature(message) => {
AppError::from_status(StatusCode::UNAUTHORIZED)
.with_message("微信支付通知签名无效")
.with_details(json!({ "provider": "wechat_pay", "reason": message }))
}
}
}
pub fn map_wechat_pay_init_error(error: WechatPayError) -> crate::state::AppStateInitError {
crate::state::AppStateInitError::WechatPay(error.to_string())
}
pub fn build_wechat_payment_request(
order_id: String,
product_title: String,
amount_cents: u64,
payer_openid: String,
) -> WechatMiniProgramOrderRequest {
WechatMiniProgramOrderRequest {
order_id,
description: format!("陶泥儿 - {product_title}"),
amount_cents,
payer_openid,
}
}
pub fn build_wechat_web_payment_request(
order_id: String,
product_title: String,
amount_cents: u64,
payer_client_ip: String,
) -> WechatWebOrderRequest {
WechatWebOrderRequest {
order_id,
description: format!("陶泥儿 - {product_title}"),
amount_cents,
payer_client_ip,
}
}
pub fn current_unix_micros() -> i64 {
let value = OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000;
i64::try_from(value).unwrap_or(i64::MAX)
}
fn map_wechat_pay_notify_error(error: WechatPayError) -> AppError {
warn!(error = %error, "微信支付通知处理失败");
map_wechat_pay_error(error)
}
fn read_wechat_message_push_config<'a>(
value: Option<&'a str>,
key: &str,
) -> Result<&'a str, WechatPayError> {
value
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| WechatPayError::InvalidConfig(format!("{key} 未配置")))
}
fn build_wechat_message_push_verify_error_response(error: WechatPayError) -> Response {
let message = match error {
WechatPayError::Disabled => "微信消息推送暂未启用".to_string(),
WechatPayError::InvalidConfig(message)
| WechatPayError::InvalidRequest(message)
| WechatPayError::OrderNotExist(message)
| WechatPayError::RequestFailed(message)
| WechatPayError::Upstream(message)
| WechatPayError::Deserialize(message)
| WechatPayError::Crypto(message)
| WechatPayError::InvalidSignature(message) => message,
};
(StatusCode::BAD_REQUEST, message).into_response()
}
#[cfg(test)]
mod tests {
use super::{
VirtualPaymentNotifyHandling, build_wechat_pay_refund_notify_error_response,
build_wechat_pay_refund_observation, build_wechat_virtual_payment_notify_response,
build_wechat_virtual_payment_query_order_request, classify_virtual_payment_notify,
is_profile_recharge_order_not_found, is_wechat_virtual_payment_order_paid,
paid_at_micros_from_wechat_virtual_payment_order, validate_wechat_pay_notify_order,
validate_wechat_virtual_payment_order,
};
use crate::config::AppConfig;
use module_runtime::{
RuntimeProfileRechargeOrderSnapshot, RuntimeProfileRechargeOrderStatus,
RuntimeProfileRechargeProductKind, RuntimeProfileRechargeRefundObservationSource,
build_runtime_profile_recharge_order_record,
};
use platform_wechat::{
WechatVirtualPaymentOrder,
pay::{WechatPayError, WechatPayRefund, build_virtual_payment_notify_debug_summary},
};
use spacetime_client::SpacetimeClientError;
#[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_only_acks_the_exact_missing_recharge_order_error() {
assert!(is_profile_recharge_order_not_found(
&SpacetimeClientError::Procedure("profile_recharge_order missing".to_string())
));
assert!(is_profile_recharge_order_not_found(
&SpacetimeClientError::Procedure("profile_recharge_order 不存在".to_string())
));
assert!(!is_profile_recharge_order_not_found(
&SpacetimeClientError::Procedure("SpacetimeDB 连接失败".to_string())
));
assert!(!is_profile_recharge_order_not_found(
&SpacetimeClientError::ConnectDropped
));
}
#[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_requires_a_stable_provider_paid_time() {
let mut order = WechatVirtualPaymentOrder {
order_id: "order-001".to_string(),
status: 2,
order_fee: 600,
order_type: 0,
paid_time: Some(1_777_111_300),
wx_order_id: None,
wxpay_order_id: None,
};
assert_eq!(
paid_at_micros_from_wechat_virtual_payment_order(&order)
.expect("paid_time should convert"),
1_777_111_300_000_000
);
order.paid_time = None;
assert!(paid_at_micros_from_wechat_virtual_payment_order(&order).is_err());
order.paid_time = Some(i64::MAX);
assert!(paid_at_micros_from_wechat_virtual_payment_order(&order).is_err());
}
#[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());
}
}
#[test]
fn virtual_payment_debug_routing_does_not_credit_refunds_or_subscriptions() {
for (body, expected) in [
(
r#"{"Event":"xpay_goods_deliver_notify","OutTradeNo":"order-1"}"#,
VirtualPaymentNotifyHandling::CreditOrder,
),
(
r#"{"Event":"xpay_goods_deliver_notify","AppleSubscriptionInfo":{"ProductId":"vip_month"}}"#,
VirtualPaymentNotifyHandling::DebugAcknowledge,
),
(
r#"{"Event":"xpay_goods_deliver_notify","OutContractCode":"contract-1","ContractWxAppid":"wx-app-1"}"#,
VirtualPaymentNotifyHandling::DebugAcknowledge,
),
(
r#"{"Event":"xpay_refund_notify","MchRefundId":"refund-1"}"#,
VirtualPaymentNotifyHandling::DebugAcknowledge,
),
(
r#"{"Event":"xpay_subscribe_ios_refund_query_notify","PayOrderId":"order-1"}"#,
VirtualPaymentNotifyHandling::DebugRetry,
),
(
r#"{"Event":"xpay_subscribe_ios_refund_query_notify","AppleSubscriptionInfo":{"ProductId":"vip_month"}}"#,
VirtualPaymentNotifyHandling::DebugRetry,
),
(
r#"{"Event":"xpay_future_notify"}"#,
VirtualPaymentNotifyHandling::DebugRetry,
),
] {
let summary =
build_virtual_payment_notify_debug_summary(body.as_bytes(), b"message-token")
.expect("debug routing fixture should parse");
assert_eq!(classify_virtual_payment_notify(&summary), expected);
}
}
#[test]
fn virtual_payment_ios_refund_query_has_no_fake_decision_response() {
let payload = serde_json::to_value(build_wechat_virtual_payment_notify_response(
1,
"iOS退款问询尚未配置基于真实履约数据的自动决策".to_string(),
))
.expect("iOS refund query retry response should serialize");
assert_eq!(payload["ErrCode"], 1);
assert!(payload.get("IosRefundQueryResponse").is_none());
}
#[tokio::test]
async fn v3_refund_notify_failure_uses_the_wechat_fail_response_contract() {
let response = build_wechat_pay_refund_notify_error_response(
WechatPayError::InvalidSignature("secret header value".to_string()),
);
assert_eq!(response.status(), axum::http::StatusCode::BAD_REQUEST);
let body = axum::body::to_bytes(response.into_body(), 1_024)
.await
.expect("refund failure response body should read");
let payload: serde_json::Value =
serde_json::from_slice(&body).expect("refund failure response should be JSON");
assert_eq!(
payload,
serde_json::json!({
"code": "FAIL",
"message": "失败"
})
);
assert!(!String::from_utf8_lossy(&body).contains("secret header value"));
}
#[test]
fn v3_refund_observation_preserves_verified_provider_money_and_status() {
let refund = WechatPayRefund {
mch_id: Some("1900000001".to_string()),
transaction_id: "tx-1".to_string(),
out_trade_no: "order-1".to_string(),
refund_id: "refund-1".to_string(),
out_refund_no: "merchant-refund-1".to_string(),
status: "SUCCESS".to_string(),
success_time: Some("2026-07-13T18:17:23+08:00".to_string()),
create_time: Some("2026-07-13T18:17:20+08:00".to_string()),
amount_total_cents: 600,
amount_refund_cents: 600,
amount_payer_total_cents: 600,
amount_payer_refund_cents: 600,
};
let observation = build_wechat_pay_refund_observation(
"callback:event-1".to_string(),
RuntimeProfileRechargeRefundObservationSource::Callback,
Some("hmac:event-1".to_string()),
"hmac:payload-1".to_string(),
&refund,
1_752_411_443_000_000,
)
.expect("verified refund fact should build");
assert_eq!(observation.total_cents, 600);
assert_eq!(observation.refund_cents, 600);
assert_eq!(observation.order_id, "order-1");
assert!(observation.success_at_micros.is_some());
let mut missing_success_time = refund;
missing_success_time.success_time = None;
assert!(
build_wechat_pay_refund_observation(
"callback:event-2".to_string(),
RuntimeProfileRechargeRefundObservationSource::Callback,
None,
"hmac:payload-2".to_string(),
&missing_success_time,
1,
)
.is_err()
);
}
#[test]
fn v3_payment_notify_requires_local_channel_amount_and_transaction_match() {
let order =
build_runtime_profile_recharge_order_record(RuntimeProfileRechargeOrderSnapshot {
order_id: "order-1".to_string(),
user_id: "user-1".to_string(),
product_id: "points-60".to_string(),
product_title: "60 points".to_string(),
kind: RuntimeProfileRechargeProductKind::Points,
amount_cents: 600,
status: RuntimeProfileRechargeOrderStatus::Paid,
payment_channel: "wechat_native".to_string(),
paid_at_micros: Some(1),
provider_transaction_id: Some("tx-1".to_string()),
created_at_micros: 1,
points_delta: 60,
membership_expires_at_micros: None,
expired_at_micros: None,
expiration_checked_at_micros: None,
expiration_provider_state: None,
expiration_last_error: None,
});
validate_wechat_pay_notify_order(&order, 600, "tx-1")
.expect("matching payment fact should pass");
assert!(validate_wechat_pay_notify_order(&order, 601, "tx-1").is_err());
assert!(validate_wechat_pay_notify_order(&order, 600, "tx-2").is_err());
}
}
fn build_virtual_payment_notify_error_response(
error: WechatPayError,
response_format: VirtualPaymentNotifyResponseFormat,
) -> Response {
let message = match error {
WechatPayError::Disabled => "微信虚拟支付暂未启用".to_string(),
WechatPayError::InvalidConfig(message)
| WechatPayError::InvalidRequest(message)
| WechatPayError::OrderNotExist(message)
| WechatPayError::RequestFailed(message)
| WechatPayError::Upstream(message)
| WechatPayError::Deserialize(message)
| WechatPayError::Crypto(message)
| WechatPayError::InvalidSignature(message) => message,
};
build_virtual_payment_notify_response(response_format, 1, message)
}
fn build_virtual_payment_notify_success_response(
response_format: VirtualPaymentNotifyResponseFormat,
) -> Response {
build_virtual_payment_notify_response(response_format, 0, "success")
}
fn build_virtual_payment_notify_response(
response_format: VirtualPaymentNotifyResponseFormat,
err_code: i32,
err_msg: impl Into<String>,
) -> Response {
let err_msg = err_msg.into();
match response_format {
VirtualPaymentNotifyResponseFormat::Json => Json(
build_wechat_virtual_payment_notify_response(err_code, err_msg),
)
.into_response(),
VirtualPaymentNotifyResponseFormat::Xml => {
let body = format!(
"<xml><ErrCode>{err_code}</ErrCode><ErrMsg><![CDATA[{err_msg}]]></ErrMsg></xml>"
);
let mut response = (StatusCode::OK, body).into_response();
response.headers_mut().insert(
CONTENT_TYPE,
HeaderValue::from_static("application/xml; charset=utf-8"),
);
response
}
}
}
fn build_wechat_virtual_payment_notify_response(
err_code: i32,
err_msg: impl Into<String>,
) -> ApiWechatVirtualPaymentNotifyResponse {
ApiWechatVirtualPaymentNotifyResponse {
err_code,
err_msg: err_msg.into(),
}
}
fn detect_virtual_payment_notify_response_format(
headers: &HeaderMap,
body: &[u8],
) -> VirtualPaymentNotifyResponseFormat {
let content_type = headers
.get(CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.unwrap_or("")
.to_ascii_lowercase();
if content_type.contains("xml") {
return VirtualPaymentNotifyResponseFormat::Xml;
}
let body_trimmed = body
.iter()
.copied()
.skip_while(|byte| byte.is_ascii_whitespace())
.next();
match body_trimmed {
Some(b'<') => VirtualPaymentNotifyResponseFormat::Xml,
_ => VirtualPaymentNotifyResponseFormat::Json,
}
}