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 与使用说明
145 lines
4.9 KiB
Rust
145 lines
4.9 KiB
Rust
use crate::{
|
|
admin::{AuthenticatedAdmin, fetch_admin_payment_rows},
|
|
http_error::AppError,
|
|
request_context::RequestContext,
|
|
state::AppState,
|
|
};
|
|
use axum::{Extension, Json, extract::State};
|
|
use serde::Serialize;
|
|
use serde_json::Value;
|
|
|
|
#[derive(Debug, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct AdminPaymentOrderEntry {
|
|
pub order_id: String,
|
|
pub app_id: String,
|
|
pub owner_user_id: String,
|
|
pub merchant_order_id: String,
|
|
pub title: String,
|
|
pub amount_cents: u64,
|
|
pub currency: String,
|
|
pub provider: String,
|
|
pub provider_trade_no: Option<String>,
|
|
pub status: String,
|
|
pub created_at: String,
|
|
pub expires_at: String,
|
|
pub paid_at: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct AdminPaymentOrderListResponse {
|
|
pub entries: Vec<AdminPaymentOrderEntry>,
|
|
pub webhooks: Vec<AdminPaymentWebhookEntry>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct AdminPaymentWebhookEntry {
|
|
pub delivery_id: String,
|
|
pub order_id: String,
|
|
pub app_id: String,
|
|
pub callback_url: String,
|
|
pub status: String,
|
|
pub attempt_count: u32,
|
|
pub available_at: String,
|
|
pub last_error_message: Option<String>,
|
|
pub updated_at: String,
|
|
}
|
|
|
|
pub async fn admin_list_payment_orders(
|
|
State(state): State<AppState>,
|
|
Extension(_admin): Extension<AuthenticatedAdmin>,
|
|
Extension(_request_context): Extension<RequestContext>,
|
|
) -> Result<Json<AdminPaymentOrderListResponse>, AppError> {
|
|
let rows = fetch_admin_payment_rows(
|
|
&state,
|
|
"SELECT order_id, app_id, owner_user_id, merchant_order_id, title, amount_cents, currency, provider, provider_trade_no, status, created_at, expires_at, paid_at FROM payment_order ORDER BY created_at DESC LIMIT 200",
|
|
)
|
|
.await?;
|
|
let entries = rows
|
|
.into_iter()
|
|
.filter_map(parse_payment_order_row)
|
|
.collect();
|
|
let webhook_rows = fetch_admin_payment_rows(
|
|
&state,
|
|
"SELECT delivery_id, order_id, app_id, callback_url, status, attempt_count, available_at, last_error_message, updated_at FROM payment_webhook_delivery ORDER BY updated_at DESC LIMIT 200",
|
|
)
|
|
.await?;
|
|
let webhooks = webhook_rows
|
|
.into_iter()
|
|
.filter_map(parse_payment_webhook_row)
|
|
.collect();
|
|
Ok(Json(AdminPaymentOrderListResponse { entries, webhooks }))
|
|
}
|
|
|
|
fn parse_payment_webhook_row(row: Value) -> Option<AdminPaymentWebhookEntry> {
|
|
let cells = row.as_array()?;
|
|
Some(AdminPaymentWebhookEntry {
|
|
delivery_id: cell_text(cells.get(0)?)?,
|
|
order_id: cell_text(cells.get(1)?)?,
|
|
app_id: cell_text(cells.get(2)?)?,
|
|
callback_url: cell_text(cells.get(3)?)?,
|
|
status: cell_text(cells.get(4)?)?,
|
|
attempt_count: cell_u64(cells.get(5)?)? as u32,
|
|
available_at: cell_timestamp(cells.get(6)?),
|
|
last_error_message: cell_option_text(cells.get(7)?),
|
|
updated_at: cell_timestamp(cells.get(8)?),
|
|
})
|
|
}
|
|
|
|
fn parse_payment_order_row(row: Value) -> Option<AdminPaymentOrderEntry> {
|
|
let cells = row.as_array()?;
|
|
Some(AdminPaymentOrderEntry {
|
|
order_id: cell_text(cells.get(0)?)?,
|
|
app_id: cell_text(cells.get(1)?)?,
|
|
owner_user_id: cell_text(cells.get(2)?)?,
|
|
merchant_order_id: cell_text(cells.get(3)?)?,
|
|
title: cell_text(cells.get(4)?)?,
|
|
amount_cents: cell_u64(cells.get(5)?)?,
|
|
currency: cell_text(cells.get(6)?)?,
|
|
provider: cell_text(cells.get(7)?)?,
|
|
provider_trade_no: cell_option_text(cells.get(8)?),
|
|
status: cell_text(cells.get(9)?)?,
|
|
created_at: cell_timestamp(cells.get(10)?),
|
|
expires_at: cell_timestamp(cells.get(11)?),
|
|
paid_at: cell_option_timestamp(cells.get(12)?),
|
|
})
|
|
}
|
|
|
|
fn cell_text(value: &Value) -> Option<String> {
|
|
match value {
|
|
Value::String(text) => Some(text.clone()),
|
|
Value::Number(number) => Some(number.to_string()),
|
|
Value::Array(values) if values.len() == 2 => cell_text(values.get(1)?),
|
|
Value::Array(values) if values.len() == 1 => cell_text(values.first()?),
|
|
Value::Object(object) if object.len() == 1 => cell_text(object.values().next()?),
|
|
_ => Some(value.to_string()),
|
|
}
|
|
}
|
|
|
|
fn cell_option_text(value: &Value) -> Option<String> {
|
|
if matches!(value, Value::Array(values) if values.first().and_then(Value::as_u64) == Some(1)) {
|
|
return None;
|
|
}
|
|
cell_text(value).filter(|text| !text.is_empty() && text != "null")
|
|
}
|
|
|
|
fn cell_u64(value: &Value) -> Option<u64> {
|
|
cell_text(value)?.parse().ok()
|
|
}
|
|
|
|
fn cell_timestamp(value: &Value) -> String {
|
|
cell_text(value)
|
|
.and_then(|text| text.parse::<i64>().ok())
|
|
.map(module_runtime::format_utc_micros)
|
|
.unwrap_or_else(|| "-".to_string())
|
|
}
|
|
|
|
fn cell_option_timestamp(value: &Value) -> Option<String> {
|
|
if matches!(value, Value::Array(values) if values.first().and_then(Value::as_u64) == Some(1)) {
|
|
return None;
|
|
}
|
|
Some(cell_timestamp(value))
|
|
}
|