feat(api): 更新API端点并集成人脸数据库导入功能
- 将所有API端点中的/booth路径替换为/serve - 新增searchFood API用于菜品搜索功能 - 新增getCollectVectorPage API用于向量数据分页获取 - 添加FaceDbImporter工具类实现从assets导入人脸数据 - 在初始化活动中集成人脸数据导入功能 - 更新订单创建逻辑适配新的API参数结构 - 重构菜品搜索和订单处理相关API调用 - 添加V2数据模型到V1模型的转换扩展函数 - 更新绑定订单接口以使用新的API端点 - 添加测试模式支持以跳过实际拍照操作
This commit is contained in:
@@ -14,6 +14,7 @@ import com.sw.dualscreen.ext.clickWithDebounce
|
|||||||
import com.sw.dualscreen.ext.invisible
|
import com.sw.dualscreen.ext.invisible
|
||||||
import com.sw.dualscreen.ext.visible
|
import com.sw.dualscreen.ext.visible
|
||||||
import com.sw.dualscreen.objbox.FoodModule
|
import com.sw.dualscreen.objbox.FoodModule
|
||||||
|
import com.sw.dualscreen.utils.FaceDbImporter
|
||||||
import com.sw.dualscreen.utils.FoodVectorTool
|
import com.sw.dualscreen.utils.FoodVectorTool
|
||||||
import com.sw.dualscreen.utils.L
|
import com.sw.dualscreen.utils.L
|
||||||
import com.sw.dualscreen.utils.NetworkUtils
|
import com.sw.dualscreen.utils.NetworkUtils
|
||||||
@@ -22,7 +23,10 @@ import com.sw.dualscreen.viewmodel.BaseViewModel
|
|||||||
import com.sw.dualscreen.viewmodel.NetViewModelV2
|
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 com.sw.plate.utils.arcface.faceserver.FaceServer
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class InitActivity : BaseActivity<ActivityInitBinding>() {
|
class InitActivity : BaseActivity<ActivityInitBinding>() {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -80,6 +84,13 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
|
|||||||
// binding.initButton.setOnClickListener {
|
// binding.initButton.setOnClickListener {
|
||||||
// viewModel.getDeviceToken()
|
// viewModel.getDeviceToken()
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
FaceServer.getInstance().clearAllFaces()
|
||||||
|
loadTestDb()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun registerDataChange() {
|
override fun registerDataChange() {
|
||||||
@@ -189,6 +200,10 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var arcsoftAppId = "Hkz1rBk6PZXbS8KwKr67K2eZtsz8bRoMHLg64bUdgCZj"
|
||||||
|
var arcsoftSdkKey = "AabXs3sHM8UhhE7oCGf4LVMLrdkGb1nJNksbjjTdVt7k"
|
||||||
|
var arcsoftActiveKey = "085F-118G-Q4GH-B2YH"
|
||||||
|
|
||||||
private fun getDeviceConfig() {
|
private fun getDeviceConfig() {
|
||||||
userViewModel.getDeviceConfig(
|
userViewModel.getDeviceConfig(
|
||||||
onSuccess = { deviceConfig ->
|
onSuccess = { deviceConfig ->
|
||||||
@@ -200,6 +215,12 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
|
|||||||
val settlementMode = if (deviceConfig.payType == 2) 0 else 1
|
val settlementMode = if (deviceConfig.payType == 2) 0 else 1
|
||||||
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 ?: ""
|
||||||
@@ -240,4 +261,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.v2.FaceVO
|
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
|
||||||
@@ -134,6 +136,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
var isSwitchFood = false
|
var isSwitchFood = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用于切换菜品时重置数据
|
* 用于切换菜品时重置数据
|
||||||
*/
|
*/
|
||||||
@@ -145,11 +148,11 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
|||||||
//联合支付,切换菜品重新请求接口
|
//联合支付,切换菜品重新请求接口
|
||||||
presentation?.let {
|
presentation?.let {
|
||||||
val userId = presentation?.currentUserId
|
val userId = presentation?.currentUserId
|
||||||
if(userId.isNullOrBlank()) {
|
if (userId.isNullOrBlank()) {
|
||||||
//切换菜品时,用户已离开重新打开识别功能
|
//切换菜品时,用户已离开重新打开识别功能
|
||||||
it.pauseCamera()
|
it.pauseCamera()
|
||||||
it.step2FaceRecognizing(foodInfo, true)
|
it.step2FaceRecognizing(foodInfo, true)
|
||||||
} else{
|
} else {
|
||||||
//切换菜品时,用户未离开,重新请求接口
|
//切换菜品时,用户未离开,重新请求接口
|
||||||
it.step3ShowRecognizeResult(userId)
|
it.step3ShowRecognizeResult(userId)
|
||||||
}
|
}
|
||||||
@@ -374,7 +377,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)
|
||||||
@@ -430,7 +435,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
|||||||
val mealPickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0
|
val mealPickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0
|
||||||
//0-计费,1-不计费
|
//0-计费,1-不计费
|
||||||
val chargeMode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0)
|
val chargeMode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0)
|
||||||
if(mealPickupMode == 0 && chargeMode == 1) {
|
if (mealPickupMode == 0 && chargeMode == 1) {
|
||||||
//即放即取+不计费模式,
|
//即放即取+不计费模式,
|
||||||
if (presentation?.currentUserId.isNullOrBlank().not()) {
|
if (presentation?.currentUserId.isNullOrBlank().not()) {
|
||||||
//已识别人脸的状态下,从秤上拿餐品后,不再读取数据,等待人脸离开提交订单
|
//已识别人脸的状态下,从秤上拿餐品后,不再读取数据,等待人脸离开提交订单
|
||||||
@@ -571,7 +576,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
|||||||
}
|
}
|
||||||
foodRecognizeState = true
|
foodRecognizeState = true
|
||||||
//查询到菜品信息,同步设置lastWeight=currentWeight
|
//查询到菜品信息,同步设置lastWeight=currentWeight
|
||||||
lastWeight=currentWeight
|
lastWeight = currentWeight
|
||||||
presentation?.updateWeight(currentWeight)
|
presentation?.updateWeight(currentWeight)
|
||||||
|
|
||||||
list.forEach { foodInfo ->
|
list.forEach { foodInfo ->
|
||||||
@@ -892,64 +897,60 @@ 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) {
|
|
||||||
// ToastUtils.showToast("餐品重量不足")
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// if (eatWeight <= 5) {
|
|
||||||
// ToastUtils.showToast("取餐重量过小")
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
if (eatNum <= 0) {
|
if (eatNum <= 0) {
|
||||||
ToastUtils.showToast("您好,当前重量不足一份")
|
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
|
||||||
)
|
)
|
||||||
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) {
|
||||||
@@ -1147,7 +1148,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
|
||||||
@@ -1325,14 +1326,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)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.sw.dualscreen.model.response.v2
|
package com.sw.dualscreen.model.response.v2
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import com.sw.dualscreen.model.response.FoodInfo
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
|
|
||||||
@@ -31,3 +32,23 @@ data class NewFoodInfo(
|
|||||||
val tablewareStatus: Boolean = false,
|
val tablewareStatus: Boolean = false,
|
||||||
val tablewareWeight: Int = 0
|
val tablewareWeight: Int = 0
|
||||||
) : Parcelable
|
) : 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
|
||||||
|
)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.sw.dualscreen.model.response.v2
|
|||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import com.sw.dualscreen.model.response.MemberInfo
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -19,3 +20,16 @@ data class NewMemberInfo(
|
|||||||
val integralBalance: Int?,
|
val integralBalance: Int?,
|
||||||
val member: Boolean?
|
val member: Boolean?
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
|
/** NewMemberInfo → MemberInfo 映射(V2 数据模型兼容旧版 UI,faceUserId 复用 id) */
|
||||||
|
fun NewMemberInfo.toMemberInfo() = MemberInfo(
|
||||||
|
id = id,
|
||||||
|
faceUserId = id,
|
||||||
|
phone = phone,
|
||||||
|
name = name,
|
||||||
|
faceUrl = faceUrl,
|
||||||
|
topUpBalance = topUpBalance?.toDouble(),
|
||||||
|
rewardBalance = rewardBalance?.toDouble(),
|
||||||
|
integralBalance = integralBalance,
|
||||||
|
member = member ?: false
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.sw.dualscreen.model.response.v2
|
package com.sw.dualscreen.model.response.v2
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import com.sw.dualscreen.model.response.FoodItem
|
||||||
|
import com.sw.dualscreen.model.response.FoodOrderModel
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
|
|
||||||
@@ -29,3 +31,23 @@ data class SettlementFoodItem(
|
|||||||
val eatWeight: Int?,
|
val eatWeight: Int?,
|
||||||
val price: BigDecimal?
|
val price: BigDecimal?
|
||||||
) : Parcelable
|
) : 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
|
||||||
|
)
|
||||||
|
|||||||
@@ -31,86 +31,98 @@ interface ApiServiceV2 {
|
|||||||
|
|
||||||
@GET
|
@GET
|
||||||
suspend fun getDeviceConfig(
|
suspend fun getDeviceConfig(
|
||||||
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/booth/device/config"
|
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/device/config"
|
||||||
): ApiResponse<DeviceConfig?>
|
): ApiResponse<DeviceConfig?>
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
suspend fun getFacePage(
|
suspend fun getFacePage(
|
||||||
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/booth/face/page",
|
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/face/page",
|
||||||
@Body request: Map<String, Long>
|
@Body request: Map<String, Long>
|
||||||
): ApiResponse<List<FaceVO>?>
|
): ApiResponse<List<FaceVO>?>
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
suspend fun getFaceIncrement(
|
suspend fun getFaceIncrement(
|
||||||
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/booth/face/increment",
|
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/face/increment",
|
||||||
@Body request: Map<String, Long>
|
@Body request: Map<String, Long>
|
||||||
): ApiResponse<List<FaceVO>?>
|
): ApiResponse<List<FaceVO>?>
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
suspend fun getFoodByNames(
|
suspend fun getFoodByNames(
|
||||||
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/booth/food/by-names",
|
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/food/by-names",
|
||||||
@Body request: FoodSearchReq
|
@Body request: FoodSearchReq
|
||||||
): ApiResponse<List<NewFoodInfo>?>
|
): 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
|
@POST
|
||||||
suspend fun getUserCurrentFood(
|
suspend fun getUserCurrentFood(
|
||||||
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/booth/user/current-food",
|
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/user/current-food",
|
||||||
@Body request: Map<String, Long>
|
@Body request: Map<String, Long>
|
||||||
): ApiResponse<UserNutrition?>
|
): ApiResponse<UserNutrition?>
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
suspend fun placeOrder(
|
suspend fun placeOrder(
|
||||||
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/booth/order/place",
|
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/order/place",
|
||||||
@Body request: PlaceOrderRequest
|
@Body request: PlaceOrderRequest
|
||||||
): ApiResponse<String?>
|
): ApiResponse<String?>
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
suspend fun getSettlementOrders(
|
suspend fun getSettlementOrders(
|
||||||
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/booth/order/settlement",
|
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/order/settlement",
|
||||||
@Body request: Map<String, Long>
|
@Body request: Map<String, Long>
|
||||||
): ApiResponse<SettlementOrder?>
|
): ApiResponse<SettlementOrder?>
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
suspend fun getMemberInfo(
|
suspend fun getMemberInfo(
|
||||||
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/booth/member/info",
|
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/member/info",
|
||||||
@Body request: Map<String, Long>
|
@Body request: Map<String, Long>
|
||||||
): ApiResponse<NewMemberInfo?>
|
): ApiResponse<NewMemberInfo?>
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
suspend fun getMemberInfoByPhone(
|
suspend fun getMemberInfoByPhone(
|
||||||
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/booth/member/info-by-phone",
|
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/member/info-by-phone",
|
||||||
@Body request: Map<String, String>
|
@Body request: Map<String, String>
|
||||||
): ApiResponse<NewMemberInfo?>
|
): ApiResponse<NewMemberInfo?>
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
suspend fun getMemberDiscount(
|
suspend fun getMemberDiscount(
|
||||||
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/booth/member/discount",
|
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/member/discount",
|
||||||
@Body request: Map<String, Long>
|
@Body request: Map<String, Long>
|
||||||
): ApiResponse<String?>
|
): ApiResponse<String?>
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
suspend fun bindUserOrder(
|
suspend fun bindUserOrder(
|
||||||
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/booth/order/bind-user",
|
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/order/bind-user",
|
||||||
@Body request: BindUserOrderRequest
|
@Body request: BindUserOrderRequest
|
||||||
): ApiResponse<Any?>
|
): ApiResponse<Any?>
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
suspend fun getCollectPage(
|
suspend fun getCollectPage(
|
||||||
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/booth/collect/page",
|
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/collect/page",
|
||||||
@Body request: Map<String, @JvmSuppressWildcards Any>
|
@Body request: Map<String, @JvmSuppressWildcards Any>
|
||||||
): ApiResponse<List<CollectedFoodV2>?>
|
): 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
|
@Multipart
|
||||||
@POST
|
@POST
|
||||||
suspend fun uploadCollect(
|
suspend fun uploadCollect(
|
||||||
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/booth/collect/upload",
|
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/collect/upload",
|
||||||
@PartMap params: HashMap<String, RequestBody>,
|
@PartMap params: HashMap<String, RequestBody>,
|
||||||
@Part foodPics: List<MultipartBody.Part>
|
@Part foodPics: List<MultipartBody.Part>
|
||||||
): ApiResponse<List<String>?>
|
): ApiResponse<List<String>?>
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
suspend fun deleteCollect(
|
suspend fun deleteCollect(
|
||||||
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/booth/collect/delete",
|
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/collect/delete",
|
||||||
@Body request: Map<String, @JvmSuppressWildcards Any>
|
@Body request: Map<String, @JvmSuppressWildcards Any>
|
||||||
): ApiResponse<Any?>
|
): ApiResponse<Any?>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,22 @@ class RemoteRepositoryV2 constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(
|
suspend fun getUserCurrentFood(
|
||||||
userId: Long
|
userId: Long
|
||||||
): ApiResponse<UserNutrition?> {
|
): ApiResponse<UserNutrition?> {
|
||||||
@@ -162,6 +178,20 @@ class RemoteRepositoryV2 constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(
|
suspend fun uploadCollect(
|
||||||
foodId: Long,
|
foodId: Long,
|
||||||
foodName: String,
|
foodName: String,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,13 +37,13 @@ object FoodVectorTool {
|
|||||||
successBlock: () -> Unit,
|
successBlock: () -> Unit,
|
||||||
failureBlock: (String) -> Unit
|
failureBlock: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
userViewModel.getCollectPage(
|
userViewModel.getCollectVectorPage(
|
||||||
pageNum = pageNum.toLong(),
|
pageNum = pageNum.toLong(),
|
||||||
pageSize = PAGE_SIZE.toLong(),
|
pageSize = PAGE_SIZE.toLong(),
|
||||||
onSuccess = { items ->
|
onSuccess = { items ->
|
||||||
if (pageNum == 1 && items.isEmpty()) {
|
if (pageNum == 1 && items.isEmpty()) {
|
||||||
successBlock()
|
successBlock()
|
||||||
return@getCollectPage
|
return@getCollectVectorPage
|
||||||
}
|
}
|
||||||
if (items.isNotEmpty()) {
|
if (items.isNotEmpty()) {
|
||||||
saveFoodVector(lifecycleScope, items)
|
saveFoodVector(lifecycleScope, items)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.sw.dualscreen.utils
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
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
|
||||||
@@ -22,6 +23,11 @@ class PhotoCaptureHelper(
|
|||||||
private val onSuccess: (Uri) -> Unit = {},
|
private val onSuccess: (Uri) -> Unit = {},
|
||||||
private val onError: (String) -> Unit = {}
|
private val onError: (String) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
|
companion object {
|
||||||
|
/** 临时测试开关:true 时跳过拍照,直接返回固定图片Uri */
|
||||||
|
const val TEST_MODE = true
|
||||||
|
}
|
||||||
|
|
||||||
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()) {
|
||||||
@@ -43,6 +49,28 @@ class PhotoCaptureHelper(
|
|||||||
fileNamePrefix: String = "IMG_",
|
fileNamePrefix: String = "IMG_",
|
||||||
fileExtension: String = ".jpg"
|
fileExtension: String = ".jpg"
|
||||||
) {
|
) {
|
||||||
|
// TODO: 测试完成后删除此段代码,并将 TEST_MODE 置为 false
|
||||||
|
if (TEST_MODE) {
|
||||||
|
val testDir = context.externalCacheDir ?: context.cacheDir
|
||||||
|
val testFile = File(testDir, "test_photo_${System.currentTimeMillis()}.jpg")
|
||||||
|
if (!testFile.exists()) {
|
||||||
|
testFile.parentFile?.mkdirs()
|
||||||
|
val bitmap = android.graphics.Bitmap.createBitmap(480, 640, android.graphics.Bitmap.Config.ARGB_8888)
|
||||||
|
val canvas = android.graphics.Canvas(bitmap)
|
||||||
|
canvas.drawColor(0xFFCCCCCC.toInt())
|
||||||
|
testFile.outputStream().use { fos ->
|
||||||
|
bitmap.compress(android.graphics.Bitmap.CompressFormat.JPEG, 95, fos)
|
||||||
|
}
|
||||||
|
bitmap.recycle()
|
||||||
|
}
|
||||||
|
val authority = "${context.packageName}.fileprovider"
|
||||||
|
val testUri = FileProvider.getUriForFile(context, authority, testFile)
|
||||||
|
Timber.d("测试模式:跳过拍照,返回 content://Uri: $testUri")
|
||||||
|
onSuccess(testUri)
|
||||||
|
callbackList.forEach { it(testUri) }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
Timber.d("开始拍照采集")
|
Timber.d("开始拍照采集")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import com.sw.dualscreen.model.request.v2.PlaceOrderRequest
|
|||||||
import com.sw.dualscreen.model.response.ApiResponse
|
import com.sw.dualscreen.model.response.ApiResponse
|
||||||
import com.sw.dualscreen.model.response.DinnerType
|
import com.sw.dualscreen.model.response.DinnerType
|
||||||
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.response.FoodOrderModel
|
import com.sw.dualscreen.model.response.FoodOrderModel
|
||||||
import com.sw.dualscreen.model.response.FoodSearchReq
|
import com.sw.dualscreen.model.response.FoodSearchReq
|
||||||
import com.sw.dualscreen.model.response.MemberInfo
|
import com.sw.dualscreen.model.response.MemberInfo
|
||||||
@@ -19,6 +18,9 @@ import com.sw.dualscreen.model.response.v2.FaceVO
|
|||||||
import com.sw.dualscreen.model.response.v2.NewFoodInfo
|
import com.sw.dualscreen.model.response.v2.NewFoodInfo
|
||||||
import com.sw.dualscreen.model.response.v2.NewMemberInfo
|
import com.sw.dualscreen.model.response.v2.NewMemberInfo
|
||||||
import com.sw.dualscreen.model.response.v2.SettlementOrder
|
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.network.ApiClient
|
||||||
import com.sw.dualscreen.utils.FileUtil
|
import com.sw.dualscreen.utils.FileUtil
|
||||||
import com.sw.dualscreen.utils.SpTool
|
import com.sw.dualscreen.utils.SpTool
|
||||||
@@ -152,14 +154,13 @@ class NetViewModelV2 : BaseViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getFoodByNames(
|
fun getFoodByNames(
|
||||||
nameList: List<String>,
|
names: String,
|
||||||
deviceType: Int = 2,
|
|
||||||
onSuccess: (list: List<NewFoodInfo>) -> Unit,
|
onSuccess: (list: List<NewFoodInfo>) -> Unit,
|
||||||
onFailure: (message: String) -> Unit
|
onFailure: (message: String) -> Unit
|
||||||
) {
|
) {
|
||||||
launch {
|
launch {
|
||||||
try {
|
try {
|
||||||
val response = ApiClient.repositoryV2.getFoodByNames(nameList, deviceType)
|
val response = ApiClient.repositoryV2.getFoodByNames(names.split(","))
|
||||||
if (parseResponse(response)) {
|
if (parseResponse(response)) {
|
||||||
onSuccess(response.data ?: emptyList())
|
onSuccess(response.data ?: emptyList())
|
||||||
} else {
|
} else {
|
||||||
@@ -172,26 +173,6 @@ class NetViewModelV2 : BaseViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getUserCurrentFood(
|
|
||||||
userId: Long,
|
|
||||||
onSuccess: (nutrition: UserNutrition?) -> Unit,
|
|
||||||
onFailure: (message: String) -> Unit
|
|
||||||
) {
|
|
||||||
launch {
|
|
||||||
try {
|
|
||||||
val response = ApiClient.repositoryV2.getUserCurrentFood(userId)
|
|
||||||
if (parseResponse(response)) {
|
|
||||||
onSuccess(response.data)
|
|
||||||
} else {
|
|
||||||
onFailure(response.msg ?: "获取就餐营养数据失败")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "获取就餐营养数据异常")
|
|
||||||
onFailure("网络异常: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun placeOrder(
|
fun placeOrder(
|
||||||
orderRequest: PlaceOrderRequest,
|
orderRequest: PlaceOrderRequest,
|
||||||
onSuccess: (orderNo: String?) -> Unit,
|
onSuccess: (orderNo: String?) -> Unit,
|
||||||
@@ -212,87 +193,6 @@ class NetViewModelV2 : BaseViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSettlementOrders(
|
|
||||||
userId: Long,
|
|
||||||
onSuccess: (order: SettlementOrder?) -> Unit,
|
|
||||||
onFailure: (message: String) -> Unit
|
|
||||||
) {
|
|
||||||
launch {
|
|
||||||
try {
|
|
||||||
val response = ApiClient.repositoryV2.getSettlementOrders(userId)
|
|
||||||
if (parseResponse(response)) {
|
|
||||||
onSuccess(response.data)
|
|
||||||
} else {
|
|
||||||
onFailure(response.msg ?: "查询结算订单失败")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "查询结算订单异常")
|
|
||||||
onFailure("网络异常: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getMemberInfo(
|
|
||||||
userId: Long,
|
|
||||||
onSuccess: (memberInfo: NewMemberInfo?) -> Unit,
|
|
||||||
onFailure: (message: String) -> Unit
|
|
||||||
) {
|
|
||||||
launch {
|
|
||||||
try {
|
|
||||||
val response = ApiClient.repositoryV2.getMemberInfo(userId)
|
|
||||||
if (parseResponse(response)) {
|
|
||||||
onSuccess(response.data)
|
|
||||||
} else {
|
|
||||||
onFailure(response.msg ?: "获取会员信息失败")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "获取会员信息异常")
|
|
||||||
onFailure("网络异常: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getMemberInfoByPhoneV2(
|
|
||||||
phone: String,
|
|
||||||
password: String = "",
|
|
||||||
onSuccess: (memberInfo: NewMemberInfo?) -> Unit,
|
|
||||||
onFailure: (message: String) -> Unit
|
|
||||||
) {
|
|
||||||
launch {
|
|
||||||
try {
|
|
||||||
val response = ApiClient.repositoryV2.getMemberInfoByPhone(phone, password)
|
|
||||||
if (parseResponse(response)) {
|
|
||||||
onSuccess(response.data)
|
|
||||||
} else {
|
|
||||||
onFailure(response.msg ?: "查询会员失败")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "查询会员异常")
|
|
||||||
onFailure("网络异常: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getMemberDiscount(
|
|
||||||
userId: Long,
|
|
||||||
onSuccess: (discount: String?) -> Unit,
|
|
||||||
onFailure: (message: String) -> Unit
|
|
||||||
) {
|
|
||||||
launch {
|
|
||||||
try {
|
|
||||||
val response = ApiClient.repositoryV2.getMemberDiscount(userId)
|
|
||||||
if (parseResponse(response)) {
|
|
||||||
onSuccess(response.data)
|
|
||||||
} else {
|
|
||||||
onFailure(response.msg ?: "查询会员折扣失败")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "查询会员折扣异常")
|
|
||||||
onFailure("网络异常: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun bindUserOrder(
|
fun bindUserOrder(
|
||||||
userId: Long,
|
userId: Long,
|
||||||
orderNo: String,
|
orderNo: String,
|
||||||
@@ -337,6 +237,27 @@ class NetViewModelV2 : BaseViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,直接返回 picUrls 列表) */
|
||||||
suspend fun uploadCollect(
|
suspend fun uploadCollect(
|
||||||
foodId: Long,
|
foodId: Long,
|
||||||
@@ -517,38 +438,22 @@ class NetViewModelV2 : BaseViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 提交就餐营养数据 */
|
|
||||||
fun postUserNutritionData(
|
|
||||||
param: List<UserNutritionParam>,
|
|
||||||
onSuccess: () -> Unit = {},
|
|
||||||
onFailure: (message: String) -> Unit = {}
|
|
||||||
) {
|
|
||||||
launch {
|
|
||||||
try {
|
|
||||||
val response = repository.postUserNutritionData(param)
|
|
||||||
if (parseResponse(response)) {
|
|
||||||
onSuccess()
|
|
||||||
} else {
|
|
||||||
onFailure(response.msg ?: "提交营养数据失败")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "提交营养数据异常")
|
|
||||||
onFailure("网络异常: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== V1 兼容包装方法(暂用 V1 API,后续逐步迁移至 V2) ==========
|
// ========== V1 兼容包装方法(暂用 V1 API,后续逐步迁移至 V2) ==========
|
||||||
|
|
||||||
private var lastFaceTimestamp = 0L
|
private var lastFaceTimestamp = 0L
|
||||||
|
|
||||||
/** 搜索食物 */
|
/** 搜索食物 */
|
||||||
fun searchByFoodName(foodName: String, action: (List<FoodInfo>) -> Unit = {}) {
|
fun searchByFoodName(foodName: String, action: (List<FoodInfo>) -> Unit = {}) {
|
||||||
Timber.tag(TAG).d("searchByFoodName foodName = $foodName")
|
Timber.tag(TAG).d("searchFood foodName = $foodName")
|
||||||
launchWithLoading {
|
launchWithLoading {
|
||||||
val response = repository.getRestInfoFoodsByType(foodName = foodName)
|
try {
|
||||||
|
val response = ApiClient.repositoryV2.searchFood(name = foodName)
|
||||||
if (parseResponse(response)) {
|
if (parseResponse(response)) {
|
||||||
action(response.data ?: emptyList())
|
val list = (response.data ?: emptyList()).map { it.toFoodInfo() }
|
||||||
|
action(list)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "搜索菜品异常")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -559,104 +464,106 @@ class NetViewModelV2 : BaseViewModel() {
|
|||||||
block: (UserNutrition?) -> Unit
|
block: (UserNutrition?) -> Unit
|
||||||
) {
|
) {
|
||||||
Timber.tag(TAG).d("getUserNutritionData userId = $userId")
|
Timber.tag(TAG).d("getUserNutritionData userId = $userId")
|
||||||
|
val uid = userId.toLongOrNull() ?: run {
|
||||||
|
block(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
launch {
|
launch {
|
||||||
val response = repository.getUserNutritionData(userId = userId)
|
try {
|
||||||
|
val response = ApiClient.repositoryV2.getUserCurrentFood(uid)
|
||||||
if (parseResponse(response)) {
|
if (parseResponse(response)) {
|
||||||
block(response.data)
|
block(response.data)
|
||||||
} else {
|
} else {
|
||||||
block(null)
|
block(null)
|
||||||
}
|
}
|
||||||
}
|
} catch (e: Exception) {
|
||||||
}
|
Timber.e(e, "获取就餐营养数据异常")
|
||||||
|
block(null)
|
||||||
/** 搜索菜品信息 */
|
|
||||||
fun getFoodInfo(foodName: String, action: (List<FoodInfo>) -> Unit = {}) {
|
|
||||||
Timber.tag(TAG).d("getFoodInfo")
|
|
||||||
launchWithLoading {
|
|
||||||
val response = repository.getFoodInfo(foodName = foodName)
|
|
||||||
if (parseResponse(response)) {
|
|
||||||
action(response.data ?: emptyList())
|
|
||||||
} else {
|
|
||||||
action(emptyList())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 创建订单 */
|
|
||||||
fun createOrder(order: FoodOrder, block: (String) -> Unit) {
|
|
||||||
Timber.tag(TAG).d("createOrder")
|
|
||||||
launchWithLoading {
|
|
||||||
val response = repository.createOrder(order)
|
|
||||||
if (parseResponse(response)) {
|
|
||||||
block(response.data.toString())
|
|
||||||
} else {
|
|
||||||
block("")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取订单列表 */
|
/** 获取订单列表 */
|
||||||
fun getFoodOrderList(userId: String, block: (FoodOrderModel?) -> Unit) {
|
fun getFoodOrderList(userId: String, block: (FoodOrderModel?) -> Unit) {
|
||||||
Timber.tag(TAG).d("getFoodOrderList")
|
Timber.tag(TAG).d("getFoodOrderList userId = $userId")
|
||||||
|
val uid = userId.toLongOrNull() ?: run {
|
||||||
|
block(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
launchWithLoading {
|
launchWithLoading {
|
||||||
val response = repository.getFoodOrderList(userId)
|
try {
|
||||||
|
val response = ApiClient.repositoryV2.getSettlementOrders(uid)
|
||||||
if (parseResponse(response)) {
|
if (parseResponse(response)) {
|
||||||
block(response.data)
|
block(response.data?.toFoodOrderModel())
|
||||||
} else {
|
} else {
|
||||||
block(null)
|
block(null)
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "查询订单列表异常")
|
||||||
|
block(null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 根据 ID 查询会员信息 */
|
/** 根据 ID 查询会员信息 */
|
||||||
fun getMemberInfoById(memberId: String, block: (MemberInfo?) -> Unit) {
|
fun getMemberInfoById(memberId: String, block: (MemberInfo?) -> Unit) {
|
||||||
Timber.tag(TAG).d("getMemberInfo")
|
Timber.tag(TAG).d("getMemberInfo memberId=$memberId")
|
||||||
|
val uid = memberId.toLongOrNull() ?: run {
|
||||||
|
block(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
launchWithLoading {
|
launchWithLoading {
|
||||||
val response = repository.getMemberInfoById(memberId)
|
try {
|
||||||
|
val response = ApiClient.repositoryV2.getMemberInfo(uid)
|
||||||
if (parseResponse(response)) {
|
if (parseResponse(response)) {
|
||||||
block(response.data)
|
block(response.data?.toMemberInfo())
|
||||||
} else {
|
} else {
|
||||||
block(null)
|
block(null)
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "查询会员信息异常")
|
||||||
|
block(null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 根据手机号查询会员信息 */
|
/** 根据手机号查询会员信息 */
|
||||||
fun getMemberInfoByPhone(phone: String, key: String, block: (MemberInfo?) -> Unit) {
|
fun getMemberInfoByPhone(phone: String, key: String, block: (MemberInfo?) -> Unit) {
|
||||||
Timber.tag(TAG).d("getMemberInfoByPhone")
|
Timber.tag(TAG).d("getMemberInfoByPhone phone=$phone")
|
||||||
launchWithLoading {
|
launchWithLoading {
|
||||||
val response = repository.getMemberInfoByPhone(phone, key)
|
try {
|
||||||
|
val response = ApiClient.repositoryV2.getMemberInfoByPhone(phone, key)
|
||||||
if (parseResponse(response)) {
|
if (parseResponse(response)) {
|
||||||
block(response.data)
|
block(response.data?.toMemberInfo())
|
||||||
} else {
|
} else {
|
||||||
block(null)
|
block(null)
|
||||||
}
|
}
|
||||||
}
|
} catch (e: Exception) {
|
||||||
}
|
Timber.e(e, "查询会员异常")
|
||||||
|
block(null)
|
||||||
/** 绑定订单 */
|
|
||||||
fun bindOrder(userId: String, orderId: String, mode: Int, block: (Boolean) -> Unit) {
|
|
||||||
Timber.tag(TAG).d("bindOrder")
|
|
||||||
launchWithLoading {
|
|
||||||
val response = repository.bindOrder(userId = userId, orderId = orderId, mode = mode)
|
|
||||||
if (parseResponse(response)) {
|
|
||||||
block(true)
|
|
||||||
} else {
|
|
||||||
block(false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取会员折扣 */
|
/** 获取会员折扣 */
|
||||||
fun getMemberDiscount(userId: String, block: (Double?) -> Unit) {
|
fun getMemberDiscount(userId: String, block: (Double?) -> Unit) {
|
||||||
Timber.tag(TAG).d("getMemberDiscount")
|
Timber.tag(TAG).d("getMemberDiscount userId = $userId")
|
||||||
|
val uid = userId.toLongOrNull() ?: run {
|
||||||
|
block(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
launchWithLoading {
|
launchWithLoading {
|
||||||
val response = repository.getMemberDiscount(userId)
|
try {
|
||||||
|
val response = ApiClient.repositoryV2.getMemberDiscount(uid)
|
||||||
if (parseResponse(response)) {
|
if (parseResponse(response)) {
|
||||||
block(response.data)
|
block(response.data?.toDoubleOrNull())
|
||||||
} else {
|
} else {
|
||||||
block(null)
|
block(null)
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "查询会员折扣异常")
|
||||||
|
block(null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user