Parcourir la source

feat: Add RFID scanning functionality and improve materials management

- Add RFIDScanMode enum and IRFIDScanHelper interface for RFID scanning.
- Implement UHFRFIDScanHelper for UHF RFID scanning.
- Add NodeIdHelper for generating CAN node ID combinations.
- Update SettingsFragment to allow selection of RFID scan mode.
- Update MaterialsExchangeFragment to display material status (wait to return, exception) and filter materials accordingly.
- Update MaterialsExchangeWaitAndFinishViewModel to handle RFID scanning, material borrowing/returning, and exception handling.
- Update MaterialsDao, IMaterialsRepository, StandardMaterialsRepository, NetworkMaterialsRepository, IMaterialsLogic, and MaterialsLogic to support new materials management operations.
- Update CanReadyPlugin to use NodeIdHelper for scanning CAN nodes and adjust timeout.
- Initialize RFIDScanHelper in ISCSMCApplication.
- Update IsMaterialsLoan to set default loanUserId and loanTime.
- Add new i18n strings for RFID scanning and material status.
- Update item_materials.xml layout to include a mask for displaying material status.
- Update fragment_settings.xml layout to include RFID scan mode selection.
- Add colorMask to theme and attrs.
周文健 il y a 1 mois
Parent
commit
9c50ca2107
23 fichiers modifiés avec 565 ajouts et 56 suppressions
  1. 20 0
      app/src/main/assets/i18n/zh-CN.json
  2. 4 1
      app/src/main/java/com/grkj/iscs_mc/ISCSMCApplication.kt
  3. 11 10
      app/src/main/java/com/grkj/iscs_mc/features/main/fragment/material_manage/MaterialsExchangeFragment.kt
  4. 19 3
      app/src/main/java/com/grkj/iscs_mc/features/main/fragment/user_info/SettingsFragment.kt
  5. 49 0
      app/src/main/java/com/grkj/iscs_mc/features/main/viewmodel/material_manage/MaterialsExchangeWaitAndFinishViewModel.kt
  6. 32 0
      app/src/main/res/layout/fragment_settings.xml
  7. 50 21
      app/src/main/res/layout/item_materials.xml
  8. 4 0
      data/src/main/java/com/grkj/data/common/MMKVConstants.kt
  9. 25 0
      data/src/main/java/com/grkj/data/domain/logic/IMaterialsLogic.kt
  10. 20 0
      data/src/main/java/com/grkj/data/domain/logic/impl/MaterialsLogic.kt
  11. 3 5
      data/src/main/java/com/grkj/data/enums/HardwareMode.kt
  12. 25 0
      data/src/main/java/com/grkj/data/enums/RFIDScanMode.kt
  13. 20 0
      data/src/main/java/com/grkj/data/hardware/IRFIDScanHelper.kt
  14. 2 10
      data/src/main/java/com/grkj/data/hardware/can/CanReadyPlugin.kt
  15. 81 0
      data/src/main/java/com/grkj/data/hardware/can/NodeIdHelper.kt
  16. 135 0
      data/src/main/java/com/grkj/data/hardware/uhf/UHFRFIDScanHelper.kt
  17. 20 4
      data/src/main/java/com/grkj/data/local/dao/MaterialsDao.kt
  18. 4 2
      data/src/main/java/com/grkj/data/local/dos/IsMaterialsLoan.kt
  19. 15 0
      data/src/main/java/com/grkj/data/repository/IMaterialsRepository.kt
  20. 12 0
      data/src/main/java/com/grkj/data/repository/impl/network/NetworkMaterialsRepository.kt
  21. 12 0
      data/src/main/java/com/grkj/data/repository/impl/standard/StandardMaterialsRepository.kt
  22. 1 0
      ui-base/src/main/res/values/attrs.xml
  23. 1 0
      ui-base/src/main/res/values/theme.xml

+ 20 - 0
app/src/main/assets/i18n/zh-CN.json

@@ -1708,5 +1708,25 @@
     "key": "materials_exchange_return_tip",
     "type": "text",
     "value": "您于{0}归还了以下{1}件物资:"
+  },
+  "rfid_scan_mode": {
+    "key": "rfid_scan_mode",
+    "type": "text",
+    "value": "RFID扫描模式"
+  },
+  "rfid_helper_not_init": {
+    "key": "rfid_helper_not_init",
+    "type": "text",
+    "value": "RFID扫描未初始化"
+  },
+  "exception": {
+    "key": "exception",
+    "type": "text",
+    "value": "异常"
+  },
+  "wait_to_return": {
+    "key": "wait_to_return",
+    "type": "text",
+    "value": "待还"
   }
 }

+ 4 - 1
app/src/main/java/com/grkj/iscs_mc/ISCSMCApplication.kt

@@ -14,6 +14,7 @@ import com.drake.statelayout.StateConfig
 import com.grkj.data.common.EventConstants
 import com.grkj.data.di.LogicManager
 import com.grkj.data.enums.HardwareMode
