增加产品导入功能

This commit is contained in:
wuweihua
2026-09-04 18:08:08 +08:00
parent 5e15ab9c44
commit 7204497a65
16 changed files with 1644 additions and 22 deletions
@@ -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<BmsProductCountry> 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<BmsProductCountry> getCountries() { return countries; }
public void setCountries(List<BmsProductCountry> 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();
}
}
@@ -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();
}
}
@@ -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<BmsProductCountry> selectBmsProductCountryList(BmsProductCountry country);
public int insertBmsProductCountry(BmsProductCountry country);
public int deleteByProductCode(String productCode);
public int deleteByProductCodes(String[] productCodes);
}
@@ -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<BmsProduct> selectBmsProductList(BmsProduct product);
public int insertBmsProduct(BmsProduct product);
public int updateBmsProduct(BmsProduct product);
public int deleteBmsProductByIds(Long[] productIds);
}
@@ -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<BmsProduct> selectBmsProductList(BmsProduct product);
public int insertBmsProduct(BmsProduct product);
public int updateBmsProduct(BmsProduct product);
public int deleteBmsProductByIds(Long[] productIds);
public List<BmsProductCountry> 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;
}
@@ -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<String> 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<W\u22641 */
private static final Pattern WEIGHT_RANGE_PATTERN = Pattern.compile(
"(\\d+(?:\\.\\d+)?)\\s*[<\\u2264]=?\\s*W\\s*[<\\u2264]=?\\s*(\\d+(?:\\.\\d+)?)",
Pattern.CASE_INSENSITIVE);
@Autowired
private BmsProductMapper productMapper;
@Autowired
private BmsProductCountryMapper countryMapper;
@Autowired
private BmsRegionMapper regionMapper;
@Override
public BmsProduct selectBmsProductById(Long productId)
{
return productMapper.selectBmsProductById(productId);
}
@Override
public List<BmsProduct> 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<String> 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<BmsProductCountry> 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<BmsProduct> products = parseCatalog(catalog);
if (products.isEmpty())
{
throw new ServiceException("\u4ea7\u54c1\u76ee\u5f55\u65e0\u6709\u6548\u6570\u636e\u884c");
}
Map<String, BmsRegion> 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<String> unmatchedCountries = new HashSet<>();
StringBuilder failureMsg = new StringBuilder();
for (BmsProduct product : products)
{
try
{
product.setSourceSheet(product.getProductName());
product.setCreateBy(operName);
product.setUpdateBy(operName);
List<BmsProductCountry> 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("<br/>").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"
+ "<br/>\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 += "<br/>\u672a\u5339\u914d\u56fd\u5bb6\u8868\uff08country_code \u4e3a\u7a7a\uff09\uff1a" + String.join("\u3001", unmatchedCountries);
}
return msg;
}
}
private void saveCountries(String productCode, List<BmsProductCountry> countries)
{
for (BmsProductCountry c : countries)
{
c.setProductCode(productCode);
countryMapper.insertBmsProductCountry(c);
}
}
private List<BmsProduct> parseCatalog(Sheet sheet)
{
List<BmsProduct> 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<BmsProductCountry> parseProductSheet(Workbook workbook, BmsProduct product,
Map<String, BmsRegion> regionByNameCn, Set<String> 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<BmsProductCountry> 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<String, BmsRegion> loadRegionByNameCn()
{
BmsRegion query = new BmsRegion();
query.setStatus("0");
List<BmsRegion> regions = regionMapper.selectRegionList(query);
Map<String, BmsRegion> 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<String, BmsRegion> 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<BmsRegion> 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;
}
}
@@ -34,6 +34,14 @@ public interface BmsRegionMapper
*/
public BmsRegion selectRegionByCode2(String countryCode2);
/**
* 根据中文名精确查询区域
*
* @param countryNameCn 中文名
* @return 区域信息
*/
public BmsRegion selectRegionByNameCn(String countryNameCn);
/**
* 新增区域
*
@@ -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<String, Object> translations = (Map<String, Object>) country.get("translations");
if (translations != null)
{
Map<String, Object> cmn = (Map<String, Object>) translations.get("cmn");
if (cmn != null)
Map<String, Object> zho = (Map<String, Object>) 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"));