Procházet zdrojové kódy

refactor(用户管理):
- 修改用户管理界面的工卡显示为RFID 1h
- 用户列表界面增加人脸录入 2h
- 新增用户界面增加人脸录入和指纹录入 2h
- 修改用户界面增加人脸录入和指纹录入 2h
refactor(作业执行):
- 移除作业执行界面的步骤点击如果当前是选择人员直接进入的问题。 1h

周文健 před 1 měsícem
rodič
revize
e5c1ea3458
28 změnil soubory, kde provedl 857 přidání a 114 odebrání
  1. 22 0
      app/src/main/assets/themes/Default/icons/trash.svg
  2. 102 4
      app/src/main/java/com/grkj/iscs/features/main/dialog/data_manage/AddUserDialog.kt
  3. 1 0
      app/src/main/java/com/grkj/iscs/features/main/dialog/data_manage/RegisterFaceDialog.kt
  4. 129 3
      app/src/main/java/com/grkj/iscs/features/main/dialog/data_manage/UpdateUserDialog.kt
  5. 148 85
      app/src/main/java/com/grkj/iscs/features/main/fragment/data_manage/UserManageFragment.kt
  6. 97 3
      app/src/main/java/com/grkj/iscs/features/main/viewmodel/data_manage/UserManageViewModel.kt
  7. 36 0
      app/src/main/res/layout/dialog_add_user.xml
  8. 36 0
      app/src/main/res/layout/dialog_update_user.xml
  9. 29 0
      app/src/main/res/layout/item_delete_btn.xml
  10. 3 0
      app/src/main/res/values-en/strings.xml
  11. 3 0
      app/src/main/res/values-zh/strings.xml
  12. 1 0
      app/src/main/res/values/dimens.xml
  13. 3 0
      app/src/main/res/values/strings.xml
  14. 37 6
      data/src/main/java/com/grkj/data/dao/UserDao.kt
  15. 20 0
      data/src/main/java/com/grkj/data/logic/IUserLogic.kt
  16. 16 0
      data/src/main/java/com/grkj/data/logic/impl/network/NetworkUserLogic.kt
  17. 20 0
      data/src/main/java/com/grkj/data/logic/impl/standard/UserLogic.kt
  18. 3 1
      data/src/main/java/com/grkj/data/model/vo/AddUserDataVo.kt
  19. 3 1
      data/src/main/java/com/grkj/data/model/vo/UpdateUserDataVo.kt
  20. 6 0
      data/src/main/java/com/grkj/data/model/vo/UserManageVo.kt
  21. 20 0
      data/src/main/java/com/grkj/data/repository/UserRepository.kt
  22. 17 0
      data/src/main/java/com/grkj/data/repository/impl/UserRepositoryImpl.kt
  23. 1 0
      shared/src/main/java/com/grkj/shared/utils/ArcSoftUtil.kt
  24. 93 9
      ui-base/src/main/java/com/grkj/ui_base/widget/FormLayout.kt
  25. 1 0
      ui-base/src/main/res/values-en/strings.xml
  26. 1 0
      ui-base/src/main/res/values-zh/strings.xml
  27. 8 2
      ui-base/src/main/res/values/attrs.xml
  28. 1 0
      ui-base/src/main/res/values/strings.xml

+ 22 - 0
app/src/main/assets/themes/Default/icons/trash.svg

@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generator: Adobe Illustrator 25.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve" width="512" height="512">
+<g>
+	<path d="M448,85.333h-66.133C371.66,35.703,328.002,0.064,277.333,0h-42.667c-50.669,0.064-94.327,35.703-104.533,85.333H64   c-11.782,0-21.333,9.551-21.333,21.333S52.218,128,64,128h21.333v277.333C85.404,464.214,133.119,511.93,192,512h128   c58.881-0.07,106.596-47.786,106.667-106.667V128H448c11.782,0,21.333-9.551,21.333-21.333S459.782,85.333,448,85.333z    M234.667,362.667c0,11.782-9.551,21.333-21.333,21.333C201.551,384,192,374.449,192,362.667v-128   c0-11.782,9.551-21.333,21.333-21.333c11.782,0,21.333,9.551,21.333,21.333V362.667z M320,362.667   c0,11.782-9.551,21.333-21.333,21.333c-11.782,0-21.333-9.551-21.333-21.333v-128c0-11.782,9.551-21.333,21.333-21.333   c11.782,0,21.333,9.551,21.333,21.333V362.667z M174.315,85.333c9.074-25.551,33.238-42.634,60.352-42.667h42.667   c27.114,0.033,51.278,17.116,60.352,42.667H174.315z"/>
+</g>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+</svg>

+ 102 - 4
app/src/main/java/com/grkj/iscs/features/main/dialog/data_manage/AddUserDialog.kt

@@ -1,13 +1,22 @@
 package com.grkj.iscs.features.main.dialog.data_manage
 
 import android.annotation.SuppressLint
+import android.util.Printer
 import android.view.View
+import android.widget.LinearLayout
+import android.widget.TextView
 import androidx.core.view.isVisible
+import com.drake.brv.annotaion.DividerOrientation
+import com.drake.brv.utils.dividerSpace
+import com.drake.brv.utils.linear
+import com.drake.brv.utils.setup
 import com.grkj.data.config.ISCSConfig
 import com.grkj.data.data.CommonConstants
+import com.grkj.data.data.MMKVConstants
 import com.grkj.data.model.vo.AddUserDataVo
 import com.grkj.iscs.R
 import com.grkj.iscs.databinding.DialogAddUserBinding
+import com.grkj.iscs.databinding.ItemDeleteBtnBinding
 import com.grkj.iscs.features.main.dialog.TextDropDownDialog
 import com.grkj.shared.utils.BCryptUtils
 import com.grkj.ui_base.utils.CommonUtils
@@ -15,6 +24,9 @@ import com.grkj.ui_base.utils.extension.tip
 import com.kongzue.dialogx.dialogs.CustomDialog
 import com.kongzue.dialogx.dialogs.PopTip
 import com.kongzue.dialogx.interfaces.OnBindView
+import com.sik.sikcore.SIKCore
+import com.sik.sikcore.extension.deleteIfExists
+import com.sik.sikcore.extension.getMMKVData
 import com.sik.sikcore.extension.setDebouncedClickListener
 import com.sik.sikcore.string.RegexUtils
 
