- 在 FaceDao 中新增按 userFaceId 删除人脸记录的SQL语句 - 在 FaceApi 类中新增 deleteByUserFaceId 方法调用该DAO方法 - 在 NetViewModelV2 中删除人脸时改为按 userFaceId 删除,避免误删所有同用户特征
319 lines
11 KiB
Kotlin
319 lines
11 KiB
Kotlin
package com.sw.platecabinet.viewmodel
|
||
|
||
import androidx.lifecycle.ViewModel
|
||
import androidx.lifecycle.viewModelScope
|
||
import com.arcsoft.face.ErrorInfo
|
||
import com.sw.inbound.utils.SPUtil
|
||
import com.sw.plate.App
|
||
import com.sw.plate.utils.Base64
|
||
import com.sw.plate.utils.ToastUtils
|
||
import com.sw.plate.utils.arcface.FaceApi
|
||
import com.sw.plate.utils.arcface.facedb.FaceDatabase
|
||
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
|
||
import com.sw.platecabinet.GlobalData
|
||
import com.sw.platecabinet.GlobalKey
|
||
import com.sw.platecabinet.model.DeviceConfigV2
|
||
import com.sw.platecabinet.model.response.UserFaceModel
|
||
import com.sw.platecabinet.model.response.UserFaceModelV2
|
||
import com.sw.platecabinet.network.ApiClient
|
||
import com.sw.platecabinet.repository.RemoteRepositoryV2
|
||
import com.sw.platecabinet.utils.SpTool
|
||
import kotlinx.coroutines.Dispatchers
|
||
import kotlinx.coroutines.launch
|
||
import kotlinx.coroutines.withContext
|
||
import timber.log.Timber
|
||
import java.io.File
|
||
|
||
/**
|
||
* 用户viewmodel
|
||
*/
|
||
class NetViewModelV2 : ViewModel() {
|
||
|
||
companion object {
|
||
const val PAGE_SIZE = 100
|
||
}
|
||
|
||
private val faceApi: FaceApi = FaceApi()
|
||
private val repository = RemoteRepositoryV2(ApiClient.apiServiceV2)
|
||
|
||
|
||
//fun getUserFace(
|
||
// pageNo: Int,
|
||
// pageSize: Int,
|
||
// callback: (Boolean, List<UserFaceModelV2>) -> Unit
|
||
//) {
|
||
// viewModelScope.launch {
|
||
// val response = repository.getUserFaceCache(pageNum = pageNo, pageSize = pageSize)
|
||
// if (response.isSuccess()) callback(true, response.data ?: emptyList())
|
||
// else callback(false, emptyList())
|
||
// }
|
||
//}
|
||
|
||
/**
|
||
* 获取人脸数据
|
||
*/
|
||
fun getUserFaceCache(
|
||
pageNo: Int = 1,
|
||
pageSize: Int = PAGE_SIZE,
|
||
callback: (Boolean, String?) -> Unit = { _, _ -> }
|
||
) {
|
||
var currentPageNo = pageNo
|
||
viewModelScope.launch {
|
||
val response = repository.getUserFaceCache(pageNo, pageSize)
|
||
if (response.isSuccess().not()) {
|
||
callback(false, response.msg)
|
||
return@launch
|
||
}
|
||
// 获取成功一次后缓存状态
|
||
SPUtil.getInstance().put(GlobalKey.KEY_FIRST_RUN, true)
|
||
val list = response.data ?: emptyList()
|
||
if (pageNo == 1 && list.isEmpty()) {
|
||
callback(false, "暂无数据")
|
||
return@launch
|
||
}
|
||
val faceEntityList = list.mapNotNull { face ->
|
||
val featureBase64 = face.resolveFeatureStr()
|
||
if (featureBase64.isNullOrBlank()) null
|
||
else {
|
||
val featureData = base64ToByteArray(featureBase64)
|
||
if (featureData == null) null
|
||
else face.toFaceEntity()
|
||
}
|
||
}
|
||
withContext(Dispatchers.Default) {
|
||
if (currentPageNo == 1) {
|
||
//第一页数据删除本地数据库,同时重置时间戳
|
||
faceApi.clearFaceData()
|
||
faceTimestamp = 0L
|
||
}
|
||
faceApi.updateFaceData(pageNo, faceEntityList)
|
||
if (list.size >= pageSize) {
|
||
// 取当前页最大时间戳,与已累积的比较取最大值
|
||
val pageMaxTimestamp = maxFaceTimestamp(list)
|
||
if (pageMaxTimestamp > faceTimestamp) {
|
||
faceTimestamp = pageMaxTimestamp
|
||
}
|
||
currentPageNo++
|
||
getUserFaceCache(
|
||
pageNo = currentPageNo, callback = callback
|
||
)
|
||
return@withContext
|
||
}
|
||
if (list.isNotEmpty()) {
|
||
// 最后一页:取当前页最大时间戳,与已累积的比较取最大值
|
||
val pageMaxTimestamp = maxFaceTimestamp(list)
|
||
if (pageMaxTimestamp > faceTimestamp) {
|
||
faceTimestamp = pageMaxTimestamp
|
||
}
|
||
}
|
||
SpTool.lastFaceTimestamp = faceTimestamp
|
||
callback(true, "查询完成")
|
||
}
|
||
}
|
||
}
|
||
|
||
private var faceTimestamp = 0L
|
||
|
||
|
||
/**
|
||
* 激活人脸识别引擎
|
||
*/
|
||
fun activeEngine() {
|
||
Timber.d("activeEngine")
|
||
|
||
faceApi.activeEngine(
|
||
App.getContext(),
|
||
GlobalData.appId,
|
||
GlobalData.sdkKey,
|
||
GlobalData.activeKey,
|
||
object : FaceApi.ActiveCallback {
|
||
override fun onSuccess(activeCode: Int) {
|
||
Timber.d("activeEngine activeCode = $activeCode")
|
||
// ThreadUtils.launch {
|
||
viewModelScope.launch(Dispatchers.Main) {
|
||
when (activeCode) {
|
||
ErrorInfo.MOK -> {
|
||
ToastUtils.showToast("激活引擎成功")
|
||
}
|
||
|
||
ErrorInfo.MERR_ASF_ALREADY_ACTIVATED -> {
|
||
// ToastUtils.showToast("引擎已激活,无需再次激活")
|
||
}
|
||
|
||
else -> {
|
||
ToastUtils.showToast("激活引擎失败($activeCode)")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
override fun onFail(e: Exception?) {
|
||
viewModelScope.launch(Dispatchers.Main) {
|
||
ToastUtils.showToast("激活引擎异常,${e?.message}")
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
|
||
fun getDeviceConfig(onSuccess: (DeviceConfigV2?) -> Unit, onFailure: (String?) -> Unit) {
|
||
// Timber.tag(TAG).d("getDeviceConfig")
|
||
viewModelScope.launch {
|
||
val response = repository.getDeviceConfig()
|
||
if (response.isSuccess()) {
|
||
onSuccess(response.data)
|
||
} else {
|
||
onFailure(response.msg)
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* 增量获取人脸数据(支持分页递归,取所有页中的最大时间戳)
|
||
*/
|
||
fun getFaceIncrementList(
|
||
pageNo: Int = 1,
|
||
pageSize: Int = PAGE_SIZE,
|
||
timestamp: Long,
|
||
refreshFaceList: () -> Unit
|
||
) {
|
||
var currentPageNo = pageNo
|
||
viewModelScope.launch {
|
||
val response = repository.getFaceIncrementList(
|
||
pageNum = currentPageNo.toLong(),
|
||
pageSize = pageSize.toLong(),
|
||
timestamp = timestamp
|
||
)
|
||
if (response.isSuccess().not()) {
|
||
return@launch
|
||
}
|
||
withContext(Dispatchers.IO) {
|
||
val list = response.data ?: emptyList()
|
||
updateFaceData(list)
|
||
// 取当前页最大时间戳,与已累积的比较取最大值
|
||
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) {
|
||
e.printStackTrace()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private fun updateFaceData(list: List<UserFaceModelV2>) {
|
||
try {
|
||
list.forEach { model ->
|
||
if (model.faceDeleted == true) {
|
||
//删除数据
|
||
// 仅删除 userFaceId 对应的单条人脸记录,避免误删该用户名下所有特征
|
||
val userFaceId = model.userFaceId
|
||
if (!userFaceId.isNullOrEmpty()) {
|
||
faceApi.deleteByUserFaceId(userFaceId)
|
||
}
|
||
} else {
|
||
// 获取该用户所有已存特征,判断当前特征是否已存在
|
||
val existList = faceApi.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) {
|
||
faceApi.insert(model.toFaceEntity())
|
||
}
|
||
}
|
||
}
|
||
} catch (e: Exception) {
|
||
e.printStackTrace()
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 添加人脸数据
|
||
*/
|
||
fun addUserFace(faceData: String, imageFile: File, block: (Boolean, UserFaceModelV2?) -> Unit) {
|
||
viewModelScope.launch {
|
||
try {
|
||
val imageResp = repository.uploadImage(imageFile)
|
||
val imageUrl = imageResp.data ?: ""
|
||
if (imageUrl.isBlank()) {
|
||
block(false, null)
|
||
return@launch
|
||
}
|
||
val faceResp = repository.addUserFace(faceData, imageUrl)
|
||
if (faceResp.isSuccess().not()) {
|
||
block(false, null)
|
||
return@launch
|
||
}
|
||
block(true, faceResp.data)
|
||
} catch (e: Exception) {
|
||
// 网络或解析异常时同样回调失败,避免 block 永不触发导致上层无反馈
|
||
e.printStackTrace()
|
||
block(false, null)
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 取餐盘上报(P-09a):记录用户开始就餐时刻,作为后续就餐记录(P-09)的强前置。
|
||
* 失败仅回调通知,不阻断吐盘等业务主流程。
|
||
*
|
||
* @param userId 用户 id(餐盘号 = userId)
|
||
* @param callback 上报结果回调:Boolean 是否成功,String? 成功为 recordNo、失败为错误信息
|
||
*/
|
||
fun platePickup(
|
||
userId: Long,
|
||
callback: (Boolean, String?) -> Unit = { _, _ -> }
|
||
) {
|
||
viewModelScope.launch {
|
||
try {
|
||
val response = repository.platePickup(userId)
|
||
if (response.isSuccess()) {
|
||
callback(true, response.data)
|
||
} else {
|
||
callback(false, response.msg)
|
||
}
|
||
} catch (e: Exception) {
|
||
e.printStackTrace()
|
||
callback(false, e.message)
|
||
}
|
||
}
|
||
}
|
||
|
||
private fun base64ToByteArray(base64: String?): ByteArray? {
|
||
base64 ?: return null
|
||
return try {
|
||
Base64.decode(base64)
|
||
} catch (e: Exception) {
|
||
null
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 从人脸数据列表中取出最大的 faceUpdateTimestamp(跳过 null 值)
|
||
*/
|
||
private fun maxFaceTimestamp(list: List<UserFaceModelV2>): Long {
|
||
return list.mapNotNull { it.faceUpdateTimestamp }.maxOrNull() ?: 0L
|
||
}
|
||
|
||
} |