18034727885
2026-05-09 bee2ef2d524e756d70140b007b18556913bb51fd
新增翼虎登录相关逻辑功能
6个文件已添加
6个文件已修改
354 ■■■■■ 已修改文件
app/build.gradle.kts 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/MyApp.kt 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/di/Koin.kt 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/extension/StringExtension.kt 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/model/YiHuLoginInfo.kt 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/net/YiHuApi.kt 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/net/YiHuRepository.kt 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/net/interceptor/YiHuHeaderInterceptor.kt 115 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/ui/user/login/LoginActivity.kt 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/ui/user/login/LoginVM.kt 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/utils/DeviceId.kt 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
common/src/main/java/com/xw/common/net/BaseHttpManager.kt 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/build.gradle.kts
@@ -84,6 +84,12 @@
            buildConfigField("String", "API_HOST", "\"http://221.182.45.100:8000/\"")
            buildConfigField("String", "WS_URL", "\"ws://221.182.45.100:8888/websocket\"")
            //翼虎appId
            buildConfigField("String", "YIHU_APPID", "\"RoboTaxi-admin-predeploy\"")
            //翼虎appSecret
            buildConfigField("String", "YIHU_APP_SECRET", "\"i6B5t4744XzO9Ed4\"")
            //翼虎网络域名
            buildConfigField("String", "YIHU_API_HOST", "\"https://xtigersso.deploy-test.xiaopeng.com/sso/\"")
        }
        create("local") { // 本地环境
            dimension = "api"
@@ -94,6 +100,12 @@
            buildConfigField("String", "API_HOST", "\"https://rt-mt.test.xiaopeng.local/\"")
            buildConfigField("String", "WS_URL", "\"ws://192.168.110.188:8888/websocket\"")
            //翼虎appId
            buildConfigField("String", "YIHU_APPID", "\"RoboTaxi-admin-test\"")
            //翼虎appSecret
            buildConfigField("String", "YIHU_APP_SECRET", "\"SXa7vEe7V23Xh3RN\"")
            //翼虎网络域名
            buildConfigField("String", "YIHU_API_HOST", "\"http://web-dev.3d.xiaopeng.local:9104/sso/\"")
        }
        create("uat") { // 预发环境
            dimension = "api"
@@ -104,6 +116,12 @@
            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_API_HOST", "\"https://xtigersso.deploy-test.xiaopeng.com/sso/\"")
        }
        create("prd") { // 正式环境
            dimension = "api"
@@ -114,6 +132,12 @@
            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_API_HOST", "\"https://xpsso.xiaopeng.com/sso/\"")
        }
    }
app/src/main/java/com/xw/yunwei/MyApp.kt
@@ -39,6 +39,7 @@
import com.xw.yunwei.ui.dialog.NewTaskDialog
import com.xw.yunwei.ui.task.accident.detail.AccidentDetailActivity
import com.xw.yunwei.ui.task.normal.NormalTaskDetailActivity
import com.xw.yunwei.utils.DeviceId
import com.xw.yunwei.utils.TTSController
import com.xw.yunwei.websocket.WebSocketManager
import kotlinx.coroutines.CompletableDeferred
@@ -97,6 +98,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 {
@@ -39,5 +42,9 @@
    }
    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/StringExtension.kt
