1. 前言
最近做了一個比較有趣的需求。需要把樹結構的目錄通過Excel的方式導入到系統中,並且該目錄層級可以是多級且不確定的。這可能是一個常見又不太常見的需求,一般目錄都是在界面上操作創建,或者是系統初始化生成。很少在系統使用一段時間后還有導入新目錄的需求。
2. 需求分析
2.1 需求難點
這個需求最大的難點就是如何找到父級節點。包括
1)如何讓一個Excel表格實現不確定目錄層級功能?
2)如何讓子個節點能正確找到其父級節點?
3)如何在遍歷完一個分枝后,還能從根節點繼續遍歷另外一個分枝?
2.2 解決難點
1)我們可以將目錄層級作為用戶輸入項,由用戶決定該數據處於第幾層目錄。解決目錄層級不確定的需求。
2)我們可以用樹節點深度遍歷的思想,遍歷一個個節點,使其找到其父節點。
3)我們同樣可以用深度遍歷的思想再結合先進后出操作,重新找回之前的根節點。
2.3 表格設計
我們可以用Level作為目錄所在層級,一級目錄的Level就是1,同理N級目錄的Level就是N。且數據從上至下可以形成一個完整樹分枝。
表格設計如下:
| 分類名稱 | 級別Level | 其他字段 |
|---|---|---|
| A棟 | 1 | |
| A棟-1樓 | 2 | |
| B棟 | 1 | |
| B棟-1樓 | 2 | |
| B棟-1樓-A區 | 3 | |
| B棟-2樓 | 2 | |
| B棟-2樓-A區 | 3 | |
| B棟-2樓-B區 | 3 |
從表格中,我們應該可以得出以下結論:
1)A棟和B棟屬於一級目錄
2)A棟有一個子目錄,A棟-1樓
3)B棟有兩個子目錄,分別是:B棟-1樓、B棟-2樓
4)B棟-1樓有一個子目錄,B棟-1樓-A區
5)B棟-2樓有兩個子目錄,分別是:B棟-2樓-A區、B棟-2樓-B區
3. 功能實現
我們對需求做了簡單的分析,現在就用代碼來實現。從易到難,從一個分枝再到多個分枝來實現。
3.1 一個分枝
一個分枝的Level排序應該是:1-2-3-N
這種情況是最簡單的,孤零零的一條直線。其父節點就是當前節點的上一個元素。
偽代碼如下:
var categoryPathStack = mutableListOf<EquipmentCategory>()
for (i in sheet.firstRowNum..sheet.lastRowNum) {
val categoryName = row.getCell(0).stringCellValue
val categoryLevel = row.getCell(1).stringCellValue.toInt()
var parentCategory: EquipmentCategory? = null
if (categoryLevel > 1) {
parentCategory = categoryPathStack.last()
}
// todo save or update
categoryPathStack.add(equipmentCategory)
}
3.2 一個分枝多個樹葉
一個分支多個樹葉的Level排序應該是:1-2-3-3-3-3
這種情況稍微復雜了一點,如果只是獲取當前節點的上一個元素是很難找到其父級節點的。我們需要把同一層的兄弟節點都剔除掉。
偽代碼如下:
var categoryPathStack = mutableListOf<EquipmentCategory>()
for (i in sheet.firstRowNum..sheet.lastRowNum) {
val categoryName = row.getCell(0).stringCellValue
val categoryLevel = row.getCell(1).stringCellValue.toInt()
var parentCategory: EquipmentCategory? = null
// 將集合中大於或等於當前層級的數據剔除掉
while (categoryPathStack.isNotEmpty() && categoryPathStack.last().level >= categoryLevel) {
categoryPathStack = categoryPathStack.subList(0, categoryPathStack.size-1).toMutableList()
}
if (categoryLevel > 1) {
parentCategory = categoryPathStack.last()
}
// todo save or update
categoryPathStack.add(equipmentCategory)
}
3.3 多個分枝多個樹葉
多個分支多個樹葉的Level排序應該是:1-2-3-3-3-3-2-3-1-2-3
這種場景依然可以用一個分支多個樹葉的代碼實現,而后面來的1就像一個分割線,將前面先進來的數據隔離開。
4. 代碼事例
4.1 目錄實體結構
目錄實體添加臨時字段level方便邏輯判斷。字段code是方便后期通過code作為StartingWith的查詢條件,從而減少遞歸查詢所有子級目錄帶來的性能損耗。code的生成規則是:父節點code拼接當前節點id,
class Category: AuditModel() {
var name: String? = null
var description: String? = null
var isLeaf: Boolean = true
var parentId: String? = null
@Column(columnDefinition = "TEXT")
var code: String? = null
@Transient
var level: Int = 0
}
4.2 Excel導入代碼
以下只是刪減過后的代碼,具體業務場景會有具體的邏輯代碼。
@Transactional
fun importCategoryData(file: MultipartFile, request: HttpServletRequest): OperateStatus {
// fileUtil.getExcelWorkbook 只是簡單封裝的讀取excel方法
val work = fileUtil.getExcelWorkbook(file.inputStream, file.originalFilename!!)
// todo 清空舊數據
val sheet: Sheet = work.getSheetAt(0)
var categoryPathStack = mutableListOf<Category>()
for (i in sheet.firstRowNum..sheet.lastRowNum) {
val row = sheet.getRow(i)
if (row == null || row.rowNum == 0) {
continue
}
// todo 數據校驗
val categoryName = row.getCell(0).stringCellValue
val categoryLevel = row.getCell(1).stringCellValue.toInt()
var parentCategory: Category? = null
while (categoryPathStack.isNotEmpty() && categoryPathStack.last().level >= categoryLevel) {
categoryPathStack = categoryPathStack.subList(0, categoryPathStack.size-1).toMutableList()
}
if (categoryLevel > 1) {
parentCategory = categoryPathStack.last()
}
var category = Category()
category.name = categoryName
category.parentId = parentCategory?.id
category = categoryRepository.save(category)
if (parentCategory == null) {
category.code = category.id
} else {
category.code = "${parentCategory.code}-${category.id}"
category.isLeaf = true
parentCategory.isLeaf = false
categoryRepository.save(parentCategory)
}
categoryRepository.save(category)
category.level = categoryLevel
categoryPathStack.add(category)
}
work.close()
return OperateStatus("Import Category Success")
}
文章到這里就結束了,感謝觀看。ITDragon博客
