use serde::{Deserialize, Serialize};
use std::sync::Arc;
use crate::auth::Verifier;
use crate::config::NotifyConfig;
use crate::crypto::Aes256GcmCipher;
use crate::error::{WxPayError, WxPayResult};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NotifyRequest {
pub id: String,
pub create_time: String,
#[serde(rename = "type")]
pub notify_type: String,
pub resource: NotifyResource,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NotifyResource {
pub algorithm: String,
pub ciphertext: String,
pub associated_data: Option<String>,
pub nonce: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaymentNotifyData {
pub appid: String,
pub mchid: String,
pub out_trade_no: String,
pub transaction_id: String,
pub trade_type: String,
pub trade_state: String,
pub trade_state_desc: String,
pub bank_type: String,
pub attach: Option<String>,
pub success_time: String,
pub payer: Option<NotifyPayer>,
pub amount: Option<NotifyAmount>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NotifyPayer {
pub openid: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NotifyAmount {
pub total: u64,
pub payer_total: Option<u64>,
pub currency: String,
pub payer_currency: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RefundNotifyData {
pub mchid: String,
pub out_trade_no: String,
pub transaction_id: String,
pub out_refund_no: String,
pub refund_id: String,
pub refund_status: String,
pub success_time: Option<String>,
pub amount: Option<RefundNotifyAmount>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RefundNotifyAmount {
pub total: u64,
pub refund: u64,
pub payer_total: u64,
pub payer_refund: u64,
}
pub struct NotifyHandler {
config: NotifyConfig,
verifier: Arc<dyn Verifier>,
cipher: Aes256GcmCipher,
}
impl NotifyHandler {
pub fn new(config: NotifyConfig, verifier: Arc<dyn Verifier>) -> WxPayResult<Self> {
let cipher = Aes256GcmCipher::new(&config.api_v3_key)?;
Ok(Self {
config,
verifier,
cipher,
})
}
pub async fn handle_payment_notify(
&self,
request: &NotifyRequest,
) -> WxPayResult<PaymentNotifyData> {
if request.notify_type != "TRANSACTION.SUCCESS" {
return Err(WxPayError::InvalidNotifyType(request.notify_type.clone()));
}
let data = self.decrypt_notify_data(request)?;
let payment_data: PaymentNotifyData = serde_json::from_str(&data)?;
Ok(payment_data)
}
pub async fn handle_refund_notify(
&self,
request: &NotifyRequest,
) -> WxPayResult<RefundNotifyData> {
if request.notify_type != "REFUND.SUCCESS" {
return Err(WxPayError::InvalidNotifyType(request.notify_type.clone()));
}
let data = self.decrypt_notify_data(request)?;
let refund_data: RefundNotifyData = serde_json::from_str(&data)?;
Ok(refund_data)
}
fn decrypt_notify_data(&self, request: &NotifyRequest) -> WxPayResult<String> {
let resource = &request.resource;
match resource.algorithm.as_str() {
"AEAD_AES_256_GCM" => {
let associated_data = resource.associated_data.as_deref().unwrap_or("");
self.cipher.decrypt_notification(
&resource.nonce,
&resource.ciphertext,
associated_data,
)
}
_ => Err(WxPayError::DecryptionError(format!(
"不支持的加密算法: {}",
resource.algorithm
))),
}
}
pub async fn verify_notify_signature(
&self,
timestamp: &str,
nonce: &str,
body: &str,
signature: &str,
) -> WxPayResult<bool> {
let message = format!("{}\n{}\n{}\n", timestamp, nonce, body);
self.verifier.verify(&message, signature).await
}
pub fn verify_timestamp(&self, timestamp: &str, tolerance_seconds: i64) -> WxPayResult<bool> {
let timestamp: i64 = timestamp
.parse()
.map_err(|e| WxPayError::InvalidNotifyFormat(format!("无效的时间戳: {}", e)))?;
Ok(crate::utils::timestamp::is_timestamp_valid(
timestamp,
tolerance_seconds,
))
}
}
impl std::fmt::Debug for NotifyHandler {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("NotifyHandler")
.field("config", &self.config)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::auth::Sha256RsaVerifier;
use aes_gcm::{Aes256Gcm, KeyInit, Nonce, aead::Aead};
use base64::Engine;
use std::sync::Arc;
const API_V3_KEY: &str = "abcdefghijklmnopqrstuvwxyz123456";
fn test_handler() -> NotifyHandler {
let verifier: Arc<dyn Verifier> = Arc::new(Sha256RsaVerifier::new(vec![]).unwrap());
let config = NotifyConfig {
api_v3_key: API_V3_KEY.to_string(),
cert_serial_number: "CERT123456".to_string(),
platform_certificate: vec![],
};
NotifyHandler::new(config, verifier).unwrap()
}
fn encrypt_resource(plaintext: &str, associated_data: &str, nonce: &str) -> (String, String) {
let mut hasher = sha2::Sha256::new();
use sha2::Digest;
hasher.update(API_V3_KEY.as_bytes());
let key = hasher.finalize();
let cipher = Aes256Gcm::new_from_slice(&key).unwrap();
let nonce_bytes: [u8; 12] = nonce.as_bytes().try_into().unwrap();
let nonce_value = Nonce::from(nonce_bytes);
let ct = cipher
.encrypt(
&nonce_value,
aes_gcm::aead::Payload {
msg: plaintext.as_bytes(),
aad: associated_data.as_bytes(),
},
)
.unwrap();
let ciphertext_b64 = base64::engine::general_purpose::STANDARD.encode(ct);
(ciphertext_b64, nonce.to_string())
}
fn make_request(
notify_type: &str,
algorithm: &str,
ciphertext: &str,
nonce: &str,
associated_data: &str,
) -> NotifyRequest {
NotifyRequest {
id: "EV-TEST".to_string(),
create_time: "2024-01-01T00:00:00+08:00".to_string(),
notify_type: notify_type.to_string(),
resource: NotifyResource {
algorithm: algorithm.to_string(),
ciphertext: ciphertext.to_string(),
associated_data: Some(associated_data.to_string()),
nonce: nonce.to_string(),
},
}
}
#[test]
fn test_notify_request_deserialization() {
let json = r#"{
"id": "EV-2018022511223320873",
"create_time": "2015-05-20T13:29:35+08:00",
"type": "TRANSACTION.SUCCESS",
"resource": {
"algorithm": "AEAD_AES_256_GCM",
"ciphertext": "...",
"associated_data": "transaction",
"nonce": "..."
}
}"#;
let request: NotifyRequest = serde_json::from_str(json).unwrap();
assert_eq!(request.id, "EV-2018022511223320873");
assert_eq!(request.notify_type, "TRANSACTION.SUCCESS");
assert_eq!(request.resource.algorithm, "AEAD_AES_256_GCM");
}
#[test]
fn test_payment_notify_data_deserialization() {
let json = r#"{
"appid": "wx88888888",
"mchid": "1900000109",
"out_trade_no": "test_trade_no",
"transaction_id": "1217752501201407033233368018",
"trade_type": "JSAPI",
"trade_state": "SUCCESS",
"trade_state_desc": "支付成功",
"bank_type": "CMB_CREDIT",
"success_time": "2018-06-08T10:34:56+08:00"
}"#;
let data: PaymentNotifyData = serde_json::from_str(json).unwrap();
assert_eq!(data.trade_state, "SUCCESS");
assert_eq!(data.transaction_id, "1217752501201407033233368018");
}
#[tokio::test]
async fn test_handle_payment_notify_decrypts_and_parses() {
let handler = test_handler();
let plaintext = r#"{
"appid": "wx88888888",
"mchid": "1900000109",
"out_trade_no": "out_20240101",
"transaction_id": "4200000001",
"trade_type": "JSAPI",
"trade_state": "SUCCESS",
"trade_state_desc": "支付成功",
"bank_type": "CMB_CREDIT",
"success_time": "2024-01-01T00:00:00+08:00"
}"#;
let nonce = "nonce1234567"; let (ciphertext, nonce) = encrypt_resource(plaintext, "transaction", nonce);
let request = make_request(
"TRANSACTION.SUCCESS",
"AEAD_AES_256_GCM",
&ciphertext,
&nonce,
"transaction",
);
let data = handler.handle_payment_notify(&request).await.unwrap();
assert_eq!(data.out_trade_no, "out_20240101");
assert_eq!(data.transaction_id, "4200000001");
assert_eq!(data.trade_state, "SUCCESS");
}
#[tokio::test]
async fn test_handle_refund_notify_decrypts_and_parses() {
let handler = test_handler();
let plaintext = r#"{
"mchid": "1900000109",
"out_trade_no": "out_20240101",
"transaction_id": "4200000001",
"out_refund_no": "refund_001",
"refund_id": "5000000038",
"refund_status": "SUCCESS"
}"#;
let nonce = "refundnonce1"; let (ciphertext, nonce) = encrypt_resource(plaintext, "refund", nonce);
let request = make_request(
"REFUND.SUCCESS",
"AEAD_AES_256_GCM",
&ciphertext,
&nonce,
"refund",
);
let data = handler.handle_refund_notify(&request).await.unwrap();
assert_eq!(data.out_refund_no, "refund_001");
assert_eq!(data.refund_id, "5000000038");
assert_eq!(data.refund_status, "SUCCESS");
}
#[tokio::test]
async fn test_handle_payment_notify_rejects_wrong_type() {
let handler = test_handler();
let request = make_request(
"REFUND.SUCCESS",
"AEAD_AES_256_GCM",
"x",
"n",
"transaction",
);
let err = handler.handle_payment_notify(&request).await.unwrap_err();
assert!(matches!(err, WxPayError::InvalidNotifyType(_)));
}
#[tokio::test]
async fn test_handle_notify_rejects_unsupported_algorithm() {
let handler = test_handler();
let request = make_request("TRANSACTION.SUCCESS", "RSA-OAEP", "x", "n", "transaction");
let err = handler.handle_payment_notify(&request).await.unwrap_err();
assert!(matches!(err, WxPayError::DecryptionError(_)));
}
#[tokio::test]
async fn test_handle_payment_notify_rejects_tampered_ciphertext() {
let handler = test_handler();
let nonce = "nonce1234567";
let (ciphertext, nonce) = encrypt_resource("{}", "transaction", nonce);
let mut bytes = base64::engine::general_purpose::STANDARD
.decode(&ciphertext)
.unwrap();
bytes[0] ^= 0xff;
let tampered = base64::engine::general_purpose::STANDARD.encode(&bytes);
let request = make_request(
"TRANSACTION.SUCCESS",
"AEAD_AES_256_GCM",
&tampered,
&nonce,
"transaction",
);
let err = handler.handle_payment_notify(&request).await.unwrap_err();
assert!(matches!(err, WxPayError::DecryptionError(_)));
}
#[tokio::test]
async fn test_verify_notify_signature_delegates_to_verifier() {
let handler = test_handler();
let result = handler
.verify_notify_signature("ts", "nonce", "body", "sig")
.await;
assert!(result.is_err());
}
#[test]
fn test_verify_timestamp_validity() {
let handler = test_handler();
let now = crate::utils::timestamp::get_timestamp();
let valid = handler.verify_timestamp(&now.to_string(), 300).unwrap();
assert!(valid);
let invalid = handler.verify_timestamp("0", 300).unwrap();
assert!(!invalid);
let bad = handler.verify_timestamp("not-a-number", 300);
assert!(bad.is_err());
}
#[test]
fn test_new_rejects_invalid_api_v3_key() {
let verifier: Arc<dyn Verifier> = Arc::new(Sha256RsaVerifier::new(vec![]).unwrap());
let config = NotifyConfig {
api_v3_key: "too-short".to_string(),
cert_serial_number: "CERT".to_string(),
platform_certificate: vec![],
};
let result = NotifyHandler::new(config, verifier);
assert!(matches!(result, Err(WxPayError::InvalidKey(_))));
}
}