fix: lock recharge flow until virtual payment settles
This commit is contained in:
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user