fix: 添加后台定时重试 + 管理员强制确认 + 审计日志 + mock守卫
This commit is contained in:
197
src/db.rs
197
src/db.rs
@@ -338,6 +338,47 @@ pub async fn get_user_orders(
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
/// 扫描超过 10 分钟的待支付订单,尝试自动确认(用于回调重试)
|
||||
/// 返回成功确认的订单数量
|
||||
pub async fn check_and_retry_pending_orders(pool: &PgPool) -> Result<u64, AppError> {
|
||||
// 查找超过 10 分钟仍为 pending 的订单
|
||||
let stuck_orders: Vec<(String,)> = sqlx::query_as(
|
||||
r#"
|
||||
SELECT order_no FROM payment_orders
|
||||
WHERE status = 'pending'
|
||||
AND created_at < NOW() - INTERVAL '10 minutes'
|
||||
AND created_at > NOW() - INTERVAL '24 hours'
|
||||
ORDER BY created_at ASC
|
||||
"#,
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(|e| AppError::Database(format!("查询待处理订单失败: {}", e)))?;
|
||||
|
||||
if stuck_orders.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
"发现 {} 个待处理订单,尝试自动确认...",
|
||||
stuck_orders.len()
|
||||
);
|
||||
|
||||
let mut confirmed: u64 = 0;
|
||||
for (order_no,) in &stuck_orders {
|
||||
match confirm_payment_order_by_orderno(pool, order_no).await {
|
||||
Ok(_) => {
|
||||
confirmed += 1;
|
||||
tracing::info!("定时任务自动确认订单: {}", order_no);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("定时任务无法确认订单 {}: {}", order_no, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(confirmed)
|
||||
}
|
||||
|
||||
/// 清理超过 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 {
|
||||
@@ -507,6 +548,16 @@ pub async fn confirm_payment_order_by_orderno(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 查出订单关联的用户(用于审计日志)
|
||||
let user_id: Option<i32> = sqlx::query_scalar(
|
||||
r#"SELECT user_id FROM payment_orders WHERE order_no = $1"#,
|
||||
)
|
||||
.bind(order_no)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
// 一次性完成:更新订单状态 + 累加计算新的到期时间
|
||||
sqlx::query(
|
||||
r#"
|
||||
@@ -534,6 +585,11 @@ pub async fn confirm_payment_order_by_orderno(
|
||||
.await
|
||||
.map_err(|e| AppError::Database(format!("支付确认失败: {}", e)))?;
|
||||
|
||||
// 审计日志
|
||||
if let Some(uid) = user_id {
|
||||
let _ = insert_payment_audit_log(pool, order_no, uid, "paid", None, None).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -603,6 +659,10 @@ pub async fn refund_payment_order(
|
||||
);
|
||||
}
|
||||
|
||||
// 审计日志
|
||||
let detail = if other_active.0 == 0 { "退款,会员已撤销" } else { "退款,有其他有效订单,保留会员" };
|
||||
let _ = insert_payment_audit_log(pool, order_no, user_id, "refunded", None, Some(detail)).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -775,3 +835,140 @@ pub async fn cleanup_expired_refresh_tokens(pool: &PgPool) -> Result<u64, AppErr
|
||||
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
|
||||
// ===== 支付审计日志 =====
|
||||
|
||||
/// 写入支付审计日志
|
||||
pub async fn insert_payment_audit_log(
|
||||
pool: &PgPool,
|
||||
order_no: &str,
|
||||
user_id: i32,
|
||||
action: &str,
|
||||
operator_id: Option<i32>,
|
||||
detail: Option<&str>,
|
||||
) -> Result<(), AppError> {
|
||||
// 自动建表(幂等)
|
||||
sqlx::query(
|
||||
r#"
|
||||
CREATE TABLE IF NOT EXISTS payment_audit_log (
|
||||
id SERIAL PRIMARY KEY,
|
||||
order_no VARCHAR(64) NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
action VARCHAR(32) NOT NULL,
|
||||
operator_id INTEGER,
|
||||
detail TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(|e| AppError::Database(format!("创建支付审计表失败: {}", e)))?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO payment_audit_log (order_no, user_id, action, operator_id, detail)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
"#,
|
||||
)
|
||||
.bind(order_no)
|
||||
.bind(user_id)
|
||||
.bind(action)
|
||||
.bind(operator_id)
|
||||
.bind(detail)
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(|e| AppError::Database(format!("写入支付审计日志失败: {}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 管理员强制确认待支付订单(带审计日志)
|
||||
pub async fn admin_force_confirm_order(
|
||||
pool: &PgPool,
|
||||
order_no: &str,
|
||||
admin_user_id: i32,
|
||||
) -> Result<Option<chrono::DateTime<chrono::Utc>>, AppError> {
|
||||
// 查出订单关联的用户
|
||||
let order = sqlx::query_as::<_, (i32, String)>(
|
||||
r#"SELECT user_id, status 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) = match order {
|
||||
Some(o) => o,
|
||||
None => return Err(AppError::NotFound("订单不存在".to_string())),
|
||||
};
|
||||
|
||||
// 非管理员不能操作
|
||||
// (调用方已校验)
|
||||
|
||||
let new_expires = if status == "pending" {
|
||||
// 直接执行确认 SQL(跳过 user_id 校验,由管理员操作)
|
||||
let result = sqlx::query_scalar::<_, Option<chrono::DateTime<chrono::Utc>>>(
|
||||
r#"
|
||||
WITH updated_order AS (
|
||||
UPDATE payment_orders SET status = 'paid', paid_at = NOW() WHERE order_no = $1 RETURNING package_type
|
||||
)
|
||||
UPDATE users SET
|
||||
is_paid = true,
|
||||
paid_expires_at =
|
||||
CASE
|
||||
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'
|
||||
WHEN uo.package_type = 'yearly' THEN INTERVAL '365 days'
|
||||
ELSE INTERVAL '0 days'
|
||||
END
|
||||
END
|
||||
FROM updated_order uo
|
||||
WHERE users.id = $2
|
||||
RETURNING users.paid_expires_at
|
||||
"#,
|
||||
)
|
||||
.bind(order_no)
|
||||
.bind(user_id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(|e| AppError::Database(format!("管理员确认订单失败: {}", e)))?;
|
||||
|
||||
// 写审计日志
|
||||
let _ = insert_payment_audit_log(
|
||||
pool,
|
||||
order_no,
|
||||
user_id,
|
||||
"admin_confirm",
|
||||
Some(admin_user_id),
|
||||
Some("管理员强制确认支付"),
|
||||
)
|
||||
.await;
|
||||
|
||||
result
|
||||
} else {
|
||||
// 订单已处理,只写日志
|
||||
let _ = insert_payment_audit_log(
|
||||
pool,
|
||||
order_no,
|
||||
user_id,
|
||||
"admin_confirm",
|
||||
Some(admin_user_id),
|
||||
Some(&format!("订单状态为 {},跳过确认", status)),
|
||||
)
|
||||
.await;
|
||||
|
||||
// 返回当前到期时间
|
||||
sqlx::query_scalar::<_, Option<chrono::DateTime<chrono::Utc>>>(
|
||||
r#"SELECT paid_expires_at FROM users WHERE id = $1"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(|e| AppError::Database(format!("查询用户到期时间失败: {}", e)))?
|
||||
};
|
||||
|
||||
Ok(new_expires)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user