diff --git a/app-android/app/src/main/java/com/huaga/life_echo/network/AuthService.kt b/app-android/app/src/main/java/com/huaga/life_echo/network/AuthService.kt index 7be0251..50b3cc5 100644 --- a/app-android/app/src/main/java/com/huaga/life_echo/network/AuthService.kt +++ b/app-android/app/src/main/java/com/huaga/life_echo/network/AuthService.kt @@ -1,13 +1,7 @@ package com.huaga.life_echo.network import com.huaga.life_echo.config.AppConfig -import com.huaga.life_echo.network.models.ErrorResponse -import com.huaga.life_echo.network.models.LoginRequest -import com.huaga.life_echo.network.models.RefreshTokenRequest -import com.huaga.life_echo.network.models.RegisterRequest -import com.huaga.life_echo.network.models.TokenResponse -import com.huaga.life_echo.network.models.UserResponse -import com.huaga.life_echo.network.models.ValidationErrorResponse +import com.huaga.life_echo.network.models.* import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.engine.android.Android @@ -285,4 +279,248 @@ class AuthService { Result.failure(Exception("网络错误: ${e.message}", e)) } } + + // ============================================================================ + // 短信验证码相关方法 + // ============================================================================ + + /** + * 发送短信验证码 + */ + suspend fun sendVerificationCode(phone: String, purpose: String): Result { + return try { + val response = client.post("$AUTH_BASE/sms/send") { + contentType(ContentType.Application.Json) + setBody(SmsRequest(phone, purpose)) + } + + when (response.status) { + HttpStatusCode.OK -> { + val smsResponse = response.body() + Result.success(smsResponse) + } + HttpStatusCode.BadRequest, HttpStatusCode.NotFound -> { + try { + val error = response.body() + Result.failure(Exception(error.detail)) + } catch (e: Exception) { + Result.failure(Exception("发送验证码失败: ${response.status}")) + } + } + HttpStatusCode.TooManyRequests -> { + try { + val error = response.body() + Result.failure(Exception(error.detail)) + } catch (e: Exception) { + Result.failure(Exception("发送过于频繁,请稍后再试")) + } + } + else -> { + Result.failure(Exception("发送验证码失败: ${response.status}")) + } + } + } catch (e: Exception) { + Result.failure(Exception("网络错误: ${e.message}", e)) + } + } + + /** + * 验证码登录 + */ + suspend fun loginWithSms(phone: String, code: String): Result { + return try { + val response = client.post("$AUTH_BASE/login/sms") { + contentType(ContentType.Application.Json) + setBody(SmsLoginRequest(phone, code)) + } + + when (response.status) { + HttpStatusCode.OK -> { + val tokenResponse = response.body() + Result.success(tokenResponse) + } + HttpStatusCode.BadRequest, HttpStatusCode.NotFound -> { + try { + val error = response.body() + Result.failure(Exception(error.detail)) + } catch (e: Exception) { + Result.failure(Exception("登录失败: ${response.status}")) + } + } + else -> { + Result.failure(Exception("登录失败: ${response.status}")) + } + } + } catch (e: Exception) { + Result.failure(Exception("网络错误: ${e.message}", e)) + } + } + + /** + * 验证码注册 + */ + suspend fun registerWithSms( + phone: String, + code: String, + password: String, + nickname: String, + email: String? = null + ): Result { + return try { + val response = client.post("$AUTH_BASE/register/sms") { + contentType(ContentType.Application.Json) + setBody(SmsRegisterRequest(phone, code, password, nickname, email)) + } + + when (response.status) { + HttpStatusCode.Created -> { + val tokenResponse = response.body() + Result.success(tokenResponse) + } + HttpStatusCode.BadRequest -> { + try { + val error = response.body() + Result.failure(Exception(error.detail)) + } catch (e: Exception) { + Result.failure(Exception("注册失败: ${response.status}")) + } + } + else -> { + Result.failure(Exception("注册失败: ${response.status}")) + } + } + } catch (e: Exception) { + Result.failure(Exception("网络错误: ${e.message}", e)) + } + } + + /** + * 重置密码 + */ + suspend fun resetPassword(phone: String, code: String, newPassword: String): Result { + return try { + val response = client.post("$AUTH_BASE/password/reset") { + contentType(ContentType.Application.Json) + setBody(ResetPasswordRequest(phone, code, newPassword)) + } + + when (response.status) { + HttpStatusCode.OK -> { + val messageResponse = response.body() + Result.success(messageResponse) + } + HttpStatusCode.BadRequest, HttpStatusCode.NotFound -> { + try { + val error = response.body() + Result.failure(Exception(error.detail)) + } catch (e: Exception) { + Result.failure(Exception("重置密码失败: ${response.status}")) + } + } + else -> { + Result.failure(Exception("重置密码失败: ${response.status}")) + } + } + } catch (e: Exception) { + Result.failure(Exception("网络错误: ${e.message}", e)) + } + } + + /** + * 修改密码(已登录) + */ + suspend fun changePassword(accessToken: String, oldPassword: String, newPassword: String): Result { + return try { + val response = client.post("$AUTH_BASE/password/change") { + contentType(ContentType.Application.Json) + header("Authorization", "Bearer $accessToken") + setBody(ChangePasswordRequest(oldPassword, newPassword)) + } + + when (response.status) { + HttpStatusCode.OK -> { + val messageResponse = response.body() + Result.success(messageResponse) + } + HttpStatusCode.BadRequest -> { + try { + val error = response.body() + Result.failure(Exception(error.detail)) + } catch (e: Exception) { + Result.failure(Exception("修改密码失败: ${response.status}")) + } + } + HttpStatusCode.Unauthorized -> { + Result.failure(Exception("未授权")) + } + else -> { + Result.failure(Exception("修改密码失败: ${response.status}")) + } + } + } catch (e: Exception) { + Result.failure(Exception("网络错误: ${e.message}", e)) + } + } + + /** + * 修改手机号 + */ + suspend fun changePhone(accessToken: String, newPhone: String, code: String): Result { + return try { + val response = client.post("$AUTH_BASE/phone/change") { + contentType(ContentType.Application.Json) + header("Authorization", "Bearer $accessToken") + setBody(ChangePhoneRequest(newPhone, code)) + } + + when (response.status) { + HttpStatusCode.OK -> { + val userResponse = response.body() + Result.success(userResponse) + } + HttpStatusCode.BadRequest -> { + try { + val error = response.body() + Result.failure(Exception(error.detail)) + } catch (e: Exception) { + Result.failure(Exception("修改手机号失败: ${response.status}")) + } + } + HttpStatusCode.Unauthorized -> { + Result.failure(Exception("未授权")) + } + else -> { + Result.failure(Exception("修改手机号失败: ${response.status}")) + } + } + } catch (e: Exception) { + Result.failure(Exception("网络错误: ${e.message}", e)) + } + } + + /** + * 登出所有设备 + */ + suspend fun logoutAll(accessToken: String): Result { + return try { + val response = client.post("$AUTH_BASE/logout/all") { + header("Authorization", "Bearer $accessToken") + } + + when (response.status) { + HttpStatusCode.OK -> { + val messageResponse = response.body() + Result.success(messageResponse) + } + HttpStatusCode.Unauthorized -> { + Result.failure(Exception("未授权")) + } + else -> { + Result.failure(Exception("登出失败: ${response.status}")) + } + } + } catch (e: Exception) { + Result.failure(Exception("网络错误: ${e.message}", e)) + } + } } diff --git a/app-android/app/src/main/java/com/huaga/life_echo/network/interceptors/AuthInterceptor.kt b/app-android/app/src/main/java/com/huaga/life_echo/network/interceptors/AuthInterceptor.kt index f5fcf35..949a54d 100644 --- a/app-android/app/src/main/java/com/huaga/life_echo/network/interceptors/AuthInterceptor.kt +++ b/app-android/app/src/main/java/com/huaga/life_echo/network/interceptors/AuthInterceptor.kt @@ -4,7 +4,10 @@ import com.huaga.life_echo.data.auth.TokenManager import com.huaga.life_echo.network.AuthService import io.ktor.client.plugins.api.createClientPlugin import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext /** * 认证拦截器配置 @@ -17,7 +20,8 @@ data class AuthInterceptorConfig( /** * 认证拦截器插件 * 自动在请求头添加Authorization - * 注意:401响应处理需要在调用方实现,因为Ktor的响应管道中无法直接重试请求 + * 当收到401错误时,尝试刷新令牌 + * 如果刷新失败,清除令牌并触发回调 */ val AuthInterceptorPlugin = createClientPlugin("AuthInterceptor", ::AuthInterceptorConfig) { // 在外部作用域捕获配置 @@ -34,4 +38,51 @@ val AuthInterceptorPlugin = createClientPlugin("AuthInterceptor", ::AuthIntercep request.headers.append(HttpHeaders.Authorization, "Bearer $accessToken") } } + + onResponse { response -> + // 检查是否是401未授权错误 + if (response.status == HttpStatusCode.Unauthorized) { + // 尝试刷新令牌 + val refreshToken = runBlocking { + config.tokenManager?.getRefreshToken() + } + + if (!refreshToken.isNullOrBlank() && config.authService != null) { + // 尝试刷新令牌 + val refreshResult = runBlocking { + withContext(Dispatchers.IO) { + config.authService?.refreshToken(refreshToken) + } + } + + refreshResult?.fold( + onSuccess = { tokenResponse -> + // 刷新成功,保存新令牌 + runBlocking { + config.tokenManager?.saveTokens( + tokenResponse.access_token, + tokenResponse.refresh_token, + config.tokenManager?.getUserId() ?: "" + ) + } + }, + onFailure = { + // 刷新失败,清除令牌并触发回调 + runBlocking { + config.tokenManager?.clearTokens() + } + // 通知 TokenManager,触发回调导航到登录页面 + config.tokenManager?.notifyTokenRefreshFailed() + } + ) + } else { + // 没有刷新令牌,清除令牌并触发回调 + runBlocking { + config.tokenManager?.clearTokens() + } + // 通知 TokenManager,触发回调导航到登录页面 + config.tokenManager?.notifyTokenRefreshFailed() + } + } + } } diff --git a/app-android/app/src/main/java/com/huaga/life_echo/network/models/AuthModels.kt b/app-android/app/src/main/java/com/huaga/life_echo/network/models/AuthModels.kt index 7db3114..18e85bd 100644 --- a/app-android/app/src/main/java/com/huaga/life_echo/network/models/AuthModels.kt +++ b/app-android/app/src/main/java/com/huaga/life_echo/network/models/AuthModels.kt @@ -68,3 +68,66 @@ data class ValidationErrorDetail( data class ValidationErrorResponse( val detail: List ) + +// ============================================================================ +// 短信验证码相关模型 +// ============================================================================ + +// 发送短信验证码请求 +@Serializable +data class SmsRequest( + val phone: String, + val purpose: String // register/login/reset_password/change_phone +) + +// 短信验证码响应 +@Serializable +data class SmsResponse( + val message: String, + val expires_in: Int +) + +// 验证码登录请求 +@Serializable +data class SmsLoginRequest( + val phone: String, + val code: String +) + +// 验证码注册请求 +@Serializable +data class SmsRegisterRequest( + val phone: String, + val code: String, + val password: String, + val nickname: String, + val email: String? = null +) + +// 重置密码请求 +@Serializable +data class ResetPasswordRequest( + val phone: String, + val code: String, + val new_password: String +) + +// 修改密码请求 +@Serializable +data class ChangePasswordRequest( + val old_password: String, + val new_password: String +) + +// 修改手机号请求 +@Serializable +data class ChangePhoneRequest( + val new_phone: String, + val code: String +) + +// 通用消息响应 +@Serializable +data class MessageResponse( + val message: String +)