物料导入繁体解析

This commit is contained in:
秦鸿展
2026-05-29 10:02:26 +08:00
parent a7f9c5f3a9
commit 6b58811f4c
4 changed files with 295 additions and 51 deletions
@@ -7,7 +7,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.mhd.common.core.domain.po.UserPo;
import com.mhd.common.core.exception.ServiceException;
import com.mhd.common.core.enums.DictCode;
import com.mhd.common.core.utils.poi.ExcelUtil;
import com.mhd.wms.util.ZhTextNormalizeUtil;
import com.mhd.common.core.web.domain.AjaxResult;
import com.mhd.common.core.web.page.TableDataInfo;
import com.mhd.common.security.utils.SecurityUtils;
@@ -42,6 +42,7 @@ import com.mhd.wms.domain.materialWarehouseControl.repository.po.MaterialWarehou
import com.mhd.wms.domain.materialWarehouseControl.repository.todo.MaterialWarehouseControlDO;
import com.mhd.wms.domain.materialWarehouseControl.service.MaterialWarehouseControlDomainService;
import com.mhd.wms.interfaces.dto.materialBaseInfo.MaterialBaseInfoDTO;
import com.mhd.wms.util.MaterialBaseInfoImportExcelUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
@@ -54,6 +55,7 @@ import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
@@ -398,8 +400,7 @@ public class MaterialBaseInfoApplicationService {
final List<MaterialBaseInfoDTO> rows;
try (InputStream is = file.getInputStream()) {
ExcelUtil<MaterialBaseInfoDTO> util = new ExcelUtil<>(MaterialBaseInfoDTO.class);
rows = util.importExcel(is);
rows = MaterialBaseInfoImportExcelUtil.importRows(is);
} catch (Exception e) {
throw new ServiceException("导入模板解析失败:" + e.getMessage());
}
@@ -434,10 +435,10 @@ public class MaterialBaseInfoApplicationService {
continue;
}
if (row.getOrganizationId() == null && loginOrgId != null) {
if (!isValidOrgId(row.getOrganizationId()) && isValidOrgId(loginOrgId)) {
row.setOrganizationId(loginOrgId);
}
if (row.getTopOrganizationId() == null && loginTopOrgId != null) {
if (row.getTopOrganizationId() == null && isValidOrgId(loginTopOrgId)) {
row.setTopOrganizationId(loginTopOrgId);
}
if (row.getNullify() == null) {
@@ -451,6 +452,9 @@ public class MaterialBaseInfoApplicationService {
}
// Excel 表头错位或重复时,HS 可能被读入「物料条形码」列;与 HS 完全相同时视为误填,保留 hsCode、清空条码
normalizeImportBarCodeWhenDuplicateOfHs(row);
// 先解析货主并补全组织,再做重复/存在性校验(组织维度依赖 organizationId
fillIdsFromExistingMasterData(row);
fillOrganizationFromShipperIfNeeded(row);
fillOrganizationName(row, loginUser);
String shipperCode = StringUtils.defaultString(row.getShipperCode(), "");
@@ -473,8 +477,6 @@ public class MaterialBaseInfoApplicationService {
errors.append("").append(rowNum).append("行:物料已存在(组织+货主编码+物料编码);");
continue;
}
// 根据导入的名称/编码回填现有ID,避免要求模板传ID
fillIdsFromExistingMasterData(row);
fillClassifyTypeAndUnit(row);
Object result = systemServiceFeign.getPackList(null, row.getPackName(), row.getOrganizationId());
@@ -551,23 +553,9 @@ public class MaterialBaseInfoApplicationService {
if (StringUtils.isBlank(row.getPackName()) && StringUtils.isNotBlank(row.getPackImport())) {
row.setPackName(row.getPackImport().trim());
}
// 货主:先按本组织已有物料反查,再用用户中心按「货主编码/货主名称」解析 userId(与入库单 import 一致;纯数字也可作名称,例如「1449」
// 货主:先按本组织已有物料反查,再用用户中心按「货主编码/货主名称」解析 userId(支持繁体名称
if (row.getShipperId() == null && (StringUtils.isNotBlank(row.getShipperCode()) || StringUtils.isNotBlank(row.getShipperName()))) {
MaterialBaseInfo matched = materialBaseInfoService.getOne(new LambdaQueryWrapper<MaterialBaseInfo>()
.eq(MaterialBaseInfo::getDelFlag, 1)
.eq(MaterialBaseInfo::getOrganizationId, row.getOrganizationId())
.and(w -> {
if (StringUtils.isNotBlank(row.getShipperCode())) {
w.eq(MaterialBaseInfo::getShipperCode, row.getShipperCode());
}
if (StringUtils.isNotBlank(row.getShipperName())) {
if (StringUtils.isNotBlank(row.getShipperCode())) {
w.or();
}
w.eq(MaterialBaseInfo::getShipperName, row.getShipperName());
}
})
.last("limit 1"));
MaterialBaseInfo matched = findExistingMaterialByShipper(row);
if (matched != null) {
row.setShipperId(matched.getShipperId());
if (StringUtils.isBlank(row.getShipperCode())) {
@@ -642,6 +630,38 @@ public class MaterialBaseInfoApplicationService {
}
}
/**
* 按货主编码/名称在本组织已有物料中反查(名称支持简繁体归一化匹配)。
*/
private MaterialBaseInfo findExistingMaterialByShipper(MaterialBaseInfoDTO row) {
if (row.getOrganizationId() == null) {
return null;
}
if (StringUtils.isNotBlank(row.getShipperCode())) {
MaterialBaseInfo byCode = materialBaseInfoService.getOne(new LambdaQueryWrapper<MaterialBaseInfo>()
.eq(MaterialBaseInfo::getDelFlag, 1)
.eq(MaterialBaseInfo::getOrganizationId, row.getOrganizationId())
.eq(MaterialBaseInfo::getShipperCode, row.getShipperCode().trim())
.last("limit 1"));
if (byCode != null) {
return byCode;
}
}
if (StringUtils.isNotBlank(row.getShipperName())) {
for (String nameVariant : ZhTextNormalizeUtil.nameLookupVariants(row.getShipperName())) {
MaterialBaseInfo byName = materialBaseInfoService.getOne(new LambdaQueryWrapper<MaterialBaseInfo>()
.eq(MaterialBaseInfo::getDelFlag, 1)
.eq(MaterialBaseInfo::getOrganizationId, row.getOrganizationId())
.eq(MaterialBaseInfo::getShipperName, nameVariant)
.last("limit 1"));
if (byName != null) {
return byName;
}
}
}
return null;
}
/**
* 导入时按货主名称/编号从用户中心解析货主ID(与 {@link com.mhd.wms.application.service.stockInOrder.StockInOrderApplicationService#getShipperIdByNameOrCode} 一致)。
* 不将纯数字的「货主名称」当作用户ID或主键,避免与业务上以数字作为企业简称/名称的货主(如「1449」)冲突。
@@ -658,17 +678,19 @@ public class MaterialBaseInfoApplicationService {
String shipperCode = StringUtils.isNotBlank(row.getShipperCode()) ? row.getShipperCode().trim() : null;
if (StringUtils.isNotBlank(shipperName)) {
try {
AjaxResult ajaxResult = userServiceFeign.getInfoByName(shipperName);
if ("200".equals(String.valueOf(ajaxResult.get("code"))) && ajaxResult.get("data") != null) {
UserPo userPo = JSON.parseObject(JSONObject.toJSONString(ajaxResult.get("data")), UserPo.class);
if (userPo != null && shipperUserMatchesImportOrganization(userPo, importOrgId)) {
applyUserPoToMaterialImportRow(row, userPo);
return;
for (String nameVariant : ZhTextNormalizeUtil.nameLookupVariants(shipperName)) {
try {
AjaxResult ajaxResult = userServiceFeign.getInfoByName(nameVariant);
if ("200".equals(String.valueOf(ajaxResult.get("code"))) && ajaxResult.get("data") != null) {
UserPo userPo = JSON.parseObject(JSONObject.toJSONString(ajaxResult.get("data")), UserPo.class);
if (userPo != null && shipperUserMatchesImportOrganization(userPo, importOrgId)) {
applyUserPoToMaterialImportRow(row, userPo);
return;
}
}
} catch (Exception e) {
log.debug("物料导入: getInfoByName 货主失败 name={}: {}", nameVariant, e.getMessage());
}
} catch (Exception e) {
log.debug("物料导入: getInfoByName 货主失败: {}", e.getMessage());
}
}
@@ -730,7 +752,8 @@ public class MaterialBaseInfoApplicationService {
continue;
}
String s = String.valueOf(cand).trim();
if (normalizedName.equals(s) || s.contains(normalizedName)) {
if (ZhTextNormalizeUtil.textEquals(normalizedName, s)
|| ZhTextNormalizeUtil.textContains(s, normalizedName)) {
matched = true;
break;
}
@@ -763,6 +786,15 @@ public class MaterialBaseInfoApplicationService {
return;
}
row.setShipperId(userPo.getUserId());
if (!isValidOrgId(row.getOrganizationId()) && isValidOrgId(userPo.getOrganizationId())) {
row.setOrganizationId(userPo.getOrganizationId());
}
if (row.getTopOrganizationId() == null && isValidOrgId(userPo.getTopOrganizationId())) {
row.setTopOrganizationId(userPo.getTopOrganizationId());
}
if (StringUtils.isBlank(row.getOrganizationName()) && StringUtils.isNotBlank(userPo.getOrganizationName())) {
row.setOrganizationName(userPo.getOrganizationName());
}
if (StringUtils.isBlank(row.getShipperCode()) && StringUtils.isNotBlank(userPo.getUserMemberCode())) {
row.setShipperCode(userPo.getUserMemberCode());
}
@@ -771,6 +803,29 @@ public class MaterialBaseInfoApplicationService {
}
}
/**
* Excel 已填货主 ID、但未填组织时,从货主用户补全组织信息。
*/
private void fillOrganizationFromShipperIfNeeded(MaterialBaseInfoDTO row) {
if (isValidOrgId(row.getOrganizationId()) || row.getShipperId() == null) {
return;
}
try {
AjaxResult infoRes = userServiceFeign.getInfo(row.getShipperId());
if (!"200".equals(String.valueOf(infoRes.get("code"))) || infoRes.get("data") == null) {
return;
}
UserPo userPo = JSON.parseObject(JSONObject.toJSONString(infoRes.get("data")), UserPo.class);
applyUserPoToMaterialImportRow(row, userPo);
} catch (Exception e) {
log.debug("物料导入: 按货主ID补全组织失败 shipperId={}: {}", row.getShipperId(), e.getMessage());
}
}
private boolean isValidOrgId(Long organizationId) {
return organizationId != null && organizationId > 0;
}
private boolean shipperUserMatchesImportOrganization(UserPo userPo, Long importOrganizationId) {
if (importOrganizationId == null) {
return true;
@@ -831,12 +886,7 @@ public class MaterialBaseInfoApplicationService {
}
private void fillClassifyTypeAndUnit(MaterialBaseInfoDTO row) {
// 物料分类:模板常用“物料分类”单列,回填到名称列表
if (CollectionUtils.isEmpty(row.getMaterialClassifyName()) && StringUtils.isNotBlank(row.getMaterialClassifyImport())) {
List<String> names = new ArrayList<>();
names.add(row.getMaterialClassifyImport().trim());
row.setMaterialClassifyName(names);
}
normalizeMaterialClassifyImportFields(row);
// 按组织从 system 主数据反查分类编码,与名称列表对齐
fillMaterialClassifyCodesFromSystem(row);
@@ -910,35 +960,81 @@ public class MaterialBaseInfoApplicationService {
}
}
/**
* Excel 导入列绑定 String 字段,再转为 List(List 字段仅用于导出/持久化)。
*/
private void normalizeMaterialClassifyImportFields(MaterialBaseInfoDTO row) {
if (CollectionUtils.isEmpty(row.getMaterialClassifyCode())
&& StringUtils.isNotBlank(row.getMaterialClassifyCodeImport())) {
row.setMaterialClassifyCode(splitImportList(row.getMaterialClassifyCodeImport()));
}
if (CollectionUtils.isEmpty(row.getMaterialClassifyName())) {
if (StringUtils.isNotBlank(row.getMaterialClassifyNameImport())) {
row.setMaterialClassifyName(splitImportList(row.getMaterialClassifyNameImport()));
} else if (StringUtils.isNotBlank(row.getMaterialClassifyImport())) {
row.setMaterialClassifyName(splitImportList(row.getMaterialClassifyImport()));
}
}
}
private List<String> splitImportList(String raw) {
if (StringUtils.isBlank(raw)) {
return null;
}
return Arrays.stream(raw.split("[,]"))
.map(String::trim)
.filter(StringUtils::isNotBlank)
.collect(Collectors.toList());
}
/**
* 导入时已有分类名称但缺少分类编码时,走 system 接口按组织+名称/编码/名称路径匹配并回填.
* Excel 已填写的编码列优先保留。
*/
private void fillMaterialClassifyCodesFromSystem(MaterialBaseInfoDTO row) {
if (CollectionUtils.isEmpty(row.getMaterialClassifyName()) || row.getOrganizationId() == null) {
if (row.getOrganizationId() == null) {
return;
}
List<String> names = row.getMaterialClassifyName();
List<String> existing = row.getMaterialClassifyCode();
if (CollectionUtils.isEmpty(names) && CollectionUtils.isEmpty(existing)) {
return;
}
int size = Math.max(names != null ? names.size() : 0, existing != null ? existing.size() : 0);
List<String> newCodes = new ArrayList<>();
for (int i = 0; i < names.size(); i++) {
for (int i = 0; i < size; i++) {
String fromExcel = (existing != null && i < existing.size()) ? existing.get(i) : null;
if (StringUtils.isNotBlank(fromExcel)) {
newCodes.add(fromExcel.trim());
continue;
}
String n = StringUtils.trimToNull(names.get(i));
String n = (names != null && i < names.size()) ? StringUtils.trimToNull(names.get(i)) : null;
if (n == null) {
newCodes.add(null);
} else {
newCodes.add(resolveClassifyCodeByFeign(n, row.getOrganizationId()));
continue;
}
String resolved = resolveClassifyCodeByFeign(n, row.getOrganizationId());
if (StringUtils.isNotBlank(resolved)) {
newCodes.add(resolved);
}
}
row.setMaterialClassifyCode(newCodes);
row.setMaterialClassifyCode(CollectionUtils.isEmpty(newCodes) ? null : newCodes);
}
/** 取 system 返回的 material_classify.code,写入本表 MATERIAL_CLASSIFY_CODEList 中一项) */
private String resolveClassifyCodeByFeign(String keyword, Long organizationId) {
if (StringUtils.isBlank(keyword) || organizationId == null) {
return null;
}
for (String variant : ZhTextNormalizeUtil.nameLookupVariants(keyword)) {
String code = resolveClassifyCodeByFeignOnce(variant, organizationId);
if (StringUtils.isNotBlank(code)) {
return code;
}
}
return null;
}
private String resolveClassifyCodeByFeignOnce(String keyword, Long organizationId) {
if (StringUtils.isBlank(keyword) || organizationId == null) {
return null;
}
@@ -31,7 +31,7 @@ public class MaterialBaseInfoDTO extends BaseVOEntity {
private Long materialBaseInfoId;
@ApiModelProperty("组织表ID")
@Excel(name = "组织表ID")
@Excel(name = "组织表id")
private Long organizationId;
@ApiModelProperty("组织名称")
@@ -39,7 +39,7 @@ public class MaterialBaseInfoDTO extends BaseVOEntity {
private String organizationName;
@ApiModelProperty("一级组织表ID")
@Excel(name = "一级组织表ID")
@Excel(name = "一级组织表id")
private Long topOrganizationId;
@ApiModelProperty("货主id")
@@ -67,16 +67,22 @@ public class MaterialBaseInfoDTO extends BaseVOEntity {
private String barCode;
@ApiModelProperty("物料分类编码")
@Excel(name = "物料分类编码")
@Excel(name = "物料分类编码", type = Excel.Type.EXPORT)
@TableField(typeHandler = ListTypeHandler.class)
private List<String> materialClassifyCode;
@ApiModelProperty("物料分类名称")
@Excel(name = "物料分类名称")
@Excel(name = "物料分类名称", type = Excel.Type.EXPORT)
@TableField(typeHandler = ListTypeHandler.class)
private List<String> materialClassifyName;
@ApiModelProperty("物料分类编码(导入)")
@Excel(name = "物料分类编码", type = Excel.Type.IMPORT)
private String materialClassifyCodeImport;
@ApiModelProperty("物料分类名称(导入)")
@Excel(name = "物料分类名称", type = Excel.Type.IMPORT)
private String materialClassifyNameImport;
@ApiModelProperty("物料分类(导入)")
@Excel(name = "物料分类")
@Excel(name = "物料分类", type = Excel.Type.IMPORT)
private String materialClassifyImport;
@ApiModelProperty("物料种类编码 数据字典")
@@ -0,0 +1,60 @@
package com.mhd.wms.util;
import com.mhd.common.core.utils.poi.ExcelUtil;
import com.mhd.wms.interfaces.dto.materialBaseInfo.MaterialBaseInfoDTO;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.DataFormatter;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.List;
/**
* 物料主数据导入:支持繁体中文表头的 Excel 解析。
*/
public final class MaterialBaseInfoImportExcelUtil {
private MaterialBaseInfoImportExcelUtil() {
}
public static List<MaterialBaseInfoDTO> importRows(InputStream is) throws Exception {
Workbook workbook = WorkbookFactory.create(is);
try {
normalizeHeaderRow(workbook.getSheetAt(0), 0);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
workbook.write(bos);
ExcelUtil<MaterialBaseInfoDTO> util = new ExcelUtil<>(MaterialBaseInfoDTO.class);
return util.importExcel(new ByteArrayInputStream(bos.toByteArray()));
} finally {
workbook.close();
}
}
private static void normalizeHeaderRow(Sheet sheet, int titleNum) {
if (sheet == null) {
return;
}
Row headerRow = sheet.getRow(titleNum);
if (headerRow == null) {
return;
}
DataFormatter formatter = new DataFormatter();
for (int i = 0; i < headerRow.getLastCellNum(); i++) {
Cell cell = headerRow.getCell(i);
if (cell == null) {
continue;
}
String value = formatter.formatCellValue(cell);
if (StringUtils.isBlank(value)) {
continue;
}
cell.setCellValue(ZhTextNormalizeUtil.normalizeExcelHeader(value));
}
}
}
@@ -0,0 +1,82 @@
package com.mhd.wms.util;
import com.github.houbb.opencc4j.util.ZhConverterUtil;
import org.apache.commons.lang3.StringUtils;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* 简繁体文本归一化,用于 Excel 导入表头及货主名称匹配。
*/
public final class ZhTextNormalizeUtil {
private ZhTextNormalizeUtil() {
}
public static String toSimple(String text) {
if (StringUtils.isBlank(text)) {
return text;
}
return ZhConverterUtil.toSimple(text.trim());
}
public static String toTraditional(String text) {
if (StringUtils.isBlank(text)) {
return text;
}
return ZhConverterUtil.toTraditional(text.trim());
}
/**
* 导入表头:繁体转简体,并将末尾 ID 统一为小写 id(与 @Excel name 一致)。
*/
public static String normalizeExcelHeader(String header) {
if (StringUtils.isBlank(header)) {
return header;
}
String simple = toSimple(header.trim());
if (simple.endsWith("ID")) {
return simple.substring(0, simple.length() - 2) + "id";
}
return simple;
}
public static boolean textEquals(String a, String b) {
if (a == null && b == null) {
return true;
}
if (a == null || b == null) {
return false;
}
return toSimple(a).equals(toSimple(b));
}
public static boolean textContains(String haystack, String needle) {
if (StringUtils.isBlank(haystack) || StringUtils.isBlank(needle)) {
return false;
}
return toSimple(haystack).contains(toSimple(needle));
}
/**
* 名称查询候选:原文、简体、繁体(去重,保持顺序)。
*/
public static Set<String> nameLookupVariants(String name) {
Set<String> variants = new LinkedHashSet<>();
if (StringUtils.isBlank(name)) {
return variants;
}
String trimmed = name.trim();
variants.add(trimmed);
String simple = toSimple(trimmed);
if (StringUtils.isNotBlank(simple)) {
variants.add(simple);
}
String traditional = toTraditional(trimmed);
if (StringUtils.isNotBlank(traditional)) {
variants.add(traditional);
}
return variants;
}
}