use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, KeyIvInit}; use base64::Engine; use chrono::Utc; use cbc::Decryptor; use sea_orm::{ ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set, }; use serde::Deserialize; use std::collections::HashMap; use std::sync::LazyLock; use std::time::{Duration, Instant}; use tokio::sync::Mutex; use uuid::Uuid; use crate::auth_state::AuthState; use crate::dto::{LoginResp, UserResp, WechatLoginResp}; use crate::entity::wechat_user; use crate::error::{AuthError, AuthResult}; use crate::service::auth_service::JwtConfig; use crate::service::token_service::TokenService; use erp_core::sanitize::sanitize_string; type Aes128CbcDec = Decryptor; /// session_key 缓存条目 struct SessionEntry { session_key: String, created_at: Instant, } /// 全局 session_key 缓存(openid → session_key) /// TTL: 5 分钟(微信 session_key 有效期约 5 分钟) static SESSION_CACHE: LazyLock>> = LazyLock::new(|| Mutex::new(HashMap::new())); const SESSION_TTL: Duration = Duration::from_secs(300); #[derive(Debug, Deserialize)] struct WechatSessionResp { openid: Option, session_key: Option, #[allow(dead_code)] unionid: Option, errcode: Option, errmsg: Option, } pub struct WechatService; impl WechatService { pub async fn login( state: &AuthState, tenant_id: Uuid, code: &str, ) -> AuthResult { tracing::info!( appid = %state.wechat_appid, code = %code, "fetch_session 开始" ); let session = fetch_session(&state.wechat_appid, &state.wechat_secret, code).await?; let openid = session .openid .clone() .ok_or_else(|| AuthError::Validation("微信登录失败:未获取到 openid".to_string()))?; // 缓存 session_key 供后续 bind_phone 使用 if let Some(sk) = session.session_key { let mut cache = SESSION_CACHE.lock().await; cache.insert( openid.clone(), SessionEntry { session_key: sk, created_at: Instant::now(), }, ); } let existing = wechat_user::Entity::find() .filter(wechat_user::Column::Openid.eq(&openid)) .filter(wechat_user::Column::TenantId.eq(tenant_id)) .filter(wechat_user::Column::DeletedAt.is_null()) .one(&state.db) .await .map_err(|e| AuthError::DbError(e.to_string()))?; if let Some(wu) = existing { let token = build_login_resp( &state.db, wu.user_id, tenant_id, &JwtConfig { secret: &state.jwt_secret, access_ttl_secs: state.access_ttl_secs, refresh_ttl_secs: state.refresh_ttl_secs, }, ) .await?; Ok(WechatLoginResp { bound: true, openid, token: Some(token), }) } else { Ok(WechatLoginResp { bound: false, openid, token: None, }) } } pub async fn bind_phone( state: &AuthState, tenant_id: Uuid, openid: &str, encrypted_data: &str, iv: &str, ) -> AuthResult { // 从缓存获取 session_key let session_key = { let mut cache = SESSION_CACHE.lock().await; if let Some(entry) = cache.get(openid) { if entry.created_at.elapsed() < SESSION_TTL { let sk = entry.session_key.clone(); cache.remove(openid); sk } else { cache.remove(openid); return Err(AuthError::Validation( "session_key 已过期,请重新登录".to_string(), )); } } else { return Err(AuthError::Validation( "未找到 session_key,请重新登录".to_string(), )); } }; let phone = decrypt_phone_number(&session_key, encrypted_data, iv)?; let existing = wechat_user::Entity::find() .filter(wechat_user::Column::Openid.eq(openid)) .filter(wechat_user::Column::TenantId.eq(tenant_id)) .filter(wechat_user::Column::DeletedAt.is_null()) .one(&state.db) .await .map_err(|e| AuthError::DbError(e.to_string()))?; if existing.is_some() { return Err(AuthError::Validation("该微信已绑定账号".to_string())); } let user_id = Self::find_or_create_user_by_phone(&state.db, tenant_id, &phone).await?; let now = Utc::now(); let wu = wechat_user::ActiveModel { id: Set(Uuid::now_v7()), tenant_id: Set(tenant_id), openid: Set(openid.to_string()), union_id: Set(None), user_id: Set(user_id), phone: Set(Some(phone.clone())), created_at: Set(now), updated_at: Set(now), created_by: Set(Some(user_id)), updated_by: Set(Some(user_id)), deleted_at: Set(None), version: Set(1), }; wu.insert(&state.db) .await .map_err(|e| AuthError::DbError(e.to_string()))?; build_login_resp( &state.db, user_id, tenant_id, &JwtConfig { secret: &state.jwt_secret, access_ttl_secs: state.access_ttl_secs, refresh_ttl_secs: state.refresh_ttl_secs, }, ) .await } async fn find_or_create_user_by_phone( db: &sea_orm::DatabaseConnection, tenant_id: Uuid, phone: &str, ) -> AuthResult { use crate::entity::user; let existing = user::Entity::find() .filter(user::Column::Phone.eq(phone)) .filter(user::Column::TenantId.eq(tenant_id)) .filter(user::Column::DeletedAt.is_null()) .one(db) .await .map_err(|e| AuthError::DbError(e.to_string()))?; if let Some(u) = existing { return Ok(u.id); } let now = Utc::now(); let user_id = Uuid::now_v7(); let suffix = &phone[phone.len().saturating_sub(4)..]; let new_user = user::ActiveModel { id: Set(user_id), tenant_id: Set(tenant_id), username: Set(format!("wx_{}", suffix)), display_name: Set(Some(sanitize_string(&format!("微信用户{}", suffix)))), phone: Set(Some(phone.to_string())), email: Set(None), avatar_url: Set(None), status: Set("active".to_string()), last_login_at: Set(None), created_at: Set(now), updated_at: Set(now), created_by: Set(user_id), updated_by: Set(user_id), deleted_at: Set(None), version: Set(1), }; new_user .insert(db) .await .map_err(|e| AuthError::DbError(e.to_string()))?; Ok(user_id) } } /// AES-128-CBC 解密微信手机号 fn decrypt_phone_number( session_key: &str, encrypted_data: &str, iv: &str, ) -> AuthResult { let engine = base64::engine::general_purpose::STANDARD; let key_bytes = engine .decode(session_key) .map_err(|e| AuthError::Validation(format!("session_key base64 解码失败: {}", e)))?; let iv_bytes = engine .decode(iv) .map_err(|e| AuthError::Validation(format!("iv base64 解码失败: {}", e)))?; let ciphertext = engine .decode(encrypted_data) .map_err(|e| AuthError::Validation(format!("encrypted_data base64 解码失败: {}", e)))?; if key_bytes.len() != 16 { return Err(AuthError::Validation( "session_key 长度不正确".to_string(), )); } if iv_bytes.len() != 16 { return Err(AuthError::Validation("iv 长度不正确".to_string())); } let decryptor = Aes128CbcDec::new_from_slices(&key_bytes, &iv_bytes) .map_err(|e| AuthError::Validation(format!("AES 初始化失败: {}", e)))?; let mut buf = ciphertext; let decrypted = decryptor .decrypt_padded_mut::(&mut buf) .map_err(|e| AuthError::Validation(format!("AES 解密失败: {}", e)))?; let plaintext = String::from_utf8(decrypted.to_vec()).map_err(|_| AuthError::Validation("解密结果非 UTF-8".to_string()))?; // 微信返回的 JSON 包含 watermark 等字段,提取 phone_number let info: serde_json::Value = serde_json::from_str(&plaintext) .map_err(|e| AuthError::Validation(format!("解密结果 JSON 解析失败: {}", e)))?; info.get("phoneNumber") .and_then(|v| v.as_str()) .map(|s| s.to_string()) .ok_or_else(|| AuthError::Validation("解密结果中无 phoneNumber".to_string())) } async fn build_login_resp( db: &sea_orm::DatabaseConnection, user_id: Uuid, tenant_id: Uuid, jwt: &JwtConfig<'_>, ) -> AuthResult { use crate::entity::user; use crate::service::auth_service::AuthService; let user_model = user::Entity::find_by_id(user_id) .one(db) .await .map_err(|e| AuthError::DbError(e.to_string()))? .ok_or_else(|| AuthError::Validation("用户不存在".to_string()))?; let roles = TokenService::get_user_roles(user_id, tenant_id, db).await?; let permissions = TokenService::get_user_permissions(user_id, tenant_id, db).await?; let access_token = TokenService::sign_access_token( user_id, tenant_id, roles.clone(), permissions, jwt.secret, jwt.access_ttl_secs, )?; let (refresh_token, _) = TokenService::sign_refresh_token( user_id, tenant_id, db, jwt.secret, jwt.refresh_ttl_secs, ) .await?; let role_resps = AuthService::get_user_role_resps(user_id, tenant_id, db).await?; Ok(LoginResp { access_token, refresh_token, expires_in: jwt.access_ttl_secs as u64, user: UserResp { id: user_model.id, username: user_model.username, email: user_model.email, phone: user_model.phone, display_name: user_model.display_name, avatar_url: user_model.avatar_url, status: user_model.status, roles: role_resps, version: user_model.version, }, }) } async fn fetch_session( appid: &str, secret: &str, code: &str, ) -> AuthResult { let client = reqwest::Client::new(); let resp = client .get("https://api.weixin.qq.com/sns/jscode2session") .query(&[ ("appid", appid), ("secret", secret), ("js_code", code), ("grant_type", "authorization_code"), ]) .send() .await .map_err(|e| AuthError::Validation(format!("微信 API 请求失败: {}", e)))?; let session: WechatSessionResp = resp .json() .await .map_err(|e| AuthError::Validation(format!("微信 API 响应解析失败: {}", e)))?; if let Some(errcode) = session.errcode { if errcode != 0 { let msg = session.errmsg.clone().unwrap_or_default(); tracing::error!(errcode, errmsg = %msg, "微信 jscode2session 返回错误"); return Err(AuthError::Validation(format!( "微信登录失败 ({}): {}", errcode, msg ))); } } tracing::info!( has_openid = session.openid.is_some(), has_session_key = session.session_key.is_some(), "微信 jscode2session 成功" ); Ok(session) }