feat(environment): 新增环境切换功能及相关优化

- 新增业务环境选择弹窗 EnvironmentSelectDialog,支持本地/测试/生产环境切换
- 实现连点三次屏幕触发环境切换弹窗功能,便于快速切换接口环境
- 切换环境时清空本地人脸库并重置增量同步状态,保证环境切换的完整生效
- 业务全局 BaseUrl 支持从持久化读取,首次无值时默认测试环境
- Retrofit Client 接口 BaseUrl 优化,自动规范尾部斜杠,保证 BaseUrl 合法
- 设备初始化界面增强,增加等待框触摸穿透,优化接口请求失败时的提示信息
- 提升人脸缓存拉取接口分页大小,默认请求 500 条数据
- 修正登录界面布局细节,隐藏密码切换按钮,调整控件顺序及样式细节
This commit is contained in:
mazengfei
2026-09-02 09:59:48 +08:00
parent d33f941d12
commit 57345f8750
7 changed files with 197 additions and 18 deletions
@@ -21,6 +21,7 @@ object GlobalData {
* 竖排数量 * 竖排数量
*/ */
var arrayVertical: Int = 11 var arrayVertical: Int = 11
/** /**
* 排列方式 0 垂直 1 水平 * 排列方式 0 垂直 1 水平
*/ */
@@ -38,9 +39,9 @@ object GlobalData {
/** /**
* 具体业务 BaseUrl * 具体业务 BaseUrl
*/ */
const val LOCAL_BASE_URL: String = "http://192.168.10.101:24801" const val LOCAL_BASE_URL: String = "http://192.168.10.101:24801"//开发环境
const val TEST_BASE_URL: String = "https://dev.yixiong-tech.com:8081" const val TEST_BASE_URL: String = "https://dev.yixiong-tech.com:8081"//测试环境
const val PROD_BASE_URL: String = "https://platform-api.uat.shuziweidao.com" const val PROD_BASE_URL: String = "https://platform-api.uat.shuziweidao.com"//生产环境
/** /**
* 设备id * 设备id
@@ -70,3 +71,22 @@ object GlobalKey {
const val PARAM_EQUIPMENT_INFO = "equipmentUserInfo" const val PARAM_EQUIPMENT_INFO = "equipmentUserInfo"
const val KEY_BASE_URL = "baseUrlKey" const val KEY_BASE_URL = "baseUrlKey"
} }
/**
* 业务环境配置项
* @param name 环境名称(用于弹窗展示)
* @param url 环境 BaseUrl(不带尾斜杠)
*/
data class Environment(
val name: String,
val url: String
)
/**
* 预设环境列表(本地 / 测试 / 生产)
*/
val ENVIRONMENTS: List<Environment> = listOf(
Environment("本地", GlobalData.LOCAL_BASE_URL),
Environment("测试", GlobalData.TEST_BASE_URL),
Environment("生产", GlobalData.PROD_BASE_URL)
)
@@ -4,6 +4,7 @@ import android.util.Log
import com.sw.plate.App import com.sw.plate.App
import com.sw.plate.utils.AppUtil import com.sw.plate.utils.AppUtil
import com.sw.platecabinet.utils.CrashHandler import com.sw.platecabinet.utils.CrashHandler
import com.sw.platecabinet.utils.SpTool
import timber.log.Timber import timber.log.Timber
class MyApp : App() { class MyApp : App() {
@@ -33,6 +34,10 @@ class MyApp : App() {
GlobalData.appVersion = AppUtil.getAppVersionCode(this).toString() GlobalData.appVersion = AppUtil.getAppVersionCode(this).toString()
GlobalData.globalEquipmentCode = "202501171634" GlobalData.globalEquipmentCode = "202501171634"
// 从持久化读取业务 BaseUrl,首次无值兜底测试环境
GlobalData.appBaseUrl = SpTool.getBaseUrl().ifBlank { GlobalData.TEST_BASE_URL }
Timber.d("initGlobalData appBaseUrl=${GlobalData.appBaseUrl}")
// GlobalData.appBaseUrl = "http://192.168.1.201:14801" // GlobalData.appBaseUrl = "http://192.168.1.201:14801"
} }
} }
@@ -1,17 +1,34 @@
package com.sw.platecabinet.activity package com.sw.platecabinet.activity
import android.content.Intent import android.content.Intent
import android.os.Handler
import android.os.Looper
import android.view.MotionEvent
import android.view.WindowManager
import androidx.lifecycle.lifecycleScope
import com.sw.plate.utils.ToastUtils import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.facedb.FaceDatabase
import com.sw.platecabinet.Environment
import com.sw.platecabinet.GlobalData import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.dialog.EnvironmentSelectDialog
import com.sw.platecabinet.member.databinding.ActivityDeviceInitBinding import com.sw.platecabinet.member.databinding.ActivityDeviceInitBinding
import com.sw.platecabinet.member.databinding.ItemTitleTimeBinding import com.sw.platecabinet.member.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.utils.SpTool import com.sw.platecabinet.utils.SpTool
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
/** /**
* 设备初始化界面 * 设备初始化界面
*/ */
class DeviceInitActivity : BaseActivity<ActivityDeviceInitBinding>() { class DeviceInitActivity : BaseActivity<ActivityDeviceInitBinding>() {
// 连点切换环境的计数与复位
private var tapCount = 0
private val tapHandler = Handler(Looper.getMainLooper())
private val resetTapRunnable = Runnable { tapCount = 0 }
override fun inflateViewBinding(): ActivityDeviceInitBinding { override fun inflateViewBinding(): ActivityDeviceInitBinding {
return ActivityDeviceInitBinding.inflate(layoutInflater) return ActivityDeviceInitBinding.inflate(layoutInflater)
} }
@@ -21,7 +38,8 @@ class DeviceInitActivity : BaseActivity<ActivityDeviceInitBinding>() {
} }
override fun initialize() { override fun initialize() {
showWaitingDialog("加载中……") // 等待框设为不拦截触摸,确保连点手势可达
showWaitingDialog("加载中……")?.window?.addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE)
// 首次启动先全量拉取人脸缓存,后续走增量同步 // 首次启动先全量拉取人脸缓存,后续走增量同步
if (SpTool.getFirstGetFace()) { if (SpTool.getFirstGetFace()) {
netViewModelV2.getUserFaceCache { status, msg -> netViewModelV2.getUserFaceCache { status, msg ->
@@ -33,6 +51,50 @@ class DeviceInitActivity : BaseActivity<ActivityDeviceInitBinding>() {
} }
} }
/**
* 连续点击 3 次(500ms 内)触发环境切换弹窗
*/
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
if (ev.action == MotionEvent.ACTION_DOWN) {
tapCount++
tapHandler.removeCallbacks(resetTapRunnable)
tapHandler.postDelayed(resetTapRunnable, 500)
if (tapCount >= 3) {
tapCount = 0
tapHandler.removeCallbacks(resetTapRunnable)
showEnvironmentDialog()
return true
}
}
return super.dispatchTouchEvent(ev)
}
/**
* 弹出环境选择弹窗
*/
private fun showEnvironmentDialog() {
EnvironmentSelectDialog(this) { env ->
switchEnvironment(env)
}.show()
}
/**
* 切换环境:持久化 + 清空本地人脸库 + 重置增量同步状态
*/
private fun switchEnvironment(env: Environment) {
SpTool.setBaseUrl(env.url)
lifecycleScope.launch(Dispatchers.IO) {
val faceDao = FaceDatabase.getInstance(this@DeviceInitActivity).faceDao()
faceDao.deleteAll()
faceDao.resetId()
SpTool.setLastFaceTimestamp(0L)
SpTool.setFirstGetFace(true)
withContext(Dispatchers.Main) {
ToastUtils.showToast("已切换到${env.name},重启后生效")
}
}
}
/** /**
* 获取设备配置(V2),并初始化虹软 SDK 激活参数 * 获取设备配置(V2),并初始化虹软 SDK 激活参数
*/ */
@@ -41,8 +103,8 @@ class DeviceInitActivity : BaseActivity<ActivityDeviceInitBinding>() {
runOnUiThread { runOnUiThread {
if (deviceConfig == null) { if (deviceConfig == null) {
hideWaitingDialog() hideWaitingDialog()
ToastUtils.showToast("获取设备配置数据失败") ToastUtils.showToast("获取设备配置失败,请连点屏幕3次切换环境")
goLoginActivity() // goLoginActivity()
return@runOnUiThread return@runOnUiThread
} }
GlobalData.appId = deviceConfig.arcsoftAppId ?: "" GlobalData.appId = deviceConfig.arcsoftAppId ?: ""
@@ -56,8 +118,9 @@ class DeviceInitActivity : BaseActivity<ActivityDeviceInitBinding>() {
}, onFailure = { errMsg -> }, onFailure = { errMsg ->
runOnUiThread { runOnUiThread {
hideWaitingDialog() hideWaitingDialog()
ToastUtils.showToast(errMsg ?: "接口异常") Timber.e("getDeviceConfig onFailure: $errMsg")
goLoginActivity() ToastUtils.showToast("服务连接失败,请连点屏幕3次切换环境")
// goLoginActivity()
} }
}) })
} }
@@ -0,0 +1,81 @@
package com.sw.platecabinet.dialog
import android.app.Dialog
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.view.Gravity
import android.view.View
import android.view.Window
import android.widget.LinearLayout
import android.widget.RadioButton
import androidx.fragment.app.FragmentActivity
import com.sw.platecabinet.ENVIRONMENTS
import com.sw.platecabinet.Environment
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.ext.dp
import com.sw.platecabinet.member.databinding.DialogEnvironmentSelectBinding
/**
* 业务环境选择弹窗
*/
class EnvironmentSelectDialog(
private val activity: FragmentActivity,
private val onConfirm: (Environment) -> Unit
) : Dialog(activity) {
private lateinit var binding: DialogEnvironmentSelectBinding
private var selected: Environment? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestWindowFeature(Window.FEATURE_NO_TITLE)
binding = DialogEnvironmentSelectBinding.inflate(layoutInflater)
setContentView(binding.root)
window?.run {
attributes = attributes.apply { width = 480.dp }
setBackgroundDrawable(ColorDrawable())
setCancelable(true)
}
renderEnvironments()
addListener()
}
/**
* 渲染环境选项,高亮当前环境
*/
private fun renderEnvironments() {
val current = GlobalData.appBaseUrl
ENVIRONMENTS.forEach { env ->
val rb = RadioButton(activity).apply {
id = View.generateViewId()
text = env.name
gravity = Gravity.CENTER_VERTICAL
textSize = 18f
setTextColor(Color.WHITE)
buttonTintList = ColorStateList.valueOf(Color.WHITE)
isChecked = env.url == current
setOnClickListener { selected = env }
}
binding.rgEnvironments.addView(
rb,
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply { topMargin = 8.dp }
)
}
selected = ENVIRONMENTS.firstOrNull { it.url == current }
}
/**
* 确认按钮:回调所选环境并关闭弹窗
*/
private fun addListener() {
binding.tvConfirm.setOnClickListener {
selected?.let(onConfirm)
dismiss()
}
}
}
@@ -29,7 +29,7 @@ object ApiClient {
private val retrofit by lazy { private val retrofit by lazy {
Retrofit.Builder() Retrofit.Builder()
.baseUrl(GlobalData.appBaseUrl) .baseUrl(resolveBaseUrl())
.client(okHttpClient) .client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create())
.build() .build()
@@ -42,4 +42,13 @@ object ApiClient {
val apiServiceV2: ApiServiceV2 by lazy { val apiServiceV2: ApiServiceV2 by lazy {
retrofit.create(ApiServiceV2::class.java) retrofit.create(ApiServiceV2::class.java)
} }
/**
* 规范化 Retrofit 要求的 baseUrl:非空且以 / 结尾。
* 所有接口走 @Url 全路径,此值不参与实际拼接,只需合法。
*/
private fun resolveBaseUrl(): String {
val url = GlobalData.appBaseUrl.ifBlank { "http://localhost/" }
return if (url.endsWith("/")) url else "$url/"
}
} }
@@ -22,7 +22,7 @@ class RemoteRepositoryV2 constructor(
*/ */
suspend fun getUserFaceCache( suspend fun getUserFaceCache(
pageNum: Int, pageNum: Int,
pageSize: Int = 100, pageSize: Int = 500,
): ApiResponse<List<UserFaceModelV2>> { ): ApiResponse<List<UserFaceModelV2>> {
return safeApiCall { return safeApiCall {
apiService.getUserFaceCache( apiService.getUserFaceCache(
@@ -59,9 +59,9 @@
android:layout_gravity="center_horizontal" android:layout_gravity="center_horizontal"
android:layout_marginTop="129dp" android:layout_marginTop="129dp"
android:text="请正视屏幕" android:text="请正视屏幕"
android:textStyle="bold"
android:textColor="#FFCC99" android:textColor="#FFCC99"
android:textSize="36sp" /> android:textSize="36sp"
android:textStyle="bold" />
<LinearLayout <LinearLayout
android:id="@+id/ll_to_pwd" android:id="@+id/ll_to_pwd"
@@ -72,7 +72,8 @@
android:background="@drawable/btn_outline" android:background="@drawable/btn_outline"
android:gravity="center" android:gravity="center"
android:paddingHorizontal="45dp" android:paddingHorizontal="45dp"
android:paddingVertical="18dp"> android:paddingVertical="18dp"
android:visibility="gone">
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
@@ -86,12 +87,12 @@
android:id="@+id/tv_to_init" android:id="@+id/tv_to_init"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
tools:text="30"
android:padding="12dp"
android:gravity="center"
android:layout_marginTop="185dp" android:layout_marginTop="185dp"
android:gravity="center"
android:padding="12dp"
android:textColor="#FFCC99" android:textColor="#FFCC99"
android:textSize="36sp" /> android:textSize="36sp"
tools:text="30" />
</FrameLayout> </FrameLayout>
</LinearLayout> </LinearLayout>