+import com.grkj.data.enums.RFIDScanMode
 import com.grkj.data.local.database.DbReadyGate
 import com.grkj.iscs_mc.features.splash.activity.SplashActivity
 import com.grkj.shared.model.EventBean
@@ -104,7 +105,9 @@ class ISCSMCApplication : Application() {
             // 5) 数据库/业务/硬件连接(你原来就放 IO,很好)
             DbReadyGate.await()
             LogicManager.init(this@ISCSMCApplication)
-            HardwareMode.getCurrentHardwareMode().connectAndAddListener(WeakReference(this@ISCSMCApplication))
+            HardwareMode.getCurrentHardwareMode()
+                .connectAndAddListener(WeakReference(this@ISCSMCApplication))
+            RFIDScanMode.getCurrentRFIDScanMode().init()
         }
     }
 

+ 11 - 10
app/src/main/java/com/grkj/iscs_mc/features/main/fragment/material_manage/MaterialsExchangeFragment.kt

@@ -1,22 +1,14 @@
 package com.grkj.iscs_mc.features.main.fragment.material_manage
 
-import androidx.core.content.ContextCompat
 import androidx.core.view.isVisible
 import androidx.fragment.app.viewModels
 import androidx.recyclerview.widget.LinearLayoutManager
 import coil.load
 import com.drake.brv.BindingAdapter
-import com.drake.brv.annotaion.DividerOrientation
-import com.drake.brv.utils.dividerSpace
 import com.drake.brv.utils.linear
 import com.drake.brv.utils.models
 import com.drake.brv.utils.setup
-import com.google.android.flexbox.AlignItems
-import com.google.android.flexbox.FlexDirection
-import com.google.android.flexbox.FlexWrap
-import com.google.android.flexbox.FlexboxItemDecoration
 import com.google.android.flexbox.FlexboxLayoutManager
-import com.google.android.flexbox.JustifyContent
 import com.grkj.data.common.EventConstants
 import com.grkj.data.local.dos.IsMaterials
 import com.grkj.iscs_mc.R
