Ver Fonte

feat(Data Export): Implement data export functionality
- Add data export UI and logic
- Support exporting data to Excel files
- Add i18n for data export related text
- Add POI library for Excel file operations
- Update entities with Excel export annotations
- Refactor BackupScheduler nextDelayMillis logic

周文健 há 2 meses atrás
pai
commit
a1cb6d3794
45 ficheiros alterados com 1555 adições e 41 exclusões
  1. 70 0
      app/src/main/assets/i18n/en-US.json
  2. 63 3
      app/src/main/assets/i18n/zh-CN.json
  3. 100 0
      app/src/main/java/com/grkj/iscs/features/main/fragment/data_manage/DataExportFragment.kt
  4. 64 0
      app/src/main/java/com/grkj/iscs/features/main/viewmodel/data_manage/DataExportViewModel.kt
  5. 1 1
      app/src/main/res/layout/fragment_data_export.xml
  6. 35 0
      app/src/main/res/layout/item_data_export.xml
  7. 9 0
      data/build.gradle.kts
  8. 11 0
      data/src/main/java/com/grkj/data/dao/HardwareDao.kt
  9. 6 0
      data/src/main/java/com/grkj/data/dao/IsSopDao.kt
  10. 6 0
      data/src/main/java/com/grkj/data/dao/JobTicketDao.kt
  11. 20 20
      data/src/main/java/com/grkj/data/database/BackupScheduler.kt
  12. 2 0
      data/src/main/java/com/grkj/data/di/AppEntryPoint.kt
  13. 3 0
      data/src/main/java/com/grkj/data/di/LogicManager.kt
  14. 11 0
      data/src/main/java/com/grkj/data/di/LogicModule.kt
  15. 75 0
      data/src/main/java/com/grkj/data/enums/DataExportTableEnum.kt
  16. 14 0
      data/src/main/java/com/grkj/data/logic/IDataExportLogic.kt
  17. 6 0
      data/src/main/java/com/grkj/data/logic/IHardwareLogic.kt
  18. 11 0
      data/src/main/java/com/grkj/data/logic/IJobTicketLogic.kt
  19. 12 0
      data/src/main/java/com/grkj/data/logic/ISopLogic.kt
  20. 5 0
      data/src/main/java/com/grkj/data/logic/IWorkstationLogic.kt
  21. 18 0
      data/src/main/java/com/grkj/data/logic/impl/network/NetworkDataExportLogic.kt
  22. 5 0
      data/src/main/java/com/grkj/data/logic/impl/network/NetworkHardwareLogic.kt
  23. 10 0
      data/src/main/java/com/grkj/data/logic/impl/network/NetworkJobTicketLogic.kt
  24. 10 0
      data/src/main/java/com/grkj/data/logic/impl/network/NetworkSopLogic.kt
  25. 4 0
      data/src/main/java/com/grkj/data/logic/impl/network/NetworkWorkstationLogic.kt
  26. 157 0
      data/src/main/java/com/grkj/data/logic/impl/standard/DataExportLogic.kt
  27. 67 11
      data/src/main/java/com/grkj/data/logic/impl/standard/HardwareLogic.kt
  28. 38 1
      data/src/main/java/com/grkj/data/logic/impl/standard/JobTicketLogic.kt
  29. 24 3
      data/src/main/java/com/grkj/data/logic/impl/standard/SopLogic.kt
  30. 5 0
      data/src/main/java/com/grkj/data/logic/impl/standard/WorkstationLogic.kt
  31. 8 0
      data/src/main/java/com/grkj/data/model/dos/BaseBean.kt
  32. 8 0
      data/src/main/java/com/grkj/data/model/dos/BaseStandardBean.kt
  33. 19 0
      data/src/main/java/com/grkj/data/model/dos/IsIsolationPoint.kt
  34. 18 0
      data/src/main/java/com/grkj/data/model/dos/IsJobTicket.kt
  35. 16 1
      data/src/main/java/com/grkj/data/model/dos/IsSop.kt
  36. 14 0
      data/src/main/java/com/grkj/data/model/dos/SysRole.kt
  37. 23 0
      data/src/main/java/com/grkj/data/model/dos/SysUserDo.kt
  38. 20 0
      data/src/main/java/com/grkj/data/model/vo/DataExportJobVo.kt
  39. 12 0
      data/src/main/java/com/grkj/data/model/vo/DataExportLockedPointVo.kt
  40. 20 0
      data/src/main/java/com/grkj/data/model/vo/DataExportPointVo.kt
  41. 18 0
      data/src/main/java/com/grkj/data/model/vo/DataExportSopVo.kt
  42. 28 0
      data/src/main/java/com/grkj/data/model/vo/DataExportVo.kt
  43. 12 1
      data/src/main/java/com/grkj/data/model/vo/LockedPointVo.kt
  44. 15 0
      data/src/main/java/com/grkj/data/model/vo/WorkstationManageVo.kt
  45. 462 0
      data/src/main/java/com/grkj/data/utils/ExcelExporter.kt

+ 70 - 0
app/src/main/assets/i18n/en-US.json

@@ -3144,6 +3144,11 @@
     "type": "text",
     "value": "Area Name"
   },
+  "workstation_manage_parent_workstation_name": {
+    "key": "workstation_manage_parent_workstation_name",
+    "type": "text",
+    "value": "Parent area Name"
+  },
   "you_are_not_locker_tip": {
     "key": "you_are_not_locker_tip",
     "type": "text",
@@ -4032,5 +4037,70 @@
     "key": "please_input_auto_logout_time_correct",
     "type": "text",
     "value": "Please input auto logout time correct"
+  },
+  "data_export": {
+    "key": "data_export",
+    "type": "text",
+    "value": "Data export"
+  },
+  "data_export_tip": {
+    "key": "data_export",
+    "type": "text",
+    "value": "Please select the table you want to export and click Export."
+  },
+  "data_table": {
+    "key": "data_table",
+    "type": "text",
+    "value": "Data Table"
+  },
+  "last_export_datetime": {
+    "key": "last_export_datetime",
+    "type": "text",
+    "value": "Last export time"
+  },
+  "please_select_data_you_want_to_export": {
+    "key": "please_select_data_you_want_to_export",
+    "type": "text",
+    "value": "Please select the data table you want to export."
+  },
+  "data_export_success_tip": {
+    "key": "data_export_success_tip",
+    "type": "text",
+    "value": "Data export completed. Please select a folder and click the bottom right button to save."
+  },
+  "data_export_error": {
+    "key": "data_export_error",
+    "type": "text",
+    "value": "Data export failed."
+  },
+  "user": {
+    "key": "user",
+    "type": "text",
+    "value": "User"
+  },
+  "role": {
+    "key": "role",
+    "type": "text",
+    "value": "Role"
+  },
+  "workstation": {
+    "key": "workstation",
+    "type": "text",
+    "value": "Workstation"
+  },
+  "point": {
+    "key": "point",
+    "type": "text",
+    "value": "Point"
+  },
+  "sop": {
+    "key": "sop",
+    "type": "text",
+    "value": "SOP"
+  },
+  "exporting": {
+    "key": "exporting",
+    "type": "text",
+    "value": "Exporting……"
   }
 }

+ 63 - 3
app/src/main/assets/i18n/zh-CN.json

@@ -3144,6 +3144,11 @@
     "type": "text",
     "value": "区域名称"
   },
+  "workstation_manage_parent_workstation_name": {
+    "key": "workstation_manage_parent_workstation_name",
+    "type": "text",
+    "value": "上级区域名称"
+  },
   "you_are_not_locker_tip": {
     "key": "you_are_not_locker_tip",
     "type": "text",
@@ -4024,6 +4029,11 @@
     "type": "text",
     "value": "请输入自动登出时间"
   },
+  "please_input_auto_logout_time_correct": {
+    "key": "please_input_auto_logout_time_correct",
+    "type": "text",
+    "value": "请设置正确的自动登出时间"
+  },
   "data_export": {
     "key": "data_export",
     "type": "text",
@@ -4044,9 +4054,59 @@
     "type": "text",
     "value": "上次导出时间"
   },
-  "please_input_auto_logout_time_correct": {
-    "key": "please_input_auto_logout_time_correct",
+  "please_select_data_you_want_to_export": {
+    "key": "please_select_data_you_want_to_export",
     "type": "text",
-    "value": "请设置正确的自动登出时间"
+    "value": "请选择你需要导出的数据表。"
+  },
+  "confirm_exec": {
+    "key": "confirm_exec",
+    "type": "text",
+    "value": "执行确认"
+  },
+  "data_export_success_tip": {
+    "key": "data_export_success_tip",
+    "type": "text",
+    "value": "数据导出完成,请选择文件夹并点击右下角按钮进行保存。"
+  },
+  "data_export_error": {
+    "key": "data_export_error",
+    "type": "text",
+    "value": "数据导出失败。"
+  },
+  "user": {
+    "key": "user",
+    "type": "text",
+    "value": "用户"
+  },
+  "role": {
+    "key": "role",
+    "type": "text",
+    "value": "角色"
+  },
+  "workstation": {
+    "key": "workstation",
+    "type": "text",
+    "value": "区域"
+  },
+  "point": {
+    "key": "point",
+    "type": "text",
+    "value": "点位"
+  },
+  "sop": {
+    "key": "sop",
+    "type": "text",
+    "value": "SOP"
+  },
+  "exporting": {
+    "key": "exporting",
+    "type": "text",
+    "value": "导出中……"
+  },
+  "ticket_name": {
+    "key": "ticket_name",
+    "type": "text",
+    "value": "作业名称"
   }
 }

+ 100 - 0
app/src/main/java/com/grkj/iscs/features/main/fragment/data_manage/DataExportFragment.kt

@@ -1,8 +1,20 @@
 package com.grkj.iscs.features.main.fragment.data_manage
 
+import androidx.fragment.app.viewModels
+import com.drake.brv.utils.linear
+import com.drake.brv.utils.models
+import com.drake.brv.utils.setup
+import com.grkj.data.enums.DataExportTableEnum
+import com.grkj.data.model.vo.DataExportVo
 import com.grkj.iscs.R
 import com.grkj.iscs.databinding.FragmentDataExportBinding
+import com.grkj.iscs.databinding.ItemDataExportBinding
+import com.grkj.iscs.features.main.viewmodel.data_manage.DataExportViewModel
+import com.grkj.shared.utils.FilePickerUtils
+import com.grkj.shared.utils.SAFHelper.copyFileToDir
 import com.grkj.ui_base.base.BaseFragment
+import com.grkj.ui_base.dialog.TipDialog
+import com.grkj.ui_base.utils.CommonUtils
 import com.sik.sikcore.extension.setDebouncedClickListener
 import dagger.hilt.android.AndroidEntryPoint
 
@@ -11,13 +23,101 @@ import dagger.hilt.android.AndroidEntryPoint
  */
 @AndroidEntryPoint
 class DataExportFragment : BaseFragment<FragmentDataExportBinding>() {
+    private val viewModel: DataExportViewModel by viewModels()
+    private lateinit var filePickerUtils: FilePickerUtils
+
     override fun getLayoutId(): Int {
         return R.layout.fragment_data_export
     }
 
     override fun initView() {
+        filePickerUtils = FilePickerUtils(this)
         binding.back.setDebouncedClickListener {
             navController.popBackStack()
         }
+        binding.export.setDebouncedClickListener {
+            if (checkSelected()) {
+                exportData()
+            }
+        }
+        binding.listRv.linear().setup {
+            addType<DataExportVo>(R.layout.item_data_export)
+            onBind {
+                val item = getModel<DataExportVo>()
+                val itemBinding = getBinding<ItemDataExportBinding>()
+                itemBinding.select.setOnCheckedChangeListener(null)
+                itemBinding.select.isChecked = item.isSelected
+                itemBinding.select.setOnCheckedChangeListener { _, isChecked ->
+                    item.isSelected = isChecked
+                    checkSelectedAll()
+                }
+                itemBinding.tableName.text = item.tableName
+                itemBinding.lastExportTime.text = item.lastUpdateTime
+            }
+        }
+        checkSelectedAll()
+    }
+
+    override fun initData() {
+        super.initData()
+        viewModel.getExportTableData().observe(this) {
+            binding.listRv.models = viewModel.dataExportTableData
+        }
+    }
+
+    /**
+     * 导出数据
+     */
+    private fun exportData() {
+        showLoading(CommonUtils.getStr("exporting"))
+        viewModel.exportData().observe(this) {
+            hideLoading()
+            if (it != null) {
+                TipDialog.showSuccess(
+                    CommonUtils.getStr("data_export_success_tip"),
+                    showCancel = false,
+                    onConfirmClick = {
+                        filePickerUtils.pickDirectory { treeUri ->
+                            treeUri?.let { treeUri ->
+                                requireContext().copyFileToDir(treeUri, it)
+                                it.delete()
+                                viewModel.updateExportTime()
+                                binding.listRv.adapter?.notifyDataSetChanged()
+                                showToast(CommonUtils.getStr("save_success"))
+                            }
+                        }
+                    }, onCancelClick = {
+                        it.delete()
+                    })
+            } else {
+                TipDialog.showError(CommonUtils.getStr("data_export_error"))
+            }
+
+        }
+    }
+
+    /**
+     * 检查是否选中
+     */
+    private fun checkSelected(): Boolean {
+        if (viewModel.dataExportTableData.none { it.isSelected }) {
+            showToast(CommonUtils.getStr("please_select_data_you_want_to_export"))
+            return false
+        }
+        return true
+    }
+
+    /**
+     * 检查是否全选
+     */
+    private fun checkSelectedAll() {
+        binding.selectAll.setOnCheckedChangeListener(null)
+        binding.selectAll.isSelected == viewModel.dataExportTableData.all { it.isSelected }
+        binding.selectAll.setOnCheckedChangeListener { _, isChecked ->
+            viewModel.dataExportTableData.forEach {
+                it.isSelected = isChecked
+            }
+            binding.listRv.adapter?.notifyDataSetChanged()
+        }
     }
 }

