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。