Author SHA1 Message Date
mazengfei a769eae2a0 fix(global): 修正生产环境接口地址
- 更新生产环境基础URL地址为新的测试平台地址
- 注释掉旧的生产环境地址备份记录
- 保持开发和测试环境地址不变
- 确保接口调用指向正确的服务器环境
2026-08-10 14:01:38 +08:00
mazengfei 0241caa5fc fix(main): 修复计算吃饭数量返回值小于1的问题
- 修改返回值逻辑,当计算结果小于1时返回1
- 防止吃饭数量为0或负值导致的异常情况
- 优化代码可读性,提升鲁棒性
2026-08-05 17:57:38 +08:00
mazengfei 512e0b5b4f feat(weightBilling): 优化按重量计费逻辑并完善界面展示
- 规格栏显示改为直接展示规格重量,去除单价/100克显示
- 新增最小计费重量常量 MIN_BILLABLE_WEIGHT,净重不足时阻止下单
- 下单时检测净重,低于最小计费重量时弹出提示并拦截
- 计算价格时按净重计算,单价基于规格价与规格重量的比值
- 本地明细中 eatNum 仅用于展示份数,不参与金额计算
- SettlementOrder 用实际食用重量 eatWeight 赋值 num 字段,配合重量栏显示逻辑调整
2026-08-05 10:23:48 +08:00
mazengfei a8ade16058 fix(MainScreenPresentation): 修正取餐重量计算逻辑
- 去除净重计算,改以实际重量做后续份数、价格、总重量计算
- 修正计价和取餐份数计算中使用的重量参数
- 调整本地数据订单中数量赋值,使用实际重量替代净重
- 在营养计算和柱状图绘制中扣减餐具重量,确保显示净重
- 保持取餐模式余量计量逻辑兼容,优化逻辑判断条件
2026-07-28 09:18:36 +08:00
mazengfei ffd00266b8 fix(recognition): 防止识别时重复触发导致多次并发处理
- 新增 isRecognizing 标记防止识别过程重复进入
- 在识别开始时设置标记,结束时释放标记
- 多处识别终止和异常处理处均释放识别锁
- 在重量回调中增加去重逻辑,避免同一重量重复触发回调
- 更新 lastCallbackWeight,确保仅在重量变化时触发回调
2026-07-24 16:54:19 +08:00
mazengfei 43f5434cbd feat(business): 增加餐具重量配置及扣减功能
- 在 BusinessFragment 中添加实时秤重读取及餐具重量持久化存储
- 在界面显示餐具重量标签,配置时显示当前保存值
- 定义全局常量 KEY_DISH_WEIGHT 用于存储餐具重量
- 在 MainActivity 内部方法添加获取配置餐具重量接口
- 修改订餐逻辑,取餐提交时自动从就餐重量中扣减餐具重量
- 在主界面相关订单及计价逻辑中扣减餐具重量影响份数和价格
- 调整重量监听及订单添加部分逻辑以应用净重计算
- 更新定时任务延迟时间,提高效率和响应速度
2026-07-24 15:56:08 +08:00
mazengfei 76f60f8ee7 fix(build): 修复ObjectBox库的release依赖配置
- 取消注释并激活app模块中ObjectBox的releaseImplementation依赖
- 取消注释并激活lib_face模块中ObjectBox的releaseImplementation依赖

fix(ui): 修复异常时Toast显示的线程调度问题

- 在CollectFragment中使用lifecycleScope切换到主线程显示Toast

refactor(main): 优化图像处理流程中的协程使用

- MainActivity中将uri2File调用放入IO线程的lifecycleScope中
- 使用withContext切换至IO线程异步获取Bitmap后再进行处理