+ 64 - 0
app/src/main/java/com/grkj/iscs/features/main/viewmodel/data_manage/DataExportViewModel.kt

@@ -0,0 +1,64 @@
+package com.grkj.iscs.features.main.viewmodel.data_manage
+
+import androidx.annotation.ReturnThis
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.liveData
+import com.grkj.data.enums.DataExportTableEnum
+import com.grkj.data.enums.getLastExportTime
+import com.grkj.data.enums.getTableName
+import com.grkj.data.enums.setLastExportTime
+import com.grkj.data.logic.IDataExportLogic
+import com.grkj.data.logic.impl.standard.DataExportLogic
+import com.grkj.data.model.vo.DataExportVo
+import com.grkj.ui_base.base.BaseViewModel
+import com.sik.sikcore.date.TimeUtils
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Dispatchers
+import java.io.File
+import javax.inject.Inject
+
+/**
+ * 数据导出
+ */
+@HiltViewModel
+class DataExportViewModel @Inject constructor(val dataExportLogic: IDataExportLogic) :
+    BaseViewModel() {
+    var dataExportTableData: List<DataExportVo> = mutableListOf()
+
+    /**
+     * 获取导出表数据
+     */
+    fun getExportTableData(): LiveData<Boolean> {
+        return liveData(Dispatchers.IO) {
+            dataExportTableData =
+                DataExportTableEnum.values().filter { it != DataExportTableEnum.NONE }.map {
+                    DataExportVo().apply {
+                        dataExportTableEnum = it
+                        tableName = it.getTableName()
+                        lastUpdateTime = it.getLastExportTime()
+                    }
+                }
+            emit(true)
+        }
+    }
+
+    /**
+     * 导出数据
+     */
+    fun exportData(): LiveData<File?> {
+        return liveData(Dispatchers.IO) {
+            emit(dataExportLogic.dataExport(dataExportTableData.filter { it.isSelected }
+                .map { it.dataExportTableEnum }))
+        }
+    }
+
+    /**
+     * 更新导出时间
+     */
+    fun updateExportTime() {
+        dataExportTableData.forEach {
+            it.dataExportTableEnum.setLastExportTime()
+            it.lastUpdateTime = it.dataExportTableEnum.getLastExportTime()
+        }
+    }
+}

+ 1 - 1
app/src/main/res/layout/fragment_data_export.xml

@@ -121,7 +121,7 @@
         </LinearLayout>
 
         <androidx.recyclerview.widget.RecyclerView
-            android:id="@+id/sop_list_rv"
+            android:id="@+id/list_rv"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             android:layout_marginHorizontal="@dimen/iscs_space_4"

+ 35 - 0
app/src/main/res/layout/item_data_export.xml

@@ -0,0 +1,35 @@
+<?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="match_parent"
+        android:layout_height="wrap_content"
+        android:divider="@drawable/divider_table"
+        android:showDividers="middle">
+
+        <com.google.android.material.checkbox.MaterialCheckBox
+            android:id="@+id/select"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            app:useMaterialThemeColors="true" />
+
+        <TextView
+            android:id="@+id/table_name"
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="1"
+            android:gravity="center"
+            android:textColor="?attr/colorTextPrimary"
+            android:textSize="@dimen/iscs_text_md" />
+
+        <TextView
+            android:id="@+id/last_export_time"
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="1"
+            android:gravity="center"
+            android:textColor="?attr/colorTextPrimary"
+            android:textSize="@dimen/iscs_text_md" />
+    </LinearLayout>
+</layout>

+ 9 - 0
data/build.gradle.kts

@@ -52,4 +52,13 @@ dependencies {
 
     implementation("net.zetetic:sqlcipher-android:4.6.0@aar")
     implementation("androidx.sqlite:sqlite:2.4.0")
+    // 轻量版 POI(只含 xlsx 必需组件)
+    api("org.apache.poi:poi:3.17")
+    api("org.apache.poi:poi-ooxml:3.17") {
+        exclude(group = "org.apache.xmlbeans", module = "xmlbeans")
+    }
+    api("org.apache.xmlbeans:xmlbeans:2.4.0") // 唯一来源
+    // 可选:给 StAX 一个高性能实现(建议一起加,兼容更好)
+    api("com.fasterxml.woodstox:woodstox-core:6.5.1")
+    api("org.codehaus.woodstox:stax2-api:4.2.1")
 }

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

@@ -474,6 +474,17 @@ interface HardwareDao {
     )
     fun getAllPointCount(workstationId: Long?): Int
 
+    /**
+     * 所有点位
+     */
+    @Query(
+        """
+        select *
+        from is_isolation_point iip where del_flag = 0
+    """
+    )
+    fun getAllPointData(): List<IsIsolationPoint>
+
     /**
      * 获取所有rfid数据
      */

+ 6 - 0
data/src/main/java/com/grkj/data/dao/IsSopDao.kt

@@ -225,4 +225,10 @@ interface IsSopDao {
      */
     @Update
     fun updateStep(sopWorkflowStep: List<IsSopWorkflowStep>)
+
+    /**
+     * 获取所有sop数据
+     */
+    @Query("select * from is_sop where del_flag = 0")
+    fun getSopData(): List<IsSop>
 }

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

@@ -835,4 +835,10 @@ interface JobTicketDao {
      */
     @Query("select * from is_job_ticket_group where ticket_id = :ticketId")
     fun getJobTicketGroupsByTicketId(ticketId: Long): List<IsJobTicketGroup>
+
+    /**
+     * 获取所有作业
+     */
+    @Query("select * from is_job_ticket where del_flag = 0")
+    fun getAllJob(): List<IsJobTicket>
 }

+ 20 - 20
data/src/main/java/com/grkj/data/database/BackupScheduler.kt

