feat(auth): 手机号验证码登录(会话模型 + 登录页 + 导航接入)

This commit is contained in:
AndyChen1024
2026-09-11 13:05:10 +08:00
parent 762e3975ca
commit b14f883e3e
13 changed files with 309 additions and 34 deletions

View File

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

View File

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