use std::{fs, path::Path, sync::Arc}; use axum::{ extract::State, http::{HeaderMap, StatusCode}, }; use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD}; use bytes::Bytes; use ring::{ aead, rand::{SecureRandom, SystemRandom}, signature, }; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; use sha2::{Digest, Sha256}; use shared_contracts::runtime::WechatMiniProgramPayParamsResponse; use shared_kernel::offset_datetime_to_unix_micros; use time::OffsetDateTime; use tracing::{info, warn}; 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_NOTIFY_SUCCESS: &str = ""; 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; #[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, } #[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 WechatPayNotifyOrder { pub out_trade_no: String, pub transaction_id: Option, pub trade_state: String, pub success_time: Option, } #[derive(Debug)] pub enum WechatPayError { Disabled, InvalidConfig(String), InvalidRequest(String), RequestFailed(String), Upstream(String), Deserialize(String), Crypto(String), InvalidSignature, } #[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(Deserialize)] struct WechatJsapiOrderResponse { prepay_id: 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, } 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", )?; 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, }))) } 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 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), } } } 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", "/v3/pay/transactions/jsapi", ×tamp, &nonce, &body, )?; let response = self .client .post(&self.jsapi_endpoint) .header("Authorization", authorization) .header("Accept", "application/json") .header("Content-Type", "application/json") .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) } 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, }) } 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 { return Err(WechatPayError::InvalidSignature); } let message = format!( "{}\n{}\n{}\n", timestamp, nonce, String::from_utf8_lossy(body) ); let signature_bytes = BASE64_STANDARD .decode(signature) .map_err(|_| WechatPayError::InvalidSignature)?; let public_key = signature::UnparsedPublicKey::new( &signature::RSA_PKCS1_2048_8192_SHA256, &self.platform_public_key_der, ); public_key .verify(message.as_bytes(), &signature_bytes) .map_err(|_| WechatPayError::InvalidSignature) } 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<&'static str, 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(WECHAT_PAY_NOTIFY_SUCCESS); } 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(WECHAT_PAY_NOTIFY_SUCCESS) } 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 => AppError::from_status(StatusCode::UNAUTHORIZED) .with_message("微信支付通知签名无效") .with_details(json!({ "provider": "wechat_pay" })), } } 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 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 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 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 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 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_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(WechatPayError::InvalidSignature) } 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 => formatter.write_str("微信支付通知签名无效"), } } } impl std::error::Error for WechatPayError {} #[cfg(test)] mod tests { use super::*; #[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 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() ); } #[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"); } }