18034727885
2026-05-13 03c43bb0d1abe7a22ce76bf903cc3c1e04648e44
登录优化
10个文件已添加
14个文件已修改
767 ■■■■■ 已修改文件
app/agconnect-services.json 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/build.gradle.kts 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/MyApp.kt 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/di/Koin.kt 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/extension/NumExtension.kt 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/extension/StringExtension.kt 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/extension/ViewExtension.kt 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/model/PasswordLoginCodeValidateInfo.kt 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/model/YiHuLoginInfo.kt 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/net/UserApi.kt 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/net/UserRepository.kt 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/net/YiHuApi.kt 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/net/YiHuRepository.kt 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/net/interceptor/YiHuHeaderInterceptor.kt 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/ui/user/login/LoginActivity.kt 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/ui/user/login/LoginVM.kt 115 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/utils/DeviceId.kt 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/activity_login.xml 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
build.gradle.kts 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
common/src/main/java/com/xw/common/net/BaseHttpManager.kt 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
common/src/main/java/com/xw/common/net/interceptor/HeaderInterceptor.kt 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
common/src/main/java/com/xw/common/net/interceptor/LoggingInterceptor.kt 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
gradle/libs.versions.toml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
settings.gradle.kts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/agconnect-services.json
New file
@@ -0,0 +1 @@
{"agcgw":{"url":"connect-drcn.dbankcloud.cn","backurl":"connect-drcn.hispace.hicloud.com","websocketurl":"connect-ws-drcn.hispace.dbankcloud.cn","websocketbackurl":"connect-ws-drcn.hispace.dbankcloud.com"},"agcgw_all":{"SG":"connect-dra.dbankcloud.cn","SG_back":"connect-dra.hispace.hicloud.com","CN":"connect-drcn.dbankcloud.cn","CN_back":"connect-drcn.hispace.hicloud.com","RU":"connect-drru.hispace.dbankcloud.ru","RU_back":"connect-drru.hispace.dbankcloud.cn","DE":"connect-dre.dbankcloud.cn","DE_back":"connect-dre.hispace.hicloud.com"},"websocketgw_all":{"SG":"connect-ws-dra.hispace.dbankcloud.cn","SG_back":"connect-ws-dra.hispace.dbankcloud.com","CN":"connect-ws-drcn.hispace.dbankcloud.cn","CN_back":"connect-ws-drcn.hispace.dbankcloud.com","RU":"connect-ws-drru.hispace.dbankcloud.ru","RU_back":"connect-ws-drru.hispace.dbankcloud.cn","DE":"connect-ws-dre.hispace.dbankcloud.cn","DE_back":"connect-ws-dre.hispace.dbankcloud.com"},"client":{"cp_id":"10086000932593129","product_id":"101653523864090831","client_id":"1947933504989878464","client_secret":"2FF3E11D2FBEA319A40BFFB113076FF808FDC879DE294A75892DE4251C330D06","project_id":"101653523864090831","app_id":"117680933","api_key":"DgEDANNbKTHvOmqNjimOJ+fartLex/1/hdD2nfgFIW4+CBa+xdVg5bSwlYRsMgMQvvuzE+sBG6gUiY21hbnwB1RJ/MvNL/lcqtoyUA==","package_name":"com.xw.yunwei"},"oauth_client":{"client_id":"117680933","client_type":1},"app_info":{"app_id":"117680933","package_name":"com.xw.yunwei"},"service":{"analytics":{"collector_url":"datacollector-drcn.dt.hicloud.com,datacollector-drcn.dt.dbankcloud.cn","collector_url_cn":"datacollector-drcn.dt.hicloud.com,datacollector-drcn.dt.dbankcloud.cn","collector_url_de":"datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn","collector_url_ru":"datacollector-drru.dt.dbankcloud.ru,datacollector-drru.dt.hicloud.com","collector_url_sg":"datacollector-dra.dt.hicloud.com,datacollector-dra.dt.dbankcloud.cn","resource_id":"p1","channel_id":""},"ml":{"mlservice_url":"ml-api-drcn.ai.dbankcloud.com,ml-api-drcn.ai.dbankcloud.cn"},"cloudstorage":{"storage_url":"https://agc-storage-drcn.platform.dbankcloud.cn","storage_url_ru":"https://agc-storage-drru.cloud.huawei.ru","storage_url_sg":"https://ops-dra.agcstorage.link","storage_url_de":"https://ops-dre.agcstorage.link","storage_url_cn":"https://agc-storage-drcn.platform.dbankcloud.cn","storage_url_ru_back":"https://agc-storage-drru.cloud.huawei.ru","storage_url_sg_back":"https://agc-storage-dra.cloud.huawei.asia","storage_url_de_back":"https://agc-storage-dre.cloud.huawei.eu","storage_url_cn_back":"https://agc-storage-drcn.cloud.huawei.com.cn"},"search":{"url":"https://search-drcn.cloud.huawei.com"},"edukit":{"edu_url":"edukit.cloud.huawei.com.cn","dh_url":"edukit.cloud.huawei.com.cn"}},"region":"CN","configuration_version":"3.0","appInfos":[{"package_name":"com.xw.yunwei","client":{"app_id":"117680933"},"oauth_client":{"client_id":"117680933","client_type":1},"app_info":{"app_id":"117680933","package_name":"com.xw.yunwei"}}]}
app/build.gradle.kts
@@ -1,3 +1,4 @@
apply(plugin = "com.huawei.agconnect")
plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.android)
@@ -75,23 +76,24 @@
    flavorDimensions.add("api")
    productFlavors {
        create("development") { // 开发环境
        create("local") { // 本地开发环境
            dimension = "api"
            applicationId = APPLICATION_ID
            manifestPlaceholders["APP_NAME"] = "@string/app_name_dev"
            manifestPlaceholders["icon"] = "@mipmap/ic_launcher"
            buildConfigField("String", "API_HOST", "\"http://221.182.45.100:8000/\"")
        }
        create("local") { // 本地环境
            dimension = "api"
            applicationId = APPLICATION_ID
            manifestPlaceholders["APP_NAME"] = "@string/app_name_dev"
            manifestPlaceholders["icon"] = "@mipmap/ic_launcher"
            buildConfigField("String", "API_HOST", "\"https://rt-mt.test.xiaopeng.local/\"")
            buildConfigField("String", "DEV_API_HOST", "\"\"")
            buildConfigField("String", "API_HOST", "\"http://rt-admin.test.xiaopeng.local/\"")
            buildConfigField("String", "WS_URL", "\"ws://221.182.45.100:8888/websocket\"")
            //翼虎appId
            buildConfigField("String", "YIHU_APPID", "\"RoboTaxi-admin-test\"")
            //翼虎appSecret
            buildConfigField("String", "YIHU_APP_SECRET", "\"SXa7vEe7V23Xh3RN\"")
            //翼虎公钥
            buildConfigField("String", "YIHU_PUBLIC_KEY", "\"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjT9Pl5kqJeinJyAjvW95ulxtON34ABhX pTRiPczVRJSVSTQzqLyr6UNRU6AvR2diZ4eGRHbeJ+NNtiuSverAZ+xu4B05xWhERThV0SETigkR 9IaJ9glucmAL6S6CNE5JCwWYYDfCgSIN3fzWhC48hGwEL8nauBIHrg2TRs0x4/rtmUR5oEMrm6RA eafdHDD62Dtsc7zm7uIxaOfn90cG35DKsp3iIcw7BLFT/HmBSVML2SLTruNkgDhH2qIZ1t6YQEJj C0uO+jiHXoluKRLWDMg9+SuoJHrsntCzRdwx246g6nMKfxtyC/jrPB6KUdrSR9xaxXZPcNQLb0ip y6mTkwIDAQAB\"")
            //翼虎网络域名
            buildConfigField("String", "YIHU_API_HOST", "\"https://sso.test.xiaopeng.com/sso/\"")
        }
        create("uat") { // 预发环境
            dimension = "api"
@@ -100,7 +102,17 @@
            manifestPlaceholders["APP_NAME"] = "@string/app_name_dev"
            manifestPlaceholders["icon"] = "@mipmap/ic_launcher"
            buildConfigField("String", "DEV_API_HOST", "\"\"")
            buildConfigField("String", "API_HOST", "\"https://rt-mt.deploy-test.xiaopeng.com/\"")
            buildConfigField("String", "WS_URL", "\"ws://192.168.110.188:8888/websocket\"")
            //翼虎appId
            buildConfigField("String", "YIHU_APPID", "\"RoboTaxi-admin-predeploy\"")
            //翼虎appSecret
            buildConfigField("String", "YIHU_APP_SECRET", "\"i6B5t4744XzO9Ed4\"")
            //翼虎公钥
            buildConfigField("String", "YIHU_PUBLIC_KEY", "\"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvvLduJASaa//KZNaVOH7ieU9QlMs/XxN CRBfFKAtWdwAGtaEnGoAdVJ+6c5dRICVm65Nbq0Xu4jqmdyZceUgWkXjBnxCRNCF0KT5HP6f08XS maDlWXXGDcE3ybZ9rhjwAplAJXoDHjLIi8aklI8qY6G5qUIPX+Occun5xhmiVDTKoyAkNFZ+n1K7 pVc+0rYzownPrGo4Yg+BmSJPoigxJJVZeVZ91jOiwt7F8jyE6EDmTQItJaElFF/zCbY6JlE3Ftsg HjkP/2lFDp+xMZi6b//BvB/GctWmLhkgFZ7j1PtH2Q5YKUzz8Ko6FhAkLaP7K7LXFfkQ1edOrygn 5XRuxQIDAQAB\"")
            //翼虎网络域名
            buildConfigField("String", "YIHU_API_HOST", "\"https://xtigersso.deploy-test.xiaopeng.com/sso/\"")
        }
        create("prd") { // 正式环境
            dimension = "api"
@@ -109,7 +121,17 @@
            manifestPlaceholders["APP_NAME"] = "@string/app_name"
            manifestPlaceholders["icon"] = "@mipmap/ic_launcher"
            buildConfigField("String", "DEV_API_HOST", "\"\"")
            buildConfigField("String", "API_HOST", "\"https://rt-mt.xiaopeng.com/\"")
            buildConfigField("String", "WS_URL", "\"ws://192.168.110.188:8888/websocket\"")
            //翼虎appId
            buildConfigField("String", "YIHU_APPID", "\"xx\"")
            //翼虎appSecret
            buildConfigField("String", "YIHU_APP_SECRET", "\"xx\"")
            //翼虎公钥
            buildConfigField("String", "YIHU_PUBLIC_KEY", "\"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3O+kbGNL8OsgS8ejbYh7MLxwr6dzrvE4euAcUn+Yk8H5aCgkd7tSCYntd8iQZoml1japizdB8MedJeMZZGeuNLXcHMpf+OmBA//ms3SG6aiocgxruHuHDVkVri/2rTuo078OsHaqnCJlvYEwbuuGj3D3So+TFz/Cw67ipL++j9z90tec40kzNebdfWG12Bv3uX+U8a34QhwE95+uKgwfCU8JEFqXWmA3KAV0COCL4glf3eioe5dBmpBvgnOwauepthjfOKPNqfGqHN88/WTgOHuQraWs9YY4QeYk5s+2CXVWMNlX++r83otgcfzMKmEdToQAHvfLRM4W4I1qaTKR8wIDAQAB\"")
            //翼虎网络域名
            buildConfigField("String", "YIHU_API_HOST", "\"https://xpsso.xiaopeng.com/sso/\"")
        }
    }
@@ -137,5 +159,6 @@
    implementation(project(":common"))
    implementation(libs.dialog.pickerview)
    implementation(libs.huawei.agconnect.core)
}
app/src/main/java/com/xw/yunwei/MyApp.kt
@@ -28,6 +28,7 @@
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.GlobalContext.startKoin
import com.tencent.bugly.crashreport.CrashReport
import com.xw.yunwei.utils.DeviceId
class MyApp : Application() {
@@ -69,6 +70,7 @@
    override fun onCreate() {
        super.onCreate()
        myApp = this
        DeviceId.init(this)
        CrashReport.initCrashReport(this, "1d59d50929", true) // bugly
        initLog()
app/src/main/java/com/xw/yunwei/di/Koin.kt
@@ -10,6 +10,9 @@
import com.xw.yunwei.net.UserRepository
import com.xw.yunwei.net.WorkApi
import com.xw.yunwei.net.WorkRepository
import com.xw.yunwei.net.YiHuApi
import com.xw.yunwei.net.YiHuRepository
import com.xw.yunwei.net.interceptor.YiHuHeaderInterceptor
import org.koin.dsl.module
val networkModule = module {
@@ -17,27 +20,47 @@
    single {
        val baseHttpManager: BaseHttpManager = get()
        baseHttpManager.create(BuildConfig.API_HOST, UserApi::class.java)
        baseHttpManager.create(
            if (BuildConfig.DEBUG && BuildConfig.DEV_API_HOST.isNotEmpty()) BuildConfig.DEV_API_HOST else BuildConfig.API_HOST,
            UserApi::class.java
        )
    }
    single { UserRepository(get()) }
    single {
        val baseHttpManager: BaseHttpManager = get()
        baseHttpManager.create(BuildConfig.API_HOST, WorkApi::class.java)
        baseHttpManager.create(
            if (BuildConfig.DEBUG && BuildConfig.DEV_API_HOST.isNotEmpty()) BuildConfig.DEV_API_HOST else BuildConfig.API_HOST,
            WorkApi::class.java
        )
    }
    single { WorkRepository(get()) }
    single {
        val baseHttpManager: BaseHttpManager = get()
        baseHttpManager.create(BuildConfig.API_HOST, TaskApi::class.java)
        baseHttpManager.create(
            if (BuildConfig.DEBUG && BuildConfig.DEV_API_HOST.isNotEmpty()) BuildConfig.DEV_API_HOST else BuildConfig.API_HOST,
            TaskApi::class.java
        )
    }
    single { TaskRepository(get()) }
    single {
        val baseHttpManager: BaseHttpManager = get()
        baseHttpManager.create(BuildConfig.API_HOST, CommonApi::class.java)
        baseHttpManager.create(
            if (BuildConfig.DEBUG && BuildConfig.DEV_API_HOST.isNotEmpty()) BuildConfig.DEV_API_HOST else BuildConfig.API_HOST,
            CommonApi::class.java
        )
    }
    single { CommonRepository(get()) }
    single {
        val baseHttpManager: BaseHttpManager = get()
        baseHttpManager.create(
            BuildConfig.YIHU_API_HOST,
            YiHuApi::class.java,
            YiHuHeaderInterceptor()
        )
    }
    single { YiHuRepository(get()) }
}
app/src/main/java/com/xw/yunwei/extension/NumExtension.kt
New file
@@ -0,0 +1,50 @@
package com.xw.yunwei.extension
import android.content.Context
import androidx.core.content.ContextCompat
// 扩展 Float
fun Double.spToPx(context: Context): Double =
    this * context.resources.displayMetrics.scaledDensity
fun Double.pxToSp(context: Context): Double =
    this / context.resources.displayMetrics.scaledDensity
// 扩展 Float
fun Float.spToPx(context: Context): Float =
    this * context.resources.displayMetrics.scaledDensity
fun Float.pxToSp(context: Context): Float =
    this / context.resources.displayMetrics.scaledDensity
// 扩展 Int
fun Int.spToPx(context: Context): Int =
    (this * context.resources.displayMetrics.scaledDensity).toInt()
fun Int.pxToSp(context: Context): Int =
    (this / context.resources.displayMetrics.scaledDensity).toInt()
// 扩展 Float
fun Double.dpToPx(context: Context): Double =
    this * context.resources.displayMetrics.density
fun Double.pxToDp(context: Context): Double =
    this / context.resources.displayMetrics.density
// 扩展 Float
fun Float.dpToPx(context: Context): Float =
    this * context.resources.displayMetrics.density
fun Float.pxToDp(context: Context): Float =
    this / context.resources.displayMetrics.density
// 扩展 Int
fun Int.dpToPx(context: Context): Int =
    (this * context.resources.displayMetrics.density + 0.5f).toInt()
fun Int.pxToDp(context: Context): Int =
    (this / context.resources.displayMetrics.density + 0.5f).toInt()
//颜色资源id转颜色值
fun Int.colorIdToColorValue(context: Context): Int =
    ContextCompat.getColor(context, this)
app/src/main/java/com/xw/yunwei/extension/StringExtension.kt
New file
@@ -0,0 +1,82 @@
package com.xw.yunwei.extension
import android.util.Base64
import java.nio.charset.StandardCharsets
import java.security.KeyFactory
import java.security.MessageDigest
import java.security.PublicKey
import java.security.spec.X509EncodedKeySpec
import javax.crypto.Cipher
import javax.crypto.Mac
import javax.crypto.SecretKey
import javax.crypto.spec.SecretKeySpec
//扩展函数 MD5加密 耗时操作
fun String.md5(): String {
    return try {
        val md = MessageDigest.getInstance("MD5")
        val digest = md.digest(toByteArray())
        digest.joinToString("") {
            "%02x".format(it)
        }
    } catch (e: Exception) {
        e.printStackTrace()
        ""
    }
}
//扩展函数 通过HmacSHA256加密
fun String.encryptByHmacSHA256(key: String): String {
    try {
        val key: SecretKey =
            SecretKeySpec(
                key.toByteArray(StandardCharsets.UTF_8),
                "HmacSHA256"
            )
        val mac = Mac.getInstance(key.algorithm)
        mac.init(key)
        val sig = mac.doFinal(toByteArray(StandardCharsets.UTF_8))
        val hex = java.lang.StringBuilder(sig.size * 2)
        for (b in sig) {
            hex.append(Character.forDigit((b.toInt() shr 4) and 0xF, 16))
            hex.append(Character.forDigit(b.toInt() and 0xF, 16))
        }
        return hex.toString()
    } catch (e: Exception) {
        throw RuntimeException(e)
    }
}
//扩展函数 通过公钥RSA加密
fun String.encryptByPublicKey(publicKeyText: String): String {
    return try {
        // 1. 解码 Base64 编码的公钥文本获取 DER 格式字节数组
        // 对应 Python: base64.b64decode(public_key_text)
        val derPublicKeyBytes = Base64.decode(publicKeyText, Base64.DEFAULT)
        // 2. 加载 DER 格式的公钥
        // 对应 Python: serialization.load_der_public_key(...)
        // 在 Java/Kotlin 中,通常使用 X509EncodedKeySpec 来加载 DER 格式的公钥
        val keySpec = X509EncodedKeySpec(derPublicKeyBytes)
        // 注意:这里使用 "RSA" 算法。如果你的后端严格要求 PKCS#1 格式,
        // 可能需要使用 "RSA" 并配合 Bouncy Castle 库,但 X509EncodedKeySpec 通常能处理大多数标准公钥。
        val keyFactory = KeyFactory.getInstance("RSA")
        val publicKey: PublicKey = keyFactory.generatePublic(keySpec)
        // 3. 使用 RSA 加密
        // 对应 Python: public_key.encrypt(..., padding.PKCS1v15())
        val cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding")
        cipher.init(Cipher.ENCRYPT_MODE, publicKey)
        // 将文本转换为字节数组并加密
        val encryptedBytes = cipher.doFinal(toByteArray(StandardCharsets.UTF_8))
        // 4. 返回 Base64 编码的加密结果
        // 对应 Python: base64.b64encode(ciphertext).decode('utf-8')
        Base64.encodeToString(encryptedBytes, Base64.NO_WRAP)
    } catch (e: Exception) {
        e.printStackTrace()
        ""
    }
}
app/src/main/java/com/xw/yunwei/extension/ViewExtension.kt
New file
@@ -0,0 +1,15 @@
package com.xw.yunwei.extension
import android.view.View
//扩展函数 设置防重复点击监听器
fun View.setOnDurationClickListener(duration: Long = 1000L, listener: View.OnClickListener) {
    var lastClickTimestamp = 0L
    setOnClickListener {
        val now = System.currentTimeMillis()
        if (now - lastClickTimestamp > duration) {
            lastClickTimestamp = now
            listener.onClick(it)
        }
    }
}
app/src/main/java/com/xw/yunwei/model/PasswordLoginCodeValidateInfo.kt
New file
@@ -0,0 +1,4 @@
package com.xw.yunwei.model
//账号密码登录验证码验证信息
data class PasswordLoginCodeValidateInfo(val randomKey: String)
app/src/main/java/com/xw/yunwei/model/YiHuLoginInfo.kt
New file
@@ -0,0 +1,14 @@
package com.xw.yunwei.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
//翼虎登录信息
@Parcelize
data class YiHuLoginInfo(
    val access_token: String?,//登录令牌
    val expires_in: String?,//access_token的有效时间,单位为‘秒’
    val refresh_token: String?,//用于刷新token
    val scope: String?,//token权限,可以忽略
    val token_type: String?//固定为‘Bearer’,可以忽略
) : Parcelable
app/src/main/java/com/xw/yunwei/net/UserApi.kt
@@ -2,6 +2,7 @@
import com.xw.common.net.interceptor.HttpResponse
import com.xw.common.net.interceptor.PageResponse
import com.xw.yunwei.BuildConfig
import com.xw.yunwei.model.AppPermissionModel
import com.xw.yunwei.model.AppVersionModel
import com.xw.yunwei.model.ClockInStatusModel
@@ -12,9 +13,8 @@
import okhttp3.RequestBody
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
import retrofit2.http.Path
import retrofit2.http.Query
@@ -30,6 +30,19 @@
    @POST("account/maintenance-app/base/v1/verificationCodeLogin")
    fun loginByCode(@Body body: RequestBody): Call<HttpResponse<LoginModel>>
    /*通过第三方token登录*/
    @GET("${BuildConfig.API_HOST}account/maintenance-app/ssoAuthSdk/appLogin")
    fun loginByThirdToken(
        @Query("deviceType") deviceType: Int = 1,
        @Header("Content-Type") contentType: String = "application/json",
        @Header("XT-Client-Type") clientType: String,
        @Header("XT-DeviceId") deviceId: String,
        @Header("XT-Nonce") nonce: String,
        @Header("XT-AppId") appId: String,
        @Header("XT-Signature") sign: String,
        @Header("Authorization") token: String
    ): Call<HttpResponse<LoginModel>>
    /*获取手机验证码*/
    @POST("account/maintenance-app/base/v1/getVerificationCode")
    fun getVerificationCode(@Body body: RequestBody): Call<HttpResponse<Any>>
app/src/main/java/com/xw/yunwei/net/UserRepository.kt
@@ -1,11 +1,17 @@
package com.xw.yunwei.net
import android.util.Log
import com.xw.common.net.BaseHttpManager.Companion.getBody
import com.xw.common.net.interceptAwait
import com.xw.yunwei.BuildConfig
import com.xw.yunwei.extension.encryptByHmacSHA256
import com.xw.yunwei.extension.md5
import com.xw.yunwei.model.LoginModel
import com.xw.yunwei.utils.DeviceId
import kotlinx.coroutines.flow.flow
class UserRepository(private val api: UserApi) {
    private val TAG = "UserRepository"
    fun loginByPassword(map: Map<String, Any?>) = flow {
        emit(api.loginByPassword(getBody(map)).interceptAwait())
    }
@@ -15,6 +21,43 @@
    suspend fun loginByCode(map: Map<String, Any?>) = api.loginByCode(getBody(map)).interceptAwait()
    /*通过第三方token登录*/
    suspend fun loginByThirdToken(token: String): LoginModel? {
        val clientType = "Android"
        val deviceId = DeviceId.get()
        val nonce = System.currentTimeMillis().toString()
        val appId = BuildConfig.YIHU_APPID
        val path = "/xtiger/app/userinfo"
        //第1步,生成parameter
        val parameter = mapOf(
            "XT-Client-Type" to clientType,
            "XT-DeviceId" to deviceId,
            "XT-Nonce" to nonce,
            "XT-AppId" to appId,
        ).toSortedMap().map { "${it.key.uppercase()}=${it.value}" }.joinToString("&")
        val content = ""
        //第2步,请求JSON body转成contentMd5 ,注意md5后为小写
        val contentMd5 = content.md5()
        Log.d(TAG, "intercept: contentMd5:${contentMd5}")
        // 第3步,组合成最终需要签名的字符串
        val strToSign = "${path}\n${parameter}\n${contentMd5}"
        Log.d(TAG, "intercept: strToSign:${strToSign}")
        // 第4步. 最终签名值
        val sign = strToSign.encryptByHmacSHA256(BuildConfig.YIHU_APP_SECRET)
        Log.d(TAG, "intercept: sign:${sign}")
        return api.loginByThirdToken(
            clientType = clientType,
            deviceId = deviceId,
            nonce = nonce,
            appId = appId,
            sign = sign,
            token = token
        ).interceptAwait()
    }
    suspend fun modifierPsw(map: Map<String, Any?>) = api.modifierPsw(getBody(map)).interceptAwait()
    suspend fun read(id: Long) = api.read(id).interceptAwait()
    suspend fun getAppPermission() = api.getAppPermission().interceptAwait()
app/src/main/java/com/xw/yunwei/net/YiHuApi.kt
New file
@@ -0,0 +1,29 @@
package com.xw.yunwei.net
import com.xw.common.net.interceptor.HttpResponse
import com.xw.yunwei.model.PasswordLoginCodeValidateInfo
import com.xw.yunwei.model.YiHuLoginInfo
import okhttp3.RequestBody
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.POST
//翼虎api
interface YiHuApi {
    /*获取手机号验证码*/
    @POST("app/sms/code")
    fun getPhoneLoginCode(@Body body: RequestBody): Call<HttpResponse<Any>>
    /*手机验证码登录*/
    @POST("app/sms/login")
    fun phoneLogin(@Body body: RequestBody): Call<HttpResponse<YiHuLoginInfo>>
    /*获取双因子验证码(非TEE版本)*/
    @POST("app/twoFactor/code")
    fun getPasswordLoginCode(@Body body: RequestBody): Call<HttpResponse<PasswordLoginCodeValidateInfo>>
    /*验证码+账密双因子登录(TEE方式)*/
    @POST("app/twoFactor/login")
    fun passwordLogin(@Body body: RequestBody): Call<HttpResponse<YiHuLoginInfo>>
}
app/src/main/java/com/xw/yunwei/net/YiHuRepository.kt
New file
@@ -0,0 +1,53 @@
package com.xw.yunwei.net
import com.xw.common.net.BaseHttpManager.Companion.getBody
import com.xw.common.net.interceptAwait
import com.xw.yunwei.BuildConfig
import kotlinx.coroutines.flow.flow
//翼虎网络接口仓库
class YiHuRepository(private val api: YiHuApi) {
    //获取登录验证码
    fun getPhoneLoginCode(phone: String) = flow {
        val map = mapOf<String, Any?>(
            "phone" to phone,
            "smsType" to "LOGIN",
            "clientId" to BuildConfig.YIHU_APPID
        )
        emit(api.getPhoneLoginCode(getBody(map)).interceptAwait())
    }
    //手机验证码登录
    fun login(phone: String, authCode: String) = flow {
        val map = mapOf<String, Any?>(
            "phone" to phone,
            "authCode" to authCode,
            "clientId" to BuildConfig.YIHU_APPID
        )
        emit(api.phoneLogin(getBody(map)).interceptAwait())
    }
    //获取双因子验证码(非TEE版本)
    fun getPasswordLoginCode(phone: String) = flow {
        val map = mapOf<String, Any?>(
            "username" to phone,
            "twoFactorType" to 1,
            "clientId" to BuildConfig.YIHU_APPID
        )
        emit(api.getPasswordLoginCode(getBody(map)).interceptAwait())
    }
    //验证码+账密双因子登录(TEE方式)
    fun passwordLogin(phone: String, authCode: String, password: String, randomKey: String) = flow {
        val map = mapOf<String, Any?>(
            "username" to phone,
            "twoFactorType" to 1,
            "authCode" to authCode,
            "randomKey" to randomKey,
            "password" to password,
            "clientId" to BuildConfig.YIHU_APPID
        )
        emit(api.passwordLogin(getBody(map)).interceptAwait())
    }
}
app/src/main/java/com/xw/yunwei/net/interceptor/YiHuHeaderInterceptor.kt
New file
@@ -0,0 +1,77 @@
package com.xw.yunwei.net.interceptor
import android.util.Log
import com.xw.yunwei.BuildConfig
import com.xw.yunwei.extension.encryptByHmacSHA256
import com.xw.yunwei.extension.md5
import com.xw.yunwei.utils.DeviceId
import okhttp3.FormBody
import okhttp3.Interceptor
import okhttp3.Response
import okio.Buffer
//翼虎网络接口header拦截器
class YiHuHeaderInterceptor : Interceptor {
    private val TAG = "YiHuHeaderInterceptor"
    override fun intercept(chain: Interceptor.Chain): Response {
        val original = chain.request()
        val publicHeader = mutableMapOf(
            "XT-Client-Type" to "Android",
            "XT-DeviceId" to DeviceId.get(),
            "XT-Nonce" to System.currentTimeMillis().toString(),
            "XT-AppId" to BuildConfig.YIHU_APPID,
        )
        Log.d(TAG, "intercept: publicHeader:${publicHeader}")
        val path = original.url.toString().let {
            val start = it.indexOf("sso/")
            val end = it.indexOf("?")
            if (end == -1) {
                it.substring(start + 3)
            } else {
                it.substring(start + 3, end)
            }
        }
        Log.d(TAG, "intercept: path:${path}")
        val map = original.url.let { url ->
            url.queryParameterNames.associateWith {
                url.queryParameter(it)
            }
        }.toMutableMap()
        map.putAll(publicHeader)
        val body = original.body
        if (body is FormBody) {
            (0 until body.size).forEach {
                map[body.name(it)] = body.value(it)
            }
        }
        //第1步,生成parameter
        val parameter =
            map.toSortedMap().map { "${it.key.uppercase()}=${it.value}" }.joinToString("&")
        Log.d(TAG, "intercept: parameter:${parameter}")
        val method = original.method
        val content =
            if ((method == "POST" || method == "PUT") && body?.contentType()?.subtype == "json") {
                val buffer = Buffer()
                body.writeTo(buffer)
                buffer.readUtf8()
            } else {
                ""
            }
        Log.d(TAG, "intercept: content:${content}")
        //第2步,请求JSON body转成contentMd5 ,注意md5后为小写
        val contentMd5 = content.md5()
        Log.d(TAG, "intercept: contentMd5:${contentMd5}")
        // 第3步,组合成最终需要签名的字符串
        val strToSign = "${path}\n${parameter}\n${contentMd5}"
        Log.d(TAG, "intercept: strToSign:${strToSign}")
        // 第4步. 最终签名值
        val sign = strToSign.encryptByHmacSHA256(BuildConfig.YIHU_APP_SECRET)
        Log.d(TAG, "intercept: sign:${sign}")
        publicHeader["XT-Signature"] = sign
        publicHeader["Content-Type"] = "application/json"
        val request = original.newBuilder()
        publicHeader.forEach { (key, value) -> request.addHeader(key, value) }
        return chain.proceed(request.build())
    }
}
app/src/main/java/com/xw/yunwei/ui/user/login/LoginActivity.kt
@@ -2,6 +2,7 @@
import android.annotation.SuppressLint
import android.graphics.Color
import android.text.InputType
import android.text.TextPaint
import android.text.method.HideReturnsTransformationMethod
import android.text.method.LinkMovementMethod
@@ -132,11 +133,36 @@
    override fun setObservable() {
        super.setObservable()
        viewModel.tabIndex.observe(this) {
//            binding.phone.setText("")
            binding.code.setText("")
            when (it) {
                0 -> {
//                    binding.phone.hint = "手机号"
//                    binding.phone.inputType = InputType.TYPE_CLASS_PHONE
//                    viewModel.passwordLoginCodeValidateInfo = null
                }
                1 -> {
//                    binding.phone.hint = "登录账户或者邮箱"
//                    binding.phone.inputType = InputType.TYPE_CLASS_TEXT
                }
            }
        }
        viewModel.protocolStatus.observe(this) {
            MMKVManager.getInstance().saveBoolean(KeyPreferences.NEED_READ, !it)
        }
        viewModel.phone.observe(this) {
            viewModel.canGetCode.value = it.length == 11
            when (viewModel.tabIndex.value) {
                0 -> {
                    viewModel.canGetCode.postValue(it.length == 11)
                }
                1 -> {
//                    viewModel.canGetCode.postValue(it.isNotEmpty())
                    viewModel.canGetCode.postValue(it.length == 11)
                }
            }
        }
        viewModel.eyeStatus2.observe(this) {
            setPasswordVisible(binding.psw, it)
app/src/main/java/com/xw/yunwei/ui/user/login/LoginVM.kt
@@ -9,17 +9,19 @@
import com.xw.common.utils.KeyPreferences
import com.xw.common.utils.MMKVManager
import com.xw.common.utils.ToastUtilCommon
import com.xw.yunwei.BuildConfig
import com.xw.yunwei.R
import com.xw.yunwei.extension.encryptByPublicKey
import com.xw.yunwei.model.PasswordLoginCodeValidateInfo
import com.xw.yunwei.net.UserRepository
import com.xw.yunwei.net.WorkRepository
import com.xw.yunwei.net.YiHuRepository
import com.xw.yunwei.ui.MainActivity
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.koin.core.component.inject
import kotlin.getValue
class LoginVM(application: Application) : BaseViewModel(application) {
    val userRepository: UserRepository by inject()
    val yiHuRepository: YiHuRepository by inject()
    val tabIndex = MutableLiveData(0)
@@ -35,6 +37,8 @@
    val protocolStatus = MutableLiveData(false)
    //账号密码验证码验证信息
    var passwordLoginCodeValidateInfo: PasswordLoginCodeValidateInfo? = null
    val onClick = View.OnClickListener {
        when (it.id) {
            R.id.protocolStatus -> protocolStatus.value = !protocolStatus.value!!
@@ -68,21 +72,39 @@
            ToastUtilCommon.showError("请输入验证码!")
            return
        }
        launchRequest(tips = "登录中...") {
            val map = mapOf("phoneNumber" to phone.value, "verificationCode" to code.value, "deviceType" to 1)
            val data = userRepository.loginByCode(map)
            data?.let {
                Constants.token = it.token?.access_token ?: ""
                MMKVManager.getInstance().saveModel(KeyPreferences.USER, it.info?.xpOperationsUser)
                startActivity(MainActivity::class.java)
                finish()
        launchRequestFlow(
            tips = "登录中...",
            flowBlock = {
                yiHuRepository.login(phone.value!!, code.value!!)
            },
            onCollect = { yiHuData ->
                yiHuData?.let {
                    launchRequest(tips = "登录中...") {
                        val data = userRepository.loginByThirdToken(yiHuData.access_token!!)
                        data?.let {
                            Constants.token = it.token?.access_token ?: ""
                            MMKVManager.getInstance()
                                .saveModel(KeyPreferences.USER, it.info?.xpOperationsUser)
                            startActivity(MainActivity::class.java)
                            finish()
                        }
                    }
                }
            }
        }
        )
    }
    private fun loginByPassword() {
        if (phone.value.isNullOrEmpty()) {
            ToastUtilCommon.showError("请输入手机号!")
            return
        }
        if (code.value.isNullOrEmpty()) {
            ToastUtilCommon.showError("请输入验证码!")
            return
        }
        if (passwordLoginCodeValidateInfo == null) {
            ToastUtilCommon.showError("请重新获取验证码!")
            return
        }
        if (psw.value.isNullOrEmpty()) {
@@ -92,15 +114,25 @@
        launchRequestFlow(
            tips = "登录中...",
            flowBlock = {
                val map = mapOf("phoneNumber" to phone.value, "password" to psw.value, "deviceType" to 1)
                userRepository.loginByPassword(map)
                yiHuRepository.passwordLogin(
                    phone.value!!,
                    code.value!!,
                    psw.value!!.encryptByPublicKey(BuildConfig.YIHU_PUBLIC_KEY),
                    passwordLoginCodeValidateInfo!!.randomKey
                )
            },
            onCollect = { data ->
                data?.let {
                    Constants.token = it.token?.access_token ?: ""
                    MMKVManager.getInstance().saveModel(KeyPreferences.USER, it.info?.xpOperationsUser)
                    startActivity(MainActivity::class.java)
                    finish()
            onCollect = { yiHuData ->
                yiHuData?.let {
                    launchRequest(tips = "登录中...") {
                        val data = userRepository.loginByThirdToken(yiHuData.access_token!!)
                        data?.let {
                            Constants.token = it.token?.access_token ?: ""
                            MMKVManager.getInstance()
                                .saveModel(KeyPreferences.USER, it.info?.xpOperationsUser)
                            startActivity(MainActivity::class.java)
                            finish()
                        }
                    }
                }
            }
        )
@@ -118,16 +150,41 @@
            closeKeyboard()
            timer.start()
            canGetCode.postValue(false)
            launchRequest(tips = "验证码获取中...", error = { code, msg ->
                canGetCode.postValue(true)
                timer.cancel()
                codeTips.postValue("重新发送")
                ToastUtilCommon.showError(msg)
            }) {
                val map = mapOf("phoneNumber" to phone.value!!, "codeType" to codeType)
                userRepository.getVerificationCode(map)
                ToastUtilCommon.showSuc("验证码已发送")
            if (codeType != 1) {
                launchRequest(tips = "验证码获取中...", error = { code, msg ->
                    canGetCode.postValue(true)
                    timer.cancel()
                    codeTips.postValue("重新发送")
                    ToastUtilCommon.showError(msg)
                }) {
                    val map = mapOf("phoneNumber" to phone.value!!, "codeType" to codeType)
                    userRepository.getVerificationCode(map)
                    ToastUtilCommon.showSuc("验证码已发送")
                }
                return
            }
            launchRequestFlow(
                tips = "验证码获取中...",
                flowBlock = {
                    if (tabIndex.value == 0) {
                        yiHuRepository.getPhoneLoginCode(phone.value!!)
                    } else {
                        yiHuRepository.getPasswordLoginCode(phone.value!!)
                    }
                },
                error = { code, msg ->
                    canGetCode.postValue(true)
                    timer.cancel()
                    codeTips.postValue("重新发送")
                    ToastUtilCommon.showError(msg)
                },
                onCollect = { data ->
                    ToastUtilCommon.showSuc("验证码已发送")
                    if (tabIndex.value == 1) {
                        passwordLoginCodeValidateInfo = data as PasswordLoginCodeValidateInfo?
                    }
                }
            )
        }
    }
app/src/main/java/com/xw/yunwei/utils/DeviceId.kt
New file
@@ -0,0 +1,26 @@
package com.xw.yunwei.utils
import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
import java.util.UUID
//设备id
object DeviceId {
    private const val KEY_DEVICE_ID = "device_id"
    private lateinit var sp: SharedPreferences
    //初始化
    fun init(context: Context) {
        sp = context.getSharedPreferences("device", Context.MODE_PRIVATE)
        var id = sp.getString(KEY_DEVICE_ID, null)
        if (id == null) {
            id = UUID.randomUUID().toString().replace("-","")
            sp.edit { putString(KEY_DEVICE_ID, id) }
        }
    }
    fun get(): String {
        return sp.getString(KEY_DEVICE_ID, null) ?: ""
    }
}
app/src/main/res/layout/activity_login.xml
@@ -83,7 +83,6 @@
        <androidx.appcompat.widget.AppCompatEditText
            android:id="@+id/code"
            show="@{viewModel.tabIndex==0}"
            android:layout_width="match_parent"
            android:layout_height="@dimen/dp_56"
            android:layout_marginHorizontal="@dimen/dp_32"
@@ -100,7 +99,6 @@
        <androidx.appcompat.widget.AppCompatTextView
            android:id="@+id/getCode"
            selected="@{viewModel.canGetCode}"
            show="@{viewModel.tabIndex==0}"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{viewModel.onClick}"
@@ -111,6 +109,13 @@
            app:layout_constraintEnd_toEndOf="@+id/code"
            tools:text="发送验证码" />
        <View
            android:layout_width="match_parent"
            android:layout_height="1px"
            android:layout_marginHorizontal="@dimen/dp_32"
            android:background="@color/gray_4"
            app:layout_constraintTop_toBottomOf="@+id/code" />
        <androidx.appcompat.widget.AppCompatEditText
            android:id="@+id/psw"
            show="@{viewModel.tabIndex==1}"
build.gradle.kts
@@ -1,4 +1,18 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
// 注意:buildscript块必须放在最前面
buildscript {
    repositories {
        google()
        mavenCentral()
        // 添加华为仓库
        maven { url = uri("https://developer.huawei.com/repo/") }
    }
    dependencies {
        classpath("com.android.tools.build:gradle:8.12.3")
        // 添加华为推送插件的classpath
        classpath("com.huawei.agconnect:agcp:1.5.2.300") // 请确认版本号,可以使用libs.versions.toml中定义的版本
    }
}
plugins {
    alias(libs.plugins.android.application) apply false
    alias(libs.plugins.android.library) apply false
common/src/main/java/com/xw/common/net/BaseHttpManager.kt
@@ -4,11 +4,10 @@
import com.xw.common.net.interceptor.HeaderInterceptor
import com.xw.common.net.interceptor.LoggingInterceptor
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient
import okhttp3.RequestBody
import org.json.JSONArray
import org.json.JSONObject
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.converter.scalars.ScalarsConverterFactory
@@ -35,23 +34,27 @@
    }
    fun <T> create(baseUrl: String, c: Class<T>): T {
    fun <T> create(
        baseUrl: String,
        c: Class<T>,
        interceptor: Interceptor = HeaderInterceptor()
    ): T {
        return Retrofit.Builder()
            .baseUrl(baseUrl.toHttpUrl())
            .client(provideOKHttpClient())
            .client(provideOKHttpClient(interceptor))
            .addConverterFactory(ScalarsConverterFactory.create())
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(c)
    }
    private fun provideOKHttpClient(): OkHttpClient {
    private fun provideOKHttpClient(interceptor: Interceptor): OkHttpClient {
        val builder = OkHttpClient.Builder()
            .connectTimeout(30, TimeUnit.SECONDS)
            .readTimeout(30, TimeUnit.SECONDS)
            .writeTimeout(30, TimeUnit.SECONDS)
            .retryOnConnectionFailure(true)
            .addInterceptor(HeaderInterceptor())
            .addInterceptor(interceptor)
            .addInterceptor(LoggingInterceptor())
        return builder.build()
    }
common/src/main/java/com/xw/common/net/interceptor/HeaderInterceptor.kt
@@ -1,7 +1,5 @@
package com.xw.common.net.interceptor
import android.os.Build
import com.blankj.utilcode.util.DeviceUtils
import com.xw.common.Constants.token
import okhttp3.Interceptor
import okhttp3.Response
@@ -9,9 +7,11 @@
class HeaderInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val original = chain.request()
        val request = original.newBuilder()
            .header("Authorization", token)
        return chain.proceed(request.build())
        val request = chain.request()
        val builder = request.newBuilder()
        if (request.header("Authorization").isNullOrEmpty()) {
            builder.header("Authorization", token)
        }
        return chain.proceed(builder.build())
    }
}
common/src/main/java/com/xw/common/net/interceptor/LoggingInterceptor.kt
@@ -1,6 +1,7 @@
package com.xw.common.net.interceptor
import com.xw.common.utils.LogUtils
import okhttp3.Headers
import okhttp3.Interceptor
import okhttp3.MediaType
import okhttp3.MultipartBody
@@ -14,50 +15,77 @@
class LoggingInterceptor : Interceptor {
    val TAG = "zmc-Log"
    val TAG = "LoggingInterceptor"
    @Throws(IOException::class)
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        val startTime = System.currentTimeMillis()
        LogUtils.d("$TAG-${Thread.currentThread().id}", "head " + request.headers)
        LogUtils.d("$TAG-${Thread.currentThread().id}", "----------Start----------------")
        if (request.method.compareTo("get", true) == 0 || request.method.compareTo("delete", true) == 0) {
            LogUtils.d("$TAG-${Thread.currentThread().id}", "| " + URLDecoder.decode(request.toString(), "UTF-8"))
        } else {
            val requestInfo = "Request{method=${request.method}, url=${request.url}, head=${request.headers}}"
            LogUtils.d("$TAG-${Thread.currentThread().id}", "| $requestInfo")
            try {
                val jsonInfo = "RequestParams:" + URLDecoder.decode(bodyToString(request), "UTF-8")
                LogUtils.d("$TAG-${Thread.currentThread().id}", "| $jsonInfo")
            } catch (e: Exception) {
                LogUtils.e("$TAG-${Thread.currentThread().id}", "| " + "RequestParams:" + bodyToString(request))
                LogUtils.e("$TAG-${Thread.currentThread().id}", "| " + "error:" + e.message)
            }
        }
        val response: Response
        try {
            response = chain.proceed(request)
        } catch (e: IOException) {
            LogUtils.d(TAG, "----------Start----------------")
            printRequest(request)
            val endTime = System.currentTimeMillis()
            val duration = endTime - startTime
            LogUtils.e("$TAG-${Thread.currentThread().id}", "| Network Error: ${e.javaClass.simpleName}: ${e.message}")
            LogUtils.e("$TAG-${Thread.currentThread().id}", "----------End(Error):" + duration + "毫秒----------")
            e.printStackTrace()
            LogUtils.e(TAG, "----------End(Error):" + duration + "毫秒----------")
            throw e // 重新抛出,让上层处理
        }
        LogUtils.d(TAG, "----------Start----------------")
        printRequest(request)
        val endTime = System.currentTimeMillis()
        val bodyString = response.body?.string() ?: ""
        LogUtils.d("$TAG-${Thread.currentThread().id}", "| Response:$bodyString")
        LogUtils.d(TAG, "response=$bodyString")
        printHeader("responseHeaders=", response.headers)
        val duration = endTime - startTime
        LogUtils.d("$TAG-${Thread.currentThread().id}","----------End:" + duration + "毫秒----------")
        LogUtils.d(TAG, "----------End:" + duration + "毫秒----------")
        val newBody: ResponseBody = bodyString.toResponseBody(response.body?.contentType())
        return response.newBuilder()
            .body(newBody)
            .build()
    }
    //打印请求体
    private fun printRequest(request: Request) {
        LogUtils.d(TAG, "url=" + request.url)
        LogUtils.d(TAG, "method=" + request.method)
        printHeader("headers=", request.headers)
        if (request.method.compareTo("get", true) == 0 || request.method.compareTo(
                "delete",
                true
            ) == 0
        ) {
            val params = request.url.let { url ->
                url.queryParameterNames.associateWith {
                    url.queryParameter(it)
                }
            }.map { "${it.key}=${it.value}" }.joinToString("&")
            LogUtils.d(TAG, "params=${params}")
        } else {
            try {
                LogUtils.d(TAG, "params=${URLDecoder.decode(bodyToString(request), "UTF-8")}")
            } catch (e: Exception) {
                e.printStackTrace()
                LogUtils.e(TAG, "params=${bodyToString(request)}")
            }
        }
    }
    private fun printHeader(prefix: String, headers: Headers) {
        LogUtils.d(TAG, prefix + buildString {
            for (i in 0 until headers.size) {
                val name = headers.name(i)
                val value = headers.value(i)
                append(name)
                append(": ")
                append(value)
                append("\n")
            }
        })
    }
    private fun bodyToString(request: Request): String? {
@@ -79,8 +107,9 @@
                copy.body!!.writeTo(buffer)
            }
            buffer.readUtf8()
        } catch (e: IOException) {
            "did not work"
        } catch (e: Exception) {
            e.printStackTrace()
            null
        }
    }
gradle/libs.versions.toml
@@ -1,6 +1,7 @@
[versions]
agp = "8.12.3"
kotlin = "2.0.21"
huawei = "1.5.2.300"
coreKtx = "1.17.0"
appcompat = "1.7.1"
junit = "4.14-SNAPSHOT"
@@ -106,6 +107,7 @@
# 腾讯bugly
crashreport = { module = "com.tencent.bugly:crashreport", version.ref = "crashreport" }
nativecrashreport = { module = "com.tencent.bugly:nativecrashreport", version.ref = "crashreport" }
huawei-agconnect-core = { module = "com.huawei.agconnect:agconnect-core", version.ref = "huawei" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
settings.gradle.kts
@@ -16,6 +16,7 @@
        maven("https://jitpack.io")  // 私有仓库地址
        maven("https://repo1.maven.org/maven2/")
        maven("https://dl.bintray.com/cjt/maven")
        maven("https://developer.huawei.com/repo/")
    }
}
dependencyResolutionManagement {
@@ -36,7 +37,6 @@
        maven("https://maven.aliyun.com/nexus/content/repositories/releases/")
        // 配置HMS Core SDK的Maven仓地址,集成华为通道需要。
        maven("https://developer.huawei.com/repo/")
    }