新增订单计费失败原因的价格周期字段,并优化前端展示,支持查看价格周期详情。

This commit is contained in:
wuweihua
2026-11-11 13:12:32 +08:00
parent 2430e344a0
commit 0545de88b4
6 changed files with 439 additions and 28 deletions
@@ -35,6 +35,12 @@ public class OrderBillFailReasonVo implements Serializable
private String setType; private String setType;
private Long psvId; private Long psvId;
private String versionNo; private String versionNo;
/** Price-cycle version effective time. */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date effectiveTime;
/** Price-cycle version expire time; null = open. */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date expireTime;
private Long psdId; private Long psdId;
private BigDecimal weight; private BigDecimal weight;
@@ -46,6 +46,7 @@ import com.bms.customer.customerinfo.mapper.BmsCustomerMapper;
import com.bms.system.mapper.SysUserMapper; import com.bms.system.mapper.SysUserMapper;
import com.bms.system.service.IBmsBizRuleService; import com.bms.system.service.IBmsBizRuleService;
import com.bms.system.service.IBmsProductCodeMappingService; import com.bms.system.service.IBmsProductCodeMappingService;
import com.bms.price.domain.PrcCustomerPriceSet;
import com.bms.price.domain.PrcIncomeBill; import com.bms.price.domain.PrcIncomeBill;
import com.bms.price.domain.PrcIncomeBillFee; import com.bms.price.domain.PrcIncomeBillFee;
import com.bms.price.domain.PrcOrderBill; import com.bms.price.domain.PrcOrderBill;
@@ -53,6 +54,7 @@ import com.bms.price.domain.PrcOrderBillExportTask;
import com.bms.price.domain.PrcOrderBillExt; import com.bms.price.domain.PrcOrderBillExt;
import com.bms.price.domain.PrcPayBill; import com.bms.price.domain.PrcPayBill;
import com.bms.price.domain.PrcPayBillFee; import com.bms.price.domain.PrcPayBillFee;
import com.bms.price.domain.PrcPriceSet;
import com.bms.price.domain.PrcSurcharge; import com.bms.price.domain.PrcSurcharge;
import com.bms.price.domain.BatchAddIncomeBillFeeTask; import com.bms.price.domain.BatchAddIncomeBillFeeTask;
import com.bms.price.domain.ChargeWeightImportTask; import com.bms.price.domain.ChargeWeightImportTask;
@@ -83,11 +85,13 @@ import com.bms.price.enums.BillOrderStatus;
import com.bms.price.enums.BillRefundStatus; import com.bms.price.enums.BillRefundStatus;
import com.bms.price.enums.BillBillingFailStatus; import com.bms.price.enums.BillBillingFailStatus;
import com.bms.price.enums.TmsOrderBillMatchType; import com.bms.price.enums.TmsOrderBillMatchType;
import com.bms.price.mapper.PrcCustomerPriceSetMapper;
import com.bms.price.mapper.PrcIncomeBillFeeMapper; import com.bms.price.mapper.PrcIncomeBillFeeMapper;
import com.bms.price.mapper.PrcIncomeBillMapper; import com.bms.price.mapper.PrcIncomeBillMapper;
import com.bms.price.mapper.PrcOrderBillExtMapper; import com.bms.price.mapper.PrcOrderBillExtMapper;
import com.bms.price.mapper.PrcOrderBillMapper; import com.bms.price.mapper.PrcOrderBillMapper;
import com.bms.price.mapper.PrcPriceSetDetailMapper; import com.bms.price.mapper.PrcPriceSetDetailMapper;
import com.bms.price.mapper.PrcPriceSetMapper;
import com.bms.price.mapper.PrcPriceSetVersionMapper; import com.bms.price.mapper.PrcPriceSetVersionMapper;
import com.bms.price.domain.PrcPriceSetDetail; import com.bms.price.domain.PrcPriceSetDetail;
import com.bms.price.domain.PrcPriceSetVersion; import com.bms.price.domain.PrcPriceSetVersion;
@@ -132,6 +136,12 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
@Autowired @Autowired
private PrcPriceSetDetailMapper priceSetDetailMapper; private PrcPriceSetDetailMapper priceSetDetailMapper;
@Autowired
private PrcCustomerPriceSetMapper customerPriceSetMapper;
@Autowired
private PrcPriceSetMapper priceSetMapper;
/** TMS 实时状态查询使用客户主档案,不读取订单上的历史状态快照。 */ /** TMS 实时状态查询使用客户主档案,不读取订单上的历史状态快照。 */
@Autowired @Autowired
private BmsCustomerMapper bmsCustomerMapper; private BmsCustomerMapper bmsCustomerMapper;
@@ -498,9 +508,11 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
PrcPriceSetVersion ver = priceSetVersionMapper.selectPrcPriceSetVersionById(bill.getPsvId()); PrcPriceSetVersion ver = priceSetVersionMapper.selectPrcPriceSetVersionById(bill.getPsvId());
if (ver != null) if (ver != null)
{ {
vo.setVersionNo(ver.getVersionNo()); applyFailReasonVersion(vo, ver);
} }
} }
// Soft-fail rows often leave package/version empty on the bill; resolve from customer bind.
fillFailReasonPackageVersionFromBind(vo, bill);
if (bill.getPsdId() != null && priceSetDetailMapper != null) if (bill.getPsdId() != null && priceSetDetailMapper != null)
{ {
PrcPriceSetDetail detail = priceSetDetailMapper.selectPrcPriceSetDetailById(bill.getPsdId()); PrcPriceSetDetail detail = priceSetDetailMapper.selectPrcPriceSetDetailById(bill.getPsdId());
@@ -512,10 +524,6 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
} }
String reason = bill.getErrorMessage(); String reason = bill.getErrorMessage();
if (StringUtils.isEmpty(reason)) if (StringUtils.isEmpty(reason))
{
reason = bill.getMatchMessage();
}
if (StringUtils.isEmpty(reason))
{ {
reason = vo.getBillingFailStatusLabel(); reason = vo.getBillingFailStatusLabel();
} }
@@ -524,6 +532,141 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
return vo; return vo;
} }
/**
* When order-bill package / version is empty, do not keep empty snapshot fields.
* Resolve package from customer bind; match version by pickupDate then bizDate.
* Leave empty when still not found.
*/
private void fillFailReasonPackageVersionFromBind(OrderBillFailReasonVo vo, PrcOrderBill bill)
{
if (vo == null || bill == null)
{
return;
}
boolean needPackage = bill.getPriceSetId() == null
&& StringUtils.isEmpty(bill.getPriceSetCode())
&& StringUtils.isEmpty(bill.getPriceSetName());
boolean needVersion = bill.getPsvId() == null && StringUtils.isEmpty(vo.getVersionNo());
if (!needPackage && !needVersion)
{
return;
}
if (needPackage)
{
vo.setPriceSetId(null);
vo.setPriceSetCode(null);
vo.setPriceSetName(null);
vo.setSetType(null);
}
if (needVersion)
{
vo.setPsvId(null);
vo.setVersionNo(null);
vo.setEffectiveTime(null);
vo.setExpireTime(null);
}
Long priceSetId = needPackage ? null : bill.getPriceSetId();
PrcPriceSet set = null;
PrcPriceSetVersion version = null;
if (priceSetId != null)
{
set = priceSetMapper == null ? null : priceSetMapper.selectPrcPriceSetById(priceSetId);
version = needVersion ? selectVersionByPickupOrBiz(priceSetId, bill) : null;
}
else if (needPackage || needVersion)
{
if (StringUtils.isEmpty(bill.getCustomerCode()) || customerPriceSetMapper == null
|| priceSetMapper == null || priceSetVersionMapper == null)
{
return;
}
List<PrcCustomerPriceSet> binds =
customerPriceSetMapper.selectEffectiveByCustomerCode(bill.getCustomerCode().trim());
if (binds == null || binds.isEmpty())
{
return;
}
for (PrcCustomerPriceSet bind : binds)
{
if (bind == null || bind.getPriceSetId() == null)
{
continue;
}
PrcPriceSet candidate = priceSetMapper.selectPrcPriceSetById(bind.getPriceSetId());
if (candidate == null || !"E".equals(candidate.getStatus()))
{
continue;
}
String category = StringUtils.trim(candidate.getSetCategory());
if (PrcPriceSet.CATEGORY_LABEL.equalsIgnoreCase(category))
{
if (set == null)
{
set = candidate;
}
continue;
}
PrcPriceSetVersion hit = selectVersionByPickupOrBiz(candidate.getPriceSetId(), bill);
if (set == null)
{
set = candidate;
}
if (hit != null)
{
set = candidate;
version = hit;
break;
}
}
}
if (needPackage && set != null)
{
vo.setPriceSetId(set.getPriceSetId());
vo.setPriceSetCode(set.getPriceSetCode());
vo.setPriceSetName(set.getPriceSetName());
vo.setSetType(set.getSetType());
}
if (needVersion && version != null)
{
applyFailReasonVersion(vo, version);
}
}
private static void applyFailReasonVersion(OrderBillFailReasonVo vo, PrcPriceSetVersion ver)
{
if (vo == null || ver == null)
{
return;
}
vo.setPsvId(ver.getPsvId());
vo.setVersionNo(ver.getVersionNo());
vo.setEffectiveTime(ver.getEffectiveTime());
vo.setExpireTime(ver.getExpireTime());
}
/** Prefer pickupDate, then bizDate, for sales package version window match. */
private PrcPriceSetVersion selectVersionByPickupOrBiz(Long priceSetId, PrcOrderBill bill)
{
if (priceSetId == null || priceSetVersionMapper == null || bill == null)
{
return null;
}
Date[] asOfList = new Date[] { bill.getPickupDate(), bill.getBizDate() };
for (Date asOf : asOfList)
{
if (asOf == null)
{
continue;
}
PrcPriceSetVersion ver = priceSetVersionMapper.selectActiveByPriceSetAt(priceSetId, asOf);
if (ver != null)
{
return ver;
}
}
return null;
}
@Override @Override
public TmsPriceQuoteResult getBillByOrderNo(String orderNo) public TmsPriceQuoteResult getBillByOrderNo(String orderNo)
{ {
@@ -1918,8 +2061,9 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
bill.setWeight(weight); bill.setWeight(weight);
bill.setBizDate(bizDate); bill.setBizDate(bizDate);
bill.setTmsId(request.getTmsId() == null ? null : Long.valueOf(request.getTmsId())); bill.setTmsId(request.getTmsId() == null ? null : Long.valueOf(request.getTmsId()));
bill.setMatchSource(result.getMatchSource()); // Do not persist match_source / match_message on order bill.
bill.setMatchMessage(result.getMatchMessage()); bill.setMatchSource(null);
bill.setMatchMessage(null);
bill.setPriceSetId(result.getPriceSetId()); bill.setPriceSetId(result.getPriceSetId());
bill.setPriceSetCode(result.getPriceSetCode()); bill.setPriceSetCode(result.getPriceSetCode());
bill.setPriceSetName(result.getPriceSetName()); bill.setPriceSetName(result.getPriceSetName());
@@ -3512,6 +3656,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
failText = failText.substring(0, 1000); failText = failText.substring(0, 1000);
} }
bill.setErrorMessage(failText); bill.setErrorMessage(failText);
bill.setMatchMessage(failText); bill.setMatchMessage(null);
bill.setMatchSource(null);
} }
} }
@@ -19,14 +19,14 @@ public final class BillingFailReasonSupport
static static
{ {
TEMPLATES.put(Integer.valueOf(1), "\u3010\u9500\u552e\u4ef7\u672a\u5339\u914d\u4ea7\u54c1\u3011\n\u8ba2\u5355 {orderNo}\uff08\u8f6c\u5355\u53f7 {transferNo}\uff09\uff0c\u5ba2\u6237 {customer}\uff0c\u4ea7\u54c1 {product}\uff0c\u56fd\u5bb6 {country}\uff0c\u8ba1\u8d39\u65f6\u95f4 {bizDate}\u3002\n\u5728\u5ba2\u6237\u9500\u552e\u4ef7\u5957\u9910\u300c{priceSet}\u300d\u4e2d\u672a\u627e\u5230\u8be5\u4ea7\u54c1\uff0c\u8bf7\u68c0\u67e5\u5957\u9910\u4ea7\u54c1\u8303\u56f4\u6216\u4ea7\u54c1\u4ee3\u7801\u6620\u5c04\u3002"); TEMPLATES.put(Integer.valueOf(1), "【销售价未匹配产品】\n订单 {orderNo}(转单号 {transferNo}),客户 {customer},产品 {product},国家 {country},计费时间 {bizDate}。\n在客户销售价套餐「{priceSet}」中未找到该产品,请检查套餐产品范围或产品代码映射。");
TEMPLATES.put(Integer.valueOf(2), "\u3010\u9500\u552e\u4ef7\u672a\u5339\u914d\u56fd\u5bb6\u3011\n\u8ba2\u5355 {orderNo}\uff08\u8f6c\u5355\u53f7 {transferNo}\uff09\uff0c\u5ba2\u6237 {customer}\uff0c\u4ea7\u54c1 {product}\uff0c\u76ee\u7684\u56fd\u5bb6 {country}\uff0c\u90ae\u7f16 {postcode}\uff0c\u8ba1\u8d39\u65f6\u95f4 {bizDate}\u3002\n\u5957\u9910\u300c{priceSet}\u300d\u7248\u672c {version}\u5df2\u547d\u4e2d\u4ea7\u54c1\uff0c\u4f46\u672a\u547d\u4e2d\u56fd\u5bb6/\u5206\u533a\uff0c\u8bf7\u8865\u9f50\u56fd\u5bb6\u4e0e\u5206\u533a\u4ef7\u683c\u3002"); TEMPLATES.put(Integer.valueOf(2), "【销售价未匹配国家】\n订单 {orderNo}(转单号 {transferNo}),客户 {customer},产品 {product},目的国家 {country},邮编 {postcode},计费时间 {bizDate}。\n套餐「{priceSet}」版本 {version}已命中产品,但未命中国家/分区,请补齐国家与分区价格。");
TEMPLATES.put(Integer.valueOf(3), "\u3010\u9500\u552e\u4ef7\u672a\u5339\u914d\u91cd\u91cf\u6bb5\u3011\n\u8ba2\u5355 {orderNo}\uff08\u8f6c\u5355\u53f7 {transferNo}\uff09\uff0c\u5ba2\u6237 {customer}\uff0c\u4ea7\u54c1 {product}\uff0c\u56fd\u5bb6 {country}\uff0c\u5206\u533a {zone}\uff0c\u8ba1\u8d39\u91cd {chargeWeight} kg\uff0c\u8ba1\u8d39\u65f6\u95f4 {bizDate}\u3002\n\u5957\u9910\u300c{priceSet}\u300d\u7248\u672c {version}\u5df2\u547d\u4e2d\u4ea7\u54c1\u4e0e\u56fd\u5bb6\uff0c\u4f46\u8ba1\u8d39\u91cd\u672a\u843d\u5728\u4ef7\u683c\u91cd\u91cf\u6bb5\u5185\uff0c\u8bf7\u68c0\u67e5\u91cd\u91cf\u6bb5\u8bbe\u7f6e\u3002"); TEMPLATES.put(Integer.valueOf(3), "【销售价未匹配重量段】\n订单 {orderNo}(转单号 {transferNo}),客户 {customer},产品 {product},国家 {country},分区 {zone},计费重 {chargeWeight} kg,计费时间 {bizDate}。\n套餐「{priceSet}」版本 {version}已命中产品与国家,但计费重未落在价格重量段内,请检查重量段设置。");
TEMPLATES.put(Integer.valueOf(23), "\u3010\u4ea7\u54c1\u4ef7\u672a\u5339\u914d\u4ea7\u54c1\u3011\n\u8ba2\u5355 {orderNo}\uff08\u8f6c\u5355\u53f7 {transferNo}\uff09\uff0c\u5ba2\u6237 {customer}\uff0c\u4ea7\u54c1 {product}\uff0c\u56fd\u5bb6 {country}\uff0c\u8ba1\u8d39\u65f6\u95f4 {bizDate}\u3002\n\u4ea7\u54c1\u4ef7\u76ee\u5f55\u4e2d\u672a\u627e\u5230\u8be5\u4ea7\u54c1\uff0c\u8bf7\u68c0\u67e5\u4ea7\u54c1\u4ef7\u6863\u6848\u662f\u5426\u5df2\u7ef4\u62a4\u3002"); TEMPLATES.put(Integer.valueOf(23), "【产品价未匹配产品】\n订单 {orderNo}(转单号 {transferNo}),客户 {customer},产品 {product},国家 {country},计费时间 {bizDate}。\n产品价目录中未找到该产品,请检查产品价档案是否已维护。");
TEMPLATES.put(Integer.valueOf(24), "\u3010\u4ea7\u54c1\u4ef7\u672a\u5339\u914d\u56fd\u5bb6\u3011\n\u8ba2\u5355 {orderNo}\uff08\u8f6c\u5355\u53f7 {transferNo}\uff09\uff0c\u4ea7\u54c1 {product}\uff0c\u76ee\u7684\u56fd\u5bb6 {country}\uff0c\u90ae\u7f16 {postcode}\uff0c\u8ba1\u8d39\u65f6\u95f4 {bizDate}\u3002\n\u4ea7\u54c1\u4ef7\u5df2\u547d\u4e2d\u4ea7\u54c1\uff0c\u4f46\u672a\u547d\u4e2d\u56fd\u5bb6/\u5206\u533a\uff0c\u8bf7\u8865\u9f50\u4ea7\u54c1\u4ef7\u56fd\u5bb6\u4ef7\u683c\u3002"); TEMPLATES.put(Integer.valueOf(24), "【产品价未匹配国家】\n订单 {orderNo}(转单号 {transferNo}),产品 {product},目的国家 {country},邮编 {postcode},计费时间 {bizDate}。\n产品价已命中产品,但未命中国家/分区,请补齐产品价国家价格。");
TEMPLATES.put(Integer.valueOf(25), "\u3010\u4ea7\u54c1\u4ef7\u672a\u5339\u914d\u91cd\u91cf\u6bb5\u3011\n\u8ba2\u5355 {orderNo}\uff08\u8f6c\u5355\u53f7 {transferNo}\uff09\uff0c\u4ea7\u54c1 {product}\uff0c\u56fd\u5bb6 {country}\uff0c\u5206\u533a {zone}\uff0c\u8ba1\u8d39\u91cd {chargeWeight} kg\uff0c\u8ba1\u8d39\u65f6\u95f4 {bizDate}\u3002\n\u4ea7\u54c1\u4ef7\u5df2\u547d\u4e2d\u4ea7\u54c1\u4e0e\u56fd\u5bb6\uff0c\u4f46\u8ba1\u8d39\u91cd\u672a\u843d\u5728\u91cd\u91cf\u6bb5\u5185\uff0c\u8bf7\u68c0\u67e5\u4ea7\u54c1\u4ef7\u91cd\u91cf\u6bb5\u3002"); TEMPLATES.put(Integer.valueOf(25), "【产品价未匹配重量段】\n订单 {orderNo}(转单号 {transferNo}),产品 {product},国家 {country},分区 {zone},计费重 {chargeWeight} kg,计费时间 {bizDate}。\n产品价已命中产品与国家,但计费重未落在重量段内,请检查产品价重量段。");
TEMPLATES.put(Integer.valueOf(4), "\u3010\u672a\u7ed1\u5b9a\u5957\u9910\u3011\n\u8ba2\u5355 {orderNo}\uff08\u8f6c\u5355\u53f7 {transferNo}\uff09\uff0c\u5ba2\u6237 {customer}\uff0c\u8ba1\u8d39\u65f6\u95f4 {bizDate}\u3002\n\u8be5\u5ba2\u6237\u672a\u7ed1\u5b9a\u9500\u552e\u4ef7\u5957\u9910\uff0c\u65e0\u6cd5\u6309\u5957\u9910\u53d6\u4ef7\uff0c\u8bf7\u5148\u5728\u5ba2\u6237/\u5957\u9910\u7ed1\u5b9a\u540e\u518d\u8ba1\u8d39\u3002"); TEMPLATES.put(Integer.valueOf(4), "【未绑定套餐】\n订单 {orderNo}(转单号 {transferNo}),客户 {customer},计费时间 {bizDate}。\n该客户未绑定销售价套餐,无法按套餐取价,请先在客户/套餐绑定后再计费。");
TEMPLATES.put(Integer.valueOf(5), "\u3010\u5957\u9910\u672a\u542f\u7528/\u65e0\u751f\u6548\u7248\u672c\u3011\n\u8ba2\u5355 {orderNo}\uff08\u8f6c\u5355\u53f7 {transferNo}\uff09\uff0c\u5ba2\u6237 {customer}\uff0c\u5957\u9910\u300c{priceSet}\u300d\uff0c\u8ba1\u8d39\u65f6\u95f4 {bizDate}\u3002\n\u8ba1\u8d39\u65e5\u6ca1\u6709\u53ef\u7528\u7684\u5957\u9910\u751f\u6548\u7248\u672c\uff0c\u8bf7\u68c0\u67e5\u5957\u9910\u542f\u7528\u72b6\u6001\u4e0e\u7248\u672c\u751f\u6548\u671f\u3002"); TEMPLATES.put(Integer.valueOf(5), "【套餐未启用/无生效版本】\n订单 {orderNo}(转单号 {transferNo}),客户 {customer},套餐「{priceSet}」,计费时间 {bizDate}。\n计费日没有可用的套餐生效版本,请检查套餐启用状态与版本生效期。");
} }
public static String buildDetail(OrderBillFailReasonVo vo, String fallback) public static String buildDetail(OrderBillFailReasonVo vo, String fallback)
@@ -40,13 +40,13 @@ public final class BillingFailReasonSupport
if (StringUtils.isEmpty(tpl)) if (StringUtils.isEmpty(tpl))
{ {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("\u3010").append(nz(vo.getBillingFailStatusLabel(), "\u8ba1\u8d39\u5931\u8d25")).append("\u3011\n"); sb.append("【").append(nz(vo.getBillingFailStatusLabel(), "计费失败")).append("】\n");
sb.append("\u8ba2\u5355 ").append(nz(vo.getOrderNo(), "-")); sb.append("订单 ").append(nz(vo.getOrderNo(), "-"));
sb.append("\uff08\u8f6c\u5355\u53f7 ").append(nz(vo.getTransferNo(), "-")).append("\uff09"); sb.append("(转单号 ").append(nz(vo.getTransferNo(), "-")).append(")");
sb.append("\uff0c\u5ba2\u6237 ").append(joinCodeName(vo.getCustomerCode(), vo.getCustomerName())); sb.append(",客户 ").append(joinCodeName(vo.getCustomerCode(), vo.getCustomerName()));
sb.append("\uff0c\u4ea7\u54c1 ").append(joinCodeName(vo.getProductCode(), vo.getProductName())); sb.append(",产品 ").append(joinCodeName(vo.getProductCode(), vo.getProductName()));
sb.append("\uff0c\u56fd\u5bb6 ").append(joinCodeName(vo.getCountryCode(), vo.getCountryName())); sb.append(",国家 ").append(joinCodeName(vo.getCountryCode(), vo.getCountryName()));
sb.append("\uff0c\u8ba1\u8d39\u65f6\u95f4 ").append(fmtDate(vo.getBizDate())).append("\u3002\n"); sb.append(",计费时间 ").append(fmtDate(vo.getBizDate())).append("。\n");
sb.append(nz(fallback, nz(vo.getErrorMessage(), vo.getBillingFailStatusLabel()))); sb.append(nz(fallback, nz(vo.getErrorMessage(), vo.getBillingFailStatusLabel())));
return sb.toString(); return sb.toString();
} }
@@ -60,7 +60,7 @@ public final class BillingFailReasonSupport
ctx.put("zone", joinCodeName(vo.getZoneCode(), vo.getZoneName())); ctx.put("zone", joinCodeName(vo.getZoneCode(), vo.getZoneName()));
ctx.put("postcode", nz(vo.getDestPostcode(), "-")); ctx.put("postcode", nz(vo.getDestPostcode(), "-"));
ctx.put("priceSet", joinCodeName(vo.getPriceSetCode(), vo.getPriceSetName())); ctx.put("priceSet", joinCodeName(vo.getPriceSetCode(), vo.getPriceSetName()));
ctx.put("version", nz(vo.getVersionNo(), "-")); ctx.put("version", formatPriceCycle(vo.getEffectiveTime(), vo.getExpireTime(), vo.getVersionNo()));
ctx.put("chargeWeight", fmtNum(vo.getChargeWeight())); ctx.put("chargeWeight", fmtNum(vo.getChargeWeight()));
ctx.put("bizDate", fmtDate(vo.getBizDate())); ctx.put("bizDate", fmtDate(vo.getBizDate()));
ctx.put("pickupDate", fmtDate(vo.getPickupDate())); ctx.put("pickupDate", fmtDate(vo.getPickupDate()));
@@ -71,7 +71,7 @@ public final class BillingFailReasonSupport
} }
if (StringUtils.isNotEmpty(fallback) && !fallback.equals(vo.getBillingFailStatusLabel())) if (StringUtils.isNotEmpty(fallback) && !fallback.equals(vo.getBillingFailStatusLabel()))
{ {
out = out + "\n\u8be6\u60c5\uff1a" + fallback; out = out + "\n详情:" + fallback;
} }
return out; return out;
} }
@@ -93,6 +93,15 @@ public final class BillingFailReasonSupport
return code + "/" + name; return code + "/" + name;
} }
private static String formatPriceCycle(Date effectiveTime, Date expireTime, String versionNo)
{
if (effectiveTime == null && expireTime == null)
{
return nz(versionNo, "-");
}
return fmtDate(effectiveTime) + " ~ " + fmtDate(expireTime);
}
private static String fmtDate(Date d) private static String fmtDate(Date d)
{ {
if (d == null) if (d == null)
@@ -0,0 +1,165 @@
<template>
<el-drawer
:model-value="modelValue"
:title="drawerTitle"
size="720px"
append-to-body
destroy-on-close
@close="close"
>
<div v-loading="loading" class="cycle-body">
<el-empty v-if="!loading && !versions.length" :description="emptyText" :image-size="72" />
<el-table v-else :data="versions" border height="calc(100vh - 220px)">
<el-table-column :label="colVer" prop="versionNo" width="90" align="center" />
<el-table-column :label="colEff" min-width="170">
<template #default="{ row }">{{ row.effectiveTime || '-' }}</template>
</el-table-column>
<el-table-column :label="colExp" min-width="170">
<template #default="{ row }">{{ row.expireTime || '-' }}</template>
</el-table-column>
<el-table-column :label="colStatus" width="100" align="center">
<template #default="{ row }">
<el-tag :type="versionStatusTag(row.status)" size="small">{{ versionStatusLabel(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column :label="colRows" width="90" align="center">
<template #default="{ row }">{{ row.detailCount != null ? row.detailCount : '-' }}</template>
</el-table-column>
<el-table-column :label="colAction" width="100" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openDetail(row)">查看</el-button>
</template>
</el-table-column>
</el-table>
</div>
<template #footer>
<el-button @click="close">关闭</el-button>
</template>
<set-version-detail-drawer
v-model="detailVisible"
:version="detailVersion"
:set-type="setType || 'FIXED'"
/>
</el-drawer>
</template>
<script setup name="OrderBillPriceCycleListDrawer">
import { priceSetApi } from '@/api/price/set'
import { exclusiveQuoteApi } from '@/api/price/exclusive'
import { versionStatusLabel, versionStatusTag } from '../../set/utils/priceSet'
import SetVersionDetailDrawer from '../../set/components/detail/SetVersionDetailDrawer.vue'
const props = defineProps({
modelValue: { type: Boolean, default: false },
priceSetId: { type: [Number, String], default: null },
priceSetCode: { type: String, default: '' },
priceSetName: { type: String, default: '' },
setType: { type: String, default: 'FIXED' }
})
const emit = defineEmits(['update:modelValue'])
const loading = ref(false)
const versions = ref([])
const detailVisible = ref(false)
const detailVersion = ref(null)
const emptyText = '暂无价格周期'
const colVer = '版本号'
const colEff = '生效时间'
const colExp = '失效时间'
const colStatus = '状态'
const colRows = '明细行数'
const colAction = '操作'
const drawerTitle = computed(() => {
const code = (props.priceSetCode || '').trim()
const name = (props.priceSetName || '').trim()
let pkg = '-'
if (code && name && code !== name) pkg = code + ' / ' + name
else if (code || name) pkg = code || name
return '价格周期列表 · ' + pkg
})
function close() {
emit('update:modelValue', false)
}
function toEffMs(v) {
if (!v || !v.effectiveTime) return NaN
const t = new Date(String(v.effectiveTime).replace(/-/g, '/')).getTime()
return Number.isNaN(t) ? NaN : t
}
function statusRank(v) {
const s = String(v && v.status != null ? v.status : '')
if (s === '0') return 0
if (s === '3') return 1
if (s === '1') return 2
if (s === '2') return 3
return 9
}
function sortVersions(list) {
return (list || []).slice().sort((a, b) => {
const ra = statusRank(a)
const rb = statusRank(b)
if (ra !== rb) return ra - rb
const ta = toEffMs(a)
const tb = toEffMs(b)
const aNull = Number.isNaN(ta)
const bNull = Number.isNaN(tb)
if (aNull && bNull) return (Number(b.versionSeq) || 0) - (Number(a.versionSeq) || 0)
if (aNull) return 1
if (bNull) return -1
if (tb !== ta) return tb - ta
return (Number(b.versionSeq) || 0) - (Number(a.versionSeq) || 0)
})
}
async function loadVersions() {
versions.value = []
if (!props.priceSetId) return
loading.value = true
try {
let data = null
try {
const res = await priceSetApi.getPriceSet(props.priceSetId)
data = (res && res.data) || null
} catch (e1) {
const res2 = await exclusiveQuoteApi.getPriceSet(props.priceSetId)
data = (res2 && res2.data) || null
}
const list = (data && data.versions) || []
versions.value = sortVersions(list.filter(v => v && String(v.isDel) !== '1'))
} catch (e) {
versions.value = []
} finally {
loading.value = false
}
}
function openDetail(row) {
if (!row || row.psvId == null) return
detailVersion.value = {
psvId: row.psvId,
versionNo: row.versionNo,
effectiveTime: row.effectiveTime || null,
expireTime: row.expireTime || null
}
detailVisible.value = true
}
watch(
() => [props.modelValue, props.priceSetId],
([vis]) => {
if (vis) loadVersions()
}
)
</script>
<style scoped>
.cycle-body {
min-height: 240px;
}
</style>
+88 -3
View File
@@ -342,10 +342,28 @@
<span :class="frValueClass(failReason.destPostcode || '-')">{{ failReason.destPostcode || '-' }}</span> <span :class="frValueClass(failReason.destPostcode || '-')">{{ failReason.destPostcode || '-' }}</span>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label-class-name="fr-label-key" class-name="fr-cell-key" label="套餐"> <el-descriptions-item label-class-name="fr-label-key" class-name="fr-cell-key" label="套餐">
<span :class="frValueClass(joinCodeName(failReason.priceSetCode, failReason.priceSetName), true)">{{ joinCodeName(failReason.priceSetCode, failReason.priceSetName) }}</span> <div class="fr-cycle-row">
<span :class="frValueClass(joinCodeName(failReason.priceSetCode, failReason.priceSetName), true)">{{ joinCodeName(failReason.priceSetCode, failReason.priceSetName) }}</span>
<el-button
v-if="failReason.priceSetId"
link
type="primary"
class="fr-cycle-btn"
@click="openPackagePriceCycles"
>查看</el-button>
</div>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="套餐版本"> <el-descriptions-item label-class-name="fr-label-key" class-name="fr-cell-key" label="价格周期">
<span :class="frValueClass(failReason.versionNo || '-')">{{ failReason.versionNo || '-' }}</span> <div class="fr-cycle-row">
<span :class="frValueClass(formatPriceCycle(failReason), true)">{{ formatPriceCycle(failReason) }}</span>
<el-button
v-if="failReason.psvId"
link
type="primary"
class="fr-cycle-btn"
@click="openPriceCycleDetail"
>查看</el-button>
</div>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="重量段"> <el-descriptions-item label="重量段">
<span :class="frValueClass(failReason.weightLabel || formatWeightRange(failReason.weightMin, failReason.weightMax))">{{ failReason.weightLabel || formatWeightRange(failReason.weightMin, failReason.weightMax) }}</span> <span :class="frValueClass(failReason.weightLabel || formatWeightRange(failReason.weightMin, failReason.weightMax))">{{ failReason.weightLabel || formatWeightRange(failReason.weightMin, failReason.weightMax) }}</span>
@@ -380,6 +398,20 @@
<el-button type="primary" @click="failReasonVisible = false">关闭</el-button> <el-button type="primary" @click="failReasonVisible = false">关闭</el-button>
</template> </template>
</el-dialog> </el-dialog>
<set-version-detail-drawer
v-model="priceCycleDrawerVisible"
:version="priceCycleVersion"
:set-type="failReason.setType || 'FIXED'"
/>
<order-bill-price-cycle-list-drawer
v-model="packageCycleDrawerVisible"
:price-set-id="failReason.priceSetId"
:price-set-code="failReason.priceSetCode"
:price-set-name="failReason.priceSetName"
:set-type="failReason.setType || 'FIXED'"
/>
</div> </div>
</template> </template>
@@ -391,6 +423,8 @@ import ExcelExportFieldDialog from '@/components/ExcelExportFieldDialog/index.vu
import { ORDER_BILL_EXPORT_FIELDS } from '@/utils/exportFieldDefs' import { ORDER_BILL_EXPORT_FIELDS } from '@/utils/exportFieldDefs'
import OrderSourceSyncLogDrawer from '../../monitor/orderSource/components/OrderSourceSyncLogDrawer.vue' import OrderSourceSyncLogDrawer from '../../monitor/orderSource/components/OrderSourceSyncLogDrawer.vue'
import OrderBillProductCheckDrawer from './components/OrderBillProductCheckDrawer.vue' import OrderBillProductCheckDrawer from './components/OrderBillProductCheckDrawer.vue'
import SetVersionDetailDrawer from '../set/components/detail/SetVersionDetailDrawer.vue'
import OrderBillPriceCycleListDrawer from './components/OrderBillPriceCycleListDrawer.vue'
const tableRef = ref() const tableRef = ref()
const exportFieldRef = ref() const exportFieldRef = ref()
@@ -1171,9 +1205,14 @@ const failReason = reactive({
zoneCode: '', zoneCode: '',
zoneName: '', zoneName: '',
destPostcode: '', destPostcode: '',
priceSetId: null,
priceSetCode: '', priceSetCode: '',
priceSetName: '', priceSetName: '',
setType: '',
psvId: null,
versionNo: '', versionNo: '',
effectiveTime: null,
expireTime: null,
weightLabel: '', weightLabel: '',
weightMin: null, weightMin: null,
weightMax: null, weightMax: null,
@@ -1225,6 +1264,37 @@ function formatDateTime(val) {
' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds()) ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds())
} }
function formatPriceCycle(row) {
const r = row || {}
const eff = formatDateTime(r.effectiveTime)
const exp = formatDateTime(r.expireTime)
if (eff === '-' && exp === '-') {
return r.versionNo || '-'
}
return eff + ' ~ ' + exp
}
const priceCycleDrawerVisible = ref(false)
const priceCycleVersion = ref(null)
function openPriceCycleDetail() {
if (!failReason.psvId) return
priceCycleVersion.value = {
psvId: failReason.psvId,
versionNo: failReason.versionNo,
effectiveTime: failReason.effectiveTime ? formatDateTime(failReason.effectiveTime) : null,
expireTime: failReason.expireTime ? formatDateTime(failReason.expireTime) : null
}
priceCycleDrawerVisible.value = true
}
const packageCycleDrawerVisible = ref(false)
function openPackagePriceCycles() {
if (!failReason.priceSetId) return
packageCycleDrawerVisible.value = true
}
function applyFailReason(data, row) { function applyFailReason(data, row) {
const src = data || {} const src = data || {}
const base = row || {} const base = row || {}
@@ -1241,9 +1311,14 @@ function applyFailReason(data, row) {
failReason.zoneCode = src.zoneCode || '' failReason.zoneCode = src.zoneCode || ''
failReason.zoneName = src.zoneName || '' failReason.zoneName = src.zoneName || ''
failReason.destPostcode = src.destPostcode || base.destPostcode || '' failReason.destPostcode = src.destPostcode || base.destPostcode || ''
failReason.priceSetId = src.priceSetId != null ? src.priceSetId : (base.priceSetId != null ? base.priceSetId : null)
failReason.priceSetCode = src.priceSetCode || base.priceSetCode || '' failReason.priceSetCode = src.priceSetCode || base.priceSetCode || ''
failReason.priceSetName = src.priceSetName || base.priceSetName || '' failReason.priceSetName = src.priceSetName || base.priceSetName || ''
failReason.setType = src.setType || base.setType || ''
failReason.psvId = src.psvId != null ? src.psvId : (base.psvId != null ? base.psvId : null)
failReason.versionNo = src.versionNo || '' failReason.versionNo = src.versionNo || ''
failReason.effectiveTime = src.effectiveTime || null
failReason.expireTime = src.expireTime || null
failReason.weightLabel = src.weightLabel || base.weightLabel || '' failReason.weightLabel = src.weightLabel || base.weightLabel || ''
failReason.weightMin = src.weightMin != null ? src.weightMin : base.weightMin failReason.weightMin = src.weightMin != null ? src.weightMin : base.weightMin
failReason.weightMax = src.weightMax != null ? src.weightMax : base.weightMax failReason.weightMax = src.weightMax != null ? src.weightMax : base.weightMax
@@ -1487,6 +1562,16 @@ onBeforeUnmount(() => {
color: #0f172a; color: #0f172a;
font-weight: 500; font-weight: 500;
} }
.fr-cycle-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.fr-cycle-btn {
flex-shrink: 0;
padding: 0;
}
.fr-val-key { .fr-val-key {
color: #c2410c; color: #c2410c;
font-weight: 700; font-weight: 700;
@@ -3,6 +3,7 @@
:model-value="modelValue" :model-value="modelValue"
:title="title" :title="title"
size="72%" size="72%"
append-to-body
destroy-on-close destroy-on-close
@close="close" @close="close"
> >