feat: 新增 goodsCode 字段并同步迁移 DB v11,修复 IPv6 WebSocket 地址格式,重构 VectorCollectionFragment 相机回调为协程

This commit is contained in:
2026-05-09 10:51:43 +08:00
parent 7a03ab8108
commit bb1ecbde43
7 changed files with 78 additions and 50 deletions
@@ -25,7 +25,7 @@ import com.shuwei.dish.match.entity.SeasoningSlotEntity
CookFoodGoodsEntity::class, CookFoodGoodsEntity::class,
SeasoningSlotEntity::class, SeasoningSlotEntity::class,
], ],
version = 10, version = 11,
exportSchema = true exportSchema = true
) )
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
@@ -232,6 +232,14 @@ val MIGRATION_9_10 = MigrationImpl(9, 10) { db ->
db.execSQL("DROP INDEX IF EXISTS dm_cook_food_foodId_IDX") db.execSQL("DROP INDEX IF EXISTS dm_cook_food_foodId_IDX")
} }
/**
* 版本10→11:为 dm_cook_food_goods 和 dm_seasoning 表新增 goodsCode(物料编码)字段
*/
val MIGRATION_10_11 = MigrationImpl(10, 11) { db ->
db.execSQL("ALTER TABLE dm_cook_food_goods ADD COLUMN goodsCode TEXT DEFAULT ''")
db.execSQL("ALTER TABLE dm_seasoning ADD COLUMN goodsCode TEXT DEFAULT ''")
}
class DatabaseProvider(private val context: Context) { class DatabaseProvider(private val context: Context) {
// 单例模式确保全局唯一实例‌ // 单例模式确保全局唯一实例‌
val instance: AppDatabase by lazy { val instance: AppDatabase by lazy {
@@ -253,6 +261,7 @@ class DatabaseProvider(private val context: Context) {
.addMigrations(MIGRATION_7_8) .addMigrations(MIGRATION_7_8)
.addMigrations(MIGRATION_8_9) .addMigrations(MIGRATION_8_9)
.addMigrations(MIGRATION_9_10) .addMigrations(MIGRATION_9_10)
.addMigrations(MIGRATION_10_11)
.build() .build()
} }
} }
@@ -66,6 +66,9 @@ class CookFoodGoodsEntity(
var materId: String? = "", var materId: String? = "",
/** 物料编码 */
var goodsCode: String? = "",
var isDel: Int = 0, var isDel: Int = 0,
var createTime: String = DateTimeUtil.formatDateTime(LocalDateTime.now()) var createTime: String = DateTimeUtil.formatDateTime(LocalDateTime.now())
) : BaseEntity, Serializable { ) : BaseEntity, Serializable {
@@ -80,6 +83,7 @@ class CookFoodGoodsEntity(
popularName = popularName, popularName = popularName,
zjmCode = zjmCode, zjmCode = zjmCode,
materId = materId, materId = materId,
materCode = goodsCode,
materialType = materialType, materialType = materialType,
useWeight = useWeight, useWeight = useWeight,
relateionType = relateionType, relateionType = relateionType,
@@ -13,6 +13,7 @@ data class GoodsItem(
val popularName: String? = null, val popularName: String? = null,
val zjmCode: String? = null, val zjmCode: String? = null,
val materId: String? = null, val materId: String? = null,
val materCode: String? = null,
// 从 CookFoodGoodsEntity 迁移的数据字段(DishPartAdapter 显示及转换用) // 从 CookFoodGoodsEntity 迁移的数据字段(DishPartAdapter 显示及转换用)
var materialType: Int = 0, var materialType: Int = 0,
@@ -33,6 +34,11 @@ data class GoodsItem(
var foodScore: Int = -1 var foodScore: Int = -1
) : Serializable { ) : Serializable {
/**
* 物料编码,与查询接口返回的 materCode 同值,统一对外暴露为 goodsCode 供下游使用
*/
val goodsCode: String? get() = materCode
/** /**
* 转换为 CookFoodGoodsEntity,用于写入数据库或传给 SubmitFoodActivity * 转换为 CookFoodGoodsEntity,用于写入数据库或传给 SubmitFoodActivity
*/ */
@@ -42,6 +48,7 @@ data class GoodsItem(
popularName = popularName, popularName = popularName,
zjmCode = zjmCode, zjmCode = zjmCode,
materId = materId, materId = materId,
goodsCode = goodsCode,
materialType = materialType, materialType = materialType,
useWeight = useWeight, useWeight = useWeight,
relateionType = relateionType, relateionType = relateionType,
@@ -58,6 +65,7 @@ data class GoodsItem(
popularName = popularName, popularName = popularName,
zjmCode = zjmCode, zjmCode = zjmCode,
materId = materId, materId = materId,
goodsCode = goodsCode,
materialType = materialType, materialType = materialType,
relateionType = relateionType, relateionType = relateionType,
allEdible = allEdible, allEdible = allEdible,
@@ -58,6 +58,9 @@ data class SeasoningEntity(
var materId: String? = "", var materId: String? = "",
/** 物料编码 */
var goodsCode: String? = "",
var sort: Int = 9999, var sort: Int = 9999,
/** /**
* isDel表示数据是否已删除,1-已删除,为无效数据,0-正常使用,有效数据 * isDel表示数据是否已删除,1-已删除,为无效数据,0-正常使用,有效数据
@@ -86,7 +86,9 @@ class ScaleWebSocketClient {
Log.d(TAG, "放弃过期连接任务: $deviceId, version=$version") Log.d(TAG, "放弃过期连接任务: $deviceId, version=$version")
return return
} }
val url = "ws://$host:$port" // IPv6 地址在 URL 中需用方括号包裹,否则 OkHttp 会将冒号后内容误解析为端口
val formattedHost = if (host.contains(':')) "[$host]" else host
val url = "ws://$formattedHost:$port"
val request = Request.Builder().url(url).build() val request = Request.Builder().url(url).build()
val ws = httpClient.newWebSocket(request, object : WebSocketListener() { val ws = httpClient.newWebSocket(request, object : WebSocketListener() {
@@ -98,7 +98,7 @@ class SettingActivity : BaseActivity() {
.setTitle("数据清除") .setTitle("数据清除")
.setContent("将物理删除全部 4 张表数据(菜品、食材、调料、槽位),此操作不可恢复,同时重置菜品模式和食堂id,确认继续?") .setContent("将物理删除全部 4 张表数据(菜品、食材、调料、槽位),此操作不可恢复,同时重置菜品模式和食堂id,确认继续?")
.setNegativeButton("取消") .setNegativeButton("取消")
.setPositiveButton("确认清除") { .setPositiveButton("确认") {
appViewModel.clearAllData { appViewModel.clearAllData {
SpTool.cookMode = -1 SpTool.cookMode = -1
SpTool.canteenId = "0" SpTool.canteenId = "0"
@@ -14,7 +14,11 @@ import android.view.inputmethod.InputMethodManager
import androidx.camera.view.PreviewView import androidx.camera.view.PreviewView
import androidx.core.view.doOnLayout import androidx.core.view.doOnLayout
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import com.shuwei.dish.match.R import com.shuwei.dish.match.R
import com.shuwei.dish.match.adapter.VectorCollectionAdapter import com.shuwei.dish.match.adapter.VectorCollectionAdapter
import com.shuwei.dish.match.adapter.Food3Adapter import com.shuwei.dish.match.adapter.Food3Adapter
@@ -111,15 +115,16 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
//typealias CameraCallback = (Uri) -> Unit //typealias CameraCallback = (Uri) -> Unit
private val cameraCallback: (Uri) -> Unit = cameraCallback@{ uri -> private val cameraCallback: (Uri) -> Unit = { uri ->
try { lifecycleScope.launch {
val index = vectorList.indexOfFirst { it.imageFile == null } try {
if (index == -1) { val index = vectorList.indexOfFirst { it.imageFile == null }
toast("每次只允许保存${MAX_COUNT}条数据") if (index == -1) {
hideWaitingDialog() toast("每次只允许保存${MAX_COUNT}条数据")
return@cameraCallback hideWaitingDialog()
} return@launch
activity?.runOnUiThread { }
// 主线程更新 UI
vectorList[index].let { vectorList[index].let {
it.imageVector = null it.imageVector = null
it.bitmap = null it.bitmap = null
@@ -128,53 +133,50 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
it.imageUri = uri it.imageUri = uri
} }
vectorAdapter.notifyItemChanged(index) vectorAdapter.notifyItemChanged(index)
} // 切到 IO 线程执行耗时的 Bitmap 解码
// hideWaitingDialog() val bitmap = withContext(Dispatchers.IO) {
Thread { ImageUtil.uriToBitmap(requireActivity(), uri)
ImageUtil.uriToBitmap(requireActivity(), uri)?.let { bitmap ->
getImageVector(index, bitmap)
} }
}.start() bitmap?.let { getImageVector(index, it) }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
hideWaitingDialog() hideWaitingDialog()
toast("程序异常${e.message}") toast("程序异常${e.message}")
log("程序异常${e.message}") log("程序异常${e.message}")
}
} }
} }
private fun getImageVector(index: Int, bitmap: Bitmap) { private suspend fun getImageVector(index: Int, bitmap: Bitmap) {
// val bitmap = BitmapCropper.cropCenter( // 切到 IO 线程执行耗时的向量计算和文件保存
// original = srcBmp, val (imageVector, file) = withContext(Dispatchers.IO) {
// targetWidth = 900, targetHeight = 900, try {
//// offsetX = 30, offsetY = 100 val vector = FoodModule.bitmap2FloatArray(bitmap, false)
// ) val file = BitmapSaver.saveToAppFilesDir(
val imageVector = try { bitmap, requireActivity(), "IMG_CROP_${System.currentTimeMillis()}.jpg"
FoodModule.bitmap2FloatArray(bitmap, false) )
} catch (e: Exception) { log("${this.javaClass.simpleName}-cameraCallback-裁剪bitmap保存文件路径:${file?.absolutePath}")
e.printStackTrace() vector to file
} catch (e: Exception) {
e.printStackTrace()
null to null
}
}
// 回到主线程处理结果
if (imageVector == null) {
toast("操作失败") toast("操作失败")
log("操作失败:${e.message}") log("操作失败")
hideWaitingDialog() hideWaitingDialog()
return return
} }
val file = BitmapSaver.saveToAppFilesDir( vectorList[index].let {
bitmap, requireActivity(), "IMG_CROP_${System.currentTimeMillis()}.jpg" it.imageVector = imageVector
) it.bitmap = null
log("${this.javaClass.simpleName}-cameraCallback-裁剪bitmap保存文件路径:${file?.absolutePath}") it.isShowCamera = false
it.imageFile = file
activity?.runOnUiThread {
vectorList[index].let {
it.imageVector = imageVector
it.bitmap = null
it.isShowCamera = false
it.imageFile = file
}
vectorAdapter.notifyItemChanged(index)
}
if (bitmap.isRecycled.not()) {
bitmap.recycle()
} }
vectorAdapter.notifyItemChanged(index)
if (bitmap.isRecycled.not()) bitmap.recycle()
hideWaitingDialog() hideWaitingDialog()
} }