feat(api): 新增V2版本接口支持并集成至网络请求客户端
- 新增 ApiServiceV2 接口,支持人脸缓存、增量数据、设备配置等接口 - ApiClient 中新增 apiServiceV2 对象,复用 Retrofit 实例,调整超时时间至60秒 - DeviceInitActivity 改用 netViewModelV2 进行人脸缓存全量拉取及设备配置读取 - BaseActivity 新增对人脸增量同步接口调用,调用成功后刷新本地识别缓存 - BaseActivity 新增清空本地人脸库功能,异步清理数据库并刷新识别缓存 - FaceApi 增加清空本地人脸库接口,重写数据库操作逻辑 - 升级 lib_face 模块 Room 数据库版本,新增字段以支持多标识人脸实体扩展 - FaceEntity 实体扩展会员编号、用户ID、人脸ID、会员标识、更新时间等字段 - FaceDao 新增按人脸ID查询和删除接口,支持多重人脸数据操作 - 优化网络层 OkHttpClient 配置,简化拦截器写法及日志级别判断 - 升级 lib_face 模块支持 arm64-v8a 架构,增强兼容性 - 规范模块间依赖关系,统一版本管理及包路径声明 - 完善 CLAUDE.md 文档,补充项目架构、模块划分与开发流程说明
This commit is contained in:
@@ -0,0 +1,325 @@
|
||||
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.setLastFaceTimestamp(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.getLastFaceTimestamp()
|
||||
if (faceTimestamp > storedTimestamp) {
|
||||
SpTool.setLastFaceTimestamp(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 {
|
||||
// 优先按 userFaceId 精确判重,避免特征字段不一致导致重复入库
|
||||
val userFaceId = model.userFaceId
|
||||
if (!userFaceId.isNullOrEmpty() && faceApi.queryByUserFaceId(userFaceId) != null) {
|
||||
// 本地已存在该 userFaceId,跳过插入
|
||||
return@forEach
|
||||
}
|
||||
// userFaceId 为空时,回退到特征比较去重(修正字段来源,保证与入库一致)
|
||||
val featureStr = model.resolveFeatureStr()
|
||||
val existList = faceApi.queryAllByUserName(model.userId)
|
||||
val alreadyExists = existList.any { entity ->
|
||||
featureStr != null && Base64.encode(entity.featureData) == featureStr
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user