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)
|
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 小时仍未支付的待处理订单
|
/// 清理超过 24 小时仍未支付的待处理订单
|
||||||
pub async fn cleanup_expired_pending_orders(pool: &PgPool, user_id: Option<i32>) -> Result<u64, AppError> {
|
pub async fn cleanup_expired_pending_orders(pool: &PgPool, user_id: Option<i32>) -> Result<u64, AppError> {
|
||||||
let affected = if let Some(uid) = user_id {
|
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)
|
/// 返回计算后的到期时间(永久会员返回 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(
|
pub async fn confirm_payment_order(
|
||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
order_no: &str,
|
order_no: &str,
|
||||||
@@ -532,6 +639,9 @@ pub async fn confirm_payment_order(
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::Database(format!("支付确认失败: {}", e)))?;
|
.map_err(|e| AppError::Database(format!("支付确认失败: {}", e)))?;
|
||||||
|
|
||||||
|
// 发送支付成功通知
|
||||||
|
let _ = insert_payment_notification(pool, user_id, order_no).await;
|
||||||
|
|
||||||
Ok(new_expires)
|
Ok(new_expires)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -598,6 +708,8 @@ pub async fn confirm_payment_order_by_orderno(
|
|||||||
// 审计日志
|
// 审计日志
|
||||||
if let Some(uid) = user_id {
|
if let Some(uid) = user_id {
|
||||||
let _ = insert_payment_audit_log(pool, order_no, uid, "paid", None, None).await;
|
let _ = insert_payment_audit_log(pool, order_no, uid, "paid", None, None).await;
|
||||||
|
// 发送支付成功通知
|
||||||
|
let _ = insert_payment_notification(pool, uid, order_no).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -267,6 +267,10 @@ async fn main() -> std::io::Result<()> {
|
|||||||
}
|
}
|
||||||
Err(e) => tracing::warn!("[定时任务] 扫描待支付订单失败: {}", e),
|
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