feat(qms): 支持检测项导入时物料类别编号和排序号校验及排序逻辑

- 统一检测项导入文件中的物料类别编号格式,并校验物料类别存在性
- 实现导入时图片单元格嵌入解析并上传替换图例字段
- 新增检测项明细排序号字段,支持不传则默认插最前面,已有排序号整体+1
- 排序号超出范围自动修正,重复排序号会抛出业务异常
- 导入时校验同一物料类别内排序号不允许重复,并标记错误信息
- 新增保存检测项明细排序号处理的服务方法,替换原先简单保存调用
- 导入时检测类型和判定类型支持文字(定向/定量,目视/量具)转数字映射
- 导入后根据物料类别编号自动生成检测项编号和名称,按物料类别分组合并导入
- 查询检测项明细时按排序号升序返回
- 导入模板示例数据更新为物料类别编号和示例文字,导入接口支持错误提示文件返回
- 优化检测项导入导出Excel响应设置,提高导入导出用户体验
This commit is contained in:
funny 2026-05-07 09:31:20 +08:00
parent 04109205c8
commit eabaf79d53
12 changed files with 231 additions and 72 deletions

View File

@ -12,11 +12,12 @@ import com.nflg.wms.common.pojo.dto.QmsInspectionItemImportDTO;
import com.nflg.wms.common.pojo.qo.QmsInspectionItemAddQO;
import com.nflg.wms.common.pojo.qo.QmsInspectionItemSearchQO;
import com.nflg.wms.common.pojo.qo.QmsInspectionItemUpdateQO;
import com.nflg.wms.common.pojo.vo.QmsInspectionItemDetailsVO;
import com.nflg.wms.common.pojo.vo.QmsInspectionItemVO;
import com.nflg.wms.common.util.DateTimeUtil;
import com.nflg.wms.common.util.EecExcelUtil;
import com.nflg.wms.repository.entity.QmsQcMaterialCategory;
import com.nflg.wms.repository.service.IQmsInspectionItemService;
import com.nflg.wms.repository.service.IQmsQcMaterialCategoryService;
import com.nflg.wms.starter.BaseController;
import com.nflg.wms.starter.service.FileUploadService;
import jakarta.annotation.Resource;
@ -37,6 +38,10 @@ import java.io.FileInputStream;
import java.io.IOException;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.HashSet;
import java.util.stream.Collectors;
/**
* 检测项管理
@ -48,6 +53,9 @@ public class QmsInspectionItemController extends BaseController {
@Resource
private IQmsInspectionItemService inspectionItemService;
@Resource
private IQmsQcMaterialCategoryService materialCategoryService;
@Resource
private FileUploadService fileUploadService;
@ -131,14 +139,13 @@ public class QmsInspectionItemController extends BaseController {
@GetMapping("template")
public void template(HttpServletResponse response) throws IOException {
// 构造一行示例数据
QmsInspectionItemExportDTO example = new QmsInspectionItemExportDTO();
example.setMaterialTypeId(1001L);
example.setDetectionType(0);
example.setInspectionItemName("示例检测项名称");
example.setInspectionNo("示例检测项编号");
example.setTestStandard("示例检测标准内容");
example.setLegend("示例图例URL可不填");
example.setDeterminationType(0);
QmsInspectionItemImportDTO example = new QmsInspectionItemImportDTO();
example.setCategoryCode("物料类别编号");
example.setSerialNo("数字1、2、3、4");
example.setTestStandard("检测标准内容");
example.setLegend("在此单元格嵌入图片");
example.setDetectionType("定向/定量");
example.setDeterminationType("目视/量具");
EecExcelUtil.export("检测项导入模板", "检测项导入模板", List.of(example), response);
}
@ -148,59 +155,100 @@ public class QmsInspectionItemController extends BaseController {
/**
* 导入检测项
* 校验失败时返回带错误信息的文件URL
* 图例字段支持图片URL
* 图例字段支持单元格嵌入图片
*
* @param file 导入文件
*/
@Transactional
@PostMapping("import")
public ApiResult<String> importFromExcel(@RequestParam("file") MultipartFile file) throws Exception {
List<QmsInspectionItemImportDTO> data = EecExcelUtil.getExcelContext(file.getInputStream(), QmsInspectionItemImportDTO.class);
byte[] fileBytes = file.getBytes();
List<QmsInspectionItemImportDTO> data = EecExcelUtil.getExcelContext(
new ByteArrayInputStream(fileBytes), QmsInspectionItemImportDTO.class);
if (CollectionUtil.isEmpty(data)) {
throw new NflgException(STATE.BusinessError, "导入文件内容为空");
}
// 读取 Excel 里的所有图片
List<Drawings.Picture> pictures = new ExcelReader(new ByteArrayInputStream(fileBytes)).listPictures();
// 2. 读取 Excel 里的所有图片EEC 标准方式
List<Drawings.Picture> pictures = new ExcelReader(file.getInputStream()).listPictures();
// 3. 遍历 DTO遇到 DISPIMG 就上传图片并替换地址
// 遍历 DTO遇到 DISPIMG 就上传图片并替换地址
for (int i = 0; i < data.size(); i++) {
QmsInspectionItemImportDTO dto = data.get(i);
String legend = dto.getLegend();
// 如果是图片公式就取图片上传把地址填回去
if (StrUtil.isNotBlank(legend) && legend.startsWith("=DISPIMG(")) {
if (i < pictures.size()) {
Drawings.Picture pic = pictures.get(i);
FileInputStream fis = new FileInputStream(pic.getLocalPath().toFile());
// 上传图片 获取地址
String imageUrl = fileUploadService.upload(
"image/" + IdUtil.fastUUID() + ".png",
fis,
"image/png"
);
dto.setLegend(imageUrl); // 把地址塞回去
try (FileInputStream fis = new FileInputStream(pic.getLocalPath().toFile())) {
String imageUrl = fileUploadService.upload(
"image/" + IdUtil.fastUUID() + ".png",
fis,
"image/png"
);
dto.setLegend(imageUrl);
}
} else {
dto.setLegend(null);
}
}
}
// 校验必填字段
// 校验必填字段 & 文字转数字
boolean hasError = false;
for (QmsInspectionItemImportDTO dto : data) {
StringBuilder sb = new StringBuilder();
if (dto.getMaterialTypeId() == null) sb.append("物料类别ID不能为空;");
if (dto.getDetectionType() == null) sb.append("检测类型不能为空;");
if (dto.getInspectionItemName() == null || dto.getInspectionItemName().isBlank())
sb.append("检测项名称不能为空;");
if (dto.getInspectionNo() == null || dto.getInspectionNo().isBlank())
sb.append("检测项编号不能为空;");
if (dto.getTestStandard() == null || dto.getTestStandard().isBlank()) sb.append("检测标准不能为空;");
if (dto.getDeterminationType() == null) sb.append("判定类型不能为空;");
if (StrUtil.isBlank(dto.getCategoryCode())) {
sb.append("物料类别编号不能为空;");
} else {
// 校验物料类别编号是否存在
QmsQcMaterialCategory category = materialCategoryService.lambdaQuery()
.eq(QmsQcMaterialCategory::getCategoryCode, dto.getCategoryCode())
.one();
if (category == null) {
sb.append("物料类别编号[" + dto.getCategoryCode() + "]不存在;");
}
}
if (StrUtil.isBlank(dto.getTestStandard())) sb.append("检测标准不能为空;");
if (StrUtil.isBlank(dto.getDetectionType())) {
sb.append("检测类型不能为空;");
} else if (!"定向".equals(dto.getDetectionType()) && !"定量".equals(dto.getDetectionType())
&& !"0".equals(dto.getDetectionType()) && !"1".equals(dto.getDetectionType())) {
sb.append("检测类型须为'定向'或'定量';");
}
if (StrUtil.isBlank(dto.getDeterminationType())) {
sb.append("判定类型不能为空;");
} else if (!"目视".equals(dto.getDeterminationType()) && !"量具".equals(dto.getDeterminationType())
&& !"0".equals(dto.getDeterminationType()) && !"1".equals(dto.getDeterminationType())) {
sb.append("判定类型须为'目视'或'量具';");
}
dto.setError(sb.toString());
if (sb.length() > 0) hasError = true;
if (!sb.isEmpty()) hasError = true;
}
// 校验同一物料类别编号内排序号不重复
Map<String, List<QmsInspectionItemImportDTO>> grouped = data.stream()
.filter(dto -> StrUtil.isNotBlank(dto.getCategoryCode()))
.collect(Collectors.groupingBy(QmsInspectionItemImportDTO::getCategoryCode));
for (List<QmsInspectionItemImportDTO> group : grouped.values()) {
List<String> sortNos = group.stream()
.map(QmsInspectionItemImportDTO::getSerialNo)
.filter(StrUtil::isNotBlank)
.collect(Collectors.toList());
Set<String> unique = new HashSet<>(sortNos);
if (unique.size() < sortNos.size()) {
// 标记重复的行
Set<String> seen = new HashSet<>();
for (QmsInspectionItemImportDTO dto : group) {
if (StrUtil.isNotBlank(dto.getSerialNo())) {
if (!seen.add(dto.getSerialNo().trim())) {
String err = dto.getError() == null ? "" : dto.getError();
dto.setError(err + "同一物料类别内排序号重复;");
hasError = true;
}
}
}
}
}
if (!hasError) {

View File

@ -30,6 +30,8 @@ import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import org.ttzero.excel.entity.ListSheet;
import org.ttzero.excel.entity.Workbook;
import org.ttzero.excel.reader.Drawings;
import org.ttzero.excel.reader.ExcelReader;
@ -168,7 +170,10 @@ public class QmsPdiStatusItemControllerService {
}
statusName.append("检测项");
EecExcelUtil.export(statusName.toString(), statusName.toString(), data, response);
EecExcelUtil.setResponseExcelHeader(response, statusName.toString());
new Workbook()
.addSheet(new ListSheet<>(statusName.toString(), data).setRowHeight(60))
.writeTo(response.getOutputStream());
}
// ========================= 下载导入模板 =========================

View File

@ -10,28 +10,16 @@ import org.ttzero.excel.annotation.ExcelColumn;
public class QmsInspectionItemImportDTO {
/**
* 物料类别ID
* 物料类别编号用户填写 category_code
*/
@ExcelColumn("物料类别ID*")
private Long materialTypeId;
@ExcelColumn("物料类别编号*")
private String categoryCode;
/**
* 检测类型0=定向1=定量
* 检测项序号数字1234...
*/
@ExcelColumn("检测类型(0定向/1定量)*")
private Integer detectionType;
/**
* 检测项名称
*/
@ExcelColumn("检测项名称*")
private String inspectionItemName;
/**
* 检测项编号
*/
@ExcelColumn("检测项编号*")
private String inspectionNo;
@ExcelColumn("检测项序号*")
private String serialNo;
/**
* 检测标准
@ -46,10 +34,16 @@ public class QmsInspectionItemImportDTO {
private String legend;
/**
* 判定类型0=目视1=量具
* 检测类型用户输入"定向"/"定量"后端匹配为0/1
*/
@ExcelColumn("检测类型(0定向/1定量)*")
private String detectionType;
/**
* 判定类型用户输入"目视"/"量具"后端匹配为0/1
*/
@ExcelColumn("判定类型(0目视/1量具)*")
private Integer determinationType;
private String determinationType;
/**
* 错误信息导入校验结果

View File

@ -3,6 +3,7 @@ package com.nflg.wms.common.pojo.dto;
import lombok.Data;
import lombok.experimental.Accessors;
import org.ttzero.excel.annotation.ExcelColumn;
import org.ttzero.excel.annotation.MediaColumn;
import java.time.LocalDateTime;
@ -20,6 +21,7 @@ public class QmsPdiStatusItemExportDTO {
private String inspectionContent;
@ExcelColumn("检测示例图")
@MediaColumn
private String inspectionImage;
@ExcelColumn("状态")

View File

@ -48,6 +48,11 @@ public class QmsInspectionItemAddQO {
*/
@Data
public static class DetailQO {
/**
* 排序号可选不传则默认插到最前面已有记录+1
*/
private Integer sortNo;
/**
* 检测标准必传
*/

View File

@ -16,6 +16,11 @@ public class QmsInspectionItemDetailAddQO {
@NotNull(message = "检测项ID不能为空")
private Long inspectionItemId;
/**
* 排序号可选不传则默认插到最前面已有记录+1
*/
private Integer sortNo;
/**
* 检测标准必传
*/

View File

@ -50,6 +50,11 @@ public class QmsInspectionItemUpdateQO {
*/
@Data
public static class DetailQO {
/**
* 排序号可选
*/
private Integer sortNo;
/**
* 检测标准必传
*/

View File

@ -15,6 +15,11 @@ public class QmsInspectionItemDetailsVO {
*/
private Long inspectionItemId;
/**
* 排序号
*/
private Integer sortNo;
/**
* 检测标准
*/

View File

@ -35,6 +35,11 @@ public class QmsInspectionItemDetails implements Serializable {
*/
private Long inspectionItemId;
/**
* 检测项序号
*/
private Integer sortNo;
/**
* 检测标准
*/

View File

@ -19,4 +19,12 @@ public interface IQmsInspectionItemDetailsService extends IService<QmsInspection
* 批量删除按明细表ID可多个
*/
void deleteByIds(List<Long> ids);
/**
* 保存明细并处理排序号逻辑
* - sortNo为null 默认1已有记录全部+1
* - sortNo已存在 报错
* - sortNo超出max+1 自动修正为max+1
*/
void saveDetailWithSortNo(QmsInspectionItemDetails detail);
}

View File

@ -1,6 +1,8 @@
package com.nflg.wms.repository.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.nflg.wms.common.constant.STATE;
import com.nflg.wms.common.exception.NflgException;
import com.nflg.wms.repository.entity.QmsInspectionItemDetails;
import com.nflg.wms.repository.mapper.QmsInspectionItemDetailsMapper;
import com.nflg.wms.repository.service.IQmsInspectionItemDetailsService;
@ -21,6 +23,7 @@ public class QmsInspectionItemDetailsServiceImpl
public List<QmsInspectionItemDetails> listByItemId(Long inspectionItemId) {
return lambdaQuery()
.eq(QmsInspectionItemDetails::getInspectionItemId, inspectionItemId)
.orderByAsc(QmsInspectionItemDetails::getSortNo)
.list();
}
@ -31,4 +34,42 @@ public class QmsInspectionItemDetailsServiceImpl
removeByIds(ids);
}
}
@Transactional
@Override
public void saveDetailWithSortNo(QmsInspectionItemDetails detail) {
Long itemId = detail.getInspectionItemId();
Integer sortNo = detail.getSortNo();
// 获取当前最大排序号
QmsInspectionItemDetails maxRecord = lambdaQuery()
.eq(QmsInspectionItemDetails::getInspectionItemId, itemId)
.orderByDesc(QmsInspectionItemDetails::getSortNo)
.last("LIMIT 1")
.one();
int maxSortNo = (maxRecord != null && maxRecord.getSortNo() != null) ? maxRecord.getSortNo() : 0;
if (sortNo == null) {
// 未传排序号 插到最前面(1)已有记录全部+1
lambdaUpdate()
.eq(QmsInspectionItemDetails::getInspectionItemId, itemId)
.setSql("sort_no = sort_no + 1")
.update();
detail.setSortNo(1);
} else {
// 检查重复
boolean exists = lambdaQuery()
.eq(QmsInspectionItemDetails::getInspectionItemId, itemId)
.eq(QmsInspectionItemDetails::getSortNo, sortNo)
.exists();
if (exists) {
throw new NflgException(STATE.BusinessError, "排序号[" + sortNo + "]已存在");
}
// 超出范围则修正为 max+1
if (sortNo > maxSortNo + 1) {
detail.setSortNo(maxSortNo + 1);
}
}
save(detail);
}
}

View File

@ -82,10 +82,11 @@ public class QmsInspectionItemServiceImpl extends ServiceImpl<QmsInspectionItemM
for (QmsInspectionItemAddQO.DetailQO detailQO : qo.getDetails()) {
QmsInspectionItemDetails detail = new QmsInspectionItemDetails()
.setInspectionItemId(item.getId())
.setSortNo(detailQO.getSortNo())
.setTestStandard(detailQO.getTestStandard())
.setLegend(detailQO.getLegend())
.setDeterminationType(detailQO.getDeterminationType());
detailsService.save(detail);
detailsService.saveDetailWithSortNo(detail);
}
}
@ -152,10 +153,11 @@ public class QmsInspectionItemServiceImpl extends ServiceImpl<QmsInspectionItemM
for (QmsInspectionItemUpdateQO.DetailQO detailQO : qo.getDetails()) {
QmsInspectionItemDetails detail = new QmsInspectionItemDetails()
.setInspectionItemId(qo.getId())
.setSortNo(detailQO.getSortNo())
.setTestStandard(detailQO.getTestStandard())
.setLegend(detailQO.getLegend())
.setDeterminationType(detailQO.getDeterminationType());
detailsService.save(detail);
detailsService.saveDetailWithSortNo(detail);
}
}
}
@ -215,6 +217,7 @@ public class QmsInspectionItemServiceImpl extends ServiceImpl<QmsInspectionItemM
// 查询明细列表
List<QmsInspectionItemDetailsVO> detailsList = detailsService.lambdaQuery()
.eq(QmsInspectionItemDetails::getInspectionItemId, id)
.orderByAsc(QmsInspectionItemDetails::getSortNo)
.list()
.stream()
.map(this::toDetailsVO)
@ -269,29 +272,40 @@ public class QmsInspectionItemServiceImpl extends ServiceImpl<QmsInspectionItemM
String operator = UserUtil.getUserName();
LocalDateTime now = LocalDateTime.now();
// 检测项编号+名称分组合并为一条主表记录多行合并到明细表
// 物料类别编号分组同一类别合并为一条主表记录多行合并到明细表
Map<String, List<QmsInspectionItemImportDTO>> grouped = dtos.stream()
.filter(dto -> Objects.nonNull(dto.getMaterialTypeId())
&& Objects.nonNull(dto.getDetectionType())
&& StrUtil.isNotBlank(dto.getInspectionItemName())
&& StrUtil.isNotBlank(dto.getInspectionNo())
.filter(dto -> StrUtil.isNotBlank(dto.getCategoryCode())
&& StrUtil.isNotBlank(dto.getTestStandard())
&& Objects.nonNull(dto.getDeterminationType()))
.collect(Collectors.groupingBy(dto -> dto.getInspectionNo() + "|" + dto.getInspectionItemName()));
&& StrUtil.isNotBlank(dto.getDetectionType())
&& StrUtil.isNotBlank(dto.getDeterminationType()))
.collect(Collectors.groupingBy(QmsInspectionItemImportDTO::getCategoryCode));
for (Map.Entry<String, List<QmsInspectionItemImportDTO>> entry : grouped.entrySet()) {
List<QmsInspectionItemImportDTO> list = entry.getValue();
if (list.isEmpty()) continue;
// 取第一行作为主表数据
String categoryCode = entry.getKey();
QmsInspectionItemImportDTO first = list.get(0);
// 查找物料类别
QmsQcMaterialCategory category = materialCategoryService.lambdaQuery()
.eq(QmsQcMaterialCategory::getCategoryCode, categoryCode)
.one();
if (category == null) continue;
// 自动生成编号和名称
String inspectionNo = "CATD_" + categoryCode;
String inspectionItemName = buildCategoryFullPath(category.getId());
// 检测类型文字转数字
Integer detectionType = parseDetectionType(first.getDetectionType());
// 创建主表记录
QmsInspectionItem item = new QmsInspectionItem()
.setMaterialTypeId(first.getMaterialTypeId())
.setDetectionType(first.getDetectionType())
.setInspectionItemName(first.getInspectionItemName())
.setInspectionNo(first.getInspectionNo())
.setMaterialTypeId(category.getId())
.setDetectionType(detectionType)
.setInspectionItemName(inspectionItemName)
.setInspectionNo(inspectionNo)
.setCreateBy(operator)
.setCreateTime(now)
.setUpdateBy(operator)
@ -300,16 +314,37 @@ public class QmsInspectionItemServiceImpl extends ServiceImpl<QmsInspectionItemM
// 所有行作为明细插入
for (QmsInspectionItemImportDTO dto : list) {
Integer determinationType = parseDeterminationType(dto.getDeterminationType());
Integer serialNo = StrUtil.isNotBlank(dto.getSerialNo()) ? Integer.valueOf(dto.getSerialNo().trim()) : null;
QmsInspectionItemDetails detail = new QmsInspectionItemDetails()
.setInspectionItemId(item.getId())
.setSortNo(serialNo)
.setTestStandard(dto.getTestStandard())
.setLegend(dto.getLegend())
.setDeterminationType(dto.getDeterminationType());
detailsService.save(detail);
.setDeterminationType(determinationType);
detailsService.saveDetailWithSortNo(detail);
}
}
}
/**
* 检测类型文字转数字定向0定量1
*/
private Integer parseDetectionType(String text) {
if ("定向".equals(text) || "0".equals(text)) return 0;
if ("定量".equals(text) || "1".equals(text)) return 1;
return null;
}
/**
* 判定类型文字转数字目视0量具1
*/
private Integer parseDeterminationType(String text) {
if ("目视".equals(text) || "0".equals(text)) return 0;
if ("量具".equals(text) || "1".equals(text)) return 1;
return null;
}
// ==================== 私有工具 ====================
private QmsInspectionItemVO toVO(QmsInspectionItem item) {
@ -339,6 +374,7 @@ public class QmsInspectionItemServiceImpl extends ServiceImpl<QmsInspectionItemM
QmsInspectionItemDetailsVO vo = new QmsInspectionItemDetailsVO();
vo.setId(d.getId());
vo.setInspectionItemId(d.getInspectionItemId());
vo.setSortNo(d.getSortNo());
vo.setTestStandard(d.getTestStandard());
vo.setLegend(d.getLegend());
vo.setDeterminationType(d.getDeterminationType());