fix: 支付确认TOCTOU竞态(AND status=pending)+定时器补充过期订单清理
This commit is contained in:
15
src/db.rs
15
src/db.rs
@@ -585,7 +585,7 @@ pub async fn confirm_payment_order(
|
|||||||
let new_expires: Option<chrono::DateTime<chrono::Utc>> = sqlx::query_scalar(
|
let new_expires: Option<chrono::DateTime<chrono::Utc>> = sqlx::query_scalar(
|
||||||
r#"
|
r#"
|
||||||
WITH updated_order AS (
|
WITH updated_order AS (
|
||||||
UPDATE payment_orders SET status = 'paid', paid_at = NOW() WHERE order_no = $1 RETURNING package_type
|
UPDATE payment_orders SET status = 'paid', paid_at = NOW() WHERE order_no = $1 AND status = 'pending' RETURNING package_type
|
||||||
)
|
)
|
||||||
UPDATE users SET
|
UPDATE users SET
|
||||||
is_member = true,
|
is_member = true,
|
||||||
@@ -658,10 +658,11 @@ pub async fn confirm_payment_order_by_orderno(
|
|||||||
.flatten();
|
.flatten();
|
||||||
|
|
||||||
// 一次性完成:更新订单状态 + 累加计算新的到期时间
|
// 一次性完成:更新订单状态 + 累加计算新的到期时间
|
||||||
sqlx::query(
|
// 加上 AND status = 'pending' 防止竞态覆盖已取消/退款的订单
|
||||||
|
let result = sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
WITH updated_order AS (
|
WITH updated_order AS (
|
||||||
UPDATE payment_orders SET status = 'paid', paid_at = NOW() WHERE order_no = $1 RETURNING package_type, user_id
|
UPDATE payment_orders SET status = 'paid', paid_at = NOW() WHERE order_no = $1 AND status = 'pending' RETURNING package_type, user_id
|
||||||
)
|
)
|
||||||
UPDATE users SET
|
UPDATE users SET
|
||||||
is_member = true,
|
is_member = true,
|
||||||
@@ -686,6 +687,12 @@ pub async fn confirm_payment_order_by_orderno(
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::Database(format!("支付确认失败: {}", e)))?;
|
.map_err(|e| AppError::Database(format!("支付确认失败: {}", e)))?;
|
||||||
|
|
||||||
|
// 0 行 update 说明订单已被取消/退款/已确认(竞态保护生效)
|
||||||
|
if result.rows_affected() == 0 {
|
||||||
|
tracing::warn!("支付确认: 订单 {} 状态已变更,跳过处理", order_no);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
// 审计日志
|
// 审计日志
|
||||||
if let Some(uid) = user_id {
|
if let Some(uid) = user_id {
|
||||||
let _ = insert_payment_audit_log(pool, order_no, uid, "paid", None, None).await;
|
let _ = insert_payment_audit_log(pool, order_no, uid, "paid", None, None).await;
|
||||||
@@ -1019,7 +1026,7 @@ pub async fn admin_force_confirm_order(
|
|||||||
let result = sqlx::query_scalar::<_, Option<chrono::DateTime<chrono::Utc>>>(
|
let result = sqlx::query_scalar::<_, Option<chrono::DateTime<chrono::Utc>>>(
|
||||||
r#"
|
r#"
|
||||||
WITH updated_order AS (
|
WITH updated_order AS (
|
||||||
UPDATE payment_orders SET status = 'paid', paid_at = NOW() WHERE order_no = $1 RETURNING package_type
|
UPDATE payment_orders SET status = 'paid', paid_at = NOW() WHERE order_no = $1 AND status = 'pending' RETURNING package_type
|
||||||
)
|
)
|
||||||
UPDATE users SET
|
UPDATE users SET
|
||||||
is_member = true,
|
is_member = true,
|
||||||
|
|||||||
@@ -275,6 +275,9 @@ async fn main() -> std::io::Result<()> {
|
|||||||
// 会员到期前 7 天提醒
|
// 会员到期前 7 天提醒
|
||||||
let _ = db::check_member_expiry_soon(&pool_clone).await;
|
let _ = db::check_member_expiry_soon(&pool_clone).await;
|
||||||
|
|
||||||
|
// 清理超过 24 小时的过期待支付订单
|
||||||
|
let _ = db::cleanup_expired_pending_orders(&pool_clone, None).await;
|
||||||
|
|
||||||
// 清理过期的 refresh_token
|
// 清理过期的 refresh_token
|
||||||
let _ = db::cleanup_expired_refresh_tokens(&pool_clone).await;
|
let _ = db::cleanup_expired_refresh_tokens(&pool_clone).await;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user