From 6d3c5490a406595087a6782498a5a7eabaab2963 Mon Sep 17 00:00:00 2001 From: mazengfei <331023091@qq.com> Date: Thu, 10 Sep 2026 13:41:35 +0800 Subject: [PATCH] =?UTF-8?q?feat(food-prepare):=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E9=A3=9F=E6=9D=90=E4=BF=A1=E6=81=AF=E5=BD=95=E5=85=A5=E5=8D=A1?= =?UTF-8?q?=E5=8F=8A=E7=9B=B8=E5=85=B3=E6=8E=A5=E5=8F=A3=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增手动录入食材信息的UI布局,包含食材名称搜索、主辅材选择及用量输入 - 整合采样模式下的食材搜索弹窗,支持食材模糊查询与净材种类选择 - 新增相关网络接口调用,实现食材搜索、菜品搜索、菜品构成查询及采样历史等功能 - 优化食材列表展示,支持采样历史审核状态和熟重显示,增加颜色区分 - 修改PrepareFoodActivity,采样模式启用手动新增食材功能,包含添加、重置操作 - 调整Adapter和ViewModel,兼容新接口数据模型,支持分页加载和状态回调 - 修正接口路径和网络请求地址,统一使用nutrition/neglect路径前缀 - 删除冗余代码和未使用的旧接口代码,简化搜索逻辑及界面响应机制 --- .../0/菜品配比终端v1.0-release.dm | Bin 3229 -> 0 bytes .../1/菜品配比终端v1.0-release.dm | Bin 3170 -> 0 bytes .../dish/match/adapter/FoodListAdapter.kt | 24 + .../dish/match/dialog/FoodSearchDialog.kt | 84 +-- .../com/shuwei/dish/match/model/FoodRecord.kt | 11 +- .../com/shuwei/dish/match/model/GoodsItem.kt | 2 + .../shuwei/dish/match/model/SampleModels.kt | 271 ++++++++ .../com/shuwei/dish/match/net/ApiService.kt | 85 ++- .../com/shuwei/dish/match/net/NetViewModel.kt | 139 ++++ .../shuwei/dish/match/net/RemoteRepository.kt | 39 ++ .../dish/match/scale/ScaleDeviceConfig.kt | 2 +- .../dish/match/ui/FoodSearchActivity.kt | 130 ++-- .../dish/match/ui/PrepareFoodActivity.kt | 193 ++++-- .../dish/match/ui/SamplingModeActivity.kt | 29 +- .../dish/match/ui/SubmitFoodActivity.kt | 80 ++- .../match/ui/fragment/FoodListFragment.kt | 5 +- .../main/res/layout/activity_prepare_food.xml | 172 +++++ app/src/main/res/values/strings.xml | 4 + 配比秤-制作菜品-API文档.md | 173 +++-- 配比秤-采样模式-API文档.md | 611 ++++++++++++++++++ 20 files changed, 1755 insertions(+), 299 deletions(-) delete mode 100644 app/release/baselineProfiles/0/菜品配比终端v1.0-release.dm delete mode 100644 app/release/baselineProfiles/1/菜品配比终端v1.0-release.dm create mode 100644 app/src/main/java/com/shuwei/dish/match/model/SampleModels.kt create mode 100644 配比秤-采样模式-API文档.md diff --git a/app/release/baselineProfiles/0/菜品配比终端v1.0-release.dm b/app/release/baselineProfiles/0/菜品配比终端v1.0-release.dm deleted file mode 100644 index b3008e96ead486fc56810ea79aaf26c26b39c2e4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3229 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 AD*ylh diff --git a/app/src/main/java/com/shuwei/dish/match/adapter/FoodListAdapter.kt b/app/src/main/java/com/shuwei/dish/match/adapter/FoodListAdapter.kt index 3c1bc34..ff5eb84 100644 --- a/app/src/main/java/com/shuwei/dish/match/adapter/FoodListAdapter.kt +++ b/app/src/main/java/com/shuwei/dish/match/adapter/FoodListAdapter.kt @@ -88,6 +88,14 @@ class FoodListAdapter( // 非烹饪中,采样模式 tvDishName.setTextColor(ContextCompat.getColor(holder.itemView.context, R.color.dish_green)) clBlock.setBackgroundResource(R.drawable.shape_white_fb_15_corners) + // 新采样历史(sample/history):展示审核状态 + 熟重(g) + val auditName = item.auditStatusName + if (!auditName.isNullOrBlank()) { + tvDishCount.text = "熟重:${formatGram(foodWeight)}" + tvShowState.text = auditName + tvShowState.setTextColor(auditStatusColor(holder, item.auditStatus)) + return@run + } val realTotalWeight = if (totalWeight > 0.0) totalWeight else foodWeight val showTotalWeight = if (realTotalWeight == 0.0) "-" else df.format(realTotalWeight / 1000.0F) tvDishCount.text = "累计统计:${showTotalWeight}kg(${item.count ?: "-"}次)" @@ -96,5 +104,21 @@ class FoodListAdapter( } } + /** 熟重(g)格式化,整数不带小数,小数保留 2 位 */ + private fun formatGram(weight: Double): String { + val gramDf = DecimalFormat("0.##") + return "${gramDf.format(weight)}g" + } + + /** 审核状态着色:1=待审核(灰) / 2=已通过(绿) / 3=已驳回(红) */ + private fun auditStatusColor(holder: VH, status: Int?): Int { + val context = holder.itemView.context + return when (status) { + 2 -> ContextCompat.getColor(context, R.color.dish_green) + 3 -> ContextCompat.getColor(context, R.color.red_ff4444) + else -> ContextCompat.getColor(context, R.color.black999) + } + } + private val df = DecimalFormat("0.000") } diff --git a/app/src/main/java/com/shuwei/dish/match/dialog/FoodSearchDialog.kt b/app/src/main/java/com/shuwei/dish/match/dialog/FoodSearchDialog.kt index 59ae253..0f3fc88 100644 --- a/app/src/main/java/com/shuwei/dish/match/dialog/FoodSearchDialog.kt +++ b/app/src/main/java/com/shuwei/dish/match/dialog/FoodSearchDialog.kt @@ -11,9 +11,6 @@ import android.view.View import android.view.Window import android.widget.FrameLayout import androidx.core.widget.addTextChangedListener -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager import com.chad.library.adapter4.util.setOnDebouncedItemClick @@ -24,6 +21,7 @@ import com.shuwei.dish.match.base.BaseActivity import com.shuwei.dish.match.databinding.DialogFoodSearchBinding import com.shuwei.dish.match.databinding.LayoutEmptyViewBinding import com.shuwei.dish.match.model.GoodsItem +import com.shuwei.dish.match.model.MaterSearchItem import com.shuwei.dish.match.net.UiState import com.shuwei.dish.match.utils.KeyboardUtil import com.shuwei.dish.match.utils.showRawMaterialsDialog @@ -54,15 +52,23 @@ class FoodSearchDialog( isStateViewEnable = true setOnDebouncedItemClick { _, _, position -> val item = list.getOrNull(position) ?: return@setOnDebouncedItemClick - // 弹出净材种类单选弹窗,确认后再回调并关闭弹窗 + item.isClicked = true + notifyItemChanged(position) + // mater-search 不返回净材种类,rawMaterialsTypes 为空时直接选中返回,避免卡在净材弹窗 + if (item.rawMaterialsTypes.isNullOrBlank()) { + binding.root.postDelayed({ + onItemSelected(item) + dismiss() + }, 300) + return@setOnDebouncedItemClick + } + // 有净材种类时弹出单选弹窗,确认后再回调并关闭弹窗 showRawMaterialsDialog( context = activity, types = item.rawMaterialsTypes, checkedType = item.rawMaterialsType ) { selectedType -> item.rawMaterialsType = selectedType - item.isClicked = true - notifyItemChanged(position) binding.root.postDelayed({ onItemSelected(item) dismiss() @@ -121,8 +127,6 @@ class FoodSearchDialog( binding.ivSearch.setOnClickListener { searchGoods(it) } binding.root.setOnClickListener { KeyboardUtil.hideKeyboard(it) } - initObserver() - // 若有默认食材名称,自动填充并触发搜索 //if (!defGoodsName.isNullOrBlank()) { //binding.etSheetInput.setText(defGoodsName) @@ -170,27 +174,6 @@ class FoodSearchDialog( } } - /** - * 收集 foodSearchGoodsListState,统一处理 Loading / Success / Error 状态 - * 使用独立的 State,避免与 FoodRecognizeActivity 等页面共用 goodsListState 产生干扰 - */ - private fun initObserver() { - activity.lifecycleScope.launch { - activity.repeatOnLifecycle(Lifecycle.State.STARTED) { - activity.netViewModel.foodSearchGoodsListState.collect { state -> - when (state) { - is UiState.Success -> loadGoodsList(state.data) - is UiState.Error -> { - activity.toast(state.msg) - finishRefresh() - } - else -> {} - } - } - } - } - } - /** * 触发搜索:校验输入、隐藏键盘、发起请求 */ @@ -205,14 +188,24 @@ class FoodSearchDialog( } /** - * 请求食材列表,使用弹窗专用接口,结果写入 foodSearchGoodsListState + * 请求食材列表(采样模式新配比秤接口 mater-search,回调版本) */ private fun getGoodsList() { - activity.netViewModel.queryFoodSearchGoodsList( - goodsType = "0", + activity.netViewModel.materSearch( + 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 -> {} + } + } ) } @@ -220,7 +213,7 @@ class FoodSearchDialog( * 将返回数据填充到列表 */ @SuppressLint("NotifyDataSetChanged") - private fun loadGoodsList(records: MutableList?) { + private fun loadGoodsList(records: List?) { finishRefresh() if (records.isNullOrEmpty()) { if (pageNo == 1) { @@ -235,20 +228,27 @@ class FoodSearchDialog( } } if (pageNo == 1) list.clear() - // 将接口返回的 rawMaterialsType(全部种类)移存到 rawMaterialsTypes,供弹窗使用 - records.forEach { - it.rawMaterialsTypes = it.rawMaterialsType - it.rawMaterialsType = null - it.oldGoodsId = it.goodsId - it.goodsId = it.materId ?: "" - } - list.addAll(records) + // mater-search 不返回净材种类,rawMaterialsTypes 保持 null,点击时直接选中返回 + val goodsItems = records.map { it.toGoodsItem() } + list.addAll(goodsItems) adapter.notifyDataSetChanged() val isLoadMoreEnable = records.size >= PAGE_SIZE binding.refreshLayout.setEnableLoadMore(isLoadMoreEnable) if (isLoadMoreEnable) pageNo++ } + /** + * MaterSearchItem → GoodsItem 映射 + * id 即食材id,同时作为 goodsId 和 materId(提交 sample/save 时使用) + */ + private fun MaterSearchItem.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/FoodRecord.kt b/app/src/main/java/com/shuwei/dish/match/model/FoodRecord.kt index 8882dd0..06294b1 100644 --- a/app/src/main/java/com/shuwei/dish/match/model/FoodRecord.kt +++ b/app/src/main/java/com/shuwei/dish/match/model/FoodRecord.kt @@ -15,5 +15,14 @@ data class FoodRecord( var dinnerType: String = "0", //true-原始数据,false-非原始数据 var isOriginalData: Boolean = true, - var sort: Int = 0 + var sort: Int = 0, + // ===== 采样历史(sample/history)扩展字段,制作模式不涉及 ===== + /** 菜品编号 */ + var foodCode: String? = null, + /** 审核状态: 1=待审核 / 2=已通过 / 3=已驳回 */ + var auditStatus: Int? = null, + /** 审核状态中文 */ + var auditStatusName: String? = null, + /** 审核记录id */ + var auditRecordId: String? = null ) : Serializable diff --git a/app/src/main/java/com/shuwei/dish/match/model/GoodsItem.kt b/app/src/main/java/com/shuwei/dish/match/model/GoodsItem.kt index 05eda21..8471283 100644 --- a/app/src/main/java/com/shuwei/dish/match/model/GoodsItem.kt +++ b/app/src/main/java/com/shuwei/dish/match/model/GoodsItem.kt @@ -32,6 +32,8 @@ data class GoodsItem( var isClicked: Boolean = false, // true-原始数据,false-非原始数据(识图新增) var isOriginalData: Boolean = true, + // true-用户手动指定了主辅材,updateMaterialTypes 自动分配时跳过 + var isMaterialTypeManual: Boolean = false, // 识图匹配分数,-1 表示不显示 var foodScore: Int = -1, // 净材种类(当前选中值) diff --git a/app/src/main/java/com/shuwei/dish/match/model/SampleModels.kt b/app/src/main/java/com/shuwei/dish/match/model/SampleModels.kt new file mode 100644 index 0000000..1fe5e86 --- /dev/null +++ b/app/src/main/java/com/shuwei/dish/match/model/SampleModels.kt @@ -0,0 +1,271 @@ +package com.shuwei.dish.match.model + +/** + * 采样与制作模式相关模型,对应配比秤接口(Controller: NutRatioScaleController) + * 路径前缀: /neglect/ratio-scale + * 序列化规则: Long 字段服务端返回字符串;请求体字段按后端约定(id 类字段用 String 与现有代码一致) + */ + +/** 提交构成接口 cookMode 取值(与本地 cookMode 0=制作/1=采样 不同,注意映射) */ +object ConstituteCookMode { + /** 制作模式(基于已有菜品,foodId 必填) */ + const val MAKE = 1 + /** 采样模式(从零建菜,foodName 必填) */ + const val SAMPLE = 2 +} + +/** 采样菜品审核状态 */ +object SampleAuditStatus { + /** 待审核 */ + const val PENDING = 1 + /** 已通过 */ + const val APPROVED = 2 + /** 已驳回 */ + const val REJECTED = 3 +} + +// ==================== 一、食材模糊搜索 ==================== + +/** + * 食材模糊搜索请求参数 + * 对应接口: POST /neglect/ratio-scale/mater-search + * 数据源: nut_mater_base,限定设备对应食堂 + */ +data class MaterSearchRequest( + /** 模糊匹配食材名称和别名,为空时返回全部 */ + val keyword: String? = null, + /** 页码 */ + val pageNum: Int = 1, + /** 每页条数 */ + val pageSize: Int = 20 +) + +/** + * 食材模糊搜索结果条目 + * 注意: id / canteenId 为 Long 类型,服务端序列化为 String + */ +data class MaterSearchItem( + /** 食材id(Long → String,提交构成 items.materId 使用) */ + val id: String = "", + /** 食材名称 */ + val materName: String = "", + /** 食材编码(一级+二级+自编码,全局唯一) */ + val materCode: String = "", + /** 别名,可能为 null */ + val alias: String? = null, + /** 食材一级分类 */ + val materFirstClass: String? = null, + /** 食材二级分类 */ + val materSecondClass: String? = null, + /** 食材图片url */ + val materUrl: String? = null, + /** 归属食堂id(Long → String) */ + val canteenId: String = "" +) + +// ==================== 二、搜菜品 ==================== + +/** + * 搜菜品请求参数(制作模式选已有菜品) + * 对应接口: POST /neglect/ratio-scale/food-search + * 数据源: nut_food(status=1 上架) + */ +data class FoodSearchRequest( + /** 菜品名称或编号模糊搜索 */ + val keyword: String? = null, + /** 页码 */ + val pageNum: Int = 1, + /** 每页条数 */ + val pageSize: Int = 20 +) + +/** + * 搜菜品结果条目 + * 注意: foodId 为 Long 类型,服务端序列化为 String + */ +data class FoodSearchItem( + /** 菜品id(Long → String) */ + val foodId: String = "", + /** 菜品编号 */ + val foodCode: String = "", + /** 菜品名称 */ + val foodName: String = "" +) + +// ==================== 三、查菜品构成 ==================== + +/** + * 查菜品构成响应 — 单个食材 + * 对应接口: GET /neglect/ratio-scale/food/{foodId}/composition + */ +data class FoodCompositionIngredient( + /** 食材id(Long → String) */ + val materId: String = "", + /** 食材名称 */ + val materName: String = "", + /** 用量(g) */ + val useWeight: Double = 0.0, + /** 1=主材 / 2=辅材 / 3=调料 */ + val isMain: Int = 0, + /** 分类中文名 */ + val isMainName: String = "" +) + +/** + * 查菜品构成响应(主辅料+调料 + 熟重) + * 对应接口: GET /neglect/ratio-scale/food/{foodId}/composition + */ +data class FoodCompositionResponse( + /** 菜品id(Long → String) */ + val foodId: String = "", + /** 菜品名称 */ + val foodName: String = "", + /** 熟重(g),从 nut_food_cook.output_weight 反查 */ + val foodWeight: Double = 0.0, + /** 食材构成列表 */ + val ingredients: List = emptyList() +) + +// ==================== 四、提交构成 ==================== + +/** + * 提交构成 — 单个食材构成条目 + * 对应接口: POST /neglect/ratio-scale/constitute/save + */ +data class ConstituteSaveItem( + /** 食材id(nut_mater_base.id) */ + val materId: String = "", + /** 食材称量用量(g),必须 > 0 */ + val useWeight: Double = 0.0, + /** 食材分类: 1=主材 / 2=辅材 / 3=调料 */ + val isMain: Int = 0 +) + +/** + * 提交构成请求,cookMode 区分采样/制作 + * 对应接口: POST /neglect/ratio-scale/constitute/save + */ +data class ConstituteSaveRequest( + /** 1=制作模式 / 2=采样模式 */ + val cookMode: Int = ConstituteCookMode.SAMPLE, + /** 制作模式必填 — 已有菜品id */ + val foodId: String? = null, + /** 采样模式必填 — 菜品名称 */ + val foodName: String? = null, + /** 熟重(g),必须 > 0 */ + val foodWeight: Double = 0.0, + /** 餐次: 1=早餐/2=午餐/3=晚餐/4=加餐,选填 */ + val mealType: Int? = null, + /** 食材构成列表,至少 1 条 */ + val items: List = emptyList() +) + +// ==================== 五、采样历史 ==================== + +/** + * 采样历史查询请求参数 + * 对应接口: POST /neglect/ratio-scale/sample/history + */ +data class SampleHistoryRequest( + /** 页码 */ + val pageNum: Int = 1, + /** 每页条数 */ + val pageSize: Int = 10, + /** 菜品名称模糊搜索 */ + val keyword: String? = null, + /** 审核状态筛选: 1=待审核 / 2=已通过 / 3=已驳回,不传查全部 */ + val auditStatus: Int? = null, + /** 采样日期 yyyy-MM-dd,不传默认当天 */ + val sampleDate: String? = null +) + +/** + * 采样历史结果条目 + * 注意: foodId / auditRecordId 为 Long 类型,服务端序列化为 String + */ +data class SampleHistoryItem( + /** 菜品id(Long → String) */ + val foodId: String = "", + /** 菜品编号 */ + val foodCode: String = "", + /** 菜品名称 */ + val foodName: String = "", + /** 熟重(g) */ + val foodWeight: Double = 0.0, + /** 审核状态: 1=待审核 / 2=已通过 / 3=已驳回 */ + val auditStatus: Int = 0, + /** 审核状态中文 */ + val auditStatusName: String = "", + /** 审核记录id(Long → String) */ + val auditRecordId: String = "", + /** 提交时间 */ + val createTime: String = "" +) + +// ==================== 六、制作历史 ==================== + +/** + * 制作历史查询请求参数 + * 对应接口: POST /neglect/ratio-scale/make/history + * 数据源: nut_food_matching_use,按设备过滤 + */ +data class MakeHistoryRequest( + /** 页码 */ + val pageNum: Int = 1, + /** 每页条数 */ + val pageSize: Int = 10, + /** 制作日期 yyyy-MM-dd,不传查全部 */ + val day: String? = null +) + +/** + * 制作历史结果条目 + * 注意: id / foodId 为 Long 类型,服务端序列化为 String + */ +data class MakeHistoryItem( + /** 制作记录id(Long → String) */ + val id: String = "", + /** 菜品id(Long → String) */ + val foodId: String = "", + /** 菜品名称 */ + val foodName: String = "", + /** 熟重(g) */ + val foodWeight: Double = 0.0, + /** 餐次: 1=早餐/2=午餐/3=晚餐/4=加餐 */ + val mealType: Int = 0, + /** 餐次中文 */ + val mealTypeName: String = "", + /** 制作日期 */ + val day: String = "", + /** 制作时间 */ + val createTime: String = "" +) + +// ==================== 七、制作历史图表 ==================== + +/** + * 制作历史图表 — 横轴项 + * 对应接口: GET /neglect/ratio-scale/make/history/{foodId} + */ +data class MakeHistoryChartAxis( + /** 食材id(时间/熟重项为空字符串) */ + val goodId: String = "", + /** 项名称(时间/熟重/食材名称) */ + val goodName: String = "" +) + +/** + * 制作历史图表 — 纵轴值 + * value 为 String(时间项) 或 Double(数值项) + */ +data class MakeHistoryChartValue( + val value: Any? = null +) + +/** + * 制作历史图表 — 单条记录(xAxes 与 yAxes 一一对应) + */ +data class MakeHistoryChartRow( + val xAxes: List = emptyList(), + val yAxes: List = emptyList() +) 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 29b9067..7c72b9d 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 @@ -7,7 +7,18 @@ 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.ConstituteSaveRequest import com.shuwei.dish.match.model.CookCompleteRequest +import com.shuwei.dish.match.model.FoodCompositionResponse +import com.shuwei.dish.match.model.FoodSearchItem +import com.shuwei.dish.match.model.FoodSearchRequest +import com.shuwei.dish.match.model.MakeHistoryChartRow +import com.shuwei.dish.match.model.MakeHistoryItem +import com.shuwei.dish.match.model.MakeHistoryRequest +import com.shuwei.dish.match.model.MaterSearchItem +import com.shuwei.dish.match.model.MaterSearchRequest +import com.shuwei.dish.match.model.SampleHistoryItem +import com.shuwei.dish.match.model.SampleHistoryRequest import com.shuwei.dish.match.model.SeasoningSearchItem import com.shuwei.dish.match.model.SeasoningSearchRequest import okhttp3.MultipartBody @@ -29,7 +40,7 @@ interface ApiService { */ @GET suspend fun getFoodDetail( - @Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/matching/app/getConstituteByFoodId", + @Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/matching/app/getConstituteByFoodId", @Query("foodId") foodId: String ): ApiResponse @@ -39,7 +50,7 @@ interface ApiService { */ @POST suspend fun submitCookFood( - @Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/matching/app/saveConstitute", + @Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/matching/app/saveConstitute", @Body param: CookFoodDTO ): ApiResponse @@ -48,7 +59,7 @@ interface ApiService { */ @POST suspend fun searchFoodList( - @Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/matching/app/queryPageList", + @Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/matching/app/queryPageList", @Body param: MutableMap ): ApiResponse?> @@ -57,7 +68,7 @@ interface ApiService { */ @POST suspend fun getSamplingList( - @Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/matching/app/queryHistoryGoodsInfoList", + @Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/matching/app/queryHistoryGoodsInfoList", @Body param: MutableMap ): ApiResponse?> @@ -66,7 +77,7 @@ interface ApiService { */ @POST suspend fun queryGoodsList( - @Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/matching/app/queryGoodsInfoList", + @Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/matching/app/queryGoodsInfoList", @Body param: MutableMap ): ApiResponse?> @@ -76,7 +87,7 @@ interface ApiService { @Multipart @POST suspend fun uploadFoodVectorData( - @Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/matching/app/addWeighingCollectionData", + @Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/matching/app/addWeighingCollectionData", @PartMap params: MutableMap, @Part foodPics: List ): ApiResponse?> @@ -120,4 +131,66 @@ interface ApiService { @Body request: CookCompleteRequest ): ApiResponse + /** + * 食材模糊搜索(采样/制作通用,称量前检索食材) + * 数据源: nut_mater_base,限定设备对应食堂,模糊匹配名称和别名 + */ + @POST + suspend fun materSearch( + @Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/ratio-scale/mater-search", + @Body request: MaterSearchRequest + ): ApiResponse?> + + /** + * 搜菜品(制作模式选已有上架菜品) + */ + @POST + suspend fun foodSearch( + @Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/ratio-scale/food-search", + @Body request: FoodSearchRequest + ): ApiResponse?> + + /** + * 查菜品构成(制作模式查看已有菜品主辅料+调料 + 熟重) + */ + @GET("nutrition/neglect/ratio-scale/food/{foodId}/composition") + suspend fun getFoodComposition( + @Path("foodId") foodId: String + ): ApiResponse + + /** + * 提交构成(cookMode 区分采样/制作) + */ + @POST + suspend fun constituteSave( + @Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/ratio-scale/constitute/save", + @Body request: ConstituteSaveRequest + ): ApiResponse + + /** + * 采样历史(本食堂,默认当天) + */ + @POST + suspend fun sampleHistory( + @Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/ratio-scale/sample/history", + @Body request: SampleHistoryRequest + ): ApiResponse?> + + /** + * 制作历史(本设备,按日期可选过滤) + */ + @POST + suspend fun makeHistory( + @Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/ratio-scale/make/history", + @Body request: MakeHistoryRequest + ): ApiResponse?> + + /** + * 制作历史图表(按菜品id) + */ + @GET("nutrition/neglect/ratio-scale/make/history/{foodId}") + suspend fun makeHistoryChart( + @Path("foodId") foodId: String + ): 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 9d147ed..e96c2a3 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 @@ -10,7 +10,18 @@ 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.ConstituteSaveRequest import com.shuwei.dish.match.model.CookCompleteRequest +import com.shuwei.dish.match.model.FoodCompositionResponse +import com.shuwei.dish.match.model.FoodSearchItem +import com.shuwei.dish.match.model.FoodSearchRequest +import com.shuwei.dish.match.model.MakeHistoryChartRow +import com.shuwei.dish.match.model.MakeHistoryItem +import com.shuwei.dish.match.model.MakeHistoryRequest +import com.shuwei.dish.match.model.MaterSearchItem +import com.shuwei.dish.match.model.MaterSearchRequest +import com.shuwei.dish.match.model.SampleHistoryItem +import com.shuwei.dish.match.model.SampleHistoryRequest import com.shuwei.dish.match.model.SeasoningSearchItem import com.shuwei.dish.match.model.SeasoningSearchRequest import kotlinx.coroutines.flow.MutableStateFlow @@ -313,6 +324,134 @@ class NetViewModel( } } + /** + * 食材模糊搜索(回调版本) + * @param keyword 模糊匹配食材名称和别名,为 null 时不传该字段 + * @param pageNum 页码,默认第 1 页 + * @param pageSize 每页条数,默认 20 + */ + fun materSearch( + keyword: String? = null, + pageNum: Int = 1, + pageSize: Int = 20, + onLoading: () -> Unit = {}, + onResult: (UiState?>) -> Unit + ) { + viewModelScope.launch { + onLoading() + onResult(repository.materSearch( + MaterSearchRequest(keyword = keyword, pageNum = pageNum, pageSize = pageSize) + )) + } + } + + /** + * 搜菜品(回调版本,制作模式选已有上架菜品) + */ + fun foodSearch( + keyword: String? = null, + pageNum: Int = 1, + pageSize: Int = 20, + onLoading: () -> Unit = {}, + onResult: (UiState?>) -> Unit + ) { + viewModelScope.launch { + onLoading() + onResult(repository.foodSearch( + FoodSearchRequest(keyword = keyword, pageNum = pageNum, pageSize = pageSize) + )) + } + } + + /** + * 查菜品构成(回调版本,制作模式) + */ + fun getFoodComposition( + foodId: String, + onLoading: () -> Unit = {}, + onResult: (UiState) -> Unit + ) { + viewModelScope.launch { + onLoading() + onResult(repository.getFoodComposition(foodId)) + } + } + + /** + * 提交构成(回调版本,cookMode 区分采样/制作) + */ + fun constituteSave( + request: ConstituteSaveRequest, + onLoading: () -> Unit = {}, + onResult: (UiState) -> Unit + ) { + viewModelScope.launch { + onLoading() + onResult(repository.constituteSave(request)) + } + } + + /** + * 采样历史(回调版本) + * @param keyword 菜品名称模糊搜索,为 null 时不传该字段 + * @param auditStatus 审核状态筛选,为 null 时不传该字段 + * @param sampleDate 采样日期 yyyy-MM-dd,为 null 时不传该字段 + */ + fun sampleHistory( + pageNum: Int = 1, + pageSize: Int = 10, + keyword: String? = null, + auditStatus: Int? = null, + sampleDate: String? = null, + onLoading: () -> Unit = {}, + onResult: (UiState?>) -> Unit + ) { + viewModelScope.launch { + onLoading() + onResult(repository.sampleHistory( + SampleHistoryRequest( + pageNum = pageNum, + pageSize = pageSize, + keyword = keyword, + auditStatus = auditStatus, + sampleDate = sampleDate + ) + )) + } + } + + /** + * 制作历史(回调版本,本设备按日期可选过滤) + */ + fun makeHistory( + pageNum: Int = 1, + pageSize: Int = 10, + day: String? = null, + onLoading: () -> Unit = {}, + onResult: (UiState?>) -> Unit + ) { + viewModelScope.launch { + onLoading() + onResult(repository.makeHistory( + MakeHistoryRequest(pageNum = pageNum, pageSize = pageSize, day = day) + )) + } + } + + /** + * 制作历史图表(回调版本,按菜品id) + */ + fun makeHistoryChart( + foodId: String, + onLoading: () -> Unit = {}, + onResult: (UiState?>) -> Unit + ) { + viewModelScope.launch { + onLoading() + onResult(repository.makeHistoryChart(foodId)) + } + } + /** * 上传采集菜品信息 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 c0a0eaa..d315912 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 @@ -6,7 +6,18 @@ 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.ConstituteSaveRequest import com.shuwei.dish.match.model.CookCompleteRequest +import com.shuwei.dish.match.model.FoodCompositionResponse +import com.shuwei.dish.match.model.FoodSearchItem +import com.shuwei.dish.match.model.FoodSearchRequest +import com.shuwei.dish.match.model.MakeHistoryChartRow +import com.shuwei.dish.match.model.MakeHistoryItem +import com.shuwei.dish.match.model.MakeHistoryRequest +import com.shuwei.dish.match.model.MaterSearchItem +import com.shuwei.dish.match.model.MaterSearchRequest +import com.shuwei.dish.match.model.SampleHistoryItem +import com.shuwei.dish.match.model.SampleHistoryRequest import com.shuwei.dish.match.model.SeasoningSearchItem import com.shuwei.dish.match.model.SeasoningSearchRequest import okhttp3.MultipartBody @@ -65,6 +76,34 @@ class RemoteRepository { suspend fun cookOrderComplete(request: CookCompleteRequest): UiState = safeApiCall { apiService.cookOrderComplete(request = request) } + /** 食材模糊搜索 */ + suspend fun materSearch(request: MaterSearchRequest): UiState?> = + safeApiCall { apiService.materSearch(request = request) } + + /** 搜菜品(制作模式选已有上架菜品) */ + suspend fun foodSearch(request: FoodSearchRequest): UiState?> = + safeApiCall { apiService.foodSearch(request = request) } + + /** 查菜品构成(制作模式) */ + suspend fun getFoodComposition(foodId: String): UiState = + safeApiCall { apiService.getFoodComposition(foodId = foodId) } + + /** 提交构成(cookMode 区分采样/制作) */ + suspend fun constituteSave(request: ConstituteSaveRequest): UiState = + safeApiCall { apiService.constituteSave(request = request) } + + /** 采样历史(本食堂,默认当天) */ + suspend fun sampleHistory(request: SampleHistoryRequest): UiState?> = + safeApiCall { apiService.sampleHistory(request = request) } + + /** 制作历史(本设备,按日期可选过滤) */ + suspend fun makeHistory(request: MakeHistoryRequest): UiState?> = + safeApiCall { apiService.makeHistory(request = request) } + + /** 制作历史图表(按菜品id) */ + suspend fun makeHistoryChart(foodId: String): UiState?> = + safeApiCall { apiService.makeHistoryChart(foodId = foodId) } + /** 上传采集数据 */ suspend fun uploadFoodVectorData( params: MutableMap, diff --git a/app/src/main/java/com/shuwei/dish/match/scale/ScaleDeviceConfig.kt b/app/src/main/java/com/shuwei/dish/match/scale/ScaleDeviceConfig.kt index 16f57a2..8c42655 100644 --- a/app/src/main/java/com/shuwei/dish/match/scale/ScaleDeviceConfig.kt +++ b/app/src/main/java/com/shuwei/dish/match/scale/ScaleDeviceConfig.kt @@ -6,7 +6,7 @@ package com.shuwei.dish.match.scale */ object ScaleDeviceConfig { - const val DEVICE_ID_2 = "8fc2ab34-2137-3112-acca-f884ea8736d4"//测试0bd74d78-c221-3182-b5ee-55d86dd79283 正式8fc2ab34-2137-3112-acca-f884ea8736d4 + const val DEVICE_ID_2 = "0bd74d78-c221-3182-b5ee-55d86dd79283"//测试0bd74d78-c221-3182-b5ee-55d86dd79283 正式8fc2ab34-2137-3112-acca-f884ea8736d4 const val DEVICE_ID_22 = "a46fa55c-113c-3511-bb1f-41e5eff77c4b" const val DEVICE_ID_18 = "1038da9f-c6eb-326e-a1d9-d6d3af978b22" const val DEVICE_ID_1 = "7cc0f6ea-f13d-3013-a867-fc998eb554ac" 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 786ee59..746ea23 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 @@ -4,34 +4,26 @@ import android.annotation.SuppressLint import android.content.Intent import android.os.Bundle import android.view.LayoutInflater -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.LinearLayoutManager import com.shuwei.dish.match.R import com.shuwei.dish.match.adapter.Food2Adapter import com.shuwei.dish.match.base.BaseActivity -import com.shuwei.dish.match.base.BaseApp import com.shuwei.dish.match.databinding.ActivityFoodSearchBinding import com.shuwei.dish.match.databinding.LayoutEmptyViewBinding -import com.shuwei.dish.match.model.CookOrderItem -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.FoodSearchItem import com.shuwei.dish.match.net.UiState import com.shuwei.dish.match.utils.KeyboardUtil -import com.shuwei.dish.match.utils.SpTool import com.shuwei.dish.match.utils.ext.addOnActionSearchListener import com.shuwei.dish.match.utils.ext.gone -import com.shuwei.dish.match.utils.ext.startActivity import com.shuwei.dish.match.utils.ext.toast import com.shuwei.dish.match.utils.ext.visible import java.io.Serializable -import kotlinx.coroutines.launch import kotlin.collections.getOrNull /** - * 菜品搜索页面 + * 菜品搜索页面(采样模式:搜索已有上架菜品作为称量模板) + * 数据源: POST /neglect/ratio-scale/food-search */ class FoodSearchActivity : BaseActivity() { @@ -40,25 +32,16 @@ class FoodSearchActivity : BaseActivity() { const val FOOD_ITEM = "foodItem" const val FOOD_ID = "foodId" const val FOOD_NAME = "foodName" - const val DINNER_TYPE = "dinnerType" } private lateinit var binding: ActivityFoodSearchBinding private var list: MutableList = mutableListOf() - private var dinnerType = "1" private val recordAdapter by lazy { Food2Adapter(list).apply { isStateViewEnable = true setOnItemClickListener { adapter, view, position -> - var jumpItem = list.getOrNull(position) ?: return@setOnItemClickListener - if (SpTool.cookMode == 0) { - jumpItem.dinnerType = dinnerType - startActivity { - putExtra(PrepareFoodActivity.FOOD_ITEM, jumpItem as Serializable) - } - return@setOnItemClickListener - } + val jumpItem = list.getOrNull(position) ?: return@setOnItemClickListener setResult(RESULT_OK, Intent().apply { putExtra(FOOD_ITEM, jumpItem as Serializable) }) @@ -73,7 +56,6 @@ class FoodSearchActivity : BaseActivity() { super.onCreate(savedInstanceState) binding = ActivityFoodSearchBinding.inflate(layoutInflater) setContentView(binding.root) - dinnerType = intent.getStringExtra(DINNER_TYPE) ?: "0" setHeaderBackground() foodName = intent.getStringExtra(FOOD_NAME) binding.etInputDish.setText(foodName) @@ -94,31 +76,6 @@ class FoodSearchActivity : BaseActivity() { adapter = recordAdapter } addViewListener() - initObserver() - } - - /** - * 收集 cookOrderPageState,统一处理 Loading / Success / Error 状态 - */ - private fun initObserver() { - lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - netViewModel.cookOrderPageState.collect { state -> - when (state) { - is UiState.Loading -> showLoading() - is UiState.Success -> handleSearchResult(state.data) - is UiState.Error -> { - delayDismissLoading() - finishRefresh() - binding.refreshLayout.setEnableRefresh(true) - toast(state.msg) - loadEmptyView() - } - is UiState.Idle -> {} - } - } - } - } } private fun addViewListener() { @@ -164,63 +121,62 @@ class FoodSearchActivity : BaseActivity() { } private var pageNo = 1 - private var pageSize = 10 - fun getDinnerTypeText(): String { - return when (dinnerType) { - "1" -> "早餐" - "2" -> "午餐" - "3" -> "晚餐" - else -> "早餐" - } - } + private var pageSize = 20 @SuppressLint("NotifyDataSetChanged") private fun queryListInfo(input: String) { - val request = CookOrderPageRequest( - mealType = dinnerType.toIntOrNull(), - keyword = input.trim() + netViewModel.foodSearch( + keyword = input.trim(), + pageNum = pageNo, + pageSize = pageSize, + onLoading = { showLoading() }, + onResult = { state -> + when (state) { + is UiState.Success -> handleSearchResult(state.data) + is UiState.Error -> { + delayDismissLoading() + finishRefresh() + binding.refreshLayout.setEnableRefresh(true) + toast(state.msg) + if (pageNo == 1) loadEmptyView() + } + else -> {} + } + } ) - netViewModel.getCookOrderPage(request = request) } /** - * 将 CookOrderItem 映射为 FoodRecord,适配现有 Adapter + * FoodSearchItem → FoodRecord 映射,供 Food2Adapter 展示 + * 采样模式下返回给 PrepareFoodActivity 作为称量模板 */ - private fun CookOrderItem.toFoodRecord(): FoodRecord { - return FoodRecord( - foodId = foodId, - foodName = dishName, - totalWeight = ((cookedWeight ?: 0.0) * 1000), - foodWeight = ((cookedWeight ?: 0.0) * 1000), - count = cookedPortions, - cookMode = 0, - isCooking = cookStatus == 1, - dinnerType = mealType.toString(), - isOriginalData = true - ) - } + private fun FoodSearchItem.toFoodRecord(): FoodRecord = FoodRecord( + foodId = foodId, + foodName = foodName, + foodCode = foodCode, + cookMode = 1 + ) /** - * 处理搜索结果:从分组响应中 flatten 所有 cookOrders,映射为 FoodRecord 后填充列表 + * 处理搜索结果:FoodSearchItem → FoodRecord 填充列表 */ @SuppressLint("NotifyDataSetChanged") - private fun handleSearchResult(groups: List?) { + private fun handleSearchResult(items: List?) { delayDismissLoading() finishRefresh() binding.refreshLayout.setEnableRefresh(true) - val records = groups - ?.flatMap { it.cookOrders } - ?.map { it.toFoodRecord() } - ?.toMutableList() - if (records.isNullOrEmpty()) { - loadEmptyView() + if (items.isNullOrEmpty()) { + if (pageNo == 1) { + loadEmptyView() + } return } - list.clear() - list.addAll(records) + if (pageNo == 1) list.clear() + list.addAll(items.map { it.toFoodRecord() }) recordAdapter.notifyDataSetChanged() - // 新接口暂不分页,禁用加载更多 - binding.refreshLayout.setEnableLoadMore(false) + val isLoadMoreEnable = items.size >= pageSize + binding.refreshLayout.setEnableLoadMore(isLoadMoreEnable) + if (isLoadMoreEnable) pageNo++ } private fun finishRefresh() { @@ -249,4 +205,4 @@ class FoodSearchActivity : BaseActivity() { setEnableLoadMore(false) } } -} \ No newline at end of file +} 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 6b751f7..925d4b0 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 @@ -19,8 +19,9 @@ import com.shuwei.dish.match.base.BaseActivity 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.dialog.FoodSearchDialog import com.shuwei.dish.match.model.CookOrderCompositionItem +import com.shuwei.dish.match.model.FoodCompositionResponse import com.shuwei.dish.match.db.entity.CookFoodGoodsEntity import com.shuwei.dish.match.model.FoodRecord import com.shuwei.dish.match.model.GoodsItem @@ -35,6 +36,7 @@ import com.shuwei.dish.match.utils.ext.addOnActionSearchListener import com.shuwei.dish.match.utils.ext.clickWithDebounce import com.shuwei.dish.match.utils.ext.dp import com.shuwei.dish.match.utils.ext.gone +import com.shuwei.dish.match.utils.ext.roundedOneDecimalPlace import com.shuwei.dish.match.utils.ext.startActivity import com.shuwei.dish.match.utils.ext.toJsonString import com.shuwei.dish.match.utils.ext.toast @@ -77,6 +79,12 @@ class PrepareFoodActivity : BaseActivity() { private var pageFrom: String? = null private var goodsList: MutableList? = null + + /** 食材检索弹窗,手动新增构成时复用 */ + private var foodSearchDialog: FoodSearchDialog? = null + + /** 手动录入卡当前选中的食材(含 materId),点击搜索后回填 */ + private var selectedIngredient: GoodsItem? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(binding.root) @@ -111,19 +119,23 @@ class PrepareFoodActivity : BaseActivity() { binding.etInputDish.let { it.setSelection(it.length()) } + // 采集模式:启用食材信息录入卡(手动新增构成) + initIngredientInput() } else { // 制作模式:不可编辑,仅展示菜名 binding.etInputDish.isFocusable = false binding.etInputDish.isFocusableInTouchMode = false binding.ivDishSearch.gone() + // 制作模式:不展示手动新增卡 + binding.llIngredientInput.gone() } addViewClickListener() addBackKeyListener() // 仅采样模式启用摄像头识别,制作模式不需要 - if (isSamplingMode) { - initCamera(binding.flCameraContainer) - } +// if (isSamplingMode) { +// initCamera(binding.flCameraContainer) +// } initRecyclerView() initObserver() @@ -136,11 +148,11 @@ class PrepareFoodActivity : BaseActivity() { } private fun addViewClickListener() { -// binding.tvShowWeight.setOnClickListener { -// showLoading("正在清零……") -// WeightUtil.tareTwo(AddressUtil.ONE) -// delayDismissLoading() -// } + binding.tvShowWeight.setOnClickListener { + showLoading("正在清零……") + WeightUtil.tareTwo(AddressUtil.ONE) + delayDismissLoading() + } // // TODO: 测试功能------------------------------- // binding.btnTestConfirm.clickWithDebounce { // if (currentWeight <= 0.0) return@clickWithDebounce @@ -171,8 +183,11 @@ class PrepareFoodActivity : BaseActivity() { tag = netWeight } } - // 仅采样模式触发食材识别 + // 仅采样模式:用量自动回填秤重 + 触发食材识别 if (SpTool.cookMode == 1) { + val displayWeight = + if (netWeight > 0) netWeight.roundedOneDecimalPlace().toString() else "" + binding.etAmount.setText(displayWeight) recognizeFood(netWeight) } } @@ -320,8 +335,8 @@ class PrepareFoodActivity : BaseActivity() { val hasOriginalData = list.any { it.isOriginalData } // 接口数据中主材数量 val n = list.count { it.isOriginalData && it.materialType == 1 } - // 待分配的非接口数据 - val nonOriginalList = list.filter { !it.isOriginalData } + // 待分配的非接口数据(跳过手动指定过主辅材的项) + val nonOriginalList = list.filter { !it.isOriginalData && !it.isMaterialTypeManual } if (hasOriginalData && MATERIAL_COUNT - n <= 0) { // 主材已满,所有非接口数据均为辅材 @@ -338,6 +353,79 @@ class PrepareFoodActivity : BaseActivity() { materialAdapter.notifyDataSetChanged() } + /** + * 食材信息录入卡初始化(仅采样模式): + * 搜索食材 → 选中回填名称并暂存 GoodsItem(含 materId)→ 选择主辅材 → 输入用量 → 添加 + */ + private fun initIngredientInput() { + binding.ivIngredientSearch.setOnClickListener { openIngredientSearch() } + binding.etIngredientName.setOnClickListener { openIngredientSearch() } + binding.btnReset.setOnClickListener { + binding.etAmount.setText("") + } + binding.btnAdd.clickWithDebounce { + addManualIngredient() + } + } + + /** 打开食材检索弹窗(食材名称不可手输,点击即搜索) */ + private fun openIngredientSearch() { + if (foodSearchDialog?.isShowing == true) return + foodSearchDialog = FoodSearchDialog( + activity = this, + onItemSelected = { item -> onIngredientSelected(item) } + ).also { it.show() } + } + + /** + * 食材检索选中回调:回填名称,暂存 GoodsItem(materId 用于提交) + */ + private fun onIngredientSelected(item: GoodsItem) { + selectedIngredient = item + binding.etIngredientName.setText(item.goodsName) + } + + /** + * 手动添加食材到构成列表:名称(materId)+ 主辅材 + 用量(克,取自秤重) + */ + private fun addManualIngredient() { + val ingredient = selectedIngredient + if (ingredient == null || ingredient.materId.isNullOrBlank()) { + toast("请先搜索选择食材") + return + } + val amount = currentWeight.roundedOneDecimalPlace() + if (amount <= 0) { + toast("请先称重获取用量") + return + } + val type = if (binding.rbAux.isChecked) 2 else 1 + val existing = list.firstOrNull { it.goodsId == ingredient.goodsId } + if (existing != null) { + // 已存在同食材:累加用量并同步主辅材 + existing.useWeight = (existing.useWeight ?: 0.0) + amount + existing.materialType = type + existing.isMaterialTypeManual = true + existing.isSetFinished = true + existing.isNewDishType = true + } else { + ingredient.materialType = type + ingredient.useWeight = amount + ingredient.isMaterialTypeManual = true + ingredient.isNewDishType = true + ingredient.isOriginalData = false + ingredient.isSetFinished = true + ingredient.isClicked = false + list.add(ingredient) + binding.rvMaterialList.smoothScrollToPosition(list.size - 1) + } + // 清空录入区 + selectedIngredient = null + binding.etIngredientName.setText("") + binding.etAmount.setText("") + updateMaterialTypes() + } + private fun openSubmitPage() { getGoodsList() if (goodsList.isNullOrEmpty()) { @@ -378,23 +466,6 @@ class PrepareFoodActivity : BaseActivity() { private fun initObserver() { - // 旧接口 observer(采样模式使用) - lifecycleScope.launch { - netViewModel.foodDetailState.collect { state -> - when (state) { - is UiState.Success -> { - val detail = state.data - if (detail == null) { - toast("查询菜品信息为空") - return@collect - } - loadDishDetail(detail) - } - is UiState.Error -> toast(state.msg) - else -> {} - } - } - } // 新配比秤接口 observer(制作模式使用) lifecycleScope.launch { netViewModel.cookOrderCompositionState.collect { state -> @@ -414,9 +485,25 @@ class PrepareFoodActivity : BaseActivity() { } } - /** 旧接口:通过 foodId 查询菜品构成(采样模式使用) */ + /** 新接口:通过 foodId 查询菜品构成(采样模式称量模板) */ private fun getDishDetail(foodId: String) { - netViewModel.getFoodDetail(foodId) + netViewModel.getFoodComposition( + foodId = foodId, + onResult = { state -> + when (state) { + is UiState.Success -> { + val response = state.data + if (response == null) { + toast("查询菜品构成为空") + } else { + loadFoodComposition(response) + } + } + is UiState.Error -> toast(state.msg) + else -> {} + } + } + ) } /** 新接口:通过 cookOrderId 查询菜品食材构成(制作模式使用) */ @@ -425,17 +512,23 @@ class PrepareFoodActivity : BaseActivity() { } - private fun loadDishDetail(detail: CookFoodDTO) { -// val voList = detail.stFoodInfoConstituteList - val voList = detail.foodConstituteList - if (voList.isNullOrEmpty()) { - return - } - // 筛选出主材和辅材,转换为 GoodsItem(useWeight 置零) - val tempData = voList.filter { it.materialType == 1 || it.materialType == 2 } - .map { it.toGoodsItem().also { item -> item.useWeight = 0.0 } } - // 直接替换 adapter.items,确保 adapter 数据源被完全替换 - // 同时同步外部 list,保证后续 updateMaterialTypes/removeFood 等操作正确 + /** + * 采样模式:将 food/{foodId}/composition 返回的构成映射为 GoodsItem 并刷新 UI + * 筛选主材(1)/辅材(2),调料(3)仍走槽位秤;useWeight 置零待重新称量 + */ + private fun loadFoodComposition(response: FoodCompositionResponse) { + val tempData = response.ingredients + .filter { it.isMain == 1 || it.isMain == 2 } + .map { + GoodsItem( + goodsId = it.materId, + materId = it.materId, + goodsName = it.materName, + materialType = it.isMain, + useWeight = 0.0, + isOriginalData = true + ) + } list.clear() list.addAll(tempData) materialAdapter.items = list @@ -475,13 +568,11 @@ class PrepareFoodActivity : BaseActivity() { list.forEachIndexed { index, entity -> entity.isClicked = index == position } - // 制作模式:选中食材时同步当前秤重 - if (SpTool.cookMode == 0) { - val item = list.getOrNull(position) - if (item != null && currentWeight > 0) { - item.useWeight = currentWeight - item.isSetFinished = true - } + // 选中食材时同步当前秤重(制作/采样均支持) + val item = list.getOrNull(position) + if (item != null && currentWeight > 0) { + item.useWeight = currentWeight + item.isSetFinished = true } notifyDataSetChanged() } @@ -519,9 +610,9 @@ class PrepareFoodActivity : BaseActivity() { private fun initRecyclerView() { binding.rvMaterialList.let { it.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) - // 仅 isNewDishType=true 的 item 显示侧滑删除菜单(必须在 setAdapter 之前调用) + // 采样模式下所有食材均可侧滑删除(必须在 setAdapter 之前调用) it.setSwipeMenuCreator { _, rightMenu, position -> - if (list.getOrNull(position)?.isNewDishType == true) { + if (SpTool.cookMode == 1) { rightMenu.addMenuItem(buildDeleteMenuItem()) } } diff --git a/app/src/main/java/com/shuwei/dish/match/ui/SamplingModeActivity.kt b/app/src/main/java/com/shuwei/dish/match/ui/SamplingModeActivity.kt index 957afb5..16ae0ae 100644 --- a/app/src/main/java/com/shuwei/dish/match/ui/SamplingModeActivity.kt +++ b/app/src/main/java/com/shuwei/dish/match/ui/SamplingModeActivity.kt @@ -13,11 +13,11 @@ import androidx.recyclerview.widget.LinearLayoutManager import com.shuwei.dish.match.R import com.shuwei.dish.match.adapter.FoodListAdapter import com.shuwei.dish.match.base.BaseActivity -import com.shuwei.dish.match.base.BaseApp import com.shuwei.dish.match.databinding.ActivitySamplingModeBinding import com.shuwei.dish.match.databinding.LayoutEmptyViewBinding import com.shuwei.dish.match.dialog.CommonDialog import com.shuwei.dish.match.model.FoodRecord +import com.shuwei.dish.match.model.SampleHistoryItem import com.shuwei.dish.match.net.UiState import com.shuwei.dish.match.utils.DateTimeUtil import com.shuwei.dish.match.utils.ext.dp @@ -251,12 +251,9 @@ class SamplingModeActivity : BaseActivity() { @SuppressLint("NotifyDataSetChanged") fun getSamplingList() { - netViewModel.getSamplingListWithCallback( - param = mutableMapOf( - "pageNum" to pageNo, - "pageSize" to pageSize, - "placeId" to BaseApp.canteenId - ), + netViewModel.sampleHistory( + pageNum = pageNo, + pageSize = pageSize, onLoading = { showLoading() } ) { state -> when (state) { @@ -264,14 +261,14 @@ class SamplingModeActivity : BaseActivity() { // 回调期间用户可能已切换 Tab,需再次校验 if (binding.rbSamplingFinished.isChecked.not()) { delayDismissLoading() - return@getSamplingListWithCallback + return@sampleHistory } - loadDishList(state.data) + loadDishList(state.data?.map { it.toFoodRecord() }?.toMutableList()) } is UiState.Error -> { if (binding.rbSamplingFinished.isChecked.not()) { delayDismissLoading() - return@getSamplingListWithCallback + return@sampleHistory } binding.refreshLayout.setEnableRefresh(true) toast(state.msg) @@ -284,6 +281,18 @@ class SamplingModeActivity : BaseActivity() { } } + /** 采样历史条目 → FoodRecord 映射,供 FoodListAdapter(SAMPLING_MODE) 展示 */ + private fun SampleHistoryItem.toFoodRecord(): FoodRecord = FoodRecord( + foodId = foodId, + foodName = foodName, + foodWeight = foodWeight, + cookMode = 1, + foodCode = foodCode, + auditStatus = auditStatus, + auditStatusName = auditStatusName, + auditRecordId = auditRecordId + ) + private fun finishRefresh() { if (pageNo == 1) { binding.refreshLayout.finishRefresh(1200) 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 b9700b2..93834ef 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,9 @@ 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.ConstituteCookMode +import com.shuwei.dish.match.model.ConstituteSaveItem +import com.shuwei.dish.match.model.ConstituteSaveRequest import com.shuwei.dish.match.model.CookCompleteItem import com.shuwei.dish.match.model.CookCompleteRequest import com.shuwei.dish.match.model.FoodRecord @@ -375,7 +378,7 @@ class SubmitFoodActivity : BaseActivity() { // 监听主设备 2格秤的熟重 WeightUtil.addWeightListener(TAG) { address, _, weight -> - if (address == AddressUtil.TWO) { + if (address == AddressUtil.ONE) { // 首次收到数据时记录初始重量 // if (!initWeightSet) { // initWeight = weight @@ -456,7 +459,13 @@ class SubmitFoodActivity : BaseActivity() { return@launch } - // 旧接口路径(采样模式 / 无烹制单 ID) + // 采样模式 → 调新配比秤提交构成接口(cookMode=2,从零建菜,进入待审核) + if (food?.cookMode == 1) { + submitConstitute() + return@launch + } + + // 旧接口路径(无烹制单 ID 的制作模式兜底) cookFoodEntity.let { it.matchingConstituteInfoList = this@SubmitFoodActivity.goodsList it.dinnerType = when (it.dinnerType) { @@ -537,6 +546,73 @@ class SubmitFoodActivity : BaseActivity() { ) } + /** + * 采样提交:从 goodsList 提取全部构成(主材/辅材/调料),组装 ConstituteSaveRequest 提交 + * cookMode=2(采样),熟重 foodWeight 单位为 g,isMain 直接取 materialType(1主/2辅/3调) + */ + private fun submitConstitute() { + val allGoods = goodsList ?: run { + toast("未获取到菜品或构成信息") + dismissLoading() + return + } + val foodName = cookFoodEntity.foodName ?: food?.foodName ?: "" + if (foodName.isBlank()) { + toast("菜品名称为空") + dismissLoading() + return + } + + // 仅保留用量 > 0 的食材;materId 优先取 materId,为空时回退 goodsId + val items = allGoods + .filter { (it.useWeight ?: 0.0) > 0.0 } + .map { + ConstituteSaveItem( + materId = it.materId?.takeIf { mid -> mid.isNotEmpty() } ?: it.goodsId, + useWeight = it.useWeight ?: 0.0, + isMain = it.materialType + ) + } + if (items.isEmpty()) { + toast("请完善菜品构成信息") + dismissLoading() + return + } + if (items.any { it.materId.isBlank() }) { + toast("菜品食材不能为空") + dismissLoading() + return + } + val foodWeight = cookFoodEntity.foodWeight + if (foodWeight <= 0.0) { + toast("熟重必须大于0") + dismissLoading() + return + } + + val request = ConstituteSaveRequest( + cookMode = ConstituteCookMode.SAMPLE, + foodName = foodName, + foodWeight = foodWeight, + items = items + ) + Log.d(TAG, "submitConstitute: ${request.toJsonString()}") + + netViewModel.constituteSave( + request = request, + onResult = { state -> + when (state) { + is UiState.Success -> submitSuccess(isSamplingData = true) + 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 086dea6..32f2d5c 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 @@ -224,7 +224,10 @@ class FoodListFragment : BaseFragment() { currentJob?.cancel() currentJob = viewLifecycleOwner.lifecycleScope.launch { // 首次加载传 mealType=null 获取所有餐次,切换 Tab 时传当前餐次精确请求 - val mealType = if (isFirstLoad) null else dinnerType.toIntOrNull() + val mealType = if (isFirstLoad) null else when (dinnerType) { + "1", "2", "3", "4" -> dinnerType.toInt() + else -> null // "0" 或非法值不传,由后端返回全部餐次 + } val request = CookOrderPageRequest( mealType = mealType, keyword = foodName diff --git a/app/src/main/res/layout/activity_prepare_food.xml b/app/src/main/res/layout/activity_prepare_food.xml index 5215b94..89e6b3c 100644 --- a/app/src/main/res/layout/activity_prepare_food.xml +++ b/app/src/main/res/layout/activity_prepare_food.xml @@ -51,6 +51,178 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 主材 新增采样 添加食材 + 清零 + 添加 + 点击搜索食材 + 称重获取 \ No newline at end of file diff --git a/配比秤-制作菜品-API文档.md b/配比秤-制作菜品-API文档.md index ecfcbbd..3d8eed8 100644 --- a/配比秤-制作菜品-API文档.md +++ b/配比秤-制作菜品-API文档.md @@ -15,20 +15,19 @@ ## 业务背景 -配比秤是一款独立于现有组配秤(`/neglect` 路径)的新硬件设备,部署在食堂后厨。第一期实现"按单烹制"功能:厨师在配比秤终端上查看当日烹饪任务 → 查看菜品食材构成 → 提交食材实际用量并触发组配完成 → 开始烹制 → 烹饪完成后提交熟重和调料用量。 +配比秤是一款独立于现有组配秤(`/neglect` 路径)的新硬件设备,部署在食堂后厨。第一期实现"按单烹制"功能:厨师在配比秤终端上查看当日烹饪任务 → 查看菜品食材构成 → 提交主辅材实际用量+熟重+调料用量,一次性完成烹制。 ### 核心流程 ``` -当日烹饪任务列表 → 查看菜品食材构成 → 去制作(提交主辅材实际用量) → 烹饪完成(提交熟重 + 调料用量) +当日烹饪任务列表 → 查看菜品食材构成 → 烹饪完成(提交主辅材用量 + 熟重 + 调料) ``` ### 烹制状态枚举 | 状态码 | 状态名 | 说明 | |:------:|--------|------| -| 0 | 待烹制 | 初始状态,可进行"去制作"操作 | -| 1 | 烹制中 | 已提交制作,可进行"烹饪完成"操作 | +| 0 | 待烹制 | 初始状态,可进行"烹饪完成"操作 | | 2 | 已完成 | 烹制结束,不可再操作 | | 3 | 异常 | 异常状态 | @@ -46,17 +45,17 @@ | 序号 | 接口 | 路径 | 用途 | |:----:|------|------|------| -| 一 | 当日烹饪任务列表(按餐次分组) | `POST /neglect/ratio-scale/cook-orders/page` | 按餐次分组返回今日需烹制的菜品列表 | +| 一 | 当日烹饪任务列表(按餐次分组) | `POST /neglect/ratio-scale/cook-orders/list` | 按餐次分组返回今日需烹制的菜品列表 | | 二 | 菜品食材构成 | `GET /neglect/ratio-scale/cook-orders/{cookOrderId}/composition` | 查看菜品的主材和辅材清单(含标准用量) | -| 三 | 去制作 | `POST /neglect/ratio-scale/cook-orders/submit` | 提交主辅材实际用量,完善组配并开始烹制 | -| 四 | 烹饪完成 | `POST /neglect/ratio-scale/cook-orders/finish` | 提交熟重和调料用量,计算生熟比 | +| 三 | 烹饪完成 | `POST /neglect/ratio-scale/cook-orders/complete` | 提交主辅材用量+熟重+调料,一次完成烹制 | +| 四 | 调料模糊搜索 | `POST /neglect/ratio-scale/seasoning-search` | 模糊搜索调料(糖蜜饯+油脂+调味品三大类) | --- ## 一、当日烹饪任务列表(按餐次分组) ``` -POST /neglect/ratio-scale/cook-orders/page +POST /neglect/ratio-scale/cook-orders/list Content-Type: application/json X-DEVICE-CODE: <设备编码> X-DEVICE-TOKEN: <设备登录Token> @@ -70,9 +69,7 @@ X-DEVICE-TOKEN: <设备登录Token> { "date": "2026-07-29", // 选填 — 计划日期 yyyy-MM-dd,默认当天 "mealType": 2, // 选填 — 餐次筛选: 1=早餐 / 2=午餐 / 3=晚餐 / 4=加餐 - "keyword": "红烧肉", // 选填 — 菜品名称模糊搜索 - "pageNum": 1, // 选填 — 保留字段,当前不使用 - "pageSize": 20 // 选填 — 保留字段,当前不使用 + "keyword": "红烧肉" // 选填 — 菜品名称模糊搜索 } ``` @@ -203,16 +200,16 @@ X-DEVICE-TOKEN: <设备登录Token> --- -## 三、去制作:提交食材实际用量并开始烹制 +## 三、烹饪完成:提交主辅材用量+熟重+调料 ``` -POST /neglect/ratio-scale/cook-orders/submit +POST /neglect/ratio-scale/cook-orders/complete Content-Type: application/json X-DEVICE-CODE: <设备编码> X-DEVICE-TOKEN: <设备登录Token> ``` -厨师在秤上完成主辅材称重后,提交各食材的实际用量。后端自动完成:组配任务完善 → 组配完成 → 烹制单状态切换为"烹制中" → 烹制食材明细更新。 +厨师在秤上完成称重后一次性提交全部数据,后端自动完成:组配任务完善 → 组配完成 → 计算生重生熟比 → 保存调料 → 烹制单置为已完成。 **该接口为事务性操作(`@Transactional`),所有步骤在同一事务中执行,任意步骤失败则全部回滚。** @@ -221,7 +218,7 @@ X-DEVICE-TOKEN: <设备登录Token> ```json { "cookOrderId": 10001, // 必填 — 烹制单id - "items": [ // 必填 — 食材实际用量列表,至少1条 + "items": [ // 必填 — 主辅材实际用量列表,至少1条 { "materId": 20001, // 必填 — 食材id "actualQty": 520.5 // 必填 — 实际用量(g) @@ -230,6 +227,13 @@ X-DEVICE-TOKEN: <设备登录Token> "materId": 20002, "actualQty": 310.0 } + ], + "cookedWeight": 3.200, // 必填 — 熟重(kg) + "seasonings": [ // 选填 — 调料用量列表 + { + "materId": 30001, // 必填 — 调料食材id + "actualQty": 15.0 // 必填 — 实际用量(g) + } ] } ``` @@ -249,22 +253,26 @@ X-DEVICE-TOKEN: <设备登录Token> ``` 1. 校验烹制单状态 = 0(待烹制),否则报错 2. 通过内供申领单关联餐品净菜包: - 链路: nut_prod_cook_order.food_id → nut_sup_internal_supply(food_id + canteen_id + clean_type=2) + 链路: nut_prod_cook_order(food_id + canteen_id + meal_type) → nut_prod_clean_order(food_id + canteen_id + meal_type + clean_type=2) + → clean_order_id → nut_sup_internal_supply(clean_order_id + clean_type=2) → batch_no → nut_sup_meal_package → nut_sup_meal_pkg_ingredient → trace_code + 餐次匹配:通过 nut_prod_clean_order.meal_type 过滤,确保同菜不同餐次不会串单 3. 解析各食材的溯源码(materId → traceCode 映射) 4. 完善组配任务明细(按 materCode 匹配 comboTaskItem,更新 actualQty + traceCode) 5. 完成组配任务(comboStatus=2,记录完成时间) 6. 计算生重(Σ actualQty / 1000,g→kg,保留3位小数) 7. 从设备Token获取厨师信息(chef + chefId) -8. 更新烹制单: - - cookStatus → 1(烹制中) - - cookStart → 当前时间 - - rawWeight → 生重(kg) - - chef / chefId → 登录厨师 - - needPortions / cookedPortions → 份数 -9. 更新烹制食材明细(cook_ingredient): - - 已有明细 → 更新 actualQty + traceCode - - 无已有明细 → 按 foodComposition 自动创建兜底记录 +8. 更新烹制食材明细(主辅材 actualQty + traceCode,无已有明细时兜底创建) +9. 保存调料用量(ingredientClass=3) +10. 计算生熟比 = 熟重 / 生重(保留3位小数,HALF_UP,生重>0且熟重≠null时才计算) +11. 更新烹制单: + - cookStatus → 2(已完成) + - cookStart / cookEnd → 当前时间 + - rawWeight → 生重(kg) + - cookedWeight → 熟重(kg) + - rawCookedRatio → 生熟比 + - chef / chefId → 登录厨师 + - needPortions / cookedPortions → 份数 ``` ### 份数计算优先级 @@ -278,7 +286,7 @@ X-DEVICE-TOKEN: <设备登录Token> | 场景 | 错误信息 | |------|---------| | 烹制单不存在 | `烹制单不存在` | -| 当前状态不是"待烹制" | `当前状态不允许此操作,仅待烹制状态可提交制作` | +| 当前状态不是"待烹制" | `当前状态不允许此操作,仅待烹制状态可提交` | | 烹制单未关联组配任务(comboNo 为空) | `烹制单未关联组配任务,无法开始烹制` | | 组配任务不存在 | `组配任务不存在,comboNo=xxx` | @@ -286,47 +294,36 @@ X-DEVICE-TOKEN: <设备登录Token> | 表名 | 操作 | 说明 | |------|:--:|------| -| `nut_prod_cook_order` | 查询 + 更新 | 校验状态,更新为烹制中 | -| `nut_sup_internal_supply` | 查询 | 通过 foodId + canteenId + cleanType=2 关联,获取 batchNo 和 quantity | +| `nut_prod_cook_order` | 查询 + 更新 | 校验状态,更新为已完成 | +| `nut_prod_clean_order` | 查询 | 通过 foodId + canteenId + mealType + cleanType=2 匹配净菜订单,获取 clean_order_id 用于内供申领单的餐次过滤 | +| `nut_sup_internal_supply` | 查询 | 通过 clean_order_id + cleanType=2 关联,获取 batchNo 和 quantity | | `nut_sup_meal_package` | 查询 | 通过 batchNo 查餐品净菜包 | | `nut_sup_meal_pkg_ingredient` | 查询 | 获取各食材的溯源码 | | `nut_prod_combo_task` | 查询 + 更新 | 完善组配状态为已完成 | | `nut_prod_combo_task_item` | 查询 + 更新 | 更新 actualQty + traceCode | -| `nut_prod_cook_ingredient` | 查询 + 新增/更新 | 更新实际用量,不存在时兜底创建 | +| `nut_prod_cook_ingredient` | 查询 + 新增/更新 | 更新主辅材实际用量 + 保存调料用量 | | `nut_mater_base` | 查询 | 获取食材编码和名称 | | `nut_food_composition` | 查询 | 兜底创建 cook_ingredient 时获取食材分类等信息 | --- -## 四、烹饪完成:提交熟重和调料用量 +## 四、调料模糊搜索 ``` -POST /neglect/ratio-scale/cook-orders/finish +POST /neglect/ratio-scale/seasoning-search Content-Type: application/json X-DEVICE-CODE: <设备编码> -X-DEVICE-TOKEN: <设备登录Token> ``` -烹饪结束后,厨师在秤上称量熟重,并提交调料用量。后端自动计算生熟比。 - -**该接口为事务性操作(`@Transactional`)。** +搜索调料食材库,数据源 `nut_mater_base` 一级分类为 18(糖蜜饯)、19(油脂)、20(调味品) 的食材。 ### 请求体 ```json { - "cookOrderId": 10001, // 必填 — 烹制单id - "cookedWeight": 3.200, // 必填 — 熟重(kg) - "seasonings": [ // 选填 — 调料用量列表 - { - "materId": 30001, // 必填 — 调料食材id - "actualQty": 15.0 // 必填 — 实际用量(g) - }, - { - "materId": 30002, - "actualQty": 8.5 - } - ] + "keyword": "酱油", // 选填 — 模糊匹配食材名称和别名 + "pageNum": 1, // 必填 — 页码 + "pageSize": 20 // 必填 — 每页条数 } ``` @@ -336,47 +333,28 @@ X-DEVICE-TOKEN: <设备登录Token> { "code": "200", "msg": "操作成功", - "data": null + "data": [ + { + "id": "30001", + "materName": "酱油", + "materCode": "19192004", + "alias": "生抽", + "canteenId": "2" + } + ], + "total": 1 } ``` -### 处理流程 +### 字段说明 -``` -1. 校验烹制单状态 = 1(烹制中),否则报错 -2. 如有调料:保存到 nut_prod_cook_ingredient - - 已在 cook_ingredient 中的调料(ingredientClass=3) → 更新 actualQty - - 新调料 → 新增记录(ingredientClass=3 调料) -3. 计算生熟比 = cookedWeight / rawWeight(保留3位小数,HALF_UP) -4. 更新烹制单: - - cookStatus → 2(已完成) - - cookedWeight → 熟重(kg) - - rawCookedRatio → 生熟比 - - cookEnd → 当前时间 -``` - -### 生熟比计算 - -``` -生熟比 = 熟重(kg) / 生重(kg) -精度: 3位小数, HALF_UP -条件: 生重 > 0 且熟重 != null 时才计算,否则生熟比为 null -``` - -### 异常场景 - -| 场景 | 错误信息 | -|------|---------| -| 烹制单不存在 | `烹制单不存在` | -| 当前状态不是"烹制中" | `当前状态不允许此操作,仅烹制中状态可完成烹制` | - -### 涉及数据表 - -| 表名 | 操作 | 说明 | -|------|:--:|------| -| `nut_prod_cook_order` | 查询 + 更新 | 校验状态,更新为已完成,记录熟重和生熟比 | -| `nut_prod_cook_ingredient` | 查询 + 新增/更新 | 保存调料用量(ingredientClass=3) | -| `nut_mater_base` | 查询 | 获取调料食材编码和名称 | +| 字段 | 类型 | 说明 | +|------|------|------| +| id | Long → String | 食材id | +| materName | String | 食材名称 | +| materCode | String | 食材编码 | +| alias | String | 别名 | +| canteenId | Long → String | 归属食堂id | --- @@ -389,13 +367,17 @@ nut_prod_cook_order (烹制单) │ │ │ └── mater_id ──→ nut_mater_base (食材库: 编码、名称、分类) │ - ├── canteen_id + food_id ──→ nut_sup_internal_supply (内供申领单) - │ │ - │ ├── batch_no ──→ nut_sup_meal_package (餐品净菜包) - │ │ │ - │ │ └── nut_sup_meal_pkg_ingredient (溯源码) - │ │ - │ └── quantity ──→ 份数 + ├── food_id + canteen_id + meal_type ──→ nut_prod_clean_order (净菜订单, clean_type=2) + │ │ + │ └── id = clean_order_id + │ + ├── clean_order_id ──→ nut_sup_internal_supply (内供申领单, 按 clean_order_id + clean_type=2 匹配) + │ │ + │ ├── batch_no ──→ nut_sup_meal_package (餐品净菜包) + │ │ │ + │ │ └── nut_sup_meal_pkg_ingredient (溯源码) + │ │ + │ └── quantity ──→ 份数 │ ├── combo_no ──→ nut_prod_combo_task (组配任务) │ │ @@ -424,20 +406,15 @@ nut_prod_cook_order (烹制单) │ │──→ nut_mater_base (食材名称) │ ← 食材清单(含标准用量) │←────────────────────│ │ │ │ - │ 3. POST /cook-orders/submit │ + │ 3. POST /cook-orders/complete │ │─────────────────────→│ │ │ │──→ 校验状态=待烹制 │ + │ │──→ 匹配净菜订单(餐次过滤) │ │ │──→ 关联internal_supply │ │ │──→ 解析溯源码 │ │ │──→ 完善+完成组配任务 │ │ │──→ 计算生重 │ - │ │──→ 更新烹制单=烹制中 │ │ │──→ 更新食材明细 │ - │ ← 操作成功 │←──── 事务提交 ──────│ - │ │ │ - │ 4. POST /cook-orders/finish │ - │─────────────────────→│ │ - │ │──→ 校验状态=烹制中 │ │ │──→ 保存调料用量 │ │ │──→ 计算生熟比 │ │ │──→ 更新烹制单=已完成 │ diff --git a/配比秤-采样模式-API文档.md b/配比秤-采样模式-API文档.md new file mode 100644 index 0000000..f2bd54a --- /dev/null +++ b/配比秤-采样模式-API文档.md @@ -0,0 +1,611 @@ +# 配比秤(新设备)— 采样与制作模式 API 文档 + +> Controller: `NutRatioScaleController` +> 路径前缀: `/neglect/ratio-scale`(Nacos 白名单 `/nutrition/neglect/**`,无需 Sa-Token) +> 设备上下文通过请求头 `X-DEVICE-CODE` 解析(`TerminalContextHelper`,据此拿到食堂 id) +> 日期: 2026-09-09 +> +> **序列化规则**:Long 类型字段响应中均为字符串(Jackson `ToStringSerializer`),`BigDecimal` 为普通数字 +> **单位约定**: +> - 提交构成的熟重 `foodWeight` 单位为 **克 (g)**(与 `nut_food_cook.output_weight` 口径一致) +> - 食材称量用量 `useWeight` 单位为 **克 (g)** +> - 烹制单生重 `rawWeight`、熟重 `cookedWeight` 单位为 **千克 (kg)** +> - 食材实际用量 `actualQty` 单位为 **克 (g)** + +--- + +## 业务背景 + +配比秤新增「采样模式」与「制作模式」,两者通过**同一个提交接口** `POST /constitute/save` 的 `cookMode` 字段区分: + +| cookMode | 模式 | 说明 | +|:--------:|------|------| +| 2 | 采样模式 | **从零建菜**:称量食材 → 新增菜品(生成构成/营养/宝塔)→ 进入「待审核」→ 后台审核通过后上架 | +| 1 | 制作模式 | **基于已有菜品**:称量食材 → 记录熟重+构成快照(历史参考,不覆盖正式构成)→ 标记菜品为「配比秤制作」 | + +### 制作方式枚举 + +| 值 | 名称 | 菜品对象 | 数据来源 data_source | +|:--:|------|---------|---------------------| +| 1 | 制作模式 | 已有菜品(foodId 必填) | 3(配比秤制作) | +| 2 | 采样模式 | 新增菜品(foodName 必填) | 2(配比秤采样) | + +### 采样菜品审核状态枚举 + +| 状态码 | 状态名 | 说明 | +|:------:|--------|------| +| 1 | 待审核 | 采样提交后的初始状态,后台可审核 | +| 2 | 已通过 | 审核通过,菜品上架生效 | +| 3 | 已驳回 | 审核驳回,菜品保持下架,可重新采样同名菜品 | + +### 食材分类枚举 + +| 分类 | 名称 | 说明 | +|:----:|------|------| +| 1 | 主材 | 菜品主要食材 | +| 2 | 辅材 | 菜品辅助食材 | +| 3 | 调料 | 油盐酱醋等 | + +--- + +## 接口清单 + +| 序号 | 接口 | 路径 | 用途 | +|:----:|------|------|------| +| 一 | 食材模糊搜索 | `POST /neglect/ratio-scale/mater-search` | 称量前检索食材(设备对应食堂食材库) | +| 二 | 搜菜品 | `POST /neglect/ratio-scale/food-search` | 制作模式选已有菜品(按名称模糊查上架菜品) | +| 三 | 查菜品构成 | `GET /neglect/ratio-scale/food/{foodId}/composition` | 制作模式查看已有菜品的主辅料+调料构成 | +| 四 | 提交构成 | `POST /neglect/ratio-scale/constitute/save` | 统一提交,cookMode 区分制作/采样 | +| 五 | 采样历史 | `POST /neglect/ratio-scale/sample/history` | 查询本食堂采样提交历史(默认当天) | +| 六 | 制作历史 | `POST /neglect/ratio-scale/make/history` | 按设备+日期查询制作记录分页 | +| 七 | 制作历史图表 | `GET /neglect/ratio-scale/make/history/{foodId}` | 按菜品id查制作历史图表数据 | +| 八 | 烹饪完成 | `POST /neglect/ratio-scale/cook-orders/complete` | 按单烹制,任意状态提交均累加 | + +--- + +## 一、食材模糊搜索 + +``` +POST /neglect/ratio-scale/mater-search +Content-Type: application/json +X-DEVICE-CODE: <设备编码> +``` + +称量食材前,从食材库检索食材(设备对应食堂食材)。 + +### 请求体 + +```json +{ + "keyword": "萝卜", // 选填 — 模糊匹配食材名称和别名 + "pageNum": 1, // 必填 — 页码 + "pageSize": 20 // 必填 — 每页条数 +} +``` + +### 响应 + +```json +{ + "code": "00000", + "msg": "操作成功", + "data": [ + { + "id": "1979825467306471454", // 食材id + "materName": "樱桃萝卜", // 食材名称 + "materCode": "04041012", // 食材编码 + "alias": null, // 别名 + "materFirstClass": "04", // 食材一级分类 + "materSecondClass": "043", // 食材二级分类 + "materUrl": null, // 食材图片url + "canteenId": "2" // 归属食堂id + } + ], + "total": 1 +} +``` + +### 字段说明 + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | Long → String | 食材id(提交构成 items.materId 使用) | +| materName | String | 食材名称 | +| materCode | String | 食材编码(一级+二级+自编码,全局唯一) | +| alias | String | 别名 | +| materFirstClass | String | 食材一级分类 | +| materSecondClass | String | 食材二级分类 | +| materUrl | String | 食材图片url | +| canteenId | Long → String | 归属食堂id | + +### 逻辑说明 + +- 数据源:`nut_mater_base` +- 过滤:`canteen_id` = 当前食堂(由 `X-DEVICE-CODE` 解析) +- 关键词模糊匹配 `mater_name` 和 `alias` +- 排序:`mater_name ASC` + +--- + +## 二、搜菜品 + +``` +POST /neglect/ratio-scale/food-search +Content-Type: application/json +X-DEVICE-CODE: <设备编码> +``` + +制作模式下,检索本食堂已上架的菜品,供后厨选择目标菜品。 + +### 请求体 + +```json +{ + "keyword": "西兰花", // 选填 — 菜品名称或编号模糊搜索 + "pageNum": 1, // 必填 — 页码 + "pageSize": 20 // 必填 — 每页条数 +} +``` + +### 响应 + +```json +{ + "code": "00000", + "msg": "操作成功", + "data": [ + { + "foodId": "2091839370283118593", // 菜品id + "foodCode": "FOOD00012", // 菜品编号 + "foodName": "清炒西兰花" // 菜品名称 + } + ], + "total": 1 +} +``` + +### 逻辑说明 + +- 数据源:`nut_food`(`status=1` 上架) +- 过滤:`canteen_id` = 当前食堂 +- 关键词模糊匹配 `food_name` 和 `food_code` +- 排序:`id DESC` + +--- + +## 三、查菜品构成 + +``` +GET /neglect/ratio-scale/food/{foodId}/composition +X-DEVICE-CODE: <设备编码> +``` + +制作模式下,根据菜品 id 查询已有菜品的主辅料+调料构成及熟重。 + +### 响应 + +```json +{ + "code": "00000", + "msg": "操作成功", + "data": { + "foodId": "2091839370283118593", // 菜品id + "foodName": "清炒西兰花", // 菜品名称 + "foodWeight": 450.0, // 熟重(g),从 nut_food_cook.output_weight 反查 + "ingredients": [ + { + "materId": "1979825467306471454", // 食材id + "materName": "樱桃萝卜", // 食材名称 + "useWeight": 300.0, // 用量(g) + "isMain": 1, // 1=主材 / 2=辅材 / 3=调料 + "isMainName": "主材" + } + ] + } +} +``` + +### 逻辑说明 + +- 数据源:`nut_food_composition`(按 `food_id` 查全部构成) +- 食材名称从 `nut_mater_base` 批量反查 + +--- + +## 四、提交构成(cookMode 区分采样/制作) + +``` +POST /neglect/ratio-scale/constitute/save +Content-Type: application/json +X-DEVICE-CODE: <设备编码> +``` + +**统一提交接口**,通过 `cookMode` 区分采样/制作。 + +### 请求体 + +```json +{ + "cookMode": 2, // 必填 — 1=制作模式 / 2=采样模式 + "foodId": 2091839370283118593, // 制作模式必填 — 已有菜品id + "foodName": "清炒西兰花", // 采样模式必填 — 菜品名称 + "foodWeight": 500, // 必填 — 熟重(g),必须 > 0 + "mealType": 2, // 选填 — 餐次: 1=早餐/2=午餐/3=晚餐/4=加餐 + "items": [ // 必填 — 食材构成列表,至少1条 + { + "materId": 20001, // 必填 — 食材id(nut_mater_base.id) + "useWeight": 300, // 必填 — 食材称量用量(g),必须 > 0 + "isMain": 1 // 必填 — 食材分类: 1=主材 / 2=辅材 / 3=调料 + }, + { + "materId": 30001, + "useWeight": 50, + "isMain": 3 + } + ] +} +``` + +### 响应 + +```json +{ + "code": "00000", + "msg": "操作成功", + "data": null +} +``` + +### 处理流程(采样模式 cookMode=2) + +``` +1. 校验食材构成非空 + materId 非空且唯一 +2. 校验菜品名称在同食堂下唯一(排除已驳回菜品,允许驳回后重新采样同名) +3. 生成菜品编号(FOOD + 5位序号) +4. 保存菜品 nut_food:data_source=2(配比秤采样) / audit_status=1(待审核) / status=0(下架) +5. 保存熟重 nut_food_cook:output_weight = foodWeight +6. 保存食材构成 nut_food_composition:mater_id + use_weight + is_main +7. 根据构成自动计算营养指标 nut_food_nutri + 膳食宝塔 nut_food_pagoda +8. 发起审核记录 nut_food_audit_record:audit_status=1(待审核) +``` + +### 处理流程(制作模式 cookMode=1) + +``` +1. 校验食材构成非空 + materId 非空且唯一 +2. 校验菜品存在(foodId 必填) +3. 标记菜品 nut_food.data_source = 3(配比秤制作) +4. 保存制作记录 nut_food_matching_use:device_code + food_id + food_name + meal_type + day + food_weight(熟重) +5. 保存制作明细 nut_food_matching_use_item:mater_id + mater_name + use_weight + is_main(构成快照) +6. 不覆盖菜品的正式构成/营养/宝塔(历史参考) +``` + +### 异常场景 + +| 场景 | 错误信息 | +|------|---------| +| 食材构成列表为空 | `请完善菜品构成信息` | +| 食材id为空 | `菜品食材不能为空` | +| 食材id重复 | `菜品食材不能重复,重复食材id:xxx` | +| 制作方式非法 | `不支持的制作方式:xxx` | +| 采样:同食堂已存在同名菜品(非驳回) | `该食堂下已存在同名餐品` | +| 采样:食材id无效 | `存在无效的食材id` | +| 制作:菜品不存在 | `菜品不存在` | +| 熟重/用量 ≤ 0 | 参数校验:`熟重必须大于0` / `食材用量必须大于0` | + +### 涉及数据表 + +**采样模式(cookMode=2)** + +| 表名 | 操作 | 说明 | +|------|:--:|------| +| `nut_food` | 新增 | 采样菜品主表(采样来源 + 待审核 + 下架) | +| `nut_food_cook` | 新增 | 保存熟重 | +| `nut_food_composition` | 新增 | 食材构成 | +| `nut_food_nutri` | 新增 | 每100g营养指标(自动计算) | +| `nut_food_pagoda` | 新增 | 膳食宝塔(自动计算) | +| `nut_food_audit_record` | 新增 | 待审核记录 | + +**制作模式(cookMode=1)** + +| 表名 | 操作 | 说明 | +|------|:--:|------| +| `nut_food` | 更新 | 标记 data_source=3(配比秤制作) | +| `nut_food_matching_use` | 新增 | 制作记录(熟重记录) | +| `nut_food_matching_use_item` | 新增 | 制作明细(构成快照) | + +--- + +## 五、采样历史 + +``` +POST /neglect/ratio-scale/sample/history +Content-Type: application/json +X-DEVICE-CODE: <设备编码> +``` + +查询本食堂(由 `X-DEVICE-CODE` 解析)的采样提交历史,默认查询当天。 + +### 请求体 + +```json +{ + "pageNum": 1, // 必填 — 页码 + "pageSize": 10, // 必填 — 每页条数 + "keyword": "西兰花", // 选填 — 菜品名称模糊搜索 + "auditStatus": 1, // 选填 — 审核状态筛选: 1=待审核 / 2=已通过 / 3=已驳回 + "sampleDate": "2026-09-09" // 选填 — 采样日期,不传默认当天 +} +``` + +### 响应 + +```json +{ + "code": "00000", + "msg": "操作成功", + "data": [ + { + "foodId": "2097486806812893186", // 菜品id + "foodCode": "FOOD00030", // 菜品编号 + "foodName": "清炒西兰花", // 菜品名称 + "foodWeight": 500.0, // 熟重(g) + "auditStatus": 1, // 审核状态: 1=待审核 / 2=已通过 / 3=已驳回 + "auditStatusName": "待审核", // 审核状态中文 + "auditRecordId": "2097486809375612929", // 审核记录id + "createTime": "2026-09-09 08:46:39" // 提交时间 + } + ], + "total": 1 +} +``` + +### 逻辑说明 + +- 数据源:`nut_food`(`data_source=2` 配比秤采样) +- `canteenId` 由 `X-DEVICE-CODE` 解析终端后自动限定 +- 排序:`create_time DESC` +- `foodWeight` 从 `nut_food_cook.output_weight` 反查 +- `auditRecordId` 从 `nut_food_audit_record` 反查 + +--- + +## 六、制作历史 + +``` +POST /neglect/ratio-scale/make/history +Content-Type: application/json +X-DEVICE-CODE: <设备编码> +``` + +查询本设备(由 `X-DEVICE-CODE` 解析)的制作记录。 + +### 请求体 + +```json +{ + "pageNum": 1, // 必填 — 页码 + "pageSize": 10, // 必填 — 每页条数 + "day": "2026-09-09" // 选填 — 制作日期,不传查全部 +} +``` + +### 响应 + +```json +{ + "code": "00000", + "msg": "操作成功", + "data": [ + { + "id": "2097499999999999999", // 制作记录id + "foodId": "2091839370283118593", // 菜品id + "foodName": "清炒西兰花", // 菜品名称 + "foodWeight": 500.0, // 熟重(g) + "mealType": 2, // 餐次: 1=早餐/2=午餐/3=晚餐/4=加餐 + "mealTypeName": "午餐", + "day": "2026-09-09", // 制作日期 + "createTime": "2026-09-09 08:46:39" + } + ], + "total": 1 +} +``` + +### 逻辑说明 + +- 数据源:`nut_food_matching_use` +- 过滤:`device_code` = 当前设备(由 `X-DEVICE-CODE` 解析) +- 排序:`create_time DESC` + +--- + +## 七、制作历史图表(按菜品id) + +``` +GET /neglect/ratio-scale/make/history/{foodId} +X-DEVICE-CODE: <设备编码> +``` + +根据菜品 id 查询该菜品的所有制作记录,返回图表数据(对齐借鉴项目 `queryGoodsInfoByFoodIdMatchingList`),用于前端渲染制作趋势图。 + +### 响应 + +```json +{ + "code": "00000", + "msg": "操作成功", + "data": [ + { + "xAxes": [ + {"goodId": "", "goodName": "时间"}, + {"goodId": "", "goodName": "熟重"}, + {"goodId": "20001", "goodName": "樱桃萝卜"}, + {"goodId": "30001", "goodName": "酱油"} + ], + "yAxes": [ + {"value": "2026-09-09 08:46:39"}, + {"value": 500.0}, + {"value": 300.0}, + {"value": 50.0} + ] + } + ] +} +``` + +### 字段说明 + +| 字段 | 说明 | +|------|------| +| xAxes | 横轴模板:时间、熟重、该菜品所有出现过的食材(去重保序),所有记录共用 | +| xAxes[].goodId | 食材id(时间/熟重项为空字符串) | +| xAxes[].goodName | 项名称(时间/熟重/食材名称) | +| yAxes | 纵轴值,与 xAxes 一一对应 | +| yAxes[].value | 值:时间项为时间戳,其余为数值(缺失食材补 0) | + +### 逻辑说明 + +- 数据源:`nut_food_matching_use`(主表)+ `nut_food_matching_use_item`(子表) +- 主表按 `food_id` 过滤,`create_time DESC` 排序 +- 子表按 `matching_use_id` 批量查询并分组(消除 N+1) +- 时间项取 `create_time`,熟重项取 `food_weight`,食材项取 `use_weight`(缺失补 0) + +--- + +## 八、烹饪完成(支持再次烹制) + +``` +POST /neglect/ratio-scale/cook-orders/complete +Content-Type: application/json +X-DEVICE-CODE: <设备编码> +``` + +按单烹制提交。**改造点**:原逻辑一次性完成(`待烹制 → 已完成`),现支持**任意状态下再次烹制**——每次提交累加份数/生重/熟重并留存一条流水,不校验烹制状态,需求达标后仍可继续烹制(应对菜不够卖接着做的场景)。 + +### 请求体 + +```json +{ + "cookOrderId": 10001, // 必填 — 烹制单id + "portions": 10, // 选填 — 本次烹制份数,不传则按申领单/需求份数兜底 + "cookStart": "2026-09-09 10:00:00", // 选填 — 本次烹制开始时间 + "cookedWeight": 3.5, // 必填 — 本次熟重(kg) + "items": [ // 必填 — 主辅材实际用量列表,至少1条 + { + "materId": 20001, // 必填 — 食材id + "actualQty": 5000 // 必填 — 实际用量(g) + } + ], + "seasonings": [ // 选填 — 调料用量列表 + { + "materId": 30001, + "actualQty": 15.0 + } + ] +} +``` + +### 响应 + +```json +{ + "code": "00000", + "msg": "操作成功", + "data": null +} +``` + +### 状态流转规则(改造核心) + +``` +每次提交后(不校验状态,任何状态下提交均累加): + cooked_portions 累加 += 本次份数 + raw_weight 累加 += 本次生重 + cooked_weight 累加 += 本次熟重 + cook_status → 2(已完成,每次提交都置此状态,允许继续烹制累加) + 生熟比 = 累计熟重 / 累计生重(用累加后的值) + +时间语义: + cook_start = 首次烹制开始时间(后续烹制不覆盖) + cook_end = 最后一次烹制结束时间(每次更新) +``` + +### 处理流程 + +``` +1. 校验烹制单存在 + 食堂权限(不校验烹制状态,任何状态下提交均累加数据) +2. 本次烹制份数 = 入参 portions(优先)→ 申领单 quantity → 需求份数 → 1 +3. 首次烹制(cooked_portions=0)才完善组配任务并完成 +4. 计算本次生重(Σ actualQty / 1000,g→kg) +5. 计算生熟比 = 累计熟重 / 累计生重(用累加后的值) +6. 写烹制流水 nut_prod_cook_flow(本次生重/熟重/份数/起止时间) +7. 保存食材明细 + 调料(首次更新预生成明细,再次插入新明细,均关联 flow_id) +8. 累加主表:份数/生重/熟重累加 + 状态置已完成 + 起止时间 +``` + +### 异常场景 + +| 场景 | 错误信息 | +|------|---------| +| 烹制单不存在 | `烹制单不存在` | +| 首次烹制未关联组配任务 | `烹制单未关联组配任务,无法开始烹制` | +| 组配任务不存在 | `组配任务不存在,comboNo=xxx` | + +### 涉及数据表 + +| 表名 | 操作 | 说明 | +|------|:--:|------| +| `nut_prod_cook_order` | 查询 + 更新 | 累加份数/生重/熟重 + 状态置已完成 | +| `nut_prod_cook_flow` | 新增 | **烹制流水**,每次烹制一条 | +| `nut_prod_cook_ingredient` | 查询 + 更新/新增 | 首次更新预生成明细,再次插入新明细,均带 `flow_id` | +| `nut_prod_combo_task` / `_item` | 查询 + 更新 | 仅首次烹制时完善组配 | +| `nut_sup_internal_supply` / `nut_sup_meal_package` / `_ingredient` | 查询 | 解析溯源码与份数 | +| `nut_mater_base` / `nut_food_composition` | 查询 | 食材编码/名称/分类 | + +--- + +## 数据链路图(采样模式 cookMode=2) + +``` +nut_mater_base (食材库) + │ + │ 采样称量 mater_id + use_weight(g) + ▼ +POST /constitute/save (cookMode=2) + │ + ├── 生成 nut_food (采样来源 + 待审核 + 下架) + ├── 生成 nut_food_cook (熟重 = foodWeight) + ├── 生成 nut_food_composition (食材构成) + ├── 自动计算 nut_food_nutri (每100g营养) + nut_food_pagoda (膳食宝塔) + └── 生成 nut_food_audit_record (待审核) +``` + +## 数据链路图(制作模式 cookMode=1) + +``` +nut_food (已有菜品) + │ + │ 制作称量 mater_id + use_weight(g) + 熟重 + ▼ +POST /constitute/save (cookMode=1) + │ + ├── 标记 nut_food.data_source = 3 (配比秤制作) + ├── 生成 nut_food_matching_use (制作记录:熟重/餐次/日期/设备) + └── 生成 nut_food_matching_use_item (构成快照,历史参考) + (不覆盖 nut_food_composition / nut_food_nutri / nut_food_pagoda) +``` + +## 数据链路图(再次烹制) + +``` +nut_prod_cook_order (烹制单,累加数据) + │ + ├── POST /cook-orders/complete (每次烹制) + │ ├── 生成 nut_prod_cook_flow (流水,每次一条) + │ └── 生成/更新 nut_prod_cook_ingredient (明细,带 flow_id) + │ + └── 主表累加: cooked_portions / raw_weight / cooked_weight + cook_start = 首次开始 / cook_end = 末次结束 + cook_status = 已完成(每次提交都置此状态,允许继续烹制累加) +```