支付系统核心:配额限制与用户付费状态管理
- 添加 tokio 依赖用于异步测试 - 启用 User 结构体,添加 is_paid/is_admin/paid_expires_at 字段 - 添加 UpdatePaymentRequest 请求体 - insert_weather_data 集成配额检查逻辑 - 新增 get_user_by_id、count_user_weather_data、update_user_payment_status - 添加 payment_fields 数据库迁移脚本
This commit is contained in:
99
src/db.rs
99
src/db.rs
@@ -1,28 +1,30 @@
|
||||
use sqlx::{PgPool, postgres::PgPoolOptions};
|
||||
use std::env;
|
||||
use std::error::Error;
|
||||
use chrono::Utc;
|
||||
|
||||
// 从 models 模块引入 WeatherData 结构体
|
||||
use crate::models::{WeatherData, WeatherDataBrief, WeatherListResponse};
|
||||
use crate::models::{User, WeatherData, WeatherDataBrief, WeatherListResponse};
|
||||
|
||||
// 用于插入weather_data的数据
|
||||
pub async fn insert_weather_data(pool: &PgPool, weather_data: &WeatherData) -> Result<i32, String> {
|
||||
// 1. 根据 openid 查询 user_id (这部分逻辑不变)
|
||||
let user_id = match sqlx::query_as::<_, (i32,)>("SELECT id FROM users WHERE openid = $1")
|
||||
.bind(&weather_data.openid)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
{
|
||||
Ok(Some((id,))) => id,
|
||||
Ok(None) => {
|
||||
return Err(format!("未找到openid为 {} 的用户", weather_data.openid));
|
||||
pub async fn insert_weather_data(pool: &PgPool, weather_data: &WeatherData, user_id: i32) -> Result<i32, String> {
|
||||
// 配额检查:非付费用户数据条数限制
|
||||
let user = get_user_by_id(pool, user_id).await?;
|
||||
let is_paid_active = user.is_paid && (user.paid_expires_at.is_none() || user.paid_expires_at.unwrap() > Utc::now());
|
||||
|
||||
if !is_paid_active {
|
||||
let current_count = count_user_weather_data(pool, user_id).await?;
|
||||
let limit: i64 = env::var("FREE_USER_DATA_LIMIT")
|
||||
.unwrap_or_else(|_| "20".to_string())
|
||||
.parse()
|
||||
.unwrap_or(20);
|
||||
|
||||
if current_count >= limit {
|
||||
return Err("数据条数已达上限,请升级为付费用户".to_string());
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(format!("查询用户失败: {}", e));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 2. 准备插入数据的 SQL 语句 (SQL本身不变)
|
||||
// 准备插入数据的 SQL 语句
|
||||
let insert_query = r#"
|
||||
INSERT INTO weather_data (
|
||||
user_id, title, date, hour, min, longitude, latitude, daysincejanfirst,
|
||||
@@ -242,3 +244,68 @@ pub async fn delete_weather_data(
|
||||
Err(e) => Err(format!("删除天气数据失败: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
// 根据用户ID获取用户信息
|
||||
pub async fn get_user_by_id(pool: &PgPool, user_id: i32) -> Result<User, String> {
|
||||
let query = r#"
|
||||
SELECT
|
||||
id, name, openid, phone, type, desc, is_paid, is_admin, paid_expires_at
|
||||
FROM users
|
||||
WHERE id = $1
|
||||
"#;
|
||||
|
||||
let row = match sqlx::query_as::<_, User>(query)
|
||||
.bind(user_id)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
{
|
||||
Ok(Some(row)) => row,
|
||||
Ok(None) => return Err(format!("未找到ID为 {} 的用户", user_id)),
|
||||
Err(e) => return Err(format!("查询用户信息失败: {}", e)),
|
||||
};
|
||||
|
||||
Ok(row)
|
||||
}
|
||||
|
||||
// 统计用户的天气数据条数
|
||||
pub async fn count_user_weather_data(pool: &PgPool, user_id: i32) -> Result<i64, String> {
|
||||
let query = r#"
|
||||
SELECT COUNT(*) FROM weather_data WHERE user_id = $1
|
||||
"#;
|
||||
|
||||
let count = match sqlx::query_as::<_, (i64,)>(query)
|
||||
.bind(user_id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
{
|
||||
Ok((count,)) => count,
|
||||
Err(e) => return Err(format!("查询天气数据条数失败: {}", e)),
|
||||
};
|
||||
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
// 更新用户付费状态
|
||||
pub async fn update_user_payment_status(
|
||||
pool: &PgPool,
|
||||
user_id: i32,
|
||||
is_paid: bool,
|
||||
paid_expires_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
) -> Result<(), String> {
|
||||
let query = r#"
|
||||
UPDATE users
|
||||
SET is_paid = $1, paid_expires_at = $2
|
||||
WHERE id = $3
|
||||
"#;
|
||||
|
||||
match sqlx::query(query)
|
||||
.bind(is_paid)
|
||||
.bind(paid_expires_at)
|
||||
.bind(user_id)
|
||||
.execute(pool)
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(format!("更新用户付费状态失败: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user