feat: 接入微信小程序支付
This commit is contained in:
780
server-rs/crates/api-server/src/wechat_pay.rs
Normal file
780
server-rs/crates/api-server/src/wechat_pay.rs
Normal file
@@ -0,0 +1,780 @@
|
||||
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 = "<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>";
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum WechatPayClient {
|
||||
Disabled,
|
||||
Mock,
|
||||
Real(Arc<RealWechatPayClient>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RealWechatPayClient {
|
||||
client: reqwest::Client,
|
||||
app_id: String,
|
||||
mch_id: String,
|
||||
merchant_serial_no: String,
|
||||
private_key: Arc<signature::RsaKeyPair>,
|
||||
platform_public_key_der: Vec<u8>,
|
||||
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<String>,
|
||||
pub trade_state: String,
|
||||
pub success_time: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum WechatPayError {
|
||||
Disabled,
|
||||
InvalidConfig(String),
|
||||
InvalidRequest(String),
|
||||
RequestFailed(String),
|
||||
Upstream(String),
|
||||
Deserialize(String),
|
||||
Crypto(String),
|
||||
InvalidSignature,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
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<String>,
|
||||
code: Option<String>,
|
||||
message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct WechatPayNotifyBody {
|
||||
#[serde(default)]
|
||||
resource: Option<WechatPayNotifyResource>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct WechatPayNotifyResource {
|
||||
ciphertext: String,
|
||||
nonce: String,
|
||||
#[serde(default)]
|
||||
associated_data: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct WechatPayTransactionResource {
|
||||
out_trade_no: String,
|
||||
#[serde(default)]
|
||||
transaction_id: Option<String>,
|
||||
trade_state: String,
|
||||
#[serde(default)]
|
||||
success_time: Option<String>,
|
||||
}
|
||||
|
||||
impl WechatPayClient {
|
||||
pub fn from_config(config: &crate::config::AppConfig) -> Result<Self, WechatPayError> {
|
||||
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",
|
||||
)?;
|
||||
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<WechatMiniProgramPayParamsResponse, WechatPayError> {
|
||||
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<WechatPayNotifyOrder, WechatPayError> {
|
||||
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<WechatMiniProgramPayParamsResponse, WechatPayError> {
|
||||
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::<WechatJsapiOrderResponse>(&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<String, WechatPayError> {
|
||||
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<WechatMiniProgramPayParamsResponse, WechatPayError> {
|
||||
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<WechatPayNotifyOrder, WechatPayError> {
|
||||
self.verify_notify_signature(headers, body)?;
|
||||
let notify = serde_json::from_slice::<WechatPayNotifyBody>(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::<WechatPayTransactionResource>(&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<String, WechatPayError> {
|
||||
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<AppState>,
|
||||
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<WechatPayNotifyOrder, WechatPayError> {
|
||||
let value = serde_json::from_slice::<Value>(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<String, WechatPayError> {
|
||||
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<String, WechatPayError> {
|
||||
let value = value.trim();
|
||||
if value.starts_with("https://") {
|
||||
return Ok(value.to_string());
|
||||
}
|
||||
|
||||
Err(WechatPayError::InvalidConfig(format!(
|
||||
"{key} 必须是 https 地址"
|
||||
)))
|
||||
}
|
||||
|
||||
fn read_private_key_pem(
|
||||
inline_pem: Option<&str>,
|
||||
path: Option<&Path>,
|
||||
) -> Result<String, WechatPayError> {
|
||||
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<String, WechatPayError> {
|
||||
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<signature::RsaKeyPair, WechatPayError> {
|
||||
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<Vec<u8>, 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<u8>), WechatPayError> {
|
||||
let mut label: Option<String> = 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<String, WechatPayError> {
|
||||
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<Vec<u8>, 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 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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user