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:
阿猫
2026-09-03 14:44:30 +08:00
parent f1243078ad
commit 8854ff4288
2 changed files with 61 additions and 16 deletions

View File

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