diff --git a/src/db.rs b/src/db.rs index 3fd0cc8..f93e776 100644 --- a/src/db.rs +++ b/src/db.rs @@ -972,3 +972,117 @@ pub async fn admin_force_confirm_order( Ok(new_expires) } + +/// 用户主动取消待支付订单 +pub async fn cancel_payment_order( + pool: &PgPool, + order_no: &str, + user_id: i32, +) -> Result<(), AppError> { + // 检查订单有效且属于该用户 + let row = sqlx::query_as::<_, (i32, String)>( + r#"SELECT user_id, status FROM payment_orders WHERE order_no = $1"#, + ) + .bind(order_no) + .fetch_optional(pool) + .await + .map_err(|e| AppError::Database(format!("查询订单失败: {}", e)))?; + + let (order_user_id, status) = match row { + Some(r) => r, + None => return Err(AppError::NotFound("订单不存在".to_string())), + }; + + if order_user_id != user_id { + return Err(AppError::Forbidden("无权操作此订单".to_string())); + } + + if status != "pending" { + return Err(AppError::BadRequest(format!("订单状态为 {},无法取消", status))); + } + + sqlx::query("UPDATE payment_orders SET status = 'cancelled' WHERE order_no = $1") + .bind(order_no) + .execute(pool) + .await + .map_err(|e| AppError::Database(format!("取消订单失败: {}", e)))?; + + let _ = insert_payment_audit_log(pool, order_no, user_id, "cancelled", Some(user_id), + Some("用户主动取消待支付订单")).await; + + Ok(()) +} + +/// 管理员手动退款(标记订单为 refunded + 重新计算会员) +pub async fn admin_refund_order( + pool: &PgPool, + order_no: &str, + admin_user_id: i32, +) -> Result<(), AppError> { + // 查出订单信息 + let order = sqlx::query_as::<_, (i32, String)>( + r#"SELECT user_id, status FROM payment_orders WHERE order_no = $1"#, + ) + .bind(order_no) + .fetch_optional(pool) + .await + .map_err(|e| AppError::Database(format!("查询订单失败: {}", e)))?; + + let (user_id, status) = match order { + Some(o) => o, + None => return Err(AppError::NotFound("订单不存在".to_string())), + }; + + if status != "paid" && status != "pending" { + return Err(AppError::BadRequest(format!("订单状态为 {},无法退款", status))); + } + + // 标记为 refunded + sqlx::query("UPDATE payment_orders SET status = 'refunded' WHERE order_no = $1") + .bind(order_no) + .execute(pool) + .await + .map_err(|e| AppError::Database(format!("退款标记失败: {}", e)))?; + + // 如果订单是已支付状态,重新计算会员 + if status == "paid" { + // 检查该用户是否有其他有效的已支付订单 + let other_active: (i64,) = sqlx::query_as( + r#" + SELECT COUNT(*) FROM payment_orders + WHERE user_id = $1 + AND status = 'paid' + AND order_no != $2 + AND (expires_at IS NULL OR expires_at > NOW()) + "#, + ) + .bind(user_id) + .bind(order_no) + .fetch_one(pool) + .await + .map_err(|e| AppError::Database(format!("查询其他有效订单失败: {}", e)))?; + + if other_active.0 == 0 { + sqlx::query( + "UPDATE users SET is_paid = false, paid_expires_at = NULL WHERE id = $1", + ) + .bind(user_id) + .execute(pool) + .await + .map_err(|e| AppError::Database(format!("撤销会员失败: {}", e)))?; + tracing::info!("管理员退款: 用户 {} 的会员因订单 {} 退款已被撤销", user_id, order_no); + } else { + tracing::info!( + "管理员退款: 用户 {} 有其他有效订单({}笔),保留会员", + user_id, + other_active.0 + ); + } + } + + let detail = if status == "paid" { "管理员手动退款(已支付订单)" } else { "管理员取消订单(待支付订单)" }; + let _ = insert_payment_audit_log(pool, order_no, user_id, "refunded", + Some(admin_user_id), Some(detail)).await; + + Ok(()) +} diff --git a/src/handlers/admin.rs b/src/handlers/admin.rs index 43f3f79..42728a3 100644 --- a/src/handlers/admin.rs +++ b/src/handlers/admin.rs @@ -81,7 +81,6 @@ pub async fn admin_force_confirm_order( let order_no = path.into_inner(); tracing::info!("管理员强制确认订单: {}", order_no); - // 验证当前用户是否为管理员 let current_user = db::get_user_by_id(pool.get_ref(), claims.user_id).await?; if !current_user.is_admin { return Err(AppError::Forbidden("无权限执行此操作".to_string())); @@ -97,3 +96,27 @@ pub async fn admin_force_confirm_order( } }))) } + +/// POST /api/admin/orders/{order_no}/refund +/// 管理员手动退款(标记订单 + 重新计算会员) +#[post("/api/admin/orders/{order_no}/refund")] +pub async fn admin_refund_order( + path: web::Path, + pool: web::Data, + claims: web::ReqData, +) -> Result { + let order_no = path.into_inner(); + tracing::info!("管理员退款订单: {}", order_no); + + let current_user = db::get_user_by_id(pool.get_ref(), claims.user_id).await?; + if !current_user.is_admin { + return Err(AppError::Forbidden("无权限执行此操作".to_string())); + } + + db::admin_refund_order(pool.get_ref(), &order_no, claims.user_id).await?; + + Ok(HttpResponse::Ok().json(serde_json::json!({ + "success": true, + "message": "订单已退款" + }))) +} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 4fdac19..85a8386 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -19,6 +19,7 @@ pub static TEMPLATES_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates"); pub use admin::admin_get_user; pub use admin::admin_update_user_payment; pub use admin::admin_force_confirm_order; +pub use admin::admin_refund_order; pub use auth::login; pub use auth::mock_login; pub use auth::refresh_token; @@ -40,6 +41,7 @@ pub use weather::post_weather_data; pub use payment::alipay_notify; pub use payment::alipay_pay_page; pub use payment::alipay_refund_notify; +pub use payment::cancel_order; pub use payment::create_order; pub use payment::generate_code; pub use payment::get_user_quota; diff --git a/src/handlers/payment.rs b/src/handlers/payment.rs index fa14552..0d2bafc 100644 --- a/src/handlers/payment.rs +++ b/src/handlers/payment.rs @@ -1003,6 +1003,25 @@ pub async fn sync_order( }))) } +/// POST /api/payment/cancel-order — 用户主动取消待支付订单 +#[derive(Debug, Deserialize)] +pub struct CancelOrderRequest { + pub order_id: String, +} + +#[post("/api/payment/cancel-order")] +pub async fn cancel_order( + pool: web::Data, + claims: web::ReqData, + body: web::Json, +) -> Result { + db::cancel_payment_order(pool.get_ref(), &body.order_id, claims.user_id).await?; + Ok(HttpResponse::Ok().json(serde_json::json!({ + "success": true, + "message": "订单已取消" + }))) +} + /// GET /api/payment/orders — 获取当前用户的订单记录 #[get("/api/payment/orders")] pub async fn get_user_orders( diff --git a/src/main.rs b/src/main.rs index 9422562..6ac5e6c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,7 +18,7 @@ use auth::jwt_middleware; use config::AppConfig; use db::create_pool; use handlers::{ - admin_force_confirm_order, admin_get_user, admin_update_user_payment, add_favorite, alipay_notify, alipay_pay_page, alipay_refund_notify, + admin_force_confirm_order, admin_get_user, admin_refund_order, admin_update_user_payment, add_favorite, alipay_notify, alipay_pay_page, alipay_refund_notify, cancel_order, create_order, delete_weather, generate_code, generate_temp_token_handler, get_current_user_profile, get_favorites, get_user_quota, get_weather_brief, get_weather_details, get_user_orders, health_check, login, mock_login, mock_confirm, sync_order, payment_index, payment_login_status, payment_page, payment_success, @@ -108,9 +108,11 @@ fn create_server_config( .service(admin_get_user) .service(admin_update_user_payment) .service(admin_force_confirm_order) + .service(admin_refund_order) .service(create_order) .service(mock_confirm) .service(sync_order) + .service(cancel_order) .service(get_user_orders) .service(get_user_quota) .service(get_favorites)