Compare commits
3
Commits
d53ea8f798
...
57345f8750
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57345f8750 | ||
|
|
d33f941d12 | ||
|
|
41cfec0583 |
@@ -21,6 +21,7 @@ object GlobalData {
|
||||
* 竖排数量
|
||||
*/
|
||||
var arrayVertical: Int = 11
|
||||
|
||||
/**
|
||||
* 排列方式 0 垂直 1 水平
|
||||
*/
|
||||
@@ -38,9 +39,9 @@ object GlobalData {
|
||||
/**
|
||||
* 具体业务 BaseUrl
|
||||
*/
|
||||
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 PROD_BASE_URL: String = "https://platform-api.uat.shuziweidao.com"
|
||||
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 PROD_BASE_URL: String = "https://platform-api.uat.shuziweidao.com"//生产环境
|
||||
|
||||
/**
|
||||
* 设备id
|
||||
@@ -69,4 +70,23 @@ object GlobalKey {
|
||||
const val KEY_USER_INFO = "userInfoKey"
|
||||
const val PARAM_EQUIPMENT_INFO = "equipmentUserInfo"
|
||||
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.utils.AppUtil
|
||||
import com.sw.platecabinet.utils.CrashHandler
|
||||
import com.sw.platecabinet.utils.SpTool
|
||||
import timber.log.Timber
|
||||
|
||||
class MyApp : App() {
|
||||
@@ -33,6 +34,10 @@ class MyApp : App() {
|
||||
GlobalData.appVersion = AppUtil.getAppVersionCode(this).toString()
|
||||
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"
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,34 @@
|
||||
package com.sw.platecabinet.activity
|
||||
|
||||
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.arcface.facedb.FaceDatabase
|
||||
import com.sw.platecabinet.Environment
|
||||
import com.sw.platecabinet.GlobalData
|
||||
import com.sw.platecabinet.dialog.EnvironmentSelectDialog
|
||||
import com.sw.platecabinet.member.databinding.ActivityDeviceInitBinding
|
||||
import com.sw.platecabinet.member.databinding.ItemTitleTimeBinding
|
||||
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>() {
|
||||
|
||||
// 连点切换环境的计数与复位
|
||||
private var tapCount = 0
|
||||
private val tapHandler = Handler(Looper.getMainLooper())
|
||||
private val resetTapRunnable = Runnable { tapCount = 0 }
|
||||
|
||||
override fun inflateViewBinding(): ActivityDeviceInitBinding {
|
||||
return ActivityDeviceInitBinding.inflate(layoutInflater)
|
||||
}
|
||||
@@ -21,7 +38,8 @@ class DeviceInitActivity : BaseActivity<ActivityDeviceInitBinding>() {
|
||||
}
|
||||
|
||||
override fun initialize() {
|
||||
showWaitingDialog("加载中……")
|
||||
// 等待框设为不拦截触摸,确保连点手势可达
|
||||
showWaitingDialog("加载中……")?.window?.addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE)
|
||||
// 首次启动先全量拉取人脸缓存,后续走增量同步
|
||||
if (SpTool.getFirstGetFace()) {
|
||||
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 激活参数
|
||||
*/
|
||||
@@ -41,8 +103,8 @@ class DeviceInitActivity : BaseActivity<ActivityDeviceInitBinding>() {
|
||||
runOnUiThread {
|
||||
if (deviceConfig == null) {
|
||||
hideWaitingDialog()
|
||||
ToastUtils.showToast("获取设备配置数据失败")
|
||||
goLoginActivity()
|
||||
ToastUtils.showToast("获取设备配置失败,请连点屏幕3次切换环境")
|
||||
// goLoginActivity()
|
||||
return@runOnUiThread
|
||||
}
|
||||
GlobalData.appId = deviceConfig.arcsoftAppId ?: ""
|
||||
@@ -56,8 +118,9 @@ class DeviceInitActivity : BaseActivity<ActivityDeviceInitBinding>() {
|
||||
}, onFailure = { errMsg ->
|
||||
runOnUiThread {
|
||||
hideWaitingDialog()
|
||||
ToastUtils.showToast(errMsg ?: "接口异常")
|
||||
goLoginActivity()
|
||||
Timber.e("getDeviceConfig onFailure: $errMsg")
|
||||
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 {
|
||||
Retrofit.Builder()
|
||||
.baseUrl(GlobalData.appBaseUrl)
|
||||
.baseUrl(resolveBaseUrl())
|
||||
.client(okHttpClient)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build()
|
||||
@@ -42,4 +42,13 @@ object ApiClient {
|
||||
val apiServiceV2: ApiServiceV2 by lazy {
|
||||
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(
|
||||
pageNum: Int,
|
||||
pageSize: Int = 100,
|
||||
pageSize: Int = 500,
|
||||
): ApiResponse<List<UserFaceModelV2>> {
|
||||
return safeApiCall {
|
||||
apiService.getUserFaceCache(
|
||||
|
||||
@@ -59,9 +59,9 @@
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="129dp"
|
||||
android:text="请正视屏幕"
|
||||
android:textStyle="bold"
|
||||
android:textColor="#FFCC99"
|
||||
android:textSize="36sp" />
|
||||
android:textSize="36sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_to_pwd"
|
||||
@@ -72,7 +72,8 @@
|
||||
android:background="@drawable/btn_outline"
|
||||
android:gravity="center"
|
||||
android:paddingHorizontal="45dp"
|
||||
android:paddingVertical="18dp">
|
||||
android:paddingVertical="18dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
@@ -86,12 +87,12 @@
|
||||
android:id="@+id/tv_to_init"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="30"
|
||||
android:padding="12dp"
|
||||
android:gravity="center"
|
||||
android:layout_marginTop="185dp"
|
||||
android:gravity="center"
|
||||
android:padding="12dp"
|
||||
android:textColor="#FFCC99"
|
||||
android:textSize="36sp" />
|
||||
android:textSize="36sp"
|
||||
tools:text="30" />
|
||||
|
||||
</FrameLayout>
|
||||
</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