Ver código fonte

feat(数据管理)
- 新增数据库备份Worker
- 新增步骤动作枚举
- 调整钥匙归还逻辑,增加强制上传数据选项

refactor(用户)
- 优化指纹登录和指纹校验逻辑,提高匹配效率
- 调整设置指纹界面的数据展示和删除逻辑

fix(流程)
- 修复流程导入时SHA256校验不一致的问题
- 修复作业票待办事项组装逻辑,确保不同类型的步骤能正确生成和关联

style(提示)
- 优化解压进度的文本展示格式
- 调整部分中英文提示文案

周文健 3 meses atrás
pai
commit
36ba9571f0
23 arquivos alterados com 314 adições e 197 exclusões
  1. 25 4
      app/src/main/java/com/grkj/iscs/ISCSApplication.kt
  2. 5 1
      app/src/main/java/com/grkj/iscs/features/main/activity/MainActivity.kt
  3. 12 0
      app/src/main/java/com/grkj/iscs/features/main/entity/WorkflowDataBundle.kt
  4. 10 7
      app/src/main/java/com/grkj/iscs/features/main/fragment/user_info/SetFingerprintFragment.kt
  5. 5 3
      app/src/main/java/com/grkj/iscs/features/main/viewmodel/WorkflowViewModel.kt
  6. 1 1
      app/src/main/res/values-en/strings.xml
  7. 1 1
      app/src/main/res/values-zh/strings.xml
  8. 1 1
      app/src/main/res/values/strings.xml
  9. 2 2
      data/src/main/java/com/grkj/data/database/ISCSDatabase.kt
  10. 61 0
      data/src/main/java/com/grkj/data/database/RoomBackupWorker.kt
  11. 8 0
      data/src/main/java/com/grkj/data/enums/StepAction.kt
  12. 9 0
      data/src/main/java/com/grkj/data/model/local/TodoStepJoin.kt
  13. 2 2
      data/src/main/java/com/grkj/data/repository/IUserRepository.kt
  14. 2 2
      data/src/main/java/com/grkj/data/repository/impl/network/NetworkUserRepository.kt
  15. 56 102
      data/src/main/java/com/grkj/data/repository/impl/standard/JobTicketRepository.kt
  16. 58 50
      data/src/main/java/com/grkj/data/repository/impl/standard/UserRepository.kt
  17. 1 1
      gradle/libs.versions.toml
  18. 5 1
      shared/src/main/java/com/grkj/shared/config/AESConfig.kt
  19. 2 2
      shared/src/main/java/com/grkj/shared/utils/BiometricVerifier.kt
  20. 45 17
      ui-base/src/main/java/com/grkj/ui_base/business/BleBusinessManager.kt
  21. 1 0
      ui-base/src/main/res/values-en/strings.xml
  22. 1 0
      ui-base/src/main/res/values-zh/strings.xml
  23. 1 0
      ui-base/src/main/res/values/strings.xml

+ 25 - 4
app/src/main/java/com/grkj/iscs/ISCSApplication.kt

@@ -3,12 +3,16 @@ package com.grkj.iscs
 import android.app.AlarmManager
 import android.app.Application
 import android.app.PendingIntent
-import android.bluetooth.BluetoothDevice
 import android.content.Context
 import android.content.Intent
+import androidx.work.Constraints
+import androidx.work.ExistingPeriodicWorkPolicy
+import androidx.work.PeriodicWorkRequestBuilder
+import androidx.work.WorkManager
 import ch.qos.logback.classic.Level
 import com.drake.statelayout.StateConfig
 import com.grkj.data.data.EventConstants
+import com.grkj.data.database.RoomBackupWorker
 import com.grkj.data.di.RepositoryManager
 import com.grkj.iscs.features.splash.activity.SplashActivity
 import com.grkj.shared.model.EventBean
@@ -27,18 +31,16 @@ import com.scwang.smart.refresh.layout.api.RefreshLayout
 import com.scwang.smart.refresh.layout.listener.DefaultRefreshFooterCreator
 import com.scwang.smart.refresh.layout.listener.DefaultRefreshHeaderCreator
 import com.sik.sikcore.SIKCore
-import com.sik.sikcore.bluetooth.BLEScanner
-import com.sik.sikcore.bluetooth.IBluetoothScanCallback
 import com.sik.sikcore.log.LogUtils
 import com.sik.sikcore.thread.ThreadUtils
 import dagger.hilt.android.HiltAndroidApp
-import kotlinx.coroutines.delay
 import me.jessyan.autosize.AutoSizeConfig
 import org.greenrobot.eventbus.EventBus
 import org.greenrobot.eventbus.Subscribe
 import org.greenrobot.eventbus.ThreadMode
 import org.slf4j.Logger
 import org.slf4j.LoggerFactory
+import java.util.concurrent.TimeUnit
 
 
 /**
@@ -48,6 +50,19 @@ import org.slf4j.LoggerFactory
 class ISCSApplication : Application() {
     private val logger: Logger = LoggerFactory.getLogger(ISCSApplication::class.java)
 
+    /**
+     * 数据库备份
+     */
+    val backupWork = PeriodicWorkRequestBuilder<RoomBackupWorker>(
+        1, TimeUnit.DAYS
+    )
+        .setConstraints(
+            Constraints.Builder()
+                .setRequiresBatteryNotLow(true)
+                .build()
+        )
+        .build()
+
     /**
      * 程序创建
      */
