feat: 扩展前端认证服务,支持短信验证码

- 扩展AuthService添加短信验证码接口
- 更新AuthInterceptor认证拦截器
- 更新AuthModels数据模型
This commit is contained in:
iammm0
2026-01-27 11:36:04 +08:00
parent 101783cdfd
commit 61b7c25440
3 changed files with 360 additions and 8 deletions

View File

@@ -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<SmsResponse> {
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<SmsResponse>()
Result.success(smsResponse)
}
HttpStatusCode.BadRequest, HttpStatusCode.NotFound -> {
try {
val error = response.body<ErrorResponse>()
Result.failure(Exception(error.detail))
} catch (e: Exception) {
Result.failure(Exception("发送验证码失败: ${response.status}"))
}
}
HttpStatusCode.TooManyRequests -> {
try {
val error = response.body<ErrorResponse>()
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<TokenResponse> {
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<TokenResponse>()
Result.success(tokenResponse)
}
HttpStatusCode.BadRequest, HttpStatusCode.NotFound -> {
try {
val error = response.body<ErrorResponse>()
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<TokenResponse> {
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<TokenResponse>()
Result.success(tokenResponse)
}
HttpStatusCode.BadRequest -> {
try {
val error = response.body<ErrorResponse>()
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<MessageResponse> {
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<MessageResponse>()
Result.success(messageResponse)
}
HttpStatusCode.BadRequest, HttpStatusCode.NotFound -> {
try {
val error = response.body<ErrorResponse>()
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<MessageResponse> {
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<MessageResponse>()
Result.success(messageResponse)
}
HttpStatusCode.BadRequest -> {
try {
val error = response.body<ErrorResponse>()
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<UserResponse> {
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<UserResponse>()
Result.success(userResponse)
}
HttpStatusCode.BadRequest -> {
try {
val error = response.body<ErrorResponse>()
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<MessageResponse> {
return try {
val response = client.post("$AUTH_BASE/logout/all") {
header("Authorization", "Bearer $accessToken")
}
when (response.status) {
HttpStatusCode.OK -> {
val messageResponse = response.body<MessageResponse>()
Result.success(messageResponse)
}
HttpStatusCode.Unauthorized -> {
Result.failure(Exception("未授权"))
}
else -> {
Result.failure(Exception("登出失败: ${response.status}"))
}
}
} catch (e: Exception) {
Result.failure(Exception("网络错误: ${e.message}", e))
}
}
}

View File

@@ -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()
}
}
}
}

View File

@@ -68,3 +68,66 @@ data class ValidationErrorDetail(
data class ValidationErrorResponse(
val detail: List<ValidationErrorDetail>
)
// ============================================================================
// 短信验证码相关模型
// ============================================================================
// 发送短信验证码请求
@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
)