Просмотр исходного кода

refactor(更新)
- 作业流程完善

周文健 5 месяцев назад
Родитель
Сommit
35bd63cc76
54 измененных файлов с 1438 добавлено и 526 удалено
  1. 6 2
      app/src/main/java/com/grkj/iscs/features/main/activity/MainActivity.kt
  2. 30 11
      app/src/main/java/com/grkj/iscs/features/main/fragment/job_manage/JobExecuteFragment.kt
  3. 21 1
      app/src/main/java/com/grkj/iscs/features/main/viewmodel/MainViewModel.kt
  4. 38 16
      app/src/main/java/com/grkj/iscs/features/main/viewmodel/job_manage/JobExecuteViewModel.kt
  5. 1 0
      app/src/main/res/values-en/strings.xml
  6. 1 0
      app/src/main/res/values-zh/strings.xml
  7. 1 0
      app/src/main/res/values/strings.xml
  8. 17 0
      data/src/main/java/com/grkj/data/check_data/ICheckDataMode.kt
  9. 34 0
      data/src/main/java/com/grkj/data/check_data/impl/ForceMultiLockMode.kt
  10. 34 0
      data/src/main/java/com/grkj/data/check_data/impl/ForceSharedLockMode.kt
  11. 20 0
      data/src/main/java/com/grkj/data/check_data/impl/MixedMode.kt
  12. 50 12
      data/src/main/java/com/grkj/data/dao/HardwareDao.kt
  13. 25 0
      data/src/main/java/com/grkj/data/dao/JobTicketDao.kt
  14. 11 0
      data/src/main/java/com/grkj/data/data/CommonConstants.kt
  15. 1 1
      data/src/main/java/com/grkj/data/data/DictConstants.kt
  16. 7 1
      data/src/main/java/com/grkj/data/data/EventConstants.kt
  17. 6 1
      data/src/main/java/com/grkj/data/data/MMKVConstants.kt
  18. 53 0
      data/src/main/java/com/grkj/data/di/CheckDataModeModule.kt
  19. 15 0
      data/src/main/java/com/grkj/data/enums/LockPointModeEnum.kt
  20. 2 1
      data/src/main/java/com/grkj/data/enums/LockStepEnum.kt
  21. 1 0
      data/src/main/java/com/grkj/data/model/dos/IsJobTicket.kt
  22. 1 1
      data/src/main/java/com/grkj/data/model/dos/IsKey.kt
  23. 9 0
      data/src/main/java/com/grkj/data/model/local/LockData.kt
  24. 9 0
      data/src/main/java/com/grkj/data/model/local/PointData.kt
  25. 64 6
      data/src/main/java/com/grkj/data/model/local/WorkTicketGet.kt
  26. 12 15
      data/src/main/java/com/grkj/data/repository/IHardwareRepository.kt
  27. 26 0
      data/src/main/java/com/grkj/data/repository/IJobTicketRepository.kt
  28. 9 38
      data/src/main/java/com/grkj/data/repository/impl/HardwareRepository.kt
  29. 133 21
      data/src/main/java/com/grkj/data/repository/impl/JobTicketRepository.kt
  30. 1 1
      ui-base/src/main/java/com/grkj/ui_base/base/BaseActivity.kt
  31. 515 232
      ui-base/src/main/java/com/grkj/ui_base/business/BleBusinessManager.kt
  32. 2 2
      ui-base/src/main/java/com/grkj/ui_base/business/ModbusBusinessManager.kt
  33. 1 1
      ui-base/src/main/java/com/grkj/ui_base/config/ISCSConfig.kt
  34. 3 0
      ui-base/src/main/java/com/grkj/ui_base/utils/ble/BleCmdManager.kt
  35. 207 103
      ui-base/src/main/java/com/grkj/ui_base/utils/ble/BleConnectionManager.kt
  36. 1 1
      ui-base/src/main/java/com/grkj/ui_base/utils/event/BottomNavVisibilityEvent.kt
  37. 1 1
      ui-base/src/main/java/com/grkj/ui_base/utils/event/CurrentModeEvent.kt
  38. 1 1
      ui-base/src/main/java/com/grkj/ui_base/utils/event/DeviceExceptionEvent.kt
  39. 1 1
      ui-base/src/main/java/com/grkj/ui_base/utils/event/DeviceTakeUpdateEvent.kt
  40. 1 1
      ui-base/src/main/java/com/grkj/ui_base/utils/event/GetTicketStatusEvent.kt
  41. 1 2
      ui-base/src/main/java/com/grkj/ui_base/utils/event/JumpViewEvent.kt
  42. 1 1
      ui-base/src/main/java/com/grkj/ui_base/utils/event/LoadingEvent.kt
  43. 1 1
      ui-base/src/main/java/com/grkj/ui_base/utils/event/RFIDCardReadEvent.kt
  44. 1 1
      ui-base/src/main/java/com/grkj/ui_base/utils/event/SwitchCollectionUpdateEvent.kt
  45. 24 0
      ui-base/src/main/java/com/grkj/ui_base/utils/event/TicketFinishedEvent.kt
  46. 1 1
      ui-base/src/main/java/com/grkj/ui_base/utils/event/UpdateTicketProgressEvent.kt
  47. 1 1
      ui-base/src/main/java/com/grkj/ui_base/utils/modbus/DockBean.kt
  48. 2 2
      ui-base/src/main/java/com/grkj/ui_base/utils/modbus/ModBusCMDHelper.kt
  49. 27 44
      ui-base/src/main/java/com/grkj/ui_base/utils/modbus/ModBusController.kt
  50. 1 1
      ui-base/src/main/java/com/grkj/ui_base/utils/modbus/ModBusManager.kt
  51. 1 1
      ui-base/src/main/java/com/grkj/ui_base/utils/modbus/PortManager.kt
  52. 2 0
      ui-base/src/main/res/values-en/strings.xml
  53. 2 0
      ui-base/src/main/res/values-zh/strings.xml
  54. 2 0
      ui-base/src/main/res/values/strings.xml

+ 6 - 2
app/src/main/java/com/grkj/iscs/features/main/activity/MainActivity.kt

@@ -13,11 +13,10 @@ import com.grkj.data.enums.RoleFunctionalPermissionsEnum
 import com.grkj.data.model.local.TabConfig
 import com.grkj.iscs.R
 import com.grkj.iscs.databinding.ActivityMainBinding
-import com.grkj.iscs.features.main.entity.RoleManageFunctionalPermissionsEntity
 import com.grkj.iscs.features.main.viewmodel.MainViewModel
 import com.grkj.shared.model.EventBean
 import com.grkj.ui_base.base.BaseActivity
-import com.grkj.ui_base.data.EventConstants
+import com.grkj.data.data.EventConstants
 import com.grkj.ui_base.utils.event.BottomNavVisibilityEvent
 import com.grkj.ui_base.utils.extension.toByteArrays
 import com.grkj.ui_base.utils.extension.toHexStrings
@@ -110,6 +109,11 @@ class MainActivity() : BaseActivity<ActivityMainBinding>() {
         }
     }
 
