Files
asd-backend/src/main.rs
Milky0217 e19d6af2fa 将println都修改为了info,
eprintln修改为了error,
将部分debug信息输出修改为了debug
2025-10-17 12:11:07 +08:00

750 lines
26 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 std::pin::Pin;
use actix_web::middleware::from_fn;
use actix_web::{App, HttpResponse, HttpServer, Responder, delete, get, post, web};
use include_dir::{Dir, include_dir};
use log::{debug, error, info, warn};
use openssl::ssl::{SslAcceptor, SslAcceptorBuilder, SslFiletype, SslMethod};
use reqwest::Client;
use sqlx::postgres::PgPool;
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};
// 编译时嵌入整个 static 目录
static STATIC_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/static");
static TEMPLATES_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates");
// 加载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>,
) -> impl Responder {
// 从环境变量获取微信小程序配置
let appid = match std::env::var("WECHAT_APPID") {
Ok(id) => {
debug!("成功加载微信小程序appid");
id
}
Err(_) => {
error!("环境变量中未配置WECHAT_APPID");
return HttpResponse::InternalServerError().json(ErrorResponse {
error: "服务器配置错误缺少微信小程序appid".to_string(),
errcode: None,
errmsg: None,
});
}
};
let secret = match std::env::var("WECHAT_SECRET") {
Ok(secret) => {
debug!("成功加载微信小程序secret");
secret
}
Err(_) => {
error!("环境变量中未配置WECHAT_SECRET");
return HttpResponse::InternalServerError().json(ErrorResponse {
error: "服务器配置错误缺少微信小程序secret".to_string(),
errcode: None,
errmsg: None,
});
}
};
// 构建微信API请求URL
let url = format!(
"https://api.weixin.qq.com/sns/jscode2session?appid={}&secret={}&js_code={}&grant_type=authorization_code",
appid, secret, req.code
);
debug!("准备调用微信API: {}", url.replace(&secret, "***")); // 日志中隐藏secret
// 调用微信API
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,
});
}
};
// 解析微信API响应
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,
});
}
};
// 检查微信API响应中的错误
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,
});
}
// 获取openid
let openid = match wechat_data.openid {
Some(id) => {
// 日志中只显示openid的前4位和后4位保护用户隐私
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 query = r#"
INSERT INTO users (openid, name, type)
VALUES ($1, left($1, 8), 2)
ON CONFLICT (openid) DO NOTHING
RETURNING id
"#;
let user_id = match sqlx::query_as::<_, (i32,)>(query)
.bind(&openid)
.fetch_optional(pool.get_ref())
.await
{
Ok(Some((id,))) => {
info!("用户已存在用户ID: {}", id);
id
}
Ok(None) => {
// 新用户创建成功查询ID
match sqlx::query_as::<_, (i32,)>("SELECT id FROM users WHERE openid = $1")
.bind(&openid)
.fetch_one(pool.get_ref())
.await
{
Ok((id,)) => {
info!("新用户创建成功用户ID: {}", id);
id
}
Err(e) => {
error!("新用户创建后查询ID失败: {}", e);
return HttpResponse::InternalServerError().json(ErrorResponse {
error: "创建用户后查询ID失败".to_string(),
errcode: Some(500),
errmsg: Some(e.to_string()),
});
}
}
}
Err(e) => {
error!("用户查询/创建失败: {}", e);
return HttpResponse::InternalServerError().json(ErrorResponse {
error: "用户信息处理失败".to_string(),
errcode: Some(500),
errmsg: Some(e.to_string()),
});
}
};
// 生成JWT token
let jwt_secret = match std::env::var("JWT_SECRET") {
Ok(secret) => secret,
Err(_) => {
error!("环境变量中未配置JWT_SECRET");
return HttpResponse::InternalServerError().json(ErrorResponse {
error: "服务器配置错误缺少JWT_SECRET".to_string(),
errcode: Some(500),
errmsg: None,
});
}
};
// 生成token
let token = match generate_token(user_id, &openid, 2, &jwt_secret) {
Ok(t) => {
// 日志中只显示token的前8位保护安全
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()),
});
}
};
info!("用户登录成功用户ID: {}", user_id);
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>,
) -> impl Responder {
info!("Received weather data, preparing to insert into DB...");
// 调用 db.rs 中的函数来处理数据库逻辑
match insert_weather_data(pool.get_ref(), &data).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);
// 根据错误信息返回统一格式的错误响应
let mut errcode = 500;
if error_msg.starts_with("未找到openid") {
errcode = 404;
}
HttpResponse::Ok().json(serde_json::json!({
"success": false,
"errcode": errcode,
"errmsg": error_msg
}))
}
}
}
// main.rs
#[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>, // 依赖原有JWT验证确保是合法用户
) -> impl Responder {
let resource_id = path.into_inner();
let openid: &String = &claims.openid;
// 校验用户是否有权限访问该资源防止生成其他用户的资源token
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,
});
}
}
// 获取JWT密钥
let jwt_secret = match std::env::var("JWT_SECRET") {
Ok(secret) => secret,
Err(_) => {
return HttpResponse::InternalServerError().json(ErrorResponse {
error: "服务器配置错误缺少JWT_SECRET".to_string(),
errcode: Some(500),
errmsg: None,
});
}
};
// 生成临时token有效期10分钟
let temp_token = match auth::generate_temp_token(openid, resource_id, &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!({
"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>,
) -> impl Responder {
let jwt_secret = match std::env::var("JWT_SECRET") {
Ok(secret) => secret,
Err(_) => {
return HttpResponse::InternalServerError().json(ErrorResponse {
error: "服务器配置错误缺少JWT_SECRET".to_string(),
errcode: Some(500),
errmsg: None,
});
}
};
// 标记是否使用临时token
let mut is_temp_token = false;
// 1. 优先检查临时tokenURL参数
let (openid, weather_id) =
if let Some(temp_token) = query.get("temp_token").and_then(|v| v.as_str()) {
is_temp_token = true; // 标记为临时token访问
// 验证临时token
let temp_claims = match auth::verify_temp_token(temp_token, &jwt_secret) {
Ok(c) => c,
Err(e) => {
return HttpResponse::Unauthorized().json(ErrorResponse {
error: format!("临时token无效: {}", e),
errcode: Some(401),
errmsg: None,
});
}
};
// 从临时token中获取资源ID
(temp_claims.openid, temp_claims.resource_id)
}
// 2. 若没有临时token使用原有JWT验证需要查询参数id
else if let Some(claims) = claims {
// 从查询参数获取id
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)
}
// 3. 两种验证方式都没有,返回未授权
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 {
// 临时token访问返回HTML页面数据交由前端afterbody.js渲染
// 读取模板文件
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,
});
}
};
// 将天气数据序列化为JSON字符串供前端JS使用
let weather_data_json = serde_json::to_string(&weather_data).unwrap_or_default();
// 生成数据注入脚本将数据挂载到window对象供afterbody.js访问
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 {
// JWT访问返回JSON数据
HttpResponse::Ok().json(serde_json::json!({
"success": true,
"data": weather_data
}))
}
}
// 修改main.rs中的get_weather_brief函数
#[get("/weather")]
async fn get_weather_brief(
query: web::Query<serde_json::Value>,
pool: web::Data<PgPool>,
claims: web::ReqData<Claims>,
) -> impl Responder {
// 同时处理数字和字符串类型的page参数
let page = match query.get("page") {
Some(v) => {
// 尝试直接解析为数字
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
}
}
None => 1, // 无参数时用默认值
};
// 同理优化limit参数解析
let limit = match query.get("limit") {
Some(v) => {
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
}
}
None => 10,
};
debug!("获取天气数据列表, 页码: {}, 每页条数: {}", page, limit);
match db::get_weather_list(pool.get_ref(), claims.user_id, page, limit).await {
Ok(response) => {
// 这里的response是WeatherListResponse类型
// 打印获取到的天气列表和总数
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
}))
}
}
}
// 在 main.rs 中添加以下函数,用于处理静态文件请求
async fn serve_static_files(path: web::Path<String>) -> impl Responder {
// 获取请求的文件路径(例如 "css/style.css"
let file_path = path.into_inner();
// 从嵌入的 STATIC_DIR 中查找文件
match STATIC_DIR.get_file(&file_path) {
Some(file) => {
// 根据文件扩展名设置 Content-Type
let content_type = match file_path.split('.').last() {
Some("css") => "text/css",
Some("js") => "application/javascript",
Some("html") => "text/html",
Some("ttf") | Some("woff") | Some("woff2") => "font/woff2", // 字体文件
_ => "application/octet-stream", // 默认类型
};
HttpResponse::Ok()
.content_type(content_type)
.body(file.contents())
}
None => {
// 文件不存在时返回 404
HttpResponse::NotFound().body("静态文件不存在")
}
}
}
// 创建服务器配置的函数
fn create_server_config(
pool: PgPool,
http_client: Client,
) -> 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))
// 公开接口(无需验证)
// 注册静态文件服务:处理 /static/* 路径的请求
.service(web::resource("/static/{tail:.*}").route(web::get().to(serve_static_files)))
.service(login)
.service(get_weather_details) // 新增
// 需要验证的接口(使用 from_fn 包装中间件)
.service(
web::scope("")
.wrap(from_fn(jwt_middleware)) // 关键修改:用 from_fn 包装
.service(post_weather_data)
.service(get_weather_brief) // 新增
.service(generate_temp_token_handler)
.service(delete_weather),
)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
if let Err(e) = dotenvy::dotenv() {
error!("警告:加载.env文件失败使用系统环境变量: {}", e);
}
// 打印当前 RUST_LOG 的值
let rust_log = std::env::var("RUST_LOG").unwrap_or_else(|_| "未设置".to_string());
error!("当前 RUST_LOG 级别:{}", rust_log);
env_logger::init();
info!("logger init successful");
// 初始化数据库连接池
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);
}
};
// 初始化HTTP客户端
let http_client = Client::new();
info!("Attempting to start server...");
// 尝试多个端口
let ports = vec![443, 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; // 记录绑定的端口
// 使用&ports创建引用迭代器而不是获取所有权
for port in &ports {
let addr = format!("0.0.0.0:{}", port);
info!("Trying to bind to {}", addr);
// 为每个服务器创建克隆的连接池和HTTP客户端
let pool_clone = pool.clone();
let http_client_clone = http_client.clone();
// 根据端口选择是否使用SSL
if *port == 443 || *port == 8443 {
// 对于443和8443端口使用HTTPS
// 为每个端口创建新的SSL构建器
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())
})
.bind_openssl(&addr, ssl_builder) // 直接传递构建器
{
Ok(s) => {
info!("Successfully bound to {} with HTTPS", addr);
// 将服务器运行Future转换为正确的类型
let server_future = s.run();
// 使用Box::pin将Pin<Box<Server>>转换为Pin<Box<dyn Future<Output = Result<(), std::io::Error>> + Unpin>>
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 {
// 对于其他端口使用HTTP
match HttpServer::new(move || {
create_server_config(pool_clone.clone(), http_client_clone.clone())
})
.bind(&addr)
{
Ok(s) => {
info!("Successfully bound to {} with HTTP", addr);
// 将服务器运行Future转换为正确的类型
let server_future = s.run();
// 使用Box::pin将Pin<Box<Server>>转换为Pin<Box<dyn Future<Output = Result<(), std::io::Error>> + Unpin>>
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) => {
// 使用记录的端口判断是否为HTTPS
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);
}
}
}