feat(network): 添加API V2版本支持并重构网络层

- 新增ApiServiceV2接口定义,支持人脸缓存、增量数据、设备配置等功能
- 添加NetViewModelV2视图模型,实现新版本API的数据处理逻辑
- 创建RemoteRepositoryV2仓库,封装API V2的网络请求操作
- 新增UserFaceModelV2数据模型,优化人脸特征数据结构
- 添加DeviceConfigV2设备配置模型,支持更多设备参数
- 在ApiClient中注册apiServiceV2实例
- 更新BaseActivity使用NetViewModelV2替代UserViewModel
- 修改DeviceInitActivity中的人脸数据获取和设备配置逻辑
- 重构FaceDao数据库访问对象,优化用户类型查询和删除操作
- 更新FaceEntity实体类,增加更多用户相关字段
- 移除BaseViewModel中的旧版网络基础功能代码
- 优化人脸数据同步和存储流程,提升性能表现
This commit is contained in:
2026-06-30 10:08:10 +08:00
parent e8735db27b
commit 95b4f5e8f2
15 changed files with 792 additions and 395 deletions
@@ -25,11 +25,10 @@ import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
import com.sw.platecabinet.R
import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.ext.clickWithCoroutines
import com.sw.platecabinet.model.response.UserFaceModel
import com.sw.platecabinet.utils.IntervalExecutor
import com.sw.platecabinet.utils.SpTool
import com.sw.platecabinet.view.CustomLoadingDialog
import com.sw.platecabinet.viewmodel.UserViewModel
import com.sw.platecabinet.viewmodel.NetViewModelV2
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
@@ -48,10 +47,8 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
// private val permissionHelpers = mutableMapOf<Int, PermissionHelper>()
// protected var keyEventHelper: ScanGunKeyEventHelper? = null
// 用户对应的viewModel
protected val userViewModel by viewModels<UserViewModel>()
public val recognizeViewModel by viewModels<RecognizeViewModel>()
public val netViewModelV2 by viewModels<NetViewModelV2>()
private var startTime: Long = 0
@@ -298,84 +295,23 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
// private val dealyMillis = 10 * 60 * 1000L
private val initialDelay = 1 * 1000L
private val dealyMillis = 10 * 1000L
fun startFaceTask(block: (List<String?>) -> Unit) {
fun startFaceTask() {
faceTaskJob =
intervalExecutor.startIntervalTaskWithInitialDelay(initialDelay, dealyMillis) {
getFaceIncrementList(action = block)
getFaceIncrementList()
}
}
private var faceTimestamp = 0L
//private var taskPageNo = 1
private fun getFaceIncrementList(pageNo: Int = 1, action: (List<String?>) -> Unit) {
private fun getFaceIncrementList(pageNo: Int = 1) {
val timestamp = SpTool.lastFaceTimestamp
userViewModel.getFaceIncrementList(
netViewModelV2.getFaceIncrementList(
pageNo = pageNo,
timestamp = timestamp
) { list ->
runOnUiThread {
if (pageNo == 1 && list.isEmpty()) {
return@runOnUiThread
}
updateFaceData(list)
val userIdList = list.map { it.userId }
action(userIdList)
if (list.isNotEmpty()) {
faceTimestamp = list.last().faceUpdateTimestamp ?: 0
}
SpTool.lastFaceTimestamp = faceTimestamp
}
}
}
private fun updateFaceData(list: List<UserFaceModel>) {
lifecycleScope.launch(Dispatchers.IO) {
val faceList = mutableListOf<FaceEntity>()
val faceDao = FaceDatabase.getInstance(this@BaseActivity).faceDao()
try {
list.forEach { model ->
if (model.faceDeleted == true) {
//删除数据
faceDao.deleteFaceById(model.userId)
} else {
// 获取该用户所有已存特征,判断当前特征是否已存在
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) {
//当前特征不存在,保存
runCatching { getNewFaceEntity(model) }
.onSuccess { faceList.add(it) }
.onFailure { Timber.e(it, "人脸特征解码失败: userId=${model.userId}") }
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
try {
if (faceList.isNotEmpty()) {
FaceDatabase.getInstance(this@BaseActivity).faceDao().insert(faceList)
}
recognizeViewModel.refreshFaceList()
} catch (e: Exception) {
e.printStackTrace()
}
}
}
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
) {
recognizeViewModel.refreshFaceList()
}
}
@@ -3,7 +3,6 @@ package com.sw.platecabinet.activity
import android.content.Intent
import androidx.lifecycle.lifecycleScope
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.FaceApi
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.databinding.ActivityDeviceInitBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding
@@ -32,7 +31,7 @@ class DeviceInitActivity : BaseActivity<ActivityDeviceInitBinding>() {
//val count = FaceApi().queryFaceCount()
//if (count == 0) {
if (SpTool.firstGetFace) {
userViewModel.getUserFaceCache { status, msg ->
netViewModelV2.getUserFaceCache { status, msg ->
SpTool.firstGetFace = false
getDeviceConfig()
}
@@ -44,7 +43,7 @@ class DeviceInitActivity : BaseActivity<ActivityDeviceInitBinding>() {
}
private fun getDeviceConfig() {
userViewModel.getDeviceConfig { deviceConfig ->
netViewModelV2.getDeviceConfig(onSuccess = { deviceConfig ->
runOnUiThread {
if (deviceConfig == null) {
hideWaitingDialog()
@@ -61,7 +60,11 @@ class DeviceInitActivity : BaseActivity<ActivityDeviceInitBinding>() {
goLoginActivity()
}, 500)
}
}
}, onFailure = { errMsg ->
runOnUiThread {
ToastUtils.showToast(errMsg ?: "接口异常")
}
})
}
override fun registerDataChange() {
@@ -20,7 +20,6 @@ import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.arcsoft.face.ErrorInfo
import com.bumptech.glide.Glide
import com.google.gson.Gson
import com.sw.inbound.utils.DateTimeUtils
import com.sw.plate.utils.Base64
import com.sw.plate.utils.ToastUtils
@@ -33,7 +32,6 @@ import com.sw.plate.utils.arcface.PreviewConfig
import com.sw.plate.utils.arcface.camera.CameraListener
import com.sw.plate.utils.arcface.camera.DualCameraHelper
import com.sw.plate.utils.arcface.face.constants.LivenessType
import com.sw.plate.utils.arcface.face.constants.RecognizeColor
import com.sw.plate.utils.arcface.face.model.CompareResult
import com.sw.plate.utils.arcface.face.model.FacePreviewInfo
import com.sw.plate.utils.arcface.face.model.RecognizeConfiguration
@@ -49,7 +47,7 @@ import com.sw.platecabinet.dialog.CustomDialog
import com.sw.platecabinet.ext.clickWithDebounce
import com.sw.platecabinet.ext.gone
import com.sw.platecabinet.ext.visible
import com.sw.platecabinet.model.response.UserFaceModel
import com.sw.platecabinet.model.response.UserFaceModelV2
import com.sw.platecabinet.utils.BitmapSaver
import com.sw.platecabinet.utils.CountDownUtil
import com.sw.platecabinet.utils.Debouncer
@@ -113,9 +111,7 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
EventBus.getDefault().register(this)
//开启人脸增量数据定时任务
startFaceTask {
loadLogInfo("collectFace增量接口数据用户id:${Gson().toJson(it)}")
}
startFaceTask()
val soundMap = hashMapOf<String, Int>()
soundMap[COLLECT_SUCCESS] = R.raw.collect_success
@@ -887,7 +883,7 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
}
private fun addUserFace(faceData: String, imageFile: File) {
userViewModel.addUserFace(faceData, imageFile) { status, faceModel ->
netViewModelV2.addUserFace(faceData, imageFile) { status, faceModel ->
loadLogInfo("collectFace,saveFaceInfo: ----------12,人脸数据发送完成时间:${DateTimeUtils.getDateTimeString()},结果:$status-----------")
if (status.not()) {
ToastUtils.showToast("新增数据失败")
@@ -943,18 +939,11 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
private var currentUserId: String? = null
private fun saveFaceInfo(faceModel: UserFaceModel, block: () -> Unit = {}) {
private fun saveFaceInfo(faceModel: UserFaceModelV2, block: () -> Unit = {}) {
try {
loadLogInfo("collectFace,saveFaceInfo: ----------8,保存人脸取特征数据-----------")
val faceDao = FaceDatabase.getInstance(this).faceDao()
val faceEntity = FaceEntity(
faceModel.userId,
null,
Base64.decode(faceModel.faceFeatureStr)
).also {
//1-会员、2-临时用户
it.userType = if (faceModel.member) "1" else "2"
}
val faceEntity = faceModel.toFaceEntity()
val faceId = faceDao.insert(faceEntity)
faceEntity.faceId = faceId
loadLogInfo("collectFace,saveFaceInfo: ----------10,人脸数据保存完成-----------")
@@ -1,5 +1,7 @@
package com.sw.platecabinet.model
import com.google.gson.annotations.SerializedName
/**
* 错误信息 code == 200 成功
*/
@@ -19,6 +21,27 @@ data class DeviceConfig(
var arcsoftActiveKey: String? = null
)
data class DeviceConfigV2(
/** 业务服务器 BASE URL */
val appPackageUrl: String? = null,
/** 食堂/餐厅名称 */
val canteenName: String? = null,
/** 食堂/餐厅 ID(后续所有业务 API 的 restId */
val canteenId: String? = null,
/** 虹软人脸 SDK App ID */
val arcsoftAppId: String? = null,
/** 虹软人脸 SDK Key */
val arcsoftSdkKey: String? = null,
/** 虹软人脸 SDK Active Key */
val arcsoftActiveKey: String? = null,
/** MQTT 客户端服务器 IP */
val clientServerIp: String? = null,
/** 智慧食堂服务器 IP(MQTT 端口) */
val zhstServerIp: String? = null
)
data class CodeMsg(
val code: String? = "",
val msg: String? = ""
@@ -9,19 +9,6 @@ import kotlinx.parcelize.Parcelize
*/
@Parcelize
data class UserFaceModel(
// @SerializedName("faceFeature")
// val faceFeature: String? = "",
// @SerializedName("faceFeatureString")
// val faceFeatureString: String? = "",
// @SerializedName("faceType")
// val faceType: String? = "",
// @SerializedName("userFaceId")
// val userFaceId: String? = "",
// @SerializedName("userId")
// val userId: String? = "",
// @SerializedName("updateTimeStamp")
// val updateTimeStamp: Long = 0,
val userId: String? = "",
val faceFeatureStr: String? = "",
val faceUpdateTimestamp: Long? = null,
@@ -32,10 +19,10 @@ data class UserFaceModel(
/**
* 会员编号
*/
val cardNo: String,
val cardNo: String? = null,
/**
* 是否会员
*/
val member: Boolean
val member: Boolean? = null
) : Parcelable
@@ -0,0 +1,44 @@
package com.sw.platecabinet.model.response
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
import kotlinx.parcelize.Parcelize
/**
* 用户人脸信息
*/
@Parcelize
data class UserFaceModelV2(
val userFaceId: String? = null,
val userId: String? = null,
val faceFeature: String? = null,
val faceFeatureStr: String? = null,
val faceFeatureString: String? = null,
val faceUpdateTimestamp: Long? = null,
val cardNo: String? = null,
val member: Boolean? = false,
val faceDeleted: Boolean? = false,
val personType: String? = null
) : Parcelable {
fun resolveFeatureStr(): String? {
return faceFeature?.takeIf { it.isNotEmpty() }
?: faceFeatureStr?.takeIf { it.isNotEmpty() }
?: faceFeatureString?.takeIf { it.isNotEmpty() }
}
fun toFaceEntity(): FaceEntity {
val featureBase64 = resolveFeatureStr()
return FaceEntity(
userId,
android.util.Base64.decode(featureBase64, android.util.Base64.DEFAULT),
personType,
cardNo,
userId,
userFaceId,
member ?: false,
faceUpdateTimestamp ?: 0L
)
}
}
@@ -3,6 +3,7 @@ package com.sw.platecabinet.network
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.MyApp
import com.sw.platecabinet.network.api.ApiService
import com.sw.platecabinet.network.api.ApiServiceV2
import com.sw.platecabinet.network.interceptor.RequestInterceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
@@ -37,4 +38,8 @@ object ApiClient {
val apiService: ApiService by lazy {
retrofit.create(ApiService::class.java)
}
val apiServiceV2: ApiServiceV2 by lazy {
retrofit.create(ApiServiceV2::class.java)
}
}
@@ -0,0 +1,66 @@
package com.sw.platecabinet.network.api
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.model.DeviceConfigV2
import com.sw.platecabinet.model.response.ApiResponse
import com.sw.platecabinet.model.response.UserFaceModel
import com.sw.platecabinet.model.response.UserFaceModelV2
import okhttp3.MultipartBody
import retrofit2.http.Body
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.GET
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part
import retrofit2.http.Url
interface ApiServiceV2 {
/**
* 获取人脸缓存数据
*/
@POST
suspend fun getUserFaceCache(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/common/face/page",
@Body param: Map<String, Int>
): ApiResponse<List<UserFaceModelV2>>
/**
* 获取人脸增量数据
*/
@POST
suspend fun getFaceIncrementList(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/common/face/increment",
@Body param: Map<String, Long>
): ApiResponse<List<UserFaceModelV2>?>
/**
* 获取设备配置数据
*/
@GET
suspend fun getDeviceConfig(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/pickup/device/config"
): ApiResponse<DeviceConfigV2?>
/**
* 添加人脸数据
*/
@POST
@FormUrlEncoded
suspend fun addUserFace(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/user/add-by-face",
@Field("featureChar") faceData: String,
@Field("url") imageUr: String
): ApiResponse<UserFaceModelV2?>
/**
* 上传图片
*/
@Multipart
@POST
suspend fun uploadImage(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/upload",
@Part file: MultipartBody.Part,
): ApiResponse<String?>
}
@@ -0,0 +1,70 @@
package com.sw.platecabinet.repository
import com.sw.platecabinet.model.DeviceConfigV2
import com.sw.platecabinet.model.response.ApiResponse
import com.sw.platecabinet.model.response.UserFaceModel
import com.sw.platecabinet.model.response.UserFaceModelV2
import com.sw.platecabinet.network.api.ApiServiceV2
import com.sw.platecabinet.utils.FileUtils
import java.io.File
/**
* 远程数据处理
*/
class RemoteRepositoryV2 constructor(
private val apiService: ApiServiceV2
) : BaseRepository() {
/**
* 获取人脸数据
*/
suspend fun getUserFaceCache(
pageNum: Int,
pageSize: Int = 100,
): ApiResponse<List<UserFaceModelV2>> {
return safeApiCall {
apiService.getUserFaceCache(
param = mapOf(
"pageNum" to pageNum,
"pageSize" to pageSize
)
)
}
}
suspend fun getFaceIncrementList(
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
)
)
}
}
suspend fun getDeviceConfig(): ApiResponse<DeviceConfigV2?> {
return safeApiCall { apiService.getDeviceConfig() }
}
suspend fun addUserFace(faceData: String, imageUr: String): ApiResponse<UserFaceModelV2?> {
return safeApiCall { apiService.addUserFace(faceData = faceData, imageUr = imageUr) }
}
suspend fun uploadImage(file: File): ApiResponse<String?> {
return safeApiCall {
val part = FileUtils.genRequestPart(file)
if (part == null) {
ApiResponse(code = "-1", msg = "解析图片失败")
} else {
apiService.uploadImage(file = part)
}
}
}
}
@@ -1,74 +1,74 @@
package com.sw.platecabinet.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.sw.plate.utils.ToastUtils
import com.sw.platecabinet.model.response.ApiResponse
import com.sw.platecabinet.network.ApiClient
import com.sw.platecabinet.repository.RemoteRepository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
abstract class BaseViewModel() : ViewModel() {
protected val repository = RemoteRepository(ApiClient.apiService)
// 显示进度条
private val _showLoading = MutableStateFlow<Boolean>(false)
val showLoading: StateFlow<Boolean> = _showLoading
/**
* 带进度条的请求
*/
protected fun launchWithLoading(block: suspend () -> Unit) {
viewModelScope.launch {
try {
_showLoading.value = true
block()
} catch (e: Exception) {
// 错误处理可被子类重写
handleError(e)
} finally {
_showLoading.value = false
}
}
}
/**
* 不带进度条的请求
*/
protected fun launch(block: suspend () -> Unit) {
viewModelScope.launch() {
try {
block()
} catch (e: Exception) {
// 错误处理可被子类重写
handleError(e)
}
}
}
protected open fun parseResponse(response: ApiResponse<*>): Boolean {
if (response.isSuccess()) {
return true
}
Timber.d("msg = ${response.msg}, code = ${response.code}")
ToastUtils.showToast("${response.msg}(${response.code})")
return false
}
fun handleError(exception: Exception) {
Timber.d("handleError ${exception.message}")
}
// fun parseEquipmentInfo(equipmentInfo: EquipmentInfo) {
//// GlobalData.appBaseUrl = equipmentInfo.appPackageUrl!!
//// GlobalData.appBaseUrl="https://vip.shuziweidao.com"
//// GlobalData.sdkKey = equipmentInfo.arcsoftSdkKey!!
//// GlobalData.appId = equipmentInfo.arcsoftAppId!!
//// GlobalData.activeKey = equipmentInfo.arcsoftActiveKey!!
//// GlobalData.restId = equipmentInfo.canteenId!!
//package com.sw.platecabinet.viewmodel
//
//import androidx.lifecycle.ViewModel
//import androidx.lifecycle.viewModelScope
//import com.sw.plate.utils.ToastUtils
//import com.sw.platecabinet.model.response.ApiResponse
//import com.sw.platecabinet.network.ApiClient
//import com.sw.platecabinet.repository.RemoteRepository
//import kotlinx.coroutines.flow.MutableStateFlow
//import kotlinx.coroutines.flow.StateFlow
//import kotlinx.coroutines.launch
//import timber.log.Timber
//
//abstract class BaseViewModel() : ViewModel() {
// protected val repository = RemoteRepository(ApiClient.apiService)
//
// // 显示进度条
// private val _showLoading = MutableStateFlow<Boolean>(false)
// val showLoading: StateFlow<Boolean> = _showLoading
//
// /**
// * 带进度条的请求
// */
// protected fun launchWithLoading(block: suspend () -> Unit) {
// viewModelScope.launch {
// try {
// _showLoading.value = true
// block()
// } catch (e: Exception) {
// // 错误处理可被子类重写
// handleError(e)
// } finally {
// _showLoading.value = false
// }
// }
// }
}
//
// /**
// * 不带进度条的请求
// */
// protected fun launch(block: suspend () -> Unit) {
// viewModelScope.launch() {
// try {
// block()
// } catch (e: Exception) {
// // 错误处理可被子类重写
// handleError(e)
// }
// }
// }
//
// protected open fun parseResponse(response: ApiResponse<*>): Boolean {
// if (response.isSuccess()) {
// return true
// }
// Timber.d("msg = ${response.msg}, code = ${response.code}")
// ToastUtils.showToast("${response.msg}(${response.code})")
// return false
// }
//
//
// fun handleError(exception: Exception) {
// Timber.d("handleError ${exception.message}")
// }
//
//// fun parseEquipmentInfo(equipmentInfo: EquipmentInfo) {
////// GlobalData.appBaseUrl = equipmentInfo.appPackageUrl!!
////// GlobalData.appBaseUrl="https://vip.shuziweidao.com"
////// GlobalData.sdkKey = equipmentInfo.arcsoftSdkKey!!
////// GlobalData.appId = equipmentInfo.arcsoftAppId!!
////// GlobalData.activeKey = equipmentInfo.arcsoftActiveKey!!
////// GlobalData.restId = equipmentInfo.canteenId!!
//// }
//}
@@ -0,0 +1,249 @@
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()
}
faceApi.updateFaceData(pageNo, faceEntityList)
if (list.size >= pageSize) {
faceTimestamp = list.last().faceUpdateTimestamp ?: 0
currentPageNo++
getUserFaceCache(
pageNo = currentPageNo, callback = callback
)
return@withContext
}
if (list.isNotEmpty()) {
faceTimestamp = list.last().faceUpdateTimestamp ?: 0
}
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
) {
viewModelScope.launch {
val response = repository.getFaceIncrementList(
pageNum = pageNo.toLong(),
pageSize = pageSize.toLong(),
timestamp = timestamp
)
if (response.isSuccess().not()) {
return@launch
}
withContext(Dispatchers.IO) {
val list = response.data ?: emptyList()
updateFaceData(list)
if (list.isNotEmpty()) {
faceTimestamp = list.last().faceUpdateTimestamp ?: 0
}
SpTool.lastFaceTimestamp = faceTimestamp
try {
refreshFaceList()
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
private fun updateFaceData(list: List<UserFaceModelV2>) {
try {
list.forEach { model ->
if (model.faceDeleted == true) {
//删除数据
faceApi.deleteByUserName(model.userId)
} 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 {
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)
}
}
private fun base64ToByteArray(base64: String?): ByteArray? {
base64 ?: return null
return try {
Base64.decode(base64)
} catch (e: Exception) {
null
}
}
}
@@ -1,201 +1,201 @@
package com.sw.platecabinet.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.entity.FaceEntity
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.GlobalKey
import com.sw.platecabinet.model.DeviceConfig
import com.sw.platecabinet.model.response.UserFaceModel
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 UserViewModel : BaseViewModel() {
companion object {
const val PAGE_SIZE = 100
}
private val faceApi: FaceApi = FaceApi()
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>()
}
}
/**
* 获取人脸数据
*/
fun getUserFaceCache(
pageNo: Int = 1,
pageSize: Int = PAGE_SIZE,
callback: (Boolean, String?) -> Unit = { statue, msg -> }
) {
var currentPageNo = pageNo
launchWithLoading {
val response = repository.getUserFaceCache(pageNum = pageNo, pageSize = pageSize)
val status = parseResponse(response)
if (status.not()) {
callback(false, response.msg)
return@launchWithLoading
}
// 获取成功一次后缓存状态
SPUtil.getInstance().put(GlobalKey.KEY_FIRST_RUN, true)
val list: List<UserFaceModel> = response.data ?: emptyList()
if (pageNo == 1 && list.isEmpty()) {
callback(false, "暂无数据")
return@launchWithLoading
}
val faceEntityList = list.map {
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
currentPageNo++
getUserFaceCache(
pageNo = currentPageNo,
callback = callback
)
return@withContext
}
if (list.isNotEmpty()) {
faceTimestamp = list.last().faceUpdateTimestamp ?: 0
}
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(block: (DeviceConfig?) -> Unit) {
// Timber.tag(TAG).d("getDeviceConfig")
launch {
val response = repository.getDeviceConfig()
if (parseResponse(response)) {
block(response.data)
} else {
block(null)
}
}
}
/**
* 获取人脸数据
*/
fun getFaceIncrementList(
pageNo: Int = 1,
pageSize: Int = PAGE_SIZE,
timestamp: Long,
block: (List<UserFaceModel>) -> Unit
) {
launch {
val response = repository.getFaceIncrementList(
pageNum = pageNo.toLong(),
pageSize = pageSize.toLong(),
timestamp = timestamp
)
if (parseResponse(response)) {
withContext(Dispatchers.Default) {
val list: List<UserFaceModel> = response.data ?: emptyList()
block(list)
}
}
}
}
/**
* 添加人脸数据
*/
fun addUserFace(faceData: String, imageFile: File, block: (Boolean, UserFaceModel?) -> Unit) {
launch {
val imageResp = repository.uploadImage(imageFile)
val imageUrl = imageResp.data ?: ""
if (imageUrl.isBlank()) {
block(false, null)
return@launch
}
val faceResp = repository.addUserFace(faceData, imageUrl)
val status = parseResponse(faceResp)
if (status.not()) {
block(false, null)
return@launch
}
block(true, faceResp.data)
}
}
}
//package com.sw.platecabinet.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.entity.FaceEntity
//import com.sw.platecabinet.GlobalData
//import com.sw.platecabinet.GlobalKey
//import com.sw.platecabinet.model.DeviceConfig
//import com.sw.platecabinet.model.response.UserFaceModel
//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 UserViewModel : BaseViewModel() {
//
// companion object {
// const val PAGE_SIZE = 100
// }
//
// private val faceApi: FaceApi = FaceApi()
//
//
// 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>()
// }
// }
//
// /**
// * 获取人脸数据
// */
// fun getUserFaceCache(
// pageNo: Int = 1,
// pageSize: Int = PAGE_SIZE,
// callback: (Boolean, String?) -> Unit = { statue, msg -> }
// ) {
// var currentPageNo = pageNo
// launchWithLoading {
// val response = repository.getUserFaceCache(pageNum = pageNo, pageSize = pageSize)
// val status = parseResponse(response)
// if (status.not()) {
// callback(false, response.msg)
// return@launchWithLoading
// }
// // 获取成功一次后缓存状态
// SPUtil.getInstance().put(GlobalKey.KEY_FIRST_RUN, true)
// val list: List<UserFaceModel> = response.data ?: emptyList()
// if (pageNo == 1 && list.isEmpty()) {
// callback(false, "暂无数据")
// return@launchWithLoading
// }
// val faceEntityList = list.map {
// FaceEntity(it.userId, null, Base64.decode(it.faceFeatureStr)).apply {
// userType = if (it.member == true) "1" else "2"
// }
// }
// withContext(Dispatchers.Default) {
// if (currentPageNo == 1) {
// //第一页数据删除本地数据库
// faceApi.clearFaceData()
// }
// faceApi.updateFaceData(pageNo, faceEntityList)
// if (list.size >= pageSize) {
// faceTimestamp = list.last().faceUpdateTimestamp ?: 0
// currentPageNo++
// getUserFaceCache(
// pageNo = currentPageNo,
// callback = callback
// )
// return@withContext
// }
// if (list.isNotEmpty()) {
// faceTimestamp = list.last().faceUpdateTimestamp ?: 0
// }
// 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(block: (DeviceConfig?) -> Unit) {
//// Timber.tag(TAG).d("getDeviceConfig")
// launch {
// val response = repository.getDeviceConfig()
// if (parseResponse(response)) {
// block(response.data)
// } else {
// block(null)
// }
// }
// }
//
//
// /**
// * 获取人脸数据
// */
// fun getFaceIncrementList(
// pageNo: Int = 1,
// pageSize: Int = PAGE_SIZE,
// timestamp: Long,
// block: (List<UserFaceModel>) -> Unit
// ) {
// launch {
// val response = repository.getFaceIncrementList(
// pageNum = pageNo.toLong(),
// pageSize = pageSize.toLong(),
// timestamp = timestamp
// )
// if (parseResponse(response)) {
// withContext(Dispatchers.Default) {
// val list: List<UserFaceModel> = response.data ?: emptyList()
// block(list)
// }
// }
// }
// }
//
// /**
// * 添加人脸数据
// */
// fun addUserFace(faceData: String, imageFile: File, block: (Boolean, UserFaceModel?) -> Unit) {
// launch {
// val imageResp = repository.uploadImage(imageFile)
// val imageUrl = imageResp.data ?: ""
// if (imageUrl.isBlank()) {
// block(false, null)
// return@launch
// }
// val faceResp = repository.addUserFace(faceData, imageUrl)
// val status = parseResponse(faceResp)
// if (status.not()) {
// block(false, null)
// return@launch
// }
// block(true, faceResp.data)
// }
// }
//
//}