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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user