可以正常使用get_weather_details获取前端指定的数据了
This commit is contained in:
20
Cargo.lock
generated
20
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
41
src/auth.rs
41
src/auth.rs
@@ -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)
|
||||
}
|
||||
|
||||
141
src/main.rs
141
src/main.rs
@@ -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. 优先检查临时token(URL参数)
|
||||
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),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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('获取详细信息失败');
|
||||
});
|
||||
});
|
||||
|
||||
// 工具函数
|
||||
|
||||
Reference in New Issue
Block a user