diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 06c8e38..26489e5 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -47,10 +47,6 @@
android:exported="false"
android:launchMode="singleTask">
-
() {
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()
}
diff --git a/app/src/main/java/com/sw/platecabinet/activity/LoginByFaceActivity.kt b/app/src/main/java/com/sw/platecabinet/activity/LoginByFaceActivity.kt
index 8b7c5ed..ea6ccf4 100644
--- a/app/src/main/java/com/sw/platecabinet/activity/LoginByFaceActivity.kt
+++ b/app/src/main/java/com/sw/platecabinet/activity/LoginByFaceActivity.kt
@@ -111,11 +111,6 @@ class LoginByFaceActivity : BaseActivity(),
//开启人脸增量数据定时任务(V2)
startFaceTask()
- binding.llToPwd.setOnClickListener {
- val intent = Intent(this, LoginByPwdActivity::class.java)
- startActivity(intent)
- }
-
// TODO: 测试请求
TaskManager.startTask()
diff --git a/app/src/main/java/com/sw/platecabinet/activity/LoginByPwdActivity.kt b/app/src/main/java/com/sw/platecabinet/activity/LoginByPwdActivity.kt
deleted file mode 100644
index ec8230e..0000000
--- a/app/src/main/java/com/sw/platecabinet/activity/LoginByPwdActivity.kt
+++ /dev/null
@@ -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() {
-
- 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()
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/platecabinet/activity/OpsActivity.kt b/app/src/main/java/com/sw/platecabinet/activity/OpsActivity.kt
index 985adc6..10f1e56 100644
--- a/app/src/main/java/com/sw/platecabinet/activity/OpsActivity.kt
+++ b/app/src/main/java/com/sw/platecabinet/activity/OpsActivity.kt
@@ -283,8 +283,14 @@ class OpsActivity : BaseActivity() {
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()
+ }
}
}
diff --git a/app/src/main/java/com/sw/platecabinet/fragment/BindPlateFragment.kt b/app/src/main/java/com/sw/platecabinet/fragment/BindPlateFragment.kt
index e8b1c4a..a709f7e 100644
--- a/app/src/main/java/com/sw/platecabinet/fragment/BindPlateFragment.kt
+++ b/app/src/main/java/com/sw/platecabinet/fragment/BindPlateFragment.kt
@@ -262,7 +262,7 @@ class BindPlateFragment : BaseFragment(
//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(
}
override fun onFail(e: Exception?) {
- ToastUtils.showToast("绑定失败")
+ // 走到这里说明 plateBinding 接口已成功(code=00000),
+ // 只是开柜动作失败,不能误报「绑定失败」
+ ToastUtils.showToast("绑定成功,开柜失败")
+ activity?.finish()
}
})
// }
diff --git a/app/src/main/java/com/sw/platecabinet/model/request/LoginParam.kt b/app/src/main/java/com/sw/platecabinet/model/request/LoginParam.kt
deleted file mode 100644
index 6e923e9..0000000
--- a/app/src/main/java/com/sw/platecabinet/model/request/LoginParam.kt
+++ /dev/null
@@ -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
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/platecabinet/model/request/SearchParam.kt b/app/src/main/java/com/sw/platecabinet/model/request/SearchParam.kt
index 86f32cf..fbbc36f 100644
--- a/app/src/main/java/com/sw/platecabinet/model/request/SearchParam.kt
+++ b/app/src/main/java/com/sw/platecabinet/model/request/SearchParam.kt
@@ -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,
diff --git a/app/src/main/java/com/sw/platecabinet/model/response/ApiResponse.kt b/app/src/main/java/com/sw/platecabinet/model/response/ApiResponse.kt
index d55492c..5485f06 100644
--- a/app/src/main/java/com/sw/platecabinet/model/response/ApiResponse.kt
+++ b/app/src/main/java/com/sw/platecabinet/model/response/ApiResponse.kt
@@ -5,6 +5,7 @@ data class ApiResponse(
// val success: Boolean? = false,
val msg: String? = "",
val data: T? = null,
+ val total: Int = 0,
// val result: T? = null,
) {
// fun isSuccess(): Boolean = code == 200
diff --git a/app/src/main/java/com/sw/platecabinet/model/response/EquipmentUserInfo.kt b/app/src/main/java/com/sw/platecabinet/model/response/EquipmentUserInfo.kt
index 5801b86..9d6b793 100644
--- a/app/src/main/java/com/sw/platecabinet/model/response/EquipmentUserInfo.kt
+++ b/app/src/main/java/com/sw/platecabinet/model/response/EquipmentUserInfo.kt
@@ -12,9 +12,9 @@ import kotlinx.parcelize.Parcelize
@Parcelize
data class EquipmentUserInfo(
/**
- * 主键
+ * 主键(绑定记录id,后端序列化为字符串)
*/
- val id: Long? = 0,
+ val id: String? = "",
/**
*
* 设备Id
diff --git a/app/src/main/java/com/sw/platecabinet/network/api/ApiService.kt b/app/src/main/java/com/sw/platecabinet/network/api/ApiService.kt
index 453dccb..7bb7ecf 100644
--- a/app/src/main/java/com/sw/platecabinet/network/api/ApiService.kt
+++ b/app/src/main/java/com/sw/platecabinet/network/api/ApiService.kt
@@ -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
- /**
- * 餐盘用户信息获取
+ /**
+ * 通过用户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
/**
@@ -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>
// /**
// * 餐盘用户信息绑定解绑
@@ -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
/**
- * 餐盘解绑
+ * 餐盘解绑(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
): ApiResponse
/**
@@ -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?>
@@ -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
diff --git a/app/src/main/java/com/sw/platecabinet/repository/RemoteRepository.kt b/app/src/main/java/com/sw/platecabinet/repository/RemoteRepository.kt
index 4280054..8bc6036 100644
--- a/app/src/main/java/com/sw/platecabinet/repository/RemoteRepository.kt
+++ b/app/src/main/java/com/sw/platecabinet/repository/RemoteRepository.kt
@@ -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 {
- return safeApiCall { apiService.equipmentBoxLogin(param = param) }
+ suspend fun getMemberRefPlateByUserId(userId: String?): ApiResponse {
+ 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 {
- return safeApiCall { apiService.plateUnbind(id = id) }
+ suspend fun plateUnbind(id: String?): ApiResponse {
+ return safeApiCall { apiService.plateUnbind(param = mapOf("id" to (id ?: ""))) }
}
/**
diff --git a/app/src/main/java/com/sw/platecabinet/utils/DiagnosticExporter.kt b/app/src/main/java/com/sw/platecabinet/utils/DiagnosticExporter.kt
index d592024..963e920 100644
--- a/app/src/main/java/com/sw/platecabinet/utils/DiagnosticExporter.kt
+++ b/app/src/main/java/com/sw/platecabinet/utils/DiagnosticExporter.kt
@@ -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"))
diff --git a/app/src/main/java/com/sw/platecabinet/utils/UsbStorageHelper.kt b/app/src/main/java/com/sw/platecabinet/utils/UsbStorageHelper.kt
index bfffcbb..90517af 100644
--- a/app/src/main/java/com/sw/platecabinet/utils/UsbStorageHelper.kt
+++ b/app/src/main/java/com/sw/platecabinet/utils/UsbStorageHelper.kt
@@ -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 = 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) {
diff --git a/app/src/main/java/com/sw/platecabinet/viewmodel/NetViewModelV2.kt b/app/src/main/java/com/sw/platecabinet/viewmodel/NetViewModelV2.kt
index 4167735..1e89650 100644
--- a/app/src/main/java/com/sw/platecabinet/viewmodel/NetViewModelV2.kt
+++ b/app/src/main/java/com/sw/platecabinet/viewmodel/NetViewModelV2.kt
@@ -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) {
diff --git a/app/src/main/java/com/sw/platecabinet/viewmodel/SettingViewModel.kt b/app/src/main/java/com/sw/platecabinet/viewmodel/SettingViewModel.kt
index d8c16de..ff3238b 100644
--- a/app/src/main/java/com/sw/platecabinet/viewmodel/SettingViewModel.kt
+++ b/app/src/main/java/com/sw/platecabinet/viewmodel/SettingViewModel.kt
@@ -158,7 +158,7 @@ class SettingViewModel : BaseViewModel() {
// }
// }
- fun unbindPlate(id: Long?, block:(Pair)-> Unit) {
+ fun unbindPlate(id: String?, block:(Pair)-> Unit) {
block(null to ErrorInfo())
launchWithLoading {
val response = repository.plateUnbind(id)
diff --git a/app/src/main/java/com/sw/platecabinet/viewmodel/UserViewModel.kt b/app/src/main/java/com/sw/platecabinet/viewmodel/UserViewModel.kt
index cb3736a..a4b3e41 100644
--- a/app/src/main/java/com/sw/platecabinet/viewmodel/UserViewModel.kt
+++ b/app/src/main/java/com/sw/platecabinet/viewmodel/UserViewModel.kt
@@ -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)
}
}
}
diff --git a/app/src/main/res/layout/activity_login_by_pwd.xml b/app/src/main/res/layout/activity_login_by_pwd.xml
deleted file mode 100644
index 7633776..0000000
--- a/app/src/main/res/layout/activity_login_by_pwd.xml
+++ /dev/null
@@ -1,122 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/餐盘柜-API文档.md b/餐盘柜-API文档.md
new file mode 100644
index 0000000..e7212d4
--- /dev/null
+++ b/餐盘柜-API文档.md
@@ -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`,同时记录开柜时间
+
+---
+
+### 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。