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
6 changed files with 117 additions and 23 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
@@ -468,9 +469,9 @@ 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) {
@@ -480,19 +481,14 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
// FaceEntity("-1", null, byteArrayOf(-1)).also { it.userType = "-1" } // 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.member) "1" else "2"
it.cardNo = model.cardNo
}
faceList.add(faceEntity)
} }
} }
} }
@@ -501,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() {
@@ -1,6 +1,8 @@
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.plate.utils.arcface.facedb.FaceDatabase
import com.sw.platecabinet.MyApp 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.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 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>() {
@@ -34,6 +38,26 @@ class FunActivity : BaseActivity<ActivityFunBinding>() {
binding.btnPutPlate.clickWithDebounce { putPlate() } binding.btnPutPlate.clickWithDebounce { putPlate() }
queryUserCount() 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() {
@@ -59,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
@@ -348,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()
}
/** /**
* 查询餐盘数据 * 查询餐盘数据
*/ */
@@ -674,13 +689,24 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
super.onResume() super.onResume()
try { try {
if (isFirstOpen) { if (isFirstOpen) {
isFirstOpen = false
goInitActivity() goInitActivity()
binding.root.postDelayed({
isFirstOpen = false
}, 1000)
return 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) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
@@ -51,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>()
}
}
/** /**
* 获取人脸数据 * 获取人脸数据
*/ */
@@ -84,7 +97,7 @@ class UserViewModel : BaseViewModel() {
//第一页数据删除本地数据库 //第一页数据删除本地数据库
faceApi.clearFaceData() 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
currentPageNo++ currentPageNo++
+15 -1
View File
@@ -65,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
@@ -93,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>
@@ -92,6 +92,15 @@ 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 删除临时用户人脸
*/ */