use std::{fs, path::Path, sync::Arc}; use aes::Aes256; use axum::{ Json, extract::{Query, State}, http::{HeaderMap, HeaderValue, StatusCode, header::CONTENT_TYPE}, response::{IntoResponse, Response}, }; use base64::{ Engine as _, alphabet, engine::general_purpose::{GeneralPurpose, GeneralPurposeConfig, STANDARD as BASE64_STANDARD}, }; use bytes::Bytes; use cbc::cipher::{BlockDecryptMut, KeyIvInit, block_padding::NoPadding}; use ring::{ aead, rand::{SecureRandom, SystemRandom}, signature, }; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; use sha1::Sha1; use sha2::{Digest, Sha256}; use shared_contracts::runtime::{ WechatH5PaymentResponse, WechatMiniProgramPayParamsResponse, WechatNativePaymentResponse, }; use shared_kernel::offset_datetime_to_unix_micros; use std::convert::TryInto; use time::OffsetDateTime; use tracing::{info, warn}; use url::Url; use crate::{http_error::AppError, state::AppState}; const WECHAT_PAY_PROVIDER_MOCK: &str = "mock"; const WECHAT_PAY_PROVIDER_REAL: &str = "real"; const WECHAT_PAY_BODY_SIGNATURE_METHOD: &str = "WECHATPAY2-SHA256-RSA2048"; const WECHAT_PAY_PAY_SIGN_TYPE: &str = "RSA"; const WECHAT_PAY_ACCEPT_HEADER: &str = "application/json"; const WECHAT_PAY_CONTENT_TYPE_HEADER: &str = "application/json"; const WECHAT_PAY_USER_AGENT: &str = "Genarrative-WechatPay/1.0"; const WECHAT_PAY_SERIAL_HEADER: &str = "Wechatpay-Serial"; const WECHAT_PAY_SIGNATURE_TEST_PREFIX: &str = "WECHATPAY/SIGNTEST/"; const WECHAT_PAY_APP_ID_MAX_CHARS: usize = 32; const WECHAT_PAY_MCH_ID_MAX_CHARS: usize = 32; const WECHAT_PAY_DESCRIPTION_MAX_CHARS: usize = 127; const WECHAT_PAY_OUT_TRADE_NO_MAX_CHARS: usize = 32; const WECHAT_PAY_NOTIFY_URL_MAX_CHARS: usize = 255; const WECHAT_PAY_OPENID_MAX_CHARS: usize = 128; const WECHAT_PAY_CLIENT_IP_MAX_CHARS: usize = 45; const WECHAT_PAY_JSAPI_PATH: &str = "/v3/pay/transactions/jsapi"; const WECHAT_PAY_H5_PATH: &str = "/v3/pay/transactions/h5"; const WECHAT_PAY_NATIVE_PATH: &str = "/v3/pay/transactions/native"; const WECHAT_MINIPROGRAM_MESSAGE_ENCODING_AES_KEY_BYTES: usize = 43; const WECHAT_MINIPROGRAM_MESSAGE_AES_KEY_BYTES: usize = 32; const WECHAT_MINIPROGRAM_MESSAGE_RANDOM_BYTES: usize = 16; const WECHAT_MINIPROGRAM_MESSAGE_LENGTH_BYTES: usize = 4; const WECHAT_MINIPROGRAM_MESSAGE_AES_KEY_BASE64: GeneralPurpose = GeneralPurpose::new( &alphabet::STANDARD, GeneralPurposeConfig::new().with_decode_allow_trailing_bits(true), ); #[derive(Clone, Debug)] pub enum WechatPayClient { Disabled, Mock, Real(Arc), } #[derive(Clone, Debug)] pub struct RealWechatPayClient { client: reqwest::Client, app_id: String, mch_id: String, merchant_serial_no: String, private_key: Arc, platform_public_key_der: Vec, platform_serial_no: String, api_v3_key: String, notify_url: String, jsapi_endpoint: String, h5_endpoint: String, native_endpoint: String, query_order_endpoint_base: String, } #[derive(Clone, Debug)] pub struct WechatMiniProgramOrderRequest { pub order_id: String, pub description: String, pub amount_cents: u64, pub payer_openid: String, } #[derive(Clone, Debug)] pub struct WechatWebOrderRequest { pub order_id: String, pub description: String, pub amount_cents: u64, pub payer_client_ip: String, } #[derive(Clone, Debug)] pub struct WechatPayNotifyOrder { pub out_trade_no: String, pub transaction_id: Option, pub trade_state: String, pub success_time: Option, } #[derive(Clone, Debug, PartialEq, Eq)] struct WechatVirtualPaymentNotifyOrder { out_trade_no: String, transaction_id: Option, paid_at_micros: Option, event: String, } #[derive(Serialize)] pub struct WechatVirtualPaymentNotifyResponse { #[serde(rename = "ErrCode")] err_code: i32, #[serde(rename = "ErrMsg")] err_msg: String, } #[derive(Debug)] pub enum WechatPayError { Disabled, InvalidConfig(String), InvalidRequest(String), RequestFailed(String), Upstream(String), Deserialize(String), Crypto(String), InvalidSignature(String), } #[derive(Serialize)] struct WechatJsapiOrderRequest<'a> { appid: &'a str, mchid: &'a str, description: &'a str, out_trade_no: &'a str, notify_url: &'a str, amount: WechatJsapiAmount, payer: WechatJsapiPayer<'a>, } #[derive(Serialize)] struct WechatJsapiAmount { total: i64, currency: &'static str, } #[derive(Serialize)] struct WechatJsapiPayer<'a> { openid: &'a str, } #[derive(Serialize)] struct WechatH5OrderRequest<'a> { appid: &'a str, mchid: &'a str, description: &'a str, out_trade_no: &'a str, notify_url: &'a str, amount: WechatJsapiAmount, scene_info: WechatH5SceneInfo<'a>, } #[derive(Serialize)] struct WechatH5SceneInfo<'a> { payer_client_ip: &'a str, h5_info: WechatH5Info, } #[derive(Serialize)] struct WechatH5Info { #[serde(rename = "type")] kind: &'static str, } #[derive(Serialize)] struct WechatNativeOrderRequest<'a> { appid: &'a str, mchid: &'a str, description: &'a str, out_trade_no: &'a str, notify_url: &'a str, amount: WechatJsapiAmount, scene_info: WechatNativeSceneInfo<'a>, } #[derive(Serialize)] struct WechatNativeSceneInfo<'a> { payer_client_ip: &'a str, } #[derive(Deserialize)] struct WechatJsapiOrderResponse { prepay_id: Option, code: Option, message: Option, } #[derive(Deserialize)] struct WechatH5OrderResponse { h5_url: Option, code: Option, message: Option, } #[derive(Deserialize)] struct WechatNativeOrderResponse { code_url: Option, code: Option, message: Option, } #[derive(Deserialize)] struct WechatPayNotifyBody { #[serde(default)] resource: Option, } #[derive(Deserialize)] struct WechatPayNotifyResource { ciphertext: String, nonce: String, #[serde(default)] associated_data: Option, } #[derive(Deserialize)] struct WechatPayTransactionResource { out_trade_no: String, #[serde(default)] transaction_id: Option, trade_state: String, #[serde(default)] success_time: Option, } #[derive(Deserialize)] struct WechatPayQueryOrderResponse { out_trade_no: String, #[serde(default)] transaction_id: Option, trade_state: String, #[serde(default)] success_time: Option, } #[derive(Deserialize)] struct WechatVirtualPaymentNotifyBody { #[serde(rename = "Event", alias = "event")] event: String, #[serde(rename = "OutTradeNo", alias = "out_trade_no", default)] out_trade_no: Option, #[serde(rename = "MchOrderId", alias = "mch_order_id", default)] mch_order_id: Option, #[serde(rename = "WeChatPayInfo", alias = "wechat_pay_info", default)] wechat_pay_info: Option, } #[derive(Deserialize)] struct WechatVirtualPaymentNotifyPayInfo { #[serde(rename = "MchOrderNo", alias = "mch_order_no", default)] mch_order_no: Option, #[serde(rename = "TransactionId", alias = "transaction_id", default)] transaction_id: Option, #[serde(rename = "PaidTime", alias = "paid_time", default)] paid_time: Option, } #[derive(Debug, Deserialize)] pub(crate) struct WechatMiniProgramMessagePushQuery { signature: Option, timestamp: Option, nonce: Option, echostr: Option, msg_signature: Option, } #[derive(Debug, Deserialize)] struct WechatMiniProgramEncryptedMessage { #[serde(rename = "ToUserName", alias = "to_user_name", default)] to_user_name: Option, #[serde(rename = "Encrypt", alias = "encrypt")] encrypt: String, } impl WechatPayClient { pub fn from_config(config: &crate::config::AppConfig) -> Result { if !config.wechat_pay_enabled { return Ok(Self::Disabled); } if config .wechat_pay_provider .trim() .eq_ignore_ascii_case(WECHAT_PAY_PROVIDER_MOCK) { return Ok(Self::Mock); } if !config .wechat_pay_provider .trim() .eq_ignore_ascii_case(WECHAT_PAY_PROVIDER_REAL) { return Err(WechatPayError::InvalidConfig( "WECHAT_PAY_PROVIDER 仅支持 mock 或 real".to_string(), )); } let app_id = config .wechat_mini_program_app_id .as_ref() .or(config.wechat_app_id.as_ref()) .map(|value| value.trim()) .filter(|value| !value.is_empty()) .ok_or_else(|| WechatPayError::InvalidConfig("微信支付缺少小程序 AppID".to_string()))? .to_string(); let mch_id = required_config(config.wechat_pay_mch_id.as_deref(), "WECHAT_PAY_MCH_ID")?; let merchant_serial_no = required_config( config.wechat_pay_merchant_serial_no.as_deref(), "WECHAT_PAY_MERCHANT_SERIAL_NO", )?; let private_key_pem = read_private_key_pem( config.wechat_pay_private_key_pem.as_deref(), config.wechat_pay_private_key_path.as_deref(), )?; let private_key = Arc::new(parse_rsa_private_key(&private_key_pem)?); let platform_public_key_pem = read_pem( config.wechat_pay_platform_public_key_pem.as_deref(), config.wechat_pay_platform_public_key_path.as_deref(), "WECHAT_PAY_PLATFORM_PUBLIC_KEY_PEM 或 WECHAT_PAY_PLATFORM_PUBLIC_KEY_PATH 未配置", "读取微信支付平台公钥失败", )?; let platform_public_key_der = parse_public_key_pem(&platform_public_key_pem)?; let platform_serial_no = required_config( config.wechat_pay_platform_serial_no.as_deref(), "WECHAT_PAY_PLATFORM_SERIAL_NO", )?; let api_v3_key = required_config( config.wechat_pay_api_v3_key.as_deref(), "WECHAT_PAY_API_V3_KEY", )?; if api_v3_key.as_bytes().len() != 32 { return Err(WechatPayError::InvalidConfig( "WECHAT_PAY_API_V3_KEY 必须是 32 字节字符串".to_string(), )); } let notify_url = required_config( config.wechat_pay_notify_url.as_deref(), "WECHAT_PAY_NOTIFY_URL", )?; validate_notify_url(¬ify_url, "WECHAT_PAY_NOTIFY_URL")?; let jsapi_endpoint = normalize_required_url( &config.wechat_pay_jsapi_endpoint, "WECHAT_PAY_JSAPI_ENDPOINT", )?; let h5_endpoint = resolve_wechat_pay_transaction_endpoint(&jsapi_endpoint, WECHAT_PAY_H5_PATH)?; let native_endpoint = resolve_wechat_pay_transaction_endpoint(&jsapi_endpoint, WECHAT_PAY_NATIVE_PATH)?; let query_order_endpoint_base = resolve_query_order_endpoint_base(&jsapi_endpoint)?; Ok(Self::Real(Arc::new(RealWechatPayClient { client: reqwest::Client::new(), app_id, mch_id, merchant_serial_no, private_key, platform_public_key_der, platform_serial_no, api_v3_key, notify_url, jsapi_endpoint, h5_endpoint, native_endpoint, query_order_endpoint_base, }))) } pub async fn create_mini_program_order( &self, request: WechatMiniProgramOrderRequest, ) -> Result { match self { Self::Disabled => Err(WechatPayError::Disabled), Self::Mock => Ok(build_mock_pay_params(&request.order_id)), Self::Real(client) => client.create_mini_program_order(request).await, } } pub async fn create_h5_order( &self, request: WechatWebOrderRequest, ) -> Result { match self { Self::Disabled => Err(WechatPayError::Disabled), Self::Mock => Ok(build_mock_h5_payment(&request.order_id)), Self::Real(client) => client.create_h5_order(request).await, } } pub async fn create_native_order( &self, request: WechatWebOrderRequest, ) -> Result { match self { Self::Disabled => Err(WechatPayError::Disabled), Self::Mock => Ok(build_mock_native_payment(&request.order_id)), Self::Real(client) => client.create_native_order(request).await, } } pub fn parse_notify( &self, headers: &HeaderMap, body: &[u8], ) -> Result { match self { Self::Disabled => Err(WechatPayError::Disabled), Self::Mock => parse_mock_notify(body), Self::Real(client) => client.parse_notify(headers, body), } } pub async fn query_order_by_out_trade_no( &self, order_id: &str, ) -> Result { match self { Self::Disabled => Err(WechatPayError::Disabled), Self::Mock => Ok(WechatPayNotifyOrder { out_trade_no: normalize_out_trade_no(order_id)?, transaction_id: Some(format!("mock-{order_id}")), trade_state: "SUCCESS".to_string(), success_time: Some(OffsetDateTime::now_utc().to_string()), }), Self::Real(client) => client.query_order_by_out_trade_no(order_id).await, } } } impl RealWechatPayClient { async fn create_mini_program_order( &self, request: WechatMiniProgramOrderRequest, ) -> Result { validate_jsapi_order_request(self, &request)?; let amount_total = i64::try_from(request.amount_cents) .map_err(|_| WechatPayError::InvalidRequest("微信支付金额超出 i64 范围".to_string()))?; let body = serde_json::to_string(&WechatJsapiOrderRequest { appid: &self.app_id, mchid: &self.mch_id, description: &request.description, out_trade_no: &request.order_id, notify_url: &self.notify_url, amount: WechatJsapiAmount { total: amount_total, currency: "CNY", }, payer: WechatJsapiPayer { openid: &request.payer_openid, }, }) .map_err(|error| WechatPayError::Deserialize(format!("微信支付请求序列化失败:{error}")))?; let timestamp = OffsetDateTime::now_utc().unix_timestamp().to_string(); let nonce = create_nonce()?; let authorization = self.build_authorization("POST", WECHAT_PAY_JSAPI_PATH, ×tamp, &nonce, &body)?; let response = with_wechat_pay_jsapi_headers( self.client .post(&self.jsapi_endpoint) .header("Authorization", authorization), &self.platform_serial_no, ) .body(body) .send() .await .map_err(|error| { WechatPayError::RequestFailed(format!("微信支付 JSAPI 下单请求失败:{error}")) })?; let status = response.status(); let response_text = response.text().await.map_err(|error| { WechatPayError::Deserialize(format!("微信支付 JSAPI 下单响应读取失败:{error}")) })?; let payload = serde_json::from_str::(&response_text).map_err(|error| { WechatPayError::Deserialize(format!("微信支付 JSAPI 下单响应解析失败:{error}")) })?; if !status.is_success() { return Err(WechatPayError::Upstream(format!( "微信支付 JSAPI 下单失败:{}", payload .message .or(payload.code) .unwrap_or_else(|| format!("HTTP {status}")) ))); } let prepay_id = payload .prepay_id .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()) .ok_or_else(|| WechatPayError::Upstream("微信支付未返回 prepay_id".to_string()))?; self.build_pay_params(&prepay_id) } async fn create_h5_order( &self, request: WechatWebOrderRequest, ) -> Result { validate_web_order_request(self, &request)?; let amount_total = i64::try_from(request.amount_cents) .map_err(|_| WechatPayError::InvalidRequest("微信支付金额超出 i64 范围".to_string()))?; let body = serde_json::to_string(&WechatH5OrderRequest { appid: &self.app_id, mchid: &self.mch_id, description: &request.description, out_trade_no: &request.order_id, notify_url: &self.notify_url, amount: WechatJsapiAmount { total: amount_total, currency: "CNY", }, scene_info: WechatH5SceneInfo { payer_client_ip: &request.payer_client_ip, h5_info: WechatH5Info { kind: "Wap" }, }, }) .map_err(|error| { WechatPayError::Deserialize(format!("微信支付 H5 请求序列化失败:{error}")) })?; let response_text = self .post_wechat_json( &self.h5_endpoint, WECHAT_PAY_H5_PATH, body, "微信支付 H5 下单请求失败", ) .await?; let payload = serde_json::from_str::(&response_text).map_err(|error| { WechatPayError::Deserialize(format!("微信支付 H5 下单响应解析失败:{error}")) })?; let h5_url = payload .h5_url .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()) .ok_or_else(|| { WechatPayError::Upstream( payload .message .or(payload.code) .unwrap_or_else(|| "微信支付未返回 h5_url".to_string()), ) })?; Ok(WechatH5PaymentResponse { h5_url }) } async fn create_native_order( &self, request: WechatWebOrderRequest, ) -> Result { validate_web_order_request(self, &request)?; let amount_total = i64::try_from(request.amount_cents) .map_err(|_| WechatPayError::InvalidRequest("微信支付金额超出 i64 范围".to_string()))?; let body = serde_json::to_string(&WechatNativeOrderRequest { appid: &self.app_id, mchid: &self.mch_id, description: &request.description, out_trade_no: &request.order_id, notify_url: &self.notify_url, amount: WechatJsapiAmount { total: amount_total, currency: "CNY", }, scene_info: WechatNativeSceneInfo { payer_client_ip: &request.payer_client_ip, }, }) .map_err(|error| { WechatPayError::Deserialize(format!("微信支付 Native 请求序列化失败:{error}")) })?; let response_text = self .post_wechat_json( &self.native_endpoint, WECHAT_PAY_NATIVE_PATH, body, "微信支付 Native 下单请求失败", ) .await?; let payload = serde_json::from_str::(&response_text).map_err(|error| { WechatPayError::Deserialize(format!("微信支付 Native 下单响应解析失败:{error}")) })?; let code_url = payload .code_url .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()) .ok_or_else(|| { WechatPayError::Upstream( payload .message .or(payload.code) .unwrap_or_else(|| "微信支付未返回 code_url".to_string()), ) })?; Ok(WechatNativePaymentResponse { code_url }) } async fn post_wechat_json( &self, endpoint: &str, canonical_path: &str, body: String, request_error_prefix: &str, ) -> Result { let timestamp = OffsetDateTime::now_utc().unix_timestamp().to_string(); let nonce = create_nonce()?; let authorization = self.build_authorization("POST", canonical_path, ×tamp, &nonce, &body)?; let response = with_wechat_pay_json_headers( self.client .post(endpoint) .header("Authorization", authorization), &self.platform_serial_no, ) .body(body) .send() .await .map_err(|error| { WechatPayError::RequestFailed(format!("{request_error_prefix}:{error}")) })?; let status = response.status(); let response_text = response.text().await.map_err(|error| { WechatPayError::Deserialize(format!("微信支付响应读取失败:{error}")) })?; if !status.is_success() { return Err(WechatPayError::Upstream(format!( "微信支付下单失败:HTTP {status},{response_text}" ))); } Ok(response_text) } fn build_authorization( &self, method: &str, canonical_url: &str, timestamp: &str, nonce: &str, body: &str, ) -> Result { let message = format!("{method}\n{canonical_url}\n{timestamp}\n{nonce}\n{body}\n"); let signature = self.sign_message(&message)?; Ok(format!( "{WECHAT_PAY_BODY_SIGNATURE_METHOD} mchid=\"{}\",nonce_str=\"{}\",timestamp=\"{}\",serial_no=\"{}\",signature=\"{}\"", self.mch_id, nonce, timestamp, self.merchant_serial_no, signature )) } fn build_pay_params( &self, prepay_id: &str, ) -> Result { let time_stamp = OffsetDateTime::now_utc().unix_timestamp().to_string(); let nonce_str = create_nonce()?; let package = format!("prepay_id={prepay_id}"); let message = format!( "{}\n{}\n{}\n{}\n", self.app_id, time_stamp, nonce_str, package ); let pay_sign = self.sign_message(&message)?; Ok(WechatMiniProgramPayParamsResponse { time_stamp, nonce_str, package, sign_type: WECHAT_PAY_PAY_SIGN_TYPE.to_string(), pay_sign, }) } fn parse_notify( &self, headers: &HeaderMap, body: &[u8], ) -> Result { self.verify_notify_signature(headers, body)?; let notify = serde_json::from_slice::(body).map_err(|error| { WechatPayError::Deserialize(format!("微信支付通知解析失败:{error}")) })?; let resource = notify.resource.ok_or_else(|| { WechatPayError::InvalidRequest("微信支付通知缺少 resource".to_string()) })?; let plain_text = decrypt_aes_256_gcm( self.api_v3_key.as_bytes(), resource.nonce.as_bytes(), resource.associated_data.as_deref().unwrap_or("").as_bytes(), resource.ciphertext.as_str(), )?; let transaction = serde_json::from_slice::(&plain_text) .map_err(|error| { WechatPayError::Deserialize(format!("微信支付通知资源解析失败:{error}")) })?; Ok(WechatPayNotifyOrder { out_trade_no: transaction.out_trade_no, transaction_id: transaction .transaction_id .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()), trade_state: transaction.trade_state, success_time: transaction.success_time, }) } async fn query_order_by_out_trade_no( &self, order_id: &str, ) -> Result { let order_id = normalize_out_trade_no(order_id)?; let path = format!( "/v3/pay/transactions/out-trade-no/{}?mchid={}", urlencoding::encode(&order_id), urlencoding::encode(&self.mch_id), ); let request_url = format!( "{}/{}?mchid={}", self.query_order_endpoint_base.trim_end_matches('/'), urlencoding::encode(&order_id), urlencoding::encode(&self.mch_id), ); let timestamp = OffsetDateTime::now_utc().unix_timestamp().to_string(); let nonce = create_nonce()?; let authorization = self.build_authorization("GET", &path, ×tamp, &nonce, "")?; let response = with_wechat_pay_json_headers( self.client .get(request_url) .header("Authorization", authorization), &self.platform_serial_no, ) .send() .await .map_err(|error| WechatPayError::RequestFailed(format!("微信支付查单请求失败:{error}")))?; let status = response.status(); let response_text = response.text().await.map_err(|error| { WechatPayError::Deserialize(format!("微信支付查单响应读取失败:{error}")) })?; if !status.is_success() { return Err(WechatPayError::Upstream(format!( "微信支付查单失败:HTTP {status},{response_text}" ))); } let payload = serde_json::from_str::(&response_text).map_err( |error| WechatPayError::Deserialize(format!("微信支付查单响应解析失败:{error}")), )?; Ok(WechatPayNotifyOrder { out_trade_no: payload.out_trade_no, transaction_id: payload .transaction_id .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()), trade_state: payload.trade_state, success_time: payload.success_time, }) } fn verify_notify_signature( &self, headers: &HeaderMap, body: &[u8], ) -> Result<(), WechatPayError> { let timestamp = read_required_header(headers, "Wechatpay-Timestamp")?; let nonce = read_required_header(headers, "Wechatpay-Nonce")?; let signature = read_required_header(headers, "Wechatpay-Signature")?; let serial = read_required_header(headers, "Wechatpay-Serial")?; if serial != self.platform_serial_no { warn!( received_serial = serial, configured_serial = self.platform_serial_no.as_str(), "微信支付通知平台公钥序列号不匹配" ); return Err(WechatPayError::InvalidSignature(format!( "微信支付通知平台公钥序列号不匹配:received={serial}" ))); } if signature.starts_with(WECHAT_PAY_SIGNATURE_TEST_PREFIX) { warn!("收到微信支付签名探测通知"); return Err(WechatPayError::InvalidSignature( "微信支付签名探测通知".to_string(), )); } let message = build_notify_signature_message(timestamp.as_bytes(), nonce.as_bytes(), body); let signature_bytes = BASE64_STANDARD.decode(signature).map_err(|_| { WechatPayError::InvalidSignature("微信支付通知签名 base64 无效".to_string()) })?; let public_key = signature::UnparsedPublicKey::new( &signature::RSA_PKCS1_2048_8192_SHA256, &self.platform_public_key_der, ); public_key .verify(&message, &signature_bytes) .map_err(|_| WechatPayError::InvalidSignature("微信支付通知签名验签失败".to_string())) } fn sign_message(&self, message: &str) -> Result { let rng = SystemRandom::new(); let mut signature = vec![0_u8; self.private_key.public().modulus_len()]; self.private_key .sign( &signature::RSA_PKCS1_SHA256, &rng, message.as_bytes(), &mut signature, ) .map_err(|_| WechatPayError::Crypto("微信支付签名失败".to_string()))?; Ok(BASE64_STANDARD.encode(signature)) } } 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 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 resolve_wechat_message_push_verify_response( token: &str, aes_key: &str, expected_app_id: Option<&str>, query: &WechatMiniProgramMessagePushQuery, ) -> Result { let timestamp = query.timestamp.as_deref().map(str::trim).unwrap_or(""); let nonce = query.nonce.as_deref().map(str::trim).unwrap_or(""); let echostr = query.echostr.as_deref().map(str::trim).unwrap_or(""); if timestamp.is_empty() || nonce.is_empty() || echostr.is_empty() { return Err(WechatPayError::InvalidRequest( "微信消息推送校验参数不完整".to_string(), )); } let msg_signature = query .msg_signature .as_deref() .map(str::trim) .filter(|value| !value.is_empty()); if let Some(signature) = msg_signature { if !verify_wechat_message_push_signature(token, timestamp, nonce, echostr, signature) { return Err(WechatPayError::InvalidSignature( "微信消息推送 msg_signature 无效".to_string(), )); } return decrypt_wechat_message_push_ciphertext(aes_key, echostr, expected_app_id); } let signature = query .signature .as_deref() .map(str::trim) .filter(|value| !value.is_empty()) .ok_or_else(|| WechatPayError::InvalidRequest("微信消息推送校验参数不完整".to_string()))?; if !verify_wechat_message_push_signature(token, timestamp, nonce, "", signature) { return Err(WechatPayError::InvalidSignature( "微信消息推送校验签名无效".to_string(), )); } Ok(echostr.to_string()) } fn parse_wechat_mini_program_message_push_payload( body: &[u8], ) -> Result { serde_json::from_slice(body).map_err(|error| { WechatPayError::Deserialize(format!("微信消息推送 JSON 解析失败:{error}")) }) } fn verify_wechat_message_push_signature( token: &str, timestamp: &str, nonce: &str, value: &str, signature: &str, ) -> bool { let mut parts = [token, timestamp, nonce, value]; parts.sort_unstable(); let mut hasher = Sha1::new(); hasher.update(parts.join("").as_bytes()); let expected = hex::encode(hasher.finalize()); expected.eq_ignore_ascii_case(signature) } fn decrypt_wechat_message_push_ciphertext( encoding_aes_key: &str, ciphertext: &str, expected_app_id: Option<&str>, ) -> Result { let key = decode_wechat_message_push_encoding_aes_key(encoding_aes_key)?; let ciphertext = BASE64_STANDARD .decode(ciphertext.as_bytes()) .map_err(|error| { WechatPayError::Crypto(format!("微信消息推送密文 Base64 解码失败:{error}")) })?; let iv = &key[..WECHAT_MINIPROGRAM_MESSAGE_RANDOM_BYTES]; let cipher = cbc::Decryptor::::new_from_slices(&key, iv) .map_err(|error| WechatPayError::Crypto(format!("微信消息推送 AES 初始化失败:{error}")))?; let decrypted = cipher .decrypt_padded_vec_mut::(&ciphertext) .map_err(|error| WechatPayError::Crypto(format!("微信消息推送密文解密失败:{error}")))?; let plaintext = remove_wechat_message_push_pkcs7_padding(&decrypted)?; let payload = parse_wechat_message_push_plaintext(&plaintext)?; if let Some(app_id) = expected_app_id .map(str::trim) .filter(|value| !value.is_empty()) && payload.app_id != app_id { return Err(WechatPayError::InvalidSignature( "微信消息推送明文 appid 校验失败".to_string(), )); } Ok(payload.message) } fn decode_wechat_message_push_encoding_aes_key( encoding_aes_key: &str, ) -> Result, WechatPayError> { if encoding_aes_key.chars().count() != WECHAT_MINIPROGRAM_MESSAGE_ENCODING_AES_KEY_BYTES { return Err(WechatPayError::InvalidConfig(format!( "WECHAT_MINIPROGRAM_MESSAGE_ENCODING_AES_KEY 必须是 {WECHAT_MINIPROGRAM_MESSAGE_ENCODING_AES_KEY_BYTES} 位" ))); } let padded_key = format!("{encoding_aes_key}="); let key = WECHAT_MINIPROGRAM_MESSAGE_AES_KEY_BASE64 .decode(padded_key.as_bytes()) .map_err(|error| { WechatPayError::InvalidConfig(format!( "WECHAT_MINIPROGRAM_MESSAGE_ENCODING_AES_KEY Base64 解析失败:{error}" )) })?; if key.len() != WECHAT_MINIPROGRAM_MESSAGE_AES_KEY_BYTES { return Err(WechatPayError::InvalidConfig( "WECHAT_MINIPROGRAM_MESSAGE_ENCODING_AES_KEY 解码后长度必须为 32 字节".to_string(), )); } Ok(key) } fn remove_wechat_message_push_pkcs7_padding(plaintext: &[u8]) -> Result, WechatPayError> { let Some(&pad_len) = plaintext.last() else { return Err(WechatPayError::Deserialize( "微信消息推送明文为空".to_string(), )); }; let pad_len = pad_len as usize; if pad_len == 0 || pad_len > 32 || pad_len > plaintext.len() { return Err(WechatPayError::Deserialize( "微信消息推送 PKCS7 填充无效".to_string(), )); } if plaintext[plaintext.len() - pad_len..] .iter() .any(|byte| *byte as usize != pad_len) { return Err(WechatPayError::Deserialize( "微信消息推送 PKCS7 填充校验失败".to_string(), )); } Ok(plaintext[..plaintext.len() - pad_len].to_vec()) } struct WechatMessagePushPlaintext { message: String, app_id: String, } fn parse_wechat_message_push_plaintext( plaintext: &[u8], ) -> Result { if plaintext.len() < WECHAT_MINIPROGRAM_MESSAGE_LENGTH_BYTES + WECHAT_MINIPROGRAM_MESSAGE_RANDOM_BYTES + 1 { return Err(WechatPayError::Deserialize( "微信消息推送明文长度不足".to_string(), )); } let len_offset = WECHAT_MINIPROGRAM_MESSAGE_RANDOM_BYTES; let length_bytes: [u8; WECHAT_MINIPROGRAM_MESSAGE_LENGTH_BYTES] = plaintext [len_offset..len_offset + WECHAT_MINIPROGRAM_MESSAGE_LENGTH_BYTES] .try_into() .map_err(|_| WechatPayError::Deserialize("微信消息推送长度字段解析失败".to_string()))?; let message_len = u32::from_be_bytes(length_bytes) as usize; let message_start = len_offset + WECHAT_MINIPROGRAM_MESSAGE_LENGTH_BYTES; let message_end = message_start + message_len; if plaintext.len() <= message_end { return Err(WechatPayError::Deserialize( "微信消息推送明文长度与内容不匹配".to_string(), )); } let app_id_start = message_end; let message = String::from_utf8(plaintext[message_start..message_end].to_vec()).map_err(|error| { WechatPayError::Deserialize(format!("微信消息推送明文不是合法 UTF-8:{error}")) })?; let app_id = String::from_utf8(plaintext[app_id_start..plaintext.len()].to_vec()).map_err(|error| { WechatPayError::Deserialize(format!("微信消息推送 appid 不是合法 UTF-8:{error}")) })?; Ok(WechatMessagePushPlaintext { message, app_id }) } fn parse_virtual_payment_notify( body: &[u8], ) -> Result { if let Ok(notify) = serde_json::from_slice::(body) { return build_virtual_payment_notify_order( notify.event, notify.out_trade_no, notify.mch_order_id, notify.wechat_pay_info, ); } let text = std::str::from_utf8(body).map_err(|error| { WechatPayError::Deserialize(format!("微信虚拟支付推送不是合法 UTF-8:{error}")) })?; let event = extract_virtual_payment_text_value(text, "Event") .ok_or_else(|| WechatPayError::InvalidRequest("微信虚拟支付推送缺少 Event".to_string()))?; let out_trade_no = extract_virtual_payment_text_value(text, "OutTradeNo"); let mch_order_id = extract_virtual_payment_text_value(text, "MchOrderId"); let wechat_pay_info = extract_virtual_payment_block(text, "WeChatPayInfo").map(|inner| { WechatVirtualPaymentNotifyPayInfo { mch_order_no: extract_virtual_payment_text_value(&inner, "MchOrderNo"), transaction_id: extract_virtual_payment_text_value(&inner, "TransactionId"), paid_time: extract_virtual_payment_text_value(&inner, "PaidTime") .and_then(|value| value.parse::().ok()), } }); build_virtual_payment_notify_order(event, out_trade_no, mch_order_id, wechat_pay_info) } fn build_virtual_payment_notify_order( event: String, out_trade_no: Option, mch_order_id: Option, wechat_pay_info: Option, ) -> Result { let event = event.trim().to_string(); if event.is_empty() { return Err(WechatPayError::InvalidRequest( "微信虚拟支付推送缺少 Event".to_string(), )); } let out_trade_no = out_trade_no .or(mch_order_id) .or_else(|| { wechat_pay_info .as_ref() .and_then(|info| info.mch_order_no.clone()) }) .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()) .ok_or_else(|| { WechatPayError::InvalidRequest("微信虚拟支付推送缺少 OutTradeNo".to_string()) })?; let transaction_id = wechat_pay_info .as_ref() .and_then(|info| info.transaction_id.clone()) .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()); let paid_at_micros = wechat_pay_info .and_then(|info| info.paid_time) .map(|paid_time| paid_time.saturating_mul(1_000_000)); Ok(WechatVirtualPaymentNotifyOrder { out_trade_no, transaction_id, paid_at_micros, event, }) } fn extract_virtual_payment_text_value(text: &str, tag: &str) -> Option { let open = format!("<{tag}>"); let close = format!(""); let start = text.find(&open)? + open.len(); let end = text[start..].find(&close)? + start; let raw = &text[start..end]; Some(trim_virtual_payment_text_value(raw)) } fn extract_virtual_payment_block(text: &str, tag: &str) -> Option { let open = format!("<{tag}>"); let close = format!(""); let start = text.find(&open)? + open.len(); let end = text[start..].find(&close)? + start; Some(text[start..end].to_string()) } fn trim_virtual_payment_text_value(value: &str) -> String { let trimmed = value.trim(); if let Some(inner) = trimmed .strip_prefix("")) { return inner.trim().to_string(); } trimmed.to_string() } 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 with_wechat_pay_json_headers( builder: reqwest::RequestBuilder, platform_serial_no: &str, ) -> reqwest::RequestBuilder { builder .header(reqwest::header::ACCEPT, WECHAT_PAY_ACCEPT_HEADER) .header( reqwest::header::CONTENT_TYPE, WECHAT_PAY_CONTENT_TYPE_HEADER, ) .header(reqwest::header::USER_AGENT, WECHAT_PAY_USER_AGENT) .header(WECHAT_PAY_SERIAL_HEADER, platform_serial_no) } fn with_wechat_pay_jsapi_headers( builder: reqwest::RequestBuilder, platform_serial_no: &str, ) -> reqwest::RequestBuilder { with_wechat_pay_json_headers(builder, platform_serial_no) } fn build_mock_pay_params(order_id: &str) -> WechatMiniProgramPayParamsResponse { let time_stamp = OffsetDateTime::now_utc().unix_timestamp().to_string(); let nonce_str = "mock-nonce".to_string(); let package = format!("prepay_id=mock-{order_id}"); let pay_sign = hex_sha256(format!("{time_stamp}\n{nonce_str}\n{package}\n").as_bytes()); WechatMiniProgramPayParamsResponse { time_stamp, nonce_str, package, sign_type: WECHAT_PAY_PAY_SIGN_TYPE.to_string(), pay_sign, } } fn build_mock_h5_payment(order_id: &str) -> WechatH5PaymentResponse { WechatH5PaymentResponse { h5_url: format!( "https://mock.wechat-pay.local/h5?out_trade_no={}", urlencoding::encode(order_id) ), } } fn build_mock_native_payment(order_id: &str) -> WechatNativePaymentResponse { WechatNativePaymentResponse { code_url: format!( "weixin://pay.weixin.qq.com/bizpayurl/up?pr=mock-{}", hex_sha256(order_id.as_bytes()) ), } } fn parse_mock_notify(body: &[u8]) -> Result { let value = serde_json::from_slice::(body).map_err(|error| { WechatPayError::Deserialize(format!("mock 微信支付通知解析失败:{error}")) })?; Ok(WechatPayNotifyOrder { out_trade_no: value .get("outTradeNo") .or_else(|| value.get("out_trade_no")) .and_then(Value::as_str) .map(str::trim) .filter(|value| !value.is_empty()) .ok_or_else(|| { WechatPayError::InvalidRequest("mock 微信支付通知缺少 outTradeNo".to_string()) })? .to_string(), transaction_id: value .get("transactionId") .or_else(|| value.get("transaction_id")) .and_then(Value::as_str) .map(str::trim) .filter(|value| !value.is_empty()) .map(ToOwned::to_owned), trade_state: value .get("tradeState") .or_else(|| value.get("trade_state")) .and_then(Value::as_str) .unwrap_or("SUCCESS") .to_string(), success_time: value .get("successTime") .or_else(|| value.get("success_time")) .and_then(Value::as_str) .map(ToOwned::to_owned), }) } fn build_wechat_virtual_payment_notify_response( err_code: i32, err_msg: impl Into, ) -> WechatVirtualPaymentNotifyResponse { WechatVirtualPaymentNotifyResponse { err_code, err_msg: err_msg.into(), } } #[derive(Clone, Copy)] enum VirtualPaymentNotifyResponseFormat { Json, Xml, } 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, } } fn required_config(value: Option<&str>, key: &str) -> Result { value .map(str::trim) .filter(|value| !value.is_empty()) .map(ToOwned::to_owned) .ok_or_else(|| WechatPayError::InvalidConfig(format!("{key} 未配置"))) } fn normalize_required_url(value: &str, key: &str) -> Result { let value = value.trim(); if value.starts_with("https://") { return Ok(value.to_string()); } Err(WechatPayError::InvalidConfig(format!( "{key} 必须是 https 地址" ))) } fn validate_notify_url(value: &str, key: &str) -> Result<(), WechatPayError> { if value.chars().count() > WECHAT_PAY_NOTIFY_URL_MAX_CHARS { return Err(WechatPayError::InvalidConfig(format!( "{key} 不能超过 {WECHAT_PAY_NOTIFY_URL_MAX_CHARS} 字符" ))); } if value.contains('?') || value.contains('#') { return Err(WechatPayError::InvalidConfig(format!( "{key} 不能包含 query 或 fragment" ))); } Ok(()) } fn resolve_query_order_endpoint_base(jsapi_endpoint: &str) -> Result { let url = Url::parse(jsapi_endpoint) .map_err(|_| WechatPayError::InvalidConfig("WECHAT_PAY_JSAPI_ENDPOINT 无效".to_string()))?; let origin = url .origin() .ascii_serialization() .trim_end_matches('/') .to_string(); Ok(format!("{origin}/v3/pay/transactions/out-trade-no")) } fn resolve_wechat_pay_transaction_endpoint( jsapi_endpoint: &str, transaction_path: &str, ) -> Result { let url = Url::parse(jsapi_endpoint) .map_err(|_| WechatPayError::InvalidConfig("WECHAT_PAY_JSAPI_ENDPOINT 无效".to_string()))?; let origin = url .origin() .ascii_serialization() .trim_end_matches('/') .to_string(); Ok(format!("{origin}{transaction_path}")) } fn normalize_out_trade_no(value: &str) -> Result { let value = value.trim(); validate_out_trade_no(value)?; Ok(value.to_string()) } fn validate_jsapi_order_request( client: &RealWechatPayClient, request: &WechatMiniProgramOrderRequest, ) -> Result<(), WechatPayError> { validate_non_empty_max_chars( &client.app_id, WECHAT_PAY_APP_ID_MAX_CHARS, "微信支付 appid", )?; if !client.app_id.starts_with("wx") { return Err(WechatPayError::InvalidConfig( "微信支付 appid 必须使用小程序 AppID".to_string(), )); } validate_non_empty_max_chars( &client.mch_id, WECHAT_PAY_MCH_ID_MAX_CHARS, "微信支付 mchid", )?; if !client.mch_id.chars().all(|ch| ch.is_ascii_digit()) { return Err(WechatPayError::InvalidConfig( "微信支付 mchid 必须是数字字符串".to_string(), )); } validate_non_empty_max_chars( &request.description, WECHAT_PAY_DESCRIPTION_MAX_CHARS, "微信支付商品描述", )?; validate_out_trade_no(&request.order_id)?; if request.amount_cents == 0 { return Err(WechatPayError::InvalidRequest( "微信支付金额必须大于 0 分".to_string(), )); } validate_non_empty_max_chars( &request.payer_openid, WECHAT_PAY_OPENID_MAX_CHARS, "微信支付 payer.openid", )?; Ok(()) } fn validate_web_order_request( client: &RealWechatPayClient, request: &WechatWebOrderRequest, ) -> Result<(), WechatPayError> { validate_non_empty_max_chars( &client.app_id, WECHAT_PAY_APP_ID_MAX_CHARS, "微信支付 appid", )?; if !client.app_id.starts_with("wx") { return Err(WechatPayError::InvalidConfig( "微信支付 appid 必须使用已绑定的微信 AppID".to_string(), )); } validate_non_empty_max_chars( &client.mch_id, WECHAT_PAY_MCH_ID_MAX_CHARS, "微信支付 mchid", )?; if !client.mch_id.chars().all(|ch| ch.is_ascii_digit()) { return Err(WechatPayError::InvalidConfig( "微信支付 mchid 必须是数字字符串".to_string(), )); } validate_non_empty_max_chars( &request.description, WECHAT_PAY_DESCRIPTION_MAX_CHARS, "微信支付商品描述", )?; validate_out_trade_no(&request.order_id)?; if request.amount_cents == 0 { return Err(WechatPayError::InvalidRequest( "微信支付金额必须大于 0 分".to_string(), )); } validate_non_empty_max_chars( &request.payer_client_ip, WECHAT_PAY_CLIENT_IP_MAX_CHARS, "微信支付 payer_client_ip", )?; Ok(()) } fn validate_non_empty_max_chars( value: &str, max_chars: usize, field_name: &str, ) -> Result<(), WechatPayError> { let value = value.trim(); if value.is_empty() { return Err(WechatPayError::InvalidRequest(format!( "{field_name} 不能为空" ))); } if value.chars().count() > max_chars { return Err(WechatPayError::InvalidRequest(format!( "{field_name} 不能超过 {max_chars} 字符" ))); } Ok(()) } fn validate_out_trade_no(value: &str) -> Result<(), WechatPayError> { validate_non_empty_max_chars( value, WECHAT_PAY_OUT_TRADE_NO_MAX_CHARS, "微信支付 out_trade_no", )?; if value.chars().count() < 6 { return Err(WechatPayError::InvalidRequest( "微信支付 out_trade_no 不能少于 6 字符".to_string(), )); } if !value .chars() .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '|' | '*')) { return Err(WechatPayError::InvalidRequest( "微信支付 out_trade_no 只能包含数字、大小写字母、_、-、|、*".to_string(), )); } Ok(()) } fn read_private_key_pem( inline_pem: Option<&str>, path: Option<&Path>, ) -> Result { read_pem( inline_pem, path, "WECHAT_PAY_PRIVATE_KEY_PEM 或 WECHAT_PAY_PRIVATE_KEY_PATH 未配置", "读取微信支付私钥失败", ) } fn read_pem( inline_pem: Option<&str>, path: Option<&Path>, missing_message: &str, read_error_prefix: &str, ) -> Result { if let Some(value) = inline_pem.map(str::trim).filter(|value| !value.is_empty()) { return Ok(value.replace("\\n", "\n")); } let Some(path) = path else { return Err(WechatPayError::InvalidConfig(missing_message.to_string())); }; fs::read_to_string(path).map_err(|error| { WechatPayError::InvalidConfig(format!("{read_error_prefix}:{}:{error}", path.display())) }) } fn parse_rsa_private_key(pem: &str) -> Result { let (label, der) = parse_single_pem_block(pem)?; match label.as_str() { "PRIVATE KEY" => signature::RsaKeyPair::from_pkcs8(&der), "RSA PRIVATE KEY" => signature::RsaKeyPair::from_der(&der), _ => { return Err(WechatPayError::InvalidConfig( "微信支付私钥必须是 PRIVATE KEY 或 RSA PRIVATE KEY PEM".to_string(), )); } } .map_err(|error| WechatPayError::InvalidConfig(format!("微信支付私钥解析失败:{error}"))) } fn parse_public_key_pem(pem: &str) -> Result, WechatPayError> { let (label, der) = parse_single_pem_block(pem)?; if label != "PUBLIC KEY" { return Err(WechatPayError::InvalidConfig( "微信支付平台公钥必须是 PUBLIC KEY PEM".to_string(), )); } Ok(der) } fn parse_single_pem_block(pem: &str) -> Result<(String, Vec), WechatPayError> { let mut label: Option = None; let mut content = String::new(); for line in pem.lines().map(str::trim).filter(|line| !line.is_empty()) { if let Some(raw_label) = line .strip_prefix("-----BEGIN ") .and_then(|value| value.strip_suffix("-----")) { label = Some(raw_label.trim().to_string()); continue; } if line.starts_with("-----END ") { break; } if label.is_some() { content.push_str(line); } } let label = label .ok_or_else(|| WechatPayError::InvalidConfig("微信支付 PEM 缺少 BEGIN 标记".to_string()))?; let der = BASE64_STANDARD .decode(content) .map_err(|_| WechatPayError::InvalidConfig("微信支付 PEM base64 无效".to_string()))?; if der.is_empty() { return Err(WechatPayError::InvalidConfig( "微信支付 PEM 内容为空".to_string(), )); } Ok((label, der)) } fn create_nonce() -> Result { let mut bytes = [0_u8; 16]; SystemRandom::new() .fill(&mut bytes) .map_err(|_| WechatPayError::Crypto("生成微信支付 nonce 失败".to_string()))?; Ok(hex_encode(&bytes)) } fn decrypt_aes_256_gcm( key: &[u8], nonce: &[u8], associated_data: &[u8], ciphertext_base64: &str, ) -> Result, WechatPayError> { let mut ciphertext = BASE64_STANDARD .decode(ciphertext_base64) .map_err(|_| WechatPayError::Crypto("微信支付通知密文 base64 无效".to_string()))?; if ciphertext.len() < aead::AES_256_GCM.tag_len() { return Err(WechatPayError::Crypto( "微信支付通知密文长度无效".to_string(), )); } let nonce = aead::Nonce::try_assume_unique_for_key(nonce) .map_err(|_| WechatPayError::Crypto("微信支付通知 nonce 长度无效".to_string()))?; let key = aead::UnboundKey::new(&aead::AES_256_GCM, key) .map_err(|_| WechatPayError::Crypto("微信支付通知解密 key 无效".to_string()))?; let plain_text = aead::LessSafeKey::new(key) .open_in_place( nonce, aead::Aad::from(associated_data), ciphertext.as_mut_slice(), ) .map_err(|_| WechatPayError::Crypto("微信支付通知认证或解密失败".to_string()))?; Ok(plain_text.to_vec()) } fn read_required_header<'a>( headers: &'a HeaderMap, name: &'static str, ) -> Result<&'a str, WechatPayError> { headers .get(name) .and_then(|value| value.to_str().ok()) .map(str::trim) .filter(|value| !value.is_empty()) .ok_or_else(|| WechatPayError::InvalidSignature(format!("微信支付通知缺少 {name} 请求头"))) } fn build_notify_signature_message(timestamp: &[u8], nonce: &[u8], body: &[u8]) -> Vec { let mut message = Vec::with_capacity(timestamp.len() + nonce.len() + body.len() + 3); message.extend_from_slice(timestamp); message.push(b'\n'); message.extend_from_slice(nonce); message.push(b'\n'); message.extend_from_slice(body); message.push(b'\n'); message } fn hex_sha256(content: &[u8]) -> String { let mut hasher = Sha256::new(); hasher.update(content); hex_encode(&hasher.finalize()) } fn hex_encode(bytes: &[u8]) -> String { bytes.iter().map(|byte| format!("{byte:02x}")).collect() } impl std::fmt::Display for WechatPayError { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Disabled => formatter.write_str("微信支付暂未启用"), Self::InvalidConfig(message) | Self::InvalidRequest(message) | Self::RequestFailed(message) | Self::Upstream(message) | Self::Deserialize(message) | Self::Crypto(message) => formatter.write_str(message), Self::InvalidSignature(message) => formatter.write_str(message), } } } impl std::error::Error for WechatPayError {} #[cfg(test)] mod tests { use super::*; use cbc::cipher::{BlockEncryptMut, block_padding::NoPadding}; #[test] fn mock_pay_params_use_request_payment_shape() { let params = build_mock_pay_params("recharge:user:1:points_60"); assert!(!params.time_stamp.is_empty()); assert_eq!(params.sign_type, "RSA"); assert!(params.package.starts_with("prepay_id=mock-")); assert!(!params.pay_sign.is_empty()); } #[test] fn jsapi_order_request_uses_wechat_v3_snake_case_fields() { let body = serde_json::to_value(WechatJsapiOrderRequest { appid: "wx-test-app", mchid: "1900000001", description: "陶泥儿 - 60泥点", out_trade_no: "rcgtest001", notify_url: "https://api.example.com/api/profile/recharge/wechat/notify", amount: WechatJsapiAmount { total: 600, currency: "CNY", }, payer: WechatJsapiPayer { openid: "openid-test", }, }) .expect("JSAPI order request should serialize"); assert_eq!(body["out_trade_no"], "rcgtest001"); assert_eq!( body["notify_url"], "https://api.example.com/api/profile/recharge/wechat/notify" ); assert!(body.get("outTradeNo").is_none()); assert!(body.get("notifyUrl").is_none()); } #[test] fn h5_order_request_uses_wechat_required_scene_info() { let body = serde_json::to_value(WechatH5OrderRequest { appid: "wx-test-app", mchid: "1900000001", description: "陶泥儿 - 60泥点", out_trade_no: "rcgtest001", notify_url: "https://api.example.com/api/profile/recharge/wechat/notify", amount: WechatJsapiAmount { total: 600, currency: "CNY", }, scene_info: WechatH5SceneInfo { payer_client_ip: "203.0.113.10", h5_info: WechatH5Info { kind: "Wap" }, }, }) .expect("H5 order request should serialize"); assert_eq!(body["scene_info"]["payer_client_ip"], "203.0.113.10"); assert_eq!(body["scene_info"]["h5_info"]["type"], "Wap"); assert_eq!(body["amount"]["currency"], "CNY"); assert!(body.get("sceneInfo").is_none()); assert!(body["scene_info"].get("payerClientIp").is_none()); } #[test] fn native_order_request_uses_code_url_response_shape() { let body = serde_json::to_value(WechatNativeOrderRequest { appid: "wx-test-app", mchid: "1900000001", description: "陶泥儿 - 60泥点", out_trade_no: "rcgtest001", notify_url: "https://api.example.com/api/profile/recharge/wechat/notify", amount: WechatJsapiAmount { total: 600, currency: "CNY", }, scene_info: WechatNativeSceneInfo { payer_client_ip: "203.0.113.10", }, }) .expect("Native order request should serialize"); let response = serde_json::from_value::(json!({ "code_url": "weixin://pay.weixin.qq.com/bizpayurl/up?pr=test" })) .expect("Native order response should deserialize"); assert_eq!(body["scene_info"]["payer_client_ip"], "203.0.113.10"); assert_eq!( response.code_url.as_deref(), Some("weixin://pay.weixin.qq.com/bizpayurl/up?pr=test") ); } #[test] fn transaction_endpoints_reuse_configured_wechat_pay_origin() { let h5_endpoint = resolve_wechat_pay_transaction_endpoint( "https://pay-gateway.example.com/v3/pay/transactions/jsapi", WECHAT_PAY_H5_PATH, ) .expect("H5 endpoint should resolve"); let native_endpoint = resolve_wechat_pay_transaction_endpoint( "https://pay-gateway.example.com/v3/pay/transactions/jsapi", WECHAT_PAY_NATIVE_PATH, ) .expect("Native endpoint should resolve"); assert_eq!( h5_endpoint, "https://pay-gateway.example.com/v3/pay/transactions/h5" ); assert_eq!( native_endpoint, "https://pay-gateway.example.com/v3/pay/transactions/native" ); } #[test] fn jsapi_order_request_rejects_provider_field_limit_violations() { assert!(validate_out_trade_no("abc12").is_err()); assert!(validate_out_trade_no("abc123").is_ok()); assert!(validate_out_trade_no("abc123_-|*").is_ok()); assert!(validate_out_trade_no("abc123中文").is_err()); assert!(validate_out_trade_no("a".repeat(33).as_str()).is_err()); assert!(validate_notify_url("https://api.example.com/pay/notify", "notify").is_ok()); assert!(validate_notify_url("https://api.example.com/pay/notify?x=1", "notify").is_err()); assert!(validate_notify_url(&format!("https://{}", "a".repeat(248)), "notify").is_err()); validate_non_empty_max_chars("陶泥儿 - 60泥点", WECHAT_PAY_DESCRIPTION_MAX_CHARS, "描述") .expect("short description should pass"); assert!( validate_non_empty_max_chars( &"泥".repeat(128), WECHAT_PAY_DESCRIPTION_MAX_CHARS, "描述" ) .is_err() ); validate_non_empty_max_chars("openid-test", WECHAT_PAY_OPENID_MAX_CHARS, "openid") .expect("short openid should pass"); assert!( validate_non_empty_max_chars(&"o".repeat(129), WECHAT_PAY_OPENID_MAX_CHARS, "openid") .is_err() ); validate_non_empty_max_chars( "203.0.113.10", WECHAT_PAY_CLIENT_IP_MAX_CHARS, "payer_client_ip", ) .expect("short client ip should pass"); assert!( validate_non_empty_max_chars( &"1".repeat(46), WECHAT_PAY_CLIENT_IP_MAX_CHARS, "payer_client_ip", ) .is_err() ); } #[test] fn jsapi_order_request_sets_wechat_required_http_headers() { let request = with_wechat_pay_jsapi_headers( reqwest::Client::new() .post("https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi") .header( "Authorization", "WECHATPAY2-SHA256-RSA2048 mchid=\"1900000001\"", ), "PUB_KEY_ID_0119000000012026051400000000000001", ) .build() .expect("request should build"); let headers = request.headers(); assert_eq!( headers .get(reqwest::header::ACCEPT) .and_then(|value| value.to_str().ok()), Some(WECHAT_PAY_ACCEPT_HEADER) ); assert_eq!( headers .get(reqwest::header::CONTENT_TYPE) .and_then(|value| value.to_str().ok()), Some(WECHAT_PAY_CONTENT_TYPE_HEADER) ); assert_eq!( headers .get(reqwest::header::USER_AGENT) .and_then(|value| value.to_str().ok()), Some(WECHAT_PAY_USER_AGENT) ); assert_eq!( headers .get(WECHAT_PAY_SERIAL_HEADER) .and_then(|value| value.to_str().ok()), Some("PUB_KEY_ID_0119000000012026051400000000000001") ); } #[test] fn notify_signature_message_preserves_raw_body_bytes() { let body = b"{\"message\":\"hello\\r\\nworld\"}\r\n"; let message = build_notify_signature_message(b"1778759600", b"nonce-1", body); assert_eq!( message, b"1778759600\nnonce-1\n{\"message\":\"hello\\r\\nworld\"}\r\n\n".to_vec() ); } #[test] fn parse_mock_notify_defaults_success_state() { let notify = parse_mock_notify(br#"{"outTradeNo":"order-1"}"#).expect("mock notify should parse"); assert_eq!(notify.out_trade_no, "order-1"); assert_eq!(notify.transaction_id, None); assert_eq!(notify.trade_state, "SUCCESS"); } #[test] fn parse_virtual_payment_notify_supports_goods_event_json() { let notify = parse_virtual_payment_notify( br#"{"Event":"xpay_goods_deliver_notify","OutTradeNo":"order-1","WeChatPayInfo":{"TransactionId":"wx-1","PaidTime":1710000000}}"#, ) .expect("virtual payment notify should parse"); assert_eq!(notify.event, "xpay_goods_deliver_notify"); assert_eq!(notify.out_trade_no, "order-1"); assert_eq!(notify.transaction_id.as_deref(), Some("wx-1")); assert_eq!(notify.paid_at_micros, Some(1_710_000_000_000_000)); } #[test] fn parse_virtual_payment_notify_supports_coin_event_xml() { let notify = parse_virtual_payment_notify( br#"1710000001"#, ) .expect("virtual payment xml notify should parse"); assert_eq!(notify.event, "xpay_coin_pay_notify"); assert_eq!(notify.out_trade_no, "order-2"); assert_eq!(notify.transaction_id.as_deref(), Some("wx-2")); assert_eq!(notify.paid_at_micros, Some(1_710_000_001_000_000)); } #[test] fn parse_virtual_payment_notify_rejects_missing_order_no() { let error = parse_virtual_payment_notify(br#"{"Event":"xpay_goods_deliver_notify"}"#) .expect_err("missing order id should fail"); match error { WechatPayError::InvalidRequest(message) => { assert!(message.contains("OutTradeNo")); } other => panic!("unexpected error: {other:?}"), } } #[test] fn decode_wechat_message_push_encoding_aes_key_allows_trailing_bits() { let canonical_key = BASE64_STANDARD.encode([0u8; 32]); let mut encoding_aes_key = canonical_key.trim_end_matches('=').to_string(); encoding_aes_key.replace_range(encoding_aes_key.len() - 1.., "B"); let decoded = decode_wechat_message_push_encoding_aes_key(&encoding_aes_key) .expect("wechat aes key with trailing bits should decode"); assert_eq!(decoded, vec![0u8; WECHAT_MINIPROGRAM_MESSAGE_AES_KEY_BYTES]); } #[test] fn wechat_message_push_signature_uses_sorted_sha1_parts() { let token = "token-1"; let timestamp = "1710000000"; let nonce = "nonce-1"; let encrypt = "encrypted-payload"; let signature = build_wechat_message_push_test_signature(token, timestamp, nonce, encrypt); assert!(verify_wechat_message_push_signature( token, timestamp, nonce, encrypt, &signature )); assert!(!verify_wechat_message_push_signature( token, timestamp, nonce, "tampered-payload", &signature )); } #[test] fn wechat_message_push_plain_get_verify_returns_echostr() { let token = "AAAAA"; let timestamp = "1714036504"; let nonce = "1514711492"; let echostr = "4375120948345356249"; let signature = "f464b24fc39322e44b38aa78f5edd27bd1441696"; let plaintext = resolve_wechat_message_push_verify_response( token, "unused-aes-key", Some("wx-test-app"), &WechatMiniProgramMessagePushQuery { signature: Some(signature.to_string()), timestamp: Some(timestamp.to_string()), nonce: Some(nonce.to_string()), echostr: Some(echostr.to_string()), msg_signature: None, }, ) .expect("plain url verification should return echostr"); assert_eq!(plaintext, echostr); } #[test] fn wechat_message_push_decrypts_safe_mode_ciphertext() { let app_id = "wx-test-app"; let message = r#"{"Event":"xpay_coin_pay_notify","OutTradeNo":"order-1"}"#; let encoding_aes_key = build_wechat_message_push_test_encoding_aes_key(); let encrypted = encrypt_wechat_message_push_test_ciphertext(&encoding_aes_key, message, app_id); let decrypted = decrypt_wechat_message_push_ciphertext(&encoding_aes_key, &encrypted, Some(app_id)) .expect("encrypted message should decrypt"); assert_eq!(decrypted, message); } #[test] fn wechat_message_push_rejects_mismatched_app_id() { let encoding_aes_key = build_wechat_message_push_test_encoding_aes_key(); let encrypted = encrypt_wechat_message_push_test_ciphertext( &encoding_aes_key, r#"{"Event":"xpay_coin_pay_notify","OutTradeNo":"order-1"}"#, "wx-real-app", ); let error = decrypt_wechat_message_push_ciphertext(&encoding_aes_key, &encrypted, Some("wx-other")) .expect_err("mismatched app id should fail"); match error { WechatPayError::InvalidSignature(message) => { assert!(message.contains("appid")); } other => panic!("unexpected error: {other:?}"), } } fn build_wechat_message_push_test_signature( token: &str, timestamp: &str, nonce: &str, value: &str, ) -> String { let mut parts = [token, timestamp, nonce, value]; parts.sort_unstable(); let mut hasher = Sha1::new(); hasher.update(parts.join("").as_bytes()); hex::encode(hasher.finalize()) } fn build_wechat_message_push_test_encoding_aes_key() -> String { let raw_key = std::array::from_fn::<_, 32, _>(|index| index as u8); BASE64_STANDARD .encode(raw_key) .trim_end_matches('=') .to_string() } fn encrypt_wechat_message_push_test_ciphertext( encoding_aes_key: &str, message: &str, app_id: &str, ) -> String { let key = decode_wechat_message_push_encoding_aes_key(encoding_aes_key) .expect("test aes key should decode"); let mut plaintext = Vec::new(); plaintext.extend_from_slice(b"0123456789abcdef"); plaintext.extend_from_slice(&(message.as_bytes().len() as u32).to_be_bytes()); plaintext.extend_from_slice(message.as_bytes()); plaintext.extend_from_slice(app_id.as_bytes()); let pad_len = 32 - (plaintext.len() % 32); plaintext.extend(std::iter::repeat(pad_len as u8).take(pad_len)); let iv = &key[..WECHAT_MINIPROGRAM_MESSAGE_RANDOM_BYTES]; let cipher = cbc::Encryptor::::new_from_slices(&key, iv) .expect("test aes cipher should init"); let encrypted = cipher.encrypt_padded_vec_mut::(&plaintext); BASE64_STANDARD.encode(encrypted) } }