feat(activity): 优化食材识别重量逻辑,修复主辅材重复保存问题

- PrepareFoodActivity:改为以首次秤读数为基准计算净重,注释清零按钮和测试按钮,修复 foodSelectCallback 中 isClicked 赋值对象错误导致选中状态异常的 bug,修复 getGoodsList 重复调用时主辅材累加的问题,向 FoodRecognizeActivity 传递 initialWeight
- FoodRecognizeActivity:启用秤重量监听,改为净重显示;用滑动窗口(最近10帧)替换延迟更新逻辑,关闭页面时从窗口中提取连续稳定值作为 useWeight,数据不稳定时弹窗提示而非直接关闭
- DbViewModel:saveCookFoodAndGoods 首次插入后回填 entity.id,防止重复进入时主辅材重复插入
- InitActivity:注释启动时自动清零秤的逻辑
- SubmitFoodActivity:将魔法数字 0.5 提取为常量 WEIGHT_THRESHOLD
This commit is contained in:
2026-05-13 16:01:28 +08:00
parent 65958bf637
commit ce7d2da6d8
5 changed files with 125 additions and 73 deletions
@@ -73,6 +73,8 @@ class DbViewModel : ViewModel() {
// 首次保存:主表 + 主辅材 + 调料 全量 insert // 首次保存:主表 + 主辅材 + 调料 全量 insert
rep.insertCookFood(entity) rep.insertCookFood(entity)
val newData = rep.getCookFoodById(entity.foodId, cookMode, entity.dinnerType ?: "0") val newData = rep.getCookFoodById(entity.foodId, cookMode, entity.dinnerType ?: "0")
// 回填 id,防止下次调用时 getCookFoodById 查不到而重复插入主辅材
newData?.let { entity.id = it.id }
list?.let { list?.let {
it.forEach { goods -> it.forEach { goods ->
goods.id = 0 goods.id = 0
@@ -10,10 +10,12 @@ import androidx.recyclerview.widget.RecyclerView
import com.shuwei.dish.match.adapter.FoodAdapter import com.shuwei.dish.match.adapter.FoodAdapter
import com.shuwei.dish.match.base.BaseActivity import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.databinding.ActivityFoodRecognizeBinding import com.shuwei.dish.match.databinding.ActivityFoodRecognizeBinding
import com.shuwei.dish.match.dialog.CommonDialog
import com.shuwei.dish.match.dialog.FoodSearchDialog import com.shuwei.dish.match.dialog.FoodSearchDialog
import com.shuwei.dish.match.entity.GoodsItem import com.shuwei.dish.match.entity.GoodsItem
import com.shuwei.dish.match.entity.GoodsNameQueryDTO import com.shuwei.dish.match.entity.GoodsNameQueryDTO
import com.shuwei.dish.match.utils.ActivityManager import com.shuwei.dish.match.utils.ActivityManager
import com.shuwei.dish.match.utils.AddressUtil
import com.shuwei.dish.match.utils.WeightUtil import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.clickWithDebounce import com.shuwei.dish.match.utils.ext.clickWithDebounce
import com.shuwei.dish.match.utils.ext.gone import com.shuwei.dish.match.utils.ext.gone
@@ -38,6 +40,7 @@ class FoodRecognizeActivity : BaseActivity() {
private const val TAG = "FoodRecognizeActivity" private const val TAG = "FoodRecognizeActivity"
private const val EXTRA_IMAGE_URI = "extra_image_uri" private const val EXTRA_IMAGE_URI = "extra_image_uri"
private const val EXTRA_CURRENT_WEIGHT = "extra_current_weight" private const val EXTRA_CURRENT_WEIGHT = "extra_current_weight"
private const val EXTRA_INITIAL_WEIGHT = "extra_initial_weight"
const val EXTRA_SELECTED_ITEM = "extra_selected_item" const val EXTRA_SELECTED_ITEM = "extra_selected_item"
const val IS_MANUAL_CANCEL = "isManualCancel" const val IS_MANUAL_CANCEL = "isManualCancel"
@@ -54,6 +57,7 @@ class FoodRecognizeActivity : BaseActivity() {
activity: BaseActivity, activity: BaseActivity,
imageUri: String? = null, imageUri: String? = null,
currentWeight: Double = 0.0, currentWeight: Double = 0.0,
initialWeight: Double = 0.0,
nameScoreList: ArrayList<FoodModule.IdNameScore> = arrayListOf(), nameScoreList: ArrayList<FoodModule.IdNameScore> = arrayListOf(),
finishCallback: (Boolean, GoodsItem?) -> Unit = { _, _ -> } finishCallback: (Boolean, GoodsItem?) -> Unit = { _, _ -> }
) { ) {
@@ -61,6 +65,7 @@ class FoodRecognizeActivity : BaseActivity() {
val launchIntent = Intent(activity, FoodRecognizeActivity::class.java).apply { val launchIntent = Intent(activity, FoodRecognizeActivity::class.java).apply {
putExtra(EXTRA_IMAGE_URI, imageUri) putExtra(EXTRA_IMAGE_URI, imageUri)
putExtra(EXTRA_CURRENT_WEIGHT, currentWeight) putExtra(EXTRA_CURRENT_WEIGHT, currentWeight)
putExtra(EXTRA_INITIAL_WEIGHT, initialWeight)
} }
activity.startActivity(launchIntent) { resultIntent -> activity.startActivity(launchIntent) { resultIntent ->
val isManualCancel = resultIntent?.getBooleanExtra(IS_MANUAL_CANCEL, false) ?: false val isManualCancel = resultIntent?.getBooleanExtra(IS_MANUAL_CANCEL, false) ?: false
@@ -94,14 +99,17 @@ class FoodRecognizeActivity : BaseActivity() {
/** 当前选中的列表位置,-1 表示无选中 */ /** 当前选中的列表位置,-1 表示无选中 */
private var selectedPosition = -1 private var selectedPosition = -1
/** 秤1当前实时重量 */ /** 秤1当前实时净重(已减去初始读数) */
private var currentWeight = 0.0 private var currentWeight = 0.0
/** 重量减少时延迟更新的任务,页面关闭前取消可防止用错误重量 */ /** 初始秤读数,净重 = 秤读数 - initialWeight */
private var pendingWeightRunnable: Runnable? = null private var initialWeight = 0.0
/** 当前待延迟更新的目标重量,避免相同重量重复重置计时器 */ /**
private var pendingWeight = 0.0 * 滑动窗口:保存最近 10 条稳定帧净重,用于关闭页面时判断数据是否稳定
* 超过 10 条时移除队头,保持最新数据
*/
private val weightWindow = ArrayDeque<Double>(10)
/** 右侧自定义滚动指示条最小高度 */ /** 右侧自定义滚动指示条最小高度 */
private val scrollIndicatorMinHeightPx by lazy { (24 * resources.displayMetrics.density).toInt() } private val scrollIndicatorMinHeightPx by lazy { (24 * resources.displayMetrics.density).toInt() }
@@ -199,6 +207,7 @@ class FoodRecognizeActivity : BaseActivity() {
binding.ivFoodPhoto.setImageURI(imageUri.toUri()) binding.ivFoodPhoto.setImageURI(imageUri.toUri())
} }
currentWeight = intent.getDoubleExtra(EXTRA_CURRENT_WEIGHT, 0.0) currentWeight = intent.getDoubleExtra(EXTRA_CURRENT_WEIGHT, 0.0)
initialWeight = intent.getDoubleExtra(EXTRA_INITIAL_WEIGHT, 0.0)
binding.tvWeight.text = "${currentWeight}g" binding.tvWeight.text = "${currentWeight}g"
// val tempList = arrayListOf<GoodsItem>() // val tempList = arrayListOf<GoodsItem>()
// Process recognized name list if available // Process recognized name list if available
@@ -436,58 +445,82 @@ class FoodRecognizeActivity : BaseActivity() {
* 低于阈值说明食材已被取走,保留上次有效值等待页面关闭 * 低于阈值说明食材已被取走,保留上次有效值等待页面关闭
*/ */
private fun registerWeightListener() { private fun registerWeightListener() {
// WeightUtil.addWeightListener(TAG) { address, state, weight -> WeightUtil.addWeightListener(TAG) { address, state, weight ->
// if (address == AddressUtil.ONE && state == WeightUtil.STATE_STABLE) { if (address == AddressUtil.ONE && state == WeightUtil.STATE_STABLE) {
// runOnUiThread { runOnUiThread {
// currentWeight = weight // 净重 = 秤读数 - 初始读数
// binding.tvWeight.run { val netWeight = weight - initialWeight
// if (tag != weight) { currentWeight = netWeight
// text = "$weight g" binding.tvWeight.run {
// tag = weight if (tag != netWeight) {
// } text = "${netWeight}g"
// } tag = netWeight
// if (weight >= PrepareCookActivity.WEIGHT_RECOGNIZE_VALUE) { }
// adapter.items.firstOrNull { it.isClicked }?.let { item -> }
// if (pendingWeight != weight) { // 入队,超过 10 条移除队头
// // 无论增减,均延迟更新,避免手拿物品时的瞬时增重干扰 if (weightWindow.size >= 10) weightWindow.removeFirst()
// cancelPendingWeightUpdate() weightWindow.addLast(netWeight)
// pendingWeight = weight // 实时更新选中食材重量
// pendingWeightRunnable = Runnable { list.getOrNull(selectedPosition)?.useWeight = netWeight
// item.useWeight = weight }
// pendingWeightRunnable = null }
// }.also { handler.postDelayed(it, 3000) } }
// }
// }
// }
// }
// }
// }
} }
/** /**
* 取消待执行的延迟重量更新任务 * 从滑动窗口尾部向前查找连续稳定值:
* 跳过尾部不稳定的过渡帧,找到第一段连续相同且大于阈值的序列
* 连续条数 >= 3 时返回该值,否则返回 null(数据不稳定)
*/ */
private fun cancelPendingWeightUpdate() { private fun getStableWeightFromWindow(): Double? {
// pendingWeightRunnable?.let { handler.removeCallbacks(it) } val threshold = PrepareFoodActivity.WEIGHT_RECOGNIZE_VALUE
// pendingWeightRunnable = null var i = weightWindow.indices.last
while (i >= 0) {
val candidate = weightWindow[i]
// 跳过低于阈值的过渡帧
if (candidate <= threshold) {
i--
continue
}
// 以 candidate 为基准,向前统计连续相同条数
var count = 0
var j = i
while (j >= 0 && weightWindow[j] == candidate) {
count++
j--
}
if (count >= 3) return candidate
// 不足 3 条,继续向前找
i = j
}
return null
} }
/** /**
* 携带当前选中食材关闭页面 * 携带当前选中食材关闭页面
* @param isManualCancel true=用户手动返回;false=被动关闭(重量低于阈值)
*/ */
private fun finishWithResult(isManualCancel: Boolean) { private fun finishWithResult(isManualCancel: Boolean) {
cancelPendingWeightUpdate() val item = list.getOrNull(selectedPosition)
if (!isManualCancel && item != null) {
// 被动关闭:从窗口取稳定值
val stableWeight = getStableWeightFromWindow()
if (stableWeight == null) {
// 数据不稳定,不关闭页面,弹窗提示
CommonDialog(this)
.setTitle("提示")
.setContent("数据不稳定,请重新放置食材后再取走")
.setPositiveButton("确认")
.show()
return
}
item.useWeight = stableWeight
}
val intent = Intent().apply { val intent = Intent().apply {
putExtra(IS_MANUAL_CANCEL, isManualCancel) putExtra(IS_MANUAL_CANCEL, isManualCancel)
if (isManualCancel.not()) { if (!isManualCancel) {
list.getOrNull(selectedPosition)?.let { item?.let { putExtra(EXTRA_SELECTED_ITEM, it) }
// 兜底:若 useWeight 仍为空或0,使用当前实时重量
if ((it.useWeight ?: 0.0) <= 0.0) {
it.useWeight = currentWeight
}
putExtra(EXTRA_SELECTED_ITEM, it)
}
} }
} }
setResult(RESULT_OK, intent) setResult(RESULT_OK, intent)
@@ -496,7 +529,6 @@ class FoodRecognizeActivity : BaseActivity() {
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
cancelPendingWeightUpdate()
WeightUtil.removeWeightListener(TAG) WeightUtil.removeWeightListener(TAG)
} }
} }
@@ -86,8 +86,8 @@ class InitActivity : BaseActivity() {
WeightUtil.getWeight() WeightUtil.getWeight()
WeightUtil.startContinuousRead() WeightUtil.startContinuousRead()
//对净材秤1和熟食秤2称进行清零 //对净材秤1和熟食秤2称进行清零
WeightUtil.tareTwo(AddressUtil.ONE) //WeightUtil.tareTwo(AddressUtil.ONE)
WeightUtil.tareTwo(AddressUtil.TWO) //WeightUtil.tareTwo(AddressUtil.TWO)
initViews() initViews()
} }
@@ -124,34 +124,41 @@ class PrepareFoodActivity : BaseActivity() {
} }
private fun addViewClickListener() { private fun addViewClickListener() {
binding.tvShowWeight.setOnClickListener { // binding.tvShowWeight.setOnClickListener {
showLoading("正在清零……") // showLoading("正在清零……")
WeightUtil.tareTwo(AddressUtil.ONE) // WeightUtil.tareTwo(AddressUtil.ONE)
delayDismissLoading() // delayDismissLoading()
} // }
// TODO: 测试功能------------------------------- // // TODO: 测试功能-------------------------------
binding.btnTestConfirm.clickWithDebounce { // binding.btnTestConfirm.clickWithDebounce {
if (currentWeight <= 0.0) return@clickWithDebounce // if (currentWeight <= 0.0) return@clickWithDebounce
val (index, item) = list.withIndex().find { (_, it) -> it.isOriginalData && it.isClicked && (it.useWeight?:0.0) <= 0.0 } ?: return@clickWithDebounce // val (index, item) = list.withIndex().find { (_, it) -> it.isOriginalData && it.isClicked && (it.useWeight?:0.0) <= 0.0 } ?: return@clickWithDebounce
item.useWeight = currentWeight // item.useWeight = currentWeight
item.isSetFinished = true // item.isSetFinished = true
list.forEachIndexed { i, it -> it.isClicked = i == index } // list.forEachIndexed { i, it -> it.isClicked = i == index }
materialAdapter.notifyDataSetChanged() // materialAdapter.notifyDataSetChanged()
} // }
// TODO: 测试功能------------------------------- // // TODO: 测试功能-------------------------------
//binding.root.setOnClickListener { hideKeyboard() } //binding.root.setOnClickListener { hideKeyboard() }
WeightUtil.addWeightListener( WeightUtil.addWeightListener(
weightKey = TAG, weightKey = TAG,
getWeight = { address, state, weight -> getWeight = { address, state, weight ->
if (address == AddressUtil.ONE && state == WeightUtil.STATE_STABLE) { if (address == AddressUtil.ONE && state == WeightUtil.STATE_STABLE) {
runOnUiThread { runOnUiThread {
// 首次收到稳定读数时,将其作为初始值(去皮基准)
if (!initialWeightReady) {
initialWeight = weight
initialWeightReady = true
}
// 净重 = 秤读数 - 初始读数
val netWeight = weight - initialWeight
binding.tvShowWeight.run { binding.tvShowWeight.run {
if (tag != weight) { if (tag != netWeight) {
text = "称重:${weight}g" text = "称重:${netWeight}g"
tag = weight tag = netWeight
} }
} }
recognizeFood(weight) recognizeFood(netWeight)
} }
} }
}) })
@@ -216,6 +223,12 @@ class PrepareFoodActivity : BaseActivity() {
private var lastWeight = 0.0 private var lastWeight = 0.0
private var currentWeight = 0.0 private var currentWeight = 0.0
/** 初始秤读数,净重 = 秤读数 - initialWeight */
private var initialWeight = 0.0
/** 初始读数是否已从秤获取 */
private var initialWeightReady = false
/** /**
* 重量稳定时触发菜品识别 * 重量稳定时触发菜品识别
* @param weight 当前稳定重量 * @param weight 当前稳定重量
@@ -257,11 +270,11 @@ class PrepareFoodActivity : BaseActivity() {
val filterValue = list.withIndex().find { (_, it) -> it.goodsId == entity.goodsId } val filterValue = list.withIndex().find { (_, it) -> it.goodsId == entity.goodsId }
if (filterValue != null) { if (filterValue != null) {
val index = filterValue.index val index = filterValue.index
val item = filterValue.value val goods = filterValue.value
item.useWeight = (item.useWeight ?: 0.0) + (entity.useWeight ?: 0.0) goods.useWeight = (goods.useWeight ?: 0.0) + (entity.useWeight ?: 0.0)
item.isSetFinished = true goods.isSetFinished = true
list.forEachIndexed { i, it -> list.forEachIndexed { i, it ->
item.isClicked = i == index it.isClicked = i == index
} }
} else { } else {
list.forEach { it.isClicked = false } list.forEach { it.isClicked = false }
@@ -325,6 +338,8 @@ class PrepareFoodActivity : BaseActivity() {
private fun getGoodsList() { private fun getGoodsList() {
if (goodsList == null) { if (goodsList == null) {
goodsList = mutableListOf() goodsList = mutableListOf()
} else {
goodsList!!.clear()
} }
// 将 GoodsItem 转换为 CookFoodGoodsEntity 并补充菜品相关字段 // 将 GoodsItem 转换为 CookFoodGoodsEntity 并补充菜品相关字段
list.filter { it.isSetFinished }.forEach { list.filter { it.isSetFinished }.forEach {
@@ -629,6 +644,7 @@ class PrepareFoodActivity : BaseActivity() {
activity = this, activity = this,
imageUri = lastPhotoUri?.toString(), imageUri = lastPhotoUri?.toString(),
currentWeight = currentWeight, currentWeight = currentWeight,
initialWeight = initialWeight,
nameScoreList = ArrayList(nameScoreList), nameScoreList = ArrayList(nameScoreList),
finishCallback = { isManualCancel, entity -> finishCallback = { isManualCancel, entity ->
Log.d( Log.d(
@@ -49,6 +49,8 @@ class SubmitFoodActivity : BaseActivity() {
/** 本地临时 foodId 前缀;以此开头说明菜品尚未在服务器登记,提交时需清空让服务器分配真实 ID */ /** 本地临时 foodId 前缀;以此开头说明菜品尚未在服务器登记,提交时需清空让服务器分配真实 ID */
const val LOCAL_ID_PREFIX = "localId_" const val LOCAL_ID_PREFIX = "localId_"
const val WEIGHT_THRESHOLD = 0.01
} }
private lateinit var binding: ActivitySubmitFoodBinding private lateinit var binding: ActivitySubmitFoodBinding
@@ -247,7 +249,7 @@ class SubmitFoodActivity : BaseActivity() {
.coerceAtLeast(0.0) .coerceAtLeast(0.0)
.roundedOneDecimalPlace() .roundedOneDecimalPlace()
if (useWeight < 0.5) { if (useWeight < WEIGHT_THRESHOLD) {
// 放回场景:必须稳定后才移除,避免拿取过程中误删 // 放回场景:必须稳定后才移除,避免拿取过程中误删
if (data.state != WeightUtil.STATE_STABLE) return@forEach if (data.state != WeightUtil.STATE_STABLE) return@forEach
if (rawWeightMap.remove(key) != null) { if (rawWeightMap.remove(key) != null) {
@@ -279,8 +281,8 @@ class SubmitFoodActivity : BaseActivity() {
val (baseGoodsId, baseWeight) = baseSeasoningMap[goodsName] ?: ("" to 0.0) val (baseGoodsId, baseWeight) = baseSeasoningMap[goodsName] ?: ("" to 0.0)
val totalWeight = (newWeight + baseWeight).roundedOneDecimalPlace() val totalWeight = (newWeight + baseWeight).roundedOneDecimalPlace()
Log.d(TAG, "refreshAdapterByName, goodsName=$goodsName, newWeight=$newWeight, baseWeight=$baseWeight, totalWeight=$totalWeight, isCooking=${food?.isCooking}") Log.d(TAG, "refreshAdapterByName, goodsName=$goodsName, newWeight=$newWeight, baseWeight=$baseWeight, totalWeight=$totalWeight, isCooking=${food?.isCooking}")
if (totalWeight < 0.5) { if (totalWeight < WEIGHT_THRESHOLD) {
Log.d(TAG, "refreshAdapterByName,重量小于0.5g, isCooking=${food?.isCooking}, goodsName=$goodsName, totalWeight=$totalWeight") Log.d(TAG, "refreshAdapterByName,重量小于${WEIGHT_THRESHOLD}g, isCooking=${food?.isCooking}, goodsName=$goodsName, totalWeight=$totalWeight")
// 仅保护 baseSeasoningMap 中的历史调料;本次会话新加的调料可以移除 // 仅保护 baseSeasoningMap 中的历史调料;本次会话新加的调料可以移除
if (!baseSeasoningMap.containsKey(goodsName)) { if (!baseSeasoningMap.containsKey(goodsName)) {
// 只有 item 确实存在时才调用 remove,避免无效遍历 // 只有 item 确实存在时才调用 remove,避免无效遍历