feat(auth): 添加 Refresh Token 双 Token 机制
- 添加 /api/refresh-token 接口支持 Token 续期 - 登录接口返回 access_token 和 refresh_token - 新增 refresh_tokens 表存储 refresh_token - 部署脚本添加数据库备份和迁移功能 - deploy.sh 添加 4 项 API 测试 - 更新 AGENTS.md 文档
This commit is contained in:
70
src/auth.rs
70
src/auth.rs
@@ -1,52 +1,54 @@
|
||||
use chrono::Utc;
|
||||
use chrono::{Utc, Duration};
|
||||
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode};
|
||||
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
|
||||
|
||||
use crate::error::AppError;
|
||||
use crate::models::{Claims, TempTokenClaims};
|
||||
use actix_web::{
|
||||
Error, HttpMessage, body::MessageBody, dev::ServiceRequest, dev::ServiceResponse,
|
||||
middleware::Next,
|
||||
}; // 新增:用于包装中间件函数
|
||||
};
|
||||
|
||||
// 常量
|
||||
const ACCESS_TOKEN_EXPIRE_HOURS: i64 = 24;
|
||||
const REFRESH_TOKEN_EXPIRE_DAYS: i64 = 7;
|
||||
|
||||
// 中间件函数:泛型 B 约束为 MessageBody,返回 Result<ServiceResponse<B>, Error>
|
||||
pub async fn jwt_middleware<B: MessageBody>(
|
||||
req: ServiceRequest,
|
||||
next: Next<B>,
|
||||
) -> Result<ServiceResponse<B>, Error> {
|
||||
// (保持原有逻辑不变)
|
||||
let auth_header = req
|
||||
.headers()
|
||||
.get("Authorization")
|
||||
.ok_or_else(|| actix_web::error::ErrorUnauthorized("缺少Authorization头"))?
|
||||
.ok_or_else(|| AppError::Unauthorized("请先登录".to_string()))?
|
||||
.to_str()
|
||||
.map_err(|_| actix_web::error::ErrorUnauthorized("Authorization格式无效"))?;
|
||||
.map_err(|_| AppError::Unauthorized("Authorization格式无效".to_string()))?;
|
||||
|
||||
let token = auth_header.strip_prefix("Bearer ").ok_or_else(|| {
|
||||
actix_web::error::ErrorUnauthorized("Authorization格式应为 Bearer <token>")
|
||||
AppError::Unauthorized("请使用有效的登录凭证".to_string())
|
||||
})?;
|
||||
|
||||
let jwt_secret = std::env::var("JWT_SECRET")
|
||||
.map_err(|_| actix_web::error::ErrorInternalServerError("服务器未配置JWT密钥"))?;
|
||||
.map_err(|_| AppError::Internal("服务器配置错误".to_string()))?;
|
||||
|
||||
let claims = verify_token(token, &jwt_secret)
|
||||
.map_err(|e| actix_web::error::ErrorUnauthorized(format!("无效的token: {}", e)))?;
|
||||
.map_err(|e| AppError::Unauthorized(format!("登录已过期,请重新登录: {}", e)))?;
|
||||
|
||||
req.extensions_mut().insert(claims);
|
||||
next.call(req).await
|
||||
}
|
||||
|
||||
// 生成JWT的函数
|
||||
// 生成 access_token
|
||||
pub fn generate_token(
|
||||
user_id: i32,
|
||||
openid: &str,
|
||||
user_type: i32,
|
||||
secret: &str,
|
||||
) -> Result<String, String> {
|
||||
// 设置过期时间:当前时间 + 24小时(86400秒)
|
||||
let now = Utc::now();
|
||||
let exp = (now + chrono::Duration::hours(24)).timestamp();
|
||||
let exp = (now + Duration::hours(ACCESS_TOKEN_EXPIRE_HOURS)).timestamp();
|
||||
let iat = now.timestamp();
|
||||
|
||||
// 构建Claims
|
||||
let claims = Claims {
|
||||
exp,
|
||||
iat,
|
||||
@@ -55,15 +57,51 @@ pub fn generate_token(
|
||||
user_type,
|
||||
};
|
||||
|
||||
// 生成token
|
||||
encode(
|
||||
&Header::new(Algorithm::HS256), // 使用HS256算法
|
||||
&Header::new(Algorithm::HS256),
|
||||
&claims,
|
||||
&EncodingKey::from_secret(secret.as_bytes()), // 签名密钥
|
||||
&EncodingKey::from_secret(secret.as_bytes()),
|
||||
)
|
||||
.map_err(|e| format!("生成JWT失败: {}", e))
|
||||
}
|
||||
|
||||
// 生成 refresh_token(简单 base64 编码的随机字符串)
|
||||
pub fn generate_refresh_token(user_id: i32, secret: &str) -> Result<String, String> {
|
||||
let now = Utc::now();
|
||||
let exp = (now + Duration::days(REFRESH_TOKEN_EXPIRE_DAYS)).timestamp();
|
||||
|
||||
let payload = format!("{}:{}:{}", user_id, exp, secret);
|
||||
let token = BASE64.encode(payload.as_bytes());
|
||||
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
// 解析 refresh_token,返回 (user_id, expires_at)
|
||||
pub fn verify_refresh_token(token: &str, secret: &str) -> Result<(i32, i64), String> {
|
||||
let decoded = BASE64.decode(token)
|
||||
.map_err(|e| format!("Refresh token 格式错误: {}", e))?;
|
||||
|
||||
let payload = String::from_utf8(decoded)
|
||||
.map_err(|_| "Refresh token 解析失败".to_string())?;
|
||||
|
||||
let parts: Vec<&str> = payload.split(':').collect();
|
||||
if parts.len() != 3 {
|
||||
return Err("Refresh token 结构错误".to_string());
|
||||
}
|
||||
|
||||
let user_id: i32 = parts[0].parse()
|
||||
.map_err(|_| "Refresh token user_id 解析失败".to_string())?;
|
||||
let exp: i64 = parts[1].parse()
|
||||
.map_err(|_| "Refresh token exp 解析失败".to_string())?;
|
||||
|
||||
let now = Utc::now().timestamp();
|
||||
if now > exp {
|
||||
return Err("Refresh token 已过期".to_string());
|
||||
}
|
||||
|
||||
Ok((user_id, exp))
|
||||
}
|
||||
|
||||
// 验证并解析JWT的函数
|
||||
pub fn verify_token(token: &str, secret: &str) -> Result<Claims, String> {
|
||||
// 验证配置(指定算法,默认会检查exp等字段)
|
||||
|
||||
88
src/db.rs
88
src/db.rs
@@ -1,4 +1,4 @@
|
||||
use sqlx::{PgPool, postgres::PgPoolOptions};
|
||||
use sqlx::{PgPool, postgres::PgPoolOptions, FromRow};
|
||||
use std::env;
|
||||
use std::error::Error;
|
||||
use chrono::Utc;
|
||||
@@ -476,3 +476,89 @@ pub async fn set_weather_favorite(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ===== Refresh Token DB 函数 =====
|
||||
|
||||
#[derive(Debug, FromRow)]
|
||||
pub struct RefreshToken {
|
||||
pub id: i32,
|
||||
pub user_id: i32,
|
||||
pub token: String,
|
||||
pub expires_at: chrono::DateTime<chrono::Utc>,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
pub async fn create_refresh_token(
|
||||
pool: &PgPool,
|
||||
user_id: i32,
|
||||
token: &str,
|
||||
expires_at: chrono::DateTime<chrono::Utc>,
|
||||
) -> Result<(), AppError> {
|
||||
let query = r#"
|
||||
INSERT INTO refresh_tokens (user_id, token, expires_at)
|
||||
VALUES ($1, $2, $3)
|
||||
"#;
|
||||
|
||||
sqlx::query(query)
|
||||
.bind(user_id)
|
||||
.bind(token)
|
||||
.bind(expires_at)
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(|e| AppError::Database(format!("创建 refresh_token 失败: {}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn verify_refresh_token(
|
||||
pool: &PgPool,
|
||||
token: &str,
|
||||
) -> Result<RefreshToken, AppError> {
|
||||
let query = r#"
|
||||
SELECT id, user_id, token, expires_at, created_at
|
||||
FROM refresh_tokens
|
||||
WHERE token = $1 AND expires_at > NOW()
|
||||
"#;
|
||||
|
||||
sqlx::query_as::<_, RefreshToken>(query)
|
||||
.bind(token)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
.map_err(|e| AppError::Database(format!("验证 refresh_token 失败: {}", e)))?
|
||||
.ok_or_else(|| AppError::Unauthorized("Refresh token 无效或已过期".to_string()))
|
||||
}
|
||||
|
||||
pub async fn delete_refresh_token(pool: &PgPool, user_id: i32) -> Result<(), AppError> {
|
||||
let query = r#"DELETE FROM refresh_tokens WHERE user_id = $1"#;
|
||||
|
||||
sqlx::query(query)
|
||||
.bind(user_id)
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(|e| AppError::Database(format!("删除 refresh_token 失败: {}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_specific_refresh_token(pool: &PgPool, token: &str) -> Result<(), AppError> {
|
||||
let query = r#"DELETE FROM refresh_tokens WHERE token = $1"#;
|
||||
|
||||
sqlx::query(query)
|
||||
.bind(token)
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(|e| AppError::Database(format!("删除 refresh_token 失败: {}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn cleanup_expired_refresh_tokens(pool: &PgPool) -> Result<u64, AppError> {
|
||||
let query = r#"DELETE FROM refresh_tokens WHERE expires_at <= NOW()"#;
|
||||
|
||||
let result = sqlx::query(query)
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(|e| AppError::Database(format!("清理过期 refresh_token 失败: {}", e)))?;
|
||||
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
use actix_web::{web, HttpResponse, Responder, post};
|
||||
use chrono::{Utc, Duration};
|
||||
use reqwest::Client;
|
||||
use sqlx::postgres::PgPool;
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
use crate::auth::generate_token;
|
||||
use crate::auth::{generate_token, generate_refresh_token, verify_refresh_token};
|
||||
use crate::db;
|
||||
use crate::error::ErrorResponse;
|
||||
use crate::models::{
|
||||
AppState, ErrorResponse, TokenResponse, WeChatApiResponse, WeChatLoginRequest,
|
||||
AppState, LoginResponse, RefreshTokenRequest, TokenRefreshResponse,
|
||||
WeChatApiResponse, WeChatLoginRequest,
|
||||
};
|
||||
|
||||
#[post("/api/login")]
|
||||
@@ -28,11 +32,7 @@ pub async fn login(
|
||||
}
|
||||
Err(e) => {
|
||||
error!("微信API请求失败: {}", e);
|
||||
return HttpResponse::InternalServerError().json(ErrorResponse {
|
||||
error: "请求微信服务失败".to_string(),
|
||||
errcode: None,
|
||||
errmsg: None,
|
||||
});
|
||||
return HttpResponse::InternalServerError().json(ErrorResponse::<()>::error("请求微信服务失败"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -43,11 +43,7 @@ pub async fn login(
|
||||
}
|
||||
Err(e) => {
|
||||
error!("微信API响应解析失败: {}", e);
|
||||
return HttpResponse::InternalServerError().json(ErrorResponse {
|
||||
error: "解析微信响应失败".to_string(),
|
||||
errcode: None,
|
||||
errmsg: None,
|
||||
});
|
||||
return HttpResponse::InternalServerError().json(ErrorResponse::<()>::error("解析微信响应失败"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -56,11 +52,11 @@ pub async fn login(
|
||||
"微信登录验证失败 - 错误码: {}, 错误信息: {:?}",
|
||||
errcode, wechat_data.errmsg
|
||||
);
|
||||
return HttpResponse::BadRequest().json(ErrorResponse {
|
||||
error: "微信登录失败".to_string(),
|
||||
errcode: Some(errcode),
|
||||
errmsg: wechat_data.errmsg,
|
||||
});
|
||||
let errmsg = wechat_data.errmsg.unwrap_or_default();
|
||||
return HttpResponse::BadRequest().json(ErrorResponse::<()>::error(format!(
|
||||
"微信登录失败: {} {}",
|
||||
errcode, errmsg
|
||||
)));
|
||||
}
|
||||
|
||||
let openid = match wechat_data.openid {
|
||||
@@ -71,11 +67,7 @@ pub async fn login(
|
||||
}
|
||||
None => {
|
||||
error!("微信API响应中未包含openid");
|
||||
return HttpResponse::InternalServerError().json(ErrorResponse {
|
||||
error: "微信API未返回openid".to_string(),
|
||||
errcode: None,
|
||||
errmsg: None,
|
||||
});
|
||||
return HttpResponse::InternalServerError().json(ErrorResponse::<()>::error("微信API未返回openid"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -97,11 +89,7 @@ pub async fn login(
|
||||
}
|
||||
Err(e) => {
|
||||
error!("用户信息处理失败: {}", e);
|
||||
return HttpResponse::InternalServerError().json(ErrorResponse {
|
||||
error: "用户信息处理失败".to_string(),
|
||||
errcode: Some(500),
|
||||
errmsg: None,
|
||||
});
|
||||
return HttpResponse::InternalServerError().json(ErrorResponse::<()>::error("用户信息处理失败"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -112,14 +100,93 @@ pub async fn login(
|
||||
}
|
||||
Err(e) => {
|
||||
error!("JWT生成失败: {}", e);
|
||||
return HttpResponse::InternalServerError().json(ErrorResponse {
|
||||
error: "生成身份令牌失败".to_string(),
|
||||
errcode: Some(500),
|
||||
errmsg: None,
|
||||
});
|
||||
return HttpResponse::InternalServerError().json(ErrorResponse::<()>::error("生成身份令牌失败"));
|
||||
}
|
||||
};
|
||||
|
||||
let token_response = TokenResponse::new(token);
|
||||
HttpResponse::Ok().json(token_response)
|
||||
let refresh_token_str = match generate_refresh_token(user_id, &app_state.jwt_secret) {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
error!("Refresh token 生成失败: {}", e);
|
||||
return HttpResponse::InternalServerError().json(ErrorResponse::<()>::error("生成刷新令牌失败"));
|
||||
}
|
||||
};
|
||||
|
||||
let refresh_expires_at = Utc::now() + Duration::days(7);
|
||||
if let Err(e) = db::create_refresh_token(pool.get_ref(), user_id, &refresh_token_str, refresh_expires_at).await {
|
||||
error!("保存 refresh_token 失败: {}", e);
|
||||
return HttpResponse::InternalServerError().json(ErrorResponse::<()>::error("保存刷新令牌失败"));
|
||||
}
|
||||
|
||||
let login_response = LoginResponse::new(token, refresh_token_str);
|
||||
HttpResponse::Ok().json(login_response)
|
||||
}
|
||||
|
||||
#[post("/api/refresh-token")]
|
||||
pub async fn refresh_token(
|
||||
pool: web::Data<PgPool>,
|
||||
req: web::Json<RefreshTokenRequest>,
|
||||
app_state: web::Data<AppState>,
|
||||
) -> impl Responder {
|
||||
let refresh_token = &req.refresh_token;
|
||||
|
||||
let refresh_token_record = match db::verify_refresh_token(pool.get_ref(), refresh_token).await {
|
||||
Ok(record) => record,
|
||||
Err(e) => {
|
||||
warn!("Refresh token 验证失败: {}", e);
|
||||
return HttpResponse::Unauthorized().json(ErrorResponse::<()>::error("Refresh token 无效或已过期"));
|
||||
}
|
||||
};
|
||||
|
||||
let user_id = refresh_token_record.user_id;
|
||||
|
||||
let user_query = r#"SELECT openid FROM users WHERE id = $1"#;
|
||||
let openid: String = match sqlx::query_as::<_, (String,)>(user_query)
|
||||
.bind(user_id)
|
||||
.fetch_optional(pool.get_ref())
|
||||
.await
|
||||
{
|
||||
Ok(Some((openid,))) => openid,
|
||||
Ok(None) => {
|
||||
error!("用户不存在: {}", user_id);
|
||||
return HttpResponse::Unauthorized().json(ErrorResponse::<()>::error("用户不存在"));
|
||||
}
|
||||
Err(e) => {
|
||||
error!("查询用户失败: {}", e);
|
||||
return HttpResponse::InternalServerError().json(ErrorResponse::<()>::error("查询用户信息失败"));
|
||||
}
|
||||
};
|
||||
|
||||
let new_access_token = match generate_token(user_id, &openid, 2, &app_state.jwt_secret) {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
error!("生成新 JWT 失败: {}", e);
|
||||
return HttpResponse::InternalServerError().json(ErrorResponse::<()>::error("生成令牌失败"));
|
||||
}
|
||||
};
|
||||
|
||||
let new_refresh_token = match generate_refresh_token(user_id, &app_state.jwt_secret) {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
error!("生成新 refresh_token 失败: {}", e);
|
||||
return HttpResponse::InternalServerError().json(ErrorResponse::<()>::error("生成刷新令牌失败"));
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = db::delete_specific_refresh_token(pool.get_ref(), refresh_token).await {
|
||||
warn!("删除旧 refresh_token 失败(继续): {}", e);
|
||||
}
|
||||
|
||||
let new_expires_at = Utc::now() + Duration::days(7);
|
||||
if let Err(e) = db::create_refresh_token(pool.get_ref(), user_id, &new_refresh_token, new_expires_at).await {
|
||||
error!("保存新 refresh_token 失败: {}", e);
|
||||
return HttpResponse::InternalServerError().json(ErrorResponse::<()>::error("保存刷新令牌失败"));
|
||||
}
|
||||
|
||||
let response = TokenRefreshResponse {
|
||||
success: true,
|
||||
token: new_access_token,
|
||||
refresh_token: new_refresh_token,
|
||||
};
|
||||
HttpResponse::Ok().json(response)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ pub mod admin;
|
||||
pub mod auth;
|
||||
pub mod favorites;
|
||||
pub mod health;
|
||||
pub mod meta;
|
||||
pub mod payment;
|
||||
pub mod static_files;
|
||||
pub mod user;
|
||||
@@ -17,8 +18,10 @@ pub static TEMPLATES_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates");
|
||||
pub use admin::admin_get_user;
|
||||
pub use admin::admin_update_user_payment;
|
||||
pub use auth::login;
|
||||
pub use auth::refresh_token;
|
||||
pub use favorites::{add_favorite, get_favorites, remove_favorite};
|
||||
pub use health::health_check;
|
||||
pub use meta::root;
|
||||
pub use static_files::serve_static_files;
|
||||
pub use user::get_current_user_profile;
|
||||
pub use user::save_user_profile;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use actix_web::middleware::from_fn;
|
||||
use actix_web::{App, HttpServer, web};
|
||||
use include_dir::{Dir, include_dir};
|
||||
use tracing::{error, info};
|
||||
use openssl::ssl::{SslAcceptor, SslAcceptorBuilder, SslFiletype, SslMethod};
|
||||
use reqwest::Client;
|
||||
@@ -21,8 +20,8 @@ use handlers::{
|
||||
admin_get_user, admin_update_user_payment, add_favorite, create_order,
|
||||
delete_weather, generate_temp_token_handler, get_current_user_profile,
|
||||
get_favorites, get_user_quota, get_weather_brief, get_weather_details,
|
||||
health_check, login, mock_confirm, post_weather_data, remove_favorite,
|
||||
save_user_profile, serve_static_files,
|
||||
health_check, login, mock_confirm, post_weather_data, refresh_token,
|
||||
remove_favorite, root, save_user_profile, serve_static_files,
|
||||
};
|
||||
use models::AppState;
|
||||
|
||||
@@ -60,10 +59,13 @@ fn create_server_config(
|
||||
.app_data(web::Data::new(pool))
|
||||
.app_data(web::Data::new(http_client))
|
||||
.app_data(web::Data::new(app_state))
|
||||
// 根路径(无需认证)
|
||||
.service(root) // #[get("/")] - 返回服务信息
|
||||
// 静态文件(无需认证)
|
||||
.service(web::resource("/static/{tail:.*}").route(web::get().to(serve_static_files)))
|
||||
// API 接口
|
||||
.service(login) // #[post("/api/login")]
|
||||
.service(refresh_token) // #[post("/api/refresh-token")](公开接口,无需认证)
|
||||
.service(get_weather_details) // #[get("/weather/details")](支持 JWT 或 temp_token,公开接口)
|
||||
.service(health_check) // #[get("/health")](公开接口,无需认证)
|
||||
// 受保护接口(JWT)
|
||||
@@ -110,6 +112,7 @@ async fn main() -> std::io::Result<()> {
|
||||
std::env::set_var("SSL_CERT_PATH", &app_config.ssl_cert_path);
|
||||
std::env::set_var("RUST_LOG", &app_config.rust_log);
|
||||
std::env::set_var("FREE_USER_DATA_LIMIT", app_config.free_user_data_limit.to_string());
|
||||
std::env::set_var("APP_VERSION", &app_config.app_version);
|
||||
}
|
||||
|
||||
// 初始化文件日志(JSON 格式,带轮转)
|
||||
|
||||
@@ -28,14 +28,28 @@ pub struct TempTokenClaims {
|
||||
pub resource_id: i32, // 允许访问的资源ID(如天气数据id)
|
||||
}
|
||||
|
||||
// 登录成功后的令牌响应结构体
|
||||
/// 仅包含操作状态和令牌信息
|
||||
// 登录成功后的令牌响应结构体(双 token)
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
pub struct LoginResponse {
|
||||
pub success: bool,
|
||||
pub token: String,
|
||||
pub refresh_token: String,
|
||||
}
|
||||
|
||||
impl LoginResponse {
|
||||
pub fn new(token: String, refresh_token: String) -> Self {
|
||||
Self {
|
||||
success: true,
|
||||
token,
|
||||
refresh_token,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 兼容旧的 TokenResponse
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
pub struct TokenResponse {
|
||||
/// 操作状态:true表示登录成功,false表示失败
|
||||
pub success: bool,
|
||||
|
||||
/// 登录成功后生成的JWT令牌,客户端后续请求需携带此令牌
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
@@ -48,6 +62,20 @@ impl TokenResponse {
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh Token 请求
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct RefreshTokenRequest {
|
||||
pub refresh_token: String,
|
||||
}
|
||||
|
||||
// Token 刷新响应
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct TokenRefreshResponse {
|
||||
pub success: bool,
|
||||
pub token: String,
|
||||
pub refresh_token: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct WeChatLoginRequest {
|
||||
pub code: String,
|
||||
@@ -60,13 +88,6 @@ pub struct WeChatApiResponse {
|
||||
pub errmsg: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ErrorResponse {
|
||||
pub error: String,
|
||||
pub errcode: Option<i32>,
|
||||
pub errmsg: Option<String>,
|
||||
}
|
||||
|
||||
// 定义新类型,包装 Vec<f64>(当前 crate 内的类型)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FloatVec(pub Vec<f64>);
|
||||
@@ -316,6 +337,7 @@ pub struct AppState {
|
||||
pub wechat_appid: String,
|
||||
pub wechat_secret: String,
|
||||
pub free_user_data_limit: i32,
|
||||
pub app_version: String,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
@@ -330,6 +352,8 @@ impl AppState {
|
||||
free_user_data_limit: std::env::var("FREE_USER_DATA_LIMIT")
|
||||
.map(|v| v.parse().unwrap_or(20))
|
||||
.unwrap_or(20),
|
||||
app_version: std::env::var("APP_VERSION")
|
||||
.unwrap_or_else(|_| "unknown".to_string()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user