refactor(api): 优化餐盘柜接口路径和数据模型
- 删除密码登录相关代码和布局资源,简化登录流程 - 修改 ApiService 接口路径由旧项目旧路径切换为新 nutrition 模块路径 - 修改设备用户信息 EquipmentUserInfo 中 id 类型由 Long 改为 String,防止精度丢失 - 重构 RemoteRepository 中绑定和解绑接口,适配新参数结构 - 优化 UserViewModel 中获取用户信息接口,添加静默请求支持 - 改进 DiagnosticExporter,优先导出到 U 盘,不可用时回落到应用目录 - UsbStorageHelper 增强 U 盘识别算法,结合路径与文件系统类型双重校验 - 移除无用的 LoginParam 请求模型以及密码登录相关引用 - 调整网络请求相关的导入语句,清理多余依赖 - 修正设备初始化 ActiveKey 的使用方式,恢复为后台下发值 - BindPlateFragment 中绑定失败提示改为“绑定成功,开柜失败” - OpsActivity 导出诊断包按钮弹窗改为 AlertDialog 显示结果信息 - SearchParam 默认分页参数改为 pageNum=1,pageSize=10 - 修改 SettingViewModel.unbindPlate 参数类型为 String,统一接口调用参数格式
This commit is contained in:
@@ -47,10 +47,6 @@
|
||||
android:exported="false"
|
||||
android:launchMode="singleTask">
|
||||
</activity>
|
||||
<activity
|
||||
android:name="com.sw.platecabinet.activity.LoginByPwdActivity"
|
||||
android:exported="false"
|
||||
android:launchMode="singleTask"/>
|
||||
<activity
|
||||
android:name="com.sw.platecabinet.activity.LoginByFaceActivity"
|
||||
android:exported="true"
|
||||
|
||||
@@ -31,6 +31,7 @@ class MyApp : App() {
|
||||
private fun initGlobalData() {
|
||||
var deviceId = AppUtil.getUDID(this)
|
||||
// deviceId = "be154831-3466-3ba2-a2ea-57652c919fed"
|
||||
// deviceId="2987f0c5-5754-33e9-b00a-251db5e2e55f"
|
||||
Log.d("MyApp", "initialize: deviceId=$deviceId")
|
||||
GlobalData.deviceId = deviceId
|
||||
|
||||
|
||||
@@ -50,8 +50,8 @@ class DeviceInitActivity : BaseActivity<ActivityDeviceInitBinding>() {
|
||||
GlobalData.appId = deviceConfig.arcsoftAppId ?: ""
|
||||
GlobalData.sdkKey = deviceConfig.arcsoftSdkKey ?: ""
|
||||
// 测试设备后台下发的激活码不正确,手动硬编码覆盖为正确值(后台修复后可回退为下行)
|
||||
// GlobalData.activeKey = deviceConfig.arcsoftActiveKey ?: ""
|
||||
GlobalData.activeKey = "085F-118G-Q4V1-THBP"
|
||||
GlobalData.activeKey = deviceConfig.arcsoftActiveKey ?: ""
|
||||
// GlobalData.activeKey = "085F-118G-Q4V1-THBP"
|
||||
hideWaitingDialog()
|
||||
goLoginActivity()
|
||||
}
|
||||
|
||||
@@ -111,11 +111,6 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
|
||||
//开启人脸增量数据定时任务(V2)
|
||||
startFaceTask()
|
||||
|
||||
binding.llToPwd.setOnClickListener {
|
||||
val intent = Intent(this, LoginByPwdActivity::class.java)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
// TODO: 测试请求
|
||||
TaskManager.startTask()
|
||||
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
package com.sw.platecabinet.activity
|
||||
|
||||
import android.content.Intent
|
||||
import com.sw.plate.utils.ToastUtils
|
||||
import com.sw.platecabinet.member.databinding.ActivityLoginByPwdBinding
|
||||
import com.sw.platecabinet.member.databinding.ItemTitleTimeBinding
|
||||
import com.sw.platecabinet.dialog.BalanceNotEnoughDialog
|
||||
import com.sw.platecabinet.model.request.LoginParam
|
||||
import com.sw.platecabinet.model.response.EquipmentUserInfo
|
||||
import com.sw.platecabinet.utils.KeyboardUtils
|
||||
|
||||
/**
|
||||
* 密码登录
|
||||
*/
|
||||
class LoginByPwdActivity : BaseActivity<ActivityLoginByPwdBinding>() {
|
||||
|
||||
override fun inflateViewBinding(): ActivityLoginByPwdBinding {
|
||||
return ActivityLoginByPwdBinding.inflate(layoutInflater)
|
||||
}
|
||||
|
||||
override fun inflateTitleBinding(): ItemTitleTimeBinding {
|
||||
return binding.includeHeader
|
||||
}
|
||||
|
||||
override fun initialize() {
|
||||
binding.btnLogin.setOnClickListener {
|
||||
val phone = binding.etPhone.text.toString()
|
||||
val pwd = binding.etPwd.text.toString()
|
||||
if (phone.isEmpty() || pwd.isEmpty()) {
|
||||
ToastUtils.showToast("手机或校验码不能为空")
|
||||
return@setOnClickListener
|
||||
}
|
||||
val loginParam = LoginParam(
|
||||
//equipmentId = equipmentId,
|
||||
phone = phone,
|
||||
password = pwd
|
||||
)
|
||||
viewModel.loginWithPwd(loginParam)
|
||||
KeyboardUtils.hideKeyboard(this)
|
||||
}
|
||||
binding.tvFaceRec.setOnClickListener {
|
||||
val intent = Intent(this, LoginByFaceActivity::class.java)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.resetUserInfo()
|
||||
}
|
||||
|
||||
override fun handleLoginSuccess(equipmentUserInfo: EquipmentUserInfo, isAdmin: Boolean) {
|
||||
super.handleLoginSuccess(equipmentUserInfo, isAdmin)
|
||||
// val cardBalance = equipmentUserInfo.cardBalance?:0.toDouble()
|
||||
// val balanceIsNotEnough = cardBalance <= 0.toDouble()
|
||||
if (equipmentUserInfo.isIntercept) {
|
||||
return
|
||||
}
|
||||
LoginByFaceActivity.goInitActivity()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
@@ -283,8 +283,14 @@ class OpsActivity : BaseActivity<ActivityOpsBinding>() {
|
||||
|
||||
binding.btnExport.setOnClickListener {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val result = DiagnosticExporter.exportToUsb(this@OpsActivity)
|
||||
withContext(Dispatchers.Main) { ToastUtils.showToast(result.message) }
|
||||
val result = DiagnosticExporter.export(this@OpsActivity)
|
||||
withContext(Dispatchers.Main) {
|
||||
AlertDialog.Builder(this@OpsActivity)
|
||||
.setTitle(if (result.success) "导出成功" else "导出失败")
|
||||
.setMessage(result.message)
|
||||
.setPositiveButton("确定", null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -262,7 +262,7 @@ class BindPlateFragment : BaseFragment<FragmentBindPlateBinding>(
|
||||
//TODO 调用接口查询用户是否已绑盘
|
||||
this@BindPlateFragment.equipmentBoxCode = null
|
||||
(activity as? MainActivity)?.showWaitingDialog("查询中,请稍后……")
|
||||
userViewModel.getUserInfoById(memberId = item.faceId) {
|
||||
userViewModel.getUserInfoById(memberId = item.faceId, silent = true) {
|
||||
(activity as? MainActivity)?.hideWaitingDialog()
|
||||
this@BindPlateFragment.equipmentBoxCode = it?.equipmentBoxCode
|
||||
}
|
||||
@@ -306,7 +306,10 @@ class BindPlateFragment : BaseFragment<FragmentBindPlateBinding>(
|
||||
}
|
||||
|
||||
override fun onFail(e: Exception?) {
|
||||
ToastUtils.showToast("绑定失败")
|
||||
// 走到这里说明 plateBinding 接口已成功(code=00000),
|
||||
// 只是开柜动作失败,不能误报「绑定失败」
|
||||
ToastUtils.showToast("绑定成功,开柜失败")
|
||||
activity?.finish()
|
||||
}
|
||||
})
|
||||
// }
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
package com.sw.platecabinet.model.request
|
||||
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* 登录参数
|
||||
*/
|
||||
@Parcelize
|
||||
data class LoginParam(
|
||||
/**
|
||||
* 设备Id
|
||||
*/
|
||||
val equipmentId: Int? = null,
|
||||
/**
|
||||
* 会员信息
|
||||
*/
|
||||
// val memberId: String? = null,
|
||||
val faceId: String? = null,
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
val password: String? = null,
|
||||
/**
|
||||
* 手机号
|
||||
*/
|
||||
val phone: String? = null
|
||||
) : Parcelable
|
||||
@@ -26,9 +26,9 @@ data class SearchParam(
|
||||
// @SerializedName("memberFrom")
|
||||
// val memberFrom: Int? = 0,
|
||||
// @SerializedName("pageNum")
|
||||
var pageNum: Int? = 0,
|
||||
var pageNum: Int? = 1,
|
||||
// @SerializedName("pageSize")
|
||||
var pageSize: Int? = 0,
|
||||
var pageSize: Int? = 10,
|
||||
// @SerializedName("param")
|
||||
// val `param`: String? = "",
|
||||
var name:String?=null,
|
||||
|
||||
@@ -5,6 +5,7 @@ data class ApiResponse<T>(
|
||||
// val success: Boolean? = false,
|
||||
val msg: String? = "",
|
||||
val data: T? = null,
|
||||
val total: Int = 0,
|
||||
// val result: T? = null,
|
||||
) {
|
||||
// fun isSuccess(): Boolean = code == 200
|
||||
|
||||
@@ -12,9 +12,9 @@ import kotlinx.parcelize.Parcelize
|
||||
@Parcelize
|
||||
data class EquipmentUserInfo(
|
||||
/**
|
||||
* 主键
|
||||
* 主键(绑定记录id,后端序列化为字符串)
|
||||
*/
|
||||
val id: Long? = 0,
|
||||
val id: String? = "",
|
||||
/**
|
||||
*
|
||||
* 设备Id
|
||||
|
||||
@@ -4,7 +4,6 @@ import com.sw.platecabinet.GlobalData
|
||||
import com.sw.platecabinet.model.DeviceConfig
|
||||
import com.sw.platecabinet.model.request.BindParam
|
||||
import com.sw.platecabinet.model.request.EquipmentParam
|
||||
import com.sw.platecabinet.model.request.LoginParam
|
||||
import com.sw.platecabinet.model.request.SearchParam
|
||||
import com.sw.platecabinet.model.response.ApiResponse
|
||||
import com.sw.platecabinet.model.response.EquipmentInfo
|
||||
@@ -12,8 +11,6 @@ import com.sw.platecabinet.model.response.EquipmentUserInfo
|
||||
import com.sw.platecabinet.model.response.SearchResult
|
||||
import com.sw.platecabinet.model.response.UserFaceModel
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.Field
|
||||
import retrofit2.http.FormUrlEncoded
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.POST
|
||||
@@ -75,13 +72,13 @@ interface ApiService {
|
||||
// @Body param: LoginParam
|
||||
// ): ApiResponse<EquipmentUserInfo>
|
||||
|
||||
/**
|
||||
* 餐盘用户信息获取
|
||||
/**
|
||||
* 通过用户ID获取信息(人脸识别匹配到 userId 后调用,替代旧 getPlateBoxUserInfo 登录接口)
|
||||
*/
|
||||
@POST//("/shuwei-zhct/swEquipmentRelUser/equipmentBoxLogin")
|
||||
suspend fun equipmentBoxLogin(
|
||||
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/sideboard/app/getPlateBoxUserInfo",
|
||||
@Body param: LoginParam
|
||||
@GET
|
||||
suspend fun getMemberRefPlateByUserId(
|
||||
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/sideboard/app/getMemberRefPlateByUserId",
|
||||
@Query("userId") userId: String
|
||||
): ApiResponse<EquipmentUserInfo>
|
||||
|
||||
/**
|
||||
@@ -95,7 +92,7 @@ interface ApiService {
|
||||
|
||||
@GET
|
||||
suspend fun getEquipmentList(
|
||||
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/sideboard/app/getYxMemberRefPlateByEquipmentCode",
|
||||
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/sideboard/app/getYxMemberRefPlateByEquipmentCode",
|
||||
): ApiResponse<List<EquipmentUserInfo>>
|
||||
// /**
|
||||
// * 餐盘用户信息绑定解绑
|
||||
@@ -111,18 +108,17 @@ interface ApiService {
|
||||
*/
|
||||
@POST
|
||||
suspend fun plateBind(
|
||||
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/sideboard/app/plateBinding",
|
||||
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/sideboard/app/plateBinding",
|
||||
@Body param: BindParam
|
||||
): ApiResponse<EquipmentUserInfo?>
|
||||
|
||||
/**
|
||||
* 餐盘解绑
|
||||
* 餐盘解绑(JSON 请求体)
|
||||
*/
|
||||
@POST
|
||||
@FormUrlEncoded
|
||||
suspend fun plateUnbind(
|
||||
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/sideboard/app/plateUnbind",
|
||||
@Field("id") id: Long?
|
||||
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/sideboard/app/plateUnbind",
|
||||
@Body param: Map<String, String>
|
||||
): ApiResponse<Any?>
|
||||
|
||||
/**
|
||||
@@ -131,7 +127,7 @@ interface ApiService {
|
||||
@POST//("/shuwei-user/swclientUserInfoShop/selectList")
|
||||
suspend fun searchUser(
|
||||
// @Url url: String = "${GlobalData.appBaseUrl}/shuwei-user/swclientUserInfoShop/selectList",
|
||||
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/getUserInfoByNameOrPhone",
|
||||
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/common/app/getUserInfoByNameOrPhone",
|
||||
@Body param: SearchParam
|
||||
): ApiResponse<List<SearchResult.Member>?>
|
||||
|
||||
@@ -150,7 +146,7 @@ interface ApiService {
|
||||
*/
|
||||
@GET
|
||||
suspend fun findByPlateNumber(
|
||||
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/sideboard/app/getMemberRefPlateByPlateNumber",
|
||||
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/sideboard/app/getMemberRefPlateByPlateNumber",
|
||||
@Query("plateNumber") plateNumber: String
|
||||
): ApiResponse<EquipmentUserInfo>
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.sw.platecabinet.repository
|
||||
|
||||
import com.sw.platecabinet.model.DeviceConfig
|
||||
import com.sw.platecabinet.model.request.BindParam
|
||||
import com.sw.platecabinet.model.request.LoginParam
|
||||
import com.sw.platecabinet.model.request.SearchParam
|
||||
import com.sw.platecabinet.model.response.ApiResponse
|
||||
import com.sw.platecabinet.model.response.EquipmentUserInfo
|
||||
@@ -71,10 +70,10 @@ class RemoteRepository constructor(
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录
|
||||
* 通过用户ID获取信息(人脸识别匹配到 userId 后调用)
|
||||
*/
|
||||
suspend fun equipmentBoxLogin(param: LoginParam): ApiResponse<EquipmentUserInfo> {
|
||||
return safeApiCall { apiService.equipmentBoxLogin(param = param) }
|
||||
suspend fun getMemberRefPlateByUserId(userId: String?): ApiResponse<EquipmentUserInfo> {
|
||||
return safeApiCall { apiService.getMemberRefPlateByUserId(userId = userId ?: "") }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -95,8 +94,8 @@ class RemoteRepository constructor(
|
||||
return safeApiCall { apiService.plateBind(param = param) }
|
||||
}
|
||||
|
||||
suspend fun plateUnbind(id: Long?): ApiResponse<Any?> {
|
||||
return safeApiCall { apiService.plateUnbind(id = id) }
|
||||
suspend fun plateUnbind(id: String?): ApiResponse<Any?> {
|
||||
return safeApiCall { apiService.plateUnbind(param = mapOf("id" to (id ?: ""))) }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.sw.plate.utils.arcface.FaceApi
|
||||
import com.sw.plate.utils.comn.SerialApi
|
||||
import com.sw.platecabinet.GlobalData
|
||||
import com.sw.platecabinet.mqtt.MqttManager
|
||||
import timber.log.Timber
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
@@ -15,8 +16,9 @@ import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
/**
|
||||
* 诊断包导出:把设备信息、运行日志、崩溃日志打包成 zip,直接写入 U 盘。
|
||||
* 工控机现场无微信等分享应用,故不走 FileProvider 分享,改为写 U 盘。
|
||||
* 诊断包导出:把设备信息、运行日志、崩溃日志打包成 zip。
|
||||
* 优先写 U 盘根目录;U 盘不可用或写入失败时兜底到应用 filesDir/diagnostic/。
|
||||
* 工控机现场无微信等分享应用,故不走 FileProvider 分享。
|
||||
*/
|
||||
object DiagnosticExporter {
|
||||
|
||||
@@ -25,13 +27,27 @@ object DiagnosticExporter {
|
||||
|
||||
private val faceApi = FaceApi()
|
||||
|
||||
/** 导出到 U 盘(在 IO 线程调用) */
|
||||
fun exportToUsb(context: Context): ExportResult {
|
||||
val usbDir = UsbStorageHelper.findUsbDir(context)
|
||||
?: return ExportResult(false, "未检测到可写 U 盘,请插入后重试")
|
||||
/**
|
||||
* 导出诊断包(在 IO 线程调用):
|
||||
* 1. 优先写 U 盘根目录;
|
||||
* 2. U 盘不可用或写入失败时,兜底到应用 filesDir/diagnostic/,提示中给出绝对路径(便于 adb pull)。
|
||||
*/
|
||||
fun export(context: Context): ExportResult {
|
||||
// 1. 优先尝试 U 盘
|
||||
UsbStorageHelper.findUsbDir(context)?.let { usbDir ->
|
||||
try {
|
||||
val file = buildZip(context, usbDir)
|
||||
return ExportResult(true, "已导出到 U 盘: ${file.name}", file)
|
||||
} catch (_: Exception) {
|
||||
// U 盘写入失败,落入应用目录兜底
|
||||
}
|
||||
}
|
||||
// 2. 兜底:应用目录(仅保留最新一份,避免累积占用内部存储)
|
||||
val appDir = File(context.filesDir, "diagnostic").apply { mkdirs() }
|
||||
appDir.listFiles { f -> f.isFile && f.name.endsWith(".zip") }?.forEach { it.delete() }
|
||||
return try {
|
||||
val file = buildZip(context, usbDir)
|
||||
ExportResult(true, "已导出: ${file.name}", file)
|
||||
val file = buildZip(context, appDir)
|
||||
ExportResult(true, "U 盘不可用,已导出到应用目录:\n${file.absolutePath}", file)
|
||||
} catch (e: Exception) {
|
||||
ExportResult(false, "导出失败: ${e.message}")
|
||||
}
|
||||
@@ -41,7 +57,7 @@ object DiagnosticExporter {
|
||||
private fun buildZip(context: Context, outDir: File): File {
|
||||
val time = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
|
||||
val zipFile = File(outDir, "diagnostic_$time.zip")
|
||||
|
||||
Timber.d("Building zip file: ${zipFile.absolutePath}")
|
||||
ZipOutputStream(BufferedOutputStream(FileOutputStream(zipFile))).use { zos ->
|
||||
// 1. 设备快照 info.txt
|
||||
zos.putNextEntry(ZipEntry("info.txt"))
|
||||
|
||||
@@ -7,21 +7,65 @@ import java.io.File
|
||||
|
||||
/**
|
||||
* U 盘定位:工控机现场无微信等分享应用,诊断包需直接写入 U 盘。
|
||||
* 按可靠性依次尝试:StorageManager 可移动卷(API30+)→ /storage 卷目录 → /mnt 常见挂载点 → /proc/mounts。
|
||||
*
|
||||
* 关键教训:仅凭「vfat/exfat/ntfs」判断会误判——部分瑞芯微固件把 /oempriv 等
|
||||
* OEM 私有分区也格式化成 vfat。因此必须「路径 + 文件系统」双重校验:
|
||||
* 只有挂载点落在 /storage/ 下、或路径明显含 usb/udisk/sdcard 关键字,才算 U 盘;
|
||||
* 根目录的 /oem /odm /vendor /system 等系统分区一律排除。
|
||||
*/
|
||||
object UsbStorageHelper {
|
||||
|
||||
fun findUsbDir(context: Context): File? {
|
||||
api30Volume(context)?.let { return it }
|
||||
// 1. /storage 下卷标目录(XXXX-XXXX),Android 标准可移动存储位置
|
||||
storageVolumeDir()?.let { return it }
|
||||
commonMountPoints().forEach { if (isWritableDir(it)) return it }
|
||||
// 2. /proc/mounts 里"明显是外接存储"的可移动介质挂载点
|
||||
procMountsDir()?.let { return it }
|
||||
// 3. 硬编码常见挂载点兜底(路径本身含 usb/udisk/sdcard,无需额外校验路径)
|
||||
commonMountPoints().forEach { if (isWritableDir(it) && isRemovableFs(it)) return it }
|
||||
// 4. StorageManager removable 卷(路径 + fsType 双重校验)
|
||||
api30Volume(context)?.let {
|
||||
if (isWritableDir(it) && isUsbMountPoint(it.absolutePath) && isRemovableFs(it)) return it
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun isWritableDir(dir: File): Boolean = dir.exists() && dir.isDirectory && dir.canWrite()
|
||||
|
||||
/** API 30+ 通过 StorageManager 拿可移动卷目录(最可靠) */
|
||||
/** 挂载点是否"明显是外接存储":排除根目录系统分区 */
|
||||
private fun isUsbMountPoint(path: String): Boolean {
|
||||
val lower = path.lowercase()
|
||||
if (lower.startsWith("/oem") || lower.startsWith("/odm") ||
|
||||
lower.startsWith("/vendor") || lower.startsWith("/system") ||
|
||||
lower.startsWith("/product") || lower.startsWith("/system_ext") ||
|
||||
lower == "/data" || lower == "/cache" || lower == "/metadata" ||
|
||||
lower == "/persist" || lower == "/mnt/vendor"
|
||||
) return false
|
||||
return lower.startsWith("/storage/") ||
|
||||
lower.startsWith("/mnt/media_rw/") ||
|
||||
lower.contains("usb") || lower.contains("udisk") ||
|
||||
lower.contains("sdcard") || lower.contains("external_sd")
|
||||
}
|
||||
|
||||
/** 判断目录在 /proc/mounts 中的文件系统是否为可移动介质类型 */
|
||||
private fun isRemovableFs(dir: File): Boolean {
|
||||
val path = dir.absolutePath
|
||||
return try {
|
||||
File("/proc/mounts").readLines().any { line ->
|
||||
val parts = line.split(" ")
|
||||
parts.size >= 3 &&
|
||||
parts[1].replace("\\040", " ") == path &&
|
||||
isRemovableFsType(parts[2])
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun isRemovableFsType(fsType: String): Boolean =
|
||||
fsType.contains("vfat") || fsType.contains("exfat") ||
|
||||
fsType.contains("ntfs") || fsType.contains("fuseblk")
|
||||
|
||||
/** API 30+ 通过 StorageManager 拿可移动卷目录(不可靠,需路径 + fsType 双重佐证) */
|
||||
private fun api30Volume(context: Context): File? {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return null
|
||||
return try {
|
||||
@@ -32,27 +76,28 @@ object UsbStorageHelper {
|
||||
}
|
||||
}
|
||||
|
||||
/** /storage 下非 emulated/self 的目录(U 盘卷标形如 /storage/ABCD-1234) */
|
||||
/** /storage 下卷标目录(U 盘卷形如 /storage/ABCD-1234) */
|
||||
private fun storageVolumeDir(): File? {
|
||||
return try {
|
||||
File("/storage").listFiles()
|
||||
?.filter { it.isDirectory && it.name != "emulated" && it.name != "self" && it.canWrite() }
|
||||
?.firstOrNull()
|
||||
?.firstOrNull { isRemovableFs(it) }
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/** 常见挂载点(不同 Android 版本/固件差异大,做兜底) */
|
||||
/** 常见挂载点(路径本身即含 usb/udisk/sdcard 关键字) */
|
||||
private fun commonMountPoints(): List<File> = listOf(
|
||||
File("/mnt/usb_storage"),
|
||||
File("/mnt/usb"),
|
||||
File("/mnt/udisk"),
|
||||
File("/storage/usb"),
|
||||
File("/mnt/external_sd"),
|
||||
File("/mnt/sdcard2")
|
||||
)
|
||||
|
||||
/** 读 /proc/mounts 找可移动介质挂载点(vfat/exfat/ntfs/fuseblk) */
|
||||
/** 读 /proc/mounts 找"明显是外接存储"的可移动介质挂载点 */
|
||||
private fun procMountsDir(): File? {
|
||||
return try {
|
||||
File("/proc/mounts").readLines()
|
||||
@@ -61,9 +106,7 @@ object UsbStorageHelper {
|
||||
if (parts.size < 3) return@mapNotNull null
|
||||
val fsType = parts[2]
|
||||
val mountPoint = parts[1].replace("\\040", " ")
|
||||
if (fsType.contains("vfat") || fsType.contains("exfat") ||
|
||||
fsType.contains("ntfs") || fsType.contains("fuseblk")
|
||||
) File(mountPoint) else null
|
||||
if (isRemovableFsType(fsType) && isUsbMountPoint(mountPoint)) File(mountPoint) else null
|
||||
}
|
||||
.firstOrNull { isWritableDir(it) }
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -130,7 +130,7 @@ class NetViewModelV2 : ViewModel() {
|
||||
GlobalData.activeKey,
|
||||
object : FaceApi.ActiveCallback {
|
||||
override fun onSuccess(activeCode: Int) {
|
||||
Timber.d("activeEngine activeCode = $activeCode")
|
||||
Timber.d("activeEngine activeCode = $activeCode=="+GlobalData.activeKey)
|
||||
// ThreadUtils.launch {
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
when (activeCode) {
|
||||
|
||||
@@ -158,7 +158,7 @@ class SettingViewModel : BaseViewModel() {
|
||||
// }
|
||||
// }
|
||||
|
||||
fun unbindPlate(id: Long?, block:(Pair<Boolean?, ErrorInfo>)-> Unit) {
|
||||
fun unbindPlate(id: String?, block:(Pair<Boolean?, ErrorInfo>)-> Unit) {
|
||||
block(null to ErrorInfo())
|
||||
launchWithLoading {
|
||||
val response = repository.plateUnbind(id)
|
||||
|
||||
@@ -12,7 +12,6 @@ import com.sw.platecabinet.GlobalData
|
||||
import com.sw.platecabinet.GlobalData.globalEquipmentCode
|
||||
import com.sw.platecabinet.GlobalKey
|
||||
import com.sw.platecabinet.model.DeviceConfig
|
||||
import com.sw.platecabinet.model.request.LoginParam
|
||||
import com.sw.platecabinet.model.response.EquipmentUserInfo
|
||||
import com.sw.platecabinet.model.response.UserFaceModel
|
||||
import com.sw.platecabinet.utils.SpTool
|
||||
@@ -93,29 +92,29 @@ class UserViewModel : BaseViewModel() {
|
||||
private var faceTimestamp = 0L
|
||||
|
||||
|
||||
/**
|
||||
* 校验码登录
|
||||
*/
|
||||
fun loginWithPwd(loginParam: LoginParam) {
|
||||
launchWithLoading {
|
||||
val response = repository.equipmentBoxLogin(loginParam)
|
||||
if (parseResponse(response)) {
|
||||
_currentUserInfo.value = response.data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过用户id获取用户信息
|
||||
*
|
||||
* 无论成功或失败(如「未找到用户绑定信息」)都会回调 action,
|
||||
* 以便调用方统一收尾(例如关闭 loading 弹窗)。
|
||||
* 成功时回调绑定的用户信息,失败时回调 null。
|
||||
*
|
||||
* @param silent true 表示失败时不弹错误 toast。绑定页点会员检查是否已绑定
|
||||
* 时,「未找到绑定信息」属于正常态,应静默处理。
|
||||
*/
|
||||
fun getUserInfoById(memberId: String?, action:(EquipmentUserInfo?)->Unit={}) {
|
||||
fun getUserInfoById(memberId: String?, silent: Boolean = false, action:(EquipmentUserInfo?)->Unit={}) {
|
||||
_currentUserInfo.value = null
|
||||
launchWithLoading {
|
||||
val loginParam = LoginParam(faceId = memberId)
|
||||
val response = repository.equipmentBoxLogin(loginParam)
|
||||
if (parseResponse(response)) {
|
||||
val response = repository.getMemberRefPlateByUserId(userId = memberId)
|
||||
if (response.isSuccess()) {
|
||||
_currentUserInfo.value = response.data
|
||||
action(response.data)
|
||||
} else {
|
||||
if (!silent) {
|
||||
Timber.d("msg = ${response.msg}, code = ${response.code}")
|
||||
ToastUtils.showToast("${response.msg}(${response.code})")
|
||||
}
|
||||
action(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/bg"
|
||||
android:orientation="vertical">
|
||||
|
||||
<include
|
||||
android:id="@+id/includeHeader"
|
||||
layout="@layout/item_title_time" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="60dp"
|
||||
android:paddingEnd="60dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="手机后四位"
|
||||
android:textColor="#FFCC99"
|
||||
android:textSize="26sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="80dp"
|
||||
android:layout_marginTop="23dp"
|
||||
android:background="@drawable/shape_pwd_bg"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginEnd="55dp"
|
||||
android:src="@drawable/ic_phone" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_phone"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:hint="输入手机后四位"
|
||||
android:imeOptions="actionNext"
|
||||
android:inputType="number"
|
||||
android:maxLength="4"
|
||||
android:maxLines="1"
|
||||
android:textColor="#F3D2BD"
|
||||
android:textColorHint="#7B6D6A"
|
||||
android:textSize="26sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="25dp"
|
||||
android:text="校验码"
|
||||
android:textColor="#FFCC99"
|
||||
android:textSize="26sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="80dp"
|
||||
android:layout_marginTop="23dp"
|
||||
android:background="@drawable/shape_pwd_bg"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginEnd="55dp"
|
||||
android:src="@drawable/ic_pwd" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_pwd"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:hint="输入校验码"
|
||||
android:inputType="numberPassword"
|
||||
android:maxLines="1"
|
||||
android:textColor="#F3D2BD"
|
||||
android:textColorHint="#7B6D6A"
|
||||
android:imeOptions="actionDone"
|
||||
android:textSize="26sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<android.widget.Button
|
||||
android:id="@+id/btnLogin"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="80dp"
|
||||
android:layout_marginTop="46dp"
|
||||
android:background="@drawable/shape_btn_bg"
|
||||
android:text="登录"
|
||||
android:textColor="#11111C"
|
||||
android:textSize="30sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvFaceRec"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="40dp"
|
||||
android:padding="8dp"
|
||||
android:text="人脸识别"
|
||||
android:textColor="#FFCC99"
|
||||
android:textSize="26sp"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
+271
@@ -0,0 +1,271 @@
|
||||
# 餐盘柜设备端 API 文档
|
||||
|
||||
> 更新时间:2026-09-09 | 服务:platform-nutrition(端口 24810)
|
||||
|
||||
## 一、通用约定
|
||||
|
||||
### 1. 路径与鉴权
|
||||
- 所有接口路径前缀为 `/nutrition`,在 Nacos 白名单 `/nutrition/neglect/**` 下,**无需登录 token**
|
||||
- 设备上下文统一靠请求头 **`X-DEVICE-CODE`** 解析(值为终端管理的设备编码),请求体无需传 deviceCode
|
||||
|
||||
### 2. 统一响应结构
|
||||
|
||||
```json
|
||||
{ "code": "00000", "msg": "操作成功", "data": { }, "total": 0 }
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| code | string | `00000` 成功;`99999` 等为失败 |
|
||||
| msg | string | 失败时直接展示给设备端(业务异常中文提示) |
|
||||
| data | object/array | 业务数据 |
|
||||
| total | int | 分页接口返回总条数,非分页为 0 |
|
||||
|
||||
### 3. 数据类型说明
|
||||
- 所有 id(Long)序列化为**字符串**,防止精度丢失
|
||||
- 金额为 BigDecimal,按原样输出(不转科学计数法)
|
||||
- 时间格式 `yyyy-MM-dd HH:mm:ss`,日期 `yyyy-MM-dd`
|
||||
|
||||
### 4. 收费模式 chargeType
|
||||
|
||||
| 值 | 含义 | price |
|
||||
|----|------|-------|
|
||||
| 1 | 按餐计费 | 有值(元/份) |
|
||||
| 2 | 称重计费 | null |
|
||||
| 3 | 免费 | null |
|
||||
|
||||
---
|
||||
|
||||
## 二、接口明细
|
||||
|
||||
### 1. 获取设备绑定用户列表
|
||||
|
||||
`GET /nutrition/neglect/sideboard/app/getYxMemberRefPlateByEquipmentCode`
|
||||
|
||||
返回当前设备(按 X-DEVICE-CODE)全部格子,按格子序号升序。
|
||||
|
||||
**返回 data:数组**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | string | 绑定记录id(解绑时使用) |
|
||||
| equipmentId | string | 设备id(nut_terminal.id) |
|
||||
| equipmentName | string | 设备名称 |
|
||||
| equipmentCode | string | 设备编码 |
|
||||
| equipmentBoxCode | string | 格子编号(1..N) |
|
||||
| faceId | string | 绑定用户id(=nut_user.id),空格子为 null |
|
||||
| plateNumber | string | 餐盘号,空格子为 null |
|
||||
| orderNo | int | 格子排序号 |
|
||||
| name | string | 用户姓名(未绑定为 null) |
|
||||
| phone | string | 用户手机号(未绑定为 null) |
|
||||
| faceUrl | string | 预留,人脸头像 |
|
||||
| mealTime | string | 就餐时间(预留) |
|
||||
| openTime | string | 最近开柜绑定时间 |
|
||||
| updateTime | string | 更新时间 |
|
||||
| eatCount | int | 预留 |
|
||||
|
||||
**示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "00000", "msg": "操作成功", "total": 0,
|
||||
"data": [
|
||||
{ "id": "1948000001", "equipmentId": "1001", "equipmentName": "1号餐盘柜",
|
||||
"equipmentCode": "DEV-PLATE-CABINET-01", "equipmentBoxCode": "1",
|
||||
"faceId": "10001", "plateNumber": "PLATE-001", "orderNo": 1,
|
||||
"name": "张三", "phone": "138****0001", "faceUrl": null,
|
||||
"mealTime": null, "openTime": "2026-09-04 11:20:00",
|
||||
"updateTime": "2026-09-04 11:20:00", "eatCount": 0 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. 餐盘绑定
|
||||
|
||||
`POST /nutrition/neglect/sideboard/app/plateBinding`
|
||||
|
||||
**入参(JSON 请求体)**
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| equipmentBoxCode | string | 是 | 目标格子编号 |
|
||||
| faceId | long | 是 | 用户id(用户搜索接口返回的 faceId) |
|
||||
| plateNumber | string | 是 | 餐盘号 |
|
||||
| equipmentId / equipmentCode | - | 否 | 兼容旧项目的冗余字段,后端以请求头为准 |
|
||||
|
||||
```json
|
||||
{ "equipmentBoxCode": "1", "faceId": 10001, "plateNumber": "PLATE-001" }
|
||||
```
|
||||
|
||||
**处理规则**
|
||||
1. 目标格子已绑定用户 → 报错「当前柜子已绑定用户」
|
||||
2. 餐盘号已被其他用户绑定 → 报错「当前柜子已绑定用户」
|
||||
3. 用户不存在或**不是会员**(is_vip≠1)→ 报错「用户查询失败」
|
||||
4. 用户已绑定其他格子 → 自动释放旧格子并重建空格
|
||||
5. 成功返回 `Result<Void>`,同时记录开柜时间
|
||||
|
||||
---
|
||||
|
||||
### 3. 餐盘解绑
|
||||
|
||||
`POST /nutrition/neglect/sideboard/app/plateUnbind`
|
||||
|
||||
> ⚠️ 与旧项目不同:参数在 **JSON 请求体**,不是 QueryString
|
||||
|
||||
**入参(JSON 请求体)**
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| id | long | 是 | 绑定记录id(列表接口返回的 id) |
|
||||
|
||||
```json
|
||||
{ "id": 1948000001 }
|
||||
```
|
||||
|
||||
**错误**:记录不存在 → 「绑定记录不存在」。成功后原格子重建为空格。
|
||||
|
||||
---
|
||||
|
||||
### 4. 用户信息模糊搜索
|
||||
|
||||
`POST /nutrition/neglect/common/app/getUserInfoByNameOrPhone`
|
||||
|
||||
按姓名或手机号模糊搜索**会员**(仅 is_vip=1 且状态正常),不按食堂过滤。
|
||||
|
||||
**入参(JSON 请求体)**
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| name | string | 否 | 姓名模糊 |
|
||||
| phone | string | 否 | 手机号模糊(name/phone 至少一个) |
|
||||
| pageNum | int | 否 | 默认 1 |
|
||||
| pageSize | int | 否 | 默认 10,最大 500 |
|
||||
|
||||
**返回 data:数组**(total 为总条数)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | string | 用户id |
|
||||
| faceId | string | 同 id(键名对齐旧项目) |
|
||||
| name | string | 姓名 |
|
||||
| phone | string | 手机号 |
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "00000", "msg": "操作成功", "total": 1,
|
||||
"data": [ { "id": "10001", "faceId": "10001", "name": "张三", "phone": "13800000001" } ]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. 通过餐盘号获取信息
|
||||
|
||||
`GET /nutrition/neglect/sideboard/app/getMemberRefPlateByPlateNumber?plateNumber=PLATE-001`
|
||||
|
||||
用户刷餐盘取餐时调用;**cardBalance 为真实账户余额**:余额 > 0 可开柜,负数提示用户充值。
|
||||
|
||||
**返回 data**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| equipmentId | string | 设备id |
|
||||
| equipmentName | string | 设备名称 |
|
||||
| equipmentCode | string | 设备编码 |
|
||||
| id | string | 绑定记录id |
|
||||
| equipmentBoxCode | string | 格子编号 |
|
||||
| updateTime | string | 更新时间 |
|
||||
| plateNumber | string | 餐盘号 |
|
||||
| faceId | string | 绑定用户id,未绑定为 null |
|
||||
| name | string | 用户姓名,未绑定为 null |
|
||||
| phone | string | 用户手机号,未绑定为 null |
|
||||
| cardBalance | number | 账户真实余额(元);未绑定/无账户为 null |
|
||||
|
||||
**错误**:餐盘号为空 → 「餐盘号不能为空」;查无绑定 → 「未找到餐盘绑定信息」
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "00000", "msg": "操作成功", "total": 0,
|
||||
"data": {
|
||||
"equipmentId": "1001", "equipmentName": "1号餐盘柜", "equipmentCode": "DEV-PLATE-CABINET-01",
|
||||
"id": "1948000001", "equipmentBoxCode": "1", "updateTime": "2026-09-04 11:20:00",
|
||||
"plateNumber": "PLATE-001", "faceId": "10001", "name": "张三", "phone": "13800000001",
|
||||
"cardBalance": 25.50
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. 通过用户ID获取信息
|
||||
|
||||
`GET /nutrition/neglect/sideboard/app/getMemberRefPlateByUserId?userId=10001`
|
||||
|
||||
人脸识别匹配到 `userId` 后调用;查询**当前设备**上该用户绑定的餐盘/格子,**cardBalance 为真实账户余额**:余额 > 0 可开柜,负数提示用户充值。
|
||||
|
||||
**返回 data**(字段同「通过餐盘号获取信息」)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| equipmentId | string | 设备id |
|
||||
| equipmentName | string | 设备名称 |
|
||||
| equipmentCode | string | 设备编码 |
|
||||
| id | string | 绑定记录id |
|
||||
| equipmentBoxCode | string | 格子编号 |
|
||||
| updateTime | string | 更新时间 |
|
||||
| plateNumber | string | 餐盘号 |
|
||||
| faceId | string | 绑定用户id |
|
||||
| name | string | 用户姓名 |
|
||||
| phone | string | 用户手机号 |
|
||||
| cardBalance | number | 账户真实余额(元) |
|
||||
|
||||
**错误**:用户ID为空 → 「用户ID不能为空」;查无绑定 → 「未找到用户绑定信息」
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "00000", "msg": "操作成功", "total": 0,
|
||||
"data": {
|
||||
"equipmentId": "1001", "equipmentName": "1号餐盘柜", "equipmentCode": "DEV-PLATE-CABINET-01",
|
||||
"id": "1948000001", "equipmentBoxCode": "1", "updateTime": "2026-09-04 11:20:00",
|
||||
"plateNumber": "PLATE-001", "faceId": "10001", "name": "张三", "phone": "13800000001",
|
||||
"cardBalance": 25.50
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. 获取扣费规则
|
||||
|
||||
`GET /nutrition/neglect/sideboard/app/getChargeRuleByEquipmentCode`
|
||||
|
||||
键名对齐旧项目 `getRegionRuleByEquipmentCode`。按「终端绑定餐线 → 今天星期 × 当前时段餐次」查询收费模式矩阵(nut_canteen_line_charge)。
|
||||
|
||||
**返回 data**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| chargeType | int | 1按餐计费 / 2称重计费 / 3免费 |
|
||||
| price | number | 仅 chargeType=1 时有值(元/份) |
|
||||
|
||||
**规则**
|
||||
- 终端未绑定餐线 / 餐线停用 / 当日当餐未配置 → 默认返回 `chargeType=2`(称重计费)
|
||||
- 餐次时段:早餐 06:00-10:00 / 午餐 10:00-14:00 / 加餐 14:00-16:00 / 晚餐 16:00-20:00(全局统一)
|
||||
- **按餐计费的扣费由后端定时任务完成**:该餐次取餐结束(最后一条取餐记录 15 分钟后)统一按份扣余额,**硬件无需在取盘时扣款**,仅需按 cardBalance > 0 判断是否开柜
|
||||
- 余额不足会扣成负数(后续充值回补),负余额即提示充值
|
||||
|
||||
```json
|
||||
{ "code": "00000", "msg": "操作成功", "total": 0, "data": { "chargeType": 1, "price": 15.00 } }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、错误响应示例
|
||||
|
||||
```json
|
||||
{ "code": "99999", "msg": "当前柜子已绑定用户", "data": null, "total": 0 }
|
||||
```
|
||||
|
||||
业务校验失败的 msg 为中文提示,可直接在设备端展示;参数校验失败(缺必填字段)返回 PARAM_ERROR。
|
||||
Reference in New Issue
Block a user