Author SHA1 Message Date
mazengfei 86c3c07e42 隐藏测试代码 2026-03-12 11:10:26 +08:00
lvmengandClaude Sonnet 4.6 aa96455f66 refactor(face): 优化人脸特征存储与增量同步逻辑
- FaceDao 新增 queryAllByUserName 查询用户全部特征记录
- 改为查询所有记录后做去重判断,支持同一用户保存多条特征数据
- 将 Thread 替换为 lifecycleScope.launch(Dispatchers.IO) 协程
- 提取 getNewFaceEntity 方法消除重复代码
- 新增特征比对调试日志

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 16:02:13 +08:00
lvmengandClaude Sonnet 4.6 786f2a123c feat(face): 人脸库为空时自动进入新用户采集模式
- 新增 newUserCollect() 方法,人脸数据库记录为零时自动显示采集界面
- onResume 中异步查询人脸数量,据此决定进入采集模式或正常识别模式
- 修复 isFirstOpen 依赖 1000ms 延迟的竞态问题,改为立即置 false
- 顺带修改 FunActivity 按钮文案为「清除并拉取全量数据」

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 15:01:39 +08:00
lvmeng f2a6de55b9 优化默认值 2026-03-10 10:47:24 +08:00
lvmeng e51864d400 优化默认值 2026-03-10 10:45:16 +08:00
lvmeng e63d3f7624 修改会员字段名称 2026-03-10 08:32:18 +08:00
lvmengandClaude Sonnet 4.6 0a110020b9 refactor(face): 重构倒计时工具,统一使用 CountDownUtil 实例化方案
- CountDownUtil 由 object 单例改为 class,各调用方持有独立实例,互不干扰
- 修复 onCompletion 取消时误触发 onFinish 的 bug
- LoginByFaceActivity 新增 recognizeCountDownUtil / leftCountDownUtil 两个实例
- 移除 CountDownManager 及其所有调用,消除两套倒计时并存的混乱

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 15:50:36 +08:00
lvmeng f99538890c 代码优化 2026-03-06 15:30:25 +08:00
lvmeng 6d8ca7c83f 人脸增量接口改为间隔10秒执行 2026-03-05 16:21:45 +08:00
mazengfei c6bcdc03ba 增量时间戳为0不再限制 2026-03-05 12:38:48 +08:00
lvmeng 3b88b011a3 全量人脸数据改为首次打开app获取 2026-03-05 09:43:41 +08:00
mazengfei b96f9872cb 优化 2026-03-04 14:56:16 +08:00
lvmengandClaude Sonnet 4.6 eef13ff56e refactor(activity): 将 refreshFaceList 移至 insert 判断块外,确保删除操作后也触发刷新
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 09:15:00 +08:00
lvmengandClaude Sonnet 4.6 6c937c5f5f fix(activity): 修复 queryUserCount 在主线程执行 Room 查询导致崩溃的问题
- DB 查询改用 withContext(Dispatchers.IO) 切换至 IO 线程
- clearFace/clearFaceAndGetAll 操作完成后自动刷新用户计数
- 删除未使用的 CoroutineDispatcher import

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 09:07:48 +08:00
lvmeng 513f4dbaea 增加查询用户数量 2026-03-04 09:03:32 +08:00
12 changed files with 293 additions and 250 deletions
@@ -14,6 +14,7 @@ import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import com.google.gson.Gson import com.google.gson.Gson
import com.sw.inbound.utils.DateTimeUtils import com.sw.inbound.utils.DateTimeUtils
@@ -417,8 +418,8 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
// private val initialDelay = 5 * 60 * 1000L // private val initialDelay = 5 * 60 * 1000L
// private val dealyMillis = 10 * 60 * 1000L // private val dealyMillis = 10 * 60 * 1000L
private val initialDelay = 5 * 60 * 1000L private val initialDelay = 1 * 1000L
private val dealyMillis = 5 * 60 * 1000L private val dealyMillis = 10 * 1000L
fun startFaceTask(block: (List<String?>) -> Unit) { fun startFaceTask(block: (List<String?>) -> Unit) {
faceTaskJob = faceTaskJob =
intervalExecutor.startIntervalTaskWithInitialDelay(initialDelay, dealyMillis) { intervalExecutor.startIntervalTaskWithInitialDelay(initialDelay, dealyMillis) {
@@ -431,9 +432,9 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
//private var taskPageNo = 1 //private var taskPageNo = 1
private fun getFaceIncrementList(pageNo: Int = 1, action:(List<String?>)-> Unit) { private fun getFaceIncrementList(pageNo: Int = 1, action:(List<String?>)-> Unit) {
val timestamp = SpTool.lastFaceTimestamp val timestamp = SpTool.lastFaceTimestamp
if (timestamp == 0L) { // if (timestamp == 0L) {
return // return
} // }
settingViewModel.getFaceIncrementList( settingViewModel.getFaceIncrementList(
pageNo = pageNo, pageNo = pageNo,
timestamp = timestamp timestamp = timestamp
@@ -441,6 +442,12 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
runOnUiThread { runOnUiThread {
if (pageNo == 1 && list.isEmpty()) { if (pageNo == 1 && list.isEmpty()) {
//未查询到增量数据 //未查询到增量数据
// Thread{
// val faceDao = FaceDatabase.getInstance(this).faceDao()
// if (faceDao.getFaceCount() == 0) {
// FaceEntity("-1", null, byteArrayOf(-1)).also { it.userType = "-1" }
// }
// }.start()
return@runOnUiThread return@runOnUiThread
} }
updateFaceData(list) updateFaceData(list)
@@ -462,28 +469,26 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
public val recognizeViewModel by viewModels<RecognizeViewModel>() public val recognizeViewModel by viewModels<RecognizeViewModel>()
private fun updateFaceData(list: List<UserFaceModel>) { private fun updateFaceData(list: List<UserFaceModel>) {
Thread { lifecycleScope.launch(Dispatchers.IO) {
val faceList = mutableListOf<FaceEntity>() val faceList = mutableListOf<FaceEntity>()
val faceDao = FaceDatabase.getInstance(this).faceDao() val faceDao = FaceDatabase.getInstance(this@BaseActivity).faceDao()
try { try {
list.forEach { model -> list.forEach { model ->
if (model.faceDeleted == true) { if (model.faceDeleted == true) {
//删除数据 //删除数据
faceDao.deleteFaceById(model.userId) faceDao.deleteFaceById(model.userId)
// if (faceDao.getFaceCount() == 0) {
// FaceEntity("-1", null, byteArrayOf(-1)).also { it.userType = "-1" }
// }
} else { } else {
val faceData = faceDao.queryByUserName(model.userId) // 获取该用户所有已存特征,判断当前特征是否已存在
if (faceData == null) { val existList = faceDao.queryAllByUserName(model.userId)
//保存数据 val featureMatches = existList.map { Base64.encode(it.featureData) == model.faceFeatureStr }
val faceEntity = FaceEntity( Timber.d("userId=${model.userId}, 已存记录数=${existList.size}, 各条特征是否与服务器一致=$featureMatches")
model.userId, val alreadyExists = featureMatches.any { it }
null, if (!alreadyExists) {
Base64.decode(model.faceFeatureStr) //当前特征不存在,保存
).also { faceList.add(getNewFaceEntity(model))
//1-会员、2-临时用户
it.userType = if (model.isMember) "1" else "2"
it.cardNo = model.cardNo
}
faceList.add(faceEntity)
} }
} }
} }
@@ -492,13 +497,25 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
} }
try { try {
if (faceList.isNotEmpty()) { if (faceList.isNotEmpty()) {
FaceDatabase.getInstance(this).faceDao().insert(faceList) FaceDatabase.getInstance(this@BaseActivity).faceDao().insert(faceList)
recognizeViewModel.refreshFaceList()
} }
recognizeViewModel.refreshFaceList()
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
}.start() }
}
private fun getNewFaceEntity(model: UserFaceModel) : FaceEntity {
return FaceEntity(
model.userId,
null,
Base64.decode(model.faceFeatureStr)
).also {
//1-会员、2-临时用户
it.userType = if (model.member) "1" else "2"
it.cardNo = model.cardNo
}
} }
fun initPlateList() { fun initPlateList() {
@@ -518,6 +535,10 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
val faceDao = FaceDatabase.getInstance(App.getContext()).faceDao() val faceDao = FaceDatabase.getInstance(App.getContext()).faceDao()
faceDao.deleteAll() faceDao.deleteAll()
faceDao.resetId() faceDao.resetId()
// faceDao.insert(
// FaceEntity("-1", null, byteArrayOf(-1)).also { it.userType = "-1" }
// )
recognizeViewModel.refreshFaceList()
runOnUiThread { block() } runOnUiThread { block() }
}.start() }.start()
} }
@@ -7,6 +7,7 @@ import com.sw.plate.utils.arcface.FaceApi
import com.sw.platecabinet.GlobalData import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.databinding.ActivityDeviceInitBinding import com.sw.platecabinet.databinding.ActivityDeviceInitBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.utils.SpTool
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -48,9 +49,11 @@ class DeviceInitActivity : BaseActivity<ActivityDeviceInitBinding>() {
showWaitingDialog("加载中……") showWaitingDialog("加载中……")
lifecycleScope.launch { lifecycleScope.launch {
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
val count = FaceApi().queryFaceCount() //val count = FaceApi().queryFaceCount()
if (count == 0) { //if (count == 0) {
if (SpTool.firstGetFace) {
userViewModel.getUserFaceCache { status, msg -> userViewModel.getUserFaceCache { status, msg ->
SpTool.firstGetFace = false
getDeviceConfig() getDeviceConfig()
} }
} else { } else {
@@ -1,7 +1,10 @@
package com.sw.platecabinet.activity package com.sw.platecabinet.activity
import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.sw.plate.utils.Base64
import com.sw.plate.utils.ToastUtils import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.facedb.FaceDatabase
import com.sw.platecabinet.MyApp import com.sw.platecabinet.MyApp
import com.sw.platecabinet.databinding.ActivityFunBinding import com.sw.platecabinet.databinding.ActivityFunBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding import com.sw.platecabinet.databinding.ItemTitleTimeBinding
@@ -9,11 +12,12 @@ import com.sw.platecabinet.dialog.CustomDialog
import com.sw.platecabinet.ext.clickWithDebounce import com.sw.platecabinet.ext.clickWithDebounce
import com.sw.platecabinet.utils.PlateUtils import com.sw.platecabinet.utils.PlateUtils
import com.sw.platecabinet.utils.SpTool import com.sw.platecabinet.utils.SpTool
import kotlinx.coroutines.CoroutineDispatcher import com.sw.platecabinet.viewmodel.UserViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlin.getValue
class FunActivity : BaseActivity<ActivityFunBinding>() { class FunActivity : BaseActivity<ActivityFunBinding>() {
@@ -32,6 +36,28 @@ class FunActivity : BaseActivity<ActivityFunBinding>() {
} }
} }
binding.btnPutPlate.clickWithDebounce { putPlate() } binding.btnPutPlate.clickWithDebounce { putPlate() }
queryUserCount()
binding.btnTestFeature.clickWithDebounce {
testFeature()
}
}
private fun testFeature() {
userViewModel.getUserFace(1, 1) {list ->
lifecycleScope.launch {
withContext(Dispatchers.IO) {
if (list.isEmpty()) return@withContext
val userId = list[0].userId
val base64Feature = list[0].faceFeatureStr
val faceDao = FaceDatabase.getInstance(this@FunActivity).faceDao()
val entityList = faceDao.queryAllByUserName(userId)
val resultList = entityList.map { base64Feature == Base64.encode(it.featureData) }
val isEqual = resultList.any { true }
ToastUtils.showToast("比较结果:$isEqual")
}
}
}
} }
private fun clearFace() { private fun clearFace() {
@@ -42,6 +68,7 @@ class FunActivity : BaseActivity<ActivityFunBinding>() {
{ {
hideWaitingDialog() hideWaitingDialog()
ToastUtils.showToast("已清除") ToastUtils.showToast("已清除")
queryUserCount()
}, 500 }, 500
) )
} }
@@ -57,6 +84,7 @@ class FunActivity : BaseActivity<ActivityFunBinding>() {
ToastUtils.showToast( ToastUtils.showToast(
if (status) "请求成功,$msg" else "请求失败,$msg" if (status) "请求成功,$msg" else "请求失败,$msg"
) )
queryUserCount()
} }
} }
} }
@@ -111,4 +139,14 @@ class FunActivity : BaseActivity<ActivityFunBinding>() {
} }
private fun queryUserCount() {
lifecycleScope.launch {
val faceDao = FaceDatabase.getInstance(this@FunActivity).faceDao()
val memberUserCount = withContext(Dispatchers.IO) { faceDao.getFaceCount("1") }
val tempUserCount = withContext(Dispatchers.IO) { faceDao.getFaceCount("2") }
binding.tvMemberUserCount.text = "会员用户:${memberUserCount}"
binding.tvTempUserCount.text = "临时用户:${tempUserCount}"
}
}
} }
@@ -49,7 +49,6 @@ import com.sw.platecabinet.ext.visible
import com.sw.platecabinet.model.response.EquipmentUserInfo import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.model.response.UserFaceModel import com.sw.platecabinet.model.response.UserFaceModel
import com.sw.platecabinet.utils.BitmapSaver import com.sw.platecabinet.utils.BitmapSaver
import com.sw.platecabinet.utils.CountDownManager
import com.sw.platecabinet.utils.CountDownUtil import com.sw.platecabinet.utils.CountDownUtil
import com.sw.platecabinet.utils.Debouncer import com.sw.platecabinet.utils.Debouncer
import com.sw.platecabinet.utils.LogFileUtil import com.sw.platecabinet.utils.LogFileUtil
@@ -60,6 +59,7 @@ import com.sw.platecabinet.utils.SoundPoolUtil
import com.sw.platecabinet.utils.SpTool import com.sw.platecabinet.utils.SpTool
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode import org.greenrobot.eventbus.ThreadMode
@@ -169,6 +169,12 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
private val leftDebouncer = Debouncer(30 * 1000) private val leftDebouncer = Debouncer(30 * 1000)
/** 识别超时倒计时(15秒无操作跳回初始页) */
private val recognizeCountDownUtil = CountDownUtil()
/** 用户离开画面后的倒计时(llCollectButton 显示状态下无人脸时触发) */
private val leftCountDownUtil = CountDownUtil()
private var registerHandler: RegisterCallbackHandler? = null private var registerHandler: RegisterCallbackHandler? = null
private fun isFitData(userFaceInfo: UserFaceInfo?): Boolean { private fun isFitData(userFaceInfo: UserFaceInfo?): Boolean {
@@ -331,7 +337,7 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
} }
private fun recognizeFail() { private fun recognizeFail() {
CountDownManager.cancelCountDown() recognizeCountDownUtil.cancelCountDown()
binding.tvToInit.text = "" binding.tvToInit.text = ""
binding.tvSimilarValue.text = "" binding.tvSimilarValue.text = ""
binding.llCollectButton.visible() binding.llCollectButton.visible()
@@ -343,6 +349,20 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
binding.ivCollectPhoto.visible() binding.ivCollectPhoto.visible()
} }
private fun newUserCollect() {
recognizeCountDownUtil.cancelCountDown()
binding.tvToInit.text = ""
binding.tvSimilarValue.text = ""
binding.llCollectButton.visible()
binding.btnRetry.gone()
binding.btnCollect.run {
visible()
text = "新用户采集"
}
binding.ivCollectPhoto.gone()
resumeCamera()
}
/** /**
* 查询餐盘数据 * 查询餐盘数据
*/ */
@@ -389,13 +409,14 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
PlateUtils.open(this, item.equipmentBoxCode, false) { PlateUtils.open(this, item.equipmentBoxCode, false) {
goInitActivity() goInitActivity()
} }
CountDownManager.cancelCountDown() recognizeCountDownUtil.cancelCountDown()
} }
currentPlateInfo = item currentPlateInfo = item
MyApp.plateList = items MyApp.plateList = items
SpTool.savePlateData(items) SpTool.savePlateData(items)
} }
} }
private val openDebouncer by lazy { Debouncer(5000) } private val openDebouncer by lazy { Debouncer(5000) }
private fun initArcView() { private fun initArcView() {
@@ -407,10 +428,8 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
override fun onDestroy() { override fun onDestroy() {
loadLogInfo("onDestroy") loadLogInfo("onDestroy")
if (rgbCameraHelper != null) {
rgbCameraHelper?.release() rgbCameraHelper?.release()
rgbCameraHelper = null rgbCameraHelper = null
}
recognizeViewModel.destroy() recognizeViewModel.destroy()
@@ -605,10 +624,10 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
} }
} }
fun openRectInfoDraw(view: View?) { // fun openRectInfoDraw(view: View?) {
openRectInfoDraw = !openRectInfoDraw // openRectInfoDraw = !openRectInfoDraw
recognizeViewModel.setDrawRectInfoTextValue(openRectInfoDraw) // recognizeViewModel.setDrawRectInfoTextValue(openRectInfoDraw)
} // }
override fun onGlobalLayout() { override fun onGlobalLayout() {
@@ -623,7 +642,7 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
if (rgbCameraHelper != null && rgbCameraHelper!!.isStopped) { if (rgbCameraHelper != null && rgbCameraHelper!!.isStopped) {
loadLogInfo("collectFace,resumeCamera: ") loadLogInfo("collectFace,resumeCamera: ")
isRecognition = true isRecognition = true
rgbCameraHelper?.start() rgbCameraHelper!!.start()
} else { } else {
//recognizeViewModel.onPreviewFrame(ByteArray(1382400), true) //recognizeViewModel.onPreviewFrame(ByteArray(1382400), true)
@@ -658,7 +677,8 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
} }
// 调用示例:构造 1920×1080 的全黑帧(长度 1382400 // 调用示例:构造 1920×1080 的全黑帧(长度 1382400
private val emptyFrame = createBlackNV21Frame(1920, 1080) // private val emptyFrame = createBlackNV21Frame(1920, 1080)
private val emptyFrame = createBlackNV21Frame(1280, 720)
// 传递“空帧” recognizeViewModel.onPreviewFrame(emptyFrame, true) // 传递“空帧” recognizeViewModel.onPreviewFrame(emptyFrame, true)
// /** // /**
@@ -669,13 +689,24 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
super.onResume() super.onResume()
try { try {
if (isFirstOpen) { if (isFirstOpen) {
goInitActivity()
binding.root.postDelayed({
isFirstOpen = false isFirstOpen = false
}, 1000) goInitActivity()
return return
} }
lifecycleScope.launch {
try {
val faceDao = FaceDatabase.getInstance(this@LoginByFaceActivity).faceDao()
val faceCount = withContext(Dispatchers.IO) { faceDao.faceCount }
if (faceCount == 0) {
newUserCollect()
} else {
resetRecognizeState() resetRecognizeState()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
@@ -699,8 +730,9 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
resumeCamera() resumeCamera()
registerHandler = RegisterCallbackHandler() registerHandler = RegisterCallbackHandler()
CountDownManager.startCountDown( recognizeCountDownUtil.startCountDown(
totalSeconds = 15, total = 15,
lifecycleScope = lifecycleScope,
onTick = { remaining -> onTick = { remaining ->
loadLogInfo("resetRecognizeState剩余时间:$remaining") loadLogInfo("resetRecognizeState剩余时间:$remaining")
if (judgeRecognizeState()) return@startCountDown if (judgeRecognizeState()) return@startCountDown
@@ -725,7 +757,7 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
override fun onPause() { override fun onPause() {
pauseCamera() pauseCamera()
super.onPause() super.onPause()
CountDownManager.cancelCountDown() recognizeCountDownUtil.cancelCountDown()
} }
private fun pauseCamera() { private fun pauseCamera() {
@@ -735,7 +767,9 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
private fun judgeRecognizeState(): Boolean { private fun judgeRecognizeState(): Boolean {
val rectColor = binding.dualCameraFaceRectView.rectColor val rectColor = binding.dualCameraFaceRectView.rectColor
if (rectColor == RecognizeColor.COLOR_SUCCESS && currentUserId.isNullOrBlank().not() && facePreviewInfoList.isNullOrEmpty().not()) { if (rectColor == RecognizeColor.COLOR_SUCCESS && currentUserId.isNullOrBlank()
.not() && facePreviewInfoList.isNullOrEmpty().not()
) {
//人未离开,再次识别 //人未离开,再次识别
loadLogInfo("userId=$currentUserId") loadLogInfo("userId=$currentUserId")
openPlateDebouncer.debounce { openPlateDebouncer.debounce {
@@ -802,7 +836,8 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
saveFaceInfo(faceModel) { saveFaceInfo(faceModel) {
runOnUiThread { runOnUiThread {
Glide.with(this@LoginByFaceActivity).load(imageFile).into(binding.ivCollectPhoto) Glide.with(this@LoginByFaceActivity).load(imageFile)
.into(binding.ivCollectPhoto)
} }
} }
uploadFaceDebouncer.reset() uploadFaceDebouncer.reset()
@@ -852,13 +887,18 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
Base64.decode(faceModel.faceFeatureStr) Base64.decode(faceModel.faceFeatureStr)
).also { ).also {
//1-会员、2-临时用户 //1-会员、2-临时用户
it.userType = if (faceModel.isMember) "1" else "2" it.userType = if (faceModel.member) "1" else "2"
} }
faceDao.insert(faceEntity) val faceId = faceDao.insert(faceEntity)
faceEntity.faceId = faceId
loadLogInfo("collectFace,saveFaceInfo: ----------10,人脸数据保存完成-----------") loadLogInfo("collectFace,saveFaceInfo: ----------10,人脸数据保存完成-----------")
recognizeViewModel.refreshFaceList() recognizeViewModel.refreshFaceList()
loadLogInfo("collectFace,saveFaceInfo: ----------11,刷新人脸数据-----------") loadLogInfo("collectFace,saveFaceInfo: ----------11,刷新人脸数据-----------")
runOnUiThread { recognizeViewModel.onPreviewFrame(emptyFrame, true) } runOnUiThread {
//采集完成,不再识别,直接提示成功
isRecognition = false
recognizeSuccess("${System.currentTimeMillis()}")
}
loadLogInfo("---------------------------------") loadLogInfo("---------------------------------")
collectCount = 0 collectCount = 0
block() block()
@@ -875,22 +915,20 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
private var isCollectFace = false private var isCollectFace = false
private fun leftCountDown() { private fun leftCountDown() {
CountDownUtil.startCountDown(total = 15, lifecycleScope = lifecycleScope, onStart = { leftCountDownUtil.startCountDown(total = 15, lifecycleScope = lifecycleScope, onStart = {
loadLogInfo("onPreview-----------leftCountDown-------------") loadLogInfo("onPreview-----------leftCountDown-------------")
}, onTick = { }, onTick = {
loadLogInfo("leftCountDown.onTick:$it") loadLogInfo("leftCountDown.onTick:$it")
if (facePreviewInfoList.isNullOrEmpty().not()) { if (facePreviewInfoList.isNullOrEmpty().not()) {
loadLogInfo("leftCountDown.onTick-cancel()") loadLogInfo("leftCountDown.onTick-cancel()")
CountDownUtil.cancelCountDown() leftCountDownUtil.cancelCountDown()
return@startCountDown return@startCountDown
} }
}, onFinish = { }, onFinish = {
if (facePreviewInfoList.isNullOrEmpty().not()) { if (facePreviewInfoList.isNullOrEmpty().not()) {
loadLogInfo("leftCountDown.onFinish-cancel()") loadLogInfo("leftCountDown.onFinish-人脸未离开,不跳转")
CountDownUtil.cancelCountDown()
return@startCountDown return@startCountDown
} }
CountDownUtil.cancelCountDown()
goInitActivity() goInitActivity()
}) })
} }
@@ -2,7 +2,6 @@ package com.sw.platecabinet.model.response
import android.os.Parcelable import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
/** /**
@@ -38,5 +37,5 @@ data class UserFaceModel(
/** /**
* 是否会员 * 是否会员
*/ */
val isMember: Boolean val member: Boolean
) : Parcelable ) : Parcelable
@@ -1,135 +0,0 @@
package com.sw.platecabinet.utils
import android.os.CountDownTimer
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.util.Log
/**
* 无状态残留的CountDownTimer封装(解决时间残留问题)
* @param millisInFuture 总时长(毫秒)
* @param countDownInterval 倒计时间隔(毫秒)
* @param onTick 倒计时回调(剩余毫秒)
* @param onFinish 结束回调
*/
class SafeCountDownTimer(
private val millisInFuture: Long,
private val countDownInterval: Long,
private val onTick: (Long) -> Unit,
private val onFinish: () -> Unit
) {
// 独立Handler,避免与其他CountDownTimer复用消息队列
private val handler = object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
synchronized(this@SafeCountDownTimer) {
// 仅当timer未取消时处理消息,避免残留消息触发
if (!isCancelled) {
super.handleMessage(msg)
}
}
}
}
// 核心:标记是否已取消,避免残留消息触发
@Volatile
private var isCancelled = false
// 实际的CountDownTimer实例
private var timer: CountDownTimer? = null
/**
* 启动倒计时(启动前自动清空旧状态)
*/
fun start() {
// 第一步:强制取消旧实例+清空所有残留消息(核心!)
cancel()
// 第二步:重置取消标记
isCancelled = false
// 第三步:新建CountDownTimer实例,绑定独立Handler
timer = object : CountDownTimer(millisInFuture, countDownInterval) {
override fun onTick(millisUntilFinished: Long) {
// 双重校验:仅未取消时触发回调
if (!isCancelled) {
onTick.invoke(millisUntilFinished)
}
}
override fun onFinish() {
if (!isCancelled) {
onFinish.invoke()
// 结束后主动清空,避免残留
cancel()
}
}
}
// 第四步:启动新实例
timer?.start()
Log.d("SafeCountDownTimer", "倒计时启动,总时长:$millisInFuture 毫秒")
}
/**
* 取消倒计时(彻底清空所有状态)
*/
fun cancel() {
// 标记为已取消,拦截后续消息
isCancelled = true
// 取消timer实例
timer?.cancel()
timer = null
// 清空Handler所有残留消息(核心:解决旧消息触发问题)
handler.removeCallbacksAndMessages(null)
Log.d("SafeCountDownTimer", "倒计时已取消,清空所有残留状态")
}
}
// ========== 封装成工具类,方便全局调用 ==========
object CountDownManager {
// 保存当前倒计时实例,避免重复启动
private var currentTimer: SafeCountDownTimer? = null
/**
* 启动/重启倒计时(适配人脸识别防抖场景)
* @param totalSeconds 总秒数(如15
* @param onTick 每秒回调剩余秒数
* @param onFinish 结束回调
*/
fun startCountDown(
totalSeconds: Int = 15,
onTick: (Int) -> Unit = {},
onFinish: () -> Unit
) {
// 先取消旧倒计时
cancelCountDown()
// 新建安全倒计时实例
currentTimer = SafeCountDownTimer(
millisInFuture = totalSeconds * 1000L,
countDownInterval = 1000L,
onTick = { millis ->
// 转换为秒数回调
val sec = (millis / 1000).toInt() + 1
onTick.invoke(sec)
},
onFinish = {
onFinish.invoke()
}
)
// 启动倒计时
currentTimer?.start()
}
/**
* 取消当前倒计时
*/
fun cancelCountDown() {
currentTimer?.cancel()
currentTimer = null
}
}
@@ -1,33 +1,26 @@
package com.sw.platecabinet.utils package com.sw.platecabinet.utils
import android.util.Log
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import android.util.Log
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import kotlin.ranges.downTo
/** /**
* 人脸识别防抖倒计时工具类(单例+防重复+无死循环 * 基于协程的倒计时工具类(实例化使用,各调用方持有独立实例,互不干扰
* 修复:onCompletion 区分正常结束与取消,手动取消时不触发 onFinish
*/ */
object CountDownUtil { class CountDownUtil {
// 原子布尔值:标记是否正在倒计时(防重复启动)
private val isCounting = AtomicBoolean(false)
// 当前倒计时Job(用于取消) private val isCounting = AtomicBoolean(false)
private var currentJob: Job? = null private var currentJob: Job? = null
/** /**
* 启动/重启防抖倒计时(同一时间仅一个倒计时运行 * 启动倒计时(若已在运行则忽略,需重启请先调用 cancelCountDown
* @param totalSeconds 倒计时总秒数(如15 * @param total 总秒数
* @param lifecycleScope Activity/FragmentlifecycleScope(必须) * @param lifecycleScope Activity/FragmentlifecycleScope
* @param onStart 启动回调(主线程)
* @param onTick 每秒回调剩余秒数(主线程) * @param onTick 每秒回调剩余秒数(主线程)
* @param onFinish 倒计时结束回调(主线程) * @param onFinish 正常结束回调(主线程,手动取消时不触发
*/ */
fun startCountDown( fun startCountDown(
total: Int = 15, total: Int = 15,
@@ -36,46 +29,37 @@ object CountDownUtil {
onTick: (Int) -> Unit = {}, onTick: (Int) -> Unit = {},
onFinish: () -> Unit onFinish: () -> Unit
) { ) {
// 1. 防重复:如果正在倒计时,先取消旧任务 if (isCounting.get()) return
if (isCounting.get()) {
//cancelCountDown()
return
}
// 2. 标记为正在倒计时
isCounting.set(true) isCounting.set(true)
// 3. 启动新倒计时
currentJob = flow { currentJob = flow {
Log.d("CountDownUtil", "倒计时启动,总时长:$total")
for (i in total downTo 1) { for (i in total downTo 1) {
emit(i) emit(i)
delay(1000) // IO线程等待1秒,无死循环 delay(1000L)
} }
} }
.flowOn(Dispatchers.IO) // 上游IO线程,避免阻塞主线程 .flowOn(Dispatchers.IO)
.onStart { .onStart {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) { onStart() }
onStart.invoke()
}
} }
.onEach { remaining -> .onEach { remaining ->
// 主线程回调剩余时间 withContext(Dispatchers.Main) { onTick(remaining) }
withContext(Dispatchers.Main) {
onTick.invoke(remaining)
} }
} .onCompletion { cause ->
.onCompletion {
// 倒计时结束:重置状态+回调
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
isCounting.set(false) // 重置标记 isCounting.set(false)
currentJob = null // 置空Job currentJob = null
// cause == null 为正常结束,有异常(含 CancellationException)时不触发 onFinish
if (cause == null) {
Log.d("CountDownUtil", "倒计时正常结束") Log.d("CountDownUtil", "倒计时正常结束")
onFinish.invoke() onFinish()
} else {
Log.d("CountDownUtil", "倒计时被取消:${cause.message}")
}
} }
} }
.catch { e -> .catch { e ->
// 捕获异常:避免崩溃,重置状态
Log.e("CountDownUtil", "倒计时异常:${e.message}", e) Log.e("CountDownUtil", "倒计时异常:${e.message}", e)
isCounting.set(false) isCounting.set(false)
currentJob = null currentJob = null
@@ -84,12 +68,12 @@ object CountDownUtil {
} }
/** /**
* 取消倒计时(手动停止 * 取消倒计时(不触发 onFinish
*/ */
fun cancelCountDown() { fun cancelCountDown() {
currentJob?.cancel() // 取消Job currentJob?.cancel()
currentJob = null // 置空,避免残留 currentJob = null
isCounting.set(false) // 重置标记 isCounting.set(false)
Log.d("CountDownUtil", "倒计时被手动取消") Log.d("CountDownUtil", "倒计时取消")
} }
} }
@@ -6,10 +6,14 @@ import com.sw.inbound.utils.SPUtil
import com.sw.inbound.utils.SPUtil.Companion.getInstance import com.sw.inbound.utils.SPUtil.Companion.getInstance
import com.sw.platecabinet.MyApp import com.sw.platecabinet.MyApp
import com.sw.platecabinet.model.response.EquipmentUserInfo import com.sw.platecabinet.model.response.EquipmentUserInfo
import kotlin.text.get
object SpTool { object SpTool {
const val LAST_FACE_TIMESTAMP: String = "faceTimestamp" const val LAST_FACE_TIMESTAMP: String = "faceTimestamp"
const val PLATE_BOX_DATA: String = "plateBoxBind" const val PLATE_BOX_DATA: String = "plateBoxBind"
const val IS_FIRST_GET_FACE = "isFirstGetFace"
private var spUtil: SPUtil? = null private var spUtil: SPUtil? = null
init { init {
@@ -39,4 +43,10 @@ object SpTool {
return GsonUtils.fromJsonList(jsonData, EquipmentUserInfo::class.java) return GsonUtils.fromJsonList(jsonData, EquipmentUserInfo::class.java)
} }
var firstGetFace: Boolean
get() = spUtil?.get(IS_FIRST_GET_FACE, true)?:true
set(value) {
spUtil?.boolean(IS_FIRST_GET_FACE, value)
}
} }
@@ -9,16 +9,12 @@ import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.FaceApi import com.sw.plate.utils.arcface.FaceApi
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
import com.sw.platecabinet.GlobalData import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.GlobalData.globalEquipmentCode
import com.sw.platecabinet.GlobalKey import com.sw.platecabinet.GlobalKey
import com.sw.platecabinet.model.DeviceConfig import com.sw.platecabinet.model.DeviceConfig
import com.sw.platecabinet.model.request.LoginParam import com.sw.platecabinet.model.request.LoginParam
import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.model.response.UserFaceModel import com.sw.platecabinet.model.response.UserFaceModel
import com.sw.platecabinet.utils.SpTool import com.sw.platecabinet.utils.SpTool
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
@@ -55,6 +51,19 @@ class UserViewModel : BaseViewModel() {
// } // }
// } // }
fun getUserFace(
pageNo: Int,
pageSize: Int,
callback: (List<UserFaceModel>) -> Unit
) {
launchWithLoading{
val response = repository.getUserFaceCache(pageNum = pageNo, pageSize = pageSize)
val status = parseResponse(response)
if (status) callback(response.data?:emptyList())
else emptyList<UserFaceModel>()
}
}
/** /**
* 获取人脸数据 * 获取人脸数据
*/ */
@@ -79,9 +88,15 @@ class UserViewModel : BaseViewModel() {
return@launchWithLoading return@launchWithLoading
} }
val faceEntityList = list.map { val faceEntityList = list.map {
FaceEntity(it.userId, null, Base64.decode(it.faceFeatureStr)) FaceEntity(it.userId, null, Base64.decode(it.faceFeatureStr)).apply {
userType = if (it.member) "1" else "2"
}
} }
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
if (currentPageNo == 1) {
//第一页数据删除本地数据库
faceApi.clearFaceData()
}
faceApi.updateFaceData(pageNo, faceEntityList) faceApi.updateFaceData(pageNo, faceEntityList)
if (list.size >= pageSize) { if (list.size >= pageSize) {
faceTimestamp = list.last().faceUpdateTimestamp ?: 0 faceTimestamp = list.last().faceUpdateTimestamp ?: 0
@@ -103,6 +118,7 @@ class UserViewModel : BaseViewModel() {
private var faceTimestamp = 0L private var faceTimestamp = 0L
/** /**
* 激活人脸识别引擎 * 激活人脸识别引擎
*/ */
+49 -2
View File
@@ -3,12 +3,45 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@drawable/bg" android:background="@drawable/bg"
android:orientation="vertical"> android:orientation="vertical"
xmlns:tools="http://schemas.android.com/tools">
<include <include
android:id="@+id/includeHeader" android:id="@+id/includeHeader"
layout="@layout/item_title_time" /> layout="@layout/item_title_time" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="60dp"
android:gravity="center_vertical"
android:layout_marginTop="30dp">
<TextView
android:id="@+id/tvMemberUserCount"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:textSize="16sp"
android:textColor="@color/white"
android:gravity="center"
tools:text="会员用户:100人"/>
<View
android:layout_width="1dp"
android:layout_height="30dp"
android:background="@color/white"/>
<TextView
android:id="@+id/tvTempUserCount"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:textSize="16sp"
android:textColor="@color/white"
android:gravity="center"
tools:text="临时用户:100人"/>
</LinearLayout>
<TextView <TextView
android:id="@+id/btnClearFace" android:id="@+id/btnClearFace"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -32,7 +65,7 @@
android:layout_marginHorizontal="50dp" android:layout_marginHorizontal="50dp"
android:paddingHorizontal="30dp" android:paddingHorizontal="30dp"
android:background="@drawable/btn_outline" android:background="@drawable/btn_outline"
android:text="清除并重新拉取数据" android:text="清除并拉取全量数据"
android:textColor="#FFCC99" /> android:textColor="#FFCC99" />
<TextView <TextView
@@ -60,4 +93,18 @@
android:background="@drawable/btn_outline" android:background="@drawable/btn_outline"
android:text="模拟放置餐盘" android:text="模拟放置餐盘"
android:textColor="#FFCC99" /> android:textColor="#FFCC99" />
<TextView
android:id="@+id/btnTestFeature"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginTop="30dp"
android:textSize="24sp"
android:gravity="center"
android:layout_marginHorizontal="50dp"
android:paddingHorizontal="30dp"
android:background="@drawable/btn_outline"
android:text="人脸特征比较"
android:textColor="#FFCC99"
android:visibility="gone"/>
</LinearLayout> </LinearLayout>
@@ -40,6 +40,12 @@ public class FaceApi {
faceDao.insert(list); faceDao.insert(list);
} }
public void clearFaceData() {
FaceDao faceDao = FaceDatabase.getInstance(App.getContext()).faceDao();
faceDao.deleteAll();
faceDao.resetId();
}
/** /**
* 激活arcsoft 人脸 * 激活arcsoft 人脸
* *
@@ -92,9 +92,25 @@ public interface FaceDao {
@Query("SELECT * FROM face WHERE user_name = :userName limit 1") @Query("SELECT * FROM face WHERE user_name = :userName limit 1")
FaceEntity queryByUserName(String userName); FaceEntity queryByUserName(String userName);
/**
* 查询指定用户的所有人脸记录(同一用户可能存有多条特征数据)
*
* @param userName 用户ID
* @return 该用户所有人脸记录列表
*/
@Query("SELECT * FROM face WHERE user_name = :userName")
List<FaceEntity> queryAllByUserName(String userName);
/** /**
* @return 删除临时用户人脸 * @return 删除临时用户人脸
*/ */
@Query("DELETE from face WHERE user_type = '2'") @Query("DELETE from face WHERE user_type = '2'")
int deleteTempUserFaceData(); int deleteTempUserFaceData();
/**
* 查询用户人脸数
* @param userType 1-会员,2-临时用户
*/
@Query("SELECT COUNT(1) FROM face WHERE user_type = :userType")
int getFaceCount(String userType);
} }