refactor(seasoning): 调料数据改用 dm_seasoning 存储并统一逻辑删除

- SeasoningSelectDialog/SeasoningSearchAdapter 改用 GoodsItem 替代 SeasoningEntity
- SeasoningConfigFragment 选中调料时同步 upsert 完整信息到 dm_seasoning
- SubmitFoodActivity cook/submit 按 goodsId 查 dm_seasoning 补全调料字段
- SeasoningEntity 删除 @Ignore 的 pageType/isClicked 字段
- GoodsItem.toSeasoningEntity() 补全 materialType/relateionType 等字段映射
- DAO 物理删除改为逻辑删除(isDel=1),getSeasoningByGoodsId 参数改为 String
- AppDatabase version 9→10,删除 dm_cook_food.foodId 唯一索引以支持逻辑删除
- SamplingModeActivity 从 DeviceConfigFragment 返回时刷新本地列表
- ScaleWebSocketClient 修复 scheduler 关闭后重连抛出 RejectedExecutionException

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-07 10:47:32 +08:00
co-authored by Claude Sonnet 4.6
parent 6595c4bde0
commit a64a00e099
16 changed files with 175 additions and 85 deletions
@@ -8,18 +8,18 @@ import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.shuwei.dish.match.R
import com.shuwei.dish.match.databinding.ListItemFoodBinding
import com.shuwei.dish.match.entity.SeasoningEntity
import com.shuwei.dish.match.entity.GoodsItem
/**
* 调料搜索弹窗适配器
*/
class SeasoningSearchAdapter(private var list: MutableList<SeasoningEntity>) :
BaseQuickAdapter<SeasoningEntity, SeasoningSearchAdapter.VH>(list) {
class SeasoningSearchAdapter(private var list: MutableList<GoodsItem>) :
BaseQuickAdapter<GoodsItem, SeasoningSearchAdapter.VH>(list) {
override fun onBindViewHolder(
holder: VH,
position: Int,
item: SeasoningEntity?
item: GoodsItem?
) {
holder.binding.tvGoodsInfo.run {
text = item?.goodsName
@@ -25,7 +25,7 @@ import com.shuwei.dish.match.entity.SeasoningSlotEntity
CookFoodGoodsEntity::class,
SeasoningSlotEntity::class,
],
version = 9,
version = 10,
exportSchema = true
)
abstract class AppDatabase : RoomDatabase() {
@@ -224,6 +224,14 @@ val MIGRATION_8_9 = MigrationImpl(8, 9) { db ->
db.execSQL("ALTER TABLE dm_seasoning ADD COLUMN materId TEXT DEFAULT ''")
}
/**
* 版本9→10:删除 dm_cook_food 表 foodId 唯一索引
* 改为由应用层保证同一 foodId 且 isDel=0 只有一条记录,以支持逻辑删除
*/
val MIGRATION_9_10 = MigrationImpl(9, 10) { db ->
db.execSQL("DROP INDEX IF EXISTS dm_cook_food_foodId_IDX")
}
class DatabaseProvider(private val context: Context) {
// 单例模式确保全局唯一实例‌
val instance: AppDatabase by lazy {
@@ -244,6 +252,7 @@ class DatabaseProvider(private val context: Context) {
// .addMigrations(MIGRATION_6_7)
.addMigrations(MIGRATION_7_8)
.addMigrations(MIGRATION_8_9)
.addMigrations(MIGRATION_9_10)
.build()
}
}
@@ -67,7 +67,7 @@ class DbRepository {
db.seasoningDao().getSeasoningBySort(sort)
}
suspend fun getSeasoningByGoodsId(goodsId: Int) = withContext(Dispatchers.IO) {
suspend fun getSeasoningByGoodsId(goodsId: String) = withContext(Dispatchers.IO) {
db.seasoningDao().getSeasoningByGoodsId(goodsId)
}
@@ -100,6 +100,20 @@ class DbRepository {
db.seasoningDao().insertSeasoning(item)
}
/**
* 按 goodsId 查重后 upsert:存在则更新,不存在则插入
* 确保同一个 goodsId 在 dm_seasoning 中只有一条记录
*/
suspend fun upsertSeasoningByGoodsId(item: SeasoningEntity) = withContext(Dispatchers.IO) {
val existing = db.seasoningDao().getSeasoningByGoodsId(item.goodsId)
if (existing != null) {
db.seasoningDao().updateSeasoning(item.also { it.id = existing.id })
} else {
item.id = 0
db.seasoningDao().insertSeasoning(item)
}
}
/**
* 查询指定设备是否已配置调料槽位
* @param deviceId 目标设备 ID
@@ -266,6 +266,20 @@ class DbViewModel : ViewModel() {
}
}
/**
* 按 goodsId 查重后 upsert 调料数据,确保同一 goodsId 只有一条记录
*/
suspend fun upsertSeasoningByGoodsId(entity: SeasoningEntity) {
rep.upsertSeasoningByGoodsId(entity)
}
/**
* 按 goodsId 查询调料详情
*/
suspend fun getSeasoningByGoodsId(goodsId: String): SeasoningEntity? {
return rep.getSeasoningByGoodsId(goodsId)
}
//--------------------------------------------------------------------------------------------------------
/**
@@ -25,6 +25,7 @@ interface CookFoodDao {
@Query("SELECT count(1) FROM dm_cook_food WHERE isDel = 0 AND canteenId = :canteenId AND cookMode = :cookMode")
suspend fun countCookFood(canteenId: String, cookMode: Int): Int
@Query("DELETE FROM dm_cook_food WHERE canteenId = :canteenId AND foodId = :foodId AND cookMode = :cookMode")
// @Query("DELETE FROM dm_cook_food WHERE canteenId = :canteenId AND foodId = :foodId AND cookMode = :cookMode")
@Query("UPDATE dm_cook_food SET isDel = 1 WHERE isDel = 0 AND canteenId = :canteenId AND foodId = :foodId AND cookMode = :cookMode")
suspend fun deleteCookFood(canteenId: String, foodId: String, cookMode: Int)
}
@@ -25,6 +25,7 @@ interface CookFoodGoodsDao {
@Query("SELECT * FROM dm_cook_food_goods WHERE isDel = 0 AND foodId = :foodId AND materialType = :materialType")
suspend fun getCookFoodGoodsListByType(foodId: String, materialType: Int): MutableList<CookFoodGoodsEntity>
@Query("DELETE FROM dm_cook_food_goods WHERE pid = :pid AND foodId = :foodId")
// @Query("DELETE FROM dm_cook_food_goods WHERE pid = :pid AND foodId = :foodId")
@Query("UPDATE dm_cook_food_goods SET isDel = 1 WHERE isDel = 0 AND pid = :pid AND foodId = :foodId")
suspend fun deleteCookFoodGoodsList(pid: Long, foodId: String)
}
@@ -26,7 +26,7 @@ interface SeasoningDao {
suspend fun getSeasoningBySort(sort: Int): MutableList<SeasoningEntity>?
@Query("SELECT * FROM dm_seasoning WHERE isDel = 0 AND goodsId = :goodsId")
suspend fun getSeasoningByGoodsId(goodsId: Int): SeasoningEntity?
suspend fun getSeasoningByGoodsId(goodsId: String): SeasoningEntity?
@Query("SELECT * FROM dm_seasoning WHERE isDel = 0 AND goodsId != -1 AND goodsName != '' ORDER BY sort ASC")
fun getAllStream(): MutableList<SeasoningEntity>
@@ -34,9 +34,11 @@ interface SeasoningDao {
@Query("SELECT * FROM dm_seasoning WHERE isDel = 0 AND goodsName LIKE '%' || :query || '%'")
suspend fun search(query: String): MutableList<SeasoningEntity>
@Query("DELETE FROM dm_seasoning WHERE isDel = 0 AND sort = :sort")
// @Query("DELETE FROM dm_seasoning WHERE isDel = 0 AND sort = :sort")
@Query("UPDATE dm_seasoning SET isDel = 1 WHERE isDel = 0 AND sort = :sort")
suspend fun deleteSeasoningBySort(sort: Int): Int
@Query("DELETE FROM dm_seasoning WHERE isDel = 0")
// @Query("DELETE FROM dm_seasoning WHERE isDel = 0")
@Query("UPDATE dm_seasoning SET isDel = 1 WHERE isDel = 0")
suspend fun clearAllSeasoning(): Int
}
@@ -25,7 +25,6 @@ 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.entity.GoodsItem
import com.shuwei.dish.match.entity.SeasoningEntity
import com.shuwei.dish.match.net.UiState
import com.shuwei.dish.match.utils.KeyboardUtil
import com.shuwei.dish.match.utils.ext.addOnActionSearchListener
@@ -40,7 +39,7 @@ import kotlinx.coroutines.launch
*/
class SeasoningSelectDialog(
private val activity: BaseActivity,
private val onItemSelected: (item: SeasoningEntity) -> Unit
private val onItemSelected: (item: GoodsItem) -> Unit
) : BottomSheetDialog(activity, R.style.BottomSheet) {
companion object {
@@ -49,7 +48,7 @@ class SeasoningSelectDialog(
}
private val binding = DialogSeasoningSelectBinding.inflate(LayoutInflater.from(activity))
private val list = mutableListOf<SeasoningEntity>()
private val list = mutableListOf<GoodsItem>()
private val adapter = SeasoningSearchAdapter(list).apply {
isStateViewEnable = true
setOnItemClickListener { _, v, position ->
@@ -210,7 +209,7 @@ class SeasoningSelectDialog(
}
binding.recyclerView.layoutManager = GridLayoutManager(activity, 3, GridLayoutManager.VERTICAL, false)
if (pageNo == 1) list.clear()
list.addAll(records.map { it.toSeasoningEntity() })
list.addAll(records)
adapter.notifyDataSetChanged()
val isLoadMoreEnable = records.size >= PAGE_SIZE
binding.refreshLayout.setEnableLoadMore(isLoadMoreEnable)
@@ -21,8 +21,8 @@ data class CookFoodEntity(
var createTime: String = DateTimeUtil.formatDateTime(LocalDateTime.now()),
var dinnerType: String? = "0",
) : BaseEntity {
@Ignore
var stFoodInfoConstituteList: MutableList<CookFoodGoodsEntity>? = null
// @Ignore
// var stFoodInfoConstituteList: MutableList<CookFoodGoodsEntity>? = null
@Ignore
var matchingConstituteInfoList: MutableList<CookFoodGoodsEntity>? = null
@Ignore
@@ -50,13 +50,17 @@ data class GoodsItem(
)
/**
* 转换为 SeasoningEntity,用于调料选择弹窗
* 转换为 SeasoningEntity,用于保存调料完整信息到 dm_seasoning
*/
fun toSeasoningEntity(): SeasoningEntity = SeasoningEntity(
goodsId = goodsId,
goodsName = goodsName,
popularName = popularName,
zjmCode = zjmCode,
materId = materId
materId = materId,
materialType = materialType,
relateionType = relateionType,
allEdible = allEdible,
goodsOrRelationCode = goodsOrRelationCode
)
}
@@ -1,7 +1,6 @@
package com.shuwei.dish.match.entity
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
import com.shuwei.dish.match.db.BaseEntity
import com.shuwei.dish.match.utils.DateTimeUtil
@@ -66,11 +65,5 @@ data class SeasoningEntity(
var isDel: Int = 0,
var createTime: String = DateTimeUtil.formatDateTime(LocalDateTime.now())
) : BaseEntity {
@Ignore
var pageType: Int = 0
@Ignore
var isClicked: Boolean = false
constructor() : this(goodsId = "") // 必需的空构造
}
@@ -123,8 +123,10 @@ class ScaleWebSocketClient {
* 指数退避重连
* 入口不拦截 removedDevices,仅在延迟任务执行前检查,
* 确保 disconnect() 后已入队的任务不执行,但新断线事件始终能进入重连流程
* scheduler 已关闭(shutdown 后)时直接跳过,避免 RejectedExecutionException
*/
private fun scheduleReconnect(deviceId: String, host: String, port: Int) {
if (scheduler.isShutdown) return
val delay = reconnectDelays[deviceId] ?: RECONNECT_BASE_MS
Log.d(TAG, "将在 ${delay}ms 后重连子设备: $deviceId")
@@ -176,21 +176,49 @@ class FoodRecognizeActivity : BaseActivity() {
}
currentWeight = intent.getDoubleExtra(EXTRA_CURRENT_WEIGHT, 0.0)
binding.tvWeight.text = "${currentWeight}g"
val tempList = arrayListOf<GoodsItem>()
recognizeNameList?.apply {
forEach {
val goodsName = it.name.split("WP").first()
goodsNameList.add(goodsName)
nameScoreMap.put(goodsName, it)
// TODO: 测试数据,临时使用-------------------------
val testData = buildTestData(it)
tempList.add(testData)
// TODO: 测试数据,临时使用-------------------------
}
// TODO: 测试数据,临时使用-------------------------
loadRecognizeList(tempList)
// TODO: 测试数据,临时使用-------------------------
recognizeNameList = null
}
netViewModel.queryGoodsList(
goodsType = "0",
goodsNames = goodsNameList
)
// TODO: 注释掉等接口正常再调用,临时使用测试数据
//netViewModel.queryGoodsList(goodsType = "0", goodsNames = goodsNameList)
}
private fun buildTestData(item: FoodModule.IdNameScore): GoodsItem {
var foodName = ""
var foodId = ""
item.name.split("WP").let {
foodName = it[0]
foodId = if (it.size > 1) {
"WP${it[1]}"
} else {
item.name
}
}
return GoodsItem(
goodsId = foodId,
goodsName = foodName
).also {
it.foodScore = ((1 - item.score) * 10000).toInt()
it.isOriginalData = false
}
}
private fun initObserver() {
lifecycleScope.launch {
netViewModel.goodsListState.collect { state ->
@@ -140,7 +140,10 @@ class SamplingModeActivity : BaseActivity() {
fun getCookFoodList() {
appViewModel.getCookFoodList(cookMode = 1)
val checkedId = binding.dishRadioGroup.checkedRadioButtonId
if (checkedId == R.id.rbSamplingCooking) {
appViewModel.getCookFoodList(cookMode = 1)
}
}
fun deleteCookFoodAndGoods(foodId: String, action: () -> Unit) {
@@ -154,6 +157,7 @@ class SamplingModeActivity : BaseActivity() {
super.onNewIntent(intent)
val isConfigPage = intent?.getBooleanExtra(IS_CONFIG_PAGE, false) ?: false
if (isConfigPage) {
getCookFoodList()
return
}
val isCooking = intent?.getBooleanExtra(IS_COOKING, false) ?: false
@@ -300,27 +300,6 @@ class SubmitFoodActivity : BaseActivity() {
// }
showLoading()
// 将调料数据转为 CookFoodGoodsEntity 追加到 goodsList
// val typeToken = object : TypeToken<List<CookFoodGoodsEntity>>() {}
val tempSeasoningList = seasoningData.map { item ->
CookFoodGoodsEntity().also { entity ->
entity.goodsId = item.goodsId
entity.goodsName = item.goodsName
entity.useWeight = item.useWeight
entity.materialType = 3
}
}
goodsList?.addAll(tempSeasoningList)
cookFoodEntity.let {
it.dinnerType = when (it.dinnerType) {
"早餐" -> "1"
"午餐" -> "2"
"晚餐" -> "3"
else -> it.dinnerType
}
}
val isSamplingData = food!!.cookMode == 1
if (isSamplingData) {
val tempFoodId = cookFoodEntity.foodId.ifBlank { "${System.currentTimeMillis()}" }
@@ -329,6 +308,31 @@ class SubmitFoodActivity : BaseActivity() {
goodsList?.forEach { it.foodId = cookFoodEntity.foodId }
lifecycleScope.launch {
cookFoodEntity.let {
it.dinnerType = when (it.dinnerType) {
"早餐" -> "1"
"午餐" -> "2"
"晚餐" -> "3"
else -> it.dinnerType
}
}
// 将调料数据转为 CookFoodGoodsEntity 追加到 goodsList
val tempSeasoningList = seasoningData.map { item ->
val seasoning = appViewModel.getSeasoningByGoodsId(item.goodsId)
CookFoodGoodsEntity().also { entity ->
entity.goodsId = item.goodsId
entity.goodsName = item.goodsName
entity.useWeight = item.useWeight
entity.materialType = 3
entity.popularName = seasoning?.popularName
entity.zjmCode = seasoning?.zjmCode
entity.materId = seasoning?.materId
entity.relateionType = seasoning?.relateionType ?: 0
entity.allEdible = seasoning?.allEdible ?: true
entity.goodsOrRelationCode = seasoning?.goodsOrRelationCode
}
}
goodsList?.addAll(tempSeasoningList)
appViewModel.saveCookFoodAndGoods(
cookMode = food!!.cookMode,
entity = cookFoodEntity,
@@ -347,36 +351,45 @@ class SubmitFoodActivity : BaseActivity() {
val seasoningData = seasoningAdapter.items
showLoading()
// 将重量大于0且不重复的调料追加到 goodsList
seasoningData.forEach { item ->
if (item.useWeight > 0.0 && goodsList?.none { it.goodsId == item.goodsId } == true) {
goodsList?.add(CookFoodGoodsEntity().also { entity ->
entity.goodsId = item.goodsId
entity.goodsName = item.goodsName
entity.useWeight = item.useWeight
entity.materialType = 3
})
lifecycleScope.launch {
// 将重量大于0且不重复的调料追加到 goodsList
seasoningData.forEach { item ->
if (item.useWeight > 0.0 && goodsList?.none { it.goodsId == item.goodsId } == true) {
val seasoning = appViewModel.getSeasoningByGoodsId(item.goodsId)
goodsList?.add(CookFoodGoodsEntity().also { entity ->
entity.goodsId = item.goodsId
entity.goodsName = item.goodsName
entity.useWeight = item.useWeight
entity.materialType = 3
entity.popularName = seasoning?.popularName
entity.zjmCode = seasoning?.zjmCode
entity.materId = seasoning?.materId
entity.relateionType = seasoning?.relateionType ?: 0
entity.allEdible = seasoning?.allEdible ?: true
entity.goodsOrRelationCode = seasoning?.goodsOrRelationCode
})
}
}
}
cookFoodEntity.let {
it.matchingConstituteInfoList = this@SubmitFoodActivity.goodsList
it.dinnerType = when (it.dinnerType) {
"1" -> "早餐"
"2" -> "午餐"
"3" -> "晚餐"
else -> null
cookFoodEntity.let {
it.matchingConstituteInfoList = this@SubmitFoodActivity.goodsList
it.dinnerType = when (it.dinnerType) {
"1" -> "早餐"
"2" -> "午餐"
"3" -> "晚餐"
else -> null
}
}
}
val isSamplingData = food!!.cookMode == 1
if (isSamplingData) {
cookFoodEntity.foodId = ""
goodsList?.forEach { it.foodId = "" }
}
val isSamplingData = food!!.cookMode == 1
if (isSamplingData) {
cookFoodEntity.foodId = ""
goodsList?.forEach { it.foodId = "" }
}
Log.d(TAG, "submit: json=${cookFoodEntity.toJsonString()}")
netViewModel.submitCookFood(entity = cookFoodEntity)
Log.d(TAG, "submit: json=${cookFoodEntity.toJsonString()}")
netViewModel.submitCookFood(entity = cookFoodEntity)
}
}
private fun submitSuccess(isSamplingData: Boolean) {
@@ -184,14 +184,20 @@ class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
onItemSelected = { item ->
val name = item.goodsName ?: ""
onUpdateName(name)
saveSlotAndBroadcast(
SeasoningSlotEntity(
deviceId = deviceId,
address = address,
goodsId = item.goodsId,
goodsName = name
lifecycleScope.launch {
// 保存槽位配置
currentActivity.appViewModel.upsertSeasoningSlot(
SeasoningSlotEntity(
deviceId = deviceId,
address = address,
goodsId = item.goodsId,
goodsName = name
)
)
)
// 按 goodsId 去重保存完整调料信息到 dm_seasoning
currentActivity.appViewModel.upsertSeasoningByGoodsId(item.toSeasoningEntity())
broadcastAllSlots()
}
}
).show()
}