Merge remote-tracking branch 'base-version/main' into dev

This commit is contained in:
chenbowen
2025-11-03 14:21:25 +08:00
20 changed files with 580 additions and 11 deletions

View File

@@ -86,6 +86,7 @@ public interface ErrorCodeConstants {
ErrorCode DICT_TYPE_NAME_DUPLICATE = new ErrorCode(1_002_006_003, "已经存在该名字的字典类型");
ErrorCode DICT_TYPE_TYPE_DUPLICATE = new ErrorCode(1_002_006_004, "已经存在该类型的字典类型");
ErrorCode DICT_TYPE_HAS_CHILDREN = new ErrorCode(1_002_006_005, "无法删除,该字典类型还有字典数据");
ErrorCode DICT_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_002_006_100, "导入字典数据不能为空!");
// ========== 字典数据 1-002-007-000 ==========
ErrorCode DICT_DATA_NOT_EXISTS = new ErrorCode(1_002_007_001, "当前字典数据不存在");

View File

@@ -1,11 +1,20 @@
package com.zt.plat.module.system.controller.admin.dict;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.zt.plat.framework.apilog.core.annotation.ApiAccessLog;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.framework.common.pojo.PageParam;
import com.zt.plat.framework.common.pojo.PageResult;
import com.zt.plat.framework.common.util.http.HttpUtils;
import com.zt.plat.framework.common.util.object.BeanUtils;
import com.zt.plat.framework.excel.core.handler.SelectSheetWriteHandler;
import com.zt.plat.framework.excel.core.util.ExcelUtils;
import com.alibaba.excel.converters.longconverter.LongStringConverter;
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import com.zt.plat.module.system.controller.admin.dict.vo.type.DictImportExcelVO;
import com.zt.plat.module.system.controller.admin.dict.vo.type.DictImportRespVO;
import com.zt.plat.module.system.controller.admin.dict.vo.type.DictTypePageReqVO;
import com.zt.plat.module.system.controller.admin.dict.vo.type.DictTypeRespVO;
import com.zt.plat.module.system.controller.admin.dict.vo.type.DictTypeSaveReqVO;
@@ -14,6 +23,7 @@ import com.zt.plat.module.system.dal.dataobject.dict.DictTypeDO;
import com.zt.plat.module.system.service.dict.DictTypeService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
@@ -21,8 +31,10 @@ import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import static com.zt.plat.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
@@ -99,4 +111,45 @@ public class DictTypeController {
BeanUtils.toBean(list, DictTypeRespVO.class));
}
@GetMapping("/get-import-template")
@Operation(summary = "获得字典导入模板")
public void importTemplate(HttpServletResponse response) throws IOException {
List<DictImportExcelVO> samples = Arrays.asList(
DictImportExcelVO.builder()
.dictTypeName("性别").dictType("system_user_sex").dictTypeRemark("系统内置示例")
.label("").value("1").sort(1).colorType("primary").dataRemark("示例数据").build(),
DictImportExcelVO.builder()
.dictTypeName("证件类型").dictType("system_id_card_type").dictTypeRemark("自定义示例")
.label("身份证").value("ID").sort(1).dataRemark("示例数据").build()
);
try (ExcelWriter writer = EasyExcel.write(response.getOutputStream())
.autoCloseStream(false)
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
.registerConverter(new LongStringConverter())
.build()) {
WriteSheet sheet = EasyExcel.writerSheet(0, "字典导入")
.head(DictImportExcelVO.class)
.registerWriteHandler(new SelectSheetWriteHandler(DictImportExcelVO.class))
.build();
writer.write(samples, sheet);
}
response.addHeader("Content-Disposition",
"attachment;filename=" + HttpUtils.encodeUtf8("字典导入模板.xls"));
response.setContentType("application/vnd.ms-excel;charset=UTF-8");
}
@PostMapping("/import")
@Operation(summary = "导入字典")
@Parameters({
@Parameter(name = "file", description = "Excel 文件", required = true)
})
@PreAuthorize("@ss.hasPermission('system:dict:import')")
public CommonResult<DictImportRespVO> importDict(@RequestParam("file") MultipartFile file) throws IOException {
List<DictImportExcelVO> importList = ExcelUtils.read(file, DictImportExcelVO.class, 0);
DictImportRespVO respVO = dictTypeService.importDictList(importList);
return success(respVO);
}
}

