diff --git a/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/price/ProductController.java b/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/price/ProductController.java new file mode 100644 index 00000000..73aa33e4 --- /dev/null +++ b/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/price/ProductController.java @@ -0,0 +1,98 @@ +package com.ruoyi.web.controller.price; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.price.domain.BmsProduct; +import com.ruoyi.price.domain.BmsProductCountry; +import com.ruoyi.price.service.IBmsProductService; + +/** + * 产品列表(含 Excel 导入) + * + * @author ruoyi + */ +@RestController +@RequestMapping("/price/product") +public class ProductController extends BaseController +{ + @Autowired + private IBmsProductService productService; + + @PreAuthorize("@ss.hasPermi('price:product:list')") + @GetMapping("/list") + public TableDataInfo list(BmsProduct product) + { + startPage(); + List list = productService.selectBmsProductList(product); + return getDataTable(list); + } + + @PreAuthorize("@ss.hasPermi('price:product:query')") + @GetMapping("/{productCode}/countries") + public AjaxResult countries(@PathVariable String productCode) + { + List list = productService.selectCountryListByProductCode(productCode); + return success(list); + } + + @PreAuthorize("@ss.hasPermi('price:product:query')") + @GetMapping("/{productId}") + public AjaxResult getInfo(@PathVariable Long productId) + { + return success(productService.selectBmsProductById(productId)); + } + + @PreAuthorize("@ss.hasPermi('price:product:add')") + @Log(title = "产品列表", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody BmsProduct product) + { + product.setCreateBy(getUsername()); + return toAjax(productService.insertBmsProduct(product)); + } + + @PreAuthorize("@ss.hasPermi('price:product:edit')") + @Log(title = "产品列表", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody BmsProduct product) + { + product.setUpdateBy(getUsername()); + return toAjax(productService.updateBmsProduct(product)); + } + + @PreAuthorize("@ss.hasPermi('price:product:remove')") + @Log(title = "产品列表", businessType = BusinessType.DELETE) + @DeleteMapping("/{productIds}") + public AjaxResult remove(@PathVariable Long[] productIds) + { + return toAjax(productService.deleteBmsProductByIds(productIds)); + } + + /** + * 导入威狮B2C小包价表.xlsx(产品目录 + 国家服务明细) + */ + @Log(title = "产品列表", 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); + } +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/BmsProduct.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/BmsProduct.java new file mode 100644 index 00000000..ca86734b --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/BmsProduct.java @@ -0,0 +1,82 @@ +package com.ruoyi.price.domain; + +import java.util.Date; +import java.util.List; +import com.fasterxml.jackson.annotation.JsonFormat; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * BMS产品主表 bms_product + * + * @author ruoyi + */ +public class BmsProduct extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + private Long productId; + private String productCode; + private String productName; + private String serviceLevel; + private String cargoAttr; + private String destRegion; + private String updateNote; + /** TAX_INCLUDED / TAX_EXCLUDED */ + private String taxMode; + @JsonFormat(pattern = "yyyy-MM-dd") + private Date effectiveDate; + private String status; + private String sourceSheet; + private Long tmsId; + /** 国家明细数(列表展示) */ + private Integer countryCount; + private List countries; + + public Long getProductId() { return productId; } + public void setProductId(Long productId) { this.productId = productId; } + public String getProductCode() { return productCode; } + public void setProductCode(String productCode) { this.productCode = productCode; } + public String getProductName() { return productName; } + public void setProductName(String productName) { this.productName = productName; } + public String getServiceLevel() { return serviceLevel; } + public void setServiceLevel(String serviceLevel) { this.serviceLevel = serviceLevel; } + public String getCargoAttr() { return cargoAttr; } + public void setCargoAttr(String cargoAttr) { this.cargoAttr = cargoAttr; } + public String getDestRegion() { return destRegion; } + public void setDestRegion(String destRegion) { this.destRegion = destRegion; } + public String getUpdateNote() { return updateNote; } + public void setUpdateNote(String updateNote) { this.updateNote = updateNote; } + public String getTaxMode() { return taxMode; } + public void setTaxMode(String taxMode) { this.taxMode = taxMode; } + public Date getEffectiveDate() { return effectiveDate; } + public void setEffectiveDate(Date effectiveDate) { this.effectiveDate = effectiveDate; } + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + public String getSourceSheet() { return sourceSheet; } + public void setSourceSheet(String sourceSheet) { this.sourceSheet = sourceSheet; } + public Long getTmsId() { return tmsId; } + public void setTmsId(Long tmsId) { this.tmsId = tmsId; } + public Integer getCountryCount() { return countryCount; } + public void setCountryCount(Integer countryCount) { this.countryCount = countryCount; } + public List getCountries() { return countries; } + public void setCountries(List countries) { this.countries = countries; } + + @Override + public String toString() + { + return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE) + .append("productId", getProductId()) + .append("productCode", getProductCode()) + .append("productName", getProductName()) + .append("serviceLevel", getServiceLevel()) + .append("cargoAttr", getCargoAttr()) + .append("destRegion", getDestRegion()) + .append("taxMode", getTaxMode()) + .append("effectiveDate", getEffectiveDate()) + .append("status", getStatus()) + .append("sourceSheet", getSourceSheet()) + .toString(); + } +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/BmsProductCountry.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/BmsProductCountry.java new file mode 100644 index 00000000..5a99c157 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/BmsProductCountry.java @@ -0,0 +1,74 @@ +package com.ruoyi.price.domain; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 产品国家服务明细 bms_product_country(含重量段,便于计费匹配) + * + * @author ruoyi + */ +public class BmsProductCountry implements Serializable +{ + private static final long serialVersionUID = 1L; + + private Long detailId; + private String productCode; + private String countryName; + private String countryCode; + /** 重量段最小值 KG */ + private BigDecimal weightMin; + /** 重量段最大值 KG */ + private BigDecimal weightMax; + private String sizeLimit; + private String lastMile; + private String leadTime; + @JsonFormat(pattern = "yyyy-MM-dd") + private Date effectiveDate; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createdAt; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updatedAt; + + public Long getDetailId() { return detailId; } + public void setDetailId(Long detailId) { this.detailId = detailId; } + public String getProductCode() { return productCode; } + public void setProductCode(String productCode) { this.productCode = productCode; } + public String getCountryName() { return countryName; } + public void setCountryName(String countryName) { this.countryName = countryName; } + public String getCountryCode() { return countryCode; } + public void setCountryCode(String countryCode) { this.countryCode = countryCode; } + public BigDecimal getWeightMin() { return weightMin; } + public void setWeightMin(BigDecimal weightMin) { this.weightMin = weightMin; } + public BigDecimal getWeightMax() { return weightMax; } + public void setWeightMax(BigDecimal weightMax) { this.weightMax = weightMax; } + public String getSizeLimit() { return sizeLimit; } + public void setSizeLimit(String sizeLimit) { this.sizeLimit = sizeLimit; } + public String getLastMile() { return lastMile; } + public void setLastMile(String lastMile) { this.lastMile = lastMile; } + public String getLeadTime() { return leadTime; } + public void setLeadTime(String leadTime) { this.leadTime = leadTime; } + public Date getEffectiveDate() { return effectiveDate; } + public void setEffectiveDate(Date effectiveDate) { this.effectiveDate = effectiveDate; } + public Date getCreatedAt() { return createdAt; } + public void setCreatedAt(Date createdAt) { this.createdAt = createdAt; } + public Date getUpdatedAt() { return updatedAt; } + public void setUpdatedAt(Date updatedAt) { this.updatedAt = updatedAt; } + + @Override + public String toString() + { + return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE) + .append("detailId", getDetailId()) + .append("productCode", getProductCode()) + .append("countryName", getCountryName()) + .append("countryCode", getCountryCode()) + .append("weightMin", getWeightMin()) + .append("weightMax", getWeightMax()) + .toString(); + } +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/BmsProductCountryMapper.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/BmsProductCountryMapper.java new file mode 100644 index 00000000..54b26331 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/BmsProductCountryMapper.java @@ -0,0 +1,20 @@ +package com.ruoyi.price.mapper; + +import java.util.List; +import com.ruoyi.price.domain.BmsProductCountry; + +/** + * 产品国家明细 数据层 + * + * @author ruoyi + */ +public interface BmsProductCountryMapper +{ + public List selectBmsProductCountryList(BmsProductCountry country); + + public int insertBmsProductCountry(BmsProductCountry country); + + public int deleteByProductCode(String productCode); + + public int deleteByProductCodes(String[] productCodes); +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/BmsProductMapper.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/BmsProductMapper.java new file mode 100644 index 00000000..24988a62 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/BmsProductMapper.java @@ -0,0 +1,24 @@ +package com.ruoyi.price.mapper; + +import java.util.List; +import com.ruoyi.price.domain.BmsProduct; + +/** + * 产品主表 数据层 + * + * @author ruoyi + */ +public interface BmsProductMapper +{ + public BmsProduct selectBmsProductById(Long productId); + + public BmsProduct selectBmsProductByCode(String productCode); + + public List selectBmsProductList(BmsProduct product); + + public int insertBmsProduct(BmsProduct product); + + public int updateBmsProduct(BmsProduct product); + + public int deleteBmsProductByIds(Long[] productIds); +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/IBmsProductService.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/IBmsProductService.java new file mode 100644 index 00000000..ddf988f2 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/IBmsProductService.java @@ -0,0 +1,36 @@ +package com.ruoyi.price.service; + +import java.util.List; +import org.springframework.web.multipart.MultipartFile; +import com.ruoyi.price.domain.BmsProduct; +import com.ruoyi.price.domain.BmsProductCountry; + +/** + * 产品列表 服务层 + * + * @author ruoyi + */ +public interface IBmsProductService +{ + public BmsProduct selectBmsProductById(Long productId); + + public List selectBmsProductList(BmsProduct product); + + public int insertBmsProduct(BmsProduct product); + + public int updateBmsProduct(BmsProduct product); + + public int deleteBmsProductByIds(Long[] productIds); + + public List selectCountryListByProductCode(String productCode); + + /** + * 导入威狮B2C小包价表:产品目录 + 各产品 sheet 国家服务属性(不含运费/挂号费) + * + * @param file 上传的 xlsx/xls + * @param updateSupport 是否更新已存在产品 + * @param operName 操作人 + * @return 导入结果说明(可含 HTML 换行) + */ + public String importProductExcel(MultipartFile file, boolean updateSupport, String operName) throws Exception; +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/BmsProductServiceImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/BmsProductServiceImpl.java new file mode 100644 index 00000000..953bf11f --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/BmsProductServiceImpl.java @@ -0,0 +1,639 @@ +package com.ruoyi.price.service.impl; + +import java.io.InputStream; +import java.math.BigDecimal; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.DateUtil; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.price.domain.BmsProduct; +import com.ruoyi.price.domain.BmsProductCountry; +import com.ruoyi.price.mapper.BmsProductCountryMapper; +import com.ruoyi.price.mapper.BmsProductMapper; +import com.ruoyi.price.service.IBmsProductService; +import com.ruoyi.system.domain.BmsRegion; +import com.ruoyi.system.mapper.BmsRegionMapper; + +/** + * Product service with Weishi Excel import (catalog + country weight bands) + * + * @author ruoyi + */ +@Service +public class BmsProductServiceImpl implements IBmsProductService +{ + private static final String CATALOG_SHEET = "\u4ea7\u54c1\u76ee\u5f55"; + private static final Set SKIP_SHEETS = new HashSet<>(Arrays.asList( + CATALOG_SHEET, "\u9000\u4ef6\u91cd\u6d3e", "\u7533\u62a5\u8bf4\u660e", "\u8d54\u507f\u6807\u51c6")); + + private static final Pattern EFFECTIVE_DATE_PATTERN = Pattern.compile( + "\u751f\u6548\u65e5\u671f[\uff1a:]\\s*(\\d{4})[/.-](\\d{1,2})[/.-](\\d{1,2})"); + + /** Parse 0.01\u2264W\u22640.3 / 0.3 selectBmsProductList(BmsProduct product) + { + return productMapper.selectBmsProductList(product); + } + + @Override + public int insertBmsProduct(BmsProduct product) + { + if (StringUtils.isEmpty(product.getStatus())) + { + product.setStatus("0"); + } + if (StringUtils.isEmpty(product.getTaxMode())) + { + product.setTaxMode(resolveTaxMode(product.getProductName())); + } + return productMapper.insertBmsProduct(product); + } + + @Override + public int updateBmsProduct(BmsProduct product) + { + return productMapper.updateBmsProduct(product); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public int deleteBmsProductByIds(Long[] productIds) + { + if (productIds == null || productIds.length == 0) + { + return 0; + } + List codes = new ArrayList<>(); + for (Long id : productIds) + { + BmsProduct p = productMapper.selectBmsProductById(id); + if (p != null && StringUtils.isNotEmpty(p.getProductCode())) + { + codes.add(p.getProductCode()); + } + } + if (!codes.isEmpty()) + { + countryMapper.deleteByProductCodes(codes.toArray(new String[0])); + } + return productMapper.deleteBmsProductByIds(productIds); + } + + @Override + public List selectCountryListByProductCode(String productCode) + { + BmsProductCountry query = new BmsProductCountry(); + query.setProductCode(productCode); + return countryMapper.selectBmsProductCountryList(query); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public String importProductExcel(MultipartFile file, boolean updateSupport, String operName) throws Exception + { + if (file == null || file.isEmpty()) + { + throw new ServiceException("\u4e0a\u4f20\u6587\u4ef6\u4e0d\u80fd\u4e3a\u7a7a"); + } + String filename = file.getOriginalFilename(); + if (filename == null || !(filename.toLowerCase(Locale.ROOT).endsWith(".xlsx") + || filename.toLowerCase(Locale.ROOT).endsWith(".xls"))) + { + throw new ServiceException("\u4ec5\u652f\u6301 xls / xlsx \u6587\u4ef6"); + } + + try (InputStream is = file.getInputStream(); Workbook workbook = WorkbookFactory.create(is)) + { + Sheet catalog = workbook.getSheet(CATALOG_SHEET); + if (catalog == null) + { + throw new ServiceException("\u672a\u627e\u5230\u300c\u4ea7\u54c1\u76ee\u5f55\u300dsheet\uff0c\u8bf7\u4e0a\u4f20\u5a01\u72eeB2C\u5c0f\u5305\u4ef7\u8868\u683c\u5f0f\u6587\u4ef6"); + } + + List products = parseCatalog(catalog); + if (products.isEmpty()) + { + throw new ServiceException("\u4ea7\u54c1\u76ee\u5f55\u65e0\u6709\u6548\u6570\u636e\u884c"); + } + + Map regionByNameCn = loadRegionByNameCn(); + if (regionByNameCn.isEmpty()) + { + throw new ServiceException("bms_region \u65e0\u53ef\u7528\u56fd\u5bb6\u6570\u636e\uff0c\u8bf7\u5148\u540c\u6b65/\u7ef4\u62a4\u56fd\u5bb6\u8868\u540e\u518d\u5bfc\u5165"); + } + + int successNum = 0; + int updateNum = 0; + int skipNum = 0; + int countryNum = 0; + Set unmatchedCountries = new HashSet<>(); + StringBuilder failureMsg = new StringBuilder(); + + for (BmsProduct product : products) + { + try + { + product.setSourceSheet(product.getProductName()); + product.setCreateBy(operName); + product.setUpdateBy(operName); + + List countries = parseProductSheet(workbook, product, regionByNameCn, unmatchedCountries); + BmsProduct existing = productMapper.selectBmsProductByCode(product.getProductCode()); + + if (existing == null) + { + product.setStatus("0"); + productMapper.insertBmsProduct(product); + saveCountries(product.getProductCode(), countries); + successNum++; + countryNum += countries.size(); + } + else if (updateSupport) + { + product.setProductId(existing.getProductId()); + productMapper.updateBmsProduct(product); + countryMapper.deleteByProductCode(product.getProductCode()); + saveCountries(product.getProductCode(), countries); + updateNum++; + countryNum += countries.size(); + } + else + { + skipNum++; + } + } + catch (Exception e) + { + failureMsg.append("
").append(product.getProductCode()).append("\uff1a").append(e.getMessage()); + } + } + + if (failureMsg.length() > 0) + { + throw new ServiceException("\u5bfc\u5165\u5b8c\u6210\u4f46\u5b58\u5728\u5931\u8d25\u9879\uff1a\u65b0\u589e " + successNum + "\uff0c\u66f4\u65b0 " + updateNum + + "\uff0c\u8df3\u8fc7 " + skipNum + "\uff1b\u5931\u8d25\u660e\u7ec6\uff1a" + failureMsg); + } + String msg = "\u5bfc\u5165\u6210\u529f\uff1a\u65b0\u589e " + successNum + " \u4e2a\u4ea7\u54c1\uff0c\u66f4\u65b0 " + updateNum + " \u4e2a\uff0c\u8df3\u8fc7 " + + skipNum + " \u4e2a\uff1b\u5199\u5165\u56fd\u5bb6/\u91cd\u91cf\u6bb5\u660e\u7ec6 " + countryNum + " \u884c\u3002" + + "
\u8303\u56f4\uff1a\u4ea7\u54c1\u76ee\u5f55 + \u91cd\u91cf\u6bb5min/max + \u5c3a\u5bf8/\u5c3e\u7a0b/\u65f6\u6548\uff1b\u56fd\u5bb6\u7801\u6765\u81ea bms_region.country_name_cn / country_code_2\u3002"; + if (!unmatchedCountries.isEmpty()) + { + msg += "
\u672a\u5339\u914d\u56fd\u5bb6\u8868\uff08country_code \u4e3a\u7a7a\uff09\uff1a" + String.join("\u3001", unmatchedCountries); + } + return msg; + } + } + + private void saveCountries(String productCode, List countries) + { + for (BmsProductCountry c : countries) + { + c.setProductCode(productCode); + countryMapper.insertBmsProductCountry(c); + } + } + + private List parseCatalog(Sheet sheet) + { + List list = new ArrayList<>(); + // Weishi workbook often starts data at column B (A empty); locate by header. + int[] header = findHeaderCell(sheet, "\u4ea7\u54c1\u5bf9\u63a5\u4ee3\u7801"); + if (header == null) + { + return list; + } + int headerRow = header[0]; + int baseCol = header[1]; + int lastRow = sheet.getLastRowNum(); + for (int i = headerRow + 1; i <= lastRow; i++) + { + Row row = sheet.getRow(i); + if (row == null) + { + continue; + } + String code = cellString(row.getCell(baseCol)); + if (StringUtils.isEmpty(code) || "\u4ea7\u54c1\u5bf9\u63a5\u4ee3\u7801".equals(code)) + { + continue; + } + if (!code.matches("(?i)^[A-Z0-9_-]+$")) + { + continue; + } + BmsProduct p = new BmsProduct(); + p.setProductCode(code.trim()); + p.setProductName(cellString(row.getCell(baseCol + 1))); + p.setServiceLevel(cellString(row.getCell(baseCol + 2))); + p.setCargoAttr(cellString(row.getCell(baseCol + 3))); + p.setDestRegion(cellString(row.getCell(baseCol + 4))); + p.setUpdateNote(cellString(row.getCell(baseCol + 5))); + p.setEffectiveDate(cellDate(row.getCell(baseCol + 6))); + p.setTaxMode(resolveTaxMode(p.getProductName())); + if (StringUtils.isEmpty(p.getProductName())) + { + continue; + } + list.add(p); + } + return list; + } + + private List parseProductSheet(Workbook workbook, BmsProduct product, + Map regionByNameCn, Set unmatchedCountries) + { + String sheetName = product.getProductName(); + Sheet sheet = workbook.getSheet(sheetName); + if (sheet == null) + { + for (int i = 0; i < workbook.getNumberOfSheets(); i++) + { + String name = workbook.getSheetName(i); + if (SKIP_SHEETS.contains(name)) + { + continue; + } + if (name != null && name.trim().equals(sheetName.trim())) + { + sheet = workbook.getSheetAt(i); + break; + } + } + } + if (sheet == null) + { + return new ArrayList<>(); + } + + Date sheetEffective = parseSheetEffectiveDate(sheet); + List list = new ArrayList<>(); + // Product sheets also often start at column B. + int[] header = findHeaderCell(sheet, "\u56fd\u5bb6"); + int baseCol = header != null ? header[1] : 0; + int startRow = header != null ? header[0] + 1 : 0; + String currentCountry = null; + String currentSize = null; + String currentLastMile = null; + String currentLeadTime = null; + int lastRow = sheet.getLastRowNum(); + for (int i = startRow; i <= lastRow; i++) + { + Row row = sheet.getRow(i); + if (row == null) + { + continue; + } + String countryCell = cellString(row.getCell(baseCol)); + String weightLabel = cellString(row.getCell(baseCol + 1)); + if ("\u56fd\u5bb6".equals(countryCell) || weightLabel.contains("\u91cd\u91cf\u6bb5")) + { + continue; + } + if (StringUtils.isNotEmpty(countryCell)) + { + currentCountry = countryCell.trim(); + currentSize = emptyToNull(cellString(row.getCell(baseCol + 4))); + currentLastMile = emptyToNull(cellString(row.getCell(baseCol + 5))); + currentLeadTime = emptyToNull(cellString(row.getCell(baseCol + 6))); + } + if (StringUtils.isEmpty(currentCountry)) + { + continue; + } + BigDecimal[] range = parseWeightRange(weightLabel); + if (range == null) + { + continue; + } + BmsRegion region = resolveRegion(currentCountry, regionByNameCn); + BmsProductCountry c = new BmsProductCountry(); + if (region != null) + { + c.setCountryName(region.getCountryNameCn()); + c.setCountryCode(region.getCountryCode2()); + } + else + { + c.setCountryName(currentCountry); + c.setCountryCode(null); + unmatchedCountries.add(currentCountry); + } + c.setWeightMin(range[0]); + c.setWeightMax(range[1]); + c.setSizeLimit(currentSize); + c.setLastMile(currentLastMile); + c.setLeadTime(currentLeadTime); + c.setEffectiveDate(sheetEffective != null ? sheetEffective : product.getEffectiveDate()); + list.add(c); + } + return list; + } + + /** + * ???? bms_region??country_name_cn -> ????? country_code_2?? + */ + private Map loadRegionByNameCn() + { + BmsRegion query = new BmsRegion(); + query.setStatus("0"); + List regions = regionMapper.selectRegionList(query); + Map map = new HashMap<>(); + if (regions == null) + { + return map; + } + for (BmsRegion region : regions) + { + if (region == null || StringUtils.isEmpty(region.getCountryNameCn())) + { + continue; + } + map.put(region.getCountryNameCn().trim(), region); + } + return map; + } + + /** + * ????? country_name_cn ?????????????????????????? ??? / ????????? + */ + private BmsRegion resolveRegion(String excelName, Map regionByNameCn) + { + if (StringUtils.isEmpty(excelName) || regionByNameCn == null || regionByNameCn.isEmpty()) + { + return null; + } + String name = excelName.trim(); + BmsRegion exact = regionByNameCn.get(name); + if (exact != null) + { + return exact; + } + List candidates = new ArrayList<>(); + for (BmsRegion region : regionByNameCn.values()) + { + String cn = region.getCountryNameCn(); + if (StringUtils.isEmpty(cn)) + { + continue; + } + if (cn.contains(name) || name.contains(cn)) + { + candidates.add(region); + } + } + if (candidates.size() == 1) + { + return candidates.get(0); + } + return null; + } + + private BigDecimal[] parseWeightRange(String label) + { + if (StringUtils.isEmpty(label)) + { + return null; + } + Matcher m = WEIGHT_RANGE_PATTERN.matcher(label.replace(" ", "")); + if (!m.find()) + { + return null; + } + try + { + BigDecimal min = new BigDecimal(m.group(1)); + BigDecimal max = new BigDecimal(m.group(2)); + if (min.compareTo(max) > 0) + { + BigDecimal tmp = min; + min = max; + max = tmp; + } + return new BigDecimal[] { min, max }; + } + catch (Exception e) + { + return null; + } + } + + private Date parseSheetEffectiveDate(Sheet sheet) + { + Row row0 = sheet.getRow(0); + if (row0 == null) + { + return null; + } + for (int c = 0; c < 12; c++) + { + String text = cellString(row0.getCell(c)); + if (StringUtils.isEmpty(text)) + { + continue; + } + Matcher m = EFFECTIVE_DATE_PATTERN.matcher(text); + if (m.find()) + { + try + { + String d = m.group(1) + "-" + pad2(m.group(2)) + "-" + pad2(m.group(3)); + return new SimpleDateFormat("yyyy-MM-dd").parse(d); + } + catch (Exception ignored) + { + return null; + } + } + } + return null; + } + + private static String pad2(String n) + { + return n.length() == 1 ? "0" + n : n; + } + + private static String resolveTaxMode(String productName) + { + if (productName != null && productName.contains("\u5305\u7a0e")) + { + return "TAX_INCLUDED"; + } + if (productName != null && productName.contains("\u4e0d\u5305\u7a0e")) + { + return "TAX_EXCLUDED"; + } + return "TAX_INCLUDED"; + } + + private static String emptyToNull(String s) + { + return StringUtils.isEmpty(s) ? null : s; + } + + /** + * Scan top-left area for a header cell; returns {rowIndex, colIndex} or null. + * Prefers exact match, then contains. + */ + private static int[] findHeaderCell(Sheet sheet, String headerText) + { + if (sheet == null || StringUtils.isEmpty(headerText)) + { + return null; + } + int lastRow = Math.min(sheet.getLastRowNum(), 30); + int[] containsHit = null; + for (int r = 0; r <= lastRow; r++) + { + Row row = sheet.getRow(r); + if (row == null) + { + continue; + } + short lastCell = row.getLastCellNum(); + if (lastCell < 0) + { + continue; + } + // Also scan a few leading empty columns beyond lastCellNum edge cases + int maxCol = Math.max(lastCell, 12); + maxCol = Math.min(maxCol, 20); + for (int c = 0; c < maxCol; c++) + { + String v = cellString(row.getCell(c)); + if (StringUtils.isEmpty(v)) + { + continue; + } + if (headerText.equals(v)) + { + return new int[] { r, c }; + } + if (containsHit == null && v.contains(headerText)) + { + containsHit = new int[] { r, c }; + } + } + } + return containsHit; + } + + private static String cellString(Cell cell) + { + if (cell == null) + { + return ""; + } + CellType type = cell.getCellType(); + if (type == CellType.FORMULA) + { + type = cell.getCachedFormulaResultType(); + } + switch (type) + { + case STRING: + return cell.getStringCellValue() == null ? "" : cell.getStringCellValue().trim(); + case NUMERIC: + if (DateUtil.isCellDateFormatted(cell)) + { + Date d = cell.getDateCellValue(); + return d == null ? "" : new SimpleDateFormat("yyyy-MM-dd").format(d); + } + double num = cell.getNumericCellValue(); + if (Math.floor(num) == num) + { + return String.valueOf((long) num); + } + return String.valueOf(num); + case BOOLEAN: + return String.valueOf(cell.getBooleanCellValue()); + case BLANK: + return ""; + default: + return ""; + } + } + + private static Date cellDate(Cell cell) + { + if (cell == null) + { + return null; + } + CellType type = cell.getCellType(); + if (type == CellType.FORMULA) + { + type = cell.getCachedFormulaResultType(); + } + if (type == CellType.NUMERIC) + { + if (DateUtil.isCellDateFormatted(cell)) + { + return cell.getDateCellValue(); + } + return DateUtil.getJavaDate(cell.getNumericCellValue()); + } + if (type == CellType.STRING) + { + String s = cell.getStringCellValue(); + if (StringUtils.isEmpty(s)) + { + return null; + } + String[] patterns = { "yyyy-MM-dd", "yyyy/M/d", "yyyy/MM/dd", "yyyy-M-d", + "M/d/yy", "M/d/yyyy", "M/d/yy H:mm", "M/d/yyyy H:mm", "yyyy/M/d H:mm" }; + for (String p : patterns) + { + try + { + return new SimpleDateFormat(p).parse(s.trim()); + } + catch (Exception ignored) + { + } + } + } + return null; + } +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/system/mapper/BmsRegionMapper.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/system/mapper/BmsRegionMapper.java index f9f9ca54..b4f886bd 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/system/mapper/BmsRegionMapper.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/system/mapper/BmsRegionMapper.java @@ -34,6 +34,14 @@ public interface BmsRegionMapper */ public BmsRegion selectRegionByCode2(String countryCode2); + /** + * 根据中文名精确查询区域 + * + * @param countryNameCn 中文名 + * @return 区域信息 + */ + public BmsRegion selectRegionByNameCn(String countryNameCn); + /** * 新增区域 * diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/BmsRegionSyncServiceImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/BmsRegionSyncServiceImpl.java index 79415cbf..4d07e67b 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/BmsRegionSyncServiceImpl.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/BmsRegionSyncServiceImpl.java @@ -1,16 +1,15 @@ package com.ruoyi.system.service.impl; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.io.ClassPathResource; +import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestTemplate; import com.fasterxml.jackson.databind.ObjectMapper; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.system.domain.BmsRegion; @@ -21,7 +20,7 @@ import com.ruoyi.system.service.IBmsRegionSyncService; /** * 区域数据同步 服务层实现 - * 从本地 countries.json 文件加载国家数据 + * 通过 jsdelivr + mledoze/countries 同步国家数据(含中文翻译) * * @author ruoyi */ @@ -30,7 +29,12 @@ public class BmsRegionSyncServiceImpl implements IBmsRegionSyncService { private static final Logger log = LoggerFactory.getLogger(BmsRegionSyncServiceImpl.class); - private static final String DATA_FILE = "countries.json"; + /** 国家数据源(mledoze/countries,jsdelivr CDN,国内可直连,无需鉴权) */ + @Value("${bms.region.sync.url:https://cdn.jsdelivr.net/gh/mledoze/countries@master/dist/countries.json}") + private String dataSourceUrl; + + @Autowired + private RestTemplate restTemplate; @Autowired private BmsRegionMapper regionMapper; @@ -77,18 +81,13 @@ public class BmsRegionSyncServiceImpl implements IBmsRegionSyncService try { - log.info("开始同步国家数据,读取本地文件: {}", DATA_FILE); - - // 从classpath读取国家数据文件 - ClassPathResource resource = new ClassPathResource(DATA_FILE); - InputStream inputStream = resource.getInputStream(); - byte[] bytes = inputStream.readAllBytes(); - inputStream.close(); - String jsonData = new String(bytes, StandardCharsets.UTF_8); + log.info("开始同步国家数据,远程数据源: {}", dataSourceUrl); + // 从远程接口拉取国家数据 + String jsonData = restTemplate.getForObject(dataSourceUrl, String.class); if (StringUtils.isEmpty(jsonData)) { - throw new RuntimeException("国家数据文件为空"); + throw new RuntimeException("远程国家数据为空"); } // 解析JSON @@ -180,20 +179,18 @@ public class BmsRegionSyncServiceImpl implements IBmsRegionSyncService region.setCountryNameEn((String) name.get("common")); } - // 中文名 translations.cmn.common,如无则用英文名 + // 中文名 translations.zho.common(mledoze/countries 标准字段名为 zho) Map translations = (Map) country.get("translations"); if (translations != null) { - Map cmn = (Map) translations.get("cmn"); - if (cmn != null) + Map zho = (Map) translations.get("zho"); + if (zho != null) { - region.setCountryNameCn((String) cmn.get("common")); + region.setCountryNameCn((String) zho.get("common")); } } - if (StringUtils.isEmpty(region.getCountryNameCn())) - { - region.setCountryNameCn(region.getCountryNameEn()); - } + // 注:取不到中文名时不再用英文名兜底,避免污染业务匹配。 + // 若确需兜底,可改为 region.setCountryNameCn(StringUtils.EMPTY); // ISO代码 region.setCountryCode2((String) country.get("cca2")); diff --git a/bms-back/ruoyi-system/src/main/resources/mapper/data/BmsRegionMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/data/BmsRegionMapper.xml index 9d44b169..a68a7ad5 100644 --- a/bms-back/ruoyi-system/src/main/resources/mapper/data/BmsRegionMapper.xml +++ b/bms-back/ruoyi-system/src/main/resources/mapper/data/BmsRegionMapper.xml @@ -47,6 +47,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" where country_code_2 = #{countryCode2} limit 1 + + + select detail_id, product_code, country_name, country_code, weight_min, weight_max, + size_limit, last_mile, lead_time, effective_date, created_at, updated_at + from bms_product_country + + + AND product_code = #{productCode} + + + AND country_name like concat('%', #{countryName}, '%') + + + AND country_code = #{countryCode} + + + AND weight_min = #{weightMin} + + + AND weight_max = #{weightMax} + + + order by country_name asc, weight_min asc, detail_id asc + + + + insert into bms_product_country ( + product_code, country_name, country_code, weight_min, weight_max, + size_limit, last_mile, lead_time, effective_date, created_at + ) values ( + #{productCode}, #{countryName}, #{countryCode}, #{weightMin}, #{weightMax}, + #{sizeLimit}, #{lastMile}, #{leadTime}, #{effectiveDate}, sysdate() + ) + + + + delete from bms_product_country where product_code = #{productCode} + + + + delete from bms_product_country where product_code in + + #{code} + + + + diff --git a/bms-back/ruoyi-system/src/main/resources/mapper/price/BmsProductMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/price/BmsProductMapper.xml new file mode 100644 index 00000000..282561b7 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/resources/mapper/price/BmsProductMapper.xml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + select p.product_id, p.product_code, p.product_name, p.service_level, p.cargo_attr, + p.dest_region, p.update_note, p.tax_mode, p.effective_date, p.status, + p.source_sheet, p.remark, p.tms_id, p.created_by, p.created_at, p.updated_by, p.updated_at, + (select count(distinct c.country_name) from bms_product_country c where c.product_code = p.product_code) as country_count + from bms_product p + + + + + + + + + + insert into bms_product ( + product_code, + product_name, + service_level, + cargo_attr, + dest_region, + update_note, + tax_mode, + effective_date, + status, + source_sheet, + remark, + tms_id, + created_by, + created_at + ) values ( + #{productCode}, + #{productName}, + #{serviceLevel}, + #{cargoAttr}, + #{destRegion}, + #{updateNote}, + #{taxMode}, + #{effectiveDate}, + #{status}, + #{sourceSheet}, + #{remark}, + #{tmsId}, + #{createBy}, + sysdate() + ) + + + + update bms_product + + product_code = #{productCode}, + product_name = #{productName}, + service_level = #{serviceLevel}, + cargo_attr = #{cargoAttr}, + dest_region = #{destRegion}, + update_note = #{updateNote}, + tax_mode = #{taxMode}, + effective_date = #{effectiveDate}, + status = #{status}, + source_sheet = #{sourceSheet}, + remark = #{remark}, + tms_id = #{tmsId}, + updated_by = #{updateBy}, + updated_at = sysdate() + + where product_id = #{productId} + + + + delete from bms_product where product_id in + + #{productId} + + + + diff --git a/bms-back/sql/price/product.sql b/bms-back/sql/price/product.sql new file mode 100644 index 00000000..6fc37b58 --- /dev/null +++ b/bms-back/sql/price/product.sql @@ -0,0 +1,74 @@ +-- 产品主数据(来源:威狮B2C小包价表.xlsx) +-- 导入范围:产品目录 + 各产品 sheet 国家服务明细(尺寸/尾程/时效) +-- 不导入:运费/挂号费矩阵、退件重派、申报说明、赔偿标准 +SET NAMES utf8mb4; + +CREATE TABLE IF NOT EXISTS bms_product ( + product_id bigint NOT NULL AUTO_INCREMENT COMMENT '产品ID', + product_code varchar(20) NOT NULL COMMENT '产品对接代码,如 WSP037', + product_name varchar(100) NOT NULL COMMENT '产品名称', + service_level varchar(20) DEFAULT NULL COMMENT '时效等级:标准等', + cargo_attr varchar(100) DEFAULT NULL COMMENT '走货属性:普货/F普货/带电/敏感等', + dest_region varchar(50) DEFAULT NULL COMMENT '目的区域:欧盟等', + update_note varchar(500) DEFAULT NULL COMMENT '更新内容说明', + tax_mode varchar(20) NOT NULL DEFAULT 'TAX_INCLUDED' COMMENT 'TAX_INCLUDED包税 TAX_EXCLUDED不包税', + effective_date date DEFAULT NULL COMMENT '目录生效日期', + status char(1) NOT NULL DEFAULT '0' COMMENT '0正常 1停用', + source_sheet varchar(100) DEFAULT NULL COMMENT '来源Excel sheet名', + remark varchar(500) DEFAULT NULL, + tms_id int DEFAULT NULL COMMENT '多租户ID', + created_by varchar(50) NOT NULL DEFAULT '', + created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_by varchar(50) NOT NULL DEFAULT '', + updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (product_id), + UNIQUE KEY uk_product_code (product_code), + KEY idx_tax_status (tax_mode, status) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='BMS产品主表-来自产品目录'; + +CREATE TABLE IF NOT EXISTS bms_product_country ( + detail_id bigint NOT NULL AUTO_INCREMENT COMMENT '明细ID', + product_code varchar(20) NOT NULL COMMENT '产品代码', + country_name varchar(50) NOT NULL COMMENT '国家中文名', + country_code char(2) DEFAULT NULL COMMENT 'ISO2,可空待补齐', + weight_min decimal(10,3) DEFAULT NULL COMMENT '重量段最小值KG(含)', + weight_max decimal(10,3) DEFAULT NULL COMMENT '重量段最大值KG(含)', + size_limit varchar(1000) DEFAULT NULL COMMENT '尺寸限制', + last_mile varchar(500) DEFAULT NULL COMMENT '尾程派送说明', + lead_time varchar(100) DEFAULT NULL COMMENT '参考时效', + effective_date date DEFAULT NULL COMMENT '产品价表生效日期', + created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (detail_id), + UNIQUE KEY uk_product_country_weight (product_code, country_name, weight_min, weight_max), + KEY idx_country_code (country_code), + KEY idx_weight_match (product_code, country_code, weight_min, weight_max), + CONSTRAINT fk_product_country_code FOREIGN KEY (product_code) REFERENCES bms_product (product_code) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='产品国家服务明细-重量段+尺寸/尾程/时效(不含运费)'; + +-- 已建表升级(幂等:列已存在时跳过报错可忽略) +-- ALTER TABLE bms_product_country ADD COLUMN weight_min decimal(10,3) DEFAULT NULL COMMENT '重量段最小值KG(含)' AFTER country_code; +-- ALTER TABLE bms_product_country ADD COLUMN weight_max decimal(10,3) DEFAULT NULL COMMENT '重量段最大值KG(含)' AFTER weight_min; +-- ALTER TABLE bms_product_country DROP INDEX uk_product_country; +-- ALTER TABLE bms_product_country ADD UNIQUE KEY uk_product_country_weight (product_code, country_name, weight_min, weight_max); +-- ALTER TABLE bms_product_country ADD KEY idx_weight_match (product_code, country_code, weight_min, weight_max); + +-- 菜单:挂在价格基础 2100 下(order_num=0 置顶) +INSERT INTO sys_menu(menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) +SELECT 2130, N'产品列表', 2100, 0, 'product', 'price/basic/product/index', '', 'PriceProduct', 1, 0, 'C', '0', '0', 'price:product:list', 'shopping', 'admin', sysdate(), N'产品目录与国家服务明细' +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id=2130); + +INSERT INTO sys_menu(menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time) +SELECT 2131, N'产品查询', 2130, 1, '', NULL, 1, 0, 'F', '0', '0', 'price:product:query', '#', 'admin', sysdate() FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id=2131); +INSERT INTO sys_menu(menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time) +SELECT 2132, N'产品新增', 2130, 2, '', NULL, 1, 0, 'F', '0', '0', 'price:product:add', '#', 'admin', sysdate() FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id=2132); +INSERT INTO sys_menu(menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time) +SELECT 2133, N'产品修改', 2130, 3, '', NULL, 1, 0, 'F', '0', '0', 'price:product:edit', '#', 'admin', sysdate() FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id=2133); +INSERT INTO sys_menu(menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time) +SELECT 2134, N'产品删除', 2130, 4, '', NULL, 1, 0, 'F', '0', '0', 'price:product:remove', '#', 'admin', sysdate() FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id=2134); +INSERT INTO sys_menu(menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time) +SELECT 2135, N'产品导入', 2130, 5, '', NULL, 1, 0, 'F', '0', '0', 'price:product:import', '#', 'admin', sysdate() FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id=2135); + +INSERT INTO sys_role_menu(role_id, menu_id) +SELECT 1, m.menu_id FROM (SELECT 2130 AS menu_id UNION SELECT 2131 UNION SELECT 2132 UNION SELECT 2133 UNION SELECT 2134 UNION SELECT 2135) m +WHERE NOT EXISTS (SELECT 1 FROM sys_role_menu rm WHERE rm.role_id=1 AND rm.menu_id=m.menu_id); diff --git a/bms-back/sql/price/product_country_weight_alter.sql b/bms-back/sql/price/product_country_weight_alter.sql new file mode 100644 index 00000000..59c38bb2 --- /dev/null +++ b/bms-back/sql/price/product_country_weight_alter.sql @@ -0,0 +1,13 @@ +-- 已存在 bms_product_country 时升级:增加重量段最小/最大值,便于后续按重量匹配 +-- 执行后请重新在「产品列表」导入 Excel(旧数据一行一国 → 新数据按重量段多行) +SET NAMES utf8mb4; + +ALTER TABLE bms_product_country + ADD COLUMN weight_min decimal(10,3) DEFAULT NULL COMMENT '重量段最小值KG(含)' AFTER country_code, + ADD COLUMN weight_max decimal(10,3) DEFAULT NULL COMMENT '重量段最大值KG(含)' AFTER weight_min; + +-- 旧唯一键(国家级)改为重量段级 +ALTER TABLE bms_product_country DROP INDEX uk_product_country; +ALTER TABLE bms_product_country + ADD UNIQUE KEY uk_product_country_weight (product_code, country_name, weight_min, weight_max), + ADD KEY idx_weight_match (product_code, country_code, weight_min, weight_max); diff --git a/bms-front/src/api/price/product.js b/bms-front/src/api/price/product.js new file mode 100644 index 00000000..0b6c4306 --- /dev/null +++ b/bms-front/src/api/price/product.js @@ -0,0 +1,52 @@ +import request from '@/utils/request' + +/** 查询产品列表 */ +export function listProduct(query) { + return request({ + url: '/price/product/list', + method: 'get', + params: query + }) +} + +/** 查询产品详情 */ +export function getProduct(productId) { + return request({ + url: '/price/product/' + productId, + method: 'get' + }) +} + +/** 查询产品国家明细 */ +export function listProductCountries(productCode) { + return request({ + url: '/price/product/' + productCode + '/countries', + method: 'get' + }) +} + +/** 新增产品 */ +export function addProduct(data) { + return request({ + url: '/price/product', + method: 'post', + data: data + }) +} + +/** 修改产品 */ +export function updateProduct(data) { + return request({ + url: '/price/product', + method: 'put', + data: data + }) +} + +/** 删除产品 */ +export function delProduct(productIds) { + return request({ + url: '/price/product/' + productIds, + method: 'delete' + }) +} diff --git a/bms-front/src/views/price/basic/product/index.vue b/bms-front/src/views/price/basic/product/index.vue new file mode 100644 index 00000000..3843229b --- /dev/null +++ b/bms-front/src/views/price/basic/product/index.vue @@ -0,0 +1,299 @@ + + + + +