11 changed files with 932 additions and 20 deletions
-
7src/main/java/com/xujie/modules/supplier/service/impl/SupplierPartMoqPriceServiceImpl.java
-
73src/main/java/com/xujie/modules/sys/controller/SysInitialImportController.java
-
38src/main/java/com/xujie/modules/sys/dao/SysInitialImportMapper.java
-
17src/main/java/com/xujie/modules/sys/service/SysInitialImportService.java
-
630src/main/java/com/xujie/modules/sys/service/impl/SysInitialImportServiceImpl.java
-
19src/main/java/com/xujie/modules/sys/vo/SupplierPriceInitialImportPreviewResultVo.java
-
35src/main/java/com/xujie/modules/sys/vo/SupplierPriceInitialImportRowVo.java
-
16src/main/java/com/xujie/modules/sys/vo/SupplierPriceInitialImportSaveVo.java
-
12src/main/resources/mapper/supplier/SupplierPartMoqPriceMapper.xml
-
85src/main/resources/mapper/sys/SysInitialImportMapper.xml
-
18src/main/resources/mapper/sys/SysMenuDao.xml
@ -0,0 +1,73 @@ |
|||
package com.xujie.modules.sys.controller; |
|||
|
|||
import com.xujie.common.utils.R; |
|||
import com.xujie.modules.sys.service.SysInitialImportService; |
|||
import com.xujie.modules.sys.vo.SupplierPriceInitialImportPreviewResultVo; |
|||
import com.xujie.modules.sys.vo.SupplierPriceInitialImportSaveVo; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.core.io.ByteArrayResource; |
|||
import org.springframework.core.io.Resource; |
|||
import org.springframework.http.HttpHeaders; |
|||
import org.springframework.http.MediaType; |
|||
import org.springframework.http.ResponseEntity; |
|||
import org.springframework.web.bind.annotation.GetMapping; |
|||
import org.springframework.web.bind.annotation.PostMapping; |
|||
import org.springframework.web.bind.annotation.RequestMapping; |
|||
import org.springframework.web.bind.annotation.RequestBody; |
|||
import org.springframework.web.bind.annotation.RequestParam; |
|||
import org.springframework.web.bind.annotation.RestController; |
|||
import org.springframework.web.multipart.MultipartFile; |
|||
|
|||
import java.net.URLEncoder; |
|||
import java.nio.charset.StandardCharsets; |
|||
|
|||
/** |
|||
* 系统管理 - 期初导入。 |
|||
*/ |
|||
@RestController |
|||
@RequestMapping("/sys/initialImport") |
|||
public class SysInitialImportController { |
|||
|
|||
@Autowired |
|||
private SysInitialImportService initialImportService; |
|||
|
|||
@GetMapping("/supplierPrice/template") |
|||
public ResponseEntity<Resource> downloadSupplierPriceTemplate() { |
|||
byte[] bytes = initialImportService.buildSupplierPriceTemplate(); |
|||
String fileName = URLEncoder.encode("供应商价格期初导入模板.xlsx", StandardCharsets.UTF_8) |
|||
.replace("+", "%20"); |
|||
return ResponseEntity.ok() |
|||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + fileName) |
|||
.header(HttpHeaders.CONTENT_LENGTH, String.valueOf(bytes.length)) |
|||
.contentType(MediaType.APPLICATION_OCTET_STREAM) |
|||
.body(new ByteArrayResource(bytes)); |
|||
} |
|||
|
|||
@PostMapping("/supplierPrice/preview") |
|||
public R previewSupplierPrice(@RequestParam("file") MultipartFile file, |
|||
@RequestParam("site") String site) { |
|||
try { |
|||
SupplierPriceInitialImportPreviewResultVo result = initialImportService.previewSupplierPrice(file, site); |
|||
return R.ok() |
|||
.put("data", result.getRows()) |
|||
.put("total", result.getRows().size()) |
|||
.put("missingItems", result.getMissingItems()) |
|||
.put("missingSuppliers", result.getMissingSuppliers()); |
|||
} catch (RuntimeException ex) { |
|||
return R.error(ex.getMessage()); |
|||
} |
|||
} |
|||
|
|||
@PostMapping("/supplierPrice/save") |
|||
public R saveSupplierPrice(@RequestBody SupplierPriceInitialImportSaveVo data) { |
|||
try { |
|||
int savedCount = initialImportService.saveSupplierPrice(data); |
|||
int totalCount = data == null || data.getRows() == null ? 0 : data.getRows().size(); |
|||
return R.ok() |
|||
.put("savedCount", savedCount) |
|||
.put("skippedCount", totalCount - savedCount); |
|||
} catch (RuntimeException ex) { |
|||
return R.error(ex.getMessage()); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
package com.xujie.modules.sys.dao; |
|||
|
|||
import org.apache.ibatis.annotations.Mapper; |
|||
import org.apache.ibatis.annotations.Param; |
|||
|
|||
import java.math.BigDecimal; |
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 系统期初导入专用数据访问。 |
|||
*/ |
|||
@Mapper |
|||
public interface SysInitialImportMapper { |
|||
|
|||
List<String> listExistingItems(@Param("site") String site, @Param("items") List<String> items); |
|||
|
|||
List<String> listExistingSupplierShortNames(@Param("site") String site, |
|||
@Param("shortNames") List<String> shortNames); |
|||
|
|||
List<String> listPartNosByItem(@Param("site") String site, @Param("item") String item); |
|||
|
|||
List<String> listSupplierNosByShortNpc(@Param("site") String site, |
|||
@Param("shortNpc") String shortNpc); |
|||
|
|||
Integer countSupplierPart(@Param("site") String site, |
|||
@Param("supplierNo") String supplierNo, |
|||
@Param("partNo") String partNo); |
|||
|
|||
int updateSupplierPartMoq(@Param("site") String site, |
|||
@Param("supplierNo") String supplierNo, |
|||
@Param("partNo") String partNo, |
|||
@Param("minOrderQty") BigDecimal minOrderQty); |
|||
|
|||
int insertSupplierPart(@Param("site") String site, |
|||
@Param("supplierNo") String supplierNo, |
|||
@Param("partNo") String partNo, |
|||
@Param("minOrderQty") BigDecimal minOrderQty); |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
package com.xujie.modules.sys.service; |
|||
|
|||
import com.xujie.modules.sys.vo.SupplierPriceInitialImportPreviewResultVo; |
|||
import com.xujie.modules.sys.vo.SupplierPriceInitialImportSaveVo; |
|||
import org.springframework.web.multipart.MultipartFile; |
|||
|
|||
/** |
|||
* 系统期初导入服务。 |
|||
*/ |
|||
public interface SysInitialImportService { |
|||
|
|||
byte[] buildSupplierPriceTemplate(); |
|||
|
|||
SupplierPriceInitialImportPreviewResultVo previewSupplierPrice(MultipartFile file, String site); |
|||
|
|||
int saveSupplierPrice(SupplierPriceInitialImportSaveVo data); |
|||
} |
|||
@ -0,0 +1,630 @@ |
|||
package com.xujie.modules.sys.service.impl; |
|||
|
|||
import com.xujie.modules.supplier.data.SupplierPartTierPriceHistoryMainVo; |
|||
import com.xujie.modules.supplier.data.SupplierPartTierPriceItemVo; |
|||
import com.xujie.modules.supplier.data.SupplierPartTierPriceQueryVo; |
|||
import com.xujie.modules.supplier.mapper.SupplierPartMoqPriceMapper; |
|||
import com.xujie.modules.sys.dao.SysInitialImportMapper; |
|||
import com.xujie.modules.sys.entity.SysUserEntity; |
|||
import com.xujie.modules.sys.service.SysInitialImportService; |
|||
import com.xujie.modules.sys.vo.SupplierPriceInitialImportPreviewResultVo; |
|||
import com.xujie.modules.sys.vo.SupplierPriceInitialImportRowVo; |
|||
import com.xujie.modules.sys.vo.SupplierPriceInitialImportSaveVo; |
|||
import org.apache.poi.ss.usermodel.BorderStyle; |
|||
import org.apache.poi.ss.usermodel.Cell; |
|||
import org.apache.poi.ss.usermodel.CellStyle; |
|||
import org.apache.poi.ss.usermodel.CellType; |
|||
import org.apache.poi.ss.usermodel.DataFormatter; |
|||
import org.apache.poi.ss.usermodel.DateUtil; |
|||
import org.apache.poi.ss.usermodel.FillPatternType; |
|||
import org.apache.poi.ss.usermodel.Font; |
|||
import org.apache.poi.ss.usermodel.FormulaEvaluator; |
|||
import org.apache.poi.ss.usermodel.HorizontalAlignment; |
|||
import org.apache.poi.ss.usermodel.IndexedColors; |
|||
import org.apache.poi.ss.usermodel.Row; |
|||
import org.apache.poi.ss.usermodel.Sheet; |
|||
import org.apache.poi.ss.usermodel.VerticalAlignment; |
|||
import org.apache.poi.ss.usermodel.Workbook; |
|||
import org.apache.poi.ss.usermodel.WorkbookFactory; |
|||
import org.apache.poi.xssf.usermodel.XSSFWorkbook; |
|||
import org.apache.shiro.SecurityUtils; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.stereotype.Service; |
|||
import org.springframework.transaction.annotation.Transactional; |
|||
import org.springframework.util.StringUtils; |
|||
import org.springframework.web.multipart.MultipartFile; |
|||
|
|||
import java.io.ByteArrayOutputStream; |
|||
import java.math.BigDecimal; |
|||
import java.math.RoundingMode; |
|||
import java.time.LocalDate; |
|||
import java.time.LocalDateTime; |
|||
import java.time.ZoneId; |
|||
import java.time.format.DateTimeFormatter; |
|||
import java.time.format.DateTimeParseException; |
|||
import java.util.ArrayList; |
|||
import java.util.Arrays; |
|||
import java.util.Date; |
|||
import java.util.LinkedHashSet; |
|||
import java.util.List; |
|||
import java.util.Locale; |
|||
import java.util.Set; |
|||
import java.util.UUID; |
|||
|
|||
@Service |
|||
public class SysInitialImportServiceImpl implements SysInitialImportService { |
|||
|
|||
private static final int MAX_IMPORT_ROWS = 1000; |
|||
private static final String SHEET_NAME = "price list"; |
|||
private static final List<String> HEADERS = Arrays.asList( |
|||
"RAW CODE", "SUPPLIER", "MOQ/COLOR", "START DATE", |
|||
"Tier_QTY_1", "Tier_QTY_2", "Tier_QTY_3", "Tier_QTY_4", |
|||
"Tier_QTY_5", "Tier_QTY_6", "Tier_QTY_7", "Tier_QTY_8", |
|||
"Price_Tier_1", "Price_Tier_2", "Price_Tier_3", "Price_Tier_4", |
|||
"Price_Tier_5", "Price_Tier_6", "Price_Tier_7", "Price_Tier_8" |
|||
); |
|||
private static final List<DateTimeFormatter> DATE_FORMATTERS = Arrays.asList( |
|||
DateTimeFormatter.ofPattern("yyyy-M-d", Locale.ENGLISH), |
|||
DateTimeFormatter.ofPattern("yyyy/M/d", Locale.ENGLISH), |
|||
DateTimeFormatter.ofPattern("M/d/yyyy", Locale.ENGLISH), |
|||
DateTimeFormatter.ofPattern("d/M/yyyy", Locale.ENGLISH), |
|||
DateTimeFormatter.ofPattern("d-MMM-yy", Locale.ENGLISH), |
|||
DateTimeFormatter.ofPattern("d-MMM-yyyy", Locale.ENGLISH), |
|||
DateTimeFormatter.ofPattern("MMM d, yyyy", Locale.ENGLISH), |
|||
DateTimeFormatter.ofPattern("yyyy年M月d日", Locale.CHINA) |
|||
); |
|||
private static final List<DateTimeFormatter> DATE_TIME_FORMATTERS = Arrays.asList( |
|||
DateTimeFormatter.ofPattern("yyyy-M-d H:m:s", Locale.ENGLISH), |
|||
DateTimeFormatter.ofPattern("yyyy/M/d H:m:s", Locale.ENGLISH), |
|||
DateTimeFormatter.ofPattern("yyyy-M-d H:m", Locale.ENGLISH), |
|||
DateTimeFormatter.ofPattern("yyyy/M/d H:m", Locale.ENGLISH), |
|||
DateTimeFormatter.ofPattern("M/d/yyyy H:m:s", Locale.ENGLISH), |
|||
DateTimeFormatter.ofPattern("M/d/yyyy H:m", Locale.ENGLISH) |
|||
); |
|||
|
|||
@Autowired |
|||
private SysInitialImportMapper initialImportMapper; |
|||
|
|||
@Autowired |
|||
private SupplierPartMoqPriceMapper supplierPartMoqPriceMapper; |
|||
|
|||
@Override |
|||
public byte[] buildSupplierPriceTemplate() { |
|||
try (Workbook workbook = new XSSFWorkbook(); ByteArrayOutputStream output = new ByteArrayOutputStream()) { |
|||
Sheet sheet = workbook.createSheet(SHEET_NAME); |
|||
Row headerRow = sheet.createRow(0); |
|||
CellStyle headerStyle = createHeaderStyle(workbook); |
|||
for (int i = 0; i < HEADERS.size(); i++) { |
|||
Cell cell = headerRow.createCell(i); |
|||
cell.setCellValue(HEADERS.get(i)); |
|||
cell.setCellStyle(headerStyle); |
|||
sheet.setColumnWidth(i, resolveColumnWidth(i) * 256); |
|||
} |
|||
sheet.createFreezePane(0, 1); |
|||
workbook.write(output); |
|||
return output.toByteArray(); |
|||
} catch (Exception ex) { |
|||
throw new RuntimeException("生成供应商价格导入模板失败", ex); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public SupplierPriceInitialImportPreviewResultVo previewSupplierPrice(MultipartFile file, String site) { |
|||
validateFile(file); |
|||
if (!StringUtils.hasText(site)) { |
|||
throw new RuntimeException("登录Site不能为空"); |
|||
} |
|||
try (Workbook workbook = WorkbookFactory.create(file.getInputStream())) { |
|||
if (workbook.getNumberOfSheets() == 0) { |
|||
throw new RuntimeException("Excel中没有工作表"); |
|||
} |
|||
Sheet sheet = workbook.getSheetAt(0); |
|||
DataFormatter formatter = new DataFormatter(Locale.ENGLISH); |
|||
FormulaEvaluator evaluator = workbook.getCreationHelper().createFormulaEvaluator(); |
|||
validateHeader(sheet.getRow(0), formatter, evaluator); |
|||
|
|||
List<SupplierPriceInitialImportRowVo> result = new ArrayList<SupplierPriceInitialImportRowVo>(); |
|||
for (int rowIndex = 1; rowIndex <= sheet.getLastRowNum(); rowIndex++) { |
|||
Row row = sheet.getRow(rowIndex); |
|||
if (isEmptyRow(row, formatter, evaluator)) { |
|||
continue; |
|||
} |
|||
if (result.size() >= MAX_IMPORT_ROWS) { |
|||
throw new RuntimeException("单次导入上限是1000行,请分批导入"); |
|||
} |
|||
result.add(parseRow(row, rowIndex + 1, formatter, evaluator)); |
|||
} |
|||
if (result.isEmpty()) { |
|||
throw new RuntimeException("Excel中没有可预览的数据"); |
|||
} |
|||
return validatePreviewMappings(site.trim(), result); |
|||
} catch (RuntimeException ex) { |
|||
throw ex; |
|||
} catch (Exception ex) { |
|||
throw new RuntimeException("读取Excel失败,请检查文件格式", ex); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
@Transactional(rollbackFor = Exception.class) |
|||
public int saveSupplierPrice(SupplierPriceInitialImportSaveVo data) { |
|||
if (data == null || !StringUtils.hasText(data.getSite())) { |
|||
throw new RuntimeException("登录Site不能为空"); |
|||
} |
|||
if (data.getRows() == null || data.getRows().isEmpty()) { |
|||
throw new RuntimeException("没有可保存的导入数据"); |
|||
} |
|||
if (data.getRows().size() > MAX_IMPORT_ROWS) { |
|||
throw new RuntimeException("单次导入上限是1000行,请分批导入"); |
|||
} |
|||
|
|||
String site = data.getSite().trim(); |
|||
String submitBy = resolveSubmitBy(); |
|||
int savedCount = 0; |
|||
for (int index = 0; index < data.getRows().size(); index++) { |
|||
try { |
|||
if (saveSupplierPriceRow(site, submitBy, data.getRows().get(index))) { |
|||
savedCount++; |
|||
} |
|||
} catch (RuntimeException ex) { |
|||
throw new RuntimeException("Excel第" + (index + 2) + "行:" + ex.getMessage(), ex); |
|||
} |
|||
} |
|||
return savedCount; |
|||
} |
|||
|
|||
private boolean saveSupplierPriceRow(String site, String submitBy, SupplierPriceInitialImportRowVo row) { |
|||
if (row == null) { |
|||
throw new RuntimeException("数据不能为空"); |
|||
} |
|||
String item = trimToEmpty(row.getRawCode()); |
|||
String shortNpc = trimToEmpty(row.getSupplier()); |
|||
if (!StringUtils.hasText(item) || !StringUtils.hasText(shortNpc)) { |
|||
return false; |
|||
} |
|||
List<String> partNos = initialImportMapper.listPartNosByItem(site, item); |
|||
List<String> supplierNos = initialImportMapper.listSupplierNosByShortNpc(site, shortNpc); |
|||
if (partNos == null || partNos.isEmpty() || supplierNos == null || supplierNos.isEmpty()) { |
|||
return false; |
|||
} |
|||
if (row.getMoqColor() != null && row.getMoqColor().compareTo(BigDecimal.ZERO) < 0) { |
|||
throw new RuntimeException("MOQ/COLOR不能小于0"); |
|||
} |
|||
|
|||
String partNo = resolveUniqueCode(partNos, |
|||
"Item未找到对应物料:" + item, |
|||
"Item对应多个物料,请先处理重复数据:" + item); |
|||
String supplierNo = resolveUniqueCode(supplierNos, |
|||
"Short_NPC未找到对应供应商:" + shortNpc, |
|||
"Short_NPC对应多个供应商,请先处理重复数据:" + shortNpc); |
|||
|
|||
Integer relationCount = initialImportMapper.countSupplierPart(site, supplierNo, partNo); |
|||
if (relationCount != null && relationCount > 0) { |
|||
initialImportMapper.updateSupplierPartMoq(site, supplierNo, partNo, row.getMoqColor()); |
|||
} else { |
|||
initialImportMapper.insertSupplierPart(site, supplierNo, partNo, row.getMoqColor()); |
|||
} |
|||
|
|||
List<SupplierPartTierPriceItemVo> newTierPrices = buildTierPriceList(row); |
|||
if (newTierPrices.isEmpty()) { |
|||
return true; |
|||
} |
|||
LocalDate newPriceDate = parseImportDate(row.getStartDate()); |
|||
|
|||
SupplierPartTierPriceQueryVo query = new SupplierPartTierPriceQueryVo(); |
|||
query.setSite(site); |
|||
query.setSupplierCode(supplierNo); |
|||
query.setPartNo(partNo); |
|||
List<SupplierPartTierPriceItemVo> currentTierPrices = supplierPartMoqPriceMapper.listCurrentTierPrice(query); |
|||
|
|||
if (currentTierPrices != null && !currentTierPrices.isEmpty()) { |
|||
LocalDate currentPriceDate = resolveCurrentPriceDate(currentTierPrices); |
|||
if (!currentPriceDate.equals(newPriceDate)) { |
|||
archiveCurrentTierPrice(site, supplierNo, partNo, submitBy, currentPriceDate, currentTierPrices); |
|||
} |
|||
supplierPartMoqPriceMapper.deleteCurrentTierPrice(site, supplierNo, partNo); |
|||
} |
|||
|
|||
supplierPartMoqPriceMapper.insertCurrentTierPriceBatch( |
|||
site, |
|||
supplierNo, |
|||
partNo, |
|||
newPriceDate.toString(), |
|||
"", |
|||
submitBy, |
|||
newTierPrices |
|||
); |
|||
return true; |
|||
} |
|||
|
|||
private SupplierPriceInitialImportPreviewResultVo validatePreviewMappings( |
|||
String site, List<SupplierPriceInitialImportRowVo> rows) { |
|||
Set<String> requestedItems = new LinkedHashSet<>(); |
|||
Set<String> requestedSupplierShortNames = new LinkedHashSet<>(); |
|||
for (SupplierPriceInitialImportRowVo row : rows) { |
|||
if (row != null && StringUtils.hasText(row.getRawCode())) { |
|||
requestedItems.add(normalizeLookupValue(row.getRawCode())); |
|||
} |
|||
if (row != null && StringUtils.hasText(row.getSupplier())) { |
|||
requestedSupplierShortNames.add(normalizeLookupValue(row.getSupplier())); |
|||
} |
|||
} |
|||
|
|||
Set<String> existingItems = normalizeLookupValues(requestedItems.isEmpty() |
|||
? new ArrayList<>() |
|||
: initialImportMapper.listExistingItems(site, new ArrayList<>(requestedItems))); |
|||
Set<String> existingSupplierShortNames = normalizeLookupValues(requestedSupplierShortNames.isEmpty() |
|||
? new ArrayList<>() |
|||
: initialImportMapper.listExistingSupplierShortNames( |
|||
site, new ArrayList<>(requestedSupplierShortNames))); |
|||
|
|||
Set<String> missingItems = new LinkedHashSet<>(); |
|||
Set<String> missingSuppliers = new LinkedHashSet<>(); |
|||
for (SupplierPriceInitialImportRowVo row : rows) { |
|||
String item = row == null ? "" : trimToEmpty(row.getRawCode()); |
|||
String supplierShortName = row == null ? "" : trimToEmpty(row.getSupplier()); |
|||
boolean itemExists = StringUtils.hasText(item) |
|||
&& existingItems.contains(normalizeLookupValue(item)); |
|||
boolean supplierExists = StringUtils.hasText(supplierShortName) |
|||
&& existingSupplierShortNames.contains(normalizeLookupValue(supplierShortName)); |
|||
|
|||
if (!itemExists) { |
|||
missingItems.add(StringUtils.hasText(item) ? item : "(空)"); |
|||
} |
|||
if (!supplierExists) { |
|||
missingSuppliers.add(StringUtils.hasText(supplierShortName) ? supplierShortName : "(空)"); |
|||
} |
|||
if (row != null) { |
|||
row.setValid(itemExists && supplierExists); |
|||
row.setValidationMessage(buildValidationMessage(itemExists, supplierExists)); |
|||
} |
|||
} |
|||
|
|||
SupplierPriceInitialImportPreviewResultVo result = new SupplierPriceInitialImportPreviewResultVo(); |
|||
result.setRows(rows); |
|||
result.setMissingItems(new ArrayList<>(missingItems)); |
|||
result.setMissingSuppliers(new ArrayList<>(missingSuppliers)); |
|||
return result; |
|||
} |
|||
|
|||
private Set<String> normalizeLookupValues(List<String> values) { |
|||
Set<String> result = new LinkedHashSet<>(); |
|||
if (values == null) { |
|||
return result; |
|||
} |
|||
for (String value : values) { |
|||
if (StringUtils.hasText(value)) { |
|||
result.add(normalizeLookupValue(value)); |
|||
} |
|||
} |
|||
return result; |
|||
} |
|||
|
|||
private String normalizeLookupValue(String value) { |
|||
return trimToEmpty(value).toUpperCase(Locale.ENGLISH); |
|||
} |
|||
|
|||
private String buildValidationMessage(boolean itemExists, boolean supplierExists) { |
|||
if (!itemExists && !supplierExists) { |
|||
return "Item和供应商简称不存在"; |
|||
} |
|||
if (!itemExists) { |
|||
return "Item不存在"; |
|||
} |
|||
if (!supplierExists) { |
|||
return "供应商简称不存在"; |
|||
} |
|||
return ""; |
|||
} |
|||
|
|||
List<SupplierPartTierPriceItemVo> buildTierPriceList(SupplierPriceInitialImportRowVo row) { |
|||
BigDecimal[] quantities = { |
|||
row.getTierQty1(), row.getTierQty2(), row.getTierQty3(), row.getTierQty4(), |
|||
row.getTierQty5(), row.getTierQty6(), row.getTierQty7(), row.getTierQty8() |
|||
}; |
|||
BigDecimal[] prices = { |
|||
row.getPriceTier1(), row.getPriceTier2(), row.getPriceTier3(), row.getPriceTier4(), |
|||
row.getPriceTier5(), row.getPriceTier6(), row.getPriceTier7(), row.getPriceTier8() |
|||
}; |
|||
|
|||
List<SupplierPartTierPriceItemVo> result = new ArrayList<>(); |
|||
BigDecimal quantityStart = BigDecimal.ZERO; |
|||
boolean tierEnded = false; |
|||
for (int index = 0; index < quantities.length; index++) { |
|||
BigDecimal quantityEnd = quantities[index]; |
|||
BigDecimal unitPrice = prices[index]; |
|||
if (quantityEnd == null && unitPrice == null) { |
|||
tierEnded = true; |
|||
continue; |
|||
} |
|||
if (tierEnded) { |
|||
throw new RuntimeException("阶梯数量和价格不能在空档后继续填写"); |
|||
} |
|||
if (quantityEnd == null || unitPrice == null) { |
|||
throw new RuntimeException("第" + (index + 1) + "档的Tier_QTY和Price_Tier必须同时填写"); |
|||
} |
|||
if (quantityEnd.compareTo(quantityStart) < 0) { |
|||
throw new RuntimeException("第" + (index + 1) + "档数量上限必须大于上一档数量上限"); |
|||
} |
|||
if (unitPrice.compareTo(BigDecimal.ZERO) < 0) { |
|||
throw new RuntimeException("第" + (index + 1) + "档价格不能小于0"); |
|||
} |
|||
|
|||
SupplierPartTierPriceItemVo tierPrice = new SupplierPartTierPriceItemVo(); |
|||
// Keep every row at the target SQL Server scale. Mixed BigDecimal scales in a |
|||
// multi-row VALUES statement can otherwise be inferred as the lowest scale. |
|||
tierPrice.setQtyStart(quantityStart.setScale(4, RoundingMode.HALF_UP)); |
|||
tierPrice.setQtyEnd(quantityEnd.setScale(4, RoundingMode.HALF_UP)); |
|||
tierPrice.setUnitPrice(unitPrice.setScale(6, RoundingMode.HALF_UP)); |
|||
tierPrice.setRemark(""); |
|||
result.add(tierPrice); |
|||
quantityStart = quantityEnd.add(BigDecimal.ONE); |
|||
} |
|||
return result; |
|||
} |
|||
|
|||
private void archiveCurrentTierPrice(String site, |
|||
String supplierNo, |
|||
String partNo, |
|||
String submitBy, |
|||
LocalDate priceDate, |
|||
List<SupplierPartTierPriceItemVo> currentTierPrices) { |
|||
Date submitDate = new Date(); |
|||
String versionNo = generateInitialImportVersionNo(); |
|||
String attachmentRefs = resolveAttachmentRefs(currentTierPrices); |
|||
|
|||
SupplierPartTierPriceHistoryMainVo historyMain = new SupplierPartTierPriceHistoryMainVo(); |
|||
historyMain.setSite(site); |
|||
historyMain.setSupplierCode(supplierNo); |
|||
historyMain.setPartNo(partNo); |
|||
historyMain.setVersionNo(versionNo); |
|||
historyMain.setSubmitDate(submitDate); |
|||
historyMain.setPriceDate(java.sql.Date.valueOf(priceDate)); |
|||
historyMain.setSubmitBy(submitBy); |
|||
historyMain.setCurrency("USD"); |
|||
historyMain.setAttachmentRefs(attachmentRefs); |
|||
supplierPartMoqPriceMapper.insertHistoryTierPriceMain(historyMain); |
|||
if (historyMain.getId() == null) { |
|||
throw new RuntimeException("当前价格归档失败"); |
|||
} |
|||
|
|||
supplierPartMoqPriceMapper.insertHistoryTierPriceBatch( |
|||
site, |
|||
historyMain.getId(), |
|||
supplierNo, |
|||
partNo, |
|||
versionNo, |
|||
submitDate, |
|||
priceDate.toString(), |
|||
attachmentRefs, |
|||
submitBy, |
|||
currentTierPrices |
|||
); |
|||
} |
|||
|
|||
private LocalDate resolveCurrentPriceDate(List<SupplierPartTierPriceItemVo> rows) { |
|||
LocalDate priceDate = null; |
|||
for (SupplierPartTierPriceItemVo row : rows) { |
|||
if (row == null || row.getPriceDate() == null) { |
|||
throw new RuntimeException("当前价格记录的价格日期为空"); |
|||
} |
|||
LocalDate rowPriceDate = toLocalDate(row.getPriceDate()); |
|||
if (priceDate == null) { |
|||
priceDate = rowPriceDate; |
|||
} else if (!priceDate.equals(rowPriceDate)) { |
|||
throw new RuntimeException("当前价格记录存在多个价格日期,请先处理异常数据"); |
|||
} |
|||
} |
|||
return priceDate; |
|||
} |
|||
|
|||
private LocalDate toLocalDate(Date value) { |
|||
if (value instanceof java.sql.Date) { |
|||
return ((java.sql.Date) value).toLocalDate(); |
|||
} |
|||
return value.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); |
|||
} |
|||
|
|||
private LocalDate parseImportDate(String value) { |
|||
if (!StringUtils.hasText(value)) { |
|||
throw new RuntimeException("START DATE不能为空"); |
|||
} |
|||
try { |
|||
return LocalDate.parse(value.trim(), DateTimeFormatter.ISO_LOCAL_DATE); |
|||
} catch (DateTimeParseException ex) { |
|||
throw new RuntimeException("START DATE格式错误,应为yyyy-MM-dd"); |
|||
} |
|||
} |
|||
|
|||
private String resolveUniqueCode(List<String> values, String emptyMessage, String duplicateMessage) { |
|||
if (values == null || values.isEmpty()) { |
|||
throw new RuntimeException(emptyMessage); |
|||
} |
|||
if (values.size() > 1) { |
|||
throw new RuntimeException(duplicateMessage + "(" + String.join("、", values) + ")"); |
|||
} |
|||
return values.get(0); |
|||
} |
|||
|
|||
private String trimToEmpty(String value) { |
|||
return StringUtils.hasText(value) ? value.trim() : ""; |
|||
} |
|||
|
|||
private String resolveSubmitBy() { |
|||
Object principal = SecurityUtils.getSubject().getPrincipal(); |
|||
if (principal instanceof SysUserEntity && StringUtils.hasText(((SysUserEntity) principal).getUsername())) { |
|||
return ((SysUserEntity) principal).getUsername().trim(); |
|||
} |
|||
return "system"; |
|||
} |
|||
|
|||
private String resolveAttachmentRefs(List<SupplierPartTierPriceItemVo> rows) { |
|||
for (SupplierPartTierPriceItemVo row : rows) { |
|||
if (row != null && StringUtils.hasText(row.getAttachmentRefs())) { |
|||
return row.getAttachmentRefs().trim(); |
|||
} |
|||
} |
|||
return ""; |
|||
} |
|||
|
|||
private String generateInitialImportVersionNo() { |
|||
String time = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS").format(LocalDateTime.now()); |
|||
String random = UUID.randomUUID().toString().substring(0, 8).toUpperCase(Locale.ENGLISH); |
|||
return "INIT-" + time + "-" + random; |
|||
} |
|||
|
|||
private SupplierPriceInitialImportRowVo parseRow(Row row, int excelRowNo, |
|||
DataFormatter formatter, FormulaEvaluator evaluator) { |
|||
SupplierPriceInitialImportRowVo value = new SupplierPriceInitialImportRowVo(); |
|||
value.setRawCode(readText(row.getCell(0), formatter, evaluator)); |
|||
value.setSupplier(readText(row.getCell(1), formatter, evaluator)); |
|||
value.setMoqColor(readDecimal(row.getCell(2), excelRowNo, HEADERS.get(2), formatter, evaluator)); |
|||
value.setStartDate(readDate(row.getCell(3), excelRowNo, formatter, evaluator)); |
|||
value.setTierQty1(readDecimal(row.getCell(4), excelRowNo, HEADERS.get(4), formatter, evaluator)); |
|||
value.setTierQty2(readDecimal(row.getCell(5), excelRowNo, HEADERS.get(5), formatter, evaluator)); |
|||
value.setTierQty3(readDecimal(row.getCell(6), excelRowNo, HEADERS.get(6), formatter, evaluator)); |
|||
value.setTierQty4(readDecimal(row.getCell(7), excelRowNo, HEADERS.get(7), formatter, evaluator)); |
|||
value.setTierQty5(readDecimal(row.getCell(8), excelRowNo, HEADERS.get(8), formatter, evaluator)); |
|||
value.setTierQty6(readDecimal(row.getCell(9), excelRowNo, HEADERS.get(9), formatter, evaluator)); |
|||
value.setTierQty7(readDecimal(row.getCell(10), excelRowNo, HEADERS.get(10), formatter, evaluator)); |
|||
value.setTierQty8(readDecimal(row.getCell(11), excelRowNo, HEADERS.get(11), formatter, evaluator)); |
|||
value.setPriceTier1(readDecimal(row.getCell(12), excelRowNo, HEADERS.get(12), formatter, evaluator)); |
|||
value.setPriceTier2(readDecimal(row.getCell(13), excelRowNo, HEADERS.get(13), formatter, evaluator)); |
|||
value.setPriceTier3(readDecimal(row.getCell(14), excelRowNo, HEADERS.get(14), formatter, evaluator)); |
|||
value.setPriceTier4(readDecimal(row.getCell(15), excelRowNo, HEADERS.get(15), formatter, evaluator)); |
|||
value.setPriceTier5(readDecimal(row.getCell(16), excelRowNo, HEADERS.get(16), formatter, evaluator)); |
|||
value.setPriceTier6(readDecimal(row.getCell(17), excelRowNo, HEADERS.get(17), formatter, evaluator)); |
|||
value.setPriceTier7(readDecimal(row.getCell(18), excelRowNo, HEADERS.get(18), formatter, evaluator)); |
|||
value.setPriceTier8(readDecimal(row.getCell(19), excelRowNo, HEADERS.get(19), formatter, evaluator)); |
|||
return value; |
|||
} |
|||
|
|||
private void validateHeader(Row headerRow, DataFormatter formatter, FormulaEvaluator evaluator) { |
|||
if (headerRow == null) { |
|||
throw new RuntimeException("Excel第一行必须是模板表头"); |
|||
} |
|||
for (int i = 0; i < HEADERS.size(); i++) { |
|||
String actual = normalizeHeader(readText(headerRow.getCell(i), formatter, evaluator)); |
|||
String expected = normalizeHeader(HEADERS.get(i)); |
|||
if (!expected.equals(actual)) { |
|||
throw new RuntimeException("Excel第" + (i + 1) + "列表头应为 " + HEADERS.get(i)); |
|||
} |
|||
} |
|||
short lastCellNum = headerRow.getLastCellNum(); |
|||
for (int i = HEADERS.size(); i < lastCellNum; i++) { |
|||
if (StringUtils.hasText(readText(headerRow.getCell(i), formatter, evaluator))) { |
|||
throw new RuntimeException("Excel表头必须与下载模板一致,不能增加额外栏位"); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private String readText(Cell cell, DataFormatter formatter, FormulaEvaluator evaluator) { |
|||
if (cell == null) { |
|||
return null; |
|||
} |
|||
String text = formatter.formatCellValue(cell, evaluator); |
|||
return StringUtils.hasText(text) ? text.trim() : null; |
|||
} |
|||
|
|||
private BigDecimal readDecimal(Cell cell, int rowNo, String columnName, |
|||
DataFormatter formatter, FormulaEvaluator evaluator) { |
|||
String text = readText(cell, formatter, evaluator); |
|||
if (!StringUtils.hasText(text)) { |
|||
return null; |
|||
} |
|||
try { |
|||
String normalized = text.replace('\u00A0', ' ').trim(); |
|||
boolean negativeByParentheses = normalized.startsWith("(") && normalized.endsWith(")"); |
|||
normalized = normalized.replaceAll("[\\p{Sc}\\p{L}\\s]", "") |
|||
.replace(",", "") |
|||
.replace("(", "") |
|||
.replace(")", ""); |
|||
if (!normalized.matches("[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:[eE][+-]?\\d+)?")) { |
|||
throw new NumberFormatException(); |
|||
} |
|||
BigDecimal number = new BigDecimal(normalized); |
|||
return negativeByParentheses ? number.negate() : number; |
|||
} catch (NumberFormatException ex) { |
|||
throw new RuntimeException("第" + rowNo + "行 " + columnName + " 必须是数字"); |
|||
} |
|||
} |
|||
|
|||
private String readDate(Cell cell, int rowNo, DataFormatter formatter, FormulaEvaluator evaluator) { |
|||
String text = readText(cell, formatter, evaluator); |
|||
if (!StringUtils.hasText(text)) { |
|||
return null; |
|||
} |
|||
if ((cell.getCellType() == CellType.NUMERIC || cell.getCellType() == CellType.FORMULA) |
|||
&& DateUtil.isCellDateFormatted(cell)) { |
|||
return DateUtil.getLocalDateTime(cell.getNumericCellValue()).toLocalDate().toString(); |
|||
} |
|||
for (DateTimeFormatter dateFormatter : DATE_FORMATTERS) { |
|||
try { |
|||
return LocalDate.parse(text, dateFormatter).toString(); |
|||
} catch (DateTimeParseException ignored) { |
|||
// 继续尝试下一种格式。 |
|||
} |
|||
} |
|||
for (DateTimeFormatter dateTimeFormatter : DATE_TIME_FORMATTERS) { |
|||
try { |
|||
return LocalDateTime.parse(text, dateTimeFormatter).toLocalDate().toString(); |
|||
} catch (DateTimeParseException ignored) { |
|||
// 继续尝试下一种格式。 |
|||
} |
|||
} |
|||
throw new RuntimeException("第" + rowNo + "行 START DATE 日期格式无法识别"); |
|||
} |
|||
|
|||
private boolean isEmptyRow(Row row, DataFormatter formatter, FormulaEvaluator evaluator) { |
|||
if (row == null) { |
|||
return true; |
|||
} |
|||
for (int i = 0; i < HEADERS.size(); i++) { |
|||
if (StringUtils.hasText(readText(row.getCell(i), formatter, evaluator))) { |
|||
return false; |
|||
} |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
private void validateFile(MultipartFile file) { |
|||
if (file == null || file.isEmpty()) { |
|||
throw new RuntimeException("请选择Excel文件"); |
|||
} |
|||
String fileName = file.getOriginalFilename(); |
|||
String lowerName = fileName == null ? "" : fileName.toLowerCase(Locale.ENGLISH); |
|||
if (!lowerName.endsWith(".xlsx") && !lowerName.endsWith(".xls")) { |
|||
throw new RuntimeException("仅支持.xls或.xlsx文件"); |
|||
} |
|||
} |
|||
|
|||
private CellStyle createHeaderStyle(Workbook workbook) { |
|||
CellStyle style = workbook.createCellStyle(); |
|||
Font font = workbook.createFont(); |
|||
font.setBold(true); |
|||
style.setFont(font); |
|||
style.setAlignment(HorizontalAlignment.CENTER); |
|||
style.setVerticalAlignment(VerticalAlignment.CENTER); |
|||
style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex()); |
|||
style.setFillPattern(FillPatternType.SOLID_FOREGROUND); |
|||
style.setBorderTop(BorderStyle.THIN); |
|||
style.setBorderRight(BorderStyle.THIN); |
|||
style.setBorderBottom(BorderStyle.THIN); |
|||
style.setBorderLeft(BorderStyle.THIN); |
|||
return style; |
|||
} |
|||
|
|||
private int resolveColumnWidth(int columnIndex) { |
|||
if (columnIndex == 0 || columnIndex == 1) { |
|||
return 20; |
|||
} |
|||
if (columnIndex == 3) { |
|||
return 15; |
|||
} |
|||
return 14; |
|||
} |
|||
|
|||
private String normalizeHeader(String value) { |
|||
return value == null ? "" : value.replace("\uFEFF", "").trim(); |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
package com.xujie.modules.sys.vo; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 供应商价格期初导入预览结果。 |
|||
*/ |
|||
@Data |
|||
public class SupplierPriceInitialImportPreviewResultVo { |
|||
|
|||
private List<SupplierPriceInitialImportRowVo> rows = new ArrayList<>(); |
|||
|
|||
private List<String> missingItems = new ArrayList<>(); |
|||
|
|||
private List<String> missingSuppliers = new ArrayList<>(); |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
package com.xujie.modules.sys.vo; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import java.math.BigDecimal; |
|||
|
|||
/** |
|||
* 供应商价格期初导入预览行。 |
|||
*/ |
|||
@Data |
|||
public class SupplierPriceInitialImportRowVo { |
|||
|
|||
private String rawCode; |
|||
private String supplier; |
|||
private BigDecimal moqColor; |
|||
private String startDate; |
|||
private BigDecimal tierQty1; |
|||
private BigDecimal tierQty2; |
|||
private BigDecimal tierQty3; |
|||
private BigDecimal tierQty4; |
|||
private BigDecimal tierQty5; |
|||
private BigDecimal tierQty6; |
|||
private BigDecimal tierQty7; |
|||
private BigDecimal tierQty8; |
|||
private BigDecimal priceTier1; |
|||
private BigDecimal priceTier2; |
|||
private BigDecimal priceTier3; |
|||
private BigDecimal priceTier4; |
|||
private BigDecimal priceTier5; |
|||
private BigDecimal priceTier6; |
|||
private BigDecimal priceTier7; |
|||
private BigDecimal priceTier8; |
|||
private Boolean valid; |
|||
private String validationMessage; |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
package com.xujie.modules.sys.vo; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 供应商价格期初导入保存参数。 |
|||
*/ |
|||
@Data |
|||
public class SupplierPriceInitialImportSaveVo { |
|||
|
|||
private String site; |
|||
|
|||
private List<SupplierPriceInitialImportRowVo> rows; |
|||
} |
|||
@ -0,0 +1,85 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> |
|||
|
|||
<mapper namespace="com.xujie.modules.sys.dao.SysInitialImportMapper"> |
|||
|
|||
<select id="listExistingItems" resultType="java.lang.String"> |
|||
SELECT DISTINCT LTRIM(RTRIM(item)) |
|||
FROM part |
|||
WHERE site = #{site} |
|||
AND UPPER(LTRIM(RTRIM(item))) IN |
|||
<foreach collection="items" item="item" open="(" separator="," close=")"> |
|||
#{item} |
|||
</foreach> |
|||
</select> |
|||
|
|||
<select id="listExistingSupplierShortNames" resultType="java.lang.String"> |
|||
SELECT DISTINCT LTRIM(RTRIM(c_short_npc)) |
|||
FROM srm_supplier |
|||
WHERE site = #{site} |
|||
AND UPPER(LTRIM(RTRIM(c_short_npc))) IN |
|||
<foreach collection="shortNames" item="shortName" open="(" separator="," close=")"> |
|||
#{shortName} |
|||
</foreach> |
|||
</select> |
|||
|
|||
<select id="listPartNosByItem" resultType="java.lang.String"> |
|||
SELECT DISTINCT part_no |
|||
FROM part |
|||
WHERE site = #{site} |
|||
AND UPPER(LTRIM(RTRIM(item))) = UPPER(#{item}) |
|||
ORDER BY part_no |
|||
</select> |
|||
|
|||
<select id="listSupplierNosByShortNpc" resultType="java.lang.String"> |
|||
SELECT DISTINCT supplier_no |
|||
FROM srm_supplier |
|||
WHERE site = #{site} |
|||
AND UPPER(LTRIM(RTRIM(c_short_npc))) = UPPER(#{shortNpc}) |
|||
ORDER BY supplier_no |
|||
</select> |
|||
|
|||
<select id="countSupplierPart" resultType="java.lang.Integer"> |
|||
SELECT COUNT(1) |
|||
FROM SupplierforPart |
|||
WHERE site = #{site} |
|||
AND supplier_no = #{supplierNo} |
|||
AND part_no = #{partNo} |
|||
</select> |
|||
|
|||
<update id="updateSupplierPartMoq"> |
|||
UPDATE SupplierforPart |
|||
SET [minOrderQty] = #{minOrderQty} |
|||
WHERE site = #{site} |
|||
AND supplier_no = #{supplierNo} |
|||
AND part_no = #{partNo} |
|||
</update> |
|||
|
|||
<insert id="insertSupplierPart"> |
|||
INSERT INTO SupplierforPart |
|||
( |
|||
site, |
|||
supplier_no, |
|||
part_no, |
|||
active, |
|||
currency, |
|||
[minOrderQty], |
|||
new_product_audited, |
|||
need_new_product_audit, |
|||
need_submit_test |
|||
) |
|||
VALUES |
|||
( |
|||
#{site}, |
|||
#{supplierNo}, |
|||
#{partNo}, |
|||
'Y', |
|||
'USD', |
|||
#{minOrderQty}, |
|||
'Y', |
|||
'N', |
|||
'N' |
|||
) |
|||
</insert> |
|||
|
|||
</mapper> |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue