提交第三方接口订单同步功能

This commit is contained in:
wuweihua
2026-09-10 15:53:23 +08:00
parent 4f5ba43d28
commit e69a6845f5
22 changed files with 1258 additions and 4 deletions
@@ -2,10 +2,13 @@ package com.ruoyi.web.controller.api;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping; 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.bind.annotation.RestController;
import com.ruoyi.common.annotation.Anonymous; import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.controller.BaseController;
@@ -13,8 +16,10 @@ import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.config.properties.TmsApiProperties; import com.ruoyi.framework.config.properties.TmsApiProperties;
import com.ruoyi.price.domain.vo.TmsPriceBillRequest;
import com.ruoyi.price.domain.vo.TmsPriceQuoteRequest; import com.ruoyi.price.domain.vo.TmsPriceQuoteRequest;
import com.ruoyi.price.domain.vo.TmsPriceQuoteResult; import com.ruoyi.price.domain.vo.TmsPriceQuoteResult;
import com.ruoyi.price.service.IPrcBillService;
import com.ruoyi.price.service.ITmsPriceQuoteService; import com.ruoyi.price.service.ITmsPriceQuoteService;
/** /**
@@ -35,6 +40,9 @@ public class TmsPriceController extends BaseController
@Autowired @Autowired
private ITmsPriceQuoteService tmsPriceQuoteService; private ITmsPriceQuoteService tmsPriceQuoteService;
@Autowired
private IPrcBillService prcBillService;
@Autowired @Autowired
private TmsApiProperties tmsApiProperties; private TmsApiProperties tmsApiProperties;
@@ -52,6 +60,33 @@ public class TmsPriceController extends BaseController
return success(result); return success(result);
} }
/**
* Sync order and persist receivable bill (prc_bill / prc_bill_fee).
* Same price match as quote. Same orderNo upserts.
*/
@PostMapping("/bill")
public AjaxResult bill(
@RequestHeader(value = HEADER_API_CODE, required = false) String headerApiCode,
@Validated @RequestBody TmsPriceBillRequest request)
{
assertApiCode(headerApiCode, request == null ? null : request.getApiCode());
TmsPriceQuoteResult result = prcBillService.bill(request);
return success(result);
}
/**
* Query receivable bill by orderNo after sync.
*/
@GetMapping("/bill/by-order/{orderNo}")
public AjaxResult getBillByOrder(
@RequestHeader(value = HEADER_API_CODE, required = false) String headerApiCode,
@PathVariable("orderNo") String orderNo,
@RequestParam(value = "apiCode", required = false) String apiCode)
{
assertApiCode(headerApiCode, apiCode);
return success(prcBillService.getBillByOrderNo(orderNo));
}
private void assertApiCode(String headerApiCode, String bodyApiCode) private void assertApiCode(String headerApiCode, String bodyApiCode)
{ {
String expected = tmsApiProperties.getApiCode(); String expected = tmsApiProperties.getApiCode();
@@ -13,9 +13,12 @@ import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo; import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.price.domain.PrcBill;
import com.ruoyi.price.domain.PrcQuote; import com.ruoyi.price.domain.PrcQuote;
import com.ruoyi.price.domain.vo.TmsPriceBillRequest;
import com.ruoyi.price.domain.vo.TmsPriceQuoteRequest; import com.ruoyi.price.domain.vo.TmsPriceQuoteRequest;
import com.ruoyi.price.domain.vo.TmsPriceQuoteResult; import com.ruoyi.price.domain.vo.TmsPriceQuoteResult;
import com.ruoyi.price.service.IPrcBillService;
import com.ruoyi.price.service.ITmsPriceQuoteService; import com.ruoyi.price.service.ITmsPriceQuoteService;
/** /**
@@ -30,6 +33,9 @@ public class BillingController extends BaseController
@Autowired @Autowired
private ITmsPriceQuoteService tmsPriceQuoteService; private ITmsPriceQuoteService tmsPriceQuoteService;
@Autowired
private IPrcBillService prcBillService;
/** /**
* Historical quote list. * Historical quote list.
*/ */
@@ -62,4 +68,28 @@ public class BillingController extends BaseController
TmsPriceQuoteResult result = tmsPriceQuoteService.quote(request); TmsPriceQuoteResult result = tmsPriceQuoteService.quote(request);
return success(result); return success(result);
} }
@PreAuthorize("@ss.hasPermi('price:billing:bill')")
@GetMapping("/bill/list")
public TableDataInfo billList(PrcBill query)
{
startPage();
List<PrcBill> list = prcBillService.selectBillList(query);
return getDataTable(list);
}
@PreAuthorize("@ss.hasPermi('price:billing:bill')")
@GetMapping("/bill/{billId}")
public AjaxResult getBill(@PathVariable String billId)
{
return success(prcBillService.getBill(billId));
}
@PreAuthorize("@ss.hasPermi('price:billing:bill')")
@PostMapping("/bill")
public AjaxResult bill(@Validated @RequestBody TmsPriceBillRequest request)
{
TmsPriceQuoteResult result = prcBillService.bill(request);
return success(result);
}
} }
@@ -150,4 +150,4 @@ xss:
bms: bms:
tms: tms:
# Agreed API identification code for TMS callers # Agreed API identification code for TMS callers
api-code: BMS.TMS._Q2WnPXiXwGW2Dl-iSGNzLfrhUgQogNo api-code: BMS.TMS.ZYaOvlQzpcfau8TUaCFfO3Hp8ieHIqGw
@@ -77,6 +77,7 @@ price.set.customer.customerId.empty=客户ID不能为空
price.set.customer.beginDate.empty=客户绑定生效日期不能为空 price.set.customer.beginDate.empty=客户绑定生效日期不能为空
price.set.customer.date.invalid=客户绑定生效日期不能晚于失效日期 price.set.customer.date.invalid=客户绑定生效日期不能晚于失效日期
price.set.customer.alreadyBound=该客户已绑定套餐,一个客户只能绑定一个套餐 price.set.customer.alreadyBound=该客户已绑定套餐,一个客户只能绑定一个套餐
price.set.customer.masterNotFound=客户主数据不存在
price.set.customer.priority.invalid=优先级必须大于等于 1 price.set.customer.priority.invalid=优先级必须大于等于 1
# 重量段 PrcWeight # 重量段 PrcWeight
price.weight.range.empty=起始重量和结束重量不能为空 price.weight.range.empty=起始重量和结束重量不能为空
@@ -119,3 +120,8 @@ price.quote.hitCustomerFixedPackageDetail=命中客户套餐固定价格明细
# ---- 状态自动刷新 ---- # ---- 状态自动刷新 ----
price.status.refresh.success=状态刷新完成:bms_product 更新 {0} 行,bms_product_country 更新 {1} 行 price.status.refresh.success=状态刷新完成:bms_product 更新 {0} 行,bms_product_country 更新 {1} 行
# receivable bill
price.bill.orderNoRequired=订单号为空
price.bill.billIdRequired=计费ID为空
price.bill.notFound=计费记录不存在
price.bill.bizDateRequired=业务日期为空
@@ -15,7 +15,7 @@ public class TmsApiProperties
/** /**
* Agreed API identification code (header X-Api-Code or body apiCode). * Agreed API identification code (header X-Api-Code or body apiCode).
*/ */
private String apiCode = "BMS.TMS._Q2WnPXiXwGW2Dl-iSGNzLfrhUgQogNo"; private String apiCode = "BMS.TMS.ZYaOvlQzpcfau8TUaCFfO3Hp8ieHIqGw";
public String getApiCode() public String getApiCode()
{ {
@@ -0,0 +1,74 @@
package com.ruoyi.price.domain;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/** receivable bill header prc_bill */
@Data
@EqualsAndHashCode(callSuper = true)
public class PrcBill extends BaseEntity
{
private static final long serialVersionUID = 1L;
private String billId;
private String billKey;
private String orderNo;
private String trackingNo;
private Long customerId;
private String customerCode;
private String customerName;
private Integer pieces;
private BigDecimal actualWeight;
private BigDecimal volume;
private BigDecimal volumeWeight;
private BigDecimal pkgLength;
private BigDecimal pkgWidth;
private BigDecimal pkgHeight;
private String shipperName;
private String consigneeName;
private String destPostcode;
private String warehouseCode;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date orderTime;
private String orderRemark;
private String productCode;
private String productName;
private String countryCode;
private String countryName;
private BigDecimal weight;
@JsonFormat(pattern = "yyyy-MM-dd")
private Date bizDate;
private Long tmsId;
private String matchSource;
private String matchMessage;
private Long priceSetId;
private String priceSetCode;
private String priceSetName;
private String setType;
private Integer bindPriority;
private Long customerPricesetId;
private Long productCountryId;
private BigDecimal weightMin;
private BigDecimal weightMax;
private String weightLabel;
@JsonFormat(pattern = "yyyy-MM-dd")
private Date effectiveDate;
private BigDecimal chargeWeight;
private BigDecimal freightRate;
private BigDecimal baseFreightRate;
private BigDecimal discountPercent;
private BigDecimal registrationFee;
private BigDecimal freightAmount;
private BigDecimal surchargeAmount;
private BigDecimal payableTotal;
private String currency;
private String roundMode;
private List<PrcBillFee> fees;
}
@@ -0,0 +1,26 @@
package com.ruoyi.price.domain;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
/** receivable bill fee detail prc_bill_fee */
@Data
public class PrcBillFee implements Serializable
{
private static final long serialVersionUID = 1L;
private String feeId;
private String billId;
private String feeType;
private String feeName;
private Long scId;
private BigDecimal amount;
private String currency;
private String roundMode;
private String remark;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createdAt;
}
@@ -0,0 +1,46 @@
package com.ruoyi.price.domain.vo;
import java.math.BigDecimal;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
/** Sync order -> receivable bill. Quote fields plus order snapshot. */
@Data
@EqualsAndHashCode(callSuper = true)
public class TmsPriceBillRequest extends TmsPriceQuoteRequest
{
private static final long serialVersionUID = 1L;
@NotBlank(message = "orderNo is required")
private String orderNo;
private String trackingNo;
private String customerName;
private Integer pieces;
private BigDecimal actualWeight;
private BigDecimal volume;
private BigDecimal volumeWeight;
private BigDecimal pkgLength;
private BigDecimal pkgWidth;
private BigDecimal pkgHeight;
private String shipperName;
private String consigneeName;
private String destPostcode;
private String warehouseCode;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date orderTime;
private String orderRemark;
@JsonIgnore
@AssertTrue(message = "bizDate is required")
public boolean isBizDatePresent()
{
String bizDate = getBizDate();
return bizDate != null && !bizDate.trim().isEmpty();
}
}
@@ -53,6 +53,24 @@ public class TmsPriceQuoteResult implements Serializable
/** 费用明细:运费/挂号费各一行,附加费按具体名称各一行 */ /** 费用明细:运费/挂号费各一行,附加费按具体名称各一行 */
private List<TmsPriceQuoteFeeLine> feeDetails; private List<TmsPriceQuoteFeeLine> feeDetails;
private String quoteId; private String quoteId;
/** true=命中已落库试算 */ private String billId;
private String orderNo;
private String trackingNo;
private String customerCode;
private String customerName;
private Integer pieces;
private BigDecimal actualWeight;
private BigDecimal volume;
private BigDecimal volumeWeight;
private BigDecimal pkgLength;
private BigDecimal pkgWidth;
private BigDecimal pkgHeight;
private String shipperName;
private String consigneeName;
private String destPostcode;
private String warehouseCode;
private Date orderTime;
private String orderRemark;
/** true=hit stored quote */
private Boolean fromCache; private Boolean fromCache;
} }
@@ -0,0 +1,17 @@
package com.ruoyi.price.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.ruoyi.price.domain.PrcBillFee;
/** receivable bill fee detail mapper */
public interface PrcBillFeeMapper
{
public List<PrcBillFee> selectByBillId(String billId);
public int insertPrcBillFee(PrcBillFee fee);
public int insertBatch(@Param("list") List<PrcBillFee> fees);
public int deleteByBillId(String billId);
}
@@ -0,0 +1,24 @@
package com.ruoyi.price.mapper;
import java.math.BigDecimal;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.ruoyi.price.domain.PrcBill;
/** receivable bill header mapper */
public interface PrcBillMapper
{
public PrcBill selectPrcBillByOrderNo(String orderNo);
public PrcBill selectPrcBillById(String billId);
public List<PrcBill> selectPrcBillList(PrcBill bill);
public int insertPrcBill(PrcBill bill);
public int updatePrcBill(PrcBill bill);
public int updateBillAmounts(@Param("billId") String billId,
@Param("surchargeAmount") BigDecimal surchargeAmount,
@Param("payableTotal") BigDecimal payableTotal);
}
@@ -0,0 +1,18 @@
package com.ruoyi.price.service;
import java.util.List;
import com.ruoyi.price.domain.PrcBill;
import com.ruoyi.price.domain.vo.TmsPriceBillRequest;
import com.ruoyi.price.domain.vo.TmsPriceQuoteResult;
/** Receivable billing from synced orders. Same price match as quote, persist prc_bill. */
public interface IPrcBillService
{
TmsPriceQuoteResult bill(TmsPriceBillRequest request);
List<PrcBill> selectBillList(PrcBill query);
TmsPriceQuoteResult getBill(String billId);
TmsPriceQuoteResult getBillByOrderNo(String orderNo);
}
@@ -18,6 +18,9 @@ public interface ITmsPriceQuoteService
{ {
TmsPriceQuoteResult quote(TmsPriceQuoteRequest request); TmsPriceQuoteResult quote(TmsPriceQuoteRequest request);
/** Calc only, no prc_quote persist. Used by receivable billing. */
TmsPriceQuoteResult calculate(TmsPriceQuoteRequest request);
List<PrcQuote> selectQuoteList(PrcQuote query); List<PrcQuote> selectQuoteList(PrcQuote query);
TmsPriceQuoteResult getQuote(String quoteId); TmsPriceQuoteResult getQuote(String quoteId);
@@ -0,0 +1,291 @@
package com.ruoyi.price.service.impl;
import java.math.BigDecimal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.MessageUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.sign.Md5Utils;
import com.ruoyi.price.domain.PrcBill;
import com.ruoyi.price.domain.PrcBillFee;
import com.ruoyi.price.domain.vo.TmsPriceBillRequest;
import com.ruoyi.price.domain.vo.TmsPriceQuoteFeeLine;
import com.ruoyi.price.domain.vo.TmsPriceQuoteResult;
import com.ruoyi.price.mapper.PrcBillFeeMapper;
import com.ruoyi.price.mapper.PrcBillMapper;
import com.ruoyi.price.service.IPrcBillService;
import com.ruoyi.price.service.ITmsPriceQuoteService;
@Service
public class PrcBillServiceImpl implements IPrcBillService
{
@Autowired
private ITmsPriceQuoteService tmsPriceQuoteService;
@Autowired
private PrcBillMapper billMapper;
@Autowired
private PrcBillFeeMapper billFeeMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public TmsPriceQuoteResult bill(TmsPriceBillRequest request)
{
if (request == null || StringUtils.isEmpty(request.getOrderNo()))
{
throw new ServiceException(MessageUtils.message("price.bill.orderNoRequired"));
}
request.setOrderNo(request.getOrderNo().trim());
if (StringUtils.isEmpty(request.getBizDate()))
{
throw new ServiceException(MessageUtils.message("price.bill.bizDateRequired"));
}
Date bizDate = resolveBizDate(request.getBizDate());
BigDecimal weight = request.getWeight();
TmsPriceQuoteResult result = tmsPriceQuoteService.calculate(request);
persistBill(request, weight, bizDate, result);
return result;
}
@Override
public List<PrcBill> selectBillList(PrcBill query)
{
return billMapper.selectPrcBillList(query);
}
@Override
public TmsPriceQuoteResult getBill(String billId)
{
if (billId == null)
{
throw new ServiceException(MessageUtils.message("price.bill.billIdRequired"));
}
PrcBill bill = billMapper.selectPrcBillById(billId);
if (bill == null)
{
throw new ServiceException(MessageUtils.message("price.bill.notFound"));
}
return toResult(bill, billFeeMapper.selectByBillId(billId));
}
@Override
public TmsPriceQuoteResult getBillByOrderNo(String orderNo)
{
if (StringUtils.isEmpty(orderNo))
{
throw new ServiceException(MessageUtils.message("price.bill.orderNoRequired"));
}
PrcBill bill = billMapper.selectPrcBillByOrderNo(orderNo.trim());
if (bill == null)
{
throw new ServiceException(MessageUtils.message("price.bill.notFound"));
}
return toResult(bill, billFeeMapper.selectByBillId(bill.getBillId()));
}
private void persistBill(TmsPriceBillRequest request, BigDecimal weight, Date bizDate,
TmsPriceQuoteResult result)
{
String orderNo = request.getOrderNo();
String billKey = Md5Utils.hash(orderNo);
PrcBill existed = billMapper.selectPrcBillByOrderNo(orderNo);
PrcBill bill = existed == null ? new PrcBill() : existed;
if (existed == null)
{
bill.setBillId(UUID.randomUUID().toString());
}
bill.setBillKey(billKey);
bill.setOrderNo(orderNo);
bill.setCustomerId(request.getCustomerId() == null ? Long.valueOf(0L) : request.getCustomerId());
bill.setCustomerCode(request.getCustomerCode());
bill.setCustomerName(request.getCustomerName());
bill.setTrackingNo(request.getTrackingNo());
bill.setPieces(request.getPieces());
bill.setActualWeight(request.getActualWeight());
bill.setVolume(request.getVolume());
bill.setVolumeWeight(request.getVolumeWeight());
bill.setPkgLength(request.getPkgLength());
bill.setPkgWidth(request.getPkgWidth());
bill.setPkgHeight(request.getPkgHeight());
bill.setShipperName(request.getShipperName());
bill.setConsigneeName(request.getConsigneeName());
bill.setDestPostcode(request.getDestPostcode());
bill.setWarehouseCode(request.getWarehouseCode());
bill.setOrderTime(request.getOrderTime());
bill.setOrderRemark(request.getOrderRemark());
bill.setProductCode(result.getProductCode());
bill.setProductName(result.getProductName());
bill.setCountryCode(result.getCountryCode());
bill.setCountryName(result.getCountryName());
bill.setWeight(weight);
bill.setBizDate(bizDate);
bill.setTmsId(request.getTmsId() == null ? null : Long.valueOf(request.getTmsId()));
bill.setMatchSource(result.getMatchSource());
bill.setMatchMessage(result.getMatchMessage());
bill.setPriceSetId(result.getPriceSetId());
bill.setPriceSetCode(result.getPriceSetCode());
bill.setPriceSetName(result.getPriceSetName());
bill.setSetType(result.getSetType());
bill.setBindPriority(result.getBindPriority());
bill.setCustomerPricesetId(result.getCustomerPricesetId());
bill.setProductCountryId(result.getProductCountryId());
bill.setWeightMin(result.getWeightMin());
bill.setWeightMax(result.getWeightMax());
bill.setWeightLabel(result.getWeightLabel());
bill.setEffectiveDate(result.getEffectiveDate());
bill.setChargeWeight(result.getChargeWeight());
bill.setFreightRate(result.getFreightRate());
bill.setBaseFreightRate(result.getBaseFreightRate());
bill.setDiscountPercent(result.getDiscountPercent());
bill.setRegistrationFee(result.getRegistrationFee());
bill.setFreightAmount(result.getFreightAmount());
bill.setSurchargeAmount(result.getSurchargeAmount());
bill.setPayableTotal(result.getPayableTotal());
bill.setCurrency(result.getCurrency());
bill.setRoundMode(result.getRoundMode());
if (existed == null)
{
billMapper.insertPrcBill(bill);
}
else
{
billMapper.updatePrcBill(bill);
billFeeMapper.deleteByBillId(bill.getBillId());
}
List<PrcBillFee> fees = toFeeRows(bill.getBillId(), result);
if (!fees.isEmpty())
{
billFeeMapper.insertBatch(fees);
}
result.setBillId(bill.getBillId());
result.setOrderNo(orderNo);
result.setCustomerCode(request.getCustomerCode());
fillResultOrder(result, bill);
result.setFromCache(Boolean.valueOf(existed != null));
}
private List<PrcBillFee> toFeeRows(String billId, TmsPriceQuoteResult result)
{
List<PrcBillFee> fees = new ArrayList<PrcBillFee>();
if (result.getFeeDetails() == null)
{
return fees;
}
for (TmsPriceQuoteFeeLine line : result.getFeeDetails())
{
PrcBillFee fee = new PrcBillFee();
fee.setFeeId(UUID.randomUUID().toString());
fee.setBillId(billId);
fee.setFeeType(line.getFeeType());
fee.setFeeName(line.getFeeName());
fee.setScId(line.getScId() == null ? 0L : line.getScId());
fee.setAmount(line.getAmount() == null ? BigDecimal.ZERO : line.getAmount());
fee.setCurrency(line.getCurrency());
fee.setRoundMode(line.getRoundMode());
fee.setRemark(line.getRemark());
fees.add(fee);
}
return fees;
}
private TmsPriceQuoteResult toResult(PrcBill bill, List<PrcBillFee> fees)
{
TmsPriceQuoteResult r = new TmsPriceQuoteResult();
r.setBillId(bill.getBillId());
r.setOrderNo(bill.getOrderNo());
r.setCustomerCode(bill.getCustomerCode());
fillResultOrder(r, bill);
r.setFromCache(Boolean.TRUE);
r.setMatchSource(bill.getMatchSource());
r.setMatchMessage(bill.getMatchMessage());
r.setCustomerId(bill.getCustomerId());
r.setPriceSetId(bill.getPriceSetId());
r.setPriceSetCode(bill.getPriceSetCode());
r.setPriceSetName(bill.getPriceSetName());
r.setSetType(bill.getSetType());
r.setBindPriority(bill.getBindPriority());
r.setCustomerPricesetId(bill.getCustomerPricesetId());
r.setProductCode(bill.getProductCode());
r.setProductName(bill.getProductName());
r.setCountryCode(bill.getCountryCode());
r.setCountryName(bill.getCountryName());
r.setProductCountryId(bill.getProductCountryId());
r.setWeightMin(bill.getWeightMin());
r.setWeightMax(bill.getWeightMax());
r.setWeightLabel(bill.getWeightLabel());
r.setEffectiveDate(bill.getEffectiveDate());
r.setWeight(bill.getWeight());
r.setChargeWeight(bill.getChargeWeight());
r.setFreightRate(bill.getFreightRate());
r.setBaseFreightRate(bill.getBaseFreightRate());
r.setDiscountPercent(bill.getDiscountPercent());
r.setRegistrationFee(bill.getRegistrationFee());
r.setFreightAmount(bill.getFreightAmount());
r.setSurchargeAmount(bill.getSurchargeAmount());
r.setPayableTotal(bill.getPayableTotal());
r.setCurrency(bill.getCurrency());
r.setRoundMode(bill.getRoundMode());
List<TmsPriceQuoteFeeLine> lines = new ArrayList<TmsPriceQuoteFeeLine>();
if (fees != null)
{
for (PrcBillFee fee : fees)
{
TmsPriceQuoteFeeLine line = new TmsPriceQuoteFeeLine();
line.setFeeType(fee.getFeeType());
line.setFeeName(fee.getFeeName());
line.setScId(fee.getScId());
line.setAmount(fee.getAmount());
line.setCurrency(fee.getCurrency());
line.setRoundMode(fee.getRoundMode());
line.setRemark(fee.getRemark());
lines.add(line);
}
}
r.setFeeDetails(lines);
return r;
}
private void fillResultOrder(TmsPriceQuoteResult r, PrcBill bill)
{
r.setTrackingNo(bill.getTrackingNo());
r.setCustomerName(bill.getCustomerName());
r.setPieces(bill.getPieces());
r.setActualWeight(bill.getActualWeight());
r.setVolume(bill.getVolume());
r.setVolumeWeight(bill.getVolumeWeight());
r.setPkgLength(bill.getPkgLength());
r.setPkgWidth(bill.getPkgWidth());
r.setPkgHeight(bill.getPkgHeight());
r.setShipperName(bill.getShipperName());
r.setConsigneeName(bill.getConsigneeName());
r.setDestPostcode(bill.getDestPostcode());
r.setWarehouseCode(bill.getWarehouseCode());
r.setOrderTime(bill.getOrderTime());
r.setOrderRemark(bill.getOrderRemark());
}
private Date resolveBizDate(String bizDate)
{
if (StringUtils.isEmpty(bizDate))
{
throw new ServiceException(MessageUtils.message("price.bill.bizDateRequired"));
}
try
{
return new SimpleDateFormat("yyyy-MM-dd").parse(bizDate.trim());
}
catch (ParseException e)
{
throw new ServiceException(MessageUtils.message("price.quote.bizDateInvalid"));
}
}
}
@@ -113,6 +113,16 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
return result; return result;
} }
@Override
public TmsPriceQuoteResult calculate(TmsPriceQuoteRequest request)
{
validateRequest(request);
Date bizDate = resolveBizDate(request.getBizDate());
TmsPriceQuoteResult result = doQuote(request, bizDate);
applySurchargesAndFees(result);
return result;
}
@Override @Override
public List<PrcQuote> selectQuoteList(PrcQuote query) public List<PrcQuote> selectQuoteList(PrcQuote query)
{ {
@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.price.mapper.PrcBillFeeMapper">
<resultMap type="PrcBillFee" id="PrcBillFeeResult">
<id property="feeId" column="fee_id" />
<result property="billId" column="bill_id" />
<result property="feeType" column="fee_type" />
<result property="feeName" column="fee_name" />
<result property="scId" column="sc_id" />
<result property="amount" column="amount" />
<result property="currency" column="currency" />
<result property="roundMode" column="round_mode" />
<result property="remark" column="remark" />
<result property="createdAt" column="created_at" />
</resultMap>
<select id="selectByBillId" parameterType="String" resultMap="PrcBillFeeResult">
select fee_id, bill_id, fee_type, fee_name, sc_id, amount, currency, round_mode, remark, created_at
from prc_bill_fee
where bill_id = #{billId}
order by field(fee_type, 'BASIC_FREIGHT', 'REGISTRATION_FEE', 'SURCHARGE'), fee_id
</select>
<insert id="insertPrcBillFee" parameterType="PrcBillFee">
insert into prc_bill_fee (
fee_id, bill_id, fee_type, fee_name, sc_id, amount, currency, round_mode, remark, created_at
) values (
#{feeId}, #{billId}, #{feeType}, #{feeName}, #{scId}, #{amount}, #{currency}, #{roundMode}, #{remark}, sysdate()
)
</insert>
<insert id="insertBatch" parameterType="java.util.List">
insert into prc_bill_fee (
fee_id, bill_id, fee_type, fee_name, sc_id, amount, currency, round_mode, remark, created_at
) values
<foreach collection="list" item="item" separator=",">
(#{item.feeId}, #{item.billId}, #{item.feeType}, #{item.feeName}, #{item.scId}, #{item.amount},
#{item.currency}, #{item.roundMode}, #{item.remark}, sysdate())
</foreach>
</insert>
<delete id="deleteByBillId" parameterType="String">
delete from prc_bill_fee where bill_id = #{billId}
</delete>
</mapper>
@@ -0,0 +1,212 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.price.mapper.PrcBillMapper">
<resultMap type="PrcBill" id="PrcBillResult">
<id property="billId" column="bill_id" />
<result property="billKey" column="bill_key" />
<result property="orderNo" column="order_no" />
<result property="trackingNo" column="tracking_no" />
<result property="customerId" column="customer_id" />
<result property="customerCode" column="customer_code" />
<result property="customerName" column="customer_name" />
<result property="pieces" column="pieces" />
<result property="actualWeight" column="actual_weight" />
<result property="volume" column="volume" />
<result property="volumeWeight" column="volume_weight" />
<result property="pkgLength" column="pkg_length" />
<result property="pkgWidth" column="pkg_width" />
<result property="pkgHeight" column="pkg_height" />
<result property="shipperName" column="shipper_name" />
<result property="consigneeName" column="consignee_name" />
<result property="destPostcode" column="dest_postcode" />
<result property="warehouseCode" column="warehouse_code" />
<result property="orderTime" column="order_time" />
<result property="orderRemark" column="order_remark" />
<result property="productCode" column="product_code" />
<result property="productName" column="product_name" />
<result property="countryCode" column="country_code" />
<result property="countryName" column="country_name" />
<result property="weight" column="weight" />
<result property="bizDate" column="biz_date" />
<result property="tmsId" column="tms_id" />
<result property="matchSource" column="match_source" />
<result property="matchMessage" column="match_message" />
<result property="priceSetId" column="price_set_id" />
<result property="priceSetCode" column="price_set_code" />
<result property="priceSetName" column="price_set_name" />
<result property="setType" column="set_type" />
<result property="bindPriority" column="bind_priority" />
<result property="customerPricesetId" column="customer_priceset_id" />
<result property="productCountryId" column="product_country_id" />
<result property="weightMin" column="weight_min" />
<result property="weightMax" column="weight_max" />
<result property="weightLabel" column="weight_label" />
<result property="effectiveDate" column="effective_date" />
<result property="chargeWeight" column="charge_weight" />
<result property="freightRate" column="freight_rate" />
<result property="baseFreightRate" column="base_freight_rate" />
<result property="discountPercent" column="discount_percent" />
<result property="registrationFee" column="registration_fee" />
<result property="freightAmount" column="freight_amount" />
<result property="surchargeAmount" column="surcharge_amount" />
<result property="payableTotal" column="payable_total" />
<result property="currency" column="currency" />
<result property="roundMode" column="round_mode" />
<result property="createBy" column="created_by" />
<result property="createTime" column="created_at" />
<result property="updateBy" column="updated_by" />
<result property="updateTime" column="updated_at" />
</resultMap>
<sql id="selectPrcBillVo">
select bill_id, bill_key, order_no, tracking_no, customer_id, customer_code, customer_name,
pieces, actual_weight, volume, volume_weight, pkg_length, pkg_width, pkg_height,
shipper_name, consignee_name, dest_postcode, warehouse_code, order_time, order_remark,
product_code, product_name, country_code, country_name, weight, biz_date, tms_id,
match_source, match_message, price_set_id, price_set_code, price_set_name,
set_type, bind_priority, customer_priceset_id, product_country_id,
weight_min, weight_max, weight_label, effective_date, charge_weight,
freight_rate, base_freight_rate, discount_percent, registration_fee,
freight_amount, surcharge_amount, payable_total, currency, round_mode,
created_by, created_at, updated_by, updated_at
from prc_bill
</sql>
<select id="selectPrcBillByOrderNo" parameterType="String" resultMap="PrcBillResult">
<include refid="selectPrcBillVo"/>
where order_no = #{orderNo}
</select>
<select id="selectPrcBillById" parameterType="String" resultMap="PrcBillResult">
<include refid="selectPrcBillVo"/>
where bill_id = #{billId}
</select>
<select id="selectPrcBillList" parameterType="PrcBill" resultMap="PrcBillResult">
<include refid="selectPrcBillVo"/>
<where>
<if test="orderNo != null and orderNo != ''">
AND order_no like concat('%', #{orderNo}, '%')
</if>
<if test="trackingNo != null and trackingNo != ''">
AND tracking_no like concat('%', #{trackingNo}, '%')
</if>
<if test="customerCode != null and customerCode != ''">
AND customer_code like concat('%', #{customerCode}, '%')
</if>
<if test="customerName != null and customerName != ''">
AND customer_name like concat('%', #{customerName}, '%')
</if>
<if test="productCode != null and productCode != ''">
AND product_code like concat('%', #{productCode}, '%')
</if>
<if test="countryCode != null and countryCode != ''">
AND country_code = #{countryCode}
</if>
<if test="matchSource != null and matchSource != ''">
AND match_source = #{matchSource}
</if>
<if test="bizDate != null">
AND biz_date = #{bizDate}
</if>
<if test="params.beginTime != null and params.beginTime != ''">
AND date_format(created_at,'%y%m%d') &gt;= date_format(#{params.beginTime},'%y%m%d')
</if>
<if test="params.endTime != null and params.endTime != ''">
AND date_format(created_at,'%y%m%d') &lt;= date_format(#{params.endTime},'%y%m%d')
</if>
</where>
order by created_at desc, bill_id desc
</select>
<insert id="insertPrcBill" parameterType="PrcBill">
insert into prc_bill (
bill_id, bill_key, order_no, tracking_no, customer_id, customer_code, customer_name,
pieces, actual_weight, volume, volume_weight, pkg_length, pkg_width, pkg_height,
shipper_name, consignee_name, dest_postcode, warehouse_code, order_time, order_remark,
product_code, product_name, country_code, country_name, weight, biz_date, tms_id,
match_source, match_message, price_set_id, price_set_code, price_set_name,
set_type, bind_priority, customer_priceset_id, product_country_id,
weight_min, weight_max, weight_label, effective_date, charge_weight,
freight_rate, base_freight_rate, discount_percent, registration_fee,
freight_amount, surcharge_amount, payable_total, currency, round_mode,
created_at
) values (
#{billId}, #{billKey}, #{orderNo}, #{trackingNo}, #{customerId}, #{customerCode}, #{customerName},
#{pieces}, #{actualWeight}, #{volume}, #{volumeWeight}, #{pkgLength}, #{pkgWidth}, #{pkgHeight},
#{shipperName}, #{consigneeName}, #{destPostcode}, #{warehouseCode}, #{orderTime}, #{orderRemark},
#{productCode}, #{productName}, #{countryCode}, #{countryName}, #{weight}, #{bizDate}, #{tmsId},
#{matchSource}, #{matchMessage}, #{priceSetId}, #{priceSetCode}, #{priceSetName},
#{setType}, #{bindPriority}, #{customerPricesetId}, #{productCountryId},
#{weightMin}, #{weightMax}, #{weightLabel}, #{effectiveDate}, #{chargeWeight},
#{freightRate}, #{baseFreightRate}, #{discountPercent}, #{registrationFee},
#{freightAmount}, #{surchargeAmount}, #{payableTotal}, #{currency}, #{roundMode},
sysdate()
)
</insert>
<update id="updatePrcBill" parameterType="PrcBill">
update prc_bill
set bill_key = #{billKey},
tracking_no = #{trackingNo},
customer_id = #{customerId},
customer_code = #{customerCode},
customer_name = #{customerName},
pieces = #{pieces},
actual_weight = #{actualWeight},
volume = #{volume},
volume_weight = #{volumeWeight},
pkg_length = #{pkgLength},
pkg_width = #{pkgWidth},
pkg_height = #{pkgHeight},
shipper_name = #{shipperName},
consignee_name = #{consigneeName},
dest_postcode = #{destPostcode},
warehouse_code = #{warehouseCode},
order_time = #{orderTime},
order_remark = #{orderRemark},
product_code = #{productCode},
product_name = #{productName},
country_code = #{countryCode},
country_name = #{countryName},
weight = #{weight},
biz_date = #{bizDate},
tms_id = #{tmsId},
match_source = #{matchSource},
match_message = #{matchMessage},
price_set_id = #{priceSetId},
price_set_code = #{priceSetCode},
price_set_name = #{priceSetName},
set_type = #{setType},
bind_priority = #{bindPriority},
customer_priceset_id = #{customerPricesetId},
product_country_id = #{productCountryId},
weight_min = #{weightMin},
weight_max = #{weightMax},
weight_label = #{weightLabel},
effective_date = #{effectiveDate},
charge_weight = #{chargeWeight},
freight_rate = #{freightRate},
base_freight_rate = #{baseFreightRate},
discount_percent = #{discountPercent},
registration_fee = #{registrationFee},
freight_amount = #{freightAmount},
surcharge_amount = #{surchargeAmount},
payable_total = #{payableTotal},
currency = #{currency},
round_mode = #{roundMode},
updated_at = sysdate()
where bill_id = #{billId}
</update>
<update id="updateBillAmounts">
update prc_bill
set surcharge_amount = #{surchargeAmount},
payable_total = #{payableTotal},
updated_at = sysdate()
where bill_id = #{billId}
</update>
</mapper>
+82
View File
@@ -0,0 +1,82 @@
-- prc_bill / prc_bill_fee: receivable billing from synced orders
-- fields follow prc_quote / prc_quote_fee; extra order_no + customer_code
SET NAMES utf8mb4;
CREATE TABLE IF NOT EXISTS prc_bill (
bill_id VARCHAR(36) NOT NULL COMMENT '计费ID',
bill_key CHAR(32) NOT NULL COMMENT 'MD5(order_no)',
order_no VARCHAR(64) NOT NULL COMMENT '订单号',
tracking_no VARCHAR(64) DEFAULT NULL COMMENT '运单号',
customer_id BIGINT(20) NOT NULL DEFAULT 0 COMMENT '客户ID',
customer_code VARCHAR(50) DEFAULT NULL COMMENT '客户代码',
customer_name VARCHAR(100) DEFAULT NULL COMMENT '客户名称',
pieces INT(11) DEFAULT NULL COMMENT '件数',
actual_weight DECIMAL(10,3) DEFAULT NULL COMMENT '实际重量KG',
volume DECIMAL(12,4) DEFAULT NULL COMMENT '体积m3',
volume_weight DECIMAL(10,3) DEFAULT NULL COMMENT '材积重KG',
pkg_length DECIMAL(10,2) DEFAULT NULL COMMENT '长CM',
pkg_width DECIMAL(10,2) DEFAULT NULL COMMENT '宽CM',
pkg_height DECIMAL(10,2) DEFAULT NULL COMMENT '高CM',
shipper_name VARCHAR(100) DEFAULT NULL COMMENT '发件人',
consignee_name VARCHAR(100) DEFAULT NULL COMMENT '收件人',
dest_postcode VARCHAR(20) DEFAULT NULL COMMENT '目的邮编',
warehouse_code VARCHAR(50) DEFAULT NULL COMMENT '仓库',
order_time DATETIME DEFAULT NULL COMMENT '下单时间',
order_remark VARCHAR(500) DEFAULT NULL COMMENT '订单备注',
product_code VARCHAR(20) NOT NULL COMMENT '产品代码',
product_name VARCHAR(100) DEFAULT NULL COMMENT '产品名称',
country_code CHAR(2) DEFAULT NULL COMMENT '国家二字码',
country_name VARCHAR(50) DEFAULT NULL COMMENT '国家名称',
weight DECIMAL(10,3) NOT NULL COMMENT '计费重量KG',
biz_date DATE NOT NULL COMMENT '计费日期',
tms_id BIGINT(20) DEFAULT NULL COMMENT '多租户系统ID',
match_source VARCHAR(30) DEFAULT NULL COMMENT '命中来源',
match_message VARCHAR(200) DEFAULT NULL COMMENT '命中说明',
price_set_id BIGINT(20) DEFAULT NULL COMMENT '价格套餐ID',
price_set_code VARCHAR(20) DEFAULT NULL COMMENT '套餐编码',
price_set_name VARCHAR(50) DEFAULT NULL COMMENT '套餐名称',
set_type VARCHAR(10) DEFAULT NULL COMMENT '计价类型',
bind_priority INT(11) DEFAULT NULL COMMENT '客户套餐优先级',
customer_priceset_id BIGINT(20) DEFAULT NULL COMMENT '客户套餐绑定ID',
product_country_id BIGINT(20) DEFAULT NULL COMMENT '关联 bms_product_country.detail_id',
weight_min DECIMAL(10,4) DEFAULT NULL COMMENT '重量段下限',
weight_max DECIMAL(10,4) DEFAULT NULL COMMENT '重量段上限',
weight_label VARCHAR(50) DEFAULT NULL COMMENT '重量段展示',
effective_date DATE DEFAULT NULL COMMENT '产品明细生效日',
charge_weight DECIMAL(10,3) DEFAULT NULL COMMENT '计费重量',
freight_rate DECIMAL(10,4) DEFAULT NULL COMMENT '运费单价',
base_freight_rate DECIMAL(10,4) DEFAULT NULL COMMENT '折扣前单价',
discount_percent DECIMAL(5,2) DEFAULT NULL COMMENT '折扣百分数',
registration_fee DECIMAL(10,2) DEFAULT NULL COMMENT '挂号费',
freight_amount DECIMAL(10,2) DEFAULT NULL COMMENT '运费金额',
surcharge_amount DECIMAL(10,2) DEFAULT NULL COMMENT '附加费合计',
payable_total DECIMAL(10,2) DEFAULT NULL COMMENT '应收总额',
currency VARCHAR(10) NOT NULL DEFAULT 'CNY' COMMENT '币种',
round_mode VARCHAR(20) NOT NULL DEFAULT 'HALF_UP' COMMENT '取值方式',
created_by VARCHAR(64) DEFAULT '' COMMENT '创建人',
created_at DATETIME DEFAULT NULL COMMENT '创建时间',
updated_by VARCHAR(64) DEFAULT '' COMMENT '更新人',
updated_at DATETIME DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (bill_id),
UNIQUE KEY uk_bill_order (order_no),
UNIQUE KEY uk_bill_key (bill_key),
INDEX idx_bill_customer (customer_code, biz_date),
INDEX idx_bill_product (product_code, country_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='计费应收主表-订单同步计费';
CREATE TABLE IF NOT EXISTS prc_bill_fee (
fee_id VARCHAR(36) NOT NULL COMMENT '费用明细ID',
bill_id VARCHAR(36) NOT NULL COMMENT '计费ID',
fee_type VARCHAR(30) NOT NULL COMMENT '费用类型(字典bms_fee_type)',
fee_name VARCHAR(100) DEFAULT NULL COMMENT '费用名称',
sc_id BIGINT(20) NOT NULL DEFAULT 0 COMMENT '附加费配置ID',
amount DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT '金额',
currency VARCHAR(10) NOT NULL DEFAULT 'CNY' COMMENT '币种',
round_mode VARCHAR(20) DEFAULT 'HALF_UP' COMMENT '取值方式',
remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
created_at DATETIME DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (fee_id),
UNIQUE KEY uk_bill_fee_item (bill_id, fee_type, sc_id),
INDEX idx_bill_fee_type (fee_type),
CONSTRAINT fk_bill_fee_bill FOREIGN KEY (bill_id) REFERENCES prc_bill (bill_id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='计费应收费用明细';
+23
View File
@@ -0,0 +1,23 @@
SET NAMES utf8mb4;
-- visible C menu under billing center (2300)
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
SELECT 2310, N'同步计费', 2300, 2, 'bill', 'price/billing/bill', '', 'BillingBill', 1, 0, 'C', '0', '0', 'price:billing:bill', 'list', 'admin', sysdate(), N'订单同步计费应收'
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2310);
UPDATE sys_menu SET menu_name = N'同步计费', parent_id = 2300, order_num = 2, path = 'bill', component = 'price/billing/bill', route_name = 'BillingBill', menu_type = 'C', visible = '0', perms = 'price:billing:bill', remark = N'订单同步计费应收'
WHERE menu_id = 2310;
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
SELECT 2311, N'计费查询', 2310, 1, '', NULL, 1, 0, 'F', '0', '0', 'price:billing:bill', '#', 'admin', sysdate()
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2311);
UPDATE sys_menu SET menu_name = N'计费查询', parent_id = 2310, menu_type = 'F', path = '', component = NULL, perms = 'price:billing:bill' WHERE menu_id = 2311;
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
SELECT 2312, N'计费详情', 2310, 2, '', NULL, 1, 0, 'F', '0', '0', 'price:billing:bill', '#', 'admin', sysdate()
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2312);
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT 1, m.menu_id FROM (SELECT 2310 AS menu_id UNION SELECT 2311 UNION SELECT 2312) m
WHERE NOT EXISTS (SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = 1 AND rm.menu_id = m.menu_id);
@@ -0,0 +1,21 @@
-- prc_bill add order snapshot columns
SET NAMES utf8mb4;
ALTER TABLE prc_bill
ADD COLUMN tracking_no VARCHAR(64) DEFAULT NULL COMMENT '运单号' AFTER order_no,
ADD COLUMN customer_name VARCHAR(100) DEFAULT NULL COMMENT '客户名称' AFTER customer_code,
ADD COLUMN pieces INT(11) DEFAULT NULL COMMENT '件数' AFTER customer_name,
ADD COLUMN actual_weight DECIMAL(10,3) DEFAULT NULL COMMENT '实际重量KG' AFTER pieces,
ADD COLUMN volume DECIMAL(12,4) DEFAULT NULL COMMENT '体积m3' AFTER actual_weight,
ADD COLUMN volume_weight DECIMAL(10,3) DEFAULT NULL COMMENT '材积重KG' AFTER volume,
ADD COLUMN pkg_length DECIMAL(10,2) DEFAULT NULL COMMENT '长CM' AFTER volume_weight,
ADD COLUMN pkg_width DECIMAL(10,2) DEFAULT NULL COMMENT '宽CM' AFTER pkg_length,
ADD COLUMN pkg_height DECIMAL(10,2) DEFAULT NULL COMMENT '高CM' AFTER pkg_width,
ADD COLUMN shipper_name VARCHAR(100) DEFAULT NULL COMMENT '发件人' AFTER pkg_height,
ADD COLUMN consignee_name VARCHAR(100) DEFAULT NULL COMMENT '收件人' AFTER shipper_name,
ADD COLUMN dest_postcode VARCHAR(20) DEFAULT NULL COMMENT '目的邮编' AFTER consignee_name,
ADD COLUMN warehouse_code VARCHAR(50) DEFAULT NULL COMMENT '仓库' AFTER dest_postcode,
ADD COLUMN order_time DATETIME DEFAULT NULL COMMENT '下单时间' AFTER warehouse_code,
ADD COLUMN order_remark VARCHAR(500) DEFAULT NULL COMMENT '订单备注' AFTER order_time;
ALTER TABLE prc_bill ADD INDEX idx_bill_tracking (tracking_no);
+26
View File
@@ -25,3 +25,29 @@ export function getQuote(quoteId) {
method: 'get' method: 'get'
}) })
} }
/** Sync order receivable bill */
export function billOrder(data) {
return request({
url: '/price/billing/bill',
method: 'post',
data
})
}
/** Receivable bill list */
export function listBill(query) {
return request({
url: '/price/billing/bill/list',
method: 'get',
params: query
})
}
/** Receivable bill detail */
export function getBill(billId) {
return request({
url: '/price/billing/bill/' + billId,
method: 'get'
})
}
+244
View File
@@ -0,0 +1,244 @@
<template>
<div class="app-container billing-bill">
<basic-table
ref="tableRef"
:columns="columns"
:search-columns="searchColumns"
:query="queryList"
row-key="billId"
>
<template #col-product="{ row }">
{{ row.productName || '-' }} ({{ row.productCode }})
</template>
<template #col-country="{ row }">
{{ row.countryName || '-' }} ({{ row.countryCode || '-' }})
</template>
<template #col-matchSource="{ row }">
<el-tag size="small" :type="sourceTagType(row.matchSource)">{{ sourceLabel(row.matchSource) }}</el-tag>
</template>
<template #col-payableTotal="{ row }">
<span class="payable">{{ formatMoney(row.payableTotal) }}</span>
</template>
<template #col-action="{ row }">
<el-button link type="primary" icon="View" @click="handleView(row)">查看</el-button>
</template>
</basic-table>
<el-drawer v-model="open" size="780px" :title="title" append-to-body>
<div v-loading="loading">
<el-empty v-if="!detail" description="暂无详情" />
<div v-else class="bill-detail">
<div class="sec-title">订单信息</div>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="订单号">{{ detail.orderNo || '-' }}</el-descriptions-item>
<el-descriptions-item label="运单号">{{ detail.trackingNo || '-' }}</el-descriptions-item>
<el-descriptions-item label="客户">{{ displayCustomer(detail) }}</el-descriptions-item>
<el-descriptions-item label="仓库">{{ detail.warehouseCode || '-' }}</el-descriptions-item>
<el-descriptions-item label="发件人">{{ detail.shipperName || '-' }}</el-descriptions-item>
<el-descriptions-item label="收件人">{{ detail.consigneeName || '-' }}</el-descriptions-item>
<el-descriptions-item label="目的国/地区">{{ (detail.countryName || '-') + ' (' + (detail.countryCode || '-') + ')' }}</el-descriptions-item>
<el-descriptions-item label="目的邮编">{{ detail.destPostcode || '-' }}</el-descriptions-item>
<el-descriptions-item label="件数">{{ detail.pieces != null ? detail.pieces : '-' }}</el-descriptions-item>
<el-descriptions-item label="尺寸(cm)">{{ dimLabel(detail) }}</el-descriptions-item>
<el-descriptions-item label="实重(kg)">{{ formatMoney(detail.actualWeight) }}</el-descriptions-item>
<el-descriptions-item label="材积重(kg)">{{ formatMoney(detail.volumeWeight) }}</el-descriptions-item>
<el-descriptions-item label="体积(m³)">{{ formatMoney(detail.volume) }}</el-descriptions-item>
<el-descriptions-item label="下单时间">{{ formatDateTime(detail.orderTime) }}</el-descriptions-item>
<el-descriptions-item label="订单备注" :span="2">{{ detail.orderRemark || '-' }}</el-descriptions-item>
</el-descriptions>
<div class="sec-title">计费信息</div>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="产品">{{ (detail.productName || '-') + ' (' + (detail.productCode || '-') + ')' }}</el-descriptions-item>
<el-descriptions-item label="计费重量(kg)">{{ detail.chargeWeight ?? detail.weight ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="重量段">{{ detail.weightLabel || '-' }}</el-descriptions-item>
<el-descriptions-item label="计费日期">{{ formatDate(detail.bizDate) }}</el-descriptions-item>
<el-descriptions-item label="计价来源">
<el-tag size="small" :type="sourceTagType(detail.matchSource)">{{ sourceLabel(detail.matchSource) }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="套餐">{{ detail.priceSetName || '-' }} {{ detail.priceSetCode ? '(' + detail.priceSetCode + ')' : '' }}</el-descriptions-item>
<el-descriptions-item label="币种">
<dict-tag :options="bms_currency" :value="displayCurrency(detail.currency)" />
</el-descriptions-item>
<el-descriptions-item label="取值方式">{{ roundModeLabel(detail.roundMode) }}</el-descriptions-item>
<el-descriptions-item label="运费单价">{{ formatMoney(detail.freightRate) }}</el-descriptions-item>
<el-descriptions-item label="运费金额">{{ formatMoney(detail.freightAmount) }}</el-descriptions-item>
<el-descriptions-item label="挂号费">{{ formatMoney(detail.registrationFee) }}</el-descriptions-item>
<el-descriptions-item label="附加费">{{ formatMoney(detail.surchargeAmount) }}</el-descriptions-item>
<el-descriptions-item label="应收总额">
<span class="payable">{{ formatMoney(detail.payableTotal) }}</span>
</el-descriptions-item>
<el-descriptions-item label="命中说明" :span="2">{{ detail.matchMessage || '-' }}</el-descriptions-item>
</el-descriptions>
<el-table
v-if="detail.feeDetails && detail.feeDetails.length"
:data="detail.feeDetails"
border
size="small"
class="fee-table"
>
<el-table-column label="费用名称" min-width="140" show-overflow-tooltip>
<template #default="{ row }">{{ displayFeeName(row) }}</template>
</el-table-column>
<el-table-column label="费用类型" width="110" align="center">
<template #default="{ row }">{{ feeTypeLabel(row.feeType) || row.feeType }}</template>
</el-table-column>
<el-table-column label="金额" min-width="120" align="right">
<template #default="{ row }">{{ formatMoney(row.amount) }}</template>
</el-table-column>
<el-table-column label="币种" width="90" align="center">
<template #default="{ row }">
<dict-tag :options="bms_currency" :value="displayCurrency(row.currency)" />
</template>
</el-table-column>
<el-table-column label="说明" min-width="160" show-overflow-tooltip>
<template #default="{ row }">{{ row.remark || '-' }}</template>
</el-table-column>
</el-table>
</div>
</div>
</el-drawer>
</div>
</template>
<script setup name="BillingBill">
import { listBill, getBill } from '@/api/price/billing'
const { bms_fee_type, bms_currency } = useDict('bms_fee_type', 'bms_currency')
const tableRef = ref()
const open = ref(false)
const title = ref('计费详情')
const loading = ref(false)
const detail = ref(null)
const columns = [
{ label: '订单号', prop: 'orderNo', minWidth: 140, showOverflowTooltip: true },
{ label: '运单号', prop: 'trackingNo', minWidth: 140, showOverflowTooltip: true },
{ label: '客户', prop: 'customerName', minWidth: 140, showOverflowTooltip: true },
{ label: '客户代码', prop: 'customerCode', width: 120 },
{ label: '产品', prop: 'product', minWidth: 160, slot: 'product' },
{ label: '国家', prop: 'country', minWidth: 130, slot: 'country' },
{ label: '件数', prop: 'pieces', align: 'center', width: 70 },
{ label: '计费重', prop: 'chargeWeight', align: 'center', width: 90 },
{ label: '应收总额', prop: 'payableTotal', align: 'right', width: 120, slot: 'payableTotal' },
{ label: '计价来源', prop: 'matchSource', align: 'center', width: 130, slot: 'matchSource' },
{ label: '下单时间', prop: 'orderTime', align: 'center', width: 170, parseTime: true },
{ label: '操作', prop: 'action', align: 'center', width: 90, fixed: 'right', slot: 'action', showInToolbar: false }
]
const searchColumns = [
{ label: '订单号', prop: 'orderNo', component: 'input', placeholder: '订单号', width: '160px' },
{ label: '运单号', prop: 'trackingNo', component: 'input', placeholder: '运单号', width: '160px' },
{ label: '客户代码', prop: 'customerCode', component: 'input', placeholder: '客户代码', width: '140px' },
{ label: '产品代码', prop: 'productCode', component: 'input', placeholder: '产品代码', width: '140px' }
]
function queryList(params) {
return listBill(params)
}
function handleView(row) {
title.value = '计费详情 #' + (row.orderNo || row.billId)
open.value = true
loading.value = true
detail.value = null
getBill(row.billId).then(res => {
detail.value = res.data || null
}).finally(() => {
loading.value = false
})
}
function displayCustomer(row) {
if (row.customerName && row.customerCode) return row.customerName + ' (' + row.customerCode + ')'
return row.customerName || row.customerCode || row.customerId || '-'
}
function dimLabel(row) {
if (row.pkgLength == null && row.pkgWidth == null && row.pkgHeight == null) return '-'
return [row.pkgLength, row.pkgWidth, row.pkgHeight].map(v => v == null ? '-' : v).join(' × ')
}
function formatMoney(val) {
if (val === null || val === undefined || val === '') return '-'
const num = Number(val)
if (Number.isNaN(num)) return val
return num.toFixed(4)
}
function formatDate(val) {
if (!val) return '-'
const d = new Date(val)
if (Number.isNaN(d.getTime())) return val
return d.toISOString().slice(0, 10)
}
function formatDateTime(val) {
if (!val) return '-'
const d = new Date(val)
if (Number.isNaN(d.getTime())) return val
const pad = n => String(n).padStart(2, '0')
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds())
}
function sourceLabel(source) {
const map = {
PACKAGE_FIXED: '客户固定价套餐',
PACKAGE_DISCOUNT: '客户折扣套餐',
PRODUCT_DEFAULT: '产品默认价',
FIXED: '客户固定价套餐',
DISCOUNT: '客户折扣套餐'
}
return map[source] || source || '-'
}
function roundModeLabel(mode) {
const map = { UP: '向上取值', DOWN: '向下取整', HALF_UP: '四舍五入' }
return map[mode] || mode || '四舍五入'
}
function feeTypeLabel(type) {
const found = (bms_fee_type.value || []).find(d => d.value === type)
return found ? found.label : ''
}
function displayCurrency(v) {
if (!v || v === 'RMB' || v === 'rmb') return 'CNY'
return v
}
function displayFeeName(row) {
const typeLabel = feeTypeLabel(row.feeType)
const name = String(row.feeName || row.fee_name || '').trim()
const isSurcharge = row.feeType === 'SURCHARGE' || row.feeType === '附加费'
if (isSurcharge && name && name !== typeLabel && name !== 'SURCHARGE' && name !== '附加费') {
return name
}
return name || typeLabel || row.feeType || '-'
}
function sourceTagType(source) {
if (source === 'PACKAGE_FIXED' || source === 'FIXED') return 'success'
if (source === 'PACKAGE_DISCOUNT' || source === 'DISCOUNT') return 'warning'
if (source === 'PRODUCT_DEFAULT') return 'info'
return ''
}
</script>
<style scoped>
.billing-bill .payable {
color: #f56c6c;
font-weight: 600;
}
.billing-bill .sec-title {
margin: 12px 0 8px;
font-weight: 600;
}
.billing-bill .sec-title:first-child {
margin-top: 0;
}
.billing-bill .fee-table {
margin-top: 12px;
}
</style>