优化Excel导入功能,处理公式计算并支持特征项动态生成

This commit is contained in:
syruan 2025-12-12 18:53:20 +08:00
parent 7290d3fb0f
commit 14128f47a4
4 changed files with 276 additions and 46 deletions

View File

@ -11,6 +11,7 @@ import com.bonus.common.core.web.page.TableDataInfo;
import com.bonus.common.log.annotation.SysLog;
import com.bonus.common.log.enums.OperaType;
import com.bonus.common.security.utils.SecurityUtils;
import com.bonus.material.devConfig.domain.EquipmentProperty;
import com.bonus.material.devchange.domain.MaDevInfo;
import com.bonus.material.devchange.domain.MaDevInfoXlsx;
import com.bonus.material.device.domain.DevInfo;
@ -22,9 +23,16 @@ import com.bonus.material.device.service.DevInfoService;
import com.bonus.material.device.service.DevMergeService;
import com.bonus.material.utils.CenterExcelUtil;
import com.bonus.material.utils.FolderZipUtil;
import com.bonus.material.utils.MultiSheetExcelUtil;
import com.bonus.material.utils.ReflectUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.poi.ss.usermodel.Sheet;
import org.springframework.web.bind.annotation.*;
@ -46,6 +54,7 @@ import java.util.Map;
*
* @author syruan
*/
@Slf4j
@RestController
@RequestMapping("/order")
@Api(value = "设备信息", tags = "设备管理")
@ -282,8 +291,11 @@ public class DevMergeController extends BaseController {
@SysLog(title = "用户管理", businessType = OperaType.IMPORT, logType = 0, module = "系统管理->用户管理", details = "导入用户信息")
public AjaxResult importData(MultipartFile file, String orderId) throws Exception {
try {
ExcelUtil<EquipmentImportDTO> util = new ExcelUtil<EquipmentImportDTO>(EquipmentImportDTO.class);
List<EquipmentImportDTO> list = util.importExcel(file.getInputStream(), 1);
// 先计算Excel中的公式将公式结果转换为文本值
MultipartFile processedFile = processExcelFormulas(file);
ExcelUtil<EquipmentImportDTO> util = new ExcelUtil<>(EquipmentImportDTO.class);
List<EquipmentImportDTO> list = util.importExcel(processedFile.getInputStream(), 0);
return service.importData(list, orderId);
} catch (Exception e) {
logger.error(e.toString(), e);
@ -292,6 +304,85 @@ public class DevMergeController extends BaseController {
}
/**
* 处理Excel中的公式将公式计算结果转换为文本值
* 这样ExcelUtil就能正确读取公式单元格的值
*/
private MultipartFile processExcelFormulas(MultipartFile file) throws Exception {
try (InputStream inputStream = file.getInputStream();
Workbook workbook = new XSSFWorkbook(inputStream)) {
// 创建公式计算器
FormulaEvaluator evaluator = workbook.getCreationHelper().createFormulaEvaluator();
// 获取第一个Sheet装备信息
Sheet sheet = workbook.getSheetAt(0);
// 遍历所有行
for (Row row : sheet) {
if (row == null) continue;
// 遍历所有单元格
for (Cell cell : row) {
if (cell == null) continue;
// 如果是公式单元格计算公式并将结果转换为文本值
if (cell.getCellType() == CellType.FORMULA) {
try {
// 计算公式
CellValue cellValue = evaluator.evaluate(cell);
// 根据计算结果类型将公式转换为对应的值
switch (cellValue.getCellType()) {
case STRING:
String stringValue = cellValue.getStringValue();
cell.setCellFormula(null); // 移除公式
cell.setCellValue(stringValue); // 设置为文本值
break;
case NUMERIC:
double numericValue = cellValue.getNumberValue();
cell.setCellFormula(null);
cell.setCellValue(numericValue);
break;
case BOOLEAN:
boolean booleanValue = cellValue.getBooleanValue();
cell.setCellFormula(null);
cell.setCellValue(booleanValue);
break;
case BLANK:
case ERROR:
default:
// 空值或错误转换为空字符串
cell.setCellFormula(null);
cell.setCellValue("");
break;
}
} catch (Exception e) {
// 公式计算失败设置为空字符串
logger.warn("公式计算失败,单元格位置:行{} 列{},错误:{}",
row.getRowNum(), cell.getColumnIndex(), e.getMessage());
cell.setCellFormula(null);
cell.setCellValue("");
}
}
}
}
// 将处理后的Workbook写入到字节数组
ByteArrayOutputStream baos = new ByteArrayOutputStream();
workbook.write(baos);
byte[] bytes = baos.toByteArray();
// 创建新的MultipartFile对象
return new org.springframework.mock.web.MockMultipartFile(
file.getName(),
file.getOriginalFilename(),
file.getContentType(),
bytes
);
}
}
/**
* 下载指定空文件夹结构的 ZIP
@ -317,39 +408,37 @@ public class DevMergeController extends BaseController {
}
/**
* 下载导入模板
* 下载导入模板带特征项联动功能
*/
@PostMapping("/template")
public void downloadTemplate(HttpServletResponse response) {
// 1. 查询系统中的专业列表核心动态获取下拉选数据
List<String> professionList = service.listAllProfessionNames();
// 转换为数组工具类需要String[]类型
String[] professionArray = professionList.toArray(new String[0]);
// 1.2 生产厂家列表
List<String> manufacturerList = service.listAllManufacturerNames();
String[] manufacturerArray = manufacturerList.toArray(new String[0]);
// 2. 反射修改@Excel注解的combo属性关键动态注入下拉选值
try {
Field professionField = EquipmentImportDTO.class.getDeclaredField("profession");
Excel excelAnnotation = professionField.getAnnotation(Excel.class);
// 通过反射修改注解的combo属性需借助自定义的注解修改工具或直接复用工具类逻辑
// 方案1使用反射工具修改注解属性推荐
ReflectUtils.setAnnotationValue(excelAnnotation, "combo", professionArray);
// 1. 查询所有装备类型及其特征项
Map<String, List<EquipmentProperty>> typePropertiesMap = service.getAllTypePropertiesMap();
Field manufacturerField = EquipmentImportDTO.class.getDeclaredField("manufacturer");
Excel manufacturerExcel = manufacturerField.getAnnotation(Excel.class);
ReflectUtils.setAnnotationValue(manufacturerExcel, "combo", manufacturerArray);
// 2. 查询装备类型列表用于下拉框
List<String> professionList = service.listAllProfessionNames();
String[] professionArray = professionList.toArray(new String[0]);
// 3. 查询生产厂家列表用于下拉框
List<String> manufacturerList = service.listAllManufacturerNames();
String[] manufacturerArray = manufacturerList.toArray(new String[0]);
// 4. 计数单位列表用于下拉框
String[] unitArray = new String[]{"", "", "", "", ""};
// 5. 使用多Sheet工具类生成模板
MultiSheetExcelUtil.generateMultiSheetTemplate(
response,
"装备信息导入模板",
typePropertiesMap,
professionArray,
manufacturerArray,
unitArray
);
} catch (Exception e) {
throw new RuntimeException("设置专业下拉选失败", e);
log.error("生成模板失败", e);
throw new RuntimeException("生成模板失败: " + e.getMessage(), e);
}
// 3. 调用工具类生成模板自动触发下拉选生成
// 使用 CenterExcelUtil 替代 ExcelUtil确保所有单元格包括空白行都居中对齐
CenterExcelUtil<EquipmentImportDTO> util = new CenterExcelUtil<>(EquipmentImportDTO.class);
// importTemplateExcel专门生成导入模板的方法仅表头+下拉选无数据
util.importTemplateExcel(response, "装备信息导入模板", "装备信息导入模板");
}
}

View File

@ -56,5 +56,61 @@ public class EquipmentImportDTO {
@Excel(name = "采购日期", sort = 11, dateFormat = "yyyy-MM-dd", align = HorizontalAlignment.CENTER)
@JsonFormat(pattern = "yyyy-MM-dd")
private Date purchaseDate;
// ==================== 特征项9组每组2列特征项名称 + 特征值 ====================
@Excel(name = "特征项1", sort = 12, align = HorizontalAlignment.CENTER)
private String propertyName1;
@Excel(name = "特征值1", sort = 13, align = HorizontalAlignment.CENTER)
private String propertyValue1;
@Excel(name = "特征项2", sort = 14, align = HorizontalAlignment.CENTER)
private String propertyName2;
@Excel(name = "特征值2", sort = 15, align = HorizontalAlignment.CENTER)
private String propertyValue2;
@Excel(name = "特征项3", sort = 16, align = HorizontalAlignment.CENTER)
private String propertyName3;
@Excel(name = "特征值3", sort = 17, align = HorizontalAlignment.CENTER)
private String propertyValue3;
@Excel(name = "特征项4", sort = 18, align = HorizontalAlignment.CENTER)
private String propertyName4;
@Excel(name = "特征值4", sort = 19, align = HorizontalAlignment.CENTER)
private String propertyValue4;
@Excel(name = "特征项5", sort = 20, align = HorizontalAlignment.CENTER)
private String propertyName5;
@Excel(name = "特征值5", sort = 21, align = HorizontalAlignment.CENTER)
private String propertyValue5;
@Excel(name = "特征项6", sort = 22, align = HorizontalAlignment.CENTER)
private String propertyName6;
@Excel(name = "特征值6", sort = 23, align = HorizontalAlignment.CENTER)
private String propertyValue6;
@Excel(name = "特征项7", sort = 24, align = HorizontalAlignment.CENTER)
private String propertyName7;
@Excel(name = "特征值7", sort = 25, align = HorizontalAlignment.CENTER)
private String propertyValue7;
@Excel(name = "特征项8", sort = 26, align = HorizontalAlignment.CENTER)
private String propertyName8;
@Excel(name = "特征值8", sort = 27, align = HorizontalAlignment.CENTER)
private String propertyValue8;
@Excel(name = "特征项9", sort = 28, align = HorizontalAlignment.CENTER)
private String propertyName9;
@Excel(name = "特征值9", sort = 29, align = HorizontalAlignment.CENTER)
private String propertyValue9;
}

View File

@ -5,6 +5,7 @@ import com.bonus.common.biz.domain.BmCompanyInfo;
import com.bonus.common.core.web.domain.AjaxResult;
import com.bonus.material.devchange.domain.MaDevInfo;
import com.bonus.material.devchange.domain.MaDevInfoXlsx;
import com.bonus.material.devConfig.domain.EquipmentProperty;
import com.bonus.material.device.domain.DevInfo;
import com.bonus.material.device.domain.EquipmentImportDTO;
import com.bonus.material.device.domain.dto.DevInfoImpDto;
@ -81,4 +82,10 @@ public interface DevMergeService {
void downloadEmptyFolderZip(HttpServletResponse response,String orderId);
AjaxResult uploadAndUnzipMultiDeviceZip(MultipartFile zipFile,String orderId);
/**
* 查询所有装备类型及其特征项
* @return Map<装备类型名称, 特征项列表>
*/
Map<String, List<EquipmentProperty>> getAllTypePropertiesMap();
}

View File

@ -46,6 +46,7 @@ import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipInputStream;
@ -349,8 +350,13 @@ public class DevMergeServiceImpl implements DevMergeService {
public List<MaDevInfo> getDeviceByOrderId(MaDevInfo o) {
List<MaDevInfo> list = devMergeMapper.getDeviceByOrderId(o);
list.forEach(item -> {
log.info("查询装备特征项maId={}", item.getMaId());
List<DevInfoPropertyVo> propertiiesList = mapper.getProperties(item);
log.info("查询到的特征项数量:{}", propertiiesList != null ? propertiiesList.size() : 0);
if (propertiiesList != null && !propertiiesList.isEmpty()) {
propertiiesList.forEach(prop -> {
log.info("特征项name={}, value={}", prop.getPropertyName(), prop.getPropertyValue());
});
item.setPropertyVoList(propertiiesList);
}
item.setAppearanceImages(mapper.getFileList(item.getMaId(), 1));
@ -431,10 +437,20 @@ public class DevMergeServiceImpl implements DevMergeService {
@Transactional(rollbackFor = Exception.class) // 添加事务确保数据一致性
@Override
public AjaxResult importData(List<EquipmentImportDTO> list, String orderId) {
// 1. 基础校验
if (ObjectUtil.isEmpty(list) || list.isEmpty()) {
// 1. 基础校验和过滤null元素Excel空行会被解析为null
if (list == null || list.isEmpty()) {
return AjaxResult.error("表格内没有数据");
}
// 2. 过滤null元素
list = list.stream().filter(Objects::nonNull).collect(Collectors.toList());
// 2.1 过滤装备类目都没选择的数据
list = list.stream().filter(item -> StringUtils.isNotBlank(item.getProfession())).collect(Collectors.toList());
// 3. 再次检查过滤后是否有有效数据
if (list.isEmpty()) {return AjaxResult.error("表格内没有有效数据");}
if (ObjectUtil.isEmpty(orderId)) {
DevMergeVo o = new DevMergeVo();
o.setCreateUser(SecurityUtils.getLoginUser().getSysUser().getNickName());
@ -461,7 +477,6 @@ public class DevMergeServiceImpl implements DevMergeService {
return AjaxResult.error("导入失败,存在以下错误:" + String.join(" ", errorMessages));
}
// 4. 遍历数据进行最终校验和导入
for (int i = 0; i < list.size(); i++) {
EquipmentImportDTO item = list.get(i);
@ -501,18 +516,50 @@ public class DevMergeServiceImpl implements DevMergeService {
Integer insertResult = devMergeMapper.interDevice(maDevInfo);
devInfoMapper.deleteDevInfoProperties(Long.valueOf(maDevInfo.getMaId()));
List<EquipmentProperty> equipmentProperties = equipmentPropertyMapper.selectByType(Long.valueOf(typeId));
if (equipmentProperties != null && !equipmentProperties.isEmpty()) {
List<DevInfoPropertyVo> devInfoPropertyVos = new ArrayList<>();
for (EquipmentProperty equipmentProperty : equipmentProperties) {
DevInfoPropertyVo entity = new DevInfoPropertyVo();
entity.setId(Math.toIntExact(equipmentProperty.getId()));
entity.setPropertyName(equipmentProperty.getPropertyName());
entity.setPropertyValue(null);
devInfoPropertyVos.add(entity);
}
devInfoMapper.insertDevInfoProperties(Long.valueOf(maDevInfo.getMaId()), devInfoPropertyVos);
// 处理特征项数据
List<DevInfoPropertyVo> devInfoPropertyVos = new ArrayList<>();
// 从Excel导入的9组特征项数据中提取有效数据
String[] propertyNames = {
item.getPropertyName1(), item.getPropertyName2(), item.getPropertyName3(),
item.getPropertyName4(), item.getPropertyName5(), item.getPropertyName6(),
item.getPropertyName7(), item.getPropertyName8(), item.getPropertyName9()
};
String[] propertyValues = {
item.getPropertyValue1(), item.getPropertyValue2(), item.getPropertyValue3(),
item.getPropertyValue4(), item.getPropertyValue5(), item.getPropertyValue6(),
item.getPropertyValue7(), item.getPropertyValue8(), item.getPropertyValue9()
};
// 遍历9组特征项只保存有效的特征项名称和特征值都不为空
for (int j = 0; j < 9; j++) {
String propertyName = propertyNames[j];
String propertyValue = propertyValues[j];
log.info("第{}行,特征项{}name={}, value={}", i + 1, j + 1, propertyName, propertyValue);
// 只有当特征项名称和特征值都不为空时才保存
if (propertyName != null && !propertyName.trim().isEmpty() && propertyValue != null && !propertyValue.trim().isEmpty()) {
DevInfoPropertyVo entity = new DevInfoPropertyVo();
entity.setPropertyName(propertyName.trim());
entity.setPropertyValue(propertyValue.trim());
// 注意这里不设置id不需要关联ma_type_properties表的id
devInfoPropertyVos.add(entity);
log.info("第{}行,特征项{}有效,已添加到列表", i + 1, j + 1);
}
}
log.info("第{}行,共提取到{}个有效特征项", i + 1, devInfoPropertyVos.size());
// 如果有特征项数据则保存
if (!devInfoPropertyVos.isEmpty()) {
log.info("开始保存特征项maId={}, 数量={}", maDevInfo.getMaId(), devInfoPropertyVos.size());
devInfoMapper.insertDevInfoProperties(Long.valueOf(maDevInfo.getMaId()), devInfoPropertyVos);
log.info("特征项保存成功");
} else {
log.info("第{}行没有有效的特征项数据,跳过保存", i + 1);
}
if (insertResult > 0) {
@ -521,15 +568,21 @@ public class DevMergeServiceImpl implements DevMergeService {
maDevQc.setQcCode(maDevInfo.getCode());
maDevQc.setCreateBy(String.valueOf(SecurityUtils.getUserId()));
maDevQc.setCreateTime(DateUtils.getNowDate());
maDevQc.setQcCom(Optional.ofNullable(SecurityUtils.getLoginUser().getSysUser().getCompanyId())
.orElse(SecurityUtils.getLoginUser().getSysUser().getDeptId()).toString());
maDevQc.setQcCom(Optional.ofNullable(SecurityUtils.getLoginUser().getSysUser().getCompanyId()).orElse(SecurityUtils.getLoginUser().getSysUser().getDeptId()).toString());
maDevQc.setNextCheckTime(maDevInfo.getNextMaintenanceDate());
qcMapper.insertDevQc(maDevQc);
devMergeMapper.insertOrderDevReal(orderId, Long.valueOf(maDevInfo.getMaId()));
// 关联订单仅当orderId有效时
if (orderId != null && !orderId.trim().isEmpty() && !"undefined".equalsIgnoreCase(orderId.trim())
&& !"null".equalsIgnoreCase(orderId.trim())) {
try {
devMergeMapper.insertOrderDevReal(orderId, Long.valueOf(maDevInfo.getMaId()));
} catch (NumberFormatException e) {
log.warn("订单ID格式无效跳过关联订单orderId={}", orderId);
}
}
}
} catch (Exception e) {
// 记录导入异常方便排查
log.error("导入第{}行数据失败:{}", i + 1, e.getMessage(), e);
errorMessages.add("" + (i + 1) + "行导入失败:系统错误");
}
@ -1066,4 +1119,29 @@ public class DevMergeServiceImpl implements DevMergeService {
throw new IllegalArgumentException("不支持的子目录:" + subFolder);
}
}
@Override
public Map<String, List<EquipmentProperty>> getAllTypePropertiesMap() {
// 1. 查询所有装备类型名称
List<String> professionList = devMergeMapper.listAllProfessionNames();
// 2. 为每个装备类型查询其特征项
Map<String, List<EquipmentProperty>> typePropertiesMap = new LinkedHashMap<>();
for (String profession : professionList) {
// 根据装备类型名称获取typeId
Integer typeId = devMergeMapper.getTypeId(profession);
if (typeId != null) {
// 查询该类型的特征项
List<EquipmentProperty> properties = equipmentPropertyMapper.selectByTypeId(typeId.longValue());
typePropertiesMap.put(profession, properties != null ? properties : new ArrayList<>());
} else {
// 如果没有typeId放入空列表
typePropertiesMap.put(profession, new ArrayList<>());
}
}
return typePropertiesMap;
}
}