use axum::{ Json, extract::{Query, State}, http::{HeaderMap, HeaderValue, StatusCode, header::CONTENT_TYPE}, response::{IntoResponse, Response}, }; use bytes::Bytes; use platform_wechat::pay::{ WechatMiniProgramMessagePushQuery, WechatMiniProgramOrderRequest, WechatPayConfig, WechatPayError, WechatWebOrderRequest, 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 serde::Serialize; use serde_json::json; use shared_kernel::offset_datetime_to_unix_micros; use time::OffsetDateTime; use tracing::{info, warn}; use crate::{config::AppConfig, http_error::AppError, state::AppState}; #[derive(Clone, Copy)] enum VirtualPaymentNotifyResponseFormat { Json, Xml, } #[derive(Serialize)] struct ApiWechatVirtualPaymentNotifyResponse { #[serde(rename = "ErrCode")] err_code: i32, #[serde(rename = "ErrMsg")] err_msg: String, } pub async fn handle_wechat_pay_notify( State(state): State, headers: HeaderMap, body: Bytes, ) -> Result { 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() .and_then(|value| shared_kernel::parse_rfc3339(value).ok()) .map(offset_datetime_to_unix_micros) .unwrap_or_else(current_unix_micros); state .spacetime_client() .mark_profile_recharge_order_paid( notify.out_trade_no.clone(), paid_at_micros, notify.transaction_id.clone(), ) .await .map_err(|error| { AppError::from_status(StatusCode::BAD_GATEWAY) .with_message(format!("确认微信支付订单失败:{error}")) })?; info!( order_id = notify.out_trade_no.as_str(), "微信支付通知已确认订单入账" ); Ok(StatusCode::NO_CONTENT) } pub async fn handle_wechat_virtual_payment_message_push_verify( State(state): State, Query(query): Query, ) -> 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, headers: HeaderMap, Query(query): Query, body: Bytes, ) -> Response { let response_format = detect_virtual_payment_notify_response_format(&headers, &body); let encrypted_payload = match parse_wechat_mini_program_message_push_payload(&body) { Ok(payload) => payload, Err(error) => return build_virtual_payment_notify_error_response(error, response_format), }; 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_virtual_payment_notify_error_response(error, response_format), }; 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_virtual_payment_notify_error_response(error, response_format), }; 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 build_virtual_payment_notify_error_response( WechatPayError::InvalidRequest("微信消息推送加密参数不完整".to_string()), response_format, ); } if !verify_wechat_message_push_signature( token, timestamp, nonce, encrypted_payload.encrypt.as_str(), signature, ) { return build_virtual_payment_notify_error_response( WechatPayError::InvalidSignature("微信消息推送 msg_signature 无效".to_string()), response_format, ); } 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 build_virtual_payment_notify_error_response(error, response_format), }; let notify = match parse_virtual_payment_notify(notify_body.as_bytes()) { Ok(notify) => notify, Err(error) => return build_virtual_payment_notify_error_response(error, response_format), }; if notify.event != "xpay_goods_deliver_notify" && notify.event != "xpay_coin_pay_notify" { info!( event = notify.event.as_str(), order_id = notify.out_trade_no.as_str(), "收到非订单入账虚拟支付推送" ); return build_virtual_payment_notify_success_response(response_format); } let paid_at_micros = notify.paid_at_micros.unwrap_or_else(current_unix_micros); if state .spacetime_client() .mark_profile_recharge_order_paid( notify.out_trade_no.clone(), paid_at_micros, notify.transaction_id.clone(), ) .await .is_err() { warn!( order_id = notify.out_trade_no.as_str(), "确认微信虚拟支付订单失败" ); return build_virtual_payment_notify_error_response( WechatPayError::Upstream("确认微信虚拟支付订单失败".to_string()), response_format, ); } state.publish_profile_recharge_order_update(notify.out_trade_no.clone()); info!( event = notify.event.as_str(), order_id = notify.out_trade_no.as_str(), "微信虚拟支付推送已确认订单入账" ); build_virtual_payment_notify_success_response(response_format) } 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 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::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::RequestFailed(message) | WechatPayError::Upstream(message) | WechatPayError::Deserialize(message) | WechatPayError::Crypto(message) | WechatPayError::InvalidSignature(message) => message, }; (StatusCode::BAD_REQUEST, message).into_response() } fn build_virtual_payment_notify_error_response( error: WechatPayError, response_format: VirtualPaymentNotifyResponseFormat, ) -> Response { warn!(error = %error, "微信虚拟支付通知处理失败"); let message = match error { WechatPayError::Disabled => "微信虚拟支付暂未启用".to_string(), WechatPayError::InvalidConfig(message) | WechatPayError::InvalidRequest(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, ) -> 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!( "{err_code}" ); 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, ) -> 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, } }