Files
StallDualScreen/app/src/main/java/com/sw/dualscreen/viewmodel/NetViewModelV2.kt
T
mazengfei 28a475ca43 feat(face): 优化人脸数据同步及类型映射
- 新增接口注释,完善全量及增量人脸数据获取说明
- FaceVO中faceUpdateTimestamp由Long改为String以防JS大数精度丢失
- 增加personType字段标识人员类型
- MainActivity保存人脸数据时映射完整字段,使用personType优先判断用户类型
- NetViewModelV2中获取人脸数据时改用完整构造函数创建FaceEntity
- 累积更新lastFaceTimestamp为当前页最大时间戳,避免时间戳遗漏
- 优化时间戳更新逻辑,避免直接使用列表最后一条数据
2026-07-15 10:56:40 +08:00

576 lines
20 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.sw.dualscreen.viewmodel
import androidx.lifecycle.viewModelScope
import com.arcsoft.face.ErrorInfo
import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.model.request.UserNutritionParam
import com.sw.dualscreen.model.request.v2.BindUserOrderRequest
import com.sw.dualscreen.model.request.v2.PlaceOrderRequest
import com.sw.dualscreen.model.response.ApiResponse
import com.sw.dualscreen.model.response.DinnerType
import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.model.response.FoodOrderModel
import com.sw.dualscreen.model.response.FoodSearchReq
import com.sw.dualscreen.model.response.MemberInfo
import com.sw.dualscreen.model.response.UserNutrition
import com.sw.dualscreen.model.response.v2.CollectedFoodV2
import com.sw.dualscreen.model.response.v2.FaceVO
import com.sw.dualscreen.model.response.v2.NewFoodInfo
import com.sw.dualscreen.model.response.v2.NewMemberInfo
import com.sw.dualscreen.model.response.v2.SettlementOrder
import com.sw.dualscreen.model.response.v2.toFoodInfo
import com.sw.dualscreen.model.response.v2.toFoodOrderModel
import com.sw.dualscreen.model.response.v2.toMemberInfo
import com.sw.dualscreen.network.ApiClient
import com.sw.dualscreen.utils.FileUtil
import com.sw.dualscreen.utils.SpTool
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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.File
class NetViewModelV2 : BaseViewModel() {
companion object {
private const val TAG = "NetViewModelV2"
const val PAGE_SIZE = 100L
}
private val faceApi: FaceApi = FaceApi()
/** 人脸加载完成 */
private val _loadFaceResult = MutableStateFlow<Boolean>(false)
val loadFaceResult: StateFlow<Boolean> = _loadFaceResult
/** 饭点类型 早餐/午餐/晚餐 */
private val _dinnerTypeInfo = MutableStateFlow<DinnerType?>(null)
val dinnerTypeInfo: StateFlow<DinnerType?> = _dinnerTypeInfo
fun getDeviceConfig(
onSuccess: (config: com.sw.dualscreen.model.response.DeviceConfig?) -> Unit,
onFailure: (message: String) -> Unit
) {
launch {
try {
val response = ApiClient.repositoryV2.getDeviceConfig()
if (parseResponse(response)) {
onSuccess(response.data)
} else {
onFailure(response.msg ?: "获取设备配置失败")
}
} catch (e: Exception) {
Timber.e(e, "获取设备配置异常")
onFailure("网络异常: ${e.message}")
}
}
}
/** 获取人脸全量数据(递归分页 + FaceApi 集成) */
fun getFacePage(
pageNum: Long = 1L,
pageSize: Long = PAGE_SIZE,
onSuccess: () -> Unit = {},
onFailure: (String) -> Unit = {}
) {
var currentPageNum = pageNum
Timber.tag(TAG).d("getFacePage index = $currentPageNum")
launch {
_loadFaceResult.value = false
// 重置持久化时间戳和内存累加器
SpTool.lastFaceTimestamp = 0
lastFaceTimestamp = 0L
FileUtil.saveLog("获取人脸全量数据开始,重置时间戳为0")
val response = ApiClient.repositoryV2.getFacePage(currentPageNum, pageSize)
if (!parseResponse(response)) {
onFailure("获取人脸数据失败,${response.msg}(${response.code})")
return@launch
}
withContext(Dispatchers.Default) {
val list = response.data ?: emptyList()
if (pageNum == 1L && list.isEmpty()) {
onFailure("查询人脸数据为空")
return@withContext
}
val faceEntity = list.map { vo ->
FaceEntity(
vo.userId, // userName
Base64.decode(vo.faceFeature), // featureData
vo.personType ?: if (vo.member == true) "1" else "2", // userType
vo.cardNo ?: "", // cardNo
vo.userId ?: "", // userId
vo.userFaceId ?: "", // userFaceId
vo.member ?: false, // member
vo.faceUpdateTimestamp?.toLongOrNull() ?: 0L // faceUpdateTimestamp
)
}
faceApi.updateFaceData(currentPageNum.toInt(), faceEntity)
// 取当前页最大时间戳(后端返回 String 类型,需转为 Long 比较),跨页累加防止中间页有更大值被覆盖
val pageMaxTimestamp = list.maxOf { it.faceUpdateTimestamp?.toLongOrNull() ?: 0L }
lastFaceTimestamp = maxOf(lastFaceTimestamp, pageMaxTimestamp)
if (list.size >= pageSize) {
currentPageNum++
getFacePage(currentPageNum, pageSize, onSuccess, onFailure)
return@withContext
}
SpTool.lastFaceTimestamp = lastFaceTimestamp
FileUtil.saveLog("获取人脸[全量]数据结束,时间戳为:$lastFaceTimestamp")
_loadFaceResult.value = true
onSuccess()
}
}
}
/** 获取人脸增量数据(FaceApi 集成) */
fun getFaceIncrement(
pageNum: Long = 1L,
pageSize: Long = PAGE_SIZE,
timestamp: Long,
onAllQueryFinished: () -> Unit,
onPageQueryFinished: (List<FaceVO>) -> Unit,
onFailure: (String) -> Unit = {}
) {
Timber.tag(TAG).d("getFaceIncrement timestamp=$timestamp")
launch {
val response = ApiClient.repositoryV2.getFaceIncrement(pageNum, pageSize, timestamp)
if (!parseResponse(response)) {
onFailure(response.msg ?: "获取增量人脸数据失败")
return@launch
}
withContext(Dispatchers.Default) {
val list = response.data ?: emptyList()
if (pageNum == 1L && list.isEmpty()) {
return@withContext
}
onPageQueryFinished(list)
// 取列表中最大时间戳(后端返回 String 类型,需转为 Long),而非依赖列表最后一条
lastFaceTimestamp = list.maxOf { it.faceUpdateTimestamp?.toLongOrNull() ?: 0L }
SpTool.lastFaceTimestamp = lastFaceTimestamp
FileUtil.saveLog("获取人脸[增量]数据结束,时间戳为:$lastFaceTimestamp")
onAllQueryFinished()
}
}
}
fun getFoodByNames(
names: String,
onSuccess: (list: List<NewFoodInfo>) -> Unit,
onFailure: (message: String) -> Unit
) {
launch {
try {
val response = ApiClient.repositoryV2.getFoodByNames(names.split(","))
if (parseResponse(response)) {
onSuccess(response.data ?: emptyList())
} else {
onFailure(response.msg ?: "查询菜品失败")
}
} catch (e: Exception) {
Timber.e(e, "查询菜品异常")
onFailure("网络异常: ${e.message}")
}
}
}
fun placeOrder(
orderRequest: PlaceOrderRequest,
onSuccess: (orderNo: String?) -> Unit,
onFailure: (message: String) -> Unit
) {
launchWithLoading {
try {
val response = ApiClient.repositoryV2.placeOrder(orderRequest)
if (parseResponse(response)) {
onSuccess(response.data)
} else {
onFailure(response.msg ?: "下单失败")
}
} catch (e: Exception) {
Timber.e(e, "开餐下单异常")
onFailure("网络异常: ${e.message}")
}
}
}
fun bindUserOrder(
userId: Long,
orderNo: String,
mode: Int? = null,
onSuccess: () -> Unit,
onFailure: (message: String) -> Unit
) {
launch {
try {
val response = ApiClient.repositoryV2.bindUserOrder(userId, orderNo, mode)
if (parseResponse(response)) {
onSuccess()
} else {
onFailure(response.msg ?: "绑定订单失败")
}
} catch (e: Exception) {
Timber.e(e, "绑定订单异常")
onFailure("网络异常: ${e.message}")
}
}
}
fun getCollectPage(
pageNum: Long = 1L,
pageSize: Long = PAGE_SIZE,
foodName: String? = null,
onSuccess: (list: List<CollectedFoodV2>) -> Unit,
onFailure: (message: String) -> Unit
) {
launch {
try {
val response = ApiClient.repositoryV2.getCollectPage(pageNum, pageSize, foodName)
if (parseResponse(response)) {
onSuccess(response.data ?: emptyList())
} else {
onFailure(response.msg ?: "获取采集列表失败")
}
} catch (e: Exception) {
Timber.e(e, "获取采集列表异常")
onFailure("网络异常: ${e.message}")
}
}
}
fun getCollectVectorPage(
pageNum: Long = 1L,
pageSize: Long = PAGE_SIZE,
onSuccess: (list: List<CollectedFoodV2>) -> Unit,
onFailure: (message: String) -> Unit
) {
launch {
try {
val response = ApiClient.repositoryV2.getCollectVectorPage(pageNum, pageSize)
if (parseResponse(response)) {
onSuccess(response.data ?: emptyList())
} else {
onFailure(response.msg ?: "获取采集列表失败")
}
} catch (e: Exception) {
Timber.e(e, "获取采集列表异常")
onFailure("网络异常: ${e.message}")
}
}
}
/** 上传采集图片(suspend,直接返回 picUrls 列表) */
suspend fun uploadCollect(
foodId: Long,
foodName: String,
version: String,
foodVector: String,
fileList: List<File>
): List<String>? {
Timber.tag(TAG).d("uploadCollect foodId=$foodId, foodName=$foodName")
val fileListNotNull = fileList.filter { it.exists() }
if (fileListNotNull.isEmpty()) {
return null
}
val response = ApiClient.repositoryV2.uploadCollect(
foodId, foodName, version, foodVector, fileListNotNull
)
if (!parseResponse(response)) {
return null
}
return response.data
}
/** 删除采集菜品 */
fun deleteCollect(
foodId: String?,
version: String?,
block: (Boolean) -> Unit
) {
Timber.tag(TAG).d("deleteCollect foodId=$foodId, version=$version")
val id = foodId?.toLongOrNull() ?: run {
block(false)
return
}
launchWithLoading {
try {
val response = ApiClient.repositoryV2.deleteCollect(id, version ?: "")
if (parseResponse(response)) {
block(true)
} else {
block(false)
}
} catch (e: Exception) {
Timber.e(e, "删除采集菜品异常")
block(false)
}
}
}
// ========== 以下方法暂用 V1 API 实现(待 V2 接口补充) ==========
/** 激活虹软人脸识别引擎 */
fun activeEngine() {
Timber.tag(TAG).d("activeEngine")
faceApi.activeEngine(
App.getContext(),
GlobalData.appId,
GlobalData.sdkKey,
GlobalData.activeKey,
object : FaceApi.ActiveCallback {
override fun onSuccess(activeCode: Int) {
Timber.tag(TAG).d("activeEngine activeCode = $activeCode")
viewModelScope.launch(Dispatchers.Main) {
when (activeCode) {
ErrorInfo.MOK -> {
ToastUtils.showToast("激活引擎成功")
}
ErrorInfo.MERR_ASF_ALREADY_ACTIVATED -> {
// 引擎已激活,无需再次激活
}
else -> {
ToastUtils.showToast("激活引擎失败($activeCode)")
}
}
}
}
override fun onFail(e: Exception?) {
viewModelScope.launch(Dispatchers.Main) {
ToastUtils.showToast("激活引擎异常,${e?.message}")
}
}
})
}
/** 获取支付二维码 */
fun getQrCodeImg(
orderId: String,
userId: String? = null,
totalFee: String? = null,
block: (String?) -> Unit
) {
Timber.tag(TAG).d("getQrCodeImg orderId = $orderId, userId = $userId")
launch {
val response = repository.getQrCodeImg(
orderNo = orderId, memberId = userId, totalFee = totalFee
)
if (parseResponse(response)) {
block(response.data)
} else {
block(null)
}
}
}
/** 扫码支付 */
fun qrCodePay(
authCode: String,
orderNo: String,
memberId: String?,
block: (Boolean, String?) -> Unit
) {
launch {
val response = repository.qrCodePay(authCode, orderNo, memberId)
if (parseResponse(response)) {
block(true, response.data)
} else {
block(false, null)
}
}
}
/** 现金支付 */
fun cashPay(param: HashMap<String, String>, block: (Boolean) -> Unit) {
Timber.tag(TAG).d("cashPay")
launchWithLoading {
val response = repository.cashPay(param)
if (parseResponse(response)) {
block(response.data ?: false)
} else {
block(false)
}
}
}
/** 会员支付 */
fun memberPay(param: HashMap<String, String?>, block: (Boolean) -> Unit) {
Timber.tag(TAG).d("memberPay")
launchWithLoading {
val response = repository.memberPay(param)
if (parseResponse(response)) {
block(true)
} else {
block(false)
}
}
}
/** 查询订单支付状态 */
suspend fun queryOrderState(orderId: String, block: (Boolean) -> Unit) {
Timber.tag(TAG).d("queryOrderState")
val response = repository.queryOrderState(orderNo = orderId)
if (parseResponse(response)) {
block(response.data == "1")
} else {
block(false)
}
}
/** 获取饭点类型 */
fun getDinnerType(
onSuccess: (DinnerType?) -> Unit = {},
onFailure: (message: String) -> Unit = {}
) {
_dinnerTypeInfo.value = null
launch {
try {
val response = repository.getDinnerType()
if (parseResponse(response)) {
_dinnerTypeInfo.value = response.data
onSuccess(response.data)
} else {
onFailure(response.msg ?: "获取饭点类型失败")
}
} catch (e: Exception) {
Timber.e(e, "获取饭点类型异常")
onFailure("网络异常: ${e.message}")
}
}
}
// ========== V1 兼容包装方法(暂用 V1 API,后续逐步迁移至 V2 ==========
private var lastFaceTimestamp = 0L
/** 搜索食物 */
fun searchByFoodName(foodName: String, action: (List<FoodInfo>) -> Unit = {}) {
Timber.tag(TAG).d("searchFood foodName = $foodName")
launchWithLoading {
try {
val response = ApiClient.repositoryV2.searchFood(name = foodName)
if (parseResponse(response)) {
val list = (response.data ?: emptyList()).map { it.toFoodInfo() }
action(list)
}
} catch (e: Exception) {
Timber.e(e, "搜索菜品异常")
}
}
}
/** 获取用户就餐营养数据 */
fun getUserNutritionData(
userId: String,
block: (UserNutrition?) -> Unit
) {
Timber.tag(TAG).d("getUserNutritionData userId = $userId")
val uid = userId.toLongOrNull() ?: run {
block(null)
return
}
launch {
try {
val response = ApiClient.repositoryV2.getUserCurrentFood(uid)
if (parseResponse(response)) {
block(response.data)
} else {
block(null)
}
} catch (e: Exception) {
Timber.e(e, "获取就餐营养数据异常")
block(null)
}
}
}
/** 获取订单列表 */
fun getFoodOrderList(userId: String, block: (FoodOrderModel?) -> Unit) {
Timber.tag(TAG).d("getFoodOrderList userId = $userId")
val uid = userId.toLongOrNull() ?: run {
block(null)
return
}
launchWithLoading {
try {
val response = ApiClient.repositoryV2.getSettlementOrders(uid)
if (parseResponse(response)) {
block(response.data?.toFoodOrderModel())
} else {
block(null)
}
} catch (e: Exception) {
Timber.e(e, "查询订单列表异常")
block(null)
}
}
}
/** 根据 ID 查询会员信息 */
fun getMemberInfoById(memberId: String, block: (MemberInfo?) -> Unit) {
Timber.tag(TAG).d("getMemberInfo memberId=$memberId")
val uid = memberId.toLongOrNull() ?: run {
block(null)
return
}
launchWithLoading {
try {
val response = ApiClient.repositoryV2.getMemberInfo(uid)
if (parseResponse(response)) {
block(response.data?.toMemberInfo())
} else {
block(null)
}
} catch (e: Exception) {
Timber.e(e, "查询会员信息异常")
block(null)
}
}
}
/** 根据手机号查询会员信息 */
fun getMemberInfoByPhone(phone: String, key: String, block: (MemberInfo?) -> Unit) {
Timber.tag(TAG).d("getMemberInfoByPhone phone=$phone")
launchWithLoading {
try {
val response = ApiClient.repositoryV2.getMemberInfoByPhone(phone, key)
if (parseResponse(response)) {
block(response.data?.toMemberInfo())
} else {
block(null)
}
} catch (e: Exception) {
Timber.e(e, "查询会员异常")
block(null)
}
}
}
/** 获取会员折扣 */
fun getMemberDiscount(userId: String, block: (Double?) -> Unit) {
Timber.tag(TAG).d("getMemberDiscount userId = $userId")
val uid = userId.toLongOrNull() ?: run {
block(null)
return
}
launchWithLoading {
try {
val response = ApiClient.repositoryV2.getMemberDiscount(uid)
if (parseResponse(response)) {
block(response.data?.toDoubleOrNull())
} else {
block(null)
}
} catch (e: Exception) {
Timber.e(e, "查询会员折扣异常")
block(null)
}
}
}
}