- 新增接口注释,完善全量及增量人脸数据获取说明 - FaceVO中faceUpdateTimestamp由Long改为String以防JS大数精度丢失 - 增加personType字段标识人员类型 - MainActivity保存人脸数据时映射完整字段,使用personType优先判断用户类型 - NetViewModelV2中获取人脸数据时改用完整构造函数创建FaceEntity - 累积更新lastFaceTimestamp为当前页最大时间戳,避免时间戳遗漏 - 优化时间戳更新逻辑,避免直接使用列表最后一条数据
576 lines
20 KiB
Kotlin
576 lines
20 KiB
Kotlin
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)
|
||
}
|
||
}
|
||
}
|
||
}
|