可以正常使用get_weather_details获取前端指定的数据了

This commit is contained in:
2025-09-29 11:20:21 +08:00
parent 8cc75be7cd
commit 56844a537f
6 changed files with 212 additions and 146 deletions

20
Cargo.lock generated
View File

@@ -1181,6 +1181,25 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2"
[[package]]
name = "include_dir"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd"
dependencies = [
"include_dir_macros",
]
[[package]]
name = "include_dir_macros"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75"
dependencies = [
"proc-macro2",
"quote",
]
[[package]]
name = "indexmap"
version = "2.11.0"
@@ -1860,6 +1879,7 @@ dependencies = [
"chrono",
"dotenvy",
"error",
"include_dir",
"jsonwebtoken",
"reqwest",
"serde",

View File

@@ -8,6 +8,7 @@ actix-web = "4.11.0"
chrono = {version = "0.4.41", features=["serde"]}
dotenvy = "0.15.7"
error = "0.1.9"
include_dir = "0.7.4"
jsonwebtoken = "9.3.1"
reqwest = { version = "0.12.23", features=["json"]}
serde = { version = "1.0.219", features = ["derive"] }

View File

@@ -1,7 +1,7 @@
use chrono::Utc;
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode};
use crate::models::Claims;
use crate::models::{Claims, TempTokenClaims};
use actix_web::{
Error, HttpMessage, body::MessageBody, dev::ServiceRequest, dev::ServiceResponse,
middleware::Next,
@@ -79,3 +79,42 @@ pub fn verify_token(token: &str, secret: &str) -> Result<Claims, String> {
Ok(decoded.claims)
}
// 生成临时token包含openid和resource_id短期有效
pub fn generate_temp_token(
openid: &str,
resource_id: i32,
secret: &str,
expire_minutes: i64, // 有效期(分钟)
) -> Result<String, String> {
let now = Utc::now();
let exp = (now + chrono::Duration::minutes(expire_minutes)).timestamp();
let iat = now.timestamp();
let claims = TempTokenClaims {
exp,
iat,
openid: openid.to_string(),
resource_id,
};
encode(
&Header::new(Algorithm::HS256),
&claims,
&EncodingKey::from_secret(secret.as_bytes()),
)
.map_err(|e| format!("生成临时token失败: {}", e))
}
// 验证临时token并返回载荷
pub fn verify_temp_token(token: &str, secret: &str) -> Result<TempTokenClaims, String> {
let validation = Validation::new(Algorithm::HS256);
let decoded = decode::<TempTokenClaims>(
token,
&DecodingKey::from_secret(secret.as_bytes()),
&validation,
)
.map_err(|e| format!("临时token验证失败: {}", e))?;
Ok(decoded.claims)
}

View File

@@ -1,5 +1,6 @@
use actix_web::middleware::from_fn;
use actix_web::{App, HttpResponse, HttpServer, Responder, delete, get, post, web};
use include_dir::{Dir, include_dir};
use reqwest::Client;
use sqlx::postgres::PgPool;
mod auth;
@@ -12,6 +13,10 @@ use models::{ErrorResponse, WeChatApiResponse, WeChatLoginRequest, WeatherData};
use crate::models::Claims;
// 编译时嵌入整个 static 目录
static STATIC_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/static");
static TEMPLATES_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates");
// 登录的API端点
#[post("/api/login")]
async fn login(
@@ -200,29 +205,138 @@ async fn post_weather_data(
}
}
// main.rs中添加以下处理函数
// 获取天气数据详情
#[get("/weather/details/{id}")]
async fn get_weather_details(
// 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>,
claims: web::ReqData<Claims>, // 依赖原有JWT验证确保是合法用户
) -> impl Responder {
let weather_id = path.into_inner();
println!("获取天气数据详情, ID: {}", weather_id);
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,
}))
}
// 获取天气数据详情
// 修改获取天气数据详情接口移除路径中的id参数
#[get("/weather/details")]
async fn get_weather_details(
pool: web::Data<PgPool>,
claims: Option<web::ReqData<Claims>>, // 原有JWT的claims变为可选
query: web::Query<serde_json::Value>, // 用于获取URL参数temp_token和id
) -> 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,
});
}
};
// 1. 优先检查临时tokenURL参数
let (openid, weather_id) =
if let Some(temp_token) = query.get("temp_token").and_then(|v| v.as_str()) {
// 验证临时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,
});
};
// 后续逻辑校验openid对资源的权限复用原有逻辑
match db::get_weather_details(pool.get_ref(), weather_id).await {
Ok(weather_data) => {
// 检查数据是否属于当前用户
if weather_data.openid != claims.openid {
if weather_data.openid != openid {
return HttpResponse::Forbidden().json(ErrorResponse {
error: "无权限访问该数据".to_string(),
errcode: Some(403),
errmsg: None,
});
}
HttpResponse::Ok().json(serde_json::json!({
"success": true,
"data": weather_data
@@ -362,14 +476,15 @@ fn create_server_config(
.app_data(web::Data::new(http_client))
// 公开接口(无需验证)
.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_details) // 新增
.service(get_weather_brief) // 新增
.service(delete_weather), // 新增
.service(generate_temp_token_handler)
.service(delete_weather),
)
}

View File

@@ -19,6 +19,15 @@ pub struct Claims {
pub user_type: i32,
}
// 临时访问用 token
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct TempTokenClaims {
pub exp: i64, // 过期时间(时间戳)
pub iat: i64, // 签发时间
pub openid: String, // 用户身份标识
pub resource_id: i32, // 允许访问的资源ID如天气数据id
}
#[derive(Debug, Deserialize)]
pub struct WeChatLoginRequest {
pub code: String,

View File

@@ -1,7 +1,5 @@
const version = "0.1.9";
let weatherdata = null;
//公式相关
function toggleAccordion(header) {
@@ -54,7 +52,20 @@ document.getElementById('downloadBtn').addEventListener('click', function() {
// 隐藏按钮(而不是移除)
btn.style.display = 'none';
//
// 从 api 中取得 weatherdata
let weatherdata = null;
selectedId = 300;
fetch(`/weather/details?id=${selectedId}`)
.then(response => response.json())
.then(data => {
//更新全局变量
weatherdata = data;
})
.catch(error => {
console.error('Error:', error);
alert('获取详细信息失败');
});
const data = weatherdata;
const areaTypeMap = {
@@ -356,135 +367,6 @@ document.getElementById('downloadBtn').addEventListener('click', function() {
// 获取记录内容
document.getElementById('viewDetailsBtn').addEventListener('click', function() {
const selectedRadio = document.querySelector('input[name="selectedData"]:checked');
if (!selectedRadio) {
alert('请先选择一条数据');
return;
}
const selectedId = selectedRadio.value;
fetch(`/weather/details?id=${selectedId}`)
.then(response => response.json())
.then(data => {
//更新全局变量
weatherdata = data;
const detailsTable = document.getElementById('detailsTable');
detailsTable.innerHTML = '';
const row = document.createElement('div');
row.innerHTML = `
<div class="card">
<div class="card-header">
<h3>${data.title}</h3>
<h3>${data.inspectiontype}</h3>
<h3>${data.assignmentnumber}</h3>
</div>
<div class="card-body">
<div class="info-item">
<span class="info-label">时间:</span>
<span class="info-value">${formatDateToYYYYMMDD(data.date)} ${data.hour}${data.min}分</span>
</div>
<div class="info-item">
<span class="info-label">经纬度:</span>
<span class="info-value">位置: 经度 ${data.longitude}, 纬度 ${data.latitude}</span>
</div>
<div class="info-item">
<span class="info-label">地球公转角:</span>
<span class="info-value">${data.theta}</span>
</div>
<div class="info-item">
<span class="info-label">太阳倾角:</span>
<span class="info-value">${data.solardeclination}</span>
</div>
<div class="info-item">
<span class="info-label">太阳高度角:</span>
<span class="info-value">${data.sunaltitude}</span>
</div>
<div class="info-item">
<span class="info-label">云量指数:${data.cloudindex}</span>
<span class="info-value">低云 ${data.lowcloudiness}, 总云量 ${data.overallcloudiness}</span>
</div>
<div class="info-item">
<span class="info-label">太阳辐射等级:</span>
<span class="info-value">${data.solarradiationlevel}</span>
</div>
<div id="measured-data" style="display: none;">
<div class="info-item">
<span class="info-label">测量风速:</span>
<span class="info-value" id="measured-windspeed">${data.measuredwindspeed}</span>
</div>
<div class="info-item">
<span class="info-label">转换风速:</span>
<span class="info-value" id="converted-windspeed">${data.convertedwindspeed}</span>
</div>
</div>
<div id="calculated-data" style="display: none;">
<div class="info-item">
<span class="info-label">测量高度:</span>
<span class="info-value" id="measurement-height">${data.measurementheight}</span>
</div>
<div class="info-item">
<span class="info-label">区域类型:</span>
<span class="info-value" id="area-type">${data.areatype}</span>
</div>
<div class="info-item">
<span class="info-label">测点风速:</span>
<span class="info-value" id="point-windspeed">${data.pointwindspeed}</span>
</div>
</div>
<div class="info-item">
<span class="info-label">大气稳定度:</span>
<span class="info-value">${data.atmosphericstability}</span>
</div>
<div class="info-item">
<span class="info-label">适宜度:</span>
<span class="info-value">${data.suitabilitydegree}</span>
</div>
<div class="info-item">
<span class="info-label">风速:${data.windspeed}</span>
<span class="info-value">平均风速:${data.averagewindspeed}</span>
<span class="info-value">适宜度:${data.windspeedsuitability}</span>
</div>
<div class="info-item">
<span class="info-label">风向:${data.winddirection}</span>
<span class="info-value">平均风向 ${data.averagewinddirection} 风向标准差 ${data.winddirectionstandarddeviation}</span>
<span class="info-value">适宜度:${data.winddirectionsuitability}</span>
</div>
<div class="info-item">
<span class="info-label">整体适宜度:</span>
<span class="info-value rating-high">${data.overallsuitability}</span>
</div>
</div>
</div>
`;
detailsTable.appendChild(row);
// 根据条件显示相应的数据
if (data.hasmeasuredwindspeed==='true') {
document.getElementById('measured-data').style.display = 'block';
document.getElementById('calculated-data').style.display = 'none';
document.getElementById('measured-windspeed').textContent = data.measuredwindspeed;
document.getElementById('converted-windspeed').textContent = data.convertedwindspeed;
} else {
document.getElementById('measured-data').style.display = 'none';
document.getElementById('calculated-data').style.display = 'block';
document.getElementById('measurement-height').textContent = data.measurementheight;
document.getElementById('area-type').textContent = data.areatype;
document.getElementById('point-windspeed').textContent = data.pointwindspeed;
};
document.getElementById('detailsContainer').style.display = 'block';
})
.catch(error => {
console.error('Error:', error);
alert('获取详细信息失败');
});
});
// 工具函数