From 722ab837d066be307f5a142dfd8924f7f1016563 Mon Sep 17 00:00:00 2001 From: milky0217 Date: Mon, 25 May 2026 15:33:31 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20is=5Fpaid=E2=86=92is=5Fmember,=20pa?= =?UTF-8?q?id=5Fexpires=5Fat=E2=86=92membership=5Fexpires=5Fat=20=E5=85=A8?= =?UTF-8?q?=E5=B1=80=E9=87=8D=E5=91=BD=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- migrations/010_rename_paid_fields.sql | 18 ++++++++ src/db.rs | 50 ++++++++++---------- src/handlers/admin.rs | 8 ++-- src/handlers/auth.rs | 48 +++++++++---------- src/handlers/payment.rs | 66 +++++++++++++-------------- src/handlers/user.rs | 12 ++--- src/models.rs | 12 ++--- 7 files changed, 116 insertions(+), 98 deletions(-) create mode 100644 migrations/010_rename_paid_fields.sql diff --git a/migrations/010_rename_paid_fields.sql b/migrations/010_rename_paid_fields.sql new file mode 100644 index 0000000..1153eb6 --- /dev/null +++ b/migrations/010_rename_paid_fields.sql @@ -0,0 +1,18 @@ +-- ============================================ +-- 迁移: 010_rename_paid_fields.sql +-- 目的: 将 users 表中的 is_paid/paid_expires_at 重命名为 +-- is_member/membership_expires_at,语义更准确 +-- 日期: 2026-05-25 +-- 依赖: 001_add_payment_fields.sql +-- ============================================ + +BEGIN; + +ALTER TABLE users RENAME COLUMN is_paid TO is_member; +ALTER TABLE users RENAME COLUMN paid_expires_at TO membership_expires_at; + +-- 更新 audit_log 中的备注(不阻塞) +COMMENT ON COLUMN users.is_member IS '会员身份凭证:曾经拥有过会员资格(即使已过期)'; +COMMENT ON COLUMN users.membership_expires_at IS '会员到期时间:NULL 表示永久会员'; + +COMMIT; diff --git a/src/db.rs b/src/db.rs index f73b851..ee51a3e 100644 --- a/src/db.rs +++ b/src/db.rs @@ -11,12 +11,12 @@ use crate::error::AppError; pub async fn insert_weather_data(pool: &PgPool, weather_data: &WeatherData, user_id: i32) -> Result { // 配额检查:非付费用户数据条数限制 let user = get_user_by_id(pool, user_id).await?; - let is_paid_active = user.is_paid && user.paid_expires_at.map_or(true, |expires| expires > Utc::now()); + let is_active_member = user.is_member && user.membership_expires_at.map_or(true, |expires| expires > Utc::now()); // 维护模式:非会员使用更高的临时限额,防止资源滥用 let is_maintenance = std::env::var("PAYMENT_MAINTENANCE_MODE").ok() == Some("true".to_string()); - if !is_paid_active { + if !is_active_member { let current_count = count_user_weather_data(pool, user_id).await?; let limit: i64 = if is_maintenance { env::var("MAINTENANCE_MODE_DATA_LIMIT") @@ -237,7 +237,7 @@ pub async fn delete_weather_data( pub async fn get_user_by_id(pool: &PgPool, user_id: i32) -> Result { let query = r#" SELECT - id, name, openid, phone, type, "desc", is_paid, is_admin, paid_expires_at, + id, name, openid, phone, type, "desc", is_member, is_admin, membership_expires_at, avatar_url, nickname FROM users WHERE id = $1 @@ -278,18 +278,18 @@ pub async fn count_user_weather_data(pool: &PgPool, user_id: i32) -> Result>, + is_member: bool, + membership_expires_at: Option>, ) -> Result<(), AppError> { let query = r#" UPDATE users - SET is_paid = $1, paid_expires_at = $2 + SET is_member = $1, membership_expires_at = $2 WHERE id = $3 "#; match sqlx::query(query) - .bind(is_paid) - .bind(paid_expires_at) + .bind(is_member) + .bind(membership_expires_at) .bind(user_id) .execute(pool) .await @@ -510,11 +510,11 @@ pub async fn confirm_payment_order( UPDATE payment_orders SET status = 'paid', paid_at = NOW() WHERE order_no = $1 RETURNING package_type ) UPDATE users SET - is_paid = true, - paid_expires_at = + is_member = true, + membership_expires_at = CASE WHEN uo.package_type = 'permanent' THEN NULL - ELSE GREATEST(COALESCE(users.paid_expires_at, NOW()), NOW()) + + ELSE GREATEST(COALESCE(users.membership_expires_at, NOW()), NOW()) + CASE WHEN uo.package_type = 'monthly' THEN INTERVAL '30 days' WHEN uo.package_type = 'yearly' THEN INTERVAL '365 days' @@ -523,7 +523,7 @@ pub async fn confirm_payment_order( END FROM updated_order uo WHERE users.id = $2 - RETURNING users.paid_expires_at + RETURNING users.membership_expires_at "#, ) .bind(order_no) @@ -575,11 +575,11 @@ pub async fn confirm_payment_order_by_orderno( UPDATE payment_orders SET status = 'paid', paid_at = NOW() WHERE order_no = $1 RETURNING package_type, user_id ) UPDATE users SET - is_paid = true, - paid_expires_at = + is_member = true, + membership_expires_at = CASE WHEN uo.package_type = 'permanent' THEN NULL - ELSE GREATEST(COALESCE(users.paid_expires_at, NOW()), NOW()) + + ELSE GREATEST(COALESCE(users.membership_expires_at, NOW()), NOW()) + CASE WHEN uo.package_type = 'monthly' THEN INTERVAL '30 days' WHEN uo.package_type = 'yearly' THEN INTERVAL '365 days' @@ -654,7 +654,7 @@ pub async fn refund_payment_order( // 如果没有其他有效订单,撤销会员状态 if other_active.0 == 0 { sqlx::query( - "UPDATE users SET is_paid = false, paid_expires_at = NULL WHERE id = $1", + "UPDATE users SET is_member = false, membership_expires_at = NULL WHERE id = $1", ) .bind(user_id) .execute(pool) @@ -684,10 +684,10 @@ pub async fn get_user_quota( user_id: i32, ) -> Result<(i64, bool, Option>, bool), AppError> { let user = get_user_by_id(pool, user_id).await?; - let is_paid_active = user.is_paid - && user.paid_expires_at.map_or(true, |expires| expires > chrono::Utc::now()); + let is_active_member = user.is_member + && user.membership_expires_at.map_or(true, |expires| expires > chrono::Utc::now()); let used = count_user_weather_data(pool, user_id).await?; - Ok((used, is_paid_active, user.paid_expires_at, user.is_paid)) + Ok((used, is_active_member, user.membership_expires_at, user.is_member)) } // ===== 收藏功能 DB 函数 ===== @@ -924,11 +924,11 @@ pub async fn admin_force_confirm_order( UPDATE payment_orders SET status = 'paid', paid_at = NOW() WHERE order_no = $1 RETURNING package_type ) UPDATE users SET - is_paid = true, - paid_expires_at = + is_member = true, + membership_expires_at = CASE WHEN uo.package_type = 'permanent' THEN NULL - ELSE GREATEST(COALESCE(users.paid_expires_at, NOW()), NOW()) + + ELSE GREATEST(COALESCE(users.membership_expires_at, NOW()), NOW()) + CASE WHEN uo.package_type = 'monthly' THEN INTERVAL '30 days' WHEN uo.package_type = 'yearly' THEN INTERVAL '365 days' @@ -937,7 +937,7 @@ pub async fn admin_force_confirm_order( END FROM updated_order uo WHERE users.id = $2 - RETURNING users.paid_expires_at + RETURNING users.membership_expires_at "#, ) .bind(order_no) @@ -972,7 +972,7 @@ pub async fn admin_force_confirm_order( // 返回当前到期时间 sqlx::query_scalar::<_, Option>>( - r#"SELECT paid_expires_at FROM users WHERE id = $1"#, + r#"SELECT membership_expires_at FROM users WHERE id = $1"#, ) .bind(user_id) .fetch_one(pool) @@ -1074,7 +1074,7 @@ pub async fn admin_refund_order( if other_active.0 == 0 { sqlx::query( - "UPDATE users SET is_paid = false, paid_expires_at = NULL WHERE id = $1", + "UPDATE users SET is_member = false, membership_expires_at = NULL WHERE id = $1", ) .bind(user_id) .execute(pool) diff --git a/src/handlers/admin.rs b/src/handlers/admin.rs index 42728a3..672b271 100644 --- a/src/handlers/admin.rs +++ b/src/handlers/admin.rs @@ -45,8 +45,8 @@ pub async fn admin_update_user_payment( return Err(AppError::Forbidden("无权限执行此操作".to_string())); } - // 解析 paid_expires_at - let paid_expires_at = match &body.paid_expires_at { + // 解析 membership_expires_at + let membership_expires_at = match &body.membership_expires_at { Some(date_str) => match DateTime::parse_from_rfc3339(date_str) { Ok(dt) => Some(dt.with_timezone(&Utc)), Err(e) => { @@ -57,12 +57,12 @@ pub async fn admin_update_user_payment( }; // 更新用户付费状态 - db::update_user_payment_status(pool.get_ref(), target_user_id, body.is_paid, paid_expires_at).await?; + db::update_user_payment_status(pool.get_ref(), target_user_id, body.is_member, membership_expires_at).await?; // 审计日志 let _ = db::insert_payment_audit_log( pool.get_ref(), "ADMIN_MANUAL", target_user_id, "admin_revoke", Some(claims.user_id), - Some(&format!("管理员手动更新付费状态: is_paid={}, expires_at={:?}", body.is_paid, body.paid_expires_at)), + Some(&format!("管理员手动更新付费状态: is_member={}, expires_at={:?}", body.is_member, body.membership_expires_at)), ).await; Ok(HttpResponse::Ok().json(serde_json::json!({ "success": true, diff --git a/src/handlers/auth.rs b/src/handlers/auth.rs index 07e5888..5e15ecd 100644 --- a/src/handlers/auth.rs +++ b/src/handlers/auth.rs @@ -498,8 +498,8 @@ pub struct WebLoginConfirmRequest { pub struct WebLoginConfirmResponse { pub success: bool, pub token: Option, - pub is_paid_active: bool, - pub paid_expires_at: Option, + pub is_active_member: bool, + pub membership_expires_at: Option, } #[post("/api/web-login/confirm")] @@ -529,8 +529,8 @@ pub async fn web_login_confirm( return HttpResponse::Ok().json(WebLoginConfirmResponse { success: false, token: None, - is_paid_active: false, - paid_expires_at: None, + is_active_member: false, + membership_expires_at: None, }); } }; @@ -548,12 +548,12 @@ pub async fn web_login_confirm( return HttpResponse::Ok().json(WebLoginConfirmResponse { success: false, token: None, - is_paid_active: false, - paid_expires_at: None, + is_active_member: false, + membership_expires_at: None, }); } - // 如果已有用户,验证并获取最新 is_paid 信息 + // 如果已有用户,验证并获取最新 is_member 信息 let user_id = if let Some(uid) = existing_user_id { uid } else { @@ -588,16 +588,16 @@ pub async fn web_login_confirm( }; // 查询付费状态 - let (is_paid_active, paid_expires_at): (bool, Option) = + let (is_active_member, membership_expires_at): (bool, Option) = match sqlx::query_as::<_, (bool, Option>)>( - "SELECT is_paid, paid_expires_at FROM users WHERE id = $1", + "SELECT is_member, membership_expires_at FROM users WHERE id = $1", ) .bind(user_id) .fetch_optional(pool.get_ref()) .await { - Ok(Some((is_paid, expires))) => { - let active = is_paid && expires.map_or(true, |e| e > Utc::now()); + Ok(Some((is_member, expires))) => { + let active = is_member && expires.map_or(true, |e| e > Utc::now()); (active, expires.map(|e| e.to_rfc3339())) } _ => (false, None), @@ -613,12 +613,12 @@ pub async fn web_login_confirm( tracing::warn!("更新登录码 token 失败: {}", e); } - info!("[WEB LOGIN CONFIRM] user_id={} is_paid={}", user_id, is_paid_active); + info!("[WEB LOGIN CONFIRM] user_id={} is_member={}", user_id, is_active_member); HttpResponse::Ok().json(WebLoginConfirmResponse { success: true, token: Some(token), - is_paid_active, - paid_expires_at, + is_active_member, + membership_expires_at, }) } @@ -631,8 +631,8 @@ pub struct AutoConfirmRequest { pub struct AutoConfirmResponse { pub success: bool, pub token: Option, - pub is_paid_active: bool, - pub paid_expires_at: Option, + pub is_active_member: bool, + pub membership_expires_at: Option, pub payment_url: Option, } @@ -719,7 +719,7 @@ pub async fn web_login_auto_confirm( }; let paid_info = sqlx::query_as::<_, (bool, Option>)>( - "SELECT is_paid, paid_expires_at FROM users WHERE id = $1", + "SELECT is_member, membership_expires_at FROM users WHERE id = $1", ) .bind(user_id) .fetch_optional(pool.get_ref()) @@ -727,9 +727,9 @@ pub async fn web_login_auto_confirm( .ok() .flatten(); - let (is_paid_active, paid_expires_at): (bool, Option) = match paid_info { - Some((is_paid, expires)) => { - let active = is_paid && expires.map_or(true, |e| e > Utc::now()); + let (is_active_member, membership_expires_at): (bool, Option) = match paid_info { + Some((is_member, expires)) => { + let active = is_member && expires.map_or(true, |e| e > Utc::now()); (active, expires.map(|e| e.to_rfc3339())) } None => (false, None), @@ -737,18 +737,18 @@ pub async fn web_login_auto_confirm( let base_url = std::env::var("APP_BASE_URL") .unwrap_or_else(|_| "https://dev.xmclassmate.top".to_string()); - let payment_url = if is_paid_active { + let payment_url = if is_active_member { None } else { Some(format!("{}/payment?jwt={}", base_url, token)) }; - info!("[WEB LOGIN AUTO-CONFIRM] user_id={} is_paid={}", user_id, is_paid_active); + info!("[WEB LOGIN AUTO-CONFIRM] user_id={} is_member={}", user_id, is_active_member); HttpResponse::Ok().json(AutoConfirmResponse { success: true, token: Some(token), - is_paid_active, - paid_expires_at, + is_active_member, + membership_expires_at, payment_url, }) } diff --git a/src/handlers/payment.rs b/src/handlers/payment.rs index f33f17e..5e150e8 100644 --- a/src/handlers/payment.rs +++ b/src/handlers/payment.rs @@ -475,8 +475,8 @@ pub async fn payment_index() -> Result { if (resp.ok) { const data = await resp.json(); if (data.success) { - isPaidActive = data.data.is_paid_active || false; - paidExpiresAt = data.data.paid_expires_at || null; + isPaidActive = data.data.is_active_member || false; + paidExpiresAt = data.data.membership_expires_at || null; } } } catch(e) {} @@ -499,8 +499,8 @@ pub async fn payment_index() -> Result { const data = await resp.json(); if (data.success && data.token) { jwt = data.token; - isPaidActive = data.is_paid_active || false; - paidExpiresAt = data.paid_expires_at; + isPaidActive = data.is_active_member || false; + paidExpiresAt = data.membership_expires_at; clearInterval(pollTimer); showLoggedIn(); } @@ -995,8 +995,8 @@ pub async fn mock_confirm( Ok(HttpResponse::Ok().json(serde_json::json!({ "success": true, "data": { - "is_paid_active": true, - "paid_expires_at": expires_at, + "is_active_member": true, + "membership_expires_at": expires_at, } }))) } @@ -1026,7 +1026,7 @@ pub async fn sync_order( // 查询会员最新状态 let user = sqlx::query_as::<_, (bool, Option>)>( - r#"SELECT is_paid, paid_expires_at FROM users WHERE id = $1"#, + r#"SELECT is_member, membership_expires_at FROM users WHERE id = $1"#, ) .bind(user_id) .fetch_optional(pool.get_ref()) @@ -1034,16 +1034,16 @@ pub async fn sync_order( .map_err(|e| AppError::Database(format!("查询用户失败: {}", e)))? .ok_or_else(|| AppError::NotFound("用户不存在".to_string()))?; - let (is_paid, paid_expires_at) = user; - let is_paid_active = is_paid && paid_expires_at.map_or(true, |expires| expires > Utc::now()); + let (is_member, membership_expires_at) = user; + let is_active_member = is_member && membership_expires_at.map_or(true, |expires| expires > Utc::now()); Ok(HttpResponse::Ok().json(serde_json::json!({ "success": true, "data": { "order_status": order_status, - "is_paid": is_paid, - "is_paid_active": is_paid_active, - "paid_expires_at": paid_expires_at, + "is_member": is_member, + "is_active_member": is_active_member, + "membership_expires_at": membership_expires_at, } }))) } @@ -1100,15 +1100,15 @@ pub async fn get_user_quota( let is_maintenance = std::env::var("PAYMENT_MAINTENANCE_MODE").ok() == Some("true".to_string()); - let (used, is_paid_active, paid_expires_at, was_paid) = + let (used, is_active_member, membership_expires_at, was_member) = db::get_user_quota(pool.get_ref(), user_id).await?; // 维护模式使用更高的临时限额,但非会员仍然有限制防止滥用 - let (limit, unlimited) = if is_maintenance && !is_paid_active { + let (limit, unlimited) = if is_maintenance && !is_active_member { let ml: i64 = std::env::var("MAINTENANCE_MODE_DATA_LIMIT") .ok().and_then(|v| v.parse().ok()).unwrap_or(500); (ml, false) - } else if is_paid_active { + } else if is_active_member { (0, true) } else { let fl: i64 = std::env::var("FREE_USER_DATA_LIMIT") @@ -1116,7 +1116,7 @@ pub async fn get_user_quota( (fl, false) }; - let active = if is_maintenance { true } else { is_paid_active }; + let active = if is_maintenance { true } else { is_active_member }; Ok(HttpResponse::Ok().json(serde_json::json!({ "success": true, @@ -1124,9 +1124,9 @@ pub async fn get_user_quota( "used": used, "limit": limit, "unlimited": unlimited, - "is_paid_active": active, - "paid_expires_at": paid_expires_at, - "was_paid": was_paid, + "is_active_member": active, + "membership_expires_at": membership_expires_at, + "was_member": was_member, "maintenance_mode": is_maintenance, } }))) @@ -1144,8 +1144,8 @@ pub struct LoginStatusResponse { pub success: bool, pub confirmed: bool, pub token: Option, - pub is_paid_active: bool, - pub paid_expires_at: Option, + pub is_active_member: bool, + pub membership_expires_at: Option, } // ===== Handler: GET /payment/generate-code — 网页端生成登录码 ===== @@ -1212,8 +1212,8 @@ pub async fn payment_login_status( success: false, confirmed: false, token: None, - is_paid_active: false, - paid_expires_at: None, + is_active_member: false, + membership_expires_at: None, })); } }; @@ -1231,8 +1231,8 @@ pub async fn payment_login_status( success: false, confirmed: false, token: None, - is_paid_active: false, - paid_expires_at: None, + is_active_member: false, + membership_expires_at: None, })); } @@ -1243,8 +1243,8 @@ pub async fn payment_login_status( success: true, confirmed: false, token: None, - is_paid_active: false, - paid_expires_at: None, + is_active_member: false, + membership_expires_at: None, })); } @@ -1252,16 +1252,16 @@ pub async fn payment_login_status( let token = token.unwrap(); let user_id = user_id.unwrap(); - let (is_paid_active, paid_expires_at): (bool, Option) = + let (is_active_member, membership_expires_at): (bool, Option) = match sqlx::query_as::<_, (bool, Option>)>( - "SELECT is_paid, paid_expires_at FROM users WHERE id = $1" + "SELECT is_member, membership_expires_at FROM users WHERE id = $1" ) .bind(user_id) .fetch_optional(pool.get_ref()) .await { - Ok(Some((is_paid, expires))) => { - let active = is_paid && expires.map_or(true, |e| e > Utc::now()); + Ok(Some((is_member, expires))) => { + let active = is_member && expires.map_or(true, |e| e > Utc::now()); (active, expires.map(|e| e.to_rfc3339())) } _ => (false, None), @@ -1278,7 +1278,7 @@ pub async fn payment_login_status( success: true, confirmed: true, token: Some(token), - is_paid_active, - paid_expires_at, + is_active_member, + membership_expires_at, })) } diff --git a/src/handlers/user.rs b/src/handlers/user.rs index bba1f0d..effb6d5 100644 --- a/src/handlers/user.rs +++ b/src/handlers/user.rs @@ -17,11 +17,11 @@ pub async fn get_current_user_profile( let user = db::get_user_by_id(pool.get_ref(), user_id).await?; let is_maintenance = std::env::var("PAYMENT_MAINTENANCE_MODE").ok() == Some("true".to_string()); - let is_paid_active = if is_maintenance { + let is_active_member = if is_maintenance { true } else { - user.is_paid && - user.paid_expires_at.map_or(true, |expires| expires > chrono::Utc::now()) + user.is_member && + user.membership_expires_at.map_or(true, |expires| expires > chrono::Utc::now()) }; Ok(HttpResponse::Ok().json(serde_json::json!({ @@ -31,10 +31,10 @@ pub async fn get_current_user_profile( "name": user.name, "nickname": user.nickname, "avatarUrl": user.avatar_url, - "is_paid": user.is_paid, - "is_paid_active": is_paid_active, + "is_member": user.is_member, + "is_active_member": is_active_member, "is_admin": user.is_admin, - "paid_expires_at": user.paid_expires_at + "membership_expires_at": user.membership_expires_at } }))) } diff --git a/src/models.rs b/src/models.rs index f6027f2..e66fa84 100644 --- a/src/models.rs +++ b/src/models.rs @@ -318,12 +318,12 @@ pub struct User { #[serde(rename = "desc")] #[sqlx(rename = "desc")] pub description: Option, - #[sqlx(rename = "is_paid")] - pub is_paid: bool, + #[sqlx(rename = "is_member")] + pub is_member: bool, #[sqlx(rename = "is_admin")] pub is_admin: bool, - #[sqlx(rename = "paid_expires_at")] - pub paid_expires_at: Option>, + #[sqlx(rename = "membership_expires_at")] + pub membership_expires_at: Option>, #[serde(rename = "avatarUrl")] #[sqlx(rename = "avatar_url")] pub avatar_url: Option, @@ -335,8 +335,8 @@ pub struct User { // 管理员更新用户付费状态的请求体 #[derive(Debug, Deserialize)] pub struct UpdatePaymentRequest { - pub is_paid: bool, - pub paid_expires_at: Option, // ISO 8601 格式 + pub is_member: bool, + pub membership_expires_at: Option, // ISO 8601 格式 } // 应用状态结构体