feat(api): 新增V2版本接口支持并集成至网络请求客户端

- 新增 ApiServiceV2 接口,支持人脸缓存、增量数据、设备配置等接口
- ApiClient 中新增 apiServiceV2 对象,复用 Retrofit 实例,调整超时时间至60秒
- DeviceInitActivity 改用 netViewModelV2 进行人脸缓存全量拉取及设备配置读取
- BaseActivity 新增对人脸增量同步接口调用,调用成功后刷新本地识别缓存
- BaseActivity 新增清空本地人脸库功能,异步清理数据库并刷新识别缓存
- FaceApi 增加清空本地人脸库接口,重写数据库操作逻辑
- 升级 lib_face 模块 Room 数据库版本,新增字段以支持多标识人脸实体扩展
- FaceEntity 实体扩展会员编号、用户ID、人脸ID、会员标识、更新时间等字段
- FaceDao 新增按人脸ID查询和删除接口,支持多重人脸数据操作
- 优化网络层 OkHttpClient 配置,简化拦截器写法及日志级别判断
- 升级 lib_face 模块支持 arm64-v8a 架构,增强兼容性
- 规范模块间依赖关系,统一版本管理及包路径声明
- 完善 CLAUDE.md 文档,补充项目架构、模块划分与开发流程说明
This commit is contained in:
mazengfei
2026-08-18 17:14:06 +08:00
parent 5b8db65fa2
commit d53ea8f798
41 changed files with 1786 additions and 258 deletions
@@ -17,11 +17,9 @@ import androidx.core.view.WindowCompat
import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding
import com.sw.inbound.utils.DateTimeUtils
import com.sw.plate.utils.Base64
import com.sw.plate.utils.ScanGunKeyEventHelper
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.facedb.FaceDatabase
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
import com.sw.plate.utils.comn.SerialApi
import com.sw.plate.utils.comn.SerialPortManager
@@ -30,17 +28,18 @@ import com.sw.platecabinet.member.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.dialog.BalanceNotEnoughDialog
import com.sw.platecabinet.ext.setClickListeners
import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.model.response.UserFaceModel
import com.sw.platecabinet.utils.IntervalExecutor
import com.sw.platecabinet.utils.PermissionHelper
import com.sw.platecabinet.utils.SpTool
import com.sw.platecabinet.view.CustomLoadingDialog
import com.sw.platecabinet.viewmodel.NetViewModelV2
import com.sw.platecabinet.viewmodel.SettingViewModel
import com.sw.platecabinet.viewmodel.UserViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
/**
@@ -61,6 +60,9 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
// 用户对应的viewModel
protected val viewModel by viewModels<UserViewModel>()
// V2 网络请求 viewModel(人脸缓存/增量同步/设备配置)
val netViewModelV2 by viewModels<NetViewModelV2>()
private var startTime: Long = 0
override fun onCreate(savedInstanceState: Bundle?) {
@@ -84,8 +86,6 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
registerKeyEvent()
val durationTime = System.currentTimeMillis() - startTime
Timber.d("启动时间:$durationTime")
startFaceTask()
}
/**
@@ -396,6 +396,9 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
// private val dealyMillis = 10 * 60 * 1000L
private val initialDelay = 5 * 60 * 1000L
private val dealyMillis = 5 * 60 * 1000L
val recognizeViewModel by viewModels<RecognizeViewModel>()
fun startFaceTask() {
faceTaskJob =
intervalExecutor.startIntervalTaskWithInitialDelay(initialDelay, dealyMillis) {
@@ -403,70 +406,33 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
}
}
private var faceTimestamp = 0L
/**
* 人脸增量同步(V2,同步逻辑在 NetViewModelV2 内部处理)
*/
private fun getFaceIncrementList(pageNo: Int = 1) {
val timestamp = SpTool.getLastFaceTimestamp()
if (timestamp == 0L) {
return
}
settingViewModel.getFaceIncrementList(
netViewModelV2.getFaceIncrementList(
pageNo = pageNo,
timestamp = timestamp
) { list ->
runOnUiThread {
if (pageNo == 1 && list.isEmpty()) {
//未查询到增量数据
return@runOnUiThread
}
updateFaceData(list)
// if (list.size >= settingViewModel.PAGE_SIZE) {
// faceTimestamp = list.last().faceUpdateTimestamp?:0
// taskPageNo++
// getFaceIncrementList()
// return@runOnUiThread
// }
if (list.isNotEmpty()) {
faceTimestamp = list.last().faceUpdateTimestamp?:0
}
SpTool.setLastFaceTimestamp(faceTimestamp)
}
) {
recognizeViewModel.refreshFaceList()
}
}
private val recognizeViewModel by viewModels<RecognizeViewModel>()
private fun updateFaceData(list: List<UserFaceModel>) {
Thread {
val faceList = mutableListOf<FaceEntity>()
try {
list.forEach { model ->
if (model.faceDeleted == true) {
//删除数据
FaceDatabase.getInstance(this).faceDao().deleteFaceById(model.userId)
} else {
//保存数据
val faceEntity = FaceEntity(
model.userId,
null,
Base64.decode(model.faceFeatureStr)
).also {
it.userType = "1"
}
faceList.add(faceEntity)
}
}
} catch (e: Exception) {
e.printStackTrace()
}
try {
if (faceList.isNotEmpty()) {
FaceDatabase.getInstance(this).faceDao().insert(faceList)
}
recognizeViewModel.refreshFaceList()
} catch (e: Exception) {
e.printStackTrace()
}
}.start()
/**
* 清空本地人脸库
*/
fun clearAllFace(block: () -> Unit) {
lifecycleScope.launch(Dispatchers.IO) {
val faceDao = FaceDatabase.getInstance(this@BaseActivity).faceDao()
faceDao.deleteAll()
faceDao.resetId()
recognizeViewModel.refreshFaceList()
withContext(Dispatchers.Main) { block() }
}
}
}
@@ -1,16 +1,11 @@
package com.sw.platecabinet.activity
import android.content.Intent
import android.util.Log
import androidx.activity.viewModels
import com.sw.plate.utils.AppUtil
import com.sw.plate.utils.ToastUtils
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.member.databinding.ActivityDeviceInitBinding
import com.sw.platecabinet.member.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.ext.gone
import com.sw.platecabinet.ext.invisible
import com.sw.platecabinet.viewmodel.SettingViewModel
import com.sw.platecabinet.utils.SpTool
/**
* 设备初始化界面
@@ -26,48 +21,45 @@ class DeviceInitActivity : BaseActivity<ActivityDeviceInitBinding>() {
}
override fun initialize() {
// val isSuccess = deviceViewModel.checkEquipmentInfo()
// if (isSuccess) {
// goLoginActivity()
// return
// }
// binding.ivQrCode.setImageBitmap(
// QRCodeUtil.generateQRCode(
// content = GlobalData.deviceId,
// size = 200
// )
// )
// binding.btnInit.setOnClickListener {
// deviceViewModel.getDeviceToken()
// }
// binding.btnInit.gone()
// binding.ivQrCode.invisible()
// binding.root.postDelayed({
// goLoginActivity()
// }, 1000)
// showWaitingDialog("加载中……")
// viewModel.getDeviceConfig { deviceConfig ->
// runOnUiThread {
// if (deviceConfig == null) {
// hideWaitingDialog()
// ToastUtils.showToast("获取设备配置数据失败")
// hideWaitingDialog()
// goLoginActivity()
// return@runOnUiThread
// }
// GlobalData.appId = deviceConfig.arcsoftAppId ?: ""
// GlobalData.sdkKey = deviceConfig.arcsoftSdkKey ?: ""
// GlobalData.activeKey = deviceConfig.arcsoftActiveKey ?: ""
// binding.root.postDelayed({
// hideWaitingDialog()
// goLoginActivity()
// }, 500)
// }
// }
showWaitingDialog("加载中……")
// 首次启动先全量拉取人脸缓存,后续走增量同步
if (SpTool.getFirstGetFace()) {
netViewModelV2.getUserFaceCache { status, msg ->
SpTool.setFirstGetFace(false)
getDeviceConfig()
}
} else {
getDeviceConfig()
}
}
binding.root.postDelayed({
goLoginActivity()
}, 500)
/**
* 获取设备配置(V2),并初始化虹软 SDK 激活参数
*/
private fun getDeviceConfig() {
netViewModelV2.getDeviceConfig(onSuccess = { deviceConfig ->
runOnUiThread {
if (deviceConfig == null) {
hideWaitingDialog()
ToastUtils.showToast("获取设备配置数据失败")
goLoginActivity()
return@runOnUiThread
}
GlobalData.appId = deviceConfig.arcsoftAppId ?: ""
GlobalData.sdkKey = deviceConfig.arcsoftSdkKey ?: ""
GlobalData.activeKey = deviceConfig.arcsoftActiveKey ?: ""
binding.root.postDelayed({
hideWaitingDialog()
goLoginActivity()
}, 500)
}
}, onFailure = { errMsg ->
runOnUiThread {
hideWaitingDialog()
ToastUtils.showToast(errMsg ?: "接口异常")
goLoginActivity()
}
})
}
override fun registerDataChange() {
@@ -24,11 +24,13 @@ import com.sw.plate.utils.arcface.PreviewConfig
import com.sw.plate.utils.arcface.camera.CameraListener
import com.sw.plate.utils.arcface.camera.DualCameraHelper
import com.sw.plate.utils.arcface.face.constants.LivenessType
import com.sw.plate.utils.arcface.face.model.CompareResult
import com.sw.plate.utils.arcface.face.model.FacePreviewInfo
import com.sw.plate.utils.arcface.face.model.RecognizeConfiguration
import com.sw.plate.utils.arcface.faceserver.FaceServer
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
import com.sw.platecabinet.utils.Debouncer
import com.sw.platecabinet.member.R
import com.sw.platecabinet.member.databinding.ActivityLoginFaceBinding
import com.sw.platecabinet.member.databinding.ItemTitleTimeBinding
@@ -45,7 +47,6 @@ import timber.log.Timber
*/
class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
ViewTreeObserver.OnGlobalLayoutListener {
private val recognizeViewModel by viewModels<RecognizeViewModel>()
private var countDownTimer: CountDownTimer? = null
private val CAMERA_PERMISSION_REQUEST_CODE = 100
@@ -73,16 +74,17 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
override fun initialize() {
instance = this
viewModel.activeEngine()
// V2 引擎激活(appId/sdkKey/activeKey 来自 V2 设备配置)
netViewModelV2.activeEngine()
initCountTime()
initArcViewModel()
initArcView()
openRectInfoDraw = true
recognizeViewModel.setDrawRectInfoTextValue(true)
//viewModel.generateToken()
// viewModel.activeEngine()
viewModel.getUserFaceCache()
//开启人脸增量数据定时任务(V2
startFaceTask()
binding.llToPwd.setOnClickListener {
val intent = Intent(this, LoginByPwdActivity::class.java)
@@ -171,9 +173,12 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
Timber.i("recognizeNotice observe notice = $notice")
})
recognizeViewModel.recognizeUserId.observe(this, Observer { userId: String? ->
Timber.i("recognizeUserId observe userId = $userId")
viewModel.getUserInfoById(memberId = userId)
recognizeViewModel.recognizeUserId.observe(this, Observer { result: CompareResult? ->
Timber.i(
"recognizeUserId observe userId=${result?.faceEntity?.userName}," +
"similar=${result?.similar},userType=${result?.faceEntity?.userType}"
)
loadFaceRecognizeResult(result)
})
recognizeViewModel.drawRectInfoText.observe(this, Observer { info ->
@@ -187,6 +192,47 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
recognizeViewModel.getCompareResultList().getValue()
}
/**
* 识别阈值,默认值为0.8
*/
private val faceSuccessThreshold by lazy { ConfigUtil.getRecognizeThreshold(this) }
/**
* 识别成功后开柜(查询用户信息)防抖,防止重复触发
*/
private val loginDebouncer = Debouncer(3000)
/**
* 处理识别结果:达到阈值走登录流程,未达阈值仅记录(本版本无采集模式)
*/
private fun loadFaceRecognizeResult(result: CompareResult?) {
val similar = result?.similar ?: return
Timber.i(
"loadFaceRecognizeResult 阈值=$faceSuccessThreshold, similar=$similar"
)
if (similar >= faceSuccessThreshold) {
val userId = result.faceEntity?.userName
recognizeSuccess(userId)
return
}
// 未达阈值:暂不处理(无采集模式),等待下一次识别
}
/**
* 识别成功:防抖后查询用户信息,走开柜门业务流程
*/
private fun recognizeSuccess(userId: String?) {
if (userId.isNullOrBlank()) {
Timber.i("recognizeSuccess 识别成功但 userId 为空,忽略")
return
}
Timber.i("recognizeSuccess userId=$userId")
loginDebouncer.debounce {
viewModel.getUserInfoById(memberId = userId)
}
}
override fun onDestroy() {
Timber.d("onDestroy")
instance = null
@@ -392,14 +438,24 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
}
private fun resumeCamera() {
isRecognition = true
if (rgbCameraHelper != null && rgbCameraHelper!!.isStopped) {
rgbCameraHelper!!.start()
val helper = rgbCameraHelper
if (helper != null && helper.isStopped) {
isRecognition = true
helper.start()
} else {
// 相机未停止时,发送黑帧清除 ViewModel 内部识别缓存,防止恢复后使用旧数据
recognizeViewModel.onPreviewFrame(emptyFrame, true)
binding.dualCameraFaceRectView.clearFaceInfo()
binding.root.postDelayed({
isRecognition = true
}, 500)
}
}
override fun onResume() {
super.onResume()
// 清空上次识别结果和 FaceHelper 内部状态,防止短时间内再次识别无法触发
recognizeViewModel.resetFaceState()
resumeCamera()
viewModel.resetUserInfo()
countDownTimer?.let {
@@ -416,10 +472,26 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
private fun pauseCamera() {
isRecognition = false
recognizeViewModel.onPreviewFrame(ByteArray(1382400), true)
recognizeViewModel.onPreviewFrame(emptyFrame, true)
}
/**
* 构造符合分辨率的 NV21 格式全黑帧
* @param width 图像宽度
* @param height 图像高度
* @return 合法的全黑帧字节数组
*/
private fun createBlackNV21Frame(width: Int, height: Int): ByteArray {
val frameSize = width * height
val nv21Data = ByteArray(frameSize * 3 / 2) // NV21 格式长度 = 宽×高×1.5
// Y 分量(亮度)设为 0(全黑),UV 分量设为 128(默认值)
nv21Data.fill(0, 0, frameSize)
nv21Data.fill(128.toByte(), frameSize, nv21Data.size)
return nv21Data
}
private val emptyFrame = createBlackNV21Frame(1280, 720)
override fun handleLoginSuccess(equipmentUserInfo: EquipmentUserInfo, isAdmin: Boolean) {
super.handleLoginSuccess(equipmentUserInfo, isAdmin)
// goInitActivity()