@@ -100,9 +100,16 @@ object BackupScheduler {
      */
     private fun nextDelayMillis(cfg: SimpleBackupConfig): Long {
         val now = Calendar.getInstance()
+        val nowMs = now.timeInMillis
+
         for (i in 0..7) {
-            val cand = now.clone() as Calendar
-            cand.add(Calendar.DAY_OF_YEAR, i)
+            val cand = (now.clone() as Calendar).apply {
+                add(Calendar.DAY_OF_YEAR, i)
+                set(Calendar.HOUR_OF_DAY, cfg.hour)
+                set(Calendar.MINUTE, cfg.minute)
+                set(Calendar.SECOND, 0)
+                set(Calendar.MILLISECOND, 0)
+            }
 
             val dow = cand.get(Calendar.DAY_OF_WEEK) // 1..7
             val bit = when (dow) {
@@ -116,28 +123,21 @@ object BackupScheduler {
                 else -> 0
             }
             val selected = (cfg.daysMask and (1 shl bit)) != 0
+            if (!selected) continue
 
-            if (selected) {
-                cand.set(Calendar.HOUR_OF_DAY, cfg.hour)
-                cand.set(Calendar.MINUTE, cfg.minute)
-                cand.set(Calendar.SECOND, 0)
-                cand.set(Calendar.MILLISECOND, 0)
-                if (cand.after(now)) {
-                    return cand.timeInMillis - now.timeInMillis
-                }
-                // 同一天但已过时刻 -> 继续找后面的被选日
-            }
+            val diff = cand.timeInMillis - nowMs
+            // 今天是选中日,但已过目标时刻 => 立即执行
+            if (i == 0 && diff <= 0L) return 0L
+
+            // 未来选中日的目标时刻
+            if (diff > 0L) return diff
         }
-        // 理论到不了,兜底:明天同一时刻
-        val cand = now.clone() as Calendar
-        cand.add(Calendar.DAY_OF_YEAR, 1)
-        cand.set(Calendar.HOUR_OF_DAY, cfg.hour)
-        cand.set(Calendar.MINUTE, cfg.minute)
-        cand.set(Calendar.SECOND, 0)
-        cand.set(Calendar.MILLISECOND, 0)
-        return cand.timeInMillis - now.timeInMillis
+
+        // 兜底:没有任何选中日(或者异常)=> 立即执行
+        return 0L
     }
 
+
     /** 省电约束:电量不低再跑 */
     private fun defaultConstraints() = Constraints.Builder()
         .setRequiresBatteryNotLow(true)

+ 2 - 0
data/src/main/java/com/grkj/data/di/AppEntryPoint.kt

@@ -1,5 +1,6 @@
 package com.grkj.data.di
 
+import com.grkj.data.logic.IDataExportLogic
 import com.grkj.data.logic.IExceptionLogic
 import com.grkj.data.logic.IHardwareLogic
 import com.grkj.data.logic.IIsolationPointLogic
@@ -29,4 +30,5 @@ interface AppEntryPoint {
     fun sysMenuLogic(): ISysMenuLogic
     fun workflowLogic(): IWorkflowLogic
     fun exceptionLogic(): IExceptionLogic
+    fun dataExportLogic(): IDataExportLogic
 }

+ 3 - 0
data/src/main/java/com/grkj/data/di/LogicManager.kt

@@ -1,6 +1,7 @@
 package com.grkj.data.di
 
 import android.app.Application
+import com.grkj.data.logic.IDataExportLogic
 import com.grkj.data.logic.IExceptionLogic
 import com.grkj.data.logic.IHardwareLogic
 import com.grkj.data.logic.IIsolationPointLogic
@@ -29,6 +30,7 @@ object LogicManager {
     lateinit var sysMenuLogic: ISysMenuLogic
     lateinit var workflowLogic: IWorkflowLogic
     lateinit var exceptionLogic: IExceptionLogic
+    lateinit var dataExportLogic: IDataExportLogic
 
     fun init(app: Application) {
         val ep = EntryPointAccessors.fromApplication(app, AppEntryPoint::class.java)
@@ -43,5 +45,6 @@ object LogicManager {
         sysMenuLogic = ep.sysMenuLogic()
         workflowLogic = ep.workflowLogic()
         exceptionLogic = ep.exceptionLogic()
+        dataExportLogic = ep.dataExportLogic()
     }
 }

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

@@ -1,6 +1,7 @@
 package com.grkj.data.di
 
 import com.grkj.data.data.MMKVConstants
+import com.grkj.data.logic.IDataExportLogic
 import com.grkj.data.logic.IExceptionLogic
 import com.grkj.data.logic.IHardwareLogic
 import com.grkj.data.logic.IIsolationPointLogic
@@ -14,6 +15,7 @@ import com.grkj.data.logic.IWorkflowLogic
 import com.grkj.data.logic.IWorkstationLogic
 import com.grkj.data.logic.impl.network.NetworkExceptionLogic
 import com.grkj.data.logic.impl.network.NetworkHardwareLogic
+import com.grkj.data.logic.impl.network.NetworkDataExportLogic
 import com.grkj.data.logic.impl.network.NetworkIsolationPointLogic
 import com.grkj.data.logic.impl.network.NetworkJobTicketLogic
 import com.grkj.data.logic.impl.network.NetworkRfidTokenLogic
@@ -23,6 +25,7 @@ import com.grkj.data.logic.impl.network.NetworkSysMenuLogic
 import com.grkj.data.logic.impl.network.NetworkUserLogic
 import com.grkj.data.logic.impl.network.NetworkWorkflowLogic
 import com.grkj.data.logic.impl.network.NetworkWorkstationLogic
+import com.grkj.data.logic.impl.standard.DataExportLogic
 import com.grkj.data.logic.impl.standard.ExceptionLogic
 import com.grkj.data.logic.impl.standard.HardwareLogic
 import com.grkj.data.logic.impl.standard.IsolationPointLogic
@@ -135,4 +138,12 @@ object LogicModule {
         network: NetworkExceptionLogic,
     ): IExceptionLogic =
         if (MMKVConstants.SERVER_ADDRESS.getMMKVData("").isNotEmpty()) network else standard
+
+    @Provides
+    @Singleton
+    fun provideDataExportLogic(
+        standard: DataExportLogic,
+        network: NetworkDataExportLogic,
+    ): IDataExportLogic =
+        if (MMKVConstants.SERVER_ADDRESS.getMMKVData("").isNotEmpty()) network else standard
 }

+ 75 - 0
data/src/main/java/com/grkj/data/enums/DataExportTableEnum.kt

@@ -0,0 +1,75 @@
+package com.grkj.data.enums
+
+import com.grkj.shared.utils.i18n.I18nManager
+import com.sik.sikcore.date.TimeUtils
+import com.sik.sikcore.extension.getMMKVData
+import com.sik.sikcore.extension.saveMMKVData
+
+/**
+ * 数据导出表枚举
+ */
+enum class DataExportTableEnum(val tableKey: String) {
+    /**
+     * 空
+     */
+    NONE("none"),
+
+    /**
+     * 用户表
+     */
+    USER("user"),
+
+    /**
+     * 角色表
+     */
+    ROLE("role"),
+
+    /**
+     * 区域表
+     */
+    WORKSTATION("workstation"),
+
+    /**
+     * 点位表
+     */
+    POINT("point"),
+
+    /**
+     * sop表
+     */
+    SOP("sop"),
+
+    /**
+     * 作业表
+     */
+    JOB("job"),
+
+    /**
+     * 锁定中的点位
+     */
+    LOCKED_POINT("locked_point"),
+    ;
+
+
+}
+
+/**
+ * 设置最后导出时间
+ */
+fun DataExportTableEnum.setLastExportTime() {
+    tableKey.saveMMKVData(TimeUtils.nowString(TimeUtils.DEFAULT_DATE_HOUR_MIN_SEC_FORMAT))
+}
+
+/**
+ * 获取最后导出时间
+ */
+fun DataExportTableEnum.getLastExportTime(): String {
+    return tableKey.getMMKVData("")
+}
+
+/**
+ * 获取表名称
+ */
+fun DataExportTableEnum.getTableName(): String {
+    return I18nManager.t(tableKey)
+}

+ 14 - 0
data/src/main/java/com/grkj/data/logic/IDataExportLogic.kt

@@ -0,0 +1,14 @@
+package com.grkj.data.logic
+
+import com.grkj.data.enums.DataExportTableEnum
+import java.io.File
+
+/**
+ * 数据导出业务
+ */
+interface IDataExportLogic {
+    /**
+     * 数据导出
+     */
+    fun dataExport(dataExportTables: List<DataExportTableEnum>): File?
+}

+ 6 - 0
data/src/main/java/com/grkj/data/logic/IHardwareLogic.kt

@@ -18,6 +18,7 @@ import com.grkj.data.model.res.KeyPageRes
 import com.grkj.data.model.res.LockInfoRes
 import com.grkj.data.model.res.LockPageRes
 import com.grkj.data.model.vo.CardManageFilterVo
+import com.grkj.data.model.vo.DataExportPointVo
 import com.grkj.data.model.vo.KeyManageFilterVo
 import com.grkj.data.model.vo.LockManageFilterVo
 import com.grkj.data.model.vo.PointToMapVo
@@ -354,6 +355,11 @@ interface IHardwareLogic {
      */
     fun getAllPointCount(overviewWorkstationId: Long?): Int
 
+    /**
+     * 获取所有导出点位
+     */
+    fun getAllDataExportPointData(): List<DataExportPointVo>
+
     /**
      * 获取所有硬件数量
      */

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

@@ -7,6 +7,8 @@ import com.grkj.data.model.local.TodoStepJoin
 import com.grkj.data.model.req.LockPointUpdateReq
 import com.grkj.data.model.res.StepDetailRes
 import com.grkj.data.model.res.TicketDetailRes
+import com.grkj.data.model.vo.DataExportJobVo
+import com.grkj.data.model.vo.DataExportLockedPointVo
 import com.grkj.data.model.vo.IsJobTicketDataVo
 import com.grkj.data.model.vo.IsJobTicketKeyDataVo
 import com.grkj.data.model.vo.IsJobTicketLockDataVo
@@ -188,6 +190,10 @@ interface IJobTicketLogic {
      * 获取所有锁定中的点位
      */
     fun getAllLockedPointsData(): List<LockedPointVo>
+    /**
+     * 获取导出的所有锁定中的点位
+     */
+    fun getAllLockedPointsExportData(): List<DataExportLockedPointVo>
 
     /**
      * 获取所有使用中的点位
@@ -351,4 +357,9 @@ interface IJobTicketLogic {
      * 检查钥匙是否在使用
      */
     fun checkKeyInUse(keyIds: List<Long>): Boolean
+
+    /**
+     * 获取所有作业数据
+     */
+    fun getAllJobData(): List<DataExportJobVo>
 }

+ 12 - 0
data/src/main/java/com/grkj/data/logic/ISopLogic.kt

@@ -1,7 +1,9 @@
 package com.grkj.data.logic
 
+import com.grkj.data.model.dos.IsSop
 import com.grkj.data.model.dos.IsSopGroup
 import com.grkj.data.model.dos.IsSopWorkflowStep
+import com.grkj.data.model.vo.DataExportSopVo
 import com.grkj.data.model.vo.JobPointVo
 import com.grkj.data.model.vo.JobTicketGroupDataVo
 import com.grkj.data.model.vo.JobUserVo
@@ -97,4 +99,14 @@ interface ISopLogic {
      * 更新流程步骤
      */
     fun updateStep(isSopWorkflowStep: List<IsSopWorkflowStep>)
+
+    /**
+     * 获取所有sop数据
+     */
+    fun getSopData(): List<IsSop>
+
+    /**
+     * 获取sop导出数据
+     */
+    fun getDataExportSopData(): List<DataExportSopVo>
 }

+ 5 - 0
data/src/main/java/com/grkj/data/logic/IWorkstationLogic.kt

@@ -51,4 +51,9 @@ interface IWorkstationLogic {
      * 更新区域设定
      */
     fun updateWorkstation(workstationId: Long, workstationName: String)
+
+    /**
+     * 获取所有区域数据
+     */
+    fun getAllWorkstationData(): List<IsWorkstation>
 }

+ 18 - 0
data/src/main/java/com/grkj/data/logic/impl/network/NetworkDataExportLogic.kt

@@ -0,0 +1,18 @@
+package com.grkj.data.logic.impl.network
+
+import com.grkj.data.enums.DataExportTableEnum
+import com.grkj.data.logic.BaseLogic
+import com.grkj.data.logic.IDataExportLogic
+import java.io.File
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * 联网版数据导出业务
+ */
+@Singleton
+class NetworkDataExportLogic @Inject constructor() : BaseLogic(), IDataExportLogic {
+    override fun dataExport(dataExportTables: List<DataExportTableEnum>): File? {
+        TODO("Not yet implemented")
+    }
+}

+ 5 - 0
data/src/main/java/com/grkj/data/logic/impl/network/NetworkHardwareLogic.kt

@@ -23,6 +23,7 @@ import com.grkj.data.logic.BaseLogic
 import com.grkj.data.logic.IHardwareLogic
 import com.grkj.data.model.dos.IsIsolationPoint
 import com.grkj.data.model.dos.IsMapPoint
+import com.grkj.data.model.vo.DataExportPointVo
 import com.grkj.data.model.vo.PointToMapVo
 import javax.inject.Inject
 import javax.inject.Singleton
@@ -355,6 +356,10 @@ class NetworkHardwareLogic  @Inject constructor() : BaseLogic(), IHardwareLogic
         TODO("Not yet implemented")
     }
 
+    override fun getAllDataExportPointData(): List<DataExportPointVo> {
+        TODO("Not yet implemented")
+    }
+
     override fun getAllHardwareCount(): Int {
         TODO("Not yet implemented")
     }

+ 10 - 0
data/src/main/java/com/grkj/data/logic/impl/network/NetworkJobTicketLogic.kt

@@ -20,6 +20,8 @@ import com.grkj.data.model.vo.JobUserVo
 import com.grkj.data.model.vo.LockedPointVo
 import com.grkj.data.logic.BaseLogic
 import com.grkj.data.logic.IJobTicketLogic
+import com.grkj.data.model.vo.DataExportJobVo
+import com.grkj.data.model.vo.DataExportLockedPointVo
 import javax.inject.Inject
 import javax.inject.Singleton
 
@@ -277,6 +279,10 @@ class NetworkJobTicketLogic @Inject constructor() : BaseLogic(), IJobTicketLogic
         TODO("Not yet implemented")
     }
 
+    override fun getAllLockedPointsExportData(): List<DataExportLockedPointVo> {
+        TODO("Not yet implemented")
+    }
+
     override fun checkKeyInUse(keyIds: List<Long>): Boolean {
         TODO("Not yet implemented")
     }
@@ -289,6 +295,10 @@ class NetworkJobTicketLogic @Inject constructor() : BaseLogic(), IJobTicketLogic
         TODO("Not yet implemented")
     }
 
+    override fun getAllJobData(): List<DataExportJobVo> {
+        TODO("Not yet implemented")
+    }
+
     override fun isNextLockOrUnLock(ticketId: Long): NextJobPrompt {
         TODO("Not yet implemented")
     }

+ 10 - 0
data/src/main/java/com/grkj/data/logic/impl/network/NetworkSopLogic.kt

@@ -8,6 +8,8 @@ import com.grkj.data.model.vo.JobUserVo
 import com.grkj.data.model.vo.SopManageVo
 import com.grkj.data.logic.BaseLogic
 import com.grkj.data.logic.ISopLogic
+import com.grkj.data.model.dos.IsSop
+import com.grkj.data.model.vo.DataExportSopVo
 import javax.inject.Inject
 import javax.inject.Singleton
 
@@ -45,6 +47,14 @@ class NetworkSopLogic @Inject constructor()  : BaseLogic(), ISopLogic{
         TODO("Not yet implemented")
     }
 
+    override fun getSopData(): List<IsSop> {
+        TODO("Not yet implemented")
+    }
+
+    override fun getDataExportSopData(): List<DataExportSopVo> {
+        TODO("Not yet implemented")
+    }
+
     override fun saveSopPoint(
         selectedSopPint: List<JobTicketGroupDataVo<JobPointVo>>,
         sopId: Long

+ 4 - 0
data/src/main/java/com/grkj/data/logic/impl/network/NetworkWorkstationLogic.kt

@@ -47,6 +47,10 @@ class NetworkWorkstationLogic @Inject constructor()  : BaseLogic(), IWorkstation
         TODO("Not yet implemented")
     }
 
+    override fun getAllWorkstationData(): List<IsWorkstation> {
+        TODO("Not yet implemented")
+    }
+
     override fun deleteWorkstationByWorkstationId(workstationId: Long) {
         TODO("Not yet implemented")
     }

+ 157 - 0
data/src/main/java/com/grkj/data/logic/impl/standard/DataExportLogic.kt

