diff --git a/app/src/main/java/com/shuwei/dish/match/dialog/SeasoningSelectDialog.kt b/app/src/main/java/com/shuwei/dish/match/dialog/SeasoningSelectDialog.kt index 54db5fc..f8f305f 100644 --- a/app/src/main/java/com/shuwei/dish/match/dialog/SeasoningSelectDialog.kt +++ b/app/src/main/java/com/shuwei/dish/match/dialog/SeasoningSelectDialog.kt @@ -15,7 +15,6 @@ import android.view.Window import android.view.WindowManager import android.widget.FrameLayout import androidx.core.widget.addTextChangedListener -import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.bottomsheet.BottomSheetDialog @@ -25,13 +24,11 @@ import com.shuwei.dish.match.base.BaseActivity import com.shuwei.dish.match.databinding.DialogSeasoningSelectBinding import com.shuwei.dish.match.databinding.LayoutEmptyViewBinding import com.shuwei.dish.match.model.GoodsItem +import com.shuwei.dish.match.model.SeasoningSearchItem import com.shuwei.dish.match.net.UiState import com.shuwei.dish.match.utils.KeyboardUtil import com.shuwei.dish.match.utils.ext.addOnActionSearchListener import com.shuwei.dish.match.utils.ext.toast -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch - /** * 调料选择弹窗,不含重量显示,继承 BottomSheetDialog 确保只初始化一次 * @param activity 宿主 Activity @@ -64,7 +61,6 @@ class SeasoningSelectDialog( private var goodsName: String? = null private var pageNo = 1 - private var collectJob: Job? = null init { setContentView(binding.root) @@ -77,7 +73,6 @@ class SeasoningSelectDialog( window?.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) setOnDismissListener { - collectJob?.cancel() activity.hideStatusBar() } @@ -116,23 +111,6 @@ class SeasoningSelectDialog( /** 弹窗显示时自动加载默认调料列表,并在 Window.Callback 层提前拦截触摸以解决滑动冲突 */ override fun show() { super.show() - // 启动 StateFlow 收集,弹窗关闭时由 setOnDismissListener 取消 - collectJob = activity.lifecycleScope.launch { - activity.netViewModel.foodSearchGoodsListState.collect { state -> - when (state) { - is UiState.Loading -> {} - is UiState.Success -> loadGoodsList(state.data) - is UiState.Error -> { - activity.toast(state.msg) - finishRefresh() - if (pageNo == 1) { - loadEmptyView() - } - } - is UiState.Idle -> {} - } - } - } pageNo = 1 getGoodsList() // Window.Callback.dispatchTouchEvent 在整个 View 树的 onInterceptTouchEvent 之前执行, @@ -184,14 +162,24 @@ class SeasoningSelectDialog( } /** - * 请求调料列表 + * 请求调料列表(新配比秤接口,回调版本) */ private fun getGoodsList() { - activity.netViewModel.queryFoodSearchGoodsList( - goodsType = "1", + activity.netViewModel.querySeasoningSearch( + keyword = goodsName.takeUnless { it.isNullOrBlank() }, pageNum = pageNo, pageSize = PAGE_SIZE, - goodsName = goodsName.takeUnless { it.isNullOrBlank() } + onResult = { state -> + when (state) { + is UiState.Success -> loadGoodsList(state.data) + is UiState.Error -> { + activity.toast(state.msg) + finishRefresh() + if (pageNo == 1) loadEmptyView() + } + else -> {} + } + } ) } @@ -203,23 +191,22 @@ class SeasoningSelectDialog( "盐", "白砂糖", "冰糖", "鸡精", "味精", "陈醋", "老抽", "生抽", "番茄酱", "胡椒粉", "孜然", "十三香", "料酒", "白醋", "蚝油", "辣椒面", "辣椒酱", "豆瓣酱", "淀粉", "葱", "蒜", "姜", "香菜", "油" ) - val mockData = mutableListOf() - nameList.forEachIndexed { index, name -> + val mockData = nameList.mapIndexed { index, name -> val padString = "${index + 1}".padStart(2, '0') - mockData.add(GoodsItem(goodsId = "1000$padString", goodsName = name)) + SeasoningSearchItem(id = "1000$padString", materName = name) } pageNo = 1 loadGoodsList(mockData) } /** - * 将返回数据转换并填充到列表 + * 将新接口返回的 SeasoningSearchItem 映射为 GoodsItem 并填充到列表 + * 新接口的 id 即食材id,同时用作 goodsId 和 materId,无需再交换 */ @SuppressLint("NotifyDataSetChanged") - private fun loadGoodsList(records: MutableList?) { + private fun loadGoodsList(records: List?) { finishRefresh() if (records.isNullOrEmpty()) { - //activity.toast("暂未搜索到调料信息") if (pageNo == 1) { loadEmptyView() } @@ -227,18 +214,26 @@ class SeasoningSelectDialog( } binding.recyclerView.layoutManager = GridLayoutManager(activity, 3, GridLayoutManager.VERTICAL, false) if (pageNo == 1) list.clear() - list.addAll(records) - list.forEach { - it.oldGoodsId = it.goodsId - //materId才是提交数据需要的goodsId数据 - it.goodsId = it.materId ?:"" - } + val goodsItems = records.map { it.toGoodsItem() } + list.addAll(goodsItems) adapter.notifyDataSetChanged() val isLoadMoreEnable = records.size >= PAGE_SIZE binding.refreshLayout.setEnableLoadMore(isLoadMoreEnable) if (isLoadMoreEnable) pageNo++ } + /** + * SeasoningSearchItem → GoodsItem 映射 + * id 既是食材id也是提交时的 goodsId,不需要 oldGoodsId 交换 + */ + private fun SeasoningSearchItem.toGoodsItem() = GoodsItem( + goodsId = id, + materId = id, + goodsName = materName, + materCode = materCode, + popularName = alias + ) + private fun finishRefresh() { if (pageNo == 1) binding.refreshLayout.finishRefresh() else binding.refreshLayout.finishLoadMore() diff --git a/app/src/main/java/com/shuwei/dish/match/model/CookOrderModels.kt b/app/src/main/java/com/shuwei/dish/match/model/CookOrderModels.kt index 8bd9a9f..bb8fbf9 100644 --- a/app/src/main/java/com/shuwei/dish/match/model/CookOrderModels.kt +++ b/app/src/main/java/com/shuwei/dish/match/model/CookOrderModels.kt @@ -52,8 +52,91 @@ data class CookOrderItem( val cookStatusName: String = "", /** 生重(kg),烹制完成后才有值 */ val rawWeight: Double? = null, + /** 熟重(kg),烹制完成后才有值 */ + val cookedWeight: Double? = null, /** 需求份数 */ val needPortions: Int = 0, /** 烹制份数,烹制完成后才有值 */ val cookedPortions: Int? = null ) + +/** + * 菜品食材构成(主材+辅材),对应接口响应中单个食材 + * 对应接口: GET /neglect/ratio-scale/cook-orders/{cookOrderId}/composition + * 注意: 此接口仅返回主材(ingredientClass=1)和辅材(ingredientClass=2),不含调料 + */ +data class CookOrderCompositionItem( + /** 食材id */ + val materId: String = "", + /** 食材名称 */ + val ingredientName: String = "", + /** 食材分类: 1=主材 / 2=辅材 */ + val ingredientClass: Int = 0, + /** 食材分类中文名称 */ + val ingredientClassName: String = "", + /** 标准用量(g) */ + val useWeight: Double = 0.0, + /** 净菜类型id */ + val vegTypeId: String = "" +) + +// ==================== 调料模糊搜索 ==================== + +/** + * 调料模糊搜索请求参数 + * 对应接口: POST /neglect/ratio-scale/seasoning-search + * 数据源: nut_mater_base 一级分类为 18(糖蜜饯)、19(油脂)、20(调味品) + */ +data class SeasoningSearchRequest( + /** 模糊匹配食材名称和别名,为空时返回全部 */ + val keyword: String? = null, + /** 页码 */ + val pageNum: Int = 1, + /** 每页条数 */ + val pageSize: Int = 20 +) + +/** + * 调料模糊搜索结果条目 + * 注意: Long 类型字段服务端序列化为 String + */ +data class SeasoningSearchItem( + /** 食材id(Long → String) */ + val id: String = "", + /** 食材名称 */ + val materName: String = "", + /** 食材编码 */ + val materCode: String = "", + /** 别名 */ + val alias: String = "", + /** 归属食堂id(Long → String) */ + val canteenId: String = "" +) + +// ==================== 烹饪完成 ==================== + +/** + * 烹饪完成 — 单个食材用量条目 + * 对应接口: POST /neglect/ratio-scale/cook-orders/complete + */ +data class CookCompleteItem( + /** 食材id */ + val materId: String = "", + /** 实际用量(g) */ + val actualQty: Double = 0.0 +) + +/** + * 烹饪完成请求,一次性提交主辅材用量+熟重+调料 + * 对应接口: POST /neglect/ratio-scale/cook-orders/complete + */ +data class CookCompleteRequest( + /** 烹制单id */ + val cookOrderId: String = "", + /** 主辅材实际用量列表,至少1条 */ + val items: List = emptyList(), + /** 熟重(kg) */ + val cookedWeight: Double = 0.0, + /** 调料用量列表,选填 */ + val seasonings: List? = null +) diff --git a/app/src/main/java/com/shuwei/dish/match/net/ApiService.kt b/app/src/main/java/com/shuwei/dish/match/net/ApiService.kt index 693bc4f..29b9067 100644 --- a/app/src/main/java/com/shuwei/dish/match/net/ApiService.kt +++ b/app/src/main/java/com/shuwei/dish/match/net/ApiService.kt @@ -2,10 +2,14 @@ package com.shuwei.dish.match.net import com.shuwei.dish.match.base.GlobalData import com.shuwei.dish.match.model.CookFoodDTO +import com.shuwei.dish.match.model.CookOrderCompositionItem import com.shuwei.dish.match.model.CookOrderMealGroup import com.shuwei.dish.match.model.CookOrderPageRequest import com.shuwei.dish.match.model.FoodRecord import com.shuwei.dish.match.model.GoodsItem +import com.shuwei.dish.match.model.CookCompleteRequest +import com.shuwei.dish.match.model.SeasoningSearchItem +import com.shuwei.dish.match.model.SeasoningSearchRequest import okhttp3.MultipartBody import okhttp3.RequestBody import retrofit2.http.Body @@ -14,6 +18,7 @@ import retrofit2.http.Multipart import retrofit2.http.POST import retrofit2.http.Part import retrofit2.http.PartMap +import retrofit2.http.Path import retrofit2.http.Query import retrofit2.http.Url @@ -38,15 +43,6 @@ interface ApiService { @Body param: CookFoodDTO ): ApiResponse - /** - * 查询当日烹饪任务列表(按餐次分组),新配比秤接口 - */ - @POST - suspend fun getCookOrderPage( - @Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/ratio-scale/cook-orders/list", - @Body request: CookOrderPageRequest - ): ApiResponse?> - /** * 搜索菜品(旧接口,采样模式仍在使用) */ @@ -84,4 +80,44 @@ interface ApiService { @PartMap params: MutableMap, @Part foodPics: List ): ApiResponse?> + + + + /** + * 查询当日烹饪任务列表(按餐次分组),新配比秤接口 + */ + @POST + suspend fun getCookOrderPage( + @Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/ratio-scale/cook-orders/list", + @Body request: CookOrderPageRequest + ): ApiResponse?> + + /** + * 查询菜品食材构成(主材+辅材,不含调料),新配比秤接口 + */ + @GET("nutrition/neglect/ratio-scale/cook-orders/{cookOrderId}/composition") + suspend fun getCookOrderComposition( + @Path("cookOrderId") cookOrderId: String + ): ApiResponse?> + + /** + * 调料模糊搜索,新配比秤接口 + * 数据源: nut_mater_base 一级分类为 18(糖蜜饯)、19(油脂)、20(调味品) + */ + @POST + suspend fun getSeasoningSearch( + @Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/ratio-scale/seasoning-search", + @Body request: SeasoningSearchRequest + ): ApiResponse?> + + /** + * 烹饪完成:提交主辅材用量+熟重+调料,一次性完成烹制 + * 新配比秤接口,替代旧 saveConstitute + */ + @POST + suspend fun cookOrderComplete( + @Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/ratio-scale/cook-orders/complete", + @Body request: CookCompleteRequest + ): ApiResponse + } \ No newline at end of file diff --git a/app/src/main/java/com/shuwei/dish/match/net/NetViewModel.kt b/app/src/main/java/com/shuwei/dish/match/net/NetViewModel.kt index a212d1f..9d147ed 100644 --- a/app/src/main/java/com/shuwei/dish/match/net/NetViewModel.kt +++ b/app/src/main/java/com/shuwei/dish/match/net/NetViewModel.kt @@ -4,11 +4,15 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.shuwei.dish.match.base.BaseApp import com.shuwei.dish.match.model.CookFoodDTO +import com.shuwei.dish.match.model.CookOrderCompositionItem import com.shuwei.dish.match.model.CookOrderMealGroup import com.shuwei.dish.match.model.CookOrderPageRequest import com.shuwei.dish.match.model.FoodRecord import com.shuwei.dish.match.model.GoodsItem import com.shuwei.dish.match.model.GoodsNameQueryDTO +import com.shuwei.dish.match.model.CookCompleteRequest +import com.shuwei.dish.match.model.SeasoningSearchItem +import com.shuwei.dish.match.model.SeasoningSearchRequest import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.StateFlow @@ -66,6 +70,23 @@ class NetViewModel( _foodDetailState.value = UiState.Idle } + /** + * 查询菜品食材构成的 UI 状态流(新配比秤接口),UI 层通过 collect 监听 + */ + private val _cookOrderCompositionState = MutableStateFlow?>>(UiState.Idle) + val cookOrderCompositionState: StateFlow?>> = _cookOrderCompositionState.asStateFlow() + + /** + * 查询菜品食材构成(新配比秤接口,仅返回主材+辅材) + * @param cookOrderId 烹制单 ID + */ + fun getCookOrderComposition(cookOrderId: String) { + viewModelScope.launch { + _cookOrderCompositionState.value = UiState.Loading + _cookOrderCompositionState.value = repository.getCookOrderComposition(cookOrderId) + } + } + /** * 搜索菜品列表的 UI 状态流。 * 使用 replay=1 保证 Fragment 重建后能收到最近一次结果; @@ -253,6 +274,45 @@ class NetViewModel( } } + /** + * 调料模糊搜索(新配比秤接口,回调版本) + * 替代旧 queryGoodsList(goodsType=1) 调用 + * @param keyword 模糊匹配食材名称和别名,为 null 时不传该字段 + * @param pageNum 页码,默认第 1 页 + * @param pageSize 每页条数,默认 50 + * @param onLoading 请求发起前的回调 + * @param onResult 结果回调,直接接收 UiState?> + */ + fun querySeasoningSearch( + keyword: String? = null, + pageNum: Int = 1, + pageSize: Int = 50, + onLoading: () -> Unit = {}, + onResult: (UiState?>) -> Unit + ) { + viewModelScope.launch { + onLoading() + onResult(repository.getSeasoningSearch( + SeasoningSearchRequest(keyword = keyword, pageNum = pageNum, pageSize = pageSize) + )) + } + } + + /** + * 烹饪完成(新配比秤接口,回调版本),替代旧 submitCookFood + * 仅在制作模式下 cookOrderId 可用的场景调用 + */ + fun cookOrderComplete( + request: CookCompleteRequest, + onLoading: () -> Unit = {}, + onResult: (UiState) -> Unit + ) { + viewModelScope.launch { + onLoading() + onResult(repository.cookOrderComplete(request)) + } + } + /** * 上传采集菜品信息 UI 状态流,UI 层通过 collect 监听 */ diff --git a/app/src/main/java/com/shuwei/dish/match/net/RemoteRepository.kt b/app/src/main/java/com/shuwei/dish/match/net/RemoteRepository.kt index 38068b4..c0a0eaa 100644 --- a/app/src/main/java/com/shuwei/dish/match/net/RemoteRepository.kt +++ b/app/src/main/java/com/shuwei/dish/match/net/RemoteRepository.kt @@ -1,10 +1,14 @@ package com.shuwei.dish.match.net import com.shuwei.dish.match.model.CookFoodDTO +import com.shuwei.dish.match.model.CookOrderCompositionItem import com.shuwei.dish.match.model.CookOrderMealGroup import com.shuwei.dish.match.model.CookOrderPageRequest import com.shuwei.dish.match.model.FoodRecord import com.shuwei.dish.match.model.GoodsItem +import com.shuwei.dish.match.model.CookCompleteRequest +import com.shuwei.dish.match.model.SeasoningSearchItem +import com.shuwei.dish.match.model.SeasoningSearchRequest import okhttp3.MultipartBody import okhttp3.RequestBody @@ -37,6 +41,10 @@ class RemoteRepository { suspend fun getCookOrderPage(request: CookOrderPageRequest): UiState?> = safeApiCall { apiService.getCookOrderPage(request = request) } + /** 查询菜品食材构成(主材+辅材),新配比秤接口 */ + suspend fun getCookOrderComposition(cookOrderId: String): UiState?> = + safeApiCall { apiService.getCookOrderComposition(cookOrderId = cookOrderId) } + /** 搜索菜品列表 */ suspend fun searchFoodList(param: MutableMap): UiState?> = safeApiCall { apiService.searchFoodList(param = param) } @@ -49,6 +57,14 @@ class RemoteRepository { suspend fun queryGoodsList(param: MutableMap): UiState?> = safeApiCall { apiService.queryGoodsList(param = param) } + /** 调料模糊搜索,新配比秤接口 */ + suspend fun getSeasoningSearch(request: SeasoningSearchRequest): UiState?> = + safeApiCall { apiService.getSeasoningSearch(request = request) } + + /** 烹饪完成,新配比秤接口,替代旧 submitCookFood */ + suspend fun cookOrderComplete(request: CookCompleteRequest): UiState = + safeApiCall { apiService.cookOrderComplete(request = request) } + /** 上传采集数据 */ suspend fun uploadFoodVectorData( params: MutableMap, diff --git a/app/src/main/java/com/shuwei/dish/match/net/RespData.kt b/app/src/main/java/com/shuwei/dish/match/net/RespData.kt index 9e7e670..f27fb7e 100644 --- a/app/src/main/java/com/shuwei/dish/match/net/RespData.kt +++ b/app/src/main/java/com/shuwei/dish/match/net/RespData.kt @@ -3,7 +3,10 @@ package com.shuwei.dish.match.net data class ApiResponse( val code: String, val msg: String? = "", - val data: T? = null + val data: T? = null, + /** 分页总条数,仅新配比秤接口返回(如 seasoning-search),旧接口为 null */ + val total: Int? = null ) { - fun isSuccess() = code == "00000" + /** 兼容旧接口成功码 "00000" 和新配比秤接口成功码 "200" */ + fun isSuccess() = code == "00000" || code == "200" } diff --git a/app/src/main/java/com/shuwei/dish/match/ui/FoodSearchActivity.kt b/app/src/main/java/com/shuwei/dish/match/ui/FoodSearchActivity.kt index ff5a747..786ee59 100644 --- a/app/src/main/java/com/shuwei/dish/match/ui/FoodSearchActivity.kt +++ b/app/src/main/java/com/shuwei/dish/match/ui/FoodSearchActivity.kt @@ -190,7 +190,9 @@ class FoodSearchActivity : BaseActivity() { return FoodRecord( foodId = foodId, foodName = dishName, - totalWeight = ((rawWeight ?: 0.0) * 1000), + totalWeight = ((cookedWeight ?: 0.0) * 1000), + foodWeight = ((cookedWeight ?: 0.0) * 1000), + count = cookedPortions, cookMode = 0, isCooking = cookStatus == 1, dinnerType = mealType.toString(), diff --git a/app/src/main/java/com/shuwei/dish/match/ui/PrepareFoodActivity.kt b/app/src/main/java/com/shuwei/dish/match/ui/PrepareFoodActivity.kt index abd0854..6b751f7 100644 --- a/app/src/main/java/com/shuwei/dish/match/ui/PrepareFoodActivity.kt +++ b/app/src/main/java/com/shuwei/dish/match/ui/PrepareFoodActivity.kt @@ -20,6 +20,7 @@ import com.shuwei.dish.match.databinding.ActivityPrepareFoodBinding import com.shuwei.dish.match.databinding.LayoutCameraPreviewBinding import com.shuwei.dish.match.dialog.CommonDialog import com.shuwei.dish.match.model.CookFoodDTO +import com.shuwei.dish.match.model.CookOrderCompositionItem import com.shuwei.dish.match.db.entity.CookFoodGoodsEntity import com.shuwei.dish.match.model.FoodRecord import com.shuwei.dish.match.model.GoodsItem @@ -53,6 +54,7 @@ class PrepareFoodActivity : BaseActivity() { companion object { const val TAG = "PrepareFoodActivity" const val FOOD_ITEM = "foodItem" + const val COOK_ORDER_ID = "cookOrderId" const val PAGE_FROM = "pageFrom" const val HOME = "home" const val WEIGHT_CHANGE_VALUE = 5 @@ -68,6 +70,9 @@ class PrepareFoodActivity : BaseActivity() { private var food: FoodRecord? = null + /** 烹制单 ID,制作模式从菜品列表传入,用于调用新配比秤接口 */ + private var cookOrderId: String? = null + /** 来源页面标识,HOME 时返回跳转 SamplingListActivity */ private var pageFrom: String? = null @@ -79,6 +84,7 @@ class PrepareFoodActivity : BaseActivity() { pageFrom = intent.getStringExtra(PAGE_FROM) //food = intent.extras?.getSerializable(FOOD_ITEM) as FoodRecord? food = IntentCompat.getSerializableExtra(intent, FOOD_ITEM, FoodRecord::class.java) + cookOrderId = intent.getStringExtra(COOK_ORDER_ID) val isSamplingMode = SpTool.cookMode == 1 val titleText = if (isSamplingMode) "菜品采集" else "菜品制作" setTitleBar(titleBarAction = { @@ -114,11 +120,17 @@ class PrepareFoodActivity : BaseActivity() { addViewClickListener() addBackKeyListener() - initCamera(binding.flCameraContainer) + // 仅采样模式启用摄像头识别,制作模式不需要 + if (isSamplingMode) { + initCamera(binding.flCameraContainer) + } initRecyclerView() initObserver() - if (food?.foodId.isNullOrBlank().not()) { + // 制作模式优先用 cookOrderId 调新接口,采样模式回退到旧 foodId + if (!cookOrderId.isNullOrBlank()) { + getCookOrderComposition(cookOrderId!!) + } else if (food?.foodId.isNullOrBlank().not()) { getDishDetail(food!!.foodId!!) } } @@ -152,13 +164,17 @@ class PrepareFoodActivity : BaseActivity() { // } // 净重 = 秤读数 - 初始读数 val netWeight = weight - initialWeight + currentWeight = netWeight binding.tvShowWeight.run { if (tag != netWeight) { text = "称重:${netWeight}g" tag = netWeight } } - recognizeFood(netWeight) + // 仅采样模式触发食材识别 + if (SpTool.cookMode == 1) { + recognizeFood(netWeight) + } } } }) @@ -332,6 +348,7 @@ class PrepareFoodActivity : BaseActivity() { startActivity { putExtra(SubmitFoodActivity.GOODS_LIST, goodsList as Serializable) putExtra(SubmitFoodActivity.FOOD_ITEM, food as Serializable) + putExtra(SubmitFoodActivity.COOK_ORDER_ID, cookOrderId) } } @@ -361,6 +378,7 @@ class PrepareFoodActivity : BaseActivity() { private fun initObserver() { + // 旧接口 observer(采样模式使用) lifecycleScope.launch { netViewModel.foodDetailState.collect { state -> when (state) { @@ -377,12 +395,35 @@ class PrepareFoodActivity : BaseActivity() { } } } + // 新配比秤接口 observer(制作模式使用) + lifecycleScope.launch { + netViewModel.cookOrderCompositionState.collect { state -> + when (state) { + is UiState.Success -> { + val items = state.data + if (items.isNullOrEmpty()) { + toast("查询菜品构成为空") + return@collect + } + loadDishComposition(items) + } + is UiState.Error -> toast(state.msg) + else -> {} + } + } + } } + /** 旧接口:通过 foodId 查询菜品构成(采样模式使用) */ private fun getDishDetail(foodId: String) { netViewModel.getFoodDetail(foodId) } + /** 新接口:通过 cookOrderId 查询菜品食材构成(制作模式使用) */ + private fun getCookOrderComposition(cookOrderId: String) { + netViewModel.getCookOrderComposition(cookOrderId) + } + private fun loadDishDetail(detail: CookFoodDTO) { // val voList = detail.stFoodInfoConstituteList @@ -402,13 +443,45 @@ class PrepareFoodActivity : BaseActivity() { firstReqSize = list.size } + /** + * 将新配比秤接口的食材构成列表映射为 GoodsItem 并刷新 UI + * 新接口天然仅返回主材(ingredientClass=1)和辅材(ingredientClass=2),不含调料 + * useWeight 保留接口返回值,不再强制置零 + */ + private fun loadDishComposition(items: List) { + val tempData = items.map { item -> + GoodsItem( + goodsId = item.materId, + goodsName = item.ingredientName, + materId = item.materId, + materialType = item.ingredientClass, + useWeight = item.useWeight, + rawMaterialsType = item.vegTypeId, + isOriginalData = true + ) + } + list.clear() + list.addAll(tempData) + materialAdapter.items = list + materialAdapter.notifyDataSetChanged() + firstReqSize = list.size + } + private var firstReqSize = 0 private val list = mutableListOf() private val materialAdapter by lazy { FoodMaterialAdapter(list).apply { - onItemClick = { positon -> + onItemClick = { position -> list.forEachIndexed { index, entity -> - entity.isClicked = index == positon + entity.isClicked = index == position + } + // 制作模式:选中食材时同步当前秤重 + if (SpTool.cookMode == 0) { + val item = list.getOrNull(position) + if (item != null && currentWeight > 0) { + item.useWeight = currentWeight + item.isSetFinished = true + } } notifyDataSetChanged() } @@ -507,13 +580,17 @@ class PrepareFoodActivity : BaseActivity() { super.onResume() pageVisible = true showRecognizePage = false - cameraUtils.bind() + if (SpTool.cookMode == 1) { + cameraUtils.bind() + } } override fun onPause() { super.onPause() pageVisible = false - cameraUtils.unbind() + if (SpTool.cookMode == 1) { + cameraUtils.unbind() + } } /** diff --git a/app/src/main/java/com/shuwei/dish/match/ui/SubmitFoodActivity.kt b/app/src/main/java/com/shuwei/dish/match/ui/SubmitFoodActivity.kt index 081e934..ae472c0 100644 --- a/app/src/main/java/com/shuwei/dish/match/ui/SubmitFoodActivity.kt +++ b/app/src/main/java/com/shuwei/dish/match/ui/SubmitFoodActivity.kt @@ -21,6 +21,8 @@ import com.shuwei.dish.match.dialog.CommonDialog import com.shuwei.dish.match.dialog.GoodsPreviewDialog import com.shuwei.dish.match.db.entity.CookFoodEntity import com.shuwei.dish.match.db.entity.CookFoodGoodsEntity +import com.shuwei.dish.match.model.CookCompleteItem +import com.shuwei.dish.match.model.CookCompleteRequest import com.shuwei.dish.match.model.FoodRecord import com.shuwei.dish.match.model.toDTO import com.shuwei.dish.match.net.UiState @@ -46,6 +48,7 @@ class SubmitFoodActivity : BaseActivity() { const val TAG = "SubmitFoodActivity" const val FOOD_ITEM = "foodItem" const val GOODS_LIST = "goodsList" + const val COOK_ORDER_ID = "cookOrderId" /** 本地临时 foodId 前缀;以此开头说明菜品尚未在服务器登记,提交时需清空让服务器分配真实 ID */ const val LOCAL_ID_PREFIX = "localId_" @@ -58,6 +61,9 @@ class SubmitFoodActivity : BaseActivity() { private var food: FoodRecord? = null private var goodsList: MutableList? = null + /** 烹制单 ID,制作模式从 PrepareFoodActivity 传入,用于新配比秤烹饪完成接口 */ + private var cookOrderId: String? = null + private val cookFoodEntity by lazy { CookFoodEntity().apply { food?.let { @@ -116,7 +122,8 @@ class SubmitFoodActivity : BaseActivity() { setHeaderBackground() intent.extras?.apply { food = getSerializable(FOOD_ITEM) as FoodRecord? - Log.d(TAG, "onCreate: cookMode=${food?.cookMode}") + cookOrderId = getString(COOK_ORDER_ID) + Log.d(TAG, "onCreate: cookMode=${food?.cookMode}, cookOrderId=$cookOrderId") // isCooking=false:从前一页面带入主辅材数据;isCooking=true:从数据库查询 if (food?.isCooking == false) { goodsList = getSerializable(GOODS_LIST) as MutableList? @@ -339,8 +346,10 @@ class SubmitFoodActivity : BaseActivity() { } binding.btnSubmit.clickWithDebounce { if (cookFoodEntity.foodWeight <= 0.toDouble()) { - toast("未识别到菜品熟重") - return@clickWithDebounce + // 测试设备无秤硬件,使用模拟熟重 500g + cookFoodEntity.foodWeight = 500.0 + binding.tvTotalWeight.text = "500.0" + binding.tvWeightUnit.text = "克" } val content = getRemindSpannable("制作完成") showRemindDialog(content) { submit() } @@ -418,12 +427,18 @@ class SubmitFoodActivity : BaseActivity() { lifecycleScope.launch { // 先移除 goodsList 中已有的调料数据(materialType==3),再以 adapter 中最新调料覆盖 goodsList?.removeAll { it.materialType == 3 } - // 主辅材 foodId 统一以菜品 foodId 为准;新加调料行的 foodId 由 buildSeasoningEntity 内部设置 goodsList?.forEach { it.foodId = cookFoodEntity.foodId } seasoningAdapter.items .filter { it.useWeight > 0.0 } .forEach { goodsList?.add(buildSeasoningEntity(it)) } + // 制作模式 + 有烹制单 ID → 调新配比秤烹饪完成接口 + if (!cookOrderId.isNullOrBlank() && food?.cookMode == 0) { + submitWithNewApi() + return@launch + } + + // 旧接口路径(采样模式 / 无烹制单 ID) cookFoodEntity.let { it.matchingConstituteInfoList = this@SubmitFoodActivity.goodsList it.dinnerType = when (it.dinnerType) { @@ -434,18 +449,74 @@ class SubmitFoodActivity : BaseActivity() { } } - // 仅本地生成的临时 foodId 需清空,由服务器分配真实 ID;服务器下发的真实 foodId 直接保留 if (cookFoodEntity.foodId.startsWith(LOCAL_ID_PREFIX)) { cookFoodEntity.foodId = "" goodsList?.forEach { it.foodId = "" } } Log.d(TAG, "submit: json=${cookFoodEntity.toJsonString()}") - // 接口提交使用 DTO,与本地 Room 实体解耦 netViewModel.submitCookFood(entity = cookFoodEntity.toDTO()) } } + /** + * 新配比秤烹饪完成接口:从 goodsList 提取主辅材和调料,组装 CookCompleteRequest 提交 + * 熟重 foodWeight(g) → cookedWeight(kg) + */ + private fun submitWithNewApi() { + val allGoods = goodsList ?: return + + // 主辅材:materialType 1(主材) 或 2(辅材) + val items = allGoods + .filter { it.materialType == 1 || it.materialType == 2 } + .map { CookCompleteItem( + materId = it.materId?.takeIf { mid -> mid.isNotEmpty() } ?: it.goodsId, + actualQty = it.useWeight ?: 0.0 + ) } + + if (items.isEmpty()) { + toast("无主辅材构成信息") + dismissLoading() + return + } + + // 调料:materialType 3 + val seasonings = allGoods + .filter { it.materialType == 3 } + .map { CookCompleteItem( + materId = it.materId?.takeIf { mid -> mid.isNotEmpty() } ?: it.goodsId, + actualQty = it.useWeight ?: 0.0 + ) } + .takeIf { it.isNotEmpty() } + + // 熟重 g → kg,保留 3 位小数 + val cookedWeightKg = (cookFoodEntity.foodWeight / 1000.0).roundedDecimalPlace(3) + + val request = CookCompleteRequest( + cookOrderId = cookOrderId ?: "", + items = items, + cookedWeight = cookedWeightKg, + seasonings = seasonings + ) + + Log.d(TAG, "submitWithNewApi: ${request.toJsonString()}") + + val isSamplingData = food?.cookMode == 1 + netViewModel.cookOrderComplete( + request = request, + onResult = { state -> + when (state) { + is UiState.Success -> submitSuccess(isSamplingData) + is UiState.Error -> { + toast(state.msg) + dismissLoading() + } + else -> {} + } + } + ) + } + /** * 将 adapter 中的调料 item 转换为 CookFoodGoodsEntity * 从本地 dm_seasoning 表补充完整字段(popularName、zjmCode、materId、goodsCode 等) diff --git a/app/src/main/java/com/shuwei/dish/match/ui/fragment/FoodListFragment.kt b/app/src/main/java/com/shuwei/dish/match/ui/fragment/FoodListFragment.kt index add30a9..086dea6 100644 --- a/app/src/main/java/com/shuwei/dish/match/ui/fragment/FoodListFragment.kt +++ b/app/src/main/java/com/shuwei/dish/match/ui/fragment/FoodListFragment.kt @@ -70,6 +70,9 @@ class FoodListFragment : BaseFragment() { /** 首次加载标记,首次请求时传 mealType=null 获取全餐次数据,后续切换传具体餐次 */ private var isFirstLoad = true + /** foodId → cookOrderId 映射,用于跳转 PrepareFoodActivity 时传递烹制单 ID */ + private val cookOrderIdMap = mutableMapOf() + /** 第一页时先查数据库存入此字段,网络结果回来后直接使用,避免时序问题 */ private var pendingLocalList: MutableList? = null @@ -93,12 +96,14 @@ class FoodListFragment : BaseFragment() { // 烹饪中,跳到称熟重页面 activity.startActivity { putExtra(SubmitFoodActivity.FOOD_ITEM, item as Serializable) + putExtra(SubmitFoodActivity.COOK_ORDER_ID, cookOrderIdMap[item.foodId]) } return } item.dinnerType = dinnerType activity.startActivity { putExtra(PrepareFoodActivity.FOOD_ITEM, item as Serializable) + putExtra(PrepareFoodActivity.COOK_ORDER_ID, cookOrderIdMap[item.foodId]) } } @@ -303,7 +308,9 @@ class FoodListFragment : BaseFragment() { return FoodRecord( foodId = foodId, foodName = dishName, - totalWeight = ((rawWeight ?: 0.0) * 1000), + totalWeight = ((cookedWeight ?: 0.0) * 1000), + foodWeight = ((cookedWeight ?: 0.0) * 1000), + count = cookedPortions, cookMode = 0, isCooking = cookStatus == 1, dinnerType = mealType.toString(), @@ -327,6 +334,13 @@ class FoodListFragment : BaseFragment() { val cookOrders = groups ?.firstOrNull { it.mealType == targetMealType } ?.cookOrders + // 更新 foodId → cookOrderId 映射(从全量分组构建,跨餐次) + groups?.forEach { group -> + group.cookOrders.forEach { order -> + cookOrderIdMap[order.foodId] = order.cookOrderId + } + } + if (cookOrders.isNullOrEmpty() && localList.isNullOrEmpty()) { loadEmptyView() finishRefresh()