diff --git a/src/db.rs b/src/db.rs index ee51a3e..0f23366 100644 --- a/src/db.rs +++ b/src/db.rs @@ -389,6 +389,69 @@ pub async fn check_and_retry_pending_orders(pool: &PgPool) -> Result Result { + 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 { + 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) -> Result { let affected = if let Some(uid) = user_id { @@ -476,6 +539,50 @@ pub async fn cleanup_expired_login_codes(pool: &PgPool) -> Result /// 确认订单支付(模拟):更新订单状态 + 激活用户付费(累加有效期) /// /// 返回计算后的到期时间(永久会员返回 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(()) diff --git a/src/main.rs b/src/main.rs index 1a3043e..6a048f1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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; } });