feat: 自动通知-支付成功+到期前7天提醒+已过期通知
This commit is contained in:
112
src/db.rs
112
src/db.rs
@@ -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(())
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user