一、项目背景与痛点洞察
在升级菜单系统时,尝试在现有基础上添加新菜单项时,如果直接导入全部菜单数据,系统会提示“重复数据错误”,这让整个升级过程变得不那么顺畅。特别是当产品经理提出了一种包含五层嵌套结构的复杂菜单设计后,手动创建这些菜单显得尤为繁琐,传统的全量导入方式也显得不太合适了。为了克服这些问题,主要是两个方面:一是如何有效避免因重复数据导致的问题;二是怎样更好地管理和呈现复杂的树状菜单结构。
graph TD
A[传统方案] --> B[手工维护效率低]
A --> C[层级越界风险]
A --> D[数据一致性难保证]
为此,采取了几项创新措施来优化流程:
- 动态冲突检测:通过智能算法自动识别并妥善处理可能存在的重复信息。
- 树形结构解析:自动生成菜单间的层级关系图谱,简化了构建过程。
- 事务化操作:采用批量处理技术确保每次更新都能保持数据的一致性和完整性。
二、技术解决方案全景
核心架构设计:
flowchart LR
subgraph 问题域
P1[数据冲突] --> S1[智能去重]
P2[层级维护] --> S2[结构解析]
P3[数据完整] --> S3[事务保障]
end
subgraph 方案层
S1 --> D1[动态冲突检测]
S2 --> D2[树形构建引擎]
S3 --> D3[批处理事务]
end
sequenceDiagram
participant Excel
participant 校验引擎
participant 清洗中心
participant 增量识别
participant 结构构建
participant 数据库
Excel->>校验引擎: 上传原始数据
校验引擎->>清洗中心: 去重/补全
清洗中心->>增量识别: 数据分类
增量识别->>结构构建: 生成树形
结构构建->>数据库: 事务写入
数据库-->>Excel: 反馈结果
三、项目代码
/**
* 菜单配置批量导入处理器(支持增量更新)
* @param excelFile 上传的菜单配置文件
* @param productId 所属产品线标识
*/
@Transactional(rollbackFor = Exception.class)
public String importMenuExcelNew(MultipartFile multipartFile, String productId) throws Exception {
// 1. 读取并校验Excel数据
ExcelImportResult<SysMenuExcelVO> excelImportResult = readAndValidateExcel(multipartFile);
// 2. 处理导入数据 判断数据数量和格式,父级和子级的状态
List<SysMenuExcelVO> processedMenuList = processImportData(excelImportResult);
// 3. 收集并处理错误信息
List<String> errorMessages = collectErrorMessages(processedMenuList);
if (CollectionUtils.isNotEmpty(errorMessages)) {
return String.join("", errorMessages);
}
// 4. 业务校验
performBusinessValidations(processedMenuList, errorMessages);
// 5. 数据分拣(新增/更新)
// 增量操作,把code相同的做更新
List<SysMenuExcelVO> sysMenuListToAdd = new ArrayList<>();
List<SysMenuExcelVO> sysMenuListToUpdate = new ArrayList<>();
User currentUser = getCurrentUser(User.class);
// 不使用原有导入的id 使用新的id
Map<String, String> nameIdsMap = new HashMap<>();
checkExistConflict(processedMenuList, productId, currentUser, sysMenuListToAdd, sysMenuListToUpdate, nameIdsMap);
if (CollectionUtils.isNotEmpty(errorMessages)) {
return String.join("", errorMessages);
}
// 6. 执行数据库操作
executeDatabaseOperations(processedMenuList,sysMenuListToUpdate, sysMenuListToAdd, nameIdsMap, currentUser, productId);
// 7. 处理缓存
// 删除租户菜单树缓存
redisUtil.delByPattern(UserCenterConstant.TENANT_MENU_PERMISSION + "*");
// 刷新缓存对应的产品菜单
sysMenuCacheService.refreshSysMenuByProductIds(productId);
return UserCenterConstant.IMPORT_SUCCESS;
}
// 数据分拣(新增/更新)
private void checkExistConflict(List<SysMenuExcelVO> menuListToImport, String productId, User currentUser, List<SysMenuExcelVO> sysMenuListToAdd, List<SysMenuExcelVO> sysMenuListToUpdate, Map<String, String> nameIdsMap) {
List<SysMenu> sysMenuListDB = this.lambdaQuery()
.eq(SysMenu::getProductId, productId)
.eq(SysMenu::getTenantId,currentUser.getTenantId())
.list();
Map<String, SysMenu> existingUserMap = sysMenuListDB.stream().collect(Collectors.toMap(SysMenu::getButtonCode, Function.identity()));
// 如果code重复,添加到更新列表 不重复添加到新增列表
classify(menuListToImport, existingUserMap, sysMenuListToAdd, sysMenuListToUpdate, nameIdsMap);
}
/**
*增量识别算法
* 分类处理新增/更新用户
*/
private void classify(List<SysMenuExcelVO> menuListToImport,
Map<String, SysMenu> existingUserMap,
List<SysMenuExcelVO> toAdd,
List<SysMenuExcelVO> toUpdate,
Map<String, String> nameIdMap) {
menuListToImport.forEach(menu -> {
SysMenu existing = existingUserMap.get(menu.getButtonCode());
if (existing != null) {
toUpdate.add(menu);
nameIdMap.put(menu.getButtonCode(), existing.getId());
} else {
toAdd.add(menu);
}
});
}
// 智能分片保存
private void partitionAndSave(List<SysMenu> menus) {
int maxBatchSize = UserCenterConstant.MAX_MENUS;
int total = menus.size();
for (int i=0; i<total; i+=maxBatchSize) {
int end = Math.min(i + maxBatchSize, total);
List<SysMenu> batch = menus.subList(i, end);
saveBatch(batch);
// 释放内存(针对超大数据集)
if (total > 1000) {
batch.clear();
}
}
}
树形结构构建
/**
* 处理菜单项ID重构并保持父子关系
* 1. 预生成新ID映射表解决父子引用问题
* 2. 保留原始树形结构的同时替换所有节点ID
* @param menuList 需要处理的菜单项集合,需包含id/parentId字段
* 要求parentId为0表示根节点,其他情况需与已有id对应
*/
private void batchCreateMenus(List<SysMenuExcelVO> processedMenuListList,List<SysMenuExcelVO> creates, User currentUser, String productId) {
// 预生成所有新ID,保持之前的父子关系,解决父子引用问题
Map<String, String> idMap = new HashMap<>();
// 为每个父节点预生成新ID,建立旧父ID到新父ID的映射
menuList.forEach(item -> {
idMap.put(item.getParentId(), IdWorker.getIdStr());
});
// 根节点固定
idMap.put("0", "0");
Date currentDate = new Date();
// 遍历处理所有菜单项,替换父子ID
menuList.stream().forEach(item -> {
// 替换父节点ID为预生成的新ID
item.setParentId(idMap.get(item.getParentId()));
// 处理当前节点ID替换逻辑
if (StrUtil.isNotBlank(idMap.get(item.getId()))) {
// 当前节点是其他节点的父节点时,使用预生成的新ID
item.setId(idMap.get(item.getId()));
} else {
// 叶子节点生成全新ID
item.setId(IdWorker.getIdStr());
}
item.setxxx(xxx);
});
if (CollectionUtil.isNotEmpty(menuList)) {
// 构建菜单层级
List<SysMenu> totalSysMenu = BeanCopyUtil.copyPropertiesOfList(processedMenuListList, SysMenu.class);
// 构造parentIds和level 传入全量数据防止死循环
totalSysMenu = buildMenuTree(totalSysMenu);
Map<String, SysMenu> totalSysMenuMap = totalSysMenu.stream().collect(Collectors.toMap(SysMenu::getButtonCode, item -> item));
// 从全量数据拿到parentIds和level
menuList.forEach(item -> {
if(totalSysMenuMap.containsKey(item.getButtonCode())){
item.setParentIds(totalSysMenuMap.get(item.getButtonCode()).getParentIds());
item.setWlevel(totalSysMenuMap.get(item.getButtonCode()).getWlevel());
}
});
// 智能分片保存
partitionAndSave(menuList);
}
}
void buildMenuTree(List<SysMenu> menus) {
menus.forEach(menu -> {
LinkedList<String> parentChain = new LinkedList<>();
SysMenu current = menu;
while (!"0".equals(current.getParentId())) {
SysMenu parent = findParent(current.getParentId());
parentChain.addFirst(parent.getId());
current = parent;
}
menu.setParentIds(String.join(",", parentChain));
menu.setLevel(parentChain.size());
});
}
四、性能优化实践
- 分片批处理技术
// 智能分片保存
private void partitionAndSave(List<SysMenu> menus) {
int maxBatchSize = UserCenterConstant.MAX_MENUS;
int total = menus.size();
for (int i=0; i<total; i+=maxBatchSize) {
int end = Math.min(i + maxBatchSize, total);
List<SysMenu> batch = menus.subList(i, end);
// 每个批次独立事务
saveBatchInNewTransaction(batch);
}
}
@Transactional(propagation = Propagation.REQUIRES_NEW) // 新开事务
public void saveBatchInNewTransaction(List<SysMenu> batch) {
saveBatch(batch);
}
- id生成
// 辅助方法 ID预生成(保证线程安全)
private synchronized void generateNewId(SysMenu menu) {
menu.setId(IdWorker.getIdStr());
}
- 缓存预热策略
@Async
public void preheatCache(String productId) {
List<SysMenu> menus = loadFromDB(productId);
redisTemplate.opsForValue().set(buildCacheKey(productId),
buildMenuTree(menus),
30, TimeUnit.MINUTES);
}
五、演进方向
- 版本化控制:支持菜单变更历史追溯
- 可视化编排:提供图形化菜单结构编辑器
- 智能预测:基于历史数据的导入策略推荐