feat(face): 新增人脸库展示与增量更新支持

- 在PLC设置界面新增人脸库统计模块,显示人脸总数并支持刷新
- 人脸总数数据从本地数据库异步查询并更新界面
- 修改API接口调用,采用POST请求体方式提交人脸数据(包含url和特征字符串)
- 在网络调用中实现人脸数据的增量分页获取,递归拉取所有数据页
- 维护人脸数据的最大时间戳,确保增量更新只处理最新数据
- 优化人脸数据缓存逻辑,第一页重置数据并同步时间戳
- 添加FeatureBody数据类封装人脸特征请求体参数
- 修正相关仓库层方法以适配新的API接口参数和返回结构
This commit is contained in:
mazengfei
2026-07-07 17:33:04 +08:00
parent cb107c2da2
commit 9a8ac83d33
6 changed files with 136 additions and 21 deletions
@@ -5,6 +5,7 @@ import android.content.Intent
import android.view.inputmethod.InputMethodManager
import androidx.lifecycle.lifecycleScope
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.facedb.FaceDatabase
import com.sw.plate.utils.arcface.faceserver.FaceServer
import com.sw.platecabinet.databinding.ActivityPlcSettingBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding
@@ -60,6 +61,7 @@ class PlcSettingActivity : BaseActivity<ActivityPlcSettingBinding>() {
initAlarmButton()
initParamButtons()
initStatusButton()
initFaceCount()
binding.btnSwitchEnv.setOnClickListener {
EnvSwitchDialog(context = this, onEnvChanged = {
showWaitingDialog("重置人脸数据中……")
@@ -334,6 +336,30 @@ class PlcSettingActivity : BaseActivity<ActivityPlcSettingBinding>() {
""".trimIndent()
}
// ==================== 人脸库统计 ====================
/**
* 初始化人脸库统计区:页面加载时查询并显示人脸总数,绑定刷新按钮
*/
private fun initFaceCount() {
refreshFaceCount()
binding.btnRefreshFaceCount.clickWithDebounce {
refreshFaceCount()
}
}
/**
* 从数据库查询人脸总数并更新 UI
*/
private fun refreshFaceCount() {
lifecycleScope.launch {
val count = withContext(Dispatchers.IO) {
FaceDatabase.getInstance(this@PlcSettingActivity).faceDao().faceCount
}
binding.tvFaceCount.text = "人脸总数:${count}"
}
}
// ==================== 生命周期 ====================
/**
@@ -0,0 +1,6 @@
package com.sw.platecabinet.model.request
data class FeatureBody(
val url:String,
val featureChar:String
)
@@ -2,6 +2,7 @@ package com.sw.platecabinet.network.api
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.model.DeviceConfigV2
import com.sw.platecabinet.model.request.FeatureBody
import com.sw.platecabinet.model.response.ApiResponse
import com.sw.platecabinet.model.response.UserFaceModel
import com.sw.platecabinet.model.response.UserFaceModelV2
@@ -46,11 +47,9 @@ interface ApiServiceV2 {
* 添加人脸数据
*/
@POST
@FormUrlEncoded
suspend fun addUserFace(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/user/add-by-face",
@Field("featureChar") faceData: String,
@Field("url") imageUr: String
@Body faceData: FeatureBody
): ApiResponse<UserFaceModelV2?>
/**
@@ -1,6 +1,7 @@
package com.sw.platecabinet.repository
import com.sw.platecabinet.model.DeviceConfigV2
import com.sw.platecabinet.model.request.FeatureBody
import com.sw.platecabinet.model.response.ApiResponse
import com.sw.platecabinet.model.response.UserFaceModel
import com.sw.platecabinet.model.response.UserFaceModelV2
@@ -25,8 +26,7 @@ class RemoteRepositoryV2 constructor(
return safeApiCall {
apiService.getUserFaceCache(
param = mapOf(
"pageNum" to pageNum,
"pageSize" to pageSize
"pageNum" to pageNum, "pageSize" to pageSize
)
)
}
@@ -34,16 +34,12 @@ class RemoteRepositoryV2 constructor(
suspend fun getFaceIncrementList(
pageNum: Long,
pageSize: Long = 100L,
timestamp: Long
pageNum: Long, pageSize: Long = 100L, timestamp: Long
): ApiResponse<List<UserFaceModelV2>?> {
return safeApiCall {
apiService.getFaceIncrementList(
param = mapOf(
"pageNum" to pageNum,
"pageSize" to pageSize,
"timestamp" to timestamp
"pageNum" to pageNum, "pageSize" to pageSize, "timestamp" to timestamp
)
)
}
@@ -53,8 +49,14 @@ class RemoteRepositoryV2 constructor(
return safeApiCall { apiService.getDeviceConfig() }
}
suspend fun addUserFace(faceData: String, imageUr: String): ApiResponse<UserFaceModelV2?> {
return safeApiCall { apiService.addUserFace(faceData = faceData, imageUr = imageUr) }
suspend fun addUserFace(faceData: String, imageUrl: String): ApiResponse<UserFaceModelV2?> {
return safeApiCall {
apiService.addUserFace(
faceData = FeatureBody(
url = imageUrl, featureChar = faceData
)
)
}
}
suspend fun uploadImage(file: File): ApiResponse<String?> {
@@ -82,12 +82,17 @@ class NetViewModelV2 : ViewModel() {
}
withContext(Dispatchers.Default) {
if (currentPageNo == 1) {
//第一页数据删除本地数据库
//第一页数据删除本地数据库,同时重置时间戳
faceApi.clearFaceData()
faceTimestamp = 0L
}
faceApi.updateFaceData(pageNo, faceEntityList)
if (list.size >= pageSize) {
faceTimestamp = list.last().faceUpdateTimestamp ?: 0
// 取当前页最大时间戳,与已累积的比较取最大值
val pageMaxTimestamp = maxFaceTimestamp(list)
if (pageMaxTimestamp > faceTimestamp) {
faceTimestamp = pageMaxTimestamp
}
currentPageNo++
getUserFaceCache(
pageNo = currentPageNo, callback = callback
@@ -95,7 +100,11 @@ class NetViewModelV2 : ViewModel() {
return@withContext
}
if (list.isNotEmpty()) {
faceTimestamp = list.last().faceUpdateTimestamp ?: 0
// 最后一页:取当前页最大时间戳,与已累积的比较取最大值
val pageMaxTimestamp = maxFaceTimestamp(list)
if (pageMaxTimestamp > faceTimestamp) {
faceTimestamp = pageMaxTimestamp
}
}
SpTool.lastFaceTimestamp = faceTimestamp
callback(true, "查询完成")
@@ -161,7 +170,7 @@ class NetViewModelV2 : ViewModel() {
/**
* 获取人脸数据
* 增量获取人脸数据(支持分页递归,取所有页中的最大时间戳)
*/
fun getFaceIncrementList(
pageNo: Int = 1,
@@ -169,9 +178,10 @@ class NetViewModelV2 : ViewModel() {
timestamp: Long,
refreshFaceList: () -> Unit
) {
var currentPageNo = pageNo
viewModelScope.launch {
val response = repository.getFaceIncrementList(
pageNum = pageNo.toLong(),
pageNum = currentPageNo.toLong(),
pageSize = pageSize.toLong(),
timestamp = timestamp
)
@@ -181,10 +191,27 @@ class NetViewModelV2 : ViewModel() {
withContext(Dispatchers.IO) {
val list = response.data ?: emptyList()
updateFaceData(list)
if (list.isNotEmpty()) {
faceTimestamp = list.last().faceUpdateTimestamp ?: 0
// 取当前页最大时间戳,与已累积的比较取最大值
val pageMaxTimestamp = maxFaceTimestamp(list)
if (pageMaxTimestamp > faceTimestamp) {
faceTimestamp = pageMaxTimestamp
}
if (list.size >= pageSize) {
// 还有下一页,继续递归拉取
currentPageNo++
getFaceIncrementList(
pageNo = currentPageNo,
pageSize = pageSize,
timestamp = timestamp,
refreshFaceList = refreshFaceList
)
return@withContext
}
// 所有页拉取完毕,保存最终时间戳(只升不降)
val storedTimestamp = SpTool.lastFaceTimestamp
if (faceTimestamp > storedTimestamp) {
SpTool.lastFaceTimestamp = faceTimestamp
}
try {
refreshFaceList()
} catch (e: Exception) {
@@ -246,4 +273,11 @@ class NetViewModelV2 : ViewModel() {
}
}
/**
* 从人脸数据列表中取出最大的 faceUpdateTimestamp(跳过 null 值)
*/
private fun maxFaceTimestamp(list: List<UserFaceModelV2>): Long {
return list.mapNotNull { it.faceUpdateTimestamp }.maxOrNull() ?: 0L
}
}
@@ -324,6 +324,54 @@
android:textColor="#FFCC99"
android:textSize="22sp" />
<!-- ===== 人脸库 ===== -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="人脸库"
android:textColor="#FFCC99"
android:textSize="20sp"
android:textStyle="bold" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tvFaceCount"
android:layout_width="0dp"
android:layout_height="60dp"
android:layout_weight="1"
android:background="@drawable/btn_outline"
android:gravity="center"
android:text="人脸总数:--"
android:textColor="#FFCC99"
android:textSize="20sp" />
<View
android:layout_width="20dp"
android:layout_height="1dp" />
<TextView
android:id="@+id/btnRefreshFaceCount"
android:layout_width="0dp"
android:layout_height="60dp"
android:layout_weight="1"
android:background="@drawable/btn_outline"
android:gravity="center"
android:text="刷新"
android:textColor="#FFCC99"
android:textSize="20sp" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="30dp" />
</LinearLayout>
</ScrollView>