增加产品价编辑

This commit is contained in:
wuweihua
2026-11-02 09:00:38 +08:00
parent d185995dc7
commit 9d4dbdcbe6
10 changed files with 169 additions and 14 deletions
@@ -116,6 +116,17 @@ public interface BmsProductCountryMapper
@Param("weight") BigDecimal weight, @Param("weight") BigDecimal weight,
@Param("bizDate") Date bizDate); @Param("bizDate") Date bizDate);
/**
* Lowest PRODUCT catalog weight band for product/country/zone (ignore weight).
* Used when charge weight is below the minimum band.
*/
public BmsProductCountry selectMinBandByProductCountry(
@Param("productCode") String productCode,
@Param("countryCode") String countryCode,
@Param("countryName") String countryName,
@Param("zoneCode") String zoneCode,
@Param("bizDate") Date bizDate);
/** Count product-country rows that have a zone for this product+country+bizDate */ /** Count product-country rows that have a zone for this product+country+bizDate */
public int countZonedByProductCountry( public int countZonedByProductCountry(
@Param("productCode") String productCode, @Param("productCode") String productCode,
@@ -71,6 +71,17 @@ public interface PrcPriceSetDetailMapper
@Param("zoneCode") String zoneCode, @Param("zoneCode") String zoneCode,
@Param("weight") BigDecimal weight); @Param("weight") BigDecimal weight);
/**
* Lowest FIXED weight band for product/country/zone (ignore weight).
* Used when charge weight is below the minimum band.
*/
public PrcPriceSetDetail selectFixedMinBand(
@Param("psvId") Long psvId,
@Param("productCode") String productCode,
@Param("countryCode") String countryCode,
@Param("countryName") String countryName,
@Param("zoneCode") String zoneCode);
public int countZonedByPsvProductCountry( public int countZonedByPsvProductCountry(
@Param("psvId") Long psvId, @Param("psvId") Long psvId,
@Param("productCode") String productCode, @Param("productCode") String productCode,
@@ -520,13 +520,28 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
else if (TYPE_DISCOUNT.equals(setType)) else if (TYPE_DISCOUNT.equals(setType))
{ {
// DISCOUNT still needs catalog as base rate; do not soft-fail as 产品价未匹配 for now. // DISCOUNT still needs catalog as base rate; do not soft-fail as 产品价未匹配 for now.
if (!hasProductCatalogPrice(catalog)) BmsProductCountry discCatalog = catalog;
BigDecimal discWeight = weight;
if (!hasProductCatalogPrice(discCatalog))
{
// Below min catalog band: snap charge weight to lowest band min.
BigDecimal snapped = snapWeightToCatalogMinBand(
productCode, cc, cn, zone.zoneCode, weight, bizDate);
if (snapped != null)
{
discWeight = snapped;
discCatalog = softMatchCatalog(
productCode, cc, cn, zone.zoneCode, discWeight, bizDate, zone.zone);
}
}
if (!hasProductCatalogPrice(discCatalog))
{ {
// catalogMissCode = Math.max(catalogMissCode, // catalogMissCode = Math.max(catalogMissCode,
// diagnoseCatalogMiss(productCode, cc, cn, zone.zoneCode, bizDate)); // diagnoseCatalogMiss(productCode, cc, cn, zone.zoneCode, bizDate));
continue; continue;
} }
TmsPriceQuoteResult hit = tryDiscount(bind, set, version, catalog, productCode, productName, weight, bizDate); TmsPriceQuoteResult hit = tryDiscount(bind, set, version, discCatalog,
productCode, productName, discWeight, bizDate);
if (hit != null) if (hit != null)
{ {
stampRequestMeta(request, hit); stampRequestMeta(request, hit);
@@ -971,6 +986,21 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
PrcPriceSetDetail detail = detailMapper.selectFixedMatch( PrcPriceSetDetail detail = detailMapper.selectFixedMatch(
psvId, productCode, psvId, productCode,
countryCode, countryName, zoneCode, weight); countryCode, countryName, zoneCode, weight);
BigDecimal billWeight = weight;
boolean snappedToMinBand = false;
// Miss: if charge weight is below the lowest band, bill at that band's weight_min.
if (detail == null)
{
PrcPriceSetDetail minBand = detailMapper.selectFixedMinBand(
psvId, productCode, countryCode, countryName, zoneCode);
if (minBand != null && minBand.getWeightMin() != null
&& weight.compareTo(minBand.getWeightMin()) < 0)
{
detail = minBand;
billWeight = minBand.getWeightMin().setScale(3, RoundingMode.HALF_UP);
snappedToMinBand = true;
}
}
if (detail == null) if (detail == null)
{ {
return null; return null;
@@ -979,16 +1009,55 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
{ {
return null; return null;
} }
TmsPriceQuoteResult r = buildFromPackageDetail(SRC_FIXED, detail, catalog, resolvedZone, BmsProductCountry useCatalog = catalog;
productCode, productName, weight, detail.getFvValue(), null, null, detail.getFvRegistrationFee(), bizDate); if (snappedToMinBand)
{
BmsProductCountry rematched = softMatchCatalog(
productCode, countryCode, countryName, zoneCode, billWeight, bizDate, resolvedZone);
if (hasProductCatalogPrice(rematched))
{
useCatalog = rematched;
}
}
TmsPriceQuoteResult r = buildFromPackageDetail(SRC_FIXED, detail, useCatalog, resolvedZone,
productCode, productName, billWeight, 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") String verLabel = version.getVersionNo() == null ? ("psv=" + psvId) : version.getVersionNo();
+ " [" + (version.getVersionNo() == null ? ("psv=" + psvId) : version.getVersionNo()) + "]"); String msg = MessageUtils.message("price.quote.hitCustomerFixedPackageDetail") + " [" + verLabel + "]";
if (snappedToMinBand)
{
msg = msg + "; chargeWeight raised to min band " + billWeight.toPlainString();
}
r.setMatchMessage(msg);
r.setCustomerId(bind.getCustomerId()); r.setCustomerId(bind.getCustomerId());
return r; return r;
} }
/**
* When weight misses catalog bands but is below the lowest band, return that band's weight_min.
* Otherwise null (caller keeps original miss).
*/
private BigDecimal snapWeightToCatalogMinBand(String productCode, String countryCode, String countryName,
String zoneCode, BigDecimal weight, Date bizDate)
{
if (weight == null || StringUtils.isEmpty(productCode))
{
return null;
}
BmsProductCountry minBand = productCountryMapper.selectMinBandByProductCountry(
productCode, countryCode, countryName, zoneCode, bizDate);
if (minBand == null || minBand.getWeightMin() == null)
{
return null;
}
if (weight.compareTo(minBand.getWeightMin()) < 0)
{
return minBand.getWeightMin().setScale(3, RoundingMode.HALF_UP);
}
return null;
}
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, Date bizDate) BmsProductCountry base, String productCode, String productName, BigDecimal weight, Date bizDate)
{ {
@@ -446,8 +446,7 @@ public class CatalogVersionSupport
} }
/** /**
* Catalog version that can accept preview apply / Excel attach / edit. * Catalog force-edit: draft, pending, effect, and expired are writable.
* Same as sales force-edit: draft, pending, and effect are writable.
*/ */
public PrcPriceSetVersion requireEditableCatalogVersion(Long psvId, String priceType) public PrcPriceSetVersion requireEditableCatalogVersion(Long psvId, String priceType)
{ {
@@ -467,9 +466,10 @@ public class CatalogVersionSupport
} }
if (!STATUS_DRAFT.equals(ver.getStatus()) if (!STATUS_DRAFT.equals(ver.getStatus())
&& !STATUS_PENDING.equals(ver.getStatus()) && !STATUS_PENDING.equals(ver.getStatus())
&& !STATUS_EFFECT.equals(ver.getStatus())) && !STATUS_EFFECT.equals(ver.getStatus())
&& !STATUS_EXPIRED.equals(ver.getStatus()))
{ {
throw new ServiceException("仅草稿、待生效或已生效版本可以编辑"); throw new ServiceException("仅草稿、待生效、已生效或已失效版本可以编辑");
} }
return ver; return ver;
} }
@@ -357,6 +357,36 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
limit 1 limit 1
</select> </select>
<select id="selectMinBandByProductCountry" resultMap="BmsProductCountryResult">
<include refid="selectVo"/>
where c.price_type = 'PRODUCT'
and ifnull(c.is_del, '0') = '0'
and c.product_code = #{productCode}
and c.weight_min is not null
<include refid="catalogBizWindow"/>
<choose>
<when test="countryCode != null and countryCode != ''">
and upper(c.country_code) = upper(#{countryCode})
</when>
<when test="countryName != null and countryName != ''">
and c.country_name = #{countryName}
</when>
<otherwise>
and 1 = 0
</otherwise>
</choose>
<choose>
<when test="zoneCode != null and zoneCode != ''">
and upper(ifnull(c.zone_code,'')) = upper(#{zoneCode})
</when>
<otherwise>
and (c.zone_code is null or c.zone_code = '')
</otherwise>
</choose>
order by v.effective_time desc, c.weight_min asc, c.psd_id asc
limit 1
</select>
<select id="countZonedByProductCountry" resultType="int"> <select id="countZonedByProductCountry" resultType="int">
select count(1) select count(1)
from prc_price_set_detail c from prc_price_set_detail c
@@ -490,6 +490,35 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
limit 1 limit 1
</select> </select>
<select id="selectFixedMinBand" resultMap="PrcPriceSetDetailResult">
<include refid="selectPrcPriceSetDetailVo"/>
where psv_id = #{psvId}
and ifnull(is_del, '0') = '0'
and product_code = #{productCode}
and weight_min is not null
<choose>
<when test="countryCode != null and countryCode != ''">
and upper(ifnull(country_code,'')) = upper(#{countryCode})
</when>
<when test="countryName != null and countryName != ''">
and country_name = #{countryName}
</when>
<otherwise>
and 1 = 0
</otherwise>
</choose>
<choose>
<when test="zoneCode != null and zoneCode != ''">
and upper(ifnull(zone_code,'')) = upper(#{zoneCode})
</when>
<otherwise>
and (zone_code is null or zone_code = '')
</otherwise>
</choose>
order by weight_min asc, psd_id asc
limit 1
</select>
<select id="countZonedByPsvProductCountry" resultType="int"> <select id="countZonedByPsvProductCountry" resultType="int">
select count(1) select count(1)
from prc_price_set_detail from prc_price_set_detail
@@ -81,6 +81,7 @@ import {
isDraftVersion, isDraftVersion,
isPendingVersion, isPendingVersion,
isEffectVersion, isEffectVersion,
isExpiredVersion,
isPriceEditableVersion isPriceEditableVersion
} from '@/views/price/set/utils/priceSet' } from '@/views/price/set/utils/priceSet'
import { checkPermi } from '@/utils/permission' import { checkPermi } from '@/utils/permission'
@@ -152,8 +153,8 @@ const filteredVersions = computed(() => {
function showEditButton(row) { function showEditButton(row) {
if (!checkPermi([props.editPerm])) return false if (!checkPermi([props.editPerm])) return false
// Align with sales force-edit: draft / pending / effect can edit // draft / pending / effect / expired can edit
return isPriceEditableVersion(row) || isEffectVersion(row) return isPriceEditableVersion(row) || isEffectVersion(row) || isExpiredVersion(row)
} }
function isRemoving(row) { function isRemoving(row) {
@@ -51,7 +51,7 @@ import { listProduct, listProductVersions, activateProductVersion, deleteProduct
const { proxy } = getCurrentInstance() const { proxy } = getCurrentInstance()
const alertTitle = "按版本管理产品价(与套餐版本一致)。点击版本可查看产品与明细;待生效(含草稿)可编辑、启动或硬删;已生效可软删(上一版自动恢复生效并清空失效时间)。建价请点「新建产品价格」。" const alertTitle = "按版本管理产品价(与套餐版本一致)。点击版本可查看产品与明细;待生效(含草稿)可编辑、启动或硬删;已生效、已失效可编辑;已生效可软删(上一版自动恢复生效并清空失效时间)。建价请点「新建产品价格」。"
const verTitle = '产品价版本' const verTitle = '产品价版本'
const createLabel = '新建产品价格' const createLabel = '新建产品价格'
@@ -51,7 +51,7 @@ import { listRedline, listRedlineVersions, activateRedlineVersion, deleteRedline
const { proxy } = getCurrentInstance() const { proxy } = getCurrentInstance()
const alertTitle = "按版本管理红线价(与套餐版本一致)。点击版本可查看产品与明细;待生效(含草稿)可编辑、启动或硬删;已生效可软删(上一版自动恢复生效并清空失效时间)。建价请点「新建红线价」。" const alertTitle = "按版本管理红线价(与套餐版本一致)。点击版本可查看产品与明细;待生效(含草稿)可编辑、启动或硬删;已生效、已失效可编辑;已生效可软删(上一版自动恢复生效并清空失效时间)。建价请点「新建红线价」。"
const verTitle = '红线价版本' const verTitle = '红线价版本'
const createLabel = '新建红线价' const createLabel = '新建红线价'
@@ -105,6 +105,10 @@ export function isEffectVersion(row) {
return !!row && String(row.status) === VER_STATUS.EFFECT return !!row && String(row.status) === VER_STATUS.EFFECT
} }
export function isExpiredVersion(row) {
return !!row && String(row.status) === VER_STATUS.EXPIRED
}
/** Draft or pending: may continue editing price details */ /** Draft or pending: may continue editing price details */
export function isPriceEditableVersion(row) { export function isPriceEditableVersion(row) {
return isDraftVersion(row) || isPendingVersion(row) return isDraftVersion(row) || isPendingVersion(row)