@@ -0,0 +1,157 @@
+package com.grkj.data.logic.impl.standard
+
+import com.grkj.data.enums.DataExportTableEnum
+import com.grkj.data.enums.JobTicketStatusEnum
+import com.grkj.data.logic.BaseLogic
+import com.grkj.data.logic.IDataExportLogic
+import com.grkj.data.logic.IHardwareLogic
+import com.grkj.data.logic.IJobTicketLogic
+import com.grkj.data.logic.IRoleLogic
+import com.grkj.data.logic.ISopLogic
+import com.grkj.data.logic.IWorkstationLogic
+import com.grkj.data.model.vo.WorkstationManageVo
+import com.grkj.data.repository.UserRepository
+import com.grkj.data.utils.ExcelExporter
+import com.grkj.data.utils.ExportSheet
+import com.grkj.data.utils.FileStorageUtils
+import com.grkj.data.utils.toExportSheet
+import com.grkj.shared.utils.i18n.I18nManager
+import com.sik.sikcore.date.TimeUtils
+import com.sik.sikcore.extension.file
+import com.sik.sikcore.extension.isNullOrEmpty
+import java.io.File
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * 标准版数据导出业务
+ */
+@Singleton
+class DataExportLogic @Inject constructor(
+    val userRepository: UserRepository,
+    val roleLogic: IRoleLogic,
+    val workstationLogic: IWorkstationLogic,
+    val hardwareLogic: IHardwareLogic,
+    val jobTicketLogic: IJobTicketLogic,
+    val sopLogic: ISopLogic
+) : BaseLogic(), IDataExportLogic {
+    /**
+     * 导出文件位置
+     */
+    private val destFile
+        get() = FileStorageUtils.getFilePath(
+            "iscs",
+            "data_export_${TimeUtils.nowString("yyyyMMddHHmmss")}.xlsx"
+        ).apply {
+            val destFile = File(this)
+            if (destFile.parentFile?.exists() == false) {
+                destFile.parentFile?.mkdirs()
+            }
+            if (!destFile.exists()) {
+                destFile.createNewFile()
+            }
+        }.file()
+
+    override fun dataExport(dataExportTables: List<DataExportTableEnum>): File? {
+        val exportDataSheet = createExportSheetByDataExportTableEnum(dataExportTables)
+        return ExcelExporter.export(destFile, exportDataSheet)
+    }
+
+    /**
+     * 创建导出表
+     */
+    private fun createExportSheetByDataExportTableEnum(dataExportTables: List<DataExportTableEnum>): List<ExportSheet<out Any>> {
+        return dataExportTables.filter { it != DataExportTableEnum.NONE }.map {
+            when (it) {
+                DataExportTableEnum.USER -> userRepository.getAllUserInfos()
+                    .toExportSheet(
+                        valueMappers = mapOf(
+                            "status" to {
+                                if ("1" == it) I18nManager.t("common_enable") else I18nManager.t(
+                                    "common_disable"
+                                )
+                            }
+                        ))
+
+                DataExportTableEnum.ROLE -> roleLogic.getRoleData()
+                    .toExportSheet(
+                        valueMappers = mapOf(
+                            "status" to {
+                                if ("0" == it) I18nManager.t("common_enable") else I18nManager.t(
+                                    "common_disable"
+                                )
+                            }
+                        ))
+
+                DataExportTableEnum.WORKSTATION -> flattenWorkstations(
+                    workstationLogic.getWorkstationManageData(),
+                    { it.sortedBy { it.orderNum } })
+                    .toExportSheet()
+
+                DataExportTableEnum.POINT -> hardwareLogic.getAllDataExportPointData()
+                    .toExportSheet(
+                        valueMappers = mapOf(
+                            "powerType" to {
+                                I18nManager.t(it.toString().lowercase())
+                            }
+                        ))
+
+                DataExportTableEnum.SOP -> sopLogic.getDataExportSopData()
+                    .toExportSheet()
+
+                DataExportTableEnum.JOB -> jobTicketLogic.getAllJobData()
+                    .toExportSheet(
+                        valueMappers = mapOf(
+                            "ticketStatus" to {
+                                JobTicketStatusEnum.getTicketStatusStr(it.toString())
+                            },
+                            "exStatus" to {
+                                if ("1" == it) I18nManager.t("abnormal") else I18nManager.t(
+                                    "normal"
+                                )
+                            },
+                            "sopName" to {
+                                it.takeIf { !it.isNullOrEmpty() } ?: ""
+                            }
+                        ))
+
+                DataExportTableEnum.LOCKED_POINT -> jobTicketLogic.getAllLockedPointsExportData()
+                    .toExportSheet(
+                        valueMappers = mapOf(
+                            "ticketStatus" to {
+                                JobTicketStatusEnum.getTicketStatusStr(it.toString())
+                            }
+                        ))
+
+                else -> TODO()
+            }
+        }
+    }
+
+    /**
+     * 树转列表
+     */
+    fun flattenWorkstations(
+        roots: List<WorkstationManageVo>,
+        sortChildren: (List<WorkstationManageVo>) -> List<WorkstationManageVo> = { it } // 需要可在这儿按 orderNum/name 排
+    ): List<WorkstationManageVo> {
+        val out = mutableListOf<WorkstationManageVo>()
+
+        fun dfs(node: WorkstationManageVo, level: Int) {
+            node.level = level
+            // 给名字加缩进(可按需改成 "│  " 风格)
+            val prefix = buildString {
+                repeat(level) { append("  ") }              // 两个空格一层
+                if (level > 0) append("├─ ")
+            }
+            node.workstationName = prefix + node.workstationName
+            out += node
+            for (child in sortChildren(node.children)) {
+                dfs(child, level + 1)
+            }
+        }
+
+        for (root in sortChildren(roots)) dfs(root, 0)
+        return out
+    }
+}

+ 67 - 11
data/src/main/java/com/grkj/data/logic/impl/standard/HardwareLogic.kt

@@ -34,6 +34,7 @@ import com.grkj.data.model.vo.RfidTokenManageFilterVo
 import com.grkj.data.logic.BaseLogic
 import com.grkj.data.model.dos.IsIsolationPoint
 import com.grkj.data.model.dos.IsMapPoint
+import com.grkj.data.model.vo.DataExportPointVo
 import com.grkj.data.model.vo.PointToMapVo
 import com.grkj.shared.utils.i18n.I18nManager
 import com.sik.sikcore.data.BeanUtils
