Merge branch 'test'
This commit is contained in:
+10
@@ -12,6 +12,7 @@ import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import com.bms.common.annotation.Log;
|
||||
@@ -43,6 +44,15 @@ public class PayPostcodeController extends BaseController
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
/** 按国家去重区块,供附加费编辑分区下拉 */
|
||||
@PreAuthorize("@ss.hasPermi('data:payPostcode:list')")
|
||||
@GetMapping("/blockOptions")
|
||||
public AjaxResult blockOptions(@RequestParam(required = false) String countryCode,
|
||||
@RequestParam(required = false) String countryName)
|
||||
{
|
||||
return success(payPostcodeService.selectBlockTitlesByCountry(countryCode, countryName));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('data:payPostcode:query')")
|
||||
@GetMapping("/{groupId}")
|
||||
public AjaxResult getInfo(@PathVariable Long groupId)
|
||||
|
||||
@@ -147,6 +147,22 @@ public class BillingController extends BaseController
|
||||
@PreAuthorize("@ss.hasPermi('price:billing:bill')")
|
||||
@GetMapping("/bill/list")
|
||||
public TableDataInfo billList(PrcOrderBill query)
|
||||
{
|
||||
return doBillList(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST 计费列表:大批单号等筛选放请求体,避免 GET URL 过长触发 HTTP 431。
|
||||
* pageNum/pageSize 仍走 URL 查询参数,供 PageHelper 读取。
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('price:billing:bill')")
|
||||
@PostMapping("/bill/list")
|
||||
public TableDataInfo billListPost(@RequestBody(required = false) PrcOrderBill query)
|
||||
{
|
||||
return doBillList(query);
|
||||
}
|
||||
|
||||
private TableDataInfo doBillList(PrcOrderBill query)
|
||||
{
|
||||
if (query == null)
|
||||
{
|
||||
@@ -220,6 +236,19 @@ public class BillingController extends BaseController
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST 列表:筛选(尤其是大批量单号)放请求体,避免 GET URL 过长触发 HTTP 431。
|
||||
* pageNum/pageSize 仍走 URL 查询参数,供 TableSupport/PageHelper 读取。
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('price:orderBill:list')")
|
||||
@PostMapping("/orderBill/list")
|
||||
public TableDataInfo orderBillListPost(@RequestBody(required = false) PrcOrderBill query)
|
||||
{
|
||||
startPage();
|
||||
List<PrcOrderBill> list = prcOrderBillService.selectBillList(query == null ? new PrcOrderBill() : query);
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
|
||||
/** 订单列表弹窗:按 billId 查询计费失败原因(受数据权限控制)。 */
|
||||
@PreAuthorize("@ss.hasPermi('price:orderBill:list')")
|
||||
|
||||
+13
@@ -20,6 +20,7 @@ import com.bms.common.core.page.TableDataInfo;
|
||||
import com.bms.common.enums.BusinessType;
|
||||
import com.bms.price.domain.PrcSurcharge;
|
||||
import com.bms.price.domain.PrcSurchargeRel;
|
||||
import com.bms.price.service.IPayPostcodeService;
|
||||
import com.bms.price.service.IPrcSurchargeRelService;
|
||||
import com.bms.price.service.IPrcSurchargeService;
|
||||
|
||||
@@ -38,6 +39,9 @@ public class SurchargeController extends BaseController
|
||||
@Autowired
|
||||
private IPrcSurchargeRelService surchargeRelService;
|
||||
|
||||
@Autowired
|
||||
private IPayPostcodeService payPostcodeService;
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:surcharge:list')")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(PrcSurcharge surcharge)
|
||||
@@ -47,6 +51,15 @@ public class SurchargeController extends BaseController
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
/** 按国家从应付邮编配置拉取区块/分区选项 */
|
||||
@PreAuthorize("@ss.hasPermi('price:surcharge:list') or @ss.hasPermi('price:surcharge:query') or @ss.hasPermi('price:surcharge:add') or @ss.hasPermi('price:surcharge:edit')")
|
||||
@GetMapping("/blockOptions")
|
||||
public AjaxResult blockOptions(@RequestParam(required = false) String countryCode,
|
||||
@RequestParam(required = false) String countryName)
|
||||
{
|
||||
return success(payPostcodeService.selectBlockTitlesByCountry(countryCode, countryName));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:surcharge:query')")
|
||||
@GetMapping("/{scId}")
|
||||
public AjaxResult getInfo(@PathVariable Long scId)
|
||||
|
||||
+8
-1
@@ -325,9 +325,16 @@ public class ArBillController extends BaseController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载已成功生成的导出文件;文件内容由服务层从临时文件或配置存储读取。
|
||||
* 获取已成功生成导出文件的下载方式;OSS 文件返回短时签名链接,本地历史文件走原下载接口。
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('ar:bill:download')")
|
||||
@GetMapping("/export/{exportId}/download-url")
|
||||
public AjaxResult exportDownloadInfo(@PathVariable Long exportId) {
|
||||
return success(billService.getExportDownloadInfo(exportId));
|
||||
}
|
||||
|
||||
/** 下载本地历史导出文件;OSS 文件由前端通过签名链接直接下载。 */
|
||||
@PreAuthorize("@ss.hasPermi('ar:bill:download')")
|
||||
@GetMapping("/export/{exportId}/download")
|
||||
public void download(@PathVariable Long exportId, HttpServletResponse response) throws Exception {
|
||||
billService.downloadExport(exportId, response);
|
||||
|
||||
@@ -191,7 +191,7 @@ xss:
|
||||
# 过滤开关
|
||||
enabled: true
|
||||
# 排除链接(多个用逗号分隔)
|
||||
excludes: /system/notice
|
||||
excludes: /system/notice,/tool/migration
|
||||
# 匹配链接
|
||||
urlPatterns: /system/*,/monitor/*,/tool/*
|
||||
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
package com.bms.common.utils.file;
|
||||
|
||||
import java.net.URL;
|
||||
import java.net.URLEncoder;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Date;
|
||||
|
||||
import com.aliyun.oss.ClientBuilderConfiguration;
|
||||
import com.aliyun.oss.HttpMethod;
|
||||
import com.aliyun.oss.OSS;
|
||||
import com.aliyun.oss.OSSClientBuilder;
|
||||
import com.aliyun.oss.model.CannedAccessControlList;
|
||||
import com.aliyun.oss.model.GeneratePresignedUrlRequest;
|
||||
import com.aliyun.oss.model.OSSObject;
|
||||
import com.aliyun.oss.model.ObjectMetadata;
|
||||
import com.aliyun.oss.model.PutObjectRequest;
|
||||
import com.aliyun.oss.model.ResponseHeaderOverrides;
|
||||
import com.bms.common.config.OssProperties;
|
||||
import com.bms.common.exception.ServiceException;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -67,6 +74,28 @@ public class OssStorage {
|
||||
}
|
||||
}
|
||||
|
||||
/** 为私有 OSS 对象生成带附件文件名的限时下载链接。 */
|
||||
public String generatePresignedDownloadUrl(String storageKey, String fileName, Date expiresAt) {
|
||||
String objectKey = resolveObjectKey(storageKey);
|
||||
OSS client = createClient();
|
||||
try {
|
||||
GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(
|
||||
properties.getBucket(), objectKey, HttpMethod.GET);
|
||||
request.setExpiration(expiresAt);
|
||||
|
||||
String downloadName = StringUtils.hasText(fileName) ? fileName : "账单导出.xlsx";
|
||||
String encodedFileName = URLEncoder.encode(downloadName, StandardCharsets.UTF_8).replace("+", "%20");
|
||||
ResponseHeaderOverrides responseHeaders = new ResponseHeaderOverrides();
|
||||
responseHeaders.setContentDisposition("attachment; filename=" + encodedFileName
|
||||
+ ";filename*=utf-8''" + encodedFileName);
|
||||
request.setResponseHeaders(responseHeaders);
|
||||
URL signedUrl = client.generatePresignedUrl(request);
|
||||
return signedUrl.toExternalForm();
|
||||
} finally {
|
||||
client.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
public void delete(String storageKey) {
|
||||
String objectKey = resolveObjectKey(storageKey);
|
||||
OSS client = createClient();
|
||||
|
||||
@@ -73,4 +73,10 @@ public class PaySurcharge
|
||||
private BigDecimal weightMax;
|
||||
|
||||
private Integer sortNo;
|
||||
|
||||
/**
|
||||
* 是否计费并落库:false 不写入 pay_surcharge;null/缺省视为要计费(兼容应付全量保存)。
|
||||
* 仅作为前端传参,不落库字段。
|
||||
*/
|
||||
private Boolean needBill;
|
||||
}
|
||||
|
||||
@@ -16,8 +16,10 @@ public class PrcSurcharge extends BaseEntity
|
||||
private String scName;
|
||||
/** 字典 bms_fee_type */
|
||||
private String feeType;
|
||||
/** FIXED / WEIGHT / RATE */
|
||||
/** 计费方式:ALWAYS/每票都收、POSTCODE/命中邮编、ZONE/命中分区、EXTRA_KG/超重续收、WEIGHT/超重、SIZE/超尺寸、RETURN/退件、MANUAL/暂不计费 */
|
||||
private String chargeType;
|
||||
/** 渠道产品(字典 payment_product_type.dict_value) */
|
||||
private String dictValue;
|
||||
private BigDecimal chargeValue;
|
||||
private String currency;
|
||||
/** 字典 bms_round_mode:UP / DOWN / HALF_UP */
|
||||
@@ -29,6 +31,8 @@ public class PrcSurcharge extends BaseEntity
|
||||
public static final String IS_DEL_YES = "1";
|
||||
private String isDel;
|
||||
private String countryCode;
|
||||
/** 分区/区块(对应 pay_postcode_group.block_title) */
|
||||
private String blockTitle;
|
||||
private String scNote;
|
||||
private Long tmsId;
|
||||
}
|
||||
|
||||
@@ -60,6 +60,9 @@ public class TmsPriceBillRequest extends TmsPriceQuoteRequest
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date pickupDate;
|
||||
private String orderRemark;
|
||||
/** 上游 PRODUCT_FLOOR_CODE;打单计费在账单落库前也可直接用 */
|
||||
private String productFloorCode;
|
||||
|
||||
/** 上游 TAB_ORDER 整行,用于写入 prc_order_bill_ext 扩展字段 */
|
||||
private Map<String, Object> upstreamRow;
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ public class TmsPriceQuoteRequest implements Serializable
|
||||
private String productCode;
|
||||
|
||||
/**
|
||||
* 打单套餐(LABEL)试算/计费用渠道ID;计费也可从订单扩展 productChannelId 解析。
|
||||
* 打单套餐(LABEL)试算用渠道ID;计费优先取订单 product_floor_code。
|
||||
*/
|
||||
private String channelId;
|
||||
|
||||
|
||||
@@ -10,6 +10,10 @@ public interface PayPostcodeMapper
|
||||
{
|
||||
List<PayPostcodeGroup> selectGroupList(PayPostcodeGroup query);
|
||||
|
||||
/** 按国家去重区块标题 */
|
||||
List<String> selectBlockTitlesByCountry(@Param("countryCode") String countryCode,
|
||||
@Param("countryName") String countryName);
|
||||
|
||||
PayPostcodeGroup selectGroupById(Long groupId);
|
||||
|
||||
int updateGroup(PayPostcodeGroup group);
|
||||
|
||||
@@ -44,6 +44,7 @@ public class OracleOrderBillMapper
|
||||
}
|
||||
req.setProductCode(product);
|
||||
req.setProductName(firstStr(row, "LINE_NAME", "PRODUCT_NAME"));
|
||||
req.setProductFloorCode(firstStr(row, "PRODUCT_FLOOR_CODE"));
|
||||
req.setCountryCode(firstStr(row, "COUNTRY"));
|
||||
String countryName = firstStr(row, "COUNTRY_NAME", "COUNTRY_EINGTH_NAME", "COUNTRY");
|
||||
req.setCountryName(countryName);
|
||||
|
||||
@@ -0,0 +1,830 @@
|
||||
package com.bms.price.pay;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import com.bms.common.enums.PaymentMesEnum;
|
||||
import com.bms.price.domain.PayCostLine;
|
||||
import com.bms.price.domain.PayOrderCostResult;
|
||||
import com.bms.price.domain.PayProduct;
|
||||
import com.bms.price.domain.PaySurcharge;
|
||||
import com.bms.price.domain.PayWeightBand;
|
||||
|
||||
/**
|
||||
* 打单套餐应收计费(与应付 {@link PayCostCalculator} 独立)。
|
||||
* 按打单 AR 价卡的重量段、附加费计算;后续仅改本类,不影响应付计费。
|
||||
* 重量段价格默认是该段的一票价格;单位写明每公斤时才乘重量。
|
||||
*/
|
||||
public final class LabelArCostCalculator
|
||||
{
|
||||
private static final ZoneId SHANGHAI = ZoneId.of("GMT+8");
|
||||
|
||||
private static final Set<Integer> REFUND_STATUS_SET = Set.of(9, 10);
|
||||
|
||||
private LabelArCostCalculator()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* 按重量、尺寸和邮编计算一票打单应收。不指定地区,也不传入偏远命中结果。
|
||||
*
|
||||
* @param product 当前价卡版本,含重量段和附加费
|
||||
* @param weight 计费重,公斤
|
||||
* @param length 长,厘米
|
||||
* @param width 宽,厘米
|
||||
* @param height 高,厘米
|
||||
* @param postcode 收件邮编
|
||||
* @param bizDate 取价日,用来判断附加费是否在有效期内
|
||||
* @param orderStatus 订单状态,退件时才收退件类附加费
|
||||
* @return 费用明细和算不了的附加费说明
|
||||
*/
|
||||
public static PayOrderCostResult calculate(PayProduct product, BigDecimal weight, BigDecimal length, BigDecimal width,
|
||||
BigDecimal height, String postcode, Date bizDate, Integer orderStatus)
|
||||
{
|
||||
return calculate(product, weight, length, width, height, postcode, null, bizDate, orderStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在已经选定价卡地区时计算打单应收。偏远仍按价卡上的邮编条件判断。
|
||||
*
|
||||
* @param product 当前价卡版本
|
||||
* @param weight 计费重,公斤
|
||||
* @param length 长,厘米
|
||||
* @param width 宽,厘米
|
||||
* @param height 高,厘米
|
||||
* @param postcode 收件邮编
|
||||
* @param zoneName 邮编对上的价卡地区,空表示不按地区过滤
|
||||
* @param bizDate 取价日
|
||||
* @param orderStatus 订单状态
|
||||
* @return 费用明细和算不了的附加费说明
|
||||
*/
|
||||
public static PayOrderCostResult calculate(PayProduct product, BigDecimal weight, BigDecimal length, BigDecimal width,
|
||||
BigDecimal height, String postcode, String zoneName, Date bizDate, Integer orderStatus)
|
||||
{
|
||||
return calculate(product, weight, length, width, height, postcode, zoneName, bizDate, orderStatus, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算一票打单应收。remoteHits 为空列表表示产品有偏远邮编但这次没命中;为 null 表示没有这份配置,仍按价卡上的邮编条件。
|
||||
*
|
||||
* @param product 当前价卡版本
|
||||
* @param weight 计费重,公斤
|
||||
* @param length 长,厘米
|
||||
* @param width 宽,厘米
|
||||
* @param height 高,厘米
|
||||
* @param postcode 收件邮编
|
||||
* @param zoneName 价卡地区
|
||||
* @param bizDate 取价日
|
||||
* @param orderStatus 订单状态
|
||||
* @param remoteHits 偏远邮编命中结果
|
||||
* @return 费用明细和算不了的附加费说明
|
||||
*/
|
||||
public static PayOrderCostResult calculate(PayProduct product, BigDecimal weight, BigDecimal length, BigDecimal width,
|
||||
BigDecimal height, String postcode, String zoneName, Date bizDate, Integer orderStatus, List<PayRemoteBook.Hit> remoteHits)
|
||||
{
|
||||
PayOrderCostResult result = new PayOrderCostResult();
|
||||
result.setFees(new ArrayList<>());
|
||||
result.setSkipped(new ArrayList<>());
|
||||
if (product == null)
|
||||
{
|
||||
result.setMessage(PaymentMesEnum.BMS_PAY_NO_PRODUCT.getCnMes());
|
||||
return result;
|
||||
}
|
||||
result.setProductId(product.getProductId());
|
||||
result.setProductName(product.getProductName());
|
||||
if (weight == null || weight.compareTo(BigDecimal.ZERO) <= 0)
|
||||
{
|
||||
result.setMessage(PaymentMesEnum.BMS_PAY_NO_WEIGHT.getCnMes());
|
||||
return result;
|
||||
}
|
||||
List<PayWeightBand> usable = usableBands(product.getWeightBands());
|
||||
List<PayWeightBand> bands = forZone(usable, zoneName);
|
||||
if (zoneName != null && !zoneName.isBlank() && bands.isEmpty() && hasZoneFee(product, zoneName))
|
||||
{
|
||||
bands = deliveryBands(usable, product);
|
||||
}
|
||||
if (zoneName != null && !zoneName.isBlank() && bands.isEmpty())
|
||||
{
|
||||
result.setMessage("邮编对应的地区「" + zoneName + "」不在价卡重量段里");
|
||||
return result;
|
||||
}
|
||||
Set<String> zones = zones(bands);
|
||||
if (zones.size() > 1)
|
||||
{
|
||||
result.setMessage("产品「" + product.getProductName() + "」有多个地区(" + String.join("、", zones)
|
||||
+ "),邮编没有对上其中的地区,无法确定运费");
|
||||
return result;
|
||||
}
|
||||
PayWeightBand band = matchBand(bands, weight);
|
||||
if (band == null)
|
||||
{
|
||||
PayWeightBand top = highestBand(bands);
|
||||
if (top == null || weight.compareTo(top.getWeightMax()) <= 0
|
||||
|| !hasExtraKg(product, zoneName, top.getWeightMax(), top.getSectionName()))
|
||||
{
|
||||
result.setMessage(outOfRange(product, bands, weight));
|
||||
return result;
|
||||
}
|
||||
band = top;
|
||||
}
|
||||
String currency = currencyOf(product, band);
|
||||
BigDecimal freight = bandAmount(band, weight);
|
||||
String freightRemark = band.getWeightLabel() + " " + plain(band.getPrice()) + " " + blank(band.getPriceUnit());
|
||||
if (zoneName != null && !zoneName.isBlank())
|
||||
{
|
||||
freightRemark = freightRemark + ",地区 " + zoneName.replace('\n', ' ');
|
||||
}
|
||||
PayCostLine freightLine = line("BASIC_FREIGHT", "基础运费", 0L, freight, currency, freightRemark);
|
||||
result.getFees().add(freightLine);
|
||||
result.setCurrency(currency);
|
||||
addSurcharges(product, weight, length, width, height, postcode, zoneName, bizDate, orderStatus, freight, currency,
|
||||
band.getSectionName(), remoteHits, result);
|
||||
BigDecimal total = BigDecimal.ZERO;
|
||||
for (PayCostLine fee : result.getFees())
|
||||
{
|
||||
total = total.add(fee.getAmount());
|
||||
}
|
||||
result.setPayTotal(total.setScale(2, RoundingMode.HALF_UP));
|
||||
result.setMessage(summary(result));
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 邮编落到偏远分区,而价卡重量段只有大陆派送费时,运费用大陆档。 */
|
||||
private static boolean hasZoneFee(PayProduct product, String zoneName)
|
||||
{
|
||||
if (product.getSurcharges() == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (PaySurcharge fee : product.getSurcharges())
|
||||
{
|
||||
if (fee != null && PaySurchargeRules.ZONE.equals(fee.getCalcRule()) && PayZoneMatcher.sameZone(fee.getZoneName(), zoneName))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static List<PayWeightBand> deliveryBands(List<PayWeightBand> bands, PayProduct product)
|
||||
{
|
||||
List<String> remoteZones = new ArrayList<>();
|
||||
if (product.getSurcharges() != null)
|
||||
{
|
||||
for (PaySurcharge fee : product.getSurcharges())
|
||||
{
|
||||
if (fee != null && PaySurchargeRules.ZONE.equals(fee.getCalcRule()) && fee.getZoneName() != null && !fee.getZoneName().isBlank())
|
||||
{
|
||||
remoteZones.add(fee.getZoneName());
|
||||
}
|
||||
}
|
||||
}
|
||||
List<PayWeightBand> kept = new ArrayList<>();
|
||||
for (PayWeightBand band : bands)
|
||||
{
|
||||
boolean remote = false;
|
||||
for (String zone : remoteZones)
|
||||
{
|
||||
if (PayZoneMatcher.sameZone(band.getZoneName(), zone))
|
||||
{
|
||||
remote = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!remote)
|
||||
{
|
||||
kept.add(band);
|
||||
}
|
||||
}
|
||||
return kept;
|
||||
}
|
||||
|
||||
/** 首重对更重的包裹仍然收取,超出部分由续重另计。 */
|
||||
private static boolean weightAtLeast(PaySurcharge fee, BigDecimal weight)
|
||||
{
|
||||
if (fee.getWeightMin() == null || weight == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return weight.compareTo(fee.getWeightMin()) >= 0;
|
||||
}
|
||||
|
||||
private static boolean weightIn(PaySurcharge fee, BigDecimal weight)
|
||||
{
|
||||
if (weight == null || (fee.getWeightMin() == null && fee.getWeightMax() == null))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (fee.getWeightMin() != null && weight.compareTo(fee.getWeightMin()) < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return fee.getWeightMax() == null || weight.compareTo(fee.getWeightMax()) <= 0;
|
||||
}
|
||||
|
||||
private static List<PayWeightBand> forZone(List<PayWeightBand> bands, String zoneName)
|
||||
{
|
||||
if (zoneName == null || zoneName.isBlank())
|
||||
{
|
||||
return bands;
|
||||
}
|
||||
List<PayWeightBand> kept = new ArrayList<>();
|
||||
for (PayWeightBand band : bands)
|
||||
{
|
||||
if (PayZoneMatcher.sameZone(band.getZoneName(), zoneName))
|
||||
{
|
||||
kept.add(band);
|
||||
}
|
||||
}
|
||||
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)
|
||||
{
|
||||
if (product.getSurcharges() == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
List<PayCostLine> sized = new ArrayList<>();
|
||||
for (PaySurcharge fee : product.getSurcharges())
|
||||
{
|
||||
if (fee == null || !inForce(fee, bizDate))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String rule = fee.getCalcRule() == null || fee.getCalcRule().isBlank() ? PaySurchargeRules.MANUAL : fee.getCalcRule();
|
||||
if (remoteHits != null && remoteFee(fee))
|
||||
{
|
||||
String name = blank(fee.getFeeName());
|
||||
if (postcode == null || postcode.isBlank())
|
||||
{
|
||||
result.getSkipped().add(name + ":订单没有邮编");
|
||||
}
|
||||
else if (bookApplies(remoteHits, fee, rule))
|
||||
{
|
||||
String remark = "邮编 " + postcode.trim();
|
||||
if (PaySurchargeRules.EXTRA_KG.equals(rule))
|
||||
{
|
||||
addExtraKg(result.getFees(), fee, weight, currency);
|
||||
}
|
||||
else if (perKg(fee))
|
||||
{
|
||||
addPerKg(result.getFees(), fee, weight, currency, remark);
|
||||
}
|
||||
else
|
||||
{
|
||||
addFlat(result.getFees(), fee, currency, remark);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
boolean postcodeDriven = PaySurchargeRules.POSTCODE.equals(rule) || !blank(fee.getPostcodeParam()).isEmpty();
|
||||
if (!postcodeDriven && zoneName != null && !zoneName.isBlank() && fee.getZoneName() != null && !fee.getZoneName().isBlank()
|
||||
&& !PayZoneMatcher.sameZone(fee.getZoneName(), zoneName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String name = blank(fee.getFeeName());
|
||||
if (PaySurchargeRules.POSTCODE.equals(rule))
|
||||
{
|
||||
String param = blank(fee.getPostcodeParam()).isEmpty() ? fee.getCalcParam() : fee.getPostcodeParam();
|
||||
if (postcode == null || postcode.isBlank())
|
||||
{
|
||||
result.getSkipped().add(name + ":订单没有邮编");
|
||||
}
|
||||
else if (PaySurchargeRules.matchesPostcode(param, postcode) && weightAtLeast(fee, weight))
|
||||
{
|
||||
addFlat(result.getFees(), fee, currency, "邮编 " + postcode.trim());
|
||||
}
|
||||
}
|
||||
else if (PaySurchargeRules.ZONE.equals(rule))
|
||||
{
|
||||
boolean already = result.getFees().stream().anyMatch(line -> name.equals(line.getFeeName()));
|
||||
if (!already && zoneName != null && !zoneName.isBlank() && PayZoneMatcher.sameZone(fee.getZoneName(), zoneName) && weightIn(fee, weight))
|
||||
{
|
||||
addFlat(result.getFees(), fee, currency, "地区 " + zoneName.replace('\n', ' '));
|
||||
}
|
||||
}
|
||||
else if (PaySurchargeRules.WEIGHT.equals(rule))
|
||||
{
|
||||
if (PaySurchargeRules.matchesWeight(fee.getCalcParam(), weight))
|
||||
{
|
||||
addFlat(result.getFees(), fee, currency, "计费重超过 " + fee.getCalcParam() + "kg");
|
||||
}
|
||||
}
|
||||
// 超出尺寸费用
|
||||
else if (PaySurchargeRules.SIZE.equals(rule))
|
||||
{
|
||||
if (length == null || width == null || height == null)
|
||||
{
|
||||
result.getSkipped().add(name + ":订单没有长宽高");
|
||||
}
|
||||
else if (PaySurchargeRules.matchesSize(fee.getCalcParam(), length, width, height))
|
||||
{
|
||||
PayCostLine matched = matchedFee(fee, freight, currency, "尺寸命中 " + fee.getCalcParam());
|
||||
if (matched != null)
|
||||
{
|
||||
sized.add(matched);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 退件费用
|
||||
else if (PaySurchargeRules.RETURN.equals(rule))
|
||||
{
|
||||
if (orderStatus != null && REFUND_STATUS_SET.contains(orderStatus))
|
||||
{
|
||||
addFlat(result.getFees(), fee, currency, "订单已退件");
|
||||
}
|
||||
}
|
||||
// 超重费
|
||||
else if (PaySurchargeRules.EXTRA_KG.equals(rule))
|
||||
{
|
||||
if (postcodeDriven)
|
||||
{
|
||||
if (postcode == null || postcode.isBlank())
|
||||
{
|
||||
result.getSkipped().add(name + ":订单没有邮编");
|
||||
}
|
||||
else if (PaySurchargeRules.matchesPostcode(fee.getPostcodeParam(), postcode))
|
||||
{
|
||||
addExtraKg(result.getFees(), fee, weight, currency);
|
||||
}
|
||||
}
|
||||
else if (sameSection(fee.getSectionName(), freightSection))
|
||||
{
|
||||
addExtraKg(result.getFees(), fee, weight, currency);
|
||||
}
|
||||
}
|
||||
else if (PaySurchargeRules.ALWAYS.equals(rule))
|
||||
{
|
||||
PayCostLine matched = matchedFee(fee, freight, currency, blank(fee.getChargeText()));
|
||||
if (matched != null)
|
||||
{
|
||||
result.getFees().add(matched);
|
||||
}
|
||||
}
|
||||
else if (!name.isEmpty() && !name.contains("报价说明"))
|
||||
{
|
||||
result.getSkipped().add(name + ":价卡没有可计算的条件,暂不计费");
|
||||
}
|
||||
}
|
||||
result.getFees().addAll(keepHighestByName(sized));
|
||||
}
|
||||
|
||||
/** 同一费用名的尺寸档同时命中时,只收较高的一档,例如 70% 覆盖 35%。 */
|
||||
private static List<PayCostLine> keepHighestByName(List<PayCostLine> lines)
|
||||
{
|
||||
List<PayCostLine> kept = new ArrayList<>();
|
||||
for (PayCostLine line : lines)
|
||||
{
|
||||
PayCostLine same = null;
|
||||
for (PayCostLine exists : kept)
|
||||
{
|
||||
if (exists.getFeeName().equals(line.getFeeName()))
|
||||
{
|
||||
same = exists;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (same == null)
|
||||
{
|
||||
kept.add(line);
|
||||
}
|
||||
else if (line.getAmount().compareTo(same.getAmount()) > 0)
|
||||
{
|
||||
kept.set(kept.indexOf(same), line);
|
||||
}
|
||||
}
|
||||
return kept;
|
||||
}
|
||||
|
||||
private static void addExtraKg(List<PayCostLine> fees, PaySurcharge fee, BigDecimal weight, String currency)
|
||||
{
|
||||
if (fee.getAmount() == null || weight == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
BigDecimal threshold;
|
||||
try
|
||||
{
|
||||
threshold = new BigDecimal(fee.getCalcParam() == null ? "" : fee.getCalcParam().trim());
|
||||
}
|
||||
catch (NumberFormatException ex)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (weight.compareTo(threshold) <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
String feeCurrency = normalize(fee.getCurrency());
|
||||
if (!feeCurrency.isEmpty() && !feeCurrency.equals(currency))
|
||||
{
|
||||
return;
|
||||
}
|
||||
BigDecimal over = weight.subtract(threshold);
|
||||
BigDecimal amount = fee.getAmount().multiply(over).setScale(2, RoundingMode.HALF_UP);
|
||||
long scId = fee.getSurchargeId() == null ? 0L : fee.getSurchargeId();
|
||||
String remark = "超出 " + plain(over) + "kg × " + plain(fee.getAmount()) + " " + blank(fee.getPriceUnit());
|
||||
fees.add(line("SURCHARGE", blank(fee.getFeeName()), scId, amount, currency, remark));
|
||||
}
|
||||
|
||||
/** 同一地区有多张服务表时,续重只跟本次首重所在的那一张表。 */
|
||||
private static boolean sameSection(String feeSection, String freightSection)
|
||||
{
|
||||
if (feeSection == null || feeSection.isBlank() || freightSection == null || freightSection.isBlank())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return feeSection.trim().equals(freightSection.trim());
|
||||
}
|
||||
|
||||
/** 重量超过最高档,且该地区、该服务表有每公斤续重时,基础运费先用最高档。 */
|
||||
private static boolean hasExtraKg(PayProduct product, String zoneName, BigDecimal threshold, String section)
|
||||
{
|
||||
if (product.getSurcharges() == null || threshold == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (PaySurcharge fee : product.getSurcharges())
|
||||
{
|
||||
if (fee == null || !PaySurchargeRules.EXTRA_KG.equals(fee.getCalcRule()) || fee.getAmount() == null
|
||||
|| !blank(fee.getPostcodeParam()).isEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (zoneName != null && !zoneName.isBlank() && fee.getZoneName() != null && !fee.getZoneName().isBlank()
|
||||
&& !PayZoneMatcher.sameZone(fee.getZoneName(), zoneName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!sameSection(fee.getSectionName(), section))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
try
|
||||
{
|
||||
if (fee.getCalcParam() != null && new BigDecimal(fee.getCalcParam().trim()).compareTo(threshold) == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (NumberFormatException ex)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static PayWeightBand highestBand(List<PayWeightBand> bands)
|
||||
{
|
||||
PayWeightBand top = null;
|
||||
for (PayWeightBand band : bands)
|
||||
{
|
||||
if (top == null || band.getWeightMax().compareTo(top.getWeightMax()) > 0)
|
||||
{
|
||||
top = band;
|
||||
}
|
||||
}
|
||||
return top;
|
||||
}
|
||||
|
||||
private static boolean remoteFee(PaySurcharge fee)
|
||||
{
|
||||
String name = blank(fee.getFeeName()).toLowerCase(Locale.ROOT);
|
||||
return name.contains("偏远") || name.contains("岛屿") || name.contains("remote") || name.contains("island");
|
||||
}
|
||||
|
||||
/** 邮编配置命中后,金额相同的那一档才收;分区写明时还要对上地区。 */
|
||||
private static boolean bookApplies(List<PayRemoteBook.Hit> hits, PaySurcharge fee, String rule)
|
||||
{
|
||||
boolean extra = PaySurchargeRules.EXTRA_KG.equals(rule);
|
||||
boolean zoned = !blank(fee.getZoneName()).isEmpty();
|
||||
for (PayRemoteBook.Hit hit : hits)
|
||||
{
|
||||
if (hit == null || !sameFeeName(hit.getFeeName(), fee.getFeeName()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
boolean amountKnown = hit.getAmount() != null && fee.getAmount() != null;
|
||||
if (amountKnown && hit.getAmount().compareTo(fee.getAmount()) != 0 && !extra)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
boolean areaKnown = !blank(hit.getZoneName()).isEmpty() || !blank(hit.getRegionName()).isEmpty();
|
||||
if (zoned && areaKnown && !sameArea(hit, fee))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (extra && zoned && !sameArea(hit, fee))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (extra && amountKnown && hit.getAmount().compareTo(fee.getAmount()) != 0 && !sameArea(hit, fee))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean sameFeeName(String bookName, String feeName)
|
||||
{
|
||||
String book = feeKey(bookName);
|
||||
String fee = feeKey(feeName);
|
||||
if (book.isEmpty())
|
||||
{
|
||||
return fee.contains("偏远") || fee.contains("岛屿") || fee.contains("remote") || fee.contains("island");
|
||||
}
|
||||
return fee.contains(book) || book.contains(fee);
|
||||
}
|
||||
|
||||
private static String feeKey(String name)
|
||||
{
|
||||
return blank(name).toLowerCase(Locale.ROOT).replace("地区", "").replace(" ", "");
|
||||
}
|
||||
|
||||
private static boolean sameArea(PayRemoteBook.Hit hit, PaySurcharge fee)
|
||||
{
|
||||
return PayZoneMatcher.sameZone(fee.getZoneName(), hit.getZoneName())
|
||||
|| PayZoneMatcher.sameZone(fee.getZoneName(), hit.getRegionName());
|
||||
}
|
||||
|
||||
private static boolean perKg(PaySurcharge fee)
|
||||
{
|
||||
String unit = blank(fee.getPriceUnit()).toUpperCase(Locale.ROOT);
|
||||
return unit.contains("KG") || unit.contains("公斤");
|
||||
}
|
||||
|
||||
private static void addPerKg(List<PayCostLine> fees, PaySurcharge fee, BigDecimal weight, String currency, String remark)
|
||||
{
|
||||
if (fee.getAmount() == null || weight == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
String feeCurrency = normalize(fee.getCurrency());
|
||||
if (!feeCurrency.isEmpty() && !feeCurrency.equals(currency))
|
||||
{
|
||||
return;
|
||||
}
|
||||
BigDecimal amount = fee.getAmount().multiply(weight).setScale(2, RoundingMode.HALF_UP);
|
||||
long scId = fee.getSurchargeId() == null ? 0L : fee.getSurchargeId();
|
||||
fees.add(line("SURCHARGE", blank(fee.getFeeName()), scId, amount, currency, remark + " × " + plain(weight) + "kg"));
|
||||
}
|
||||
|
||||
private static void addFlat(List<PayCostLine> fees, PaySurcharge fee, String currency, String remark)
|
||||
{
|
||||
PayCostLine line = matchedFee(fee, null, currency, remark);
|
||||
if (line != null && !percent(fee))
|
||||
{
|
||||
fees.add(line);
|
||||
}
|
||||
}
|
||||
|
||||
private static PayCostLine matchedFee(PaySurcharge fee, BigDecimal freight, String currency, String remark)
|
||||
{
|
||||
if (fee.getAmount() == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
BigDecimal amount = fee.getAmount();
|
||||
if (percent(fee))
|
||||
{
|
||||
if (freight == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
amount = freight.multiply(fee.getAmount()).divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP);
|
||||
}
|
||||
else
|
||||
{
|
||||
amount = amount.setScale(2, RoundingMode.HALF_UP);
|
||||
String feeCurrency = normalize(fee.getCurrency());
|
||||
if (!feeCurrency.isEmpty() && !feeCurrency.equals(currency))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
long scId = fee.getSurchargeId() == null ? 0L : fee.getSurchargeId();
|
||||
return line("SURCHARGE", blank(fee.getFeeName()), scId, amount, currency, remark);
|
||||
}
|
||||
|
||||
private static boolean percent(PaySurcharge fee)
|
||||
{
|
||||
String charge = fee.getChargeText() == null ? "" : fee.getChargeText();
|
||||
return charge.contains("%") || "%".equals(fee.getCurrency());
|
||||
}
|
||||
|
||||
private static boolean inForce(PaySurcharge fee, Date bizDate)
|
||||
{
|
||||
if (bizDate == null || (fee.getEffectiveStart() == null && fee.getEffectiveEnd() == null))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
LocalDate day = toLocal(bizDate);
|
||||
if (fee.getEffectiveStart() != null && day.isBefore(toLocal(fee.getEffectiveStart())))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (fee.getEffectiveEnd() != null && day.isAfter(toLocal(fee.getEffectiveEnd())))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static List<PayWeightBand> usableBands(List<PayWeightBand> bands)
|
||||
{
|
||||
List<PayWeightBand> usable = new ArrayList<>();
|
||||
if (bands == null)
|
||||
{
|
||||
return usable;
|
||||
}
|
||||
for (PayWeightBand band : bands)
|
||||
{
|
||||
if (band != null && band.getPrice() != null && band.getWeightMin() != null && band.getWeightMax() != null)
|
||||
{
|
||||
usable.add(band);
|
||||
}
|
||||
}
|
||||
usable.sort((left, right) -> left.getWeightMin().compareTo(right.getWeightMin()));
|
||||
return usable;
|
||||
}
|
||||
|
||||
private static Set<String> zones(List<PayWeightBand> bands)
|
||||
{
|
||||
Set<String> zones = new LinkedHashSet<>();
|
||||
for (PayWeightBand band : bands)
|
||||
{
|
||||
if (band.getZoneName() != null && !band.getZoneName().isBlank())
|
||||
{
|
||||
zones.add(band.getZoneName().trim());
|
||||
}
|
||||
}
|
||||
return zones;
|
||||
}
|
||||
|
||||
private static PayWeightBand matchBand(List<PayWeightBand> bands, BigDecimal weight)
|
||||
{
|
||||
for (PayWeightBand band : bands)
|
||||
{
|
||||
if (weight.compareTo(band.getWeightMin()) >= 0 && weight.compareTo(band.getWeightMax()) <= 0)
|
||||
{
|
||||
return band;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < bands.size() - 1; i++)
|
||||
{
|
||||
PayWeightBand current = bands.get(i);
|
||||
PayWeightBand next = bands.get(i + 1);
|
||||
BigDecimal gap = next.getWeightMin().subtract(current.getWeightMax());
|
||||
if (weight.compareTo(current.getWeightMax()) > 0 && weight.compareTo(next.getWeightMin()) < 0
|
||||
&& gap.compareTo(new BigDecimal("0.01")) <= 0)
|
||||
{
|
||||
return next;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String outOfRange(PayProduct product, List<PayWeightBand> bands, BigDecimal weight)
|
||||
{
|
||||
String name = blank(product.getProductName());
|
||||
if (bands.isEmpty())
|
||||
{
|
||||
return "产品「" + name + "」没有可匹配的重量段";
|
||||
}
|
||||
BigDecimal max = bands.get(bands.size() - 1).getWeightMax();
|
||||
if (weight.compareTo(max) > 0)
|
||||
{
|
||||
return "计费重 " + plain(weight) + "kg 超出「" + name + "」的重量段,最大 " + plain(max) + "kg";
|
||||
}
|
||||
return "计费重 " + plain(weight) + "kg 没有落在「" + name + "」的重量段里";
|
||||
}
|
||||
|
||||
private static BigDecimal bandAmount(PayWeightBand band, BigDecimal weight)
|
||||
{
|
||||
BigDecimal price = band.getPrice();
|
||||
if (perKilogram(band))
|
||||
{
|
||||
return price.multiply(weight).setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
return price.setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
private static boolean perKilogram(PayWeightBand band)
|
||||
{
|
||||
String blob = (blank(band.getPriceUnit()) + " " + blank(band.getChargeName())).toLowerCase(Locale.ROOT);
|
||||
return blob.contains("/kg") || blob.contains("每公斤") || blob.contains("每千克") || blob.contains("/公斤");
|
||||
}
|
||||
|
||||
private static String currencyOf(PayProduct product, PayWeightBand band)
|
||||
{
|
||||
String unit = blank(band.getPriceUnit()).toUpperCase(Locale.ROOT);
|
||||
if (unit.contains("GBP") || unit.contains("英镑"))
|
||||
{
|
||||
return "GBP";
|
||||
}
|
||||
if (unit.contains("EUR") || unit.contains("欧元"))
|
||||
{
|
||||
return "EUR";
|
||||
}
|
||||
if (unit.contains("USD") || unit.contains("美元"))
|
||||
{
|
||||
return "USD";
|
||||
}
|
||||
if (unit.contains("CNY") || unit.contains("RMB") || unit.contains("人民币"))
|
||||
{
|
||||
return "CNY";
|
||||
}
|
||||
String currency = blank(product.getCurrency());
|
||||
return currency.isEmpty() ? "CNY" : currency;
|
||||
}
|
||||
|
||||
private static PayCostLine line(String feeType, String feeName, Long scId, BigDecimal amount, String currency, String remark)
|
||||
{
|
||||
PayCostLine line = new PayCostLine();
|
||||
line.setFeeType(feeType);
|
||||
line.setFeeName(feeName);
|
||||
line.setScId(scId);
|
||||
line.setAmount(amount.setScale(2, RoundingMode.HALF_UP));
|
||||
line.setCurrency(currency);
|
||||
line.setRemark(remark == null ? "" : remark.trim());
|
||||
return line;
|
||||
}
|
||||
|
||||
private static String summary(PayOrderCostResult result)
|
||||
{
|
||||
String text = "打单应收 " + plain(result.getPayTotal()) + " " + result.getCurrency();
|
||||
if (result.getSkipped() != null && !result.getSkipped().isEmpty())
|
||||
{
|
||||
text = text + ",另有 " + result.getSkipped().size() + " 项附加费未自动计算";
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
private static LocalDate toLocal(Date date)
|
||||
{
|
||||
if (date instanceof java.sql.Date sql)
|
||||
{
|
||||
return sql.toLocalDate();
|
||||
}
|
||||
return date.toInstant().atZone(SHANGHAI).toLocalDate();
|
||||
}
|
||||
|
||||
private static String plain(BigDecimal value)
|
||||
{
|
||||
return value == null ? "" : value.stripTrailingZeros().toPlainString();
|
||||
}
|
||||
|
||||
private static String blank(String value)
|
||||
{
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
private static String normalize(String currency)
|
||||
{
|
||||
if (currency == null || currency.isBlank() || "%".equals(currency.trim()))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
String value = currency.trim().toUpperCase(Locale.ROOT);
|
||||
if ("英镑".equals(currency))
|
||||
{
|
||||
return "GBP";
|
||||
}
|
||||
if ("欧元".equals(currency))
|
||||
{
|
||||
return "EUR";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,8 @@ import com.bms.price.domain.PaySurcharge;
|
||||
import com.bms.price.domain.PayWeightBand;
|
||||
|
||||
/**
|
||||
* 用渠道上的应付产品,按订单重量、尺寸、邮编和是否退件计算应付。
|
||||
* 应付(AP)计费:用渠道应付产品按重量、尺寸、邮编和是否退件计算。
|
||||
* 打单应收请用 {@link LabelArCostCalculator},两者独立,互不影响。
|
||||
* 重量段价格默认是该段的一票价格;单位写明每公斤时才乘重量。
|
||||
*/
|
||||
public final class PayCostCalculator
|
||||
|
||||
@@ -10,6 +10,9 @@ public interface IPayPostcodeService
|
||||
{
|
||||
List<PayPostcodeGroup> selectGroupList(PayPostcodeGroup query);
|
||||
|
||||
/** 附加费分区下拉:按国家去重区块标题 */
|
||||
List<String> selectBlockTitlesByCountry(String countryCode, String countryName);
|
||||
|
||||
PayPostcodeGroup selectGroupById(Long groupId);
|
||||
|
||||
int updateGroup(PayPostcodeGroup group);
|
||||
|
||||
+6
@@ -34,6 +34,12 @@ public class PayPostcodeServiceImpl implements IPayPostcodeService
|
||||
return payPostcodeMapper.selectGroupList(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> selectBlockTitlesByCountry(String countryCode, String countryName)
|
||||
{
|
||||
return payPostcodeMapper.selectBlockTitlesByCountry(countryCode, countryName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayPostcodeGroup selectGroupById(Long groupId)
|
||||
{
|
||||
|
||||
+24
@@ -461,6 +461,8 @@ public class PayProductServiceImpl implements IPayProductService
|
||||
{
|
||||
product.setExtraLines(new ArrayList<>());
|
||||
}
|
||||
// 计数前先按「是否计费」过滤,与落库行数一致
|
||||
retainBillableSurcharges(product);
|
||||
product.setWeightCount(product.getWeightBands().size());
|
||||
product.setSurchargeCount(product.getSurcharges().size());
|
||||
product.setExtraCount(product.getExtraLines().size());
|
||||
@@ -1529,6 +1531,7 @@ public class PayProductServiceImpl implements IPayProductService
|
||||
|
||||
private void insertSurcharges(PayProduct product)
|
||||
{
|
||||
retainBillableSurcharges(product);
|
||||
List<PaySurcharge> rows = product.getSurcharges();
|
||||
if (rows == null || rows.isEmpty())
|
||||
{
|
||||
@@ -1547,6 +1550,27 @@ public class PayProductServiceImpl implements IPayProductService
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅保留需要计费的附加费:needBill=false 不入库;null 视为要计费。
|
||||
*/
|
||||
private void retainBillableSurcharges(PayProduct product)
|
||||
{
|
||||
List<PaySurcharge> rows = product.getSurcharges();
|
||||
if (rows == null || rows.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
List<PaySurcharge> kept = new ArrayList<PaySurcharge>();
|
||||
for (PaySurcharge fee : rows)
|
||||
{
|
||||
if (fee != null && !Boolean.FALSE.equals(fee.getNeedBill()))
|
||||
{
|
||||
kept.add(fee);
|
||||
}
|
||||
}
|
||||
product.setSurcharges(kept);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按附加费名称映射 prc_surcharge.sc_id;币种不能为空:附加费 -> 价卡 -> EUR。
|
||||
*/
|
||||
|
||||
+6
-13
@@ -1716,21 +1716,13 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
RoundingMode rm = resolveFeeRoundingMode(surcharge.getRoundMode());
|
||||
String chargeType = surcharge.getChargeType() == null ? "FIXED" : surcharge.getChargeType().toUpperCase();
|
||||
BigDecimal amount;
|
||||
if ("WEIGHT".equals(chargeType))
|
||||
String chargeType = surcharge.getChargeType() == null ? "MANUAL" : surcharge.getChargeType().toUpperCase();
|
||||
// MANUAL=暂不计费;其他计费方式暂按固定金额 charge_value
|
||||
if ("MANUAL".equals(chargeType))
|
||||
{
|
||||
amount = surcharge.getChargeValue().multiply(nz(weight));
|
||||
return BigDecimal.ZERO.setScale(4, rm);
|
||||
}
|
||||
else if ("RATE".equals(chargeType))
|
||||
{
|
||||
amount = nz(freightAmount).multiply(surcharge.getChargeValue());
|
||||
}
|
||||
else
|
||||
{
|
||||
amount = surcharge.getChargeValue();
|
||||
}
|
||||
return amount.setScale(4, rm);
|
||||
return surcharge.getChargeValue().setScale(4, rm);
|
||||
}
|
||||
|
||||
private RoundingMode resolveFeeRoundingMode(String roundMode)
|
||||
@@ -3180,6 +3172,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
req.setCustomerName(bill.getCustomerName());
|
||||
req.setProductCode(bill.getProductCode());
|
||||
req.setProductName(resolveBillProductName(bill));
|
||||
req.setProductFloorCode(bill.getProductFloorCode());
|
||||
req.setCountryCode(bill.getCountryCode());
|
||||
req.setCountryName(bill.getCountryName());
|
||||
req.setDestPostcode(bill.getDestPostcode());
|
||||
|
||||
+8
@@ -79,11 +79,13 @@ public class PrcSurchargeServiceImpl implements IPrcSurchargeService
|
||||
fresh.setScName(surcharge.getScName());
|
||||
fresh.setFeeType(StringUtils.isNotEmpty(surcharge.getFeeType()) ? surcharge.getFeeType() : old.getFeeType());
|
||||
fresh.setChargeType(surcharge.getChargeType());
|
||||
fresh.setDictValue(surcharge.getDictValue());
|
||||
fresh.setChargeValue(surcharge.getChargeValue());
|
||||
fresh.setCurrency(surcharge.getCurrency());
|
||||
fresh.setRoundMode(surcharge.getRoundMode());
|
||||
fresh.setSsCode(old.getSsCode());
|
||||
fresh.setCountryCode(surcharge.getCountryCode());
|
||||
fresh.setBlockTitle(surcharge.getBlockTitle());
|
||||
fresh.setScNote(surcharge.getScNote());
|
||||
applyInsertDefaults(fresh);
|
||||
fresh.setCreateBy(operator);
|
||||
@@ -99,10 +101,12 @@ public class PrcSurchargeServiceImpl implements IPrcSurchargeService
|
||||
return eq(old.getScName(), updated.getScName())
|
||||
&& eq(old.getFeeType(), updated.getFeeType())
|
||||
&& eq(old.getChargeType(), updated.getChargeType())
|
||||
&& eq(old.getDictValue(), updated.getDictValue())
|
||||
&& eqDecimal(old.getChargeValue(), updated.getChargeValue())
|
||||
&& eq(old.getCurrency(), updated.getCurrency())
|
||||
&& eq(old.getRoundMode(), updated.getRoundMode())
|
||||
&& eq(old.getCountryCode(), updated.getCountryCode())
|
||||
&& eq(old.getBlockTitle(), updated.getBlockTitle())
|
||||
&& eq(old.getScNote(), updated.getScNote());
|
||||
}
|
||||
|
||||
@@ -161,6 +165,10 @@ public class PrcSurchargeServiceImpl implements IPrcSurchargeService
|
||||
{
|
||||
surcharge.setRoundMode("HALF_UP");
|
||||
}
|
||||
if (StringUtils.isEmpty(surcharge.getChargeType()))
|
||||
{
|
||||
surcharge.setChargeType("MANUAL");
|
||||
}
|
||||
}
|
||||
|
||||
private void validate(PrcSurcharge surcharge)
|
||||
|
||||
+57
-26
@@ -32,6 +32,7 @@ import com.bms.price.domain.PayCostLine;
|
||||
import com.bms.price.domain.PayOrderCostResult;
|
||||
import com.bms.price.domain.PayProduct;
|
||||
import com.bms.price.domain.PrcCustomerPriceSet;
|
||||
import com.bms.price.domain.PrcOrderBill;
|
||||
import com.bms.price.domain.PrcOrderBillExt;
|
||||
import com.bms.price.domain.PrcPriceSet;
|
||||
import com.bms.price.domain.PrcPriceSetDetail;
|
||||
@@ -41,7 +42,7 @@ import com.bms.price.domain.PrcQuote;
|
||||
import com.bms.price.domain.PrcQuoteFee;
|
||||
import com.bms.price.domain.PrcSurchargeRel;
|
||||
import com.bms.price.domain.PrcZone;
|
||||
import com.bms.price.pay.PayCostCalculator;
|
||||
import com.bms.price.pay.LabelArCostCalculator;
|
||||
import com.bms.price.mapper.PrcZoneDetailMapper;
|
||||
import com.bms.price.domain.vo.TmsPriceBillRequest;
|
||||
import com.bms.price.domain.vo.TmsPriceQuoteFeeLine;
|
||||
@@ -52,8 +53,11 @@ import com.bms.price.enums.BillCustomerActiveStatus;
|
||||
import com.bms.price.mapper.BmsProductCountryMapper;
|
||||
import com.bms.price.mapper.BmsProductMapper;
|
||||
import com.bms.price.mapper.BmsRedlineProductCountryMapper;
|
||||
import com.bms.masterdata.domain.ServiceChannel;
|
||||
import com.bms.masterdata.mapper.ServiceChannelMapper;
|
||||
import com.bms.price.mapper.PrcCustomerPriceSetMapper;
|
||||
import com.bms.price.mapper.PrcOrderBillExtMapper;
|
||||
import com.bms.price.mapper.PrcOrderBillMapper;
|
||||
import com.bms.price.mapper.PrcPriceSetDetailMapper;
|
||||
import com.bms.price.mapper.PrcPriceSetMapper;
|
||||
import com.bms.price.mapper.PrcPriceSetProductMapper;
|
||||
@@ -143,6 +147,12 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
@Autowired
|
||||
private PrcOrderBillExtMapper orderBillExtMapper;
|
||||
|
||||
@Autowired
|
||||
private PrcOrderBillMapper orderBillMapper;
|
||||
|
||||
@Autowired
|
||||
private ServiceChannelMapper serviceChannelMapper;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public TmsPriceQuoteResult quote(TmsPriceQuoteRequest request)
|
||||
@@ -199,7 +209,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
return result;
|
||||
}
|
||||
|
||||
/** LABEL 费用已由 PayCostCalculator 算完,不再叠加销售附加费。 */
|
||||
/** LABEL 费用已由 LabelArCostCalculator 算完,不再叠加销售附加费。 */
|
||||
private void applySalesSurchargesIfNeeded(TmsPriceQuoteResult result)
|
||||
{
|
||||
if (result == null || SRC_LABEL.equals(result.getMatchSource()))
|
||||
@@ -655,7 +665,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
}
|
||||
|
||||
/**
|
||||
* 打单套餐计费:渠道 AR → product_id,套餐+时间 → term,PayCostCalculator 算费。
|
||||
* 打单套餐计费:渠道 AR → product_id,套餐+时间 → term,LabelArCostCalculator 算费(与应付独立)。
|
||||
*/
|
||||
private TmsPriceQuoteResult doLabelQuote(TmsPriceQuoteRequest request, PrcCustomerPriceSet bind,
|
||||
PrcPriceSet set, String productCode, String productName, String countryCode, String countryName,
|
||||
@@ -697,7 +707,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
height = billReq.getPkgHeight();
|
||||
orderStatus = billReq.getOrderStatus();
|
||||
}
|
||||
PayOrderCostResult cost = PayCostCalculator.calculate(product, weight, length, width, height,
|
||||
PayOrderCostResult cost = LabelArCostCalculator.calculate(product, weight, length, width, height,
|
||||
request.getDestPostcode(), bizDate, orderStatus);
|
||||
if (cost == null || cost.getFees() == null || cost.getFees().isEmpty())
|
||||
{
|
||||
@@ -715,7 +725,10 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
return r;
|
||||
}
|
||||
|
||||
/** 试算用 request.channelId;计费优先订单扩展 productChannelId。 */
|
||||
/**
|
||||
* 试算用 request.channelId;计费优先 prc_order_bill.product_floor_code,
|
||||
* 没有再取 prc_order_bill_ext.product_floor_code,再解析为渠道ID。
|
||||
*/
|
||||
private String resolveLabelChannelId(TmsPriceQuoteRequest request)
|
||||
{
|
||||
if (request != null && StringUtils.isNotEmpty(request.getChannelId()))
|
||||
@@ -727,22 +740,48 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
return null;
|
||||
}
|
||||
TmsPriceBillRequest billReq = (TmsPriceBillRequest) request;
|
||||
PrcOrderBillExt ext = null;
|
||||
if (StringUtils.isNotEmpty(billReq.getOrderNo()))
|
||||
String floorCode = emptyToNull(billReq.getProductFloorCode());
|
||||
String orderNo = emptyToNull(billReq.getOrderNo());
|
||||
if (StringUtils.isEmpty(floorCode) && StringUtils.isNotEmpty(orderNo))
|
||||
{
|
||||
ext = orderBillExtMapper.selectByOrderNo(billReq.getOrderNo().trim());
|
||||
PrcOrderBill bill = orderBillMapper.selectPrcOrderBillByOrderNo(orderNo);
|
||||
if (bill != null)
|
||||
{
|
||||
floorCode = emptyToNull(bill.getProductFloorCode());
|
||||
}
|
||||
}
|
||||
if (ext == null)
|
||||
if (StringUtils.isEmpty(floorCode) && StringUtils.isNotEmpty(orderNo))
|
||||
{
|
||||
PrcOrderBillExt ext = orderBillExtMapper.selectByOrderNo(orderNo);
|
||||
if (ext != null)
|
||||
{
|
||||
floorCode = emptyToNull(ext.getProductFloorCode());
|
||||
}
|
||||
}
|
||||
if (StringUtils.isEmpty(floorCode))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (StringUtils.isNotEmpty(ext.getProductChannelId()))
|
||||
return resolveChannelIdByFloorCode(floorCode);
|
||||
}
|
||||
|
||||
/** 将 product_floor_code 解析为 srv_service_channel.id(先按编码,再按ID) */
|
||||
private String resolveChannelIdByFloorCode(String floorCode)
|
||||
{
|
||||
if (StringUtils.isEmpty(floorCode) || serviceChannelMapper == null)
|
||||
{
|
||||
return ext.getProductChannelId().trim();
|
||||
return null;
|
||||
}
|
||||
if (StringUtils.isNotEmpty(ext.getProductFloorCode()))
|
||||
String code = floorCode.trim();
|
||||
ServiceChannel byCode = serviceChannelMapper.selectServiceChannelByCode(code);
|
||||
if (byCode != null && StringUtils.isNotEmpty(byCode.getId()))
|
||||
{
|
||||
return ext.getProductFloorCode().trim();
|
||||
return byCode.getId().trim();
|
||||
}
|
||||
ServiceChannel byId = serviceChannelMapper.selectServiceChannelById(code);
|
||||
if (byId != null && StringUtils.isNotEmpty(byId.getId()))
|
||||
{
|
||||
return byId.getId().trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -1802,21 +1841,13 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
RoundingMode rm = resolveRoundingMode(rel.getRoundMode());
|
||||
String chargeType = rel.getChargeType() == null ? "FIXED" : rel.getChargeType().toUpperCase();
|
||||
BigDecimal amount;
|
||||
if ("WEIGHT".equals(chargeType))
|
||||
String chargeType = rel.getChargeType() == null ? "MANUAL" : rel.getChargeType().toUpperCase();
|
||||
// MANUAL=暂不计费;其他计费方式暂按固定金额 charge_value
|
||||
if ("MANUAL".equals(chargeType))
|
||||
{
|
||||
amount = rel.getChargeValue().multiply(nvl(weight));
|
||||
return BigDecimal.ZERO.setScale(2, rm);
|
||||
}
|
||||
else if ("RATE".equals(chargeType))
|
||||
{
|
||||
amount = nvl(freightAmount).multiply(rel.getChargeValue());
|
||||
}
|
||||
else
|
||||
{
|
||||
amount = rel.getChargeValue();
|
||||
}
|
||||
return amount.setScale(2, rm);
|
||||
return rel.getChargeValue().setScale(2, rm);
|
||||
}
|
||||
|
||||
private void persistQuote(String quoteKey, TmsPriceQuoteRequest request, BigDecimal weight,
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.bms.receivable.service;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.bms.receivable.domain.ArBill;
|
||||
import com.bms.receivable.domain.ArBankAccount;
|
||||
import com.bms.receivable.domain.ArBillDetail;
|
||||
@@ -85,6 +86,8 @@ public interface IArBillService
|
||||
|
||||
List<ArBillExport> selectExports(Long arBillId);
|
||||
|
||||
Map<String, Object> getExportDownloadInfo(Long exportId);
|
||||
|
||||
void downloadExport(Long exportId, jakarta.servlet.http.HttpServletResponse response) throws Exception;
|
||||
|
||||
List<ArBillLog> selectLogList(ArBillLog query);
|
||||
|
||||
+34
-8
@@ -119,6 +119,7 @@ public class ArBillServiceImpl implements IArBillService {
|
||||
private static final Logger log = LoggerFactory.getLogger(ArBillServiceImpl.class);
|
||||
private static final String EXPORT_TEMPLATE_BUSINESS_TYPE = "AR_BILL";
|
||||
private static final String EXPORT_TEMPLATE_RESOURCE = "templates/receivable/ar_bill_template.xlsx";
|
||||
private static final long EXPORT_DOWNLOAD_URL_TTL_MILLIS = 10 * 60 * 1000L;
|
||||
/**
|
||||
* Excel 行号从 1 开始;模板第 4 行是表头,明细从第 5 行开始写入。
|
||||
*/
|
||||
@@ -1245,17 +1246,29 @@ public class ArBillServiceImpl implements IArBillService {
|
||||
return exports;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getExportDownloadInfo(Long exportId) {
|
||||
ArBillExport export = requireDownloadableExport(exportId);
|
||||
String resource = export.getStorageKey();
|
||||
Map<String, Object> info = new HashMap<>(3);
|
||||
if (resource.startsWith("oss://")) {
|
||||
Date expiresAt = new Date(System.currentTimeMillis() + EXPORT_DOWNLOAD_URL_TTL_MILLIS);
|
||||
info.put("direct", true);
|
||||
info.put("downloadUrl", ossStorage.generatePresignedDownloadUrl(resource, export.getFileName(), expiresAt));
|
||||
info.put("expiresAt", expiresAt.getTime());
|
||||
} else {
|
||||
if (!FileUtils.checkAllowDownload(resource)) {
|
||||
throw new ServiceException("导出文件路径非法");
|
||||
}
|
||||
info.put("direct", false);
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void downloadExport(Long exportId, HttpServletResponse response) throws Exception {
|
||||
long startedAt = System.nanoTime();
|
||||
ArBillExport export = exportMapper.selectById(exportId);
|
||||
if (export == null) {
|
||||
throw new ServiceException("账单不存在或无权访问");
|
||||
}
|
||||
billScope.requireBillAccessible(export.getArBillId());
|
||||
if (!ArExportStatus.SUCCESS.getCode().equals(export.getExportStatus()) || StringUtils.isEmpty(export.getStorageKey())) {
|
||||
throw new ServiceException("导出文件不存在或尚未生成成功");
|
||||
}
|
||||
ArBillExport export = requireDownloadableExport(exportId);
|
||||
String resource = export.getStorageKey();
|
||||
boolean ossFile = resource.startsWith("oss://");
|
||||
if (!ossFile && !FileUtils.checkAllowDownload(resource)) {
|
||||
@@ -1272,6 +1285,19 @@ public class ArBillServiceImpl implements IArBillService {
|
||||
exportId, export.getFileSize(), elapsedMillis(startedAt));
|
||||
}
|
||||
|
||||
private ArBillExport requireDownloadableExport(Long exportId) {
|
||||
ArBillExport export = exportMapper.selectById(exportId);
|
||||
if (export == null) {
|
||||
throw new ServiceException("账单不存在或无权访问");
|
||||
}
|
||||
billScope.requireBillAccessible(export.getArBillId());
|
||||
if (!ArExportStatus.SUCCESS.getCode().equals(export.getExportStatus())
|
||||
|| StringUtils.isEmpty(export.getStorageKey())) {
|
||||
throw new ServiceException("导出文件不存在或尚未生成成功");
|
||||
}
|
||||
return export;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ArBillLog> selectLogList(ArBillLog query) {
|
||||
long startedAt = System.nanoTime();
|
||||
|
||||
@@ -30,6 +30,26 @@
|
||||
order by group_id desc
|
||||
</select>
|
||||
|
||||
<select id="selectBlockTitlesByCountry" resultType="string">
|
||||
select distinct block_title
|
||||
from pay_postcode_group
|
||||
where block_title is not null
|
||||
and block_title <> ''
|
||||
<if test="countryCode != null and countryCode != ''">
|
||||
and (
|
||||
upper(ifnull(country_code, '')) = upper(#{countryCode})
|
||||
or upper(ifnull(catalog_country, '')) = upper(#{countryCode})
|
||||
)
|
||||
</if>
|
||||
<if test="countryName != null and countryName != ''">
|
||||
and (
|
||||
country_name like concat('%', #{countryName}, '%')
|
||||
or catalog_country like concat('%', #{countryName}, '%')
|
||||
)
|
||||
</if>
|
||||
order by block_title asc
|
||||
</select>
|
||||
|
||||
<select id="selectLineList" parameterType="com.bms.price.domain.PayPostcodeLine" resultType="com.bms.price.domain.PayPostcodeLine">
|
||||
select line_id as lineId, group_id as groupId, country_name as countryName, country_code as countryCode,
|
||||
region_name as regionName, region_name_cn as regionNameCn, city_name as cityName, region_code as regionCode,
|
||||
|
||||
@@ -694,14 +694,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
#{excludedBillId}
|
||||
</foreach>
|
||||
</if>
|
||||
<choose>
|
||||
<when test="dateType != null and dateType == 'PICKUP_DATE'">
|
||||
order by p.pickup_date, p.order_no, p.bill_id
|
||||
</when>
|
||||
<otherwise>
|
||||
order by p.biz_date, p.order_no, p.bill_id
|
||||
</otherwise>
|
||||
</choose>
|
||||
order by p.bill_id
|
||||
</select>
|
||||
|
||||
<select id="sumPrcOrderBillIncomeByCustomerPeriod" resultType="java.math.BigDecimal">
|
||||
|
||||
@@ -9,12 +9,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<result property="scName" column="sc_name" />
|
||||
<result property="feeType" column="fee_type" />
|
||||
<result property="chargeType" column="charge_type" />
|
||||
<result property="dictValue" column="dict_value" />
|
||||
<result property="chargeValue" column="charge_value" />
|
||||
<result property="currency" column="currency" />
|
||||
<result property="roundMode" column="round_mode" />
|
||||
<result property="ssCode" column="ss_code" />
|
||||
<result property="isDel" column="is_del" />
|
||||
<result property="countryCode" column="country_code" />
|
||||
<result property="blockTitle" column="block_title" />
|
||||
<result property="scNote" column="sc_note" />
|
||||
<result property="tmsId" column="tms_id" />
|
||||
<result property="createBy" column="created_by" />
|
||||
@@ -24,8 +26,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectPrcSurchargeVo">
|
||||
select sc_id, sc_name, fee_type, charge_type, charge_value, currency, round_mode, ss_code, is_del,
|
||||
country_code, sc_note, tms_id, created_by, create_time, updated_by, update_time
|
||||
select sc_id, sc_name, fee_type, charge_type, dict_value, charge_value, currency, round_mode,
|
||||
ss_code, is_del, country_code, block_title, sc_note, tms_id, created_by, create_time, updated_by, update_time
|
||||
from prc_surcharge
|
||||
</sql>
|
||||
|
||||
@@ -55,6 +57,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="chargeType != null and chargeType != ''">
|
||||
AND charge_type = #{chargeType}
|
||||
</if>
|
||||
<if test="dictValue != null and dictValue != ''">
|
||||
AND dict_value = #{dictValue}
|
||||
</if>
|
||||
<if test="blockTitle != null and blockTitle != ''">
|
||||
AND block_title like concat('%', #{blockTitle}, '%')
|
||||
</if>
|
||||
<if test="roundMode != null and roundMode != ''">
|
||||
AND round_mode = #{roundMode}
|
||||
</if>
|
||||
@@ -82,12 +90,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="scName != null and scName != ''">sc_name,</if>
|
||||
<if test="feeType != null and feeType != ''">fee_type,</if>
|
||||
<if test="chargeType != null and chargeType != ''">charge_type,</if>
|
||||
<if test="dictValue != null">dict_value,</if>
|
||||
<if test="chargeValue != null">charge_value,</if>
|
||||
<if test="currency != null and currency != ''">currency,</if>
|
||||
<if test="roundMode != null and roundMode != ''">round_mode,</if>
|
||||
<if test="ssCode != null and ssCode != ''">ss_code,</if>
|
||||
is_del,
|
||||
<if test="countryCode != null">country_code,</if>
|
||||
<if test="blockTitle != null">block_title,</if>
|
||||
<if test="scNote != null">sc_note,</if>
|
||||
<if test="tmsId != null">tms_id,</if>
|
||||
<if test="createBy != null and createBy != ''">created_by,</if>
|
||||
@@ -96,12 +106,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="scName != null and scName != ''">#{scName},</if>
|
||||
<if test="feeType != null and feeType != ''">#{feeType},</if>
|
||||
<if test="chargeType != null and chargeType != ''">#{chargeType},</if>
|
||||
<if test="dictValue != null">#{dictValue},</if>
|
||||
<if test="chargeValue != null">#{chargeValue},</if>
|
||||
<if test="currency != null and currency != ''">#{currency},</if>
|
||||
<if test="roundMode != null and roundMode != ''">#{roundMode},</if>
|
||||
<if test="ssCode != null and ssCode != ''">#{ssCode},</if>
|
||||
ifnull(#{isDel}, '0'),
|
||||
<if test="countryCode != null">#{countryCode},</if>
|
||||
<if test="blockTitle != null">#{blockTitle},</if>
|
||||
<if test="scNote != null">#{scNote},</if>
|
||||
<if test="tmsId != null">#{tmsId},</if>
|
||||
<if test="createBy != null and createBy != ''">#{createBy},</if>
|
||||
|
||||
@@ -11,20 +11,27 @@
|
||||
AND bill_id IN
|
||||
<foreach collection="sourceIds" item="sourceId" open="(" separator="," close=")">#{sourceId}</foreach>
|
||||
</select>
|
||||
<!-- c 固定为客户表别名;无部门层级扩展,不按角色管理 data_scope 过滤。 -->
|
||||
<!-- c 固定为客户表别名;销售经理可见本部门及下级部门,不按角色管理 data_scope 过滤。 -->
|
||||
<sql id="customerScope">
|
||||
<choose>
|
||||
<when test="scope != null and scope.all"/>
|
||||
<when test="scope != null and (scope.self or scope.department)">
|
||||
AND c.del_flag = '0'
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM sys_user su
|
||||
WHERE CAST(su.user_id AS CHAR) = c.sales_user_id AND su.del_flag = '0'
|
||||
AND (1 = 0
|
||||
<if test="scope.self">OR su.user_id = #{scope.userId}</if>
|
||||
<if test="scope.department">OR su.dept_id = #{scope.deptId}</if>
|
||||
SELECT 1 FROM sys_user su
|
||||
WHERE CAST(su.user_id AS CHAR) = c.sales_user_id AND su.del_flag = '0'
|
||||
AND (1 = 0
|
||||
<if test="scope.self">OR su.user_id = #{scope.userId}</if>
|
||||
<if test="scope.department">
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM sys_dept dept
|
||||
WHERE dept.dept_id = su.dept_id
|
||||
AND (dept.dept_id = #{scope.deptId}
|
||||
OR find_in_set(#{scope.deptId}, dept.ancestors))
|
||||
)
|
||||
</if>
|
||||
)
|
||||
)
|
||||
)
|
||||
</when>
|
||||
<otherwise>AND 1 = 0</otherwise>
|
||||
</choose>
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
-- prc_surcharge: drop calc_rule; keep dict_value / block_title; charge_type stores billing mode
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
-- If calc_rule exists, copy into charge_type then drop
|
||||
SET @col := (
|
||||
SELECT COLUMN_NAME FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'prc_surcharge'
|
||||
AND COLUMN_NAME = 'calc_rule'
|
||||
LIMIT 1
|
||||
);
|
||||
SET @sql := IF(@col IS NULL,
|
||||
'SELECT 1',
|
||||
'UPDATE prc_surcharge SET charge_type = calc_rule WHERE calc_rule IS NOT NULL AND calc_rule <> ''''');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @col := (
|
||||
SELECT COLUMN_NAME FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'prc_surcharge'
|
||||
AND COLUMN_NAME = 'calc_rule'
|
||||
LIMIT 1
|
||||
);
|
||||
SET @sql := IF(@col IS NULL,
|
||||
'SELECT 1',
|
||||
'ALTER TABLE prc_surcharge DROP COLUMN calc_rule');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @col := (
|
||||
SELECT COLUMN_NAME FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'prc_surcharge'
|
||||
AND COLUMN_NAME = 'dict_value'
|
||||
LIMIT 1
|
||||
);
|
||||
SET @sql := IF(@col IS NULL,
|
||||
'ALTER TABLE prc_surcharge ADD COLUMN dict_value varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT ''渠道产品 payment_product_type.dict_value'' AFTER charge_type',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @col := (
|
||||
SELECT COLUMN_NAME FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'prc_surcharge'
|
||||
AND COLUMN_NAME = 'block_title'
|
||||
LIMIT 1
|
||||
);
|
||||
SET @sql := IF(@col IS NULL,
|
||||
'ALTER TABLE prc_surcharge ADD COLUMN block_title varchar(200) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT ''分区/区块(pay_postcode_group.block_title)'' AFTER country_code',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- legacy amount modes: FIXED -> ALWAYS; RATE no longer used as amount mode -> MANUAL
|
||||
UPDATE prc_surcharge SET charge_type = 'ALWAYS' WHERE charge_type IS NULL OR charge_type = '' OR upper(charge_type) = 'FIXED';
|
||||
UPDATE prc_surcharge SET charge_type = 'MANUAL' WHERE upper(charge_type) = 'RATE';
|
||||
@@ -91,6 +91,10 @@ export function downloadArBillExport(exportId) {
|
||||
return request({ url: '/ar/bill/export/' + exportId + '/download', method: 'get', responseType: 'blob' })
|
||||
}
|
||||
|
||||
export function getArBillExportDownloadInfo(exportId) {
|
||||
return request({ url: '/ar/bill/export/' + exportId + '/download-url', method: 'get' })
|
||||
}
|
||||
|
||||
export function listArBillLogs(query) {
|
||||
return request({ url: '/ar/bill/log/list', method: 'get', params: query })
|
||||
}
|
||||
|
||||
@@ -50,18 +50,26 @@ export function billOrder(data) {
|
||||
|
||||
/** Receivable bill list */
|
||||
export function listBill(query) {
|
||||
const { pageNum, pageSize, ...filters } = query || {}
|
||||
return request({
|
||||
url: '/price/billing/bill/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
method: 'post',
|
||||
// 分页参数走 URL(PageHelper);筛选(大批单号)走 body,避免 HTTP 431
|
||||
params: { pageNum, pageSize },
|
||||
data: filters,
|
||||
timeout: 120000
|
||||
})
|
||||
}
|
||||
|
||||
export function listOrderBill(query) {
|
||||
const { pageNum, pageSize, ...filters } = query || {}
|
||||
return request({
|
||||
url: '/price/billing/orderBill/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
method: 'post',
|
||||
// 分页参数走 URL(PageHelper);筛选(大批单号)走 body,避免 HTTP 431
|
||||
params: { pageNum, pageSize },
|
||||
data: filters,
|
||||
timeout: 120000
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,15 @@ export function listSurchargeOptions(query) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 按国家拉取应付邮编区块(分区下拉) */
|
||||
export function listSurchargeBlockOptions(query) {
|
||||
return request({
|
||||
url: '/price/surcharge/blockOptions',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
/** 绑定附加费到产品/明细(scId + productCode + detailId) */
|
||||
export function addSurchargeRel(data) {
|
||||
return request({
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
|
||||
<script setup name="ArBill">
|
||||
import { saveAs } from 'file-saver'
|
||||
import { listArBillCustomerOptions, listArBill, getArBill, confirmArBill, voidArBill, exportArBill, getArBillExport, downloadArBillExport, getArBillGenerateTask, listActiveArBillGenerateTasks, cancelArBillGenerateTask, retryArBillGenerateTask } from '@/api/ar/bill'
|
||||
import { listArBillCustomerOptions, listArBill, getArBill, confirmArBill, voidArBill, exportArBill, getArBillExport, downloadArBillExport, getArBillExportDownloadInfo, getArBillGenerateTask, listActiveArBillGenerateTasks, cancelArBillGenerateTask, retryArBillGenerateTask } from '@/api/ar/bill'
|
||||
import BillDetail from './components/BillDetail.vue'
|
||||
import GenerateBillDialog from './components/GenerateBillDialog.vue'
|
||||
import { AR_BILL_STATUS, AR_BILL_STATUS_OPTIONS, AR_BILL_STATUS_TYPES } from '@/constants/ar/bill'
|
||||
@@ -382,7 +382,21 @@ function openExportTask(task) {
|
||||
}
|
||||
|
||||
// 仅下载 SUCCESS 状态的导出任务,文件名优先使用后端保存的名称。
|
||||
function downloadExport(row) { downloadArBillExport(row.exportId).then(blob => saveAs(blob, row.fileName || `账单_${row.exportId}.xlsx`)) }
|
||||
function downloadExport(row) {
|
||||
getArBillExportDownloadInfo(row.exportId).then(response => {
|
||||
const info = response.data || {}
|
||||
if (info.direct && info.downloadUrl) {
|
||||
const link = document.createElement('a')
|
||||
link.href = info.downloadUrl
|
||||
link.style.display = 'none'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
link.remove()
|
||||
return
|
||||
}
|
||||
return downloadArBillExport(row.exportId).then(blob => saveAs(blob, row.fileName || `账单_${row.exportId}.xlsx`))
|
||||
})
|
||||
}
|
||||
function clearCustomerFilter() {
|
||||
const query = { ...route.query }
|
||||
delete query.customerId
|
||||
|
||||
@@ -75,12 +75,22 @@
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane :label="`附加费(${(pane.item.surcharges || []).length})`" name="fee">
|
||||
<el-tab-pane :label="feeTabLabel(pane)" name="fee">
|
||||
<div class="pay-edit-bar">
|
||||
<el-input v-model="pane.feeKeyword" clearable placeholder="筛选附加费用、地区、价格、货币或费用解释" class="pay-filter" @input="pane.feePageNo = 1" />
|
||||
<el-button v-if="editable" size="small" type="primary" plain @click="addFee(pane)">添加附加费</el-button>
|
||||
<el-button v-if="editable" size="small" type="primary" plain @click="openFeePicker(pane)">添加附加费</el-button>
|
||||
<template v-if="billSelect">
|
||||
<el-button size="small" @click="setFeesBill(pane, true)">全选计费</el-button>
|
||||
<el-button size="small" @click="setFeesBill(pane, false)">取消全选</el-button>
|
||||
<span class="pay-bill-tip">勾选后才会写入 pay_surcharge;默认不勾选</span>
|
||||
</template>
|
||||
</div>
|
||||
<el-table :data="pageFees(pane)" border size="small" empty-text="没有附加费">
|
||||
<el-table-column v-if="billSelect" label="是否计费" width="88" align="center" fixed="left">
|
||||
<template #default="{ row }">
|
||||
<el-checkbox :model-value="row.needBill === true" @update:model-value="v => { row.needBill = !!v }" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="feeName" label="附加费用" min-width="180" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
@@ -227,6 +237,12 @@
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</section>
|
||||
|
||||
<pay-surcharge-picker-dialog
|
||||
v-model="feePickerVisible"
|
||||
:exclude-sc-ids="feePickerExcludeIds"
|
||||
@confirm="onFeePickerConfirm"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -235,6 +251,7 @@
|
||||
import { h, onMounted, watch } from 'vue'
|
||||
import { ElInput } from 'element-plus'
|
||||
import { listSurchargeOptions } from '@/api/price/surcharge'
|
||||
import PaySurchargePickerDialog from './PaySurchargePickerDialog.vue'
|
||||
|
||||
const props = defineProps({
|
||||
product: {
|
||||
@@ -249,6 +266,11 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/** 打单套餐:附加费表显示「是否计费」勾选,仅勾选行入库 */
|
||||
billSelect: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
savingRowKey: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
@@ -517,24 +539,93 @@ function filteredFees(pane) {
|
||||
return filterRows(pane.item.surcharges || [], pane.feeKeyword, ['feeName', 'zoneName', 'chargeText', 'amount', 'currency', 'priceUnit', 'feeDesc', 'calcRule', 'calcParam', 'postcodeParam'])
|
||||
}
|
||||
|
||||
function addFee(pane) {
|
||||
if (!pane.item.surcharges) pane.item.surcharges = []
|
||||
const currency = String(pane.item.currency || 'EUR').trim().toUpperCase() || 'EUR'
|
||||
const row = {
|
||||
feeName: '',
|
||||
scId: undefined,
|
||||
zoneName: '',
|
||||
chargeText: '',
|
||||
amount: '',
|
||||
priceUnit: currency + '/ITEM',
|
||||
currency,
|
||||
feeDesc: '',
|
||||
calcRule: 'MANUAL',
|
||||
calcParam: ''
|
||||
function feeTabLabel(pane) {
|
||||
const fees = pane.item.surcharges || []
|
||||
if (!props.billSelect) return '附加费(' + fees.length + ')'
|
||||
const n = fees.filter(f => f && f.needBill === true).length
|
||||
return '附加费(已选 ' + n + '/' + fees.length + ')'
|
||||
}
|
||||
|
||||
function setFeesBill(pane, value) {
|
||||
const fees = pane.item.surcharges || []
|
||||
for (const fee of fees) {
|
||||
if (fee) fee.needBill = !!value
|
||||
}
|
||||
}
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const feePickerVisible = ref(false)
|
||||
const feePickerPane = ref(null)
|
||||
|
||||
const feePickerExcludeIds = computed(() => {
|
||||
const fees = feePickerPane.value?.item?.surcharges || []
|
||||
return fees.map(fee => fee?.scId).filter(id => id != null)
|
||||
})
|
||||
|
||||
function openFeePicker(pane) {
|
||||
feePickerPane.value = pane
|
||||
feePickerVisible.value = true
|
||||
}
|
||||
|
||||
function toCalcRule(chargeType) {
|
||||
const t = String(chargeType || '').trim().toUpperCase()
|
||||
if (!t || t === 'RATE') return 'MANUAL'
|
||||
if (t === 'FIXED') return 'ALWAYS'
|
||||
const allowed = ['ALWAYS', 'POSTCODE', 'ZONE', 'EXTRA_KG', 'WEIGHT', 'SIZE', 'RETURN', 'MANUAL']
|
||||
return allowed.includes(t) ? t : 'MANUAL'
|
||||
}
|
||||
|
||||
function configToFeeRow(cfg, product) {
|
||||
const currency = String(cfg?.currency || product?.currency || 'EUR').trim().toUpperCase() || 'EUR'
|
||||
const amount = cfg?.chargeValue
|
||||
const isPercent = String(cfg?.chargeType || '').toUpperCase() === 'RATE'
|
||||
return {
|
||||
feeName: String(cfg?.scName || '').trim(),
|
||||
scId: cfg?.scId,
|
||||
zoneName: String(cfg?.blockTitle || cfg?.countryCode || '').trim(),
|
||||
chargeText: '',
|
||||
amount: amount == null ? '' : amount,
|
||||
currency,
|
||||
priceUnit: isPercent ? '%' : (currency + '/ITEM'),
|
||||
feeDesc: String(cfg?.scNote || '').trim(),
|
||||
calcRule: toCalcRule(cfg?.chargeType),
|
||||
calcParam: '',
|
||||
// 从配置添加默认要计费
|
||||
needBill: true
|
||||
}
|
||||
}
|
||||
|
||||
function onFeePickerConfirm(list) {
|
||||
const pane = feePickerPane.value
|
||||
if (!pane?.item) return
|
||||
if (!pane.item.surcharges) pane.item.surcharges = []
|
||||
const existed = new Set(
|
||||
(pane.item.surcharges || []).map(fee => fee?.scId).filter(id => id != null).map(id => String(id))
|
||||
)
|
||||
let added = 0
|
||||
let skipped = 0
|
||||
for (const cfg of list || []) {
|
||||
if (cfg?.scId != null && existed.has(String(cfg.scId))) {
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
const row = configToFeeRow(cfg, pane.item)
|
||||
if (!row.feeName) continue
|
||||
pane.item.surcharges.push(row)
|
||||
if (row.scId != null) existed.add(String(row.scId))
|
||||
added++
|
||||
}
|
||||
if (added > 0) {
|
||||
pane.feePageNo = Math.ceil(pane.item.surcharges.length / pageSize) || 1
|
||||
pane.tab = 'fee'
|
||||
}
|
||||
if (added && skipped) {
|
||||
proxy.$modal.msgSuccess(`已添加 ${added} 项,跳过重复 ${skipped} 项`)
|
||||
} else if (added) {
|
||||
proxy.$modal.msgSuccess(`已添加 ${added} 项附加费`)
|
||||
} else {
|
||||
proxy.$modal.msgWarning('没有可添加的附加费(可能已全部存在)')
|
||||
}
|
||||
pane.item.surcharges.push(row)
|
||||
startEdit(row)
|
||||
pane.feePageNo = Math.ceil(pane.item.surcharges.length / pageSize) || 1
|
||||
}
|
||||
|
||||
function removeRow(list, row) {
|
||||
@@ -643,9 +734,14 @@ function statusText(status) {
|
||||
.pay-edit-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.pay-bill-tip {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
.pay-hint {
|
||||
margin: 0 0 8px;
|
||||
color: var(--el-text-color-secondary);
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="选择附加费配置"
|
||||
width="920px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
@opened="onOpened"
|
||||
>
|
||||
<div class="picker-bar">
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
clearable
|
||||
placeholder="搜索附加费名称、计费方式、币种、国家或备注"
|
||||
class="picker-search"
|
||||
/>
|
||||
<span class="picker-meta">共 {{ filteredRows.length }} 项,已选 {{ picked.length }} 项</span>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
v-loading="loading"
|
||||
:data="filteredRows"
|
||||
row-key="scId"
|
||||
border
|
||||
size="small"
|
||||
max-height="420"
|
||||
empty-text="暂无附加费配置"
|
||||
@selection-change="onSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="46" :selectable="canSelect" />
|
||||
<el-table-column prop="scName" label="附加费名称" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="计费方式" width="120" align="center">
|
||||
<template #default="{ row }">{{ chargeTypeLabel(row.chargeType) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="计费数值" width="130" align="right">
|
||||
<template #default="{ row }">{{ formatValue(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="currency" label="币种" width="80" align="center" />
|
||||
<el-table-column label="国家/分区" min-width="140" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ scopeText(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="scNote" label="备注" min-width="160" show-overflow-tooltip />
|
||||
</el-table>
|
||||
|
||||
<div class="picker-hint">勾选后点确定写入当前产品附加费表;已存在相同配置的不会重复添加。</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取 消</el-button>
|
||||
<el-button type="primary" :disabled="!picked.length" @click="submit">确 定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { listSurchargeOptions } from '@/api/price/surcharge'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
/** 已在表中的 scId,用于禁用重复勾选 */
|
||||
excludeScIds: { type: Array, default: () => [] }
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const tableRef = ref()
|
||||
const loading = ref(false)
|
||||
const keyword = ref('')
|
||||
const rows = ref([])
|
||||
const pickedIds = ref(new Set())
|
||||
const syncing = ref(false)
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: v => emit('update:modelValue', v)
|
||||
})
|
||||
|
||||
const excludeSet = computed(() => new Set((props.excludeScIds || []).map(id => String(id))))
|
||||
|
||||
const chargeTypeLabelMap = {
|
||||
MANUAL: '暂不计费',
|
||||
ALWAYS: '每票都收',
|
||||
POSTCODE: '命中邮编',
|
||||
ZONE: '命中分区',
|
||||
EXTRA_KG: '超重续收',
|
||||
WEIGHT: '超重',
|
||||
SIZE: '超尺寸',
|
||||
RETURN: '退件',
|
||||
FIXED: '每票都收',
|
||||
RATE: '暂不计费'
|
||||
}
|
||||
|
||||
function chargeTypeLabel(val) {
|
||||
if (!val) return chargeTypeLabelMap.MANUAL
|
||||
const key = String(val).toUpperCase()
|
||||
return chargeTypeLabelMap[key] || val
|
||||
}
|
||||
|
||||
function formatValue(row) {
|
||||
if (row == null || row.chargeValue == null || row.chargeValue === '') return '-'
|
||||
const v = Number(row.chargeValue)
|
||||
if (!Number.isFinite(v)) return row.chargeValue
|
||||
if (String(row.chargeType || '').toUpperCase() === 'RATE') return (v * 100).toFixed(2) + '%'
|
||||
return String(v)
|
||||
}
|
||||
|
||||
function scopeText(row) {
|
||||
const parts = []
|
||||
if (row.countryCode) parts.push(row.countryCode)
|
||||
if (row.blockTitle) parts.push(row.blockTitle)
|
||||
return parts.join(' / ') || '全部'
|
||||
}
|
||||
|
||||
function canSelect(row) {
|
||||
if (row?.scId == null) return false
|
||||
return !excludeSet.value.has(String(row.scId))
|
||||
}
|
||||
|
||||
const filteredRows = computed(() => {
|
||||
const text = String(keyword.value || '').trim().toLowerCase()
|
||||
const list = rows.value || []
|
||||
if (!text) return list
|
||||
return list.filter(row => {
|
||||
const bag = [
|
||||
row.scName,
|
||||
chargeTypeLabel(row.chargeType),
|
||||
row.chargeType,
|
||||
row.currency,
|
||||
row.countryCode,
|
||||
row.blockTitle,
|
||||
row.scNote,
|
||||
row.chargeValue
|
||||
].map(v => String(v ?? '').toLowerCase())
|
||||
return bag.some(item => item.includes(text))
|
||||
})
|
||||
})
|
||||
|
||||
const picked = computed(() => {
|
||||
const ids = pickedIds.value
|
||||
return (rows.value || []).filter(row => row?.scId != null && ids.has(String(row.scId)) && canSelect(row))
|
||||
})
|
||||
|
||||
function onSelectionChange(selection) {
|
||||
if (syncing.value) return
|
||||
const visibleIds = new Set((filteredRows.value || []).map(row => String(row.scId)))
|
||||
const next = new Set(pickedIds.value)
|
||||
for (const id of visibleIds) {
|
||||
next.delete(id)
|
||||
}
|
||||
for (const row of selection || []) {
|
||||
if (row?.scId != null && canSelect(row)) next.add(String(row.scId))
|
||||
}
|
||||
pickedIds.value = next
|
||||
}
|
||||
|
||||
function restoreSelection() {
|
||||
const table = tableRef.value
|
||||
if (!table) return
|
||||
syncing.value = true
|
||||
table.clearSelection()
|
||||
for (const row of filteredRows.value || []) {
|
||||
if (row?.scId != null && pickedIds.value.has(String(row.scId))) {
|
||||
table.toggleRowSelection(row, true)
|
||||
}
|
||||
}
|
||||
nextTick(() => {
|
||||
syncing.value = false
|
||||
})
|
||||
}
|
||||
|
||||
async function loadRows() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await listSurchargeOptions({ ssCode: 'on' })
|
||||
rows.value = res.data || []
|
||||
} catch (e) {
|
||||
rows.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
nextTick(() => restoreSelection())
|
||||
}
|
||||
}
|
||||
|
||||
function onOpened() {
|
||||
keyword.value = ''
|
||||
pickedIds.value = new Set()
|
||||
nextTick(() => {
|
||||
tableRef.value?.clearSelection?.()
|
||||
})
|
||||
if (!rows.value.length) {
|
||||
loadRows()
|
||||
}
|
||||
}
|
||||
|
||||
function submit() {
|
||||
const list = picked.value
|
||||
if (!list.length) {
|
||||
proxy.$modal.msgWarning('请先勾选要添加的附加费')
|
||||
return
|
||||
}
|
||||
emit('confirm', list)
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
watch(keyword, () => {
|
||||
nextTick(() => restoreSelection())
|
||||
})
|
||||
|
||||
watch(() => props.modelValue, v => {
|
||||
if (v && !rows.value.length) loadRows()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.picker-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.picker-search {
|
||||
width: 360px;
|
||||
}
|
||||
.picker-meta {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
.picker-hint {
|
||||
margin-top: 10px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
@@ -55,7 +55,7 @@ import SurchargeRelDialog from './SurchargeRelDialog.vue'
|
||||
|
||||
const emit = defineEmits(['success'])
|
||||
const { proxy } = getCurrentInstance()
|
||||
const { bms_round_mode, bms_charge_type } = useDict('bms_round_mode', 'bms_charge_type')
|
||||
const { bms_round_mode } = useDict('bms_round_mode')
|
||||
|
||||
const relRef = ref()
|
||||
const visible = ref(false)
|
||||
@@ -107,9 +107,22 @@ function priceTypeLabel(val) {
|
||||
return '产品价'
|
||||
}
|
||||
|
||||
const chargeTypeLabelMap = {
|
||||
MANUAL: '暂不计费',
|
||||
ALWAYS: '每票都收',
|
||||
POSTCODE: '命中邮编',
|
||||
ZONE: '命中分区',
|
||||
EXTRA_KG: '超重续收',
|
||||
WEIGHT: '超重',
|
||||
SIZE: '超尺寸',
|
||||
RETURN: '退件',
|
||||
FIXED: '每票都收',
|
||||
RATE: '暂不计费'
|
||||
}
|
||||
|
||||
function chargeTypeLabel(val) {
|
||||
const found = bms_charge_type.value.find(c => c.value === val)
|
||||
return found ? found.label : (val || '-')
|
||||
if (!val) return chargeTypeLabelMap.MANUAL
|
||||
return chargeTypeLabelMap[val] || chargeTypeLabelMap[String(val).toUpperCase()] || val
|
||||
}
|
||||
|
||||
function roundModeLabel(val) {
|
||||
@@ -120,7 +133,6 @@ function roundModeLabel(val) {
|
||||
function formatChargeValue(row) {
|
||||
if (row.chargeValue == null) return '-'
|
||||
const v = Number(row.chargeValue)
|
||||
if (row.chargeType === 'RATE') return (v * 100).toFixed(2) + '%'
|
||||
return v + ' ' + (row.currency || 'CNY')
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ import RedlineSurchargeRelDialog from './RedlineSurchargeRelDialog.vue'
|
||||
|
||||
const emit = defineEmits(['success'])
|
||||
const { proxy } = getCurrentInstance()
|
||||
const { bms_round_mode, bms_charge_type } = useDict('bms_round_mode', 'bms_charge_type')
|
||||
const { bms_round_mode } = useDict('bms_round_mode')
|
||||
|
||||
const relRef = ref()
|
||||
const visible = ref(false)
|
||||
@@ -110,9 +110,22 @@ function priceTypeLabel(val) {
|
||||
return '产品价'
|
||||
}
|
||||
|
||||
const chargeTypeLabelMap = {
|
||||
MANUAL: '暂不计费',
|
||||
ALWAYS: '每票都收',
|
||||
POSTCODE: '命中邮编',
|
||||
ZONE: '命中分区',
|
||||
EXTRA_KG: '超重续收',
|
||||
WEIGHT: '超重',
|
||||
SIZE: '超尺寸',
|
||||
RETURN: '退件',
|
||||
FIXED: '每票都收',
|
||||
RATE: '暂不计费'
|
||||
}
|
||||
|
||||
function chargeTypeLabel(val) {
|
||||
const found = bms_charge_type.value.find(c => c.value === val)
|
||||
return found ? found.label : (val || '-')
|
||||
if (!val) return chargeTypeLabelMap.MANUAL
|
||||
return chargeTypeLabelMap[val] || chargeTypeLabelMap[String(val).toUpperCase()] || val
|
||||
}
|
||||
|
||||
function roundModeLabel(val) {
|
||||
@@ -123,7 +136,6 @@ function roundModeLabel(val) {
|
||||
function formatChargeValue(row) {
|
||||
if (row.chargeValue == null) return '-'
|
||||
const v = Number(row.chargeValue)
|
||||
if (row.chargeType === 'RATE') return (v * 100).toFixed(2) + '%'
|
||||
return v + ' ' + (row.currency || 'CNY')
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<el-dialog :title="title" v-model="visible" width="640px" append-to-body>
|
||||
<el-dialog :title="title" v-model="visible" width="720px" append-to-body>
|
||||
<basic-form ref="formRef" v-model="form" :schemas="schemas" label-width="100px" />
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||
@@ -10,101 +10,148 @@
|
||||
|
||||
<script setup>
|
||||
import BasicForm from '@/components/BasicForm'
|
||||
import { getSurcharge, addSurcharge, updateSurcharge } from '@/api/price/surcharge'
|
||||
import { getSurcharge, addSurcharge, updateSurcharge, listSurchargeBlockOptions } from '@/api/price/surcharge'
|
||||
import { listRegionOptions } from '@/api/data/region'
|
||||
|
||||
const emit = defineEmits(['success'])
|
||||
const { proxy } = getCurrentInstance()
|
||||
const { payment_product_type } = useDict('payment_product_type')
|
||||
|
||||
const formRef = ref()
|
||||
const visible = ref(false)
|
||||
const title = ref('')
|
||||
const form = ref(emptyForm())
|
||||
const regionOptions = ref([])
|
||||
const blockOptions = ref([])
|
||||
const loadingBlocks = ref(false)
|
||||
const suppressCountryWatch = ref(false)
|
||||
|
||||
const calcModeOptions = [
|
||||
{ value: 'MANUAL', label: '暂不计费' },
|
||||
{ value: 'ALWAYS', label: '每票都收' },
|
||||
{ value: 'POSTCODE', label: '命中邮编' },
|
||||
{ value: 'ZONE', label: '命中分区' },
|
||||
{ value: 'EXTRA_KG', label: '超重续收' },
|
||||
{ value: 'WEIGHT', label: '超重' },
|
||||
{ value: 'SIZE', label: '超尺寸' },
|
||||
{ value: 'RETURN', label: '退件' }
|
||||
]
|
||||
|
||||
const countrySelectOptions = computed(() => {
|
||||
const all = [{ label: "全部国家", value: '' }]
|
||||
const all = [{ label: '全部国家', value: '' }]
|
||||
return all.concat((regionOptions.value || []).map(r => ({
|
||||
label: regionLabel(r),
|
||||
value: r.countryCode2
|
||||
})))
|
||||
})
|
||||
|
||||
const channelProductOptions = computed(() =>
|
||||
(payment_product_type.value || []).map(item => ({
|
||||
label: item.label,
|
||||
value: item.value
|
||||
}))
|
||||
)
|
||||
|
||||
const blockSelectOptions = computed(() =>
|
||||
(blockOptions.value || []).map(title => ({ label: title, value: title }))
|
||||
)
|
||||
|
||||
const schemas = computed(() => [
|
||||
{
|
||||
label: "附加费名称",
|
||||
label: '附加费名称',
|
||||
prop: 'scName',
|
||||
component: 'input',
|
||||
placeholder: "如 燃油附加费",
|
||||
placeholder: '如 燃油附加费',
|
||||
span: 12,
|
||||
width: '100%',
|
||||
props: { maxlength: 100 },
|
||||
rules: [{ required: true, message: "附加费名称不能为空", trigger: 'blur' }]
|
||||
rules: [{ required: true, message: '附加费名称不能为空', trigger: 'blur' }]
|
||||
},
|
||||
{
|
||||
label: "费用类型",
|
||||
label: '费用类型',
|
||||
prop: 'feeType',
|
||||
component: 'select',
|
||||
placeholder: "请选择",
|
||||
placeholder: '请选择',
|
||||
dict: 'bms_fee_type',
|
||||
span: 12,
|
||||
width: '100%',
|
||||
disabled: true,
|
||||
rules: [{ required: true, message: "请选择费用类型", trigger: 'change' }]
|
||||
rules: [{ required: true, message: '请选择费用类型', trigger: 'change' }]
|
||||
},
|
||||
{
|
||||
label: "计费方式",
|
||||
label: '计费方式',
|
||||
prop: 'chargeType',
|
||||
component: 'select',
|
||||
placeholder: "请选择",
|
||||
dict: 'bms_charge_type',
|
||||
placeholder: '请选择',
|
||||
options: calcModeOptions,
|
||||
span: 12,
|
||||
width: '100%',
|
||||
rules: [{ required: true, message: "请选择计费方式", trigger: 'change' }]
|
||||
rules: [{ required: true, message: '请选择计费方式', trigger: 'change' }]
|
||||
},
|
||||
{
|
||||
label: "计费数值",
|
||||
label: '渠道产品',
|
||||
prop: 'dictValue',
|
||||
component: 'select',
|
||||
placeholder: '请选择渠道产品',
|
||||
options: channelProductOptions.value,
|
||||
span: 12,
|
||||
width: '100%',
|
||||
props: { filterable: true, clearable: true }
|
||||
},
|
||||
{
|
||||
label: '计费数值',
|
||||
prop: 'chargeValue',
|
||||
component: 'inputNumber',
|
||||
span: 12,
|
||||
width: '100%',
|
||||
props: { min: 0, precision: 6, step: 1, controlsPosition: 'right' },
|
||||
rules: [{ required: true, message: "计费数值不能为空", trigger: 'blur' }]
|
||||
rules: [{ required: true, message: '计费数值不能为空', trigger: 'blur' }]
|
||||
},
|
||||
{
|
||||
label: "币种",
|
||||
label: '币种',
|
||||
prop: 'currency',
|
||||
component: 'select',
|
||||
placeholder: "请选择",
|
||||
placeholder: '请选择',
|
||||
dict: 'bms_currency',
|
||||
span: 12,
|
||||
width: '100%'
|
||||
},
|
||||
{
|
||||
label: "取值方式",
|
||||
label: '取值方式',
|
||||
prop: 'roundMode',
|
||||
component: 'select',
|
||||
placeholder: "请选择",
|
||||
placeholder: '请选择',
|
||||
dict: 'bms_round_mode',
|
||||
span: 12,
|
||||
width: '100%',
|
||||
rules: [{ required: true, message: "请选择取值方式", trigger: 'change' }]
|
||||
rules: [{ required: true, message: '请选择取值方式', trigger: 'change' }]
|
||||
},
|
||||
{
|
||||
label: "适用国家",
|
||||
label: '适用国家',
|
||||
prop: 'countryCode',
|
||||
component: 'select',
|
||||
placeholder: "全部国家",
|
||||
placeholder: '全部国家',
|
||||
options: countrySelectOptions.value,
|
||||
span: 12,
|
||||
width: '100%',
|
||||
props: { filterable: true, clearable: true }
|
||||
},
|
||||
{
|
||||
label: "备注",
|
||||
label: '分区',
|
||||
prop: 'blockTitle',
|
||||
component: 'select',
|
||||
placeholder: form.value.countryCode ? (loadingBlocks.value ? '加载中...' : '请选择分区/区块') : '请先选择国家',
|
||||
options: blockSelectOptions.value,
|
||||
span: 12,
|
||||
width: '100%',
|
||||
disabled: !form.value.countryCode || loadingBlocks.value,
|
||||
props: { filterable: true, clearable: true }
|
||||
},
|
||||
{
|
||||
label: '备注',
|
||||
prop: 'scNote',
|
||||
component: 'textarea',
|
||||
placeholder: "请输入备注",
|
||||
placeholder: '请输入备注',
|
||||
span: 24,
|
||||
width: '100%',
|
||||
props: { rows: 2, maxlength: 500 }
|
||||
@@ -116,11 +163,13 @@ function emptyForm() {
|
||||
scId: undefined,
|
||||
scName: undefined,
|
||||
feeType: 'SURCHARGE',
|
||||
chargeType: 'FIXED',
|
||||
dictValue: undefined,
|
||||
chargeType: 'MANUAL',
|
||||
chargeValue: 0,
|
||||
currency: 'CNY',
|
||||
roundMode: 'HALF_UP',
|
||||
countryCode: undefined,
|
||||
blockTitle: undefined,
|
||||
scNote: undefined
|
||||
}
|
||||
}
|
||||
@@ -137,25 +186,64 @@ function ensureRegions() {
|
||||
})
|
||||
}
|
||||
|
||||
function resolveCountryName(code) {
|
||||
if (!code) return ''
|
||||
const region = (regionOptions.value || []).find(item => item.countryCode2 === code)
|
||||
return region?.countryNameCn || region?.countryNameEn || ''
|
||||
}
|
||||
|
||||
function loadBlockOptions(code, keepCurrent) {
|
||||
if (!code) {
|
||||
blockOptions.value = []
|
||||
if (!keepCurrent) form.value.blockTitle = undefined
|
||||
return Promise.resolve()
|
||||
}
|
||||
loadingBlocks.value = true
|
||||
const countryName = resolveCountryName(code)
|
||||
return listSurchargeBlockOptions({ countryCode: code, countryName: countryName || undefined })
|
||||
.then(res => {
|
||||
const list = (res.data || []).filter(Boolean)
|
||||
blockOptions.value = list
|
||||
if (!keepCurrent) {
|
||||
form.value.blockTitle = undefined
|
||||
} else if (form.value.blockTitle && !list.includes(form.value.blockTitle)) {
|
||||
// keep current title even if not in options (legacy)
|
||||
blockOptions.value = [form.value.blockTitle].concat(list)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
blockOptions.value = []
|
||||
})
|
||||
.finally(() => {
|
||||
loadingBlocks.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function reset() {
|
||||
form.value = emptyForm()
|
||||
blockOptions.value = []
|
||||
nextTick(() => formRef.value?.clearValidate())
|
||||
}
|
||||
|
||||
function open(row) {
|
||||
reset()
|
||||
ensureRegions()
|
||||
if (!row) {
|
||||
title.value = "新建附加费"
|
||||
visible.value = true
|
||||
return
|
||||
}
|
||||
getSurcharge(row.scId).then(res => {
|
||||
const data = res.data || {}
|
||||
form.value = { ...emptyForm(), ...data }
|
||||
title.value = "修改附加费"
|
||||
visible.value = true
|
||||
nextTick(() => formRef.value?.clearValidate())
|
||||
ensureRegions().then(() => {
|
||||
if (!row) {
|
||||
title.value = '新建附加费'
|
||||
visible.value = true
|
||||
return
|
||||
}
|
||||
getSurcharge(row.scId).then(res => {
|
||||
const data = res.data || {}
|
||||
suppressCountryWatch.value = true
|
||||
form.value = { ...emptyForm(), ...data }
|
||||
title.value = '修改附加费'
|
||||
visible.value = true
|
||||
loadBlockOptions(form.value.countryCode, true).finally(() => {
|
||||
suppressCountryWatch.value = false
|
||||
nextTick(() => formRef.value?.clearValidate())
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -164,17 +252,26 @@ function submitForm() {
|
||||
if (!valid) return
|
||||
const payload = { ...form.value }
|
||||
if (!payload.countryCode) payload.countryCode = undefined
|
||||
if (!payload.dictValue) payload.dictValue = undefined
|
||||
if (!payload.blockTitle) payload.blockTitle = undefined
|
||||
if (!payload.chargeType) payload.chargeType = 'MANUAL'
|
||||
if (payload.chargeValue != null) payload.chargeValue = Number(payload.chargeValue)
|
||||
const req = payload.scId != null ? updateSurcharge(payload) : addSurcharge(payload)
|
||||
req.then(() => {
|
||||
proxy.$modal.msgSuccess(payload.scId != null ? "修改成功" : "新增成功")
|
||||
proxy.$modal.msgSuccess(payload.scId != null ? '修改成功' : '新增成功')
|
||||
visible.value = false
|
||||
emit('success')
|
||||
}).catch(() => {
|
||||
// 后端返回 warn(601「未做任何修改」) 时由全局拦截器弹提示,此处吞掉 reject
|
||||
// backend warn(601) handled globally
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
watch(() => form.value.countryCode, (code, prev) => {
|
||||
if (suppressCountryWatch.value) return
|
||||
if (code === prev) return
|
||||
loadBlockOptions(code, false)
|
||||
})
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
@@ -66,7 +66,7 @@ import { listRegionOptions } from '@/api/data/region'
|
||||
|
||||
// 只读抽屉:查看已被版本化编辑淘汰的作废记录(ss_code = vd)
|
||||
const { proxy } = getCurrentInstance()
|
||||
const { bms_charge_type, bms_round_mode, bms_fee_type } = useDict('bms_charge_type', 'bms_round_mode', 'bms_fee_type')
|
||||
const { bms_round_mode, bms_fee_type } = useDict('bms_round_mode', 'bms_fee_type')
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
@@ -105,9 +105,22 @@ function feeTypeLabel(val) {
|
||||
return found ? found.label : (val || '-')
|
||||
}
|
||||
|
||||
const chargeTypeLabelMap = {
|
||||
MANUAL: '暂不计费',
|
||||
ALWAYS: '每票都收',
|
||||
POSTCODE: '命中邮编',
|
||||
ZONE: '命中分区',
|
||||
EXTRA_KG: '超重续收',
|
||||
WEIGHT: '超重',
|
||||
SIZE: '超尺寸',
|
||||
RETURN: '退件',
|
||||
FIXED: '每票都收',
|
||||
RATE: '暂不计费'
|
||||
}
|
||||
|
||||
function chargeTypeLabel(val) {
|
||||
const found = bms_charge_type.value.find(d => d.value === val)
|
||||
return found ? found.label : (val || '-')
|
||||
if (!val) return chargeTypeLabelMap.MANUAL
|
||||
return chargeTypeLabelMap[val] || chargeTypeLabelMap[String(val).toUpperCase()] || val
|
||||
}
|
||||
|
||||
function roundModeLabel(val) {
|
||||
@@ -118,7 +131,6 @@ function roundModeLabel(val) {
|
||||
function formatChargeValue(row) {
|
||||
if (row.chargeValue == null) return '-'
|
||||
const v = Number(row.chargeValue)
|
||||
if (row.chargeType === 'RATE') return (v * 100).toFixed(2) + '%'
|
||||
return v + ' ' + (row.currency || 'CNY')
|
||||
}
|
||||
|
||||
|
||||
@@ -77,20 +77,32 @@ import { listRegionOptions } from '@/api/data/region'
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const { bms_charge_type } = useDict('bms_charge_type')
|
||||
|
||||
const tableRef = ref()
|
||||
const formDialogRef = ref()
|
||||
const voidDrawerRef = ref()
|
||||
const regionOptions = ref([])
|
||||
|
||||
const calcModeLabelMap = {
|
||||
MANUAL: '暂不计费',
|
||||
ALWAYS: '每票都收',
|
||||
POSTCODE: '命中邮编',
|
||||
ZONE: '命中分区',
|
||||
EXTRA_KG: '超重续收',
|
||||
WEIGHT: '超重',
|
||||
SIZE: '超尺寸',
|
||||
RETURN: '退件'
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ label: '附加费名称', prop: 'scName', align: 'center', minWidth: 140, showOverflowTooltip: true },
|
||||
{ label: '费用类型', prop: 'feeType', align: 'center', width: 110, dict: 'bms_fee_type' },
|
||||
{ label: '计费方式', prop: 'chargeType', align: 'center', width: 130 },
|
||||
{ label: '计费方式', prop: 'chargeType', align: 'center', width: 110 },
|
||||
{ label: '渠道产品', prop: 'dictValue', align: 'center', minWidth: 130, showOverflowTooltip: true },
|
||||
{ label: '计费数值', prop: 'chargeValue', align: 'center', width: 140 },
|
||||
{ label: '取值方式', prop: 'roundMode', align: 'center', width: 110, dict: 'bms_round_mode' },
|
||||
{ label: '适用国家', prop: 'countryCode', align: 'center', minWidth: 120, showOverflowTooltip: true },
|
||||
{ label: '分区', prop: 'blockTitle', align: 'center', minWidth: 140, showOverflowTooltip: true },
|
||||
{ label: '状态', prop: 'ssCode', align: 'center', width: 80 },
|
||||
{ label: '备注', prop: 'scNote', align: 'center', minWidth: 140, showOverflowTooltip: true },
|
||||
{ label: '创建时间', prop: 'createTime', align: 'center', width: 160, parseTime: true },
|
||||
@@ -103,10 +115,18 @@ const searchColumns = [
|
||||
label: '计费方式',
|
||||
prop: 'chargeType',
|
||||
component: 'select',
|
||||
dict: 'bms_charge_type',
|
||||
options: Object.keys(calcModeLabelMap).map(value => ({ value, label: calcModeLabelMap[value] })),
|
||||
placeholder: '计费方式',
|
||||
width: '150px'
|
||||
},
|
||||
{
|
||||
label: '渠道产品',
|
||||
prop: 'dictValue',
|
||||
component: 'select',
|
||||
dict: 'payment_product_type',
|
||||
placeholder: '渠道产品',
|
||||
width: '160px'
|
||||
},
|
||||
{ label: '取值方式', prop: 'roundMode', component: 'select', dict: 'bms_round_mode', placeholder: '取值方式', width: '150px' },
|
||||
{
|
||||
label: '状态',
|
||||
@@ -127,8 +147,8 @@ function queryList(params) {
|
||||
}
|
||||
|
||||
function chargeTypeLabel(val) {
|
||||
const found = bms_charge_type.value.find(c => c.value === val)
|
||||
return found ? found.label : (val || '-')
|
||||
if (!val) return calcModeLabelMap.MANUAL
|
||||
return calcModeLabelMap[val] || val
|
||||
}
|
||||
|
||||
function countryLabel(code) {
|
||||
@@ -150,9 +170,6 @@ onMounted(() => {
|
||||
function formatChargeValue(row) {
|
||||
if (row.chargeValue == null) return '-'
|
||||
const v = Number(row.chargeValue)
|
||||
if (row.chargeType === 'RATE') {
|
||||
return (v * 100).toFixed(2) + '%'
|
||||
}
|
||||
return v + ' ' + (row.currency || 'CNY')
|
||||
}
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
</div>
|
||||
|
||||
<el-drawer v-model="detailVisible" :title="detailTitle" size="78%" destroy-on-close append-to-body>
|
||||
<pay-config-detail :product="detailProduct" :editable="false" />
|
||||
<pay-config-detail :product="detailProduct" :editable="false" :bill-select="true" />
|
||||
<template #footer>
|
||||
<el-button @click="detailVisible = false">关闭</el-button>
|
||||
</template>
|
||||
@@ -187,7 +187,9 @@ async function onFile(uploadFile) {
|
||||
const data = res.data || {}
|
||||
data.products = (data.products || []).map((item, index) => ({
|
||||
...item,
|
||||
rowKey: `${item.sheetName || ''}-${item.productName || ''}-${index}`
|
||||
rowKey: `${item.sheetName || ''}-${item.productName || ''}-${index}`,
|
||||
// 解析后默认不计费,勾选后才入 pay_surcharge
|
||||
surcharges: (item.surcharges || []).map(fee => ({ ...fee, needBill: fee?.needBill === true }))
|
||||
}))
|
||||
preview.value = data
|
||||
const picked = data.products.filter(row => canSelect(row))
|
||||
@@ -310,12 +312,17 @@ async function submit(payload = {}) {
|
||||
const productsPayload = (payload.products && payload.products.length)
|
||||
? payload.products
|
||||
: selected.value.map(row => ({ ...row, productType: 'AR' }))
|
||||
const filteredProducts = productsPayload.map(product => ({
|
||||
...product,
|
||||
productType: product.productType || 'AR',
|
||||
surcharges: (product.surcharges || []).filter(fee => fee && fee.needBill === true)
|
||||
}))
|
||||
const res = await saveLabelImport({
|
||||
priceSetId: payload.priceSetId || null,
|
||||
priceSetName: String(payload.priceSetName || '').trim(),
|
||||
priceSetNote: payload.priceSetNote || '',
|
||||
overwrite: payload.overwrite != null ? payload.overwrite : overwrite.value,
|
||||
products: productsPayload
|
||||
products: filteredProducts
|
||||
})
|
||||
const data = res.data || {}
|
||||
return {
|
||||
|
||||
@@ -128,10 +128,9 @@ function statusTag(status) {
|
||||
return map[String(status)] || 'info'
|
||||
}
|
||||
|
||||
/** 待生效、已生效:可编辑价卡 */
|
||||
/** 不限状态:待生效/已生效/已失效均可编辑价卡 */
|
||||
function canEdit(row) {
|
||||
const s = row ? String(row.status) : ''
|
||||
return s === '0' || s === '1'
|
||||
return !!row
|
||||
}
|
||||
|
||||
/** 仅待生效可改生效时间 */
|
||||
|
||||
@@ -21,9 +21,18 @@
|
||||
<span>{{ cardTitle }}</span>
|
||||
<span class="header-tip">{{ headerTip }}</span>
|
||||
</div>
|
||||
<el-button type="primary" plain icon="Upload" @click="openImport" v-hasPermi="['price:label:import', 'price:label:add']">
|
||||
导入价卡
|
||||
</el-button>
|
||||
<div class="header-actions">
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
icon="Delete"
|
||||
:disabled="!selectedProducts.length"
|
||||
@click="removeSelectedProducts"
|
||||
>批量删除</el-button>
|
||||
<el-button type="primary" plain icon="Upload" @click="openImport" v-hasPermi="['price:label:import', 'price:label:add']">
|
||||
导入价卡
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -36,7 +45,18 @@
|
||||
class="meta-alert"
|
||||
/>
|
||||
|
||||
<el-table v-loading="loadingProducts" :data="productRows" border size="small" max-height="420" empty-text="请点击右上角「导入价卡」">
|
||||
<el-table
|
||||
ref="productTableRef"
|
||||
v-loading="loadingProducts"
|
||||
:data="productRows"
|
||||
border
|
||||
size="small"
|
||||
max-height="420"
|
||||
row-key="rowKey"
|
||||
empty-text="请点击右上角「导入价卡」"
|
||||
@selection-change="onProductSelect"
|
||||
>
|
||||
<el-table-column type="selection" width="48" align="center" />
|
||||
<el-table-column prop="productName" label="产品" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="sheetName" label="报价工作表" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="countryScope" label="国家/口岸" min-width="110" show-overflow-tooltip />
|
||||
@@ -54,9 +74,10 @@
|
||||
<span v-else>新记录</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="90" align="center" fixed="right">
|
||||
<el-table-column label="操作" width="140" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" icon="View" @click="openDetail(row)">查看</el-button>
|
||||
<el-button link type="primary" icon="Edit" @click="openDetail(row)">编辑</el-button>
|
||||
<el-button link type="danger" icon="Delete" @click="removeProduct(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -79,6 +100,7 @@
|
||||
<pay-config-detail
|
||||
:product="detailProduct"
|
||||
:editable="true"
|
||||
:bill-select="true"
|
||||
:saving-row-key="savingRowKey"
|
||||
@save-row="saveDetailRow"
|
||||
/>
|
||||
@@ -104,6 +126,8 @@ const form = reactive({
|
||||
basic: createEmptyBasicInfo()
|
||||
})
|
||||
const productRows = ref([])
|
||||
const productTableRef = ref()
|
||||
const selectedProducts = ref([])
|
||||
const importMeta = reactive({
|
||||
sourceFile: '',
|
||||
supplierName: '',
|
||||
@@ -141,7 +165,7 @@ const pageBanner = computed(() => {
|
||||
const headerTip = computed(() => {
|
||||
if (editVersionMode.value) return '可重新导入或修改明细;完成后覆盖本版本,不新增版本号'
|
||||
if (newVersionMode.value) return '导入后生成待生效新版本;生效时间请回详情页设置'
|
||||
return '打单价卡与应付独立;解析后回填到此表,查看明细后可在行内保存修改'
|
||||
return '打单价卡与应付独立;解析后回填到此表,可删除不需要的产品,编辑明细后点完成保存'
|
||||
})
|
||||
|
||||
function goBack() {
|
||||
@@ -196,7 +220,9 @@ async function loadEditProducts() {
|
||||
}
|
||||
}
|
||||
}))
|
||||
productRows.value = details
|
||||
// 库中已落库的附加费默认勾选(保留原计费项)
|
||||
productRows.value = markSurchargeBillFlags(details, true)
|
||||
selectedProducts.value = []
|
||||
importMeta.sourceFile = details[0]?.sourceFile || ''
|
||||
importMeta.supplierName = details[0]?.supplierName || ''
|
||||
importMeta.overwrite = true
|
||||
@@ -209,8 +235,71 @@ async function openImport() {
|
||||
importRef.value?.open()
|
||||
}
|
||||
|
||||
function markSurchargeBillFlags(products, defaultNeedBill) {
|
||||
return (products || []).map(product => ({
|
||||
...product,
|
||||
surcharges: (product.surcharges || []).map(fee => ({
|
||||
...fee,
|
||||
needBill: fee?.needBill === true ? true : (fee?.needBill === false ? false : !!defaultNeedBill)
|
||||
}))
|
||||
}))
|
||||
}
|
||||
|
||||
function withBillableSurcharges(products) {
|
||||
return (products || []).map(product => ({
|
||||
...product,
|
||||
surcharges: (product.surcharges || []).filter(fee => fee && fee.needBill === true)
|
||||
}))
|
||||
}
|
||||
|
||||
function onProductSelect(rows) {
|
||||
selectedProducts.value = rows || []
|
||||
}
|
||||
|
||||
function productKey(row) {
|
||||
return row?.rowKey || `${row?.sheetName || ''}-${row?.productName || ''}`
|
||||
}
|
||||
|
||||
async function removeProduct(row) {
|
||||
if (!row) return
|
||||
try {
|
||||
await proxy.$modal.confirm(`确认从本次导入中移除产品「${row.productName || row.sheetName || ''}」?`)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
const key = productKey(row)
|
||||
productRows.value = productRows.value.filter(item => productKey(item) !== key)
|
||||
selectedProducts.value = selectedProducts.value.filter(item => productKey(item) !== key)
|
||||
if (detailRowKey.value === key) {
|
||||
detailVisible.value = false
|
||||
}
|
||||
proxy.$modal.msgSuccess('已删除')
|
||||
}
|
||||
|
||||
async function removeSelectedProducts() {
|
||||
const rows = selectedProducts.value || []
|
||||
if (!rows.length) {
|
||||
proxy.$modal.msgWarning('请先勾选要删除的产品')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await proxy.$modal.confirm(`确认删除所选 ${rows.length} 个产品?`)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
const keys = new Set(rows.map(productKey))
|
||||
productRows.value = productRows.value.filter(item => !keys.has(productKey(item)))
|
||||
selectedProducts.value = []
|
||||
if (keys.has(detailRowKey.value)) {
|
||||
detailVisible.value = false
|
||||
}
|
||||
proxy.$modal.msgSuccess('已批量删除')
|
||||
}
|
||||
|
||||
function onImportConfirm(payload) {
|
||||
productRows.value = payload?.products || []
|
||||
// 价卡解析行默认不计费,需手动勾选
|
||||
productRows.value = markSurchargeBillFlags(payload?.products || [], false)
|
||||
selectedProducts.value = []
|
||||
importMeta.sourceFile = payload?.sourceFile || ''
|
||||
importMeta.supplierName = payload?.supplierName || ''
|
||||
importMeta.effectiveDate = payload?.effectiveDate || ''
|
||||
@@ -226,6 +315,10 @@ function openDetail(row) {
|
||||
}
|
||||
|
||||
function onDetailClosed() {
|
||||
// 关闭抽屉时回写勾选结果(不依赖行内「保存」)
|
||||
if (detailProduct.value && detailRowKey.value) {
|
||||
applyEditedProduct()
|
||||
}
|
||||
detailProduct.value = null
|
||||
detailRowKey.value = ''
|
||||
savingRowKey.value = ''
|
||||
@@ -303,6 +396,10 @@ async function finish() {
|
||||
proxy.$modal.msgWarning('请先导入并确认价卡产品')
|
||||
return
|
||||
}
|
||||
// 抽屉未关闭时先回写勾选,避免丢失
|
||||
if (detailVisible.value && detailProduct.value && detailRowKey.value) {
|
||||
applyEditedProduct()
|
||||
}
|
||||
const tip = editVersionMode.value
|
||||
? `将覆盖版本 V${versionNo.value} 的 ${productRows.value.length} 个产品,确认继续?`
|
||||
: newVersionMode.value
|
||||
@@ -317,7 +414,7 @@ async function finish() {
|
||||
try {
|
||||
const payload = {
|
||||
overwrite: editVersionMode.value ? true : importMeta.overwrite,
|
||||
products: productRows.value
|
||||
products: withBillableSurcharges(productRows.value)
|
||||
}
|
||||
if (editVersionMode.value) {
|
||||
payload.priceSetId = priceSetId.value
|
||||
@@ -367,6 +464,11 @@ watch([priceSetId, versionNo, () => route.query.editVersion, () => route.query.n
|
||||
color: #909399;
|
||||
font-weight: normal;
|
||||
}
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.meta-alert {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ function openCreateVersion() {
|
||||
})
|
||||
}
|
||||
|
||||
/** 编辑待生效/已生效版本价卡 */
|
||||
/** 编辑版本价卡(不限状态) */
|
||||
function openEditVersion(row) {
|
||||
if (!priceSetId.value || !row?.versionNo) return
|
||||
proxy.$router.push({
|
||||
|
||||
@@ -68,11 +68,22 @@ defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
const { bms_charge_type } = useDict('bms_charge_type')
|
||||
const chargeTypeLabelMap = {
|
||||
MANUAL: '暂不计费',
|
||||
ALWAYS: '每票都收',
|
||||
POSTCODE: '命中邮编',
|
||||
ZONE: '命中分区',
|
||||
EXTRA_KG: '超重续收',
|
||||
WEIGHT: '超重',
|
||||
SIZE: '超尺寸',
|
||||
RETURN: '退件',
|
||||
FIXED: '每票都收',
|
||||
RATE: '暂不计费'
|
||||
}
|
||||
|
||||
function chargeTypeLabel(val) {
|
||||
const found = (bms_charge_type.value || []).find(c => c.value === val)
|
||||
return found ? found.label : (val || '-')
|
||||
if (!val) return chargeTypeLabelMap.MANUAL
|
||||
return chargeTypeLabelMap[val] || chargeTypeLabelMap[String(val).toUpperCase()] || val
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -62,8 +62,6 @@ const props = defineProps({
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||
|
||||
const { bms_charge_type } = useDict('bms_charge_type')
|
||||
|
||||
const tableRef = ref()
|
||||
const picked = ref([])
|
||||
|
||||
@@ -91,9 +89,22 @@ const rows = computed(() => {
|
||||
}))
|
||||
})
|
||||
|
||||
const chargeTypeLabelMap = {
|
||||
MANUAL: '暂不计费',
|
||||
ALWAYS: '每票都收',
|
||||
POSTCODE: '命中邮编',
|
||||
ZONE: '命中分区',
|
||||
EXTRA_KG: '超重续收',
|
||||
WEIGHT: '超重',
|
||||
SIZE: '超尺寸',
|
||||
RETURN: '退件',
|
||||
FIXED: '每票都收',
|
||||
RATE: '暂不计费'
|
||||
}
|
||||
|
||||
function chargeTypeLabel(val) {
|
||||
const found = (bms_charge_type.value || []).find(c => c.value === val)
|
||||
return found ? found.label : (val || '-')
|
||||
if (!val) return chargeTypeLabelMap.MANUAL
|
||||
return chargeTypeLabelMap[val] || chargeTypeLabelMap[String(val).toUpperCase()] || val
|
||||
}
|
||||
|
||||
function onSelectionChange(selection) {
|
||||
|
||||
@@ -30,11 +30,11 @@
|
||||
<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">
|
||||
<!-- <el-table-column :label="colStatus" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="isDeletedVersion(row) ? 'danger' : versionStatusTag(row.status)" size="small">{{ statusText(row) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table-column> -->
|
||||
<el-table-column :label="colCreate" min-width="170" align="center">
|
||||
<template #default="{ row }">{{ row.createTime || '-' }}</template>
|
||||
</el-table-column>
|
||||
|
||||
Reference in New Issue
Block a user