use serde::{Deserialize, Serialize};
use std::sync::Arc;
use crate::auth::Signer;
use crate::config::WxPayConfig;
use crate::error::WxPayResult;
use crate::http::{HttpClient, HttpMethod};
use crate::services::transport::{ServiceTransport, TransportObserver};
#[derive(Debug, Clone, Serialize)]
pub struct JsapiRequest {
pub appid: String,
pub mchid: String,
pub description: String,
pub out_trade_no: String,
pub amount: Option<Amount>,
pub payer: Option<Payer>,
pub notify_url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Amount {
pub total: u64,
pub currency: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Payer {
pub openid: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct JsapiResponse {
pub prepay_id: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct JsapiPayParams {
pub timestamp: String,
pub nonce_str: String,
pub prepay_id: String,
pub sign_type: String,
pub pay_sign: String,
}
#[allow(dead_code)]
pub struct JsapiService {
config: Arc<WxPayConfig>,
http_client: Arc<dyn HttpClient>,
signer: Arc<dyn Signer>,
transport: ServiceTransport,
}
impl JsapiService {
pub fn new(
config: Arc<WxPayConfig>,
http_client: Arc<dyn HttpClient>,
signer: Arc<dyn Signer>,
) -> Self {
Self::new_with_observer(config.clone(), http_client.clone(), signer.clone(), None)
}
pub fn new_with_observer(
config: Arc<WxPayConfig>,
http_client: Arc<dyn HttpClient>,
signer: Arc<dyn Signer>,
transport_observer: Option<Arc<dyn TransportObserver>>,
) -> Self {
Self {
config: config.clone(),
http_client: http_client.clone(),
signer: signer.clone(),
transport: ServiceTransport::new_with_observer(
config,
http_client,
signer,
transport_observer,
),
}
}
pub async fn create_order(&self, request: &JsapiRequest) -> WxPayResult<JsapiResponse> {
let body = serde_json::to_string(request)?;
self.transport
.request(
HttpMethod::Post,
"/v3/pay/transactions/jsapi",
Some(&body),
"payments.jsapi.create_order",
)
.await
}
pub async fn prepay(&self, request: &JsapiRequest) -> WxPayResult<JsapiResponse> {
self.create_order(request).await
}
pub async fn build_pay_params(&self, prepay_id: &str) -> WxPayResult<JsapiPayParams> {
self.generate_pay_params(prepay_id).await
}
pub async fn generate_pay_params(&self, prepay_id: &str) -> WxPayResult<JsapiPayParams> {
let timestamp = crate::utils::timestamp::get_timestamp();
let nonce = crate::utils::nonce::generate_nonce();
let message = format!("{}\n{}\nprepay_id={}\n", timestamp, nonce, prepay_id);
let signature = self.signer.sign(&message).await?;
Ok(JsapiPayParams {
timestamp: timestamp.to_string(),
nonce_str: nonce,
prepay_id: prepay_id.to_string(),
sign_type: "RSA".to_string(),
pay_sign: signature,
})
}
}
impl std::fmt::Debug for JsapiService {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("JsapiService").finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_jsapi_request_serialization() {
let request = JsapiRequest {
appid: "wx88888888".to_string(),
mchid: "1900000109".to_string(),
description: "测试商品".to_string(),
out_trade_no: "test_trade_no_123".to_string(),
amount: Some(Amount {
total: 100,
currency: Some("CNY".to_string()),
}),
payer: Some(Payer {
openid: "test_openid".to_string(),
}),
notify_url: None,
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("wx88888888"));
assert!(json.contains("1900000109"));
assert!(json.contains("测试商品"));
}
#[test]
fn test_jsapi_response_deserialization() {
let json = r#"{"prepay_id":"wx201410272009395522657a690ac89ed300"}"#;
let response: JsapiResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.prepay_id, "wx201410272009395522657a690ac89ed300");
}
}