@@ -29,12 +41,18 @@ class AddUserDialog(
     private val roleData: List<TextDropDownDialog.TextDropDownEntity>,
     private val workstationData: List<TextDropDownDialog.TextDropDownEntity>,
     private val apply: AddUserDialog.() -> Unit,
-    private val onConfirm: (AddUserDataVo, CustomDialog) -> Unit
+    private val onConfirm: (AddUserDataVo, CustomDialog) -> Unit,
+    private val registerFingerPrint: (registerFingerprintResult: (String) -> Unit) -> Unit,
+    private val clearData: () -> Unit
 ) : OnBindView<CustomDialog>(R.layout.dialog_add_user) {
 
     private lateinit var binding: DialogAddUserBinding
     private var selectedRoles: List<TextDropDownDialog.TextDropDownEntity> = emptyList()
     private var selectedWorkstations: List<TextDropDownDialog.TextDropDownEntity> = emptyList()
+    private var faceData: MutableList<String> =
+        mutableListOf(CommonUtils.getStr(R.string.register))
+    private var fingerprintGroupData: MutableList<String> =
+        mutableListOf(CommonUtils.getStr(R.string.register))
 
     override fun onBind(dialog: CustomDialog, v: View) {
         binding = DialogAddUserBinding.bind(v)
@@ -70,13 +88,20 @@ class AddUserDialog(
         // 取消/关闭
         binding.cancel.setDebouncedClickListener {
             addUserDialogView = null
+            faceData.forEach {
+                it.deleteIfExists()
+            }
+            clearData()
             dialog.dismiss()
         }
         binding.closeIv.setDebouncedClickListener {
             addUserDialogView = null
+            faceData.forEach {
+                it.deleteIfExists()
+            }
+            clearData()
             dialog.dismiss()
         }
-
         // 确认
         binding.confirm.setDebouncedClickListener {
             if (!checkData()) return@setDebouncedClickListener
@@ -92,9 +117,80 @@ class AddUserDialog(
                 selectedWorkstations.mapNotNull { it.getId() },
                 binding.statusRg.checkedRadioButtonId == binding.activateRb.id
             )
+            if (faceData.none { it == CommonUtils.getStr(R.string.register) }) {
+                vo.faceSavePath = faceData[0]
+            }
+            vo.fingerprintData =
+                fingerprintGroupData.filter { it != CommonUtils.getStr(R.string.register) }
             addUserDialogView = null
             onConfirm(vo, dialog)
         }
+        binding.faceRvList.linear(LinearLayout.HORIZONTAL).dividerSpace(
+            SIKCore.getApplication().resources.getDimension(com.grkj.ui_base.R.dimen.iscs_space_2)
+                .toInt(), DividerOrientation.GRID
+        ).setup {
+            addType<String>(R.layout.item_delete_btn)
+            onBind {
+                val item = getModel<String>()
+                val itemBinding = getBinding<ItemDeleteBtnBinding>()
+                itemBinding.text.text = item
+                itemBinding.root.setDebouncedClickListener {
+                    RegisterFaceDialog.show { imageData, savePath ->
+                        faceData.add(0, savePath)
+                        faceData.removeLastOrNull()
+                        notifyDataSetChanged()
+                    }
+                }
+                itemBinding.delete.isVisible = item != CommonUtils.getStr(R.string.register)
+                itemBinding.delete.setDebouncedClickListener {
+                    item.deleteIfExists()
+                    faceData.remove(item)
+                    if (faceData.isEmpty()) {
+                        faceData.add(CommonUtils.getStr(R.string.register))
+                    }
+                    notifyDataSetChanged()
+                }
+            }
+        }.models = faceData
+        binding.fingerprintRvList.linear(LinearLayout.HORIZONTAL).dividerSpace(
+            SIKCore.getApplication().resources.getDimension(com.grkj.ui_base.R.dimen.iscs_space_2)
+                .toInt(), DividerOrientation.GRID
+        ).setup {
+            addType<String>(R.layout.item_delete_btn)
+            onBind {
+                val item = getModel<String>()
+                val itemBinding = getBinding<ItemDeleteBtnBinding>()
+                itemBinding.text.text =
+                    if (item == CommonUtils.getStr(R.string.register)) item else "${
+                        CommonUtils.getStr(R.string.fingerprint)
+                    }-${item.take(6)}"
+                itemBinding.root.setDebouncedClickListener {
+                    if (item == CommonUtils.getStr(R.string.register)) {
+                        registerFingerPrint {
+                            fingerprintGroupData.add(fingerprintGroupData.size - 1, it)
+                            if (fingerprintGroupData.size - 1 == MMKVConstants.KEY_MAX_FINGERPRINT_INSERT.getMMKVData(
+                                    CommonConstants.DEFAULT_MAX_FINGERPRINT_INSERT_SIZE
+                                )
+                            ) {
+                                fingerprintGroupData.removeLastOrNull()
+                            }
+                            notifyDataSetChanged()
+                        }
+                    }
+                }
+                itemBinding.delete.isVisible = item != CommonUtils.getStr(R.string.register)
+                itemBinding.delete.setDebouncedClickListener {
+                    fingerprintGroupData.removeIf { it == item }
+                    if (fingerprintGroupData.size - 1 < MMKVConstants.KEY_MAX_FINGERPRINT_INSERT.getMMKVData(
+                            CommonConstants.DEFAULT_MAX_FINGERPRINT_INSERT_SIZE
+                        ) && fingerprintGroupData.none { it == CommonUtils.getStr(R.string.register) }
+                    ) {
+                        fingerprintGroupData.add(CommonUtils.getStr(R.string.register))
+                    }
+                    notifyDataSetChanged()
+                }
+            }
+        }.models = fingerprintGroupData
         apply()
     }
 
@@ -150,11 +246,13 @@ class AddUserDialog(
             roleData: List<TextDropDownDialog.TextDropDownEntity>,
             workstationData: List<TextDropDownDialog.TextDropDownEntity>,
             apply: AddUserDialog.() -> Unit,
-            onConfirm: (AddUserDataVo, CustomDialog) -> Unit
+            onConfirm: (AddUserDataVo, CustomDialog) -> Unit,
+            registerFingerPrint: (registerFingerprintResult: (String) -> Unit) -> Unit,
+            clearData: () -> Unit
         ): CustomDialog {
             return CustomDialog.show(
                 addUserDialogView ?: AddUserDialog(
-                    roleData, workstationData, apply, onConfirm
+                    roleData, workstationData, apply, onConfirm, registerFingerPrint,clearData
                 ).also { addUserDialogView = it }, CustomDialog.ALIGN.CENTER
             )
         }

+ 1 - 0
app/src/main/java/com/grkj/iscs/features/main/dialog/data_manage/RegisterFaceDialog.kt

@@ -71,6 +71,7 @@ class RegisterFaceDialog(private val onConfirm: (String, String) -> Unit) :
                 CommonConstants.FACE_FOLDER,
                 saveFileName
             )