View File

@@ -0,0 +1,13 @@
package com.zt.plat.module.system.controller.admin.dict.vo.data;
import com.zt.plat.module.system.controller.admin.dict.vo.type.DictImportExcelVO;
/**
* @deprecated 迁移到单工作表导入模型 {@link DictImportExcelVO}
*/
@Deprecated
public final class DictDataImportExcelVO {
private DictDataImportExcelVO() {
}
}

View File

@@ -0,0 +1,73 @@
package com.zt.plat.module.system.controller.admin.dict.vo.type;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
/**
* 字典导入 Excel VO单行同时包含字典类型与字典数据
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = false)
public class DictImportExcelVO {
/**
* 字典名称
*/
@ExcelProperty("字典名称")
private String dictTypeName;
/**
* 字典类型
*/
@ExcelProperty("字典类型")
private String dictType;
/**
* 字典类型备注
*/
@ExcelProperty("类型备注")
private String dictTypeRemark;
/**
* 字典标签
*/
@ExcelProperty("字典标签")
private String label;
/**
* 字典键值
*/
@ExcelProperty("字典键值")
private String value;
/**
* 排序
*/
@ExcelProperty("排序")
private Integer sort;
/**
* 颜色类型
*/
@ExcelProperty("颜色类型")
private String colorType;
/**
* CSS 样式
*/
@ExcelProperty("CSS 样式")
private String cssClass;
/**
* 字典数据备注
*/
@ExcelProperty("数据备注")
private String dataRemark;
}

View File

@@ -0,0 +1,36 @@
package com.zt.plat.module.system.controller.admin.dict.vo.type;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
import java.util.List;
import java.util.Map;
/**
* 字典导入响应 VO
*/
@Data
@Builder
@Schema(description = "管理后台 - 字典导入 Response VO")
public class DictImportRespVO {
@Schema(description = "创建成功的字典类型名称列表", requiredMode = Schema.RequiredMode.REQUIRED)
private List<String> createDictTypeNames;
@Schema(description = "更新成功的字典类型名称列表", requiredMode = Schema.RequiredMode.REQUIRED)
private List<String> updateDictTypeNames;
@Schema(description = "导入失败的字典类型集合key 为字典名称value 为失败原因", requiredMode = Schema.RequiredMode.REQUIRED)
private Map<String, String> failureDictTypeNames;
@Schema(description = "创建成功的字典数据标识列表", requiredMode = Schema.RequiredMode.REQUIRED)
private List<String> createDictDataKeys;
@Schema(description = "更新成功的字典数据标识列表", requiredMode = Schema.RequiredMode.REQUIRED)
private List<String> updateDictDataKeys;
@Schema(description = "导入失败的字典数据集合key 为字典数据标识value 为失败原因", requiredMode = Schema.RequiredMode.REQUIRED)
private Map<String, String> failureDictDataKeys;
}

View File

@@ -0,0 +1,11 @@
package com.zt.plat.module.system.controller.admin.dict.vo.type;
/**
* @deprecated 保留空壳文件以兼容历史引用,新的导入请使用 {@link DictImportExcelVO}
*/
@Deprecated
public final class DictTypeImportExcelVO {
private DictTypeImportExcelVO() {
}
}

View File

@@ -3,6 +3,8 @@ package com.zt.plat.module.system.service.dict;
import com.zt.plat.framework.common.pojo.PageResult;
import com.zt.plat.module.system.controller.admin.dict.vo.type.DictTypePageReqVO;
import com.zt.plat.module.system.controller.admin.dict.vo.type.DictTypeSaveReqVO;
import com.zt.plat.module.system.controller.admin.dict.vo.type.DictImportExcelVO;
import com.zt.plat.module.system.controller.admin.dict.vo.type.DictImportRespVO;
import com.zt.plat.module.system.dal.dataobject.dict.DictTypeDO;
import java.util.List;
@@ -67,4 +69,12 @@ public interface DictTypeService {
*/
List<DictTypeDO> getDictTypeList();
/**
* 导入字典类型与字典数据(单工作表)
*
* @param importList 导入行列表
* @return 导入结果
*/
DictImportRespVO importDictList(List<DictImportExcelVO> importList);
}

