新增路线产品

This commit is contained in:
liuminghui
2026-11-06 13:39:26 +08:00
parent b4da248815
commit 6f342d424f
32 changed files with 1850 additions and 785 deletions
@@ -0,0 +1,96 @@
package com.bms.web.controller.masterdata;
import java.util.List;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
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.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.bms.common.annotation.Log;
import com.bms.common.core.controller.BaseController;
import com.bms.common.core.domain.AjaxResult;
import com.bms.common.core.page.TableDataInfo;
import com.bms.common.enums.BusinessType;
import com.bms.common.utils.poi.ExcelUtil;
import com.bms.masterdata.domain.LineProduct;
import com.bms.masterdata.service.ILineProductService;
/**
* 路线产品管理(srv_line_product)
*/
@RestController
@RequestMapping("/masterdata/lineproduct")
public class LineProductController extends BaseController
{
@Autowired
private ILineProductService lineProductService;
@PreAuthorize("@ss.hasPermi('data:lineProduct:list')")
@GetMapping("/list")
public TableDataInfo list(LineProduct query)
{
startPage();
List<LineProduct> list = lineProductService.selectLineProductList(query);
return getDataTable(list);
}
@PreAuthorize("@ss.hasPermi('data:lineProduct:export')")
@Log(title = "路线产品", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, LineProduct query)
{
List<LineProduct> list = lineProductService.selectLineProductList(query);
ExcelUtil<LineProduct> util = new ExcelUtil<LineProduct>(LineProduct.class);
util.exportExcel(response, list, "路线产品");
}
@PreAuthorize("@ss.hasPermi('data:lineProduct:query')")
@GetMapping("/{id}")
public AjaxResult getInfo(@PathVariable String id)
{
return success(lineProductService.selectLineProductById(id));
}
@PreAuthorize("@ss.hasPermi('data:lineProduct:add')")
@Log(title = "路线产品", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody LineProduct product)
{
String username = getUsername();
product.setCreateName(username);
product.setModifier(username);
return toAjax(lineProductService.insertLineProduct(product));
}
@PreAuthorize("@ss.hasPermi('data:lineProduct:edit')")
@Log(title = "路线产品", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody LineProduct product)
{
product.setModifier(getUsername());
return toAjax(lineProductService.updateLineProduct(product));
}
@PreAuthorize("@ss.hasPermi('data:lineProduct:edit')")
@Log(title = "路线产品", businessType = BusinessType.UPDATE)
@PutMapping("/changeStatus")
public AjaxResult changeStatus(@RequestBody LineProduct product)
{
product.setModifier(getUsername());
return toAjax(lineProductService.updateLineProductStatus(product));
}
@PreAuthorize("@ss.hasPermi('data:lineProduct:remove')")
@Log(title = "路线产品", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable String[] ids)
{
return toAjax(lineProductService.deleteLineProductByIds(ids));
}
}
@@ -1,6 +1,5 @@
package com.bms.web.controller.price;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import org.springframework.beans.factory.annotation.Autowired;
@@ -25,7 +24,6 @@ import com.bms.common.exception.ServiceException;
import com.bms.common.utils.file.FileUtils;
import com.bms.common.utils.poi.ExcelUtil;
import com.bms.price.domain.ChannelProductMap;
import com.bms.price.domain.PayFeeRuleGroup;
import com.bms.price.domain.PayProduct;
import com.bms.price.domain.PayProductTerm;
import com.bms.price.domain.PaySaveCommand;
@@ -72,35 +70,6 @@ public class PayProductController extends BaseController
return toAjax(payProductService.updateTerm(term));
}
/** 勾选的应付产品各自带出附加费开关。productIds 用逗号分隔 */
@PreAuthorize("@ss.hasPermi('price:pay:query')")
@GetMapping("/feeRule/list")
public AjaxResult feeRuleList(@RequestParam String productIds)
{
List<Long> ids = new ArrayList<>();
if (productIds != null)
{
for (String part : productIds.split(","))
{
if (part != null && !part.isBlank())
{
ids.add(Long.valueOf(part.trim()));
}
}
}
return success(payProductService.listFeeRules(ids));
}
/** 按产品保存附加费开关。多个产品各写一份 */
@PreAuthorize("@ss.hasPermi('price:pay:edit')")
@Log(title = "应付附加费开关", businessType = BusinessType.UPDATE)
@PutMapping("/feeRule")
public AjaxResult saveFeeRule(@RequestBody List<PayFeeRuleGroup> products)
{
payProductService.saveFeeRules(products);
return success();
}
@PreAuthorize("@ss.hasPermi('price:pay:edit')")
@Log(title = "应付有效期", businessType = BusinessType.UPDATE)
@PutMapping("/term/status")
@@ -200,10 +169,19 @@ public class PayProductController extends BaseController
@PreAuthorize("@ss.hasPermi('price:pay:import')")
@Log(title = "应付配置", businessType = BusinessType.IMPORT)
@PostMapping(value = "/preview", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public AjaxResult preview(MultipartFile file, @RequestParam(value = "dictValue", required = false) String dictValue) throws Exception
public AjaxResult preview(MultipartFile file, @RequestParam(value = "dictValue", required = false) String dictValue,
@RequestParam(value = "progressId", required = false) String progressId) throws Exception
{
assertWorkbook(file);
return success(payProductService.preview(file.getBytes(), file.getOriginalFilename(), dictValue));
return success(payProductService.preview(file.getBytes(), file.getOriginalFilename(), dictValue, progressId));
}
/** 导入弹框轮询解析进度。没有进度编号时返回等待中。 */
@PreAuthorize("@ss.hasPermi('price:pay:import')")
@GetMapping("/preview/progress")
public AjaxResult previewProgress(@RequestParam(value = "progressId", required = false) String progressId)
{
return success(payProductService.parseProgress(progressId));
}
/** 修改已导入的公共模板 */
@@ -26,6 +26,63 @@ class PayCostCalculatorTest
assertTrue(result.getSkipped().stream().anyMatch(text -> text.contains("签名费")));
}
@Test
void spainCttAddsPerKgAfterTheLastWeightBand()
{
PayProduct product = new PayProduct();
product.setProductName("西班牙CTT");
product.setCurrency("EUR");
product.setWeightBands(new ArrayList<>(List.of(
band("0", "1", "2.10", "Mainland / Mad High Density"),
band("4", "5", "3.18", "Mainland / Mad High Density"))));
PaySurcharge extra = fee(31L, "每公斤续重", "0.27", "EUR", "EXTRA_KG", "5");
extra.setZoneName("Mainland / Mad High Density");
extra.setPriceUnit("EUR/KG");
product.setSurcharges(List.of(extra));
PayOrderCostResult inside = PayCostCalculator.calculate(product, new BigDecimal("5"),
null, null, null, null, "Mainland / Mad High Density", null, 1);
assertTrue(hasFee(inside, "基础运费", "3.18"));
assertTrue(inside.getFees().stream().noneMatch(fee -> "每公斤续重".equals(fee.getFeeName())));
PayOrderCostResult over = PayCostCalculator.calculate(product, new BigDecimal("7"),
null, null, null, null, "Mainland / Mad High Density", null, 1);
assertTrue(hasFee(over, "基础运费", "3.18"));
assertTrue(hasFee(over, "每公斤续重", "0.54"));
assertEquals(new BigDecimal("3.72"), over.getPayTotal());
}
@Test
void extraKgFollowsTheSameServiceSection()
{
PayProduct product = new PayProduct();
product.setProductName("岛屿");
product.setCurrency("EUR");
PayWeightBand standard = band("0", "1", "13.12", "Canary Islands / Minor Islands");
standard.setSectionName("2. INSULAR RATES / STANDARD");
standard.setWeightLabel("Up to 1 kg");
PayWeightBand express = band("0", "1", "14.10", "Canary Islands / Minor Islands");
express.setSectionName("2. INSULAR RATES / EXPRESS");
express.setWeightLabel("Up to 1 kg");
product.setWeightBands(new ArrayList<>(List.of(standard, express)));
PaySurcharge standardKg = fee(41L, "每公斤续重", "1.86", "EUR", "EXTRA_KG", "1");
standardKg.setZoneName("Canary Islands / Minor Islands");
standardKg.setSectionName("2. INSULAR RATES / STANDARD");
standardKg.setPriceUnit("EUR/KG");
PaySurcharge expressKg = fee(42L, "每公斤续重", "4.72", "EUR", "EXTRA_KG", "1");
expressKg.setZoneName("Canary Islands / Minor Islands");
expressKg.setSectionName("2. INSULAR RATES / EXPRESS");
expressKg.setPriceUnit("EUR/KG");
product.setSurcharges(List.of(standardKg, expressKg));
PayOrderCostResult over = PayCostCalculator.calculate(product, new BigDecimal("2"),
null, null, null, null, "Canary Islands / Minor Islands", null, 1);
assertTrue(hasFee(over, "基础运费", "13.12"));
assertTrue(hasFee(over, "每公斤续重", "1.86"));
assertEquals(1, over.getFees().stream().filter(fee -> "每公斤续重".equals(fee.getFeeName())).count());
assertEquals(new BigDecimal("14.98"), over.getPayTotal());
}
@Test
void overweightOutsideTheCardDoesNotInventFreight()
{
@@ -9,6 +9,7 @@ import java.util.List;
import java.util.stream.Stream;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Test;
import com.bms.price.domain.PayOrderCostResult;
import com.bms.price.domain.PayPreview;
import com.bms.price.domain.PayProduct;
import com.bms.price.domain.PaySurcharge;
@@ -482,6 +483,10 @@ class PayWorkbookParserTest
void spainCttIslandsReadsFiveRateTables() throws Exception
{
Path file = Path.of("D:/payment/西班牙CTT带电专线(岛屿+其他地区).xlsx");
if (!Files.exists(file))
{
file = Path.of("D:/A-payment/西班牙CTT带电专线(岛屿+其他地区).xlsx");
}
Assumptions.assumeTrue(Files.exists(file));
PayPreview preview = parser.parseMatched(Files.readAllBytes(file), file.getFileName().toString(),
"西班牙CTT带电专线(岛屿+其他地区)", List.of("西班牙CTT带电专线(岛屿+其他地区)"));
@@ -491,45 +496,70 @@ class PayWorkbookParserTest
assertEquals("续重计费", ctt.getChargeType());
assertTrue(covers(ctt.getExtraPerKg(), "1.86"));
assertTrue(covers(ctt.getExtraPerKg(), "5.79"));
assertEquals(48, ctt.getWeightBands().size());
assertEquals(4, ctt.getSurcharges().size());
assertEquals(24, ctt.getWeightBands().size());
assertEquals(24, ctt.getSurcharges().stream().filter(fee -> "每公斤续重".equals(fee.getFeeName())).count());
assertEquals(4, ctt.getSurcharges().stream().filter(fee -> "Cubic Capacity".equals(fee.getFeeName())).count());
assertTrue(ctt.getWeightBands().stream().anyMatch(band -> "2. INSULAR RATES / STANDARD".equals(band.getSectionName())
&& "Balearic Islands Economy 48H / Majorca".equals(band.getZoneName()) && "西班牙".equals(band.getCountryName()) && "ES".equals(band.getCountryCode())
&& "Up to 1 kg".equals(band.getWeightLabel()) && decimal(band, "3.39") && "每件".equals(band.getChargeName())
&& "EUR/件".equals(band.getPriceUnit()) && band.getWeightMax().compareTo(java.math.BigDecimal.ONE) == 0));
assertTrue(ctt.getWeightBands().stream().anyMatch(band -> "2. INSULAR RATES / STANDARD".equals(band.getSectionName())
&& "Canary Islands / Minor Islands".equals(band.getZoneName()) && "Additional Kg".equals(band.getWeightLabel())
&& decimal(band, "1.86") && "每公斤".equals(band.getChargeName()) && band.getWeightMin() == null));
assertTrue(ctt.getWeightBands().stream().noneMatch(band -> band.getWeightLabel() != null && band.getWeightLabel().contains("Additional")));
assertTrue(ctt.getSurcharges().stream().anyMatch(fee -> "每公斤续重".equals(fee.getFeeName())
&& "2. INSULAR RATES / STANDARD".equals(fee.getSectionName())
&& "Canary Islands / Minor Islands".equals(fee.getZoneName()) && decimalFee(fee, "1.86")
&& "EUR".equals(fee.getCurrency()) && "EUR/KG".equals(fee.getPriceUnit())
&& "EXTRA_KG".equals(fee.getCalcRule()) && "1".equals(fee.getCalcParam())));
assertTrue(ctt.getWeightBands().stream().anyMatch(band -> "2. INSULAR RATES / EXPRESS".equals(band.getSectionName())
&& "Canary Islands / Mayor Islands".equals(band.getZoneName()) && "Up to 1 kg".equals(band.getWeightLabel())
&& decimal(band, "7.46")));
assertTrue(ctt.getWeightBands().stream().anyMatch(band -> "Price WITH CUSTOMS CLEARANCE / STANDARD".equals(band.getSectionName())
&& "Canary Islands (*) / Mayor Islands".equals(band.getZoneName()) && decimal(band, "5.33")));
assertTrue(ctt.getWeightBands().stream().anyMatch(band -> "Price WITH CUSTOMS CLEARANCE / EXPRESS".equals(band.getSectionName())
&& "Portuguese Island / Island".equals(band.getZoneName()) && "Additional Kg".equals(band.getWeightLabel())
&& decimal(band, "5.79")));
assertTrue(ctt.getSurcharges().stream().anyMatch(fee -> "每公斤续重".equals(fee.getFeeName())
&& "Price WITH CUSTOMS CLEARANCE / EXPRESS".equals(fee.getSectionName())
&& "Portuguese Island / Island".equals(fee.getZoneName()) && decimalFee(fee, "5.79")));
assertTrue(ctt.getWeightBands().stream().anyMatch(band -> band.getSectionName().contains("CEUTA")
&& "Ceuta".equals(band.getZoneName()) && "Up to 1 kg".equals(band.getWeightLabel()) && decimal(band, "7.61")));
assertTrue(ctt.getWeightBands().stream().anyMatch(band -> band.getSectionName().contains("CEUTA")
&& "Andorra".equals(band.getZoneName()) && "Additional Kg".equals(band.getWeightLabel()) && decimal(band, "2.85")));
assertTrue(ctt.getSurcharges().stream().allMatch(fee -> "Cubic Capacity".equals(fee.getFeeName()) && fee.getAmount() == null
&& "MANUAL".equals(fee.getCalcRule())));
assertTrue(ctt.getSurcharges().stream().anyMatch(fee -> "2. INSULAR RATES / STANDARD".equals(fee.getSectionName())
assertTrue(ctt.getSurcharges().stream().anyMatch(fee -> "每公斤续重".equals(fee.getFeeName())
&& fee.getSectionName().contains("CEUTA") && "Andorra".equals(fee.getZoneName()) && decimalFee(fee, "2.85")));
assertTrue(ctt.getSurcharges().stream().filter(fee -> "Cubic Capacity".equals(fee.getFeeName()))
.allMatch(fee -> fee.getAmount() == null && "MANUAL".equals(fee.getCalcRule())));
assertTrue(ctt.getSurcharges().stream().anyMatch(fee -> "Cubic Capacity".equals(fee.getFeeName())
&& "2. INSULAR RATES / STANDARD".equals(fee.getSectionName())
&& fee.getChargeText().contains("167 kg/m3") && fee.getChargeText().contains("333 kg/m3")
&& fee.getFeeDesc().contains("Balearic Islands Economy 48H / Majorca")
&& fee.getFeeDesc().contains("Canary Islands / Interislands")));
assertTrue(ctt.getSurcharges().stream().anyMatch(fee -> "2. INSULAR RATES / EXPRESS".equals(fee.getSectionName())
assertTrue(ctt.getSurcharges().stream().anyMatch(fee -> "Cubic Capacity".equals(fee.getFeeName())
&& "2. INSULAR RATES / EXPRESS".equals(fee.getSectionName())
&& fee.getFeeDesc().contains("Portuguese Island / Island")));
assertTrue(ctt.getSurcharges().stream().noneMatch(fee -> fee.getSectionName() != null && fee.getSectionName().contains("CEUTA")));
assertTrue(ctt.getSurcharges().stream().noneMatch(fee -> "Cubic Capacity".equals(fee.getFeeName())
&& fee.getSectionName() != null && fee.getSectionName().contains("CEUTA")));
assertTrue(ctt.getWeightBands().stream().noneMatch(band -> band.getZoneName() != null && band.getZoneName().contains("19E")));
assertTrue(ctt.getSurcharges().stream().noneMatch(fee -> fee.getFeeName() != null && (fee.getFeeName().contains("计费重量")
|| fee.getFeeName().contains("4.95") || fee.getFeeName().contains("退货"))));
PayOrderCostResult oneKg = PayCostCalculator.calculate(ctt, new java.math.BigDecimal("1"), null, null, null, null,
"Balearic Islands Economy 48H / Majorca", null, 1);
assertEquals(new java.math.BigDecimal("3.39"), oneKg.getPayTotal());
assertTrue(oneKg.getFees().stream().noneMatch(fee -> "每公斤续重".equals(fee.getFeeName())));
PayOrderCostResult twoKg = PayCostCalculator.calculate(ctt, new java.math.BigDecimal("2"), null, null, null, null,
"Balearic Islands Economy 48H / Majorca", null, 1);
assertEquals(new java.math.BigDecimal("4.24"), twoKg.getPayTotal());
assertEquals(1, twoKg.getFees().stream().filter(fee -> "每公斤续重".equals(fee.getFeeName())
&& fee.getAmount().compareTo(new java.math.BigDecimal("0.85")) == 0).count());
PayOrderCostResult island = PayCostCalculator.calculate(ctt, new java.math.BigDecimal("2"), null, null, null, null,
"Portuguese Island / Island", null, 1);
assertEquals(new java.math.BigDecimal("12.41"), island.getPayTotal());
assertEquals(1, island.getFees().stream().filter(fee -> "每公斤续重".equals(fee.getFeeName())
&& fee.getAmount().compareTo(new java.math.BigDecimal("3.34")) == 0).count());
}
@Test
void spainCttFillsCountryAndContinuedWeight() throws Exception
{
Path file = Path.of("D:/payment/西班牙CTT.xlsx");
if (!Files.exists(file))
{
file = Path.of("D:/A-payment/西班牙CTT.xlsx");
}
Assumptions.assumeTrue(Files.exists(file));
PayPreview preview = parser.parseMatched(Files.readAllBytes(file), file.getFileName().toString(), "西班牙CTT",
List.of("西班牙CTT"));
@@ -538,7 +568,7 @@ class PayWorkbookParserTest
assertEquals("西班牙", ctt.getCountryScope());
assertEquals("续重计费", ctt.getChargeType());
assertTrue(covers(ctt.getExtraPerKg(), "0.82"));
assertEquals(72, ctt.getWeightBands().size());
assertEquals(60, ctt.getWeightBands().size());
assertTrue(ctt.getWeightBands().stream().anyMatch(band -> "西班牙".equals(band.getCountryName()) && "ES".equals(band.getCountryCode())
&& "Mainland / Mad High Density".equals(band.getZoneName()) && "1kg".equals(band.getWeightLabel())
&& decimal(band, "2.1") && "每件".equals(band.getChargeName())
@@ -547,12 +577,19 @@ class PayWorkbookParserTest
assertTrue(ctt.getWeightBands().stream().anyMatch(band -> "Mainland / Portugal".equals(band.getZoneName())
&& "5kg".equals(band.getWeightLabel()) && decimal(band, "3.95")
&& band.getWeightMin().compareTo(new java.math.BigDecimal("4")) == 0));
assertTrue(ctt.getWeightBands().stream().anyMatch(band -> "Balearic Islands Economy 48H / Majorca".equals(band.getZoneName())
&& "Kg.ad.".equals(band.getWeightLabel()) && decimal(band, "0.82") && "每公斤".equals(band.getChargeName())
&& band.getWeightMin() == null));
assertTrue(ctt.getWeightBands().stream().noneMatch(band -> band.getWeightLabel() != null
&& (band.getWeightLabel().contains("Kg.ad") || band.getWeightLabel().contains("预估"))));
assertTrue(ctt.getWeightBands().stream().noneMatch(band -> "每公斤".equals(band.getChargeName())));
assertEquals(12, ctt.getSurcharges().stream().filter(fee -> "每公斤续重".equals(fee.getFeeName())).count());
assertTrue(ctt.getSurcharges().stream().anyMatch(fee -> "每公斤续重".equals(fee.getFeeName())
&& "Balearic Islands Economy 48H / Majorca".equals(fee.getZoneName()) && decimalFee(fee, "0.82")
&& "EUR".equals(fee.getCurrency()) && "EUR/KG".equals(fee.getPriceUnit())
&& "EXTRA_KG".equals(fee.getCalcRule()) && "5".equals(fee.getCalcParam())
&& fee.getFeeDesc().contains("超过5kg")));
assertTrue(ctt.getSurcharges().stream().anyMatch(fee -> "每公斤续重".equals(fee.getFeeName())
&& "Canary Islands / Minor Islands".equals(fee.getZoneName()) && decimalFee(fee, "1.83")));
assertTrue(ctt.getWeightBands().stream().anyMatch(band -> "Canary Islands / Minor Islands".equals(band.getZoneName())
&& "1kg".equals(band.getWeightLabel()) && decimal(band, "13.11")));
assertTrue(ctt.getWeightBands().stream().noneMatch(band -> band.getWeightLabel() != null && band.getWeightLabel().contains("预估")));
}
@Test