@@ -73,6 +88,12 @@ class ISCSApplication : Application() {
             RepositoryManager.init(this@ISCSApplication)
         }
         StateConfig.emptyLayout = com.grkj.ui_base.R.layout.layout_empty
+        WorkManager.getInstance(this)
+            .enqueueUniquePeriodicWork(
+                "room_backup_work",
+                ExistingPeriodicWorkPolicy.KEEP,
+                backupWork
+            )
     }
 
     @Subscribe(threadMode = ThreadMode.MAIN)

+ 5 - 1
app/src/main/java/com/grkj/iscs/features/main/activity/MainActivity.kt

@@ -126,7 +126,11 @@ class MainActivity() : BaseActivity<ActivityMainBinding>() {
         binding.userInfoLayout.setOnClickListener {
             if (MainDomainData.permissions.contains(RoleFunctionalPermissionsEnum.USER_INFO_HOME.functionalPermission)) {
                 binding.navBar.isVisible = true
-                replaceNavGraph(R.navigation.nav_user_info)
+                if (navController.graph.id == R.navigation.nav_user_info) {
+                    navController.popBackStack(R.id.userInfoHomeFragment, false)
+                } else {
+                    replaceNavGraph(R.navigation.nav_user_info)
+                }
             }
         }
         navController.addOnDestinationChangedListener { _, destination, _ ->

+ 12 - 0
app/src/main/java/com/grkj/iscs/features/main/entity/WorkflowDataBundle.kt

@@ -0,0 +1,12 @@
+package com.grkj.iscs.features.main.entity
+
+import com.grkj.data.model.dos.WorkflowMode
+import com.grkj.data.model.dos.WorkflowStep
+
+/**
+ * 流程模式数据包装
+ */
+data class WorkflowDataBundle(
+    val mode: WorkflowMode,
+    val steps: List<WorkflowStep>
+)

+ 10 - 7
app/src/main/java/com/grkj/iscs/features/main/fragment/user_info/SetFingerprintFragment.kt

@@ -9,6 +9,7 @@ import com.drake.brv.utils.divider
 import com.drake.brv.utils.linear
 import com.drake.brv.utils.models
 import com.drake.brv.utils.setup
+import com.grkj.data.model.vo.FingerprintDataVo
 import com.grkj.data.model.vo.SysBiometricDataVo
 import com.grkj.iscs.R
 import com.grkj.iscs.databinding.FragmentSetFingerprintBinding
@@ -52,13 +53,18 @@ class SetFingerprintFragment : BaseFragment<FragmentSetFingerprintBinding>() {
             navController.popBackStack()
         }
         binding.add.setDebouncedClickListener {
+            mFingerprintPressTimes = 0
+            mFingerprintInputErrorTimes = 0
             fingerprintTempData.clear()
             AddFingerprintDialog.show({
                 FingerprintUtil.stop()
-
                 it.dismiss()
             }) {
                 pressTip = it
+                pressTip?.text = getString(
+                    com.grkj.ui_base.R.string.fingerprint_scan_tip,
+                    maxPressTimes - mFingerprintPressTimes
+                )
             }.apply {
                 startCaptureFingerprint(this)
             }
@@ -93,10 +99,10 @@ class SetFingerprintFragment : BaseFragment<FragmentSetFingerprintBinding>() {
             this.endVisible = true
             this.orientation = DividerOrientation.VERTICAL
         }.setup {
-            addType<SysBiometricDataVo>(R.layout.item_set_fingerprint)
+            addType<FingerprintDataVo>(R.layout.item_set_fingerprint)
             onBind {
                 val itemBinding = getBinding<ItemSetFingerprintBinding>()
-                val item = getModel<SysBiometricDataVo>()
+                val item = getModel<FingerprintDataVo>()
                 itemBinding.fingerprintCode.text =
                     getString(R.string.fingerprint_code_str, modelPosition)
                 itemBinding.delete.setDebouncedClickListener {
@@ -107,10 +113,7 @@ class SetFingerprintFragment : BaseFragment<FragmentSetFingerprintBinding>() {
                         ).toString(),
                         countDownTime = 10,
                         onConfirmClick = {
-                            if (item.content.isNotEmpty()) {
-                                item.content.deleteIfExists()
-                            }
-                            viewModel.deleteFingerprintByIds(listOf(item.recordId))
+                            viewModel.deleteFingerprintByIds(item.fingerprintData.map { it.recordId })
                                 .observe(this@SetFingerprintFragment) {
                                     if (it) {
                                         TipDialog.showSuccess(getString(R.string.delete_success))

+ 5 - 3
app/src/main/java/com/grkj/iscs/features/main/viewmodel/WorkflowViewModel.kt

@@ -9,6 +9,7 @@ import com.grkj.data.model.dos.WorkflowMode
 import com.grkj.data.model.vo.WorkflowModeVo
 import com.grkj.data.repository.IWorkflowRepository
 import com.grkj.iscs.R
+import com.grkj.iscs.features.main.entity.WorkflowDataBundle
 import com.grkj.iscs.features.main.entity.WorkflowExportPackage
 import com.grkj.shared.config.AESConfig
 import com.grkj.ui_base.base.BaseViewModel
@@ -114,8 +115,9 @@ class WorkflowViewModel @Inject constructor(val workflowRepository: IWorkflowRep
                                 return
                             }
                             if (MessageDigestUtils.getMode(MessageDigestTypes.SHA256)
-                                    .digestToHex(workflowModeData.readBytes()) != workflowModeSha.readText()
+                                    .digestToHex(workflowModeData.readBytes()) != workflowModeSha.readText().uppercase()
                             ) {
+                                LoadingEvent.sendLoadingEvent()
                                 TipDialog.showError(
                                     CommonUtils.getStr(R.string.data_file_is_corrupted).toString()
                                 )
@@ -128,10 +130,10 @@ class WorkflowViewModel @Inject constructor(val workflowRepository: IWorkflowRep
                                 logger.info("流程模式字符串: $workflowModeDataJson")
                                 try {
                                     val workflowExportPackage =
-                                        Gson().fromJson<WorkflowExportPackage<List<WorkflowModeVo>>>(
+                                        Gson().fromJson<WorkflowExportPackage<WorkflowDataBundle>>(
                                             workflowModeDataJson,
                                             object :
-                                                TypeToken<WorkflowExportPackage<List<WorkflowModeVo>>>() {}.type
+                                                TypeToken<WorkflowExportPackage<WorkflowDataBundle>>() {}.type
                                         )
 
                                 } catch (e: Exception) {

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

@@ -521,7 +521,7 @@
     <string name="please_press_fingerprint_again">Please press fingerprint again</string>
     <string name="group_job_in_progress">Group job in progress</string>
     <string name="file_not_exists">File not exists</string>
-    <string name="unzip">Unzip...%s</string>
+    <string name="unzip">Unzip……%1$.2f%%</string>
     <string name="the_verification_file_not_exists">The verification file does not exist</string>
     <string name="data_file_not_exists">Data file not exists</string>
     <string name="data_file_is_corrupted">Data file is corrupted</string>

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

@@ -521,7 +521,7 @@
     <string name="please_press_fingerprint_again">请再次按压指纹</string>
     <string name="group_job_in_progress">分组作业进行中</string>
     <string name="file_not_exists">文件不存在</string>
-    <string name="unzip">解压中...%s</string>
+    <string name="unzip">解压中……%1$.2f%%</string>
     <string name="the_verification_file_not_exists">校验文件不存在</string>
     <string name="data_file_not_exists">数据文件不存在</string>
     <string name="data_file_is_corrupted">数据文件已损坏</string>

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

@@ -524,7 +524,7 @@
     <string name="please_press_fingerprint_again">请再次按压指纹</string>
     <string name="group_job_in_progress">分组作业进行中</string>
     <string name="file_not_exists">文件不存在</string>
-    <string name="unzip">解压中...%.2f</string>
+    <string name="unzip">解压中……%1$.2f%%</string>
     <string name="the_verification_file_not_exists">校验文件不存在</string>
     <string name="data_file_not_exists">数据文件不存在</string>
     <string name="data_file_is_corrupted">数据文件已损坏</string>

+ 2 - 2
data/src/main/java/com/grkj/data/database/ISCSDatabase.kt

@@ -50,11 +50,11 @@ abstract class ISCSDatabase : RoomDatabase() {
     abstract fun exceptionDao(): ExceptionDao
 
     companion object {
-        private const val DB_NAME = "iscs_database.db"
+        const val DB_NAME = "iscs_database.db"
         private val logger: Logger = LoggerFactory.getLogger(ISCSDatabase::class.java)
 
         // 外部存储目录路径
-        private val EXTERNAL_DB_FILE: File by lazy {
+        val EXTERNAL_DB_FILE: File by lazy {
             File(Environment.getExternalStorageDirectory(), "ISCS/database/$DB_NAME")
         }
 

+ 61 - 0
data/src/main/java/com/grkj/data/database/RoomBackupWorker.kt

@@ -0,0 +1,61 @@
+package com.grkj.data.database
+
+import android.content.Context
+import android.database.sqlite.SQLiteDatabase
+import androidx.work.Worker
+import androidx.work.WorkerParameters
+import com.grkj.data.utils.FileStorageUtils.exists
+import com.sik.sikcore.date.TimeUtils
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileOutputStream
+
+class RoomBackupWorker(
+    context: Context,
+    params: WorkerParameters
+) : Worker(context, params) {
+
+    override fun doWork(): Result {
+        return try {
+            // 1️⃣ 找到数据库文件
+            val dbName = ISCSDatabase.DB_NAME
+            val dbFile = ISCSDatabase.EXTERNAL_DB_FILE
+
+            // 3️⃣ 准备备份目录
+            val backupDir = File(
+                ISCSDatabase.EXTERNAL_DB_FILE.parent,
+                "room_backups"
+            ).apply { if (!exists()) mkdirs() }
+
+            // 4️⃣ 要拷贝的三个文件
+            val suffixes = listOf("", "-wal", "-shm")
+            val timeStamp = TimeUtils.nowString("yyyyMMdd_HHmmss")
+
+            suffixes.forEach { suffix ->
+                val src = File(dbFile.parent, "$dbName$suffix")
+                if (src.exists()) {
+                    val dest = File(backupDir, "backup_${timeStamp}${suffix}.db")
+                    FileInputStream(src).use { inp ->
+                        FileOutputStream(dest).use { out ->
+                            inp.copyTo(out)
+                        }
+                    }
+                }
+            }
+
+            // 5️⃣ (可选)清理过旧备份,比如只留最近 30 份
+            cleanupOldBackups(backupDir, keepCount = 30)
+
+            return Result.success()
+        } catch (e: Exception) {
+            e.printStackTrace()
+            Result.retry()
+        }
+    }
+
+    private fun cleanupOldBackups(dir: File, keepCount: Int) {
+        val files = dir.listFiles { f -> f.name.startsWith("backup_") }
+            ?.sortedByDescending { it.lastModified() } ?: return
+        files.drop(keepCount).forEach { it.delete() }
+    }
+}

+ 8 - 0
data/src/main/java/com/grkj/data/enums/StepAction.kt

@@ -0,0 +1,8 @@
+package com.grkj.data.enums
+
+/**
+ * 步骤动作
+ */
+enum class StepAction {
+    LOCK, UNLOCK, COLOCK, RELEASE_COLOCK, END_JOB, CONFIRM
+}

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

@@ -1,6 +1,7 @@
 package com.grkj.data.model.local
 
 import com.grkj.data.enums.RoleEnum
+import com.grkj.data.enums.StepAction
 
 data class TodoStepJoin(
     // ----- 基础定位字段 -----
@@ -66,6 +67,14 @@ fun TodoStepJoin.isMyTodo(currentUserId: Long, currentUserName: String): Boolean
     return !hasAnyHardwareOperationFunction() && confirmToCurrentUser
 }
 
+fun TodoStepJoin.only(action: StepAction) = copy(
+    enableLock           = action == StepAction.LOCK,
+    enableUnlock         = action == StepAction.UNLOCK,
+    enableColock         = action == StepAction.COLOCK,
+    enableReleaseColock  = action == StepAction.RELEASE_COLOCK,
+    enableEndJob         = action == StepAction.END_JOB
+)
+
 // WorkflowStep 扩展:判定是否包含任何硬件相关动作
 fun TodoStepJoin.hasAnyHardwareOperationFunction(): Boolean =
     enableLock || enableColock || enableReleaseColock || enableUnlock

+ 2 - 2
data/src/main/java/com/grkj/data/repository/IUserRepository.kt

@@ -32,12 +32,12 @@ interface IUserRepository {
     /**
      * 指纹登录
      */
-    fun loginWithFingerprint(fingerprint: String): Boolean
+    suspend fun loginWithFingerprint(fingerprint: String): Boolean
 
     /**
      * 检查指纹
      */
-    fun checkFingerprint(fingerprint: String): Boolean
+    suspend fun checkFingerprint(fingerprint: String): Boolean
 
     /**
      * 人脸登录

+ 2 - 2
data/src/main/java/com/grkj/data/repository/impl/network/NetworkUserRepository.kt

@@ -34,11 +34,11 @@ class NetworkUserRepository @Inject constructor() : BaseRepository(), IUserRepos
         TODO("Not yet implemented")
     }
 
-    override fun loginWithFingerprint(fingerprint: String): Boolean {
+    override suspend fun loginWithFingerprint(fingerprint: String): Boolean {
         TODO("Not yet implemented")
     }
 
-    override fun checkFingerprint(fingerprint: String): Boolean {
+    override suspend fun checkFingerprint(fingerprint: String): Boolean {
         TODO("Not yet implemented")
     }
 

+ 56 - 102
data/src/main/java/com/grkj/data/repository/impl/standard/JobTicketRepository.kt

@@ -12,6 +12,7 @@ import com.grkj.data.enums.IsolationPointPowerTypeEnum
 import com.grkj.data.enums.JobTicketStatusEnum
 import com.grkj.data.enums.NextJobPrompt
 import com.grkj.data.enums.RoleEnum
+import com.grkj.data.enums.StepAction
 import com.grkj.data.model.dos.IsJobTicket
 import com.grkj.data.model.dos.IsJobTicketGroup
 import com.grkj.data.model.dos.IsJobTicketKey
@@ -22,6 +23,7 @@ import com.grkj.data.model.dos.IsJobTicketUser
 import com.grkj.data.model.dos.WorkflowStep
 import com.grkj.data.model.local.TodoStepJoin
 import com.grkj.data.model.local.isMyTodo
+import com.grkj.data.model.local.only
 import com.grkj.data.model.req.LockPointUpdateReq
 import com.grkj.data.model.res.StepDetailRes
 import com.grkj.data.model.res.TicketDetailRes
@@ -246,9 +248,6 @@ class JobTicketRepository @Inject constructor(
         //1.获取所有用户参与的作业票
         val tickets = jobTicketDao.getTicketThatUserJoin(userId)
         if (tickets.isEmpty()) return emptyList()
-        // 2. 获取票对应的流程步骤定义(模板)
-        val modeIdToSteps = workflowStepDao.getStepsByModes(tickets.map { it.modeId }.distinct())
-            .groupBy { it.modeId }
         // 3. 获取作业票步骤实例
         val stepInstances = jobTicketDao.getStepsByTicketIds(tickets.map { it.ticketId })
         // 4. 获取钥匙记录(上锁/解锁 取/还钥匙逻辑)
@@ -264,7 +263,6 @@ class JobTicketRepository @Inject constructor(
         // 8. 构建待办项 TodoStepJoin
         val todoList = assembleTodoStepJoins(
             tickets,
-            modeIdToSteps,
             stepInstances,
             keyRecords,
             groups,
@@ -282,7 +280,6 @@ class JobTicketRepository @Inject constructor(
      */
     private fun assembleTodoStepJoins(
         tickets: List<IsJobTicket>,
-        modeSteps: Map<Long, List<WorkflowStep>>,
         stepInstances: List<IsJobTicketStep>,
         keyRecords: List<IsJobTicketKey>,
         groups: List<IsJobTicketGroup>,
@@ -299,112 +296,69 @@ class JobTicketRepository @Inject constructor(
             val lockerList = lockers.filter { it.ticketId == ticketId }
             val colockerList = colockers.filter { it.ticketId == ticketId }
             val ticketSteps = stepInstances.filter { it.ticketId == ticketId }
-            val workflowSteps = modeSteps[ticket.modeId] ?: emptyList()
-            val previousStepJoin: MutableList<TodoStepJoin> = mutableListOf()
+                .sortedBy { it.stepIndex }
             val currentStepId =
                 ticketSteps.filter { isJobTicketStep -> isJobTicketStep.stepStatus == "0" }
                     .minByOrNull { isJobTicketStep -> isJobTicketStep.stepIndex }?.stepId
-            ticketSteps.sortedBy { it.stepIndex }.forEach { step ->
-                when {
-                    // 🔐 JTLOCKER 处理:上锁 or 解锁 ➜ 必须按 group 拆
-                    step.enableLock || step.enableUnlock -> {
-                        val type = if (step.enableLock) 0 else 1
-                        val keys = keyRecords.filter {
-                            it.ticketId == ticketId && it.ticketType == type && it.delFlag == "0"
-                        }
+            val previousStepJoin: MutableList<TodoStepJoin> = mutableListOf()
+            ticketSteps.sortedBy { it.stepIndex }.forEach { stepDef ->
 
-                        // 如果 key 记录没有,说明尚未开始取钥匙,按 groupId 拆分一条占位待办
-                        if (keys.isEmpty()) {
-                            val groupStepJoin = groupList.map {
-                                buildTodoStepJoin(
-                                    step,
-                                    ticket,
-                                    step,
-                                    lockerList,
-                                    colockerList,
-                                    it.id,
-                                    it.groupName,
-                                    null
-                                )
-                            }
-                            groupStepJoin.forEach {
-                                if (!previousStepJoin.isEmpty()) {
-                                    it.previousTodoStepJoin = previousStepJoin.toList()
-                                }
-                            }
-                            result += groupStepJoin
-                            if (currentStepId == step.stepId) {
-                                previousStepJoin.addAll(groupStepJoin)
-                            }
-                        } else {
-                            val keysStepJoin = keys.map { key ->
-                                buildTodoStepJoin(
-                                    step,
-                                    ticket,
-                                    step,
-                                    lockerList,
-                                    colockerList,
-                                    key.groupId,
-                                    groupList.find { group -> group.id == key.groupId }?.groupName,
-                                    key
-                                )
-                            }
-                            keysStepJoin.forEach {
-                                if (!previousStepJoin.isEmpty()) {
-                                    it.previousTodoStepJoin = previousStepJoin.toList()
-                                }
-                            }
-                            result += keysStepJoin
-                            if (currentStepId == step.stepId) {
-                                previousStepJoin.addAll(keysStepJoin)
-                            }
-                        }
-                    }
+                val stepJoins = mutableListOf<TodoStepJoin>()  // 本 step 产生的所有 Todo
 
-                    // 🤝 JTCOLOCKER / RELEASE_COLOCK(不拆 group)
-                    step.enableColock || step.enableReleaseColock -> {
-                        val temp =
-                            buildTodoStepJoin(
-                                step,
-                                ticket,
-                                step,
-                                lockerList,
-                                colockerList,
-                                null,
-                                "",
-                                null
-                            )
-                        if (!previousStepJoin.isEmpty()) {
-                            temp.previousTodoStepJoin = previousStepJoin.toList()
-                        }
-                        previousStepJoin.clear()
-                        result += temp
-                        if (currentStepId == step.stepId) {
-                            previousStepJoin.add(temp)
-                        }
-                    }
+                /** 工具:给待办挂上前置链并收集 **/
+                fun collect(join: TodoStepJoin) {
+                    if (previousStepJoin.isNotEmpty()) join.previousTodoStepJoin = previousStepJoin.toList()
+                    stepJoins += join
+                }
 
-                    // 除了特殊的剩下的都是步骤确认
-                    else -> {
-                        val temp = buildTodoStepJoin(
-                            step,
-                            ticket,
-                            step,
-                            lockerList,
-                            colockerList,
-                            null,
-                            "",
-                            null
-                        )
-                        if (!previousStepJoin.isEmpty()) {
-                            temp.previousTodoStepJoin = previousStepJoin.toList()
+                /* 1️⃣ Lock / Unlock(按 group 拆) */
+                if (stepDef.enableLock || stepDef.enableUnlock) {
+                    val type = if (stepDef.enableLock) 0 else 1
+                    val keys = keyRecords.filter { it.ticketId == ticketId && it.ticketType == type && it.delFlag == "0" }
+
+                    val lockJoins = if (keys.isEmpty()) {
+                        // 没取钥匙 → 每个 group 生成一条占位
+                        groupList.map { g ->
+                            buildTodoStepJoin(stepDef, ticket, stepDef, lockerList, colockerList,
+                                g.id, g.groupName, null).only(if (stepDef.enableLock) StepAction.LOCK else StepAction.UNLOCK)
                         }
-                        previousStepJoin.clear()
-                        result += temp
-                        if (currentStepId == step.stepId) {
-                            previousStepJoin.add(temp)
+                    } else {
+                        // 有钥匙记录
+                        keys.map { k ->
+                            buildTodoStepJoin(stepDef, ticket, stepDef, lockerList, colockerList,
+                                k.groupId, groupList.firstOrNull { it.id == k.groupId }?.groupName, k).only(if (stepDef.enableLock) StepAction.LOCK else StepAction.UNLOCK)   // ⚠️
                         }
                     }
+                    lockJoins.forEach(::collect)
+                }
+
+                /* 2️⃣ Colock / Release(不拆 group) */
+                if (stepDef.enableColock || stepDef.enableReleaseColock) {
+                    collect(
+                        buildTodoStepJoin(stepDef, ticket, stepDef, lockerList, colockerList,
+                            null, "", null).only(
+                            if (stepDef.enableColock)
+                                StepAction.COLOCK
+                            else
+                                StepAction.RELEASE_COLOCK
+                        ) // ⚠️
+                    )
+                }
+
+                /* 3️⃣ 普通确认 / End‑Job */
+                val needConfirm = !stepDef.hasAnyHardwareOperationFunction() || stepDef.enableEndJob
+                if (needConfirm) {
+                    val action = if (stepDef.enableEndJob) StepAction.END_JOB else StepAction.CONFIRM
+                    collect(
+                        buildTodoStepJoin(stepDef, ticket, stepDef, lockerList, colockerList,
+                            null, "", null).only(action) // ⚠️
+                    )
+                }
+
+                /* ⭐ 将本 step 的 Todo 归档,并维护前置链 ⭐ */
+                result += stepJoins
+                if (currentStepId == stepDef.stepId) {
+                    previousStepJoin.addAll(stepJoins)
                 }
             }
         }

+ 58 - 50
data/src/main/java/com/grkj/data/repository/impl/standard/UserRepository.kt

@@ -23,6 +23,10 @@ import com.grkj.shared.utils.BCryptUtils
 import com.grkj.shared.utils.BiometricVerifier
 import com.sik.sikcore.extension.deleteIfExists
 import com.sik.sikcore.extension.file
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.coroutineScope
 import javax.inject.Inject
 import javax.inject.Singleton
 
@@ -98,71 +102,75 @@ class UserRepository @Inject constructor(
         } == true
     }
 
-    override fun loginWithFingerprint(fingerprint: String): Boolean {
+    override suspend fun loginWithFingerprint(fingerprint: String): Boolean = coroutineScope {
         val fingerprintDataList = userDao.getFingerprintData()
-        if (fingerprintDataList.isEmpty()) {
-            return false
-        }
-        var hasFingerprint = false
-        var userId: String? = null
-        for (fingerprintData in fingerprintDataList) {
-            if (fingerprintData.content.isNotEmpty()) {
-                val fileData = fingerprintData.content
-                if (BiometricVerifier.verifyFingerprint(fingerprint, fileData)) {
-                    hasFingerprint = true
-                    userId = fingerprintData.userId.toString()
-                    break
-                }
+        if (fingerprintDataList.isEmpty()) return@coroutineScope false
+
+        /* ① 并发计算匹配分数 */
+        val deferred = fingerprintDataList.map { fpData ->
+            async(Dispatchers.Default) {
+                if (fpData.content.isEmpty()) return@async null
+                val score =
+                    BiometricVerifier.verifyFingerprint(fingerprint, fpData.content) // suspend
+                if (score >= 40) Pair(fpData, score) else null
             }
         }
-        if (userId != null) {
-            val sysUserDo = userDao.getUserInfoByUserId(userId)
+
+        /* ② 找到最高分(若无满足阈值则 null) */
+        val bestMatch = deferred.awaitAll()
+            .filterNotNull()
+            .maxByOrNull { it.second }    // second == score
+
+        /* ③ 若匹配成功,加载用户信息 */
+        if (bestMatch != null) {
+            val fpData = bestMatch.first
+            val sysUserDo = userDao.getUserInfoByUserId(fpData.userId.toString())
             if (sysUserDo != null) {
+                // —— 缓存域数据 —— //
                 MainDomainData.userInfo = sysUserDo
-                val userCardList = hardwareDao.getIsJobCardByUserId(sysUserDo.userId)
-                val roleDatas = roleDao.getRoleDataByUserId(sysUserDo.userId)
-                val userBiometricDataVo = userDao.getUserBiometricData(sysUserDo.userId)
-                MainDomainData.userBiometricDataVo = userBiometricDataVo
-                MainDomainData.userCardList = userCardList
-                MainDomainData.roleKeys = roleDatas.joinToString(",") { it.roleKey }
+                MainDomainData.userCardList = hardwareDao.getIsJobCardByUserId(sysUserDo.userId)
+                MainDomainData.roleKeys =
+                    roleDao.getRoleDataByUserId(sysUserDo.userId).joinToString(",") { it.roleKey }
                 MainDomainData.permissions =
-                    sysMenuDao.getPermissionsByRoleIds(roleDatas.map { it.roleId })
-                logger.info("用户信息:{}", MainDomainData.userInfo.toString())
-                logger.info("用户角色:{}", MainDomainData.roleKeys)
-                logger.info("用户权限:{}", MainDomainData.permissions)
-                hasFingerprint = true
-            } else {
-                hasFingerprint = false
+                    sysMenuDao.getPermissionsByRoleIds(
+                        roleDao.getRoleDataByUserId(sysUserDo.userId).map { it.roleId }
+                    )
+                MainDomainData.userBiometricDataVo = userDao.getUserBiometricData(sysUserDo.userId)
+
+                logger.info("用户信息: {}", MainDomainData.userInfo)
+                logger.info("用户角色: {}", MainDomainData.roleKeys)
+                logger.info("用户权限: {}", MainDomainData.permissions)
+                return@coroutineScope true
             }
-        } else {
-            hasFingerprint = false
         }
-        return hasFingerprint
+        false
     }
 
-    override fun checkFingerprint(fingerprint: String): Boolean {
+    override suspend fun checkFingerprint(fingerprint: String): Boolean = coroutineScope {
         val fingerprintDataList = userDao.getFingerprintData()
-        if (fingerprintDataList.isEmpty()) {
-            return false
-        }
-        var hasFingerprint = false
-        var userId: String? = null
-        for (fingerprintData in fingerprintDataList) {
-            if (fingerprintData.content.isNotEmpty()) {
-                val fileData = fingerprintData.content.file().readText()
-                if (BiometricVerifier.verifyFingerprint(fingerprint, fileData)) {
-                    userId = fingerprintData.userId.toString()
-                    break
-                }
+        if (fingerprintDataList.isEmpty()) return@coroutineScope false
+
+        /* ① 并发计算匹配分数 */
+        val deferred = fingerprintDataList.map { fpData ->
+            async(Dispatchers.Default) {
+                if (fpData.content.isEmpty()) return@async null
+                val score =
+                    BiometricVerifier.verifyFingerprint(fingerprint, fpData.content) // suspend
+                if (score >= 40) Pair(fpData, score) else null
             }
         }
-        if (userId != null) {
-            val sysUserDo = userDao.getUserInfoByUserId(userId)
-            hasFingerprint = sysUserDo != null
+
+        /* ② 找到最高分(若无满足阈值则 null) */
+        val bestMatch = deferred.awaitAll()
+            .filterNotNull()
+            .maxByOrNull { it.second }    // second == score
+        if (bestMatch != null) {
+            val fpData = bestMatch.first
+            val sysUserDo = userDao.getUserInfoByUserId(fpData.userId.toString())
+            sysUserDo != null
         } else {
-            hasFingerprint = false
+            false
         }
-        return hasFingerprint
     }
 
     override fun loginWithFace(face: String): Boolean {

+ 1 - 1
gradle/libs.versions.toml

@@ -11,7 +11,7 @@ material = "1.10.0"
 activity = "1.8.0"
 constraintlayout = "2.1.4"
 jetbrainsKotlinJvm = "2.0.21"
-sikextension = "1.1.61"
+sikextension = "1.1.63"
 sikcamera = "1.0.11"
 sikcronjob = "1.0.3"
 sikfontmanager = "1.0.2"

+ 5 - 1
shared/src/main/java/com/grkj/shared/config/AESConfig.kt

@@ -1,5 +1,6 @@
 package com.grkj.shared.config
 
+import com.sik.sikcore.data.ConvertUtils
 import com.sik.sikencrypt.EncryptAlgorithm
 import com.sik.sikencrypt.EncryptMode
 import com.sik.sikencrypt.EncryptPadding
@@ -24,7 +25,7 @@ class AESConfig : IEncryptConfig {
     }
 
     override fun key(): ByteArray {
-        return aesKey().substring(0, 16).toByteArray()
+        return aesKey().toByteArray()
     }
 
     override fun mode(): EncryptMode {
@@ -35,5 +36,8 @@ class AESConfig : IEncryptConfig {
         return EncryptPadding.PKCS5Padding
     }
 
+    override val composeIV: Boolean
+        get() = true
+
     external fun aesKey(): String
 }

+ 2 - 2
shared/src/main/java/com/grkj/shared/utils/BiometricVerifier.kt

@@ -13,7 +13,7 @@ object BiometricVerifier {
      * @param b64b 指纹2 的 Base64 图像
      * @return 两者是否认为同一人(score >= threshold)
      */
-    fun verifyFingerprint(b64a: String, b64b: String, threshold: Double = 40.0): Boolean {
+    suspend fun verifyFingerprint(b64a: String, b64b: String): Double {
         // 1. 解码成 Bitmap
         val bmpA = Base64.decode(b64a, Base64.DEFAULT)
         val bmpB = Base64.decode(b64b, Base64.DEFAULT)
@@ -28,7 +28,7 @@ object BiometricVerifier {
 
         // 3. 比对
         val score = FingerprintMatcher(templateA).match(templateB)
-        return score >= threshold
+        return score
     }
 
     /**

+ 45 - 17
ui-base/src/main/java/com/grkj/ui_base/business/BleBusinessManager.kt

@@ -522,19 +522,43 @@ object BleBusinessManager {
                     }
                 } else {
                     // 当前策略:作业票未完成禁止归还钥匙
-                    TipDialog.show(
-                        msg = CommonUtils.getStr(R.string.key_return_tip)!!, onConfirmClick = {
-                            LoadingEvent.sendLoadingEvent()
-                            PopTip.build().tip(CommonUtils.getStr(R.string.continue_the_ticket))
-                            BleManager.getInstance().disconnect(bleDevice)
-                            BleConnectionManager.deviceList.removeIf { it.bleDevice.mac == bleDevice.mac }
-                            // 打开卡扣,防止初始化的时候选择不处理钥匙导致无法使用
-                            val dock = ModBusController.getDockByKeyMac(bleDevice.mac)
-                            val keyBean = dock?.getKeyList()?.find { it.mac == bleDevice.mac }
-                            keyBean?.let {
-                                ModBusController.controlKeyBuckle(true, keyBean.idx, dock.addr)
-                            }
-                        })
+                    LoadingEvent.sendLoadingEvent()
+                    if (workTicketGet.data?.any { it.dataList?.any { it.target == 0 } == true } == true) {
+                        TipDialog.show(
+                            msg = CommonUtils.getStr(R.string.lock_key_return_tip)!!,
+                            onConfirmClick = {
+                                handleKeyReturn(
+                                    bleDevice,
+                                    workTicketGet,
+                                    finishedStatus.second,
+                                    forceReturn = true
+                                )
+                            },
+                            onCancelClick = {
+                                PopTip.build().tip(CommonUtils.getStr(R.string.continue_the_ticket))
+                                BleManager.getInstance().disconnect(bleDevice)
+                                BleConnectionManager.deviceList.removeIf { it.bleDevice.mac == bleDevice.mac }
+                                // 打开卡扣,防止初始化的时候选择不处理钥匙导致无法使用
+                                val dock = ModBusController.getDockByKeyMac(bleDevice.mac)
+                                val keyBean = dock?.getKeyList()?.find { it.mac == bleDevice.mac }
+                                keyBean?.let {
+                                    ModBusController.controlKeyBuckle(true, keyBean.idx, dock.addr)
+                                }
+                            })
+                    } else {
+                        TipDialog.show(
+                            msg = CommonUtils.getStr(R.string.key_return_tip)!!, onConfirmClick = {
+                                PopTip.build().tip(CommonUtils.getStr(R.string.continue_the_ticket))
+                                BleManager.getInstance().disconnect(bleDevice)
+                                BleConnectionManager.deviceList.removeIf { it.bleDevice.mac == bleDevice.mac }
+                                // 打开卡扣,防止初始化的时候选择不处理钥匙导致无法使用
+                                val dock = ModBusController.getDockByKeyMac(bleDevice.mac)
+                                val keyBean = dock?.getKeyList()?.find { it.mac == bleDevice.mac }
+                                keyBean?.let {
+                                    ModBusController.controlKeyBuckle(true, keyBean.idx, dock.addr)
+                                }
+                            })
+                    }
                 }
             }
         }
@@ -610,21 +634,24 @@ object BleBusinessManager {
      * todo 自定义作业流程需要修改
      */
     private fun handleKeyReturn(
-        bleDevice: BleDevice, workTicketGet: WorkTicketGet?, ticketFinished: Boolean
+        bleDevice: BleDevice,
+        workTicketGet: WorkTicketGet?,
+        ticketFinished: Boolean,
+        forceReturn: Boolean = false
     ) {
         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) {
+        if (ticketFinished && !forceReturn) {
             switchReadyMode(bleDevice)
         } else {
             // 上报隔离点状态
             val keyNfc = ModBusController.getKeyByMac(bleDevice.mac)?.rfid
             workTicketGet?.data?.forEach { data ->
                 val updateList = mutableListOf<LockPointUpdateReq>()
-                data.dataList?.forEach { dataListDTO ->
+                data.dataList?.filter { it.closed == 1 }?.forEach { dataListDTO ->
                     data.taskCode?.toLong()?.let {
                         SPUtils.returnKey(it)
                     }
@@ -709,7 +736,8 @@ object BleBusinessManager {
                                         RepositoryManager.jobTicketRepo.getJobTicketStepDataByTicketId(
                                             jobTicketData.ticketId
                                         )
-                                    currentWorkflowStep = ticketStep.firstOrNull { it.stepStatus == "0" }
+                                    currentWorkflowStep =
+                                        ticketStep.firstOrNull { it.stepStatus == "0" }
                                     RepositoryManager.jobTicketRepo.updateTicketDataStatus(
                                         data.taskCode?.toLong()!!,
                                         currentWorkflowStep?.getTicketStatus()?.toInt()

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

@@ -71,6 +71,7 @@
     <string name="make_sure_to_colock">Confirm to co-lock?</string>
     <string name="make_sure_to_unlock">Confirm to unlock?</string>
     <string name="key_return_tip">Permit not completed, key return prohibited</string>
+    <string name="lock_key_return_tip">The work order has not been completed. Do you want to force data upload</string>
 
     <!--  Presentation Page  -->
     <string name="presentation_select_sop">Select SOP</string>

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

@@ -71,6 +71,7 @@
     <string name="make_sure_to_colock">确定要共锁吗?</string>
     <string name="make_sure_to_unlock">确定要解锁吗?</string>
     <string name="key_return_tip">作业票尚未完成,禁止归还钥匙</string>
+    <string name="lock_key_return_tip">作业票尚未完成,是否强制上传数据</string>
 
     <!--  演示页  -->
     <string name="presentation_select_sop">选择SOP</string>

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

@@ -71,6 +71,7 @@
     <string name="make_sure_to_colock">确定要共锁吗?</string>
     <string name="make_sure_to_unlock">确定要解锁吗?</string>
     <string name="key_return_tip">作业票尚未完成,禁止归还钥匙</string>
+    <string name="lock_key_return_tip">作业票尚未完成,是否强制上传数据</string>
 
     <!--  演示页  -->
     <string name="presentation_select_sop">选择SOP</string>