feat: 添加用户信息存储功能

- 添加 avatar_url 和 nickname 字段到 users 表
- 修改 get_user_by_id 查询包含新字段
- 添加 save_user_profile API
- 优化 deploy.sh 默认使用 release 模式
This commit is contained in:
2026-03-26 14:28:46 +08:00
parent fcf358f990
commit 6d2824589c
6 changed files with 162 additions and 276 deletions

View File

@@ -4,6 +4,7 @@ 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;
@@ -20,6 +21,26 @@ use crate::models::{Claims, TokenResponse, UpdatePaymentRequest};
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())?;
@@ -42,46 +63,14 @@ async fn login(
pool: web::Data<PgPool>,
req: web::Json<WeChatLoginRequest>,
http_client: web::Data<Client>,
app_state: web::Data<AppState>,
) -> 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
app_state.wechat_appid, app_state.wechat_secret, req.code
);
debug!("准备调用微信API: {}", url.replace(&secret, "***")); // 日志中隐藏secret
debug!("准备调用微信API: {}", url.replace(&app_state.wechat_secret, "***"));
// 调用微信API
let wechat_response = match http_client.get(&url).send().await {
Ok(response) => {
debug!("微信API请求成功状态码: {}", response.status());
@@ -97,7 +86,6 @@ async fn login(
}
};
// 解析微信API响应
let wechat_data: WeChatApiResponse = match wechat_response.json().await {
Ok(data) => {
debug!("微信API响应解析成功");
@@ -113,7 +101,6 @@ async fn login(
}
};
// 检查微信API响应中的错误
if let Some(errcode) = wechat_data.errcode {
warn!(
"微信登录验证失败 - 错误码: {}, 错误信息: {:?}",
@@ -126,10 +113,8 @@ async fn login(
});
}
// 获取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
@@ -144,25 +129,19 @@ async fn login(
}
};
// 【核心修改】使用标准的 UPSERT 语法,原子性地创建或获取用户
let upsert_query = r#"
INSERT INTO users (openid, name, type)
VALUES ($1, left($1, 8), 2)
ON CONFLICT (openid) DO UPDATE SET
-- 这里可以更新一些需要变更的字段,例如最后登录时间
-- 如果没有需要更新的,可以使用一个技巧来确保 RETURNING 总是有效
id = users.id
ON CONFLICT (openid) DO UPDATE SET id = users.id
RETURNING id
"#;
// 使用 fetch_one因为我们确定这个查询总能返回一个 id
let user_id = match sqlx::query_as::<_, (i32,)>(upsert_query)
.bind(&openid)
.fetch_one(pool.get_ref()) // <--- 注意这里是 fetch_one
.fetch_one(pool.get_ref())
.await
{
Ok((id,)) => {
// 无论是新用户还是老用户,都成功获取了 ID
info!("用户登录/注册成功用户ID: {}", id);
id
}
@@ -176,23 +155,8 @@ async fn login(
}
};
// 生成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) {
let token = match generate_token(user_id, &openid, 2, &app_state.jwt_secret) {
Ok(t) => {
// 日志中只显示token的前8位保护安全
debug!("JWT生成成功: {}", &t[0..8]);
t
}
@@ -245,17 +209,16 @@ async fn post_weather_data(
}
}
// 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验证确保是合法用户
claims: web::ReqData<Claims>,
app_state: web::Data<AppState>,
) -> 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 {
@@ -275,20 +238,7 @@ async fn generate_temp_token_handler(
}
}
// 获取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) {
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 {
@@ -306,32 +256,18 @@ async fn generate_temp_token_handler(
}))
}
// 获取天气数据详情
#[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 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) {
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 {
@@ -341,13 +277,8 @@ async fn get_weather_details(
});
}
};
// 从临时token中获取资源ID
(temp_claims.openid, temp_claims.resource_id)
}
// 2. 若没有临时token使用原有JWT验证需要查询参数id
else if let Some(claims) = claims {
// 从查询参数获取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 => {
@@ -358,11 +289,8 @@ async fn get_weather_details(
});
}
};
(claims.openid.clone(), weather_id)
}
// 3. 两种验证方式都没有,返回未授权
else {
} else {
return HttpResponse::Unauthorized().json(ErrorResponse {
error: "缺少token需提供JWT或临时token".to_string(),
errcode: Some(401),
@@ -370,7 +298,6 @@ async fn get_weather_details(
});
};
// 获取天气数据
let weather_data = match db::get_weather_details(pool.get_ref(), weather_id).await {
Ok(data) => {
if data.openid != openid {
@@ -396,10 +323,8 @@ async fn get_weather_details(
}));
}
};
// 根据访问方式返回不同格式
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 => {
@@ -411,33 +336,27 @@ async fn get_weather_details(
}
};
// 将天气数据序列化为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
@@ -445,42 +364,36 @@ async fn get_weather_details(
}
}
// 修改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() {
let raw = if let Some(num) = v.as_i64() {
num as i32
}
// 尝试将字符串解析为数字
else if let Some(s) = v.as_str() {
} else if let Some(s) = v.as_str() {
s.parse().unwrap_or(1)
}
// 解析失败用默认值
else {
} else {
1
}
};
raw.max(1)
}
None => 1, // 无参数时用默认值
None => 1,
};
// 同理优化limit参数解析
let limit = match query.get("limit") {
Some(v) => {
if let Some(num) = v.as_i64() {
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,
};
@@ -489,18 +402,16 @@ async fn get_weather_brief(
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, // 分页数据
"data": response.list,
"page": page,
"limit": limit,
"total": response.total // 总条数(关键修改)
"total": response.total
}))
}
Err(error_msg) => {
@@ -670,7 +581,7 @@ async fn serve_static_files(path: web::Path<String>) -> impl Responder {
match STATIC_DIR.get_file(&file_path) {
Some(file) => {
// 根据文件扩展名设置 Content-Type
let content_type = match file_path.split('.').last() {
let content_type = match file_path.split('.').next_back() {
Some("ico") => "image/x-icon",
Some("css") => "text/css",
Some("js") => "application/javascript",
@@ -708,7 +619,6 @@ async fn get_current_user_profile(
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());
@@ -717,6 +627,8 @@ async fn get_current_user_profile(
"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,
@@ -735,10 +647,43 @@ async fn get_current_user_profile(
}
}
// 创建服务器配置的函数
// 保存用户信息请求体
#[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,
@@ -751,20 +696,19 @@ fn create_server_config(
App::new()
.app_data(web::Data::new(pool))
.app_data(web::Data::new(http_client))
// 公开接口(无需验证)
// 注册静态文件服务:处理 /static/* 路径的请求
.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) // 新增
// 需要验证的接口(使用 from_fn 包装中间件)
.service(get_weather_details)
.service(
web::scope("")
.wrap(from_fn(jwt_middleware)) // 关键修改:用 from_fn 包装
.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),
)
@@ -773,17 +717,20 @@ fn create_server_config(
#[actix_web::main]
async fn main() -> std::io::Result<()> {
if let Err(e) = dotenvy::dotenv() {
error!("警告:加载.env文件失败使用系统环境变量: {}", e);
eprintln!("警告:加载.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 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) => {
@@ -793,31 +740,25 @@ async fn main() -> std::io::Result<()> {
}
};
// 初始化HTTP客户端
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; // 记录绑定的端口
let mut bound_port = 0;
// 使用&ports创建引用迭代器而不是获取所有权
for port in &ports {
let addr = format!("127.0.0.1:{}", port);
info!("Trying to bind to {}", addr);
// 为每个服务器创建克隆的连接池和HTTP客户端
let pool_clone = pool.clone();
let http_client_clone = http_client.clone();
let app_state_clone = app_state.clone();
// 根据端口选择是否使用SSL
if *port == 443 || *port == 8443 {
// 对于443和8443端口使用HTTPS
// 为每个端口创建新的SSL构建器
let ssl_builder = match create_ssl_acceptor() {
Ok(builder) => builder,
Err(e) => {
@@ -827,17 +768,15 @@ async fn main() -> std::io::Result<()> {
};
match HttpServer::new(move || {
create_server_config(pool_clone.clone(), http_client_clone.clone())
create_server_config(pool_clone.clone(), http_client_clone.clone(), app_state_clone.clone())
})
.bind_openssl(&addr, ssl_builder) // 直接传递构建器
.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; // 记录绑定的端口
bound_port = *port;
break;
}
Err(e) => {
@@ -849,19 +788,16 @@ async fn main() -> std::io::Result<()> {
}
}
} else {
// 对于其他端口使用HTTP
match HttpServer::new(move || {
create_server_config(pool_clone.clone(), http_client_clone.clone())
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);
// 将服务器运行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; // 记录绑定的端口
bound_port = *port;
break;
}
Err(e) => {
@@ -879,7 +815,6 @@ async fn main() -> std::io::Result<()> {
match server {
Some(s) => {
// 使用记录的端口判断是否为HTTPS
if bound_port == 443 || bound_port == 8443 {
info!("Server started successfully with HTTPS");
} else {