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

View File

@@ -713,17 +713,22 @@ pub async fn web_login_auto_confirm(
}
};
let (is_paid_active, paid_expires_at): (bool, Option<String>) =
match sqlx::query_as::<_, (bool, Option<chrono::DateTime<chrono::Utc>>)>(
"SELECT is_paid_active($1)",
)
.bind(user_id)
.fetch_optional(pool.get_ref())
.await
{
Ok(Some((active, expires))) => (active, expires.map(|e| e.to_rfc3339())),
_ => (false, None),
};
let paid_info = sqlx::query_as::<_, (bool, Option<chrono::DateTime<chrono::Utc>>)>(
"SELECT is_paid, paid_expires_at FROM users WHERE id = $1",
)
.bind(user_id)
.fetch_optional(pool.get_ref())
.await
.ok()
.flatten();
let (is_paid_active, paid_expires_at): (bool, Option<String>) = match paid_info {
Some((is_paid, expires)) => {
let active = is_paid && expires.map_or(true, |e| e > Utc::now());
(active, expires.map(|e| e.to_rfc3339()))
}
None => (false, None),
};
let base_url = std::env::var("APP_BASE_URL")
.unwrap_or_else(|_| "https://dev.xmclassmate.top".to_string());

View File

@@ -38,6 +38,7 @@ pub use weather::post_weather_data;
pub use payment::alipay_notify;
pub use payment::alipay_pay_page;
pub use payment::alipay_refund_notify;
pub use payment::create_order;
pub use payment::generate_code;
pub use payment::get_user_quota;

View File

@@ -844,6 +844,61 @@ pub async fn payment_success(
.body(html)
}
// ===== Handler: POST /payment/refund-notify — 支付宝退款异步回调 =====
#[post("/payment/refund-notify")]
pub async fn alipay_refund_notify(
pool: web::Data<PgPool>,
body: web::Form<BTreeMap<String, String>>,
) -> HttpResponse {
let body = body.into_inner();
let out_trade_no = body.get("out_trade_no").cloned().unwrap_or_default();
let refund_status = body.get("refund_status").cloned().unwrap_or_default();
tracing::info!(
"收到支付宝退款回调: out_trade_no={}, refund_status={}",
out_trade_no,
refund_status
);
// 只处理成功的退款
if refund_status != "REFUND_SUCCESS" {
return HttpResponse::Ok().body("success");
}
// 验证 RSA2 签名
let Some(config) = AlipayConfig::from_env() else {
tracing::warn!("支付宝配置不存在,无法验证退款签名");
return HttpResponse::Ok().body("fail");
};
let sign = body.get("sign").cloned().unwrap_or_default();
let sign_source: String = body
.iter()
.filter(|(k, _)| *k != "sign" && *k != "sign_type")
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<_>>()
.join("&");
if let Err(e) = rsa2_verify(&sign_source, &sign, &config.alipay_public_key) {
tracing::warn!("支付宝退款签名验证失败: {}", e);
return HttpResponse::Ok().body("fail");
}
// 处理退款:标记订单 + 撤销会员
match db::refund_payment_order(pool.get_ref(), &out_trade_no).await {
Ok(_) => {
tracing::info!("订单 {} 退款处理成功", out_trade_no);
HttpResponse::Ok().body("success")
}
Err(e) => {
tracing::error!("订单 {} 退款处理失败: {}", out_trade_no, e);
HttpResponse::Ok().body("fail")
}
}
}
// ===== 旧的 API Handler保持兼容=====
/// POST /api/payment/create-order