use parking_lot::Mutex;
use std::collections::HashMap;
use std::sync::Arc;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum PayError {
#[error("支付配置错误: {0}")]
Config(String),
#[error("支付字段缺失: {0}")]
MissingField(String),
#[error("支付请求失败: {0}")]
RequestFailed(String),
#[error("HTTP 传输失败: {0}")]
HttpTransport(String),
#[error("序列化失败: {0}")]
Serialize(String),
#[error("签名验证失败: {0}")]
VerifyFailed(String),
#[error("退款失败: {0}")]
RefundFailed(String),
#[error("查询失败: {0}")]
QueryFailed(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum PayPlatform {
#[default]
Alipay,
WechatPay,
Other,
}
impl PayPlatform {
pub fn as_str(self) -> &'static str {
match self {
Self::Alipay => "alipay",
Self::WechatPay => "wechatpay",
Self::Other => "other",
}
}
}
impl std::fmt::Display for PayPlatform {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl std::str::FromStr for PayPlatform {
type Err = PayError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"alipay" | "ali" => Ok(Self::Alipay),
"wechatpay" | "wechat" => Ok(Self::WechatPay),
"other" => Ok(Self::Other),
other => Err(PayError::Config(format!("未知支付平台: {other}"))),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct PayOrder {
pub out_trade_no: String,
pub total_amount: i64,
pub subject: String,
pub body: Option<String>,
pub notify_url: Option<String>,
pub return_url: Option<String>,
pub timeout_express: Option<i64>,
pub passback_params: Option<String>,
pub extra: serde_json::Value,
}
impl PayOrder {
pub fn new() -> Self {
Self::default()
}
pub fn out_trade_no(mut self, out_trade_no: impl Into<String>) -> Self {
self.out_trade_no = out_trade_no.into();
self
}
pub fn total_amount(mut self, total_amount: i64) -> Self {
self.total_amount = total_amount;
self
}
pub fn subject(mut self, subject: impl Into<String>) -> Self {
self.subject = subject.into();
self
}
pub fn body(mut self, body: impl Into<String>) -> Self {
self.body = Some(body.into());
self
}
pub fn notify_url(mut self, notify_url: impl Into<String>) -> Self {
self.notify_url = Some(notify_url.into());
self
}
pub fn return_url(mut self, return_url: impl Into<String>) -> Self {
self.return_url = Some(return_url.into());
self
}
pub fn timeout_express(mut self, timeout_express: i64) -> Self {
self.timeout_express = Some(timeout_express);
self
}
pub fn passback_params(mut self, passback_params: impl Into<String>) -> Self {
self.passback_params = Some(passback_params.into());
self
}
pub fn extra(mut self, extra: serde_json::Value) -> Self {
self.extra = extra;
self
}
pub fn validate(&self) -> Result<(), PayError> {
if self.out_trade_no.is_empty() {
return Err(PayError::MissingField("out_trade_no".into()));
}
if self.total_amount <= 0 {
return Err(PayError::MissingField("total_amount".into()));
}
if self.subject.is_empty() {
return Err(PayError::MissingField("subject".into()));
}
Ok(())
}
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct PayResult {
pub trade_no: String,
pub out_trade_no: String,
pub total_amount: i64,
pub trade_status: String,
pub raw: serde_json::Value,
}
#[derive(Debug, Clone, Default)]
pub struct RefundOrder {
pub out_trade_no: String,
pub refund_amount: i64,
pub out_request_no: String,
pub reason: Option<String>,
}
impl RefundOrder {
pub fn new() -> Self {
Self::default()
}
pub fn out_trade_no(mut self, out_trade_no: impl Into<String>) -> Self {
self.out_trade_no = out_trade_no.into();
self
}
pub fn refund_amount(mut self, refund_amount: i64) -> Self {
self.refund_amount = refund_amount;
self
}
pub fn out_request_no(mut self, out_request_no: impl Into<String>) -> Self {
self.out_request_no = out_request_no.into();
self
}
pub fn reason(mut self, reason: impl Into<String>) -> Self {
self.reason = Some(reason.into());
self
}
pub fn validate(&self) -> Result<(), PayError> {
if self.out_trade_no.is_empty() {
return Err(PayError::MissingField("out_trade_no".into()));
}
if self.refund_amount <= 0 {
return Err(PayError::MissingField("refund_amount".into()));
}
if self.out_request_no.is_empty() {
return Err(PayError::MissingField("out_request_no".into()));
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct PayConfig {
pub platform: PayPlatform,
pub app_id: String,
pub merchant_private_key: String,
pub platform_public_key: String,
pub notify_url: String,
pub return_url: Option<String>,
pub sandbox: bool,
pub mode: String,
}
impl PayConfig {
pub fn new(platform: PayPlatform, app_id: impl Into<String>) -> Self {
Self {
platform,
app_id: app_id.into(),
merchant_private_key: String::new(),
platform_public_key: String::new(),
notify_url: String::new(),
return_url: None,
sandbox: false,
mode: "web".to_string(),
}
}
pub fn with_merchant_private_key(mut self, key: impl Into<String>) -> Self {
self.merchant_private_key = key.into();
self
}
pub fn with_platform_public_key(mut self, key: impl Into<String>) -> Self {
self.platform_public_key = key.into();
self
}
pub fn with_notify_url(mut self, notify_url: impl Into<String>) -> Self {
self.notify_url = notify_url.into();
self
}
pub fn with_return_url(mut self, return_url: impl Into<String>) -> Self {
self.return_url = Some(return_url.into());
self
}
pub fn with_sandbox(mut self, sandbox: bool) -> Self {
self.sandbox = sandbox;
self
}
pub fn with_mode(mut self, mode: impl Into<String>) -> Self {
self.mode = mode.into();
self
}
pub fn validate(&self) -> Result<(), PayError> {
if self.app_id.is_empty() {
return Err(PayError::Config("app_id".into()));
}
if self.merchant_private_key.is_empty() {
return Err(PayError::Config("merchant_private_key".into()));
}
if self.platform_public_key.is_empty() {
return Err(PayError::Config("platform_public_key".into()));
}
if self.notify_url.is_empty() {
return Err(PayError::Config("notify_url".into()));
}
Ok(())
}
}
pub trait PayProvider: Send + Sync {
fn pay(&self, order: PayOrder) -> Result<PayResult, PayError>;
fn query(&self, out_trade_no: &str) -> Result<PayResult, PayError>;
fn close(&self, out_trade_no: &str) -> Result<(), PayError>;
fn refund(&self, refund: RefundOrder) -> Result<(), PayError>;
fn verify_notify(&self, params: &serde_json::Value) -> Result<PayResult, PayError>;
}
#[derive(Debug, Default)]
pub struct MemoryPayProvider {
orders: Arc<Mutex<HashMap<String, PayResult>>>,
refunds: Arc<Mutex<Vec<RefundOrder>>>,
query_result: Arc<Mutex<Option<PayResult>>>,
}
impl MemoryPayProvider {
pub fn new() -> Self {
Self::default()
}
pub fn orders(&self) -> Vec<PayResult> {
self.orders.lock().values().cloned().collect()
}
pub fn refunds(&self) -> Vec<RefundOrder> {
self.refunds.lock().clone()
}
pub fn set_query_result(&self, result: PayResult) {
*self.query_result.lock() = Some(result);
}
pub fn clear(&self) {
self.orders.lock().clear();
self.refunds.lock().clear();
*self.query_result.lock() = None;
}
}
impl PayProvider for MemoryPayProvider {
fn pay(&self, order: PayOrder) -> Result<PayResult, PayError> {
order.validate()?;
let mut orders = self.orders.lock();
if orders.contains_key(&order.out_trade_no) {
return Err(PayError::RequestFailed(format!(
"订单号已存在: {}",
order.out_trade_no
)));
}
let trade_no = format!("MEM{}", order.out_trade_no);
let raw = serde_json::json!({
"out_trade_no": order.out_trade_no,
"total_amount": order.total_amount,
"subject": order.subject,
"trade_no": trade_no,
});
let result = PayResult {
trade_no,
out_trade_no: order.out_trade_no.clone(),
total_amount: order.total_amount,
trade_status: "WAIT_BUYER_PAY".to_string(),
raw,
};
orders.insert(order.out_trade_no.clone(), result.clone());
Ok(result)
}
fn query(&self, out_trade_no: &str) -> Result<PayResult, PayError> {
if let Some(result) = self.query_result.lock().clone() {
return Ok(result);
}
self.orders
.lock()
.get(out_trade_no)
.cloned()
.ok_or_else(|| PayError::QueryFailed(format!("订单不存在: {out_trade_no}")))
}
fn close(&self, out_trade_no: &str) -> Result<(), PayError> {
let mut orders = self.orders.lock();
if let Some(result) = orders.get_mut(out_trade_no) {
result.trade_status = "CLOSED".to_string();
Ok(())
} else {
Err(PayError::RequestFailed(format!(
"订单不存在: {out_trade_no}"
)))
}
}
fn refund(&self, refund: RefundOrder) -> Result<(), PayError> {
refund.validate()?;
if !self.orders.lock().contains_key(&refund.out_trade_no) {
return Err(PayError::RefundFailed(format!(
"原订单不存在: {}",
refund.out_trade_no
)));
}
self.refunds.lock().push(refund);
Ok(())
}
fn verify_notify(&self, params: &serde_json::Value) -> Result<PayResult, PayError> {
let out_trade_no = params
.get("out_trade_no")
.and_then(|v| v.as_str())
.ok_or_else(|| PayError::VerifyFailed("缺少 out_trade_no".into()))?;
let trade_no = params
.get("trade_no")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let total_amount = params
.get("total_amount")
.and_then(|v| v.as_i64())
.unwrap_or(0);
let trade_status = params
.get("trade_status")
.and_then(|v| v.as_str())
.unwrap_or("TRADE_SUCCESS")
.to_string();
Ok(PayResult {
trade_no,
out_trade_no: out_trade_no.to_string(),
total_amount,
trade_status,
raw: params.clone(),
})
}
}
pub trait PayHttpTransport: Send + Sync {
fn post_json(&self, url: &str, body: &str) -> Result<String, PayError>;
fn get(&self, url: &str) -> Result<String, PayError>;
}
#[derive(Debug, Default)]
pub struct MemoryPayHttpTransport {
responses: Mutex<Vec<String>>,
requests: Mutex<Vec<(String, String, String)>>,
}
impl MemoryPayHttpTransport {
pub fn new() -> Self {
Self::default()
}
pub fn push_response(&self, response: impl Into<String>) {
self.responses.lock().push(response.into());
}
pub fn request_count(&self) -> usize {
self.requests.lock().len()
}
pub fn requests(&self) -> Vec<(String, String, String)> {
self.requests.lock().clone()
}
pub fn clear(&self) {
self.responses.lock().clear();
self.requests.lock().clear();
}
fn next_response(&self) -> Result<String, PayError> {
let mut responses = self.responses.lock();
if responses.is_empty() {
Err(PayError::HttpTransport("无可用预置响应".into()))
} else {
Ok(responses.remove(0))
}
}
}
impl PayHttpTransport for MemoryPayHttpTransport {
fn post_json(&self, url: &str, body: &str) -> Result<String, PayError> {
let response = self.next_response()?;
self.requests
.lock()
.push(("POST".to_string(), url.to_string(), body.to_string()));
Ok(response)
}
fn get(&self, url: &str) -> Result<String, PayError> {
let response = self.next_response()?;
self.requests
.lock()
.push(("GET".to_string(), url.to_string(), String::new()));
Ok(response)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pay_platform() {
assert_eq!(PayPlatform::Alipay.as_str(), "alipay");
assert_eq!(PayPlatform::WechatPay.as_str(), "wechatpay");
assert_eq!(PayPlatform::Other.as_str(), "other");
assert_eq!(PayPlatform::default(), PayPlatform::Alipay);
assert_eq!(format!("{}", PayPlatform::Alipay), "alipay");
assert_eq!(format!("{}", PayPlatform::WechatPay), "wechatpay");
assert_eq!(format!("{}", PayPlatform::Other), "other");
assert_eq!(
"alipay".parse::<PayPlatform>().unwrap(),
PayPlatform::Alipay
);
assert_eq!(
"wechatpay".parse::<PayPlatform>().unwrap(),
PayPlatform::WechatPay
);
assert_eq!("other".parse::<PayPlatform>().unwrap(), PayPlatform::Other);
assert_eq!("ali".parse::<PayPlatform>().unwrap(), PayPlatform::Alipay);
assert_eq!(
"wechat".parse::<PayPlatform>().unwrap(),
PayPlatform::WechatPay
);
assert_eq!(
"ALIPAY".parse::<PayPlatform>().unwrap(),
PayPlatform::Alipay
);
assert!("unknown".parse::<PayPlatform>().is_err());
let set = std::collections::HashSet::from([PayPlatform::Alipay, PayPlatform::WechatPay]);
assert!(set.contains(&PayPlatform::Alipay));
assert!(!set.contains(&PayPlatform::Other));
}
#[test]
fn test_pay_config_builder() {
let config = PayConfig::new(PayPlatform::Alipay, "2021001")
.with_merchant_private_key("MIIEvQIBADANB")
.with_platform_public_key("MIIBIjANBgkqh")
.with_notify_url("https://example.com/notify")
.with_return_url("https://example.com/return")
.with_sandbox(true)
.with_mode("app");
assert_eq!(config.platform, PayPlatform::Alipay);
assert_eq!(config.app_id, "2021001");
assert_eq!(config.merchant_private_key, "MIIEvQIBADANB");
assert_eq!(config.platform_public_key, "MIIBIjANBgkqh");
assert_eq!(config.notify_url, "https://example.com/notify");
assert_eq!(
config.return_url.as_deref(),
Some("https://example.com/return")
);
assert!(config.sandbox);
assert_eq!(config.mode, "app");
assert!(config.validate().is_ok());
let minimal = PayConfig::new(PayPlatform::WechatPay, "wx123");
assert_eq!(minimal.platform, PayPlatform::WechatPay);
assert_eq!(minimal.app_id, "wx123");
assert!(minimal.merchant_private_key.is_empty());
assert!(minimal.platform_public_key.is_empty());
assert!(minimal.notify_url.is_empty());
assert!(minimal.return_url.is_none());
assert!(!minimal.sandbox);
assert_eq!(minimal.mode, "web");
let bad = PayConfig::new(PayPlatform::Alipay, "");
let err = bad.validate().unwrap_err();
match err {
PayError::Config(field) => assert_eq!(field, "app_id"),
other => panic!("期望 Config, 实际 {other:?}"),
}
let bad = PayConfig::new(PayPlatform::Alipay, "app1");
let err = bad.validate().unwrap_err();
match err {
PayError::Config(field) => assert_eq!(field, "merchant_private_key"),
other => panic!("期望 Config, 实际 {other:?}"),
}
let bad = PayConfig::new(PayPlatform::Alipay, "app1")
.with_merchant_private_key("k1")
.with_platform_public_key("k2");
let err = bad.validate().unwrap_err();
match err {
PayError::Config(field) => assert_eq!(field, "notify_url"),
other => panic!("期望 Config, 实际 {other:?}"),
}
}
#[test]
fn test_pay_order_builder() {
let order = PayOrder::new()
.out_trade_no("202401010001")
.total_amount(8800)
.subject("鲜视达商品")
.body("新鲜蔬菜套餐")
.notify_url("https://example.com/notify")
.return_url("https://example.com/return")
.timeout_express(1800)
.passback_params("merchant_extra")
.extra(serde_json::json!({"channel": "alipay_app"}));
assert_eq!(order.out_trade_no, "202401010001");
assert_eq!(order.total_amount, 8800);
assert_eq!(order.subject, "鲜视达商品");
assert_eq!(order.body.as_deref(), Some("新鲜蔬菜套餐"));
assert_eq!(
order.notify_url.as_deref(),
Some("https://example.com/notify")
);
assert_eq!(
order.return_url.as_deref(),
Some("https://example.com/return")
);
assert_eq!(order.timeout_express, Some(1800));
assert_eq!(order.passback_params.as_deref(), Some("merchant_extra"));
assert_eq!(order.extra["channel"], "alipay_app");
assert!(order.validate().is_ok());
}
#[test]
fn test_pay_order_validate() {
let order = PayOrder::new()
.out_trade_no("202401010001")
.total_amount(100)
.subject("标题");
assert!(order.validate().is_ok());
let order = PayOrder::new().total_amount(100).subject("标题");
let err = order.validate().unwrap_err();
match err {
PayError::MissingField(field) => assert_eq!(field, "out_trade_no"),
other => panic!("期望 MissingField, 实际 {other:?}"),
}
let order = PayOrder::new()
.out_trade_no("202401010001")
.total_amount(0)
.subject("标题");
let err = order.validate().unwrap_err();
match err {
PayError::MissingField(field) => assert_eq!(field, "total_amount"),
other => panic!("期望 MissingField, 实际 {other:?}"),
}
let order = PayOrder::new()
.out_trade_no("202401010001")
.total_amount(-1)
.subject("标题");
let err = order.validate().unwrap_err();
match err {
PayError::MissingField(field) => assert_eq!(field, "total_amount"),
other => panic!("期望 MissingField, 实际 {other:?}"),
}
let order = PayOrder::new()
.out_trade_no("202401010001")
.total_amount(100);
let err = order.validate().unwrap_err();
match err {
PayError::MissingField(field) => assert_eq!(field, "subject"),
other => panic!("期望 MissingField, 实际 {other:?}"),
}
let err = PayOrder::default().validate().unwrap_err();
match err {
PayError::MissingField(field) => assert_eq!(field, "out_trade_no"),
other => panic!("期望 MissingField, 实际 {other:?}"),
}
}
#[test]
fn test_refund_order_builder() {
let refund = RefundOrder::new()
.out_trade_no("202401010001")
.refund_amount(5000)
.out_request_no("R202401010001")
.reason("用户申请退款");
assert_eq!(refund.out_trade_no, "202401010001");
assert_eq!(refund.refund_amount, 5000);
assert_eq!(refund.out_request_no, "R202401010001");
assert_eq!(refund.reason.as_deref(), Some("用户申请退款"));
assert!(refund.validate().is_ok());
let refund = RefundOrder::new().refund_amount(5000).out_request_no("R1");
let err = refund.validate().unwrap_err();
match err {
PayError::MissingField(field) => assert_eq!(field, "out_trade_no"),
other => panic!("期望 MissingField, 实际 {other:?}"),
}
let refund = RefundOrder::new()
.out_trade_no("T1")
.refund_amount(0)
.out_request_no("R1");
let err = refund.validate().unwrap_err();
match err {
PayError::MissingField(field) => assert_eq!(field, "refund_amount"),
other => panic!("期望 MissingField, 实际 {other:?}"),
}
let refund = RefundOrder::new().out_trade_no("T1").refund_amount(100);
let err = refund.validate().unwrap_err();
match err {
PayError::MissingField(field) => assert_eq!(field, "out_request_no"),
other => panic!("期望 MissingField, 实际 {other:?}"),
}
}
#[test]
fn test_pay_result_default() {
let result = PayResult::default();
assert!(result.trade_no.is_empty());
assert!(result.out_trade_no.is_empty());
assert_eq!(result.total_amount, 0);
assert!(result.trade_status.is_empty());
assert!(result.raw.is_null());
let result = PayResult {
trade_no: "2024MEM001".to_string(),
out_trade_no: "ORD001".to_string(),
total_amount: 8800,
trade_status: "TRADE_SUCCESS".to_string(),
raw: serde_json::json!({"code": "00"}),
};
let json = serde_json::to_string(&result).expect("序列化失败");
let back: PayResult = serde_json::from_str(&json).expect("反序列化失败");
assert_eq!(back.trade_no, "2024MEM001");
assert_eq!(back.out_trade_no, "ORD001");
assert_eq!(back.total_amount, 8800);
assert_eq!(back.trade_status, "TRADE_SUCCESS");
assert_eq!(back.raw["code"], "00");
}
#[test]
fn test_memory_pay_provider_pay() {
let provider = MemoryPayProvider::new();
let order = PayOrder::new()
.out_trade_no("202401010001")
.total_amount(8800)
.subject("鲜视达商品")
.body("新鲜蔬菜");
let result = provider.pay(order).expect("支付应成功");
assert_eq!(result.out_trade_no, "202401010001");
assert_eq!(result.total_amount, 8800);
assert_eq!(result.trade_status, "WAIT_BUYER_PAY");
assert!(result.trade_no.starts_with("MEM"));
assert_eq!(result.trade_no, "MEM202401010001");
assert_eq!(result.raw["out_trade_no"], "202401010001");
assert_eq!(result.raw["total_amount"], 8800);
assert_eq!(result.raw["subject"], "鲜视达商品");
assert_eq!(provider.orders().len(), 1);
let dup = PayOrder::new()
.out_trade_no("202401010001")
.total_amount(100)
.subject("重复订单");
let err = provider.pay(dup).unwrap_err();
match err {
PayError::RequestFailed(msg) => assert!(msg.contains("订单号已存在")),
other => panic!("期望 RequestFailed, 实际 {other:?}"),
}
assert_eq!(provider.orders().len(), 1);
}
#[test]
fn test_memory_pay_provider_query() {
let provider = MemoryPayProvider::new();
let err = provider.query("NOT_EXIST").unwrap_err();
match err {
PayError::QueryFailed(msg) => assert!(msg.contains("订单不存在")),
other => panic!("期望 QueryFailed, 实际 {other:?}"),
}
let order = PayOrder::new()
.out_trade_no("Q001")
.total_amount(1000)
.subject("查询测试");
provider.pay(order).expect("支付应成功");
let result = provider.query("Q001").expect("查询应成功");
assert_eq!(result.out_trade_no, "Q001");
assert_eq!(result.total_amount, 1000);
assert_eq!(result.trade_no, "MEMQ001");
let preset = PayResult {
trade_no: "PRESET001".to_string(),
out_trade_no: "ANY".to_string(),
total_amount: 9999,
trade_status: "TRADE_SUCCESS".to_string(),
raw: serde_json::json!({"preset": true}),
};
provider.set_query_result(preset);
let result = provider.query("NOT_EXIST").expect("应返回预置结果");
assert_eq!(result.trade_no, "PRESET001");
assert_eq!(result.total_amount, 9999);
assert_eq!(result.trade_status, "TRADE_SUCCESS");
assert_eq!(result.raw["preset"], true);
provider.clear();
let err = provider.query("NOT_EXIST").unwrap_err();
match err {
PayError::QueryFailed(_) => {}
other => panic!("期望 QueryFailed, 实际 {other:?}"),
}
}
#[test]
fn test_memory_pay_provider_close() {
let provider = MemoryPayProvider::new();
let err = provider.close("NOT_EXIST").unwrap_err();
match err {
PayError::RequestFailed(msg) => assert!(msg.contains("订单不存在")),
other => panic!("期望 RequestFailed, 实际 {other:?}"),
}
let order = PayOrder::new()
.out_trade_no("C001")
.total_amount(500)
.subject("关闭测试");
provider.pay(order).expect("支付应成功");
provider.close("C001").expect("关闭应成功");
let result = provider.query("C001").expect("查询应成功");
assert_eq!(result.trade_status, "CLOSED");
}
#[test]
fn test_memory_pay_provider_refund() {
let provider = MemoryPayProvider::new();
let refund = RefundOrder::new()
.out_trade_no("NOT_EXIST")
.refund_amount(100)
.out_request_no("R001");
let err = provider.refund(refund).unwrap_err();
match err {
PayError::RefundFailed(msg) => assert!(msg.contains("原订单不存在")),
other => panic!("期望 RefundFailed, 实际 {other:?}"),
}
assert_eq!(provider.refunds().len(), 0);
let order = PayOrder::new()
.out_trade_no("R001")
.total_amount(1000)
.subject("退款测试");
provider.pay(order).expect("支付应成功");
let refund = RefundOrder::new()
.out_trade_no("R001")
.refund_amount(500)
.out_request_no("RR001")
.reason("商品缺货");
provider.refund(refund).expect("退款应成功");
assert_eq!(provider.refunds().len(), 1);
let stored = &provider.refunds()[0];
assert_eq!(stored.out_trade_no, "R001");
assert_eq!(stored.refund_amount, 500);
assert_eq!(stored.out_request_no, "RR001");
assert_eq!(stored.reason.as_deref(), Some("商品缺货"));
let bad = RefundOrder::new()
.out_trade_no("R001")
.refund_amount(0) .out_request_no("RR002");
let err = provider.refund(bad).unwrap_err();
match err {
PayError::MissingField(field) => assert_eq!(field, "refund_amount"),
other => panic!("期望 MissingField, 实际 {other:?}"),
}
assert_eq!(provider.refunds().len(), 1);
}
#[test]
fn test_memory_pay_provider_verify_notify() {
let provider = MemoryPayProvider::new();
let params = serde_json::json!({
"out_trade_no": "CB001",
"trade_no": "2024ALIPAY001",
"total_amount": 8800,
"trade_status": "TRADE_SUCCESS",
"buyer_id": "2088000000000001"
});
let result = provider.verify_notify(¶ms).expect("验证应成功");
assert_eq!(result.out_trade_no, "CB001");
assert_eq!(result.trade_no, "2024ALIPAY001");
assert_eq!(result.total_amount, 8800);
assert_eq!(result.trade_status, "TRADE_SUCCESS");
assert_eq!(result.raw["buyer_id"], "2088000000000001");
let params = serde_json::json!({
"trade_no": "2024ALIPAY001",
"total_amount": 8800
});
let err = provider.verify_notify(¶ms).unwrap_err();
match err {
PayError::VerifyFailed(msg) => assert!(msg.contains("out_trade_no")),
other => panic!("期望 VerifyFailed, 实际 {other:?}"),
}
let params = serde_json::json!({
"out_trade_no": "CB002",
"trade_no": "T002"
});
let result = provider.verify_notify(¶ms).expect("验证应成功");
assert_eq!(result.out_trade_no, "CB002");
assert_eq!(result.trade_no, "T002");
assert_eq!(result.total_amount, 0); assert_eq!(result.trade_status, "TRADE_SUCCESS"); }
#[test]
fn test_memory_pay_provider_missing_fields() {
let provider = MemoryPayProvider::new();
let order = PayOrder::new().total_amount(100).subject("标题");
let err = provider.pay(order).unwrap_err();
match err {
PayError::MissingField(field) => assert_eq!(field, "out_trade_no"),
other => panic!("期望 MissingField, 实际 {other:?}"),
}
assert_eq!(provider.orders().len(), 0);
let order = PayOrder::new()
.out_trade_no("M001")
.total_amount(0)
.subject("标题");
let err = provider.pay(order).unwrap_err();
match err {
PayError::MissingField(field) => assert_eq!(field, "total_amount"),
other => panic!("期望 MissingField, 实际 {other:?}"),
}
assert_eq!(provider.orders().len(), 0);
let order = PayOrder::new().out_trade_no("M002").total_amount(100);
let err = provider.pay(order).unwrap_err();
match err {
PayError::MissingField(field) => assert_eq!(field, "subject"),
other => panic!("期望 MissingField, 实际 {other:?}"),
}
assert_eq!(provider.orders().len(), 0);
let err = provider.pay(PayOrder::default()).unwrap_err();
match err {
PayError::MissingField(field) => assert_eq!(field, "out_trade_no"),
other => panic!("期望 MissingField, 实际 {other:?}"),
}
assert_eq!(provider.orders().len(), 0);
}
#[test]
fn test_memory_pay_http_transport_post_json() {
let transport = MemoryPayHttpTransport::new();
let err = transport
.post_json("https://api.example.com/pay", "{}")
.unwrap_err();
match err {
PayError::HttpTransport(msg) => assert!(msg.contains("无可用预置响应")),
other => panic!("期望 HttpTransport, 实际 {other:?}"),
}
assert_eq!(transport.request_count(), 0);
transport.push_response(r#"{"code":"00","msg":"success"}"#);
let resp = transport
.post_json("https://api.example.com/pay", r#"{"out_trade_no":"P001"}"#)
.expect("应返回预置响应");
assert_eq!(resp, r#"{"code":"00","msg":"success"}"#);
assert_eq!(transport.request_count(), 1);
let requests = transport.requests();
assert_eq!(requests.len(), 1);
assert_eq!(requests[0].0, "POST");
assert_eq!(requests[0].1, "https://api.example.com/pay");
assert_eq!(requests[0].2, r#"{"out_trade_no":"P001"}"#);
let err = transport.post_json("url", "{}").unwrap_err();
match err {
PayError::HttpTransport(_) => {}
other => panic!("期望 HttpTransport, 实际 {other:?}"),
}
assert_eq!(transport.request_count(), 1);
}
#[test]
fn test_memory_pay_http_transport_get() {
let transport = MemoryPayHttpTransport::new();
let err = transport.get("https://api.example.com/query").unwrap_err();
match err {
PayError::HttpTransport(msg) => assert!(msg.contains("无可用预置响应")),
other => panic!("期望 HttpTransport, 实际 {other:?}"),
}
assert_eq!(transport.request_count(), 0);
transport.push_response(r#"{"trade_status":"TRADE_SUCCESS"}"#);
let resp = transport
.get("https://api.example.com/query?out_trade_no=Q001")
.expect("应返回预置响应");
assert_eq!(resp, r#"{"trade_status":"TRADE_SUCCESS"}"#);
assert_eq!(transport.request_count(), 1);
let requests = transport.requests();
assert_eq!(requests.len(), 1);
assert_eq!(requests[0].0, "GET");
assert_eq!(
requests[0].1,
"https://api.example.com/query?out_trade_no=Q001"
);
assert_eq!(requests[0].2, "");
transport.clear();
assert_eq!(transport.request_count(), 0);
assert!(transport.get("url").is_err());
}
#[test]
fn test_memory_pay_http_transport_queue() {
let transport = MemoryPayHttpTransport::new();
transport.push_response("resp1");
transport.push_response("resp2");
transport.push_response("resp3");
let r1 = transport.post_json("url1", "body1").expect("应返回 resp1");
assert_eq!(r1, "resp1");
let r2 = transport.get("url2").expect("应返回 resp2");
assert_eq!(r2, "resp2");
let r3 = transport.post_json("url3", "body3").expect("应返回 resp3");
assert_eq!(r3, "resp3");
assert!(transport.post_json("url4", "body4").is_err());
assert!(transport.get("url4").is_err());
assert_eq!(transport.request_count(), 3);
let requests = transport.requests();
assert_eq!(
requests[0],
("POST".to_string(), "url1".to_string(), "body1".to_string())
);
assert_eq!(
requests[1],
("GET".to_string(), "url2".to_string(), String::new())
);
assert_eq!(
requests[2],
("POST".to_string(), "url3".to_string(), "body3".to_string())
);
}
}