fix: 修复付费机制失效处理的5个后端缺口

- 新增POST /payment/refund-notify支付宝退款Webhook
- 新增待支付订单自动清理(超过24h→cancelled, 含启动清理)
- 新增web登录码过期启动清理
- 永久会员用NULL替代硬编码2099-12-31
- 修复缺失的is_paid_active数据库函数(改用内联SQL)
This commit is contained in:
2026-05-25 12:07:01 +08:00
parent f9f1ffeff6
commit af2837e15e
5 changed files with 213 additions and 16 deletions

130
src/db.rs
View File

@@ -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(())
}
/// 获取用户配额信息
///
/// 返回 (已用条数, 是否付费活跃, 到期时间)