refactor(network): 重构网络请求配置和API接口实现

- 将BASE_URL改为使用GlobalData.PROD_BASE_URL常量
- 更新ApiClient中的URL匹配规则以支持新的营养模块接口
- 将ApiServiceV2中的接口路径从/neglect/booth改为/nutrition/neglect/booth
- 使用@Url注解动态传入完整URL地址
- 在CollectedFoodActivity中切换到NetViewModelV2和新API模型
- 更新采集功能的数据模型和接口调用方式
- 调整人脸数据获取逻辑以支持增量和全量同步
- 优化文件上传和删除操作的参数传递方式
This commit is contained in:
2026-05-28 15:14:44 +08:00
parent 6abd2f9b0b
commit bf6b2547f3
17 changed files with 563 additions and 250 deletions
@@ -22,7 +22,8 @@ object GlobalData {
/** /**
* 具体业务 BaseUrl * 具体业务 BaseUrl
*/ */
const val TEST_BASE_URL = "http://192.168.1.201:14801" // const val TEST_BASE_URL = "http://192.168.1.201:14801"
const val TEST_BASE_URL = "http://192.168.10.101:24801"
const val UAT_BASE_URL = "https://dev.yixiong-tech.com:8083" const val UAT_BASE_URL = "https://dev.yixiong-tech.com:8083"
const val PROD_BASE_URL = "https://api.dm.yixiong-tech.com:8443" const val PROD_BASE_URL = "https://api.dm.yixiong-tech.com:8443"
@@ -5,7 +5,6 @@ import androidx.activity.viewModels
import androidx.core.widget.addTextChangedListener import androidx.core.widget.addTextChangedListener
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.R import com.sw.dualscreen.R
import com.sw.dualscreen.adapter.CollectedFoodNewAdapter import com.sw.dualscreen.adapter.CollectedFoodNewAdapter
import com.sw.dualscreen.databinding.ActivityCollectedFoodBinding import com.sw.dualscreen.databinding.ActivityCollectedFoodBinding
@@ -14,10 +13,10 @@ import com.sw.dualscreen.ext.addOnActionSearchListener
import com.sw.dualscreen.ext.gone import com.sw.dualscreen.ext.gone
import com.sw.dualscreen.ext.hideKeyboard import com.sw.dualscreen.ext.hideKeyboard
import com.sw.dualscreen.ext.visible import com.sw.dualscreen.ext.visible
import com.sw.dualscreen.objbox.CollectedFoodInfo import com.sw.dualscreen.model.response.v2.CollectedFoodV2
import com.sw.dualscreen.objbox.ObjectBox import com.sw.dualscreen.objbox.ObjectBox
import com.sw.dualscreen.viewmodel.BaseViewModel import com.sw.dualscreen.viewmodel.BaseViewModel
import com.sw.dualscreen.viewmodel.UserViewModel import com.sw.dualscreen.viewmodel.NetViewModelV2
import com.sw.plate.utils.ToastUtils import com.sw.plate.utils.ToastUtils
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -27,7 +26,7 @@ class CollectedFoodActivity : BaseActivity<ActivityCollectedFoodBinding>() {
private const val PAGE_SIZE = 100 private const val PAGE_SIZE = 100
} }
private val viewModel by viewModels<UserViewModel>() private val viewModel by viewModels<NetViewModelV2>()
override fun getViewModel(): BaseViewModel { override fun getViewModel(): BaseViewModel {
return viewModel return viewModel
} }
@@ -36,7 +35,7 @@ class CollectedFoodActivity : BaseActivity<ActivityCollectedFoodBinding>() {
return ActivityCollectedFoodBinding.inflate(layoutInflater) return ActivityCollectedFoodBinding.inflate(layoutInflater)
} }
private val list: MutableList<CollectedFoodInfo> = mutableListOf() private val list: MutableList<CollectedFoodV2> = mutableListOf()
private val adapter by lazy { private val adapter by lazy {
CollectedFoodNewAdapter(list).apply { CollectedFoodNewAdapter(list).apply {
// isStateViewEnable = true // isStateViewEnable = true
@@ -127,19 +126,26 @@ class CollectedFoodActivity : BaseActivity<ActivityCollectedFoodBinding>() {
// loadEmptyView() // loadEmptyView()
// } // }
viewModel.getCollectedFoodList( viewModel.getCollectPage(
pageNo = pageNo, pageNum = pageNo.toLong(),
pageSize = PAGE_SIZE, pageSize = PAGE_SIZE.toLong(),
foodName = searchName ?: "" foodName = searchName,
) { items -> onSuccess = { items ->
runOnUiThread { runOnUiThread {
loadFoodList(items) loadFoodList(items.onEach { it.foodCount = 1 })
}
},
onFailure = {
runOnUiThread {
finishRefresh()
ToastUtils.showToast(it)
}
} }
} )
} }
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
private fun loadFoodList(items: List<CollectedFoodInfo>) { private fun loadFoodList(items: List<CollectedFoodV2>) {
finishRefresh() finishRefresh()
if (pageNo == 1 && items.isEmpty()) { if (pageNo == 1 && items.isEmpty()) {
list.clear() list.clear()
@@ -191,7 +197,7 @@ class CollectedFoodActivity : BaseActivity<ActivityCollectedFoodBinding>() {
private fun deleteGoods(position: Int) { private fun deleteGoods(position: Int) {
val food = list[position] val food = list[position]
showWaitingDialog("加载中……") showWaitingDialog("加载中……")
viewModel.deleteCollectFood(food.foodId, GlobalData.foodModelVersion) { deleteSuccess -> viewModel.deleteCollect(food.foodId, food.version) { deleteSuccess ->
if (deleteSuccess) { if (deleteSuccess) {
// Thread {}.start() // Thread {}.start()
lifecycleScope.launch { lifecycleScope.launch {
@@ -19,7 +19,7 @@ import com.sw.dualscreen.utils.L
import com.sw.dualscreen.utils.NetworkUtils import com.sw.dualscreen.utils.NetworkUtils
import com.sw.dualscreen.utils.SpTool import com.sw.dualscreen.utils.SpTool
import com.sw.dualscreen.viewmodel.BaseViewModel import com.sw.dualscreen.viewmodel.BaseViewModel
import com.sw.dualscreen.viewmodel.UserViewModel import com.sw.dualscreen.viewmodel.NetViewModelV2
import com.sw.plate.utils.AppUtil import com.sw.plate.utils.AppUtil
import com.sw.plate.utils.ToastUtils import com.sw.plate.utils.ToastUtils
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -29,7 +29,7 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
const val PAGE_SIZE = 100 const val PAGE_SIZE = 100
} }
private val userViewModel by viewModels<UserViewModel>() private val userViewModel by viewModels<NetViewModelV2>()
private var pageNum = 1 private var pageNum = 1
override fun getViewModel(): BaseViewModel { override fun getViewModel(): BaseViewModel {
@@ -107,6 +107,7 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
} }
} }
} }
checkCameraPermissionAndGo()
} }
private fun goMainActivity() { private fun goMainActivity() {
@@ -189,23 +190,23 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
} }
private fun getDeviceConfig() { private fun getDeviceConfig() {
userViewModel.getDeviceConfig { deviceConfig -> userViewModel.getDeviceConfig(
runOnUiThread { onSuccess = { deviceConfig ->
if (deviceConfig == null) { runOnUiThread {
ToastUtils.showToast("获取设备配置数据失败") if (deviceConfig == null) {
return@runOnUiThread ToastUtils.showToast("获取设备配置数据失败")
} return@runOnUiThread
//var settlementMode = 1 }
//结算模式:1-独立支付,2-联合结算 val settlementMode = if (deviceConfig.payType == 2) 0 else 1
val settlementMode = if (deviceConfig.payType == 2) 0 else 1 SpTool.settlementMode = settlementMode
// TODO: 2022/10/13 修改为独立支付
SpTool.settlementMode = settlementMode
GlobalData.appId = deviceConfig.arcsoftAppId ?: "" GlobalData.appId = deviceConfig.arcsoftAppId ?: ""
GlobalData.sdkKey = deviceConfig.arcsoftSdkKey ?: "" GlobalData.sdkKey = deviceConfig.arcsoftSdkKey ?: ""
GlobalData.activeKey = deviceConfig.arcsoftActiveKey ?: "" GlobalData.activeKey = deviceConfig.arcsoftActiveKey ?: ""
} }
} },
onFailure = { runOnUiThread { ToastUtils.showToast(it) } }
)
} }
private fun loadNetworkErrorDialog() { private fun loadNetworkErrorDialog() {
@@ -48,7 +48,7 @@ import com.sw.dualscreen.model.response.FoodOrder
import com.sw.dualscreen.model.response.PaySuccessEvent import com.sw.dualscreen.model.response.PaySuccessEvent
import com.sw.dualscreen.model.response.ResetRecognizeEvent import com.sw.dualscreen.model.response.ResetRecognizeEvent
import com.sw.dualscreen.model.response.UpdateRefreshEvent import com.sw.dualscreen.model.response.UpdateRefreshEvent
import com.sw.dualscreen.model.response.UserFaceModel import com.sw.dualscreen.model.response.v2.FaceVO
import com.sw.dualscreen.objbox.Food import com.sw.dualscreen.objbox.Food
import com.sw.dualscreen.objbox.FoodModule import com.sw.dualscreen.objbox.FoodModule
import com.sw.dualscreen.objbox.FoodModule.IdNameScore import com.sw.dualscreen.objbox.FoodModule.IdNameScore
@@ -67,7 +67,7 @@ import com.sw.dualscreen.utils.SPUtil
import com.sw.dualscreen.utils.SpTool import com.sw.dualscreen.utils.SpTool
import com.sw.dualscreen.view.CustomBottomSheetDialog import com.sw.dualscreen.view.CustomBottomSheetDialog
import com.sw.dualscreen.viewmodel.BaseViewModel import com.sw.dualscreen.viewmodel.BaseViewModel
import com.sw.dualscreen.viewmodel.UserViewModel import com.sw.dualscreen.viewmodel.NetViewModelV2
import com.sw.plate.utils.Base64 import com.sw.plate.utils.Base64
import com.sw.plate.utils.LightManager import com.sw.plate.utils.LightManager
import com.sw.plate.utils.ToastUtils import com.sw.plate.utils.ToastUtils
@@ -81,8 +81,6 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode import org.greenrobot.eventbus.ThreadMode
import org.json.JSONObject import org.json.JSONObject
@@ -109,7 +107,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
const val WEIGHT_RESET_RECOGNIZE = 10 const val WEIGHT_RESET_RECOGNIZE = 10
} }
private val viewModel by viewModels<UserViewModel>() private val viewModel by viewModels<NetViewModelV2>()
private var imageCapture: ImageCapture? = null private var imageCapture: ImageCapture? = null
var isAnalyzing = true // 控制是否进行图像分析 var isAnalyzing = true // 控制是否进行图像分析
@@ -208,7 +206,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
addBackEventListener() addBackEventListener()
initView() initView()
if (SpTool.firstGetFace) { if (SpTool.firstGetFace) {
viewModel.getUserFaceCache(pageNo = 1) viewModel.getFacePage(pageNum = 1)
} }
setupCamera() setupCamera()
initData() initData()
@@ -273,7 +271,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
userViewModel = viewModel, userViewModel = viewModel,
lifecycleScope = lifecycleScope, lifecycleScope = lifecycleScope,
successBlock = { successBlock = {
viewModel.getUserFaceCache(pageNo = 1) hideWaitingDialog()
}, },
failureBlock = { failureBlock = {
hideWaitingDialog() hideWaitingDialog()
@@ -281,7 +279,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
} }
) )
} }
viewModel.getUserFaceCache(pageNo = 1, onSuccess = { viewModel.getFacePage(pageNum = 1, onSuccess = {
recognizeViewModel.refreshFaceList() recognizeViewModel.refreshFaceList()
hideWaitingDialog() hideWaitingDialog()
ToastUtils.showToast("人脸数据已更新") ToastUtils.showToast("人脸数据已更新")
@@ -1111,14 +1109,12 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
log("是否null判断,imageFile == null:${imageFile == null},imageVector == null:${imageVector == null}") log("是否null判断,imageFile == null:${imageFile == null},imageVector == null:${imageVector == null}")
return return
} }
val foodId = foodInfo.foodId val foodId = foodInfo.foodId.toLongOrNull() ?: run {
loadRemindDialog("foodId 格式错误: ${foodInfo.foodId}")
return
}
val foodName = foodInfo.foodName ?: "" val foodName = foodInfo.foodName ?: ""
val foodModelVersion = GlobalData.foodModelVersion val foodModelVersion = GlobalData.foodModelVersion
val params = HashMap<String, RequestBody>()
params["foodId"] = foodId.toRequestBody()
params["foodName"] = foodName.toRequestBody()
params["version"] = foodModelVersion.toRequestBody()
val files = listOf(imageFile)
val vectors = listOf(imageVector) val vectors = listOf(imageVector)
val foodVectorList = vectors.map { val foodVectorList = vectors.map {
it.joinToString( it.joinToString(
@@ -1128,8 +1124,13 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
val foodVectorJson = val foodVectorJson =
foodVectorList.joinToString(separator = ",", prefix = "[", postfix = "]") foodVectorList.joinToString(separator = ",", prefix = "[", postfix = "]")
log("json=$foodVectorJson") log("json=$foodVectorJson")
params["foodVector"] = foodVectorJson.toRequestBody() val idList = viewModel.uploadCollect(
val idList = viewModel.uploadCollectFoodPics(files, params) foodId = foodId,
foodName = foodName,
version = foodModelVersion,
foodVector = foodVectorJson,
fileList = listOf(imageFile)
)
if (idList.isNullOrEmpty()) { if (idList.isNullOrEmpty()) {
loadRemindDialog("上传失败,未返回向量数据id") loadRemindDialog("上传失败,未返回向量数据id")
log("idList为空") log("idList为空")
@@ -1146,7 +1147,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
ObjectBox.put( ObjectBox.put(
Food( Food(
collectId = idList[0], collectId = idList[0],
foodId = foodId, foodId = "$foodId",
foodName = foodName, foodName = foodName,
foodVector = imageVector, foodVector = imageVector,
version = foodModelVersion version = foodModelVersion
@@ -1204,7 +1205,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
//if (timestamp == 0L) { //if (timestamp == 0L) {
// return@startIntervalTaskWithInitialDelay // return@startIntervalTaskWithInitialDelay
//} //}
viewModel.getFaceIncrementList( viewModel.getFaceIncrement(
// pageNo = taskPageNo, // pageNo = taskPageNo,
timestamp = timestamp, timestamp = timestamp,
onAllQueryFinished = { onAllQueryFinished = {
@@ -1223,21 +1224,21 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
} }
} }
private fun updateFaceData(list: List<UserFaceModel>) { private fun updateFaceData(list: List<FaceVO>) {
val faceList = mutableListOf<FaceEntity>() val faceList = mutableListOf<FaceEntity>()
try { try {
list.forEach { model -> list.forEach { vo ->
if (model.faceDeleted == true) { if (vo.faceDeleted == true) {
//删除数据 //删除数据
FaceDatabase.getInstance(this).faceDao().deleteFaceById(model.userId) FaceDatabase.getInstance(this).faceDao().deleteFaceById(vo.userId)
} else { } else {
//保存数据 //保存数据
val faceEntity = FaceEntity( val faceEntity = FaceEntity(
model.userId, null, Base64.decode(model.faceFeatureStr) vo.userId, null, Base64.decode(vo.faceFeature)
).also { ).also {
//1-会员、2-临时用户 //1-会员、2-临时用户
it.userType = if (model.member) "1" else "2" it.userType = if (vo.member == true) "1" else "2"
it.cardNo = model.cardNo it.cardNo = vo.cardNo
} }
faceList.add(faceEntity) faceList.add(faceEntity)
} }
@@ -20,7 +20,7 @@ import com.sw.dualscreen.utils.Debouncer
import com.sw.dualscreen.utils.SPUtil import com.sw.dualscreen.utils.SPUtil
import com.sw.dualscreen.utils.SoundPoolUtil import com.sw.dualscreen.utils.SoundPoolUtil
import com.sw.dualscreen.viewmodel.BaseViewModel import com.sw.dualscreen.viewmodel.BaseViewModel
import com.sw.dualscreen.viewmodel.UserViewModel import com.sw.dualscreen.viewmodel.NetViewModelV2
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import timber.log.Timber import timber.log.Timber
@@ -43,7 +43,7 @@ class PayActivity : BaseActivity<ActivityPayBinding>() {
const val PAY_SUCCESS = "pay_success" const val PAY_SUCCESS = "pay_success"
} }
val userViewModel by viewModels<UserViewModel>() val userViewModel by viewModels<NetViewModelV2>()
val recognizeViewModel by viewModels<RecognizeViewModel>() val recognizeViewModel by viewModels<RecognizeViewModel>()
override fun getViewModel(): BaseViewModel { override fun getViewModel(): BaseViewModel {
@@ -11,12 +11,12 @@ import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.model.response.ClickBackEvent import com.sw.dualscreen.model.response.ClickBackEvent
import com.sw.dualscreen.model.response.UpdateRefreshEvent import com.sw.dualscreen.model.response.UpdateRefreshEvent
import com.sw.dualscreen.viewmodel.BaseViewModel import com.sw.dualscreen.viewmodel.BaseViewModel
import com.sw.dualscreen.viewmodel.UserViewModel import com.sw.dualscreen.viewmodel.NetViewModelV2
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
class SettingActivity : BaseActivity<ActivitySettingBinding>() { class SettingActivity : BaseActivity<ActivitySettingBinding>() {
val viewModel by viewModels<UserViewModel>() val viewModel by viewModels<NetViewModelV2>()
override fun getViewModel(): BaseViewModel { override fun getViewModel(): BaseViewModel {
return viewModel return viewModel
} }
@@ -36,8 +36,6 @@ import com.sw.dualscreen.utils.ImageUtil
import com.sw.plate.utils.ToastUtils import com.sw.plate.utils.ToastUtils
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import timber.log.Timber import timber.log.Timber
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
@@ -263,14 +261,15 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
lifecycleScope.launch { lifecycleScope.launch {
val totalFileCount = foodCollectionList.count { it.imageFile != null } val totalFileCount = foodCollectionList.count { it.imageFile != null }
settingActivity?.showWaitingDialog2("图片上传中0/$totalFileCount") settingActivity?.showWaitingDialog2("图片上传中0/$totalFileCount")
val params = HashMap<String, RequestBody>() val foodId = checkedItem!!.foodId.toLongOrNull() ?: run {
//params["placeId"] = restId.toRequestBody() ToastUtils.showToast("foodId 格式错误")
params["foodId"] = checkedItem!!.foodId.toRequestBody() return@launch
params["foodName"] = checkedItem!!.foodName!!.toRequestBody() }
params["version"] = GlobalData.foodModelVersion.toRequestBody() val foodName = checkedItem!!.foodName ?: ""
val version = GlobalData.foodModelVersion
ImageUploader(totalList = foodCollectionList, uploadImage = { batch -> ImageUploader(totalList = foodCollectionList, uploadImage = { batch ->
val files = batch.map { it.imageFile } val files = batch.mapNotNull { it.imageFile }
val foodVectorList = batch.filter { it.imageVector != null }.map { val foodVectorList = batch.filter { it.imageVector != null }.map {
it.imageVector!!.joinToString( it.imageVector!!.joinToString(
separator = ",", prefix = "[", postfix = "]" separator = ",", prefix = "[", postfix = "]"
@@ -279,8 +278,13 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
val foodVectorJson = val foodVectorJson =
foodVectorList.joinToString(separator = ",", prefix = "[", postfix = "]") foodVectorList.joinToString(separator = ",", prefix = "[", postfix = "]")
Timber.tag(TAG).d("json=$foodVectorJson") Timber.tag(TAG).d("json=$foodVectorJson")
params["foodVector"] = foodVectorJson.toRequestBody() settingActivity?.viewModel?.uploadCollect(
settingActivity?.viewModel?.uploadCollectFoodPics(files, params) foodId = foodId,
foodName = foodName,
version = version,
foodVector = foodVectorJson,
fileList = files
)
}, onProgress = { count, batch, idList -> }, onProgress = { count, batch, idList ->
//batch.forEach { //batch.forEach {
// it.uploadSuccess = true // it.uploadSuccess = true
@@ -8,10 +8,10 @@ import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.sw.dualscreen.databinding.ListItemCollectedFoodBinding import com.sw.dualscreen.databinding.ListItemCollectedFoodBinding
import com.sw.dualscreen.ext.gone import com.sw.dualscreen.ext.gone
import com.sw.dualscreen.ext.visible import com.sw.dualscreen.ext.visible
import com.sw.dualscreen.objbox.CollectedFoodInfo import com.sw.dualscreen.model.response.v2.CollectedFoodV2
class CollectedFoodNewAdapter(var list: MutableList<CollectedFoodInfo>) : class CollectedFoodNewAdapter(var list: MutableList<CollectedFoodV2>) :
BaseQuickAdapter<CollectedFoodInfo, CollectedFoodNewAdapter.VH>(list) { BaseQuickAdapter<CollectedFoodV2, CollectedFoodNewAdapter.VH>(list) {
inner class VH(var binding: ListItemCollectedFoodBinding) : QuickViewHolder(binding.root) inner class VH(var binding: ListItemCollectedFoodBinding) : QuickViewHolder(binding.root)
@@ -21,7 +21,7 @@ class CollectedFoodNewAdapter(var list: MutableList<CollectedFoodInfo>) :
return VH(binding) return VH(binding)
} }
override fun onBindViewHolder(holder: VH, position: Int, item: CollectedFoodInfo?) { override fun onBindViewHolder(holder: VH, position: Int, item: CollectedFoodV2?) {
holder.binding.tvFoodName.text = item?.foodName holder.binding.tvFoodName.text = item?.foodName
holder.binding.tvCollectedNum.text = "已采集${item?.foodCount}" holder.binding.tvCollectedNum.text = "已采集${item?.foodCount}"
holder.binding.divider.run { holder.binding.divider.run {
@@ -13,5 +13,7 @@ data class CollectedFoodV2(
val foodName: String?, val foodName: String?,
val version: String?, val version: String?,
val foodVector: String?, val foodVector: String?,
val picUrls: List<String>? val picUrls: List<String>?,
/** 已采集数量(客户端手动赋值,非接口字段,用于兼容旧版 UI) */
var foodCount: Int = 0
) : Parcelable ) : Parcelable
@@ -1,6 +1,7 @@
package com.sw.dualscreen.network package com.sw.dualscreen.network
import com.sw.dualscreen.BuildConfig import com.sw.dualscreen.BuildConfig
import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.MyApp import com.sw.dualscreen.MyApp
import com.sw.dualscreen.network.api.ApiService import com.sw.dualscreen.network.api.ApiService
import com.sw.dualscreen.network.api.ApiServiceV2 import com.sw.dualscreen.network.api.ApiServiceV2
@@ -17,7 +18,8 @@ import timber.log.Timber
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
object ApiClient { object ApiClient {
private const val BASE_URL = "http://device.shuziweidao.com:8889/" // private const val BASE_URL = "http://device.shuziweidao.com:8889/"
private const val BASE_URL = GlobalData.PROD_BASE_URL
private const val TIME_OUT = 30L private const val TIME_OUT = 30L
@@ -25,12 +27,15 @@ object ApiClient {
.connectTimeout(TIME_OUT, TimeUnit.SECONDS) .connectTimeout(TIME_OUT, TimeUnit.SECONDS)
.readTimeout(TIME_OUT, TimeUnit.SECONDS) .readTimeout(TIME_OUT, TimeUnit.SECONDS)
.writeTimeout(TIME_OUT, TimeUnit.SECONDS) .writeTimeout(TIME_OUT, TimeUnit.SECONDS)
.addNetworkInterceptor(Interceptor {chain -> .addNetworkInterceptor(Interceptor { chain ->
val request = chain.request() val request = chain.request()
val url = request.url.toString() val url = request.url.toString()
val tag = if (url.contains("terminal/neglect/common/app/faceFeature/increment/list")) "faceIncrement" val tag = if (
url.contains("terminal/neglect/common/app/faceFeature/increment/list") ||
url.contains("nutrition/neglect/booth/face/increment")
) "faceIncrement"
else if (url.contains("terminal/neglect/pay/app/turnOrderInfo")) "turnOrderInfo" else if (url.contains("terminal/neglect/pay/app/turnOrderInfo")) "turnOrderInfo"
else if (url.contains("neglect/booth/")) "ApiV2" else if(url.contains("nutrition/neglect")) "V2"
else "ApiClient" else "ApiClient"
val loggingInterceptor = HttpLoggingInterceptor(logger = { val loggingInterceptor = HttpLoggingInterceptor(logger = {
Timber.tag(tag).d("okhttp logger ==>${it}") Timber.tag(tag).d("okhttp logger ==>${it}")
@@ -20,81 +20,97 @@ import retrofit2.http.Multipart
import retrofit2.http.POST import retrofit2.http.POST
import retrofit2.http.Part import retrofit2.http.Part
import retrofit2.http.PartMap import retrofit2.http.PartMap
import retrofit2.http.Url
/** /**
* 新系统 API 接口服务 * 新系统 API 接口服务
* 基于 /neglect/booth 模块前缀 * 基于 /nutrition/neglect/booth 模块前缀
* 文档版本:2026-05-27 * 文档版本:2026-05-27
*/ */
interface ApiServiceV2 { interface ApiServiceV2 {
@GET("/neglect/booth/device/config") @GET
suspend fun getDeviceConfig(): ApiResponse<DeviceConfig?> suspend fun getDeviceConfig(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/booth/device/config"
): ApiResponse<DeviceConfig?>
@POST("/neglect/booth/face/page") @POST
suspend fun getFacePage( suspend fun getFacePage(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/booth/face/page",
@Body request: Map<String, Long> @Body request: Map<String, Long>
): ApiResponse<List<FaceVO>?> ): ApiResponse<List<FaceVO>?>
@POST("/neglect/booth/face/increment") @POST
suspend fun getFaceIncrement( suspend fun getFaceIncrement(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/booth/face/increment",
@Body request: Map<String, Long> @Body request: Map<String, Long>
): ApiResponse<List<FaceVO>?> ): ApiResponse<List<FaceVO>?>
@POST("/neglect/booth/food/by-names") @POST
suspend fun getFoodByNames( suspend fun getFoodByNames(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/booth/food/by-names",
@Body request: FoodSearchReq @Body request: FoodSearchReq
): ApiResponse<List<NewFoodInfo>?> ): ApiResponse<List<NewFoodInfo>?>
@POST("/neglect/booth/user/current-food") @POST
suspend fun getUserCurrentFood( suspend fun getUserCurrentFood(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/booth/user/current-food",
@Body request: Map<String, Long> @Body request: Map<String, Long>
): ApiResponse<UserNutrition?> ): ApiResponse<UserNutrition?>
@POST("/neglect/booth/order/place") @POST
suspend fun placeOrder( suspend fun placeOrder(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/booth/order/place",
@Body request: PlaceOrderRequest @Body request: PlaceOrderRequest
): ApiResponse<String?> ): ApiResponse<String?>
@POST("/neglect/booth/order/settlement") @POST
suspend fun getSettlementOrders( suspend fun getSettlementOrders(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/booth/order/settlement",
@Body request: Map<String, Long> @Body request: Map<String, Long>
): ApiResponse<SettlementOrder?> ): ApiResponse<SettlementOrder?>
@POST("/neglect/booth/member/info") @POST
suspend fun getMemberInfo( suspend fun getMemberInfo(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/booth/member/info",
@Body request: Map<String, Long> @Body request: Map<String, Long>
): ApiResponse<NewMemberInfo?> ): ApiResponse<NewMemberInfo?>
@POST("/neglect/booth/member/info-by-phone") @POST
suspend fun getMemberInfoByPhone( suspend fun getMemberInfoByPhone(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/booth/member/info-by-phone",
@Body request: Map<String, String> @Body request: Map<String, String>
): ApiResponse<NewMemberInfo?> ): ApiResponse<NewMemberInfo?>
@POST("/neglect/booth/member/discount") @POST
suspend fun getMemberDiscount( suspend fun getMemberDiscount(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/booth/member/discount",
@Body request: Map<String, Long> @Body request: Map<String, Long>
): ApiResponse<String?> ): ApiResponse<String?>
@POST("/neglect/booth/order/bind-user") @POST
suspend fun bindUserOrder( suspend fun bindUserOrder(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/booth/order/bind-user",
@Body request: BindUserOrderRequest @Body request: BindUserOrderRequest
): ApiResponse<Any?> ): ApiResponse<Any?>
@POST("/neglect/booth/collect/page") @POST
suspend fun getCollectPage( suspend fun getCollectPage(
@Body request: Map<String, Any> @Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/booth/collect/page",
@Body request: Map<String, @JvmSuppressWildcards Any>
): ApiResponse<List<CollectedFoodV2>?> ): ApiResponse<List<CollectedFoodV2>?>
@Multipart @Multipart
@POST("/neglect/booth/collect/upload") @POST
suspend fun uploadCollect( suspend fun uploadCollect(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/booth/collect/upload",
@PartMap params: HashMap<String, RequestBody>, @PartMap params: HashMap<String, RequestBody>,
@Part foodPics: List<MultipartBody.Part> @Part foodPics: List<MultipartBody.Part>
): ApiResponse<List<String>?> ): ApiResponse<List<String>?>
@POST("/neglect/booth/collect/delete") @POST
suspend fun deleteCollect( suspend fun deleteCollect(
@Body request: Map<String, Any> @Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/booth/collect/delete",
@Body request: Map<String, @JvmSuppressWildcards Any>
): ApiResponse<Any?> ): ApiResponse<Any?>
} }
@@ -45,7 +45,7 @@ import com.sw.dualscreen.model.response.UserNutrition
import com.sw.dualscreen.utils.Debouncer import com.sw.dualscreen.utils.Debouncer
import com.sw.dualscreen.utils.SPUtil import com.sw.dualscreen.utils.SPUtil
import com.sw.dualscreen.utils.countDownByFlow import com.sw.dualscreen.utils.countDownByFlow
import com.sw.dualscreen.viewmodel.UserViewModel import com.sw.dualscreen.viewmodel.NetViewModelV2
import com.sw.plate.utils.LightManager import com.sw.plate.utils.LightManager
import com.sw.plate.utils.ToastUtils import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.ConfigUtil import com.sw.plate.utils.arcface.ConfigUtil
@@ -73,7 +73,7 @@ import kotlin.math.roundToInt
class MainScreenPresentation( class MainScreenPresentation(
val activity: MainActivity, val activity: MainActivity,
display: Display, display: Display,
val userViewModel: UserViewModel, val userViewModel: NetViewModelV2,
val recognizeViewModel: RecognizeViewModel, val recognizeViewModel: RecognizeViewModel,
private val onDismissListener: () -> Unit = {} private val onDismissListener: () -> Unit = {}
) : Presentation(activity, display), ViewTreeObserver.OnGlobalLayoutListener { ) : Presentation(activity, display), ViewTreeObserver.OnGlobalLayoutListener {
@@ -34,7 +34,7 @@ import com.sw.dualscreen.ext.visible
import com.sw.dualscreen.model.response.MemberInfo import com.sw.dualscreen.model.response.MemberInfo
import com.sw.dualscreen.model.response.TextBean import com.sw.dualscreen.model.response.TextBean
import com.sw.dualscreen.utils.SpannedUtils import com.sw.dualscreen.utils.SpannedUtils
import com.sw.dualscreen.viewmodel.UserViewModel import com.sw.dualscreen.viewmodel.NetViewModelV2
import com.sw.plate.utils.ToastUtils import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.ConfigUtil import com.sw.plate.utils.arcface.ConfigUtil
import com.sw.plate.utils.arcface.ErrorCodeUtil import com.sw.plate.utils.arcface.ErrorCodeUtil
@@ -56,7 +56,7 @@ import java.util.concurrent.atomic.AtomicBoolean
class PayPresentation( class PayPresentation(
val activity: PayActivity, val activity: PayActivity,
display: Display, display: Display,
val userViewModel: UserViewModel, val userViewModel: NetViewModelV2,
val recognizeViewModel: RecognizeViewModel, val recognizeViewModel: RecognizeViewModel,
private val onDismissListener: () -> Unit = {} private val onDismissListener: () -> Unit = {}
) : Presentation(activity, display), ViewTreeObserver.OnGlobalLayoutListener { ) : Presentation(activity, display), ViewTreeObserver.OnGlobalLayoutListener {
@@ -17,6 +17,7 @@ import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody import okhttp3.MultipartBody
import okhttp3.RequestBody import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File import java.io.File
class RemoteRepositoryV2 constructor( class RemoteRepositoryV2 constructor(
@@ -169,22 +170,10 @@ class RemoteRepositoryV2 constructor(
fileList: List<File> fileList: List<File>
): ApiResponse<List<String>?> { ): ApiResponse<List<String>?> {
val params = hashMapOf<String, RequestBody>() val params = hashMapOf<String, RequestBody>()
params["foodId"] = RequestBody.create( params["foodId"] = foodId.toString().toRequestBody("text/plain".toMediaTypeOrNull())
"text/plain".toMediaTypeOrNull(), params["foodName"] = foodName.toRequestBody("text/plain".toMediaTypeOrNull())
foodId.toString() params["version"] = version.toRequestBody("text/plain".toMediaTypeOrNull())
) params["foodVector"] = foodVector.toRequestBody("text/plain".toMediaTypeOrNull())
params["foodName"] = RequestBody.create(
"text/plain".toMediaTypeOrNull(),
foodName
)
params["version"] = RequestBody.create(
"text/plain".toMediaTypeOrNull(),
version
)
params["foodVector"] = RequestBody.create(
"text/plain".toMediaTypeOrNull(),
foodVector
)
val fileParts = mutableListOf<MultipartBody.Part>() val fileParts = mutableListOf<MultipartBody.Part>()
fileList.forEach { file -> fileList.forEach { file ->
@@ -1,11 +1,10 @@
package com.sw.dualscreen.utils package com.sw.dualscreen.utils
import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.LifecycleCoroutineScope
import com.sw.dualscreen.GlobalData import com.sw.dualscreen.model.response.v2.CollectedFoodV2
import com.sw.dualscreen.model.response.FoodVector
import com.sw.dualscreen.objbox.Food import com.sw.dualscreen.objbox.Food
import com.sw.dualscreen.objbox.ObjectBox import com.sw.dualscreen.objbox.ObjectBox
import com.sw.dualscreen.viewmodel.UserViewModel import com.sw.dualscreen.viewmodel.NetViewModelV2
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
object FoodVectorTool { object FoodVectorTool {
@@ -15,19 +14,13 @@ object FoodVectorTool {
/** /**
* 统一入口:分页拉取所有食物向量数据并保存到本地 ObjectBox * 统一入口:分页拉取所有食物向量数据并保存到本地 ObjectBox
*
* @param userViewModel 请求 ViewModel
* @param lifecycleScope 协程作用域(用于保存操作)
* @param successBlock 全部数据拉取并保存成功后回调
* @param failureBlock 任意一次请求失败时回调,参数为错误信息
*/ */
fun loadAndSaveFoodVector( fun loadAndSaveFoodVector(
userViewModel: UserViewModel, userViewModel: NetViewModelV2,
lifecycleScope: LifecycleCoroutineScope, lifecycleScope: LifecycleCoroutineScope,
successBlock: () -> Unit, successBlock: () -> Unit,
failureBlock: (String) -> Unit failureBlock: (String) -> Unit
) { ) {
// 每次调用从第 1 页开始,避免单例状态污染
fetchPage( fetchPage(
pageNum = 1, pageNum = 1,
userViewModel = userViewModel, userViewModel = userViewModel,
@@ -37,68 +30,47 @@ object FoodVectorTool {
) )
} }
/**
* 递归分页请求(内部方法,外部不感知分页细节)
*
* @param pageNum 当前页码
* @param userViewModel 请求 ViewModel
* @param lifecycleScope 协程作用域
* @param successBlock 成功回调
* @param failureBlock 失败回调
*/
private fun fetchPage( private fun fetchPage(
pageNum: Int, pageNum: Int,
userViewModel: UserViewModel, userViewModel: NetViewModelV2,
lifecycleScope: LifecycleCoroutineScope, lifecycleScope: LifecycleCoroutineScope,
successBlock: () -> Unit, successBlock: () -> Unit,
failureBlock: (String) -> Unit failureBlock: (String) -> Unit
) { ) {
userViewModel.getCollectedFoodVector( userViewModel.getCollectPage(
param = mutableMapOf( pageNum = pageNum.toLong(),
"pageNum" to "$pageNum", pageSize = PAGE_SIZE.toLong(),
"pageSize" to "$PAGE_SIZE", onSuccess = { items ->
"version" to GlobalData.foodModelVersion if (pageNum == 1 && items.isEmpty()) {
) successBlock()
) { items -> return@getCollectPage
if (items == null) { }
failureBlock("未查询到向量数据") if (items.isNotEmpty()) {
return@getCollectedFoodVector saveFoodVector(lifecycleScope, items)
} }
// 第一页为空,视为成功(数据库无数据) if (items.size >= PAGE_SIZE) {
if (pageNum == 1 && items.isEmpty()) { fetchPage(pageNum + 1, userViewModel, lifecycleScope, successBlock, failureBlock)
successBlock() } else {
return@getCollectedFoodVector successBlock()
} }
// 有数据则保存 },
if (items.isNotEmpty()) { onFailure = { failureBlock(it) }
saveFoodVector(lifecycleScope, items) )
}
// 数据量达到一页上限,继续拉取下一页
if (items.size >= PAGE_SIZE) {
fetchPage(pageNum + 1, userViewModel, lifecycleScope, successBlock, failureBlock)
} else {
successBlock()
}
}
} }
/** /**
* 将向量数据列表保存到 ObjectBox 本地数据库 * 将 CollectedFoodV2 列表保存到 ObjectBox 本地数据库
*
* @param lifecycleScope 协程作用域
* @param list 待保存的食物向量数据列表
*/ */
private fun saveFoodVector(lifecycleScope: LifecycleCoroutineScope, list: List<FoodVector>) { private fun saveFoodVector(lifecycleScope: LifecycleCoroutineScope, list: List<CollectedFoodV2>) {
lifecycleScope.launch { lifecycleScope.launch {
val vectorList = list.map { vt -> val vectorList = list.map { item ->
// 解析向量字符串 "[0.1,0.2,...]" 为 FloatArray val foodVector = item.foodVector?.removeSurrounding("[", "]")
val foodVector = vt.foodVector?.removeSurrounding("[", "]")
?.split(",")?.map { it.toFloatOrNull() ?: 0.0f }?.toFloatArray() ?.split(",")?.map { it.toFloatOrNull() ?: 0.0f }?.toFloatArray()
Food( Food(
collectId = vt.id, collectId = item.foodId,
foodId = vt.foodId, foodId = item.foodId,
foodName = vt.foodName, foodName = item.foodName,
version = vt.version, version = item.version,
foodVector = foodVector foodVector = foodVector
) )
} }
@@ -15,7 +15,7 @@ import com.sw.dualscreen.databinding.BottomSheetDialogBinding
import com.sw.dualscreen.model.response.FoodInfo import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.utils.Debouncer import com.sw.dualscreen.utils.Debouncer
import com.sw.dualscreen.utils.KeyboardUtils import com.sw.dualscreen.utils.KeyboardUtils
import com.sw.dualscreen.viewmodel.UserViewModel import com.sw.dualscreen.viewmodel.NetViewModelV2
import timber.log.Timber import timber.log.Timber
import androidx.core.graphics.drawable.toDrawable import androidx.core.graphics.drawable.toDrawable
@@ -24,7 +24,7 @@ import androidx.core.graphics.drawable.toDrawable
*/ */
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
class CustomBottomSheetDialog( class CustomBottomSheetDialog(
val viewModel: UserViewModel, val viewModel: NetViewModelV2,
val itemClickCallback: (FoodInfo) -> Unit val itemClickCallback: (FoodInfo) -> Unit
) : BottomSheetDialogFragment() { ) : BottomSheetDialogFragment() {
private lateinit var binding: BottomSheetDialogBinding private lateinit var binding: BottomSheetDialogBinding
@@ -87,7 +87,7 @@ class CustomBottomSheetDialog(
companion object { companion object {
fun newInstance( fun newInstance(
viewModel: UserViewModel, viewModel: NetViewModelV2,
itemClickCallback: (FoodInfo) -> Unit itemClickCallback: (FoodInfo) -> Unit
): CustomBottomSheetDialog { ): CustomBottomSheetDialog {
return CustomBottomSheetDialog(viewModel, itemClickCallback) return CustomBottomSheetDialog(viewModel, itemClickCallback)
@@ -1,9 +1,18 @@
package com.sw.dualscreen.viewmodel package com.sw.dualscreen.viewmodel
import androidx.lifecycle.viewModelScope 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.BindUserOrderRequest
import com.sw.dualscreen.model.request.v2.PlaceOrderRequest 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.FoodOrder
import com.sw.dualscreen.model.response.FoodOrderModel
import com.sw.dualscreen.model.response.FoodSearchReq 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.UserNutrition
import com.sw.dualscreen.model.response.v2.CollectedFoodV2 import com.sw.dualscreen.model.response.v2.CollectedFoodV2
import com.sw.dualscreen.model.response.v2.FaceVO import com.sw.dualscreen.model.response.v2.FaceVO
@@ -11,10 +20,20 @@ import com.sw.dualscreen.model.response.v2.NewFoodInfo
import com.sw.dualscreen.model.response.v2.NewMemberInfo import com.sw.dualscreen.model.response.v2.NewMemberInfo
import com.sw.dualscreen.model.response.v2.SettlementOrder import com.sw.dualscreen.model.response.v2.SettlementOrder
import com.sw.dualscreen.network.ApiClient 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.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
import java.io.File
class NetViewModelV2 : BaseViewModel() { class NetViewModelV2 : BaseViewModel() {
@@ -23,6 +42,16 @@ class NetViewModelV2 : BaseViewModel() {
const val PAGE_SIZE = 100L 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( fun getDeviceConfig(
onSuccess: (config: com.sw.dualscreen.model.response.DeviceConfig?) -> Unit, onSuccess: (config: com.sw.dualscreen.model.response.DeviceConfig?) -> Unit,
onFailure: (message: String) -> Unit onFailure: (message: String) -> Unit
@@ -42,49 +71,82 @@ class NetViewModelV2 : BaseViewModel() {
} }
} }
/** 获取人脸全量数据(递归分页 + FaceApi 集成) */
fun getFacePage( fun getFacePage(
pageNum: Long = 1L, pageNum: Long = 1L,
pageSize: Long = PAGE_SIZE, pageSize: Long = PAGE_SIZE,
onSuccess: (list: List<FaceVO>, hasNext: Boolean) -> Unit, onSuccess: () -> Unit = {},
onFailure: (message: String) -> Unit onFailure: (String) -> Unit = {}
) { ) {
var currentPageNum = pageNum
Timber.tag(TAG).d("getFacePage index = $currentPageNum")
launch { launch {
try { _loadFaceResult.value = false
val response = ApiClient.repositoryV2.getFacePage(pageNum, pageSize) SpTool.lastFaceTimestamp = 0
if (parseResponse(response)) { FileUtil.saveLog("获取人脸全量数据开始,重置时间戳为0")
val list = response.data ?: emptyList() val response = ApiClient.repositoryV2.getFacePage(currentPageNum, pageSize)
val hasNext = list.size >= pageSize if (!parseResponse(response)) {
onSuccess(list, hasNext) onFailure("获取人脸数据失败,${response.msg}(${response.code})")
} else { return@launch
onFailure(response.msg ?: "获取人脸数据失败") }
withContext(Dispatchers.Default) {
val list = response.data ?: emptyList()
if (pageNum == 1L && list.isEmpty()) {
onFailure("查询人脸数据为空")
return@withContext
} }
} catch (e: Exception) { val faceEntity = list.map { vo ->
Timber.e(e, "获取全量人脸数据异常") FaceEntity(vo.userId, null, Base64.decode(vo.faceFeature)).also {
onFailure("网络异常: ${e.message}") it.userType = if (vo.member == true) "1" else "2"
it.cardNo = vo.cardNo
}
}
faceApi.updateFaceData(currentPageNum.toInt(), faceEntity)
if (list.size >= pageSize) {
lastFaceTimestamp = list.last().faceUpdateTimestamp ?: 0L
currentPageNum++
getFacePage(currentPageNum, pageSize, onSuccess, onFailure)
return@withContext
}
if (list.isNotEmpty()) {
lastFaceTimestamp = list.last().faceUpdateTimestamp ?: 0L
}
SpTool.lastFaceTimestamp = lastFaceTimestamp
FileUtil.saveLog("获取人脸[全量]数据结束,时间戳为:$lastFaceTimestamp")
_loadFaceResult.value = true
onSuccess()
} }
} }
} }
/** 获取人脸增量数据(FaceApi 集成) */
fun getFaceIncrement( fun getFaceIncrement(
pageNum: Long = 1L, pageNum: Long = 1L,
pageSize: Long = PAGE_SIZE, pageSize: Long = PAGE_SIZE,
timestamp: Long, timestamp: Long,
onSuccess: (list: List<FaceVO>, hasNext: Boolean) -> Unit, onAllQueryFinished: () -> Unit,
onFailure: (message: String) -> Unit onPageQueryFinished: (List<FaceVO>) -> Unit,
onFailure: (String) -> Unit = {}
) { ) {
Timber.tag(TAG).d("getFaceIncrement timestamp=$timestamp")
launch { launch {
try { val response = ApiClient.repositoryV2.getFaceIncrement(pageNum, pageSize, timestamp)
val response = ApiClient.repositoryV2.getFaceIncrement(pageNum, pageSize, timestamp) if (!parseResponse(response)) {
if (parseResponse(response)) { onFailure(response.msg ?: "获取增量人脸数据失败")
val list = response.data ?: emptyList() return@launch
val hasNext = list.size >= pageSize }
onSuccess(list, hasNext) withContext(Dispatchers.Default) {
} else { val list = response.data ?: emptyList()
onFailure(response.msg ?: "获取增量人脸数据失败") if (pageNum == 1L && list.isEmpty()) {
return@withContext
} }
} catch (e: Exception) { onPageQueryFinished(list)
Timber.e(e, "获取增量人脸数据异常") if (list.isNotEmpty()) {
onFailure("网络异常: ${e.message}") lastFaceTimestamp = list.last().faceUpdateTimestamp ?: 0L
}
SpTool.lastFaceTimestamp = lastFaceTimestamp
FileUtil.saveLog("获取人脸[增量]数据结束,时间戳为:$lastFaceTimestamp")
onAllQueryFinished()
} }
} }
} }
@@ -190,7 +252,7 @@ class NetViewModelV2 : BaseViewModel() {
} }
} }
fun getMemberInfoByPhone( fun getMemberInfoByPhoneV2(
phone: String, phone: String,
password: String = "", password: String = "",
onSuccess: (memberInfo: NewMemberInfo?) -> Unit, onSuccess: (memberInfo: NewMemberInfo?) -> Unit,
@@ -275,72 +337,326 @@ class NetViewModelV2 : BaseViewModel() {
} }
} }
fun uploadCollect( /** 上传采集图片(suspend,直接返回 picUrls 列表) */
suspend fun uploadCollect(
foodId: Long, foodId: Long,
foodName: String, foodName: String,
version: String, version: String,
foodVector: String, foodVector: String,
fileList: List<okhttp3.MultipartBody.Part>, fileList: List<File>
onSuccess: (picUrls: List<String>) -> Unit, ): List<String>? {
onFailure: (message: String) -> Unit 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 { launchWithLoading {
try { try {
val params = hashMapOf<String, okhttp3.RequestBody>() val response = ApiClient.repositoryV2.deleteCollect(id, version ?: "")
params["foodId"] = okhttp3.RequestBody.create(
okhttp3.MediaType.parse("text/plain"),
foodId.toString()
)
params["foodName"] = okhttp3.RequestBody.create(
okhttp3.MediaType.parse("text/plain"),
foodName
)
params["version"] = okhttp3.RequestBody.create(
okhttp3.MediaType.parse("text/plain"),
version
)
params["foodVector"] = okhttp3.RequestBody.create(
okhttp3.MediaType.parse("text/plain"),
foodVector
)
val response = ApiClient.repositoryV2.uploadCollect(
foodId, foodName, version, foodVector,
fileList.map { it.body().contentType()?.let { ct ->
okhttp3.RequestBody.create(ct, (it.body() as okhttp3.ResponseBody).bytes())
} ?: it.body() } as List<File>
)
if (parseResponse(response)) { if (parseResponse(response)) {
onSuccess(response.data ?: emptyList()) block(true)
} else { } else {
onFailure(response.msg ?: "上传采集数据失败") block(false)
} }
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "上传采集数据异常") 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}") onFailure("网络异常: ${e.message}")
} }
} }
} }
fun deleteCollect( /** 提交就餐营养数据 */
foodId: Long, fun postUserNutritionData(
version: String, param: List<UserNutritionParam>,
onSuccess: () -> Unit, onSuccess: () -> Unit = {},
onFailure: (message: String) -> Unit onFailure: (message: String) -> Unit = {}
) { ) {
launchWithLoading { launch {
try { try {
val response = ApiClient.repositoryV2.deleteCollect(foodId, version) val response = repository.postUserNutritionData(param)
if (parseResponse(response)) { if (parseResponse(response)) {
onSuccess() onSuccess()
} else { } else {
onFailure(response.msg ?: "删除采集菜品失败") onFailure(response.msg ?: "提交营养数据失败")
} }
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "删除采集菜品异常") Timber.e(e, "提交营养数据异常")
onFailure("网络异常: ${e.message}") onFailure("网络异常: ${e.message}")
} }
} }
} }
// ========== V1 兼容包装方法(暂用 V1 API,后续逐步迁移至 V2 ==========
private var lastFaceTimestamp = 0L
/** 搜索食物 */
fun searchByFoodName(foodName: String, action: (List<FoodInfo>) -> Unit = {}) {
Timber.tag(TAG).d("searchByFoodName foodName = $foodName")
launchWithLoading {
val response = repository.getRestInfoFoodsByType(foodName = foodName)
if (parseResponse(response)) {
action(response.data ?: emptyList())
}
}
}
/** 获取用户就餐营养数据 */
fun getUserNutritionData(
userId: String,
block: (UserNutrition?) -> Unit
) {
Timber.tag(TAG).d("getUserNutritionData userId = $userId")
launch {
val response = repository.getUserNutritionData(userId = userId)
if (parseResponse(response)) {
block(response.data)
} else {
block(null)
}
}
}
/** 搜索菜品信息 */
fun getFoodInfo(foodName: String, action: (List<FoodInfo>) -> Unit = {}) {
Timber.tag(TAG).d("getFoodInfo")
launchWithLoading {
val response = repository.getFoodInfo(foodName = foodName)
if (parseResponse(response)) {
action(response.data ?: emptyList())
} else {
action(emptyList())
}
}
}
/** 创建订单 */
fun createOrder(order: FoodOrder, block: (String) -> Unit) {
Timber.tag(TAG).d("createOrder")
launchWithLoading {
val response = repository.createOrder(order)
if (parseResponse(response)) {
block(response.data.toString())
} else {
block("")
}
}
}
/** 获取订单列表 */
fun getFoodOrderList(userId: String, block: (FoodOrderModel?) -> Unit) {
Timber.tag(TAG).d("getFoodOrderList")
launchWithLoading {
val response = repository.getFoodOrderList(userId)
if (parseResponse(response)) {
block(response.data)
} else {
block(null)
}
}
}
/** 根据 ID 查询会员信息 */
fun getMemberInfoById(memberId: String, block: (MemberInfo?) -> Unit) {
Timber.tag(TAG).d("getMemberInfo")
launchWithLoading {
val response = repository.getMemberInfoById(memberId)
if (parseResponse(response)) {
block(response.data)
} else {
block(null)
}
}
}
/** 根据手机号查询会员信息 */
fun getMemberInfoByPhone(phone: String, key: String, block: (MemberInfo?) -> Unit) {
Timber.tag(TAG).d("getMemberInfoByPhone")
launchWithLoading {
val response = repository.getMemberInfoByPhone(phone, key)
if (parseResponse(response)) {
block(response.data)
} else {
block(null)
}
}
}
/** 绑定订单 */
fun bindOrder(userId: String, orderId: String, mode: Int, block: (Boolean) -> Unit) {
Timber.tag(TAG).d("bindOrder")
launchWithLoading {
val response = repository.bindOrder(userId = userId, orderId = orderId, mode = mode)
if (parseResponse(response)) {
block(true)
} else {
block(false)
}
}
}
/** 获取会员折扣 */
fun getMemberDiscount(userId: String, block: (Double?) -> Unit) {
Timber.tag(TAG).d("getMemberDiscount")
launchWithLoading {
val response = repository.getMemberDiscount(userId)
if (parseResponse(response)) {
block(response.data)
} else {
block(null)
}
}
}
} }