|
|
@@ -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
|
|
|
+ }
|
|
|
+}
|