feat: implement payment order system (create-order, mock-confirm, get-quota)
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -1980,6 +1980,7 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
"tracing-appender",
|
"tracing-appender",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2982,6 +2983,7 @@ version = "1.18.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be"
|
checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"getrandom 0.3.3",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -20,6 +20,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"] }
|
||||||
|
uuid = { version = "1", features = ["v4"] }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
|
|
||||||
|
|||||||
93
src/db.rs
93
src/db.rs
@@ -311,3 +311,96 @@ pub async fn update_user_profile(
|
|||||||
Err(e) => Err(format!("更新用户个人信息失败: {}", e)),
|
Err(e) => Err(format!("更新用户个人信息失败: {}", e)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== 支付系统 DB 函数 =====
|
||||||
|
|
||||||
|
/// 创建待支付订单
|
||||||
|
pub async fn create_payment_order(
|
||||||
|
pool: &PgPool,
|
||||||
|
user_id: i32,
|
||||||
|
order_no: &str,
|
||||||
|
package_type: &str,
|
||||||
|
amount: i32,
|
||||||
|
expires_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let query = r#"
|
||||||
|
INSERT INTO payment_orders (user_id, order_no, package_type, amount, expires_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
|
"#;
|
||||||
|
|
||||||
|
sqlx::query(query)
|
||||||
|
.bind(user_id)
|
||||||
|
.bind(order_no)
|
||||||
|
.bind(package_type)
|
||||||
|
.bind(amount)
|
||||||
|
.bind(expires_at)
|
||||||
|
.execute(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("创建订单失败: {}", e))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 确认订单支付(模拟):更新订单状态 + 激活用户付费
|
||||||
|
///
|
||||||
|
/// 返回该订单的 expires_at(永久会员为 None)
|
||||||
|
pub async fn confirm_payment_order(
|
||||||
|
pool: &PgPool,
|
||||||
|
order_no: &str,
|
||||||
|
user_id: i32,
|
||||||
|
) -> Result<Option<chrono::DateTime<chrono::Utc>>, String> {
|
||||||
|
let row = sqlx::query_as::<_, (i32, String, Option<chrono::DateTime<chrono::Utc>>)>(
|
||||||
|
r#"SELECT user_id, status, expires_at FROM payment_orders WHERE order_no = $1"#,
|
||||||
|
)
|
||||||
|
.bind(order_no)
|
||||||
|
.fetch_optional(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("查询订单失败: {}", e))?;
|
||||||
|
|
||||||
|
let (order_user_id, status, expires_at) = match row {
|
||||||
|
Some(r) => r,
|
||||||
|
None => return Err("订单不存在".to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if order_user_id != user_id {
|
||||||
|
return Err("无权操作此订单".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if status != "pending" {
|
||||||
|
return Err("订单状态异常,无法确认支付".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlx::query(
|
||||||
|
r#"UPDATE payment_orders SET status = 'paid', paid_at = NOW() WHERE order_no = $1"#,
|
||||||
|
)
|
||||||
|
.bind(order_no)
|
||||||
|
.execute(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("更新订单状态失败: {}", e))?;
|
||||||
|
|
||||||
|
sqlx::query(
|
||||||
|
r#"UPDATE users SET is_paid = true, paid_expires_at = $1 WHERE id = $2"#,
|
||||||
|
)
|
||||||
|
.bind(expires_at)
|
||||||
|
.bind(user_id)
|
||||||
|
.execute(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("更新用户付费状态失败: {}", e))?;
|
||||||
|
|
||||||
|
Ok(expires_at)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取用户配额信息
|
||||||
|
///
|
||||||
|
/// 返回 (已用条数, 是否付费活跃, 到期时间)
|
||||||
|
pub async fn get_user_quota(
|
||||||
|
pool: &PgPool,
|
||||||
|
user_id: i32,
|
||||||
|
) -> Result<(i64, bool, Option<chrono::DateTime<chrono::Utc>>), 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() > chrono::Utc::now());
|
||||||
|
let used = count_user_weather_data(pool, user_id).await?;
|
||||||
|
Ok((used, is_paid_active, user.paid_expires_at))
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
pub mod admin;
|
pub mod admin;
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod health;
|
pub mod health;
|
||||||
|
pub mod payment;
|
||||||
pub mod static_files;
|
pub mod static_files;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
pub mod weather;
|
pub mod weather;
|
||||||
@@ -24,3 +25,7 @@ pub use weather::generate_temp_token_handler;
|
|||||||
pub use weather::get_weather_brief;
|
pub use weather::get_weather_brief;
|
||||||
pub use weather::get_weather_details;
|
pub use weather::get_weather_details;
|
||||||
pub use weather::post_weather_data;
|
pub use weather::post_weather_data;
|
||||||
|
|
||||||
|
pub use payment::create_order;
|
||||||
|
pub use payment::get_user_quota;
|
||||||
|
pub use payment::mock_confirm;
|
||||||
|
|||||||
160
src/handlers/payment.rs
Normal file
160
src/handlers/payment.rs
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
// handlers/payment.rs — 支付相关处理器
|
||||||
|
use actix_web::{get, post, web, HttpResponse, Responder};
|
||||||
|
use chrono::Utc;
|
||||||
|
use sqlx::postgres::PgPool;
|
||||||
|
use tracing::error;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::db;
|
||||||
|
use crate::models::{Claims, CreateOrderRequest, MockConfirmRequest};
|
||||||
|
|
||||||
|
struct PackageInfo {
|
||||||
|
amount: i32,
|
||||||
|
display_amount: &'static str,
|
||||||
|
display_name: &'static str,
|
||||||
|
days: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_package_info(package_type: &str) -> Option<PackageInfo> {
|
||||||
|
match package_type {
|
||||||
|
"monthly" => Some(PackageInfo {
|
||||||
|
amount: 990,
|
||||||
|
display_amount: "¥9.9",
|
||||||
|
display_name: "包月会员",
|
||||||
|
days: Some(30),
|
||||||
|
}),
|
||||||
|
"yearly" => Some(PackageInfo {
|
||||||
|
amount: 5900,
|
||||||
|
display_amount: "¥59",
|
||||||
|
display_name: "包年会员",
|
||||||
|
days: Some(365),
|
||||||
|
}),
|
||||||
|
"permanent" => Some(PackageInfo {
|
||||||
|
amount: 19900,
|
||||||
|
display_amount: "¥199",
|
||||||
|
display_name: "永久会员",
|
||||||
|
days: None,
|
||||||
|
}),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// POST /api/payment/create-order
|
||||||
|
#[post("/api/payment/create-order")]
|
||||||
|
pub async fn create_order(
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
claims: web::ReqData<Claims>,
|
||||||
|
body: web::Json<CreateOrderRequest>,
|
||||||
|
) -> impl Responder {
|
||||||
|
let user_id = claims.user_id;
|
||||||
|
|
||||||
|
let pkg = match get_package_info(&body.package_type) {
|
||||||
|
Some(p) => p,
|
||||||
|
None => {
|
||||||
|
return HttpResponse::Ok().json(serde_json::json!({
|
||||||
|
"success": false,
|
||||||
|
"errcode": 400,
|
||||||
|
"errmsg": "无效的套餐类型"
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let order_no = Uuid::new_v4().to_string();
|
||||||
|
let expires_at = pkg.days.map(|d| Utc::now() + chrono::Duration::days(d));
|
||||||
|
|
||||||
|
match db::create_payment_order(
|
||||||
|
pool.get_ref(),
|
||||||
|
user_id,
|
||||||
|
&order_no,
|
||||||
|
&body.package_type,
|
||||||
|
pkg.amount,
|
||||||
|
expires_at,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => HttpResponse::Ok().json(serde_json::json!({
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"order_id": order_no,
|
||||||
|
"package_type": body.package_type,
|
||||||
|
"amount": pkg.amount,
|
||||||
|
"display_amount": pkg.display_amount,
|
||||||
|
"display_name": pkg.display_name,
|
||||||
|
"expires_at": expires_at,
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
Err(e) => {
|
||||||
|
error!("创建订单失败: {}", e);
|
||||||
|
HttpResponse::Ok().json(serde_json::json!({
|
||||||
|
"success": false,
|
||||||
|
"errcode": 500,
|
||||||
|
"errmsg": "创建订单失败,请重试"
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// POST /api/payment/mock-confirm
|
||||||
|
#[post("/api/payment/mock-confirm")]
|
||||||
|
pub async fn mock_confirm(
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
claims: web::ReqData<Claims>,
|
||||||
|
body: web::Json<MockConfirmRequest>,
|
||||||
|
) -> impl Responder {
|
||||||
|
let user_id = claims.user_id;
|
||||||
|
|
||||||
|
match db::confirm_payment_order(pool.get_ref(), &body.order_id, user_id).await {
|
||||||
|
Ok(expires_at) => HttpResponse::Ok().json(serde_json::json!({
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"is_paid_active": true,
|
||||||
|
"paid_expires_at": expires_at,
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
Err(e) => {
|
||||||
|
error!("确认支付失败: {}", e);
|
||||||
|
HttpResponse::Ok().json(serde_json::json!({
|
||||||
|
"success": false,
|
||||||
|
"errcode": 400,
|
||||||
|
"errmsg": "支付确认失败,请重试"
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GET /api/user/quota
|
||||||
|
#[get("/api/user/quota")]
|
||||||
|
pub async fn get_user_quota(
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
claims: web::ReqData<Claims>,
|
||||||
|
) -> impl Responder {
|
||||||
|
let user_id = claims.user_id;
|
||||||
|
|
||||||
|
match db::get_user_quota(pool.get_ref(), user_id).await {
|
||||||
|
Ok((used, is_paid_active, paid_expires_at)) => {
|
||||||
|
let limit: i64 = std::env::var("FREE_USER_DATA_LIMIT")
|
||||||
|
.ok()
|
||||||
|
.and_then(|v| v.parse().ok())
|
||||||
|
.unwrap_or(20);
|
||||||
|
|
||||||
|
HttpResponse::Ok().json(serde_json::json!({
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"used": used,
|
||||||
|
"limit": limit,
|
||||||
|
"unlimited": is_paid_active,
|
||||||
|
"is_paid_active": is_paid_active,
|
||||||
|
"paid_expires_at": paid_expires_at,
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("获取配额信息失败: {}", e);
|
||||||
|
HttpResponse::Ok().json(serde_json::json!({
|
||||||
|
"success": false,
|
||||||
|
"errcode": 500,
|
||||||
|
"errmsg": "获取配额信息失败"
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/main.rs
10
src/main.rs
@@ -17,9 +17,10 @@ use auth::jwt_middleware;
|
|||||||
use config::AppConfig;
|
use config::AppConfig;
|
||||||
use db::create_pool;
|
use db::create_pool;
|
||||||
use handlers::{
|
use handlers::{
|
||||||
admin_get_user, admin_update_user_payment, delete_weather, generate_temp_token_handler,
|
admin_get_user, admin_update_user_payment, create_order, delete_weather,
|
||||||
get_current_user_profile, get_weather_brief, get_weather_details, health_check, login,
|
generate_temp_token_handler, get_current_user_profile, get_user_quota,
|
||||||
post_weather_data, save_user_profile, serve_static_files,
|
get_weather_brief, get_weather_details, health_check, login,
|
||||||
|
mock_confirm, post_weather_data, save_user_profile, serve_static_files,
|
||||||
};
|
};
|
||||||
use models::AppState;
|
use models::AppState;
|
||||||
|
|
||||||
@@ -74,6 +75,9 @@ fn create_server_config(
|
|||||||
.service(save_user_profile) // #[put("/api/user/profile")]
|
.service(save_user_profile) // #[put("/api/user/profile")]
|
||||||
.service(admin_get_user) // #[get("/api/admin/users/{id}")]
|
.service(admin_get_user) // #[get("/api/admin/users/{id}")]
|
||||||
.service(admin_update_user_payment) // #[put("/api/admin/users/{id}/payment")]
|
.service(admin_update_user_payment) // #[put("/api/admin/users/{id}/payment")]
|
||||||
|
.service(create_order) // #[post("/api/payment/create-order")]
|
||||||
|
.service(mock_confirm) // #[post("/api/payment/mock-confirm")]
|
||||||
|
.service(get_user_quota) // #[get("/api/user/quota")]
|
||||||
)
|
)
|
||||||
// 健康检查
|
// 健康检查
|
||||||
.service(health_check)
|
.service(health_check)
|
||||||
|
|||||||
@@ -327,3 +327,31 @@ impl AppState {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== 支付系统 =====
|
||||||
|
|
||||||
|
/// 创建订单请求体
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct CreateOrderRequest {
|
||||||
|
pub package_type: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 模拟确认支付请求体
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct MockConfirmRequest {
|
||||||
|
pub order_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// payment_orders 表行结构
|
||||||
|
#[derive(Debug, Serialize, FromRow)]
|
||||||
|
pub struct PaymentOrder {
|
||||||
|
pub id: i32,
|
||||||
|
pub user_id: i32,
|
||||||
|
pub order_no: String,
|
||||||
|
pub package_type: String,
|
||||||
|
pub amount: i32,
|
||||||
|
pub status: String,
|
||||||
|
pub paid_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
|
pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
|
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user