修改导入问题

This commit is contained in:
wuweihua
2026-09-14 18:56:31 +08:00
parent c89c1340b0
commit 6146525e62
8 changed files with 312 additions and 93 deletions
@@ -101,6 +101,9 @@ price.product.import.needCodeEffUnique=无法新增「同代码、不同生效
price.product.import.success=导入成功:新增 {0} 个产品,跳过 {1} 个(同代码同生效日不覆盖);写入国家/重量段明细 {2} 行。
price.product.import.success.detail1=产品主数据按「产品代码+生效日期」新增,不覆盖已有记录。国家明细:新生效日晚于上次则保留历史,上次失效日=本次生效日。
price.product.import.success.detail2=范围:产品目录 + 重量段 + 运费/挂号费 + 尺寸/尾程/时效;国家码来自 bms_region。
price.product.import.noSheet=未找到产品明细sheet(需与产品名称一致):{0}
price.product.import.noCountry=产品明细sheet无有效国家/重量段数据:{0}
price.product.import.euEmpty=国家表无欧盟成员(is_eu=1),无法展开「欧盟」
price.product.import.success.unmatched=未匹配国家表(country_code 为空):{0}
# 报价试算 TmsPriceQuote(对外 TMS API 错误码,保持英文契约)
price.quote.priceNotHit=没有产品/国家/重量段绑定,暂无价格
@@ -23,7 +23,11 @@ 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.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.web.multipart.MultipartFile;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.MessageUtils;
@@ -73,6 +77,9 @@ public class BmsProductServiceImpl implements IBmsProductService
@Autowired
private IPrcSurchargeRelService surchargeRelService;
@Autowired
private PlatformTransactionManager transactionManager;
@Override
public BmsProduct selectBmsProductById(Long productId)
{
@@ -207,7 +214,6 @@ public class BmsProductServiceImpl implements IBmsProductService
}
@Override
@Transactional(rollbackFor = Exception.class)
public String importProductExcel(MultipartFile file, boolean updateSupport, String operName) throws Exception
{
if (file == null || file.isEmpty())
@@ -234,12 +240,14 @@ public class BmsProductServiceImpl implements IBmsProductService
{
throw new ServiceException(MessageUtils.message("price.product.import.noData"));
}
products = dedupeCatalogProducts(products);
Map<String, BmsRegion> regionByNameCn = loadRegionByNameCn();
if (regionByNameCn.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.product.import.regionEmpty"));
}
List<BmsRegion> euRegions = loadEuRegions(regionByNameCn);
int successNum = 0;
int skipNum = 0;
@@ -249,61 +257,35 @@ public class BmsProductServiceImpl implements IBmsProductService
for (BmsProduct product : products)
{
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
TransactionStatus tx = transactionManager.getTransaction(def);
try
{
product.setSourceSheet(product.getProductName());
product.setCreateBy(operName);
product.setUpdateBy(operName);
List<BmsProductCountry> countries = parseProductSheet(workbook, product, regionByNameCn, unmatchedCountries);
// Sheet header date is the price version; catalog column is often stale
Date sheetEff = resolveImportEffectiveDate(countries);
Date newEff = toDateOnly(sheetEff != null ? sheetEff : product.getEffectiveDate());
product.setEffectiveDate(newEff);
if (newEff == null)
{
failureMsg.append("<br/>").append(product.getProductCode()).append("\uff1a")
.append(MessageUtils.message("price.product.import.effEmpty"));
continue;
}
BmsProduct existing = productMapper.selectBmsProductByCodeAndDate(
product.getProductCode(), newEff);
if (existing != null)
{
skipNum++;
}
else
{
applyProductStatus(product);
try
{
productMapper.insertBmsProduct(product);
}
catch (Exception e)
{
if (isProductCodeUniqueViolation(e))
{
throw new ServiceException(MessageUtils.message("price.product.import.needCodeEffUnique"));
}
throw e;
}
productMapper.expireOpenByProductCode(product.getProductCode(), newEff, product.getProductId());
productMapper.refreshStatusByDates();
countryNum += saveCountriesVersioned(product, countries);
successNum++;
}
int[] nums = importOneProduct(workbook, product, operName, regionByNameCn, euRegions,
unmatchedCountries);
transactionManager.commit(tx);
successNum += nums[0];
skipNum += nums[1];
countryNum += nums[2];
}
catch (Exception e)
{
failureMsg.append("<br/>").append(product.getProductCode()).append("\uff1a").append(e.getMessage());
transactionManager.rollback(tx);
String tip = e.getMessage();
if (StringUtils.isEmpty(tip))
{
tip = e.getClass().getSimpleName();
}
failureMsg.append("<br/>").append(product.getProductCode()).append("\uff1a").append(tip);
}
}
if (failureMsg.length() > 0)
{
throw new ServiceException(MessageUtils.message("price.product.import.partialFail",
successNum, skipNum, failureMsg.toString()));
// Do not throw: each product already committed/rolled back independently
return MessageUtils.message("price.product.import.partialFail",
Integer.valueOf(successNum), Integer.valueOf(skipNum), failureMsg.toString());
}
String msg = MessageUtils.message("price.product.import.success", successNum, skipNum, countryNum)
+ "<br/>" + MessageUtils.message("price.product.import.success.detail1")
@@ -316,10 +298,68 @@ public class BmsProductServiceImpl implements IBmsProductService
}
}
/** @return int[]{added, skipped, countryRows} */
private int[] importOneProduct(Workbook workbook, BmsProduct product, String operName,
Map<String, BmsRegion> regionByNameCn, List<BmsRegion> euRegions, Set<String> unmatchedCountries)
{
product.setSourceSheet(product.getProductName());
product.setCreateBy(operName);
product.setUpdateBy(operName);
Sheet productSheet = findProductSheet(workbook, product);
if (productSheet == null)
{
throw new ServiceException(MessageUtils.message("price.product.import.noSheet",
product.getProductName()));
}
product.setSourceSheet(productSheet.getSheetName());
List<BmsProductCountry> countries = parseProductSheet(productSheet, product,
regionByNameCn, euRegions, unmatchedCountries);
if (countries == null || countries.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.product.import.noCountry",
productSheet.getSheetName()));
}
Date sheetEff = resolveImportEffectiveDate(countries);
Date newEff = toDateOnly(sheetEff != null ? sheetEff : product.getEffectiveDate());
product.setEffectiveDate(newEff);
if (newEff == null)
{
throw new ServiceException(MessageUtils.message("price.product.import.effEmpty"));
}
BmsProduct existing = productMapper.selectBmsProductByCodeAndDate(
product.getProductCode(), newEff);
if (existing != null)
{
product.setProductId(existing.getProductId());
int countryRows = saveCountriesVersioned(product, countries);
return new int[] { 0, 1, countryRows };
}
applyProductStatus(product);
try
{
productMapper.insertBmsProduct(product);
}
catch (Exception e)
{
if (isProductCodeUniqueViolation(e))
{
throw new ServiceException(MessageUtils.message("price.product.import.needCodeEffUnique"));
}
throw e;
}
productMapper.expireOpenByProductCode(product.getProductCode(), newEff, product.getProductId());
productMapper.refreshStatusByDates();
int countryRows = saveCountriesVersioned(product, countries);
return new int[] { 1, 0, countryRows };
}
/**
* Versioned country save:
* - newEff &gt; last open: close old (expire_date = newEff), insert new (keep history)
* - newEff equals last open: skip (never overwrite)
* - newEff equals last open: replace open rows
* - newEff &lt; last open: reject
*/
private int saveCountriesVersioned(BmsProduct product, List<BmsProductCountry> countries)
@@ -347,8 +387,8 @@ public class BmsProductServiceImpl implements IBmsProductService
}
else if (lastOpenEff != null)
{
// Same effective day: never overwrite existing country rows
return 0;
// Same effective day: replace open rows so re-import can fill/fix details
countryMapper.deleteOpenByProductCodeAndEffectiveDate(productCode, newEff);
}
for (BmsProductCountry c : countries)
@@ -543,27 +583,141 @@ public class BmsProductServiceImpl implements IBmsProductService
return list;
}
private List<BmsProductCountry> parseProductSheet(Workbook workbook, BmsProduct product,
Map<String, BmsRegion> regionByNameCn, Set<String> unmatchedCountries)
/** Keep first row per product_code (catalog often repeats code across country rows). */
private List<BmsProduct> dedupeCatalogProducts(List<BmsProduct> products)
{
String sheetName = product.getProductName();
Sheet sheet = workbook.getSheet(sheetName);
if (sheet == null)
Map<String, BmsProduct> map = new HashMap<>();
for (BmsProduct p : products)
{
for (int i = 0; i < workbook.getNumberOfSheets(); i++)
if (p == null || StringUtils.isEmpty(p.getProductCode()))
{
String name = workbook.getSheetName(i);
if (SKIP_SHEETS.contains(name))
{
continue;
}
if (name != null && name.trim().equals(sheetName.trim()))
{
sheet = workbook.getSheetAt(i);
break;
}
continue;
}
String code = p.getProductCode().trim().toUpperCase(Locale.ROOT);
if (!map.containsKey(code))
{
map.put(code, p);
}
}
return new ArrayList<>(map.values());
}
private Sheet findProductSheet(Workbook workbook, BmsProduct product)
{
if (workbook == null || product == null)
{
return null;
}
String wantName = product.getProductName();
String wantCode = product.getProductCode();
if (StringUtils.isNotEmpty(wantName))
{
Sheet exact = workbook.getSheet(wantName.trim());
if (exact != null)
{
return exact;
}
}
String nameKey = normalizeSheetKey(wantName);
String codeKey = normalizeSheetKey(wantCode);
Sheet best = null;
int bestScore = 0;
for (int i = 0; i < workbook.getNumberOfSheets(); i++)
{
String name = workbook.getSheetName(i);
if (name == null || SKIP_SHEETS.contains(name))
{
continue;
}
String key = normalizeSheetKey(name);
if (StringUtils.isEmpty(key))
{
continue;
}
int score = 0;
if (StringUtils.isNotEmpty(nameKey) && key.equals(nameKey))
{
return workbook.getSheetAt(i);
}
if (StringUtils.isNotEmpty(nameKey) && key.contains(nameKey))
{
score = 100;
}
else if (StringUtils.isNotEmpty(nameKey) && nameKey.contains(key) && key.length() >= 4)
{
score = 80;
}
else if (StringUtils.isNotEmpty(codeKey) && key.contains(codeKey))
{
score = 60;
}
if (score > bestScore)
{
bestScore = score;
best = workbook.getSheetAt(i);
}
}
return bestScore >= 60 ? best : null;
}
private static String normalizeSheetKey(String s)
{
if (s == null)
{
return "";
}
String out = s.trim().toLowerCase(Locale.ROOT);
// unify dash variants: - \u2013 \u2014 \uff0d
out = out.replace('\u2013', '-').replace('\u2014', '-').replace('\uff0d', '-');
out = out.replace(" ", "").replace("\u3000", "");
return out;
}
private List<BmsRegion> loadEuRegions(Map<String, BmsRegion> regionByNameCn)
{
List<BmsRegion> list = new ArrayList<>();
if (regionByNameCn == null)
{
return list;
}
Set<String> seen = new HashSet<>();
for (BmsRegion region : regionByNameCn.values())
{
if (region == null || !"1".equals(region.getIsEu()))
{
continue;
}
if (StringUtils.isEmpty(region.getCountryNameCn()) || StringUtils.isEmpty(region.getCountryCode2()))
{
continue;
}
String code = region.getCountryCode2().toUpperCase(Locale.ROOT);
if (seen.contains(code))
{
continue;
}
seen.add(code);
list.add(region);
}
return list;
}
private static boolean isEuZoneName(String name)
{
if (StringUtils.isEmpty(name))
{
return false;
}
String n = name.trim();
return "\u6b27\u76df".equals(n)
|| "EU".equalsIgnoreCase(n)
|| "\u6b27\u6d32\u8054\u76df".equals(n)
|| "\u6b27\u76df\u56fd\u5bb6".equals(n);
}
private List<BmsProductCountry> parseProductSheet(Sheet sheet, BmsProduct product,
Map<String, BmsRegion> regionByNameCn, List<BmsRegion> euRegions, Set<String> unmatchedCountries)
{
if (sheet == null)
{
return new ArrayList<>();
@@ -631,7 +785,7 @@ public class BmsProductServiceImpl implements IBmsProductService
}
if (StringUtils.isNotEmpty(countryCell))
{
currentCountry = countryCell.trim();
currentCountry = normalizeCountryName(countryCell);
currentSize = emptyToNull(cellString(row.getCell(colSize)));
currentLastMile = emptyToNull(cellString(row.getCell(colLastMile)));
currentLeadTime = emptyToNull(cellString(row.getCell(colLeadTime)));
@@ -645,32 +799,54 @@ public class BmsProductServiceImpl implements IBmsProductService
{
continue;
}
BigDecimal freight = cellDecimal(row.getCell(colFreight));
BigDecimal registration = cellDecimal(row.getCell(colRegister));
Date eff = sheetEffective != null ? sheetEffective : product.getEffectiveDate();
BmsRegion region = resolveRegion(currentCountry, regionByNameCn);
BmsProductCountry c = new BmsProductCountry();
if (region != null)
{
c.setCountryName(region.getCountryNameCn());
c.setCountryCode(region.getCountryCode2());
list.add(buildCountryRow(region.getCountryNameCn(), region.getCountryCode2(),
range, freight, registration, currentSize, currentLastMile, currentLeadTime, eff));
continue;
}
else
if (isEuZoneName(currentCountry))
{
c.setCountryName(currentCountry);
c.setCountryCode(null);
unmatchedCountries.add(currentCountry);
if (euRegions == null || euRegions.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.product.import.euEmpty"));
}
for (BmsRegion eu : euRegions)
{
list.add(buildCountryRow(eu.getCountryNameCn(), eu.getCountryCode2(),
range, freight, registration, currentSize, currentLastMile, currentLeadTime, eff));
}
continue;
}
c.setWeightMin(range[0]);
c.setWeightMax(range[1]);
c.setFreightPrice(cellDecimal(row.getCell(colFreight)));
c.setRegistrationFee(cellDecimal(row.getCell(colRegister)));
c.setSizeLimit(currentSize);
c.setLastMile(currentLastMile);
c.setLeadTime(currentLeadTime);
c.setEffectiveDate(sheetEffective != null ? sheetEffective : product.getEffectiveDate());
list.add(c);
unmatchedCountries.add(currentCountry);
list.add(buildCountryRow(currentCountry, null,
range, freight, registration, currentSize, currentLastMile, currentLeadTime, eff));
}
return list;
}
private BmsProductCountry buildCountryRow(String countryName, String countryCode, BigDecimal[] range,
BigDecimal freight, BigDecimal registration, String sizeLimit, String lastMile, String leadTime, Date eff)
{
BmsProductCountry c = new BmsProductCountry();
c.setCountryName(countryName);
c.setCountryCode(countryCode);
c.setWeightMin(range[0]);
c.setWeightMax(range[1]);
c.setFreightPrice(freight);
c.setRegistrationFee(registration);
c.setSizeLimit(sizeLimit);
c.setLastMile(lastMile);
c.setLeadTime(leadTime);
c.setEffectiveDate(eff);
return c;
}
/**
* Load bms_region map: country_name_cn -> region (for country_code_2).
*/
@@ -723,11 +899,48 @@ public class BmsProductServiceImpl implements IBmsProductService
candidates.add(region);
}
}
if (candidates.isEmpty())
{
return null;
}
if (candidates.size() == 1)
{
return candidates.get(0);
}
return null;
// Prefer longest Chinese name match (e.g. 捷克共和国 over 捷克)
BmsRegion best = candidates.get(0);
for (int i = 1; i < candidates.size(); i++)
{
String cn = candidates.get(i).getCountryNameCn();
if (cn != null && cn.length() > best.getCountryNameCn().length())
{
best = candidates.get(i);
}
}
return best;
}
/** Strip Excel footnotes like "澳大利亚(1区)\\n分区点击" */
private static String normalizeCountryName(String name)
{
if (name == null)
{
return null;
}
String n = name.trim();
int cut = n.length();
int nl = n.indexOf('\n');
if (nl >= 0 && nl < cut)
{
cut = nl;
}
int cr = n.indexOf('\r');
if (cr >= 0 && cr < cut)
{
cut = cr;
}
n = n.substring(0, cut).trim();
return n.isEmpty() ? null : n;
}
private BigDecimal[] parseWeightRange(String label)
@@ -747,7 +747,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
private static String formatWeightLabel(BigDecimal min, BigDecimal max)
{
return stripTrailingZeros(min) + "\u2264W\u2264" + stripTrailingZeros(max);
return stripTrailingZeros(min) + "<W\u2264" + stripTrailingZeros(max);
}
private static String stripTrailingZeros(BigDecimal v)
@@ -897,7 +897,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
private static String formatWeightLabel(BigDecimal min, BigDecimal max)
{
return strip(min) + "\u2264W\u2264" + strip(max);
return strip(min) + "<W\u2264" + strip(max);
}
private static String strip(BigDecimal v)
@@ -25,6 +25,7 @@
<script setup>
import SimpleTable from '@/components/SimpleTable'
import { listProductCountries } from '@/api/price/product'
import { formatWeightLabel } from '@/views/price/set/utils/priceSet'
const columns = [
{ prop: 'countryName', label: '国家', width: 90, showOverflowTooltip: true },
@@ -32,9 +33,11 @@ const columns = [
{
prop: 'weightMin',
label: '重量段(KG)',
width: 120,
width: 130,
align: 'center',
formatter: (row) => (row.weightMin != null || row.weightMax != null ? (row.weightMin + ' ~ ' + row.weightMax) : '-')
formatter: (row) => (row.weightMin != null || row.weightMax != null
? formatWeightLabel(row.weightMin, row.weightMax)
: '-')
},
{ prop: 'freightPrice', label: '运费(RMB/KG)', width: 110, align: 'right' },
{ prop: 'registrationFee', label: '挂号费(RMB/票)', width: 120, align: 'right' },
+1 -1
View File
@@ -294,7 +294,7 @@ const rules = {
}
function loadProducts() {
listProduct({ pageNum: 1, pageSize: 500, status: '0' }).then(res => {
listProduct({ pageNum: 1, pageSize: 500, status: '1' }).then(res => {
productOptions.value = res.rows || []
})
}
@@ -30,9 +30,9 @@ export function usePriceSetRefData() {
const productMap = computed(() => new Map(productOptions.value.map(p => [p.value, p])))
/** 加载可选产品(在售) */
/** 加载可选产品(已生效) */
function loadProducts() {
return listProduct({ status: '0', pageNum: 1, pageSize: 1000 }).then(res => {
return listProduct({ status: '1', pageNum: 1, pageSize: 1000 }).then(res => {
productOptions.value = (res.rows || []).map(p => ({
value: p.productCode,
label: `${p.productCode} ${p.productName || ''}`.trim(),
@@ -162,7 +162,7 @@ export function useDetailCascade(refData) {
if (opt.registrationFee != null) row.fvRegistrationFee = Number(opt.registrationFee)
}
/** 回显历史明细行:补齐字段,并把旧重量段文案归一化为 min≤W≤max */
/** hydrate detail row; normalize weight label to min<W\u2264max */
function hydrateRow(d, setType) {
const row = toEditableDetailRow(d)
if (!row.productCode) return row
@@ -94,9 +94,9 @@ export function trimNum(v) {
return String(parseFloat(n.toFixed(6)))
}
/** 重量段展示文案:1≤W≤2 */
/** Weight label: left-open right-closed, e.g. 0.3<W\u22641 */
export function formatWeightLabel(min, max) {
return `${trimNum(min)}≤W≤${trimNum(max)}`
return `${trimNum(min)}<W≤${trimNum(max)}`
}
/** 国家唯一键:优先国家三字码,其次国家名称 */