+            dialog.dismiss()
             onConfirm(imageData, savePath)
         }
         binding.recapture.setDebouncedClickListener {

+ 129 - 3
app/src/main/java/com/grkj/iscs/features/main/dialog/data_manage/UpdateUserDialog.kt

@@ -1,12 +1,20 @@
 package com.grkj.iscs.features.main.dialog.data_manage
 
 import android.view.View
+import android.widget.LinearLayout
 import androidx.core.view.isVisible
+import com.drake.brv.annotaion.DividerOrientation
+import com.drake.brv.utils.dividerSpace
+import com.drake.brv.utils.linear
+import com.drake.brv.utils.setup
 import com.grkj.data.config.ISCSConfig
+import com.grkj.data.data.CommonConstants
+import com.grkj.data.data.MMKVConstants
 import com.grkj.data.model.vo.UpdateUserDataVo
 import com.grkj.data.model.vo.UserManageVo
 import com.grkj.iscs.R
 import com.grkj.iscs.databinding.DialogUpdateUserBinding
+import com.grkj.iscs.databinding.ItemDeleteBtnBinding
 import com.grkj.iscs.features.main.dialog.TextDropDownDialog
 import com.grkj.shared.utils.BCryptUtils
 import com.grkj.ui_base.utils.CommonUtils
@@ -14,6 +22,9 @@ import com.grkj.ui_base.utils.extension.tip
 import com.kongzue.dialogx.dialogs.CustomDialog
 import com.kongzue.dialogx.dialogs.PopTip
 import com.kongzue.dialogx.interfaces.OnBindView
+import com.sik.sikcore.SIKCore
+import com.sik.sikcore.extension.deleteIfExists
+import com.sik.sikcore.extension.getMMKVData
 import com.sik.sikcore.extension.setDebouncedClickListener
 
 /**
@@ -28,12 +39,18 @@ class UpdateUserDialog(
     private val roleData: List<TextDropDownDialog.TextDropDownEntity>,
     private val workstationData: List<TextDropDownDialog.TextDropDownEntity>,
     private val apply: UpdateUserDialog.() -> Unit,
-    private val onConfirm: (UpdateUserDataVo, CustomDialog) -> Unit
+    private val onConfirm: (UpdateUserDataVo, CustomDialog) -> Unit,
+    private val registerFingerPrint: (registerFingerprintResult: (String) -> Unit) -> Unit,
+    private val clearData: () -> Unit
 ) : OnBindView<CustomDialog>(R.layout.dialog_update_user) {
 
     private lateinit var binding: DialogUpdateUserBinding
     private var selectedRoles = mutableListOf<TextDropDownDialog.TextDropDownEntity>()
     private var selectedWorkstations = mutableListOf<TextDropDownDialog.TextDropDownEntity>()
+    private var faceData: MutableList<String> =
+        mutableListOf(CommonUtils.getStr(R.string.register))
+    private var fingerprintGroupData: MutableList<String> =
+        mutableListOf(CommonUtils.getStr(R.string.register))
 
     override fun onBind(dialog: CustomDialog, v: View) {
         binding = DialogUpdateUserBinding.bind(v)
@@ -55,6 +72,20 @@ class UpdateUserDialog(
         binding.workstationNameTv.text = userVo.workstationNames.filterNotNull().joinToString(",")
         binding.activateRb.isChecked = userVo.getStatus()
         binding.deactivateRb.isChecked = !userVo.getStatus()
+        if (userVo.faceSavePath != null) {
+            faceData.clear()
+            faceData.add(CommonUtils.getStr(R.string.face))
+        }
+        if (userVo.fingerprintData.isNotEmpty()) {
+            fingerprintGroupData.clear()
+            fingerprintGroupData.addAll(userVo.fingerprintData)
+            if (fingerprintGroupData.size - 1 < MMKVConstants.KEY_MAX_FINGERPRINT_INSERT.getMMKVData(
+                    CommonConstants.DEFAULT_MAX_FINGERPRINT_INSERT_SIZE
+                ) && fingerprintGroupData.none { it == CommonUtils.getStr(R.string.register) }
+            ) {
+                fingerprintGroupData.add(CommonUtils.getStr(R.string.register))
+            }
+        }
 
         // 标记已选
         selectedRoles = roleData.filter { it.getShowText() in userVo.roleNames }.toMutableList()
@@ -94,10 +125,22 @@ class UpdateUserDialog(
         // 取消/关闭
         binding.cancel.setDebouncedClickListener {
             updateUserDialogView = null
+            if (faceData.none { it == CommonUtils.getStr(R.string.register) }) {
+                if (userVo.faceSavePath != faceData[0]) {
+                    faceData[0].deleteIfExists()
+                }
+            }
+            clearData()
             dialog.dismiss()
         }
         binding.closeIv.setDebouncedClickListener {
             updateUserDialogView = null
+            if (faceData.none { it == CommonUtils.getStr(R.string.register) }) {
+                if (userVo.faceSavePath != faceData[0]) {
+                    faceData[0].deleteIfExists()
+                }
+            }
+            clearData()
             dialog.dismiss()
         }
 
@@ -126,9 +169,90 @@ class UpdateUserDialog(
                 selectedWorkstations.mapNotNull { it.getId() },
                 isActive
             )
+            if (faceData.none { it == CommonUtils.getStr(R.string.register) }) {
+                if (userVo.faceSavePath != faceData[0]) {
+                    userVo.faceSavePath?.deleteIfExists()
+                }
+                updateVo.faceSavePath = faceData[0]
+            } else {
+                userVo.faceSavePath?.deleteIfExists()
+                updateVo.faceSavePath = null
+            }
+            updateVo.fingerprintData =
+                fingerprintGroupData.filter { it != CommonUtils.getStr(R.string.register) }
             updateUserDialogView = null
             onConfirm(updateVo, dialog)
         }
+        binding.faceRvList.linear(LinearLayout.HORIZONTAL).dividerSpace(
+            SIKCore.getApplication().resources.getDimension(com.grkj.ui_base.R.dimen.iscs_space_2)
+                .toInt(), DividerOrientation.GRID
+        ).setup {
+            addType<String>(R.layout.item_delete_btn)
+            onBind {
+                val item = getModel<String>()
+                val itemBinding = getBinding<ItemDeleteBtnBinding>()
+                itemBinding.text.text =
+                    if (item != CommonUtils.getStr(R.string.register)) CommonUtils.getStr(R.string.face) else item
+                itemBinding.root.setDebouncedClickListener {
+                    if (item != CommonUtils.getStr(R.string.register)) {
+                        return@setDebouncedClickListener
+                    }
+                    RegisterFaceDialog.show { imageData, savePath ->
+                        faceData.add(0, savePath)
+                        faceData.removeLastOrNull()
+                        notifyDataSetChanged()
+                    }
+                }
+                itemBinding.delete.isVisible = item != CommonUtils.getStr(R.string.register)
+                itemBinding.delete.setDebouncedClickListener {
+                    item.deleteIfExists()
+                    faceData.remove(item)
+                    if (faceData.isEmpty()) {
+                        faceData.add(CommonUtils.getStr(R.string.register))
+                    }
+                    notifyDataSetChanged()
+                }
+            }
+        }.models = faceData
+        binding.fingerprintRvList.linear(LinearLayout.HORIZONTAL).dividerSpace(
+            SIKCore.getApplication().resources.getDimension(com.grkj.ui_base.R.dimen.iscs_space_2)
+                .toInt(), DividerOrientation.GRID
+        ).setup {
+            addType<String>(R.layout.item_delete_btn)
+            onBind {
+                val item = getModel<String>()
+                val itemBinding = getBinding<ItemDeleteBtnBinding>()
+                itemBinding.text.text =
+                    if (item == CommonUtils.getStr(R.string.register)) item else "${
+                        CommonUtils.getStr(R.string.fingerprint)
+                    }-${item.take(6)}"
+                itemBinding.root.setDebouncedClickListener {
+                    if (item == CommonUtils.getStr(R.string.register)) {
+                        registerFingerPrint {
+                            fingerprintGroupData.add(fingerprintGroupData.size - 1, it)
+                            if (fingerprintGroupData.size - 1 == MMKVConstants.KEY_MAX_FINGERPRINT_INSERT.getMMKVData(
+                                    CommonConstants.DEFAULT_MAX_FINGERPRINT_INSERT_SIZE
+                                )
+                            ) {
+                                fingerprintGroupData.removeLastOrNull()
+                            }
+                            notifyDataSetChanged()
+                        }
+                    }
+                }
+                itemBinding.delete.isVisible = item != CommonUtils.getStr(R.string.register)
+                itemBinding.delete.setDebouncedClickListener {
+                    fingerprintGroupData.removeIf { it == item }
+                    if (fingerprintGroupData.size - 1 < MMKVConstants.KEY_MAX_FINGERPRINT_INSERT.getMMKVData(
+                            CommonConstants.DEFAULT_MAX_FINGERPRINT_INSERT_SIZE
+                        ) && fingerprintGroupData.none { it == CommonUtils.getStr(R.string.register) }
+                    ) {
+                        fingerprintGroupData.add(CommonUtils.getStr(R.string.register))
+                    }
+                    notifyDataSetChanged()
+                }
+            }
+        }.models = fingerprintGroupData
         apply()
     }
 
@@ -155,7 +279,9 @@ class UpdateUserDialog(
             roleData: List<TextDropDownDialog.TextDropDownEntity>,
             workstationData: List<TextDropDownDialog.TextDropDownEntity>,
             apply: UpdateUserDialog.() -> Unit,
-            onConfirm: (UpdateUserDataVo, CustomDialog) -> Unit
+            onConfirm: (UpdateUserDataVo, CustomDialog) -> Unit,
+            registerFingerPrint: (registerFingerprintResult: (String) -> Unit) -> Unit,
+            clearData: () -> Unit
         ): CustomDialog {
             return CustomDialog.show(
                 updateUserDialogView ?: UpdateUserDialog(
@@ -163,7 +289,7 @@ class UpdateUserDialog(
                     roleData,
                     workstationData,
                     apply,
-                    onConfirm
+                    onConfirm, registerFingerPrint, clearData
                 ).also { updateUserDialogView = it },
                 CustomDialog.ALIGN.CENTER
             )

+ 148 - 85
app/src/main/java/com/grkj/iscs/features/main/fragment/data_manage/UserManageFragment.kt

@@ -10,6 +10,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.google.gson.annotations.Until
 import com.grkj.data.data.CommonConstants
 import com.grkj.data.data.EventConstants
 import com.grkj.data.data.MMKVConstants
@@ -21,6 +22,7 @@ import com.grkj.iscs.features.main.dialog.SwipCardOperationTipDialog
 import com.grkj.iscs.features.main.dialog.TextDropDownDialog
 import com.grkj.iscs.features.main.dialog.data_manage.AddUserDialog
 import com.grkj.iscs.features.main.dialog.data_manage.FilterUserDialog
+import com.grkj.iscs.features.main.dialog.data_manage.RegisterFaceDialog
 import com.grkj.iscs.features.main.dialog.data_manage.UpdateUserDialog
 import com.grkj.iscs.features.main.dialog.user_info.AddFingerprintDialog
 import com.grkj.iscs.features.main.viewmodel.data_manage.UserManageViewModel
@@ -113,7 +115,7 @@ class UserManageFragment : BaseFragment<FragmentUserManageBinding>() {
                         }
                 }, {
                     addUserDialog = this
-                }) { data, dialog ->
+                }, { data, dialog ->
                     viewModel.validateUserData(data.username).observe(this) {
                         viewModel.addUser(data).observe(this) {
                             dialog.dismiss()
@@ -130,7 +132,14 @@ class UserManageFragment : BaseFragment<FragmentUserManageBinding>() {
                             }
                         }
                     }
-                }.setDialogLifecycleCallback(object : DialogLifecycleCallback<CustomDialog>() {
+                }, { registerFingerprintResult ->
+                    startRegisterTempFingerprint {
+                        registerFingerprintResult(it)
+                    }
+                }, {
+                    viewModel.clearNoUserFingerprint().observe(this@UserManageFragment) {}
+                    viewModel.clearNoUserFace().observe(this@UserManageFragment) {}
+                }).setDialogLifecycleCallback(object : DialogLifecycleCallback<CustomDialog>() {
                     override fun onDismiss(dialog: CustomDialog?) {
                         viewModel.startReadCard = false
                         viewModel.isDialogRead = false
@@ -201,99 +210,152 @@ class UserManageFragment : BaseFragment<FragmentUserManageBinding>() {
             })
         }
         itemBinding.registerFingerprint.setDebouncedClickListener {
-            if (item.fingerprintSize >= maxFingerprintInsertSize) {
-                showToast(CommonUtils.getStr("fingerprint_limit_tip"))
-                return@setDebouncedClickListener
-            }
+            startRegisterFingerprint(item)
+        }
+        itemBinding.registerFace.setDebouncedClickListener {
             viewModel.currentHandleUser = item
-            mFingerprintPressTimes = 0
-            mFingerprintInputErrorTimes = 0
-            fingerprintTempData.clear()
-            AddFingerprintDialog.show({
-                FingerprintUtil.stop()
-                it.dismiss()
-            }) {
-                pressTip = it
-                it.text = CommonUtils.getStr(
-                    "fingerprint_scan_tip",
-                    maxPressTimes - mFingerprintPressTimes
-                )
-            }.apply {
-                startCaptureFingerprint(this)
+            RegisterFaceDialog.show { imageData, savePath ->
+                viewModel.saveUserFace(savePath).observe(this@UserManageFragment) {
+                    TipDialog.showSuccess(
+                        CommonUtils.getStr(com.grkj.ui_base.R.string.user_face_register_success),
+                        onConfirmClick = {
+                            getUserData(nextPage = false)
+                        })
+                }
             }
         }
         itemBinding.root.setDebouncedClickListener {
             viewModel.startReadCard = true
             viewModel.isDialogRead = true
             viewModel.getRoleAndWorkStationData().observe(this@UserManageFragment) {
-                UpdateUserDialog.show(item, viewModel.roleData.map {
-                    val i18NRoleName = I18nManager.t(it.roleKey ?: "")
-                    val roleName = if (i18NRoleName == it.roleKey || i18NRoleName.isEmpty()) {
-                        it.roleName
-                    } else {
-                        i18NRoleName
-                    }
-                    TextDropDownDialog.SimpleTextDropDownEntity(
-                        dataId = it.roleId,
-                        dataText = roleName
-                    )
-                }, viewModel.workstationData.map {
-                    TextDropDownDialog.SimpleTextDropDownEntity(
-                        dataId = it.workstationId,
-                        dataText = it.workstationName,
-                    )
-                        .apply {
-                            setChildren(it.children.map {
-                                TextDropDownDialog.SimpleTextDropDownEntity(
-                                    dataId = it.workstationId,
-                                    dataText = it.workstationName,
-                                    dataObject = it,
-                                    dataLevel = 1
-                                ).apply {
-                                    setSelected(viewModel.workstationData.map { it.workstationName }
-                                        .contains(getShowText()))
-                                }
-                            })
-                        }
-                }, {
-                    updateUserDialog = this
-                }) { data, dialog ->
-                    viewModel.updateUser(data).observe(this@UserManageFragment) {
-                        dialog.dismiss()
-                        if (it) {
-                            TipDialog.show(
-                                title = CommonUtils.getStr("action_succeed"),
-                                dialogType = TipDialog.DialogType.SUCCESS,
-                                msg = CommonUtils.getStr("update_user_succeed"),
-                                showCancel = false,
-                                onConfirmClick = {
-                                    getUserData(false)
-                                }
-                            )
+                viewModel.getUserFingerprintAndFaceData(item).observe(this@UserManageFragment) {
+                    UpdateUserDialog.show(item, viewModel.roleData.map {
+                        val i18NRoleName = I18nManager.t(it.roleKey ?: "")
+                        val roleName = if (i18NRoleName == it.roleKey || i18NRoleName.isEmpty()) {
+                            it.roleName
                         } else {
-                            TipDialog.show(
-                                title = CommonUtils.getStr("action_failed"),
-                                dialogType = TipDialog.DialogType.ERROR,
-                                msg = CommonUtils.getStr("update_user_failed"),
-                                showCancel = false,
-                                onConfirmClick = {
-                                    getUserData(false)
-                                }
-                            )
+                            i18NRoleName
                         }
-                    }
-                }.setDialogLifecycleCallback(object : DialogLifecycleCallback<CustomDialog>() {
-                    override fun onDismiss(dialog: CustomDialog?) {
-                        viewModel.startReadCard = false
-                        viewModel.isDialogRead = false
-                        super.onDismiss(dialog)
-                    }
-                })
+                        TextDropDownDialog.SimpleTextDropDownEntity(
+                            dataId = it.roleId,
+                            dataText = roleName
+                        )
+                    }, viewModel.workstationData.map {
+                        TextDropDownDialog.SimpleTextDropDownEntity(
+                            dataId = it.workstationId,
+                            dataText = it.workstationName,
+                        )
+                            .apply {
+                                setChildren(it.children.map {
+                                    TextDropDownDialog.SimpleTextDropDownEntity(
+                                        dataId = it.workstationId,
+                                        dataText = it.workstationName,
+                                        dataObject = it,
+                                        dataLevel = 1
+                                    ).apply {
+                                        setSelected(viewModel.workstationData.map { it.workstationName }
+                                            .contains(getShowText()))
+                                    }
+                                })
+                            }
+                    }, {
+                        updateUserDialog = this
+                    }, { data, dialog ->
+                        viewModel.updateUser(data).observe(this@UserManageFragment) {
+                            dialog.dismiss()
+                            if (it) {
+                                TipDialog.show(
+                                    title = CommonUtils.getStr("action_succeed"),
+                                    dialogType = TipDialog.DialogType.SUCCESS,
+                                    msg = CommonUtils.getStr("update_user_succeed"),
+                                    showCancel = false,
+                                    onConfirmClick = {
+                                        getUserData(false)
+                                    }
+                                )
+                            } else {
+                                TipDialog.show(
+                                    title = CommonUtils.getStr("action_failed"),
+                                    dialogType = TipDialog.DialogType.ERROR,
+                                    msg = CommonUtils.getStr("update_user_failed"),
+                                    showCancel = false,
+                                    onConfirmClick = {
+                                        getUserData(false)
+                                    }
+                                )
+                            }
+                        }
+                    }, { registerFingerprintResult ->
+                        startRegisterTempFingerprint {
+                            registerFingerprintResult(it)
+                        }
+                    }, {
+                        viewModel.clearNoUserFingerprint().observe(this@UserManageFragment) {}
+                        viewModel.clearNoUserFace().observe(this@UserManageFragment) {}
+                    }).setDialogLifecycleCallback(object : DialogLifecycleCallback<CustomDialog>() {
+                        override fun onDismiss(dialog: CustomDialog?) {
+                            viewModel.startReadCard = false
+                            viewModel.isDialogRead = false
+                            super.onDismiss(dialog)
+                        }
+                    })
+                }
             }
         }
     }
 
-    private fun startCaptureFingerprint(dialog: CustomDialog) {
+    /**
+     * 录入指纹
+     */
+    private fun startRegisterFingerprint(item: UserManageVo) {
+        if (item.fingerprintSize >= maxFingerprintInsertSize) {
+            showToast(CommonUtils.getStr("fingerprint_limit_tip"))
+            return
+        }
+        viewModel.currentHandleUser = item
+        mFingerprintPressTimes = 0
+        mFingerprintInputErrorTimes = 0
+        fingerprintTempData.clear()
+        AddFingerprintDialog.show({
+            FingerprintUtil.stop()
+            it.dismiss()
+        }) {
+            pressTip = it
+            it.text = CommonUtils.getStr(
+                "fingerprint_scan_tip",
+                maxPressTimes - mFingerprintPressTimes
+            )
+        }.apply {
+            startCaptureFingerprint(this)
+        }
+    }
+
+    /**
+     * 临时录入指纹
+     */
+    private fun startRegisterTempFingerprint(registerResult: (String) -> Unit = {}) {
+        mFingerprintPressTimes = 0
+        mFingerprintInputErrorTimes = 0
+        fingerprintTempData.clear()
+        AddFingerprintDialog.show({
+            FingerprintUtil.stop()
+            it.dismiss()
+        }) {
+            pressTip = it
+            it.text = CommonUtils.getStr(
+                "fingerprint_scan_tip",
+                maxPressTimes - mFingerprintPressTimes
+            )
+        }.apply {
+            startCaptureFingerprint(this, true, registerResult)
+        }
+    }
+
+    private fun startCaptureFingerprint(
+        dialog: CustomDialog,
+        isTemp: Boolean = false,
+        registerResult: (String) -> Unit = {}
+    ) {
         FingerprintUtil.init(requireContext())
         FingerprintUtil.start()
         mFingerprintGroupName = UUID.randomUUID().toString()
@@ -301,16 +363,17 @@ class UserManageFragment : BaseFragment<FragmentUserManageBinding>() {
             override fun onScan(bitmap: Bitmap) {
                 viewModel.saveUserFingerprint(
                     ImageConvertUtils.bitmapToBase64(bitmap) ?: "",
-                    mFingerprintGroupName
+                    mFingerprintGroupName,
+                    isTemp
                 ).observe(this@UserManageFragment) {
                     if (it != null) {
                         logger.info("添加指纹:${it}")
                         inputFingerprintIds.add(it)
                         mFingerprintPressTimes++
                         if (mFingerprintPressTimes == maxPressTimes) {
-                            dialog?.dismiss()
+                            dialog.dismiss()
                             showToast(CommonUtils.getStr("fingerprint_add_success_tip"))
-                            getUserData(false)
+                            registerResult(mFingerprintGroupName)
                         } else if (mFingerprintInputErrorTimes == inputFingerprintErrorTimes) {
                             mFingerprintGroupName = UUID.randomUUID().toString()
                             mFingerprintPressTimes = 0

+ 97 - 3
app/src/main/java/com/grkj/iscs/features/main/viewmodel/data_manage/UserManageViewModel.kt

@@ -2,7 +2,6 @@ package com.grkj.iscs.features.main.viewmodel.data_manage
 
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.liveData
-import com.grkj.data.data.MainDomainData
 import com.grkj.data.model.dos.SysRole
 import com.grkj.data.model.vo.AddUserDataVo
 import com.grkj.data.model.vo.UpdateUserDataVo
@@ -89,6 +88,18 @@ class UserManageViewModel @Inject constructor(
                     hardwareRepository.updateUserJobCard(it, userId)
                 }
             }
+            userData.faceSavePath?.let {
+                userLogic.deleteFaceDataByUserId(userId)
+                val sysUserCharacteristicDo = SysUserCharacteristicDo()
+                sysUserCharacteristicDo.userId = userId
+                sysUserCharacteristicDo.content = it
+                sysUserCharacteristicDo.type = "2"
+                logger.info("保存的人脸数据:${sysUserCharacteristicDo}")
+                userLogic.saveUserCharacteristic(sysUserCharacteristicDo)
+            }
+            userData.fingerprintData.forEach {
+                userLogic.bindUserCharacteristic(it, userId)
+            }
             roleRepository.addUserRoleData(userId, userData.roleId)
             emit(true)
         }
@@ -105,6 +116,19 @@ class UserManageViewModel @Inject constructor(
         }
     }
 
+    /**
+     * 获取指纹和人脸数据
+     */
+    fun getUserFingerprintAndFaceData(userManageVo: UserManageVo): LiveData<Boolean> {
+        return liveData(Dispatchers.IO) {
+            userManageVo.fingerprintData = userLogic.getFingerprintDataByUserId(userManageVo.userId)
+                .groupBy { it.group }.mapNotNull { it.key }
+            userManageVo.faceSavePath =
+                userLogic.getFaceDataByUserId(userManageVo.userId).firstOrNull()?.content
+            emit(true)
+        }
+    }
+
     /**
      * 更新用户
      */
@@ -124,6 +148,22 @@ class UserManageViewModel @Inject constructor(
                     hardwareRepository.updateUserJobCard(it, updateUserDataVo.userId)
                 }
             }
+            updateUserDataVo.faceSavePath?.let {
+                userLogic.deleteFaceDataByUserId(updateUserDataVo.userId)
+                val sysUserCharacteristicDo = SysUserCharacteristicDo()
+                sysUserCharacteristicDo.userId = updateUserDataVo.userId
+                sysUserCharacteristicDo.content = it
+                sysUserCharacteristicDo.type = "2"
+                logger.info("保存的人脸数据:${sysUserCharacteristicDo}")
+                userLogic.saveUserCharacteristic(sysUserCharacteristicDo)
+            } ?: userLogic.deleteFaceDataByUserId(updateUserDataVo.userId)
+            updateUserDataVo.fingerprintData.forEach {
+                userLogic.bindUserCharacteristic(it, updateUserDataVo.userId)
+            }
+            userLogic.deleteNoUseFingerprint(
+                updateUserDataVo.fingerprintData,
+                updateUserDataVo.userId
+            )
             roleRepository.addUserRoleData(updateUserDataVo.userId, updateUserDataVo.roleId)
             emit(true)
         }
@@ -194,10 +234,15 @@ class UserManageViewModel @Inject constructor(
     /**
      * 保存用户指纹
      */
-    fun saveUserFingerprint(b64: String, group: String): LiveData<Long> {
+    fun saveUserFingerprint(
+        b64: String, group: String,
+        isTemp: Boolean = false
+    ): LiveData<Long> {
         return liveData(Dispatchers.IO) {
             val sysUserCharacteristicDo = SysUserCharacteristicDo()
-            sysUserCharacteristicDo.userId = currentHandleUser.userId
+            if (!isTemp) {
+                sysUserCharacteristicDo.userId = currentHandleUser.userId
+            }
             sysUserCharacteristicDo.content = b64
             sysUserCharacteristicDo.type = "1"
             sysUserCharacteristicDo.group = group
@@ -206,10 +251,59 @@ class UserManageViewModel @Inject constructor(
         }
     }
 
+    /**
+     * 绑定用户指纹
+     */
+    fun bindUserFingerprint(
+        group: String,
+        userId: Long
+    ): LiveData<Boolean> {
+        return liveData(Dispatchers.IO) {
+            userLogic.bindUserCharacteristic(group, userId)
+            emit(true)
+        }
+    }
+
     fun checkJobCardInUse(rfid: String): LiveData<Boolean> {
         return liveData(Dispatchers.IO) {
             val isJobCard = hardwareRepository.getCardByCardRfid(rfid)
             emit(isJobCard != null && isJobCard.userId != null)
         }
     }
+
+    /**
+     * 清除没有用户的指纹
+     */
+    fun clearNoUserFingerprint(): LiveData<Boolean> {
+        return liveData(Dispatchers.IO) {
+            userLogic.clearNoUserFingerprint()
+            emit(true)
+        }
+    }
+
+    /**
+     * 清除没有用户的脸部
+     */
+    fun clearNoUserFace(): LiveData<Boolean> {
+        return liveData(Dispatchers.IO) {
+            userLogic.clearNoUserFace()
+            emit(true)
+        }
+    }
+
+    /**
+     * 保存人脸数据
+     */
+    fun saveUserFace(savePath: String, userId: Long = currentHandleUser.userId): LiveData<Boolean> {
+        return liveData(Dispatchers.IO) {
+            userLogic.deleteFaceDataByUserId(userId)
+            val sysUserCharacteristicDo = SysUserCharacteristicDo()
+            sysUserCharacteristicDo.userId = userId
+            sysUserCharacteristicDo.content = savePath
+            sysUserCharacteristicDo.type = "2"
+            logger.info("保存的人脸数据:${sysUserCharacteristicDo}")
+            userLogic.saveUserCharacteristic(sysUserCharacteristicDo)
+            emit(true)
+        }
+    }
 }

+ 36 - 0
app/src/main/res/layout/dialog_add_user.xml

@@ -226,6 +226,42 @@
                 app:formRole="field"
                 app:i18nHint='@{"please_select_area"}' />
 
+            <com.grkj.ui_base.widget.RequiredTextView
+                android:id="@+id/face_tv"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/face"
+                android:textColor="?attr/colorTextPrimary"
+                android:textSize="@dimen/iscs_text_md"
+                app:formRole="label"
+                app:markPosition="start"
+                app:required="false" />
+
+            <androidx.recyclerview.widget.RecyclerView
+                android:id="@+id/face_rv_list"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginLeft="@dimen/iscs_space_2"
+                app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
+
+            <com.grkj.ui_base.widget.RequiredTextView
+                android:id="@+id/fingerprint_tv"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/fingerprint"
+                android:textColor="?attr/colorTextPrimary"
+                android:textSize="@dimen/iscs_text_md"
+                app:formRole="label"
+                app:markPosition="start"
+                app:required="false" />
+
+            <androidx.recyclerview.widget.RecyclerView
+                android:id="@+id/fingerprint_rv_list"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginLeft="@dimen/iscs_space_2"
+                app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
+
             <com.grkj.ui_base.widget.RequiredTextView
                 android:id="@+id/status_tv"
                 android:layout_width="wrap_content"

+ 36 - 0
app/src/main/res/layout/dialog_update_user.xml

@@ -225,6 +225,42 @@
                 app:formRole="field"
                 app:i18nHint='@{"please_select_area"}' />
 
+            <com.grkj.ui_base.widget.RequiredTextView
+                android:id="@+id/face_tv"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/face"
+                android:textColor="?attr/colorTextPrimary"
+                android:textSize="@dimen/iscs_text_md"
+                app:formRole="label"
+                app:markPosition="start"
+                app:required="false" />
+
+            <androidx.recyclerview.widget.RecyclerView
+                android:id="@+id/face_rv_list"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginLeft="@dimen/iscs_space_2"
+                app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
+
+            <com.grkj.ui_base.widget.RequiredTextView
+                android:id="@+id/fingerprint_tv"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/fingerprint"
+                android:textColor="?attr/colorTextPrimary"
+                android:textSize="@dimen/iscs_text_md"
+                app:formRole="label"
+                app:markPosition="start"
+                app:required="false" />
+
+            <androidx.recyclerview.widget.RecyclerView
+                android:id="@+id/fingerprint_rv_list"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginLeft="@dimen/iscs_space_2"
+                app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
+
             <com.grkj.ui_base.widget.RequiredTextView
                 android:id="@+id/status_tv"
                 android:layout_width="wrap_content"

+ 29 - 0
app/src/main/res/layout/item_delete_btn.xml

@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:background="@drawable/common_btn_secondary"
+        android:orientation="horizontal"
+        android:paddingHorizontal="@dimen/iscs_space_2"
+        android:gravity="center"
+        android:paddingVertical="@dimen/iscs_space_1">
+
+        <TextView
+            android:id="@+id/text"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textColor="?attr/colorTextPrimary"
+            android:textSize="@dimen/iscs_text_md" />
+
+        <ImageView
+            android:id="@+id/delete"
+            android:layout_width="@dimen/icon_size_sm"
+            android:layout_height="@dimen/icon_size_sm"
+            android:tint="@color/palette_red_bright"
+            app:skinSrc='@{"trash.svg"}'
+            android:visibility="gone"/>
+    </LinearLayout>
+</layout>

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

@@ -5,4 +5,7 @@
     <string name="register_face">Register Face</string>
     <string name="register_point_rfid">Register Point RFID</string>
     <string name="swip_card_to_register">Swip Card to Register</string>
+    <string name="face">Face</string>
+    <string name="fingerprint">Fingerprint</string>
+    <string name="register">Register</string>
 </resources>

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

@@ -5,4 +5,7 @@
     <string name="register_face">录入人脸</string>
     <string name="register_point_rfid">录入RFID</string>
     <string name="swip_card_to_register">刷卡录入</string>
+    <string name="face">人脸</string>
+    <string name="fingerprint">指纹</string>
+    <string name="register">录入</string>
 </resources>

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

@@ -73,6 +73,7 @@
     <dimen name="avatar_size">30dp</dimen>
     <dimen name="avatar_user_info_size">120dp</dimen>
     <dimen name="icon_size">30dp</dimen>
+    <dimen name="icon_size_sm">20dp</dimen>
     <dimen name="dialog_check_face_close_size">60dp</dimen>
     <dimen name="selected_point_info_layout_width">400dp</dimen>
     <dimen name="common_rv_min_height">200dp</dimen>

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

@@ -5,4 +5,7 @@
     <string name="register_face">录入人脸</string>
     <string name="register_point_rfid">录入RFID</string>
     <string name="swip_card_to_register">刷卡录入</string>
+    <string name="face">人脸</string>
+    <string name="fingerprint">指纹</string>
+    <string name="register">录入</string>
 </resources>

+ 37 - 6
data/src/main/java/com/grkj/data/dao/UserDao.kt

@@ -36,25 +36,25 @@ interface UserDao {
     /**
      * 获取所有指纹数据
      */
-    @Query("select * from sys_user_characteristic left join sys_user su on su.user_id = sys_user_characteristic.user_id where su.del_flag = 0 and sys_user_characteristic.type = 1 ")
+    @Query("select * from sys_user_characteristic left join sys_user su on su.user_id = sys_user_characteristic.user_id where su.del_flag = 0 and sys_user_characteristic.del_flag = 0 and sys_user_characteristic.type = 1 ")
     fun getFingerprintData(): List<SysUserCharacteristicDo>
 
     /**
      * 根据用户id获取指纹数据
      */
-    @Query("select * from sys_user_characteristic where type = 1 and user_id = :userId")
+    @Query("select * from sys_user_characteristic where type = 1 and user_id = :userId and del_flag = 0")
     fun getFingerprintDataByUserId(userId: Long): List<SysUserCharacteristicDo>
 
     /**
      * 获取所有人脸数据
      */
-    @Query("select * from sys_user_characteristic left join sys_user su on su.user_id = sys_user_characteristic.user_id where su.del_flag = 0 and sys_user_characteristic.type = 2")
+    @Query("select * from sys_user_characteristic left join sys_user su on su.user_id = sys_user_characteristic.user_id where su.del_flag = 0 and sys_user_characteristic.del_flag = 0 and sys_user_characteristic.type = 2")
     fun getFaceData(): List<SysUserCharacteristicDo>
 
     /**
      * 根据用户id获取人脸数据
      */
-    @Query("select * from sys_user_characteristic where type = 2 and user_id = :userId")
+    @Query("select * from sys_user_characteristic where type = 2 and user_id = :userId and del_flag = 0")
     fun getFaceDataByUserId(userId: Long): List<SysBiometricDataVo>
 
     /**
@@ -177,7 +177,8 @@ interface UserDao {
     /**
      * 获取所有用户数据
      */
-    @Query("""
+    @Query(
+        """
     SELECT
       su.user_id AS userId,
       su.nick_name AS nickName,
@@ -204,7 +205,8 @@ interface UserDao {
             OR (:includeDescendants = 1 AND (','||ifnull(iw.ancestors,'')||',') LIKE '%,'||:workstationId||',%')
           )
     GROUP BY su.user_id,su.nick_name,su.status
-    """)
+    """
+    )
     fun getAllUserDataWithWorkstationId(
         workstationId: Long,
         includeDescendants: Int = ISCSConfig.includeDescendants
@@ -325,4 +327,33 @@ interface UserDao {
      */
     @Query("select * from sys_user_characteristic where del_flag = 0")
     fun getAllUserCharacteristic(): List<SysUserCharacteristicDo>
+
+    /**
+     * 绑定用户特征数据
+     */
+    @Query("update sys_user_characteristic set user_id = :userId where `group` = :group")
+    fun bindUserCharacteristic(group: String, userId: Long)
+
+    /**
+     * 清除没有用户的指纹数据
+     */
+    @Query("delete from sys_user_characteristic where user_id = 0 and type = 1")
+    fun clearNoUserFingerprint()
+
+    /**
+     * 清除没有用户的人脸数据
+     */
+    @Query("delete from sys_user_characteristic where user_id = 0 and type = 2")
+    fun clearNoUserFace()
+    /**
+     * 根据用户id删除用户特征数据
+     */
+    @Query("update sys_user_characteristic set del_flag = 1 where user_id in (:userIds)")
+    fun deleteUserCharacteristicByIds(userIds: List<Long>)
+
+    /**
+     * 删除不在列表中的指纹
+     */
+    @Query("update sys_user_characteristic set del_flag = 1 where `group` not in (:fingerprintData) and user_id = :userId")
+    fun deleteNoUseFingerprint(fingerprintData: List<String>, userId: Long)
 }

+ 20 - 0
data/src/main/java/com/grkj/data/logic/IUserLogic.kt

@@ -176,4 +176,24 @@ interface IUserLogic {
      * 根据用户id获取用户
      */
     fun getUserByUserId(userId: Long): SysUserDo?
+
+    /**
+     * 绑定用户指纹
+     */
+    fun bindUserCharacteristic(group: String, userId: Long)
+
+    /**
+     * 清除没有用户的指纹
+     */
+    fun clearNoUserFingerprint()
+
+    /**
+     * 清除没有用户的脸部
+     */
+    fun clearNoUserFace()
+
+    /**
+     * 删除不在列表中的指纹
+     */
+    fun deleteNoUseFingerprint(fingerprintData: List<String>, userId: Long)
 }

+ 16 - 0
data/src/main/java/com/grkj/data/logic/impl/network/NetworkUserLogic.kt

@@ -131,6 +131,22 @@ class NetworkUserLogic @Inject constructor() : BaseLogic(), IUserLogic {
         TODO("Not yet implemented")
     }
 
+    override fun bindUserCharacteristic(group: String, userId: Long) {
+        TODO("Not yet implemented")
+    }
+
+    override fun clearNoUserFingerprint() {
+        TODO("Not yet implemented")
+    }
+
+    override fun clearNoUserFace() {
+        TODO("Not yet implemented")
+    }
+
+    override fun deleteNoUseFingerprint(fingerprintData: List<String>, userId: Long) {
+        TODO("Not yet implemented")
+    }
+
     override fun getAllUsersWithRole(): List<SysUserVo> {
         TODO("Not yet implemented")
     }

+ 20 - 0
data/src/main/java/com/grkj/data/logic/impl/standard/UserLogic.kt

@@ -25,6 +25,7 @@ import com.grkj.shared.utils.BCryptUtils
 import com.grkj.shared.utils.BiometricVerifier
 import com.grkj.shared.utils.i18n.I18nManager
 import com.sik.sikcore.data.BeanUtils
+import com.sik.sikcore.extension.exists
 import com.sik.sikcore.extension.file
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.async
@@ -106,6 +107,22 @@ class UserLogic @Inject constructor(
         }
     }
 
+    override fun deleteNoUseFingerprint(fingerprintData: List<String>, userId: Long) {
+        userRepository.deleteNoUseFingerprint(fingerprintData,userId)
+    }
+
+    override fun clearNoUserFace() {
+        userRepository.clearNoUserFace()
+    }
+
+    override fun clearNoUserFingerprint() {
+        userRepository.clearNoUserFingerprint()
+    }
+
+    override fun bindUserCharacteristic(group: String, userId: Long) {
+        userRepository.bindUserCharacteristic(group, userId)
+    }
+
     override fun getUserByUserId(userId: Long): SysUserDo? {
         return userRepository.getUserInfoByUserId(userId)
     }
@@ -218,6 +235,9 @@ class UserLogic @Inject constructor(
         var userId: String? = null
         for (faceData in faceDataList) {
             if (faceData.content.isNotEmpty()) {
+                if (!faceData.content.exists()){
+                    continue
+                }
                 val fileData = faceData.content.file().readText()
                 if (BiometricVerifier.verifyFaceArcSoft(face, fileData)) {
                     userId = faceData.userId.toString()

+ 3 - 1
data/src/main/java/com/grkj/data/model/vo/AddUserDataVo.kt

@@ -10,5 +10,7 @@ data class AddUserDataVo(
     val cardNfc: String?,
     val roleId: List<Long>,
     val workstationId: List<Long>?,
-    val status: Boolean
+    val status: Boolean,
+    var faceSavePath: String? = null,
+    var fingerprintData: List<String> = emptyList()
 )

+ 3 - 1
data/src/main/java/com/grkj/data/model/vo/UpdateUserDataVo.kt

@@ -11,5 +11,7 @@ data class UpdateUserDataVo(
     val cardNfc: String,
     val roleId: List<Long>,
     val workstationId: List<Long>?,
-    val status: Boolean
+    val status: Boolean,
+    var faceSavePath: String? = null,
+    var fingerprintData: List<String> = emptyList()
 )

+ 6 - 0
data/src/main/java/com/grkj/data/model/vo/UserManageVo.kt

@@ -21,6 +21,12 @@ class UserManageVo {
     var status: String? = null
     var fingerprintSize: Int = 0
 
+    @Ignore
+    var faceSavePath: String? = null
+
+    @Ignore
+    var fingerprintData: List<String> = listOf()
+
     @Ignore
     var isSelected: Boolean = false
 

+ 20 - 0
data/src/main/java/com/grkj/data/repository/UserRepository.kt

@@ -111,4 +111,24 @@ interface UserRepository {
      * 获取所有用户特征数据
      */
     fun getAllUserCharacteristic(): List<SysUserCharacteristicDo>
+
+    /**
+     * 绑定用户特征数据
+     */
+    fun bindUserCharacteristic(group: String, userId: Long)
+
+    /**
+     * 清除没有用户的指纹数据
+     */
+    fun clearNoUserFingerprint()
+
+    /**
+     * 清除没有用户的人脸数据
+     */
+    fun clearNoUserFace()
+
+    /**
+     * 删除不在列表中的指纹
+     */
+    fun deleteNoUseFingerprint(fingerprintData: List<String>, userId: Long)
 }

+ 17 - 0
data/src/main/java/com/grkj/data/repository/impl/UserRepositoryImpl.kt

@@ -52,6 +52,7 @@ class UserRepositoryImpl @Inject constructor(
 
     override fun deleteUserByIds(userIds: List<Long>) {
         userDao.deleteUserByIds(userIds)
+        userDao.deleteUserCharacteristicByIds(userIds)
     }
 
     override fun getUserIdsByRoleKey(roleKey: String): List<Long> {
@@ -74,6 +75,22 @@ class UserRepositoryImpl @Inject constructor(
         return userDao.getUserBiometricDataByUserIds(userIds)
     }
 
+    override fun bindUserCharacteristic(group: String, userId: Long) {
+        userDao.bindUserCharacteristic(group, userId)
+    }
+
+    override fun clearNoUserFingerprint() {
+        userDao.clearNoUserFingerprint()
+    }
+
+    override fun deleteNoUseFingerprint(fingerprintData: List<String>, userId: Long) {
+        userDao.deleteNoUseFingerprint(fingerprintData,userId)
+    }
+
+    override fun clearNoUserFace() {
+        userDao.clearNoUserFace()
+    }
+
     override fun getAllUserCharacteristic(): List<SysUserCharacteristicDo> {
         return userDao.getAllUserCharacteristic()
     }

+ 1 - 0
shared/src/main/java/com/grkj/shared/utils/ArcSoftUtil.kt

@@ -384,6 +384,7 @@ object ArcSoftUtil {
      * 注册人脸
      */
     fun registerFace(faceData: List<Pair<Long, String>>) {
+        faceEngine?.removeFaceFeature(-1)
         faceData.forEachIndexed { _, userFace ->
             val faceBitmap = decodeBase64ToBitmap(userFace.second)
             val imageData = bitmapToBgr24(faceBitmap)

+ 93 - 9
ui-base/src/main/java/com/grkj/ui_base/widget/FormLayout.kt

@@ -4,6 +4,7 @@ import android.content.Context
 import android.util.AttributeSet
 import android.view.View
 import android.view.ViewGroup
+import androidx.annotation.IntDef
 import androidx.core.view.MarginLayoutParamsCompat
 import com.grkj.ui_base.R
 import kotlin.math.max
@@ -23,27 +24,54 @@ class FormLayout @JvmOverloads constructor(
     private var maxLabelWidth = 0
     private val rows = ArrayList<Pair<View?, View?>>()
 
+    private var labelAlignment = ALIGN_CENTER  // 新增
+
+
     init {
         context.obtainStyledAttributes(attrs, R.styleable.FormLayout).apply {
             rowSpacingPx = getDimensionPixelSize(R.styleable.FormLayout_rowSpacing, rowSpacingPx)
             colSpacingPx = getDimensionPixelSize(R.styleable.FormLayout_columnSpacing, colSpacingPx)
             labelMinPx = getDimensionPixelSize(R.styleable.FormLayout_labelMinWidth, labelMinPx)
             labelMaxPx = getDimensionPixelSize(R.styleable.FormLayout_labelMaxWidth, labelMaxPx)
+            // 读取新增属性
+            labelAlignment = getInt(R.styleable.FormLayout_labelAlignment, ALIGN_CENTER)
             recycle()
         }
     }
 
     class LayoutParams : MarginLayoutParams {
         var formRole: Int = ROLE_FIELD
+
+        // 可选:逐行覆盖父布局的对齐策略
+        var labelAlignmentOverride: Int? = null
+
         constructor(c: Context, attrs: AttributeSet?) : super(c, attrs) {
             val a = c.obtainStyledAttributes(attrs, R.styleable.FormLayout_Layout)
             formRole = a.getInt(R.styleable.FormLayout_Layout_formRole, ROLE_FIELD)
+            if (a.hasValue(R.styleable.FormLayout_Layout_labelAlignment)) {
+                labelAlignmentOverride =
+                    a.getInt(R.styleable.FormLayout_Layout_labelAlignment, ALIGN_CENTER)
+            }
             a.recycle()
         }
+
         constructor(width: Int, height: Int) : super(width, height)
         constructor(source: ViewGroup.LayoutParams) : super(source)
     }
 
+    // 便捷方法:代码动态修改
+    fun setLabelAlignment(@Alignment mode: Int) {
+        if (labelAlignment != mode) {
+            labelAlignment = mode
+            requestLayout()
+        }
+    }
+
+    @Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION)
+    @IntDef(ALIGN_CENTER, ALIGN_TOP, ALIGN_BASELINE)
+    @Retention(AnnotationRetention.SOURCE)
+    annotation class Alignment
+
     override fun generateLayoutParams(attrs: AttributeSet): ViewGroup.LayoutParams =
         LayoutParams(context, attrs)
 
@@ -58,6 +86,11 @@ class FormLayout @JvmOverloads constructor(
     companion object {
         const val ROLE_LABEL = 0
         const val ROLE_FIELD = 1
+
+        // 新增:对齐常量
+        const val ALIGN_CENTER = 0
+        const val ALIGN_TOP = 1
+        const val ALIGN_BASELINE = 2
     }
 
     override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
@@ -113,8 +146,10 @@ class FormLayout @JvmOverloads constructor(
             val childWSpec = when (lp.width) {
                 ViewGroup.LayoutParams.MATCH_PARENT ->
                     MeasureSpec.makeMeasureSpec(avail, MeasureSpec.EXACTLY)
+
                 ViewGroup.LayoutParams.WRAP_CONTENT ->
                     MeasureSpec.makeMeasureSpec(avail, MeasureSpec.AT_MOST)
+
                 else -> {
                     val target = min(avail, max(0, lp.width))
                     MeasureSpec.makeMeasureSpec(target, MeasureSpec.EXACTLY)
@@ -156,8 +191,10 @@ class FormLayout @JvmOverloads constructor(
                 val childWSpec = when (lp.width) {
                     ViewGroup.LayoutParams.MATCH_PARENT ->
                         MeasureSpec.makeMeasureSpec(avail, MeasureSpec.EXACTLY)
+
                     ViewGroup.LayoutParams.WRAP_CONTENT ->
                         MeasureSpec.makeMeasureSpec(maxWrapFieldWidth, MeasureSpec.EXACTLY)
+
                     else -> {
                         val target = min(avail, max(0, lp.width))
                         MeasureSpec.makeMeasureSpec(target, MeasureSpec.EXACTLY)
@@ -172,7 +209,8 @@ class FormLayout @JvmOverloads constructor(
                 field.measure(childWSpec, childHSpec)
 
                 rowH = max(rowH, field.measuredHeight + lp.topMargin + lp.bottomMargin)
-                maxFieldMeasuredWithMargins = max(maxFieldMeasuredWithMargins, field.measuredWidth + startM + endM)
+                maxFieldMeasuredWithMargins =
+                    max(maxFieldMeasuredWithMargins, field.measuredWidth + startM + endM)
             }
 
             if (!firstRow) totalH += rowSpacingPx
@@ -204,16 +242,62 @@ class FormLayout @JvmOverloads constructor(
             if (!firstRow) y += rowSpacingPx
             firstRow = false
 
-            val lpL = label?.layoutParams as? MarginLayoutParams
-            val lpF = field?.layoutParams as? MarginLayoutParams
-            val labelTotalH = label?.let { (lpL?.topMargin ?: 0) + it.measuredHeight + (lpL?.bottomMargin ?: 0) } ?: 0
-            val fieldTotalH = field?.let { (lpF?.topMargin ?: 0) + it.measuredHeight + (lpF?.bottomMargin ?: 0) } ?: 0
+            val lpL = label?.layoutParams as? LayoutParams
+            val lpF = field?.layoutParams as? LayoutParams
+
+            val labelTotalH =
+                label?.let { (lpL?.topMargin ?: 0) + it.measuredHeight + (lpL?.bottomMargin ?: 0) }
+                    ?: 0
+            val fieldTotalH =
+                field?.let { (lpF?.topMargin ?: 0) + it.measuredHeight + (lpF?.bottomMargin ?: 0) }
+                    ?: 0
             val rowH = max(labelTotalH, fieldTotalH)
 
-            val topL = label?.let { y + ((rowH - (it.measuredHeight + (lpL!!.topMargin + lpL.bottomMargin))) / 2) + lpL.topMargin }
-            val topF = field?.let { y + ((rowH - (it.measuredHeight + (lpF!!.topMargin + lpF.bottomMargin))) / 2) + lpF.topMargin }
+            // 逐行对齐策略:优先 LP 覆盖,否则用全局
+            val rowAlign = lpL?.labelAlignmentOverride ?: labelAlignment
+
+            // 先算 field 的 top,后续对齐 label 用
+            val topF = field?.let {
+                val lp = lpF!!
+                y + ((rowH - (it.measuredHeight + (lp.topMargin + lp.bottomMargin))) / 2) + lp.topMargin
+            }
+
+            // 根据 rowAlign 计算 label 的 top
+            val topL = label?.let {
+                val lp = lpL!!
+                when (rowAlign) {
+                    ALIGN_TOP -> {
+                        // 与 field 可绘制区顶对齐(考虑 margin)
+                        if (field != null) {
+                            topF!!
+                        } else {
+                            // 没有 field 就居中回退
+                            y + ((rowH - (it.measuredHeight + (lp.topMargin + lp.bottomMargin))) / 2) + lp.topMargin
+                        }
+                    }
+
+                    ALIGN_BASELINE -> {
+                        // 若两边都有 baseline,基线对齐;否则回退到 ALIGN_TOP
+                        val labelBase = it.baseline
+                        val fieldBase = field?.baseline ?: -1
+                        if (labelBase >= 0 && fieldBase >= 0 && field != null) {
+                            val fieldTop = topF!!
+                            val targetBaselineY = fieldTop + fieldBase
+                            targetBaselineY - labelBase
+                        } else {
+                            if (field != null) topF!! else
+                                y + ((rowH - (it.measuredHeight + (lp.topMargin + lp.bottomMargin))) / 2) + lp.topMargin
+                        }
+                    }
+
+                    else -> {
+                        // ALIGN_CENTER(保持你原行为)
+                        y + ((rowH - (it.measuredHeight + (lp.topMargin + lp.bottomMargin))) / 2) + lp.topMargin
+                    }
+                }
+            }
 
-            // label:右对齐到 barrier
+            // --- 布局 label(与原逻辑一致)
             label?.let {
                 val lp = lpL!!
                 val startM = MarginLayoutParamsCompat.getMarginStart(lp)
@@ -231,7 +315,7 @@ class FormLayout @JvmOverloads constructor(
                 }
             }
 
-            // field:左对齐到 barrier;MATCH_PARENT 铺满,其它用自身 measuredWidth(wrap 已被统一
+            // --- 布局 field(原逻辑
             field?.let {
                 val lp = lpF!!
                 val startM = MarginLayoutParamsCompat.getMarginStart(lp)

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

@@ -840,4 +840,5 @@
     <string name="zone">Area Range</string>
     <string name="job_card_already_bind">Job card already bind</string>
     <string name="bind_job_card_success">Job card bind success</string>
+    <string name="user_face_register_success">User face register success</string>
 </resources>

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

@@ -840,4 +840,5 @@
     <string name="zone">区域范围</string>
     <string name="job_card_already_bind">该工卡已被绑定</string>
     <string name="bind_job_card_success">工卡绑定成功</string>
+    <string name="user_face_register_success">人脸录入成功</string>
 </resources>

+ 8 - 2
ui-base/src/main/res/values/attrs.xml

@@ -167,13 +167,19 @@
         <!-- 可选:限制左列宽度范围 -->
         <attr name="labelMinWidth" format="dimension" />
         <attr name="labelMaxWidth" format="dimension" />
+        <attr name="labelAlignment" format="enum">
+            <enum name="center" value="0" />
+            <enum name="top" value="1" />
+            <enum name="baseline" value="2" />
+        </attr>
     </declare-styleable>
 
     <declare-styleable name="FormLayout_Layout">
         <attr name="formRole" format="enum">
-            <enum name="label" value="0"/>
-            <enum name="field" value="1"/>
+            <enum name="label" value="0" />
+            <enum name="field" value="1" />
         </attr>
+        <attr name="labelAlignment" />
     </declare-styleable>
     <declare-styleable name="MaxHeightRecyclerView">
         <attr name="maxHeight" format="dimension"/>

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

@@ -840,4 +840,5 @@
     <string name="zone">区域范围</string>
     <string name="job_card_already_bind">该工卡已被绑定</string>
     <string name="bind_job_card_success">工卡绑定成功</string>
+    <string name="user_face_register_success">人脸录入成功</string>
 </resources>