refactor: is_paid→is_member, paid_expires_at→membership_expires_at 全局重命名

This commit is contained in:
2026-05-25 15:33:31 +08:00
parent 94ad9ba190
commit 722ab837d0
7 changed files with 116 additions and 98 deletions

View File

@@ -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;

View File

@@ -11,12 +11,12 @@ use crate::error::AppError;
pub async fn insert_weather_data(pool: &PgPool, weather_data: &WeatherData, user_id: i32) -> Result<i32, AppError> { pub async fn insert_weather_data(pool: &PgPool, weather_data: &WeatherData, user_id: i32) -> Result<i32, AppError> {
// 配额检查:非付费用户数据条数限制 // 配额检查:非付费用户数据条数限制
let user = get_user_by_id(pool, user_id).await?; 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()); 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 current_count = count_user_weather_data(pool, user_id).await?;
let limit: i64 = if is_maintenance { let limit: i64 = if is_maintenance {
env::var("MAINTENANCE_MODE_DATA_LIMIT") 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<User, AppError> { pub async fn get_user_by_id(pool: &PgPool, user_id: i32) -> Result<User, AppError> {
let query = r#" let query = r#"
SELECT 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 avatar_url, nickname
FROM users FROM users
WHERE id = $1 WHERE id = $1
@@ -278,18 +278,18 @@ pub async fn count_user_weather_data(pool: &PgPool, user_id: i32) -> Result<i64,
pub async fn update_user_payment_status( pub async fn update_user_payment_status(
pool: &PgPool, pool: &PgPool,
user_id: i32, user_id: i32,
is_paid: bool, is_member: bool,
paid_expires_at: Option<chrono::DateTime<chrono::Utc>>, membership_expires_at: Option<chrono::DateTime<chrono::Utc>>,
) -> Result<(), AppError> { ) -> Result<(), AppError> {
let query = r#" let query = r#"
UPDATE users UPDATE users
SET is_paid = $1, paid_expires_at = $2 SET is_member = $1, membership_expires_at = $2
WHERE id = $3 WHERE id = $3
"#; "#;
match sqlx::query(query) match sqlx::query(query)
.bind(is_paid) .bind(is_member)
.bind(paid_expires_at) .bind(membership_expires_at)
.bind(user_id) .bind(user_id)
.execute(pool) .execute(pool)
.await .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 payment_orders SET status = 'paid', paid_at = NOW() WHERE order_no = $1 RETURNING package_type
) )
UPDATE users SET UPDATE users SET
is_paid = true, is_member = true,
paid_expires_at = membership_expires_at =
CASE CASE
WHEN uo.package_type = 'permanent' THEN NULL 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 CASE
WHEN uo.package_type = 'monthly' THEN INTERVAL '30 days' WHEN uo.package_type = 'monthly' THEN INTERVAL '30 days'
WHEN uo.package_type = 'yearly' THEN INTERVAL '365 days' WHEN uo.package_type = 'yearly' THEN INTERVAL '365 days'
@@ -523,7 +523,7 @@ pub async fn confirm_payment_order(
END END
FROM updated_order uo FROM updated_order uo
WHERE users.id = $2 WHERE users.id = $2
RETURNING users.paid_expires_at RETURNING users.membership_expires_at
"#, "#,
) )
.bind(order_no) .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 payment_orders SET status = 'paid', paid_at = NOW() WHERE order_no = $1 RETURNING package_type, user_id
) )
UPDATE users SET UPDATE users SET
is_paid = true, is_member = true,
paid_expires_at = membership_expires_at =
CASE CASE
WHEN uo.package_type = 'permanent' THEN NULL 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 CASE
WHEN uo.package_type = 'monthly' THEN INTERVAL '30 days' WHEN uo.package_type = 'monthly' THEN INTERVAL '30 days'
WHEN uo.package_type = 'yearly' THEN INTERVAL '365 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 { if other_active.0 == 0 {
sqlx::query( 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) .bind(user_id)
.execute(pool) .execute(pool)
@@ -684,10 +684,10 @@ pub async fn get_user_quota(
user_id: i32, user_id: i32,
) -> Result<(i64, bool, Option<chrono::DateTime<chrono::Utc>>, bool), AppError> { ) -> Result<(i64, bool, Option<chrono::DateTime<chrono::Utc>>, bool), AppError> {
let user = get_user_by_id(pool, user_id).await?; let user = get_user_by_id(pool, user_id).await?;
let is_paid_active = user.is_paid let is_active_member = user.is_member
&& user.paid_expires_at.map_or(true, |expires| expires > chrono::Utc::now()); && user.membership_expires_at.map_or(true, |expires| expires > chrono::Utc::now());
let used = count_user_weather_data(pool, user_id).await?; 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 函数 ===== // ===== 收藏功能 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 payment_orders SET status = 'paid', paid_at = NOW() WHERE order_no = $1 RETURNING package_type
) )
UPDATE users SET UPDATE users SET
is_paid = true, is_member = true,
paid_expires_at = membership_expires_at =
CASE CASE
WHEN uo.package_type = 'permanent' THEN NULL 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 CASE
WHEN uo.package_type = 'monthly' THEN INTERVAL '30 days' WHEN uo.package_type = 'monthly' THEN INTERVAL '30 days'
WHEN uo.package_type = 'yearly' THEN INTERVAL '365 days' WHEN uo.package_type = 'yearly' THEN INTERVAL '365 days'
@@ -937,7 +937,7 @@ pub async fn admin_force_confirm_order(
END END
FROM updated_order uo FROM updated_order uo
WHERE users.id = $2 WHERE users.id = $2
RETURNING users.paid_expires_at RETURNING users.membership_expires_at
"#, "#,
) )
.bind(order_no) .bind(order_no)
@@ -972,7 +972,7 @@ pub async fn admin_force_confirm_order(
// 返回当前到期时间 // 返回当前到期时间
sqlx::query_scalar::<_, Option<chrono::DateTime<chrono::Utc>>>( sqlx::query_scalar::<_, Option<chrono::DateTime<chrono::Utc>>>(
r#"SELECT paid_expires_at FROM users WHERE id = $1"#, r#"SELECT membership_expires_at FROM users WHERE id = $1"#,
) )
.bind(user_id) .bind(user_id)
.fetch_one(pool) .fetch_one(pool)
@@ -1074,7 +1074,7 @@ pub async fn admin_refund_order(
if other_active.0 == 0 { if other_active.0 == 0 {
sqlx::query( 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) .bind(user_id)
.execute(pool) .execute(pool)

View File

@@ -45,8 +45,8 @@ pub async fn admin_update_user_payment(
return Err(AppError::Forbidden("无权限执行此操作".to_string())); return Err(AppError::Forbidden("无权限执行此操作".to_string()));
} }
// 解析 paid_expires_at // 解析 membership_expires_at
let paid_expires_at = match &body.paid_expires_at { let membership_expires_at = match &body.membership_expires_at {
Some(date_str) => match DateTime::parse_from_rfc3339(date_str) { Some(date_str) => match DateTime::parse_from_rfc3339(date_str) {
Ok(dt) => Some(dt.with_timezone(&Utc)), Ok(dt) => Some(dt.with_timezone(&Utc)),
Err(e) => { 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( let _ = db::insert_payment_audit_log(
pool.get_ref(), "ADMIN_MANUAL", target_user_id, "admin_revoke", pool.get_ref(), "ADMIN_MANUAL", target_user_id, "admin_revoke",
Some(claims.user_id), 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; ).await;
Ok(HttpResponse::Ok().json(serde_json::json!({ Ok(HttpResponse::Ok().json(serde_json::json!({
"success": true, "success": true,

View File

@@ -498,8 +498,8 @@ pub struct WebLoginConfirmRequest {
pub struct WebLoginConfirmResponse { pub struct WebLoginConfirmResponse {
pub success: bool, pub success: bool,
pub token: Option<String>, pub token: Option<String>,
pub is_paid_active: bool, pub is_active_member: bool,
pub paid_expires_at: Option<String>, pub membership_expires_at: Option<String>,
} }
#[post("/api/web-login/confirm")] #[post("/api/web-login/confirm")]
@@ -529,8 +529,8 @@ pub async fn web_login_confirm(
return HttpResponse::Ok().json(WebLoginConfirmResponse { return HttpResponse::Ok().json(WebLoginConfirmResponse {
success: false, success: false,
token: None, token: None,
is_paid_active: false, is_active_member: false,
paid_expires_at: None, membership_expires_at: None,
}); });
} }
}; };
@@ -548,12 +548,12 @@ pub async fn web_login_confirm(
return HttpResponse::Ok().json(WebLoginConfirmResponse { return HttpResponse::Ok().json(WebLoginConfirmResponse {
success: false, success: false,
token: None, token: None,
is_paid_active: false, is_active_member: false,
paid_expires_at: None, membership_expires_at: None,
}); });
} }
// 如果已有用户,验证并获取最新 is_paid 信息 // 如果已有用户,验证并获取最新 is_member 信息
let user_id = if let Some(uid) = existing_user_id { let user_id = if let Some(uid) = existing_user_id {
uid uid
} else { } else {
@@ -588,16 +588,16 @@ pub async fn web_login_confirm(
}; };
// 查询付费状态 // 查询付费状态
let (is_paid_active, paid_expires_at): (bool, Option<String>) = let (is_active_member, membership_expires_at): (bool, Option<String>) =
match sqlx::query_as::<_, (bool, Option<chrono::DateTime<chrono::Utc>>)>( match sqlx::query_as::<_, (bool, Option<chrono::DateTime<chrono::Utc>>)>(
"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) .bind(user_id)
.fetch_optional(pool.get_ref()) .fetch_optional(pool.get_ref())
.await .await
{ {
Ok(Some((is_paid, expires))) => { Ok(Some((is_member, expires))) => {
let active = is_paid && expires.map_or(true, |e| e > Utc::now()); let active = is_member && expires.map_or(true, |e| e > Utc::now());
(active, expires.map(|e| e.to_rfc3339())) (active, expires.map(|e| e.to_rfc3339()))
} }
_ => (false, None), _ => (false, None),
@@ -613,12 +613,12 @@ pub async fn web_login_confirm(
tracing::warn!("更新登录码 token 失败: {}", e); 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 { HttpResponse::Ok().json(WebLoginConfirmResponse {
success: true, success: true,
token: Some(token), token: Some(token),
is_paid_active, is_active_member,
paid_expires_at, membership_expires_at,
}) })
} }
@@ -631,8 +631,8 @@ pub struct AutoConfirmRequest {
pub struct AutoConfirmResponse { pub struct AutoConfirmResponse {
pub success: bool, pub success: bool,
pub token: Option<String>, pub token: Option<String>,
pub is_paid_active: bool, pub is_active_member: bool,
pub paid_expires_at: Option<String>, pub membership_expires_at: Option<String>,
pub payment_url: Option<String>, pub payment_url: Option<String>,
} }
@@ -719,7 +719,7 @@ pub async fn web_login_auto_confirm(
}; };
let paid_info = sqlx::query_as::<_, (bool, Option<chrono::DateTime<chrono::Utc>>)>( let paid_info = sqlx::query_as::<_, (bool, Option<chrono::DateTime<chrono::Utc>>)>(
"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) .bind(user_id)
.fetch_optional(pool.get_ref()) .fetch_optional(pool.get_ref())
@@ -727,9 +727,9 @@ pub async fn web_login_auto_confirm(
.ok() .ok()
.flatten(); .flatten();
let (is_paid_active, paid_expires_at): (bool, Option<String>) = match paid_info { let (is_active_member, membership_expires_at): (bool, Option<String>) = match paid_info {
Some((is_paid, expires)) => { Some((is_member, expires)) => {
let active = is_paid && expires.map_or(true, |e| e > Utc::now()); let active = is_member && expires.map_or(true, |e| e > Utc::now());
(active, expires.map(|e| e.to_rfc3339())) (active, expires.map(|e| e.to_rfc3339()))
} }
None => (false, None), None => (false, None),
@@ -737,18 +737,18 @@ pub async fn web_login_auto_confirm(
let base_url = std::env::var("APP_BASE_URL") let base_url = std::env::var("APP_BASE_URL")
.unwrap_or_else(|_| "https://dev.xmclassmate.top".to_string()); .unwrap_or_else(|_| "https://dev.xmclassmate.top".to_string());
let payment_url = if is_paid_active { let payment_url = if is_active_member {
None None
} else { } else {
Some(format!("{}/payment?jwt={}", base_url, token)) 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 { HttpResponse::Ok().json(AutoConfirmResponse {
success: true, success: true,
token: Some(token), token: Some(token),
is_paid_active, is_active_member,
paid_expires_at, membership_expires_at,
payment_url, payment_url,
}) })
} }

View File

@@ -475,8 +475,8 @@ pub async fn payment_index() -> Result<HttpResponse, AppError> {
if (resp.ok) { if (resp.ok) {
const data = await resp.json(); const data = await resp.json();
if (data.success) { if (data.success) {
isPaidActive = data.data.is_paid_active || false; isPaidActive = data.data.is_active_member || false;
paidExpiresAt = data.data.paid_expires_at || null; paidExpiresAt = data.data.membership_expires_at || null;
} }
} }
} catch(e) {} } catch(e) {}
@@ -499,8 +499,8 @@ pub async fn payment_index() -> Result<HttpResponse, AppError> {
const data = await resp.json(); const data = await resp.json();
if (data.success && data.token) { if (data.success && data.token) {
jwt = data.token; jwt = data.token;
isPaidActive = data.is_paid_active || false; isPaidActive = data.is_active_member || false;
paidExpiresAt = data.paid_expires_at; paidExpiresAt = data.membership_expires_at;
clearInterval(pollTimer); clearInterval(pollTimer);
showLoggedIn(); showLoggedIn();
} }
@@ -995,8 +995,8 @@ pub async fn mock_confirm(
Ok(HttpResponse::Ok().json(serde_json::json!({ Ok(HttpResponse::Ok().json(serde_json::json!({
"success": true, "success": true,
"data": { "data": {
"is_paid_active": true, "is_active_member": true,
"paid_expires_at": expires_at, "membership_expires_at": expires_at,
} }
}))) })))
} }
@@ -1026,7 +1026,7 @@ pub async fn sync_order(
// 查询会员最新状态 // 查询会员最新状态
let user = sqlx::query_as::<_, (bool, Option<chrono::DateTime<chrono::Utc>>)>( let user = sqlx::query_as::<_, (bool, Option<chrono::DateTime<chrono::Utc>>)>(
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) .bind(user_id)
.fetch_optional(pool.get_ref()) .fetch_optional(pool.get_ref())
@@ -1034,16 +1034,16 @@ pub async fn sync_order(
.map_err(|e| AppError::Database(format!("查询用户失败: {}", e)))? .map_err(|e| AppError::Database(format!("查询用户失败: {}", e)))?
.ok_or_else(|| AppError::NotFound("用户不存在".to_string()))?; .ok_or_else(|| AppError::NotFound("用户不存在".to_string()))?;
let (is_paid, paid_expires_at) = user; let (is_member, membership_expires_at) = user;
let is_paid_active = is_paid && paid_expires_at.map_or(true, |expires| expires > Utc::now()); let is_active_member = is_member && membership_expires_at.map_or(true, |expires| expires > Utc::now());
Ok(HttpResponse::Ok().json(serde_json::json!({ Ok(HttpResponse::Ok().json(serde_json::json!({
"success": true, "success": true,
"data": { "data": {
"order_status": order_status, "order_status": order_status,
"is_paid": is_paid, "is_member": is_member,
"is_paid_active": is_paid_active, "is_active_member": is_active_member,
"paid_expires_at": paid_expires_at, "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 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?; 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") let ml: i64 = std::env::var("MAINTENANCE_MODE_DATA_LIMIT")
.ok().and_then(|v| v.parse().ok()).unwrap_or(500); .ok().and_then(|v| v.parse().ok()).unwrap_or(500);
(ml, false) (ml, false)
} else if is_paid_active { } else if is_active_member {
(0, true) (0, true)
} else { } else {
let fl: i64 = std::env::var("FREE_USER_DATA_LIMIT") let fl: i64 = std::env::var("FREE_USER_DATA_LIMIT")
@@ -1116,7 +1116,7 @@ pub async fn get_user_quota(
(fl, false) (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!({ Ok(HttpResponse::Ok().json(serde_json::json!({
"success": true, "success": true,
@@ -1124,9 +1124,9 @@ pub async fn get_user_quota(
"used": used, "used": used,
"limit": limit, "limit": limit,
"unlimited": unlimited, "unlimited": unlimited,
"is_paid_active": active, "is_active_member": active,
"paid_expires_at": paid_expires_at, "membership_expires_at": membership_expires_at,
"was_paid": was_paid, "was_member": was_member,
"maintenance_mode": is_maintenance, "maintenance_mode": is_maintenance,
} }
}))) })))
@@ -1144,8 +1144,8 @@ pub struct LoginStatusResponse {
pub success: bool, pub success: bool,
pub confirmed: bool, pub confirmed: bool,
pub token: Option<String>, pub token: Option<String>,
pub is_paid_active: bool, pub is_active_member: bool,
pub paid_expires_at: Option<String>, pub membership_expires_at: Option<String>,
} }
// ===== Handler: GET /payment/generate-code — 网页端生成登录码 ===== // ===== Handler: GET /payment/generate-code — 网页端生成登录码 =====
@@ -1212,8 +1212,8 @@ pub async fn payment_login_status(
success: false, success: false,
confirmed: false, confirmed: false,
token: None, token: None,
is_paid_active: false, is_active_member: false,
paid_expires_at: None, membership_expires_at: None,
})); }));
} }
}; };
@@ -1231,8 +1231,8 @@ pub async fn payment_login_status(
success: false, success: false,
confirmed: false, confirmed: false,
token: None, token: None,
is_paid_active: false, is_active_member: false,
paid_expires_at: None, membership_expires_at: None,
})); }));
} }
@@ -1243,8 +1243,8 @@ pub async fn payment_login_status(
success: true, success: true,
confirmed: false, confirmed: false,
token: None, token: None,
is_paid_active: false, is_active_member: false,
paid_expires_at: None, membership_expires_at: None,
})); }));
} }
@@ -1252,16 +1252,16 @@ pub async fn payment_login_status(
let token = token.unwrap(); let token = token.unwrap();
let user_id = user_id.unwrap(); let user_id = user_id.unwrap();
let (is_paid_active, paid_expires_at): (bool, Option<String>) = let (is_active_member, membership_expires_at): (bool, Option<String>) =
match sqlx::query_as::<_, (bool, Option<chrono::DateTime<chrono::Utc>>)>( match sqlx::query_as::<_, (bool, Option<chrono::DateTime<chrono::Utc>>)>(
"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) .bind(user_id)
.fetch_optional(pool.get_ref()) .fetch_optional(pool.get_ref())
.await .await
{ {
Ok(Some((is_paid, expires))) => { Ok(Some((is_member, expires))) => {
let active = is_paid && expires.map_or(true, |e| e > Utc::now()); let active = is_member && expires.map_or(true, |e| e > Utc::now());
(active, expires.map(|e| e.to_rfc3339())) (active, expires.map(|e| e.to_rfc3339()))
} }
_ => (false, None), _ => (false, None),
@@ -1278,7 +1278,7 @@ pub async fn payment_login_status(
success: true, success: true,
confirmed: true, confirmed: true,
token: Some(token), token: Some(token),
is_paid_active, is_active_member,
paid_expires_at, membership_expires_at,
})) }))
} }

View File

@@ -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 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_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 true
} else { } else {
user.is_paid && user.is_member &&
user.paid_expires_at.map_or(true, |expires| expires > chrono::Utc::now()) user.membership_expires_at.map_or(true, |expires| expires > chrono::Utc::now())
}; };
Ok(HttpResponse::Ok().json(serde_json::json!({ Ok(HttpResponse::Ok().json(serde_json::json!({
@@ -31,10 +31,10 @@ pub async fn get_current_user_profile(
"name": user.name, "name": user.name,
"nickname": user.nickname, "nickname": user.nickname,
"avatarUrl": user.avatar_url, "avatarUrl": user.avatar_url,
"is_paid": user.is_paid, "is_member": user.is_member,
"is_paid_active": is_paid_active, "is_active_member": is_active_member,
"is_admin": user.is_admin, "is_admin": user.is_admin,
"paid_expires_at": user.paid_expires_at "membership_expires_at": user.membership_expires_at
} }
}))) })))
} }

View File

@@ -318,12 +318,12 @@ pub struct User {
#[serde(rename = "desc")] #[serde(rename = "desc")]
#[sqlx(rename = "desc")] #[sqlx(rename = "desc")]
pub description: Option<String>, pub description: Option<String>,
#[sqlx(rename = "is_paid")] #[sqlx(rename = "is_member")]
pub is_paid: bool, pub is_member: bool,
#[sqlx(rename = "is_admin")] #[sqlx(rename = "is_admin")]
pub is_admin: bool, pub is_admin: bool,
#[sqlx(rename = "paid_expires_at")] #[sqlx(rename = "membership_expires_at")]
pub paid_expires_at: Option<chrono::DateTime<chrono::Utc>>, pub membership_expires_at: Option<chrono::DateTime<chrono::Utc>>,
#[serde(rename = "avatarUrl")] #[serde(rename = "avatarUrl")]
#[sqlx(rename = "avatar_url")] #[sqlx(rename = "avatar_url")]
pub avatar_url: Option<String>, pub avatar_url: Option<String>,
@@ -335,8 +335,8 @@ pub struct User {
// 管理员更新用户付费状态的请求体 // 管理员更新用户付费状态的请求体
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct UpdatePaymentRequest { pub struct UpdatePaymentRequest {
pub is_paid: bool, pub is_member: bool,
pub paid_expires_at: Option<String>, // ISO 8601 格式 pub membership_expires_at: Option<String>, // ISO 8601 格式
} }
// 应用状态结构体 // 应用状态结构体