@@ -50,7 +51,8 @@ import javax.inject.Singleton
 class HardwareLogic @Inject constructor(
     val hardwareDao: HardwareDao,
     val isolationPointDao: IsolationPointDao,
-    val jobTicketDao: JobTicketDao
+    val jobTicketDao: JobTicketDao,
+    val workstationLogic: WorkstationLogic
 ) : BaseLogic(), IHardwareLogic {
     private val updateLock = Any()
 
@@ -312,7 +314,11 @@ class HardwareLogic @Inject constructor(
         isKey.keyNfc = keyNfc
         isKey.macAddress = keyMacAddress
         isKey.exStatus =
-            CommonDictDataEnum.KEY_STATUS.commonDictRes.find { I18nManager.t(it.dictLabel) == I18nManager.t("normal") }?.dictValue
+            CommonDictDataEnum.KEY_STATUS.commonDictRes.find {
+                I18nManager.t(it.dictLabel) == I18nManager.t(
+                    "normal"
+                )
+            }?.dictValue
         hardwareDao.addKeyInfo(isKey)
     }
 
@@ -322,7 +328,11 @@ class HardwareLogic @Inject constructor(
         isLock.lockCode = "LOCK_${defaultLockCodeSize + 1}"
         isLock.lockNfc = lockNfc
         isLock.exStatus =
-            CommonDictDataEnum.PADLOCK_STATUS.commonDictRes.find { I18nManager.t(it.dictLabel) == I18nManager.t("normal") }?.dictValue
+            CommonDictDataEnum.PADLOCK_STATUS.commonDictRes.find {
+                I18nManager.t(it.dictLabel) == I18nManager.t(
+                    "normal"
+                )
+            }?.dictValue
         hardwareDao.addLockInfo(isLock)
     }
 
@@ -542,7 +552,11 @@ class HardwareLogic @Inject constructor(
         hardwareDao.removeSlotsException(
             row,
             col,
-            CommonDictDataEnum.SLOT_STATUS.commonDictRes.find { I18nManager.t(it.dictLabel) != I18nManager.t("abnormal") }?.dictValue
+            CommonDictDataEnum.SLOT_STATUS.commonDictRes.find {
+                I18nManager.t(it.dictLabel) != I18nManager.t(
+                    "abnormal"
+                )
+            }?.dictValue
         )
     }
 
@@ -567,7 +581,11 @@ class HardwareLogic @Inject constructor(
         hardwareDao.tagSlotsException(
             row,
             col,
-            CommonDictDataEnum.SLOT_STATUS.commonDictRes.find { I18nManager.t(it.dictLabel) == I18nManager.t("abnormal") }?.dictValue,
+            CommonDictDataEnum.SLOT_STATUS.commonDictRes.find {
+                I18nManager.t(it.dictLabel) == I18nManager.t(
+                    "abnormal"
+                )
+            }?.dictValue,
             remark
         )
     }
@@ -575,28 +593,44 @@ class HardwareLogic @Inject constructor(
     override fun tagKeyException(rfid: String?, remark: String) {
         hardwareDao.tagKeyException(
             rfid, remark,
-            CommonDictDataEnum.KEY_STATUS.commonDictRes.find { I18nManager.t(it.dictLabel) == I18nManager.t("abnormal") }?.dictValue
+            CommonDictDataEnum.KEY_STATUS.commonDictRes.find {
+                I18nManager.t(it.dictLabel) == I18nManager.t(
+                    "abnormal"
+                )
+            }?.dictValue
         )
     }
 
     override fun tagLockException(rfid: String?, remark: String) {
         hardwareDao.tagLockException(
             rfid, remark,
-            CommonDictDataEnum.KEY_STATUS.commonDictRes.find { I18nManager.t(it.dictLabel) == I18nManager.t("abnormal") }?.dictValue
+            CommonDictDataEnum.KEY_STATUS.commonDictRes.find {
+                I18nManager.t(it.dictLabel) == I18nManager.t(
+                    "abnormal"
+                )
+            }?.dictValue
         )
     }
 
     override fun removeKeyException(rfid: String?) {
         hardwareDao.removeKeyException(
             rfid,
-            CommonDictDataEnum.KEY_STATUS.commonDictRes.find { I18nManager.t(it.dictLabel) == I18nManager.t("normal") }?.dictValue
+            CommonDictDataEnum.KEY_STATUS.commonDictRes.find {
+                I18nManager.t(it.dictLabel) == I18nManager.t(
+                    "normal"
+                )
+            }?.dictValue
         )
     }
 
     override fun removeLockException(rfid: String?) {
         hardwareDao.removeLockException(
             rfid,
-            CommonDictDataEnum.PADLOCK_STATUS.commonDictRes.find { I18nManager.t(it.dictLabel) == I18nManager.t("normal") }?.dictValue
+            CommonDictDataEnum.PADLOCK_STATUS.commonDictRes.find {
+                I18nManager.t(it.dictLabel) == I18nManager.t(
+                    "normal"
+                )
+            }?.dictValue
         )
     }
 
@@ -626,9 +660,17 @@ class HardwareLogic @Inject constructor(
 
     override fun getAllHardwareCount(): Int {
         val lockStatus =
-            CommonDictDataEnum.PADLOCK_STATUS.commonDictRes.find { I18nManager.t(it.dictLabel) == I18nManager.t("normal") }?.dictValue
+            CommonDictDataEnum.PADLOCK_STATUS.commonDictRes.find {
+                I18nManager.t(it.dictLabel) == I18nManager.t(
+                    "normal"
+                )
+            }?.dictValue
         val keyStatus =
-            CommonDictDataEnum.KEY_STATUS.commonDictRes.find { I18nManager.t(it.dictLabel) == I18nManager.t("normal") }?.dictValue
+            CommonDictDataEnum.KEY_STATUS.commonDictRes.find {
+                I18nManager.t(it.dictLabel) == I18nManager.t(
+                    "normal"
+                )
+            }?.dictValue
         val rfidTokenStatus =
             CommonDictDataEnum.RFID_TOKEN_STATUS.commonDictRes.find {
                 I18nManager.t(it.dictLabel) == I18nManager.t(
@@ -652,4 +694,18 @@ class HardwareLogic @Inject constructor(
     override fun getAllPointCount(overviewWorkstationId: Long?): Int {
         return hardwareDao.getAllPointCount(overviewWorkstationId)
     }
+
+    override fun getAllDataExportPointData(): List<DataExportPointVo> {
+        val allPointData = hardwareDao.getAllPointData()
+        val allDataExportPointVo = BeanUtils.copyList(allPointData, DataExportPointVo::class.java)
+        val allPointRfid = hardwareDao.getAllRfidTokenData()
+        val allWorkstation = workstationLogic.getWorkstationManageData()
+        allDataExportPointVo.forEach { point ->
+            point.pointFunction = point.remark.toString()
+            point.workstationName =
+                allWorkstation.find { it.workstationId == point.workstationId }?.workstationName.toString()
+            point.pointNfc = allPointRfid.find { it.rfidId == point.rfidId }?.rfid ?: ""
+        }
+        return allDataExportPointVo
+    }
 }

+ 38 - 1
data/src/main/java/com/grkj/data/logic/impl/standard/JobTicketLogic.kt

@@ -3,6 +3,7 @@ package com.grkj.data.logic.impl.standard
 import com.grkj.data.check_data.ICheckDataMode
 import com.grkj.data.dao.ExceptionDao
 import com.grkj.data.dao.HardwareDao
+import com.grkj.data.dao.IsSopDao
 import com.grkj.data.dao.IsolationPointDao
 import com.grkj.data.dao.JobTicketDao
 import com.grkj.data.dao.WorkflowStepDao
@@ -17,6 +18,8 @@ import com.grkj.data.enums.StepAction
 import com.grkj.data.enums.TodoStatusEnum
 import com.grkj.data.logic.BaseLogic
 import com.grkj.data.logic.IJobTicketLogic
+import com.grkj.data.logic.ISopLogic
+import com.grkj.data.logic.IWorkstationLogic
 import com.grkj.data.model.dos.IsJobTicket
 import com.grkj.data.model.dos.IsJobTicketGroup
 import com.grkj.data.model.dos.IsJobTicketKey
@@ -29,6 +32,8 @@ 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
+import com.grkj.data.model.vo.DataExportJobVo
+import com.grkj.data.model.vo.DataExportLockedPointVo
 import com.grkj.data.model.vo.IsJobTicketDataVo
 import com.grkj.data.model.vo.IsJobTicketKeyDataVo
 import com.grkj.data.model.vo.IsJobTicketLockDataVo
@@ -55,7 +60,9 @@ class JobTicketLogic @Inject constructor(
     val hardwareDao: HardwareDao,
     val isolationPointDao: IsolationPointDao,
     val workflowStepDao: WorkflowStepDao,
-    val exceptionDao: ExceptionDao
+    val exceptionDao: ExceptionDao,
+    val sopLogic: ISopLogic,
+    val workstationLogic: IWorkstationLogic
 ) : BaseLogic(), IJobTicketLogic {
     override fun createJob(
         selectedPointsData: List<JobTicketGroupDataVo<JobPointVo>>,
@@ -280,6 +287,20 @@ class JobTicketLogic @Inject constructor(
         return jobTicketDao.getAllLockedPointsData()
     }
 
+    override fun getAllLockedPointsExportData(): List<DataExportLockedPointVo> {
+        val allLockedPointsData = jobTicketDao.getAllLockedPointsData()
+        val allLockedPointsExportData =
+            BeanUtils.copyList(allLockedPointsData, DataExportLockedPointVo::class.java)
+        val allWorkstation = workstationLogic.getWorkstationManageData()
+        val allRfidData = hardwareDao.getAllRfidTokenData()
+        allLockedPointsExportData.forEach { point ->
+            allWorkstation.find { it.workstationId == point.workstationId }?.let {
+                point.workstationName = it.workstationName
+            }
+        }
+        return allLockedPointsExportData
+    }
+
     override fun checkSopHasJobInProgress(sopId: Long): Boolean {
         return jobTicketDao.checkSopHasJobInProgress(sopId) > 1
     }
@@ -1381,4 +1402,20 @@ class JobTicketLogic @Inject constructor(
     override fun isUnLockBeforeLock(ticketId: Long): Boolean {
         return jobTicketDao.isUnLockBeforeLock(ticketId)
     }
+
+    override fun getAllJobData(): List<DataExportJobVo> {
+        val allJobData = jobTicketDao.getAllJob()
+        val allDataExportJobVo = BeanUtils.copyList(allJobData, DataExportJobVo::class.java)
+        val allWorkstation = workstationLogic.getAllWorkstationData()
+        val allWorkflowMode = workflowStepDao.getWorkflowModes()
+        val allSop = sopLogic.getSopData()
+        allDataExportJobVo.forEach { ticket ->
+            ticket.workstationName =
+                allWorkstation.find { it.workstationId == ticket.workstationId }?.workstationName.toString()
+            ticket.modeName =
+                allWorkflowMode.find { it.modeId == ticket.modeId }?.modeName.toString()
+            ticket.sopName = allSop.find { it.sopId == ticket.sopId }?.sopName.toString()
+        }
+        return allDataExportJobVo
+    }
 }

+ 24 - 3
data/src/main/java/com/grkj/data/logic/impl/standard/SopLogic.kt

@@ -3,17 +3,19 @@ package com.grkj.data.logic.impl.standard
 import com.grkj.data.dao.IsSopDao
 import com.grkj.data.dao.WorkflowStepDao
 import com.grkj.data.enums.RoleEnum
+import com.grkj.data.logic.BaseLogic
+import com.grkj.data.logic.ISopLogic
+import com.grkj.data.logic.IWorkstationLogic
 import com.grkj.data.model.dos.IsSop
 import com.grkj.data.model.dos.IsSopGroup
 import com.grkj.data.model.dos.IsSopPoints
 import com.grkj.data.model.dos.IsSopUser
 import com.grkj.data.model.dos.IsSopWorkflowStep
+import com.grkj.data.model.vo.DataExportSopVo
 import com.grkj.data.model.vo.JobPointVo
 import com.grkj.data.model.vo.JobTicketGroupDataVo
 import com.grkj.data.model.vo.JobUserVo
 import com.grkj.data.model.vo.SopManageVo
-import com.grkj.data.logic.BaseLogic
-import com.grkj.data.logic.ISopLogic
 import com.sik.sikcore.data.BeanUtils
 import javax.inject.Inject
 import javax.inject.Singleton
@@ -24,7 +26,8 @@ import javax.inject.Singleton
 @Singleton
 class SopLogic @Inject constructor(
     val isSopDao: IsSopDao,
-    val workflowStepDao: WorkflowStepDao
+    val workflowStepDao: WorkflowStepDao,
+    val workstationLogic: IWorkstationLogic
 ) : BaseLogic(), ISopLogic {
 
     override fun saveSop(
@@ -67,6 +70,24 @@ class SopLogic @Inject constructor(
         isSopDao.saveSopPoints(isSopPoints.flatten())
     }
 
+    override fun getDataExportSopData(): List<DataExportSopVo> {
+        val allSop = getSopData()
+        val allDataExportSopVo = BeanUtils.copyList(allSop, DataExportSopVo::class.java)
+        val allWorkstation = workstationLogic.getAllWorkstationData()
+        val allWorkflowMode = workflowStepDao.getWorkflowModes()
+        allDataExportSopVo.forEach { ticket ->
+            ticket.workstationName =
+                allWorkstation.find { it.workstationId == ticket.workstationId }?.workstationName.toString()
+            ticket.modeName =
+                allWorkflowMode.find { it.modeId == ticket.modeId }?.modeName.toString()
+        }
+        return allDataExportSopVo
+    }
+
+    override fun getSopData(): List<IsSop> {
+        return isSopDao.getSopData()
+    }
+
     override fun updateStep(isSopWorkflowStep: List<IsSopWorkflowStep>) {
         isSopDao.updateStep(isSopWorkflowStep)
     }

+ 5 - 0
data/src/main/java/com/grkj/data/logic/impl/standard/WorkstationLogic.kt

@@ -35,6 +35,10 @@ class WorkstationLogic @Inject constructor(val workstationDao: WorkstationDao) :
         workstationDao.updateWorkstationNameByWorkstationId(workstationId, workstationName)
     }
 
+    override fun getAllWorkstationData(): List<IsWorkstation> {
+        return workstationDao.getWorkstationData()
+    }
+
     override fun getWorkstationManageData(): List<WorkstationManageVo> {
         return buildTree(workstationDao.getWorkstationData())
     }
@@ -93,6 +97,7 @@ class WorkstationLogic @Inject constructor(val workstationDao: WorkstationDao) :
             if (node.parentId == null || node.parentId == 0L) {
                 roots += node
             } else {
+                node.parentWorkstationName = map[node.parentId]?.workstationName ?: ""
                 map[node.parentId]?.children?.add(node)
             }
         }

+ 8 - 0
data/src/main/java/com/grkj/data/model/dos/BaseBean.kt

@@ -3,6 +3,7 @@ package com.grkj.data.model.dos
 import androidx.room.ColumnInfo
 import androidx.room.Ignore
 import com.grkj.data.data.MainDomainData
+import com.grkj.data.utils.ExcelIgnore
 import com.sik.sikcore.date.TimeUtils
 import java.io.Serializable
 
@@ -12,36 +13,43 @@ import java.io.Serializable
  * @author ruoyi
  */
 open class BaseBean : Serializable {
+    @ExcelIgnore
     @ColumnInfo("create_by")
     var createBy: String? = MainDomainData.userInfo?.userName
 
+    @ExcelIgnore
     @ColumnInfo("create_time")
     var createTime: String? = TimeUtils.nowString(TimeUtils.DEFAULT_DATE_HOUR_MIN_SEC_FORMAT)
 
     /**
      * 更新者
      */
+    @ExcelIgnore
     @ColumnInfo("update_by")
     var updateBy: String? = MainDomainData.userInfo?.userName
 
     /**
      * 更新时间
      */
+    @ExcelIgnore
     @ColumnInfo("update_time")
     var updateTime: String? = TimeUtils.nowString(TimeUtils.DEFAULT_DATE_HOUR_MIN_SEC_FORMAT)
 
     /**
      * 备注
      */
+    @ExcelIgnore
     @ColumnInfo("remark")
     var remark: String? = null
 
     /**
      * 是否选中
      */
+    @ExcelIgnore
     @Ignore
     var isSelected: Boolean = false
 
+    @ExcelIgnore
     @Ignore
     var paramMap: MutableMap<String?, Any?>? = null
         get() {

+ 8 - 0
data/src/main/java/com/grkj/data/model/dos/BaseStandardBean.kt

@@ -3,6 +3,7 @@ package com.grkj.data.model.dos
 import androidx.room.ColumnInfo
 import androidx.room.Ignore
 import com.grkj.data.data.MainDomainData
+import com.grkj.data.utils.ExcelIgnore
 import com.sik.sikcore.date.TimeUtils
 import java.io.Serializable
 
@@ -12,36 +13,43 @@ import java.io.Serializable
  * @author ruoyi
  */
 open class BaseStandardBean : Serializable {
+    @ExcelIgnore
     @ColumnInfo("creator")
     var creator: String? = MainDomainData.userInfo?.userName
 
+    @ExcelIgnore
     @ColumnInfo("create_time")
     var createTime: String? = TimeUtils.nowString(TimeUtils.DEFAULT_DATE_HOUR_MIN_SEC_FORMAT)
 
     /**
      * 更新者
      */
+    @ExcelIgnore
     @ColumnInfo("updater")
     var updater: String? = MainDomainData.userInfo?.userName
 
     /**
      * 更新时间
      */
+    @ExcelIgnore
     @ColumnInfo("update_time")
     var updateTime: String? = TimeUtils.nowString(TimeUtils.DEFAULT_DATE_HOUR_MIN_SEC_FORMAT)
 
     /**
      * 备注
      */
+    @ExcelIgnore
     @ColumnInfo("remark")
     var remark: String? = null
 
     /**
      * 是否选中
      */
+    @ExcelIgnore
     @Ignore
     var isSelected: Boolean = false
 
+    @ExcelIgnore
     @Ignore
     var paramMap: MutableMap<String?, Any?>? = null
         get() {

+ 19 - 0
data/src/main/java/com/grkj/data/model/dos/IsIsolationPoint.kt

@@ -3,64 +3,83 @@ package com.grkj.data.model.dos
 import androidx.room.ColumnInfo
 import androidx.room.Entity
 import androidx.room.PrimaryKey
+import com.grkj.data.utils.ExcelColumn
+import com.grkj.data.utils.ExcelIgnore
 
 /**
  * 隔离点表
  */
 @Entity(tableName = "is_isolation_point")
 open class IsIsolationPoint : BaseBean() {
+    @ExcelIgnore
     @PrimaryKey(autoGenerate = true)
     @ColumnInfo("point_id")
     var pointId: Long = 0
 
+    @ExcelIgnore
     @ColumnInfo("point_code")
     var pointCode: String = ""
 
+    @ExcelColumn("point_manage_point_name", order = 0)
     @ColumnInfo("point_name")
     var pointName: String = ""
 
     @ColumnInfo("point_type")
     var pointType: String? = null
 
+    @ExcelIgnore
     @ColumnInfo("rfid_id")
     var rfidId: Long? = null
 
+    @ExcelIgnore
     @ColumnInfo("workshop_id")
     var workshopId: Long? = null
 
+    @ExcelIgnore
     @ColumnInfo("workarea_id")
     var workareaId: Long? = null
 
+    @ExcelIgnore
     @ColumnInfo("workstation_id")
     var workstationId: Long? = null
 
+    @ExcelIgnore
     @ColumnInfo("loto_id")
     var lotoId: Long? = null
 
+    @ExcelColumn("point_manage_point_power_type", order = 3)
     @ColumnInfo("power_type")
     var powerType: String? = null
 
+    @ExcelIgnore
     @ColumnInfo("isolation_method")
     var isolationMethod: String? = null
 
+    @ExcelIgnore
     @ColumnInfo("point_icon")
     var pointIcon: String? = null
 
+    @ExcelIgnore
     @ColumnInfo("point_picture")
     var pointPicture: String? = null
 
+    @ExcelIgnore
     @ColumnInfo("lock_type_id")
     var lockTypeId: Long? = null
 
+    @ExcelIgnore
     @ColumnInfo("lockset_type_id")
     var locksetTypeId: Long? = null
 
+    @ExcelIgnore
     @ColumnInfo("switch_status")
     var switchStatus: String? = null
 
+    @ExcelIgnore
     @ColumnInfo("point_serial_number")
     var pointSerialNumber: String? = null
 
+    @field:ExcelIgnore
     @ColumnInfo("del_flag")
     var delFlag: String? = "0"
 }

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

@@ -3,6 +3,8 @@ package com.grkj.data.model.dos
 import androidx.room.ColumnInfo
 import androidx.room.Entity
 import androidx.room.PrimaryKey
+import com.grkj.data.utils.ExcelColumn
+import com.grkj.data.utils.ExcelIgnore
 import com.sik.sikcore.date.TimeUtils
 
 /**
@@ -10,53 +12,69 @@ import com.sik.sikcore.date.TimeUtils
  */
 @Entity(tableName = "is_job_ticket")
 open class IsJobTicket : BaseBean() {
+    @ExcelIgnore
     @PrimaryKey(autoGenerate = true)
     @ColumnInfo("ticket_id")
     var ticketId: Long = 0
 
+    @ExcelIgnore
     @ColumnInfo("ticket_code")
     var ticketCode: String? = null
 
+    @ExcelColumn("ticket_name", order = 0)
     @ColumnInfo("ticket_name")
     var ticketName: String = ""
 
+    @ExcelIgnore
     @ColumnInfo("workshop_id")
     var workshopId: Long? = null
 
+    @ExcelIgnore
     @ColumnInfo("workarea_id")
     var workareaId: Long? = null
 
+    @ExcelIgnore
     @ColumnInfo("workstation_id")
     var workstationId: Long = 0
 
+    @ExcelIgnore
     @ColumnInfo("machinery_id")
     var machineryId: Long? = null
 
+    @ExcelIgnore
     @ColumnInfo("mode_id")
     var modeId: Long = 0
 
+    @ExcelIgnore
     @ColumnInfo("sop_id")
     var sopId: Long? = null
 
+    @ExcelIgnore
     @ColumnInfo("ticket_type")
     var ticketType: String? = null
 
+    @ExcelIgnore
     @ColumnInfo("ticket_content")
     var ticketContent: String? = null
 
     //(0未开始 1待上锁 2进行中 3待解锁 4已解锁 5已结束 6已取消 7进行中)
+    @ExcelColumn("status", order = 6)
     @ColumnInfo("ticket_status")
     var ticketStatus: String? = "0"
 
+    @ExcelColumn("start_time", order = 4)
     @ColumnInfo("ticket_start_time")
     var ticketStartTime: String? = TimeUtils.nowString(TimeUtils.DEFAULT_DATE_HOUR_MIN_SEC_FORMAT)
 
+    @ExcelColumn("end_time", order = 5)
     @ColumnInfo("ticket_end_time")
     var ticketEndTime: String? = null
 
+    @ExcelColumn("exception_type_header")
     @ColumnInfo("ex_status")
     var exStatus: Int?=null
 
+    @ExcelIgnore
     @ColumnInfo("del_flag")
     var delFlag: String? = "0"
 }

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

@@ -3,49 +3,64 @@ package com.grkj.data.model.dos
 import androidx.room.ColumnInfo
 import androidx.room.Entity
 import androidx.room.PrimaryKey
+import com.grkj.data.utils.ExcelColumn
+import com.grkj.data.utils.ExcelIgnore
 
 /**
  * SOP
  */
 @Entity(tableName = "is_sop")
-class IsSop : BaseBean() {
+open class IsSop : BaseBean() {
+    @ExcelIgnore
     @PrimaryKey(autoGenerate = true)
     @ColumnInfo("sop_id")
     var sopId: Long = 0
 
+    @ExcelIgnore
     @ColumnInfo("sop_code")
     var sopCode: String? = null
 
+    @ExcelColumn("create_sop_name", order = 0)
     @ColumnInfo("sop_name")
     var sopName: String? = null
 
+    @ExcelIgnore
     @ColumnInfo("sop_type")
     var sopType: String? = null
 
+    @ExcelIgnore
     @ColumnInfo("mode_id")
     var modeId: Long = 0
 
+    @ExcelIgnore
     @ColumnInfo("workshop_id")
     var workshopId: Long? = null
 
+    @ExcelIgnore
     @ColumnInfo("workarea_id")
     var workareaId: Long? = null
 
+    @ExcelIgnore
     @ColumnInfo("workstation_id")
     var workstationId: Long = 0
 
+    @ExcelIgnore
     @ColumnInfo("machinery_id")
     var machineryId: Long? = null
 
+    @ExcelIgnore
     @ColumnInfo("sop_content")
     var sopContent: String? = null
 
+    @ExcelIgnore
     @ColumnInfo("sop_status")
     var sopStatus: String? = null
 
+    @ExcelIgnore
     @ColumnInfo("sop_index")
     var sopIndex: Long? = null
 
+    @ExcelIgnore
     @ColumnInfo("del_flag")
     var delFlag: String? = "0"
 }

+ 14 - 0
data/src/main/java/com/grkj/data/model/dos/SysRole.kt

@@ -3,6 +3,9 @@ package com.grkj.data.model.dos
 import androidx.room.ColumnInfo
 import androidx.room.Entity
 import androidx.room.PrimaryKey
+import com.grkj.data.utils.ExcelColumn
+import com.grkj.data.utils.ExcelIgnore
+import com.grkj.data.utils.ExcelSheet
 
 
 /**
@@ -10,11 +13,13 @@ import androidx.room.PrimaryKey
  *
  * @author ruoyi
  */
+@ExcelSheet("role")
 @Entity(tableName = "sys_role")
 class SysRole : BaseBean() {
     /**
      * 角色ID
      */
+    @ExcelIgnore
     @PrimaryKey(autoGenerate = true)
     @ColumnInfo("role_id")
     var roleId: Long = 0
@@ -22,50 +27,59 @@ class SysRole : BaseBean() {
     /**
      * 角色名称
      */
+    @ExcelColumn("role_manage_role_name", order = 0)
     @ColumnInfo("role_name")
     var roleName: String = ""
 
     /**
      * 角色权限
      */
+    @ExcelColumn("role_manage_permission_string", order = 1)
     @ColumnInfo("role_key")
     var roleKey: String = ""
 
     /**
      * 角色排序
      */
+    @ExcelIgnore
     @ColumnInfo("role_sort")
     var roleSort: Int = 0
 
     /**
      * 数据范围(1:所有数据权限;2:自定义数据权限;3:本部门数据权限;4:本部门及以下数据权限;5:仅本人数据权限)
      */
+    @ExcelIgnore
     @ColumnInfo("data_scope")
     var dataScope: String? = null
 
     /**
      * 菜单树选择项是否关联显示( 0:父子不互相关联显示 1:父子互相关联显示)
      */
+    @ExcelIgnore
     @ColumnInfo("menu_check_strictly")
     var menuCheckStrictly: Int? = null
 
     /**
      * 部门树选择项是否关联显示(0:父子不互相关联显示 1:父子互相关联显示 )
      */
+    @ExcelIgnore
     @ColumnInfo("dept_check_strictly")
     var deptCheckStrictly: Int? = null
 
     /**
      * 角色状态(0正常 1停用)
      */
+    @ExcelColumn("status", order = 2)
     var status: String = "0"
 
     /**
      * 删除标志(0代表存在 2代表删除)
      */
+    @ExcelIgnore
     @ColumnInfo("del_flag")
     var delFlag: String? = "0"
 
+    @ExcelIgnore
     @ColumnInfo("mars_data_scope")
     var marsDataScope: String? = null
 

+ 23 - 0
data/src/main/java/com/grkj/data/model/dos/SysUserDo.kt

@@ -3,60 +3,83 @@ package com.grkj.data.model.dos
 import androidx.room.ColumnInfo
 import androidx.room.Entity
 import androidx.room.PrimaryKey
+import com.grkj.data.enums.DataExportTableEnum
+import com.grkj.data.utils.ExcelColumn
+import com.grkj.data.utils.ExcelIgnore
+import com.grkj.data.utils.ExcelSheet
 import com.grkj.shared.utils.BCryptUtils
+import com.grkj.shared.utils.i18n.I18nManager
 import com.sik.sikcore.date.TimeUtils
 
+@ExcelSheet(name = "user")
 @Entity(tableName = "sys_user")
 open class SysUserDo : BaseBean() {
+
+    @ExcelIgnore
     @PrimaryKey(autoGenerate = true)
     @ColumnInfo("user_id")
     var userId: Long = 0
 
+    @ExcelIgnore
     @ColumnInfo("dept_id")
     var deptId: Long? = null
 
+    @ExcelColumn("username", order = 1)
     @ColumnInfo("user_name")
     var userName: String = ""
 
+    @ExcelColumn("nickname", order = 2)
     @ColumnInfo("nick_name")
     var nickName: String = ""
 
+    @ExcelIgnore
     @ColumnInfo("user_type")
     var userType: String? = "00"
 
+    @ExcelIgnore
     @ColumnInfo("email")
     var email: String? = null
 
+    @ExcelColumn("phone", order = 3)
     @ColumnInfo("phonenumber")
     var phoneNumber: String? = ""
 
+    @ExcelIgnore
     @ColumnInfo("sex")
     var sex: String? = "0"
 
+    @ExcelIgnore
     @ColumnInfo("avatar")
     var avatar: String? = ""
 
+    @ExcelIgnore
     @ColumnInfo("password")
     var password: String? = BCryptUtils.encryptPassword("123456")
 
+    @ExcelIgnore
     @ColumnInfo("key_code")
     var keyCode: String? = "123456"
 
     /**
      * 状态 0、禁用;1、启用
      */
+    @ExcelColumn("status", order = 4)
     @ColumnInfo("status")
     var status: String? = null
 
+    @ExcelIgnore
     @ColumnInfo("del_flag")
     var delFlag: String? = "0"
 
+    @ExcelIgnore
     @ColumnInfo("login_ip")
     var loginIp: String? = null
 
+    @field:ExcelIgnore
     @ColumnInfo("login_date")
     var loginData: String? = null
 
+    @field:ExcelIgnore
     @ColumnInfo("quick_entrance_config")
     var quickEntranceConfig: String? = null
 

+ 20 - 0
data/src/main/java/com/grkj/data/model/vo/DataExportJobVo.kt

@@ -0,0 +1,20 @@
+package com.grkj.data.model.vo
+
+import com.grkj.data.model.dos.IsJobTicket
+import com.grkj.data.utils.ExcelColumn
+import com.grkj.data.utils.ExcelSheet
+
+@ExcelSheet("job")
+class DataExportJobVo : IsJobTicket() {
+
+    @field:ExcelColumn("workstation_manage_workstation_name", order = 3)
+    var workstationName: String = ""
+
+    @field:ExcelColumn("workflow_name", order = 2)
+    var modeName:String =""
+
+    @field:ExcelColumn("sop", order = 1)
+    var sopName:String =""
+
+
+}

+ 12 - 0
data/src/main/java/com/grkj/data/model/vo/DataExportLockedPointVo.kt

@@ -0,0 +1,12 @@
+package com.grkj.data.model.vo
+
+import com.grkj.data.model.dos.IsIsolationPoint
+import com.grkj.data.utils.ExcelColumn
+import com.grkj.data.utils.ExcelSheet
+
+@ExcelSheet("locked_point")
+class DataExportLockedPointVo : LockedPointVo() {
+
+    @field:ExcelColumn("workstation", order = 2)
+    var workstationName: String = ""
+}

+ 20 - 0
data/src/main/java/com/grkj/data/model/vo/DataExportPointVo.kt

@@ -0,0 +1,20 @@
+package com.grkj.data.model.vo
+
+import com.grkj.data.model.dos.IsIsolationPoint
+import com.grkj.data.utils.ExcelColumn
+import com.grkj.data.utils.ExcelSheet
+
+/**
+ * 数据导出点位数据
+ */
+@ExcelSheet("point")
+class DataExportPointVo : IsIsolationPoint() {
+    @field:ExcelColumn("point_manage_rfid", order = 1)
+    var pointNfc: String = ""
+
+    @field:ExcelColumn("workstation", order = 2)
+    var workstationName: String = ""
+
+    @field:ExcelColumn("point_manage_point_function", order = 4)
+    var pointFunction: String = ""
+}

+ 18 - 0
data/src/main/java/com/grkj/data/model/vo/DataExportSopVo.kt

@@ -0,0 +1,18 @@
+package com.grkj.data.model.vo
+
+import com.grkj.data.model.dos.IsSop
+import com.grkj.data.utils.ExcelColumn
+import com.grkj.data.utils.ExcelSheet
+
+/**
+ * sop导出数据
+ */
+@ExcelSheet("sop")
+class DataExportSopVo: IsSop() {
+
+    @field:ExcelColumn("workstation_manage_workstation_name", order = 1)
+    var workstationName: String = ""
+
+    @field:ExcelColumn("workflow_name", order = 2)
+    var modeName:String =""
+}

+ 28 - 0
data/src/main/java/com/grkj/data/model/vo/DataExportVo.kt

@@ -0,0 +1,28 @@
+package com.grkj.data.model.vo
+
+import com.grkj.data.enums.DataExportTableEnum
+
+/**
+ * 数据导出列表实体
+ */
+class DataExportVo {
+    /**
+     * 数据导出枚举
+     */
+    var dataExportTableEnum: DataExportTableEnum = DataExportTableEnum.NONE
+
+    /**
+     * 表名
+     */
+    var tableName: String = ""
+
+    /**
+     * 最后导出时间
+     */
+    var lastUpdateTime: String = ""
+
+    /**
+     * 是否选中
+     */
+    var isSelected: Boolean = false
+}

+ 12 - 1
data/src/main/java/com/grkj/data/model/vo/LockedPointVo.kt

@@ -2,33 +2,44 @@ package com.grkj.data.model.vo
 
 import androidx.room.ColumnInfo
 import androidx.room.Ignore
+import com.grkj.data.utils.ExcelColumn
+import com.grkj.data.utils.ExcelIgnore
 
 /**
  * 锁定点位数据
  */
-class LockedPointVo {
+open class LockedPointVo {
+
+    @ExcelIgnore
     @ColumnInfo("record_id")
     var recordId: Long = 0
 
+    @ExcelIgnore
     @ColumnInfo("ticket_id")
     var ticketId: Long = 0
 
+    @ExcelIgnore
     @ColumnInfo("point_id")
     var pointId: Long? = null
 
+    @ExcelColumn("point_name_tv", order = 0)
     @ColumnInfo("point_name")
     var pointName: String? = null
 
+    @ExcelColumn("ticket_name", order = 1)
     @ColumnInfo("ticket_name")
     var ticketName: String = ""
 
     //(0未开始 1待上锁 2进行中 3待解锁 4已解锁 5已结束 6已取消 7进行中)
+    @ExcelColumn("job_status", order = 3)
     @ColumnInfo("ticket_status")
     var ticketStatus: String? = "0"
 
+    @ExcelIgnore
     @ColumnInfo("workstation_id")
     var workstationId: Long? = null
 
+    @ExcelIgnore
     @Ignore
     var isSelected: Boolean = false
 }

+ 15 - 0
data/src/main/java/com/grkj/data/model/vo/WorkstationManageVo.kt

@@ -1,25 +1,40 @@
 package com.grkj.data.model.vo
 
 import androidx.room.Ignore
+import com.grkj.data.utils.ExcelColumn
+import com.grkj.data.utils.ExcelIgnore
+import com.grkj.data.utils.ExcelSheet
 
 /**
  * 岗位管理数据树形
  */
+@ExcelSheet("workstation")
 class WorkstationManageVo {
+    @ExcelIgnore
     var workstationId: Long = 0
+    @ExcelColumn("workstation_manage_workstation_name", order = 0)
     var workstationName: String = ""
+    @ExcelColumn("workstation_manage_parent_workstation_name", order = 1)
+    var parentWorkstationName: String = ""
+    @ExcelIgnore
     var parentId: Long? = null
+    @ExcelIgnore
     var ancestors: String? = null
+    @ExcelIgnore
     var orderNum: Int? = null
 
+    @ExcelIgnore
     @Ignore
     var isSelected: Boolean = false
 
     // 新增:树结构 & 层级 & 展开状态,用 @Ignore 标记 Room 不映射
+    @ExcelIgnore
     @Ignore
     var children: MutableList<WorkstationManageVo> = mutableListOf()
+    @ExcelIgnore
     @Ignore
     var level: Int = 0
+    @ExcelIgnore
     @Ignore
     var isExpanded: Boolean = false
 

+ 462 - 0
data/src/main/java/com/grkj/data/utils/ExcelExporter.kt

@@ -0,0 +1,462 @@
+package com.grkj.data.utils
+
+import com.grkj.shared.utils.i18n.I18nManager
+import org.apache.poi.ss.usermodel.*
+import org.apache.poi.xssf.usermodel.XSSFCellStyle
+import org.apache.poi.xssf.usermodel.XSSFWorkbook
+import java.io.File
+import java.io.FileOutputStream
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+import kotlin.math.max
+import kotlin.reflect.KClass
+import kotlin.reflect.KProperty1
+import kotlin.reflect.full.findAnnotation
+import kotlin.reflect.full.hasAnnotation
+import kotlin.reflect.full.memberProperties
+import kotlin.reflect.jvm.isAccessible
+import kotlin.reflect.jvm.javaField
+import kotlin.reflect.jvm.jvmErasure
+
+// ======================= 注解 =======================
+@Target(AnnotationTarget.CLASS)
+@Retention(AnnotationRetention.RUNTIME)
+annotation class ExcelSheet(val name: String)
+
+@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY)
+@Retention(AnnotationRetention.RUNTIME)
+annotation class ExcelColumn(
+    val header: String = "",      // 表头(建议写 i18n key;实参再走 I18nManager.t)
+    val order: Int = 0,           // 越小越靠前
+    val width: Int = -1,          // -1 自动列宽
+    val datePattern: String = ""
+)
+
+@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY)
+@Retention(AnnotationRetention.RUNTIME)
+annotation class ExcelIgnore
+
+// ======================= 模型 & 便捷 API =======================
+data class ExportSheet<T : Any>(
+    val sheetName: String? = null,
+    val data: List<T>,
+    val customHeaders: LinkedHashMap<String, String>? = null,
+    // 可选:按字段名进行值转换(先转再格式化)
+    val valueMappers: Map<String, (Any?) -> Any?> = emptyMap()
+)
+
+inline fun <reified T : Any> List<T>.toExportSheet(
+    sheetNameOverride: String? = null,
+    valueMappers: Map<String, (Any?) -> Any?> = emptyMap()
+): ExportSheet<T> = ExportSheet(sheetNameOverride, this, null, valueMappers)
+
+// ======================= 导出器(Kotlin 反射版) =======================
+object ExcelExporter {
+    private val EXCLUDED_PROP_NAMES = setOf("Companion")
+    private const val USE_AUTO_SIZE = false
+    private const val SCAN_ROWS_FOR_WIDTH = 200 // 扫描前200行估宽
+
+    @JvmStatic
+    fun export(
+        destination: File,
+        sheets: List<ExportSheet<out Any>>,
+        locale: Locale = Locale.getDefault(),
+        autoSizeMaxColumns: Int = 30
+    ): File {
+        require(sheets.isNotEmpty()) { "No sheets to export" }
+
+        val wb = XSSFWorkbook()
+        val styles = Styles(wb, locale)
+
+        for (spec in sheets) {
+            writeOneSheet(wb, styles, spec, autoSizeMaxColumns)
+        }
+
+        destination.parentFile?.let { if (!it.exists()) it.mkdirs() }
+        FileOutputStream(destination).use { wb.write(it) }
+        wb.close()
+        return destination
+    }
+
+    private fun writeOneSheet(
+        wb: XSSFWorkbook,
+        styles: Styles,
+        sheetSpec: ExportSheet<out Any>,
+        autoSizeMaxColumns: Int
+    ) {
+        val data = sheetSpec.data
+        val sheetName = decideSheetName(sheetSpec)
+        val sheet = wb.createSheet(safeSheetName(sheetName))
+
+        val kClass: KClass<out Any>? = data.firstOrNull()?.let { it::class }
+
+        val (orderedProps, headers, widths, formatters) =
+            if (sheetSpec.customHeaders != null) {
+                pickByCustomHeaders(kClass, sheetSpec.customHeaders, styles.locale)
+            } else {
+                buildColumnsFromAnnotationsOrProps(kClass, styles.locale)
+            }
+
+        // Header
+        run {
+            val row = sheet.createRow(0)
+            headers.forEachIndexed { idx, title ->
+                val cell = row.createCell(idx)
+                cell.setCellValue(title)
+                cell.setCellStyle(styles.header)
+            }
+        }
+
+        // Rows
+        data.forEachIndexed { index, item ->
+            val row = sheet.createRow(index + 1)
+            orderedProps.forEachIndexed { c, p ->
+                val cell = row.createCell(c)
+                val raw = getPropertyValue(p, item)
+                val mapped = sheetSpec.valueMappers[p.name]?.invoke(raw) ?: raw
+                val formatted = formatters[c].invoke(mapped)
+                setCellValueCompat(cell, formatted, styles)
+            }
+        }
+
+        // Widths
+        val colCount = headers.size
+        for (i in 0 until colCount) {
+            val w = widths[i]
+            if (w > 0) {
+                sheet.setColumnWidth(i, w * 256)
+                continue
+            }
+            if (USE_AUTO_SIZE) {
+                // Android 下不建议开
+                // sheet.autoSizeColumn(i)
+                // sheet.setColumnWidth(i, max(sheet.getColumnWidth(i), 12 * 256))
+            } else {
+                val maxChars = estimateColumnChars(sheet, i, SCAN_ROWS_FOR_WIDTH)
+                val finalChars = max(12, minOf(maxChars + 2, 60))
+                sheet.setColumnWidth(i, finalChars * 256)
+            }
+        }
+
+        // 冻结表头 & 筛选
+        sheet.createFreezePane(0, 1)
+        sheet.setAutoFilter(org.apache.poi.ss.util.CellRangeAddress(0, 0, 0, colCount - 1))
+    }
+
+    /** 扫描前 N 行单元格内容估算列宽(ASCII 算 1,中文等算 2) */
+    private fun estimateColumnChars(
+        sheet: org.apache.poi.ss.usermodel.Sheet,
+        col: Int,
+        scanRows: Int
+    ): Int {
+        var maxLen = 0
+        val last = minOf(sheet.lastRowNum, scanRows)
+        for (r in 0..last) {
+            val row = sheet.getRow(r) ?: continue
+            val cell = row.getCell(col) ?: continue
+            val text = when (cell.cellType) {
+                CellType.STRING.code -> cell.stringCellValue
+                CellType.NUMERIC.code -> {
+                    if (org.apache.poi.ss.usermodel.DateUtil.isCellDateFormatted(cell))
+                        cell.dateCellValue?.toString() ?: ""
+                    else cell.numericCellValue.toString()
+                }
+                CellType.BOOLEAN.code -> cell.booleanCellValue.toString()
+                CellType.FORMULA.code -> try {
+                    cell.stringCellValue
+                } catch (_: Throwable) {
+                    cell.cellFormula
+                }
+                else -> ""
+            }
+            val len = visualLength(text)
+            if (len > maxLen) maxLen = len
+        }
+        return maxLen
+    }
+
+    /** 估算文本视觉长度:ASCII 算 1,中文/非 ASCII 算 2 */
+    private fun visualLength(s: String?): Int {
+        if (s.isNullOrEmpty()) return 0
+        var n = 0
+        for (ch in s) {
+            n += if (ch.code in 32..126) 1 else 2
+        }
+        return n
+    }
+
+
+    // ----------- 列定义:注解 or 兜底(Kotlin 反射) -----------
+    private fun buildColumnsFromAnnotationsOrProps(
+        kClass: KClass<*>?,
+        locale: Locale
+    ): Quad<List<KProperty1<out Any, *>>, List<String>, List<Int>, List<(Any?) -> Any?>> {
+        if (kClass == null) return Quad(emptyList(), emptyList(), emptyList(), emptyList())
+
+        val all = getAllExportableProps(kClass).toMutableList()
+
+        // 带 ExcelColumn 的优先(order 升序)
+        val annotated = all.filter { it.findExcelColumn() != null }
+            .sortedBy { it.findExcelColumn()!!.order }
+        val unAnnotated = all.filter { it.findExcelColumn() == null }
+
+        val ordered = annotated + unAnnotated
+        val headers = ordered.map { p ->
+            val hdr = p.findExcelColumn()?.header?.takeIf { it.isNotBlank() }
+                ?: prettifyName(p.name)
+            I18nManager.t(hdr)
+        }
+        val widths = ordered.map { p -> p.findExcelColumn()?.width ?: -1 }
+        val formatters = ordered.map { p ->
+            val pattern = p.findExcelColumn()?.datePattern ?: ""
+            buildFormatterForProp(p, pattern, locale)
+        }
+
+        @Suppress("UNCHECKED_CAST")
+        return Quad(ordered as List<KProperty1<out Any, *>>, headers, widths, formatters)
+    }
+
+    // 自定义列(仅导出指定字段顺序)
+    private fun pickByCustomHeaders(
+        kClass: KClass<*>?,
+        headersMap: LinkedHashMap<String, String>,
+        locale: Locale
+    ): Quad<List<KProperty1<out Any, *>>, List<String>, List<Int>, List<(Any?) -> Any?>> {
+        if (kClass == null) return Quad(emptyList(), emptyList(), emptyList(), emptyList())
+
+        val all = getAllExportableProps(kClass)
+        val byName = all.associateBy { it.name }
+
+        val selected = ArrayList<KProperty1<out Any, *>>(headersMap.size)
+        val headers = ArrayList<String>(headersMap.size)
+        val widths = ArrayList<Int>(headersMap.size)
+        val formatters = ArrayList<(Any?) -> Any?>(headersMap.size)
+
+        headersMap.forEach { (propName, headerText) ->
+            val p = byName[propName]
+                ?: error("Property '$propName' not found or not exportable.")
+            selected += p
+            headers += I18nManager.t(headerText)
+
+            val ann = p.findExcelColumn()
+            widths += (ann?.width ?: -1)
+            val pattern = ann?.datePattern ?: "yyyy-MM-dd HH:mm:ss"
+            formatters += buildFormatterForProp(p, pattern, locale)
+        }
+
+        return Quad(selected, headers, widths, formatters)
+    }
+
+    // ----------- 属性枚举 & 过滤(Kotlin 反射)-----------
+    private fun getAllExportableProps(kClass: KClass<*>): List<KProperty1<out Any, *>> {
+        // 包含继承的成员属性
+        val props = kClass.memberProperties
+
+        return props.filter { p ->
+            // 过滤无意义/危险项
+            if (EXCLUDED_PROP_NAMES.contains(p.name)) return@filter false
+            if (p.isSyntheticOrDelegated()) return@filter false
+            if (p.hasExcelIgnore()) return@filter false
+            if (p.hasRoomIgnore()) return@filter false
+            // static 成员不会作为 KProperty1 出现;这里无需额外过滤
+            true
+        }.map { p ->
+            p.also { it.isAccessible = true }
+        }
+    }
+
+    // ----------- 值获取(Kotlin 反射) -----------
+    private fun getPropertyValue(p: KProperty1<out Any, *>, bean: Any): Any? = try {
+        @Suppress("UNCHECKED_CAST")
+        (p as KProperty1<Any, Any?>).get(bean)
+    } catch (_: Throwable) {
+        null
+    }
+
+    // ----------- 单元格写入(兼容 3.17) -----------
+    private fun setCellValueCompat(cell: Cell, value: Any?, styles: Styles) {
+        when (value) {
+            null -> {
+                cell.setCellType(CellType.BLANK)
+                cell.setCellValue("")
+                cell.setCellStyle(styles.body)
+            }
+            is Number -> {
+                cell.setCellValue(value.toDouble())
+                cell.setCellStyle(styles.num)
+            }
+            is Boolean -> {
+                cell.setCellValue(value)
+                cell.setCellStyle(styles.body)
+            }
+            is Date -> {
+                // 为兼容 3.17:写成字符串
+                val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", styles.locale)
+                cell.setCellValue(sdf.format(value))
+                cell.setCellStyle(styles.body)
+            }
+            else -> {
+                cell.setCellValue(value.toString())
+                cell.setCellStyle(styles.body)
+            }
+        }
+    }
+
+    // ----------- 格式化器(基于 KType)-----------
+    private fun buildFormatterForProp(
+        p: KProperty1<out Any, *>,
+        datePattern: String,
+        locale: Locale
+    ): (Any?) -> Any? {
+        val kType = p.returnType
+        val kCls = kType.jvmErasure
+
+        // helper:判定 Number 子类
+        fun isNumberLike(kc: KClass<*>) =
+            Number::class.java.isAssignableFrom(kc.java) ||
+                    kc == Int::class || kc == Long::class ||
+                    kc == Short::class || kc == Byte::class ||
+                    kc == Double::class || kc == Float::class
+
+        return when {
+            // 字段本身是 Date:格式化为字符串
+            (kCls == Date::class) && datePattern.isNotEmpty() -> { v: Any? ->
+                val d = v as? Date
+                val fmt = SimpleDateFormat(datePattern, locale)
+                if (d != null) {
+                    fmt.format(d)
+                }
+            }
+
+            // Long/Int 等时间戳(自动识别秒/毫秒)
+            datePattern.isNotEmpty() && (kCls == Long::class || kCls == Int::class
+                    || kCls == java.lang.Long::class || kCls == java.lang.Integer::class) -> { v: Any? ->
+                val ts: Long? = when (v) {
+                    is Long -> v
+                    is Int -> v.toLong()
+                    else -> null
+                }
+                if (ts == null) null else {
+                    val fmt = SimpleDateFormat(datePattern, locale)
+                    val ms = if (ts in 1_000_000_000L..9_999_999_999L) ts * 1000 else ts
+                    fmt.format(Date(ms))
+                }
+            }
+
+            // 其他类型:原样返回(数值等在 setCellValueCompat 再处理)
+            else -> { v: Any? -> v }
+        }
+    }
+
+    // ----------- 表名 & 注解读取 -----------
+    private fun decideSheetName(spec: ExportSheet<out Any>): String {
+        if (!spec.sheetName.isNullOrBlank()) return I18nManager.t(spec.sheetName)
+        val first = spec.data.firstOrNull() ?: return I18nManager.t("Sheet1")
+        val ann = first::class.findAnnotation<ExcelSheet>()
+        val raw = ann?.name?.takeIf { it.isNotBlank() } ?: first::class.simpleName ?: "Sheet1"
+        return I18nManager.t(raw)
+    }
+
+    // ======================= 样式(全部 setter) =======================
+    private class Styles(wb: XSSFWorkbook, val locale: Locale) {
+        val header: XSSFCellStyle = wb.createCellStyle().apply {
+            setAlignment(HorizontalAlignment.CENTER)
+            setVerticalAlignment(VerticalAlignment.CENTER)
+            setFillPattern(FillPatternType.SOLID_FOREGROUND)
+            setFillForegroundColor(IndexedColors.GREY_25_PERCENT.index)
+            setBorderAll(this, BorderStyle.THIN)
+            setFontCompat(this, wb, 11, true)
+        }
+        val body: XSSFCellStyle = wb.createCellStyle().apply {
+            setVerticalAlignment(VerticalAlignment.CENTER)
+            setWrapText(false)
+            setBorderAll(this, BorderStyle.THIN)
+            setFontCompat(this, wb, 10, false)
+        }
+        val num: XSSFCellStyle = wb.createCellStyle().apply {
+            cloneStyleFrom(body)
+            val fmt = wb.creationHelper.createDataFormat().getFormat("#,##0.########")
+            setDataFormat(fmt)
+        }
+        val date: XSSFCellStyle = wb.createCellStyle().apply {
+            cloneStyleFrom(body)
+            val fmt = wb.creationHelper.createDataFormat().getFormat("yyyy-mm-dd hh:mm:ss")
+            setDataFormat(fmt)
+        }
+    }
+
+    private fun setBorderAll(style: CellStyle, bs: BorderStyle) {
+        style.setBorderTop(bs); style.setBorderBottom(bs)
+        style.setBorderLeft(bs); style.setBorderRight(bs)
+    }
+
+    private fun setFontCompat(style: CellStyle, wb: XSSFWorkbook, sizePt: Int, bold: Boolean) {
+        val font = wb.createFont()
+        font.setFontHeightInPoints(sizePt.toShort())
+        font.setBold(bold)
+        style.setFont(font)
+    }
+
+    // ======================= Utils =======================
+    private fun safeSheetName(input: String): String {
+        var name = input.trim()
+        if (name.isEmpty()) name = "Sheet1"
+        val illegal = charArrayOf('\\', '/', '*', '?', ':', '[', ']')
+        for (ch in illegal) name = name.replace(ch.toString(), "_")
+        if (name.length > 31) name = name.substring(0, 31)
+        return name
+    }
+
+    private fun prettifyName(field: String): String {
+        // userName -> User Name;rfid_code -> Rfid Code
+        val s1 = field.replace('_', ' ')
+        val sb = StringBuilder(s1.length + 4)
+        var prevLower = false
+        for (ch in s1) {
+            if (ch.isUpperCase() && prevLower) sb.append(' ')
+            sb.append(ch)
+            prevLower = ch.isLowerCase()
+        }
+        return sb.toString().split(' ')
+            .filter { it.isNotBlank() }
+            .joinToString(" ") { it.lowercase().replaceFirstChar { c -> c.titlecase() } }
+    }
+
+    private data class Quad<A, B, C, D>(val a: A, val b: B, val c: C, val d: D)
+    private fun <A, B, C, D> Quad<A, B, C, D>.component1() = a
+    private fun <A, B, C, D> Quad<A, B, C, D>.component2() = b
+    private fun <A, B, C, D> Quad<A, B, C, D>.component3() = c
+    private fun <A, B, C, D> Quad<A, B, C, D>.component4() = d
+
+    // ======================= Kotlin 反射扩展 =======================
+    /** 读取 ExcelColumn:兼容 @property 与 @field 用法 */
+    private fun KProperty1<*, *>.findExcelColumn(): ExcelColumn? {
+        return this.findAnnotation()
+            ?: this.javaField?.getAnnotation(ExcelColumn::class.java)
+    }
+
+    /** 是否标记了 ExcelIgnore(兼容 @property 与 @field) */
+    private fun KProperty1<*, *>.hasExcelIgnore(): Boolean {
+        return this.hasAnnotation<ExcelIgnore>() ||
+                (this.javaField?.isAnnotationPresent(ExcelIgnore::class.java) == true)
+    }
+
+    /** 是否标记了 Room 的 Ignore(不强依赖 Room:用名字匹配) */
+    private fun KProperty1<*, *>.hasRoomIgnore(): Boolean {
+        // 直接读注解的全名,避免导入依赖
+        if (this.annotations.any { it.annotationClass.qualifiedName == "androidx.room.Ignore" }) return true
+        val jf = this.javaField
+        if (jf != null && jf.annotations.any { it.annotationClass.qualifiedName == "androidx.room.Ignore" }) return true
+        return false
+    }
+
+    /** 合成/委托属性过滤(含 lateinit backing/委托字段等) */
+    private fun KProperty1<*, *>.isSyntheticOrDelegated(): Boolean {
+        // kotlin 合成名形如 `$xx` / `this$0`;委托常见 `...$delegate`
+        val n = this.name
+        if (n.startsWith("$") || n.endsWith("\$delegate")) return true
+        // 没有后备字段但又不是真正的可读属性的,这里也放行(Kotlin property 均可 get)
+        return false
+    }
+}