feat: 自动通知-支付成功+到期前7天提醒+已过期通知

This commit is contained in:
2026-05-26 15:28:11 +08:00
parent e89307c7a2
commit 667231520d
2 changed files with 116 additions and 0 deletions

112
src/db.rs
View File

@@ -389,6 +389,69 @@ pub async fn check_and_retry_pending_orders(pool: &PgPool) -> Result<u64, AppErr
Ok(confirmed)
}
/// 检查会员到期前 7 天的用户,发送即将到期通知
pub async fn check_member_expiry_soon(pool: &PgPool) -> Result<u64, AppError> {
let affected = sqlx::query(
r#"
INSERT INTO notifications (scope, user_id, type, title, content)
SELECT 'user', u.id, 'member_expiry_soon',
'会员即将到期',
CONCAT('您的会员将于 ', TO_CHAR(u.membership_expires_at, 'YYYY-MM-DD'), ' 到期,请及时续费')
FROM users u
WHERE u.is_member = true
AND u.membership_expires_at IS NOT NULL
AND u.membership_expires_at > NOW()
AND u.membership_expires_at <= NOW() + INTERVAL '7 days'
AND NOT EXISTS (
SELECT 1 FROM notifications n
WHERE n.user_id = u.id
AND n.type = 'member_expiry_soon'
AND n.created_at > NOW() - INTERVAL '1 day'
)
"#,
)
.execute(pool)
.await
.map_err(|e| AppError::Database(format!("检查会员到期失败: {}", e)))?
.rows_affected();
if affected > 0 {
tracing::info!("已发送 {} 条会员到期提醒", affected);
}
Ok(affected)
}
/// 检查已过期的会员,发送过期通知
pub async fn check_member_expired(pool: &PgPool) -> Result<u64, AppError> {
let affected = sqlx::query(
r#"
INSERT INTO notifications (scope, user_id, type, title, content)
SELECT 'user', u.id, 'member_expired',
'会员已过期',
'您的会员已过期,续费后可恢复无限存储额度'
FROM users u
WHERE u.is_member = true
AND u.membership_expires_at IS NOT NULL
AND u.membership_expires_at < NOW()
AND NOT EXISTS (
SELECT 1 FROM notifications n
WHERE n.user_id = u.id
AND n.type = 'member_expired'
AND n.created_at > NOW() - INTERVAL '1 day'
)
"#,
)
.execute(pool)
.await
.map_err(|e| AppError::Database(format!("检查会员过期失败: {}", e)))?
.rows_affected();
if affected > 0 {
tracing::info!("已发送 {} 条会员过期通知", affected);
}
Ok(affected)
}
/// 清理超过 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 {
@@ -476,6 +539,50 @@ pub async fn cleanup_expired_login_codes(pool: &PgPool) -> Result<u64, AppError>
/// 确认订单支付(模拟):更新订单状态 + 激活用户付费(累加有效期)
///
/// 返回计算后的到期时间(永久会员返回 None
/// 插入支付成功通知
async fn insert_payment_notification(pool: &PgPool, user_id: i32, order_no: &str) -> Result<(), AppError> {
// 查出套餐信息
let pkg_info: Option<(String, String)> = sqlx::query_as(
r#"SELECT package_type, status FROM payment_orders WHERE order_no = $1"#,
)
.bind(order_no)
.fetch_optional(pool)
.await
.map_err(|e| AppError::Database(format!("查询订单失败: {}", e)))?;
let (pkg_type, status) = match pkg_info {
Some(info) => info,
None => return Ok(()),
};
if status != "paid" {
return Ok(());
}
let pkg_label = match pkg_type.as_str() {
"monthly" => "包月会员",
"yearly" => "包年会员",
"permanent" => "永久会员",
_ => "会员",
};
let title = format!("{}开通成功", pkg_label);
let content = format!("恭喜!您已成功开通{}", pkg_label);
sqlx::query(
r#"INSERT INTO notifications (scope, user_id, type, title, content)
VALUES ('user', $1, 'payment_success', $2, $3)"#,
)
.bind(user_id)
.bind(&title)
.bind(&content)
.execute(pool)
.await
.ok();
Ok(())
}
pub async fn confirm_payment_order(
pool: &PgPool,
order_no: &str,
@@ -532,6 +639,9 @@ pub async fn confirm_payment_order(
.await
.map_err(|e| AppError::Database(format!("支付确认失败: {}", e)))?;
// 发送支付成功通知
let _ = insert_payment_notification(pool, user_id, order_no).await;
Ok(new_expires)
}
@@ -598,6 +708,8 @@ pub async fn confirm_payment_order_by_orderno(
// 审计日志
if let Some(uid) = user_id {
let _ = insert_payment_audit_log(pool, order_no, uid, "paid", None, None).await;
// 发送支付成功通知
let _ = insert_payment_notification(pool, uid, order_no).await;
}
Ok(())

View File

@@ -267,6 +267,10 @@ async fn main() -> std::io::Result<()> {
}
Err(e) => tracing::warn!("[定时任务] 扫描待支付订单失败: {}", e),
}
// 会员到期提醒
let _ = db::check_member_expiry_soon(&pool_clone).await;
let _ = db::check_member_expired(&pool_clone).await;
}
});