Compare commits
3
Commits
d53ea8f798
...
57345f8750
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57345f8750 | ||
|
|
d33f941d12 | ||
|
|
41cfec0583 |
@@ -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>
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
# 业务 BaseUrl 切换 — 设计文档
|
||||||
|
|
||||||
|
- 日期:2026-09-01
|
||||||
|
- 状态:已确认
|
||||||
|
- 关联模块:网络层 / 设备初始化 / 全局配置
|
||||||
|
|
||||||
|
## 1. 背景与目标
|
||||||
|
|
||||||
|
SmartTakePlate(发盘机)当前业务服务地址由 `GlobalData.appBaseUrl` 决定,但该字段:
|
||||||
|
- 默认值为空串,全局无任何活跃代码赋值(仅剩注释)。
|
||||||
|
- 已具备一套"半成品"基础设施(`SpTool.getBaseUrl/setBaseUrl`、`GlobalKey.KEY_BASE_URL`、
|
||||||
|
`LOCAL/TEST/PROD_BASE_URL` 常量),但从未接线。
|
||||||
|
|
||||||
|
目标:让现场运维人员能在**设备端**切换业务环境(本地 / 测试 / 生产),切换结果持久化,
|
||||||
|
重启后生效。
|
||||||
|
|
||||||
|
## 2. 需求
|
||||||
|
|
||||||
|
- **入口**:设备端隐藏入口,置于**登录前的初始化界面**(`DeviceInitActivity`)。
|
||||||
|
原因:若入口放在业务主界面,一旦配置的服务地址访问不到,业务界面因依赖网络请求成功才能
|
||||||
|
进入,将无法切回正确地址,形成死锁。
|
||||||
|
- **生效方式**:持久化到 SharedPreferences,重启 app 后生效(最简单可靠)。
|
||||||
|
- **选项来源**:仅预设环境列表(本地 / 测试 / 生产),不支持手输。
|
||||||
|
|
||||||
|
## 3. 现状分析
|
||||||
|
|
||||||
|
| 已存在 | 状态 |
|
||||||
|
|---|---|
|
||||||
|
| `GlobalData.appBaseUrl`(`var String = ""`) | 默认空串,无活跃赋值 |
|
||||||
|
| `LOCAL_BASE_URL` / `TEST_BASE_URL` / `PROD_BASE_URL` | 已定义,不带尾斜杠 |
|
||||||
|
| `GlobalKey.KEY_BASE_URL = "baseUrlKey"` | 已定义 |
|
||||||
|
| `SpTool.getBaseUrl() / setBaseUrl()` | 已实现,无调用方 |
|
||||||
|
| `ApiClient.retrofit`(`by lazy`,`baseUrl(GlobalData.appBaseUrl)`) | 用空串构建,切环境不重建 |
|
||||||
|
| `ApiService` / `ApiServiceV2` 全部方法 | 用 `@Url url = "${GlobalData.appBaseUrl}/..."` 默认参数,调用时求值 |
|
||||||
|
|
||||||
|
关键结论:
|
||||||
|
1. 所有请求地址由 `GlobalData.appBaseUrl` 在**每次调用时**经默认参数拼接决定,故切环境
|
||||||
|
只需改这一个变量(配合重启重建 `Retrofit`)。
|
||||||
|
2. `GlobalData.appBaseUrl=""` 会让 `Retrofit.Builder().baseUrl("")` 抛异常 —— 现有隐患。
|
||||||
|
|
||||||
|
## 4. 设计
|
||||||
|
|
||||||
|
### 4.1 组件划分与改动清单
|
||||||
|
|
||||||
|
| 文件 | 改动 |
|
||||||
|
|---|---|
|
||||||
|
| `GlobalData.kt` | 补充环境列表模型(名称 + URL),复用已有 `LOCAL/TEST/PROD_BASE_URL` |
|
||||||
|
| `MyApp.kt` | `initGlobalData()` 接线:读 `SpTool.getBaseUrl()`,兜底 `TEST_BASE_URL` |
|
||||||
|
| `DeviceInitActivity.kt` | 加隐藏连点手势 + 弹环境选择弹窗 |
|
||||||
|
| `ApiClient.kt` | 修复 Retrofit baseUrl 校验(补尾斜杠 / 占位),消除空串崩溃 |
|
||||||
|
| 新增 `EnvironmentSelectDialog`(或内联 Dialog) | 环境单选弹窗,高亮当前项 |
|
||||||
|
| `BaseActivity.kt`(复用) | 复用已有 `clearAllFace()` 清人脸库 |
|
||||||
|
|
||||||
|
### 4.2 关键设计决策
|
||||||
|
|
||||||
|
**决策 A — URL 规范化(消除现有隐患)**
|
||||||
|
|
||||||
|
- 约定 `GlobalData.appBaseUrl` **不带尾斜杠**,供 `@Url` 拼接得到形如
|
||||||
|
`https://dev.yixiong-tech.com:8081/terminal/...` 的完整地址。
|
||||||
|
- `Retrofit` 的 baseUrl 单独规范化:空则占位 `http://localhost/`,非空且无尾斜杠则补 `/`。
|
||||||
|
因所有请求走 `@Url` 全路径,Retrofit baseUrl 不参与实际拼接,只需合法即可。
|
||||||
|
|
||||||
|
**决策 B — 切换时清理环境残留**
|
||||||
|
|
||||||
|
选中环境后执行:
|
||||||
|
1. `SpTool.setBaseUrl(url)` 持久化;
|
||||||
|
2. 清空本地人脸库(`FaceDatabase` / `clearAllFace()`);
|
||||||
|
3. 重置人脸时间戳 `lastFaceTimestamp = 0`;
|
||||||
|
4. 重置 `firstGetFace = true`。
|
||||||
|
|
||||||
|
原因:不同环境的人脸数据与时间戳不同,不清理会导致重启后增量同步对不上。
|
||||||
|
|
||||||
|
另:ArcSoft 激活参数(`appId` / `sdkKey` / `activeKey`)无需单独处理——`DeviceInitActivity`
|
||||||
|
每次启动都会调用 `getDeviceConfig()` 从新环境重新获取并覆盖,切环境后自动适配。
|
||||||
|
|
||||||
|
**决策 C — 隐藏手势**
|
||||||
|
|
||||||
|
在 `DeviceInitActivity` 连续点击屏幕(如 5 次)触发,不新增可见 UI 元素。该界面登录前
|
||||||
|
可达、不依赖网络,满足"配错地址也能切回"的要求。
|
||||||
|
|
||||||
|
## 5. 数据流
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant User as 运维人员
|
||||||
|
participant App as MyApp/DeviceInit
|
||||||
|
participant Sp as SpTool(SharedPrefs)
|
||||||
|
participant Net as Retrofit/网络
|
||||||
|
|
||||||
|
App->>Sp: 启动时读 KEY_BASE_URL
|
||||||
|
alt 有值
|
||||||
|
Sp-->>App: 返回已存 URL
|
||||||
|
else 无值(首次)
|
||||||
|
Sp-->>App: 空 → 用 TEST_BASE_URL
|
||||||
|
end
|
||||||
|
App->>App: GlobalData.appBaseUrl = 结果
|
||||||
|
|
||||||
|
User->>App: 初始化界面连点5次
|
||||||
|
App->>App: 弹环境选择Dialog(高亮当前)
|
||||||
|
User->>App: 选择「生产」并确认
|
||||||
|
App->>Sp: setBaseUrl(PROD)
|
||||||
|
App->>App: 清人脸库 + 重置时间戳 + firstGetFace=true
|
||||||
|
App->>User: Toast「重启后生效」
|
||||||
|
User->>App: 重启 app
|
||||||
|
App->>Net: appBaseUrl 已指向生产 → 全量拉新环境人脸
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 错误处理
|
||||||
|
|
||||||
|
- **baseUrl 空 / 非法**:兜底 `TEST_BASE_URL`,并保证 Retrofit baseUrl 合法(占位/补斜杠)。
|
||||||
|
- **清人脸库失败**:非致命,仅记日志,不阻塞切换(重启后首次拉取仍会兜底全量)。
|
||||||
|
- **写 SharedPreferences 失败**:几乎不抛;Toast 提示用户重试。
|
||||||
|
|
||||||
|
## 7. 验证方式
|
||||||
|
|
||||||
|
- 构建:`./gradlew assembleDebug` 通过。
|
||||||
|
- 手动:初始化界面连点 5 次 → 弹窗 → 切「本地」→ 重启 → 抓包/日志确认请求打到新地址;
|
||||||
|
再切回「测试」验证可恢复。
|
||||||
|
|
||||||
Reference in New Issue
Block a user