Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a769eae2a0 | ||
|
|
0241caa5fc | ||
|
|
512e0b5b4f | ||
|
|
a8ade16058 | ||
|
|
ffd00266b8 | ||
|
|
43f5434cbd | ||
|
|
76f60f8ee7 | ||
|
|
28a475ca43 | ||
|
|
16dba32eb7 | ||
|
|
5a7b090c02 | ||
|
|
b6ed47ff87 | ||
|
|
088aabc535 | ||
|
|
061680b32b | ||
|
|
5c3e51626c | ||
|
|
936bf206dc | ||
|
|
8e4dcb9f61 | ||
|
|
8ba4ba6db0 | ||
|
|
37f86e202f | ||
|
|
737baf8486 | ||
|
|
bf6b2547f3 | ||
|
|
6abd2f9b0b |
@@ -117,7 +117,7 @@ dependencies {
|
|||||||
|
|
||||||
val objectboxVersion = "5.0.1"
|
val objectboxVersion = "5.0.1"
|
||||||
debugImplementation(libs.objectbox.android.objectbrowser)
|
debugImplementation(libs.objectbox.android.objectbrowser)
|
||||||
// releaseImplementation("io.objectbox:objectbox-android:$objectboxVersion")
|
releaseImplementation("io.objectbox:objectbox-android:$objectboxVersion")
|
||||||
// implementation("io.objectbox:objectbox-fulltext:5.0.1")
|
// implementation("io.objectbox:objectbox-fulltext:5.0.1")
|
||||||
|
|
||||||
implementation(libs.androidx.recyclerview)
|
implementation(libs.androidx.recyclerview)
|
||||||
|
|||||||
@@ -22,9 +22,11 @@ object GlobalData {
|
|||||||
/**
|
/**
|
||||||
* 具体业务 BaseUrl
|
* 具体业务 BaseUrl
|
||||||
*/
|
*/
|
||||||
const val TEST_BASE_URL = "http://192.168.1.201:14801"
|
// const val LOCAL_BASE_URL = "http://192.168.1.201:14801"
|
||||||
const val UAT_BASE_URL = "https://dev.yixiong-tech.com:8083"
|
const val LOCAL_BASE_URL = "http://192.168.10.101:24801"
|
||||||
const val PROD_BASE_URL = "https://api.dm.yixiong-tech.com:8443"
|
// const val TEST_BASE_URL = "https://dev.yixiong-tech.com:8083"
|
||||||
|
const val TEST_BASE_URL = "https://dev.yixiong-tech.com:8081"
|
||||||
|
const val PROD_BASE_URL = "https://platform-api.uat.shuziweidao.com"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 菜品识别模型版本号,后边以接口返回为准
|
* 菜品识别模型版本号,后边以接口返回为准
|
||||||
@@ -71,6 +73,8 @@ object GlobalKey {
|
|||||||
const val KEY_USER_INFO = "userInfoKey"
|
const val KEY_USER_INFO = "userInfoKey"
|
||||||
const val KEY_PICKUP_MODE = "pickupMode"
|
const val KEY_PICKUP_MODE = "pickupMode"
|
||||||
const val KEY_CHARGE_MODE = "chargeMode"
|
const val KEY_CHARGE_MODE = "chargeMode"
|
||||||
|
// 餐具重量(克),取餐提交时用于从就餐重量中扣减,默认 0 表示不扣减
|
||||||
|
const val KEY_DISH_WEIGHT = "dishWeight"
|
||||||
const val KEY_SETTLEMENT_MODE = "settlementMode"
|
const val KEY_SETTLEMENT_MODE = "settlementMode"
|
||||||
const val KEY_BASE_URL = "baseUrlKey"
|
const val KEY_BASE_URL = "baseUrlKey"
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,6 @@ import com.sw.dualscreen.objbox.ObjectBox
|
|||||||
import com.sw.dualscreen.sdk.SensorScaleUtils
|
import com.sw.dualscreen.sdk.SensorScaleUtils
|
||||||
import com.sw.dualscreen.utils.ActivityManager
|
import com.sw.dualscreen.utils.ActivityManager
|
||||||
import com.sw.dualscreen.utils.CrashHandler
|
import com.sw.dualscreen.utils.CrashHandler
|
||||||
import com.sw.dualscreen.utils.SPUtil
|
|
||||||
import com.sw.dualscreen.utils.SpTool
|
import com.sw.dualscreen.utils.SpTool
|
||||||
import com.sw.plate.App
|
import com.sw.plate.App
|
||||||
import com.sw.plate.utils.AppUtil
|
import com.sw.plate.utils.AppUtil
|
||||||
@@ -38,7 +37,7 @@ class MyApp : App() {
|
|||||||
if (!savedUrl.isNullOrEmpty()) {
|
if (!savedUrl.isNullOrEmpty()) {
|
||||||
GlobalData.appBaseUrl = savedUrl
|
GlobalData.appBaseUrl = savedUrl
|
||||||
} else {
|
} else {
|
||||||
// 未保存过则使用默认逻辑:特定设备走测试环境,其余走 UAT
|
// 未保存过则使用默认逻辑:特定设备走测试环境,其余走 UAT 测试档口机:2987f0c5-5754-33e9-b00a-251db5e2e55f
|
||||||
GlobalData.appBaseUrl =
|
GlobalData.appBaseUrl =
|
||||||
if ("2987f0c5-5754-33e9-b00a-251db5e2e55f" == deviceId) GlobalData.TEST_BASE_URL else GlobalData.PROD_BASE_URL
|
if ("2987f0c5-5754-33e9-b00a-251db5e2e55f" == deviceId) GlobalData.TEST_BASE_URL else GlobalData.PROD_BASE_URL
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import androidx.activity.viewModels
|
|||||||
import androidx.core.widget.addTextChangedListener
|
import androidx.core.widget.addTextChangedListener
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.sw.dualscreen.GlobalData
|
|
||||||
import com.sw.dualscreen.R
|
import com.sw.dualscreen.R
|
||||||
import com.sw.dualscreen.adapter.CollectedFoodNewAdapter
|
import com.sw.dualscreen.adapter.CollectedFoodNewAdapter
|
||||||
import com.sw.dualscreen.databinding.ActivityCollectedFoodBinding
|
import com.sw.dualscreen.databinding.ActivityCollectedFoodBinding
|
||||||
@@ -14,10 +13,10 @@ import com.sw.dualscreen.ext.addOnActionSearchListener
|
|||||||
import com.sw.dualscreen.ext.gone
|
import com.sw.dualscreen.ext.gone
|
||||||
import com.sw.dualscreen.ext.hideKeyboard
|
import com.sw.dualscreen.ext.hideKeyboard
|
||||||
import com.sw.dualscreen.ext.visible
|
import com.sw.dualscreen.ext.visible
|
||||||
import com.sw.dualscreen.objbox.CollectedFoodInfo
|
import com.sw.dualscreen.model.response.v2.CollectedFoodV2
|
||||||
import com.sw.dualscreen.objbox.ObjectBox
|
import com.sw.dualscreen.objbox.ObjectBox
|
||||||
import com.sw.dualscreen.viewmodel.BaseViewModel
|
import com.sw.dualscreen.viewmodel.BaseViewModel
|
||||||
import com.sw.dualscreen.viewmodel.UserViewModel
|
import com.sw.dualscreen.viewmodel.NetViewModelV2
|
||||||
import com.sw.plate.utils.ToastUtils
|
import com.sw.plate.utils.ToastUtils
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -27,7 +26,7 @@ class CollectedFoodActivity : BaseActivity<ActivityCollectedFoodBinding>() {
|
|||||||
private const val PAGE_SIZE = 100
|
private const val PAGE_SIZE = 100
|
||||||
}
|
}
|
||||||
|
|
||||||
private val viewModel by viewModels<UserViewModel>()
|
private val viewModel by viewModels<NetViewModelV2>()
|
||||||
override fun getViewModel(): BaseViewModel {
|
override fun getViewModel(): BaseViewModel {
|
||||||
return viewModel
|
return viewModel
|
||||||
}
|
}
|
||||||
@@ -36,7 +35,7 @@ class CollectedFoodActivity : BaseActivity<ActivityCollectedFoodBinding>() {
|
|||||||
return ActivityCollectedFoodBinding.inflate(layoutInflater)
|
return ActivityCollectedFoodBinding.inflate(layoutInflater)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val list: MutableList<CollectedFoodInfo> = mutableListOf()
|
private val list: MutableList<CollectedFoodV2> = mutableListOf()
|
||||||
private val adapter by lazy {
|
private val adapter by lazy {
|
||||||
CollectedFoodNewAdapter(list).apply {
|
CollectedFoodNewAdapter(list).apply {
|
||||||
// isStateViewEnable = true
|
// isStateViewEnable = true
|
||||||
@@ -127,19 +126,26 @@ class CollectedFoodActivity : BaseActivity<ActivityCollectedFoodBinding>() {
|
|||||||
// loadEmptyView()
|
// loadEmptyView()
|
||||||
// }
|
// }
|
||||||
|
|
||||||
viewModel.getCollectedFoodList(
|
viewModel.getCollectPage(
|
||||||
pageNo = pageNo,
|
pageNum = pageNo.toLong(),
|
||||||
pageSize = PAGE_SIZE,
|
pageSize = PAGE_SIZE.toLong(),
|
||||||
foodName = searchName ?: ""
|
foodName = searchName,
|
||||||
) { items ->
|
onSuccess = { items ->
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
loadFoodList(items)
|
loadFoodList(items)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onFailure = {
|
||||||
|
runOnUiThread {
|
||||||
|
finishRefresh()
|
||||||
|
ToastUtils.showToast(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
private fun loadFoodList(items: List<CollectedFoodInfo>) {
|
private fun loadFoodList(items: List<CollectedFoodV2>) {
|
||||||
finishRefresh()
|
finishRefresh()
|
||||||
if (pageNo == 1 && items.isEmpty()) {
|
if (pageNo == 1 && items.isEmpty()) {
|
||||||
list.clear()
|
list.clear()
|
||||||
@@ -191,7 +197,7 @@ class CollectedFoodActivity : BaseActivity<ActivityCollectedFoodBinding>() {
|
|||||||
private fun deleteGoods(position: Int) {
|
private fun deleteGoods(position: Int) {
|
||||||
val food = list[position]
|
val food = list[position]
|
||||||
showWaitingDialog("加载中……")
|
showWaitingDialog("加载中……")
|
||||||
viewModel.deleteCollectFood(food.foodId, GlobalData.foodModelVersion) { deleteSuccess ->
|
viewModel.deleteCollect(food.foodId, food.version) { deleteSuccess ->
|
||||||
if (deleteSuccess) {
|
if (deleteSuccess) {
|
||||||
// Thread {}.start()
|
// Thread {}.start()
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import com.sw.dualscreen.utils.L
|
|||||||
import com.sw.dualscreen.utils.NetworkUtils
|
import com.sw.dualscreen.utils.NetworkUtils
|
||||||
import com.sw.dualscreen.utils.SpTool
|
import com.sw.dualscreen.utils.SpTool
|
||||||
import com.sw.dualscreen.viewmodel.BaseViewModel
|
import com.sw.dualscreen.viewmodel.BaseViewModel
|
||||||
import com.sw.dualscreen.viewmodel.UserViewModel
|
import com.sw.dualscreen.viewmodel.NetViewModelV2
|
||||||
import com.sw.plate.utils.AppUtil
|
import com.sw.plate.utils.AppUtil
|
||||||
import com.sw.plate.utils.ToastUtils
|
import com.sw.plate.utils.ToastUtils
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -29,7 +29,7 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
|
|||||||
const val PAGE_SIZE = 100
|
const val PAGE_SIZE = 100
|
||||||
}
|
}
|
||||||
|
|
||||||
private val userViewModel by viewModels<UserViewModel>()
|
private val userViewModel by viewModels<NetViewModelV2>()
|
||||||
private var pageNum = 1
|
private var pageNum = 1
|
||||||
|
|
||||||
override fun getViewModel(): BaseViewModel {
|
override fun getViewModel(): BaseViewModel {
|
||||||
@@ -65,21 +65,6 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
|
|||||||
}
|
}
|
||||||
showWaitingDialog("网络连接中...")
|
showWaitingDialog("网络连接中...")
|
||||||
checkNetworkTimer?.start()
|
checkNetworkTimer?.start()
|
||||||
|
|
||||||
// val isSuccess = viewModel.checkEquipmentInfo()
|
|
||||||
// if (isSuccess) {
|
|
||||||
// goMainActivity()
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// binding.imgQr.setImageBitmap(
|
|
||||||
// QRCodeUtil.generateQRCode(
|
|
||||||
// content = GlobalData.deviceId,
|
|
||||||
// size = 200
|
|
||||||
// )
|
|
||||||
// )
|
|
||||||
// binding.initButton.setOnClickListener {
|
|
||||||
// viewModel.getDeviceToken()
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun registerDataChange() {
|
override fun registerDataChange() {
|
||||||
@@ -100,12 +85,12 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
|
|||||||
getCollectedFoodVector {
|
getCollectedFoodVector {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
hideWaitingDialog()
|
hideWaitingDialog()
|
||||||
binding.root.postDelayed({
|
|
||||||
checkCameraPermissionAndGo()
|
|
||||||
}, 1000)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
binding.root.postDelayed({
|
||||||
|
checkCameraPermissionAndGo()
|
||||||
|
}, 1000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,24 +173,34 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDeviceConfig() {
|
// var arcsoftAppId = "Hkz1rBk6PZXbS8KwKr67K2eZtsz8bRoMHLg64bUdgCZj"
|
||||||
userViewModel.getDeviceConfig { deviceConfig ->
|
// var arcsoftSdkKey = "AabXs3sHM8UhhE7oCGf4LVMLrdkGb1nJNksbjjTdVt7k"
|
||||||
runOnUiThread {
|
// var arcsoftActiveKey = "085F-118G-Q4GH-B2YH"
|
||||||
if (deviceConfig == null) {
|
|
||||||
ToastUtils.showToast("获取设备配置数据失败")
|
|
||||||
return@runOnUiThread
|
|
||||||
}
|
|
||||||
//var settlementMode = 1
|
|
||||||
//结算模式:1-独立支付,2-联合结算
|
|
||||||
val settlementMode = if (deviceConfig.payType == 2) 0 else 1
|
|
||||||
// TODO: 2022/10/13 修改为独立支付
|
|
||||||
SpTool.settlementMode = settlementMode
|
|
||||||
|
|
||||||
GlobalData.appId = deviceConfig.arcsoftAppId ?: ""
|
private fun getDeviceConfig() {
|
||||||
GlobalData.sdkKey = deviceConfig.arcsoftSdkKey ?: ""
|
userViewModel.getDeviceConfig(
|
||||||
GlobalData.activeKey = deviceConfig.arcsoftActiveKey ?: ""
|
onSuccess = { deviceConfig ->
|
||||||
}
|
runOnUiThread {
|
||||||
}
|
if (deviceConfig == null) {
|
||||||
|
ToastUtils.showToast("获取设备配置数据失败")
|
||||||
|
return@runOnUiThread
|
||||||
|
}
|
||||||
|
val settlementMode = if (deviceConfig.payType == 2) 0 else 1
|
||||||
|
SpTool.settlementMode = settlementMode
|
||||||
|
|
||||||
|
// // TODO: 临时写死用于测试----------------------------------------
|
||||||
|
// deviceConfig.arcsoftAppId = arcsoftAppId
|
||||||
|
// deviceConfig.arcsoftSdkKey = arcsoftSdkKey
|
||||||
|
// deviceConfig.arcsoftActiveKey = arcsoftActiveKey
|
||||||
|
// // TODO: 临时写死用于测试----------------------------------------
|
||||||
|
|
||||||
|
GlobalData.appId = deviceConfig.arcsoftAppId ?: ""
|
||||||
|
GlobalData.sdkKey = deviceConfig.arcsoftSdkKey ?: ""
|
||||||
|
GlobalData.activeKey = deviceConfig.arcsoftActiveKey ?: ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onFailure = { runOnUiThread { ToastUtils.showToast(it) } }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadNetworkErrorDialog() {
|
private fun loadNetworkErrorDialog() {
|
||||||
@@ -239,4 +234,27 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
|
|||||||
checkNetworkTimer?.cancel()
|
checkNetworkTimer?.cancel()
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// private fun loadTestDb() {
|
||||||
|
// // 从测试库导入人脸数据
|
||||||
|
// FaceDbImporter.importFromAssets(this, object : FaceDbImporter.ImportCallback {
|
||||||
|
// public override fun onProgress(current: Int, total: Int) {
|
||||||
|
// showWaitingDialog("导入人脸数据 " + current + "/" + total)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// public override fun onError(message: String?) {
|
||||||
|
// hideWaitingDialog()
|
||||||
|
// ToastUtils.showToast("离线数据导入失败:" + message)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// public override fun onComplete() {
|
||||||
|
// hideWaitingDialog()
|
||||||
|
// ToastUtils.showToast("离线数据导入成功")
|
||||||
|
// // 后续流程会自动跳转
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -44,11 +44,13 @@ import com.sw.dualscreen.ext.load
|
|||||||
import com.sw.dualscreen.ext.visible
|
import com.sw.dualscreen.ext.visible
|
||||||
import com.sw.dualscreen.model.response.ChargeModeEvent
|
import com.sw.dualscreen.model.response.ChargeModeEvent
|
||||||
import com.sw.dualscreen.model.response.FoodInfo
|
import com.sw.dualscreen.model.response.FoodInfo
|
||||||
import com.sw.dualscreen.model.response.FoodOrder
|
import com.sw.dualscreen.model.request.v2.PlaceOrderRequest
|
||||||
import com.sw.dualscreen.model.response.PaySuccessEvent
|
import com.sw.dualscreen.model.response.PaySuccessEvent
|
||||||
import com.sw.dualscreen.model.response.ResetRecognizeEvent
|
import com.sw.dualscreen.model.response.ResetRecognizeEvent
|
||||||
import com.sw.dualscreen.model.response.UpdateRefreshEvent
|
import com.sw.dualscreen.model.response.UpdateRefreshEvent
|
||||||
import com.sw.dualscreen.model.response.UserFaceModel
|
import com.sw.dualscreen.model.response.v2.FaceVO
|
||||||
|
import com.sw.dualscreen.model.response.v2.NewFoodInfo
|
||||||
|
import com.sw.dualscreen.model.response.v2.toFoodInfo
|
||||||
import com.sw.dualscreen.objbox.Food
|
import com.sw.dualscreen.objbox.Food
|
||||||
import com.sw.dualscreen.objbox.FoodModule
|
import com.sw.dualscreen.objbox.FoodModule
|
||||||
import com.sw.dualscreen.objbox.FoodModule.IdNameScore
|
import com.sw.dualscreen.objbox.FoodModule.IdNameScore
|
||||||
@@ -67,7 +69,7 @@ import com.sw.dualscreen.utils.SPUtil
|
|||||||
import com.sw.dualscreen.utils.SpTool
|
import com.sw.dualscreen.utils.SpTool
|
||||||
import com.sw.dualscreen.view.CustomBottomSheetDialog
|
import com.sw.dualscreen.view.CustomBottomSheetDialog
|
||||||
import com.sw.dualscreen.viewmodel.BaseViewModel
|
import com.sw.dualscreen.viewmodel.BaseViewModel
|
||||||
import com.sw.dualscreen.viewmodel.UserViewModel
|
import com.sw.dualscreen.viewmodel.NetViewModelV2
|
||||||
import com.sw.plate.utils.Base64
|
import com.sw.plate.utils.Base64
|
||||||
import com.sw.plate.utils.LightManager
|
import com.sw.plate.utils.LightManager
|
||||||
import com.sw.plate.utils.ToastUtils
|
import com.sw.plate.utils.ToastUtils
|
||||||
@@ -81,8 +83,6 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.RequestBody
|
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
|
||||||
import org.greenrobot.eventbus.Subscribe
|
import org.greenrobot.eventbus.Subscribe
|
||||||
import org.greenrobot.eventbus.ThreadMode
|
import org.greenrobot.eventbus.ThreadMode
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
@@ -107,9 +107,11 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
|||||||
|
|
||||||
const val WEIGHT_CHANGE_VALUE = 20
|
const val WEIGHT_CHANGE_VALUE = 20
|
||||||
const val WEIGHT_RESET_RECOGNIZE = 10
|
const val WEIGHT_RESET_RECOGNIZE = 10
|
||||||
|
// 档口按重量计费的最小计费重量(克),净重超过此值才计费/下单
|
||||||
|
const val MIN_BILLABLE_WEIGHT = 10
|
||||||
}
|
}
|
||||||
|
|
||||||
private val viewModel by viewModels<UserViewModel>()
|
private val viewModel by viewModels<NetViewModelV2>()
|
||||||
private var imageCapture: ImageCapture? = null
|
private var imageCapture: ImageCapture? = null
|
||||||
var isAnalyzing = true // 控制是否进行图像分析
|
var isAnalyzing = true // 控制是否进行图像分析
|
||||||
|
|
||||||
@@ -136,6 +138,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
var isSwitchFood = false
|
var isSwitchFood = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用于切换菜品时重置数据
|
* 用于切换菜品时重置数据
|
||||||
*/
|
*/
|
||||||
@@ -147,11 +150,11 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
|||||||
//联合支付,切换菜品重新请求接口
|
//联合支付,切换菜品重新请求接口
|
||||||
presentation?.let {
|
presentation?.let {
|
||||||
val userId = presentation?.currentUserId
|
val userId = presentation?.currentUserId
|
||||||
if(userId.isNullOrBlank()) {
|
if (userId.isNullOrBlank()) {
|
||||||
//切换菜品时,用户已离开重新打开识别功能
|
//切换菜品时,用户已离开重新打开识别功能
|
||||||
it.pauseCamera()
|
it.pauseCamera()
|
||||||
it.step2FaceRecognizing(foodInfo, true)
|
it.step2FaceRecognizing(foodInfo, true)
|
||||||
} else{
|
} else {
|
||||||
//切换菜品时,用户未离开,重新请求接口
|
//切换菜品时,用户未离开,重新请求接口
|
||||||
it.step3ShowRecognizeResult(userId)
|
it.step3ShowRecognizeResult(userId)
|
||||||
}
|
}
|
||||||
@@ -177,6 +180,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
|||||||
private var lastPhotoUri: Uri? = null // 最后拍照的图片
|
private var lastPhotoUri: Uri? = null // 最后拍照的图片
|
||||||
private var debouncer = Debouncer(2000)
|
private var debouncer = Debouncer(2000)
|
||||||
private var isRecognitionFood = true
|
private var isRecognitionFood = true
|
||||||
|
private var isRecognizing = false
|
||||||
private var isFirstOpen = true
|
private var isFirstOpen = true
|
||||||
|
|
||||||
private var isStartRecognize = false
|
private var isStartRecognize = false
|
||||||
@@ -208,7 +212,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
|||||||
addBackEventListener()
|
addBackEventListener()
|
||||||
initView()
|
initView()
|
||||||
if (SpTool.firstGetFace) {
|
if (SpTool.firstGetFace) {
|
||||||
viewModel.getUserFaceCache(pageNo = 1)
|
viewModel.getFacePage(pageNum = 1)
|
||||||
}
|
}
|
||||||
setupCamera()
|
setupCamera()
|
||||||
initData()
|
initData()
|
||||||
@@ -273,7 +277,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
|||||||
userViewModel = viewModel,
|
userViewModel = viewModel,
|
||||||
lifecycleScope = lifecycleScope,
|
lifecycleScope = lifecycleScope,
|
||||||
successBlock = {
|
successBlock = {
|
||||||
viewModel.getUserFaceCache(pageNo = 1)
|
hideWaitingDialog()
|
||||||
},
|
},
|
||||||
failureBlock = {
|
failureBlock = {
|
||||||
hideWaitingDialog()
|
hideWaitingDialog()
|
||||||
@@ -281,7 +285,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
viewModel.getUserFaceCache(pageNo = 1, onSuccess = {
|
viewModel.getFacePage(pageNum = 1, onSuccess = {
|
||||||
recognizeViewModel.refreshFaceList()
|
recognizeViewModel.refreshFaceList()
|
||||||
hideWaitingDialog()
|
hideWaitingDialog()
|
||||||
ToastUtils.showToast("人脸数据已更新")
|
ToastUtils.showToast("人脸数据已更新")
|
||||||
@@ -376,7 +380,9 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
|||||||
log("registerDataChange weight 联合支付-余量计量")
|
log("registerDataChange weight 联合支付-余量计量")
|
||||||
if (weight <= WEIGHT_RESET_RECOGNIZE) {
|
if (weight <= WEIGHT_RESET_RECOGNIZE) {
|
||||||
log("registerDataChange weight 联合支付-余量计量,重量小于${WEIGHT_RESET_RECOGNIZE}g,step1FoodRecognizing")
|
log("registerDataChange weight 联合支付-余量计量,重量小于${WEIGHT_RESET_RECOGNIZE}g,step1FoodRecognizing")
|
||||||
if (presentation?.currentUserId.isNullOrBlank().not() && presentation!!.jointPaymentQueryFinish) {
|
if (presentation?.currentUserId.isNullOrBlank()
|
||||||
|
.not() && presentation!!.jointPaymentQueryFinish
|
||||||
|
) {
|
||||||
//联合支付+余量计量,已识别人脸的状态下,从秤上拿餐品后,不再读取数据,等待人脸离开提交订单
|
//联合支付+余量计量,已识别人脸的状态下,从秤上拿餐品后,不再读取数据,等待人脸离开提交订单
|
||||||
if (lastWeight != weight) {
|
if (lastWeight != weight) {
|
||||||
presentation?.updateWeight(weight)
|
presentation?.updateWeight(weight)
|
||||||
@@ -432,7 +438,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
|||||||
val mealPickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0
|
val mealPickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0
|
||||||
//0-计费,1-不计费
|
//0-计费,1-不计费
|
||||||
val chargeMode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0)
|
val chargeMode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0)
|
||||||
if(mealPickupMode == 0 && chargeMode == 1) {
|
if (mealPickupMode == 0 && chargeMode == 1) {
|
||||||
//即放即取+不计费模式,
|
//即放即取+不计费模式,
|
||||||
if (presentation?.currentUserId.isNullOrBlank().not()) {
|
if (presentation?.currentUserId.isNullOrBlank().not()) {
|
||||||
//已识别人脸的状态下,从秤上拿餐品后,不再读取数据,等待人脸离开提交订单
|
//已识别人脸的状态下,从秤上拿餐品后,不再读取数据,等待人脸离开提交订单
|
||||||
@@ -517,6 +523,15 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
|||||||
private var startTime = 0L
|
private var startTime = 0L
|
||||||
|
|
||||||
fun recognizeFood() {
|
fun recognizeFood() {
|
||||||
|
// 防重入:识别进行中则跳过,避免重量变化触发多次并发识别
|
||||||
|
if (isRecognizing) {
|
||||||
|
log("recognizeFood 已在识别中,跳过重复调用")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isRecognizing = true
|
||||||
|
// 立即更新基准重量,缩小后续重量变化触发识别的窗口
|
||||||
|
lastWeight = currentWeight
|
||||||
|
|
||||||
startTime = System.currentTimeMillis()
|
startTime = System.currentTimeMillis()
|
||||||
// LightManager.openRedLight()
|
// LightManager.openRedLight()
|
||||||
LightManager.closeRedLight()
|
LightManager.closeRedLight()
|
||||||
@@ -542,6 +557,8 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
|||||||
shutdownCamera()
|
shutdownCamera()
|
||||||
setupCamera()
|
setupCamera()
|
||||||
|
|
||||||
|
// 释放识别锁,允许递归重试时重新获取
|
||||||
|
isRecognizing = false
|
||||||
binding.root.postDelayed({
|
binding.root.postDelayed({
|
||||||
recognizeFood()
|
recognizeFood()
|
||||||
}, 1000)
|
}, 1000)
|
||||||
@@ -565,6 +582,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
|||||||
log("registerDataChange识别后查询接口数据:$queryData,识别数据:$recData")
|
log("registerDataChange识别后查询接口数据:$queryData,识别数据:$recData")
|
||||||
if (list.isEmpty()) {
|
if (list.isEmpty()) {
|
||||||
foodRecognizeState = false
|
foodRecognizeState = false
|
||||||
|
isRecognizing = false
|
||||||
tvToSearch.let {
|
tvToSearch.let {
|
||||||
it.text = "未查询到,手动搜索"
|
it.text = "未查询到,手动搜索"
|
||||||
it.visible()
|
it.visible()
|
||||||
@@ -572,8 +590,9 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
foodRecognizeState = true
|
foodRecognizeState = true
|
||||||
|
isRecognizing = false
|
||||||
//查询到菜品信息,同步设置lastWeight=currentWeight
|
//查询到菜品信息,同步设置lastWeight=currentWeight
|
||||||
lastWeight=currentWeight
|
lastWeight = currentWeight
|
||||||
presentation?.updateWeight(currentWeight)
|
presentation?.updateWeight(currentWeight)
|
||||||
|
|
||||||
list.forEach { foodInfo ->
|
list.forEach { foodInfo ->
|
||||||
@@ -787,6 +806,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
|||||||
checkedItem = null
|
checkedItem = null
|
||||||
lastWeight = 0
|
lastWeight = 0
|
||||||
foodRecognizeState = true
|
foodRecognizeState = true
|
||||||
|
isRecognizing = false
|
||||||
isSwitchFood = false
|
isSwitchFood = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -894,63 +914,61 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
|||||||
val chargeMode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0)
|
val chargeMode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0)
|
||||||
//0-即放即取,1-余量计量
|
//0-即放即取,1-余量计量
|
||||||
val pickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0
|
val pickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0
|
||||||
// if (pickupMode == 0) {
|
// 按重量计费:净重不足最小计费重量时,提示并拦截下单
|
||||||
// if (foodWeight <= 5) {
|
if (eatWeight < MIN_BILLABLE_WEIGHT) {
|
||||||
// ToastUtils.showToast("餐品重量不足")
|
ToastUtils.showToast("重量不足,无法下单")
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// if (eatWeight <= 5) {
|
|
||||||
// ToastUtils.showToast("取餐重量过小")
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
if (eatNum <= 0) {
|
|
||||||
ToastUtils.showToast("您好,当前重量不足一份")
|
|
||||||
notEnoughOneBlock()
|
notEnoughOneBlock()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
showWaitingDialog("加载中,请稍候……")
|
showWaitingDialog("加载中,请稍候……")
|
||||||
val order = FoodOrder(
|
val request = PlaceOrderRequest(
|
||||||
deviceId = GlobalData.deviceId,
|
deviceId = GlobalData.deviceId,
|
||||||
foodId = foodInfo.foodId,
|
foodId = foodInfo.foodId.toLong(),
|
||||||
foodName = foodInfo.foodName ?: "",
|
foodName = foodInfo.foodName ?: "",
|
||||||
foodMaterialId = foodInfo.foodMaterialId ?: "",
|
foodMaterialId = foodInfo.foodMaterialId?.toLongOrNull(),
|
||||||
specId = foodInfo.specId ?: "",
|
specId = foodInfo.specId?.toLongOrNull(),
|
||||||
foodWeight = foodWeight,
|
foodWeight = foodWeight.toBigDecimal(),
|
||||||
eatWeight = eatWeight,
|
eatWeight = eatWeight.toBigDecimal(),
|
||||||
//根据specId对应规格重量计算
|
|
||||||
eatNum = eatNum,
|
eatNum = eatNum,
|
||||||
userId = userId,
|
userId = userId?.toLongOrNull(),
|
||||||
notPay = chargeMode != 0,
|
notPay = chargeMode != 0,
|
||||||
//即放即取-1,称重-2
|
//即放即取-1,称重-2
|
||||||
mode = if (pickupMode == 1) 1 else 2,
|
mode = if (pickupMode == 1) 1 else 2,
|
||||||
paymentFrom = if (settlementMode == 0) 1 else 2,
|
paymentFrom = if (settlementMode == 0) 1 else 2,
|
||||||
member = isMember
|
member = isMember,
|
||||||
|
cookOrderId = foodInfo.cookOrderId
|
||||||
)
|
)
|
||||||
submitOrder(order, successBlock)
|
submitOrder(request, successBlock)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun submitOrder(order: FoodOrder, successBlock: () -> Unit) {
|
private fun submitOrder(request: PlaceOrderRequest, successBlock: () -> Unit) {
|
||||||
viewModel.createOrder(order) { orderId ->
|
viewModel.placeOrder(
|
||||||
runOnUiThread {
|
orderRequest = request,
|
||||||
binding.root.postDelayed({
|
onSuccess = { orderId ->
|
||||||
hideWaitingDialog()
|
runOnUiThread {
|
||||||
if (orderId.isBlank()) {
|
binding.root.postDelayed({
|
||||||
//ToastUtils.showToast("订单id为空")
|
hideWaitingDialog()
|
||||||
return@postDelayed
|
if (orderId.isNullOrBlank()) {
|
||||||
}
|
return@postDelayed
|
||||||
foodOrderId = orderId
|
|
||||||
successBlock()
|
|
||||||
|
|
||||||
if (checkedItem?.isFromSearch == true) {
|
|
||||||
//当前菜名为手动搜索选择,非识别结果,保存向量数据
|
|
||||||
lifecycleScope.launch {
|
|
||||||
saveFoodVector(checkedItem!!)
|
|
||||||
}
|
}
|
||||||
}
|
foodOrderId = orderId
|
||||||
}, 300)
|
successBlock()
|
||||||
|
|
||||||
|
if (checkedItem?.isFromSearch == true) {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
saveFoodVector(checkedItem!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onFailure = { errorMsg ->
|
||||||
|
runOnUiThread {
|
||||||
|
hideWaitingDialog()
|
||||||
|
ToastUtils.showToast(errorMsg)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getEatNum(realWeight: Double?, specWeight: Double?): Int {
|
fun getEatNum(realWeight: Double?, specWeight: Double?): Int {
|
||||||
@@ -960,7 +978,15 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
|||||||
}
|
}
|
||||||
val eatNum = (realWeight / specWeight).roundToInt()
|
val eatNum = (realWeight / specWeight).roundToInt()
|
||||||
log("getEatNum:eatNum=$eatNum,realWeight=$realWeight,specWeight=$specWeight,realWeight/$specWeight=${realWeight / specWeight}}")
|
log("getEatNum:eatNum=$eatNum,realWeight=$realWeight,specWeight=$specWeight,realWeight/$specWeight=${realWeight / specWeight}}")
|
||||||
return eatNum;
|
return if (eatNum < 1) 1 else eatNum;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取已配置的餐具重量(克)。
|
||||||
|
* 用于取餐提交时从就餐重量中扣减,默认 0 表示未配置、不扣减。
|
||||||
|
*/
|
||||||
|
internal fun getDishWeight(): Int {
|
||||||
|
return SPUtil.getInstance().get(GlobalKey.KEY_DISH_WEIGHT, 0) ?: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
private var foodOrderId: String = ""
|
private var foodOrderId: String = ""
|
||||||
@@ -1039,6 +1065,8 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
|||||||
val mealPickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0
|
val mealPickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0
|
||||||
//0-计费,1-不计费
|
//0-计费,1-不计费
|
||||||
val chargeMode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0) ?: 0
|
val chargeMode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0) ?: 0
|
||||||
|
// 餐具重量(克),提交就餐数据时从就餐重量中扣减
|
||||||
|
val dishWeight = getDishWeight()
|
||||||
var foodWeight = 0
|
var foodWeight = 0
|
||||||
var eatWeight = 0
|
var eatWeight = 0
|
||||||
if (chargeMode == 0) {
|
if (chargeMode == 0) {
|
||||||
@@ -1064,6 +1092,10 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
|||||||
recognizeWeight - currentWeight
|
recognizeWeight - currentWeight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 扣减餐具重量(仅影响就餐重量与份数,菜品原始重量 foodWeight 保持不变),不足则归零
|
||||||
|
val rawEatWeight = eatWeight
|
||||||
|
eatWeight = (rawEatWeight - dishWeight).coerceAtLeast(0)
|
||||||
|
log("clickPayButton dishWeight=$dishWeight, rawEatWeight=$rawEatWeight, deductedEatWeight=$eatWeight")
|
||||||
val eatNum = getEatNum(eatWeight.toDouble(), checkedItem!!.specWeight)
|
val eatNum = getEatNum(eatWeight.toDouble(), checkedItem!!.specWeight)
|
||||||
createOrder(
|
createOrder(
|
||||||
foodInfo = checkedItem!!,
|
foodInfo = checkedItem!!,
|
||||||
@@ -1091,9 +1123,11 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
|||||||
if (lastPhotoUri == null) {
|
if (lastPhotoUri == null) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
uri2File(lastPhotoUri!!) { imageFile, imageVector ->
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
runBlocking {
|
uri2File(lastPhotoUri!!) { imageFile, imageVector ->
|
||||||
uploadCollectFoodPics(foodInfo, imageFile, imageVector)
|
runBlocking {
|
||||||
|
uploadCollectFoodPics(foodInfo, imageFile, imageVector)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -1111,14 +1145,12 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
|||||||
log("是否null判断,imageFile == null:${imageFile == null},imageVector == null:${imageVector == null}")
|
log("是否null判断,imageFile == null:${imageFile == null},imageVector == null:${imageVector == null}")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val foodId = foodInfo.foodId
|
val foodId = foodInfo.foodId.toLongOrNull() ?: run {
|
||||||
|
loadRemindDialog("foodId 格式错误: ${foodInfo.foodId}")
|
||||||
|
return
|
||||||
|
}
|
||||||
val foodName = foodInfo.foodName ?: ""
|
val foodName = foodInfo.foodName ?: ""
|
||||||
val foodModelVersion = GlobalData.foodModelVersion
|
val foodModelVersion = GlobalData.foodModelVersion
|
||||||
val params = HashMap<String, RequestBody>()
|
|
||||||
params["foodId"] = foodId.toRequestBody()
|
|
||||||
params["foodName"] = foodName.toRequestBody()
|
|
||||||
params["version"] = foodModelVersion.toRequestBody()
|
|
||||||
val files = listOf(imageFile)
|
|
||||||
val vectors = listOf(imageVector)
|
val vectors = listOf(imageVector)
|
||||||
val foodVectorList = vectors.map {
|
val foodVectorList = vectors.map {
|
||||||
it.joinToString(
|
it.joinToString(
|
||||||
@@ -1128,8 +1160,13 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
|||||||
val foodVectorJson =
|
val foodVectorJson =
|
||||||
foodVectorList.joinToString(separator = ",", prefix = "[", postfix = "]")
|
foodVectorList.joinToString(separator = ",", prefix = "[", postfix = "]")
|
||||||
log("json=$foodVectorJson")
|
log("json=$foodVectorJson")
|
||||||
params["foodVector"] = foodVectorJson.toRequestBody()
|
val idList = viewModel.uploadCollect(
|
||||||
val idList = viewModel.uploadCollectFoodPics(files, params)
|
foodId = foodId,
|
||||||
|
foodName = foodName,
|
||||||
|
version = foodModelVersion,
|
||||||
|
foodVector = foodVectorJson,
|
||||||
|
fileList = listOf(imageFile)
|
||||||
|
)
|
||||||
if (idList.isNullOrEmpty()) {
|
if (idList.isNullOrEmpty()) {
|
||||||
loadRemindDialog("上传失败,未返回向量数据id")
|
loadRemindDialog("上传失败,未返回向量数据id")
|
||||||
log("idList为空")
|
log("idList为空")
|
||||||
@@ -1146,7 +1183,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
|||||||
ObjectBox.put(
|
ObjectBox.put(
|
||||||
Food(
|
Food(
|
||||||
collectId = idList[0],
|
collectId = idList[0],
|
||||||
foodId = foodId,
|
foodId = foodId.toString(),
|
||||||
foodName = foodName,
|
foodName = foodName,
|
||||||
foodVector = imageVector,
|
foodVector = imageVector,
|
||||||
version = foodModelVersion
|
version = foodModelVersion
|
||||||
@@ -1191,10 +1228,8 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
|||||||
private val intervalExecutor by lazy { IntervalExecutor() }
|
private val intervalExecutor by lazy { IntervalExecutor() }
|
||||||
private var faceTaskJob: Job? = null
|
private var faceTaskJob: Job? = null
|
||||||
|
|
||||||
// private val initialDelay = 5 * 60 * 1000L
|
private val initialDelay = 60 * 1000L
|
||||||
// private val dealyMillis = 10 * 60 * 1000L
|
private val dealyMillis = 30 * 1000L
|
||||||
private val initialDelay = 10 * 1000L
|
|
||||||
private val dealyMillis = 5 * 1000L
|
|
||||||
|
|
||||||
// private var taskPageNo = 1
|
// private var taskPageNo = 1
|
||||||
fun startFaceTask() {
|
fun startFaceTask() {
|
||||||
@@ -1204,7 +1239,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
|||||||
//if (timestamp == 0L) {
|
//if (timestamp == 0L) {
|
||||||
// return@startIntervalTaskWithInitialDelay
|
// return@startIntervalTaskWithInitialDelay
|
||||||
//}
|
//}
|
||||||
viewModel.getFaceIncrementList(
|
viewModel.getFaceIncrement(
|
||||||
// pageNo = taskPageNo,
|
// pageNo = taskPageNo,
|
||||||
timestamp = timestamp,
|
timestamp = timestamp,
|
||||||
onAllQueryFinished = {
|
onAllQueryFinished = {
|
||||||
@@ -1223,22 +1258,27 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateFaceData(list: List<UserFaceModel>) {
|
private fun updateFaceData(list: List<FaceVO>) {
|
||||||
val faceList = mutableListOf<FaceEntity>()
|
val faceList = mutableListOf<FaceEntity>()
|
||||||
try {
|
try {
|
||||||
list.forEach { model ->
|
list.forEach { vo ->
|
||||||
if (model.faceDeleted == true) {
|
if (vo.faceDeleted == true) {
|
||||||
//删除数据
|
//删除数据
|
||||||
FaceDatabase.getInstance(this).faceDao().deleteFaceById(model.userId)
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
} else {
|
FaceDatabase.getInstance(this@MainActivity).faceDao().deleteFaceById(vo.userId)
|
||||||
//保存数据
|
|
||||||
val faceEntity = FaceEntity(
|
|
||||||
model.userId, null, Base64.decode(model.faceFeatureStr)
|
|
||||||
).also {
|
|
||||||
//1-会员、2-临时用户
|
|
||||||
it.userType = if (model.member) "1" else "2"
|
|
||||||
it.cardNo = model.cardNo
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// 保存数据:使用完整构造函数,映射后端全部字段到数据库
|
||||||
|
val faceEntity = FaceEntity(
|
||||||
|
vo.userId, // userName
|
||||||
|
Base64.decode(vo.faceFeature), // featureData
|
||||||
|
vo.personType ?: if (vo.member == true) "1" else "2", // userType
|
||||||
|
vo.cardNo ?: "", // cardNo
|
||||||
|
vo.userId ?: "", // userId
|
||||||
|
vo.userFaceId ?: "", // userFaceId
|
||||||
|
vo.member ?: false, // member
|
||||||
|
vo.faceUpdateTimestamp?.toLongOrNull() ?: 0L // faceUpdateTimestamp
|
||||||
|
)
|
||||||
faceList.add(faceEntity)
|
faceList.add(faceEntity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1247,7 +1287,9 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (faceList.isNotEmpty()) {
|
if (faceList.isNotEmpty()) {
|
||||||
FaceDatabase.getInstance(this).faceDao().insert(faceList)
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
FaceDatabase.getInstance(this@MainActivity).faceDao().insert(faceList)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
recognizeViewModel.refreshFaceList();
|
recognizeViewModel.refreshFaceList();
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -1272,9 +1314,10 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
|||||||
startTime = System.currentTimeMillis()
|
startTime = System.currentTimeMillis()
|
||||||
log("registerDataChange photoUri = ${photoUri.path}")
|
log("registerDataChange photoUri = ${photoUri.path}")
|
||||||
// viewModel.getIdentifiedFoodList()
|
// viewModel.getIdentifiedFoodList()
|
||||||
ImageUtil.uriToBitmap(this@MainActivity, photoUri)?.let { bitmap ->
|
val bitmap = withContext(Dispatchers.IO) {
|
||||||
queryFoodData(bitmap)
|
ImageUtil.uriToBitmap(this@MainActivity, photoUri)
|
||||||
}
|
}
|
||||||
|
bitmap?.let { queryFoodData(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun queryFoodData(bitmap: Bitmap) {
|
private suspend fun queryFoodData(bitmap: Bitmap) {
|
||||||
@@ -1290,7 +1333,24 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
|||||||
startTime = System.currentTimeMillis()
|
startTime = System.currentTimeMillis()
|
||||||
log("registerDataChange photoUri bitmap保存文件路径:${file?.absolutePath}")
|
log("registerDataChange photoUri bitmap保存文件路径:${file?.absolutePath}")
|
||||||
//val nameList = FoodModule.queryFood(bitmap)
|
//val nameList = FoodModule.queryFood(bitmap)
|
||||||
val scoreList = FoodModule.getFoodScoreList(bitmap)
|
// 推理前同时暂停主屏相机和副屏人脸识别,让 ArcSoft 线程进入空闲后再启动 PyTorch
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
shutdownCamera()
|
||||||
|
presentation?.pauseCamera()
|
||||||
|
}
|
||||||
|
// 等待相机 HAL 在内核层(uvcvideo URB / DMC system_status)完成异步清理
|
||||||
|
// 实测:unbindAll() 之后 HAL 仍会在 ~200ms 内写 sysfs_dmc/system_status,
|
||||||
|
// 若此时 PyTorch forward() 同时占用大量 DRAM 带宽,RK3588 DMC 频率切换
|
||||||
|
// 与高带宽访问并发会触发硬件级故障导致设备重启。
|
||||||
|
// 1500ms 给 HAL 足够时间完成 DMC 带宽 hold 释放后再启动推理。
|
||||||
|
Timber.tag(TAG).d("queryFoodData: 等待相机HAL完成DMC清理 (1500ms)...")
|
||||||
|
kotlinx.coroutines.delay(1500)
|
||||||
|
Timber.tag(TAG).d("queryFoodData: 延迟结束,开始 forward()")
|
||||||
|
val scoreList = try {
|
||||||
|
FoodModule.getFoodScoreList(bitmap)
|
||||||
|
} finally {
|
||||||
|
withContext(Dispatchers.Main) { setupCamera() }
|
||||||
|
}
|
||||||
val foodName = if (scoreList.isNotEmpty()) {
|
val foodName = if (scoreList.isNotEmpty()) {
|
||||||
val recDataJson = GsonUtils.toJson(scoreList)
|
val recDataJson = GsonUtils.toJson(scoreList)
|
||||||
log("registerDataChange main,getFoodScoreList耗时:${System.currentTimeMillis() - startTime}")
|
log("registerDataChange main,getFoodScoreList耗时:${System.currentTimeMillis() - startTime}")
|
||||||
@@ -1316,6 +1376,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
|||||||
//binding.layoutRescan.visibility = View.VISIBLE
|
//binding.layoutRescan.visibility = View.VISIBLE
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
|
isRecognizing = false
|
||||||
tvToSearch.let {
|
tvToSearch.let {
|
||||||
it.text = "未识别到,手动搜索"
|
it.text = "未识别到,手动搜索"
|
||||||
it.visible()
|
it.visible()
|
||||||
@@ -1324,14 +1385,17 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
|||||||
} else {
|
} else {
|
||||||
//binding.layoutRescan.visibility = View.GONE
|
//binding.layoutRescan.visibility = View.GONE
|
||||||
showWaitingDialog("正在查询菜品信息,请稍后……")
|
showWaitingDialog("正在查询菜品信息,请稍后……")
|
||||||
viewModel.getFoodInfo(foodName) { list ->
|
viewModel.getFoodByNames(names = foodName, onSuccess = { list ->
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
hideWaitingDialog()
|
hideWaitingDialog()
|
||||||
log("registerDataChange main,getFoodInfo耗时:${System.currentTimeMillis() - startTime}")
|
log("registerDataChange main,getFoodInfo耗时:${System.currentTimeMillis() - startTime}")
|
||||||
startTime = System.currentTimeMillis()
|
startTime = System.currentTimeMillis()
|
||||||
updateFoodInfo(list.toMutableList(), scoreList)
|
updateFoodInfo(list.map { it.toFoodInfo() }.toMutableList(), scoreList)
|
||||||
}
|
}
|
||||||
}
|
}, onFailure = { errorMsg ->
|
||||||
|
hideWaitingDialog()
|
||||||
|
ToastUtils.showToast(errorMsg)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import com.sw.dualscreen.utils.Debouncer
|
|||||||
import com.sw.dualscreen.utils.SPUtil
|
import com.sw.dualscreen.utils.SPUtil
|
||||||
import com.sw.dualscreen.utils.SoundPoolUtil
|
import com.sw.dualscreen.utils.SoundPoolUtil
|
||||||
import com.sw.dualscreen.viewmodel.BaseViewModel
|
import com.sw.dualscreen.viewmodel.BaseViewModel
|
||||||
import com.sw.dualscreen.viewmodel.UserViewModel
|
import com.sw.dualscreen.viewmodel.NetViewModelV2
|
||||||
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
|
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@@ -43,7 +43,7 @@ class PayActivity : BaseActivity<ActivityPayBinding>() {
|
|||||||
const val PAY_SUCCESS = "pay_success"
|
const val PAY_SUCCESS = "pay_success"
|
||||||
}
|
}
|
||||||
|
|
||||||
val userViewModel by viewModels<UserViewModel>()
|
val userViewModel by viewModels<NetViewModelV2>()
|
||||||
val recognizeViewModel by viewModels<RecognizeViewModel>()
|
val recognizeViewModel by viewModels<RecognizeViewModel>()
|
||||||
|
|
||||||
override fun getViewModel(): BaseViewModel {
|
override fun getViewModel(): BaseViewModel {
|
||||||
@@ -196,7 +196,7 @@ class PayActivity : BaseActivity<ActivityPayBinding>() {
|
|||||||
//val onePrice = if (isVip) foodInfo?.vipPrice else foodInfo?.specPrice
|
//val onePrice = if (isVip) foodInfo?.vipPrice else foodInfo?.specPrice
|
||||||
if (isVip) {
|
if (isVip) {
|
||||||
userViewModel.getMemberDiscount(memberInfo?.faceUserId?:"") { discount ->
|
userViewModel.getMemberDiscount(memberInfo?.faceUserId?:"") { discount ->
|
||||||
memberDiscount = if (discount == null) 1.0 else discount / 10
|
memberDiscount = discount ?: 1.0
|
||||||
var onePrice = foodInfo?.specPrice ?: 0.0
|
var onePrice = foodInfo?.specPrice ?: 0.0
|
||||||
onePrice = if (isVip) onePrice * memberDiscount else onePrice
|
onePrice = if (isVip) onePrice * memberDiscount else onePrice
|
||||||
it.totalPrice = onePrice * eatNum
|
it.totalPrice = onePrice * eatNum
|
||||||
@@ -302,11 +302,17 @@ class PayActivity : BaseActivity<ActivityPayBinding>() {
|
|||||||
//0-即放即取,1-余量计量
|
//0-即放即取,1-余量计量
|
||||||
val pickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0)
|
val pickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0)
|
||||||
val mode = if (pickupMode == 1) 1 else 2
|
val mode = if (pickupMode == 1) 1 else 2
|
||||||
userViewModel.bindOrder(userId, foodOrderId, mode = mode) { bindResult ->
|
val uid = userId.toLongOrNull() ?: run {
|
||||||
runOnUiThread {
|
runOnUiThread { block(false) }
|
||||||
block(bindResult)
|
return
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
userViewModel.bindUserOrder(
|
||||||
|
userId = uid,
|
||||||
|
orderNo = foodOrderId,
|
||||||
|
mode = mode,
|
||||||
|
onSuccess = { runOnUiThread { block(true) } },
|
||||||
|
onFailure = { runOnUiThread { block(false) } }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun qrCodePay(scanInfo: String) {
|
fun qrCodePay(scanInfo: String) {
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ import com.sw.dualscreen.model.response.FoodInfo
|
|||||||
import com.sw.dualscreen.model.response.ClickBackEvent
|
import com.sw.dualscreen.model.response.ClickBackEvent
|
||||||
import com.sw.dualscreen.model.response.UpdateRefreshEvent
|
import com.sw.dualscreen.model.response.UpdateRefreshEvent
|
||||||
import com.sw.dualscreen.viewmodel.BaseViewModel
|
import com.sw.dualscreen.viewmodel.BaseViewModel
|
||||||
import com.sw.dualscreen.viewmodel.UserViewModel
|
import com.sw.dualscreen.viewmodel.NetViewModelV2
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
|
||||||
class SettingActivity : BaseActivity<ActivitySettingBinding>() {
|
class SettingActivity : BaseActivity<ActivitySettingBinding>() {
|
||||||
|
|
||||||
val viewModel by viewModels<UserViewModel>()
|
val viewModel by viewModels<NetViewModelV2>()
|
||||||
override fun getViewModel(): BaseViewModel {
|
override fun getViewModel(): BaseViewModel {
|
||||||
return viewModel
|
return viewModel
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,11 +16,18 @@ import kotlin.math.roundToInt
|
|||||||
|
|
||||||
class BusinessFragment: BaseFragment<FragmentBusinessBinding>() {
|
class BusinessFragment: BaseFragment<FragmentBusinessBinding>() {
|
||||||
|
|
||||||
|
// 当前秤的实时读数(克),作为待保存的餐具重量
|
||||||
|
private var dishWeight: Int = 0
|
||||||
|
|
||||||
override fun inflateViewBinding(): FragmentBusinessBinding {
|
override fun inflateViewBinding(): FragmentBusinessBinding {
|
||||||
return FragmentBusinessBinding.inflate(LayoutInflater.from(context))
|
return FragmentBusinessBinding.inflate(LayoutInflater.from(context))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun initialize() {
|
override fun initialize() {
|
||||||
|
// 初始化为已保存的餐具重量,未配置过则为 0
|
||||||
|
dishWeight = SPUtil.getInstance().get(GlobalKey.KEY_DISH_WEIGHT, 0) ?: 0
|
||||||
|
// 在标题后显示当前已保存的餐具重量
|
||||||
|
refreshDishWeightLabel()
|
||||||
binding.rgSettlement.let {
|
binding.rgSettlement.let {
|
||||||
//val settlementMode = if (binding.rgSettlement.checkedRadioButtonId == R.id.rbJointPayment) 0 else 1
|
//val settlementMode = if (binding.rgSettlement.checkedRadioButtonId == R.id.rbJointPayment) 0 else 1
|
||||||
|
|
||||||
@@ -68,6 +75,10 @@ class BusinessFragment: BaseFragment<FragmentBusinessBinding>() {
|
|||||||
|
|
||||||
val pickupMode = if (binding.rgTakeFood.checkedRadioButtonId == R.id.rbPickAndPlace) 0 else 1
|
val pickupMode = if (binding.rgTakeFood.checkedRadioButtonId == R.id.rbPickAndPlace) 0 else 1
|
||||||
SPUtil.getInstance().put(GlobalKey.KEY_PICKUP_MODE, pickupMode)
|
SPUtil.getInstance().put(GlobalKey.KEY_PICKUP_MODE, pickupMode)
|
||||||
|
// 持久化餐具重量(克)
|
||||||
|
SPUtil.getInstance().put(GlobalKey.KEY_DISH_WEIGHT, dishWeight)
|
||||||
|
// 刷新标签文字,显示当前已保存的餐具重量
|
||||||
|
refreshDishWeightLabel()
|
||||||
EventBus.getDefault().post(ResetRecognizeEvent())
|
EventBus.getDefault().post(ResetRecognizeEvent())
|
||||||
//ToastUtils.showToast("设置已保存")
|
//ToastUtils.showToast("设置已保存")
|
||||||
(activity as SettingActivity).let {
|
(activity as SettingActivity).let {
|
||||||
@@ -80,7 +91,17 @@ class BusinessFragment: BaseFragment<FragmentBusinessBinding>() {
|
|||||||
}
|
}
|
||||||
SensorScaleUtils.addWeightListener { value ->
|
SensorScaleUtils.addWeightListener { value ->
|
||||||
//val realWeight = (value * 1000).roundToInt()
|
//val realWeight = (value * 1000).roundToInt()
|
||||||
|
// 追踪秤的实时读数(克),作为待保存的餐具重量
|
||||||
|
dishWeight = value
|
||||||
binding.tvFoodWeight.text = "$value"
|
binding.tvFoodWeight.text = "$value"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新"餐具重量(克)"标签,在末尾显示当前已保存的配置值。
|
||||||
|
* 未配置(0)时只显示原标题。
|
||||||
|
*/
|
||||||
|
private fun refreshDishWeightLabel() {
|
||||||
|
binding.tvDishWeightLabel.text = if (dishWeight > 0) "餐具重量(克):${dishWeight}g" else "餐具重量(克)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -34,10 +34,9 @@ import com.sw.dualscreen.utils.Debouncer
|
|||||||
import com.sw.dualscreen.utils.ImageUploader
|
import com.sw.dualscreen.utils.ImageUploader
|
||||||
import com.sw.dualscreen.utils.ImageUtil
|
import com.sw.dualscreen.utils.ImageUtil
|
||||||
import com.sw.plate.utils.ToastUtils
|
import com.sw.plate.utils.ToastUtils
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import okhttp3.RequestBody
|
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
@@ -101,13 +100,14 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
|
|||||||
|
|
||||||
private val cameraCallback: (Uri) -> Unit = { uri ->
|
private val cameraCallback: (Uri) -> Unit = { uri ->
|
||||||
try {
|
try {
|
||||||
val index = foodCollectionList.indexOfFirst { it.imageFile == null }
|
activity?.lifecycleScope?.launch(Dispatchers.Main) {
|
||||||
if (index == -1) {
|
// 主线程原子操作:查找空闲槽位并立即标记,避免竞态
|
||||||
ToastUtils.showToast("每次只允许保存${MAX_COUNT}条数据")
|
val index = foodCollectionList.indexOfFirst { it.isShowCamera }
|
||||||
settingActivity?.hideWaitingDialog()
|
if (index == -1) {
|
||||||
rerurn@ cameraCallback
|
ToastUtils.showToast("每次只允许保存${MAX_COUNT}条数据")
|
||||||
}
|
settingActivity?.hideWaitingDialog()
|
||||||
activity?.runOnUiThread {
|
return@launch
|
||||||
|
}
|
||||||
foodCollectionList[index].let {
|
foodCollectionList[index].let {
|
||||||
it.imageVector = null
|
it.imageVector = null
|
||||||
it.bitmap = null
|
it.bitmap = null
|
||||||
@@ -116,11 +116,11 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
|
|||||||
it.imageUri = uri
|
it.imageUri = uri
|
||||||
}
|
}
|
||||||
collectionAdapter.notifyItemChanged(index)
|
collectionAdapter.notifyItemChanged(index)
|
||||||
}
|
// IO线程处理bitmap
|
||||||
// settingActivity?.hideWaitingDialog()
|
launch(Dispatchers.IO) {
|
||||||
activity?.lifecycleScope?.launch {
|
ImageUtil.uriToBitmap(requireActivity(), uri)?.let { bitmap ->
|
||||||
ImageUtil.uriToBitmap(requireActivity(), uri)?.let { bitmap ->
|
getImageVector(index, bitmap)
|
||||||
getImageVector(index, bitmap)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -131,6 +131,10 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val photoFailCallback: (String) -> Unit = { errMsg ->
|
||||||
|
settingActivity?.hideWaitingDialog()
|
||||||
|
}
|
||||||
|
|
||||||
private fun getImageVector(index: Int, bitmap: Bitmap) {
|
private fun getImageVector(index: Int, bitmap: Bitmap) {
|
||||||
// val bitmap = BitmapCropper.cropCenter(
|
// val bitmap = BitmapCropper.cropCenter(
|
||||||
// original = srcBmp,
|
// original = srcBmp,
|
||||||
@@ -141,7 +145,9 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
|
|||||||
FoodModule.bitmap2FloatArray(bitmap, false)
|
FoodModule.bitmap2FloatArray(bitmap, false)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
ToastUtils.showToast("操作失败")
|
activity?.lifecycleScope?.launch(Dispatchers.Main) {
|
||||||
|
ToastUtils.showToast("操作失败")
|
||||||
|
}
|
||||||
settingActivity?.log("操作失败:${e.message}")
|
settingActivity?.log("操作失败:${e.message}")
|
||||||
settingActivity?.hideWaitingDialog()
|
settingActivity?.hideWaitingDialog()
|
||||||
return
|
return
|
||||||
@@ -151,19 +157,16 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
|
|||||||
)
|
)
|
||||||
settingActivity?.log("${this.javaClass.simpleName}-cameraCallback-裁剪bitmap保存文件路径:${file?.absolutePath}")
|
settingActivity?.log("${this.javaClass.simpleName}-cameraCallback-裁剪bitmap保存文件路径:${file?.absolutePath}")
|
||||||
|
|
||||||
activity?.runOnUiThread {
|
foodCollectionList[index].let {
|
||||||
foodCollectionList[index].let {
|
it.imageVector = imageVector
|
||||||
it.imageVector = imageVector
|
it.bitmap = null
|
||||||
it.bitmap = null
|
it.isShowCamera = false
|
||||||
it.isShowCamera = false
|
it.imageFile = file
|
||||||
it.imageFile = file
|
|
||||||
}
|
|
||||||
collectionAdapter.notifyItemChanged(index)
|
|
||||||
}
|
}
|
||||||
|
settingActivity?.hideWaitingDialog()
|
||||||
if (bitmap.isRecycled.not()) {
|
if (bitmap.isRecycled.not()) {
|
||||||
bitmap.recycle()
|
bitmap.recycle()
|
||||||
}
|
}
|
||||||
settingActivity?.hideWaitingDialog()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
@@ -173,19 +176,9 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
|
|||||||
ToastUtils.showToast("每次只允许保存${MAX_COUNT}条数据")
|
ToastUtils.showToast("每次只允许保存${MAX_COUNT}条数据")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
settingActivity?.showWaitingDialog("采集中……")
|
settingActivity?.showWaitingDialog("采集中……")
|
||||||
cameraUtils.takePhoto(cameraCallback) { errMsg ->
|
cameraUtils.takePhoto(succCallback = cameraCallback, failCallback = photoFailCallback)
|
||||||
settingActivity?.hideWaitingDialog()
|
|
||||||
// ToastUtils.showToast("拍照异常,请重新操作")
|
|
||||||
// if (cameraErrorCount >= 10) {
|
|
||||||
// ToastUtils.showToast("拍照异常,请重新操作")
|
|
||||||
// settingActivity?.log("拍照异常,请重新操作:$errMsg")
|
|
||||||
// cameraErrorCount = 0
|
|
||||||
// return@takePhoto
|
|
||||||
// }
|
|
||||||
// cameraErrorCount++
|
|
||||||
// takePhoto()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var cameraErrorCount = 0
|
private var cameraErrorCount = 0
|
||||||
@@ -263,14 +256,15 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
|
|||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val totalFileCount = foodCollectionList.count { it.imageFile != null }
|
val totalFileCount = foodCollectionList.count { it.imageFile != null }
|
||||||
settingActivity?.showWaitingDialog2("图片上传中0/$totalFileCount")
|
settingActivity?.showWaitingDialog2("图片上传中0/$totalFileCount")
|
||||||
val params = HashMap<String, RequestBody>()
|
val foodId = checkedItem!!.foodId.toLongOrNull() ?: run {
|
||||||
//params["placeId"] = restId.toRequestBody()
|
ToastUtils.showToast("foodId 格式错误")
|
||||||
params["foodId"] = checkedItem!!.foodId.toRequestBody()
|
return@launch
|
||||||
params["foodName"] = checkedItem!!.foodName!!.toRequestBody()
|
}
|
||||||
params["version"] = GlobalData.foodModelVersion.toRequestBody()
|
val foodName = checkedItem!!.foodName ?: ""
|
||||||
|
val version = GlobalData.foodModelVersion
|
||||||
|
|
||||||
ImageUploader(totalList = foodCollectionList, uploadImage = { batch ->
|
ImageUploader(totalList = foodCollectionList, uploadImage = { batch ->
|
||||||
val files = batch.map { it.imageFile }
|
val files = batch.mapNotNull { it.imageFile }
|
||||||
val foodVectorList = batch.filter { it.imageVector != null }.map {
|
val foodVectorList = batch.filter { it.imageVector != null }.map {
|
||||||
it.imageVector!!.joinToString(
|
it.imageVector!!.joinToString(
|
||||||
separator = ",", prefix = "[", postfix = "]"
|
separator = ",", prefix = "[", postfix = "]"
|
||||||
@@ -279,8 +273,13 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
|
|||||||
val foodVectorJson =
|
val foodVectorJson =
|
||||||
foodVectorList.joinToString(separator = ",", prefix = "[", postfix = "]")
|
foodVectorList.joinToString(separator = ",", prefix = "[", postfix = "]")
|
||||||
Timber.tag(TAG).d("json=$foodVectorJson")
|
Timber.tag(TAG).d("json=$foodVectorJson")
|
||||||
params["foodVector"] = foodVectorJson.toRequestBody()
|
settingActivity?.viewModel?.uploadCollect(
|
||||||
settingActivity?.viewModel?.uploadCollectFoodPics(files, params)
|
foodId = foodId,
|
||||||
|
foodName = foodName,
|
||||||
|
version = version,
|
||||||
|
foodVector = foodVectorJson,
|
||||||
|
fileList = files
|
||||||
|
)
|
||||||
}, onProgress = { count, batch, idList ->
|
}, onProgress = { count, batch, idList ->
|
||||||
//batch.forEach {
|
//batch.forEach {
|
||||||
// it.uploadSuccess = true
|
// it.uploadSuccess = true
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import com.chad.library.adapter4.viewholder.QuickViewHolder
|
|||||||
import com.sw.dualscreen.databinding.ListItemCollectedFoodBinding
|
import com.sw.dualscreen.databinding.ListItemCollectedFoodBinding
|
||||||
import com.sw.dualscreen.ext.gone
|
import com.sw.dualscreen.ext.gone
|
||||||
import com.sw.dualscreen.ext.visible
|
import com.sw.dualscreen.ext.visible
|
||||||
import com.sw.dualscreen.objbox.CollectedFoodInfo
|
import com.sw.dualscreen.model.response.v2.CollectedFoodV2
|
||||||
|
|
||||||
class CollectedFoodNewAdapter(var list: MutableList<CollectedFoodInfo>) :
|
class CollectedFoodNewAdapter(var list: MutableList<CollectedFoodV2>) :
|
||||||
BaseQuickAdapter<CollectedFoodInfo, CollectedFoodNewAdapter.VH>(list) {
|
BaseQuickAdapter<CollectedFoodV2, CollectedFoodNewAdapter.VH>(list) {
|
||||||
|
|
||||||
inner class VH(var binding: ListItemCollectedFoodBinding) : QuickViewHolder(binding.root)
|
inner class VH(var binding: ListItemCollectedFoodBinding) : QuickViewHolder(binding.root)
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ class CollectedFoodNewAdapter(var list: MutableList<CollectedFoodInfo>) :
|
|||||||
return VH(binding)
|
return VH(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: VH, position: Int, item: CollectedFoodInfo?) {
|
override fun onBindViewHolder(holder: VH, position: Int, item: CollectedFoodV2?) {
|
||||||
holder.binding.tvFoodName.text = item?.foodName
|
holder.binding.tvFoodName.text = item?.foodName
|
||||||
holder.binding.tvCollectedNum.text = "已采集${item?.foodCount}"
|
holder.binding.tvCollectedNum.text = "已采集${item?.foodCount}"
|
||||||
holder.binding.divider.run {
|
holder.binding.divider.run {
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class FoodCollectionAdapter (var list: MutableList<FoodCollectionBean>) :
|
|||||||
//setImageURI(it.imageUri)
|
//setImageURI(it.imageUri)
|
||||||
//setImageBitmap(it.bitmap)
|
//setImageBitmap(it.bitmap)
|
||||||
if (it.imageUri!=null) {
|
if (it.imageUri!=null) {
|
||||||
setImageURI(it.imageUri)
|
load(it.imageUri)
|
||||||
} else {
|
} else {
|
||||||
load(it.imageFile)
|
load(it.imageFile)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ class FoodOrderAdapter(list: MutableList<FoodItem>) :
|
|||||||
if (it.orderFrom == 2) {
|
if (it.orderFrom == 2) {
|
||||||
binding.tvPriceNorm.text = if (it.specName.isNullOrBlank()) "${it.specWeight}克" else "${it.specName}/${it.specWeight}克"
|
binding.tvPriceNorm.text = if (it.specName.isNullOrBlank()) "${it.specWeight}克" else "${it.specName}/${it.specWeight}克"
|
||||||
} else {
|
} else {
|
||||||
val foodPrice = it.foodPrice.format2String(2)
|
// 营养秤结算:规格栏显示规格重量 specWeight
|
||||||
binding.tvPriceNorm.text = "${foodPrice}/100克"
|
binding.tvPriceNorm.text = "${it.specWeight}克"
|
||||||
}
|
}
|
||||||
//if (it.specName.isNullOrBlank().not()) {
|
//if (it.specName.isNullOrBlank().not()) {
|
||||||
// binding.tvPriceFlag.text = "[${it.specName}]"
|
// binding.tvPriceFlag.text = "[${it.specName}]"
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ class EnvSwitchDialog(
|
|||||||
override fun initView() {
|
override fun initView() {
|
||||||
// 根据当前 appBaseUrl 预选对应 RadioButton,不匹配则不选
|
// 根据当前 appBaseUrl 预选对应 RadioButton,不匹配则不选
|
||||||
when (GlobalData.appBaseUrl) {
|
when (GlobalData.appBaseUrl) {
|
||||||
GlobalData.TEST_BASE_URL -> binding.rbTest.isChecked = true
|
GlobalData.LOCAL_BASE_URL -> binding.rbTest.isChecked = true
|
||||||
GlobalData.UAT_BASE_URL -> binding.rbUat.isChecked = true
|
GlobalData.TEST_BASE_URL -> binding.rbUat.isChecked = true
|
||||||
GlobalData.PROD_BASE_URL -> binding.rbProd.isChecked = true
|
GlobalData.PROD_BASE_URL -> binding.rbProd.isChecked = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,8 +55,8 @@ class EnvSwitchDialog(
|
|||||||
return@setOnClickListener
|
return@setOnClickListener
|
||||||
}
|
}
|
||||||
val newUrl = when (binding.rgEnv.checkedRadioButtonId) {
|
val newUrl = when (binding.rgEnv.checkedRadioButtonId) {
|
||||||
binding.rbTest.id -> GlobalData.TEST_BASE_URL
|
binding.rbTest.id -> GlobalData.LOCAL_BASE_URL
|
||||||
binding.rbUat.id -> GlobalData.UAT_BASE_URL
|
binding.rbUat.id -> GlobalData.TEST_BASE_URL
|
||||||
binding.rbProd.id -> GlobalData.PROD_BASE_URL
|
binding.rbProd.id -> GlobalData.PROD_BASE_URL
|
||||||
else -> return@setOnClickListener
|
else -> return@setOnClickListener
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.sw.dualscreen.model.request.v2
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新系统绑定用户与订单请求参数
|
||||||
|
* 对应接口:/neglect/booth/order/bind-user
|
||||||
|
*/
|
||||||
|
data class BindUserOrderRequest(
|
||||||
|
val userId: Long,
|
||||||
|
val orderNo: String,
|
||||||
|
val mode: Int? = null
|
||||||
|
)
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.sw.dualscreen.model.request.v2
|
||||||
|
|
||||||
|
import java.math.BigDecimal
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新系统开餐下单请求参数
|
||||||
|
* 对应接口:/neglect/booth/order/place
|
||||||
|
*/
|
||||||
|
data class PlaceOrderRequest(
|
||||||
|
val foodId: Long,
|
||||||
|
val foodName: String,
|
||||||
|
val foodWeight: BigDecimal,
|
||||||
|
val eatWeight: BigDecimal,
|
||||||
|
val eatNum: Int,
|
||||||
|
val notPay: Boolean,
|
||||||
|
val mode: Int,
|
||||||
|
val specId: Long?,
|
||||||
|
val userId: Long?,
|
||||||
|
val paymentFrom: Int? = null,
|
||||||
|
val foodMaterialId: Long? = null,
|
||||||
|
val member: Boolean? = null,
|
||||||
|
val remark: String? = null,
|
||||||
|
val deviceId: String? = null,
|
||||||
|
val cookOrderId: String? = null
|
||||||
|
)
|
||||||
@@ -43,6 +43,8 @@ data class FoodInfo(
|
|||||||
val recommendCalorie: Double? = null,
|
val recommendCalorie: Double? = null,
|
||||||
//图片
|
//图片
|
||||||
var foodImg: String? = "",
|
var foodImg: String? = "",
|
||||||
|
//按单烹制 ID(菜品列表返回,就餐时原样回传,用于溯源码追踪)
|
||||||
|
var cookOrderId: String? = "",
|
||||||
|
|
||||||
//-----------------------------------
|
//-----------------------------------
|
||||||
var score: Int = 0,
|
var score: Int = 0,
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.sw.dualscreen.model.response.v2
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新系统采集菜品信息
|
||||||
|
* 对应接口:/neglect/booth/collect/page
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class CollectedFoodV2(
|
||||||
|
val foodId: String?,
|
||||||
|
val foodName: String?,
|
||||||
|
val version: String?,
|
||||||
|
val foodVector: String?,
|
||||||
|
val picUrls: List<String>?,
|
||||||
|
/** 已采集数量(客户端手动赋值,非接口字段,用于兼容旧版 UI) */
|
||||||
|
var foodCount: Int = 0
|
||||||
|
) : Parcelable
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.sw.dualscreen.model.response.v2
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新系统人脸数据 VO
|
||||||
|
* 对应接口:/nutrition/neglect/common/face/page 和 /nutrition/neglect/common/face/increment
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class FaceVO(
|
||||||
|
val userFaceId: String?,
|
||||||
|
val userId: String?,
|
||||||
|
val faceFeature: String?,
|
||||||
|
/** 后端返回 String 类型(防止 JS 大数精度丢失),映射时自行转为 Long */
|
||||||
|
val faceUpdateTimestamp: String?,
|
||||||
|
val cardNo: String?,
|
||||||
|
val member: Boolean?,
|
||||||
|
val faceDeleted: Boolean?,
|
||||||
|
/** 人员类型:由后端定义,如 "1"-会员、"2"-临时用户、"3"-其他等 */
|
||||||
|
val personType: String?
|
||||||
|
) : Parcelable
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package com.sw.dualscreen.model.response.v2
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import com.sw.dualscreen.model.response.FoodInfo
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import java.math.BigDecimal
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新系统菜品信息
|
||||||
|
* 对应接口:/neglect/booth/food/by-names
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class NewFoodInfo(
|
||||||
|
val foodId: Long,
|
||||||
|
val foodName: String?,
|
||||||
|
val calorie: BigDecimal?,
|
||||||
|
val protein: BigDecimal?,
|
||||||
|
val fat: BigDecimal?,
|
||||||
|
val carbohydrate: BigDecimal?,
|
||||||
|
val price: BigDecimal?,
|
||||||
|
val specPrice: BigDecimal?,
|
||||||
|
val specWeight: BigDecimal?,
|
||||||
|
val stapleFood: BigDecimal?,
|
||||||
|
val fruitsVegetables: BigDecimal?,
|
||||||
|
val meatEggs: BigDecimal?,
|
||||||
|
val foodMaterialId: Long?,
|
||||||
|
val specId: Long?,
|
||||||
|
val foodImg: String?,
|
||||||
|
val foodLabel: String?,
|
||||||
|
val vipPrice: BigDecimal? = null,
|
||||||
|
val recommendCalorie: Int = 0,
|
||||||
|
val tablewareStatus: Boolean = false,
|
||||||
|
val tablewareWeight: Int = 0,
|
||||||
|
//按单烹制 ID(菜品列表返回,就餐时原样回传,用于溯源码追踪)
|
||||||
|
var cookOrderId: String? = "",
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
|
/** NewFoodInfo → FoodInfo 映射(V2 数据模型兼容旧版 UI) */
|
||||||
|
fun NewFoodInfo.toFoodInfo() = FoodInfo(
|
||||||
|
foodId = foodId.toString(),
|
||||||
|
foodName = foodName,
|
||||||
|
calorie = calorie?.toDouble(),
|
||||||
|
protein = protein?.toDouble(),
|
||||||
|
fat = fat?.toDouble(),
|
||||||
|
carbohydrate = carbohydrate?.toDouble(),
|
||||||
|
specPrice = specPrice?.toDouble(),
|
||||||
|
vipPrice = vipPrice?.toDouble(),
|
||||||
|
foodMaterialId = foodMaterialId?.toString(),
|
||||||
|
specId = specId?.toString(),
|
||||||
|
specWeight = specWeight?.toDouble(),
|
||||||
|
stapleFood = stapleFood?.toDouble(),
|
||||||
|
fruitsVegetables = fruitsVegetables?.toDouble(),
|
||||||
|
meatEggs = meatEggs?.toDouble(),
|
||||||
|
recommendCalorie = recommendCalorie.toDouble(),
|
||||||
|
foodImg = foodImg,
|
||||||
|
cookOrderId = cookOrderId,
|
||||||
|
)
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.sw.dualscreen.model.response.v2
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import com.sw.dualscreen.model.response.MemberInfo
|
||||||
|
import java.math.BigDecimal
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新系统会员信息
|
||||||
|
* 对应接口:/neglect/booth/member/info 和 /neglect/booth/member/info-by-phone
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class NewMemberInfo(
|
||||||
|
val id: String?,
|
||||||
|
val phone: String?,
|
||||||
|
val name: String?,
|
||||||
|
val faceUrl: String?,
|
||||||
|
val topUpBalance: BigDecimal?,
|
||||||
|
val rewardBalance: BigDecimal? = BigDecimal.ZERO,
|
||||||
|
val integralBalance: Int?,
|
||||||
|
val member: Boolean?
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
|
/** NewMemberInfo → MemberInfo 映射(V2 数据模型兼容旧版 UI,faceUserId 复用 id) */
|
||||||
|
fun NewMemberInfo.toMemberInfo() = MemberInfo(
|
||||||
|
id = id,
|
||||||
|
faceUserId = id,
|
||||||
|
phone = phone,
|
||||||
|
name = name,
|
||||||
|
faceUrl = faceUrl,
|
||||||
|
topUpBalance = topUpBalance?.toDouble(),
|
||||||
|
rewardBalance = rewardBalance?.toDouble(),
|
||||||
|
integralBalance = integralBalance,
|
||||||
|
member = member ?: false
|
||||||
|
)
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package com.sw.dualscreen.model.response.v2
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import com.sw.dualscreen.model.response.FoodItem
|
||||||
|
import com.sw.dualscreen.model.response.FoodOrderModel
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import java.math.BigDecimal
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新系统结算订单数据
|
||||||
|
* 对应接口:/neglect/booth/order/settlement
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class SettlementOrder(
|
||||||
|
val calorie: BigDecimal?,
|
||||||
|
val incomeSum: BigDecimal?,
|
||||||
|
val discountSum: BigDecimal?,
|
||||||
|
val eatWeightSum: Int?,
|
||||||
|
val orderNo: String?,
|
||||||
|
val list: List<SettlementFoodItem>?
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class SettlementFoodItem(
|
||||||
|
val foodId: String?,
|
||||||
|
val foodName: String?,
|
||||||
|
val specId: String?,
|
||||||
|
val specName: String?,
|
||||||
|
val specWeight: BigDecimal?,
|
||||||
|
val eatNum: Int?,
|
||||||
|
val eatWeight: Int?,
|
||||||
|
val price: BigDecimal?
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
|
/** SettlementOrder → FoodOrderModel 映射(V2 数据模型兼容旧版 UI) */
|
||||||
|
fun SettlementOrder.toFoodOrderModel() = FoodOrderModel(
|
||||||
|
calorie = calorie?.toDouble() ?: 0.0,
|
||||||
|
incomeSum = incomeSum?.toDouble() ?: 0.0,
|
||||||
|
discountSum = discountSum?.toDouble() ?: 0.0,
|
||||||
|
eatWeightSum = eatWeightSum ?: 0,
|
||||||
|
orderNo = orderNo,
|
||||||
|
list = list?.map { it.toFoodItem() }
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun SettlementFoodItem.toFoodItem() = FoodItem(
|
||||||
|
foodId = foodId,
|
||||||
|
foodName = foodName,
|
||||||
|
specId = specId,
|
||||||
|
specName = specName,
|
||||||
|
specWeight = specWeight?.toInt() ?: 0,
|
||||||
|
eatNum = eatNum ?: 0,
|
||||||
|
price = price?.toDouble() ?: 0.0,
|
||||||
|
// 重量栏读取 num 字段,这里用实际食用重量 eatWeight 赋值
|
||||||
|
num = eatWeight ?: 0
|
||||||
|
)
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
package com.sw.dualscreen.network
|
package com.sw.dualscreen.network
|
||||||
|
|
||||||
import com.sw.dualscreen.BuildConfig
|
import com.sw.dualscreen.BuildConfig
|
||||||
|
import com.sw.dualscreen.GlobalData
|
||||||
import com.sw.dualscreen.MyApp
|
import com.sw.dualscreen.MyApp
|
||||||
import com.sw.dualscreen.network.api.ApiService
|
import com.sw.dualscreen.network.api.ApiService
|
||||||
|
import com.sw.dualscreen.network.api.ApiServiceV2
|
||||||
import com.sw.dualscreen.network.interceptor.RequestInterceptor
|
import com.sw.dualscreen.network.interceptor.RequestInterceptor
|
||||||
|
import com.sw.dualscreen.repository.RemoteRepository
|
||||||
|
import com.sw.dualscreen.repository.v2.RemoteRepositoryV2
|
||||||
import com.sw.dualscreen.utils.FileUtil
|
import com.sw.dualscreen.utils.FileUtil
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
@@ -14,19 +18,24 @@ import timber.log.Timber
|
|||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
object ApiClient {
|
object ApiClient {
|
||||||
private const val BASE_URL = "http://device.shuziweidao.com:8889/"
|
// private const val BASE_URL = "http://device.shuziweidao.com:8889/"
|
||||||
|
private const val BASE_URL = GlobalData.PROD_BASE_URL
|
||||||
|
|
||||||
private const val TIME_OUT = 30L // 超时时间(秒)
|
private const val TIME_OUT = 30L
|
||||||
|
|
||||||
private val okHttpClient = OkHttpClient.Builder()
|
private val okHttpClient = OkHttpClient.Builder()
|
||||||
.connectTimeout(TIME_OUT, TimeUnit.SECONDS)
|
.connectTimeout(TIME_OUT, TimeUnit.SECONDS)
|
||||||
.readTimeout(TIME_OUT, TimeUnit.SECONDS)
|
.readTimeout(TIME_OUT, TimeUnit.SECONDS)
|
||||||
.writeTimeout(TIME_OUT, TimeUnit.SECONDS)
|
.writeTimeout(TIME_OUT, TimeUnit.SECONDS)
|
||||||
.addNetworkInterceptor(Interceptor {chain ->
|
.addNetworkInterceptor(Interceptor { chain ->
|
||||||
val request = chain.request()
|
val request = chain.request()
|
||||||
val url = request.url.toString()
|
val url = request.url.toString()
|
||||||
val tag = if (url.contains("terminal/neglect/common/app/faceFeature/increment/list")) "faceIncrement"
|
val tag = if (
|
||||||
|
url.contains("terminal/neglect/common/app/faceFeature/increment/list") ||
|
||||||
|
url.contains("nutrition/neglect/serve/face/increment")
|
||||||
|
) "faceIncrement"
|
||||||
else if (url.contains("terminal/neglect/pay/app/turnOrderInfo")) "turnOrderInfo"
|
else if (url.contains("terminal/neglect/pay/app/turnOrderInfo")) "turnOrderInfo"
|
||||||
|
else if(url.contains("nutrition/neglect")) "V2"
|
||||||
else "ApiClient"
|
else "ApiClient"
|
||||||
val loggingInterceptor = HttpLoggingInterceptor(logger = {
|
val loggingInterceptor = HttpLoggingInterceptor(logger = {
|
||||||
Timber.tag(tag).d("okhttp logger ==>${it}")
|
Timber.tag(tag).d("okhttp logger ==>${it}")
|
||||||
@@ -47,10 +56,21 @@ object ApiClient {
|
|||||||
.baseUrl(BASE_URL)
|
.baseUrl(BASE_URL)
|
||||||
.client(okHttpClient)
|
.client(okHttpClient)
|
||||||
.addConverterFactory(GsonConverterFactory.create())
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
// .addCallAdapterFactory(CoroutineCallAdapterFactory()) // 协程适配器
|
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val apiService: ApiService by lazy {
|
val apiService: ApiService by lazy {
|
||||||
retrofit.create(ApiService::class.java)
|
retrofit.create(ApiService::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val apiServiceV2: ApiServiceV2 by lazy {
|
||||||
|
retrofit.create(ApiServiceV2::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
val repository: RemoteRepository by lazy {
|
||||||
|
RemoteRepository(apiService)
|
||||||
|
}
|
||||||
|
|
||||||
|
val repositoryV2: RemoteRepositoryV2 by lazy {
|
||||||
|
RemoteRepositoryV2(apiServiceV2)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
package com.sw.dualscreen.network.api
|
||||||
|
|
||||||
|
import com.sw.dualscreen.GlobalData
|
||||||
|
import com.sw.dualscreen.model.request.v2.BindUserOrderRequest
|
||||||
|
import com.sw.dualscreen.model.request.v2.PlaceOrderRequest
|
||||||
|
import com.sw.dualscreen.model.response.ApiResponse
|
||||||
|
import com.sw.dualscreen.model.response.DeviceConfig
|
||||||
|
import com.sw.dualscreen.model.response.FoodSearchReq
|
||||||
|
import com.sw.dualscreen.model.response.UserNutrition
|
||||||
|
import com.sw.dualscreen.model.response.v2.CollectedFoodV2
|
||||||
|
import com.sw.dualscreen.model.response.v2.FaceVO
|
||||||
|
import com.sw.dualscreen.model.response.v2.NewFoodInfo
|
||||||
|
import com.sw.dualscreen.model.response.v2.NewMemberInfo
|
||||||
|
import com.sw.dualscreen.model.response.v2.SettlementOrder
|
||||||
|
import okhttp3.MultipartBody
|
||||||
|
import okhttp3.RequestBody
|
||||||
|
import retrofit2.http.Body
|
||||||
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.Multipart
|
||||||
|
import retrofit2.http.POST
|
||||||
|
import retrofit2.http.Part
|
||||||
|
import retrofit2.http.PartMap
|
||||||
|
import retrofit2.http.Url
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新系统 API 接口服务
|
||||||
|
* 基于 /nutrition/neglect/booth 模块前缀
|
||||||
|
* 文档版本:2026-05-27
|
||||||
|
*/
|
||||||
|
interface ApiServiceV2 {
|
||||||
|
|
||||||
|
@GET
|
||||||
|
suspend fun getDeviceConfig(
|
||||||
|
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/device/config"
|
||||||
|
): ApiResponse<DeviceConfig?>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取全量人脸数据
|
||||||
|
*/
|
||||||
|
@POST
|
||||||
|
suspend fun getFacePage(
|
||||||
|
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/common/face/page",
|
||||||
|
@Body request: Map<String, Long>
|
||||||
|
): ApiResponse<List<FaceVO>?>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取增量人脸数据
|
||||||
|
*/
|
||||||
|
@POST
|
||||||
|
suspend fun getFaceIncrement(
|
||||||
|
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/common/face/increment",
|
||||||
|
@Body request: Map<String, Long>
|
||||||
|
): ApiResponse<List<FaceVO>?>
|
||||||
|
|
||||||
|
@POST
|
||||||
|
suspend fun getFoodByNames(
|
||||||
|
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/food/by-names",
|
||||||
|
@Body request: FoodSearchReq
|
||||||
|
): ApiResponse<List<NewFoodInfo>?>
|
||||||
|
|
||||||
|
@POST
|
||||||
|
suspend fun searchFood(
|
||||||
|
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/food/search",
|
||||||
|
@Body request: Map<String, @JvmSuppressWildcards Any>
|
||||||
|
): ApiResponse<List<NewFoodInfo>?>
|
||||||
|
|
||||||
|
@POST
|
||||||
|
suspend fun getUserCurrentFood(
|
||||||
|
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/user/current-food",
|
||||||
|
@Body request: Map<String, Long>
|
||||||
|
): ApiResponse<UserNutrition?>
|
||||||
|
|
||||||
|
@POST
|
||||||
|
suspend fun placeOrder(
|
||||||
|
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/order/place",
|
||||||
|
@Body request: PlaceOrderRequest
|
||||||
|
): ApiResponse<String?>
|
||||||
|
|
||||||
|
@POST
|
||||||
|
suspend fun getSettlementOrders(
|
||||||
|
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/order/settlement",
|
||||||
|
@Body request: Map<String, Long>
|
||||||
|
): ApiResponse<SettlementOrder?>
|
||||||
|
|
||||||
|
@POST
|
||||||
|
suspend fun getMemberInfo(
|
||||||
|
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/member/info",
|
||||||
|
@Body request: Map<String, Long>
|
||||||
|
): ApiResponse<NewMemberInfo?>
|
||||||
|
|
||||||
|
@POST
|
||||||
|
suspend fun getMemberInfoByPhone(
|
||||||
|
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/member/info-by-phone",
|
||||||
|
@Body request: Map<String, String>
|
||||||
|
): ApiResponse<NewMemberInfo?>
|
||||||
|
|
||||||
|
@POST
|
||||||
|
suspend fun getMemberDiscount(
|
||||||
|
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/member/discount",
|
||||||
|
@Body request: Map<String, Long>
|
||||||
|
): ApiResponse<String?>
|
||||||
|
|
||||||
|
@POST
|
||||||
|
suspend fun bindUserOrder(
|
||||||
|
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/order/bind-user",
|
||||||
|
@Body request: BindUserOrderRequest
|
||||||
|
): ApiResponse<Any?>
|
||||||
|
|
||||||
|
@POST
|
||||||
|
suspend fun getCollectPage(
|
||||||
|
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/collect/page",
|
||||||
|
@Body request: Map<String, @JvmSuppressWildcards Any>
|
||||||
|
): ApiResponse<List<CollectedFoodV2>?>
|
||||||
|
|
||||||
|
@POST
|
||||||
|
suspend fun getCollectVectorPage(
|
||||||
|
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/collect/vector-page",
|
||||||
|
@Body request: Map<String, Long>
|
||||||
|
): ApiResponse<List<CollectedFoodV2>?>
|
||||||
|
|
||||||
|
@Multipart
|
||||||
|
@POST
|
||||||
|
suspend fun uploadCollect(
|
||||||
|
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/collect/upload",
|
||||||
|
@PartMap params: HashMap<String, RequestBody>,
|
||||||
|
@Part foodPics: List<MultipartBody.Part>
|
||||||
|
): ApiResponse<List<String>?>
|
||||||
|
|
||||||
|
@POST
|
||||||
|
suspend fun deleteCollect(
|
||||||
|
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/collect/delete",
|
||||||
|
@Body request: Map<String, @JvmSuppressWildcards Any>
|
||||||
|
): ApiResponse<Any?>
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.pytorch.IValue
|
import org.pytorch.IValue
|
||||||
import org.pytorch.Module
|
import org.pytorch.Module
|
||||||
|
import org.pytorch.Tensor
|
||||||
import org.pytorch.torchvision.TensorImageUtils
|
import org.pytorch.torchvision.TensorImageUtils
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -29,6 +30,9 @@ object FoodModule {
|
|||||||
// private const val THRESHOLD = 0.0
|
// private const val THRESHOLD = 0.0
|
||||||
private var module: Module? = null
|
private var module: Module? = null
|
||||||
|
|
||||||
|
// 专用推理线程:与 Kotlin IO 调度器完全隔离,避免 ArcSoft 污染该线程的 FPU 状态(FPSCR)
|
||||||
|
private val inferenceExecutor = java.util.concurrent.Executors.newSingleThreadExecutor()
|
||||||
|
|
||||||
// private lateinit var embeddingsList: List<List<Float>>
|
// private lateinit var embeddingsList: List<List<Float>>
|
||||||
// private lateinit var labelsList: IntArray
|
// private lateinit var labelsList: IntArray
|
||||||
// private lateinit var classInfo: FoodClassInfo
|
// private lateinit var classInfo: FoodClassInfo
|
||||||
@@ -46,6 +50,23 @@ object FoodModule {
|
|||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val modelPath = copyAssetToCache(context, "best_embedding_model_mobile.pt")
|
val modelPath = copyAssetToCache(context, "best_embedding_model_mobile.pt")
|
||||||
module = Module.load(modelPath)
|
module = Module.load(modelPath)
|
||||||
|
// 预热:在相机启动前先跑一次 forward,触发 PyTorch 线程池创建
|
||||||
|
// 在预热前将当前线程优先级调低,使 PyTorch 内部新建的工作线程也继承低优先级
|
||||||
|
// 从而降低后续推理时的瞬间功率,避免触发 PMIC 过流保护
|
||||||
|
Timber.tag(TAG).d("预热 forward() 开始")
|
||||||
|
val warmupTensor = Tensor.fromBlob(
|
||||||
|
FloatArray(1 * 3 * 224 * 224),
|
||||||
|
longArrayOf(1, 3, 224, 224)
|
||||||
|
)
|
||||||
|
inferenceExecutor.submit { module?.forward(IValue.from(warmupTensor)) }.get()
|
||||||
|
context.let { ctx ->
|
||||||
|
val am = ctx.getSystemService(android.content.Context.ACTIVITY_SERVICE)
|
||||||
|
as android.app.ActivityManager
|
||||||
|
val mi = android.app.ActivityManager.MemoryInfo()
|
||||||
|
am.getMemoryInfo(mi)
|
||||||
|
Timber.tag(TAG).d("预热完成后系统可用内存: ${mi.availMem / 1024 / 1024} MB / 总: ${mi.totalMem / 1024 / 1024} MB")
|
||||||
|
}
|
||||||
|
Timber.tag(TAG).d("预热 forward() 完成")
|
||||||
//初始化默认重新拉取数据,先清空本地数据
|
//初始化默认重新拉取数据,先清空本地数据
|
||||||
val list = ObjectBox.getAll()
|
val list = ObjectBox.getAll()
|
||||||
if (list.isNotEmpty()) {
|
if (list.isNotEmpty()) {
|
||||||
@@ -86,8 +107,28 @@ object FoodModule {
|
|||||||
if (module == null) {
|
if (module == null) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
val outputTensor = module?.forward(IValue.from(inputTensor))?.toTensor()
|
Timber.tag(TAG).d("module:${module}")
|
||||||
return outputTensor?.dataAsFloatArray
|
val iValue = IValue.from(inputTensor)
|
||||||
|
Timber.tag(TAG).d("inputTensor:${inputTensor}")
|
||||||
|
|
||||||
|
// 推理前记录系统可用内存,排查是否因内存不足触发 OOM 重启
|
||||||
|
MyApp.instance?.let { ctx ->
|
||||||
|
val am = ctx.getSystemService(android.content.Context.ACTIVITY_SERVICE)
|
||||||
|
as android.app.ActivityManager
|
||||||
|
val mi = android.app.ActivityManager.MemoryInfo()
|
||||||
|
am.getMemoryInfo(mi)
|
||||||
|
Timber.tag(TAG).d("forward() 前系统可用内存: ${mi.availMem / 1024 / 1024} MB / 总: ${mi.totalMem / 1024 / 1024} MB, lowMemory=${mi.lowMemory}")
|
||||||
|
}
|
||||||
|
Timber.tag(TAG).d("forward() 开始: ${System.currentTimeMillis()}")
|
||||||
|
val outputIValue = inferenceExecutor.submit<IValue?> { module?.forward(iValue) }
|
||||||
|
.get(10, java.util.concurrent.TimeUnit.SECONDS) ?: return null
|
||||||
|
Timber.tag(TAG).d("forward() 结束: ${System.currentTimeMillis()}")
|
||||||
|
|
||||||
|
|
||||||
|
Timber.tag(TAG).d("outputIValue:${outputIValue}")
|
||||||
|
val outputTensor = outputIValue.toTensor()
|
||||||
|
Timber.tag(TAG).d("outputTensor:${outputTensor}")
|
||||||
|
return outputTensor.dataAsFloatArray
|
||||||
} catch (e: OutOfMemoryError) {
|
} catch (e: OutOfMemoryError) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
} finally {
|
} finally {
|
||||||
@@ -124,7 +165,7 @@ object FoodModule {
|
|||||||
|
|
||||||
suspend fun queryFoodNameScore(
|
suspend fun queryFoodNameScore(
|
||||||
floatArray: FloatArray?,
|
floatArray: FloatArray?,
|
||||||
queryCount: Int = 50
|
queryCount: Int = 15
|
||||||
): List<IdNameScore> {
|
): List<IdNameScore> {
|
||||||
if (floatArray == null) return emptyList()
|
if (floatArray == null) return emptyList()
|
||||||
val startTime = System.currentTimeMillis()
|
val startTime = System.currentTimeMillis()
|
||||||
@@ -133,27 +174,21 @@ object FoodModule {
|
|||||||
//查询比较分数
|
//查询比较分数
|
||||||
// val tempList = query.findWithScores().sortedBy { it.score }.map { "${it.get().name}|${it.get().foodIdx}|${it.score}" }
|
// val tempList = query.findWithScores().sortedBy { it.score }.map { "${it.get().name}|${it.get().foodIdx}|${it.score}" }
|
||||||
// val idScoreList = query.findIdsWithScores()
|
// val idScoreList = query.findIdsWithScores()
|
||||||
val objScoreList = ObjectBox.query(floatArray, queryCount)
|
val tripleList = ObjectBox.query(floatArray, queryCount)
|
||||||
Timber.tag(TAG).d("idScoreList:${GsonUtils.toJson(objScoreList)}")
|
Timber.tag(TAG).d("idScoreList:${GsonUtils.toJson(tripleList)}")
|
||||||
val nameScoreList = mutableListOf<IdNameScore>()
|
val nameScoreList = tripleList.map { (id, name, score) ->
|
||||||
objScoreList.forEach {
|
IdNameScore(id = id, name = name ?: "", score = score)
|
||||||
val food = it.get()
|
|
||||||
nameScoreList.add(
|
|
||||||
IdNameScore(
|
|
||||||
id = food.id,
|
|
||||||
name = food.foodName ?: "",
|
|
||||||
score = it.score
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
val nameScoreData = GsonUtils.toJson(nameScoreList)
|
val nameScoreData = GsonUtils.toJson(nameScoreList)
|
||||||
Timber.tag(TAG).d("queryFood,耗时:${System.currentTimeMillis() - startTime},数据:$nameScoreData")
|
Timber.tag(TAG).d("queryFood,耗时:${System.currentTimeMillis() - startTime},数据:$nameScoreData")
|
||||||
return nameScoreList
|
return nameScoreList
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getFoodScoreList(bitmap: Bitmap, queryCount: Int = 50): List<IdNameScore> {
|
suspend fun getFoodScoreList(bitmap: Bitmap, queryCount: Int = 15): List<IdNameScore> {
|
||||||
val startTime = System.currentTimeMillis()
|
val startTime = System.currentTimeMillis()
|
||||||
val floatArray = bitmap2FloatArray(bitmap, false)
|
val floatArray = withContext(Dispatchers.IO) {
|
||||||
|
bitmap2FloatArray(bitmap, false)
|
||||||
|
}
|
||||||
Timber.tag(TAG).d("bitmap2FloatArray,耗时:${System.currentTimeMillis() - startTime}")
|
Timber.tag(TAG).d("bitmap2FloatArray,耗时:${System.currentTimeMillis() - startTime}")
|
||||||
|
|
||||||
val nameScoreList = queryFoodNameScore(floatArray, queryCount)
|
val nameScoreList = queryFoodNameScore(floatArray, queryCount)
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import io.objectbox.Box
|
|||||||
import io.objectbox.BoxStore
|
import io.objectbox.BoxStore
|
||||||
import io.objectbox.BoxStoreBuilder
|
import io.objectbox.BoxStoreBuilder
|
||||||
import io.objectbox.android.Admin
|
import io.objectbox.android.Admin
|
||||||
import io.objectbox.android.ObjectBoxLiveData
|
|
||||||
import io.objectbox.config.DebugFlags
|
import io.objectbox.config.DebugFlags
|
||||||
import io.objectbox.exception.DbException
|
import io.objectbox.exception.DbException
|
||||||
import io.objectbox.exception.FileCorruptException
|
import io.objectbox.exception.FileCorruptException
|
||||||
@@ -152,19 +152,118 @@ object ObjectBox {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun query(floatArray: FloatArray, queryCount: Int) = safeDbOp {
|
suspend fun query(floatArray: FloatArray, queryCount: Int) = safeDbOp {
|
||||||
val query: Query<Food>? = getBox<Food>()?.query()
|
val box = getBox<Food>() ?: return@safeDbOp emptyList<Triple<Long, String?, Double>>()
|
||||||
?.equal(Food_.isDel, false)
|
|
||||||
?.and()
|
val query: Query<Food> = box.query()
|
||||||
?.nearestNeighbors(Food_.foodVector, floatArray, queryCount)
|
.equal(Food_.isDel, false)
|
||||||
?.build()
|
.and()
|
||||||
|
.nearestNeighbors(Food_.foodVector, floatArray, queryCount)
|
||||||
|
.build()
|
||||||
try {
|
try {
|
||||||
query?.findWithScores()
|
// 统一使用 findIdsWithScores,避免在 32 位系统上因加载大对象而崩溃
|
||||||
|
val idScoreList = query.findIdsWithScores()
|
||||||
|
if (idScoreList.isNotEmpty()) {
|
||||||
|
// 根据 ID 查询完整对象,仅提取 id 和 name,丢弃 foodVector 以节省内存
|
||||||
|
val foodMap = box.get(idScoreList.map { it.id }).associateBy { it.id }
|
||||||
|
idScoreList.map { scoreId ->
|
||||||
|
val food = foodMap[scoreId.id]
|
||||||
|
Triple(scoreId.id, food?.foodName, scoreId.score)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
} catch (e: DbException) {
|
||||||
|
Timber.tag(TAG).e(e, "向量查询失败")
|
||||||
|
emptyList()
|
||||||
} finally {
|
} finally {
|
||||||
// 先关闭Query,释放Cursor
|
query.close()
|
||||||
query?.close()
|
|
||||||
}
|
}
|
||||||
} ?: emptyList()
|
} ?: emptyList()
|
||||||
|
|
||||||
|
// suspend fun queryWithScore(floatArray: FloatArray, queryCount: Int) = safeDbOp {
|
||||||
|
// val box = getBox<Food>() ?: return@safeDbOp emptyList()
|
||||||
|
//
|
||||||
|
// val query: Query<Food>? = box.query()
|
||||||
|
// ?.equal(Food_.isDel, false)
|
||||||
|
// ?.and()
|
||||||
|
// ?.nearestNeighbors(Food_.foodVector, floatArray, queryCount)
|
||||||
|
// ?.build()
|
||||||
|
//
|
||||||
|
// try {
|
||||||
|
// // 检测是否为 32 位系统
|
||||||
|
// val is32Bit = Build.SUPPORTED_ABIS.any {
|
||||||
|
// it.contains("armeabi") && !it.contains("arm64")
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if (is32Bit) {
|
||||||
|
// // 32位系统:使用 findWithScoresAndIds 替代 findWithScores
|
||||||
|
// val scoreIds = query?.findWithScoresAndIds()
|
||||||
|
// if (scoreIds != null && scoreIds.isNotEmpty()) {
|
||||||
|
// // 如果需要分数,返回 ScoreId 对象
|
||||||
|
// // 注意:这需要修改返回类型或使用其他方式传递分数
|
||||||
|
// scoreIds.toList()
|
||||||
|
// } else {
|
||||||
|
// emptyList()
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// // 64位系统:正常使用 findWithScores
|
||||||
|
// query?.findWithScores() ?: emptyList()
|
||||||
|
// }
|
||||||
|
// } catch (e: DbException) {
|
||||||
|
// // 如果仍然失败,记录日志并返回空列表
|
||||||
|
// Timber.tag(TAG).e(e, "Vector query with score failed, returning empty list")
|
||||||
|
// emptyList()
|
||||||
|
// } finally {
|
||||||
|
// query?.close()
|
||||||
|
// }
|
||||||
|
// } ?: emptyList()
|
||||||
|
|
||||||
|
// suspend fun query(floatArray: FloatArray, queryCount: Int) = safeDbOp {
|
||||||
|
// val box = getBox<Food>() ?: return@safeDbOp emptyList()
|
||||||
|
//
|
||||||
|
// val query: Query<Food>? = box.query()
|
||||||
|
// ?.equal(Food_.isDel, false)
|
||||||
|
// ?.and()
|
||||||
|
// ?.nearestNeighbors(Food_.foodVector, floatArray, queryCount)
|
||||||
|
// ?.build()
|
||||||
|
//
|
||||||
|
// try {
|
||||||
|
// // 检测是否为 32 位系统
|
||||||
|
// val is32Bit = Build.SUPPORTED_ABIS.any {
|
||||||
|
// it.contains("armeabi") && !it.contains("arm64")
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if (is32Bit) {
|
||||||
|
// // 32位系统:先获取带分数的IDs,再批量获取完整对象
|
||||||
|
// val scoreIds = query?.findWithScoresAndIds()
|
||||||
|
// if (scoreIds != null && scoreIds.isNotEmpty()) {
|
||||||
|
// // 提取 IDs
|
||||||
|
// val ids = scoreIds.map { it.id }.toLongArray()
|
||||||
|
// // 批量获取完整对象
|
||||||
|
// val foods = box.get(ids.toList()).associateBy { it.id }
|
||||||
|
// // 按照分数顺序组装结果(保持排序)
|
||||||
|
// scoreIds.mapNotNull { scoreId ->
|
||||||
|
// foods[scoreId.id]
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// emptyList()
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// // 64位系统:直接使用 findWithScores
|
||||||
|
// query?.findWithScores() ?: emptyList()
|
||||||
|
// }
|
||||||
|
// } catch (e: DbException) {
|
||||||
|
// // 如果仍然失败,记录日志并返回空列表
|
||||||
|
// Timber.tag(TAG).e(e, "Vector query failed, returning empty list")
|
||||||
|
// emptyList()
|
||||||
|
// } finally {
|
||||||
|
// query?.close()
|
||||||
|
// }
|
||||||
|
// } ?: emptyList()
|
||||||
|
// ... existing code ...
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
suspend fun get(id: Long) = safeDbOp {
|
suspend fun get(id: Long) = safeDbOp {
|
||||||
getBox<Food>()?.get(id)
|
getBox<Food>()?.get(id)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ import com.sw.dualscreen.model.response.UserNutrition
|
|||||||
import com.sw.dualscreen.utils.Debouncer
|
import com.sw.dualscreen.utils.Debouncer
|
||||||
import com.sw.dualscreen.utils.SPUtil
|
import com.sw.dualscreen.utils.SPUtil
|
||||||
import com.sw.dualscreen.utils.countDownByFlow
|
import com.sw.dualscreen.utils.countDownByFlow
|
||||||
import com.sw.dualscreen.viewmodel.UserViewModel
|
import com.sw.dualscreen.viewmodel.NetViewModelV2
|
||||||
import com.sw.plate.utils.LightManager
|
import com.sw.plate.utils.LightManager
|
||||||
import com.sw.plate.utils.ToastUtils
|
import com.sw.plate.utils.ToastUtils
|
||||||
import com.sw.plate.utils.arcface.ConfigUtil
|
import com.sw.plate.utils.arcface.ConfigUtil
|
||||||
@@ -73,7 +73,7 @@ import kotlin.math.roundToInt
|
|||||||
class MainScreenPresentation(
|
class MainScreenPresentation(
|
||||||
val activity: MainActivity,
|
val activity: MainActivity,
|
||||||
display: Display,
|
display: Display,
|
||||||
val userViewModel: UserViewModel,
|
val userViewModel: NetViewModelV2,
|
||||||
val recognizeViewModel: RecognizeViewModel,
|
val recognizeViewModel: RecognizeViewModel,
|
||||||
private val onDismissListener: () -> Unit = {}
|
private val onDismissListener: () -> Unit = {}
|
||||||
) : Presentation(activity, display), ViewTreeObserver.OnGlobalLayoutListener {
|
) : Presentation(activity, display), ViewTreeObserver.OnGlobalLayoutListener {
|
||||||
@@ -689,7 +689,7 @@ class MainScreenPresentation(
|
|||||||
Timber.tag(TAG) .d("addFoodToOrder: ${weight == lastAddWeight}")
|
Timber.tag(TAG) .d("addFoodToOrder: ${weight == lastAddWeight}")
|
||||||
// if (weight == lastAddWeight) return
|
// if (weight == lastAddWeight) return
|
||||||
lastAddWeight = weight
|
lastAddWeight = weight
|
||||||
if (weight <= 10) {
|
if (weight <= MainActivity.MIN_BILLABLE_WEIGHT) {
|
||||||
//0-即放即取,1-余量计量
|
//0-即放即取,1-余量计量
|
||||||
mealPickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0
|
mealPickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0
|
||||||
if (foodOrderList.isNotEmpty() && mealPickupMode == 1) {
|
if (foodOrderList.isNotEmpty() && mealPickupMode == 1) {
|
||||||
@@ -702,10 +702,17 @@ class MainScreenPresentation(
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
//val foodPrice = if (isMember) foodInfo.vipPrice else foodInfo.specPrice
|
// 按重量计价:每克单价 = 规格价 / 规格重量,再按会员折扣折算
|
||||||
val foodPrice = (foodInfo.specPrice ?: 0.0) * memberDiscount
|
// 单价 = specPrice / specWeight,金额 = 当前秤上净重 × 单价 × 会员折扣
|
||||||
|
val specWeight = foodInfo.specWeight ?: 0.0
|
||||||
|
val specPrice = foodInfo.specPrice ?: 0.0
|
||||||
|
val price = if (specWeight > 0.0) {
|
||||||
|
weight * (specPrice / specWeight) * memberDiscount
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
}
|
||||||
|
// eatNum 仅用于本地明细展示(份数),不再参与金额计算
|
||||||
val eatNum = activity.getEatNum(weight.toDouble(), foodInfo.specWeight)
|
val eatNum = activity.getEatNum(weight.toDouble(), foodInfo.specWeight)
|
||||||
val price = eatNum * foodPrice
|
|
||||||
if (foodOrderList.isNotEmpty() && mealPickupMode == 1) {
|
if (foodOrderList.isNotEmpty() && mealPickupMode == 1) {
|
||||||
val last = foodOrderList.last()
|
val last = foodOrderList.last()
|
||||||
if (last.isLocalData) {
|
if (last.isLocalData) {
|
||||||
@@ -753,6 +760,8 @@ class MainScreenPresentation(
|
|||||||
}
|
}
|
||||||
var weight = value
|
var weight = value
|
||||||
if (weight < 0) weight = 0
|
if (weight < 0) weight = 0
|
||||||
|
// 扣减餐具重量,后续营养计算、柱图绘制、取餐明细均以净重为准
|
||||||
|
weight = (weight - activity.getDishWeight()).coerceAtLeast(0)
|
||||||
if (activity.settlementMode == 0) {
|
if (activity.settlementMode == 0) {
|
||||||
activity.runOnUiThread {
|
activity.runOnUiThread {
|
||||||
addFoodToOrder(weight, currentFood!!)
|
addFoodToOrder(weight, currentFood!!)
|
||||||
@@ -1034,7 +1043,7 @@ class MainScreenPresentation(
|
|||||||
// pauseCamera()
|
// pauseCamera()
|
||||||
//先查询会员折扣
|
//先查询会员折扣
|
||||||
userViewModel.getMemberDiscount(userId) { discount ->
|
userViewModel.getMemberDiscount(userId) { discount ->
|
||||||
memberDiscount = if (discount == null) 1.0 else discount / 10
|
memberDiscount = discount ?: 1.0
|
||||||
step3ShowRecognizeResult(userId)
|
step3ShowRecognizeResult(userId)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -1379,17 +1388,22 @@ class MainScreenPresentation(
|
|||||||
fun createOrder(notEnoughOneBlock: () -> Unit = {}, successBlock: () -> Unit) {
|
fun createOrder(notEnoughOneBlock: () -> Unit = {}, successBlock: () -> Unit) {
|
||||||
//0-即放即取,1-余量计量
|
//0-即放即取,1-余量计量
|
||||||
mealPickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0
|
mealPickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0
|
||||||
|
// 餐具重量(克),从就餐重量中扣减
|
||||||
|
val dishWeight = activity.getDishWeight()
|
||||||
//if (settlementMode == 0) {
|
//if (settlementMode == 0) {
|
||||||
// //使用联合支付方式
|
// //使用联合支付方式
|
||||||
// ToastUtils.showToast("使用联合支付方式")
|
// ToastUtils.showToast("使用联合支付方式")
|
||||||
// return
|
// return
|
||||||
//}
|
//}
|
||||||
val eatWeight = if (mealPickupMode == 0) {
|
val rawEatWeight = if (mealPickupMode == 0) {
|
||||||
//即放即取
|
//即放即取
|
||||||
recognitionWeight
|
recognitionWeight
|
||||||
} else {
|
} else {
|
||||||
recognitionWeight - lastWeight
|
recognitionWeight - lastWeight
|
||||||
}
|
}
|
||||||
|
// 扣减餐具重量(不足则归零),foodWeight 保持不变
|
||||||
|
val eatWeight = (rawEatWeight - dishWeight).coerceAtLeast(0)
|
||||||
|
activity.log("createOrder,rawEatWeight=$rawEatWeight,dishWeight=$dishWeight,eatWeight=$eatWeight")
|
||||||
//val userNutritionParam = UserNutritionParam(
|
//val userNutritionParam = UserNutritionParam(
|
||||||
// userId = userNutritionData?.userId!!,
|
// userId = userNutritionData?.userId!!,
|
||||||
// foodId = currentFood?.foodId!!,
|
// foodId = currentFood?.foodId!!,
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import com.sw.dualscreen.ext.visible
|
|||||||
import com.sw.dualscreen.model.response.MemberInfo
|
import com.sw.dualscreen.model.response.MemberInfo
|
||||||
import com.sw.dualscreen.model.response.TextBean
|
import com.sw.dualscreen.model.response.TextBean
|
||||||
import com.sw.dualscreen.utils.SpannedUtils
|
import com.sw.dualscreen.utils.SpannedUtils
|
||||||
import com.sw.dualscreen.viewmodel.UserViewModel
|
import com.sw.dualscreen.viewmodel.NetViewModelV2
|
||||||
import com.sw.plate.utils.ToastUtils
|
import com.sw.plate.utils.ToastUtils
|
||||||
import com.sw.plate.utils.arcface.ConfigUtil
|
import com.sw.plate.utils.arcface.ConfigUtil
|
||||||
import com.sw.plate.utils.arcface.ErrorCodeUtil
|
import com.sw.plate.utils.arcface.ErrorCodeUtil
|
||||||
@@ -56,7 +56,7 @@ import java.util.concurrent.atomic.AtomicBoolean
|
|||||||
class PayPresentation(
|
class PayPresentation(
|
||||||
val activity: PayActivity,
|
val activity: PayActivity,
|
||||||
display: Display,
|
display: Display,
|
||||||
val userViewModel: UserViewModel,
|
val userViewModel: NetViewModelV2,
|
||||||
val recognizeViewModel: RecognizeViewModel,
|
val recognizeViewModel: RecognizeViewModel,
|
||||||
private val onDismissListener: () -> Unit = {}
|
private val onDismissListener: () -> Unit = {}
|
||||||
) : Presentation(activity, display), ViewTreeObserver.OnGlobalLayoutListener {
|
) : Presentation(activity, display), ViewTreeObserver.OnGlobalLayoutListener {
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
package com.sw.dualscreen.repository.v2
|
||||||
|
|
||||||
|
import com.sw.dualscreen.model.request.v2.BindUserOrderRequest
|
||||||
|
import com.sw.dualscreen.model.request.v2.PlaceOrderRequest
|
||||||
|
import com.sw.dualscreen.model.response.ApiResponse
|
||||||
|
import com.sw.dualscreen.model.response.DeviceConfig
|
||||||
|
import com.sw.dualscreen.model.response.FoodSearchReq
|
||||||
|
import com.sw.dualscreen.model.response.UserNutrition
|
||||||
|
import com.sw.dualscreen.model.response.v2.CollectedFoodV2
|
||||||
|
import com.sw.dualscreen.model.response.v2.FaceVO
|
||||||
|
import com.sw.dualscreen.model.response.v2.NewFoodInfo
|
||||||
|
import com.sw.dualscreen.model.response.v2.NewMemberInfo
|
||||||
|
import com.sw.dualscreen.model.response.v2.SettlementOrder
|
||||||
|
import com.sw.dualscreen.network.api.ApiServiceV2
|
||||||
|
import com.sw.dualscreen.repository.BaseRepository
|
||||||
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
|
import okhttp3.MultipartBody
|
||||||
|
import okhttp3.RequestBody
|
||||||
|
import okhttp3.RequestBody.Companion.asRequestBody
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class RemoteRepositoryV2 constructor(
|
||||||
|
private val apiService: ApiServiceV2
|
||||||
|
) : BaseRepository() {
|
||||||
|
|
||||||
|
suspend fun getDeviceConfig(): ApiResponse<DeviceConfig?> {
|
||||||
|
return safeApiCall {
|
||||||
|
apiService.getDeviceConfig()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getFacePage(
|
||||||
|
pageNum: Long = 1L,
|
||||||
|
pageSize: Long = 100L
|
||||||
|
): ApiResponse<List<FaceVO>?> {
|
||||||
|
return safeApiCall {
|
||||||
|
apiService.getFacePage(
|
||||||
|
request = mapOf(
|
||||||
|
"pageNum" to pageNum,
|
||||||
|
"pageSize" to pageSize
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getFaceIncrement(
|
||||||
|
pageNum: Long = 1L,
|
||||||
|
pageSize: Long = 100L,
|
||||||
|
timestamp: Long
|
||||||
|
): ApiResponse<List<FaceVO>?> {
|
||||||
|
return safeApiCall {
|
||||||
|
apiService.getFaceIncrement(
|
||||||
|
request = mapOf(
|
||||||
|
"pageNum" to pageNum,
|
||||||
|
"pageSize" to pageSize,
|
||||||
|
"timestamp" to timestamp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getFoodByNames(
|
||||||
|
nameList: List<String>,
|
||||||
|
deviceType: Int = 2
|
||||||
|
): ApiResponse<List<NewFoodInfo>?> {
|
||||||
|
return safeApiCall {
|
||||||
|
apiService.getFoodByNames(
|
||||||
|
request = FoodSearchReq(nameList, deviceType)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun searchFood(
|
||||||
|
name: String,
|
||||||
|
pageNum: Long = 1L,
|
||||||
|
pageSize: Long = 100L
|
||||||
|
): ApiResponse<List<NewFoodInfo>?> {
|
||||||
|
return safeApiCall {
|
||||||
|
apiService.searchFood(
|
||||||
|
request = mapOf(
|
||||||
|
"name" to name,
|
||||||
|
"pageNum" to pageNum,
|
||||||
|
"pageSize" to pageSize
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getUserCurrentFood(
|
||||||
|
userId: Long
|
||||||
|
): ApiResponse<UserNutrition?> {
|
||||||
|
return safeApiCall {
|
||||||
|
apiService.getUserCurrentFood(
|
||||||
|
request = mapOf("id" to userId)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun placeOrder(
|
||||||
|
orderRequest: PlaceOrderRequest
|
||||||
|
): ApiResponse<String?> {
|
||||||
|
return safeApiCall {
|
||||||
|
apiService.placeOrder(request = orderRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getSettlementOrders(
|
||||||
|
userId: Long
|
||||||
|
): ApiResponse<SettlementOrder?> {
|
||||||
|
return safeApiCall {
|
||||||
|
apiService.getSettlementOrders(
|
||||||
|
request = mapOf("id" to userId)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getMemberInfo(
|
||||||
|
userId: Long
|
||||||
|
): ApiResponse<NewMemberInfo?> {
|
||||||
|
return safeApiCall {
|
||||||
|
apiService.getMemberInfo(
|
||||||
|
request = mapOf("id" to userId)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getMemberInfoByPhone(
|
||||||
|
phone: String,
|
||||||
|
password: String = ""
|
||||||
|
): ApiResponse<NewMemberInfo?> {
|
||||||
|
return safeApiCall {
|
||||||
|
apiService.getMemberInfoByPhone(
|
||||||
|
request = mapOf(
|
||||||
|
"phone" to phone,
|
||||||
|
"password" to password
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getMemberDiscount(
|
||||||
|
userId: Long
|
||||||
|
): ApiResponse<String?> {
|
||||||
|
return safeApiCall {
|
||||||
|
apiService.getMemberDiscount(
|
||||||
|
request = mapOf("id" to userId)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun bindUserOrder(
|
||||||
|
userId: Long,
|
||||||
|
orderNo: String,
|
||||||
|
mode: Int? = null
|
||||||
|
): ApiResponse<Any?> {
|
||||||
|
return safeApiCall {
|
||||||
|
apiService.bindUserOrder(
|
||||||
|
request = BindUserOrderRequest(userId, orderNo, mode)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getCollectPage(
|
||||||
|
pageNum: Long = 1L,
|
||||||
|
pageSize: Long = 100L,
|
||||||
|
foodName: String? = null
|
||||||
|
): ApiResponse<List<CollectedFoodV2>?> {
|
||||||
|
return safeApiCall {
|
||||||
|
val request = mutableMapOf<String, Any>(
|
||||||
|
"pageNum" to pageNum,
|
||||||
|
"pageSize" to pageSize
|
||||||
|
)
|
||||||
|
if (!foodName.isNullOrEmpty()) {
|
||||||
|
request["foodName"] = foodName
|
||||||
|
}
|
||||||
|
apiService.getCollectPage(request = request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getCollectVectorPage(
|
||||||
|
pageNum: Long = 1L,
|
||||||
|
pageSize: Long = 100L
|
||||||
|
): ApiResponse<List<CollectedFoodV2>?> {
|
||||||
|
return safeApiCall {
|
||||||
|
apiService.getCollectVectorPage(
|
||||||
|
request = mutableMapOf(
|
||||||
|
"pageNum" to pageNum,
|
||||||
|
"pageSize" to pageSize
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun uploadCollect(
|
||||||
|
foodId: Long,
|
||||||
|
foodName: String,
|
||||||
|
version: String,
|
||||||
|
foodVector: String,
|
||||||
|
fileList: List<File>
|
||||||
|
): ApiResponse<List<String>?> {
|
||||||
|
val params = hashMapOf<String, RequestBody>()
|
||||||
|
params["foodId"] = foodId.toString().toRequestBody("text/plain".toMediaTypeOrNull())
|
||||||
|
params["foodName"] = foodName.toRequestBody("text/plain".toMediaTypeOrNull())
|
||||||
|
params["version"] = version.toRequestBody("text/plain".toMediaTypeOrNull())
|
||||||
|
params["foodVector"] = foodVector.toRequestBody("text/plain".toMediaTypeOrNull())
|
||||||
|
|
||||||
|
val fileParts = mutableListOf<MultipartBody.Part>()
|
||||||
|
fileList.forEach { file ->
|
||||||
|
val requestFile = file.asRequestBody("multipart/form-data".toMediaTypeOrNull())
|
||||||
|
val filePart = MultipartBody.Part.createFormData(
|
||||||
|
"foodPics",
|
||||||
|
file.name,
|
||||||
|
requestFile
|
||||||
|
)
|
||||||
|
fileParts.add(filePart)
|
||||||
|
}
|
||||||
|
|
||||||
|
return safeApiCall {
|
||||||
|
apiService.uploadCollect(params = params, foodPics = fileParts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun deleteCollect(
|
||||||
|
foodId: Long,
|
||||||
|
version: String
|
||||||
|
): ApiResponse<Any?> {
|
||||||
|
return safeApiCall {
|
||||||
|
apiService.deleteCollect(
|
||||||
|
request = mapOf<String, Any>(
|
||||||
|
"foodId" to foodId,
|
||||||
|
"version" to version
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ object SensorScaleUtils {
|
|||||||
var isZero = false
|
var isZero = false
|
||||||
private var callback: Callback? = {}
|
private var callback: Callback? = {}
|
||||||
private var lastWeight: Double? = null
|
private var lastWeight: Double? = null
|
||||||
|
private var lastCallbackWeight: Double? = null
|
||||||
|
|
||||||
private fun init() {
|
private fun init() {
|
||||||
mSensorScale = SensorScale(object : OnScaleResult {
|
mSensorScale = SensorScale(object : OnScaleResult {
|
||||||
@@ -45,9 +46,13 @@ object SensorScaleUtils {
|
|||||||
callback?.invoke(value)
|
callback?.invoke(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
weightCallbackList.forEach {
|
// 去重:仅在重量变化时回调 weightCallbackList,避免同一重量反复触发
|
||||||
val currentWeight = value * 1000
|
if (lastCallbackWeight != value) {
|
||||||
it.invoke(currentWeight.roundToInt())
|
lastCallbackWeight = value
|
||||||
|
weightCallbackList.forEach {
|
||||||
|
val currentWeight = value * 1000
|
||||||
|
it.invoke(currentWeight.roundToInt())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ class CameraUtils(private var activity: ComponentActivity) {
|
|||||||
photoCaptureHelper = PhotoCaptureHelper(
|
photoCaptureHelper = PhotoCaptureHelper(
|
||||||
context = activity,
|
context = activity,
|
||||||
cameraController = cameraController!!,
|
cameraController = cameraController!!,
|
||||||
onSuccess = {},
|
|
||||||
onError = { msg ->
|
onError = { msg ->
|
||||||
//toast(msg)
|
//toast(msg)
|
||||||
failCallback?.invoke(msg)
|
failCallback?.invoke(msg)
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
package com.sw.dualscreen.utils;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
|
||||||
|
import com.sw.plate.utils.arcface.facedb.FaceDatabase;
|
||||||
|
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 assets 目录下的测试人脸数据库导入数据到 Room 数据库
|
||||||
|
*
|
||||||
|
* <p>以分批方式读取,每批 {@link #BATCH_SIZE} 条,边读边写,
|
||||||
|
* 避免 4350 条 + 17MB 特征数据一次性加载到内存。</p>
|
||||||
|
*/
|
||||||
|
public class FaceDbImporter {
|
||||||
|
|
||||||
|
private static final String TAG = "FaceDbImporter";
|
||||||
|
|
||||||
|
/** 每批写入条数 */
|
||||||
|
private static final int BATCH_SIZE = 200;
|
||||||
|
|
||||||
|
/** assets 中测试数据库的相对路径 */
|
||||||
|
private static final String ASSETS_DB_PATH = "db/faceDB.db";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导入回调
|
||||||
|
*/
|
||||||
|
public interface ImportCallback {
|
||||||
|
/** 进度更新,已切到主线程 */
|
||||||
|
void onProgress(int current, int total);
|
||||||
|
/** 导入失败,已切到主线程 */
|
||||||
|
void onError(String message);
|
||||||
|
/** 导入完成,已切到主线程 */
|
||||||
|
void onComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
private FaceDbImporter() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 assets 导入测试人脸数据到 Room 数据库
|
||||||
|
*
|
||||||
|
* @param context 上下文
|
||||||
|
* @param callback 回调(所有回调均已在主线程)
|
||||||
|
*/
|
||||||
|
public static void importFromAssets(Context context, ImportCallback callback) {
|
||||||
|
Context appContext = context.getApplicationContext();
|
||||||
|
new Thread(() -> {
|
||||||
|
File tempFile = null;
|
||||||
|
SQLiteDatabase testDb = null;
|
||||||
|
Cursor cursor = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 从 assets 复制到缓存目录(SQLiteDatabase 需要文件路径)
|
||||||
|
tempFile = new File(appContext.getCacheDir(), "faceDB_import_temp.db");
|
||||||
|
copyAssetToFile(appContext, ASSETS_DB_PATH, tempFile);
|
||||||
|
|
||||||
|
// 2. 以只读方式打开测试数据库
|
||||||
|
testDb = SQLiteDatabase.openDatabase(
|
||||||
|
tempFile.getAbsolutePath(),
|
||||||
|
null,
|
||||||
|
SQLiteDatabase.OPEN_READONLY
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. 查询总记录数
|
||||||
|
int totalCount = 0;
|
||||||
|
Cursor countCursor = testDb.rawQuery("SELECT COUNT(*) FROM face", null);
|
||||||
|
if (countCursor.moveToFirst()) {
|
||||||
|
totalCount = countCursor.getInt(0);
|
||||||
|
}
|
||||||
|
countCursor.close();
|
||||||
|
|
||||||
|
if (totalCount == 0) {
|
||||||
|
postToMain(() -> {
|
||||||
|
if (callback != null) callback.onComplete();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 分批读取 + 批量写入
|
||||||
|
cursor = testDb.rawQuery(
|
||||||
|
"SELECT user_name, feature_data, register_time FROM face ORDER BY faceId",
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
List<FaceEntity> batch = new ArrayList<>(BATCH_SIZE);
|
||||||
|
int processedCount = 0;
|
||||||
|
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
String userName = cursor.getString(0);
|
||||||
|
byte[] featureData = cursor.getBlob(1);
|
||||||
|
long registerTime = cursor.getLong(2);
|
||||||
|
|
||||||
|
FaceEntity entity = new FaceEntity(userName, null, featureData);
|
||||||
|
entity.setUserType("2");
|
||||||
|
entity.setRegisterTime(registerTime);
|
||||||
|
batch.add(entity);
|
||||||
|
|
||||||
|
if (batch.size() >= BATCH_SIZE) {
|
||||||
|
FaceDatabase.getInstance(appContext).faceDao().insert(batch);
|
||||||
|
processedCount += batch.size();
|
||||||
|
notifyProgress(callback, processedCount, totalCount);
|
||||||
|
batch.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 写入剩余不足一批的数据
|
||||||
|
if (!batch.isEmpty()) {
|
||||||
|
FaceDatabase.getInstance(appContext).faceDao().insert(batch);
|
||||||
|
processedCount += batch.size();
|
||||||
|
notifyProgress(callback, processedCount, totalCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
L.d(TAG, "导入完成,共 " + processedCount + " 条记录");
|
||||||
|
postToMain(() -> {
|
||||||
|
if (callback != null) callback.onComplete();
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
L.e(TAG, "导入失败: " + e.getMessage());
|
||||||
|
postToMain(() -> {
|
||||||
|
if (callback != null) callback.onError(e.getMessage());
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
if (cursor != null) cursor.close();
|
||||||
|
if (testDb != null && testDb.isOpen()) testDb.close();
|
||||||
|
if (tempFile != null && tempFile.exists()) tempFile.delete();
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 assets 复制文件到指定路径
|
||||||
|
*/
|
||||||
|
private static void copyAssetToFile(Context context, String assetPath, File destFile)
|
||||||
|
throws Exception {
|
||||||
|
try (InputStream is = context.getAssets().open(assetPath);
|
||||||
|
FileOutputStream fos = new FileOutputStream(destFile)) {
|
||||||
|
byte[] buffer = new byte[8192];
|
||||||
|
int length;
|
||||||
|
while ((length = is.read(buffer)) > 0) {
|
||||||
|
fos.write(buffer, 0, length);
|
||||||
|
}
|
||||||
|
fos.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void notifyProgress(ImportCallback callback, int current, int total) {
|
||||||
|
postToMain(() -> {
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onProgress(current, total);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void postToMain(Runnable runnable) {
|
||||||
|
new Handler(Looper.getMainLooper()).post(runnable);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
package com.sw.dualscreen.utils
|
package com.sw.dualscreen.utils
|
||||||
|
|
||||||
import androidx.lifecycle.LifecycleCoroutineScope
|
import androidx.lifecycle.LifecycleCoroutineScope
|
||||||
import com.sw.dualscreen.GlobalData
|
import com.sw.dualscreen.model.response.v2.CollectedFoodV2
|
||||||
import com.sw.dualscreen.model.response.FoodVector
|
|
||||||
import com.sw.dualscreen.objbox.Food
|
import com.sw.dualscreen.objbox.Food
|
||||||
import com.sw.dualscreen.objbox.ObjectBox
|
import com.sw.dualscreen.objbox.ObjectBox
|
||||||
import com.sw.dualscreen.viewmodel.UserViewModel
|
import com.sw.dualscreen.viewmodel.NetViewModelV2
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
object FoodVectorTool {
|
object FoodVectorTool {
|
||||||
@@ -15,19 +14,13 @@ object FoodVectorTool {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 统一入口:分页拉取所有食物向量数据并保存到本地 ObjectBox
|
* 统一入口:分页拉取所有食物向量数据并保存到本地 ObjectBox
|
||||||
*
|
|
||||||
* @param userViewModel 请求 ViewModel
|
|
||||||
* @param lifecycleScope 协程作用域(用于保存操作)
|
|
||||||
* @param successBlock 全部数据拉取并保存成功后回调
|
|
||||||
* @param failureBlock 任意一次请求失败时回调,参数为错误信息
|
|
||||||
*/
|
*/
|
||||||
fun loadAndSaveFoodVector(
|
fun loadAndSaveFoodVector(
|
||||||
userViewModel: UserViewModel,
|
userViewModel: NetViewModelV2,
|
||||||
lifecycleScope: LifecycleCoroutineScope,
|
lifecycleScope: LifecycleCoroutineScope,
|
||||||
successBlock: () -> Unit,
|
successBlock: () -> Unit,
|
||||||
failureBlock: (String) -> Unit
|
failureBlock: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
// 每次调用从第 1 页开始,避免单例状态污染
|
|
||||||
fetchPage(
|
fetchPage(
|
||||||
pageNum = 1,
|
pageNum = 1,
|
||||||
userViewModel = userViewModel,
|
userViewModel = userViewModel,
|
||||||
@@ -37,68 +30,47 @@ object FoodVectorTool {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 递归分页请求(内部方法,外部不感知分页细节)
|
|
||||||
*
|
|
||||||
* @param pageNum 当前页码
|
|
||||||
* @param userViewModel 请求 ViewModel
|
|
||||||
* @param lifecycleScope 协程作用域
|
|
||||||
* @param successBlock 成功回调
|
|
||||||
* @param failureBlock 失败回调
|
|
||||||
*/
|
|
||||||
private fun fetchPage(
|
private fun fetchPage(
|
||||||
pageNum: Int,
|
pageNum: Int,
|
||||||
userViewModel: UserViewModel,
|
userViewModel: NetViewModelV2,
|
||||||
lifecycleScope: LifecycleCoroutineScope,
|
lifecycleScope: LifecycleCoroutineScope,
|
||||||
successBlock: () -> Unit,
|
successBlock: () -> Unit,
|
||||||
failureBlock: (String) -> Unit
|
failureBlock: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
userViewModel.getCollectedFoodVector(
|
userViewModel.getCollectVectorPage(
|
||||||
param = mutableMapOf(
|
pageNum = pageNum.toLong(),
|
||||||
"pageNum" to "$pageNum",
|
pageSize = PAGE_SIZE.toLong(),
|
||||||
"pageSize" to "$PAGE_SIZE",
|
onSuccess = { items ->
|
||||||
"version" to GlobalData.foodModelVersion
|
if (pageNum == 1 && items.isEmpty()) {
|
||||||
)
|
successBlock()
|
||||||
) { items ->
|
return@getCollectVectorPage
|
||||||
if (items == null) {
|
}
|
||||||
failureBlock("未查询到向量数据")
|
if (items.isNotEmpty()) {
|
||||||
return@getCollectedFoodVector
|
saveFoodVector(lifecycleScope, items)
|
||||||
}
|
}
|
||||||
// 第一页为空,视为成功(数据库无数据)
|
if (items.size >= PAGE_SIZE) {
|
||||||
if (pageNum == 1 && items.isEmpty()) {
|
fetchPage(pageNum + 1, userViewModel, lifecycleScope, successBlock, failureBlock)
|
||||||
successBlock()
|
} else {
|
||||||
return@getCollectedFoodVector
|
successBlock()
|
||||||
}
|
}
|
||||||
// 有数据则保存
|
},
|
||||||
if (items.isNotEmpty()) {
|
onFailure = { failureBlock(it) }
|
||||||
saveFoodVector(lifecycleScope, items)
|
)
|
||||||
}
|
|
||||||
// 数据量达到一页上限,继续拉取下一页
|
|
||||||
if (items.size >= PAGE_SIZE) {
|
|
||||||
fetchPage(pageNum + 1, userViewModel, lifecycleScope, successBlock, failureBlock)
|
|
||||||
} else {
|
|
||||||
successBlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将向量数据列表保存到 ObjectBox 本地数据库
|
* 将 CollectedFoodV2 列表保存到 ObjectBox 本地数据库
|
||||||
*
|
|
||||||
* @param lifecycleScope 协程作用域
|
|
||||||
* @param list 待保存的食物向量数据列表
|
|
||||||
*/
|
*/
|
||||||
private fun saveFoodVector(lifecycleScope: LifecycleCoroutineScope, list: List<FoodVector>) {
|
private fun saveFoodVector(lifecycleScope: LifecycleCoroutineScope, list: List<CollectedFoodV2>) {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val vectorList = list.map { vt ->
|
val vectorList = list.map { item ->
|
||||||
// 解析向量字符串 "[0.1,0.2,...]" 为 FloatArray
|
val foodVector = item.foodVector?.removeSurrounding("[", "]")
|
||||||
val foodVector = vt.foodVector?.removeSurrounding("[", "]")
|
|
||||||
?.split(",")?.map { it.toFloatOrNull() ?: 0.0f }?.toFloatArray()
|
?.split(",")?.map { it.toFloatOrNull() ?: 0.0f }?.toFloatArray()
|
||||||
Food(
|
Food(
|
||||||
collectId = vt.id,
|
collectId = item.foodId,
|
||||||
foodId = vt.foodId,
|
foodId = item.foodId,
|
||||||
foodName = vt.foodName,
|
foodName = item.foodName,
|
||||||
version = vt.version,
|
version = item.version,
|
||||||
foodVector = foodVector
|
foodVector = foodVector
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,17 +15,23 @@ object ImageUtil {
|
|||||||
|
|
||||||
fun uriToBitmap(context: Context, uri: Uri): Bitmap? {
|
fun uriToBitmap(context: Context, uri: Uri): Bitmap? {
|
||||||
return try {
|
return try {
|
||||||
val options = BitmapFactory.Options()
|
val options = BitmapFactory.Options().apply {
|
||||||
//options.inSampleSize = 2; // 这会将图片的尺寸缩小到原来的1/2
|
inJustDecodeBounds = true
|
||||||
options.inJustDecodeBounds = false
|
}
|
||||||
// options.inPreferredConfig = Bitmap.Config.ARGB_8888
|
// 先只读尺寸,计算合适的采样率
|
||||||
options.inPreferredConfig = Bitmap.Config.RGB_565
|
|
||||||
context.contentResolver.openInputStream(uri)?.use { stream ->
|
context.contentResolver.openInputStream(uri)?.use { stream ->
|
||||||
BitmapFactory.decodeStream(stream, null, options)
|
BitmapFactory.decodeStream(stream, null, options)
|
||||||
// BitmapFactory.decodeStream(stream)
|
}
|
||||||
|
// 目标最长边不超过 800px,减少内存占用
|
||||||
|
val maxDim = maxOf(options.outWidth, options.outHeight)
|
||||||
|
val sampleSize = (maxDim / 800).coerceAtLeast(1)
|
||||||
|
options.apply {
|
||||||
|
inJustDecodeBounds = false
|
||||||
|
inSampleSize = sampleSize
|
||||||
|
inPreferredConfig = Bitmap.Config.RGB_565
|
||||||
}
|
}
|
||||||
context.contentResolver.openInputStream(uri)?.use { stream ->
|
context.contentResolver.openInputStream(uri)?.use { stream ->
|
||||||
BitmapFactory.decodeStream(stream)
|
BitmapFactory.decodeStream(stream, null, options)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ package com.sw.dualscreen.utils
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
import androidx.camera.core.ImageCapture
|
import androidx.camera.core.ImageCapture
|
||||||
import androidx.camera.core.ImageCaptureException
|
import androidx.camera.core.ImageCaptureException
|
||||||
import androidx.camera.view.CameraController
|
import androidx.camera.view.CameraController
|
||||||
@@ -19,9 +22,9 @@ import java.io.File
|
|||||||
class PhotoCaptureHelper(
|
class PhotoCaptureHelper(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val cameraController: CameraController,
|
private val cameraController: CameraController,
|
||||||
private val onSuccess: (Uri) -> Unit = {},
|
|
||||||
private val onError: (String) -> Unit = {}
|
private val onError: (String) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val callbackList: MutableList<(Uri) -> Unit> = mutableListOf()
|
private val callbackList: MutableList<(Uri) -> Unit> = mutableListOf()
|
||||||
fun addSuccessCallback(callback:(Uri) -> Unit) {
|
fun addSuccessCallback(callback:(Uri) -> Unit) {
|
||||||
if (callbackList.contains(callback).not()) {
|
if (callbackList.contains(callback).not()) {
|
||||||
@@ -41,7 +44,8 @@ class PhotoCaptureHelper(
|
|||||||
*/
|
*/
|
||||||
fun takePhoto(
|
fun takePhoto(
|
||||||
fileNamePrefix: String = "IMG_",
|
fileNamePrefix: String = "IMG_",
|
||||||
fileExtension: String = ".jpg"
|
fileExtension: String = ".jpg",
|
||||||
|
isRetry: Boolean = false
|
||||||
) {
|
) {
|
||||||
Timber.d("开始拍照采集")
|
Timber.d("开始拍照采集")
|
||||||
|
|
||||||
@@ -63,20 +67,22 @@ class PhotoCaptureHelper(
|
|||||||
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
|
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
|
||||||
val photoUri = outputFileResults.savedUri ?: Uri.fromFile(photoFile)
|
val photoUri = outputFileResults.savedUri ?: Uri.fromFile(photoFile)
|
||||||
Timber.d("照片保存成功: $photoUri")
|
Timber.d("照片保存成功: $photoUri")
|
||||||
onSuccess(photoUri)
|
|
||||||
callbackList.forEach {
|
callbackList.forEach {
|
||||||
it(photoUri)
|
it(photoUri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onError(exception: ImageCaptureException) {
|
override fun onError(exception: ImageCaptureException) {
|
||||||
|
Timber.e(exception)
|
||||||
val errorMsg = "拍照失败: ${exception.message}"
|
val errorMsg = "拍照失败: ${exception.message}"
|
||||||
if (errorMsg.contains("Not bound to a valid Camera")) {
|
if (errorMsg.contains("Not bound to a valid Camera") && !isRetry) {
|
||||||
if (bindCamera != null) {
|
Timber.w(exception, "相机未绑定,尝试重新绑定后重试")
|
||||||
bindCamera?.invoke()
|
bindCamera?.invoke()
|
||||||
//takePhoto()
|
// 延迟重试,等待相机异步绑定完成
|
||||||
}
|
Handler(Looper.getMainLooper()).postDelayed({
|
||||||
|
takePhoto(fileNamePrefix, fileExtension, isRetry = true)
|
||||||
|
}, 300)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
Timber.e(exception, errorMsg)
|
Timber.e(exception, errorMsg)
|
||||||
onError(errorMsg)
|
onError(errorMsg)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import com.sw.dualscreen.databinding.BottomSheetDialogBinding
|
|||||||
import com.sw.dualscreen.model.response.FoodInfo
|
import com.sw.dualscreen.model.response.FoodInfo
|
||||||
import com.sw.dualscreen.utils.Debouncer
|
import com.sw.dualscreen.utils.Debouncer
|
||||||
import com.sw.dualscreen.utils.KeyboardUtils
|
import com.sw.dualscreen.utils.KeyboardUtils
|
||||||
import com.sw.dualscreen.viewmodel.UserViewModel
|
import com.sw.dualscreen.viewmodel.NetViewModelV2
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import androidx.core.graphics.drawable.toDrawable
|
import androidx.core.graphics.drawable.toDrawable
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ import androidx.core.graphics.drawable.toDrawable
|
|||||||
*/
|
*/
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
class CustomBottomSheetDialog(
|
class CustomBottomSheetDialog(
|
||||||
val viewModel: UserViewModel,
|
val viewModel: NetViewModelV2,
|
||||||
val itemClickCallback: (FoodInfo) -> Unit
|
val itemClickCallback: (FoodInfo) -> Unit
|
||||||
) : BottomSheetDialogFragment() {
|
) : BottomSheetDialogFragment() {
|
||||||
private lateinit var binding: BottomSheetDialogBinding
|
private lateinit var binding: BottomSheetDialogBinding
|
||||||
@@ -87,7 +87,7 @@ class CustomBottomSheetDialog(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun newInstance(
|
fun newInstance(
|
||||||
viewModel: UserViewModel,
|
viewModel: NetViewModelV2,
|
||||||
itemClickCallback: (FoodInfo) -> Unit
|
itemClickCallback: (FoodInfo) -> Unit
|
||||||
): CustomBottomSheetDialog {
|
): CustomBottomSheetDialog {
|
||||||
return CustomBottomSheetDialog(viewModel, itemClickCallback)
|
return CustomBottomSheetDialog(viewModel, itemClickCallback)
|
||||||
|
|||||||
@@ -0,0 +1,575 @@
|
|||||||
|
package com.sw.dualscreen.viewmodel
|
||||||
|
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.arcsoft.face.ErrorInfo
|
||||||
|
import com.sw.dualscreen.GlobalData
|
||||||
|
import com.sw.dualscreen.model.request.UserNutritionParam
|
||||||
|
import com.sw.dualscreen.model.request.v2.BindUserOrderRequest
|
||||||
|
import com.sw.dualscreen.model.request.v2.PlaceOrderRequest
|
||||||
|
import com.sw.dualscreen.model.response.ApiResponse
|
||||||
|
import com.sw.dualscreen.model.response.DinnerType
|
||||||
|
import com.sw.dualscreen.model.response.FoodInfo
|
||||||
|
import com.sw.dualscreen.model.response.FoodOrderModel
|
||||||
|
import com.sw.dualscreen.model.response.FoodSearchReq
|
||||||
|
import com.sw.dualscreen.model.response.MemberInfo
|
||||||
|
import com.sw.dualscreen.model.response.UserNutrition
|
||||||
|
import com.sw.dualscreen.model.response.v2.CollectedFoodV2
|
||||||
|
import com.sw.dualscreen.model.response.v2.FaceVO
|
||||||
|
import com.sw.dualscreen.model.response.v2.NewFoodInfo
|
||||||
|
import com.sw.dualscreen.model.response.v2.NewMemberInfo
|
||||||
|
import com.sw.dualscreen.model.response.v2.SettlementOrder
|
||||||
|
import com.sw.dualscreen.model.response.v2.toFoodInfo
|
||||||
|
import com.sw.dualscreen.model.response.v2.toFoodOrderModel
|
||||||
|
import com.sw.dualscreen.model.response.v2.toMemberInfo
|
||||||
|
import com.sw.dualscreen.network.ApiClient
|
||||||
|
import com.sw.dualscreen.utils.FileUtil
|
||||||
|
import com.sw.dualscreen.utils.SpTool
|
||||||
|
import com.sw.plate.App
|
||||||
|
import com.sw.plate.utils.Base64
|
||||||
|
import com.sw.plate.utils.ToastUtils
|
||||||
|
import com.sw.plate.utils.arcface.FaceApi
|
||||||
|
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class NetViewModelV2 : BaseViewModel() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "NetViewModelV2"
|
||||||
|
const val PAGE_SIZE = 100L
|
||||||
|
}
|
||||||
|
|
||||||
|
private val faceApi: FaceApi = FaceApi()
|
||||||
|
|
||||||
|
/** 人脸加载完成 */
|
||||||
|
private val _loadFaceResult = MutableStateFlow<Boolean>(false)
|
||||||
|
val loadFaceResult: StateFlow<Boolean> = _loadFaceResult
|
||||||
|
|
||||||
|
/** 饭点类型 早餐/午餐/晚餐 */
|
||||||
|
private val _dinnerTypeInfo = MutableStateFlow<DinnerType?>(null)
|
||||||
|
val dinnerTypeInfo: StateFlow<DinnerType?> = _dinnerTypeInfo
|
||||||
|
|
||||||
|
fun getDeviceConfig(
|
||||||
|
onSuccess: (config: com.sw.dualscreen.model.response.DeviceConfig?) -> Unit,
|
||||||
|
onFailure: (message: String) -> Unit
|
||||||
|
) {
|
||||||
|
launch {
|
||||||
|
try {
|
||||||
|
val response = ApiClient.repositoryV2.getDeviceConfig()
|
||||||
|
if (parseResponse(response)) {
|
||||||
|
onSuccess(response.data)
|
||||||
|
} else {
|
||||||
|
onFailure(response.msg ?: "获取设备配置失败")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "获取设备配置异常")
|
||||||
|
onFailure("网络异常: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取人脸全量数据(递归分页 + FaceApi 集成) */
|
||||||
|
fun getFacePage(
|
||||||
|
pageNum: Long = 1L,
|
||||||
|
pageSize: Long = PAGE_SIZE,
|
||||||
|
onSuccess: () -> Unit = {},
|
||||||
|
onFailure: (String) -> Unit = {}
|
||||||
|
) {
|
||||||
|
var currentPageNum = pageNum
|
||||||
|
Timber.tag(TAG).d("getFacePage index = $currentPageNum")
|
||||||
|
launch {
|
||||||
|
_loadFaceResult.value = false
|
||||||
|
// 重置持久化时间戳和内存累加器
|
||||||
|
SpTool.lastFaceTimestamp = 0
|
||||||
|
lastFaceTimestamp = 0L
|
||||||
|
FileUtil.saveLog("获取人脸全量数据开始,重置时间戳为0")
|
||||||
|
val response = ApiClient.repositoryV2.getFacePage(currentPageNum, pageSize)
|
||||||
|
if (!parseResponse(response)) {
|
||||||
|
onFailure("获取人脸数据失败,${response.msg}(${response.code})")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Default) {
|
||||||
|
val list = response.data ?: emptyList()
|
||||||
|
if (pageNum == 1L && list.isEmpty()) {
|
||||||
|
onFailure("查询人脸数据为空")
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
val faceEntity = list.map { vo ->
|
||||||
|
FaceEntity(
|
||||||
|
vo.userId, // userName
|
||||||
|
Base64.decode(vo.faceFeature), // featureData
|
||||||
|
vo.personType ?: if (vo.member == true) "1" else "2", // userType
|
||||||
|
vo.cardNo ?: "", // cardNo
|
||||||
|
vo.userId ?: "", // userId
|
||||||
|
vo.userFaceId ?: "", // userFaceId
|
||||||
|
vo.member ?: false, // member
|
||||||
|
vo.faceUpdateTimestamp?.toLongOrNull() ?: 0L // faceUpdateTimestamp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
faceApi.updateFaceData(currentPageNum.toInt(), faceEntity)
|
||||||
|
// 取当前页最大时间戳(后端返回 String 类型,需转为 Long 比较),跨页累加防止中间页有更大值被覆盖
|
||||||
|
val pageMaxTimestamp = list.maxOf { it.faceUpdateTimestamp?.toLongOrNull() ?: 0L }
|
||||||
|
lastFaceTimestamp = maxOf(lastFaceTimestamp, pageMaxTimestamp)
|
||||||
|
if (list.size >= pageSize) {
|
||||||
|
currentPageNum++
|
||||||
|
getFacePage(currentPageNum, pageSize, onSuccess, onFailure)
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
SpTool.lastFaceTimestamp = lastFaceTimestamp
|
||||||
|
FileUtil.saveLog("获取人脸[全量]数据结束,时间戳为:$lastFaceTimestamp")
|
||||||
|
_loadFaceResult.value = true
|
||||||
|
onSuccess()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取人脸增量数据(FaceApi 集成) */
|
||||||
|
fun getFaceIncrement(
|
||||||
|
pageNum: Long = 1L,
|
||||||
|
pageSize: Long = PAGE_SIZE,
|
||||||
|
timestamp: Long,
|
||||||
|
onAllQueryFinished: () -> Unit,
|
||||||
|
onPageQueryFinished: (List<FaceVO>) -> Unit,
|
||||||
|
onFailure: (String) -> Unit = {}
|
||||||
|
) {
|
||||||
|
Timber.tag(TAG).d("getFaceIncrement timestamp=$timestamp")
|
||||||
|
launch {
|
||||||
|
val response = ApiClient.repositoryV2.getFaceIncrement(pageNum, pageSize, timestamp)
|
||||||
|
if (!parseResponse(response)) {
|
||||||
|
onFailure(response.msg ?: "获取增量人脸数据失败")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Default) {
|
||||||
|
val list = response.data ?: emptyList()
|
||||||
|
if (pageNum == 1L && list.isEmpty()) {
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
onPageQueryFinished(list)
|
||||||
|
// 取列表中最大时间戳(后端返回 String 类型,需转为 Long),而非依赖列表最后一条
|
||||||
|
lastFaceTimestamp = list.maxOf { it.faceUpdateTimestamp?.toLongOrNull() ?: 0L }
|
||||||
|
SpTool.lastFaceTimestamp = lastFaceTimestamp
|
||||||
|
FileUtil.saveLog("获取人脸[增量]数据结束,时间戳为:$lastFaceTimestamp")
|
||||||
|
onAllQueryFinished()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getFoodByNames(
|
||||||
|
names: String,
|
||||||
|
onSuccess: (list: List<NewFoodInfo>) -> Unit,
|
||||||
|
onFailure: (message: String) -> Unit
|
||||||
|
) {
|
||||||
|
launch {
|
||||||
|
try {
|
||||||
|
val response = ApiClient.repositoryV2.getFoodByNames(names.split(","))
|
||||||
|
if (parseResponse(response)) {
|
||||||
|
onSuccess(response.data ?: emptyList())
|
||||||
|
} else {
|
||||||
|
onFailure(response.msg ?: "查询菜品失败")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "查询菜品异常")
|
||||||
|
onFailure("网络异常: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun placeOrder(
|
||||||
|
orderRequest: PlaceOrderRequest,
|
||||||
|
onSuccess: (orderNo: String?) -> Unit,
|
||||||
|
onFailure: (message: String) -> Unit
|
||||||
|
) {
|
||||||
|
launchWithLoading {
|
||||||
|
try {
|
||||||
|
val response = ApiClient.repositoryV2.placeOrder(orderRequest)
|
||||||
|
if (parseResponse(response)) {
|
||||||
|
onSuccess(response.data)
|
||||||
|
} else {
|
||||||
|
onFailure(response.msg ?: "下单失败")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "开餐下单异常")
|
||||||
|
onFailure("网络异常: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bindUserOrder(
|
||||||
|
userId: Long,
|
||||||
|
orderNo: String,
|
||||||
|
mode: Int? = null,
|
||||||
|
onSuccess: () -> Unit,
|
||||||
|
onFailure: (message: String) -> Unit
|
||||||
|
) {
|
||||||
|
launch {
|
||||||
|
try {
|
||||||
|
val response = ApiClient.repositoryV2.bindUserOrder(userId, orderNo, mode)
|
||||||
|
if (parseResponse(response)) {
|
||||||
|
onSuccess()
|
||||||
|
} else {
|
||||||
|
onFailure(response.msg ?: "绑定订单失败")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "绑定订单异常")
|
||||||
|
onFailure("网络异常: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCollectPage(
|
||||||
|
pageNum: Long = 1L,
|
||||||
|
pageSize: Long = PAGE_SIZE,
|
||||||
|
foodName: String? = null,
|
||||||
|
onSuccess: (list: List<CollectedFoodV2>) -> Unit,
|
||||||
|
onFailure: (message: String) -> Unit
|
||||||
|
) {
|
||||||
|
launch {
|
||||||
|
try {
|
||||||
|
val response = ApiClient.repositoryV2.getCollectPage(pageNum, pageSize, foodName)
|
||||||
|
if (parseResponse(response)) {
|
||||||
|
onSuccess(response.data ?: emptyList())
|
||||||
|
} else {
|
||||||
|
onFailure(response.msg ?: "获取采集列表失败")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "获取采集列表异常")
|
||||||
|
onFailure("网络异常: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCollectVectorPage(
|
||||||
|
pageNum: Long = 1L,
|
||||||
|
pageSize: Long = PAGE_SIZE,
|
||||||
|
onSuccess: (list: List<CollectedFoodV2>) -> Unit,
|
||||||
|
onFailure: (message: String) -> Unit
|
||||||
|
) {
|
||||||
|
launch {
|
||||||
|
try {
|
||||||
|
val response = ApiClient.repositoryV2.getCollectVectorPage(pageNum, pageSize)
|
||||||
|
if (parseResponse(response)) {
|
||||||
|
onSuccess(response.data ?: emptyList())
|
||||||
|
} else {
|
||||||
|
onFailure(response.msg ?: "获取采集列表失败")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "获取采集列表异常")
|
||||||
|
onFailure("网络异常: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 上传采集图片(suspend,直接返回 picUrls 列表) */
|
||||||
|
suspend fun uploadCollect(
|
||||||
|
foodId: Long,
|
||||||
|
foodName: String,
|
||||||
|
version: String,
|
||||||
|
foodVector: String,
|
||||||
|
fileList: List<File>
|
||||||
|
): List<String>? {
|
||||||
|
Timber.tag(TAG).d("uploadCollect foodId=$foodId, foodName=$foodName")
|
||||||
|
val fileListNotNull = fileList.filter { it.exists() }
|
||||||
|
if (fileListNotNull.isEmpty()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val response = ApiClient.repositoryV2.uploadCollect(
|
||||||
|
foodId, foodName, version, foodVector, fileListNotNull
|
||||||
|
)
|
||||||
|
if (!parseResponse(response)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除采集菜品 */
|
||||||
|
fun deleteCollect(
|
||||||
|
foodId: String?,
|
||||||
|
version: String?,
|
||||||
|
block: (Boolean) -> Unit
|
||||||
|
) {
|
||||||
|
Timber.tag(TAG).d("deleteCollect foodId=$foodId, version=$version")
|
||||||
|
val id = foodId?.toLongOrNull() ?: run {
|
||||||
|
block(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
launchWithLoading {
|
||||||
|
try {
|
||||||
|
val response = ApiClient.repositoryV2.deleteCollect(id, version ?: "")
|
||||||
|
if (parseResponse(response)) {
|
||||||
|
block(true)
|
||||||
|
} else {
|
||||||
|
block(false)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "删除采集菜品异常")
|
||||||
|
block(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 以下方法暂用 V1 API 实现(待 V2 接口补充) ==========
|
||||||
|
|
||||||
|
/** 激活虹软人脸识别引擎 */
|
||||||
|
fun activeEngine() {
|
||||||
|
Timber.tag(TAG).d("activeEngine")
|
||||||
|
faceApi.activeEngine(
|
||||||
|
App.getContext(),
|
||||||
|
GlobalData.appId,
|
||||||
|
GlobalData.sdkKey,
|
||||||
|
GlobalData.activeKey,
|
||||||
|
object : FaceApi.ActiveCallback {
|
||||||
|
override fun onSuccess(activeCode: Int) {
|
||||||
|
Timber.tag(TAG).d("activeEngine activeCode = $activeCode")
|
||||||
|
viewModelScope.launch(Dispatchers.Main) {
|
||||||
|
when (activeCode) {
|
||||||
|
ErrorInfo.MOK -> {
|
||||||
|
ToastUtils.showToast("激活引擎成功")
|
||||||
|
}
|
||||||
|
ErrorInfo.MERR_ASF_ALREADY_ACTIVATED -> {
|
||||||
|
// 引擎已激活,无需再次激活
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
ToastUtils.showToast("激活引擎失败($activeCode)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFail(e: Exception?) {
|
||||||
|
viewModelScope.launch(Dispatchers.Main) {
|
||||||
|
ToastUtils.showToast("激活引擎异常,${e?.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取支付二维码 */
|
||||||
|
fun getQrCodeImg(
|
||||||
|
orderId: String,
|
||||||
|
userId: String? = null,
|
||||||
|
totalFee: String? = null,
|
||||||
|
block: (String?) -> Unit
|
||||||
|
) {
|
||||||
|
Timber.tag(TAG).d("getQrCodeImg orderId = $orderId, userId = $userId")
|
||||||
|
launch {
|
||||||
|
val response = repository.getQrCodeImg(
|
||||||
|
orderNo = orderId, memberId = userId, totalFee = totalFee
|
||||||
|
)
|
||||||
|
if (parseResponse(response)) {
|
||||||
|
block(response.data)
|
||||||
|
} else {
|
||||||
|
block(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 扫码支付 */
|
||||||
|
fun qrCodePay(
|
||||||
|
authCode: String,
|
||||||
|
orderNo: String,
|
||||||
|
memberId: String?,
|
||||||
|
block: (Boolean, String?) -> Unit
|
||||||
|
) {
|
||||||
|
launch {
|
||||||
|
val response = repository.qrCodePay(authCode, orderNo, memberId)
|
||||||
|
if (parseResponse(response)) {
|
||||||
|
block(true, response.data)
|
||||||
|
} else {
|
||||||
|
block(false, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 现金支付 */
|
||||||
|
fun cashPay(param: HashMap<String, String>, block: (Boolean) -> Unit) {
|
||||||
|
Timber.tag(TAG).d("cashPay")
|
||||||
|
launchWithLoading {
|
||||||
|
val response = repository.cashPay(param)
|
||||||
|
if (parseResponse(response)) {
|
||||||
|
block(response.data ?: false)
|
||||||
|
} else {
|
||||||
|
block(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 会员支付 */
|
||||||
|
fun memberPay(param: HashMap<String, String?>, block: (Boolean) -> Unit) {
|
||||||
|
Timber.tag(TAG).d("memberPay")
|
||||||
|
launchWithLoading {
|
||||||
|
val response = repository.memberPay(param)
|
||||||
|
if (parseResponse(response)) {
|
||||||
|
block(true)
|
||||||
|
} else {
|
||||||
|
block(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查询订单支付状态 */
|
||||||
|
suspend fun queryOrderState(orderId: String, block: (Boolean) -> Unit) {
|
||||||
|
Timber.tag(TAG).d("queryOrderState")
|
||||||
|
val response = repository.queryOrderState(orderNo = orderId)
|
||||||
|
if (parseResponse(response)) {
|
||||||
|
block(response.data == "1")
|
||||||
|
} else {
|
||||||
|
block(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取饭点类型 */
|
||||||
|
fun getDinnerType(
|
||||||
|
onSuccess: (DinnerType?) -> Unit = {},
|
||||||
|
onFailure: (message: String) -> Unit = {}
|
||||||
|
) {
|
||||||
|
_dinnerTypeInfo.value = null
|
||||||
|
launch {
|
||||||
|
try {
|
||||||
|
val response = repository.getDinnerType()
|
||||||
|
if (parseResponse(response)) {
|
||||||
|
_dinnerTypeInfo.value = response.data
|
||||||
|
onSuccess(response.data)
|
||||||
|
} else {
|
||||||
|
onFailure(response.msg ?: "获取饭点类型失败")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "获取饭点类型异常")
|
||||||
|
onFailure("网络异常: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== V1 兼容包装方法(暂用 V1 API,后续逐步迁移至 V2) ==========
|
||||||
|
|
||||||
|
private var lastFaceTimestamp = 0L
|
||||||
|
|
||||||
|
/** 搜索食物 */
|
||||||
|
fun searchByFoodName(foodName: String, action: (List<FoodInfo>) -> Unit = {}) {
|
||||||
|
Timber.tag(TAG).d("searchFood foodName = $foodName")
|
||||||
|
launchWithLoading {
|
||||||
|
try {
|
||||||
|
val response = ApiClient.repositoryV2.searchFood(name = foodName)
|
||||||
|
if (parseResponse(response)) {
|
||||||
|
val list = (response.data ?: emptyList()).map { it.toFoodInfo() }
|
||||||
|
action(list)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "搜索菜品异常")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取用户就餐营养数据 */
|
||||||
|
fun getUserNutritionData(
|
||||||
|
userId: String,
|
||||||
|
block: (UserNutrition?) -> Unit
|
||||||
|
) {
|
||||||
|
Timber.tag(TAG).d("getUserNutritionData userId = $userId")
|
||||||
|
val uid = userId.toLongOrNull() ?: run {
|
||||||
|
block(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
try {
|
||||||
|
val response = ApiClient.repositoryV2.getUserCurrentFood(uid)
|
||||||
|
if (parseResponse(response)) {
|
||||||
|
block(response.data)
|
||||||
|
} else {
|
||||||
|
block(null)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "获取就餐营养数据异常")
|
||||||
|
block(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取订单列表 */
|
||||||
|
fun getFoodOrderList(userId: String, block: (FoodOrderModel?) -> Unit) {
|
||||||
|
Timber.tag(TAG).d("getFoodOrderList userId = $userId")
|
||||||
|
val uid = userId.toLongOrNull() ?: run {
|
||||||
|
block(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
launchWithLoading {
|
||||||
|
try {
|
||||||
|
val response = ApiClient.repositoryV2.getSettlementOrders(uid)
|
||||||
|
if (parseResponse(response)) {
|
||||||
|
block(response.data?.toFoodOrderModel())
|
||||||
|
} else {
|
||||||
|
block(null)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "查询订单列表异常")
|
||||||
|
block(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 根据 ID 查询会员信息 */
|
||||||
|
fun getMemberInfoById(memberId: String, block: (MemberInfo?) -> Unit) {
|
||||||
|
Timber.tag(TAG).d("getMemberInfo memberId=$memberId")
|
||||||
|
val uid = memberId.toLongOrNull() ?: run {
|
||||||
|
block(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
launchWithLoading {
|
||||||
|
try {
|
||||||
|
val response = ApiClient.repositoryV2.getMemberInfo(uid)
|
||||||
|
if (parseResponse(response)) {
|
||||||
|
block(response.data?.toMemberInfo())
|
||||||
|
} else {
|
||||||
|
block(null)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "查询会员信息异常")
|
||||||
|
block(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 根据手机号查询会员信息 */
|
||||||
|
fun getMemberInfoByPhone(phone: String, key: String, block: (MemberInfo?) -> Unit) {
|
||||||
|
Timber.tag(TAG).d("getMemberInfoByPhone phone=$phone")
|
||||||
|
launchWithLoading {
|
||||||
|
try {
|
||||||
|
val response = ApiClient.repositoryV2.getMemberInfoByPhone(phone, key)
|
||||||
|
if (parseResponse(response)) {
|
||||||
|
block(response.data?.toMemberInfo())
|
||||||
|
} else {
|
||||||
|
block(null)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "查询会员异常")
|
||||||
|
block(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取会员折扣 */
|
||||||
|
fun getMemberDiscount(userId: String, block: (Double?) -> Unit) {
|
||||||
|
Timber.tag(TAG).d("getMemberDiscount userId = $userId")
|
||||||
|
val uid = userId.toLongOrNull() ?: run {
|
||||||
|
block(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
launchWithLoading {
|
||||||
|
try {
|
||||||
|
val response = ApiClient.repositoryV2.getMemberDiscount(uid)
|
||||||
|
if (parseResponse(response)) {
|
||||||
|
block(response.data?.toDoubleOrNull())
|
||||||
|
} else {
|
||||||
|
block(null)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "查询会员折扣异常")
|
||||||
|
block(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
android:layout_height="72dp"
|
android:layout_height="72dp"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:paddingStart="16dp"
|
android:paddingStart="16dp"
|
||||||
android:text="测试环境"
|
android:text="本地环境"
|
||||||
android:textColor="#ff141428"
|
android:textColor="#ff141428"
|
||||||
android:textSize="28sp" />
|
android:textSize="28sp" />
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
android:layout_height="72dp"
|
android:layout_height="72dp"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:paddingStart="16dp"
|
android:paddingStart="16dp"
|
||||||
android:text="UAT 环境"
|
android:text="测试环境"
|
||||||
android:textColor="#ff141428"
|
android:textColor="#ff141428"
|
||||||
android:textSize="28sp" />
|
android:textSize="28sp" />
|
||||||
|
|
||||||
|
|||||||
@@ -214,6 +214,7 @@
|
|||||||
android:background="@drawable/setting_border_gray">
|
android:background="@drawable/setting_border_gray">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
android:id="@+id/tvDishWeightLabel"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="120dp"
|
android:layout_height="120dp"
|
||||||
android:layout_marginStart="42dp"
|
android:layout_marginStart="42dp"
|
||||||
|
|||||||
@@ -68,5 +68,5 @@ dependencies {
|
|||||||
|
|
||||||
val objectboxVersion = "5.0.1"
|
val objectboxVersion = "5.0.1"
|
||||||
debugImplementation("io.objectbox:objectbox-android-objectbrowser:$objectboxVersion")
|
debugImplementation("io.objectbox:objectbox-android-objectbrowser:$objectboxVersion")
|
||||||
// releaseImplementation("io.objectbox:objectbox-android:$objectboxVersion")
|
releaseImplementation("io.objectbox:objectbox-android:$objectboxVersion")
|
||||||
}
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -88,14 +88,8 @@ public class ConfigUtil {
|
|||||||
* 默认相机分辨率
|
* 默认相机分辨率
|
||||||
*/
|
*/
|
||||||
// private static final String DEFAULT_PREVIEW_SIZE = "1280x720";
|
// private static final String DEFAULT_PREVIEW_SIZE = "1280x720";
|
||||||
// private static final String DEFAULT_PREVIEW_SIZE = "1080x720";
|
|
||||||
// private static final String DEFAULT_PREVIEW_SIZE = "720x1080";
|
|
||||||
private static final String DEFAULT_PREVIEW_SIZE = "720x1280";
|
private static final String DEFAULT_PREVIEW_SIZE = "720x1280";
|
||||||
// private static final String DEFAULT_PREVIEW_SIZE = "608x456";
|
// private static final String DEFAULT_PREVIEW_SIZE = "400x640";
|
||||||
// private static final String DEFAULT_PREVIEW_SIZE = "1024x768";
|
|
||||||
// private static final String DEFAULT_PREVIEW_SIZE = "800x600";
|
|
||||||
// private static final String DEFAULT_PREVIEW_SIZE = "640x480";
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取String类型的preference
|
* 获取String类型的preference
|
||||||
@@ -244,7 +238,7 @@ public class ConfigUtil {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO: 该Demo基于单人脸识别实现,若想使用多人脸识别,请将 return true 改成 return getBoolean,并修改相关配置项的preference.xml和业务代码
|
* TODO: 该Demo基于单人脸识别实现,若想使用多人脸识别,请将 return true 改成 return getBoolean,并修改相关配置项的preference.xml和业务代码
|
||||||
* <p>
|
*
|
||||||
* 获取识别界面是否保留最大人脸
|
* 获取识别界面是否保留最大人脸
|
||||||
*
|
*
|
||||||
* @param context 上下文
|
* @param context 上下文
|
||||||
@@ -349,7 +343,7 @@ public class ConfigUtil {
|
|||||||
return Float.parseFloat(getString(context, R.string.preference_ir_liveness_threshold, String.valueOf(RECOMMEND_IR_LIVENESS_THRESHOLD)));
|
return Float.parseFloat(getString(context, R.string.preference_ir_liveness_threshold, String.valueOf(RECOMMEND_IR_LIVENESS_THRESHOLD)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static float getLivenessFqThreshold(Context context) {
|
public static float getLivenessFqThreshold(Context context){
|
||||||
return Float.parseFloat(getString(context, R.string.preference_liveness_fq_threshold, String.valueOf(RECOMMEND_LIVENESS_FQ_THRESHOLD)));
|
return Float.parseFloat(getString(context, R.string.preference_liveness_fq_threshold, String.valueOf(RECOMMEND_LIVENESS_FQ_THRESHOLD)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ public class ErrorCodeUtil {
|
|||||||
}
|
}
|
||||||
return "unknown error";
|
return "unknown error";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将ArcSoftImageUtil错误码转换为对应的错误码常量名,便于理解
|
* 将ArcSoftImageUtil错误码转换为对应的错误码常量名,便于理解
|
||||||
* TODO:目前每次都遍历,如果使用频繁,建议将Field缓存处理,避免每次都反射
|
* TODO:目前每次都遍历,如果使用频繁,建议将Field缓存处理,避免每次都反射
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.sw.plate.utils.arcface;
|
package com.sw.plate.utils.arcface;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import com.arcsoft.face.FaceEngine;
|
import com.arcsoft.face.FaceEngine;
|
||||||
@@ -31,25 +32,21 @@ public class FaceApi {
|
|||||||
*/
|
*/
|
||||||
public void updateFaceData(int index, List<FaceEntity> list) {
|
public void updateFaceData(int index, List<FaceEntity> list) {
|
||||||
Log.d(TAG, "updateFaceData: index = " + index + ", listSize = " + list.size());
|
Log.d(TAG, "updateFaceData: index = " + index + ", listSize = " + list.size());
|
||||||
FaceDao faceDao = FaceDatabase.getInstance(App.getContext()).faceDao();
|
FaceDao faceDao = getFaceDao();
|
||||||
|
// 档口机业务:首页(index==1)全量同步前清空旧特征库,避免特征重复堆积
|
||||||
if (index == 1) {
|
if (index == 1) {
|
||||||
faceDao.deleteAll();
|
faceDao.deleteAll();
|
||||||
faceDao.resetId();
|
faceDao.resetId();
|
||||||
}
|
}
|
||||||
List<Long> insertIdList = faceDao.insert(list);
|
faceDao.insert(list);
|
||||||
Log.d(TAG, "updateFaceData: count = " + insertIdList.size());
|
|
||||||
List<FaceEntity> queryList = faceDao.getAllFaces();
|
List<FaceEntity> queryList = faceDao.getAllFaces();
|
||||||
Log.d(TAG, "updateFaceData: queryList.size = " + queryList.size());
|
Log.d(TAG, "updateFaceData: queryList.size = " + queryList.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateFaceData2(int index, List<FaceEntity> list) {
|
public void clearFaceData() {
|
||||||
Log.d(TAG, "updateFaceData: index = " + index + ", listSize = " + list.size());
|
FaceDao faceDao = getFaceDao();
|
||||||
FaceDao faceDao = FaceDatabase.getInstance(App.getContext()).faceDao();
|
faceDao.deleteAll();
|
||||||
if (index == 0) {
|
faceDao.resetId();
|
||||||
faceDao.deleteAll();
|
|
||||||
faceDao.resetId();
|
|
||||||
}
|
|
||||||
faceDao.insert(list);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -92,4 +89,35 @@ public class FaceApi {
|
|||||||
// List<FacePreviewInfo> facePreviewInfoList = recognizeViewModel.onPreviewFrame(nv21, true);
|
// List<FacePreviewInfo> facePreviewInfoList = recognizeViewModel.onPreviewFrame(nv21, true);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void deleteByUserName(String userName) {
|
||||||
|
getFaceDao().deleteFaceById(userName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long insert(FaceEntity entity) {
|
||||||
|
if (entity == null) {
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
return getFaceDao().insert(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FaceEntity queryByUserName(String userName) {
|
||||||
|
if (TextUtils.isEmpty(userName)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return getFaceDao().queryByUserName(userName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int queryFaceCount() {
|
||||||
|
return getFaceDao().getFaceCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<FaceEntity> queryAllByUserName(String userName) {
|
||||||
|
return getFaceDao().queryAllByUserName(userName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FaceDao getFaceDao() {
|
||||||
|
return FaceDatabase.getInstance(App.getContext()).faceDao();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ public class FaceRectTransformer {
|
|||||||
rect.bottom *= verticalRatio;
|
rect.bottom *= verticalRatio;
|
||||||
|
|
||||||
Rect newRect = new Rect();
|
Rect newRect = new Rect();
|
||||||
// L.e("cameraDisplayOrientation " + cameraDisplayOrientation + " === " + cameraId);
|
L.e("cameraDisplayOrientation " + cameraDisplayOrientation + " === " + cameraId);
|
||||||
switch (cameraDisplayOrientation) {
|
switch (cameraDisplayOrientation) {
|
||||||
case 0:
|
case 0:
|
||||||
if (cameraId == Camera.CameraInfo.CAMERA_FACING_FRONT) {
|
if (cameraId == Camera.CameraInfo.CAMERA_FACING_FRONT) {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.sw.plate.utils.arcface;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
import android.graphics.Color;
|
|
||||||
import android.graphics.Paint;
|
import android.graphics.Paint;
|
||||||
import android.graphics.Path;
|
import android.graphics.Path;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
@@ -47,6 +46,10 @@ public class FaceRectView extends View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getRectColor() {
|
||||||
|
return paint.getColor();
|
||||||
|
}
|
||||||
|
|
||||||
public void clearFaceInfo() {
|
public void clearFaceInfo() {
|
||||||
drawInfoList.clear();
|
drawInfoList.clear();
|
||||||
postInvalidate();
|
postInvalidate();
|
||||||
@@ -216,7 +219,6 @@ public class FaceRectView extends View {
|
|||||||
paint.setStyle(Paint.Style.STROKE);
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
paint.setStrokeWidth(faceRectThickness);
|
paint.setStrokeWidth(faceRectThickness);
|
||||||
paint.setColor(drawInfo.getColor());
|
paint.setColor(drawInfo.getColor());
|
||||||
// paint.setColor(Color.parseColor("#FF0000"));
|
|
||||||
paint.setAntiAlias(true);
|
paint.setAntiAlias(true);
|
||||||
|
|
||||||
Path mPath = new Path();
|
Path mPath = new Path();
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ public class FileUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static boolean saveDataToFile(byte[] data, File file, boolean append) {
|
public static boolean saveDataToFile(byte[] data, File file, boolean append) {
|
||||||
if (data == null) {
|
if (data == null){
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
File parentFile = file.getParentFile();
|
File parentFile = file.getParentFile();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.hardware.Camera;
|
|||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.IntDef;
|
import androidx.annotation.IntDef;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
@@ -18,6 +19,7 @@ import com.arcsoft.face.ImageQualitySimilar;
|
|||||||
import com.arcsoft.face.LivenessInfo;
|
import com.arcsoft.face.LivenessInfo;
|
||||||
import com.arcsoft.face.MaskInfo;
|
import com.arcsoft.face.MaskInfo;
|
||||||
import com.arcsoft.face.enums.ExtractType;
|
import com.arcsoft.face.enums.ExtractType;
|
||||||
|
import com.sw.plate.App;
|
||||||
import com.sw.plate.utils.L;
|
import com.sw.plate.utils.L;
|
||||||
import com.sw.plate.utils.arcface.FaceRectTransformer;
|
import com.sw.plate.utils.arcface.FaceRectTransformer;
|
||||||
import com.sw.plate.utils.arcface.face.constants.LivenessType;
|
import com.sw.plate.utils.arcface.face.constants.LivenessType;
|
||||||
@@ -37,7 +39,6 @@ import java.lang.annotation.Retention;
|
|||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Enumeration;
|
import java.util.Enumeration;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -102,6 +103,10 @@ public class FaceHelper implements FaceListener {
|
|||||||
* 活体检测引擎
|
* 活体检测引擎
|
||||||
*/
|
*/
|
||||||
private FaceEngine flEngine;
|
private FaceEngine flEngine;
|
||||||
|
/**
|
||||||
|
* 口罩检测引擎(5.0 独立引擎)
|
||||||
|
*/
|
||||||
|
private FaceEngine maskEngine;
|
||||||
|
|
||||||
private Camera.Size previewSize;
|
private Camera.Size previewSize;
|
||||||
|
|
||||||
@@ -146,11 +151,6 @@ public class FaceHelper implements FaceListener {
|
|||||||
*/
|
*/
|
||||||
private boolean onlyDetectLiveness;
|
private boolean onlyDetectLiveness;
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否需要更新faceData
|
|
||||||
*/
|
|
||||||
private boolean needUpdateFaceData;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 识别的配置项
|
* 识别的配置项
|
||||||
*/
|
*/
|
||||||
@@ -180,9 +180,9 @@ public class FaceHelper implements FaceListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private FaceHelper(Builder builder) {
|
private FaceHelper(Builder builder) {
|
||||||
needUpdateFaceData = builder.needUpdateFaceData;
|
|
||||||
onlyDetectLiveness = builder.onlyDetectLiveness;
|
onlyDetectLiveness = builder.onlyDetectLiveness;
|
||||||
ftEngine = builder.ftEngine;
|
ftEngine = builder.ftEngine;
|
||||||
|
maskEngine = builder.maskEngine;
|
||||||
trackedFaceCount = builder.trackedFaceCount;
|
trackedFaceCount = builder.trackedFaceCount;
|
||||||
previewSize = builder.previewSize;
|
previewSize = builder.previewSize;
|
||||||
frEngine = builder.frEngine;
|
frEngine = builder.frEngine;
|
||||||
@@ -350,11 +350,11 @@ public class FaceHelper implements FaceListener {
|
|||||||
return facePreviewInfoList;
|
return facePreviewInfoList;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!onlyDetectLiveness) {
|
if (!onlyDetectLiveness && maskEngine != null) {
|
||||||
code = ftEngine.process(rgbNv21, previewSize.width, previewSize.height, FaceEngine.CP_PAF_NV21, faceInfoList,
|
code = maskEngine.process(rgbNv21, previewSize.width, previewSize.height, FaceEngine.CP_PAF_NV21, faceInfoList,
|
||||||
FaceEngine.ASF_MASK_DETECT);
|
FaceEngine.ASF_MASK_DETECT);
|
||||||
if (code == ErrorInfo.MOK) {
|
if (code == ErrorInfo.MOK) {
|
||||||
code = ftEngine.getMask(maskInfoList);
|
code = maskEngine.getMask(maskInfoList);
|
||||||
if (code != ErrorInfo.MOK) {
|
if (code != ErrorInfo.MOK) {
|
||||||
onFail(new Exception("process getMask failed,code is " + code));
|
onFail(new Exception("process getMask failed,code is " + code));
|
||||||
return facePreviewInfoList;
|
return facePreviewInfoList;
|
||||||
@@ -430,7 +430,7 @@ public class FaceHelper implements FaceListener {
|
|||||||
if (!contained) {
|
if (!contained) {
|
||||||
RecognizeInfo recognizeInfo = recognizeInfoMap.remove(key);
|
RecognizeInfo recognizeInfo = recognizeInfoMap.remove(key);
|
||||||
if (recognizeInfo != null) {
|
if (recognizeInfo != null) {
|
||||||
recognizeCallback.onNoticeChanged("leave");
|
recognizeCallback.onNoticeChanged("");
|
||||||
// 人脸离开时,通知特征提取线程,避免一直等待活体结果
|
// 人脸离开时,通知特征提取线程,避免一直等待活体结果
|
||||||
synchronized (recognizeInfo.getWaitLock()) {
|
synchronized (recognizeInfo.getWaitLock()) {
|
||||||
recognizeInfo.getWaitLock().notifyAll();
|
recognizeInfo.getWaitLock().notifyAll();
|
||||||
@@ -660,21 +660,52 @@ public class FaceHelper implements FaceListener {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int failCount = 0;
|
||||||
|
private long startTime = 0;
|
||||||
private void searchFace(final FaceFeature faceFeature, final Integer trackId) {
|
private void searchFace(final FaceFeature faceFeature, final Integer trackId) {
|
||||||
CompareResult compareResult = FaceServer.getInstance().searchFaceFeature(faceFeature, frEngine);
|
CompareResult compareResult = FaceServer.getInstance().searchFaceFeature(faceFeature, frEngine);
|
||||||
if (compareResult == null || compareResult.getFaceEntity() == null) {
|
if (compareResult == null || compareResult.getFaceEntity() == null) {
|
||||||
|
if (startTime == 0) {
|
||||||
|
failCount = 0;
|
||||||
|
startTime = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
if (System.currentTimeMillis() - startTime > 10*1000) {
|
||||||
|
failCount = 0;
|
||||||
|
startTime = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
failCount++;
|
||||||
|
float similar;
|
||||||
|
if (compareResult != null) {
|
||||||
|
similar = compareResult.getSimilar();
|
||||||
|
} else {
|
||||||
|
similar = 0f;
|
||||||
|
}
|
||||||
|
// new Handler(Looper.getMainLooper()).post(()-> {
|
||||||
|
// try {
|
||||||
|
// Toast.makeText(App.getContext(),"识别失败"+failCount+"次,similar="+similar, Toast.LENGTH_SHORT).show();
|
||||||
|
// } catch (Exception e) {
|
||||||
|
// e.printStackTrace();
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
Log.d(TAG, "collectFace,searchFace查询失败"+failCount+"次,similar="+similar);
|
||||||
|
if (failCount >= 2) {
|
||||||
|
recognizeCallback.onRecognized(null, LivenessInfo.UNKNOWN, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
retryRecognizeDelayed(trackId);
|
retryRecognizeDelayed(trackId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
compareResult.setTrackId(trackId);
|
compareResult.setTrackId(trackId);
|
||||||
boolean pass = compareResult.getSimilar() > recognizeConfiguration.getSimilarThreshold();
|
boolean pass = compareResult.getSimilar() > recognizeConfiguration.getSimilarThreshold();
|
||||||
|
compareResult.setSimilarPass(pass);
|
||||||
|
Log.d(TAG, "collectFace,searchFace: pass="+pass+",similar="+compareResult.getSimilar()+",threshold="+recognizeConfiguration.getSimilarThreshold());
|
||||||
recognizeCallback.onRecognized(compareResult, getRecognizeInfo(recognizeInfoMap, trackId).getLiveness(), pass);
|
recognizeCallback.onRecognized(compareResult, getRecognizeInfo(recognizeInfoMap, trackId).getLiveness(), pass);
|
||||||
if (pass) {
|
if (pass) {
|
||||||
setName(trackId, "识别通过");
|
setName(trackId, "识别通过");
|
||||||
noticeCurrentStatus("识别通过");
|
noticeCurrentStatus("识别通过");
|
||||||
changeRecognizeStatus(trackId, RequestFeatureStatus.SUCCEED);
|
changeRecognizeStatus(trackId, RequestFeatureStatus.SUCCEED);
|
||||||
} else {
|
} else {
|
||||||
noticeCurrentStatus("未通过:NOT_REGISTERED");
|
noticeCurrentStatus("未通过:NOT_REGISTERED,"+compareResult.getSimilar());
|
||||||
retryRecognizeDelayed(trackId);
|
retryRecognizeDelayed(trackId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -847,18 +878,7 @@ public class FaceHelper implements FaceListener {
|
|||||||
int fdCode = flEngine.detectFaces(nv21Data, width, height, format, faceInfoList);
|
int fdCode = flEngine.detectFaces(nv21Data, width, height, format, faceInfoList);
|
||||||
boolean isFaceExists = isFaceExists(faceInfoList, faceInfo);
|
boolean isFaceExists = isFaceExists(faceInfoList, faceInfo);
|
||||||
if (fdCode == ErrorInfo.MOK && isFaceExists) {
|
if (fdCode == ErrorInfo.MOK && isFaceExists) {
|
||||||
if (needUpdateFaceData) {
|
flCode = flEngine.processIr(nv21Data, width, height, format, Arrays.asList(faceInfo), FaceEngine.ASF_IR_LIVENESS);
|
||||||
/*
|
|
||||||
* 若IR人脸框有偏移,则需要对IR的人脸数据进行updateFaceData处理,再将处理后的FaceInfo信息传输给活体检测接口
|
|
||||||
*/
|
|
||||||
flCode = flEngine.updateFaceData(nv21Data, previewSize.width, previewSize.height, FaceEngine.CP_PAF_NV21,
|
|
||||||
new ArrayList<>(Collections.singletonList(faceInfo)));
|
|
||||||
if (flCode == ErrorInfo.MOK) {
|
|
||||||
flCode = flEngine.processIr(nv21Data, width, height, format, Arrays.asList(faceInfo), FaceEngine.ASF_IR_LIVENESS);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
flCode = flEngine.processIr(nv21Data, width, height, format, Arrays.asList(faceInfo), FaceEngine.ASF_IR_LIVENESS);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
onFail(new Exception("ir detectFaces failed fdCode:" + fdCode + ",isFaceExists:" + isFaceExists));
|
onFail(new Exception("ir detectFaces failed fdCode:" + fdCode + ",isFaceExists:" + isFaceExists));
|
||||||
}
|
}
|
||||||
@@ -1065,9 +1085,9 @@ public class FaceHelper implements FaceListener {
|
|||||||
private FaceEngine ftEngine;
|
private FaceEngine ftEngine;
|
||||||
private FaceEngine frEngine;
|
private FaceEngine frEngine;
|
||||||
private FaceEngine flEngine;
|
private FaceEngine flEngine;
|
||||||
|
private FaceEngine maskEngine;
|
||||||
private Camera.Size previewSize;
|
private Camera.Size previewSize;
|
||||||
private boolean onlyDetectLiveness;
|
private boolean onlyDetectLiveness;
|
||||||
private boolean needUpdateFaceData;
|
|
||||||
private RecognizeConfiguration recognizeConfiguration;
|
private RecognizeConfiguration recognizeConfiguration;
|
||||||
private RecognizeCallback recognizeCallback;
|
private RecognizeCallback recognizeCallback;
|
||||||
private IDualCameraFaceInfoTransformer dualCameraFaceInfoTransformer;
|
private IDualCameraFaceInfoTransformer dualCameraFaceInfoTransformer;
|
||||||
@@ -1108,6 +1128,11 @@ public class FaceHelper implements FaceListener {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Builder maskEngine(FaceEngine val) {
|
||||||
|
maskEngine = val;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public Builder previewSize(Camera.Size val) {
|
public Builder previewSize(Camera.Size val) {
|
||||||
previewSize = val;
|
previewSize = val;
|
||||||
return this;
|
return this;
|
||||||
@@ -1133,11 +1158,6 @@ public class FaceHelper implements FaceListener {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Builder needUpdateFaceData(boolean val) {
|
|
||||||
needUpdateFaceData = val;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public FaceHelper build() {
|
public FaceHelper build() {
|
||||||
return new FaceHelper(this);
|
return new FaceHelper(this);
|
||||||
}
|
}
|
||||||
|
|||||||
-1
@@ -2,7 +2,6 @@ package com.sw.plate.utils.arcface.face.constants;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 人脸识别中可能出现的状态
|
* 人脸识别中可能出现的状态
|
||||||
*
|
|
||||||
* @author
|
* @author
|
||||||
*/
|
*/
|
||||||
public @interface RequestFeatureStatus {
|
public @interface RequestFeatureStatus {
|
||||||
|
|||||||
@@ -10,6 +10,16 @@ public class CompareResult {
|
|||||||
private int compareCode;
|
private int compareCode;
|
||||||
private long cost;
|
private long cost;
|
||||||
|
|
||||||
|
private boolean similarPass;
|
||||||
|
|
||||||
|
public void setSimilarPass(boolean similarPass) {
|
||||||
|
this.similarPass = similarPass;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSimilarPass() {
|
||||||
|
return similarPass;
|
||||||
|
}
|
||||||
|
|
||||||
public CompareResult(FaceEntity faceEntity, float similar) {
|
public CompareResult(FaceEntity faceEntity, float similar) {
|
||||||
this.faceEntity = faceEntity;
|
this.faceEntity = faceEntity;
|
||||||
this.similar = similar;
|
this.similar = similar;
|
||||||
|
|||||||
-1
@@ -169,7 +169,6 @@ public class RecognizeConfiguration {
|
|||||||
this.enableImageQuality = val;
|
this.enableImageQuality = val;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Builder enableFaceAreaLimit(boolean val) {
|
public Builder enableFaceAreaLimit(boolean val) {
|
||||||
this.enableFaceAreaLimit = val;
|
this.enableFaceAreaLimit = val;
|
||||||
return this;
|
return this;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import androidx.room.RoomDatabase;
|
|||||||
import com.sw.plate.utils.arcface.facedb.dao.FaceDao;
|
import com.sw.plate.utils.arcface.facedb.dao.FaceDao;
|
||||||
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity;
|
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity;
|
||||||
|
|
||||||
@Database(entities = {FaceEntity.class}, version = 1, exportSchema = false)
|
@Database(entities = {FaceEntity.class}, version = 2, exportSchema = false)
|
||||||
public abstract class FaceDatabase extends RoomDatabase {
|
public abstract class FaceDatabase extends RoomDatabase {
|
||||||
public abstract FaceDao faceDao();
|
public abstract FaceDao faceDao();
|
||||||
|
|
||||||
@@ -22,7 +22,10 @@ public abstract class FaceDatabase extends RoomDatabase {
|
|||||||
faceDatabase = Room.databaseBuilder(context, FaceDatabase.class,
|
faceDatabase = Room.databaseBuilder(context, FaceDatabase.class,
|
||||||
context.getDatabasePath("faceDB.db").getPath()
|
context.getDatabasePath("faceDB.db").getPath()
|
||||||
// context.getExternalFilesDir("database") + File.separator + "faceDB.db"
|
// context.getExternalFilesDir("database") + File.separator + "faceDB.db"
|
||||||
).build();
|
)
|
||||||
|
// 5.0 特征模型不兼容,升级时直接删除旧表重建
|
||||||
|
.fallbackToDestructiveMigration()
|
||||||
|
.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ public interface FaceDao {
|
|||||||
Long insert(FaceEntity faceEntity);
|
Long insert(FaceEntity faceEntity);
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
List<Long> insert(List<FaceEntity> items);
|
void insert(List<FaceEntity> items);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取已注册的人脸数
|
* 获取已注册的人脸数
|
||||||
@@ -91,4 +91,26 @@ public interface FaceDao {
|
|||||||
|
|
||||||
@Query("SELECT * FROM face WHERE user_name = :userName limit 1")
|
@Query("SELECT * FROM face WHERE user_name = :userName limit 1")
|
||||||
FaceEntity queryByUserName(String userName);
|
FaceEntity queryByUserName(String userName);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询指定用户的所有人脸记录(同一用户可能存有多条特征数据)
|
||||||
|
*
|
||||||
|
* @param userName 用户ID
|
||||||
|
* @return 该用户所有人脸记录列表
|
||||||
|
*/
|
||||||
|
@Query("SELECT * FROM face WHERE user_name = :userName")
|
||||||
|
List<FaceEntity> queryAllByUserName(String userName);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return 删除临时用户人脸
|
||||||
|
*/
|
||||||
|
@Query("DELETE from face WHERE user_type = :userType")
|
||||||
|
int deleteUserFaceData(int userType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询用户人脸数
|
||||||
|
* @param userType 1-会员,2-临时用户
|
||||||
|
*/
|
||||||
|
@Query("SELECT COUNT(1) FROM face WHERE user_type = :userType")
|
||||||
|
int getFaceCountByUserType(int userType);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,12 +52,39 @@ public class FaceEntity implements Parcelable {
|
|||||||
/**
|
/**
|
||||||
* 会员编号
|
* 会员编号
|
||||||
*/
|
*/
|
||||||
@ColumnInfo(name = "cardNo")
|
@ColumnInfo(name = "card_no")
|
||||||
private String cardNo;
|
private String cardNo;
|
||||||
|
|
||||||
@Ignore
|
@Ignore
|
||||||
private int trackId;//人脸追踪ID
|
private int trackId;//人脸追踪ID
|
||||||
|
|
||||||
|
@ColumnInfo(name = "user_idd")
|
||||||
|
private String userId;
|
||||||
|
@ColumnInfo(name = "user_face_id")
|
||||||
|
private String userFaceId;
|
||||||
|
@ColumnInfo(name = "member")
|
||||||
|
private boolean member;
|
||||||
|
@ColumnInfo(name = "face_update_timestamp")
|
||||||
|
private long faceUpdateTimestamp;
|
||||||
|
|
||||||
|
public FaceEntity() {
|
||||||
|
registerTime = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
public FaceEntity(String userName, byte[] featureData, String userType, String cardNo, String userId, String userFaceId, boolean member, long faceUpdateTimestamp) {
|
||||||
|
this.userName = userName;
|
||||||
|
this.featureData = featureData;
|
||||||
|
this.userType = userType;
|
||||||
|
this.cardNo = cardNo;
|
||||||
|
this.userId = userId;
|
||||||
|
this.userFaceId = userFaceId;
|
||||||
|
this.member = member;
|
||||||
|
this.faceUpdateTimestamp = faceUpdateTimestamp;
|
||||||
|
registerTime = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
public FaceEntity(String userName, String imagePath, byte[] featureData) {
|
public FaceEntity(String userName, String imagePath, byte[] featureData) {
|
||||||
this.userName = userName;
|
this.userName = userName;
|
||||||
this.imagePath = imagePath;
|
this.imagePath = imagePath;
|
||||||
@@ -65,6 +92,7 @@ public class FaceEntity implements Parcelable {
|
|||||||
registerTime = System.currentTimeMillis();
|
registerTime = System.currentTimeMillis();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
public FaceEntity(FaceEntity faceEntity) {
|
public FaceEntity(FaceEntity faceEntity) {
|
||||||
this.faceId = faceEntity.faceId;
|
this.faceId = faceEntity.faceId;
|
||||||
this.userName = faceEntity.userName;
|
this.userName = faceEntity.userName;
|
||||||
@@ -73,9 +101,14 @@ public class FaceEntity implements Parcelable {
|
|||||||
this.registerTime = faceEntity.registerTime;
|
this.registerTime = faceEntity.registerTime;
|
||||||
this.userType = faceEntity.getUserType();
|
this.userType = faceEntity.getUserType();
|
||||||
this.cardNo = faceEntity.getCardNo();
|
this.cardNo = faceEntity.getCardNo();
|
||||||
|
this.userId = faceEntity.getUserId();
|
||||||
|
this.userFaceId = faceEntity.getUserFaceId();
|
||||||
|
this.member = faceEntity.isMember();
|
||||||
|
this.faceUpdateTimestamp = faceEntity.getFaceUpdateTimestamp();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Ignore
|
||||||
protected FaceEntity(Parcel in) {
|
protected FaceEntity(Parcel in) {
|
||||||
faceId = in.readLong();
|
faceId = in.readLong();
|
||||||
registerTime = in.readLong();
|
registerTime = in.readLong();
|
||||||
@@ -84,6 +117,10 @@ public class FaceEntity implements Parcelable {
|
|||||||
featureData = in.createByteArray();
|
featureData = in.createByteArray();
|
||||||
userType = in.readString();
|
userType = in.readString();
|
||||||
cardNo = in.readString();
|
cardNo = in.readString();
|
||||||
|
userId = in.readString();
|
||||||
|
userFaceId = in.readString();
|
||||||
|
member = in.readByte() != 0;
|
||||||
|
faceUpdateTimestamp = in.readLong();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final Creator<FaceEntity> CREATOR = new Creator<FaceEntity>() {
|
public static final Creator<FaceEntity> CREATOR = new Creator<FaceEntity>() {
|
||||||
@@ -162,7 +199,37 @@ public class FaceEntity implements Parcelable {
|
|||||||
this.cardNo = cardNo;
|
this.cardNo = cardNo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getUserId() {
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserId(String userId) {
|
||||||
|
this.userId = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUserFaceId() {
|
||||||
|
return userFaceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserFaceId(String userFaceId) {
|
||||||
|
this.userFaceId = userFaceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isMember() {
|
||||||
|
return member;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMember(boolean member) {
|
||||||
|
this.member = member;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getFaceUpdateTimestamp() {
|
||||||
|
return faceUpdateTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFaceUpdateTimestamp(long faceUpdateTimestamp) {
|
||||||
|
this.faceUpdateTimestamp = faceUpdateTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int describeContents() {
|
public int describeContents() {
|
||||||
@@ -178,6 +245,10 @@ public class FaceEntity implements Parcelable {
|
|||||||
dest.writeByteArray(featureData);
|
dest.writeByteArray(featureData);
|
||||||
dest.writeString(userType);
|
dest.writeString(userType);
|
||||||
dest.writeString(cardNo);
|
dest.writeString(cardNo);
|
||||||
|
dest.writeString(userId);
|
||||||
|
dest.writeString(userFaceId);
|
||||||
|
dest.writeByte((byte) (member ? 1 : 0));
|
||||||
|
dest.writeLong(faceUpdateTimestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -195,12 +266,16 @@ public class FaceEntity implements Parcelable {
|
|||||||
TextUtils.equals(this.imagePath, that.imagePath) &&
|
TextUtils.equals(this.imagePath, that.imagePath) &&
|
||||||
Arrays.equals(featureData, that.featureData) &&
|
Arrays.equals(featureData, that.featureData) &&
|
||||||
TextUtils.equals(this.userType, that.userType) &&
|
TextUtils.equals(this.userType, that.userType) &&
|
||||||
TextUtils.equals(this.cardNo, that.cardNo);
|
TextUtils.equals(this.cardNo, that.cardNo) &&
|
||||||
|
TextUtils.equals(this.userId, that.userId) &&
|
||||||
|
TextUtils.equals(this.userFaceId, that.userFaceId) &&
|
||||||
|
this.member == that.member &&
|
||||||
|
this.faceUpdateTimestamp == that.faceUpdateTimestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
int result = Objects.hash(faceId, registerTime, userName, imagePath, userType, cardNo);
|
int result = Objects.hash(faceId, registerTime, userName, imagePath, userType, cardNo, userId, userFaceId, member, faceUpdateTimestamp);
|
||||||
result = 31 * result + Arrays.hashCode(featureData);
|
result = 31 * result + Arrays.hashCode(featureData);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import java.io.File;
|
|||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -60,6 +61,10 @@ public class FaceServer {
|
|||||||
faceRegisterInfoList = new ArrayList<>();
|
faceRegisterInfoList = new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public FaceEngine getFaceEngine() {
|
||||||
|
return faceEngine;
|
||||||
|
}
|
||||||
|
|
||||||
public static FaceServer getInstance() {
|
public static FaceServer getInstance() {
|
||||||
if (faceServer == null) {
|
if (faceServer == null) {
|
||||||
synchronized (FaceServer.class) {
|
synchronized (FaceServer.class) {
|
||||||
@@ -123,16 +128,16 @@ public class FaceServer {
|
|||||||
*/
|
*/
|
||||||
public void initFaceList(final Context context, FaceEngine faceEngine, final OnInitFinishedCallback onInitFinishedCallback, boolean recognize) {
|
public void initFaceList(final Context context, FaceEngine faceEngine, final OnInitFinishedCallback onInitFinishedCallback, boolean recognize) {
|
||||||
Disposable disposable = Observable.create((ObservableOnSubscribe<Integer>) emitter -> {
|
Disposable disposable = Observable.create((ObservableOnSubscribe<Integer>) emitter -> {
|
||||||
if (recognize) {
|
if (recognize) {
|
||||||
List<FaceEntity> faceEntityList = FaceDatabase.getInstance(context).faceDao().getAllFaces();
|
List<FaceEntity> faceEntityList = FaceDatabase.getInstance(context).faceDao().getAllFaces();
|
||||||
registerFaceFeatureInfoListFromDb(faceEngine, faceEntityList);
|
registerFaceFeatureInfoListFromDb(faceEngine, faceEntityList);
|
||||||
emitter.onNext(faceEntityList.size());
|
emitter.onNext(faceEntityList.size());
|
||||||
} else {
|
} else {
|
||||||
faceRegisterInfoList = FaceDatabase.getInstance(context).faceDao().getAllFaces();
|
faceRegisterInfoList = FaceDatabase.getInstance(context).faceDao().getAllFaces();
|
||||||
emitter.onNext(faceRegisterInfoList == null ? 0 : faceRegisterInfoList.size());
|
emitter.onNext(faceRegisterInfoList == null ? 0 : faceRegisterInfoList.size());
|
||||||
}
|
}
|
||||||
emitter.onComplete();
|
emitter.onComplete();
|
||||||
}).subscribeOn(Schedulers.io())
|
}).subscribeOn(Schedulers.io())
|
||||||
.unsubscribeOn(Schedulers.io())
|
.unsubscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(size -> {
|
.subscribe(size -> {
|
||||||
@@ -164,6 +169,7 @@ public class FaceServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@SuppressLint("CheckResult")
|
@SuppressLint("CheckResult")
|
||||||
public synchronized int clearAllFaces() {
|
public synchronized int clearAllFaces() {
|
||||||
if (faceRegisterInfoList != null) {
|
if (faceRegisterInfoList != null) {
|
||||||
@@ -340,6 +346,7 @@ public class FaceServer {
|
|||||||
*/
|
*/
|
||||||
public void registerFaceFeatureInfoFromDb(FaceEntity faceEntity, FaceEngine faceEngine) {
|
public void registerFaceFeatureInfoFromDb(FaceEntity faceEntity, FaceEngine faceEngine) {
|
||||||
if (faceEntity != null && faceEngine != null) {
|
if (faceEntity != null && faceEngine != null) {
|
||||||
|
Log.i(TAG, "registerFaceFeature:" + faceEntity.getFaceId()+"==="+ Arrays.toString(faceEntity.getFeatureData()));
|
||||||
FaceFeatureInfo faceFeatureInfo = new FaceFeatureInfo((int) faceEntity.getFaceId(), faceEntity.getFeatureData());
|
FaceFeatureInfo faceFeatureInfo = new FaceFeatureInfo((int) faceEntity.getFaceId(), faceEntity.getFeatureData());
|
||||||
int res = faceEngine.registerFaceFeature(faceFeatureInfo);
|
int res = faceEngine.registerFaceFeature(faceFeatureInfo);
|
||||||
Log.i(TAG, "registerFaceFeature:" + res);
|
Log.i(TAG, "registerFaceFeature:" + res);
|
||||||
|
|||||||
+51
-38
@@ -12,7 +12,6 @@ import androidx.lifecycle.ViewModel;
|
|||||||
|
|
||||||
import com.arcsoft.face.AgeInfo;
|
import com.arcsoft.face.AgeInfo;
|
||||||
import com.arcsoft.face.ErrorInfo;
|
import com.arcsoft.face.ErrorInfo;
|
||||||
import com.arcsoft.face.FaceAttributeParam;
|
|
||||||
import com.arcsoft.face.FaceEngine;
|
import com.arcsoft.face.FaceEngine;
|
||||||
import com.arcsoft.face.FaceInfo;
|
import com.arcsoft.face.FaceInfo;
|
||||||
import com.arcsoft.face.GenderInfo;
|
import com.arcsoft.face.GenderInfo;
|
||||||
@@ -132,13 +131,14 @@ public class RecognizeViewModel extends ViewModel implements RecognizeCallback {
|
|||||||
private MutableLiveData<Integer> ftInitCode = new MutableLiveData<>();
|
private MutableLiveData<Integer> ftInitCode = new MutableLiveData<>();
|
||||||
private MutableLiveData<Integer> frInitCode = new MutableLiveData<>();
|
private MutableLiveData<Integer> frInitCode = new MutableLiveData<>();
|
||||||
private MutableLiveData<Integer> flInitCode = new MutableLiveData<>();
|
private MutableLiveData<Integer> flInitCode = new MutableLiveData<>();
|
||||||
|
private MutableLiveData<Integer> maskInitCode = new MutableLiveData<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 人脸操作辅助类,推帧即可,内部会进行特征提取、识别
|
* 人脸操作辅助类,推帧即可,内部会进行特征提取、识别
|
||||||
*/
|
*/
|
||||||
private FaceHelper faceHelper;
|
private FaceHelper faceHelper;
|
||||||
/**
|
/**
|
||||||
* VIDEO模式人脸检测引擎,用于预览帧人脸追踪及图像质量检测
|
* VIDEO模式人脸检测引擎,用于预览帧人脸追踪
|
||||||
*/
|
*/
|
||||||
private FaceEngine ftEngine;
|
private FaceEngine ftEngine;
|
||||||
/**
|
/**
|
||||||
@@ -149,6 +149,10 @@ public class RecognizeViewModel extends ViewModel implements RecognizeCallback {
|
|||||||
* IMAGE模式活体检测引擎,用于预览帧人脸活体检测
|
* IMAGE模式活体检测引擎,用于预览帧人脸活体检测
|
||||||
*/
|
*/
|
||||||
private FaceEngine flEngine;
|
private FaceEngine flEngine;
|
||||||
|
/**
|
||||||
|
* IMAGE模式口罩检测引擎(5.0 独立引擎)
|
||||||
|
*/
|
||||||
|
private FaceEngine maskEngine;
|
||||||
|
|
||||||
private PreviewConfig previewConfig;
|
private PreviewConfig previewConfig;
|
||||||
|
|
||||||
@@ -160,10 +164,6 @@ public class RecognizeViewModel extends ViewModel implements RecognizeCallback {
|
|||||||
|
|
||||||
private MutableLiveData<CompareResult> recognizeUserId = new MutableLiveData<>();
|
private MutableLiveData<CompareResult> recognizeUserId = new MutableLiveData<>();
|
||||||
|
|
||||||
/**
|
|
||||||
* 检测ir活体前,是否需要更新faceData
|
|
||||||
*/
|
|
||||||
private boolean needUpdateFaceData;
|
|
||||||
/**
|
/**
|
||||||
* 当前活体检测的检测类型
|
* 当前活体检测的检测类型
|
||||||
*/
|
*/
|
||||||
@@ -270,6 +270,7 @@ public class RecognizeViewModel extends ViewModel implements RecognizeCallback {
|
|||||||
*/
|
*/
|
||||||
public void init(PreviewConfig previewConfig1) {
|
public void init(PreviewConfig previewConfig1) {
|
||||||
Context context = App.getContext();
|
Context context = App.getContext();
|
||||||
|
// 档口机业务:支持外部传入 PreviewConfig(指定 RGB/IR 摄像头 id 及旋转角度),未传时走默认配置
|
||||||
if (previewConfig1 != null) {
|
if (previewConfig1 != null) {
|
||||||
previewConfig = previewConfig1;
|
previewConfig = previewConfig1;
|
||||||
} else {
|
} else {
|
||||||
@@ -281,9 +282,11 @@ public class RecognizeViewModel extends ViewModel implements RecognizeCallback {
|
|||||||
Integer.parseInt(ConfigUtil.getIrCameraAdditionalRotation(context))
|
Integer.parseInt(ConfigUtil.getIrCameraAdditionalRotation(context))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 填入在设置界面设置好的配置信息
|
// 填入在设置界面设置好的配置信息
|
||||||
boolean enableLive = !ConfigUtil.getLivenessDetectType(context).equals(context.getString(R.string.value_liveness_type_disable));
|
boolean enableLive = !ConfigUtil.getLivenessDetectType(context).equals(context.getString(R.string.value_liveness_type_disable));
|
||||||
// enableLive = false;
|
enableLive = false;
|
||||||
|
|
||||||
boolean enableFaceQualityDetect = ConfigUtil.isEnableImageQualityDetect(context);
|
boolean enableFaceQualityDetect = ConfigUtil.isEnableImageQualityDetect(context);
|
||||||
boolean enableFaceMoveLimit = ConfigUtil.isEnableFaceMoveLimit(context);
|
boolean enableFaceMoveLimit = ConfigUtil.isEnableFaceMoveLimit(context);
|
||||||
boolean enableFaceSizeLimit = ConfigUtil.isEnableFaceSizeLimit(context);
|
boolean enableFaceSizeLimit = ConfigUtil.isEnableFaceSizeLimit(context);
|
||||||
@@ -299,22 +302,22 @@ public class RecognizeViewModel extends ViewModel implements RecognizeCallback {
|
|||||||
.similarThreshold(ConfigUtil.getRecognizeThreshold(context))
|
.similarThreshold(ConfigUtil.getRecognizeThreshold(context))
|
||||||
.imageQualityNoMaskRecognizeThreshold(ConfigUtil.getImageQualityNoMaskRecognizeThreshold(context))
|
.imageQualityNoMaskRecognizeThreshold(ConfigUtil.getImageQualityNoMaskRecognizeThreshold(context))
|
||||||
.imageQualityMaskRecognizeThreshold(ConfigUtil.getImageQualityMaskRecognizeThreshold(context))
|
.imageQualityMaskRecognizeThreshold(ConfigUtil.getImageQualityMaskRecognizeThreshold(context))
|
||||||
.livenessParam(new LivenessParam(ConfigUtil.getRgbLivenessThreshold(context), ConfigUtil.getIrLivenessThreshold(context),
|
.livenessParam(new LivenessParam(ConfigUtil.getRgbLivenessThreshold(context), ConfigUtil.getIrLivenessThreshold(context)))
|
||||||
ConfigUtil.getLivenessFqThreshold(context)))
|
|
||||||
.build();
|
.build();
|
||||||
int cameraOffsetX = ConfigUtil.getDualCameraHorizontalOffset(context);
|
|
||||||
int cameraOffsetY = ConfigUtil.getDualCameraVerticalOffset(context);
|
|
||||||
needUpdateFaceData = (livenessType == LivenessType.IR && (cameraOffsetX != 0 || cameraOffsetY != 0));
|
|
||||||
|
|
||||||
|
// 人脸追踪引擎(VIDEO模式,仅检测)
|
||||||
ftEngine = new FaceEngine();
|
ftEngine = new FaceEngine();
|
||||||
int ftEngineMask = FaceEngine.ASF_FACE_DETECT | FaceEngine.ASF_MASK_DETECT;
|
int ftEngineMask = FaceEngine.ASF_FACE_DETECT;
|
||||||
ftInitCode.postValue(ftEngine.init(context, DetectMode.ASF_DETECT_MODE_VIDEO, ConfigUtil.getFtOrient(context),
|
ftInitCode.postValue(ftEngine.init(context, DetectMode.ASF_DETECT_MODE_VIDEO, ConfigUtil.getFtOrient(context),
|
||||||
ConfigUtil.getRecognizeMaxDetectFaceNum(context), ftEngineMask));
|
ConfigUtil.getRecognizeMaxDetectFaceNum(context), ftEngineMask));
|
||||||
FaceAttributeParam attributeParam = new FaceAttributeParam(
|
|
||||||
ConfigUtil.getRecognizeEyeOpenThreshold(context), ConfigUtil.getRecognizeMouthCloseThreshold(context),
|
|
||||||
ConfigUtil.getRecognizeWearGlassesThreshold(context));
|
|
||||||
ftEngine.setFaceAttributeParam(attributeParam);
|
|
||||||
|
|
||||||
|
// 口罩检测引擎(5.0 独立引擎)
|
||||||
|
maskEngine = new FaceEngine();
|
||||||
|
maskInitCode.postValue(maskEngine.init(context, DetectMode.ASF_DETECT_MODE_IMAGE,
|
||||||
|
DetectFaceOrientPriority.ASF_OP_ALL_OUT,
|
||||||
|
ConfigUtil.getRecognizeMaxDetectFaceNum(context), FaceEngine.ASF_MASK_DETECT));
|
||||||
|
|
||||||
|
// 特征提取引擎
|
||||||
frEngine = new FaceEngine();
|
frEngine = new FaceEngine();
|
||||||
int frEngineMask = FaceEngine.ASF_FACE_RECOGNITION;
|
int frEngineMask = FaceEngine.ASF_FACE_RECOGNITION;
|
||||||
if (enableFaceQualityDetect) {
|
if (enableFaceQualityDetect) {
|
||||||
@@ -324,16 +327,13 @@ public class RecognizeViewModel extends ViewModel implements RecognizeCallback {
|
|||||||
10, frEngineMask));
|
10, frEngineMask));
|
||||||
FaceServer.getInstance().initFaceList(context, frEngine, faceCount -> loadFaceList = true, true);
|
FaceServer.getInstance().initFaceList(context, frEngine, faceCount -> loadFaceList = true, true);
|
||||||
|
|
||||||
//启用活体检测时,才初始化活体引擎
|
// 启用活体检测时,才初始化活体引擎
|
||||||
if (enableLive) {
|
if (enableLive) {
|
||||||
flEngine = new FaceEngine();
|
flEngine = new FaceEngine();
|
||||||
int flEngineMask = (livenessType == LivenessType.RGB ? FaceEngine.ASF_LIVENESS : (FaceEngine.ASF_IR_LIVENESS | FaceEngine.ASF_FACE_DETECT));
|
int flEngineMask = (livenessType == LivenessType.RGB ? FaceEngine.ASF_LIVENESS : (FaceEngine.ASF_IR_LIVENESS | FaceEngine.ASF_FACE_DETECT));
|
||||||
if (needUpdateFaceData) {
|
|
||||||
flEngineMask |= FaceEngine.ASF_UPDATE_FACEDATA;
|
|
||||||
}
|
|
||||||
flInitCode.postValue(flEngine.init(context, DetectMode.ASF_DETECT_MODE_IMAGE,
|
flInitCode.postValue(flEngine.init(context, DetectMode.ASF_DETECT_MODE_IMAGE,
|
||||||
DetectFaceOrientPriority.ASF_OP_ALL_OUT, 10, flEngineMask));
|
DetectFaceOrientPriority.ASF_OP_ALL_OUT, 10, flEngineMask));
|
||||||
LivenessParam livenessParam = new LivenessParam(ConfigUtil.getRgbLivenessThreshold(context), ConfigUtil.getIrLivenessThreshold(context), ConfigUtil.getLivenessFqThreshold(context));
|
LivenessParam livenessParam = new LivenessParam(ConfigUtil.getRgbLivenessThreshold(context), ConfigUtil.getIrLivenessThreshold(context));
|
||||||
flEngine.setLivenessParam(livenessParam);
|
flEngine.setLivenessParam(livenessParam);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,8 +341,10 @@ public class RecognizeViewModel extends ViewModel implements RecognizeCallback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void addFace(FaceEntity faceEntity) {
|
public void addFace(FaceEntity faceEntity) {
|
||||||
if (frEngine != null)
|
if (frEngine != null) {
|
||||||
|
Log.e("performSync", "addFace=" + faceEntity.getRegisterTime());
|
||||||
FaceServer.getInstance().registerFaceFeatureInfoFromDb(faceEntity, frEngine);
|
FaceServer.getInstance().registerFaceFeatureInfoFromDb(faceEntity, frEngine);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void refreshFaceList() {
|
public void refreshFaceList() {
|
||||||
@@ -356,19 +358,25 @@ public class RecognizeViewModel extends ViewModel implements RecognizeCallback {
|
|||||||
if (ftEngine != null) {
|
if (ftEngine != null) {
|
||||||
synchronized (ftEngine) {
|
synchronized (ftEngine) {
|
||||||
int ftUnInitCode = ftEngine.unInit();
|
int ftUnInitCode = ftEngine.unInit();
|
||||||
Log.i(TAG, "unInitEngine: " + ftUnInitCode);
|
Log.i(TAG, "unInitEngine ft: " + ftUnInitCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (maskEngine != null) {
|
||||||
|
synchronized (maskEngine) {
|
||||||
|
int maskUnInitCode = maskEngine.unInit();
|
||||||
|
Log.i(TAG, "unInitEngine mask: " + maskUnInitCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (frEngine != null) {
|
if (frEngine != null) {
|
||||||
synchronized (frEngine) {
|
synchronized (frEngine) {
|
||||||
int frUnInitCode = frEngine.unInit();
|
int frUnInitCode = frEngine.unInit();
|
||||||
Log.i(TAG, "unInitEngine: " + frUnInitCode);
|
Log.i(TAG, "unInitEngine fr: " + frUnInitCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (flEngine != null) {
|
if (flEngine != null) {
|
||||||
synchronized (flEngine) {
|
synchronized (flEngine) {
|
||||||
int flUnInitCode = flEngine.unInit();
|
int flUnInitCode = flEngine.unInit();
|
||||||
Log.i(TAG, "unInitEngine: " + flUnInitCode);
|
Log.i(TAG, "unInitEngine fl: " + flUnInitCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -400,6 +408,7 @@ public class RecognizeViewModel extends ViewModel implements RecognizeCallback {
|
|||||||
/**
|
/**
|
||||||
* 重置人脸识别状态:清空上次识别结果及 FaceHelper 内部的 recognizeInfoMap,
|
* 重置人脸识别状态:清空上次识别结果及 FaceHelper 内部的 recognizeInfoMap,
|
||||||
* 使下一帧进入时能重新触发识别流程。
|
* 使下一帧进入时能重新触发识别流程。
|
||||||
|
* 适用场景:短时间内再次识别、点击重试按钮等需要重新开始识别的时机。
|
||||||
*/
|
*/
|
||||||
public void resetFaceState() {
|
public void resetFaceState() {
|
||||||
// 清空粘性 LiveData,防止旧结果被重新投递给 observer
|
// 清空粘性 LiveData,防止旧结果被重新投递给 observer
|
||||||
@@ -468,7 +477,7 @@ public class RecognizeViewModel extends ViewModel implements RecognizeCallback {
|
|||||||
.ftEngine(ftEngine)
|
.ftEngine(ftEngine)
|
||||||
.frEngine(frEngine)
|
.frEngine(frEngine)
|
||||||
.flEngine(flEngine)
|
.flEngine(flEngine)
|
||||||
.needUpdateFaceData(needUpdateFaceData)
|
.maskEngine(maskEngine)
|
||||||
.frQueueSize(maxDetectFaceNum)
|
.frQueueSize(maxDetectFaceNum)
|
||||||
.flQueueSize(maxDetectFaceNum)
|
.flQueueSize(maxDetectFaceNum)
|
||||||
.previewSize(previewSize)
|
.previewSize(previewSize)
|
||||||
@@ -484,13 +493,20 @@ public class RecognizeViewModel extends ViewModel implements RecognizeCallback {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getUserId(CompareResult result) {
|
||||||
|
if (result != null && result.getFaceEntity() != null) {
|
||||||
|
return result.getFaceEntity().getUserName();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRecognized(CompareResult compareResult, Integer live, boolean similarPass) {
|
public void onRecognized(CompareResult compareResult, Integer live, boolean similarPass) {
|
||||||
Disposable disposable = Observable.just(true).observeOn(AndroidSchedulers.mainThread()).subscribe(aBoolean -> {
|
// TODO: 2026/1/21 测试使用 Observable.just(similarPass)代替Observable.just(true)---
|
||||||
|
Observable.just(similarPass).observeOn(AndroidSchedulers.mainThread()).subscribe(aBoolean -> {
|
||||||
|
Log.d(TAG, "collectFace,onRecognized: similarPass=" + similarPass + ",live=" + live + ",userId=" + getUserId(compareResult));
|
||||||
if (similarPass) {
|
if (similarPass) {
|
||||||
if (recognizeUserId != null) {
|
recognizeUserId.postValue(compareResult);
|
||||||
recognizeUserId.postValue(compareResult);
|
|
||||||
}
|
|
||||||
boolean isAdded = false;
|
boolean isAdded = false;
|
||||||
List<CompareResult> compareResults = compareResultList.getValue();
|
List<CompareResult> compareResults = compareResultList.getValue();
|
||||||
if (compareResults != null && !compareResults.isEmpty()) {
|
if (compareResults != null && !compareResults.isEmpty()) {
|
||||||
@@ -512,6 +528,8 @@ public class RecognizeViewModel extends ViewModel implements RecognizeCallback {
|
|||||||
getFaceItemEventMutableLiveData().postValue(new FaceItemEvent(compareResults.size() - 1, EventType.INSERTED));
|
getFaceItemEventMutableLiveData().postValue(new FaceItemEvent(compareResults.size() - 1, EventType.INSERTED));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
recognizeUserId.postValue(compareResult);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -599,14 +617,9 @@ public class RecognizeViewModel extends ViewModel implements RecognizeCallback {
|
|||||||
if (recognizeStatus != null) {
|
if (recognizeStatus != null) {
|
||||||
if (recognizeStatus == RequestFeatureStatus.FAILED) {
|
if (recognizeStatus == RequestFeatureStatus.FAILED) {
|
||||||
color = RecognizeColor.COLOR_FAILED;
|
color = RecognizeColor.COLOR_FAILED;
|
||||||
}else if (recognizeStatus == RequestFeatureStatus.SUCCEED) {
|
}
|
||||||
|
if (recognizeStatus == RequestFeatureStatus.SUCCEED) {
|
||||||
color = RecognizeColor.COLOR_SUCCESS;
|
color = RecognizeColor.COLOR_SUCCESS;
|
||||||
} else if (recognizeStatus == RequestFeatureStatus.TO_RETRY) {
|
|
||||||
color = RecognizeColor.COLOR_UNKNOWN;
|
|
||||||
//需要重试
|
|
||||||
// FaceEntity faceEntity = new FaceEntity("2",null, null);
|
|
||||||
// CompareResult result = new CompareResult(faceEntity, 0.0f);
|
|
||||||
// recognizeUserId.postValue(result);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (liveness != null && liveness == LivenessInfo.NOT_ALIVE) {
|
if (liveness != null && liveness == LivenessInfo.NOT_ALIVE) {
|
||||||
|
|||||||
Reference in New Issue
Block a user