更新计费失败状态,新增销售价和产品价未匹配的错误信息,调整相关代码逻辑以支持新的状态处理。

This commit is contained in:
wuweihua
2026-10-27 15:24:16 +08:00
parent 0ba975656b
commit 4d4065b9e9
14 changed files with 189 additions and 68 deletions
@@ -195,7 +195,10 @@ public class BillingController extends BaseController
private static List<Integer> limitBillingListFailStatus(List<Integer> requested)
{
List<Integer> allowed = Arrays.asList(Integer.valueOf(0), Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3));
// Soft-fail list: success + sales 1/2/3 + product-catalog 23/24/25
List<Integer> allowed = Arrays.asList(
Integer.valueOf(0), Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3),
Integer.valueOf(23), Integer.valueOf(24), Integer.valueOf(25));
if (requested == null || requested.isEmpty())
{
return new ArrayList<Integer>(allowed);
@@ -216,9 +216,12 @@ price.set.detail.duplicate.fixed=产品{0} 国家{1} 重量段{2} {3}(共{4}
price.set.detail.duplicate.discount=产品{0}(共{1}条)
price.set.detail.duplicate.hasZone=有分区({0})
price.set.detail.duplicate.noZone=无分区
price.bill.billingFail.product=计费失败:未匹配产品
price.bill.billingFail.country=计费失败:未匹配国家
price.bill.billingFail.weight=计费失败:未匹配重量段
price.bill.billingFail.product=计费失败:销售价未匹配产品
price.bill.billingFail.country=计费失败:销售价未匹配国家
price.bill.billingFail.weight=计费失败:销售价未匹配重量段
price.bill.billingFail.catalogProduct=计费失败:产品价未匹配产品
price.bill.billingFail.catalogCountry=计费失败:产品价未匹配国家
price.bill.billingFail.catalogWeight=计费失败:产品价未匹配重量段
price.bill.export.filterRequired=请先设置列表查询条件后再导出
price.bill.export.taskNotExists=导出任务不存在或已过期
price.bill.export.tooMany=匹配订单超过上限 {0} 条,请缩小筛选范围
@@ -1715,9 +1715,12 @@ public class ArBillServiceImpl implements IArBillService {
}
return switch (status) {
case SUCCESS -> "";
case PRODUCT -> "未匹配产品";
case COUNTRY -> "未匹配国家";
case WEIGHT -> "未匹配重量段";
case PRODUCT -> "销售价未匹配产品";
case COUNTRY -> "销售价未匹配国家";
case WEIGHT -> "销售价未匹配重量段";
case CATALOG_PRODUCT -> "产品价未匹配产品";
case CATALOG_COUNTRY -> "产品价未匹配国家";
case CATALOG_WEIGHT -> "产品价未匹配重量段";
case NO_CUSTOMER_PACKAGE -> "未绑定套餐";
case NO_ACTIVE_PACKAGE_VERSION -> "无生效套餐版本";
case ALREADY_IN_AR_BILL -> "已生成应收账单";
@@ -122,7 +122,7 @@ public class PrcOrderBill extends BaseEntity
private String creditArrearsStatus;
/** NONE / REFUNDED */
private String refundStatus;
/** 计费失败状态:0成功/1未匹配产品/2未匹配国家/3未匹配重量段;软失败时金额为0且不落费用明细 */
/** Billing fail: 0 ok; 1/2/3 sales miss; 23/24/25 catalog miss; soft-fail amount=0, no fee rows */
private Integer billingFailStatus;
/** 失败原因 */
private String errorMessage;
@@ -103,7 +103,7 @@ public class TmsPriceQuoteResult implements Serializable
private String creditArrearsStatus;
/** NONE / REFUNDED */
private String refundStatus;
/** 计费失败状态:0成功/1未匹配产品/2未匹配国家/3未匹配重量段(仅上游同步计费软失败) */
/** Billing fail: 0 ok; 1/2/3 sales; 23/24/25 catalog (soft-fail on bill sync) */
private Integer billingFailStatus;
private Integer pieces;
private BigDecimal actualWeight;
@@ -1,10 +1,12 @@
package com.bms.price.enums;
/**
* prc_order_bill.billing_fail_status 计费失败状态(数字)。
* 0成功;1未匹配产品;2未匹配国家;3未匹配重量段。
* 4 起与 prc_billing_task.fail_code 一一对应。
* 仅 0/1/2/3 写入 prc_income_bill;仅 0 写入 prc_income_bill_fee。
* prc_order_bill.billing_fail_status numeric codes.
* 0 success;
* 1/2/3 sales-price (package) miss: product / country / weight;
* 23/24/25 product-catalog price miss: product / country / weight;
* 4+ map to prc_billing_task.fail_code.
* Soft-fail income bill: 0/1/2/3/23/24/25; fee rows only for 0.
*/
public enum BillBillingFailStatus
{
@@ -30,7 +32,13 @@ public enum BillBillingFailStatus
CUSTOMER_CODE_REQUIRED(19),
CUSTOMER_DISABLED(20),
FREIGHT_MISSING(21),
UNKNOWN(22);
UNKNOWN(22),
/** Product catalog (产品价) miss: no product rows. */
CATALOG_PRODUCT(23),
/** Product catalog miss: product hit but country/zone miss. */
CATALOG_COUNTRY(24),
/** Product catalog miss: product+country hit but weight band miss. */
CATALOG_WEIGHT(25);
private final int code;
@@ -65,11 +73,13 @@ public enum BillBillingFailStatus
return code != null && code.intValue() > SUCCESS.code;
}
/** 0/1/2/3 才落 prc_income_bill。 */
/** Soft-fail sales/catalog misses also persist prc_income_bill. */
public static boolean storesIncomeBill(Integer code)
{
int value = code == null ? SUCCESS.code : code.intValue();
return value == SUCCESS.code || value == PRODUCT.code || value == COUNTRY.code || value == WEIGHT.code;
return value == SUCCESS.code
|| value == PRODUCT.code || value == COUNTRY.code || value == WEIGHT.code
|| value == CATALOG_PRODUCT.code || value == CATALOG_COUNTRY.code || value == CATALOG_WEIGHT.code;
}
/** 仅 0 落 prc_income_bill_fee。 */
@@ -110,6 +120,12 @@ public enum BillBillingFailStatus
return BillTaskFailCode.COUNTRY_NOT_HIT.getCode();
case WEIGHT:
return BillTaskFailCode.WEIGHT_NOT_HIT.getCode();
case CATALOG_PRODUCT:
return BillTaskFailCode.CATALOG_PRODUCT_NOT_HIT.getCode();
case CATALOG_COUNTRY:
return BillTaskFailCode.CATALOG_COUNTRY_NOT_HIT.getCode();
case CATALOG_WEIGHT:
return BillTaskFailCode.CATALOG_WEIGHT_NOT_HIT.getCode();
case NO_CUSTOMER_PACKAGE:
return BillTaskFailCode.NO_CUSTOMER_PACKAGE.getCode();
case NO_ACTIVE_PACKAGE_VERSION:
@@ -12,12 +12,18 @@ public enum BillTaskFailCode
NO_ACTIVE_PACKAGE_VERSION("NO_ACTIVE_PACKAGE_VERSION"),
ALREADY_IN_AR_BILL("ALREADY_IN_AR_BILL"),
PRODUCT_ARCHIVE_MISSING("PRODUCT_ARCHIVE_MISSING"),
/** Soft-fail: product not in package (incl. after product_code_mapping). */
/** Soft-fail: sales price (package) product miss. */
PRODUCT_NOT_HIT("PRODUCT_NOT_HIT"),
/** Soft-fail: product hit but country/zone miss. */
/** Soft-fail: sales price country/zone miss. */
COUNTRY_NOT_HIT("COUNTRY_NOT_HIT"),
/** Soft-fail: product+country hit but weight band miss. */
/** Soft-fail: sales price weight band miss. */
WEIGHT_NOT_HIT("WEIGHT_NOT_HIT"),
/** Soft-fail: product catalog (产品价) product miss. */
CATALOG_PRODUCT_NOT_HIT("CATALOG_PRODUCT_NOT_HIT"),
/** Soft-fail: product catalog country/zone miss. */
CATALOG_COUNTRY_NOT_HIT("CATALOG_COUNTRY_NOT_HIT"),
/** Soft-fail: product catalog weight band miss. */
CATALOG_WEIGHT_NOT_HIT("CATALOG_WEIGHT_NOT_HIT"),
PRICE_NOT_HIT("PRICE_NOT_HIT"),
ZONE_PRICE_NOT_HIT("ZONE_PRICE_NOT_HIT"),
POSTCODE_REQUIRED("POSTCODE_REQUIRED"),
@@ -72,16 +78,29 @@ public enum BillTaskFailCode
{
return PRODUCT_ARCHIVE_MISSING.code;
}
// Soft-fail messages from price.bill.billingFail.* (order matters: weight/country before generic)
if (containsAny(msg, "未匹配重量段", "WEIGHT_NOT_HIT", "billingFail.weight"))
// Soft-fail: product catalog before sales (more specific Chinese prefix)
if (containsAny(msg, "产品价未匹配重量段", "CATALOG_WEIGHT_NOT_HIT", "billingFail.catalogWeight"))
{
return CATALOG_WEIGHT_NOT_HIT.code;
}
if (containsAny(msg, "产品价未匹配国家", "CATALOG_COUNTRY_NOT_HIT", "billingFail.catalogCountry"))
{
return CATALOG_COUNTRY_NOT_HIT.code;
}
if (containsAny(msg, "产品价未匹配产品", "CATALOG_PRODUCT_NOT_HIT", "billingFail.catalogProduct"))
{
return CATALOG_PRODUCT_NOT_HIT.code;
}
// Soft-fail sales price (order matters: weight/country before generic product)
if (containsAny(msg, "销售价未匹配重量段", "未匹配重量段", "WEIGHT_NOT_HIT", "billingFail.weight"))
{
return WEIGHT_NOT_HIT.code;
}
if (containsAny(msg, "未匹配国家", "COUNTRY_NOT_HIT", "billingFail.country"))
if (containsAny(msg, "销售价未匹配国家", "未匹配国家", "COUNTRY_NOT_HIT", "billingFail.country"))
{
return COUNTRY_NOT_HIT.code;
}
if (containsAny(msg, "未匹配产品", "PRODUCT_NOT_HIT", "billingFail.product", "productNotInPackage"))
if (containsAny(msg, "销售价未匹配产品", "未匹配产品", "PRODUCT_NOT_HIT", "billingFail.product", "productNotInPackage"))
{
return PRODUCT_NOT_HIT.code;
}
@@ -420,8 +420,9 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
boolean productExcludedBySetProducts = false;
boolean attemptedPriceMatch = false;
boolean anyZoneRequired = false;
// Highest specificity among soft-failable misses: WEIGHT(3) > COUNTRY(2) > PRODUCT(1).
int missCode = 0;
// Sales miss: WEIGHT(3) > COUNTRY(2) > PRODUCT(1). Catalog miss: 25 > 24 > 23.
int salesMissCode = 0;
int catalogMissCode = 0;
// 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)
@@ -467,7 +468,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
if (anyProducts != null && !anyProducts.isEmpty())
{
productExcludedBySetProducts = true;
missCode = Math.max(missCode, BillBillingFailStatus.PRODUCT.getCode());
salesMissCode = Math.max(salesMissCode, BillBillingFailStatus.PRODUCT.getCode());
continue;
}
}
@@ -497,23 +498,26 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
productCode, productName, cc, cn, zone.zoneCode, weight, bizDate);
if (hit != null)
{
// Billing requires product catalog price (产品价); package hit alone is not enough.
if (request instanceof TmsPriceBillRequest && !hasProductCatalogPrice(catalog))
{
missCode = Math.max(missCode,
diagnoseCatalogMiss(productCode, cc, cn, zone.zoneCode, bizDate));
continue;
}
// Temporarily skip product-catalog (产品价) soft-fail; sales package hit is enough.
// if (request instanceof TmsPriceBillRequest && !hasProductCatalogPrice(catalog))
// {
// catalogMissCode = Math.max(catalogMissCode,
// diagnoseCatalogMiss(productCode, cc, cn, zone.zoneCode, bizDate));
// continue;
// }
stampRequestMeta(request, hit);
return hit;
}
missCode = Math.max(missCode, diagnoseFixedMiss(version.getPsvId(), productCode, cc, cn, zone.zoneCode));
salesMissCode = Math.max(salesMissCode,
diagnoseFixedMiss(version.getPsvId(), productCode, cc, cn, zone.zoneCode));
}
else if (TYPE_DISCOUNT.equals(setType))
{
// DISCOUNT still needs catalog as base rate; do not soft-fail as 产品价未匹配 for now.
if (!hasProductCatalogPrice(catalog))
{
missCode = Math.max(missCode, diagnoseCatalogMiss(productCode, cc, cn, zone.zoneCode, bizDate));
// catalogMissCode = Math.max(catalogMissCode,
// diagnoseCatalogMiss(productCode, cc, cn, zone.zoneCode, bizDate));
continue;
}
TmsPriceQuoteResult hit = tryDiscount(bind, set, version, catalog, productCode, productName, weight, bizDate);
@@ -523,7 +527,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
return hit;
}
// Discount detail missing for product (or invalid discount) after catalog hit.
missCode = Math.max(missCode, BillBillingFailStatus.PRODUCT.getCode());
salesMissCode = Math.max(salesMissCode, BillBillingFailStatus.PRODUCT.getCode());
}
}
@@ -536,8 +540,10 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
}
if (!attemptedPriceMatch && productExcludedBySetProducts)
{
missCode = Math.max(missCode, BillBillingFailStatus.PRODUCT.getCode());
salesMissCode = Math.max(salesMissCode, BillBillingFailStatus.PRODUCT.getCode());
}
// Prefer product-catalog miss when present (sales may already have hit).
int missCode = catalogMissCode > 0 ? catalogMissCode : salesMissCode;
if (request instanceof TmsPriceBillRequest && BillBillingFailStatus.isFail(Integer.valueOf(missCode)))
{
return buildBillingMissResult(request, productCode, productName, countryCode, countryName,
@@ -591,27 +597,27 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
}
/**
* Diagnose catalog miss for DISCOUNT base price.
* Diagnose product-catalog (产品价) miss: product absent -> 23; country/zone -> 24; else weight -> 25.
*/
private int diagnoseCatalogMiss(String productCode, String countryCode, String countryName,
String zoneCode, Date bizDate)
{
if (StringUtils.isEmpty(productCode))
{
return BillBillingFailStatus.PRODUCT.getCode();
return BillBillingFailStatus.CATALOG_PRODUCT.getCode();
}
int byProduct = productCountryMapper.countByProduct(productCode, bizDate);
if (byProduct <= 0)
{
return BillBillingFailStatus.PRODUCT.getCode();
return BillBillingFailStatus.CATALOG_PRODUCT.getCode();
}
int byCountry = productCountryMapper.countByProductCountryZone(
productCode, countryCode, countryName, zoneCode, bizDate);
if (byCountry <= 0)
{
return BillBillingFailStatus.COUNTRY.getCode();
return BillBillingFailStatus.CATALOG_COUNTRY.getCode();
}
return BillBillingFailStatus.WEIGHT.getCode();
return BillBillingFailStatus.CATALOG_WEIGHT.getCode();
}
/**
@@ -675,6 +681,18 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
{
return MessageUtils.message("price.bill.billingFail.weight");
}
if (missCode == BillBillingFailStatus.CATALOG_PRODUCT.getCode())
{
return MessageUtils.message("price.bill.billingFail.catalogProduct");
}
if (missCode == BillBillingFailStatus.CATALOG_COUNTRY.getCode())
{
return MessageUtils.message("price.bill.billingFail.catalogCountry");
}
if (missCode == BillBillingFailStatus.CATALOG_WEIGHT.getCode())
{
return MessageUtils.message("price.bill.billingFail.catalogWeight");
}
return MessageUtils.message("price.bill.packagePriceNotHit");
}
@@ -239,9 +239,12 @@ public final class OrderBillExportSupport
switch (status.intValue())
{
case 0: return "计费成功";
case 1: return "未匹配产品";
case 2: return "未匹配国家";
case 3: return "未匹配重量段";
case 1: return "销售价未匹配产品";
case 2: return "销售价未匹配国家";
case 3: return "销售价未匹配重量段";
case 23: return "产品价未匹配产品";
case 24: return "产品价未匹配国家";
case 25: return "产品价未匹配重量段";
case 4: return "未绑定套餐";
case 5: return "无生效套餐版本";
case 6: return "已生成应收账单";