use crate::AppState; use aes_gcm::{ aead::{Aead, Payload}, Aes256Gcm, KeyInit, Nonce, }; use axum::http; use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _}; use chrono::Utc; use rsa::pkcs1::{DecodeRsaPrivateKey, DecodeRsaPublicKey}; use rsa::pkcs1v15::{ Signature as RsaPkcs1v15Signature, SigningKey as RsaPkcs1v15SigningKey, VerifyingKey as RsaPkcs1v15VerifyingKey, }; use rsa::pkcs8::{DecodePrivateKey, DecodePublicKey}; use rsa::signature::{SignatureEncoding, Signer, Verifier}; use rsa::{RsaPrivateKey, RsaPublicKey}; use serde_json::{json, Value}; use sha2::{Digest, Sha256}; use std::collections::BTreeMap; const ALIPAY_DEFAULT_GATEWAY_URL: &str = "https://openapi.alipay.com/gateway.do"; const WXPAY_DEFAULT_BASE_URL: &str = "https://api.mch.weixin.qq.com"; const WXPAY_NOTIFY_SUCCESS: &str = "TRANSACTION.SUCCESS"; const WXPAY_TRADE_SUCCESS: &str = "SUCCESS"; const WXPAY_CURRENCY: &str = "CNY"; #[derive(Debug, Clone)] pub(crate) struct DirectPaymentCheckoutInput { pub(crate) payment_channel: String, pub(crate) display_name: String, pub(crate) order_no: String, pub(crate) subject: String, pub(crate) pay_amount: f64, pub(crate) pay_currency: String, pub(crate) notify_url: String, pub(crate) return_url: Option, pub(crate) client_ip: Option, pub(crate) expires_at: chrono::DateTime, } #[derive(Debug, Clone)] pub(crate) struct DirectGatewayRefundResult { pub(crate) gateway_refund_id: String, pub(crate) status: String, pub(crate) payload: Value, } #[derive(Debug, Clone)] struct DirectGatewayConfig { record: aether_data_contracts::repository::billing::PaymentGatewayConfigRecord, config: serde_json::Map, secrets: serde_json::Map, } fn payment_payload_hash(payload: &Value) -> Result { let encoded = serde_json::to_vec(payload) .map_err(|err| format!("payment callback payload encode failed: {err}"))?; let digest = Sha256::digest(&encoded); Ok(digest.iter().map(|byte| format!("{byte:02x}")).collect()) } fn stripe_minor_unit_amount(pay_amount: f64, pay_currency: &str) -> Result { let currency = pay_currency.trim().to_ascii_lowercase(); let multiplier = match currency.as_str() { "bif" | "clp" | "djf" | "gnf" | "jpy" | "kmf" | "krw" | "mga" | "pyg" | "rwf" | "ugx" | "vnd" | "vuv" | "xaf" | "xof" | "xpf" => 1.0, _ => 100.0, }; let amount = (pay_amount * multiplier).round(); if !amount.is_finite() || amount <= 0.0 { return Err("Stripe 支付金额无效".to_string()); } Ok(amount as i64) } fn config_string(config: &serde_json::Map, key: &str) -> Option { config .get(key) .and_then(Value::as_str) .map(str::trim) .filter(|value| !value.is_empty()) .map(ToOwned::to_owned) } fn secret_string(secrets: &serde_json::Map, key: &str) -> Option { secrets .get(key) .and_then(Value::as_str) .map(str::trim) .filter(|value| !value.is_empty()) .map(ToOwned::to_owned) } fn record_config_map( record: &aether_data_contracts::repository::billing::PaymentGatewayConfigRecord, ) -> serde_json::Map { super::payment_gateway_config_json(&record.channels_json) .as_object() .cloned() .unwrap_or_default() } async fn load_direct_gateway_config( state: &AppState, provider: &str, ) -> Result { let record = state .find_payment_gateway_config(provider) .await .map_err(|err| format!("{provider} 配置读取失败: {err:?}"))? .ok_or_else(|| format!("{provider} 未配置"))?; if !record.enabled { return Err(format!("{provider} 未启用")); } let Some(encrypted) = record.merchant_key_encrypted.as_deref() else { return Err(format!("{provider} 密钥未配置")); }; let Some(plaintext) = super::decrypt_catalog_secret_with_fallbacks(state.encryption_key(), encrypted) else { return Err(format!("{provider} 密钥解密失败")); }; let secrets = serde_json::from_str::(&plaintext) .ok() .and_then(|value| value.as_object().cloned()) .ok_or_else(|| format!("{provider} 密钥格式无效"))?; let config = record_config_map(&record); Ok(DirectGatewayConfig { record, config, secrets, }) } fn pem_candidates(raw: &str, labels: &[&str]) -> Vec { let trimmed = raw.trim(); if trimmed.starts_with("-----BEGIN") { return vec![trimmed.to_string()]; } labels .iter() .map(|label| format!("-----BEGIN {label}-----\n{trimmed}\n-----END {label}-----")) .collect() } fn decode_rsa_private_key(raw: &str) -> Result { let mut errors = Vec::new(); for candidate in pem_candidates(raw, &["PRIVATE KEY", "RSA PRIVATE KEY"]) { match RsaPrivateKey::from_pkcs8_pem(&candidate) { Ok(key) => return Ok(key), Err(err) => errors.push(format!("pkcs8: {err}")), } match RsaPrivateKey::from_pkcs1_pem(&candidate) { Ok(key) => return Ok(key), Err(err) => errors.push(format!("pkcs1: {err}")), } } Err(format!("RSA 私钥解析失败: {}", errors.join("; "))) } fn decode_rsa_public_key(raw: &str) -> Result { let mut errors = Vec::new(); for candidate in pem_candidates(raw, &["PUBLIC KEY", "RSA PUBLIC KEY"]) { match RsaPublicKey::from_public_key_pem(&candidate) { Ok(key) => return Ok(key), Err(err) => errors.push(format!("spki: {err}")), } match RsaPublicKey::from_pkcs1_pem(&candidate) { Ok(key) => return Ok(key), Err(err) => errors.push(format!("pkcs1: {err}")), } } Err(format!("RSA 公钥解析失败: {}", errors.join("; "))) } fn rsa_sha256_sign_base64(private_key: &str, message: &str) -> Result { let private_key = decode_rsa_private_key(private_key)?; let signing_key = RsaPkcs1v15SigningKey::::new(private_key); let signature = signing_key.sign(message.as_bytes()); Ok(BASE64_STANDARD.encode(signature.to_bytes())) } fn rsa_sha256_verify_base64( public_key: &str, message: &str, signature_base64: &str, ) -> Result { let public_key = decode_rsa_public_key(public_key)?; let signature_bytes = BASE64_STANDARD .decode(signature_base64.trim()) .map_err(|err| format!("签名 base64 解码失败: {err}"))?; let signature = RsaPkcs1v15Signature::try_from(signature_bytes.as_slice()) .map_err(|err| format!("签名格式无效: {err}"))?; let verifying_key = RsaPkcs1v15VerifyingKey::::new(public_key); Ok(verifying_key.verify(message.as_bytes(), &signature).is_ok()) } fn alipay_timestamp() -> String { (Utc::now() + chrono::Duration::hours(8)) .format("%Y-%m-%d %H:%M:%S") .to_string() } fn alipay_gateway_url(config: &DirectGatewayConfig) -> String { let endpoint_url = config.record.endpoint_url.trim(); if endpoint_url.is_empty() { ALIPAY_DEFAULT_GATEWAY_URL.to_string() } else { endpoint_url.to_string() } } fn alipay_app_id(config: &DirectGatewayConfig) -> Result { config_string(&config.config, "app_id").ok_or_else(|| "支付宝 app_id 未配置".to_string()) } fn alipay_private_key(config: &DirectGatewayConfig) -> Result { secret_string(&config.secrets, "private_key").ok_or_else(|| "支付宝应用私钥未配置".to_string()) } fn alipay_public_key(config: &DirectGatewayConfig) -> Result { secret_string(&config.secrets, "alipay_public_key") .or_else(|| secret_string(&config.secrets, "public_key")) .ok_or_else(|| "支付宝公钥未配置".to_string()) } fn alipay_request_sign_content(params: &BTreeMap) -> String { params .iter() .filter(|(key, value)| key.as_str() != "sign" && !value.trim().is_empty()) .map(|(key, value)| format!("{key}={value}")) .collect::>() .join("&") } fn alipay_notify_sign_content(params: &BTreeMap) -> String { params .iter() .filter(|(key, value)| { key.as_str() != "sign" && key.as_str() != "sign_type" && !value.trim().is_empty() }) .map(|(key, value)| format!("{key}={value}")) .collect::>() .join("&") } fn alipay_signed_params( config: &DirectGatewayConfig, method: &str, biz_content: Value, notify_url: Option<&str>, return_url: Option<&str>, ) -> Result, String> { let mut params = BTreeMap::new(); params.insert("app_id".to_string(), alipay_app_id(config)?); params.insert("method".to_string(), method.to_string()); params.insert("format".to_string(), "JSON".to_string()); params.insert("charset".to_string(), "utf-8".to_string()); params.insert("sign_type".to_string(), "RSA2".to_string()); params.insert("timestamp".to_string(), alipay_timestamp()); params.insert("version".to_string(), "1.0".to_string()); params.insert( "biz_content".to_string(), serde_json::to_string(&biz_content) .map_err(|err| format!("支付宝 biz_content 编码失败: {err}"))?, ); if let Some(notify_url) = notify_url.map(str::trim).filter(|value| !value.is_empty()) { params.insert("notify_url".to_string(), notify_url.to_string()); } if let Some(return_url) = return_url.map(str::trim).filter(|value| !value.is_empty()) { params.insert("return_url".to_string(), return_url.to_string()); } let sign_content = alipay_request_sign_content(¶ms); let sign = rsa_sha256_sign_base64(&alipay_private_key(config)?, &sign_content)?; params.insert("sign".to_string(), sign); Ok(params) } fn url_with_query(base: &str, params: &BTreeMap) -> Result { let mut url = url::Url::parse(base).map_err(|err| format!("支付网关地址无效: {err}"))?; { let mut query = url.query_pairs_mut(); for (key, value) in params { query.append_pair(key, value); } } Ok(url.to_string()) } async fn alipay_post( state: &AppState, config: &DirectGatewayConfig, params: &BTreeMap, ) -> Result { let response = state .client .post(alipay_gateway_url(config)) .form(params) .send() .await .map_err(|err| format!("支付宝请求失败: {err}"))?; let status = response.status(); let body = response .text() .await .map_err(|err| format!("支付宝响应读取失败: {err}"))?; let value = serde_json::from_str::(&body).map_err(|_| "支付宝响应格式无效".to_string())?; if !status.is_success() { return Err(format!("支付宝 HTTP 状态异常: {status}")); } Ok(value) } fn alipay_response_success<'a>(value: &'a Value, key: &str) -> Result<&'a Value, String> { let response = value .get(key) .ok_or_else(|| "支付宝响应缺少业务结果".to_string())?; if response.get("code").and_then(Value::as_str) == Some("10000") { return Ok(response); } let message = response .get("sub_msg") .or_else(|| response.get("msg")) .and_then(Value::as_str) .unwrap_or("支付宝业务请求失败"); Err(message.to_string()) } pub(crate) async fn create_alipay_direct_checkout( state: &AppState, input: &DirectPaymentCheckoutInput, ) -> Result { let config = load_direct_gateway_config(state, "alipay").await?; if !input.pay_currency.eq_ignore_ascii_case("CNY") { return Err("支付宝官方直连当前仅支持 CNY".to_string()); } let mode = config_string(&config.config, "payment_mode") .unwrap_or_else(|| "precreate".to_string()) .to_ascii_lowercase(); let total_amount = format!("{:.2}", input.pay_amount); let gateway_url = alipay_gateway_url(&config); let return_url = input.return_url.as_deref(); if matches!(mode.as_str(), "page" | "redirect") { let params = alipay_signed_params( &config, "alipay.trade.page.pay", json!({ "out_trade_no": input.order_no, "total_amount": total_amount, "subject": input.subject, "product_code": "FAST_INSTANT_TRADE_PAY", }), Some(&input.notify_url), return_url, )?; return Ok(json!({ "gateway": "alipay", "display_name": input.display_name, "gateway_order_id": input.order_no, "payment_url": url_with_query(&gateway_url, ¶ms)?, "submit_method": "GET", "qr_code": Value::Null, "pay_amount": input.pay_amount, "pay_currency": input.pay_currency, "payment_channel": input.payment_channel, "callback_url": input.notify_url, "return_url": return_url, "expires_at": input.expires_at.to_rfc3339(), })); } if matches!(mode.as_str(), "wap" | "h5") { let params = alipay_signed_params( &config, "alipay.trade.wap.pay", json!({ "out_trade_no": input.order_no, "total_amount": total_amount, "subject": input.subject, "product_code": "QUICK_WAP_WAY", }), Some(&input.notify_url), return_url, )?; return Ok(json!({ "gateway": "alipay", "display_name": input.display_name, "gateway_order_id": input.order_no, "payment_url": url_with_query(&gateway_url, ¶ms)?, "submit_method": "GET", "qr_code": Value::Null, "pay_amount": input.pay_amount, "pay_currency": input.pay_currency, "payment_channel": input.payment_channel, "callback_url": input.notify_url, "return_url": return_url, "expires_at": input.expires_at.to_rfc3339(), })); } let params = alipay_signed_params( &config, "alipay.trade.precreate", json!({ "out_trade_no": input.order_no, "total_amount": total_amount, "subject": input.subject, "product_code": "FACE_TO_FACE_PAYMENT", }), Some(&input.notify_url), None, )?; match alipay_post(state, &config, ¶ms) .await .and_then(|value| { let response = alipay_response_success(&value, "alipay_trade_precreate_response")?; response .get("qr_code") .and_then(Value::as_str) .map(str::trim) .filter(|value| !value.is_empty()) .map(|qr_code| { json!({ "gateway": "alipay", "display_name": input.display_name, "gateway_order_id": input.order_no, "payment_url": Value::Null, "submit_method": "qrcode", "qr_code": qr_code, "pay_amount": input.pay_amount, "pay_currency": input.pay_currency, "payment_channel": input.payment_channel, "callback_url": input.notify_url, "return_url": return_url, "expires_at": input.expires_at.to_rfc3339(), }) }) .ok_or_else(|| "支付宝预下单响应缺少 qr_code".to_string()) }) { Ok(value) => Ok(value), Err(precreate_err) => { let params = alipay_signed_params( &config, "alipay.trade.page.pay", json!({ "out_trade_no": input.order_no, "total_amount": total_amount, "subject": input.subject, "product_code": "FAST_INSTANT_TRADE_PAY", }), Some(&input.notify_url), return_url, )?; Ok(json!({ "gateway": "alipay", "display_name": input.display_name, "gateway_order_id": input.order_no, "payment_url": url_with_query(&gateway_url, ¶ms)?, "submit_method": "GET", "qr_code": Value::Null, "pay_amount": input.pay_amount, "pay_currency": input.pay_currency, "payment_channel": input.payment_channel, "callback_url": input.notify_url, "return_url": return_url, "expires_at": input.expires_at.to_rfc3339(), "integration_status": format!("precreate_fallback: {precreate_err}"), })) } } } pub(crate) async fn verify_alipay_notify_callback( state: &AppState, body: &[u8], ) -> Result { let config = load_direct_gateway_config(state, "alipay").await?; let raw = std::str::from_utf8(body).map_err(|_| "支付宝通知请求体不是 UTF-8".to_string())?; let params = url::form_urlencoded::parse(raw.as_bytes()) .map(|(key, value)| (key.into_owned(), value.into_owned())) .collect::>(); let signature = params .get("sign") .map(String::as_str) .ok_or_else(|| "支付宝通知缺少 sign".to_string())?; let sign_content = alipay_notify_sign_content(¶ms); if !rsa_sha256_verify_base64(&alipay_public_key(&config)?, &sign_content, signature)? { return Err("支付宝通知签名无效".to_string()); } if let Some(app_id) = params.get("app_id").map(String::as_str) { if app_id != alipay_app_id(&config)? { return Err("支付宝通知 app_id 不匹配".to_string()); } } if !matches!( params.get("trade_status").map(String::as_str), Some("TRADE_SUCCESS" | "TRADE_FINISHED") ) { return Err("支付宝通知不是成功支付状态".to_string()); } let order_no = params .get("out_trade_no") .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()) .ok_or_else(|| "支付宝通知缺少 out_trade_no".to_string())?; let pay_amount = params .get("total_amount") .or_else(|| params.get("receipt_amount")) .and_then(|value| value.parse::().ok()) .filter(|value| *value > 0.0) .ok_or_else(|| "支付宝通知金额无效".to_string())?; let payload = serde_json::to_value(¶ms).unwrap_or_else(|_| json!({})); let payload_hash = payment_payload_hash(&payload)?; let callback_key = params .get("notify_id") .or_else(|| params.get("trade_no")) .cloned() .unwrap_or_else(|| format!("alipay:{order_no}:{payload_hash}")); let exchange_rate = config.record.usd_exchange_rate; Ok( aether_data::repository::wallet::ProcessPaymentCallbackInput { payment_method: "alipay".to_string(), payment_provider: Some("alipay".to_string()), payment_channel: Some("alipay".to_string()), callback_key, order_no: Some(order_no), gateway_order_id: params.get("trade_no").cloned(), amount_usd: if exchange_rate > 0.0 { pay_amount / exchange_rate } else { pay_amount }, pay_amount: Some(pay_amount), pay_currency: Some(config.record.pay_currency), exchange_rate: Some(exchange_rate), payload_hash, payload, signature_valid: true, }, ) } fn wxpay_base_url(config: &DirectGatewayConfig) -> String { let base = config.record.endpoint_url.trim(); if base.is_empty() { WXPAY_DEFAULT_BASE_URL.to_string() } else { base.trim_end_matches('/').to_string() } } fn wxpay_config_string(config: &DirectGatewayConfig, key: &str) -> Result { config_string(&config.config, key).ok_or_else(|| format!("微信支付 {key} 未配置")) } fn wxpay_secret_string(config: &DirectGatewayConfig, key: &str) -> Result { secret_string(&config.secrets, key).ok_or_else(|| format!("微信支付 {key} 未配置")) } fn wxpay_money_to_fen(amount: f64) -> Result { let value = (amount * 100.0).round(); if !value.is_finite() || value <= 0.0 { return Err("微信支付金额无效".to_string()); } Ok(value as i64) } fn wxpay_authorization( config: &DirectGatewayConfig, method: &str, canonical_url: &str, body: &str, ) -> Result { let mch_id = wxpay_config_string(config, "mch_id")?; let serial_no = wxpay_config_string(config, "cert_serial")?; let private_key = wxpay_secret_string(config, "private_key")?; let timestamp = Utc::now().timestamp(); let nonce = uuid::Uuid::new_v4().simple().to_string(); let message = format!("{method}\n{canonical_url}\n{timestamp}\n{nonce}\n{body}\n"); let signature = rsa_sha256_sign_base64(&private_key, &message)?; Ok(format!( "WECHATPAY2-SHA256-RSA2048 mchid=\"{mch_id}\",nonce_str=\"{nonce}\",signature=\"{signature}\",timestamp=\"{timestamp}\",serial_no=\"{serial_no}\"" )) } async fn wxpay_post_json( state: &AppState, config: &DirectGatewayConfig, canonical_url: &str, body: Value, ) -> Result { let body = serde_json::to_string(&body).map_err(|err| format!("微信支付请求体编码失败: {err}"))?; let auth = wxpay_authorization(config, "POST", canonical_url, &body)?; let url = format!("{}{}", wxpay_base_url(config), canonical_url); let response = state .client .post(url) .header(http::header::AUTHORIZATION, auth) .header(http::header::ACCEPT, "application/json") .header(http::header::CONTENT_TYPE, "application/json") .body(body) .send() .await .map_err(|err| format!("微信支付请求失败: {err}"))?; let status = response.status(); let text = response .text() .await .map_err(|err| format!("微信支付响应读取失败: {err}"))?; if !status.is_success() { let detail = serde_json::from_str::(&text) .ok() .and_then(|value| { value .get("message") .and_then(Value::as_str) .map(ToOwned::to_owned) .or_else(|| { value .get("code") .and_then(Value::as_str) .map(ToOwned::to_owned) }) }) .unwrap_or_else(|| format!("微信支付 HTTP 状态异常: {status}")); return Err(detail); } if text.trim().is_empty() { return Ok(json!({})); } serde_json::from_str::(&text).map_err(|_| "微信支付响应格式无效".to_string()) } async fn wxpay_post_empty_success( state: &AppState, config: &DirectGatewayConfig, canonical_url: &str, body: Value, ) -> Result { wxpay_post_json(state, config, canonical_url, body).await } fn wxpay_client_ip(headers: &http::HeaderMap) -> Option { crate::headers::header_value_str(headers, "x-forwarded-for") .and_then(|value| { value .split(',') .map(str::trim) .find(|segment| !segment.is_empty() && !segment.eq_ignore_ascii_case("unknown")) .map(ToOwned::to_owned) }) .or_else(|| { crate::headers::header_value_str(headers, "x-real-ip").and_then(|value| { let value = value.trim(); (!value.is_empty() && !value.eq_ignore_ascii_case("unknown")) .then(|| value.to_string()) }) }) } pub(crate) fn direct_payment_client_ip(headers: &http::HeaderMap) -> Option { wxpay_client_ip(headers) } pub(crate) async fn create_wxpay_direct_checkout( state: &AppState, input: &DirectPaymentCheckoutInput, ) -> Result { let config = load_direct_gateway_config(state, "wxpay").await?; if !input.pay_currency.eq_ignore_ascii_case(WXPAY_CURRENCY) { return Err("微信支付官方直连当前仅支持 CNY".to_string()); } let total_fen = wxpay_money_to_fen(input.pay_amount)?; let app_id = wxpay_config_string(&config, "app_id")?; let mch_id = wxpay_config_string(&config, "mch_id")?; let common = json!({ "appid": app_id, "mchid": mch_id, "description": input.subject, "out_trade_no": input.order_no, "notify_url": input.notify_url, "amount": { "total": total_fen, "currency": WXPAY_CURRENCY, }, }); match input.payment_channel.as_str() { "native" => { let value = wxpay_post_json(state, &config, "/v3/pay/transactions/native", common).await?; let code_url = value .get("code_url") .and_then(Value::as_str) .map(str::trim) .filter(|value| !value.is_empty()) .ok_or_else(|| "微信 Native 响应缺少 code_url".to_string())?; Ok(json!({ "gateway": "wxpay", "display_name": input.display_name, "gateway_order_id": input.order_no, "payment_url": Value::Null, "submit_method": "qrcode", "qr_code": code_url, "code_url": code_url, "pay_amount": input.pay_amount, "pay_currency": input.pay_currency, "payment_channel": input.payment_channel, "callback_url": input.notify_url, "return_url": input.return_url, "expires_at": input.expires_at.to_rfc3339(), })) } "h5" => { let client_ip = input .client_ip .as_deref() .map(str::trim) .filter(|value| !value.is_empty()) .ok_or_else(|| "微信 H5 支付需要客户端 IP".to_string())?; let mut body = common; body["scene_info"] = json!({ "payer_client_ip": client_ip, "h5_info": { "type": "Wap" }, }); let value = wxpay_post_json(state, &config, "/v3/pay/transactions/h5", body).await?; let mut h5_url = value .get("h5_url") .and_then(Value::as_str) .map(str::trim) .filter(|value| !value.is_empty()) .ok_or_else(|| "微信 H5 响应缺少 h5_url".to_string())? .to_string(); if let Some(return_url) = input.return_url.as_deref() { let sep = if h5_url.contains('?') { "&" } else { "?" }; h5_url = format!( "{h5_url}{sep}redirect_url={}", url::form_urlencoded::byte_serialize(return_url.as_bytes()).collect::() ); } Ok(json!({ "gateway": "wxpay", "display_name": input.display_name, "gateway_order_id": input.order_no, "payment_url": h5_url, "h5_url": h5_url, "submit_method": "GET", "qr_code": Value::Null, "pay_amount": input.pay_amount, "pay_currency": input.pay_currency, "payment_channel": input.payment_channel, "callback_url": input.notify_url, "return_url": input.return_url, "expires_at": input.expires_at.to_rfc3339(), })) } "jsapi" => Err("微信 JSAPI 需要前端提供 OpenID,当前充值入口尚未接入".to_string()), _ => Err("微信支付通道不可用".to_string()), } } pub(crate) async fn create_stripe_direct_checkout( state: &AppState, input: &DirectPaymentCheckoutInput, ) -> Result { let config = load_direct_gateway_config(state, "stripe").await?; let Some(secret_key) = secret_string(&config.secrets, "secret_key") else { return Err("Stripe secret_key 未配置".to_string()); }; let Some(publishable_key) = config_string(&config.config, "publishable_key") else { return Err("Stripe publishable_key 未配置".to_string()); }; let amount = stripe_minor_unit_amount(input.pay_amount, &config.record.pay_currency)?; let currency = config.record.pay_currency.trim().to_ascii_lowercase(); let mut form = vec![ ("amount".to_string(), amount.to_string()), ("currency".to_string(), currency.clone()), ("description".to_string(), input.subject.clone()), ("metadata[order_no]".to_string(), input.order_no.clone()), ( "metadata[payment_provider]".to_string(), "stripe".to_string(), ), ( "metadata[payment_channel]".to_string(), input.payment_channel.clone(), ), ( "payment_method_types[]".to_string(), input.payment_channel.clone(), ), ]; if input.payment_channel == "wechat_pay" { form.push(( "payment_method_options[wechat_pay][client]".to_string(), "web".to_string(), )); } let response = state .client .post("https://api.stripe.com/v1/payment_intents") .basic_auth(secret_key, Some("")) .form(&form) .send() .await .map_err(|err| format!("Stripe PaymentIntent 创建失败: {err}"))?; let status = response.status(); let body = response .text() .await .map_err(|err| format!("Stripe 响应读取失败: {err}"))?; let value = serde_json::from_str::(&body).map_err(|_| "Stripe 响应格式无效".to_string())?; if !status.is_success() { let message = value .get("error") .and_then(|error| error.get("message")) .and_then(Value::as_str) .unwrap_or("Stripe PaymentIntent 创建失败"); return Err(message.to_string()); } let Some(intent_id) = value.get("id").and_then(Value::as_str) else { return Err("Stripe 响应缺少 PaymentIntent ID".to_string()); }; let Some(client_secret) = value.get("client_secret").and_then(Value::as_str) else { return Err("Stripe 响应缺少 client_secret".to_string()); }; Ok(json!({ "gateway": "stripe", "display_name": input.display_name, "gateway_order_id": intent_id, "intent_id": intent_id, "client_secret": client_secret, "publishable_key": publishable_key, "expires_at": input.expires_at.to_rfc3339(), "pay_amount": input.pay_amount, "pay_currency": config.record.pay_currency, "payment_channel": input.payment_channel, "payment_method_types": [input.payment_channel], "submit_method": "stripe_payment_intent" })) } fn wxpay_header(headers: &http::HeaderMap, name: &str) -> Result { crate::headers::header_value_str(headers, name) .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()) .ok_or_else(|| format!("微信支付通知缺少 {name}")) } fn wxpay_verify_notify_headers( config: &DirectGatewayConfig, headers: &http::HeaderMap, body: &[u8], ) -> Result<(), String> { let signature = wxpay_header(headers, "wechatpay-signature")?; let timestamp = wxpay_header(headers, "wechatpay-timestamp")?; let nonce = wxpay_header(headers, "wechatpay-nonce")?; let serial = wxpay_header(headers, "wechatpay-serial")?; if let Some(expected) = config_string(&config.config, "public_key_id") { if serial != expected { return Err("微信支付通知公钥 ID 不匹配".to_string()); } } let body = std::str::from_utf8(body).map_err(|_| "微信支付通知体不是 UTF-8".to_string())?; let message = format!("{timestamp}\n{nonce}\n{body}\n"); let public_key = wxpay_secret_string(config, "public_key")?; if rsa_sha256_verify_base64(&public_key, &message, &signature)? { Ok(()) } else { Err("微信支付通知签名无效".to_string()) } } fn wxpay_decrypt_resource(config: &DirectGatewayConfig, resource: &Value) -> Result { let algorithm = resource .get("algorithm") .and_then(Value::as_str) .unwrap_or(""); if algorithm != "AEAD_AES_256_GCM" { return Err("微信支付通知加密算法不支持".to_string()); } let api_v3_key = wxpay_secret_string(config, "api_v3_key")?; if api_v3_key.as_bytes().len() != 32 { return Err("微信支付 api_v3_key 必须为 32 字节".to_string()); } let nonce = resource .get("nonce") .and_then(Value::as_str) .ok_or_else(|| "微信支付通知 resource.nonce 缺失".to_string())?; let associated_data = resource .get("associated_data") .and_then(Value::as_str) .unwrap_or(""); let ciphertext = resource .get("ciphertext") .and_then(Value::as_str) .ok_or_else(|| "微信支付通知 resource.ciphertext 缺失".to_string())?; let ciphertext = BASE64_STANDARD .decode(ciphertext) .map_err(|err| format!("微信支付通知密文解码失败: {err}"))?; let cipher = Aes256Gcm::new_from_slice(api_v3_key.as_bytes()) .map_err(|_| "微信支付 api_v3_key 无效".to_string())?; let plaintext = cipher .decrypt( Nonce::from_slice(nonce.as_bytes()), Payload { msg: &ciphertext, aad: associated_data.as_bytes(), }, ) .map_err(|_| "微信支付通知解密失败".to_string())?; serde_json::from_slice::(&plaintext).map_err(|_| "微信支付通知明文格式无效".to_string()) } pub(crate) async fn verify_wxpay_notify_callback( state: &AppState, headers: &http::HeaderMap, body: &[u8], ) -> Result { let config = load_direct_gateway_config(state, "wxpay").await?; wxpay_verify_notify_headers(&config, headers, body)?; let payload = serde_json::from_slice::(body).map_err(|_| "微信支付通知请求体无效".to_string())?; if payload.get("event_type").and_then(Value::as_str) != Some(WXPAY_NOTIFY_SUCCESS) { return Err("微信支付通知不是成功支付事件".to_string()); } let tx = wxpay_decrypt_resource( &config, payload .get("resource") .ok_or_else(|| "微信支付通知缺少 resource".to_string())?, )?; if tx.get("trade_state").and_then(Value::as_str) != Some(WXPAY_TRADE_SUCCESS) { return Err("微信支付交易不是成功状态".to_string()); } let order_no = tx .get("out_trade_no") .and_then(Value::as_str) .map(str::trim) .filter(|value| !value.is_empty()) .map(ToOwned::to_owned) .ok_or_else(|| "微信支付通知缺少 out_trade_no".to_string())?; let transaction_id = tx .get("transaction_id") .and_then(Value::as_str) .map(str::trim) .filter(|value| !value.is_empty()) .map(ToOwned::to_owned); let amount_fen = tx .get("amount") .and_then(|value| value.get("total")) .and_then(Value::as_i64) .filter(|value| *value > 0) .ok_or_else(|| "微信支付通知金额无效".to_string())?; let pay_amount = amount_fen as f64 / 100.0; let exchange_rate = config.record.usd_exchange_rate; let callback_key = payload .get("id") .and_then(Value::as_str) .map(str::trim) .filter(|value| !value.is_empty()) .map(ToOwned::to_owned) .or_else(|| transaction_id.clone()) .unwrap_or_else(|| format!("wxpay:{order_no}")); let callback_payload = json!({ "notification": payload, "transaction": tx, }); let payload_hash = payment_payload_hash(&callback_payload)?; Ok( aether_data::repository::wallet::ProcessPaymentCallbackInput { payment_method: "wxpay".to_string(), payment_provider: Some("wxpay".to_string()), payment_channel: None, callback_key, order_no: Some(order_no), gateway_order_id: transaction_id, amount_usd: if exchange_rate > 0.0 { pay_amount / exchange_rate } else { pay_amount }, pay_amount: Some(pay_amount), pay_currency: Some(config.record.pay_currency), exchange_rate: Some(exchange_rate), payload_hash, payload: callback_payload, signature_valid: true, }, ) } pub(crate) async fn close_direct_gateway_order( state: &AppState, order: &crate::AdminWalletPaymentOrderRecord, ) -> Result, String> { match order.payment_method.as_str() { "alipay" => { let config = load_direct_gateway_config(state, "alipay").await?; let params = alipay_signed_params( &config, "alipay.trade.close", json!({ "out_trade_no": order.order_no }), None, None, )?; let value = alipay_post(state, &config, ¶ms).await?; let response = alipay_response_success(&value, "alipay_trade_close_response")?; Ok(Some(json!({ "gateway": "alipay", "closed": true, "gateway_order_id": response.get("trade_no").and_then(Value::as_str), "payload": value, }))) } "wxpay" => { let config = load_direct_gateway_config(state, "wxpay").await?; let canonical_url = format!("/v3/pay/transactions/out-trade-no/{}/close", order.order_no); let value = wxpay_post_empty_success( state, &config, &canonical_url, json!({ "mchid": wxpay_config_string(&config, "mch_id")? }), ) .await?; Ok(Some(json!({ "gateway": "wxpay", "closed": true, "gateway_order_id": order.gateway_order_id, "payload": value, }))) } _ => Ok(None), } } fn refund_pay_amount( order: &crate::AdminWalletPaymentOrderRecord, amount_usd: f64, ) -> Result { if amount_usd <= 0.0 || !amount_usd.is_finite() { return Err("退款金额无效".to_string()); } let total_pay_amount = order.pay_amount.unwrap_or_else(|| { let exchange_rate = order.exchange_rate.unwrap_or(1.0); order.amount_usd * exchange_rate }); if order.amount_usd <= 0.0 || total_pay_amount <= 0.0 { return Err("原支付订单金额无效".to_string()); } Ok((amount_usd * total_pay_amount / order.amount_usd * 100.0).round() / 100.0) } pub(crate) async fn refund_direct_gateway_order( state: &AppState, order: &crate::AdminWalletPaymentOrderRecord, refund_no: &str, amount_usd: f64, reason: Option<&str>, ) -> Result, String> { match order.payment_method.as_str() { "alipay" => { let config = load_direct_gateway_config(state, "alipay").await?; let refund_amount = refund_pay_amount(order, amount_usd)?; let params = alipay_signed_params( &config, "alipay.trade.refund", json!({ "out_trade_no": order.order_no, "refund_amount": format!("{refund_amount:.2}"), "refund_reason": reason.unwrap_or("wallet refund"), "out_request_no": refund_no, }), None, None, )?; let value = alipay_post(state, &config, ¶ms).await?; let response = alipay_response_success(&value, "alipay_trade_refund_response")?; let gateway_refund_id = response .get("trade_no") .and_then(Value::as_str) .unwrap_or(&order.order_no) .to_string(); Ok(Some(DirectGatewayRefundResult { gateway_refund_id, status: "success".to_string(), payload: value, })) } "wxpay" => { let config = load_direct_gateway_config(state, "wxpay").await?; let total_pay_amount = order.pay_amount.ok_or_else(|| { "微信支付退款需要原订单 pay_amount,请确认订单已通过官方直连创建".to_string() })?; let refund_amount = refund_pay_amount(order, amount_usd)?; let value = wxpay_post_json( state, &config, "/v3/refund/domestic/refunds", json!({ "out_trade_no": order.order_no, "out_refund_no": refund_no, "reason": reason.unwrap_or("wallet refund"), "amount": { "refund": wxpay_money_to_fen(refund_amount)?, "total": wxpay_money_to_fen(total_pay_amount)?, "currency": WXPAY_CURRENCY, }, }), ) .await?; let gateway_refund_id = value .get("refund_id") .and_then(Value::as_str) .unwrap_or(refund_no) .to_string(); let status = value .get("status") .and_then(Value::as_str) .unwrap_or("PROCESSING") .to_ascii_lowercase(); Ok(Some(DirectGatewayRefundResult { gateway_refund_id, status, payload: value, })) } _ => Ok(None), } }