feat: 添加用户信息存储功能
- 添加 avatar_url 和 nickname 字段到 users 表 - 修改 get_user_by_id 查询包含新字段 - 添加 save_user_profile API - 优化 deploy.sh 默认使用 release 模式
This commit is contained in:
70
deploy.sh
70
deploy.sh
@@ -1,23 +1,19 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==============================================================================
|
||||
# 部署脚本 - Rust Backend (支持编译模式选择)
|
||||
# 部署脚本 - Rust Backend
|
||||
# ==============================================================================
|
||||
|
||||
# --- 1. 配置 ---
|
||||
# 项目名称
|
||||
PROJECT_NAME="rust-backend"
|
||||
# 远程服务器信息
|
||||
REMOTE_USER="root"
|
||||
REMOTE_HOST="1panel-server"
|
||||
REMOTE_DIR="/root/rust/rust_backend"
|
||||
# 远程服务名称 (用于 systemctl restart)
|
||||
SERVICE_NAME="rust-backend.service"
|
||||
|
||||
# --- 2. 脚本初始化 ---
|
||||
set -euo pipefail
|
||||
|
||||
# 定义日志函数
|
||||
log_info() {
|
||||
echo -e "\033[32m[INFO]\033[0m $1"
|
||||
}
|
||||
@@ -27,50 +23,7 @@ log_error() {
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 打印帮助信息
|
||||
usage() {
|
||||
echo "用法: $0 [--release]"
|
||||
echo ""
|
||||
echo "选项:"
|
||||
echo " --release 以 Release 模式编译并部署 (默认为 Debug 模式)"
|
||||
echo " -h, --help 显示此帮助信息"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# --- 3. 参数解析与动态配置 ---
|
||||
|
||||
# 默认为 Debug 模式
|
||||
IS_RELEASE=false
|
||||
BUILD_MODE="debug"
|
||||
CARGO_ARGS=""
|
||||
TARGET_BASE_DIR="/home/milky/Documents/ASD-backend/rust-backend/target/x86_64-unknown-linux-gnu/"
|
||||
|
||||
# 解析命令行参数
|
||||
if [[ "$#" -gt 0 ]]; then
|
||||
case "$1" in
|
||||
--release)
|
||||
IS_RELEASE=true
|
||||
BUILD_MODE="release"
|
||||
CARGO_ARGS="--release"
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
log_error "未知参数: '$1'。使用 '$0 --help' 查看用法。"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# 根据模式动态设置目标目录
|
||||
TARGET_DIR="${TARGET_BASE_DIR}${BUILD_MODE}/"
|
||||
|
||||
log_info "部署模式: ${BUILD_MODE}"
|
||||
log_info "本地目标目录: ${TARGET_DIR}"
|
||||
|
||||
# --- 4. 核心功能函数 ---
|
||||
|
||||
# 检查必要的命令是否存在
|
||||
# --- 3. 检查依赖 ---
|
||||
check_dependencies() {
|
||||
local deps=("cargo" "rsync" "ssh")
|
||||
for cmd in "${deps[@]}"; do
|
||||
@@ -81,20 +34,19 @@ check_dependencies() {
|
||||
log_info "所有依赖项检查通过。"
|
||||
}
|
||||
|
||||
# 编译项目
|
||||
# --- 4. 编译项目 ---
|
||||
build_project() {
|
||||
log_info "开始编译项目 (${BUILD_MODE}模式)..."
|
||||
# 使用动态的 CARGO_ARGS
|
||||
if cargo build ${CARGO_ARGS} --target x86_64-unknown-linux-gnu --quiet; then
|
||||
log_info "开始编译项目 (release模式)..."
|
||||
if cargo build --release; then
|
||||
log_info "项目编译成功。"
|
||||
else
|
||||
log_error "项目编译失败!"
|
||||
fi
|
||||
}
|
||||
|
||||
# 上传二进制文件
|
||||
# --- 5. 上传二进制文件 ---
|
||||
upload_binary() {
|
||||
local binary_path="${TARGET_DIR}${PROJECT_NAME}"
|
||||
local binary_path="./target/release/${PROJECT_NAME}"
|
||||
if [ ! -f "$binary_path" ]; then
|
||||
log_error "编译产物未找到: $binary_path"
|
||||
fi
|
||||
@@ -104,7 +56,7 @@ upload_binary() {
|
||||
log_info "文件上传成功。"
|
||||
}
|
||||
|
||||
# 重启远程服务
|
||||
# --- 6. 重启远程服务 ---
|
||||
restart_remote_service() {
|
||||
log_info "正在重启远程服务 '${SERVICE_NAME}'..."
|
||||
if ssh "${REMOTE_USER}@${REMOTE_HOST}" "systemctl restart ${SERVICE_NAME}"; then
|
||||
@@ -114,10 +66,9 @@ restart_remote_service() {
|
||||
fi
|
||||
}
|
||||
|
||||
# --- 5. 主执行流程 ---
|
||||
|
||||
# --- 7. 主执行流程 ---
|
||||
main() {
|
||||
log_info "========== 开始部署 (${BUILD_MODE}模式) =========="
|
||||
log_info "========== 开始部署 =========="
|
||||
check_dependencies
|
||||
build_project
|
||||
upload_binary
|
||||
@@ -125,5 +76,4 @@ main() {
|
||||
log_info "========== 部署完成! =========="
|
||||
}
|
||||
|
||||
# 执行主函数
|
||||
main
|
||||
|
||||
2
migrations/002_add_user_profile_fields.sql
Normal file
2
migrations/002_add_user_profile_fields.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS avatar_url VARCHAR DEFAULT NULL;
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS nickname VARCHAR DEFAULT NULL;
|
||||
1
migrations/003_set_all_users_paid.sql
Normal file
1
migrations/003_set_all_users_paid.sql
Normal file
@@ -0,0 +1 @@
|
||||
UPDATE users SET is_paid = true WHERE is_paid = false;
|
||||
72
src/db.rs
72
src/db.rs
@@ -57,7 +57,7 @@ pub async fn insert_weather_data(pool: &PgPool, weather_data: &WeatherData, user
|
||||
.bind(&weather_data.overall_cloudiness)
|
||||
.bind(&weather_data.low_cloudiness)
|
||||
.bind(&weather_data.cloud_index)
|
||||
.bind(weather_data.solar_radiation_level as i32)
|
||||
.bind(weather_data.solar_radiation_level)
|
||||
.bind(weather_data.has_measured_wind_speed)
|
||||
.bind(weather_data.measured_wind_speed) // <-- 修改: 直接绑定 Option<f64>
|
||||
.bind(weather_data.converted_wind_speed) // <-- 修改: 直接绑定 Option<i32>,并移除 `as i32`
|
||||
@@ -99,11 +99,6 @@ pub async fn insert_weather_data(pool: &PgPool, weather_data: &WeatherData, user
|
||||
}
|
||||
|
||||
pub async fn create_pool() -> Result<PgPool, Box<dyn Error>> {
|
||||
// 加载环境变量,非致命错误处理
|
||||
if let Err(e) = dotenvy::dotenv() {
|
||||
eprintln!("警告: 无法加载.env文件 - {}", e);
|
||||
}
|
||||
|
||||
// 获取数据库URL并提供友好错误信息
|
||||
let database_url =
|
||||
env::var("DATABASE_URL").map_err(|_| "环境变量DATABASE_URL未设置,请在.env文件中配置")?;
|
||||
@@ -201,55 +196,36 @@ pub async fn get_weather_list(
|
||||
Ok(WeatherListResponse { list, total })
|
||||
}
|
||||
|
||||
// 删除天气数据
|
||||
pub async fn delete_weather_data(
|
||||
pool: &PgPool,
|
||||
weather_id: i32,
|
||||
user_id: i32,
|
||||
) -> Result<(), String> {
|
||||
// 先检查数据是否存在且属于当前用户
|
||||
let query_check = r#"
|
||||
SELECT id FROM weather_data
|
||||
WHERE id = $1 AND user_id = $2
|
||||
"#;
|
||||
|
||||
let exists = match sqlx::query_as::<_, (i32,)>(query_check)
|
||||
.bind(weather_id)
|
||||
.bind(user_id)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
{
|
||||
Ok(Some(_)) => true,
|
||||
Ok(None) => return Err(format!("未找到ID为 {} 的天气数据或无权限删除", weather_id)),
|
||||
Err(e) => return Err(format!("检查天气数据失败: {}", e)),
|
||||
};
|
||||
|
||||
if !exists {
|
||||
return Err(format!("未找到ID为 {} 的天气数据或无权限删除", weather_id));
|
||||
}
|
||||
|
||||
// 执行删除操作
|
||||
let query_delete = r#"
|
||||
let query = r#"
|
||||
DELETE FROM weather_data
|
||||
WHERE id = $1 AND user_id = $2
|
||||
"#;
|
||||
|
||||
match sqlx::query(query_delete)
|
||||
let result = sqlx::query(query)
|
||||
.bind(weather_id)
|
||||
.bind(user_id)
|
||||
.execute(pool)
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(format!("删除天气数据失败: {}", e)),
|
||||
.map_err(|e| format!("删除天气数据失败: {}", e))?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err(format!("未找到ID为 {} 的天气数据或无权限删除", weather_id));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// 根据用户ID获取用户信息
|
||||
pub async fn get_user_by_id(pool: &PgPool, user_id: i32) -> Result<User, String> {
|
||||
let query = r#"
|
||||
SELECT
|
||||
id, name, openid, phone, type, "desc", is_paid, is_admin, paid_expires_at
|
||||
id, name, openid, phone, type, "desc", is_paid, is_admin, paid_expires_at,
|
||||
avatar_url, nickname
|
||||
FROM users
|
||||
WHERE id = $1
|
||||
"#;
|
||||
@@ -309,3 +285,29 @@ pub async fn update_user_payment_status(
|
||||
Err(e) => Err(format!("更新用户付费状态失败: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
// 更新用户个人信息
|
||||
pub async fn update_user_profile(
|
||||
pool: &PgPool,
|
||||
user_id: i32,
|
||||
nickname: &Option<String>,
|
||||
avatar_url: &Option<String>,
|
||||
) -> Result<(), String> {
|
||||
let query = r#"
|
||||
UPDATE users
|
||||
SET nickname = COALESCE($1, nickname),
|
||||
avatar_url = COALESCE($2, avatar_url)
|
||||
WHERE id = $3
|
||||
"#;
|
||||
|
||||
match sqlx::query(query)
|
||||
.bind(nickname)
|
||||
.bind(avatar_url)
|
||||
.bind(user_id)
|
||||
.execute(pool)
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(format!("更新用户个人信息失败: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
275
src/main.rs
275
src/main.rs
@@ -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. 优先检查临时token(URL参数)
|
||||
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 {
|
||||
|
||||
@@ -40,22 +40,12 @@ pub struct TokenResponse {
|
||||
}
|
||||
|
||||
impl TokenResponse {
|
||||
/// 快速创建登录成功的令牌响应
|
||||
/// - token: 生成的JWT令牌字符串
|
||||
pub fn new(token: String) -> Self {
|
||||
Self {
|
||||
success: true, // 登录成功场景固定为true
|
||||
success: true,
|
||||
token,
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建登录失败的响应(可选,用于统一错误响应格式)
|
||||
pub fn failed() -> Self {
|
||||
Self {
|
||||
success: false,
|
||||
token: String::new(), // 失败时令牌为空
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -294,6 +284,12 @@ pub struct User {
|
||||
pub is_admin: bool,
|
||||
#[sqlx(rename = "paid_expires_at")]
|
||||
pub paid_expires_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
#[serde(rename = "avatarUrl")]
|
||||
#[sqlx(rename = "avatar_url")]
|
||||
pub avatar_url: Option<String>,
|
||||
#[serde(rename = "nickname")]
|
||||
#[sqlx(rename = "nickname")]
|
||||
pub nickname: Option<String>,
|
||||
}
|
||||
|
||||
// 管理员更新用户付费状态的请求体
|
||||
|
||||
Reference in New Issue
Block a user