@@ -116,6 +108,15 @@ class MaterialsExchangeFragment : BaseFragment<FragmentMaterialExchangeBinding>(
         itemBinding.materialsPicture.load(viewModel.materialsType.find { it.materialsTypeId == item.materialsTypeId }?.materialsTypePicture)
         itemBinding.materialsName.text = item.materialsName
         itemBinding.materialsRfid.text = item.materialsRfid
+        itemBinding.maskLayout.isVisible =
+            item.loanState == "0" || item.status == "3"
+        if (item.loanState == "0") {
+            itemBinding.materialsType.text = CommonUtils.getStr("wait_to_return")
+        }
+        if (item.status == "3") {
+            itemBinding.materialsType.text = CommonUtils.getStr("exception")
+        }
+
     }
 
     private fun BindingAdapter.BindingViewHolder.onHeaderRVListBinding() {
@@ -159,9 +160,9 @@ class MaterialsExchangeFragment : BaseFragment<FragmentMaterialExchangeBinding>(
         currentMaterialsTypeId = materialsTypeId
         binding.listRv.models = viewModel.materialsData.filter {
             if (tabPosition == 0) {
-                it.materialsId !in viewModel.materialsLoan.map { it.materialsId }
+                it.loanState == "1"
             } else {
-                it.materialsId in viewModel.materialsLoan.map { it.materialsId }
+                it.loanState == "0"
             } && (materialsTypeId == -1L || it.materialsTypeId == materialsTypeId)
         }
     }

+ 19 - 3
app/src/main/java/com/grkj/iscs_mc/features/main/fragment/user_info/SettingsFragment.kt

@@ -3,6 +3,7 @@ package com.grkj.iscs_mc.features.main.fragment.user_info
 import com.grkj.data.common.CommonConstants
 import com.grkj.data.common.MMKVConstants
 import com.grkj.data.enums.HardwareMode
+import com.grkj.data.enums.RFIDScanMode
 import com.grkj.iscs_mc.R
 import com.grkj.iscs_mc.databinding.FragmentSettingsBinding
 import com.grkj.iscs_mc.features.main.dialog.TextDropDownDialog
@@ -24,6 +25,11 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding>() {
      * 硬件模式修改
      */
     private var hardwareModeChanged: Boolean = false
+
+    /**
+     * RFID扫描模式修改
+     */
+    private var rfidScanModeChanged: Boolean = false
     override fun getLayoutId(): Int {
         return R.layout.fragment_settings
     }
@@ -48,6 +54,8 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding>() {
         )
         binding.hardwareMode.text =
             MMKVConstants.KEY_HARDWARE_MODE.getMMKVData(HardwareMode.CAN.name)
+        binding.rfidScanMode.text =
+            MMKVConstants.KEY_RFID_SCAN_MODE.getMMKVData(RFIDScanMode.UHF.name)
         binding.hardwareMode.setDebouncedClickListener {
             val hardwareModeData = HardwareMode.values()
                 .map { TextDropDownDialog.SimpleTextDropDownEntity(dataText = it.name) }
@@ -57,18 +65,26 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding>() {
                 hardwareModeChanged = true
             }
         }
+        binding.rfidScanMode.setDebouncedClickListener {
+            val rfidScanModeData = RFIDScanMode.values()
+                .map { TextDropDownDialog.SimpleTextDropDownEntity(dataText = it.name) }
+            TextDropDownDialog.showSingle(rfidScanModeData, binding.rfidScanMode) {
+                binding.rfidScanMode.text = it.getShowText()
+                MMKVConstants.KEY_RFID_SCAN_MODE.saveMMKVData(it.getShowText())
+                rfidScanModeChanged = true
+            }
+        }
         binding.confirm.setDebouncedClickListener {
             if (checkData()) {
                 MMKVConstants.KEY_MAX_FINGERPRINT_INSERT.saveMMKVData(
                     binding.maxFingerprintInsert.text.toString().toInt()
                 )
-                val autoLogoutTime =
-                    binding.autoLogoutTime.text.toString().toLong() * 1000
+                val autoLogoutTime = binding.autoLogoutTime.text.toString().toLong() * 1000
                 MMKVConstants.KEY_AUTO_LOGOUT_TIME.saveMMKVData(
                     autoLogoutTime
                 )
                 CountdownTimer.reset(autoLogoutTime)
-                if (hardwareModeChanged) {
+                if (hardwareModeChanged || rfidScanModeChanged) {
                     showToast(CommonUtils.getStr("save_success"))
                     RestartAppEvent.sendRestartAppEvent()
                 } else {

+ 49 - 0
app/src/main/java/com/grkj/iscs_mc/features/main/viewmodel/material_manage/MaterialsExchangeWaitAndFinishViewModel.kt

@@ -2,12 +2,16 @@ package com.grkj.iscs_mc.features.main.viewmodel.material_manage
 
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.liveData
+import com.grkj.data.common.MainDomainData
 import com.grkj.data.domain.logic.IMaterialsLogic
+import com.grkj.data.enums.RFIDScanMode
 import com.grkj.data.local.dos.IsMaterials
 import com.grkj.data.local.dos.IsMaterialsLoan
 import com.grkj.data.local.dos.IsMaterialsType
 import com.grkj.iscs_mc.features.main.entity.IsMaterialsEntity
 import com.grkj.ui_base.base.BaseViewModel
+import com.sik.sikcore.data.BeanUtils
+import com.sik.sikcore.date.TimeUtils
 import dagger.hilt.android.lifecycle.HiltViewModel
 import kotlinx.coroutines.Dispatchers
 import javax.inject.Inject
@@ -29,6 +33,11 @@ class MaterialsExchangeWaitAndFinishViewModel @Inject constructor(
      */
     var returnMaterials: List<IsMaterialsEntity> = listOf()
 
+    /**
+     * 异常物资
+     */
+    var exceptionMaterials: List<String> = listOf()
+
     /**
      * 物资数据
      */
@@ -49,6 +58,46 @@ class MaterialsExchangeWaitAndFinishViewModel @Inject constructor(
      */
     fun scanMaterialsExchange(): LiveData<Boolean> {
         return liveData(Dispatchers.IO) {
+            val currentRfidList = RFIDScanMode.getCurrentRFIDScanMode().scanMaterials()
+            borrowMaterials =
+                materialsData.filter { it.materialsRfid !in currentRfidList && it.materialsId !in materialsLoan.map { it.materialsId } }
+                    .mapNotNull { BeanUtils.copyProperties(it, IsMaterialsEntity::class.java) }
+            returnMaterials =
+                materialsData.filter {
+                    it.materialsRfid in currentRfidList
+                }.filter { it.materialsId in materialsLoan.map { it.materialsId } }
+                    .mapNotNull { BeanUtils.copyProperties(it, IsMaterialsEntity::class.java) }
+            materialsLogic.borrowMaterials(borrowMaterials.filter { it.status != "3" }.map {
+                IsMaterialsLoan().apply {
+                    materialsId = it.materialsId
+                }
+            })
+            materialsLogic.borrowExceptionMaterials(borrowMaterials.filter { it.status == "3" }
+                .map { it.materialsId })
+            materialsLogic.returnMaterials(materialsLoan.filter { it.materialsId in returnMaterials.map { it.materialsId } }
+                .apply {
+                    forEach {
+                        it.status = "1"
+                        it.restitutionUserId = MainDomainData.userInfo?.userId ?: 0L
+                        it.actualRestitutionTime =
+                            TimeUtils.nowString(TimeUtils.DEFAULT_DATE_HOUR_MIN_SEC_FORMAT)
+                    }
+                })
+            materialsLogic.updateMaterials(materialsData.apply {
+                forEach {
+                    it.loanState =
+                        if (it.materialsId in returnMaterials.map { it.materialsId }) "1" else "0"
+                }
+            })
+            exceptionMaterials =
+                currentRfidList.filter { it !in materialsData.map { it.materialsRfid } }
+            materialsLogic.insertMaterials(exceptionMaterials.map {
+                IsMaterials().apply {
+                    materialsRfid = it
+                    status = "3"
+                    loanState = "1"
+                }
+            })
             emit(true)
         }
     }

+ 32 - 0
app/src/main/res/layout/fragment_settings.xml

@@ -146,6 +146,38 @@
                 android:layout_below="@+id/hardware_mode_tv"
                 android:layout_alignLeft="@+id/hardware_mode_tv"
                 android:layout_marginTop="@dimen/iscs_space_2"
+                android:drawableRight="@mipmap/icon_drop_down"
+                android:background="@drawable/bg_common_input"
+                android:inputType="number"
+                android:maxLines="1"
+                android:minWidth="@dimen/iscs_input_min_width"
+                android:paddingHorizontal="@dimen/iscs_space_2"
+                android:paddingVertical="2dp"
+                android:singleLine="true"
+                android:textColor="?attr/colorTextPrimary"
+                android:textSize="@dimen/iscs_text_md"
+                app:formRole="field" />
+            <TextView
+                android:id="@+id/rfid_scan_mode_tv"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_below="@+id/auto_logout_time"
+                android:layout_marginTop="@dimen/iscs_space_4"
+                android:textColor="?attr/colorTextPrimary"
+                android:textSize="@dimen/iscs_text_md"
+                app:formRole="label"
+                app:i18nKey='@{"rfid_scan_mode"}'
+                app:markPosition="start"
+                app:required="true" />
+
+            <TextView
+                android:id="@+id/rfid_scan_mode"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_below="@+id/rfid_scan_mode_tv"
+                android:layout_alignLeft="@+id/rfid_scan_mode_tv"
+                android:layout_marginTop="@dimen/iscs_space_2"
+                android:drawableRight="@mipmap/icon_drop_down"
                 android:background="@drawable/bg_common_input"
                 android:inputType="number"
                 android:maxLines="1"

+ 50 - 21
app/src/main/res/layout/item_materials.xml

@@ -1,33 +1,62 @@
 <?xml version="1.0" encoding="utf-8"?>
 <layout xmlns:android="http://schemas.android.com/apk/res/android">
 
-    <LinearLayout
+    <RelativeLayout
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:layout_marginLeft="@dimen/iscs_space_4"
-        android:gravity="center_horizontal"
-        android:orientation="vertical">
+        android:layout_marginLeft="@dimen/iscs_space_4">
 
-        <ImageView
-            android:id="@+id/materials_picture"
-            android:layout_width="120dp"
-            android:layout_height="120dp"
-            android:scaleType="fitCenter" />
-
-        <TextView
-            android:id="@+id/materials_name"
+        <LinearLayout
+            android:id="@+id/main_data_layout"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:layout_marginTop="@dimen/iscs_space_2"
-            android:textColor="?attr/colorTextPrimary"
-            android:textSize="@dimen/iscs_text_md" />
+            android:gravity="center_horizontal"
+            android:orientation="vertical">
+
+            <ImageView
+                android:id="@+id/materials_picture"
+                android:layout_width="120dp"
+                android:layout_height="120dp"
+                android:scaleType="fitCenter" />
+
+            <TextView
+                android:id="@+id/materials_name"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="@dimen/iscs_space_2"
+                android:textColor="?attr/colorTextPrimary"
+                android:textSize="@dimen/iscs_text_md" />
 
-        <TextView
-            android:id="@+id/materials_rfid"
+            <TextView
+                android:id="@+id/materials_rfid"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="@dimen/iscs_space_2"
+                android:textColor="?attr/colorTextPrimary"
+                android:textSize="@dimen/iscs_text_xs" />
+        </LinearLayout>
+
+        <FrameLayout
+            android:id="@+id/mask_layout"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:layout_marginTop="@dimen/iscs_space_2"
-            android:textColor="?attr/colorTextPrimary"
-            android:textSize="@dimen/iscs_text_xs" />
-    </LinearLayout>
+            android:layout_alignLeft="@+id/main_data_layout"
+            android:layout_alignTop="@+id/main_data_layout"
+            android:layout_alignRight="@+id/main_data_layout"
+            android:layout_alignBottom="@+id/main_data_layout"
+            android:background="?attr/colorMask"
+            android:visibility="gone">
+
+            <TextView
+                android:id="@+id/materials_type"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:background="?attr/colorStatusRed"
+                android:gravity="center"
+                android:paddingHorizontal="@dimen/iscs_space_2"
+                android:paddingVertical="@dimen/iscs_space_1"
+                android:textColor="?attr/colorTextPrimary"
+                android:textSize="@dimen/iscs_text_md" />
+        </FrameLayout>
+    </RelativeLayout>
 </layout>

+ 4 - 0
data/src/main/java/com/grkj/data/common/MMKVConstants.kt

@@ -23,4 +23,8 @@ object MMKVConstants {
      * 硬件模式
      */
     const val KEY_HARDWARE_MODE = "key_hardware_mode"
+    /**
+     * RFID扫描模式
+     */
+    const val KEY_RFID_SCAN_MODE = "key_rfid_scan_mode"
 }

+ 25 - 0
data/src/main/java/com/grkj/data/domain/logic/IMaterialsLogic.kt

@@ -38,11 +38,21 @@ interface IMaterialsLogic {
      */
     fun insertMaterials(isMaterials: IsMaterials)
 
+    /**
+     * 批量新增物资
+     */
+    fun insertMaterials(isMaterials: List<IsMaterials>)
+
     /**
      * 更新物资
      */
     fun updateMaterials(isMaterials: IsMaterials)
 
+    /**
+     * 批量更新物资
+     */
+    fun updateMaterials(isMaterials: List<IsMaterials>)
+
     /**
      * 获取物资
      */
@@ -62,4 +72,19 @@ interface IMaterialsLogic {
      * 获取借出物资
      */
     fun getMaterialsLoan(): List<IsMaterialsLoan>
+
+    /**
+     * 添加领取物资
+     */
+    fun borrowMaterials(materialsLoan: List<IsMaterialsLoan>)
+
+    /**
+     * 归还物资
+     */
+    fun returnMaterials(materialsLoan: List<IsMaterialsLoan>)
+
+    /**
+     * 取出异常物资
+     */
+    fun borrowExceptionMaterials(isMaterials: List<Long>)
 }

+ 20 - 0
data/src/main/java/com/grkj/data/domain/logic/impl/MaterialsLogic.kt

@@ -28,6 +28,14 @@ class MaterialsLogic @Inject constructor(
         materialsRepository.insertMaterials(isMaterials)
     }
 
+    override fun insertMaterials(isMaterials: List<IsMaterials>) {
+        materialsRepository.insertMaterials(isMaterials)
+    }
+
+    override fun updateMaterials(isMaterials: List<IsMaterials>) {
+        materialsRepository.updateMaterials(isMaterials)
+    }
+
     override fun getMaterials(): List<IsMaterials> {
         return materialsRepository.getMaterials()
     }
@@ -44,6 +52,18 @@ class MaterialsLogic @Inject constructor(
         return materialsRepository.getMaterialsLoan()
     }
 
+    override fun borrowExceptionMaterials(isMaterials: List<Long>) {
+        materialsRepository.borrowExceptionMaterials(isMaterials)
+    }
+
+    override fun borrowMaterials(materialsLoan: List<IsMaterialsLoan>) {
+        materialsRepository.updateMaterialsLoan(materialsLoan)
+    }
+
+    override fun returnMaterials(materialsLoan: List<IsMaterialsLoan>) {
+        materialsRepository.updateMaterialsLoan(materialsLoan)
+    }
+
     override fun updateMaterials(isMaterials: IsMaterials) {
         materialsRepository.updateMaterials(isMaterials)
     }

+ 3 - 5
data/src/main/java/com/grkj/data/enums/HardwareMode.kt

@@ -8,8 +8,8 @@ import com.sik.sikcore.extension.getMMKVData
 /**
  * 硬件模式
  */
-enum class HardwareMode {
-    CAN;
+enum class HardwareMode(val iHardwareHelper: IHardwareHelper) {
+    CAN(CanHardwareHelper());
 
     companion object {
         /**
@@ -18,9 +18,7 @@ enum class HardwareMode {
         fun getCurrentHardwareMode(): IHardwareHelper {
             val currentHardwareMode =
                 valueOf(MMKVConstants.KEY_HARDWARE_MODE.getMMKVData(CAN.name))
-            return when (currentHardwareMode) {
-                CAN -> CanHardwareHelper()
-            }
+            return currentHardwareMode.iHardwareHelper
         }
     }
 }

+ 25 - 0
data/src/main/java/com/grkj/data/enums/RFIDScanMode.kt

@@ -0,0 +1,25 @@
+package com.grkj.data.enums
+
+import com.grkj.data.common.MMKVConstants
+import com.grkj.data.hardware.IRFIDScanHelper
+import com.grkj.data.hardware.can.CanHardwareHelper
+import com.grkj.data.hardware.uhf.UHFRFIDScanHelper
+import com.sik.sikcore.extension.getMMKVData
+
+/**
+ * RFID扫描模式
+ */
+enum class RFIDScanMode(val iRfidScanHelper: IRFIDScanHelper) {
+    UHF(UHFRFIDScanHelper());
+
+    companion object {
+        /**
+         * 根据当前RFID扫描模式获取RFID扫描帮助类
+         */
+        fun getCurrentRFIDScanMode(): IRFIDScanHelper {
+            val currentRFIDScanMode =
+                valueOf(MMKVConstants.KEY_RFID_SCAN_MODE.getMMKVData(UHF.name))
+            return currentRFIDScanMode.iRfidScanHelper
+        }
+    }
+}

+ 20 - 0
data/src/main/java/com/grkj/data/hardware/IRFIDScanHelper.kt

@@ -0,0 +1,20 @@
+package com.grkj.data.hardware
+
+/**
+ * RFID扫描帮助类
+ */
+interface IRFIDScanHelper {
+    /**
+     * 初始化
+     */
+    fun init()
+    /**
+     * 扫描物资
+     */
+    suspend fun scanMaterials():List<String>
+
+    /**
+     * 释放资源
+     */
+    fun release()
+}

+ 2 - 10
data/src/main/java/com/grkj/data/hardware/can/CanReadyPlugin.kt

@@ -10,13 +10,11 @@ import org.slf4j.Logger
 import org.slf4j.LoggerFactory
 import kotlin.coroutines.resume
 import kotlin.coroutines.suspendCoroutine
-import kotlin.math.log
 
 class CanReadyPlugin : CommPlugin {
     private val logger: Logger = LoggerFactory.getLogger(CanReadyPlugin::class.java)
 
     /** 轮询哪些节点 */
-    private val scanRange: IntRange = 0..2
     private val activeNodes = mutableSetOf<Int>()
 
     /** 周期 */
@@ -61,13 +59,7 @@ class CanReadyPlugin : CommPlugin {
     private fun startPollingSingleLoop() {
         pollJob?.cancel()
         pollJob = scope.launch {
-            logger.info("CAN poll single-loop start, nodes={}", scanRange)
-            // 每个节点独立的 RFID 读取节流时间戳
-            val lastRfidAt = LongArray(scanRange.last + 1) { 0L }
-
-            for (size in scanRange) {
-                val nodeId = 1 shl size
-                logger.info("扫描NodeId:${nodeId}")
+            NodeIdHelper.scanRange { nodeId ->
                 safeRead(CanCommands.Common.getDeviceType(nodeId))?.let {
                     activeNodes.add(nodeId)
                     CanHelper.addNode(nodeId, it.payload[0].toInt())
@@ -124,7 +116,7 @@ class CanReadyPlugin : CommPlugin {
     /** 一次性读取:超时返回 null,不抛异常、不打崩循环 */
     private suspend fun safeRead(
         req: SdoRequest.Read,
-        timeoutMs: Long = 1500
+        timeoutMs: Long = 100
     ): SdoResponse.ReadData? {
         return try {
             withTimeoutOrNull(timeoutMs) {

+ 81 - 0
data/src/main/java/com/grkj/data/hardware/can/NodeIdHelper.kt

@@ -0,0 +1,81 @@
+package com.grkj.data.hardware.can
+
+/**
+ * NodeId 计算器:
+ * 以“置位个数递增 + 位索引字典序”的顺序枚举 8 位拨码的所有组合。
+ * 例如:
+ *  k=1: 00000001, 00000010, ..., 10000000
+ *  k=2: 00000011, 00000101, ..., 10000001, 00000110, ..., 11000000
+ *  ...
+ *  k=8: 11111111
+ */
+object NodeIdHelper {
+
+    /** 总位数(1..8 对应 bit0..bit7) */
+    private const val MAX_BITS = 8
+
+    /**
+     * 枚举所有非零掩码,按置位个数递增(1..8),
+     * 同一置位个数内按“位索引组合字典序”输出。
+     *
+     * @param emit 对每个掩码调用一次(0x01..0xFF),顺序如上。
+     */
+    suspend fun scanRange(emit: suspend (Int) -> Unit) {
+        // 置位个数 k:1..8
+        for (k in 1..MAX_BITS) {
+            generateKBitMasks(k, MAX_BITS, emit)
+        }
+    }
+
+    /**
+     * 生成固定置位个数 k 的所有掩码(组合字典序)。
+     * 例如 k=2, n=8:
+     * 索引组合 (0,1) → 00000011, (0,2) → 00000101, ..., (6,7) → 11000000
+     */
+    private suspend fun generateKBitMasks(
+        k: Int,
+        n: Int,
+        emit: suspend (Int) -> Unit
+    ) {
+        // 组合索引数组,初始为 [0,1,2,...,k-1]
+        val idx = IntArray(k) { it }
+        while (true) {
+            // 根据当前索引组合生成掩码
+            var mask = 0
+            for (i in 0 until k) {
+                mask = mask or (1 shl idx[i])
+            }
+            emit(mask)
+
+            // 生成下一个组合(典型字典序组合生成算法)
+            var pos = k - 1
+            while (pos >= 0 && idx[pos] == pos + n - k) pos--
+            if (pos < 0) break
+            idx[pos]++
+            for (j in pos + 1 until k) {
+                idx[j] = idx[j - 1] + 1
+            }
+        }
+    }
+
+    /**
+     * 如果你不需要挂起回调,也可以拿到一个 Sequence 方便 for-each。
+     */
+    fun scanRangeSeq(): Sequence<Int> = sequence {
+        for (k in 1..MAX_BITS) {
+            // 复用相同生成逻辑,但以 yield 返回
+            val idx = IntArray(k) { it }
+            while (true) {
+                var mask = 0
+                for (i in 0 until k) mask = mask or (1 shl idx[i])
+                yield(mask)
+
+                var pos = k - 1
+                while (pos >= 0 && idx[pos] == pos + MAX_BITS - k) pos--
+                if (pos < 0) break
+                idx[pos]++
+                for (j in pos + 1 until k) idx[j] = idx[j - 1] + 1
+            }
+        }
+    }
+}

+ 135 - 0
data/src/main/java/com/grkj/data/hardware/uhf/UHFRFIDScanHelper.kt

@@ -0,0 +1,135 @@
+package com.grkj.data.hardware.uhf
+
+import com.grkj.data.hardware.IRFIDScanHelper
+import com.grkj.data.utils.event.ToastEvent
+import com.grkj.shared.utils.i18n.I18nManager
+import com.uhf.api.cls.Reader
+import com.uhf.api.cls.Reader.READER_ERR
+import com.uhf.api.cls.Reader.TAGINFO
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+
+/**
+ * UHF
+ * 久佰特物资柜RFID资料
+ */
+class UHFRFIDScanHelper : IRFIDScanHelper {
+    private val logger: Logger = LoggerFactory.getLogger(UHFRFIDScanHelper::class.java)
+
+    companion object {
+        /**
+         * 串口地址
+         */
+        private const val ADDRESS: String = "/dev/USB"
+
+        /**
+         * 串口波特率
+         */
+        private const val ANT_PORT: Int = 921600
+
+        /**
+         * 天线数量
+         */
+        private const val ANT_SCNT: Int = 4
+
+        /**
+         * 盘点超时时间
+         */
+        private const val TIMEOUT = 200
+
+        /**
+         *
+         */
+        private var ants: IntArray = intArrayOf(1)
+
+        /**
+         *
+         */
+        private var tagCnt: IntArray = IntArray(1)
+
+        /**
+         *
+         */
+        private var err: READER_ERR? = null
+    }
+
+    /**
+     * 是否初始化完成
+     */
+    private var isInit: Boolean = false
+
+    /**
+     * 阅读器
+     */
+    val reader: Reader by lazy { Reader() }
+    override fun init() {
+        val errCode = reader.InitReader_Notype("$ADDRESS:$ANT_PORT", ANT_SCNT)
+        if (errCode != Reader.READER_ERR.MT_OK_ERR) {
+            logger.info("读取器初始化失败:${errCode}")
+            return
+        } else {
+            isInit = true
+        }
+        printRFIDScanData()
+        setParams()
+    }
+
+    private fun printRFIDScanData() {
+        val version = Reader.GetSDKVersion()
+        logger.info("版本号:${version}")
+    }
+
+    /**
+     * 设置参数
+     */
+    private fun setParams() {
+        val sessionParams = intArrayOf(1)
+        val qParams = intArrayOf(-1)
+        reader.ParamSet(Reader.Mtr_Param.MTR_PARAM_POTL_GEN2_SESSION, sessionParams)
+        reader.ParamSet(Reader.Mtr_Param.MTR_PARAM_POTL_GEN2_Q, qParams)
+    }
+
+    override suspend fun scanMaterials(): List<String> {
+        val materialsRfidSet = mutableSetOf<String>()
+        repeat(10) {
+            err = reader.TagInventory_Raw(ants, ants.size, TIMEOUT.toShort(), tagCnt)
+            if (err === READER_ERR.MT_OK_ERR) {
+                println("invenotry ---------------- tagcnt:" + tagCnt[0])
+                for (j in 0..<tagCnt[0]) {
+                    val tag_: TAGINFO = reader.TAGINFO()
+                    err = reader.GetNextTag(tag_)
+                    // 获取缓冲区标签一旦出错,不可以继续获取,重新盘点标签。
+                    // if failed when called getnexttag,you should inventory again.
+                    if (err !== READER_ERR.MT_OK_ERR) break
+                    materialsRfidSet.add(Reader.bytes_Hexstr(tag_.EpcId))
+                    logger.info(
+                        ("epcid:" + Reader.bytes_Hexstr(tag_.EpcId) + " ant:" + tag_.AntennaID + "  fre:"
+                                + tag_.Frequency + "  rssi:" + tag_.RSSI)
+                    )
+                }
+            }
+            if (err !== READER_ERR.MT_OK_ERR) {
+                logger.info("inventory tags err:" + err)
+            }
+        }
+        return materialsRfidSet.toList()
+    }
+
+    override fun release() {
+        if (checkInit()) {
+            reader.CloseReader()
+        }
+    }
+
+    /**
+     * 检查是否初始化
+     */
+    private fun checkInit(): Boolean {
+        if (!isInit) {
+            ToastEvent.sendToastEvent(I18nManager.t("rfid_helper_not_init"))
+            return false
+        }
+        return true
+    }
+}

+ 20 - 4
data/src/main/java/com/grkj/data/local/dao/MaterialsDao.kt

@@ -79,11 +79,13 @@ interface MaterialsDao {
     /**
      * 检查是否有正在使用的物资
      */
-    @Query("""
+    @Query(
+        """
         select count(*) from is_materials_loan iml
         left join is_materials im on iml.materials_id = im.materials_id
         where iml.status = 0 and iml.del_flag = 0 and im.materials_id in (:materialsId)
-    """)
+    """
+    )
     fun checkMaterialsInBorrowed(materialsId: List<Long>): Int
 
     /**
@@ -101,9 +103,23 @@ interface MaterialsDao {
     /**
      * 获取借出物资
      */
-    @Query("""
+    @Query(
+        """
         select * from is_materials_loan iml
         where del_flag = 0 and iml.restitution_required = 0 and iml.status = 0
-    """)
+    """
+    )
     fun getMaterialsLoan(): List<IsMaterialsLoan>
+
+    /**
+     * 批量更新物资
+     */
+    @Update(onConflict = OnConflictStrategy.REPLACE)
+    fun updateMaterials(isMaterials: List<IsMaterials>)
+
+    /**
+     * 批量新增物资
+     */
+    @Insert(onConflict = OnConflictStrategy.REPLACE)
+    fun insertMaterials(materials: List<IsMaterials>)
 }

+ 4 - 2
data/src/main/java/com/grkj/data/local/dos/IsMaterialsLoan.kt

@@ -3,6 +3,8 @@ package com.grkj.data.local.dos
 import androidx.room.ColumnInfo
 import androidx.room.Entity
 import androidx.room.PrimaryKey
+import com.grkj.data.common.MainDomainData
+import com.sik.sikcore.date.TimeUtils
 
 /**
  * 物资借出表
@@ -21,7 +23,7 @@ open class IsMaterialsLoan : BaseBean() {
 
     /** 领取人ID */
     @ColumnInfo(name = "loan_user_id")
-    var loanUserId: Long = 0L
+    var loanUserId: Long = MainDomainData.userInfo?.userId ?: 0L
 
     /** 领取柜ID */
     @ColumnInfo(name = "loan_from_id")
@@ -29,7 +31,7 @@ open class IsMaterialsLoan : BaseBean() {
 
     /** 领取时间 */
     @ColumnInfo(name = "loan_time")
-    var loanTime: String? = null
+    var loanTime: String? = TimeUtils.nowString(TimeUtils.DEFAULT_DATE_HOUR_MIN_SEC_FORMAT)
 
     /** 提醒时间 */
     @ColumnInfo(name = "reminder_time")

+ 15 - 0
data/src/main/java/com/grkj/data/repository/IMaterialsRepository.kt

@@ -72,4 +72,19 @@ interface IMaterialsRepository {
      * 更新领取物资
      */
     fun updateMaterialsLoan(materialsLoan: List<IsMaterialsLoan>)
+
+    /**
+     * 批量更新物资
+     */
+    fun updateMaterials(isMaterials: List<IsMaterials>)
+
+    /**
+     * 批量新增物资
+     */
+    fun insertMaterials(materials: List<IsMaterials>)
+
+    /**
+     * 取出异常物资
+     */
+    fun borrowExceptionMaterials(materials: List<Long>)
 }

+ 12 - 0
data/src/main/java/com/grkj/data/repository/impl/network/NetworkMaterialsRepository.kt

@@ -64,4 +64,16 @@ class NetworkMaterialsRepository @Inject constructor(): BaseRepository(), IMater
     override fun updateMaterialsLoan(materialsLoan: List<IsMaterialsLoan>) {
         TODO("Not yet implemented")
     }
+
+    override fun updateMaterials(isMaterials: List<IsMaterials>) {
+        TODO("Not yet implemented")
+    }
+
+    override fun insertMaterials(materials: List<IsMaterials>) {
+        TODO("Not yet implemented")
+    }
+
+    override fun borrowExceptionMaterials(materials: List<Long>) {
+        TODO("Not yet implemented")
+    }
 }

+ 12 - 0
data/src/main/java/com/grkj/data/repository/impl/standard/StandardMaterialsRepository.kt

@@ -44,6 +44,18 @@ class StandardMaterialsRepository @Inject constructor(
         materialsDao.updateMaterialsLoan(materialsLoan)
     }
 
+    override fun insertMaterials(materials: List<IsMaterials>) {
+        materialsDao.insertMaterials(materials)
+    }
+
+    override fun borrowExceptionMaterials(materials: List<Long>) {
+        materialsDao.deleteMaterials(materials)
+    }
+
+    override fun updateMaterials(isMaterials: List<IsMaterials>) {
+        materialsDao.updateMaterials(isMaterials)
+    }
+
     override fun getMaterialsLoan(): List<IsMaterialsLoan> {
         return materialsDao.getMaterialsLoan()
     }

+ 1 - 0
ui-base/src/main/res/values/attrs.xml

@@ -12,6 +12,7 @@
     <attr name="colorSecBg" format="color" />
     <attr name="colorSurface" format="color" />
     <attr name="colorBackground" format="color" />
+    <attr name="colorMask" format="color" />
 
     <!-- 白/黑(含透明度) -->
     <attr name="colorWhite" format="color" />

+ 1 - 0
ui-base/src/main/res/values/theme.xml

@@ -8,6 +8,7 @@
         <item name="colorSecBg">@color/palette_dark_gray_bright</item>
         <item name="colorInputBg">@color/palette_dark_gray_bright_1</item>
         <item name="colorSurface">@color/palette_surface_dark</item>
+        <item name="colorMask">@color/white30</item>
 
         <!-- 基础白/黑与透明 -->
         <item name="colorWhite">@color/palette_white</item>