use chrono::Utc; use sea_orm::{ ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set, }; use serde::Deserialize; 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; #[derive(Debug, Deserialize)] struct WechatSessionResp { openid: Option, #[allow(dead_code)] 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 { let session = fetch_session(&state.wechat_appid, &state.wechat_secret, code).await?; let openid = session .openid .ok_or_else(|| AuthError::Validation("微信登录失败:未获取到 openid".to_string()))?; 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 { // MVP 占位:开发阶段使用固定手机号,上线前替换为真实微信手机号解密 let phone = "13800000000"; 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.to_string())), 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(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) } } 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(); return Err(AuthError::Validation(format!( "微信登录失败 ({}): {}", errcode, msg ))); } } Ok(session) }