feat(auth): 手机号验证码登录(会话模型 + 登录页 + 导航接入)
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
package com.mingyuan.android_client.core.data.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.preferencesDataStoreFile
|
||||
import com.mingyuan.android_client.core.data.repository.FakeCheckinRepository
|
||||
import com.mingyuan.android_client.core.data.repository.FakeHomeRepository
|
||||
import com.mingyuan.android_client.core.data.repository.FakePlanRepository
|
||||
@@ -12,7 +17,9 @@ import com.mingyuan.android_client.core.domain.repository.ProfileRepository
|
||||
import com.mingyuan.android_client.core.domain.repository.SessionRepository
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@@ -39,4 +46,11 @@ abstract class DataModule {
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun bindSessionRepository(impl: FakeSessionRepository): SessionRepository
|
||||
|
||||
companion object {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSessionDataStore(@ApplicationContext context: Context): DataStore<Preferences> =
|
||||
PreferenceDataStoreFactory.create { context.preferencesDataStoreFile("session") }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,71 @@
|
||||
package com.mingyuan.android_client.core.data.repository
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import com.mingyuan.android_client.core.domain.repository.LoginResult
|
||||
import com.mingyuan.android_client.core.domain.repository.SessionRepository
|
||||
import com.mingyuan.android_client.core.domain.repository.SmsCodeResult
|
||||
import com.mingyuan.android_client.core.model.Session
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Local-mock session implementation. It simulates the "phone + SMS code" login flow and persists
|
||||
* the token via DataStore so the login survives process restarts (mirroring the mini-program's
|
||||
* `wx.getStorageSync('token')`). Swap this for a network-backed implementation when the backend
|
||||
* endpoints are ready.
|
||||
*
|
||||
* Mock credentials: phone `13812341234`, code `123456`.
|
||||
*/
|
||||
@Singleton
|
||||
class FakeSessionRepository @Inject constructor() : SessionRepository {
|
||||
class FakeSessionRepository @Inject constructor(
|
||||
private val dataStore: DataStore<Preferences>,
|
||||
) : SessionRepository {
|
||||
|
||||
private val _isLoggedIn = MutableStateFlow(true)
|
||||
override val isLoggedIn: Flow<Boolean> = _isLoggedIn
|
||||
private companion object {
|
||||
val TOKEN_KEY = stringPreferencesKey("token")
|
||||
const val MOCK_SMS_CODE = "123456"
|
||||
val PHONE_PATTERN = Regex("^1\\d{10}$")
|
||||
// Mirrors the backend's "phone must be pre-provisioned" constraint.
|
||||
val PHONE_WHITELIST = setOf("13812341234")
|
||||
}
|
||||
|
||||
override suspend fun login() {
|
||||
delay(300)
|
||||
_isLoggedIn.value = true
|
||||
private var pendingCode: String? = null
|
||||
private var pendingPhone: String? = null
|
||||
|
||||
override val session: Flow<Session?> = dataStore.data
|
||||
.map { prefs -> prefs[TOKEN_KEY]?.let(::Session) }
|
||||
|
||||
override suspend fun requestSmsCode(phone: String): SmsCodeResult {
|
||||
if (!PHONE_PATTERN.matches(phone)) {
|
||||
return SmsCodeResult.Failure("请输入正确的手机号")
|
||||
}
|
||||
delay(500)
|
||||
pendingPhone = phone
|
||||
pendingCode = MOCK_SMS_CODE
|
||||
return SmsCodeResult.Success
|
||||
}
|
||||
|
||||
override suspend fun login(phone: String, code: String): LoginResult {
|
||||
if (phone != pendingPhone || code != pendingCode) {
|
||||
return LoginResult.Failure("验证码错误")
|
||||
}
|
||||
if (phone !in PHONE_WHITELIST) {
|
||||
return LoginResult.Failure("手机号未匹配,无法登录")
|
||||
}
|
||||
delay(500)
|
||||
dataStore.edit { it[TOKEN_KEY] = "mock-token-$phone" }
|
||||
pendingPhone = null
|
||||
pendingCode = null
|
||||
return LoginResult.Success
|
||||
}
|
||||
|
||||
override suspend fun logout() {
|
||||
delay(300)
|
||||
_isLoggedIn.value = false
|
||||
dataStore.edit { it.remove(TOKEN_KEY) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@ android {
|
||||
|
||||
dependencies {
|
||||
implementation(project(":core:model"))
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
api(libs.androidx.datastore.preferences)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,30 @@
|
||||
package com.mingyuan.android_client.core.domain.repository
|
||||
|
||||
import com.mingyuan.android_client.core.model.Session
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
sealed interface SmsCodeResult {
|
||||
data object Success : SmsCodeResult
|
||||
data class Failure(val message: String) : SmsCodeResult
|
||||
}
|
||||
|
||||
sealed interface LoginResult {
|
||||
data object Success : LoginResult
|
||||
data class Failure(val message: String) : LoginResult
|
||||
}
|
||||
|
||||
interface SessionRepository {
|
||||
val isLoggedIn: Flow<Boolean>
|
||||
/** The current session, or `null` when logged out. */
|
||||
val session: Flow<Session?>
|
||||
|
||||
suspend fun login()
|
||||
/** Derived convenience kept for the existing feature ViewModels (home/checkin/plan/profile). */
|
||||
val isLoggedIn: Flow<Boolean>
|
||||
get() = session.map { it != null }
|
||||
|
||||
suspend fun requestSmsCode(phone: String): SmsCodeResult
|
||||
|
||||
suspend fun login(phone: String, code: String): LoginResult
|
||||
|
||||
suspend fun logout()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.mingyuan.android_client.core.model
|
||||
|
||||
/**
|
||||
* A logged-in session. Deliberately minimal for now: the token is the only thing needed to decide
|
||||
* whether the user is logged in, and the user profile is still provided by [ProfileRepository].
|
||||
* When the real backend lands, this grows a `user: AuthUser` field and the local data source grows
|
||||
* a user cache.
|
||||
*/
|
||||
data class Session(
|
||||
val token: String,
|
||||
)
|
||||
Reference in New Issue
Block a user