导入物料BUG修改

This commit is contained in:
秦鸿展
2026-07-24 18:49:14 +08:00
parent d268718f9b
commit 82909efe6b
8 changed files with 250 additions and 19 deletions
@@ -18,7 +18,7 @@ import java.util.Set;
/**
* bms财务结算系统feign调用接口
*/
@FeignClient(contextId = "BmsService", value = ServiceNameConstants.BMS_SERVICE, url = "http://10.102.192.3:8019",fallbackFactory = RemoteBmsFeignFallbackFactory.class, configuration = FeignAutoConfiguration.class)
@FeignClient(contextId = "BmsService", value = ServiceNameConstants.BMS_SERVICE, url = "http://10.33.0.109:8019",fallbackFactory = RemoteBmsFeignFallbackFactory.class, configuration = FeignAutoConfiguration.class)
public interface BmsServiceFeign {
@@ -16,7 +16,7 @@ import org.springframework.web.bind.annotation.RequestBody;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@FeignClient(contextId = "OmsService", value = ServiceNameConstants.OMS_SERVICE,url = "http://10.102.192.4:8017", fallbackFactory = RemoteOmsFeignFallbackFactory.class, configuration = FeignAutoConfiguration.class)
@FeignClient(contextId = "OmsService", value = ServiceNameConstants.OMS_SERVICE,url = "http://10.33.0.99:8017", fallbackFactory = RemoteOmsFeignFallbackFactory.class, configuration = FeignAutoConfiguration.class)
public interface OmsServiceFeign {
@@ -26,7 +26,7 @@ import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@FeignClient(contextId = "commonWlhyServiceFeign",value = ServiceNameConstants.WLHY_SERVICE,url = "http://10.102.192.30:8014",configuration = FeignAutoConfiguration.class)
@FeignClient(contextId = "commonWlhyServiceFeign",value = ServiceNameConstants.WLHY_SERVICE,url = "http://10.33.0.129:8014",configuration = FeignAutoConfiguration.class)
public interface WlhyServiceFeign {
/**
@@ -21,7 +21,7 @@ import java.util.Map;
* @description: TODO
* @date 2024/5/10 8:58
**/
@FeignClient(contextId = "remoteWmsServiceFeign",value = ServiceNameConstants.WMS_SERVICE, url = "http://10.102.192.3:8016", fallbackFactory = RemoteWmsFallbackFactory.class, configuration = FeignAutoConfiguration.class)
@FeignClient(contextId = "remoteWmsServiceFeign",value = ServiceNameConstants.WMS_SERVICE, url = "http://10.33.0.109:8016", fallbackFactory = RemoteWmsFallbackFactory.class, configuration = FeignAutoConfiguration.class)
public interface WmsServiceFeign {
/**
@@ -9,7 +9,7 @@ import com.mhd.system.api.feign.FeignAutoConfiguration;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
@FeignClient(contextId = "YmsService", value = ServiceNameConstants.YMS_SERVICE,fallbackFactory = RemoteBmsFeignFallbackFactory.class,url = "http://10.102.192.4:8018", configuration = FeignAutoConfiguration.class)
@FeignClient(contextId = "YmsService", value = ServiceNameConstants.YMS_SERVICE,fallbackFactory = RemoteBmsFeignFallbackFactory.class,url = "http://10.33.0.99:8018", configuration = FeignAutoConfiguration.class)
public interface YmsServiceFeign {
/**
@@ -49,12 +49,15 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.*;
import java.util.stream.Collectors;
@@ -436,8 +439,237 @@ public class MaterialBaseInfoApplicationService {
return materialBaseInfoDomainService.getInfoByNoOrderReceipt(materialBaseInfoDO);
}
/**
* 导入物料主数据(仅导入主表字段
* 导入物料主数据(异步入口
*
* <p>接口层调用此方法,将文件保存到临时目录后立即返回"异步处理中",
* 实际导入由 {@link #doImportData(File, String, LoginUser)} 异步执行,
* 避免大数据量时接口超时。</p>
*/
public AjaxResult importDataAsync(MultipartFile file) {
if (file == null || file.isEmpty()) {
throw new ServiceException("上传文件不能为空");
}
String fileName = file.getOriginalFilename();
if (org.apache.commons.lang3.StringUtils.isBlank(fileName)
|| (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx"))) {
throw new ServiceException("请上传xls或xlsx格式文件");
}
// 保存文件到临时目录
String tmpDir = System.getProperty("java.io.tmpdir") + File.separator + "material_import";
File dir = new File(tmpDir);
if (!dir.exists()) {
dir.mkdirs();
}
String tmpFileName = System.currentTimeMillis() + "_" + fileName;
File tmpFile = new File(dir, tmpFileName);
try (FileOutputStream fos = new FileOutputStream(tmpFile)) {
fos.write(file.getBytes());
} catch (Exception e) {
throw new ServiceException("文件保存失败:" + e.getMessage());
}
LoginUser loginUser = SecurityUtils.getLoginUser();
// 异步执行导入
doImportData(tmpFile, fileName, loginUser);
return AjaxResult.success("导入任务已提交,正在后台异步处理,请稍后查看导入结果日志");
}
/**
* 异步执行物料主数据导入
*
* <p>@Async 确保在独立线程中执行,不阻塞接口响应。
* 每行独立事务,避免大事务锁表导致性能问题。</p>
*/
@Async
public void doImportData(File tmpFile, String fileName, LoginUser loginUser) {
long startTime = System.currentTimeMillis();
log.info("=== 物料主数据异步导入开始,文件:{},大小:{}KB ===", fileName, tmpFile.length() / 1024);
try {
List<MaterialBaseInfoDTO> rows;
try (InputStream is = new java.io.FileInputStream(tmpFile)) {
rows = MaterialBaseInfoImportExcelUtil.importRows(is);
} catch (Exception e) {
log.error("物料主数据异步导入失败 - 文件解析异常:{}", e.getMessage(), e);
return;
}
if (CollectionUtils.isEmpty(rows)) {
log.warn("物料主数据异步导入结束 - 导入数据为空");
return;
}
Long loginOrgId = null;
Long loginTopOrgId = null;
if (loginUser != null && loginUser.getUserPo() != null) {
loginOrgId = loginUser.getUserPo().getOrganizationId();
loginTopOrgId = loginUser.getUserPo().getTopOrganizationId();
}
StringBuilder errors = new StringBuilder();
Set<String> duplicateInFile = new HashSet<>();
int successCount = 0;
int totalCount = rows.size();
// 包装规格缓存:避免 5000 行数据对 system 服务发起 5000 次重复远程调用
Map<String, Map<String, Object>> packCache = new HashMap<>();
for (int i = 0; i < rows.size(); i++) {
MaterialBaseInfoDTO row = rows.get(i);
int rowNum = i + 2;
if (row == null) {
continue;
}
try {
if (StringUtils.isBlank(row.getMaterialCode())) {
errors.append("").append(rowNum).append("行:物料编码不能为空;");
continue;
}
if (StringUtils.isBlank(row.getMaterialName())) {
errors.append("").append(rowNum).append("行:物料名称不能为空;");
continue;
}
if (row.getNullify() == null) {
row.setNullify(2);
}
if (row.getCommon() == null) {
row.setCommon(2);
}
if (StringUtils.isBlank(row.getHsCode()) && StringUtils.isNotBlank(row.getHsCodeImport())) {
row.setHsCode(row.getHsCodeImport().trim());
}
normalizeImportBarCodeWhenDuplicateOfHs(row);
fillIdsFromExistingMasterData(row);
fillOrganizationFromShipperIfNeeded(row);
if (!isValidOrgId(row.getOrganizationId()) && isValidOrgId(loginOrgId)) {
row.setOrganizationId(loginOrgId);
}
if (row.getTopOrganizationId() == null && isValidOrgId(loginTopOrgId)) {
row.setTopOrganizationId(loginTopOrgId);
}
fillOrganizationName(row, loginUser);
String shipperCode = StringUtils.defaultString(row.getShipperCode(), "");
String duplicateKey = row.getOrganizationId() + "|" + shipperCode + "|" + row.getMaterialCode();
if (!duplicateInFile.add(duplicateKey)) {
errors.append("").append(rowNum).append("行:文件内存在重复物料(组织+货主编码+物料编码);");
continue;
}
LambdaQueryWrapper<MaterialBaseInfo> wrapper = new LambdaQueryWrapper<MaterialBaseInfo>()
.eq(MaterialBaseInfo::getDelFlag, 1)
.eq(MaterialBaseInfo::getOrganizationId, row.getOrganizationId())
.eq(MaterialBaseInfo::getMaterialCode, row.getMaterialCode());
if (StringUtils.isBlank(row.getShipperCode())) {
wrapper.and(w -> w.isNull(MaterialBaseInfo::getShipperCode).or().eq(MaterialBaseInfo::getShipperCode, ""));
} else {
wrapper.eq(MaterialBaseInfo::getShipperCode, row.getShipperCode());
}
if (materialBaseInfoService.count(wrapper) > 0) {
errors.append("").append(rowNum).append("行:物料已存在(组织+货主编码+物料编码);");
continue;
}
fillClassifyTypeAndUnit(row);
// 包装规格查询(带本地缓存,避免 5000 行对 system 服务发起 5000 次远程调用)
String packCacheKey = row.getOrganizationId() + "|" + StringUtils.defaultString(row.getPackName());
Map<String, Object> cachedPack = packCache.get(packCacheKey);
if (cachedPack == null) {
Object result = systemServiceFeign.getPackList(null, row.getPackName(), row.getOrganizationId());
if (result == null) {
// 缓存空结果,避免重复查询不存在的包装规格
packCache.put(packCacheKey, Collections.emptyMap());
errors.append("").append(rowNum).append("行:包装规格查询无返回,请稍后重试或检查系统服务;");
continue;
}
Map<String, Object> resultMap = JSON.parseObject(JSONObject.toJSONString(result), new TypeReference<Map<String, Object>>() {});
List<Map<String, Object>> rowsPack = extractTableDataRows(resultMap);
if (CollectionUtils.isEmpty(rowsPack)) {
packCache.put(packCacheKey, Collections.emptyMap());
if (StringUtils.isNotBlank(row.getPackName())) {
errors.append("").append(rowNum).append("行:包装规格「").append(row.getPackName()).append("」不存在或未在系统中维护;");
} else {
errors.append("").append(rowNum).append("行:包装规格未填写,或系统中无匹配的包装数据;");
}
continue;
}
cachedPack = rowsPack.get(0);
packCache.put(packCacheKey, cachedPack);
}
if (cachedPack.isEmpty()) {
errors.append("").append(rowNum).append("行:包装规格不存在(来自缓存);");
continue;
}
if (cachedPack == null) {
errors.append("").append(rowNum).append("行:包装规格查询结果为空;");
continue;
}
String packCode = cachedPack.get("packCode") == null ? "" : String.valueOf(cachedPack.get("packCode"));
row.setPackCode(packCode);
String packId = cachedPack.get("packId") == null ? "" : String.valueOf(cachedPack.get("packId"));
if (StringUtils.isBlank(packId) || !StringUtils.isNumeric(packId)) {
errors.append("").append(rowNum).append("行:包装规格数据异常(缺少包装ID);");
continue;
}
row.setPackId(Long.valueOf(packId));
String unitName = cachedPack.get("unitName") == null ? "" : String.valueOf(cachedPack.get("unitName"));
row.setUnitCode("EA");
row.setUnitName(unitName);
// 每行独立事务写入
MaterialBaseInfoDO materialBaseInfoDO = new MaterialBaseInfoDO();
BeanUtils.copyProperties(row, materialBaseInfoDO);
materialBaseInfoDO.setMaterialGoodsRule(buildImportMaterialGoodsRule(row));
materialBaseInfoDO.setMaterialCommon(null);
materialBaseInfoDO.setMaterialWarehouseControl(null);
materialBaseInfoDO.setMaterialBarCodeList(buildImportBarCodeList(row));
insert(materialBaseInfoDO);
Long materialBaseInfoId = materialBaseInfoDO.getMaterialBaseInfoId();
MaterialCommonDO materialCommonDO = materialBaseInfoDO.getMaterialCommon();
if (materialCommonDO != null) {
materialCommonDO.setMaterialBaseInfoId(materialBaseInfoId);
materialCommonDomainService.insert(materialCommonDO);
}
MaterialWarehouseControlDO materialWarehouseControlDO = materialBaseInfoDO.getMaterialWarehouseControl();
if (materialWarehouseControlDO != null) {
materialWarehouseControlDO.setMaterialBaseInfoId(materialBaseInfoId);
materialWarehouseControlDomainService.insert(materialWarehouseControlDO);
}
materialInventoryWarningDomainService.batchInsertOrUpdate(materialBaseInfoDO);
materialBarCodeDomainService.batchInsertOrUpdate(materialBaseInfoDO);
successCount++;
} catch (Exception e) {
log.error("物料主数据异步导入 - 第{}行处理异常", rowNum, e);
errors.append("").append(rowNum).append("行:处理异常-").append(e.getMessage()).append("");
}
}
long costTime = System.currentTimeMillis() - startTime;
if (errors.length() > 0) {
log.warn("=== 物料主数据异步导入完成,文件:{},总数:{},成功:{},失败:{},耗时:{}ms,失败原因:{} ===",
fileName, totalCount, successCount, totalCount - successCount, costTime, errors);
} else {
log.info("=== 物料主数据异步导入完成,文件:{},总数:{},成功:{},耗时:{}ms ===",
fileName, totalCount, successCount, costTime);
}
} finally {
// 清理临时文件
try {
if (tmpFile.exists()) {
tmpFile.delete();
}
} catch (Exception e) {
log.warn("物料主数据异步导入 - 临时文件清理失败:{}", e.getMessage());
}
}
}
/**
* 导入物料主数据(仅导入主表字段)- 原同步方法保留,供其他场景调用
*/
@Transactional(rollbackFor = Exception.class)
public AjaxResult importData(MultipartFile file) {
@@ -496,7 +728,6 @@ public class MaterialBaseInfoApplicationService {
row.setHsCode(row.getHsCodeImport().trim());
}
normalizeImportBarCodeWhenDuplicateOfHs(row);
// 客户模板通常只有「货主名称」无「货主id」:须先按名称解析货主,再回填组织;勿提前写入登录组织以免组织校验拦截货主匹配
fillIdsFromExistingMasterData(row);
fillOrganizationFromShipperIfNeeded(row);
if (!isValidOrgId(row.getOrganizationId()) && isValidOrgId(loginOrgId)) {
@@ -567,25 +798,20 @@ public class MaterialBaseInfoApplicationService {
materialBaseInfoDO.setMaterialCommon(null);
materialBaseInfoDO.setMaterialWarehouseControl(null);
materialBaseInfoDO.setMaterialBarCodeList(buildImportBarCodeList(row));
// 商品规则由 buildImportMaterialGoodsRule + insert() 统一写入,勿再单独 insert,避免 MATERIAL_GOODS_RULE 重复
insert(materialBaseInfoDO);
Long materialBaseInfoId = materialBaseInfoDO.getMaterialBaseInfoId();
//公共信息
MaterialCommonDO materialCommonDO = materialBaseInfoDO.getMaterialCommon();
if (materialCommonDO != null) {
materialCommonDO.setMaterialBaseInfoId(materialBaseInfoId);
materialCommonDomainService.insert(materialCommonDO);
}
//仓库控制信息
MaterialWarehouseControlDO materialWarehouseControlDO = materialBaseInfoDO.getMaterialWarehouseControl();
if (materialWarehouseControlDO != null) {
materialWarehouseControlDO.setMaterialBaseInfoId(materialBaseInfoId);
materialWarehouseControlDomainService.insert(materialWarehouseControlDO);
}
//库存预警
materialInventoryWarningDomainService.batchInsertOrUpdate(materialBaseInfoDO);
//条码信息
materialBarCodeDomainService.batchInsertOrUpdate(materialBaseInfoDO);
successCount++;
}
@@ -596,6 +822,7 @@ public class MaterialBaseInfoApplicationService {
return AjaxResult.success("导入成功,共" + successCount + "");
}
/**
* 使用已存在主数据按名称/编码反查并回填ID
*/
@@ -106,12 +106,16 @@ public class MaterialBaseInfoApi extends BaseController {
}
/**
* 导入物料主数据
* 导入物料主数据异步执行
*
* <p>数据量大时同步导入会导致接口超时改为异步模式
* 接口立即返回"已提交异步处理"后台线程执行实际导入
* 导入结果通过日志输出</p>
*/
@ApiOperation("导入物料主数据")
@ApiOperation("导入物料主数据(异步)")
@PostMapping("/importData")
public AjaxResult importData(@RequestParam("file") MultipartFile file) {
return materialBaseInfoApplicationService.importData(file);
return materialBaseInfoApplicationService.importDataAsync(file);
}
@ApiOperation("物料管理更新推送信息")
+4 -4
View File
@@ -14,18 +14,18 @@ spring:
nacos:
discovery:
# server-addr: 127.0.0.1:8848
# server-addr: 10.33.0.129:6010
server-addr: 10.33.0.129:6010
# 线上测试环境配置 容器名+端口号
server-addr: 10.102.192.30:6848
# server-addr: 10.102.192.30:6848
username: nacos
password: manhuoda@2023
#线上正式环境
# server-addr: 10.102.192.105:6848
config:
# server-addr: 127.0.0.1:8848
# server-addr: 10.33.0.129:6010
server-addr: 10.33.0.129:6010
# 线上测试环境配置 容器名+端口号
server-addr: 10.102.192.30:6848
# server-addr: 10.102.192.30:6848
username: nacos
password: manhuoda@2023
#线上正式环境