Files
asd-backend/src/main.rs
Milky0217 6d2824589c feat: 添加用户信息存储功能
- 添加 avatar_url 和 nickname 字段到 users 表
- 修改 get_user_by_id 查询包含新字段
- 添加 save_user_profile API
- 优化 deploy.sh 默认使用 release 模式
2026-03-26 14:28:46 +08:00

831 lines
27 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use actix_web::middleware::from_fn;
use actix_web::{App, HttpResponse, HttpServer, Responder, delete, get, post, put, web};
use include_dir::{Dir, include_dir};
use log::{debug, error, info, warn};
use openssl::ssl::{SslAcceptor, SslAcceptorBuilder, SslFiletype, SslMethod};
use reqwest::Client;
use serde::Deserialize;
use sqlx::postgres::PgPool;
use std::pin::Pin;
mod auth;
mod db;
mod models;
use auth::{generate_token, jwt_middleware};
use db::{create_pool, insert_weather_data};
use models::{ErrorResponse, WeChatApiResponse, WeChatLoginRequest, WeatherData};
use crate::models::{Claims, TokenResponse, UpdatePaymentRequest};
// 编译时嵌入整个 static 目录
static STATIC_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/static");
static TEMPLATES_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates");
#[derive(Clone)]
struct AppState {
jwt_secret: String,
wechat_appid: String,
wechat_secret: String,
}
impl AppState {
fn load() -> Result<Self, String> {
Ok(Self {
jwt_secret: std::env::var("JWT_SECRET")
.map_err(|_| "环境变量JWT_SECRET未设置".to_string())?,
wechat_appid: std::env::var("WECHAT_APPID")
.map_err(|_| "环境变量WECHAT_APPID未设置".to_string())?,
wechat_secret: std::env::var("WECHAT_SECRET")
.map_err(|_| "环境变量WECHAT_SECRET未设置".to_string())?,
})
}
}
// 加载TLS证书和私钥
fn create_ssl_acceptor() -> Result<SslAcceptorBuilder, Box<dyn std::error::Error>> {
let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls())?;
// 从环境变量获取证书和私钥路径
let key_path =
std::env::var("SSL_KEY_PATH").unwrap_or_else(|_| "path/to/private.key".to_string());
let cert_path =
std::env::var("SSL_CERT_PATH").unwrap_or_else(|_| "path/to/certificate.crt".to_string());
builder.set_private_key_file(&key_path, SslFiletype::PEM)?;
builder.set_certificate_chain_file(&cert_path)?;
// 返回构建器而不是构建后的接受器
Ok(builder)
}
#[post("/api/login")]
async fn login(
pool: web::Data<PgPool>,
req: web::Json<WeChatLoginRequest>,
http_client: web::Data<Client>,
app_state: web::Data<AppState>,
) -> impl Responder {
let url = format!(
"https://api.weixin.qq.com/sns/jscode2session?appid={}&secret={}&js_code={}&grant_type=authorization_code",
app_state.wechat_appid, app_state.wechat_secret, req.code
);
debug!("准备调用微信API: {}", url.replace(&app_state.wechat_secret, "***"));
let wechat_response = match http_client.get(&url).send().await {
Ok(response) => {
debug!("微信API请求成功状态码: {}", response.status());
response
}
Err(e) => {
error!("微信API请求失败: {}", e);
return HttpResponse::InternalServerError().json(ErrorResponse {
error: "请求微信服务失败".to_string(),
errcode: None,
errmsg: None,
});
}
};
let wechat_data: WeChatApiResponse = match wechat_response.json().await {
Ok(data) => {
debug!("微信API响应解析成功");
data
}
Err(e) => {
error!("微信API响应解析失败: {}", e);
return HttpResponse::InternalServerError().json(ErrorResponse {
error: "解析微信响应失败".to_string(),
errcode: None,
errmsg: None,
});
}
};
if let Some(errcode) = wechat_data.errcode {
warn!(
"微信登录验证失败 - 错误码: {}, 错误信息: {:?}",
errcode, wechat_data.errmsg
);
return HttpResponse::BadRequest().json(ErrorResponse {
error: "微信登录失败".to_string(),
errcode: Some(errcode),
errmsg: wechat_data.errmsg,
});
}
let openid = match wechat_data.openid {
Some(id) => {
let masked_openid = format!("{}***{}", &id[0..4], &id[id.len() - 4..]);
debug!("获取到用户openid: {}", masked_openid);
id
}
None => {
error!("微信API响应中未包含openid");
return HttpResponse::InternalServerError().json(ErrorResponse {
error: "微信API未返回openid".to_string(),
errcode: None,
errmsg: None,
});
}
};
let upsert_query = r#"
INSERT INTO users (openid, name, type)
VALUES ($1, left($1, 8), 2)
ON CONFLICT (openid) DO UPDATE SET id = users.id
RETURNING id
"#;
let user_id = match sqlx::query_as::<_, (i32,)>(upsert_query)
.bind(&openid)
.fetch_one(pool.get_ref())
.await
{
Ok((id,)) => {
info!("用户登录/注册成功用户ID: {}", id);
id
}
Err(e) => {
error!("用户信息处理失败: {}", e);
return HttpResponse::InternalServerError().json(ErrorResponse {
error: "用户信息处理失败".to_string(),
errcode: Some(500),
errmsg: Some(e.to_string()),
});
}
};
let token = match generate_token(user_id, &openid, 2, &app_state.jwt_secret) {
Ok(t) => {
debug!("JWT生成成功: {}", &t[0..8]);
t
}
Err(e) => {
error!("JWT生成失败: {}", e);
return HttpResponse::InternalServerError().json(ErrorResponse {
error: "生成身份令牌失败".to_string(),
errcode: Some(500),
errmsg: Some(e.to_string()),
});
}
};
let token_response = TokenResponse::new(token);
HttpResponse::Ok().json(token_response)
}
#[post("/api/post-weather-data")]
async fn post_weather_data(
data: web::Json<WeatherData>,
pool: web::Data<PgPool>,
claims: web::ReqData<Claims>,
) -> impl Responder {
info!("Received weather data, preparing to insert into DB...");
let user_id = claims.user_id;
// 调用 db.rs 中的函数来处理数据库逻辑
match insert_weather_data(pool.get_ref(), &data, user_id).await {
Ok(inserted_id) => {
info!(
"Successfully inserted weather data with id: {}",
inserted_id
);
HttpResponse::Ok().json(serde_json::json!({
"success": true,
"message": "Weather data inserted successfully",
"inserted_id": inserted_id,
"received_assignment": data.assignment_number
}))
}
Err(error_msg) => {
error!("Database operation failed: {}", error_msg);
HttpResponse::Ok().json(serde_json::json!({
"success": false,
"errcode": 500,
"errmsg": error_msg
}))
}
}
}
#[post("/api/generate-temp-token/{resource_id}")]
async fn generate_temp_token_handler(
path: web::Path<i32>,
pool: web::Data<PgPool>,
claims: web::ReqData<Claims>,
app_state: web::Data<AppState>,
) -> impl Responder {
let resource_id = path.into_inner();
let openid: &String = &claims.openid;
match db::get_weather_details(pool.get_ref(), resource_id).await {
Ok(weather_data) => {
if weather_data.openid != *openid {
return HttpResponse::Forbidden().json(ErrorResponse {
error: "无权限为该资源生成临时token".to_string(),
errcode: Some(403),
errmsg: None,
});
}
}
Err(e) => {
return HttpResponse::BadRequest().json(ErrorResponse {
error: format!("资源不存在: {}", e),
errcode: Some(404),
errmsg: None,
});
}
}
let temp_token = match auth::generate_temp_token(openid, resource_id, &app_state.jwt_secret, 10) {
Ok(token) => token,
Err(e) => {
return HttpResponse::InternalServerError().json(ErrorResponse {
error: format!("生成临时token失败: {}", e),
errcode: Some(500),
errmsg: None,
});
}
};
HttpResponse::Ok().json(serde_json::json!({
"success": true,
"temp_token": temp_token,
"expire_minutes": 10,
}))
}
#[get("/weather/details")]
async fn get_weather_details(
pool: web::Data<PgPool>,
claims: Option<web::ReqData<Claims>>,
query: web::Query<serde_json::Value>,
app_state: web::Data<AppState>,
) -> impl Responder {
let mut is_temp_token = false;
let (openid, weather_id) =
if let Some(temp_token) = query.get("temp_token").and_then(|v| v.as_str()) {
is_temp_token = true;
let temp_claims = match auth::verify_temp_token(temp_token, &app_state.jwt_secret) {
Ok(c) => c,
Err(e) => {
return HttpResponse::Unauthorized().json(ErrorResponse {
error: format!("临时token无效: {}", e),
errcode: Some(401),
errmsg: None,
});
}
};
(temp_claims.openid, temp_claims.resource_id)
} else if let Some(claims) = claims {
let weather_id = match query.get("id").and_then(|v| v.as_i64()) {
Some(id) => id as i32,
None => {
return HttpResponse::BadRequest().json(ErrorResponse {
error: "缺少资源ID参数id".to_string(),
errcode: Some(400),
errmsg: None,
});
}
};
(claims.openid.clone(), weather_id)
} else {
return HttpResponse::Unauthorized().json(ErrorResponse {
error: "缺少token需提供JWT或临时token".to_string(),
errcode: Some(401),
errmsg: None,
});
};
let weather_data = match db::get_weather_details(pool.get_ref(), weather_id).await {
Ok(data) => {
if data.openid != openid {
return HttpResponse::Forbidden().json(ErrorResponse {
error: "无权限访问该数据".to_string(),
errcode: Some(403),
errmsg: None,
});
}
data
}
Err(error_msg) => {
error!("获取天气数据详情失败: {}", error_msg);
let errcode = if error_msg.starts_with("未找到") {
404
} else {
500
};
return HttpResponse::Ok().json(serde_json::json!({
"success": false,
"errcode": errcode,
"errmsg": error_msg
}));
}
};
if is_temp_token {
let index_html = match TEMPLATES_DIR.get_file("index.html") {
Some(file) => file.contents_utf8().unwrap_or_default(),
None => {
return HttpResponse::InternalServerError().json(ErrorResponse {
error: "无法找到模板文件".to_string(),
errcode: Some(500),
errmsg: None,
});
}
};
let weather_data_json = serde_json::to_string(&weather_data).unwrap_or_default();
let data_script = format!(
r#"
<script>
window.weatherData = {};
</script>
"#,
weather_data_json
);
let rendered_html = index_html
.replace("<!-- 详细信息将在这里显示 -->", &data_script)
.replace(
"style=\"display: none;\"",
"",
);
HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(rendered_html)
} else {
HttpResponse::Ok().json(serde_json::json!({
"success": true,
"data": weather_data
}))
}
}
#[get("/weather")]
async fn get_weather_brief(
query: web::Query<serde_json::Value>,
pool: web::Data<PgPool>,
claims: web::ReqData<Claims>,
) -> impl Responder {
let page = match query.get("page") {
Some(v) => {
let raw = if let Some(num) = v.as_i64() {
num as i32
} else if let Some(s) = v.as_str() {
s.parse().unwrap_or(1)
} else {
1
};
raw.max(1)
}
None => 1,
};
let limit = match query.get("limit") {
Some(v) => {
let raw = if let Some(num) = v.as_i64() {
num as i32
} else if let Some(s) = v.as_str() {
s.parse().unwrap_or(10)
} else {
10
};
raw.clamp(1, 100)
}
None => 10,
};
debug!("获取天气数据列表, 页码: {}, 每页条数: {}", page, limit);
match db::get_weather_list(pool.get_ref(), claims.user_id, page, limit).await {
Ok(response) => {
debug!(
"获取到的天气列表: {:?}, 总条数: {}",
response.list, response.total
);
HttpResponse::Ok().json(serde_json::json!({
"success": true,
"data": response.list,
"page": page,
"limit": limit,
"total": response.total
}))
}
Err(error_msg) => {
error!("获取天气数据列表失败: {}", error_msg);
HttpResponse::Ok().json(serde_json::json!({
"success": false,
"errcode": 500,
"errmsg": error_msg
}))
}
}
}
// 删除天气数据
#[delete("/weather/delete/{id}")]
async fn delete_weather(
path: web::Path<i32>,
pool: web::Data<PgPool>,
claims: web::ReqData<Claims>,
) -> impl Responder {
let weather_id = path.into_inner();
info!("删除天气数据, ID: {}", weather_id);
match db::delete_weather_data(pool.get_ref(), weather_id, claims.user_id).await {
Ok(_) => HttpResponse::Ok().json(serde_json::json!({
"success": true,
"message": format!("天气数据 {} 已成功删除", weather_id)
})),
Err(error_msg) => {
error!("删除天气数据失败: {}", error_msg);
let errcode = if error_msg.starts_with("未找到") {
404
} else {
500
};
HttpResponse::Ok().json(serde_json::json!({
"success": false,
"errcode": errcode,
"errmsg": error_msg
}))
}
}
}
// 管理员获取用户信息
#[get("/api/admin/users/{id}")]
async fn admin_get_user(
path: web::Path<i32>,
pool: web::Data<PgPool>,
claims: web::ReqData<Claims>,
) -> impl Responder {
let target_user_id = path.into_inner();
info!("管理员获取用户信息, 目标用户ID: {}", target_user_id);
// 验证当前用户是否为管理员
let current_user = match db::get_user_by_id(pool.get_ref(), claims.user_id).await {
Ok(user) => user,
Err(e) => {
error!("获取当前用户信息失败: {}", e);
return HttpResponse::Ok().json(serde_json::json!({
"success": false,
"errcode": 500,
"errmsg": "获取用户信息失败"
}));
}
};
if !current_user.is_admin {
return HttpResponse::Forbidden().json(serde_json::json!({
"success": false,
"errcode": 403,
"errmsg": "无权限执行此操作"
}));
}
// 获取目标用户信息
match db::get_user_by_id(pool.get_ref(), target_user_id).await {
Ok(user) => HttpResponse::Ok().json(serde_json::json!({
"success": true,
"data": user
})),
Err(e) => {
error!("获取目标用户信息失败: {}", e);
let errcode = if e.starts_with("未找到") { 404 } else { 500 };
HttpResponse::Ok().json(serde_json::json!({
"success": false,
"errcode": errcode,
"errmsg": e
}))
}
}
}
// 管理员更新用户付费状态
#[put("/api/admin/users/{id}/payment")]
async fn admin_update_user_payment(
path: web::Path<i32>,
pool: web::Data<PgPool>,
claims: web::ReqData<Claims>,
body: web::Json<UpdatePaymentRequest>,
) -> impl Responder {
let target_user_id = path.into_inner();
info!("管理员更新用户付费状态, 目标用户ID: {}", target_user_id);
// 验证当前用户是否为管理员
let current_user = match db::get_user_by_id(pool.get_ref(), claims.user_id).await {
Ok(user) => user,
Err(e) => {
error!("获取当前用户信息失败: {}", e);
return HttpResponse::Ok().json(serde_json::json!({
"success": false,
"errcode": 500,
"errmsg": "获取用户信息失败"
}));
}
};
if !current_user.is_admin {
return HttpResponse::Forbidden().json(serde_json::json!({
"success": false,
"errcode": 403,
"errmsg": "无权限执行此操作"
}));
}
// 解析 paid_expires_at
let paid_expires_at = match &body.paid_expires_at {
Some(date_str) => match chrono::DateTime::parse_from_rfc3339(date_str) {
Ok(dt) => Some(dt.with_timezone(&chrono::Utc)),
Err(e) => {
return HttpResponse::BadRequest().json(serde_json::json!({
"success": false,
"errcode": 400,
"errmsg": format!("日期格式错误: {}", e)
}));
}
},
None => None,
};
// 更新用户付费状态
match db::update_user_payment_status(pool.get_ref(), target_user_id, body.is_paid, paid_expires_at).await {
Ok(_) => HttpResponse::Ok().json(serde_json::json!({
"success": true,
"message": "用户付费状态已更新"
})),
Err(e) => {
error!("更新用户付费状态失败: {}", e);
HttpResponse::Ok().json(serde_json::json!({
"success": false,
"errcode": 500,
"errmsg": e
}))
}
}
}
// 在 main.rs 中添加以下函数,用于处理静态文件请求
async fn serve_static_files(path: web::Path<String>) -> impl Responder {
// 获取请求的文件路径(例如 "css/style.css"
let file_path = path.into_inner();
// 【调试日志 1】打印服务器收到的文件路径
debug!("请求静态文件: {}", file_path);
// 从嵌入的 STATIC_DIR 中查找文件
match STATIC_DIR.get_file(&file_path) {
Some(file) => {
// 根据文件扩展名设置 Content-Type
let content_type = match file_path.split('.').next_back() {
Some("ico") => "image/x-icon",
Some("css") => "text/css",
Some("js") => "application/javascript",
Some("html") => "text/html",
Some("ttf") => "font/ttf",
Some("woff") => "font/woff",
Some("woff2") => "font/woff2",
_ => "application/octet-stream", // 默认类型
};
debug!(
"成功找到文件: {}, Content-Type: {}",
file_path, content_type
);
HttpResponse::Ok()
.content_type(content_type)
.body(file.contents())
}
None => {
// 文件不存在时返回 404
// 【调试日志 3】打印文件未找到的信息
error!("文件未找到: {}", file_path);
HttpResponse::NotFound().body("静态文件不存在")
}
}
}
// 获取当前用户信息(普通用户可用)
#[get("/api/user/profile")]
async fn get_current_user_profile(
pool: web::Data<PgPool>,
claims: web::ReqData<Claims>,
) -> impl Responder {
info!("获取当前用户信息, 用户ID: {}", claims.user_id);
match db::get_user_by_id(pool.get_ref(), claims.user_id).await {
Ok(user) => {
let is_paid_active = user.is_paid &&
(user.paid_expires_at.is_none() || user.paid_expires_at.unwrap() > chrono::Utc::now());
HttpResponse::Ok().json(serde_json::json!({
"success": true,
"data": {
"id": user.id,
"name": user.name,
"nickname": user.nickname,
"avatarUrl": user.avatar_url,
"is_paid": user.is_paid,
"is_paid_active": is_paid_active,
"is_admin": user.is_admin,
"paid_expires_at": user.paid_expires_at
}
}))
},
Err(e) => {
error!("获取用户信息失败: {}", e);
HttpResponse::Ok().json(serde_json::json!({
"success": false,
"errcode": 500,
"errmsg": e
}))
}
}
}
// 保存用户信息请求体
#[derive(Debug, Deserialize)]
pub struct SaveUserProfileRequest {
pub nickname: Option<String>,
pub avatar_url: Option<String>,
}
// 保存用户信息(普通用户可用)
#[put("/api/user/profile")]
async fn save_user_profile(
pool: web::Data<PgPool>,
claims: web::ReqData<Claims>,
body: web::Json<SaveUserProfileRequest>,
) -> impl Responder {
info!("保存用户信息, 用户ID: {}", claims.user_id);
match db::update_user_profile(pool.get_ref(), claims.user_id, &body.nickname, &body.avatar_url).await {
Ok(_) => {
HttpResponse::Ok().json(serde_json::json!({
"success": true
}))
},
Err(e) => {
error!("保存用户信息失败: {}", e);
HttpResponse::Ok().json(serde_json::json!({
"success": false,
"errcode": 500,
"errmsg": e
}))
}
}
}
fn create_server_config(
pool: PgPool,
http_client: Client,
app_state: AppState,
) -> App<
impl actix_web::dev::ServiceFactory<
actix_web::dev::ServiceRequest,
Config = (),
Response = actix_web::dev::ServiceResponse,
Error = actix_web::Error,
InitError = (),
>,
> {
App::new()
.app_data(web::Data::new(pool))
.app_data(web::Data::new(http_client))
.app_data(web::Data::new(app_state))
.service(web::resource("/static/{tail:.*}").route(web::get().to(serve_static_files)))
.service(login)
.service(get_weather_details)
.service(
web::scope("")
.wrap(from_fn(jwt_middleware))
.service(post_weather_data)
.service(get_weather_brief)
.service(generate_temp_token_handler)
.service(delete_weather)
.service(get_current_user_profile)
.service(save_user_profile)
.service(admin_get_user)
.service(admin_update_user_payment),
)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
if let Err(e) = dotenvy::dotenv() {
eprintln!("警告:加载.env文件失败使用系统环境变量: {}", e);
}
env_logger::init();
info!("logger init successful");
let app_state = match AppState::load() {
Ok(state) => state,
Err(e) => {
error!("环境变量配置错误: {}", e);
std::process::exit(1);
}
};
let pool = match create_pool().await {
Ok(pool) => pool,
Err(e) => {
error!("Failed to create database pool: {}", e);
error!("Please check your database connection configuration in .env file");
std::process::exit(1);
}
};
let http_client = Client::new();
info!("Attempting to start server...");
let ports = vec![4433, 8443, 8080, 3000, 8000, 8888];
let mut server: Option<
Pin<Box<dyn std::future::Future<Output = std::io::Result<()>> + Unpin>>,
> = None;
let mut bound_port = 0;
for port in &ports {
let addr = format!("127.0.0.1:{}", port);
info!("Trying to bind to {}", addr);
let pool_clone = pool.clone();
let http_client_clone = http_client.clone();
let app_state_clone = app_state.clone();
if *port == 443 || *port == 8443 {
let ssl_builder = match create_ssl_acceptor() {
Ok(builder) => builder,
Err(e) => {
error!("Failed to create SSL acceptor: {}", e);
continue;
}
};
match HttpServer::new(move || {
create_server_config(pool_clone.clone(), http_client_clone.clone(), app_state_clone.clone())
})
.bind_openssl(&addr, ssl_builder)
{
Ok(s) => {
info!("Successfully bound to {} with HTTPS", addr);
let server_future = s.run();
server = Some(Box::pin(server_future));
bound_port = *port;
break;
}
Err(e) => {
error!("Failed to bind to {} with HTTPS: {}", addr, e);
if e.kind() == std::io::ErrorKind::PermissionDenied {
error!(" -> Permission denied. Try running with sudo or use a port > 1024");
}
continue;
}
}
} else {
match HttpServer::new(move || {
create_server_config(pool_clone.clone(), http_client_clone.clone(), app_state_clone.clone())
})
.bind(&addr)
{
Ok(s) => {
info!("Successfully bound to {} with HTTP", addr);
let server_future = s.run();
server = Some(Box::pin(server_future));
bound_port = *port;
break;
}
Err(e) => {
error!("Failed to bind to {} with HTTP: {}", addr, e);
if e.kind() == std::io::ErrorKind::PermissionDenied {
error!(
" -> Permission denied. Try running with sudo or use a port > 1024"
);
}
continue;
}
}
}
}
match server {
Some(s) => {
if bound_port == 443 || bound_port == 8443 {
info!("Server started successfully with HTTPS");
} else {
info!("Server started successfully with HTTP");
}
s.await
}
None => {
error!("Failed to bind to any port. Please check your system configuration.");
std::process::exit(1);
}
}
}