chore(objbox): 移除未使用的ObjectBoxLiveData导入声明
2026-07-17 09:53:43 +08:00
mazengfei 28a475ca43 feat(face): 优化人脸数据同步及类型映射
- 新增接口注释,完善全量及增量人脸数据获取说明
- FaceVO中faceUpdateTimestamp由Long改为String以防JS大数精度丢失
- 增加personType字段标识人员类型
- MainActivity保存人脸数据时映射完整字段,使用personType优先判断用户类型
- NetViewModelV2中获取人脸数据时改用完整构造函数创建FaceEntity
- 累积更新lastFaceTimestamp为当前页最大时间戳,避免时间戳遗漏
- 优化时间戳更新逻辑,避免直接使用列表最后一条数据
2026-07-15 10:56:40 +08:00
mazengfei 16dba32eb7 feat(face): 升级虹软SDK到5.0及完善人脸识别逻辑
- FaceHelper中集成独立口罩检测引擎maskEngine并调用口罩检测接口
- FaceEngine初始化中增加口罩检测引擎初始化及状态管理
- FaceServer注册人脸时打印特征信息日志,便于调试
- CompareResult新增similarPass字段,标记识别是否通过
- FaceApi新增针对人脸数据的增删查接口,方便管理
- FaceDatabase数据库版本升级至2,支持人脸特征库重建迁移
- FaceDao新增多用户查询、人脸计数及临时用户数据删除接口
- FaceEntity新增userId、userFaceId、member标记及更新时间字段及相关方法
- 优化人脸识别失败重试机制,连续失败后回调识别失败结果
- 优化识别回调逻辑,成功与失败结果均及时通知观察者
- 调整配置获取逻辑,禁用人脸活体检测时关闭相关初始化
- 精简并修正部分冗余代码与日志输出,提升代码清晰度与可维护性
2026-07-15 09:17:23 +08:00
mazengfei 5a7b090c02 fix(ml): 优化PyTorch模型推理流程与资源管理
- 修改API接口路径从serve切换到common,统一资源调用
- 增加独立推理线程,隔离ArcSoft线程污染,保障FPU状态稳定
- 推理模块预热,降低PyTorch线程初启功率峰值,避免电流保护触发
- 推理前后记录系统内存信息,辅助OOM重启排查
- 主屏相机与副屏人脸识别推理前暂停,规避硬件并发带宽冲突重启
- 控制相机HAL清理时长,确保DMC带宽资源释放后再启动推理
- 异步推理调用增加超时机制,避免长时间卡死
- 推理结束后恢复相机,确保正常工作流程
2026-07-08 16:23:37 +08:00
lvmeng b6ed47ff87 fix(camera): 解决相机拍照功能的竞态条件和重试机制问题
- 修复了相机回调中的竞态条件,使用isShowCamera标识查找空闲槽位
- 将图片处理逻辑移至IO线程,避免主线程阻塞
- 添加了照片拍摄失败回调处理函数
- 简化了相机拍照调用方式,直接传递回调函数
- 在FoodCollectionAdapter中使用Glide加载图片uri
- 添加了相机绑定失败时的自动重试机制,包含延迟重试逻辑
- 增加了详细的错误日志记录和异常处理
2026-06-30 15:54:45 +08:00
lvmeng 088aabc535 refactor(discount): 简化会员折扣空值处理逻辑
- 使用Elvis操作符替代条件表达式判断空值
- 统一两个位置的折扣计算逻辑实现方式
- 提高代码可读性和简洁性
2026-06-30 09:51:35 +08:00
mazengfeiandClaude Opus 4.6 061680b32b fix(objectbox): 修复32位系统向量查询崩溃,统一使用findIdsWithScores避免加载大对象
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-04 17:26:36 +08:00
mazengfei 5c3e51626c fix(network): 修正人脸识别接口路径并移除硬编码的SDK密钥
- 修复 ApiClient 中的人脸识别增量同步接口路径
- 将 InitActivity 中的 ArcSoft SDK 密钥配置改为注释状态
- 移除临时测试用的硬编码 SDK 配置逻辑
- 调整代码执行顺序以确保正确的初始化流程
2026-06-04 11:19:50 +08:00
lvmeng 936bf206dc ```
refactor(env): 调整环境配置和切换逻辑

- 将本地环境配置从注释状态启用,并更新对应的IP地址和端口
- 修改环境切换对话框中的文本标签,重新排列环境选项顺序
- 更新全局数据类中的环境URL常量定义,调整测试和UAT环境地址
- 移除未使用的SPUtil导入,改用SpTool工具类
- 修改应用启动时的默认环境判断逻辑,将特定设备指向本地环境
- 调整环境切换对话框中的RadioButton选中状态和URL映射关系
```
2026-06-03 16:34:16 +08:00
lvmeng 8e4dcb9f61 fix(arcface): 禁用活体检测功能
- 将enableLive变量强制设置为false以禁用活体检测
- 移除注释掉的调试代码行
2026-06-03 16:32:39 +08:00
lvmeng 8ba4ba6db0 feat(order): 开餐下单新增cookOrderId支持用于溯源追踪
- 在FoodInfo数据模型中新增cookOrderId字段
- 在NewFoodInfo数据模型中新增cookOrderId字段并实现映射
- 在PlaceOrderRequest请求模型中新增cookOrderId参数
- 在订单提交流程中传递cookOrderId参数
- 实现菜品列表到就餐环节的ID回传机制
2026-06-02 09:07:21 +08:00
lvmeng 37f86e202f refactor(camera): 优化相机拍照功能实现
- 移除 PhotoCaptureHelper 中的测试模式和无用的 onSuccess 回调参数
- 将 CollectFragment 中的 UI 操作迁移到正确的协程调度器 (Main 和 IO)
- 使用 lifecycleScope 替代 runOnUiThread 确保生命周期安全
- 重构 ImageUtil.uriToBitmap 方法,添加图片尺寸压缩逻辑以减少内存占用
- 从 InitActivity 中移除已注释的人脸识别相关初始化代码
- 修复 cameraCallback 中的拼写错误 (rerurn@ -> return@)
2026-05-29 13:52:07 +08:00
lvmeng 737baf8486 feat(api): 更新API端点并集成人脸数据库导入功能
- 将所有API端点中的/booth路径替换为/serve
- 新增searchFood API用于菜品搜索功能
- 新增getCollectVectorPage API用于向量数据分页获取
- 添加FaceDbImporter工具类实现从assets导入人脸数据
- 在初始化活动中集成人脸数据导入功能
- 更新订单创建逻辑适配新的API参数结构
- 重构菜品搜索和订单处理相关API调用
- 添加V2数据模型到V1模型的转换扩展函数
- 更新绑定订单接口以使用新的API端点
- 添加测试模式支持以跳过实际拍照操作
2026-05-28 19:57:40 +08:00
lvmeng bf6b2547f3 refactor(network): 重构网络请求配置和API接口实现
- 将BASE_URL改为使用GlobalData.PROD_BASE_URL常量
- 更新ApiClient中的URL匹配规则以支持新的营养模块接口
- 将ApiServiceV2中的接口路径从/neglect/booth改为/nutrition/neglect/booth
- 使用@Url注解动态传入完整URL地址
- 在CollectedFoodActivity中切换到NetViewModelV2和新API模型
- 更新采集功能的数据模型和接口调用方式
- 调整人脸数据获取逻辑以支持增量和全量同步
- 优化文件上传和删除操作的参数传递方式
2026-05-28 15:14:44 +08:00
lvmeng 6abd2f9b0b feat(network): 添加新系统API V2支持及相关数据模型
- 引入 ApiServiceV2 接口定义新系统API端点
- 添加 RemoteRepositoryV2 实现新API的数据访问层
- 创建 NetViewModelV2 提供新API的业务逻辑处理
- 定义 v2 包下的数据模型类包括请求响应对象
- 在 ApiClient 中集成新旧两套API服务实例
- 配置API日志拦截器识别新API调用并打上ApiV2标签
- 实现新系统菜品、人脸、订单、会员等完整功能接口
2026-05-28 09:33:05 +08:00
58 changed files with 2237 additions and 452 deletions
+1 -1
View File
@@ -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"
} }
+1 -2
View File
@@ -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,14 +85,14 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
getCollectedFoodVector { getCollectedFoodVector {
runOnUiThread { runOnUiThread {
hideWaitingDialog() hideWaitingDialog()
}
}
}
binding.root.postDelayed({ binding.root.postDelayed({
checkCameraPermissionAndGo() checkCameraPermissionAndGo()
}, 1000) }, 1000)
} }
} }
}
}
}
private fun goMainActivity() { private fun goMainActivity() {
val intent = Intent(this, MainActivity::class.java) val intent = Intent(this, MainActivity::class.java)
@@ -188,24 +173,34 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
} }
} }
// var arcsoftAppId = "Hkz1rBk6PZXbS8KwKr67K2eZtsz8bRoMHLg64bUdgCZj"
// var arcsoftSdkKey = "AabXs3sHM8UhhE7oCGf4LVMLrdkGb1nJNksbjjTdVt7k"
// var arcsoftActiveKey = "085F-118G-Q4GH-B2YH"
private fun getDeviceConfig() { private fun getDeviceConfig() {
userViewModel.getDeviceConfig { deviceConfig -> userViewModel.getDeviceConfig(
onSuccess = { deviceConfig ->
runOnUiThread { runOnUiThread {
if (deviceConfig == null) { if (deviceConfig == null) {
ToastUtils.showToast("获取设备配置数据失败") ToastUtils.showToast("获取设备配置数据失败")
return@runOnUiThread return@runOnUiThread
} }
//var settlementMode = 1
//结算模式:1-独立支付,2-联合结算
val settlementMode = if (deviceConfig.payType == 2) 0 else 1 val settlementMode = if (deviceConfig.payType == 2) 0 else 1
// TODO: 2022/10/13 修改为独立支付
SpTool.settlementMode = settlementMode SpTool.settlementMode = settlementMode
// // TODO: 临时写死用于测试----------------------------------------
// deviceConfig.arcsoftAppId = arcsoftAppId
// deviceConfig.arcsoftSdkKey = arcsoftSdkKey
// deviceConfig.arcsoftActiveKey = arcsoftActiveKey
// // TODO: 临时写死用于测试----------------------------------------
GlobalData.appId = deviceConfig.arcsoftAppId ?: "" GlobalData.appId = deviceConfig.arcsoftAppId ?: ""
GlobalData.sdkKey = deviceConfig.arcsoftSdkKey ?: "" GlobalData.sdkKey = deviceConfig.arcsoftSdkKey ?: ""
GlobalData.activeKey = deviceConfig.arcsoftActiveKey ?: "" 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
/** /**
* 用于切换菜品时重置数据 * 用于切换菜品时重置数据
*/ */
@@ -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)
@@ -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,6 +590,7 @@ 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)
@@ -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,64 +914,62 @@ 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(
orderRequest = request,
onSuccess = { orderId ->
runOnUiThread { runOnUiThread {
binding.root.postDelayed({ binding.root.postDelayed({
hideWaitingDialog() hideWaitingDialog()
if (orderId.isBlank()) { if (orderId.isNullOrBlank()) {
//ToastUtils.showToast("订单id为空")
return@postDelayed return@postDelayed
} }
foodOrderId = orderId foodOrderId = orderId
successBlock() successBlock()
if (checkedItem?.isFromSearch == true) { if (checkedItem?.isFromSearch == true) {
//当前菜名为手动搜索选择,非识别结果,保存向量数据
lifecycleScope.launch { lifecycleScope.launch {
saveFoodVector(checkedItem!!) saveFoodVector(checkedItem!!)
} }
} }
}, 300) }, 300)
} }
},
onFailure = { errorMsg ->
runOnUiThread {
hideWaitingDialog()
ToastUtils.showToast(errorMsg)
} }
} }
)
}
fun getEatNum(realWeight: Double?, specWeight: Double?): Int { fun getEatNum(realWeight: Double?, specWeight: Double?): Int {
if (realWeight == null || realWeight <= 0.0 || specWeight == null || specWeight <= 0.0) { if (realWeight == null || realWeight <= 0.0 || specWeight == null || specWeight <= 0.0) {
@@ -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,11 +1123,13 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
if (lastPhotoUri == null) { if (lastPhotoUri == null) {
return return
} }
lifecycleScope.launch(Dispatchers.IO) {
uri2File(lastPhotoUri!!) { imageFile, imageVector -> uri2File(lastPhotoUri!!) { imageFile, imageVector ->
runBlocking { runBlocking {
uploadCollectFoodPics(foodInfo, imageFile, imageVector) uploadCollectFoodPics(foodInfo, imageFile, imageVector)
} }
} }
}
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
@@ -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) {
// 主线程原子操作:查找空闲槽位并立即标记,避免竞态
val index = foodCollectionList.indexOfFirst { it.isShowCamera }
if (index == -1) { if (index == -1) {
ToastUtils.showToast("每次只允许保存${MAX_COUNT}条数据") ToastUtils.showToast("每次只允许保存${MAX_COUNT}条数据")
settingActivity?.hideWaitingDialog() settingActivity?.hideWaitingDialog()
rerurn@ cameraCallback return@launch
} }
activity?.runOnUiThread {
foodCollectionList[index].let { foodCollectionList[index].let {
it.imageVector = null it.imageVector = null
it.bitmap = null it.bitmap = null
@@ -116,13 +116,13 @@ 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) {
e.printStackTrace() e.printStackTrace()
settingActivity?.hideWaitingDialog() settingActivity?.hideWaitingDialog()
@@ -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()
activity?.lifecycleScope?.launch(Dispatchers.Main) {
ToastUtils.showToast("操作失败") 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 数据模型兼容旧版 UIfaceUserId 复用 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,9 +18,10 @@ 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)
@@ -25,8 +30,12 @@ object ApiClient {
.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,12 +46,16 @@ object SensorScaleUtils {
callback?.invoke(value) callback?.invoke(value)
} }
// 去重:仅在重量变化时回调 weightCallbackList,避免同一重量反复触发
if (lastCallbackWeight != value) {
lastCallbackWeight = value
weightCallbackList.forEach { weightCallbackList.forEach {
val currentWeight = value * 1000 val currentWeight = value * 1000
it.invoke(currentWeight.roundToInt()) 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
)
) { items ->
if (items == null) {
failureBlock("未查询到向量数据")
return@getCollectedFoodVector
}
// 第一页为空,视为成功(数据库无数据)
if (pageNum == 1 && items.isEmpty()) { if (pageNum == 1 && items.isEmpty()) {
successBlock() successBlock()
return@getCollectedFoodVector return@getCollectVectorPage
} }
// 有数据则保存
if (items.isNotEmpty()) { if (items.isNotEmpty()) {
saveFoodVector(lifecycleScope, items) saveFoodVector(lifecycleScope, items)
} }
// 数据量达到一页上限,继续拉取下一页
if (items.size >= PAGE_SIZE) { if (items.size >= PAGE_SIZE) {
fetchPage(pageNum + 1, userViewModel, lifecycleScope, successBlock, failureBlock) fetchPage(pageNum + 1, userViewModel, lifecycleScope, successBlock, failureBlock)
} else { } else {
successBlock() successBlock()
} }
} },
onFailure = { failureBlock(it) }
)
} }
/** /**
* 将向量数据列表保存到 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"
+1 -1
View File
@@ -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 上下文
@@ -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,26 +32,22 @@ 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();
if (index == 0) {
faceDao.deleteAll(); faceDao.deleteAll();
faceDao.resetId(); faceDao.resetId();
} }
faceDao.insert(list);
}
/** /**
* 激活arcsoft 人脸 * 激活arcsoft 人脸
@@ -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();
@@ -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) {
/*
* 若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); 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);
} }
@@ -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;
@@ -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) {
@@ -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);
@@ -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) {
@@ -328,12 +331,9 @@ public class RecognizeViewModel extends ViewModel implements RecognizeCallback {
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,9 +341,11 @@ 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() {
FaceServer.getInstance().initFaceList(App.getContext(), frEngine, faceCount -> loadFaceList = true, true); FaceServer.getInstance().initFaceList(App.getContext(), frEngine, faceCount -> loadFaceList = true, true);
@@ -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) {