支付系统核心:配额限制与用户付费状态管理
- 添加 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:
13
Cargo.lock
generated
13
Cargo.lock
generated
@@ -2079,6 +2079,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2718,9 +2719,21 @@ dependencies = [
|
|||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
"slab",
|
"slab",
|
||||||
"socket2 0.6.0",
|
"socket2 0.6.0",
|
||||||
|
"tokio-macros",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-macros"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-native-tls"
|
name = "tokio-native-tls"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
|
|||||||
@@ -18,3 +18,7 @@ reqwest = { version = "0.12.23", features=["json"]}
|
|||||||
serde = { version = "1.0.219", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
serde_json = "1.0.143"
|
serde_json = "1.0.143"
|
||||||
sqlx = { version = "0.8.6", features = ["runtime-tokio-rustls", "postgres", "chrono", "uuid"] }
|
sqlx = { version = "0.8.6", features = ["runtime-tokio-rustls", "postgres", "chrono", "uuid"] }
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
|||||||
3
migrations/001_add_payment_fields.sql
Normal file
3
migrations/001_add_payment_fields.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS is_paid BOOLEAN NOT NULL DEFAULT false;
|
||||||
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS is_admin BOOLEAN NOT NULL DEFAULT false;
|
||||||
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS paid_expires_at TIMESTAMPTZ DEFAULT NULL;
|
||||||
99
src/db.rs
99
src/db.rs
@@ -1,28 +1,30 @@
|
|||||||
use sqlx::{PgPool, postgres::PgPoolOptions};
|
use sqlx::{PgPool, postgres::PgPoolOptions};
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
use chrono::Utc;
|
||||||
|
|
||||||
// 从 models 模块引入 WeatherData 结构体
|
// 从 models 模块引入 WeatherData 结构体
|
||||||
use crate::models::{WeatherData, WeatherDataBrief, WeatherListResponse};
|
use crate::models::{User, WeatherData, WeatherDataBrief, WeatherListResponse};
|
||||||
|
|
||||||
// 用于插入weather_data的数据
|
// 用于插入weather_data的数据
|
||||||
pub async fn insert_weather_data(pool: &PgPool, weather_data: &WeatherData) -> Result<i32, String> {
|
pub async fn insert_weather_data(pool: &PgPool, weather_data: &WeatherData, user_id: i32) -> Result<i32, String> {
|
||||||
// 1. 根据 openid 查询 user_id (这部分逻辑不变)
|
// 配额检查:非付费用户数据条数限制
|
||||||
let user_id = match sqlx::query_as::<_, (i32,)>("SELECT id FROM users WHERE openid = $1")
|
let user = get_user_by_id(pool, user_id).await?;
|
||||||
.bind(&weather_data.openid)
|
let is_paid_active = user.is_paid && (user.paid_expires_at.is_none() || user.paid_expires_at.unwrap() > Utc::now());
|
||||||
.fetch_optional(pool)
|
|
||||||
.await
|
if !is_paid_active {
|
||||||
{
|
let current_count = count_user_weather_data(pool, user_id).await?;
|
||||||
Ok(Some((id,))) => id,
|
let limit: i64 = env::var("FREE_USER_DATA_LIMIT")
|
||||||
Ok(None) => {
|
.unwrap_or_else(|_| "20".to_string())
|
||||||
return Err(format!("未找到openid为 {} 的用户", weather_data.openid));
|
.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#"
|
let insert_query = r#"
|
||||||
INSERT INTO weather_data (
|
INSERT INTO weather_data (
|
||||||
user_id, title, date, hour, min, longitude, latitude, daysincejanfirst,
|
user_id, title, date, hour, min, longitude, latitude, daysincejanfirst,
|
||||||
@@ -242,3 +244,68 @@ pub async fn delete_weather_data(
|
|||||||
Err(e) => Err(format!("删除天气数据失败: {}", e)),
|
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)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -272,8 +272,7 @@ pub struct WeatherData {
|
|||||||
pub wind_speed_suitability: String,
|
pub wind_speed_suitability: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||||
#[derive(Debug, Serialize, FromRow)]
|
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
#[serde(rename = "name")]
|
#[serde(rename = "name")]
|
||||||
@@ -289,5 +288,17 @@ 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")]
|
||||||
|
pub is_paid: bool,
|
||||||
|
#[sqlx(rename = "is_admin")]
|
||||||
|
pub is_admin: bool,
|
||||||
|
#[sqlx(rename = "paid_expires_at")]
|
||||||
|
pub paid_expires_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 管理员更新用户付费状态的请求体
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct UpdatePaymentRequest {
|
||||||
|
pub is_paid: bool,
|
||||||
|
pub paid_expires_at: Option<String>, // ISO 8601 格式
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|||||||
Reference in New Issue
Block a user