From eb76c2e10aa61b3d81129599916f7d56abe88012 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=B8=E7=86=9F=E5=90=B4=E5=BD=A6=E7=A5=96?= Date: Tue, 8 Sep 2026 12:28:20 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E4=BE=9B=E5=BA=94=E5=95=86?= =?UTF-8?q?=E4=BB=B7=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/SupplierPartMoqPriceServiceImpl.java | 7 +- .../SysInitialImportController.java | 73 ++ .../sys/dao/SysInitialImportMapper.java | 38 ++ .../sys/service/SysInitialImportService.java | 17 + .../impl/SysInitialImportServiceImpl.java | 630 ++++++++++++++++++ ...lierPriceInitialImportPreviewResultVo.java | 19 + .../vo/SupplierPriceInitialImportRowVo.java | 35 + .../vo/SupplierPriceInitialImportSaveVo.java | 16 + .../supplier/SupplierPartMoqPriceMapper.xml | 12 +- .../mapper/sys/SysInitialImportMapper.xml | 85 +++ src/main/resources/mapper/sys/SysMenuDao.xml | 20 +- 11 files changed, 932 insertions(+), 20 deletions(-) create mode 100644 src/main/java/com/xujie/modules/sys/controller/SysInitialImportController.java create mode 100644 src/main/java/com/xujie/modules/sys/dao/SysInitialImportMapper.java create mode 100644 src/main/java/com/xujie/modules/sys/service/SysInitialImportService.java create mode 100644 src/main/java/com/xujie/modules/sys/service/impl/SysInitialImportServiceImpl.java create mode 100644 src/main/java/com/xujie/modules/sys/vo/SupplierPriceInitialImportPreviewResultVo.java create mode 100644 src/main/java/com/xujie/modules/sys/vo/SupplierPriceInitialImportRowVo.java create mode 100644 src/main/java/com/xujie/modules/sys/vo/SupplierPriceInitialImportSaveVo.java create mode 100644 src/main/resources/mapper/sys/SysInitialImportMapper.xml diff --git a/src/main/java/com/xujie/modules/supplier/service/impl/SupplierPartMoqPriceServiceImpl.java b/src/main/java/com/xujie/modules/supplier/service/impl/SupplierPartMoqPriceServiceImpl.java index 1d626dd..1fdbc32 100644 --- a/src/main/java/com/xujie/modules/supplier/service/impl/SupplierPartMoqPriceServiceImpl.java +++ b/src/main/java/com/xujie/modules/supplier/service/impl/SupplierPartMoqPriceServiceImpl.java @@ -30,6 +30,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import java.math.BigDecimal; +import java.math.RoundingMode; import java.text.SimpleDateFormat; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; @@ -479,9 +480,9 @@ public class SupplierPartMoqPriceServiceImpl implements ISupplierPartMoqPriceSer private SupplierPartTierPriceItemVo normalizeTierRow(SupplierPartTierPriceItemVo row) { SupplierPartTierPriceItemVo normalizedRow = new SupplierPartTierPriceItemVo(); - normalizedRow.setQtyStart(row.getQtyStart()); - normalizedRow.setQtyEnd(row.getQtyEnd()); - normalizedRow.setUnitPrice(row.getUnitPrice()); + normalizedRow.setQtyStart(row.getQtyStart().setScale(4, RoundingMode.HALF_UP)); + normalizedRow.setQtyEnd(row.getQtyEnd().setScale(4, RoundingMode.HALF_UP)); + normalizedRow.setUnitPrice(row.getUnitPrice().setScale(6, RoundingMode.HALF_UP)); normalizedRow.setRemark(StringUtils.hasText(row.getRemark()) ? row.getRemark().trim() : ""); return normalizedRow; } diff --git a/src/main/java/com/xujie/modules/sys/controller/SysInitialImportController.java b/src/main/java/com/xujie/modules/sys/controller/SysInitialImportController.java new file mode 100644 index 0000000..abb6d1f --- /dev/null +++ b/src/main/java/com/xujie/modules/sys/controller/SysInitialImportController.java @@ -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 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()); + } + } +} diff --git a/src/main/java/com/xujie/modules/sys/dao/SysInitialImportMapper.java b/src/main/java/com/xujie/modules/sys/dao/SysInitialImportMapper.java new file mode 100644 index 0000000..015794a --- /dev/null +++ b/src/main/java/com/xujie/modules/sys/dao/SysInitialImportMapper.java @@ -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 listExistingItems(@Param("site") String site, @Param("items") List items); + + List listExistingSupplierShortNames(@Param("site") String site, + @Param("shortNames") List shortNames); + + List listPartNosByItem(@Param("site") String site, @Param("item") String item); + + List 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); +} diff --git a/src/main/java/com/xujie/modules/sys/service/SysInitialImportService.java b/src/main/java/com/xujie/modules/sys/service/SysInitialImportService.java new file mode 100644 index 0000000..9cc8758 --- /dev/null +++ b/src/main/java/com/xujie/modules/sys/service/SysInitialImportService.java @@ -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); +} diff --git a/src/main/java/com/xujie/modules/sys/service/impl/SysInitialImportServiceImpl.java b/src/main/java/com/xujie/modules/sys/service/impl/SysInitialImportServiceImpl.java new file mode 100644 index 0000000..0526fe2 --- /dev/null +++ b/src/main/java/com/xujie/modules/sys/service/impl/SysInitialImportServiceImpl.java @@ -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 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 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 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 result = new ArrayList(); + 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 partNos = initialImportMapper.listPartNosByItem(site, item); + List 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 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 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 rows) { + Set requestedItems = new LinkedHashSet<>(); + Set 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 existingItems = normalizeLookupValues(requestedItems.isEmpty() + ? new ArrayList<>() + : initialImportMapper.listExistingItems(site, new ArrayList<>(requestedItems))); + Set existingSupplierShortNames = normalizeLookupValues(requestedSupplierShortNames.isEmpty() + ? new ArrayList<>() + : initialImportMapper.listExistingSupplierShortNames( + site, new ArrayList<>(requestedSupplierShortNames))); + + Set missingItems = new LinkedHashSet<>(); + Set 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 normalizeLookupValues(List values) { + Set 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 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 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 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 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 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 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(); + } +} diff --git a/src/main/java/com/xujie/modules/sys/vo/SupplierPriceInitialImportPreviewResultVo.java b/src/main/java/com/xujie/modules/sys/vo/SupplierPriceInitialImportPreviewResultVo.java new file mode 100644 index 0000000..7d3a41a --- /dev/null +++ b/src/main/java/com/xujie/modules/sys/vo/SupplierPriceInitialImportPreviewResultVo.java @@ -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 rows = new ArrayList<>(); + + private List missingItems = new ArrayList<>(); + + private List missingSuppliers = new ArrayList<>(); +} diff --git a/src/main/java/com/xujie/modules/sys/vo/SupplierPriceInitialImportRowVo.java b/src/main/java/com/xujie/modules/sys/vo/SupplierPriceInitialImportRowVo.java new file mode 100644 index 0000000..59d0bec --- /dev/null +++ b/src/main/java/com/xujie/modules/sys/vo/SupplierPriceInitialImportRowVo.java @@ -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; +} diff --git a/src/main/java/com/xujie/modules/sys/vo/SupplierPriceInitialImportSaveVo.java b/src/main/java/com/xujie/modules/sys/vo/SupplierPriceInitialImportSaveVo.java new file mode 100644 index 0000000..8bfa943 --- /dev/null +++ b/src/main/java/com/xujie/modules/sys/vo/SupplierPriceInitialImportSaveVo.java @@ -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 rows; +} diff --git a/src/main/resources/mapper/supplier/SupplierPartMoqPriceMapper.xml b/src/main/resources/mapper/supplier/SupplierPartMoqPriceMapper.xml index 8288a9f..d2f9431 100644 --- a/src/main/resources/mapper/supplier/SupplierPartMoqPriceMapper.xml +++ b/src/main/resources/mapper/supplier/SupplierPartMoqPriceMapper.xml @@ -292,9 +292,9 @@ #{site}, #{supplierCode}, #{partNo}, - #{item.qtyStart}, - #{item.qtyEnd}, - #{item.unitPrice}, + CAST(#{item.qtyStart,jdbcType=DECIMAL} AS DECIMAL(18,4)), + CAST(#{item.qtyEnd,jdbcType=DECIMAL} AS DECIMAL(18,4)), + CAST(#{item.unitPrice,jdbcType=DECIMAL} AS DECIMAL(18,6)), CAST(#{priceDate} AS DATETIME), #{attachmentRefs}, #{item.remark}, @@ -368,9 +368,9 @@ #{versionNo}, #{submitDate}, #{submitBy}, - #{item.qtyStart}, - #{item.qtyEnd}, - #{item.unitPrice}, + CAST(#{item.qtyStart,jdbcType=DECIMAL} AS DECIMAL(18,4)), + CAST(#{item.qtyEnd,jdbcType=DECIMAL} AS DECIMAL(18,4)), + CAST(#{item.unitPrice,jdbcType=DECIMAL} AS DECIMAL(18,6)), CAST(#{priceDate} AS DATETIME), #{attachmentRefs}, #{item.remark}, diff --git a/src/main/resources/mapper/sys/SysInitialImportMapper.xml b/src/main/resources/mapper/sys/SysInitialImportMapper.xml new file mode 100644 index 0000000..9525613 --- /dev/null +++ b/src/main/resources/mapper/sys/SysInitialImportMapper.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + UPDATE SupplierforPart + SET [minOrderQty] = #{minOrderQty} + WHERE site = #{site} + AND supplier_no = #{supplierNo} + AND part_no = #{partNo} + + + + 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' + ) + + + diff --git a/src/main/resources/mapper/sys/SysMenuDao.xml b/src/main/resources/mapper/sys/SysMenuDao.xml index 5e405f9..b4a2425 100644 --- a/src/main/resources/mapper/sys/SysMenuDao.xml +++ b/src/main/resources/mapper/sys/SysMenuDao.xml @@ -108,17 +108,15 @@ sm.url, sm.perms, sm.type, sm.icon, sm.order_num FROM sys_menu sm LEFT JOIN sys_object_language_menu sml ON sm.menu_id = sml.menu_id and sml.language_code = #{language} - LEFT JOIN sys_role_menu srm ON srm.menu_id = sm.menu_id - LEFT JOIN sys_user_role sur ON sur.role_id = srm.role_id - LEFT JOIN sys_user su ON su.user_id = sur.user_id - - srm.menu_id = sm.menu_id AND sur.role_id = srm.role_id AND su.user_id = sur.user_id - AND sm.menu_type = #{menuType} - - AND su.user_id = #{userId} - - + WHERE sm.menu_type = #{menuType} + AND EXISTS ( + SELECT 1 + FROM sys_user_role sur + INNER JOIN sys_role_menu srm ON srm.role_id = sur.role_id + WHERE sur.user_id = #{userId} + AND srm.menu_id = sm.menu_id + ) ORDER BY sm.order_num ASC - \ No newline at end of file +