提单费用调整

This commit is contained in:
liuminghui
2026-11-10 15:21:03 +08:00
parent e6a9fcf0a2
commit da9efc422e
16 changed files with 911 additions and 170 deletions
@@ -73,7 +73,8 @@ public class SurchargeController extends BaseController
@PreAuthorize("@ss.hasPermi('price:surcharge:list') or @ss.hasPermi('price:surcharge:query')"
+ " or @ss.hasPermi('price:set:query') or @ss.hasPermi('price:set:add') or @ss.hasPermi('price:set:edit')"
+ " or @ss.hasPermi('price:billing:bill')"
+ " or @ss.hasPermi('price:pay:query') or @ss.hasPermi('price:pay:edit') or @ss.hasPermi('price:pay:import')")
+ " or @ss.hasPermi('price:pay:query') or @ss.hasPermi('price:pay:edit') or @ss.hasPermi('price:pay:import')"
+ " or @ss.hasPermi('price:airBill:fee')")
@GetMapping("/options")
public AjaxResult listOptions(PrcSurcharge surcharge)
{
@@ -15,9 +15,24 @@ public class SrvBillAirFee
private String billCode;
/** 字典 bms_fee_type */
/** 附加费编号 prc_surcharge.sc_id */
private String feeType;
/** 附加费名称,仅展示,不入库 */
private String feeName;
/** 费用阶段,字典 bms_fee_level */
private String feeLevel;
/** 提单费用状态,字典 bill_fee_status。只随分摊、作废操作变化 */
private String billFeeStatus;
/** 是否分摊,字典 bill_fee_alloc:1是 0否 */
private String allocFlag;
/** 分摊维度(票),字典 bill_fee_alloc_dim:1订单 2重量。是否分摊为否时为空 */
private String allocDim;
private BigDecimal price;
/** 字典 bms_currency */
@@ -11,7 +11,13 @@ public class PayPriceCompare
private PayProduct newer;
/** 有效期、来源文件、币种 */
private List<PayPriceDiff> versionDiffs;
private List<PayPriceDiff> weightDiffs;
private List<PayPriceDiff> surchargeDiffs;
/** 邮编等不能归进重量段和附加费的原文 */
private List<PayPriceDiff> extraDiffs;
}
@@ -6,6 +6,9 @@ import lombok.Data;
@Data
public class PayPriceDiff
{
/** 版本、重量段、附加费、其他配置 */
private String kind;
private String name;
private String zoneName;
@@ -26,6 +26,12 @@ public class PrcPayBillFee implements Serializable
/** CALC 计算,LADING 提单分摊,MANUAL 人工 */
private String feeSource;
/** 来源数据id */
private String sourceId;
/** 是否删除:0未删除 1已删除 */
private String isDeleted;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
}
@@ -24,6 +24,9 @@ public final class PayCostCalculator
{
private static final ZoneId SHANGHAI = ZoneId.of("GMT+8");
// 定义常量集合,建议放在类级别作为 static final 常量
private static final Set<Integer> REFUND_STATUS_SET = Set.of(9, 10);
private PayCostCalculator()
{
}
@@ -242,6 +245,23 @@ public final class PayCostCalculator
return kept;
}
/**
* 添加相关附加费,根据计费规则去判断
* @param product
* @param weight
* @param length
* @param width
* @param height
* @param postcode
* @param zoneName
* @param bizDate
* @param orderStatus
* @param freight
* @param currency
* @param freightSection
* @param remoteHits
* @param result
*/
private static void addSurcharges(PayProduct product, BigDecimal weight, BigDecimal length, BigDecimal width,
BigDecimal height, String postcode, String zoneName, Date bizDate, Integer orderStatus, BigDecimal freight, String currency,
String freightSection, List<PayRemoteBook.Hit> remoteHits, PayOrderCostResult result)
@@ -317,6 +337,7 @@ public final class PayCostCalculator
addFlat(result.getFees(), fee, currency, "计费重超过 " + fee.getCalcParam() + "kg");
}
}
// 超出尺寸费用
else if (PaySurchargeRules.SIZE.equals(rule))
{
if (length == null || width == null || height == null)
@@ -332,13 +353,15 @@ public final class PayCostCalculator
}
}
}
// 退件费用
else if (PaySurchargeRules.RETURN.equals(rule))
{
if (orderStatus != null && orderStatus == 9)
if (orderStatus != null && REFUND_STATUS_SET.contains(orderStatus))
{
addFlat(result.getFees(), fee, currency, "订单已退件");
}
}
// 超重费
else if (PaySurchargeRules.EXTRA_KG.equals(rule))
{
if (postcodeDriven)
@@ -3,6 +3,7 @@ package com.bms.price.service.impl;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -15,6 +16,7 @@ import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -164,6 +166,7 @@ public class PayProductServiceImpl implements IPayProductService
}
else
{
product.setProductType(PAY_TYPE_AP);
Long existsId = isBlank(product.getDictValue()) ? null : payProductMapper.selectTemplateId(product.getDictValue());
product.setDuplicated(existsId != null);
product.setExistsId(existsId);
@@ -266,7 +269,7 @@ public class PayProductServiceImpl implements IPayProductService
skipped++;
continue;
}
product.setProductType(requireProductType(product.getProductType()));
product.setProductType(PAY_TYPE_AP);
product.setFilePath(resolveSource(product));
prepareCounts(product);
fitColumns(product);
@@ -1694,8 +1697,10 @@ public class PayProductServiceImpl implements IPayProductService
PayPriceCompare compared = new PayPriceCompare();
compared.setOlder(older);
compared.setNewer(newer);
compared.setVersionDiffs(diffVersion(older, newer));
compared.setWeightDiffs(diffWeights(older, newer));
compared.setSurchargeDiffs(diffFees(older, newer));
compared.setExtraDiffs(diffExtras(older, newer));
return compared;
}
@@ -1753,15 +1758,6 @@ public class PayProductServiceImpl implements IPayProductService
return stableId(link.getProductId());
}
private String requireProductType(String productType)
{
if (isBlank(productType))
{
throw new ServiceException("请选择支付类型");
}
return payTypeOf(productType);
}
private String payTypeOf(String payType)
{
if (isBlank(payType))
@@ -1861,111 +1857,305 @@ public class PayProductServiceImpl implements IPayProductService
return left <= otherRight && otherLeft <= right;
}
/** 有效期和来源文件记在版本上,价格没变时也要能看出来选的是哪两版。 */
private List<PayPriceDiff> diffVersion(PayProduct older, PayProduct newer)
{
PayProductTerm oldTerm = payProductTermMapper.selectByProductId(older.getProductId());
PayProductTerm newTerm = payProductTermMapper.selectByProductId(newer.getProductId());
List<PayPriceDiff> diffs = new ArrayList<>();
diffs.add(metaDiff("生效开始", termStart(oldTerm, older), termStart(newTerm, newer)));
diffs.add(metaDiff("生效结束", termEnd(oldTerm), termEnd(newTerm)));
diffs.add(metaDiff("来源文件", blankTo(older.getSourceFile(), "-"), blankTo(newer.getSourceFile(), "-")));
diffs.add(metaDiff("币种", blankTo(older.getCurrency(), "-"), blankTo(newer.getCurrency(), "-")));
return diffs;
}
private List<PayPriceDiff> diffWeights(PayProduct older, PayProduct newer)
{
Map<String, PayWeightBand> oldRows = new LinkedHashMap<>();
Map<String, PayWeightBand> newRows = new LinkedHashMap<>();
if (older.getWeightBands() != null)
{
for (PayWeightBand band : older.getWeightBands())
return pairDiffs(groupBy(older.getWeightBands(), PayProductServiceImpl::bandKey),
groupBy(newer.getWeightBands(), PayProductServiceImpl::bandKey), (oldRow, newRow) ->
{
oldRows.put(bandKey(band), band);
}
}
if (newer.getWeightBands() != null)
{
for (PayWeightBand band : newer.getWeightBands())
{
newRows.put(bandKey(band), band);
}
}
List<PayPriceDiff> diffs = new ArrayList<>();
for (Map.Entry<String, PayWeightBand> entry : oldRows.entrySet())
{
PayWeightBand current = newRows.remove(entry.getKey());
PayPriceDiff diff = new PayPriceDiff();
diff.setName(blankTo(entry.getValue().getWeightLabel(), ""));
diff.setZoneName(blankTo(entry.getValue().getZoneName(), ""));
diff.setOldValue(priceText(entry.getValue()));
diff.setNewValue(current == null ? "" : priceText(current));
diff.setChange(current == null ? "删除" : (diff.getOldValue().equals(diff.getNewValue()) ? "相同" : "变更"));
diffs.add(diff);
}
for (PayWeightBand band : newRows.values())
{
PayPriceDiff diff = new PayPriceDiff();
diff.setName(blankTo(band.getWeightLabel(), ""));
diff.setZoneName(blankTo(band.getZoneName(), ""));
diff.setOldValue("");
diff.setNewValue(priceText(band));
diff.setChange("新增");
diffs.add(diff);
}
return diffs;
PayWeightBand sample = oldRow != null ? oldRow : newRow;
PayPriceDiff diff = new PayPriceDiff();
diff.setKind("重量段");
diff.setName(bandName(sample));
diff.setZoneName(blankTo(sample.getZoneName(), ""));
diff.setOldValue(oldRow == null ? "" : priceText(oldRow));
diff.setNewValue(newRow == null ? "" : priceText(newRow));
return diff;
});
}
private List<PayPriceDiff> diffFees(PayProduct older, PayProduct newer)
{
Map<String, PaySurcharge> oldRows = new LinkedHashMap<>();
Map<String, PaySurcharge> newRows = new LinkedHashMap<>();
if (older.getSurcharges() != null)
{
for (PaySurcharge fee : older.getSurcharges())
return pairDiffs(groupBy(older.getSurcharges(), PayProductServiceImpl::feeKey),
groupBy(newer.getSurcharges(), PayProductServiceImpl::feeKey), (oldRow, newRow) ->
{
oldRows.put(feeKey(fee), fee);
}
}
if (newer.getSurcharges() != null)
{
for (PaySurcharge fee : newer.getSurcharges())
PaySurcharge sample = oldRow != null ? oldRow : newRow;
PayPriceDiff diff = new PayPriceDiff();
diff.setKind("附加费");
diff.setName(joinText(sample.getSectionName(), sample.getFeeName()));
diff.setZoneName(blankTo(sample.getZoneName(), ""));
diff.setOldValue(oldRow == null ? "" : feeText(oldRow));
diff.setNewValue(newRow == null ? "" : feeText(newRow));
return diff;
});
}
private List<PayPriceDiff> diffExtras(PayProduct older, PayProduct newer)
{
return pairDiffs(groupBy(older.getExtraLines(), PayProductServiceImpl::extraKey),
groupBy(newer.getExtraLines(), PayProductServiceImpl::extraKey), (oldRow, newRow) ->
{
newRows.put(feeKey(fee), fee);
}
}
PayExtraLine sample = oldRow != null ? oldRow : newRow;
PayPriceDiff diff = new PayPriceDiff();
diff.setKind("其他配置");
diff.setName(blankTo(sample.getLineType(), "配置"));
diff.setZoneName("");
diff.setOldValue(oldRow == null ? "" : blankTo(oldRow.getLineText(), ""));
diff.setNewValue(newRow == null ? "" : blankTo(newRow.getLineText(), ""));
return diff;
});
}
/**
* 同一行按顺序对齐。相同费用有多条邮编或重量档时不能并成一条,否则后面的改动会被盖掉。
*/
private <T> List<PayPriceDiff> pairDiffs(Map<String, List<T>> oldRows, Map<String, List<T>> newRows, RowDiff<T> factory)
{
Set<String> keys = new LinkedHashSet<>();
keys.addAll(oldRows.keySet());
keys.addAll(newRows.keySet());
List<PayPriceDiff> diffs = new ArrayList<>();
for (Map.Entry<String, PaySurcharge> entry : oldRows.entrySet())
for (String key : keys)
{
PaySurcharge current = newRows.remove(entry.getKey());
PayPriceDiff diff = new PayPriceDiff();
diff.setName(blankTo(entry.getValue().getFeeName(), ""));
diff.setZoneName(blankTo(entry.getValue().getZoneName(), ""));
diff.setOldValue(blankTo(entry.getValue().getChargeText(), ""));
diff.setNewValue(current == null ? "" : blankTo(current.getChargeText(), ""));
diff.setChange(current == null ? "删除" : (diff.getOldValue().equals(diff.getNewValue()) ? "相同" : "变更"));
diffs.add(diff);
}
for (PaySurcharge fee : newRows.values())
{
PayPriceDiff diff = new PayPriceDiff();
diff.setName(blankTo(fee.getFeeName(), ""));
diff.setZoneName(blankTo(fee.getZoneName(), ""));
diff.setOldValue("");
diff.setNewValue(blankTo(fee.getChargeText(), ""));
diff.setChange("新增");
diffs.add(diff);
List<T> older = oldRows.getOrDefault(key, Collections.emptyList());
List<T> newer = newRows.getOrDefault(key, Collections.emptyList());
int count = Math.max(older.size(), newer.size());
for (int i = 0; i < count; i++)
{
T oldRow = i < older.size() ? older.get(i) : null;
T newRow = i < newer.size() ? newer.get(i) : null;
PayPriceDiff diff = factory.build(oldRow, newRow);
diff.setChange(oldRow == null ? "新增" : (newRow == null ? "删除"
: (diff.getOldValue().equals(diff.getNewValue()) ? "相同" : "变更")));
diffs.add(diff);
}
}
return diffs;
}
private static String bandKey(PayWeightBand band)
private <T> Map<String, List<T>> groupBy(List<T> rows, Function<T, String> keyFn)
{
return blankTo(band.getZoneName(), "") + "|" + blankTo(band.getWeightLabel(), "") + "|" + blankTo(band.getChargeName(), "");
Map<String, List<T>> grouped = new LinkedHashMap<>();
if (rows == null)
{
return grouped;
}
for (T row : rows)
{
grouped.computeIfAbsent(keyFn.apply(row), key -> new ArrayList<>()).add(row);
}
return grouped;
}
private static PayPriceDiff metaDiff(String name, String oldValue, String newValue)
{
PayPriceDiff diff = new PayPriceDiff();
diff.setKind("版本");
diff.setName(name);
diff.setZoneName("");
diff.setOldValue(oldValue);
diff.setNewValue(newValue);
diff.setChange(oldValue.equals(newValue) ? "相同" : "变更");
return diff;
}
private static String termStart(PayProductTerm term, PayProduct product)
{
if (term != null)
{
return dayOrOpen(term.getEffectiveStart());
}
return dayOrOpen(product.getEffectiveDate());
}
private static String termEnd(PayProductTerm term)
{
return term == null ? "不限" : dayOrOpen(term.getEffectiveEnd());
}
private static String dayOrOpen(Date date)
{
return date == null ? "不限" : formatDay(date);
}
/** 分区、重量段和费用名确定一行单价,价格本身不参与对齐。 */
private static String bandKey(PayWeightBand band)
{
return joinKey(band.getSectionName(), band.getZoneName(), band.getCountryCode(), band.getChargeName(),
band.getWeightLabel(), numKey(band.getWeightMin()), numKey(band.getWeightMax()));
}
/** 同一费用、同一分区、同一重量档里的多条邮编按导入顺序逐条比对。 */
private static String feeKey(PaySurcharge fee)
{
return blankTo(fee.getFeeName(), "") + "|" + blankTo(fee.getZoneName(), "") + "|" + blankTo(fee.getCalcRule(), "");
return joinKey(fee.getSectionName(), fee.getFeeName(), fee.getZoneName(), numKey(fee.getWeightMin()), numKey(fee.getWeightMax()));
}
private static String extraKey(PayExtraLine line)
{
return blankTo(line.getLineType(), "");
}
private static String bandName(PayWeightBand band)
{
String label = blankTo(band.getWeightLabel(), rangeText(band.getWeightMin(), band.getWeightMax()));
return joinText(band.getSectionName(), label, band.getChargeName());
}
private static String priceText(PayWeightBand band)
{
String price = band.getPrice() == null ? "" : band.getPrice().stripTrailingZeros().toPlainString();
String price = money(band.getPrice());
String unit = blankTo(band.getPriceUnit(), "");
String volume = blankTo(band.getVolumeText(), "");
String text = (price + " " + unit).trim();
return volume.isEmpty() ? text : (text + " " + volume).trim();
}
/** 金额、收费原文、计费规则和参数都算差异。只比原文会把改过的邮编和续重看成没变。 */
private static String feeText(PaySurcharge fee)
{
StringBuilder text = new StringBuilder();
String amount = money(fee.getAmount());
String unit = blankTo(fee.getPriceUnit(), "");
String priced = (amount + " " + unit).trim();
String charge = blankTo(fee.getChargeText(), "");
appendPart(text, priced);
if (!charge.isEmpty() && !charge.equals(priced) && !charge.equals(amount))
{
appendPart(text, charge);
}
String rule = ruleText(fee.getCalcRule());
String param = blankTo(fee.getCalcParam(), "");
String postcode = blankTo(fee.getPostcodeParam(), "");
if (!postcode.isEmpty() && !postcode.equals(param))
{
param = param.isEmpty() ? postcode : param + " " + postcode;
}
if (!rule.isEmpty() || !param.isEmpty())
{
appendPart(text, (rule + " " + param).trim());
}
appendPart(text, rangeText(fee.getWeightMin(), fee.getWeightMax()));
String period = blankTo(fee.getPeriodText(), "");
if (period.isEmpty() && (fee.getEffectiveStart() != null || fee.getEffectiveEnd() != null))
{
period = dayOrOpen(fee.getEffectiveStart()) + " 至 " + dayOrOpen(fee.getEffectiveEnd());
}
appendPart(text, period);
return text.toString();
}
private static String ruleText(String rule)
{
if (isBlank(rule) || "MANUAL".equalsIgnoreCase(rule))
{
return "";
}
switch (rule.toUpperCase(Locale.ROOT))
{
case "POSTCODE":
return "邮编";
case "EXTRA_KG":
return "续重";
case "ALWAYS":
return "固定";
case "ZONE":
return "分区";
case "WEIGHT":
return "重量";
case "SIZE":
return "尺寸";
case "RETURN":
return "退件";
default:
return rule;
}
}
private static String rangeText(BigDecimal min, BigDecimal max)
{
String left = money(min);
String right = money(max);
if (left.isEmpty() && right.isEmpty())
{
return "";
}
return (left.isEmpty() ? "0" : left) + "-" + (right.isEmpty() ? "不限" : right) + "kg";
}
private static String money(BigDecimal value)
{
return value == null ? "" : value.stripTrailingZeros().toPlainString();
}
private static String numKey(BigDecimal value)
{
return money(value);
}
private static String joinKey(String... parts)
{
StringBuilder text = new StringBuilder();
for (String part : parts)
{
if (text.length() > 0)
{
text.append('|');
}
text.append(blankTo(part, ""));
}
return text.toString();
}
private static String joinText(String... parts)
{
StringBuilder text = new StringBuilder();
for (String part : parts)
{
if (isBlank(part))
{
continue;
}
if (text.length() > 0)
{
text.append(' ');
}
text.append(part.trim());
}
return text.toString();
}
private static void appendPart(StringBuilder text, String part)
{
if (isBlank(part))
{
return;
}
if (text.length() > 0)
{
text.append(';');
}
text.append(part.trim());
}
@FunctionalInterface
private interface RowDiff<T>
{
PayPriceDiff build(T older, T newer);
}
private static Date today()
{
return toDate(java.time.LocalDate.now(java.time.ZoneId.of("GMT+8")));
@@ -21,8 +21,10 @@ import com.bms.common.utils.SecurityUtils;
import com.bms.price.airbill.domain.SrvBillAirFee;
import com.bms.price.airbill.domain.SrvBillAirFeeLog;
import com.bms.price.airbill.domain.SrvBillAirFeeSave;
import com.bms.price.domain.PrcSurcharge;
import com.bms.price.mapper.SrvBillAirFeeMapper;
import com.bms.price.mapper.SrvBillAirMapper;
import com.bms.price.service.IPrcSurchargeService;
import com.bms.price.service.ISrvBillAirFeeService;
import com.bms.system.service.ISysDictTypeService;
@@ -32,13 +34,22 @@ import com.bms.system.service.ISysDictTypeService;
@Service
public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService
{
private static final String FEE_DICT = "bms_fee_type";
private static final String LEVEL_DICT = "bms_fee_level";
private static final String ALLOC_DICT = "bill_fee_alloc";
private static final String ALLOC_DIM_DICT = "bill_fee_alloc_dim";
private static final String STATUS_DICT = "bill_fee_status";
private static final String CURRENCY_DICT = "bms_currency";
@Autowired
private SrvBillAirFeeMapper feeMapper;
@Autowired
private IPrcSurchargeService surchargeService;
@Autowired
private SrvBillAirMapper billMapper;
@@ -84,7 +95,10 @@ public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService
{
throw new ServiceException("没有需要保存的变更");
}
Map<String, String> feeLabels = dictLabels(FEE_DICT, "费用类型");
Map<String, String> levelLabels = dictLabels(LEVEL_DICT, "费用阶段");
Map<String, String> allocLabels = dictLabels(ALLOC_DICT, "是否分摊");
Map<String, String> dimLabels = dictLabels(ALLOC_DIM_DICT, "分摊维度");
Map<String, String> statusLabels = dictLabels(STATUS_DICT, "费用状态");
Map<String, String> currencyLabels = dictLabels(CURRENCY_DICT, "币种");
Operator operator = operator();
Date now = DateUtils.getNowDate();
@@ -110,7 +124,7 @@ public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService
int rows = feeMapper.deleteFee(current.getId(), billCode, operator.account, now);
if (rows > 0)
{
writeLog(billCode, id, SrvBillAirFeeLog.DELETE, current, null, feeLabels, currencyLabels, operator, now);
writeLog(billCode, id, SrvBillAirFeeLog.DELETE, current, null, feeNames(current, null), levelLabels, allocLabels, dimLabels, statusLabels, currencyLabels, operator, now);
removed++;
}
}
@@ -129,8 +143,10 @@ public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService
{
throw new ServiceException("费用已删除或不属于该提单");
}
SrvBillAirFee next = normalize(incoming, feeLabels, currencyLabels);
List<SrvBillAirFeeLog.FieldChange> changes = diff(current, next, feeLabels, currencyLabels);
SrvBillAirFee next = normalize(incoming, levelLabels, allocLabels, dimLabels, currencyLabels);
next.setBillFeeStatus(current.getBillFeeStatus() == null || current.getBillFeeStatus().isBlank() ? "0" : current.getBillFeeStatus());
Map<String, String> feeNames = feeNames(current, next);
List<SrvBillAirFeeLog.FieldChange> changes = diff(current, next, feeNames, levelLabels, allocLabels, dimLabels, statusLabels, currencyLabels);
if (changes.isEmpty())
{
continue;
@@ -141,17 +157,19 @@ public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService
next.setUpdateTime(now);
if (feeMapper.updateFee(next) > 0)
{
writeLog(billCode, current.getId(), SrvBillAirFeeLog.UPDATE, current, next, feeLabels, currencyLabels, operator, now);
writeLog(billCode, current.getId(), SrvBillAirFeeLog.UPDATE, current, next, feeNames, levelLabels, allocLabels, dimLabels, statusLabels, currencyLabels, operator, now);
updated++;
}
}
for (SrvBillAirFee incoming : inserts)
{
if (incoming == null || blank(incoming.getFeeType()) && incoming.getPrice() == null && blank(incoming.getCurrency()) && blank(incoming.getRemark()))
if (incoming == null || blank(incoming.getFeeType()) && blank(incoming.getFeeLevel()) && incoming.getPrice() == null
&& blank(incoming.getCurrency()) && blank(incoming.getRemark()))
{
continue;
}
SrvBillAirFee next = normalize(incoming, feeLabels, currencyLabels);
SrvBillAirFee next = normalize(incoming, levelLabels, allocLabels, dimLabels, currencyLabels);
next.setBillFeeStatus("0");
next.setId(null);
next.setBillCode(billCode);
next.setCreateBy(operator.account);
@@ -159,7 +177,7 @@ public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService
next.setUpdateBy(operator.account);
next.setUpdateTime(now);
feeMapper.insertFee(next);
writeLog(billCode, next.getId(), SrvBillAirFeeLog.INSERT, null, next, feeLabels, currencyLabels, operator, now);
writeLog(billCode, next.getId(), SrvBillAirFeeLog.INSERT, null, next, feeNames(null, next), levelLabels, allocLabels, dimLabels, statusLabels, currencyLabels, operator, now);
inserted++;
}
if (inserted + updated + removed == 0)
@@ -174,18 +192,48 @@ public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService
return feeMapper.selectLive(id, billCode);
}
private SrvBillAirFee normalize(SrvBillAirFee raw, Map<String, String> feeLabels, Map<String, String> currencyLabels)
private SrvBillAirFee normalize(SrvBillAirFee raw, Map<String, String> levelLabels,
Map<String, String> allocLabels, Map<String, String> dimLabels, Map<String, String> currencyLabels)
{
String feeType = trim(raw.getFeeType());
String feeLevel = trim(raw.getFeeLevel());
String allocFlag = trim(raw.getAllocFlag());
String allocDim = trim(raw.getAllocDim());
String currency = trim(raw.getCurrency());
String remark = trim(raw.getRemark());
if (feeType.isEmpty())
Long scId = parseScId(feeType);
if (scId == null)
{
throw new ServiceException("请选择费用项");
}
if (feeType.length() > 32 || !feeLabels.containsKey(feeType))
PrcSurcharge surcharge = surchargeService.selectPrcSurchargeById(scId);
if (surcharge == null)
{
throw new ServiceException("费用项不在费用类型字典中");
throw new ServiceException("费用项不存在");
}
feeType = String.valueOf(scId);
if (allocFlag.isEmpty() || !allocLabels.containsKey(allocFlag))
{
throw new ServiceException("请选择是否分摊");
}
if (!feeLevel.isEmpty() && (feeLevel.length() > 32 || !levelLabels.containsKey(feeLevel)))
{
throw new ServiceException("费用阶段不在字典中");
}
if ("1".equals(allocFlag) && feeLevel.isEmpty())
{
throw new ServiceException("如需分摊费用至订单,请选择费用所属阶段");
}
if ("1".equals(allocFlag))
{
if (allocDim.isEmpty() || !dimLabels.containsKey(allocDim))
{
throw new ServiceException("如需分摊费用至订单,请选择分摊维度");
}
}
else
{
allocDim = "";
}
if (raw.getPrice() == null)
{
@@ -213,6 +261,9 @@ public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService
}
SrvBillAirFee fee = new SrvBillAirFee();
fee.setFeeType(feeType);
fee.setFeeLevel(feeLevel);
fee.setAllocFlag(allocFlag);
fee.setAllocDim(allocDim);
fee.setPrice(raw.getPrice());
fee.setCurrency(currency);
fee.setRemark(remark);
@@ -220,9 +271,10 @@ public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService
}
private void writeLog(String billCode, Long feeId, String operation, SrvBillAirFee before, SrvBillAirFee after,
Map<String, String> feeLabels, Map<String, String> currencyLabels, Operator operator, Date now)
Map<String, String> feeLabels, Map<String, String> levelLabels, Map<String, String> allocLabels,
Map<String, String> dimLabels, Map<String, String> statusLabels, Map<String, String> currencyLabels, Operator operator, Date now)
{
List<SrvBillAirFeeLog.FieldChange> changes = diff(before, after, feeLabels, currencyLabels);
List<SrvBillAirFeeLog.FieldChange> changes = diff(before, after, feeLabels, levelLabels, allocLabels, dimLabels, statusLabels, currencyLabels);
SrvBillAirFeeLog log = new SrvBillAirFeeLog();
log.setBillCode(billCode);
log.setFeeId(feeId);
@@ -236,11 +288,20 @@ public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService
}
private static List<SrvBillAirFeeLog.FieldChange> diff(SrvBillAirFee before, SrvBillAirFee after,
Map<String, String> feeLabels, Map<String, String> currencyLabels)
Map<String, String> feeLabels, Map<String, String> levelLabels, Map<String, String> allocLabels,
Map<String, String> dimLabels, Map<String, String> statusLabels, Map<String, String> currencyLabels)
{
List<SrvBillAirFeeLog.FieldChange> changes = new ArrayList<>();
addChange(changes, "feeType", "费用类型", label(feeLabels, value(before, SrvBillAirFee::getFeeType)),
addChange(changes, "feeType", "费用项", label(feeLabels, value(before, SrvBillAirFee::getFeeType)),
label(feeLabels, value(after, SrvBillAirFee::getFeeType)));
addChange(changes, "feeLevel", "费用阶段", label(levelLabels, value(before, SrvBillAirFee::getFeeLevel)),
label(levelLabels, value(after, SrvBillAirFee::getFeeLevel)));
addChange(changes, "allocFlag", "是否分摊", label(allocLabels, value(before, SrvBillAirFee::getAllocFlag)),
label(allocLabels, value(after, SrvBillAirFee::getAllocFlag)));
addChange(changes, "allocDim", "分摊维度(票)", label(dimLabels, value(before, SrvBillAirFee::getAllocDim)),
label(dimLabels, value(after, SrvBillAirFee::getAllocDim)));
addChange(changes, "billFeeStatus", "费用状态", label(statusLabels, value(before, SrvBillAirFee::getBillFeeStatus)),
label(statusLabels, value(after, SrvBillAirFee::getBillFeeStatus)));
addChange(changes, "price", "价格", priceText(before), priceText(after));
addChange(changes, "currency", "收费币种", label(currencyLabels, value(before, SrvBillAirFee::getCurrency)),
label(currencyLabels, value(after, SrvBillAirFee::getCurrency)));
@@ -329,6 +390,60 @@ public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService
return fee.getPrice().stripTrailingZeros().toPlainString();
}
private Map<String, String> feeNames(SrvBillAirFee before, SrvBillAirFee after)
{
Map<String, String> names = new LinkedHashMap<>();
putFeeName(names, before);
putFeeName(names, after);
return names;
}
private void putFeeName(Map<String, String> names, SrvBillAirFee fee)
{
if (fee == null)
{
return;
}
String id = trim(fee.getFeeType());
if (id.isEmpty() || names.containsKey(id))
{
return;
}
names.put(id, surchargeName(id));
}
private String surchargeName(String feeType)
{
Long scId = parseScId(feeType);
if (scId == null)
{
return feeType;
}
PrcSurcharge surcharge = surchargeService.selectPrcSurchargeById(scId);
if (surcharge == null || surcharge.getScName() == null || surcharge.getScName().isBlank())
{
return feeType;
}
return surcharge.getScName().trim();
}
private static Long parseScId(String feeType)
{
if (feeType == null || feeType.isBlank() || !feeType.chars().allMatch(Character::isDigit))
{
return null;
}
try
{
long id = Long.parseLong(feeType);
return id > 0 ? id : null;
}
catch (NumberFormatException ex)
{
return null;
}
}
private static String label(Map<String, String> labels, String value)
{
if (value == null || value.isBlank())
@@ -47,7 +47,7 @@
e.supplier_name as supplierName,
(select group_concat(concat(ifnull(nullif(trim(f.fee_name), ''), ifnull(f.fee_type, '费用')), ' ', format(f.amount, 2))
order by field(f.fee_type, 'BASIC_FREIGHT', 'REGISTRATION_FEE', 'SURCHARGE', 'REFUND'), f.fee_id separator ';')
from prc_pay_bill_fee f where f.bill_id = o.bill_id) as feeText,
from prc_pay_bill_fee f where f.bill_id = o.bill_id and f.is_deleted = '0') as feeText,
(select b.bill_no from pay_bill_line l
inner join pay_bill b on b.bill_id = l.bill_id
where l.order_bill_id = o.bill_id and b.status &lt;&gt; 'VOID'
@@ -81,9 +81,11 @@
</select>
<select id="selectOrderFees" resultType="com.bms.price.domain.PrcPayBillFee">
select fee_id as feeId, bill_id as billId, fee_name as feeName, amount, currency, fee_source as feeSource
select fee_id as feeId, bill_id as billId, fee_name as feeName, amount, currency, fee_source as feeSource,
source_id as sourceId, is_deleted as isDeleted
from prc_pay_bill_fee
where bill_id in
where is_deleted = '0'
and bill_id in
<foreach collection="billIds" item="id" open="(" separator="," close=")">#{id}</foreach>
order by bill_id, fee_id
</select>
@@ -15,31 +15,38 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="currency" column="currency" />
<result property="roundMode" column="round_mode" />
<result property="remark" column="remark" />
<result property="sourceId" column="source_id" />
<result property="isDeleted" column="is_deleted" />
<result property="createTime" column="create_time" />
</resultMap>
<select id="selectByBillId" parameterType="String" resultMap="PrcPayBillFeeResult">
select fee_id, bill_id, pay_bill_id, fee_type, fee_name, sc_id, amount, currency, round_mode, remark, create_time
select fee_id, bill_id, pay_bill_id, fee_type, fee_name, sc_id, amount, currency, round_mode, remark,
source_id, is_deleted, create_time
from prc_pay_bill_fee
where bill_id = #{billId}
and is_deleted = '0'
order by field(fee_type, 'BASIC_FREIGHT', 'REGISTRATION_FEE', 'SURCHARGE', 'REFUND'), fee_id
</select>
<insert id="insertPrcPayBillFee" parameterType="PrcPayBillFee">
insert into prc_pay_bill_fee (
fee_id, bill_id, pay_bill_id, fee_type, fee_name, sc_id, amount, currency, round_mode, remark, create_time
fee_id, bill_id, pay_bill_id, fee_type, fee_name, sc_id, amount, currency, round_mode, remark,
source_id, is_deleted, create_time
) values (
#{feeId}, #{billId}, #{payBillId}, #{feeType}, #{feeName}, #{scId}, #{amount}, #{currency}, #{roundMode}, #{remark}, sysdate()
#{feeId}, #{billId}, #{payBillId}, #{feeType}, #{feeName}, #{scId}, #{amount}, #{currency}, #{roundMode}, #{remark},
#{sourceId}, ifnull(#{isDeleted}, '0'), sysdate()
)
</insert>
<insert id="insertBatch" parameterType="java.util.List">
insert into prc_pay_bill_fee (
fee_id, bill_id, pay_bill_id, fee_type, fee_name, sc_id, amount, currency, round_mode, remark, create_time
fee_id, bill_id, pay_bill_id, fee_type, fee_name, sc_id, amount, currency, round_mode, remark,
source_id, is_deleted, create_time
) values
<foreach collection="list" item="item" separator=",">
(#{item.feeId}, #{item.billId}, #{item.payBillId}, #{item.feeType}, #{item.feeName}, #{item.scId}, #{item.amount},
#{item.currency}, #{item.roundMode}, #{item.remark}, sysdate())
#{item.currency}, #{item.roundMode}, #{item.remark}, #{item.sourceId}, ifnull(#{item.isDeleted}, '0'), sysdate())
</foreach>
</insert>
@@ -3,31 +3,37 @@
<mapper namespace="com.bms.price.mapper.SrvBillAirFeeMapper">
<select id="selectByBillCode" resultType="com.bms.price.airbill.domain.SrvBillAirFee">
select id, bill_code as billCode, fee_type as feeType, price, currency, remark,
create_by as createBy, create_time as createTime, update_by as updateBy,
update_time as updateTime, del_flag as delFlag
from srv_bill_air_fee
where bill_code = #{billCode}
and del_flag = '0'
order by id
select f.id, f.bill_code as billCode, f.fee_type as feeType, sc.sc_name as feeName,
f.fee_level as feeLevel, f.bill_fee_status as billFeeStatus,
f.alloc_flag as allocFlag, f.alloc_dim as allocDim, f.price, f.currency, f.remark,
f.create_by as createBy, f.create_time as createTime, f.update_by as updateBy,
f.update_time as updateTime, f.del_flag as delFlag
from srv_bill_air_fee f
left join prc_surcharge sc on sc.sc_id = f.fee_type and ifnull(sc.is_del, '0') = '0'
where f.bill_code = #{billCode}
and f.del_flag = '0'
order by f.id
</select>
<select id="selectLive" resultType="com.bms.price.airbill.domain.SrvBillAirFee">
select id, bill_code as billCode, fee_type as feeType, price, currency, remark,
create_by as createBy, create_time as createTime, update_by as updateBy,
update_time as updateTime, del_flag as delFlag
from srv_bill_air_fee
where id = #{id}
and bill_code = #{billCode}
and del_flag = '0'
select f.id, f.bill_code as billCode, f.fee_type as feeType, sc.sc_name as feeName,
f.fee_level as feeLevel, f.bill_fee_status as billFeeStatus,
f.alloc_flag as allocFlag, f.alloc_dim as allocDim, f.price, f.currency, f.remark,
f.create_by as createBy, f.create_time as createTime, f.update_by as updateBy,
f.update_time as updateTime, f.del_flag as delFlag
from srv_bill_air_fee f
left join prc_surcharge sc on sc.sc_id = f.fee_type and ifnull(sc.is_del, '0') = '0'
where f.id = #{id}
and f.bill_code = #{billCode}
and f.del_flag = '0'
</select>
<insert id="insertFee" useGeneratedKeys="true" keyProperty="id">
insert into srv_bill_air_fee (
bill_code, fee_type, price, currency, remark,
bill_code, fee_type, fee_level, bill_fee_status, alloc_flag, alloc_dim, price, currency, remark,
create_by, create_time, update_by, update_time, del_flag
) values (
#{billCode}, #{feeType}, #{price}, #{currency}, #{remark},
#{billCode}, #{feeType}, #{feeLevel}, '0', #{allocFlag}, #{allocDim}, #{price}, #{currency}, #{remark},
#{createBy}, #{createTime}, #{updateBy}, #{updateTime}, '0'
)
</insert>
@@ -35,6 +41,9 @@
<update id="updateFee">
update srv_bill_air_fee
set fee_type = #{feeType},
fee_level = #{feeLevel},
alloc_flag = #{allocFlag},
alloc_dim = #{allocDim},
price = #{price},
currency = #{currency},
remark = #{remark},
+119
View File
@@ -0,0 +1,119 @@
-- 提单费用阶段、提单费用状态、是否分摊、分摊维度。可重复执行。
SET NAMES utf8mb4;
INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark)
SELECT N'费用阶段', 'bms_fee_level', '0', 'admin', sysdate(), N'srv_bill_air_fee.fee_level:1 a段头程仓储,2 b段提单运输,3 c段清关,4 d段尾程'
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'bms_fee_level');
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark)
SELECT v.dict_sort, v.dict_label, v.dict_value, 'bms_fee_level', '', v.list_class, 'N', '0', 'admin', sysdate(), v.remark
FROM (
SELECT 1 AS dict_sort, N'a段(头程仓储)' AS dict_label, '1' AS dict_value, 'primary' AS list_class, N'fee_level=1' AS remark
UNION ALL SELECT 2, N'b段(提单运输)', '2', 'success', N'fee_level=2'
UNION ALL SELECT 3, N'c段(清关)', '3', 'warning', N'fee_level=3'
UNION ALL SELECT 4, N'd段(尾程)', '4', 'info', N'fee_level=4'
) v
WHERE NOT EXISTS (
SELECT 1 FROM sys_dict_data d WHERE d.dict_type = 'bms_fee_level' AND d.dict_value = v.dict_value
);
INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark)
SELECT N'提单费用状态', 'bill_fee_status', '0', 'admin', sysdate(), N'srv_bill_air_fee.bill_fee_status:0未分摊 1已分摊 2已作废'
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'bill_fee_status');
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark)
SELECT v.dict_sort, v.dict_label, v.dict_value, 'bill_fee_status', '', v.list_class, v.is_default, '0', 'admin', sysdate(), v.remark
FROM (
SELECT 1 AS dict_sort, N'未分摊' AS dict_label, '0' AS dict_value, 'info' AS list_class, 'Y' AS is_default, N'新建费用的初始状态' AS remark
UNION ALL SELECT 2, N'已分摊', '1', 'success', 'N', N'分摊操作完成后'
UNION ALL SELECT 3, N'已作废', '2', 'danger', 'N', N'作废操作完成后'
) v
WHERE NOT EXISTS (
SELECT 1 FROM sys_dict_data d WHERE d.dict_type = 'bill_fee_status' AND d.dict_value = v.dict_value
);
SET @sql = (
SELECT IF(
COUNT(*) = 0,
'ALTER TABLE srv_bill_air_fee ADD COLUMN bill_fee_status char(1) NOT NULL DEFAULT ''0'' COMMENT ''提单费用状态,字典 bill_fee_status:0未分摊 1已分摊 2已作废'' AFTER fee_level',
'SELECT 1'
)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'srv_bill_air_fee' AND COLUMN_NAME = 'bill_fee_status'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark)
SELECT N'是否分摊', 'bill_fee_alloc', '0', 'admin', sysdate(), N'srv_bill_air_fee.alloc_flag:1是 0否'
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'bill_fee_alloc');
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark)
SELECT v.dict_sort, v.dict_label, v.dict_value, 'bill_fee_alloc', '', v.list_class, v.is_default, '0', 'admin', sysdate(), v.remark
FROM (
SELECT 1 AS dict_sort, N'是' AS dict_label, '1' AS dict_value, 'success' AS list_class, 'N' AS is_default, N'这条费用参与分摊,须选择费用阶段' AS remark
UNION ALL SELECT 2, N'否', '0', 'info', 'Y', N'这条费用不分摊,默认'
) v
WHERE NOT EXISTS (
SELECT 1 FROM sys_dict_data d WHERE d.dict_type = 'bill_fee_alloc' AND d.dict_value = v.dict_value
);
SET @sql = (
SELECT IF(
COUNT(*) = 0,
'ALTER TABLE srv_bill_air_fee ADD COLUMN alloc_flag char(1) NOT NULL DEFAULT ''0'' COMMENT ''是否分摊,字典 bill_fee_alloc:1是 0否。为是时必须选择费用阶段'' AFTER bill_fee_status',
'SELECT 1'
)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'srv_bill_air_fee' AND COLUMN_NAME = 'alloc_flag'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
UPDATE sys_dict_data SET is_default = 'N', remark = N'这条费用参与分摊,须选择费用阶段'
WHERE dict_type = 'bill_fee_alloc' AND dict_value = '1';
UPDATE sys_dict_data SET is_default = 'Y', remark = N'这条费用不分摊,默认'
WHERE dict_type = 'bill_fee_alloc' AND dict_value = '0';
SET @sql = (
SELECT IF(
COUNT(*) > 0,
'ALTER TABLE srv_bill_air_fee MODIFY COLUMN alloc_flag char(1) NOT NULL DEFAULT ''0'' COMMENT ''是否分摊,字典 bill_fee_alloc:1是 0否。为是时必须选择费用阶段''',
'SELECT 1'
)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'srv_bill_air_fee' AND COLUMN_NAME = 'alloc_flag'
AND (COLUMN_DEFAULT IS NULL OR COLUMN_DEFAULT <> '0')
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark)
SELECT N'分摊维度(票)', 'bill_fee_alloc_dim', '0', 'admin', sysdate(), N'srv_bill_air_fee.alloc_dim:1订单 2重量。是否分摊为是时必填'
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'bill_fee_alloc_dim');
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark)
SELECT v.dict_sort, v.dict_label, v.dict_value, 'bill_fee_alloc_dim', '', v.list_class, 'N', '0', 'admin', sysdate(), v.remark
FROM (
SELECT 1 AS dict_sort, N'订单' AS dict_label, '1' AS dict_value, 'primary' AS list_class, N'按订单票数分摊' AS remark
UNION ALL SELECT 2, N'重量', '2', 'success', N'按重量占比分摊'
) v
WHERE NOT EXISTS (
SELECT 1 FROM sys_dict_data d WHERE d.dict_type = 'bill_fee_alloc_dim' AND d.dict_value = v.dict_value
);
SET @sql = (
SELECT IF(
COUNT(*) = 0,
'ALTER TABLE srv_bill_air_fee ADD COLUMN alloc_dim char(1) NOT NULL DEFAULT '''' COMMENT ''分摊维度(票),字典 bill_fee_alloc_dim:1订单 2重量。是否分摊为是时必填'' AFTER alloc_flag',
'SELECT 1'
)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'srv_bill_air_fee' AND COLUMN_NAME = 'alloc_dim'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
+26
View File
@@ -12,6 +12,32 @@ PREPARE stmt FROM @fee_source;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql = (
SELECT IF(
COUNT(*) = 0,
'ALTER TABLE prc_pay_bill_fee ADD COLUMN source_id varchar(64) DEFAULT NULL COMMENT ''来源数据id'' AFTER fee_source',
'SELECT 1'
)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'prc_pay_bill_fee' AND COLUMN_NAME = 'source_id'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql = (
SELECT IF(
COUNT(*) = 0,
'ALTER TABLE prc_pay_bill_fee ADD COLUMN is_deleted char(1) NOT NULL DEFAULT ''0'' COMMENT ''是否删除:0未删除 1已删除'' AFTER source_id',
'SELECT 1'
)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'prc_pay_bill_fee' AND COLUMN_NAME = 'is_deleted'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
CREATE TABLE IF NOT EXISTS `pay_lading` (
`lading_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '提单ID',
`lading_no` varchar(64) NOT NULL COMMENT '提单号或航空主单号',
@@ -1,11 +1,6 @@
<template>
<el-dialog v-model="visible" title="导入应付价卡" width="1080px" append-to-body destroy-on-close @closed="reset">
<el-form label-width="88px" class="import-form">
<el-form-item label="支付类型" required>
<el-select v-model="productType" filterable placeholder="请选择支付类型" style="width: 420px">
<el-option v-for="item in bms_pay_type" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="产品">
<el-select v-model="dictValue" clearable filterable placeholder="不选则按工作表名称匹配全部字典产品" style="width: 420px">
<el-option v-for="item in payment_product_type" :key="item.value" :label="item.label" :value="item.value" />
@@ -115,14 +110,13 @@ import PayConfigDetail from './PayConfigDetail.vue'
const emit = defineEmits(['saved'])
const { proxy } = getCurrentInstance()
const { payment_product_type, bms_pay_type } = useDict('payment_product_type', 'bms_pay_type')
const { payment_product_type } = useDict('payment_product_type')
const visible = ref(false)
const recognizing = ref(false)
const saving = ref(false)
const overwrite = ref(false)
const dictValue = ref('')
const productType = ref('AP')
const preview = ref(null)
const selected = ref([])
const tableRef = ref()
@@ -150,7 +144,6 @@ function reset() {
selected.value = []
overwrite.value = false
dictValue.value = ''
productType.value = 'AP'
activeKey.value = ''
recognizing.value = false
saving.value = false
@@ -265,10 +258,6 @@ function onSelect(rows) {
}
async function save() {
if (!productType.value) {
proxy.$modal.msgError('请选择支付类型')
return
}
if (!selected.value.length) {
proxy.$modal.msgError('请勾选要保存的产品')
return
@@ -284,8 +273,7 @@ async function save() {
}
saving.value = true
try {
const products = selected.value.map(row => ({ ...row, productType: productType.value }))
const res = await savePay({ overwrite: overwrite.value, products })
const res = await savePay({ overwrite: overwrite.value, products: selected.value })
const message = res.msg || '保存成功'
reset()
visible.value = false
+118 -18
View File
@@ -61,12 +61,12 @@
<pay-import-dialog ref="importRef" @saved="tableRef.refresh()" />
<el-drawer v-model="historyVisible" :title="historyTitle" size="980px" destroy-on-close>
<el-drawer v-model="historyVisible" :title="historyTitle" size="980px" append-to-body destroy-on-close>
<div class="history-bar">
<el-button type="primary" plain :disabled="historyPicked.length !== 2" @click="openCompare">比对所选版本</el-button>
<span>勾选两个版本后比对。历史订单按生效时间取价,不看当前开关。</span>
</div>
<el-table v-loading="historyLoading" :data="historyRows" border size="small" empty-text="还没有价格版本" @selection-change="onHistoryPick">
<el-table v-loading="historyLoading" :data="historyRows" row-key="termId" border size="small" empty-text="还没有价格版本" @selection-change="onHistoryPick">
<el-table-column type="selection" width="42" align="center" />
<el-table-column prop="versionNo" label="版本" width="70" align="center" />
<el-table-column label="来源文件" min-width="160" show-overflow-tooltip>
@@ -123,15 +123,42 @@
</template>
</el-dialog>
<el-drawer v-model="compareVisible" :title="compareTitle" size="88%" append-to-body destroy-on-close>
<el-table v-loading="compareLoading" :data="compareRows" border size="small" max-height="640" empty-text="没有差异">
<el-table-column prop="kind" label="类型" width="90" align="center" />
<el-table-column prop="change" label="变化" width="80" align="center" />
<el-table-column prop="name" label="项目" min-width="160" show-overflow-tooltip />
<el-table-column prop="zoneName" label="地区" min-width="140" show-overflow-tooltip />
<el-table-column prop="oldValue" label="旧版" min-width="160" show-overflow-tooltip />
<el-table-column prop="newValue" label="新版" min-width="160" show-overflow-tooltip />
</el-table>
<el-drawer v-model="compareVisible" :title="compareTitle" size="88%" append-to-body :z-index="4000" destroy-on-close>
<div class="compare-panel">
<div v-if="compareReady && !compareLoading" class="history-bar">
<el-tag type="primary" effect="plain">变更 {{ compareStat.changed }}</el-tag>
<el-tag type="success" effect="plain">新增 {{ compareStat.added }}</el-tag>
<el-tag type="danger" effect="plain">删除 {{ compareStat.removed }}</el-tag>
<el-tag type="info" effect="plain">相同 {{ compareStat.same }}</el-tag>
</div>
<el-alert
v-if="compareReady && !compareLoading && !compareRows.length"
title="这两版的有效期、重量段和附加费一致"
type="success"
:closable="false"
show-icon
/>
<el-table v-show="compareLoading || compareRows.length" v-loading="compareLoading" :data="compareRows" :row-class-name="compareRowClass" border size="small" max-height="640" empty-text="没有差异">
<el-table-column prop="kind" label="类型" width="100" align="center" />
<el-table-column label="变化" width="90" align="center">
<template #default="{ row }">
<el-tag :type="changeTagType(row.change)" effect="light">{{ row.change }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="name" label="项目" min-width="180" show-overflow-tooltip />
<el-table-column prop="zoneName" label="地区" min-width="140" show-overflow-tooltip />
<el-table-column label="旧版" min-width="200" show-overflow-tooltip>
<template #default="{ row }">
<span :class="changeTextClass(row.change, 'old')">{{ row.oldValue || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="新版" min-width="200" show-overflow-tooltip>
<template #default="{ row }">
<span :class="changeTextClass(row.change, 'new')">{{ row.newValue || '-' }}</span>
</template>
</el-table-column>
</el-table>
</div>
</el-drawer>
<el-drawer
@@ -187,6 +214,12 @@ const compareVisible = ref(false)
const compareLoading = ref(false)
const compareTitle = ref('版本比对')
const compareRows = ref([])
const compareAll = ref([])
const compareReady = ref(false)
const compareStat = computed(() => {
const count = (name) => compareAll.value.filter(row => row.change === name).length
return { changed: count('变更'), added: count('新增'), removed: count('删除'), same: count('相同') }
})
const detailTitle = computed(() => {
if (!detail.value) return '应付配置'
const no = detail.value.versionNo ? `第 ${detail.value.versionNo} 版` : ''
@@ -356,23 +389,60 @@ function onHistoryPick(rows) {
function openCompare() {
if (historyPicked.value.length !== 2) return
const [first, second] = historyPicked.value
const [first, second] = [...historyPicked.value].sort((a, b) => (Number(a.versionNo) || 0) - (Number(b.versionNo) || 0))
if (!first.productId || !second.productId) {
proxy.$modal.msgError('选中的版本没有价格数据')
return
}
compareTitle.value = `第 ${first.versionNo || '-'} 版 对比 第 ${second.versionNo || '-'} 版`
compareVisible.value = true
compareLoading.value = true
compareRows.value = []
compareAll.value = []
compareReady.value = false
comparePay(first.productId, second.productId).then(res => {
const data = res.data || {}
const older = data.older?.versionNo || '-'
const newer = data.newer?.versionNo || '-'
compareTitle.value = `第 ${older} 版 对比 第 ${newer} 版`
const weights = (data.weightDiffs || []).map(row => ({ ...row, kind: '重量段' }))
const fees = (data.surchargeDiffs || []).map(row => ({ ...row, kind: '附加费' }))
compareRows.value = weights.concat(fees).filter(row => row.change !== '相同')
const older = data.older?.versionNo
const newer = data.newer?.versionNo
if (older && newer) compareTitle.value = `第 ${older} 版 对比 第 ${newer} 版`
const rows = []
.concat(markKind(data.versionDiffs, '版本'))
.concat(markKind(data.weightDiffs, '重量段'))
.concat(markKind(data.surchargeDiffs, '附加费'))
.concat(markKind(data.extraDiffs, '其他配置'))
compareAll.value = rows
compareRows.value = rows.filter(row => row.change !== '相同')
compareReady.value = true
}).finally(() => {
compareLoading.value = false
})
}
function markKind(rows, kind) {
return (rows || []).map(row => ({ ...row, kind: row.kind || kind }))
}
function changeTagType(change) {
if (change === '变更') return 'primary'
if (change === '新增') return 'success'
if (change === '删除') return 'danger'
return 'info'
}
function compareRowClass({ row }) {
if (row.change === '变更') return 'compare-changed'
if (row.change === '新增') return 'compare-added'
if (row.change === '删除') return 'compare-removed'
return ''
}
function changeTextClass(change, side) {
if (change === '变更') return 'compare-text-changed'
if (change === '删除' && side === 'old') return 'compare-text-removed'
if (change === '新增' && side === 'new') return 'compare-text-added'
return ''
}
function loadHistory() {
if (!historyProductId.value) return
historyLoading.value = true
@@ -446,4 +516,34 @@ function statusType(status) {
margin-bottom: 12px;
color: var(--el-text-color-secondary);
}
.compare-text-changed {
color: var(--el-color-primary);
font-weight: 600;
}
.compare-text-added {
color: var(--el-color-success);
font-weight: 600;
}
.compare-text-removed {
color: var(--el-color-danger);
font-weight: 600;
}
.compare-panel :deep(tr.compare-changed > td.el-table__cell) {
background: var(--el-color-primary-light-9);
}
.compare-panel :deep(tr.compare-added > td.el-table__cell) {
background: var(--el-color-success-light-9);
}
.compare-panel :deep(tr.compare-removed > td.el-table__cell) {
background: var(--el-color-danger-light-9);
}
.compare-panel :deep(tr.compare-changed:hover > td.el-table__cell) {
background: var(--el-color-primary-light-8);
}
.compare-panel :deep(tr.compare-added:hover > td.el-table__cell) {
background: var(--el-color-success-light-8);
}
.compare-panel :deep(tr.compare-removed:hover > td.el-table__cell) {
background: var(--el-color-danger-light-8);
}
</style>
@@ -1,24 +1,57 @@
<template>
<el-dialog v-model="visible" :title="title" width="980px" append-to-body destroy-on-close @closed="reset">
<el-dialog v-model="visible" :title="title" width="1360px" append-to-body destroy-on-close @closed="reset">
<div v-loading="loading">
<div class="fee-toolbar">
<el-button type="primary" plain icon="Plus" @click="addRow">新增</el-button>
<span class="fee-hint">只保存本次新增、修改和删除的费用,未改动的行不会重写。</span>
</div>
<el-table :data="rows" border size="small" max-height="320" row-key="rowKey" empty-text="暂无费用,点击新增">
<el-table-column label="费用项" min-width="160">
<el-table-column min-width="180">
<template #header><span class="col-label"><i v-if="allocRequired" class="req-star">*</i>费用阶段</span></template>
<template #default="{ row }">
<el-select v-model="row.feeType" filterable clearable placeholder="请选择" style="width: 100%">
<el-option v-for="item in bms_fee_type" :key="item.value" :label="item.label" :value="item.value" />
<el-select v-model="row.feeLevel" filterable clearable placeholder="请选择" style="width: 100%">
<el-option v-for="item in bms_fee_level" :key="item.value" :label="item.label" :value="String(item.value)" />
</el-select>
</template>
</el-table-column>
<el-table-column label="价格" width="150">
<el-table-column width="120">
<template #header><span class="col-label"><i class="req-star">*</i>是否分摊</span></template>
<template #default="{ row }">
<el-select v-model="row.allocFlag" placeholder="请选择" style="width: 100%" @change="changeAlloc(row)">
<el-option v-for="item in bill_fee_alloc" :key="item.value" :label="item.label" :value="String(item.value)" />
</el-select>
</template>
</el-table-column>
<el-table-column v-if="allocRequired" min-width="150">
<template #header><span class="col-label"><i class="req-star">*</i>分摊维度(票)</span></template>
<template #default="{ row }">
<el-select v-if="String(row.allocFlag) === '1'" v-model="row.allocDim" placeholder="请选择" style="width: 100%">
<el-option v-for="item in bill_fee_alloc_dim" :key="item.value" :label="item.label" :value="String(item.value)" />
</el-select>
</template>
</el-table-column>
<el-table-column label="费用状态" width="100" align="center">
<template #default="{ row }">
<dict-tag :options="bill_fee_status" :value="row.billFeeStatus || '0'" />
</template>
</el-table-column>
<el-table-column min-width="160">
<template #header><span class="col-label"><i class="req-star">*</i>费用项</span></template>
<template #default="{ row }">
<el-select v-model="row.feeType" filterable remote clearable reserve-keyword placeholder="输入费用名称"
:remote-method="searchSurcharge" :loading="surchargeLoading" style="width: 100%" @change="name => onFeeChange(row, name)">
<el-option v-for="item in surchargeOptions" :key="item.scId" :label="item.scName" :value="item.scId" />
</el-select>
</template>
</el-table-column>
<el-table-column width="150">
<template #header><span class="col-label"><i class="req-star">*</i>价格</span></template>
<template #default="{ row }">
<el-input-number v-model="row.price" :min="0" :precision="4" :controls="false" :value-on-clear="null" placeholder="价格" style="width: 100%" />
</template>
</el-table-column>
<el-table-column label="收费币种" width="140">
<el-table-column width="140">
<template #header><span class="col-label"><i class="req-star">*</i>收费币种</span></template>
<template #default="{ row }">
<el-select v-model="row.currency" filterable clearable placeholder="请选择" style="width: 100%">
<el-option v-for="item in bms_currency" :key="item.value" :label="item.label" :value="item.value" />
@@ -91,10 +124,11 @@
<script setup>
import { listAirBillFee, listAirBillFeeLog, saveAirBillFee } from '@/api/price/airBill'
import { listSurchargeOptions } from '@/api/price/surcharge'
import { parseTime } from '@/utils/bms'
const { proxy } = getCurrentInstance()
const { bms_fee_type, bms_currency } = useDict('bms_fee_type', 'bms_currency')
const { bms_fee_level, bms_currency, bill_fee_status, bill_fee_alloc, bill_fee_alloc_dim } = useDict('bms_fee_level', 'bms_currency', 'bill_fee_status', 'bill_fee_alloc', 'bill_fee_alloc_dim')
const visible = ref(false)
const loading = ref(false)
@@ -107,10 +141,14 @@ const logs = ref([])
const logPageNum = ref(1)
const logPageSize = ref(10)
const logTotal = ref(0)
const surchargeOptions = ref([])
const surchargeLoading = ref(false)
let rowSeed = 0
let logSeq = 0
let surchargeSeq = 0
const title = computed(() => billCode.value ? `提单费用 - ${billCode.value}` : '提单费用')
const allocRequired = computed(() => rows.value.some(row => String(row.allocFlag) === '1'))
const dirty = computed(() => deletedIds.value.length > 0 || rows.value.some(row => rowChanged(row)))
function open(row) {
@@ -130,6 +168,9 @@ function reset() {
logPageSize.value = 10
logTotal.value = 0
logSeq += 1
surchargeSeq += 1
surchargeOptions.value = []
surchargeLoading.value = false
saving.value = false
loading.value = false
logLoading.value = false
@@ -139,6 +180,7 @@ function load() {
logPageNum.value = 1
loadFees()
loadLogs()
searchSurcharge('')
}
function loadFees() {
@@ -151,16 +193,58 @@ function loadFees() {
rowKey: `fee-${item.id}`,
id: item.id,
feeType: item.feeType == null ? '' : String(item.feeType),
feeName: item.feeName || '',
feeLevel: item.feeLevel == null ? '' : String(item.feeLevel),
allocFlag: item.allocFlag == null || item.allocFlag === '' ? '0' : String(item.allocFlag),
allocDim: item.allocDim == null ? '' : String(item.allocDim),
billFeeStatus: item.billFeeStatus == null || item.billFeeStatus === '' ? '0' : String(item.billFeeStatus),
price: item.price === null || item.price === undefined ? null : Number(item.price),
currency: item.currency || '',
remark: item.remark || '',
origin: snapshot(item)
}))
pinSelectedFees()
}).finally(() => {
if (code === billCode.value) loading.value = false
})
}
function pinSelectedFees() {
const known = new Map(surchargeOptions.value.map(item => [String(item.scId), item]))
for (const row of rows.value) {
if (!row.feeType) continue
if (!known.has(String(row.feeType))) {
known.set(String(row.feeType), { scId: String(row.feeType), scName: row.feeName || String(row.feeType) })
}
}
surchargeOptions.value = [...known.values()]
}
function searchSurcharge(keyword) {
const seq = ++surchargeSeq
surchargeLoading.value = true
const params = { ssCode: 'on' }
const name = String(keyword || '').trim()
if (name) params.scName = name
listSurchargeOptions(params).then(res => {
if (seq !== surchargeSeq) return
surchargeOptions.value = (res.data || []).map(item => ({
scId: item.scId == null ? '' : String(item.scId),
scName: item.scName || ''
})).filter(item => item.scId)
pinSelectedFees()
}).catch(() => {
if (seq === surchargeSeq) pinSelectedFees()
}).finally(() => {
if (seq === surchargeSeq) surchargeLoading.value = false
})
}
function onFeeChange(row, id) {
const found = surchargeOptions.value.find(item => String(item.scId) === String(id || ''))
row.feeName = found ? found.scName : ''
}
function unwrapLogs(res) {
const rows = res?.rows
if (Array.isArray(rows)) {
@@ -207,6 +291,11 @@ function addRow() {
rowKey: `new-${rowSeed}`,
id: null,
feeType: '',
feeName: '',
feeLevel: '',
allocFlag: '0',
allocDim: '',
billFeeStatus: '0',
price: null,
currency: '',
remark: '',
@@ -214,6 +303,10 @@ function addRow() {
})
}
function changeAlloc(row) {
if (String(row.allocFlag) !== '1') row.allocDim = ''
}
function removeRow(row) {
if (row.id != null && !deletedIds.value.some(id => String(id) === String(row.id))) {
deletedIds.value.push(row.id)
@@ -224,6 +317,9 @@ function removeRow(row) {
function snapshot(item) {
return {
feeType: item.feeType || '',
feeLevel: item.feeLevel || '',
allocFlag: item.allocFlag == null || item.allocFlag === '' ? '0' : String(item.allocFlag),
allocDim: item.allocDim || '',
price: normPrice(item.price),
currency: item.currency || '',
remark: (item.remark || '').trim()
@@ -233,6 +329,9 @@ function snapshot(item) {
function currentOf(row) {
return {
feeType: row.feeType || '',
feeLevel: row.feeLevel || '',
allocFlag: row.allocFlag || '',
allocDim: String(row.allocFlag) === '1' ? (row.allocDim || '') : '',
price: normPrice(row.price),
currency: row.currency || '',
remark: (row.remark || '').trim()
@@ -248,20 +347,40 @@ function normPrice(value) {
function rowChanged(row) {
if (!row.origin) {
const now = currentOf(row)
return !!(now.feeType || now.price || now.currency || now.remark)
return !!(now.feeType || now.feeLevel || now.allocFlag === '1' || now.allocDim || now.price || now.currency || now.remark)
}
const now = currentOf(row)
return now.feeType !== row.origin.feeType || now.price !== row.origin.price
return now.feeType !== row.origin.feeType || now.feeLevel !== row.origin.feeLevel || now.allocFlag !== row.origin.allocFlag
|| now.allocDim !== row.origin.allocDim || now.price !== row.origin.price
|| now.currency !== row.origin.currency || now.remark !== row.origin.remark
}
function rowError(row) {
if (String(row.allocFlag) !== '0' && String(row.allocFlag) !== '1') return '请选择是否分摊'
if (String(row.allocFlag) === '1' && !row.feeLevel) return '如需分摊费用至订单,请选择费用所属阶段'
if (String(row.allocFlag) === '1' && !row.allocDim) return '如需分摊费用至订单,请选择分摊维度'
if (!row.feeType) return '请选择费用项'
if (row.price === null || row.price === undefined || row.price === '') return '请填写价格'
if (Number(row.price) < 0) return '价格不能小于0'
if (!row.currency) return '请选择收费币种'
return ''
}
function save() {
const invalid = rows.value.find(row => rowChanged(row) && rowError(row))
if (invalid) {
proxy.$modal.msgError(rowError(invalid))
return
}
const inserts = []
const updates = []
rows.value.forEach(row => {
if (!rowChanged(row)) return
const payload = {
feeType: row.feeType || '',
feeLevel: row.feeLevel || '',
allocFlag: row.allocFlag || '',
allocDim: String(row.allocFlag) === '1' ? (row.allocDim || '') : '',
price: row.price,
currency: row.currency || '',
remark: (row.remark || '').trim()
@@ -323,6 +442,18 @@ defineExpose({ open })
color: var(--el-text-color-secondary);
font-size: 12px;
}
.col-label {
display: inline-flex;
align-items: flex-start;
line-height: 1.2;
}
.req-star {
color: var(--el-color-danger);
font-style: normal;
font-weight: 700;
margin-right: 2px;
line-height: 1;
}
.log-title {
margin: 16px 0 8px;
font-weight: 600;