From ccf85ebefce4abfca0d600fd4a26021a3ccac2b8 Mon Sep 17 00:00:00 2001 From: wuweihua <88888888> Date: Tue, 15 Sep 2026 16:55:11 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=88=86=E5=8C=BA=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/api/TmsPriceController.java | 3 +- .../main/resources/i18n/messages.properties | 3 + .../java/com/ruoyi/price/domain/PrcQuote.java | 1 + .../price/domain/vo/TmsPriceBillRequest.java | 1 - .../price/domain/vo/TmsPriceQuoteRequest.java | 7 +- .../price/domain/vo/TmsPriceQuoteResult.java | 3 + .../price/mapper/BmsProductCountryMapper.java | 13 +- .../price/mapper/PrcZoneDetailMapper.java | 8 ++ .../impl/TmsPriceQuoteServiceImpl.java | 119 +++++++++++++++++- .../mapper/price/BmsProductCountryMapper.xml | 37 +++++- .../mapper/price/PrcOrderBillMapper.xml | 4 +- .../resources/mapper/price/PrcQuoteMapper.xml | 7 +- .../mapper/price/PrcZoneDetailMapper.xml | 24 ++++ bms-back/sql/price/README_billing_postcode.md | 22 ++++ bms-back/sql/price/billing_postcode_alter.sql | 28 +++++ bms-back/sql/price/billing_zone_alter.sql | 89 +++++++++++++ bms-front/src/views/price/billing/quote.vue | 43 ++++++- 17 files changed, 394 insertions(+), 18 deletions(-) create mode 100644 bms-back/sql/price/README_billing_postcode.md create mode 100644 bms-back/sql/price/billing_postcode_alter.sql create mode 100644 bms-back/sql/price/billing_zone_alter.sql diff --git a/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/TmsPriceController.java b/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/TmsPriceController.java index b1d0c606..07074984 100644 --- a/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/TmsPriceController.java +++ b/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/TmsPriceController.java @@ -53,7 +53,8 @@ public class TmsPriceController extends BaseController private IBillSearchStore billSearchStore; /** - * Quote by customerId + productCode + country + weight. + * Quote by customer + product + country + weight (+ destPostcode when country has zones). + * Zone resolve: destPostcode -> prc_zone_detail/prc_zone -> bms_product_country.zone_code. * Match: customer package FIXED > DISCOUNT > product default. */ @PostMapping("/quote") diff --git a/bms-back/ruoyi-admin/src/main/resources/i18n/messages.properties b/bms-back/ruoyi-admin/src/main/resources/i18n/messages.properties index 693f561e..3c975a5e 100644 --- a/bms-back/ruoyi-admin/src/main/resources/i18n/messages.properties +++ b/bms-back/ruoyi-admin/src/main/resources/i18n/messages.properties @@ -117,6 +117,9 @@ price.product.import.noCountry=产品明细sheet无有效国家/重量段数据 price.product.import.euEmpty=国家表无欧盟成员(is_eu=1),无法展开「欧盟」 price.product.import.success.unmatched=未匹配国家表(country_code 为空):{0} # 报价试算 TmsPriceQuote(对外 TMS API 错误码,保持英文契约) +price.quote.postcodeRequired=该国家/产品有分区,请填写目的邮编 +price.quote.postcodeZoneNotFound=邮编未匹配到分区,请核对目的邮编 +price.quote.zonePriceNotHit=已匹配分区,但该分区无对应产品重量段价格 price.quote.priceNotHit=没有产品/国家/重量段绑定,暂无价格 price.quote.freightMissing1=产品默认运费为空 price.quote.freightMissing2=产品默认运费或挂号费为空 diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/PrcQuote.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/PrcQuote.java index 903ef458..815fd1c1 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/PrcQuote.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/PrcQuote.java @@ -22,6 +22,7 @@ public class PrcQuote extends BaseEntity private String productName; private String countryCode; private String countryName; + private String destPostcode; private BigDecimal weight; @JsonFormat(pattern = "yyyy-MM-dd") private Date bizDate; diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceBillRequest.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceBillRequest.java index 0fdf1c87..9f2a1ba4 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceBillRequest.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceBillRequest.java @@ -30,7 +30,6 @@ public class TmsPriceBillRequest extends TmsPriceQuoteRequest private BigDecimal pkgHeight; private String shipperName; private String consigneeName; - private String destPostcode; private String warehouseCode; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date orderTime; diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceQuoteRequest.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceQuoteRequest.java index 9368006d..163553fe 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceQuoteRequest.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceQuoteRequest.java @@ -19,11 +19,14 @@ public class TmsPriceQuoteRequest implements Serializable @NotBlank(message = "productCode is required") private String productCode; - /** ISO2;与 countryName 二选一 */ + /** ISO2; countryName alternative */ private String countryCode; - /** 国家中文名;与 countryCode 二选一 */ + /** country display name; countryCode alternative */ private String countryName; + /** Dest postcode; required when product-country has zones */ + private String destPostcode; + @NotNull(message = "weight is required") private BigDecimal weight; diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceQuoteResult.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceQuoteResult.java index 57d1c26c..ab5ad279 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceQuoteResult.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceQuoteResult.java @@ -30,6 +30,9 @@ public class TmsPriceQuoteResult implements Serializable private String productName; private String countryCode; private String countryName; + private Long zoneId; + private String zoneCode; + private String zoneName; private Long productCountryId; private BigDecimal weightMin; private BigDecimal weightMax; diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/BmsProductCountryMapper.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/BmsProductCountryMapper.java index 90911c7e..65e1a907 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/BmsProductCountryMapper.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/BmsProductCountryMapper.java @@ -42,13 +42,22 @@ public interface BmsProductCountryMapper @Param("effectiveDate") Date effectiveDate); /** - * Match by product + country + weight + bizDate. - * effective_date <= bizDate and (expire_date is null or expire_date > bizDate). + * Match by product + country + weight + bizDate (+ optional zoneCode). + * When zoneCode is empty, prefer rows without zone. + * When zoneCode is set, require matching zone_code. */ public BmsProductCountry selectMatchByProductCountryWeight( @Param("productCode") String productCode, @Param("countryCode") String countryCode, @Param("countryName") String countryName, + @Param("zoneCode") String zoneCode, @Param("weight") BigDecimal weight, @Param("bizDate") Date bizDate); + + /** Count product-country rows that have a zone for this product+country+bizDate */ + public int countZonedByProductCountry( + @Param("productCode") String productCode, + @Param("countryCode") String countryCode, + @Param("countryName") String countryName, + @Param("bizDate") Date bizDate); } diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcZoneDetailMapper.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcZoneDetailMapper.java index aa400c6d..a3d6dab9 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcZoneDetailMapper.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcZoneDetailMapper.java @@ -2,6 +2,7 @@ package com.ruoyi.price.mapper; import java.util.List; import org.apache.ibatis.annotations.Param; +import com.ruoyi.price.domain.PrcZone; import com.ruoyi.price.domain.PrcZoneDetail; public interface PrcZoneDetailMapper @@ -25,4 +26,11 @@ public interface PrcZoneDetailMapper public int deletePrcZoneDetailByZoneId(Long zoneId); public int deletePrcZoneDetailByRegionId(Long regionId); + + /** + * Resolve zone by country ISO2 + postcode (exact first, then longest prefix). + */ + public PrcZone selectZoneByCountryPostcode( + @Param("countryCode") String countryCode, + @Param("postcode") String postcode); } diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/TmsPriceQuoteServiceImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/TmsPriceQuoteServiceImpl.java index 1c9e1d8e..f33591df 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/TmsPriceQuoteServiceImpl.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/TmsPriceQuoteServiceImpl.java @@ -28,6 +28,8 @@ import com.ruoyi.price.domain.PrcPriceSetDetail; import com.ruoyi.price.domain.PrcQuote; import com.ruoyi.price.domain.PrcQuoteFee; import com.ruoyi.price.domain.PrcSurchargeRel; +import com.ruoyi.price.domain.PrcZone; +import com.ruoyi.price.mapper.PrcZoneDetailMapper; import com.ruoyi.price.domain.vo.TmsPriceQuoteFeeLine; import com.ruoyi.price.domain.vo.TmsPriceQuoteRequest; import com.ruoyi.price.domain.vo.TmsPriceQuoteResult; @@ -87,6 +89,9 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService @Autowired private PrcQuoteFeeMapper quoteFeeMapper; + @Autowired + private PrcZoneDetailMapper zoneDetailMapper; + @Autowired private IBillSearchStore billSearchStore; @@ -178,11 +183,55 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService } BigDecimal weight = request.getWeight(); + String destPostcode = emptyToNull(request.getDestPostcode()); + String zoneCode = null; + PrcZone resolvedZone = null; + int zonedCount = productCountryMapper.countZonedByProductCountry( + productCode, countryCode, countryName, bizDate); + if (zonedCount > 0) + { + if (StringUtils.isEmpty(destPostcode)) + { + throw new ServiceException(MessageUtils.message("price.quote.postcodeRequired")); + } + String iso2 = resolveCountryIso2(productCode, countryCode, countryName); + if (StringUtils.isEmpty(iso2)) + { + throw new ServiceException(MessageUtils.message("price.quote.countryRequired")); + } + resolvedZone = zoneDetailMapper.selectZoneByCountryPostcode(iso2, normalizePostcode(destPostcode)); + if (resolvedZone == null || StringUtils.isEmpty(resolvedZone.getZoneCode())) + { + throw new ServiceException(MessageUtils.message("price.quote.postcodeZoneNotFound")); + } + zoneCode = resolvedZone.getZoneCode(); + if (countryCode == null) + { + countryCode = iso2; + } + } + BmsProductCountry base = productCountryMapper.selectMatchByProductCountryWeight( - productCode, countryCode, countryName, weight, bizDate); + productCode, countryCode, countryName, zoneCode, weight, bizDate); if (base == null) { - throw new ServiceException(MessageUtils.message("price.quote.priceNotHit")); + throw new ServiceException(MessageUtils.message( + zonedCount > 0 ? "price.quote.zonePriceNotHit" : "price.quote.priceNotHit")); + } + if (resolvedZone != null) + { + if (base.getZoneId() == null) + { + base.setZoneId(resolvedZone.getZoneId()); + } + if (StringUtils.isEmpty(base.getZoneCode())) + { + base.setZoneCode(resolvedZone.getZoneCode()); + } + if (StringUtils.isEmpty(base.getZoneName())) + { + base.setZoneName(resolvedZone.getZoneName()); + } } String productName = resolveProductName(productCode, bizDate); @@ -213,6 +262,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService TmsPriceQuoteResult hit = tryFixed(bind, set, base, productCode, productName, weight); if (hit != null) { + stampRequestMeta(request, hit); return hit; } } @@ -221,14 +271,67 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService TmsPriceQuoteResult hit = tryDiscount(bind, set, base, productCode, productName, weight); if (hit != null) { + stampRequestMeta(request, hit); return hit; } } } } - return buildDefault(request.getCustomerId(), base, productCode, productName, weight, + TmsPriceQuoteResult fallback = buildDefault(request.getCustomerId(), base, productCode, productName, weight, MessageUtils.message("price.quote.noCustomerPackageOverride")); + stampRequestMeta(request, fallback); + return fallback; + } + + private void stampRequestMeta(TmsPriceQuoteRequest request, TmsPriceQuoteResult result) + { + if (result == null || request == null) + { + return; + } + if (StringUtils.isEmpty(result.getDestPostcode())) + { + result.setDestPostcode(emptyToNull(request.getDestPostcode())); + } + } + + private String resolveCountryIso2(String productCode, String countryCode, String countryName) + { + if (StringUtils.isNotEmpty(countryCode)) + { + return countryCode.toUpperCase(); + } + BmsProductCountry query = new BmsProductCountry(); + query.setProductCode(productCode); + query.setCountryName(countryName); + List rows = productCountryMapper.selectBmsProductCountryList(query); + if (rows == null) + { + return null; + } + for (BmsProductCountry row : rows) + { + if (StringUtils.isNotEmpty(row.getCountryCode())) + { + return row.getCountryCode().toUpperCase(); + } + } + return null; + } + + private static String normalizePostcode(String postcode) + { + if (StringUtils.isEmpty(postcode)) + { + return postcode; + } + String n = postcode.trim().replace(" ", ""); + if (n.endsWith(".0") && n.matches("\\d+\\.0")) + { + return n.substring(0, n.length() - 2); + } + return n; } private TmsPriceQuoteResult tryFixed(PrcCustomerPriceSet bind, PrcPriceSet set, @@ -359,6 +462,9 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService r.setProductName(productName); r.setCountryCode(base.getCountryCode()); r.setCountryName(base.getCountryName()); + r.setZoneId(base.getZoneId()); + r.setZoneCode(base.getZoneCode()); + r.setZoneName(base.getZoneName()); r.setProductCountryId(base.getDetailId()); r.setWeightMin(base.getWeightMin()); r.setWeightMax(base.getWeightMax()); @@ -506,6 +612,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService quote.setProductName(result.getProductName()); quote.setCountryCode(result.getCountryCode()); quote.setCountryName(result.getCountryName()); + quote.setDestPostcode(result.getDestPostcode()); quote.setWeight(weight); quote.setBizDate(bizDate); quote.setTmsId(request.getTmsId() == null ? null : Long.valueOf(request.getTmsId())); @@ -637,6 +744,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService r.setProductName(quote.getProductName()); r.setCountryCode(quote.getCountryCode()); r.setCountryName(quote.getCountryName()); + r.setDestPostcode(quote.getDestPostcode()); r.setProductCountryId(quote.getProductCountryId()); r.setWeightMin(quote.getWeightMin()); r.setWeightMax(quote.getWeightMax()); @@ -683,6 +791,10 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService to.setProductName(from.getProductName()); to.setCountryCode(from.getCountryCode()); to.setCountryName(from.getCountryName()); + to.setDestPostcode(from.getDestPostcode()); + to.setZoneId(from.getZoneId()); + to.setZoneCode(from.getZoneCode()); + to.setZoneName(from.getZoneName()); to.setProductCountryId(from.getProductCountryId()); to.setWeightMin(from.getWeightMin()); to.setWeightMax(from.getWeightMax()); @@ -716,6 +828,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService + request.getProductCode().trim() + "|" + nvlStr(countryCode) + "|" + nvlStr(countryName) + "|" + + nvlStr(emptyToNull(request.getDestPostcode())) + "|" + weight.toPlainString() + "|" + biz + "|" + (request.getTmsId() == null ? "" : request.getTmsId()); diff --git a/bms-back/ruoyi-system/src/main/resources/mapper/price/BmsProductCountryMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/price/BmsProductCountryMapper.xml index 03344aaf..5ba5e5fe 100644 --- a/bms-back/ruoyi-system/src/main/resources/mapper/price/BmsProductCountryMapper.xml +++ b/bms-back/ruoyi-system/src/main/resources/mapper/price/BmsProductCountryMapper.xml @@ -167,6 +167,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" and 1 = 0 + + + and upper(ifnull(zone_code,'')) = upper(#{zoneCode}) + + + and (zone_code is null or zone_code = '') + + and (effective_date is null or effective_date <= #{bizDate}) and (expire_date is null or expire_date > #{bizDate}) @@ -174,10 +182,33 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" and expire_date is null - 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 + order by effective_date desc, weight_min asc, detail_id asc limit 1 + + diff --git a/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcOrderBillMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcOrderBillMapper.xml index fc0276af..c975fddc 100644 --- a/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcOrderBillMapper.xml +++ b/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcOrderBillMapper.xml @@ -28,7 +28,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - + @@ -172,7 +172,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" product_code = #{productCode}, product_name = #{productName}, country_code = #{countryCode}, - country_name = #{countryName}, + country_name = #{countryName}, weight = #{weight}, biz_date = #{bizDate}, tms_id = #{tmsId}, diff --git a/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcQuoteMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcQuoteMapper.xml index aeb3e97c..e267bf7d 100644 --- a/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcQuoteMapper.xml +++ b/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcQuoteMapper.xml @@ -12,6 +12,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + @@ -46,7 +47,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" select quote_id, quote_key, customer_id, product_code, product_name, - country_code, country_name, weight, biz_date, tms_id, + country_code, country_name, dest_postcode, weight, biz_date, tms_id, match_source, match_message, price_set_id, price_set_code, price_set_name, set_type, bind_priority, customer_priceset_id, product_country_id, weight_min, weight_max, weight_label, effective_date, charge_weight, @@ -103,7 +104,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" insert into prc_quote ( quote_id, quote_key, customer_id, product_code, product_name, - country_code, country_name, weight, biz_date, tms_id, + country_code, country_name, dest_postcode, weight, biz_date, tms_id, match_source, match_message, price_set_id, price_set_code, price_set_name, set_type, bind_priority, customer_priceset_id, product_country_id, weight_min, weight_max, weight_label, effective_date, charge_weight, @@ -112,7 +113,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" create_time ) values ( #{quoteId}, #{quoteKey}, #{customerId}, #{productCode}, #{productName}, - #{countryCode}, #{countryName}, #{weight}, #{bizDate}, #{tmsId}, + #{countryCode}, #{countryName}, #{destPostcode}, #{weight}, #{bizDate}, #{tmsId}, #{matchSource}, #{matchMessage}, #{priceSetId}, #{priceSetCode}, #{priceSetName}, #{setType}, #{bindPriority}, #{customerPricesetId}, #{productCountryId}, #{weightMin}, #{weightMax}, #{weightLabel}, #{effectiveDate}, #{chargeWeight}, diff --git a/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcZoneDetailMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcZoneDetailMapper.xml index d491969b..8885ebc8 100644 --- a/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcZoneDetailMapper.xml +++ b/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcZoneDetailMapper.xml @@ -91,4 +91,28 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" where z.region_id = #{regionId} + + diff --git a/bms-back/sql/price/README_billing_postcode.md b/bms-back/sql/price/README_billing_postcode.md new file mode 100644 index 00000000..ab259466 --- /dev/null +++ b/bms-back/sql/price/README_billing_postcode.md @@ -0,0 +1,22 @@ +# Billing quote / order sync: postcode -> zone + +## Rule +- Whether a country needs a zone is decided by `bms_product_country` (rows with `zone_code`). +- If the product+country has zones: request **must** include `destPostcode`. +- Zone is resolved at runtime: `destPostcode` -> `prc_zone_detail` + `prc_zone` -> `zone_code`, then match `bms_product_country`. +- Billing tables store **postcode only**, not zone columns: + - `prc_quote.dest_postcode` + - `prc_order_bill.dest_postcode` (already existed) + +## SQL +Run: `bms-back/sql/price/billing_postcode_alter.sql` + +## API fields +- Request: `destPostcode` on `/api/tms/price/quote` and `/api/tms/price/bill` (and console `/price/billing/quote`). +- Response may include `zoneId` / `zoneCode` / `zoneName` for display only (not persisted on bill/quote tables). + +## Errors (i18n) +- `price.quote.postcodeRequired` +- `price.quote.postcodeZoneNotFound` +- `price.quote.zonePriceNotHit` +- `price.quote.priceNotHit` diff --git a/bms-back/sql/price/billing_postcode_alter.sql b/bms-back/sql/price/billing_postcode_alter.sql new file mode 100644 index 00000000..c8962ad8 --- /dev/null +++ b/bms-back/sql/price/billing_postcode_alter.sql @@ -0,0 +1,28 @@ +-- prc_quote: add dest_postcode only (zone resolved at runtime by postcode) +-- billing tables do NOT store zone_* columns +SET NAMES utf8mb4; +SET @db := DATABASE(); + +SET @has := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='prc_quote' AND COLUMN_NAME='dest_postcode'); +SET @sql := IF(@has=0, 'ALTER TABLE prc_quote ADD COLUMN dest_postcode VARCHAR(20) DEFAULT NULL COMMENT ''dest postcode'' AFTER country_name', 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @has := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='prc_quote' AND COLUMN_NAME='zone_id'); +SET @sql := IF(@has>0, 'ALTER TABLE prc_quote DROP COLUMN zone_id', 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; +SET @has := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='prc_quote' AND COLUMN_NAME='zone_code'); +SET @sql := IF(@has>0, 'ALTER TABLE prc_quote DROP COLUMN zone_code', 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; +SET @has := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='prc_quote' AND COLUMN_NAME='zone_name'); +SET @sql := IF(@has>0, 'ALTER TABLE prc_quote DROP COLUMN zone_name', 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @has := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='prc_order_bill' AND COLUMN_NAME='zone_id'); +SET @sql := IF(@has>0, 'ALTER TABLE prc_order_bill DROP COLUMN zone_id', 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; +SET @has := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='prc_order_bill' AND COLUMN_NAME='zone_code'); +SET @sql := IF(@has>0, 'ALTER TABLE prc_order_bill DROP COLUMN zone_code', 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; +SET @has := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='prc_order_bill' AND COLUMN_NAME='zone_name'); +SET @sql := IF(@has>0, 'ALTER TABLE prc_order_bill DROP COLUMN zone_name', 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; diff --git a/bms-back/sql/price/billing_zone_alter.sql b/bms-back/sql/price/billing_zone_alter.sql new file mode 100644 index 00000000..fab86b77 --- /dev/null +++ b/bms-back/sql/price/billing_zone_alter.sql @@ -0,0 +1,89 @@ +-- Add zone + dest_postcode to prc_quote / zone to prc_order_bill +-- Idempotent +-- charset: utf8mb4 + +SET NAMES utf8mb4; +SET @db := DATABASE(); + +-- ========== prc_quote ========== +SET @has := ( + SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'prc_quote' AND COLUMN_NAME = 'dest_postcode' +); +SET @sql := IF(@has = 0, + 'ALTER TABLE prc_quote ADD COLUMN dest_postcode VARCHAR(20) DEFAULT NULL COMMENT ''dest postcode'' AFTER country_name', + 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @has := ( + SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'prc_quote' AND COLUMN_NAME = 'zone_id' +); +SET @sql := IF(@has = 0, + 'ALTER TABLE prc_quote ADD COLUMN zone_id BIGINT(20) DEFAULT NULL COMMENT ''prc_zone.zone_id'' AFTER dest_postcode', + 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @has := ( + SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'prc_quote' AND COLUMN_NAME = 'zone_code' +); +SET @sql := IF(@has = 0, + 'ALTER TABLE prc_quote ADD COLUMN zone_code VARCHAR(50) DEFAULT NULL COMMENT ''zone code'' AFTER zone_id', + 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @has := ( + SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'prc_quote' AND COLUMN_NAME = 'zone_name' +); +SET @sql := IF(@has = 0, + 'ALTER TABLE prc_quote 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; + +SET @has := ( + SELECT COUNT(*) FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'prc_quote' AND INDEX_NAME = 'idx_quote_zone' +); +SET @sql := IF(@has = 0, + 'ALTER TABLE prc_quote ADD INDEX idx_quote_zone (zone_id)', + 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +-- ========== prc_order_bill ========== +SET @has := ( + SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'prc_order_bill' AND COLUMN_NAME = 'zone_id' +); +SET @sql := IF(@has = 0, + 'ALTER TABLE prc_order_bill 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; + +SET @has := ( + SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'prc_order_bill' AND COLUMN_NAME = 'zone_code' +); +SET @sql := IF(@has = 0, + 'ALTER TABLE prc_order_bill ADD COLUMN zone_code VARCHAR(50) DEFAULT NULL COMMENT ''zone code'' AFTER zone_id', + 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @has := ( + SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'prc_order_bill' AND COLUMN_NAME = 'zone_name' +); +SET @sql := IF(@has = 0, + 'ALTER TABLE prc_order_bill 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; + +SET @has := ( + SELECT COUNT(*) FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'prc_order_bill' AND INDEX_NAME = 'idx_bill_zone' +); +SET @sql := IF(@has = 0, + 'ALTER TABLE prc_order_bill ADD INDEX idx_bill_zone (zone_id)', + 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; diff --git a/bms-front/src/views/price/billing/quote.vue b/bms-front/src/views/price/billing/quote.vue index 9e38ff88..d936097d 100644 --- a/bms-front/src/views/price/billing/quote.vue +++ b/bms-front/src/views/price/billing/quote.vue @@ -34,6 +34,7 @@ placeholder="请先选择产品" style="width: 100%" :disabled="!form.productCode" + @change="form.destPostcode = undefined" > + + +
通过邮编判定分区后再匹配产品国家价
+
{{ result.customerId }} {{ result.productName || '-' }} ({{ result.productCode }}) {{ result.countryName || '-' }} ({{ result.countryCode }}) + {{ result.destPostcode || '-' }} + {{ result.zoneName || result.zoneCode || '-' }} {{ result.chargeWeight ?? result.weight }} {{ result.weightLabel || '-' }} {{ formatDate(result.effectiveDate) }} @@ -179,6 +191,8 @@ {{ viewResult.customerId }} {{ viewResult.productName || '-' }} ({{ viewResult.productCode }}) {{ viewResult.countryName || '-' }} ({{ viewResult.countryCode }}) + {{ viewResult.destPostcode || '-' }} + {{ viewResult.zoneName || viewResult.zoneCode || '-' }} {{ viewResult.chargeWeight ?? viewResult.weight }} {{ viewResult.weightLabel || '-' }} @@ -235,6 +249,7 @@ const historyTableRef = ref() const loading = ref(false) const productOptions = ref([]) const countryOptions = ref([]) +const countryRows = ref([]) const result = ref(null) const errorMsg = ref('') const historyOpen = ref(false) @@ -282,6 +297,7 @@ const form = reactive({ customerCode: undefined, productCode: undefined, countryCode: undefined, + destPostcode: undefined, weight: undefined, bizDate: undefined }) @@ -290,6 +306,16 @@ const rules = { customerCode: [{ required: true, message: '请选择客户', trigger: 'change' }], productCode: [{ required: true, message: '请选择产品', trigger: 'change' }], countryCode: [{ required: true, message: '请选择目的国/地区', trigger: 'change' }], + destPostcode: [{ + validator: (_r, val, cb) => { + if (needPostcode.value && !(val && String(val).trim())) { + cb(new Error('该国家有分区,请填写邮编')) + } else { + cb() + } + }, + trigger: 'blur' + }], weight: [{ required: true, message: '请输入计费重量', trigger: 'blur' }] } @@ -314,14 +340,26 @@ function countryOptionLabel(item) { return name + ' (' + item.countryCode + ')' } +const needPostcode = computed(() => { + if (!form.countryCode) return false + return (countryRows.value || []).some(r => + String(r.countryCode || '').toUpperCase() === String(form.countryCode || '').toUpperCase() + && !!(r.zoneCode || r.zoneId) + ) +}) + function handleProductChange(productCode) { form.countryCode = undefined + form.destPostcode = undefined countryOptions.value = [] + countryRows.value = [] if (!productCode) { return } listProductCountries(productCode).then(res => { - countryOptions.value = uniqueCountries((res.data || []).filter(item => !item.expireDate)) + const rows = (res.data || []).filter(item => !item.expireDate) + countryRows.value = rows + countryOptions.value = uniqueCountries(rows) }) } @@ -337,6 +375,7 @@ function handleQuote() { customerCode: form.customerCode, productCode: form.productCode, countryCode: form.countryCode, + destPostcode: form.destPostcode || undefined, weight: form.weight } if (form.bizDate) { @@ -361,9 +400,11 @@ function resetForm() { form.customerCode = undefined form.productCode = undefined form.countryCode = undefined + form.destPostcode = undefined form.weight = undefined form.bizDate = undefined countryOptions.value = [] + countryRows.value = [] result.value = null errorMsg.value = '' proxy.$nextTick(() => {