fix: 修复付费机制失效处理的5个后端缺口
- 新增POST /payment/refund-notify支付宝退款Webhook - 新增待支付订单自动清理(超过24h→cancelled, 含启动清理) - 新增web登录码过期启动清理 - 永久会员用NULL替代硬编码2099-12-31 - 修复缺失的is_paid_active数据库函数(改用内联SQL)
This commit is contained in:
130
src/db.rs
130
src/db.rs
@@ -338,6 +338,44 @@ pub async fn get_user_orders(
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
/// 清理超过 24 小时仍未支付的待处理订单
|
||||
pub async fn cleanup_expired_pending_orders(pool: &PgPool, user_id: Option<i32>) -> Result<u64, AppError> {
|
||||
let affected = if let Some(uid) = user_id {
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE payment_orders
|
||||
SET status = 'cancelled'
|
||||
WHERE status = 'pending'
|
||||
AND created_at < NOW() - INTERVAL '24 hours'
|
||||
AND user_id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(uid)
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(|e| AppError::Database(format!("清理过期订单失败: {}", e)))?
|
||||
.rows_affected()
|
||||
} else {
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE payment_orders
|
||||
SET status = 'cancelled'
|
||||
WHERE status = 'pending'
|
||||
AND created_at < NOW() - INTERVAL '24 hours'
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(|e| AppError::Database(format!("清理过期订单失败: {}", e)))?
|
||||
.rows_affected()
|
||||
};
|
||||
|
||||
if affected > 0 {
|
||||
tracing::info!("已清理 {} 个过期待支付订单", affected);
|
||||
}
|
||||
Ok(affected)
|
||||
}
|
||||
|
||||
pub async fn create_payment_order(
|
||||
pool: &PgPool,
|
||||
user_id: i32,
|
||||
@@ -346,6 +384,9 @@ pub async fn create_payment_order(
|
||||
amount: i32,
|
||||
expires_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
) -> Result<(), AppError> {
|
||||
// 创建前先清理该用户的过期 pending 订单
|
||||
let _ = cleanup_expired_pending_orders(pool, Some(user_id)).await;
|
||||
|
||||
let query = r#"
|
||||
INSERT INTO payment_orders (user_id, order_no, package_type, amount, expires_at)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
@@ -364,12 +405,26 @@ pub async fn create_payment_order(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 清理所有已过期的 web 登录码
|
||||
pub async fn cleanup_expired_login_codes(pool: &PgPool) -> Result<u64, AppError> {
|
||||
let affected = sqlx::query("DELETE FROM web_login_codes WHERE expires_at < NOW()")
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(|e| AppError::Database(format!("清理过期登录码失败: {}", e)))?
|
||||
.rows_affected();
|
||||
|
||||
if affected > 0 {
|
||||
tracing::info!("已清理 {} 个过期的 web 登录码", affected);
|
||||
}
|
||||
Ok(affected)
|
||||
}
|
||||
|
||||
/// 确认订单支付(模拟):更新订单状态 + 激活用户付费
|
||||
///
|
||||
/// 返回该订单的 expires_at(永久会员为 None)
|
||||
/// 确认订单支付(模拟):更新订单状态 + 激活用户付费(累加有效期)
|
||||
///
|
||||
/// 返回计算后的到期时间(永久会员返回 2099-12-31)
|
||||
/// 返回计算后的到期时间(永久会员返回 None)
|
||||
pub async fn confirm_payment_order(
|
||||
pool: &PgPool,
|
||||
order_no: &str,
|
||||
@@ -407,7 +462,7 @@ pub async fn confirm_payment_order(
|
||||
is_paid = true,
|
||||
paid_expires_at =
|
||||
CASE
|
||||
WHEN uo.package_type = 'permanent' THEN '2099-12-31 23:59:59+00'::timestamptz
|
||||
WHEN uo.package_type = 'permanent' THEN NULL
|
||||
ELSE GREATEST(COALESCE(users.paid_expires_at, NOW()), NOW()) +
|
||||
CASE
|
||||
WHEN uo.package_type = 'monthly' THEN INTERVAL '30 days'
|
||||
@@ -462,7 +517,7 @@ pub async fn confirm_payment_order_by_orderno(
|
||||
is_paid = true,
|
||||
paid_expires_at =
|
||||
CASE
|
||||
WHEN uo.package_type = 'permanent' THEN '2099-12-31 23:59:59+00'::timestamptz
|
||||
WHEN uo.package_type = 'permanent' THEN NULL
|
||||
ELSE GREATEST(COALESCE(users.paid_expires_at, NOW()), NOW()) +
|
||||
CASE
|
||||
WHEN uo.package_type = 'monthly' THEN INTERVAL '30 days'
|
||||
@@ -482,6 +537,75 @@ pub async fn confirm_payment_order_by_orderno(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 退款处理:标记订单为 refunded + 撤销用户会员(如果无其他有效订单)
|
||||
pub async fn refund_payment_order(
|
||||
pool: &PgPool,
|
||||
order_no: &str,
|
||||
) -> Result<(), AppError> {
|
||||
// 先查出订单关联的用户
|
||||
let order_info: Option<(i32, String, Option<chrono::DateTime<chrono::Utc>>)> =
|
||||
sqlx::query_as(
|
||||
r#"SELECT user_id, status, paid_at FROM payment_orders WHERE order_no = $1"#,
|
||||
)
|
||||
.bind(order_no)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
.map_err(|e| AppError::Database(format!("查询订单失败: {}", e)))?;
|
||||
|
||||
let (user_id, status, _paid_at) = match order_info {
|
||||
Some(info) => info,
|
||||
None => return Err(AppError::NotFound("订单不存在".to_string())),
|
||||
};
|
||||
|
||||
if status != "paid" {
|
||||
tracing::warn!("订单 {} 状态为 {},跳过退款处理", order_no, status);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 标记订单为 refunded
|
||||
sqlx::query("UPDATE payment_orders SET status = 'refunded' WHERE order_no = $1")
|
||||
.bind(order_no)
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(|e| AppError::Database(format!("退款标记失败: {}", e)))?;
|
||||
|
||||
// 检查该用户是否有其他有效的已支付订单
|
||||
let other_active: (i64,) = sqlx::query_as(
|
||||
r#"
|
||||
SELECT COUNT(*) FROM payment_orders
|
||||
WHERE user_id = $1
|
||||
AND status = 'paid'
|
||||
AND order_no != $2
|
||||
AND (expires_at IS NULL OR expires_at > NOW())
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(order_no)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(|e| AppError::Database(format!("查询其他有效订单失败: {}", e)))?;
|
||||
|
||||
// 如果没有其他有效订单,撤销会员状态
|
||||
if other_active.0 == 0 {
|
||||
sqlx::query(
|
||||
"UPDATE users SET is_paid = false, paid_expires_at = NULL WHERE id = $1",
|
||||
)
|
||||
.bind(user_id)
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(|e| AppError::Database(format!("撤销会员失败: {}", e)))?;
|
||||
tracing::info!("用户 {} 的会员因订单 {} 退款已被撤销", user_id, order_no);
|
||||
} else {
|
||||
tracing::info!(
|
||||
"用户 {} 有其他有效订单({}笔),跳过会员撤销",
|
||||
user_id,
|
||||
other_active.0
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取用户配额信息
|
||||
///
|
||||
/// 返回 (已用条数, 是否付费活跃, 到期时间)
|
||||
|
||||
Reference in New Issue
Block a user