New file
@@ -0,0 +1,17 @@
package com.xw.yunwei.extension
import java.security.MessageDigest
//扩展函数 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()
        ""
    }
}
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/YiHuApi.kt
New file
@@ -0,0 +1,20 @@
package com.xw.yunwei.net
import com.xw.common.net.interceptor.HttpResponse
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 getLoginCode(@Body body: RequestBody): Call<HttpResponse<Any>>
    /*手机验证码登录*/
    @POST("app/sms/login")
    fun login(@Body body: RequestBody): Call<HttpResponse<YiHuLoginInfo>>
}
app/src/main/java/com/xw/yunwei/net/YiHuRepository.kt
New file
@@ -0,0 +1,30 @@
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 getLoginCode(phone: String) = flow {
        val map = mapOf<String, Any?>(
            "phone" to phone,
            "smsType" to "LOGIN",
            "clientId" to BuildConfig.YIHU_APPID
        )
        emit(api.getLoginCode(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.login(getBody(map)).interceptAwait())
    }
}
app/src/main/java/com/xw/yunwei/net/interceptor/YiHuHeaderInterceptor.kt
New file
@@ -0,0 +1,115 @@
package com.xw.yunwei.net.interceptor
import android.util.Log
import com.xw.yunwei.BuildConfig
import com.xw.yunwei.extension.md5
import com.xw.yunwei.utils.DeviceId
import okhttp3.FormBody
import okhttp3.Interceptor
import okhttp3.Response
import okio.Buffer
import java.nio.charset.StandardCharsets
import java.util.Locale
import javax.crypto.Mac
import javax.crypto.SecretKey
import javax.crypto.spec.SecretKeySpec
//翼虎网络接口header拦截器
class YiHuHeaderInterceptor : Interceptor {
    private val TAG = "YiHuHeaderInterceptor"
    override fun intercept(chain: Interceptor.Chain): Response {
        val original = chain.request()
        val publicHeader = mutableMapOf(
            "Content-Type" to "application/json",
            "XT-Client-Type" to "Android",
            "XT-DeviceId" to DeviceId.get(),
            "XT-Nonce" to System.currentTimeMillis().toString(),
            "XT-AppId" to BuildConfig.YIHU_APPID,
        )
        original.headers.forEach {
            publicHeader[it.first] = it.second
        }
        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
        when {
            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 = encryptHmacSHA256(strToSign)
        publicHeader["XT-Signature"] = sign
        val request = original.newBuilder()
        publicHeader.forEach { (key, value) -> request.addHeader(key, value) }
        return chain.proceed(request.build())
    }
    // 签名入口, data - 需要签名的字符串; secretKey -
    private fun encryptHmacSHA256(data: String): String {
        val encodedData = encryptHmac(data, BuildConfig.YIHU_APP_SECRET, "HmacSHA256") ?: return ""
        return byte2hexString(encodedData)
    }
    private fun encryptHmac(data: String, secret: String, algorithm: String?): ByteArray? {
        try {
            val secretKey: SecretKey =
                SecretKeySpec(secret.toByteArray(StandardCharsets.UTF_8), algorithm)
            val mac = Mac.getInstance(secretKey.algorithm)
            mac.init(secretKey)
            return mac.doFinal(data.toByteArray(StandardCharsets.UTF_8))
        } catch (e: Exception) {
            e.printStackTrace()
        }
        return null
    }
    private fun byte2hexString(bytes: ByteArray): String {
        val sign = StringBuilder()
        for (aByte in bytes) {
            val hex = Integer.toHexString(aByte.toInt() and 0xFF)
            if (hex.length == 1) {
                sign.append('0')
            }
            sign.append(hex.lowercase(Locale.getDefault()))
        }
        return sign.toString()
    }
}
app/src/main/java/com/xw/yunwei/ui/user/login/LoginActivity.kt
@@ -45,10 +45,10 @@
            MyApp.myApp.unbindPushAccount()
            val tab1 = binding.tabLayout.newTab().apply { text = "验证码登录" }
            val tab2 = binding.tabLayout.newTab().apply { text = "密码登录" }
            //val tab2 = binding.tabLayout.newTab().apply { text = "密码登录" }
            binding.tabLayout.addTab(tab1, true)
            binding.tabLayout.addTab(tab2, false)
            //binding.tabLayout.addTab(tab2, false)
            viewModel.getAgreeConfig()
        }
app/src/main/java/com/xw/yunwei/ui/user/login/LoginVM.kt
@@ -4,6 +4,7 @@
import android.os.CountDownTimer
import android.view.View
import androidx.lifecycle.MutableLiveData
import com.amap.location.support.util.TextUtils
import com.xw.common.Constants
import com.xw.common.base.BaseViewModel
import com.xw.common.utils.KeyPreferences
@@ -12,12 +13,15 @@
import com.xw.yunwei.MyApp
import com.xw.yunwei.R
import com.xw.yunwei.model.AgreeInfo
import com.xw.yunwei.model.YiHuLoginInfo
import com.xw.yunwei.net.UserRepository
import com.xw.yunwei.net.YiHuRepository
import com.xw.yunwei.ui.MainActivity
import org.koin.core.component.inject
class LoginVM(application: Application) : BaseViewModel(application) {
    val userRepository: UserRepository by inject()
    val yiHuRepository: YiHuRepository by inject()
    val tabIndex = MutableLiveData(0)
@@ -68,21 +72,32 @@
            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)
                MyApp.myApp.connectWebSocket()
                startActivity(MainActivity::class.java)
                finish()
        launchRequestFlow(
            tips = "登录中...",
            flowBlock = {
                yiHuRepository.login(phone.value!!, code.value!!)
            },
            onCollect = { yiHuData ->
                yiHuData?.let {
                    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)
                            MyApp.myApp.connectWebSocket()
                            startActivity(MainActivity::class.java)
                            finish()
                        }
                    }
                }
            }
        }
        )
    }
    private fun loginByPassword() {
@@ -126,16 +141,31 @@
            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("验证码已发送")
            }
//            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("验证码已发送")
//            }
            launchRequestFlow(
                tips = "验证码获取中...",
                flowBlock = {
                    yiHuRepository.getLoginCode(phone.value!!)
                },
                error = { code, msg ->
                    canGetCode.postValue(true)
                    timer.cancel()
                    codeTips.postValue("重新发送")
                    ToastUtilCommon.showError(msg)
                },
                onCollect = { data ->
                    ToastUtilCommon.showSuc("验证码已发送")
                }
            )
        }
    }
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) ?: ""
    }
}
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()
    }