perf: improve compression reliability and deployment safety
This commit is contained in:
@@ -48,8 +48,9 @@ async fn stripe_webhook(
|
||||
|
||||
let payload_str = std::str::from_utf8(&body)
|
||||
.map_err(|_| AppError::new(ErrorCode::InvalidRequest, "Webhook payload 非 UTF-8"))?;
|
||||
let event: StripeEvent = serde_json::from_str(payload_str)
|
||||
.map_err(|err| AppError::new(ErrorCode::InvalidRequest, "Webhook JSON 解析失败").with_source(err))?;
|
||||
let event: StripeEvent = serde_json::from_str(payload_str).map_err(|err| {
|
||||
AppError::new(ErrorCode::InvalidRequest, "Webhook JSON 解析失败").with_source(err)
|
||||
})?;
|
||||
|
||||
let inserted: Option<String> = sqlx::query_scalar(
|
||||
r#"
|
||||
@@ -112,21 +113,31 @@ fn verify_stripe_signature(payload: &[u8], sig_header: &str, secret: &str) -> Re
|
||||
}
|
||||
|
||||
let Some(ts) = timestamp else {
|
||||
return Err(AppError::new(ErrorCode::InvalidRequest, "Stripe-Signature 缺少 t"));
|
||||
return Err(AppError::new(
|
||||
ErrorCode::InvalidRequest,
|
||||
"Stripe-Signature 缺少 t",
|
||||
));
|
||||
};
|
||||
if signatures.is_empty() {
|
||||
return Err(AppError::new(ErrorCode::InvalidRequest, "Stripe-Signature 缺少 v1"));
|
||||
return Err(AppError::new(
|
||||
ErrorCode::InvalidRequest,
|
||||
"Stripe-Signature 缺少 v1",
|
||||
));
|
||||
}
|
||||
|
||||
// 5 minutes tolerance
|
||||
let now = Utc::now().timestamp();
|
||||
if (now - ts).abs() > 300 {
|
||||
return Err(AppError::new(ErrorCode::InvalidRequest, "Webhook 时间戳过期"));
|
||||
return Err(AppError::new(
|
||||
ErrorCode::InvalidRequest,
|
||||
"Webhook 时间戳过期",
|
||||
));
|
||||
}
|
||||
|
||||
type HmacSha256 = Hmac<Sha256>;
|
||||
let mut mac = HmacSha256::new_from_slice(secret.as_bytes())
|
||||
.map_err(|err| AppError::new(ErrorCode::Internal, "Webhook secret 错误").with_source(err))?;
|
||||
let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).map_err(|err| {
|
||||
AppError::new(ErrorCode::Internal, "Webhook secret 错误").with_source(err)
|
||||
})?;
|
||||
mac.update(ts.to_string().as_bytes());
|
||||
mac.update(b".");
|
||||
mac.update(payload);
|
||||
@@ -159,7 +170,9 @@ async fn process_stripe_event(state: &AppState, event: &StripeEvent) -> Result<(
|
||||
upsert_subscription(state, &event.data.object).await
|
||||
}
|
||||
"customer.subscription.deleted" => cancel_subscription(state, &event.data.object).await,
|
||||
"invoice.paid" | "invoice.payment_failed" => upsert_invoice(state, &event.data.object).await,
|
||||
"invoice.paid" | "invoice.payment_failed" => {
|
||||
upsert_invoice(state, &event.data.object).await
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
@@ -206,7 +219,9 @@ async fn map_checkout_session_completed(
|
||||
.bind(customer_id)
|
||||
.execute(&state.db)
|
||||
.await
|
||||
.map_err(|err| AppError::new(ErrorCode::Internal, "更新 Stripe Customer 映射失败").with_source(err))?;
|
||||
.map_err(|err| {
|
||||
AppError::new(ErrorCode::Internal, "更新 Stripe Customer 映射失败").with_source(err)
|
||||
})?;
|
||||
|
||||
if updated.rows_affected() == 0 {
|
||||
let existing: Option<String> = sqlx::query_scalar::<_, Option<String>>(
|
||||
@@ -270,7 +285,11 @@ async fn upsert_subscription(state: &AppState, object: &serde_json::Value) -> Re
|
||||
let price_id = object
|
||||
.pointer("/items/data/0/price/id")
|
||||
.and_then(|v| v.as_str())
|
||||
.or_else(|| object.pointer("/items/data/0/plan/id").and_then(|v| v.as_str()))
|
||||
.or_else(|| {
|
||||
object
|
||||
.pointer("/items/data/0/plan/id")
|
||||
.and_then(|v| v.as_str())
|
||||
})
|
||||
.ok_or_else(|| AppError::new(ErrorCode::InvalidRequest, "subscription.price 缺失"))?;
|
||||
|
||||
let user_id: Option<uuid::Uuid> =
|
||||
@@ -401,7 +420,10 @@ async fn upsert_invoice(state: &AppState, object: &serde_json::Value) -> Result<
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let stripe_status = object.get("status").and_then(|v| v.as_str()).unwrap_or("open");
|
||||
let stripe_status = object
|
||||
.get("status")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("open");
|
||||
let status = map_invoice_status(stripe_status);
|
||||
|
||||
let invoice_number = object
|
||||
@@ -418,11 +440,23 @@ async fn upsert_invoice(state: &AppState, object: &serde_json::Value) -> Result<
|
||||
.to_uppercase();
|
||||
let total_amount_cents = object.get("total").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
|
||||
|
||||
let hosted_invoice_url = object.get("hosted_invoice_url").and_then(|v| v.as_str()).map(|v| v.to_string());
|
||||
let pdf_url = object.get("invoice_pdf").and_then(|v| v.as_str()).map(|v| v.to_string());
|
||||
let hosted_invoice_url = object
|
||||
.get("hosted_invoice_url")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|v| v.to_string());
|
||||
let pdf_url = object
|
||||
.get("invoice_pdf")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|v| v.to_string());
|
||||
|
||||
let period_start = object.get("period_start").and_then(|v| v.as_i64()).and_then(|ts| Utc.timestamp_opt(ts, 0).single());
|
||||
let period_end = object.get("period_end").and_then(|v| v.as_i64()).and_then(|ts| Utc.timestamp_opt(ts, 0).single());
|
||||
let period_start = object
|
||||
.get("period_start")
|
||||
.and_then(|v| v.as_i64())
|
||||
.and_then(|ts| Utc.timestamp_opt(ts, 0).single());
|
||||
let period_end = object
|
||||
.get("period_end")
|
||||
.and_then(|v| v.as_i64())
|
||||
.and_then(|ts| Utc.timestamp_opt(ts, 0).single());
|
||||
|
||||
let paid_at = object
|
||||
.pointer("/status_transitions/paid_at")
|
||||
|
||||
Reference in New Issue
Block a user