From bf6b2547f3947e6e6d027ec8a579ac93bf5d49fc Mon Sep 17 00:00:00 2001 From: lvmeng <848755140@qq.com> Date: Thu, 28 May 2026 15:14:44 +0800 Subject: [PATCH] =?UTF-8?q?refactor(network):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E7=BD=91=E7=BB=9C=E8=AF=B7=E6=B1=82=E9=85=8D=E7=BD=AE=E5=92=8C?= =?UTF-8?q?API=E6=8E=A5=E5=8F=A3=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将BASE_URL改为使用GlobalData.PROD_BASE_URL常量 - 更新ApiClient中的URL匹配规则以支持新的营养模块接口 - 将ApiServiceV2中的接口路径从/neglect/booth改为/nutrition/neglect/booth - 使用@Url注解动态传入完整URL地址 - 在CollectedFoodActivity中切换到NetViewModelV2和新API模型 - 更新采集功能的数据模型和接口调用方式 - 调整人脸数据获取逻辑以支持增量和全量同步 - 优化文件上传和删除操作的参数传递方式 --- .../main/java/com/sw/dualscreen/GlobalData.kt | 3 +- .../activity/CollectedFoodActivity.kt | 36 +- .../sw/dualscreen/activity/InitActivity.kt | 37 +- .../sw/dualscreen/activity/MainActivity.kt | 51 +- .../com/sw/dualscreen/activity/PayActivity.kt | 4 +- .../sw/dualscreen/activity/SettingActivity.kt | 4 +- .../activity/fragment/CollectFragment.kt | 24 +- .../adapter/CollectedFoodNewAdapter.kt | 8 +- .../model/response/v2/CollectedFoodV2.kt | 4 +- .../com/sw/dualscreen/network/ApiClient.kt | 13 +- .../sw/dualscreen/network/api/ApiServiceV2.kt | 52 +- .../presentation/MainScreenPresentation.kt | 4 +- .../presentation/pay/PayPresentation.kt | 4 +- .../repository/v2/RemoteRepositoryV2.kt | 21 +- .../com/sw/dualscreen/utils/FoodVectorTool.kt | 90 ++-- .../view/CustomBottomSheetDialog.kt | 6 +- .../sw/dualscreen/viewmodel/NetViewModelV2.kt | 452 +++++++++++++++--- 17 files changed, 563 insertions(+), 250 deletions(-) diff --git a/app/src/main/java/com/sw/dualscreen/GlobalData.kt b/app/src/main/java/com/sw/dualscreen/GlobalData.kt index a9d9a77..0ce9c31 100644 --- a/app/src/main/java/com/sw/dualscreen/GlobalData.kt +++ b/app/src/main/java/com/sw/dualscreen/GlobalData.kt @@ -22,7 +22,8 @@ object GlobalData { /** * 具体业务 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 PROD_BASE_URL = "https://api.dm.yixiong-tech.com:8443" diff --git a/app/src/main/java/com/sw/dualscreen/activity/CollectedFoodActivity.kt b/app/src/main/java/com/sw/dualscreen/activity/CollectedFoodActivity.kt index 5ea28bf..44230e5 100644 --- a/app/src/main/java/com/sw/dualscreen/activity/CollectedFoodActivity.kt +++ b/app/src/main/java/com/sw/dualscreen/activity/CollectedFoodActivity.kt @@ -5,7 +5,6 @@ import androidx.activity.viewModels import androidx.core.widget.addTextChangedListener import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager -import com.sw.dualscreen.GlobalData import com.sw.dualscreen.R import com.sw.dualscreen.adapter.CollectedFoodNewAdapter 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.hideKeyboard 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.viewmodel.BaseViewModel -import com.sw.dualscreen.viewmodel.UserViewModel +import com.sw.dualscreen.viewmodel.NetViewModelV2 import com.sw.plate.utils.ToastUtils import kotlinx.coroutines.launch @@ -27,7 +26,7 @@ class CollectedFoodActivity : BaseActivity() { private const val PAGE_SIZE = 100 } - private val viewModel by viewModels() + private val viewModel by viewModels() override fun getViewModel(): BaseViewModel { return viewModel } @@ -36,7 +35,7 @@ class CollectedFoodActivity : BaseActivity() { return ActivityCollectedFoodBinding.inflate(layoutInflater) } - private val list: MutableList = mutableListOf() + private val list: MutableList = mutableListOf() private val adapter by lazy { CollectedFoodNewAdapter(list).apply { // isStateViewEnable = true @@ -127,19 +126,26 @@ class CollectedFoodActivity : BaseActivity() { // loadEmptyView() // } - viewModel.getCollectedFoodList( - pageNo = pageNo, - pageSize = PAGE_SIZE, - foodName = searchName ?: "" - ) { items -> - runOnUiThread { - loadFoodList(items) + viewModel.getCollectPage( + pageNum = pageNo.toLong(), + pageSize = PAGE_SIZE.toLong(), + foodName = searchName, + onSuccess = { items -> + runOnUiThread { + loadFoodList(items.onEach { it.foodCount = 1 }) + } + }, + onFailure = { + runOnUiThread { + finishRefresh() + ToastUtils.showToast(it) + } } - } + ) } @SuppressLint("NotifyDataSetChanged") - private fun loadFoodList(items: List) { + private fun loadFoodList(items: List) { finishRefresh() if (pageNo == 1 && items.isEmpty()) { list.clear() @@ -191,7 +197,7 @@ class CollectedFoodActivity : BaseActivity() { private fun deleteGoods(position: Int) { val food = list[position] showWaitingDialog("加载中……") - viewModel.deleteCollectFood(food.foodId, GlobalData.foodModelVersion) { deleteSuccess -> + viewModel.deleteCollect(food.foodId, food.version) { deleteSuccess -> if (deleteSuccess) { // Thread {}.start() lifecycleScope.launch { diff --git a/app/src/main/java/com/sw/dualscreen/activity/InitActivity.kt b/app/src/main/java/com/sw/dualscreen/activity/InitActivity.kt index 78292db..2626056 100644 --- a/app/src/main/java/com/sw/dualscreen/activity/InitActivity.kt +++ b/app/src/main/java/com/sw/dualscreen/activity/InitActivity.kt @@ -19,7 +19,7 @@ import com.sw.dualscreen.utils.L import com.sw.dualscreen.utils.NetworkUtils import com.sw.dualscreen.utils.SpTool 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.ToastUtils import kotlinx.coroutines.launch @@ -29,7 +29,7 @@ class InitActivity : BaseActivity() { const val PAGE_SIZE = 100 } - private val userViewModel by viewModels() + private val userViewModel by viewModels() private var pageNum = 1 override fun getViewModel(): BaseViewModel { @@ -107,6 +107,7 @@ class InitActivity : BaseActivity() { } } } + checkCameraPermissionAndGo() } private fun goMainActivity() { @@ -189,23 +190,23 @@ class InitActivity : BaseActivity() { } private fun getDeviceConfig() { - userViewModel.getDeviceConfig { deviceConfig -> - runOnUiThread { - if (deviceConfig == null) { - ToastUtils.showToast("获取设备配置数据失败") - return@runOnUiThread - } - //var settlementMode = 1 - //结算模式:1-独立支付,2-联合结算 - val settlementMode = if (deviceConfig.payType == 2) 0 else 1 - // TODO: 2022/10/13 修改为独立支付 - SpTool.settlementMode = settlementMode + userViewModel.getDeviceConfig( + onSuccess = { deviceConfig -> + runOnUiThread { + if (deviceConfig == null) { + ToastUtils.showToast("获取设备配置数据失败") + return@runOnUiThread + } + val settlementMode = if (deviceConfig.payType == 2) 0 else 1 + SpTool.settlementMode = settlementMode - GlobalData.appId = deviceConfig.arcsoftAppId ?: "" - GlobalData.sdkKey = deviceConfig.arcsoftSdkKey ?: "" - GlobalData.activeKey = deviceConfig.arcsoftActiveKey ?: "" - } - } + GlobalData.appId = deviceConfig.arcsoftAppId ?: "" + GlobalData.sdkKey = deviceConfig.arcsoftSdkKey ?: "" + GlobalData.activeKey = deviceConfig.arcsoftActiveKey ?: "" + } + }, + onFailure = { runOnUiThread { ToastUtils.showToast(it) } } + ) } private fun loadNetworkErrorDialog() { diff --git a/app/src/main/java/com/sw/dualscreen/activity/MainActivity.kt b/app/src/main/java/com/sw/dualscreen/activity/MainActivity.kt index 4753028..ee19271 100644 --- a/app/src/main/java/com/sw/dualscreen/activity/MainActivity.kt +++ b/app/src/main/java/com/sw/dualscreen/activity/MainActivity.kt @@ -48,7 +48,7 @@ import com.sw.dualscreen.model.response.FoodOrder import com.sw.dualscreen.model.response.PaySuccessEvent import com.sw.dualscreen.model.response.ResetRecognizeEvent 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.FoodModule 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.view.CustomBottomSheetDialog 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.LightManager import com.sw.plate.utils.ToastUtils @@ -81,8 +81,6 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext -import okhttp3.RequestBody -import okhttp3.RequestBody.Companion.toRequestBody import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import org.json.JSONObject @@ -109,7 +107,7 @@ class MainActivity : BaseActivity() { const val WEIGHT_RESET_RECOGNIZE = 10 } - private val viewModel by viewModels() + private val viewModel by viewModels() private var imageCapture: ImageCapture? = null var isAnalyzing = true // 控制是否进行图像分析 @@ -208,7 +206,7 @@ class MainActivity : BaseActivity() { addBackEventListener() initView() if (SpTool.firstGetFace) { - viewModel.getUserFaceCache(pageNo = 1) + viewModel.getFacePage(pageNum = 1) } setupCamera() initData() @@ -273,7 +271,7 @@ class MainActivity : BaseActivity() { userViewModel = viewModel, lifecycleScope = lifecycleScope, successBlock = { - viewModel.getUserFaceCache(pageNo = 1) + hideWaitingDialog() }, failureBlock = { hideWaitingDialog() @@ -281,7 +279,7 @@ class MainActivity : BaseActivity() { } ) } - viewModel.getUserFaceCache(pageNo = 1, onSuccess = { + viewModel.getFacePage(pageNum = 1, onSuccess = { recognizeViewModel.refreshFaceList() hideWaitingDialog() ToastUtils.showToast("人脸数据已更新") @@ -1111,14 +1109,12 @@ class MainActivity : BaseActivity() { log("是否null判断,imageFile == null:${imageFile == null},imageVector == null:${imageVector == null}") return } - val foodId = foodInfo.foodId + val foodId = foodInfo.foodId.toLongOrNull() ?: run { + loadRemindDialog("foodId 格式错误: ${foodInfo.foodId}") + return + } val foodName = foodInfo.foodName ?: "" val foodModelVersion = GlobalData.foodModelVersion - val params = HashMap() - params["foodId"] = foodId.toRequestBody() - params["foodName"] = foodName.toRequestBody() - params["version"] = foodModelVersion.toRequestBody() - val files = listOf(imageFile) val vectors = listOf(imageVector) val foodVectorList = vectors.map { it.joinToString( @@ -1128,8 +1124,13 @@ class MainActivity : BaseActivity() { val foodVectorJson = foodVectorList.joinToString(separator = ",", prefix = "[", postfix = "]") log("json=$foodVectorJson") - params["foodVector"] = foodVectorJson.toRequestBody() - val idList = viewModel.uploadCollectFoodPics(files, params) + val idList = viewModel.uploadCollect( + foodId = foodId, + foodName = foodName, + version = foodModelVersion, + foodVector = foodVectorJson, + fileList = listOf(imageFile) + ) if (idList.isNullOrEmpty()) { loadRemindDialog("上传失败,未返回向量数据id") log("idList为空") @@ -1146,7 +1147,7 @@ class MainActivity : BaseActivity() { ObjectBox.put( Food( collectId = idList[0], - foodId = foodId, + foodId = "$foodId", foodName = foodName, foodVector = imageVector, version = foodModelVersion @@ -1204,7 +1205,7 @@ class MainActivity : BaseActivity() { //if (timestamp == 0L) { // return@startIntervalTaskWithInitialDelay //} - viewModel.getFaceIncrementList( + viewModel.getFaceIncrement( // pageNo = taskPageNo, timestamp = timestamp, onAllQueryFinished = { @@ -1223,21 +1224,21 @@ class MainActivity : BaseActivity() { } } - private fun updateFaceData(list: List) { + private fun updateFaceData(list: List) { val faceList = mutableListOf() try { - list.forEach { model -> - if (model.faceDeleted == true) { + list.forEach { vo -> + if (vo.faceDeleted == true) { //删除数据 - FaceDatabase.getInstance(this).faceDao().deleteFaceById(model.userId) + FaceDatabase.getInstance(this).faceDao().deleteFaceById(vo.userId) } else { //保存数据 val faceEntity = FaceEntity( - model.userId, null, Base64.decode(model.faceFeatureStr) + vo.userId, null, Base64.decode(vo.faceFeature) ).also { //1-会员、2-临时用户 - it.userType = if (model.member) "1" else "2" - it.cardNo = model.cardNo + it.userType = if (vo.member == true) "1" else "2" + it.cardNo = vo.cardNo } faceList.add(faceEntity) } diff --git a/app/src/main/java/com/sw/dualscreen/activity/PayActivity.kt b/app/src/main/java/com/sw/dualscreen/activity/PayActivity.kt index 5055ced..5ba4fce 100644 --- a/app/src/main/java/com/sw/dualscreen/activity/PayActivity.kt +++ b/app/src/main/java/com/sw/dualscreen/activity/PayActivity.kt @@ -20,7 +20,7 @@ import com.sw.dualscreen.utils.Debouncer import com.sw.dualscreen.utils.SPUtil import com.sw.dualscreen.utils.SoundPoolUtil 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 org.greenrobot.eventbus.EventBus import timber.log.Timber @@ -43,7 +43,7 @@ class PayActivity : BaseActivity() { const val PAY_SUCCESS = "pay_success" } - val userViewModel by viewModels() + val userViewModel by viewModels() val recognizeViewModel by viewModels() override fun getViewModel(): BaseViewModel { diff --git a/app/src/main/java/com/sw/dualscreen/activity/SettingActivity.kt b/app/src/main/java/com/sw/dualscreen/activity/SettingActivity.kt index 22a6fe7..0cf6437 100644 --- a/app/src/main/java/com/sw/dualscreen/activity/SettingActivity.kt +++ b/app/src/main/java/com/sw/dualscreen/activity/SettingActivity.kt @@ -11,12 +11,12 @@ import com.sw.dualscreen.model.response.FoodInfo import com.sw.dualscreen.model.response.ClickBackEvent import com.sw.dualscreen.model.response.UpdateRefreshEvent import com.sw.dualscreen.viewmodel.BaseViewModel -import com.sw.dualscreen.viewmodel.UserViewModel +import com.sw.dualscreen.viewmodel.NetViewModelV2 import org.greenrobot.eventbus.EventBus class SettingActivity : BaseActivity() { - val viewModel by viewModels() + val viewModel by viewModels() override fun getViewModel(): BaseViewModel { return viewModel } diff --git a/app/src/main/java/com/sw/dualscreen/activity/fragment/CollectFragment.kt b/app/src/main/java/com/sw/dualscreen/activity/fragment/CollectFragment.kt index 0e84beb..288c318 100644 --- a/app/src/main/java/com/sw/dualscreen/activity/fragment/CollectFragment.kt +++ b/app/src/main/java/com/sw/dualscreen/activity/fragment/CollectFragment.kt @@ -36,8 +36,6 @@ import com.sw.dualscreen.utils.ImageUtil import com.sw.plate.utils.ToastUtils import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import okhttp3.RequestBody -import okhttp3.RequestBody.Companion.toRequestBody import timber.log.Timber @SuppressLint("NotifyDataSetChanged") @@ -263,14 +261,15 @@ class CollectFragment : BaseFragment() { lifecycleScope.launch { val totalFileCount = foodCollectionList.count { it.imageFile != null } settingActivity?.showWaitingDialog2("图片上传中0/$totalFileCount") - val params = HashMap() - //params["placeId"] = restId.toRequestBody() - params["foodId"] = checkedItem!!.foodId.toRequestBody() - params["foodName"] = checkedItem!!.foodName!!.toRequestBody() - params["version"] = GlobalData.foodModelVersion.toRequestBody() + val foodId = checkedItem!!.foodId.toLongOrNull() ?: run { + ToastUtils.showToast("foodId 格式错误") + return@launch + } + val foodName = checkedItem!!.foodName ?: "" + val version = GlobalData.foodModelVersion 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 { it.imageVector!!.joinToString( separator = ",", prefix = "[", postfix = "]" @@ -279,8 +278,13 @@ class CollectFragment : BaseFragment() { val foodVectorJson = foodVectorList.joinToString(separator = ",", prefix = "[", postfix = "]") Timber.tag(TAG).d("json=$foodVectorJson") - params["foodVector"] = foodVectorJson.toRequestBody() - settingActivity?.viewModel?.uploadCollectFoodPics(files, params) + settingActivity?.viewModel?.uploadCollect( + foodId = foodId, + foodName = foodName, + version = version, + foodVector = foodVectorJson, + fileList = files + ) }, onProgress = { count, batch, idList -> //batch.forEach { // it.uploadSuccess = true diff --git a/app/src/main/java/com/sw/dualscreen/adapter/CollectedFoodNewAdapter.kt b/app/src/main/java/com/sw/dualscreen/adapter/CollectedFoodNewAdapter.kt index 15c388b..95eaab8 100644 --- a/app/src/main/java/com/sw/dualscreen/adapter/CollectedFoodNewAdapter.kt +++ b/app/src/main/java/com/sw/dualscreen/adapter/CollectedFoodNewAdapter.kt @@ -8,10 +8,10 @@ import com.chad.library.adapter4.viewholder.QuickViewHolder import com.sw.dualscreen.databinding.ListItemCollectedFoodBinding import com.sw.dualscreen.ext.gone 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) : - BaseQuickAdapter(list) { +class CollectedFoodNewAdapter(var list: MutableList) : + BaseQuickAdapter(list) { inner class VH(var binding: ListItemCollectedFoodBinding) : QuickViewHolder(binding.root) @@ -21,7 +21,7 @@ class CollectedFoodNewAdapter(var list: MutableList) : 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.tvCollectedNum.text = "已采集${item?.foodCount}" holder.binding.divider.run { diff --git a/app/src/main/java/com/sw/dualscreen/model/response/v2/CollectedFoodV2.kt b/app/src/main/java/com/sw/dualscreen/model/response/v2/CollectedFoodV2.kt index 20a744e..4c88d31 100644 --- a/app/src/main/java/com/sw/dualscreen/model/response/v2/CollectedFoodV2.kt +++ b/app/src/main/java/com/sw/dualscreen/model/response/v2/CollectedFoodV2.kt @@ -13,5 +13,7 @@ data class CollectedFoodV2( val foodName: String?, val version: String?, val foodVector: String?, - val picUrls: List? + val picUrls: List?, + /** 已采集数量(客户端手动赋值,非接口字段,用于兼容旧版 UI) */ + var foodCount: Int = 0 ) : Parcelable diff --git a/app/src/main/java/com/sw/dualscreen/network/ApiClient.kt b/app/src/main/java/com/sw/dualscreen/network/ApiClient.kt index aec8e0a..9026ef7 100644 --- a/app/src/main/java/com/sw/dualscreen/network/ApiClient.kt +++ b/app/src/main/java/com/sw/dualscreen/network/ApiClient.kt @@ -1,6 +1,7 @@ package com.sw.dualscreen.network import com.sw.dualscreen.BuildConfig +import com.sw.dualscreen.GlobalData import com.sw.dualscreen.MyApp import com.sw.dualscreen.network.api.ApiService import com.sw.dualscreen.network.api.ApiServiceV2 @@ -17,7 +18,8 @@ import timber.log.Timber import java.util.concurrent.TimeUnit 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 @@ -25,12 +27,15 @@ object ApiClient { .connectTimeout(TIME_OUT, TimeUnit.SECONDS) .readTimeout(TIME_OUT, TimeUnit.SECONDS) .writeTimeout(TIME_OUT, TimeUnit.SECONDS) - .addNetworkInterceptor(Interceptor {chain -> + .addNetworkInterceptor(Interceptor { chain -> val request = chain.request() 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("neglect/booth/")) "ApiV2" + else if(url.contains("nutrition/neglect")) "V2" else "ApiClient" val loggingInterceptor = HttpLoggingInterceptor(logger = { Timber.tag(tag).d("okhttp logger ==>${it}") diff --git a/app/src/main/java/com/sw/dualscreen/network/api/ApiServiceV2.kt b/app/src/main/java/com/sw/dualscreen/network/api/ApiServiceV2.kt index 8735bb9..bcd27a3 100644 --- a/app/src/main/java/com/sw/dualscreen/network/api/ApiServiceV2.kt +++ b/app/src/main/java/com/sw/dualscreen/network/api/ApiServiceV2.kt @@ -20,81 +20,97 @@ import retrofit2.http.Multipart import retrofit2.http.POST import retrofit2.http.Part import retrofit2.http.PartMap +import retrofit2.http.Url /** * 新系统 API 接口服务 - * 基于 /neglect/booth 模块前缀 + * 基于 /nutrition/neglect/booth 模块前缀 * 文档版本:2026-05-27 */ interface ApiServiceV2 { - @GET("/neglect/booth/device/config") - suspend fun getDeviceConfig(): ApiResponse + @GET + suspend fun getDeviceConfig( + @Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/booth/device/config" + ): ApiResponse - @POST("/neglect/booth/face/page") + @POST suspend fun getFacePage( + @Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/booth/face/page", @Body request: Map ): ApiResponse?> - @POST("/neglect/booth/face/increment") + @POST suspend fun getFaceIncrement( + @Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/booth/face/increment", @Body request: Map ): ApiResponse?> - @POST("/neglect/booth/food/by-names") + @POST suspend fun getFoodByNames( + @Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/booth/food/by-names", @Body request: FoodSearchReq ): ApiResponse?> - @POST("/neglect/booth/user/current-food") + @POST suspend fun getUserCurrentFood( + @Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/booth/user/current-food", @Body request: Map ): ApiResponse - @POST("/neglect/booth/order/place") + @POST suspend fun placeOrder( + @Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/booth/order/place", @Body request: PlaceOrderRequest ): ApiResponse - @POST("/neglect/booth/order/settlement") + @POST suspend fun getSettlementOrders( + @Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/booth/order/settlement", @Body request: Map ): ApiResponse - @POST("/neglect/booth/member/info") + @POST suspend fun getMemberInfo( + @Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/booth/member/info", @Body request: Map ): ApiResponse - @POST("/neglect/booth/member/info-by-phone") + @POST suspend fun getMemberInfoByPhone( + @Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/booth/member/info-by-phone", @Body request: Map ): ApiResponse - @POST("/neglect/booth/member/discount") + @POST suspend fun getMemberDiscount( + @Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/booth/member/discount", @Body request: Map ): ApiResponse - @POST("/neglect/booth/order/bind-user") + @POST suspend fun bindUserOrder( + @Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/booth/order/bind-user", @Body request: BindUserOrderRequest ): ApiResponse - @POST("/neglect/booth/collect/page") + @POST suspend fun getCollectPage( - @Body request: Map + @Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/booth/collect/page", + @Body request: Map ): ApiResponse?> @Multipart - @POST("/neglect/booth/collect/upload") + @POST suspend fun uploadCollect( + @Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/booth/collect/upload", @PartMap params: HashMap, @Part foodPics: List ): ApiResponse?> - @POST("/neglect/booth/collect/delete") + @POST suspend fun deleteCollect( - @Body request: Map + @Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/booth/collect/delete", + @Body request: Map ): ApiResponse } diff --git a/app/src/main/java/com/sw/dualscreen/presentation/MainScreenPresentation.kt b/app/src/main/java/com/sw/dualscreen/presentation/MainScreenPresentation.kt index 6f1a651..ec88b17 100644 --- a/app/src/main/java/com/sw/dualscreen/presentation/MainScreenPresentation.kt +++ b/app/src/main/java/com/sw/dualscreen/presentation/MainScreenPresentation.kt @@ -45,7 +45,7 @@ import com.sw.dualscreen.model.response.UserNutrition import com.sw.dualscreen.utils.Debouncer import com.sw.dualscreen.utils.SPUtil 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.ToastUtils import com.sw.plate.utils.arcface.ConfigUtil @@ -73,7 +73,7 @@ import kotlin.math.roundToInt class MainScreenPresentation( val activity: MainActivity, display: Display, - val userViewModel: UserViewModel, + val userViewModel: NetViewModelV2, val recognizeViewModel: RecognizeViewModel, private val onDismissListener: () -> Unit = {} ) : Presentation(activity, display), ViewTreeObserver.OnGlobalLayoutListener { diff --git a/app/src/main/java/com/sw/dualscreen/presentation/pay/PayPresentation.kt b/app/src/main/java/com/sw/dualscreen/presentation/pay/PayPresentation.kt index d58a1de..d544087 100644 --- a/app/src/main/java/com/sw/dualscreen/presentation/pay/PayPresentation.kt +++ b/app/src/main/java/com/sw/dualscreen/presentation/pay/PayPresentation.kt @@ -34,7 +34,7 @@ import com.sw.dualscreen.ext.visible import com.sw.dualscreen.model.response.MemberInfo import com.sw.dualscreen.model.response.TextBean 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.arcface.ConfigUtil import com.sw.plate.utils.arcface.ErrorCodeUtil @@ -56,7 +56,7 @@ import java.util.concurrent.atomic.AtomicBoolean class PayPresentation( val activity: PayActivity, display: Display, - val userViewModel: UserViewModel, + val userViewModel: NetViewModelV2, val recognizeViewModel: RecognizeViewModel, private val onDismissListener: () -> Unit = {} ) : Presentation(activity, display), ViewTreeObserver.OnGlobalLayoutListener { diff --git a/app/src/main/java/com/sw/dualscreen/repository/v2/RemoteRepositoryV2.kt b/app/src/main/java/com/sw/dualscreen/repository/v2/RemoteRepositoryV2.kt index e42e533..8295b73 100644 --- a/app/src/main/java/com/sw/dualscreen/repository/v2/RemoteRepositoryV2.kt +++ b/app/src/main/java/com/sw/dualscreen/repository/v2/RemoteRepositoryV2.kt @@ -17,6 +17,7 @@ import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody import okhttp3.RequestBody import okhttp3.RequestBody.Companion.asRequestBody +import okhttp3.RequestBody.Companion.toRequestBody import java.io.File class RemoteRepositoryV2 constructor( @@ -169,22 +170,10 @@ class RemoteRepositoryV2 constructor( fileList: List ): ApiResponse?> { val params = hashMapOf() - params["foodId"] = RequestBody.create( - "text/plain".toMediaTypeOrNull(), - foodId.toString() - ) - params["foodName"] = RequestBody.create( - "text/plain".toMediaTypeOrNull(), - foodName - ) - params["version"] = RequestBody.create( - "text/plain".toMediaTypeOrNull(), - version - ) - params["foodVector"] = RequestBody.create( - "text/plain".toMediaTypeOrNull(), - foodVector - ) + params["foodId"] = foodId.toString().toRequestBody("text/plain".toMediaTypeOrNull()) + params["foodName"] = foodName.toRequestBody("text/plain".toMediaTypeOrNull()) + params["version"] = version.toRequestBody("text/plain".toMediaTypeOrNull()) + params["foodVector"] = foodVector.toRequestBody("text/plain".toMediaTypeOrNull()) val fileParts = mutableListOf() fileList.forEach { file -> diff --git a/app/src/main/java/com/sw/dualscreen/utils/FoodVectorTool.kt b/app/src/main/java/com/sw/dualscreen/utils/FoodVectorTool.kt index 2d16b71..85abea6 100644 --- a/app/src/main/java/com/sw/dualscreen/utils/FoodVectorTool.kt +++ b/app/src/main/java/com/sw/dualscreen/utils/FoodVectorTool.kt @@ -1,11 +1,10 @@ package com.sw.dualscreen.utils import androidx.lifecycle.LifecycleCoroutineScope -import com.sw.dualscreen.GlobalData -import com.sw.dualscreen.model.response.FoodVector +import com.sw.dualscreen.model.response.v2.CollectedFoodV2 import com.sw.dualscreen.objbox.Food import com.sw.dualscreen.objbox.ObjectBox -import com.sw.dualscreen.viewmodel.UserViewModel +import com.sw.dualscreen.viewmodel.NetViewModelV2 import kotlinx.coroutines.launch object FoodVectorTool { @@ -15,19 +14,13 @@ object FoodVectorTool { /** * 统一入口:分页拉取所有食物向量数据并保存到本地 ObjectBox - * - * @param userViewModel 请求 ViewModel - * @param lifecycleScope 协程作用域(用于保存操作) - * @param successBlock 全部数据拉取并保存成功后回调 - * @param failureBlock 任意一次请求失败时回调,参数为错误信息 */ fun loadAndSaveFoodVector( - userViewModel: UserViewModel, + userViewModel: NetViewModelV2, lifecycleScope: LifecycleCoroutineScope, successBlock: () -> Unit, failureBlock: (String) -> Unit ) { - // 每次调用从第 1 页开始,避免单例状态污染 fetchPage( pageNum = 1, userViewModel = userViewModel, @@ -37,68 +30,47 @@ object FoodVectorTool { ) } - /** - * 递归分页请求(内部方法,外部不感知分页细节) - * - * @param pageNum 当前页码 - * @param userViewModel 请求 ViewModel - * @param lifecycleScope 协程作用域 - * @param successBlock 成功回调 - * @param failureBlock 失败回调 - */ private fun fetchPage( pageNum: Int, - userViewModel: UserViewModel, + userViewModel: NetViewModelV2, lifecycleScope: LifecycleCoroutineScope, successBlock: () -> Unit, failureBlock: (String) -> Unit ) { - userViewModel.getCollectedFoodVector( - param = mutableMapOf( - "pageNum" to "$pageNum", - "pageSize" to "$PAGE_SIZE", - "version" to GlobalData.foodModelVersion - ) - ) { items -> - if (items == null) { - failureBlock("未查询到向量数据") - return@getCollectedFoodVector - } - // 第一页为空,视为成功(数据库无数据) - if (pageNum == 1 && items.isEmpty()) { - successBlock() - return@getCollectedFoodVector - } - // 有数据则保存 - if (items.isNotEmpty()) { - saveFoodVector(lifecycleScope, items) - } - // 数据量达到一页上限,继续拉取下一页 - if (items.size >= PAGE_SIZE) { - fetchPage(pageNum + 1, userViewModel, lifecycleScope, successBlock, failureBlock) - } else { - successBlock() - } - } + userViewModel.getCollectPage( + pageNum = pageNum.toLong(), + pageSize = PAGE_SIZE.toLong(), + onSuccess = { items -> + if (pageNum == 1 && items.isEmpty()) { + successBlock() + return@getCollectPage + } + if (items.isNotEmpty()) { + saveFoodVector(lifecycleScope, items) + } + if (items.size >= PAGE_SIZE) { + fetchPage(pageNum + 1, userViewModel, lifecycleScope, successBlock, failureBlock) + } else { + successBlock() + } + }, + onFailure = { failureBlock(it) } + ) } /** - * 将向量数据列表保存到 ObjectBox 本地数据库 - * - * @param lifecycleScope 协程作用域 - * @param list 待保存的食物向量数据列表 + * 将 CollectedFoodV2 列表保存到 ObjectBox 本地数据库 */ - private fun saveFoodVector(lifecycleScope: LifecycleCoroutineScope, list: List) { + private fun saveFoodVector(lifecycleScope: LifecycleCoroutineScope, list: List) { lifecycleScope.launch { - val vectorList = list.map { vt -> - // 解析向量字符串 "[0.1,0.2,...]" 为 FloatArray - val foodVector = vt.foodVector?.removeSurrounding("[", "]") + val vectorList = list.map { item -> + val foodVector = item.foodVector?.removeSurrounding("[", "]") ?.split(",")?.map { it.toFloatOrNull() ?: 0.0f }?.toFloatArray() Food( - collectId = vt.id, - foodId = vt.foodId, - foodName = vt.foodName, - version = vt.version, + collectId = item.foodId, + foodId = item.foodId, + foodName = item.foodName, + version = item.version, foodVector = foodVector ) } diff --git a/app/src/main/java/com/sw/dualscreen/view/CustomBottomSheetDialog.kt b/app/src/main/java/com/sw/dualscreen/view/CustomBottomSheetDialog.kt index 13585d7..afbe783 100644 --- a/app/src/main/java/com/sw/dualscreen/view/CustomBottomSheetDialog.kt +++ b/app/src/main/java/com/sw/dualscreen/view/CustomBottomSheetDialog.kt @@ -15,7 +15,7 @@ import com.sw.dualscreen.databinding.BottomSheetDialogBinding import com.sw.dualscreen.model.response.FoodInfo import com.sw.dualscreen.utils.Debouncer import com.sw.dualscreen.utils.KeyboardUtils -import com.sw.dualscreen.viewmodel.UserViewModel +import com.sw.dualscreen.viewmodel.NetViewModelV2 import timber.log.Timber import androidx.core.graphics.drawable.toDrawable @@ -24,7 +24,7 @@ import androidx.core.graphics.drawable.toDrawable */ @SuppressLint("NotifyDataSetChanged") class CustomBottomSheetDialog( - val viewModel: UserViewModel, + val viewModel: NetViewModelV2, val itemClickCallback: (FoodInfo) -> Unit ) : BottomSheetDialogFragment() { private lateinit var binding: BottomSheetDialogBinding @@ -87,7 +87,7 @@ class CustomBottomSheetDialog( companion object { fun newInstance( - viewModel: UserViewModel, + viewModel: NetViewModelV2, itemClickCallback: (FoodInfo) -> Unit ): CustomBottomSheetDialog { return CustomBottomSheetDialog(viewModel, itemClickCallback) diff --git a/app/src/main/java/com/sw/dualscreen/viewmodel/NetViewModelV2.kt b/app/src/main/java/com/sw/dualscreen/viewmodel/NetViewModelV2.kt index f6f50cd..c1629ad 100644 --- a/app/src/main/java/com/sw/dualscreen/viewmodel/NetViewModelV2.kt +++ b/app/src/main/java/com/sw/dualscreen/viewmodel/NetViewModelV2.kt @@ -1,9 +1,18 @@ 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.FoodOrder +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 @@ -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.SettlementOrder 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() { @@ -23,6 +42,16 @@ class NetViewModelV2 : BaseViewModel() { const val PAGE_SIZE = 100L } + private val faceApi: FaceApi = FaceApi() + + /** 人脸加载完成 */ + private val _loadFaceResult = MutableStateFlow(false) + val loadFaceResult: StateFlow = _loadFaceResult + + /** 饭点类型 早餐/午餐/晚餐 */ + private val _dinnerTypeInfo = MutableStateFlow(null) + val dinnerTypeInfo: StateFlow = _dinnerTypeInfo + fun getDeviceConfig( onSuccess: (config: com.sw.dualscreen.model.response.DeviceConfig?) -> Unit, onFailure: (message: String) -> Unit @@ -42,49 +71,82 @@ class NetViewModelV2 : BaseViewModel() { } } + /** 获取人脸全量数据(递归分页 + FaceApi 集成) */ fun getFacePage( pageNum: Long = 1L, pageSize: Long = PAGE_SIZE, - onSuccess: (list: List, hasNext: Boolean) -> Unit, - onFailure: (message: String) -> Unit + onSuccess: () -> Unit = {}, + onFailure: (String) -> Unit = {} ) { + var currentPageNum = pageNum + Timber.tag(TAG).d("getFacePage index = $currentPageNum") launch { - try { - val response = ApiClient.repositoryV2.getFacePage(pageNum, pageSize) - if (parseResponse(response)) { - val list = response.data ?: emptyList() - val hasNext = list.size >= pageSize - onSuccess(list, hasNext) - } else { - onFailure(response.msg ?: "获取人脸数据失败") + _loadFaceResult.value = false + SpTool.lastFaceTimestamp = 0 + 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 } - } catch (e: Exception) { - Timber.e(e, "获取全量人脸数据异常") - onFailure("网络异常: ${e.message}") + val faceEntity = list.map { vo -> + FaceEntity(vo.userId, null, Base64.decode(vo.faceFeature)).also { + 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( pageNum: Long = 1L, pageSize: Long = PAGE_SIZE, timestamp: Long, - onSuccess: (list: List, hasNext: Boolean) -> Unit, - onFailure: (message: String) -> Unit + onAllQueryFinished: () -> Unit, + onPageQueryFinished: (List) -> Unit, + onFailure: (String) -> Unit = {} ) { + Timber.tag(TAG).d("getFaceIncrement timestamp=$timestamp") launch { - try { - val response = ApiClient.repositoryV2.getFaceIncrement(pageNum, pageSize, timestamp) - if (parseResponse(response)) { - val list = response.data ?: emptyList() - val hasNext = list.size >= pageSize - onSuccess(list, hasNext) - } else { - onFailure(response.msg ?: "获取增量人脸数据失败") + 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 } - } catch (e: Exception) { - Timber.e(e, "获取增量人脸数据异常") - onFailure("网络异常: ${e.message}") + onPageQueryFinished(list) + if (list.isNotEmpty()) { + 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, password: String = "", onSuccess: (memberInfo: NewMemberInfo?) -> Unit, @@ -275,72 +337,326 @@ class NetViewModelV2 : BaseViewModel() { } } - fun uploadCollect( + /** 上传采集图片(suspend,直接返回 picUrls 列表) */ + suspend fun uploadCollect( foodId: Long, foodName: String, version: String, foodVector: String, - fileList: List, - onSuccess: (picUrls: List) -> Unit, - onFailure: (message: String) -> Unit + fileList: List + ): List? { + 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 params = hashMapOf() - 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 - ) - + val response = ApiClient.repositoryV2.deleteCollect(id, version ?: "") if (parseResponse(response)) { - onSuccess(response.data ?: emptyList()) + block(true) } else { - onFailure(response.msg ?: "上传采集数据失败") + block(false) } } 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, 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, 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}") } } } - fun deleteCollect( - foodId: Long, - version: String, - onSuccess: () -> Unit, - onFailure: (message: String) -> Unit + /** 提交就餐营养数据 */ + fun postUserNutritionData( + param: List, + onSuccess: () -> Unit = {}, + onFailure: (message: String) -> Unit = {} ) { - launchWithLoading { + launch { try { - val response = ApiClient.repositoryV2.deleteCollect(foodId, version) + val response = repository.postUserNutritionData(param) if (parseResponse(response)) { onSuccess() } else { - onFailure(response.msg ?: "删除采集菜品失败") + onFailure(response.msg ?: "提交营养数据失败") } } catch (e: Exception) { - Timber.e(e, "删除采集菜品异常") + Timber.e(e, "提交营养数据异常") onFailure("网络异常: ${e.message}") } } } + + // ========== V1 兼容包装方法(暂用 V1 API,后续逐步迁移至 V2) ========== + + private var lastFaceTimestamp = 0L + + /** 搜索食物 */ + fun searchByFoodName(foodName: String, action: (List) -> 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) -> 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) + } + } + } }