+    override fun initData() {
+        super.initData()
+        viewModel.bleIndicate()
+    }
+
     override fun dispatchKeyEvent(event: KeyEvent): Boolean {
         if (event.action == KeyEvent.ACTION_UP && event.source == InputDevice.SOURCE_KEYBOARD) {
             // 检测到回车开始处理

+ 30 - 11
app/src/main/java/com/grkj/iscs/features/main/fragment/job_manage/JobExecuteFragment.kt

@@ -4,7 +4,6 @@ import android.graphics.drawable.GradientDrawable
 import android.widget.LinearLayout
 import androidx.core.view.isVisible
 import androidx.fragment.app.viewModels
-import androidx.lifecycle.ViewModelProvider
 import com.drake.brv.BindingAdapter
 import com.drake.brv.annotaion.DividerOrientation
 import com.drake.brv.utils.dividerSpace
@@ -12,7 +11,6 @@ import com.drake.brv.utils.grid
 import com.drake.brv.utils.linear
 import com.drake.brv.utils.models
 import com.drake.brv.utils.setup
-import com.grkj.data.enums.LockModeEnum
 import com.grkj.data.enums.LockStepEnum
 import com.grkj.data.enums.RoleEnum
 import com.grkj.data.model.vo.IsJobTicketPointsDataVo
@@ -28,7 +26,8 @@ import com.grkj.iscs.databinding.ItemJobExecuteStepBinding
 import com.grkj.iscs.features.main.viewmodel.job_manage.JobExecuteViewModel
 import com.grkj.shared.model.EventBean
 import com.grkj.ui_base.base.BaseFragment
-import com.grkj.ui_base.data.EventConstants
+import com.grkj.data.data.EventConstants
+import com.grkj.ui_base.dialog.TipDialog
 import com.grkj.ui_base.utils.event.RFIDCardReadEvent
 import com.kongzue.dialogx.dialogs.PopTip
 import com.sik.sikcore.data.GlobalDataTempStore
@@ -79,7 +78,9 @@ class JobExecuteFragment : BaseFragment<FragmentJobExecuteBinding>() {
             }
         }
         binding.toUnlock.setDebouncedClickListener {
-            viewModel.toUnLock().observe(this) {}
+            viewModel.toUnLock().observe(this) {
+
+            }
         }
         binding.waitToColockRv.grid(3).dividerSpace(10, DividerOrientation.GRID).setup {
             addType<IsJobTicketUserDataVo>(R.layout.item_job_execute_colock)
@@ -214,15 +215,33 @@ class JobExecuteFragment : BaseFragment<FragmentJobExecuteBinding>() {
 
     override fun onEvent(event: EventBean<Any>) {
         super.onEvent(event)
-        if (event.code == EventConstants.EVENT_RFID_CARD_READ) {
-            (event.data as RFIDCardReadEvent).let {
-                logger.info("读卡器获取卡片RFID:${it.rfidNo}")
-                if (viewModel.currentStepData?.stepIndex == LockStepEnum.COLOCK.type) {
-                    logger.info("添加共锁")
-                } else {
-                    logger.info("当前阶段无法共锁")
+        when (event.code) {
+            EventConstants.EVENT_RFID_CARD_READ -> {
+                (event.data as RFIDCardReadEvent).let {
+                    logger.info("读卡器获取卡片RFID:${it.rfidNo}")
+                    if (viewModel.currentStepData?.stepIndex == LockStepEnum.COLOCK.type) {
+                        logger.info("添加共锁")
+                    } else {
+                        logger.info("当前阶段无法共锁")
+                    }
                 }
             }
+
+            EventConstants.EVENT_UPDATE_TICKET_PROGRESS -> {
+                getData()
+            }
+
+            EventConstants.EVENT_TICKET_FINISHED -> {
+                TipDialog.show(
+                    title = getString(com.grkj.ui_base.R.string.action_hint),
+                    msg = getString(com.grkj.ui_base.R.string.job_already_finished),
+                    dialogType = TipDialog.DialogType.ERROR,
+                    showCancel = false,
+                    onConfirmClick = {
+                        navController.popBackStack()
+                    }
+                )
+            }
         }
     }
 

+ 21 - 1
app/src/main/java/com/grkj/iscs/features/main/viewmodel/MainViewModel.kt

@@ -1,6 +1,10 @@
 package com.grkj.iscs.features.main.viewmodel
 
 import com.grkj.ui_base.base.BaseViewModel
+import com.grkj.ui_base.business.BleBusinessManager
+import com.grkj.ui_base.utils.ble.BleBean
+import com.grkj.ui_base.utils.ble.BleConnectionManager
+import com.grkj.ui_base.utils.ble.BleIndicateListener
 import dagger.hilt.android.lifecycle.HiltViewModel
 import javax.inject.Inject
 
@@ -8,5 +12,21 @@ import javax.inject.Inject
  * 首页界面模型
  */
 @HiltViewModel
-class MainViewModel @Inject constructor(): BaseViewModel() {
+class MainViewModel @Inject constructor() : BaseViewModel() {
+    /**
+     * 蓝牙监听
+     */
+    fun bleIndicate() {
+        BleConnectionManager.addBLeIndicateListener(this, object : BleIndicateListener {
+            override fun handleRsp(
+                bleBean: BleBean,
+                byteArray: ByteArray,
+                isNeedLoading: Boolean,
+                prepareDoneCallBack: ((Boolean, BleBean?) -> Unit)?
+            ) {
+                BleBusinessManager.handleRsp(bleBean, byteArray, isNeedLoading, prepareDoneCallBack)
+            }
+
+        })
+    }
 }

+ 38 - 16
app/src/main/java/com/grkj/iscs/features/main/viewmodel/job_manage/JobExecuteViewModel.kt

@@ -13,6 +13,7 @@ import com.grkj.data.model.vo.IsJobTicketStepDataVo
 import com.grkj.data.model.vo.IsJobTicketUserDataVo
 import com.grkj.data.model.vo.UserManageVo
 import com.grkj.data.repository.IJobTicketRepository
+import com.grkj.iscs.R
 import com.grkj.ui_base.base.BaseViewModel
 import com.grkj.ui_base.business.BleBusinessManager
 import com.grkj.ui_base.business.ModbusBusinessManager
@@ -21,6 +22,7 @@ import com.grkj.ui_base.utils.CommonUtils
 import com.grkj.ui_base.utils.event.LoadingEvent
 import com.grkj.ui_base.utils.modbus.DeviceConst
 import com.grkj.ui_base.utils.modbus.ModBusController
+import com.kongzue.dialogx.dialogs.PopTip
 import dagger.hilt.android.lifecycle.HiltViewModel
 import javax.inject.Inject
 import kotlinx.coroutines.Dispatchers
@@ -192,28 +194,48 @@ class JobExecuteViewModel @Inject constructor(val jobTicketRepository: IJobTicke
                 CommonUtils.getStr(com.grkj.ui_base.R.string.check_key_and_lock),
                 true
             )
-            ModbusBusinessManager.checkEquipCount(0, true) { keyMap, _ ->
-                LoadingEvent.sendLoadingEvent()
-                if (keyMap == null) {
-                    TipDialog.show(
-                        CommonUtils.getStr(com.grkj.ui_base.R.string.action_failed).toString(),
-                        CommonUtils.getStr(com.grkj.ui_base.R.string.no_available_key).toString(),
-                        TipDialog.DialogType.ERROR,
-                        countDownTime = 10
+            if (checkBeforeToUnlock()) {
+                ModbusBusinessManager.checkEquipCount(0, true) { keyMap, _ ->
+                    LoadingEvent.sendLoadingEvent()
+                    if (keyMap == null) {
+                        TipDialog.show(
+                            CommonUtils.getStr(com.grkj.ui_base.R.string.action_failed).toString(),
+                            CommonUtils.getStr(com.grkj.ui_base.R.string.no_available_key)
+                                .toString(),
+                            TipDialog.DialogType.ERROR,
+                            countDownTime = 10
+                        )
+                        return@checkEquipCount
+                    }
+                    val deviceTakeUpdate = DeviceTakeUpdate(
+                        DeviceConst.DEVICE_TYPE_KEY,
+                        ticketId,
+                        keyMap.second?.rfid ?: ""
                     )
-                    return@checkEquipCount
+                    BleBusinessManager.handleGiveKey(deviceTakeUpdate)
                 }
-                val deviceTakeUpdate = DeviceTakeUpdate(
-                    DeviceConst.DEVICE_TYPE_KEY,
-                    ticketId,
-                    keyMap.second?.rfid ?: ""
-                )
-                BleBusinessManager.handleGiveKey(deviceTakeUpdate)
+                emit(true)
             }
-            emit(true)
         }
     }
 
+    /**
+     * 解锁前检查
+     * 判断交叉点位是否有作业在上锁
+     */
+    private fun checkBeforeToUnlock(): Boolean {
+        val tickets = jobTicketRepository.samePointLockingTicket(ticketId)
+        tickets.firstOrNull()?.let {
+            PopTip.tip(
+                CommonUtils.getStr(
+                    R.string.please_wait_ticket_name_lock_complete,
+                    args = listOf<String>(it.ticketName).toTypedArray()
+                )
+            )
+        }
+        return tickets.isEmpty()
+    }
+
     /**
      * 更新共锁人状态
      */

+ 1 - 0
app/src/main/res/values-en/strings.xml

@@ -169,4 +169,5 @@
     <string name="job_manage_delete_failed">Delete selected job failed</string>
     <string name="please_select_job">Please select job</string>
     <string name="check_delete_job">Do you want to delete selected job</string>
+    <string name="please_wait_ticket_name_lock_complete">Please wait ticket [%1$d] locking complete</string>
 </resources>

+ 1 - 0
app/src/main/res/values-zh/strings.xml

@@ -169,4 +169,5 @@
     <string name="job_manage_delete_failed">无法删除选中的作业</string>
     <string name="please_select_job">请选择作业</string>
     <string name="check_delete_job">您确认要删除作业吗</string>
+    <string name="please_wait_ticket_name_lock_complete">请等待[%1$d]上锁完成</string>
 </resources>

+ 1 - 0
app/src/main/res/values/strings.xml

@@ -168,4 +168,5 @@
     <string name="job_manage_delete_failed">无法删除选中的作业</string>
     <string name="please_select_job">请选择作业</string>
     <string name="check_delete_job">您确认要删除作业吗</string>
+    <string name="please_wait_ticket_name_lock_complete">请等待[%1$d]上锁完成</string>
 </resources>

+ 17 - 0
data/src/main/java/com/grkj/data/check_data/ICheckDataMode.kt

@@ -0,0 +1,17 @@
+package com.grkj.data.check_data
+
+import com.grkj.data.model.req.LockPointUpdateReq
+import com.grkj.data.model.vo.IsJobTicketPointsDataVo
+
+/**
+ * 检查数据模式接口
+ */
+interface ICheckDataMode {
+    /**
+     * 检查上锁点位是否符合要求
+     */
+    fun checkUpdatePointData(
+        updateReq: LockPointUpdateReq,
+        isTicketPoint: List<IsJobTicketPointsDataVo>
+    ): String
+}

+ 34 - 0
data/src/main/java/com/grkj/data/check_data/impl/ForceMultiLockMode.kt

@@ -0,0 +1,34 @@
+package com.grkj.data.check_data.impl
+
+import com.grkj.data.check_data.ICheckDataMode
+import com.grkj.data.model.req.LockPointUpdateReq
+import com.grkj.data.model.vo.IsJobTicketPointsDataVo
+import com.grkj.data.repository.IHardwareRepository
+import com.grkj.data.repository.IJobTicketRepository
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * 强制多锁模式
+ */
+@Singleton
+class ForceMultiLockMode @Inject constructor(
+    val jobTicketRepository: IJobTicketRepository,
+    val hardwareRepository: IHardwareRepository
+) :
+    ICheckDataMode {
+    override fun checkUpdatePointData(
+        updateReq: LockPointUpdateReq,
+        isTicketPoint: List<IsJobTicketPointsDataVo>
+    ): String {
+        val pointNfcDataList =
+            hardwareRepository.getPointNfcDataByPointIds(isTicketPoint.map { it.pointId })
+        val pointData = pointNfcDataList.first { it.pointNfc == updateReq.pointNfc }
+        val lockNfcDataList =
+            hardwareRepository.getJobTicketPointLockNfcDataListByPointId(pointData.pointId)
+        if (updateReq.lockNfc in lockNfcDataList.map { it.lockNfc }) {
+            return "点位的锁不能与已存在的锁相同"
+        }
+        return ""
+    }
+}

+ 34 - 0
data/src/main/java/com/grkj/data/check_data/impl/ForceSharedLockMode.kt

@@ -0,0 +1,34 @@
+package com.grkj.data.check_data.impl
+
+import com.grkj.data.check_data.ICheckDataMode
+import com.grkj.data.model.req.LockPointUpdateReq
+import com.grkj.data.model.vo.IsJobTicketPointsDataVo
+import com.grkj.data.repository.IHardwareRepository
+import com.grkj.data.repository.IJobTicketRepository
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * 强制共享锁模式
+ */
+@Singleton
+class ForceSharedLockMode @Inject constructor(
+    val jobTicketRepository: IJobTicketRepository,
+    val hardwareRepository: IHardwareRepository
+) :
+    ICheckDataMode {
+    override fun checkUpdatePointData(
+        updateReq: LockPointUpdateReq,
+        isTicketPoint: List<IsJobTicketPointsDataVo>
+    ): String {
+        val pointNfcDataList =
+            hardwareRepository.getPointNfcDataByPointIds(isTicketPoint.map { it.pointId })
+        val pointData = pointNfcDataList.first { it.pointNfc == updateReq.pointNfc }
+        val lockNfcDataList =
+            hardwareRepository.getJobTicketPointLockNfcDataListByPointId(pointData.pointId)
+        if (updateReq.lockNfc !in lockNfcDataList.map { it.lockNfc }) {
+            return "点位的锁必须与已存在的锁相同"
+        }
+        return ""
+    }
+}

+ 20 - 0
data/src/main/java/com/grkj/data/check_data/impl/MixedMode.kt

@@ -0,0 +1,20 @@
+package com.grkj.data.check_data.impl
+
+import com.grkj.data.check_data.ICheckDataMode
+import com.grkj.data.model.req.LockPointUpdateReq
+import com.grkj.data.model.vo.IsJobTicketPointsDataVo
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * 混合模式
+ */
+@Singleton
+class MixedMode @Inject constructor() : ICheckDataMode {
+    override fun checkUpdatePointData(
+        updateReq: LockPointUpdateReq,
+        isTicketPoint: List<IsJobTicketPointsDataVo>
+    ): String {
+        return ""
+    }
+}

+ 50 - 12
data/src/main/java/com/grkj/data/dao/HardwareDao.kt

@@ -8,6 +8,8 @@ import com.grkj.data.model.dos.IsJobCard
 import com.grkj.data.model.dos.IsKey
 import com.grkj.data.model.dos.IsLock
 import com.grkj.data.model.dos.IsLockCabinetSlots
+import com.grkj.data.model.local.LockData
+import com.grkj.data.model.local.PointData
 import com.sik.sikcore.date.TimeUtils
 
 /**
@@ -39,18 +41,6 @@ interface HardwareDao {
     @Query("select * from is_key where key_nfc = :rfid")
     fun getKeyInfoByRfid(rfid: String): IsKey
 
-    /**
-     * 更新钥匙取出
-     */
-    @Query("update is_job_ticket_key set key_id = :keyId,key_status = '1',collect_time = :takeTime,update_time = :takeTime where ticket_id = :ticketId")
-    fun updateKeyTake(ticketId: Long, keyId: Long, takeTime: String)
-
-    /**
-     * 更新钥匙归还
-     */
-    @Query("update is_job_ticket_key set key_id = :keyId,key_status = '2',give_back_time = :giveBackTime,update_time = :giveBackTime where ticket_id = :ticketId")
-    fun updateKeyReturn(ticketId: Long, keyId: Long, giveBackTime: String)
-
     /**
      * 更新挂锁取出
      */
@@ -130,4 +120,52 @@ interface HardwareDao {
      */
     @Query("delete from is_job_card where user_id = :userId and card_code = :cardCode")
     fun deleteCardByUserIdAndCardCode(userId: Long, cardCode: String)
+
+    /**
+     * 根据锁nfc获取锁信息
+     */
+    @Query("select * from is_lock where lock_nfc = :lockRfid")
+    fun getLockDataByLockRfid(lockRfid: String): IsLock?
+
+    /**
+     * 根据钥匙的nfc找到钥匙的数据
+     */
+    @Query("select * from is_key where key_nfc = :keyRfid")
+    fun getKeyDataByKeyRfid(keyRfid: String): IsKey?
+
+    /**
+     * 根据点位id获取点位nfc数据
+     */
+    @Query(
+        """
+        select iip.point_id as pointId,
+        irt.rfid as pointNfc
+        from is_isolation_point iip
+        left join is_rfid_token irt on iip.rfid_id = irt.rfid_id
+        where iip.point_id in (:pointIds)
+    """
+    )
+    fun getPointNfcDataByPointIds(pointIds: List<Long?>): List<PointData>
+
+    /**
+     * 根据点位id获取这个点位上的锁的数据
+     */
+    @Query(
+        """
+        select il.lock_id as lockId,
+        il.lock_nfc as lockNfc
+        from is_lock il 
+        left join is_job_ticket_points ijtp on ijtp.lock_id = il.lock_id
+        left join is_job_ticket ijt on ijt.ticket_id = ijtp.ticket_id
+        where ijtp.point_id = :pointId and ijt.ticket_status = 3
+    """
+    )
+    fun getJobTicketPointLockNfcDataListByPointId(pointId: Long): List<LockData>
+
+    /**
+     * 根据点位nfc查询点位id
+     */
+    @Query("select point_id from is_isolation_point iip left join is_rfid_token irt on iip.rfid_id = irt.rfid_id where irt.rfid = :pointNfc")
+    fun getPointIdByPointNfc(pointNfc: String?): Long
+
 }

+ 25 - 0
data/src/main/java/com/grkj/data/dao/JobTicketDao.kt

@@ -312,4 +312,29 @@ interface JobTicketDao {
      */
     @Update
     fun colockerStatusChange(jobTicketUser: IsJobTicketUser)
+
+    /**
+     * 根据点位获取正在上锁的作业票
+     */
+    @Query(
+        """
+        select ijt.* 
+        from is_job_ticket ijt 
+        left join is_job_ticket_points ijtp on ijt.ticket_id = ijtp.ticket_id
+        where ijt.ticket_status = "2" and ijtp.point_id in (:pointIds)
+    """
+    )
+    fun getLockingTicketByPointId(pointIds: List<Long>): List<IsJobTicketDataVo>
+
+    /**
+     * 更新点位信息
+     */
+    @Update
+    fun updateJobTicketPointData(points: MutableList<IsJobTicketPoints>)
+
+    /**
+     * 更新钥匙信息
+     */
+    @Update
+    fun updateIsJobTicketKey(keys: List<IsJobTicketKey>)
 }

+ 11 - 0
data/src/main/java/com/grkj/data/data/CommonConstants.kt

@@ -0,0 +1,11 @@
+package com.grkj.data.data
+
+/**
+ * 通用常量
+ */
+object CommonConstants {
+    /**
+     * 作业票数据丢失
+     */
+    const val TICKET_LOST = "{\"msg\":\"作业票数据丢失啦!\",\"code\":500}"
+}

+ 1 - 1
ui-base/src/main/java/com/grkj/ui_base/data/DictConstants.kt → data/src/main/java/com/grkj/data/data/DictConstants.kt

@@ -1,4 +1,4 @@
-package com.grkj.ui_base.data
+package com.grkj.data.data
 
 /**
  * 字典参数

+ 7 - 1
ui-base/src/main/java/com/grkj/ui_base/data/EventConstants.kt → data/src/main/java/com/grkj/data/data/EventConstants.kt

@@ -1,4 +1,4 @@
-package com.grkj.ui_base.data
+package com.grkj.data.data
 
 /**
  * 通知常量
@@ -15,6 +15,7 @@ object EventConstants {
      */
     const val EVENT_UPDATE_TICKET_PROGRESS: Int = 100_000_002
 
+
     /**
      * 跳转事件,设计导航切换
      */
@@ -25,6 +26,11 @@ object EventConstants {
      */
     const val EVENT_BOTTOM_NAV_VISIBILITY: Int = 100_000_004
 
+    /**
+     * 作业票已结束
+     */
+    const val EVENT_TICKET_FINISHED = 100_000_005
+
     //---------------------------作业票------------------------
     const val EVENT_GET_TICKET_STATUS: Int = 100_001_001
 

+ 6 - 1
ui-base/src/main/java/com/grkj/ui_base/data/MMKVConstants.kt → data/src/main/java/com/grkj/data/data/MMKVConstants.kt

@@ -1,4 +1,4 @@
-package com.grkj.ui_base.data
+package com.grkj.data.data
 
 /**
  * 持久化存储的KEY
@@ -18,4 +18,9 @@ object MMKVConstants {
      * 区域是否打开
      */
     const val WORKSTATION_OPEN = "workstation_open"
+
+    /**
+     * 更新隔离点锁模式
+     */
+    const val UPDATE_LOCK_POINT_MODE= "update_lock_point_mode"
 }

+ 53 - 0
data/src/main/java/com/grkj/data/di/CheckDataModeModule.kt

@@ -0,0 +1,53 @@
+package com.grkj.data.di
+
+import com.grkj.data.check_data.ICheckDataMode
+import com.grkj.data.check_data.impl.ForceMultiLockMode
+import com.grkj.data.check_data.impl.ForceSharedLockMode
+import com.grkj.data.check_data.impl.MixedMode
+import com.grkj.data.data.MMKVConstants
+import com.grkj.data.enums.LockPointModeEnum
+import com.sik.sikcore.extension.getMMKVData
+import com.tencent.mmkv.MMKV
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+// 1. 在 Module 中引入 MMKV 和枚举
+@Module
+@InstallIn(SingletonComponent::class)
+object CheckDataModeModule {
+
+    // 2. 把三种实现类都注册给 Hilt
+    //    这三行可省略,只要它们有 @Inject 构造、@Singleton,就能被 Hilt 识别。
+    @Provides
+    fun provideMixedMode(m: MixedMode): MixedMode = m
+    @Provides
+    fun provideForceSharedLockMode(s: ForceSharedLockMode): ForceSharedLockMode = s
+    @Provides
+    fun provideForceMultiLockMode(m: ForceMultiLockMode): ForceMultiLockMode = m
+
+    // 3. 最关键:根据 MMKV 里储存的模式 type(0/1/2)选出枚举,并返回对应实现
+    @Provides
+    @Singleton
+    fun provideCheckDataMode(
+        mixed: MixedMode,
+        shared: ForceSharedLockMode,
+        multi: ForceMultiLockMode
+    ): ICheckDataMode {
+        // 从 MMKV 读出存储的类型(整数),例如 key="lock_mode_type"
+        val savedType =
+            MMKVConstants.UPDATE_LOCK_POINT_MODE.getMMKVData(LockPointModeEnum.MIXED_MODE.type)
+
+        // 把整数映射成枚举
+        val modeEnum = LockPointModeEnum.values().firstOrNull { it.type == savedType }
+            ?: LockPointModeEnum.MIXED_MODE
+
+        return when (modeEnum) {
+            LockPointModeEnum.FORCE_SHARED_LOCK -> shared
+            LockPointModeEnum.FORCE_MULTI_LOCK -> multi
+            LockPointModeEnum.MIXED_MODE -> mixed
+        }
+    }
+}

+ 15 - 0
data/src/main/java/com/grkj/data/enums/LockPointModeEnum.kt

@@ -0,0 +1,15 @@
+package com.grkj.data.enums
+
+import com.grkj.data.check_data.ICheckDataMode
+
+/**
+ * 锁定点位模式
+ */
+enum class LockPointModeEnum(
+    val type: Int,
+    val description: String,
+) {
+    FORCE_SHARED_LOCK(0, "强制共享锁模式"),
+    FORCE_MULTI_LOCK(1, "强制多锁模式"),
+    MIXED_MODE(2, "混合模式"), ;
+}

+ 2 - 1
data/src/main/java/com/grkj/data/enums/LockStepEnum.kt

@@ -7,5 +7,6 @@ enum class LockStepEnum(val type: Int, val description: String) {
     SELECT_MEMBER(0, "选择人员"),
     LOCK(1, "上锁"),
     COLOCK(2, "共锁"),
-    UNLOCK(3, "解锁");
+    UNLOCK(3, "解锁"),
+    UNLOCKED(4, "已解锁");
 }

+ 1 - 0
data/src/main/java/com/grkj/data/model/dos/IsJobTicket.kt

@@ -44,6 +44,7 @@ open class IsJobTicket : BaseBean() {
     @ColumnInfo("ticket_content")
     var ticketContent: String? = null
 
+    //(0未开始 1待上锁 2进行中 3待解锁 4已解锁 5已结束6已取消)
     @ColumnInfo("ticket_status")
     var ticketStatus: String? = "0"
 

+ 1 - 1
data/src/main/java/com/grkj/data/model/dos/IsKey.kt

@@ -7,7 +7,7 @@ class IsKey : BaseBean() {
 
     @PrimaryKey(autoGenerate = true)
     @ColumnInfo(name = "key_id")
-    var keyId: Int = 0
+    var keyId: Long = 0
 
     @ColumnInfo(name = "key_code")
     var keyCode: String = ""

+ 9 - 0
data/src/main/java/com/grkj/data/model/local/LockData.kt

@@ -0,0 +1,9 @@
+package com.grkj.data.model.local
+
+/**
+ * 锁信息
+ */
+class LockData {
+    var lockId: Long = 0
+    var lockNfc: String = ""
+}

+ 9 - 0
data/src/main/java/com/grkj/data/model/local/PointData.kt

@@ -0,0 +1,9 @@
+package com.grkj.data.model.local
+
+/**
+ * 点位数据
+ */
+class PointData {
+    var pointId: Long = 0
+    var pointNfc: String = ""
+}

+ 64 - 6
data/src/main/java/com/grkj/data/model/local/WorkTicketGet.kt

@@ -1,16 +1,24 @@
 package com.grkj.data.model.local
 
+import com.grkj.data.di.RepositoryManager
+import com.grkj.data.repository.IJobTicketRepository
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
 import kotlin.collections.forEach
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
 
 class WorkTicketGet {
     /**
      * 权限卡号
      */
     var cardNo: String? = null
+
     /**
      * 用户密码
      */
     var password: String? = null
+
     /**
      * 工作票数组
      */
@@ -21,10 +29,12 @@ class WorkTicketGet {
          * 工作票号
          */
         var taskCode: String? = null
+
         /**
          * 工作票ID
          */
         var taskId: String? = null
+
         /**
          * 工作票下挂任务列表
          */
@@ -35,23 +45,28 @@ class WorkTicketGet {
              * 任务ID
              */
             var dataId: Int? = null
+
             /**
              * 工作点位RFID号
              */
             var equipRfidNo: String? = null
+
             /**
              * 锁RFID号
              */
             var infoRfidNo: String? = null
+
             /**
              * 任务目标 0:挂锁 1:解锁
              */
             var target: Int? = null
+
             /**
              * 任务当前状态:
              * 0—挂锁;1—解锁;2-无操作
              */
             var status: Int? = null
+
             /**
              * 任务操作状态
              * 0—待完成;1—已完成
@@ -76,14 +91,57 @@ class WorkTicketGet {
     }
 
     // 判断是否有closed字段为0的
-    fun hasFinished(): Boolean {
-        data?.forEach {
-            it.dataList?.forEach {
-                if (it.closed == 0) {
-                    return false
+    suspend fun hasFinished(): Pair<Boolean, Boolean> {
+        // 如果没有数据,默认本地完成且远程未完成
+        if (data.isNullOrEmpty()) return true to false
+
+        var anyRemoteFinished = false
+        //点位的rfid是否被刷到锁的rfid
+        val pointRfidEqLockRfid = data?.any { dataDto ->
+            dataDto.dataList?.any { dataListDto ->
+                dataDto.dataList?.map { it.equipRfidNo }?.contains(dataListDto.infoRfidNo) == true
+            } == true
+        }
+        if (pointRfidEqLockRfid == true) {
+            return false to false
+        }
+        for (item in data) {
+            // 1. 调用回调接口获取远程的 ticketStatus,并根据情况设置两个标志
+            val (localTicketFinish, remoteTicketFinished) = suspendCoroutine<Pair<Boolean, Boolean>> { cont ->
+                RepositoryManager.jobTicketRepo.getTicketDetail(item.taskCode?.toLong() ?: 0) { res ->
+                    logger.info("作业票状态: ${res?.ticketStatus}")
+                    if (res?.ticketStatus in listOf(
+                            "5",
+                            "6"
+                        )
+                    ) {
+                        // 只要远程状态是 5 或 6,就认为 remoteTicketFinished = true
+                        cont.resume(true to true)
+                    } else {
+                        // 否则,用本地 dataList 的所有 closed 字段判断 localTicketFinish,本地没完成就返回 false
+                        val allClosed = item.dataList?.all { it.closed == 1 } == true
+                        cont.resume(allClosed to false)
+                    }
                 }
             }
+
+            // 如果这一次远程已经为 true,就标记下来
+            if (remoteTicketFinished) {
+                anyRemoteFinished = true
+            }
+
+            // 如果本地没走完,就可以立刻返回:本地 false + 这时 anyRemoteFinished 的值
+            if (!localTicketFinish) {
+                return false to anyRemoteFinished
+            }
+            // 本地都走完才继续下一轮
         }
-        return true
+
+        // 如果所有项本地都完成,就返回 true;远程是否有一次命中,就看 anyRemoteFinished
+        return true to anyRemoteFinished
+    }
+
+    companion object {
+        val logger: Logger = LoggerFactory.getLogger(WorkTicketGet::class.java)
     }
 }

+ 12 - 15
data/src/main/java/com/grkj/data/repository/IHardwareRepository.kt

@@ -1,5 +1,7 @@
 package com.grkj.data.repository
 
+import com.grkj.data.model.local.LockData
+import com.grkj.data.model.local.PointData
 import com.grkj.data.model.req.LockPointUpdateReq
 import com.grkj.data.model.req.LockTakeUpdateReq
 import com.grkj.data.model.res.CabinetSlotsRes
@@ -29,21 +31,6 @@ interface IHardwareRepository {
      */
     fun canReturn(): Boolean
 
-    /**
-     * 上报钥匙取出
-     */
-    fun updateKeyTake(ticketId: Long, keyNfc: String, serialNo: String, callback: (Boolean) -> Unit)
-
-    /**
-     * 上报钥匙归还
-     */
-    fun updateKeyReturn(
-        ticketId: Long,
-        keyNfc: String,
-        serialNo: String,
-        callback: (Boolean, String) -> Unit
-    )
-
     /**
      * 批量更新隔离点的锁位状态
      */
@@ -86,4 +73,14 @@ interface IHardwareRepository {
      * 根据用户id和卡片码删除数据
      */
     fun deleteCardByUserIdAndCardCode(userId: Long, cardCode: String)
+
+    /**
+     * 获取点位nfc数据
+     */
+    fun getPointNfcDataByPointIds(pointIds: List<Long?>): List<PointData>
+
+    /**
+     * 根据点位id获取已上锁的同点位的锁
+     */
+    fun getJobTicketPointLockNfcDataListByPointId(pointId: Long): List<LockData>
 }

+ 26 - 0
data/src/main/java/com/grkj/data/repository/IJobTicketRepository.kt

@@ -1,5 +1,6 @@
 package com.grkj.data.repository
 
+import com.grkj.data.model.req.LockPointUpdateReq
 import com.grkj.data.model.res.StepDetailRes
 import com.grkj.data.model.res.TicketDetailRes
 import com.grkj.data.model.vo.IsJobTicketDataVo
@@ -157,4 +158,29 @@ interface IJobTicketRepository {
      * 更新共锁人状态
      */
     fun colockerStatusChange(jobTicketUserDataVo: IsJobTicketUserDataVo)
+
+    /**
+     * 交叉点位正在上锁的作业
+     */
+    fun samePointLockingTicket(ticketId: Long): List<IsJobTicketDataVo>
+
+    /**
+     * 批量更新点位信息
+     */
+    fun updateLockPointBatch(
+        lockPointsUpdateReq: List<LockPointUpdateReq>,
+        callback: (Boolean, String, Int) -> Unit
+    )
+
+    /**
+     * 更新钥匙归还
+     */
+    fun updateKeyReturn(
+        ticketId: Long,
+        keyNfc: String,
+        serialNo: String,
+        callback: (Boolean, String, Int) -> Unit
+    )
+
+    fun updateKeyTake(ticketId: Long, keyNfc: String, serialNo: String, callback: (Boolean) -> Unit)
 }

+ 9 - 38
data/src/main/java/com/grkj/data/repository/impl/HardwareRepository.kt

@@ -4,6 +4,8 @@ import com.grkj.data.dao.HardwareDao
 import com.grkj.data.dao.IsolationPointDao
 import com.grkj.data.enums.CommonDictDataEnum
 import com.grkj.data.model.dos.IsJobCard
+import com.grkj.data.model.local.LockData
+import com.grkj.data.model.local.PointData
 import com.grkj.data.repository.IHardwareRepository
 import com.grkj.data.model.req.LockPointUpdateReq
 import com.grkj.data.model.req.LockTakeUpdateReq
@@ -80,44 +82,6 @@ class HardwareRepository @Inject constructor(
         return true
     }
 
-    override fun updateKeyTake(
-        ticketId: Long,
-        keyNfc: String,
-        serialNo: String,
-        callback: (Boolean) -> Unit
-    ) {
-        val keyInfo = getKeyInfo(keyNfc)
-        keyInfo?.keyId?.let {
-            hardwareDao.updateKeyTake(
-                ticketId,
-                keyInfo.keyId,
-                TimeUtils.nowString(TimeUtils.DEFAULT_DATE_HOUR_MIN_SEC_FORMAT)
-            )
-            callback(true)
-        } ?: run {
-            callback(false)
-        }
-    }
-
-    override fun updateKeyReturn(
-        ticketId: Long,
-        keyNfc: String,
-        serialNo: String,
-        callback: (Boolean, String) -> Unit
-    ) {
-        val keyInfo = getKeyInfo(keyNfc)
-        keyInfo?.keyId?.let {
-            hardwareDao.updateKeyReturn(
-                ticketId,
-                keyInfo.keyId,
-                TimeUtils.nowString(TimeUtils.DEFAULT_DATE_HOUR_MIN_SEC_FORMAT)
-            )
-            callback(true, "")
-        } ?: run {
-            callback(false, "{\"msg\":\"作业票数据丢失啦!\",\"code\":500}")
-        }
-    }
-
     override fun updateLockPointBatch(
         lockPointUpdateData: MutableList<LockPointUpdateReq>,
         callback: (Boolean, String) -> Unit
@@ -203,4 +167,11 @@ class HardwareRepository @Inject constructor(
         hardwareDao.deleteCardByUserIdAndCardCode(userId,cardCode)
     }
 
+    override fun getPointNfcDataByPointIds(pointIds: List<Long?>): List<PointData> {
+        return hardwareDao.getPointNfcDataByPointIds(pointIds)
+    }
+
+    override fun getJobTicketPointLockNfcDataListByPointId(pointId: Long): List<LockData> {
+        return hardwareDao.getJobTicketPointLockNfcDataListByPointId(pointId)
+    }
 }

+ 133 - 21
data/src/main/java/com/grkj/data/repository/impl/JobTicketRepository.kt

@@ -1,5 +1,7 @@
 package com.grkj.data.repository.impl
 
+import com.grkj.data.check_data.ICheckDataMode
+import com.grkj.data.dao.HardwareDao
 import com.grkj.data.dao.JobTicketDao
 import com.grkj.data.enums.JobTicketStatusEnum
 import com.grkj.data.enums.LockModeEnum
@@ -11,6 +13,7 @@ import com.grkj.data.model.dos.IsJobTicketLock
 import com.grkj.data.model.dos.IsJobTicketPoints
 import com.grkj.data.model.dos.IsJobTicketStep
 import com.grkj.data.model.dos.IsJobTicketUser
+import com.grkj.data.model.req.LockPointUpdateReq
 import com.grkj.data.model.res.StepDetailRes
 import com.grkj.data.model.res.TicketDetailRes
 import com.grkj.data.model.vo.IsJobTicketDataVo
@@ -25,6 +28,7 @@ import com.grkj.data.model.vo.UserManageVo
 import com.grkj.data.repository.BaseRepository
 import com.grkj.data.repository.IJobTicketRepository
 import com.sik.sikcore.data.BeanUtils
+import com.sik.sikcore.date.TimeUtils
 import javax.inject.Inject
 import javax.inject.Singleton
 
@@ -32,7 +36,11 @@ import javax.inject.Singleton
  * 作业票实现
  */
 @Singleton
-class JobTicketRepository @Inject constructor(val jobTicketDao: JobTicketDao) : BaseRepository(),
+class JobTicketRepository @Inject constructor(
+    val jobTicketDao: JobTicketDao,
+    val hardwareDao: HardwareDao,
+    val checkDataMode: ICheckDataMode
+) : BaseRepository(),
     IJobTicketRepository {
     override fun createJob(
         selectedSopPoints: List<PointManageVo>,
@@ -77,14 +85,6 @@ class JobTicketRepository @Inject constructor(val jobTicketDao: JobTicketDao) :
         }
         jobTicketDao.saveIsJobTicketUser(ticketLockerUsers)
         jobTicketDao.saveIsJobTicketUser(ticketColockerUsers)
-        val ticketKeys = mutableListOf<IsJobTicketKey>().apply {
-            ticketPointIds.forEach {
-                val isJobTicketKey = IsJobTicketKey()
-                isJobTicketKey.ticketId = ticketId
-                add(isJobTicketKey)
-            }
-        }
-        jobTicketDao.saveIsJobTicketKey(ticketKeys)
         val ticketLocks = mutableListOf<IsJobTicketLock>().apply {
             ticketPointIds.forEach { point ->
                 val isJobTicketLock = IsJobTicketLock()
@@ -159,14 +159,6 @@ class JobTicketRepository @Inject constructor(val jobTicketDao: JobTicketDao) :
         }
         jobTicketDao.saveIsJobTicketUser(ticketLockerUsers)
         jobTicketDao.saveIsJobTicketUser(ticketColockerUsers)
-        val ticketKeys = mutableListOf<IsJobTicketKey>().apply {
-            ticketPointIds.forEach {
-                val isJobTicketKey = IsJobTicketKey()
-                isJobTicketKey.ticketId = ticketId
-                add(isJobTicketKey)
-            }
-        }
-        jobTicketDao.saveIsJobTicketKey(ticketKeys)
         val ticketLocks = mutableListOf<IsJobTicketLock>().apply {
             ticketPointIds.forEach { point ->
                 val isJobTicketLock = IsJobTicketLock()
@@ -312,20 +304,140 @@ class JobTicketRepository @Inject constructor(val jobTicketDao: JobTicketDao) :
     }
 
     override fun getAllJobSize(worstationId: Long?, startTime: String, endTime: String): Int {
-        return jobTicketDao.getAllJobSize(worstationId,startTime,endTime)
+        return jobTicketDao.getAllJobSize(worstationId, startTime, endTime)
     }
 
     override fun getInProgressJobSize(workstationId: Long?, selectedLockMode: String): Int {
-        return jobTicketDao.getInProgressJobSize(workstationId,selectedLockMode)
+        return jobTicketDao.getInProgressJobSize(workstationId, selectedLockMode)
     }
 
     override fun getLockedPointsData(current: Int, size: Int): List<PointManageVo> {
-        return jobTicketDao.getLockedPointsData(size,current*size)
+        return jobTicketDao.getLockedPointsData(size, current * size)
     }
 
     override fun colockerStatusChange(jobTicketUserDataVo: IsJobTicketUserDataVo) {
         val jobTicketUser = IsJobTicketUser()
-        BeanUtils.copyData<IsJobTicketUser>(jobTicketUserDataVo,jobTicketUser)
+        BeanUtils.copyData<IsJobTicketUser>(jobTicketUserDataVo, jobTicketUser)
         jobTicketDao.colockerStatusChange(jobTicketUser)
     }
+
+    override fun samePointLockingTicket(ticketId: Long): List<IsJobTicketDataVo> {
+        val points = jobTicketDao.getTicketPointsByTicketId(ticketId)
+        return jobTicketDao.getLockingTicketByPointId(points.map { it.pointId })
+    }
+
+    override fun updateLockPointBatch(
+        lockPointsUpdateReq: List<LockPointUpdateReq>,
+        callback: (Boolean, String, Int) -> Unit
+    ) {
+        var checkResult = ""
+        lockPointsUpdateReq.firstOrNull()?.ticketId?.toLong()?.let { ticketId ->
+            val isTicketPoints = jobTicketDao.getJobTicketPointsDataByTicketId(ticketId)
+            for (updateReq in lockPointsUpdateReq) {
+                if (updateReq.keyNfc == null) {
+                    callback(false, "钥匙nfc丢失", 500)
+                    return
+                }
+                if (updateReq.lockNfc == null) {
+                    callback(false, "挂锁nfc丢失", 500)
+                    return
+                }
+                val keyData = hardwareDao.getKeyDataByKeyRfid(updateReq.keyNfc)
+                if (keyData == null) {
+                    callback(false, "钥匙信息丢失", 500)
+                    return
+                }
+                val lockData = hardwareDao.getLockDataByLockRfid(updateReq.lockNfc)
+                if (lockData == null) {
+                    callback(false, "挂锁信息丢失", 500)
+                    return
+                }
+                val pointId = hardwareDao.getPointIdByPointNfc(updateReq.pointNfc)
+                val isTicketPoint = isTicketPoints.firstOrNull { it.pointId == pointId }
+                if (isTicketPoint == null) {
+                    callback(false, "隔离点信息丢失", 500)
+                    return
+                }
+                isTicketPoint.lockId = lockData.lockId
+                if (updateReq.target == 0) {
+                    checkResult = checkDataMode.checkUpdatePointData(updateReq, isTicketPoints)
+                    if (checkResult.isNotEmpty()) {
+                        break
+                    }
+                    isTicketPoint.lockedByKeyId = keyData.keyId
+                    isTicketPoint.pointStatus = "1"
+                    isTicketPoint.lockTime =
+                        TimeUtils.nowString(TimeUtils.DEFAULT_DATE_HOUR_MIN_SEC_FORMAT)
+                } else {
+                    isTicketPoint.unlockedByKeyId = keyData.keyId
+                    isTicketPoint.pointStatus = "2"
+                    isTicketPoint.unlockTime =
+                        TimeUtils.nowString(TimeUtils.DEFAULT_DATE_HOUR_MIN_SEC_FORMAT)
+                }
+            }
+            if (checkResult.isNotEmpty()) {
+                callback(false, checkResult, 500)
+                return
+            }
+            val isJobTicketPoints = mutableListOf<IsJobTicketPoints>()
+            isTicketPoints.forEach {
+                val isJobTicketPoint = IsJobTicketPoints()
+                BeanUtils.copyData(it, isJobTicketPoint)
+                isJobTicketPoints.add(isJobTicketPoint)
+            }
+            jobTicketDao.updateJobTicketPointData(isJobTicketPoints)
+            callback(true, "", 200)
+        } ?: callback(false, "作业票不存在", 500)
+    }
+
+    override fun updateKeyTake(
+        ticketId: Long,
+        keyNfc: String,
+        serialNo: String,
+        callback: (Boolean) -> Unit
+    ) {
+        val jobTicketData = jobTicketDao.getTicketDataByTicketId(ticketId)
+        val keyData = hardwareDao.getKeyDataByKeyRfid(keyNfc)
+        if (keyData == null) {
+            logger.info("钥匙信息不存在")
+            callback(false)
+            return
+        }
+        val isJobTicketKey = IsJobTicketKey()
+        isJobTicketKey.ticketId = ticketId
+        isJobTicketKey.keyId = keyData.keyId
+        if (jobTicketData?.ticketStatus == LockStepEnum.LOCK.type.toString()) {
+            isJobTicketKey.ticketType = 0
+        } else if (jobTicketData?.ticketStatus == LockStepEnum.UNLOCK.type.toString()) {
+            isJobTicketKey.ticketType = 1
+        }
+        isJobTicketKey.collectTime = TimeUtils.nowString(TimeUtils.DEFAULT_DATE_HOUR_MIN_SEC_FORMAT)
+        jobTicketDao.saveIsJobTicketKey(listOf(isJobTicketKey))
+    }
+
+    override fun updateKeyReturn(
+        ticketId: Long,
+        keyNfc: String,
+        serialNo: String,
+        callback: (Boolean, String, Int) -> Unit
+    ) {
+        val jobTicketData = jobTicketDao.getTicketDataByTicketId(ticketId)
+        val keyData = hardwareDao.getKeyDataByKeyRfid(keyNfc)
+        if (keyData == null) {
+            logger.info("钥匙信息不存在")
+            callback(false, "钥匙信息不存在", 500)
+            return
+        }
+
+        val isJobTicketKeys = jobTicketDao.getJobTicketKeyDataByTicketId(ticketId)
+        val isJobTicketKey = isJobTicketKeys.firstOrNull { it.keyId == keyData.keyId }
+        if (isJobTicketKey==null){
+            logger.info("作业的钥匙信息不存在")
+            callback(false, "作业的钥匙信息不存在", 500)
+            return
+        }
+        isJobTicketKey.keyStatus = "1"
+        isJobTicketKey.giveBackTime = TimeUtils.nowString(TimeUtils.DEFAULT_DATE_HOUR_MIN_SEC_FORMAT)
+        jobTicketDao.updateIsJobTicketKey(listOf(isJobTicketKey))
+    }
 }

+ 1 - 1
ui-base/src/main/java/com/grkj/ui_base/base/BaseActivity.kt

@@ -11,7 +11,7 @@ import androidx.navigation.NavController
 import androidx.navigation.fragment.NavHostFragment
 import com.google.android.material.bottomnavigation.BottomNavigationView
 import com.grkj.shared.model.EventBean
-import com.grkj.ui_base.data.EventConstants
+import com.grkj.data.data.EventConstants
 import com.grkj.ui_base.dialog.LoadingDialog
 import com.grkj.ui_base.utils.event.JumpViewEvent
 import com.grkj.ui_base.utils.event.LoadingEvent

+ 515 - 232
ui-base/src/main/java/com/grkj/ui_base/business/BleBusinessManager.kt

@@ -14,11 +14,13 @@ import com.grkj.data.model.req.LockPointUpdateReq
 import com.grkj.data.model.res.TicketDetailRes
 import com.grkj.shared.config.Constants
 import com.grkj.ui_base.R
-import com.grkj.ui_base.data.DictConstants
+import com.grkj.data.data.DictConstants
+import com.grkj.data.enums.LockStepEnum
 import com.grkj.ui_base.dialog.TipDialog
 import com.grkj.ui_base.utils.CommonUtils
 import com.grkj.ui_base.utils.Executor
 import com.grkj.ui_base.utils.SPUtils
+import com.grkj.ui_base.utils.ble.BleBean
 import com.grkj.ui_base.utils.ble.BleCmdManager
 import com.grkj.ui_base.utils.ble.BleConnectionManager
 import com.grkj.ui_base.utils.ble.BleConst
@@ -26,12 +28,15 @@ import com.grkj.ui_base.utils.ble.CustomBleWriteCallback
 import com.grkj.ui_base.utils.event.CurrentModeEvent
 import com.grkj.ui_base.utils.event.DeviceExceptionEvent
 import com.grkj.ui_base.utils.event.LoadingEvent
+import com.grkj.ui_base.utils.event.TicketFinishedEvent
 import com.grkj.ui_base.utils.event.UpdateTicketProgressEvent
 import com.grkj.ui_base.utils.extension.serialNo
+import com.grkj.ui_base.utils.extension.startsWith
 import com.grkj.ui_base.utils.modbus.DeviceConst
 import com.grkj.ui_base.utils.modbus.ModBusController
 import com.kongzue.dialogx.dialogs.PopTip
 import com.sik.sikcore.SIKCore
+import com.sik.sikcore.activity.ActivityTracker
 import com.sik.sikcore.thread.ThreadUtils
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.async
@@ -45,152 +50,174 @@ object BleBusinessManager {
     private val logger = LoggerFactory.getLogger(BleBusinessManager::class.java)
 
     /**
-     * 处理工作票完成情况
+     * 处理蓝牙返回
      */
-    fun handleTicketStatus(
-        bleDevice: BleDevice, byteArray: ByteArray, isNeedLoading: Boolean = false
+    fun handleRsp(
+        bleBean: BleBean,
+        byteArray: ByteArray,
+        isNeedLoading: Boolean = false,
+        prepareDoneCallBack: ((Boolean, BleBean?) -> Unit)?
     ) {
-        BleCmdManager.handleTicketStatus(bleDevice, byteArray) { ticketJson ->
-            if (ticketJson.isNullOrEmpty()) {
-                return@handleTicketStatus
-            }
-            if (isNeedLoading) LoadingEvent.sendLoadingEvent("工作票完成状态读取完成", true)
-            logger.info("Get ticket status complete : ${bleDevice.mac}")
-            // TD:Ticket Done
-            if (isNeedLoading) LoadingEvent.sendLoadingEvent("TD$ticketJson}", true)
-
-            val workTicketGet = Gson().fromJson(ticketJson, WorkTicketGet::class.java)
-
-            // 判断WorkTicketGet里是否有未完成的
-            if (workTicketGet.hasFinished()) {
-                Executor.delayOnMain(500) {
-                    handleKeyReturn(bleDevice, workTicketGet)
+        when {
+            // 获取令牌
+            byteArray.startsWith(BleConst.RSP_GET_TOKEN) -> BleCmdManager.handleToken(
+                bleBean.bleDevice, byteArray
+            ) { isSuccess ->
+                if (isSuccess) {
+                    prepareDoneCallBack?.invoke(true, bleBean)
                 }
-            } else {
-                // 当前策略:作业票未完成禁止归还钥匙
-                TipDialog.show(
-                    msg = CommonUtils.getStr(R.string.key_return_tip)!!,
-                    onConfirmClick = {
-                        LoadingEvent.sendLoadingEvent()
-                        PopTip.tip(CommonUtils.getStr(R.string.continue_the_ticket))
-                        BleManager.getInstance().disconnect(bleDevice)
-                        // 打开卡扣,防止初始化的时候选择不处理钥匙导致无法使用
-                        val dock = ModBusController.getDockByKeyMac(bleDevice.mac)
-                        val keyBean = dock?.getKeyList()?.find { it.mac == bleDevice.mac }
-                        keyBean?.let {
-                            ModBusController.controlKeyBuckle(true, keyBean.idx, dock.addr)
-                        }
-                    })
             }
-        }
-    }
-
-    /**
-     * 处理钥匙归还
-     */
-    private fun handleKeyReturn(bleDevice: BleDevice, workTicketGet: WorkTicketGet?) {
-        val dock = ModBusController.getDockByKeyMac(bleDevice.mac)
-        val keyBean = dock?.getKeyList()?.find { it.mac == bleDevice.mac }
-        keyBean?.let {
-            ModBusController.controlKeyBuckle(false, keyBean.idx, dock.addr)
-        }
-        // 上报隔离点状态
-        val keyNfc = ModBusController.getKeyByMac(bleDevice.mac)?.rfid
-        workTicketGet?.data?.forEach { data ->
-            val updateList = mutableListOf<LockPointUpdateReq>()
-            data.dataList?.forEach { dataListDTO ->
-                data.taskCode?.toLong()?.let {
-                    SPUtils.returnKey(it)
+            // 工作模式切换
+            byteArray.startsWith(BleConst.RSP_SWITCH_MODE) -> {
+                handleSwitchModeResult(byteArray, isNeedLoading) { res, job ->
+                    switchModeResultDeal(job.toInt(), res.toInt(), bleBean)
                 }
-                val updateVO = LockPointUpdateReq(
-                    data.taskCode?.toLong(),
-                    dataListDTO.infoRfidNo,
-                    dataListDTO.equipRfidNo,
-                    keyNfc!!,
-                    dataListDTO.target,
-                    dataListDTO.status
-                )
-                updateList.add(updateVO)
             }
+            // 工作票下发
+            byteArray.startsWith(BleConst.RSP_SEND_WORK_TICKET) -> handleWorkTicketResult(
+                bleBean, byteArray, isNeedLoading
+            )
+            // 获取设备当前状态
+            byteArray.startsWith(BleConst.RSP_CURRENT_STATUS) -> BleCmdManager.handleCurrentStatus(
+                byteArray
+            ) {
+                handleCurrentMode(CurrentModeEvent(bleBean, it))
+            }
+            // 获取设备工作票完成情况
+            byteArray.startsWith(BleConst.RSP_WORK_TICKET_RESULT) && byteArray[3] == 0x02.toByte() -> handleTicketStatus(
+                bleBean.bleDevice, byteArray, isNeedLoading
+            )
 
-            LoadingEvent.sendLoadingEvent()
-            PopTip.tip(R.string.key_return_success)
-            if (RepositoryManager.hardwareRepo.canReturn()) {
-                // 上报点位钥匙绑定
-                RepositoryManager.hardwareRepo.updateLockPointBatch(updateList) { isSuccess, msg ->
-                    if (isSuccess || msg == CommonUtils.getStr(R.string.lock_nfc_lost)) {
-                        data.taskCode?.toLong()?.let {
-                            UpdateTicketProgressEvent.sendUpdateTicketProgressEvent(it)
-                        }
-                        // 确认归还,切换为待机模式
-                        switchReadyMode(bleDevice)
-                    } else if (msg != CommonUtils.getStr(R.string.lock_nfc_lost)) {
-                        SPUtils.saveUpdateLockPoint(SIKCore.getApplication(), updateList)
+            byteArray.startsWith(BleConst.RSP_POWER_STATUS) -> {
+                val power = byteArray[4].toInt()
+                if (power < 50) {//如果电量小于50就打开仓位充电
+                    ModBusController.controlKeyCharge(true, bleBean.bleDevice.mac) {
+                        logger.info("钥匙: ${bleBean.bleDevice.mac} 开始充电")
                     }
-                }
-
-                // 上报钥匙归还
-                RepositoryManager.hardwareRepo.updateKeyReturn(
-                    data.taskCode?.toLong()!!, keyNfc!!, SIKCore.getApplication().serialNo()
-                ) { isSuccess, msg ->
-                    if (!isSuccess && msg != CommonUtils.getStr(R.string.ticket_lost)) {
-                        SPUtils.saveUpdateKeyReturn(
-                            SIKCore.getApplication(),
-                            UpdateKeyReturn(data.taskCode?.toLong()!!, keyNfc!!)
-                        )
+                } else {
+                    ModBusController.controlKeyCharge(false, bleBean.bleDevice.mac) {
+                        logger.info("钥匙: ${bleBean.bleDevice.mac} 关闭充电")
                     }
                 }
-            } else {
-                SPUtils.saveUpdateLockPoint(SIKCore.getApplication(), updateList)
-                SPUtils.saveUpdateKeyReturn(
-                    SIKCore.getApplication(), UpdateKeyReturn(data.taskCode?.toLong()!!, keyNfc!!)
-                )
-                // 保存待发数据,切换为待机模式
-                switchReadyMode(bleDevice)
             }
         }
     }
 
     /**
-     * 处理虚拟钥匙取出,如果作业的全部点位已经上锁更新钥匙的状态使用
+     * 工作模式切换结果
+     * job : 0x01:工作模式 0x02:待机模式
+     * res : 0x01:成功 0x02:失败
      */
-    fun handleVirtualKeyGive(ticketId: Long, keyNfc: String, done: () -> Unit) {
-        // 上报钥匙取出
-        RepositoryManager.hardwareRepo.updateKeyTake(
-            ticketId,
-            keyNfc,
-            SIKCore.getApplication().serialNo()
-        ) { isSuccess ->
-            if (isSuccess) {
-                done()
+    private fun handleSwitchModeResult(
+        byteArray: ByteArray,
+        isNeedLoading: Boolean = false,
+        callBack: ((Byte, Byte) -> Unit)? = null
+    ) {
+        BleCmdManager.handleSwitchModeResult(byteArray) { job, res ->
+            if (res == 0x01.toByte() && job == 0x01.toByte()) {
+                logger.info("切换工作模式成功")
+                if (isNeedLoading) LoadingEvent.sendLoadingEvent("切换工作模式成功", true)
+            } else if (res == 0x01.toByte() && job == 0x02.toByte()) {
+                logger.info("切换待机模式成功")
+                if (isNeedLoading) LoadingEvent.sendLoadingEvent("切换待机模式成功", true)
+            } else {
+                logger.error("切换模式失败 : ${job.toInt()} - ${res.toInt()}")
+                if (isNeedLoading) LoadingEvent.sendLoadingEvent("切换模式失败", true)
             }
+            callBack?.invoke(res, job)
         }
     }
 
     /**
-     * 处理虚拟钥匙归还,如果作业的全部点位已经上锁更新钥匙的状态使用
+     * 切换模式结果处理
      */
-    fun handleVirtualKeyReturn(ticketId: Long, keyNfc: String, done: () -> Unit) {
-        // 上报钥匙归还
-        RepositoryManager.hardwareRepo.updateKeyReturn(
-            ticketId, keyNfc, SIKCore.getApplication().serialNo()
-        ) { isSuccess, msg ->
-            if (!isSuccess && msg != CommonUtils.getStr(R.string.ticket_lost)
-            ) {
-                SPUtils.saveUpdateKeyReturn(
-                    SIKCore.getApplication(), UpdateKeyReturn(ticketId, keyNfc)
-                )
-            } else {
-                done()
-                UpdateTicketProgressEvent.sendUpdateTicketProgressEvent(ticketId)
+    private fun switchModeResultDeal(
+        job: Int,
+        res: Int,
+        bleBean: BleBean
+    ) {
+        when (job) {
+            // 工作模式
+            1 -> {
+                if (res == 1) {
+                    // 只能在这里断开,不能全部断开
+                    BleManager.getInstance().disconnect(bleBean.bleDevice)
+
+                    // 打开钥匙卡扣
+                    val keyBean =
+                        ModBusController.getKeyByMac(bleBean.bleDevice.mac)
+                    if (keyBean == null) {
+                        LoadingEvent.sendLoadingEvent("未找到钥匙信息", true)
+                        PopTip.tip(R.string.key_not_exists)
+                    } else {
+                        LoadingEvent.sendLoadingEvent(
+                            CommonUtils.getStr(R.string.take_out_key_tip),
+                            true
+                        )
+                        val dock =
+                            ModBusController.getDockByKeyMac(bleBean.bleDevice.mac)
+                        keyBean.isReady = false
+                        ModBusController.controlKeyBuckle(
+                            true, keyBean.idx, dock?.addr
+                        )
+                        ModBusController.updateKeyReadyStatus(
+                            bleBean.bleDevice.mac, false, 1
+                        )
+                        PopTip.tip(R.string.take_out_key)
+                    }
+                } else {
+                    logger.error("切换工作模式失败 : ${bleBean.bleDevice.mac}")
+                    Executor.delayOnMain(500) {
+                        switchWorkMode(bleBean.bleDevice, false)
+                    }
+                }
+            }
+            // 待机模式
+            2 -> {
+                if (res == 1) {
+                    ModBusController.updateKeyReadyStatus(
+                        bleBean.bleDevice.mac, true, 2
+                    )
+                    // 延时再次获取当前状态,触发handleCurrentMode里工作票下发状态检查
+                    Executor.delayOnMain(500) {
+                        BleConnectionManager.getCurrentStatus(1, bleBean.bleDevice)
+                    }
+                } else {
+                    logger.error("切换待机模式失败 : ${bleBean.bleDevice.mac}")
+                    Executor.delayOnMain(500) {
+                        switchReadyMode(bleBean.bleDevice)
+                    }
+                }
             }
         }
     }
 
+    /**
+     * 切换工作模式
+     */
+    private fun switchWorkMode(bleDevice: BleDevice, isNeedLoading: Boolean = false) {
+        logger.info("switchWorkMode - ${bleDevice.mac}")
+        BleCmdManager.switchMode(
+            BleConst.STATUS_WORK,
+            bleDevice,
+            object : CustomBleWriteCallback() {
+                override fun onWriteSuccess(current: Int, total: Int, justWrite: ByteArray?) {
+                    logger.info("switch mode work success : ${bleDevice.mac}")
+                }
+
+                override fun onWriteFailure(exception: BleException?) {
+                    logger.error("switch mode work fail : ${exception?.code} - ${exception?.description}")
+                    Executor.delayOnMain(500) {
+                        switchWorkMode(bleDevice, isNeedLoading)
+                    }
+                }
+            })
+    }
+
     /**
      * 切换待机模式
      */
-    fun switchReadyMode(bleDevice: BleDevice) {
+    private fun switchReadyMode(bleDevice: BleDevice) {
         BleCmdManager.switchMode(
             BleConst.STATUS_READY,
             bleDevice,
@@ -209,26 +236,101 @@ object BleBusinessManager {
     }
 
     /**
-     * 获取工作票完成情况
+     * 工作票下发结果
+     * res:0x00:成功 0x01:失败 0x02:传输超时 0x0D:当前IDX超出范围 0x0E:当前数据CRC校验失败 0x14:JSON结构错误 0x63:未知错误
      */
-    private fun getTicketStatus(
+    private fun handleWorkTicketResult(
+        bleBean: BleBean, byteArray: ByteArray, isNeedLoading: Boolean = false
+    ) {
+        BleCmdManager.handleWorkTicketResult(bleBean, byteArray) { isSuccess, rst ->
+            if (isNeedLoading) LoadingEvent.sendLoadingEvent()
+            if (isSuccess) {
+                // 下发完毕,切换工作模式
+                logger.info("工作票下发完毕")
+                if (isNeedLoading) LoadingEvent.sendLoadingEvent("切换钥匙为工作模式", true)
+                Executor.delayOnIO(800) {
+                    //切换到工作模式
+                    switchWorkMode(bleBean.bleDevice, isNeedLoading)
+                }
+            } else {
+                LoadingEvent.sendLoadingEvent()
+                if (bleBean.retryCount < 3) {
+                    Executor.delayOnMain(500) {
+                        bleBean.retryCount++
+                        LoadingEvent.sendLoadingEvent(
+                            CommonUtils.getStr(R.string.start_to_send_ticket),
+                            true
+                        )
+                        sendTicketWithRetry(bleBean.ticketSend!!, bleBean.bleDevice, isNeedLoading)
+                    }
+                } else {
+                    PopTip.tip(R.string.send_ticket_fail)
+                    logger.error("Send ticket fail")
+                    ModBusController.getKeyByMac(bleBean.bleDevice.mac)?.let { itKey ->
+                        ModbusBusinessManager.mDeviceTakeList.removeIf { it.deviceType == DeviceConst.DEVICE_TYPE_KEY && it.nfc == itKey.rfid }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * 带重试的下发工作票,重试次数3,间隔500ms
+     */
+    private fun sendTicketWithRetry(
+        json: String,
         bleDevice: BleDevice,
         isNeedLoading: Boolean = false,
-        processCallback: ((Boolean) -> Unit)? = null
+        maxRetries: Int = 3,
+        delayMillis: Long = 500
     ) {
-        if (isNeedLoading) LoadingEvent.sendLoadingEvent("开始获取工作票", true)
-        BleCmdManager.getTicketStatus(bleDevice, object : CustomBleWriteCallback() {
-            override fun onWriteSuccess(current: Int, total: Int, justWrite: ByteArray?) {
-                if (isNeedLoading) LoadingEvent.sendLoadingEvent("工作票获取成功", true)
-                logger.info("getTicketStatus success")
-            }
+        var retryCount = 0
 
-            override fun onWriteFailure(exception: BleException?) {
-                if (isNeedLoading) LoadingEvent.sendLoadingEvent("工作票获取失败", true)
-                processCallback?.invoke(false)
-                logger.error("getTicketStatus fail")
+        fun attemptSend() {
+            sendTicket(json, bleDevice, isNeedLoading) { sendRst ->
+                if (!sendRst && retryCount < maxRetries) {
+                    retryCount++
+                    // 等待一段时间后再次尝试
+                    Executor.delayOnMain(delayMillis) {
+                        logger.info("Retry attempt, mac : ${bleDevice.mac}, retryCount : $retryCount")
+                        attemptSend()
+                    }
+                }
             }
-        })
+        }
+
+        attemptSend()
+    }
+
+    private fun sendTicket(
+        jsonStr: String,
+        bleDevice: BleDevice,
+        isNeedLoading: Boolean = false,
+        processCallback: ((Boolean) -> Unit)? = null
+    ) {
+        if (isNeedLoading) LoadingEvent.sendLoadingEvent(
+            CommonUtils.getStr(R.string.start_to_send_ticket),
+            true
+        )
+        BleCmdManager.sendWorkTicket(
+            jsonStr, bleDevice = bleDevice, callback = object : CustomBleWriteCallback() {
+                override fun onWriteSuccess(current: Int, total: Int, justWrite: ByteArray?) {
+                    logger.info("sendTicket success")
+                    if (isNeedLoading) LoadingEvent.sendLoadingEvent(
+                        CommonUtils.getStr(R.string.sending_ticket),
+                        true
+                    )
+                }
+
+                override fun onWriteFailure(exception: BleException?) {
+                    logger.error("sendTicket fail : ${bleDevice.mac}")
+                    if (isNeedLoading) LoadingEvent.sendLoadingEvent(
+                        CommonUtils.getStr(R.string.send_ticket_fail),
+                        true
+                    )
+                    processCallback?.invoke(false)
+                }
+            })
     }
 
     /**
@@ -306,6 +408,287 @@ object BleBusinessManager {
         }
     }
 
+    /**
+     * 读取工作票完成情况
+     */
+    private fun getTicketStatusBusiness(
+        mac: String, isNeedLoading: Boolean = false
+    ) {
+        BleConnectionManager.registerConnectListener(mac) { isDone, bleBean ->
+            if (isDone) {
+                Executor.delayOnMain(500) {
+                    getTicketStatusWithRetry(bleBean!!.bleDevice, isNeedLoading)
+                }
+            } else {
+                if (isNeedLoading) LoadingEvent.sendLoadingEvent()
+            }
+        }
+    }
+
+    private fun getTicketStatusWithRetry(
+        bleDevice: BleDevice,
+        isNeedLoading: Boolean = false,
+        maxRetries: Int = 3,
+        delayMillis: Long = 500
+    ) {
+        var retryCount = 0
+
+        fun attemptSend() {
+            getTicketStatus(bleDevice, isNeedLoading) { sendRst ->
+                if (!sendRst && retryCount < maxRetries) {
+                    retryCount++
+                    // 等待一段时间后再次尝试
+                    Executor.delayOnMain(delayMillis) {
+                        logger.info("Retry attempt, mac : ${bleDevice.mac}, retryCount : $retryCount")
+                        attemptSend()
+                    }
+                }
+            }
+        }
+
+        attemptSend()
+    }
+
+    /**
+     * 获取工作票完成情况
+     */
+    private fun getTicketStatus(
+        bleDevice: BleDevice,
+        isNeedLoading: Boolean = false,
+        processCallback: ((Boolean) -> Unit)? = null
+    ) {
+        if (isNeedLoading) LoadingEvent.sendLoadingEvent("开始获取工作票", true)
+        BleCmdManager.getTicketStatus(bleDevice, object : CustomBleWriteCallback() {
+            override fun onWriteSuccess(current: Int, total: Int, justWrite: ByteArray?) {
+                if (isNeedLoading) LoadingEvent.sendLoadingEvent("工作票获取成功", true)
+                logger.info("getTicketStatus success")
+            }
+
+            override fun onWriteFailure(exception: BleException?) {
+                if (isNeedLoading) LoadingEvent.sendLoadingEvent("工作票获取失败", true)
+                processCallback?.invoke(false)
+                logger.error("getTicketStatus fail")
+            }
+        })
+    }
+
+    /**
+     * 处理工作票完成情况
+     */
+    private fun handleTicketStatus(
+        bleDevice: BleDevice, byteArray: ByteArray, isNeedLoading: Boolean = false
+    ) {
+        BleCmdManager.handleTicketStatus(bleDevice, byteArray) { ticketJson ->
+            if (ticketJson.isNullOrEmpty()) {
+                return@handleTicketStatus
+            }
+            if (isNeedLoading) LoadingEvent.sendLoadingEvent("工作票完成状态读取完成", true)
+            logger.info("Get ticket status complete : ${bleDevice.mac}")
+            // TD:Ticket Done
+            if (isNeedLoading) LoadingEvent.sendLoadingEvent("TD$ticketJson}", true)
+
+            val workTicketGet = try {
+                Gson().fromJson(ticketJson, WorkTicketGet::class.java)
+            } catch (e: Exception) {
+                null
+            }
+            if (workTicketGet == null) {
+                PopTip.tip(R.string.ticket_data_error)
+                return@handleTicketStatus
+            }
+
+
+            // 判断WorkTicketGet里是否有未完成的
+            ThreadUtils.runOnIO {
+                val finishedStatus = workTicketGet.hasFinished()
+                logger.info("作业票结束情况:${finishedStatus}")
+                if (finishedStatus.first) {
+                    Executor.delayOnMain(500) {
+                        handleKeyReturn(bleDevice, workTicketGet, finishedStatus.second)
+                    }
+                } else {
+                    // 当前策略:作业票未完成禁止归还钥匙
+                    TipDialog.show(
+                        msg = CommonUtils.getStr(R.string.key_return_tip)!!,
+                        onConfirmClick = {
+                            LoadingEvent.sendLoadingEvent()
+                            PopTip.tip(CommonUtils.getStr(R.string.continue_the_ticket))
+                            BleManager.getInstance().disconnect(bleDevice)
+                            // 打开卡扣,防止初始化的时候选择不处理钥匙导致无法使用
+                            val dock = ModBusController.getDockByKeyMac(bleDevice.mac)
+                            val keyBean = dock?.getKeyList()?.find { it.mac == bleDevice.mac }
+                            keyBean?.let {
+                                ModBusController.controlKeyBuckle(true, keyBean.idx, dock.addr)
+                            }
+                        })
+                }
+            }
+        }
+    }
+
+    /**
+     * 处理钥匙归还
+     */
+    private fun handleKeyReturn(
+        bleDevice: BleDevice,
+        workTicketGet: WorkTicketGet?,
+        ticketFinished: Boolean
+    ) {
+        val dock = ModBusController.getDockByKeyMac(bleDevice.mac)
+        val keyBean = dock?.getKeyList()?.find { it.mac == bleDevice.mac }
+        keyBean?.let {
+            ModBusController.controlKeyBuckle(false, keyBean.idx, dock.addr)
+        }
+        if (ticketFinished) {
+            switchReadyMode(bleDevice)
+        } else {
+            // 上报隔离点状态
+            val keyNfc = ModBusController.getKeyByMac(bleDevice.mac)?.rfid
+            workTicketGet?.data?.forEach { data ->
+                val updateList = mutableListOf<LockPointUpdateReq>()
+                data.dataList?.forEach { dataListDTO ->
+                    data.taskCode?.toLong()?.let {
+                        SPUtils.returnKey(it)
+                    }
+                    val updateVO = LockPointUpdateReq(
+                        data.taskCode?.toLong(),
+                        dataListDTO.infoRfidNo,
+                        dataListDTO.equipRfidNo,
+                        keyNfc!!,
+                        dataListDTO.target,
+                        dataListDTO.status
+                    )
+                    updateList.add(updateVO)
+                }
+
+                LoadingEvent.sendLoadingEvent()
+                // 上报点位钥匙绑定
+                RepositoryManager.jobTicketRepo.updateLockPointBatch(updateList) { isSuccess, msg, code ->
+                    logger.info("还锁操作:${isSuccess},${msg},${code}")
+                    if (isSuccess) {
+                        // 上报钥匙归还
+                        RepositoryManager.jobTicketRepo.updateKeyReturn(
+                            data.taskCode?.toLong()!!,
+                            keyNfc!!,
+                            SIKCore.getApplication().serialNo()
+                        ) { isSuccess, msg, code ->
+                            if (!isSuccess && msg != SIKCore.getApplication().getString(
+                                    R.string.ticket_lost
+                                )
+                            ) {
+                                SPUtils.saveUpdateKeyReturn(
+                                    SIKCore.getApplication(),
+                                    UpdateKeyReturn(data.taskCode?.toLong()!!, keyNfc!!)
+                                )
+                                if (msg == CommonUtils.getStr(R.string.ticket_lost)) {
+                                    data.taskCode?.let {
+                                        TicketFinishedEvent.sendTicketFinishedEvent(it.toLong())
+                                    }
+                                }
+                                PopTip.tip(R.string.key_return_success)
+                            } else {
+                                //更新作业票的状态,如果是上锁就更新到共锁,如果是解锁就更新到解锁之后的步骤
+                                RepositoryManager.jobTicketRepo.updateTicketDataStatus(
+                                    data.taskCode?.toLong()!!,
+                                    if (data.dataList?.any { it.status == 0 } == true) LockStepEnum.COLOCK.type else LockStepEnum.UNLOCKED.type
+                                )
+                                PopTip.tip(R.string.key_return_success)
+                            }
+                        }
+                        data.taskCode?.toLong()?.let {
+                            UpdateTicketProgressEvent.sendUpdateTicketProgressEvent(it)
+                        }
+                        // 确认归还,切换为待机模式
+                        switchReadyMode(bleDevice)
+                    } else {
+                        ThreadUtils.runOnMain {
+                            // 当前策略:作业票未完成禁止归还钥匙
+                            fun keyReturnErrorConfirm() {
+                                LoadingEvent.sendLoadingEvent()
+                                PopTip.tip(R.string.continue_the_ticket)
+                                BleManager.getInstance().disconnect(bleDevice)
+                                // 打开卡扣,防止初始化的时候选择不处理钥匙导致无法使用
+                                if (workTicketGet.data?.all { it.dataList?.all { it.closed == 1 } == true } == true) {
+                                    workTicketGet.data?.firstOrNull()?.taskCode?.toLong()
+                                        ?.let {
+                                            checkStepAndTicketDetailThenSendTicket(
+                                                it,
+                                                bleDevice.mac
+                                            )
+                                        }
+                                } else {
+                                    val dock = ModBusController.getDockByKeyMac(bleDevice.mac)
+                                    val keyBean =
+                                        dock?.getKeyList()?.find { it.mac == bleDevice.mac }
+                                    keyBean?.let {
+                                        ModBusController.controlKeyBuckle(
+                                            true, keyBean.idx, dock.addr
+                                        )
+                                    }
+                                }
+                            }
+                            TipDialog.show(
+                                msg = CommonUtils.getStr(R.string.key_return_tip).toString(),
+                                onCancelClick = {
+                                    keyReturnErrorConfirm()
+                                },
+                                onConfirmClick = {
+                                    keyReturnErrorConfirm()
+                                }
+                            )
+                        }
+                        SPUtils.clearUpdateKeyReturn(SIKCore.getApplication())
+                        SPUtils.clearUpdateLockPoint(SIKCore.getApplication())
+                    }
+                }
+
+            }
+        }
+    }
+
+    /**
+     * 检查步骤和作业票详情并且下发作业票
+     */
+    private fun checkStepAndTicketDetailThenSendTicket(ticketId: Long, mac: String) {
+        RepositoryManager.jobTicketRepo.getStepDetail(ticketId) {
+            var step = 0
+            it?.filter { it.stepStatus == "1" }
+                ?.maxByOrNull { it.stepIndex!! }?.stepIndex?.let {
+                    step = it
+                }
+            RepositoryManager.jobTicketRepo.getTicketDetail(ticketId) { ticketDetail ->
+                if (ticketDetail == null) {
+                    return@getTicketDetail
+                }
+                val role = ticketDetail?.ticketUserVOList?.find {
+                    it.userId == SPUtils.getLoginUser(SIKCore.getApplication())?.userId && it.userType == Constants.USER_TYPE_LOCKER
+                }
+                if (role == null) {
+                    PopTip.tip(R.string.you_are_not_locker_tip)
+                    return@getTicketDetail
+                }
+                if (step == 4) {    // 上锁工作票
+                    sendTicketBusiness(
+                        true,
+                        mac,
+                        ticketDetail,
+                        ticketDetail.ticketLockVOList?.filter { it.lockStatus != "2" }
+                            ?.map { it.lockNfc }?.toMutableList(),
+                        true
+                    )
+                } else if (step == 7) { // 解锁工作票
+                    sendTicketBusiness(
+                        false,
+                        mac,
+                        ticketDetail,
+                        null,
+                        true
+                    )
+                }
+            }
+        }
+    }
+
     /**
      * 分配钥匙
      */
@@ -366,47 +749,6 @@ object BleBusinessManager {
         }
     }
 
-    /**
-     * 读取工作票完成情况
-     */
-    private fun getTicketStatusBusiness(
-        mac: String, isNeedLoading: Boolean = false
-    ) {
-        BleConnectionManager.registerConnectListener(mac) { isDone, bleBean ->
-            if (isDone) {
-                Executor.delayOnMain(500) {
-                    getTicketStatusWithRetry(bleBean!!.bleDevice, isNeedLoading)
-                }
-            } else {
-                if (isNeedLoading) LoadingEvent.sendLoadingEvent()
-            }
-        }
-    }
-
-    private fun getTicketStatusWithRetry(
-        bleDevice: BleDevice,
-        isNeedLoading: Boolean = false,
-        maxRetries: Int = 3,
-        delayMillis: Long = 500
-    ) {
-        var retryCount = 0
-
-        fun attemptSend() {
-            getTicketStatus(bleDevice, isNeedLoading) { sendRst ->
-                if (!sendRst && retryCount < maxRetries) {
-                    retryCount++
-                    // 等待一段时间后再次尝试
-                    Executor.delayOnMain(delayMillis) {
-                        logger.info("Retry attempt, mac : ${bleDevice.mac}, retryCount : $retryCount")
-                        attemptSend()
-                    }
-                }
-            }
-        }
-
-        attemptSend()
-    }
-
     /**
      * 连接一把存在的可连接的钥匙
      */
@@ -536,63 +878,4 @@ object BleBusinessManager {
         logger.info("json : $jsonStr")
         return jsonStr
     }
-
-    /**
-     * 带重试的下发工作票,重试次数3,间隔500ms
-     */
-    private fun sendTicketWithRetry(
-        json: String,
-        bleDevice: BleDevice,
-        isNeedLoading: Boolean = false,
-        maxRetries: Int = 3,
-        delayMillis: Long = 500
-    ) {
-        var retryCount = 0
-
-        fun attemptSend() {
-            sendTicket(json, bleDevice, isNeedLoading) { sendRst ->
-                if (!sendRst && retryCount < maxRetries) {
-                    retryCount++
-                    // 等待一段时间后再次尝试
-                    Executor.delayOnMain(delayMillis) {
-                        logger.info("Retry attempt, mac : ${bleDevice.mac}, retryCount : $retryCount")
-                        attemptSend()
-                    }
-                }
-            }
-        }
-
-        attemptSend()
-    }
-
-    private fun sendTicket(
-        jsonStr: String,
-        bleDevice: BleDevice,
-        isNeedLoading: Boolean = false,
-        processCallback: ((Boolean) -> Unit)? = null
-    ) {
-        if (isNeedLoading) LoadingEvent.sendLoadingEvent(
-            CommonUtils.getStr(R.string.start_to_send_ticket),
-            true
-        )
-        BleCmdManager.sendWorkTicket(
-            jsonStr, bleDevice = bleDevice, callback = object : CustomBleWriteCallback() {
-                override fun onWriteSuccess(current: Int, total: Int, justWrite: ByteArray?) {
-                    logger.info("sendTicket success")
-                    if (isNeedLoading) LoadingEvent.sendLoadingEvent(
-                        CommonUtils.getStr(R.string.sending_ticket),
-                        true
-                    )
-                }
-
-                override fun onWriteFailure(exception: BleException?) {
-                    logger.error("sendTicket fail : ${bleDevice.mac}")
-                    if (isNeedLoading) LoadingEvent.sendLoadingEvent(
-                        CommonUtils.getStr(R.string.send_ticket_fail),
-                        true
-                    )
-                    processCallback?.invoke(false)
-                }
-            })
-    }
 }

+ 2 - 2
ui-base/src/main/java/com/grkj/ui_base/business/ModbusBusinessManager.kt

@@ -5,7 +5,7 @@ import com.grkj.data.di.RepositoryManager
 import com.grkj.data.model.local.DeviceTakeUpdate
 import com.grkj.data.model.req.LockTakeUpdateReq
 import com.grkj.ui_base.R
-import com.grkj.ui_base.data.DictConstants
+import com.grkj.data.data.DictConstants
 import com.grkj.ui_base.utils.CommonUtils
 import com.grkj.ui_base.utils.Executor
 import com.grkj.ui_base.utils.SPUtils
@@ -64,7 +64,7 @@ object ModbusBusinessManager {
                     ?.let { info ->
                         LoadingEvent.sendLoadingEvent()
                         SPUtils.takeKey(info.ticketId)
-                        RepositoryManager.hardwareRepo.updateKeyTake(
+                        RepositoryManager.jobTicketRepo.updateKeyTake(
                             info.ticketId, info.nfc, SIKCore.getApplication().serialNo()!!
                         ) { isSuccess ->
                             if (isSuccess) {

+ 1 - 1
ui-base/src/main/java/com/grkj/ui_base/config/ISCSConfig.kt

@@ -1,6 +1,6 @@
 package com.grkj.ui_base.config
 
-import com.grkj.ui_base.data.MMKVConstants
+import com.grkj.data.data.MMKVConstants
 import com.sik.sikcore.extension.getMMKVData
 
 /**

+ 3 - 0
ui-base/src/main/java/com/grkj/ui_base/utils/ble/BleCmdManager.kt

@@ -307,6 +307,9 @@ object BleCmdManager {
 
                     override fun onWriteFailure(exception: BleException?) {
                         logger.error("getTicketStatusPart fail")
+                        BleConnectionManager.getBleDeviceByMac(bleDevice.mac)?.let {
+                            it.ticketStatus = byteArrayOf()
+                        }
                         GetTicketStatusEvent.sendGetTicketStatusEvent(false, bleDevice)
                     }
                 })

+ 207 - 103
ui-base/src/main/java/com/grkj/ui_base/utils/ble/BleConnectionManager.kt

@@ -13,8 +13,14 @@ import com.grkj.ui_base.utils.extension.startsWith
 import com.grkj.ui_base.utils.extension.toHexStrings
 import com.sik.sikcore.activity.ActivityTracker
 import com.sik.sikcore.thread.ThreadUtils
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withContext
 import org.slf4j.Logger
 import org.slf4j.LoggerFactory
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
 
 /**
  * BLE 连接管理工具:保持原有扫描、连接、监听、取 Token 流程,
@@ -89,26 +95,45 @@ object BleConnectionManager {
      * - 如果 mac 已在待连接队列或正在连接,忽略重复请求
      * - 否则将 mac 添加到队列并触发连接流程
      */
-    fun registerConnectListener(mac: String, callBack: ((Boolean, BleBean?) -> Unit)? = null) {
-        logger.info("registerConnectListener : $mac")
+    fun registerConnectListener(
+        mac: String,
+        connectNow: Boolean = false,
+        callBack: ((Boolean, BleBean?) -> Unit)? = null
+    ) {
+        logger.info("蓝牙连接-开始连接 : $mac")
         // 已连接且已获取 token
-        deviceList.find { it.bleDevice.mac == mac && it.token != null }?.let { bean ->
+        deviceList.find {
+            it.bleDevice.mac == mac && BleManager.getInstance().isConnected(mac) && it.token != null
+        }?.let { bean ->
+            logger.info("蓝牙连接-设备已连接")
             callBack?.invoke(true, bean)
             return
         }
+        if (connectNow) {
+            logger.warn("蓝牙连接-立即连接 mac: $mac")
+            unregisterConnectListener(mac)
+        }
         // 重复注册检查
         if (connectListeners.any { it.mac == mac } || currentConnectingMac == mac) {
             logger.warn("忽略重复注册 mac: $mac")
+            callBack?.invoke(false, null)
             return
         }
         // 加入队列并启动连接
-        fun checkAndConnect() {
+        fun checkAndConnect(isDisconnectAll: Boolean = false) {
+            logger.warn("蓝牙连接-开始检查连接 mac: $mac")
             if (BleManager.getInstance().allConnectedDevice.size < maxConnectCount) {
                 connectListeners.add(ConnectListener(mac, callBack))
                 connectKey()
             } else {
-                ThreadUtils.runOnIODelayed(500) {
-                    checkAndConnect()
+                if (connectNow && !isDisconnectAll) {
+                    logger.info("蓝牙连接-超过最大连接数,但是需要立即连接,断开所有连接进行连接")
+                    BleManager.getInstance().disconnectAllDevice()
+                    checkAndConnect(true)
+                } else {
+                    ThreadUtils.runOnIODelayed(500) {
+                        checkAndConnect()
+                    }
                 }
             }
         }
@@ -119,7 +144,7 @@ object BleConnectionManager {
      * 连接监听反注册
      */
     fun unregisterConnectListener(mac: String, bleBean: BleBean? = null) {
-        logger.info("unregisterConnectListener : $mac")
+        logger.info("蓝牙连接-unregisterConnectListener : $mac")
         connectListeners.removeAll { it.mac == mac }
     }
 
@@ -140,9 +165,9 @@ object BleConnectionManager {
     /**
      * 检查是否能进行蓝牙连接准备的下一步,防止未准备完但是已经取消订阅
      */
-    private fun checkProcess(mac: String?): Boolean {
+    private fun checkProcess(mac: String?, hideLoading: Boolean = true): Boolean {
         val canProcess = connectListeners.any { it.mac == mac }
-        if (!canProcess) LoadingEvent.sendLoadingEvent(null, false)
+        if (!canProcess && hideLoading) LoadingEvent.sendLoadingEvent(null, false)
         return canProcess
     }
 
@@ -152,14 +177,18 @@ object BleConnectionManager {
     private fun connectKey() {
         if (connectListeners.isEmpty()) return
         if (isPreparing || BleManager.getInstance().allConnectedDevice.size >= maxStandbyCount) {
+            logger.info("暂时不能连接:${isPreparing},${BleManager.getInstance().allConnectedDevice.size > maxStandbyCount}")
             ThreadUtils.runOnMainDelayed(1000) { connectKey() }
             return
         }
         val listener = connectListeners.first()
         currentConnectingMac = listener.mac
+        ThreadUtils.runOnIODelayed(10 * 1000) {
+            isPreparing = false
+        }
         isPreparing = true
         if (ActivityTracker.getCurrentActivity() == null) {
-            logger.warn("Ignore connectKey : ${listener.mac} no current activity")
+            logger.warn("蓝牙连接-Ignore connectKey : ${listener.mac} no current activity")
             isPreparing = false
             currentConnectingMac = null
             return
@@ -173,12 +202,8 @@ object BleConnectionManager {
                 if (!isDone) {
                     // 判断是否仍然待连,防止拿走;移到末尾,防止循环影响
                     if (checkProcess(listener.mac)) {
+                        listener.callBack?.invoke(false, null)
                         unregisterConnectListener(listener.mac)
-                        ThreadUtils.runOnMainDelayed(2000) {
-                            registerConnectListener(
-                                listener.mac, listener.callBack
-                            )
-                        }
                     }
                     return@runOnMain
                 }
@@ -203,7 +228,7 @@ object BleConnectionManager {
         prepareDoneCallBack: ((Boolean, BleBean?) -> Unit)?
     ) {
         if (!checkProcess(mac)) {
-            logger.error("Prepare is canceled : $mac")
+            logger.error("蓝牙连接-Prepare is canceled : $mac")
             return
         }
         ThreadUtils.runOnMain {
@@ -216,21 +241,22 @@ object BleConnectionManager {
         isNeedLoading: Boolean = false,
         prepareDoneCallBack: ((Boolean, BleBean?) -> Unit)?
     ) {
-        logger.info("doScanBle:$mac")
+        logger.info("蓝牙连接-doScanBle:$mac")
         if (!checkProcess(mac)) {
-            logger.error("Prepare is canceled : $mac")
+            logger.error("蓝牙连接-Prepare is canceled : $mac")
             return
         }
         if (isNeedLoading) LoadingEvent.sendLoadingEvent("正在扫描设备...", true)
         BleUtil.Companion.instance?.scan(object : CustomBleScanCallback() {
             override fun onPrompt(promptStr: String?) {
                 // 蓝牙未启动重试
+                logger.info("蓝牙连接-参数:${promptStr}")
                 BleManager.getInstance().enableBluetooth()
                 doScanBle(mac, isNeedLoading, prepareDoneCallBack)
             }
 
             override fun onScanStarted(success: Boolean) {
-                logger.info("onScanStarted:${success}")
+                logger.info("蓝牙连接-onScanStarted:${success}")
                 if (!success) {
                     if (isNeedLoading) LoadingEvent.sendLoadingEvent(null, false)
                     prepareDoneCallBack?.invoke(false, null)
@@ -238,19 +264,28 @@ object BleConnectionManager {
             }
 
             override fun onScanning(bleDevice: BleDevice?) {
-                logger.info("onScanning:${bleDevice?.mac}")
-                bleDevice?.let {
-                    doConnect(it, isNeedLoading, prepareDoneCallBack)
+                val mac = bleDevice?.mac ?: return
+                logger.info("蓝牙连接-onScanning:${mac}")
+                if (mac.equals(mac, ignoreCase = true)) {
+                    // 找到目标设备,马上停止扫描
+                    logger.info("找到目标设备 $mac,停止扫描并尝试连接")
+                    BleManager.getInstance().cancelScan()
+                    // 立刻调用 doConnect,下一步进入连接流程
+                    doConnect(
+                        bleDevice,
+                        isNeedLoading,
+                        prepareDoneCallBack
+                    )
                 }
             }
 
             override fun onScanFinished(scanResultList: MutableList<BleDevice>?) {
-                logger.info("onScanFinished: $mac - ${scanResultList?.none { it.mac == mac }}")
+                logger.info("蓝牙连接-onScanFinished: $mac - ${scanResultList?.none { it.mac == mac }}")
                 if (isNeedLoading) LoadingEvent.sendLoadingEvent(null, false)
                 // 没有扫描到
+                // 没有扫描到
                 if (scanResultList?.none { it.mac == mac } == true) {
                     logger.warn("$mac is not scanned")
-                    connectListeners.removeIf { it.mac == mac }
                     prepareDoneCallBack?.invoke(false, null)
                 }
             }
@@ -265,97 +300,102 @@ object BleConnectionManager {
         isNeedLoading: Boolean = false,
         prepareDoneCallBack: ((Boolean, BleBean?) -> Unit)?
     ) {
-        logger.info("doConnect : ${bleDevice.mac}")
+        logger.info("蓝牙连接-doConnect : ${bleDevice.mac}")
         if (!checkProcess(bleDevice.mac)) {
-            logger.error("Prepare is canceled : ${bleDevice.mac}")
+            logger.error("蓝牙连接-Prepare is canceled : ${bleDevice.mac}")
             return
         }
         if (isNeedLoading) LoadingEvent.sendLoadingEvent(
             CommonUtils.getStr(R.string.ble_connecting), true
         )
-        BleManager.getInstance().disconnect(bleDevice)
-        BleUtil.Companion.instance?.connectBySelect(
-            bleDevice, object : CustomBleGattCallback() {
-                override fun onPrompt(promptStr: String?) {
-                    logger.info(promptStr)
-                    if (isNeedLoading) LoadingEvent.sendLoadingEvent(
-                        promptStr, false
-                    )
-                }
+        ThreadUtils.runOnIO {
+            BleManager.getInstance().disconnect(bleDevice)
+            delay(300)
+            BleUtil.Companion.instance?.connectBySelect(
+                bleDevice, object : CustomBleGattCallback() {
+                    override fun onPrompt(promptStr: String?) {
+                        logger.info(promptStr)
+                        if (isNeedLoading) LoadingEvent.sendLoadingEvent(
+                            promptStr, false
+                        )
+                    }
 
-                override fun onStartConnect() {}
+                    override fun onStartConnect() {}
 
-                override fun onConnectFail(bleDevice: BleDevice?, exception: BleException?) {
-                    if (isNeedLoading) LoadingEvent.sendLoadingEvent(
-                        CommonUtils.getStr(R.string.ble_connect_fail), false
-                    )
-                    logger.error("onConnectFail : ${bleDevice?.mac} - ${exception?.description}")
-                    prepareDoneCallBack?.invoke(false, null)
-                }
+                    override fun onConnectFail(bleDevice: BleDevice?, exception: BleException?) {
+                        if (isNeedLoading) LoadingEvent.sendLoadingEvent(
+                            CommonUtils.getStr(R.string.ble_connect_fail), false
+                        )
+                        logger.error("蓝牙连接-onConnectFail : ${bleDevice?.mac} - ${exception?.description}")
+                        prepareDoneCallBack?.invoke(false, null)
+                    }
 
-                override fun onConnectSuccess(
-                    bleDevice: BleDevice?, gatt: BluetoothGatt?, status: Int
-                ) {
-                    if (isNeedLoading) LoadingEvent.sendLoadingEvent(
-                        null, false
-                    )
-                    logger.info("onConnectSuccess : ${bleDevice?.mac}")
-                    bleDevice?.let {
-                        deviceList.removeIf { it.bleDevice.mac == bleDevice.mac }
-                        val bleBean = BleBean(it)
-                        deviceList.add(bleBean)
-//todo 移除异常钥匙                        removeExceptionKey(it.mac)
-                        // 设置MTU
-                        ThreadUtils.runOnMainDelayed(200) {
-                            if (!checkProcess(bleDevice.mac)) {
-                                logger.error("Prepare is canceled : ${bleDevice.mac}")
-                                return@runOnMainDelayed
+                    override fun onConnectSuccess(
+                        bleDevice: BleDevice?, gatt: BluetoothGatt?, status: Int
+                    ) {
+                        if (isNeedLoading) LoadingEvent.sendLoadingEvent(
+                            null, false
+                        )
+                        logger.info("蓝牙连接-onConnectSuccess : ${bleDevice?.mac}")
+                        bleDevice?.let {
+                            deviceList.removeIf { it.bleDevice.mac == bleDevice.mac }
+                            val bleBean = BleBean(it)
+                            deviceList.add(bleBean)
+//                            todo 移除异常钥匙
+                             removeExceptionKey(it.mac)
+                            // 设置MTU
+                            ThreadUtils.runOnMainDelayed(200) {
+                                if (!checkProcess(bleDevice.mac)) {
+                                    logger.error("Prepare is canceled : ${bleDevice.mac}")
+                                    return@runOnMainDelayed
+                                }
+                                BleUtil.Companion.instance?.setMtu(it)
+                            }
+                            // 监听
+                            ThreadUtils.runOnMainDelayed(500) {
+                                indicate(bleBean, isNeedLoading, prepareDoneCallBack)
                             }
-                            BleUtil.Companion.instance?.setMtu(it)
-                        }
-                        // 监听
-                        ThreadUtils.runOnMainDelayed(500) {
-                            indicate(bleBean, isNeedLoading, prepareDoneCallBack)
                         }
                     }
-                }
 
-                override fun onDisConnected(
-                    isActiveDisConnected: Boolean,
-                    device: BleDevice?,
-                    gatt: BluetoothGatt?,
-                    status: Int
-                ) {
-                    if (isNeedLoading) LoadingEvent.sendLoadingEvent(
-                        null, false
-                    )
-                    logger.info("onDisConnected : ${device?.mac} - $isActiveDisConnected")
-                    getBleDeviceByMac(device?.mac)?.let {
-                        deviceList.remove(it)
-                    }
-                    bleDevice.mac?.let { itMac ->
-                        unregisterConnectListener(itMac)
-                    }
-                    if (!isActiveDisConnected) {
-                        // 测试模式下不重连
-                        if (ISCSConfig.isTestMode) {
-                            return
+                    override fun onDisConnected(
+                        isActiveDisConnected: Boolean,
+                        device: BleDevice?,
+                        gatt: BluetoothGatt?,
+                        status: Int
+                    ) {
+                        if (isNeedLoading) LoadingEvent.sendLoadingEvent(
+                            null, false
+                        )
+                        logger.info("蓝牙连接-onDisConnected : ${device?.mac} - $isActiveDisConnected")
+                        getBleDeviceByMac(device?.mac)?.let {
+                            deviceList.remove(it)
+                            it.token = null
+                        }
+                        bleDevice.mac?.let { itMac ->
+                            unregisterConnectListener(itMac)
                         }
-                        // 断开和重连之间最好间隔一段时间,否则可能会出现长时间连接不上的情况
-                        ThreadUtils.runOnMainDelayed(300) {
-                            registerConnectListener(bleDevice.mac) { isDone, bleBean ->
-                                if (isDone && bleBean != null) {
-                                    ThreadUtils.runOnMainDelayed(300) {
-                                        getCurrentStatus(6, bleBean.bleDevice)
+                        if (!isActiveDisConnected) {
+                            // 测试模式下不重连
+                            if (ISCSConfig.isTestMode) {
+                                return
+                            }
+                            // 断开和重连之间最好间隔一段时间,否则可能会出现长时间连接不上的情况
+                            ThreadUtils.runOnMainDelayed(300) {
+                                registerConnectListener(bleDevice.mac) { isDone, bleBean ->
+                                    if (isDone && bleBean != null) {
+                                        ThreadUtils.runOnMainDelayed(300) {
+                                            getCurrentStatus(6, bleBean.bleDevice)
+                                        }
                                     }
                                 }
                             }
+                        } else {
+                            ModBusController.updateKeyReadyStatus(bleDevice.mac, false, 3)
                         }
-                    } else {
-                        ModBusController.updateKeyReadyStatus(bleDevice.mac, false, 3)
                     }
-                }
-            })
+                })
+        }
     }
 
     /**
@@ -453,7 +493,7 @@ object BleConnectionManager {
         prepareDoneCallBack: ((Boolean, BleBean?) -> Unit)?
     ) {
         if (!checkProcess(bleBean?.bleDevice?.mac)) {
-            logger.error("Prepare is canceled : ${bleBean?.bleDevice?.mac}")
+            logger.error("蓝牙连接-Prepare is canceled : ${bleBean?.bleDevice?.mac}")
             return
         }
         if (isNeedLoading) LoadingEvent.sendLoadingEvent("开始监听......", true)
@@ -462,26 +502,26 @@ object BleConnectionManager {
             BleUtil.Companion.instance?.indicate(
                 it.bleDevice, indicateCallback = object : CustomBleIndicateCallback() {
                     override fun onPrompt(promptStr: String?) {
-                        logger.info("indicate onPrompt : $promptStr")
+                        logger.info("蓝牙连接-indicate onPrompt : $promptStr")
                     }
 
                     override fun onConnectPrompt(promptStr: String?) {
-                        logger.info("indicate onConnectPrompt : $promptStr")
+                        logger.info("蓝牙连接-indicate onConnectPrompt : $promptStr")
                     }
 
                     override fun onDisConnectPrompt(promptStr: String?) {
-                        logger.info("indicate onDisConnectPrompt : $promptStr")
+                        logger.info("蓝牙连接-indicate onDisConnectPrompt : $promptStr")
                     }
 
                     override fun onIndicateSuccess() {
-                        logger.info("onIndicateSuccess")
+                        logger.info("蓝牙连接-onIndicateSuccess")
                         isIndicateSuccess = true
                         getToken(bleBean, isNeedLoading, prepareDoneCallBack)
                     }
 
                     override fun onIndicateFailure(exception: BleException?) {
                         if (isNeedLoading) LoadingEvent.sendLoadingEvent(null, false)
-                        logger.error("onIndicateFailure : ${bleBean.bleDevice.mac} - ${exception?.description}")
+                        logger.error("蓝牙连接-onIndicateFailure : ${bleBean.bleDevice.mac} - ${exception?.description}")
                         ThreadUtils.runOnIODelayed(500) {
                             if (isIndicateSuccess) {
                                 return@runOnIODelayed
@@ -491,7 +531,7 @@ object BleConnectionManager {
                     }
 
                     override fun onCharacteristicChanged(data: ByteArray?) {
-                        logger.info("onCharacteristicChanged : ${data?.toHexStrings()}")
+                        logger.info("蓝牙连接-onCharacteristicChanged : ${data?.toHexStrings()}")
                         if (bleIndicateListeners.isEmpty) {
                             bleIndicateListeners.put(this, baseIndicateListeners)
                         }
@@ -536,6 +576,70 @@ object BleConnectionManager {
         }
     }
 
+    /**
+     * 对单个 MAC 做下面两步:
+     *   1. 先尝试不充电连接,若成功就返回 true;
+     *   2. 否则开启“充电”,等 500ms,再尝试一次连接,连接成功后断电并返回 true;否则返回 false。
+     */
+    suspend fun tryConnectWithOptionalCharge(mac: String): Boolean =
+        withContext(Dispatchers.IO) {
+            // -------- 第一次尝试 --------
+            logger.info("蓝牙连接-第一次尝试")
+            val firstTry = suspendCancellableCoroutine<Boolean> { cont ->
+                // 1. 定义一个 flag,确保只 resume 一次
+                var isCalled = false
+                registerConnectListener(mac, true) { isDone, _ ->
+                    if (isCalled) {
+                        return@registerConnectListener
+                    }
+                    isCalled = true
+                    if (isDone) {
+                        // 连接成功后,把电关掉(以防万一)
+                        ModBusController.controlKeyCharge(false, mac) { }
+                        cont.resume(true)
+                    } else {
+                        cont.resume(false)
+                    }
+                    cont.cancel()
+                }
+            }
+            logger.info("蓝牙连接-第一次连接:${firstTry}")
+            if (firstTry) {
+                return@withContext true
+            }
+            // -------- 第二次尝试:先开电,再连 --------
+            // 开电,并等待回调
+            suspendCoroutine<Unit> { unitCont ->
+                ModBusController.controlKeyCharge(false, mac) {
+                    ThreadUtils.runOnIO {
+                        delay(500)
+                        ModBusController.controlKeyCharge(true, mac) {
+                            unitCont.resume(Unit)
+                        }
+                    }
+                }
+            }
+            logger.info("蓝牙连接-开启充电并等待500ms")
+            // 等 500ms 保证硬件电源稳定
+            delay(500)
+
+            // 再次注册连接监听
+            val secondTry = suspendCancellableCoroutine<Boolean> { cont ->
+                var isCalled = false
+                registerConnectListener(mac, true) { isDone, _ ->
+                    if (isCalled) {
+                        return@registerConnectListener
+                    }
+                    isCalled = true
+                    // 无论成功或失败,都先把电关掉
+                    ModBusController.controlKeyCharge(false, mac) { }
+                    cont.resume(isDone)
+                    cont.cancel()
+                }
+            }
+            return@withContext secondTry
+        }
+
 
     // 蓝牙连接准备监听
     data class ConnectListener(

+ 1 - 1
ui-base/src/main/java/com/grkj/ui_base/utils/event/BottomNavVisibilityEvent.kt

@@ -1,7 +1,7 @@
 package com.grkj.ui_base.utils.event
 
 import com.grkj.shared.model.EventBean
-import com.grkj.ui_base.data.EventConstants
+import com.grkj.data.data.EventConstants
 
 /**
  * 底部栏显隐

+ 1 - 1
ui-base/src/main/java/com/grkj/ui_base/utils/event/CurrentModeEvent.kt

@@ -1,7 +1,7 @@
 package com.grkj.ui_base.utils.event
 
 import com.grkj.shared.model.EventBean
-import com.grkj.ui_base.data.EventConstants
+import com.grkj.data.data.EventConstants
 import com.grkj.ui_base.utils.ble.BleBean
 
 /**

+ 1 - 1
ui-base/src/main/java/com/grkj/ui_base/utils/event/DeviceExceptionEvent.kt

@@ -1,7 +1,7 @@
 package com.grkj.ui_base.utils.event
 
 import com.grkj.shared.model.EventBean
-import com.grkj.ui_base.data.EventConstants
+import com.grkj.data.data.EventConstants
 
 /**
  * 设备异常事件

+ 1 - 1
ui-base/src/main/java/com/grkj/ui_base/utils/event/DeviceTakeUpdateEvent.kt

@@ -1,7 +1,7 @@
 package com.grkj.ui_base.utils.event
 
 import com.grkj.shared.model.EventBean
-import com.grkj.ui_base.data.EventConstants
+import com.grkj.data.data.EventConstants
 
 /**
  * 设备取出事件

+ 1 - 1
ui-base/src/main/java/com/grkj/ui_base/utils/event/GetTicketStatusEvent.kt

@@ -2,7 +2,7 @@ package com.grkj.ui_base.utils.event
 
 import com.clj.fastble.data.BleDevice
 import com.grkj.shared.model.EventBean
-import com.grkj.ui_base.data.EventConstants
+import com.grkj.data.data.EventConstants
 
 /**
  * 获取作业票状态事件

+ 1 - 2
ui-base/src/main/java/com/grkj/ui_base/utils/event/JumpViewEvent.kt

@@ -1,8 +1,7 @@
 package com.grkj.ui_base.utils.event
 
 import com.grkj.shared.model.EventBean
-import com.grkj.ui_base.data.EventConstants
-import com.grkj.ui_base.utils.ble.BleBean
+import com.grkj.data.data.EventConstants
 
 class JumpViewEvent(val navGraphId: Int, val targetId: Int) {
     companion object {

+ 1 - 1
ui-base/src/main/java/com/grkj/ui_base/utils/event/LoadingEvent.kt

@@ -1,7 +1,7 @@
 package com.grkj.ui_base.utils.event
 
 import com.grkj.shared.model.EventBean
-import com.grkj.ui_base.data.EventConstants
+import com.grkj.data.data.EventConstants
 
 /**
  * 加载事件

+ 1 - 1
ui-base/src/main/java/com/grkj/ui_base/utils/event/RFIDCardReadEvent.kt

@@ -1,7 +1,7 @@
 package com.grkj.ui_base.utils.event
 
 import com.grkj.shared.model.EventBean
-import com.grkj.ui_base.data.EventConstants
+import com.grkj.data.data.EventConstants
 
 /**
  * RFID读取事件

+ 1 - 1
ui-base/src/main/java/com/grkj/ui_base/utils/event/SwitchCollectionUpdateEvent.kt

@@ -1,7 +1,7 @@
 package com.grkj.ui_base.utils.event
 
 import com.grkj.shared.model.EventBean
-import com.grkj.ui_base.data.EventConstants
+import com.grkj.data.data.EventConstants
 
 /**
  * 开关采集更新事件

+ 24 - 0
ui-base/src/main/java/com/grkj/ui_base/utils/event/TicketFinishedEvent.kt

@@ -0,0 +1,24 @@
+package com.grkj.ui_base.utils.event
+
+import com.grkj.shared.model.EventBean
+import com.grkj.data.data.EventConstants
+
+/**
+ * 作业票已结束事件
+ */
+class TicketFinishedEvent(val ticketId: Long) {
+
+    companion object {
+        /**
+         * 作业票已结束事件
+         */
+        @JvmStatic
+        fun sendTicketFinishedEvent(ticketId: Long) {
+            val ticketFinishedEvent = TicketFinishedEvent(ticketId)
+            val ticketFinishedEventEventBean = EventBean<TicketFinishedEvent>(
+                EventConstants.EVENT_TICKET_FINISHED, ticketFinishedEvent
+            )
+            EventHelper.sendEvent(ticketFinishedEventEventBean)
+        }
+    }
+}

+ 1 - 1
ui-base/src/main/java/com/grkj/ui_base/utils/event/UpdateTicketProgressEvent.kt

@@ -1,7 +1,7 @@
 package com.grkj.ui_base.utils.event
 
 import com.grkj.shared.model.EventBean
-import com.grkj.ui_base.data.EventConstants
+import com.grkj.data.data.EventConstants
 
 /**
  * 更新作业票事件

+ 1 - 1
ui-base/src/main/java/com/grkj/ui_base/utils/modbus/DockBean.kt

@@ -2,7 +2,7 @@ package com.grkj.ui_base.utils.modbus
 
 import com.google.gson.Gson
 import com.google.gson.reflect.TypeToken
-import com.grkj.ui_base.data.MMKVConstants
+import com.grkj.data.data.MMKVConstants
 import com.grkj.ui_base.utils.event.SwitchCollectionUpdateEvent
 import com.sik.sikcore.extension.getMMKVData
 import org.slf4j.Logger

+ 2 - 2
ui-base/src/main/java/com/grkj/ui_base/utils/modbus/ModBusCMDHelper.kt

@@ -130,8 +130,8 @@ object ModBusCMDHelper {
             byteArrayOf(
                 0x00,
                 0x11,
-                if (index == 1) 0b00100000.toByte() else 0b0000010.toByte(),
-                if (isOpen) 0xFF.toByte() else 0x00.toByte()
+                (if (index == 1) 0b00100000 else 0b00000010).toByte(),
+                (if (isOpen) 0xFF else 0x00).toByte()
             )
         )
     }

+ 27 - 44
ui-base/src/main/java/com/grkj/ui_base/utils/modbus/ModBusController.kt

@@ -6,6 +6,7 @@ import com.grkj.data.model.res.CabinetSlotsRecord
 import com.grkj.data.repository.IHardwareRepository
 import com.grkj.data.repository.impl.HardwareRepository
 import com.grkj.ui_base.R
+import com.grkj.ui_base.business.BleBusinessManager
 import com.grkj.ui_base.utils.CommonUtils
 import com.grkj.ui_base.utils.Executor
 import com.grkj.ui_base.utils.ble.BleConnectionManager
@@ -180,9 +181,9 @@ object ModBusController {
                         val rfid = res.copyOfRange(3, 11).toHexStrings(false).removeLeadingZeros()
                         logger.info("初始化锁具 RFID : $rfid")
                         updateLockRfid(dockBean.addr, idx, rfid)
-                        RepositoryManager.hardwareRepo.getLockInfo(rfid) {
-                            updateLockNewHardware(dockBean.addr, idx, it == null)
-                        }
+//                        RepositoryManager.hardwareRepo.getLockInfo(rfid) {
+//                            updateLockNewHardware(dockBean.addr, idx, it == null)
+//                        }
                     }
                 }
                 controlLockBuckle(false, dockBean.addr, hasLockIdxList)
@@ -947,55 +948,37 @@ object ModBusController {
     ): Pair<Byte, DockBean.KeyBean?>? {
         // 1. 过滤并准备钥匙列表
         val slotCols = exceptionSlots.mapNotNull { it.col?.toInt() }
-        val keyDockList = dockList
-            .filter { it.type == DeviceConst.DOCK_TYPE_KEY || it.type == DeviceConst.DOCK_TYPE_PORTABLE }
-            .sortedBy { it.addr }
-            .onEach { it.deviceList.sortBy { dev -> dev.idx } }
-
-        val keyList = keyDockList
-            .flatMap { it.deviceList }
-            .filterIsInstance<DockBean.KeyBean>()
-            .filterIndexed { idx, _ -> (idx + 1) !in slotCols }
-            .filter { kb ->
-                !kb.rfid.isNullOrEmpty()
-                        && kb.rfid !in exceptionKeysRfid
-                        && kb.mac !in exceptionKeysMac
-                        && !kb.mac.isNullOrEmpty()
-                        && kb.isExist
-            }
-            .shuffled()
-
+        val keyDockList =
+            dockList.filter { it.type == DeviceConst.DOCK_TYPE_KEY || it.type == DeviceConst.DOCK_TYPE_PORTABLE }
+                .sortedBy { it.addr }.onEach { it.deviceList.sortBy { dev -> dev.idx } }
+
+        val keyList = keyDockList.flatMap { it.deviceList }.apply {
+            logger.info("keyStatus:${this}")
+        }.filterIsInstance<DockBean.KeyBean>()
+            .filterIndexed { idx, _ -> (idx + 1) !in slotCols }.filter { kb ->
+                !kb.rfid.isNullOrEmpty() && kb.rfid !in exceptionKeysRfid && kb.mac !in exceptionKeysMac && !kb.mac.isNullOrEmpty() && kb.isExist
+            }.shuffled()
+
+        logger.info("蓝牙连接-获取到钥匙信息:${keyList}")
         if (keyList.isEmpty()) {
-            PopTip.tip(CommonUtils.getStr(R.string.no_available_key))
+            PopTip.tip(R.string.no_available_key)
             return null
         }
+        keyList.sortedBy { BleConnectionManager.getBleDeviceByMac(it.mac)?.token != null }
 
-        // —— 优先检查已经连接的 ——
-        val already = keyList.firstOrNull { kb ->
-            BleManager.getInstance().isConnected(kb.mac!!)  // mac 一定 non-null
-        }
-        if (already != null) {
-            val addr = keyDockList
-                .firstOrNull { dock ->
-                    dock.getKeyList().any { it.rfid == already.rfid }
-                }?.addr
-            if (addr != null) return addr to already
-        }
-
-        // —— 如果没有已连的,再顺序挂起尝试连接 ——
         for (kb in keyList) {
             val mac = kb.mac ?: continue
-            val found = suspendCoroutine<DockBean.KeyBean?> { cont ->
-                BleConnectionManager.registerConnectListener(mac) { isDone, _ ->
-                    if (isDone) cont.resume(kb)
+
+            val connected = BleConnectionManager.tryConnectWithOptionalCharge(mac)
+            if (connected) {
+                logger.info("蓝牙连接完成 :${mac}")
+                // 找到第一个能连的:从 keyDockList 里拿同 rfid 的 addr
+                val addr =
+                    keyDockList.firstOrNull { it.getKeyList().any { it.rfid == kb.rfid } }?.addr
+                if (addr != null) {
+                    return addr to kb
                 }
             }
-            if (found != null) {
-                val addr = keyDockList
-                    .firstOrNull { it.getKeyList().any { it.rfid == found.rfid } }
-                    ?.addr
-                return if (addr != null) addr to found else null
-            }
         }
 
         // 一个都没成功

+ 1 - 1
ui-base/src/main/java/com/grkj/ui_base/utils/modbus/ModBusManager.kt

@@ -2,7 +2,7 @@ package com.grkj.ui_base.utils.modbus
 
 import com.google.gson.Gson
 import com.google.gson.reflect.TypeToken
-import com.grkj.ui_base.data.MMKVConstants
+import com.grkj.data.data.MMKVConstants
 import com.grkj.ui_base.utils.Executor
 import com.grkj.ui_base.utils.extension.toHexStrings
 import com.sik.sikcore.extension.getMMKVData

+ 1 - 1
ui-base/src/main/java/com/grkj/ui_base/utils/modbus/PortManager.kt

@@ -2,7 +2,7 @@ package com.grkj.ui_base.utils.modbus
 
 import androidx.annotation.WorkerThread
 import com.epton.sdk.SerialPort
-import com.grkj.ui_base.data.MMKVConstants
+import com.grkj.data.data.MMKVConstants
 import com.kongzue.dialogx.dialogs.PopTip
 import com.sik.sikcore.extension.getMMKVData
 import com.sik.sikcore.thread.ThreadUtils

+ 2 - 0
ui-base/src/main/res/values-en/strings.xml

@@ -361,4 +361,6 @@
     <string name="action_succeed">action succeed</string>
     <string name="action_failed">action failed</string>
     <string name="select_coloker">select colocker</string>
+    <string name="ticket_data_error">Ticket data error</string>
+    <string name="job_already_finished">job already finished</string>
 </resources>

+ 2 - 0
ui-base/src/main/res/values-zh/strings.xml

@@ -361,4 +361,6 @@
     <string name="action_succeed">操作成功</string>
     <string name="action_failed">操作失败</string>
     <string name="select_coloker">请选择共锁人</string>
+    <string name="ticket_data_error">工作票数据损坏</string>
+    <string name="job_already_finished">该作业已被结束</string>
 </resources>

+ 2 - 0
ui-base/src/main/res/values/strings.xml

@@ -361,4 +361,6 @@
     <string name="action_succeed">操作成功</string>
     <string name="action_failed">操作失败</string>
     <string name="select_coloker">请选择共锁人</string>
+    <string name="ticket_data_error">工作票数据损坏</string>
+    <string name="job_already_finished">该作业已被结束</string>
 </resources>