feat(payment): 添加会员折扣功能并重构支付逻辑

- 新增 getMemberDiscount 接口用于查询会员折扣
- 在人脸识别支付中集成会员折扣计算逻辑
- 将支付金额计算从 vipPrice 切换到 specPrice 并应用会员折扣
- 在初始化活动中添加设备配置获取功能
- 使用 ConstraintLayout 重构主餐品列表布局
- 添加 Timber 日志框架替换原有 Log 工具
- 重构 MainScreenPresentation 中的食物价格计算逻辑
- 移除重复的设备配置获取代码,统一在 InitActivity 中处理
This commit is contained in:
2026-04-27 17:52:45 +08:00
parent 416a8366e1
commit 8f1f15dd74
10 changed files with 113 additions and 55 deletions
@@ -17,9 +17,11 @@ import com.sw.dualscreen.objbox.FoodModule
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
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.UserViewModel
import com.sw.plate.utils.AppUtil import com.sw.plate.utils.AppUtil
import com.sw.plate.utils.ToastUtils
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class InitActivity : BaseActivity<ActivityInitBinding>() { class InitActivity : BaseActivity<ActivityInitBinding>() {
@@ -180,11 +182,32 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
loadNetworkErrorDialog() loadNetworkErrorDialog()
return return
} }
getDeviceConfig()
getCollectedFoodVector() getCollectedFoodVector()
} }
} }
} }
private fun getDeviceConfig() {
userViewModel.getDeviceConfig { deviceConfig ->
runOnUiThread {
if (deviceConfig == null) {
ToastUtils.showToast("获取设备配置数据失败")
return@runOnUiThread
}
var settlementMode = 1
//结算模式:1-独立支付,2-联合结算
//settlementMode = if (deviceConfig.payType == 2) 0 else 1
// TODO: 2022/10/13 修改为独立支付
SpTool.settlementMode = settlementMode
GlobalData.appId = deviceConfig.arcsoftAppId ?: ""
GlobalData.sdkKey = deviceConfig.arcsoftSdkKey ?: ""
GlobalData.activeKey = deviceConfig.arcsoftActiveKey ?: ""
}
}
}
private fun loadNetworkErrorDialog() { private fun loadNetworkErrorDialog() {
RemindDialog( RemindDialog(
context = context, context = context,
@@ -86,6 +86,7 @@ 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
import timber.log.Timber
import java.io.File import java.io.File
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.ZoneId import java.time.ZoneId
@@ -200,6 +201,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
override fun initialize() { override fun initialize() {
super.initialize() super.initialize()
settlementMode = SpTool.settlementMode
isPageVisible = true isPageVisible = true
// 拦截扫码枪按键事件,防止触发页面按钮点击 // 拦截扫码枪按键事件,防止触发页面按钮点击
registerKeyEvent() registerKeyEvent()
@@ -212,9 +214,10 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
initData() initData()
} }
private fun initLayoutEatDetailBinding(mode: Int) { private fun initLayoutEatDetailBinding() {
Timber.tag(TAG).d("initLayoutEatDetailBinding,settlementMode=$settlementMode")
binding.flFoodList.removeAllViews() binding.flFoodList.removeAllViews()
if (mode == 0) { if (settlementMode == 0) {
//联合支付 //联合支付
val b = LayoutMainFoodList2Binding.inflate(layoutInflater, binding.flFoodList, true) val b = LayoutMainFoodList2Binding.inflate(layoutInflater, binding.flFoodList, true)
rvFoodList = b.rvFoodList rvFoodList = b.rvFoodList
@@ -255,25 +258,8 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
} }
private fun initView() { private fun initView() {
initLayoutEatDetailBinding(SpTool.settlementMode)
viewModel.getDeviceConfig { deviceConfig ->
runOnUiThread {
if (deviceConfig == null) {
ToastUtils.showToast("获取设备配置数据失败")
initLayoutEatDetailBinding(SpTool.settlementMode)
return@runOnUiThread
}
//结算模式:1-独立支付,2-联合结算
settlementMode = if (deviceConfig.payType == 2) 0 else 1
SpTool.settlementMode = settlementMode
GlobalData.appId = deviceConfig.arcsoftAppId ?: ""
GlobalData.sdkKey = deviceConfig.arcsoftSdkKey ?: ""
GlobalData.activeKey = deviceConfig.arcsoftActiveKey ?: ""
viewModel.activeEngine() viewModel.activeEngine()
initLayoutEatDetailBinding(settlementMode) initLayoutEatDetailBinding()
}
}
updateDateTime() updateDateTime()
binding.tvTitleTime.setOnClickListener { binding.tvTitleTime.setOnClickListener {
EnvSwitchDialog(context = this, onEnvChanged = { EnvSwitchDialog(context = this, onEnvChanged = {
@@ -166,6 +166,8 @@ class PayActivity : BaseActivity<ActivityPayBinding>() {
super.onDestroy() super.onDestroy()
} }
var memberDiscount = 1.0
var memberInfo: MemberInfo? = null var memberInfo: MemberInfo? = null
fun showPayInfo(type: Int = 1, isVip: Boolean = false, memberInfo: MemberInfo? = null) { fun showPayInfo(type: Int = 1, isVip: Boolean = false, memberInfo: MemberInfo? = null) {
this.memberInfo = memberInfo this.memberInfo = memberInfo
@@ -191,10 +193,20 @@ class PayActivity : BaseActivity<ActivityPayBinding>() {
it.type = type it.type = type
it.foodName = foodInfo?.foodName it.foodName = foodInfo?.foodName
//确认使用vipPrice 还是 specPrice //确认使用vipPrice 还是 specPrice
val onePrice = if (isVip) foodInfo?.vipPrice else foodInfo?.specPrice //val onePrice = if (isVip) foodInfo?.vipPrice else foodInfo?.specPrice
it.totalPrice = (onePrice ?: 0.0) * eatNum if (isVip) {
userViewModel.getMemberDiscount(memberInfo?.faceUserId?:"") { discount ->
memberDiscount = if (discount == null) 1.0 else discount / 10
var onePrice = foodInfo?.specPrice ?: 0.0
onePrice = if (isVip) onePrice * memberDiscount else onePrice
it.totalPrice = onePrice * eatNum
presentation.initView() presentation.initView()
} }
} else {
it.totalPrice = (foodInfo?.specPrice ?: 0.0) * eatNum
presentation.initView()
}
}
} }
fun updateQrCodeImage(qrCodeUrl: String?) { fun updateQrCodeImage(qrCodeUrl: String?) {
@@ -23,7 +23,9 @@ class FacePayFragment : BaseFragment<FragmentFacePayBinding>() {
// } // }
payActivity.foodInfo?.let { payActivity.foodInfo?.let {
foodName = it.foodName foodName = it.foodName
payAmount = (it.vipPrice ?: 0.0) * payActivity.eatNum // payAmount = (it.vipPrice ?: 0.0) * payActivity.eatNum
val price = it.specPrice ?: 0.0
payAmount = price * payActivity.memberDiscount * payActivity.eatNum
} }
showSubScreen() showSubScreen()
} }
@@ -75,7 +75,9 @@ class PayResultFragment : BaseFragment<FragmentPayResultBinding>() {
} }
if (memberInfo != null && memberInfo!!.member) { if (memberInfo != null && memberInfo!!.member) {
totalPrice = (payActivity.foodInfo?.vipPrice ?: 0.0) * payActivity.eatNum // totalPrice = (payActivity.foodInfo?.vipPrice ?: 0.0) * payActivity.eatNum
val price = payActivity.foodInfo?.specPrice ?: 0.0
totalPrice = price * payActivity.memberDiscount * payActivity.eatNum
binding.layoutVip.visible() binding.layoutVip.visible()
balance = (memberInfo!!.topUpBalance ?: 0.0) + (memberInfo!!.rewardBalance ?: 0.0) balance = (memberInfo!!.topUpBalance ?: 0.0) + (memberInfo!!.rewardBalance ?: 0.0)
realPayPrice = if (balance >= totalPrice) 0.0 else totalPrice - balance realPayPrice = if (balance >= totalPrice) 0.0 else totalPrice - balance
@@ -321,4 +321,13 @@ interface ApiService {
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/settlement/terminal/app/getFoodInfoBySettlement", @Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/settlement/terminal/app/getFoodInfoBySettlement",
@Query("userId") userId: String @Query("userId") userId: String
): ApiResponse<FoodOrderModel?> ): ApiResponse<FoodOrderModel?>
/**
* 查询会员折扣
*/
@GET
suspend fun getMemberDiscount(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/getMemberDiscount",
@Query("userId") userId: String
): ApiResponse<Double?>
} }
@@ -69,6 +69,7 @@ import kotlin.math.abs
import kotlin.math.max import kotlin.math.max
import kotlin.math.roundToInt import kotlin.math.roundToInt
@SuppressLint("NotifyDataSetChanged", "SetTextI18n")
class MainScreenPresentation( class MainScreenPresentation(
val activity: MainActivity, val activity: MainActivity,
display: Display, display: Display,
@@ -338,6 +339,7 @@ class MainScreenPresentation(
} }
private val step3Debouncer = Debouncer(10*1000) private val step3Debouncer = Debouncer(10*1000)
private var memberDiscount:Double = 1.0
/** /**
* 根据接口数据更新热量、营养数据 * 根据接口数据更新热量、营养数据
@@ -464,7 +466,6 @@ class MainScreenPresentation(
/** /**
* 餐品识别成功,不计费模式 * 餐品识别成功,不计费模式
*/ */
@SuppressLint("SetTextI18n")
fun loadUnbilledMode() { fun loadUnbilledMode() {
if (currentUserId.isNullOrBlank()) { if (currentUserId.isNullOrBlank()) {
return return
@@ -685,7 +686,7 @@ class MainScreenPresentation(
private var lastAddWeight = 0 private var lastAddWeight = 0
private fun addFoodToOrder(weight: Int, foodInfo: FoodInfo) { private fun addFoodToOrder(weight: Int, foodInfo: FoodInfo) {
Log.d(TAG, "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 <= 10) {
@@ -701,9 +702,10 @@ class MainScreenPresentation(
} }
return return
} }
val foodPrice = if (isMember) foodInfo.vipPrice else foodInfo.specPrice //val foodPrice = if (isMember) foodInfo.vipPrice else foodInfo.specPrice
val foodPrice = (foodInfo.specPrice ?: 0.0) * memberDiscount
val eatNum = activity.getEatNum(weight.toDouble(), foodInfo.specWeight) val eatNum = activity.getEatNum(weight.toDouble(), foodInfo.specWeight)
val price = eatNum * (foodPrice ?: 0.0) 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) {
@@ -732,8 +734,7 @@ class MainScreenPresentation(
) )
// foodOrderAdapter.submitList(foodOrderList) // foodOrderAdapter.submitList(foodOrderList)
foodOrderAdapter.notifyDataSetChanged() foodOrderAdapter.notifyDataSetChanged()
Log.d( Timber.tag(TAG) .d(
TAG,
"addFoodToOrder: size=${foodOrderList.size},新增数据:${foodOrderList.last()}" "addFoodToOrder: size=${foodOrderList.size},新增数据:${foodOrderList.last()}"
) )
//foodOrderAdapter.notifyDataSetChanged() //foodOrderAdapter.notifyDataSetChanged()
@@ -742,7 +743,7 @@ class MainScreenPresentation(
eatWeightSum += it.num eatWeightSum += it.num
} }
binding.detailInclude.tvTotalWeight.text = "总重量:${eatWeightSum}" binding.detailInclude.tvTotalWeight.text = "总重量:${eatWeightSum}"
Log.d(TAG, "addFoodToOrder: 更新总重量,新增数据------------------------------") Timber.tag(TAG) .d("addFoodToOrder: 更新总重量,新增数据------------------------------")
} }
private fun calculateNutrition(value: Int) { private fun calculateNutrition(value: Int) {
@@ -1031,7 +1032,11 @@ class MainScreenPresentation(
} }
} }
// pauseCamera() // pauseCamera()
//先查询会员折扣
userViewModel.getMemberDiscount(userId) { discount ->
memberDiscount = if (discount == null) 1.0 else discount / 10
step3ShowRecognizeResult(userId) step3ShowRecognizeResult(userId)
}
}) })
recognizeViewModel.drawRectInfoText.observe(activity, Observer { info -> recognizeViewModel.drawRectInfoText.observe(activity, Observer { info ->
@@ -1471,7 +1476,6 @@ class MainScreenPresentation(
/** /**
* 加载订单列表 * 加载订单列表
*/ */
@SuppressLint("NotifyDataSetChanged", "SetTextI18n")
private fun loadOrderList(userId: String, callback: () -> Unit) { private fun loadOrderList(userId: String, callback: () -> Unit) {
binding.llTopInfo.gone() binding.llTopInfo.gone()
binding.flFoodDetail.visible() binding.flFoodDetail.visible()
@@ -1489,7 +1493,7 @@ class MainScreenPresentation(
//foodOrderAdapter.submitList(foodOrderList) //foodOrderAdapter.submitList(foodOrderList)
foodOrderAdapter.notifyDataSetChanged() foodOrderAdapter.notifyDataSetChanged()
//binding.detailInclude.tvTotalWeight.text = "总重量:${model.eatWeightSum}克" //binding.detailInclude.tvTotalWeight.text = "总重量:${model.eatWeightSum}克"
Log.d(TAG, "addFoodToOrder: 更新总重量,订单列表------------------------------") Timber.tag(TAG).d("addFoodToOrder: 更新总重量,订单列表------------------------------")
// binding.detailInclude.tvTotalCalorie.text = "总热量:${model.calorie.roundedDecimalPlace(1)}千卡" // binding.detailInclude.tvTotalCalorie.text = "总热量:${model.calorie.roundedDecimalPlace(1)}千卡"
jointPaymentQueryFinish = true jointPaymentQueryFinish = true
@@ -359,4 +359,10 @@ class RemoteRepository constructor(
apiService.getFoodOrderList(userId = userId) apiService.getFoodOrderList(userId = userId)
} }
} }
suspend fun getMemberDiscount(userId: String): ApiResponse<Double?> {
return safeApiCall {
apiService.getMemberDiscount(userId = userId)
}
}
} }
@@ -651,4 +651,16 @@ class UserViewModel : BaseViewModel() {
} }
} }
} }
fun getMemberDiscount(userId: String, block: (Double?) -> Unit) {
Timber.tag(TAG).d("getMemberDiscount")
launchWithLoading {
val response = repository.getMemberDiscount(userId)
if (parseResponse(response)) {
block(response.data)
} else {
block(null)
}
}
}
} }
@@ -1,42 +1,44 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto">
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvFoodList"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginHorizontal="14dp"
android:layout_marginTop="42dp"
android:layout_weight="1"
android:overScrollMode="never"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="2"
tools:itemCount="8"
tools:listitem="@layout/list_item_search_food" />
<TextView <TextView
android:id="@+id/tvToSearch" android:id="@+id/tvToSearch"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="38dp" android:layout_marginTop="38dp"
android:layout_gravity="center_horizontal" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:padding="10dp" android:padding="10dp"
tools:text="以上都不是,手动搜索" tools:text="以上都不是,手动搜索"
android:textColor="#FFFF3232" android:textColor="#FFFF3232"
android:textSize="31sp" android:textSize="31sp"
android:textStyle="bold"/> android:textStyle="bold"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvFoodList"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/tvToSearch"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginHorizontal="14dp"
android:layout_marginTop="42dp"
android:overScrollMode="never"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="2"
tools:itemCount="8"
tools:listitem="@layout/list_item_search_food" />
<include <include
android:id="@+id/detailInclude" android:id="@+id/detailInclude"
layout="@layout/layout_eat_detail" layout="@layout/layout_eat_detail"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="0dp"
android:layout_marginHorizontal="14dp"
android:visibility="gone"/> android:visibility="gone"/>
</LinearLayout> </androidx.constraintlayout.widget.ConstraintLayout>