3 Commits

Author SHA1 Message Date
wuweihua 7679a7d091 套餐加分区功能 2026-09-15 14:10:44 +08:00
wuweihua 76208604bb 增加分区修改 2026-09-15 11:23:39 +08:00
wuweihua c28b5a2b6e 增加分区功能 2026-09-15 10:28:07 +08:00
49 changed files with 2855 additions and 1417 deletions
@@ -12,133 +12,136 @@ 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.XtdZoneCountry;
import com.ruoyi.price.domain.XtdZoneDivision;
import com.ruoyi.price.domain.XtdZoneScheme;
import com.ruoyi.price.service.IXtdZoneService;
import com.ruoyi.price.domain.PrcZone;
import com.ruoyi.price.domain.PrcZoneDetail;
import com.ruoyi.price.domain.PrcZoneRegion;
import com.ruoyi.price.service.IPrcZoneService;
/**
* 分区管理
*
* @author ruoyi
* Zone management: bms_region -> zone -> postcode detail
*/
@RestController
@RequestMapping("/price/zone")
public class ZoneController extends BaseController
{
@Autowired
private IXtdZoneService zoneService;
private IPrcZoneService zoneService;
// ========== countries that already have zones (from bms_region) ==========
@PreAuthorize("@ss.hasPermi('price:zone:list')")
@GetMapping("/scheme/list")
public TableDataInfo schemeList(XtdZoneScheme scheme)
@GetMapping("/country/list")
public TableDataInfo countryList(PrcZoneRegion query)
{
startPage();
List<XtdZoneScheme> list = zoneService.selectXtdZoneSchemeList(scheme);
List<PrcZoneRegion> list = zoneService.selectZoneRegionList(query);
return getDataTable(list);
}
@PreAuthorize("@ss.hasPermi('price:zone:query')")
@GetMapping("/scheme/{zsId}")
public AjaxResult getScheme(@PathVariable Long zsId)
@GetMapping("/country/{regionId}")
public AjaxResult getCountry(@PathVariable Long regionId)
{
return success(zoneService.selectXtdZoneSchemeById(zsId));
}
@PreAuthorize("@ss.hasPermi('price:zone:add')")
@Log(title = "分区方案", businessType = BusinessType.INSERT)
@PostMapping("/scheme")
public AjaxResult addScheme(@Validated @RequestBody XtdZoneScheme scheme)
{
scheme.setCreateBy(getUsername());
return toAjax(zoneService.insertXtdZoneScheme(scheme));
}
@PreAuthorize("@ss.hasPermi('price:zone:edit')")
@Log(title = "分区方案", businessType = BusinessType.UPDATE)
@PutMapping("/scheme")
public AjaxResult editScheme(@Validated @RequestBody XtdZoneScheme scheme)
{
scheme.setUpdateBy(getUsername());
return toAjax(zoneService.updateXtdZoneScheme(scheme));
return success(zoneService.selectZoneRegionById(regionId));
}
@PreAuthorize("@ss.hasPermi('price:zone:remove')")
@Log(title = "分区方案", businessType = BusinessType.DELETE)
@DeleteMapping("/scheme/{zsIds}")
public AjaxResult removeScheme(@PathVariable Long[] zsIds)
@Log(title = "ZoneRegion", businessType = BusinessType.DELETE)
@DeleteMapping("/country/{regionIds}")
public AjaxResult removeCountryZones(@PathVariable Long[] regionIds)
{
return toAjax(zoneService.deleteXtdZoneSchemeByIds(zsIds));
return toAjax(zoneService.deleteZonesByRegionIds(regionIds));
}
// ========== zone ==========
@PreAuthorize("@ss.hasPermi('price:zone:list')")
@GetMapping("/division/list")
public TableDataInfo divisionList(XtdZoneDivision division)
@GetMapping("/item/list")
public TableDataInfo zoneList(PrcZone zone)
{
startPage();
List<XtdZoneDivision> list = zoneService.selectXtdZoneDivisionList(division);
List<PrcZone> list = zoneService.selectPrcZoneList(zone);
return getDataTable(list);
}
@PreAuthorize("@ss.hasPermi('price:zone:query')")
@GetMapping("/item/{zoneId}")
public AjaxResult getZone(@PathVariable Long zoneId)
{
return success(zoneService.selectPrcZoneById(zoneId));
}
@PreAuthorize("@ss.hasPermi('price:zone:add')")
@Log(title = "Zone", businessType = BusinessType.INSERT)
@PostMapping("/item")
public AjaxResult addZone(@Validated @RequestBody PrcZone zone)
{
return toAjax(zoneService.insertPrcZone(zone));
}
@PreAuthorize("@ss.hasPermi('price:zone:edit')")
@Log(title = "Zone", businessType = BusinessType.UPDATE)
@PutMapping("/item")
public AjaxResult editZone(@Validated @RequestBody PrcZone zone)
{
return toAjax(zoneService.updatePrcZone(zone));
}
@PreAuthorize("@ss.hasPermi('price:zone:remove')")
@Log(title = "Zone", businessType = BusinessType.DELETE)
@DeleteMapping("/item/{zoneIds}")
public AjaxResult removeZone(@PathVariable Long[] zoneIds)
{
return toAjax(zoneService.deletePrcZoneByIds(zoneIds));
}
// ========== detail ==========
@PreAuthorize("@ss.hasPermi('price:zone:list')")
@GetMapping("/detail/list")
public TableDataInfo detailList(PrcZoneDetail detail)
{
startPage();
List<PrcZoneDetail> list = zoneService.selectPrcZoneDetailList(detail);
return getDataTable(list);
}
@PreAuthorize("@ss.hasPermi('price:zone:add')")
@Log(title = "分区区划", businessType = BusinessType.INSERT)
@PostMapping("/division")
public AjaxResult addDivision(@Validated @RequestBody XtdZoneDivision division)
@Log(title = "ZoneDetail", businessType = BusinessType.INSERT)
@PostMapping("/detail")
public AjaxResult addDetail(@Validated @RequestBody PrcZoneDetail detail)
{
return toAjax(zoneService.insertXtdZoneDivision(division));
return toAjax(zoneService.insertPrcZoneDetail(detail));
}
@PreAuthorize("@ss.hasPermi('price:zone:edit')")
@Log(title = "分区区划", businessType = BusinessType.UPDATE)
@PutMapping("/division")
public AjaxResult editDivision(@Validated @RequestBody XtdZoneDivision division)
@Log(title = "ZoneDetail", businessType = BusinessType.UPDATE)
@PutMapping("/detail")
public AjaxResult editDetail(@Validated @RequestBody PrcZoneDetail detail)
{
return toAjax(zoneService.updateXtdZoneDivision(division));
return toAjax(zoneService.updatePrcZoneDetail(detail));
}
@PreAuthorize("@ss.hasPermi('price:zone:remove')")
@Log(title = "分区区划", businessType = BusinessType.DELETE)
@DeleteMapping("/division/{zdIds}")
public AjaxResult removeDivision(@PathVariable Long[] zdIds)
@Log(title = "ZoneDetail", businessType = BusinessType.DELETE)
@DeleteMapping("/detail/{detailIds}")
public AjaxResult removeDetail(@PathVariable Long[] detailIds)
{
return toAjax(zoneService.deleteXtdZoneDivisionByIds(zdIds));
}
@PreAuthorize("@ss.hasPermi('price:zone:list')")
@GetMapping("/country/list")
public TableDataInfo countryList(XtdZoneCountry country)
{
startPage();
List<XtdZoneCountry> list = zoneService.selectXtdZoneCountryList(country);
return getDataTable(list);
return toAjax(zoneService.deletePrcZoneDetailByIds(detailIds));
}
@PreAuthorize("@ss.hasPermi('price:zone:add')")
@Log(title = "分区国家", businessType = BusinessType.INSERT)
@PostMapping("/country")
public AjaxResult addCountry(@Validated @RequestBody XtdZoneCountry country)
@Log(title = "ZoneImport", businessType = BusinessType.IMPORT)
@PostMapping("/importData")
public AjaxResult importData(MultipartFile file) throws Exception
{
return toAjax(zoneService.insertXtdZoneCountry(country));
}
@PreAuthorize("@ss.hasPermi('price:zone:edit')")
@Log(title = "分区国家", businessType = BusinessType.UPDATE)
@PutMapping("/country")
public AjaxResult editCountry(@Validated @RequestBody XtdZoneCountry country)
{
return toAjax(zoneService.updateXtdZoneCountry(country));
}
@PreAuthorize("@ss.hasPermi('price:zone:remove')")
@Log(title = "分区国家", businessType = BusinessType.DELETE)
@DeleteMapping("/country/{zcIds}")
public AjaxResult removeCountry(@PathVariable Long[] zcIds)
{
return toAjax(zoneService.deleteXtdZoneCountryByIds(zcIds));
String message = zoneService.importZoneExcel(file, getUsername());
return success(message);
}
}
@@ -70,6 +70,7 @@ price.set.detail.country.empty=固定价明细必须填写国家
price.set.detail.weight.empty=固定价明细必须填写重量段
price.set.detail.freight.empty=固定价明细必须填写运费单价
price.set.detail.registration.empty=固定价明细必须填写挂号费
price.set.detail.zone.empty=固定价明细必须选择分区
price.set.detail.roundMode.invalid=取值方式仅支持 UP / DOWN / HALF_UP
price.set.customer.notExists=客户绑定不存在
price.set.customer.unbindAudit=审核中的套餐不允许解绑客户
@@ -82,8 +83,18 @@ price.set.customer.priority.invalid=优先级必须大于等于 1
# 重量段 PrcWeight
price.weight.range.empty=起始重量和结束重量不能为空
price.weight.range.invalid=起始重量必须小于结束重量
# 分区 XtdZone
price.zone.delete.hasDivision=分区方案【{0}】存在区划明细,不允许删除
# Zone PrcZone (country -> zone -> postcode)
price.zone.import.fileEmpty=上传文件不能为空
price.zone.import.fileType=仅支持 xls / xlsx 文件
price.zone.import.noSheet=未找到可导入的分区 sheet(澳大利亚分区/加拿大偏远附加费/法国MR邮编/日本附加费/英国附加费)
price.zone.import.noData=分区 sheet 无有效邮编/分区数据
price.zone.import.success=导入成功:{0} 个国家,{1} 个分区,{2} 条邮编明细({3})
price.zone.import.unmatched=;未匹配国家已跳过:{0}
price.zone.import.regionNotFound=未在 bms_region 匹配到国家:{0}
price.zone.import.regionEmpty=bms_region 无可用国家数据,请先同步/维护国家表
price.zone.region.notFound=国家不存在或已删除
price.zone.region.required=请选择国家(bms_region)
price.zone.countryCode.empty=国家编码不能为空,请检查 bms_region.country_code_2
# 产品导入 BmsProduct
price.product.codeDate.exists=同一产品代码与生效日期已存在,不能覆盖历史数据
price.product.effectiveDate.empty=生效日期不能为空
@@ -18,6 +18,12 @@ public class BmsProductCountry implements Serializable
private String productCode;
private String countryName;
private String countryCode;
/** prc_zone.zone_id; null when product country has no zone */
private Long zoneId;
/** e.g. Z1; null when no zone */
private String zoneCode;
/** e.g. Australia(Z1); null when no zone */
private String zoneName;
private BigDecimal weightMin;
private BigDecimal weightMax;
private BigDecimal freightPrice;
@@ -18,6 +18,12 @@ public class PrcPriceSetDetail implements Serializable
private String productName;
private String countryCode;
private String countryName;
/** prc_zone.zone_id; null when country has no zone */
private Long zoneId;
/** e.g. Z1; null when no zone */
private String zoneCode;
/** zone display name; null when no zone */
private String zoneName;
private Long zdId;
private Long wsdId;
private String weightLabel;
@@ -0,0 +1,30 @@
package com.ruoyi.price.domain;
import java.io.Serializable;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
/** Zone under bms_region: prc_zone */
@Data
public class PrcZone implements Serializable
{
private static final long serialVersionUID = 1L;
private Long zoneId;
private Long regionId;
/** ISO2 country code stored on prc_zone */
private String countryCode;
private String zoneCode;
private String zoneName;
private Integer zoneSort;
private String remark;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
/** list display, not a table column */
private Integer detailCount;
/** list display from bms_region */
private String countryNameCn;
}
@@ -5,16 +5,15 @@ import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
/** 分区国家 xtd_zonecountry */
/** Zone postcode detail prc_zone_detail */
@Data
public class XtdZoneCountry implements Serializable
public class PrcZoneDetail implements Serializable
{
private static final long serialVersionUID = 1L;
private Long zcId;
private Long zdId;
private String countryCode;
private String zcDividekind;
private Long detailId;
private Long zoneId;
private String postcode;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@@ -0,0 +1,21 @@
package com.ruoyi.price.domain;
import java.io.Serializable;
import lombok.Data;
/**
* Zone country list row: bms_region + zone aggregates (not a table)
*/
@Data
public class PrcZoneRegion implements Serializable
{
private static final long serialVersionUID = 1L;
private Long regionId;
private String countryNameCn;
private String countryNameEn;
private String countryCode2;
private String status;
private Integer zoneCount;
private Integer detailCount;
}
@@ -1,27 +0,0 @@
package com.ruoyi.price.domain;
import java.io.Serializable;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
/** 分区区划 xtd_zone_division */
@Data
public class XtdZoneDivision implements Serializable
{
private static final long serialVersionUID = 1L;
private Long zdId;
private Long zsId;
private Integer zdSort;
private String zdCode;
private String znName;
private String znEname;
private String znNote;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
/** 列表展示,非表字段 */
private Integer countryCount;
}
@@ -1,24 +0,0 @@
package com.ruoyi.price.domain;
import com.ruoyi.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/** 分区方案 xtd_zone_scheme */
@Data
@EqualsAndHashCode(callSuper = true)
public class XtdZoneScheme extends BaseEntity
{
private static final long serialVersionUID = 1L;
private Long zsId;
private String zsName;
/** on / off */
private String ssCode;
private String pgCode;
private Long snId;
private String zsNote;
private Long tmsId;
/** 列表展示,非表字段 */
private Integer divisionCount;
}
@@ -27,12 +27,13 @@ public interface PrcPriceSetDetailMapper
public int countByPriceSetId(Long priceSetId);
/** FIXED:按产品+国家+关联产品国家明细/重量段命中 */
/** FIXED: product + country + zone + productCountryId / weight */
public PrcPriceSetDetail selectFixedMatch(
@Param("priceSetId") Long priceSetId,
@Param("productCode") String productCode,
@Param("countryCode") String countryCode,
@Param("countryName") String countryName,
@Param("zoneCode") String zoneCode,
@Param("productCountryId") Long productCountryId,
@Param("weightLabel") String weightLabel);
@@ -0,0 +1,28 @@
package com.ruoyi.price.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.ruoyi.price.domain.PrcZoneDetail;
public interface PrcZoneDetailMapper
{
public PrcZoneDetail selectPrcZoneDetailById(Long detailId);
public List<PrcZoneDetail> selectPrcZoneDetailList(PrcZoneDetail detail);
public int countByZoneId(Long zoneId);
public int countByRegionId(Long regionId);
public int insertPrcZoneDetail(PrcZoneDetail detail);
public int insertPrcZoneDetailBatch(@Param("list") List<PrcZoneDetail> list);
public int updatePrcZoneDetail(PrcZoneDetail detail);
public int deletePrcZoneDetailByIds(Long[] detailIds);
public int deletePrcZoneDetailByZoneId(Long zoneId);
public int deletePrcZoneDetailByRegionId(Long regionId);
}
@@ -0,0 +1,30 @@
package com.ruoyi.price.mapper;
import java.util.List;
import com.ruoyi.price.domain.PrcZone;
import com.ruoyi.price.domain.PrcZoneRegion;
public interface PrcZoneMapper
{
public PrcZone selectPrcZoneById(Long zoneId);
public List<PrcZone> selectPrcZoneList(PrcZone zone);
public List<PrcZone> selectPrcZoneListByRegionId(Long regionId);
public List<PrcZoneRegion> selectZoneRegionList(PrcZoneRegion query);
public PrcZoneRegion selectZoneRegionById(Long regionId);
public int countByRegionId(Long regionId);
public int insertPrcZone(PrcZone zone);
public int updatePrcZone(PrcZone zone);
public int deletePrcZoneByIds(Long[] zoneIds);
public int deletePrcZoneByRegionId(Long regionId);
public int deletePrcZoneByRegionIds(Long[] regionIds);
}
@@ -1,26 +0,0 @@
package com.ruoyi.price.mapper;
import java.util.List;
import com.ruoyi.price.domain.XtdZoneCountry;
/**
* 分区国家 数据层
*
* @author ruoyi
*/
public interface XtdZoneCountryMapper
{
public XtdZoneCountry selectXtdZoneCountryById(Long zcId);
public List<XtdZoneCountry> selectXtdZoneCountryListByZdId(Long zdId);
public List<XtdZoneCountry> selectXtdZoneCountryList(XtdZoneCountry country);
public int insertXtdZoneCountry(XtdZoneCountry country);
public int updateXtdZoneCountry(XtdZoneCountry country);
public int deleteXtdZoneCountryByIds(Long[] zcIds);
public int countByZdId(Long zdId);
}
@@ -1,26 +0,0 @@
package com.ruoyi.price.mapper;
import java.util.List;
import com.ruoyi.price.domain.XtdZoneDivision;
/**
* 分区区划 数据层
*
* @author ruoyi
*/
public interface XtdZoneDivisionMapper
{
public XtdZoneDivision selectXtdZoneDivisionById(Long zdId);
public List<XtdZoneDivision> selectXtdZoneDivisionListByZsId(Long zsId);
public List<XtdZoneDivision> selectXtdZoneDivisionList(XtdZoneDivision division);
public int insertXtdZoneDivision(XtdZoneDivision division);
public int updateXtdZoneDivision(XtdZoneDivision division);
public int deleteXtdZoneDivisionByIds(Long[] zdIds);
public int countByZsId(Long zsId);
}
@@ -1,22 +0,0 @@
package com.ruoyi.price.mapper;
import java.util.List;
import com.ruoyi.price.domain.XtdZoneScheme;
/**
* 分区方案 数据层
*
* @author ruoyi
*/
public interface XtdZoneSchemeMapper
{
public XtdZoneScheme selectXtdZoneSchemeById(Long zsId);
public List<XtdZoneScheme> selectXtdZoneSchemeList(XtdZoneScheme scheme);
public int insertXtdZoneScheme(XtdZoneScheme scheme);
public int updateXtdZoneScheme(XtdZoneScheme scheme);
public int deleteXtdZoneSchemeByIds(Long[] zsIds);
}
@@ -0,0 +1,39 @@
package com.ruoyi.price.service;
import java.util.List;
import org.springframework.web.multipart.MultipartFile;
import com.ruoyi.price.domain.PrcZone;
import com.ruoyi.price.domain.PrcZoneDetail;
import com.ruoyi.price.domain.PrcZoneRegion;
public interface IPrcZoneService
{
public PrcZoneRegion selectZoneRegionById(Long regionId);
public List<PrcZoneRegion> selectZoneRegionList(PrcZoneRegion query);
/** Delete all zones (and details) for the given bms_region ids */
public int deleteZonesByRegionIds(Long[] regionIds);
public PrcZone selectPrcZoneById(Long zoneId);
public List<PrcZone> selectPrcZoneList(PrcZone zone);
public int insertPrcZone(PrcZone zone);
public int updatePrcZone(PrcZone zone);
public int deletePrcZoneByIds(Long[] zoneIds);
public PrcZoneDetail selectPrcZoneDetailById(Long detailId);
public List<PrcZoneDetail> selectPrcZoneDetailList(PrcZoneDetail detail);
public int insertPrcZoneDetail(PrcZoneDetail detail);
public int updatePrcZoneDetail(PrcZoneDetail detail);
public int deletePrcZoneDetailByIds(Long[] detailIds);
public String importZoneExcel(MultipartFile file, String operName) throws Exception;
}
@@ -1,44 +0,0 @@
package com.ruoyi.price.service;
import java.util.List;
import com.ruoyi.price.domain.XtdZoneCountry;
import com.ruoyi.price.domain.XtdZoneDivision;
import com.ruoyi.price.domain.XtdZoneScheme;
/**
* 分区管理 服务层
*
* @author ruoyi
*/
public interface IXtdZoneService
{
public XtdZoneScheme selectXtdZoneSchemeById(Long zsId);
public List<XtdZoneScheme> selectXtdZoneSchemeList(XtdZoneScheme scheme);
public int insertXtdZoneScheme(XtdZoneScheme scheme);
public int updateXtdZoneScheme(XtdZoneScheme scheme);
public int deleteXtdZoneSchemeByIds(Long[] zsIds);
public XtdZoneDivision selectXtdZoneDivisionById(Long zdId);
public List<XtdZoneDivision> selectXtdZoneDivisionList(XtdZoneDivision division);
public int insertXtdZoneDivision(XtdZoneDivision division);
public int updateXtdZoneDivision(XtdZoneDivision division);
public int deleteXtdZoneDivisionByIds(Long[] zdIds);
public XtdZoneCountry selectXtdZoneCountryById(Long zcId);
public List<XtdZoneCountry> selectXtdZoneCountryList(XtdZoneCountry country);
public int insertXtdZoneCountry(XtdZoneCountry country);
public int updateXtdZoneCountry(XtdZoneCountry country);
public int deleteXtdZoneCountryByIds(Long[] zcIds);
}
@@ -34,10 +34,13 @@ import com.ruoyi.common.utils.MessageUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.price.domain.BmsProduct;
import com.ruoyi.price.domain.BmsProductCountry;
import com.ruoyi.price.domain.PrcZone;
import com.ruoyi.price.mapper.BmsProductCountryMapper;
import com.ruoyi.price.mapper.BmsProductMapper;
import com.ruoyi.price.mapper.PrcZoneMapper;
import com.ruoyi.price.service.IBmsProductService;
import com.ruoyi.price.service.IPrcSurchargeRelService;
import com.ruoyi.price.util.ZoneCodeAbbrevs;
import com.ruoyi.system.domain.BmsRegion;
import com.ruoyi.system.mapper.BmsRegionMapper;
@@ -74,6 +77,9 @@ public class BmsProductServiceImpl implements IBmsProductService
@Autowired
private BmsRegionMapper regionMapper;
@Autowired
private PrcZoneMapper zoneMapper;
@Autowired
private IPrcSurchargeRelService surchargeRelService;
@@ -248,6 +254,7 @@ public class BmsProductServiceImpl implements IBmsProductService
throw new ServiceException(MessageUtils.message("price.product.import.regionEmpty"));
}
List<BmsRegion> euRegions = loadEuRegions(regionByNameCn);
Map<String, PrcZone> zoneByCountryAndCode = loadZoneByCountryAndCode();
int successNum = 0;
int skipNum = 0;
@@ -263,7 +270,7 @@ public class BmsProductServiceImpl implements IBmsProductService
try
{
int[] nums = importOneProduct(workbook, product, operName, regionByNameCn, euRegions,
unmatchedCountries);
zoneByCountryAndCode, unmatchedCountries);
transactionManager.commit(tx);
successNum += nums[0];
skipNum += nums[1];
@@ -300,7 +307,8 @@ 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)
Map<String, BmsRegion> regionByNameCn, List<BmsRegion> euRegions,
Map<String, PrcZone> zoneByCountryAndCode, Set<String> unmatchedCountries)
{
product.setSourceSheet(product.getProductName());
product.setCreateBy(operName);
@@ -314,7 +322,7 @@ public class BmsProductServiceImpl implements IBmsProductService
}
product.setSourceSheet(productSheet.getSheetName());
List<BmsProductCountry> countries = parseProductSheet(productSheet, product,
regionByNameCn, euRegions, unmatchedCountries);
regionByNameCn, euRegions, zoneByCountryAndCode, unmatchedCountries);
if (countries == null || countries.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.product.import.noCountry",
@@ -412,7 +420,8 @@ public class BmsProductServiceImpl implements IBmsProductService
{
throw new ServiceException(MessageUtils.message("price.product.import.needFeeAlter"));
}
if (tip.contains("expire_date") || tip.contains("uk_product_country_weight"))
if (tip.contains("expire_date") || tip.contains("uk_product_country_weight")
|| tip.contains("uk_product_country_zone_weight"))
{
throw new ServiceException(MessageUtils.message("price.product.import.needExpireAlter"));
}
@@ -716,7 +725,8 @@ public class BmsProductServiceImpl implements IBmsProductService
}
private List<BmsProductCountry> parseProductSheet(Sheet sheet, BmsProduct product,
Map<String, BmsRegion> regionByNameCn, List<BmsRegion> euRegions, Set<String> unmatchedCountries)
Map<String, BmsRegion> regionByNameCn, List<BmsRegion> euRegions,
Map<String, PrcZone> zoneByCountryAndCode, Set<String> unmatchedCountries)
{
if (sheet == null)
{
@@ -739,7 +749,6 @@ public class BmsProductServiceImpl implements IBmsProductService
int colSize = findColByHeader(headerRow, "\u5c3a\u5bf8", colCountry + 1);
int colLastMile = findColByHeader(headerRow, "\u5c3e\u7a0b", colCountry + 1);
int colLeadTime = findColByHeader(headerRow, "\u65f6\u6548", colCountry + 1);
// Fallback offsets when header text missing (Weishi layout: B=country ... H=lead time)
if (colWeight < 0)
{
colWeight = colCountry + 1;
@@ -765,7 +774,7 @@ public class BmsProductServiceImpl implements IBmsProductService
colLeadTime = colCountry + 6;
}
String currentCountry = null;
String currentCountryRaw = null;
String currentSize = null;
String currentLastMile = null;
String currentLeadTime = null;
@@ -785,12 +794,12 @@ public class BmsProductServiceImpl implements IBmsProductService
}
if (StringUtils.isNotEmpty(countryCell))
{
currentCountry = normalizeCountryName(countryCell);
currentCountryRaw = countryCell;
currentSize = emptyToNull(cellString(row.getCell(colSize)));
currentLastMile = emptyToNull(cellString(row.getCell(colLastMile)));
currentLeadTime = emptyToNull(cellString(row.getCell(colLeadTime)));
}
if (StringUtils.isEmpty(currentCountry))
if (StringUtils.isEmpty(currentCountryRaw))
{
continue;
}
@@ -803,14 +812,23 @@ public class BmsProductServiceImpl implements IBmsProductService
BigDecimal registration = cellDecimal(row.getCell(colRegister));
Date eff = sheetEffective != null ? sheetEffective : product.getEffectiveDate();
BmsRegion region = resolveRegion(currentCountry, regionByNameCn);
ZoneCodeAbbrevs.CountryZoneParts parts = ZoneCodeAbbrevs.splitCountryZone(currentCountryRaw);
if (parts == null || StringUtils.isEmpty(parts.countryName))
{
continue;
}
String baseCountry = parts.countryName;
String zoneLabel = parts.zoneLabel;
BmsRegion region = resolveRegion(baseCountry, regionByNameCn);
if (region != null)
{
list.add(buildCountryRow(region.getCountryNameCn(), region.getCountryCode2(),
zoneLabel, zoneByCountryAndCode,
range, freight, registration, currentSize, currentLastMile, currentLeadTime, eff));
continue;
}
if (isEuZoneName(currentCountry))
if (isEuZoneName(baseCountry) && StringUtils.isEmpty(zoneLabel))
{
if (euRegions == null || euRegions.isEmpty())
{
@@ -819,23 +837,26 @@ public class BmsProductServiceImpl implements IBmsProductService
for (BmsRegion eu : euRegions)
{
list.add(buildCountryRow(eu.getCountryNameCn(), eu.getCountryCode2(),
null, zoneByCountryAndCode,
range, freight, registration, currentSize, currentLastMile, currentLeadTime, eff));
}
continue;
}
unmatchedCountries.add(currentCountry);
list.add(buildCountryRow(currentCountry, null,
unmatchedCountries.add(currentCountryRaw.trim().replace("\n", " ").replace("\r", " "));
list.add(buildCountryRow(baseCountry, null, zoneLabel, zoneByCountryAndCode,
range, freight, registration, currentSize, currentLastMile, currentLeadTime, eff));
}
return list;
}
private BmsProductCountry buildCountryRow(String countryName, String countryCode, BigDecimal[] range,
private BmsProductCountry buildCountryRow(String countryName, String countryCode, String zoneLabel,
Map<String, PrcZone> zoneByCountryAndCode, BigDecimal[] range,
BigDecimal freight, BigDecimal registration, String sizeLimit, String lastMile, String leadTime, Date eff)
{
BmsProductCountry c = new BmsProductCountry();
c.setCountryName(countryName);
c.setCountryCode(countryCode);
applyZoneFields(c, countryCode, zoneLabel, zoneByCountryAndCode);
c.setWeightMin(range[0]);
c.setWeightMax(range[1]);
c.setFreightPrice(freight);
@@ -847,6 +868,68 @@ public class BmsProductServiceImpl implements IBmsProductService
return c;
}
private void applyZoneFields(BmsProductCountry c, String countryCode, String zoneLabel,
Map<String, PrcZone> zoneByCountryAndCode)
{
if (StringUtils.isEmpty(zoneLabel))
{
c.setZoneId(null);
c.setZoneCode(null);
c.setZoneName(null);
return;
}
String zoneCode = ZoneCodeAbbrevs.toAbbrev(zoneLabel);
c.setZoneCode(zoneCode);
PrcZone zone = null;
if (StringUtils.isNotEmpty(countryCode) && zoneByCountryAndCode != null)
{
zone = zoneByCountryAndCode.get(zoneKey(countryCode, zoneCode));
}
if (zone != null)
{
c.setZoneId(zone.getZoneId());
c.setZoneCode(zone.getZoneCode());
c.setZoneName(zone.getZoneName());
}
else
{
c.setZoneId(null);
c.setZoneName(StringUtils.isNotEmpty(c.getCountryName())
? c.getCountryName() + "(" + zoneCode + ")"
: zoneCode);
}
}
private Map<String, PrcZone> loadZoneByCountryAndCode()
{
Map<String, PrcZone> map = new HashMap<>();
List<PrcZone> list = zoneMapper.selectPrcZoneList(new PrcZone());
if (list == null)
{
return map;
}
for (PrcZone zone : list)
{
if (zone == null || StringUtils.isEmpty(zone.getZoneCode()))
{
continue;
}
String code2 = zone.getCountryCode();
if (StringUtils.isEmpty(code2))
{
continue;
}
map.put(zoneKey(code2, zone.getZoneCode()), zone);
}
return map;
}
private static String zoneKey(String countryCode, String zoneCode)
{
return String.valueOf(countryCode).trim().toUpperCase(Locale.ROOT)
+ "|" + String.valueOf(zoneCode).trim().toUpperCase(Locale.ROOT);
}
/**
* Load bms_region map: country_name_cn -> region (for country_code_2).
*/
@@ -907,7 +990,6 @@ public class BmsProductServiceImpl implements IBmsProductService
{
return candidates.get(0);
}
// Prefer longest Chinese name match (e.g. 捷克共和国 over 捷克)
BmsRegion best = candidates.get(0);
for (int i = 1; i < candidates.size(); i++)
{
@@ -920,29 +1002,6 @@ public class BmsProductServiceImpl implements IBmsProductService
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)
{
if (StringUtils.isEmpty(label))
@@ -248,6 +248,9 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
nd.setProductName(d.getProductName());
nd.setCountryCode(d.getCountryCode());
nd.setCountryName(d.getCountryName());
nd.setZoneId(d.getZoneId());
nd.setZoneCode(d.getZoneCode());
nd.setZoneName(d.getZoneName());
nd.setZdId(d.getZdId());
nd.setWsdId(d.getWsdId());
nd.setWeightLabel(d.getWeightLabel());
@@ -659,6 +662,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
{
throw new ServiceException(MessageUtils.message("price.set.detail.country.empty"));
}
requireZoneIfNeeded(detail);
if (detail.getWsdId() == null && StringUtils.isEmpty(detail.getWeightLabel()))
{
throw new ServiceException(MessageUtils.message("price.set.detail.weight.empty"));
@@ -692,6 +696,29 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
}
}
/**
* When product country rows for this country have zones, zone is required.
*/
private void requireZoneIfNeeded(PrcPriceSetDetail detail)
{
if (detail.getZoneId() != null || StringUtils.isNotEmpty(detail.getZoneCode()))
{
return;
}
List<BmsProductCountry> rows = listOpenProductCountries(detail.getProductCode());
for (BmsProductCountry row : rows)
{
if (!matchDetailCountryOnly(detail, row))
{
continue;
}
if (row.getZoneId() != null || StringUtils.isNotEmpty(row.getZoneCode()))
{
throw new ServiceException(MessageUtils.message("price.set.detail.zone.empty"));
}
}
}
/**
* Resolve / verify product_country_id from bms_product_country when missing.
*/
@@ -699,25 +726,16 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
{
if (detail.getProductCountryId() != null)
{
syncZoneFromProductCountry(detail);
return;
}
if (StringUtils.isEmpty(detail.getProductCode()) || StringUtils.isEmpty(detail.getWeightLabel()))
{
return;
}
BmsProductCountry query = new BmsProductCountry();
query.setProductCode(detail.getProductCode());
List<BmsProductCountry> rows = productCountryMapper.selectBmsProductCountryList(query);
if (rows == null || rows.isEmpty())
{
return;
}
List<BmsProductCountry> rows = listOpenProductCountries(detail.getProductCode());
for (BmsProductCountry row : rows)
{
if (row.getExpireDate() != null)
{
continue;
}
if (!matchDetailCountry(detail, row))
{
continue;
@@ -730,12 +748,80 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
&& detail.getWeightLabel().contains(stripTrailingZeros(row.getWeightMax()))))
{
detail.setProductCountryId(row.getDetailId());
applyZoneFromRow(detail, row);
return;
}
}
}
private static boolean matchDetailCountry(PrcPriceSetDetail detail, BmsProductCountry row)
private void syncZoneFromProductCountry(PrcPriceSetDetail detail)
{
if (detail.getProductCountryId() == null)
{
return;
}
if (detail.getZoneId() != null || StringUtils.isNotEmpty(detail.getZoneCode()))
{
return;
}
BmsProductCountry query = new BmsProductCountry();
query.setProductCode(detail.getProductCode());
List<BmsProductCountry> rows = productCountryMapper.selectBmsProductCountryList(query);
if (rows == null)
{
return;
}
for (BmsProductCountry row : rows)
{
if (detail.getProductCountryId().equals(row.getDetailId()))
{
applyZoneFromRow(detail, row);
return;
}
}
}
private List<BmsProductCountry> listOpenProductCountries(String productCode)
{
if (StringUtils.isEmpty(productCode))
{
return java.util.Collections.emptyList();
}
BmsProductCountry query = new BmsProductCountry();
query.setProductCode(productCode);
List<BmsProductCountry> rows = productCountryMapper.selectBmsProductCountryList(query);
if (rows == null || rows.isEmpty())
{
return java.util.Collections.emptyList();
}
List<BmsProductCountry> open = new ArrayList<BmsProductCountry>();
for (BmsProductCountry row : rows)
{
if (row.getExpireDate() == null)
{
open.add(row);
}
}
return open;
}
private static void applyZoneFromRow(PrcPriceSetDetail detail, BmsProductCountry row)
{
if (detail.getZoneId() == null)
{
detail.setZoneId(row.getZoneId());
}
if (StringUtils.isEmpty(detail.getZoneCode()))
{
detail.setZoneCode(row.getZoneCode());
}
if (StringUtils.isEmpty(detail.getZoneName()))
{
detail.setZoneName(row.getZoneName());
}
}
private static boolean matchDetailCountryOnly(PrcPriceSetDetail detail, BmsProductCountry row)
{
if (StringUtils.isNotEmpty(detail.getCountryCode()) && StringUtils.isNotEmpty(row.getCountryCode()))
{
@@ -745,6 +831,38 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
&& detail.getCountryName().equals(row.getCountryName());
}
private static boolean matchDetailCountry(PrcPriceSetDetail detail, BmsProductCountry row)
{
if (!matchDetailCountryOnly(detail, row))
{
return false;
}
return matchDetailZone(detail, row);
}
private static boolean matchDetailZone(PrcPriceSetDetail detail, BmsProductCountry row)
{
boolean detailHas = detail.getZoneId() != null || StringUtils.isNotEmpty(detail.getZoneCode());
boolean rowHas = row.getZoneId() != null || StringUtils.isNotEmpty(row.getZoneCode());
if (!detailHas && !rowHas)
{
return true;
}
if (detailHas != rowHas)
{
return false;
}
if (detail.getZoneId() != null && row.getZoneId() != null)
{
return detail.getZoneId().equals(row.getZoneId());
}
if (StringUtils.isNotEmpty(detail.getZoneCode()) && StringUtils.isNotEmpty(row.getZoneCode()))
{
return detail.getZoneCode().equalsIgnoreCase(row.getZoneCode());
}
return false;
}
private static String formatWeightLabel(BigDecimal min, BigDecimal max)
{
return stripTrailingZeros(min) + "<W\u2264" + stripTrailingZeros(max);
@@ -0,0 +1,892 @@
package com.ruoyi.price.service.impl;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
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.DataFormatter;
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.MessageUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.price.domain.PrcZone;
import com.ruoyi.price.domain.PrcZoneDetail;
import com.ruoyi.price.domain.PrcZoneRegion;
import com.ruoyi.price.mapper.PrcZoneDetailMapper;
import com.ruoyi.price.mapper.PrcZoneMapper;
import com.ruoyi.price.service.IPrcZoneService;
import com.ruoyi.price.util.ZoneCodeAbbrevs;
import com.ruoyi.system.domain.BmsRegion;
import com.ruoyi.system.mapper.BmsRegionMapper;
/**
* Zone management: bms_region -> prc_zone -> postcode detail
*/
@Service
public class PrcZoneServiceImpl implements IPrcZoneService
{
private static final String SHEET_SUFFIX = "\u5206\u533a";
private static final String COL_POSTCODE = "\u90ae\u7f16";
private static final String COL_ZONE = "\u5206\u533a";
private static final Pattern ZONE_NUM = Pattern.compile("(\\d+)");
private static final int BATCH_SIZE = 500;
private static final DataFormatter CELL_FMT = new DataFormatter(Locale.ROOT);
@Autowired
private PrcZoneMapper zoneMapper;
@Autowired
private PrcZoneDetailMapper detailMapper;
@Autowired
private BmsRegionMapper regionMapper;
@Override
public PrcZoneRegion selectZoneRegionById(Long regionId)
{
return zoneMapper.selectZoneRegionById(regionId);
}
@Override
public List<PrcZoneRegion> selectZoneRegionList(PrcZoneRegion query)
{
return zoneMapper.selectZoneRegionList(query);
}
@Override
@Transactional(rollbackFor = Exception.class)
public int deleteZonesByRegionIds(Long[] regionIds)
{
if (regionIds == null || regionIds.length == 0)
{
return 0;
}
for (Long regionId : regionIds)
{
detailMapper.deletePrcZoneDetailByRegionId(regionId);
}
return zoneMapper.deletePrcZoneByRegionIds(regionIds);
}
@Override
public PrcZone selectPrcZoneById(Long zoneId)
{
PrcZone zone = zoneMapper.selectPrcZoneById(zoneId);
if (zone != null)
{
zone.setDetailCount(detailMapper.countByZoneId(zoneId));
}
return zone;
}
@Override
public List<PrcZone> selectPrcZoneList(PrcZone zone)
{
return zoneMapper.selectPrcZoneList(zone);
}
@Override
public int insertPrcZone(PrcZone zone)
{
if (zone.getRegionId() == null)
{
throw new ServiceException(MessageUtils.message("price.zone.region.required"));
}
BmsRegion region = regionMapper.selectRegionById(zone.getRegionId());
if (region == null)
{
throw new ServiceException(MessageUtils.message("price.zone.region.notFound"));
}
if (StringUtils.isEmpty(zone.getCountryCode()))
{
zone.setCountryCode(region.getCountryCode2());
}
else
{
zone.setCountryCode(zone.getCountryCode().trim().toUpperCase(Locale.ROOT));
}
if (StringUtils.isEmpty(zone.getCountryCode()))
{
throw new ServiceException(MessageUtils.message("price.zone.countryCode.empty"));
}
if (zone.getZoneSort() == null)
{
zone.setZoneSort(extractZoneSort(zone.getZoneCode()));
}
if (StringUtils.isNotEmpty(zone.getZoneCode()))
{
zone.setZoneCode(ZoneCodeAbbrevs.toAbbrev(zone.getZoneCode()));
}
if (StringUtils.isEmpty(zone.getZoneName()))
{
zone.setZoneName(buildZoneName(region.getCountryNameCn(), zone.getZoneCode()));
}
return zoneMapper.insertPrcZone(zone);
}
@Override
public int updatePrcZone(PrcZone zone)
{
if (zone.getRegionId() != null && StringUtils.isEmpty(zone.getCountryCode()))
{
BmsRegion region = regionMapper.selectRegionById(zone.getRegionId());
if (region != null)
{
zone.setCountryCode(region.getCountryCode2());
}
}
else if (StringUtils.isNotEmpty(zone.getCountryCode()))
{
zone.setCountryCode(zone.getCountryCode().trim().toUpperCase(Locale.ROOT));
}
if (StringUtils.isNotEmpty(zone.getZoneCode()))
{
zone.setZoneCode(ZoneCodeAbbrevs.toAbbrev(zone.getZoneCode()));
}
if (zone.getZoneSort() == null && StringUtils.isNotEmpty(zone.getZoneCode()))
{
zone.setZoneSort(extractZoneSort(zone.getZoneCode()));
}
return zoneMapper.updatePrcZone(zone);
}
@Override
@Transactional(rollbackFor = Exception.class)
public int deletePrcZoneByIds(Long[] zoneIds)
{
return zoneMapper.deletePrcZoneByIds(zoneIds);
}
@Override
public PrcZoneDetail selectPrcZoneDetailById(Long detailId)
{
return detailMapper.selectPrcZoneDetailById(detailId);
}
@Override
public List<PrcZoneDetail> selectPrcZoneDetailList(PrcZoneDetail detail)
{
return detailMapper.selectPrcZoneDetailList(detail);
}
@Override
public int insertPrcZoneDetail(PrcZoneDetail detail)
{
return detailMapper.insertPrcZoneDetail(detail);
}
@Override
public int updatePrcZoneDetail(PrcZoneDetail detail)
{
return detailMapper.updatePrcZoneDetail(detail);
}
@Override
public int deletePrcZoneDetailByIds(Long[] detailIds)
{
return detailMapper.deletePrcZoneDetailByIds(detailIds);
}
@Override
@Transactional(rollbackFor = Exception.class)
public String importZoneExcel(MultipartFile file, String operName) throws Exception
{
if (file == null || file.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.zone.import.fileEmpty"));
}
String filename = file.getOriginalFilename();
if (filename == null || !(filename.toLowerCase(Locale.ROOT).endsWith(".xls")
|| filename.toLowerCase(Locale.ROOT).endsWith(".xlsx")))
{
throw new ServiceException(MessageUtils.message("price.zone.import.fileType"));
}
Map<String, BmsRegion> regionByNameCn = loadRegionByNameCn();
if (regionByNameCn.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.zone.import.regionEmpty"));
}
int countryCount = 0;
int zoneCount = 0;
int detailCount = 0;
List<String> importedCountries = new ArrayList<>();
List<String> unmatched = new ArrayList<>();
try (InputStream is = file.getInputStream(); Workbook workbook = WorkbookFactory.create(is))
{
List<SheetImport> imports = findZoneImports(workbook);
if (imports.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.zone.import.noSheet"));
}
for (SheetImport item : imports)
{
Map<String, List<String>> zonePostcodes = item.zonePostcodes;
if (zonePostcodes == null || zonePostcodes.isEmpty())
{
continue;
}
BmsRegion region = resolveRegion(item.countryHint, regionByNameCn);
if (region == null)
{
unmatched.add(item.countryHint + "(" + item.sheetName + ")");
continue;
}
int[] counts = replaceRegionZones(region, zonePostcodes);
countryCount += 1;
zoneCount += counts[0];
detailCount += counts[1];
importedCountries.add(region.getCountryNameCn());
}
}
if (countryCount == 0)
{
if (!unmatched.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.zone.import.regionNotFound",
String.join(",", unmatched)));
}
throw new ServiceException(MessageUtils.message("price.zone.import.noData"));
}
String msg = MessageUtils.message("price.zone.import.success", countryCount, zoneCount, detailCount,
String.join(",", importedCountries));
if (!unmatched.isEmpty())
{
msg = msg + " " + MessageUtils.message("price.zone.import.unmatched", String.join(",", unmatched));
}
return msg;
}
private int[] replaceRegionZones(BmsRegion region, Map<String, List<String>> zonePostcodes)
{
if (StringUtils.isEmpty(region.getCountryCode2()))
{
throw new ServiceException(MessageUtils.message("price.zone.countryCode.empty"));
}
Long regionId = region.getRegionId();
detailMapper.deletePrcZoneDetailByRegionId(regionId);
zoneMapper.deletePrcZoneByRegionId(regionId);
int zoneInserted = 0;
int detailInserted = 0;
List<Map.Entry<String, List<String>>> entries = new ArrayList<>(zonePostcodes.entrySet());
entries.sort(Comparator.comparingInt(e -> extractZoneSort(e.getKey())));
String countryName = region.getCountryNameCn();
for (Map.Entry<String, List<String>> entry : entries)
{
String zoneCode = ZoneCodeAbbrevs.toAbbrev(entry.getKey());
PrcZone zone = new PrcZone();
zone.setRegionId(regionId);
zone.setCountryCode(region.getCountryCode2());
zone.setZoneCode(zoneCode);
zone.setZoneName(buildZoneName(countryName, zoneCode));
zone.setZoneSort(extractZoneSort(entry.getKey()));
zoneMapper.insertPrcZone(zone);
zoneInserted++;
Set<String> uniquePostcodes = new LinkedHashSet<>(entry.getValue());
List<PrcZoneDetail> batch = new ArrayList<>(BATCH_SIZE);
for (String postcode : uniquePostcodes)
{
PrcZoneDetail detail = new PrcZoneDetail();
detail.setZoneId(zone.getZoneId());
detail.setPostcode(postcode);
batch.add(detail);
if (batch.size() >= BATCH_SIZE)
{
detailMapper.insertPrcZoneDetailBatch(batch);
detailInserted += batch.size();
batch = new ArrayList<>(BATCH_SIZE);
}
}
if (!batch.isEmpty())
{
detailMapper.insertPrcZoneDetailBatch(batch);
detailInserted += batch.size();
}
}
return new int[] { zoneInserted, detailInserted };
}
private static final class SheetImport
{
private final String sheetName;
private final String countryHint;
private final Map<String, List<String>> zonePostcodes;
private SheetImport(String sheetName, String countryHint, Map<String, List<String>> zonePostcodes)
{
this.sheetName = sheetName;
this.countryHint = countryHint;
this.zonePostcodes = zonePostcodes;
}
}
private List<SheetImport> findZoneImports(Workbook workbook)
{
List<SheetImport> list = new ArrayList<>();
for (int i = 0; i < workbook.getNumberOfSheets(); i++)
{
Sheet sheet = workbook.getSheetAt(i);
if (sheet == null)
{
continue;
}
String sheetName = StringUtils.trim(sheet.getSheetName());
if (StringUtils.isEmpty(sheetName))
{
continue;
}
String countryHint = guessCountryFromSheetName(sheetName);
Map<String, List<String>> parsed = parseSheetByName(sheet, sheetName);
if (parsed.isEmpty())
{
parsed = parseZonePostcodes(sheet);
}
if (parsed.isEmpty())
{
parsed = parseRemoteZonePostcodes(sheet);
}
if (parsed.isEmpty())
{
parsed = parseProvincePostcodes(sheet);
}
if (parsed.isEmpty())
{
parsed = parseCoveragePostcodes(sheet);
}
if (parsed.isEmpty())
{
parsed = parseUkSurchargePostcodes(sheet);
}
if (parsed.isEmpty())
{
continue;
}
if (StringUtils.isEmpty(countryHint))
{
countryHint = sheetName;
}
list.add(new SheetImport(sheetName, countryHint, parsed));
}
return list;
}
private String guessCountryFromSheetName(String sheetName)
{
String name = StringUtils.trim(sheetName);
if (name == null)
{
return null;
}
if (name.endsWith(SHEET_SUFFIX) && name.length() > SHEET_SUFFIX.length())
{
return StringUtils.trim(name.substring(0, name.length() - SHEET_SUFFIX.length()));
}
// strip common suffixes: remote surcharge / surcharge / postcode
String[] suffixes = {
"\u504f\u8fdc\u9644\u52a0\u8d39",
"\u9644\u52a0\u8d39",
"MR\u90ae\u7f16",
"\u90ae\u7f16"
};
for (String suffix : suffixes)
{
if (name.endsWith(suffix) && name.length() > suffix.length())
{
return StringUtils.trim(name.substring(0, name.length() - suffix.length()));
}
}
if (name.contains("\u52a0\u62ff\u5927"))
{
return "\u52a0\u62ff\u5927";
}
if (name.contains("\u6fb3\u5927\u5229\u4e9a"))
{
return "\u6fb3\u5927\u5229\u4e9a";
}
if (name.contains("\u82f1\u56fd"))
{
return "\u82f1\u56fd";
}
if (name.contains("\u65e5\u672c"))
{
return "\u65e5\u672c";
}
if (name.contains("\u6cd5\u56fd"))
{
return "\u6cd5\u56fd";
}
return null;
}
private Map<String, List<String>> parseSheetByName(Sheet sheet, String sheetName)
{
if (sheetName.endsWith(SHEET_SUFFIX))
{
return parseZonePostcodes(sheet);
}
if (sheetName.contains("\u52a0\u62ff\u5927"))
{
return parseRemoteZonePostcodes(sheet);
}
if (sheetName.contains("\u6cd5\u56fd") && sheetName.contains("\u90ae\u7f16"))
{
return parseCoveragePostcodes(sheet);
}
if (sheetName.contains("\u65e5\u672c"))
{
return parseProvincePostcodes(sheet);
}
if (sheetName.contains("\u82f1\u56fd"))
{
return parseUkSurchargePostcodes(sheet);
}
return new LinkedHashMap<>();
}
private List<Sheet> findZoneSheets(Workbook workbook)
{
return new ArrayList<>();
}
private String parseCountryNameFromSheet(String sheetName)
{
return guessCountryFromSheetName(sheetName);
}
/** Australia style: postcode + zone */
private Map<String, List<String>> parseZonePostcodes(Sheet sheet)
{
return parseTwoColZoneMap(sheet, true);
}
/** Canada style: remote postcode + remote zone */
private Map<String, List<String>> parseRemoteZonePostcodes(Sheet sheet)
{
Map<String, List<String>> map = new LinkedHashMap<>();
int postcodeCol = -1;
int zoneCol = -1;
int headerRow = -1;
int lastRow = sheet.getLastRowNum();
for (int r = 0; r <= Math.min(lastRow, 20); r++)
{
Row row = sheet.getRow(r);
if (row == null)
{
continue;
}
for (int c = 0; c < row.getLastCellNum(); c++)
{
String val = cellText(row.getCell(c));
if (val.contains("\u90ae\u7f16") && postcodeCol < 0)
{
postcodeCol = c;
headerRow = r;
}
if ((val.equals("\u504f\u8fdc\u5206\u533a") || val.equals(COL_ZONE) || val.contains("\u5206\u533a"))
&& !val.contains("\u9644\u52a0") && zoneCol < 0)
{
zoneCol = c;
headerRow = r;
}
}
if (postcodeCol >= 0 && zoneCol >= 0)
{
break;
}
}
if (postcodeCol < 0 || zoneCol < 0 || headerRow < 0)
{
return map;
}
fillTwoCol(map, sheet, headerRow, postcodeCol, zoneCol);
return map;
}
/** Japan style: province + postcode */
private Map<String, List<String>> parseProvincePostcodes(Sheet sheet)
{
Map<String, List<String>> map = new LinkedHashMap<>();
int provinceCol = -1;
int postcodeCol = -1;
int headerRow = -1;
int lastRow = sheet.getLastRowNum();
for (int r = 0; r <= Math.min(lastRow, 10); r++)
{
Row row = sheet.getRow(r);
if (row == null)
{
continue;
}
for (int c = 0; c < row.getLastCellNum(); c++)
{
String val = cellText(row.getCell(c));
if ((val.contains("\u5dde") || val.contains("\u7701") || "\u5dde\u7701".equals(val)) && provinceCol < 0)
{
provinceCol = c;
headerRow = r;
}
if (COL_POSTCODE.equals(val) || val.equals("\u90ae\u7f16"))
{
postcodeCol = c;
headerRow = r;
}
}
if (provinceCol >= 0 && postcodeCol >= 0)
{
break;
}
}
if (provinceCol < 0 || postcodeCol < 0 || headerRow < 0)
{
return map;
}
// zone = province, detail = postcode (swap vs AU)
for (int r = headerRow + 1; r <= lastRow; r++)
{
Row row = sheet.getRow(r);
if (row == null)
{
continue;
}
String zoneCode = normalizeCell(cellText(row.getCell(provinceCol)));
String postcode = normalizePostcode(cellText(row.getCell(postcodeCol)));
if (StringUtils.isEmpty(zoneCode) || StringUtils.isEmpty(postcode))
{
continue;
}
map.computeIfAbsent(zoneCode, k -> new ArrayList<>()).add(postcode);
}
return map;
}
/** France MR style: carrier/zone + coverage postcode */
private Map<String, List<String>> parseCoveragePostcodes(Sheet sheet)
{
Map<String, List<String>> map = new LinkedHashMap<>();
int zoneCol = -1;
int postcodeCol = -1;
int headerRow = -1;
int lastRow = sheet.getLastRowNum();
for (int r = 0; r <= Math.min(lastRow, 10); r++)
{
Row row = sheet.getRow(r);
if (row == null)
{
continue;
}
for (int c = 0; c < row.getLastCellNum(); c++)
{
String val = cellText(row.getCell(c));
if (val.contains("\u8986\u76d6\u90ae\u7f16") || val.equals(COL_POSTCODE))
{
postcodeCol = c;
headerRow = r;
}
if (val.contains("\u5c3e\u7a0b") || val.contains("\u81ea\u63d0"))
{
zoneCol = c;
headerRow = r;
}
}
if (zoneCol >= 0 && postcodeCol >= 0)
{
break;
}
}
if (postcodeCol < 0)
{
return map;
}
if (zoneCol < 0)
{
zoneCol = 0;
}
if (headerRow < 0)
{
headerRow = 0;
}
for (int r = headerRow + 1; r <= lastRow; r++)
{
Row row = sheet.getRow(r);
if (row == null)
{
continue;
}
String zoneCode = normalizeCell(cellText(row.getCell(zoneCol)));
if (StringUtils.isEmpty(zoneCode))
{
zoneCode = "Mondial Relay";
}
String postcode = normalizePostcode(cellText(row.getCell(postcodeCol)));
if (StringUtils.isEmpty(postcode))
{
continue;
}
map.computeIfAbsent(zoneCode, k -> new ArrayList<>()).add(postcode);
}
return map;
}
/** UK surcharge sheet: multi blocks of region + postcode prefix */
private Map<String, List<String>> parseUkSurchargePostcodes(Sheet sheet)
{
Map<String, List<String>> map = new LinkedHashMap<>();
int lastRow = sheet.getLastRowNum();
// detect header row containing ????
int headerRow = -1;
for (int r = 0; r <= Math.min(lastRow, 10); r++)
{
Row row = sheet.getRow(r);
if (row == null)
{
continue;
}
for (int c = 0; c < row.getLastCellNum(); c++)
{
if (cellText(row.getCell(c)).contains("\u90ae\u7f16\u5f00\u5934")
|| cellText(row.getCell(c)).contains("\u7a7a\u6c14\u51cf\u6392"))
{
headerRow = r;
break;
}
}
if (headerRow >= 0)
{
break;
}
}
if (headerRow < 0)
{
return map;
}
String lastRm = "";
for (int r = headerRow + 1; r <= lastRow; r++)
{
Row row = sheet.getRow(r);
if (row == null)
{
continue;
}
String rmName = normalizeCell(cellText(row.getCell(0)));
if (StringUtils.isNotEmpty(rmName))
{
lastRm = rmName;
}
String rmPc = normalizePostcode(cellText(row.getCell(1)));
if (StringUtils.isNotEmpty(lastRm) && StringUtils.isNotEmpty(rmPc))
{
map.computeIfAbsent(lastRm, k -> new ArrayList<>()).add(rmPc);
}
String evName = normalizeCell(cellText(row.getCell(5)));
if (StringUtils.isEmpty(evName))
{
evName = normalizeCell(cellText(row.getCell(4)));
}
String evPc = normalizePostcode(cellText(row.getCell(6)));
if (StringUtils.isNotEmpty(evName) && StringUtils.isNotEmpty(evPc))
{
map.computeIfAbsent(evName, k -> new ArrayList<>()).add(evPc);
}
String airName = normalizeCell(cellText(row.getCell(8)));
String airPc = normalizePostcode(cellText(row.getCell(9)));
if (StringUtils.isNotEmpty(airName) && StringUtils.isNotEmpty(airPc))
{
map.computeIfAbsent(airName, k -> new ArrayList<>()).add(airPc);
}
}
return map;
}
private Map<String, List<String>> parseTwoColZoneMap(Sheet sheet, boolean requireExactPostcode)
{
Map<String, List<String>> map = new LinkedHashMap<>();
int postcodeCol = -1;
int zoneCol = -1;
int headerRow = -1;
int lastRow = sheet.getLastRowNum();
for (int r = 0; r <= Math.min(lastRow, 20); r++)
{
Row row = sheet.getRow(r);
if (row == null)
{
continue;
}
for (int c = 0; c < row.getLastCellNum(); c++)
{
String val = cellText(row.getCell(c));
if (COL_POSTCODE.equals(val) || (!requireExactPostcode && val.contains(COL_POSTCODE)))
{
postcodeCol = c;
headerRow = r;
}
else if (COL_ZONE.equals(val))
{
zoneCol = c;
headerRow = r;
}
}
if (postcodeCol >= 0 && zoneCol >= 0)
{
break;
}
}
if (postcodeCol < 0 || zoneCol < 0 || headerRow < 0)
{
return map;
}
fillTwoCol(map, sheet, headerRow, postcodeCol, zoneCol);
return map;
}
private void fillTwoCol(Map<String, List<String>> map, Sheet sheet, int headerRow, int postcodeCol, int zoneCol)
{
int lastRow = sheet.getLastRowNum();
for (int r = headerRow + 1; r <= lastRow; r++)
{
Row row = sheet.getRow(r);
if (row == null)
{
continue;
}
String postcode = normalizePostcode(cellText(row.getCell(postcodeCol)));
String zoneCode = normalizeCell(cellText(row.getCell(zoneCol)));
if (StringUtils.isEmpty(postcode) || StringUtils.isEmpty(zoneCode))
{
continue;
}
map.computeIfAbsent(zoneCode, k -> new ArrayList<>()).add(postcode);
}
}
private static String normalizePostcode(String postcode)
{
if (StringUtils.isEmpty(postcode))
{
return postcode;
}
if (postcode.endsWith(".0") && postcode.matches("\\d+\\.0"))
{
return postcode.substring(0, postcode.length() - 2);
}
return postcode;
}
private static String normalizeCell(String text)
{
if (text == null)
{
return "";
}
return text.replace('\r', ' ').replace('\n', ' ').replaceAll("\\s+", " ").trim();
}
private String cellText(Cell cell)
{
if (cell == null)
{
return "";
}
return StringUtils.trim(CELL_FMT.formatCellValue(cell));
}
private Map<String, BmsRegion> loadRegionByNameCn()
{
BmsRegion query = new BmsRegion();
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.isNotEmpty(region.getCountryNameCn()))
{
map.put(region.getCountryNameCn().trim(), region);
}
}
return map;
}
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;
}
BmsRegion best = null;
int bestLen = -1;
for (BmsRegion region : regionByNameCn.values())
{
String cn = region.getCountryNameCn();
if (StringUtils.isEmpty(cn))
{
continue;
}
if (name.contains(cn) || cn.contains(name))
{
if (cn.length() > bestLen)
{
best = region;
bestLen = cn.length();
}
}
}
return best;
}
private static String buildZoneName(String countryName, String zoneCode)
{
return (countryName == null ? "" : countryName) + "(" + zoneCode + ")";
}
private static int extractZoneSort(String zoneCode)
{
if (StringUtils.isEmpty(zoneCode))
{
return 0;
}
Matcher m = ZONE_NUM.matcher(zoneCode);
if (m.find())
{
try
{
return Integer.parseInt(m.group(1));
}
catch (NumberFormatException ignored)
{
return 0;
}
}
return 0;
}
}
@@ -248,7 +248,8 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
PrcPriceSetDetail detail = detailMapper.selectFixedMatch(
set.getPriceSetId(), productCode,
base.getCountryCode(), base.getCountryName(), base.getDetailId(),
base.getCountryCode(), base.getCountryName(), base.getZoneCode(),
base.getDetailId(),
formatWeightLabel(base.getWeightMin(), base.getWeightMax()));
if (detail == null)
{
@@ -1,161 +0,0 @@
package com.ruoyi.price.service.impl;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.MessageUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.price.domain.XtdZoneCountry;
import com.ruoyi.price.domain.XtdZoneDivision;
import com.ruoyi.price.domain.XtdZoneScheme;
import com.ruoyi.price.mapper.XtdZoneCountryMapper;
import com.ruoyi.price.mapper.XtdZoneDivisionMapper;
import com.ruoyi.price.mapper.XtdZoneSchemeMapper;
import com.ruoyi.price.service.IXtdZoneService;
/**
* 分区管理 服务层实现
*
* @author ruoyi
*/
@Service
public class XtdZoneServiceImpl implements IXtdZoneService
{
@Autowired
private XtdZoneSchemeMapper schemeMapper;
@Autowired
private XtdZoneDivisionMapper divisionMapper;
@Autowired
private XtdZoneCountryMapper countryMapper;
@Override
public XtdZoneScheme selectXtdZoneSchemeById(Long zsId)
{
XtdZoneScheme scheme = schemeMapper.selectXtdZoneSchemeById(zsId);
if (scheme != null)
{
scheme.setDivisionCount(divisionMapper.countByZsId(zsId));
}
return scheme;
}
@Override
public List<XtdZoneScheme> selectXtdZoneSchemeList(XtdZoneScheme scheme)
{
List<XtdZoneScheme> list = schemeMapper.selectXtdZoneSchemeList(scheme);
for (XtdZoneScheme item : list)
{
item.setDivisionCount(divisionMapper.countByZsId(item.getZsId()));
}
return list;
}
@Override
public int insertXtdZoneScheme(XtdZoneScheme scheme)
{
if (StringUtils.isEmpty(scheme.getSsCode()))
{
scheme.setSsCode("on");
}
return schemeMapper.insertXtdZoneScheme(scheme);
}
@Override
public int updateXtdZoneScheme(XtdZoneScheme scheme)
{
return schemeMapper.updateXtdZoneScheme(scheme);
}
@Override
public int deleteXtdZoneSchemeByIds(Long[] zsIds)
{
for (Long zsId : zsIds)
{
int count = divisionMapper.countByZsId(zsId);
if (count > 0)
{
XtdZoneScheme scheme = schemeMapper.selectXtdZoneSchemeById(zsId);
String name = scheme != null ? scheme.getZsName() : String.valueOf(zsId);
throw new ServiceException(MessageUtils.message("price.zone.delete.hasDivision", name));
}
}
return schemeMapper.deleteXtdZoneSchemeByIds(zsIds);
}
@Override
public XtdZoneDivision selectXtdZoneDivisionById(Long zdId)
{
XtdZoneDivision division = divisionMapper.selectXtdZoneDivisionById(zdId);
if (division != null)
{
division.setCountryCount(countryMapper.countByZdId(zdId));
}
return division;
}
@Override
public List<XtdZoneDivision> selectXtdZoneDivisionList(XtdZoneDivision division)
{
List<XtdZoneDivision> list = divisionMapper.selectXtdZoneDivisionList(division);
for (XtdZoneDivision item : list)
{
item.setCountryCount(countryMapper.countByZdId(item.getZdId()));
}
return list;
}
@Override
public int insertXtdZoneDivision(XtdZoneDivision division)
{
return divisionMapper.insertXtdZoneDivision(division);
}
@Override
public int updateXtdZoneDivision(XtdZoneDivision division)
{
return divisionMapper.updateXtdZoneDivision(division);
}
@Override
public int deleteXtdZoneDivisionByIds(Long[] zdIds)
{
return divisionMapper.deleteXtdZoneDivisionByIds(zdIds);
}
@Override
public XtdZoneCountry selectXtdZoneCountryById(Long zcId)
{
return countryMapper.selectXtdZoneCountryById(zcId);
}
@Override
public List<XtdZoneCountry> selectXtdZoneCountryList(XtdZoneCountry country)
{
return countryMapper.selectXtdZoneCountryList(country);
}
@Override
public int insertXtdZoneCountry(XtdZoneCountry country)
{
if (StringUtils.isEmpty(country.getZcDividekind()))
{
country.setZcDividekind("C");
}
return countryMapper.insertXtdZoneCountry(country);
}
@Override
public int updateXtdZoneCountry(XtdZoneCountry country)
{
return countryMapper.updateXtdZoneCountry(country);
}
@Override
public int deleteXtdZoneCountryByIds(Long[] zcIds)
{
return countryMapper.deleteXtdZoneCountryByIds(zcIds);
}
}
@@ -0,0 +1,144 @@
package com.ruoyi.price.util;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.ruoyi.common.utils.StringUtils;
/**
* Parse Excel country cell with optional zone suffix, map labels to English abbreviations.
*/
public final class ZoneCodeAbbrevs
{
// fullwidth () and ASCII ()
private static final Pattern ZONE_SUFFIX = Pattern.compile(
"^(.+?)[\\(\\uFF08]([^\\)\\uFF09]+)[\\)\\uFF09]\\s*$");
private ZoneCodeAbbrevs()
{
}
public static final class CountryZoneParts
{
public final String countryName;
public final String zoneLabel;
public CountryZoneParts(String countryName, String zoneLabel)
{
this.countryName = countryName;
this.zoneLabel = zoneLabel;
}
}
/**
* Split country cell like Country(zoneLabel); no parenthesis -> zoneLabel null.
*/
public static CountryZoneParts splitCountryZone(String raw)
{
if (StringUtils.isEmpty(raw))
{
return null;
}
String n = raw.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();
if (n.isEmpty())
{
return null;
}
Matcher m = ZONE_SUFFIX.matcher(n);
if (m.matches())
{
String country = m.group(1).trim();
String zone = m.group(2).trim();
if (StringUtils.isEmpty(country))
{
return new CountryZoneParts(n, null);
}
return new CountryZoneParts(country, StringUtils.isEmpty(zone) ? null : zone);
}
return new CountryZoneParts(n, null);
}
public static String toAbbrev(String raw)
{
if (StringUtils.isEmpty(raw))
{
return raw;
}
String key = raw.replace('\r', ' ').replace('\n', ' ').replaceAll("\\s+", " ").trim();
switch (key)
{
case "1\u533a":
return "Z1";
case "2\u533a":
return "Z2";
case "3\u533a":
return "Z3";
case "4\u533a":
return "Z4";
case "5\u533a":
return "Z5";
case "Mondial Relay":
return "MR";
case "\u6c96\u7e04\u770c":
return "OKN";
case "\u5317\u7231\u5c14\u5170":
return "NI";
case "\u9ad8\u5730\u548c\u5c9b\u5c7f":
return "HI";
case "\u6d77\u5ce1\u7fa4\u5c9b(\u542b\u9521\u5229\u5c9b)":
return "CI";
case "\u9a6c\u6069\u5c9b":
return "IM";
case "Beckton":
return "BEC";
case "Bristol":
return "BRS";
case "Cambridge":
return "CAM";
case "Collier Wood":
return "CW";
case "Guernsey":
return "GG";
case "Isle of Man Bailiwick of Guernsey States of Jersey":
return "IM_GG_JE";
case "Lakeside":
return "LKS";
case "London Congestion Zone":
return "LCZ";
case "London Congestion Zone - Gemini":
return "LCZ_GEM";
case "London ULEZ - Wimbledon":
return "ULEZ_WIM";
case "Northern Ireland Isles of Scilly":
return "NI_SCILLY";
case "Old Kent Road Cargo Bikes":
return "OKR_CB";
case "Oxford":
return "OXF";
case "The Highlands Islands of Scotland":
return "HIGHLANDS";
default:
if (key.matches("(?i)Z[1-9]\\d*"))
{
return key.toUpperCase(Locale.ROOT);
}
if (key.matches(".*[\\u4e00-\\u9fff].*"))
{
return key;
}
return key.replace(' ', '_').toUpperCase(Locale.ROOT).replaceAll("_+", "_");
}
}
}
@@ -10,6 +10,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="productCode" column="product_code" />
<result property="countryName" column="country_name" />
<result property="countryCode" column="country_code" />
<result property="zoneId" column="zone_id" />
<result property="zoneCode" column="zone_code" />
<result property="zoneName" column="zone_name" />
<result property="weightMin" column="weight_min" />
<result property="weightMax" column="weight_max" />
<result property="freightPrice" column="freight_price" />
@@ -26,7 +29,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</resultMap>
<sql id="selectVo">
select detail_id, product_id, product_code, country_name, country_code, weight_min, weight_max,
select detail_id, product_id, product_code, country_name, country_code, zone_id, zone_code, zone_name,
weight_min, weight_max,
freight_price, registration_fee, size_limit, last_mile, lead_time,
effective_date, expire_date, status, create_time, update_time
from bms_product_country
@@ -47,6 +51,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="countryCode != null and countryCode != ''">
AND country_code = #{countryCode}
</if>
<if test="zoneId != null">
AND zone_id = #{zoneId}
</if>
<if test="zoneCode != null and zoneCode != ''">
AND zone_code = #{zoneCode}
</if>
<if test="weightMin != null">
AND weight_min = #{weightMin}
</if>
@@ -66,16 +76,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
AND status = #{effectStatus}
</if>
</where>
order by effective_date desc, country_name asc, weight_min asc, detail_id asc
order by effective_date desc, country_name asc, zone_code asc, weight_min asc, detail_id asc
</select>
<insert id="insertBmsProductCountry" parameterType="BmsProductCountry" useGeneratedKeys="true" keyProperty="detailId">
insert into bms_product_country (
product_id, product_code, country_name, country_code, weight_min, weight_max,
product_id, product_code, country_name, country_code, zone_id, zone_code, zone_name,
weight_min, weight_max,
freight_price, registration_fee, size_limit, last_mile, lead_time,
effective_date, expire_date, status, create_time
) values (
#{productId}, #{productCode}, #{countryName}, #{countryCode}, #{weightMin}, #{weightMax},
#{productId}, #{productCode}, #{countryName}, #{countryCode}, #{zoneId}, #{zoneCode}, #{zoneName},
#{weightMin}, #{weightMax},
#{freightPrice}, #{registrationFee}, #{sizeLimit}, #{lastMile}, #{leadTime},
#{effectiveDate}, #{expireDate}, IFNULL(#{status}, '0'), sysdate()
)
@@ -162,7 +174,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="bizDate == null">
and expire_date is null
</if>
order by effective_date desc, weight_min asc, detail_id asc
order by
case when zone_code is null or zone_code = '' then 0 else 1 end,
effective_date desc, weight_min asc, detail_id asc
limit 1
</select>
@@ -11,6 +11,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="productName" column="product_name" />
<result property="countryCode" column="country_code" />
<result property="countryName" column="country_name" />
<result property="zoneId" column="zone_id" />
<result property="zoneCode" column="zone_code" />
<result property="zoneName" column="zone_name" />
<result property="zdId" column="zd_id" />
<result property="wsdId" column="wsd_id" />
<result property="weightLabel" column="weight_label" />
@@ -26,6 +29,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<sql id="selectPrcPriceSetDetailVo">
select psd_id, price_set_id, product_code, product_name, country_code, country_name,
zone_id, zone_code, zone_name,
zd_id, wsd_id, weight_label, product_country_id, fv_value, fv_registration_fee,
discount_value, round_mode, pr_id, create_time, update_time
from prc_price_set_detail
@@ -48,6 +52,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="countryCode != null and countryCode != ''">
AND country_code = #{countryCode}
</if>
<if test="zoneCode != null and zoneCode != ''">
AND zone_code = #{zoneCode}
</if>
</where>
order by psd_id asc
</select>
@@ -67,6 +74,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="productName != null">product_name,</if>
<if test="countryCode != null">country_code,</if>
<if test="countryName != null">country_name,</if>
<if test="zoneId != null">zone_id,</if>
<if test="zoneCode != null">zone_code,</if>
<if test="zoneName != null">zone_name,</if>
<if test="zdId != null">zd_id,</if>
<if test="wsdId != null">wsd_id,</if>
<if test="weightLabel != null">weight_label,</if>
@@ -83,6 +93,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="productName != null">#{productName},</if>
<if test="countryCode != null">#{countryCode},</if>
<if test="countryName != null">#{countryName},</if>
<if test="zoneId != null">#{zoneId},</if>
<if test="zoneCode != null">#{zoneCode},</if>
<if test="zoneName != null">#{zoneName},</if>
<if test="zdId != null">#{zdId},</if>
<if test="wsdId != null">#{wsdId},</if>
<if test="weightLabel != null">#{weightLabel},</if>
@@ -103,6 +116,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="productName != null">product_name = #{productName},</if>
country_code = #{countryCode},
country_name = #{countryName},
zone_id = #{zoneId},
zone_code = #{zoneCode},
zone_name = #{zoneName},
zd_id = #{zdId},
wsd_id = #{wsdId},
weight_label = #{weightLabel},
@@ -149,6 +165,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
1 = 0
</otherwise>
</choose>
<if test="zoneCode != null and zoneCode != ''">
and (zone_code is null or upper(zone_code) = upper(#{zoneCode}))
</if>
<if test="weightLabel != null and weightLabel != ''">
and (weight_label is null or weight_label = #{weightLabel} or weight_label like concat('%', #{weightLabel}, '%'))
</if>
@@ -156,6 +175,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
)
order by
case when #{productCountryId} is not null and product_country_id = #{productCountryId} then 0 else 1 end,
case when #{zoneCode} is not null and upper(ifnull(zone_code,'')) = upper(#{zoneCode}) then 0 else 1 end,
case when zone_code is null then 1 else 0 end,
psd_id asc
limit 1
</select>
@@ -0,0 +1,94 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.price.mapper.PrcZoneDetailMapper">
<resultMap type="PrcZoneDetail" id="PrcZoneDetailResult">
<id property="detailId" column="detail_id" />
<result property="zoneId" column="zone_id" />
<result property="postcode" column="postcode" />
<result property="createTime" column="create_time" />
<result property="updateTime" column="update_time" />
</resultMap>
<sql id="selectPrcZoneDetailVo">
select detail_id, zone_id, postcode, create_time, update_time
from prc_zone_detail
</sql>
<select id="selectPrcZoneDetailById" parameterType="Long" resultMap="PrcZoneDetailResult">
<include refid="selectPrcZoneDetailVo"/>
where detail_id = #{detailId}
</select>
<select id="selectPrcZoneDetailList" parameterType="PrcZoneDetail" resultMap="PrcZoneDetailResult">
<include refid="selectPrcZoneDetailVo"/>
<where>
<if test="zoneId != null">
AND zone_id = #{zoneId}
</if>
<if test="postcode != null and postcode != ''">
AND postcode like concat('%', #{postcode}, '%')
</if>
</where>
order by postcode asc, detail_id asc
</select>
<select id="countByZoneId" parameterType="Long" resultType="int">
select count(1) from prc_zone_detail where zone_id = #{zoneId}
</select>
<select id="countByRegionId" parameterType="Long" resultType="int">
select count(1) from prc_zone_detail d
inner join prc_zone z on z.zone_id = d.zone_id
where z.region_id = #{regionId}
</select>
<insert id="insertPrcZoneDetail" parameterType="PrcZoneDetail" useGeneratedKeys="true" keyProperty="detailId">
insert into prc_zone_detail (
<if test="zoneId != null">zone_id,</if>
<if test="postcode != null and postcode != ''">postcode,</if>
create_time
) values (
<if test="zoneId != null">#{zoneId},</if>
<if test="postcode != null and postcode != ''">#{postcode},</if>
sysdate()
)
</insert>
<insert id="insertPrcZoneDetailBatch">
insert into prc_zone_detail (zone_id, postcode, create_time) values
<foreach collection="list" item="item" separator=",">
(#{item.zoneId}, #{item.postcode}, sysdate())
</foreach>
</insert>
<update id="updatePrcZoneDetail" parameterType="PrcZoneDetail">
update prc_zone_detail
<set>
<if test="zoneId != null">zone_id = #{zoneId},</if>
<if test="postcode != null and postcode != ''">postcode = #{postcode},</if>
update_time = sysdate()
</set>
where detail_id = #{detailId}
</update>
<delete id="deletePrcZoneDetailByIds" parameterType="Long">
delete from prc_zone_detail where detail_id in
<foreach item="detailId" collection="array" open="(" separator="," close=")">
#{detailId}
</foreach>
</delete>
<delete id="deletePrcZoneDetailByZoneId" parameterType="Long">
delete from prc_zone_detail where zone_id = #{zoneId}
</delete>
<delete id="deletePrcZoneDetailByRegionId" parameterType="Long">
delete d from prc_zone_detail d
inner join prc_zone z on z.zone_id = d.zone_id
where z.region_id = #{regionId}
</delete>
</mapper>
@@ -0,0 +1,162 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.price.mapper.PrcZoneMapper">
<resultMap type="PrcZone" id="PrcZoneResult">
<id property="zoneId" column="zone_id" />
<result property="regionId" column="region_id" />
<result property="countryCode" column="country_code" />
<result property="zoneCode" column="zone_code" />
<result property="zoneName" column="zone_name" />
<result property="zoneSort" column="zone_sort" />
<result property="remark" column="remark" />
<result property="createTime" column="create_time" />
<result property="updateTime" column="update_time" />
<result property="detailCount" column="detail_count" />
<result property="countryNameCn" column="country_name_cn" />
</resultMap>
<resultMap type="PrcZoneRegion" id="PrcZoneRegionResult">
<id property="regionId" column="region_id" />
<result property="countryNameCn" column="country_name_cn" />
<result property="countryNameEn" column="country_name_en" />
<result property="countryCode2" column="country_code_2" />
<result property="status" column="status" />
<result property="zoneCount" column="zone_count" />
<result property="detailCount" column="detail_count" />
</resultMap>
<sql id="selectPrcZoneVo">
select zone_id, region_id, country_code, zone_code, zone_name, zone_sort, remark, create_time, update_time
from prc_zone
</sql>
<select id="selectPrcZoneById" parameterType="Long" resultMap="PrcZoneResult">
<include refid="selectPrcZoneVo"/>
where zone_id = #{zoneId}
</select>
<select id="selectPrcZoneListByRegionId" parameterType="Long" resultMap="PrcZoneResult">
select z.zone_id, z.region_id, z.country_code, z.zone_code, z.zone_name, z.zone_sort, z.remark,
z.create_time, z.update_time,
(select count(1) from prc_zone_detail d where d.zone_id = z.zone_id) as detail_count
from prc_zone z
where z.region_id = #{regionId}
order by z.zone_sort asc, z.zone_id asc
</select>
<select id="selectPrcZoneList" parameterType="PrcZone" resultMap="PrcZoneResult">
select z.zone_id, z.region_id, z.country_code, z.zone_code, z.zone_name, z.zone_sort, z.remark,
z.create_time, z.update_time,
r.country_name_cn,
(select count(1) from prc_zone_detail d where d.zone_id = z.zone_id) as detail_count
from prc_zone z
left join bms_region r on r.region_id = z.region_id
<where>
<if test="regionId != null">
AND z.region_id = #{regionId}
</if>
<if test="countryCode != null and countryCode != ''">
AND z.country_code = #{countryCode}
</if>
<if test="zoneCode != null and zoneCode != ''">
AND z.zone_code like concat('%', #{zoneCode}, '%')
</if>
<if test="zoneName != null and zoneName != ''">
AND z.zone_name like concat('%', #{zoneName}, '%')
</if>
</where>
order by z.zone_sort asc, z.zone_id asc
</select>
<select id="selectZoneRegionList" parameterType="PrcZoneRegion" resultMap="PrcZoneRegionResult">
select r.region_id, r.country_name_cn, r.country_name_en, r.country_code_2, r.status,
count(distinct z.zone_id) as zone_count,
count(d.detail_id) as detail_count
from prc_zone z
inner join bms_region r on r.region_id = z.region_id
left join prc_zone_detail d on d.zone_id = z.zone_id
<where>
<if test="countryNameCn != null and countryNameCn != ''">
AND r.country_name_cn like concat('%', #{countryNameCn}, '%')
</if>
<if test="countryCode2 != null and countryCode2 != ''">
AND (r.country_code_2 = #{countryCode2} OR z.country_code = #{countryCode2})
</if>
<if test="status != null and status != ''">
AND r.status = #{status}
</if>
</where>
group by r.region_id, r.country_name_cn, r.country_name_en, r.country_code_2, r.status
order by r.country_name_cn asc, r.region_id asc
</select>
<select id="selectZoneRegionById" parameterType="Long" resultMap="PrcZoneRegionResult">
select r.region_id, r.country_name_cn, r.country_name_en, r.country_code_2, r.status,
(select count(1) from prc_zone z where z.region_id = r.region_id) as zone_count,
(select count(1) from prc_zone_detail d
inner join prc_zone z on z.zone_id = d.zone_id
where z.region_id = r.region_id) as detail_count
from bms_region r
where r.region_id = #{regionId}
</select>
<select id="countByRegionId" parameterType="Long" resultType="int">
select count(1) from prc_zone where region_id = #{regionId}
</select>
<insert id="insertPrcZone" parameterType="PrcZone" useGeneratedKeys="true" keyProperty="zoneId">
insert into prc_zone (
<if test="regionId != null">region_id,</if>
<if test="countryCode != null and countryCode != ''">country_code,</if>
<if test="zoneCode != null and zoneCode != ''">zone_code,</if>
<if test="zoneName != null">zone_name,</if>
<if test="zoneSort != null">zone_sort,</if>
<if test="remark != null">remark,</if>
create_time
) values (
<if test="regionId != null">#{regionId},</if>
<if test="countryCode != null and countryCode != ''">#{countryCode},</if>
<if test="zoneCode != null and zoneCode != ''">#{zoneCode},</if>
<if test="zoneName != null">#{zoneName},</if>
<if test="zoneSort != null">#{zoneSort},</if>
<if test="remark != null">#{remark},</if>
sysdate()
)
</insert>
<update id="updatePrcZone" parameterType="PrcZone">
update prc_zone
<set>
<if test="regionId != null">region_id = #{regionId},</if>
<if test="countryCode != null and countryCode != ''">country_code = #{countryCode},</if>
<if test="zoneCode != null and zoneCode != ''">zone_code = #{zoneCode},</if>
<if test="zoneName != null">zone_name = #{zoneName},</if>
<if test="zoneSort != null">zone_sort = #{zoneSort},</if>
<if test="remark != null">remark = #{remark},</if>
update_time = sysdate()
</set>
where zone_id = #{zoneId}
</update>
<delete id="deletePrcZoneByIds" parameterType="Long">
delete from prc_zone where zone_id in
<foreach item="zoneId" collection="array" open="(" separator="," close=")">
#{zoneId}
</foreach>
</delete>
<delete id="deletePrcZoneByRegionId" parameterType="Long">
delete from prc_zone where region_id = #{regionId}
</delete>
<delete id="deletePrcZoneByRegionIds" parameterType="Long">
delete from prc_zone where region_id in
<foreach item="regionId" collection="array" open="(" separator="," close=")">
#{regionId}
</foreach>
</delete>
</mapper>
@@ -1,84 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.price.mapper.XtdZoneCountryMapper">
<resultMap type="XtdZoneCountry" id="XtdZoneCountryResult">
<id property="zcId" column="zc_id" />
<result property="zdId" column="zd_id" />
<result property="countryCode" column="country_code" />
<result property="zcDividekind" column="zc_dividekind" />
<result property="createTime" column="create_time" />
<result property="updateTime" column="update_time" />
</resultMap>
<sql id="selectXtdZoneCountryVo">
select zc_id, zd_id, country_code, zc_dividekind, create_time, update_time
from xtd_zonecountry
</sql>
<select id="selectXtdZoneCountryById" parameterType="Long" resultMap="XtdZoneCountryResult">
<include refid="selectXtdZoneCountryVo"/>
where zc_id = #{zcId}
</select>
<select id="selectXtdZoneCountryListByZdId" parameterType="Long" resultMap="XtdZoneCountryResult">
<include refid="selectXtdZoneCountryVo"/>
where zd_id = #{zdId}
order by zc_id asc
</select>
<select id="selectXtdZoneCountryList" parameterType="XtdZoneCountry" resultMap="XtdZoneCountryResult">
<include refid="selectXtdZoneCountryVo"/>
<where>
<if test="zdId != null">
AND zd_id = #{zdId}
</if>
<if test="countryCode != null and countryCode != ''">
AND country_code = #{countryCode}
</if>
<if test="zcDividekind != null and zcDividekind != ''">
AND zc_dividekind = #{zcDividekind}
</if>
</where>
order by zc_id asc
</select>
<select id="countByZdId" parameterType="Long" resultType="int">
select count(1) from xtd_zonecountry where zd_id = #{zdId}
</select>
<insert id="insertXtdZoneCountry" parameterType="XtdZoneCountry" useGeneratedKeys="true" keyProperty="zcId">
insert into xtd_zonecountry (
<if test="zdId != null">zd_id,</if>
<if test="countryCode != null and countryCode != ''">country_code,</if>
<if test="zcDividekind != null and zcDividekind != ''">zc_dividekind,</if>
create_time
) values (
<if test="zdId != null">#{zdId},</if>
<if test="countryCode != null and countryCode != ''">#{countryCode},</if>
<if test="zcDividekind != null and zcDividekind != ''">#{zcDividekind},</if>
sysdate()
)
</insert>
<update id="updateXtdZoneCountry" parameterType="XtdZoneCountry">
update xtd_zonecountry
<set>
<if test="zdId != null">zd_id = #{zdId},</if>
<if test="countryCode != null and countryCode != ''">country_code = #{countryCode},</if>
<if test="zcDividekind != null and zcDividekind != ''">zc_dividekind = #{zcDividekind},</if>
update_time = sysdate()
</set>
where zc_id = #{zcId}
</update>
<delete id="deleteXtdZoneCountryByIds" parameterType="Long">
delete from xtd_zonecountry where zc_id in
<foreach item="zcId" collection="array" open="(" separator="," close=")">
#{zcId}
</foreach>
</delete>
</mapper>
@@ -1,97 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.price.mapper.XtdZoneDivisionMapper">
<resultMap type="XtdZoneDivision" id="XtdZoneDivisionResult">
<id property="zdId" column="zd_id" />
<result property="zsId" column="zs_id" />
<result property="zdSort" column="zd_sort" />
<result property="zdCode" column="zd_code" />
<result property="znName" column="zn_name" />
<result property="znEname" column="zn_ename" />
<result property="znNote" column="zn_note" />
<result property="createTime" column="create_time" />
<result property="updateTime" column="update_time" />
<result property="countryCount" column="country_count" />
</resultMap>
<sql id="selectXtdZoneDivisionVo">
select zd_id, zs_id, zd_sort, zd_code, zn_name, zn_ename, zn_note, create_time, update_time
from xtd_zone_division
</sql>
<select id="selectXtdZoneDivisionById" parameterType="Long" resultMap="XtdZoneDivisionResult">
<include refid="selectXtdZoneDivisionVo"/>
where zd_id = #{zdId}
</select>
<select id="selectXtdZoneDivisionListByZsId" parameterType="Long" resultMap="XtdZoneDivisionResult">
<include refid="selectXtdZoneDivisionVo"/>
where zs_id = #{zsId}
order by zd_sort asc, zd_id asc
</select>
<select id="selectXtdZoneDivisionList" parameterType="XtdZoneDivision" resultMap="XtdZoneDivisionResult">
<include refid="selectXtdZoneDivisionVo"/>
<where>
<if test="zsId != null">
AND zs_id = #{zsId}
</if>
<if test="zdCode != null and zdCode != ''">
AND zd_code like concat('%', #{zdCode}, '%')
</if>
<if test="znName != null and znName != ''">
AND zn_name like concat('%', #{znName}, '%')
</if>
</where>
order by zd_sort asc, zd_id asc
</select>
<select id="countByZsId" parameterType="Long" resultType="int">
select count(1) from xtd_zone_division where zs_id = #{zsId}
</select>
<insert id="insertXtdZoneDivision" parameterType="XtdZoneDivision" useGeneratedKeys="true" keyProperty="zdId">
insert into xtd_zone_division (
<if test="zsId != null">zs_id,</if>
<if test="zdSort != null">zd_sort,</if>
<if test="zdCode != null">zd_code,</if>
<if test="znName != null">zn_name,</if>
<if test="znEname != null">zn_ename,</if>
<if test="znNote != null">zn_note,</if>
create_time
) values (
<if test="zsId != null">#{zsId},</if>
<if test="zdSort != null">#{zdSort},</if>
<if test="zdCode != null">#{zdCode},</if>
<if test="znName != null">#{znName},</if>
<if test="znEname != null">#{znEname},</if>
<if test="znNote != null">#{znNote},</if>
sysdate()
)
</insert>
<update id="updateXtdZoneDivision" parameterType="XtdZoneDivision">
update xtd_zone_division
<set>
<if test="zsId != null">zs_id = #{zsId},</if>
<if test="zdSort != null">zd_sort = #{zdSort},</if>
<if test="zdCode != null">zd_code = #{zdCode},</if>
<if test="znName != null">zn_name = #{znName},</if>
<if test="znEname != null">zn_ename = #{znEname},</if>
<if test="znNote != null">zn_note = #{znNote},</if>
update_time = sysdate()
</set>
where zd_id = #{zdId}
</update>
<delete id="deleteXtdZoneDivisionByIds" parameterType="Long">
delete from xtd_zone_division where zd_id in
<foreach item="zdId" collection="array" open="(" separator="," close=")">
#{zdId}
</foreach>
</delete>
</mapper>
@@ -1,99 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.price.mapper.XtdZoneSchemeMapper">
<resultMap type="XtdZoneScheme" id="XtdZoneSchemeResult">
<id property="zsId" column="zs_id" />
<result property="zsName" column="zs_name" />
<result property="ssCode" column="ss_code" />
<result property="pgCode" column="pg_code" />
<result property="snId" column="sn_id" />
<result property="zsNote" column="zs_note" />
<result property="tmsId" column="tms_id" />
<result property="createBy" column="created_by" />
<result property="createTime" column="create_time" />
<result property="updateBy" column="updated_by" />
<result property="updateTime" column="update_time" />
<result property="divisionCount" column="division_count" />
</resultMap>
<sql id="selectXtdZoneSchemeVo">
select zs_id, zs_name, ss_code, pg_code, sn_id, zs_note, tms_id,
created_by, create_time, updated_by, update_time
from xtd_zone_scheme
</sql>
<select id="selectXtdZoneSchemeById" parameterType="Long" resultMap="XtdZoneSchemeResult">
<include refid="selectXtdZoneSchemeVo"/>
where zs_id = #{zsId}
</select>
<select id="selectXtdZoneSchemeList" parameterType="XtdZoneScheme" resultMap="XtdZoneSchemeResult">
<include refid="selectXtdZoneSchemeVo"/>
<where>
<if test="zsName != null and zsName != ''">
AND zs_name like concat('%', #{zsName}, '%')
</if>
<if test="ssCode != null and ssCode != ''">
AND ss_code = #{ssCode}
</if>
<if test="pgCode != null and pgCode != ''">
AND pg_code = #{pgCode}
</if>
<if test="snId != null">
AND sn_id = #{snId}
</if>
<if test="tmsId != null">
AND tms_id = #{tmsId}
</if>
</where>
order by zs_id desc
</select>
<insert id="insertXtdZoneScheme" parameterType="XtdZoneScheme" useGeneratedKeys="true" keyProperty="zsId">
insert into xtd_zone_scheme (
<if test="zsName != null and zsName != ''">zs_name,</if>
<if test="ssCode != null and ssCode != ''">ss_code,</if>
<if test="pgCode != null">pg_code,</if>
<if test="snId != null">sn_id,</if>
<if test="zsNote != null">zs_note,</if>
<if test="tmsId != null">tms_id,</if>
<if test="createBy != null and createBy != ''">created_by,</if>
create_time
) values (
<if test="zsName != null and zsName != ''">#{zsName},</if>
<if test="ssCode != null and ssCode != ''">#{ssCode},</if>
<if test="pgCode != null">#{pgCode},</if>
<if test="snId != null">#{snId},</if>
<if test="zsNote != null">#{zsNote},</if>
<if test="tmsId != null">#{tmsId},</if>
<if test="createBy != null and createBy != ''">#{createBy},</if>
sysdate()
)
</insert>
<update id="updateXtdZoneScheme" parameterType="XtdZoneScheme">
update xtd_zone_scheme
<set>
<if test="zsName != null and zsName != ''">zs_name = #{zsName},</if>
<if test="ssCode != null and ssCode != ''">ss_code = #{ssCode},</if>
<if test="pgCode != null">pg_code = #{pgCode},</if>
<if test="snId != null">sn_id = #{snId},</if>
<if test="zsNote != null">zs_note = #{zsNote},</if>
<if test="tmsId != null">tms_id = #{tmsId},</if>
<if test="updateBy != null and updateBy != ''">updated_by = #{updateBy},</if>
update_time = sysdate()
</set>
where zs_id = #{zsId}
</update>
<delete id="deleteXtdZoneSchemeByIds" parameterType="Long">
delete from xtd_zone_scheme where zs_id in
<foreach item="zsId" collection="array" open="(" separator="," close=")">
#{zsId}
</foreach>
</delete>
</mapper>
+6 -3
View File
@@ -38,9 +38,12 @@ CREATE TABLE IF NOT EXISTS prc_price_set_detail (
price_set_id BIGINT(20) NOT NULL COMMENT '价格套餐id',
product_code VARCHAR(10) NOT NULL COMMENT '产品代码',
product_name VARCHAR(50) DEFAULT NULL COMMENT '产品名称',
country_code CHAR(2) DEFAULT NULL COMMENT '国家二字码',
country_name VARCHAR(50) DEFAULT NULL COMMENT '国家名称',
zd_id BIGINT(20) DEFAULT NULL COMMENT '收费区划id',
country_code CHAR(2) DEFAULT NULL COMMENT 'country ISO2',
country_name VARCHAR(50) DEFAULT NULL COMMENT 'country name',
zone_id BIGINT(20) DEFAULT NULL COMMENT 'prc_zone.zone_id',
zone_code VARCHAR(50) DEFAULT NULL COMMENT 'zone code e.g. Z1',
zone_name VARCHAR(100) DEFAULT NULL COMMENT 'zone display name',
zd_id BIGINT(20) DEFAULT NULL COMMENT 'legacy zone division id',
wsd_id BIGINT(20) DEFAULT NULL COMMENT '重量段明细id',
weight_label VARCHAR(50) DEFAULT NULL COMMENT '重量段展示',
product_country_id BIGINT(20) DEFAULT NULL COMMENT '关联 bms_product_country.detail_id',
@@ -0,0 +1,55 @@
-- Add zone fields to prc_price_set_detail (nullable when country has no zone)
-- Idempotent: safe to re-run
-- charset: utf8mb4
SET NAMES utf8mb4;
SET @db := DATABASE();
-- zone_id
SET @has := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'prc_price_set_detail' AND COLUMN_NAME = 'zone_id'
);
SET @sql := IF(@has = 0,
'ALTER TABLE prc_price_set_detail ADD COLUMN zone_id BIGINT(20) DEFAULT NULL COMMENT ''prc_zone.zone_id'' AFTER country_name',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- zone_code
SET @has := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'prc_price_set_detail' AND COLUMN_NAME = 'zone_code'
);
SET @sql := IF(@has = 0,
'ALTER TABLE prc_price_set_detail ADD COLUMN zone_code VARCHAR(50) DEFAULT NULL COMMENT ''zone code e.g. Z1'' AFTER zone_id',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- zone_name
SET @has := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'prc_price_set_detail' AND COLUMN_NAME = 'zone_name'
);
SET @sql := IF(@has = 0,
'ALTER TABLE prc_price_set_detail ADD COLUMN zone_name VARCHAR(100) DEFAULT NULL COMMENT ''zone display name'' AFTER zone_code',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- index
SET @has := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'prc_price_set_detail' AND INDEX_NAME = 'idx_psd_zone'
);
SET @sql := IF(@has = 0,
'ALTER TABLE prc_price_set_detail ADD INDEX idx_psd_zone (zone_id)',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @has := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'prc_price_set_detail' AND INDEX_NAME = 'idx_psd_country_zone'
);
SET @sql := IF(@has = 0,
'ALTER TABLE prc_price_set_detail ADD INDEX idx_psd_country_zone (price_set_id, product_code, country_code, zone_code)',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+6 -1
View File
@@ -34,6 +34,9 @@ CREATE TABLE IF NOT EXISTS bms_product_country (
product_code varchar(20) NOT NULL COMMENT '产品代码',
country_name varchar(50) NOT NULL COMMENT '国家中文名',
country_code char(2) DEFAULT NULL COMMENT 'ISO2,可空待补齐',
zone_id bigint DEFAULT NULL COMMENT 'prc_zone.zone_id',
zone_code varchar(50) DEFAULT NULL COMMENT 'zone code e.g. Z1',
zone_name varchar(100) DEFAULT NULL COMMENT 'zone display name',
weight_min decimal(10,3) DEFAULT NULL COMMENT '重量段最小值KG(含)',
weight_max decimal(10,3) DEFAULT NULL COMMENT '重量段最大值KG(含)',
freight_price decimal(10,4) DEFAULT NULL COMMENT '运费 RMB/KG',
@@ -47,9 +50,11 @@ CREATE TABLE IF NOT EXISTS bms_product_country (
create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (detail_id),
UNIQUE KEY uk_product_country_weight_eff (product_code, country_name, weight_min, weight_max, effective_date),
UNIQUE KEY uk_product_country_zone_weight_eff (product_code, country_name, zone_code, weight_min, weight_max, effective_date),
KEY idx_product_country_pid (product_id),
KEY idx_country_code (country_code),
KEY idx_product_country_zone (zone_id),
KEY idx_product_country_zone_code (country_code, zone_code),
KEY idx_weight_match (product_code, country_code, weight_min, weight_max),
KEY idx_product_eff (product_code, effective_date, expire_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='产品国家明细-重量段+运费挂号费+尺寸/尾程/时效';
@@ -0,0 +1,74 @@
-- Add zone fields to bms_product_country (nullable when no zone)
-- Idempotent: safe to re-run (skip if column/index already exists)
-- charset: utf8mb4
SET NAMES utf8mb4;
SET @db := DATABASE();
-- zone_id
SET @has := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'bms_product_country' AND COLUMN_NAME = 'zone_id'
);
SET @sql := IF(@has = 0,
'ALTER TABLE bms_product_country ADD COLUMN zone_id BIGINT(20) DEFAULT NULL COMMENT ''prc_zone.zone_id'' AFTER country_code',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- zone_code
SET @has := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'bms_product_country' AND COLUMN_NAME = 'zone_code'
);
SET @sql := IF(@has = 0,
'ALTER TABLE bms_product_country ADD COLUMN zone_code VARCHAR(50) DEFAULT NULL COMMENT ''zone code e.g. Z1'' AFTER zone_id',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- zone_name
SET @has := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'bms_product_country' AND COLUMN_NAME = 'zone_name'
);
SET @sql := IF(@has = 0,
'ALTER TABLE bms_product_country ADD COLUMN zone_name VARCHAR(100) DEFAULT NULL COMMENT ''zone display name'' AFTER zone_code',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- indexes
SET @has := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'bms_product_country' AND INDEX_NAME = 'idx_product_country_zone'
);
SET @sql := IF(@has = 0,
'ALTER TABLE bms_product_country ADD INDEX idx_product_country_zone (zone_id)',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @has := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'bms_product_country' AND INDEX_NAME = 'idx_product_country_zone_code'
);
SET @sql := IF(@has = 0,
'ALTER TABLE bms_product_country ADD INDEX idx_product_country_zone_code (country_code, zone_code)',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- unique key: drop old, add new
SET @has := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'bms_product_country' AND INDEX_NAME = 'uk_product_country_weight_eff'
);
SET @sql := IF(@has > 0,
'ALTER TABLE bms_product_country DROP INDEX uk_product_country_weight_eff',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @has := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'bms_product_country' AND INDEX_NAME = 'uk_product_country_zone_weight_eff'
);
SET @sql := IF(@has = 0,
'ALTER TABLE bms_product_country ADD UNIQUE KEY uk_product_country_zone_weight_eff (product_code, country_name, zone_code, weight_min, weight_max, effective_date)',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+43
View File
@@ -0,0 +1,43 @@
-- ----------------------------
-- Zone management: bms_region -> prc_zone -> prc_zone_detail
-- Country master is bms_region (no prc_zone_country)
-- charset: utf8mb4
-- ----------------------------
SET FOREIGN_KEY_CHECKS = 0;
DROP TABLE IF EXISTS prc_zone_detail;
DROP TABLE IF EXISTS prc_zone;
DROP TABLE IF EXISTS prc_zone_country;
DROP TABLE IF EXISTS xtd_zonecountry;
DROP TABLE IF EXISTS xtd_zone_division;
DROP TABLE IF EXISTS xtd_zone_scheme;
SET FOREIGN_KEY_CHECKS = 1;
CREATE TABLE IF NOT EXISTS prc_zone (
zone_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'zone id',
region_id BIGINT(20) NOT NULL COMMENT 'bms_region.region_id',
country_code VARCHAR(10) NOT NULL COMMENT 'ISO2 country code from bms_region',
zone_code VARCHAR(50) NOT NULL COMMENT 'zone code e.g. 1',
zone_name VARCHAR(100) DEFAULT NULL COMMENT 'display name e.g. AU(1)',
zone_sort INT(11) DEFAULT 0 COMMENT 'sort',
remark VARCHAR(500) DEFAULT NULL COMMENT 'remark',
create_time DATETIME DEFAULT NULL COMMENT 'created at',
update_time DATETIME DEFAULT NULL COMMENT 'updated at',
PRIMARY KEY (zone_id),
UNIQUE KEY uk_zone_region_code (region_id, zone_code),
INDEX idx_zone_region_id (region_id),
INDEX idx_zone_country_code (country_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='zone under bms_region';
CREATE TABLE IF NOT EXISTS prc_zone_detail (
detail_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'detail id',
zone_id BIGINT(20) NOT NULL COMMENT 'zone id',
postcode VARCHAR(32) NOT NULL COMMENT 'postcode',
create_time DATETIME DEFAULT NULL COMMENT 'created at',
update_time DATETIME DEFAULT NULL COMMENT 'updated at',
PRIMARY KEY (detail_id),
INDEX idx_zd_zone_id (zone_id),
INDEX idx_zd_postcode (postcode),
UNIQUE KEY uk_zd_zone_postcode (zone_id, postcode),
CONSTRAINT fk_zd_zone FOREIGN KEY (zone_id) REFERENCES prc_zone (zone_id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='zone postcode detail';
@@ -0,0 +1,16 @@
-- Add country_code to existing prc_zone (skip if re-running full zone_country.sql)
-- charset: utf8mb4
ALTER TABLE prc_zone
ADD COLUMN country_code VARCHAR(10) NULL COMMENT 'ISO2 country code from bms_region' AFTER region_id;
UPDATE prc_zone z
INNER JOIN bms_region r ON r.region_id = z.region_id
SET z.country_code = r.country_code_2
WHERE z.country_code IS NULL OR z.country_code = '';
ALTER TABLE prc_zone
MODIFY COLUMN country_code VARCHAR(10) NOT NULL COMMENT 'ISO2 country code from bms_region';
ALTER TABLE prc_zone
ADD INDEX idx_zone_country_code (country_code);
+2 -44
View File
@@ -4,50 +4,8 @@
-- menu_id: 2100-2123 (2115-2123 reserved)
-- ----------------------------
CREATE TABLE IF NOT EXISTS xtd_zone_scheme (
zs_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'zone scheme id',
zs_name VARCHAR(100) NOT NULL COMMENT 'scheme name',
ss_code VARCHAR(20) NOT NULL DEFAULT 'on' COMMENT 'status on/off',
pg_code VARCHAR(50) DEFAULT NULL COMMENT 'product group code',
sn_id BIGINT(20) DEFAULT NULL COMMENT 'service channel id',
zs_note VARCHAR(500) DEFAULT NULL COMMENT 'note',
tms_id BIGINT(20) DEFAULT NULL COMMENT 'tenant id',
created_by VARCHAR(64) DEFAULT '' COMMENT 'created by',
create_time DATETIME DEFAULT NULL COMMENT 'created at',
updated_by VARCHAR(64) DEFAULT '' COMMENT 'updated by',
update_time DATETIME DEFAULT NULL COMMENT 'updated at',
PRIMARY KEY (zs_id),
INDEX idx_zs_ss_code (ss_code),
INDEX idx_zs_tms_id (tms_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='zone scheme';
CREATE TABLE IF NOT EXISTS xtd_zone_division (
zd_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'division id globally unique',
zs_id BIGINT(20) NOT NULL COMMENT 'scheme id',
zd_sort INT(11) DEFAULT 0 COMMENT 'sort',
zd_code VARCHAR(50) DEFAULT NULL COMMENT 'division code',
zn_name VARCHAR(100) DEFAULT NULL COMMENT 'name',
zn_ename VARCHAR(100) DEFAULT NULL COMMENT 'english name',
zn_note VARCHAR(500) DEFAULT NULL COMMENT 'note',
create_time DATETIME DEFAULT NULL COMMENT 'created at',
update_time DATETIME DEFAULT NULL COMMENT 'updated at',
PRIMARY KEY (zd_id),
INDEX idx_zd_zs_id (zs_id),
CONSTRAINT fk_zd_zs FOREIGN KEY (zs_id) REFERENCES xtd_zone_scheme (zs_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='zone division';
CREATE TABLE IF NOT EXISTS xtd_zonecountry (
zc_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'zone country id',
zd_id BIGINT(20) NOT NULL COMMENT 'division id',
country_code CHAR(2) NOT NULL COMMENT 'ISO2 country code',
zc_dividekind VARCHAR(10) NOT NULL DEFAULT 'C' COMMENT 'divide kind default C',
create_time DATETIME DEFAULT NULL COMMENT 'created at',
update_time DATETIME DEFAULT NULL COMMENT 'updated at',
PRIMARY KEY (zc_id),
INDEX idx_zc_zd_id (zd_id),
INDEX idx_zc_country (country_code),
CONSTRAINT fk_zc_zd FOREIGN KEY (zd_id) REFERENCES xtd_zone_division (zd_id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='zone country';
-- Zone tables moved to zone_country.sql (prc_zone_country / prc_zone / prc_zone_detail)
-- Run: bms-back/sql/price/zone_country.sql
CREATE TABLE IF NOT EXISTS prc_weight_section_template (
wst_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'weight template id',
+73 -94
View File
@@ -1,90 +1,7 @@
import request from '@/utils/request'
// ========== 分区方案 ==========
// ========== countries that have zones (from bms_region) ==========
/** 查询分区方案列表 */
export function listScheme(query) {
return request({
url: '/price/zone/scheme/list',
method: 'get',
params: query
})
}
/** 查询分区方案详细 */
export function getScheme(zsId) {
return request({
url: '/price/zone/scheme/' + zsId,
method: 'get'
})
}
/** 新增分区方案 */
export function addScheme(data) {
return request({
url: '/price/zone/scheme',
method: 'post',
data: data
})
}
/** 修改分区方案 */
export function updateScheme(data) {
return request({
url: '/price/zone/scheme',
method: 'put',
data: data
})
}
/** 删除分区方案 */
export function delScheme(zsIds) {
return request({
url: '/price/zone/scheme/' + zsIds,
method: 'delete'
})
}
// ========== 区划 ==========
/** 查询区划列表(按 zsId) */
export function listDivision(query) {
return request({
url: '/price/zone/division/list',
method: 'get',
params: query
})
}
/** 新增区划 */
export function addDivision(data) {
return request({
url: '/price/zone/division',
method: 'post',
data: data
})
}
/** 修改区划 */
export function updateDivision(data) {
return request({
url: '/price/zone/division',
method: 'put',
data: data
})
}
/** 删除区划 */
export function delDivision(zdIds) {
return request({
url: '/price/zone/division/' + zdIds,
method: 'delete'
})
}
// ========== 国家 ==========
/** 查询国家列表(按 zdId) */
export function listCountry(query) {
return request({
url: '/price/zone/country/list',
@@ -93,28 +10,90 @@ export function listCountry(query) {
})
}
/** 新增国家 */
export function addCountry(data) {
export function getCountry(regionId) {
return request({
url: '/price/zone/country',
url: '/price/zone/country/' + regionId,
method: 'get'
})
}
/** Delete all zones + postcodes for region(s); does not delete bms_region */
export function delCountryZones(regionIds) {
return request({
url: '/price/zone/country/' + regionIds,
method: 'delete'
})
}
// ========== zone item ==========
export function listZone(query) {
return request({
url: '/price/zone/item/list',
method: 'get',
params: query
})
}
export function getZone(zoneId) {
return request({
url: '/price/zone/item/' + zoneId,
method: 'get'
})
}
export function addZone(data) {
return request({
url: '/price/zone/item',
method: 'post',
data: data
})
}
/** 修改国家 */
export function updateCountry(data) {
export function updateZone(data) {
return request({
url: '/price/zone/country',
url: '/price/zone/item',
method: 'put',
data: data
})
}
/** 删除国家 */
export function delCountry(zcIds) {
export function delZone(zoneIds) {
return request({
url: '/price/zone/country/' + zcIds,
url: '/price/zone/item/' + zoneIds,
method: 'delete'
})
}
}
// ========== detail ==========
export function listDetail(query) {
return request({
url: '/price/zone/detail/list',
method: 'get',
params: query
})
}
export function addDetail(data) {
return request({
url: '/price/zone/detail',
method: 'post',
data: data
})
}
export function updateDetail(data) {
return request({
url: '/price/zone/detail',
method: 'put',
data: data
})
}
export function delDetail(detailIds) {
return request({
url: '/price/zone/detail/' + detailIds,
method: 'delete'
})
}
@@ -1,7 +1,7 @@
<template>
<el-drawer v-model="visible" size="920px" :title="title" append-to-body>
<el-drawer v-model="visible" size="80%" :title="title" append-to-body>
<div class="mb8" style="display: flex; align-items: center; gap: 12px">
<el-input v-model="keyword" placeholder="搜索国家(中文名 / ISO2)" clearable style="width: 260px" />
<el-input v-model="keyword" placeholder="搜索国家/分区(中文名 / ISO2 / 分区)" clearable style="width: 260px" />
</div>
<simple-table
:data="filteredList"
@@ -12,6 +12,9 @@
empty-text="暂无国家明细"
:span-method="spanMethod"
>
<template #col-countryName="{ row }">
<span class="pre-wrap">{{ row.countryName }} ({{ row.countryCode }})</span>
</template>
<template #col-sizeLimit="{ row }">
<span class="pre-wrap">{{ row.sizeLimit }}</span>
</template>
@@ -28,8 +31,9 @@ import { listProductCountries } from '@/api/price/product'
import { formatWeightLabel } from '@/views/price/set/utils/priceSet'
const columns = [
{ prop: 'countryName', label: '国家', width: 90, showOverflowTooltip: true },
{ prop: 'countryCode', label: 'ISO2', width: 60, align: 'center' },
{ prop: 'countryName', label: '国家', width: 120, showOverflowTooltip: true },
// { prop: 'countryCode', label: 'ISO2', width: 60, align: 'center' },
{ prop: 'zoneName', label: '分区名称', width: 140, showOverflowTooltip: true, formatter: (row) => row.zoneName || '-' },
{
prop: 'weightMin',
label: '重量段(KG)',
@@ -61,7 +65,9 @@ const filteredList = computed(() => {
const st = status.value
return list.value.filter(r => {
if (kw && !(String(r.countryName || '').toLowerCase().includes(kw) ||
String(r.countryCode || '').toLowerCase().includes(kw))) return false
String(r.countryCode || '').toLowerCase().includes(kw) ||
String(r.zoneCode || '').toLowerCase().includes(kw) ||
String(r.zoneName || '').toLowerCase().includes(kw))) return false
if (st && String(r.status) !== String(st)) return false
return true
})
@@ -81,23 +87,29 @@ function open(row) {
status.value = ''
listProductCountries(row.productCode, formatEffDate(row.effectiveDate), row.productId).then(res => {
list.value = (res.data || []).slice().sort((a, b) => {
return String(a.countryCode || '').localeCompare(String(b.countryCode || ''))
const c = String(a.countryCode || '').localeCompare(String(b.countryCode || ''))
if (c !== 0) return c
return String(a.zoneCode || '').localeCompare(String(b.zoneCode || ''))
})
}).finally(() => {
loading.value = false
})
}
function rowGroupKey(row) {
return String(row?.countryCode || '') + '|' + String(row?.zoneCode || '') + '|' + String(row?.countryName || '')
}
function spanMethod({ column, rowIndex }) {
if (column.property !== 'countryName' && column.property !== 'countryCode') return
if (!['countryName', 'countryCode', 'zoneCode', 'zoneName', 'zoneId'].includes(column.property)) return
const rows = filteredList.value
if (!rows.length) return
const cur = rows[rowIndex]?.countryCode
if (rowIndex > 0 && rows[rowIndex - 1]?.countryCode === cur) {
const cur = rowGroupKey(rows[rowIndex])
if (rowIndex > 0 && rowGroupKey(rows[rowIndex - 1]) === cur) {
return { rowspan: 0, colspan: 0 }
}
let count = 1
while (rowIndex + count < rows.length && rows[rowIndex + count]?.countryCode === cur) count++
while (rowIndex + count < rows.length && rowGroupKey(rows[rowIndex + count]) === cur) count++
return { rowspan: count, colspan: 1 }
}
@@ -0,0 +1,135 @@
<template>
<el-drawer v-model="visible" size="720px" append-to-body>
<template #header>
<div class="drawer-head">
<span class="drawer-title">邮编明细</span>
<span v-if="selectedZone" class="drawer-sub">
{{ selectedZone.zoneName || selectedZone.zoneCode }}
({{ detailTotal }}条)
</span>
</div>
</template>
<div class="drawer-toolbar">
<el-input
v-model="query.postcode"
clearable
placeholder="搜索邮编"
style="width: 200px; margin-right: 8px"
@keyup.enter="loadList"
/>
<el-button type="primary" plain icon="Search" size="small" @click="loadList">搜索</el-button>
<el-button type="primary" plain icon="Plus" size="small" @click="handleAdd" v-hasPermi="['price:zone:add']">新增邮编</el-button>
</div>
<el-table v-loading="loading" :data="list" border row-key="detailId" empty-text="暂无邮编">
<el-table-column label="ID" prop="detailId" align="center" width="90" />
<el-table-column label="邮编" prop="postcode" align="center" min-width="140" />
<el-table-column label="操作" align="center" width="160">
<template #default="{ row }">
<el-button link type="primary" icon="Edit" @click="handleUpdate(row)" v-hasPermi="['price:zone:edit']">编辑</el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(row.detailId)" v-hasPermi="['price:zone:remove']">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="drawer-pager">
<el-pagination
v-model:current-page="query.pageNum"
v-model:page-size="query.pageSize"
:total="detailTotal"
:page-sizes="[20, 50, 100, 200]"
layout="total, sizes, prev, pager, next"
@size-change="loadList"
@current-change="loadList"
/>
</div>
<postcode-form-dialog ref="postcodeFormRef" @success="onChanged" />
</el-drawer>
</template>
<script setup name="PostcodeDrawer">
import { listDetail, delDetail } from '@/api/price/zone'
import PostcodeFormDialog from './PostcodeFormDialog.vue'
const { proxy } = getCurrentInstance()
const emit = defineEmits(['changed'])
const visible = ref(false)
const selectedZone = ref(null)
const list = ref([])
const loading = ref(false)
const detailTotal = ref(0)
const query = ref({ pageNum: 1, pageSize: 50, postcode: undefined })
const postcodeFormRef = ref()
function open(zone) {
selectedZone.value = zone
query.value = { pageNum: 1, pageSize: 50, postcode: undefined }
visible.value = true
loadList()
}
function loadList() {
if (!selectedZone.value?.zoneId) {
list.value = []
detailTotal.value = 0
return
}
loading.value = true
listDetail({
zoneId: selectedZone.value.zoneId,
postcode: query.value.postcode || undefined,
pageNum: query.value.pageNum,
pageSize: query.value.pageSize
}).then(res => {
list.value = res.rows || []
detailTotal.value = res.total || 0
}).catch(() => {
list.value = []
detailTotal.value = 0
}).finally(() => {
loading.value = false
})
}
function handleAdd() {
postcodeFormRef.value.open(selectedZone.value)
}
function handleUpdate(row) {
postcodeFormRef.value.open(selectedZone.value, row)
}
/** 邮编增删改后:刷新本列表,并向上通知(分区表需更新邮编数) */
function onChanged() {
loadList()
emit('changed')
}
function handleDelete(detailIds) {
proxy.$modal.confirm('是否确认删除邮编编号为"' + detailIds + '"的数据?').then(() => {
return delDetail(detailIds)
}).then(() => {
proxy.$modal.msgSuccess('删除成功')
onChanged()
}).catch(() => {})
}
defineExpose({ open })
</script>
<style scoped>
.drawer-head {
display: flex;
align-items: center;
gap: 10px;
}
.drawer-title { font-size: 16px; font-weight: 600; }
.drawer-sub { color: #909399; font-size: 13px; }
.drawer-toolbar { margin-bottom: 12px; display: flex; align-items: center; flex-wrap: wrap; gap: 4px; }
.drawer-pager { margin-top: 16px; display: flex; justify-content: flex-end; }
</style>
@@ -0,0 +1,63 @@
<template>
<el-dialog :title="title" v-model="visible" width="420px" append-to-body>
<el-form ref="postcodeFormRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="邮编" prop="postcode">
<el-input v-model="form.postcode" placeholder="请输入邮编" maxlength="32" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="visible = false">取 消</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup name="PostcodeFormDialog">
import { addDetail, updateDetail } from '@/api/price/zone'
const { proxy } = getCurrentInstance()
const emit = defineEmits(['success'])
const visible = ref(false)
const title = ref('')
const postcodeFormRef = ref()
const form = ref({})
const rules = {
postcode: [{ required: true, message: '邮编不能为空', trigger: 'blur' }]
}
/** zone 为所属分区;row 传空为新增,传邮编行为编辑 */
function open(zone, row) {
form.value = row
? { ...row }
: {
detailId: undefined,
zoneId: zone?.zoneId,
postcode: undefined
}
title.value = row ? '编辑邮编' : '新增邮编'
visible.value = true
nextTick(() => postcodeFormRef.value?.clearValidate())
}
function submitForm() {
postcodeFormRef.value.validate(valid => {
if (!valid) return
const payload = {
...form.value,
postcode: String(form.value.postcode || '').trim()
}
const req = payload.detailId != null ? updateDetail(payload) : addDetail(payload)
req.then(() => {
proxy.$modal.msgSuccess(payload.detailId != null ? '修改成功' : '新增成功')
visible.value = false
emit('success')
})
})
}
defineExpose({ open })
</script>
@@ -0,0 +1,222 @@
<template>
<el-drawer v-model="visible" size="860px" append-to-body>
<template #header>
<div class="drawer-head">
<span class="drawer-title">国家分区</span>
<el-select
v-model="selectedRegionId"
filterable
clearable
placeholder="选择国家"
class="country-select"
@change="loadZoneList"
>
<el-option
v-for="r in regionOptions"
:key="r.regionId"
:label="regionLabel(r)"
:value="r.regionId"
/>
</el-select>
<span v-if="selectedCountry" class="drawer-sub">
当前:{{ selectedCountry.countryNameCn }}
<template v-if="selectedCountry.countryCode2">({{ selectedCountry.countryCode2 }})</template>
,共 {{ zoneList.length }} 个分区
</span>
</div>
</template>
<div v-loading="zoneLoading">
<div class="zone-toolbar">
<el-button
type="primary"
plain
icon="Plus"
size="small"
:disabled="!selectedCountry"
@click="handleZoneAdd"
v-hasPermi="['price:zone:add']"
>新增分区</el-button>
<el-button
type="danger"
plain
icon="Delete"
size="small"
:disabled="!selectedCountry"
@click="handleClearZones"
v-hasPermi="['price:zone:remove']"
>清空分区</el-button>
</div>
<el-table
:data="zoneList"
border
highlight-current-row
row-key="zoneId"
:empty-text="selectedCountry ? '该国家暂无分区,点击「新增分区」创建' : '请先选择国家'"
>
<el-table-column label="国家" align="center" min-width="180">
<template #default="{ row }">
<div class="zone-country-cell">
<div class="zone-title">{{ row.zoneName || ((selectedCountry?.countryNameCn || '') + '(' + row.zoneCode + ')') }}</div>
<el-button link type="primary" class="zone-click" @click="openPostcodeDrawer(row)" v-hasPermi="['price:zone:query']">分区点击</el-button>
</div>
</template>
</el-table-column>
<el-table-column label="国家编码" prop="countryCode" align="center" width="110" />
<el-table-column label="分区代码" prop="zoneCode" align="center" width="120" />
<el-table-column label="邮编数" prop="detailCount" align="center" width="100" />
<el-table-column label="排序" prop="zoneSort" align="center" width="80" />
<el-table-column label="操作" align="center" width="220" fixed="right">
<template #default="{ row }">
<el-button link type="primary" icon="View" @click="openPostcodeDrawer(row)" v-hasPermi="['price:zone:query']">明细</el-button>
<el-button link type="primary" icon="Edit" @click="handleZoneUpdate(row)" v-hasPermi="['price:zone:edit']">编辑</el-button>
<el-button link type="primary" icon="Delete" @click="handleZoneDelete(row.zoneId)" v-hasPermi="['price:zone:remove']">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<zone-form-dialog ref="zoneFormRef" :country="selectedCountry" @success="onZoneChanged" />
<postcode-drawer ref="postcodeDrawerRef" @changed="onZoneChanged" />
</el-drawer>
</template>
<script setup name="ZoneDrawer">
import { listRegionOptions } from '@/api/data/region'
import {
delCountryZones,
listZone, addZone, updateZone, delZone
} from '@/api/price/zone'
import ZoneFormDialog from './ZoneFormDialog.vue'
import PostcodeDrawer from './PostcodeDrawer.vue'
const { proxy } = getCurrentInstance()
const emit = defineEmits(['changed'])
const visible = ref(false)
const selectedRegionId = ref(null)
const selectedCountry = computed(() =>
regionOptions.value.find(r => r.regionId === selectedRegionId.value) || null
)
const zoneList = ref([])
const zoneLoading = ref(false)
const regionOptions = ref([])
const zoneFormRef = ref()
const postcodeDrawerRef = ref()
function regionLabel(r) {
const cn = r.countryNameCn || r.countryNameEn || ''
return cn ? `${r.flagEmoji || ''} ${cn} (${r.countryCode2})`.trim() : r.countryCode2
}
function ensureRegions() {
if (regionOptions.value.length) return Promise.resolve()
return listRegionOptions().then(res => {
regionOptions.value = res.data || []
})
}
/** 打开抽屉;传入国家行则自动选中,不传则让用户在头部下拉选择 */
function open(country) {
visible.value = true
ensureRegions().then(() => {
selectedRegionId.value = country?.regionId || null
loadZoneList(selectedRegionId.value)
})
}
function reload() {
loadZoneList(selectedRegionId.value)
}
function loadZoneList(regionId) {
if (!regionId) {
zoneList.value = []
return
}
zoneLoading.value = true
listZone({ regionId, pageNum: 1, pageSize: 200 }).then(res => {
zoneList.value = res.rows || []
}).catch(() => {
zoneList.value = []
}).finally(() => {
zoneLoading.value = false
})
}
function handleZoneAdd() {
zoneFormRef.value.open()
}
function handleZoneUpdate(row) {
zoneFormRef.value.open(row)
}
/** 分区增删改 / 邮编变动后:刷新分区列表,并通知父页刷新国家表格统计 */
function onZoneChanged() {
loadZoneList(selectedRegionId.value)
emit('changed')
}
function handleZoneDelete(zoneIds) {
proxy.$modal.confirm('是否确认删除分区编号为"' + zoneIds + '"的数据?将同时删除邮编明细。').then(() => {
return delZone(zoneIds)
}).then(() => {
proxy.$modal.msgSuccess('删除成功')
onZoneChanged()
}).catch(() => {})
}
function handleClearZones() {
if (!selectedRegionId.value) return
proxy.$modal.confirm('是否清空国家 regionId="' + selectedRegionId.value + '" 下的全部分区与邮编?(不会删除 bms_region 国家主数据)').then(() => {
return delCountryZones(selectedRegionId.value)
}).then(() => {
zoneList.value = []
proxy.$modal.msgSuccess('清空成功')
emit('changed')
}).catch(() => {})
}
/** 抽屉打开期间,外部清空了某国家(国家表格「清空」按钮)时同步列表 */
function onCountryCleared(regionIds) {
if (visible.value && String(regionIds).split(',').map(String).includes(String(selectedRegionId.value))) {
zoneList.value = []
}
}
function openPostcodeDrawer(row) {
postcodeDrawerRef.value.open(row)
}
defineExpose({ open, reload, onCountryCleared })
</script>
<style scoped>
.drawer-head {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.drawer-title { font-size: 16px; font-weight: 600; }
.drawer-sub { color: #909399; font-size: 13px; }
.country-select { width: 260px; }
.zone-toolbar {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.zone-country-cell {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
padding: 4px 0;
}
.zone-title { font-weight: 500; }
.zone-click { padding: 0; height: auto; font-size: 12px; }
</style>
@@ -0,0 +1,100 @@
<template>
<el-dialog :title="title" v-model="open" width="520px" append-to-body>
<el-form ref="zoneFormRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="国家">
<span>{{ country.countryNameCn }} ({{ country.countryCode2 }})</span>
</el-form-item>
<el-form-item label="国家编码" prop="countryCode">
<el-input v-model="form.countryCode" placeholder="ISO2,如 AU" maxlength="10" style="text-transform: uppercase" />
</el-form-item>
<el-form-item label="分区代码" prop="zoneCode">
<el-input v-model="form.zoneCode" placeholder="如 1区" maxlength="50" />
</el-form-item>
<el-form-item label="显示名" prop="zoneName">
<el-input v-model="form.zoneName" placeholder="如 澳大利亚(1区)" maxlength="100" />
</el-form-item>
<el-form-item label="排序号" prop="zoneSort">
<el-input-number v-model="form.zoneSort" :min="0" :max="9999" controls-position="right" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="备注" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="open = false">取 消</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup name="ZoneFormDialog">
import { addZone, updateZone } from '@/api/price/zone'
const { proxy } = getCurrentInstance()
const props = defineProps({
/** 当前抽屉选中的国家(bms_region 行) */
country: {
type: Object,
default: null
}
})
const emit = defineEmits(['success'])
const open = ref(false)
const title = ref('')
const zoneFormRef = ref()
const form = ref({})
const rules = {
countryCode: [{ required: true, message: '国家编码不能为空', trigger: 'blur' }],
zoneCode: [{ required: true, message: '分区代码不能为空', trigger: 'blur' }]
}
/** row 传空为新增,传分区行为编辑 */
function open2(row) {
form.value = row
? { ...row }
: {
zoneId: undefined,
regionId: props.country?.regionId,
countryCode: props.country?.countryCode2 || undefined,
zoneCode: undefined,
zoneName: undefined,
zoneSort: 0,
remark: undefined
}
title.value = row ? '编辑分区' : '新增分区'
open.value = true
nextTick(() => zoneFormRef.value?.clearValidate())
}
function submitForm() {
zoneFormRef.value.validate(valid => {
if (!valid) return
const region = props.country
const cn = region?.countryNameCn || ''
const code = (form.value.countryCode || region?.countryCode2 || '')
.toString().trim().toUpperCase()
const payload = {
...form.value,
regionId: region?.regionId,
countryCode: code,
zoneName: form.value.zoneName || (cn ? (cn + '(' + form.value.zoneCode + ')') : form.value.zoneCode)
}
delete payload.detailCount
delete payload.countryNameCn
delete payload.countryCode2
const req = payload.zoneId != null ? updateZone(payload) : addZone(payload)
req.then(() => {
proxy.$modal.msgSuccess(payload.zoneId != null ? '修改成功' : '新增成功')
open.value = false
emit('success')
})
})
}
defineExpose({ open: open2 })
</script>
+68 -498
View File
@@ -1,548 +1,118 @@
<template>
<div class="app-container">
<!-- 分区方案 -->
<el-alert
type="info"
:closable="false"
show-icon
class="mb16"
title="国家来自 bms_region;点击「分区」在抽屉中查看/维护国家分区,抽屉内点击「分区点击」查看邮编明细。导入 Excel 时需 sheet 名以「分区」结尾且能匹配国家表。"
/>
<basic-table
ref="schemeTableRef"
:columns="schemeColumns"
:search-columns="schemeSearchColumns"
:query="querySchemeList"
row-key="zsId"
@row-click="handleSchemeRowClick"
ref="countryTableRef"
:columns="countryColumns"
:search-columns="countrySearchColumns"
:query="queryCountryList"
row-key="regionId"
>
<template #toolbar="{ selection, ids }">
<template #toolbar>
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="Plus"
@click="handleSchemeAdd"
v-hasPermi="['price:zone:add']"
>新建分区方案</el-button>
<el-button type="primary" plain icon="Plus" @click="handleAddZone" v-hasPermi="['price:zone:add']">为国家新增分区</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="Edit"
:disabled="selection.length !== 1"
@click="handleSchemeUpdate(selection[0])"
v-hasPermi="['price:zone:edit']"
>修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="Delete"
:disabled="!selection.length"
@click="handleSchemeDelete(ids)"
v-hasPermi="['price:zone:remove']"
>删除</el-button>
<el-button type="info" plain icon="Upload" @click="handleImport" v-hasPermi="['price:zone:add']">导入 Excel</el-button>
</el-col>
</template>
<template #col-ssCode="{ row }">
<el-tag :type="row.ssCode === 'on' ? 'success' : 'info'" size="small">
{{ row.ssCode === 'on' ? 'on' : 'of' }}
<template #col-status="{ row }">
<el-tag :type="String(row.status) === '0' ? 'success' : 'info'" size="small">
{{ String(row.status) === '0' ? '正常' : '停用' }}
</el-tag>
</template>
<template #col-action="{ row }">
<el-button link type="primary" icon="Edit" @click.stop="handleSchemeUpdate(row)" v-hasPermi="['price:zone:edit']">编辑</el-button>
<el-button link type="primary" icon="Delete" @click.stop="handleSchemeDelete(row.zsId)" v-hasPermi="['price:zone:remove']">删除</el-button>
<el-button link type="primary" icon="List" @click.stop="openZoneDrawer(row)" v-hasPermi="['price:zone:query']">分区</el-button>
<el-button link type="primary" icon="Delete" @click.stop="handleCountryDelete(row.regionId)" v-hasPermi="['price:zone:remove']">清空</el-button>
</template>
</basic-table>
<!-- 区划明细 -->
<el-card shadow="never" class="detail-card" v-loading="divisionLoading">
<template #header>
<div class="card-header">
<span>
区划明细
<span v-if="selectedScheme" class="sub-hint">当前方案:{{ selectedScheme.zsName }}(zsId={{ selectedScheme.zsId }})</span>
<span v-else class="sub-hint">请点击上方方案行查看区划</span>
</span>
<el-button
type="primary"
plain
icon="Plus"
size="small"
:disabled="!selectedScheme"
@click="handleDivisionAdd"
v-hasPermi="['price:zone:add']"
>新增区划</el-button>
</div>
</template>
<zone-drawer ref="zoneDrawerRef" @changed="onZoneChanged" />
<el-table
:data="divisionList"
border
highlight-current-row
row-key="zdId"
empty-text="暂无区划数据"
>
<el-table-column label="zdId" prop="zdId" align="center" width="80" />
<el-table-column label="区划代码" prop="zdCode" align="center" min-width="120" show-overflow-tooltip />
<el-table-column label="中文名" prop="znName" align="center" min-width="120" show-overflow-tooltip />
<el-table-column label="英文名" prop="znEname" align="center" min-width="140" show-overflow-tooltip />
<el-table-column label="国家数" prop="countryCount" align="center" width="90" />
<el-table-column label="操作" align="center" width="240" fixed="right">
<template #default="{ row }">
<el-button link type="primary" icon="View" @click="openCountryDrawer(row)" v-hasPermi="['price:zone:query']">查看国家</el-button>
<el-button link type="primary" icon="Edit" @click="handleDivisionUpdate(row)" v-hasPermi="['price:zone:edit']">编辑</el-button>
<el-button link type="primary" icon="Delete" @click="handleDivisionDelete(row.zdId)" v-hasPermi="['price:zone:remove']">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 方案表单 -->
<el-dialog :title="schemeTitle" v-model="schemeOpen" width="560px" append-to-body>
<el-form ref="schemeFormRef" :model="schemeForm" :rules="schemeRules" label-width="100px">
<el-form-item label="方案名称" prop="zsName">
<el-input v-model="schemeForm.zsName" placeholder="请输入分区方案名称" maxlength="50" />
</el-form-item>
<el-form-item label="状态" prop="ssCode">
<el-radio-group v-model="schemeForm.ssCode">
<el-radio value="on">有效(on)</el-radio>
<el-radio value="of">停用(of)</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="产品组" prop="pgCode">
<el-input v-model="schemeForm.pgCode" placeholder="如 EXP / EMS / LINE" maxlength="10" />
</el-form-item>
<el-form-item label="渠道ID" prop="snId">
<el-input v-model="schemeForm.snId" placeholder="请输入渠道 snId" />
</el-form-item>
<el-form-item label="备注" prop="zsNote">
<el-input v-model="schemeForm.zsNote" type="textarea" placeholder="请输入备注" :rows="3" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitSchemeForm">确 定</el-button>
<el-button @click="schemeOpen = false">取 消</el-button>
</div>
</template>
</el-dialog>
<!-- 区划表单 -->
<el-dialog :title="divisionTitle" v-model="divisionOpen" width="560px" append-to-body>
<el-form ref="divisionFormRef" :model="divisionForm" :rules="divisionRules" label-width="100px">
<el-form-item label="区划代码" prop="zdCode">
<el-input v-model="divisionForm.zdCode" placeholder="如 DHL-ASIA" maxlength="50" />
</el-form-item>
<el-form-item label="中文名" prop="znName">
<el-input v-model="divisionForm.znName" placeholder="请输入中文名" maxlength="50" />
</el-form-item>
<el-form-item label="英文名" prop="znEname">
<el-input v-model="divisionForm.znEname" placeholder="请输入英文名" maxlength="50" />
</el-form-item>
<el-form-item label="排序号" prop="zdSort">
<el-input v-model="divisionForm.zdSort" placeholder="显示排序号" maxlength="3" />
</el-form-item>
<el-form-item label="备注" prop="znNote">
<el-input v-model="divisionForm.znNote" type="textarea" placeholder="请输入备注" :rows="3" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitDivisionForm">确 定</el-button>
<el-button @click="divisionOpen = false">取 消</el-button>
</div>
</template>
</el-dialog>
<!-- 国家抽屉 -->
<el-drawer v-model="countryDrawerVisible" size="640px" append-to-body>
<template #header>
<div class="drawer-head">
<span class="drawer-title">区划国家</span>
<span v-if="selectedDivision" class="drawer-sub">
{{ selectedDivision.zdCode }} {{ selectedDivision.znName }}
</span>
</div>
</template>
<div class="drawer-toolbar">
<el-button
type="primary"
plain
icon="Plus"
size="small"
@click="handleCountryAdd"
v-hasPermi="['price:zone:add']"
>新增国家</el-button>
</div>
<el-table v-loading="countryLoading" :data="countryList" border row-key="zcId" empty-text="暂无国家">
<el-table-column label="zcId" prop="zcId" align="center" width="80" />
<el-table-column label="国家代码" prop="countryCode" align="center" width="100" />
<el-table-column label="划分类型" prop="zcDividekind" align="center" width="120">
<template #default="{ row }">
<el-tag size="small" :type="divideKindTag(row.zcDividekind)">
{{ divideKindLabel(row.zcDividekind) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="160">
<template #default="{ row }">
<el-button link type="primary" icon="Edit" @click="handleCountryUpdate(row)" v-hasPermi="['price:zone:edit']">编辑</el-button>
<el-button link type="primary" icon="Delete" @click="handleCountryDelete(row.zcId)" v-hasPermi="['price:zone:remove']">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-drawer>
<!-- 国家表单 -->
<el-dialog :title="countryTitle" v-model="countryOpen" width="480px" append-to-body>
<el-form ref="countryFormRef" :model="countryForm" :rules="countryRules" label-width="100px">
<el-form-item label="国家代码" prop="countryCode">
<el-input v-model="countryForm.countryCode" placeholder="ISO两位码,如 DE" maxlength="2" style="text-transform: uppercase" />
</el-form-item>
<el-form-item label="划分类型" prop="zcDividekind">
<el-radio-group v-model="countryForm.zcDividekind">
<el-radio value="C">国家(C)</el-radio>
<el-radio value="T">城市(T)</el-radio>
<el-radio value="P">邮编(P)</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitCountryForm">确 定</el-button>
<el-button @click="countryOpen = false">取 消</el-button>
</div>
</template>
</el-dialog>
<excel-import-dialog
ref="importRef"
title="分区 Excel 导入"
action="/price/zone/importData"
width="560px"
:show-update-support="false"
@success="onImportSuccess"
/>
</div>
</template>
<script setup name="PriceZone">
import {
listScheme, getScheme, addScheme, updateScheme, delScheme,
listDivision, addDivision, updateDivision, delDivision,
listCountry, addCountry, updateCountry, delCountry
} from '@/api/price/zone'
import ExcelImportDialog from '@/components/ExcelImportDialog'
import { listCountry, delCountryZones } from '@/api/price/zone'
import ZoneDrawer from './components/ZoneDrawer.vue'
const { proxy } = getCurrentInstance()
const schemeTableRef = ref()
const selectedScheme = ref(null)
const divisionList = ref([])
const divisionLoading = ref(false)
const countryTableRef = ref()
const zoneDrawerRef = ref()
const importRef = ref()
const schemeOpen = ref(false)
const schemeTitle = ref('')
const schemeFormRef = ref()
const schemeForm = ref({})
const schemeRules = {
zsName: [{ required: true, message: '方案名称不能为空', trigger: 'blur' }],
ssCode: [{ required: true, message: '状态不能为空', trigger: 'change' }]
}
const divisionOpen = ref(false)
const divisionTitle = ref('')
const divisionFormRef = ref()
const divisionForm = ref({})
const divisionRules = {
zdCode: [{ required: true, message: '区划代码不能为空', trigger: 'blur' }],
znName: [{ required: true, message: '中文名不能为空', trigger: 'blur' }]
}
const countryDrawerVisible = ref(false)
const selectedDivision = ref(null)
const countryList = ref([])
const countryLoading = ref(false)
const countryOpen = ref(false)
const countryTitle = ref('')
const countryFormRef = ref()
const countryForm = ref({})
const countryRules = {
countryCode: [
{ required: true, message: '国家代码不能为空', trigger: 'blur' },
{ min: 2, max: 2, message: '国家代码须为2位', trigger: 'blur' }
],
zcDividekind: [{ required: true, message: '划分类型不能为空', trigger: 'change' }]
}
const schemeSearchColumns = [
{ label: '方案名称', prop: 'zsName', component: 'input', placeholder: '请输入方案名称', width: '220px' },
{
label: '状态',
prop: 'ssCode',
component: 'select',
options: [
{ label: '有效(on)', value: 'on' },
{ label: '停用(of)', value: 'of' }
],
placeholder: '状态',
width: '160px'
}
const countrySearchColumns = [
{ label: '国家名称', prop: 'countryNameCn', component: 'input', placeholder: '请输入国家名称', width: '220px' },
{ label: '国家代码', prop: 'countryCode2', component: 'input', placeholder: 'AU', width: '140px' }
]
const schemeColumns = [
{ label: 'zsId', prop: 'zsId', align: 'center', width: 80 },
{ label: '方案名称', prop: 'zsName', align: 'center', minWidth: 140 },
{ label: '状态', prop: 'ssCode', align: 'center', width: 90 },
{ label: '产品组', prop: 'pgCode', align: 'center', width: 100 },
{ label: '渠道', prop: 'snId', align: 'center', width: 100 },
{ label: '区划数', prop: 'divisionCount', align: 'center', width: 90 },
const countryColumns = [
{ label: 'ID', prop: 'regionId', align: 'center', width: 80 },
{ label: '国家', prop: 'countryNameCn', align: 'center', minWidth: 140 },
{ label: '代码', prop: 'countryCode2', align: 'center', width: 90 },
{ label: '状态', prop: 'status', align: 'center', width: 90 },
{ label: '分区数', prop: 'zoneCount', align: 'center', width: 90 },
{ label: '邮编数', prop: 'detailCount', align: 'center', width: 100 },
{ label: '操作', prop: 'action', align: 'center', width: 160, showInToolbar: false }
]
function querySchemeList(params) {
return listScheme(params)
function queryCountryList(params) {
return listCountry(params)
}
function handleSchemeRowClick(row) {
if (!row?.zsId) return
selectedScheme.value = row
loadDivisionList(row.zsId)
/** 工具栏「为国家新增分区」:不传国家,抽屉内自行选择 */
function handleAddZone() {
zoneDrawerRef.value.open()
}
function loadDivisionList(zsId) {
if (!zsId) {
divisionList.value = []
return
}
divisionLoading.value = true
listDivision({ zsId, pageNum: 1, pageSize: 200 }).then(res => {
divisionList.value = res.rows || []
}).catch(() => {
divisionList.value = []
}).finally(() => {
divisionLoading.value = false
})
function openZoneDrawer(row) {
zoneDrawerRef.value.open(row)
}
function resetSchemeForm() {
schemeForm.value = {
zsId: undefined,
zsName: undefined,
ssCode: 'on',
pgCode: undefined,
snId: undefined,
zsNote: undefined
}
proxy.resetForm('schemeFormRef')
/** 抽屉内分区/邮编有变动 → 刷新国家表格的分区数/邮编数统计 */
function onZoneChanged() {
countryTableRef.value.refresh()
}
function handleSchemeAdd() {
resetSchemeForm()
schemeOpen.value = true
schemeTitle.value = '新建分区方案'
}
function handleSchemeUpdate(row) {
resetSchemeForm()
getScheme(row.zsId).then(res => {
schemeForm.value = res.data || {}
schemeOpen.value = true
schemeTitle.value = '编辑分区方案'
})
}
function submitSchemeForm() {
proxy.$refs['schemeFormRef'].validate(valid => {
if (!valid) return
const payload = { ...schemeForm.value }
if (payload.snId === '' || payload.snId === null) {
payload.snId = undefined
} else if (payload.snId !== undefined) {
payload.snId = Number(payload.snId)
}
const req = payload.zsId != null ? updateScheme(payload) : addScheme(payload)
req.then(() => {
proxy.$modal.msgSuccess(payload.zsId != null ? '修改成功' : '新增成功')
schemeOpen.value = false
schemeTableRef.value.refresh()
if (selectedScheme.value?.zsId === payload.zsId) {
loadDivisionList(payload.zsId)
}
})
})
}
function handleSchemeDelete(zsIds) {
proxy.$modal.confirm('是否确认删除分区方案编号为"' + zsIds + '"的数据项?').then(() => {
return delScheme(zsIds)
function handleCountryDelete(regionIds) {
proxy.$modal.confirm('是否清空国家 regionId="' + regionIds + '" 下的全部分区与邮编?(不会删除 bms_region 国家主数据)').then(() => {
return delCountryZones(regionIds)
}).then(() => {
schemeTableRef.value.refresh()
if (selectedScheme.value && String(zsIds).split(',').map(String).includes(String(selectedScheme.value.zsId))) {
selectedScheme.value = null
divisionList.value = []
}
proxy.$modal.msgSuccess('删除成功')
countryTableRef.value.refresh()
zoneDrawerRef.value.onCountryCleared(regionIds)
proxy.$modal.msgSuccess('清空成功')
}).catch(() => {})
}
function resetDivisionForm() {
divisionForm.value = {
zdId: undefined,
zsId: selectedScheme.value?.zsId,
zdCode: undefined,
znName: undefined,
znEname: undefined,
zdSort: undefined,
znNote: undefined
}
proxy.resetForm('divisionFormRef')
function handleImport() {
importRef.value.open()
}
function handleDivisionAdd() {
if (!selectedScheme.value) {
proxy.$modal.msgWarning('请先选择分区方案')
return
}
resetDivisionForm()
divisionOpen.value = true
divisionTitle.value = '新增区划'
}
function handleDivisionUpdate(row) {
resetDivisionForm()
divisionForm.value = { ...row }
divisionOpen.value = true
divisionTitle.value = '编辑区划'
}
function submitDivisionForm() {
proxy.$refs['divisionFormRef'].validate(valid => {
if (!valid) return
const payload = { ...divisionForm.value, zsId: selectedScheme.value.zsId }
const req = payload.zdId != null ? updateDivision(payload) : addDivision(payload)
req.then(() => {
proxy.$modal.msgSuccess(payload.zdId != null ? '修改成功' : '新增成功')
divisionOpen.value = false
loadDivisionList(selectedScheme.value.zsId)
schemeTableRef.value.refresh()
})
})
}
function handleDivisionDelete(zdIds) {
proxy.$modal.confirm('是否确认删除区划编号为"' + zdIds + '"的数据项?').then(() => {
return delDivision(zdIds)
}).then(() => {
loadDivisionList(selectedScheme.value.zsId)
schemeTableRef.value.refresh()
proxy.$modal.msgSuccess('删除成功')
}).catch(() => {})
}
function divideKindLabel(kind) {
const map = { C: '国家(C)', T: '城市(T)', P: '邮编(P)' }
return map[kind] || kind
}
function divideKindTag(kind) {
const map = { C: 'primary', T: 'warning', P: 'success' }
return map[kind] || 'info'
}
function openCountryDrawer(row) {
selectedDivision.value = row
countryDrawerVisible.value = true
loadCountryList(row.zdId)
}
function loadCountryList(zdId) {
if (!zdId) {
countryList.value = []
return
}
countryLoading.value = true
listCountry({ zdId, pageNum: 1, pageSize: 500 }).then(res => {
countryList.value = res.rows || []
}).catch(() => {
countryList.value = []
}).finally(() => {
countryLoading.value = false
})
}
function resetCountryForm() {
countryForm.value = {
zcId: undefined,
zdId: selectedDivision.value?.zdId,
countryCode: undefined,
zcDividekind: 'C'
}
proxy.resetForm('countryFormRef')
}
function handleCountryAdd() {
resetCountryForm()
countryOpen.value = true
countryTitle.value = '新增国家'
}
function handleCountryUpdate(row) {
resetCountryForm()
countryForm.value = { ...row }
countryOpen.value = true
countryTitle.value = '编辑国家'
}
function submitCountryForm() {
proxy.$refs['countryFormRef'].validate(valid => {
if (!valid) return
const payload = {
...countryForm.value,
zdId: selectedDivision.value.zdId,
countryCode: (countryForm.value.countryCode || '').toUpperCase()
}
const req = payload.zcId != null ? updateCountry(payload) : addCountry(payload)
req.then(() => {
proxy.$modal.msgSuccess(payload.zcId != null ? '修改成功' : '新增成功')
countryOpen.value = false
loadCountryList(selectedDivision.value.zdId)
loadDivisionList(selectedScheme.value.zsId)
})
})
}
function handleCountryDelete(zcIds) {
proxy.$modal.confirm('是否确认删除国家编号为"' + zcIds + '"的数据项?').then(() => {
return delCountry(zcIds)
}).then(() => {
loadCountryList(selectedDivision.value.zdId)
loadDivisionList(selectedScheme.value.zsId)
proxy.$modal.msgSuccess('删除成功')
}).catch(() => {})
function onImportSuccess() {
countryTableRef.value.refresh()
zoneDrawerRef.value.reload()
}
</script>
<style scoped>
.detail-card {
margin-top: 16px;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.sub-hint {
margin-left: 10px;
color: #909399;
font-size: 13px;
font-weight: normal;
}
.drawer-head {
display: flex;
align-items: center;
gap: 10px;
}
.drawer-title {
font-size: 16px;
font-weight: 600;
}
.drawer-sub {
color: #909399;
font-size: 13px;
}
.drawer-toolbar {
margin-bottom: 12px;
}
.mb16 { margin-bottom: 16px; }
</style>
@@ -16,7 +16,7 @@
:closable="false"
show-icon
class="mb12"
title="FIXED 模式:可不填明细(计费取 bms_product_country 默认运费/挂号费);有明细时按产品×国家×重量段覆盖固定价。附加费来自产品关联,只读展示。"
title="FIXED 模式:可不填明细(计费取 bms_product_country 默认运费/挂号费);有明细时按产品×国家×分区×重量段覆盖固定价。附加费来自产品关联,只读展示。"
/>
<el-alert
v-else
@@ -64,6 +64,29 @@
</template>
</el-table-column>
<el-table-column v-if="isFixedType(setType)" label="分区" min-width="140">
<template #default="{ row }">
<el-select
v-if="refData.hasZonesOf(row)"
v-model="row.zoneCode"
filterable
clearable
placeholder="选择分区"
style="width: 100%"
:disabled="!row._countryKey"
@change="onZoneChange(row)"
>
<el-option
v-for="z in refData.zoneOptionsOf(row)"
:key="z.key"
:label="z.label"
:value="z.zoneCode"
/>
</el-select>
<span v-else class="muted">-</span>
</template>
</el-table-column>
<el-table-column v-if="isFixedType(setType)" label="重量段" min-width="150">
<template #default="{ row }">
<el-select
@@ -72,7 +95,7 @@
clearable
placeholder="选择重量段"
style="width: 100%"
:disabled="!row._countryKey"
:disabled="!row._countryKey || (refData.hasZonesOf(row) && !row.zoneCode)"
@change="onWeightChange(row)"
>
<el-option
@@ -158,7 +181,7 @@ const { bms_round_mode } = useDict('bms_round_mode')
/** 参考数据来自父页面,缓存与请求去重全页面共享 */
const { productOptions, surchargesOf, countryOptionsOf, weightOptionsOf } = props.refData
const { onProductChange, onCountryChange, onWeightChange } = useDetailCascade(props.refData)
const { onProductChange, onCountryChange, onZoneChange, onWeightChange } = useDetailCascade(props.refData)
function addRow() {
props.details.push(createEmptyDetailRow())
@@ -37,6 +37,21 @@
</el-select>
</el-form-item>
</el-col>
<el-col v-if="hasZones" :span="12">
<el-form-item label="分区" prop="zoneCode">
<el-select
v-model="form.zoneCode"
filterable
clearable
placeholder="选择分区"
style="width: 100%"
:disabled="!form._countryKey"
@change="onZoneChange(form)"
>
<el-option v-for="z in zoneOptions" :key="z.key" :label="z.label" :value="z.zoneCode" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="重量段" prop="weightLabel">
<el-select
@@ -45,7 +60,7 @@
clearable
placeholder="选择重量段"
style="width: 100%"
:disabled="!form._countryKey"
:disabled="!form._countryKey || (hasZones && !form.zoneCode)"
@change="onWeightChange(form)"
>
<el-option v-for="w in weightOptions" :key="w.value" :label="w.label" :value="w.value" />
@@ -173,8 +188,8 @@ const emit = defineEmits(['success'])
const { proxy } = getCurrentInstance()
const { bms_round_mode } = useDict('bms_round_mode')
const { productOptions, surchargesOf, countryOptionsOf, weightOptionsOf } = props.refData
const { onProductChange, onCountryChange, onWeightChange } = useDetailCascade(props.refData)
const { productOptions, surchargesOf, countryOptionsOf, zoneOptionsOf, hasZonesOf, weightOptionsOf } = props.refData
const { onProductChange, onCountryChange, onZoneChange, onWeightChange } = useDetailCascade(props.refData)
const visible = ref(false)
const title = ref('')
@@ -189,6 +204,8 @@ const rules = {
const surcharges = computed(() => surchargesOf(form.value))
const countryOptions = computed(() => countryOptionsOf(form.value))
const zoneOptions = computed(() => zoneOptionsOf(form.value))
const hasZones = computed(() => hasZonesOf(form.value))
const weightOptions = computed(() => weightOptionsOf(form.value))
/** 打开弹窗:row 为空表示新增 */
@@ -30,6 +30,9 @@
<el-table-column label="国家" min-width="130">
<template #default="{ row }">{{ row.countryName || row.countryCode || '-' }}</template>
</el-table-column>
<el-table-column label="分区" min-width="120">
<template #default="{ row }">{{ row.zoneName || row.zoneCode || '-' }}</template>
</el-table-column>
<el-table-column label="重量段" prop="weightLabel" min-width="130" />
<el-table-column label="运费单价" min-width="130" align="right">
<template #default="{ row }">
@@ -119,7 +122,7 @@ const details = computed(() => props.set.details || [])
const editable = computed(() => isChildEditable(props.set.status))
const subTitle = computed(() => isFixedType(props.set.setType)
? '按 产品 × 国家 × 重量段 覆盖固定价'
? '按 产品 × 国家 × 分区 × 重量段 覆盖固定价'
: '按产品限定折扣,留空走套餐默认折扣')
const hint = computed(() => isFixedType(props.set.setType)
@@ -8,10 +8,12 @@ import { listProduct, listProductCountries } from '@/api/price/product'
import { listSurchargeByProduct } from '@/api/price/surcharge'
import {
buildCountryOptions,
buildZoneOptions,
buildWeightOptions,
countryHasZones,
countryKeyOf,
formatWeightLabel,
matchCountry,
matchCountryZone,
toEditableDetailRow
} from '../utils/priceSet'
@@ -96,6 +98,14 @@ export function usePriceSetRefData() {
return buildCountryOptions(countryCache[row?.productCode] || [])
}
function zoneOptionsOf(row) {
return buildZoneOptions(countryCache[row?.productCode] || [], row || {})
}
function hasZonesOf(row) {
return countryHasZones(countryCache[row?.productCode] || [], row || {})
}
/** 明细行可选重量段 */
function weightOptionsOf(row) {
return buildWeightOptions(countryCache[row?.productCode] || [], row || {})
@@ -109,12 +119,14 @@ export function usePriceSetRefData() {
ensureSurcharges,
surchargesOf,
countryOptionsOf,
zoneOptionsOf,
hasZonesOf,
weightOptionsOf
}
}
/**
* 明细行级联:产品 → 国家 → 重量段,并带出默认运费 / 挂号费
* 明细行级联:产品 → 国家 → 分区 → 重量段,并带出默认运费 / 挂号费
*
* @param {Object} refData usePriceSetRefData() 的返回值(页面内共享同一份)
*/
@@ -124,6 +136,9 @@ export function useDetailCascade(refData) {
row.countryCode = undefined
row.countryName = undefined
row._countryKey = undefined
row.zoneId = undefined
row.zoneCode = undefined
row.zoneName = undefined
row.weightLabel = undefined
row.productCountryId = undefined
row.fvValue = undefined
@@ -144,6 +159,20 @@ export function useDetailCascade(refData) {
const opt = refData.countryOptionsOf(row).find(c => c.key === row._countryKey)
row.countryCode = opt ? opt.code : undefined
row.countryName = opt ? opt.name : undefined
row.zoneId = undefined
row.zoneCode = undefined
row.zoneName = undefined
row.weightLabel = undefined
row.productCountryId = undefined
row.fvValue = undefined
row.fvRegistrationFee = undefined
}
function onZoneChange(row) {
const opt = refData.zoneOptionsOf(row).find(z => z.key === row.zoneCode)
row.zoneId = opt ? opt.zoneId : undefined
row.zoneCode = opt ? opt.zoneCode : undefined
row.zoneName = opt ? opt.zoneName : undefined
row.weightLabel = undefined
row.productCountryId = undefined
row.fvValue = undefined
@@ -158,6 +187,9 @@ export function useDetailCascade(refData) {
return
}
row.productCountryId = opt.detailId
if (opt.zoneId != null) row.zoneId = opt.zoneId
if (opt.zoneCode) row.zoneCode = opt.zoneCode
if (opt.zoneName) row.zoneName = opt.zoneName
if (opt.freightPrice != null) row.fvValue = Number(opt.freightPrice)
if (opt.registrationFee != null) row.fvRegistrationFee = Number(opt.registrationFee)
}
@@ -171,7 +203,8 @@ export function useDetailCascade(refData) {
refData.ensureCountries(row.productCode).then(rows => {
if (!row.weightLabel) return
const hit = rows.find(item => {
if (!matchCountry(row, item)) return false
if (row.productCountryId != null && Number(item.detailId) === Number(row.productCountryId)) return true
if (!matchCountryZone(row, item)) return false
return formatWeightLabel(item.weightMin, item.weightMax) === row.weightLabel
|| (row.weightLabel.includes(String(item.weightMin)) && row.weightLabel.includes(String(item.weightMax)))
})
@@ -181,9 +214,12 @@ export function useDetailCascade(refData) {
row.productCountryId = hit.detailId
if (!row.countryCode) row.countryCode = hit.countryCode
if (!row.countryName) row.countryName = hit.countryName
if (hit.zoneId != null) row.zoneId = hit.zoneId
if (hit.zoneCode) row.zoneCode = hit.zoneCode
if (hit.zoneName) row.zoneName = hit.zoneName
})
return row
}
return { resetRow, onProductChange, onCountryChange, onWeightChange, hydrateRow }
return { resetRow, onProductChange, onCountryChange, onZoneChange, onWeightChange, hydrateRow }
}
@@ -115,6 +115,22 @@ export function matchCountry(detail, item) {
return !!detail.countryName && item.countryName === detail.countryName
}
/** detail row zone vs product-country row */
export function matchZone(detail, item) {
const detailHas = !!(detail.zoneCode || detail.zoneId)
const itemHas = !!(item.zoneCode || item.zoneId)
if (!detailHas && !itemHas) return true
if (detailHas !== itemHas) return false
if (detail.zoneId != null && item.zoneId != null) return Number(detail.zoneId) === Number(item.zoneId)
if (detail.zoneCode && item.zoneCode) return String(detail.zoneCode) === String(item.zoneCode)
return false
}
/** country + zone match */
export function matchCountryZone(detail, item) {
return matchCountry(detail, item) && matchZone(detail, item)
}
/**
* 国家下拉选项(按产品的国家配置去重)
* @param {Array} rows bms_product_country 行
@@ -141,9 +157,39 @@ export function buildCountryOptions(rows) {
* @param {Object} row 当前明细行(含国家)
* @returns {Array<{value, label, detailId, freightPrice, registrationFee}>}
*/
export function buildWeightOptions(rows, row) {
/**
* Zone options for selected country (from product-country rows).
* @returns {Array<{key, zoneId, zoneCode, zoneName, label}>}
*/
export function buildZoneOptions(rows, row) {
const map = new Map()
;(rows || []).filter(item => matchCountry(row, item)).forEach(item => {
if (!item.zoneCode && item.zoneId == null) return
const key = item.zoneCode || String(item.zoneId)
if (map.has(key)) return
map.set(key, {
key,
zoneId: item.zoneId,
zoneCode: item.zoneCode || undefined,
zoneName: item.zoneName || undefined,
label: item.zoneName || item.zoneCode || key
})
})
return [...map.values()]
}
/** Whether selected country has any zone rows */
export function countryHasZones(rows, row) {
return buildZoneOptions(rows, row).length > 0
}
export function buildWeightOptions(rows, row) {
const list = rows || []
if (countryHasZones(list, row) && !(row && (row.zoneCode || row.zoneId))) {
return []
}
const map = new Map()
list.filter(item => matchCountryZone(row, item)).forEach(item => {
if (item.weightMin == null && item.weightMax == null) return
const value = formatWeightLabel(item.weightMin, item.weightMax)
if (map.has(value)) return
@@ -151,6 +197,9 @@ export function buildWeightOptions(rows, row) {
value,
label: value,
detailId: item.detailId,
zoneId: item.zoneId,
zoneCode: item.zoneCode,
zoneName: item.zoneName,
freightPrice: item.freightPrice,
registrationFee: item.registrationFee
})
@@ -186,6 +235,9 @@ export function createEmptyDetailRow() {
countryCode: undefined,
countryName: undefined,
_countryKey: undefined,
zoneId: undefined,
zoneCode: undefined,
zoneName: undefined,
weightLabel: undefined,
productCountryId: undefined,
fvValue: undefined,
@@ -235,6 +287,9 @@ export function toDetailPayload(d) {
productName: d.productName,
countryCode: d.countryCode || undefined,
countryName: d.countryName || undefined,
zoneId: d.zoneId || undefined,
zoneCode: d.zoneCode || undefined,
zoneName: d.zoneName || undefined,
weightLabel: d.weightLabel || undefined,
productCountryId: d.productCountryId || undefined,
fvValue: d.fvValue,