View File

@@ -1,19 +1,36 @@
package com.zt.plat.module.system.service.dict;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.google.common.annotations.VisibleForTesting;
import com.zt.plat.framework.common.enums.CommonStatusEnum;
import com.zt.plat.framework.common.pojo.PageResult;
import com.zt.plat.framework.common.util.collection.CollectionUtils;
import com.zt.plat.framework.common.util.date.LocalDateTimeUtils;
import com.zt.plat.framework.common.util.object.BeanUtils;
import com.zt.plat.module.system.controller.admin.dict.vo.type.DictImportExcelVO;
import com.zt.plat.module.system.controller.admin.dict.vo.type.DictImportRespVO;
import com.zt.plat.module.system.controller.admin.dict.vo.type.DictTypePageReqVO;
import com.zt.plat.module.system.controller.admin.dict.vo.type.DictTypeSaveReqVO;
import com.zt.plat.module.system.controller.admin.dict.vo.data.DictDataSaveReqVO;
import com.zt.plat.module.system.dal.dataobject.dict.DictDataDO;
import com.zt.plat.module.system.dal.dataobject.dict.DictTypeDO;
import com.zt.plat.module.system.dal.mysql.dict.DictTypeMapper;
import com.google.common.annotations.VisibleForTesting;
import org.springframework.stereotype.Service;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import static com.zt.plat.framework.common.exception.util.ServiceExceptionUtil.exception;
import static com.zt.plat.module.system.enums.ErrorCodeConstants.*;
@@ -24,6 +41,7 @@ import static com.zt.plat.module.system.enums.ErrorCodeConstants.*;
* @author ZT
*/
@Service
@Slf4j
public class DictTypeServiceImpl implements DictTypeService {
@Resource
@@ -92,6 +110,128 @@ public class DictTypeServiceImpl implements DictTypeService {
return dictTypeMapper.selectList();
}
@Override
@Transactional(rollbackFor = Exception.class)
public DictImportRespVO importDictList(List<DictImportExcelVO> importList) {
if (CollUtil.isEmpty(importList)) {
throw exception(DICT_IMPORT_LIST_IS_EMPTY);
}
Map<String, DictTypeDO> typeByType = new HashMap<>();
Map<String, DictTypeDO> typeByName = new HashMap<>();
List<DictTypeDO> existingTypes = dictTypeMapper.selectList();
existingTypes.forEach(item -> {
typeByType.put(item.getType(), item);
typeByName.put(item.getName(), item);
});
List<String> createTypeNames = new ArrayList<>();
Map<String, String> failureTypeNames = new LinkedHashMap<>();
List<String> createDataKeys = new ArrayList<>();
Map<String, String> failureDataKeys = new LinkedHashMap<>();
Map<String, Map<String, DictDataDO>> dataCacheByType = new HashMap<>();
Set<String> seenRowKeys = new HashSet<>();
for (DictImportExcelVO row : importList) {
String dictTypeName = trimToNull(row.getDictTypeName());
String dictTypeCode = trimToNull(row.getDictType());
String label = trimToNull(row.getLabel());
String value = trimToNull(row.getValue());
String displayTypeKey = StrUtil.nullToDefault(dictTypeName, StrUtil.nullToDefault(dictTypeCode, "未知字典"));
String displayDataKey = String.format("%s-%s", displayTypeKey, StrUtil.nullToDefault(label, "未知标签"));
if (StrUtil.isEmpty(dictTypeName) || StrUtil.isEmpty(dictTypeCode)) {
failureTypeNames.put(displayTypeKey, "字典名称与字典类型均不能为空");
continue;
}
if (StrUtil.isEmpty(label) || StrUtil.isEmpty(value)) {
failureDataKeys.put(displayDataKey, "字典标签和值不能为空");
continue;
}
String rowKey = dictTypeCode.toLowerCase(Locale.ROOT) + "::" + value.toLowerCase(Locale.ROOT);
if (!seenRowKeys.add(rowKey)) {
failureDataKeys.put(displayDataKey, "Excel 中存在重复的字典数据");
continue;
}
DictTypeDO dictType = typeByType.get(dictTypeCode);
if (dictType == null) {
dictType = typeByName.get(dictTypeName);
}
try {
if (dictType == null) {
DictTypeSaveReqVO typeReq = new DictTypeSaveReqVO();
typeReq.setName(dictTypeName);
typeReq.setType(dictTypeCode);
typeReq.setStatus(CommonStatusEnum.ENABLE.getStatus());
typeReq.setRemark(trimToNull(row.getDictTypeRemark()));
Long id = createDictType(typeReq);
dictType = dictTypeMapper.selectById(id);
if (dictType == null) {
dictType = new DictTypeDO();
dictType.setId(id);
dictType.setName(typeReq.getName());
dictType.setType(typeReq.getType());
dictType.setStatus(typeReq.getStatus());
dictType.setRemark(typeReq.getRemark());
}
typeByType.put(dictType.getType(), dictType);
typeByName.put(dictType.getName(), dictType);
createTypeNames.add(dictType.getName());
}
} catch (Exception ex) {
String message = ex.getMessage();
failureTypeNames.put(displayTypeKey, StrUtil.blankToDefault(message, "导入失败"));
log.warn("Import dict type failed, key={}, message=", displayTypeKey, ex);
continue;
}
try {
Map<String, DictDataDO> cache = dataCacheByType.computeIfAbsent(dictType.getType(),
key -> CollectionUtils.convertMap(dictDataService.getDictDataListByDictType(key), DictDataDO::getValue));
DictDataDO existsData = cache.get(value);
if (existsData != null) {
failureDataKeys.put(displayDataKey, "字典数据已存在,不允许重复导入");
continue;
}
DictDataSaveReqVO dataReq = new DictDataSaveReqVO();
dataReq.setDictType(dictType.getType());
dataReq.setLabel(label);
dataReq.setValue(value);
dataReq.setSort(ObjectUtil.defaultIfNull(row.getSort(), 0));
dataReq.setStatus(CommonStatusEnum.ENABLE.getStatus());
dataReq.setColorType(trimToNull(row.getColorType()));
dataReq.setCssClass(trimToNull(row.getCssClass()));
dataReq.setRemark(trimToNull(row.getDataRemark()));
Long dataId = dictDataService.createDictData(dataReq);
DictDataDO created = dictDataService.getDictData(dataId);
if (created == null) {
created = new DictDataDO();
created.setId(dataId);
created.setDictType(dataReq.getDictType());
created.setLabel(dataReq.getLabel());
created.setValue(dataReq.getValue());
}
cache.put(created.getValue(), created);
createDataKeys.add(displayDataKey);
} catch (Exception ex) {
String message = ex.getMessage();
failureDataKeys.put(displayDataKey, StrUtil.blankToDefault(message, "导入失败"));
log.warn("Import dict data failed, key={}, message=", displayDataKey, ex);
}
}
return DictImportRespVO.builder()
.createDictTypeNames(createTypeNames)
.updateDictTypeNames(List.of())
.failureDictTypeNames(failureTypeNames)
.createDictDataKeys(createDataKeys)
.updateDictDataKeys(List.of())
.failureDictDataKeys(failureDataKeys)
.build();
}
@VisibleForTesting
void validateDictTypeNameUnique(Long id, String name) {
DictTypeDO dictType = dictTypeMapper.selectByName(name);
@@ -137,4 +277,12 @@ public class DictTypeServiceImpl implements DictTypeService {
return dictType;
}
private String trimToNull(String value) {
if (value == null) {
return null;
}
String trim = StrUtil.trim(value);
return StrUtil.isEmpty(trim) ? null : trim;
}
}