fix: lock recharge flow until virtual payment settles

This commit is contained in:
kdletters
2026-06-02 01:47:39 +08:00
parent 1cb11bc1dd
commit 2fdeb34567
13 changed files with 1167 additions and 246 deletions

View File

@@ -2,7 +2,10 @@ use axum::{
Json,
extract::{Extension, Path, Query, State},
http::{HeaderMap, StatusCode},
response::Response,
response::{
IntoResponse, Response,
sse::{Event, Sse},
},
};
use hmac::{Hmac, Mac};
use module_runtime::{
@@ -23,7 +26,7 @@ use module_runtime::{
RuntimeProfileWalletLedgerSourceType, RuntimeReferralInviteCenterRecord,
RuntimeTrackingScopeKind,
};
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use sha2::Sha256;
use shared_contracts::runtime::{
@@ -63,10 +66,12 @@ use shared_contracts::runtime::{
RedeemProfileRewardCodeRequest, RedeemProfileRewardCodeResponse, SubmitProfileFeedbackRequest,
SubmitProfileFeedbackResponse, TRACKING_SCOPE_KIND_MODULE, TRACKING_SCOPE_KIND_SITE,
TRACKING_SCOPE_KIND_USER, TRACKING_SCOPE_KIND_WORK, WechatMiniProgramPaymentParamsResponse,
WechatMiniProgramVirtualPayParamsResponse,
WechatMiniProgramVirtualPayParamsResponse, WechatProfileRechargeOrderDoneEvent,
WechatProfileRechargeOrderErrorEvent,
};
use shared_kernel::{offset_datetime_to_unix_micros, parse_rfc3339};
use spacetime_client::SpacetimeClientError;
use std::{convert::Infallible, time::Duration};
use time::OffsetDateTime;
use crate::{
@@ -347,19 +352,19 @@ pub async fn confirm_wechat_profile_recharge_order(
if order.status == RuntimeProfileRechargeOrderStatus::Paid {
return Ok(json_success_body(
Some(&request_context),
ConfirmWechatProfileRechargeOrderResponse {
order: build_profile_recharge_order_response(order),
center: build_profile_recharge_center_response(center),
},
build_wechat_profile_recharge_order_confirmation(center, order),
));
}
if order.status != RuntimeProfileRechargeOrderStatus::Pending {
return Ok(json_success_body(
Some(&request_context),
ConfirmWechatProfileRechargeOrderResponse {
order: build_profile_recharge_order_response(order),
center: build_profile_recharge_center_response(center),
},
build_wechat_profile_recharge_order_confirmation(center, order),
));
}
if order.payment_channel == PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_MINI_PROGRAM_VIRTUAL {
return Ok(json_success_body(
Some(&request_context),
build_wechat_profile_recharge_order_confirmation(center, order),
));
}
@@ -381,10 +386,7 @@ pub async fn confirm_wechat_profile_recharge_order(
if wechat_order.trade_state != "SUCCESS" {
return Ok(json_success_body(
Some(&request_context),
ConfirmWechatProfileRechargeOrderResponse {
order: build_profile_recharge_order_response(order),
center: build_profile_recharge_center_response(center),
},
build_wechat_profile_recharge_order_confirmation(center, order),
));
}
@@ -406,13 +408,94 @@ pub async fn confirm_wechat_profile_recharge_order(
Ok(json_success_body(
Some(&request_context),
ConfirmWechatProfileRechargeOrderResponse {
order: build_profile_recharge_order_response(order),
center: build_profile_recharge_center_response(center),
},
build_wechat_profile_recharge_order_confirmation(center, order),
))
}
pub async fn stream_wechat_profile_recharge_order_events(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
Extension(authenticated): Extension<AuthenticatedAccessToken>,
Path(order_id): Path<String>,
) -> Result<Response, Response> {
let user_id = authenticated.claims().user_id().to_string();
let (center, order) = load_user_wechat_profile_recharge_order(
&state,
&request_context,
user_id.clone(),
order_id.clone(),
)
.await?;
let stream_state = state.clone();
let stream_context = request_context.clone();
let stream = async_stream::stream! {
let initial_response = build_wechat_profile_recharge_order_confirmation(center, order.clone());
yield Ok::<Event, Infallible>(wechat_profile_recharge_sse_json_event(
"order",
&initial_response,
));
if order.status != RuntimeProfileRechargeOrderStatus::Pending {
yield Ok::<Event, Infallible>(wechat_profile_recharge_sse_json_event(
"done",
&WechatProfileRechargeOrderDoneEvent {
order_id: order.order_id.clone(),
status: build_profile_recharge_order_status(order.status),
},
));
return;
}
let mut updates = stream_state.subscribe_profile_recharge_order_updates();
let mut poll_interval = tokio::time::interval(Duration::from_millis(1200));
for _ in 0..25 {
tokio::select! {
maybe_order_id = updates.recv() => {
if !matches!(maybe_order_id, Ok(ref value) if value == &order_id) {
continue;
}
}
_ = poll_interval.tick() => {}
}
match load_user_wechat_profile_recharge_order(
&stream_state,
&stream_context,
user_id.clone(),
order_id.clone(),
).await {
Ok((center, order)) => {
let response = build_wechat_profile_recharge_order_confirmation(center, order.clone());
yield Ok::<Event, Infallible>(wechat_profile_recharge_sse_json_event(
"order",
&response,
));
if order.status != RuntimeProfileRechargeOrderStatus::Pending {
yield Ok::<Event, Infallible>(wechat_profile_recharge_sse_json_event(
"done",
&WechatProfileRechargeOrderDoneEvent {
order_id: order.order_id.clone(),
status: build_profile_recharge_order_status(order.status),
},
));
return;
}
}
Err(_) => {
yield Ok::<Event, Infallible>(wechat_profile_recharge_sse_json_event(
"error",
&WechatProfileRechargeOrderErrorEvent {
message: "读取充值订单状态失败".to_string(),
},
));
return;
}
}
}
};
Ok(Sse::new(stream).into_response())
}
pub async fn submit_profile_feedback(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
@@ -1029,6 +1112,81 @@ fn runtime_profile_error_response(request_context: &RequestContext, error: AppEr
error.into_response_with_context(Some(request_context))
}
async fn load_user_wechat_profile_recharge_order(
state: &AppState,
request_context: &RequestContext,
user_id: String,
order_id: String,
) -> Result<
(
RuntimeProfileRechargeCenterRecord,
RuntimeProfileRechargeOrderRecord,
),
Response,
> {
let (center, order) = state
.spacetime_client()
.get_profile_recharge_order(order_id)
.await
.map_err(|error| {
runtime_profile_error_response(request_context, map_runtime_profile_client_error(error))
})?;
if order.user_id != user_id {
return Err(runtime_profile_error_response(
request_context,
AppError::from_status(StatusCode::NOT_FOUND).with_message("充值订单不存在"),
));
}
if !is_wechat_recharge_payment_channel(&order.payment_channel) {
return Err(runtime_profile_error_response(
request_context,
AppError::from_status(StatusCode::BAD_REQUEST)
.with_message("该充值订单不是微信支付订单"),
));
}
Ok((center, order))
}
fn build_wechat_profile_recharge_order_confirmation(
center: RuntimeProfileRechargeCenterRecord,
order: RuntimeProfileRechargeOrderRecord,
) -> ConfirmWechatProfileRechargeOrderResponse {
ConfirmWechatProfileRechargeOrderResponse {
order: build_profile_recharge_order_response(order),
center: build_profile_recharge_center_response(center),
}
}
fn build_profile_recharge_order_status(status: RuntimeProfileRechargeOrderStatus) -> String {
match status {
RuntimeProfileRechargeOrderStatus::Pending => "pending",
RuntimeProfileRechargeOrderStatus::Paid => "paid",
RuntimeProfileRechargeOrderStatus::Failed => "failed",
RuntimeProfileRechargeOrderStatus::Closed => "closed",
RuntimeProfileRechargeOrderStatus::Refunded => "refunded",
}
.to_string()
}
fn wechat_profile_recharge_sse_json_event<T>(event_name: &str, payload: &T) -> Event
where
T: Serialize,
{
Event::default()
.event(event_name)
.json_data(payload)
.unwrap_or_else(|_| {
Event::default()
.event("error")
.json_data(&WechatProfileRechargeOrderErrorEvent {
message: "充值订单状态事件序列化失败".to_string(),
})
.unwrap_or_else(|_| Event::default().event("error").data("{}"))
})
}
fn normalize_recharge_payment_channel(raw: Option<String>) -> Result<String, AppError> {
raw.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())