修改计费逻辑

This commit is contained in:
wuweihua
2026-10-12 14:44:20 +08:00
parent 4d41b0fe12
commit a115926e9f
6 changed files with 39 additions and 44 deletions
@@ -26,7 +26,8 @@ public class PrcQuote extends BaseEntity
private String countryName; private String countryName;
private String destPostcode; private String destPostcode;
private BigDecimal weight; private BigDecimal weight;
@JsonFormat(pattern = "yyyy-MM-dd") /** 计费时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date bizDate; private Date bizDate;
private Long tmsId; private Long tmsId;
@@ -12,7 +12,7 @@ import java.util.List;
* *
* Match order: * Match order:
* 1) Customer binds (package not stopped), bind_priority ASC; bind date range is not used for billing * 1) Customer binds (package not stopped), bind_priority ASC; bind date range is not used for billing
* 2) Active package version at bill-time end-of-day (version effective_time / expire_time) * 2) Active package version at bill datetime (version effective_time / expire_time)
* 3) FIXED detail override, else DISCOUNT on that version * 3) FIXED detail override, else DISCOUNT on that version
* 4) Else product default only when a version existed but detail missed * 4) Else product default only when a version existed but detail missed
* Package hit uses version surcharge snapshot when present; else product library. * Package hit uses version surcharge snapshot when present; else product library.
@@ -240,8 +240,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
{ {
return Collections.emptyList(); return Collections.emptyList();
} }
Date day = bizDate != null ? bizDate : new Date(); Date asOf = bizDate != null ? bizDate : new Date();
Date asOf = endOfDay(day);
for (PrcCustomerPriceSet bind : binds) for (PrcCustomerPriceSet bind : binds)
{ {
if (bind == null || bind.getPriceSetId() == null) if (bind == null || bind.getPriceSetId() == null)
@@ -355,7 +354,8 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
boolean productExcludedBySetProducts = false; boolean productExcludedBySetProducts = false;
boolean attemptedPriceMatch = false; boolean attemptedPriceMatch = false;
boolean anyZoneRequired = false; boolean anyZoneRequired = false;
Date asOf = endOfDay(bizDate); // Match version by exact bill datetime (effective_time / expire_time), not end-of-day.
Date asOf = bizDate != null ? bizDate : new Date();
for (PrcCustomerPriceSet bind : binds) for (PrcCustomerPriceSet bind : binds)
{ {
PrcPriceSet set = priceSetMapper.selectPrcPriceSetById(bind.getPriceSetId()); PrcPriceSet set = priceSetMapper.selectPrcPriceSetById(bind.getPriceSetId());
@@ -406,7 +406,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
if (TYPE_FIXED.equals(setType)) if (TYPE_FIXED.equals(setType))
{ {
TmsPriceQuoteResult hit = tryFixed(bind, set, version, catalog, zone.zone, TmsPriceQuoteResult hit = tryFixed(bind, set, version, catalog, zone.zone,
productCode, productName, cc, cn, zone.zoneCode, weight); productCode, productName, cc, cn, zone.zoneCode, weight, bizDate);
if (hit != null) if (hit != null)
{ {
stampRequestMeta(request, hit); stampRequestMeta(request, hit);
@@ -419,7 +419,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
{ {
continue; continue;
} }
TmsPriceQuoteResult hit = tryDiscount(bind, set, version, catalog, productCode, productName, weight); TmsPriceQuoteResult hit = tryDiscount(bind, set, version, catalog, productCode, productName, weight, bizDate);
if (hit != null) if (hit != null)
{ {
stampRequestMeta(request, hit); stampRequestMeta(request, hit);
@@ -691,7 +691,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
private TmsPriceQuoteResult tryFixed(PrcCustomerPriceSet bind, PrcPriceSet set, PrcPriceSetVersion version, private TmsPriceQuoteResult tryFixed(PrcCustomerPriceSet bind, PrcPriceSet set, PrcPriceSetVersion version,
BmsProductCountry catalog, PrcZone resolvedZone, String productCode, String productName, BmsProductCountry catalog, PrcZone resolvedZone, String productCode, String productName,
String countryCode, String countryName, String zoneCode, BigDecimal weight) String countryCode, String countryName, String zoneCode, BigDecimal weight, Date bizDate)
{ {
Long psvId = version.getPsvId(); Long psvId = version.getPsvId();
int detailCount = detailMapper.countByPsvId(psvId); int detailCount = detailMapper.countByPsvId(psvId);
@@ -713,7 +713,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
return null; return null;
} }
TmsPriceQuoteResult r = buildFromPackageDetail(SRC_FIXED, detail, catalog, resolvedZone, TmsPriceQuoteResult r = buildFromPackageDetail(SRC_FIXED, detail, catalog, resolvedZone,
productCode, productName, weight, detail.getFvValue(), null, null, detail.getFvRegistrationFee()); productCode, productName, weight, detail.getFvValue(), null, null, detail.getFvRegistrationFee(), bizDate);
fillPackage(r, bind, set); fillPackage(r, bind, set);
stampVersion(r, version, detail.getPsdId()); stampVersion(r, version, detail.getPsdId());
r.setMatchMessage(MessageUtils.message("price.quote.hitCustomerFixedPackageDetail") r.setMatchMessage(MessageUtils.message("price.quote.hitCustomerFixedPackageDetail")
@@ -723,7 +723,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
} }
private TmsPriceQuoteResult tryDiscount(PrcCustomerPriceSet bind, PrcPriceSet set, PrcPriceSetVersion version, private TmsPriceQuoteResult tryDiscount(PrcCustomerPriceSet bind, PrcPriceSet set, PrcPriceSetVersion version,
BmsProductCountry base, String productCode, String productName, BigDecimal weight) BmsProductCountry base, String productCode, String productName, BigDecimal weight, Date bizDate)
{ {
Long psvId = version.getPsvId(); Long psvId = version.getPsvId();
int detailCount = detailMapper.countByPsvId(psvId); int detailCount = detailMapper.countByPsvId(psvId);
@@ -760,7 +760,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
BigDecimal rate = baseRate.multiply(discount) BigDecimal rate = baseRate.multiply(discount)
.divide(new BigDecimal("100"), 2, rm); .divide(new BigDecimal("100"), 2, rm);
TmsPriceQuoteResult r = buildFromRates(SRC_DISCOUNT, rate, baseRate, discount, TmsPriceQuoteResult r = buildFromRates(SRC_DISCOUNT, rate, baseRate, discount,
base.getRegistrationFee(), base, productCode, productName, weight, roundMode); base.getRegistrationFee(), base, productCode, productName, weight, roundMode, bizDate);
fillPackage(r, bind, set); fillPackage(r, bind, set);
stampVersion(r, version, psdId); stampVersion(r, version, psdId);
r.setMatchMessage("hit customer DISCOUNT package version " r.setMatchMessage("hit customer DISCOUNT package version "
@@ -787,7 +787,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
private TmsPriceQuoteResult buildFromPackageDetail(String source, PrcPriceSetDetail detail, private TmsPriceQuoteResult buildFromPackageDetail(String source, PrcPriceSetDetail detail,
BmsProductCountry catalog, PrcZone resolvedZone, String productCode, String productName, BmsProductCountry catalog, PrcZone resolvedZone, String productCode, String productName,
BigDecimal weight, BigDecimal freightRate, BigDecimal baseFreightRate, BigDecimal discountPercent, BigDecimal weight, BigDecimal freightRate, BigDecimal baseFreightRate, BigDecimal discountPercent,
BigDecimal registrationFee) BigDecimal registrationFee, Date bizDate)
{ {
if (freightRate == null) if (freightRate == null)
{ {
@@ -879,15 +879,16 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
r.setCurrency("CNY"); r.setCurrency("CNY");
r.setRoundMode(mode); r.setRoundMode(mode);
r.setFromCache(false); r.setFromCache(false);
r.setBizDate(bizDate);
fillProductAmountAndPay(r, catalog, weight, rm, mode); fillProductAmountAndPay(r, catalog, weight, rm, mode);
fillRedlineAmount(r, weight, rm); fillRedlineAmount(r, weight, rm, bizDate);
return r; return r;
} }
private TmsPriceQuoteResult buildFromRates(String source, BigDecimal freightRate, private TmsPriceQuoteResult buildFromRates(String source, BigDecimal freightRate,
BigDecimal baseFreightRate, BigDecimal discountPercent, BigDecimal registrationFee, BigDecimal baseFreightRate, BigDecimal discountPercent, BigDecimal registrationFee,
BmsProductCountry base, String productCode, String productName, BigDecimal weight, BmsProductCountry base, String productCode, String productName, BigDecimal weight,
String roundMode) String roundMode, Date bizDate)
{ {
if (freightRate == null) if (freightRate == null)
{ {
@@ -935,8 +936,9 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
r.setCurrency("CNY"); r.setCurrency("CNY");
r.setRoundMode(mode); r.setRoundMode(mode);
r.setFromCache(false); r.setFromCache(false);
r.setBizDate(bizDate);
fillProductAmountAndPay(r, base, weight, rm, mode); fillProductAmountAndPay(r, base, weight, rm, mode);
fillRedlineAmount(r, weight, rm); fillRedlineAmount(r, weight, rm, bizDate);
return r; return r;
} }
@@ -971,16 +973,17 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
} }
/** /**
* Redline catalog amount from redline_product_country (same match keys as product catalog). * Redline catalog amount from redline_product_country
* (same match keys as product catalog, including bill datetime).
*/ */
private void fillRedlineAmount(TmsPriceQuoteResult r, BigDecimal weight, RoundingMode rm) private void fillRedlineAmount(TmsPriceQuoteResult r, BigDecimal weight, RoundingMode rm, Date bizDate)
{ {
if (r == null || weight == null || StringUtils.isEmpty(r.getProductCode())) if (r == null || weight == null || StringUtils.isEmpty(r.getProductCode()))
{ {
return; return;
} }
BmsRedlineProductCountry base = redlineProductCountryMapper.selectMatchByProductCountryWeight( BmsRedlineProductCountry base = redlineProductCountryMapper.selectMatchByProductCountryWeight(
r.getProductCode(), r.getCountryCode(), r.getCountryName(), r.getZoneCode(), weight, null); r.getProductCode(), r.getCountryCode(), r.getCountryName(), r.getZoneCode(), weight, bizDate);
if (base == null || base.getFreightPrice() == null) if (base == null || base.getFreightPrice() == null)
{ {
return; return;
@@ -1469,7 +1472,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
countryCode = countryCode.toUpperCase(); countryCode = countryCode.toUpperCase();
} }
String countryName = emptyToNull(request.getCountryName()); String countryName = emptyToNull(request.getCountryName());
String biz = new SimpleDateFormat("yyyy-MM-dd").format(bizDate); String biz = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(bizDate);
String raw = request.getCustomerCode().trim() + "|" String raw = request.getCustomerCode().trim() + "|"
+ request.getProductCode().trim() + "|" + request.getProductCode().trim() + "|"
+ nvlStr(countryCode) + "|" + nvlStr(countryCode) + "|"
@@ -1639,24 +1642,6 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
return parsed; return parsed;
} }
/** Use end of bill day so same-day version activate still matches that day's quote. */
private static Date endOfDay(Date day)
{
if (day == null)
{
return new Date();
}
try
{
String d = new SimpleDateFormat("yyyy-MM-dd").format(day);
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(d + " 23:59:59");
}
catch (ParseException e)
{
return day;
}
}
private String resolveProductName(String productCode, Date bizDate) private String resolveProductName(String productCode, Date bizDate)
{ {
BmsProduct p = productMapper.selectEffectiveBmsProductByCode(productCode, bizDate); BmsProduct p = productMapper.selectEffectiveBmsProductByCode(productCode, bizDate);
+2 -2
View File
@@ -1427,7 +1427,7 @@ CREATE TABLE `prc_price_set_product` (
-- ---------------------------- -- ----------------------------
CREATE TABLE `prc_quote` ( CREATE TABLE `prc_quote` (
`quote_id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, `quote_id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL,
`quote_key` char(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '试算指纹 MD5(客户|产品|国家|重量|计费日期)', `quote_key` char(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '试算指纹 MD5(客户|产品|国家|重量|计费时间)',
`customer_id` bigint(20) NOT NULL COMMENT '客户ID', `customer_id` bigint(20) NOT NULL COMMENT '客户ID',
`customer_code` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'customer code', `customer_code` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'customer code',
`product_code` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '产品代码', `product_code` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '产品代码',
@@ -1437,7 +1437,7 @@ CREATE TABLE `prc_quote` (
`dest_postcode` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'dest postcode', `dest_postcode` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'dest postcode',
`quote_postcode_key` varchar(20) COLLATE utf8mb4_unicode_ci GENERATED ALWAYS AS (ifnull(`dest_postcode`,'')) STORED COMMENT 'dest_postcode for unique key', `quote_postcode_key` varchar(20) COLLATE utf8mb4_unicode_ci GENERATED ALWAYS AS (ifnull(`dest_postcode`,'')) STORED COMMENT 'dest_postcode for unique key',
`weight` decimal(10,3) NOT NULL COMMENT '计费重量KG', `weight` decimal(10,3) NOT NULL COMMENT '计费重量KG',
`biz_date` date NOT NULL COMMENT '计费日期', `biz_date` datetime NOT NULL COMMENT '计费时间',
`tms_id` bigint(20) DEFAULT NULL COMMENT '多租户系统ID', `tms_id` bigint(20) DEFAULT NULL COMMENT '多租户系统ID',
`match_source` varchar(30) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '命中来源 PACKAGE_FIXED/套餐固定价 PACKAGE_DISCOUNT/套餐折扣 PRODUCT_DEFAULT/产品默认价', `match_source` varchar(30) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '命中来源 PACKAGE_FIXED/套餐固定价 PACKAGE_DISCOUNT/套餐折扣 PRODUCT_DEFAULT/产品默认价',
`match_message` varchar(200) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '命中说明', `match_message` varchar(200) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '命中说明',
@@ -0,0 +1,4 @@
-- prc_quote.biz_date: date -> datetime (same-day different bill times)
ALTER TABLE prc_quote
MODIFY COLUMN `biz_date` datetime NOT NULL COMMENT '计费时间',
MODIFY COLUMN `quote_key` char(32) NOT NULL COMMENT 'MD5(customer|product|country|weight|biz_datetime)';
+10 -5
View File
@@ -68,8 +68,8 @@
<el-form-item label="计费时间" prop="bizDate"> <el-form-item label="计费时间" prop="bizDate">
<el-date-picker <el-date-picker
v-model="form.bizDate" v-model="form.bizDate"
type="date" type="datetime"
value-format="YYYY-MM-DD" value-format="YYYY-MM-DD HH:mm:ss"
placeholder="默认今天,用于筛选已生效的产品明细" placeholder="默认今天,用于筛选已生效的产品明细"
style="width: 100%" style="width: 100%"
/> />
@@ -288,7 +288,7 @@ const historyColumns = [
{ label: '产品', prop: 'product', minWidth: 160, slot: 'product' }, { label: '产品', prop: 'product', minWidth: 160, slot: 'product' },
{ label: '国家', prop: 'country', minWidth: 130, slot: 'country' }, { label: '国家', prop: 'country', minWidth: 130, slot: 'country' },
{ label: '计费重量', prop: 'weight', align: 'center', width: 100, slot: 'weight' }, { label: '计费重量', prop: 'weight', align: 'center', width: 100, slot: 'weight' },
{ label: '计费日期', prop: 'bizDate', align: 'center', width: 120 }, { label: '计费时间', prop: 'bizDate', align: 'center', width: 170, parseTime: true },
{ label: '计价来源', prop: 'matchSource', align: 'center', width: 140, slot: 'matchSource' }, { label: '计价来源', prop: 'matchSource', align: 'center', width: 140, slot: 'matchSource' },
{ label: '套餐', prop: 'priceSetName', minWidth: 120, showOverflowTooltip: true }, { label: '套餐', prop: 'priceSetName', minWidth: 120, showOverflowTooltip: true },
{ label: '产品价', prop: 'productAmount', align: 'right', width: 110 }, { label: '产品价', prop: 'productAmount', align: 'right', width: 110 },
@@ -527,10 +527,15 @@ function formatRate(val) {
function formatDate(val) { function formatDate(val) {
if (!val) return '-' if (!val) return '-'
// 兼容后端 java.util.Date 转 ISO 字符串 const raw = String(val).replace('T', ' ')
if (/^\d{4}-\d{2}-\d{2}/.test(raw)) {
return raw.length >= 19 ? raw.slice(0, 19) : raw.slice(0, 10)
}
const d = new Date(val) const d = new Date(val)
if (Number.isNaN(d.getTime())) return val if (Number.isNaN(d.getTime())) return val
return d.toISOString().slice(0, 10) const pad = (n) => String(n).padStart(2, '0')
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate())
+ ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds())
} }
function sourceLabel(source) { function sourceLabel(source) {