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
12 changed files with 236 additions and 247 deletions
@@ -14,6 +14,7 @@ import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat
import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding
import com.google.gson.Gson
import com.sw.inbound.utils.DateTimeUtils
@@ -417,8 +418,8 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
// private val initialDelay = 5 * 60 * 1000L
// private val dealyMillis = 10 * 60 * 1000L
private val initialDelay = 5 * 60 * 1000L
private val dealyMillis = 5 * 60 * 1000L
private val initialDelay = 1 * 1000L
private val dealyMillis = 10 * 1000L
fun startFaceTask(block: (List<String?>) -> Unit) {
faceTaskJob =
intervalExecutor.startIntervalTaskWithInitialDelay(initialDelay, dealyMillis) {
@@ -431,9 +432,9 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
//private var taskPageNo = 1
private fun getFaceIncrementList(pageNo: Int = 1, action:(List<String?>)-> Unit) {
val timestamp = SpTool.lastFaceTimestamp
if (timestamp == 0L) {
return
}
// if (timestamp == 0L) {
// return
// }
settingViewModel.getFaceIncrementList(
pageNo = pageNo,
timestamp = timestamp
@@ -441,6 +442,12 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
runOnUiThread {
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
}
updateFaceData(list)
@@ -462,28 +469,26 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
public val recognizeViewModel by viewModels<RecognizeViewModel>()
private fun updateFaceData(list: List<UserFaceModel>) {
Thread {
lifecycleScope.launch(Dispatchers.IO) {
val faceList = mutableListOf<FaceEntity>()
val faceDao = FaceDatabase.getInstance(this).faceDao()
val faceDao = FaceDatabase.getInstance(this@BaseActivity).faceDao()
try {
list.forEach { model ->
if (model.faceDeleted == true) {
//删除数据
faceDao.deleteFaceById(model.userId)
// if (faceDao.getFaceCount() == 0) {
// FaceEntity("-1", null, byteArrayOf(-1)).also { it.userType = "-1" }
// }
} else {
val faceData = faceDao.queryByUserName(model.userId)
if (faceData == null) {
//保存数据
val faceEntity = FaceEntity(
model.userId,
null,
Base64.decode(model.faceFeatureStr)
).also {
//1-会员、2-临时用户
it.userType = if (model.isMember) "1" else "2"
it.cardNo = model.cardNo
}
faceList.add(faceEntity)
// 获取该用户所有已存特征,判断当前特征是否已存在
val existList = faceDao.queryAllByUserName(model.userId)
val featureMatches = existList.map { Base64.encode(it.featureData) == model.faceFeatureStr }
Timber.d("userId=${model.userId}, 已存记录数=${existList.size}, 各条特征是否与服务器一致=$featureMatches")
val alreadyExists = featureMatches.any { it }
if (!alreadyExists) {
//当前特征不存在,保存
faceList.add(getNewFaceEntity(model))
}
}
}
@@ -492,13 +497,25 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
}
try {
if (faceList.isNotEmpty()) {
FaceDatabase.getInstance(this).faceDao().insert(faceList)
FaceDatabase.getInstance(this@BaseActivity).faceDao().insert(faceList)
}
recognizeViewModel.refreshFaceList()
} catch (e: Exception) {
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() {
@@ -518,6 +535,10 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
val faceDao = FaceDatabase.getInstance(App.getContext()).faceDao()
faceDao.deleteAll()
faceDao.resetId()
// faceDao.insert(
// FaceEntity("-1", null, byteArrayOf(-1)).also { it.userType = "-1" }
// )
recognizeViewModel.refreshFaceList()
runOnUiThread { block() }
}.start()
}
@@ -7,6 +7,7 @@ import com.sw.plate.utils.arcface.FaceApi
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.databinding.ActivityDeviceInitBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.utils.SpTool
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -48,9 +49,11 @@ class DeviceInitActivity : BaseActivity<ActivityDeviceInitBinding>() {
showWaitingDialog("加载中……")
lifecycleScope.launch {
withContext(Dispatchers.Default) {
val count = FaceApi().queryFaceCount()
if (count == 0) {
//val count = FaceApi().queryFaceCount()
//if (count == 0) {
if (SpTool.firstGetFace) {
userViewModel.getUserFaceCache { status, msg ->
SpTool.firstGetFace = false
getDeviceConfig()
}
} else {
@@ -1,6 +1,8 @@
package com.sw.platecabinet.activity
import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope
import com.sw.plate.utils.Base64
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.facedb.FaceDatabase
import com.sw.platecabinet.MyApp
@@ -10,10 +12,12 @@ import com.sw.platecabinet.dialog.CustomDialog
import com.sw.platecabinet.ext.clickWithDebounce
import com.sw.platecabinet.utils.PlateUtils
import com.sw.platecabinet.utils.SpTool
import com.sw.platecabinet.viewmodel.UserViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.getValue
class FunActivity : BaseActivity<ActivityFunBinding>() {
@@ -34,6 +38,26 @@ class FunActivity : BaseActivity<ActivityFunBinding>() {
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() {
@@ -49,7 +49,6 @@ import com.sw.platecabinet.ext.visible
import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.model.response.UserFaceModel
import com.sw.platecabinet.utils.BitmapSaver
import com.sw.platecabinet.utils.CountDownManager
import com.sw.platecabinet.utils.CountDownUtil
import com.sw.platecabinet.utils.Debouncer
import com.sw.platecabinet.utils.LogFileUtil
@@ -60,6 +59,7 @@ import com.sw.platecabinet.utils.SoundPoolUtil
import com.sw.platecabinet.utils.SpTool
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
@@ -167,7 +167,13 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
private val livenessType = LivenessType.RGB
private var openRectInfoDraw = false
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
@@ -331,7 +337,7 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
}
private fun recognizeFail() {
CountDownManager.cancelCountDown()
recognizeCountDownUtil.cancelCountDown()
binding.tvToInit.text = ""
binding.tvSimilarValue.text = ""
binding.llCollectButton.visible()
@@ -343,6 +349,20 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
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) {
goInitActivity()
}
CountDownManager.cancelCountDown()
recognizeCountDownUtil.cancelCountDown()
}
currentPlateInfo = item
MyApp.plateList = items
SpTool.savePlateData(items)
}
}
private val openDebouncer by lazy { Debouncer(5000) }
private fun initArcView() {
@@ -407,10 +428,8 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
override fun onDestroy() {
loadLogInfo("onDestroy")
if (rgbCameraHelper != null) {
rgbCameraHelper?.release()
rgbCameraHelper = null
}
rgbCameraHelper?.release()
rgbCameraHelper = null
recognizeViewModel.destroy()
@@ -605,10 +624,10 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
}
}
fun openRectInfoDraw(view: View?) {
openRectInfoDraw = !openRectInfoDraw
recognizeViewModel.setDrawRectInfoTextValue(openRectInfoDraw)
}
// fun openRectInfoDraw(view: View?) {
// openRectInfoDraw = !openRectInfoDraw
// recognizeViewModel.setDrawRectInfoTextValue(openRectInfoDraw)
// }
override fun onGlobalLayout() {
@@ -623,7 +642,7 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
if (rgbCameraHelper != null && rgbCameraHelper!!.isStopped) {
loadLogInfo("collectFace,resumeCamera: ")
isRecognition = true
rgbCameraHelper?.start()
rgbCameraHelper!!.start()
} else {
//recognizeViewModel.onPreviewFrame(ByteArray(1382400), true)
@@ -658,10 +677,11 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
}
// 调用示例:构造 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)
// /**
// /**
// * 临时用户id
// */
// private var tempUserId: String = ""
@@ -669,13 +689,24 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
super.onResume()
try {
if (isFirstOpen) {
isFirstOpen = false
goInitActivity()
binding.root.postDelayed({
isFirstOpen = false
}, 1000)
return
}
resetRecognizeState()
lifecycleScope.launch {
try {
val faceDao = FaceDatabase.getInstance(this@LoginByFaceActivity).faceDao()
val faceCount = withContext(Dispatchers.IO) { faceDao.faceCount }
if (faceCount == 0) {
newUserCollect()
} else {
resetRecognizeState()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
} catch (e: Exception) {
e.printStackTrace()
}
@@ -699,8 +730,9 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
resumeCamera()
registerHandler = RegisterCallbackHandler()
CountDownManager.startCountDown(
totalSeconds = 15,
recognizeCountDownUtil.startCountDown(
total = 15,
lifecycleScope = lifecycleScope,
onTick = { remaining ->
loadLogInfo("resetRecognizeState剩余时间:$remaining")
if (judgeRecognizeState()) return@startCountDown
@@ -725,7 +757,7 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
override fun onPause() {
pauseCamera()
super.onPause()
CountDownManager.cancelCountDown()
recognizeCountDownUtil.cancelCountDown()
}
private fun pauseCamera() {
@@ -735,7 +767,9 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
private fun judgeRecognizeState(): Boolean {
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")
openPlateDebouncer.debounce {
@@ -800,9 +834,10 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
return@addUserFace
}
lifecycleScope.launch(Dispatchers.IO) {
saveFaceInfo(faceModel) {
saveFaceInfo(faceModel) {
runOnUiThread {
Glide.with(this@LoginByFaceActivity).load(imageFile).into(binding.ivCollectPhoto)
Glide.with(this@LoginByFaceActivity).load(imageFile)
.into(binding.ivCollectPhoto)
}
}
uploadFaceDebouncer.reset()
@@ -852,13 +887,18 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
Base64.decode(faceModel.faceFeatureStr)
).also {
//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,人脸数据保存完成-----------")
recognizeViewModel.refreshFaceList()
loadLogInfo("collectFace,saveFaceInfo: ----------11,刷新人脸数据-----------")
runOnUiThread { recognizeViewModel.onPreviewFrame(emptyFrame, true) }
runOnUiThread {
//采集完成,不再识别,直接提示成功
isRecognition = false
recognizeSuccess("${System.currentTimeMillis()}")
}
loadLogInfo("---------------------------------")
collectCount = 0
block()
@@ -875,22 +915,20 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
private var isCollectFace = false
private fun leftCountDown() {
CountDownUtil.startCountDown(total = 15, lifecycleScope = lifecycleScope, onStart = {
leftCountDownUtil.startCountDown(total = 15, lifecycleScope = lifecycleScope, onStart = {
loadLogInfo("onPreview-----------leftCountDown-------------")
}, onTick = {
loadLogInfo("leftCountDown.onTick:$it")
if (facePreviewInfoList.isNullOrEmpty().not()) {
loadLogInfo("leftCountDown.onTick-cancel()")
CountDownUtil.cancelCountDown()
leftCountDownUtil.cancelCountDown()
return@startCountDown
}
}, onFinish = {
if (facePreviewInfoList.isNullOrEmpty().not()) {
loadLogInfo("leftCountDown.onFinish-cancel()")
CountDownUtil.cancelCountDown()
loadLogInfo("leftCountDown.onFinish-人脸未离开,不跳转")
return@startCountDown
}
CountDownUtil.cancelCountDown()
goInitActivity()
})
}
@@ -2,7 +2,6 @@ package com.sw.platecabinet.model.response
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
/**
@@ -38,5 +37,5 @@ data class UserFaceModel(
/**
* 是否会员
*/
val isMember: Boolean
val member: Boolean
) : 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
import android.util.Log
import kotlinx.coroutines.*
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 kotlin.ranges.downTo
/**
* 人脸识别防抖倒计时工具类(单例+防重复+无死循环
* 基于协程的倒计时工具类(实例化使用,各调用方持有独立实例,互不干扰
* 修复:onCompletion 区分正常结束与取消,手动取消时不触发 onFinish
*/
object CountDownUtil {
// 原子布尔值:标记是否正在倒计时(防重复启动)
private val isCounting = AtomicBoolean(false)
class CountDownUtil {
// 当前倒计时Job(用于取消)
private val isCounting = AtomicBoolean(false)
private var currentJob: Job? = null
/**
* 启动/重启防抖倒计时(同一时间仅一个倒计时运行
* @param totalSeconds 倒计时总秒数(如15
* @param lifecycleScope Activity/FragmentlifecycleScope(必须)
* 启动倒计时(若已在运行则忽略,需重启请先调用 cancelCountDown
* @param total 总秒数
* @param lifecycleScope Activity/FragmentlifecycleScope
* @param onStart 启动回调(主线程)
* @param onTick 每秒回调剩余秒数(主线程)
* @param onFinish 倒计时结束回调(主线程)
* @param onFinish 正常结束回调(主线程,手动取消时不触发
*/
fun startCountDown(
total: Int = 15,
@@ -36,46 +29,37 @@ object CountDownUtil {
onTick: (Int) -> Unit = {},
onFinish: () -> Unit
) {
// 1. 防重复:如果正在倒计时,先取消旧任务
if (isCounting.get()) {
//cancelCountDown()
return
}
if (isCounting.get()) return
// 2. 标记为正在倒计时
isCounting.set(true)
// 3. 启动新倒计时
currentJob = flow {
Log.d("CountDownUtil", "倒计时启动,总时长:$total")
for (i in total downTo 1) {
emit(i)
delay(1000) // IO线程等待1秒,无死循环
delay(1000L)
}
}
.flowOn(Dispatchers.IO) // 上游IO线程,避免阻塞主线程
.flowOn(Dispatchers.IO)
.onStart {
withContext(Dispatchers.Main) {
onStart.invoke()
}
withContext(Dispatchers.Main) { onStart() }
}
.onEach { remaining ->
// 主线程回调剩余时间
withContext(Dispatchers.Main) {
onTick.invoke(remaining)
}
withContext(Dispatchers.Main) { onTick(remaining) }
}
.onCompletion {
// 倒计时结束:重置状态+回调
.onCompletion { cause ->
withContext(Dispatchers.Main) {
isCounting.set(false) // 重置标记
currentJob = null // 置空Job
Log.d("CountDownUtil", "倒计时正常结束")
onFinish.invoke()
isCounting.set(false)
currentJob = null
// cause == null 为正常结束,有异常(含 CancellationException)时不触发 onFinish
if (cause == null) {
Log.d("CountDownUtil", "倒计时正常结束")
onFinish()
} else {
Log.d("CountDownUtil", "倒计时被取消:${cause.message}")
}
}
}
.catch { e ->
// 捕获异常:避免崩溃,重置状态
Log.e("CountDownUtil", "倒计时异常:${e.message}", e)
isCounting.set(false)
currentJob = null
@@ -84,12 +68,12 @@ object CountDownUtil {
}
/**
* 取消倒计时(手动停止
* 取消倒计时(不触发 onFinish
*/
fun cancelCountDown() {
currentJob?.cancel() // 取消Job
currentJob = null // 置空,避免残留
isCounting.set(false) // 重置标记
Log.d("CountDownUtil", "倒计时被手动取消")
currentJob?.cancel()
currentJob = null
isCounting.set(false)
Log.d("CountDownUtil", "倒计时取消")
}
}
@@ -6,10 +6,14 @@ import com.sw.inbound.utils.SPUtil
import com.sw.inbound.utils.SPUtil.Companion.getInstance
import com.sw.platecabinet.MyApp
import com.sw.platecabinet.model.response.EquipmentUserInfo
import kotlin.text.get
object SpTool {
const val LAST_FACE_TIMESTAMP: String = "faceTimestamp"
const val PLATE_BOX_DATA: String = "plateBoxBind"
const val IS_FIRST_GET_FACE = "isFirstGetFace"
private var spUtil: SPUtil? = null
init {
@@ -39,4 +43,10 @@ object SpTool {
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.facedb.entity.FaceEntity
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.GlobalData.globalEquipmentCode
import com.sw.platecabinet.GlobalKey
import com.sw.platecabinet.model.DeviceConfig
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.utils.SpTool
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
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
}
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) {
if (currentPageNo == 1) {
//第一页数据删除本地数据库
faceApi.clearFaceData()
}
faceApi.updateFaceData(pageNo, faceEntityList)
if (list.size >= pageSize) {
faceTimestamp = list.last().faceUpdateTimestamp ?: 0
@@ -103,6 +118,7 @@ class UserViewModel : BaseViewModel() {
private var faceTimestamp = 0L
/**
* 激活人脸识别引擎
*/
+15 -1
View File
@@ -65,7 +65,7 @@
android:layout_marginHorizontal="50dp"
android:paddingHorizontal="30dp"
android:background="@drawable/btn_outline"
android:text="清除并重新拉取数据"
android:text="清除并拉取全量数据"
android:textColor="#FFCC99" />
<TextView
@@ -93,4 +93,18 @@
android:background="@drawable/btn_outline"
android:text="模拟放置餐盘"
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>
@@ -40,6 +40,12 @@ public class FaceApi {
faceDao.insert(list);
}
public void clearFaceData() {
FaceDao faceDao = FaceDatabase.getInstance(App.getContext()).faceDao();
faceDao.deleteAll();
faceDao.resetId();
}
/**
* 激活arcsoft 人脸
*
@@ -92,6 +92,15 @@ public interface FaceDao {
@Query("SELECT * FROM face WHERE user_name = :userName limit 1")
FaceEntity queryByUserName(String userName);
/**
* 查询指定用户的所有人脸记录(同一用户可能存有多条特征数据)
*
* @param userName 用户ID
* @return 该用户所有人脸记录列表
*/
@Query("SELECT * FROM face WHERE user_name = :userName")
List<FaceEntity> queryAllByUserName(String userName);
/**
* @return 删除临时用户人脸
*/