diff --git a/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/ProductController.java b/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/ProductController.java index ac4aa212..6cf7032f 100644 --- a/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/ProductController.java +++ b/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/ProductController.java @@ -1,7 +1,9 @@ package com.bms.web.controller.price; import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; @@ -20,12 +22,14 @@ import com.bms.common.core.controller.BaseController; import com.bms.common.core.domain.AjaxResult; import com.bms.common.core.page.TableDataInfo; import com.bms.common.enums.BusinessType; +import com.bms.common.utils.MessageUtils; import com.bms.price.domain.BmsProduct; import com.bms.price.domain.BmsProductCountry; +import com.bms.price.domain.BmsProductImportTask; import com.bms.price.service.IBmsProductService; /** - * 产品列表(含 Excel 导入) + * Product list (async Excel import) * * @author bms */ @@ -45,6 +49,17 @@ public class ProductController extends BaseController return getDataTable(list); } + @PreAuthorize("@ss.hasPermi('price:product:import')") + @GetMapping("/import/task/{taskId}") + public AjaxResult getImportTask(@PathVariable String taskId) + { + BmsProductImportTask task = productService.getImportTask(taskId); + if (task == null) + { + return error(MessageUtils.message("price.product.import.task.notExists")); + } + return success(task); + } @PreAuthorize("@ss.hasPermi('price:product:query')") @GetMapping("/{productCode}/countries") @@ -64,7 +79,7 @@ public class ProductController extends BaseController } @PreAuthorize("@ss.hasPermi('price:product:add')") - @Log(title = "产品列表", businessType = BusinessType.INSERT) + @Log(title = "\u4ea7\u54c1\u5217\u8868", businessType = BusinessType.INSERT) @PostMapping public AjaxResult add(@Validated @RequestBody BmsProduct product) { @@ -73,7 +88,7 @@ public class ProductController extends BaseController } @PreAuthorize("@ss.hasPermi('price:product:edit')") - @Log(title = "产品列表", businessType = BusinessType.UPDATE) + @Log(title = "\u4ea7\u54c1\u5217\u8868", businessType = BusinessType.UPDATE) @PutMapping public AjaxResult edit(@Validated @RequestBody BmsProduct product) { @@ -81,11 +96,8 @@ public class ProductController extends BaseController return toAjax(productService.updateBmsProduct(product)); } - /** - * 修改产品国家明细行(重量段/运费/挂号费/尺寸/尾程/时效/生效失效日期) - */ @PreAuthorize("@ss.hasPermi('price:product:edit')") - @Log(title = "产品国家明细", businessType = BusinessType.UPDATE) + @Log(title = "\u4ea7\u54c1\u56fd\u5bb6\u660e\u7ec6", businessType = BusinessType.UPDATE) @PutMapping("/country") public AjaxResult editCountry(@RequestBody BmsProductCountry country) { @@ -93,7 +105,7 @@ public class ProductController extends BaseController } @PreAuthorize("@ss.hasPermi('price:product:remove')") - @Log(title = "产品列表", businessType = BusinessType.DELETE) + @Log(title = "\u4ea7\u54c1\u5217\u8868", businessType = BusinessType.DELETE) @DeleteMapping("/{productIds}") public AjaxResult remove(@PathVariable Long[] productIds) { @@ -101,26 +113,30 @@ public class ProductController extends BaseController } /** - * 导入威狮B2C小包价表.xlsx(产品目录 + 国家服务明细) + * Async import Weishi B2C workbook. Returns taskId; poll /import/task/{taskId}. */ - @Log(title = "产品列表", businessType = BusinessType.IMPORT) + @Log(title = "\u4ea7\u54c1\u5217\u8868", businessType = BusinessType.IMPORT) @PreAuthorize("@ss.hasPermi('price:product:import')") @PostMapping("/importData") public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception { - String message = productService.importProductExcel(file, updateSupport, getUsername()); - return success(message); + String taskId = productService.submitImportProductExcelAsync(file, updateSupport, getUsername()); + Map payload = new HashMap(); + payload.put("taskId", taskId); + return success(payload); } /** - * Import a single-country linehaul cost sheet. countryCode is the destination. + * Async single-country cost-sheet import. Returns taskId; poll /import/task/{taskId}. */ @Log(title = "\u4ea7\u54c1\u5217\u8868", businessType = BusinessType.IMPORT) @PreAuthorize("@ss.hasPermi('price:product:import')") @PostMapping("/importCountryData") public AjaxResult importCountryData(MultipartFile file, String countryCode) throws Exception { - String message = productService.importCountryProductExcel(file, countryCode, getUsername()); - return success(message); + String taskId = productService.submitImportCountryProductExcelAsync(file, countryCode, getUsername()); + Map payload = new HashMap(); + payload.put("taskId", taskId); + return success(payload); } } diff --git a/bms-back/bms-admin/src/main/resources/i18n/messages.properties b/bms-back/bms-admin/src/main/resources/i18n/messages.properties index 6faec538..a03777a5 100644 --- a/bms-back/bms-admin/src/main/resources/i18n/messages.properties +++ b/bms-back/bms-admin/src/main/resources/i18n/messages.properties @@ -127,6 +127,7 @@ price.product.import.countryNotFound=国家不存在或已停用:{0} price.product.import.countrySheetNotFound=未找到「产品目录」sheet,请上传单国专线成本价表 price.product.import.countryNoPrice=未找到该产品的重量段价格:{0} price.product.import.countrySuccess=按国家导入成功({0}):新增 {1} 个产品,跳过 {2} 个(同代码同生效日不覆盖主数据);写入重量段 {3} 行。 +price.product.import.task.notExists=任务不存在或已过期 # 报价试算 TmsPriceQuote(对外 TMS API 错误码,保持英文契约) price.quote.postcodeRequired=该国家/产品有分区,请填写目的邮编 price.quote.postcodeZoneNotFound=邮编未匹配到分区,请核对目的邮编 diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/BmsProductImportTask.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/BmsProductImportTask.java new file mode 100644 index 00000000..5a842a1b --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/BmsProductImportTask.java @@ -0,0 +1,72 @@ +package com.bms.price.domain; + +import java.io.Serializable; + +/** + * Async product Excel import progress (Redis). + */ +public class BmsProductImportTask implements Serializable +{ + private static final long serialVersionUID = 1L; + + public static final String STATUS_PENDING = "PENDING"; + public static final String STATUS_RUNNING = "RUNNING"; + public static final String STATUS_SUCCESS = "SUCCESS"; + public static final String STATUS_FAIL = "FAIL"; + + private String taskId; + private String status; + private int percent; + private String message; + private String error; + + public String getTaskId() + { + return taskId; + } + + public void setTaskId(String taskId) + { + this.taskId = taskId; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + public int getPercent() + { + return percent; + } + + public void setPercent(int percent) + { + this.percent = percent; + } + + public String getMessage() + { + return message; + } + + public void setMessage(String message) + { + this.message = message; + } + + public String getError() + { + return error; + } + + public void setError(String error) + { + this.error = error; + } +} diff --git a/bms-back/bms-system/src/main/java/com/bms/price/mapper/BmsProductCountryMapper.java b/bms-back/bms-system/src/main/java/com/bms/price/mapper/BmsProductCountryMapper.java index f40d717b..5eb90276 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/mapper/BmsProductCountryMapper.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/mapper/BmsProductCountryMapper.java @@ -39,6 +39,16 @@ public interface BmsProductCountryMapper @Param("productCode") String productCode, @Param("newEffectiveDate") Date newEffectiveDate); + /** + * Close open rows for one product + country where effective_date < newEffectiveDate. + * Match by country_code when present, otherwise country_name. + */ + public int expireOpenByProductCodeAndCountry( + @Param("productCode") String productCode, + @Param("countryCode") String countryCode, + @Param("countryName") String countryName, + @Param("newEffectiveDate") Date newEffectiveDate); + /** Recompute status from effective_date / expire_date vs today */ public int refreshStatusByDates(); @@ -47,6 +57,16 @@ public interface BmsProductCountryMapper @Param("productCode") String productCode, @Param("effectiveDate") Date effectiveDate); + /** + * Delete open rows for one product + country on a given effective_date (same-day replace). + * Needed because unique key keeps expired same-day weight bands. + */ + public int deleteOpenByProductCodeAndCountryOnDate( + @Param("productCode") String productCode, + @Param("countryCode") String countryCode, + @Param("countryName") String countryName, + @Param("effectiveDate") Date effectiveDate); + /** Open country rows (expire_date is null) for product */ public List selectOpenByProductCode(@Param("productCode") String productCode); diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/IBmsProductService.java b/bms-back/bms-system/src/main/java/com/bms/price/service/IBmsProductService.java index b75e2ae3..1aa8333f 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/IBmsProductService.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/IBmsProductService.java @@ -5,9 +5,10 @@ import java.util.List; import org.springframework.web.multipart.MultipartFile; import com.bms.price.domain.BmsProduct; import com.bms.price.domain.BmsProductCountry; +import com.bms.price.domain.BmsProductImportTask; /** - * 产品列表 服务层 + * Product list service * * @author bms */ @@ -21,9 +22,6 @@ public interface IBmsProductService public int updateBmsProduct(BmsProduct product); - /** - * 修改产品国家明细行(重量段/运费/挂号费/尺寸/尾程/时效/生效失效日期),状态按日期重算 - */ public int updateBmsProductCountry(BmsProductCountry country); public int deleteBmsProductByIds(Long[] productIds); @@ -34,34 +32,26 @@ public interface IBmsProductService public List selectCountryListByProductCode(String productCode, Date effectiveDate, Long productId); - /** - * 导入威狮B2C小包价表:产品目录 + 各产品 sheet 国家×重量段(含运费/挂号费/尺寸/尾程/时效) - * - * @param file 上传的 xlsx/xls - * @param updateSupport unused; same code+date never overwrites, different date always inserts - * @param operName 操作人 - * @return 导入结果说明(可含 HTML 换行) - */ public String importProductExcel(MultipartFile file, boolean updateSupport, String operName) throws Exception; - /** - * Import a single-country cost sheet (catalog + product weight rows). - * The destination country is chosen by the user, not read from the sheet. - * - * @param file uploaded xlsx/xls - * @param countryCode ISO country code (region.country_code_2) - * @param operName operator - * @return HTML result message - */ public String importCountryProductExcel(MultipartFile file, String countryCode, String operName) throws Exception; + /** Submit async all-country import; returns taskId for polling getImportTask. */ + public String submitImportProductExcelAsync(MultipartFile file, boolean updateSupport, String operName) throws Exception; + + /** Submit async single-country import; returns taskId for polling getImportTask. */ + public String submitImportCountryProductExcelAsync(MultipartFile file, String countryCode, String operName) throws Exception; + + /** Poll Redis progress for product import task. */ + public BmsProductImportTask getImportTask(String taskId); + + /** Sync import from bytes (used by async runner; taskId may be null). */ + public String importProductExcel(byte[] bytes, String filename, boolean updateSupport, String operName, String taskId) throws Exception; + + /** Sync country import from bytes (used by async runner; taskId may be null). */ + public String importCountryProductExcel(byte[] bytes, String filename, String countryCode, String operName, String taskId) throws Exception; - /** - * Parse Weishi B2C quote Excel into productCode -> country/zone/weight price rows (no persist). - * Used by price-set detail price import. - */ public java.util.Map> parseQuoteExcelByProduct(MultipartFile file) throws Exception; - /** Parse quote Excel from in-memory bytes (async import). */ public java.util.Map> parseQuoteExcelByProduct(byte[] bytes, String filename) throws Exception; } diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/BmsProductServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/BmsProductServiceImpl.java index 9cf9ebdc..2453fb53 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/BmsProductServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/BmsProductServiceImpl.java @@ -33,14 +33,17 @@ import org.springframework.web.multipart.MultipartFile; import com.bms.common.exception.ServiceException; import com.bms.common.utils.MessageUtils; import com.bms.common.utils.StringUtils; +import com.bms.common.utils.uuid.IdUtils; import com.bms.price.domain.BmsProduct; import com.bms.price.domain.BmsProductCountry; +import com.bms.price.domain.BmsProductImportTask; import com.bms.price.domain.PrcZone; import com.bms.price.mapper.BmsProductCountryMapper; import com.bms.price.mapper.BmsProductMapper; import com.bms.price.mapper.PrcZoneMapper; import com.bms.price.service.IBmsProductService; import com.bms.price.service.IPrcSurchargeRelService; +import com.bms.price.task.BmsProductImportAsyncRunner; import com.bms.price.util.ZoneCodeAbbrevs; import com.bms.system.domain.BmsRegion; import com.bms.system.mapper.BmsRegionMapper; @@ -88,6 +91,9 @@ public class BmsProductServiceImpl implements IBmsProductService @Autowired private PlatformTransactionManager transactionManager; + @Autowired + private BmsProductImportAsyncRunner importAsyncRunner; + @Override public BmsProduct selectBmsProductById(Long productId) { @@ -240,18 +246,39 @@ public class BmsProductServiceImpl implements IBmsProductService @Override public String importProductExcel(MultipartFile file, boolean updateSupport, String operName) throws Exception { - if (file == null || file.isEmpty()) - { - throw new ServiceException(MessageUtils.message("price.product.import.fileEmpty")); - } - String filename = file.getOriginalFilename(); - if (filename == null || !(filename.toLowerCase(Locale.ROOT).endsWith(".xlsx") - || filename.toLowerCase(Locale.ROOT).endsWith(".xls"))) - { - throw new ServiceException(MessageUtils.message("price.product.import.fileType")); - } + validateImportFile(file); + return importProductExcel(file.getBytes(), file.getOriginalFilename(), updateSupport, operName, null); + } - try (InputStream is = file.getInputStream(); Workbook workbook = WorkbookFactory.create(is)) + @Override + public String submitImportProductExcelAsync(MultipartFile file, boolean updateSupport, String operName) throws Exception + { + validateImportFile(file); + byte[] bytes = file.getBytes(); + String filename = file.getOriginalFilename(); + String taskId = IdUtils.fastSimpleUUID(); + importAsyncRunner.put(taskId, BmsProductImportTask.STATUS_PENDING, 0, "queued", null); + importAsyncRunner.runImportAll(taskId, bytes, filename, updateSupport, operName); + return taskId; + } + + @Override + public BmsProductImportTask getImportTask(String taskId) + { + if (StringUtils.isEmpty(taskId) || importAsyncRunner == null) + { + return null; + } + return importAsyncRunner.get(taskId); + } + + @Override + public String importProductExcel(byte[] bytes, String filename, boolean updateSupport, String operName, String taskId) throws Exception + { + validateImportBytes(bytes, filename); + reportImportProgress(taskId, 10, "parsing excel"); + + try (InputStream is = new java.io.ByteArrayInputStream(bytes); Workbook workbook = WorkbookFactory.create(is)) { Sheet catalog = workbook.getSheet(CATALOG_SHEET); if (catalog == null) @@ -265,6 +292,7 @@ public class BmsProductServiceImpl implements IBmsProductService throw new ServiceException(MessageUtils.message("price.product.import.noData")); } products = dedupeCatalogProducts(products); + reportImportProgress(taskId, 20, "parsing excel"); Map regionByNameCn = loadRegionByNameCn(); if (regionByNameCn.isEmpty()) @@ -279,9 +307,13 @@ public class BmsProductServiceImpl implements IBmsProductService int countryNum = 0; Set unmatchedCountries = new HashSet<>(); StringBuilder failureMsg = new StringBuilder(); + int total = products.size(); + int index = 0; for (BmsProduct product : products) { + index++; + reportImportProductProgress(taskId, index, total, product.getProductCode()); DefaultTransactionDefinition def = new DefaultTransactionDefinition(); def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); TransactionStatus tx = transactionManager.getTransaction(def); @@ -306,6 +338,7 @@ public class BmsProductServiceImpl implements IBmsProductService } } + reportImportProgress(taskId, 95, "finishing"); if (failureMsg.length() > 0) { // Do not throw: each product already committed/rolled back independently @@ -326,16 +359,30 @@ public class BmsProductServiceImpl implements IBmsProductService @Override public String importCountryProductExcel(MultipartFile file, String countryCode, String operName) throws Exception { - if (file == null || file.isEmpty()) + validateImportFile(file); + return importCountryProductExcel(file.getBytes(), file.getOriginalFilename(), countryCode, operName, null); + } + + @Override + public String submitImportCountryProductExcelAsync(MultipartFile file, String countryCode, String operName) throws Exception + { + validateImportFile(file); + if (StringUtils.isEmpty(countryCode)) { - throw new ServiceException(MessageUtils.message("price.product.import.fileEmpty")); + throw new ServiceException(MessageUtils.message("price.product.import.countryRequired")); } + byte[] bytes = file.getBytes(); String filename = file.getOriginalFilename(); - if (filename == null || !(filename.toLowerCase(Locale.ROOT).endsWith(".xlsx") - || filename.toLowerCase(Locale.ROOT).endsWith(".xls"))) - { - throw new ServiceException(MessageUtils.message("price.product.import.fileType")); - } + String taskId = IdUtils.fastSimpleUUID(); + importAsyncRunner.put(taskId, BmsProductImportTask.STATUS_PENDING, 0, "queued", null); + importAsyncRunner.runImportCountry(taskId, bytes, filename, countryCode, operName); + return taskId; + } + + @Override + public String importCountryProductExcel(byte[] bytes, String filename, String countryCode, String operName, String taskId) throws Exception + { + validateImportBytes(bytes, filename); if (StringUtils.isEmpty(countryCode)) { throw new ServiceException(MessageUtils.message("price.product.import.countryRequired")); @@ -346,8 +393,9 @@ public class BmsProductServiceImpl implements IBmsProductService { throw new ServiceException(MessageUtils.message("price.product.import.countryNotFound", countryCode)); } + reportImportProgress(taskId, 10, "parsing excel"); - try (InputStream is = file.getInputStream(); Workbook workbook = WorkbookFactory.create(is)) + try (InputStream is = new java.io.ByteArrayInputStream(bytes); Workbook workbook = WorkbookFactory.create(is)) { Sheet catalog = workbook.getSheet(CATALOG_SHEET); if (catalog == null) @@ -361,6 +409,7 @@ public class BmsProductServiceImpl implements IBmsProductService } products = dedupeCatalogProducts(products); Map priceBlocks = parseCountryPriceBlocks(workbook); + reportImportProgress(taskId, 20, "parsing excel"); int successNum = 0; int skipNum = 0; @@ -368,9 +417,13 @@ public class BmsProductServiceImpl implements IBmsProductService StringBuilder failureMsg = new StringBuilder(); String countryLabel = StringUtils.isNotEmpty(region.getCountryNameCn()) ? region.getCountryNameCn() : region.getCountryCode2(); + int total = products.size(); + int index = 0; for (BmsProduct product : products) { + index++; + reportImportProductProgress(taskId, index, total, product.getProductCode()); DefaultTransactionDefinition def = new DefaultTransactionDefinition(); def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); TransactionStatus tx = transactionManager.getTransaction(def); @@ -407,6 +460,7 @@ public class BmsProductServiceImpl implements IBmsProductService } } + reportImportProgress(taskId, 95, "finishing"); if (failureMsg.length() > 0) { return MessageUtils.message("price.product.import.partialFail", @@ -417,6 +471,58 @@ public class BmsProductServiceImpl implements IBmsProductService } } + private void validateImportFile(MultipartFile file) + { + if (file == null || file.isEmpty()) + { + throw new ServiceException(MessageUtils.message("price.product.import.fileEmpty")); + } + validateImportFilename(file.getOriginalFilename()); + } + + private void validateImportBytes(byte[] bytes, String filename) + { + if (bytes == null || bytes.length == 0) + { + throw new ServiceException(MessageUtils.message("price.product.import.fileEmpty")); + } + validateImportFilename(filename); + } + + private void validateImportFilename(String filename) + { + if (filename == null || !(filename.toLowerCase(Locale.ROOT).endsWith(".xlsx") + || filename.toLowerCase(Locale.ROOT).endsWith(".xls"))) + { + throw new ServiceException(MessageUtils.message("price.product.import.fileType")); + } + } + + private void reportImportProductProgress(String taskId, int index, int total, String productCode) + { + if (total <= 0) + { + reportImportProgress(taskId, 50, "importing"); + return; + } + int percent = 20 + (int) Math.floor(70.0 * index / total); + if (percent > 90) + { + percent = 90; + } + String code = StringUtils.isEmpty(productCode) ? "-" : productCode; + reportImportProgress(taskId, percent, "importing " + code + " (" + index + "/" + total + ")"); + } + + private void reportImportProgress(String taskId, int percent, String message) + { + if (StringUtils.isEmpty(taskId) || importAsyncRunner == null) + { + return; + } + importAsyncRunner.put(taskId, BmsProductImportTask.STATUS_RUNNING, percent, message, null); + } + @Override public Map> parseQuoteExcelByProduct(MultipartFile file) throws Exception { @@ -546,10 +652,12 @@ public class BmsProductServiceImpl implements IBmsProductService /** - * Save country rows by effective_date: - * - newEff > last open: expire previous open rows, copy them to newEff, then match import (hit=update, miss=insert) - * - newEff equals last open: match update/insert on open rows - * - newEff < last open: reject + * Save country rows by effective_date. + * For each product_code + country in the Excel: + * - expire all earlier open weight bands under that country + * - delete same-day open weight bands (unique key blocks insert if only expired) + * - insert Excel weight bands only (no copy / merge of old bands) + * Reject when newEff < last open effective_date. */ private int saveCountries(BmsProduct product, List countries) { @@ -576,69 +684,33 @@ public class BmsProductServiceImpl implements IBmsProductService formatDate(newEff), formatDate(lastOpenEff))); } - boolean newVersionDay = lastOpenEff == null || newEff.after(lastOpenEff); - if (newVersionDay && lastOpenEff != null) - { - String newStatus = resolveStoredStatus(newEff, null); - // Copy open rows first (effective_date < newEff), then expire the previous open set. - countryMapper.copyOpenCountries(productCode, productId, newEff, newStatus); - countryMapper.expireOpenByProductCode(productCode, newEff); - } - - List baseRows = countryMapper.selectOpenByProductCode(productCode); - if (baseRows == null) - { - baseRows = new ArrayList<>(); - } - // keep only rows for this effective date when same-day / after copy - Map byKey = new HashMap<>(); - for (BmsProductCountry row : baseRows) - { - Date rowEff = toDateOnly(row.getEffectiveDate()); - if (rowEff != null && !rowEff.equals(newEff)) - { - continue; - } - byKey.put(countryMatchKey(row), row); - } - + Set replacedCountries = new HashSet<>(); + String rowStatus = resolveStoredStatus(newEff, null); int touched = 0; for (BmsProductCountry incoming : countries) { + if (incoming == null) + { + continue; + } + String countryKey = countryIdentityKey(incoming); + if (StringUtils.isNotEmpty(countryKey) && !replacedCountries.contains(countryKey)) + { + countryMapper.expireOpenByProductCodeAndCountry( + productCode, incoming.getCountryCode(), incoming.getCountryName(), newEff); + countryMapper.deleteOpenByProductCodeAndCountryOnDate( + productCode, incoming.getCountryCode(), incoming.getCountryName(), newEff); + replacedCountries.add(countryKey); + } + incoming.setProductId(productId); incoming.setProductCode(productCode); incoming.setEffectiveDate(newEff); incoming.setExpireDate(null); - incoming.setStatus(resolveStoredStatus(newEff, null)); - - String key = countryMatchKey(incoming); - BmsProductCountry existing = byKey.get(key); + incoming.setStatus(rowStatus); try { - if (existing != null) - { - existing.setProductId(productId); - existing.setCountryCode(incoming.getCountryCode()); - existing.setZoneId(incoming.getZoneId()); - existing.setZoneCode(incoming.getZoneCode()); - existing.setZoneName(incoming.getZoneName()); - existing.setWeightMin(incoming.getWeightMin()); - existing.setWeightMax(incoming.getWeightMax()); - existing.setFreightPrice(incoming.getFreightPrice()); - existing.setRegistrationFee(incoming.getRegistrationFee()); - existing.setSizeLimit(incoming.getSizeLimit()); - existing.setLastMile(incoming.getLastMile()); - existing.setLeadTime(incoming.getLeadTime()); - existing.setEffectiveDate(newEff); - existing.setExpireDate(null); - existing.setStatus(resolveStoredStatus(newEff, null)); - countryMapper.updateBmsProductCountryImport(existing); - } - else - { - countryMapper.insertBmsProductCountry(incoming); - byKey.put(key, incoming); - } + countryMapper.insertBmsProductCountry(incoming); touched++; } catch (Exception e) @@ -660,23 +732,22 @@ public class BmsProductServiceImpl implements IBmsProductService return touched; } - /** Match key: country + zone + weight band */ - private String countryMatchKey(BmsProductCountry row) + /** Identity for replace scope: prefer country_code, else country_name. */ + private String countryIdentityKey(BmsProductCountry row) { - String country = row.getCountryName() == null ? "" : row.getCountryName().trim(); - String zone = row.getZoneCode() == null ? "" : row.getZoneCode().trim().toUpperCase(Locale.ROOT); - String wMin = normalizeDecimal(row.getWeightMin()); - String wMax = normalizeDecimal(row.getWeightMax()); - return country + "|" + zone + "|" + wMin + "|" + wMax; - } - - private String normalizeDecimal(BigDecimal v) - { - if (v == null) + if (row == null) { return ""; } - return v.stripTrailingZeros().toPlainString(); + if (StringUtils.isNotEmpty(row.getCountryCode())) + { + return "C:" + row.getCountryCode().trim().toUpperCase(Locale.ROOT); + } + if (StringUtils.isNotEmpty(row.getCountryName())) + { + return "N:" + row.getCountryName().trim(); + } + return ""; } private Date resolveImportEffectiveDate(List countries) diff --git a/bms-back/bms-system/src/main/java/com/bms/price/task/BmsProductImportAsyncRunner.java b/bms-back/bms-system/src/main/java/com/bms/price/task/BmsProductImportAsyncRunner.java new file mode 100644 index 00000000..c7f8dc74 --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/task/BmsProductImportAsyncRunner.java @@ -0,0 +1,92 @@ +package com.bms.price.task; + +import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import com.bms.common.core.redis.RedisCache; +import com.bms.common.utils.StringUtils; +import com.bms.price.domain.BmsProductImportTask; +import com.bms.price.service.IBmsProductService; + +/** + * Async runner for product Excel import (avoids HTTP timeout on large workbooks). + */ +@Component +public class BmsProductImportAsyncRunner +{ + private static final Logger log = LoggerFactory.getLogger(BmsProductImportAsyncRunner.class); + + public static final String CACHE_KEY_PREFIX = "price:product:import:task:"; + public static final int TTL_MINUTES = 60; + + @Autowired + private IBmsProductService productService; + + @Autowired + private RedisCache redisCache; + + /** Background all-country (Weishi catalog) import. */ + @Async("threadPoolTaskExecutor") + public void runImportAll(String taskId, byte[] bytes, String filename, boolean updateSupport, String operName) + { + try + { + put(taskId, BmsProductImportTask.STATUS_RUNNING, 5, "parsing excel", null); + String message = productService.importProductExcel(bytes, filename, updateSupport, operName, taskId); + put(taskId, BmsProductImportTask.STATUS_SUCCESS, 100, message, null); + log.info("async product import done, taskId={}", taskId); + } + catch (Exception e) + { + log.error("async product import failed, taskId={}", taskId, e); + put(taskId, BmsProductImportTask.STATUS_FAIL, 100, "failed", errorMessage(e)); + } + } + + /** Background single-country cost-sheet import. */ + @Async("threadPoolTaskExecutor") + public void runImportCountry(String taskId, byte[] bytes, String filename, String countryCode, String operName) + { + try + { + put(taskId, BmsProductImportTask.STATUS_RUNNING, 5, "parsing excel", null); + String message = productService.importCountryProductExcel(bytes, filename, countryCode, operName, taskId); + put(taskId, BmsProductImportTask.STATUS_SUCCESS, 100, message, null); + log.info("async product country import done, taskId={}, country={}", taskId, countryCode); + } + catch (Exception e) + { + log.error("async product country import failed, taskId={}, country={}", taskId, countryCode, e); + put(taskId, BmsProductImportTask.STATUS_FAIL, 100, "failed", errorMessage(e)); + } + } + + public void put(String taskId, String status, int percent, String message, String error) + { + BmsProductImportTask task = new BmsProductImportTask(); + task.setTaskId(taskId); + task.setStatus(status); + task.setPercent(percent); + task.setMessage(message); + task.setError(error); + redisCache.setCacheObject(CACHE_KEY_PREFIX + taskId, task, TTL_MINUTES, TimeUnit.MINUTES); + } + + public BmsProductImportTask get(String taskId) + { + return redisCache.getCacheObject(CACHE_KEY_PREFIX + taskId); + } + + private static String errorMessage(Exception e) + { + String msg = e.getMessage(); + if (StringUtils.isEmpty(msg)) + { + msg = e.getClass().getSimpleName(); + } + return msg; + } +} diff --git a/bms-back/bms-system/src/main/resources/mapper/price/BmsProductCountryMapper.xml b/bms-back/bms-system/src/main/resources/mapper/price/BmsProductCountryMapper.xml index 40438f88..b225abf9 100644 --- a/bms-back/bms-system/src/main/resources/mapper/price/BmsProductCountryMapper.xml +++ b/bms-back/bms-system/src/main/resources/mapper/price/BmsProductCountryMapper.xml @@ -171,6 +171,23 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" and (effective_date is null or effective_date < #{newEffectiveDate}) + + update product_country + set expire_date = #{newEffectiveDate}, + update_time = sysdate() + where product_code = #{productCode} + and expire_date is null + and (effective_date is null or effective_date < #{newEffectiveDate}) + + + and upper(ifnull(country_code,'')) = upper(#{countryCode}) + + + and country_name = #{countryName} + + + + update product_country set status = case @@ -190,6 +207,24 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" ) + + delete from product_country + where product_code = #{productCode} + and expire_date is null + and ( + (#{effectiveDate} is null and effective_date is null) + or effective_date = #{effectiveDate} + ) + + + and upper(ifnull(country_code,'')) = upper(#{countryCode}) + + + and country_name = #{countryName} + + + +