4b529a8952
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m59s
Project CI / Backend tests (pull_request) Failing after 4m8s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 5m9s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 6m20s
Project CI / Frontend tests (pull_request) Successful in 3m17s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m48s
Project CI / Repository checks (pull_request) Failing after 4m15s
Project CI / Native shell tests (pull_request) Successful in 6m52s
新增支付应用、API Key 与外部产品订单接口 接入微信 Native 下单、二维码收银台与支付结果回调 新增后台支付订单与回调投递页面 补齐 SpacetimeDB 支付表、生成绑定、OpenAPI 与使用说明
557 lines
21 KiB
Rust
557 lines
21 KiB
Rust
use axum::{
|
|
Extension, Json,
|
|
body::Bytes,
|
|
extract::{Path, State},
|
|
http::{HeaderMap, StatusCode},
|
|
};
|
|
use hmac::{Hmac, Mac};
|
|
use serde::Deserialize;
|
|
use serde_json::{Value, json};
|
|
use sha2::Sha256;
|
|
use shared_contracts::payment::{
|
|
PaymentApiKeyResponse, PaymentAppCreateRequest, PaymentAppCreateResponse,
|
|
PaymentAppListResponse, PaymentAppResponse, PaymentOrderCreateRequest,
|
|
PaymentOrderCreateResponse, PaymentOrderResponse,
|
|
};
|
|
use shared_kernel::{build_prefixed_uuid_id, offset_datetime_to_unix_micros, parse_rfc3339};
|
|
use spacetime_client::{
|
|
PaymentApiKeyCreateRecordInput, PaymentAppCreateRecordInput, PaymentOrderCreateRecordInput,
|
|
PaymentOrderGetRecordInput, PaymentOrderMarkPaidRecordInput,
|
|
PaymentOrderProviderAttemptRecordInput, PaymentOrderProviderCodeRecordInput,
|
|
PaymentOrderRecord,
|
|
};
|
|
|
|
use crate::{
|
|
api_response::json_success_body,
|
|
auth::AuthenticatedAccessToken,
|
|
editor_project::current_utc_micros,
|
|
external_api_auth::ExternalApiPrincipal,
|
|
external_api_keys::hash_external_api_key,
|
|
http_error::AppError,
|
|
request_context::RequestContext,
|
|
state::AppState,
|
|
wechat::pay::{build_wechat_payment_service_order_request, map_wechat_pay_error},
|
|
};
|
|
|
|
const PAYMENT_APP_ID_PREFIX: &str = "payment-app-";
|
|
const PAYMENT_KEY_ID_PREFIX: &str = "payment-key-";
|
|
const PAYMENT_CHECKOUT_TOKEN_PREFIX: &str = "checkout-";
|
|
const PAYMENT_RAW_KEY_PREFIX: &str = "tnr_pay_";
|
|
const PAYMENT_CHECKOUT_EXPIRATION_SECONDS: i64 = 5 * 60;
|
|
const PAYMENT_IDEMPOTENCY_HEADER: &str = "idempotency-key";
|
|
|
|
pub async fn list_payment_apps(
|
|
State(state): State<AppState>,
|
|
Extension(request_context): Extension<RequestContext>,
|
|
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
|
) -> Result<Json<Value>, AppError> {
|
|
let apps = state
|
|
.spacetime_client()
|
|
.list_payment_apps(authenticated.claims().user_id().to_string())
|
|
.await
|
|
.map_err(map_payment_client_error)?
|
|
.into_iter()
|
|
.map(payment_app_response)
|
|
.collect();
|
|
let keys = state
|
|
.spacetime_client()
|
|
.list_payment_api_keys(authenticated.claims().user_id().to_string())
|
|
.await
|
|
.map_err(map_payment_client_error)?
|
|
.into_iter()
|
|
.map(payment_api_key_response)
|
|
.collect();
|
|
|
|
Ok(json_success_body(
|
|
Some(&request_context),
|
|
PaymentAppListResponse { apps, keys },
|
|
))
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PaymentAppUpdateRequest {
|
|
pub enabled: bool,
|
|
}
|
|
|
|
pub async fn update_payment_app(
|
|
State(state): State<AppState>,
|
|
Extension(request_context): Extension<RequestContext>,
|
|
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
|
Path(app_id): Path<String>,
|
|
Json(payload): Json<PaymentAppUpdateRequest>,
|
|
) -> Result<Json<Value>, AppError> {
|
|
let app = state
|
|
.spacetime_client()
|
|
.update_payment_app(spacetime_client::PaymentAppUpdateRecordInput {
|
|
app_id,
|
|
owner_user_id: authenticated.claims().user_id().to_string(),
|
|
enabled: payload.enabled,
|
|
updated_at_micros: current_utc_micros(),
|
|
})
|
|
.await
|
|
.map_err(map_payment_client_error)?;
|
|
Ok(json_success_body(
|
|
Some(&request_context),
|
|
PaymentAppResponse {
|
|
app_id: app.app_id,
|
|
name: app.name,
|
|
callback_url: app.callback_url,
|
|
enabled: app.enabled,
|
|
created_at: module_runtime::format_utc_micros(app.created_at_micros),
|
|
updated_at: module_runtime::format_utc_micros(app.updated_at_micros),
|
|
},
|
|
))
|
|
}
|
|
|
|
pub async fn revoke_payment_api_key(
|
|
State(state): State<AppState>,
|
|
Extension(request_context): Extension<RequestContext>,
|
|
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
|
Path(key_id): Path<String>,
|
|
) -> Result<Json<Value>, AppError> {
|
|
let key = state
|
|
.spacetime_client()
|
|
.revoke_payment_api_key(spacetime_client::PaymentApiKeyRevokeRecordInput {
|
|
key_id,
|
|
owner_user_id: authenticated.claims().user_id().to_string(),
|
|
revoked_at_micros: current_utc_micros(),
|
|
})
|
|
.await
|
|
.map_err(map_payment_client_error)?;
|
|
Ok(json_success_body(
|
|
Some(&request_context),
|
|
payment_api_key_response(key),
|
|
))
|
|
}
|
|
|
|
pub async fn create_payment_app(
|
|
State(state): State<AppState>,
|
|
Extension(request_context): Extension<RequestContext>,
|
|
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
|
Json(payload): Json<PaymentAppCreateRequest>,
|
|
) -> Result<Json<Value>, AppError> {
|
|
let user_id = authenticated.claims().user_id().to_string();
|
|
let now_micros = current_utc_micros();
|
|
let app = state
|
|
.spacetime_client()
|
|
.create_payment_app(PaymentAppCreateRecordInput {
|
|
app_id: build_prefixed_uuid_id(PAYMENT_APP_ID_PREFIX),
|
|
owner_user_id: user_id.clone(),
|
|
name: payload.name,
|
|
callback_url: payload.callback_url,
|
|
now_micros,
|
|
})
|
|
.await
|
|
.map_err(map_payment_client_error)?;
|
|
|
|
let raw_key = format!(
|
|
"{PAYMENT_RAW_KEY_PREFIX}{}.{}",
|
|
shared_kernel::new_uuid_simple_string(),
|
|
shared_kernel::new_uuid_simple_string()
|
|
);
|
|
let key = state
|
|
.spacetime_client()
|
|
.create_payment_api_key(PaymentApiKeyCreateRecordInput {
|
|
key_id: build_prefixed_uuid_id(PAYMENT_KEY_ID_PREFIX),
|
|
app_id: app.app_id.clone(),
|
|
owner_user_id: user_id,
|
|
name: "支付服务默认 Key".to_string(),
|
|
key_prefix: raw_key.chars().take(18).collect(),
|
|
key_hash: hash_external_api_key(raw_key.as_str()),
|
|
scopes: vec![
|
|
module_runtime::PAYMENT_SCOPE_ORDERS_CREATE.to_string(),
|
|
module_runtime::PAYMENT_SCOPE_ORDERS_READ.to_string(),
|
|
module_runtime::PAYMENT_SCOPE_ORDERS_CLOSE.to_string(),
|
|
],
|
|
now_micros,
|
|
})
|
|
.await
|
|
.map_err(map_payment_client_error)?;
|
|
|
|
Ok(json_success_body(
|
|
Some(&request_context),
|
|
PaymentAppCreateResponse {
|
|
app: payment_app_response(app),
|
|
api_key: raw_key,
|
|
key: payment_api_key_response(key),
|
|
},
|
|
))
|
|
}
|
|
|
|
pub async fn create_external_payment_order(
|
|
State(state): State<AppState>,
|
|
Extension(request_context): Extension<RequestContext>,
|
|
Extension(principal): Extension<ExternalApiPrincipal>,
|
|
headers: HeaderMap,
|
|
Json(payload): Json<PaymentOrderCreateRequest>,
|
|
) -> Result<Json<Value>, AppError> {
|
|
require_payment_scope(&principal, module_runtime::PAYMENT_SCOPE_ORDERS_CREATE)?;
|
|
let app_id = principal.payment_app_id().ok_or_else(|| {
|
|
AppError::from_status(StatusCode::FORBIDDEN).with_message("该 Key 不是支付应用 Key")
|
|
})?;
|
|
let idempotency_key = headers
|
|
.get(PAYMENT_IDEMPOTENCY_HEADER)
|
|
.and_then(|value| value.to_str().ok())
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty())
|
|
.ok_or_else(|| {
|
|
AppError::from_status(StatusCode::BAD_REQUEST).with_message("缺少 Idempotency-Key")
|
|
})?
|
|
.to_string();
|
|
if payload.provider != module_runtime::PAYMENT_PROVIDER_WECHAT_NATIVE {
|
|
return Err(AppError::from_status(StatusCode::BAD_REQUEST)
|
|
.with_message("当前仅支持微信 Native 支付"));
|
|
}
|
|
let now_micros = current_utc_micros();
|
|
let expires_at_micros = now_micros
|
|
.checked_add(PAYMENT_CHECKOUT_EXPIRATION_SECONDS * 1_000_000)
|
|
.ok_or_else(|| {
|
|
AppError::from_status(StatusCode::BAD_REQUEST).with_message("订单有效期无效")
|
|
})?;
|
|
let items_json = serde_json::to_string(&payload.items).map_err(|_| {
|
|
AppError::from_status(StatusCode::BAD_REQUEST).with_message("商品明细格式无效")
|
|
})?;
|
|
let order = state
|
|
.spacetime_client()
|
|
.create_payment_order(PaymentOrderCreateRecordInput {
|
|
order_id: shared_kernel::new_uuid_simple_string(),
|
|
app_id: app_id.to_string(),
|
|
owner_user_id: principal.owner_user_id().to_string(),
|
|
merchant_order_id: payload.merchant_order_id,
|
|
idempotency_lookup: format!("{app_id}:{idempotency_key}"),
|
|
title: payload.title,
|
|
items_json,
|
|
amount_cents: payload.amount_cents,
|
|
currency: payload.currency,
|
|
provider: module_runtime::PaymentProvider::WechatNative,
|
|
checkout_token: format!(
|
|
"{PAYMENT_CHECKOUT_TOKEN_PREFIX}{}",
|
|
shared_kernel::new_uuid_simple_string()
|
|
),
|
|
now_micros,
|
|
expires_at_micros,
|
|
callback_url: principal.payment_callback_url().map(str::to_string),
|
|
callback_signing_key_hash: Some(principal.payment_key_hash().to_string()),
|
|
})
|
|
.await
|
|
.map_err(map_payment_client_error)?;
|
|
|
|
if order.provider_code_url.is_some()
|
|
|| matches!(
|
|
order.status,
|
|
module_runtime::PaymentOrderStatus::Paid
|
|
| module_runtime::PaymentOrderStatus::Closed
|
|
| module_runtime::PaymentOrderStatus::Expired
|
|
)
|
|
{
|
|
return Ok(json_success_body(
|
|
Some(&request_context),
|
|
PaymentOrderCreateResponse {
|
|
order: payment_order_response(order),
|
|
},
|
|
));
|
|
}
|
|
|
|
let order = state
|
|
.spacetime_client()
|
|
.mark_payment_order_provider_attempted(PaymentOrderProviderAttemptRecordInput {
|
|
order_id: order.order_id,
|
|
attempted_at_micros: current_utc_micros(),
|
|
})
|
|
.await
|
|
.map_err(map_payment_client_error)?;
|
|
let provider_payment = state
|
|
.wechat_pay_client()
|
|
.create_native_order(build_wechat_payment_service_order_request(
|
|
order.order_id.clone(),
|
|
order.title.clone(),
|
|
order.amount_cents,
|
|
resolve_payment_client_ip(&headers),
|
|
resolve_payment_notify_url()?,
|
|
))
|
|
.await
|
|
.map_err(map_wechat_pay_error)?;
|
|
let order = state
|
|
.spacetime_client()
|
|
.set_payment_order_provider_code(PaymentOrderProviderCodeRecordInput {
|
|
order_id: order.order_id,
|
|
provider_code_url: provider_payment.code_url,
|
|
updated_at_micros: current_utc_micros(),
|
|
})
|
|
.await
|
|
.map_err(map_payment_client_error)?;
|
|
|
|
Ok(json_success_body(
|
|
Some(&request_context),
|
|
PaymentOrderCreateResponse {
|
|
order: payment_order_response(order),
|
|
},
|
|
))
|
|
}
|
|
|
|
pub async fn get_external_payment_order(
|
|
State(state): State<AppState>,
|
|
Extension(request_context): Extension<RequestContext>,
|
|
Extension(principal): Extension<ExternalApiPrincipal>,
|
|
Path(order_id): Path<String>,
|
|
) -> Result<Json<Value>, AppError> {
|
|
require_payment_scope(&principal, module_runtime::PAYMENT_SCOPE_ORDERS_READ)?;
|
|
let order = state
|
|
.spacetime_client()
|
|
.get_payment_order(PaymentOrderGetRecordInput {
|
|
order_id,
|
|
owner_user_id: Some(principal.owner_user_id().to_string()),
|
|
})
|
|
.await
|
|
.map_err(map_payment_client_error)?;
|
|
Ok(json_success_body(
|
|
Some(&request_context),
|
|
PaymentOrderCreateResponse {
|
|
order: payment_order_response(order),
|
|
},
|
|
))
|
|
}
|
|
|
|
pub async fn get_public_payment_checkout(
|
|
State(state): State<AppState>,
|
|
Extension(request_context): Extension<RequestContext>,
|
|
Path(checkout_token): Path<String>,
|
|
) -> Result<Json<Value>, AppError> {
|
|
let order = state
|
|
.spacetime_client()
|
|
.get_payment_order_by_checkout_token(checkout_token)
|
|
.await
|
|
.map_err(map_payment_client_error)?;
|
|
Ok(json_success_body(
|
|
Some(&request_context),
|
|
PaymentOrderCreateResponse {
|
|
order: payment_order_response(order),
|
|
},
|
|
))
|
|
}
|
|
|
|
pub async fn handle_payment_wechat_notify(
|
|
State(state): State<AppState>,
|
|
headers: HeaderMap,
|
|
body: Bytes,
|
|
) -> Result<StatusCode, AppError> {
|
|
let notify = state
|
|
.wechat_pay_client()
|
|
.parse_notify(&headers, &body)
|
|
.map_err(map_wechat_pay_error)?;
|
|
if notify.trade_state != "SUCCESS" {
|
|
return Ok(StatusCode::NO_CONTENT);
|
|
}
|
|
let transaction_id = notify.transaction_id.ok_or_else(|| {
|
|
AppError::from_status(StatusCode::BAD_REQUEST)
|
|
.with_message("微信支付成功通知缺少 transaction_id")
|
|
})?;
|
|
let amount_cents = notify.amount_total_cents.ok_or_else(|| {
|
|
AppError::from_status(StatusCode::BAD_REQUEST).with_message("微信支付成功通知缺少金额")
|
|
})?;
|
|
let paid_at_micros = notify
|
|
.success_time
|
|
.as_deref()
|
|
.ok_or_else(|| {
|
|
AppError::from_status(StatusCode::BAD_REQUEST)
|
|
.with_message("微信支付成功通知缺少 success_time")
|
|
})
|
|
.and_then(|value| {
|
|
parse_rfc3339(value)
|
|
.map(offset_datetime_to_unix_micros)
|
|
.map_err(|_| {
|
|
AppError::from_status(StatusCode::BAD_REQUEST)
|
|
.with_message("微信支付成功通知 success_time 无效")
|
|
})
|
|
})?;
|
|
let order = state
|
|
.spacetime_client()
|
|
.get_payment_order(PaymentOrderGetRecordInput {
|
|
order_id: notify.out_trade_no,
|
|
owner_user_id: None,
|
|
})
|
|
.await
|
|
.map_err(map_payment_client_error)?;
|
|
if order.provider != module_runtime::PaymentProvider::WechatNative
|
|
|| order.amount_cents != amount_cents
|
|
{
|
|
return Err(AppError::from_status(StatusCode::BAD_REQUEST)
|
|
.with_message("微信支付通知与本地支付订单不一致"));
|
|
}
|
|
let paid_order = state
|
|
.spacetime_client()
|
|
.mark_payment_order_paid(PaymentOrderMarkPaidRecordInput {
|
|
order_id: order.order_id,
|
|
provider_trade_no: transaction_id,
|
|
amount_cents,
|
|
paid_at_micros,
|
|
})
|
|
.await
|
|
.map_err(map_payment_client_error)?;
|
|
if paid_order.callback_url.is_some() {
|
|
let callback_key = paid_order
|
|
.callback_signing_key_hash
|
|
.as_deref()
|
|
.filter(|value| !value.is_empty())
|
|
.ok_or_else(|| {
|
|
AppError::from_status(StatusCode::BAD_GATEWAY)
|
|
.with_message("支付订单缺少外部回调签名配置")
|
|
})?;
|
|
let payload_json = serde_json::to_string(&json!({
|
|
"event": "payment.paid",
|
|
"orderId": paid_order.order_id,
|
|
"appId": paid_order.app_id,
|
|
"merchantOrderId": paid_order.merchant_order_id,
|
|
"amountCents": paid_order.amount_cents,
|
|
"currency": paid_order.currency,
|
|
"provider": paid_order.provider.as_str(),
|
|
"providerTradeNo": paid_order.provider_trade_no,
|
|
"status": "paid",
|
|
"paidAt": paid_order.paid_at_micros.map(module_runtime::format_utc_micros),
|
|
}))
|
|
.map_err(|_| {
|
|
AppError::from_status(StatusCode::BAD_GATEWAY).with_message("支付回调内容序列化失败")
|
|
})?;
|
|
let signature = payment_webhook_signature(callback_key, payload_json.as_bytes());
|
|
state
|
|
.spacetime_client()
|
|
.enqueue_payment_webhook(spacetime_client::PaymentWebhookEnqueueRecordInput {
|
|
order_id: paid_order.order_id,
|
|
payload_json,
|
|
signature,
|
|
now_micros: current_utc_micros(),
|
|
})
|
|
.await
|
|
.map_err(map_payment_client_error)?;
|
|
}
|
|
Ok(StatusCode::NO_CONTENT)
|
|
}
|
|
|
|
fn payment_webhook_signature(key_hash: &str, payload: &[u8]) -> String {
|
|
let mut mac = Hmac::<Sha256>::new_from_slice(key_hash.as_bytes())
|
|
.expect("HMAC accepts a non-empty SHA-256 key hash");
|
|
mac.update(payload);
|
|
format!("sha256={}", hex::encode(mac.finalize().into_bytes()))
|
|
}
|
|
|
|
fn payment_app_response(record: spacetime_client::PaymentAppRecord) -> PaymentAppResponse {
|
|
PaymentAppResponse {
|
|
app_id: record.app_id,
|
|
name: record.name,
|
|
callback_url: record.callback_url,
|
|
enabled: record.enabled,
|
|
created_at: module_runtime::format_utc_micros(record.created_at_micros),
|
|
updated_at: module_runtime::format_utc_micros(record.updated_at_micros),
|
|
}
|
|
}
|
|
|
|
fn payment_api_key_response(
|
|
record: spacetime_client::PaymentApiKeyRecord,
|
|
) -> PaymentApiKeyResponse {
|
|
PaymentApiKeyResponse {
|
|
key_id: record.key_id,
|
|
app_id: record.app_id,
|
|
name: record.name,
|
|
key_prefix: record.key_prefix,
|
|
scopes: serde_json::from_str(&record.scopes_json).unwrap_or_default(),
|
|
created_at: module_runtime::format_utc_micros(record.created_at_micros),
|
|
last_used_at: record
|
|
.last_used_at_micros
|
|
.map(module_runtime::format_utc_micros),
|
|
revoked_at: record
|
|
.revoked_at_micros
|
|
.map(module_runtime::format_utc_micros),
|
|
updated_at: module_runtime::format_utc_micros(record.updated_at_micros),
|
|
}
|
|
}
|
|
|
|
fn payment_order_response(record: PaymentOrderRecord) -> PaymentOrderResponse {
|
|
let items = serde_json::from_str::<Vec<Value>>(&record.items_json).unwrap_or_default();
|
|
PaymentOrderResponse {
|
|
order_id: record.order_id,
|
|
app_id: record.app_id,
|
|
merchant_order_id: record.merchant_order_id,
|
|
title: record.title,
|
|
items,
|
|
amount_cents: record.amount_cents,
|
|
currency: record.currency,
|
|
provider: record.provider.as_str().to_string(),
|
|
provider_trade_no: record.provider_trade_no,
|
|
checkout_token: record.checkout_token.clone(),
|
|
status: payment_order_status_text(record.status).to_string(),
|
|
checkout_url: format!("/pay/{}", record.checkout_token),
|
|
provider_qr_code: record.provider_code_url,
|
|
created_at: module_runtime::format_utc_micros(record.created_at_micros),
|
|
expires_at: module_runtime::format_utc_micros(record.expires_at_micros),
|
|
paid_at: record.paid_at_micros.map(module_runtime::format_utc_micros),
|
|
updated_at: module_runtime::format_utc_micros(record.updated_at_micros),
|
|
}
|
|
}
|
|
|
|
fn payment_order_status_text(status: module_runtime::PaymentOrderStatus) -> &'static str {
|
|
match status {
|
|
module_runtime::PaymentOrderStatus::Pending => "pending",
|
|
module_runtime::PaymentOrderStatus::Paying => "paying",
|
|
module_runtime::PaymentOrderStatus::Paid => "paid",
|
|
module_runtime::PaymentOrderStatus::Expired => "expired",
|
|
module_runtime::PaymentOrderStatus::Closed => "closed",
|
|
module_runtime::PaymentOrderStatus::Refunded => "refunded",
|
|
}
|
|
}
|
|
|
|
fn require_payment_scope(principal: &ExternalApiPrincipal, scope: &str) -> Result<(), AppError> {
|
|
if principal.payment_app_id().is_some() && principal.has_scope(scope) {
|
|
return Ok(());
|
|
}
|
|
Err(AppError::from_status(StatusCode::FORBIDDEN)
|
|
.with_message("支付 API Key 没有当前操作权限")
|
|
.with_details(json!({ "scope": scope })))
|
|
}
|
|
|
|
fn resolve_payment_client_ip(headers: &HeaderMap) -> String {
|
|
headers
|
|
.get("x-forwarded-for")
|
|
.and_then(|value| value.to_str().ok())
|
|
.and_then(|value| value.split(',').next())
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty())
|
|
.unwrap_or("127.0.0.1")
|
|
.to_string()
|
|
}
|
|
|
|
fn resolve_payment_notify_url() -> Result<String, AppError> {
|
|
let value = std::env::var("WECHAT_PAYMENT_NOTIFY_URL")
|
|
.ok()
|
|
.map(|value| value.trim().to_string())
|
|
.filter(|value| !value.is_empty());
|
|
if let Some(value) = value {
|
|
return Ok(value);
|
|
}
|
|
if std::env::var("WECHAT_PAY_PROVIDER").ok().as_deref() == Some("mock") {
|
|
return Ok("http://127.0.0.1/api/payment/wechat/notify".to_string());
|
|
}
|
|
Err(AppError::from_status(StatusCode::SERVICE_UNAVAILABLE)
|
|
.with_message("缺少 WECHAT_PAYMENT_NOTIFY_URL,无法创建支付服务订单"))
|
|
}
|
|
|
|
fn map_payment_client_error(error: spacetime_client::SpacetimeClientError) -> AppError {
|
|
match error {
|
|
spacetime_client::SpacetimeClientError::Procedure(message)
|
|
if message.contains("PAYMENT_PROVIDER_PENDING") =>
|
|
{
|
|
AppError::from_status(StatusCode::CONFLICT)
|
|
.with_message("支付平台下单处理中,请稍后查单")
|
|
}
|
|
spacetime_client::SpacetimeClientError::Procedure(message)
|
|
| spacetime_client::SpacetimeClientError::Runtime(message) => {
|
|
AppError::from_status(StatusCode::BAD_REQUEST)
|
|
.with_message("支付服务请求未通过")
|
|
.with_details(json!({ "message": message }))
|
|
}
|
|
other => AppError::from_status(StatusCode::BAD_GATEWAY)
|
|
.with_message("支付服务暂时不可用")
|
|
.with_details(json!({ "message": other.to_string() })),
|
|
}
|
|
}
|