task(1ff499d8-9640-4cda-a068-b52bac9f6731): Fix login response contract deviation blocking real device login
- Change AuthApi login and getUserInfo return types to ApiEnvelope<JsonElement>
- Add tokenFromData to handle both string and {"Token":"..."} object responses
- Add userInfoFromData to handle both flat UserInfo and {"UserInfo":"<escaped JSON>"} responses
- Inject Json into SessionRepository for unified deserialization
- Update signIn flow to extract token and user info through unified helpers
- Add contract tolerance documentation explaining V4.5 spec vs test environment behavior
This commit is contained in:
@@ -15,6 +15,11 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@@ -32,12 +37,18 @@ sealed class SessionState {
|
||||
* 会话门面(M1-01/02/03/04/05/07):
|
||||
* 登录 → Token 持久化(ConfigRepository/DataStore)→ 拉取用户信息进入 SignedIn;
|
||||
* 登出 / 改密成功 → 清 Token 回 SignedOut;冷启动凭存量 Token 自动恢复会话。
|
||||
*
|
||||
* 契约宽容:登录(2.2.1/2.2.3)与用户信息(2.4)接口的 data 形态与文档口径不符
|
||||
* (实测为 {"Token":"…"} 对象与 {"UserInfo":"<二次 JSON 编码字符串>"}),
|
||||
* AuthApi 以 JsonElement 宽容承载,本类经 [tokenFromData] / [userInfoFromData]
|
||||
* 归一提取,同时兼容文档口径与服务端实际口径。
|
||||
*/
|
||||
@Singleton
|
||||
class SessionRepository @Inject constructor(
|
||||
private val authApi: AuthApi,
|
||||
private val apiCaller: ApiCaller,
|
||||
private val configRepository: ConfigRepository,
|
||||
private val json: Json,
|
||||
) {
|
||||
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
@@ -61,7 +72,7 @@ class SessionRepository @Inject constructor(
|
||||
return
|
||||
}
|
||||
_state.value = try {
|
||||
SessionState.SignedIn(apiCaller.call { authApi.getUserInfo() })
|
||||
SessionState.SignedIn(userInfoFromData(apiCaller.call { authApi.getUserInfo() }))
|
||||
} catch (e: ApiError) {
|
||||
if (e !is ApiError.NetworkError) {
|
||||
// 待确认问题 #2 缺省口径:Token 失效按重新登录处理
|
||||
@@ -73,18 +84,16 @@ class SessionRepository @Inject constructor(
|
||||
|
||||
/** M1-01 账号密码登录(2.2.1)。 */
|
||||
suspend fun loginByAccount(loginName: String, password: String) {
|
||||
val token = apiCaller.call {
|
||||
signIn(apiCaller.call {
|
||||
authApi.loginByAccount(PasswordLoginRequest(loginName, password))
|
||||
}
|
||||
signIn(token)
|
||||
})
|
||||
}
|
||||
|
||||
/** M1-03 手机验证码登录(2.2.3);[mobile] 须为归一后的 11 位国内号。 */
|
||||
suspend fun loginByMobile(mobile: String, verificationCode: String) {
|
||||
val token = apiCaller.call {
|
||||
signIn(apiCaller.call {
|
||||
authApi.loginByMobile(MobileLoginRequest(mobile, verificationCode))
|
||||
}
|
||||
signIn(token)
|
||||
})
|
||||
}
|
||||
|
||||
/** M1-02 发送验证码(2.2.2);[mobilePhone] 传 [Phones.toQueryValue] 归一结果。 */
|
||||
@@ -106,9 +115,40 @@ class SessionRepository @Inject constructor(
|
||||
_state.value = SessionState.SignedOut
|
||||
}
|
||||
|
||||
private suspend fun signIn(rawToken: String) {
|
||||
val token = Phones.normalizeToken(rawToken)
|
||||
private suspend fun signIn(loginData: JsonElement) {
|
||||
val token = Phones.normalizeToken(tokenFromData(loginData))
|
||||
configRepository.updateToken(token)
|
||||
_state.value = SessionState.SignedIn(apiCaller.call { authApi.getUserInfo() })
|
||||
_state.value =
|
||||
SessionState.SignedIn(userInfoFromData(apiCaller.call { authApi.getUserInfo() }))
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录 data → Token 归一提取。兼容两种口径:
|
||||
* 文档 2.2.1/2.2.3 的裸字符串;测试环境实测(任务 ca3394be)的 {"Token":"…"} 对象。
|
||||
* 结构无法识别时以 [ApiError.EmptyBodyError] 抛出,UI 层统一按 ApiError 提示。
|
||||
*/
|
||||
private fun tokenFromData(data: JsonElement): String = when (data) {
|
||||
is JsonPrimitive -> data.content
|
||||
is JsonObject ->
|
||||
(data["Token"] as? JsonPrimitive ?: data["token"] as? JsonPrimitive)?.content
|
||||
?: throw ApiError.EmptyBodyError("登录响应缺少 Token 字段")
|
||||
else -> throw ApiError.EmptyBodyError("登录响应数据结构异常")
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户信息 data → [UserInfo] 归一提取。兼容两种口径:
|
||||
* 文档 2.4 的平铺 UserInfo 对象;测试环境实测的 {"UserInfo":"<二次 JSON 编码字符串>"}。
|
||||
* 二次编码字符串与平铺对象之外的形态、或字段与模型不符时,以 [ApiError.EmptyBodyError] 抛出。
|
||||
*/
|
||||
private fun userInfoFromData(data: JsonElement): UserInfo {
|
||||
val payload = (data as? JsonObject)?.get("UserInfo") ?: data
|
||||
return try {
|
||||
when (payload) {
|
||||
is JsonPrimitive -> json.decodeFromString(UserInfo.serializer(), payload.content)
|
||||
else -> json.decodeFromJsonElement(UserInfo.serializer(), payload)
|
||||
}
|
||||
} catch (e: SerializationException) {
|
||||
throw ApiError.EmptyBodyError("用户信息解析失败:${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user